@torkbot/sandbox 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -14
- package/dist/host-process.d.ts +2 -2
- package/dist/host-process.d.ts.map +1 -1
- package/dist/host-process.js +313 -5
- package/dist/host-process.js.map +1 -1
- package/dist/index.d.ts +241 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +162 -43
- package/dist/index.js.map +1 -1
- package/dist/launch-options.d.ts +8 -1
- package/dist/launch-options.d.ts.map +1 -1
- package/dist/spawn-options.d.ts +3 -0
- package/dist/spawn-options.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -116,8 +116,14 @@ const sandbox = defineSandbox({
|
|
|
116
116
|
memoryMiB: 4096,
|
|
117
117
|
},
|
|
118
118
|
network: network.policy(async (conn) => {
|
|
119
|
-
if (conn.
|
|
120
|
-
|
|
119
|
+
if (conn.matchDns("10.0.2.1")?.accept()) return;
|
|
120
|
+
|
|
121
|
+
const github = conn.transport === "tcp"
|
|
122
|
+
? conn.matchHttp("api.github.com")
|
|
123
|
+
: undefined;
|
|
124
|
+
if (github) {
|
|
125
|
+
github.accept(async (request) => {
|
|
126
|
+
if (request.destination.hostname !== "api.github.com") return;
|
|
121
127
|
request.headers.set(
|
|
122
128
|
"authorization",
|
|
123
129
|
`Bearer ${await githubTokens.tokenForRequest(request)}`,
|
|
@@ -125,8 +131,6 @@ const sandbox = defineSandbox({
|
|
|
125
131
|
});
|
|
126
132
|
return;
|
|
127
133
|
}
|
|
128
|
-
|
|
129
|
-
conn.allowHttp();
|
|
130
134
|
}),
|
|
131
135
|
});
|
|
132
136
|
|
|
@@ -225,21 +229,26 @@ connection requests and grants only the traffic it explicitly allows:
|
|
|
225
229
|
|
|
226
230
|
```ts
|
|
227
231
|
const policy = network.policy(async (conn) => {
|
|
228
|
-
if (conn.
|
|
229
|
-
conn.
|
|
232
|
+
if (conn.transport === "tcp" && conn.dst.isPublicInternet() && conn.dst.port === 443) {
|
|
233
|
+
conn.accept();
|
|
230
234
|
}
|
|
231
235
|
});
|
|
232
236
|
```
|
|
233
237
|
|
|
234
|
-
`conn.
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
`conn.accept()` grants the observed connection or flow at the transport layer.
|
|
239
|
+
It does not classify the application protocol, does not enter HTTP middleware,
|
|
240
|
+
and does not MITM TLS. `conn.acceptHttp(...)` is TCP-only and explicitly opts
|
|
241
|
+
the flow into Sandbox's HTTP-family enforcement path. If the accepted flow is
|
|
242
|
+
not actually HTTP or HTTPS, it fails closed.
|
|
237
243
|
|
|
238
244
|
```ts
|
|
239
245
|
const policy = network.policy(async (conn) => {
|
|
240
|
-
|
|
246
|
+
const api = conn.transport === "tcp"
|
|
247
|
+
? conn.matchHttp("api.example.com")
|
|
248
|
+
: undefined;
|
|
249
|
+
if (!api) return;
|
|
241
250
|
|
|
242
|
-
|
|
251
|
+
api.accept(async (request) => {
|
|
243
252
|
request.headers.set(
|
|
244
253
|
"authorization",
|
|
245
254
|
`Bearer ${await credentialBroker.authorizationFor(request)}`,
|
|
@@ -248,10 +257,52 @@ const policy = network.policy(async (conn) => {
|
|
|
248
257
|
});
|
|
249
258
|
```
|
|
250
259
|
|
|
260
|
+
Every TCP and UDP policy request carries source and destination IP-layer
|
|
261
|
+
endpoints:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
conn.src.ip;
|
|
265
|
+
conn.src.port;
|
|
266
|
+
conn.dst.ip;
|
|
267
|
+
conn.dst.port;
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Endpoint helpers classify logical address ranges without relying on hostnames:
|
|
271
|
+
`isLoopback()`, `isPrivate()`, `isLinkLocal()`, `isMulticast()`,
|
|
272
|
+
`isBroadcast()`, `isDocumentation()`, `isReserved()`, and
|
|
273
|
+
`isPublicInternet()`. `transport` is the TCP/UDP discriminator. Sandbox does
|
|
274
|
+
not expose a best-effort `conn.protocol` classifier.
|
|
275
|
+
|
|
276
|
+
HTTP middleware receives trusted destination metadata separately from the
|
|
277
|
+
request URL. `request.destination.hostname` is populated only when Sandbox can
|
|
278
|
+
pin the destination IP to trusted connection metadata, such as its own DNS
|
|
279
|
+
answer cache. IP-addressed requests still work under `acceptHttp(...)`, but they
|
|
280
|
+
do not advertise a hostname. Do not use the HTTP `Host` header as authority for
|
|
281
|
+
policy decisions.
|
|
282
|
+
|
|
283
|
+
For common policy checks, protocol match helpers acquire a typed capability
|
|
284
|
+
before accepting traffic:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
network.policy(async (conn) => {
|
|
288
|
+
if (conn.matchDns("10.0.2.1")?.accept()) return;
|
|
289
|
+
|
|
290
|
+
if (conn.transport !== "tcp") return;
|
|
291
|
+
const http = conn.matchHttp((candidate) =>
|
|
292
|
+
candidate.hostname === "api.github.com"
|
|
293
|
+
);
|
|
294
|
+
if (!http) return;
|
|
295
|
+
|
|
296
|
+
if (!(await policyManager.allow(http))) return;
|
|
297
|
+
|
|
298
|
+
http.accept((request) => policyManager.handle(request));
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
251
302
|
Deny remains the default. If the policy callback does not create a grant, the
|
|
252
|
-
connection is blocked. The
|
|
253
|
-
|
|
254
|
-
|
|
303
|
+
connection is blocked. The grants returned by `accept()` and `acceptHttp()` are
|
|
304
|
+
reserved as future extension points for instance-local state, such as
|
|
305
|
+
remembering a grant for a time window.
|
|
255
306
|
|
|
256
307
|
The runtime uses this policy shape to keep the JavaScript boundary explicit.
|
|
257
308
|
Native rules can be added under the same model later without changing the
|
|
@@ -330,6 +381,14 @@ TypeScript API:
|
|
|
330
381
|
decisions and delegate to JavaScript only when a policy callback is required.
|
|
331
382
|
- HTTP request middleware is caller-provided JavaScript, but Sandbox owns the
|
|
332
383
|
interception machinery and certificate plumbing.
|
|
384
|
+
- When HTTP interception is enabled, the host generates the CA material and
|
|
385
|
+
passes only the public CA certificate to Sandbox init. Init does not generate
|
|
386
|
+
or manage certificates; the host exposes the supplied CA through an internal
|
|
387
|
+
read-only virtiofs mount, then init installs it using the selected rootfs'
|
|
388
|
+
native trust-store mechanism. Built-in rootfs launches use an ephemeral
|
|
389
|
+
writable COW view for HTTP interception so init can update the guest trust
|
|
390
|
+
store deterministically. If a rootfs does not provide a supported native
|
|
391
|
+
trust-store installer, init fails closed.
|
|
333
392
|
|
|
334
393
|
The intended boundary is that Sandbox knows how to launch, isolate, mount,
|
|
335
394
|
intercept, and enforce. User-space owns artifact selection, filesystem
|
package/dist/host-process.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { hostBinaryPath } from "./artifacts.ts";
|
|
2
2
|
import type { HostControlChannel } from "./control.ts";
|
|
3
3
|
import type { HostSpawnSandboxOptions } from "./spawn-options.ts";
|
|
4
|
-
import type { InternalSandboxOptions, RegisteredHttpRequestHeadersHook } from "./launch-options.ts";
|
|
4
|
+
import type { InternalSandboxOptions, RegisteredHttpRequestHeadersHook, RegisteredNetworkConnectionHook } from "./launch-options.ts";
|
|
5
5
|
export declare class HostProcessSandboxVm implements HostControlChannel {
|
|
6
6
|
#private;
|
|
7
7
|
readonly hasControlSocket = true;
|
|
8
8
|
readonly packets: AsyncIterable<Uint8Array>;
|
|
9
9
|
private constructor();
|
|
10
|
-
static spawn(options: InternalSandboxOptions, hostOptions: HostSpawnSandboxOptions, requestHeaderHooks?: Map<string, RegisteredHttpRequestHeadersHook
|
|
10
|
+
static spawn(options: InternalSandboxOptions, hostOptions: HostSpawnSandboxOptions, requestHeaderHooks?: Map<string, RegisteredHttpRequestHeadersHook>, networkConnectionHook?: RegisteredNetworkConnectionHook): Promise<HostProcessSandboxVm>;
|
|
11
11
|
writeControlPacket(packet: Uint8Array): void;
|
|
12
12
|
close(): Promise<void>;
|
|
13
13
|
terminateHostForTest(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-process.d.ts","sourceRoot":"","sources":["../src/host-process.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAyB,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAQlE,OAAO,KAAK,EACV,sBAAsB,EACtB,gCAAgC,
|
|
1
|
+
{"version":3,"file":"host-process.d.ts","sourceRoot":"","sources":["../src/host-process.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAyB,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAQlE,OAAO,KAAK,EACV,sBAAsB,EACtB,gCAAgC,EAChC,+BAA+B,EAChC,MAAM,qBAAqB,CAAC;AAkB7B,qBAAa,oBAAqB,YAAW,kBAAkB;;IAC7D,QAAQ,CAAC,gBAAgB,QAAQ;IACjC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAmB5C,OAAO,eAkDN;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,uBAAuB,EACpC,kBAAkB,GAAE,GAAG,CAAC,MAAM,EAAE,gCAAgC,CAAa,EAC7E,qBAAqB,CAAC,EAAE,+BAA+B,GACtD,OAAO,CAAC,oBAAoB,CAAC,CAyB/B;IAED,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAG3C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CA4B3B;IAEK,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB1C;CAqqBF;AA2BD,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/host-process.js
CHANGED
|
@@ -16,16 +16,19 @@ export class HostProcessSandboxVm {
|
|
|
16
16
|
#rootBlockStore;
|
|
17
17
|
#rootBlockStoreContext;
|
|
18
18
|
#requestHeaderHooks;
|
|
19
|
+
#networkConnectionHook;
|
|
20
|
+
#httpMiddlewareByFlow = new Map();
|
|
19
21
|
#buffer = new Uint8Array();
|
|
20
22
|
#stderr = "";
|
|
21
23
|
#closed = false;
|
|
22
24
|
#exitError = null;
|
|
23
25
|
#stdinError = null;
|
|
24
|
-
constructor(child, options, requestHeaderHooks) {
|
|
26
|
+
constructor(child, options, requestHeaderHooks, networkConnectionHook) {
|
|
25
27
|
this.#child = child;
|
|
26
28
|
this.#options = options;
|
|
27
29
|
this.packets = this.#packets;
|
|
28
30
|
this.#requestHeaderHooks = requestHeaderHooks;
|
|
31
|
+
this.#networkConnectionHook = networkConnectionHook;
|
|
29
32
|
this.#rootBlockStore = options.rootfs.storage?.blockStore;
|
|
30
33
|
this.#rootBlockStoreContext = options.rootfs.storage?.context;
|
|
31
34
|
for (const mount of options.mounts ?? []) {
|
|
@@ -62,14 +65,14 @@ export class HostProcessSandboxVm {
|
|
|
62
65
|
this.#launchReady.close(this.#exitError);
|
|
63
66
|
});
|
|
64
67
|
}
|
|
65
|
-
static async spawn(options, hostOptions, requestHeaderHooks = new Map()) {
|
|
68
|
+
static async spawn(options, hostOptions, requestHeaderHooks = new Map(), networkConnectionHook) {
|
|
66
69
|
let vm;
|
|
67
70
|
const hostPath = hostBinaryPath();
|
|
68
71
|
try {
|
|
69
72
|
const child = spawn(hostPath, ["--stdio"], {
|
|
70
73
|
stdio: ["pipe", "pipe", "pipe"],
|
|
71
74
|
});
|
|
72
|
-
vm = new HostProcessSandboxVm(child, options, requestHeaderHooks);
|
|
75
|
+
vm = new HostProcessSandboxVm(child, options, requestHeaderHooks, networkConnectionHook);
|
|
73
76
|
await Promise.race([
|
|
74
77
|
once(child, "spawn"),
|
|
75
78
|
once(child, "error").then(([error]) => {
|
|
@@ -225,6 +228,8 @@ export class HostProcessSandboxVm {
|
|
|
225
228
|
&& type !== "host.vfs.removexattr"
|
|
226
229
|
&& type !== "host.http.requestHeaders"
|
|
227
230
|
&& type !== "host.http.activeRequestHeaderHooks"
|
|
231
|
+
&& type !== "host.network.connection"
|
|
232
|
+
&& type !== "host.network.closed"
|
|
228
233
|
&& type !== "host.block.list"
|
|
229
234
|
&& type !== "host.block.read"
|
|
230
235
|
&& type !== "host.block.write"
|
|
@@ -237,6 +242,12 @@ export class HostProcessSandboxVm {
|
|
|
237
242
|
else if (type === "host.http.activeRequestHeaderHooks") {
|
|
238
243
|
void this.#handleActiveRequestHeaderHooks(document);
|
|
239
244
|
}
|
|
245
|
+
else if (type === "host.network.connection") {
|
|
246
|
+
void this.#handleNetworkConnection(document);
|
|
247
|
+
}
|
|
248
|
+
else if (type === "host.network.closed") {
|
|
249
|
+
this.#handleNetworkClosed(document);
|
|
250
|
+
}
|
|
240
251
|
else if (type === "host.block.list"
|
|
241
252
|
|| type === "host.block.read"
|
|
242
253
|
|| type === "host.block.write"
|
|
@@ -248,6 +259,133 @@ export class HostProcessSandboxVm {
|
|
|
248
259
|
}
|
|
249
260
|
return true;
|
|
250
261
|
}
|
|
262
|
+
async #handleNetworkConnection(document) {
|
|
263
|
+
const id = typeof document.id === "string" ? document.id : "";
|
|
264
|
+
try {
|
|
265
|
+
const transport = assertNetworkTransport(document.transport);
|
|
266
|
+
const protocol = optionalString(document.protocol, "protocol");
|
|
267
|
+
const srcIp = assertString(document.srcIp, "srcIp");
|
|
268
|
+
const srcPort = assertNumber(document.srcPort, "srcPort");
|
|
269
|
+
const dstIp = assertString(document.dstIp, "dstIp");
|
|
270
|
+
const dstPort = assertNumber(document.dstPort, "dstPort");
|
|
271
|
+
const hostname = optionalString(document.hostname, "hostname");
|
|
272
|
+
const src = createNetworkEndpoint(srcIp, srcPort);
|
|
273
|
+
const dst = createNetworkEndpoint(dstIp, dstPort);
|
|
274
|
+
const decision = { action: "deny" };
|
|
275
|
+
let httpMiddleware;
|
|
276
|
+
const accept = () => {
|
|
277
|
+
decision.action = "accept";
|
|
278
|
+
return {};
|
|
279
|
+
};
|
|
280
|
+
const connection = {
|
|
281
|
+
transport,
|
|
282
|
+
src,
|
|
283
|
+
dst,
|
|
284
|
+
accept,
|
|
285
|
+
matchDns(matcher) {
|
|
286
|
+
if (protocol !== "dns") {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const candidate = {
|
|
290
|
+
src,
|
|
291
|
+
dst,
|
|
292
|
+
transport,
|
|
293
|
+
accept,
|
|
294
|
+
};
|
|
295
|
+
return dnsMatcherMatches(matcher, candidate) ? candidate : undefined;
|
|
296
|
+
},
|
|
297
|
+
...(transport === "tcp"
|
|
298
|
+
? {
|
|
299
|
+
acceptHttp(middleware) {
|
|
300
|
+
decision.action = "acceptHttp";
|
|
301
|
+
httpMiddleware = middleware;
|
|
302
|
+
return {};
|
|
303
|
+
},
|
|
304
|
+
matchTcp(matcher) {
|
|
305
|
+
const candidate = {
|
|
306
|
+
src,
|
|
307
|
+
dst,
|
|
308
|
+
accept,
|
|
309
|
+
};
|
|
310
|
+
return endpointMatcherMatches(matcher, candidate) ? candidate : undefined;
|
|
311
|
+
},
|
|
312
|
+
matchHttp(matcher) {
|
|
313
|
+
if (hostname === undefined) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
const candidate = {
|
|
317
|
+
src,
|
|
318
|
+
dst,
|
|
319
|
+
hostname,
|
|
320
|
+
port: dst.port,
|
|
321
|
+
accept(middleware) {
|
|
322
|
+
decision.action = "acceptHttp";
|
|
323
|
+
httpMiddleware = middleware;
|
|
324
|
+
return {};
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return httpMatcherMatches(matcher, candidate) ? candidate : undefined;
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
: {
|
|
331
|
+
matchUdp(matcher) {
|
|
332
|
+
const candidate = {
|
|
333
|
+
src,
|
|
334
|
+
dst,
|
|
335
|
+
accept,
|
|
336
|
+
};
|
|
337
|
+
return endpointMatcherMatches(matcher, candidate) ? candidate : undefined;
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
if (this.#networkConnectionHook?.active === true) {
|
|
342
|
+
await this.#networkConnectionHook.hook(connection);
|
|
343
|
+
}
|
|
344
|
+
if (decision.action === "acceptHttp") {
|
|
345
|
+
this.#httpMiddlewareByFlow.set(networkFlowKey(src, dst), httpMiddleware);
|
|
346
|
+
}
|
|
347
|
+
this.#tryWriteToHost(encodePacket({
|
|
348
|
+
type: "host.network.response",
|
|
349
|
+
id,
|
|
350
|
+
ok: true,
|
|
351
|
+
action: decision.action,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
this.#tryWriteToHost(encodePacket({
|
|
356
|
+
type: "host.network.response",
|
|
357
|
+
id,
|
|
358
|
+
ok: false,
|
|
359
|
+
error: error instanceof Error ? error.message : String(error),
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
#handleNetworkClosed(document) {
|
|
364
|
+
const id = typeof document.id === "string" ? document.id : "";
|
|
365
|
+
try {
|
|
366
|
+
const transport = assertNetworkTransport(document.transport);
|
|
367
|
+
const srcIp = assertString(document.srcIp, "srcIp");
|
|
368
|
+
const srcPort = assertNumber(document.srcPort, "srcPort");
|
|
369
|
+
const dstIp = assertString(document.dstIp, "dstIp");
|
|
370
|
+
const dstPort = assertNumber(document.dstPort, "dstPort");
|
|
371
|
+
if (transport === "tcp") {
|
|
372
|
+
this.#httpMiddlewareByFlow.delete(networkFlowKey(createNetworkEndpoint(srcIp, srcPort), createNetworkEndpoint(dstIp, dstPort)));
|
|
373
|
+
}
|
|
374
|
+
this.#tryWriteToHost(encodePacket({
|
|
375
|
+
type: "host.network.response",
|
|
376
|
+
id,
|
|
377
|
+
ok: true,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
this.#tryWriteToHost(encodePacket({
|
|
382
|
+
type: "host.network.response",
|
|
383
|
+
id,
|
|
384
|
+
ok: false,
|
|
385
|
+
error: error instanceof Error ? error.message : String(error),
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
251
389
|
async #handleHttpRequestHeaders(document) {
|
|
252
390
|
const id = typeof document.id === "string" ? document.id : "";
|
|
253
391
|
try {
|
|
@@ -261,13 +399,15 @@ export class HostProcessSandboxVm {
|
|
|
261
399
|
method: assertString(document.method, "method"),
|
|
262
400
|
headers,
|
|
263
401
|
destination: {
|
|
402
|
+
sourceIp: assertString(document.sourceIp, "sourceIp"),
|
|
403
|
+
sourcePort: assertNumber(document.sourcePort, "sourcePort"),
|
|
264
404
|
originalIp: assertString(document.originalDestinationIp, "originalDestinationIp"),
|
|
265
405
|
originalPort: assertNumber(document.originalDestinationPort, "originalDestinationPort"),
|
|
266
|
-
|
|
267
|
-
upstreamPort: assertNumber(document.upstreamDialPort, "upstreamDialPort"),
|
|
406
|
+
hostname: optionalString(document.originalDestinationHostname, "originalDestinationHostname"),
|
|
268
407
|
},
|
|
269
408
|
tls: optionalTlsMetadata(document.tls),
|
|
270
409
|
};
|
|
410
|
+
await this.#httpMiddlewareByFlow.get(networkFlowKey(createNetworkEndpoint(request.destination.sourceIp, request.destination.sourcePort), createNetworkEndpoint(request.destination.originalIp, request.destination.originalPort)))?.(request);
|
|
271
411
|
for (const hookId of hookIds) {
|
|
272
412
|
const registration = this.#requestHeaderHooks.get(hookId);
|
|
273
413
|
if (registration?.active === true) {
|
|
@@ -746,6 +886,170 @@ function assertProtocol(value) {
|
|
|
746
886
|
}
|
|
747
887
|
throw new Error("host request protocol must be http/1.1 or h2");
|
|
748
888
|
}
|
|
889
|
+
function assertNetworkTransport(value) {
|
|
890
|
+
if (value === "tcp" || value === "udp") {
|
|
891
|
+
return value;
|
|
892
|
+
}
|
|
893
|
+
throw new Error("host network request transport must be tcp or udp");
|
|
894
|
+
}
|
|
895
|
+
function createNetworkEndpoint(ip, port) {
|
|
896
|
+
return {
|
|
897
|
+
ip,
|
|
898
|
+
port,
|
|
899
|
+
isLoopback: () => isLoopbackIp(ip),
|
|
900
|
+
isPrivate: () => isPrivateIp(ip),
|
|
901
|
+
isLinkLocal: () => isLinkLocalIp(ip),
|
|
902
|
+
isMulticast: () => isMulticastIp(ip),
|
|
903
|
+
isBroadcast: () => ip === "255.255.255.255",
|
|
904
|
+
isDocumentation: () => isDocumentationIp(ip),
|
|
905
|
+
isReserved: () => isReservedIp(ip),
|
|
906
|
+
isPublicInternet: () => isPublicInternetIp(ip),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function dnsMatcherMatches(matcher, candidate) {
|
|
910
|
+
if (typeof matcher === "function") {
|
|
911
|
+
return matcher(candidate);
|
|
912
|
+
}
|
|
913
|
+
const spec = typeof matcher === "string" ? { ip: matcher, port: 53 } : matcher;
|
|
914
|
+
return candidate.dst.ip === spec.ip && candidate.dst.port === (spec.port ?? 53);
|
|
915
|
+
}
|
|
916
|
+
function endpointMatcherMatches(matcher, candidate) {
|
|
917
|
+
if (typeof matcher === "function") {
|
|
918
|
+
return matcher(candidate);
|
|
919
|
+
}
|
|
920
|
+
const endpoint = parseEndpointSpec(matcher);
|
|
921
|
+
return candidate.dst.ip === endpoint.ip && candidate.dst.port === endpoint.port;
|
|
922
|
+
}
|
|
923
|
+
function httpMatcherMatches(matcher, candidate) {
|
|
924
|
+
if (typeof matcher === "function") {
|
|
925
|
+
return matcher(candidate);
|
|
926
|
+
}
|
|
927
|
+
const authority = parseHttpAuthoritySpec(matcher);
|
|
928
|
+
return candidate.hostname === authority.hostname
|
|
929
|
+
&& (authority.port === undefined || candidate.port === authority.port);
|
|
930
|
+
}
|
|
931
|
+
function parseEndpointSpec(spec) {
|
|
932
|
+
if (typeof spec !== "string") {
|
|
933
|
+
return spec;
|
|
934
|
+
}
|
|
935
|
+
const ipv6Match = /^\[(.*)]:(\d+)$/.exec(spec);
|
|
936
|
+
if (ipv6Match !== null) {
|
|
937
|
+
return { ip: ipv6Match[1] ?? "", port: Number(ipv6Match[2]) };
|
|
938
|
+
}
|
|
939
|
+
const separator = spec.lastIndexOf(":");
|
|
940
|
+
if (separator < 0) {
|
|
941
|
+
throw new Error("network endpoint spec must include a port");
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
ip: spec.slice(0, separator),
|
|
945
|
+
port: Number(spec.slice(separator + 1)),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function parseHttpAuthoritySpec(spec) {
|
|
949
|
+
if (typeof spec !== "string") {
|
|
950
|
+
return spec;
|
|
951
|
+
}
|
|
952
|
+
const ipv6Match = /^\[(.*)](?::(\d+))?$/.exec(spec);
|
|
953
|
+
if (ipv6Match !== null) {
|
|
954
|
+
return {
|
|
955
|
+
hostname: ipv6Match[1] ?? "",
|
|
956
|
+
port: ipv6Match[2] === undefined ? undefined : Number(ipv6Match[2]),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const separator = spec.lastIndexOf(":");
|
|
960
|
+
if (separator > -1) {
|
|
961
|
+
const portText = spec.slice(separator + 1);
|
|
962
|
+
if (/^\d+$/.test(portText)) {
|
|
963
|
+
return {
|
|
964
|
+
hostname: spec.slice(0, separator),
|
|
965
|
+
port: Number(portText),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return { hostname: spec };
|
|
970
|
+
}
|
|
971
|
+
function networkFlowKey(src, dst) {
|
|
972
|
+
return `${src.ip}:${src.port}->${dst.ip}:${dst.port}`;
|
|
973
|
+
}
|
|
974
|
+
function isLoopbackIp(ip) {
|
|
975
|
+
const ipv4 = parseIpv4(ip);
|
|
976
|
+
if (ipv4 !== undefined) {
|
|
977
|
+
return ipv4[0] === 127;
|
|
978
|
+
}
|
|
979
|
+
return ip.toLowerCase() === "::1";
|
|
980
|
+
}
|
|
981
|
+
function isPrivateIp(ip) {
|
|
982
|
+
const ipv4 = parseIpv4(ip);
|
|
983
|
+
if (ipv4 !== undefined) {
|
|
984
|
+
return ipv4[0] === 10
|
|
985
|
+
|| (ipv4[0] === 172 && ipv4[1] >= 16 && ipv4[1] <= 31)
|
|
986
|
+
|| (ipv4[0] === 192 && ipv4[1] === 168);
|
|
987
|
+
}
|
|
988
|
+
const lower = ip.toLowerCase();
|
|
989
|
+
return lower.startsWith("fc") || lower.startsWith("fd");
|
|
990
|
+
}
|
|
991
|
+
function isLinkLocalIp(ip) {
|
|
992
|
+
const ipv4 = parseIpv4(ip);
|
|
993
|
+
if (ipv4 !== undefined) {
|
|
994
|
+
return ipv4[0] === 169 && ipv4[1] === 254;
|
|
995
|
+
}
|
|
996
|
+
return /^fe[89ab]/i.test(ip);
|
|
997
|
+
}
|
|
998
|
+
function isMulticastIp(ip) {
|
|
999
|
+
const ipv4 = parseIpv4(ip);
|
|
1000
|
+
if (ipv4 !== undefined) {
|
|
1001
|
+
return ipv4[0] >= 224 && ipv4[0] <= 239;
|
|
1002
|
+
}
|
|
1003
|
+
return ip.toLowerCase().startsWith("ff");
|
|
1004
|
+
}
|
|
1005
|
+
function isDocumentationIp(ip) {
|
|
1006
|
+
const ipv4 = parseIpv4(ip);
|
|
1007
|
+
if (ipv4 !== undefined) {
|
|
1008
|
+
return (ipv4[0] === 192 && ipv4[1] === 0 && ipv4[2] === 2)
|
|
1009
|
+
|| (ipv4[0] === 198 && ipv4[1] === 51 && ipv4[2] === 100)
|
|
1010
|
+
|| (ipv4[0] === 203 && ipv4[1] === 0 && ipv4[2] === 113);
|
|
1011
|
+
}
|
|
1012
|
+
return ip.toLowerCase().startsWith("2001:db8:");
|
|
1013
|
+
}
|
|
1014
|
+
function isReservedIp(ip) {
|
|
1015
|
+
const ipv4 = parseIpv4(ip);
|
|
1016
|
+
if (ipv4 !== undefined) {
|
|
1017
|
+
return ipv4[0] === 0
|
|
1018
|
+
|| (ipv4[0] === 100 && ipv4[1] >= 64 && ipv4[1] <= 127)
|
|
1019
|
+
|| (ipv4[0] === 192 && ipv4[1] === 0 && ipv4[2] === 0)
|
|
1020
|
+
|| (ipv4[0] === 192 && ipv4[1] === 88 && ipv4[2] === 99)
|
|
1021
|
+
|| (ipv4[0] === 198 && (ipv4[1] === 18 || ipv4[1] === 19))
|
|
1022
|
+
|| ipv4[0] >= 240
|
|
1023
|
+
|| isDocumentationIp(ip);
|
|
1024
|
+
}
|
|
1025
|
+
return ip.toLowerCase().startsWith("2001:db8:");
|
|
1026
|
+
}
|
|
1027
|
+
function isPublicInternetIp(ip) {
|
|
1028
|
+
return parseIpv4(ip) !== undefined
|
|
1029
|
+
&& !isLoopbackIp(ip)
|
|
1030
|
+
&& !isPrivateIp(ip)
|
|
1031
|
+
&& !isLinkLocalIp(ip)
|
|
1032
|
+
&& !isMulticastIp(ip)
|
|
1033
|
+
&& !isReservedIp(ip)
|
|
1034
|
+
&& !isDocumentationIp(ip)
|
|
1035
|
+
&& ip !== "255.255.255.255";
|
|
1036
|
+
}
|
|
1037
|
+
function parseIpv4(ip) {
|
|
1038
|
+
const parts = ip.split(".");
|
|
1039
|
+
if (parts.length !== 4) {
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
const octets = parts.map((part) => {
|
|
1043
|
+
if (!/^(0|[1-9][0-9]{0,2})$/.test(part)) {
|
|
1044
|
+
return undefined;
|
|
1045
|
+
}
|
|
1046
|
+
const value = Number(part);
|
|
1047
|
+
return value <= 255 ? value : undefined;
|
|
1048
|
+
});
|
|
1049
|
+
return octets.every((part) => part !== undefined)
|
|
1050
|
+
? octets
|
|
1051
|
+
: undefined;
|
|
1052
|
+
}
|
|
749
1053
|
function optionalTlsMetadata(value) {
|
|
750
1054
|
if (value === undefined || value === null) {
|
|
751
1055
|
return undefined;
|
|
@@ -763,6 +1067,9 @@ function optionalTlsMetadata(value) {
|
|
|
763
1067
|
: assertString(document.alpn, "tls.alpn"),
|
|
764
1068
|
};
|
|
765
1069
|
}
|
|
1070
|
+
function optionalString(value, field) {
|
|
1071
|
+
return value === undefined || value === null ? undefined : assertString(value, field);
|
|
1072
|
+
}
|
|
766
1073
|
function binaryField(value, field) {
|
|
767
1074
|
if (value instanceof Uint8Array) {
|
|
768
1075
|
return value;
|
|
@@ -925,6 +1232,7 @@ function encodeHostSpawn(options) {
|
|
|
925
1232
|
mounts: options.mounts ?? [],
|
|
926
1233
|
networkOutbound: options.network?.outbound,
|
|
927
1234
|
networkHttp: options.network?.http === undefined ? undefined : options.network.http,
|
|
1235
|
+
networkPolicy: options.network?.policy === undefined ? undefined : options.network.policy,
|
|
928
1236
|
});
|
|
929
1237
|
}
|
|
930
1238
|
function encodePacket(document) {
|