diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index 83654b88dbdc..1145831ee2ea 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -823,6 +823,16 @@ let (assertValueOneOf "OnLink" boolValues) ]; + sectionDHCPServerStaticLease = checkUnitConfig "DHCPServerStaticLease" [ + (assertOnlyFields [ + "MACAddress" + "Address" + ]) + (assertHasField "MACAddress") + (assertHasField "Address") + (assertMacAddress "MACAddress") + ]; + }; }; @@ -1163,6 +1173,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 { @@ -1275,6 +1304,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; } ]; @@ -1646,6 +1686,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 a1113ff631e3..06305460c6ac 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -454,6 +454,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") + ''; +})