From 7c021fdfcd602c0364c97a658d8748663389e3b1 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Thu, 9 May 2024 20:24:56 +0200 Subject: [PATCH] nixos/tests/libreswan-nat: add test --- nixos/tests/all-tests.nix | 1 + nixos/tests/libreswan-nat.nix | 238 ++++++++++++++++++++ pkgs/tools/networking/libreswan/default.nix | 2 +- 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/libreswan-nat.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index eca040ea85fb..ba772bcd986f 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -501,6 +501,7 @@ in { librenms = handleTest ./librenms.nix {}; libresprite = handleTest ./libresprite.nix {}; libreswan = runTest ./libreswan.nix; + libreswan-nat = runTest ./libreswan-nat.nix; librewolf = handleTest ./firefox.nix { firefoxPackage = pkgs.librewolf; }; libuiohook = handleTest ./libuiohook.nix {}; libvirtd = handleTest ./libvirtd.nix {}; diff --git a/nixos/tests/libreswan-nat.nix b/nixos/tests/libreswan-nat.nix new file mode 100644 index 000000000000..973e304f9e3a --- /dev/null +++ b/nixos/tests/libreswan-nat.nix @@ -0,0 +1,238 @@ +# This test sets up an IPsec VPN server that allows a client behind an IPv4 NAT +# router to access the IPv6 internet. We check that the client initially can't +# ping an IPv6 hosts and its connection to the server can be eavesdropped by +# the router, but once the IPsec tunnel is enstablished it can talk to an +# IPv6-only host and the connection is secure. +# +# Notes: +# - the VPN is implemented using policy-based routing. +# - the client is assigned an IPv6 address from the same /64 subnet +# of the server, without DHCPv6 or SLAAC. +# - the server acts as NDP proxy for the client, so that the latter +# becomes reachable at its assigned IPv6 via the server. +# - the client falls back to TCP if UDP is blocked + +{ lib, pkgs, ... }: + +let + + # Common network setup + baseNetwork = { + # shared hosts file + networking.extraHosts = lib.mkVMOverride '' + 203.0.113.1 router + 203.0.113.2 server + 2001:db8::2 inner + 192.168.1.1 client + ''; + # open a port for testing + networking.firewall.allowedUDPPorts = [ 1234 ]; + }; + + # Common IPsec configuration + baseTunnel = { + services.libreswan.enable = true; + environment.etc."ipsec.d/tunnel.secrets" = + { text = ''@server %any : PSK "j1JbIi9WY07rxwcNQ6nbyThKCf9DGxWOyokXIQcAQUnafsNTUJxfsxwk9WYK8fHj"''; + mode = "600"; + }; + }; + + # Helpers to add a static IP address on an interface + setAddress4 = iface: addr: { + networking.interfaces.${iface}.ipv4.addresses = + lib.mkVMOverride [ { address = addr; prefixLength = 24; } ]; + }; + setAddress6 = iface: addr: { + networking.interfaces.${iface}.ipv6.addresses = + lib.mkVMOverride [ { address = addr; prefixLength = 64; } ]; + }; + +in + +{ + name = "libreswan-nat"; + meta = with lib.maintainers; { + maintainers = [ rnhmjoj ]; + }; + + nodes.router = { pkgs, ... }: lib.mkMerge [ + baseNetwork + (setAddress4 "eth1" "203.0.113.1") + (setAddress4 "eth2" "192.168.1.1") + { + virtualisation.vlans = [ 1 2 ]; + environment.systemPackages = [ pkgs.tcpdump ]; + networking.nat = { + enable = true; + externalInterface = "eth1"; + internalInterfaces = [ "eth2" ]; + }; + networking.firewall.trustedInterfaces = [ "eth2" ]; + } + ]; + + nodes.inner = lib.mkMerge [ + baseNetwork + (setAddress6 "eth1" "2001:db8::2") + { virtualisation.vlans = [ 3 ]; } + ]; + + nodes.server = lib.mkMerge [ + baseNetwork + baseTunnel + (setAddress4 "eth1" "203.0.113.2") + (setAddress6 "eth2" "2001:db8::1") + { + virtualisation.vlans = [ 1 3 ]; + networking.firewall.allowedUDPPorts = [ 500 4500 ]; + networking.firewall.allowedTCPPorts = [ 993 ]; + + # see https://github.com/NixOS/nixpkgs/pull/310857 + networking.firewall.checkReversePath = false; + + boot.kernel.sysctl = { + # enable forwarding packets + "net.ipv6.conf.all.forwarding" = 1; + "net.ipv4.conf.all.forwarding" = 1; + # enable NDP proxy for VPN clients + "net.ipv6.conf.all.proxy_ndp" = 1; + }; + + services.libreswan.configSetup = "listen-tcp=yes"; + services.libreswan.connections.tunnel = '' + # server + left=203.0.113.2 + leftid=@server + leftsubnet=::/0 + leftupdown=${pkgs.writeScript "updown" '' + # act as NDP proxy for VPN clients + if test "$PLUTO_VERB" = up-client-v6; then + ip neigh add proxy "$PLUTO_PEER_CLIENT_NET" dev eth2 + fi + if test "$PLUTO_VERB" = down-client-v6; then + ip neigh del proxy "$PLUTO_PEER_CLIENT_NET" dev eth2 + fi + ''} + + # clients + right=%any + rightaddresspool=2001:db8:0:0:c::/97 + modecfgdns=2001:db8::1 + + # clean up vanished clients + dpddelay=30 + + auto=add + keyexchange=ikev2 + rekey=no + narrowing=yes + fragmentation=yes + authby=secret + + leftikeport=993 + retransmit-timeout=10s + ''; + } + ]; + + nodes.client = lib.mkMerge [ + baseNetwork + baseTunnel + (setAddress4 "eth1" "192.168.1.2") + { + virtualisation.vlans = [ 2 ]; + networking.defaultGateway = { + address = "192.168.1.1"; + interface = "eth1"; + }; + services.libreswan.connections.tunnel = '' + # client + left=%defaultroute + leftid=@client + leftmodecfgclient=yes + leftsubnet=::/0 + + # server + right=203.0.113.2 + rightid=@server + rightsubnet=::/0 + + auto=add + narrowing=yes + rekey=yes + fragmentation=yes + authby=secret + + # fallback when UDP is blocked + enable-tcp=fallback + tcp-remoteport=993 + retransmit-timeout=5s + ''; + } + ]; + + testScript = + '' + def client_to_host(machine, msg: str): + """ + Sends a message from client to server + """ + machine.execute("nc -lu :: 1234 >/tmp/msg &") + client.sleep(1) + client.succeed(f"echo '{msg}' | nc -uw 0 {machine.name} 1234") + client.sleep(1) + machine.succeed(f"grep '{msg}' /tmp/msg") + + + def eavesdrop(): + """ + Starts eavesdropping on the router + """ + match = "udp port 1234" + router.execute(f"tcpdump -i eth1 -c 1 -Avv {match} >/tmp/log &") + + + start_all() + + with subtest("Network is up"): + client.wait_until_succeeds("ping -c1 server") + client.succeed("systemctl restart ipsec") + server.succeed("systemctl restart ipsec") + + with subtest("Router can eavesdrop cleartext traffic"): + eavesdrop() + client_to_host(server, "I secretly love turnip") + router.sleep(1) + router.succeed("grep turnip /tmp/log") + + with subtest("Libreswan is ready"): + client.wait_for_unit("ipsec") + server.wait_for_unit("ipsec") + client.succeed("ipsec checkconfig") + server.succeed("ipsec checkconfig") + + with subtest("Client can't ping VPN host"): + client.fail("ping -c1 inner") + + with subtest("Client can start the tunnel"): + client.succeed("ipsec start tunnel") + client.succeed("ip -6 addr show lo | grep -q 2001:db8:0:0:c") + + with subtest("Client can ping VPN host"): + client.wait_until_succeeds("ping -c1 2001:db8::1") + client.succeed("ping -c1 inner") + + with subtest("Eve no longer can eavesdrop"): + eavesdrop() + client_to_host(inner, "Just kidding, I actually like rhubarb") + router.sleep(1) + router.fail("grep rhubarb /tmp/log") + + with subtest("TCP fallback is available"): + server.succeed("iptables -I nixos-fw -p udp -j DROP") + client.succeed("ipsec restart") + client.execute("ipsec start tunnel") + client.wait_until_succeeds("ping -c1 inner") + ''; +} diff --git a/pkgs/tools/networking/libreswan/default.nix b/pkgs/tools/networking/libreswan/default.nix index 07f35663752b..7dac682407bd 100644 --- a/pkgs/tools/networking/libreswan/default.nix +++ b/pkgs/tools/networking/libreswan/default.nix @@ -104,7 +104,7 @@ stdenv.mkDerivation rec { -i $out/bin/ipsec ''; - passthru.tests.libreswan = nixosTests.libreswan; + passthru.tests = { inherit (nixosTests) libreswan libreswan-nat; }; meta = with lib; { homepage = "https://libreswan.org";