A lightweight DERP-like UDP relay that connects WireGuard peers across NAT, dynamic IPs, or mixed IPv4/IPv6 — without tunnel-in-tunnel overhead and without decrypting traffic.
Single-file script, standard library only, runs on Python 3.8+.
- No encapsulation overhead: packets are forwarded at the UDP layer.
- End-to-end encrypted: the bridge never holds private keys and only sees encrypted WireGuard packets.
- NAT & dynamic IP friendly: learns each peer's current address from observed traffic.
- Dual-stack: one socket serves IPv4 and IPv6 (best-effort
IPV6_V6ONLY=0). - Peer authorization: validates handshakes via WireGuard's
mac1field against allowed public keys. - Resource guards: per-IP handshake rate limiting and a bounded peer table with oldest-first eviction.
- Optional config file: YAML (only if PyYAML is installed) with runtime reload via
SIGHUP.
Use at your own risk. Forwarded data stays secure due to WireGuard's design, but the bridge itself is a routing middleman — trust the operator and understand the threat model.
The bridge observes WireGuard handshakes to learn which 4-byte session indices belong to which public keys, then relays traffic between verified peers in the same key group.
- Handshake observation: an initiation is verified against allowed keys via
mac1. - Peer discovery: the initiation is forwarded to all other peers in the matching key group.
- Session tracking: once a response is seen, both indices are cross-referenced and transport packets are relayed directly.
- Timeout cleanup: idle and broken entries are removed periodically; the table is bounded.
- Python 3.8+ (standard library only)
- Optional:
pyyamlfor YAML config files (pip install pyyaml)
# One group of peers allowed to talk to each other
python3 wg-bridge.py --keys "PubKey1,PubKey2,PubKey3"
# Multiple isolated groups
python3 wg-bridge.py \
--keys "GroupA_Key1,GroupA_Key2" \
--keys "GroupB_Key1,GroupB_Key2"
# Custom port and rate limit
python3 wg-bridge.py --keys "PubKey1,PubKey2" --port 51821 --rate-limit 100| Flag | Description | Default |
|---|---|---|
--port, -p |
UDP listen port | 51820 |
--keys, -k |
Comma-separated public keys for a group (repeatable) | — |
--config, -c |
YAML config file (requires PyYAML); overrides CLI keys | — |
--max-peers |
Max peer table size before eviction | 10000 |
--rate-limit |
Handshake packets/sec per IP (0 disables) |
50 |
--listen-address |
Bind address (default: all interfaces) | all |
--log-level |
DEBUG/INFO/WARNING/ERROR |
INFO |
port: 51820
keys:
- ["PubKey1", "PubKey2", "PubKey3"]
- ["GroupB_Key1", "GroupB_Key2"]
max_peers: 10000
rate_limit: 50
timeout_init: 10
timeout_established: 60
log_level: INFO
# listen_address: "::"python3 wg-bridge.py --config config.yamlIf PyYAML is not installed, config-file support is unavailable and the bridge exits with a clear message; CLI --keys still work.
With --config, send SIGHUP to re-read keys/groups. Existing sessions for keys that remain configured are preserved; a failed reload keeps the previous config.
kill -HUP <pid> # direct
systemctl reload wg-bridge # systemd (ExecReload sends SIGHUP)
docker exec <container> kill -HUP 1docker build -t wg-bridge .
docker run -d --name wg-bridge \
--cap-drop ALL \
-p 51820:51820/udp \
wg-bridge --keys "PubKey1,PubKey2"The image runs as a non-root user and drops all capabilities (the default port is unprivileged, so none are required). With docker-compose use example.docker-compose.yml.
- Copy the script to
/opt/wg-bridge/wg-bridge.py(and optionally a config file). - Edit
example.wg-bridge.serviceto match your setup. - Install:
cp example.wg-bridge.service /etc/systemd/system/wg-bridge.service
systemctl daemon-reload
systemctl enable --now wg-bridgeThe unit includes minimal hardening (NoNewPrivileges, PrivateTmp, ProtectSystem=strict, ProtectHome=read-only, capability restrictions).
Point the Endpoint of all relevant peers at the bridge, and enable persistent keepalive to hold NAT mappings:
[Interface]
PrivateKey = <client_private_key>
Address = 10.0.0.2/32
MTU = 1392
[Peer]
PublicKey = <remote_peer_public_key>
Endpoint = bridge.yourserver.com:51820
PersistentKeepalive = 25
AllowedIPs = 10.0.0.3/32MTU note: paths vary (IPv4/IPv6/PPPoE/DS-Lite). An interface MTU of 1392 avoids fragmentation in most setups.
python3 -m unittest discover -s tests -vTo check compatibility across Python versions with Docker, compile-check and start the bridge (with a dummy key) to catch import/runtime errors:
for v in 3.8 3.11 3.12; do
docker run --rm -e PYTHONDONTWRITEBYTECODE=1 -v "$PWD":/w:ro -w /w python:$v-slim sh -c '
python3 -c "compile(open(\"wg-bridge.py\").read(), \"wg-bridge.py\", \"exec\")" \
&& timeout 2 python3 wg-bridge.py --keys "$(head -c32 /dev/urandom | base64)" 2>&1 \
| grep -q "listening on" && echo "'"$v"' OK" || echo "'"$v"' FAILED"
'
doneProvided "as-is". See the source header for details.