changeset 4530:999c0c21711d draft

RA: expire RDNSS and DNSSL entries This allows us to remember N fully expired RA's which works around an obscure issue where a received RA has no lifetime or any prefixes with lifetimes but does have an instruction to start DHCP6. It was harmless but filled the log with spam and now there is no log spam!
author Roy Marples <roy@marples.name>
date Fri, 14 Jun 2019 13:53:51 +0100
parents edec0822a9d7
children a44c7ef76f2d
files hooks/20-resolv.conf src/ipv6.c src/ipv6nd.c
diffstat 3 files changed, 128 insertions(+), 62 deletions(-) [+]
line wrap: on
line diff
--- a/hooks/20-resolv.conf	Wed Jun 12 19:44:16 2019 +0100
+++ b/hooks/20-resolv.conf	Fri Jun 14 13:53:51 2019 +0100
@@ -69,30 +69,26 @@
 }
 
 # Extract any ND DNS options from the RA
-# For now, we ignore the lifetime of the DNS options unless they
-# are absent or zero.
-# In this case they are removed from consideration.
-# See draft-gont-6man-slaac-dns-config-issues-01 for issues
-# regarding DNS option lifetime in ND messages.
+# Obey the lifetimes
 eval_nd_dns()
 {
-	eval ltime=\$nd${i}_rdnss${j}_lifetime
-	if [ -z "$ltime" ] || [ "$ltime" = 0 ]; then
-		rdnss=
-	else
+
+	eval rdnsstime=\$nd${i}_rdnss${j}_lifetime
+	[ -z "$rdnsstime" ] && return 1
+	ltime=$(($rdnsstime - $offset))
+	if [ "$ltime" -gt 0 ]; then
 		eval rdnss=\$nd${i}_rdnss${j}_servers
-	fi
-	eval ltime=\$nd${i}_dnssl${j}_lifetime
-	if [ -z "$ltime" ] || [ "$ltime" = 0 ]; then
-		dnssl=
-	else
-		eval dnssl=\$nd${i}_dnssl${j}_search
+		[ -n "$rdnss" ] && new_rdnss="$new_rdnss${new_rdnss:+ }$rdnss"
 	fi
 
-	[ -z "${rdnss}${dnssl}" ] && return 1
+	eval dnssltime=\$nd${i}_dnssl${j}_lifetime
+	[ -z "$dnssltime" ] && return 1
+	ltime=$(($dnssltime - $offset))
+	if [ "$ltime" -gt 0 ]; then
+		eval dnssl=\$nd${i}_dnssl${j}_search
+		[ -n "$dnssl" ] && new_dnssl="$new_dnssl${new_dnssl:+ }$dnssl"
+	fi
 
-	[ -n "$rdnss" ] && new_rdnss="$new_rdnss${new_rdnss:+ }$rdnss"
-	[ -n "$dnssl" ] && new_dnssl="$new_dnssl${new_dnssl:+ }$dnssl"
 	j=$(($j + 1))
 	return 0
 }
@@ -106,12 +102,16 @@
 	i=1
 	j=1
 	while true; do
+		eval acquired=\$nd${i}_acquired
+		[ -z "$acquired" ] && break
+		eval now=\$nd${i}_now
+		[ -z "$now" ] && break
+		offset=$(($now - $acquired))
 		while true; do
 			eval_nd_dns || break
 		done
 		i=$(($i + 1))
 		j=1
-		eval_nd_dns || break
 	done
 	[ -n "$new_rdnss" ] && \
 	    new_domain_name_servers="$new_domain_name_servers${new_domain_name_servers:+ }$new_rdnss"
--- a/src/ipv6.c	Wed Jun 12 19:44:16 2019 +0100
+++ b/src/ipv6.c	Fri Jun 14 13:53:51 2019 +0100
@@ -2240,15 +2240,14 @@
 }
 
 static int
-inet6_raroutes(struct rt_head *routes, struct dhcpcd_ctx *ctx, int expired,
-    bool *have_default)
+inet6_raroutes(struct rt_head *routes, struct dhcpcd_ctx *ctx)
 {
 	struct rt *rt;
 	struct ra *rap;
 	const struct ipv6_addr *addr;
 
 	TAILQ_FOREACH(rap, ctx->ra_routers, next) {
-		if (rap->expired != expired)
+		if (rap->expired)
 			continue;
 		TAILQ_FOREACH(addr, &rap->addrs, next) {
 			if (addr->prefix_vltime == 0)
@@ -2259,15 +2258,13 @@
 				TAILQ_INSERT_TAIL(routes, rt, rt_next);
 			}
 		}
-		if (rap->lifetime) {
-			rt = inet6_makerouter(rap);
-			if (rt) {
-				rt->rt_dflags |= RTDF_RA;
-				TAILQ_INSERT_TAIL(routes, rt, rt_next);
-				if (have_default)
-					*have_default = true;
-			}
-		}
+		if (rap->lifetime == 0)
+			continue;
+		rt = inet6_makerouter(rap);
+		if (rt == NULL)
+			continue;
+		rt->rt_dflags |= RTDF_RA;
+		TAILQ_INSERT_TAIL(routes, rt, rt_next);
 	}
 	return 0;
 }
@@ -2301,15 +2298,13 @@
 bool
 inet6_getroutes(struct dhcpcd_ctx *ctx, struct rt_head *routes)
 {
-	bool have_default;
 
 	/* Should static take priority? */
 	if (inet6_staticroutes(routes, ctx) == -1)
 		return false;
 
 	/* First add reachable routers and their prefixes */
-	have_default = false;
-	if (inet6_raroutes(routes, ctx, 0, &have_default) == -1)
+	if (inet6_raroutes(routes, ctx) == -1)
 		return false;
 
 #ifdef DHCP6
@@ -2322,21 +2317,5 @@
 		return false;
 #endif
 
-#ifdef HAVE_ROUTE_METRIC
-	/* If we have an unreachable router, we really do need to remove the
-	 * route to it beause it could be a lower metric than a reachable
-	 * router. Of course, we should at least have some routers if all
-	 * are unreachable. */
-	if (!have_default) {
-#endif
-	/* Add our non-reachable routers and prefixes
-	 * Unsure if this is needed, but it's a close match to kernel
-	 * behaviour */
-		if (inet6_raroutes(routes, ctx, 1, NULL) == -1)
-			return false;
-#ifdef HAVE_ROUTE_METRIC
-	}
-#endif
-
 	return true;
 }
--- a/src/ipv6nd.c	Wed Jun 12 19:44:16 2019 +0100
+++ b/src/ipv6nd.c	Fri Jun 14 13:53:51 2019 +0100
@@ -105,6 +105,9 @@
 #define RTPREF_RESERVED	(-2)
 #define RTPREF_INVALID	(-3)	/* internal */
 
+#define	EXPIRED_MAX	5	/* Remember 5 expired routers to avoid
+				   logspam. */
+
 #define MIN_RANDOM_FACTOR	500				/* millisecs */
 #define MAX_RANDOM_FACTOR	1500				/* millisecs */
 #define MIN_RANDOM_FACTOR_U	MIN_RANDOM_FACTOR * 1000	/* usecs */
@@ -1228,6 +1231,10 @@
 			break;
 
 		case ND_OPT_MTU:
+			if (len < sizeof(mtu)) {
+				logerrx("%s: short MTU option", ifp->name);
+				break;
+			}
 			memcpy(&mtu, p, sizeof(mtu));
 			mtu.nd_opt_mtu_mtu = ntohl(mtu.nd_opt_mtu_mtu);
 			if (mtu.nd_opt_mtu_mtu < IPV6_MMTU) {
@@ -1239,6 +1246,10 @@
 			break;
 
 		case ND_OPT_RDNSS:
+			if (len < sizeof(rdnss)) {
+				logerrx("%s: short RDNSS option", ifp->name);
+				break;
+			}
 			memcpy(&rdnss, p, sizeof(rdnss));
 			if (rdnss.nd_opt_rdnss_lifetime &&
 			    rdnss.nd_opt_rdnss_len > 1)
@@ -1480,11 +1491,12 @@
 		 * from the prefix information options as well. */
 		j = 0;
 		TAILQ_FOREACH(ia, &rap->addrs, next) {
-			if (!(ia->flags & IPV6_AF_AUTOCONF)
+			if (!(ia->flags & IPV6_AF_AUTOCONF) ||
 #ifdef IPV6_AF_TEMPORARY
-			    || ia->flags & IPV6_AF_TEMPORARY
+			    ia->flags & IPV6_AF_TEMPORARY ||
 #endif
-			    )
+			    !(ia->flags & IPV6_AF_ADDED) ||
+			    ia->prefix_vltime == 0)
 				continue;
 			j++;
 			if (env) {
@@ -1522,6 +1534,16 @@
 	struct timespec now, lt, expire, next;
 	bool expired, valid, validone;
 	struct ipv6_addr *ia;
+	size_t len, olen;
+	uint8_t *p;
+	struct nd_opt_hdr ndo;
+#if 0
+	struct nd_opt_prefix_info pi;
+#endif
+	struct nd_opt_dnssl dnssl;
+	struct nd_opt_rdnss rdnss;
+	uint32_t ltime;
+	size_t nexpired = 0;
 
 	ifp = arg;
 	clock_gettime(CLOCK_MONOTONIC, &now);
@@ -1536,8 +1558,7 @@
 			lt.tv_sec = (time_t)rap->lifetime;
 			lt.tv_nsec = 0;
 			timespecadd(&rap->acquired, &lt, &expire);
-			if (rap->lifetime == 0 || timespeccmp(&now, &expire, >))
-			{
+			if (timespeccmp(&now, &expire, >)) {
 				if (!rap->expired) {
 					logwarnx("%s: %s: router expired",
 					    ifp->name, rap->sfrom);
@@ -1588,14 +1609,79 @@
 			}
 		}
 
-		/* XXX FixMe!
-		 * We need to extract the lifetime from each option and check
-		 * if that has expired or not.
-		 * If it has, zero the option out in the returned data. */
+		/* Work out expiry for ND options */
+		len = rap->data_len - sizeof(struct nd_router_advert);
+		for (p = rap->data + sizeof(struct nd_router_advert);
+		    len >= sizeof(ndo);
+		    p += olen, len -= olen)
+		{
+			memcpy(&ndo, p, sizeof(ndo));
+			olen = (size_t)(ndo.nd_opt_len * 8);
+			if (olen > len) {
+				errno =	EINVAL;
+				break;
+			}
+
+			if (has_option_mask(rap->iface->options->nomasknd,
+			    ndo.nd_opt_type))
+				continue;
 
-		/* No valid lifetimes are left on the RA, so we might
-		 * as well punt it. */
-		if (!valid && !validone)
+			switch (ndo.nd_opt_type) {
+			/* Prefix info is already checked in the above loop. */
+#if 0
+			case ND_OPT_PREFIX_INFORMATION:
+				if (len < sizeof(pi))
+					break;
+				memcpy(&pi, p, sizeof(pi));
+				ltime = pi.nd_opt_pi_valid_time;
+				break;
+#endif
+			case ND_OPT_DNSSL:
+				if (len < sizeof(dnssl))
+					break;
+				memcpy(&dnssl, p, sizeof(dnssl));
+				ltime = dnssl.nd_opt_dnssl_lifetime;
+				break;
+			case ND_OPT_RDNSS:
+				if (len < sizeof(rdnss))
+					break;
+				memcpy(&rdnss, p, sizeof(rdnss));
+				ltime = rdnss.nd_opt_rdnss_lifetime;
+				break;
+			default:
+				continue;
+			}
+
+			if (ltime == 0)
+				continue;
+			if (ltime == ND6_INFINITE_LIFETIME) {
+				validone = true;
+				continue;
+			}
+
+			lt.tv_sec = (time_t)ntohl(ltime);
+			lt.tv_nsec = 0;
+			timespecadd(&rap->acquired, &lt, &expire);
+			if (timespeccmp(&now, &expire, >)) {
+				expired = true;
+				continue;
+			}
+
+			timespecsub(&expire, &now, &lt);
+			if (!timespecisset(&next) ||
+			    timespeccmp(&next, &lt, >))
+			{
+				next = lt;
+				validone = true;
+			}
+		}
+
+		if (valid || validone)
+			continue;
+
+		/* Router has expired. Let's not keep a lot of them.
+		 * We should work out if all the options have expired .... */
+		if (++nexpired > EXPIRED_MAX)
 			ipv6nd_free_ra(rap);
 	}
 
@@ -1603,6 +1689,7 @@
 		eloop_timeout_add_tv(ifp->ctx->eloop,
 		    &next, ipv6nd_expirera, ifp);
 	if (expired) {
+		logwarnx("%s: part of Router Advertisement expired", ifp->name);
 		rt_build(ifp->ctx, AF_INET6);
 		script_runreason(ifp, "ROUTERADVERT");
 	}