Waarom MTU en fragmentatie verbindingen stil laten hangen
Soms doet een verbinding alles behalve het belangrijkste. De website antwoordt, de headers komen binnen, ping werkt, en dan blijft de pagina leeg. Geen foutmelding die iets verklaart. De oorzaak zit vaak in één getal dat je nooit ziet: hoe groot een pakketje mag zijn op een stuk van de route.
Ik bouwde het probleem na in een testnetwerk op één Linux-machine: een laptop, twee routers en een webserver, met in het midden een link die alleen pakketjes tot 1400 bytes doorlaat. Daarin zie je hoe het hoort te werken, hoe één firewallregel alles stillegt en hoe het weer wordt opgelost.
Wat MTU betekent
De MTU (maximum transmission unit) is het grootste IP-pakket dat een link in één keer vervoert. Voor Ethernet is dat 1500 bytes: RFC 894 legt het dataveld van een Ethernet-frame vast op maximaal 1500 octets.
Daarvan gaat een deel op aan headers. IPv4 heeft een vaste header van 20 bytes, TCP ook. Wat overblijft voor data heet de MSS (maximum segment size): 1500 − 40 = 1460 bytes. Bij ping is het 1472, want een ICMP-header is 8 bytes.
Niet elke link haalt 1500. Bij PPPoE gaat er een extra header af; RFC 4638 noemt 1492 als de gebruikelijke grens. Tunnels en VPN's verpakken pakketten opnieuw en houden dus ook minder over. Het minimum is klein: volgens RFC 791 moet elk IPv4-systeem 68 bytes kunnen doorsturen en 576 bytes kunnen ontvangen.
Opknippen of weggooien
Wat doet een router met een pakket van 1500 bytes dat over een link van 1400 moet? Hij kan het in fragmenten knippen, die de ontvanger weer samenvoegt. Maar heeft de afzender de Don't Fragment-vlag (DF) gezet, dan is RFC 791 duidelijk: kan het pakket niet heel worden afgeleverd, dan wordt het weggegooid.
Fragmenteren is fragiel. RFC 8900 uit 2020 heet niet voor niets IP Fragmentation Considered Fragile: alleen het eerste fragment bevat poortnummers, dus NAT-routers en firewalls die op poorten filteren hebben er moeite mee. Linux zet bij TCP standaard DF aan; in al mijn metingen had elk TCP-pakket die vlag. In IPv6 fragmenteren routers helemaal niet, alleen de afzender mag knippen, zoals ook in het IPv6-artikel stond.
Path MTU Discovery: de router meldt het
Als routers te grote pakketten weggooien, moet de afzender horen dat hij kleiner moet. Dat is Path MTU Discovery, uit RFC 1191 (november 1990). De afzender begint met de MTU van zijn eigen netwerkkaart en zet DF aan. Een router die een pakket niet kwijt kan, gooit het weg en stuurt een ICMP-bericht terug: Destination Unreachable, code 4, "fragmentation needed and DF set". Daarin staat de MTU van de link waar het vastliep.
In het testnetwerk zie je dat met één ping:
$ ping -c1 -M do -s 1472 203.0.113.80
From 192.168.1.1 icmp_seq=1 Frag needed and DF set (mtu = 1400)
Daarna liet ik de laptop een bestand van 1 MB downloaden en las ik mee op de netwerkkaart van de server. Die stuurt eerst tien pakketten van 1500 bytes. Binnen een milliseconde komen er tien ICMP-berichten terug van de router voor de smalle link, type 3, code 4, MTU 1400. Vanaf dan gaat alles in pakketten van 1400 bytes. De download was in 0,019 seconden klaar.
De server onthoudt het: ip route get toont daarna cache expires 596sec mtu 1400. Linux bewaart een geleerde MTU standaard 600 seconden, de 10 minuten die RFC 1191 als voorbeeld noemt.
Het zwarte gat
Dit werkt alleen als die ICMP-berichten aankomen. RFC 2923 uit 2000 beschrijft wat er gebeurt als dat niet zo is en noemt het een black hole. Firewalls zijn volgens die RFC vaak zo ingesteld dat ze alle ICMP tegenhouden.
Ik zette op de webserver één regel: iptables -A INPUT -p icmp -j DROP. De TCP-verbinding kwam gewoon tot stand, want de handshake-pakketjes zijn klein. Het HTTP-antwoord met de headers paste in een pakket van 257 bytes; curl meldde keurig statuscode 200. Daarna niets. Na 30 seconden liet ik curl stoppen, met 0 bytes van het bestand binnen.
Op de server zie je waarom. Het pakket van 1500 bytes wordt steeds opnieuw verstuurd, met telkens een twee keer zo lange wachttijd: 0,42, 0,83, 1,66, 3,33, 6,66 en 13,31 seconden. Elke keer stuurt de router zijn ICMP-bericht, en elke keer gooit de firewall het weg.
Dat maakt het zo lastig te vinden. RFC 2923 schrijft dat ping en sommige interactieve verbindingen gewoon werken, terwijl grote overdrachten vastlopen bij het eerste grote pakket.
TCP gaat zelf zoeken
TCP kan ook zonder ICMP uitzoeken wat past: Packetization Layer Path MTU Discovery (RFC 4821, maart 2007). De afzender probeert zelf groottes en kijkt welke aankomen. Linux heeft dat via net.ipv4.tcp_mtu_probing. Op mijn Debian-machine staat die op 0, uit. Met waarde 1 gaat TCP pas zoeken als het een zwart gat vermoedt.
Met die instelling op de server lukte de download alsnog, in 6,46 seconden in plaats van 0,02. Eerst ging het pakket van 1500 bytes nog vier keer verloren. Ruim 6,4 seconden na het eerste pakket schakelde de server terug naar 1076 bytes: de standaard tcp_base_mss van 1024 bytes data, plus headers en 12 bytes TCP-opties. Daarna zocht hij omhoog via 1288 en 1394, probeerde 1447 en 1420 (allebei weg) en bleef op 1399 bytes. Eén byte onder de echte grens, omdat Linux stopt als het zoekgebied smal genoeg is: standaard 8 bytes (tcp_probe_threshold).
QUIC, het protocol onder HTTP/3, werkt zo vanaf het begin. RFC 9000 verbiedt IP-fragmentatie, eist DF bij IPv4 en gaat uit van minstens 1200 bytes UDP-data per pakket. Wat groter kan, zoekt QUIC uit met proefpakketten volgens RFC 8899.
MSS clamping: de router past het aan
De handleiding van iptables wijst op een eenvoudiger oplossing. Bij het opzetten van een TCP-verbinding meldt elke kant in zijn SYN-pakket welke MSS hij aankan. Een router die weet dat er een smalle link achter zit, kan dat getal onderweg verlagen. Op de eerste router zette ik:
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
De laptop verstuurt zijn SYN met MSS 1460; bij de server komt hij aan met 1360, precies 1400 − 40. Ook het antwoord van de server ging van 1460 naar 1360. De server liet ICMP nog steeds vallen, maar dat maakte niet meer uit: hij stuurde vanaf het begin pakketten van 1400 bytes, er kwam geen enkel ICMP-bericht en de download duurde 0,018 seconden.
De handleiding noemt de symptomen waarvoor de regel bedoeld is: browsers maken verbinding en blijven hangen, ssh werkt maar scp hangt na de handshake. Over providers en servers die ICMP blokkeren is de toon daar niet mild.
Een echte server laat zijn MSS ook zien. Via TCP_INFO op een open verbinding zag ik vanaf een Nederlandse verbinding dat wallieweb.nl een MSS van 1440 meldt en ripe.net 1460.
Zelf proberen
Op Linux test je met ping -M do of een pad 1500 bytes aankan. 1472 bytes data plus 28 bytes headers is 1500:
$ ping -c 2 -M do -s 1472 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 1472(1500) bytes of data.
1480 bytes from 1.1.1.1: icmp_seq=1 ttl=56 time=13.6 ms
1480 bytes from 1.1.1.1: icmp_seq=2 ttl=56 time=13.0 ms
Eén byte meer en je eigen computer weigert al:
$ ping -c 1 -M do -s 1473 1.1.1.1
ping: local error: message too long, mtu=1500
Op Windows is het ping /f /l 1472 1.1.1.1: /f zet DF, /l is de grootte van de data. Komt er "Frag needed" terug, dan zie je meteen welke MTU de router meldt. Komt er niets, dan wordt het puzzelen. Naar nos.nl kwamen pings met 1452 bytes data netjes terug; vanaf 1453 bleef het stil, met en zonder DF. Waar die grens zit, zie je van buitenaf niet. De site zelf laadde gewoon.
Tot slot
Wat me verraste, is hoe goed het zwarte gat zich vermomt. Curl kreeg een nette statuscode 200, dus monitoring die alleen naar de headers kijkt, ziet niets. En de router deed zijn werk wel: achttien keer stuurde hij de juiste melding met de juiste MTU, en achttien keer werd die bij de voordeur van de server weggegooid. Het probleem zat nooit in de smalle link, alleen in het bericht erover.