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.

Changed files
+126
nixos
+44
nixos/modules/system/boot/networkd.nix
···
(assertValueOneOf "OnLink" boolValues)
];
+
sectionDHCPServerStaticLease = checkUnitConfig "DHCPServerStaticLease" [
+
(assertOnlyFields [
+
"MACAddress"
+
"Address"
+
])
+
(assertHasField "MACAddress")
+
(assertHasField "Address")
+
(assertMacAddress "MACAddress")
+
];
+
};
};
···
};
};
+
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
+
<literal>[DHCPServerStaticLease]</literal> section of the unit. See
+
<citerefentry><refentrytitle>systemd.network</refentrytitle>
+
<manvolnum>5</manvolnum></citerefentry> for details.
+
+
Make sure to configure the corresponding client interface to use
+
<literal>ClientIdentifier=mac</literal>.
+
'';
+
};
+
};
+
};
+
networkOptions = commonNetworkOptions // {
linkConfig = mkOption {
···
description = ''
Each attribute in this set specifies an option in the
<literal>[IPv6SendRA]</literal> section of the unit. See
+
<citerefentry><refentrytitle>systemd.network</refentrytitle>
+
<manvolnum>5</manvolnum></citerefentry> for details.
+
'';
+
};
+
+
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
<citerefentry><refentrytitle>systemd.network</refentrytitle>
<manvolnum>5</manvolnum></citerefentry> for details.
'';
···
+ flip concatMapStrings def.ipv6Prefixes (x: ''
[IPv6Prefix]
${attrsToSection x.ipv6PrefixConfig}
+
'')
+
+ flip concatMapStrings def.dhcpServerStaticLeases (x: ''
+
[DHCPServerStaticLease]
+
${attrsToSection x.dhcpServerStaticLeaseConfig}
'')
+ def.extraConfig;
};
+1
nixos/tests/all-tests.nix
···
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 {};
+81
nixos/tests/systemd-networkd-dhcpserver-static-leases.nix
···
+
# 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")
+
'';
+
})