Skip to content

Networking

Networking inside a VM is local by default. Guest programs can talk to each other over sockets, and previews reach guest servers, but nothing in the VM can reach the internet. A browser tab cannot open the TCP connections that Linux programs make, so outside access goes through a relay: a native program, run by the user or by you, that opens those connections for the guest.

Connect a relay when guest programs need the internet, for example to download packages with npm install. Everything else still runs in the browser: files, processes, installs and builds.

A VM starts with network: { mode: "local" }. To go online, it connects to one relay, described by a RelayConnectionOptions value:

Connection Runs on Who allows a session
{ kind: "personal", workspaceName? } The user’s own computer The user, who compares a six-digit pairing code and approves the session in the relay.
{ kind: "server", url, sessionUrl?, headers? } A server you operate Your backend, through the session route at sessionUrl.

The public Playground uses the personal relay flow. Server relays are for your own SDK applications. You operate HTTPS and application authorization. A relay URL alone does not grant access. The native server binary runs independently of the SDK and VM.

See Self-host a relay for signed downloads and native installation, or Host a relay on Fly.io for the hosted recipe.

The VM’s relay owner is vm.network, the same object for the VM’s whole lifetime. Both modes use it the same way.

With network: { mode: "relay", connection }, createVM connects once the VM is ready and before its promise resolves:

online-vm.ts
import { createVM } from "@oxidevm/sdk";
export function openOnlineProject() {
return createVM({
mode: "userspace",
workspaceId: "my-project",
network: {
mode: "relay",
connection: {
kind: "server",
url: "https://relay.example.com/",
sessionUrl: "/api/oxidevm/relay-session",
},
},
});
}

If the relay refuses the session or the connection fails, the SDK disposes the VM and createVM rejects with the relay’s error. Creation cannot show a pairing code, so connect a personal relay after creation, as shown in the next section.

The server options:

Option Meaning
url The relay’s address. HTTPS, or HTTP on localhost and 127.0.0.1. No user name, password, query or fragment.
sessionUrl Your backend route that authorizes this user and workspace. A relative URL resolves against the page. Without it, the SDK sends the session request to v2/sessions under url.
headers Browser-safe application credentials for the session request, such as a CSRF token. At most 32 headers. Names such as Cookie, Origin, Host, Connection and Sec-* are refused.

Invalid options reject with INVALID_ARGUMENT before anything connects.

Show the pairing code for a personal relay

Section titled “Show the pairing code for a personal relay”

The user runs the OxideVM relay on their own computer, and the SDK looks for it at http://127.0.0.1:7337/. The user approves each session in the relay after comparing its code with the one your page shows, so your page must display that code.

  1. Create the VM with the default local network.
  2. Watch vm.network, so you see every state, including the code.
  3. Call vm.network.connect({ kind: "personal" }). It resolves once the user has approved and the connection is ready.
personal-relay.ts
import { VMError, type NetworkState, type VM } from "@oxidevm/sdk";
function describe(state: NetworkState): string {
switch (state.kind) {
case "off":
return "Internet is off.";
case "discovering":
return "Looking for the relay on this computer.";
case "awaiting-approval":
return `In the relay, check that code ${state.code} is shown for ${state.workspaceName}, then approve it.`;
case "connecting":
return "Connecting.";
case "connected":
return "Internet is on.";
case "failed":
return `Internet is off (${state.reason}).`;
}
}
export async function enableInternet(vm: VM, status: HTMLElement, signal: AbortSignal): Promise<void> {
vm.network
.watch((state) => { status.textContent = describe(state); }, { signal })
.catch((error: unknown) => console.error(error));
try {
await vm.network.connect({ kind: "personal", workspaceName: "My project" }, { signal });
} catch (error) {
if (error instanceof VMError) status.textContent = `${error.code}: ${String(error.detail?.["reason"] ?? error.message)}`;
throw error;
}
}

Call it with a VM from createVM({ mode: "userspace" }) or createVM({ mode: "system" }), and abort signal when the view closes. While the relay waits for approval, the state carries code, origin (your page’s origin), workspaceId, workspaceName and expiresAt. The code only helps the user match the request to your page; it is not a password. If nobody approves before expiresAt, the attempt fails with approval-expired.

workspaceName (at most 100 characters) is the name the relay shows. Without it, the relay shows the VM’s workspace ID, which for an ephemeral VM is a generated string. The browser may ask the user to allow your page to reach devices on the local network; if no relay answers, the attempt fails with discovery-failed.

vm.network.state holds the current state. vm.network.watch(onChange, { signal }) calls onChange right away with the current state and then on every change. Its promise resolves when signal aborts or the VM is disposed, and rejects if onChange throws.

state.kind Meaning Extra fields
off No relay. Outside connections fail.
discovering Looking for the personal relay on this computer.
awaiting-approval Waiting for the user to approve in the relay. code, origin, workspaceId, workspaceName, expiresAt
connecting Opening the connection. For a server relay, this includes the session request. relay ("personal" or "server")
connected Guest programs can reach the internet. relay, expiresAt
failed The attempt or an open connection failed. Internet is off. reason
offline.ts
import type { VM } from "@oxidevm/sdk";
export async function goOffline(vm: VM): Promise<void> {
await vm.network.disconnect();
}

disconnect() is safe to call more than once. Files, processes, terminals and previews keep working. Restoring a restore point, vm.suspend() and vm.dispose() also revoke access, and restore points and backups never store it, so connect again after a restore. To switch relays, disconnect first: connect rejects with INVALID_ARGUMENT while another connection is open or pending.

Through a relay, guest programs open TCP connections to public addresses on ports 80 and 443, and look up names through the relay. Other ports, other UDP traffic, and private, loopback, link-local or other reserved addresses are refused, and those connections fail inside the guest. TLS stays end to end between the guest program and the server: the relay forwards encrypted bytes.

A failed attempt rejects with a VMError and leaves internet off. detail.reason names the exact cause, and the state becomes failed with the same reason. Branch on code and detail.reason, never on the message.

code detail.reason
RELAY_ERROR discovery-failed (no personal relay answered), relay-not-found, approval-denied, approval-expired, authentication-failed (your session route answered 401 or 403), version-mismatch, timed-out, disconnected, connection-refused, connection-reset, network-unreachable, host-unreachable
PERMISSION_DENIED permission-denied, destination-denied
QUOTA_EXCEEDED quota-exceeded
PROTOCOL_ERROR protocol-error
INVALID_ARGUMENT No reason: the options are invalid, or another connection is open or pending.
ABORTED No reason: your signal aborted the attempt.

A relay URL alone grants no access. When a VM connects to a server relay, the SDK sends a POST to sessionUrl with your headers and the page’s cookies. Its JSON body holds version (2), workspaceId (the VM’s workspace ID), browserSessionId and origin (your page’s origin). Keep the route on your app’s own origin; a relative sessionUrl does that.

Your route should:

  1. Authenticate the user and check that they may use this workspace. Answer 401 or 403 to refuse; the SDK reports authentication-failed.
  2. Ask your relay for a session ticket for exactly these fields, with a credential that never leaves your server.
  3. Return the relay’s JSON response unchanged, with Cache-Control: no-store.

If your app sends a Content Security Policy, allow the relay in connect-src. See Deploying.