Skip to content

Repository files navigation

🌐 WireGuard Bridge

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


Key Features

  • 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 mac1 field 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.

⚠️ Disclaimer

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.


How it Works

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.

  1. Handshake observation: an initiation is verified against allowed keys via mac1.
  2. Peer discovery: the initiation is forwarded to all other peers in the matching key group.
  3. Session tracking: once a response is seen, both indices are cross-referenced and transport packets are relayed directly.
  4. Timeout cleanup: idle and broken entries are removed periodically; the table is bounded.

Getting Started

Requirements

  • Python 3.8+ (standard library only)
  • Optional: pyyaml for YAML config files (pip install pyyaml)

Usage

# 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

Parameters

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

Config File (optional, YAML only)

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

If PyYAML is not installed, config-file support is unavailable and the bridge exits with a clear message; CLI --keys still work.

Runtime reload (SIGHUP)

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 1

Deployment

Docker

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

Systemd

  1. Copy the script to /opt/wg-bridge/wg-bridge.py (and optionally a config file).
  2. Edit example.wg-bridge.service to match your setup.
  3. Install:
cp example.wg-bridge.service /etc/systemd/system/wg-bridge.service
systemctl daemon-reload
systemctl enable --now wg-bridge

The unit includes minimal hardening (NoNewPrivileges, PrivateTmp, ProtectSystem=strict, ProtectHome=read-only, capability restrictions).


WireGuard Client Configuration

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

MTU note: paths vary (IPv4/IPv6/PPPoE/DS-Lite). An interface MTU of 1392 avoids fragmentation in most setups.


Testing

python3 -m unittest discover -s tests -v

To 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"
  '
done

License

Provided "as-is". See the source header for details.

About

Lightweight WireGuard UDP Relay (DERP-like) for NAT traversal and dynamic IP connectivity. No tunnel overhead. No traffic decryption.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Contributors

Languages