Nachdem in ein Testbed steht, geht es nun an das erste XDP-Programm. Das Augenmerk liegt zunächst auf einer funktionsfähigen Entwicklungsumgebung für solche Programme.

Mein erstes Programm

So ziemlich das kürzeste Programm, dessen Wirkung man auch bemerkt, wirft einfach alle eingehenden Frames weg. Ausnahmslos. Das sollte in jedem Fall auffallen.

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("xdp")
int nat_on_stick(struct xdp_md *ctx) {
   return XDP_DROP;
}

char _license[] SEC("license") = "GPL";

Die Includes gestatten die einfache Formulierung der zugrundeliegenden Struktur eines eBPF-Programms, das an die XDP-Schnittstelle eines Interfaces gebunden werden kann. Beim Laden von Code in den Kernel werden viele Dinge überprüft. Dazu gehört auch, dass eine passende Lizenz des Codes vorliegt, die vom Kernel akzeptiert wird. In dem Fall nehme ich die GPL.

Als Programm wird der Kernel nur Funktionen in der Section "xdp" akzeptieren, wenn dann die Bindung ans Interface erfolgen soll. Die Section gibt also an, für welchen Andockpunkt (Hook) das eBPF Programm geeignet ist. Der Name der Funktionen ist egal. Es können auch mehrere Prozeduren in einer Datei existieren, aus denen man später beim Andocken auswählt.

Eine XDP-Funktion bekommt einen Datensatz (Context) übergeben, der den aktuellen Frame, der die Bearbeitung getriggert hat, enthält. Man findet dort Informationen über die beteiligten Interfaces, ggfls. Zeitstempel, aber auch bereits von der Hardware extrahierte Teildaten (z.B. das Vlan) des eingegangenen Frames, und natürlich das Frame selbst.

Der Rückgabewert beschreibt, wie mit dem Frame nach der XDP-Behandlung verfahren werden soll:

  • XDP_DROP: Wirf den Frame weg.
  • XDP_PASS: Bearbeite ihn weiter, als wenn es kein XDP gäbe (wobei der Frame aber verändert worden sein kann)
  • XDP_REDIRECT: Schicke den Frame an eine andere Stelle (Interface, CPU, ...) zur weiteren Bearbeitung
  • XDP_TX: Schicke den (bearbeiteten) Frame zurück an das Interface, von dem er kam.

Es gibt noch weitere Rückgabewerte, aber die sind produktiv nicht von Belang.

Dieses Programm ignoriert also den auslösenden Frame und weist den Device-Treiber an, den Frame zu verwerfen. Es ist also so, als wäre gar nichts auf dem Interface angekommen. Würde man mit tcpdump versuchen, hier Netzwerk-Aktivitäten mitzuschneiden, so sagt der Netzwerk-Treiber stets: Keine Daten angekommen,

Kompilieren

XDP-Programme sollen auf verschiedenster Hardware laufen. Einige Netzwerkkarten erlauben es, diese Programme direkt auf dem Netzwerkchip abarbeiten zu lassen. Die CPU-Architektur zum Arbeitszeitpunkt kann also stark variieren: Es muss nicht die Host-CPU sein. Deswegen werden XDP-Programme als eBPF-Zwischenformat übertragen. Der Compiler darf keinen Maschinen-Code für die aktuelle CPU erstellen, sondern muss für die fremde Architektur cross-compilieren. Dies kann der clang-Compiler mit dem Target "bpf".

# apt install llvm clang xdp-tools make linux-headers-amd64 libbpf-dev
$ clang -target bpf -c nat-on-stick.c

Das Ergebnis ist eine Art Interpreter-Bytecode für eine streng definierte virtuelle Maschine innerhalb des Kernels. Beim Laden übersetzt der Kernel diesen Bytecode in native Maschinesprache, die aber keinen Schaden anrichten kann, weil die strenge Definition eine rigorose Validierung des Codes ermöglicht.

In meinem Fall heißt die Quelldatei "nat-on-stick.c". Der minimale Compileraufruf soll nur das Objekt-File "nat-on-stick.o" mit dem Bytecode erzeugen, kein direkt ausführbares Programm erstellen. Deswegen die Einschränkung mit "-c".

Es gibt weitere sinnvolle Optionen beim Kompilieren:

  • -g Erzeugen von Debug-Informationen (BTF), die später beim Arbeiten mit dem eBPF Dateien sehr nützlich sind.
  • -O2 Erzeugen von gutem Code, auch wenn die Formulierungen durch die Programmiersprache das nicht so her geben.
  • -I /usr/include/x86_64-linux-gnu Einbinden der benötigten Grunddefinitionen für die Architektur, auf der der Cross-Compiler arbeitet

Die letzte Option weist auf eine Stolperfalle hin: Dadurch, dass der Cross-Compiler nicht selbst als BPF-Programm geschrieben ist, sind seine Systembibliotheken normalerweise unbenutzbar. Man müsste die alle für eBPF portieren. Das macht natürlich keinen Sinn, also nimmt man die Systembibliotheken des Hostumgebung und hofft, dass der Compiler den Unterschied schon erkennen wird.

Einige Datentypen sind auf der eBPF-Architektur als 32bit-Typen definiert, die es in meiner 64bit Host-Architektur nicht gibt. Da kommt der Compiler ins Stolpern, weil er die 32bit-Header nicht findet. Glücklicherweise kann man diese aber nachinstallieren, so dass die auch auf der 64bit Host-Architektur parallel genutzt werden können.

# apt install libc6-dev-i386

Den Objektcode kann man sich übrigens auch ansehen:

$ llvm-objdump-19 -S nat-on-stick.o
nat-on-stick.o: file format elf64-bpf
Disassembly of section xdp:
0000000000000000 <nat_on_stick>:
;    return XDP_DROP;
       0:       b7 00 00 00 01 00 00 00 r0 = 0x1
       1:       95 00 00 00 00 00 00 00 exit

Man erkennt den 64bit breiten Bytecode und die symbolische Übersetzung. Für dieses triviale Programm ist der Code auch trivial.

Aktivieren von XDP

Für das Arbeiten mit eBPF-Programmen gibt es ein besonderes Werkzeug "bpftool"

# apt install bpftool

Das Script "xdp-on.sh" läd ein Objekt-File an den xdp-Hook des "nat"-Intefaces im Namespace des "Router"s.

#! /bin/sh

PROG="$1"
if [ ! -s "$PROG" ] ; then
  echo "$0 object-file"
  exit 1
fi

BPFFS="/sys/fs/bpf"
fgrep -q "$BPFFS" /proc/mounts ||
  mount -t bpf bpffs "$BPFFS"

ip netns exec router sh -c "
  bpftool prog load $PROG $BPFFS/nat-proc
  bpftool net attach xdp pinned $BPFFS/nat-proc dev nat
  rm -f $BPFFS/nat-proc
"

Der Weg wirkt etwas kompliziert:

  • Zunächst mal braucht man tatsächlich ein Object-File.
  • Zur Kommunikation mit dem Kernel hilft es, das bpf-Filesystem zu mounten.
  • Damit kann man ein Objekt-File in den Kernel laden und es unter einem bestimmten Namen wieder finden (pinning). Normalerweise bekommt man beim Laden nur eine ID zurück, die man sich merken muss, um das Programm später wieder zu finden. Dies ersetzt hier der Name.
  • Anhand des Names kann man dann das schon in den Kernel geladene Programm an die Netzwerk-Schnittstelle binden.
  • Danach wird der Name nicht mehr benötigt.

In umgekehrter Richtung geht es mit "xdp-off.sh" ähnlich:

#! /bin/sh

ip netns exec router \
  bpftool net detach xdp dev nat 

Es ist auch möglich, sich den Programmcode nach der Bearbeitung durch den Kernel anzusehen

# bpftool prog dump xlated name nat_on_stick
int nat_on_stick(struct xdp_md * ctx):
; return XDP_DROP;
   0: (b7) r0 = 1
   1: (95) exit 

Und das entspricht ja genau dem, was der Objekt-Dump nach dem Kompilieren erzählt hat.

Zusammenfassung

Natürlich will man nicht alles immer von Hand tippen. Also gibt's ein Makefile:

CC = clang
CFLAGS = -target bpf -c -O2 -g -I /usr/include/x86_64-linux-gnu
OBJDUMP = llvm-objdump-19
SUDO = sudo
TESTBED_NS = /run/netns/router

OBJ = nat-on-stick.o

.PHONY: tests
tests: | $(TESTBED_NS)
        $(SUDO) ./tests.sh

.PHONY: install
install: $(OBJ) | $(TESTBED_NS)
        $(SUDO) ./xdp-on.sh $^

.PHONY: clean
clean:
        rm -f *.o *~ *.S *.dot
        $(SUDO) ./xdp-off.sh
        $(SUDO) ./testbed-off.sh

$(TESTBED_NS):
        $(SUDO) ./testbed-on.sh

.c.o:
        $(CC) $(CFLAGS) -o $@ $^

.o.S:
        $(OBJDUMP) -S $^ > $@

test.dot: install
        $(SUDO) ./xdp-dump.sh > $@ 

Der Trick mit TESTBED_NS prüft, ob der Namespace schon existiert und baut bei Bedarf das Testbed auf.

"xdp-dump.sh" ist ein Script, was den geladenen Code zur besseren Visualisierung nach GraphWiz umwandelt:

#! /bin/sh
bpftool prog dump xlated name nat_on_stick visual

Das Ergebnis sieht dann so aus:

xdp-drop

Und der Test?

$ make install tests
clang -target bpf -c -O2 -g -I /usr/include/x86_64-linux-gnu -o nat-on-stick.o nat-on-stick.c
sudo ./testbed-on.sh
sudo ./xdp-on.sh nat-on-stick.o
sudo ./tests.sh

Jetzt geht's los!

### Client2 pings the gateway
PING 192.168.20.254 (192.168.20.254) 56(84) bytes of data.

--- 192.168.20.254 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2054ms

### Client2 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
22:07:33.077927 66:b8:47:f2:ad:8f > ff:ff:ff:ff:ff:ff, ethertype 802.1Q (0x8100), length 46: vlan 20, p 0, ethertype ARP (0x0806), Request who-has 192.168.20.254 tell 192.168.20.2, length 28
22:07:34.101949 66:b8:47:f2:ad:8f > ff:ff:ff:ff:ff:ff, ethertype 802.1Q (0x8100), length 46: vlan 20, p 0, ethertype ARP (0x0806), Request who-has 192.168.20.254 tell 192.168.20.2, length 28
22:07:36.053988 66:b8:47:f2:ad:8f > ff:ff:ff:ff:ff:ff, ethertype 802.1Q (0x8100), length 46: vlan 20, p 0, ethertype ARP (0x0806), Request who-has 192.168.20.254 tell 192.168.20.2, length 28
22:07:37.077923 66:b8:47:f2:ad:8f > ff:ff:ff:ff:ff:ff, ethertype 802.1Q (0x8100), length 46: vlan 20, p 0, ethertype ARP (0x0806), Request who-has 192.168.20.254 tell 192.168.20.2, length 28 

Man sieht nur, wie der Client verzweifelt versucht, eine Adressauflösung für die Gateway-Adresse zu bekommen. Aber es kommt keine Antwort zurück.
Das XDP-Programm hat also erfolgreich die Kommunikation mit dem Router verhindert, indem es alle eingehenden Frames verwarf. Oder ist einfach was kaputt?

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.

In die Netze von Internetprovidern speisen die Kunden alles ein, was nicht lokal abgehandelt wird. Gebraucht gekaufte IoT-Geräte, internen Traffic, der am Firmen-VPN vorbei leaked, etc. pp. Ein Großteil davon geht an private IPv4 Adressen, die grundsätzlich nicht erreichbar sind. Ein Teil davon sind DNS Anfragen, Und die sollen beantwortet werden.

Seitdem ich den unzustellbaren Traffic einfange, möchte ich den Kundengeräten eine Antwort geben können, die sowohl ungefährlich als auch ausreichend ist. Hört dann das Gerät auf, weitere Pakete zu senden, kann das als Erfolg gelten. Noch besser ist es, wenn der Anwender erkennen kann, wo sein Fehler liegt.

DNS

Anfragen an Nameserver mit privaten IPs machen einen substantiellen Anteil des unzustellbaren Traffics aus. Die meisten Anfragen sind irgendwelche internen Namen wie Fritz-Repeater.fritz.box oder _ldap._tcp.dc._msdcs.WORKGROUP.WORKGROUP. Es würde also gar nichts bringen, in diesen Fällen die korrekte Antwort zu liefern, denn die gibt es nicht.

Um Schwierigkeiten mit anfälligen Funktionalitäten zu vermeiden wähle ich als DNS Server die Software nsd, ein rein authoritiver, sehr belastbarer Name-Server.

Die Konfig ist ziemlich einfach, denn der Server soll nur auf der lokalen IP 127.0.0.2 laufen:

server:
        ip-address: 127.0.0.2

zone:
        name: .
        zonefile: /etc/nsd/catch.zone
        allow-query: 127.0.0.0/8 NOKEY
        allow-query: 100.64.0.0/10 NOKEY

Zuständig ist er für die Root-Zone und damit für alles, was darunter liegt. Alles also. Und damit das keinen unkontrollierbaren Mißbrauch gibt, wird die Anfrage auf die IP Bereiche eingeschränkt, die auch in diesem Netz bedient werden. Zugriffe aus dem Rest des Internet werden abgelehnt, auch wenn diese eigentlich gar nicht ankommen können.

Die Zone muss nun alle sinnvoll auftretenden Recordtypen enthalten:

$ORIGIN .
$TTL 600 ; automatic recovery after 10 minutes
.       SOA     catch-private. noc.... (
                2026020301  ;Serial
                7200        ;Refresh
                3600        ;Retry
                1209600     ;Expire
                600         ;Negative response caching TTL
        )
        NS      catch-private.
*       A       127.0.0.1
        AAAA    ::1
        NS      your.dns.config.is.invalid.
        MX      0 your.dns.config.is.invalid.
        TXT     "Please fix your DNS config"
        PTR     your.dns.config.is.invalid.
        SRV     0 0 443 your.dns.config.is.invalid.
        SVCB    0 your.dns.config.is.invalid.
        HTTPS   0 your.dns.config.is.invalid.

Jede Art von Anfrage wird per Wildcard erfasst und mit den typischen Antworten bedacht:

  • Verbinde Dich zu Dir selbst (localhost)
  • Wenn Du etwas wissen musst, dann bekommst Du einen Hinweis auf Deine kaputte Konfiguration
  • Der Hinweis verbindet sich zu Dir selbst.

Dank der DNAT Konfig scheinen dann die Antworten vom angefragten System zu stammen.

100.75.204.163.61225 > 10.2.0.1.53: 30905+ SRV? _ldap._tcp.dc._msdcs.fritz.box. (48)
10.2.0.1.53 > 100.75.204.163.61225: 30905*- 1/1/4 SRV your.dns.config.is.invalid.:443 0 0 (227)
100.75.145.170.63061 > 10.0.0.243.53: 10330+ A? wpad.fritz.box. (32)
10.0.0.243.53 > 100.75.145.170.63061: 10330*- 1/1/2 A 127.0.0.1 (134)
100.75.145.170.62904 > 10.0.0.243.53: 37418+ A? wpad.fritz.box. (32)
10.0.0.243.53 > 100.75.145.170.62904: 37418*- 1/1/2 A 127.0.0.1 (134)
100.75.145.170.62904 > 10.0.0.243.53: 28489+ A? wpad.fritz.box. (32)
10.0.0.243.53 > 100.75.145.170.62904: 28489*- 1/1/2 A 127.0.0.1 (134)
100.75.145.170.63061 > 10.0.0.243.53: 48991+ A? wpad.fritz.box. (32)
10.0.0.243.53 > 100.75.145.170.63061: 48991*- 1/1/2 A 127.0.0.1 (134)
100.77.241.104.61466 > 192.168.1.1.53: 203+ A? www.3322.org. (30)
192.168.1.1.53 > 100.77.241.104.61466: 203*- 1/1/2 A 127.0.0.1 (132)
100.75.8.92.62066 > 10.238.2.2.53: 53864+ A? a.root-servers.net. (36)
10.238.2.2.53 > 100.75.8.92.62066: 53864*- 1/1/2 A 127.0.0.1 (138)
100.75.98.217.55031 > 192.168.19.2.53: 2898+ SRV? _ldap._tcp.dc._msdcs.WORKGROUP.WORKGROUP. (58)
192.168.19.2.53 > 100.75.98.217.55031: 2898*- 1/1/4 SRV your.dns.config.is.invalid.:443 0 0 (237)
100.75.98.217.58566 > 192.168.19.2.53: 2899+ A? your.dns.config.is.invalid. (43)
192.168.19.2.53 > 100.75.98.217.58566: 2899*- 1/1/2 A 127.0.0.1 (145)
100.75.98.217.58566 > 192.168.19.2.53: 2900+ AAAA? your.dns.config.is.invalid. (43)
192.168.19.2.53 > 100.75.98.217.58566: 2900*- 1/1/2 AAAA ::1 (157)
100.75.155.21.63241 > 10.176.95.252.53: 18753+ AAAA? dns.msftncsi.com. (34)
10.176.95.252.53 > 100.75.155.21.63241: 18753*- 1/1/2 AAAA ::1 (148)
100.75.155.21.61397 > 10.176.95.252.53: 46222+ A? dns.msftncsi.com. (34)
10.176.95.252.53 > 100.75.155.21.61397: 46222*- 1/1/2 A 127.0.0.1 (136)
100.75.155.21.63241 > 10.176.95.252.53: 32607+ A? ipv6.msftconnecttest.com. (42)
10.176.95.252.53 > 100.75.155.21.63241: 32607*- 1/1/2 A 127.0.0.1 (144)
100.75.155.21.61397 > 10.176.95.252.53: 11911+ A? www.msftconnecttest.com. (41)
10.176.95.252.53 > 100.75.155.21.61397: 11911*- 1/1/2 A 127.0.0.1 (143)
100.75.155.21.62785 > 10.176.95.252.53: 61859+ A? www.msftconnecttest.com. (41)
100.75.155.21.64381 > 10.176.95.252.53: 61057+ A? ipv6.msftconnecttest.com. (42)
10.176.95.252.53 > 100.75.155.21.62785: 61859*- 1/1/2 A 127.0.0.1 (143)
10.176.95.252.53 > 100.75.155.21.64381: 61057*- 1/1/2 A 127.0.0.1 (144)
100.75.155.21.63241 > 10.176.95.252.53: 64996+ A? ipv6.msftconnecttest.com. (42)
10.176.95.252.53 > 100.75.155.21.63241: 64996*- 1/1/2 A 127.0.0.1 (144)
100.75.155.21.61397 > 10.176.95.252.53: 26122+ A? www.msftconnecttest.com. (41)
10.176.95.252.53 > 100.75.155.21.61397: 26122*- 1/1/2 A 127.0.0.1 (143)

Man sieht schön, wie

  • Typische interne Namen (...fritz.box) nun aufgelöst werden können.
  • Die Auflösung (_ldap...) akzeptiert wird und zu Folgeabfragen führt.
  • Verschiedenste Verbindungstests beantwortet werden können, auch wenn dann lokal kein Webserver antwortet.

Was man nicht sieht, ist der dramatische Rückgang an DNS Anfragen, die sonst unbeantwortet blieben oder abgelehnt wurden. Das fehlkonfigurierte, anfragende System ist zumindest für einige Minuten ruhig gestellt.

In die Netze von Internetprovidern speisen die Kunden alles ein, was nicht lokal abgehandelt wird. Gebraucht gekaufte IoT-Geräte, internen Traffic, der am Firmen-VPN vorbei leaked, etc. pp. Ein Großteil davon geht an private IPv4 Adressen, die grundsätzlich nicht erreichbar sind. Leider geben sich die meisten Geräte nicht mit Ablehnungen und Fehlermeldungen zufrieden, sondern bestehen auf einer Antwort. Und wenn sie dafür zig-Mal pro Sekunde anfragen müssen.

Catch All

Die Idee besteht nun darin, Anfragen an garantiert unzustellbare Adressen in einer Weise zu beantworten, dass die anfragenden Geräte zufrieden sind, aber keinen Schaden anrichten können. Gleichzeitig soll der – möglicherweise irritierte – Nutzer erkennen können, was da schief geht. Dazu wird zunächst eine Maschine aufgesetzt, die all den fehlerhaften Traffic abbekommt. Dazu wird der Traffic an nicht anderweitig bekannte, private IPs an diese Kiste geroutet:

(conf)# ip route 10.0.0.0/8 a.b.c.d
(conf)# ip route 172.16.0.0/12 a.b.c.d
(conf)# ip route 192.168.0.0/16 a.b.c.d

Warum nicht auch noch den Shared-IP-Space 100.64.0.0/10? Weil wir den selbst benutzen für die Masse der Endkunden.

Was kommt denn da so geflogen?

100.76.113.229.55323 > 192.168.40.12.389: Flags [S]
100.75.2.125.54724 > 192.168.121.1.20080: UDP
100.98.190.49 > 172.18.10.10: ICMP echo request
100.75.253.235.65019 > 10.3.76.152.389: Flags [S]
100.75.121.232.13564 > 10.10.10.3.18899: Flags [S]
100.75.196.34.63612 > 10.10.10.3.18899: Flags [S]
100.75.8.243.63753 > 10.1.7.20.53: 43789+ A? europe.smartscreen.microsoft.com. (50)
100.75.226.148.62734 > 10.10.10.3.18899: Flags [S]
100.74.166.72.59236 > 10.10.10.3.18899: Flags [S]
100.76.98.43.65100 > 192.168.2.200.22000: Flags [SEW]
100.75.177.69.51982 > 10.10.10.3.18899: Flags [S]

Da haben wir also Ping, LDAP (vermutlich Kontaktversuche in ein AD), DNS (natürlich), die Steuerkonsole für PV-Anlagen (what?) und verschiedene seltsame Ports. Und alles an wilde private Ziele.

Destination NAT

Um auf diese Anfragen reagieren zu können, müssen alle die Pakete auf die lokale Maschine umgeleitet werden. Und dann müssen die Antworten wieder so aussehen, als ob sie von dem eigentlich angefragten Servern stammen. Als lokale Adresse möchte ich die IP 127.0.0.2 auf einem zweiten Loopback oder Dummy-Interface benutzen.

FreeBSD

Nimmt man FreeBSD so gibt es die Implementationen: ipfw und pf. Beide beherrschen den das Umleitung von Pakten an ein anderes Ziel

ipfw add fwd 127.0.0.2 ip from any to 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 recv veth0
(pf) rdr pass inet from any to {10.0.0.0/8,172.16.0.0/12,192.168.0.0/16} -> 127.0.0.2

Wenn dann an dem betreffenden Port ein Dienst lauscht, so antwortet der und die Antwort geht von der lokalen IP wieder nach draußen:

100.79.236.62.42046 > 172.20.10.1.53: 40857*- ...
127.0.0.2.53 > 100.79.236.62.42046: 40857*- ...

Damit kann natürlich niemand etwas anfangen. Es müsst schon von der gleichen privaten IP eine Antwort kommen.

Aber wie? Mit NAT kann man die IP Adressen umschreiben, sich die Umschreibung merken und wieder zurück wandeln. Nur gibt es leider abertausende von möglichen Zielen, die zu natten wären. ipfw-NAT kennt redirect_address und akzeptiert dort auch den Spezialparameter 0.0.0.0.

ipfw nat 1 config if veth0 same_ports redirect_addr 127.0.0.2 0.0.0.0
ipfw add nat 1 from any to 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 recv veth0
ipfw add nat 1 from 127.0.0.2 to any xmit veth0

Der Traffic muss auf dem Hin- und auf dem Rückweg in die richtige NAT-Instanz geschoben werden, damit diese die richtigen Adressen umschreiben kann. In welche Richtung die NAT-Instanz arbeitet, hängt von der Richtung ab, in der das Paket das Interface passieren will. Es macht also einen Unterschied, auf welchem Interface man den Traffic zum NAT umleitet.

Unglücklicherweise funktioniert das aber überhaupt nicht. Es scheint daran zu liegen, dass die IP-Adressen auch aus einem reservierten Bereich stammen. Da scheint die Erkennung dessen, was zu tun ist, durcheinander zu kommen.

Linux

Also der Versuch mit Linux. Das alte iptables hat sich im Rahmen des Netfilter-Projektes weiter entwickelt:

flush ruleset

table ip fremdtraffic {
        chain catch {
                type nat hook prerouting priority dstnat; policy accept;
                ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } counter dnat to 127.0.0.2
        }
}
table ip dienste {
        chain management {
                type filter hook input priority filter; policy accept;
                ip saddr m.g.m.t/net counter accept
                tcp dport { 22, ... } counter reject
        }
}

Die Tables sind Verwaltungs-Container, die man atomar einfügen, aktivieren und löschen kann. Sie gelten für einen bestimmte Art von Daten, hier IPv4.

Die Chains enthalten sequentiell abzuarbeitende Blöcke von Regeln gleicher Funktion (type) an der gleichen Eingriffstelle (hook). Endet ein Regel mit reject, wird das Paket verworfen. Endet eine Regel mit accept, geht's mit der nächsten Chain weiter. Nicht von Regeln erfasste Pakete bekommen die generelle Policy der Chain zu spüren.

Die Reihenfolge (priority) der Chains wirkt pro Eingriffstelle (hook). Haben zwei Chains die gleiche Priorität, ist es unbestimmt, welche Chain zuerst dran kommt.

Von dem o.g. Regelwerk bleibt also übrig:

  • Der Hook prerouting führt seine einzige Regel aus, indem er Pakete an private IPs auf die lokale IP umschreibt (und sich das merkt)
  • Der Hook input führt seine einzige Regel für lokal adressierten Traffic aus und filtert Zugriffe auf Management-Dienste aus, wenn sie nicht von dem zulässigen Managment-Netz stammen.
  • Auf dem Rückweg greift das Nat automatisch ein und übersetzt die Adressen wieder zurück

Aktuell existieren keine Dienste, aber der Kernel kann schon ein paar Dinge selbst:

100.98.190.49 > 172.18.10.10: ICMP echo request
100.98.190.49 > 172.18.10.10: ICMP echo request
100.98.190.49 > 172.18.10.10: ICMP echo request
100.98.190.49 > 172.18.10.10: ICMP echo request
172.18.10.10 > 100.98.190.49: ICMP echo reply
172.18.10.10 > 100.98.190.49: ICMP echo reply
172.18.10.10 > 100.98.190.49: ICMP echo reply
172.18.10.10 > 100.98.190.49: ICMP echo reply
100.98.190.49 > 172.18.10.10: ICMP echo request
100.98.190.49 > 172.18.10.10: ICMP echo request
100.98.190.49 > 172.18.10.10: ICMP echo request
100.98.190.49 > 172.18.10.10: ICMP echo request
172.18.10.10 > 100.98.190.49: ICMP echo reply
172.18.10.10 > 100.98.190.49: ICMP echo reply
172.18.10.10 > 100.98.190.49: ICMP echo reply

Sieht schon mal gut aus. Der Kernel hat ein Rate-Limit, beantwortet also nicht alle Anfragen.

100.75.121.212.57726 > 192.168.0.101.55443: Flags [S]
192.168.0.101.55443 > 100.75.121.212.57726: Flags [R.]
100.75.196.199.55444 > 192.168.1.166.55444: UDP
192.168.1.166 > 100.75.196.199: ICMP 192.168.1.166 udp port 55444 unreachable

Auch Anfragen an unbekannte Dienste werden korrekt beantwortet: Dienst nicht verfügbar.

Das sieht man dann auch an den Zählern:

 # nft list ruleset
table ip fremdtraffic {
        chain catch {
                type nat hook prerouting priority dstnat; policy accept;
                ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } counter packets 2598458 bytes 166126098 dnat to 127.0.0.2
        }
}
table ip dienste {
        chain management {
                type filter hook input priority filter; policy accept;
                ip saddr m.g.m.t/net counter packets 1880 bytes 333471 accept
                tcp dport { 22, ... } counter packets 21572 bytes 1231360 reject
        }
}

Als Entwickler von Kommunikationslösungen und Dienstleister im Umfeld von Internet-Service-Providern bin ich mit den Realitäten der Internetversorgung für die typischen privaten Haushalte vertraut. Mir ist der Referentenentwurf erst vor wenigen Tagen zugegangen und ich möchte auf ein paar grundsätzliche Punkte in diesem Entwurf hinweisen. Dazu beschränke ich mich auf den Teil, der den Massenmarkt der Internetversorgung für Privatleute betrifft.

In der Begründung des Referentenentwurfes heißt es:

  • Nach dem Entwurf werden Internetzugangsdiensteanbieter erstens verpflichtet, für drei Monate die von ihnen an Endkunden vergebenen IP-Adressen und weitere Daten wie die Portnummer zu speichern, sofern dies für eine eindeutige Zuordnung der IP-Adresse zu einem Anschlussinhaber erforderlich ist. Dadurch können Strafverfolgungs- und Gefahrenabwehrbehörden anhand eines Zeitstempels, einer IP-Adresse und gegebenenfalls einer Portnummer die Bestandsdaten beim Internetzugangsdiensteanbieter abfragen, […]

Der Referentenentwurf sieht dazu insbesondere folgende Änderungen vor:

  • TKG §176 Speicherpflicht und Verwendungsbefugnis von Verkehrsdaten zur Identifizierung von Anschlussinhabern
    (1) Anbieter von Internetzugangsdiensten sind verpflichtet, mit der Zuweisung einer öffentlichen Internetprotokoll-Adresse an einen Anschlussinhaber folgende Daten für drei Monate zu speichern:
    1. die dem Anschlussinhaber für eine Internetverbindung zugewiesene, öffentliche Internetprotokoll-Adresse,
    2. eine eindeutige Kennung des Anschlusses, über den die Internetverbindung erfolgt, sowie eine zugewiesene Benutzerkennung,
    3. das Datum und die sekundengenaue Uhrzeit von Beginn und Ende der Zuweisung der öffentlichen Internetprotokoll-Adressen an einen Anschlussinhaber unter Angabe der jeweils zugrunde liegenden Zeitzone sowie
    4. die der Internetprotokoll-Adresse zugehörigen Portnummern und weitere Verkehrsdaten, soweit diese für eine Identifizierung des Anschlussinhabers anhand einer zu einem bestimmten Zeitpunkt zugewiesenen Internetprotokoll-Adresse erforderlich sind.
  • BKAG § 10b Sicherung von Verkehrsdaten
    (1) Zur Erfüllung der Aufgabe als Zentralstelle nach § 2 Absatz 1 kann das Bundeskriminalamt zum Zwecke einer etwaigen Erhebung gegenüber demjenigen, der öffentlich zugängliche Telekommunikationsdienste anbietet oder daran mitwirkt, anordnen, Verkehrsdaten gemäß § 3 Nummer 70 des Telekommunikationsgesetzes von betroffenen Personen unverzüglich zu sichern (Sicherungsanordnung), wenn die zuständige Strafverfolgungsbehörde oder die zuständige Polizeibehörde noch nicht erkennbar ist […]

Lassen Sie mich dazu einige Begrifflichkeiten aus den referenzierten Gesetzen zitieren:

  • TKG §3(6): Bestandsdaten Daten eines Endnutzers, die erforderlich sind für die Begründung, inhaltliche Ausgestaltung, Änderung oder Beendigung eines Vertragsverhältnisses über Telekommunikationsdienste
  • TKG §3(70): Verkehrsdaten Daten, deren Erhebung, Verarbeitung oder Nutzung bei der Erbringung eines Telekommunikationsdienstes erforderlich sind
  • BSIG §2(31): Protokolldaten Steuerdaten eines informationstechnischen Protokolls zur Datenübertragung, die a) zur Gewährleistung der Kommunikation zwischen Empfänger und Sender notwendig sind und b)  unabhängig vom Inhalt des Kommunikationsvorgangs übertragen oder auf den am Kommunikationsvorgang beteiligten Servern gespeichert werden
  • TDDDG §2(3) Nutzungsdaten die personenbezogenen Daten eines Nutzers von digitalen Diensten, deren Verarbeitung erforderlich ist, um die Inanspruchnahme von digitalen Diensten zu ermöglichen und abzurechnen; dazu gehören insbesondere a) Merkmale zur Identifikation des Nutzers, b)  Angaben über Beginn und Ende sowie über den Umfang der jeweiligen Nutzung und c) Angaben über die vom Nutzer in Anspruch genommenen digitalen Dienste

Die offenkundige Intention des Referentenentwurfs ist, aus Protokolldaten, die an einer bestimmten Stelle (z.B. einem bestimmte Daten bereitstellenden Server) erfasst wurden, einen Rückschluss auf den Kommunikationspartner (z.B. den PC eines Benutzers, der die Daten abruft) ziehen zu können. Wie die gesamte Gesetzeslage so atmet auch der Referentenentwurf die Idee der klassischen Telefonie: Es besteht die (irrige) Annahme, es gäbe eine direkte Kommunikationsverbindung zwischen zwei Teilnehmern, die man – dem Kabel folgend – finden kann. Da Internet-Protokolle i.d.R. tatsächlich eine Kommunikation zwischen zwei Endstellen abbilden, besteht die (irrige) Annahme, jede Gegenstelle sei bereits der Endpunkt der Kommunikation und damit einem Benutzer zuordenbar.

Der fundamentale Irrtum besteht im Glauben an das Prinzip der Ende-zu-Ende-Kommunikation. Dieses Prinzip wurde Mitte der 2000er Jahre durch fortschreitenden Mangel an IPv4 Adressen aufgegeben. Stattdessen entwickelte  sich das Privatkunden-Internet in drei Richtungen, die oft gleichzeitig angeboten werden:

  1. Kunden bekommen öffentliche IPv4-Adressen als Premiumprodukt
  2. Kunden bekommen öffentliche IPv6-Adressen
  3. Kunden bekommen nicht-öffentliche IPv4-Adressen zugewiesen und erreichen das Internet über Mittelboxen, die die Adressen umschreiben (Carrier Grade Network Address Translation – CGNAT)

Um aus den (entfernt erfassten) Protokolldaten auf die Bestandsdaten der (unbekannten) Gegenstelle schließen zu können, müssen die Verkehrsdaten beim ISP rechtzeitig abgefragt werden. Bisher gibt es keine Notwendigkeit für eine längere Speicherung der Verkehrsdaten beim ISP. Aktuell wird seitens der Datenschützer aber eine Frist von einigen Tagen zwecks Beseitigung von Betriebsstörungen toleriert. Dieses Zeitfenster soll der Referentenentwurf auf drei Monate ausdehnen.

Gestatten Sie mir zunächst eine Abschätzung der dabei anfallenden Datenmengen: Im Fall der Zuteilung von öffentlichen IP-Adressen an den Kunden bleibt diese Zuteilung meist mehrere Stunden stabil. Es genügt also zu erfassen, welcher Kunde wann welche Zuteilung für wie lange bekam. Die Datenmengen liegen dabei pro Kunde bei ca. 50 Byte pro Zuweisung (128bit Adresse, 64bit Zeitstempel, 64bit Kundenkennung plus weitere Metadaten). Bei durchschnittlich vier Änderungen pro Tag sind das 20kByte pro Kunde für drei Monate. Ein mittlerer ISP mit ca. 100000 Endkundenanschlüssen benötigt dafür also minimal 2 GByte Speicherplatz. In der Praxis wird die Art der Datenspeicherung das Zehnfache (plus Backup) benötigen.

Im Fall von (CG)NAT ist die Situation deutlich schwieriger. Viele Kunden mit nicht-öffentlichen IPv4-Adressen werden durch Mittelboxen auf wenige öffentliche IPv4-Adressen umgesetzt. Dabei werden die Portnummern der (TCP oder UDP)-Verbindungen umgeschrieben. Ein Verhältnis von 250 Kunden pro öffentliche IPv4-Adresse ist für einen mittleren ISP durchaus typisch. Als Faustformel gilt: Je mehr Kunden ein ISP hat, desto mehr Kunden teilen sich eine öffentliche NAT-IP.

Da nur ca. 64000 Port-Nummern (es gibt reservierte Bereiche) pro IPv4-Adresse zur Verfügung stehen, kann ein Kunde bei einer direkten Zuordnung der Portnummer zum Kunden (Cone-NAT) nur ca. 250 Ports benutzen. Offenbar hat der Referentenentwurf dieses Modell im Blick, wenn von der Erfassung der Portnummern zusammen mit der öffentlichen IPv4-Adresse gesprochen wird.

Leider ist Cone-NAT in der Praxis völlig unzureichend: Die meisten modernen Webseiten benötigen in schneller Folge mehrere hundert Verbindungen. Dazu kommen hunderte von DNS Anfragen. Eine Webseite wie z.B. Google Maps wird mit 250 freien Ports schlicht nicht vollständig aufgebaut. Bei vielen Social-Media Seiten fehlen dann Bilder oder andere Teile des Inhaltes. Eine Wiederverwendung der Port-Nummern ist insbesondere bei UDP Kommunikation schwierig, da es kein definiertes Ende der Kommunikation gibt. Der Port muss also über längere Zeit auf Verdacht mit diesem Kunden belegt bleiben.

Bei der Verwendung von Symmetric-NAT geht auch die Gegenstelle der Kommunikation mit ein. So stehen alle 64000 Ports einer öffentlichen IPv4-NAT-Adresse für jede Server-IP und jeden Server-Port (i.d.R. immer 443) zu Verfügung. Eine Anfrage welcher Nutzer hatte zum Zeitpunkt x die IP y mit Port z ist deswegen nicht beantwortbar. Es wird zusätzlich die Information die eine s-Verbindung mit Server a auf Port b hatte benötigt.

Man benötigt ca. 100 Byte pro Verbindung (3∙32bit IPv4, 3∙16bit Port, 16bit Protokoll, 64bit Zeitstempel, 64bit Kundenkennung plus Metadaten). Mit durchschnittlich einem Verbindungsaufbau pro Minute (über den Tag gerechnet, d.h. einige zig-tausend in kurzer Zeit und dann lange Pausen) bei Wenignutzern entstehen so 13 MByte pro Kunde für drei Monate. Ein mittlerer ISP wird also 2 TByte Verkehrsdaten verwalten müssen, die real sich auf 20 TByte Plattenplatz plus Backup ausdehnen.

Hat der ISP auch einige Poweruser (Familien mit Kindern reichen da schon), so kann man mit dem Mehrfachen an Platzbedarf rechnen. 10% der Nutzer machen jeder für sich so viele Verbindungen auf, wie der Rest aller Kunden zusammen, so dass der Platzbedarf um den Faktor 10 auf 200 TByte (plus Backup) anwächst.

Die Rechnung ist allerdings für normale Nutzung ohne besondere Vorkommnisse. Sollte ein(!) Nutzer Teil einer(!) dDOS-Attacke sein (z.B. durch ein(!) gekapertes IoT-Device), so generiert allein ein(!) solcher Vorgang ca. 1000 Verbindungsaufbauten pro Sekunde: Es entstehen hierbei 360 MByte pro Stunde an Verkehrsdaten für den einzelnen(!) Angriff.

Solche Angriffe passieren leider täglich und sind für die Strafverfolgung von hohem Interesse. Derartige explosionsartig anfallende Datenmengen sind also genau die Verkehrsdaten, auf die der Referentenentwurf abzielt. Deswegen ist es nicht möglich, in Anbetracht der Datenmenge eine Ausnahme für Angriffe von der Speicherpflicht zu machen, wenn der Referentenentwurf überhaupt nützlich sein soll.

Zusammenfassend kann man bei einem mittleren ISP also von ca. 500 TByte an Plattenplatz für die Speicherung von Verkehrsdaten beim Einsatz von CGNAT ausgehen. Backup kommt extra dazu, um im Falle einer Störung nicht die Daten der letzten Monate zu verlieren. Die zusätzlichen Anforderungen an einen mittleren ISP landen also im PByte Bereich, wenn dieser Referentenentwurf so realisiert würde.

Weiter heißt es in der Begründung des Referentenentwurfs:

  • Die Speicherpflicht betrifft zwar alle Nutzer von Internetzugangsdiensten in Deutschland. Allerdings lassen die Daten nichts anderes zu als die Identifizierung des Anschlussinhabers anhand einer IP-Adresse. Es handelt sich gerade nicht um eine Vorratsdatenspeicherung von allen Verkehrs- und Standortdaten. Aus den gespeicherten IP-Adressdaten lassen sich insbesondere keine Surf- oder Bewegungsprofile erstellen. Die Beeinträchtigung insbesondere der unbescholtenen Nutzer ist damit überschaubar.

Diese Aussagen sind bei NAT nicht haltbar. Da für die korrekte Zuordnung beide Seiten der Kommunikation bekannt sein müssen, handelt es sich bei der geplanten Speicherung von Verkehrsdaten um eine unzulässige Vorratsdatenspeicherung, die genaue Rückschlüsse über das Kommunikationsverhalten des Nutzers zulässt. Das betrifft nicht nur die genauen Zeiten der Kommunikation, sondern auch die Kenntnis der Kommunikationspartner, so dass ein genaues Surf- und Bewegungsprofil erstellt werden kann.

Abschließend gibt es also drei Möglichkeiten:

  1. Alle Referenzen auf Portnummern werden aus dem Referentenentwurf entfernt. Dies führt dazu, dass nur die Speicherfrist für die Zuordnung öffentlicher IP-Adressen erweitert wird. CGNAT-Nutzer werden dann nicht erfasst. Dies entspricht einer graduellen Ausweitung der aktuellen Rechtslage.
  2. Der Einsatz von CGNAT wird verboten. Da es nicht genügend IPv4 Adressen für alle Kunden gibt, wird die Anzahl der Internetanschlüsse für Haushalte beschränkt.
  3. Die verpflichtende Umsetzung des RFC 6540 für die deutschen ISPs wird eingefordert: Mit dem Wechsel auf IPv6 only besteht kein Bedarf mehr an NAT. Die Umsetzung des RFC muss deswegen gesetzlich verpflichtend sein, damit die ISPs sich nicht in einem ruinösen Wettbewerb für legacy IPv4-Nutzer gegenseitig kaputt machen. Während der Übergangszeit bleibt es bei Möglichkeit 1 für Bestandsverträge. Neuverträge sind ab einem Stichtag generell nur noch IPv6 only.

Danke für Ihr Interesse.

Microsoft veröffentlich am zweiten Dienstag im Monat neue Patches. In größeren Unternehmen wird bei normalen Updates eine Woche auf Unverträglichkeiten getesten und dann die Patches unternehmensweit ausgerollt. In dieser "Patchwoche" sollen auch automatisch Updates ausgerollt werden, in anderen Wochen aber nicht.

Das Problem besteht offensichtlich darin, dass Patches, die auf einem Gerät am Mittwoch in der Patchwoche ausgerollt werden sollen, jeden Monat von Hand mit dem Datum verknüpft werden müssen. So kann der 1. des Monats ein Montag oder Dienstag sein, dann ist der relevante Tag der 16. oder 18. des Monats. Ist der 1. des Monats aber ein Mittwoch, so wäre der relevante Tag am 21. des Monats.

Wie automatisiert man sowas?

Offensichtlich ändert sich an der Situation nichts, wenn der Wochentag und gleichzeitig das Datum erhöht werden: Die gesuchte Woche ist charaterisiert durch die Differenz von Datum und Wochentag.

Diese Differenz bezieht sich auf den Montag (1) als Monatsanfang (1), denn da ist die Differenz 0. Nun soll der Wochenanfang so liegen, als sei der Dienstag der Wochenbeginn. Die Differenz ist für einen Dienstag (2) als Monatsanfang (1) aber -1. Um das wieder auf 0 zu bekommen, muss man diese 1 hinzu addieren.

Um die Woche zu ermitteln teilt man einfach durch 7 und verwirft den Rest. 0 ist dann die erste Woche, 1 die Woche, in der der Microsoft veröffentlicht und 2 die Woche, in der regulär gepatcht werden soll.

Die Formel lautet also: (Monatstag - Wochentag + 1)/7 == 2

Als Shellscript sieht das so aus:

$ cat patchwoche-mache
#! /usr/bin/bash

[ 2 -eq $[$(/usr/bin/date +'(%-d-%u+1)/7')] ] || exit
exec mache "$@"

Nun steht einem generischen Crontab nichts mehr im Wege:

# Daueraufgaben
0 2 * * 1      mache am montag
0 2 * * 2      mache am dienstag
0 2 * * 3      mache am mittwoch
0 2 * * 4      mache am donnerstag
0 2 * * 5      mache am freitag
...
0 2 * * 3      patchwoche-mache am mittwoch
...

Und tut.

Mein eigner CLI-Prometheus-Exporter lieferte heute spontan Label-Werte, die der Prometheus als ungültig im Sinne von UTF8 ansah.

Am Exporter-Programm und an der Konfig einer ASA hatte sich nichts geändert. Der Exporter ist durchgelaufen, es gab also auch kein unbewusste Codeänderung. Der Abruf der Metrik im Browser funktionierte tadellos, die Ausgabe war unauffällig. Keine Sonderzeichen oder irgendetwas ungewöhnliches zu erkenne, Trotzdem verstand der Prometheus mit einem Male die Metriken nicht mehr.

Es gibt wenige Stellen an denen "Freitext" aus der CLI in die Label-Werte übernommen wurde. Eine davon ist der Nutzername von eingewählten Remote-Access-VPNs. Dort steht öfter mal die Domain mit dabei, also DOMAIN\user. Kann es sein, dass der Prometheus irgendwo eine Backslash-Sequenz missversteht anstatt es literal zu übernehmen?

Ich filtere also nun die Domain incl. des Backslash weg und die Ausgabe wird immer noch nicht akzeptiert. Irgendwo soll noch eine ungültige UTF8 Sequenz stehen.

Beim Durchmustern der Ausgabe fällt mir einpeer_user="`user" auf. Was soll das sein? Ein Accent Acute vor dem Nutzernamen? Mal in die ASA schauen:

Username     : ´user
Assigned IP  : a.b.c.d
Protocol     : IKEv1 IPsecOverNatT L2TPOverIPsecOverNatT
License      : Other VPN

Was sucht da ein Accent-Zeichen? Der Nutzer hat dieses Zeichen nicht im Login-Namen! Trotzdem hat die Anmeldung geklappt!

Ohne eine sinnvolle Quelle gefunden zu haben, scheint Windows dieses Zeichen als Domain-Separator zu interpretieren oder in dem Fall als "domainloses Login".

Jetzt filtere ich es raus, so wie die domain\ Vorsätze.

Obwohl IPv6 entwickelt wurde, um IPv4 zu ersetzen, ist der Übergangsprozess nicht wie erwartet in Gang gekommen und wird es auch weiterhin nicht sein.  Der Hauptgrund für die ins Stocken geratene Einführung ist nicht technischer Natur, sondern liegt im historischen Wissen und in den etablierten Verfahren.  Der fehlende Teil zwischen IPv4 und IPv6 soll die Migrationsbemühungen unterstützen, indem die nichttechnischen Anforderungen für einen reibungslosen Übergang berücksichtigt werden.

IPv4 kommt mit dem visuellen Erscheinungsbild von gepunkteten Quad-Zahlen daher, während IPv6 wie ein Speicherauszug während einer Kernel-Panik aussieht. Älzere Administratoren sowie Lehrer, Dokumentationsautoren oder Filmemacher sind mit dem IPv4-Adressstil vertraut und können und werden ihre Gewohnheiten, Folien oder Bücher nicht ändern.  IPv6 wird sich erst durchsetzen, wenn diese Generation von Menschen die Unternehmen und Schulen verlassen haben.
Um ein neues Protokoll einzuführen, darf sich das Erscheinungsbild und die Bedienung nicht ändern.  Wenn man von IPv4 kommt, sollte es bei der Verwendung von IPv5 keinen Unterschied geben.  Bei der Umstellung auf IPv6 sollte es nur einen minimalen Unterschied zu IPv5 geben.  IPv5 ist so konzipiert, dass die ältere Generation an Bord bleibt und gleichzeitig ein Protokoll eingeführt wird, das wie das neuere IPv6 verwendet werden könnte.
Außerdem sollten die organisatorischen Auswirkungen so gering wie möglich gehalten werden.  Personen, die bereits IPv4 oder IPv6 verwenden, sollten ohne zusätzliche bürokratische Hindernisse zu IPv5 wechseln können.

Weitere Details finden sich in https://datatracker.ietf.org/doc/draft-april-ipv5/

Ein FreeBSD vom Source aktuell zu halten geht mit etcupdate ziemlich gut. Diese Programm macht einen 3-Wege-Merge, berücksichtigt also die Änderungen durch den Update und die vom Nutzer. Manchmal aber übersieht es etwas.

Nach einem Update blieben einige Warnungen bestehen und liesen sich nicht beheben:

# etcupdate status
Warnings:
  Removed file changed: /etc/kyua/kyua.conf
  Removed file changed: /etc/rc.d/tlsclntd
  Removed file changed: /etc/rc.d/tlsservd
  Removed file changed: /etc/rc.d/zpool
  Removed file changed: /root/.shrc

Wie das passieren konnte, ist ein anderes Thema. Wichtig ist hier, wie man da wieder raus kommt. Normalerweise per etcupdate resolve.

# etcupdate resolve

Exakt: Da geht gar nichts. Lösen (resolve) kann man nur Konflikte, keine Warnungen.

Die Warnungen der Statusmeldngen sind gecached und stehen in /var/db/etcupdate/warnings. Man muss diese Datei nur leeren:

# : > /var/db/etcupdate/warnings
# etcupdate status

Fertig!

Und damit das irgendwo dokumentiert von Google und Co gefunden werden kann, steht's im Blog.

So langsam muss ich wieder anfangen, über die kleinen Erfolge zu bloggen, die sich in den letzten Jahren ergeben haben. Hier geht es nun um unwillkommene Spikes im Trafficgraphen, wenn dieser sich aus einer generalisierten Metrik erzeugt.

Trafficcounter in heterogenden Netzen kommen aus vielen unterschiedlichen Quellen, die alle ihre eigene Syntax und Labelstruktur aufweisen. Mit sowas will man möglichst schnell nichts mehr zu tun haben. Stattdessen soll eine generalisierte Metrik common:interface:counter die ganzen hässlichen Details wegabstrahieren.

groups:
- name: Leitungen
  rules:
  - record: common:interface:counter
    expr: >-
      sum by (device,port,type,direction,description) (
        label_replace(label_replace(
          sum by (direction,instance,intf,type,description) (
            intfCounter + on(instance,intf) group_left(description)
              (0*label_replace(interfaceDescription,"intf","$1","interface","(.*)"))
          ) or sum by (direction,instance,intf,type) (
            label_replace(intfLagCounter,"type","$1","unnamedLabel4","(.*)")
          ),
          "device","$1","instance","(.*):.*"),
          "port","$1","intf","(.*)")
      or label_replace(label_replace(
          label_replace(label_replace(cisco_interface_receive_bytes,"direction","in","job",".*"),"type","Octets","job",".*") or
          label_replace(label_replace(cisco_interface_transmit_bytes,"direction","out","job",".*"),"type","Octets","job",".*") or
          label_replace(label_replace(cisco_interface_receive_errors,"direction","out","job",".*"),"type","Errors","job",".*") or
          label_replace(label_replace(cisco_interface_transmit_errors,"direction","out","job",".*"),"type","Errors","job",".*") or
          label_replace(label_replace(cisco_interface_receive_drops,"direction","out","job",".*"),"type","Discards","job",".*") or
          label_replace(label_replace(cisco_interface_transmit_drops,"direction","out","job",".*"),"type","Discards","job",".*"),
          "device","$1","target","(.*)"),
          "port","$1","name","(.*)")
      or label_replace(label_replace(
          label_replace(label_replace(junos_interface_receive_bytes,"direction","in","job",".*"),"type","Octets","job",".*") or
          label_replace(label_replace(junos_interface_transmit_bytes,"direction","out","job",".*"),"type","Octets","job",".*") or
          label_replace(label_replace(junos_interface_receive_errors,"direction","out","job",".*"),"type","Errors","job",".*") or
          label_replace(label_replace(junos_interface_transmit_errors,"direction","out","job",".*"),"type","Errors","job",".*") or
          label_replace(label_replace(junos_interface_receive_drops,"direction","out","job",".*"),"type","Discards","job",".*") or
          label_replace(label_replace(junos_interface_transmit_drops,"direction","out","job",".*"),"type","Discards","job",".*") ,
          "device","$1","target","(.*)"),
          "port","$1","name","(.*)")
      ) 

Das sieht zunächst komplett wahnsinnig aus, ist aber relativ schnell erklärt:

Mit or werden Metriken, die eigentlich nichts miteinander zu tun haben, zu einer Metrik vereinigt. Dabei bleiben die Detail-Labels erhalten, der Name der Metrik fällt nur weg. Da ich hinterher eine Metrik mit neuen Label-Bezeichnern haben will, muss die die neuen Label-Namen erzeugen und mit den Werten der alten Labels befüllen. Die Funktion label_replace tut dies: Sie befüllt ein neues Label mit den Treffern des regulären Ausdrucks eines existierenden Labels. Dies kann man besonders schön an den letzten Zeilen sehen, wo das neue Label port mit dem vollen Text des alten Labels name gefüllt wird. So werden also nacheinander die Labels direction, type, device und port befüllt.

Um hinterher nicht mit einem Zoo weiterer Alt-Labels rumlaufen zu müssen, wir per sum by (labels) einfach die "Summe" ohne die unnötigen Labels ausgeführt: Da sie verbleibenden Labels eindeutig sind, ist es eine Summe über jeweils genau einen Eintrag, der einfach die nicht benötigten Labels weg wirft. Das ist ein typisches Idiom in PromQL.

Schwieriger ist der erst Teil, bei dem die Zählerstände zwar schon schön vorliegen, aber die Informationen wie description fehlen. Man muss also hier die Labels aus einer ganz anderen Metrik passend dazu pappen. Das zugehörige Idiom addiert nun also zwei Metriken (unter Berücksichtigung ausgewählter Labels als passend angesehen) und übernimmt das Label description. Damit die Summe aber nichts kaputt macht, multipliziert man die zweite Metrik mit 0.

Auf diese Weise entsteht eine neue Metrik common:interface:counter mit den Labels device, port, type, direction und description, die nun universell einsetzbar ist.

Schaut man sich die Traffiggraphen solcher Metriken an, so haben die häuftige und störende Ausreißer:

Screenshot 2022-09-23 235337
Screenshot 2022-09-23 235455

Beiden Bildern ist anzusehen, wie zittrig oder sprunghaft der Graph ist. Dieser Effekt stört mich seit mindestens einem Jahr und ich hatte ich bisher nicht in den Griff bekommen.

Mwhr durch Zufall kam ich heute dazu, den generierte Metrik und die Originalmetrik mal übereinander zu legen. Das Ergebis war, dass die Metriken fast identisch sind. Jedoch läuft die originale Metrik der generierten Metrik etwas voraus, bis die generierte Metrik den Fehler nach Minuten oder Stunden wieder einholt, dann aber sprunghaft.

Wie kann das sein? Wann wird denn die generierte Metrik erzeugt? Sicher nicht immer dann, wenn irgendeine der Metriken irgendeinen Update bekommt. Nein, das geschieht in regelmäßigen Intervallen durch den Prometheus selbst.

global:
  scrape_interval:     59s
  evaluation_interval: 1m 

Den Werte habe ich so gesetzt, damit er kurz nach neu eintreffenden Werten diese verarbeiten kann und diese nicht verpasst. Im Laufe der Zeit hatten sich dann andere Geräte mit scrape_interval von 29s bis 3m eingefunden. Die Daten kommen also zu sehr unterschiedlichen Zeiten rein.

Aber selbst diese Sekunde Unterschied bedeutet, dass die generierende Metrik jede Stunde (60x aufgerufen) doppelte Daten sieht. Diesen Takt kann man in der gelben Kurve ziemlich gut erkennen. Es kommen dann ja auch noch so Dinge rein, wie Verzögerungen beim Auslesen der Metriken, etc. pp.

Worin besteht also die Lösung? Viel häufiger die Metrik generieren! Das neue evaluation_interval beträgt nun die Hälfte des kleines Scrape-Intervals also 10 Sekunden.

Glücklicherweise ist Prometheus sehr effizient mit dem Abspeichern von "unveränderlichen" Passagen von Metriken. So ist das zumindest kein Plattenfresser.

Achja, ganz rechts im Bild ist die Änderung wirksam. Die Kurven haben aufgehört zu zittern.