From 7d2c83f447b33bfcd7cd90dfc82f0f0984b0fbbe Mon Sep 17 00:00:00 2001 From: "Mark H. Spatz" Date: Mon, 12 Jan 2026 22:07:42 -0500 Subject: [PATCH] dhcpv6-ia: add support for RFC6603 Prefix Exclude Option If option dhcpv6_pd_exclude is set in a dhcp config section (in addition to option dhcpv6_pd), consider delegating prefixes that include on-link prefixes. This allows an entire delegated prefix to be re-delegated to a downstream router under certain circumstances. Signed-off-by: Mark H. Spatz --- README.md | 1 + src/config.c | 6 ++++ src/dhcpv6-ia.c | 83 ++++++++++++++++++++++++++++++++++++++++++++----- src/dhcpv6-ia.h | 6 +++- src/dhcpv6.h | 1 + src/odhcpd.h | 2 ++ 6 files changed, 90 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7226e526..e47bc5cb 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ and may also receive information from ubus | dhcpv6_pd |bool | 1 | DHCPv6 stateful addressing hands out IA_PD - Internet Address - Prefix Delegation (PD) | | dhcpv6_pd_preferred |bool | 0 | Set the DHCPv6-PD Preferred (P) flag in outgoing ICMPv6 RA message PIOs (RFC9762); requires `dhcpv6` and `dhcpv6_pd`. | | dhcpv6_pd_min_len |integer| 62 | Minimum prefix length to delegate with IA_PD (adjusted, if necessary, to be longer than the interface prefix length). Range [1,64] | +| dhcpv6_pd_exclude |bool | 0 | Allow delegation of a prefix containing the smaller on-link prefix (RFC6603); allows an entire interface prefix to be delegated. | | router |list |``| IPv4 addresses of routers on a given subnet (provided via DHCPv4, should be in order of preference) | | dns |list |``| DNS servers to announce, accepts IPv4 and IPv6 | | dnr |list |disabled| Encrypted DNS servers to announce, ` [ ...]` | diff --git a/src/config.c b/src/config.c index 29384482..1d890fe5 100644 --- a/src/config.c +++ b/src/config.c @@ -113,6 +113,7 @@ enum { IFACE_ATTR_DHCPV6_PD_PREFERRED, IFACE_ATTR_DHCPV6_PD, IFACE_ATTR_DHCPV6_PD_MIN_LEN, + IFACE_ATTR_DHCPV6_PD_EXCLUDE, IFACE_ATTR_DHCPV6_NA, IFACE_ATTR_DHCPV6_HOSTID_LEN, IFACE_ATTR_RA_DEFAULT, @@ -167,6 +168,7 @@ static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = { [IFACE_ATTR_DHCPV6_PD_PREFERRED] = { .name = "dhcpv6_pd_preferred", .type = BLOBMSG_TYPE_BOOL }, [IFACE_ATTR_DHCPV6_PD] = { .name = "dhcpv6_pd", .type = BLOBMSG_TYPE_BOOL }, [IFACE_ATTR_DHCPV6_PD_MIN_LEN] = { .name = "dhcpv6_pd_min_len", .type = BLOBMSG_TYPE_INT32 }, + [IFACE_ATTR_DHCPV6_PD_EXCLUDE] = { .name = "dhcpv6_pd_exclude", .type = BLOBMSG_TYPE_BOOL }, [IFACE_ATTR_DHCPV6_NA] = { .name = "dhcpv6_na", .type = BLOBMSG_TYPE_BOOL }, [IFACE_ATTR_DHCPV6_HOSTID_LEN] = { .name = "dhcpv6_hostidlength", .type = BLOBMSG_TYPE_INT32 }, [IFACE_ATTR_RA_DEFAULT] = { .name = "ra_default", .type = BLOBMSG_TYPE_INT32 }, @@ -324,6 +326,7 @@ static void set_interface_defaults(struct interface *iface) iface->dhcpv6_pd = true; iface->dhcpv6_pd_preferred = false; iface->dhcpv6_pd_min_len = PD_MIN_LEN_DEFAULT; + iface->dhcpv6_pd_exclude = false; iface->dhcpv6_na = true; iface->dhcpv6_hostid_len = HOSTID_LEN_DEFAULT; iface->dns_service = true; @@ -1465,6 +1468,9 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr iface->dhcpv6_pd_min_len = pd_min_len; } + if ((c = tb[IFACE_ATTR_DHCPV6_PD_EXCLUDE])) + iface->dhcpv6_pd_exclude = blobmsg_get_bool(c); + if ((c = tb[IFACE_ATTR_DHCPV6_NA])) iface->dhcpv6_na = blobmsg_get_bool(c); diff --git a/src/dhcpv6-ia.c b/src/dhcpv6-ia.c index 3366bd09..519c829c 100644 --- a/src/dhcpv6-ia.c +++ b/src/dhcpv6-ia.c @@ -346,8 +346,15 @@ static bool assign_pd(struct interface *iface, struct dhcpv6_lease *assign) if (iface->addr6_len < 1) return false; + bool allow_exclude = + iface->dhcpv6_pd_exclude && + (assign->flags & OAF_DHCPV6_PD_EXCLUDE) && + (assign->length < 64); /* Excluded prefix must be larger than the delegated prefix (RFC6603 ยง 4.2) */ + + const uint32_t asize = (1 << (64 - assign->length)) - 1; + /* Try honoring the hint first */ - uint32_t current = 1, asize = (1 << (64 - assign->length)) - 1; + uint32_t current = allow_exclude ? 0 : 1; if (assign->assigned_subnet_id) { list_for_each_entry(c, &iface->ia_assignments, head) { if (c->flags & OAF_DHCPV6_NA) @@ -355,6 +362,8 @@ static bool assign_pd(struct interface *iface, struct dhcpv6_lease *assign) if (assign->assigned_subnet_id >= current && assign->assigned_subnet_id + asize < c->assigned_subnet_id) { list_add_tail(&assign->head, &c->head); + debug("assign_pd chose subnet_id %08x on %s (hint honored)", + assign->assigned_subnet_id, iface->name); if (assign->bound) apply_lease(assign, true); @@ -367,7 +376,7 @@ static bool assign_pd(struct interface *iface, struct dhcpv6_lease *assign) } /* Fallback to a variable assignment */ - current = 1; + current = allow_exclude ? 0 : 1; list_for_each_entry(c, &iface->ia_assignments, head) { if (c->flags & OAF_DHCPV6_NA) continue; @@ -377,6 +386,8 @@ static bool assign_pd(struct interface *iface, struct dhcpv6_lease *assign) if (current + asize < c->assigned_subnet_id) { assign->assigned_subnet_id = current; list_add_tail(&assign->head, &c->head); + debug("assign_pd chose subnet_id %08x on %s", + assign->assigned_subnet_id, iface->name); if (assign->bound) apply_lease(assign, true); @@ -666,9 +677,42 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status, prefix_preferred_lt = prefix_valid_lt; if (a->flags & OAF_DHCPV6_PD) { + if (!valid_prefix_length(a, addrs[i].prefix_len)) + continue; + + /* If assign_pd() chose subnet id 0, send a PD-Exclude option for the first /64 in the delegated prefix */ + struct { + uint16_t option_code; + uint16_t option_len; + uint8_t prefix_len; + } _o_packed o_pd_exl; + size_t o_pd_exl_len = 0; + if (a->assigned_subnet_id == 0) { + const uint8_t excluded_prefix_len = 64; + + if (a->length >= excluded_prefix_len) { + error("BUG: Can't exclude a prefix from from IA_PD of size %u on %s", + a->length, iface->name); + continue; + } + + uint8_t excl_subnet_id_nbits = excluded_prefix_len - a->length; + uint8_t excl_subnet_id_nbytes = ((excl_subnet_id_nbits - 1) / 8) + 1; + o_pd_exl_len = sizeof(o_pd_exl) + excl_subnet_id_nbytes; + + /* Work around a bug in odhcp6c that ignores DHCPV6_OPT_PD_EXCLUDE with valid option length of 2. */ + if(o_pd_exl_len - DHCPV6_OPT_HDR_SIZE == 2) + o_pd_exl_len++; + + o_pd_exl.option_code = htons(DHCPV6_OPT_PD_EXCLUDE); + o_pd_exl.option_len = htons(o_pd_exl_len - DHCPV6_OPT_HDR_SIZE); + o_pd_exl.prefix_len = excluded_prefix_len; + /* (IPv6 subnet ID field is all zeros) */ + } + struct dhcpv6_ia_prefix o_ia_p = { .type = htons(DHCPV6_OPT_IA_PREFIX), - .len = htons(sizeof(o_ia_p) - DHCPV6_OPT_HDR_SIZE), + .len = htons(sizeof(o_ia_p) - DHCPV6_OPT_HDR_SIZE + o_pd_exl_len), .preferred_lt = htonl(prefix_preferred_lt), .valid_lt = htonl(prefix_valid_lt), .prefix_len = a->length, @@ -678,14 +722,17 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status, o_ia_p.addr.s6_addr32[1] |= htonl(a->assigned_subnet_id); o_ia_p.addr.s6_addr32[2] = o_ia_p.addr.s6_addr32[3] = 0; - if (!valid_prefix_length(a, addrs[i].prefix_len)) - continue; - - if (buflen < ia_len + sizeof(o_ia_p)) + if (buflen < ia_len + sizeof(o_ia_p) + o_pd_exl_len) return 0; memcpy(buf + ia_len, &o_ia_p, sizeof(o_ia_p)); ia_len += sizeof(o_ia_p); + + if(o_pd_exl_len) { + memset(buf + ia_len, 0, o_pd_exl_len); + memcpy(buf + ia_len, &o_pd_exl, sizeof(o_pd_exl)); + ia_len += o_pd_exl_len; + } } if (a->flags & OAF_DHCPV6_NA) { @@ -955,6 +1002,7 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *buf, size_t buflen, struct interface *ifac uint8_t *duid = NULL, mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; size_t hostname_len = 0, response_len = 0; bool notonlink = false, rapid_commit = false, accept_reconf = false; + bool oro_pd_exclude = false; char duidbuf[DUID_HEXSTRLEN], hostname[256]; dhcpv6_for_each_option(start, end, otype, olen, odata) { @@ -994,6 +1042,20 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *buf, size_t buflen, struct interface *ifac rapid_commit = true; break; + case DHCPV6_OPT_ORO: { + size_t reqopts_cnt = olen / sizeof(uint16_t); + for (size_t i = 0; i < reqopts_cnt; i++) { + uint16_t opt = odata[i * sizeof(uint16_t)] << 8 | + odata[i * sizeof(uint16_t) + 1]; + switch (opt) { + case DHCPV6_OPT_PD_EXCLUDE: + oro_pd_exclude = true; + break; + } + } + break; + } + default: break; } @@ -1180,7 +1242,12 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *buf, size_t buflen, struct interface *ifac a->length = reqlen; a->peer = *addr; a->iface = iface; - a->flags = is_pd ? OAF_DHCPV6_PD : OAF_DHCPV6_NA; + if (is_pd) { + a->flags = OAF_DHCPV6_PD; + if (oro_pd_exclude) + a->flags |= OAF_DHCPV6_PD_EXCLUDE; + } else + a->flags = OAF_DHCPV6_NA; a->valid_until = now; a->preferred_until = now; diff --git a/src/dhcpv6-ia.h b/src/dhcpv6-ia.h index 28bc9c06..1ca0aaae 100644 --- a/src/dhcpv6-ia.h +++ b/src/dhcpv6-ia.h @@ -27,7 +27,11 @@ struct in6_addr in6_from_prefix_and_iid(const struct odhcpd_ipaddr *prefix, uint static inline bool valid_prefix_length(const struct dhcpv6_lease *a, const uint8_t prefix_length) { - return a->length > prefix_length; + /* If the assigned_subnet_id is 0, allow the interface's entire prefix to be delegated. */ + if (a->assigned_subnet_id == 0) + return a->length >= prefix_length; + else + return a->length > prefix_length; } static inline bool valid_addr(const struct odhcpd_ipaddr *addr, time_t now) diff --git a/src/dhcpv6.h b/src/dhcpv6.h index 79f8c93c..9240723b 100644 --- a/src/dhcpv6.h +++ b/src/dhcpv6.h @@ -70,6 +70,7 @@ #define DHCPV6_OPT_BOOTFILE_URL 59 #define DHCPV6_OPT_BOOTFILE_PARAM 60 #define DHCPV6_OPT_CLIENT_ARCH 61 +#define DHCPV6_OPT_PD_EXCLUDE 67 #define DHCPV6_OPT_SOL_MAX_RT 82 #define DHCPV6_OPT_INF_MAX_RT 83 #define DHCPV6_OPT_DHCPV4_MSG 87 diff --git a/src/odhcpd.h b/src/odhcpd.h index 9298d62a..d3b769e9 100644 --- a/src/odhcpd.h +++ b/src/odhcpd.h @@ -192,6 +192,7 @@ enum odhcpd_mode { enum odhcpd_assignment_flags { OAF_DHCPV6_NA = (1 << 0), OAF_DHCPV6_PD = (1 << 1), + OAF_DHCPV6_PD_EXCLUDE = (1 << 2), }; #define DHCPV6_OPT_HDR_SIZE 4 @@ -468,6 +469,7 @@ struct interface { bool dhcpv6_na; uint32_t dhcpv6_hostid_len; uint32_t dhcpv6_pd_min_len; // minimum delegated prefix length + bool dhcpv6_pd_exclude; char *upstream; size_t upstream_len;