Module networking
Every WebAssembly module states how far it reaches on the network. The engine holds each module process to that statement. A module that may reach the internet uses ordinary fetch, WebSocket, DNS and TLS. All of it travels on the module process’s own sockets inside the VM, so it follows the rules every guest program follows. Nothing leaves the VM until your application connects it through a relay and the user allows the session. Only TCP ports 80 and 443 go out. Disconnecting cuts everything off at once. Module code never uses the browser’s own network.
The rules below are numbered N-1 to N-10, as in the engine’s architecture record ADR 0029. A module author follows five of them in practice:
- declare the reach;
- use the host’s network;
- adapt an interpreter’s network layer with the SDK’s helpers;
- handle being offline with one clear message;
- test with the SDK’s fake network.
N-1 One declared reach
Section titled “N-1 One declared reach”A module’s requires.host names at most one reach token:
| Reach | Declaration | The module may | It gets |
|---|---|---|---|
| none | no token | Open no connections of its own. A service still serves programs in the VM through expose. |
Nothing network-related. fetch and WebSocket fail with “network not declared: add net.vm or net.internet to requires.host”. |
| VM-only | net.vm |
Connect to programs in this VM: loopback, Unix sockets and the VM’s own addresses. Resolve names from the VM’s /etc/hosts. |
fetch, Request, Response, Headers, WebSocket and host.net |
| internet | net.internet (includes VM-only) |
Also reach public hosts on TCP 80 and 443 through the VM’s relay connection, with DNS through the VM’s resolver. | The same |
- Naming both tokens is refused.
sockets.connect, the raw client sockets ofhost.sockets, needs a reach token.oxidevm wasm buildrefuses it alone, so everymodule.jsonbuilt from now on states its reach.- A
module.jsonbuilt by an earlier CLI that namessockets.connectalone has VM-only reach. Rebuild it withnet.internetto reach the internet. - The tokens need SDK 0.0.10 or later (Userspace API 146).
module.jsonstays at schema 1.
N-2 Disclosure, never an allowlist
Section titled “N-2 Disclosure, never an allowlist”A module with net.internet may add network:
network: { hosts: ["pypi.org", "*.pythonhosted.org"], purpose: "Installs Python packages that a program asks for." },hosts: up to 32 DNS names it contacts by default. A leading*.is allowed.purpose: one sentence of up to 200 characters.
The install review and oxidevm wasm doctor show them. Nothing enforces them, because an interpreter reaches whatever host its program asks for. network without net.internet is invalid.
N-3 Only the host’s network
Section titled “N-3 Only the host’s network”Module workers run in the VM’s module frame, whose content security policy allows no network (connect-src blob:). For a module with a reach, the runner installs fetch, Request, Response, Headers, WebSocket and host.net. They run on the module process’s own VM sockets:
- DNS: names resolve from the VM’s
/etc/hosts. With internet reach, the runner also asks the nameservers in the VM’s/etc/resolv.confover the process’s own datagram socket. The module gets the answers a guest program gets. A VM-only module resolves from/etc/hostsalone. - TLS: runs inside the module’s worker, end to end, through the engine’s TLS module. The VM fetches that module once, the first time a module with a reach starts, and never in a VM whose modules have none.
fetch: follows Node’s semantics, not a web page’s. There is no CORS, any request header is allowed, and it handles redirects, gzip and deflate, keep-alive, streamed bodies andAbortSignal. Timeouts are 10 s to connect, and 300 s for the response head and between body reads.WebSocket: the standard client over RFC 6455.
The browser’s other network APIs do not work in a module worker: XMLHttpRequest, EventSource, navigator.sendBeacon, RTCPeerConnection, WebTransport, and import() or importScripts of anything but a blob: URL. oxidevm wasm build --check warns about bundled uses of them, with locations (adapter-network). oxidevm wasm test fails network-direct when a module tries one.
N-4 Adapting a program’s own network layer
Section titled “N-4 Adapting a program’s own network layer”| Program | Use |
|---|---|
| Emscripten | host.kit.emscripten.network(host, Module) connects the program’s SOCKFS stream sockets (blocking, through host.net.connectSync) and getaddrinfo to the VM network. The program must export SOCKFS, DNS and FS through EXPORTED_RUNTIME_METHODS. Listening sockets are refused with EOPNOTSUPP. |
| Without TLS of its own | host.net.connect({ host, port, tls: true, alpn }), or the sans-IO session of host.kit.tls.session and host.kit.tls.handshakeSync for a program that moves the bytes itself. |
| WASI preview 1 | It has no sockets. Its JavaScript glue uses fetch or host.net. |
N-5 Offline, revoked and refused
Section titled “N-5 Offline, revoked and refused”| Situation | fetch |
Sockets and host.net |
|---|---|---|
| Before the VM is connected | TypeError whose cause has code: "EPERM", reason: "vm-local-only" |
EPERM |
| After a revoke | ECONNRESET |
ECONNRESET |
| Beyond the declared reach | EACCES, reason: "module-network-reach" |
EACCES |
| A certificate the VM does not trust | Node’s codes, such as UNABLE_TO_GET_ISSUER_CERT_LOCALLY, ERR_TLS_CERT_ALTNAME_INVALID or CERT_HAS_EXPIRED |
The same |
The cause is a NetError, exported from @oxidevm/sdk/wasm. A command prints one line from host.kit.net.describe(error, argv0) and returns 69, MODULE_EXIT.unavailable (sysexits EX_UNAVAILABLE). A service fails only the affected request and stays ready. Nothing retries in a loop.
The adapter bundle imports no SDK value except defineModule, so write the number in adapter code and use the constant in tests:
// module.ts declares requires: { host: ["files.sync", "net.internet"] }.async main(io) { const [output = "rates.json"] = io.args; try { const signal = AbortSignal.any([host.signal, AbortSignal.timeout(30_000)]); const response = await fetch("https://api.example.com/rates", { signal }); if (!response.ok) return host.fail(1, `rates: HTTP ${String(response.status)}`); host.fs.writeFile(output, new Uint8Array(await response.arrayBuffer())); return 0; } catch (error) { // For example "rates: network unavailable: this VM is not connected to the internet". host.log(host.kit.net.describe(error, "rates")); return 69; // MODULE_EXIT.unavailable }},N-6 Cancellation and bounds
Section titled “N-6 Cancellation and bounds”Every request carries host.signal, or a signal derived from it, and a timeout. Every body is streamed or bounded.
N-7 Trust and credentials
Section titled “N-7 Trust and credentials”- TLS always verifies against the VM’s own trust store,
/etc/ssl/cert.pem, which is whatcurlin the image trusts. No host API switches verification off, and the SDK bundles no CA data. - Credentials reach a module only through its options or guest files. Its frame holds no page cookies or storage.
N-8 Limits
Section titled “N-8 Limits”- Internet means TCP 80 and 443 plus DNS, the relay’s policy.
- Nothing on the internet can connect in. Services serve VM clients through
expose. - Programs that a module starts with
process.spawnare ordinary Linux programs under the VM’s own network policy, not the module’s reach. The install review says so. - Module code gets no raw datagram sockets. The runner uses one internally for DNS.
N-9 Testing
Section titled “N-9 Testing”testHost from @oxidevm/sdk/wasm/testing runs a module definition through the SDK’s real runner against an in-memory process and network, with the same reach checks and errors:
import { testHost } from "@oxidevm/sdk/wasm/testing";import definition from "../module.ts";
// Local-only by default: the command prints one line and exits 69.const offline = await testHost(definition, { args: ["rates.json"] }).run();
// Connected, with an in-memory server for the origin.const online = await testHost(definition, { args: ["rates.json"], net: { connected: true, http: { "https://api.example.com": () => Response.json({ eur: 1 }) } },}).run();net.vm serves handlers by port at the VM’s own address. A run resolves with status, stdout, stderr, files and the signal it ended by, if any.
oxidevm wasm test checks three network properties in a real browser:
| Property | Holds when |
|---|---|
network-direct |
The module tried no browser network API. |
network-declared |
The module connected to nothing beyond its declared reach. |
network-offline |
With the VM local-only, a command run with your --arg values ends with a status from 0 to 119 and, when it fails, a line on standard error within its startup bound. A service stays ready. It applies to every module, with a reach or without one, because the VM is local-only for both. |
N-10 Documentation
Section titled “N-10 Documentation”A module’s README has a “Network” section: the reach, the disclosed hosts and purpose, and what happens offline. oxidevm wasm init --network none|vm|internet generates the declaration, that section, AGENTS.md rules and tests. With a reach it adds src/net.ts, a bounded and cancellable request, and test/net.test.ts. The default is none.
Enforcement
Section titled “Enforcement”- Rust decides. Every module process carries its declared reach from its launch. The socket owner refuses
connectandsendtobeyond that reach withEACCESand the trace reasonmodule-network-reach, whichreadJSProcessTracereports as a refusal. Listeners the runner owns forexposeare unaffected, and child processes do not inherit the reach. - The broker in the module frame refuses
host.netoperations beyond the reach before they reach Rust, as defence in depth. - The runner installs the network globals only for VM-only or internet reach.
For applications that install a module
Section titled “For applications that install a module”- The install review’s
networkrow shows the reach, the hosts the module discloses and its purpose. A release that widens its reach asks for consent again. - A module with internet reach reaches the internet only after your application connects the VM and the user allows the session in the relay. Until then its requests fail with
EPERM. - Its traffic is ordinary traffic of the VM. It appears in a personal relay’s destination list, and TLS ends inside the module frame, never at the relay or in your page.
- An interpreter command lets every program in the VM that runs it use its reach.
- A module is not a sandbox for the VM.