360 lines
10 KiB
TypeScript
360 lines
10 KiB
TypeScript
import { assert, expect, describe } from 'vitest'
|
|
|
|
import { CommandExitError } from '../../src'
|
|
import { sandboxTest, isDebug } from '../setup.js'
|
|
|
|
describe('allow only 1.1.1.1', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
denyOut: ({ allTraffic }) => [allTraffic],
|
|
allowOut: ['1.1.1.1'],
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'allow specific IP with deny all traffic',
|
|
async ({ sandbox }) => {
|
|
// Test that allowed IP works
|
|
const result = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
|
)
|
|
assert.equal(result.exitCode, 0)
|
|
assert.equal(result.stdout.trim(), '301')
|
|
|
|
// Test that other IPs are denied
|
|
await expect(
|
|
sandbox.commands.run(
|
|
'curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8'
|
|
)
|
|
).rejects.toBeInstanceOf(CommandExitError)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('deny specific IP address', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
denyOut: ['8.8.8.8'],
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'deny specific IP address',
|
|
async ({ sandbox }) => {
|
|
// Test that denied IP fails
|
|
await expect(
|
|
sandbox.commands.run(
|
|
'curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8'
|
|
)
|
|
).rejects.toBeInstanceOf(CommandExitError)
|
|
|
|
// Test that other IPs work
|
|
const result = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
|
)
|
|
assert.equal(result.exitCode, 0)
|
|
assert.equal(result.stdout.trim(), '301')
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('deny all traffic using allTraffic selector', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
denyOut: ({ allTraffic }) => [allTraffic],
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'deny all traffic using allTraffic selector',
|
|
async ({ sandbox }) => {
|
|
// Test that all traffic is denied
|
|
await expect(
|
|
sandbox.commands.run(
|
|
'curl --connect-timeout 3 --max-time 5 -Is https://1.1.1.1'
|
|
)
|
|
).rejects.toBeInstanceOf(CommandExitError)
|
|
|
|
await expect(
|
|
sandbox.commands.run(
|
|
'curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8'
|
|
)
|
|
).rejects.toBeInstanceOf(CommandExitError)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('allow takes precedence over deny', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
denyOut: ({ allTraffic }) => [allTraffic],
|
|
allowOut: ['1.1.1.1', '8.8.8.8'],
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'allow takes precedence over deny',
|
|
async ({ sandbox }) => {
|
|
// Test that 1.1.1.1 works (explicitly allowed)
|
|
const result1 = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
|
)
|
|
assert.equal(result1.exitCode, 0)
|
|
assert.equal(result1.stdout.trim(), '301')
|
|
|
|
// Test that 8.8.8.8 also works (explicitly allowed, takes precedence over denyOut)
|
|
const result2 = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://8.8.8.8"
|
|
)
|
|
assert.equal(result2.exitCode, 0)
|
|
assert.equal(result2.stdout.trim(), '302')
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('allowPublicTraffic=false', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
allowPublicTraffic: false,
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'sandbox requires traffic access token',
|
|
async ({ sandbox }) => {
|
|
// Verify the sandbox was created successfully and has a traffic access token
|
|
assert(sandbox.trafficAccessToken)
|
|
|
|
// Start a simple HTTP server in the sandbox
|
|
const port = 8080
|
|
sandbox.commands.run(`python3 -m http.server ${port}`, {
|
|
background: true,
|
|
})
|
|
|
|
// Wait for server to start
|
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
|
|
// Get the public URL for the sandbox
|
|
const sandboxUrl = `https://${sandbox.getHost(port)}`
|
|
|
|
// Test 1: Request without traffic access token should fail with 403
|
|
const response1 = await fetch(sandboxUrl)
|
|
assert.equal(response1.status, 403)
|
|
|
|
// Test 2: Request with valid traffic access token should succeed
|
|
const response2 = await fetch(sandboxUrl, {
|
|
headers: {
|
|
'e2b-traffic-access-token': sandbox.trafficAccessToken,
|
|
},
|
|
})
|
|
assert.equal(response2.status, 200)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('allowPublicTraffic=true', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
allowPublicTraffic: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'sandbox works without token',
|
|
async ({ sandbox }) => {
|
|
// Start a simple HTTP server in the sandbox
|
|
const port = 8080
|
|
sandbox.commands.run(`python3 -m http.server ${port}`, {
|
|
background: true,
|
|
})
|
|
|
|
// Wait for server to start
|
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
|
|
// Get the public URL for the sandbox
|
|
const sandboxUrl = `https://${sandbox.getHost(port)}`
|
|
|
|
// Request without traffic access token should succeed (public access enabled)
|
|
const response = await fetch(sandboxUrl)
|
|
assert.equal(response.status, 200)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('firewall transform injects headers', () => {
|
|
const injectedHeader = 'X-E2B-Test-Token'
|
|
const injectedValue = 'e2b-transform-value-123'
|
|
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
rules: {
|
|
'httpbin.e2b.team': [
|
|
{
|
|
transform: {
|
|
headers: {
|
|
[injectedHeader]: injectedValue,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'injected header is reflected by httpbin.e2b.team/headers',
|
|
async ({ sandbox }) => {
|
|
const result = await sandbox.commands.run(
|
|
'curl -sS --max-time 10 https://httpbin.e2b.team/headers'
|
|
)
|
|
assert.equal(result.exitCode, 0)
|
|
|
|
const parsed = JSON.parse(result.stdout) as {
|
|
headers: Record<string, string>
|
|
}
|
|
const reflected = parsed.headers[injectedHeader]
|
|
assert.equal(
|
|
reflected,
|
|
injectedValue,
|
|
`expected httpbin to reflect ${injectedHeader}=${injectedValue}, got headers: ${JSON.stringify(parsed.headers)}`
|
|
)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('updateNetwork applies new egress rules', () => {
|
|
sandboxTest.skipIf(isDebug)(
|
|
'denies a previously reachable IP after update',
|
|
async ({ sandbox }) => {
|
|
// Baseline: 8.8.8.8 is reachable.
|
|
const before = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://8.8.8.8"
|
|
)
|
|
assert.equal(before.exitCode, 0)
|
|
|
|
await sandbox.updateNetwork({ denyOut: ['8.8.8.8'] })
|
|
|
|
// 8.8.8.8 should now be denied.
|
|
await expect(
|
|
sandbox.commands.run(
|
|
'curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8'
|
|
)
|
|
).rejects.toBeInstanceOf(CommandExitError)
|
|
|
|
// Other destinations stay reachable.
|
|
const after = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
|
)
|
|
assert.equal(after.exitCode, 0)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('updateNetwork clears existing rules when fields are omitted', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
denyOut: ({ allTraffic }) => [allTraffic],
|
|
allowOut: ['1.1.1.1'],
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'omitting fields replaces all egress rules',
|
|
async ({ sandbox }) => {
|
|
// Baseline from create-time config: 8.8.8.8 denied.
|
|
await expect(
|
|
sandbox.commands.run(
|
|
'curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8'
|
|
)
|
|
).rejects.toBeInstanceOf(CommandExitError)
|
|
|
|
// Empty update clears allow_out / deny_out entirely.
|
|
await sandbox.updateNetwork({})
|
|
|
|
const r1 = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
|
)
|
|
assert.equal(r1.exitCode, 0)
|
|
|
|
const r2 = await sandbox.commands.run(
|
|
"curl -s -o /dev/null -w '%{http_code}' https://8.8.8.8"
|
|
)
|
|
assert.equal(r2.exitCode, 0)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('maskRequestHost option', () => {
|
|
sandboxTest.scoped({
|
|
sandboxOpts: {
|
|
network: {
|
|
maskRequestHost: 'custom-host.example.com:${PORT}',
|
|
},
|
|
},
|
|
})
|
|
|
|
sandboxTest.skipIf(isDebug)(
|
|
'verify maskRequestHost modifies Host header correctly',
|
|
async ({ sandbox }) => {
|
|
const port = 8080
|
|
const outputFile = '/tmp/headers.txt'
|
|
|
|
// Start a Python HTTP server that captures request headers and writes them to a file
|
|
sandbox.commands.run(
|
|
`python3 -c "
|
|
import http.server
|
|
class H(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
with open('${outputFile}', 'w') as f:
|
|
for k, v in self.headers.items():
|
|
f.write(k + ': ' + v + chr(10))
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
def log_message(self, *a): pass
|
|
http.server.HTTPServer(('', ${port}), H).handle_request()
|
|
"`,
|
|
{ background: true }
|
|
)
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
|
|
// Get the public URL for the sandbox
|
|
const sandboxUrl = `https://${sandbox.getHost(port)}`
|
|
|
|
// Make a request from OUTSIDE the sandbox through the proxy
|
|
// The Host header should be modified according to maskRequestHost
|
|
try {
|
|
await fetch(sandboxUrl, { signal: AbortSignal.timeout(5000) })
|
|
} catch (error) {
|
|
// Request may timeout, but headers are captured by the server
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
|
|
// Read the captured headers from inside the sandbox
|
|
const result = await sandbox.commands.run(`cat ${outputFile}`)
|
|
|
|
// Verify the Host header was modified according to maskRequestHost
|
|
assert.include(result.stdout, 'Host:')
|
|
assert.include(result.stdout, 'custom-host.example.com')
|
|
assert.include(result.stdout, `${port}`)
|
|
}
|
|
)
|
|
})
|