odhcpd: support multiple per-client DUIDs
authorDavid Härdeman <[email protected]>
Fri, 1 Aug 2025 17:39:17 +0000 (19:39 +0200)
committerÁlvaro Fernández Rojas <[email protected]>
Thu, 9 Oct 2025 06:58:44 +0000 (08:58 +0200)
This patch allows for multiple per-client DHCPv6 DUIDs. One use-case for
multiple DUIDs is dual-boot hosts, where e.g. Windows creates it's own DUID and
Linux (say, systemd-networkd) creates it's own DUID, and the two cannot be
synced in a simple manner. This also makes DUIDs similar to multiple per-host
MAC addresses (which is already supported by dnsmasq for DHCPv4 leases).

Note that the "old" behaviour is still kept, in other words, where a
simple DUID (as opposed to a "DUID%IAID") static association is defined,
the address will be given out on a first-come-first-serve basis.

Signed-off-by: David Härdeman <[email protected]>
Link: https://github.com/openwrt/odhcpd/pull/255
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
README.md
src/config.c
src/odhcpd.h

index 69d7a6c437d4a3575c13e6f7bed0da7498aaac41..cbea25f5f0e4864b9250abfb0791add52f3d017a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@ and may also receive information from ubus
 | :-------------------- | :---- | :---- | :---------- |
 | ip                   |string |(none) | IPv4 host address |
 | mac                  |list\|string|(none) | HexadecimalMACaddress(es) |
-| duid                 |string |(none) | Hexadecimal DUID, or DUID%IAID |
+| duid                 |list\|string|(none) | Hexadecimal DUID(s), or DUID%IAID(s) |
 | hostid               |string |(none) | IPv6hostidentifier |
 | name                 |string |(none) | Hostname |
 | leasetime            |string |(none) | DHCPv4/v6leasetime |
index 445b97aee0ffde806d72f7019047e8c5f1c5d203..cfc0af64917b679d1c5c93e70fd7b8f1a33ccab3 100644 (file)
@@ -194,7 +194,7 @@ const struct uci_blob_param_list interface_attr_list = {
 const struct blobmsg_policy lease_attrs[LEASE_ATTR_MAX] = {
        [LEASE_ATTR_IP] = { .name = "ip", .type = BLOBMSG_TYPE_STRING },
        [LEASE_ATTR_MAC] = { .name = "mac", .type = BLOBMSG_TYPE_ARRAY },
-       [LEASE_ATTR_DUID] = { .name = "duid", .type = BLOBMSG_TYPE_STRING },
+       [LEASE_ATTR_DUID] = { .name = "duid", .type = BLOBMSG_TYPE_ARRAY },
        [LEASE_ATTR_HOSTID] = { .name = "hostid", .type = BLOBMSG_TYPE_STRING },
        [LEASE_ATTR_LEASETIME] = { .name = "leasetime", .type = BLOBMSG_TYPE_STRING },
        [LEASE_ATTR_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
@@ -463,14 +463,59 @@ static void free_lease(struct lease *l)
        free(l);
 }
 
+static bool parse_duid(struct duid *duid, struct blob_attr *c)
+{
+       const char *duid_str = blobmsg_get_string(c);
+       size_t duid_str_len = blobmsg_data_len(c) - 1;
+       ssize_t duid_len;
+       const char *iaid_str;
+
+       /* We support a hex string with either "<DUID>", or "<DUID>%<IAID>" */
+       iaid_str = strrchr(duid_str, '%');
+       if (iaid_str) {
+               size_t iaid_str_len = strlen(++iaid_str);
+               char *end;
+
+               /* IAID = uint32, RFC8415, §21.4, §21.5, §21.21 */
+               if (iaid_str_len < 1 || iaid_str_len > 2 * sizeof(uint32_t)) {
+                       syslog(LOG_ERR, "Invalid IAID length '%s'", iaid_str);
+                       return false;
+               }
+
+               errno = 0;
+               duid->iaid = strtoul(iaid_str, &end, 16);
+               if (errno || *end != '\0') {
+                       syslog(LOG_ERR, "Invalid IAID '%s'", iaid_str);
+                       return false;
+               }
+
+               duid->iaid_set = true;
+               duid_str_len -= (iaid_str_len + 1);
+       }
+
+       if (duid_str_len < 2 || duid_str_len > DUID_MAX_LEN * 2 || duid_str_len % 2) {
+               syslog(LOG_ERR, "Invalid DUID length '%.*s'", (int)duid_str_len, duid_str);
+               return false;
+       }
+
+       duid_len = odhcpd_unhexlify(duid->id, duid_str_len / 2, duid_str);
+       if (duid_len < 0) {
+               syslog(LOG_ERR, "Invalid DUID '%.*s'", (int)duid_str_len, duid_str);
+               return false;
+       }
+
+       duid->len = duid_len;
+       return true;
+}
+
 int set_lease_from_blobmsg(struct blob_attr *ba)
 {
        struct blob_attr *tb[LEASE_ATTR_MAX], *c;
        struct lease *l = NULL;
        int mac_count = 0;
        struct ether_addr *macs;
-       size_t duid_alloc_size = 0;
-       uint8_t *duid;
+       int duid_count = 0;
+       struct duid *duids;
 
        blobmsg_parse(lease_attrs, LEASE_ATTR_MAX, tb, blob_data(ba), blob_len(ba));
 
@@ -480,13 +525,15 @@ int set_lease_from_blobmsg(struct blob_attr *ba)
                        goto err;
        }
 
-       if ((c = tb[LEASE_ATTR_DUID]))
-               /* We might overallocate some bytes here, that's fine */
-               duid_alloc_size = (blobmsg_data_len(c) - 1) / 2;
+       if ((c = tb[LEASE_ATTR_DUID])) {
+               duid_count = blobmsg_check_array_len(c, BLOBMSG_TYPE_STRING, blob_raw_len(c));
+               if (duid_count < 0)
+                       goto err;
+       }
 
        l = calloc_a(sizeof(*l),
                     &macs, mac_count * sizeof(*macs),
-                    &duid, duid_alloc_size);
+                    &duids, duid_count * sizeof(*duids));
        if (!l)
                goto err;
 
@@ -504,50 +551,16 @@ int set_lease_from_blobmsg(struct blob_attr *ba)
        }
 
        if ((c = tb[LEASE_ATTR_DUID])) {
-               const char *duid_str = blobmsg_get_string(c);
-               size_t duid_str_len = blobmsg_data_len(c) - 1;
-               ssize_t duid_len;
-               const char *iaid_str;
-
-               /* We support a hex string with either "<DUID>", or "<DUID>%<IAID>" */
-               iaid_str = strrchr(duid_str, '%');
-               if (iaid_str) {
-                       size_t iaid_str_len = strlen(++iaid_str);
-
-                       /* IAID = uint32, RFC8415, §21.4, §21.5, §21.21 */
-                       if (iaid_str_len < 1 || iaid_str_len > 2 * sizeof(uint32_t)) {
-                               syslog(LOG_ERR, "Invalid IAID length '%s'", iaid_str);
-                               goto err;
-                       }
-
-                       errno = 0;
-                       l->iaid = strtoull(iaid_str, NULL, 16);
-                       if (errno) {
-                               syslog(LOG_ERR, "Invalid IAID '%s'", iaid_str);
-                               goto err;
-                       }
-
-                       l->iaid_set = true;
-                       duid_str_len -= (iaid_str_len + 1);
-               }
-
-               if (duid_str_len < 2 || duid_str_len > DUID_MAX_LEN * 2 || duid_str_len % 2) {
-                       syslog(LOG_ERR, "Invalid DUID length '%.*s'", (int)duid_str_len, duid_str);
-                       goto err;
-               }
+               struct blob_attr *cur;
+               size_t rem;
+               unsigned i = 0;
 
-               l->duid = duid;
-               duid_len = odhcpd_unhexlify(l->duid, duid_str_len / 2, duid_str);
-               if (duid_len < 0) {
-                       syslog(LOG_ERR, "Invalid DUID '%.*s'", (int)duid_str_len, duid_str);
-                       goto err;
-               }
+               l->duid_count = duid_count;
+               l->duids = duids;
 
-               l->duid_len = duid_len;
-               syslog(LOG_DEBUG,
-                      "Found a static lease for DUID '%.*s' (%zi bytes), IAID 0x%08" PRIx32 "%s",
-                      (int)duid_str_len, duid_str, duid_len,
-                      l->iaid, l->iaid_set ? "" : " (not defined)");
+               blobmsg_for_each_attr(cur, c, rem)
+                       if (!parse_duid(&duids[i++], cur))
+                               goto err;
        }
 
        if ((c = tb[LEASE_ATTR_NAME])) {
@@ -1590,14 +1603,19 @@ static int lease_cmp(const void *k1, const void *k2, _unused void *ptr)
        const struct lease *l1 = k1, *l2 = k2;
        int cmp = 0;
 
-       if (l1->duid_len != l2->duid_len)
-               return l1->duid_len - l2->duid_len;
+       if (l1->duid_count != l2->duid_count)
+               return l1->duid_count - l2->duid_count;
 
-       if (l1->duid_len && l2->duid_len)
-               cmp = memcmp(l1->duid, l2->duid, l1->duid_len);
+       for (size_t i = 0; i < l1->duid_count; i++) {
+               if (l1->duids[i].len != l2->duids[i].len)
+                       return l1->duids[i].len - l2->duids[i].len;
 
-       if (cmp)
-               return cmp;
+               if (l1->duids[i].len && l2->duids[i].len) {
+                       cmp = memcmp(l1->duids[i].id, l2->duids[i].id, l1->duids[i].len);
+                       if (cmp)
+                               return cmp;
+               }
+       }
 
        if (l1->mac_count != l2->mac_count)
                return l1->mac_count - l2->mac_count;
@@ -1682,16 +1700,21 @@ struct lease *config_find_lease_by_duid_and_iaid(const uint8_t *duid, const uint
        struct lease *l, *candidate = NULL;
 
        vlist_for_each_element(&leases, l, node) {
-               if (l->duid_len != len || memcmp(l->duid, duid, len))
-                       continue;
+               for (size_t i = 0; i < l->duid_count; i++) {
+                       if (l->duids[i].len != len)
+                               continue;
 
-               if (!l->iaid_set) {
-                       candidate = l;
-                       continue;
-               }
+                       if (memcmp(l->duids[i].id, duid, len))
+                               continue;
 
-               if (l->iaid == iaid)
-                       return l;
+                       if (!l->duids[i].iaid_set) {
+                               candidate = l;
+                               continue;
+                       }
+
+                       if (l->duids[i].iaid == iaid)
+                               return l;
+               }
        }
 
        return candidate;
index e8ce5a92857b7a2aae6e867c895a9630b08c5bf5..e96cc359d6b3ee563d2ee0baba99da77e19e8524 100644 (file)
@@ -184,6 +184,13 @@ struct config {
 #define DUID_MAX_LEN 130
 #define DUID_HEXSTRLEN (DUID_MAX_LEN * 2 + 1)
 
+struct duid {
+       uint8_t len;
+       uint8_t id[DUID_MAX_LEN];
+       uint32_t iaid;
+       bool iaid_set;
+};
+
 struct lease {
        struct vlist_node node;
        struct list_head assignments;
@@ -191,10 +198,8 @@ struct lease {
        uint64_t hostid;
        size_t mac_count;
        struct ether_addr *macs;
-       uint16_t duid_len;
-       uint8_t *duid;
-       uint32_t iaid;
-       bool iaid_set;
+       size_t duid_count;
+       struct duid *duids;
        uint32_t leasetime;
        char *hostname;
 };