WIP: toybox: OCapN vats on workerd, with ingress and egress as capabilities #4
No files matched your search
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
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.
|
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`.
|
- [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 grant of one host.
|
||||||
- [switchboard.js](switchboard.js): a separate workerd process, the vats' only way out: a forward proxy for HTTP and WebSockets.
|
- [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.
|
- [config.capnp](config.capnp): the sockets have no addresses; whoever starts workerd hands them in, so holding a socket is the authority it grants.
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ Vats whose code is installed at runtime: each vat is a Durable Object whose app
|
|||||||
| `http` | `Public` | `GET /<vat>/…`: talk to a vat's app |
|
| `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. |
|
| `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.
|
An app must export `class App extends DurableObject` (see [apps/](apps/)). Its global `fetch` always throws: nothing in a vat is ambient. A granted host arrives as `env.egress`, an `Egress` with the host in `ctx.props`: its `fetch` forwards matching requests, WebSocket upgrades included, to the switchboard and refuses the rest with a 403. It is an object, so the app can pass it to whichever of its parts should have it.
|
||||||
|
|
||||||
toybox reaches the switchboard over a unix socket (`-e switchboard=unix:…`), in HTTP proxy style so the full URL, scheme included, survives the hop.
|
toybox reaches the switchboard over a unix socket (`-e switchboard=unix:…`), in HTTP proxy style so the full URL, scheme included, survives the hop.
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
import { DurableObject } from "cloudflare:workers";
|
||||||
|
|
||||||
/** `?url=<target>` fetches it; with `&ws=1`, opens a WebSocket there and reports the echo of "hello". */
|
/** `?url=<target>` fetches it through `env.egress`; with `&ws=1`, opens a WebSocket there and reports the echo of "hello". */
|
||||||
export class App extends DurableObject {
|
export class App extends DurableObject {
|
||||||
async fetch(request) {
|
async fetch(request) {
|
||||||
const params = new URL(request.url).searchParams;
|
const params = new URL(request.url).searchParams;
|
||||||
const target = params.get("url");
|
const target = params.get("url");
|
||||||
|
const { egress } = this.env;
|
||||||
|
if (!egress) return new Response("no egress granted\n");
|
||||||
try {
|
try {
|
||||||
if (!params.has("ws")) return new Response(`got: ${await (await fetch(target)).text()}\n`);
|
if (!params.has("ws")) return new Response(`got: ${await (await egress.fetch(target)).text()}\n`);
|
||||||
const resp = await fetch(target, { headers: { Upgrade: "websocket" } });
|
const resp = await egress.fetch(target, { headers: { Upgrade: "websocket" } });
|
||||||
const ws = resp.webSocket;
|
const ws = resp.webSocket;
|
||||||
if (!ws) return new Response(`no websocket: ${resp.status} ${await resp.text()}`);
|
if (!ws) return new Response(`no websocket: ${resp.status} ${await resp.text()}`);
|
||||||
ws.accept();
|
ws.accept();
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart
|
|||||||
* Holds the vat's installed code and egress grant; the app facet holds the app's state.
|
* Holds the vat's installed code and egress grant; the app facet holds the app's state.
|
||||||
*
|
*
|
||||||
* Code is stored by hash so the facet can be rebuilt after hibernation or a
|
* Code is stored by hash so the facet can be rebuilt after hibernation or a
|
||||||
* restart. The app gets an empty `env`; its `fetch` reaches only the host it
|
* restart. The app's global `fetch` throws; a granted host arrives as
|
||||||
* was granted, if any, through {@link Egress}.
|
* `env.egress`, an {@link Egress} the app can pass along like any object.
|
||||||
*
|
*
|
||||||
* Each `start` begins a new incarnation, which names the loaded Worker: the
|
* Each `start` begins a new incarnation, which names the loaded Worker: the
|
||||||
* same code with a different grant must not share a Worker.
|
* same code with a different grant must not share a Worker.
|
||||||
@@ -47,8 +47,8 @@ export class Vat extends DurableObject {
|
|||||||
compatibilityDate: "2026-10-01",
|
compatibilityDate: "2026-10-01",
|
||||||
mainModule: "app.js",
|
mainModule: "app.js",
|
||||||
modules: { "app.js": src },
|
modules: { "app.js": src },
|
||||||
env: {},
|
env: egress ? { egress: this.ctx.exports.Egress({ props: { host: egress } }) } : {},
|
||||||
globalOutbound: egress ? this.ctx.exports.Egress({ props: { host: egress } }) : null,
|
globalOutbound: null,
|
||||||
}));
|
}));
|
||||||
return { class: worker.getDurableObjectClass("App") };
|
return { class: worker.getDurableObjectClass("App") };
|
||||||
});
|
});
|
||||||
@@ -67,8 +67,8 @@ export class Vat extends DurableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A vat's `globalOutbound`: its `fetch`, WebSocket upgrades included, reaches
|
* A grant of one host: `fetch`, WebSocket upgrades included, reaches the
|
||||||
* the switchboard only for the host in `ctx.props`, which the vat can't set.
|
* switchboard only for the host in `ctx.props`, which no holder can change.
|
||||||
*/
|
*/
|
||||||
export class Egress extends WorkerEntrypoint {
|
export class Egress extends WorkerEntrypoint {
|
||||||
fetch(request) {
|
fetch(request) {
|
||||||
|
|||||||
@@ -167,9 +167,14 @@ test("a vat granted a host reaches it through the switchboard: fetch and WebSock
|
|||||||
assert.equal(await probe("erin", echo.host, true), "echo: hello\n");
|
assert.equal(await probe("erin", echo.host, true), "echo: hello\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("same code, no grant: no fetch", async () => {
|
test("same code, no grant: no egress", async () => {
|
||||||
await admin.install("frank", await app("egress-probe.js"));
|
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/);
|
assert.equal(await probe("frank", echo.host), "no egress granted\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a grant is an object, not ambient: the global fetch still throws", async () => {
|
||||||
|
await admin.install("heidi", await app("try-fetch.js"), echo.host);
|
||||||
|
assert.match(await pub.get("heidi"), /^fetch refused: This worker is not permitted to access the internet/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a grant covers one host only", async () => {
|
test("a grant covers one host only", async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user