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>
4.0 KiB
toybox
Vats whose code is installed at runtime: each vat is a Durable Object whose app runs as a facet on code loaded through workerd's Worker Loader. Think vatAdmin.createVat(bundle), with a supervisor per vat.
- supervisor.js:
Vatstores code by SHA-256 and runs the app on it;PublicandAdminare the two entrypoints;Egressis a grant of one host. - switchboard.js: a separate workerd process, the vats' only way out, after Flower's switchboard: each of its sockets serves a Cap'n Web
Handlewith fixed labels, andconstrain(labels)returns a narrower one. - labels.js: Flower's label rule, shared by both: labels only accumulate, a missing key matches anything, and a key both sides set must agree.
- config.capnp: the sockets have no addresses; whoever starts workerd hands them in, so holding a socket is the authority it grants.
| socket | reaches | what its holder can do |
|---|---|---|
http |
Public |
GET /<vat>/…: talk to a vat's app |
admin |
Admin |
PUT /<vat> with an ES module body: install it and (re)start the app; its storage carries over. ?egress=<labels as JSON> grants the app egress within those labels. |
An app must export class App extends DurableObject (see apps/). Its global fetch always throws: nothing in a vat is ambient. A grant arrives as env.egress, an Egress with the labels in ctx.props. Its fetch, WebSocket upgrades included, goes through a switchboard handle constrained to those labels; the switchboard refuses the rest, which comes back as a 403. env.egress.constrain(labels) returns a narrower Egress. It is an object, so the app can pass it to whichever of its parts should have it.
| label | constrains |
|---|---|
scheme |
http or https |
host |
host name or address, no port |
port |
explicit, or the scheme's default |
network |
set by the switchboard socket: public or local |
The switchboard has two sockets, so two root handles: public (no loopback or private addresses) and local. toybox holds one, as an external service (-e switchboard=unix:…): pnpm start and the systemd units give it public; the tests give theirs local to reach an echo server.
Run it
pnpm install
pnpm test # real workerds: install, upgrade, isolation, egress grants and constrain, fetch and WebSocket out, crash and restart
pnpm switchboard # systemd-socket-activate on ./switchboard-public.sock and ./switchboard-local.sock
pnpm start # likewise on 127.0.0.1:8787 and ./admin.sock; state in ./state
Then, in a third terminal:
curl -X PUT --unix-socket admin.sock --data-binary @apps/counter-v1.js http://toybox/alice
curl localhost:8787/alice # v1 count 1
curl -X PUT --unix-socket admin.sock --data-binary @apps/counter-v2.js http://toybox/alice
curl localhost:8787/alice # v2 count 2
curl -g -X PUT --unix-socket admin.sock --data-binary @apps/egress-probe.js 'http://toybox/erin?egress={"host":"example.com"}'
curl 'localhost:8787/erin?url=https://example.com/' # got: <!doctype html>…
curl 'localhost:8787/erin?url=https://example.org/' # got: egress refused: egress to example.org not granted
Under systemd
systemd/ has socket and service pairs for toybox and its switchboard, assuming this directory is installed at /opt/toybox. toybox gets no network of its own (PrivateNetwork=yes): only the two sockets it is handed and the switchboard's public socket, which only members of the toybox group can open. The switchboard binds its local socket in its own 0700 runtime directory, so no other unit holds that handle.
Neither unit has been run under the system manager yet.
PrivateNetwork= needs the system manager; as a systemd-run --user unit, systemd logs "lack privileges for network namespace, proceeding without".