diff --git a/tests/nat64-pref64/README.md b/tests/nat64-pref64/README.md new file mode 100644 index 0000000..c7c1008 --- /dev/null +++ b/tests/nat64-pref64/README.md @@ -0,0 +1,23 @@ +# NAT64 with PREF64 + +This configuration extends the [nat64-dns64](../nat64-dns64/) test by replacing DNS64 +functionality with PREF64. Instead of using a DNS64-enabled DNS server, the NAT64 gateway +now uses radvd (Router Advertisement Daemon) to advertise IPv6 prefixes that embed IPv4 addresses, +allowing the client to generate IPv6 addresses for IPv4 destinations automatically using clatd. + +```mermaid +flowchart LR + server["**Server** + nginx HTTP server + bind DNS server"] + + nat64gw["**NAT64 Gateway** + jool NAT64 + dnsmasq DNS server + radvd"] + + client["**Client** + clatd"] + + server <-- ipv4 only --> nat64gw <-- ipv6 only --> client +``` diff --git a/tests/nat64-pref64/client.nix b/tests/nat64-pref64/client.nix new file mode 100644 index 0000000..ecbc33f --- /dev/null +++ b/tests/nat64-pref64/client.nix @@ -0,0 +1,32 @@ +{ lib, pkgs, ... }: +{ + virtualisation.interfaces.eth1 = { + vlan = 2; + assignIP = false; + }; + + networking = { + interfaces.eth1 = { }; + nameservers = [ "2001:db8::1" ]; + }; + + services.clatd = { + enable = true; + settings = { + plat-prefix = "64:ff9b::/96"; + debug = 1; + }; + }; + + systemd.services.clatd.preStart = + let + ip = lib.getExe' pkgs.iproute2 "ip"; + jq = lib.getExe pkgs.jq; + in + '' + while [ $(${ip} -j -6 address show eth1 | ${jq} '.[] | .addr_info | map(select((.scope == "global") and (.tentative == true))) | length') -ne 0 ] + do + sleep 0.1 + done + ''; +} diff --git a/tests/nat64-pref64/default.nix b/tests/nat64-pref64/default.nix new file mode 100644 index 0000000..5c3e9eb --- /dev/null +++ b/tests/nat64-pref64/default.nix @@ -0,0 +1,114 @@ +{ + lib, + pkgs, + ... +}: +{ + name = "nat64-pref64"; + + defaults = { + networking = { + useDHCP = false; + firewall.enable = false; + }; + }; + + nodes = { + server = import ./server.nix { inherit pkgs; }; + nat64gw = import ./nat64gw.nix; + client = import ./client.nix { inherit lib pkgs; }; + }; + + interactive.nodes = lib.listToAttrs ( + map + (name: { + inherit name; + value.environment.systemPackages = with pkgs; [ + curl + dig + tcpdump + pwru + ]; + }) + [ + "server" + "client" + "nat64gw" + ] + ); + + testScript = + let + curl = lib.getExe pkgs.curl; + dig = lib.getExe pkgs.dig; + jq = lib.getExe pkgs.jq; + in + '' + start_all() + + for m in [server, nat64gw, client]: + m.wait_for_unit("network.target") + + server.wait_for_unit("nginx.service") + server.wait_for_unit("bind.service") + nat64gw.wait_for_unit("dnsmasq.service") + nat64gw.wait_for_unit("jool-nat64-eth2.service") + nat64gw.wait_for_unit("radvd.service") + + with subtest('ensure "public" dns server is working properly'): + assert server.succeed("${dig} +short A example.com").strip() == "192.0.2.2" + assert server.succeed("${dig} +short AAAA example.com").strip() == "" + + with subtest('ensure http server is online'): + server.succeed("${curl} -sI http://example.com") + + with subtest('ensure ipv4 network between server and gateway is working'): + server.succeed("ping -c 1 192.0.2.1") + nat64gw.succeed("ping -c 1 192.0.2.2") + + # first dig fails for whatever reason + nat64gw.succeed("${dig} A example.com @2001:db8::1") + + with subtest('ensure dns server on nat64gw works as expected'): + assert nat64gw.succeed("${dig} +short A example.com @2001:db8::1").strip() == "192.0.2.2" + assert nat64gw.succeed("${dig} +short AAAA example.com @2001:db8::1").strip() == "" + + with subtest('ensure ipv6 network between gateway and client is working'): + client.wait_until_succeeds(""" + ip -j -6 a sh eth1 | \ + ${jq} -r '.[] | .addr_info | .[] | select((.family == "inet6") and .dynamic == true) | .local' | \ + grep 2001:db8 + """) + client.succeed("ping -6 -c 1 2001:db8::1") + + with subtest('ensure dns works as expected from client, and there is no aaaa record'): + assert client.succeed("${dig} +short A example.com @2001:db8::1").strip() == "192.0.2.2" + assert client.succeed("${dig} +short AAAA example.com @2001:db8::1").strip() == "" + + with subtest('ensure nat64 works as expected'): + client.wait_until_succeeds("ping -6 -c 1 64:ff9b::192.0.2.2") + client.wait_until_succeeds("${curl} -sI http://[64:ff9b::192.0.2.2] | grep 'HTTP/1.1 200 OK'") + + # clat needs to be started after slaac, restarting it now should be sufficient + client.succeed("systemctl restart clatd") + client.wait_for_unit("clatd.service") + client.wait_until_succeeds("ip link show clat") + + with subtest('ensure clat uses correct address from ipv4 service continuity prefix'): + client.wait_until_succeeds(""" + ip -j -4 address sh clat | ${jq} -e -r '.[].addr_info.[].local == "192.0.0.1"' + """) + + with subtest('ensure clat created a ipv4 default route to attract ipv4 traffic'): + client.wait_until_succeeds(""" + ip -4 -j route show default | ${jq} -e -r '.[].dev == "clat"' + """) + + # for debugging purpose: + print(client.succeed("${curl} -v http://example.com")) + # connect to 192.0.2.2 port 80 from 192.0.0.1 port 37636 failed: No route to host + + with subtest('ensure clat works as expected'): + client.succeed("${curl} -sI http://example.com | grep 'HTTP/1.1 200 OK'") + ''; +} diff --git a/tests/nat64-pref64/nat64gw.nix b/tests/nat64-pref64/nat64gw.nix new file mode 100644 index 0000000..fd970c0 --- /dev/null +++ b/tests/nat64-pref64/nat64gw.nix @@ -0,0 +1,59 @@ +{ + virtualisation.interfaces.eth1 = { + vlan = 1; + assignIP = false; + }; + virtualisation.interfaces.eth2 = { + vlan = 2; + assignIP = false; + }; + + networking.interfaces.eth1 = { + ipv4.addresses = [ + { + address = "192.0.2.1"; + prefixLength = 24; + } + ]; + }; + + networking.interfaces.eth2 = { + ipv6.addresses = [ + { + address = "2001:db8::1"; + prefixLength = 64; + } + ]; + }; + + # nat64 + boot.kernelModules = [ "jool" ]; + networking.jool = { + enable = true; + nat64.eth2.framework = "netfilter"; + }; + + services.dnsmasq = { + enable = true; + alwaysKeepRunning = true; + settings = { + listen-address = "2001:db8::1"; + no-hosts = true; + no-resolv = true; + server = [ + "192.0.2.2" + ]; + }; + }; + + services.radvd = { + enable = true; + config = '' + interface eth2 { + AdvSendAdvert on; + prefix 2001:db8::/64 { }; + nat64prefix 64:ff9b::/96 { }; + }; + ''; + }; +} diff --git a/tests/nat64-pref64/server.nix b/tests/nat64-pref64/server.nix new file mode 100644 index 0000000..fe08cda --- /dev/null +++ b/tests/nat64-pref64/server.nix @@ -0,0 +1,50 @@ +{ pkgs, ... }: +{ + virtualisation.interfaces.eth1 = { + vlan = 1; + assignIP = false; + }; + networking.interfaces.eth1 = { + ipv4.addresses = [ + { + address = "192.0.2.2"; + prefixLength = 24; + } + ]; + }; + services.nginx = { + enable = true; + virtualHosts."example.com".locations."/".return = "200"; + }; + + # public dns server + services.resolved.enable = false; + services.bind = { + enable = true; + cacheNetworks = [ "0.0.0.0/0" ]; + # first line (comment) is to align config + extraOptions = '' + # + recursion yes; + auth-nxdomain no; + dnssec-validation no; + ''; + zones."example.com" = { + master = true; + file = pkgs.writeText "zone-example.com.conf" '' + $TTL 1 + @ IN SOA example.com. zonemaster.example.com. ( + 2023013100 ; serial number + 86400 ; refresh: 1d + 900 ; update retry: 15m + 604800 ; expiry: 1w + 3600 ) ; negative caching 1h + + @ IN NS example.com. + @ IN A 192.0.2.2 + + ''; + }; + }; + networking.nameservers = [ "127.0.0.1" ]; +}