§11 Networking
Status: finalized (v0.8.1, 2026-09-21) — merged into the spec freeze surface along with the server roadmap's S0 batch; subsequent revisions are recorded as v0.8.x notes. v0.8.1 = write-back of semantics left over from the P3/P4 final review (additive: the four landed AF_UNIX items into §11.2, the resolve pool semantics into §11.5, and a new TLS facade semantics pointer as §11.9); it does not alter the v0.8 frozen surface. Implementation anchors: tests/modules/caps_net (E4010), tests/net/ (behavior). Execution-model semantics belong to §7; this chapter defines the network facade, transport semantics, and the HTTP profile layering. Normative conventions (MUST / MUST NOT / SHOULD / MAY) follow the spec README.
11.1 Capability Keys (Normative)
- All network access goes through capability objects; the package-level
[caps](§2.7) declares the upper bounds, the set the program actually uses ⊆ the declared set, and exceeding them = E4010:
| Key | Granted surface | Consequence without the key |
|---|---|---|
net.listen |
bind + accept for TcpListener/UnixListener |
E4010 |
net.connect |
TcpStream/UnixStream connect, UdpSocket |
E4010 |
net.resolve |
DNS resolution (§11.5) | E4010 |
- A
#[pure]function (§8) touching any network facade = E4020 (rejected at compile time). - Capability objects cannot be constructed, nor copied out of the grant chain; connection handles derived from
listener.accept()inherit the listener's authorization and do not consumenet.connectagain. - For the database-access
db.connectkey, see §12.1.
11.2 Addresses and the Socket Facade (Normative)
let ln = caps.net.listen(TcpListener.bind("127.0.0.1:0")?) // :0 = kernel-assigned port
let conn = ln.accept()? // -> TcpStream (inherits authorization)
let n = conn.read(var buf)? // buf: Byte[] / T[N]
conn.write(bytes)?
conn.shutdown(Write)?
let addrs = caps.net.resolve(Dns.name("example.com")?) // List[SocketAddr], dual-stack
SocketAddr: IPv4/IPv6 dual-stack (no v6 discrimination); two forms, literal and resolved.- Value-type handles:
TcpListener,TcpStream,UdpSocket,UnixListener,UnixStream(the Unix family is available only on posix targets; referencing them on a windows target is a compile error, E). Allimpl Drop(§6.4): deterministic close on scope exit; classes must not hold them (the §6.2 hard rule applies verbatim). - Semantic shape: facade APIs always have "blocking semantics" — read/write/connect with no callbacks and no Futures; actual concurrency is carried by the runtime (§11.4).
- The underlying file descriptors /
SOCKEThandles are forbidden to be touched directly by programs (the emission surface refuses to export them; E anchors are registered with the wave that closes off the emission surface — see the divergences server surface for the register).
11.2.1 The Four Landed AF_UNIX Items (v0.8.1 note; implementation anchors: the std/net.ct P3-E surface + tests/net/unix_sock)
On top of this profile's frozen semantics, the Unix-family handles (UnixListener/UnixStream) pin down the following four landed items (one consistent policy across platforms; silent divergence is not allowed):
- Path limit =
min(sizeof sun_path)= 104 bytes (darwin 104 / linux 108, including NUL):strlen(path) >= 104→EINVAL(the shim sets the code uniformly, same shape across platforms; the native per-platform differences are not exposed). - Unlinking a stale socket file before bind = last binder wins: when a listener is created, an existing path is
unlinked first (ENOENT is the normal case and is ignored) — leftover files self-heal, with no alive/dead discrimination (the race surface where a live listener is displaced by a later binder belongs to caller orchestration; the shim does not arbitrate). - Drop only closes the fd, never removes the socket file: the handle's RAII scope exit only deterministically closes the descriptor; "the last Drop removes the path" would wrongly remove a successor's live path (an inevitable consequence of last-binder-wins semantics), so the file's lifecycle belongs explicitly to the caller.
- Explicit cleanup =
net_unix_unlink(path):ENOENTalso returnsrc < 0(same shape as a real failure) — the "already gone" discrimination for idempotent cleanup paths is decided by the caller; the shim sets no special case.
11.3 Transport Semantics Defaults (Normative)
The server-engineering defaults are pinned here; implementations must not silently deviate:
TCP_NODELAYon by default (Nagle disabled; low latency is the default; bulk-throughput scenarios may turn it off explicitly).SO_KEEPALIVEon by default; idle/interval/probes use the platform defaults, with explicit tuning knobs provided.- Listeners default to
SO_REUSEADDR;SO_REUSEPORTis not in the facade (the P9 multi-tenancy aspiration). - Facade
writegoes straight to the kernel; no large user-space buffers. Batching /writevis there for protocol layers to use explicitly. - Half-close
shutdown(Write)is legal; platform differences in the peer reading EOF (winsock) are smoothed over by the shim. - Read/write timeouts should be expressed via context deadline parameters (§11.4); per-call setters are allowed, but same-shape tests must pass under both runtimes.
11.4 Execution Model and the Same-Shape-Different-Wiring Contract (Normative)
- Beneath the facade, two runtimes carry the same semantics: the P1 blocking runtime (1:1 threads) and the P2 coroutine runtime (N:M, §7.1 stackful coroutines). Same-shape-different-wiring contract: the same program source, with zero changes, has identical observable semantics under both runtimes — pinned down by mechanical test invariants (run at every wave exit).
- Suspension-point contract (coroutine policy): network facade calls,
sleep, and channel operations are the only suspension points; recognized by the compiler and the runtime, imperceptible and unannotated in user code. - Stack policy note: §7.1 promises a "growable contiguous stack"; the transitional implementation is a fixed 64KB mmap stack (a subset), and hitting the top = task-boundary panic (consistent with §7.1's cap policy); full conformance is reached with the P9 stack-economics project. The C100K/C10M figures are governed by that project; the transitional figures must not be extrapolated.
- Cancellation: propagated along the §7.2 task-level cancellation tokens; network operations respond to cancellation at suspension points, returning
NetErr::Cancelled; on the blocking runtime the policy is join equivalence (cancellation means waiting for completion). Facade operations targeting an already-cancelled scope returnErr(ScopeCancelled), semantically aligned with §7.2. - FFI discipline (MUST): inside
extern "c"callbacks, no touching the network facade / channels / sleep (coroutine stacks are not reentrant); violators = debug assertion + a declared undefined behavior. - Timeout budgets: deadlines enter the facade via context parameters; cross-task inheritance follows the scope tree (§7.2).
11.5 DNS and Resolution (Normative)
resolve(host) -> List[SocketAddr]: returns multiple records, no v6 discrimination; constrained by thenet.resolvekey.- Implementation policy: pooled threads running
getaddrinfo+ coroutine wrapping (semantically colorless); c-ares is listed as an aspiration tier. - Connection orchestration: the caller attempts in order; the connect timeout is independent of the read timeout; happy-eyeballs is listed as an aspiration tier.
11.5.1 Asynchronous resolve Semantics (v0.8.1 note; implementation anchor: the ctron_net.c DNS helper pool)
- Pool-thread semantics: under the coroutine policy, resolve completes via a 2-thread helper pool + park/wake (the pool is built lazily and stays resident for the process lifetime) — resolution does not sit on the worker hosting the calling coroutine (with workers=1 the old shape starved the whole runtime; differentially proven); on the bare-thread surface (rt not linked), the P1 inline original path is unchanged byte for byte.
- Not cancellable: the task-cancellation broadcast does not interrupt an in-flight resolution —
getaddrinfoitself has no cancellation surface; after the coroutine is woken by cancellation it parks again in awhile !doneloop to absorb spurious wakeups, until the resolution returns; the maximum wait = getaddrinfo itself (registered policy: the CI surface uses only localhost/numeric hosts, millisecond scale; resolution duration for arbitrary outbound hosts is not covered by the facade's guarantee). - Degraded mode: if pool creation fails entirely (thread creation failure), it degrades to inline blocking on the calling surface (the old semantics; degradation does not hang); signature and return values are unchanged throughout.
11.6 Clock and Timers (Normative)
now_ns() -> I64: a monotonic clock built in (on a clock_gettime / QueryPerformanceCounter foundation).sleep_ns: colorless, cancellable (§11.4).- Testing mode: the virtual clock — in test mode it can jump, and timers fire deterministically (the foundation of deterministic async testing).
11.7 HTTP Profile Layering (Overview; Detailed Contracts Finalized Along Roadmap P4/P6)
- Protocol half-layer: HTTP/1.1 (RFC 9110/9112) codec, chunked, keep-alive, 100-continue, header/body limits (capability-parameterized), compression negotiation (gzip/deflate to start, zstd as an aspiration tier), SSE, WebSocket (RFC 6455). Version roadmap: HTTP/2 = aspiration tier (P9).
- Framework half-layer: comptime routing, middleware (auth/CORS/CSRF/rate limiting/timeouts/security headers), static files (conditional requests/Range), multipart, comptime OpenAPI export.
- Layering discipline: the framework half-layer is forbidden from bypassing the protocol half-layer to touch the network; the protocol half-layer is forbidden from embedding routing/business concepts.
- Landed-surface note (v0.8.1, P4-A): the protocol half-layer's first landed item =
std/http/(parse.ct for request-line/status-line/header parsing + message.ct for message construction / chunked codec / 100-continue hooks), a pure-Ctron half-layer with zero use (it does not import std.net: avoiding the loader's diamond-use false-positive E5020 + keeping it testable on the interpreter, same policy as std/tls.ct); request-smuggling hardening posture is strict (RFC 9112 §5.2 obs-fold rejected, §6.1 TE+CL coexistence rejected, duplicate CL rejected, bare LF rejected); five parameterized limit slots (line / header count / single header / total headers / body, each over-limit with its own err code; the 400/414/431/413/505 mapping belongs to the framework wave); the IO glue (write-side/read-side timeouts over the net facade) lands with the framework half-layer.
11.8 Correspondence with the Test Suite
tests/net/ (loopback discipline: :0 kernel-assigned ports, zero dependence on the external network), tests/http/; anchors tests/modules/caps_net (E4010, multi-file package negative case), r7b_pure_net.neg.ct (E4020), r7c_http_caps.neg.ct; same-shape compatibility invariant fixtures (examples/ctecho source unchanged across runtimes).
11.9 TLS Facade Semantics Pointer (v0.8.1; P3-C landed surface, implementation anchors: std/tls.ct + ctron_tls.c)
The three facade semantics are pinned here (details in the std/tls.ct header comment; an isomorphic extension of the §11.2/§11.3 frozen surface):
- Hostname matching is opt-out by default: not calling
tls_set_hostnameskips name matching; certificate-chain verification (REQUIRED) is retained and is not turned off along with it. Passing an empty string = back to the opt-out state. Explicitly validating the host name is the caller's obligation (connection-orchestration-layer craft). - Timeouts are per-record:
tls_read'stimeout_mstakes effect via the mbedTLSconf_read_timeout+ BIOrecv_timeoutcontract and governs the wait for a single record's arrival, not "a total budget for the whole record chain" (when a multi-record chain is in hand, the wait resets per record); on timeout it returnsrc < 0(slot =SSL_TIMEOUT). The overall read budget (deadline) belongs to the §11.4 context parameters; the facade sets none. - Both EOF forms map to 0: the two forms of peer close — the BIO-layer fd FIN (mbedTLS fetch_input receiving 0, converted to
CONN_EOF) and the record-layer close_notify (PEER_CLOSE_NOTIFY) — both map totls_read() == 0(the eof policy, aligned with §11.2's single read-EOF form); callers do not distinguish them, and must not rely on distinguishing them.