Skip to content

Commit 4467132

Browse files
committed
Broadcast TLS information in demo banners with API
1 parent 76390f2 commit 4467132

9 files changed

Lines changed: 98 additions & 2 deletions

File tree

bin/server_mud.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,12 @@ async def shell(reader: Any, writer: Any) -> None:
763763
writer.iac(WILL, MSSP)
764764
writer.set_ext_callback(GMCP, lambda pkg, data: on_gmcp(writer, pkg, data))
765765
writer.set_ext_callback(MSDP, lambda variables: on_msdp(writer, variables))
766-
writer.write("Welcome to the Mini-MUD!\r\n")
766+
ssl_obj = writer.get_extra_info("ssl_object")
767+
if ssl_obj is not None:
768+
version = ssl_obj.version() or "TLS"
769+
writer.write(f"Welcome to the Mini-MUD! ({version} Secured)\r\n")
770+
else:
771+
writer.write("Welcome to the Mini-MUD!\r\n")
767772

768773
env = writer.get_extra_info("USER") or writer.get_extra_info("LOGNAME") or ""
769774
if env:

bin/server_tls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323

2424
async def shell(reader, writer):
2525
"""Simple echo shell over TLS."""
26-
writer.write("Welcome to the TLS echo server!\r\n")
26+
ssl_obj = writer.get_extra_info("ssl_object")
27+
if ssl_obj is not None:
28+
version = ssl_obj.version() or "TLS"
29+
writer.write(f"Welcome to the TLS echo server! ({version} Secured)\r\n")
30+
else:
31+
writer.write("Welcome to the echo server!\r\n")
2732
await writer.drain()
2833

2934
while True:

docs/history.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ History
88
telnet clients (e.g. GNU inetutils) are now automatically split into multiple SB frames.
99
* bugfix: ``telnetlib3-server`` argument ``--tls-auto`` deadlocked with plain telnet clients.
1010
Detection now uses a non-blocking with a configurable timeout.
11+
* enhancement: ``writer.get_extra_info("ssl_object")`` is available in shell callbacks to detect
12+
TLS-secured connections and query the negotiated protocol version and cipher.
1113
* enhancement: ``telnetlib3-fingerprint-server`` integrates with the optional ``tv-detect``
1214
package for terminal vulnerability probing.
1315

telnetlib3/_base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,15 @@ def get_extra_info(self, name: str, default: Any = None) -> Any:
125125
if self._transport:
126126
default = self._transport.get_extra_info(name, default)
127127
return self._extra.get(name, default)
128+
129+
def _log_tls_info(self, logger: logging.Logger) -> None:
130+
"""Log TLS connection details at debug level, if TLS is active."""
131+
ssl_obj = self.get_extra_info("ssl_object")
132+
if ssl_obj is None:
133+
return
134+
version = getattr(ssl_obj, "version", lambda: None)() or "TLS"
135+
cipher_info = getattr(ssl_obj, "cipher", lambda: None)()
136+
if cipher_info:
137+
logger.debug("TLS handshake: %s cipher=%s", version, cipher_info[0])
138+
else:
139+
logger.debug("TLS handshake: %s", version)

telnetlib3/client_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None:
186186
)
187187

188188
self.log.info("Connected to %s", self)
189+
self._log_tls_info(self.log)
189190

190191
self._waiter_connected.add_done_callback(self.begin_shell)
191192
asyncio.get_event_loop().call_soon(self.begin_negotiation)

telnetlib3/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,9 @@ async def _upgrade_to_tls(self) -> None:
853853
self._transport.close()
854854
return
855855
assert ssl_transport is not None
856+
logger.debug(
857+
"tls-auto: TLS handshake succeeded for %s", self._transport.get_extra_info("peername")
858+
)
856859
protocol.connection_made(ssl_transport)
857860

858861
def _handoff_plain(self) -> None:

telnetlib3/server_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None:
171171
)
172172

173173
logger.info("Connection from %s", self)
174+
self._log_tls_info(logger)
174175

175176
self._waiter_connected.add_done_callback(self.begin_shell)
176177
asyncio.get_event_loop().call_soon(self.begin_negotiation)

telnetlib3/tests/test_server.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,41 @@ def close(self):
459459
data = b"plain text no iac"
460460
server.data_received(data)
461461
assert bytes(server.reader._buffer).endswith(data)
462+
463+
464+
@pytest.mark.parametrize(
465+
"ssl_obj,expect_log",
466+
[
467+
pytest.param(None, False, id="no_tls"),
468+
pytest.param(
469+
type(
470+
"SSL",
471+
(),
472+
{"version": lambda self: "TLSv1.3", "cipher": lambda self: ("AES", "TLSv1.3", 256)},
473+
)(),
474+
True,
475+
id="tls_with_cipher",
476+
),
477+
pytest.param(
478+
type("SSL", (), {"version": lambda self: None, "cipher": lambda self: None})(),
479+
True,
480+
id="tls_no_cipher",
481+
),
482+
],
483+
)
484+
def test_log_tls_info(ssl_obj, expect_log, caplog):
485+
from telnetlib3._base import TelnetProtocolBase
486+
487+
proto = TelnetProtocolBase.__new__(TelnetProtocolBase)
488+
proto._extra = {}
489+
proto._transport = MagicMock()
490+
proto._transport.get_extra_info = lambda name, default=None: (
491+
ssl_obj if name == "ssl_object" else default
492+
)
493+
log = logging.getLogger("test_tls_info")
494+
with caplog.at_level(logging.DEBUG, logger="test_tls_info"):
495+
proto._log_tls_info(log)
496+
tls_msgs = [r for r in caplog.records if "TLS handshake" in r.message]
497+
assert bool(tls_msgs) == expect_log
498+
if ssl_obj and ssl_obj.cipher():
499+
assert "AES" in tls_msgs[0].message

telnetlib3/tests/test_tls.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,3 +725,32 @@ def test_cli_run_fingerprint_client_ssl(
725725
ca.cert_pem.write_to_path(ca_pem)
726726
extra_argv = extra_argv_factory(ca_pem)
727727
_pty_run_fingerprint(bind_host, unused_tcp_port, extra_argv, server_ssl_ctx)
728+
729+
730+
@_start_tls_xfail
731+
@_start_tls_timeout
732+
async def test_tls_handshake_logged(
733+
bind_host, unused_tcp_port, server_ssl_ctx, client_ssl_ctx, caplog
734+
):
735+
"""Successful TLS handshake logs version and cipher at debug level."""
736+
shell, waiter = _echo_shell("hi", "ok")
737+
import logging
738+
739+
with caplog.at_level(logging.DEBUG):
740+
async with create_server(
741+
host=bind_host, port=unused_tcp_port, shell=shell, ssl=server_ssl_ctx
742+
):
743+
async with open_connection(
744+
bind_host,
745+
unused_tcp_port,
746+
ssl=client_ssl_ctx,
747+
server_hostname="localhost",
748+
**_FAST_CLIENT,
749+
) as (reader, writer):
750+
writer.write("hi")
751+
await writer.drain()
752+
await asyncio.wait_for(waiter, 2.0)
753+
754+
tls_logs = [r for r in caplog.records if "TLS handshake" in r.message]
755+
assert len(tls_logs) >= 1
756+
assert any("TLSv1" in r.message for r in tls_logs)

0 commit comments

Comments
 (0)