Under the hood

Protocol & security boundaries

Admission, encrypted datagrams, reliable delivery, sessions, and process isolation.

Source snapshot 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:

OffsetSizeField
04ASCII PSH1
41Packet type
51Reserved, zero
616Random 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:

TypeBody
RELIABLE=1Reliable transport packet
PATH_CHALLENGE=232-byte unpredictable token
PATH_RESPONSE=3Echoed token
PING=4 / PONG=5Empty

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:

OffsetSizeField
02ASCII PS
21Version 1
31DATA=1, ACK=2, PROBE=3
42Payload length, at most 1,024
62Reserved, zero
88Delivery sequence, zero for ACK/PROBE
168Next missing sequence (cumulative ACK)
248SACK bitmap relative to cumulative ACK
324Receive 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.

END OF DOCUMENT / psh/3All documentation