All network I/O in Inferno goes through the /net filesystem. The Dial module is a thin wrapper that opens the right files and writes the right commands so you usually don't have to touch /net directly.
Interface: module/dial.m
Implementation: appl/lib/dial.b
include "dial.m";
dial: Dial;
# Load once at startup
dial = load Dial Dial->PATH;
if(dial == nil)
raise "fail:cannot load Dial";
Connection: adt {
dfd: ref Sys->FD; # data fd — read/write application data through this
cfd: ref Sys->FD; # control fd — ctl file for the conversation
dir: string; # conversation directory, e.g. "/net/tcp/3"
};
Conninfo: adt {
dir: string; # connection directory
root: string; # /net root used
spec: string; # device spec
lsys: string; # local IP
lserv: string; # local port
rsys: string; # remote IP
rserv: string; # remote port
laddr: string; # full local address string
raddr: string; # full remote address string
};
dial(addr, local: string): ref Connection
# Connect to addr. local is local address hint (nil = any).
# Returns nil on failure; check sys->sprint("%r") for reason.
announce(addr: string): ref Connection
# Open a listening endpoint. addr uses * for host to mean all interfaces.
listen(c: ref Connection): ref Connection
# Block until an incoming connection arrives on an announced endpoint.
# Returns a new Connection for the incoming call.
accept(c: ref Connection): ref Sys->FD
# Accept the incoming call; returns data fd. Call after listen.
reject(c: ref Connection, why: string): int
# Reject an incoming call with a reason string.
netmkaddr(addr, net, svc: string): string
# Normalize an address. If addr has no protocol, net is used.
# Example: netmkaddr("example.com", "tcp", "80") → "tcp!example.com!80"
netinfo(c: ref Connection): ref Conninfo
# Read local/remote address info from connection directory.
[netdir/]proto!host!service
| Format | Example | Meaning |
|---|---|---|
tcp!host!port |
tcp!example.com!80 |
TCP to hostname |
tcp!ip!port |
tcp!1.2.3.4!443 |
TCP to IP |
udp!host!port |
udp!8.8.8.8!53 |
UDP |
tcp!*!port |
tcp!*!8080 |
Listen on all interfaces |
net!host!svc |
net!www.example.com!http |
Let cs translate |
host!port |
example.com!8080 |
Shorthand for net!host!port |
/net/tcp!ip!port |
/net/tcp!10.0.0.1!22 |
Explicit netdir |
il!host!port |
IL (reliable datagram) protocol |
The net protocol is a meta-protocol: dial looks it up through the connection server (/net/cs), which translates service names to real protocol+address+port triples. netmkaddr is the standard way to build an address from parts.
implement HttpGet;
include "sys.m"; sys: Sys;
include "dial.m"; dial: Dial;
HttpGet: module { init: fn(nil: ref Draw->Context, args: list of string); };
init(nil: ref Draw->Context, args: list of string)
{
sys = load Sys Sys->PATH;
dial = load Dial Dial->PATH;
addr := dial->netmkaddr("example.com", "tcp", "80");
conn := dial->dial(addr, nil);
if(conn == nil)
raise "fail:dial: " + sys->sprint("%r");
fd := conn.dfd;
# write request
sys->fprint(fd, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
# read response
buf := array[8192] of byte;
for(;;) {
n := sys->read(fd, buf, len buf);
if(n <= 0) break;
sys->write(sys->fildes(1), buf, n);
}
# connection closes when fd is GC'd
}
Real example: appl/cmd/whois.b:72–89, appl/cmd/webgrab.b:51–86.
The standard server loop: announce → loop(listen → accept → spawn handler).
implement EchoSrv;
include "sys.m"; sys: Sys;
include "draw.m";
include "dial.m"; dial: Dial;
EchoSrv: module { init: fn(nil: ref Draw->Context, args: list of string); };
init(nil: ref Draw->Context, nil: list of string)
{
sys = load Sys Sys->PATH;
dial = load Dial Dial->PATH;
c := dial->announce("tcp!*!7");
if(c == nil)
raise "fail:announce: " + sys->sprint("%r");
for(;;) {
nc := dial->listen(c);
if(nc == nil)
raise "fail:listen: " + sys->sprint("%r");
spawn handler(nc);
}
}
handler(nc: ref Dial->Connection)
{
dfd := dial->accept(nc);
if(dfd == nil)
return;
# Optional: prevent the TCP stack from timing out an idle connection
if(nc.cfd != nil)
sys->fprint(nc.cfd, "keepalive");
buf := array[4096] of byte;
for(;;) {
n := sys->read(dfd, buf, len buf);
if(n <= 0) break;
sys->write(dfd, buf, n);
}
}
Real examples: appl/cmd/styxlisten.b:96–138, appl/cmd/listen.b:111–188.
After connecting or accepting, read endpoint info from the conversation directory:
ci := dial->netinfo(conn);
if(ci != nil)
sys->print("connected %s → %s\n", ci.laddr, ci.raddr);
# Or read directly from the directory
localfd := sys->open(conn.dir + "/local", Sys->OREAD);
remotefd := sys->open(conn.dir + "/remote", Sys->OREAD);
buf := array[64] of byte;
n := sys->read(remotefd, buf, len buf);
# content is "ip!port\n"
When the Dial module isn't available or you need low-level control:
# 1. Open clone file to allocate a new conversation
clonefd := sys->open("/net/tcp/clone", Sys->ORDWR);
if(clonefd == nil) raise "fail:open clone: " + sys->sprint("%r");
# 2. Read the conversation number
buf := array[12] of byte;
n := sys->read(clonefd, buf, len buf);
convnum := string buf[0:n]; # e.g. "3\n" → strip to "3"
convnum = convnum[0:len convnum - 1]; # strip newline
# 3. Write command to ctl (clonefd IS the ctl file)
sys->fprint(clonefd, "connect 1.2.3.4!80");
# For server: sys->fprint(clonefd, "announce *!8080");
# 4. Open data file
datapath := "/net/tcp/" + convnum + "/data";
datafd := sys->open(datapath, Sys->ORDWR);
# 5. Read/write through datafd; ctl file (clonefd) stays open
Files per conversation:
| File | Use |
|---|---|
ctl (= the clone fd) |
Write connect host!port or announce addr!port |
data |
Application read/write |
listen |
For announced endpoints: open and read to block on incoming call |
local |
Read: ip!port\n of local end |
remote |
Read: ip!port\n of remote end |
status |
Connection state string |
What you write to ctl:
connect host!port— client connectconnect host!port localaddr!localport— with local bindingannounce addr!port— server listen (*for all interfaces)keepalive— enable TCP keepalivesclose— half-close
/net/cs translates logical addresses to physical ones. It is a file server you write a query to and read recipes back from.
write: "tcp!www.example.com!http"
read: "/net/tcp/clone\t1.2.3.4!80\n" (one or more lines)
dial->dial does this automatically when the protocol is net or when /net/cs exists. If cs is not running, dial uses the address verbatim.
Service names → port numbers come from /lib/ndb/common and /lib/ndb/inferno:
tcp=http port=80
tcp=https port=443
tcp=styx port=6666
udp=dns port=53
The connection server source is appl/cmd/ndb/cs.b.
Names are resolved by the DNS server appl/cmd/ndb/dns.b, which serves
/net/dns (a file2chan). cs forwards name queries to it; you can also query
it directly with ndb/dnsquery <name>. Both cs and dns are normally started
by the wm desktop (lib/wmsetup); start them by hand with ndb/cs & then
ndb/dns -r &.
Resolution order inside dns.b (the important part for hosted Inferno):
- Host resolver first — with the default
usehost=1,dnsloads$Srv(emu/port/srv.c) and callssrv->iph2a(name), which drops the VM token and calls the host'sgetaddrinfo(ipif6-posix.c). On hosted emu this means name resolution works out of the box with zero Inferno-side config — it uses the host's/etc/resolv.conf. (ndb/dns -hdisables this.) - Recursive fallback — if the host map returns nothing,
dnsdoes real DNS queries against the resolvers listed inlib/ndb/local:The shipped defaults make recursion work without editing ndb; replace them with a site resolver if you prefer.dns=8.8.8.8 # forward-to resolver (Google public DNS) dns=1.1.1.1 # backup (Cloudflare)
LP64 note: the
$Srvbuiltin's generated headers (emu/Linux/srv.h/srvm.h) encode ABI-specific frame offsets; if they go stale across a 32↔64-bit switch, the symptom is a wild-address fault insideSrv_iph2athat looks like a "DNS hang". The hazard and the clean-rebuild rule live in ON_C_IN_DIS.md ("stale generated module headers").
webgrab (an HTTP curl substitute, below) and dial of any net!host!svc
both depend on this path via cs.
appl/cmd/styxlisten.b is the canonical pattern for serving a 9P file tree to network clients. Read it. The core idea:
c := dial->announce(addr);
for(;;) {
nc := dial->listen(c);
dfd := dial->accept(nc);
if(nc.cfd != nil)
sys->fprint(nc.cfd, "keepalive");
spawn exportproc(nc, dfd);
}
exportproc(nc: ref Dial->Connection, dfd: ref Sys->FD)
{
# Give the connection its own namespace and fd table
sys->pctl(Sys->NEWFD | Sys->NEWNS, 2 :: dfd.fd :: nil);
# Export the local namespace to the remote client
sys->export(dfd, "/", Sys->EXPWAIT);
}
sys->export(fd, root, flag) serves the subtree at root as a 9P server over fd. EXPWAIT blocks until the remote side disconnects; EXPASYNC returns immediately.
Authentication is separate from the transport. The standard protocol is p9any, negotiated by factotum.
Client side (from appl/cmd/import.b:81–89):
facfd := sys->open("/mnt/factotum/rpc", Sys->ORDWR);
conn := dial->dial(addr, nil);
if(conn == nil) raise "fail:dial: " + sys->sprint("%r");
# Authenticate; ai.secret is the shared session key
ai := factotum->proxy(conn.dfd, facfd, "proto=p9any role=client");
if(ai == nil)
raise "fail:auth: " + sys->sprint("%r");
# Now mount the authenticated connection
sys->mount(conn.dfd, nil, "/n/remote", Sys->MREPL, "");
Server side (from appl/cmd/9export.b:70–80):
facfd := sys->open("/mnt/factotum/rpc", Sys->ORDWR);
ai := factotum->proxy(fd, facfd, "proto=p9any role=server");
if(ai == nil)
raise "fail:auth: " + sys->sprint("%r");
# ai.cuid is the authenticated remote user name
To skip authentication entirely: pass nil for auth fd and connect as user "none", or use the -A flag in styxlisten/listen.
Push SSL on top of an existing fd using appl/lib/auth.b's pushssl helper (or use the SSL module directly via module/ssl3.m):
# After authentication establishes a shared secret:
(sslfd, err) := auth->pushssl(conn.dfd, ai.secret, ai.secret, "rc4_256 sha1");
if(sslfd == nil)
raise "fail:ssl: " + err;
# sslfd replaces conn.dfd for all further I/O
The algorithm string ("rc4_256 sha1", "des_56_cbc sha1", etc.) is negotiated between client and server before pushing SSL.
The pushssl stack above is SSL 3.0 / TLS 1.0 only (ssl3.b handshake +
devssl record layer: RC4/DES/3DES, RSA key exchange); libsec lacks the modern
primitives (ECC/X25519, AEAD/GCM, HKDF, SNI), so modern servers reject all of it.
For real https, use #T (devtls) instead.
devtls is the modern TLS 1.2/1.3 device, backed by vendored mbedTLS 3.6.2
(libmbedtls/, Apache-2.0 — follows the libfreetype vendoring precedent). It is
compiled into emu (config dev tls + lib mbedtls) and lives at
emu/port/devtls.c. It is a system-wide namespace capability — sh, dial,
webgrab, 9P-over-TLS and Charon all share it — and it layers onto a connection
exactly the way devssl does. Tested only on Linux/aarch64.
Raw device usage (mirrors devssl's "push onto an fd" model):
clone := open("#T/clone", ORDWR) # read the conversation number from it
write(ctl, "fd <n>") # attach an already-dialed connection fd
write(ctl, "servername example.com") # SNI + cert hostname check
# optional: "verify off" | "cafile <path>" | "alpn h2 http/1.1"
write(ctl, "handshake") # or let the first data I/O drive it
data := open("#T/<conv>/data", ORDWR) # cleartext stream; mbedTLS does the crypto
read("#T/<conv>/status") # "version cipher verify=<flags>\n"
Defaults: peer verification is required (verify off to disable) against
/etc/ssl/certs/ca-certificates.crt. TLS bytes flow over the underlying Chan
through the normal device layer, so blocking is handled by the emu I/O framework
(no manual release()/acquire() — mbedTLS crypto is CPU-only). The BIO
callbacks wrap the underlying I/O in waserror(), converting an Inferno error()
longjmp into an mbedTLS error return rather than unwinding through mbedTLS's C
stack.
From Limbo, prefer the Dial helpers (module/dial.m, appl/lib/dial.b)
which wrap the device:
# Layer TLS onto an already-connected fd:
(cleartext, ctlfd, err) := dial->pushtls(conn.dfd, "example.com");
# dial + pushtls in one step:
(conn, ctlfd, err) := dial->dialtls("tcp!example.com!443", nil, "example.com");
pushtls returns the cleartext fd plus the ctl fd (keep it open — closing it
tears down the TLS conversation and the underlying socket). Charon's https path
(appl/charon/http.b) calls pushtls directly; the legacy SSL3->Context
parameters that remain in its header ADT are dead fields on the modern path.
Why a device, not a builtin module: protocols layered on top of TLS —
wss, HTTP/2 framing — ride above the plaintext data fd and need no TLS-context
threading. Only TLS-layer control/introspection (ALPN, verify flags, cert
details) lives at the device, exposed via ctl/status files. A thin typed Limbo
Tls module over the device files was considered but not built — the device
plus the Dial helpers cover current needs.
dial->dial returns nil on failure. Get the reason with sys->sprint("%r"). The dial library automatically tries /net and then /net.alt before giving up, and picks the more informative error string.
Common error strings:
"connection refused"— remote port not listening"cs: no translation"— service name unknown to connection server"does not exist"— network path not found"i/o error"— network-level error
Set your own error string with sys->werrstr(s) so callers see it via %r.
Inferno has no HTTP library beyond the URL parser (module/url.m / appl/lib/url.b). HTTP is done with dial + manual framing + bufio for line reading:
include "bufio.m"; bufio: Bufio; Iobuf: import Bufio;
bufio = load Bufio Bufio->PATH;
conn := dial->dial("tcp!example.com!80", nil);
sys->fprint(conn.dfd, "GET /path HTTP/1.0\r\nHost: example.com\r\n\r\n");
iob := bufio->fopen(conn.dfd, Bufio->OREAD);
status := iob->gets('\n'); # "HTTP/1.0 200 OK\r\n"
for(;;) {
hdr := iob->gets('\n');
if(hdr == "\r\n" || hdr == "\n" || hdr == nil) break;
}
# now read body via iob->read or iob->gets
See appl/cmd/webgrab.b for a complete working example.
| File | Purpose |
|---|---|
module/dial.m |
Dial module interface, Connection/Conninfo ADTs |
appl/lib/dial.b |
Dial implementation (cs integration, address parsing) |
appl/cmd/styxlisten.b |
9P-over-network server template |
appl/cmd/listen.b |
General TCP service listener |
appl/cmd/ndb/cs.b |
Connection server |
appl/lib/auth.b |
pushssl, client/server auth helpers |
module/factotum.m |
Factotum authentication agent interface |
module/ssl3.m |
legacy SSL3/TLS1.0 module interface (devssl) |
emu/port/devtls.c |
modern TLS 1.2/1.3 device (#T), mbedTLS-backed |
libmbedtls/ |
vendored mbedTLS 3.6.2 (built to libmbedtls.a) |
module/dial.m / appl/lib/dial.b |
pushtls/dialtls helpers over #T |
appl/cmd/whois.b |
Minimal dial client example |
appl/cmd/webgrab.b |
HTTP client example |
appl/cmd/import.b |
Authenticated remote mount example |
appl/cmd/9export.b |
Authenticated namespace export example |
lib/ndb/common |
TCP/UDP port number database |
lib/ndb/local |
Local host configuration |
os/port/dial.c |
Kernel-level dial implementation (C) |
man/2/dial |
Dial module reference |
man/2/sys-bind |
mount/export reference |