overview
SECURE NETWORKING
Big-picture explanations, quick reference material, and definitive orientation docs.
Context Rail
On This Page
Secure Networking & Traffic Interception
Mutant ships a networking toolkit in its standard library so that tools such as TLS clients/servers and mitmproxy-style interception proxies can be written entirely in Mutant source. See the Capability Reference for the full network builtin table.
The toolkit is split into three layers:
- Stream sockets & TLS sessions — connect, listen, accept, read, write, and upgrade connections to TLS (client or server side).
- X.509 certificate authority — generate a CA and mint / sign leaf certificates on demand (the core primitive an interception proxy needs).
- HTTP message inspection — parse and rebuild HTTP requests and responses, either from a string or directly off a live connection with correct framing.
All fallible builtins follow the language convention of returning
(result, err); destructure with let value, err = ....
Two builtins additionally report recoverable conditions inside the returned
hash instead of through err, so that a polling loop is not forced to abort:
net_conn_read— I/O problems (including read timeouts) land in the result'serrorfield, with end-of-stream ineof. Itserrreturn only fires for bad arguments or an unknown handle, so always check both:if (err) { ... } else if (msg["error"] != "") { ... }.net_accept— an expired accept timeout is a successful call returningok=false, timeout=truewitherrnull. Genuine accept failures set both the hash'serrorfield anderr.
Everything else reports failure through err alone.
1. Sockets and TLS sessions
Connections and listeners are referenced by an integer handle. Always close
them with net_conn_close / net_listen_close when done.
| Builtin | Signature | Returns |
|---|---|---|
net_connect |
(address, timeoutMs) |
connection handle |
net_tls_connect |
(address, timeoutMs, options?) |
connection handle |
net_conn_write |
(handle, data, timeout_ms?) |
bytes written (write deadline: default 30s, or timeout_ms; <=0 blocks forever) |
net_conn_read |
(handle, maxBytes, timeoutMs) |
{data, bytes, eof, error} (maxBytes ≤ 32 MiB) |
net_conn_info |
(handle) |
addresses + negotiated TLS session |
net_conn_close |
(handle) |
bool |
net_listen |
(address) |
listener handle |
net_tls_listen |
(address, certPem, keyPem, options?) |
listener handle |
net_accept |
(listener, timeoutMs) |
{ok, handle, remote_addr, timeout, error} |
net_listen_close |
(handle) |
bool |
net_tls_upgrade_server |
(handle, certPem, keyPem, options?) |
handshake info |
net_tls_upgrade_client |
(handle, options?) |
handshake info |
Client TLS options (net_tls_connect, net_tls_upgrade_client):
server_name, insecure (skip verification), alpn (array), min_version
("1.0".."1.3"), ca_cert (PEM roots to pin), client_cert + client_key
(PEM, for mutual TLS).
Server TLS options (net_tls_listen, net_tls_upgrade_server):
alpn, min_version, client_ca (PEM; requires and verifies client certs for
mutual TLS).
handshake_timeout_ms (default 15000) applies to the two upgrade builtins only,
which drive the handshake themselves. net_tls_connect bounds its handshake
with its own timeoutMs dial timeout, and net_tls_listen handshakes lazily on
first use of the accepted connection.
net_accept with timeoutMs <= 0 blocks; a positive timeout lets a loop poll
without aborting (ok=false, timeout=true on expiry).
Example: verified TLS client
let conn, err = net_connect("example.com:443", 5000);
let opts = {"server_name": "example.com", "min_version": "1.2"};
let info, err = net_tls_upgrade_client(conn, opts);
putln("negotiated ", info["tls_version"]); // e.g. TLS1.3
2. Certificate authority
| Builtin | Signature | Returns |
|---|---|---|
tls_generate_ca |
(options?) |
{cert_pem, key_pem, serial} |
tls_generate_cert |
(options?) |
{cert_pem, key_pem, serial} (self-signed leaf) |
tls_sign_cert |
(caCertPem, caKeyPem, options?) |
{cert_pem, key_pem, serial} (CA-signed leaf) |
Options: common_name, organization, dns_names (array), ip_addresses
(array), days. Keys are ECDSA P-256; certificates are PEM-encoded.
tls_sign_cert is what makes interception possible: mint a leaf certificate for
whatever host the client asked for, signed by a CA the client already trusts.
let ca, err = tls_generate_ca({"common_name": "Mutant Dev CA"});
let ca_cert = ca["cert_pem"];
let ca_key = ca["key_pem"];
let leaf_opts = {"common_name": "example.com", "dns_names": ["example.com"]};
let leaf, err = tls_sign_cert(ca_cert, ca_key, leaf_opts);
3. HTTP message inspection
| Builtin | Signature | Returns |
|---|---|---|
http_parse_request |
(raw) |
{method, url, path, host, proto, query, headers, body} |
http_parse_response |
(raw) |
{status, status_text, proto, headers, body} |
http_build_request |
(request) |
raw request string (adds Content-Length when a body is present and none was supplied) |
http_build_response |
(response) |
raw response string (adds Content-Length) |
http_conn_read_request |
(handle, timeoutMs) |
parsed request off a live socket |
http_conn_read_response |
(handle, timeoutMs) |
parsed response off a live socket |
The http_conn_read_* builtins read exactly one message with correct
Content-Length / chunked framing (bodies are capped at 32 MiB). Byte reads
(net_conn_read) and framed reads share the same buffered stream per handle, so
they can be mixed safely on one connection.
Putting it together: an interception proxy
The CONNECT interception flow (see examples/network/mitmproxy.mut):
client mutant proxy upstream
GET/CONNECT ───────────────────▶ net_accept + http_conn_read_request
│ (CONNECT host:443)
200 Established ◀──────────────────┤ net_conn_write
│ tls_sign_cert(ca, host)
TLS handshake ◀────────────────▶ net_tls_upgrade_server(client, leaf)
real request (encrypted) ─────────▶ http_conn_read_request ── inspect ──▶
│ net_connect + net_tls_upgrade_client
│ http_build_request ─────────▶ origin
│ http_conn_read_response ◀───── origin
response ◀─────────────────────────┤ http_build_response + net_conn_write
Run it and point a client's HTTP+HTTPS proxy at 127.0.0.1:8080, trusting the
CA certificate it prints at startup.
Language gotchas (pre-existing, not specific to these builtins)
Writing multi-step network programs surfaces three parser/VM quirks worth knowing; the examples are written to avoid them:
- No
;after afor (...) { }block. A trailing semicolon there is parsed as an empty statement and fails (no prefix parse function for ;). - Don't pass array/hash literals as call arguments alongside other
arguments — bind the literal to a
letfirst. Passing a composite literal inline can corrupt an earlier argument. Safe:let opts = {...}; f(a, b, opts); - Avoid top-level
return. Wrap program logic in a function and call it;returninside a function behaves correctly, but a top-levelreturnmisbehaves.