Skip to content

Commit dc26200

Browse files
committed
Add egress header template documentation (#142)
Document the sandbox-egress-header template that uses mitmproxy to inject X-Sandbox-ID headers into all outbound HTTP/HTTPS requests.
1 parent 22502e5 commit dc26200

2 files changed

Lines changed: 307 additions & 1 deletion

File tree

docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@
148148
"docs/template/examples/expo",
149149
"docs/template/examples/desktop",
150150
"docs/template/examples/claude-code",
151-
"docs/template/examples/docker"
151+
"docs/template/examples/docker",
152+
"docs/template/examples/egress-header"
152153
]
153154
},
154155
"docs/template/migration-v2"
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
---
2+
title: "Egress header"
3+
description: "Sandbox with automatic HTTP header injection on outbound requests"
4+
---
5+
6+
This template runs a transparent [mitmproxy](https://mitmproxy.org/) inside the sandbox that automatically injects a custom `X-Sandbox-ID` header into every outbound HTTP and HTTPS request. This is useful for tracking, auditing, or routing requests that originate from specific sandboxes.
7+
8+
The template includes:
9+
- **[mitmproxy](https://mitmproxy.org/)** running in transparent mode to intercept outbound traffic
10+
- **iptables** rules that redirect HTTP/HTTPS traffic through the proxy
11+
- **Automatic CA certificate trust** so HTTPS requests work without certificate errors in Python, Node.js, and OpenSSL
12+
- **Socket marking** via an `LD_PRELOAD` library to prevent proxy redirect loops
13+
14+
## Template definition
15+
16+
The template installs mitmproxy with its dependencies, copies the proxy addon and startup scripts, and compiles a small C library used to prevent redirect loops. The start command launches the proxy and waits for it to signal readiness.
17+
18+
<CodeGroup>
19+
20+
```typescript JavaScript & TypeScript expandable
21+
// template.ts
22+
import { Template, waitForFile } from 'e2b'
23+
24+
export const template = Template()
25+
.fromBaseImage()
26+
.aptInstall([
27+
'python3',
28+
'python3-pip',
29+
'iptables',
30+
'ca-certificates',
31+
'curl',
32+
'gcc',
33+
'libc-dev',
34+
])
35+
.runCmd('pip3 install mitmproxy --break-system-packages', { user: 'root' })
36+
.runCmd('mkdir -p /opt/mitmproxy', { user: 'root' })
37+
.copy('files/mitmproxy/add_header.py', '/opt/mitmproxy/add_header.py')
38+
.copy('files/mitmproxy/sockmark.c', '/opt/mitmproxy/sockmark.c')
39+
.copy('files/scripts/start-proxy.sh', '/opt/mitmproxy/start-proxy.sh')
40+
.runCmd('gcc -shared -fPIC -o /opt/mitmproxy/sockmark.so /opt/mitmproxy/sockmark.c -ldl', { user: 'root' })
41+
.runCmd('chmod +x /opt/mitmproxy/start-proxy.sh', { user: 'root' })
42+
.setStartCmd('sudo /opt/mitmproxy/start-proxy.sh', waitForFile('/tmp/proxy-ready'))
43+
```
44+
45+
```python Python expandable
46+
# template.py
47+
from e2b import Template, wait_for_file
48+
49+
template = (
50+
Template()
51+
.from_base_image()
52+
.apt_install([
53+
"python3",
54+
"python3-pip",
55+
"iptables",
56+
"ca-certificates",
57+
"curl",
58+
"gcc",
59+
"libc-dev",
60+
])
61+
.run_cmd("pip3 install mitmproxy --break-system-packages", user="root")
62+
.run_cmd("mkdir -p /opt/mitmproxy", user="root")
63+
.copy("files/mitmproxy/add_header.py", "/opt/mitmproxy/add_header.py")
64+
.copy("files/mitmproxy/sockmark.c", "/opt/mitmproxy/sockmark.c")
65+
.copy("files/scripts/start-proxy.sh", "/opt/mitmproxy/start-proxy.sh")
66+
.run_cmd("gcc -shared -fPIC -o /opt/mitmproxy/sockmark.so /opt/mitmproxy/sockmark.c -ldl", user="root")
67+
.run_cmd("chmod +x /opt/mitmproxy/start-proxy.sh", user="root")
68+
.set_start_cmd("sudo /opt/mitmproxy/start-proxy.sh", wait_for_file("/tmp/proxy-ready"))
69+
)
70+
```
71+
72+
</CodeGroup>
73+
74+
## Proxy addon
75+
76+
The mitmproxy addon reads the sandbox ID from `/run/e2b/.E2B_SANDBOX_ID` (written by E2B at sandbox boot) and injects it as a header into every outbound request. The header name defaults to `X-Sandbox-ID` but can be configured via the `HEADER_NAME` environment variable.
77+
78+
```python add_header.py
79+
import os
80+
from mitmproxy import http
81+
82+
HEADER_NAME = os.environ.get("HEADER_NAME", "X-Sandbox-ID")
83+
SANDBOX_ID_FILE = "/run/e2b/.E2B_SANDBOX_ID"
84+
_sandbox_id: str | None = None
85+
86+
87+
def request(flow: http.HTTPFlow) -> None:
88+
global _sandbox_id
89+
90+
if _sandbox_id is not None:
91+
flow.request.headers[HEADER_NAME] = _sandbox_id
92+
return
93+
94+
# Try reading the sandbox ID file (written by envd after sandbox boot).
95+
sid = ""
96+
try:
97+
with open(SANDBOX_ID_FILE) as f:
98+
sid = f.read().strip()
99+
except Exception:
100+
pass
101+
102+
# Fall back to E2B_SANDBOX_ID env var.
103+
if not sid or sid == "unknown":
104+
sid = os.environ.get("E2B_SANDBOX_ID", "").strip()
105+
106+
# Cache only once we have a real value; otherwise retry next request.
107+
if sid and sid != "unknown":
108+
_sandbox_id = sid
109+
else:
110+
sid = "unknown"
111+
112+
flow.request.headers[HEADER_NAME] = sid
113+
```
114+
115+
## Socket marking library
116+
117+
To prevent redirect loops, mitmproxy's own outbound connections need to bypass the iptables redirect rules. This small C library is loaded via `LD_PRELOAD` and marks all sockets mitmproxy creates with `SO_MARK=1`. The iptables rules then skip any packets with that mark.
118+
119+
```c sockmark.c
120+
#define _GNU_SOURCE
121+
#include <sys/socket.h>
122+
#include <dlfcn.h>
123+
124+
int socket(int domain, int type, int protocol) {
125+
int (*real_socket)(int, int, int) = dlsym(RTLD_NEXT, "socket");
126+
int fd = real_socket(domain, type, protocol);
127+
if (fd >= 0 && (domain == AF_INET || domain == AF_INET6)) {
128+
int mark = 1;
129+
setsockopt(fd, SOL_SOCKET, SO_MARK, &mark, sizeof(mark));
130+
}
131+
return fd;
132+
}
133+
```
134+
135+
## Startup script
136+
137+
The startup script runs when the sandbox starts. It fetches the sandbox ID from the instance metadata service, generates and installs the mitmproxy CA certificate, configures iptables to redirect HTTP (port 80) and HTTPS (port 443) traffic through the proxy, and launches mitmproxy in transparent mode.
138+
139+
```bash start-proxy.sh expandable
140+
#!/usr/bin/env bash
141+
set -euo pipefail
142+
143+
PROXY_PORT="${PROXY_PORT:-8080}"
144+
ADDON_PATH="/opt/mitmproxy/add_header.py"
145+
SOCKMARK_LIB="/opt/mitmproxy/sockmark.so"
146+
CONFDIR="/root/.mitmproxy"
147+
148+
echo "=== Egress Header Proxy Setup ==="
149+
echo "Proxy port: ${PROXY_PORT}"
150+
151+
# --- 0. Resolve sandbox ID from MMDS (Firecracker metadata service) ---
152+
MMDS_TOKEN=$(curl -sf -X PUT "http://169.254.169.254/latest/api/token" \
153+
-H "X-metadata-token-ttl-seconds: 21600" 2>/dev/null || echo "")
154+
if [ -n "${MMDS_TOKEN}" ]; then
155+
SANDBOX_ID=$(curl -sf -H "X-metadata-token: ${MMDS_TOKEN}" \
156+
"http://169.254.169.254/instanceID" 2>/dev/null || echo "unknown")
157+
else
158+
SANDBOX_ID="${E2B_SANDBOX_ID:-unknown}"
159+
fi
160+
echo "Sandbox ID: ${SANDBOX_ID}"
161+
162+
# --- 1. Generate mitmproxy CA certificate ---
163+
echo "Generating mitmproxy CA certificate..."
164+
mitmdump --mode transparent --listen-port "${PROXY_PORT}" \
165+
--set confdir="${CONFDIR}" \
166+
-s "${ADDON_PATH}" &
167+
GEN_PID=$!
168+
169+
for i in $(seq 1 30); do
170+
[ -f "${CONFDIR}/mitmproxy-ca-cert.pem" ] && break
171+
sleep 0.5
172+
done
173+
174+
kill "${GEN_PID}" 2>/dev/null || true
175+
wait "${GEN_PID}" 2>/dev/null || true
176+
177+
# --- 2. Install CA certificate system-wide ---
178+
echo "Installing CA certificate..."
179+
cp "${CONFDIR}/mitmproxy-ca-cert.pem" /usr/local/share/ca-certificates/mitmproxy.crt
180+
update-ca-certificates 2>/dev/null
181+
182+
# Set CA env vars for Python, Node.js, and OpenSSL
183+
cat > /etc/profile.d/mitmproxy-ca.sh << 'ENVEOF'
184+
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
185+
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
186+
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
187+
ENVEOF
188+
189+
cat >> /etc/environment << 'ENVEOF'
190+
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
191+
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
192+
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
193+
ENVEOF
194+
195+
# --- 3. Set up iptables rules ---
196+
# Skip packets marked with 1 (mitmproxy's own outbound traffic)
197+
echo "Configuring iptables..."
198+
iptables -t nat -A OUTPUT -m mark --mark 1 -j RETURN
199+
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN
200+
iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port "${PROXY_PORT}"
201+
iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port "${PROXY_PORT}"
202+
203+
# --- 4. Start mitmproxy with LD_PRELOAD for loop prevention ---
204+
echo "Starting mitmproxy in transparent mode..."
205+
LD_PRELOAD="${SOCKMARK_LIB}" \
206+
E2B_SANDBOX_ID="${SANDBOX_ID}" \
207+
HEADER_NAME="${HEADER_NAME:-X-Sandbox-ID}" \
208+
mitmdump --mode transparent --listen-port "${PROXY_PORT}" \
209+
-s "${ADDON_PATH}" --set confdir="${CONFDIR}" \
210+
--quiet &
211+
MITM_PID=$!
212+
213+
# --- 5. Wait for proxy to be ready ---
214+
sleep 2
215+
if kill -0 "${MITM_PID}" 2>/dev/null; then
216+
echo "Proxy is ready (PID: ${MITM_PID})"
217+
else
218+
echo "WARNING: mitmproxy may not have started correctly"
219+
fi
220+
touch /tmp/proxy-ready
221+
222+
echo "=== Egress Header Proxy Running ==="
223+
wait "${MITM_PID}"
224+
```
225+
226+
## Build the template
227+
228+
Build the template with 2 CPUs and 2 GB of RAM. The build compiles the C socket marking library and installs mitmproxy, which may take a few minutes.
229+
230+
<CodeGroup>
231+
232+
```typescript JavaScript & TypeScript
233+
// build.ts
234+
import { Template, defaultBuildLogger } from 'e2b'
235+
import { template as egressTemplate } from './template'
236+
237+
await Template.build(egressTemplate, 'sandbox-egress-header', {
238+
cpuCount: 2,
239+
memoryMB: 2048,
240+
onBuildLogs: defaultBuildLogger(),
241+
})
242+
```
243+
244+
```python Python
245+
# build.py
246+
from e2b import Template, default_build_logger
247+
from .template import template as egressTemplate
248+
249+
Template.build(egressTemplate, "sandbox-egress-header",
250+
cpu_count=2,
251+
memory_mb=2048,
252+
on_build_logs=default_build_logger(),
253+
)
254+
```
255+
256+
</CodeGroup>
257+
258+
## Verify the header injection
259+
260+
Create a sandbox from the template and make a request to [httpbin.org](https://httpbin.org) to confirm the `X-Sandbox-ID` header is being injected into outbound requests.
261+
262+
<CodeGroup>
263+
264+
```typescript JavaScript & TypeScript
265+
// sandbox.ts
266+
import { Sandbox } from 'e2b'
267+
268+
const sbx = await Sandbox.create('sandbox-egress-header', { timeoutMs: 120_000 })
269+
270+
const result = await sbx.commands.run('curl -s https://httpbin.org/headers', {
271+
envs: { SSL_CERT_FILE: '/etc/ssl/certs/ca-certificates.crt' },
272+
})
273+
console.log(result.stdout)
274+
275+
await sbx.kill()
276+
```
277+
278+
```python Python
279+
# sandbox.py
280+
from e2b import Sandbox
281+
282+
sbx = Sandbox.create("sandbox-egress-header", timeout=120)
283+
284+
result = sbx.commands.run(
285+
"curl -s https://httpbin.org/headers",
286+
envs={"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt"},
287+
)
288+
print(result.stdout)
289+
290+
sbx.kill()
291+
```
292+
293+
</CodeGroup>
294+
295+
The output includes the injected header with the sandbox's unique ID:
296+
297+
```json
298+
{
299+
"headers": {
300+
"Accept": "*/*",
301+
"Host": "httpbin.org",
302+
"X-Sandbox-Id": "sandbox-id-here"
303+
}
304+
}
305+
```

0 commit comments

Comments
 (0)