nixpkgs-suyu/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
Andreas Rammhold 47529bcf7f
nixos/tests: add systemd-networkd-ipv6-prefix-delegation
This is a follow-up to the PR #82026 that contains the promised tests.

In this test I am testing if we can properly propagate prefixes received
via DHCPv6 PD with the networkd options in our module system.

The comments in the test should be sufficient to follow the idea and
what is going on.
2020-05-01 16:55:53 +02:00

295 lines
12 KiB
Nix

# 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.stdenv.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::"; prefixLength = 64; }
];
};
# Since we want to program the routes that we delegate to the "customer"
# into our routing table we must have a way to gain the required privs.
# This security wrapper will do in our test setup.
#
# DO NOT COPY THIS TO PRODUCTION AS IS. Think about it at least twice.
# Everyone on the "isp" machine will be able to add routes to the kernel.
security.wrappers.add-dhcpd-lease = {
source = pkgs.writeShellScript "add-dhcpd-lease" ''
exec ${pkgs.iproute}/bin/ip -6 route replace "$1" via "$2"
'';
capabilities = "cap_net_admin+ep";
};
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("/run/wrappers/bin/add-dhcpd-lease", concat(Prefix,"/",PrefixLength), 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";
};
ipv6PrefixDelegationConfig = {
# 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;
# Just delegate prefixes from the DHCPv6 PD pool.
# If you also want to distribute a local ULA prefix you want to
# set this to `yes` as that includes both static prefixes as well
# as PD prefixes.
IPv6PrefixDelegation = "dhcpv6";
};
# finally "act as router" (according to systemd.network(5))
ipv6PrefixDelegationConfig = {
RouterLifetimeSec = 300; # required as otherwise no RA's are being emitted
# In a production environment you should consider setting these as well:
#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::")
# 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::")
# 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::")
# 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"
)
'';
})