@voratiq/sandbox-runtime 0.0.29-voratiq0
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/CHANGELOG.md +10 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +17 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +158 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/generate-seccomp-filter.d.ts +65 -0
- package/dist/sandbox/generate-seccomp-filter.d.ts.map +1 -0
- package/dist/sandbox/generate-seccomp-filter.js +185 -0
- package/dist/sandbox/generate-seccomp-filter.js.map +1 -0
- package/dist/sandbox/http-proxy.d.ts +14 -0
- package/dist/sandbox/http-proxy.d.ts.map +1 -0
- package/dist/sandbox/http-proxy.js +238 -0
- package/dist/sandbox/http-proxy.js.map +1 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts +121 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -0
- package/dist/sandbox/linux-sandbox-utils.js +723 -0
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts +57 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.js +611 -0
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -0
- package/dist/sandbox/observability.d.ts +56 -0
- package/dist/sandbox/observability.d.ts.map +1 -0
- package/dist/sandbox/observability.js +140 -0
- package/dist/sandbox/observability.js.map +1 -0
- package/dist/sandbox/sandbox-config.d.ts +277 -0
- package/dist/sandbox/sandbox-config.d.ts.map +1 -0
- package/dist/sandbox/sandbox-config.js +166 -0
- package/dist/sandbox/sandbox-config.js.map +1 -0
- package/dist/sandbox/sandbox-manager.d.ts +50 -0
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox/sandbox-manager.js +816 -0
- package/dist/sandbox/sandbox-manager.js.map +1 -0
- package/dist/sandbox/sandbox-schemas.d.ts +53 -0
- package/dist/sandbox/sandbox-schemas.d.ts.map +1 -0
- package/dist/sandbox/sandbox-schemas.js +3 -0
- package/dist/sandbox/sandbox-schemas.js.map +1 -0
- package/dist/sandbox/sandbox-utils.d.ts +83 -0
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -0
- package/dist/sandbox/sandbox-utils.js +343 -0
- package/dist/sandbox/sandbox-utils.js.map +1 -0
- package/dist/sandbox/sandbox-violation-store.d.ts +19 -0
- package/dist/sandbox/sandbox-violation-store.d.ts.map +1 -0
- package/dist/sandbox/sandbox-violation-store.js +54 -0
- package/dist/sandbox/sandbox-violation-store.js.map +1 -0
- package/dist/sandbox/socks-proxy.d.ts +14 -0
- package/dist/sandbox/socks-proxy.d.ts.map +1 -0
- package/dist/sandbox/socks-proxy.js +109 -0
- package/dist/sandbox/socks-proxy.js.map +1 -0
- package/dist/utils/config-loader.d.ts +11 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +60 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/debug.d.ts +7 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +25 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/platform.d.ts +15 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +49 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/ripgrep.d.ts +20 -0
- package/dist/utils/ripgrep.d.ts.map +1 -0
- package/dist/utils/ripgrep.js +51 -0
- package/dist/utils/ripgrep.js.map +1 -0
- package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/vendor/seccomp/arm64/unix-block.bpf +0 -0
- package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/vendor/seccomp/x64/unix-block.bpf +0 -0
- package/dist/vendor/seccomp-src/apply-seccomp.c +98 -0
- package/dist/vendor/seccomp-src/seccomp-unix-block.c +97 -0
- package/package.json +90 -0
- package/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/vendor/seccomp/arm64/unix-block.bpf +0 -0
- package/vendor/seccomp/x64/apply-seccomp +0 -0
- package/vendor/seccomp/x64/unix-block.bpf +0 -0
- package/vendor/seccomp-src/apply-seccomp.c +98 -0
- package/vendor/seccomp-src/seccomp-unix-block.c +97 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { logForDebugging } from '../utils/debug.js';
|
|
5
|
+
/**
|
|
6
|
+
* Map Node.js process.arch to our vendor directory architecture names
|
|
7
|
+
* Returns null for unsupported architectures
|
|
8
|
+
*/
|
|
9
|
+
function getVendorArchitecture() {
|
|
10
|
+
const arch = process.arch;
|
|
11
|
+
switch (arch) {
|
|
12
|
+
case 'x64':
|
|
13
|
+
case 'x86_64':
|
|
14
|
+
return 'x64';
|
|
15
|
+
case 'arm64':
|
|
16
|
+
case 'aarch64':
|
|
17
|
+
return 'arm64';
|
|
18
|
+
case 'ia32':
|
|
19
|
+
case 'x86':
|
|
20
|
+
// TODO: Add support for 32-bit x86 (ia32)
|
|
21
|
+
// Currently blocked because the seccomp filter does not block the socketcall() syscall,
|
|
22
|
+
// which is used on 32-bit x86 for all socket operations (socket, socketpair, bind, connect, etc.).
|
|
23
|
+
// On 32-bit x86, the direct socket() syscall doesn't exist - instead, all socket operations
|
|
24
|
+
// are multiplexed through socketcall(SYS_SOCKET, ...), socketcall(SYS_SOCKETPAIR, ...), etc.
|
|
25
|
+
//
|
|
26
|
+
// To properly support 32-bit x86, we need to:
|
|
27
|
+
// 1. Build a separate i386 BPF filter (BPF bytecode is architecture-specific)
|
|
28
|
+
// 2. Modify vendor/seccomp-src/seccomp-unix-block.c to conditionally add rules that block:
|
|
29
|
+
// - socketcall(SYS_SOCKET, [AF_UNIX, ...])
|
|
30
|
+
// - socketcall(SYS_SOCKETPAIR, [AF_UNIX, ...])
|
|
31
|
+
// 3. This requires complex BPF logic to inspect socketcall's sub-function argument
|
|
32
|
+
//
|
|
33
|
+
// Until then, 32-bit x86 is not supported to avoid a security bypass.
|
|
34
|
+
logForDebugging(`[SeccompFilter] 32-bit x86 (ia32) is not currently supported due to missing socketcall() syscall blocking. ` +
|
|
35
|
+
`The current seccomp filter only blocks socket(AF_UNIX, ...), but on 32-bit x86, socketcall() can be used to bypass this.`, { level: 'error' });
|
|
36
|
+
return null;
|
|
37
|
+
default:
|
|
38
|
+
logForDebugging(`[SeccompFilter] Unsupported architecture: ${arch}. Only x64 and arm64 are supported.`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the path to a pre-generated BPF filter file from the vendor directory
|
|
44
|
+
* Returns the path if it exists, null otherwise
|
|
45
|
+
*
|
|
46
|
+
* Pre-generated BPF files are organized by architecture:
|
|
47
|
+
* - vendor/seccomp/{x64,arm64}/unix-block.bpf
|
|
48
|
+
*
|
|
49
|
+
* Tries multiple paths for resilience:
|
|
50
|
+
* 0. Expected path provided via parameter (checked first if provided)
|
|
51
|
+
* 1. vendor/seccomp/{arch}/unix-block.bpf (bundled - when bundled into consuming packages)
|
|
52
|
+
* 2. ../../vendor/seccomp/{arch}/unix-block.bpf (package root - standard npm installs)
|
|
53
|
+
* 3. ../vendor/seccomp/{arch}/unix-block.bpf (dist/vendor - for bundlers)
|
|
54
|
+
*
|
|
55
|
+
* @param expectedPath - Optional path to check first (highest priority)
|
|
56
|
+
*/
|
|
57
|
+
export function getPreGeneratedBpfPath(expectedPath) {
|
|
58
|
+
// Check expected path first (highest priority)
|
|
59
|
+
if (expectedPath) {
|
|
60
|
+
if (fs.existsSync(expectedPath)) {
|
|
61
|
+
logForDebugging(`[SeccompFilter] Using BPF filter from expected path: ${expectedPath}`);
|
|
62
|
+
return expectedPath;
|
|
63
|
+
}
|
|
64
|
+
logForDebugging(`[SeccompFilter] Expected path provided but file not found: ${expectedPath}`);
|
|
65
|
+
}
|
|
66
|
+
// Determine architecture
|
|
67
|
+
const arch = getVendorArchitecture();
|
|
68
|
+
if (!arch) {
|
|
69
|
+
logForDebugging(`[SeccompFilter] Cannot find pre-generated BPF filter: unsupported architecture ${process.arch}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
logForDebugging(`[SeccompFilter] Detected architecture: ${arch}`);
|
|
73
|
+
// Try to locate the BPF file with fallback paths
|
|
74
|
+
// Path is relative to the compiled code location (dist/sandbox/)
|
|
75
|
+
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const relativePath = join('vendor', 'seccomp', arch, 'unix-block.bpf');
|
|
77
|
+
// Try paths in order of preference
|
|
78
|
+
const pathsToTry = [
|
|
79
|
+
join(baseDir, relativePath), // bundled: same directory as bundle (e.g., when bundled into claude-cli)
|
|
80
|
+
join(baseDir, '..', '..', relativePath), // package root: vendor/seccomp/...
|
|
81
|
+
join(baseDir, '..', relativePath), // dist: dist/vendor/seccomp/...
|
|
82
|
+
];
|
|
83
|
+
for (const bpfPath of pathsToTry) {
|
|
84
|
+
if (fs.existsSync(bpfPath)) {
|
|
85
|
+
logForDebugging(`[SeccompFilter] Found pre-generated BPF filter: ${bpfPath} (${arch})`);
|
|
86
|
+
return bpfPath;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
logForDebugging(`[SeccompFilter] Pre-generated BPF filter not found in any expected location (${arch})`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the path to the apply-seccomp binary from the vendor directory
|
|
94
|
+
* Returns the path if it exists, null otherwise
|
|
95
|
+
*
|
|
96
|
+
* Pre-built apply-seccomp binaries are organized by architecture:
|
|
97
|
+
* - vendor/seccomp/{x64,arm64}/apply-seccomp
|
|
98
|
+
*
|
|
99
|
+
* Tries multiple paths for resilience:
|
|
100
|
+
* 0. Expected path provided via parameter (checked first if provided)
|
|
101
|
+
* 1. vendor/seccomp/{arch}/apply-seccomp (bundled - when bundled into consuming packages)
|
|
102
|
+
* 2. ../../vendor/seccomp/{arch}/apply-seccomp (package root - standard npm installs)
|
|
103
|
+
* 3. ../vendor/seccomp/{arch}/apply-seccomp (dist/vendor - for bundlers)
|
|
104
|
+
*
|
|
105
|
+
* @param expectedPath - Optional path to check first (highest priority)
|
|
106
|
+
*/
|
|
107
|
+
export function getApplySeccompBinaryPath(expectedPath) {
|
|
108
|
+
// Check expected path first (highest priority)
|
|
109
|
+
if (expectedPath) {
|
|
110
|
+
if (fs.existsSync(expectedPath)) {
|
|
111
|
+
logForDebugging(`[SeccompFilter] Using apply-seccomp binary from expected path: ${expectedPath}`);
|
|
112
|
+
return expectedPath;
|
|
113
|
+
}
|
|
114
|
+
logForDebugging(`[SeccompFilter] Expected path provided but file not found: ${expectedPath}`);
|
|
115
|
+
}
|
|
116
|
+
// Determine architecture
|
|
117
|
+
const arch = getVendorArchitecture();
|
|
118
|
+
if (!arch) {
|
|
119
|
+
logForDebugging(`[SeccompFilter] Cannot find apply-seccomp binary: unsupported architecture ${process.arch}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
logForDebugging(`[SeccompFilter] Looking for apply-seccomp binary for architecture: ${arch}`);
|
|
123
|
+
// Try to locate the binary with fallback paths
|
|
124
|
+
// Path is relative to the compiled code location (dist/sandbox/)
|
|
125
|
+
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
126
|
+
const relativePath = join('vendor', 'seccomp', arch, 'apply-seccomp');
|
|
127
|
+
// Try paths in order of preference
|
|
128
|
+
const pathsToTry = [
|
|
129
|
+
join(baseDir, relativePath), // bundled: same directory as bundle (e.g., when bundled into claude-cli)
|
|
130
|
+
join(baseDir, '..', '..', relativePath), // package root: vendor/seccomp/...
|
|
131
|
+
join(baseDir, '..', relativePath), // dist: dist/vendor/seccomp/...
|
|
132
|
+
];
|
|
133
|
+
for (const binaryPath of pathsToTry) {
|
|
134
|
+
if (fs.existsSync(binaryPath)) {
|
|
135
|
+
logForDebugging(`[SeccompFilter] Found apply-seccomp binary: ${binaryPath} (${arch})`);
|
|
136
|
+
return binaryPath;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
logForDebugging(`[SeccompFilter] apply-seccomp binary not found in any expected location (${arch})`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the path to a pre-generated seccomp BPF filter that blocks Unix domain socket creation
|
|
144
|
+
* Returns the path to the BPF filter file, or null if not available
|
|
145
|
+
*
|
|
146
|
+
* The filter blocks socket(AF_UNIX, ...) syscalls while allowing all other syscalls.
|
|
147
|
+
* This prevents creation of new Unix domain socket file descriptors.
|
|
148
|
+
*
|
|
149
|
+
* Security scope:
|
|
150
|
+
* - Blocks: socket(AF_UNIX, ...) syscall (creating new Unix socket FDs)
|
|
151
|
+
* - Does NOT block: Operations on inherited Unix socket FDs (bind, connect, sendto, etc.)
|
|
152
|
+
* - Does NOT block: Unix socket FDs passed via SCM_RIGHTS
|
|
153
|
+
* - For most sandboxing scenarios, blocking socket creation is sufficient
|
|
154
|
+
*
|
|
155
|
+
* Note: This blocks ALL Unix socket creation, regardless of path. The allowUnixSockets
|
|
156
|
+
* configuration is not supported on Linux due to seccomp-bpf limitations (it cannot
|
|
157
|
+
* read user-space memory to inspect socket paths).
|
|
158
|
+
*
|
|
159
|
+
* Requirements:
|
|
160
|
+
* - Pre-generated BPF filters included for x64 and ARM64 only
|
|
161
|
+
* - Other architectures are not supported
|
|
162
|
+
*
|
|
163
|
+
* @param expectedPath - Optional path to check first (highest priority)
|
|
164
|
+
* @returns Path to the pre-generated BPF filter file, or null if not available
|
|
165
|
+
*/
|
|
166
|
+
export function generateSeccompFilter(expectedPath) {
|
|
167
|
+
const preGeneratedBpf = getPreGeneratedBpfPath(expectedPath);
|
|
168
|
+
if (preGeneratedBpf) {
|
|
169
|
+
logForDebugging('[SeccompFilter] Using pre-generated BPF filter');
|
|
170
|
+
return preGeneratedBpf;
|
|
171
|
+
}
|
|
172
|
+
logForDebugging('[SeccompFilter] Pre-generated BPF filter not available for this architecture. ' +
|
|
173
|
+
'Only x64 and arm64 are supported.', { level: 'error' });
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Clean up a seccomp filter file
|
|
178
|
+
* Since we only use pre-generated BPF files from vendor/, this is a no-op.
|
|
179
|
+
* Pre-generated files are never deleted.
|
|
180
|
+
* Kept for backward compatibility with existing code that calls it.
|
|
181
|
+
*/
|
|
182
|
+
export function cleanupSeccompFilter(_filterPath) {
|
|
183
|
+
// No-op: pre-generated BPF files are never cleaned up
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=generate-seccomp-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-seccomp-filter.js","sourceRoot":"","sources":["../../src/sandbox/generate-seccomp-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Socket, Server } from 'node:net';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
import { type NetworkFilterResult } from './observability.js';
|
|
4
|
+
export interface HttpProxyServerOptions {
|
|
5
|
+
filter(port: number, host: string, socket: Socket | Duplex): Promise<boolean | NetworkFilterResult> | boolean | NetworkFilterResult;
|
|
6
|
+
/**
|
|
7
|
+
* Optional function to get the MITM proxy socket path for a given host.
|
|
8
|
+
* If returns a socket path, the request will be routed through that MITM proxy.
|
|
9
|
+
* If returns undefined, the request will be handled directly.
|
|
10
|
+
*/
|
|
11
|
+
getMitmSocketPath?(host: string): string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function createHttpProxyServer(options: HttpProxyServerOptions): Server;
|
|
14
|
+
//# sourceMappingURL=http-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-proxy.d.ts","sourceRoot":"","sources":["../../src/sandbox/http-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOzC,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,sBAAsB;IACrC,MAAM,CACJ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,MAAM,GACtB,OAAO,CAAC,OAAO,GAAG,mBAAmB,CAAC,GAAG,OAAO,GAAG,mBAAmB,CAAA;IAEzE;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CACrD;AAWD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAiR7E"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Agent, createServer } from 'node:http';
|
|
2
|
+
import { request as httpRequest } from 'node:http';
|
|
3
|
+
import { request as httpsRequest } from 'node:https';
|
|
4
|
+
import { connect } from 'node:net';
|
|
5
|
+
import { URL } from 'node:url';
|
|
6
|
+
import { logForDebugging } from '../utils/debug.js';
|
|
7
|
+
import { emitNetworkDecisionEvent, } from './observability.js';
|
|
8
|
+
function normalizeFilterResult(result) {
|
|
9
|
+
if (typeof result === 'boolean') {
|
|
10
|
+
return { allowed: result, reason: 'no-match' };
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
export function createHttpProxyServer(options) {
|
|
15
|
+
const server = createServer();
|
|
16
|
+
// Handle CONNECT requests for HTTPS traffic
|
|
17
|
+
server.on('connect', async (req, socket) => {
|
|
18
|
+
// Attach error handler immediately to prevent unhandled errors
|
|
19
|
+
socket.on('error', err => {
|
|
20
|
+
logForDebugging(`Client socket error: ${err.message}`, { level: 'error' });
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
const [hostname, portStr] = req.url.split(':');
|
|
24
|
+
const port = portStr === undefined ? undefined : parseInt(portStr, 10);
|
|
25
|
+
if (!hostname || !port) {
|
|
26
|
+
logForDebugging(`Invalid CONNECT request: ${req.url}`, {
|
|
27
|
+
level: 'error',
|
|
28
|
+
});
|
|
29
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const filterResult = normalizeFilterResult(await options.filter(port, hostname, socket));
|
|
33
|
+
const mitmSocketPath = options.getMitmSocketPath?.(hostname);
|
|
34
|
+
const route = mitmSocketPath ? 'mitm' : 'direct';
|
|
35
|
+
emitNetworkDecisionEvent({
|
|
36
|
+
host: hostname,
|
|
37
|
+
port,
|
|
38
|
+
decision: filterResult.allowed ? 'allow' : 'deny',
|
|
39
|
+
reason: filterResult.reason,
|
|
40
|
+
route,
|
|
41
|
+
});
|
|
42
|
+
if (!filterResult.allowed) {
|
|
43
|
+
logForDebugging(`Connection blocked to ${hostname}:${port}`, {
|
|
44
|
+
level: 'error',
|
|
45
|
+
});
|
|
46
|
+
socket.end('HTTP/1.1 403 Forbidden\r\n' +
|
|
47
|
+
'Content-Type: text/plain\r\n' +
|
|
48
|
+
'X-Proxy-Error: blocked-by-allowlist\r\n' +
|
|
49
|
+
'\r\n' +
|
|
50
|
+
'Connection blocked by network allowlist');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (mitmSocketPath) {
|
|
54
|
+
// Route through MITM proxy via Unix socket
|
|
55
|
+
logForDebugging(`Routing CONNECT ${hostname}:${port} through MITM proxy at ${mitmSocketPath}`);
|
|
56
|
+
const mitmSocket = connect({ path: mitmSocketPath }, () => {
|
|
57
|
+
// Send CONNECT request to the MITM proxy
|
|
58
|
+
mitmSocket.write(`CONNECT ${hostname}:${port} HTTP/1.1\r\n` +
|
|
59
|
+
`Host: ${hostname}:${port}\r\n` +
|
|
60
|
+
'\r\n');
|
|
61
|
+
});
|
|
62
|
+
// Buffer to accumulate the MITM proxy's response
|
|
63
|
+
let responseBuffer = '';
|
|
64
|
+
const onMitmData = (chunk) => {
|
|
65
|
+
responseBuffer += chunk.toString();
|
|
66
|
+
// Check if we've received the full HTTP response headers
|
|
67
|
+
const headerEndIndex = responseBuffer.indexOf('\r\n\r\n');
|
|
68
|
+
if (headerEndIndex !== -1) {
|
|
69
|
+
// Remove data listener, we're done parsing the response
|
|
70
|
+
mitmSocket.removeListener('data', onMitmData);
|
|
71
|
+
// Check if MITM proxy accepted the connection
|
|
72
|
+
const statusLine = responseBuffer.substring(0, responseBuffer.indexOf('\r\n'));
|
|
73
|
+
if (statusLine.includes(' 200 ')) {
|
|
74
|
+
// Connection established, now pipe data between client and MITM
|
|
75
|
+
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
76
|
+
// If there's any data after the headers, write it to the client
|
|
77
|
+
const remainingData = responseBuffer.substring(headerEndIndex + 4);
|
|
78
|
+
if (remainingData.length > 0) {
|
|
79
|
+
socket.write(remainingData);
|
|
80
|
+
}
|
|
81
|
+
mitmSocket.pipe(socket);
|
|
82
|
+
socket.pipe(mitmSocket);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
logForDebugging(`MITM proxy rejected CONNECT: ${statusLine}`, {
|
|
86
|
+
level: 'error',
|
|
87
|
+
});
|
|
88
|
+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
89
|
+
mitmSocket.destroy();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
mitmSocket.on('data', onMitmData);
|
|
94
|
+
mitmSocket.on('error', err => {
|
|
95
|
+
logForDebugging(`MITM proxy connection failed: ${err.message}`, {
|
|
96
|
+
level: 'error',
|
|
97
|
+
});
|
|
98
|
+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
99
|
+
});
|
|
100
|
+
socket.on('error', err => {
|
|
101
|
+
logForDebugging(`Client socket error: ${err.message}`, {
|
|
102
|
+
level: 'error',
|
|
103
|
+
});
|
|
104
|
+
mitmSocket.destroy();
|
|
105
|
+
});
|
|
106
|
+
socket.on('end', () => mitmSocket.end());
|
|
107
|
+
mitmSocket.on('end', () => socket.end());
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Direct connection (original behavior)
|
|
111
|
+
const serverSocket = connect(port, hostname, () => {
|
|
112
|
+
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
113
|
+
serverSocket.pipe(socket);
|
|
114
|
+
socket.pipe(serverSocket);
|
|
115
|
+
});
|
|
116
|
+
serverSocket.on('error', err => {
|
|
117
|
+
logForDebugging(`CONNECT tunnel failed: ${err.message}`, {
|
|
118
|
+
level: 'error',
|
|
119
|
+
});
|
|
120
|
+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
121
|
+
});
|
|
122
|
+
socket.on('error', err => {
|
|
123
|
+
logForDebugging(`Client socket error: ${err.message}`, {
|
|
124
|
+
level: 'error',
|
|
125
|
+
});
|
|
126
|
+
serverSocket.destroy();
|
|
127
|
+
});
|
|
128
|
+
socket.on('end', () => serverSocket.end());
|
|
129
|
+
serverSocket.on('end', () => socket.end());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logForDebugging(`Error handling CONNECT: ${err}`, { level: 'error' });
|
|
134
|
+
socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// Handle regular HTTP requests
|
|
138
|
+
server.on('request', async (req, res) => {
|
|
139
|
+
try {
|
|
140
|
+
const url = new URL(req.url);
|
|
141
|
+
const hostname = url.hostname;
|
|
142
|
+
const port = url.port
|
|
143
|
+
? parseInt(url.port, 10)
|
|
144
|
+
: url.protocol === 'https:'
|
|
145
|
+
? 443
|
|
146
|
+
: 80;
|
|
147
|
+
const filterResult = normalizeFilterResult(await options.filter(port, hostname, req.socket));
|
|
148
|
+
const mitmSocketPath = options.getMitmSocketPath?.(hostname);
|
|
149
|
+
const route = mitmSocketPath ? 'mitm' : 'direct';
|
|
150
|
+
emitNetworkDecisionEvent({
|
|
151
|
+
host: hostname,
|
|
152
|
+
port,
|
|
153
|
+
decision: filterResult.allowed ? 'allow' : 'deny',
|
|
154
|
+
reason: filterResult.reason,
|
|
155
|
+
route,
|
|
156
|
+
});
|
|
157
|
+
if (!filterResult.allowed) {
|
|
158
|
+
logForDebugging(`HTTP request blocked to ${hostname}:${port}`, {
|
|
159
|
+
level: 'error',
|
|
160
|
+
});
|
|
161
|
+
res.writeHead(403, {
|
|
162
|
+
'Content-Type': 'text/plain',
|
|
163
|
+
'X-Proxy-Error': 'blocked-by-allowlist',
|
|
164
|
+
});
|
|
165
|
+
res.end('Connection blocked by network allowlist');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (mitmSocketPath) {
|
|
169
|
+
// Route through MITM proxy via Unix socket
|
|
170
|
+
// Use an agent that connects via the Unix socket
|
|
171
|
+
logForDebugging(`Routing HTTP ${req.method} ${hostname}:${port} through MITM proxy at ${mitmSocketPath}`);
|
|
172
|
+
const mitmAgent = new Agent({
|
|
173
|
+
// @ts-expect-error - socketPath is valid but not in types
|
|
174
|
+
socketPath: mitmSocketPath,
|
|
175
|
+
});
|
|
176
|
+
// Send request to MITM proxy with full URL (proxy-style request)
|
|
177
|
+
const proxyReq = httpRequest({
|
|
178
|
+
agent: mitmAgent,
|
|
179
|
+
// For proxy requests, path should be the full URL
|
|
180
|
+
path: req.url,
|
|
181
|
+
method: req.method,
|
|
182
|
+
headers: {
|
|
183
|
+
...req.headers,
|
|
184
|
+
host: url.host,
|
|
185
|
+
},
|
|
186
|
+
}, proxyRes => {
|
|
187
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
188
|
+
proxyRes.pipe(res);
|
|
189
|
+
});
|
|
190
|
+
proxyReq.on('error', err => {
|
|
191
|
+
logForDebugging(`MITM proxy request failed: ${err.message}`, {
|
|
192
|
+
level: 'error',
|
|
193
|
+
});
|
|
194
|
+
if (!res.headersSent) {
|
|
195
|
+
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
196
|
+
res.end('Bad Gateway');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
req.pipe(proxyReq);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Direct request (original behavior)
|
|
203
|
+
// Choose http or https module
|
|
204
|
+
const requestFn = url.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
205
|
+
const proxyReq = requestFn({
|
|
206
|
+
hostname,
|
|
207
|
+
port,
|
|
208
|
+
path: url.pathname + url.search,
|
|
209
|
+
method: req.method,
|
|
210
|
+
headers: {
|
|
211
|
+
...req.headers,
|
|
212
|
+
host: url.host,
|
|
213
|
+
},
|
|
214
|
+
}, proxyRes => {
|
|
215
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
216
|
+
proxyRes.pipe(res);
|
|
217
|
+
});
|
|
218
|
+
proxyReq.on('error', err => {
|
|
219
|
+
logForDebugging(`Proxy request failed: ${err.message}`, {
|
|
220
|
+
level: 'error',
|
|
221
|
+
});
|
|
222
|
+
if (!res.headersSent) {
|
|
223
|
+
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
224
|
+
res.end('Bad Gateway');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
req.pipe(proxyReq);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
logForDebugging(`Error handling HTTP request: ${err}`, { level: 'error' });
|
|
232
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
233
|
+
res.end('Internal Server Error');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
return server;
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=http-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-proxy.js","sourceRoot":"","sources":["../../src/sandbox/http-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { FsReadRestrictionConfig, FsWriteRestrictionConfig } from './sandbox-schemas.js';
|
|
3
|
+
export interface LinuxNetworkBridgeContext {
|
|
4
|
+
httpSocketPath: string;
|
|
5
|
+
socksSocketPath: string;
|
|
6
|
+
httpBridgeProcess: ChildProcess;
|
|
7
|
+
socksBridgeProcess: ChildProcess;
|
|
8
|
+
httpProxyPort: number;
|
|
9
|
+
socksProxyPort: number;
|
|
10
|
+
}
|
|
11
|
+
export interface LinuxSandboxParams {
|
|
12
|
+
command: string;
|
|
13
|
+
needsNetworkRestriction: boolean;
|
|
14
|
+
httpSocketPath?: string;
|
|
15
|
+
socksSocketPath?: string;
|
|
16
|
+
httpProxyPort?: number;
|
|
17
|
+
socksProxyPort?: number;
|
|
18
|
+
readConfig?: FsReadRestrictionConfig;
|
|
19
|
+
writeConfig?: FsWriteRestrictionConfig;
|
|
20
|
+
enableWeakerNestedSandbox?: boolean;
|
|
21
|
+
allowAllUnixSockets?: boolean;
|
|
22
|
+
binShell?: string;
|
|
23
|
+
ripgrepConfig?: {
|
|
24
|
+
command: string;
|
|
25
|
+
args?: string[];
|
|
26
|
+
};
|
|
27
|
+
/** Maximum directory depth to search for dangerous files (default: 3) */
|
|
28
|
+
mandatoryDenySearchDepth?: number;
|
|
29
|
+
/** Allow writes to .git/config files (default: false) */
|
|
30
|
+
allowGitConfig?: boolean;
|
|
31
|
+
/** Custom seccomp binary paths */
|
|
32
|
+
seccompConfig?: {
|
|
33
|
+
bpfPath?: string;
|
|
34
|
+
applyPath?: string;
|
|
35
|
+
};
|
|
36
|
+
/** Abort signal to cancel the ripgrep scan */
|
|
37
|
+
abortSignal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if Linux sandbox dependencies are available (synchronous)
|
|
41
|
+
* Returns true if bwrap and socat are installed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function hasLinuxSandboxDependenciesSync(allowAllUnixSockets?: boolean, seccompConfig?: {
|
|
44
|
+
bpfPath?: string;
|
|
45
|
+
applyPath?: string;
|
|
46
|
+
}): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the Linux network bridge for sandbox networking
|
|
49
|
+
*
|
|
50
|
+
* ARCHITECTURE NOTE:
|
|
51
|
+
* Linux network sandboxing uses bwrap --unshare-net which creates a completely isolated
|
|
52
|
+
* network namespace with NO network access. To enable network access, we:
|
|
53
|
+
*
|
|
54
|
+
* 1. Host side: Run socat bridges that listen on Unix sockets and forward to host proxy servers
|
|
55
|
+
* - HTTP bridge: Unix socket -> host HTTP proxy (for HTTP/HTTPS traffic)
|
|
56
|
+
* - SOCKS bridge: Unix socket -> host SOCKS5 proxy (for SSH/git traffic)
|
|
57
|
+
*
|
|
58
|
+
* 2. Sandbox side: Bind the Unix sockets into the isolated namespace and run socat listeners
|
|
59
|
+
* - HTTP listener on port 3128 -> HTTP Unix socket -> host HTTP proxy
|
|
60
|
+
* - SOCKS listener on port 1080 -> SOCKS Unix socket -> host SOCKS5 proxy
|
|
61
|
+
*
|
|
62
|
+
* 3. Configure environment:
|
|
63
|
+
* - HTTP_PROXY=http://localhost:3128 for HTTP/HTTPS tools
|
|
64
|
+
* - GIT_SSH_COMMAND with socat for SSH through SOCKS5
|
|
65
|
+
*
|
|
66
|
+
* LIMITATION: Unlike macOS sandbox which can enforce domain-based allowlists at the kernel level,
|
|
67
|
+
* Linux's --unshare-net provides only all-or-nothing network isolation. Domain filtering happens
|
|
68
|
+
* at the host proxy level, not the sandbox boundary. This means network restrictions on Linux
|
|
69
|
+
* depend on the proxy's filtering capabilities.
|
|
70
|
+
*
|
|
71
|
+
* DEPENDENCIES: Requires bwrap (bubblewrap) and socat
|
|
72
|
+
*/
|
|
73
|
+
export declare function initializeLinuxNetworkBridge(httpProxyPort: number, socksProxyPort: number): Promise<LinuxNetworkBridgeContext>;
|
|
74
|
+
/**
|
|
75
|
+
* Wrap a command with sandbox restrictions on Linux
|
|
76
|
+
*
|
|
77
|
+
* UNIX SOCKET BLOCKING (APPLY-SECCOMP):
|
|
78
|
+
* This implementation uses a custom apply-seccomp binary to block Unix domain socket
|
|
79
|
+
* creation for user commands while allowing network infrastructure:
|
|
80
|
+
*
|
|
81
|
+
* Stage 1: Outer bwrap - Network and filesystem isolation (NO seccomp)
|
|
82
|
+
* - Bubblewrap starts with isolated network namespace (--unshare-net)
|
|
83
|
+
* - Bubblewrap applies PID namespace isolation (--unshare-pid and --proc)
|
|
84
|
+
* - Filesystem restrictions are applied (read-only mounts, bind mounts, etc.)
|
|
85
|
+
* - Socat processes start and connect to Unix socket bridges (can use socket(AF_UNIX, ...))
|
|
86
|
+
*
|
|
87
|
+
* Stage 2: apply-seccomp - Seccomp filter application (ONLY seccomp)
|
|
88
|
+
* - apply-seccomp binary applies seccomp filter via prctl(PR_SET_SECCOMP)
|
|
89
|
+
* - Sets PR_SET_NO_NEW_PRIVS to allow seccomp without root
|
|
90
|
+
* - Execs user command with seccomp active (cannot create new Unix sockets)
|
|
91
|
+
*
|
|
92
|
+
* This solves the conflict between:
|
|
93
|
+
* - Security: Blocking arbitrary Unix socket creation in user commands
|
|
94
|
+
* - Functionality: Network sandboxing requires socat to call socket(AF_UNIX, ...) for bridge connections
|
|
95
|
+
*
|
|
96
|
+
* The seccomp-bpf filter blocks socket(AF_UNIX, ...) syscalls, preventing:
|
|
97
|
+
* - Creating new Unix domain socket file descriptors
|
|
98
|
+
*
|
|
99
|
+
* Security limitations:
|
|
100
|
+
* - Does NOT block operations (bind, connect, sendto, etc.) on inherited Unix socket FDs
|
|
101
|
+
* - Does NOT prevent passing Unix socket FDs via SCM_RIGHTS
|
|
102
|
+
* - For most sandboxing use cases, blocking socket creation is sufficient
|
|
103
|
+
*
|
|
104
|
+
* The filter allows:
|
|
105
|
+
* - All TCP/UDP sockets (AF_INET, AF_INET6) for normal network operations
|
|
106
|
+
* - All other syscalls
|
|
107
|
+
*
|
|
108
|
+
* PLATFORM NOTE:
|
|
109
|
+
* The allowUnixSockets configuration is not path-based on Linux (unlike macOS)
|
|
110
|
+
* because seccomp-bpf cannot inspect user-space memory to read socket paths.
|
|
111
|
+
*
|
|
112
|
+
* Requirements for seccomp filtering:
|
|
113
|
+
* - Pre-built apply-seccomp binaries are included for x64 and ARM64
|
|
114
|
+
* - Pre-generated BPF filters are included for x64 and ARM64
|
|
115
|
+
* - Other architectures are not currently supported (no apply-seccomp binary available)
|
|
116
|
+
* - To use sandboxing without Unix socket blocking on unsupported architectures,
|
|
117
|
+
* set allowAllUnixSockets: true in your configuration
|
|
118
|
+
* Dependencies are checked by hasLinuxSandboxDependenciesSync() before enabling the sandbox.
|
|
119
|
+
*/
|
|
120
|
+
export declare function wrapCommandWithSandboxLinux(params: LinuxSandboxParams): Promise<string>;
|
|
121
|
+
//# sourceMappingURL=linux-sandbox-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linux-sandbox-utils.d.ts","sourceRoot":"","sources":["../../src/sandbox/linux-sandbox-utils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAWtD,OAAO,KAAK,EACV,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,sBAAsB,CAAA;AAQ7B,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,YAAY,CAAA;IAC/B,kBAAkB,EAAE,YAAY,CAAA;IAChC,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,uBAAuB,EAAE,OAAO,CAAA;IAChC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,uBAAuB,CAAA;IACpC,WAAW,CAAC,EAAE,wBAAwB,CAAA;IACtC,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IACpD,yEAAyE;IACzE,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,yDAAyD;IACzD,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,kCAAkC;IAClC,aAAa,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxD,8CAA8C;IAC9C,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AA2MD;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,mBAAmB,UAAQ,EAC3B,aAAa,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAuCT;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,yBAAyB,CAAC,CA2HpC;AAoOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,MAAM,CAAC,CAuPjB"}
|