isol8 0.7.0 → 0.8.1
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 +7 -0
- package/dist/cli.js +49 -23
- 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 +45 -8
- package/dist/src/engine/docker.d.ts.map +1 -1
- package/dist/src/engine/pool.d.ts.map +1 -1
- package/package.json +8 -2
- package/dist/docker/proxy.mjs +0 -127
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# isol8
|
|
2
2
|
|
|
3
|
+
[](https://github.com/Illusion47586/isol8/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Illusion47586/isol8/actions)
|
|
5
|
+
[](https://www.npmjs.com/package/isol8)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
3
8
|
Secure code execution engine for AI agents. Run untrusted Python, Node.js, Bun, Deno, and Bash code inside locked-down Docker containers with network filtering, resource limits, and output controls.
|
|
4
9
|
|
|
5
10
|
## Features
|
|
@@ -270,6 +275,8 @@ const isol8 = new DockerIsol8({
|
|
|
270
275
|
});
|
|
271
276
|
```
|
|
272
277
|
|
|
278
|
+
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.
|
|
279
|
+
|
|
273
280
|
## Configuration
|
|
274
281
|
|
|
275
282
|
Create `isol8.config.json` in your project root or `~/.isol8/config.json`.
|
package/dist/cli.js
CHANGED
|
@@ -6316,23 +6316,12 @@ var require_bcrypt_pbkdf = __commonJS((exports, module) => {
|
|
|
6316
6316
|
};
|
|
6317
6317
|
});
|
|
6318
6318
|
|
|
6319
|
-
// node_modules/cpu-features/build/Release/cpufeatures.node
|
|
6320
|
-
var require_cpufeatures = __commonJS((exports, module) => {
|
|
6321
|
-
module.exports = __require("./cpufeatures-tjjrgpt7.node");
|
|
6322
|
-
});
|
|
6323
|
-
|
|
6324
|
-
// node_modules/cpu-features/lib/index.js
|
|
6325
|
-
var require_lib2 = __commonJS((exports, module) => {
|
|
6326
|
-
var binding = require_cpufeatures();
|
|
6327
|
-
module.exports = binding.getCPUInfo;
|
|
6328
|
-
});
|
|
6329
|
-
|
|
6330
6319
|
// node_modules/ssh2/lib/protocol/constants.js
|
|
6331
6320
|
var require_constants = __commonJS((exports, module) => {
|
|
6332
6321
|
var crypto = __require("crypto");
|
|
6333
6322
|
var cpuInfo;
|
|
6334
6323
|
try {
|
|
6335
|
-
cpuInfo =
|
|
6324
|
+
cpuInfo = (()=>{throw new Error("Cannot require module "+"cpu-features");})()();
|
|
6336
6325
|
} catch {}
|
|
6337
6326
|
var { bindingAvailable, CIPHER_INFO, MAC_INFO } = require_crypto();
|
|
6338
6327
|
var eddsaSupported = (() => {
|
|
@@ -20975,7 +20964,7 @@ var require_keygen = __commonJS((exports, module) => {
|
|
|
20975
20964
|
});
|
|
20976
20965
|
|
|
20977
20966
|
// node_modules/ssh2/lib/index.js
|
|
20978
|
-
var
|
|
20967
|
+
var require_lib2 = __commonJS((exports, module) => {
|
|
20979
20968
|
var {
|
|
20980
20969
|
AgentProtocol,
|
|
20981
20970
|
BaseAgent,
|
|
@@ -21021,7 +21010,7 @@ var require_lib3 = __commonJS((exports, module) => {
|
|
|
21021
21010
|
|
|
21022
21011
|
// node_modules/docker-modem/lib/ssh.js
|
|
21023
21012
|
var require_ssh = __commonJS((exports, module) => {
|
|
21024
|
-
var Client =
|
|
21013
|
+
var Client = require_lib2().Client;
|
|
21025
21014
|
var http = __require("http");
|
|
21026
21015
|
module.exports = function(opt) {
|
|
21027
21016
|
var conn = new Client;
|
|
@@ -55088,7 +55077,11 @@ class ContainerPool {
|
|
|
55088
55077
|
}
|
|
55089
55078
|
try {
|
|
55090
55079
|
const killExec = await container.exec({
|
|
55091
|
-
Cmd: [
|
|
55080
|
+
Cmd: [
|
|
55081
|
+
"sh",
|
|
55082
|
+
"-c",
|
|
55083
|
+
"pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; true"
|
|
55084
|
+
]
|
|
55092
55085
|
});
|
|
55093
55086
|
await killExec.start({ Detach: true });
|
|
55094
55087
|
let killInfo = await killExec.inspect();
|
|
@@ -55334,7 +55327,7 @@ async function startProxy(container, networkFilter) {
|
|
|
55334
55327
|
}
|
|
55335
55328
|
const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
|
|
55336
55329
|
const startExec = await container.exec({
|
|
55337
|
-
Cmd: ["sh", "-c", `${envPrefix}
|
|
55330
|
+
Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
|
|
55338
55331
|
});
|
|
55339
55332
|
await startExec.start({ Detach: true });
|
|
55340
55333
|
const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
|
|
@@ -55357,6 +55350,27 @@ async function startProxy(container, networkFilter) {
|
|
|
55357
55350
|
}
|
|
55358
55351
|
throw new Error("Proxy failed to start within timeout");
|
|
55359
55352
|
}
|
|
55353
|
+
async function setupIptables(container) {
|
|
55354
|
+
const rules = [
|
|
55355
|
+
"/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
|
|
55356
|
+
"/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
|
|
55357
|
+
`/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
|
|
55358
|
+
"/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
|
|
55359
|
+
].join(" && ");
|
|
55360
|
+
const exec = await container.exec({
|
|
55361
|
+
Cmd: ["sh", "-c", rules]
|
|
55362
|
+
});
|
|
55363
|
+
await exec.start({ Detach: true });
|
|
55364
|
+
let info2 = await exec.inspect();
|
|
55365
|
+
while (info2.Running) {
|
|
55366
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
55367
|
+
info2 = await exec.inspect();
|
|
55368
|
+
}
|
|
55369
|
+
if (info2.ExitCode !== 0) {
|
|
55370
|
+
throw new Error(`Failed to set up iptables rules (exit code ${info2.ExitCode})`);
|
|
55371
|
+
}
|
|
55372
|
+
logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
|
|
55373
|
+
}
|
|
55360
55374
|
function wrapWithTimeout(cmd, timeoutSec) {
|
|
55361
55375
|
return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
|
|
55362
55376
|
}
|
|
@@ -55543,6 +55557,7 @@ class DockerIsol8 {
|
|
|
55543
55557
|
await container.start();
|
|
55544
55558
|
if (this.network === "filtered") {
|
|
55545
55559
|
await startProxy(container, this.networkFilter);
|
|
55560
|
+
await setupIptables(container);
|
|
55546
55561
|
}
|
|
55547
55562
|
const ext = req.fileExtension ?? adapter.getFileExtension();
|
|
55548
55563
|
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
@@ -55623,6 +55638,7 @@ class DockerIsol8 {
|
|
|
55623
55638
|
try {
|
|
55624
55639
|
if (this.network === "filtered") {
|
|
55625
55640
|
await startProxy(container, this.networkFilter);
|
|
55641
|
+
await setupIptables(container);
|
|
55626
55642
|
}
|
|
55627
55643
|
const ext = req.fileExtension ?? adapter.getFileExtension();
|
|
55628
55644
|
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
@@ -55782,6 +55798,7 @@ class DockerIsol8 {
|
|
|
55782
55798
|
await this.container.start();
|
|
55783
55799
|
if (this.network === "filtered") {
|
|
55784
55800
|
await startProxy(this.container, this.networkFilter);
|
|
55801
|
+
await setupIptables(this.container);
|
|
55785
55802
|
}
|
|
55786
55803
|
this.persistentRuntime = adapter;
|
|
55787
55804
|
}
|
|
@@ -55802,6 +55819,7 @@ class DockerIsol8 {
|
|
|
55802
55819
|
};
|
|
55803
55820
|
if (this.network === "filtered") {
|
|
55804
55821
|
config.NetworkMode = "bridge";
|
|
55822
|
+
config.CapAdd = ["NET_ADMIN"];
|
|
55805
55823
|
} else if (this.network === "host") {
|
|
55806
55824
|
config.NetworkMode = "host";
|
|
55807
55825
|
}
|
|
@@ -55861,9 +55879,11 @@ class DockerIsol8 {
|
|
|
55861
55879
|
env2.push(`${key}=${value}`);
|
|
55862
55880
|
}
|
|
55863
55881
|
}
|
|
55864
|
-
if (this.network === "filtered"
|
|
55865
|
-
|
|
55866
|
-
|
|
55882
|
+
if (this.network === "filtered") {
|
|
55883
|
+
if (this.networkFilter) {
|
|
55884
|
+
env2.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
|
|
55885
|
+
env2.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
|
|
55886
|
+
}
|
|
55867
55887
|
env2.push(`HTTP_PROXY=http://127.0.0.1:${PROXY_PORT}`);
|
|
55868
55888
|
env2.push(`HTTPS_PROXY=http://127.0.0.1:${PROXY_PORT}`);
|
|
55869
55889
|
env2.push(`http_proxy=http://127.0.0.1:${PROXY_PORT}`);
|
|
@@ -56025,7 +56045,7 @@ var package_default;
|
|
|
56025
56045
|
var init_package = __esm(() => {
|
|
56026
56046
|
package_default = {
|
|
56027
56047
|
name: "isol8",
|
|
56028
|
-
version: "0.
|
|
56048
|
+
version: "0.8.0",
|
|
56029
56049
|
description: "Secure code execution engine for AI agents",
|
|
56030
56050
|
author: "Illusion47586",
|
|
56031
56051
|
license: "MIT",
|
|
@@ -56086,6 +56106,8 @@ var init_package = __esm(() => {
|
|
|
56086
56106
|
},
|
|
56087
56107
|
devDependencies: {
|
|
56088
56108
|
"@biomejs/biome": "^2.3.15",
|
|
56109
|
+
"@commitlint/cli": "^20.4.1",
|
|
56110
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
56089
56111
|
"@semantic-release/changelog": "^6.0.3",
|
|
56090
56112
|
"@semantic-release/exec": "^7.1.0",
|
|
56091
56113
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -56118,7 +56140,8 @@ var init_package = __esm(() => {
|
|
|
56118
56140
|
}
|
|
56119
56141
|
],
|
|
56120
56142
|
"simple-git-hooks": {
|
|
56121
|
-
"pre-commit": "bun run lint-staged"
|
|
56143
|
+
"pre-commit": "bun run lint-staged",
|
|
56144
|
+
"commit-msg": "bunx commitlint --edit $1"
|
|
56122
56145
|
},
|
|
56123
56146
|
"lint-staged": {
|
|
56124
56147
|
"*.{ts,tsx}": [
|
|
@@ -56128,6 +56151,9 @@ var init_package = __esm(() => {
|
|
|
56128
56151
|
"src/types.ts": [
|
|
56129
56152
|
"bash -c 'bun run schema'",
|
|
56130
56153
|
"git add schema/isol8.config.schema.json"
|
|
56154
|
+
],
|
|
56155
|
+
"*.{yaml,yml,json}": [
|
|
56156
|
+
"ultracite fix"
|
|
56131
56157
|
]
|
|
56132
56158
|
}
|
|
56133
56159
|
};
|
|
@@ -61365,7 +61391,7 @@ async function buildBaseImages(docker, onProgress) {
|
|
|
61365
61391
|
const target = adapter.name;
|
|
61366
61392
|
onProgress?.({ runtime: target, status: "building" });
|
|
61367
61393
|
try {
|
|
61368
|
-
const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.
|
|
61394
|
+
const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.sh", "proxy-handler.sh"] }, {
|
|
61369
61395
|
t: adapter.image,
|
|
61370
61396
|
target,
|
|
61371
61397
|
dockerfile: "Dockerfile"
|
|
@@ -62026,4 +62052,4 @@ if (!process.argv.slice(2).length) {
|
|
|
62026
62052
|
}
|
|
62027
62053
|
program2.parse();
|
|
62028
62054
|
|
|
62029
|
-
//# debugId=
|
|
62055
|
+
//# debugId=F5B0FBC3FF234CB364756E2164756E21
|
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();
|
|
@@ -503,7 +507,7 @@ async function startProxy(container, networkFilter) {
|
|
|
503
507
|
}
|
|
504
508
|
const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
|
|
505
509
|
const startExec = await container.exec({
|
|
506
|
-
Cmd: ["sh", "-c", `${envPrefix}
|
|
510
|
+
Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
|
|
507
511
|
});
|
|
508
512
|
await startExec.start({ Detach: true });
|
|
509
513
|
const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
|
|
@@ -526,6 +530,27 @@ async function startProxy(container, networkFilter) {
|
|
|
526
530
|
}
|
|
527
531
|
throw new Error("Proxy failed to start within timeout");
|
|
528
532
|
}
|
|
533
|
+
async function setupIptables(container) {
|
|
534
|
+
const rules = [
|
|
535
|
+
"/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
|
|
536
|
+
"/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
|
|
537
|
+
`/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
|
|
538
|
+
"/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
|
|
539
|
+
].join(" && ");
|
|
540
|
+
const exec = await container.exec({
|
|
541
|
+
Cmd: ["sh", "-c", rules]
|
|
542
|
+
});
|
|
543
|
+
await exec.start({ Detach: true });
|
|
544
|
+
let info = await exec.inspect();
|
|
545
|
+
while (info.Running) {
|
|
546
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
547
|
+
info = await exec.inspect();
|
|
548
|
+
}
|
|
549
|
+
if (info.ExitCode !== 0) {
|
|
550
|
+
throw new Error(`Failed to set up iptables rules (exit code ${info.ExitCode})`);
|
|
551
|
+
}
|
|
552
|
+
logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
|
|
553
|
+
}
|
|
529
554
|
function wrapWithTimeout(cmd, timeoutSec) {
|
|
530
555
|
return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
|
|
531
556
|
}
|
|
@@ -712,6 +737,7 @@ class DockerIsol8 {
|
|
|
712
737
|
await container.start();
|
|
713
738
|
if (this.network === "filtered") {
|
|
714
739
|
await startProxy(container, this.networkFilter);
|
|
740
|
+
await setupIptables(container);
|
|
715
741
|
}
|
|
716
742
|
const ext = req.fileExtension ?? adapter.getFileExtension();
|
|
717
743
|
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
@@ -792,6 +818,7 @@ class DockerIsol8 {
|
|
|
792
818
|
try {
|
|
793
819
|
if (this.network === "filtered") {
|
|
794
820
|
await startProxy(container, this.networkFilter);
|
|
821
|
+
await setupIptables(container);
|
|
795
822
|
}
|
|
796
823
|
const ext = req.fileExtension ?? adapter.getFileExtension();
|
|
797
824
|
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
@@ -951,6 +978,7 @@ class DockerIsol8 {
|
|
|
951
978
|
await this.container.start();
|
|
952
979
|
if (this.network === "filtered") {
|
|
953
980
|
await startProxy(this.container, this.networkFilter);
|
|
981
|
+
await setupIptables(this.container);
|
|
954
982
|
}
|
|
955
983
|
this.persistentRuntime = adapter;
|
|
956
984
|
}
|
|
@@ -971,6 +999,7 @@ class DockerIsol8 {
|
|
|
971
999
|
};
|
|
972
1000
|
if (this.network === "filtered") {
|
|
973
1001
|
config.NetworkMode = "bridge";
|
|
1002
|
+
config.CapAdd = ["NET_ADMIN"];
|
|
974
1003
|
} else if (this.network === "host") {
|
|
975
1004
|
config.NetworkMode = "host";
|
|
976
1005
|
}
|
|
@@ -1030,9 +1059,11 @@ class DockerIsol8 {
|
|
|
1030
1059
|
env.push(`${key}=${value}`);
|
|
1031
1060
|
}
|
|
1032
1061
|
}
|
|
1033
|
-
if (this.network === "filtered"
|
|
1034
|
-
|
|
1035
|
-
|
|
1062
|
+
if (this.network === "filtered") {
|
|
1063
|
+
if (this.networkFilter) {
|
|
1064
|
+
env.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
|
|
1065
|
+
env.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
|
|
1066
|
+
}
|
|
1036
1067
|
env.push(`HTTP_PROXY=http://127.0.0.1:${PROXY_PORT}`);
|
|
1037
1068
|
env.push(`HTTPS_PROXY=http://127.0.0.1:${PROXY_PORT}`);
|
|
1038
1069
|
env.push(`http_proxy=http://127.0.0.1:${PROXY_PORT}`);
|
|
@@ -1393,7 +1424,7 @@ init_logger();
|
|
|
1393
1424
|
// package.json
|
|
1394
1425
|
var package_default = {
|
|
1395
1426
|
name: "isol8",
|
|
1396
|
-
version: "0.
|
|
1427
|
+
version: "0.8.0",
|
|
1397
1428
|
description: "Secure code execution engine for AI agents",
|
|
1398
1429
|
author: "Illusion47586",
|
|
1399
1430
|
license: "MIT",
|
|
@@ -1454,6 +1485,8 @@ var package_default = {
|
|
|
1454
1485
|
},
|
|
1455
1486
|
devDependencies: {
|
|
1456
1487
|
"@biomejs/biome": "^2.3.15",
|
|
1488
|
+
"@commitlint/cli": "^20.4.1",
|
|
1489
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
1457
1490
|
"@semantic-release/changelog": "^6.0.3",
|
|
1458
1491
|
"@semantic-release/exec": "^7.1.0",
|
|
1459
1492
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -1486,7 +1519,8 @@ var package_default = {
|
|
|
1486
1519
|
}
|
|
1487
1520
|
],
|
|
1488
1521
|
"simple-git-hooks": {
|
|
1489
|
-
"pre-commit": "bun run lint-staged"
|
|
1522
|
+
"pre-commit": "bun run lint-staged",
|
|
1523
|
+
"commit-msg": "bunx commitlint --edit $1"
|
|
1490
1524
|
},
|
|
1491
1525
|
"lint-staged": {
|
|
1492
1526
|
"*.{ts,tsx}": [
|
|
@@ -1496,6 +1530,9 @@ var package_default = {
|
|
|
1496
1530
|
"src/types.ts": [
|
|
1497
1531
|
"bash -c 'bun run schema'",
|
|
1498
1532
|
"git add schema/isol8.config.schema.json"
|
|
1533
|
+
],
|
|
1534
|
+
"*.{yaml,yml,json}": [
|
|
1535
|
+
"ultracite fix"
|
|
1499
1536
|
]
|
|
1500
1537
|
}
|
|
1501
1538
|
};
|
|
@@ -1722,4 +1759,4 @@ export {
|
|
|
1722
1759
|
BunAdapter
|
|
1723
1760
|
};
|
|
1724
1761
|
|
|
1725
|
-
//# debugId=
|
|
1762
|
+
//# debugId=90624257FBE4C46A64756E2164756E21
|
|
@@ -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;
|
|
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;AAqSlB,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;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 +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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isol8",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Secure code execution engine for AI agents",
|
|
5
5
|
"author": "Illusion47586",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@biomejs/biome": "^2.3.15",
|
|
64
|
+
"@commitlint/cli": "^20.4.1",
|
|
65
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
64
66
|
"@semantic-release/changelog": "^6.0.3",
|
|
65
67
|
"@semantic-release/exec": "^7.1.0",
|
|
66
68
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -93,7 +95,8 @@
|
|
|
93
95
|
}
|
|
94
96
|
],
|
|
95
97
|
"simple-git-hooks": {
|
|
96
|
-
"pre-commit": "bun run lint-staged"
|
|
98
|
+
"pre-commit": "bun run lint-staged",
|
|
99
|
+
"commit-msg": "bunx commitlint --edit $1"
|
|
97
100
|
},
|
|
98
101
|
"lint-staged": {
|
|
99
102
|
"*.{ts,tsx}": [
|
|
@@ -103,6 +106,9 @@
|
|
|
103
106
|
"src/types.ts": [
|
|
104
107
|
"bash -c 'bun run schema'",
|
|
105
108
|
"git add schema/isol8.config.schema.json"
|
|
109
|
+
],
|
|
110
|
+
"*.{yaml,yml,json}": [
|
|
111
|
+
"ultracite fix"
|
|
106
112
|
]
|
|
107
113
|
}
|
|
108
114
|
}
|
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
|
-
});
|