Skip to content
GitHub Get Started
Reference

Networking

This is the internals view of agentOS networking: the kernel socket table, the layers a request crosses, and where policy is enforced. For the user-facing API (vmFetch, preview URLs, the confinement model from a caller’s perspective), see Networking & Previews. For the trust boundary this all sits inside, see Architecture.

The governing rule is that there is exactly one authoritative transport for everything VM-local: the kernel socket table. No part of guest networking opens a real host socket on its own. Guest fetch(), node:http, node:net, WASM TCP clients and servers, and host-into-guest requests (vmFetch / rt.fetch) all target the same listener table.

The socket table is the floor of the stack and the only component that actually moves bytes between two in-VM endpoints. It is per VM, so two VMs never share a listener or a connection.

  • It exposes POSIX-style primitives: socket_create, socket_bind_inet, socket_connect_inet_loopback, socket_read, socket_write, poll_targets.
  • Every call is owner-checked (the calling process must own the descriptor) and resource-accounted against the VM’s limits.
  • Failures return correct POSIX errnos (ECONNREFUSED, EACCES, …) so guest code branches the way it would on real Linux.
  • Connecting pairs two in-VM sockets and shuttles bytes between them. No host networking happens at this layer.

Because every server is a kernel TCP listener, a client never needs to know whether the server it is talking to is JS, WASM, raw TCP, or HTTP. HTTP is layered on top of kernel TCP bytes, so every listener lives in the one table and is reachable identically.

A request passes through four layers. Only the top and bottom understand HTTP; the middle two move bytes and enforce policy.

LayerRoleTrustLives in
4 · Guest bridgenode:http / node:net / fetch / undici shimuntrusted (V8 isolate)crates/execution/assets/v8-bridge.source.js
3 · Sync-RPC dispatchroutes net.connect, net.http_request, net.listen, …trustedcrates/sidecar/src/service.rs
2 · Execution & enforcementlistener state, host fetch client, permission checkstrusted (TCB)crates/sidecar/src/execution.rs
1 · Kernel socket tablebind / listen / connect / read / write, loopback routingtrusted (TCB floor)crates/kernel/src/socket_table.rs, kernel.rs

crates/kernel/src/kernel.rs exposes the primitives above. Loopback routing is the heart of VM-local networking: socket_connect_inet_loopback only succeeds against a socket that is actually bound and listening in the same VM’s table; otherwise it returns ECONNREFUSED. Resource-limit checks run before the two sockets are paired.

Layer 2: sidecar execution (enforcement point / TCB)

Section titled “Layer 2: sidecar execution (enforcement point / TCB)”

crates/sidecar/src/execution.rs is where policy is applied. Two roles matter for networking:

  • Listener state. build_javascript_socket_path_context walks every active process and records what is listening on which port, including a map of HTTP loopback targets keyed by (family, port). This is the source of truth a connect consults to learn that, say, “port 3000 is an HTTP server owned by process X, server Y.”
  • Host fetch client. When the host calls vmFetch / rt.fetch(), the sidecar resolves the target to a VM-owned kernel listener, opens its own kernel socket, connects over loopback, and speaks HTTP/1.1 to the guest server. This is the only HTTP client that lives in the sidecar (the host has no guest isolate to do framing for it).

crates/sidecar/src/service.rs routes the bridge calls guest code makes. The guest-to-guest loopback HTTP path lands here as net.http_request. It is the most security-sensitive RPC, so it is guarded in order:

  1. The host must be a loopback address.
  2. The applied network policy must permit the operation.
  3. The requested (process_id, server_id) must match a listener that is currently live.

That last check stops a guest from forging a target to reach a process it should not.

crates/execution/assets/v8-bridge.source.js is the Node-compatibility shim inside the untrusted V8 isolate. It presents node:http, node:net, fetch, and undici to guest code and translates them into Layer 3 bridge calls. http.createServer() is implemented on top of net.Server: each accepted byte socket is parsed as HTTP and dispatched to the guest’s request handler.

  • node:net (raw TCP). net.connect / net.createServer map directly onto kernel connect / bind + listen. The bytes are the payload; no framing is added.
  • node:http and fetch. A guest HTTP server is a net.Server whose accepted sockets are HTTP-parsed in the bridge. A guest HTTP client runs undici over a kernel-backed dispatcher (or a raw serializer for the loopback fast path). Either way the bytes travel as kernel TCP.
  • DNS. Name resolution is serviced by the kernel resolver, not the host. Outbound connections that leave the VM resolve through it, and the resolved addresses are then filtered by the egress allowlist (see below). DNS pinning ties the connection to the address that was checked, closing the resolve-then-reconnect TOCTOU gap.

There is no shared HTTP/TCP translation module. Because the wire between every endpoint is raw TCP bytes through the kernel, HTTP is framed and deframed at each edge that speaks HTTP. The kernel (Layer 1) and the sidecar routing (Layer 2) never parse HTTP. There are three independent codecs, one per kind of endpoint:

EndpointLives inEncode / decode
Guest HTTP serverguest bridgeparseLoopbackRequestBuffer (bytes to object), serializeLoopbackResponse (object to bytes), wired per accepted socket by attachHttpServerSocket
Guest HTTP clientguest bridgeundici over a kernel-backed dispatcher, or serializeRawHttpRequest + waitForRawHttpResponse
Host fetch clientsidecar executionserialize_kernel_http_fetch_request (request to bytes), parse_kernel_http_fetch_response (bytes to JSON)

A WASM HTTP server or client does its own framing in guest code (reading the request line, writing a response with standard C socket calls). The kernel does not help it; it is just bytes, the same as for the JS endpoints.

  • Host to guest (vmFetch / rt.fetch). The sidecar resolves the port to a VM-owned kernel listener, opens a sidecar-owned kernel socket, connects over loopback, serializes the request bytes, drives the target process forward so it can accept and respond, then parses the response bytes back into the host response object. It is fail-closed: no DNS, no external networking, no host-loopback fallback. If no VM-owned listener exists, it returns a missing-listener error.
  • Guest to guest. net.connect goes through the sidecar, which returns a loopback HTTP target handle. The guest sends the request through net.http_request, which dispatches into the target process’s request handler. Cross-process loopback passes through the enforcement point rather than taking an in-isolate shortcut.
  • Cross-runtime (JS and WASM, either direction). Client and server connect through a kernel loopback socket pair and exchange raw bytes. JS to WASM, WASM to JS, and WASM to WASM all use the same path; only the side that runs the HTTP codec differs.
  • Guest outbound to host or external. Connections that do not target a VM-owned listener take the external network path: permission checks, DNS pinning, then a real host TcpStream. Reaching a host loopback port still requires an explicit loopback exemption entry.

Guest networking is confined by three distinct controls plus the loopback-only default. The permission policy and limits are trusted configuration; the guest executor is the untrusted subject they bind.

Guest listeners are reachable only over loopback (127.0.0.1 / ::1) inside the VM.

  • Binding to 0.0.0.0 or :: does not widen this: the kernel normalizes the unspecified address down to loopback, so the listener still answers only on loopback.
  • A connection that originates outside the loopback interface and targets a port the VM does not own is refused with EACCES, noting the port is not exempt.
  • This confinement is independent of the permission policy. Even with the network allowed, a guest server stays loopback-only unless its port is explicitly exempted.

These are often conflated but are separate. They stack, and a request must pass every one that applies:

  1. Permission policy (network.listen / network.connect). Decides whether the guest may open a listener or initiate an outbound connection at all. A blocked operation fails with blocked by network.listen policy or blocked by network.connect policy.
  2. Loopback confinement. Decides who may reach an already-permitted guest listener. By default only loopback inside the VM; a per-port exemption loosens it.
  3. DNS / egress allowlist. Constrains where permitted outbound connections may go. The kernel filters resolved addresses, blocking outbound access to restricted ranges, so an allowed connect can still be refused by destination.

The per-port loopback exemption belongs to layer 2 only. It is a trusted, per-port whitelist that loosens the default loopback confinement (for example, exposing an in-VM dev server beyond loopback). It is not an egress control and grants no outbound reach; layers 1 and 3 still apply. It is configured with loopbackExemptPorts, a list of ports that are exempt from the SSRF checks at layer 2; each listed port is reachable from outside the loopback interface, while the permission policy and egress allowlist continue to apply.

Every guest connect, listen, read, and write passes through sidecar ownership and kernel owner checks. Guest-to-guest loopback is allowed only when the destination is a VM-owned listener and the applied network policy permits the connect. Host-loopback access from guest code is separate and still requires a loopback exemption plus the applied network policy. Long-lived waits must not block the sync-RPC path, so the stack uses stream events, bounded polling, and kernel socket waits with explicit timeouts.

A preview URL is port forwarding for a VM service: a time-limited, signed, publicly reachable URL that proxies HTTP to a port inside the VM. Mechanically it reuses the host-to-guest path:

  • A signed token is minted for a (VM, port) pair with an expiration, capped by preview.maxExpiresInSeconds. Tokens are stored in SQLite, survive sleep/wake cycles, and expired ones are cleaned up automatically.
  • An incoming request to the preview path is authenticated against the token, then proxied into the VM exactly like vmFetch: resolve the port to a VM-owned kernel listener, connect over loopback, frame HTTP/1.1, drive the target process, and stream the response back. The same fail-closed, VM-owned-listener-only rules apply.
  • CORS is enabled so browsers can reach preview URLs from any origin.
  • Revocation (expireSignedPreviewUrl) invalidates the token immediately, after which the proxy refuses the request before touching the socket table.

Because previews ride the host fetch path, they are subject to loopback confinement at the kernel but not to the guest egress allowlist: the request enters the listener from the host side and never becomes guest outbound traffic.