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 kernel socket table
Section titled “The kernel socket 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.
The four layers
Section titled “The four layers”A request passes through four layers. Only the top and bottom understand HTTP; the middle two move bytes and enforce policy.
| Layer | Role | Trust | Lives in |
|---|---|---|---|
| 4 · Guest bridge | node:http / node:net / fetch / undici shim | untrusted (V8 isolate) | crates/execution/assets/v8-bridge.source.js |
| 3 · Sync-RPC dispatch | routes net.connect, net.http_request, net.listen, … | trusted | crates/sidecar/src/service.rs |
| 2 · Execution & enforcement | listener state, host fetch client, permission checks | trusted (TCB) | crates/sidecar/src/execution.rs |
| 1 · Kernel socket table | bind / listen / connect / read / write, loopback routing | trusted (TCB floor) | crates/kernel/src/socket_table.rs, kernel.rs |
Layer 1: kernel socket table
Section titled “Layer 1: kernel socket table”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_contextwalks 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).
Layer 3: sync-RPC dispatch
Section titled “Layer 3: sync-RPC dispatch”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:
- The host must be a loopback address.
- The applied network policy must permit the operation.
- 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.
Layer 4: guest bridge
Section titled “Layer 4: guest bridge”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.
How fetch, net, and dns route through it
Section titled “How fetch, net, and dns route through it”node:net(raw TCP).net.connect/net.createServermap directly onto kernelconnect/bind+listen. The bytes are the payload; no framing is added.node:httpandfetch. A guest HTTP server is anet.Serverwhose 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.
Where HTTP meets TCP
Section titled “Where HTTP meets TCP”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:
| Endpoint | Lives in | Encode / decode |
|---|---|---|
| Guest HTTP server | guest bridge | parseLoopbackRequestBuffer (bytes to object), serializeLoopbackResponse (object to bytes), wired per accepted socket by attachHttpServerSocket |
| Guest HTTP client | guest bridge | undici over a kernel-backed dispatcher, or serializeRawHttpRequest + waitForRawHttpResponse |
| Host fetch client | sidecar execution | serialize_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.
Data flows
Section titled “Data flows”- 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.connectgoes through the sidecar, which returns a loopback HTTP target handle. The guest sends the request throughnet.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.
Egress policy and loopback confinement
Section titled “Egress policy and loopback confinement”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.
Loopback-only by default
Section titled “Loopback-only by default”Guest listeners are reachable only over loopback (127.0.0.1 / ::1) inside the VM.
- Binding to
0.0.0.0or::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.
Three stacked controls
Section titled “Three stacked controls”These are often conflated but are separate. They stack, and a request must pass every one that applies:
- 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 withblocked by network.listen policyorblocked by network.connect policy. - Loopback confinement. Decides who may reach an already-permitted guest listener. By default only loopback inside the VM; a per-port exemption loosens it.
- DNS / egress allowlist. Constrains where permitted outbound connections may go. The kernel filters resolved addresses, blocking outbound access to restricted ranges, so an allowed
connectcan 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.
Trust and ownership
Section titled “Trust and ownership”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.
Preview URLs
Section titled “Preview URLs”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 bypreview.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.
Where to go next
Section titled “Where to go next”- Networking & Previews: the
vmFetchand preview URL API, with usage examples. - Architecture: the client / sidecar / executor trust boundary this stack lives inside.
- Security Model: the full in-scope and out-of-scope threat model.