WireGuard MTU and Handshake Failures: Fix Guide - 夜莺博客

WireGuard MTU and Handshake Failures: Fix Guide

WireGuard is deliberately silent: it does not answer unauthenticated packets and never sends an ICMP error. A misconfiguration therefore produces the worst possible symptom — wg-quick up exits 0, the interface shows UP, and nothing flows. Worse, the classic WireGuard failure is partial: ping works, DNS works, and HTTPS hangs forever. This guide maps symptoms to causes, then fixes each one with commands you can paste into a change window.

Symptom to Cause Lookup

Symptom Most likely cause
Handshake never completes, wg show shows no latest handshake Wrong Endpoint, UDP blocked by firewall/ISP, or key mismatch
Handshake completes, then nothing passes AllowedIPs does not cover the traffic, or IP forwarding / NAT missing on the server
Ping and small DNS queries work, HTTPS and file copies hang MTU black hole — the signature WireGuard problem
Works, then dies after 30–60 seconds of idle Stateful NAT evicted the UDP mapping; no keepalive
Client can reach the tunnel but not the internet AllowedIPs = 0.0.0.0/0 missing on the client, or no MASQUERADE rule on the server

Why WireGuard Fails Silently

WireGuard's design decisions are all security wins and all troubleshooting costs. It is a UDP-only protocol with no negotiation phase: a peer that receives a packet it cannot authenticate simply drops it, with no reply. There is no "phase 1 / phase 2" log, no ISAKMP exchange to stare at, no vendor-specific debug level. If the keys do not match, the packets vanish.

The practical consequence is that you must reason from the wire, not from the logs. Three observations separate almost every case:

  • Is the handshake completing? wg show prints "latest handshake" and a byte counter per peer. A handshake that never arrives means the problem is below the tunnel: routing, firewall, NAT, or keys.
  • Is the encrypted traffic leaving the host? tcpdump on the physical interface tells you whether WireGuard's UDP ever hits the wire.
  • Is the inner traffic arriving? tcpdump on the tunnel interface tells you whether the packets that were supposed to be encapsulated actually got there.
# on the physical interface: do the encrypted packets leave and come back?
tcpdump -ni eth0 udp port 51820

# on the tunnel interface: is the inner traffic actually being encapsulated?
tcpdump -ni wg0 icmp or tcp port 443

WireGuard's data packets carry a 4-byte message type plus a 4-byte receiver index, and a handshake initiation is type 1. If you see only type 1 packets leaving one side and nothing coming back, the far end is not answering — wrong endpoint, wrong port, wrong public key, or a firewall in between. If you see type 2 responses and then type 4 data packets flowing both ways while the user still reports "it doesn't work", the tunnel is fine and you are looking at an MTU or AllowedIPs problem.

Fix 1: Find the Real Path MTU

WireGuard adds 60 bytes of overhead for IPv4 (20-byte outer IP header, 8-byte UDP header, 32-byte WireGuard header) and 80 bytes for IPv6. The 1420 default assumes a clean 1500-byte path — which PPPoE, CGNAT and some VPN-over-VPN setups do not have. Measure, do not guess:

# -M do sets DF; -s is ICMP payload. 1472 + 8 + 20 = 1500 bytes on the wire
ping -M do -s 1472 10.0.0.2      # fails? bisect downwards
ping -M do -s 1400 10.0.0.2      # try 1400, then 1300
# largest working payload + 28 = path MTU;  path MTU - 80 = safe WireGuard MTU

Example: if -s 1400 works and -s 1408 fails, the path MTU is 1428 and a safe tunnel MTU is 1348 — round down to 1340. Then set it explicitly in the client configuration:

[Interface]
PrivateKey = <client-private-key>
Address = 10.8.0.2/24
MTU = 1340
DNS = 10.8.0.1

[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

The overhead table is worth memorising, because the number you subtract depends entirely on what the outer packet looks like:

Outer transport Overhead Safe tunnel MTU on a 1500 path
IPv4 outer header 60 bytes (20 IP + 8 UDP + 32 WG) 1440 absolute maximum, 1420 practical
IPv6 outer header 80 bytes (40 IP + 8 UDP + 32 WG) 1420 absolute maximum, 1400 practical
PPPoE underlay (1492 path) 60 bytes approximately 1400
Double encapsulation (WG over WG/VXLAN) 60 bytes per layer subtract roughly 120 or more

A black hole looks exactly like a broken tunnel: TCP works because the handshake packet is small, then the first full-sized data packet with the DF bit set is dropped by a device that cannot fragment it and will not send an ICMP "fragmentation needed". That is why ping succeeds and scp hangs at 0%. Setting MTU in the tunnel configuration is the fix; TCP MSS clamping is the belt-and-braces version for traffic that originates behind a router you do not manage.

Fix 2: PersistentKeepalive for Anything Behind NAT

Consumer routers, corporate gateways and mobile CGNAT typically drop idle UDP mappings after 30–60 seconds. PersistentKeepalive = 25 sends a 32-byte heartbeat often enough to keep the binding alive: it is mandatory for mobile and CGNAT clients, harmless on a LAN. On a mesh of peers, resist setting it on every node — a down peer turns every keepalive into a handshake attempt, which is how you build a handshake storm.

The symptom to recognise: the tunnel works when someone is using it, then stops after a minute of silence and only recovers when the client sends new traffic. The "latest handshake" timestamp rolls forward to a fresh handshake on demand, which is easy to misread as healthy. Comparing wg show output before and after a 60-second idle period makes the eviction obvious. Only the side behind NAT needs the keepalive; setting it on both ends costs nothing but is not required.

Fix 3: AllowedIPs Has to Match Exactly

AllowedIPs is both a routing table and a cryptographic filter. A server-side entry of 10.8.0.2/32 for a client that also expects to use two addresses will silently drop the other one; a client with only a subnet in AllowedIPs will route everything else outside the tunnel and look "half broken". Full-tunnel clients need 0.0.0.0/0, ::/0; site-to-site tunnels need the remote subnets listed explicitly on both ends.

There are three distinct mistakes hiding in that one line, and they fail differently:

  • Too narrow on the client. With AllowedIPs = 10.8.0.0/24 the client's default route still points at its physical gateway, so internet traffic bypasses the tunnel entirely. Everything "works", which is why the leak goes unnoticed. Compare ip route get 1.1.1.1 on the client against what you expect.
  • Too narrow on the server. The server must list each client's tunnel address, and every route it wants to push back. If the server only learns 10.8.0.0/24 but the client's LAN is 192.168.50.0/24, the reply packets have nowhere to go and you see one-way traffic in the capture.
  • Overlapping entries. Two peers with overlapping prefixes are resolved by longest-prefix match, so the "wrong" peer can win without any error message. Keep peer ranges disjoint, and use /32 for individual clients.
# what does the kernel actually think, and which peer wins?
wg show wg0 allowed-ips
ip -4 route show table all | grep wg0
ip route get 192.168.50.10

Fix 4: Forwarding and Masquerade on the Server

If the handshake works but routed traffic does not, the kernel is dropping it:

echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

# server config: allow forwarding and NAT outbound
PostUp   = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Note the interface name in the MASQUERADE rule: on cloud instances it is often ens5, not eth0. A wrong interface here is invisible until you try to reach the internet through the tunnel.

With iptables replaced by nftables on most current distributions, the equivalent rules have to be written against the nat table explicitly — check which one your firewall front end (firewalld, ufw, nftables) actually manages before adding a rule by hand, because a rule added directly to iptables can be silently bypassed by the default nftables chain. The fastest way to confirm the forwarding path is to watch the counters: iptables -t nat -L POSTROUTING -v -n should increment while traffic flows. On a router platform with its own firewall (a VyOS-style appliance, for example), a policy-based NAT and firewall configuration replaces both the sysctl and the PostUp rules.

Fix 5: Rule Out the Underlay Before Touching the Tunnel

Half of all "WireGuard is broken" tickets are underlay problems that WireGuard merely revealed. Run these checks before editing a single line of configuration:

  1. Confirm the endpoint address resolves and the port is actually open from outside: nc -vuz vpn.example.com 51820 from a host outside the network.
  2. Confirm the server hears the handshake on wg0 and on the physical interface. If the packet arrives at the NIC but the counter never moves, something upstream is dropping it.
  3. Confirm the clock. WireGuard keys themselves do not expire, but a skewed clock breaks certificate-based configuration management and makes the "latest handshake" timestamps meaningless when you correlate with logs.
  4. Confirm the client's public key on the server matches wg show wg0 public-key on the client exactly. One transposed character produces a silent drop, not an error.
  5. Confirm only one VPN client is writing routes on the same host: a stale wg-quick down that failed halfway through leaves routes pointing into a deleted interface.

Fix 6: Keys, Duplicates and the Handshake Storm

The most common configuration error is not a wrong key but a duplicated one. Two clients sharing a private key both try to use the same public key; the server's peer entry can only point at one endpoint, so one of them never completes a handshake. The second most common is copying a client's private key into a server's PrivateKey line — the tunnel comes up, and neither side can talk to the other.

# derive the public key from a private key without touching the config
wg pubkey < client-private.key
wg show wg0 public-key
wg show wg0 latest-handshakes
wg show wg0 endpoints        # where the server believes each peer lives

Watch the endpoints output on a roaming client: WireGuard updates the peer's endpoint automatically when it receives authenticated traffic from a new address, which is exactly what you want — and also why a client that reconnects from a different network can leave the server pointing at a stale address until the next handshake.

Verification Toolkit

wg show                        # latest handshake, transfer counters per peer
wg show wg0 allowed-ips
ip -4 route show table all | grep wg
tcpdump -ni wg0 icmp           # does the inner traffic actually arrive?
tcpdump -ni eth0 udp port 51820  # do the encrypted packets leave/arrive?

Read the counters as evidence: encrypted bytes rising with inner traffic stalled points at MTU; latest handshake frozen points at connectivity or credentials; inner traffic arriving but not returning points at forwarding or the remote AllowedIPs.

Two more counters are worth watching during an incident. The transfer counters in wg show are cumulative per peer, not per second, so take two samples a minute apart and compare rather than reading the absolute number. And on a busy server, a kernel-level trace gives you the timeline that WireGuard itself refuses to produce:

# per-second packet counts on the tunnel interface (needs bpftrace)
sudo bpftrace -e 'tracepoint:net:netif_receive_skb /args->name == "wg0"/ { @[comm] = count(); }'
sudo bpftrace -e 'kprobe:udp_sendmsg { @bytes = hist(args->len); }'

A quick sanity test that separates the layers: bring the tunnel up, then run ping -M do -s 1300 across it. Small packets pass, large fail — MTU. Small packets fail too, but wg show shows a recent handshake — routing or AllowedIPs. No handshake at all — connectivity or keys. That three-way split resolves the large majority of WireGuard incidents before you open a capture.

相关阅读:WireGuard 站点到站点 VPN:wg0.conf 实例TCP MSS 钳制与 PMTUD 排障IPsec IKEv2 调试:SA 协商与认证失败 以及 巨型帧 MTU 不匹配与 ping DF 位排查

原文链接:Why WireGuard Connections Silently Fail