net
import net from "net";
import io from "io";
import time from "time";
// Connect to a host by name (IPv6 and IPv4 raced), send a request, read the reply
var s: net.TcpStream = net.connect_host("example.com", 80, time.seconds(10)) or { return 1; };
defer s.close();
s.set_read_timeout(time.seconds(10)) or { return 1; };
s.write_all("HEAD / HTTP/1.0\r\nHost: example.com\r\n\r\n") or { return 1; };
var r: io.BufReader = io.buf_reader(s.reader());
defer r.close();
let status: ?string = r.read_line() or { return 1; };
if (status != null) { io.println(status!); }net is sockets for programs: TCP streams and listeners, UDP sockets,
name resolution, a readiness poll, and connect_host, which resolves a
name and races its addresses. It is a capability module, a thin layer over
the operating system’s sockets (BSD sockets on Linux and macOS, Winsock on
Windows). Every failure is an io.IoError; the network ones are
listed under Errors.
Sockets are blocking by default. Timeouts are per call and opt-in, and so
are nonblocking mode and poll. A socket is a plain value holding the
runtime’s handle: copying it copies the handle, not the socket, and you
close it yourself (defer s.close();), like an fs.File.
TLS (tls_connect, tls_wrap) runs the tls engine
over a TCP stream, checking the server against tape’s bundled roots or a
trust set you build.
Endpoints
An Endpoint is an IP address and a port:
pub tagged Endpoint {
V4 { addr: [4]u8; port: u16; }
V6 { addr: [16]u8; port: u16; scope_id: u32; }
}IPv4 bytes are in network order (10.0.0.1 is [10, 0, 0, 1]); an IPv6
address has its zone in scope_id (0 for none).
| Function | Does |
|---|---|
v4(a, b, c, d, port) | a.b.c.d:port |
v6(addr, port, scope_id = 0) | An IPv6 endpoint from 16 address bytes (traps for another length) |
localhost_v4(port) / localhost_v6(port) | 127.0.0.1 / ::1 |
any_v4(port) / any_v6(port) | 0.0.0.0 / ::, every address, for listen and bind_udp |
parse_ip(text, port) -> ?Endpoint | An address literal, or null when text is not one (a name to resolve) |
parse_endpoint(text) -> ?Endpoint | "1.2.3.4:80" or "[::1]:443" (brackets required for IPv6) |
e.port(), e.with_port(p), e.is_v6(), e.equals(&other) | |
e.format() -> string | "10.0.0.1:80", "[2001:db8::1]:443", "[fe80::1%3]:22" |
e.format_ip() -> string | The address alone: "fe80::1%3" |
parse_ip reads IPv4 as exactly four decimal parts 0-255 without leading
zeros (010.0.0.1 is refused rather than guessed as octal), and IPv6 as
RFC 4291 writes it: :: for a run of zero groups, an IPv4 tail
(::ffff:1.2.3.4), hex in either case, an optional numeric %zone.
format writes IPv6 in the RFC 5952 form (lowercase, the longest run of
zero groups as ::, no leading zeros, an IPv4-mapped address with its IPv4
tail), so parsing and formatting round-trip to one spelling.
let e: net.Endpoint = net.parse_endpoint("[2001:DB8:0:0:0:0:0:1]:443")!;
io.println(e.format()); // [2001:db8::1]:443TCP
| Function | Does |
|---|---|
connect(ep, timeout = null) -> TcpStream | A blocking connection to ep; with a timeout, TimedOut after it |
connect_host(host, port, timeout = null) -> TcpStream | Resolve host and connect to one of its addresses, racing them (below); the timeout covers the whole call |
connect_nonblocking(ep) -> TcpStream | Start a connection and return at once, the stream nonblocking |
listen(ep, backlog = 128) -> TcpListener | Bind and listen; port 0 picks a free port |
l.accept() -> Accepted | The next connection: .stream and .peer |
A TcpStream has:
| Method | Does |
|---|---|
read(buf) -> i64 | Bytes read; 0 when the peer closed its side |
read_exact(buf) | Fill buf (UnexpectedEof if the peer closes first) |
write(data) -> i64 | Bytes the system took, possibly fewer than data.len |
write_all(data) | Write everything |
shutdown_write() | Send FIN: the peer reads the end of the stream; this side can still read |
set_read_timeout(t), set_write_timeout(t) | Each later read or write gives up after t (TimedOut); null waits forever |
set_nonblocking(on) | A read or write that would wait is WouldBlock instead |
set_nodelay(on) | Turn Nagle’s algorithm off |
local_endpoint(), peer_endpoint() | |
take_error() | The pending error, cleared: how a connect_nonblocking ended |
poll_item(want) -> PollItem | For poll |
reader() -> io.Reader, writer() -> io.Writer | The stream as an io stream |
close() | Close it (twice is fine) |
A TcpListener has accept, set_timeout(t) (each later accept gives
up after t), set_nonblocking(on), local_endpoint, poll_item and
close. A stream from accept starts blocking whatever the listener’s
mode.
A timeout of zero or less is InvalidInput. connect refuses port 0 and
the any_* addresses (InvalidInput). Every method on a closed socket is
InvalidInput. Writing to a peer that has gone away is an error
(BrokenPipe, ConnectionReset or ConnectionAborted), never a signal.
A listener on an IPv6 address accepts IPv6 only: to serve both families,
listen twice. On Linux and macOS a restarted server can take its port back
at once (SO_REUSEADDR); on Windows the port is exclusive to its
listener.
// A server on loopback, one connection
var l: net.TcpListener = net.listen(net.localhost_v4(8080)) or { return 1; };
defer l.close();
var a: net.Accepted = l.accept() or { return 1; };
defer a.stream.close();
io.println("connection from {a.peer.format()}");
a.stream.write_all("hello\n") or { return 1; };Happy Eyeballs
connect_host connects the way RFC 8305 describes, in a fixed,
predictable form:
- The resolver’s addresses, duplicates removed, alternate between the families starting with the first address’s (IPv6, IPv4, IPv6, …); at most 16 are tried.
- The first attempt starts at once. Each next one starts 250 ms after the previous one if nothing has connected yet, or at once when every attempt in flight has failed.
- The first attempt to connect wins (the earliest started, if several connect together); the others are closed. The stream comes back blocking.
- When every address fails, the error of the first attempt is returned;
when the timeout passes first,
TimedOut.
An address literal (connect_host("127.0.0.1", 80)) is not resolved, and
a name with a single address is a plain connect.
Streams
s.reader() and s.writer() are an io.Reader and
io.Writer over the socket, so io.BufReader, read_line,
read_all, io.BufWriter and anything written against io streams works
over TCP. They don’t own the socket; close the TcpStream as usual. The
stream’s timeouts and nonblocking mode apply to them (TimedOut,
WouldBlock).
var w: io.BufWriter = io.buf_writer(s.writer());
w.write_all("one\ntwo\n") or { return 1; };
w.close() or { return 1; }; // flushes; the socket stays openUDP
| Function | Does |
|---|---|
bind_udp(ep) -> UdpSocket | A UDP socket bound to ep (port 0 picks one) |
u.send_to(data, to) -> i64 | Send one datagram |
u.recv_from(buf) -> Datagram | Receive one: .len, .peer (the sender), .truncated |
u.connect(peer) | Fix the peer: send and recv use it |
u.send(data), u.recv(buf) | To and from the connected peer |
u.set_broadcast(on), u.set_read_timeout(t), u.set_nonblocking(on) | |
u.local_endpoint(), u.poll_item(want), u.close() |
A datagram is sent whole or not at all (one too large is InvalidInput).
A received datagram longer than buf is cut to fit and truncated is set.
Sending to a broadcast address needs set_broadcast(true). On a connected
socket, a peer port that is closed shows up as ConnectionRefused on a
later call (on Windows only where the firewall lets the ICMP message
through; otherwise the read times out).
Resolution
resolve(host, port) -> Vec_Endpoint asks the system resolver (the hosts
file and the configured DNS) for host’s addresses, in the resolver’s
order, each with port. An address literal comes back as it is. Errors:
HostNotFound (no such name, or no addresses), TimedOut (no DNS server
answered), InvalidInput (empty, over 253 bytes, a NUL byte). Names are
ASCII; an internationalized name is passed in its xn-- form.
let addrs: net.Vec_Endpoint = net.resolve("localhost", 80) or { return 1; };
for (i in 0..addrs.len()) { io.println(addrs.get(i).format()); }Polling
poll(items, timeout = null) -> i64 waits until at least one item is ready
or the timeout passes (null: no limit; zero: just look), sets each item’s
got and returns how many are ready (0 on timeout). At most POLL_MAX
(1024) items.
pub struct PollItem { h: i64; want: u8; got: u8; }
pub const READABLE: u8 = 1; // data, or a connection to accept
pub const WRITABLE: u8 = 2; // room to write, or a connection finished
pub const HANGUP: u8 = 4; // the peer closed (reported whether wanted or not)
pub const ERROR: u8 = 8; // take_error has something to say (likewise)With poll and nonblocking sockets, one thread can serve several
connections:
var items: [2]net.PollItem;
items[0] = l.poll_item(net.READABLE);
items[1] = s.poll_item(net.READABLE);
let n: i64 = net.poll(items[0..2], time.seconds(1)) or { return 1; };
if ((items[0].got & net.READABLE) != 0) { /* accept */ }connect_nonblocking plus poll for WRITABLE, then take_error, is a
connection that doesn’t block; read_exact and write_all on a
nonblocking stream wait with poll for as long as it takes.
TLS
var s: net.TlsStream = net.tls_connect("example.com", 443, net.tls_options()) or {
io.eprintln("TLS: {err.desc()}");
return 1;
};
defer s.close();
s.write_all("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n") or { return 1; };
var buf: [4096]u8;
while (true) {
let n: i64 = s.read(buf[0..4096]) or { return 1; };
if (n == 0) { break; } // the server's close_notify
io.print(buf[0..n]);
}| Function | Does |
|---|---|
tls_options() -> TlsOptions | The bundled roots, no timeouts, no ALPN, no pin |
tls_connect(host, port, opts) -> TlsStream | connect_host, then tls_wrap with host as the server name |
tls_wrap(tcp, server_name, opts) -> TlsStream | TLS 1.3 over a connected TcpStream it takes over (closed on failure) |
s.read(buf) -> i64 | Decrypted bytes; 0 after the server’s close_notify |
s.write(data) -> i64, s.write_all(data) | Encrypt and send (up to 16 KiB a record) |
s.update_keys(request_peer) | A KeyUpdate: new send keys, and with request_peer new ones from the server too |
s.suite(), s.alpn() | The cipher suite; the ALPN protocol the server chose (null for none) |
s.peer_closed(), s.last_error() | Whether close_notify came; the error the stream last returned |
s.buffered() -> bool | Whether a read can return without the socket (data already received): an event loop polls the socket only when this is false |
s.reader(), s.writer() | The stream as an io.Reader/io.Writer |
s.tcp() | The TCP stream underneath (its endpoints) |
s.close() | close_notify, the socket closed, the keys wiped, the memory freed |
pub struct TlsOptions {
trust: ?*const x509.TrustStore = null; // null: tape's bundled roots
connect_timeout: ?time.Duration = null; // tls_connect's connecting
handshake_timeout: ?time.Duration = null; // the whole handshake
io_timeout: ?time.Duration = null; // each socket read and write
alpn: []const []const u8 = []; // protocols to offer, e.g. ["h2", "http/1.1"]
pin_leaf_sha256: []const u8 = []; // the SHA-256 of the one server certificate accepted
pin_only: bool = false; // the pin instead of the chain
}The server’s certificate must chain to a root in trust, be valid now
(the wall clock) and name server_name. There is no option to skip that
check. A pin (pin_leaf_sha256) is checked too; with pin_only it is
the whole check, for a known server with a self-signed certificate.
Errors are SecureError: Io { e: io.IoError } from the socket (a
timeout is TimedOut) or Tls { e: tls.TlsError } from TLS
(UnknownCa, NameMismatch, CertificateExpired, Truncated when the
connection ends without close_notify, …); err.desc() gives the text.
Through reader() and writer() a TLS error reads as UnexpectedEof
(Truncated) or Other, and last_error() says which.
A TlsStream holds about 70 KiB on the heap: close it (defer s.close();).
Copying one copies a reference to the same connection.
Trust
// The bundled roots plus the operating system's (a corporate root)
var t: net.Trust = net.trust();
defer t.free();
t.add_bundled();
t.add_system() or { io.eprintln("no system roots: {err.desc()}"); };
var opts: net.TlsOptions = net.tls_options();
opts.trust = t.store();| Function | Does |
|---|---|
bundled_trust() -> *const x509.TrustStore | tape’s bundled roots (ca_roots), loaded once: the default. Safe from any thread, so a thread or @pool task can connect with the default trust |
trust() -> Trust | An empty set |
trust_from_pem(pem), trust_from_der(certs) | A set of the certificates of a PEM bundle, or of DER certificates |
t.add_bundled() | Add the bundled roots |
t.add_system() | Add the operating system’s roots |
t.add_pem(pem), t.add_der(cert) | Add a PEM bundle’s certificates, one DER certificate |
t.store() | The store to put in TlsOptions.trust or http.Options.trust |
t.len(), t.skipped() | Anchors in the set; certificates add_system could not use |
t.free() | Free the set, after the connections using it have closed |
The operating system’s store is used only when you add it. add_system
reads $SSL_CERT_FILE or the distribution’s bundle on Linux
(/etc/ssl/certs/ca-certificates.crt and the other usual places), the
ROOT certificate store on Windows, the Security framework’s anchors on
macOS. A certificate already in the set is not added again. Errors are
TrustError: BadPem, BadCertificate, Empty, and Io when the
system store could not be read; on an error nothing from that input is
added.
Errors
The network variants of io.IoError:
| Variant | Means |
|---|---|
ConnectionRefused | Nothing listens there (or a UDP peer’s port is closed) |
ConnectionReset | The peer reset the connection |
ConnectionAborted | The connection was dropped on this side, or reset before accept |
NotConnected | The socket has no connection |
AddrInUse | Another socket has the address and port |
AddrNotAvailable | The address is not one of this machine’s |
NetworkUnreachable, HostUnreachable | No route there |
TimedOut | A timeout passed; or no DNS server answered |
WouldBlock | Nonblocking, and the call would have to wait |
HostNotFound | The name does not exist or has no addresses |
Besides these: BrokenPipe (writing after the peer closed),
PermissionDenied (a port below 1024 on Unix, a firewall),
InvalidInput, Interrupted, Unsupported (no sockets on this platform)
and Other.
Platforms
Windows, Linux and macOS, and leivos (the Uusi kernel), where the sockets
are netd’s. On leivos: IPv4 only (IPv6 is Unsupported), a
datagram of at most 200 bytes sent with send_to (224 with a connected
send) and 192 received with recv_from (longer ones are cut, and
truncated is set), at most 9 addresses from resolve, and eight
sockets in all across the system’s programs. Bytes move to and from netd
in its 256-byte messages, a couple of hundred bytes a round trip. A netd
that shares a memory window with the program instead moves up to 64 KiB
a request, and a datagram can be that long. The runtime asks netd on the
program’s first send or receive and uses the window when netd offers
one, and the messages otherwise. Nothing changes in your code. While netd
is not running yet (early in boot, or before the network card is up),
every socket call fails with NetworkUnreachable, a transient error: the
next call asks again.
Last modified: