network: introduce ClientIdentifier= setting in [DHCPServerStaticLease] section

Previously, a static lease could be configured only with MACAddress=, which matches a client by its
hardware address. Hosts that share a hardware address, e.g. ipvlan interfaces in L2 mode, but send
different client identifiers could not get different static addresses.

This introduces ClientIdentifier=, IAID=, DUIDType=, and DUIDRawData= settings, with the same
semantics as the ones in the [DHCPv4] section. With ClientIdentifier=mac, the default, the client
identifier is built from MACAddress= as before. With ClientIdentifier=duid, MACAddress= is ignored,
and the client identifier is built from IAID= and the DUID specified by DUIDType= and DUIDRawData=,
all of which must be specified.
This commit is contained in:
khromenokroman committed 2026-10-24 22:21:50 +02:00
1 parent 2f997b997a
commit 18f3b466c6
7 files changed
+303 -13

No files matched your search

+7
View File
@@ -153,6 +153,13 @@ CHANGES WITH 263 in spe:
The reason is also exposed in the new ConfiguringReasons field of
the interface description provided over Varlink.
* The [DHCPServerStaticLease] section gained new ClientIdentifier=,
IAID=, DUIDType=, and DUIDRawData= settings. With
ClientIdentifier=duid, a static address is assigned to a client
based on the IAID and DUID it sends, instead of its hardware
address. This is useful e.g. to tell apart hosts that share a
hardware address, like ipvlan interfaces in L2 mode.
Changes in systemd-sysupdate:
* systemd-sysupdate's "update" command acquired a new --loop= switch.
+46 -5
View File
@@ -4371,23 +4371,64 @@ ServerAddress=192.168.0.1/24</programlisting>
<refsect1>
<title>[DHCPServerStaticLease] Section Options</title>
<para>The <literal>[DHCPServerStaticLease]</literal> section configures a static DHCP lease to assign a
fixed IPv4 address to a specific device based on its MAC address. This section can be specified multiple
times.</para>
fixed IPv4 address to a specific device based on its MAC address or client identifier. This section can be
specified multiple times.</para>
<variablelist class='network-directives'>
<varlistentry>
<term><varname>MACAddress=</varname></term>
<listitem><para>The hardware address of a device to match. This key is mandatory.</para>
<listitem><para>The hardware address of a device to match. This key is mandatory when
<varname>ClientIdentifier=</varname> is <option>mac</option>, and ignored when it is
<option>duid</option>.</para>
<xi:include href="version-info.xml" xpointer="v249"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>ClientIdentifier=</varname></term>
<listitem><para>Specifies how the client identifier (option 61) of a device to match is built, as in
the [DHCPv4] section. Takes one of <option>mac</option> or <option>duid</option>. When
<option>mac</option>, the client identifier is built from <varname>MACAddress=</varname>. As the
server falls back to the hardware address of a device when no static lease matches the client
identifier the device sends, such a lease matches the device whatever client identifier it sends.
When <option>duid</option>, the client identifier is built from <varname>IAID=</varname> and the
DUID specified by <varname>DUIDType=</varname> and <varname>DUIDRawData=</varname> (RFC 4361), and
all of them must be specified. Such a lease matches only a device that sends exactly that client
identifier. This is useful e.g. to tell apart hosts that share a hardware address, like ipvlan
interfaces in L2 mode. Defaults to <option>mac</option>.</para>
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>IAID=</varname></term>
<listitem><para>The IAID of a device to match, a 32-bit unsigned integer, as in the [DHCPv4] section of
the client. Used only when <varname>ClientIdentifier=duid</varname>.</para>
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>DUIDType=</varname></term>
<term><varname>DUIDRawData=</varname></term>
<listitem><para>The DUID of a device to match, as in the [DHCPv4] section of the client. See
<citerefentry><refentrytitle>networkd.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for the format. As the DUID cannot be generated on the server side, both must be specified when
<varname>ClientIdentifier=duid</varname>, and they are ignored otherwise.</para>
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Address=</varname></term>
<listitem><para>The IPv4 address that should be assigned to the device that was matched with
<varname>MACAddress=</varname>. This key is mandatory.</para>
<varname>MACAddress=</varname> or <varname>ClientIdentifier=</varname>. This key is
mandatory.</para>
<xi:include href="version-info.xml" xpointer="v249"/></listitem>
</varlistentry>
@@ -4400,7 +4441,7 @@ ServerAddress=192.168.0.1/24</programlisting>
<literal>mydevice.example.com</literal>), following RFC 1123 naming conventions. Each label can be
up to 63 characters, with a total maximum length of 253 characters for FQDNs. When this option is
set, the DHCP server will include the hostname in DHCP replies (both OFFER and ACK) to the client
with the matched MAC address.</para>
with the matched MAC address or client identifier.</para>
<para>The server automatically selects the appropriate DHCP option based on the hostname format:
simple hostnames (single DNS label) are sent via option 12 (Host Name) per RFC 2132, while FQDNs
+111 -8
View File
@@ -7,6 +7,7 @@
#include "hashmap.h"
#include "networkd-dhcp-server-static-lease.h"
#include "networkd-network.h"
#include "parse-util.h"
#include "string-util.h"
static DHCPStaticLease* dhcp_static_lease_free(DHCPStaticLease *static_lease) {
@@ -33,10 +34,15 @@ static int dhcp_static_lease_new(DHCPStaticLease **ret) {
assert(ret);
p = new0(DHCPStaticLease, 1);
p = new(DHCPStaticLease, 1);
if (!p)
return -ENOMEM;
*p = (DHCPStaticLease) {
.client_identifier = DHCP_CLIENT_ID_MAC,
.duid.type = _DUID_TYPE_INVALID,
};
*ret = TAKE_PTR(p);
return 0;
}
@@ -90,14 +96,42 @@ static int static_lease_verify(DHCPStaticLease *static_lease) {
/* TODO: check that the address is in the pool. */
if (ether_addr_is_null(&static_lease->hw_addr))
return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
"%s: DHCP static lease without MACAddress= field configured. "
"Ignoring [DHCPServerStaticLease] section from line %u.",
static_lease->section->filename, static_lease->section->line);
switch (static_lease->client_identifier) {
case DHCP_CLIENT_ID_MAC:
if (ether_addr_is_null(&static_lease->hw_addr))
return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
"%s: DHCP static lease without MACAddress= field configured. "
"Ignoring [DHCPServerStaticLease] section from line %u.",
static_lease->section->filename, static_lease->section->line);
/* set client id type to 1: Ethernet Link-Layer (RFC 2132) */
r = sd_dhcp_client_id_set(&static_lease->client_id, 1, &static_lease->hw_addr, ETH_ALEN);
/* set client id type to 1: Ethernet Link-Layer (RFC 2132) */
r = sd_dhcp_client_id_set(&static_lease->client_id, 1, &static_lease->hw_addr, ETH_ALEN);
break;
case DHCP_CLIENT_ID_DUID: {
sd_dhcp_duid duid = {};
/* MACAddress= is ignored here, as the client identifier does not contain it. */
if (!static_lease->iaid_set ||
static_lease->duid.type < 0 ||
static_lease->duid.raw_data_len == 0)
return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
"%s: DHCP static lease with ClientIdentifier=duid requires IAID=, "
"DUIDType=, and DUIDRawData= fields configured. "
"Ignoring [DHCPServerStaticLease] section from line %u.",
static_lease->section->filename, static_lease->section->line);
r = sd_dhcp_duid_set(&duid, static_lease->duid.type,
static_lease->duid.raw_data, static_lease->duid.raw_data_len);
if (r >= 0)
r = sd_dhcp_client_id_set_iaid_duid(&static_lease->client_id, static_lease->iaid, &duid);
break;
}
default:
assert_not_reached();
}
if (r < 0)
return log_warning_errno(r,
"%s: Failed to set client ID of DHCP static lease. "
@@ -215,6 +249,75 @@ int config_parse_dhcp_static_lease_hwaddr(
return 0;
}
static int config_parse_dhcp_static_lease_iaid(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
DHCPStaticLease *lease = ASSERT_PTR(userdata);
int r;
assert(rvalue);
if (isempty(rvalue)) {
lease->iaid = 0;
lease->iaid_set = false;
return 1;
}
r = safe_atou32(rvalue, &lease->iaid);
if (r < 0)
return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue);
lease->iaid_set = true;
return 1;
}
int config_parse_dhcp_static_lease_section(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
static const ConfigSectionParser table[_DHCP_STATIC_LEASE_CONF_PARSER_MAX] = {
[DHCP_STATIC_LEASE_CLIENT_IDENTIFIER] = { .parser = config_parse_dhcp_client_identifier, .ltype = 0, .offset = offsetof(DHCPStaticLease, client_identifier), },
[DHCP_STATIC_LEASE_IAID] = { .parser = config_parse_dhcp_static_lease_iaid, .ltype = 0, .offset = 0, },
[DHCP_STATIC_LEASE_DUID_TYPE] = { .parser = config_parse_duid_type, .ltype = true, .offset = offsetof(DHCPStaticLease, duid), },
[DHCP_STATIC_LEASE_DUID_RAW_DATA] = { .parser = config_parse_duid_rawdata, .ltype = true, .offset = offsetof(DHCPStaticLease, duid), },
};
_cleanup_(dhcp_static_lease_free_or_set_invalidp) DHCPStaticLease *lease = NULL;
Network *network = ASSERT_PTR(userdata);
int r;
assert(filename);
r = lease_new_static(network, filename, section_line, &lease);
if (r < 0)
return log_oom();
r = config_section_parse(table, ELEMENTSOF(table),
unit, filename, line, section, section_line, lvalue, ltype, rvalue, lease);
if (r <= 0)
return r;
TAKE_PTR(lease);
return 0;
}
int config_parse_dhcp_static_lease_hostname(
const char *unit,
const char *filename,
@@ -6,6 +6,8 @@
#include <netinet/in.h>
#include "dhcp-client-id-internal.h"
#include "networkd-dhcp-common.h"
#include "networkd-dhcp4.h"
#include "networkd-forward.h"
typedef struct DHCPStaticLease {
@@ -14,12 +16,26 @@ typedef struct DHCPStaticLease {
struct in_addr address;
struct ether_addr hw_addr;
DHCPClientIdentifier client_identifier;
uint32_t iaid;
bool iaid_set;
DUID duid;
sd_dhcp_client_id client_id;
char *hostname;
} DHCPStaticLease;
typedef enum DHCPStaticLeaseConfParser {
DHCP_STATIC_LEASE_CLIENT_IDENTIFIER,
DHCP_STATIC_LEASE_IAID,
DHCP_STATIC_LEASE_DUID_TYPE,
DHCP_STATIC_LEASE_DUID_RAW_DATA,
_DHCP_STATIC_LEASE_CONF_PARSER_MAX,
_DHCP_STATIC_LEASE_CONF_PARSER_INVALID = -EINVAL,
} DHCPStaticLeaseConfParser;
void network_drop_invalid_static_leases(Network *network);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_address);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_hwaddr);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_section);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_hostname);
+4
View File
@@ -410,6 +410,10 @@ DHCPServer.PersistLeases, config_parse_dhcp_server_persis
DHCPServer.LocalLeaseDomain, config_parse_dns_name, 0, offsetof(Network, dhcp_server_local_lease_domain)
DHCPServerStaticLease.Address, config_parse_dhcp_static_lease_address, 0, 0
DHCPServerStaticLease.MACAddress, config_parse_dhcp_static_lease_hwaddr, 0, 0
DHCPServerStaticLease.ClientIdentifier, config_parse_dhcp_static_lease_section, DHCP_STATIC_LEASE_CLIENT_IDENTIFIER, 0
DHCPServerStaticLease.IAID, config_parse_dhcp_static_lease_section, DHCP_STATIC_LEASE_IAID, 0
DHCPServerStaticLease.DUIDType, config_parse_dhcp_static_lease_section, DHCP_STATIC_LEASE_DUID_TYPE, 0
DHCPServerStaticLease.DUIDRawData, config_parse_dhcp_static_lease_section, DHCP_STATIC_LEASE_DUID_RAW_DATA, 0
DHCPServerStaticLease.Hostname, config_parse_dhcp_static_lease_hostname, 0, 0
Bridge.Cost, config_parse_uint32, 0, offsetof(Network, cost)
Bridge.UseBPDU, config_parse_tristate, 0, offsetof(Network, use_bpdu)
+112
View File
@@ -4,6 +4,7 @@
#include "hexdecoct.h"
#include "net-condition.h"
#include "networkd-address.h"
#include "networkd-dhcp-server-static-lease.h"
#include "networkd-manager.h"
#include "networkd-network.h"
#include "networkd-route.h"
@@ -238,6 +239,117 @@ TEST(config_parse_address) {
test_config_parse_address_one("::1/-1", AF_INET6, 0, NULL, 0);
}
static void test_config_parse_dhcp_static_lease_one(
const char *hwaddr,
const char *client_identifier,
const char *iaid,
const char *duid_type,
const char *duid_rawdata,
const void *expected,
size_t expected_size) {
static const struct {
const char *lvalue;
DHCPStaticLeaseConfParser ltype;
} settings[] = {
{ "ClientIdentifier", DHCP_STATIC_LEASE_CLIENT_IDENTIFIER },
{ "IAID", DHCP_STATIC_LEASE_IAID },
{ "DUIDType", DHCP_STATIC_LEASE_DUID_TYPE },
{ "DUIDRawData", DHCP_STATIC_LEASE_DUID_RAW_DATA },
};
const char *values[] = { client_identifier, iaid, duid_type, duid_rawdata };
_cleanup_(manager_freep) Manager *manager = NULL;
_cleanup_(network_unrefp) Network *network = NULL;
DHCPStaticLease *lease;
assert_cc(ELEMENTSOF(settings) == ELEMENTSOF(values));
log_debug("/* %s(MACAddress=%s, ClientIdentifier=%s, IAID=%s, DUIDType=%s, DUIDRawData=%s) */",
__func__, strnull(hwaddr), strnull(client_identifier), strnull(iaid),
strnull(duid_type), strnull(duid_rawdata));
ASSERT_OK(manager_new(&manager, /* test_mode= */ true));
ASSERT_NOT_NULL(network = new0(Network, 1));
network->n_ref = 1;
network->manager = manager;
ASSERT_NOT_NULL(network->filename = strdup("hogehoge.network"));
ASSERT_OK_ZERO(config_parse_match_ifnames("network", "filename", 1, "section", 1, "Name", 0, "*",
&network->match.ifname, network));
ASSERT_OK_ZERO(config_parse_dhcp_static_lease_address(
"network", "filename", 1, "section", 1,
"Address", 0, "10.1.1.2", NULL, network));
if (hwaddr)
ASSERT_OK_ZERO(config_parse_dhcp_static_lease_hwaddr(
"network", "filename", 1, "section", 1,
"MACAddress", 0, hwaddr, NULL, network));
FOREACH_ELEMENT(setting, settings) {
const char *v = values[setting - settings];
if (v)
ASSERT_OK_ZERO(config_parse_dhcp_static_lease_section(
"network", "filename", 1, "section", 1,
setting->lvalue, setting->ltype, v, NULL, network));
}
ASSERT_OK(network_verify(network));
if (!expected) {
ASSERT_TRUE(hashmap_isempty(network->dhcp_static_leases_by_section));
return;
}
ASSERT_EQ(hashmap_size(network->dhcp_static_leases_by_section), 1u);
ASSERT_NOT_NULL(lease = hashmap_first(network->dhcp_static_leases_by_section));
ASSERT_EQ(lease->client_id.size, expected_size);
ASSERT_EQ(memcmp(lease->client_id.raw, expected, expected_size), 0);
}
TEST(config_parse_dhcp_static_lease) {
static const uint8_t by_hwaddr[] = { 0x01, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc };
static const uint8_t by_iaid_duid[] = {
0xff, /* type: IAID and DUID */
0x12, 0x34, 0x56, 0x78, /* IAID */
0x00, 0x02, /* DUID type: vendor */
0x00, 0x00, 0xab, 0x11, /* DUID data */
};
static const uint8_t by_iaid_duid_numeric_type[] = {
0xff, /* type: IAID and DUID */
0x00, 0x00, 0x00, 0x01, /* IAID */
0x12, 0x34, /* DUID type: 0x1234 */
0xab, /* DUID data */
};
/* ClientIdentifier=mac, the default */
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", NULL, NULL, NULL, NULL,
by_hwaddr, sizeof(by_hwaddr));
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "mac", NULL, NULL, NULL,
by_hwaddr, sizeof(by_hwaddr));
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "mac", "0x12345678", "vendor", "00:00:ab:11",
by_hwaddr, sizeof(by_hwaddr));
test_config_parse_dhcp_static_lease_one(NULL, NULL, NULL, NULL, NULL, NULL, 0);
test_config_parse_dhcp_static_lease_one(NULL, "mac", "0x12345678", "vendor", "00:00:ab:11", NULL, 0);
/* ClientIdentifier=duid, MACAddress= is ignored */
test_config_parse_dhcp_static_lease_one(NULL, "duid", "0x12345678", "vendor", "00:00:ab:11",
by_iaid_duid, sizeof(by_iaid_duid));
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", "0x12345678", "vendor", "00:00:AB:11",
by_iaid_duid, sizeof(by_iaid_duid));
test_config_parse_dhcp_static_lease_one(NULL, "duid", "1", "4660", "ab",
by_iaid_duid_numeric_type, sizeof(by_iaid_duid_numeric_type));
/* ClientIdentifier=duid without IAID=, DUIDType=, or DUIDRawData= */
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", NULL, "vendor", "00:00:ab:11", NULL, 0);
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", "0x12345678", NULL, "00:00:ab:11", NULL, 0);
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", "0x12345678", "vendor", NULL, NULL, 0);
/* Invalid values make the section dropped */
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", "-1", "vendor", "00:00:ab:11", NULL, 0);
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", "0x12345678", "hoge", "00:00:ab:11", NULL, 0);
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "duid", "0x12345678", "vendor", "00:xy", NULL, 0);
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "hoge", "0x12345678", "vendor", "00:00:ab:11", NULL, 0);
test_config_parse_dhcp_static_lease_one("12:34:56:78:9a:bc", "mac", NULL, "hoge", NULL, NULL, 0);
}
TEST(config_parse_match_ifnames) {
_cleanup_strv_free_ char **names = NULL;
@@ -29,3 +29,10 @@ Address=10.1.1.4
[DHCPServerStaticLease]
MACAddress=12:34:56:78:9a:bf
Address=10.1.1.5
[DHCPServerStaticLease]
ClientIdentifier=duid
IAID=0x12345678
DUIDType=vendor
DUIDRawData=00:00:ab:11
Address=10.1.1.6