dhcpv4: support IPv6-only preferred (RFC8925)
authorDavid Härdeman <[email protected]>
Sun, 23 Nov 2025 18:17:04 +0000 (19:17 +0100)
committerÁlvaro Fernández Rojas <[email protected]>
Fri, 28 Nov 2025 06:58:58 +0000 (07:58 +0100)
This adds support for RFC8925/IPv6-only preferred to the DHCPv4 server.

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

index 663986a2874ace8e2c31621df973124c19137ba6..390708952d9522ec5373810d4ccb10efcf88991c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -122,6 +122,7 @@ and may also receive information from ubus
 | ntp                  |list   |`<local address>`| NTP servers to announce accepts IPv4 and IPv6 |
 | upstream             |list   | -     | A list of interfaces which can be used as a source of configuration information (e.g. for NTP servers, if not set explicitly). |
 | captive_portal_uri |string | no  | The API URI to be sent in RFC8910 captive portal options, via DHCPv4, DHCPv6, and ICMPv6 RA. |
+| ipv6_only_preferred   |integer| 0 | Indicate that IPv6-only mode is preferred (RFC8925) [V6ONLY_WAIT time in seconds] |
 
 [//]: # "dhcpv6_raw - string - not documented, may change when generic DHCPv4/DHCPv6 options are added"
 
index b7f32ee20aee3f86e8ffe0e1f6c7485c85915dff..ec110b5c5db1fe4acf06278eec983099069b9d5d 100644 (file)
@@ -22,6 +22,7 @@
 #include "odhcpd.h"
 #include "router.h"
 #include "dhcpv6-pxe.h"
+#include "dhcpv4.h"
 
 static struct blob_buf b;
 
@@ -139,6 +140,7 @@ enum {
        IFACE_ATTR_MAX_VALID_LIFETIME,
        IFACE_ATTR_NTP,
        IFACE_ATTR_CAPTIVE_PORTAL_URI,
+       IFACE_ATTR_IPV6_ONLY_PREFERRED,
        IFACE_ATTR_MAX
 };
 
@@ -192,6 +194,7 @@ static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = {
        [IFACE_ATTR_MAX_VALID_LIFETIME] = { .name = "max_valid_lifetime", .type = BLOBMSG_TYPE_STRING },
        [IFACE_ATTR_NTP] = { .name = "ntp", .type = BLOBMSG_TYPE_ARRAY },
        [IFACE_ATTR_CAPTIVE_PORTAL_URI] = { .name = "captive_portal_uri", .type = BLOBMSG_TYPE_STRING },
+       [IFACE_ATTR_IPV6_ONLY_PREFERRED] = { .name = "ipv6_only_preferred", .type = BLOBMSG_TYPE_INT32 },
 };
 
 const struct uci_blob_param_list interface_attr_list = {
@@ -1652,6 +1655,19 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr
                }
        }
 
+       if ((c = tb[IFACE_ATTR_IPV6_ONLY_PREFERRED])) {
+               uint32_t v6only_wait = blobmsg_get_u32(c);
+
+               if (v6only_wait > 0 && v6only_wait < DHCPV4_MIN_V6ONLY_WAIT) {
+                       warn("Invalid %s value configured for interface '%s', clamped to %d",
+                            iface_attrs[IFACE_ATTR_IPV6_ONLY_PREFERRED].name,
+                            iface->name, DHCPV4_MIN_V6ONLY_WAIT);
+                       v6only_wait = DHCPV4_MIN_V6ONLY_WAIT;
+               }
+
+               iface->dhcpv4_v6only_wait = v6only_wait;
+       }
+
        if ((c = tb[IFACE_ATTR_RA_PREFERENCE])) {
                const char *prio = blobmsg_get_string(c);
 
index 97c1ec2bb739c5f8fd9423177ab367641f501aaa..17c34ba6c295de482ebc1c29c6dbba6886cd0280 100644 (file)
@@ -789,6 +789,7 @@ enum {
        IOV_DNR,
        IOV_DNR_BODY,
        IOV_CAPTIVE_PORTAL,
+       IOV_IPV6_ONLY_PREF,
        IOV_END,
        IOV_PADDING,
        IOV_TOTAL
@@ -810,6 +811,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
        uint8_t *req_clientid = NULL;
        size_t req_clientid_len = 0;
        bool req_accept_fr = false;
+       bool ipv6_only = false;
 
        /* Reply variables */
        struct dhcpv4_message reply = {
@@ -901,6 +903,11 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
        struct dhcpv4_option reply_dnr = {
                .code = DHCPV4_OPT_DNR,
        };
+       struct dhcpv4_option_u32 reply_ipv6_only = {
+               .code = DHCPV4_OPT_IPV6_ONLY_PREFERRED,
+               .len = sizeof(uint32_t),
+               .data = htonl(iface->dhcpv4_v6only_wait),
+       };
        uint8_t reply_end = DHCPV4_OPT_END;
 
        struct iovec iov[IOV_TOTAL] = {
@@ -931,6 +938,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                [IOV_DNR]               = { &reply_dnr, 0 },
                [IOV_DNR_BODY]          = { NULL, 0 },
                [IOV_CAPTIVE_PORTAL]    = { NULL, 0 },
+               [IOV_IPV6_ONLY_PREF]    = { &reply_ipv6_only, 0 },
                [IOV_END]               = { &reply_end, sizeof(reply_end) },
                [IOV_PADDING]           = { NULL, 0 },
        };
@@ -986,10 +994,12 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                                return;
                        break;
                case DHCPV4_OPT_REQOPTS:
-                       if (opt->len > 0) {
-                               req_opts = opt->data;
-                               req_opts_len = opt->len;
-                       }
+                       req_opts = opt->data;
+                       req_opts_len = opt->len;
+                       if (iface->dhcpv4_v6only_wait)
+                               for (uint8_t i = 0; i < opt->len; i++)
+                                       if (opt->data[i] == DHCPV4_OPT_IPV6_ONLY_PREFERRED)
+                                               ipv6_only = true;
                        break;
                case DHCPV4_OPT_CLIENTID:
                        if (opt->len >= 2) {
@@ -1028,6 +1038,9 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                             &reply_incl_fr, &fr_serverid);
                return;
        case DHCPV4_MSG_DISCOVER:
+               if (ipv6_only)
+                       break;
+               _o_fallthrough;
        case DHCPV4_MSG_REQUEST:
                lease = dhcpv4_lease(iface, req_msg, req->chaddr, req_clientid,
                                     req_clientid_len, req_addr, &req_leasetime,
@@ -1041,7 +1054,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
        /* We are at the point where we know the client expects a reply */
        switch (req_msg) {
        case DHCPV4_MSG_DISCOVER:
-               if (!lease)
+               if (!lease && !ipv6_only)
                        return;
                reply_msg.data = DHCPV4_MSG_OFFER;
                break;
@@ -1296,6 +1309,10 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                        iov[IOV_DNR_BODY].iov_len = dnrs_len;
                        break;
 
+               case DHCPV4_OPT_IPV6_ONLY_PREFERRED:
+                       iov[IOV_IPV6_ONLY_PREF].iov_len = sizeof(reply_ipv6_only);
+                       break;
+
                case DHCPV4_OPT_CAPTIVE_PORTAL:
                        size_t uri_len = iface->captive_portal_uri_len;
                        if (uri_len == 0 || uri_len > UINT8_MAX)
index 045f78b3ec4b3b54b70f8b231e2f7a45d3c94fb3..e601da6d7d94fbbefb65e6c88df04b31d8164895 100644 (file)
@@ -29,6 +29,9 @@
 #define DHCPV4_FR_MIN_DELAY    500
 #define DHCPV4_FR_MAX_FUZZ     500
 
+// RFC8925, §3.4
+#define DHCPV4_MIN_V6ONLY_WAIT 300
+
 // RFC4361, §6.1
 #define DHCPV4_CLIENTID_TYPE_DUID_IAID 255
 
@@ -80,6 +83,7 @@ enum dhcpv4_opt {
        DHCPV4_OPT_CLIENTID = 61,
        DHCPV4_OPT_USER_CLASS = 77,
        DHCPV4_OPT_AUTHENTICATION = 90,
+       DHCPV4_OPT_IPV6_ONLY_PREFERRED = 108, // RFC8925
        DHCPV4_OPT_CAPTIVE_PORTAL = 114, // RFC8910
        DHCPV4_OPT_DNS_DOMAIN_SEARCH = 119,
        DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE = 145,
index a5ba49695ec7d376195d152b8c60178a23f1fadf..8a6461cbe1d99db48c9764f5bba280d554f1c8d7 100644 (file)
@@ -450,6 +450,7 @@ struct interface {
        struct in_addr *dhcpv4_routers; // IPv4 addresses for routers on this subnet
        size_t dhcpv4_routers_cnt;      // Count of router addresses
        bool dhcpv4_forcereconf;
+       uint32_t dhcpv4_v6only_wait;    // V6ONLY_WAIT for the IPv6-only preferred option (RFC8925)
 
        // DNS
        struct in_addr *dns_addrs4;     // IPv4 DNS server addresses to announce