Bridge proxies
A bridge is an authenticated proxy node that lives on a VPS in uncensored space. The Gateway, on a censored LAN, can route specific domains through a bridge — and only those domains — keeping the rest of the traffic direct or on a normal VPN.
Bridges are designed to look unremarkable from the outside: the bridge-node binary serves a static decoy website on its root path, exposes its proxy and DoH endpoints under /.well-known/, and supports SNI fronting so an active probe sees the wrong hostname.
What you get
Section titled “What you get”- Per-domain routing — only the domains you assign to a bridge are tunnelled. Everything else uses your regular egress (direct or VPN).
- Pluggable transports — see the table below. TLS is the default;
obfs4and (planned)MASQUEare selectable. - Domain fronting — the SNI sent to censorship gear is a decoy domain you control; the real bridge hostname is in the inner request.
- Health-checked failover — bridges are probed on a cadence; unhealthy ones are skipped.
- Fail-open or fail-closed — set on the policy using the bridge: should censored traffic fall back to direct, or drop entirely if the bridge is down?
- Auto-spawned transparent proxy —
redsocksis brought up per bridge so unmodified apps can use it.
Transports
Section titled “Transports”The API accepts three transport values: quic (default), obfs4, masque. Today the client implementation behind each:
transport value |
What the client actually does |
|---|---|
quic (default) |
Dials plain TLS on the bridge-node’s :443. The UI labels it “QUIC/TLS”; true QUIC is on the roadmap. The bridge-node listens on TCP+TLS today, so this is the working path. |
obfs4 |
Lyrebird obfs4 wrapper — requires a non-empty obfs4_cert. Without one, the client warns and falls back to plain TLS. |
masque |
Spec selected; client logs a warning and falls back to plain TLS. Full HTTP/3 CONNECT wiring is not yet implemented for bridges. |
If you’re operating in a network where TLS-to-an-IP-on-:443 isn’t enough cover, set transport: obfs4 with a real cert. Otherwise stick with the default.
The bridge-node binary
Section titled “The bridge-node binary”cmd/bridge-node/ is a standalone binary that runs on your uncensored VPS. It exposes:
| Endpoint | Purpose |
|---|---|
/ |
Static decoy website (no auth) |
/.well-known/bridge-relay |
Authenticated HTTP CONNECT relay (the proxy itself) |
/.well-known/dns-query |
DoH endpoint for resolving destinations through the bridge |
/.well-known/bridge-health |
Liveness probe (authenticated) |
Authentication is a pre-shared key (X-Bridge-Key header) shared between the bridge-node and the gateways that use it.
Deploy with:
# on your VPSsudo /usr/local/bin/bridge-node \ --listen :443 \ --decoy-dir /var/www/decoy \ --psk /etc/bridge-node/psk \ --cert /etc/letsencrypt/live/example.invalid/fullchain.pem \ --key /etc/letsencrypt/live/example.invalid/privkey.pemRegistering a bridge
Section titled “Registering a bridge”From the Bridges page in the UI, or via API:
curl -X POST https://gateway.lan:8080/api/v1/bridges \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "bridge-eu", "address": "decoy.example.invalid:443", "psk": "REDACTED-PSK-FROM-BRIDGE-NODE", "transport": "quic", "decoy_domain": "decoy.example.invalid", "domains": ["youtube.com","*.youtube.com","*.googlevideo.com"], "priority": 10, "enabled": true, "fail_policy": "fail-closed" }'Field shape per internal/bridge/types.go:6-34: address is host:port (not a URL); psk carries the shared key value directly (the daemon encrypts it at rest with the secrets passphrase); decoy_domain is what the gateway uses as the SNI when fronting; transport ∈ {quic, obfs4, masque} (quic is default).
How packets flow
Section titled “How packets flow” Device → gateway → redsocks (local SOCKS5) │ TLS to bridge-node:443 │ (SNI = decoy.example.invalid) ▼ bridge-node on VPS │ ▼ real destination (youtube.com, …)The redirect to redsocks is per-domain via iptables REDIRECT. Only the domains assigned to the bridge are caught; the rest exit normally.
TPROXY (advanced)
Section titled “TPROXY (advanced)”On hosts with the nf_tproxy_ipv4 kernel module the daemon can use transparent proxy instead of the NAT-based REDIRECT. The difference matters when downstream services need the original source IP — REDIRECT overwrites it with the gateway’s loopback, TPROXY preserves it. Check capability with:
curl https://gateway.lan:8080/api/v1/tproxy/status \ -H "Authorization: Bearer $API_KEY"# → {"available":true,"kernel_module":true,"table_100":true,"mode":"redirect", ...}available: false means the kernel module isn’t loaded — pull it in via apt install linux-modules-extra-$(uname -r) (Debian/Ubuntu) or rebuild your kernel with CONFIG_NF_TPROXY_IPV4=m. The default mode is redirect; the daemon’s bridge handler will fall back to REDIRECT regardless on hosts without TPROXY support.
Domain fronting
Section titled “Domain fronting”When SNI fronting is enabled, the connection goes to decoy.example.invalid (a real hostname with a real certificate), and the inner HTTP request says Host: bridge.example.invalid. To a censor inspecting SNI only, the connection looks identical to ordinary traffic to the decoy site.
This requires the decoy and the bridge to share an IP and certificate — typical setups use a single VPS that serves both.
Fail behaviour
Section titled “Fail behaviour”A bridge’s fail behaviour is set on the policy that routes traffic into it, not on the bridge itself:
fail_policy on the policy |
Behaviour when bridge is down |
|---|---|
open |
Domains assigned to this bridge fall back to your normal egress. |
closed |
Domains assigned to this bridge are blocked. No fallback. No leakage. |
Pick fail_policy: fail-closed for high-sensitivity destinations. See Routing policies.
API surface
Section titled “API surface”| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/bridges |
List bridges |
GET |
/api/v1/bridges/status |
Per-bridge health snapshot |
POST |
/api/v1/bridges |
Register a bridge |
GET |
/api/v1/bridges/{id} |
Single bridge detail |
PUT |
/api/v1/bridges/{id} |
Update endpoint / domains / transport |
POST |
/api/v1/bridges/{id}/toggle |
Enable / disable |
POST |
/api/v1/bridges/{id}/test |
One-shot health probe |
DELETE |
/api/v1/bridges/{id} |
Remove |
Next steps
Section titled “Next steps”After the first bridge is healthy:
- Bind it to traffic. A bridge sits idle without a
dest_domainpolicy. See Routing policies. - Decide
fail_policyper workload. Some domains tolerate falling back to direct egress when the bridge is down; some absolutely don’t. Choose deliberately per policy, not globally. - Pair with the domain-fronting decoy. A bridge whose SNI matches a real, popular hostname is much harder to fingerprint than a bridge with a unique cert.
- Check TPROXY status if your downstream services need original source IPs preserved through the SOCKS5 hop.
Related
Section titled “Related”- Routing policies —
action: bridgeplusdest_domainis what actually sends traffic into a bridge. - DNS plane — domain matching needs the resolver to answer first.
- Use case: anti-censorship family router — a worked example with a bridge for YouTube + Telegram.