|
| 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