summary refs log blame commit diff
path: root/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
blob: 37a89fc21e4426c4d0901da822832d2a3ef7ab7d (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11










                                                                               
                                     




















                                                                                
                                                         



                                                                              



                                                                     






























                                                                                                                                       
                                                                                                                       





















































































                                                                                                           
                                












                                                                            
                                                                                    

                                   


                                                          
              
 

                                                                                    


                                                                                                   
                






























































                                                                                       
                                           











                                                                            
                                                         


                                                                         
                                                         














                                                                    
# 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"
    )
  '';
})