at master 6.9 kB view raw
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. 6# 7# Notes: 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 14 15{ lib, pkgs, ... }: 16 17let 18 19 # Common network setup 20 baseNetwork = { 21 # shared hosts file 22 networking.extraHosts = lib.mkVMOverride '' 23 203.0.113.1 router 24 203.0.113.2 server 25 2001:db8::2 inner 26 192.168.1.1 client 27 ''; 28 # open a port for testing 29 networking.firewall.allowedUDPPorts = [ 1234 ]; 30 }; 31 32 # Common IPsec configuration 33 baseTunnel = { 34 services.libreswan.enable = true; 35 environment.etc."ipsec.d/tunnel.secrets" = { 36 text = ''@server %any : PSK "j1JbIi9WY07rxwcNQ6nbyThKCf9DGxWOyokXIQcAQUnafsNTUJxfsxwk9WYK8fHj"''; 37 mode = "600"; 38 }; 39 }; 40 41 # Helpers to add a static IP address on an interface 42 setAddress4 = iface: addr: { 43 networking.interfaces.${iface}.ipv4.addresses = lib.mkVMOverride [ 44 { 45 address = addr; 46 prefixLength = 24; 47 } 48 ]; 49 }; 50 setAddress6 = iface: addr: { 51 networking.interfaces.${iface}.ipv6.addresses = lib.mkVMOverride [ 52 { 53 address = addr; 54 prefixLength = 64; 55 } 56 ]; 57 }; 58 59in 60 61{ 62 name = "libreswan-nat"; 63 meta = with lib.maintainers; { 64 maintainers = [ rnhmjoj ]; 65 }; 66 67 nodes.router = 68 { pkgs, ... }: 69 lib.mkMerge [ 70 baseNetwork 71 (setAddress4 "eth1" "203.0.113.1") 72 (setAddress4 "eth2" "192.168.1.1") 73 { 74 virtualisation.vlans = [ 75 1 76 2 77 ]; 78 environment.systemPackages = [ pkgs.tcpdump ]; 79 networking.nat = { 80 enable = true; 81 externalInterface = "eth1"; 82 internalInterfaces = [ "eth2" ]; 83 }; 84 networking.firewall.trustedInterfaces = [ "eth2" ]; 85 } 86 ]; 87 88 nodes.inner = lib.mkMerge [ 89 baseNetwork 90 (setAddress6 "eth1" "2001:db8::2") 91 { virtualisation.vlans = [ 3 ]; } 92 ]; 93 94 nodes.server = lib.mkMerge [ 95 baseNetwork 96 baseTunnel 97 (setAddress4 "eth1" "203.0.113.2") 98 (setAddress6 "eth2" "2001:db8::1") 99 { 100 virtualisation.vlans = [ 101 1 102 3 103 ]; 104 networking.firewall.allowedUDPPorts = [ 105 500 106 4500 107 ]; 108 networking.firewall.allowedTCPPorts = [ 993 ]; 109 110 # see https://github.com/NixOS/nixpkgs/pull/310857 111 networking.firewall.checkReversePath = false; 112 113 boot.kernel.sysctl = { 114 # enable forwarding packets 115 "net.ipv6.conf.all.forwarding" = 1; 116 "net.ipv4.conf.all.forwarding" = 1; 117 # enable NDP proxy for VPN clients 118 "net.ipv6.conf.all.proxy_ndp" = 1; 119 }; 120 121 services.libreswan.configSetup = "listen-tcp=yes"; 122 services.libreswan.connections.tunnel = '' 123 # server 124 left=203.0.113.2 125 leftid=@server 126 leftsubnet=::/0 127 leftupdown=${pkgs.writeScript "updown" '' 128 # act as NDP proxy for VPN clients 129 if test "$PLUTO_VERB" = up-client-v6; then 130 ip neigh add proxy "$PLUTO_PEER_CLIENT_NET" dev eth2 131 fi 132 if test "$PLUTO_VERB" = down-client-v6; then 133 ip neigh del proxy "$PLUTO_PEER_CLIENT_NET" dev eth2 134 fi 135 ''} 136 137 # clients 138 right=%any 139 rightaddresspool=2001:db8:0:0:c::/97 140 modecfgdns=2001:db8::1 141 142 # clean up vanished clients 143 dpddelay=30 144 145 auto=add 146 keyexchange=ikev2 147 rekey=no 148 narrowing=yes 149 fragmentation=yes 150 authby=secret 151 152 leftikeport=993 153 retransmit-timeout=10s 154 ''; 155 } 156 ]; 157 158 nodes.client = lib.mkMerge [ 159 baseNetwork 160 baseTunnel 161 (setAddress4 "eth1" "192.168.1.2") 162 { 163 virtualisation.vlans = [ 2 ]; 164 networking.defaultGateway = { 165 address = "192.168.1.1"; 166 interface = "eth1"; 167 }; 168 services.libreswan.connections.tunnel = '' 169 # client 170 left=%defaultroute 171 leftid=@client 172 leftmodecfgclient=yes 173 leftsubnet=::/0 174 175 # server 176 right=203.0.113.2 177 rightid=@server 178 rightsubnet=::/0 179 180 auto=add 181 narrowing=yes 182 rekey=yes 183 fragmentation=yes 184 authby=secret 185 186 # fallback when UDP is blocked 187 enable-tcp=fallback 188 tcp-remoteport=993 189 retransmit-timeout=5s 190 ''; 191 } 192 ]; 193 194 testScript = '' 195 def client_to_host(machine, msg: str): 196 """ 197 Sends a message from client to server 198 """ 199 machine.execute("nc -lu :: 1234 >/tmp/msg &") 200 client.sleep(1) 201 client.succeed(f"echo '{msg}' | nc -uw 0 {machine.name} 1234") 202 client.sleep(1) 203 machine.succeed(f"grep '{msg}' /tmp/msg") 204 205 206 def eavesdrop(): 207 """ 208 Starts eavesdropping on the router 209 """ 210 match = "udp port 1234" 211 router.execute(f"tcpdump -i eth1 -c 1 -Avv {match} >/tmp/log &") 212 213 214 start_all() 215 216 with subtest("Network is up"): 217 client.wait_until_succeeds("ping -c1 server") 218 client.succeed("systemctl restart ipsec") 219 server.succeed("systemctl restart ipsec") 220 221 with subtest("Router can eavesdrop cleartext traffic"): 222 eavesdrop() 223 client_to_host(server, "I secretly love turnip") 224 router.sleep(1) 225 router.succeed("grep turnip /tmp/log") 226 227 with subtest("Libreswan is ready"): 228 client.wait_for_unit("ipsec") 229 server.wait_for_unit("ipsec") 230 client.succeed("ipsec checkconfig") 231 server.succeed("ipsec checkconfig") 232 233 with subtest("Client can't ping VPN host"): 234 client.fail("ping -c1 inner") 235 236 with subtest("Client can start the tunnel"): 237 client.succeed("ipsec start tunnel") 238 client.succeed("ip -6 addr show lo | grep -q 2001:db8:0:0:c") 239 240 with subtest("Client can ping VPN host"): 241 client.wait_until_succeeds("ping -c1 2001:db8::1") 242 client.succeed("ping -c1 inner") 243 244 with subtest("Eve no longer can eavesdrop"): 245 eavesdrop() 246 client_to_host(inner, "Just kidding, I actually like rhubarb") 247 router.sleep(1) 248 router.fail("grep rhubarb /tmp/log") 249 250 with subtest("TCP fallback is available"): 251 server.succeed("iptables -I nixos-fw -p udp -j DROP") 252 client.succeed("ipsec restart") 253 client.execute("ipsec start tunnel") 254 client.wait_until_succeeds("ping -c1 inner") 255 ''; 256}