valis / Reference / API reference
Transport - API reference
Exported surface for the transport subsystem. Part of the API reference.
Package valis/src/transport/client-secure-endpoint
Functions
%drive-keyed-attach
(%drive-keyed-attach host port &key (aname "") initiator initiator-static initiator-ephemeral)
Connect to the live keyed listener at HOST/PORT, drive the NoiseXX handshake over the afid SYNCHRONOUSLY (one reader on the socket), build the client-side transport session from the completed initiator (it PAIRS with the server's responder), send the still-cleartext Tattach by hand, and read the FIRST sealed frame (the Rattach record) straight off the raw stream.
The initiator is INJECTED, never hardcoded. Pass either INITIATOR — an already-constructed mercer initiator node (production: make-initiator over the owner key, fresh random ephemeral) — or the two fixed initiator keys INITIATOR-STATIC and INITIATOR-EPHEMERAL, from which the node is built via (make-vector-node :role :initiator …). The conformance test injects fixed vector keys; the operator zone client injects the real owner key; both go through this one seam. valis performs no crypto — mercer owns the node and the seal/unseal.
Returns a plist with the live endpoint/stream, the client transport session, the captured sealed Rattach, the cleartext Tattach, and the cleartext pre-attach replies, plus the completed initiator node and the three handshake messages as they crossed the afid: :endpoint :stream :client-transport-session :initiator-node :sealed-rattach :root-fid :cleartext-tattach :rversion :rauth :msg0 :msg1 :msg2 :rwrite1 :rread
Synchronous by design: seven's async reply-reader and a manual raw read of the sealed Rattach are two readers on one socket and race for the sealed record. A single in-order reader is deterministic against the single-threaded-per- connection keyed serve loop.
%encode-record-length
(%encode-record-length len)
Encode LEN as a fresh 4-byte little-endian octet vector — the cleartext record prefix the secure-endpoint writes OUTSIDE the sealed body.
%read-exactly
(%read-exactly stream n)
Read exactly N octets from STREAM, signalling on a short read.
%read-record-length
(%read-record-length stream)
Read the cleartext 4-byte little-endian record length prefix from STREAM and
return it as an integer. Rejects a declared length beyond max-record-length
BEFORE any body is read, so a hostile prefix cannot drive an unbounded
allocation. Mirrors the responder secure-endpoint's %read-record-length.
%read-sealed-record
(%read-sealed-record stream)
Read one len[4]||sealed record off STREAM, returning the sealed blob — the mirror of the secure-endpoint's read side.
%sync-req
(%sync-req endpoint msg)
Write MSG (a wire T-message struct) as one cleartext 9P frame on ENDPOINT and read+decode the single in-order reply. The keyed serve loop processes one request per connection at a time and replies in order, so a synchronous request/reply round-trip needs no async reply reader — and crucially keeps a SINGLE reader on the socket, so nothing races the manual read of the first sealed (post-attach) record off the raw stream.
client-secure-endpoint-inner
(client-secure-endpoint-inner instance)
Undocumented: this exported symbol needs a docstring.
client-secure-endpoint-install-session
(client-secure-endpoint-install-session ep session)
Install the opaque mercer initiator SESSION on EP under the write-lock, flipping it from cleartext passthrough to delegating. Mirror of the responder secure-endpoint-install-session, roles swapped.
client-secure-endpoint-p
(client-secure-endpoint-p object)
Undocumented: this exported symbol needs a docstring.
make-client-secure-endpoint
(make-client-secure-endpoint &key inner)
Wrap INNER (a seven socket-endpoint) in a passthrough client-secure-endpoint. The endpoint stays passthrough until client-secure-endpoint-install-session flips it to delegating once the initiator handshake completes.
promote-to-sealed-session
(promote-to-sealed-session attach)
Given the result plist ATTACH from %drive-keyed-attach (holding :endpoint, :stream, :client-transport-session, :sealed-rattach, :root-fid, :rversion), promote the completed keyed attach to a continuously-sealed seven session and return (values session root-handle).
Steps:
- Unseal the captured :sealed-rattach with the client transport session to confirm a valid Rattach (this is the proof the attach boundary sealed the reply and the client session pairs with the server's responder).
- Wrap the SAME connected inner socket-endpoint in a client-secure-endpoint and install the (transport-session initiator) on it, flipping it to delegating.
- Reconstruct seven's post-Tattach session state on a make-session over that sealed endpoint: stash the negotiated msize/version, set the root fid to the by-hand root fid, bump the fid counter past the live afid+root fids so a subsequent walk cannot collide with them, register the root fid-handle, and start the reply-reader thread.
valis performs ZERO crypto – mercer owns the unseal and the session orientation. The returned session and root handle drive sealed walk/read-bytes/clunk; the reply-reader unseals every reply inside read-frame.
Package valis/src/transport/keyed-acceptor
Classes
listener
The keyed 9P listener: owns the passive socket, the accept thread, and the lock-guarded registry of live per-session threads with their atomic count. RUNNING gates the accept loop and is cleared by STOP-LISTENER; DRAINING is the distinct, terminal evacuation state that refuses new sessions and tears down live ones while leaving the socket and accept thread up.
Functions
drain-sessions
(drain-sessions listener &key (mode :graceful) (timeout 5))
Actively drain LISTENER's live 9P sessions so an evacuating instance drops its clients cleanly — they reconnect to a successor elsewhere. This is the per-session half of STOP-LISTENER, decoupled from the accept-loop / passive- socket teardown: the passive socket and accept thread are left intact, but the draining flag is set so new sessions are refused and every live session is closed.
Setting the flag and snapshotting the sessions happen under one acquisition of SESSIONS-LOCK — the same lock %SPAWN-SESSION takes to re-check the flag. That makes the boundary airtight: a connection accepted just before the flag flips is either already in the snapshot (and torn down here) or refused under the lock by %SPAWN-SESSION; it cannot slip through into a live session after the snapshot.
Closing each secure endpoint reuses the serve loop's existing close -> EOF -> reap path AND zeroizes the mercer session for free (the secure-endpoint close method does both): the parked read wakes to EOF, parked reads are reaped, and the session thread exits.
MODE :GRACEFUL bounded-joins each session thread (so the caller is guaranteed teardown — reaper fired, counter settled) then prunes the drained registry entries. MODE :HARD returns without joining (async teardown; the count settles on its own as threads exit). TIMEOUT bounds each per-thread join.
Idempotent: a second call finds the flag already set and works from whatever sessions remain (possibly none) — closes and joins are no-ops. Terminal: the flag stays set; there is no resume. Returns LISTENER.
listener-draining-p
(listener-draining-p listener)
True when LISTENER is draining: new sessions are refused and live ones are being (or have been) torn down. Terminal once set.
listener-port
(listener-port instance)
Undocumented: this exported symbol needs a docstring.
listener-running-p
(listener-running-p listener)
True when LISTENER's accept loop should continue.
listener-session-count
(listener-session-count listener)
Return the current number of live session threads on LISTENER.
listener-socket
(listener-socket instance)
Undocumented: this exported symbol needs a docstring.
listener-thread
(listener-thread instance)
Undocumented: this exported symbol needs a docstring.
make-inherited-keyed-socket-source
(make-inherited-keyed-socket-source fd)
Return a function of (HOST PORT) — the KEYED-SOCKET-SOURCE rebind — that adopts the inherited, already-LISTENING descriptor FD as the keyed accept loop's passive socket instead of binding a fresh one. This is the fd→socket rebind the seam's docstring anticipates: a host agent binds the routable owner port (the privileged bind an unprivileged valis unit cannot make) and hands valis the descriptor; valis serves the owner fabric over it without a second bind.
The returned function IGNORES its HOST/PORT arguments — FD is already bound to its wire address — and wraps FD as a :passive iolib socket via MAKE-SOCKET-FROM-FD, which recovers the address family and type by getsockname on the descriptor. Because the fd arrives already in LISTEN state, there is no bind/listen step and no second bind on the same address can signal EADDRINUSE.
Borrow/close discipline by provenance: MAKE-SOCKET-FROM-FD DUPs FD, so the accept loop owns and closes its own dup while FD — the descriptor the loop did NOT create — stays open. STOP-LISTENER closes the listener's own socket (the dup); it never closes the inherited FD. Mirrors MAKE-STREAM-SOURCE-FROM-FD's stream-fd adoption, but yields the passive iolib socket the keyed accept loop's ACCEPT-CONNECTION / LOCAL-PORT / FD-OF expect.
start-listener
(start-listener port backend &key (host "127.0.0.1"))
Bind a passive TCP socket on HOST:PORT (via KEYED-SOCKET-SOURCE) and spawn an accept-loop thread that wraps every accepted stream in a secure endpoint and hands it to seven's SERVE against BACKEND on a fresh session thread. PORT 0 selects an ephemeral port; the bound port is read off the socket and recorded on the returned LISTENER struct. Returns the LISTENER for use with STOP-LISTENER.
stop-listener
(stop-listener listener &key (timeout 5))
Stop LISTENER cleanly: clear the running flag, close the passive socket (which unblocks any in-flight accept), join the accept thread, then close every outstanding session secure endpoint and join its thread. TIMEOUT bounds the per-thread join wait; threads still alive after the wait are destroyed. Idempotent.
Variables
*current-secure-endpoint*
The secure endpoint of the connection whose 9P session is being served on this thread. Bound for the dynamic extent of SERVE by %SPAWN-SESSION and read back by node-attach-identity to reach this connection's secure endpoint for the mercer session install at the attach boundary. Valid only on the session's single serve thread; NIL elsewhere.
*keyed-socket-source*
A function of (HOST PORT) returning a bound passive iolib socket for the keyed 9P accept loop. This is the single point where a wire address becomes a listening socket: a later host agent rebinds it to hand valis a pre-bound fd — the agent dials the port and passes the fd, and this becomes fd→socket — without changing start-listener or any other call site. A rebind, not a rewrite; no fd-handoff code lives here yet.
*max-concurrent-sessions*
Maximum number of concurrent session threads a single keyed listener will spawn. When reached, further accepted connections are refused (closed) until an existing session ends. 64 is conservative — enough headroom for normal multi-client use without letting a hostile peer exhaust the image's thread budget.
Package valis/src/transport/secure-endpoint
Classes
secure-endpoint
The single keyed-transport boundary wrapping a seven socket-endpoint (INNER, which still does cleartext size[4] 9P reassembly). SESSION is the opaque mercer transport-session handle; NIL means passthrough (pre-attach), and installing it flips the endpoint to sealing/unsealing every frame. WRITE-LOCK serializes the seal-and-write critical section and the install flip — mercer owns all crypto, seven consumes the opaque session contract only.
Functions
make-secure-endpoint
(make-secure-endpoint &key inner)
Wrap INNER (a seven socket-endpoint) in a passthrough secure-endpoint. The endpoint stays passthrough — delegating cleartext to the inner carrier — until secure-endpoint-install-session flips it to delegating at the attach boundary.
INNER must be a PLAIN socket-endpoint. A control-socket-endpoint (the AFLOCAL fd-passing carrier) is refused: it satisfies socket-endpoint structurally, but wrapping it here would route its frames through the keyed record framing and silently strip the SCMRIGHTS descriptor path — the keyed wrap and fd passing are mutually exclusive carriers, not composable layers.
secure-endpoint-inner
(secure-endpoint-inner instance)
Undocumented: this exported symbol needs a docstring.
secure-endpoint-install-session
(secure-endpoint-install-session ep session)
Install the opaque mercer SESSION handle on EP under the write-lock, flipping it from cleartext passthrough to delegating. Called once at the attach boundary after the handshake completes. After this every frame is sealed/unsealed.
secure-endpoint-p
(secure-endpoint-p object)
Undocumented: this exported symbol needs a docstring.