Guidebook
This guide provides examples for using telnetlib3 to build telnet servers
and clients. All examples are available in the bin/ directory of the
repository.
Most examples are shell callbacks – async functions that receive a
(reader, writer) pair. You do not need a standalone script to run them;
just point telnetlib3-server --shell= (or telnetlib3-client --shell=)
at the callback:
telnetlib3-server --shell=bin.server_wargame.shell
These examples are not distributed with the package – they are only available in the github repository. You can retrieve them by cloning the repository, or downloading the “raw” file link.
Asyncio Interface
The primary interface for telnetlib3 uses Python’s asyncio library for asynchronous I/O. This allows handling many concurrent connections efficiently in a single process or thread.
Server Examples
server_wargame.py
https://github.com/jquast/telnetlib3/blob/master/bin/server_wargame.py
A minimal shell callback that asks a simple question and responds based on user input.
async def shell(reader, writer):
"""Handle a single client connection."""
writer.write("\r\nWould you like to play a game? ")
inp = await reader.read(1)
if inp:
writer.echo(inp)
writer.write("\r\nThey say the only way to win is to not play at all.\r\n")
await writer.drain()
writer.close()
Run with:
telnetlib3-server --shell=bin.server_wargame.shell
Then connect with:
telnet localhost 6023
server_wait_for_client.py
https://github.com/jquast/telnetlib3/blob/master/bin/server_wait_for_client.py
Demonstrates direct use of create_server() and the
wait_for_client() API for accessing client
protocols without using a shell callback. This is a standalone script because
it needs server-level control that cannot be expressed as a --shell=
callback.
# local
import telnetlib3
async def main():
"""Start server and wait for clients."""
server = await telnetlib3.create_server(host="127.0.0.1", port=6023)
print("Server running on localhost:6023")
print("Connect with: telnet localhost 6023")
while True:
print("Waiting for client...")
client = await server.wait_for_client()
print("Client connected!")
# Access negotiated terminal information
term = client.get_extra_info("TERM") or "unknown"
cols = client.get_extra_info("cols") or 80
rows = client.get_extra_info("rows") or 24
print(f"Terminal: {term}")
print(f"Window size: {cols}x{rows}")
# Send welcome message
client.writer.write(f"\r\nWelcome! Your terminal is {term} ({cols}x{rows})\r\n")
server_broadcast.py
https://github.com/jquast/telnetlib3/blob/master/bin/server_broadcast.py
A chat-style server that broadcasts messages from one client to all others.
This is a standalone script because it uses
wait_for_client() and
clients shared state. Demonstrates:
Using
clientsto access all connected protocolsHandling multiple clients with asyncio tasks
Using
wait_for()to check negotiation states
async def handle_client(server, client, client_id):
"""Handle a single client, broadcasting their input to all others."""
client.writer.write(f"\r\nYou are client #{client_id}\r\n")
client.writer.write("Type messages to broadcast (Ctrl+] to disconnect)\r\n\r\n")
# Wait for BINARY mode if available
try:
await asyncio.wait_for(client.writer.wait_for(remote={"BINARY": True}), timeout=2.0)
except asyncio.TimeoutError:
pass # Continue without BINARY mode
while True:
data = await client.reader.read(1024)
if not data:
break
# Broadcast to all other clients
message = f"[Client #{client_id}]: {data}"
for other in server.clients:
if other is not client:
other.writer.write(message)
# Notify others of disconnect
for other in server.clients:
server_wait_for_negotiation.py
https://github.com/jquast/telnetlib3/blob/master/bin/server_wait_for_negotiation.py
A shell callback demonstrating wait_for() to
await specific telnet option negotiation states before proceeding. This is useful when your
application depends on certain terminal capabilities being negotiated.
The server waits for:
NAWS (Negotiate About Window Size) - window dimensions
TTYPE (Terminal Type) - terminal identification
BINARY mode - 8-bit clean transmission
async def shell(_reader, writer):
"""Handle client with explicit negotiation waits."""
writer.write("\r\nWaiting for terminal negotiation...\r\n")
# Wait for NAWS, TTYPE, and BINARY negotiation to complete
try:
await asyncio.wait_for(
writer.wait_for(
local={"NAWS": True, "BINARY": True},
remote={"BINARY": True},
pending={"TTYPE": False},
),
timeout=1.5,
)
cols = writer.get_extra_info("cols")
rows = writer.get_extra_info("rows")
term = writer.get_extra_info("TERM")
writer.write(f"Window size: {cols}x{rows}\r\n")
writer.write(f"Terminal type: {term}\r\n")
writer.write("Binary mode enabled (bidirectional)\r\n")
except asyncio.TimeoutError:
writer.write("Negotiation timed out\r\n")
writer.write("\r\nNegotiation complete. Goodbye!\r\n")
await writer.drain()
writer.close()
Run with:
telnetlib3-server --shell=bin.server_wait_for_negotiation.shell
Client Examples
client_wargame.py
https://github.com/jquast/telnetlib3/blob/master/bin/client_wargame.py
A shell callback that connects to a server and automatically answers questions. Demonstrates the client shell callback pattern.
async def shell(reader, writer):
"""Handle client session, auto-answering questions."""
while True:
outp = await reader.read(1024)
if not outp:
break
if "?" in outp:
writer.write("y\r\n")
print(outp, flush=True, end="")
print()
Run with:
telnetlib3-client --shell=bin.client_wargame.shell localhost 6023
Server API Reference
The create_server() function returns a
Server instance with these key methods and
properties:
wait_for_client()
wait_for_client() waits for a client to
connect and complete initial negotiation:
server = await telnetlib3.create_server(port=6023)
client = await server.wait_for_client()
client.writer.write("Welcome!\r\n")
clients
The clients property provides access to all
currently connected client protocols:
# Broadcast to all clients
for client in server.clients:
client.writer.write("Server announcement\r\n")
wait_for()
wait_for() waits for specific
telnet option negotiation states:
# Wait for BINARY mode
await asyncio.wait_for(
client.writer.wait_for(remote={"BINARY": True}),
timeout=5.0
)
# Wait for terminal type negotiation to complete
await asyncio.wait_for(
client.writer.wait_for(pending={"TTYPE": False}),
timeout=5.0
)
The method accepts these keyword arguments:
remote: Dict of options to wait for inremote_option(client WILL)local: Dict of options to wait for inlocal_option(client DO)pending: Dict of options to wait for inpending_option
Option names are strings: "BINARY", "ECHO", "NAWS", "TTYPE", etc.
wait_for_condition()
The wait_for_condition() method
waits for a custom condition:
from telnetlib3.telopt import ECHO
await client.writer.wait_for_condition(
lambda w: w.mode == "kludge" and w.remote_option.enabled(ECHO)
)
Encoding and Binary Mode
By default, telnetlib3 uses encoding="utf8", which means the shell
callback receives TelnetReaderUnicode and
TelnetWriterUnicode.
These work with Python str – you read and write strings:
async def shell(reader, writer):
writer.write("Hello, world!\r\n") # str
data = await reader.read(1) # returns str
To work with raw bytes instead, pass encoding=False to
create_server() or open_connection().
The shell then receives TelnetReader and
TelnetWriter, which work with bytes:
async def binary_shell(reader, writer):
writer.write(b"Hello, world!\r\n") # bytes
data = await reader.read(1) # returns bytes
await telnetlib3.create_server(
host="127.0.0.1", port=6023,
shell=binary_shell, encoding=False
)
Binary mode is useful for specific low-level conditions, like performing xmodem transfers, or working with legacy systems that predate unicode and utf-8 support.
The same applies to clients –
open_connection(..., encoding=False)
returns a (TelnetReader,
TelnetWriter) pair that works with
bytes.
Retro BBS Encodings
telnetlib3 includes custom codecs for retro computing platforms commonly found on telnet BBS systems:
ATASCII (
--encoding=atascii) – Atari 8-bit computers (400, 800, XL, XE). Graphics characters at 0x00-0x1F, card suits, box drawing, and an inverse-video range at 0x80-0xFF. The ATASCII end-of-line character (0x9B) maps to newline. Aliases:atari8bit,atari_8bit.PETSCII (
--encoding=petscii) – Commodore 64/128 shifted (lowercase) mode. Lowercase a-z at 0x41-0x5A, uppercase A-Z at 0xC1-0xDA. Aliases:cbm,commodore,c64,c128.Atari ST (
--encoding=atarist) – Atari ST character set with extended Latin, Greek, and math symbols. Alias:atari.
These encodings use bytes 0x80-0xFF for standard glyphs, which conflicts
with the telnet protocol’s default 7-bit NVT mode. When any of these
encodings is selected, --force-binary is automatically enabled so that
high-bit bytes are transmitted without requiring BINARY option negotiation.
PETSCII inline color codes are translated to ANSI 24-bit RGB using the VIC-II C64 palette, and cursor control codes (up/down/left/right, HOME, CLR, DEL) are translated to ANSI sequences. ATASCII control character glyphs (cursor movement, backspace, clear screen) are similarly translated.
Keyboard input is also mapped: arrow keys, backspace, delete, and enter produce the correct raw bytes for each encoding:
telnetlib3-client --encoding=atascii area52.tk 5200
telnetlib3-client --encoding=petscii bbs.example.com 6400
telnetlib3-fingerprint decodes and translates banners with these
encodings, including PETSCII colors.
SyncTERM Font Detection
When a server sends a SyncTERM/CTerm font selection sequence
(CSI Ps1 ; Ps2 SP D), both telnetlib3-client and
telnetlib3-fingerprint automatically switch the session encoding
to match the font (e.g. font 36 = ATASCII, 32-35 = PETSCII, 0 = CP437).
An explicit --encoding flag takes precedence over font detection.
Line Endings
The telnet protocol (RFC 854) requires \r\n (CR LF) as the line ending
for all NVT (Network Virtual Terminal) output. This applies in all standard
modes:
NVT ASCII mode (default):
\r\nis required.Kludge mode (SGA negotiated, no LINEMODE): input is character-at-a-time, but server output is still NVT –
\r\nis expected.Binary mode (TRANSMIT-BINARY): raw bytes, no NVT transformation –
\nis acceptable if both sides agree.
The write() method on both the asyncio and blocking interfaces sends data
as-is – it does not convert \n to \r\n:
# Correct:
writer.write("Hello!\r\n")
# Wrong -- most clients will not display a proper line break:
writer.write("Hello!\n")
For maximum compatibility with MUD clients, legacy terminals, and standard
telnet implementations, always use \r\n with write().
Raw Mode and Line Mode
By default telnetlib3-client matches the terminal’s mode by the
server’s stated telnet negotiation. It starts in line mode (local echo,
line buffering) and switches dynamically depending on server:
Nothing: line mode with local echo
WILL ECHO+WILL SGA: kludge mode (raw, no local echo)WILL ECHO: raw mode, server echoesWILL SGA: character-at-a-time with local echo
Use --raw-mode to force raw mode (no line buffering, no local echo),
which is needed for some legacy BBS systems that don’t negotiate WILL
ECHO. This is set true when --encoding=petscii or atascii.
Conversely, Use --line-mode to force line-buffered input with local echo.
Similarly, telnetlib3-server --pty-exec defaults to raw PTY mode
(disabling PTY echo), which is correct for programs that handle their own
terminal I/O (bash, curses, etc.). Use --line-mode for programs
that expect cooked/canonical PTY mode:
# Default: raw PTY (correct for curses programs)
telnetlib3-server --pty-exec /bin/bash -- --login
# Line mode: cooked PTY with echo (for simple programs like bc)
telnetlib3-server --pty-exec /bin/bc --line-mode
Debugging
Use --loglevel=trace to see hexdump-style output of all bytes sent
and received on the wire:
telnetlib3-client --loglevel=trace --logfile=debug.log bbs.example.com
server_binary.py
https://github.com/jquast/telnetlib3/blob/master/bin/server_binary.py
A shell callback that echoes client input as hex bytes.
Demonstrates using encoding=False on create_server()
for raw byte I/O.
async def shell(reader, writer):
"""Echo client input back as hex bytes."""
writer.write(b"[binary echo server] type something:\r\n")
await writer.drain()
data = await reader.read(128)
if data:
hex_str = " ".join(f"{b:02x}" for b in data)
writer.write(f"hex: {hex_str}\r\n".encode("ascii"))
await writer.drain()
writer.close()
Run with:
telnetlib3-server --encoding=false --shell=bin.server_binary.shell
TLS / SSL
Telnet over TLS (TELNETS, IANA port 992) secures the connection using standard TLS encryption. The TLS handshake is handled at the transport layer, the telnet protocol sees plaintext exactly as it would over plain TCP. This is not STARTTLS (upgrade-in-place); the connection is encrypted from the start.
Server-side
Generate a self-signed certificate for testing:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
-days 365 -nodes -subj '/CN=localhost'
Run a TLS server from the CLI:
telnetlib3-server --ssl-certfile cert.pem --ssl-keyfile key.pem 0.0.0.0 6023
Or programmatically:
import ssl
import telnetlib3
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("cert.pem", keyfile="key.pem")
server = await telnetlib3.create_server(host="0.0.0.0", port=6023, shell=shell, ssl=ctx)
For production, use certificates from Let’s Encrypt or another trusted CA.
Mixed TLS / plain telnet (auto-detect)
Add --tls-auto to accept both TLS and plain telnet clients on the same
port. The server peeks at the first byte of each connection: a TLS
ClientHello (0x16) upgrades to TLS; any other byte is processed as plain telnet. An optional
timeout:
telnetlib3-server --ssl-certfile cert.pem --ssl-keyfile key.pem \
--tls-auto --line-mode --shell bin.server_mud.shell 0.0.0.0 6023
# custom timeout (seconds to wait for TLS ClientHello)
telnetlib3-server --ssl-certfile cert.pem --ssl-keyfile key.pem \
--tls-auto=2.0 0.0.0.0 6023
TLS clients send ClientHello immediately on connect, so they should be detected
quickly under the default timeout of 500ms, though very slow networks might suggest to increase the
default timeout value by specifying time in seconds as float, eg. --tls-auto=2.0.
RFC-compliant telnet clients wait for the server to send the first bytes, so they will appear to stall during this timeout unless they initiate telnet negotiation or any client input (other than Ctrl+V!) is received.
Client-side
Connect to a server with a CA-signed certificate (e.g. dunemud.net):
telnetlib3-client --ssl dunemud.net 6788
The system CA store is used automatically, just like curl or a browser.
Connect to a server with a self-signed certificate:
telnetlib3-client --ssl --ssl-cafile cert.pem localhost 6023
Or programmatically with full control:
import ssl
import telnetlib3
# CA-signed server, just pass ssl=True
reader, writer = await telnetlib3.open_connection("dunemud.net", 6788, ssl=True)
# Self-signed, load the server's cert explicitly
ctx = ssl.create_default_context(cafile="cert.pem")
reader, writer = await telnetlib3.open_connection("localhost", 6023, ssl=ctx)
Fingerprinting TLS servers:
telnetlib3-fingerprint --ssl dunemud.net 6788
telnetlib3-fingerprint --ssl --ssl-cafile cert.pem localhost 6023
To skip certificate verification (e.g. for servers with self-signed or expired certificates):
telnetlib3-client --ssl-no-verify example.com 6023
telnetlib3-fingerprint --ssl-no-verify example.com 6023
Warning
--ssl-no-verify is insecure. The connection is encrypted, but the
server’s identity is not verified, a man-in-the-middle could intercept
traffic. Only use this for testing or when you trust the network path.
server_tls.py
https://github.com/jquast/telnetlib3/blob/master/bin/server_tls.py
A TLS-encrypted echo shell callback. Demonstrates the ssl= parameter on
create_server().
async def shell(reader, writer):
"""Simple echo shell over TLS."""
ssl_obj = writer.get_extra_info("ssl_object")
if ssl_obj is not None:
version = ssl_obj.version() or "TLS"
writer.write(f"Welcome to the TLS echo server! ({version} Secured)\r\n")
else:
writer.write("Welcome to the echo server!\r\n")
await writer.drain()
while True:
data = await reader.read(256)
if not data:
Run with:
telnetlib3-server --ssl-certfile cert.pem --ssl-keyfile key.pem \
--shell=bin.server_tls.shell
Blocking Interface
Asyncio can be complex or unnecessary for many applications. For these cases,
telnetlib3 provides a blocking (synchronous) interface via telnetlib3.sync.
The asyncio event loop runs in a background thread, exposing familiar blocking
methods.
Client Usage
The TelnetConnection class provides a blocking client
interface:
from telnetlib3.sync import TelnetConnection
# Using context manager (recommended)
with TelnetConnection('localhost', 6023) as conn:
conn.write('hello\r\n')
response = conn.readline()
print(response)
# Manual lifecycle
conn = TelnetConnection('localhost', 6023, encoding='utf8')
conn.connect()
try:
conn.write('command\r\n')
data = conn.read_until(b'>>> ')
print(data)
finally:
conn.close()
Server Usage
The BlockingTelnetServer class provides a blocking
server interface with thread-per-connection handling:
from telnetlib3.sync import BlockingTelnetServer
def handle_client(conn):
"""Called in a new thread for each client."""
conn.write('Welcome!\r\n')
while True:
line = conn.readline(timeout=60)
if not line or line.strip() in ('quit', b'quit'):
break
conn.write(f'Echo: {line}')
conn.close()
# Simple: auto-spawns thread per client
server = BlockingTelnetServer('localhost', 6023, handler=handle_client)
server.serve_forever()
Or with a manual accept loop for custom threading strategies:
import threading
server = BlockingTelnetServer('localhost', 6023)
server.start()
while True:
conn = server.accept()
threading.Thread(target=handle_client, args=(conn,)).start()
Blocking Server Example
blocking_echo_server.py
https://github.com/jquast/telnetlib3/blob/master/bin/blocking_echo_server.py
A traditional threaded echo server using BlockingTelnetServer.
Each client connection runs in its own thread.
# local
from telnetlib3.sync import BlockingTelnetServer
def handle_client(conn):
"""Handle a single client connection (runs in its own thread)."""
conn.write("Welcome! Type messages and I'll echo them back.\r\n")
conn.write("Type 'quit' to disconnect.\r\n\r\n")
conn.flush()
while True:
try:
line = conn.readline(timeout=300) # 5 minute timeout
if not line:
break
line = line.strip()
if line.lower() == "quit":
conn.write("Goodbye!\r\n")
conn.flush()
break
conn.write(f"Echo: {line}\r\n")
conn.flush()
except TimeoutError:
conn.write("\r\nTimeout - disconnecting.\r\n")
conn.flush()
break
conn.close()
Run the server:
python bin/blocking_echo_server.py
Blocking Client Example
blocking_client.py
https://github.com/jquast/telnetlib3/blob/master/bin/blocking_client.py
A traditional blocking telnet client using TelnetConnection.
# std imports
import sys
# local
from telnetlib3.sync import TelnetConnection
def main():
"""Connect to a telnet server and interact."""
host = sys.argv[1] if len(sys.argv) > 1 else "localhost"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 6023
print(f"Connecting to {host}:{port}...")
with TelnetConnection(host, port, timeout=10) as conn:
print(f"Connected to {host}:{port}")
# Read initial server greeting
try:
greeting = conn.read(timeout=2)
if greeting:
print(greeting, end="")
except TimeoutError:
pass
# Interactive loop
while True:
try:
user_input = input(">>> ")
conn.write(user_input + "\r\n")
conn.flush()
# Read response
response = conn.read(timeout=5)
if response:
print(response, end="")
Usage:
python bin/blocking_client.py localhost 6023
Miniboa Compatibility
The ServerConnection class (received in handler
callbacks) provides miniboa-compatible properties and methods for easier
migration:
from telnetlib3.sync import BlockingTelnetServer
def handler(client):
# Miniboa-compatible properties
print(f"Connected: {client.addrport()}")
print(f"Terminal: {client.terminal_type}")
print(f"Size: {client.columns}x{client.rows}")
# Miniboa-compatible send (converts \n to \r\n)
client.send("Welcome!\n")
while client.active:
if client.idle() > 300:
client.send("Timeout.\n")
client.deactivate()
break
try:
line = client.readline(timeout=1)
except TimeoutError:
continue
if line:
client.send(f"Echo: {line}")
server = BlockingTelnetServer('0.0.0.0', 6023, handler=handler)
server.serve_forever()
Properties and methods with equal mapping:
active,
address,
port,
terminal_type,
columns,
rows,
send(),
addrport(),
idle(),
duration(),
deactivate()
Key differences from miniboa:
telnetlib3 uses a thread-per-connection model instead of miniboa’s poll-based
server.poll()loopminiboa’s
get_command()andcmd_readyare replaced by blockingreadline()andread()
Note
The send() method normalizes
newlines to \r\n for miniboa
compatibility. Both \n and \r\n in the input produce a single
\r\n on the wire:
conn.send("Hello!\n") # OK -- sends \r\n on the wire
conn.send("Hello!\r\n") # OK -- also sends \r\n on the wire
conn.write("Hello!\r\n") # OK -- write() sends as-is
Advanced Negotiation
Use wait_for() to block until telnet
options are negotiated:
conn.wait_for(remote={'NAWS': True, 'TTYPE': True}, timeout=5.0)
term = conn.get_extra_info('TERM')
cols = conn.get_extra_info('cols')
rows = conn.get_extra_info('rows')
The wait_for() method accepts remote,
local, and pending dicts. Option names are strings: "BINARY",
"ECHO", "NAWS", "TTYPE", etc.
For protocol state inspection, use the writer
property:
writer = conn.writer
print(f"Mode: {writer.mode}") # 'local', 'remote', or 'kludge'
print(f"ECHO enabled: {writer.remote_option.enabled(ECHO)}")
Go-Ahead (GA)
When a client does not negotiate Suppress Go-Ahead (SGA), the server sends
IAC GA after output to signal that the client may transmit. This is
correct behavior for MUD clients like Mudlet that expect prompt detection
via GA.
If GA causes unwanted output for your use case, disable it:
telnetlib3-server --never-send-ga
For PTY shells, GA is sent after 500ms of output idle time – Go ahead (GA) isn’t typically used with interactive programs, it is probably best to disable it.
Fingerprinting Server
The public telnetlib3 demonstration Fingerprinting Server is:
telnet 1984.ws 555
The fingerprinting shell
(telnetlib3.fingerprinting.fingerprinting_server_shell()) probes each
connecting client’s telnet capabilities, terminal emulator features, and unicode
support. This useful for uniquely identify clients across sessions by the
capabilities of the software used. The fingerprinting shell runs in two phases:
Telnet probe – negotiates all standard telnet options (TTYPE, NAWS, BINARY, SGA, ECHO, NEW_ENVIRON, CHARSET, LINEMODE, SLC) and records which options the client supports, the TTYPE cycle, environment variables, and SLC table. A deterministic hash is computed from the protocol-level fingerprint.
Terminal probe – if ucs-detect is installed, the shell spawns it through a PTY to probe the terminal emulator’s software and version, color depth, graphics protocols (Kitty, iTerm2, Sixel), device attributes, DEC private modes, unicode version support, and emoji rendering. A second hash is computed from the terminal fingerprint.
Running
Install with optional dependencies for full fingerprinting support (prettytable and ucs-detect):
pip install telnetlib3[extras]
A dedicated CLI entry point is provided:
telnetlib3-fingerprint-server --data-dir data
This uses FingerprintingServer as the
protocol factory and fingerprinting_server_shell()
as the default shell. All telnetlib3-server options (--host, --port,
etc.) are accepted.
Storage
Results are saved as JSON files organized by fingerprint hash:
<data-dir>/client/<telnet-hash>/<terminal-hash>/
Moderating
The bin/moderate_fingerprints.py script provides an interactive CLI for
reviewing client-submitted name suggestions and assigning names to hashes:
export TELNETLIB3_DATA_DIR=./data
python bin/moderate_fingerprints.py
Fingerprinting Client
The telnetlib3-fingerprint CLI connects to a remote telnet server,
probes its supported telnet options, captures the login banner, and saves a
structured JSON fingerprint. This is the reverse of the fingerprinting
server – it fingerprints servers instead of clients.
Running
telnetlib3-fingerprint example.com 23
Options:
--data-dir <path>– directory for fingerprint data (default:$TELNETLIB3_DATA_DIR).--save-json <path>– write the JSON result to a specific file instead of<data-dir>/server/<hash>/.--connect-timeout <secs>– TCP connection timeout (default 10).--silent– suppress fingerprint output to stdout.
The fingerprint JSON records which options the server offered (WILL) and requested (DO), which it refused, the pre-login banner text, and optional DNS resolution results. Files are stored under:
<data-dir>/server/<protocol-hash>/<session-hash>.json
The bin/moderate_fingerprints.py script handles both client and server
fingerprints.
MUD Server
The public telnetlib3 demonstration MUD Server is:
telnet 1984.ws 6063
telnetlib3 supports the common MUD (Multi-User Dungeon) protocols used by MUD clients like Mudlet, TinTin++, and BlowTorch:
GMCP (Generic MUD Communication Protocol) – JSON-based structured data for room info, character vitals, inventory, and more.
MSDP (MUD Server Data Protocol) – binary-encoded variable/value pairs for real-time game state.
MSSP (MUD Server Status Protocol) – server metadata for MUD crawlers and directories.
The telnetlib3.mud module provides encode/decode functions for all three
protocols using TelnetWriter methods
send_gmcp(),
send_msdp(), and
send_mssp().
Running
The repository includes a “mini-MUD” example at bin/server_mud.py with rooms, combat, weapons, GMCP/MSDP/MSSP support, and basic persistence. MUD servers usually run in “line mode”:
telnetlib3-server --line-mode --shell bin.server_mud.shell
Connect with any telnet or MUD client:
telnetlib3-client localhost 6023
Legacy telnetlib Compatibility
Python’s telnetlib was removed in Python 3.13 (PEP 594). telnetlib3 includes a verbatim copy from Python 3.12 with its original test
suite and a drop-in shim so that import telnetlib continues to work:
# Both of these work:
from telnetlib import Telnet
from telnetlib3.telnetlib import Telnet
The legacy module has limited negotiation support and is maintained for compatibility only.
Modern Alternative
telnetlib3.sync provides a modern blocking interface:
Old telnetlib |
|
|---|---|
|
|
|
|
|
|
|
|
|
Enhancements over legacy telnetlib:
Full RFC 854 protocol negotiation (NAWS, TTYPE, BINARY, ECHO, SGA)
wait_for()to await negotiation statesget_extra_info()for terminal type, size and other metadatawriterproperty for protocol state inspectionServer support via
BlockingTelnetServer