]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
base-files: derive DHCP DUID from device MAC address
authorFelix Fietkau <nbd@nbd.name>
Fri, 19 Jun 2026 14:32:19 +0000 (16:32 +0200)
committerFelix Fietkau <nbd@nbd.name>
Wed, 24 Jun 2026 09:21:28 +0000 (11:21 +0200)
The DUID-UUID was generated from a random UUID, which is regenerated on
every fresh flash and therefore does not survive reconfiguration events
such as reflashing without keeping the configuration. RFC8415/RFC6355
prefer a DUID that remains stable across such events.

Add a ucode helper that picks the first LAN port (falling back to WAN)
from /etc/board.json, resolves its MAC address and derives a stable UUID
from it, falling back to a random UUID only when that fails.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
package/base-files/Makefile
package/base-files/files/etc/uci-defaults/14_network-generate-duid
package/base-files/files/usr/libexec/duid-generate [new file with mode: 0755]

index 2f84179cb3049fb59b6f03975b7de8252cf92b9f..d19f6fc71244b140b2a555223eb3296e7f3dc4a8 100644 (file)
@@ -43,7 +43,8 @@ define Package/base-files
        +netifd +libc +jsonfilter +SIGNED_PACKAGES:usign +SIGNED_PACKAGES:openwrt-keyring \
        +NAND_SUPPORT:ubi-utils +fstools +fwtool \
        +SELINUX:procd-selinux +!SELINUX:procd +USE_SECCOMP:procd-seccomp \
-       +SELINUX:busybox-selinux +!SELINUX:busybox
+       +SELINUX:busybox-selinux +!SELINUX:busybox \
+       +ucode +ucode-mod-fs +ucode-mod-digest
   TITLE:=Base filesystem for OpenWrt
   URL:=https://openwrt.org/
   VERSION:=$(PKG_RELEASE)~$(lastword $(subst -, ,$(REVISION)))
index c793dba69593a1dc3b008422a4ed701e314d77fd..7742897617513e0a3d4e07ee9acba8432bed0fcc 100644 (file)
@@ -1,8 +1,10 @@
 [ "$(uci -q get network.globals.dhcp_default_duid || echo "auto")" != "auto" ] && exit 0
 
+duid="$(/usr/libexec/duid-generate)"
+[ -n "$duid" ] || exit 1
+
 uci -q batch <<-EOF >/dev/null
-       # DUID-UUID - RFC6355
-       set network.globals.dhcp_default_duid="$(printf '%s%s' '0004' $(cat /proc/sys/kernel/random/uuid | sed -e 's/-//g'))"
+       set network.globals.dhcp_default_duid="$duid"
        commit network
 EOF
 
diff --git a/package/base-files/files/usr/libexec/duid-generate b/package/base-files/files/usr/libexec/duid-generate
new file mode 100755 (executable)
index 0000000..2318239
--- /dev/null
@@ -0,0 +1,120 @@
+#!/usr/bin/env ucode
+'use strict';
+
+import { readfile } from 'fs';
+import * as digest from 'digest';
+
+// Fixed salt prefixed to the MAC address before hashing. It only separates this
+// particular use of the MAC from any other; the value itself carries no meaning.
+const DUID_SALT = 'openwrt-dhcp-duid:';
+const DUID_TYPE_UUID = '0004';
+
+function mac_normalise(mac) {
+       if (type(mac) != 'string')
+               return null;
+
+       mac = lc(trim(mac));
+
+       if (!match(mac, /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/))
+               return null;
+
+       if (mac == '00:00:00:00:00:00')
+               return null;
+
+       return mac;
+}
+
+function sysfs_mac(dev) {
+       if (!dev)
+               return null;
+
+       return mac_normalise(readfile('/sys/class/net/' + dev + '/address'));
+}
+
+function role_mac(board, role) {
+       if (type(role) != 'object')
+               return null;
+
+       let dev;
+       if (type(role.ports) == 'array' && length(role.ports) > 0)
+               dev = role.ports[0];
+       else
+               dev = role.device;
+
+       // Prefer the live hardware address of the physical port.
+       let mac = sysfs_mac(dev);
+       if (mac)
+               return mac;
+
+       // Fall back to MAC addresses recorded in board.json.
+       mac = mac_normalise(board?.network_device?.[dev]?.macaddr);
+       if (mac)
+               return mac;
+
+       return mac_normalise(role.macaddr);
+}
+
+function board_mac(board) {
+       let net = board?.network;
+       if (type(net) != 'object')
+               return null;
+
+       for (let prefix in ['lan', 'wan']) {
+               let mac = role_mac(board, net[prefix]);
+               if (mac)
+                       return mac;
+
+               for (let name in keys(net)) {
+                       if (substr(name, 0, length(prefix)) != prefix)
+                               continue;
+
+                       mac = role_mac(board, net[name]);
+                       if (mac)
+                               return mac;
+               }
+       }
+
+       return null;
+}
+
+function uuid_set_bits(u, version) {
+       let b6 = (hex(substr(u, 12, 2)) & 0x0f) | (version << 4);
+       let b8 = (hex(substr(u, 16, 2)) & 0x3f) | 0x80;
+
+       return substr(u, 0, 12) + sprintf('%02x', b6) +
+              substr(u, 14, 2) + sprintf('%02x', b8) +
+              substr(u, 18, 14);
+}
+
+function uuid_from_mac(mac) {
+       let hash = digest.sha1(DUID_SALT + mac);
+       if (type(hash) != 'string' || length(hash) < 32)
+               return null;
+
+       return uuid_set_bits(substr(hash, 0, 32), 5);
+}
+
+function uuid_random() {
+       let uuid = readfile('/proc/sys/kernel/random/uuid');
+       if (!uuid)
+               return null;
+
+       return join('', split(trim(uuid), '-'));
+}
+
+let board;
+try {
+       board = json(readfile('/etc/board.json'));
+} catch (e) {
+       board = null;
+}
+
+let mac = board_mac(board);
+let uuid = mac ? uuid_from_mac(mac) : null;
+
+uuid ??= uuid_random();
+
+if (!uuid)
+       exit(1);
+
+print(DUID_TYPE_UUID + uuid + '\n');