diff --git a/man/networkd.conf.xml b/man/networkd.conf.xml
index bf597cac2ec..9fa925e85c8 100644
--- a/man/networkd.conf.xml
+++ b/man/networkd.conf.xml
@@ -138,6 +138,13 @@
+
+
+ If DUIDType=custom, then the DUIDRawData= value will
+ used be as custom identifier. If DUIDType=custom is specified then the
+ DUIDRawData= key is mandatory. Note it applies only on DHCPv6 clients.
+
+
If DUIDType=uuid, and DUIDRawData= is not set,
diff --git a/src/libsystemd-network/dhcp-identifier.c b/src/libsystemd-network/dhcp-identifier.c
index a27d67a315d..83fb6ce3408 100644
--- a/src/libsystemd-network/dhcp-identifier.c
+++ b/src/libsystemd-network/dhcp-identifier.c
@@ -15,10 +15,11 @@
#define USEC_2000 ((usec_t) 946684800000000) /* 2000-01-01 00:00:00 UTC */
static const char * const duid_type_table[_DUID_TYPE_MAX] = {
- [DUID_TYPE_LLT] = "DUID-LLT",
- [DUID_TYPE_EN] = "DUID-EN/Vendor",
- [DUID_TYPE_LL] = "DUID-LL",
- [DUID_TYPE_UUID] = "UUID",
+ [DUID_TYPE_LLT] = "DUID-LLT",
+ [DUID_TYPE_EN] = "DUID-EN/Vendor",
+ [DUID_TYPE_LL] = "DUID-LL",
+ [DUID_TYPE_UUID] = "UUID",
+ [DUID_TYPE_CUSTOM] = "Custom",
};
DEFINE_STRING_TABLE_LOOKUP_TO_STRING(duid_type, DUIDType);
diff --git a/src/libsystemd-network/dhcp-identifier.h b/src/libsystemd-network/dhcp-identifier.h
index 523dfc4a71b..1ac34761573 100644
--- a/src/libsystemd-network/dhcp-identifier.h
+++ b/src/libsystemd-network/dhcp-identifier.h
@@ -17,6 +17,7 @@ typedef enum DUIDType {
DUID_TYPE_EN = 2,
DUID_TYPE_LL = 3,
DUID_TYPE_UUID = 4,
+ DUID_TYPE_CUSTOM = 5,
_DUID_TYPE_MAX,
_DUID_TYPE_INVALID = -EINVAL,
} DUIDType;
diff --git a/src/libsystemd-network/sd-dhcp6-client.c b/src/libsystemd-network/sd-dhcp6-client.c
index 2f4053caad0..e8d99718b30 100644
--- a/src/libsystemd-network/sd-dhcp6-client.c
+++ b/src/libsystemd-network/sd-dhcp6-client.c
@@ -212,10 +212,14 @@ static int dhcp6_client_set_duid_internal(
log_dhcp6_client(client, "Using DUID of type %i of incorrect length, proceeding.", duid_type);
}
- client->duid.type = htobe16(duid_type);
- memcpy(&client->duid.raw.data, duid, duid_len);
- client->duid_len = sizeof(client->duid.type) + duid_len;
-
+ if (duid_type == DUID_TYPE_CUSTOM) {
+ memcpy(&client->duid, duid, duid_len);
+ client->duid_len = duid_len;
+ } else {
+ client->duid.type = htobe16(duid_type);
+ memcpy(&client->duid.raw.data, duid, duid_len);
+ client->duid_len = sizeof(client->duid.type) + duid_len;
+ }
} else {
r = dhcp_identifier_set_duid(duid_type, &client->hw_addr, client->arp_type, llt_time,
client->test_mode, &client->duid, &client->duid_len);
diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c
index 5f9ef80e3e5..baf80cb6442 100644
--- a/src/network/networkd-dhcp-common.c
+++ b/src/network/networkd-dhcp-common.c
@@ -1106,10 +1106,11 @@ static const char * const dhcp_option_data_type_table[_DHCP_OPTION_DATA_MAX] = {
DEFINE_STRING_TABLE_LOOKUP(dhcp_option_data_type, DHCPOptionDataType);
static const char* const duid_type_table[_DUID_TYPE_MAX] = {
- [DUID_TYPE_LLT] = "link-layer-time",
- [DUID_TYPE_EN] = "vendor",
- [DUID_TYPE_LL] = "link-layer",
- [DUID_TYPE_UUID] = "uuid",
+ [DUID_TYPE_LLT] = "link-layer-time",
+ [DUID_TYPE_EN] = "vendor",
+ [DUID_TYPE_LL] = "link-layer",
+ [DUID_TYPE_UUID] = "uuid",
+ [DUID_TYPE_CUSTOM] = "custom",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(duid_type, DUIDType);
diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c
index 00c767e1fb8..e0f08508988 100644
--- a/src/network/networkd-dhcp6.c
+++ b/src/network/networkd-dhcp6.c
@@ -555,6 +555,11 @@ static int dhcp6_set_identifier(Link *link, sd_dhcp6_client *client) {
}
duid = link_get_dhcp6_duid(link);
+
+ if (duid->type == DUID_TYPE_CUSTOM && duid->raw_data_len == 0)
+ return log_link_debug_errno(link, SYNTHETIC_ERRNO(EINVAL),
+ "DHCPv6 CLIENT: Missing DUID Raw Data as duid type set to 'custom': %m");
+
if (duid->type == DUID_TYPE_LLT && duid->raw_data_len == 0)
r = sd_dhcp6_client_set_duid_llt(client, duid->llt_time);
else
diff --git a/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network b/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network
new file mode 100644
index 00000000000..edd5b7f5017
--- /dev/null
+++ b/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=veth99
+
+[Network]
+DHCP=ipv6
+IPv6Token=::1a:2b:3c:4d
+
+[Route]
+Gateway=_ipv6ra
+Destination=2001:1234:5:9fff:ff:ff:ff:ff/128
+
+[DHCPv6]
+DUIDType=custom
+DUIDRawData=00:00:ab:11:f9:2a:c2:77:29:f9:5c:00
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index 07afe354f79..799d3bf784d 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -5018,6 +5018,41 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities):
self.assertIn('DHCPREPLY(veth-peer)', output)
self.assertNotIn('rapid-commit', output)
+ def test_dhcp_client_ipv6_only_with_custom_client_identifier(self):
+ copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client-ipv6-only-custom-client-identifier.network')
+
+ start_networkd()
+ self.wait_online(['veth-peer:carrier'])
+ start_dnsmasq()
+ self.wait_online(['veth99:routable', 'veth-peer:routable'])
+
+ # checking address
+ output = check_output('ip address show dev veth99 scope global')
+ print(output)
+ self.assertRegex(output, r'inet6 2600::[0-9a-f:]*/128 scope global dynamic noprefixroute')
+ self.assertNotIn('192.168.5', output)
+
+ # checking semi-static route
+ output = check_output('ip -6 route list dev veth99 2001:1234:5:9fff:ff:ff:ff:ff')
+ print(output)
+ self.assertRegex(output, 'via fe80::1034:56ff:fe78:9abd')
+
+ # Confirm that ipv6 token is not set in the kernel
+ output = check_output('ip token show dev veth99')
+ print(output)
+ self.assertRegex(output, 'token :: dev veth99')
+
+ print('## dnsmasq log')
+ output = read_dnsmasq_log_file()
+ print(output)
+ self.assertIn('DHCPSOLICIT(veth-peer) 00:00:ab:11:f9:2a:c2:77:29:f9:5c:00', output)
+ self.assertNotIn('DHCPADVERTISE(veth-peer)', output)
+ self.assertNotIn('DHCPREQUEST(veth-peer)', output)
+ self.assertIn('DHCPREPLY(veth-peer)', output)
+ self.assertIn('sent size: 0 option: 14 rapid-commit', output)
+
+ stop_dnsmasq()
+
def test_dhcp_client_ipv4_only(self):
copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client-ipv4-only.network')