WireGuardLab
Flat isometric illustration of two pink corner walls with large oval openings and glowing padlocks, above a dotted slab crossed by a bright pink line.
troubleshooting

WireGuard MTU Calculator and Fragmentation Fix

Work out the right WireGuard MTU from your path MTU, understand the 60 and 80 byte overhead, measure the real ceiling, and clamp TCP MSS correctly.

By WireGuardLab Editorial · · 8 min read

The tunnel comes up. wg show prints a recent handshake. Ping works, SSH works, DNS works. Then a web page loads halfway and hangs, apt update freezes at 0%, and a file copy stalls at a few kilobytes. Nothing in the logs. That pattern is almost never a key problem or a firewall problem. It is MTU, and the arithmetic behind it is small enough to do on paper.

If you want the number without the reading, the mesh topology designer on this site computes a WireGuard MTU for the common transports alongside the peer configuration it generates. The rest of this article explains where that number comes from, how to measure your own path instead of trusting a default, and what to do when you cannot set an MTU on the client at all.

Where the bytes go

Every packet that enters a WireGuard interface comes out the other side wrapped in four layers. The overhead is fixed, which is what makes the calculation reliable.

LayerBytesNotes
WireGuard transport header16message type, reserved bytes, receiver index, 64-bit counter
Poly1305 authentication tag16appended to every encrypted payload
UDP header8source port, destination port, length, checksum
Outer IPv4 header20no options in normal use
Outer IPv6 header40fixed length, no options in normal use

Total added to each inner packet: 60 bytes over IPv4, 80 bytes over IPv6. There is no negotiation and no variable-length option block, because the protocol fixes its cipher suite rather than negotiating one. That is the whole reason the number is stable enough to precompute.

The calculator

Take the MTU of the path the encrypted UDP actually travels over, subtract 80, and use that as the WireGuard interface MTU. Subtracting 80 rather than 60 costs 20 bytes of payload and buys immunity to the day your ISP turns on IPv6 and the outer header grows.

Underlying pathPath MTUWireGuard MTUIPv4 TCP MSS
Ethernet or fibre, standard150014201380
PPPoE (DSL, some fibre)149214121372
DS-Lite, 6rd, generic IP-in-IP148014001360
GRE or some cloud overlays146013801340
Constrained mobile path143013501310
Conservative universal floor136012801240

TCP MSS is the WireGuard MTU minus 40 for IPv4 inside the tunnel (20 bytes of IP header plus 20 of TCP header), or minus 60 for IPv6 inside.

Two limits are worth remembering. RFC 8200 requires every link carrying IPv6 to support a 1280 byte MTU, so if IPv6 runs inside the tunnel, do not configure the WireGuard interface below 1280. And a tunnel MTU of 1280 is the value to reach for when a path refuses to be measured: it is almost always safe, and the cost is roughly a tenth of your per-packet payload against 1420, not a multiple.

Why wg-quick usually lands on 1420

wg-quick does not hardcode a value. When MTU is absent from the [Interface] block, it looks up the route to the peer endpoint, falls back to the system default route if there is no endpoint to look up, and subtracts the worst case 80 bytes from whatever MTU it finds. On an ordinary 1500 byte Ethernet path that produces 1420, which is where the familiar number comes from. On a 1492 byte PPPoE line it produces 1412 automatically.

That automatic discovery fails in exactly one situation, and it is a common one: when the local link claims 1500 but something between you and the peer carries less. The kernel has no idea a mid-path device is smaller, so it hands wg-quick 1500, wg-quick hands you 1420, and every packet near full size disappears silently.

Measure the path instead of guessing

Do this from the machine that terminates the tunnel, against the peer’s public endpoint address, with the tunnel down or at least with the traffic routed outside it. The do-not-fragment flag is what makes the test meaningful.

ping -M do -s 1472 vpn.example.net

ICMP adds 28 bytes on IPv4, 8 for the ICMP header and 20 for the IP header, so a payload of 1472 is a 1500 byte packet on the wire. If it replies, your path is a clean 1500. If it returns Message too long or simply times out while smaller pings succeed, step down and repeat:

for s in 1472 1452 1432 1412 1392 1372 1352 1332; do
  ping -M do -c1 -W2 -s "$s" vpn.example.net >/dev/null 2>&1 \
    && { echo "largest payload that passes: $s -> path MTU $((s+28))"; break; }
done

Add 28 to the largest payload that passes to get the path MTU, then subtract 80 for the WireGuard MTU. A path MTU of 1452 gives a payload of 1424 and a WireGuard MTU of 1372.

Once the tunnel is up with that value, confirm from inside it. The target is the far end’s tunnel address, and the payload to test is the interface MTU minus 28:

ping -M do -c3 -s 1392 10.7.0.1

A 1420 byte interface should pass 1392 and fail 1393. If it fails well below that, the interface MTU is still too high for the path.

Set it on both ends

MTU is a property of an interface, not of a session. Each end fragments or refuses to fragment based on its own configured value, so a low path affects traffic in both directions and both ends need the lower number.

[Interface]
PrivateKey = <private key>
Address    = 10.7.0.1/24
ListenPort = 51820
MTU        = 1412

Change it live without editing files by setting it on the link, which is useful while bisecting:

sudo ip link set mtu 1412 dev wg0

That reverts on the next wg-quick down and up cycle, so write the working value into the config once you have it.

Clamping TCP MSS, and why it is only half a fix

On a router that terminates the tunnel for a whole LAN you cannot reach into every client to set an MTU. The standard answer is to rewrite the MSS option in TCP SYN packets crossing the tunnel so both endpoints agree to send smaller segments.

With iptables:

sudo iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

With nftables:

table inet mangle {
  chain forward {
    type filter hook forward priority mangle;
    oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu
  }
}

Both are worth having. Neither is complete, for a specific reason: MSS clamping only affects TCP. QUIC, which now carries a large share of web traffic through HTTP/3, runs over UDP and never sees an MSS option. Neither do DNS over UDP, most VPN-inside-VPN traffic, or media streams over UDP. If the interface MTU is wrong, clamping hides the problem for HTTPS over TCP while UDP applications keep stalling in ways that look unrelated.

The order that works: set a correct interface MTU first, then clamp MSS as a safety net for clients you do not control.

Why path MTU discovery does not rescue you

Classic path MTU discovery depends on routers returning ICMP “fragmentation needed” (IPv4) or “packet too big” (IPv6) messages. Large numbers of networks filter inbound ICMP wholesale, so the sender never learns the path is smaller and simply keeps retransmitting packets that are silently dropped. RFC 4821 documents this failure mode as an ICMP black hole and specifies packetization layer path MTU discovery, which probes with real payload data instead of relying on ICMP, as the fix. Linux implements it and the behaviour is controlled by net.ipv4.tcp_mtu_probing:

sudo sysctl -w net.ipv4.tcp_mtu_probing=1

A value of 1 enables probing only after a suspected black hole is detected, which is the safe setting. Treat it as a mitigation for paths you do not control, not a substitute for a correct MTU on your own interface.

Two related traps are worth naming. If your own firewall drops all ICMP inbound, you have created the black hole yourself, so permit ICMP type 3 code 4 on IPv4 and ICMPv6 type 2 at minimum. And if containers run behind the tunnel, the container bridge MTU must be no larger than the tunnel MTU; a Docker bridge sitting at 1500 behind a 1412 tunnel reproduces the exact stall this article is about, one layer further in.

A diagnostic order for the stall

  1. wg show reports a recent handshake, so the crypto and the endpoint are fine.
  2. Small pings pass through the tunnel, so routing and AllowedIPs are fine. If they do not, the problem is upstream of MTU; start with the fundamentals of keys, AllowedIPs, and NAT traversal.
  3. ping -M do -s 1392 inside the tunnel fails while -s 500 passes. That is the confirmation.
  4. Lower the interface MTU on both ends until the large ping passes, or measure the underlay first with the loop above and compute the value directly.
  5. Clamp MSS on any router that forwards for clients you cannot configure.
  6. Re-test with something UDP based, not only a browser, so a clamp does not mask a still-wrong MTU.

When the MTU is right and it still stalls

A handful of causes imitate an MTU problem closely enough to waste an afternoon. Offload features are the usual suspect: generic receive offload and segmentation offload on the physical NIC can hand oversized segments to a tunnel that cannot carry them, and disabling them with ethtool -K eth0 gro off gso off tso off is a quick way to rule it in or out. A second tunnel in the path, such as WireGuard inside a corporate VPN or over a mobile hotspot that itself tunnels, means subtracting overhead twice. And a NAT mapping expiring mid-transfer produces a stall that looks identical for the first few seconds, which is why PersistentKeepalive = 25 belongs on the NAT’d side before you start bisecting MTU values.

If you are still building the tunnel rather than repairing one, the step by step Ubuntu server and client setup walks through the configuration with a sane MTU already in place, and the comparison of WireGuard against Tailscale covers what an overlay does about MTU on your behalf.

See also

Sources

  1. wg-quick(8) manual page
  2. wg-quick source: set_mtu_up subtracts 80 from the discovered route MTU
  3. WireGuard Protocol and Cryptography
  4. RFC 8200: Internet Protocol, Version 6 (IPv6) Specification
  5. RFC 4821: Packetization Layer Path MTU Discovery

Related