The switchboard now serves a Cap'n Web Handle on each of its sockets:
public (no loopback or private addresses) and local. A socket's labels
are fixed in config, so holding the socket's fd is holding that handle;
constrain(labels) returns a narrower one, as Flower's Constrain returns
a narrower fd. Labels only accumulate: a missing key matches anything,
and a key both sides set must agree (labels.js, shared by both).
The switchboard enforces the labels; toybox's Egress no longer checks
URLs itself. It opens a session to its one switchboard socket, constrains
the handle to the vat's grant, and forwards through it; a WebSocket
crosses the session as a pair of message streams. A vat narrows its own
grant with env.egress.constrain().
Grants are now labels as JSON. One stored before, a bare host[:port],
reads as {host, port}.
pnpm start and the systemd units give toybox the public handle; the
switchboard unit binds the local one in its own 0700 runtime directory.
The tests give their toybox the local handle to reach an echo server,
and check that a toybox holding only the public one never reaches it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
28 lines
1.2 KiB
JavaScript
28 lines
1.2 KiB
JavaScript
import { DurableObject } from "cloudflare:workers";
|
|
|
|
/** `?url=<target>` fetches it through `env.egress` (narrowed by `&constrain=<labels as JSON>`, if given); with `&ws=1`, opens a WebSocket there and reports the echo of "hello". */
|
|
export class App extends DurableObject {
|
|
async fetch(request) {
|
|
const params = new URL(request.url).searchParams;
|
|
const target = params.get("url");
|
|
if (!this.env.egress) return new Response("no egress granted\n");
|
|
try {
|
|
const egress = params.has("constrain")
|
|
? await this.env.egress.constrain(JSON.parse(params.get("constrain")))
|
|
: this.env.egress;
|
|
if (!params.has("ws")) return new Response(`got: ${await (await egress.fetch(target)).text()}\n`);
|
|
const resp = await egress.fetch(target, { headers: { Upgrade: "websocket" } });
|
|
const ws = resp.webSocket;
|
|
if (!ws) return new Response(`no websocket: ${resp.status} ${await resp.text()}`);
|
|
ws.accept();
|
|
const reply = new Promise((resolve) => ws.addEventListener("message", (e) => resolve(e.data)));
|
|
ws.send("hello");
|
|
const got = await reply;
|
|
ws.close();
|
|
return new Response(`echo: ${got}\n`);
|
|
} catch (e) {
|
|
return new Response(`refused: ${e.message}\n`);
|
|
}
|
|
}
|
|
}
|