# 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") ''; }