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}