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