summary refs log tree commit diff
path: root/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
blob: 37a89fc21e4426c4d0901da822832d2a3ef7ab7d (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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# This test verifies that we can request and assign IPv6 prefixes from upstream
# (e.g. ISP) routers.
# The setup consits of three VMs. One for the ISP, as your residential router
# and the third as a client machine in the residential network.
#
# There are two VLANs in this test:
# - VLAN 1 is the connection between the ISP and the router
# - VLAN 2 is the connection between the router and the client

import ./make-test-python.nix ({pkgs, ...}: {
  name = "systemd-networkd-ipv6-prefix-delegation";
  meta = with pkgs.lib.maintainers; {
    maintainers = [ andir ];
  };
  nodes = {

    # The ISP's routers job is to delegate IPv6 prefixes via DHCPv6. Like with
    # regular IPv6 auto-configuration it will also emit IPv6 router
    # advertisements (RAs). Those RA's will not carry a prefix but in contrast
    # just set the "Other" flag to indicate to the receiving nodes that they
    # should attempt DHCPv6.
    #
    # Note: On the ISPs device we don't really care if we are using networkd in
    # this example. That being said we can't use it (yet) as networkd doesn't
    # implement the serving side of DHCPv6. We will use ISC's well aged dhcpd6
    # for that task.
    isp = { lib, pkgs, ... }: {
      virtualisation.vlans = [ 1 ];
      networking = {
        useDHCP = false;
        firewall.enable = false;
        interfaces.eth1.ipv4.addresses = lib.mkForce []; # no need for legacy IP
        interfaces.eth1.ipv6.addresses = lib.mkForce [
          { address = "2001:DB8::1"; prefixLength = 64; }
        ];
      };

      # Since we want to program the routes that we delegate to the "customer"
      # into our routing table we must give dhcpd the required privs.
      systemd.services.dhcpd6.serviceConfig.AmbientCapabilities =
        [ "CAP_NET_ADMIN" ];

      services = {
        # Configure the DHCPv6 server
        #
        # We will hand out /48 prefixes from the subnet 2001:DB8:F000::/36.
        # That gives us ~8k prefixes. That should be enough for this test.
        #
        # Since (usually) you will not receive a prefix with the router
        # advertisements we also hand out /128 leases from the range
        # 2001:DB8:0000:0000:FFFF::/112.
        dhcpd6 = {
          enable = true;
          interfaces = [ "eth1" ];
          extraConfig = ''
            subnet6 2001:DB8::/36 {
              range6 2001:DB8:0000:0000:FFFF:: 2001:DB8:0000:0000:FFFF::FFFF;
              prefix6 2001:DB8:F000:: 2001:DB8:FFFF:: /48;
            }

            # This is the secret sauce. We have to extract the prefix and the
            # next hop when commiting the lease to the database.  dhcpd6
            # (rightfully) has not concept of adding routes to the systems
            # routing table. It really depends on the setup.
            #
            # In a production environment your DHCPv6 server is likely not the
            # router. You might want to consider BGP, custom NetConf calls, …
            # in those cases.
            on commit {
              set IP = pick-first-value(binary-to-ascii(16, 16, ":", substring(option dhcp6.ia-na, 16, 16)), "n/a");
              set Prefix = pick-first-value(binary-to-ascii(16, 16, ":", suffix(option dhcp6.ia-pd, 16)), "n/a");
              set PrefixLength = pick-first-value(binary-to-ascii(10, 8, ":", substring(suffix(option dhcp6.ia-pd, 17), 0, 1)), "n/a");
              log(concat(IP, " ", Prefix, " ", PrefixLength));
              execute("${pkgs.iproute2}/bin/ip", "-6", "route", "replace", concat(Prefix,"/",PrefixLength), "via", IP);
            }
          '';
        };

        # Finally we have to set up the router advertisements. While we could be
        # using networkd or bird for this task `radvd` is probably the most
        # venerable of them all. It was made explicitly for this purpose and
        # the configuration is much more straightforward than what networkd
        # requires.
        # As outlined above we will have to set the `Managed` flag as otherwise
        # the clients will not know if they should do DHCPv6. (Some do
        # anyway/always)
        radvd = {
          enable = true;
          config = ''
            interface eth1 {
              AdvSendAdvert on;
              AdvManagedFlag on;
              AdvOtherConfigFlag off; # we don't really have DNS or NTP or anything like that to distribute
              prefix ::/64 {
                AdvOnLink on;
                AdvAutonomous on;
              };
            };
          '';
        };

      };
    };

    # This will be our (residential) router that receives the IPv6 prefix (IA_PD)
    # and /128 (IA_NA) allocation.
    #
    # Here we will actually start using networkd.
    router = {
      virtualisation.vlans = [ 1 2 ];
      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";

      boot.kernel.sysctl = {
        # we want to forward packets from the ISP to the client and back.
        "net.ipv6.conf.all.forwarding" = 1;
      };

      networking = {
        useNetworkd = true;
        useDHCP = false;
        # Consider enabling this in production and generating firewall rules
        # for fowarding/input from the configured interfaces so you do not have
        # to manage multiple places
        firewall.enable = false;
      };

      systemd.network = {
        networks = {
          # systemd-networkd will load the first network unit file
          # that matches, ordered lexiographically by filename.
          # /etc/systemd/network/{40-eth1,99-main}.network already
          # exists. This network unit must be loaded for the test,
          # however, hence why this network is named such.

          # Configuration of the interface to the ISP.
          # We must request accept RAs and request the PD prefix.
          "01-eth1" = {
            name = "eth1";
            networkConfig = {
              Description = "ISP interface";
              IPv6AcceptRA = true;
              #DHCP = false; # no need for legacy IP
            };
            linkConfig = {
              # We care about this interface when talking about being "online".
              # If this interface is in the `routable` state we can reach
              # others and they should be able to reach us.
              RequiredForOnline = "routable";
            };
            # This configures the DHCPv6 client part towards the ISPs DHCPv6 server.
            dhcpV6Config = {
              # We have to include a request for a prefix in our DHCPv6 client
              # request packets.
              # Otherwise the upstream DHCPv6 server wouldn't know if we want a
              # prefix or not.  Note: On some installation it makes sense to
              # always force that option on the DHPCv6 server since there are
              # certain CPEs that are just not setting this field but happily
              # accept the delegated prefix.
              PrefixDelegationHint  = "::/48";
            };
            ipv6SendRAConfig = {
              # Let networkd know that we would very much like to use DHCPv6
              # to obtain the "managed" information. Not sure why they can't
              # just take that from the upstream RAs.
              Managed = true;
            };
          };

          # Interface to the client. Here we should redistribute a /64 from
          # the prefix we received from the ISP.
          "01-eth2" = {
            name = "eth2";
            networkConfig = {
              Description = "Client interface";
              # The client shouldn't be allowed to send us RAs, that would be weird.
              IPv6AcceptRA = false;

              # Delegate prefixes from the DHCPv6 PD pool.
              DHCPv6PrefixDelegation = true;
              IPv6SendRA = true;
            };

            # In a production environment you should consider setting these as well:
            # ipv6SendRAConfig = {
              #EmitDNS = true;
              #EmitDomains = true;
              #DNS= = "fe80::1"; # or whatever "well known" IP your router will have on the inside.
            # };

            # This adds a "random" ULA prefix to the interface that is being
            # advertised to the clients.
            # Not used in this test.
            # ipv6Prefixes = [
            #   {
            #     ipv6PrefixConfig = {
            #       AddressAutoconfiguration = true;
            #       PreferredLifetimeSec = 1800;
            #       ValidLifetimeSec = 1800;
            #     };
            #   }
            # ];
          };

          # finally we are going to add a static IPv6 unique local address to
          # the "lo" interface.  This will serve as ICMPv6 echo target to
          # verify connectivity from the client to the router.
          "01-lo" = {
            name = "lo";
            addresses = [
              { addressConfig.Address = "FD42::1/128"; }
            ];
          };
        };
      };

      # make the network-online target a requirement, we wait for it in our test script
      systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
    };

    # This is the client behind the router. We should be receving router
    # advertisements for both the ULA and the delegated prefix.
    # All we have to do is boot with the default (networkd) configuration.
    client = {
      virtualisation.vlans = [ 2 ];
      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
      networking = {
        useNetworkd = true;
        useDHCP = false;
      };

      # make the network-online target a requirement, we wait for it in our test script
      systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
    };
  };

  testScript = ''
    # First start the router and wait for it it reach a state where we are
    # certain networkd is up and it is able to send out RAs
    router.start()
    router.wait_for_unit("systemd-networkd.service")

    # After that we can boot the client and wait for the network online target.
    # Since we only care about IPv6 that should not involve waiting for legacy
    # IP leases.
    client.start()
    client.wait_for_unit("network-online.target")

    # the static address on the router should not be reachable
    client.wait_until_succeeds("ping -6 -c 1 FD42::1")

    # the global IP of the ISP router should still not be a reachable
    router.fail("ping -6 -c 1 2001:DB8::1")

    # Once we have internal connectivity boot up the ISP
    isp.start()

    # Since for the ISP "being online" should have no real meaning we just
    # wait for the target where all the units have been started.
    # It probably still takes a few more seconds for all the RA timers to be
    # fired etc..
    isp.wait_for_unit("multi-user.target")

    # wait until the uplink interface has a good status
    router.wait_for_unit("network-online.target")
    router.wait_until_succeeds("ping -6 -c1 2001:DB8::1")

    # shortly after that the client should have received it's global IPv6
    # address and thus be able to ping the ISP
    client.wait_until_succeeds("ping -6 -c1 2001:DB8::1")

    # verify that we got a globally scoped address in eth1 from the
    # documentation prefix
    ip_output = client.succeed("ip --json -6 address show dev eth1")

    import json

    ip_json = json.loads(ip_output)[0]
    assert any(
        addr["local"].upper().startswith("2001:DB8:")
        for addr in ip_json["addr_info"]
        if addr["scope"] == "global"
    )
  '';
})