Skip to main content

rawops.dev

P2

WireGuard Peer Unreachable — Tunnel Debugging

Fix a WireGuard tunnel where peers can't reach each other. Covers handshake failure, AllowedIPs + routing, UDP firewalling, IP forwarding, and NAT/masquerade for site-to-site setups.

20 min7 steps
Progress: 0/7 steps
0%

`wg show` answers 80% of WireGuard questions in one call.

wg show && echo '---' && ip -brief addr show wg0 && echo '---' && ip -brief link show wg0
Expected: `latest handshake: X seconds ago` for the peer. WireGuard is idle-stateless — handshakes occur only on traffic. No handshake = tunnel never established.
If `wg show` returns nothing at all, the interface isn't up. Start it: `systemctl start wg-quick@wg0` or `wg-quick up wg0`.

Each side must list the OTHER side's public key. A swapped-key config silently produces zero handshakes forever.

# On peer A:
wg show wg0 peers
grep -E 'PublicKey|Endpoint|AllowedIPs' /etc/wireguard/wg0.conf

# On peer B:
wg show wg0 peers
grep -E 'PublicKey|Endpoint|AllowedIPs' /etc/wireguard/wg0.conf

# Derive A's public key to compare:
wg pubkey < /etc/wireguard/privatekey
Expected: A's `[Peer] PublicKey` must match B's own derived public key, and vice versa. `Endpoint` is the reachable IP:port of the other side.

WireGuard is UDP-only; many firewalls silently drop UDP.

# Listen port:
grep -i listenport /etc/wireguard/wg0.conf || echo 51820

# From the OTHER peer, probe UDP:
nc -u -v -w 3 <peer-endpoint> <port> < /dev/null

# On the receiving side, confirm the kernel is actually bound:
ss -lunp | grep -E ':(51820|<your-port>)'

# Quick tcpdump on the expected interface:
tcpdump -n -i any udp port 51820 -c 10
Expected: tcpdump sees inbound UDP from the peer's public IP. If nothing arrives, it's blocked upstream (NAT / ISP / security group). If inbound arrives but no reply, the firewall on this side is dropping OUTPUT.

On the receiving side, a packet with source-IP not in AllowedIPs is dropped. On the sending side, AllowedIPs acts as the destination route.

# Per-peer AllowedIPs table:
wg show wg0 allowed-ips

# Kernel routes to the tunnel:
ip route show | grep wg0
Expected: Full-tunnel client: `AllowedIPs = 0.0.0.0/0` points a default route at wg0. Site-to-site: each peer's AllowedIPs must cover the subnets that peer serves. Asymmetry = one-way traffic.

If WireGuard is a gateway to a LAN / the internet, the kernel must forward and NAT packets.

sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding

# iptables/nftables:
iptables -t nat -S POSTROUTING | grep MASQUERADE
# or:
nft list table inet nat 2>/dev/null | grep -i masquerade
Expected: `net.ipv4.ip_forward = 1` and a MASQUERADE rule on the outbound interface. Zero forwarding = packets never leave the box. Enable: `sysctl -w net.ipv4.ip_forward=1` + make persistent in `/etc/sysctl.d/`.

Once handshake succeeds and routing is right, verify real traffic flows.

# From peer A to peer B's tunnel IP:
ping -c 3 -W 2 <peer-B-tunnel-ip>

# From peer A to an endpoint behind peer B:
curl -sS --max-time 5 http://<endpoint-behind-B>/

# Watch both sides of the tunnel simultaneously:
watch -n 1 'wg show wg0 transfer'
Expected: Ping reply + `transfer` counters increasing on BOTH peers. Counters moving only one way usually means the reply path is firewalled.

A mismatched PresharedKey, corrupted private key, or reused IP on the tunnel subnet can leave tunnels stuck forever. Fresh keys is the nuclear option.

# Generate new keys:
umask 077
wg genkey | tee /etc/wireguard/new.key | wg pubkey > /etc/wireguard/new.pub

# Replace PrivateKey in wg0.conf, update the OTHER peer's [Peer] PublicKey to match new.pub, restart:
systemctl restart wg-quick@wg0
Expected: Handshake completes within a few seconds of the first packet. `wg show` shows a fresh `latest handshake`.
Rotating keys invalidates every peer that references the old public key. In a hub-and-spoke setup, rotate the hub last.