isol8 0.8.0 → 0.8.2

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
@@ -15,6 +15,7 @@ Secure code execution engine for AI agents. Run untrusted Python, Node.js, Bun,
15
15
  - **Fast** — warm container pool for sub-100ms execution latency
16
16
  - **Security first** — read-only rootfs, `no-new-privileges`, PID/memory/CPU limits
17
17
  - **Network control** — `none` (default), `host`, or `filtered` (HTTP/HTTPS proxy with regex whitelist/blacklist)
18
+ - **Secure File I/O** — streaming file content avoids process argument leaks
18
19
  - **File I/O** — upload files into and download files from sandboxes
19
20
  - **Runtime packages** — install pip/npm/bun packages on-the-fly (`--install`)
20
21
  - **Modern Node.js** — defaults to ESM (`.mjs`), supports CommonJS (`.cjs`)
@@ -275,6 +276,8 @@ const isol8 = new DockerIsol8({
275
276
  });
276
277
  ```
277
278
 
279
+ In `filtered` mode, iptables rules are applied at the kernel level to ensure the `sandbox` user can **only** reach the internal filtering proxy (`127.0.0.1:8118`). All other outbound traffic from the sandbox user is dropped, preventing bypass via raw sockets or non-HTTP protocols.
280
+
278
281
  ## Configuration
279
282
 
280
283
  Create `isol8.config.json` in your project root or `~/.isol8/config.json`.
package/dist/cli.js CHANGED
@@ -55088,7 +55088,11 @@ class ContainerPool {
55088
55088
  }
55089
55089
  try {
55090
55090
  const killExec = await container.exec({
55091
- Cmd: ["sh", "-c", "pkill -9 -u sandbox 2>/dev/null; true"]
55091
+ Cmd: [
55092
+ "sh",
55093
+ "-c",
55094
+ "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; true"
55095
+ ]
55092
55096
  });
55093
55097
  await killExec.start({ Detach: true });
55094
55098
  let killInfo = await killExec.inspect();
@@ -55279,25 +55283,31 @@ var exports_docker = {};
55279
55283
  __export(exports_docker, {
55280
55284
  DockerIsol8: () => DockerIsol8
55281
55285
  });
55286
+ import { spawn } from "node:child_process";
55282
55287
  import { randomUUID } from "node:crypto";
55283
55288
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
55284
55289
  import { PassThrough } from "node:stream";
55285
55290
  async function writeFileViaExec(container, filePath, content) {
55286
55291
  const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
55287
- const b64 = data.toString("base64");
55288
- const exec = await container.exec({
55289
- Cmd: ["sh", "-c", `printf '%s' '${b64}' | base64 -d > ${filePath}`],
55290
- User: "sandbox"
55292
+ return new Promise((resolve2, reject) => {
55293
+ const child = spawn("docker", ["exec", "-i", "-u", "sandbox", container.id, "sh", "-c", `cat > ${filePath}`], {
55294
+ stdio: ["pipe", "ignore", "pipe"]
55295
+ });
55296
+ child.on("error", (err) => {
55297
+ reject(new Error(`Failed to spawn docker exec: ${err.message}`));
55298
+ });
55299
+ let stderr = "";
55300
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
55301
+ child.stdin.write(data);
55302
+ child.stdin.end();
55303
+ child.on("close", (code) => {
55304
+ if (code !== 0) {
55305
+ reject(new Error(`Failed to write file ${filePath}: ${stderr} (exit code ${code})`));
55306
+ } else {
55307
+ resolve2();
55308
+ }
55309
+ });
55291
55310
  });
55292
- await exec.start({ Detach: true });
55293
- let info2 = await exec.inspect();
55294
- while (info2.Running) {
55295
- await new Promise((r) => setTimeout(r, 50));
55296
- info2 = await exec.inspect();
55297
- }
55298
- if (info2.ExitCode !== 0) {
55299
- throw new Error(`Failed to write file ${filePath} in container (exit code ${info2.ExitCode})`);
55300
- }
55301
55311
  }
55302
55312
  async function readFileViaExec(container, filePath) {
55303
55313
  const exec = await container.exec({
@@ -55334,7 +55344,7 @@ async function startProxy(container, networkFilter) {
55334
55344
  }
55335
55345
  const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
55336
55346
  const startExec = await container.exec({
55337
- Cmd: ["sh", "-c", `${envPrefix}node /usr/local/bin/proxy.mjs &`]
55347
+ Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
55338
55348
  });
55339
55349
  await startExec.start({ Detach: true });
55340
55350
  const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
@@ -55357,6 +55367,27 @@ async function startProxy(container, networkFilter) {
55357
55367
  }
55358
55368
  throw new Error("Proxy failed to start within timeout");
55359
55369
  }
55370
+ async function setupIptables(container) {
55371
+ const rules = [
55372
+ "/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
55373
+ "/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
55374
+ `/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
55375
+ "/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
55376
+ ].join(" && ");
55377
+ const exec = await container.exec({
55378
+ Cmd: ["sh", "-c", rules]
55379
+ });
55380
+ await exec.start({ Detach: true });
55381
+ let info2 = await exec.inspect();
55382
+ while (info2.Running) {
55383
+ await new Promise((r) => setTimeout(r, 50));
55384
+ info2 = await exec.inspect();
55385
+ }
55386
+ if (info2.ExitCode !== 0) {
55387
+ throw new Error(`Failed to set up iptables rules (exit code ${info2.ExitCode})`);
55388
+ }
55389
+ logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
55390
+ }
55360
55391
  function wrapWithTimeout(cmd, timeoutSec) {
55361
55392
  return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
55362
55393
  }
@@ -55543,6 +55574,7 @@ class DockerIsol8 {
55543
55574
  await container.start();
55544
55575
  if (this.network === "filtered") {
55545
55576
  await startProxy(container, this.networkFilter);
55577
+ await setupIptables(container);
55546
55578
  }
55547
55579
  const ext = req.fileExtension ?? adapter.getFileExtension();
55548
55580
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
@@ -55623,6 +55655,7 @@ class DockerIsol8 {
55623
55655
  try {
55624
55656
  if (this.network === "filtered") {
55625
55657
  await startProxy(container, this.networkFilter);
55658
+ await setupIptables(container);
55626
55659
  }
55627
55660
  const ext = req.fileExtension ?? adapter.getFileExtension();
55628
55661
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
@@ -55782,6 +55815,7 @@ class DockerIsol8 {
55782
55815
  await this.container.start();
55783
55816
  if (this.network === "filtered") {
55784
55817
  await startProxy(this.container, this.networkFilter);
55818
+ await setupIptables(this.container);
55785
55819
  }
55786
55820
  this.persistentRuntime = adapter;
55787
55821
  }
@@ -55802,6 +55836,7 @@ class DockerIsol8 {
55802
55836
  };
55803
55837
  if (this.network === "filtered") {
55804
55838
  config.NetworkMode = "bridge";
55839
+ config.CapAdd = ["NET_ADMIN"];
55805
55840
  } else if (this.network === "host") {
55806
55841
  config.NetworkMode = "host";
55807
55842
  }
@@ -55861,9 +55896,11 @@ class DockerIsol8 {
55861
55896
  env2.push(`${key}=${value}`);
55862
55897
  }
55863
55898
  }
55864
- if (this.network === "filtered" && this.networkFilter) {
55865
- env2.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
55866
- env2.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
55899
+ if (this.network === "filtered") {
55900
+ if (this.networkFilter) {
55901
+ env2.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
55902
+ env2.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
55903
+ }
55867
55904
  env2.push(`HTTP_PROXY=http://127.0.0.1:${PROXY_PORT}`);
55868
55905
  env2.push(`HTTPS_PROXY=http://127.0.0.1:${PROXY_PORT}`);
55869
55906
  env2.push(`http_proxy=http://127.0.0.1:${PROXY_PORT}`);
@@ -55938,11 +55975,16 @@ class DockerIsol8 {
55938
55975
  let stderr = "";
55939
55976
  let truncated = false;
55940
55977
  let settled = false;
55978
+ let stdoutEnded = false;
55979
+ let stderrEnded = false;
55941
55980
  const timer = setTimeout(() => {
55942
55981
  if (settled) {
55943
55982
  return;
55944
55983
  }
55945
55984
  settled = true;
55985
+ if (stream.destroy) {
55986
+ stream.destroy();
55987
+ }
55946
55988
  resolve2({ stdout, stderr: `${stderr}
55947
55989
  --- EXECUTION TIMED OUT ---`, truncated });
55948
55990
  }, timeoutMs);
@@ -55965,13 +56007,23 @@ class DockerIsol8 {
55965
56007
  truncated = true;
55966
56008
  }
55967
56009
  });
55968
- stream.on("end", () => {
56010
+ const checkDone = () => {
55969
56011
  if (settled) {
55970
56012
  return;
55971
56013
  }
55972
- settled = true;
55973
- clearTimeout(timer);
55974
- resolve2({ stdout, stderr, truncated });
56014
+ if (stdoutEnded && stderrEnded) {
56015
+ settled = true;
56016
+ clearTimeout(timer);
56017
+ resolve2({ stdout, stderr, truncated });
56018
+ }
56019
+ };
56020
+ stdoutStream.on("end", () => {
56021
+ stdoutEnded = true;
56022
+ checkDone();
56023
+ });
56024
+ stderrStream.on("end", () => {
56025
+ stderrEnded = true;
56026
+ checkDone();
55975
56027
  });
55976
56028
  stream.on("error", (err) => {
55977
56029
  if (settled) {
@@ -55981,6 +56033,18 @@ class DockerIsol8 {
55981
56033
  clearTimeout(timer);
55982
56034
  reject(err);
55983
56035
  });
56036
+ stream.on("end", () => {
56037
+ if (settled) {
56038
+ return;
56039
+ }
56040
+ setTimeout(() => {
56041
+ if (!settled) {
56042
+ stdoutEnded = true;
56043
+ stderrEnded = true;
56044
+ checkDone();
56045
+ }
56046
+ }, 100);
56047
+ });
55984
56048
  });
55985
56049
  }
55986
56050
  postProcessOutput(output, _truncated) {
@@ -56025,7 +56089,7 @@ var package_default;
56025
56089
  var init_package = __esm(() => {
56026
56090
  package_default = {
56027
56091
  name: "isol8",
56028
- version: "0.7.0",
56092
+ version: "0.8.1",
56029
56093
  description: "Secure code execution engine for AI agents",
56030
56094
  author: "Illusion47586",
56031
56095
  license: "MIT",
@@ -61371,7 +61435,7 @@ async function buildBaseImages(docker, onProgress) {
61371
61435
  const target = adapter.name;
61372
61436
  onProgress?.({ runtime: target, status: "building" });
61373
61437
  try {
61374
- const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.mjs"] }, {
61438
+ const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.sh", "proxy-handler.sh"] }, {
61375
61439
  t: adapter.image,
61376
61440
  target,
61377
61441
  dockerfile: "Dockerfile"
@@ -62032,4 +62096,4 @@ if (!process.argv.slice(2).length) {
62032
62096
  }
62033
62097
  program2.parse();
62034
62098
 
62035
- //# debugId=BD6BAC9840A8A53E64756E2164756E21
62099
+ //# debugId=2B71A68DA2ABDB9664756E2164756E21
@@ -1,9 +1,10 @@
1
1
  # ── Base ──────────────────────────────────────────────────────────────
2
2
  FROM alpine:3.21 AS base
3
- RUN apk add --no-cache tini curl ca-certificates \
3
+ RUN apk add --no-cache tini curl ca-certificates iptables bash \
4
4
  && addgroup -S sandbox && adduser -S sandbox -G sandbox -h /sandbox
5
- COPY proxy.mjs /usr/local/bin/proxy.mjs
6
- RUN chmod +x /usr/local/bin/proxy.mjs
5
+ COPY proxy.sh /usr/local/bin/proxy.sh
6
+ COPY proxy-handler.sh /usr/local/bin/proxy-handler.sh
7
+ RUN chmod +x /usr/local/bin/proxy.sh /usr/local/bin/proxy-handler.sh
7
8
  WORKDIR /sandbox
8
9
  ENTRYPOINT ["/sbin/tini", "--"]
9
10
 
@@ -19,7 +20,7 @@ CMD ["node"]
19
20
 
20
21
  # ── Bun ───────────────────────────────────────────────────────────────
21
22
  FROM base AS bun
22
- RUN apk add --no-cache bash unzip libstdc++ libgcc \
23
+ RUN apk add --no-cache unzip libstdc++ libgcc \
23
24
  && curl -fsSL https://bun.sh/install | bash \
24
25
  && mv /root/.bun/bin/bun /usr/local/bin/bun \
25
26
  && ln -s /usr/local/bin/bun /usr/local/bin/bunx
@@ -27,15 +28,15 @@ CMD ["bun"]
27
28
 
28
29
  # ── Deno ──────────────────────────────────────────────────────────────
29
30
  FROM denoland/deno:alpine AS deno
30
- RUN apk add --no-cache tini curl ca-certificates \
31
+ RUN apk add --no-cache tini curl ca-certificates iptables bash \
31
32
  && addgroup -S sandbox && adduser -S sandbox -G sandbox -h /sandbox
32
- COPY proxy.mjs /usr/local/bin/proxy.mjs
33
- RUN chmod +x /usr/local/bin/proxy.mjs
33
+ COPY proxy.sh /usr/local/bin/proxy.sh
34
+ COPY proxy-handler.sh /usr/local/bin/proxy-handler.sh
35
+ RUN chmod +x /usr/local/bin/proxy.sh /usr/local/bin/proxy-handler.sh
34
36
  WORKDIR /sandbox
35
37
  ENTRYPOINT ["/sbin/tini", "--"]
36
38
  CMD ["deno"]
37
39
 
38
40
  # ── Bash ──────────────────────────────────────────────────────────────
39
41
  FROM base AS bash
40
- RUN apk add --no-cache bash
41
42
  CMD ["bash"]
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+ # isol8 proxy handler — handles a single proxied connection.
3
+ #
4
+ # Invoked by: nc -lk -e proxy-handler.sh
5
+ # stdin/stdout are wired to the client socket by nc.
6
+ #
7
+ # Env vars (inherited from proxy.sh launcher):
8
+ # ISOL8_WHITELIST_FILE - Path to file with whitelist regex patterns
9
+ # ISOL8_BLACKLIST_FILE - Path to file with blacklist regex patterns
10
+ #
11
+ # Supports:
12
+ # - HTTPS CONNECT tunneling (bidirectional relay via exec nc)
13
+ # - HTTP forwarding (GET/POST/etc via bash /dev/tcp)
14
+
15
+ WL="${ISOL8_WHITELIST_FILE:-}"
16
+ BL="${ISOL8_BLACKLIST_FILE:-}"
17
+
18
+ is_allowed() {
19
+ local host="$1"
20
+
21
+ # Check blacklist first
22
+ if [ -n "$BL" ] && [ -s "$BL" ]; then
23
+ if echo "$host" | grep -qEf "$BL" 2>/dev/null; then
24
+ return 1
25
+ fi
26
+ fi
27
+
28
+ # If whitelist is empty or missing, allow all
29
+ if [ -z "$WL" ] || [ ! -s "$WL" ]; then
30
+ return 0
31
+ fi
32
+
33
+ # Must match at least one whitelist pattern
34
+ if echo "$host" | grep -qEf "$WL" 2>/dev/null; then
35
+ return 0
36
+ fi
37
+
38
+ return 1
39
+ }
40
+
41
+ # Read the request line
42
+ # e.g. "CONNECT host:443 HTTP/1.1" or "GET http://host/path HTTP/1.1"
43
+ read -r request_line || exit 0
44
+ request_line="${request_line%%$'\r'}"
45
+
46
+ method="${request_line%% *}"
47
+ rest="${request_line#* }"
48
+ target="${rest%% *}"
49
+
50
+ # Read and store all headers until blank line
51
+ headers=""
52
+ content_length=0
53
+ while IFS= read -r hline; do
54
+ hline="${hline%%$'\r'}"
55
+ [ -z "$hline" ] && break
56
+ headers="${headers}${hline}"$'\n'
57
+ # Extract Content-Length
58
+ case "$hline" in
59
+ [Cc]ontent-[Ll]ength:*)
60
+ content_length="${hline#*: }"
61
+ content_length="${content_length// /}"
62
+ ;;
63
+ esac
64
+ done
65
+
66
+ # ── CONNECT (HTTPS tunneling) ──────────────────────────────────────────
67
+ if [ "$method" = "CONNECT" ]; then
68
+ host="${target%%:*}"
69
+ port="${target##*:}"
70
+ [ "$port" = "$host" ] && port=443
71
+
72
+ if ! is_allowed "$host"; then
73
+ msg="isol8: CONNECT to ${host} blocked by network filter"
74
+ printf "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s" \
75
+ "${#msg}" "$msg"
76
+ exit 0
77
+ fi
78
+
79
+ # Send 200 then replace this process with nc for bidirectional relay.
80
+ # nc inherits the client socket on stdin/stdout from the nc -lk -e parent.
81
+ printf "HTTP/1.1 200 Connection Established\r\n\r\n"
82
+ exec nc "$host" "$port"
83
+ fi
84
+
85
+ # ── HTTP forwarding ────────────────────────────────────────────────────
86
+ # Proxy HTTP requests use absolute URLs: GET http://host:port/path HTTP/1.1
87
+ url_rest="${target#*://}"
88
+ hostport="${url_rest%%/*}"
89
+ path="/${url_rest#*/}"
90
+ # Handle URLs with no path component
91
+ [ "$path" = "/${url_rest}" ] && path="/"
92
+
93
+ host="${hostport%%:*}"
94
+ port="${hostport##*:}"
95
+ [ "$port" = "$host" ] && port=80
96
+
97
+ if ! is_allowed "$host"; then
98
+ msg="isol8: request to ${host} blocked by network filter"
99
+ printf "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s" \
100
+ "${#msg}" "$msg"
101
+ exit 0
102
+ fi
103
+
104
+ # Open TCP connection via bash /dev/tcp
105
+ if ! exec 3<>/dev/tcp/"$host"/"$port" 2>/dev/null; then
106
+ msg="isol8: proxy error: connection to ${host}:${port} failed"
107
+ printf "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s" \
108
+ "${#msg}" "$msg"
109
+ exit 0
110
+ fi
111
+
112
+ # Send request line with relative path (not absolute URL)
113
+ printf "%s %s HTTP/1.1\r\n" "$method" "$path" >&3
114
+
115
+ # Forward headers, skipping Proxy-* headers
116
+ while IFS= read -r h; do
117
+ [ -z "$h" ] && continue
118
+ case "$h" in
119
+ Proxy-*|proxy-*) continue ;;
120
+ esac
121
+ printf "%s\r\n" "$h" >&3
122
+ done <<< "$headers"
123
+ printf "\r\n" >&3
124
+
125
+ # Forward request body if present
126
+ if [ "$content_length" -gt 0 ] 2>/dev/null; then
127
+ head -c "$content_length" >&3
128
+ fi
129
+
130
+ # Relay response back to client
131
+ cat <&3
132
+
133
+ exec 3>&-
134
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # isol8 proxy launcher — parses env vars and starts the proxy listener.
3
+ #
4
+ # Env vars:
5
+ # ISOL8_WHITELIST - JSON array of regex strings (allow these)
6
+ # ISOL8_BLACKLIST - JSON array of regex strings (block these)
7
+ # ISOL8_PROXY_PORT - Port to listen on (default: 8118)
8
+ #
9
+ # This script:
10
+ # 1. Parses ISOL8_WHITELIST/BLACKLIST JSON arrays into grep-compatible pattern files
11
+ # 2. Starts nc -lk -e /usr/local/bin/proxy-handler.sh on the specified port
12
+ #
13
+ # The pattern files are stored in /tmp/isol8-proxy/ and exported as env vars
14
+ # so the handler (forked by nc -e) can access them via inherited environment.
15
+
16
+ PORT="${ISOL8_PROXY_PORT:-8118}"
17
+ PROXY_DIR="/tmp/isol8-proxy"
18
+ mkdir -p "$PROXY_DIR"
19
+
20
+ WL_FILE="$PROXY_DIR/whitelist"
21
+ BL_FILE="$PROXY_DIR/blacklist"
22
+
23
+ # Parse JSON array of regex strings into a file with one ERE pattern per line.
24
+ # Input: JSON like '["^example\\.com$","^api\\."]'
25
+ # Output: file with one grep -E compatible pattern per line
26
+ parse_patterns() {
27
+ local json="$1" outfile="$2"
28
+ : > "$outfile"
29
+ if [ -z "$json" ] || [ "$json" = "[]" ]; then
30
+ return
31
+ fi
32
+ # Strip brackets, split on "," → one quoted pattern per line
33
+ # Then strip quotes and unescape doubled backslashes from JSON encoding
34
+ echo "$json" \
35
+ | sed 's/^\[//; s/\]$//' \
36
+ | sed 's/","/"\n"/g' \
37
+ | sed 's/^"//; s/"$//' \
38
+ | sed 's/\\\\/\\/g' \
39
+ > "$outfile"
40
+ }
41
+
42
+ parse_patterns "${ISOL8_WHITELIST:-}" "$WL_FILE"
43
+ parse_patterns "${ISOL8_BLACKLIST:-}" "$BL_FILE"
44
+
45
+ # Export paths so the handler (forked by nc -e) can find them
46
+ export ISOL8_WHITELIST_FILE="$WL_FILE"
47
+ export ISOL8_BLACKLIST_FILE="$BL_FILE"
48
+
49
+ echo "isol8 proxy listening on 127.0.0.1:${PORT}"
50
+
51
+ # Start listening — nc -lk provides a persistent server that forks
52
+ # a handler for each connection with stdin/stdout wired to the socket
53
+ exec nc -lk -s 127.0.0.1 -p "$PORT" -e /usr/local/bin/proxy-handler.sh
package/dist/index.js CHANGED
@@ -264,7 +264,11 @@ class ContainerPool {
264
264
  }
265
265
  try {
266
266
  const killExec = await container.exec({
267
- Cmd: ["sh", "-c", "pkill -9 -u sandbox 2>/dev/null; true"]
267
+ Cmd: [
268
+ "sh",
269
+ "-c",
270
+ "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; true"
271
+ ]
268
272
  });
269
273
  await killExec.start({ Detach: true });
270
274
  let killInfo = await killExec.inspect();
@@ -447,26 +451,32 @@ var exports_docker = {};
447
451
  __export(exports_docker, {
448
452
  DockerIsol8: () => DockerIsol8
449
453
  });
454
+ import { spawn } from "node:child_process";
450
455
  import { randomUUID } from "node:crypto";
451
456
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
452
457
  import { PassThrough } from "node:stream";
453
458
  import Docker from "dockerode";
454
459
  async function writeFileViaExec(container, filePath, content) {
455
460
  const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
456
- const b64 = data.toString("base64");
457
- const exec = await container.exec({
458
- Cmd: ["sh", "-c", `printf '%s' '${b64}' | base64 -d > ${filePath}`],
459
- User: "sandbox"
461
+ return new Promise((resolve2, reject) => {
462
+ const child = spawn("docker", ["exec", "-i", "-u", "sandbox", container.id, "sh", "-c", `cat > ${filePath}`], {
463
+ stdio: ["pipe", "ignore", "pipe"]
464
+ });
465
+ child.on("error", (err) => {
466
+ reject(new Error(`Failed to spawn docker exec: ${err.message}`));
467
+ });
468
+ let stderr = "";
469
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
470
+ child.stdin.write(data);
471
+ child.stdin.end();
472
+ child.on("close", (code) => {
473
+ if (code !== 0) {
474
+ reject(new Error(`Failed to write file ${filePath}: ${stderr} (exit code ${code})`));
475
+ } else {
476
+ resolve2();
477
+ }
478
+ });
460
479
  });
461
- await exec.start({ Detach: true });
462
- let info = await exec.inspect();
463
- while (info.Running) {
464
- await new Promise((r) => setTimeout(r, 50));
465
- info = await exec.inspect();
466
- }
467
- if (info.ExitCode !== 0) {
468
- throw new Error(`Failed to write file ${filePath} in container (exit code ${info.ExitCode})`);
469
- }
470
480
  }
471
481
  async function readFileViaExec(container, filePath) {
472
482
  const exec = await container.exec({
@@ -503,7 +513,7 @@ async function startProxy(container, networkFilter) {
503
513
  }
504
514
  const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
505
515
  const startExec = await container.exec({
506
- Cmd: ["sh", "-c", `${envPrefix}node /usr/local/bin/proxy.mjs &`]
516
+ Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
507
517
  });
508
518
  await startExec.start({ Detach: true });
509
519
  const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
@@ -526,6 +536,27 @@ async function startProxy(container, networkFilter) {
526
536
  }
527
537
  throw new Error("Proxy failed to start within timeout");
528
538
  }
539
+ async function setupIptables(container) {
540
+ const rules = [
541
+ "/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
542
+ "/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
543
+ `/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
544
+ "/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
545
+ ].join(" && ");
546
+ const exec = await container.exec({
547
+ Cmd: ["sh", "-c", rules]
548
+ });
549
+ await exec.start({ Detach: true });
550
+ let info = await exec.inspect();
551
+ while (info.Running) {
552
+ await new Promise((r) => setTimeout(r, 50));
553
+ info = await exec.inspect();
554
+ }
555
+ if (info.ExitCode !== 0) {
556
+ throw new Error(`Failed to set up iptables rules (exit code ${info.ExitCode})`);
557
+ }
558
+ logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
559
+ }
529
560
  function wrapWithTimeout(cmd, timeoutSec) {
530
561
  return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
531
562
  }
@@ -712,6 +743,7 @@ class DockerIsol8 {
712
743
  await container.start();
713
744
  if (this.network === "filtered") {
714
745
  await startProxy(container, this.networkFilter);
746
+ await setupIptables(container);
715
747
  }
716
748
  const ext = req.fileExtension ?? adapter.getFileExtension();
717
749
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
@@ -792,6 +824,7 @@ class DockerIsol8 {
792
824
  try {
793
825
  if (this.network === "filtered") {
794
826
  await startProxy(container, this.networkFilter);
827
+ await setupIptables(container);
795
828
  }
796
829
  const ext = req.fileExtension ?? adapter.getFileExtension();
797
830
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
@@ -951,6 +984,7 @@ class DockerIsol8 {
951
984
  await this.container.start();
952
985
  if (this.network === "filtered") {
953
986
  await startProxy(this.container, this.networkFilter);
987
+ await setupIptables(this.container);
954
988
  }
955
989
  this.persistentRuntime = adapter;
956
990
  }
@@ -971,6 +1005,7 @@ class DockerIsol8 {
971
1005
  };
972
1006
  if (this.network === "filtered") {
973
1007
  config.NetworkMode = "bridge";
1008
+ config.CapAdd = ["NET_ADMIN"];
974
1009
  } else if (this.network === "host") {
975
1010
  config.NetworkMode = "host";
976
1011
  }
@@ -1030,9 +1065,11 @@ class DockerIsol8 {
1030
1065
  env.push(`${key}=${value}`);
1031
1066
  }
1032
1067
  }
1033
- if (this.network === "filtered" && this.networkFilter) {
1034
- env.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
1035
- env.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
1068
+ if (this.network === "filtered") {
1069
+ if (this.networkFilter) {
1070
+ env.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
1071
+ env.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
1072
+ }
1036
1073
  env.push(`HTTP_PROXY=http://127.0.0.1:${PROXY_PORT}`);
1037
1074
  env.push(`HTTPS_PROXY=http://127.0.0.1:${PROXY_PORT}`);
1038
1075
  env.push(`http_proxy=http://127.0.0.1:${PROXY_PORT}`);
@@ -1107,11 +1144,16 @@ class DockerIsol8 {
1107
1144
  let stderr = "";
1108
1145
  let truncated = false;
1109
1146
  let settled = false;
1147
+ let stdoutEnded = false;
1148
+ let stderrEnded = false;
1110
1149
  const timer = setTimeout(() => {
1111
1150
  if (settled) {
1112
1151
  return;
1113
1152
  }
1114
1153
  settled = true;
1154
+ if (stream.destroy) {
1155
+ stream.destroy();
1156
+ }
1115
1157
  resolve2({ stdout, stderr: `${stderr}
1116
1158
  --- EXECUTION TIMED OUT ---`, truncated });
1117
1159
  }, timeoutMs);
@@ -1134,13 +1176,23 @@ class DockerIsol8 {
1134
1176
  truncated = true;
1135
1177
  }
1136
1178
  });
1137
- stream.on("end", () => {
1179
+ const checkDone = () => {
1138
1180
  if (settled) {
1139
1181
  return;
1140
1182
  }
1141
- settled = true;
1142
- clearTimeout(timer);
1143
- resolve2({ stdout, stderr, truncated });
1183
+ if (stdoutEnded && stderrEnded) {
1184
+ settled = true;
1185
+ clearTimeout(timer);
1186
+ resolve2({ stdout, stderr, truncated });
1187
+ }
1188
+ };
1189
+ stdoutStream.on("end", () => {
1190
+ stdoutEnded = true;
1191
+ checkDone();
1192
+ });
1193
+ stderrStream.on("end", () => {
1194
+ stderrEnded = true;
1195
+ checkDone();
1144
1196
  });
1145
1197
  stream.on("error", (err) => {
1146
1198
  if (settled) {
@@ -1150,6 +1202,18 @@ class DockerIsol8 {
1150
1202
  clearTimeout(timer);
1151
1203
  reject(err);
1152
1204
  });
1205
+ stream.on("end", () => {
1206
+ if (settled) {
1207
+ return;
1208
+ }
1209
+ setTimeout(() => {
1210
+ if (!settled) {
1211
+ stdoutEnded = true;
1212
+ stderrEnded = true;
1213
+ checkDone();
1214
+ }
1215
+ }, 100);
1216
+ });
1153
1217
  });
1154
1218
  }
1155
1219
  postProcessOutput(output, _truncated) {
@@ -1393,7 +1457,7 @@ init_logger();
1393
1457
  // package.json
1394
1458
  var package_default = {
1395
1459
  name: "isol8",
1396
- version: "0.7.0",
1460
+ version: "0.8.1",
1397
1461
  description: "Secure code execution engine for AI agents",
1398
1462
  author: "Illusion47586",
1399
1463
  license: "MIT",
@@ -1728,4 +1792,4 @@ export {
1728
1792
  BunAdapter
1729
1793
  };
1730
1794
 
1731
- //# debugId=94A90BFCC4E9D3A764756E2164756E21
1795
+ //# debugId=3E8DBF80E2D1D77264756E2164756E21
@@ -1 +1 @@
1
- {"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../../src/engine/docker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,MAAM,MAAM,WAAW,CAAC;AAG/B,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,WAAW,EAEX,YAAY,EAIZ,WAAW,EACZ,MAAM,UAAU,CAAC;AAoPlB,2HAA2H;AAC3H,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAY,YAAW,WAAW;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAElC,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,IAAI,CAA8B;IAE1C;;;OAGG;gBACS,OAAO,GAAE,kBAAuB,EAAE,aAAa,SAAK;IAwBhE;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,kFAAkF;IAC5E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3B;;;OAGG;IACG,OAAO,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAW9D;;;;;;;OAOG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYpE;;;;;;OAMG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB5C,6GAA6G;IAC7G,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED;;;OAGG;IACI,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAAC,WAAW,CAAC;YAqFzD,YAAY;YAcZ,gBAAgB;YAyGhB,iBAAiB;YA8FjB,aAAa;YAkBb,oBAAoB;YASpB,wBAAwB;IA2BtC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,yBAAyB;IAyBjC,OAAO,CAAC,QAAQ;YAmCD,gBAAgB;YA8EjB,iBAAiB;IA8D/B,OAAO,CAAC,iBAAiB;IAYzB;;;;;;;;;;;;;;;;;;;;OAoBG;WACU,OAAO,CAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CA2BlE"}
1
+ {"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../../src/engine/docker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,MAAM,MAAM,WAAW,CAAC;AAG/B,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,WAAW,EAEX,YAAY,EAIZ,WAAW,EACZ,MAAM,UAAU,CAAC;AAiTlB,2HAA2H;AAC3H,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAY,YAAW,WAAW;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAElC,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,IAAI,CAA8B;IAE1C;;;OAGG;gBACS,OAAO,GAAE,kBAAuB,EAAE,aAAa,SAAK;IAwBhE;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,kFAAkF;IAC5E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3B;;;OAGG;IACG,OAAO,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAW9D;;;;;;;OAOG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYpE;;;;;;OAMG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB5C,6GAA6G;IAC7G,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED;;;OAGG;IACI,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAAC,WAAW,CAAC;YAsFzD,YAAY;YAcZ,gBAAgB;YA0GhB,iBAAiB;YA8FjB,aAAa;YAkBb,oBAAoB;YASpB,wBAAwB;IA4BtC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,eAAe;IA2BvB,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,yBAAyB;IAyBjC,OAAO,CAAC,QAAQ;YAwCD,gBAAgB;YA8EjB,iBAAiB;IAiG/B,OAAO,CAAC,iBAAiB;IAYzB;;;;;;;;;;;;;;;;;;;;OAoBG;WACU,OAAO,CAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CA2BlE"}
@@ -1 +1 @@
1
- {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../../src/engine/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,WAAW,CAAC;AAGpC,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;CAC7D;AAOD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA+C;IAC7E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA4B;gBAEtD,OAAO,EAAE,WAAW;IAMhC;;;;OAIG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC;IAcvD;;;;;OAKG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CxE;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAgBd,eAAe;IAW7B,2DAA2D;IAC3D,OAAO,CAAC,SAAS;CAuClB"}
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../../src/engine/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,WAAW,CAAC;AAGpC,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;CAC7D;AAOD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA+C;IAC7E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA4B;gBAEtD,OAAO,EAAE,WAAW;IAMhC;;;;OAIG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC;IAcvD;;;;;OAKG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkDxE;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAgBd,eAAe;IAW7B,2DAA2D;IAC3D,OAAO,CAAC,SAAS;CAuClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isol8",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Secure code execution engine for AI agents",
5
5
  "author": "Illusion47586",
6
6
  "license": "MIT",
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * isol8 network proxy — lightweight HTTP/HTTPS filtering proxy.
5
- *
6
- * Reads whitelist/blacklist regex patterns from env vars and blocks
7
- * non-matching outbound requests with 403.
8
- *
9
- * Env vars:
10
- * ISOL8_WHITELIST - JSON array of regex strings (allow these)
11
- * ISOL8_BLACKLIST - JSON array of regex strings (block these)
12
- * ISOL8_PROXY_PORT - Port to listen on (default: 8118)
13
- *
14
- * Logic:
15
- * 1. If blacklist matches → BLOCK
16
- * 2. If whitelist is non-empty and hostname doesn't match → BLOCK
17
- * 3. Otherwise → ALLOW
18
- */
19
-
20
- import http from "node:http";
21
- import net from "node:net";
22
-
23
- const port = Number.parseInt(process.env.ISOL8_PROXY_PORT || "8118", 10);
24
-
25
- const whitelist = parsePatterns(process.env.ISOL8_WHITELIST);
26
- const blacklist = parsePatterns(process.env.ISOL8_BLACKLIST);
27
-
28
- function parsePatterns(envVar) {
29
- if (!envVar) {
30
- return [];
31
- }
32
- try {
33
- const arr = JSON.parse(envVar);
34
- return arr.map((p) => new RegExp(p));
35
- } catch {
36
- return [];
37
- }
38
- }
39
-
40
- function isAllowed(hostname) {
41
- // Check blacklist first
42
- for (const re of blacklist) {
43
- if (re.test(hostname)) {
44
- return false;
45
- }
46
- }
47
-
48
- // If whitelist is empty, allow all (only blacklist applies)
49
- if (whitelist.length === 0) {
50
- return true;
51
- }
52
-
53
- // Otherwise, must match at least one whitelist pattern
54
- for (const re of whitelist) {
55
- if (re.test(hostname)) {
56
- return true;
57
- }
58
- }
59
-
60
- return false;
61
- }
62
-
63
- const server = http.createServer((req, res) => {
64
- const url = new URL(req.url, `http://${req.headers.host}`);
65
- const hostname = url.hostname;
66
-
67
- if (!isAllowed(hostname)) {
68
- res.writeHead(403, { "Content-Type": "text/plain" });
69
- res.end(`isol8: request to ${hostname} blocked by network filter`);
70
- return;
71
- }
72
-
73
- // Forward HTTP request
74
- const proxyReq = http.request(
75
- {
76
- hostname: url.hostname,
77
- port: url.port || 80,
78
- path: url.pathname + url.search,
79
- method: req.method,
80
- headers: req.headers,
81
- },
82
- (proxyRes) => {
83
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
84
- proxyRes.pipe(res);
85
- }
86
- );
87
-
88
- proxyReq.on("error", (err) => {
89
- res.writeHead(502, { "Content-Type": "text/plain" });
90
- res.end(`isol8: proxy error: ${err.message}`);
91
- });
92
-
93
- req.pipe(proxyReq);
94
- });
95
-
96
- // Handle HTTPS CONNECT tunneling
97
- server.on("connect", (req, clientSocket, head) => {
98
- const [hostname, port] = req.url.split(":");
99
-
100
- if (!isAllowed(hostname)) {
101
- clientSocket.write(
102
- "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\n\r\n" +
103
- `isol8: CONNECT to ${hostname} blocked by network filter`
104
- );
105
- clientSocket.end();
106
- return;
107
- }
108
-
109
- const serverSocket = net.connect(Number.parseInt(port || "443", 10), hostname, () => {
110
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
111
- serverSocket.write(head);
112
- serverSocket.pipe(clientSocket);
113
- clientSocket.pipe(serverSocket);
114
- });
115
-
116
- serverSocket.on("error", (err) => {
117
- clientSocket.write(
118
- "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\n\r\n" +
119
- `isol8: tunnel error: ${err.message}`
120
- );
121
- clientSocket.end();
122
- });
123
- });
124
-
125
- server.listen(port, "127.0.0.1", () => {
126
- console.log(`isol8 proxy listening on 127.0.0.1:${port}`);
127
- });