From 726ca5340823e2d5f3b5e7b6ad83e52ba3be14a9 Mon Sep 17 00:00:00 2001
From: Aviana Cruz <gwencroft@proton.me>
Date: Sat, 16 Sep 2023 15:04:12 +0000
Subject: [PATCH] odhcpd: improve RFC 9096 compliance

and allow configuring upper limit for preferred and valid lifetime.

Signed-off-by: Aviana Cruz <gwencroft@proton.me>
---
 README          | 12 ++++++------
 src/config.c    | 35 +++++++++++++++++++++-------------
 src/dhcpv6-ia.c | 50 ++++++++++++++++++++++++++++++-------------------
 src/odhcpd.h    |  8 ++++++--
 src/router.c    | 17 ++++++++---------
 5 files changed, 73 insertions(+), 49 deletions(-)

--- a/README
+++ b/README
@@ -116,7 +116,9 @@ domain			list	<local search domain>	Sear
 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
@@ -131,11 +133,9 @@ ra_maxinterval		integer	600			Maximum ti
 							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
--- a/src/config.c
+++ b/src/config.c
@@ -79,7 +79,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,
@@ -91,7 +90,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
 };
@@ -134,7 +134,6 @@ static const struct blobmsg_policy iface
 	[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 },
@@ -144,7 +143,8 @@ static const struct blobmsg_policy iface
 	[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 },
 };
 
@@ -215,7 +215,8 @@ static void set_interface_defaults(struc
 	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;
@@ -647,15 +648,26 @@ int config_parse_interface(void *data, s
 
 	}
 
-	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])) {
@@ -979,9 +991,6 @@ int config_parse_interface(void *data, s
 	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);
 
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -120,7 +120,7 @@ static inline bool valid_prefix_length(c
 
 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)
@@ -1037,17 +1037,27 @@ static size_t build_ia(uint8_t *buf, siz
 	}
 
 	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;
@@ -1071,15 +1081,20 @@ static size_t build_ia(uint8_t *buf, siz
 			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;
 
@@ -1133,24 +1148,24 @@ static size_t build_ia(uint8_t *buf, siz
 
 			/* 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);
--- a/src/odhcpd.h
+++ b/src/odhcpd.h
@@ -37,6 +37,10 @@
 // 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
+
 #define INFINITE_VALID(x) ((x) == 0)
 
 #define _unused __attribute__((unused))
@@ -302,7 +306,6 @@ struct interface {
 	bool ra_slaac;
 	bool ra_not_onlink;
 	bool ra_advrouter;
-	bool ra_useleasetime;
 	bool ra_dns;
 	uint8_t pref64_length;
 	struct in6_addr pref64_addr;
@@ -319,7 +322,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;
--- a/src/router.c
+++ b/src/router.c
@@ -376,7 +376,7 @@ static int calc_adv_interval(struct inte
 
 static uint32_t calc_ra_lifetime(struct interface *iface, uint32_t maxival)
 {
-	uint32_t lifetime = 3*maxival;
+	uint32_t lifetime = maxival * 3;
 
 	if (iface->ra_lifetime >= 0) {
 		lifetime = iface->ra_lifetime;
@@ -600,17 +600,16 @@ static int send_router_advert(struct int
 		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) {
@@ -663,9 +662,9 @@ static int send_router_advert(struct int
 
 		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!");
 		}
 	}
 
@@ -730,7 +729,7 @@ static int send_router_advert(struct int
 
 	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 = lifetime < (UINT16_MAX - 7) ? lifetime + 7 : (UINT16_MAX - 7);
 		uint8_t prefix_length_code;
 		uint32_t mask_a1, mask_a2;
 
