Een verbinding die een time-out geeft vertelt je vrijwel niets. Aan de overkant kan niets luisteren, een firewall drie hops weg kan je SYN in stilte weggooien zonder ook maar een logregel te maken, of de route bestaat simpelweg niet — en van waar jij zit ziet elk van die dingen er hetzelfde uit. Je wacht. Er gebeurt niets.

Dus wordt het ticket geschreven als “poort 445 wordt ergens geblokkeerd”, en “ergens” is wat het laat stuiteren. Je provider kijkt zijn rand na, vindt hem schoon, en geeft het terug. Jij kijkt je hostfirewall na, vindt hem schoon, en geeft het terug. Er gaat een week voorbij. Er is niets opgelost.

Het woord dat de schade doet is “ergens”. Het hoeft er niet te zijn. Elke router tussen jou en de bestemming is verplicht je te vertellen wanneer hij degene is, en die verplichting staat sinds 1995 in de standaarden geschreven. Je hoeft het alleen op de juiste manier te vragen.

Waarom dit uitgespeld moet worden

Omdat te veel mensen in deze branche de basis niet kunnen, en het hun werkgevers elke week echt geld kost.

Ik bedoel geen junioren. Ik bedoel mensen met jaren achter zich, certificeringen aan de muur en senior in de functietitel, wiens diagnose van een poort met een time-out ophoudt bij “hij is geblokkeerd” en niet verder gaat. Ze draaien ping. Die faalt, of hij werkt, en hoe dan ook hebben ze niets geleerd over de poort waarnaar ze werden gevraagd. Dan gaat het ticket naar de provider, de provider stuurt het terug, en veertien dagen van iemands salaris gaan in een draadje dat één commando had kunnen zijn.

Niets hiervan is moeilijk. Een drop van een reject onderscheiden, de TTL op een antwoord lezen, weten dat een sterretje in een traceroute op zichzelf niets betekent — dat is een middag om te leren en het houdt een loopbaan lang stand. De reden dat mensen het niet weten is niet dat ze dom zijn. Het is dat niemand het leert. Leverancierstraining leert je de console van een leverancier. Certificeringen leren je het examen. De grondbeginselen eronder worden vanaf dag één aangenomen en nooit echt behandeld, dus mensen komen in seniorrollen zonder dat het ze ooit is getoond, en dan is het gênant om te vragen.

Dus in plaats van erover te klagen, staat het hier opgeschreven. Dit is de basis, uitgespeld, met de commando’s en een script dat je vandaag kunt draaien.

En een woord over waarom ik de moeite nam: ik draaide dit tegen mijn eigen lijn terwijl ik het schreef en vond drie storingen waarvan ik niet wist dat ik ze had. Een SMB-drop elf hops ver. Vervalste SMTP-resets één hop weg. Een firewallregel die op IPv4 bestaat en op IPv6 niet. Een middag, geen root, op een lijn die ik zelf beheer en waar ik op let. Denk eens na over wat er onopgemerkt zit op de netwerken die iemand betaald wordt te draaien.

Waarom de Fisher-Price OS (Windows) hier niet in staat

Twee redenen dat het er niet in staat. Een ervan is technisch en een niet, en ik geef je liever beide dan te doen alsof het allemaal techniek is.

De technische is dat het dit niet kan. tracert stuurt ICMP-echo-verzoeken en verder niets — de eigen naslag van Microsoft beschrijft het als “sending Internet Control Message Protocol (ICMP) echo Request or ICMPv6 messages to the destination with incrementally increasing time to live (TTL) field values”, en er is nergens in de syntaxis een poortparameter. pathping is datzelfde gereedschap met statistiek erop geschroefd. Test-NetConnection vertelt je of een TCP-poort open of dicht is en helemaal niets over hoe ver het antwoord vandaan kwam. Geen ervan kan de poort waar je echt om geeft op een gekozen afstand aftasten, en dat is de hele methode in dit bericht.

Je kunt er ook niet omheen scripten. De TTL op een socket zetten is makkelijk genoeg in .NET, maar de ICMP-fout teruglezen is de moeilijke helft, en er is geen equivalent van de truc waar dit bericht op leunt — geen manier om de kernel de fout te laten melden op de gewone socket die hem veroorzaakte. Dat laat een ruwe socket, en de eigen documentatie van Microsoft zegt “only members of the Administrators group can create sockets of type SOCK_RAW”. De weg zonder rechten bestaat dus niet en de weg met rechten wil een lokaal-admintoken. Je zit bij downloads van derden voordat je bent begonnen.

De andere reden is dat het me niet kan schelen, en dat zeg ik liever dan het op te tuigen. De naam is ook geen goedkope steek, het is een beschrijving. Het is het besturingssysteem dat je in de handen krijgt gedrukt als je nooit een ander hebt gehad, het verbergt de machine voor je als ontwerpdoel in plaats van als ongeluk, en op het moment dat je het netwerk een precieze vraag wilt stellen blijkt het gereedschap nooit gebouwd — want de mensen voor wie het is gebouwd werden nooit geacht te vragen. Dertig jaar later en tracert kan nog steeds geen pakket op een poort richten.

Het is geen echt besturingssysteem voor echte IT-mensen, en is het het enige dat je ooit hebt gebruikt, dan doe je dit werk niet op het niveau waarvoor dit bericht is geschreven. Ik ben me ervan bewust dat dat slecht valt. Ik schrijf niet om aardig gevonden te worden, ik schrijf op wat ik meet voor mensen die dingen meten, en het kan me weinig schelen dat iemand die alleen dat ooit heeft gedraaid het liever anders verwoord ziet.

De gereedschapslijst is het argument, niet de mening. Elke Unix in de tabel verderop laat je een hoplimiet zetten en een protocol kiezen, vier ervan met één commando, en Linux doet de hele meting zonder ook maar sudo. Dat is niet omdat ze moeilijker te gebruiken zijn. Het is omdat ze zijn gebouwd door mensen die verwachtten dat wie achter het toetsenbord zat dingen wilde weten. Een besturingssysteem waarvan de diagnose ophoudt bij ping heeft je onomwonden verteld wat het denkt van de persoon die het gebruikt, en dertig jaar mensen die dat aanvaarden is hoe we belandden bij een branche die een weggegooid pakket niet kan lokaliseren.

Ik draai het dus niet, ik heb het in geen jaren serieus gedraaid, en ik ga het geen eigen sectie schrijven om even ruimhartig te lijken over een gat dat echt is. Alles waar ik op bouw en alles wat het meten waard is, is Unix, en daar woont dit bericht.

Draait de kapotte bak toevallig de Fisher-Price OS (Windows), dan verandert dat niets aan de methode. Krijg een shell op iets anders — een Linux-VM, een Mac, een Raspberry Pi op dezelfde switch — en loop de hoplimiet er naartoe. De meting kan het geen zier schelen wat de overkant draait. Het geeft alleen om wat ertussen zit.

TTL is een hopbudget, en elke router is je een bon verschuldigd

De IPv4-header heeft een Time to Live-veld van 8 bits. De naam is een overblijfsel: het was in seconden gespecificeerd, en niets heeft het al decennia als seconden behandeld. RFC 1812 beslechtte de discussie in 1995 en maakte de hopteltelezing normatief:

Each router (or other module) that handles a packet MUST decrement the TTL by at least one, even if the elapsed time was much less than a second. Since this is very often the case, the TTL is effectively a hop count limit on how far a datagram can propagate through the Internet.

Dan het deel dat hier telt, uit dezelfde sectie:

If the TTL is reduced to zero (or less), the packet MUST be discarded, and if the destination is not a multicast address the router MUST send an ICMP Time Exceeded message, Code 0 (TTL Exceeded in Transit) message to the source.

Dat is een MUST. Geen beleefdheid, geen suggestie. RFC 792 uit 1981 zei alleen dat een gateway “may also notify the source host”; dertien jaar later werd de eis aangescherpt, en RFC 1812 zegt waarom met zoveel woorden:

ICMP Time Exceeded messages are required because the traceroute diagnostic tool depends on them.

IPv6 liet de schijn varen en hernoemde het veld. RFC 8200 noemt het Hop Limit — “8-bit unsigned integer. Decremented by 1 by each node that forwards the packet” — en het verloopbericht werd ICMPv6 type 3, code 0, “hop limit exceeded in transit”.

Lees dat als een instrument in plaats van een regel en het zegt iets nuttigs. Elke router op het pad is een baken dat je op afstand kunt aanspreken. Zet de hoplimiet op 4 en de vierde router maakt zich bekend. Je hoeft de topologie niet te kennen, je hoeft geen toegang tot iets te hebben, en je hebt de medewerking van de operator niet nodig. Je hebt één pakket per hop nodig.

Traceroute doet precies dit sinds de late jaren tachtig. Wat het slecht doet is juist het ding waar je om geeft, want standaard tast het UDP-poorten hoog in het bereik van 33434 af, wat een poort is die niemand filtert en niemand bedient, dus het vertelt je over een pad dat niets echts ooit gebruikt. Een schone traceroute naar een host die je niet op TCP/445 kunt bereiken bewijst alleen dat UDP/33434 er komt. Wat nooit de vraag was.

Tast dus de poort af waar je om geeft.

Lees wat er terugkwam, niet of er iets terugkwam

Voordat je hops telt, kijk naar wat de overkant doet als je hem met een normale hoplimiet bereikt. Er zijn vijf verschillende antwoorden en mensen persen ze routineus samen tot één.

Wat er terugkomtWat het betekentWie het stuurde
SYN-ACK, de verbinding opentde poort is opende host, of iets dat voor hem antwoordt
TCP RSTactief geweigerdde host zonder iets dat luistert, of een apparaat dat is ingesteld om te weigeren
ICMP 3/13, communication administratively prohibitedeen apparaat weigert op beleid en zegt hetdat apparaat — zijn bronadres is je antwoord
ICMP 3/1, 3/2, 3/3host, protocol of poort unreachablede laatste router, of de host
helemaal nietsiemand gooit in stilte wegonbekend, dus ga het meten

De derde rij is degene waar je je gewoonten voor mag veranderen. Is een firewall ingesteld om te weigeren in plaats van weg te gooien, dan zet hij zijn eigen adres in het bronveld van de ICMP en geeft je de dader gratis. Op Linux is dat wat nft ... reject with icmpx admin-prohibited oplevert, en wat iptables -j REJECT --reject-with icmp-admin-prohibited altijd heeft opgeleverd. De meeste gereedschappen gooien het weg en printen “filtered”. nmap --reason laat het zien. Het script verderop ook.

Rij twee en vijf zijn de interessante storing. Een stille drop is een beleidsbeslissing om je niets te vertellen, en omdat het de standaard is op vrijwel elke commerciële firewall is het degene die je echt zult tegenkomen. Verwacht stilte.

Rij twee verdient ook wantrouwen. Een reset is geen bewijs dat de host hem stuurde, en ik kom daarop terug met een levend voorbeeld, want ik vond er een op mijn eigen lijn terwijl ik dit schreef.

Loop de poort af waar je om geeft, twee keer

De methode is twee runs en een diff. Dat is het hele eieren eten.

  1. Loop de hoplimiet op van 1 tot 20 met precies het protocol en de poort die falen, en schrijf op welke router bij elke hop antwoordt.
  2. Doe hetzelfde met iets dat werkt — idealiter dezelfde host en een poort die opent.
  3. De hop waar de antwoorden stoppen in run een, en doorgaan in run twee, is het apparaat dat je wegkaatst. Run twee geeft je zijn adres.

Waarom de antwoorden stoppen in plaats van veranderen: een router past zijn inkomende toegangslijst toe voordat hij iets anders met het pakket doet, dus zegt het beleid drop, dan is het pakket weg voordat het doorstuurpad ooit naar de hoplimiet kijkt, wordt er geen Time Exceeded gemaakt, en zet het apparaat nooit zijn naam onder wat het deed. De stilte begint dan ook bij de schuldige hop, niet erna.

Ik schreef een klein gereedschap voor het lopen omdat geen van de eigen gereedschappen het draagbaar doet. Het zet IP_TTL (of IPV6_UNICAST_HOPS) op een gewone socket, verbindt, en leest de ICMP-fout terug. Op Linux meldt IP_RECVERR die fout op precies de socket die hem uitlokte, wat betekent dat het hele ding zonder rechten draait — geen root, geen ruwe sockets, geen capabilities. Op macOS, de BSD’s en Solaris geeft de kernel je de ICMP niet zo, dus valt het terug op een ruwe socket en heeft het root nodig.

hopfind.py — de TTL-loper hopfind.py · 11 kB

Het is 279 regels standaardbibliotheek en verder niets, en het geheel staat aan het eind van dit bericht afgedrukt als je het liever leest dan downloadt.

python3 hopfind.py example.net 445      # the port under suspicion
python3 hopfind.py example.net 443      # the reference run
python3 hopfind.py example.net 53 --proto udp
python3 hopfind.py 2001:db8::1 443 -6

Gebruik voor beide runs hetzelfde protocol. Een TCP-spoor tegen een ICMP-spoor vergelijken is twee paden vergelijken, want lastverdeling hasht op de vijftupel en ICMP heeft geen poorten om op te hashen. Dezelfde host, hetzelfde protocol, een andere poort is de eerlijke vergelijking.

Alles hieronder is echte uitvoer van mijn eigen lijn op 28 augustus 2026, gedraaid als gebruiker zonder rechten op Fedora. Elk adres erin is herschreven naar de documentatiebereiken — RFC 5737 voor IPv4, RFC 3849 voor IPv6, met ook de ene interface-identifier gewijzigd. Dus 198.51.100.x is mijn eigen router en mijn ISP, 203.0.113.x is transit en peering, 192.0.2.x is het netwerk aan de overkant, en 2001:db8::/32 is het hele IPv6-pad. De structuur is overal behouden: dezelfde prefixgrenzen, dezelfde vormen van het hostdeel, hetzelfde aantal verschillende netwerken. De hopnummers, de tijden, de ICMP-types en welke hop stilviel zijn precies zoals gemeten.

Eén host, drie poorten, drie verschillende storingen

Overal dezelfde bestemming: een host op het publieke internet, veertien hops ver, met 443 open. Eerst de referentierun.

$ python3 hopfind.py 192.0.2.4 443 --max 15
walking to 192.0.2.4  TCP/443  hop limit 1-15
  1  198.51.100.254                              0.3 ms  ICMP 11/0 time exceeded in-transit
  2  198.51.100.133                             28.8 ms  ICMP 11/0 time exceeded in-transit
  3  *
  4  198.51.100.153                              6.3 ms  ICMP 11/0 time exceeded in-transit
  5  *
  6  203.0.113.240                              16.5 ms  ICMP 11/0 time exceeded in-transit
  7  203.0.113.188                              16.5 ms  ICMP 11/0 time exceeded in-transit
  8  203.0.113.185                              16.5 ms  ICMP 11/0 time exceeded in-transit
  9  203.0.113.15                               15.6 ms  ICMP 11/0 time exceeded in-transit
 10  203.0.113.125                              16.1 ms  ICMP 11/0 time exceeded in-transit
 11  192.0.2.31                                 26.6 ms  ICMP 11/0 time exceeded in-transit
 12  *
 13  *
 14  192.0.2.4                                  19.5 ms  connected

Verdict: TCP/443 is open. It answered at hop 14.

Let op hop 3, 5, 12 en 13. Vier routers op een pad dat duidelijk werkt zeiden niets, want genoeg apparatuur is ingesteld om geen ICMP voor zichzelf te maken, of begrenst het hard. Een sterretje is geen bewijs van een firewall. Neem dat mee als niets anders. Het signaal is nooit de aanwezigheid van sterretjes in één run; het is waar twee runs ophouden overeen te stemmen.

Nu dezelfde host op 445, die van hier een time-out geeft.

$ python3 hopfind.py 192.0.2.4 445 --max 15
walking to 192.0.2.4  TCP/445  hop limit 1-15
  1  198.51.100.254                              0.3 ms  ICMP 11/0 time exceeded in-transit
  2  198.51.100.133                              8.0 ms  ICMP 11/0 time exceeded in-transit
  3  *
  4  198.51.100.153                              6.4 ms  ICMP 11/0 time exceeded in-transit
  5  203.0.113.76                                5.6 ms  ICMP 11/0 time exceeded in-transit
  6  203.0.113.240                              15.9 ms  ICMP 11/0 time exceeded in-transit
  7  203.0.113.188                              15.8 ms  ICMP 11/0 time exceeded in-transit
  8  203.0.113.185                              16.6 ms  ICMP 11/0 time exceeded in-transit
  9  203.0.113.15                               15.9 ms  ICMP 11/0 time exceeded in-transit
 10  203.0.113.125                              15.0 ms  ICMP 11/0 time exceeded in-transit
 11  *
 12  *
 13  *
 14  *
 15  *

Verdict: answers stop after hop 10 (203.0.113.125).
         Whatever swallows TCP/445 is hop 11.
         Walk a port that works and read off the address at hop 11.

Hop 11 beantwoordde de 443-run in 26,6 ms en zei helemaal niets tegen de 445-run. Dezelfde bak, hetzelfde pad, dezelfde tien routers ervoor. Kruisverwijs de run die werkt en hop 11 heeft een naam: 192.0.2.31. Dat is het apparaat dat SMB weggooit, drie hops voor de bestemming en acht hops voorbij de rand van mijn provider. Niet de mijne, en niet die van mijn ISP.

Hop 5 maakt het punt over sterretjes van de andere kant. Het was een sterretje op de 443-run en antwoordde op de 445-run — precies andersom dan de storing. Dat kan ICMP-ratelimiet zijn, of het kunnen de twee runs zijn die verschillende paden door een lastverdeler nemen. Ik weet niet welke, en jij ook niet. Herhaal beide runs voordat je een van beide gelooft.

Twee hoplimietwandelingen naar dezelfde host, en de hop waar ze ophouden overeen te stemmenTwee wandelingen naar dezelfde host, en de hop waar ze ophouden overeen te stemmenEén proef per hoplimiet. Een gearceerde cel betekent dat die router ICMP Time Exceeded terugstuurde en zichzelf noemde.router antwoorddeer kwam niets terugvanaf hier stil1234567891011121314hoplimiet gezet op de proefTCP/443referentie, hij opent••*•*••••••**✓opentTCP/445getest, hij geeft een time-out••*•••••••****hop 11 antwoordde de ene wandeling en de andere nietHop 3, 5, 12 en 13 zeiden niets op een pad dat duidelijk werkt, dus een sterretje op zichzelf betekent niks.Hop 11 antwoordde de referentiewandeling in 26,6 ms en antwoordde de testwandeling helemaal nooit.Dat is de grens, en de referentiewandeling is wat het een adres geeft: 192.0.2.31.
Dezelfde twee runs, naast elkaar. De enige cel die ertoe doet is hop 11, en wat eraan telt is het meningsverschil: hij antwoordde de ene run en de andere niet.

Dan poort 25. Deze hield me tegen.

$ python3 hopfind.py 192.0.2.4 25 --max 15
walking to 192.0.2.4  TCP/25  hop limit 1-15
  1  192.0.2.4                                   0.4 ms  TCP reset

Verdict: a reset came back to a probe with a hop limit of 1.
         Nothing more than one hop away can have sent it, so check the reply
         TTL before you believe the host did.

Een hoplimiet van 1 betekent dat het pakket bij mijn eigen router stierf. Het ging één hop. Het kan geen veertien hebben gereisd. Toch kwam er in 0,4 ms een TCP-reset terug met het adres van de bestemming erop, en mijn kernel meldde plichtsgetrouw connection refused. Zonder de hoplimiet gezet zou ik dat hebben gelezen als “de overkant heeft geen mailserver” en het ticket hebben gesloten.

Iets één hop weg vervalst resets voor uitgaande SMTP en tekent ze met het adres van de bestemming. Uitgaande 25 blokkeren is een volstrekt gewoon ding voor een consumentenrouter of een ISP, en het doen met een reset in plaats van een drop is aantoonbaar de beleefde versie, maar de reset draagt het adres van iemand anders en ik had geen idee dat de mijne het deed. De hoplimiet is wat het ving, en niets anders in het antwoord zou het hebben gedaan.

Eén ernaast: waar de toegangslijst in de pijplijn zit

Het oordeel hierboven zegt dat de wegwerper hop 11 is omdat de antwoorden na hop 10 stopten. Wees voorzichtig met die rekenkunde, want ze hangt af van de volgorde waarin het schuldige apparaat twee taken doet.

De meeste apparatuur past het inkomende beleid eerst toe en de hoplimietcontrole tweede. Deny slaat aan, het pakket wordt weggegooid, en er wordt nooit een Time Exceeded gemaakt, dus het apparaat verschijnt nooit en de stilte begint bij zijn eigen hopnummer. Dat is het geval hierboven. Het is ook het gewone geval.

Sommige platforms handelen het verlopen van de hoplimiet af in het snelle pad voordat het beleid wordt beoordeeld. Daar beantwoordt het apparaat de aan hem gerichte proef en gooit het alleen proeven weg die verder zijn gericht, dus verschijnt het normaal en begint de stilte één hop later.

Waarom de schuldige hop meestal niet verschijnt: beleid wordt beoordeeld voor de hoplimietcontroleBinnen de hop die je wegkaatst bepaalt de volgorde van twee controles wat je zietJe proef komt aan met één hop over op zijn budget, en hij past bij een regel die deny zegt.Beleid eerst — vrijwel alle firewalls, en elk geval gemeten in dit berichtde router bij hop Nproef erininkomend beleidhoplimietcontroledoorsturenweggegooid voordat iets naar de hoplimiet kijktEr wordt geen ICMP gemaakt, dus deze router zet nooit zijn naam onder de drop.Je wandeling valt stil bij hop N.Verlopen eerst — sommige platforms handelen het af in het snelle padde router bij hop Nproef erinhoplimietcontroleinkomend beleiddoorsturenverlopen, dus ICMP 11/0 gaat terug met het adres van deze routerHij antwoordt voor zichzelf en slikt alleen proeven in die verder zijn gericht.Je wandeling valt stil vanaf hop N+1.
Waarom de hop die wegkaatst meestal onzichtbaar blijft. Op vrijwel alle firewalls wordt de deny eerst beoordeeld, dus de proef is weg voordat het doorstuurpad merkt dat de hoplimiet verliep en er wordt nooit ICMP gemaakt. Op apparatuur die het verlopen in het snelle pad afhandelt beantwoordt hetzelfde apparaat de aan hem gerichte proef en slikt alleen de proeven in die verder zijn gericht.

De eerlijke lezing van “antwoorden stoppen na hop N” is dus: de wegwerper is hop N+1 als hij je proef weggooide voordat hij merkte dat het budget op was, of hop N zelf als hij de aan hem gerichte proef beantwoordde en alles inslikte wat verder was gericht. Twee naast elkaar liggende apparaten, en de run die werkt noemt ze allebei. Citeer de adressen, niet de hoptelling — een hoptelling betekent niets voor de persoon die je ticket leest, die vanaf ergens anders telt.

De TTL van het antwoord vertelt je wie er echt antwoordde

De SMTP-reset hierboven werd gevangen door de hoplimiet die naar buiten ging. Er is een tweede, onafhankelijke controle beschikbaar in elk antwoord dat terug komt, en die kost niets.

Begin-TTL-waarden zijn niet gestandaardiseerd, maar in de praktijk zijn er drie:

Begint bijTypische verzender
64Linux, macOS, de BSD’s, illumos, de meeste hosts
255Cisco IOS, Junos, Solaris, het eigen verkeer van de meeste netwerkapparatuur
128de Fisher-Price OS (Windows), die je nog steeds nodig hebt om er een antwoord van te lezen

Trek de TTL die je ontving af van de eerstvolgende waarde erboven en je hebt de hoptelling terug. Een antwoord dat aankomt met TTL 50 begon bij 64 en kwam 14 hops. Een dat aankomt met TTL 250 begon bij 255 en kwam 5. ping print het zonder erom te vragen:

ping -c1 192.0.2.4          # ttl=50 → 14 hops away
tcpdump -n -v 'icmp'        # -v prints the ttl of every packet it shows

Twee dingen vallen daaruit. Beide zijn gratis.

Een antwoord waarvan de omgekeerde hoptelling niet overeenkomt met de andere antwoorden van de host is niet door de host gestuurd. Komt een echo-antwoord van een server 14 hops ver terug en de RST op poort 25 1 hop ver, dan schreef een middlebox de RST. Dezelfde truc als de sectie hierboven, van het andere eind, en hij werkt zelfs als je de uitgaande TTL niet kunt zetten.

Een antwoord dat bij 255 begon kwam van netwerkapparatuur, niet van een server. Nuttig als je probeert uit te zoeken of het ding dat je weigert de host is of de router ervoor.

Om het veld op TCP te zien in plaats van ICMP heb je een opname nodig, en het filter is het waard uit het hoofd te leren:

# every hop-limit expiry coming back to you, IPv4 and IPv6
tcpdump -n -v 'icmp[icmptype] == 11 or icmp6[icmp6type] == 3'

# who is resetting you, and from how far
tcpdump -n -v 'tcp[tcpflags] & tcp-rst != 0'

tcpdump print het verlopen als ICMP time exceeded in-transit — dezelfde zin die de standaarden gebruiken, en dezelfde gebeurtenis die als “TTL expired in transit” verschijnt op platforms die het zo verwoorden.

Dezelfde truc met eigen gereedschap, op vijf Unixen

Draai je liever geen script, dan doet het eigen gereedschap het meeste ervan. Het is het alleen meer met elkaar oneens dan je zou verwachten. -P in het bijzonder betekent drie verschillende dingen afhankelijk van wiens traceroute je vasthoudt, en een ervan verpest stilletjes je test.

Linux (traceroute 2.1.x)macOS / FreeBSDOpenBSDNetBSDSolaris 11
TCP-proeven-T-P tcpniet bruikbaarneenee
ICMP-proeven-I-I-I-I-I
UDP naar een vaste poort-U -p N-e -p Nneeneenee
bestemmingspoort-p N (constant voor TCP)-p N (verhoogt zonder -e)-p N (verhoogt)-p N (verhoogt)-p N (verhoogt)
wat -P betekentniet gebruiktproefprotocolnumeriek protocol, “will not work reliably for most protocols”zet DF en tast path MTU afpauze tussen proeven, in seconden
heeft rechten nodigja, voor -T en -Ijajajaja

Elk daarvan heeft ruwe sockets nodig, dus elk heeft rechten nodig — al leveren macOS en de BSD’s traceroute meestal setuid root, dus je hoeft er misschien geen sudo voor te typen. Linux niet, en Fedora niet, wat de halve reden is dat het script hierboven bestaat.

Drie valstrikken in die tabel, en ik heb alle drie een middag zien verspillen.

Op de BSD’s en macOS is -p een basispoort die met elke proef verhoogt. Dus traceroute -P tcp -p 445 host test 445, dan 446, dan 447, en tegen hop 10 vraag je naar een poort waar niemand ooit van heeft gehoord. Je wilt ook -e, wat de man-pagina firewall-ontwijkingsmodus noemt en wat eigenlijk gewoon betekent “houd de poort stil”:

sudo traceroute -P tcp -e -p 445 example.net     # macOS, FreeBSD

Op Linux doet gewone -p hetzelfde voor de standaard-UDP-methode en heb je -U -p nodig voor een constante UDP-poort. Voor TCP is -T -p al constant — de man-pagina is expliciet dat “for TCP and others specifies just the (constant) destination port to connect”.

sudo traceroute -T -p 445 example.net            # Linux
sudo traceroute -U -p 53  example.net            # Linux, UDP/53 specifically

Op Solaris en NetBSD is er helemaal geen manier om de poort vast te zetten, en op Solaris is -P een pauze in seconden, dus een gekopieerde Linux-opdrachtregel draait zonder fout en meet niets waar je om vroeg. Solaris heeft ook geen TCP-proefmodus. Dit is het geval waar je het script wilt.

Redox is de vreemde eend en een zin waard omdat ik de vraag verwacht. Zijn hele netwerkgereedschap is netutils — dns, ifconfig, nc, ping, telnetd, wget. Geen traceroute, geen tcpdump, niets om mee op te nemen. Is een Redox-bak een eind van het probleem, meet dan vanaf het andere eind en richt het lopen erop.

mtr verdient ook een vermelding, want het doet het herhaal-en-gemiddeld-deel dat de tabellen hierboven je met de hand laten doen:

sudo mtr -T -P 445 --report --report-cycles 20 example.net

Draai dat tegen de falende poort en weer tegen een werkende, naast elkaar. Dezelfde methode, mooiere uitvoer.

Niets hiervan werkt als iemand ICMP blokkeert

Elke meting in dit bericht is gemaakt van ICMP-fouten die naar mij terugreizen. Blokkeer die en de hele diagnose gaat op zwart — en een heel stuk anders ook.

ICMP in het geheel blokkeren wordt plaatselijk nog als een beveiligingshouding behandeld. Het is er sinds de jaren negentig geen verdedigbare meer. De aanvallen die het geacht wordt te stoppen waren ping of death en smurf, allebei opgelost in de stacks in plaats van aan de grens, en allebei opgelost voordat sommige van de ingenieurs die het advies nog herhalen geboren waren. Wat blanket blokkeren nu stopt is diagnose. Verder niets.

RFC 1812 laat op dat punt geen ruimte voor interpretatie: Time Exceeded is een MUST, en de standaard stelt dat de reden is dat traceroute eraan hangt. Gooi het weg en je hebt een gereedschap gebroken dat het eigen routervereistendocument van het internet noemt als de rechtvaardiging dat het bericht bestaat.

Path MTU discovery is de dure. Het heeft nodig dat ICMP 3/4, fragmentation needed, terugkomt naar de verzender. Filter dat en je krijgt de storing die elke netwerkingenieur minstens één keer heeft nagejaagd, degene waar de handshake afrondt en kleine overdrachten werken en alles dat een pakket van volle grootte draagt voor eeuwig hangt. SSH verbindt en scp blijft steken. De pagina laadt en de afbeelding komt nooit aan. Niets in de logs. Niets om te grepen.

Op IPv6 houdt het op een kwestie van smaak te zijn. RFC 4890 §4.3.1 somt de berichten op die een firewall niet mag weggooien:

  • Destination Unreachable (Type 1) - All codes
  • Packet Too Big (Type 2)
  • Time Exceeded (Type 3) - Code 0 only
  • Parameter Problem (Type 4) - Codes 1 and 2 only

en over Packet Too Big is het bot over het gevolg: “Effectively, parts of the Internet will become inaccessible.” IPv6-routers fragmenteren niet. Kan Packet Too Big de verzender niet bereiken, dan is er geen herstelpad.

De controle is een ratelimiet, geen drop. Sta type 3 en type 11 inkomend toe, tel ze, begrens ze op iets als honderd per seconde, log wat de grens overschrijdt, en je hebt de diagnoses gehouden, path MTU discovery werkend gehouden, en elk stukje bescherming gehouden dat de blanket-regel geacht werd in de eerste plaats te bieden. Beperken hoeveel van iets je aanneemt is een controle. Alles weigeren en dat harden noemen is gewoon weigeren gemeten te worden.

Voor iedereen die een MSP draait: gooit de lijn van je klant ICMP-fouten weg, dan heb je hun vermogen weggenomen te bewijzen in welk netwerk een storing zit — en dat van jezelf. De volgende keer dat een storing tussen twee providers zit die allebei zeggen dat hij schoon is, is dat de rekening voor het beleid.

Behalve echo. Gooi dat weg.

Alles hierboven gaat over ICMP-fouten. Echo is een ander beest, en het is het ene deel van het protocol dat ik aan de grens van de draad zou halen.

Kijk naar wat de standaard ervan eist. In IPv4 zegt RFC 792 over een echo-verzoek dat “the data received in the echo message must be returned in the echo reply message”. IPv6 is nog botter — RFC 4443 definieert het veld als “zero or more octets of arbitrary data” en eist dan dat het “MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message”.

Lees dat als aanvaller in plaats van als operator. De standaard verplicht elke host op aarde een blok bytes dat jij kiest aan te nemen en het je direct terug te geven. Dat is geen bijwerking. Het is een voorgeschreven, tweewegkanaal met willekeurige payload, dat loopt over een protocol dat de meeste firewalls doorlaten zonder te kijken en de meeste logging vastlegt als een pakkettelling in plaats van als inhoud.

Mensen bouwen er al dertig jaar tunnels op. Loki deed het in Phrack 49 in 1996. Ptunnel draagt een hele TCP-sessie binnen ping en zit al twee decennia één pakket weg. Is je uitgaande beleid “blokkeer alles, sta ICMP toe want het netwerkteam heeft het nodig”, dan heb je geen uitgaand beleid. Je hebt een VPN met extra stappen, en het verkeer vertrekt eruitziend als iemand die test of het internet werkt.

RFC 4890 is het met me oneens, en het is de moeite dat onomwonden te zeggen in plaats van alleen de helft aan te halen die me uitkomt. §4.3.1 zet Echo Request en Echo Response in dezelfde niet-weggooien-lijst als de fouten. Lees dan de rechtvaardiging die het geeft:

For Teredo tunneling [RFC4380] to IPv6 nodes on the site to be possible, it is essential that the connectivity checking messages are allowed through the firewall.

De gestelde reden om echo open te houden is dat iemand er een tunnel door je firewall mee moet bouwen. Dat is mijn argument, opgeschreven door de mensen die de tegenovergestelde zaak maken.

Het beleid is dus smal, niet blanket:

# transit rules. permit the errors, drop the ping
ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } \
    limit rate 100/second accept
ip protocol icmp icmp type { echo-request, echo-reply } drop

ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, \
    time-exceeded, parameter-problem } limit rate 100/second accept
ip6 nexthdr ipv6-icmp icmpv6 type { echo-request, echo-reply } drop

Draag op IPv6 dat patroon niet naar een link-lokale of hostketen zonder neighbour discovery te houden. Types 133 tot 137 — nd-router-solicit tot en met nd-redirect — zijn hoe IPv6 het werk doet dat ARP in IPv4 doet. Gooi die weg en het segment houdt binnen minuten op te werken, en het zal er niet uitzien als een firewallfout. Filter echo aan de grens, niet op de draad tussen een host en zijn eigen router.

Wat kost dat je? ping over de grens, en verder niets. Alles in dit bericht blijft werken, want geen enkele meting hier stuurt een echo-verzoek. hopfind.py loopt TCP en UDP en leest de fouten die terugkomen; traceroute -T en -U doen hetzelfde. Path MTU discovery heeft Packet Too Big nodig, wat een fout is. De omgekeerde-TTL-truc werkt op elk antwoord, en een TCP-handshake geeft je er een. Het enige dat je verliest is het minst informatieve gereedschap in de doos, en dit hele bericht is een argument over waarom ophouden bij ping in de eerste plaats het probleem is.

Mijn eigen lijn doet dit al precies, al betwijfel ik of het opzet was. ping naar mijn standaardgateway krijgt 100% verlies, en ICMP Time Exceeded van diezelfde gateway komt terug in 0,3 ms — zoals elk spoor in dit bericht laat zien. Echo dicht, fouten open. Wie die firmware leverde kreeg het juiste antwoord, en ik kwam er alleen achter dat ze dat hadden door te gaan kijken.

Dezelfde regel, één keer geschreven

Hier is de storing die ik niet verwachtte in mijn eigen huis te vinden. Een publieke DNS-resolver, gelopen op TCP/443 over beide families, minuten uit elkaar.

$ python3 hopfind.py 192.0.2.53 443 --max 10
walking to 192.0.2.53  TCP/443  hop limit 1-10
  1  *
  2  *
  3  *
  4  *
  5  *
  6  *
  7  *
  8  *
  9  *
 10  *

Verdict: nothing answered at all, not even the first hop.

Helemaal niets. Geen enkele hop. Mijn eigen router meldde niet eens het verlopen dat hij moet hebben gemaakt, hetzelfde verlopen dat hij in 0,3 ms meldde voor elk ander lopen in dit bericht — dus de drop gebeurt bij hop 1, voordat de hoplimietcontrole ooit draait, en hop 1 is de mijne.

Het pad is prima, wat dezelfde bestemming bewijst op UDP:

$ python3 hopfind.py 192.0.2.53 53 --proto udp --max 10
  1  198.51.100.254                              0.3 ms  ICMP 11/0 time exceeded in-transit
  2  198.51.100.133                              5.4 ms  ICMP 11/0 time exceeded in-transit
  3  *
  4  198.51.100.167                             14.5 ms  ICMP 11/0 time exceeded in-transit
  5  203.0.113.50                                6.3 ms  ICMP 11/0 time exceeded in-transit
  6  203.0.113.174                               5.7 ms  ICMP 11/0 time exceeded in-transit
  7  203.0.113.201                               6.4 ms  ICMP 11/0 time exceeded in-transit
  8  *

Zeven hops schone antwoorden naar hetzelfde adres. Het is dus niet de routering en niet de bestemming — iets op mijn lijn gooit TCP naar die host weg en laat UDP ernaartoe door.

Dan dezelfde resolver, dezelfde poort, over IPv6:

$ python3 hopfind.py 2001:db8:53::53 443 -6 --max 10
walking to 2001:db8:53::53  TCP/443  hop limit 1-10
  1  2001:db8:1:ee:beef:abcd:fec0:2a30           0.5 ms  ICMP 3/0 hop limit exceeded in-transit
  2  2001:db8:1::15c                            24.6 ms  ICMP 3/0 hop limit exceeded in-transit
  3  *
  4  2001:db8:2:200::50                          5.5 ms  ICMP 3/0 hop limit exceeded in-transit
  5  2001:db8:2:2::4                             5.7 ms  ICMP 3/0 hop limit exceeded in-transit
  6  2001:db8:2:991::                           16.6 ms  ICMP 3/0 hop limit exceeded in-transit
  7  2001:db8:53::53                             5.7 ms  connected

Verdict: TCP/443 is open. It answered at hop 7.

Recht erdoorheen. Zeven hops, geen drama. Dezelfde dienst, dezelfde poort, dezelfde bedoeling, en de regel bestaat op maar één adresfamilie.

Wie die regel schreef schreef hem voor IPv4 en schreef nooit de tweeling. Ik heb geen idee wat het moest bereiken — mijn gok is iets over DNS lokaal houden — maar wat het ook was, het bereikt het al op de helft van het verkeer zolang deze lijn IPv6 heeft. Was het er om een reden, dan werkt het niet. Was het er niet, dan hoort het er niet te zijn.

Dat is de alledaagse versie van het dual-stackprobleem, en het is veel gewoner dan de discussies over of je IPv6 überhaupt moet uitrollen. Twee regelboeken. Eén onderhouden.

Het script, in zijn geheel

Geen afhankelijkheden, geen installatie, niets dan de standaardbibliotheek. Python 3.6 of later, en op Linux helemaal geen rechten.

De twee klassen zijn het hele draagbaarheidsverhaal. ErrorQueue is het Linux-pad: bewapen IP_RECVERR op de socket, en nadat de proef faalt, lees MSG_ERRQUEUE en trek het adres van de router uit de sock_extended_err-structuur waar de kernel het aan vastplakt. RawIcmp is overal elders: open een ruwe ICMP-socket, lees wat aankomt, neem het bronadres van het pakket. De eerste heeft niets nodig, de tweede heeft root nodig, en de rest van het programma kan het niet schelen welke van de twee het kreeg.

Eén detail is het aanwijzen waard omdat het het verschil is tussen een juist antwoord en een aannemelijk. In probe() wordt de foutwachtrij geleegd voordat SO_ERROR wordt geraadpleegd. Een ICMP-fout bereikt een TCP-socket als een gewone errno — ICMP 3/3 port unreachable komt aan als ECONNREFUSED, precies als een echte reset — dus SO_ERROR eerst controleren zou de vervalste SMTP-reset verderop in dit bericht als een eerlijke weigering van de overkant hebben gemeld. Lees de wachtrij eerst en ee_origin vertelt je dat een router sprak.

#!/usr/bin/env python3
"""hopfind - work out how many hops away the thing blocking your port is.

Walks the IPv4 TTL, or the IPv6 hop limit, up from 1 and records which router
answers at each step - using the protocol and port you actually care about
instead of traceroute's default UDP high ports.

Run it twice. Once against something that works, once against the port that
does not. The hop where the answers stop is the device dropping you, and the
run that works gives you its address.

    python3 hopfind.py example.net 445            # the port under suspicion
    python3 hopfind.py example.net 443            # the reference run
    python3 hopfind.py example.net 53 --proto udp
    python3 hopfind.py 2001:db8::1 443 -6

On Linux this needs no privileges at all: IP_RECVERR and IPV6_RECVERR hand the
ICMP errors back on the ordinary socket that caused them. On macOS, the BSDs
and Solaris the errors have to be read off a raw ICMP socket, which means root.

Written for https://blogs.damiendye.uk/networking/how-far-away-is-the-firewall/
Public domain. Do what you like with it.
"""

import argparse
import errno
import os
import select
import socket
import struct
import sys
import time

# Linux socket options. Absent from the socket module on some builds, so they
# are spelled out rather than looked up.
IP_RECVERR = 11
IPV6_RECVERR = 25

# ee_origin values from linux/errqueue.h. Anything else means the errno came
# from the local stack rather than from a router.
SO_EE_ORIGIN_ICMP = 2
SO_EE_ORIGIN_ICMP6 = 3

ICMP_V4 = {
    (11, 0): "time exceeded in-transit",
    (11, 1): "fragment reassembly time exceeded",
    (3, 0): "net unreachable",
    (3, 1): "host unreachable",
    (3, 2): "protocol unreachable",
    (3, 3): "port unreachable",
    (3, 4): "fragmentation needed",
    (3, 9): "net administratively prohibited",
    (3, 10): "host administratively prohibited",
    (3, 13): "communication administratively prohibited",
    (5, 0): "redirect",
}

ICMP_V6 = {
    (3, 0): "hop limit exceeded in-transit",
    (3, 1): "fragment reassembly time exceeded",
    (1, 0): "no route to destination",
    (1, 1): "communication administratively prohibited",
    (1, 3): "address unreachable",
    (1, 4): "port unreachable",
    (2, 0): "packet too big",
}


def describe(family, icmp_type, icmp_code):
    table = ICMP_V4 if family == socket.AF_INET else ICMP_V6
    return table.get((icmp_type, icmp_code), "unrecognised")


def is_expiry(family, icmp_type):
    """Was this the router saying 'your hop budget ran out here'?"""
    return icmp_type == (11 if family == socket.AF_INET else 3)


class ErrorQueue:
    """Linux. The kernel reports the ICMP error on the socket that provoked it."""

    def arm(self, sock, family):
        if family == socket.AF_INET:
            sock.setsockopt(socket.IPPROTO_IP, IP_RECVERR, 1)
        else:
            sock.setsockopt(socket.IPPROTO_IPV6, IPV6_RECVERR, 1)

    def extra_readers(self):
        return []

    def collect(self, sock, family):
        try:
            _, ancillary, _, _ = sock.recvmsg(0, 1024, socket.MSG_ERRQUEUE)
        except OSError:
            return None
        wanted = (socket.IPPROTO_IP, IP_RECVERR) if family == socket.AF_INET \
            else (socket.IPPROTO_IPV6, IPV6_RECVERR)
        for level, kind, data in ancillary:
            if (level, kind) != wanted or len(data) < 16:
                continue
            # struct sock_extended_err, then the sockaddr of the router that
            # sent the error - SO_EE_OFFENDER in the kernel headers.
            _, origin, icmp_type, icmp_code = struct.unpack_from("=IBBB", data, 0)
            if origin not in (SO_EE_ORIGIN_ICMP, SO_EE_ORIGIN_ICMP6):
                return None
            addr = None
            if len(data) >= 24:
                offender_family, = struct.unpack_from("=H", data, 16)
                if offender_family == socket.AF_INET:
                    addr = socket.inet_ntoa(data[20:24])
                elif offender_family == socket.AF_INET6 and len(data) >= 40:
                    addr = socket.inet_ntop(socket.AF_INET6, data[24:40])
            return addr, icmp_type, icmp_code
        return None


class RawIcmp:
    """macOS, the BSDs, illumos, Solaris. Read the ICMP off a raw socket, as root."""

    def __init__(self, family):
        proto = socket.IPPROTO_ICMP if family == socket.AF_INET else socket.IPPROTO_ICMPV6
        self.sock = socket.socket(family, socket.SOCK_RAW, proto)
        self.sock.setblocking(False)

    def arm(self, sock, family):
        pass

    def extra_readers(self):
        return [self.sock]

    def collect(self, sock, family):
        try:
            packet, peer = self.sock.recvfrom(1500)
        except OSError:
            return None
        if family == socket.AF_INET:
            # BSD raw sockets hand back the IP header too.
            header_len = (packet[0] & 0x0F) * 4
            packet = packet[header_len:]
        if len(packet) < 2:
            return None
        return peer[0], packet[0], packet[1]


def probe(dest, port, proto, family, hop_limit, timeout, listener):
    """One probe at one hop limit. Returns (icmp, socket_state, note)."""
    kind = socket.SOCK_STREAM if proto == "tcp" else socket.SOCK_DGRAM
    sock = socket.socket(family, kind)
    if family == socket.AF_INET:
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, hop_limit)
    else:
        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_UNICAST_HOPS, hop_limit)
    listener.arm(sock, family)
    sock.setblocking(False)

    try:
        if kind == socket.SOCK_DGRAM:
            sock.connect((dest, port))
            sock.send(b"\x00" * 32)
        else:
            try:
                sock.connect((dest, port))
            except BlockingIOError:
                pass
    except OSError as exc:
        sock.close()
        return None, None, "local error: %s" % exc.strerror

    readers = [sock] + listener.extra_readers()
    writers = [] if kind == socket.SOCK_DGRAM else [sock]
    deadline = time.time() + timeout
    icmp = state = None

    while time.time() < deadline:
        ready_r, ready_w, ready_x = select.select(
            readers, writers, [sock], max(0.01, deadline - time.time()))
        if not (ready_r or ready_w or ready_x):
            continue
        # Drain the error queue first, always. An ICMP error reaches a TCP
        # socket as a plain errno, so SO_ERROR on its own cannot tell you
        # whether a router spoke or the far end did.
        icmp = listener.collect(sock, family)
        if icmp:
            break
        if ready_w:
            err = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
            if err == 0:
                state = "connected"
            elif err == errno.ECONNREFUSED:
                state = "TCP reset"
            else:
                state = os.strerror(err)
            break

    sock.close()
    if icmp or state:
        return icmp, state, None
    return None, None, "no reply"


def walk(dest, port, proto, family, first, last, timeout, listener):
    print("walking to %s  %s/%d  hop limit %d-%d" % (dest, proto.upper(), port, first, last))
    answered = []
    for hop in range(first, last + 1):
        started = time.time()
        icmp, state, _ = probe(dest, port, proto, family, hop, timeout, listener)
        rtt = (time.time() - started) * 1000
        if icmp:
            addr, icmp_type, icmp_code = icmp
            print(" %2d  %-39s %7.1f ms  ICMP %d/%d %s"
                  % (hop, addr or "?", rtt, icmp_type, icmp_code,
                     describe(family, icmp_type, icmp_code)))
            if is_expiry(family, icmp_type):
                answered.append((hop, addr))
            else:
                return answered, hop, "icmp-reject", addr
        elif state:
            print(" %2d  %-39s %7.1f ms  %s" % (hop, dest, rtt, state))
            return answered, hop, state, dest
        else:
            print(" %2d  *" % hop)
    return answered, None, "silent", None


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("host")
    parser.add_argument("port", nargs="?", type=int, default=443)
    parser.add_argument("--proto", choices=("tcp", "udp"), default="tcp")
    parser.add_argument("--first", type=int, default=1, help="hop limit to start at")
    parser.add_argument("--max", type=int, default=20, help="hop limit to stop at")
    parser.add_argument("--wait", type=float, default=2.0, help="seconds to wait per hop")
    parser.add_argument("-6", dest="v6", action="store_true", help="force IPv6")
    parser.add_argument("-4", dest="v4", action="store_true", help="force IPv4")
    args = parser.parse_args()

    family = socket.AF_INET6 if args.v6 else socket.AF_INET
    kind = socket.SOCK_STREAM if args.proto == "tcp" else socket.SOCK_DGRAM
    dest = socket.getaddrinfo(args.host, args.port, family, kind)[0][4][0]

    if sys.platform.startswith("linux"):
        listener = ErrorQueue()
    else:
        try:
            listener = RawIcmp(family)
        except PermissionError:
            sys.exit("%s cannot report ICMP errors on a normal socket, so this "
                     "needs a raw one. Run it as root." % sys.platform)

    answered, stop, why, who = walk(dest, args.port, args.proto, family,
                                    args.first, args.max, args.wait, listener)
    what = "%s/%d" % (args.proto.upper(), args.port)
    print()

    if why == "connected":
        print("Verdict: %s is open. It answered at hop %d." % (what, stop))
    elif why == "icmp-reject":
        print("Verdict: %s at hop %d is refusing %s on policy, and is honest "
              "enough to say so." % (who, stop, what))
    elif why == "TCP reset":
        print("Verdict: a reset came back to a probe with a hop limit of %d." % stop)
        print("         Nothing more than %s away can have sent it, so check the reply"
              % ("one hop" if stop == 1 else "%d hops" % stop))
        print("         TTL before you believe the host did.")
    elif answered:
        last_hop, last_addr = answered[-1]
        print("Verdict: answers stop after hop %d (%s)." % (last_hop, last_addr))
        print("         Whatever swallows %s is hop %d." % (what, last_hop + 1))
        print("         Walk a port that works and read off the address at hop %d."
              % (last_hop + 1))
    else:
        print("Verdict: nothing answered at all, not even the first hop. Either the")
        print("         first hop is the one dropping you, or the ICMP errors are being")
        print("         filtered on the way back. Walk a port that works to tell those")
        print("         two apart.")


if __name__ == "__main__":
    main()

Wat dit je niet kan vertellen

De methode is goedkoop en ze is eerlijk over de meeste dingen, maar het is geen topologiescanner. Wees in het ticket eerlijk over wat je werkelijk hebt gemeten.

Verschillende vijftupels kunnen verschillende paden nemen. ECMP hasht de bron- en bestemmingspoorten in de keuze van de volgende hop, dus twee runs op twee verschillende poorten lopen niet gegarandeerd over dezelfde routers, wat een van de twee dingen is die zouden kunnen verklaren waarom hop 5 hierboven de ene run antwoordde en op de andere stil bleef. Herhaal beide runs. Een grens die beweegt is onbewezen.

MPLS verbergt hops. Een label-switched kern kan als één hop verschijnen, of als helemaal geen. Elke telling over de backbone van iemand anders is een ondergrens.

ICMP wordt vrijwel overal van een ratelimiet voorzien. Tast sneller af dan de router zal antwoorden en je maakt je eigen sterretjes. hopfind.py stuurt één proef per hop en wacht; dat is met opzet.

Het terugpad hoeft niet met het uitgaande overeen te komen. De hoptelling naar buiten is niet de hoptelling terug, en de omgekeerde-TTL-rekenkunde meet alleen het terugbeen.

Anycast betekent dat de host bij hop N niet twee keer dezelfde bak hoeft te zijn. Publieke resolvers en CDN’s in het bijzonder.

Een afgeronde handshake betekent niet dat de sessie overleeft. Een stateful firewall kan de SYN toestaan en wat volgt bij inspectie weggooien. Opent de verbinding en sterft hij dan, dan is dit het verkeerde instrument — ga opnemen.

Je hebt het eerste apparaat gevonden dat wegkaatst, niet degene die iemand zal toegeven. In een CGN of een carriernetwerk kan het adres bij hop N+1 een van meerdere bakken achter één adres zijn. Het is nog steeds het juiste om te citeren, want het is een feit over het pad.

Waar het eigenlijk voor is

Het stuiteren beëindigen. Dat is de hele opbrengst van de oefening.

“Poort 445 wordt ergens geblokkeerd” is een uitnodiging om het ticket terug te geven. Dit is dat niet:

TCP/445 naar 192.0.2.4 wordt stil weggegooid bij hop 11, adres 192.0.2.31. Hop 11 antwoordt ICMP Time Exceeded op TCP/443 over hetzelfde pad in 26 ms en antwoordt helemaal niets op 445, dus de drop is een beleidsbeslissing op dat apparaat, geen routeringsfout. Tien hops ervoor zijn schoon. Vier keer gereproduceerd over twintig minuten, vanuit een shell zonder rechten, script bijgevoegd.

Dat geeft niemand terug. Het noemt een apparaat, stelt wat het deed, stelt wat het niet deed, en toont het rekenwerk. Of ze het willen veranderen is nog steeds hun keuze. Maar de week pingpong is voorbij, en het kostte vier commando’s.

Alles erin kwam uit een veld van 8 bits dat in 1981 als timer werd gespecificeerd, nog nooit één keer als zodanig is gebruikt, en stilletjes “ergens” in een adres verandert.

Het leren lezen waard.