ZMODEM Protocol Reference
ZMODEM (Chuck Forsberg, 1988) is the modern, streaming, batch-capable evolution of the XMODEM family — and the one most terminal programs start automatically.
Where XMODEM and YMODEM send one fixed block and wait for an acknowledgement, ZMODEM is built around frames and subpackets with position-based recovery: data is identified by its byte offset in the file, so the receiver recovers from an error by telling the sender where to resume rather than which block to resend. It adds, over YMODEM:
ZRPOS, and per-file skip via ZSKIP in a batch.
The gateway implements the full Forsberg 1988 spec in
src/zmodem.rs: ZDLE escaping, hex / binary16 / binary32
headers, CRC-16 and CRC-32, and batch send and receive. Its
ZRINIT advertises CANFDX | CANOVIO | CANFC32
(full-duplex, overlapped I/O, CRC-32 capable), but the data path runs
stop-and-wait — every non-final data
subpacket is ZCRCQ-terminated and ACK-gated (the
final one closes the frame with ZCRCE). That is slightly slower than
true streaming in theory, but rock-solid over a telnet or SSH hop where
buffering and flow control are unpredictable.
Everything on the wire is either a header or a data subpacket.
A header carries a frame type plus a 4-byte argument (a file position, a
capability mask, a length). It comes in three encodings, all introduced
by the ZPAD ZDLE sync (* *
0x18 …), which is also what makes autostart possible:
| Encoding | Intro | Check | Used for |
|---|---|---|---|
| Hex | ** ZDLE B | CRC-16 | Handshake frames — human-readable, robust. |
| Binary16 | * ZDLE A | CRC-16 | Data frames with CRC-16. |
| Binary32 | * ZDLE C | CRC-32 | Data frames with CRC-32 (when CANFC32 is negotiated). |
The A / B / C suffix is exactly
what the autostart prefix **\x18[ABC] encodes.
| Frame | Hex | Sent by | Meaning |
|---|---|---|---|
ZRQINIT | 0x00 | Sender | “Request init” — opens a transfer (and triggers autostart). |
ZRINIT | 0x01 | Receiver | “Ready” — carries the capability flags. |
ZSINIT | 0x02 | Sender | Optional sender init flags / attention string. |
ZACK | 0x03 | Either | Acknowledge (e.g. a flow-control checkpoint). |
ZFILE | 0x04 | Sender | File header — followed by a name/size subpacket. |
ZSKIP | 0x05 | Receiver | “Skip this file” — decline one file in a batch. |
ZNAK | 0x06 | Either | Last header was garbled — resend it. |
ZABORT | 0x07 | Either | Abort the whole session. |
ZFIN | 0x08 | Either | Finish — tear the session down (both sides exchange it). |
ZRPOS | 0x09 | Receiver | “Resume at this byte offset” — accept, recover, or resume. |
ZDATA | 0x0A | Sender | Data frame at a byte offset; subpackets follow. |
ZEOF | 0x0B | Sender | End of file — argument is the final length. |
ZFERR | 0x0C | Sender | Sender hit a file read/write error and can't continue — the gateway (as receiver) aborts the transfer cleanly rather than waiting out a timeout. |
ZCRC | 0x0D | Either | Verified-resume CRC. A receiver holding a partial file asks the sender to prove the first N bytes match (N=0 means whole file); the gateway (as sender) answers with the CRC-32 of those bytes so the peer can resume safely instead of restarting. |
ZCHALLENGE | 0x0E | Receiver | Optional liveness check — the gateway (as sender) echoes the value back in a ZACK. |
ZFREECNT | 0x11 | Sender | Optional free-space query — the gateway (as receiver) answers with a ZACK. |
ZCOMMAND | 0x12 | Sender | Optional spec feature, not implemented — “run this command.” The gateway never executes it; it refuses every request with a non-zero ZCOMPL. Use SSH for shell access. See the note below. |
ZSTDERR | 0x13 | Sender | Informational text for the receiver's stderr (progress notes, “skipped because…”). Purely cosmetic — the gateway (as receiver) drains and logs it without changing the transfer outcome. |
ZCOMPL header. On Chuck Forsberg's original Unix
rzsz reference code the command string is handed straight to
/bin/sh -c; its classic use was a sender pushing
sz somefile to auto-start a download.
rz, one ZMODEM session here
shares the process with every other connected user. The gateway therefore
handles the frame defensively rather than ignoring it — it
recognizes ZCOMMAND, drains the command subpacket so the wire
stays in sync, replies a non-zero ZCOMPL (refused), and ends the
session. Every request is rejected and there is no configuration switch to
enable it. For command access, use the authenticated, encrypted
SSH interface instead. Every other Forsberg 1988 frame is
fully implemented.
Data rides in subpackets; the byte that ends each one tells the receiver
what to do next. The gateway sends data using ZCRCQ
(ACK-gated) between subpackets and ZCRCE on the last,
which is what gives it its safe stop-and-wait cadence.
| Terminator | Byte | Meaning |
|---|---|---|
ZCRCE | h | End of frame, no more data this frame, no ACK. (The gateway's final data subpacket.) |
ZCRCG | i | More data follows, streaming — no ACK. |
ZCRCQ | j | More data follows, ACK required (mid-frame flow control). (Each non-final data subpacket the gateway sends.) |
ZCRCW | k | End of frame, more data next frame, ACK required. (The gateway's ZFILE name/size subpacket.) |
How ZMODEM keeps control bytes from being eaten by the link.
ZDLE (ZMODEM Data Link Escape, 0x18 — the
same byte as CAN) is the escape marker. Any byte in the data
or CRC that could be intercepted by the transport — ZDLE
itself, DLE (0x10), the XON/XOFF flow-control pair
(0x11 / 0x13) and their high-bit twins (0x91 / 0x93), and
CR (0x0D, and its high-bit twin 0x8D) — is rewritten on the wire as
ZDLE followed by the byte with bit 6 flipped
(byte XOR 0x40). The receiver reverses the transform.
This is why ZMODEM survives XON/XOFF-flow-controlled serial links and
telnet hops that would choke on a raw 0x11: those bytes
never appear literally in the escaped stream. (Over telnet the gateway
additionally handles the 0xFF IAC transform; ZDLE and IAC
escaping are independent layers.)
A receiver can ask the sender to escape more than that default
set by setting ESCCTL (escape every control character) or
ESC8 (escape every 8th-bit byte) in its ZRINIT
capability flags — the gateway honors both when it is the sender.
On decode it also accepts the legacy ZRUB0 /
ZRUB1 rubout escapes (a sender's alternate encoding of
0x7F / 0xFF). And because ZDLE is
the same byte as CAN, a run of them is how a peer signals
abort: the gateway treats five or more consecutive
CANs (or a ZDLE CAN pair) as an immediate
cancel rather than waiting out a timeout.
You pull a file from the gateway. The gateway sends; your terminal's receiver drives the conversation with ZRINIT / ZRPOS.
The single-file handshake (the ZFILE → ZEOF cycle repeats per file in a batch):
gateway -> ZRQINIT open the session
term <- ZRINIT (CANFDX|CANOVIO|CANFC32) receiver ready + caps
gateway -> ZFILE + subpacket("name\0size\0", ZCRCW)
term <- ZRPOS(0) accept; start at byte 0
gateway -> ZDATA(0) + 1024-byte subpackets (ZCRCQ, ACK-gated; final ZCRCE) ... until EOF
gateway -> ZEOF(length) final file size
term <- ZRINIT ready for next file
gateway -> ZFIN
term <- ZFIN
gateway -> "OO" over and out
ZRQINIT (most terminals also auto-receive the moment they see it).ZRINIT, advertising its capabilities; the gateway notes whether CRC-32 is available.ZFILE header followed by a subpacket carrying the filename and exact size (and mtime / mode when known), terminated with ZCRCW.ZRPOS(0) to accept and start at byte 0 — or ZSKIP to decline this file, or a non-zero ZRPOS to resume a partial file.ZDATA header at that offset, then streams 1024-byte data subpackets, each non-final one ZCRCQ-terminated and waiting for the receiver's ZACK before the next (the last closes the frame with ZCRCE) — the stop-and-wait cadence.ZRPOS(offset); the gateway seeks back to that byte offset and resumes from there.ZEOF(length). The receiver answers ZRINIT (ready for the next file).ZFIN; the receiver returns ZFIN; the gateway sends the "OO" over-and-out trailer and the session ends.You send a file to the gateway. The gateway receives, so it drives with ZRINIT / ZRPOS / ZSKIP.
gateway -> ZRINIT (CANFDX|CANOVIO|CANFC32) ready + caps
term <- ZFILE + subpacket("name\0size\0")
gateway -> ZRPOS(0) accept (or ZSKIP to decline a colliding name)
term <- ZDATA + data subpackets ...
gateway -> ZRPOS(offset) only on a CRC error: rewind
term <- ZEOF(length)
gateway -> ZRINIT ready for next file
term <- ZFIN
gateway -> ZFIN
term <- "OO"
ZRINIT advertising CANFDX | CANOVIO | CANFC32.ZFILE header and a subpacket with the filename, size, and optional mtime / mode. The gateway parses it and validates the path (no traversal).ZRPOS(0) to accept. In a batch, a colliding filename is declined with ZSKIP so the sender moves on.ZDATA subpackets. The gateway checks each subpacket's CRC (16 or 32); on a bad one — or a late/garbled header, or a stall — it re-sends ZRPOS with the last good byte offset and the sender resumes there. A good subpacket clears the error count; zmodem_max_retries consecutive errors without progress ends the transfer.ZEOF(length); the gateway answers ZRINIT for the next file. mtime and mode from the ZFILE subpacket are applied to the saved file when present.ZFIN; the gateway returns ZFIN; the sender sends "OO" and the session closes.
A batch is the ZFILE → ZDATA → ZEOF cycle repeated
once per file, with the receiver answering ZRINIT after each
ZEOF to signal “ready for the next.” The single
ZFIN / ZFIN / "OO" exchange closes
the whole session. Either side can decline an individual file with
ZSKIP without aborting the batch.
Most terminals (Qodem, ZOC, SyncTERM, ExtraPuTTY) begin a ZMODEM send by
emitting the autostart sequence —
**\x18B, **\x18A, or
**\x18C (a hex / binary16 / binary32 ZRQINIT)
— directly into whatever menu the user is sitting at, expecting the
far end to “just receive.” The gateway's idle input loop
watches for that ZPAD ZPAD ZDLE prefix and, on a match:
ZRQINIT bytes (the sender re-sends once we ZRINIT),sz senders don't emit it. Drive the transfer manually
instead: File Transfer → U / D
→ Z.
| Situation | What happens |
|---|---|
| Any data-phase error (bad subpacket CRC 16/32, a late or garbled header, a position mismatch) | The receiver re-sends ZRPOS with the last good byte offset and the sender resumes there — recovery is by position, not block number. All of these share one counter that resets on each good subpacket and bounds consecutive errors at zmodem_max_retries (default 10) before the receiver cancels. |
| Garbled header | Answered with ZNAK (or a repeated request); the frame is re-sent. |
| Resume a partial file | On ZFILE the receiver may answer a non-zero ZRPOS; the gateway-as-sender honors it and starts mid-file. |
| Verified resume (CRC) | A receiver that wants to prove its partial copy matches before appending sends a ZCRC request (byte count N, or 0 for the whole file); the gateway-as-sender answers with the CRC-32 of those bytes. On a match the peer resumes; on a mismatch it restarts from zero — either way no corruption. (lrzsz rz --crc-check.) |
| Decline one file | ZSKIP — the gateway sends it to refuse a colliding name on receive, and honors it on send by advancing to the next file. |
| Abort | ZABORT, or the CAN (0x18) run a terminal sends on Ctrl-X; the session tears down cleanly. |
Stall (no data within zmodem_frame_timeout) | The receiver re-prompts with ZRPOS rather than abandoning the transfer on the first quiet moment, counting toward the same consecutive-error bound; the sender likewise retransmits stalled frames up to zmodem_max_retries. |
| File exceeds 8 MB | The transfer is aborted — the same cap as the other protocols. |
ZMODEM has its own independent timeout set (separate from the XMODEM family). Editable from the telnet Configuration → File Transfer menu, the GUI / web File Transfer → More… popup, or egateway.conf — changes apply on the next transfer, no restart.
| Key | Default | Meaning |
|---|---|---|
zmodem_negotiation_timeout | 45 s | Time to complete the ZRQINIT / ZRINIT handshake. |
zmodem_negotiation_retry_interval | 5 s | Gap between ZRINIT / ZRQINIT re-sends during the handshake. |
zmodem_frame_timeout | 30 s | Per-frame read timeout once the transfer is live (headers and subpackets). |
zmodem_max_retries | 10 | Retry cap for ZRQINIT / ZRPOS / ZDATA frames before a ZABORT. |
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.