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.
Choose a relay
Section titled “Choose a relay”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.
Connect while creating the VM
Section titled “Connect while creating the VM”With network: { mode: "relay", connection }, createVM connects once the VM is ready and before its promise resolves:
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.
- Create the VM with the default local network.
- Watch
vm.network, so you see every state, including the code. - Call
vm.network.connect({ kind: "personal" }). It resolves once the user has approved and the connection is ready.
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.
Network states
Section titled “Network states”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 |
Turn internet off
Section titled “Turn internet off”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.
What guest programs can reach
Section titled “What guest programs can reach”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.
Errors
Section titled “Errors”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. |
The session route for a server relay
Section titled “The session route for a server relay”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:
- Authenticate the user and check that they may use this workspace. Answer 401 or 403 to refuse; the SDK reports
authentication-failed. - Ask your relay for a session ticket for exactly these fields, with a credential that never leaves your server.
- 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.