XDP CGNAT: 1. Testbed
Mit XDP hatte ich ja schon öfter gespielt, es aber nie richtig erklärt. Es steht nun absehbar eine besondere CGNAT-Anforderung im Raum. Deswegen ist es einen Versuch wert, die Implementation komplett in XDP auf diesen UseCase zuzuschneiden.
Testbed
Eine Reihe von Nutzern (Clients) soll aus einem privaten Netz über einen Router, der dann NAT machen soll, ins Internet kommen. Die Verbindungen zwischen den Interfaces sollen über zwei untaggt Bridges eines Switches geführt werden. Die Verbindung des Switches zum Router soll getaggt sein, so dass der Router auf nur einem Interface angeschlossen wird.
+--------+ | router | +---||---+ vlan 10 || vlan 20 +-----------||------------+ | switch /\ untagged | | / \ | | / \ | | +------|-+ +-|-------+ | | | public | | private | | | +---|----+ +|||||||||+ | +-----|--------|||||||||--+ | ||||||||| +-----|----+ ||||||||`---- Client1 | internet | |||||||`----- Client2 +----------+ ||||||`------ Client3 |||||`------- Client4 ||||`-------- Client5 |||`--------- Client6 ||`---------- Client7 |`----------- Client8 `------------ Client9
Voraussetzungen
Die Entwicklung soll auf einem aktuellen Debian laufen. Dieses bringt von Haus aus nicht alle benötigten Pakete vorinstalliert mit. Wenn nötig, werde ich also angeben, welche Pakete nachzuinstallieren sind. Dabei versuche ich möglichst wenig Abhängigkeiten zu erzwingen.
Fragt man die KI nach der Einrichtung von Linux-bridges, so bekommt man eine Menge an Befehlen und Paketen vorgesetzt, die in älteren Distributionen tatsächlich benötigt wurden. Es ist etwas mühselig, aber lohnend, hier auf die aktuellen Werkzeuge zu setzen.
Die Implementierung des Testbeds soll mittels Network-Namespaces erfolgen, so dass ich ohne externe Technik und einen Satz von VMs/Containern/... auskommen kann.
Eines der Probleme bei der Erschaffung neuer Network-Stacks sind die Default-Einstellungen. Da es hier nur um IPv4 geht, möchte ich IPv6 generell ausklopfen, denn diese Pakete stören bei der Entwicklung ziemlich.
Der Switch
Der Switch ist ein Namespace mit zwei Bridges: "public" und "private":
ip netns add switch ip netns exec switch sysctl -q net.ipv6.conf.default.disable_ipv6=1 ip link add public netns switch type bridge ip link add private netns switch type bridge ip netns exec switch sh -c ' ip link set public up ip link set private up '
Der Router
Um zwischen zwei Namespaces Verbindungen zu bekommen, benutzt man veth: Das sind Interface-Paare, die in verschiedenen Namespaces liegen können.
Da diese veth immer paarweise erzeugt werden, haben die Linux-Entwicker es nicht eingesehen, getrennte Sende- und Empfangspuffer vorzuhalten. Die sendende Seite schreibt einfach in den Empfangspuffer auf der anderen Seite. Diese Optimierung wird später noch für schwere Probleme sorgen.
Für die Arbeit mit getaggten Frames, muss die MTU um die vier Byte des Vlan-Headers größer sein.
ip netns add router ip netns exec router sysctl -q net.ipv6.conf.default.disable_ipv6=1 ip link add router mtu 1504 netns switch type veth peer nat netns router mtu 1504
Die beiden Vlans werden als getrennte Interfaces durch Kernel zur Verfügung gestellt. Damit brauche ich mich nicht um das Tagging selbst kümmern, wenn ich mit IPs, Routen oder sonstiger Software arbeiten will.
ip netns exec router sh -c ' ip link add link nat nat.10 type vlan id 10 ip addr add 100.64.0.10/24 dev nat.10 ip link add link nat nat.20 type vlan id 20 ip addr add 192.168.20.254/24 dev nat.20 ip link set nat up ip link set nat.10 up ip link set nat.20 up sysctl -q net.ipv4.ip_forward=1 '
Das Ergebnis schaut dann schon ganz passabel aus:
# ip netns exec router ip route 100.64.0.0/24 dev nat.10 proto kernel scope link src 100.64.0.10 192.168.20.0/24 dev nat.20 proto kernel scope link src 192.168.20.254
Auf die gleiche Weise lasse ich den Kernel die Vlans auf dem Switch behandeln, um die ungetaggten Frames den Bridges zuzuführen.
ip netns exec switch sh -c ' ip link add link router router.10 type vlan id 10 ip link set router.10 master public ip link add link router router.20 type vlan id 20 ip link set router.20 master private ip link set router up ip link set router.10 up ip link set router.20 up '
Das Internet
Für den Anfang genügt es, eine einzelne IP zur Verfügung zu stellen.
ip netns add internet ip netns exec internet sysctl -q net.ipv6.conf.default.disable_ipv6=1 ip link add internet netns switch type veth peer downlink netns internet ip netns exec switch ip link set internet master public up ip netns exec internet sh -c ' ip addr add 100.64.0.1/24 dev downlink ip link set downlink up '
Wesentlich an diesem Knoten ist, dass er keine Route zu den privaten Netzen kennt. Wenn also dieser Knoten für die Nutzer erreichbar sein soll, dann muss der Router korrektes NAT machen.
# ip netns exec internet ip route 100.64.0.0/24 dev downlink proto kernel scope link src 100.64.0.1
Die Nutzer
Es gibt eine (skalierbare) Menge an Clients, die über die "private" Bridge des Switches zum Router gelangen und dorthin eine Default-Route zeigen lassen.
for c in $(seq 1 9); do ip netns add client$c ip netns exec client$c sysctl -q net.ipv6.conf.default.disable_ipv6=1 ip link add client$c netns switch type veth peer isp netns client$c ip netns exec switch ip link set client$c master private up ip netns exec client$c sh -c " ip addr add 192.168.20.$c/24 dev isp ip link set isp up ip route add default via 192.168.20.254 " done
Somit sieht die Routingtabelle ziemlich normal aus:
# ip netns exec client6 ip route default via 192.168.20.254 dev isp 192.168.20.0/24 dev isp proto kernel scope link src 192.168.20.6
Tests
Um die Funktionalität standardisiert zu prüfen, gibt es ein kleines Test-Script:
#! /usr/bin/bash c=$(($RANDOM%9+1)) dump=/tmp/nat-router.$$ ip netns exec switch tcpdump -enni router > $dump & echo "### Client$c pings the gateway" ip netns exec client$c ping -w 3 192.168.20.254 echo "### Client$c pings behind the gateway" ip netns exec client$c ping -w 3 100.64.0.1 echo "### Packet dump at the switch port towards the gateway" kill -1 %1 cat $dump rm $dump
Das tcpdump schneidet den Datenverkehr mit dem Router auf der Switch-Seite mit. Ein Mitschnitt auf der Router-Seite würde nicht viel bringen, denn XDP soll eingreifen, bevor der Kernel die Frames ernsthaft anschaut. Wenn also XDP ein Frame behandelt, wird ein tcpdump auf dem Router davon gar nichts sehen. Dieser frühen Behandlung von Frames ist maßgeblich für den Geschwindigkeits-Vorteil von XDP verantwortlich.
Der eigentliche Test besteht vorerst darin, einen zufälligen Nutzer zunächst das Default-Gateway pingen zu lassen und anschließend das Internet.
# tests.sh ### Client5 pings the gateway PING 192.168.20.254 (192.168.20.254) 56(84) bytes of data. 64 bytes from 192.168.20.254: icmp_seq=1 ttl=64 time=0.313 ms 64 bytes from 192.168.20.254: icmp_seq=2 ttl=64 time=0.059 ms 64 bytes from 192.168.20.254: icmp_seq=3 ttl=64 time=0.071 ms --- 192.168.20.254 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2055ms rtt min/avg/max/mdev = 0.059/0.147/0.313/0.117 ms ### Client5 pings behind the gateway PING 100.64.0.1 (100.64.0.1) 56(84) bytes of data. --- 100.64.0.1 ping statistics --- 3 packets transmitted, 0 received, 100% packet loss, time 2025ms ### Packet dump at the switch port towards the gateway 15:44:33.909912 4e:3b:d5:b9:61:4d > ca:61:e9:9b:52:84, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 192.168.20.254: ICMP echo request, id 1899, seq 2, length 64 15:44:33.909933 ca:61:e9:9b:52:84 > 4e:3b:d5:b9:61:4d, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.254 > 192.168.20.5: ICMP echo reply, id 1899, seq 2, length 64 15:44:34.933978 4e:3b:d5:b9:61:4d > ca:61:e9:9b:52:84, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 192.168.20.254: ICMP echo request, id 1899, seq 3, length 64 15:44:34.934001 ca:61:e9:9b:52:84 > 4e:3b:d5:b9:61:4d, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.254 > 192.168.20.5: ICMP echo reply, id 1899, seq 3, length 64
Im tcpdump ist schön zu sehen, wie zunächst 3x das Default-Gateway erreicht werden kann und dieses antwortet:
- Die MAC- und die IP-Adressen der Antwort sind vertauscht.
- Der ICMP-Typ wechselt von "Echo Request" zu "Echo Reply"
15:44:35.884891 4e:3b:d5:b9:61:4d > ca:61:e9:9b:52:84, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 100.64.0.1: ICMP echo request, id 1900, seq 1, length 64 15:44:35.884922 ca:61:e9:9b:52:84 > fa:92:df:ad:09:b1, ethertype 802.1Q (0x8100), length 102: vlan 10, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 100.64.0.1: ICMP echo request, id 1900, seq 1, length 64 15:44:36.885910 4e:3b:d5:b9:61:4d > ca:61:e9:9b:52:84, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 100.64.0.1: ICMP echo request, id 1900, seq 2, length 64 15:44:36.885926 ca:61:e9:9b:52:84 > fa:92:df:ad:09:b1, ethertype 802.1Q (0x8100), length 102: vlan 10, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 100.64.0.1: ICMP echo request, id 1900, seq 2, length 64 15:44:37.909994 4e:3b:d5:b9:61:4d > ca:61:e9:9b:52:84, ethertype 802.1Q (0x8100), length 102: vlan 20, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 100.64.0.1: ICMP echo request, id 1900, seq 3, length 64 15:44:37.910014 ca:61:e9:9b:52:84 > fa:92:df:ad:09:b1, ethertype 802.1Q (0x8100), length 102: vlan 10, p 0, ethertype IPv4 (0x0800), 192.168.20.5 > 100.64.0.1: ICMP echo request, id 1900, seq 3, length 64
Beim Versuch ins Internet zu gelangen sind auch jeweils zwei Frames zu sehen:
- Die "Echo Request" Anfrage, die ans Gateway gesendet wird (Vlan 20)
- Die "Echo Request" Anfrage, die das Gateway weiterleitet (Vlan 10)
- Beim zweiten Frame ist der Absender zwar die MAC des Gateways, aber im Paket steht immer noch die IP des Kunden.
- Da der Internet-Knoten keine Rück-Route hat, kann er keine Antwort senden.
Aufräumen
Zwischen den Tests sollte man aufräumen können, ohne rebooten zu müssen:
ip netns list | while read ns; do ip netns pids $ns | xargs -r kill ip netns pids $ns | xargs -r kill -9 ip netns del $ns done
Wenn Namespaces gelöscht werden, verschwinden auch die zugehörigen Interfaces. Das klappt allerdings nicht, wenn es noch einen Prozess gibt, der dieses Interface geöffnet hat. Deswegen werden erst alle Prozesse, die diesen Namespace benutzen, freundlich aufgefordert, sich zu beenden. Anschließend werden den Unfolgsamen hart die Ressoucen entzogen. So kann der Namespace entfernt werden.
Damit man das regelmäßig machen kann liegen die Kommandos in drei Scripten:
- testbed-on.sh
- tests.sh
- testbed-off.sh
Diese Scripte sind als Root auszuführen. Da die Entwicklung aber unter einen normalen Nutzer stattfindet, greife ich auf sudo zurück:
apt install sudo
Und nun geht's mit dem ersten Programm los.
