Skip to content

Commit f35486f

Browse files
authored
Bugfix MCCP2 decompression, very large NEW_ENVIRON, and --tls-auto (jquast#138)
* bugfix: MCCP2 decompression failed on MUD servers using raw deflate or gzip-wrapped compression, producing garbled banners. The client now auto-detects zlib/gzip format and falls back to raw deflate when needed. * bugfix: ``NEW_ENVIRON SEND`` requests that exceed the 256-byte subnegotiation buffer of some telnet clients (e.g. GNU inetutils) are now automatically split into multiple SB frames. * bugfix: ``telnetlib3-server`` argument ``--tls-auto`` deadlocked with plain telnet clients. Detection now uses a non-blocking with a configurable timeout. * enhancement: ``telnetlib3-fingerprint-server`` integrates with the optional ``tv-detect`` package for terminal vulnerability probing.
1 parent fc3791a commit f35486f

24 files changed

Lines changed: 2567 additions & 239 deletions

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: jquast

bin/moderate_fingerprints.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ def _alarm_handler(signum, frame):
127127

128128

129129
def _format_banner(banner_data):
130-
"""Return (clean_text, raw_display) from a banner dict."""
130+
"""Return (clean_text, raw_display) from a banner dict or string."""
131+
if isinstance(banner_data, str):
132+
return banner_data, ""
131133
text = banner_data.get("text", "")
132134
raw_hex = banner_data.get("raw_hex", "")
133135
if _HAS_WCWIDTH and text:

docs/guidebook.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,28 @@ Or programmatically::
369369

370370
For production, use certificates from Let's Encrypt or another trusted CA.
371371

372+
**Mixed TLS / plain telnet (auto-detect)**
373+
374+
Add ``--tls-auto`` to accept both TLS and plain telnet clients on the same
375+
port. The server peeks at the first byte of each connection: a TLS
376+
ClientHello (``0x16``) upgrades to TLS; any other byte is processed as plain telnet. An optional
377+
timeout::
378+
379+
telnetlib3-server --ssl-certfile cert.pem --ssl-keyfile key.pem \
380+
--tls-auto --line-mode --shell bin.server_mud.shell 0.0.0.0 6023
381+
382+
# custom timeout (seconds to wait for TLS ClientHello)
383+
telnetlib3-server --ssl-certfile cert.pem --ssl-keyfile key.pem \
384+
--tls-auto=2.0 0.0.0.0 6023
385+
386+
TLS clients send ClientHello immediately on connect, so they should be detected
387+
quickly under the default timeout of 500ms, though very slow networks might suggest to increase the
388+
default timeout value by specifying time in seconds as float, eg. ``--tls-auto=2.0``.
389+
390+
RFC-compliant telnet clients wait for the server to send the first bytes, so they will appear to
391+
stall during this timeout unless they initiate telnet negotiation or any client input (other than
392+
Ctrl+V!) is received.
393+
372394
**Client-side**
373395

374396
Connect to a server with a CA-signed certificate (e.g. ``dunemud.net``)::

docs/history.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
History
22
=======
3+
4.0.2 (unreleased)
4+
* bugfix: MCCP2 decompression failed on MUD servers using raw deflate or gzip-wrapped compression,
5+
producing garbled banners. The client now auto-detects zlib/gzip format and falls back to raw
6+
deflate when needed.
7+
* bugfix: ``NEW_ENVIRON SEND`` requests that exceed the 256-byte subnegotiation buffer of some
8+
telnet clients (e.g. GNU inetutils) are now automatically split into multiple SB frames.
9+
* bugfix: ``telnetlib3-server`` argument ``--tls-auto`` deadlocked with plain telnet clients.
10+
Detection now uses a non-blocking with a configurable timeout.
11+
* enhancement: ``telnetlib3-fingerprint-server`` integrates with the optional ``tv-detect``
12+
package for terminal vulnerability probing.
13+
314
4.0.1
415
* new: ``--encoding=big5bbs``, BBS 半形字 (half-width characters) encoding, matching PCMan/PttBBS
516
terminal clients, popular with Taiwanese BBS culture.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "telnetlib3"
7-
version = "4.0.1" # Keep in sync with telnetlib3/accessories.py::get_version !
7+
version = "4.0.2" # Keep in sync with telnetlib3/accessories.py::get_version !
88
description = " Python Telnet server and client CLI and Protocol library"
99
readme = "README.rst"
1010
license = "ISC"
@@ -55,7 +55,7 @@ docs = [
5555
"sphinx-autodoc-typehints",
5656
]
5757
extras = [
58-
"prettytable",
58+
"prettytable>=3.17,<4",
5959
"ucs-detect>=2,<3",
6060
]
6161

telnetlib3/accessories.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242

4343
def get_version() -> str:
4444
"""Return the current version of telnetlib3."""
45-
return "4.0.1" # keep in sync with pyproject.toml !
45+
return "4.0.2" # keep in sync with pyproject.toml !
4646

4747

4848
def encoding_from_lang(lang: str) -> Optional[str]:
@@ -124,9 +124,7 @@ def hexdump(data: bytes, prefix: str = "") -> str:
124124
return "\n".join(lines)
125125

126126

127-
_DEFAULT_LOGFMT = " ".join(
128-
("%(asctime)s", "%(levelname)s", "%(filename)s:%(lineno)d", "%(message)s")
129-
)
127+
_DEFAULT_LOGFMT = " ".join(("%(levelname)s", "%(filename)s:%(lineno)d", "%(message)s"))
130128

131129

132130
def make_logger(

telnetlib3/client_base.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969

7070
# MCCP2: server→client decompression
7171
self._mccp2_decompressor: Optional[zlib._Decompress] = None
72+
self._mccp2_wbits_fallback: bool = False
7273
# MCCP3: client→server compression
7374
self._mccp3_compressor: Optional[zlib._Compress] = None
7475
self._mccp3_orig_write: Any = None
@@ -102,6 +103,7 @@ def connection_lost(self, exc: Optional[Exception]) -> None:
102103

103104
# Clean up MCCP compressors/decompressors
104105
self._mccp2_decompressor = None
106+
self._mccp2_wbits_fallback = False
105107
self._mccp3_compressor = None
106108
self._mccp3_orig_write = None
107109

@@ -360,9 +362,20 @@ def _process_chunk(self, data: bytes) -> bool:
360362
try:
361363
data = self._mccp2_decompressor.decompress(data)
362364
except zlib.error:
363-
self.log.warning("MCCP2 decompression error, disabling")
364-
self._mccp2_end()
365-
return False
365+
if not self._mccp2_wbits_fallback:
366+
self.log.debug("MCCP2 auto-detect failed, retrying raw deflate")
367+
self._mccp2_wbits_fallback = True
368+
self._mccp2_decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
369+
try:
370+
data = self._mccp2_decompressor.decompress(data)
371+
except zlib.error:
372+
self.log.warning("MCCP2 decompression error, disabling")
373+
self._mccp2_end()
374+
return False
375+
else:
376+
self.log.warning("MCCP2 decompression error, disabling")
377+
self._mccp2_end()
378+
return False
366379
if self._mccp2_decompressor.eof:
367380
unused = self._mccp2_decompressor.unused_data
368381
self._mccp2_end()
@@ -444,7 +457,8 @@ async def _process_rx(self) -> None:
444457

445458
def _mccp2_start(self) -> None:
446459
"""Start MCCP2 decompression of server→client data."""
447-
self._mccp2_decompressor = zlib.decompressobj()
460+
self._mccp2_decompressor = zlib.decompressobj(wbits=zlib.MAX_WBITS | 32)
461+
self._mccp2_wbits_fallback = False
448462
self.log.debug("MCCP2 decompression started (server→client)")
449463

450464
def _mccp2_end(self) -> None:

0 commit comments

Comments
 (0)