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 +3 -0
- package/dist/cli.js +89 -25
- package/dist/docker/Dockerfile +9 -8
- package/dist/docker/proxy-handler.sh +134 -0
- package/dist/docker/proxy.sh +53 -0
- package/dist/index.js +88 -24
- package/dist/src/engine/docker.d.ts.map +1 -1
- package/dist/src/engine/pool.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/docker/proxy.mjs +0 -127
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: [
|
|
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
|
-
|
|
55288
|
-
|
|
55289
|
-
|
|
55290
|
-
|
|
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}
|
|
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"
|
|
55865
|
-
|
|
55866
|
-
|
|
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
|
-
|
|
56010
|
+
const checkDone = () => {
|
|
55969
56011
|
if (settled) {
|
|
55970
56012
|
return;
|
|
55971
56013
|
}
|
|
55972
|
-
|
|
55973
|
-
|
|
55974
|
-
|
|
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.
|
|
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.
|
|
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=
|
|
62099
|
+
//# debugId=2B71A68DA2ABDB9664756E2164756E21
|
package/dist/docker/Dockerfile
CHANGED
|
@@ -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.
|
|
6
|
-
|
|
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
|
|
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.
|
|
33
|
-
|
|
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: [
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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}
|
|
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"
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
1179
|
+
const checkDone = () => {
|
|
1138
1180
|
if (settled) {
|
|
1139
1181
|
return;
|
|
1140
1182
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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.
|
|
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=
|
|
1795
|
+
//# debugId=3E8DBF80E2D1D77264756E2164756E21
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../../src/engine/docker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
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;
|
|
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
package/dist/docker/proxy.mjs
DELETED
|
@@ -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
|
-
});
|