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>
47 lines
3.1 KiB
Markdown
47 lines
3.1 KiB
Markdown
# toybox
|
|
|
|
Vats whose code is installed at runtime: each vat is a Durable Object whose app runs as a [facet](https://developers.cloudflare.com/dynamic-workers/usage/durable-object-facets/) on code loaded through workerd's Worker Loader. Think `vatAdmin.createVat(bundle)`, with a supervisor per vat.
|
|
|
|
- [supervisor.js](supervisor.js): `Vat` stores code by SHA-256 and runs the app on it; `Public` and `Admin` are the two entrypoints; `Egress` is a vat's `fetch`.
|
|
- [switchboard.js](switchboard.js): a separate workerd process, the vats' only way out: a forward proxy for HTTP and WebSockets.
|
|
- [config.capnp](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=<host>` grants the app that one host. |
|
|
|
|
An app must export `class App extends DurableObject` (see [apps/](apps/)). It gets an empty `env`. With no grant, `fetch` throws; with one, its `globalOutbound` is `Egress` with the granted host in `ctx.props`, which forwards matching requests, WebSocket upgrades included, to the switchboard and refuses the rest with a 403.
|
|
|
|
toybox reaches the switchboard over a unix socket (`-e switchboard=unix:…`), in HTTP proxy style so the full URL, scheme included, survives the hop.
|
|
|
|
## Run it
|
|
|
|
```sh
|
|
pnpm install
|
|
pnpm test # real workerds: install, upgrade, isolation, egress grants, fetch and WebSocket out, crash and restart
|
|
pnpm switchboard # systemd-socket-activate on ./switchboard.sock
|
|
pnpm start # likewise on 127.0.0.1:8787 and ./admin.sock; state in ./state
|
|
```
|
|
|
|
Then, in a third terminal:
|
|
|
|
```sh
|
|
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 -X PUT --unix-socket admin.sock --data-binary @apps/egress-probe.js 'http://toybox/erin?egress=example.com'
|
|
curl 'localhost:8787/erin?url=https://example.com/' # got: <!doctype html>…
|
|
curl 'localhost:8787/erin?url=https://example.org/' # got: egress to example.org not granted
|
|
```
|
|
|
|
## Under systemd
|
|
|
|
[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 socket, which only members of the `toybox` group can open. The switchboard has the network, fenced by `IPAddressDeny=`; `switchboard.capnp` allows `local` addresses only so the tests can reach a local echo server.
|
|
|
|
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".
|