odhcpd: apply RFC9096 recommended lifetimes
authorAviana Cruz <[email protected]>
Sat, 16 Sep 2023 15:04:12 +0000 (15:04 +0000)
committerÁlvaro Fernández Rojas <[email protected]>
Thu, 18 Sep 2025 17:29:54 +0000 (19:29 +0200)
To address issues with hosts retaining stale IPv6 configuration, this change
implements the Recommended Option Lifetimes Configuration Values from
RFC 9096-Section 4.

The `ra_lifetime` is set to 2700s (45min) by default, and the DHCPv6-PD is now
capped at 2700s for preferred lifetime and 5400s (90min) for valid lifetime.

The following changes are introduced:

- The `ra_useleasetime` option is removed.
- New options `max_preferred_lifetime` and `max_valid_lifetime` are added to
  configure the upper limits for prefix lifetimes.
- The Router Advertisement, DNS, and DNR option lifetimes are now dynamically
  adjusted based on the advertised prefix lifetimes to ensure consistency.

These changes help to mitigate issues with stale configurations on client
devices, particularly after network renumbering events.

Signed-off-by: Aviana Cruz <[email protected]>
README
src/config.c
src/dhcpv6-ia.c
src/odhcpd.h
src/router.c

diff --git a/README b/README
index 0a82762c5d89f15aa0d95bbde89d263526139d1b..bb9c0827f5a533fdadc246532138e541ef4b981a 100644 (file)
--- a/README
+++ b/README
@@ -119,7 +119,9 @@ domain                      list    <local search domain>   Search domains to announce
 leasetime              string  12h                     DHCPv4 address leasetime
 start                  integer 100                     DHCPv4 pool start
 limit                  integer 150                     DHCPv4 pool size
-preferred_lifetime     string  7d                      Value for the preferred lifetime
+max_preferred_lifetime string  45m                     Upper limit for the preferred lifetime
+                                                       for a prefix
+max_valid_lifetime     string  90m                     Upper limit for the valid lifetime
                                                        for a prefix
 ra_default             integer 0                       Override default route
                        0: default, 1: ignore no public address, 2: ignore all
@@ -134,11 +136,9 @@ ra_maxinterval             integer 600                     Maximum time allowed between
                                                        sending unsolicited RA
 ra_mininterval         integer 200                     Minimum time allowed between
                                                        sending unsolicited RA
-ra_lifetime            integer 1800                    Value to be placed in Router
-                                                       Lifetime field of RA
-ra_useleasetime                bool    0                       Use configured leasetime as
-                                                       limit for the preferred and
-                                                       valid lifetime of a prefix
+ra_lifetime            integer 2700                    Value to be placed in Router
+                                                       Lifetime field of RA. Not recommended to be
+                                                       more than 2700 (RFC9096).
 ra_reachabletime       integer 0                       Reachable Time in milliseconds to be
                                                        advertised in RA messages
 ra_retranstime         integer 0                       Retransmit Time in milliseconds to be
index 79928820bab8bae3c76d0c86e5f3667aca27381f..a6b699ec41ea84fc32ae1dff747fc7d0d2ff00f6 100644 (file)
@@ -97,7 +97,6 @@ enum {
        IFACE_ATTR_RA_MININTERVAL,
        IFACE_ATTR_RA_MAXINTERVAL,
        IFACE_ATTR_RA_LIFETIME,
-       IFACE_ATTR_RA_USELEASETIME,
        IFACE_ATTR_RA_REACHABLETIME,
        IFACE_ATTR_RA_RETRANSTIME,
        IFACE_ATTR_RA_HOPLIMIT,
@@ -109,7 +108,8 @@ enum {
        IFACE_ATTR_NDPROXY_ROUTING,
        IFACE_ATTR_NDPROXY_SLAVE,
        IFACE_ATTR_PREFIX_FILTER,
-       IFACE_ATTR_PREFERRED_LIFETIME,
+       IFACE_ATTR_MAX_PREFERRED_LIFETIME,
+       IFACE_ATTR_MAX_VALID_LIFETIME,
        IFACE_ATTR_NTP,
        IFACE_ATTR_MAX
 };
@@ -153,7 +153,6 @@ static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = {
        [IFACE_ATTR_RA_MININTERVAL] = { .name = "ra_mininterval", .type = BLOBMSG_TYPE_INT32 },
        [IFACE_ATTR_RA_MAXINTERVAL] = { .name = "ra_maxinterval", .type = BLOBMSG_TYPE_INT32 },
        [IFACE_ATTR_RA_LIFETIME] = { .name = "ra_lifetime", .type = BLOBMSG_TYPE_INT32 },
-       [IFACE_ATTR_RA_USELEASETIME] = { .name = "ra_useleasetime", .type = BLOBMSG_TYPE_BOOL },
        [IFACE_ATTR_RA_REACHABLETIME] = { .name = "ra_reachabletime", .type = BLOBMSG_TYPE_INT32 },
        [IFACE_ATTR_RA_RETRANSTIME] = { .name = "ra_retranstime", .type = BLOBMSG_TYPE_INT32 },
        [IFACE_ATTR_RA_HOPLIMIT] = { .name = "ra_hoplimit", .type = BLOBMSG_TYPE_INT32 },
@@ -163,7 +162,8 @@ static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = {
        [IFACE_ATTR_NDPROXY_ROUTING] = { .name = "ndproxy_routing", .type = BLOBMSG_TYPE_BOOL },
        [IFACE_ATTR_NDPROXY_SLAVE] = { .name = "ndproxy_slave", .type = BLOBMSG_TYPE_BOOL },
        [IFACE_ATTR_PREFIX_FILTER] = { .name = "prefix_filter", .type = BLOBMSG_TYPE_STRING },
-       [IFACE_ATTR_PREFERRED_LIFETIME] = { .name = "preferred_lifetime", .type = BLOBMSG_TYPE_STRING },
+       [IFACE_ATTR_MAX_PREFERRED_LIFETIME] = { .name = "max_preferred_lifetime", .type = BLOBMSG_TYPE_STRING },
+       [IFACE_ATTR_MAX_VALID_LIFETIME] = { .name = "max_valid_lifetime", .type = BLOBMSG_TYPE_STRING },
        [IFACE_ATTR_NTP] = { .name = "ntp", .type = BLOBMSG_TYPE_ARRAY },
 };
 
@@ -260,7 +260,8 @@ static void set_interface_defaults(struct interface *iface)
        iface->ndp = MODE_DISABLED;
        iface->learn_routes = 1;
        iface->dhcp_leasetime = 43200;
-       iface->preferred_lifetime = 604800; /* rfc4861#section-6.2.1: AdvPreferredLifetime 7 days */
+       iface->max_preferred_lifetime = ND_PREFERRED_LIMIT;
+       iface->max_valid_lifetime = ND_VALID_LIMIT;
        iface->dhcpv4_start.s_addr = htonl(START_DEFAULT);
        iface->dhcpv4_end.s_addr = htonl(START_DEFAULT + LIMIT_DEFAULT - 1);
        iface->dhcpv6_assignall = true;
@@ -998,15 +999,26 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr
 
        }
 
-       if ((c = tb[IFACE_ATTR_PREFERRED_LIFETIME])) {
+       if ((c = tb[IFACE_ATTR_MAX_PREFERRED_LIFETIME])) {
                double time = parse_leasetime(c);
 
-               if (time >= 0)
-                       iface->preferred_lifetime = time;
-               else
+               if (time >= 0) {
+                       iface->max_preferred_lifetime = time;
+               } else {
                        syslog(LOG_ERR, "Invalid %s value configured for interface '%s'",
-                                       iface_attrs[IFACE_ATTR_PREFERRED_LIFETIME].name, iface->name);
+                              iface_attrs[IFACE_ATTR_MAX_PREFERRED_LIFETIME].name, iface->name);
+               }
+       }
 
+       if ((c = tb[IFACE_ATTR_MAX_VALID_LIFETIME])) {
+               double time = parse_leasetime(c);
+
+               if (time >= 0) {
+                       iface->max_valid_lifetime = time;
+               } else {
+                       syslog(LOG_ERR, "Invalid %s value configured for interface '%s'",
+                              iface_attrs[IFACE_ATTR_MAX_VALID_LIFETIME].name, iface->name);
+               }
        }
 
        if ((c = tb[IFACE_ATTR_START])) {
@@ -1330,9 +1342,6 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr
        if ((c = tb[IFACE_ATTR_RA_LIFETIME]))
                iface->ra_lifetime = blobmsg_get_u32(c);
 
-       if ((c = tb[IFACE_ATTR_RA_USELEASETIME]))
-               iface->ra_useleasetime = blobmsg_get_bool(c);
-
        if ((c = tb[IFACE_ATTR_RA_DNS]))
                iface->ra_dns = blobmsg_get_bool(c);
 
index b4ed9a059f2495886c48f59b81892094e412f9e8..b4c6c9536d16b2c7b50eeb87b925485a82569941 100644 (file)
@@ -120,7 +120,7 @@ static inline bool valid_prefix_length(const struct dhcp_assignment *a, const ui
 
 static inline bool valid_addr(const struct odhcpd_ipaddr *addr, time_t now)
 {
-       return (addr->prefix <= 96 && addr->preferred_lt > (uint32_t)now);
+       return (addr->prefix <= 96 && addr->valid_lt > (uint32_t)now && addr->preferred_lt > (uint32_t)now);
 }
 
 static size_t get_preferred_addr(const struct odhcpd_ipaddr *addrs, const size_t addrlen)
@@ -1039,17 +1039,27 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status,
        }
 
        if (a) {
-               uint32_t leasetime, preferred_lt;
+               uint32_t leasetime;
 
                if (a->leasetime) {
                        leasetime = a->leasetime;
-                       preferred_lt = a->leasetime;
                } else {
                        leasetime = iface->dhcp_leasetime;
-                       preferred_lt = iface->preferred_lifetime;
                }
 
-               uint32_t valid_lt = leasetime;
+               uint32_t floor_preferred_lifetime, floor_valid_lifetime; /* For calculating T1 / T2 */
+
+               if (iface->max_preferred_lifetime && iface->max_preferred_lifetime < leasetime) {
+                       floor_preferred_lifetime = iface->max_preferred_lifetime;
+               } else {
+                       floor_preferred_lifetime = leasetime;
+               }
+
+               if (iface->max_valid_lifetime && iface->max_valid_lifetime < leasetime) {
+                       floor_valid_lifetime = iface->max_valid_lifetime;
+               } else {
+                       floor_valid_lifetime = leasetime;
+               }
 
                struct odhcpd_ipaddr *addrs = (a->managed) ? a->managed : iface->addr6;
                size_t addrlen = (a->managed) ? (size_t)a->managed_size : iface->addr6_len;
@@ -1073,15 +1083,20 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status,
                        prefix_preferred_lt = addrs[i].preferred_lt;
                        prefix_valid_lt = addrs[i].valid_lt;
 
-                       if (prefix_preferred_lt != UINT32_MAX)
+                       if (prefix_preferred_lt != UINT32_MAX) {
                                prefix_preferred_lt -= now;
 
-                       if (prefix_preferred_lt > preferred_lt)
-                               prefix_preferred_lt = preferred_lt;
+                               if (iface->max_preferred_lifetime && prefix_preferred_lt > iface->max_preferred_lifetime)
+                                       prefix_preferred_lt = iface->max_preferred_lifetime;
+                       }
 
-                       if (prefix_valid_lt != UINT32_MAX)
+                       if (prefix_valid_lt != UINT32_MAX) {
                                prefix_valid_lt -= now;
 
+                               if (iface->max_valid_lifetime && prefix_valid_lt > iface->max_valid_lifetime)
+                                       prefix_valid_lt = iface->max_valid_lifetime;
+                       }
+
                        if (prefix_valid_lt > leasetime)
                                prefix_valid_lt = leasetime;
 
@@ -1135,24 +1150,24 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status,
 
                        /* Calculate T1 / T2 based on non-deprecated addresses */
                        if (prefix_preferred_lt > 0) {
-                               if (prefix_preferred_lt < preferred_lt)
-                                       preferred_lt = prefix_preferred_lt;
+                               if (floor_preferred_lifetime > prefix_preferred_lt)
+                                       floor_preferred_lifetime = prefix_preferred_lt;
 
-                               if (prefix_valid_lt < valid_lt)
-                                       valid_lt = prefix_valid_lt;
+                               if (floor_valid_lifetime > prefix_valid_lt)
+                                       floor_valid_lifetime = prefix_valid_lt;
                        }
                }
 
                if (!INFINITE_VALID(a->valid_until))
                        /* UINT32_MAX is RFC defined as infinite lease-time */
-                       a->valid_until = (valid_lt == UINT32_MAX) ? 0 : valid_lt + now;
+                       a->valid_until = (floor_valid_lifetime == UINT32_MAX) ? 0 : floor_valid_lifetime + now;
 
                if (!INFINITE_VALID(a->preferred_until))
                        /* UINT32_MAX is RFC defined as infinite lease-time */
-                       a->preferred_until = (preferred_lt == UINT32_MAX) ? 0 : preferred_lt + now;
+                       a->preferred_until = (floor_preferred_lifetime == UINT32_MAX) ? 0 : floor_preferred_lifetime + now;
 
-               o_ia.t1 = htonl((preferred_lt == UINT32_MAX) ? preferred_lt : preferred_lt * 5 / 10);
-               o_ia.t2 = htonl((preferred_lt == UINT32_MAX) ? preferred_lt : preferred_lt * 8 / 10);
+               o_ia.t1 = htonl((floor_preferred_lifetime == UINT32_MAX) ? floor_preferred_lifetime : floor_preferred_lifetime * 5 / 10);
+               o_ia.t2 = htonl((floor_preferred_lifetime == UINT32_MAX) ? floor_preferred_lifetime : floor_preferred_lifetime * 8 / 10);
 
                if (!o_ia.t1)
                        o_ia.t1 = htonl(1);
index 0c8b671284d337e3ea1e22ecf7a582de1c0df041..4c510e23c25c02adabe636b7b114f59a89293005 100644 (file)
 // RFC 8781 defines PREF64 option
 #define ND_OPT_PREF64 38
 
+// RFC9096 defines recommended option lifetimes configuration values
+#define ND_PREFERRED_LIMIT 2700
+#define ND_VALID_LIMIT 5400
+
 // RFC 9463 - Discovery of Network-designated Resolvers (DNR)
 #define ND_OPT_DNR 144
 
@@ -328,7 +332,6 @@ struct interface {
        bool ra_slaac;
        bool ra_not_onlink;
        bool ra_advrouter;
-       bool ra_useleasetime;
        bool ra_dns;
        uint8_t pref64_length;
        uint8_t pref64_plc;
@@ -346,7 +349,8 @@ struct interface {
        uint32_t ra_retranstime;
        uint32_t ra_hoplimit;
        int ra_mtu;
-       uint32_t preferred_lifetime;
+       uint32_t max_preferred_lifetime;
+       uint32_t max_valid_lifetime;
 
        // DHCP
        uint32_t dhcp_leasetime;
index 931c24d97694463be245fed920d6125af57d0f75..7982ccffc7ee9afc42f64cdfb6e6188ba7378344 100644 (file)
@@ -342,13 +342,8 @@ static int calc_adv_interval(struct interface *iface, uint32_t lowest_found_life
 
        *maxival = iface->ra_maxinterval;
 
-       /* 
-        * rfc4861#section-6.2.1 : AdvDefaultLifetime Default: 3 * MaxRtrAdvInterval
-        * therefore max interval shall be no greater than 1/3 of the lowest valid
-        * lease time of all known prefixes.
-        */
-       if (*maxival > lowest_found_lifetime/3)
-               *maxival = lowest_found_lifetime/3;
+       if (*maxival > lowest_found_lifetime)
+               *maxival = lowest_found_lifetime;
 
        if (*maxival > MaxRtrAdvInterval)
                *maxival = MaxRtrAdvInterval;
@@ -376,16 +371,17 @@ static int calc_adv_interval(struct interface *iface, uint32_t lowest_found_life
 
 static uint32_t calc_ra_lifetime(struct interface *iface, uint32_t maxival)
 {
-       uint32_t lifetime = 3*maxival;
+       uint32_t lifetime = iface->max_preferred_lifetime;
 
        if (iface->ra_lifetime >= 0) {
                lifetime = iface->ra_lifetime;
-               if (lifetime > 0 && lifetime < maxival)
-                       lifetime = maxival;
-               else if (lifetime > 9000)
-                       lifetime = 9000;
        }
 
+       if (lifetime > 0 && lifetime < maxival)
+               lifetime = maxival;
+       else if (lifetime > 9000)
+               lifetime = 9000;
+
        return lifetime;
 }
 
@@ -470,10 +466,10 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
        size_t valid_addr_cnt = 0, invalid_addr_cnt = 0;
        /* 
         * lowest_found_lifetime stores the lowest lifetime of all prefixes;
-        * necessary to find shortest adv interval necessary
+        * necessary to find longest adv interval necessary
         * for shortest lived prefix
         */
-       uint32_t lowest_found_lifetime = UINT32_MAX, maxival, lifetime;
+       uint32_t lowest_found_lifetime = UINT32_MAX, highest_found_lifetime = 0, maxival, ra_lifetime;
        int msecs, mtu = iface->ra_mtu, hlim = iface->ra_hoplimit;
        bool default_route = false;
        bool valid_prefix = false;
@@ -611,17 +607,16 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                if (addr->preferred_lt > (uint32_t)now) {
                        preferred_lt = TIME_LEFT(addr->preferred_lt, now);
 
-                       if (preferred_lt > iface->preferred_lifetime) {
-                               /* set to possibly user mandated preferred_lt */
-                               preferred_lt = iface->preferred_lifetime;
+                       if (iface->max_preferred_lifetime && preferred_lt > iface->max_preferred_lifetime) {
+                               preferred_lt = iface->max_preferred_lifetime;
                        }
                }
 
                if (addr->valid_lt > (uint32_t)now) {
                        valid_lt = TIME_LEFT(addr->valid_lt, now);
 
-                       if (iface->ra_useleasetime && valid_lt > iface->dhcp_leasetime)
-                               valid_lt = iface->dhcp_leasetime;
+                       if (iface->max_valid_lifetime && valid_lt > iface->max_valid_lifetime)
+                               valid_lt = iface->max_valid_lifetime;
                }
 
                if (preferred_lt > valid_lt) {
@@ -639,6 +634,11 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                if ((!IN6_IS_ADDR_ULA(&addr->addr.in6) || iface->default_router) && valid_lt)
                        valid_prefix = true;
 
+               if (!IN6_IS_ADDR_ULA(&addr->addr.in6) && valid_lt) {
+                       if (highest_found_lifetime < valid_lt)
+                               highest_found_lifetime = valid_lt;
+               }
+
                odhcpd_bmemcpy(&p->nd_opt_pi_prefix, &addr->addr.in6,
                                (iface->ra_advrouter) ? 128 : addr->prefix);
                p->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION;
@@ -660,7 +660,9 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
 
        /* Calculate periodic transmit */
        msecs = calc_adv_interval(iface, lowest_found_lifetime, &maxival);
-       lifetime = calc_ra_lifetime(iface, maxival);
+       ra_lifetime = calc_ra_lifetime(iface, maxival);
+       if (!highest_found_lifetime)
+               highest_found_lifetime = ra_lifetime;
 
        if (!iface->have_link_local) {
                syslog(LOG_NOTICE, "Skip sending a RA on %s as no link local address is available", iface->name);
@@ -668,15 +670,15 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
        }
 
        if (default_route && valid_prefix) {
-               adv.h.nd_ra_router_lifetime = htons(lifetime < UINT16_MAX ? lifetime : UINT16_MAX);
+               adv.h.nd_ra_router_lifetime = htons(ra_lifetime < UINT16_MAX ? ra_lifetime : UINT16_MAX);
        } else {
                adv.h.nd_ra_router_lifetime = 0;
 
                if (default_route) {
                        syslog(LOG_WARNING, "A default route is present but there is no public prefix "
-                                               "on %s thus we announce no default route by overriding ra_lifetime to 0!", iface->name);
+                                               "on %s thus we announce no default route by setting ra_lifetime to 0!", iface->name);
                } else {
-                       syslog(LOG_WARNING, "No default route present, overriding ra_lifetime to 0!");
+                       syslog(LOG_WARNING, "No default route present, setting ra_lifetime to 0!");
                }
        }
 
@@ -705,7 +707,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                        memset(dns, 0, dns_sz);
                        dns->type = ND_OPT_RECURSIVE_DNS;
                        dns->len = 1 + (2 * dns_cnt);
-                       dns->lifetime = htonl(lifetime);
+                       dns->lifetime = htonl(highest_found_lifetime);
                        memcpy(dns->addr, dns_addr, sizeof(struct in6_addr)*dns_cnt);
                }
 
@@ -728,7 +730,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                        memset(search, 0, search_sz);
                        search->type = ND_OPT_DNS_SEARCH;
                        search->len = search_len ? ((sizeof(*search) + search_padded) / 8) : 0;
-                       search->lifetime = htonl(lifetime);
+                       search->lifetime = htonl(highest_found_lifetime);
                        memcpy(search->name, search_domain, search_len);
                        memset(&search->name[search_len], 0, search_padded - search_len);
                }
@@ -741,7 +743,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
 
        if (iface->pref64_length) {
                /* RFC 8781 § 4.1 rounding up lifetime to multiple of 8 */
-               uint16_t pref64_lifetime = lifetime < (UINT16_MAX - 7) ? lifetime + 7 : UINT16_MAX;
+               uint16_t pref64_lifetime = ra_lifetime < (UINT16_MAX - 7) ? ra_lifetime + 7 : (UINT16_MAX - 7);
 
                pref64_sz = sizeof(*pref64);
                pref64 = alloca(pref64_sz);
@@ -783,7 +785,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                        if (iface->dnr[i].lifetime_set)
                                dnr->lifetime = htonl(iface->dnr[i].lifetime);
                        else
-                               dnr->lifetime = htonl(lifetime);
+                               dnr->lifetime = htonl(highest_found_lifetime);
 
                        dnr->adn_len = htons(iface->dnr[i].adn_len);
                        memcpy(tmp, iface->dnr[i].adn, iface->dnr[i].adn_len);
@@ -858,7 +860,9 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr
                        routes[routes_cnt].flags |= ND_RA_PREF_HIGH;
 
                valid_lt = TIME_LEFT(addr->valid_lt, now);
-               routes[routes_cnt].lifetime = htonl(valid_lt < lifetime ? valid_lt : lifetime);
+               if (iface->max_valid_lifetime && valid_lt > iface->max_valid_lifetime)
+                       valid_lt = iface->max_valid_lifetime;
+               routes[routes_cnt].lifetime = htonl(valid_lt);
                routes[routes_cnt].addr[0] = addr->addr.in6.s6_addr32[0];
                routes[routes_cnt].addr[1] = addr->addr.in6.s6_addr32[1];
                routes[routes_cnt].addr[2] = 0;