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 clients to access all connected protocols

  • Handling 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 in remote_option (client WILL)

  • local: Dict of options to wait for in local_option (client DO)

  • pending: Dict of options to wait for in pending_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\n is required.

  • Kludge mode (SGA negotiated, no LINEMODE): input is character-at-a-time, but server output is still NVT – \r\n is expected.

  • Binary mode (TRANSMIT-BINARY): raw bytes, no NVT transformation – \n is 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 echoes

  • WILL 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() loop

  • miniboa’s get_command() and cmd_ready are replaced by blocking readline() and read()

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:

  1. 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.

  2. 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

telnetlib3.sync

Telnet(host)

TelnetConnection

tn.read_until()

read_until()

tn.read_some()

read_some()

tn.write()

write()

tn.close()

close()

Enhancements over legacy telnetlib: