@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 CHANGED
@@ -116,8 +116,14 @@ const sandbox = defineSandbox({
116
116
  memoryMiB: 4096,
117
117
  },
118
118
  network: network.policy(async (conn) => {
119
- if (conn.host === "api.github.com") {
120
- conn.allowHttp(async (request) => {
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.host === "registry.npmjs.org") {
229
- conn.allowHttp();
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.allow()` grants HTTP(S)-classified traffic without request middleware.
235
- `conn.allowHttp(...)` grants HTTP(S)-classified traffic and can apply request
236
- middleware:
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
- if (conn.host !== "api.example.com") return;
246
+ const api = conn.transport === "tcp"
247
+ ? conn.matchHttp("api.example.com")
248
+ : undefined;
249
+ if (!api) return;
241
250
 
242
- conn.allowHttp(async (request) => {
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 `NetworkGrant` returned by `allow()` and
253
- `allowHttp()` is reserved as the future extension point for instance-local
254
- state, such as remembering a grant for a time window.
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
@@ -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>): Promise<HostProcessSandboxVm>;
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,EACjC,MAAM,qBAAqB,CAAC;AAI7B,qBAAa,oBAAqB,YAAW,kBAAkB;;IAC7D,QAAQ,CAAC,gBAAgB,QAAQ;IACjC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAiB5C,OAAO,eAgDN;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,uBAAuB,EACpC,kBAAkB,GAAE,GAAG,CAAC,MAAM,EAAE,gCAAgC,CAAa,GAC5E,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;CAqhBF;AA2BD,OAAO,EAAE,cAAc,EAAE,CAAC"}
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"}
@@ -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
- upstreamIp: assertString(document.upstreamDialIp, "upstreamDialIp"),
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) {