Telnet negotiations are a crucial part of the Telnet protocol, allowing clients and servers to agree on various options that affect the communication session. This document explains how Telnet negotiations work and how we handle different options in our implementation.
The Telnet negotiation process is a bidirectional protocol in which both the client and server can initiate and respond to requests for specific options. This process enables both parties to agree on features and settings to be used during the session. The typical negotiation commands are as follows:
-
DO: Sent by one party (client or server) to request that the other party enable a specific option. This command is a request for the peer to start using the option if supported.
-
DON'T: Sent by a party to instruct the peer not to enable a particular option or as a response to decline a previously sent DO request.
-
WILL: Sent in response to a DO command to indicate willingness to enable the specified option. This command signifies that the sender is ready to start using the requested option.
-
WON'T: Used to refuse enabling an option. If a peer sends a DO command and the recipient cannot or will not enable the option, it responds with WON'T.
-
SUBNEGOTIATION: Some options require additional data to be configured beyond simply being enabled. In such cases, either party can initiate subnegotiation, sending a command with necessary data. For example, options like NAWS (Negotiate About Window Size) or CHARSET (character set) often involve subnegotiation to specify values (e.g., window size or encoding).
The TelnetClient class handles these negotiation commands, regardless of their origin, and emits events for each command received, allowing the application to respond accordingly. This event-driven structure enables dynamic handling of each negotiation state, supporting a flexible exchange of capabilities.
| Telnet Option | Client Support | Client Negotiation | Server Negotiation | Remarks | Discussion |
|---|---|---|---|---|---|
| ECHO | Full | Dynamic | Dynamic | Is used in the telnet login flow to hide user input (password). This option is negotiated on the fly and can be enabled or disabled whenever needed. | |
| SGA (Suppress Go Ahead) | Full | DO | WONT | We offer a DO at startup and can handle changes on the fly. However, Unitopia WONT accept this option for unknown reasons. | #115 |
| TTYPE | Full | WILL | DO | Allows the client to send its name (originally the terminal type but it looks abused nowadays) to the server. | #126 |
| STATUS | Full | DO | WILL | Allows for comparisson of the status of the server and the client. Allows for comparison of the negotiated options | #130 |
| MSSP (Mud Server Status Protocol) | Full | DO | WILL | Allows our client to retrieve basic information about the mud, like the current player count or the server name. | #131 |
| EOR (End of Record) | Full | DO | WILL | Allows for the server to signal the end of a record which is very useful to segment messages to the client. | #112 |
| TM (TIMING MARK) | Full | DO | WILL | This option is currently used to generate Ping round-trips from the server to the client. | #129 |
| NAWS (Negotiate About Window Size) | Partial | WILL (+ Sub) | DO | We support this option to subnegotiate the window size. However, we send static values for the window size (80x25) and it does look like Unitopia is ignoring these values. | #108 |
| CHARSET | Partial | DO / WILL (+ Sub) | WILL (+ Sub) / DO | We support this option to subnegotiate the character set with the server. However, we only accept UTF-8. If the server does not subnogitiate for UTF-8, an error will be thrown and the connection will be closed. | #111 |
| LINEMODE | Full | WILL | DO | Client negotiates LINEMODE with MODE (EDIT/TRAPSIG/SOFT_TAB), acknowledges SLC changes, publishes a CR/LF forward mask so lines flush only on Enter, and surfaces EDIT toggles so the frontend can fall back to character mode. | #114 |
| STARTTLS | Unsupported | WONT | DO | This option allows to upgrade any existing connection to a secure one. However, we don't support this intentionally and recommend you to initialize a secure connection from the beginning. | #113 |
| XDISPLOC | Unsupported | WONT | DO | Crazy option to redirect graphical output directly to a X display. Probably not used by Unitopia. | |
| TSPEED (Terminal Speed) | Unsupported | WONT | DO | Allows the client to report its connection speed (in baud) to the server, which can adjust data transmission rates accordingly. Although we do not support this option now, it could be valuable, especially for managing data flow over unstable or mobile connections. | |
| ENVIRON | Unsupported | WONT | DO | Allows the client to send environment variables to the server. However, we don't support this option now. | |
| NEWENV | Unsupported | WONT | DO | Never version of ENVIRON. Still unsupported. | |
| COMPRESS | Unsupported | WONT | DO | LZW based compression. We dont need nor support this option now. | |
| COMPRESS 2 | Unsupported | WONT | DO | Compression based on zlib - which is better, but still not supported. | |
| AUTH | Unsupported | WONT | DO | We dont support any authentication options for now. | |
| MXP (Mud Extention Protocol) | Unsupported | WONT | DO | We dont support the MXP option for now, but we want to support this option in the future. | |
| GMCP (Generic Mud Communication Protocol) | Unsupported | WONT | DO | We dont support the GMCP option for now, but we want to support this option in the future. |
Define a new TelnetOptionHandler object for the option you want to handle.
const newOptionHandler: TelnetOptionHandler = {
negotiate: (socket) => {
// In this handler you can send a negotiation yourself uppon initialization.
// Use this if you want the server to enable/disable the option.
},
handleDo: (socket, getPreviousNegotiation?) => {
// Handle the DO command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleDont: (socket, getPreviousNegotiation?) => {
// Handle the DON'T command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleWill: (socket, getPreviousNegotiation?) => {
// Handle the WILL command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleWont: (socket, getPreviousNegotiation?) => {
// Handle the WON'T command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleSub: (socket, serverChunk: Buffer) => {
// Handle the subnegotiation data for the new option
// Return a TelnetSubnegotiationResult object with the appropriate client chunk and client option
},
// Whether this handler is dynamic and can be called again after initial negotiation
isDynamic?: boolean,
};Note: If you mark an option handler as dynamic, it will be called again after the initial negotiation with the server if the server requests a change to the option. This allows for more flexible and dynamic negotiation of Telnet options between the client and server.
Add the newOptionHandler object to the optionsHandler map in your TelnetClient class:
this.optionsHandler.set(TelnetOptions.TELOPT_LINEMODE, newOptionHandler);In some cases, you may need to access the previous negotiation state to determine how to respond to a new negotiation command. The TelnetOptionHandler provides a way to do this through the getPreviousNegotiation function.
The getPreviousNegotiation function returns the previous negotiation result, which can be used to determine the current state of the option. This can be useful in cases where the client needs to respond differently depending on the previous state of the option.
Here is an example of how to use getPreviousNegotiation in a TelnetOptionHandler:
const newOptionHandler: TelnetOptionHandler = {
// ...
handleDo: (getPreviousNegotiation: () => TelnetNegotiationResult | undefined) => {
const previousNegotiation = getPreviousNegotiation();
if (previousNegotiation?.client !== undefined) {
// Handle the case where the option is already enabled
} else {
// Handle the case where the option is not enabled
}
},
// ...
};