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);net.idle_alive(fd) -> bool and net.tls_idle_alive(ssl) -> bool report whether an IDLE connection is still reusable: true only when the peer has neither closed nor sent anything. One non-blocking MSG_PEEK, nothing consumed; the TLS form is also false when OpenSSL holds decrypted-but-unread bytes. They exist for connection pools, and they are a primitive rather than a recv_until with an expired deadline because recv_until checks its deadline before touching the socket — it would report every dead connection as alive.
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. "closed" is the other: another task called net.close on this fd while this call was parked on it. Those are the only error texts 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.
net.dial_until(host, port, deadline) bounds connecting: the DNS lookup and the TCP connect together. A lookup still running when the deadline passes is abandoned to the resolver thread (getaddrinfo cannot be interrupted), so the caller gets "timeout" on time. Every address the name resolves to is tried in turn — net.dial does the same, without a deadline.
DNS SRV and TXT lookups#
Atlas connection strings use DNS SRV and TXT records. net.lookup_srv_until and net.lookup_txt_until expose those records without blocking a slang worker; each lookup takes an absolute until deadline:
import "net";
import "time";
let deadline = until_of(time.mono() + 5000000000);
let seeds = net.lookup_srv_until("_mongodb._tcp.cluster.example", deadline);
guard let records = seeds else let e = err_of(seeds) { println(e); exit(1); }
// Each SRV item is "priority:weight:port:target".
let defaults = net.lookup_txt_until("cluster.example", deadline);
guard let options = defaults else let e = err_of(defaults) { println(e); exit(1); }
// Each TXT item is one record with its DNS character-strings concatenated.An empty TXT list means the domain has no TXT defaults. SRV targets retain priority and weight so a client can apply its own ordering policy. The resolver checks the response question and each returned record owner, rejects malformed names and oversized or truncated answers, and returns "timeout" when the deadline expires. At most 64 SRV/TXT lookups may be outstanding per process, bounding their answer buffers to about 4 MiB; excess calls return "DNS resolver request limit reached". The C resolver library is linked only by programs that call either record lookup; ordinary net programs keep their existing link dependencies.
Unix-domain sockets#
net.dial_unix(path, deadline) connects to a Unix-domain stream socket and net.listen_unix(path) listens on one (accept with net.accept). The fds work with every fd-based call — send/recv and their _until forms, close, idle_alive. listen_unix refuses a path that already exists rather than deleting it, since the file may belong to a server that is still running; remove a stale one with os.remove first. A path longer than the platform allows (104 bytes on macOS, 108 on Linux) is an error, not truncated.
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.
STARTTLS: tls_upgrade(fd, host, ctx) runs a client handshake on a socket from net.dial that has already spoken cleartext — how Postgres, SMTP and IMAP switch to TLS. Verification is exactly tls_dial's, hostname included, which is why the host is an argument: an fd does not remember what was dialled. On failure the fd is left open for the caller to close; on success it belongs to the returned handle and tls_close closes it. Read no further than the server's go-ahead before upgrading: anything a man in the middle queued behind it would otherwise be trusted as if it had arrived encrypted (libpq's CVE-2021-23222). tls_upgrade_until(fd, host, ctx, deadline) bounds the handshake; a server that stops answering part way through gives "timeout". tls_dial_until(host, port, ctx, deadline) is tls_dial with one deadline over the lookup, the connect and the handshake: an unreachable host gives "timeout" at the deadline instead of after the OS connect timeout, which is over a minute on macOS. Like dial, both try every address the name resolves to.
TLS on a link: c.tls_accept(ctx, deadline) runs the server handshake on a link you already accepted, and from then on the link's send/recv go through the session. Everything written against a link -- http.read_frame, http.write, a whole server loop -- serves HTTPS without a second code path, and a plain link pays one pointer test per call. Run it in the connection's own task, not the accept loop, so one slow handshake holds up nobody else:
fn serve(c: link, ctx: rawptr) {
let hr = c.tls_accept(ctx, until_of(time.mono() + 10000000000));
guard let _h = hr else let e = err_of(hr) {
return; // a scanner, a plaintext client, a timeout
}
// read_frame / write on c exactly as over plain TCP
}It returns result[int, fault]: timeout when the deadline passes, closed when the client hangs up during the handshake (browsers' preconnects do), and io with OpenSSL's reason otherwise (tls http request is a plaintext client). Client certificates are checked as tls_accept checks them. The session is freed, with a close_notify, when the link is; a second tls_accept on the same link is an error.
Reading and writing from two tasks: a link is move-only, so one task owns it -- and a task parked in recv cannot also send. c.split() returns a second link on the same connection for a writer task, which is what a server pushing to a client needs (a WebSocket broadcast, server-sent events):
fn writer(w: link, out: chan[bytes]) {
let mut_w = w;
while true {
let m = chan_recv(out);
guard let b = m else { return; } // closed: done
let sr = mut_w.send_bytes(b, until_of(time.mono() + 10000000000));
guard let _n = sr else { return; } // the peer is gone
}
}
let sr = c.split();
guard let w = sr else { return; }
let out: chan[bytes] = make_chan(256);
spawn writer(w, out);
// this task keeps reading c; anyone holding `out` can write to the clientFrom then on c only reads and the returned link only writes: a send on the reader or a recv on the writer is an io fault saying which to use, since two senders could interleave partial writes. Ending the reader (its binding goes out of scope) shuts the connection down for both halves, so a writer blocked on a client that stopped reading fails at once; ending the writer leaves the reader working. Each half holds its own fd, so a half never writes to a number the kernel has reused. Over TLS, start TLS first (tls_accept on a split link is refused): both halves then share the session, one OpenSSL call at a time. A link splits once.
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.dial_until(str, int, until) -> result[i32,str]#net.lookup_srv_until(str, until) -> result[[str],str]#net.lookup_txt_until(str, until) -> result[[str],str]#net.dial_unix(str, until) -> result[i32,str]#net.listen_unix(str) -> 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.idle_alive(int) -> bool#net.nonblock(int) -> result[bool,str]#