Tue, 09 Feb 2021 12:00:00 +0100
bash, computernetz, ipsec, software, test

Netzwerk-Tests mit bats, netcat und xxd

IPsec-Tester ist ein Nebenprojekt, dass ich im Umfeld meines letzen Buches angefangen habe. Es dient mir dazu, das IKE-Protokoll - genau genommen Version 2 davon - besser zu verstehen. Momentan kann es IKE_SA_INIT-Anfragen verstehen und beantwortet sie mit NO_PROPOSAL_CHOSEN.

Ich komme nur selten dazu, an dem Projekt weiter zu arbeiten, darum will ich den erreichten Stand mit Tests dokumentieren. Außerdem sollen die Tests mich darauf aufmerksam machen, wenn ich etwas kaputt gemacht habe.

Netzwerkprotokolle testen

Viele Netzwerkprotokolle - wie zum Beispiel HTTP, IMAP, SMTP - arbeiten mit reinem Text und können mit telnet oder Netcat sehr einfach getestet werden. Für die mit TLS verschlüsselten Varianten leistet openssl s_client ähnliches.

IKE gehört leider nicht zu diesen Protokollen. Es verwendet UDP und tauscht Daten in einem binären Format aus. Immerhin arbeitet IKE mit paarweise ausgetauschten Nachrichten, so dass man das Protokoll mit je einem Paar von gesendetem und empfangenem Datagramm simulieren kann.

Für etliche binäre Formate gibt es Compiler beziehungsweise Interpreter, die die Form von Text in das binäre Format und zurück umwandeln können. Das wäre auch ein lohnendes Teilziel für IPsec-Tester, aber im Moment ist das Projekt nicht so weit.

Binärdaten in Text umwandeln

Hier kommt xxd ins Spiel, Teil des vim-Projekts. Dieses Programm kann binäre Daten in eine Textdarstellung als Hexdump umwandeln und einen Hexdump zurück in binäre Daten. Die binären Daten kann ich mit Netcat versenden und die eintreffende Antwort mit xxd als Hexdump ausgeben. Es bleibt die Frage, wie ich an die Hexdumps für die Anfrage komme.

Eine Möglichkeit wäre, diese Daten Byte für Byte selbst aufzuschreiben und sozusagen einen IKE-Request von Hand zusammenzubauen. Das ist im Prizip möglich, die RFĆs enthalten alle dafür nötigen Informationen. So weit will ich aber doch nicht gehen.

Eine einfachere Möglichkeit ist, einen IKE-Austausch im Netzwerk mitzuschneiden, die Datagramme als Hexdump ausgeben zu lassen und damit weiter zu arbeiten.

Bei tcpdump bekomme ich einen Hexdump der Daten mit der Option -X. Dabei muss ich beachten, dass dieser Hexdump außer der IKE-Nachricht auch den UDP-, den IP- und möglicherweise den Ethernet-Header enthält. Das heißt, beim Bearbeiten des Hexdump muss ich den Teil vor der IKE-Nachricht entfernen, weil später Netcat die Daten versendet und nur die UDP-Nutzdaten braucht.

12:23:16.799419 IP 192.168.57.4.500 > 192.168.57.1.500: isakmp: parent_sa ikev2_init[I]
        0x0000:  4500 0218 e00d 0000 4011 a571 c0a8 3904  E.......@..q..9.
        0x0010:  c0a8 3901 01f4 01f4 0204 f56b 7a57 b1e8  ..9........kzW..
        0x0020:  37ff 0200 0000 0000 0000 0000 2120 2208  7...........!.".
        0x0030:  0000 0000 0000 01fc 2200 005c 0200 002c  ........"..\...,
    ...
        0x0200:  2900 0010 0000 402f 0002 0003 0004 0005  ).....@/........
        0x0210:  0000 0008 0000 4016                      ......@.

Dieses Beispiel aus einem Hexdump von tcpdump enthält den IPv4- und den UDP-Header gefolgt von den Daten des IKE-Requests. Für den Test benötige ich nur die Daten ab Position 0x01c, das heißt, zunächst muss ich die ersten 28 Bytes entfernen, um den Dump für einen Test benutzen zu können.

Etwas einfacher komme ich mit Wireshark an die Daten. Ich wähle das Request-Datagramm aus, gehe in den Dissektor für UDP und markiere die Nutzdaten. Mit Rechtsklick öffne ich das Kontextmenü und wähle mit Packetbytes exportieren eine Datei aus, in die ich die Binärdaten der Anfrage abspeichere.

Diese Datei kann ich direkt verwenden oder mit xxd vor der Weiterverarbeitung in einen Hexdump umwandeln.

Nun kann ich den Test mit folgendem Befehl ausführen:

xxd -r request.dump \
| nc -u -W1 $addr 500 \
| xxd > response.dump

Die Daten in response.dump sollten so aussehen wie die IKE-Daten im Response bei Wireshark beziehungsweise tcpdump -X.

Um den Test reproduzierbar zu machen, kopiere ich response.dump nach expected.dump und kann beim Wiederholen des Tests die beiden Dateien mit cmp vergleichen. Ich muss nicht mal mehr einen Blick auf die Daten des Antwortdatagramms werfen.

Testautomatisierung mit bats-core

Um diesen oder später auch weitere Tests automatisiert ablaufen zu lassen, benötigt es etwas mehr. Automatisiserte Testsuiten lassen sich idealerweise mit einem einzigen Befehl aufrufen und kümmern sich um die Vorbereitung der Tests und das Aufräumen hinterher.

Bei diesen Tests arbeite ich mit der Shell und ein paar zusätzlichen Programmen. Dafür gibt es mit bats (dem Bash Automated Testing System) ein Framework, dass genau das Gewünschte kann und zusätzlich die Ergebnisse mit dem Test Anything Protocol (TAP) zurück geben kann.

Bei bats kann ich mit der Shellfunktion setup() vor jedem einzelnen Test die Umgebung vorbereiten oder mit setup_file() einmalig für alle Tests in einem Skript. Analog dazu kann ich mit teardown() nach jedem einzelnen Test aufräumen oder mit teardown_file() einmalig nach dem letzten Test.

Für meine Zwecke reicht es, zu Beginn der Tests das Program itip im Hintergrund zu starten und nach dem letzten Test zu beenden.

Dementsprechend verwende ich die folgende Funktion für die Vorbereitung:

setup_file() {
  ( local pid=$(exec sh -c 'echo "$PPID"')
    echo $pid > itip.pid
    exec ./itip > itip.out 2>&1
  ) &
  sleep 1
}

Nach dem letzten Test beende ich itip mit der folgenden Funktion:

teardown_file() {
  kill $(cat itip.pid)
  wait $(cat itip.pid) 2>/dev/null || true
}

Der Test für IKE_SA_INIT benutzt eine Funktion für den eigentlichen Ablauf und sieht so aus:

ike_sa_init_01() {
  xxd -r t/ike_sa_init-01-req.dump \
  | nc -u -W1 127.0.0.1 500 \
  | xxd > t/ike_sa_init-01-res.dump

  cmp t/ike_sa_init-01-{exp,res}.dump
}

@test "IKE_SA_INIT 01" {
  run ike_sa_init_01
  [ "$status" -eq 0 ]
}

Alle Testskripte lege ich im Unterverzeichnis t/ ab und rufe sie mit dem folgenden Befehl auf:

$ bats t
 ✓ IKE_SA_INIT 01

1 test, 0 failures

Möchte ich lieber die Ausgabe im Test Anything Protocol, wie von Perl-Tests gewohnt, lautet der Aufruf:

$ bats --tap t
1..1
ok 1 IKE_SA_INIT 01