Eine Verbindung, die abläuft, sagt dir fast nichts. Am anderen Ende lauscht vielleicht nichts, eine Firewall drei Hops entfernt verwirft vielleicht deinen SYN im Stillen, ohne auch nur eine Log-Zeile zu erzeugen, oder die Route existiert schlicht nicht — und von dort, wo du sitzt, sieht jedes davon gleich aus. Du wartest. Nichts passiert.
So wird das Ticket als „Port 445 ist irgendwo blockiert“ geschrieben, und „irgendwo“ ist es, was es zurückspringen lässt. Dein Provider prüft seinen Rand, findet ihn sauber und gibt es zurück. Du prüfst deine Host-Firewall, findest sie sauber und gibst es zurück. Eine Woche vergeht. Nichts ist behoben.
Das Wort, das den Schaden anrichtet, ist „irgendwo“. Es muss nicht dort stehen. Jeder Router zwischen dir und dem Ziel ist verpflichtet, dir zu sagen, wenn er der eine ist, und diese Pflicht steht seit 1995 in den Standards. Du musst nur richtig fragen.
Warum das buchstabiert werden muss
Weil zu viele Leute in dieser Branche die Grundlagen nicht können, und das kostet ihre Arbeitgeber jede Woche echtes Geld.
Ich meine keine Junioren. Ich meine Leute mit Jahren auf dem Buckel, Zertifikaten an der Wand und „Senior“ im Jobtitel, deren Diagnose eines abgelaufenen Ports bei „ist blockiert“ endet und nicht weitergeht. Sie lassen ping laufen. Es scheitert, oder es klappt, und so oder so haben sie nichts über den Port gelernt, nach dem sie gefragt wurden. Dann geht das Ticket an den Provider, der Provider lässt es zurückspringen, und zwei Wochen Gehalt von jemandem verschwinden in einem Thread, der ein einziger Befehl hätte sein können.
Nichts davon ist schwer. Einen Drop von einem Reject unterscheiden, die TTL auf einer Antwort lesen, wissen, dass ein Stern in einem Traceroute für sich genommen nichts heißt — das ist ein Nachmittag zum Lernen und hält eine Laufbahn lang. Der Grund, warum Leute es nicht wissen, ist nicht, dass sie dumm sind. Es ist, dass niemand es lehrt. Hersteller-Schulungen bringen dir die Konsole eines Herstellers bei. Zertifikate bringen dir die Prüfung bei. Die Grundlagen darunter werden am ersten Tag vorausgesetzt und nie wirklich behandelt, also kommen Leute in Senior-Rollen, ohne dass es ihnen je gezeigt wurde, und dann ist es peinlich zu fragen.
Statt darüber zu jammern, steht es hier also aufgeschrieben. Das sind die Grundlagen, buchstabiert, mit den Befehlen und einem Skript, das du heute laufen lassen kannst.
Und ein Wort dazu, warum ich mir die Mühe gemacht habe: Ich habe das gegen meine eigene Leitung laufen lassen, während ich es schrieb, und fand drei Fehler, von denen ich nichts wusste. Einen SMB-Drop elf Hops weit draußen. Gefälschte SMTP-Resets einen Hop entfernt. Eine Firewall-Regel, die es auf IPv4 gibt und auf IPv6 nicht. Ein Nachmittag, kein Root, auf einer Leitung, um die ich mich selbst kümmere und auf die ich achte. Denk mal darüber nach, was unbemerkt auf den Netzen sitzt, für deren Betrieb jemand bezahlt wird.
Warum das Fisher-Price-OS (Windows) hier nicht vorkommt
Zwei Gründe, warum es hier nicht steht. Einer davon ist technisch und einer nicht, und ich gebe dir lieber beide, als so zu tun, als wäre alles Technik.
Der technische ist, dass es das nicht kann. tracert sendet ICMP-Echo-Requests und sonst nichts — Microsofts eigene Referenz beschreibt es als „sending Internet Control Message Protocol (ICMP) echo Request or ICMPv6 messages to the destination with incrementally increasing time to live (TTL) field values“, und es gibt nirgends in seiner Syntax einen Port-Parameter. pathping ist dasselbe Werkzeug mit angeschraubter Statistik. Test-NetConnection sagt dir, ob ein TCP-Port offen oder zu ist, und nichts weiter darüber, wie weit weg die Antwort herkam. Nicht eines davon kann den Port, um den es dir wirklich geht, in einer gewählten Entfernung untersuchen, was die ganze Methode in diesem Beitrag ist.
Du kannst dich auch nicht drumherum skripten. Die TTL auf einem Socket zu setzen ist in .NET einfach genug, aber den ICMP-Fehler zurückzulesen ist die schwere Hälfte, und es gibt kein Äquivalent zu dem Trick, auf den sich dieser Beitrag stützt — keinen Weg, den Kernel den Fehler auf dem gewöhnlichen Socket melden zu lassen, der ihn verursacht hat. Das lässt einen Raw-Socket übrig, und Microsofts eigene Dokumentation sagt „only members of the Administrators group can create sockets of type SOCK_RAW“. Der unprivilegierte Weg existiert also nicht und der privilegierte will ein lokales Admin-Token. Du bist bei Drittanbieter-Downloads, bevor du angefangen hast.
Der andere Grund ist, dass es mir egal ist, und das sage ich lieber, als es aufzuhübschen. Der Name ist auch kein billiger Seitenhieb, er ist eine Beschreibung. Es ist das OS, das man in die Hand gedrückt bekommt, wenn man nie ein anderes hatte, es versteckt die Maschine vor dir als Design-Ziel statt als Unfall, und in dem Moment, in dem du dem Netz eine präzise Frage stellen willst, stellt sich heraus, dass das Werkzeug nie gebaut wurde — weil von den Leuten, für die es gebaut ist, nie erwartet wurde, dass sie fragen. Dreißig Jahre später kann tracert immer noch kein Paket auf einen Port zielen.
Es ist kein richtiges Betriebssystem für echte IT-Leute, und wenn es das einzige ist, das du je benutzt hast, dann machst du diesen Job nicht auf dem Niveau, für das dieser Beitrag geschrieben ist. Mir ist klar, dass das schlecht ankommt. Ich schreibe nicht, um gemocht zu werden, ich schreibe auf, was ich messe, für Leute, die Dinge messen, und es ist mir ziemlich egal, dass jemand, der nur es je betrieben hat, es lieber anders formuliert hätte.
Die Werkzeugliste ist das Argument, nicht die Meinung. Jedes Unix in der Tabelle weiter unten lässt dich ein Hop-Limit setzen und ein Protokoll wählen, vier davon aus einem einzigen Befehl, und Linux macht die ganze Messung ohne auch nur ein sudo. Das liegt nicht daran, dass sie schwerer zu bedienen sind. Es liegt daran, dass sie von Leuten gebaut wurden, die erwarteten, dass der, der an der Tastatur sitzt, Dinge wissen will. Ein Betriebssystem, dessen Diagnose bei ping endet, hat dir klar gesagt, was es von der Person hält, die es benutzt, und dreißig Jahre, in denen Leute das akzeptiert haben, sind, wie wir bei einer Branche gelandet sind, die kein verworfenes Paket orten kann.
Ich betreibe es also nicht, ich habe es seit Jahren nicht im Ernst betrieben, und ich schreibe ihm keinen eigenen Abschnitt, um über eine Lücke, die real ist, ausgewogen zu wirken. Alles, worauf ich baue, und alles, von dem aus zu messen sich lohnt, ist Unix, und dort lebt dieser Beitrag.
Wenn die kaputte Kiste zufällig das Fisher-Price-OS (Windows) betreibt, ändert das nichts an der Methode. Hol dir eine Shell auf irgendetwas anderem — eine Linux-VM, einen Mac, einen Raspberry Pi am selben Switch — und lauf das Hop-Limit auf sie zu. Die Messung schert sich nicht im Geringsten darum, was das ferne Ende betreibt. Sie schert sich nur darum, was dazwischen liegt.
TTL ist ein Hop-Budget, und jeder Router schuldet dir eine Quittung
Der IPv4-Header hat ein 8-Bit-Feld Time to Live. Der Name ist ein Überbleibsel: es wurde in Sekunden spezifiziert, und seit Jahrzehnten behandelt es nichts als Sekunden. RFC 1812 hat den Streit 1995 beigelegt und die Hop-Count-Lesart normativ gemacht:
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.
Dann der Teil, der hier zählt, aus demselben Abschnitt:
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.
Das ist ein MUST. Keine Nettigkeit, kein Vorschlag. RFC 792 sagte 1981 nur, ein Gateway „may also notify the source host“; dreizehn Jahre später wurde die Anforderung verschärft, und RFC 1812 sagt in ebenso vielen Worten, warum:
ICMP Time Exceeded messages are required because the traceroute diagnostic tool depends on them.
IPv6 hat die Fassade fallen lassen und das Feld umbenannt. RFC 8200 nennt es Hop Limit — „8-bit unsigned integer. Decremented by 1 by each node that forwards the packet“ — und die Ablauf-Nachricht wurde ICMPv6 Typ 3, Code 0, „hop limit exceeded in transit“.
Lies das als Instrument statt als Regel, und es sagt etwas Nützliches. Jeder Router auf dem Pfad ist ein Leuchtfeuer, das du nach Entfernung ansprechen kannst. Setz das Hop-Limit auf 4, und der vierte Router gibt sich zu erkennen. Du musst die Topologie nicht kennen, du brauchst zu nichts Zugriff, und du brauchst nicht die Kooperation des Betreibers. Du brauchst ein Paket pro Hop.
Traceroute macht genau das seit den späten 1980ern. Was es schlecht macht, ist genau das, worauf es dir ankommt, denn standardmäßig untersucht es UDP-Ports oben im Bereich 33434, ein Port, den niemand filtert und niemand bedient, also erzählt es dir von einem Pfad, den nichts Echtes je nutzt. Ein sauberes Traceroute zu einem Host, den du auf TCP/445 nicht erreichst, beweist nur, dass UDP/33434 dort ankommt. Was nie die Frage war.
Also untersuche den Port, um den es dir geht.
Lies, was zurückkam, nicht ob etwas zurückkam
Bevor du Hops zählst, sieh dir an, was das ferne Ende tut, wenn du es mit einem normalen Hop-Limit erreichst. Es gibt fünf verschiedene Antworten, und Leute werfen sie routinemäßig in einen Topf.
| Was zurückkommt | Was es heißt | Wer es geschickt hat |
|---|---|---|
| SYN-ACK, die Verbindung öffnet | der Port ist offen | der Host, oder etwas, das für ihn antwortet |
| TCP RST | aktiv abgelehnt | der Host, auf dem nichts lauscht, oder ein Gerät, das auf Reject konfiguriert ist |
| ICMP 3/13, communication administratively prohibited | ein Gerät lehnt per Policy ab und sagt es | dieses Gerät — seine Quelladresse ist deine Antwort |
| ICMP 3/1, 3/2, 3/3 | Host, Protokoll oder Port unerreichbar | der letzte Router, oder der Host |
| gar nichts | jemand verwirft im Stillen | unbekannt, also geh und miss es |
Die dritte Zeile ist die, für die es sich lohnt, seine Gewohnheiten zu ändern. Wenn eine Firewall darauf konfiguriert ist, abzulehnen statt zu verwerfen, setzt sie ihre eigene Adresse in das Quellfeld des ICMP und liefert dir den Schuldigen gratis. Auf Linux ist das, was nft ... reject with icmpx admin-prohibited erzeugt, und was iptables -j REJECT --reject-with icmp-admin-prohibited schon immer erzeugt hat. Die meisten Werkzeuge werfen es weg und drucken „filtered“. nmap --reason zeigt es. Das Skript weiter unten auch.
Zeile zwei und fünf sind das interessante Versagen. Ein stiller Drop ist eine Policy-Entscheidung, dir nichts zu sagen, und weil er auf fast jeder kommerziellen Firewall die Voreinstellung ist, ist er der, dem du tatsächlich begegnest. Erwarte Stille.
Zeile zwei verdient auch Misstrauen. Ein Reset ist kein Beweis, dass der Host ihn geschickt hat, und darauf komme ich mit einem Live-Beispiel zurück, denn ich fand eins auf meiner eigenen Leitung, während ich das schrieb.
Lauf den Port, um den es dir geht — zweimal
Die Methode sind zwei Läufe und ein Diff. Das ist das Ganze.
- Lauf das Hop-Limit von 1 auf 20 hoch, mit genau dem Protokoll und Port, der scheitert, und schreib auf, welcher Router bei jedem Hop antwortet.
- Mach dasselbe mit etwas, das funktioniert — idealerweise derselbe Host und ein Port, der öffnet.
- Der Hop, bei dem die Antworten in Lauf eins aufhören und in Lauf zwei weitergehen, ist das Gerät, das dich verwirft. Lauf zwei gibt dir seine Adresse.
Warum die Antworten aufhören statt sich zu ändern: Ein Router wendet seine eingehende Access-List an, bevor er sonst irgendetwas mit dem Paket tut, also ist das Paket, wenn die Policy Drop sagt, weg, bevor der Weiterleitungspfad je auf das Hop-Limit schaut, kein Time Exceeded wird erzeugt, und das Gerät setzt seinen Namen nie unter das, was es tat. Damit beginnt die Stille bei dem schuldigen Hop, nicht danach.
Ich habe ein kleines Werkzeug für das Laufen geschrieben, weil keins der nativen es portabel macht. Es setzt IP_TTL (oder IPV6_UNICAST_HOPS) auf einem gewöhnlichen Socket, verbindet und liest den ICMP-Fehler zurück. Auf Linux meldet IP_RECVERR diesen Fehler auf genau dem Socket, der ihn provoziert hat, was heißt: das Ganze läuft unprivilegiert — kein Root, keine Raw-Sockets, keine Capabilities. Auf macOS, den BSDs und Solaris reicht dir der Kernel das ICMP nicht so, also fällt es auf einen Raw-Socket zurück und braucht Root.
Es sind 279 Zeilen Standardbibliothek und sonst nichts, und das Ganze ist am Ende dieses Beitrags abgedruckt, falls du es lieber liest als herunterlädst.
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
Benutze für beide Läufe dasselbe Protokoll. Eine TCP-Spur gegen eine ICMP-Spur zu vergleichen heißt, zwei Pfade zu vergleichen, denn Load-Balancing hasht über das Fünf-Tupel, und ICMP hat keine Ports zum Hashen. Derselbe Host, dasselbe Protokoll, ein anderer Port ist der ehrliche Vergleich.
Alles unten ist echte Ausgabe von meiner eigenen Leitung am 28. August 2026, als unprivilegierter Benutzer auf Fedora gelaufen. Jede Adresse darin wurde in die Dokumentationsbereiche umgeschrieben — RFC 5737 für IPv4, RFC 3849 für IPv6, mit dem einen Interface-Identifier ebenfalls geändert. Also ist 198.51.100.x mein eigener Router und mein ISP, 203.0.113.x ist Transit und Peering, 192.0.2.x ist das ferne Netz, und 2001:db8::/32 ist der ganze IPv6-Pfad. Die Struktur bleibt durchweg erhalten: dieselben Präfixgrenzen, dieselben Host-Teil-Formen, dieselbe Anzahl verschiedener Netze. Die Hop-Nummern, die Zeiten, die ICMP-Typen und welcher Hop verstummte sind genau wie gemessen.
Ein Host, drei Ports, drei verschiedene Fehler
Durchweg dasselbe Ziel: ein Host im öffentlichen Internet, vierzehn Hops entfernt, mit offenem 443. Zuerst der Referenzlauf.
$ 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.
Beachte die Hops 3, 5, 12 und 13. Vier Router auf einem Pfad, der klar funktioniert, sagten nichts, weil eine Menge Geräte so konfiguriert sind, dass sie für sich selbst kein ICMP erzeugen, oder es hart raten-limitieren. Ein Stern ist kein Beweis für eine Firewall. Nimm wenigstens das eine mit. Das Signal ist nie die Anwesenheit von Sternen in einem einzelnen Lauf; es ist, wo zwei Läufe nicht mehr übereinstimmen.
Jetzt derselbe Host auf 445, der von hier abläuft.
$ 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 beantwortete den 443-Lauf in 26,6 ms und sagte zum 445-Lauf gar nichts. Dieselbe Kiste, derselbe Pfad, dieselben zehn Router davor. Quervergleich mit dem Lauf, der funktioniert, und Hop 11 hat einen Namen: 192.0.2.31. Das ist das Gerät, das SMB verwirft, drei Hops vor dem Ziel und acht Hops hinter dem Rand meines Providers. Nicht meins, und nicht das meines ISP.
Hop 5 macht den Punkt über Sterne von der anderen Seite. Er war ein Stern im 443-Lauf und antwortete im 445-Lauf — genau andersherum als der Fehler. Das könnte ICMP-Raten-Limitierung sein, oder die zwei Läufe könnten verschiedene Pfade durch einen Load-Balancer nehmen. Ich weiß nicht, welches, und du auch nicht. Wiederhol beide Läufe, bevor du einem glaubst.
Dann Port 25. Der hat mich gestoppt.
$ 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.
Ein Hop-Limit von 1 heißt, das Paket starb an meinem eigenen Router. Es ging einen Hop. Es kann nicht vierzehn gereist sein. Trotzdem kam in 0,4 ms ein TCP-Reset mit der Adresse des Ziels darauf zurück, und mein Kernel meldete brav connection refused. Ohne gesetztes Hop-Limit hätte ich das als „das ferne Ende hat keinen Mailserver“ gelesen und das Ticket geschlossen.
Etwas einen Hop entfernt fälscht Resets für ausgehendes SMTP und signiert sie mit der Adresse des Ziels. Ausgehenden Port 25 zu blockieren ist etwas völlig Gewöhnliches für einen Consumer-Router oder einen ISP, und es mit einem Reset statt einem Drop zu tun ist wohl die höfliche Variante, aber der Reset trägt die Adresse von jemand anderem, und ich hatte keine Ahnung, dass meiner das tut. Das Hop-Limit ist, was es erwischt hat, und sonst nichts in der Antwort hätte es getan.
Um eins daneben: wo die Access-List in der Pipeline sitzt
Das Urteil oben sagt, der Verwerfer ist Hop 11, weil die Antworten nach Hop 10 aufhörten. Sei vorsichtig mit dieser Arithmetik, denn sie hängt von der Reihenfolge ab, in der das schuldige Gerät zwei Aufgaben erledigt.
Die meisten Geräte wenden zuerst die eingehende Policy an und die Hop-Limit-Prüfung als Zweites. Deny greift, das Paket wird verworfen, und nie wird ein Time Exceeded erzeugt, also taucht das Gerät nie auf und die Stille beginnt bei seiner eigenen Hop-Nummer. Das ist der Fall oben. Es ist auch der häufige.
Manche Plattformen erledigen den Hop-Limit-Ablauf im Fast Path, bevor die Policy ausgewertet wird. Dort beantwortet das Gerät die an es adressierte Probe und verwirft nur Proben, die weiter zielen, also taucht es normal auf und die Stille beginnt einen Hop später.
Die ehrliche Lesart von „Antworten hören nach Hop N auf“ ist also: der Verwerfer ist Hop N+1, wenn er deine Probe entsorgt hat, bevor er merkte, dass das Budget aufgebraucht war, oder Hop N selbst, wenn er die an ihn adressierte Probe beantwortet und alles weiter Gezielte geschluckt hat. Zwei benachbarte Geräte, und der Lauf, der funktioniert, benennt beide. Zitier die Adressen, nicht die Hop-Zahl — eine Hop-Zahl heißt nichts für die Person, die dein Ticket liest und von irgendwo anders zählt.
Die TTL der Antwort sagt dir, wer wirklich geantwortet hat
Der SMTP-Reset oben wurde vom Hop-Limit erwischt, das hinaus ging. Es gibt eine zweite, unabhängige Prüfung, die in jeder Antwort verfügbar ist, die zurück kommt, und sie kostet nichts.
Anfangs-TTL-Werte sind nicht standardisiert, aber in der Praxis gibt es drei:
| Beginnt bei | Typischer Absender |
|---|---|
| 64 | Linux, macOS, die BSDs, illumos, die meisten Hosts |
| 255 | Cisco IOS, Junos, Solaris, der Eigenverkehr der meisten Netzgeräte |
| 128 | das Fisher-Price-OS (Windows), das du zum Ablesen einer Antwort davon immer noch brauchst |
Zieh die empfangene TTL vom nächsthöheren Wert ab und du hast die Hop-Zahl zurück. Eine Antwort, die mit TTL 50 ankommt, begann bei 64 und kam 14 Hops. Eine mit TTL 250 begann bei 255 und kam 5. ping druckt es ungefragt:
ping -c1 192.0.2.4 # ttl=50 → 14 hops away
tcpdump -n -v 'icmp' # -v prints the ttl of every packet it shows
Zwei Dinge fallen daraus. Beide sind gratis.
Eine Antwort, deren Rückwärts-Hop-Zahl nicht zu den anderen Antworten des Hosts passt, wurde nicht vom Host geschickt. Wenn ein Echo-Reply von einem Server 14 Hops weit zurückkommt und der RST auf Port 25 einen Hop weit zurückkommt, hat eine Middlebox den RST geschrieben. Derselbe Trick wie im Abschnitt oben, vom anderen Ende, und er funktioniert sogar, wenn du die ausgehende TTL nicht setzen kannst.
Eine Antwort, die bei 255 begann, kam von Netzgerät, nicht von einem Server. Nützlich, wenn du herauszufinden versuchst, ob das, was dich ablehnt, der Host ist oder der Router davor.
Um das Feld auf TCP statt ICMP zu sehen, brauchst du eine Aufzeichnung, und der Filter ist es wert, ihn sich zu merken:
# 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 druckt den Ablauf als ICMP time exceeded in-transit — dieselbe Formulierung, die die Standards benutzen, und dasselbe Ereignis, das auf Plattformen, die es so formulieren, als „TTL expired in transit“ auftaucht.
Derselbe Trick mit nativen Werkzeugen, auf fünf Unixen
Wenn du lieber kein Skript laufen lässt, erledigen die nativen Werkzeuge das meiste davon. Sie sind sich nur uneiniger, als du erwarten würdest. -P insbesondere heißt drei verschiedene Dinge, je nachdem, wessen Traceroute du in der Hand hast, und eins davon ruiniert deinen Test still und leise.
Linux (traceroute 2.1.x) | macOS / FreeBSD | OpenBSD | NetBSD | Solaris 11 | |
|---|---|---|---|---|---|
| TCP-Proben | -T | -P tcp | nicht nutzbar | nein | nein |
| ICMP-Proben | -I | -I | -I | -I | -I |
| UDP zu festem Port | -U -p N | -e -p N | nein | nein | nein |
| Zielport | -p N (konstant für TCP) | -p N (erhöht sich ohne -e) | -p N (erhöht sich) | -p N (erhöht sich) | -p N (erhöht sich) |
was -P heißt | nicht genutzt | Probe-Protokoll | numerisches Protokoll, „will not work reliably for most protocols“ | DF setzen und Pfad-MTU prüfen | Pause zwischen Proben, in Sekunden |
| braucht Privilegien | ja, für -T und -I | ja | ja | ja | ja |
Jedes davon braucht Raw-Sockets, also braucht jedes Privilegien — obwohl macOS und die BSDs Traceroute meist setuid root ausliefern, sodass du vielleicht kein sudo davorsetzen musst. Linux tut es nicht, und Fedora tut es nicht, was der halbe Grund ist, warum das Skript oben existiert.
Drei Fallen in dieser Tabelle, und ich habe alle drei einen Nachmittag verschwenden sehen.
Auf den BSDs und macOS ist -p ein Basis-Port, der sich mit jeder Probe erhöht. Also testet traceroute -P tcp -p 445 host 445, dann 446, dann 447, und bei Hop 10 fragst du nach einem Port, von dem nie jemand gehört hat. Du willst auch -e, was die Man-Page firewall evasion mode nennt und was eigentlich nur „halt den Port still“ heißt:
sudo traceroute -P tcp -e -p 445 example.net # macOS, FreeBSD
Auf Linux tut schlichtes -p dasselbe für die voreingestellte UDP-Methode, und du brauchst -U -p für einen konstanten UDP-Port. Für TCP ist -T -p schon konstant — die Man-Page ist ausdrücklich, dass „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
Auf Solaris und NetBSD gibt es überhaupt keinen Weg, den Port festzunageln, und auf Solaris ist -P eine Pause in Sekunden, also läuft eine kopierte Linux-Befehlszeile ohne Fehler und misst nichts von dem, wonach du gefragt hast. Solaris hat auch keinen TCP-Probe-Modus. Das ist der Fall, in dem du das Skript willst.
Redox ist der Ausreißer und einen Satz wert, weil ich die Frage erwarte. Sein ganzes Netzwerk-Toolkit ist netutils — dns, ifconfig, nc, ping, telnetd, wget. Kein Traceroute, kein tcpdump, nichts zum Aufzeichnen. Wenn eine Redox-Kiste das eine Ende des Problems ist, miss vom anderen Ende und richte den Lauf auf sie.
mtr verdient auch eine Erwähnung, weil es den Wiederhol-und-Mittel-Teil macht, den die Tabellen oben dich von Hand tun lassen:
sudo mtr -T -P 445 --report --report-cycles 20 example.net
Lass das gegen den scheiternden Port laufen und noch einmal gegen einen funktionierenden, nebeneinander. Dieselbe Methode, hübschere Ausgabe.
Nichts davon funktioniert, wenn jemand ICMP blockiert
Jede Messung in diesem Beitrag besteht aus ICMP-Fehlern, die zu mir zurückreisen. Blockier die, und die ganze Diagnose wird dunkel — und eine Menge anderes auch.
ICMP pauschal zu blockieren wird mancherorts immer noch als Sicherheitshaltung behandelt. Verteidigbar ist sie seit den 1990ern nicht mehr. Die Angriffe, die sie stoppen soll, waren Ping of Death und Smurf, beide in den Stacks behoben statt am Rand, und beide behoben, bevor manche der Ingenieure, die den Rat noch wiederholen, geboren waren. Was pauschales Blockieren heute stoppt, ist Diagnose. Sonst nichts.
RFC 1812 lässt an dem Punkt keinen Raum für Interpretation: Time Exceeded ist ein MUST, und der Standard nennt als Grund, dass Traceroute davon abhängt. Verwirf es, und du hast ein Werkzeug kaputtgemacht, das das eigene Router-Anforderungsdokument des Internets als Rechtfertigung für die Existenz der Nachricht benennt.
Path MTU Discovery ist die teure. Sie braucht ICMP 3/4, fragmentation needed, zurück zum Absender. Filter das, und du bekommst den Fehler, den jeder Netzwerk-Ingenieur mindestens einmal gejagt hat, den, bei dem der Handshake fertig wird und kleine Übertragungen klappen und alles, was ein volles Paket trägt, für immer hängt. SSH verbindet und scp stockt. Die Seite lädt und das Bild kommt nie an. Nichts in den Logs. Nichts, wonach man greppen könnte.
Auf IPv6 hört es auf, Geschmackssache zu sein. RFC 4890 §4.3.1 listet die Nachrichten, die eine Firewall nicht verwerfen darf:
- 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
und zu Packet Too Big ist es unverblümt über die Folge: „Effectively, parts of the Internet will become inaccessible.“ IPv6-Router fragmentieren nicht. Wenn Packet Too Big den Absender nicht erreicht, gibt es keinen Erholungspfad.
Die Kontrolle ist ein Rate-Limit, kein Drop. Erlaube eingehend Typ 3 und Typ 11, zähl sie, deckle sie bei so etwas wie hundert pro Sekunde, logge, was den Deckel überschreitet, und du hast die Diagnose behalten, Path MTU Discovery am Laufen gehalten und jedes bisschen des Schutzes behalten, den die pauschale Regel überhaupt zu bieten schien. Zu beschränken, wie viel von etwas du annimmst, ist eine Kontrolle. Alles davon abzulehnen und das Härtung zu nennen, ist bloß, sich der Messung zu verweigern.
Für alle, die einen MSP betreiben: Wenn die Leitung deines Kunden ICMP-Fehler verwirft, hast du ihm die Fähigkeit genommen, zu beweisen, in welchem Netz ein Fehler steckt — und dir selbst auch. Wenn das nächste Mal ein Fehler zwischen zwei Providern sitzt, die beide sagen, es sei sauber, ist das die Rechnung für die Policy.
Außer Echo. Das verwerfe.
Alles oben handelt von ICMP-Fehlern. Echo ist ein anderes Tier, und es ist der eine Teil des Protokolls, den ich am Rand von der Leitung nehmen würde.
Sieh dir an, was der Standard von ihm verlangt. In IPv4 sagt RFC 792 über einen Echo-Request, dass „the data received in the echo message must be returned in the echo reply message“. IPv6 ist noch unverblümter — RFC 4443 definiert das Feld als „zero or more octets of arbitrary data“ und verlangt dann, dass es „MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message“.
Lies das als Angreifer statt als Betreiber. Der Standard verpflichtet jeden Host auf der Erde, einen Block Bytes deiner Wahl anzunehmen und ihn dir direkt zurückzureichen. Das ist kein Nebeneffekt. Es ist ein vorgeschriebener, bidirektionaler Kanal für beliebige Nutzlast, der über ein Protokoll läuft, das die meisten Firewalls ohne Prüfung durchlassen und das die meiste Protokollierung als Paketzahl statt als Inhalt festhält.
Leute bauen seit dreißig Jahren Tunnel darauf. Loki tat es 1996 in Phrack 49. Ptunnel trägt eine volle TCP-Sitzung in Ping und ist seit zwei Jahrzehnten nur ein Paket entfernt. Wenn deine Egress-Policy „alles blockieren, ICMP erlauben, weil das Netzwerk-Team es braucht“ ist, hast du keine Egress-Policy. Du hast ein VPN mit Extra-Schritten, und der Verkehr geht hinaus und sieht aus wie jemand, der testet, ob das Internet läuft.
RFC 4890 widerspricht mir, und es ist es wert, das klar zu sagen, statt nur die Hälfte zu zitieren, die passt. §4.3.1 setzt Echo Request und Echo Response in dieselbe Nicht-verwerfen-Liste wie die Fehler. Dann lies die Begründung, die es gibt:
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.
Der angegebene Grund, Echo offen zu halten, ist, dass jemand damit einen Tunnel durch deine Firewall bauen muss. Das ist mein Argument, aufgeschrieben von den Leuten, die den Gegenstandpunkt vertreten.
Die Policy ist also schmal, nicht pauschal:
# 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
Auf IPv6, trag dieses Muster nicht auf eine Link-Local- oder Host-Chain, ohne Neighbour Discovery zu behalten. Die Typen 133 bis 137 — nd-router-solicit bis nd-redirect — sind, wie IPv6 die Aufgabe erledigt, die ARP in IPv4 erledigt. Verwirf die, und das Segment hört innerhalb von Minuten auf zu funktionieren, und es sieht nicht wie ein Firewall-Fehler aus. Filter Echo am Rand, nicht auf der Leitung zwischen einem Host und seinem eigenen Router.
Was kostet dich das? ping über die Grenze, und sonst nichts. Alles in diesem Beitrag funktioniert weiter, denn nicht eine Messung hier sendet einen Echo-Request. hopfind.py läuft TCP und UDP und liest die Fehler, die zurückkommen; traceroute -T und -U tun dasselbe. Path MTU Discovery braucht Packet Too Big, was ein Fehler ist. Der Rückwärts-TTL-Trick funktioniert auf jeder Antwort, und ein TCP-Handshake gibt dir eine. Das Einzige, was du verlierst, ist das am wenigsten aussagekräftige Werkzeug im Kasten, und dieser ganze Beitrag ist ein Argument dafür, warum bei ping aufzuhören überhaupt das Problem ist.
Meine eigene Leitung tut schon genau das, obwohl ich bezweifle, dass es Absicht war. ping zu meinem Standard-Gateway bekommt 100 % Verlust, und ICMP Time Exceeded von genau diesem Gateway kommt in 0,3 ms zurück — wie jeder Trace in diesem Beitrag zeigt. Echo zu, Fehler offen. Wer auch immer diese Firmware ausgeliefert hat, hatte die richtige Antwort, und ich fand nur beim Nachsehen heraus, dass er sie hatte.
Dieselbe Regel, einmal geschrieben
Hier der Fehler, den ich in meinem eigenen Haus nicht zu finden erwartete. Ein öffentlicher DNS-Resolver, auf TCP/443 über beide Familien gelaufen, Minuten auseinander.
$ 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.
Gar nichts. Nicht ein Hop. Mein eigener Router meldete nicht einmal den Ablauf, den er erzeugt haben muss, denselben Ablauf, den er in 0,3 ms für jeden anderen Lauf in diesem Beitrag meldete — der Drop passiert also bei Hop 1, bevor die Hop-Limit-Prüfung je läuft, und Hop 1 ist meiner.
Der Pfad ist in Ordnung, was dasselbe Ziel auf UDP beweist:
$ 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 *
Sieben Hops sauberer Antworten an dieselbe Adresse. Es ist also nicht das Routing und es ist nicht das Ziel — etwas auf meiner Leitung verwirft TCP zu diesem Host und lässt UDP zu ihm durch.
Dann derselbe Resolver, derselbe Port, über 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.
Glatt durch. Sieben Hops, kein Drama. Derselbe Dienst, derselbe Port, dieselbe Absicht, und die Regel existiert nur auf einer Adressfamilie.
Wer auch immer diese Regel schrieb, schrieb sie für IPv4 und schrieb nie den Zwilling. Ich habe keine Ahnung, was sie erreichen sollte — meine Vermutung ist irgendetwas darüber, DNS lokal zu halten — aber was auch immer es war, sie erreicht es auf der Hälfte des Verkehrs, solange diese Leitung IPv6 hat. Wenn sie aus einem Grund da war, funktioniert sie nicht. Wenn nicht, sollte sie nicht da sein.
Das ist die Alltagsversion des Dual-Stack-Problems, und es ist weit häufiger als die Debatten darüber, ob man IPv6 überhaupt ausrollen soll. Zwei Regelwerke. Eins gepflegt.
Das ganze Skript
Keine Abhängigkeiten, keine Installation, nichts als die Standardbibliothek. Python 3.6 oder neuer, und auf Linux gar keine Privilegien.
Die zwei Klassen sind die ganze Portabilitäts-Geschichte. ErrorQueue ist der Linux-Weg: IP_RECVERR auf dem Socket scharfschalten, und nachdem die Probe scheitert, MSG_ERRQUEUE lesen und die Adresse des Routers aus der sock_extended_err-Struktur ziehen, an die der Kernel sie anhängt. RawIcmp ist überall sonst: einen Raw-ICMP-Socket öffnen, lesen, was ankommt, die Quelladresse vom Paket nehmen. Das erste braucht nichts, das zweite braucht Root, und dem Rest des Programms ist egal, welches ihm gereicht wurde.
Ein Detail ist es wert, hervorgehoben zu werden, weil es der Unterschied zwischen einer richtigen Antwort und einer plausiblen ist. In probe() wird die Error-Queue geleert, bevor SO_ERROR befragt wird. Ein ICMP-Fehler erreicht einen TCP-Socket als schlichter errno — ICMP 3/3 port unreachable kommt als ECONNREFUSED an, genau wie ein echter Reset — also hätte SO_ERROR zuerst zu prüfen den gefälschten SMTP-Reset weiter oben in diesem Beitrag als ehrliche Ablehnung vom fernen Ende gemeldet. Lies zuerst die Queue, und ee_origin sagt dir, dass ein Router gesprochen hat.
#!/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()
Was dir das nicht sagen kann
Die Methode ist billig und über die meisten Dinge ehrlich, aber sie ist kein Topologie-Scanner. Sei im Ticket ehrlich darüber, was du wirklich gemessen hast.
Verschiedene Fünf-Tupel können verschiedene Pfade nehmen. ECMP hasht die Quell- und Zielports in die Wahl des nächsten Hops, also traversieren zwei Läufe auf zwei verschiedenen Ports gar nicht garantiert dieselben Router, was eins der zwei Dinge ist, die erklären könnten, warum Hop 5 oben einen Lauf beantwortet und beim anderen still bleibt. Wiederhol beide Läufe. Eine Grenze, die sich bewegt, ist unbewiesen.
MPLS versteckt Hops. Ein Label-Switched-Core kann sich als ein Hop präsentieren, oder als gar keiner. Jede Zählung über den Backbone von jemand anderem ist eine Untergrenze.
ICMP-Erzeugung ist fast überall raten-limitiert. Untersuche schneller, als der Router antwortet, und du fertigst deine eigenen Sterne an. hopfind.py sendet eine Probe pro Hop und wartet; das ist Absicht.
Der Rückweg muss nicht dem Hinweg entsprechen. Die Hop-Zahl hinaus ist nicht die Hop-Zahl zurück, und Rückwärts-TTL-Arithmetik misst nur die Rückstrecke.
Anycast heißt, der Host bei Hop N ist vielleicht nicht zweimal dieselbe Kiste. Besonders öffentliche Resolver und CDNs.
Ein fertiger Handshake heißt nicht, dass die Sitzung überlebt. Eine zustandsbehaftete Firewall kann den SYN erlauben und verwerfen, was bei der Prüfung folgt. Wenn die Verbindung öffnet und dann stirbt, ist das das falsche Instrument — geh und zeichne auf.
Du hast das erste Gerät gefunden, das verwirft, nicht das, zu dem sich jemand bekennt. In einem CGN oder einem Carrier-Netz kann die Adresse bei Hop N+1 eine von mehreren Kisten hinter einer Adresse sein. Es ist trotzdem das Richtige zum Zitieren, weil es eine Tatsache über den Pfad ist.
Wofür es eigentlich da ist
Das Zurückspringen beenden. Das ist der ganze Ertrag der Übung.
„Port 445 ist irgendwo blockiert“ ist eine Einladung, das Ticket zurückzugeben. Das hier nicht:
TCP/445 zu 192.0.2.4 wird bei Hop 11 still verworfen, Adresse 192.0.2.31. Hop 11 antwortet ICMP Time Exceeded auf TCP/443 auf demselben Pfad in 26 ms und antwortet auf 445 gar nicht, der Drop ist also eine Policy-Entscheidung auf diesem Gerät, kein Routing-Fehler. Zehn Hops davor sind sauber. Viermal über zwanzig Minuten reproduziert, aus einer unprivilegierten Shell, Skript beigefügt.
Das gibt niemand zurück. Es benennt ein Gerät, sagt, was es tat, sagt, was es nicht tat, und zeigt den Rechenweg. Ob sie es ändern, ist immer noch ihre Entscheidung. Aber die Woche Ping-Pong ist vorbei, und es hat vier Befehle gebraucht.
Alles darin kam aus einem 8-Bit-Feld, das 1981 als Timer spezifiziert wurde, nie ein einziges Mal als solcher benutzt wurde und leise aus „irgendwo“ eine Adresse macht.
Es lohnt sich, es lesen zu lernen.