net
Package net.
TCP listener/dialer built on bytes and fixed-width ints; every fallible call returns a result[_, str] unwrapped with guard let.
import "net";
let lr: result[i32, str] = net.listen(8080); // 0 = ephemeral port
guard let lfd = lr else { exit(1); }
let pr: result[i32, str] = net.port(lfd); // assigned port number
let ar: result[i32, str] = net.accept(lfd); // blocks until a peer connects
guard let cfd = ar else { exit(1); }
net.send(cfd, b"hello");
let rr: result[bytes, str] = net.recv(cfd, 4096);
let data: bytes = rr ?? b"";
net.nonblock(cfd); // switch to non-blocking mode
let wr: result[bytes, str] = net.recv(cfd, 16); // "would block" err if idle
net.close(cfd);Deadlines#
net.recv and net.send wait for as long as the peer takes, which on a public listener is indefinitely: a client that connects and then neither sends nor reads parks the serving task on the reactor forever, holding its stack and its GC roots. That is slowloris, and the defence is recv_until / send_until, which take an until — an absolute monotonic instant, not a duration:
import "net";
import "time";
let deadline = until_of(time.mono() + 5000000000); // 5s from now
let rr = net.recv_until(cfd, 4096, deadline);
guard let data = rr else let e = err_of(rr) {
if e == "timeout" { net.close(cfd); return; } // peer went quiet
log.error("recv: " + e); // peer broke
return;
}"timeout" is a reserved error string: it means the deadline passed, and it is the only error text these calls invent rather than take from the OS. Every other error is strerror/OpenSSL text as before.
One asymmetry worth knowing: a send_until that times out has already written some bytes, and result[i32, str] has no room to report both "timed out" and "wrote this much". A "timeout" from send_until therefore means the stream is at an unknown offset and the connection must be closed, not retried. For a framed protocol that is the right contract regardless — a half-written frame is unrecoverable.
net.tls_recv_until / net.tls_send_until are the same thing over TLS, with the same reserved string. The link API takes an until on accept/send/recv already.
See examples/httpd/ for a minimal HTTP server on link plus the http stdlib package.
net.tls_* adds a TLS listener/dialer on top of the plain net primitives above, built on OpenSSL (linked automatically, and only when a program actually calls one of these — a plain-TCP net program stays dependency-free). A SSL_CTX-equivalent config is created once (tls_server_ctx / tls_client_ctx) and reused across many connections; each connection is a separate rawptr handle.
import "net";
// server: load a cert + key once, reuse the context for every connection
let sctx_r: result[rawptr, str] = net.tls_server_ctx("cert.pem", "key.pem");
guard let sctx = sctx_r else { exit(1); }
let lr: result[i32, str] = net.listen(8443);
guard let lfd = lr else { exit(1); }
let ar: result[rawptr, str] = net.tls_accept(lfd, sctx); // TCP accept + handshake
guard let sconn = ar else { exit(1); }
net.tls_send(sconn, b"hello");
net.tls_close(sconn);
// client: verify against a CA file, or "" for the system trust store
let cctx_r: result[rawptr, str] = net.tls_client_ctx("");
guard let cctx = cctx_r else { exit(1); }
let dr: result[rawptr, str] = net.tls_dial("example.com", 443, cctx);
guard let cconn = dr else { exit(1); }
let rr: result[bytes, str] = net.tls_recv(cconn, 4096);
net.tls_close(cconn);Client verification is strict by default: tls_client_ctx enables peer verification, and tls_dial checks the certificate against both the CA and the hostname you asked for (SSL_set1_host — the check that's easy to forget and, if skipped, leaves you with "TLS" that validates a certificate chain without checking it belongs to the host you're actually talking to). Sending/receiving is blocking, same as plain net — call these from a spawned task if you need a connection handled without stalling anything else.
ALPN (RFC 7301) negotiates the protocol during the handshake, which is how HTTP/2 over TLS is selected — there is no in-band upgrade. tls_ctx_alpn(ctx, "h2,http/1.1") sets the list on a server context (in preference order, so the server decides) or the offer on a client one, and tls_alpn(conn) returns what was actually negotiated, or "" if the peer offered nothing that overlapped. The list is comma-separated, not the length-prefixed wire form; building that by hand is an easy way to produce a subtly broken handshake. A client offering no protocol we support completes the handshake without ALPN rather than failing, so it simply falls back to HTTP/1.1.
net.tls_ctx_alpn(sctx, "h2,http/1.1");
let conn = ...; // after tls_accept
if net.tls_alpn(conn) == "h2" { serve_h2(conn); } else { serve_h1(conn); }Mutual TLS: tls_ctx_require_client(sctx, client_ca) on the server context demands a client certificate chained to that CA (SSL_VERIFY_FAIL_IF_NO_PEER_CERT). The client presents one with tls_ctx_use_cert(cctx, cert, key). Extra server names on one listener: tls_ctx_add_sni(sctx, host, cert, key) swaps in that cert when the ClientHello SNI matches; unmatched names keep the default tls_server_ctx cert. require_client applies to SNI certs too, regardless of call order. TLS 1.3 can let tls_dial return before the server has rejected a missing client certificate; the first send or recv then fails.
API#
net.listen(int) -> result[i32,str]#net.port(int) -> result[i32,str]#net.accept(int) -> result[i32,str]#net.dial(str, int) -> result[i32,str]#net.send(int, bytes) -> result[i32,str]#net.recv(int, int) -> result[bytes,str]#net.recv_until(int, int, until) -> result[bytes,str]#net.send_until(int, bytes, until) -> result[i32,str]#net.close(int)#net.nonblock(int) -> result[bool,str]#