1 # This test sets up an IPsec VPN server that allows a client behind an IPv4 NAT
2 # router to access the IPv6 internet. We check that the client initially can't
3 # ping an IPv6 hosts and its connection to the server can be eavesdropped by
4 # the router, but once the IPsec tunnel is enstablished it can talk to an
5 # IPv6-only host and the connection is secure.
8 # - the VPN is implemented using policy-based routing.
9 # - the client is assigned an IPv6 address from the same /64 subnet
10 # of the server, without DHCPv6 or SLAAC.
11 # - the server acts as NDP proxy for the client, so that the latter
12 # becomes reachable at its assigned IPv6 via the server.
13 # - the client falls back to TCP if UDP is blocked
19 # Common network setup
22 networking.extraHosts = lib.mkVMOverride ''
28 # open a port for testing
29 networking.firewall.allowedUDPPorts = [ 1234 ];
32 # Common IPsec configuration
34 services.libreswan.enable = true;
35 environment.etc."ipsec.d/tunnel.secrets" =
36 { text = ''@server %any : PSK "j1JbIi9WY07rxwcNQ6nbyThKCf9DGxWOyokXIQcAQUnafsNTUJxfsxwk9WYK8fHj"'';
41 # Helpers to add a static IP address on an interface
42 setAddress4 = iface: addr: {
43 networking.interfaces.${iface}.ipv4.addresses =
44 lib.mkVMOverride [ { address = addr; prefixLength = 24; } ];
46 setAddress6 = iface: addr: {
47 networking.interfaces.${iface}.ipv6.addresses =
48 lib.mkVMOverride [ { address = addr; prefixLength = 64; } ];
54 name = "libreswan-nat";
55 meta = with lib.maintainers; {
56 maintainers = [ rnhmjoj ];
59 nodes.router = { pkgs, ... }: lib.mkMerge [
61 (setAddress4 "eth1" "203.0.113.1")
62 (setAddress4 "eth2" "192.168.1.1")
64 virtualisation.vlans = [ 1 2 ];
65 environment.systemPackages = [ pkgs.tcpdump ];
68 externalInterface = "eth1";
69 internalInterfaces = [ "eth2" ];
71 networking.firewall.trustedInterfaces = [ "eth2" ];
75 nodes.inner = lib.mkMerge [
77 (setAddress6 "eth1" "2001:db8::2")
78 { virtualisation.vlans = [ 3 ]; }
81 nodes.server = lib.mkMerge [
84 (setAddress4 "eth1" "203.0.113.2")
85 (setAddress6 "eth2" "2001:db8::1")
87 virtualisation.vlans = [ 1 3 ];
88 networking.firewall.allowedUDPPorts = [ 500 4500 ];
89 networking.firewall.allowedTCPPorts = [ 993 ];
91 # see https://github.com/NixOS/nixpkgs/pull/310857
92 networking.firewall.checkReversePath = false;
94 boot.kernel.sysctl = {
95 # enable forwarding packets
96 "net.ipv6.conf.all.forwarding" = 1;
97 "net.ipv4.conf.all.forwarding" = 1;
98 # enable NDP proxy for VPN clients
99 "net.ipv6.conf.all.proxy_ndp" = 1;
102 services.libreswan.configSetup = "listen-tcp=yes";
103 services.libreswan.connections.tunnel = ''
108 leftupdown=${pkgs.writeScript "updown" ''
109 # act as NDP proxy for VPN clients
110 if test "$PLUTO_VERB" = up-client-v6; then
111 ip neigh add proxy "$PLUTO_PEER_CLIENT_NET" dev eth2
113 if test "$PLUTO_VERB" = down-client-v6; then
114 ip neigh del proxy "$PLUTO_PEER_CLIENT_NET" dev eth2
120 rightaddresspool=2001:db8:0:0:c::/97
121 modecfgdns=2001:db8::1
123 # clean up vanished clients
134 retransmit-timeout=10s
139 nodes.client = lib.mkMerge [
142 (setAddress4 "eth1" "192.168.1.2")
144 virtualisation.vlans = [ 2 ];
145 networking.defaultGateway = {
146 address = "192.168.1.1";
149 services.libreswan.connections.tunnel = ''
153 leftmodecfgclient=yes
167 # fallback when UDP is blocked
170 retransmit-timeout=5s
177 def client_to_host(machine, msg: str):
179 Sends a message from client to server
181 machine.execute("nc -lu :: 1234 >/tmp/msg &")
183 client.succeed(f"echo '{msg}' | nc -uw 0 {machine.name} 1234")
185 machine.succeed(f"grep '{msg}' /tmp/msg")
190 Starts eavesdropping on the router
192 match = "udp port 1234"
193 router.execute(f"tcpdump -i eth1 -c 1 -Avv {match} >/tmp/log &")
198 with subtest("Network is up"):
199 client.wait_until_succeeds("ping -c1 server")
200 client.succeed("systemctl restart ipsec")
201 server.succeed("systemctl restart ipsec")
203 with subtest("Router can eavesdrop cleartext traffic"):
205 client_to_host(server, "I secretly love turnip")
207 router.succeed("grep turnip /tmp/log")
209 with subtest("Libreswan is ready"):
210 client.wait_for_unit("ipsec")
211 server.wait_for_unit("ipsec")
212 client.succeed("ipsec checkconfig")
213 server.succeed("ipsec checkconfig")
215 with subtest("Client can't ping VPN host"):
216 client.fail("ping -c1 inner")
218 with subtest("Client can start the tunnel"):
219 client.succeed("ipsec start tunnel")
220 client.succeed("ip -6 addr show lo | grep -q 2001:db8:0:0:c")
222 with subtest("Client can ping VPN host"):
223 client.wait_until_succeeds("ping -c1 2001:db8::1")
224 client.succeed("ping -c1 inner")
226 with subtest("Eve no longer can eavesdrop"):
228 client_to_host(inner, "Just kidding, I actually like rhubarb")
230 router.fail("grep rhubarb /tmp/log")
232 with subtest("TCP fallback is available"):
233 server.succeed("iptables -I nixos-fw -p udp -j DROP")
234 client.succeed("ipsec restart")
235 client.execute("ipsec start tunnel")
236 client.wait_until_succeeds("ping -c1 inner")