From ce81231420ec3499ac4e8f31076cc9425dbf90f3 Mon Sep 17 00:00:00 2001 From: Vincent Haupert Date: Tue, 30 Nov 2021 09:57:15 +0100 Subject: [PATCH] nixos/networkd: add `dhcpServerStaticLeaseConfig` option Add `systemd.network.networks.*.dhcpServerStaticLeaseConfig` to allow for configuring static DHCP leases through the `[DHCPServerStaticLease]` section. See systemd.network(5) of systemd 249 for details. Also adds the NixOS test `systemd-networkd-dhcpserver-static-lease` to test the assignment of static leases. --- nixos/modules/system/boot/networkd.nix | 44 ++++++++++ nixos/tests/all-tests.nix | 1 + ...temd-networkd-dhcpserver-static-leases.nix | 81 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 nixos/tests/systemd-networkd-dhcpserver-static-leases.nix diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index 2e17bdf6bb65..c24a2ff527a5 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -821,6 +821,16 @@ let (assertValueOneOf "OnLink" boolValues) ]; + sectionDHCPServerStaticLease = checkUnitConfig "DHCPServerStaticLease" [ + (assertOnlyFields [ + "MACAddress" + "Address" + ]) + (assertHasField "MACAddress") + (assertHasField "Address") + (assertMacAddress "MACAddress") + ]; + }; }; @@ -1161,6 +1171,25 @@ let }; }; + dhcpServerStaticLeaseOptions = { + options = { + dhcpServerStaticLeaseConfig = mkOption { + default = {}; + example = { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPServerStaticLease; + description = '' + Each attribute in this set specifies an option in the + [DHCPServerStaticLease] section of the unit. See + systemd.network + 5 for details. + + Make sure to configure the corresponding client interface to use + ClientIdentifier=mac. + ''; + }; + }; + }; + networkOptions = commonNetworkOptions // { linkConfig = mkOption { @@ -1273,6 +1302,17 @@ let ''; }; + dhcpServerStaticLeases = mkOption { + default = []; + example = [ { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; } ]; + type = with types; listOf (submodule dhcpServerStaticLeaseOptions); + description = '' + A list of DHCPServerStaticLease sections to be added to the unit. See + systemd.network + 5 for details. + ''; + }; + ipv6Prefixes = mkOption { default = []; example = [ { AddressAutoconfiguration = true; OnLink = true; } ]; @@ -1644,6 +1684,10 @@ let [IPv6Prefix] ${attrsToSection x.ipv6PrefixConfig} '') + + flip concatMapStrings def.dhcpServerStaticLeases (x: '' + [DHCPServerStaticLease] + ${attrsToSection x.dhcpServerStaticLeaseConfig} + '') + def.extraConfig; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e2b9c868bc85..b223c15c94f1 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -449,6 +449,7 @@ in systemd-journal = handleTest ./systemd-journal.nix {}; systemd-networkd = handleTest ./systemd-networkd.nix {}; systemd-networkd-dhcpserver = handleTest ./systemd-networkd-dhcpserver.nix {}; + systemd-networkd-dhcpserver-static-leases = handleTest ./systemd-networkd-dhcpserver-static-leases.nix {}; systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {}; systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {}; systemd-nspawn = handleTest ./systemd-nspawn.nix {}; diff --git a/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix b/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix new file mode 100644 index 000000000000..a8254a158016 --- /dev/null +++ b/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix @@ -0,0 +1,81 @@ +# In contrast to systemd-networkd-dhcpserver, this test configures +# the router with a static DHCP lease for the client's MAC address. +import ./make-test-python.nix ({ lib, ... }: { + name = "systemd-networkd-dhcpserver-static-leases"; + meta = with lib.maintainers; { + maintainers = [ veehaitch tomfitzhenry ]; + }; + nodes = { + router = { + virtualisation.vlans = [ 1 ]; + systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug"; + networking = { + useNetworkd = true; + useDHCP = false; + 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. + "01-eth1" = { + name = "eth1"; + networkConfig = { + DHCPServer = true; + Address = "10.0.0.1/24"; + }; + dhcpServerStaticLeases = [{ + dhcpServerStaticLeaseConfig = { + MACAddress = "02:de:ad:be:ef:01"; + Address = "10.0.0.10"; + }; + }]; + }; + }; + }; + }; + + client = { + virtualisation.vlans = [ 1 ]; + systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug"; + networking = { + useNetworkd = true; + useDHCP = false; + firewall.enable = false; + interfaces.eth1 = { + useDHCP = true; + macAddress = "02:de:ad:be:ef:01"; + }; + }; + + # This setting is important to have the router assign the + # configured lease based on the client's MAC address. Also see: + # https://github.com/systemd/systemd/issues/21368#issuecomment-982193546 + systemd.network.networks."40-eth1".dhcpV4Config.ClientIdentifier = "mac"; + }; + }; + testScript = '' + start_all() + + with subtest("check router network configuration"): + router.wait_for_unit("systemd-networkd-wait-online.service") + eth1_status = router.succeed("networkctl status eth1") + assert "Network File: /etc/systemd/network/01-eth1.network" in eth1_status, \ + "The router interface eth1 is not using the expected network file" + assert "10.0.0.1" in eth1_status, "Did not find expected router IPv4" + + with subtest("check client network configuration"): + client.wait_for_unit("systemd-networkd-wait-online.service") + eth1_status = client.succeed("networkctl status eth1") + assert "Network File: /etc/systemd/network/40-eth1.network" in eth1_status, \ + "The client interface eth1 is not using the expected network file" + assert "10.0.0.10" in eth1_status, "Did not find expected client IPv4" + + with subtest("router and client can reach each other"): + client.wait_until_succeeds("ping -c 5 10.0.0.1") + router.wait_until_succeeds("ping -c 5 10.0.0.10") + ''; +})