- IPv4-NAT unterscheidet Antwortziele normalerweise über TCP/UDP-Ports, doch beim ICMP Echo von
pinggibt es keine Ports – entscheidend ist daher, welchen Wert Linux als Mapping-Schlüssel verwendet - Das Experiment bildet mit Netzwerk-Namespaces
client1,client2,natboxundservernach und reproduziert eine Konfiguration, in der mitiptablesMASQUERADE von 192.168.99.0/24 nach 10.0.100.0/24 genattet wird - Der Abgleich von RFC 792 mit Paketmitschnitten zeigt, dass Identifier und Sequence Number von ICMP Echo zum Abgleich von Anfrage und Antwort verwendet werden; im ICMP-
SOCK_DGRAM-Pfad von Linux landet der lokale Port des Sockets als ID im Paket - Wenn zwei Clients dieselbe ICMP-ID
999verwenden, ändert netfilter zur Vermeidung von Kollisionen bei einem von beiden die ICMP-ID auf einen Zufallswert und stellt in der Antwort die ursprüngliche Client-IP und ID wieder her - Linux-NAT speichert auch bei portlosem ICMP den Zustand von Original- und Antwortrichtung im conntrack-Tupel und nutzt die ICMP-ID als manipulierbaren Schlüssel, um Antworten dem korrekten internen Host zuzuordnen
Experimentumgebung und NAT-Konfiguration
- Um mehrere Geräte auf einer einzelnen Linux-Maschine zu simulieren, werden Netzwerk-Namespaces verwendet
- Zwei Clients, eine
natboxals NAT-Router und ein Server werden jeweils in eigenen Namespaces angelegt; das private Netz und das serverseitige Netz werden getrenntclient1:192.168.99.1/24client2:192.168.99.2/24- interne Schnittstelle von
natbox:192.168.99.3/24 - externe Schnittstelle von
natbox:10.0.100.1/24 server:10.0.100.2/24
- Auf einer Fedora-38-Server-VM mit Linux-Kernel 6.2.9 werden
ip,iptables,tcpdumpusw. als root ausgeführt - Die beiden Clients sind mit der Bridge
br0verbunden;natboxist jeweils mit der Bridge und mit einem serverseitigen veth pair verbunden - Die Default-Route der Clients wird auf
192.168.99.3gesetzt, sodass Traffic zum Server übernatboxläuft - Auf
natboxwird mitnet.ipv4.ip_forward=1das Paket-Forwarding aktiviert und in derPOSTROUTING-Chain dernat-Tabelle voniptableseine MASQUERADE-Regel hinzugefügtip netns exec natbox iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
ICMP-NAT im Paketmitschnitt
- In den Namespaces
client1undserverwerden mittcpdump -n icmpICMP-Pakete mitgeschnitten - Auf Client-Seite sind ein Echo Request
192.168.99.1 > 10.0.100.2und ein Echo Reply10.0.100.2 > 192.168.99.1zu sehen - Auf Server-Seite ist die Quell-IP derselben Anfrage auf
10.0.100.1geändert, womit bestätigt ist, dass NAT die Quelladresse auf die externe IP dernatboxumgeschrieben hat - ICMP-Anfragen verschiedener Clients haben unterschiedliche id-Felder
- Im Beispiel hat
client1die ID31428 client2hat die ID33391
- Im Beispiel hat
- Diese Beobachtung zeigt, dass
natboxdas ID-Feld nutzen kann, um ICMP-Antworten an interne Clients zurückzuleiten
RFC 792 und die ICMP-ID von ping
- ICMP ist ein altes Protokoll, das in dem 1981 veröffentlichten RFC 792 definiert wurde
- ICMP-Echo- und Echo-Reply-Nachrichten enthalten Type, Code, Checksum, Identifier, Sequence Number und Data
- Type unterscheidet Echo Request und Echo Reply
- Der Type eines Echo Request ist
8 - Im zitierten RFC-Abschnitt ist Echo Reply als
1angegeben - Code ist
0
- Der Type eines Echo Request ist
- RFC 792 erklärt, dass Identifier und Sequence Number zum Abgleich von Echo Request und Reply verwendet werden können
- Der Identifier kann ähnlich wie ein TCP/UDP-Port zur Sitzungsidentifikation genutzt werden, und die Sequence Number kann mit jedem Echo Request erhöht werden
- Der RFC schreibt nicht vor, wie die ID konkret gewählt wird; daher muss der Quellcode der Implementierung
pinggeprüft werden
Wie die ID in iputils ping festgelegt wird
- Der Befehl
pingist im Paket iputils enthalten - Ein Kommentar in der Nähe von
ping4_send_probeerklärt, dass beim Erzeugen eines ICMP Echo Request das ID-Feld eine Zufallszahl ist und die Sequence Number eine aufsteigende Ganzzahl - Intern enthält
pingdas Feldidentinstruct ping_rts- Der Standardwert ist
-1 - Mit der CLI-Option
-ekann er mit einem Wert zwischen0undIDENTIFIER_MAX, also0xFFFF, überschrieben werden
- Der Standardwert ist
- Wenn
rts->ident == -1ist, bindetpingeinen Socket vom TypSOCK_DGRAMmit dem ProtokollIPPROTO_ICMP - Laut der Beschreibung von
IPPROTO_ICMP-Sockets unter Linux wird der ICMP-Header beisend()geprüft und bereinigt; die id wird auf die lokale Portnummer des Sockets gesetzt - Wenn
pingkeinen Source-Port angibt, wählt der Linux-Kernel offenbar zufällig einen freien Port, und dieser Port wird als ID des ICMP-Pakets verwendet
Wenn dieselbe ICMP-ID kollidiert
- Auf zwei Clients wird
ping -e 999verwendet, um den Server mit derselben ICMP-ID 999 anzupingen - Der Mitschnitt auf dem Server zeigt: Die Anfrage eines Clients behält ID
999, während die Anfrage des anderen Clients auf ID30218geändert wird - Das NAT-Gerät ändert bei einem der beiden die ID, damit die Kombination aus gleicher externer IP und gleicher ICMP-ID nicht kollidiert
- Um die Stelle der Kollisionsbehandlung zu finden, wird im Linux-Verzeichnis
net/netfilternach Code gesucht, der das ICMP-id-Feld verwendet
Rolle von netfilter, conntrack und NAT
- Das Kernel-Subsystem, das
iptables-Regeln implementiert, ist netfilter - Da die MASQUERADE-Regel NAT ausführt, befindet sich auch die ICMP-NAT-Implementierung in netfilter
nf_nat_setup_infoinnf_nat_core.cruftget_unique_tupleauf, was wiederum zunf_nat_l4proto_unique_tupleführtnf_nat_l4proto_unique_tupleenthält einen Fall fürIPPROTO_ICMPund referenzierttuple->src.u.icmp.idnf_nat_manip_pktinnf_nat_proto.cruft übernf_nat_ipv4_manip_pktundl4proto_manip_pktbei ICMPicmp_manip_pktauficmp_manip_pktschreibt mithdr->un.echo.id = tuple->src.u.icmp.iddie tatsächliche ICMP-ID in das Paket
Darstellung von ICMP in conntrack-Tupeln
- Eine connection in netfilter meint nicht nur TCP-Verbindungen, sondern auch bei verbindungslosen Protokollen wie UDP oder ICMP einen Zustand, der ausgehende und eingehende Pakete miteinander verknüpft
nf_connenthälttuplehash[IP_CT_DIR_MAX]IP_CT_DIR_ORIGINAL: Richtung ausgehender PaketeIP_CT_DIR_REPLY: Richtung eingehender Antworten
- Jeder
nf_conntrack_tuple_hashbesitzt einnf_conntrack_tuple, das die connection identifiziert - Das Tupel ist in manipulierbares
srcund unveränderlichesdstunterteiltsrcenthält die IP-Adresse und protokollspezifische Felder- Das protokollspezifische Feld für ICMP ist
__be16 id dstenthält die unveränderte IP-Adresse sowie ICMP-typeundcode
- NAT speichert in der connection, wie es ausgehende Pakete verändert hat, und macht diese Änderung bei Antwortpaketen rückgängig
Codepfad zur Auswahl der ICMP-ID
- Wenn
natboxein ICMP Echo empfängt, erstelltnf_nat_setup_infoeine neue connection und entscheidet, ob Quell-IP und ICMP-ID geändert werden müssen - Anschließend setzt
nf_nat_manip_pktfür jedes ICMP-Paket je nach in der connection gespeicherten Werten die Quell- oder Ziel-IP und die ICMP-ID get_unique_tupleist der zentrale Pfad zur Auswahl eines verwendbaren NAT-Tupelsfind_best_ips_protoschreibt die Quell-IP-Adresse umnf_nat_used_tupleprüft, ob das Tupel bereits verwendet wird, und gibt das aktuelle Tupel unverändert zurück, wenn es noch nicht verwendet wird- Deshalb bleibt die ID in den genatteten Paketen erhalten, wenn die ICMP-IDs der beiden Clients unterschiedlich sind
- Ist das Tupel bereits in Verwendung, wird
nf_nat_l4proto_unique_tupleaufgerufen und protokollspezifisches NAT durchgeführt
- Bei ICMP wird
tuple->src.u.icmp.idals Schlüssel für das NAT-Ziel gewählt find_free_iderzeugt mitget_random_u16()eine zufällige ID, passt sie an den gültigen ICMP-ID-Bereich an und prüft anschließend, ob sie bereits verwendet wird- Der Standard-ID-Bereich ist der gesamte ID-Bereich; in einer
iptables-MASQUERADE-Regel kann mit--to-portsein Bereich wie100-200angegeben werden - Wenn kein unbenutztes Tupel gefunden wird, bleibt eine doppelte ID in der connection stehen, und später erkennt
__nf_conntrack_confirmdie Duplikation und verwirft das Paket
Überprüfung des Kernel-Verhaltens mit bpftrace
- Um das verstandene netfilter-Verhalten zu überprüfen, wird
bpftraceverwendet - Ziel der Kernel-Funktionsverfolgung sind
nf_nat_setup_infoundnf_nat_manip_pkt kprobeverfolgt den Zeitpunkt eines Funktionsaufrufs,kretprobeden Zeitpunkt der Funktionsrückgabe- In
kretprobekann nicht direkt auf Funktionsargumente zugegriffen werden; daher werden die Argumente beim Eintritt in einer BPF map gespeichert und bei der Rückgabe erneut gelesen struct sk_buffist die Struktur, mit der der Linux-Kernel Pakete darstelltbswapwird verwendet, um Big Endian, die Netzwerk-Byte-Reihenfolge, in Little Endian umzuwandelnntopwandelt IP-Adressen in Strings um- Dank BPF Type Format (BTF) in aktuellen Linux-Kerneln können BPF-Programme Kernel-Datenstrukturen wie
sk_buffundnf_connohne eingebundene Header referenzieren - Dieses
bpftrace-Programm wurde unter Linux-Kernel 6.2.9 getestet; bei anderen Kernel-Versionen kann sich die Funktionsfähigkeit unterscheiden
Trace-Ergebnisse und Fazit
- Wenn zwei Clients mit derselben ICMP-ID
999pingen, wirdnf_nat_setup_infofür jeden Client einmal aufgerufen - Beim ersten Client
192.168.99.1behalten sowohl original tuple als auch reply tuple die ICMP-ID999 - Beim zweiten Client
192.168.99.2wird die ICMP-ID im reply tuple auf32809umgeschrieben nf_nat_manip_pktändert beim Echo Request mitNF_NAT_MANIP_SRCdie Quell-IP auf10.0.100.1und stellt bei der Antwort mitNF_NAT_MANIP_DSTdie Ziel-IP wieder auf die ursprüngliche Client-IP zurück- Auch die ICMP-ID des Antwortpakets wird vom genatteten Wert auf den Wert wiederhergestellt, den der ursprüngliche Client gesendet hat
- Das Standard-Timeout für ICMP-conntrack lässt sich unter
/proc/sys/net/netfilter/nf_conntrack_icmp_timeoutprüfen; der beobachtete Standardwert beträgt 30 Sekunden - Wenn ein Client länger als 30 Sekunden kein Paket sendet, wird beim nächsten Ping
nf_nat_setup_infoerneut aufgerufen - Das NAT-Verhalten von Linux bei ping ist auch im Netfilter Hacking HOWTO dokumentiert; der Kern liegt in conntrack-Tupeln und dem Umschreiben der ICMP-ID
1 Kommentare
Kommentare auf Hacker News
Wenn der Server startet, beginnt er, feste ICMP-Echo-Request-Pakete an die feste Adresse 3.3.3.3 zu senden, wobei erwartet wird, dass diese Pakete nicht zurückkommen
3.3.3.3 ist weder ein erreichbarer Host noch ein Ziel zum Spoofing. Stattdessen kennt der Client beim Verbindungsaufbau die Server-IP und sendet dem Server daher ein ICMP-Time-Exceeded-Paket. In diesem ICMP-Paket steckt das „ursprüngliche“ feste Paket, das der Server an 3.3.3.3 gesendet hatte, und dieses hartkodierte Paket dient als Kennung von pwnat
Der Client gibt sich gewissermaßen als ein Hop im Internet aus und teilt mit, dass der ursprüngliche „ICMP echo request“ des Servers nicht zugestellt werden konnte. Das NAT sieht, dass das Paket im ICMP Time Exceeded zu dem vom Server gesendeten Paket passt, und leitet es an den Server hinter dem NAT weiter. Da dabei auch der vollständige IP-Header des Clients enthalten ist, kann der Server die IP-Adresse des Clients erfahren
Die Kernfunktion dieses Tools besteht danach darin, einen UDP-Tunnel zwischen Client und Server aufzubauen
Beim Überfliegen scheint es allerdings davon auszugehen, dass das NAT den UDP-Quellport nicht umschreibt, daher dürfte es nicht mit allen Routern funktionieren. STUN, wie es etwa bei WebRTC verwendet wird, implementiert ausgefeiltere Verfahren; wenn auch das nicht klappt, muss man jedoch TURN als Relay verwenden
Dasselbe Problem dürfte wahrscheinlich auch für den 3.3.3.3-Ping-Trick gelten. Wenn das NAT wie im Artikel den Ping-Identifier umschreibt, bricht dieser Trick
Wenn die Antwort eintrifft, verwendet der Router diesen eindeutigen ID-Wert, um die Antwort an das richtige Gerät im lokalen Netzwerk weiterzuleiten
Ein einzelner Computer und Wireshark/tcpdump reichen aus, um das zu prüfen
Der Artikel selbst ist gut und kann für jemanden ohne jegliches Netzwerkverständnis ein Aha-Erlebnis sein. Im Kern wirkt es aber eher wie der Weg, ein ordentliches Netzwerklabor aufzubauen und sich in den Sourcecode zu vertiefen, statt es selbst herzuleiten
Solche Beispiele, die man von Hand nachvollziehen kann, sind wirklich wertvoll, und ich habe vor, sie selbst durchzuspielen
Der einzige andere Artikel zu diesem Thema, den ich fast durchgehend gut verstanden habe, ist dieser von Tailscale. Er enthält viele „ausformulierte Beispiele“, sodass klar wird, wie alles zusammenspielt
https://tailscale.com/blog/how-nat-traversal-works/
Zufälligerweise habe ich mich dieses Wochenende mit Netfilter herumgeschlagen, um auf einem OpenWRT-Router einen transparenten Proxy zu aktivieren
Die Standardreferenzen, die man sich zu Netfilter ansehen sollte, sind https://wiki.nftables.org/wiki-nftables/index.php/Main_Page und https://www.netfilter.org/projects/nftables/manpage.html
ICMP-Echo-Requests haben jedoch eine ID, die praktisch dieselbe Rolle wie eine Quellportnummer spielt
Um ICMP-Echo korrekt per NAT zu behandeln, muss auch die ID in beide Richtungen neu gemappt werden, so wie man bei UDP den Quellport neu mappt
Denn wenn eine Maschine hinter NAT gleichzeitig von zwei Hosts angepingt wird und beide Hosts zufällig dieselbe Request-Nummer verwenden, wird es mehrdeutig
Eine andere Möglichkeit wäre, die Kennung nicht umzuschreiben, sondern eine Liste der entfernten Maschinen zu führen, die mit jeder ID verbunden sind. Wenn eine ID kollidiert, enthält die Liste mehr als eine entfernte IP-Adresse; wenn von der Maschine hinter NAT eine Antwort kommt, wählt das NAT eine davon aus der Liste aus, sendet die Antwort an diese Maschine und entfernt den Eintrag
Mit IPv6 hätte ich alle Knoten im Netzwerk ändern und auch das interne DNS aktualisieren müssen
Theoretisch könnte ich ein eigenes portables /48 haben, aber der neue ISP müsste es ankündigen, und selbst wenn der aktuelle ISP das täte, ist das nicht üblich
Als vor einer Woche die Telefonleitung ausfiel, holte ich ein 5G-MiFi hervor, legte die WAN-Verbindung dorthin um und musste auf diesem Interface lediglich ein einfaches masquerade einrichten. Wegen des schwachen Signals war es nicht toll, aber es funktionierte
Das Problem ist: Selbst mit IPv6 muss man weiterhin Dual Stack betreiben oder die schmutzige NAT-Abstraktion verwenden. Für mich bringt das keinen Nutzen, sondern nur mehr Arbeit
Bei der Arbeit ist es ähnlich. Fahrzeuge mit internen 172.16/12-Subnetzen verbinden sich miteinander und routen untereinander und sind über verschiedene VPN-Verbindungen mit der Außenwelt verbunden. Da sie oft unterirdisch geparkt werden und kaum Signal haben, ist die Architektur darauf ausgelegt, dass wenigstens eine von mehreren Methoden funktioniert
Beim Umstieg auf IPv6 müssten wir die /48er wieder umziehen. Außerdem bekommen diese Fahrzeuge ihr Internet in mehreren Sportstadien, von denen viele schon damit überfordert sind, MITM/443 abzuschalten oder UDP-Blockaden aufzuheben. In einer Umgebung, in der man samstags um 10 Uhr ankommt und zwei Stunden später alles funktionieren muss, klappt das nicht.
Ich sehe nicht, welchen geschäftlichen Vorteil es hätte, auf Dual Stack umzustellen und damit Arbeitsaufwand und Risiko zu verdoppeln.
https://en.wikipedia.org/wiki/Lindy_effect
https://stackoverflow.com/questions/31857419/how-to-send-a-m...
Leider wird ping vom Betriebssystem verarbeitet, sodass Apps auf der Peer-IP die Nachrichten nicht lesen können.
Vielleicht ist es an der Zeit, für einige dieser Dienste Hooks im Userspace bereitzustellen, damit echtes P2P möglich wird, bei dem beide Seiten hinter NAT sitzen. Wenigstens so etwas wie ein Read-only-Event-Stream. Inzwischen fühlen sich alle Barrieren dagegen künstlich an.
Allerdings habe ich schon Strategien zur Datenexfiltration oder andere Kommunikationswege gesehen, die ping verwenden. Heutzutage verwerfen die meisten Firewalls in der Standardeinstellung sämtliches ICMP, einschließlich ping, stillschweigend, daher dürfte das für P2P kaum praktikabel sein.
idscheint im Grunde(sport, dport)zu entsprechen, hat aber nur 16 Bit und damit deutlich weniger Raum als 32 Bit.Das Kernproblem beim NAT Hole Punching ist aber wohl, dass für den Aufbau der Verbindung Aktivität an beiden Enden nötig ist. Deshalb braucht man immer einen Koordinationsserver, der T mitteilt, dass Knoten S mit Knoten T sprechen möchte.
Trotzdem liefert das Stoff zum Nachdenken. Ich frage mich, ob es mit ICMP-Routing-Nachrichten wie unreachable oder TTL expired einen Weg gäbe. Wenn man traceroute auf eine IP ausführt, bekommt man Pakete von beliebigen anderen IPs zurück, und das geht im Allgemeinen durch NAT hindurch.
Man könnte sich vorstellen, dass ein Host T, der eingehende Verbindungen annehmen möchte, eine zufällige „Dummy“-IP-Adresse wählt,
(Router-IP, Dummy-IP)als seine Kennung veröffentlicht und regelmäßig Pakete an diese Dummy-IP sendet. Ein Host S, der mit T sprechen möchte, könnte ICMP-TTL-expired-Nachrichten zu dieser Dummy-Adresse an den Router von T senden, und der Router könnte sie sehen und an T weiterleiten.Natürlich hängt das davon ab, ob die IP-Adresse innerhalb der ICMP-Felder genauso per Ingress-Filtering behandelt wird wie die Adresse im IP-Header.
Edit: Es gibt bereits einen Top-Level-Kommentar, der auf eine Implementierung dieser Idee verweist.
[1] - https://github.com/yarrick/pingfs
Bei GitHub kann man zwar auf eine Kombination aus bestimmtem Commit-Hash, Dateiname und Zeilennummer verlinken, aber wenn sich die Codebasis stark verändert, ist das nicht besonders hilfreich. Bei weniger verbreiteten Git-Webviews wie git.blender.org hat das nicht gut funktioniert.
https://elixir.bootlin.com/linux/latest/source