Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Tests

on:
push:
branches: ['**']
branches: ['main']
pull_request:
branches: ['**']

Expand Down Expand Up @@ -73,19 +73,12 @@ jobs:
- name: Build project
run: npm run build

- name: Run unit tests
run: npm run test:unit
- name: Run tests
run: npm test

- name: Run Node.js fallback tests
run: node test/utils/which-node-test.mjs

- name: Run integration tests
run: npm run test:integration

- name: Run mandatory-deny-paths tests (Linux)
if: matrix.os == 'linux'
run: bun test test/sandbox/mandatory-deny-paths.test.ts

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
Expand All @@ -95,3 +88,46 @@ jobs:
test-results/
*.log
if-no-files-found: ignore

docker-tests:
name: Tests (docker / ${{ matrix.arch }})
runs-on: ${{ matrix.runner }}

strategy:
fail-fast: false
matrix:
include:
- arch: x86-64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Enable unprivileged user namespaces on host
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
sudo sysctl -w kernel.unprivileged_userns_clone=1 || true

- name: Run srt end-to-end in unprivileged container
run: |
docker run --rm \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
-v "${{ github.workspace }}:/work" \
-w /work \
-e SRT_E2E_DOCKER=1 \
ubuntu:24.04 \
bash -euo pipefail -c '
apt-get update -qq
apt-get install -y -qq bubblewrap socat ripgrep python3 curl ca-certificates unzip
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt-get install -y -qq nodejs
npm ci
npm run build
bun test test/docker-weak-sandbox.test.ts
'
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,10 @@ Uses an **allow-only pattern** - all network access is denied by default.

**Unix Socket Settings** (platform-specific behavior):

| Setting | macOS | Linux |
|---------|-------|-------|
| `allowUnixSockets: string[]` | Allowlist of socket paths | *Ignored* (seccomp can't filter by path) |
| `allowAllUnixSockets: boolean` | Allow all sockets | Disable seccomp blocking |
| Setting | macOS | Linux |
| ------------------------------ | ------------------------- | ---------------------------------------- |
| `allowUnixSockets: string[]` | Allowlist of socket paths | _Ignored_ (seccomp can't filter by path) |
| `allowAllUnixSockets: boolean` | Allow all sockets | Disable seccomp blocking |

Unix sockets are **blocked by default** on both platforms.

Expand Down Expand Up @@ -478,9 +478,6 @@ npm run build:seccomp
# Run tests
npm test

# Run integration tests
npm run test:integration

# Type checking
npm run typecheck

Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
"build:seccomp": "scripts/build-seccomp-binaries.sh",
"clean": "rm -rf dist",
"test": "bun test",
"test:unit": "bun test test/config-validation.test.ts test/sandbox/seccomp-filter.test.ts",
"test:integration": "bun test test/sandbox/integration.test.ts test/sandbox/allow-read.test.ts test/sandbox/wrap-with-sandbox.test.ts",
"typecheck": "tsc --noEmit",
"lint": "eslint 'src/**/*.ts' --fix --cache --cache-location=node_modules/.cache/.eslintcache",
"lint:check": "eslint 'src/**/*.ts' --cache --cache-location=node_modules/.cache/.eslintcache",
Expand Down
236 changes: 122 additions & 114 deletions test/configurable-proxy-ports.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
import { describe, it, expect, afterAll } from 'bun:test'
import { spawnSync } from 'node:child_process'
import * as http from 'node:http'
import * as net from 'node:net'
import { SandboxManager } from '../src/sandbox/sandbox-manager.js'
import type { SandboxRuntimeConfig } from '../src/sandbox/sandbox-config.js'
import { getPlatform } from '../src/utils/platform.js'
import { isLinux } from './helpers/platform.js'

/**
* Integration tests for configurable proxy ports feature
Expand Down Expand Up @@ -291,134 +291,142 @@ describe('Configurable Proxy Ports Integration Tests', () => {
})

describe('End-to-end: External proxy actually handles requests', () => {
it('should route requests through external allow-all proxy, bypassing SRT filtering', async () => {
// Skip if not on Linux (where we have full sandbox integration)
if (getPlatform() !== 'linux') {
console.log('Skipping end-to-end test on non-Linux platform')
return
}

// Create a simple HTTP CONNECT proxy that allows ALL connections (no filtering)
let externalProxyServer: http.Server | undefined
let externalProxyPort: number | undefined

try {
externalProxyServer = http.createServer()

// Handle HTTP CONNECT method for HTTPS tunneling
externalProxyServer.on('connect', (req, clientSocket, head) => {
const { port, hostname } = new URL(`http://${req.url}`)
it.if(isLinux)(
'should route requests through external allow-all proxy, bypassing SRT filtering',
async () => {
// Create a simple HTTP CONNECT proxy that allows ALL connections (no filtering)
let externalProxyServer: http.Server | undefined
let externalProxyPort: number | undefined

try {
externalProxyServer = http.createServer()

// Handle HTTP CONNECT method for HTTPS tunneling
externalProxyServer.on('connect', (req, clientSocket, head) => {
const { port, hostname } = new URL(`http://${req.url}`)

// Connect to target (allow everything - no filtering)
const serverSocket = net.connect(
parseInt(port) || 80,
hostname,
() => {
clientSocket.write(
'HTTP/1.1 200 Connection Established\r\n\r\n',
)
serverSocket.write(head)
serverSocket.pipe(clientSocket)
clientSocket.pipe(serverSocket)
},
)

serverSocket.on('error', () => {
clientSocket.end()
})

// Connect to target (allow everything - no filtering)
const serverSocket = net.connect(parseInt(port) || 80, hostname, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
serverSocket.write(head)
serverSocket.pipe(clientSocket)
clientSocket.pipe(serverSocket)
clientSocket.on('error', () => {
serverSocket.end()
})
})

serverSocket.on('error', (err) => {
clientSocket.end()
})
// Handle regular HTTP requests
externalProxyServer.on('request', (req, res) => {
const url = new URL(req.url!)
const options = {
hostname: url.hostname,
port: url.port || 80,
path: url.pathname + url.search,
method: req.method,
headers: req.headers,
}

clientSocket.on('error', (err) => {
serverSocket.end()
})
})

// Handle regular HTTP requests
externalProxyServer.on('request', (req, res) => {
const url = new URL(req.url!)
const options = {
hostname: url.hostname,
port: url.port || 80,
path: url.pathname + url.search,
method: req.method,
headers: req.headers,
}
const proxyReq = http.request(options, proxyRes => {
res.writeHead(proxyRes.statusCode!, proxyRes.headers)
proxyRes.pipe(res)
})

const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode!, proxyRes.headers)
proxyRes.pipe(res)
})
proxyReq.on('error', () => {
res.writeHead(502)
res.end('Bad Gateway')
})

proxyReq.on('error', (err) => {
res.writeHead(502)
res.end('Bad Gateway')
req.pipe(proxyReq)
})

req.pipe(proxyReq)
})

// Start the external proxy on a random port
await new Promise<void>((resolve, reject) => {
externalProxyServer!.listen(0, '127.0.0.1', () => {
const addr = externalProxyServer!.address()
if (addr && typeof addr === 'object') {
externalProxyPort = addr.port
console.log(`External allow-all proxy started on port ${externalProxyPort}`)
resolve()
} else {
reject(new Error('Failed to get proxy address'))
}
// Start the external proxy on a random port
await new Promise<void>((resolve, reject) => {
externalProxyServer!.listen(0, '127.0.0.1', () => {
const addr = externalProxyServer!.address()
if (addr && typeof addr === 'object') {
externalProxyPort = addr.port
console.log(
`External allow-all proxy started on port ${externalProxyPort}`,
)
resolve()
} else {
reject(new Error('Failed to get proxy address'))
}
})
externalProxyServer!.on('error', reject)
})
externalProxyServer!.on('error', reject)
})

// Initialize SandboxManager with restrictive config but external proxy
const config: SandboxRuntimeConfig = {
network: {
allowedDomains: ['example.com'], // Only allow example.com
deniedDomains: [],
httpProxyPort: externalProxyPort, // Use our allow-all external proxy
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}

await SandboxManager.initialize(config)

// Verify the external proxy port is being used
expect(SandboxManager.getProxyPort()).toBe(externalProxyPort)

// Try to access example.com (in allowlist)
// This verifies that requests are routed through the external proxy
const command = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 5 http://example.com'
)

const result = spawnSync(command, {
shell: true,
encoding: 'utf8',
timeout: 10000,
})
// Initialize SandboxManager with restrictive config but external proxy
const config: SandboxRuntimeConfig = {
network: {
allowedDomains: ['example.com'], // Only allow example.com
deniedDomains: [],
httpProxyPort: externalProxyPort, // Use our allow-all external proxy
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}

// The request should succeed
expect(result.status).toBe(0)
await SandboxManager.initialize(config)

// Should NOT contain SRT's block message
const output = (result.stderr || result.stdout || '').toLowerCase()
expect(output).not.toContain('blocked by network allowlist')
// Verify the external proxy port is being used
expect(SandboxManager.getProxyPort()).toBe(externalProxyPort)

console.log('✓ Request to example.com succeeded through external proxy')
console.log('✓ This verifies SRT used the external proxy on the configured port')
// Try to access example.com (in allowlist)
// This verifies that requests are routed through the external proxy
const command = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 5 http://example.com',
)

} finally {
// Clean up
await SandboxManager.reset()
const result = spawnSync(command, {
shell: true,
encoding: 'utf8',
timeout: 10000,
})

if (externalProxyServer) {
await new Promise<void>((resolve) => {
externalProxyServer!.close(() => {
console.log('External proxy server closed')
resolve()
// The request should succeed
expect(result.status).toBe(0)

// Should NOT contain SRT's block message
const output = (result.stderr || result.stdout || '').toLowerCase()
expect(output).not.toContain('blocked by network allowlist')

console.log(
'✓ Request to example.com succeeded through external proxy',
)
console.log(
'✓ This verifies SRT used the external proxy on the configured port',
)
} finally {
// Clean up
await SandboxManager.reset()

if (externalProxyServer) {
await new Promise<void>(resolve => {
externalProxyServer!.close(() => {
console.log('External proxy server closed')
resolve()
})
})
})
}
}
}
})
},
)
})
})
Loading
Loading