Network Layer
Overview
The TinyMUX network layer handles TCP connection acceptance, input parsing, output buffering, telnet negotiation, and session lifecycle management. The implementation is split across several source files: bsd.cpp owns descriptor lifecycle and global state, net.cpp provides the connection accessor layer and command dispatch, telnet.cpp implements the NVT state machine and option negotiation, and ganl_adapter.cpp bridges the GANL (Generic Asynchronous Network Library) event-driven I/O engine to the rest of the server.
Accepting Connections
TinyMUX listens on one or more TCP ports, optionally including dedicated TLS ports. The GANL network engine monitors listening sockets and delivers Accept events to the main loop. When an accept event arrives, the adapter checks whether the server has exhausted its file descriptor budget (the OS limit minus seven reserved descriptors for logging, slave pipes, and similar internal use). If capacity remains, a new DESC (descriptor_data) structure is allocated and initialized with default values: idle timeout, command quota, retry limit, default character encoding, and terminal dimensions of 78x24.
The remote address is extracted from the connection and stored in the mux_sockaddr address field. The server immediately checks the site access list (g_access_list). Connections from forbidden sites are refused before the welcome screen is sent. Connections from registered or suspect sites are flagged for later use during login processing.
The DESC Structure
Each network connection is represented by a descriptor_data struct (typedef DESC), defined in interface.h. Key fields include:
- socket – The underlying file descriptor or GANL connection handle.
- flags – Bitmask including
DS_CONNECTED(logged in),DS_AUTODARK(wizard auto-darked for idle),DS_PUEBLOCLIENT,DS_WEBSOCKET,DS_WEBSOCKET_HS(WebSocket handshake in progress),DS_NEED_PROTO(awaiting protocol detection), andDS_TLS. - player – The dbref of the connected player, or
NOTHINGif still at the login screen. - output_queue / input_queue –
std::deque<std::string>containers for outbound and inbound data. - raw_input_buf / raw_input_at – Line-editing buffer used by the NVT parser to accumulate a single command line from raw bytes.
- nvt_him_state[256] / nvt_us_state[256] – Per-option telnet negotiation state arrays tracking the Q-method state machine for both sides.
- encoding / negotiated_encoding – The active and negotiated character set (UTF-8, Latin-1, Latin-2, CP437, or ASCII).
- width / height – Terminal dimensions, updated via NAWS subnegotiation.
- ttype – Terminal type string received via TTYPE subnegotiation.
- quota – Per-connection command quota that throttles command execution rate.
- connlog_id – SQLite connection log row identifier.
Active descriptors are tracked in three containers defined in driverstate.h: g_descriptors_list (an std::list<DESC*> for iteration order), g_descriptors_map (an unordered map for O(1) lookup and erasure), and g_dbref_to_descriptors_map (a multimap keyed by player dbref for per-player iteration).
The Main Event Loop
The main loop in GanlAdapter::run_main_loop() repeatedly calls networkEngine_->processEvents() with a timeout derived from the next scheduled engine task. Each iteration processes up to 64 I/O events. Accept events create new descriptors as described above. Read events are dispatched through GANL’s ConnectionBase layer, which calls onDataReceived() in the session manager. After processing network events, the loop checks for SIGCHLD notifications from dump child processes, then calls process_tinyMUX_tasks() to update command quotas and run the engine’s task scheduler.
The loop exits when g_shutdown_flag is set, whether by a signal handler, the @shutdown command, or a fatal network engine error.
Input Processing
When bytes arrive on a connection, onDataReceived() first handles protocol detection: if the DS_NEED_PROTO flag is set, the adapter checks whether the data looks like an HTTP upgrade request (WebSocket) or normal telnet traffic. For telnet connections, the raw bytes are fed to process_input_helper() in telnet.cpp.
The NVT parser uses a state machine driven by two tables: nvt_input_xlat_table classifies each byte into one of 14 character classes, and nvt_input_action_table maps (state, class) pairs to one of 18 actions. The parser tracks eight states from NVT_IS_NORMAL through NVT_IS_HAVE_IAC_SB_IAC. Printable characters are accumulated into raw_input_buf, with on-the-fly conversion from the negotiated encoding to internal UTF-8. Backspace and Delete perform character erasure. A linefeed (LF) terminates a command line and passes it to save_command(), which queues it in d->input_queue and schedules Task_ProcessCommand for deferred execution.
Task_ProcessCommand dequeues one command at a time, checking the per-connection quota. If quota is available, the command is dispatched: connected players go through do_command() which calls the engine’s ProcessCommand with an alarm clock guard (max_cmdsecs); unconnected descriptors go through the logged-out command table or check_connect(). If quota is exhausted, the task is rescheduled after one timeslice.
Output Processing
Output flows through queue_write_LEN(), which appends data to d->output_queue. If the queue exceeds output_limit, the function calls process_output() to flush pending data and, if still over limit, drops the oldest queue entry. For WebSocket connections, data is wrapped in WebSocket text frames before queuing.
queue_string() is the higher-level interface that handles ANSI color conversion (or stripping), HTML conversion for Pueblo clients, character set encoding from internal UTF-8 to the connection’s negotiated encoding, and IAC byte doubling for telnet connections.
process_output() in bsd.cpp drains the output queue by passing each entry to the GANL adapter’s send_data() method, which hands the bytes to the network engine for asynchronous transmission.
Connection States
A descriptor progresses through several logical states:
- Protocol detection (
DS_NEED_PROTO)—Waiting for first data to distinguish WebSocket from telnet. - Login screen – The welcome file has been sent. The player can issue
connect,create,WHO,QUIT, and other logged-out commands. Failed login attempts decrementretries_left; exhausting retries causes disconnection (R_BADLOGIN). - Connected (
DS_CONNECTED)—The player has authenticated. Commands are dispatched to the engine. TheLOGOUTcommand returns the descriptor to the login screen without closing the TCP connection. - Shutdown – Triggered by
QUIT, idle timeout, boot, or server shutdown.shutdownsock()logs accounting data, announces the disconnect to the engine, flushes output, frees queues, closes the socket, and removes the descriptor from all tracking containers.
Telnet Protocol Handling
On connection finalization, the server initiates negotiation for several options by calling enable_us() and enable_him(): EOR (End of Record) in both directions, SGA (Suppress Go-Ahead) from the client, TTYPE (Terminal Type), NAWS (Negotiate About Window Size), ENV (Environment), and CHARSET (Character Sets). The negotiation follows the Q-method state machine with six states per option per direction (NO, YES, WANTNO_EMPTY, WANTNO_OPPOSITE, WANTYES_EMPTY, WANTYES_OPPOSITE).
When the client agrees to TTYPE, the server sends a TTYPE SEND subnegotiation request. When NAWS is agreed, the client sends window dimensions that update d->width and d->height. When CHARSET is agreed, the server sends a CHARSET REQUEST subnegotiation listing UTF-8, ISO-8859-1, ISO-8859-2, US-ASCII, and CP437. The client’s accepted charset updates d->encoding and d->negotiated_encoding.
Prompts (partial lines not terminated by CRLF) are followed by IAC EOR if EOR is negotiated, or IAC GA if SGA is not negotiated. This is used by @program mode.
IPv4 and IPv6 Support
The mux_sockaddr type abstracts over sockaddr_in and sockaddr_in6. Address resolution uses getaddrinfo() with AF_UNSPEC, so listening and connecting work transparently over both IPv4 and IPv6. The ntop() method formats addresses for display in logs and the addr field of each DESC.
Site-Based Access Control
The g_access_list (mux_subnets) object, populated by the @site command, holds subnet rules that classify connecting addresses. At connection time, isForbid() rejects connections outright. During login, check() returns flags including HI_REGISTER (creation disabled, must use existing account) and HI_NOGUEST (guest logins forbidden). isSuspect() marks connections for wizard-visible flagging in the WHO list.
DNS Lookups
Reverse DNS lookups are performed asynchronously to avoid blocking the main loop. On Unix, the GANL adapter spawns bin/slave as a child process via networkEngine_->spawnSlave(). The slave reads numeric IP addresses on stdin and writes back resolved hostnames. On Windows, a pool of DNS worker threads performs the same function. Results update the addr field on the matching descriptor.
Connection Limits and Idle Timeouts
The server enforces a maximum player count (max_players configuration). When the limit is reached, new login attempts receive the “game full” message and are disconnected. The file descriptor limit is also enforced at the accept stage.
check_idle() runs periodically and compares each descriptor’s last_time against the current time. Connected players whose idle time exceeds idle_timeout are disconnected (R_TIMEOUT), unless they have the Can_Idle permission. Idle wizards are automatically set DARK (DS_AUTODARK) rather than disconnected, provided idle_wiz_dark is enabled. The AUTODARK state is cleared when any session for that player receives new input.
Each descriptor also has an individual timeout field, initially set from the global idle_timeout but overridable per-player via the TIMEOUT attribute.
WHO List and Session Tracking
The dump_users() function (invoked by WHO, DOING, and SESSION commands) iterates g_descriptors_list and formats connection information. Wizards see additional details including IP addresses, site flags, and connection handles. The DOING string is stored per-descriptor in d->doing and set via the @doing command.
Session statistics are tracked per-descriptor: command_count, input_tot, output_tot, output_lost, connected_at, and last_time. The SESSION command displays these counters. Connection accounting records (player dbref, flags, command count, connect duration, location, money, site, disconnect reason) are written to the log at disconnect time.