odhcpd: Implement RFC9762 DHCPv6 PD Preferred flag for PIOs
authorPaul Donald <[email protected]>
Sun, 9 Nov 2025 23:00:05 +0000 (00:00 +0100)
committerÁlvaro Fernández Rojas <[email protected]>
Mon, 10 Nov 2025 12:09:13 +0000 (13:09 +0100)
The RFC defines the P flag for use within PIOs, and defines its meaning
as "purely a positive indicator, telling nodes that DHCPv6 PD is
available and the network prefers that nodes use it".

The RFC abstract lays out:

The (P) flag is used to indicate that the network prefers that clients
use the deployment model in RFC 9663 instead of using individual
addresses in the on-link prefix assigned using SLAAC, or DHCPv6 address
assignment.

Signed-off-by: Paul Donald <[email protected]>
Link: https://github.com/openwrt/odhcpd/pull/301
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
README.md
src/config.c
src/odhcpd.h
src/router.c
src/router.h

index 7f3d3479f68e21754331ea638a52d56d89cd43ef..c9ac2ec3c08df655d99072788fe8435edabf61a3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -87,7 +87,8 @@ and may also receive information from ubus
 | dhcpv6_assignall     |bool   | 1     | Assign all viable DHCPv6 addresses in statefull mode; if disabled only the DHCPv6 address having the longest preferred lifetime is assigned |
 | dhcpv6_hostidlength  |integer| 12    | Host ID length of dynamically created leases, allowed values: 12 - 64 (bits). |
 | dhcpv6_na            |bool   | 1     | DHCPv6 stateful addressing hands out IA_NA - Internet Address - Network Address |
-| dhcpv6_pd            |bool   | 1     | DHCPv6 stateful addressing hands out IA_PD - Internet Address - Prefix Delegation |
+| 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| -     | Minimum prefix length to delegate with IA_PD (value is adjusted if needed to be greater than the interface prefix length).  Range [1,62] |
 | router               |list   |`<local address>`| Routers to announce, accepts IPv4 only |
 | dns                  |list   |`<local address>`| DNS servers to announce, accepts IPv4 and IPv6 |
index 1821b529f346530b5e484451741f90a34b211871..109376a14dcc655ddee14158320a4c7df86a04fb 100644 (file)
@@ -106,6 +106,7 @@ enum {
        IFACE_ATTR_DHCPV4_FORCERECONF,
        IFACE_ATTR_DHCPV6_RAW,
        IFACE_ATTR_DHCPV6_ASSIGNALL,
+       IFACE_ATTR_DHCPV6_PD_PREFERRED,
        IFACE_ATTR_DHCPV6_PD,
        IFACE_ATTR_DHCPV6_PD_MIN_LEN,
        IFACE_ATTR_DHCPV6_NA,
@@ -157,6 +158,7 @@ static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = {
        [IFACE_ATTR_DHCPV4_FORCERECONF] = { .name = "dhcpv4_forcereconf", .type = BLOBMSG_TYPE_BOOL },
        [IFACE_ATTR_DHCPV6_RAW] = { .name = "dhcpv6_raw", .type = BLOBMSG_TYPE_STRING },
        [IFACE_ATTR_DHCPV6_ASSIGNALL] = { .name ="dhcpv6_assignall", .type = BLOBMSG_TYPE_BOOL },
+       [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_NA] = { .name = "dhcpv6_na", .type = BLOBMSG_TYPE_BOOL },
@@ -311,6 +313,7 @@ static void set_interface_defaults(struct interface *iface)
        iface->dhcpv4_end.s_addr = htonl(START_DEFAULT + LIMIT_DEFAULT - 1);
        iface->dhcpv6_assignall = true;
        iface->dhcpv6_pd = true;
+       iface->dhcpv6_pd_preferred = false;
        iface->dhcpv6_pd_min_len = 0;
        iface->dhcpv6_na = true;
        iface->dhcpv6_hostid_len = HOSTID_LEN_DEFAULT;
@@ -1378,6 +1381,9 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr
        if ((c = tb[IFACE_ATTR_DHCPV6_ASSIGNALL]))
                iface->dhcpv6_assignall = blobmsg_get_bool(c);
 
+       if ((c = tb[IFACE_ATTR_DHCPV6_PD_PREFERRED]))
+               iface->dhcpv6_pd_preferred = blobmsg_get_bool(c);
+
        if ((c = tb[IFACE_ATTR_DHCPV6_PD]))
                iface->dhcpv6_pd = blobmsg_get_bool(c);
 
index 23f82d8dfe14473128e176dacee5fc562c1fb87f..6cae39dfce0118a306ab6945c62c9b682f7c815f 100644 (file)
@@ -440,6 +440,7 @@ struct interface {
        size_t dhcpv6_raw_len;
        bool dhcpv6_assignall;
        bool dhcpv6_pd;
+       bool dhcpv6_pd_preferred;
        bool dhcpv6_na;
        uint32_t dhcpv6_hostid_len;
        uint32_t dhcpv6_pd_min_len; // minimum delegated prefix length
index 9b8d002d2c16ddf2339d4b24fca5907c429b0fcd..1903eb40272843ed9be3382cb9d3f998f77caf58 100644 (file)
@@ -591,6 +591,15 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                adv.h.nd_ra_flags_reserved |= ND_RA_PREF_LOW;
        else if (iface->route_preference > 0)
                adv.h.nd_ra_flags_reserved |= ND_RA_PREF_HIGH;
+       if (iface->dhcpv6 != MODE_DISABLED && iface->dhcpv6_pd && iface->dhcpv6_pd_preferred) {
+               /* RFC9762 § 5
+                * If the network desires to delegate prefixes to devices that support
+                * DHCPv6 prefix delegation but do not support the P flag, it SHOULD 
+                * also set the M or O bits in the RA to 1
+                */
+               adv.h.nd_ra_flags_reserved |= ND_RA_FLAG_MANAGED;
+       }
 
        adv.h.nd_ra_reachable = htonl(iface->ra_reachabletime);
        adv.h.nd_ra_retransmit = htonl(iface->ra_retranstime);
@@ -737,11 +746,23 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                p->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION;
                p->nd_opt_pi_len = 4;
                p->nd_opt_pi_prefix_len = (addr->prefix < 64) ? 64 : addr->prefix;
+               /* RFC9762 DHCPv6-PD Preferred Flag § 6: 
+                * Routers SHOULD set the P flag to zero by default...
+                */
                p->nd_opt_pi_flags_reserved = 0;
                if (!iface->ra_not_onlink)
                        p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK;
                if (iface->ra_slaac && addr->prefix <= 64)
                        p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO;
+               if (iface->dhcpv6 != MODE_DISABLED && iface->dhcpv6_pd && iface->dhcpv6_pd_preferred)
+                       /* RFC9762 DHCPv6-PD Preferred Flag
+                        * We can run both SLAAC and DHCPv6-PD.
+                        * §6:
+                        * "Routers MUST allow the P flag to be configured separately from the A flag. 
+                        * ...en/disabling the P flag MUST NOT trigger automatic changes in the A flag
+                        * value set by the router."
+                        */
+                       p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_PD_PREFERRED;
                if (iface->ra_advrouter)
                        // RFC6275, §7.2
                        p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_RADDR;
index afcf3df6cd0b009f80883494a4dd97e9ef23a5b7..46c6387353f3ec587f5cd189b5fba5f4c3ce3043 100644 (file)
@@ -93,3 +93,8 @@ struct icmpv6_opt {
 #define ND_RA_FLAG_PROXY               0x4
 #define ND_RA_PREF_HIGH                        (1 << 3)
 #define ND_RA_PREF_LOW                 (3 << 3)
+
+/* RFC9762 DHCPv6 PD Availability - Preferred Flag
+ * use this until it is defined in netinet/icmp6.h
+ */
+#define ND_OPT_PI_FLAG_PD_PREFERRED            0x10