Networking
NanoVM has no TCP/IP stack to the internet. Outbound HTTP(S) goes through the host's fetch() via the /dev/__net__ device; inbound requests to servers in the VM come from the service worker. Browser rules (CORS) apply to every outbound request.
/dev/__net__: the guest writes <METHOD> <URL>, the host performs fetch() and frames the HTTP/1.1 response backThe fetch bridge: /dev/__net__
A program opens /dev/__net__ read-write, writes a request head (GET https://…, optional headers, a blank line, an optional body) and reads back HTTP/1.1 <status> with headers and body until EOF. The nfetch catalog app is a tiny client for it.
Fetch JSON from the guest
nfetch runs on the RISC-V runner; the bytes come from the npm registry through the page's fetch().
nfetch https://registry.npmjs.org/left-pad/latest | grep -o '"version":"[^"]*"' nfetch -i https://registry.npmjs.org/left-pad/latest | head -c 15
installed nfetch@0.1.0 (1 file, verified) $ nfetch https://registry.npmjs.org/left-pad/latest | grep -o '"version":"[^"]*"' "version":"1.3.0" $ nfetch -i https://registry.npmjs.org/left-pad/latest | head -c 15 HTTP/1.1 200
The same device from Node.js
On the host engine, fs routes /dev/__net__ to the same bridge.
const fs = require("fs");
const fd = fs.openSync("/dev/__net__", "r+");
fs.writeSync(fd, "GET https://registry.npmjs.org/left-pad/latest\n\n");
const buf = Buffer.alloc(65536);
let raw = "";
for (let n; (n = fs.readSync(fd, buf, 0, buf.length, null)) > 0; ) raw += buf.toString("utf8", 0, n);
const body = JSON.parse(raw.slice(raw.indexOf("\r\n\r\n") + 4));
console.log(raw.split("\r\n")[0].trim(), "|", body.name, body.version);HTTP/1.1 200 | left-pad 1.3.0
CORS
The bridge is the browser's fetch(), so the browser's rules apply: a target must send Access-Control-Allow-Origin for your page, or the request fails. The guest then sees HTTP/1.1 502 Bad Gateway. If a request works with fetch() from your page, it works from the guest.
The CORS proxy
For CORS-blocked targets (git hosts, most APIs) the bridge can retry through a proxy you run. The catalog repository ships one as a Cloudflare Worker (catalog/cors-proxy). It fails closed: it answers only browser origins listed in ALLOWED_ORIGINS and target hosts in its ALLOW list, https:// only, GET/HEAD (plus POST to git smart-HTTP endpoints), and forwards a fixed header allow-list (Authorization only with FORWARD_AUTHORIZATION=true).
cd catalog/cors-proxy
npx wrangler deploy # → https://nano-cors-proxy.<account>.workers.dev
// then, in your page
nano.raw.setNetwork({ corsProxyUrl: "https://nano-cors-proxy.<account>.workers.dev" });The bridge always tries a direct fetch(url) first and only on a CORS or network failure retries fetch(corsProxyUrl + "?apiurl=" + encodeURIComponent(url)). Never point it at a proxy you don't operate.
Turning the network off
setNetwork({ disabled: true })
With the device disabled, opening /dev/__net__ fails with EACCES; setNetwork({}) turns it back on (without a proxy).
await nano.installApp("nfetch");
nano.raw.setNetwork({ disabled: true });
print((await nano.shExec("nfetch https://registry.npmjs.org/left-pad/latest")).stdout.trim());
nano.raw.setNetwork({});
print((await nano.shExec("nfetch https://registry.npmjs.org/left-pad/latest | head -c 1")).stdout);nfetch: cannot open /dev/__net__: Permission denied (os error 13)
{What doesn't work
- Ordinary sockets to the internet: there is no route out of the VM except
/dev/__net__. BusyBoxwgetreportsbad address, and Node.jsfetch/httpson the RISC-V runner fail withgetaddrinfo ECONNREFUSED. - On the host engine, Node's global
fetch()andhttps.get()don't reach the internet in SDK 0.2.10 (the process ends beforefetchsettles;https.getconnects to a local port). Use the/dev/__net__device as above. - Raw TCP/UDP, ICMP (
ping), and DNS lookups: browsers don't expose them.
Inbound traffic (previewing a server in an iframe) is covered on Virtual server and Serve mode.