TCP/IP-Stack programmieren 1: Ethernet und ARP (2016)
(saminiir.com)- Als erster Schritt einer Reihe, in der man unter Linux einen TCP/IP-Stack im User Space selbst baut und dabei Netzwerkschichten lernt, werden zunächst die Verarbeitung von Ethernet-Frames auf Layer 2 und ARP-Antworten implementiert
- Low-Level-Netzwerkverkehr des Kernels wird über ein TAP-Gerät abgegriffen; über den zurückgegebenen Dateideskriptor lassen sich Ethernet-Puffer des virtuellen Geräts per
read/writelesen und schreiben - Der Ethernet-Header wird über
dmac,smac,ethertypeundpayloadbehandelt; ist derethertype-Wert 1536 oder größer, steht er für den Payload-Typ, andernfalls für die Payload-Länge - ARP ordnet Protokolladressen wie IPv4-Adressen dynamisch 48-Bit-MAC-Adressen zu und wird als Ablauf implementiert, der auf Anfragen antwortet und dabei die Übersetzungstabelle aktualisiert
- Im
arping-Test erkennt der Linux-Kernel die ARP-Antwort des eigenen Stacks, sodass im ARP-Cache des Interfacestap0ein Eintrag für10.0.0.4hinzugefügt wird
Ausgangspunkt für einen TCP/IP-Stack im User Space
- Ziel ist es, unter Linux einen minimalen TCP/IP-Stack im User Space zu implementieren und dabei Netzwerk- und Systemprogrammierung tiefer zu verstehen
- TCP ist komplex, weil sich über mehr als 30 Jahre viele Spezifikationen angesammelt haben; die zentralen Implementierungsbestandteile lassen sich jedoch auf TCP-Header-Parsing, Zustandsmaschine, Congestion Control und Berechnung von Retransmission-Timeouts eingrenzen
- Ethernet und IP sind weniger komplex als TCP, daher beginnt die Reihe mit der Implementierung auf Layer 2
Ethernet-Traffic über ein TAP-Gerät empfangen
- Um Low-Level-Netzwerkverkehr des Linux-Kernels abzufangen, wird ein Linux-TAP-Gerät verwendet
- TUN/TAP-Geräte werden häufig von Networking-Anwendungen im User Space genutzt, um L3- bzw. L2-Traffic zu manipulieren
- Tunneling verpackt Pakete als Payload in anderen Paketen
- Auch Programme wie OpenVPN verwenden TUN/TAP-Geräte
- Da der Netzwerk-Stack ab Layer 2 aufgebaut wird, wird kein TUN-, sondern ein TAP-Gerät benötigt
- Ein TAP-Gerät wird erstellt, indem
/dev/net/tapgeöffnet undioctl(fd, TUNSETIFF, ...)aufgerufen wirdIFF_TAPwählt ein TAP-Gerät ausIFF_NO_PIverhindert, dass vor Ethernet-Frames unnötige Paketinformationen eingefügt werden
- Über den nach der Erstellung zurückgegebenen Dateideskriptor
fdwerden die Ethernet-Puffer des virtuellen Geräts gelesen und geschrieben
Format von Ethernet-Frames
- Ethernet ist eine Basistechnologie zur Verbindung von Computern in LANs; der erste Ethernet-Standard wurde 1980 von Digital Equipment Corporation, Intel und Xerox veröffentlicht
- Die erste Version nutzte eine Geschwindigkeit von etwa 10 Mb/s und Halbduplex-Kommunikation, daher war ein MAC-Protokoll zur Steuerung des Datenflusses nötig
- Bei Halbduplex-Ethernet-Interfaces ist CSMA/CD als MAC-Verfahren erforderlich
- 100BASE-T nutzt Twisted-Pair-Verkabelung und ermöglicht Vollduplex-Kommunikation sowie höheren Durchsatz
- Mit der weiten Verbreitung von Ethernet-Switches nahm die Notwendigkeit von CSMA/CD im Allgemeinen ab
- Der Ethernet-Standard wird von der Arbeitsgruppe IEEE 802.3 gepflegt
Der in der Implementierung verwendete Ethernet-Header
- Die Implementierung bindet unter Linux
if_ether.hein und verwendet die Zuordnung von Ethertypes zu Hexadezimalwerten - Der Ethernet-Header wird in einer C-Struktur durch folgende Felder dargestellt
dmac: Ziel-MAC-Adressesmac: Quell-MAC-Adresseethertype: Payload-Länge oder -Typpayload: Payload-Zeiger, der ARP- oder IPv4-Pakete enthält
ethertypeist ein 2-Oktett-Feld, dessen Bedeutung vom Wert abhängt- Ist der Wert 1536 oder größer, bezeichnet er einen Payload-Typ wie IPv4 oder ARP
- Ist der Wert kleiner, bezeichnet er die Payload-Länge
- Ethernet-Frames können Tags enthalten, die VLAN oder QoS angeben; in dieser Implementierung werden Frame-Tags jedoch ausgeschlossen
- Wenn die Payload-Länge ohne Tags kleiner als die geforderte Mindestgröße von 48 Byte ist, werden am Ende Padding-Bytes angefügt
- Am Ende des Ethernet Frame Format gibt es ein Frame-Check-Sequence-Feld, das die Integrität per CRC prüft; in dieser Implementierung wird es jedoch nicht verarbeitet
Vorgehen beim Parsen von Ethernet-Frames
- Das Attribut
packedin der Strukturdeklaration verhindert, dass der GNU-C-Compiler das Speicherlayout der Struktur durch Padding-Bytes zur Datenausrichtung optimiert - Das Parsing in der Implementierung erfolgt, indem ein Puffer per Type Cast in die passende Protokollstruktur umgewandelt wird
- Beispiel:
struct eth_hdr *hdr = (struct eth_hdr *) buf;
- Beispiel:
- Ein portablerer Ansatz wäre, Protokolldaten manuell zu serialisieren
- In diesem Fall kann der Compiler Padding-Bytes entsprechend den prozessorspezifischen Anforderungen an die Datenausrichtung hinzufügen
- Die Verarbeitung empfangener Ethernet-Frames folgt einem einfachen Ablauf
- Den Puffer vom TAP-Gerät lesen
- Den Ethernet-Header mit
init_eth_hdr(buf)initialisieren handle_frame(&netdev, hdr)entscheidet anhand desethertype-Werts über die nächste Aktion
ARP-Paketstruktur und Rolle
- ARP (Address Resolution Protocol) ordnet Protokolladressen wie IPv4-Adressen dynamisch MAC-Adressen zu, also 48-Bit-Ethernet-Adressen
- ARP ist nicht auf IPv4 beschränkt und kann mit verschiedenen L3-Protokollen verwendet werden
- Als Beispiel deklariert CHAOS 16-Bit-Protokolladressen
- Bei typischer LAN-Kommunikation ist zwar die IP-Adresse eines Dienstes bekannt, für die tatsächliche Übertragung wird aber die MAC-Adresse benötigt
- ARP sendet eine Broadcast-Anfrage ins Netzwerk, damit der Besitzer der betreffenden IP-Adresse seine Hardwareadresse bekanntgibt
ARP-Header und Payload für IPv4
- Der ARP-Header besteht aus folgenden Feldern
hwtype: 2-Oktett-Feld für den Link-Layer-Typ; der Wert für Ethernet ist0x0001protype: 2-Oktett-Feld für den Protokolltyp; der Wert für IPv4 ist0x0800hwsize: 1-Oktett-Feld für die Größe der Hardwareadresse; eine MAC-Adresse hat 6 Byteprosize: 1-Oktett-Feld für die Größe der Protokolladresse; eine IPv4-Adresse hat 4 Byteopcode: 2-Oktett-Feld für den Typ der ARP-Nachricht
opcode-Werte werden in vier Arten unterteilt- ARP request:
1 - ARP reply:
2 - RARP request:
3 - RARP reply:
4
- ARP request:
- ARP-Daten für IPv4 werden mit der Struktur
arp_ipv4behandeltsmac: MAC-Adresse des Senderssip: IP-Adresse des Sendersdmac: MAC-Adresse des Empfängersdip: IP-Adresse des Empfängers
Algorithmus zur Adressauflösung und Cache
- Der Algorithmus zur Adressauflösung aus RFC 826 prüft Hardware- und Protokolltyp, aktualisiert anschließend die Übersetzungstabelle und erzeugt eine Antwort, wenn die Zieladresse die eigene ist
- Die translation table speichert ARP-Ergebnisse, damit ein Host bereits vorhandene Einträge aus dem Cache nachschlagen kann
- Dieser Cache reduziert, dass das Netzwerk unnötig durch doppelte ARP-Anfragen gefüllt wird
- Der Implementierungscode befindet sich in
arp.c
ARP-Antwort testen und nächste Schritte
- Der abschließende Test der ARP-Implementierung prüft, ob auf Anfragen korrekt geantwortet wird
- Nach Ausführung von
arping -I tap0 10.0.0.4kommt von10.0.0.4eine Unicast-Antwort mit der MAC-Adresse00:0C:29:6D:50:25zurück - Anschließend zeigt die Ausgabe von
arp, dass im ARP-Cache des Linux-Kernels der Eintrag10.0.0.4 ether 00:0c:29:6d:50:25 tap0entstanden ist - Schon mit minimaler Ethernet-Frame-Verarbeitung und ARP-Implementierung lässt sich beobachten, dass ein eigenes Ethernet-Gerät den ARP-Cache des Linux-Hosts füllt
- Der Quellcode des Projekts ist auf GitHub verfügbar; der nächste Schritt ist die Implementierung von ICMP echo/reply, also ping, sowie des Parsings von IPv4-Paketen
1 Kommentare
Meinungen auf Hacker News
Vor ein paar Jahren habe ich in C einen Netzwerk-Stack im Userspace gebaut und Rohpakete über ein TUN-Interface verarbeitet, sodass er einigermaßen funktionierte.
Inzwischen enthält er eine einfache Shell, mit der man IP-Adressen, Routen usw. konfigurieren kann, und Netzwerkpakete werden in einer hybriden Struktur abgelegt, die wie eine Mischung aus mbuf und sk_buf wirkt.
Allerdings hatte ich nach der UDP-Implementierung weder Zeit noch Motivation, TCP umzusetzen; der Code ist hier: https://github.com/cakturk/unet
Natürlich war es vermutlich eines der langsamsten und fragilsten Dinge der Geschichte, aber es funktionierte tatsächlich und machte ziemlich viel Spaß. Ich hoffe, der Code liegt noch irgendwo herum.
Auch der „POSIX-Port“ von lwip holt sich auf dieselbe Weise rohe Ethernet-Bytes von einem TUN/TAP-Gerät.
https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
Wenn man einen minimalen Linux-Kernel ohne TCP/IP-Stack kompiliert, ist er 400 KB groß; mit TCP/IP-Stack werden daraus 800 KB.
In einem Projekt, das nur Temperaturwerte senden musste, habe ich die Werte in selbst gebaute UDP-Nachrichten aus einem kleinen C-Programm im Userspace gepackt und so viel Platz und Komplexität sparen können.
Wenn man ARP deaktiviert, kann man mehreren Servern im selben Netzwerk dieselbe IP zuweisen.
Wenn ein Server als Routing-Frontend Pakete anhand der MAC-Adresse an die Netzwerkschnittstelle eines Backend-Servers weiterleiten kann, erkennt dieses Backend sich als Ziel und kann direkt dem Client antworten, indem es Quell- und Ziel-IP vertauscht. Das Routing-Frontend wird dabei nicht erneut durchlaufen.
Alternativ kann man auch, ohne ARP abzuschalten, die gemeinsame IP-Adresse als Alias auf dem Loopback-Interface hinzufügen und denselben Effekt erzielen; das Backend erkennt sich dann als Ziel und vermeidet ARP-Konflikte. Das war ein Trick, den der IBM-WebSphere-Software-Load-Balancer in den 90er- bis 00er-Jahren nutzte.
Der Vorteil gegenüber dem weiter verbreiteten L3-Load-Balancing ist, dass der IP-Paket-Header nicht neu geschrieben werden muss.
Wenn man diese Methode verwendet, sollte man daher ein dediziertes VLAN anlegen.
Ich habe etwas Ähnliches einmal in Python gemacht: https://github.com/georgek/notebooks/blob/master/internet.ip...
Die Codequalität ist wahrscheinlich schlechter, und ehrlich gesagt habe ich mir den Algorithmus zur Adressauflösung einfach ausgedacht. Bis zum Pingen von Internet-Hosts per ICMP bin ich gekommen.
Mir gefällt, dass alles vollständig in ein kurzes Notebook passt. Der Originalartikel lässt im Text viele Details weg, die im referenzierten größeren Quellcode stecken.
Ich hatte diesen Artikel nicht gesehen und habe es nur anhand von Wikipedia gebaut. Ab TCP steigt die Komplexität allerdings stark an, wodurch mein Interesse etwas nachließ. Da Teil 3 diesen Bereich behandelt, lese ich ihn vielleicht irgendwann und bringe es zu Ende. Wenn man sich für Networking interessiert, halte ich das für eine lohnende Aufgabe für Programmierer auf jedem Niveau.
Vor ein paar Jahren habe ich an Instrumentierung für Kernkraftwerke gearbeitet. Die clientseitige Entwicklung lief auf Sun-Workstations, und tatsächlich wurde ich wegen meiner TCP/IP-Erfahrung eingestellt; diese Erfahrung hatte ich in CMUs Kurs „Betriebssysteme“ gesammelt.
Die Computer im Kraftwerk waren dagegen Minicomputer ohne TCP/IP-Stack, daher musste das Team den Stack selbst bauen.
Etwa eine Minute nach Beginn des Artikels heißt es: „dmac und smac sind ziemlich selbsterklärende Felder“; Leser, die nicht wissen, was das ist, könnten an dieser Stelle sofort aussteigen.
Man denkt dann: „Dieser Artikel ist offenbar für Leute gedacht, für die diese Felder selbsterklärend sind. Er ist nicht für mich, also sollte ich aufhören zu lesen.“
Außerdem ist es bei einem Artikel über das Bauen eines Netzwerk-Stacks durchaus vertretbar anzunehmen, dass die Leser ein gewisses Maß an Networking-Wissen mitbringen.
Verwandte Beiträge:
Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - Juni 2021, 49 Kommentare
Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - Juni 2018, 47 Kommentare
Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - Juli 2017, 30 Kommentare
Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - März 2016, 49 Kommentare
Ich weiß nicht, woher der Autor die IP-Adresse 10.0.0.4 nimmt, die er für den ARP-Auflösungstest verwendet
Wessen Adresse soll das sein? Ist es ein Fake-Gerät, das von dem hier erstellten Fake-Ethernet-Gerät aus erreichbar ist, oder ein tatsächlich im Netzwerk des Autors vorhandenes Gerät?
Ein TAP-Gerät ist so etwas wie ein per Software emulierter Ethernet-Link. Wenn man Pakete dorthin sendet, werden sie direkt an ein User-Level-Programm weitergereicht; dieses Programm entscheidet, welche IP-Adresse es haben soll und wie es auf ARP antwortet
Normalerweise übernimmt das Betriebssystem solche Dinge, und um einem Interface eine IP-Adresse hinzuzufügen, braucht man Root-Rechte. Dasselbe gilt für das Öffnen eines TAP-Geräts. Networking funktioniert im Allgemeinen kooperativ, und ein böswilliger Akteur mit Root-Rechten im Netzwerk kann Unfug treiben
Soweit ich mich erinnere, funktioniert ARP nur im lokalen Segment. Der Router trägt seine eigene Adresse ein und leitet das Paket weiter
Außerdem gibt es auch RARP, eine Möglichkeit, das „Netzwerk“ nach der eigenen IP-Adresse zu fragen. Ich weiß nicht, ob RARP heute noch in realen Umgebungen funktioniert