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.

Post a comment

Related content