Small pings cross the tunnel all day, the SSH banner arrives, and then the session freezes the moment a file transfer starts. That pattern points at a byte budget counted at the wrong layer, where the outer tunnel was sized for its own overhead and nobody subtracted the inner one. A nested IPIP packet is just another IP packet as far as WireGuard is concerned, which is exactly why it consumes bytes you never reserved. The correction is arithmetic, and it fits in three commands and one table.

The symptom that gives it away

Fragmentation trouble announces itself through size, never through addresses. Your peer answers a 56-byte ping for an hour and then goes silent at 1400 bytes while the tunnel stays up and no route changed. The handshake fits, the first full TCP window does not, and the transfer stalls seconds after it looked healthy.

Measure the ceiling before you edit anything, because an unmeasured ceiling is one you will chase twice. Send one probe below the suspected size with the do-not-fragment bit set, ping -M do -s 1000 10.9.0.2, then repeat it at a size you already know is too large. When the flip tracks payload size while the destination stays unchanged, routing is doing its job and a header is missing from the budget.

Count every header once, at the right layer

Start from the smallest MTU on the path rather than from the tunnel interface, because the wall sits upstream of both endpoints. WireGuard spends 60 bytes on an IPv4 outer transport, split as 20 for IP, 8 for UDP and 32 for its own header, and 80 bytes when that transport is IPv6. A 1420-byte wg0 keeps 20 bytes of slack under the 1440 that a 1500-byte path can really deliver, which is why the figure copied from a wiki still fits here. Write the whole stack on the diagram, because whoever changes the MTU next will otherwise pick a fresh round number and inherit your bug.

LayerBytesRemaining
Path MTU on the wired linknone yet1500
WireGuard outer: IP, UDP, WG header601440
Slack kept for safety201420 (wg0)
IPIP header inside the tunnel201400 (ipip0)
Original IPv4 and TCP headers401360 payload

The fifth row is where budgets go wrong, because the 1400 bytes a nested interface carries already include the original packet's own 40 bytes of headers. Only 1360 of those bytes reach the process reading the file. Treat 1400 as application payload, as any diagram missing that column invites you to, and the inner device ends up 40 bytes too generous against a wall that has not moved.

Set the two MTUs separately

The carrier and the nested device are separate objects, so each needs its own value. Apply them in this order, because the nested device inherits nothing from the carrier:

ip link set wg0 mtu 1420
ip tunnel add ipip0 mode ipip remote 203.0.113.9 local 198.51.100.4
ip link set ipip0 mtu 1400

The rule worth memorizing: inner MTU = outer MTU - 20 when the packet the IPIP tunnel carries is IPv4, and - 40 once either side of the inner packet is IPv6, because the nested header grows from 20 bytes to 40. Nothing else belongs in that subtraction. WireGuard's overhead was already spent when you chose 1420, and charging it twice produces a tunnel that works perfectly and stays slow for as long as nobody measures it. A deployment where both devices carry a round 1400 is the fingerprint of exactly that mistake.

IPv6 on either side of the nested packet invalidates every figure above, so recompute instead of copying a number from a similar site. The subtracted headers are 40 bytes each once IPv6 appears in the outer path or in the original datagram, and the payload figure falls by exactly that much.

The firewall rule you probably do not need

Between two WireGuard peers the only thing on the wire is UDP, so an inbound rule for protocol 4, the number RFC 2003 assigns to IPv4-in-IPv4 encapsulation, neither creates the nested path nor keeps it alive. The IPIP header lives inside the encrypted payload, where no edge firewall sees it. Opening protocol 4 anyway buys nothing beyond one more permitted way into the host.

Policy that mentions the original packet's sources and destinations belongs after decapsulation, on the tunnel interfaces and in the routing table. Put it there and the edge rule can stay closed.

What WireGuard actually validates

AllowedIPs judges the source of the packet WireGuard carries, and what it carries is the IPIP-encapsulated packet. A permitted IPIP source is accepted even when the header inside names a host you would rather not route. The original source is therefore not what the peer checked for you, and that inner pair of addresses needs its own enforcement after decapsulation.

The wrapper supplies routing structure, not a second layer of cryptography. Calling IPIP extra protection inside WireGuard describes a different design.

Verify with a ladder, not a single ping

Probe sizes that straddle the boundary you computed, then confirm what the kernel believes about both devices. With 1360 bytes of payload plus 40 bytes of headers making 1400, the first echo below fits and the second sits one byte past the ceiling:

ping -M do -s 1372 10.9.0.2
ping -M do -s 1373 10.9.0.2
wg show wg0
ip -d link show ipip0

A clean pass below the ceiling and a failure at the first size above it means the arithmetic holds end to end. Then rerun the transfer that originally stalled, because a synthetic probe proves the budget and says nothing about the sender's own write sizes. If the nested path still breaks at a size you proved fits, inspect the far endpoint's decapsulation and routes before shaving another ten bytes off the MTU.

Lahatsoratra mifandraika