diff --git a/.github/workflows/c-compile.yml b/.github/workflows/c-compile.yml index 9c94b82..4eb1cc8 100644 --- a/.github/workflows/c-compile.yml +++ b/.github/workflows/c-compile.yml @@ -14,3 +14,12 @@ jobs: run: | cd nat46/modules KCPPFLAGS='-DNAT46_VERSION=\"test\" -Werror' make + - name: Install kvm + run: | + sudo apt-get install qemu-system-x86 + - name: Install and run test harness + run: | + cd test-harness + ./run-test-harness + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4510f63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +test-harness/test-data/captured/* +test-harness/myinit +test-harness/initrd.gz +test-harness/initrd/ +test-harness/my-kernel + +nat46/modules/.* +nat46/modules/nat46.mod* +nat46/modules/Module.symvers +nat46/modules/modules.order +nat46/modules/*.o +nat46/modules/*.ko diff --git a/nat46/modules/nat46-core.h b/nat46/modules/nat46-core.h index aca331b..4bb0f5f 100644 --- a/nat46/modules/nat46-core.h +++ b/nat46/modules/nat46-core.h @@ -64,7 +64,7 @@ typedef struct { int debug; int npairs; - nat46_xlate_rulepair_t pairs[0]; /* npairs */ + nat46_xlate_rulepair_t pairs[]; /* npairs */ } nat46_instance_t; int nat46_ipv6_input(struct sk_buff *old_skb); diff --git a/test-harness/README.md b/test-harness/README.md new file mode 100644 index 0000000..42361fc --- /dev/null +++ b/test-harness/README.md @@ -0,0 +1,36 @@ +This is a (long overdue) test harness for the nat46.ko module. + +The goal is to trivially run it in two modes: + +- as a CI in github +- as a local test bench + +The high-level setup is more or less how this module was originally written some years ago: +a kernel running under KVM, mounting the host filesystem via p9, and loading +the module from there. + +However, as an experiment, I decided to do it in a much more lightweight fashion - +rather than going the classic route of building the disk image of the root device and +mounting that, with the help of LLM I built a custom /init, which gives enough of +a shell-like experience to do the system bring-up and tests within it. + +In part it is done to test-drive another project of mine: https://github.com/ayourtch/oside, +whose purpose in life is to allow to relatively easily do packet manipulations from Rust. + +Admittedly, it is a fair bit less feature-complete than Scapy at this point, but not having +to deal with installation and management of Python inside the disk image is arguably worth the hassle. + +The init shell has a command "oside", which gives a rudimentary TUI to edit the jsonl files with +the packets. Also one can use "pcap2json" command inside the shell to convert the files inside the shell. + +The tests are sitting under tests/ directory, and need to be executed one-by-one from startup.run - this +script is executed immediately at bootup. In the future the tests *may* be moved into autoexec.run, which +is also run at startup, but with a delay that allows the user to break into interactive shell. + +Each test should configure nat46 device(s) as it sees fit, inject some packets, and capture the expected +packets into test-data/captured/*testname*.jsonl. After the VM run concludes, each captured file is compared +with its sibling file in test-data/expected/*testname*.jsonl, and is expected to be identical, modulo timestamps. + +Admittedly this is not *too* much of a framework, but hopefully should allow for some relatively useful tests +to be done relatively easily. + diff --git a/test-harness/autoexec.run b/test-harness/autoexec.run new file mode 100644 index 0000000..68f316a --- /dev/null +++ b/test-harness/autoexec.run @@ -0,0 +1 @@ +echo This is a test diff --git a/test-harness/bin/check-test-result b/test-harness/bin/check-test-result new file mode 100755 index 0000000..351e42d --- /dev/null +++ b/test-harness/bin/check-test-result @@ -0,0 +1,13 @@ +#!/bin/sh +set -eux +TESTNAME=$1 + +rm -f /tmp/expected.jsonl +rm -f /tmp/captured.jsonl + + +jq 'del(.timestamp_us)' test-data/expected/${TESTNAME}.jsonl >/tmp/expected.jsonl +jq 'del(.timestamp_us)' test-data/captured/${TESTNAME}.jsonl >/tmp/captured.jsonl + +diff -c /tmp/expected.jsonl /tmp/captured.jsonl + diff --git a/test-harness/run-test-harness b/test-harness/run-test-harness new file mode 100755 index 0000000..5fdf8be --- /dev/null +++ b/test-harness/run-test-harness @@ -0,0 +1,105 @@ +#!/bin/bash +set -eux + +# latest tag for https://github.com/ayourtch/nat46-kvm-test-harness/ +MYINIT_VERSION="v0.0.10" + +find_kernel_and_modules() { + local kernel_version=$(uname -r) + + # Potential kernel paths + local potential_kernel_paths=( + "/boot/vmlinuz-${kernel_version}" + "/boot/vmlinux-${kernel_version}" + "/boot/linux-${kernel_version}" + ) + + # Potential module path locations + local potential_module_paths=( + "/lib/modules/${kernel_version}" + "/usr/lib/modules/${kernel_version}" + "/usr/local/lib/modules/${kernel_version}" + ) + + local kernel_file="" + for path in "${potential_kernel_paths[@]}"; do + if [ -f "$path" ]; then + kernel_file="$path" + break + fi + done + + local module_path="" + for path in "${potential_module_paths[@]}"; do + if [ -d "$path" ]; then + module_path="$path" + break + fi + done + + if [ -z "$kernel_file" ]; then + echo "Error: Could not find kernel file for version ${kernel_version}" >&2 + return 1 + fi + + if [ -z "$module_path" ]; then + echo "Error: Could not find module directory for version ${kernel_version}" >&2 + return 1 + fi + + # Export environment variables + export KERNEL_FILE="$kernel_file" + export KERNEL_MODULE_PATH="$module_path" + + # Print out for verification + echo "Kernel File: $KERNEL_FILE" + echo "Kernel Module Path: $KERNEL_MODULE_PATH" +} + +find_kernel_and_modules + +# download myinit - if doing local tests, feel free to make a symlink +[ -e myinit ] || (wget https://github.com/ayourtch/nat46-kvm-test-harness/releases/download/${MYINIT_VERSION}/myinit && chmod +x myinit) + +# build the initrd +rm -rf initrd && mkdir initrd +find ${KERNEL_MODULE_PATH} -name "netfs.ko*" -exec cp {} initrd/ \; +find ${KERNEL_MODULE_PATH} -name "9pnet.ko*" -exec cp {} initrd/ \; +find ${KERNEL_MODULE_PATH} -name "9pnet_virtio.ko*" -exec cp {} initrd/ \; +find ${KERNEL_MODULE_PATH} -name "9p.ko*" -exec cp {} initrd/ \; +find ${KERNEL_MODULE_PATH} -name "nf_defrag_ipv6.ko*" -exec cp {} initrd/ \; + +# copy the nat46 module from the repo +find .. -name nat46.ko -exec cp {} initrd/ \; + +cp myinit initrd/init +( cd initrd/ && find . | cpio -o -H newc ) | gzip > initrd.gz + +export HOST_SRC_PATH=$(git rev-parse --show-toplevel) + +[ -f my-kernel ] || (sudo cp ${KERNEL_FILE} my-kernel && sudo chmod 644 my-kernel) + +kvm -kernel my-kernel \ + -initrd initrd.gz \ + -net nic,model=virtio,macaddr=52:54:00:12:34:56 \ + -net user,hostfwd=tcp:127.0.0.1:4444-:22 \ + -append 'console=hvc0' \ + -chardev stdio,id=stdio,mux=on,signal=off \ + -device virtio-serial-pci \ + -device virtconsole,chardev=stdio \ + -mon chardev=stdio \ + -display none \ + -fsdev local,id=fs1,path=${HOST_SRC_PATH},security_model=none \ + -s \ + -device virtio-9p-pci,fsdev=fs1,mount_tag=host-code + + +# collect the test names +cd tests +ALL_TESTS=$(ls) +cd .. + +# verify that all the tests are matching the expectations +for t in $ALL_TESTS; do + bin/check-test-result $t +done diff --git a/test-harness/startup.run b/test-harness/startup.run new file mode 100644 index 0000000..0a5b00b --- /dev/null +++ b/test-harness/startup.run @@ -0,0 +1,51 @@ +# This file is executed after the host filesystem is mounted +# Modify this file to customize your test environment + +# List root directory to verify mount +ls / + +# Load additional kernel modules +insmod /nf_defrag_ipv6.ko +insmod /nat46.ko + +ifconfig lo 127.0.0.1 netmask 255.0.0.0 +ifconfig lo ::1/128 +ifconfig lo up + +# Setup TAP interfaces +mknod /dev/net/tun c 10 200 +tap add tap0 +ifconfig tap0 192.168.1.1 netmask 255.255.255.0 +ifconfig tap0 2001:db8:1::1/64 +ifconfig tap0 up +tap add tap1 +ifconfig tap1 2001:db8::1/64 +ifconfig tap1 up +ifconfig + +# set the mac addresses +ifconfig tap0 hw ether 0E:86:3C:CD:51:CA +ifconfig tap1 hw ether 00:11:22:33:44:55 +ifconfig nat46dev up + +# add phantom hosts +fakehost add tap1 fe80::1 icmp router +fakehost add tap1 2001:db8::2 icmp router +fakehost add tap1 fe80::2 icmp +# ping ipv6 default gateway global +# ping 2001:db8::2 +ping fe80::1%tap1 + +ip -6 route add ::/0 via fe80::1 dev tap1 +ifconfig tap0 up promisc + +# start droptrace for diagnostics +mkdir -p /sys/kernel +droptrace start + +echo 1 >/proc/sys/net/ipv4/ip_forward +echo 1 >/proc/sys/net/ipv6/conf/all/forwarding + +run /mnt/host/test-harness/tests/basic/test.run + + diff --git a/test-harness/test-data/captured/.keep b/test-harness/test-data/captured/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test-harness/test-data/expected/basic.jsonl b/test-harness/test-data/expected/basic.jsonl new file mode 100644 index 0000000..00d0647 --- /dev/null +++ b/test-harness/test-data/expected/basic.jsonl @@ -0,0 +1,2 @@ +{"timestamp_us":1761854670878200,"direction":"tx","layers":[{"layertype":"Ip","version":4,"ihl":5,"tos":0,"len":84,"id":4,"flags":{"reserved":false,"dont_fragment":false,"more_fragments":false,"fragment_offset":0},"ttl":254,"proto":6,"chksum":60035,"src":"192.168.1.100","dst":"8.8.8.8","options":[]},{"layertype":"Tcp","sport":12345,"dport":81,"seq":0,"ack":0,"dataofs":5,"reserved":0,"flags":2,"window":8192,"chksum":60790,"urgptr":0},{"layertype":"raw","data":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42]}]} +{"timestamp_us":1761854670881112,"direction":"rx","layers":[{"layertype":"Ipv6","version_class":1610612736,"payload_length":64,"next_header":6,"hop_limit":254,"src":"2001:db8:1:1::1","dst":"2001:4860:4860::8888"},{"layertype":"Tcp","sport":12345,"dport":81,"seq":0,"ack":0,"dataofs":5,"reserved":0,"flags":2,"window":8192,"chksum":22663,"urgptr":0},{"layertype":"raw","data":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42]}]} diff --git a/test-harness/tests/basic/inject-tap0.jsonl b/test-harness/tests/basic/inject-tap0.jsonl new file mode 100644 index 0000000..156d46f --- /dev/null +++ b/test-harness/tests/basic/inject-tap0.jsonl @@ -0,0 +1,5 @@ +{"timestamp_us":0,"layers":[{"layertype":"ether","dst":"FF:FF:FF:FF:FF:FF","src":"00:00:00:00:00:00","etype":0}]} +{"timestamp_us":1000000,"layers":[{"layertype":"ether","dst":"0E:86:3C:CD:51:CA","src":"52:55:0A:00:02:02","etype":2048},{"layertype":"Ip","version":4,"ihl":5,"tos":0,"len":84,"id":4,"flags":{"reserved":false,"dont_fragment":false,"more_fragments":false,"fragment_offset":0},"ttl":255,"proto":6,"chksum":59779,"src":"192.168.1.100","dst":"8.8.8.8","options":[]},{"layertype":"Tcp","sport":12345,"dport":81,"seq":0,"ack":0,"dataofs":5,"reserved":0,"flags":2,"window":8192,"chksum":60790,"urgptr":0},{"layertype":"raw","data":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42]}]} +{"timestamp_us":1000000,"layers":[{"layertype":"ether","dst":"86:8B:D5:ED:1E:03","src":"52:55:0A:00:02:02","etype":34525},{"layertype":"Ipv6","version_class":1610612736,"payload_length":65,"next_header":17,"hop_limit":64,"src":"2001:db8::1","dst":"2001:db8::2"},{"layertype":"Udp","sport":6666,"dport":888,"len":65,"chksum":49112},{"layertype":"raw","data":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42]}]} +{"timestamp_us":2000000,"layers":[{"layertype":"ether","dst":"FF:FF:FF:FF:FF:FF","src":"00:00:00:00:00:00","etype":0}]} + diff --git a/test-harness/tests/basic/test.run b/test-harness/tests/basic/test.run new file mode 100644 index 0000000..1e419ce --- /dev/null +++ b/test-harness/tests/basic/test.run @@ -0,0 +1,21 @@ +echo add nat46dev > /proc/net/nat46/control +ifconfig nat46dev up +ip route add 0.0.0.0/0 dev nat46dev + +echo config nat46dev debug 255 >/proc/net/nat46/control +echo insert nat46dev local.v4 192.168.1.100/32 local.v6 2001:db8:1:1::1/128 local.style NONE remote.v4 8.8.8.8/32 remote.v6 2001:4860:4860::8888/128 remote.style NONE >/proc/net/nat46/control +echo insert nat46dev local.v4 192.168.1.101/32 local.v6 2001:db8:1:1::2/128 local.style NONE remote.v4 8.8.8.8/32 remote.v6 2001:4860:4860::8888/128 remote.style NONE >/proc/net/nat46/control + +capture start nat46dev /mnt/host/test-harness/test-data/captured/basic.jsonl + +# inject packets +inject /mnt/host/test-harness/tests/basic/inject-tap0.jsonl tap0 + +# remove the nat46 device +echo del nat46dev >/proc/net/nat46/control + +capture stop all + +echo nat46 packets: +cat /mnt/host/test-harness/test-data/captured/basic.jsonl +