Transparent TCP Proxy with sing-box#
Route every outbound TCP byte the container emits — any protocol, any port — through a sing-box instance that you control, which in turn forwards to your real upstream proxy (SOCKS5, VMess, Shadowsocks, Trojan, …).
Scope: TCP only#
network.proxy only proxies TCP. UDP traffic (DNS, QUIC, anything else) goes out via the container’s normal network path, not through sing-box. The why is non-obvious — see Transparent Proxy in the network docs and AGD-037 for the full story. Short version: there is currently no working path for transparent UDP proxying of container traffic on Linux, and making the recipe pretend otherwise would just mislead you.
Practical implications:
- DNS: point the container at a DNS-over-TCP resolver if you want DNS through the proxy.
/etc/resolv.confentries likenameserver 1.1.1.1use UDP by default — pair with a local DoT/DoH resolver, or useresolv.confoptions that force TCP (e.g.,options use-vc). - HTTP/3 (QUIC): disable in the client (browsers have a flag) so it falls back to HTTPS over TCP.
- App-specific UDP proxying: if one particular app must go through the proxy over UDP, configure that app to speak SOCKS5 UDP ASSOCIATE to sing-box directly — no transparent redirection.
What you get#
container → alca DNAT (TCP only) → sing-box (redirect inbound) → YOUR upstream proxy- All outbound TCP is intercepted — git+ssh, database clients, plain HTTP, anything.
- sing-box starts/stops alongside the sandbox via
hooks.post_up/hooks.pre_down. - Same config on Linux and macOS.
Prerequisites#
alca upalready works for your project (see Quickstart).- Docker (or OrbStack) — we run sing-box as a sidecar container so it sits in the same network namespace as Alcatraz’s nftables DNAT rules. sing-box’s
redirectinbound recovers the original destination viaSO_ORIGINAL_DST, agetsockoptcall that queries conntrack; conntrack only has that information if the proxy is in the same namespace as the DNAT rules.--network hostsatisfies that on both platforms (on macOS, “host” means the container-runtime VM, which is also where alcatraz’s nftables live).
Step 1 — sing-box config#
Create sing-box.json next to your .alca.toml. Replace YOUR_UPSTREAM_HOST / credentials with your real upstream.
{
"log": { "level": "info" },
"inbounds": [
{
"type": "redirect",
"tag": "tcp-in",
"listen": "0.0.0.0",
"listen_port": 1080
}
],
"outbounds": [
{
"type": "socks",
"tag": "upstream",
"server": "YOUR_UPSTREAM_HOST",
"server_port": 1080,
"version": "5"
},
{ "type": "direct", "tag": "direct" }
],
"route": {
"final": "upstream"
}
}- The single
redirectinbound handles TCP; alcatraz does not redirect UDP so there is notproxyinbound here. - Swap the
upstreamoutbound for whatever protocol your upstream speaks — sing-box has dozens. If your upstream requires authentication, put the credentials in the outbound — the container never sees them.
Step 2 — Alcatraz config#
image = "alpine:3.21"
[network]
proxy = "${alca:HOST_IP}:1080"
[hooks]
post_up = """
docker rm -f alca-singbox >/dev/null 2>&1 || true
docker run -d --name alca-singbox \\
--network host \\
--restart unless-stopped \\
-v "$PWD/sing-box.json:/etc/sing-box/config.json:ro" \\
ghcr.io/sagernet/sing-box:latest \\
run -c /etc/sing-box/config.json
"""
pre_down = "docker rm -f alca-singbox >/dev/null 2>&1 || true"Notes:
proxy = "${alca:HOST_IP}:1080"— the${alca:HOST_IP}token expands to the bridge gateway IP at runtime, so the same config works on Docker, OrbStack, and Podman without hardcoding. See network.proxy.- No
lan-accessentry needed — Alcatraz automatically punches a hole for the proxy address (see Transparent Proxy). --network hostputs sing-box in the same network namespace as the nftables DNAT rules — required forSO_ORIGINAL_DSTto return the pre-DNAT destination.- The
post_uphookrm -fs any stale container first — handy whenCtrl+Con a previousalca upskippedpre_down. alca statusreports hook changes as drift, but alcatraz does not watchsing-box.json. If you edit it, restart the sidecar yourself (docker restart alca-singbox).
Step 3 — verify#
alca up
alca run curl -sS https://ifconfig.me # should return the upstream's exit IP
alca down # stops the sandbox AND sing-boxCheck docker logs alca-singbox if something looks off — sing-box logs each inbound connection along with the original destination it recovered.
Pinning the sing-box version#
ghcr.io/sagernet/sing-box:latest is fine for tinkering. For anything stable, pin to a released tag (e.g., ghcr.io/sagernet/sing-box:v1.11.0) so your sandbox doesn’t drift when the latest tag moves.