about summary refs log tree commit diff
path: root/nixos/tests/libreswan-nat.nix
blob: 973e304f9e3a36969782feb3c4282dd202424b3f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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")
    '';
}