Telnet Negotiation Reference
Telnet isn't just a raw byte pipe — it carries an in-band negotiation language so the two ends can agree on options like echo, window size, and terminal type. This page explains that language and how the gateway speaks it.
A telnet connection is a stream of ordinary data bytes with one special
value woven through it: 0xFF, the IAC
(“Interpret As Command”) byte. When a receiver sees
IAC it knows the next byte (or few) is a command, not data.
Commands let the two ends turn features on and off by mutual agreement
— this is option negotiation, defined by RFC 854
and refined by RFC 1143.
The model is deliberately symmetric and conservative: an option is only
used if both ends agree, either side may refuse, and a refusal
is always final. That is what lets a 1980s terminal and a modern client
share one protocol — each enables only what it understands and
politely declines the rest. Because a literal 0xFF can occur
in real data, it is escaped on the wire as IAC IAC and
collapsed back to a single 0xFF by the receiver.
Every option has two independent sides — “my side” and “your side.” Two verbs talk about each.
An IAC command of the form IAC <verb> <option>
asserts or requests something. WILL / WONT talk about the
sender's own side of an option; DO / DONT talk
about what the sender wants from the other side:
| Verb | Byte | Means | Valid replies |
|---|---|---|---|
WILL | 251 | “I will enable this option on my end.” | DO (yes, please) or DONT (no) |
WONT | 252 | “I won't / am disabling this option on my end.” | DONT (acknowledged) |
DO | 253 | “Please enable this option on your end.” | WILL (ok) or WONT (refused) |
DONT | 254 | “Don't enable / disable this option on your end.” | WONT (acknowledged) |
The two rules that keep negotiation sane:
WILL is only in effect once answered with DO; a DO only once answered with WILL. Either end may answer with the negative and the option stays off.WONT must be met with DONT, and a DONT with WONT. Refusals are never argued with.WILL ECHO (“I will do the
echoing”), which the client accepts with DO ECHO. It is
not the client telling the server to echo — that asymmetry
trips up nearly everyone at first.
The full command vocabulary. A command is IAC followed by one of these.
| Name | Dec | Hex | Purpose |
|---|---|---|---|
IAC | 255 | 0xFF | Interpret As Command — the escape that introduces every command (and, doubled, a literal 0xFF in data). |
WILL | 251 | 0xFB | Offer / assert an option on the sender's side. |
WONT | 252 | 0xFC | Refuse / disable an option on the sender's side. |
DO | 253 | 0xFD | Request an option on the receiver's side. |
DONT | 254 | 0xFE | Refuse / disable an option on the receiver's side. |
SB | 250 | 0xFA | Subnegotiation Begin — start an option's parameter block. |
SE | 240 | 0xF0 | Subnegotiation End — IAC SE closes the block. |
These have no option byte. The gateway consumes them so they never leak to your terminal; AYT gets a visible reply.
| Name | Dec | Purpose |
|---|---|---|
NOP | 241 | No operation (keep-alive). |
DM | 242 | Data Mark (the SYNCH sentinel). |
BRK | 243 | Break. |
IP | 244 | Interrupt Process. |
AO | 245 | Abort Output. |
AYT | 246 | Are You There — the gateway answers with a visible “yes” so a human probing the link gets a reply. |
EC | 247 | Erase Character. |
EL | 248 | Erase Line. |
GA | 249 | Go Ahead — the half-duplex line-turnaround signal (suppressed once SGA is on; consumed if received). |
Each option is one numbered feature. Here is what every option the gateway handles is for, and how the gateway treats it.
Decides which side echoes typed characters back to the
screen. When the gateway sends WILL ECHO, it takes
responsibility for echoing — so the client should stop local-echoing,
and the gateway can suppress echo for password fields and run
character-at-a-time apps. As a server the gateway asserts
WILL ECHO at connect; as an outbound proxy it accepts a remote
host's WILL ECHO with DO ECHO (a BBS that echoes
for you).
Turns off the ancient half-duplex GA “your turn”
signal. Suppressing it puts the link into full-duplex,
character-at-a-time mode — the modern default. The gateway
both offers it (WILL SGA) and asks for it (DO SGA),
and never emits a GA itself. Paired with WILL ECHO
this is what makes a session feel like a normal terminal.
Lets the client tell the server what kind of terminal it is.
The gateway sends DO TTYPE; once the client agrees
(WILL TTYPE) the gateway asks for the name with a
SB TTYPE SEND subnegotiation, and the client answers
SB TTYPE IS "<name>". The gateway maps that name to its
three classes — PETSCII (Commodore),
ANSI, or ASCII/DUMB — so it can
skip the manual BACKSPACE detection prompt entirely.
“Negotiate About Window Size” — lets the client report its
terminal width and height. The gateway sends
DO NAWS; the client agrees (WILL NAWS) and sends
SB NAWS <width><height> (two 16-bit values), and
re-sends it whenever the window is resized. The gateway uses the size for
screen layout and forwards live resizes through the outbound gateway in
cooperative mode.
A way to ask the other end which options it thinks are on.
On a DO STATUS the gateway agrees (WILL STATUS);
a later SB STATUS SEND triggers a dump of the current option
state. Rarely used in practice, but supported.
A one-shot synchronization mark: “tell me when
you've processed everything up to here.” On a
DO TIMING-MARK the gateway flushes its queued output and then
replies WILL TIMING-MARK — that reply is the
mark. No persistent state is kept.
Any option the gateway doesn't implement is politely refused:
an incoming WILL is answered with DONT, and an
incoming DO with WONT. Nothing is left
un-acknowledged, so a peer never hangs waiting on a reply.
Where the gateway sits in the wider telnet option landscape. “Refused” means it is acknowledged with WONT/DONT and left off — it does no harm, the feature just isn't used.
| Opt | Name | RFC | Gateway |
|---|---|---|---|
| 0 | BINARY (Transmit-Binary) | 856 | Refused — uses IAC-escaping instead (see Data Phase). |
| 1 | ECHO | 857 | Supported |
| 3 | SGA (Suppress Go-Ahead) | 858 | Supported |
| 5 | STATUS | 859 | Supported |
| 6 | TIMING-MARK | 860 | Supported |
| 24 | TERMINAL-TYPE (TTYPE) | 1091 | Supported |
| 31 | NAWS (Window Size) | 1073 | Supported |
| 32 | TERMINAL-SPEED | 1079 | Refused |
| 33 | TOGGLE-FLOW-CONTROL | 1372 | Refused |
| 34 | LINEMODE | 1184 | Refused (the gateway does its own line editing in char-at-a-time mode). |
| 36 | ENVIRON | 1408 | Refused |
| 39 | NEW-ENVIRON | 1572 | Refused (a stray subnegotiation is consumed and ignored). |
When an option needs to carry data — a terminal-type string, a window size — that data rides in a subnegotiation block.
A subnegotiation is framed as
IAC SB <option> <parameters> IAC SE. Only options
that were already enabled by WILL/DO may
subnegotiate. The two the gateway uses:
| Block | Meaning |
|---|---|
IAC SB TTYPE SEND IAC SE | Server → client: “send me your terminal type.” |
IAC SB TTYPE IS "xterm" IAC SE | Client → server: “I am an xterm.” |
IAC SB NAWS <w><w><h><h> IAC SE | Client → server: width and height, each a 16-bit value (high byte first). |
SB with no terminating IAC SE can't exhaust memory
— the body is bounded and the session bails rather than buffering
without limit. Any literal 0xFF inside a subnegotiation is
IAC IAC-escaped like everywhere else.
Negotiation decides the rules; the data phase is where actual bytes flow under them. Both ends pretend to be the same imaginary device — the Network Virtual Terminal.
Telnet's core abstraction is the NVT (Network Virtual Terminal): a canonical, lowest-common-denominator terminal that both ends agree to emulate so neither needs to know the other's real hardware. The NVT is a 7-bit ASCII device with a printer and a keyboard, and it imposes three wire conventions on ordinary data — the same three the gateway's raw I/O layer applies on every byte:
| Convention | On the wire | Why |
|---|---|---|
| End of line | CR LF (0x0D 0x0A) | The NVT newline is the pair, not a lone LF. |
| Bare carriage return | CR NUL (0x0D 0x00) | A CR not meant to start a new line is followed by NUL so the far end doesn't treat the next byte as a line start. The receiver strips the NUL. |
| Literal 0xFF | IAC IAC (0xFF 0xFF) | Since 0xFF is the command escape, a real 0xFF data byte is doubled and collapsed back on receipt. |
The gateway implements all three: outbound writes stuff CR
→ CR NUL and escape 0xFF →
IAC IAC; inbound reads strip the NUL and
un-double the IAC. SSH and serial links skip them — SSH
has no in-band command byte, and a serial wire isn't an NVT.
The BINARY option (0, RFC 856) exists to suspend
these NVT rules so all 256 byte values pass literally — the “clean
8-bit pipe” you'd want for a file transfer. The gateway deliberately
does not negotiate BINARY (it refuses
DO BINARY with WONT). Instead it stays in NVT
mode and relies on IAC IAC escaping and CR NUL
stuffing to carry arbitrary bytes safely. That keeps one consistent code
path for every client — including the many retro clients that don't
implement BINARY — and it is exactly the transform that
XMODEM/YMODEM transfers over telnet must respect (the I
toggle on the File Transfer menu). See the
XMODEM Protocol Reference for how that
interacts with binary file data.
How the gateway avoids negotiation loops when both ends change their minds at once.
Naive negotiation has a famous failure mode: if each side answers every
request, and both request the same option at the same time, they can
bounce WILL/DO back and forth forever. RFC 1143
solves it with a small state machine kept per option, per side.
Each side is in one of six states:
| State | Meaning |
|---|---|
NO | Option is off and idle. |
YES | Option is on and idle. |
WANTYES | We asked to turn it on; awaiting the agreement. |
WANTNO | We asked to turn it off; awaiting the acknowledgement. |
WANTYES / WANTNO (opposite queued) | A change-of-mind is queued behind the in-flight request, applied when it resolves. |
The practical payoff: the gateway only ever sends a request when the state
actually changes, and it treats a peer's spontaneous WILL TTYPE
(in answer to its own DO TTYPE) as an acknowledgement
rather than a fresh offer to answer again. No loops, no double-acks, and a
clean resolution even when a mind-change is already in flight.
Without the state machine, both ends offering at once would echo forever. With it:
both ends decide they want TTYPE on, at the same instant
S: IAC DO TTYPE gateway: state NO -> WANTYES
C: IAC WILL TTYPE client's own offer, crossing on the wire
gateway receives WILL TTYPE while in WANTYES:
-> treats it as the agreement, state -> YES
-> does NOT emit another DO (which would loop)
S: (silent — option is now on, settled)
The mirror case — a queued change of mind — works the same way:
if the gateway is in WANTYES and decides it wants the option
off again, that flips it to WANTYES (opposite queued);
when the in-flight request resolves, the queued WONT/DONT
is sent exactly once. The connection always converges.
What the gateway sends the instant your client connects.
On every non-serial connection the gateway opens with a fixed five-command burst, then waits briefly for the replies:
IAC WILL ECHO "I'll do the echoing"
IAC WILL SGA "full-duplex, no go-aheads from me"
IAC DO SGA "please suppress go-ahead too"
IAC DO TTYPE "tell me your terminal type"
IAC DO NAWS "tell me your window size"
A cooperating client answers DO ECHO / WILL SGA /
DO SGA / WILL TTYPE / WILL NAWS and
then sends its SB TTYPE IS and SB NAWS data. If
TTYPE identified the terminal, the gateway skips the manual
“press BACKSPACE” detection; otherwise it falls back to that
prompt. Raw clients that ignore the burst still work — the
unanswered options simply stay off.
When you dial a remote host through the gateway, its negotiation policy depends on the configured mode.
| Mode | Negotiation behaviour |
|---|---|
| Reactive (default) | Parse IAC both ways but send no proactive offers. Accept a remote WILL ECHO with DO ECHO; refuse every other peer-initiated option (WILL → DONT, DO → WONT); escape outbound 0xFF as IAC IAC. Works with real telnet servers and raw-TCP services on port 23. |
| Cooperative | Everything reactive does, plus proactive WILL TTYPE / WILL NAWS / DO ECHO at connect; answers SB TTYPE SEND with your terminal type (PETSCII / ANSI / DUMB) and DO NAWS with your real window size, forwarding live resizes. Turn this on for modern BBSes. |
| Raw TCP | The entire IAC layer is disabled — no parsing inbound, no escaping outbound — for destinations that aren't really telnet. (Bytes to your local client are still IAC-escaped so your own telnet client isn't confused.) |
Putting it together: everything that happens from TCP connect to disconnect, in order.
WILL ECHO, WILL SGA, DO SGA, DO TTYPE, DO NAWS, then pauses briefly for replies.DO ECHO, WILL SGA / DO SGA, WILL TTYPE, WILL NAWS) and may push its SB NAWS size right away. A raw client stays silent — those options simply remain off.SB TTYPE SEND; the client returns SB TTYPE IS "<name>", which the gateway maps to PETSCII / ANSI / ASCII.TTYPE didn't identify the terminal, the gateway prints “Press BACKSPACE to detect terminal” and classifies by the byte received (0x14 = PETSCII, 0x08/0x7F = ANSI, else ASCII).security_enabled, the login prompt runs (with the shared per-IP lockout).CR NUL stuffing and IAC IAC escaping applied throughout, echo handled by the gateway.A few negotiations as they actually appear on the wire (S: = server / gateway, C: = client).
S: IAC WILL ECHO IAC WILL SGA IAC DO SGA
C: IAC DO ECHO IAC DO SGA IAC WILL SGA
-> server echoes; both sides full-duplex
S: IAC DO TTYPE
C: IAC WILL TTYPE
S: IAC SB TTYPE SEND IAC SE
C: IAC SB TTYPE IS "xterm-256color" IAC SE
-> detected as ANSI; BACKSPACE prompt skipped
S: IAC DO NAWS
C: IAC WILL NAWS
C: IAC SB NAWS 0 80 0 24 IAC SE 80 x 24
...user resizes the window...
C: IAC SB NAWS 0 132 0 50 IAC SE now 132 x 50
C: IAC WILL LINEMODE
S: IAC DONT LINEMODE politely refused, stays off
C: IAC AYT
S: [Yes] visible reply for a human probing the link
| Term | Meaning |
|---|---|
| NVT | Network Virtual Terminal — the canonical imaginary terminal both ends emulate so neither needs to know the other's real hardware. |
| IAC | Interpret As Command (0xFF) — the escape byte that introduces every telnet command and, doubled, a literal 0xFF in data. |
| Option | A numbered, individually negotiable feature (ECHO = 1, SGA = 3, TTYPE = 24, …). |
| Verb | One of WILL / WONT / DO / DONT — the four ways to assert or request an option. |
| Subnegotiation | An IAC SB … IAC SE block that carries an enabled option's parameters (a terminal-type string, a window size). |
| Q-Method | RFC 1143's per-option state machine (NO / YES / WANTYES / WANTNO + queued change) that prevents negotiation loops. |
| Go-Ahead (GA) | The half-duplex “your turn” line-turnaround signal, suppressed once SGA is enabled. |
| SYNCH / Data Mark | An out-of-band attention mechanism (TCP urgent data + the DM command). Recognised and consumed, not specially acted on here. |
| CR NUL | How a bare carriage return is encoded on an NVT link (0x0D 0x00); the NUL is stripped on receive. |
Deep-dive reference pages for every protocol and interface, plus the character-set tables and ANSI escape-sequence reference.
128-byte blocks; CRC-16 / checksum negotiation.
Block-0 metadata, batch, exact size truncation.
Streaming; ZDLE, CRC-32, autostart, resume.
Send-Init negotiation; F / A / D / Z / B transfer.
C1 dual checksum, two-phase, GOO/BAD/ACK.
Hayes command set, S-registers, +++ escape.
IAC negotiation, every option, the NVT data phase.
Server, gateway, host keys, TOFU, auth modes.
Hex tables for every encoding: ASCII, ANSI, PETSCII, ATASCII, Baudot/ITA2, ZX Spectrum, TRS-80.
Cursor, colour/SGR, erase, and screen-mode escape codes, with the raw hex bytes.