Skip to content

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.

  • 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; obfs4 and (planned) MASQUE are 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 proxyredsocks is brought up per bridge so unmodified apps can use it.

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.

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:

Terminal window
# on your VPS
sudo /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.pem

From the Bridges page in the UI, or via API:

Terminal window
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).

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.

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:

Terminal window
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.

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.

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.

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

After the first bridge is healthy:

  1. Bind it to traffic. A bridge sits idle without a dest_domain policy. See Routing policies.
  2. Decide fail_policy per workload. Some domains tolerate falling back to direct egress when the bridge is down; some absolutely don’t. Choose deliberately per policy, not globally.
  3. 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.
  4. Check TPROXY status if your downstream services need original source IPs preserved through the SOCKS5 hop.