Ethernet Gateway — Protocol Commands
Kermit is a robust file-transfer protocol designed in 1981 by Frank da Cruz at Columbia University. Unlike XMODEM/YMODEM/ZMODEM, Kermit was built for environments where bytes can be lost, mangled, or stripped to 7 bits — old serial links, packet-switched networks, and bridged terminal hardware. Ethernet Gateway implements a spec-complete Kermit on both directions: as a client connecting to a remote Kermit server, and as a server idling waiting for commands from a peer.
The implementation follows Frank da Cruz, Kermit, A File Transfer
Protocol (Digital Press, 1987) plus the long-packet, sliding-window,
and streaming extensions used by C-Kermit. Real C-Kermit 10.0 interop
is verified by the test suite for both upload (send) and
download (get) directions.
| 7-bit | 7-bit links | Old serial connections, telnet bridges that strip the high bit, mainframe links — XMODEM family doesn't survive these without a layer of escaping; Kermit was designed for them. |
| flaky | Lossy connections | Per-packet ACK + sliding window + selective-repeat retransmit recovers cleanly from drops; resume-partial picks up where a broken link left off. |
| cross | Cross-platform peers | Kermit is on every retro platform that mattered: VMS, MVS, CP/M, MS-DOS, every UNIX. C-Kermit, G-Kermit, Kermit-95, Kermit-86, MS-Kermit, E-Kermit are all interoperable. |
| batch | Multi-file batches | A single S/B exchange carries many files; the spec is self-describing (filename, mtime, mode all carried in the A-packet metadata). |
Three entry paths drop a session into Kermit server mode:
K. Honors authentication, the network gating allowlist, and the per-session sandbox.kermit_server_enabled = true, the gateway binds a dedicated socket on kermit_server_port (default 2424). Every accepted connection enters protocol mode immediately — no menu, no auth gate. Drive it with kermit -j host:2424.allow_atdt_kermit = true, dialing ATDT KERMIT from the modem emulator hands the serial caller straight to the dispatcher. Aliases: kermit, kermit-server, kermit server.
The bottom two paths bypass security_enabled by design
— they exist so vintage receivers that can't paint a menu still
work. Both are off by default and have explicit GUI / telnet-menu
warnings before they enable.
However the session is launched, the server then idles waiting for
commands from the Kermit client. The peer drives the entire session
by sending Kermit protocol commands; the server processes each one
and returns to idle until the peer sends finish,
bye, or disconnects.
Files sent to the server are saved into transfer_dir
(or its current sub-directory if the peer changed it via
remote cwd). The server returns a per-file
✓/× summary when the session ends.
| Type | From peer | Server response |
|---|---|---|
S | Send-Init starting an upload | Y-ACK with our capabilities, then drives the receive flow (F/A/D…/Z/B). |
R | Receive-request: peer wants a file from us | Y-ACK to R, then sends S+F+A+D…+Z+B for the requested file. |
I | Init: peer asks us to advertise capabilities | Y-ACK with a fresh Send-Init payload listing our caps. |
G | Generic command (F/L/B/I/X/C/D/$/K/H/?/E/R/T/m/d) | See the G subcommand reference. |
C | Host command (run a shell command) | Refused with E-packet "Host commands disabled". Always. |
E | Error packet from peer | Logged; session ends silently (per spec, E is fatal both ways). |
B | EOT — end of transmission | ACK; session ends cleanly. |
C packets are a remote-code-execution primitive by
design. The server emits an E-packet "Host commands disabled" and
keeps idling. There is no configuration switch to enable them —
actually executing peer-supplied shell commands is out of scope
and unsafe regardless of who you trust.
These are the commands you type at the C-Kermit prompt (or any spec-compatible Kermit client) once connected to the gateway in server mode. The wire-protocol packet type the client emits is shown in the second column — useful when reading verbose logs or comparing with the packet-type reference.
| Command | Wire | What it does |
|---|---|---|
send <file>aka put |
S | Upload a file from your local system into the gateway's current effective directory (transfer_dir + any cwd you set this session). Wildcards and multi-file batches work — send *.bas sends every match in one S/B exchange. |
get <file> |
R | Download a file from the gateway's current effective directory to your local system. The filename is path-traversal-validated server-side; an unsafe name returns an E-packet and the server stays idle for the next command. |
remote dir |
G D | List the gateway's current effective directory, one entry per line as name <TAB> size|<dir>. Hidden files (leading dot) are filtered out. Long listings are paginated automatically across multiple X-packets. |
remote cwd <subdir>aka remote cd |
G C | Change the gateway's working sub-directory for the rest of this session. Resolved relative to transfer_dir. Special cases:
.. as a component anywhere except as the bare CDUP argument, leading dot, backslash, NUL) is refused. |
remote space |
G U | Report free disk space, in bytes, at the current effective directory. Returns unknown on platforms without statvfs. The wire letter is U (Frank da Cruz Generic Command Letters §6 Table 6-2: "disk Usage") — that's what real C-Kermit emits via setgen('U', …). The gateway also accepts the legacy $ letter for backward compatibility with our own client helper. |
remote kermit <text> |
K |
Ask the gateway to identify itself. Replies with Ethernet Gateway Kermit 0.6.2 regardless of the text you supply, delivered via the same X+Z inverse-transfer pattern as remote dir.
Argument is required. If you type
Wire shape note: this is a top-level K-packet (the user's command rides in the payload), not a G-K subcommand. C-Kermit builds the packet via |
remote help |
G H / G ? | Display the gateway's supported-subcommand list. Both H and ? are accepted on the wire so older clients work too. |
remote delete <file>aka remote era, rdel |
G E | Delete a file in the gateway's current effective directory. Filename is path-traversal-validated; missing or unsafe names return an E-packet. Wildcards are not expanded — operate on one filename per call. |
remote rename <old> <new>aka rrename |
G R | Rename a file in place. Both names are field-encoded per spec §6.7 and validated separately; if the destination already exists the server refuses with an E-packet rather than silently clobbering it. |
remote type <file>aka rtype |
G T | Display a file's contents on your terminal. Server delivers the file via the same X+Z inverse-transfer pattern as remote dir. Intended for text files — sending a binary will spew control bytes to your terminal, just like real Kermit. |
remote mkdir <dir> |
G m | Create an empty subdirectory under the current effective directory. Wire letter is lowercase m per C-Kermit's setgen('m', …) — distinct from M (which the spec reserves for remote message). Single-component name only; nested-path creation is refused. |
remote rmdir <dir> |
G d | Remove an empty directory. Lowercase d wire letter — distinct from D (DIRectory listing). Non-empty directories return an E-packet refusal; the operator must remote delete the contents first. |
finish |
G F | End the protocol session and return the gateway to the File Transfer menu. Your client stays connected at the telnet/SSH layer — most polite end-of-session. |
logout |
G L | End the protocol session. Semantically equivalent to finish at the wire level — some hosts treat it as "log out the user" but the gateway treats both the same. |
bye |
G B | End the protocol session. Some clients additionally drop the underlying connection after sending bye; the gateway-side effect is identical to finish. |
copy, host, set, who, print, message, query, status?
These don't fit a file-transfer gateway: host is a
remote-shell primitive (security risk), who /
set / login assume a multi-user host
the gateway doesn't model, and copy overloads the
K wire letter with KERMIT-identity (the dispatch
distinction needs special-casing on argument count, not landed
yet). The server refuses every unsupported subcommand with an
E-packet per spec §6, so your client surfaces a clean "command
not supported" error rather than the silent-ACK no-op that
pre-2026-05 builds emitted.
get filenames and remote cd sub-directory
arguments are validated before any disk I/O. Any name containing
.., leading dots, separator characters, or NUL bytes
is refused with an E-packet, and the server keeps idling for the
next command (per spec §6.7).
X, and on some legacy clients
truncated to 8.3. Frank da Cruz adopted the convention so a
file named on a VMS/MVS/Unix box would still land safely on
a CP/M target. The gateway saves whatever the F-packet says,
so put hello.txt from a default C-Kermit
becomes HELLO.TXT in transfer_dir.
To keep the original case and full length, run this
once in your C-Kermit before the transfer:
C-Kermit> set file names literal
That switches both the sent F-packet (your put)
and the received F-packet (incoming get) to
pass the name through unmodified. You can pin it permanently
in ~/.kermrc. Kermit-95 has the same setting
under Settings → File Names → Literal;
G-Kermit and E-Kermit have no toggle and always send the
name unmodified.
Every byte on a Kermit connection belongs to a packet. All packets share the same frame: a MARK byte (SOH, 0x01), length and sequence fields, a one-character TYPE, the payload, a block-check (checksum or CRC), and an end-of-line terminator. The TYPE byte determines what role the packet plays — whether it's negotiating capabilities, carrying file content, acknowledging a previous packet, or signalling an error. Below is what each TYPE letter means and why it exists.
MARK LEN SEQ TYPE DATA… CHECK EOL
\x01 N+3 s T payload c1[c2c3] \r
LEN and SEQ are tochar()-encoded
(value+0x20) so every framing byte stays in the
printable ASCII range — the protocol survives
7-bit, control-stripping links by construction. SEQ is
a 6-bit counter that wraps mod-64 across every packet
in the conversation, including ACKs and NAKs.
The opening packet of every transfer. Its payload is the sender's capability proposal: maximum payload length, packet timeout, padding requirements, end-of-line terminator, control-quote character, 8-bit-quote character, block-check type, repeat-count prefix, plus a CAPAS bitmap advertising long packets, sliding windows, attribute packets, locking shifts, and streaming. The receiver replies with its own Send-Init in the Y-ACK; the intersection of the two proposals governs the rest of the transfer.
Names the file the sender is about to deliver. The payload is the filename, plain (un-quoted) — most implementations don't escape it because filenames rarely contain QCTL or control bytes. In a multi-file batch, each file gets its own F between the initial S and the closing B; the receiver opens a new output file each time.
Carries metadata about the file that just
arrived in F: size, modification time, mode bits,
encoding, record format, system-id, charset, creator,
account, and ~10 other typed sub-fields (each
introduced by a single-byte tag — ! for
length, # for date, + for
mode, " for binary-vs-text, and so on).
Optional but useful: it's how mtime and mode survive
the transfer end-to-end. The receiver's Y-ACK to the A
can also carry disposition='R' + an
offset to ask the sender to resume from a
partial file already on disk.
One chunk of file content, sized up to the negotiated
MAXL. Control bytes are quoted with QCTL, optionally
8-bit-quoted with QBIN to survive 7-bit links, and
optionally repeat-compressed (a run of N identical
bytes encodes as ~ count byte). One or
more D-packets sit between the A (or F if no A was
sent) and the closing Z. In streaming mode the per-D
ACK is suppressed; otherwise every D gets a Y-ACK
before the next is sent.
Marks the end of one file. Empty payload on a normal
finish; D in the payload signals
"discard" — the sender aborted mid-file and the
receiver should drop what it has rather than save a
truncated file. After Z the sender either starts the
next file with a new F (still inside the same
session) or closes the session with B.
Ends the entire send session. Empty payload. The
receiver Y-ACKs and both sides return to idle. In
server-mode dispatch, B is also accepted as a
standalone "end of session" signal — the same effect
as a G F finish.
Affirmative acknowledgement. The receiver sends a Y for every non-streaming packet from the sender, echoing the same SEQ. Most Y-packets have an empty payload, but two are special: the Y to the initial S carries the receiver's counter Send-Init (its own capabilities), and the Y to A can carry a disposition + offset to drive RESEND.
Negative acknowledgement: the receiver got a corrupt packet, an out-of-order packet, or timed out waiting for the next expected SEQ. The sender retransmits the packet at the NAK'd SEQ. With sliding windows, NAK drives selective-repeat — only the missing packet is re-sent, not everything after it.
Fatal abort. Payload is a human-readable error message, control-quoted. Sent in either direction when something irrecoverable happens (filename refused for path-traversal, file too large, host commands disabled, peer abort). The recipient of an E does not reply — per spec, ACKing an E risks an error loop. The session ends silently after the receiver logs the message.
Client-side download request. Payload is the filename
the client wants from the server. The server validates
the filename, looks the file up under the current
transfer directory, and (if it exists) plays the role
of sender for the rest of the exchange — its
S follows the R in the same monotonic SEQ stream
rather than restarting at zero. R is what
C-Kermit's get command emits.
Server-mode command dispatch. Payload is a single
action byte (F/L/B
end the session, C changes directory,
D lists the directory, $
reports free space, K identifies the
server, H or ? shows help)
optionally followed by an argument. Short replies
(CWD, finish) are a single Y-ACK; long replies (DIR,
HELP) drive an inverse file transfer back to the peer
— see the G-subcommand
reference for the per-letter semantics.
Mid-session re-initialisation. Same payload shape as S, but it doesn't start a transfer — the peer just wants a fresh Send-Init to refresh its picture of our capabilities. C-Kermit emits an I before each server-mode command. The server replies with its current caps in the Y-ACK and keeps idling.
Same role as F, but flags the body as
text-for-display rather than save-to-disk
(Frank da Cruz §5.3). Used when the server is
answering a generic-command text query
(remote dir, remote space,
remote help): the body comes back as a
full inverse file transfer (S → X → D…D → Z → B), and
a spec-aware peer prints it on screen instead of
writing it to a file.
A request for the server to execute an arbitrary shell command. This is a remote-code-execution primitive by design, and the gateway always refuses it with an E-packet "Host commands disabled". No configuration switch enables host commands — actually running peer-supplied shell commands is out of scope and unsafe regardless of who you trust.
Reserved by the spec for "I timed out reading the line" — a way for the receiver to admit the line went quiet without speculating about why. Never appears on the wire in this implementation; NAK serves the same role and is what every modern peer expects.
Reserved by the spec for future use. Not emitted by the gateway and not handled in dispatch — receiving a Q falls into the unknown-type fallback (NAK back at the expected SEQ to drive a retransmit).
S → Y → F → Y → A → Y → D → Y → … → D → Y → Z → Y → B → Y.
The same shape repeats per file in a multi-file batch
— F-headed sub-transfers chained between the opening
S and the closing B. A download is the same wire
sequence preceded by an R from the client. A
server-mode session is a loop of these exchanges
interleaved with G commands, terminated by
G F / B or peer disconnect.
Generic-command (G) packets carry a single action byte
followed by an optional argument. The server recognises every
spec-defined subcommand and replies appropriately.
| Letter | Name | Reply shape | Notes |
|---|---|---|---|
F | Finish | ACK, exit | End the protocol session. Most polite end-of-session signal. |
L | Logout | ACK, exit | Same as F at the protocol level. |
B | BYE | ACK, exit | Same as F. Some clients use BYE to also signal "drop the connection". |
C | CWD | ACK on success, E on failure | Field-encoded argument is a relative subdir. Special cases: empty argument resets to transfer_dir root; bare .. pops one component (cdup, no-op at root). Other path-traversal forms (foo/.., leading dot, backslash, NUL) are refused. Targets that don't exist on disk are refused so typo'd cd fails fast. |
D | Directory | ACK + X+ + Z | Returns one entry per line: name <TAB> size|<dir>, sorted, hidden files skipped. Paginated across multiple X-packets when needed. |
$ | SPACE | ACK + X + Z | Returns free-bytes count as a decimal string, or unknown on platforms without statvfs. |
K | KERMIT | ACK + X + Z | Returns server identity + version (Ethernet Gateway Kermit 0.6.2). |
H / ? | Help | ACK + X+ + Z | Returns the list of supported subcommands. Both H and ? are accepted for compatibility. |
E | Erase / Delete | ACK on success, E on failure | Field-encoded filename per spec §6.7. Path-traversal rejected; missing files return E-packet (not silent ACK). One filename per call — no wildcard expansion. |
R | Rename | ACK on success, E on failure | Two field-encoded names back-to-back: source then destination. Server refuses if the destination already exists, to avoid silent clobber. |
T | Type | ACK + X+ + Z | Field-encoded filename. Server delivers file contents via inverse transfer. Intended for text — sending a binary will spew control bytes to the client's terminal. |
m | Mkdir | ACK on success, E on failure | Lowercase per C-Kermit ckuus7.c:8139. Field-encoded directory name; single component only. E-packet on collision (existing dir) or invalid name. |
d | Rmdir | ACK on success, E on failure | Lowercase — distinct from D (DIR). Removes only empty directories per std::fs::remove_dir semantics; non-empty returns E-packet so the operator clears contents intentionally. |
I | Logout | ACK, exit | What C-Kermit's remote logout sends (setgen('I', …)). Same effect as F/L/B at the gateway. Distinct from the I-packet (TYPE_INIT) which is a different packet type. |
X | Exit | ACK, exit | What C-Kermit's remote exit sends. Same effect as F/L/B/I at the gateway. |
| other | (unsupported) | E-packet | Per spec §6, unsupported subcommands return an explicit refusal so the peer's UI surfaces a real error. Pre-2026-05 builds silently ACKed and looked like phantom successes. |
Where the reply is ACK + X+ + Z, the body of the
response is delivered in one or more X-packets followed by a
single Z to terminate. Long responses (e.g. a directory with many
files) are paginated to fit within the classic 94-byte MAXL so
the response works against strict-spec peers that haven't
negotiated long packets.
Both peers exchange a Send-Init (S) packet at
the start of a transfer. Each side advertises what it can do; the
intersection is what the actual transfer uses. Below are the
capabilities the gateway advertises and what each one buys you.
| Capability | What it does | Default |
|---|---|---|
| Long packets | Up to 9024-byte payloads instead of the classic 94-byte cap. Massive throughput win for clean links. | On |
| Sliding windows | Up to 31 outstanding packets in flight. Hides round-trip latency; peer NAKs trigger selective-repeat retransmit per spec §5.5. | On (window=4) |
| Streaming | Sender skips per-packet ACKs entirely on reliable links (TCP/SSH). Peer only sends NAK on error. | On |
| Attribute packets (A) | Carries file size, mtime, mode, encoding, record format, creator ID, and ~10 other typed metadata fields. | On |
| Repeat compression | Runs of identical bytes are encoded as ~ count byte. Cheap RLE for spaced-out output. |
On |
| CRC-16 block check | 16-bit CRC per packet (polynomial 0x8408, reflected CCITT) instead of the classic 6-bit checksum. | Type 3 |
| RESEND | Resume a partial upload from where the previous attempt left off — receiver advertises disposition='R' + offset in the A-packet ACK. |
Off (opt-in) |
| Locking shifts | SO/SI region markers for 8-bit data over 7-bit links instead of QBIN's per-byte prefix. Spec §3.4.5; rarely used by modern peers. | Off (opt-in) |
disposition='R' would re-send a file from byte 0 while
the receiver pre-loaded partial bytes — corrupting the result.
Enable kermit_resume_partial = true in
egateway.conf only when both ends are known compatible.
Set in egateway.conf; changes take effect on the next
Kermit session.
| Key | Default | Effect |
|---|---|---|
kermit_negotiation_timeout | 300 | Seconds for the Send-Init handshake. |
kermit_packet_timeout | 10 | Per-packet read deadline after Send-Init. |
kermit_idle_timeout | 300 | Seconds the gateway's server-mode dispatch waits between commands from the peer before sending an "idle timeout" E-packet and disconnecting. Set to 0 to disable — server idles indefinitely. |
kermit_max_retries | 5 | Maximum NAK / timeout retries per packet before giving up. |
kermit_max_packet_length | 4096 | MAXL we advertise (10..=9024). Higher = more throughput, more retransmit cost on a flaky line. |
kermit_window_size | 4 | Sliding-window depth (1..=31). 1 = stop-and-wait. |
kermit_block_check_type | 3 | 1 = 6-bit checksum, 2 = 12-bit, 3 = CRC-16/KERMIT. |
kermit_long_packets | true | Advertise long-packets capability. |
kermit_sliding_windows | true | Advertise sliding-window capability. |
kermit_streaming | true | Advertise streaming-Kermit (no per-packet ACKs). Big speed win on TCP/SSH. |
kermit_attribute_packets | true | Advertise A-packet (file metadata) support. |
kermit_repeat_compression | true | Use REPT compression for runs of identical bytes. |
kermit_8bit_quote | auto | auto (only when peer asks), on, or off. |
kermit_resume_partial | false | Resume partial uploads via disposition='R' in the A-packet ACK. |
kermit_resume_max_age_hours | 168 | Ignore on-disk partials older than this. 168 = one week. |
kermit_locking_shifts | false | Advertise SO/SI region-shift quoting for 8-bit transit on 7-bit links. |
kermit_server_enabled | false | Bind a dedicated TCP listener on kermit_server_port. Every accepted connection drops straight into Kermit server mode — no telnet menu, no auth gate. Bypasses security_enabled; off by default. |
kermit_server_port | 2424 | TCP port for the standalone Kermit-server listener. Used only when kermit_server_enabled = true. |
allow_atdt_kermit | false | Permit the serial modem emulator to recognise ATDT KERMIT (alt: kermit, kermit-server, kermit server) as a dial target that drops the caller straight into the Kermit dispatcher. Bypasses security_enabled; off by default. |
$ kermit -B -j 192.168.1.160:2323 -i -q -g report.txt -a /tmp/report.txt
-B batch mode, -j host:port opens a
telnet-protocol TCP connection, -i binary (image)
transfer, -q quiet, -g remote-name get
the named file, -a local-name save it locally as a
different name. The gateway must be in Kermit Server Mode
(File Transfer menu → K) when ckermit dials in.
$ kermit -B -j 192.168.1.160:2323 -i -q -s ./localfile.bin
By default the file lands as LOCALFILE.BIN on
the gateway — C-Kermit uppercases filenames in the F-packet
for cross-platform safety. To preserve the exact case and
avoid truncation on legacy clients, send a -C
one-shot command first to flip the file-names mode to
literal:
$ kermit -B -j 192.168.1.160:2323 -i -q -C "set file names literal, send ./localfile.bin"
$ kermit
C-Kermit> set host 192.168.1.160:2323 /telnet
C-Kermit> set file names literal
C-Kermit> remote kermit version
Ethernet Gateway Kermit 0.6.2
C-Kermit> remote dir
alpha.bin 1024
beta.bin 2048
docs <dir>
C-Kermit> remote cd docs
C-Kermit> put hello.txt
C-Kermit> get readme.txt
C-Kermit> finish
C-Kermit> exit
The set file names literal line keeps
hello.txt from being uppercased to
HELLO.TXT in transfer_dir.
Drop it if you want the default common-form rewriting.
Each put commits to disk as soon as the
gateway ACKs your B-packet, so files appear in
transfer_dir immediately —
finish is just for closing the session
politely.
Interactive C-Kermit needs a controlling tty for some commands —
if you're scripting, keep using -g / -s
action flags or wrap with script -qc to provide a
pseudo-terminal.
send and get.