Under the hood
Protocol & security boundaries
Admission, encrypted datagrams, reliable delivery, sessions, and process isolation.
60c72c2bView original On this page
This document describes the implemented experimental protocol. Integer fields on the network use big-endian encoding. Native C IPC structures are confined to local Unix sockets and never appear on the Internet.
Admission and encrypted datagrams
Every UDP payload is at most 1,200 bytes and begins with a 22-byte envelope:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | ASCII PSH1 |
| 4 | 1 | Packet type |
| 5 | 1 | Reserved, zero |
| 6 | 16 | Random connection identifier |
Types are INIT=1, COOKIE=2, CONNECT=3, ACCEPT=4, RECORD=5. INIT includes 40 padding bytes. COOKIE returns an 8-byte monotonic expiry and 32-byte HMAC-SHA-256 binding the expiry, CID, source address, port, IPv6 scope, and contacted local address/zone. CONNECT echoes that cookie; its validity window is 30 seconds. COOKIE is the same size as INIT. ACCEPT has no body. Successful validation permits allocation of a worker, subject to a maximum of 16 pending authentications and 64 total connections.
RECORD contains DTLS ciphertext. The CID routes datagrams but supplies no proof of identity. The supervisor checks fixed sizes and relays ciphertext; it does not parse TLS. A network worker uses datagram memory BIOs and explicit source metadata, with no socket-library automatic peer-address updates.
The supervisor obtains each packet’s local destination from checked IPv4/IPv6 packet-info metadata. Admission replies and all subsequent DTLS replies use that same local address, including on wildcard listeners. Connection routing also checks the local endpoint. Only link-local IPv6 endpoints retain an interface zone; global addresses leave egress routing free to follow a roaming client. Workers cannot select or change their connection’s local endpoint.
DTLS is restricted to version 1.3, ALPN psh/3, mandatory hybrid
X25519MLKEM768, Ed25519, and
TLS_CHACHA20_POLY1305_SHA256. Tickets, resumption, early data, legacy fallback,
and compression are disabled. The server presents a raw public key. TLS proves
possession; the client independently enforces its configured/accepted host pin
before revealing its client identity or initial session request.
Both endpoints verify the negotiated hybrid group before confirming DTLS or
exporting authentication material. ALPN psh/3 requires paired Ed25519 and
ML-DSA-87 identities and new host pins; older psh/1 and psh/2 peers cannot connect.
The host proves Ed possession in TLS and then supplies a mandatory connection-bound
ML proof. The client verifies and trusts the complete pair before sending OPEN.
See the authentication contract
for exact state and trust rules.
Hybrid key shares require handshake fragmentation. OpenSSL performs DTLS fragmentation/reassembly within the unchanged 1,200-byte UDP ceiling (1,178 bytes for DTLS plus the envelope). Input and output BIO rings are each capped at 64 KiB. The 16 KiB certificate-list cap yields a 16,716-byte generic handshake-message ceiling in the pinned no-compression build. OpenSSL also bounds future-message buffering to ten ahead and ACK retention to 1,023 record numbers. The handshake deadline remains 15 seconds. See the detailed limits .
The client requests DTLS KeyUpdate for both traffic directions after one hour or 1,048,576 authenticated application records sent and received combined, as observed by the client. Server ciphertext lost before reaching the client does not advance this counter; it is not a strict per-server encryption-operation ceiling under loss. The server responds without independently scheduling updates: overlapping requested updates fail in the pinned OpenSSL DTLS state machine. The client side also accepts a requested response that arrives before its preceding ACK; the server side does not. A response proves the server received the client’s request even if that ACK was lost. An in-progress TLS exchange or pending write completes before the next request.
Both endpoints must be updated for this coordination policy. Older servers still schedule potentially overlapping requests; older clients lack the combined record trigger. KeyUpdate derives secrets from existing secrets; it does not perform another hybrid key exchange or recover from traffic-secret compromise. The exact retry buffer survives a KeyUpdate that temporarily blocks application writes while waiting for a DTLS flight ACK.
Encrypted application records contain a one-byte discriminator:
| Type | Body |
|---|---|
| RELIABLE=1 | Reliable transport packet |
| PATH_CHALLENGE=2 | 32-byte unpredictable token |
| PATH_RESPONSE=3 | Echoed token |
| PING=4 / PONG=5 | Empty |
A decrypted datagram from another client address creates a candidate path. The worker challenges that exact address, caps responses to three times authenticated bytes received on that candidate, and promotes it only on the matching encrypted response. Reliable metadata from candidates is discarded. Timed-out candidates are replaced; the old path remains the destination until promotion. Congestion and RTT estimates reset after promotion. Server address changes are not supported.
Reliable stream
Reliability is above DTLS. Retransmissions carry the same delivery sequence number in fresh DTLS records, preserving record replay protection.
The 36-byte reliability header is:
| Offset | Size | Field |
|---|---|---|
| 0 | 2 | ASCII PS |
| 2 | 1 | Version 1 |
| 3 | 1 | DATA=1, ACK=2, PROBE=3 |
| 4 | 2 | Payload length, at most 1,024 |
| 6 | 2 | Reserved, zero |
| 8 | 8 | Delivery sequence, zero for ACK/PROBE |
| 16 | 8 | Next missing sequence (cumulative ACK) |
| 24 | 8 | SACK bitmap relative to cumulative ACK |
| 32 | 4 | Receive credit, in whole 1,024-byte slots |
There are 64 send slots and 64 receive slots per direction. Payload bytes are delivered once, in sequence order, with partial application reads. ACK/SACK references to unsent data, invalid credit, invalid flags, and impossible sequence numbers are rejected before any state mutation. ACKs do not trigger ACK loops; bounded probes recover a lost receive-window update.
Congestion control uses a four-segment initial window, Reno-style growth and loss reduction, paced sending, smoothed RTT/variance, Karn sampling, fast loss detection, and exponentially backed-off retransmission timers (1–60 seconds). Receiver credit and congestion windows independently constrain sending.
Session frames and user authentication
The reliable byte stream carries a type byte, a 32-bit payload length, then payload. Frame types are OPEN=1, CHALLENGE=2, PROOF=3, READY=4, STDIN=5, STDOUT=6, STDERR=7, EOF=8, RESIZE=9, SIGNAL=10, EXIT=11, CLOSE=12, FAILURE=13, INPUT_CREDIT=14, HOST_PROOF=15. Lengths, ordering, and phase transitions are checked; only one OPEN is permitted.
HOST_PROOF=15 is sent before OPEN. Its body is the 2,592-byte ML-DSA-87 host public key, a fresh broker host nonce[32], and an ML signature[4627]. The client combines that public key with the actual Ed25519 TLS peer key, verifies the 161-byte host transcript against its own CID/exporter, and accepts complete-pair trust before revealing any identity or command. It is not TLS client-certificate authentication.
OPEN carries: PTY flag (1 byte), rows/columns (2 bytes each), username length (1 byte), command length (2 bytes), Ed25519 client public[32], ML-DSA-87 client public[2592], username, command. Usernames are below 64 bytes and commands at most 8192 bytes, without NUL. Empty command requests a login shell. Maximum OPEN is 10887 bytes and maximum frame payload is 11015 bytes.
The broker receives an exporter once during HOST_AUTH_BEGIN (IPC18), after its restricted TLS signing phase, and constructs one HOST_PROOF (IPC19) itself from its loaded host pair, supervisor CID, that immutable exporter and a fresh nonce. Later AUTH_BEGIN carries only exact OPEN, never replacement exporter material. The broker independently authorizes the complete client pair from one file and creates a separate 32-byte, single-use challenge. PROOF contains Ed signature[64] then ML-DSA-87 signature[4627]. Both verify over this identical transcript:
"psh-client-auth-v2\0"
|| SHA512("psh-identity-ed25519-mldsa87-v1\0" || host_Ed[32] || host_ML[2592])[64]
|| connection_id[16] || dtls_exporter[32] || broker_client_nonce[32]
|| SHA512(exact_OPEN_bytes)[64]
Domains include exactly one NUL. The exporter label is EXPORTER-psh-auth-v2, with
CID context. The client constructs its transcript independently. Challenges allow
one proof attempt and expire after 15 seconds, subject to the unchanged 65-second
outer admission limit. Wrong phases, missing components, repeated requests and
mismatched bindings terminate authentication. Workers cannot authorize themselves
or change the requested account/command. The exporter itself remains a worker
assertion; the broker does not independently verify TLS completion or state.
READY contains a big-endian uint32 initial stdin credit of exactly 8,192 bytes. INPUT_CREDIT contains a nonzero big-endian uint32 increment, at most 8,192. Clients reserve this application credit before reading local input and may send only that many STDIN payload bytes. The broker independently subtracts received bytes and returns credit only after writing them to the process. It keeps at most 8,192 pending input bytes and continues reading worker IPC while the process blocks, allowing SIGNAL, CLOSE, and RESIZE to make progress. Credit is separate from transport flow control; keepalives alone do not return it. The client rejects credit that would exceed the window, and the broker closes the session on empty STDIN, over-credit input, repeated EOF, or any STDIN after EOF. If a pipe reader closes stdin, the broker discards subsequent credited input and returns its credit while continuing to drain stdout/stderr and EXIT. PTY input hangup follows the same discard policy, retaining the shared master for final output and EXIT. A synthetic VEOF byte never consumes or returns client input credit.
EOF has no payload and consumes no credit. The broker delays pipe closure until all earlier input is written; PTY EOF inserts one VEOF byte only after earlier input drains. EOF and session controls can be sent with zero input credit. READY and INPUT_CREDIT have the same four-byte payload on local worker/broker IPC (types 7 and 17); the privileged broker owns the credit ledger independently of worker assertions. Worker and broker executables must match.
These changes require ALPN psh/3; there is no legacy or single-component
fallback. Other frame numbers and credited-input behavior remain unchanged.
Data frames are byte strings. RESIZE contains two uint16s; SIGNAL contains one uint32 from an allowlist. EXIT is a uint32 shell exit status (signal termination maps to 128+signal). The broker drains output before EXIT. CLOSE acknowledges session completion; duplicate transport packets cannot repeat input or launch another command.
Process and resource isolation
The root supervisor owns UDP listeners, configuration, host key, and connection
lifecycle. Each admitted connection gets a privileged broker and a worker that
execs as UID/GID65534 with no supplementary groups and no_new_privs.
Workers are non-dumpable and install a Linux syscall filter that denies process
creation, exec, process inspection, signal sending, and new sockets. This prevents
a compromised worker from signaling or inspecting same-UID siblings. The filter
is an additional boundary, not a complete filesystem sandbox. UID65534 is
reserved for workers and cannot be a remote login target. Worker/session-relay
standard input/output are redirected to /dev/null; inherited diagnostics are
write-only, so an administrator’s controlling terminal is never readable by them.
The worker holds a custom OpenSSL signing handle containing only the host public key and an IPC callback. The broker permits at most two correctly framed SHA-256 TLS1.3 server CertificateVerify signatures, before user authentication; it never exposes an arbitrary signing or private-key-export operation.
After valid client authentication, the broker forks a child, establishes PTY or pipes, initializes groups, drops UID/GID, closes inherited descriptors, changes to the account home, and execs its configured shell with a controlled environment. The broker then drops to that same account and execs a fresh session relay, removing inherited host-key memory. The relay retains only process descriptors and its worker IPC socket. Root login, expired accounts, and nologin shells fail.
The relay, worker, and stream use bounded queues. Slow consumers propagate backpressure to process pipes. Keepalives do not depend on application progress. Unresponsive peers expire after 120 seconds; process groups receive bounded HUP/TERM/KILL cleanup. Child exit is observed without reaping until cleanup, preventing PID/process-group reuse during signaling.
Reload validates the complete configuration and keys first. Revoked identities and pending authentications are terminated; host-key replacement closes all existing sessions. Invalid configuration or changed listeners preserve the previous configuration. Logs omit terminal contents and private-key material.
The supervisor also owns a private cgroup v2 directory for each connection.
Linux clone3(CLONE_INTO_CGROUP) places the broker there atomically, before it
can create a worker or shell. Descendants inherit membership; no writable cgroup
handles survive into workers, relays, or shells, and the directory remains root
owned. On forced retirement the supervisor uses cgroup.kill, independently of
relay cooperation, process-group/session changes, or numeric shell PID reuse.
It reaps the broker and verifies the scope is empty before removing the directory
and recycling the connection slot. Normal completion retains the brief worker
drain window before this final cleanup. Processes stuck in uninterruptible kernel
sleep may delay emptying a scope; shutdown still has its existing bounded wait.
The daemon requires Linux 5.14 or newer with cgroup v2, clone3 with
CLONE_INTO_CGROUP, and permission to create root-owned child cgroups under
/sys/fs/cgroup and write cgroup.kill. Unsupported or restricted environments
fail closed when admitting connections; they do not fall back to cooperative
cleanup. Separate random directory names allow concurrent daemon instances.