Ping works, but the transfers hang: diagnosing MTU, MSS and PMTU black holes in three commands

There is a separate sort of network fault that drives you crazy precisely because of its selectivity. Pings go through. DNS resolves. curl -I returns headers. The page loads — and freezes in the middle. SSH connects, but git clone hangs on “Receiving objects”. In the browser — half the images load and the eternal spinner.

Almost always it’s the MTU. Not “the Internet is bad,” not “the server is slow,” not “the ISP is throttling” — just somewhere along the path a packet is larger than what can be transmitted, and the mechanism that should have told you about it stayed silent.

Let’s break down: why this happens, how to diagnose in three commands, and what exactly to fix — because you can fix it in three different places, and the right place depends on whether this is your tunnel or someone else’s.


Where the “extra” bytes come from

A standard Ethernet frame carries 1500 bytes of payload. Each encapsulation layer takes its own slice.


Diagram based on RFC 791 and RFC 8200, overhead for WireGuard calculated from developer mailing list

For WireGuard, the official calculation from the project’s mailing list looks like this: “20-byte IPv4 header or 40 byte IPv6 header, 8-byte UDP header, 4-byte type, 4-byte key index, 8-byte nonce, N-byte encrypted data, 16-byte authentication tag.” In total 60 bytes of overhead on top of IPv4 and 80 on top of IPv6 — hence the default value of 1420, taken from the worst case.

Further, these subtractions stack up. A home PPPoE connection (1492) plus WireGuard over it (−60) gives 1432. Add a tunnel within a tunnel — and you’re at 1372, while the interface still shows 1420.

Info:

MTU and MSS are different things, and mixing them up is costly. MTU is the maximum IP packet size, a property of the interface. MSS is the maximum amount of data in a TCP segment, which the two sides agree on during connection establishment.

The relationship is simple: MSS = MTU − 20 (IP) − 20 (TCP). For 1500 this is 1460, for 1420 this is 1380. MSS is agreed once, in the SYN packet; MTU can change at any point along the route.


Why the problem shows up selectively

The answer to “why does the ping go through but the site doesn’t load” is in the sizes. Ping by default sends 56 bytes of data, while service requests like DNS and TLS handshakes are also small. All of that fits inside any MTU. But payload packets — the HTTP response body, image contents, git objects — come in full segments and hit the limit.

In the normal world this self-corrects: a router that sees a packet is too big sends an ICMP message “Fragmentation Needed” (type 3, code 4), the sender reduces the size and repeats. This is Path MTU Discovery.

Error:

Exactly one thing breaks: administrators who cut all ICMP “just in case.” Without ICMP type 3 the sender doesn’t get a signal, keeps sending packets of the same size, they silently disappear, TCP retransmits them — and the connection stalls instead of returning an error.

This state is called a “PMTU black hole.” It always looks like “the network works, but not completely,” and almost never as a clear failure.


Diagnosis: three commands

Command one. Find the real MTU to the host

ping -M do -s 1472 -c 3 example.com

-M do forbids fragmentation, -s sets the data size. Add 8 bytes of ICMP header and 20 bytes of IP, so the full packet = -s + 28. So -s 1472 is exactly 1500.

If it passes — MTU is at least 1500. If it fails (“Message too long” or “Frag needed and DF set”) — reduce: 1444 (→1472), 1414 (→1442), 1392 (→1420), 1352 (→1380). The first value that passes plus 28 is your MTU.

Command two. See where it’s being lost

tracepath example.com

tracepath shows not only the route but also the path MTU discovered, without root privileges. A line like pmtu 1420 identifies the node where the size dropped.

Command three. Check what you have

ip -br link show          # MTU of all interfaces
ip route get 8.8.8.8      # which interface the traffic goes out on
ss -ti dst 93.184.216.34  # actual MSS and retransmit counter for the connection

The last one is the least appreciated. In the ss -ti output you’ll see fields mss and retrans. A rising retrans on a live connection is almost a diagnosis.

Note:

If the problem occurs only for one site or service — this is likely not MTU, but filtering. MTU problems are selective by packet size, not by the destination: everything that transmits large data suffers, nothing small does. If your description is “small works, large stalls” for one host — look the other way.


Treatment: three places you can intervene

First place: MTU on your own interface

The most honest solution if the tunnel is yours.

# one-off
ip link set dev wg0 mtu 1380

# permanently, in the WireGuard config
[Interface]
MTU = 1380

The value is chosen based on the result of the first command: take the MTU found to the target and subtract the tunnel overhead. There is no universal number — 1380 and 1360 are popular simply because they safely cover most combinations.

Second place: MSS clamping on the router

When you don’t control the client MTU (home network, router with a tunnel, server providing access), the right tool is to clamp MSS in passing SYN packets.

# nftables
nft add rule inet filter forward tcp flags syn tcp option maxseg size set rt mtu

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

According to iptables documentation, TCPMSS has two modes: --set-mss value and --clamp-mss-to-pmtu; you can’t use them together.

Warning:

Two constraints that explain why the rule often doesn’t work.

First: “This target can only be used in the FORWARD, OUTPUT and POSTROUTING chains, and only for packets with the SYN bit set.” In INPUT it won’t apply, and without a SYN filter it won’t work as intended.

Second, more insidious: clamping only affects TCP. QUIC, DNS over UDP, WireGuard itself, game protocols — all UDP, and MSS doesn’t exist for them. If after clamping the web pages load but the video call still hangs — you fixed only half the problem, and the other half must be fixed via MTU.

Third place: make the kernel probe itself

If ICMP is being blocked along the way and you can’t influence it, Linux has a fallback mechanism — PLPMTUD, path MTU discovery at the packet level without ICMP.

sysctl -w net.ipv4.tcp_mtu_probing=1

Kernel documentation describes the values as: “0 - Disabled, 1 - Disabled by default, enabled when an ICMP black hole is detected, 2 - Always enabled, use initial MSS of tcp_base_mss.”

This is a reasonable default for a server: the mechanism will enable itself when it detects a black hole, and won’t interfere at other times. A value of 2 is rarely needed and can impact startup connection performance.

Important:

A simple rule of thumb: measure first, then change. Blindly tuning MTU to 1200 to “guarantee” it works really fixes the symptom — and at the same time increases overhead by about a quarter, because more packets are sent for the same data. The difference between 1380 and 1200 isn’t noticeable in a browser, but is highly noticeable when loading large files.


Quick mapping of how it usually goes

Indicator Most likely cause
Ping goes through, large files stall MTU/PMTU, ICMP black hole
SSH connects, scp stalls the same
Problem appeared after tunnel brought up MTU of the tunnel higher than the real one
Through the tunnel fine, direct connection not provider MTU (PPPoE, 1492)
Only one site doesn’t work, any size not MTU — filtering or DNS
Web works after clamping, calls don’t UDP traffic; MTU only fixes this
Separate topic — exposing access to a home server, where MTU pops up already at the route selection stage: Grey IP and CGNAT: five routes to the home server — a big guide.

Sources

Question:

What MTU value did you ultimately settle on in your tunnel and how did you choose it — did you measure it or did you take “the one everyone uses”? And did anyone encounter an ISP that throttles ICMP type 3 on their side so that it only gets fixed via tcp_mtu_probing?