capsite/toybox: a switchboard, the vats' only way out

A vat granted a host (PUT /<vat>?egress=<host> on the admin socket) gets
Egress as its globalOutbound, with the host in ctx.props where the vat
can't change it. Egress forwards matching requests, WebSocket upgrades
included, to the switchboard and refuses the rest with a 403; with no
grant, fetch still throws.

The switchboard is a separate workerd: a Switchboard DO that calls fetch
and returns the response, so a 101's webSocket passes through. toybox
reaches it over a unix socket in HTTP proxy style, which keeps the full
URL, scheme included. Its network service is the outer fence, with TLS
trusting the browser CAs; systemd units for it join toybox's, and only
the toybox group can open its socket.

The loaded Worker is now named by vat and incarnation, not by code hash:
the same code with a different grant must not share a Worker. The new
incarnation table sits beside the old ones; vats installed before it
read as incarnation 0 with no grant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dckcandClaude Opus 5.5 committed 2026-10-13 21:44:29 -05:00
1 parent 88739d9495
commit 7bf6eb44c7
13 files changed
+279 -51

No files matched your search

+92 -26
View File
@@ -1,12 +1,13 @@
/** @file toybox end to end: a real workerd, an admin socket, a public port. */
/** @file toybox end to end: a real workerd, an admin socket, a public port, a switchboard. */
import { spawn } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { request } from "node:http";
import { createServer } from "node:net";
import { createServer as createHttpServer, request } from "node:http";
import { connect, createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, before, test } from "node:test";
import assert from "node:assert/strict";
import { WebSocketServer } from "ws";
const here = new URL("..", import.meta.url).pathname;
// the package binary, not the .bin shim: the shim forks it, so a kill would orphan workerd
@@ -21,11 +22,19 @@ const freePort = () =>
});
});
/** Resolves once something accepts connections at `opts` (a port or a socket path). */
const accepting = (opts) =>
new Promise((resolve) => {
const s = connect(opts, () => (s.end(), resolve(true)));
s.on("error", () => resolve(false));
});
/** @param {string} socketPath the admin socket; holding it is the authority to install */
const makeAdmin = (socketPath) => ({
install: (vat, src) =>
install: (vat, src, egress) =>
new Promise((resolve, reject) => {
const req = request({ socketPath, path: `/${vat}`, method: "PUT", agent: false }, (res) => {
const path = egress ? `/${vat}?egress=${encodeURIComponent(egress)}` : `/${vat}`;
const req = request({ socketPath, path, method: "PUT", agent: false }, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(body.trim()));
@@ -37,49 +46,80 @@ const makeAdmin = (socketPath) => ({
/** @param {string} base the public origin */
const makePublic = (base) => ({
get: async (vat) => (await fetch(`${base}/${vat}`)).text(),
get: async (path) => (await fetch(`${base}/${path}`)).text(),
put: async (vat, src) => (await fetch(`${base}/${vat}`, { method: "PUT", body: src })).status,
});
/** Run workerd on `dir`'s state until `stop()`; resolves once the port answers. */
const startWorkerd = async (dir, port) => {
await rm(join(dir, "admin.sock"), { force: true }); // left behind by a crash; bind() would fail
const child = spawn(
workerd,
["serve", "--experimental", join(here, "config.capnp"),
"-s", `http=127.0.0.1:${port}`, "-s", `admin=unix:${join(dir, "admin.sock")}`,
"-d", `state=${join(dir, "state")}`],
{ stdio: ["ignore", "ignore", "pipe"] },
);
/**
* Run workerd on `config` until `stop()`; resolves once `listening` accepts.
*
* @param {string} config
* @param {string[]} args socket, directory, and external overrides
* @param {{ port: number } | { path: string }} listening
* @param {string[]} stale socket files a crash leaves behind; bind() would fail on them
*/
const startWorkerd = async (config, args, listening, stale = []) => {
for (const f of stale) await rm(f, { force: true });
const child = spawn(workerd, ["serve", "--experimental", join(here, config), ...args], {
stdio: ["ignore", "ignore", "pipe"],
});
let log = "";
child.stderr.on("data", (c) => (log += c));
const exited = new Promise((resolve) => child.on("exit", resolve));
for (let i = 0; i < 100; i++) {
try {
await fetch(`http://127.0.0.1:${port}/`);
// SIGKILL: a crash is the harder durability test
return { stop: () => (child.kill("SIGKILL"), exited) };
} catch {
await new Promise((r) => setTimeout(r, 50));
}
// SIGKILL: a crash is the harder durability test
if (await accepting(listening)) return { stop: () => (child.kill("SIGKILL"), exited) };
await new Promise((r) => setTimeout(r, 50));
}
child.kill("SIGKILL");
throw new Error(`workerd did not start: ${log}`);
};
let dir, port, server, admin, pub;
/** An HTTP server that answers GET with "plain ok" and echoes WebSocket messages. */
const startEcho = async () => {
const server = createHttpServer((_req, res) => res.end("plain ok"));
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => ws.on("message", (m) => ws.send(String(m))));
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
return {
host: `127.0.0.1:${server.address().port}`,
stop: () => new Promise((resolve) => (wss.close(), server.closeAllConnections(), server.close(resolve))),
};
};
let dir, port, server, switchboard, echo, admin, pub;
const startToybox = () =>
startWorkerd(
"config.capnp",
[
"-s", `http=127.0.0.1:${port}`, "-s", `admin=unix:${join(dir, "admin.sock")}`,
"-d", `state=${join(dir, "state")}`, "-e", `switchboard=unix:${join(dir, "switchboard.sock")}`,
],
{ port, host: "127.0.0.1" },
[join(dir, "admin.sock")],
);
before(async () => {
dir = await mkdtemp(join(tmpdir(), "toybox-"));
await mkdir(join(dir, "state"));
port = await freePort();
server = await startWorkerd(dir, port);
echo = await startEcho();
switchboard = await startWorkerd(
"switchboard.capnp",
["-s", `http=unix:${join(dir, "switchboard.sock")}`],
{ path: join(dir, "switchboard.sock") },
[join(dir, "switchboard.sock")],
);
server = await startToybox();
admin = makeAdmin(join(dir, "admin.sock"));
pub = makePublic(`http://127.0.0.1:${port}`);
});
after(async () => {
await server?.stop();
await switchboard?.stop();
await echo?.stop();
await rm(dir, { recursive: true, force: true });
});
@@ -114,6 +154,32 @@ test("code and state survive a restart", async () => {
await admin.install("dave", await app("counter-v1.js"));
assert.equal(await pub.get("dave"), "v1 count 1\n");
await server.stop();
server = await startWorkerd(dir, port);
server = await startToybox();
assert.equal(await pub.get("dave"), "v1 count 2\n");
});
const probe = (vat, target, ws) =>
pub.get(`${vat}?url=${encodeURIComponent(`http://${target}/`)}${ws ? "&ws=1" : ""}`);
test("a vat granted a host reaches it through the switchboard: fetch and WebSocket", async () => {
await admin.install("erin", await app("egress-probe.js"), echo.host);
assert.equal(await probe("erin", echo.host), "got: plain ok\n");
assert.equal(await probe("erin", echo.host, true), "echo: hello\n");
});
test("same code, no grant: no fetch", async () => {
await admin.install("frank", await app("egress-probe.js"));
assert.match(await probe("frank", echo.host), /^refused: This worker is not permitted to access the internet/);
});
test("a grant covers one host only", async () => {
await admin.install("grace", await app("egress-probe.js"), "127.0.0.1:1");
assert.equal(await probe("grace", echo.host), `got: egress to ${echo.host} not granted\n\n`);
assert.match(await probe("grace", echo.host, true), /^no websocket: 403 egress to .* not granted/);
});
test("a grant survives a restart", async () => {
await server.stop();
server = await startToybox();
assert.equal(await probe("erin", echo.host, true), "echo: hello\n");
});