Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 16 Apr 2026 19:38:00 +0000
From:      Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: c8d25804f56a - main - nd6: Add support for route information (RFC 4191)
Message-ID:  <69e13a98.19c1c.5140b765@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by pouria:

URL: https://cgit.FreeBSD.org/src/commit/?id=c8d25804f56ae7c35eaa492b6110807a2675d41a

commit c8d25804f56ae7c35eaa492b6110807a2675d41a
Author:     Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
AuthorDate: 2026-04-16 14:23:49 +0000
Commit:     Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
CommitDate: 2026-04-16 18:57:07 +0000

    nd6: Add support for route information (RFC 4191)
    
    Implement RFC 4191 by handling received Router Adverisement (RA)
    packets with route information option.
    For default routes, use the route information's lifetime and
    preference to overwrite the RA's lifetime/preference.
    Also install and update more-specific route prefixes with the
    option's lifetime and expire them when their lifetime elapses.
    
    PR:             263982
    Reviewed by:    markj
    Tested by:      Marek Zarychta <zarychtam@plan-b.pwste.edu.pl>
    Relnotes:       yes
    Differential Revision: https://reviews.freebsd.org/D55449
---
 sys/net/route/nhop_ctl.c |   1 +
 sys/netinet/icmp6.h      |   2 +
 sys/netinet6/nd6.c       |   8 ++
 sys/netinet6/nd6.h       |  14 +-
 sys/netinet6/nd6_rtr.c   | 337 ++++++++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 360 insertions(+), 2 deletions(-)

diff --git a/sys/net/route/nhop_ctl.c b/sys/net/route/nhop_ctl.c
index 52e7b0fefcd2..596ec9e25d1a 100644
--- a/sys/net/route/nhop_ctl.c
+++ b/sys/net/route/nhop_ctl.c
@@ -451,6 +451,7 @@ nhop_create_from_nhop(struct rib_head *rnh, const struct nhop_object *nh_orig,
 		nhop_free(nh);
 		return (error);
 	}
+	set_nhop_expire_from_info(nh, info);
 
 	*pnh = nhop_get_nhop(nh, &error);
 
diff --git a/sys/netinet/icmp6.h b/sys/netinet/icmp6.h
index 9508f221ba10..77145e4c7503 100644
--- a/sys/netinet/icmp6.h
+++ b/sys/netinet/icmp6.h
@@ -356,6 +356,8 @@ struct nd_opt_route_info {	/* route info */
 	/* prefix follows */
 } __packed;
 
+#define ND_OPT_RTI_FLAG_PRF_MASK	0x18	/* 00011000 */
+
 struct nd_opt_rdnss {		/* RDNSS option (RFC 6106) */
 	u_int8_t	nd_opt_rdnss_type;
 	u_int8_t	nd_opt_rdnss_len;
diff --git a/sys/netinet6/nd6.c b/sys/netinet6/nd6.c
index 48e80bb75e0b..c531d11f9e62 100644
--- a/sys/netinet6/nd6.c
+++ b/sys/netinet6/nd6.c
@@ -513,6 +513,14 @@ nd6_options(union nd_opts *ndopts)
 			ndopts->nd_opts_pi_end =
 				(struct nd_opt_prefix_info *)nd_opt;
 			break;
+		case ND_OPT_ROUTE_INFO:
+			if (ndopts->nd_opt_array[nd_opt->nd_opt_type] == 0) {
+				ndopts->nd_opt_array[nd_opt->nd_opt_type]
+					= nd_opt;
+			}
+			ndopts->nd_opts_rti_end =
+				(struct nd_opt_route_info *)nd_opt;
+			break;
 		/* What about ND_OPT_ROUTE_INFO? RFC 4191 */
 		case ND_OPT_RDNSS:	/* RFC 6106 */
 		case ND_OPT_DNSSL:	/* RFC 6106 */
diff --git a/sys/netinet6/nd6.h b/sys/netinet6/nd6.h
index 8c8d5a3236ca..8dc27aef5e12 100644
--- a/sys/netinet6/nd6.h
+++ b/sys/netinet6/nd6.h
@@ -287,7 +287,7 @@ VNET_DECLARE(int, ip6_temp_regen_advance); /* seconds */
 #define	V_ip6_temp_regen_advance	VNET(ip6_temp_regen_advance)
 
 union nd_opts {
-	struct nd_opt_hdr *nd_opt_array[16];	/* max = ND_OPT_NONCE */
+	struct nd_opt_hdr *nd_opt_array[24];	/* max = ND_OPT_ROUTE_INFO */
 	struct {
 		struct nd_opt_hdr *zero;
 		struct nd_opt_hdr *src_lladdr;
@@ -305,10 +305,20 @@ union nd_opts {
 		struct nd_opt_hdr *__res13;
 		struct nd_opt_nonce *nonce;
 		struct nd_opt_hdr *__res15;
+		struct nd_opt_hdr *__res16;
+		struct nd_opt_hdr *__res17;
+		struct nd_opt_hdr *__res18;
+		struct nd_opt_hdr *__res19;
+		struct nd_opt_hdr *__res20;
+		struct nd_opt_hdr *__res21;
+		struct nd_opt_hdr *__res22;
+		struct nd_opt_hdr *__res23;
+		struct nd_opt_route_info *rti_beg;
 		struct nd_opt_hdr *search;	/* multiple opts */
 		struct nd_opt_hdr *last;	/* multiple opts */
 		int done;
 		struct nd_opt_prefix_info *pi_end;/* multiple opts, end */
+		struct nd_opt_route_info *rti_end;/* multiple opts, end */
 	} nd_opt_each;
 };
 #define nd_opts_src_lladdr	nd_opt_each.src_lladdr
@@ -318,6 +328,8 @@ union nd_opts {
 #define nd_opts_rh		nd_opt_each.rh
 #define nd_opts_mtu		nd_opt_each.mtu
 #define nd_opts_nonce		nd_opt_each.nonce
+#define nd_opts_rti		nd_opt_each.rti_beg
+#define nd_opts_rti_end		nd_opt_each.rti_end
 #define nd_opts_search		nd_opt_each.search
 #define nd_opts_last		nd_opt_each.last
 #define nd_opts_done		nd_opt_each.done
diff --git a/sys/netinet6/nd6_rtr.c b/sys/netinet6/nd6_rtr.c
index 5819370a3011..623bfd60918e 100644
--- a/sys/netinet6/nd6_rtr.c
+++ b/sys/netinet6/nd6_rtr.c
@@ -60,6 +60,7 @@
 #include <net/route.h>
 #include <net/route/nhop.h>
 #include <net/route/route_ctl.h>
+#include <net/route/route_var.h>
 #include <net/radix.h>
 #include <net/vnet.h>
 
@@ -77,9 +78,20 @@
 
 MALLOC_DEFINE(M_IP6NDP, "ip6ndp", "IPv6 Neighbor Discovery");
 
+struct nd_routectl {
+	struct ifnet		*ndrt_ifp;	/* nexthop interface */
+	struct sockaddr_in6	ndrt_gateway;	/* gateway address */
+	struct sockaddr_in6	ndrt_prefix;	/* route prefix */
+	struct sockaddr_in6	ndrt_mask;	/* route prefix mask */
+	uint8_t			ndrt_plen;	/* route prefix len */
+	uint8_t			ndrt_flags;	/* route info flags */
+	uint32_t		ndrt_lifetime;	/* route info lifetime (sec) */
+};
+
 static struct nd_defrouter *defrtrlist_update(struct nd_defrouter *);
 static int prelist_update(struct nd_prefixctl *, struct nd_defrouter *,
     bool, int);
+static int nd6_routelist_update(struct nd_routectl *);
 static int nd6_prefix_onlink(struct nd_prefix *);
 static int in6_get_tmp_ifid(struct in6_aliasreq *);
 
@@ -304,6 +316,116 @@ nd6_ra_opt_pi(struct nd_opt_hdr *pt, struct ifnet *ifp,
 	(void)prelist_update(&pr, dr, auth, mcast);
 }
 
+static void
+nd6_ra_opt_rti(struct nd_opt_hdr *hdr, struct ifnet *ifp,
+    struct in6_addr saddr6, struct nd_defrouter *dr)
+{
+	struct nd_opt_route_info *rti = NULL;
+	struct nd_routectl rt;
+	struct in6_addr prefix, netmask;
+	char ip6bufs[INET6_ADDRSTRLEN];
+	int olen;
+	uint8_t	pref;
+
+	if (hdr->nd_opt_type != ND_OPT_ROUTE_INFO)
+		return;
+
+	/* RFC 4191 section 2.3, Field validation */
+	rti = (struct nd_opt_route_info *)hdr;
+	if (rti->nd_opt_rti_len == 0 || rti->nd_opt_rti_len > 3) {
+		nd6log((LOG_INFO,
+		    "%s: invalid option len %d for route "
+		    "information option, ignored\n", __func__,
+		    rti->nd_opt_rti_len));
+		return;
+	}
+	if (rti->nd_opt_rti_prefixlen > 128 ||
+	    (rti->nd_opt_rti_prefixlen > 64 && rti->nd_opt_rti_len != 3) ||
+	    (rti->nd_opt_rti_prefixlen > 0 && rti->nd_opt_rti_len < 2)) {
+		nd6log((LOG_INFO,
+		    "%s: invalid prefix len %d with option len %d for route "
+		    "information option, ignored\n", __func__,
+		    rti->nd_opt_rti_prefixlen, rti->nd_opt_rti_len));
+		return;
+	}
+	pref = rti->nd_opt_rti_flags & ND_OPT_RTI_FLAG_PRF_MASK;
+	if ((pref & ND_RA_FLAG_RTPREF_RSV) != 0) {
+		nd6log((LOG_INFO,
+		    "%s: reserved preference %d for route "
+		    "information option, ignored\n", __func__,
+		    pref));
+		return;
+	}
+
+	/*
+	 * RFC 4191 section 2.3: The bits in the prefix after
+	 * the prefix length (if any) are reserved and MUST be
+	 * initialized to zero by the sender and ignored by the receiver.
+	 */
+	memset(&prefix, 0, sizeof(prefix));
+	/*
+	 * Calculate the variable length of the option and copy the remaining
+	 * length into the prefix.
+	 * The resulting value cannot exceed the size of struct in6_addr
+	 * since the option length is limited to 3 (24 bytes).
+	 */
+	olen = (rti->nd_opt_rti_len << 3) - sizeof(struct nd_opt_route_info);
+	memcpy(&prefix, (char *)rti + sizeof(struct nd_opt_route_info), olen);
+	in6_prefixlen2mask(&netmask, rti->nd_opt_rti_prefixlen);
+	IN6_MASK_ADDR(&prefix, &netmask);
+	if (IN6_IS_ADDR_MULTICAST(&prefix) || IN6_IS_ADDR_LINKLOCAL(&prefix)) {
+		nd6log((LOG_INFO, "%s: invalid prefix %s, ignored\n",
+		    __func__, ip6_sprintf(ip6bufs, &prefix)));
+		return;
+	}
+
+	/*
+	 * RFC 4191 section 3.1: The Router Preference and Lifetime
+	 * values in a ::/0 Route Information Option override the
+	 * preference and lifetime values in the Router Advertisement header.
+	 */
+	if (rti->nd_opt_rti_prefixlen == 0 && dr != NULL) {
+		/*
+		 * We may disable routes from RA messages when
+		 * ND6_IFF_NO_RADR enabled on the receiving interface or
+		 * (ip6.forwarding == 1 && ip6.rfc6204w3 != 1).
+		 * Therefore, don't overwrite it.
+		 */
+		if (dr->rtlifetime == 0)
+			return;
+		/*
+		 * XXXPO: ignore rti lifetime bigger than uint16_max until
+		 * we update the dr structure size to uint32_t.
+		 */
+		if (ntohl(rti->nd_opt_rti_lifetime) > UINT16_MAX)
+			return;
+		dr->rtlifetime = ntohl(rti->nd_opt_rti_lifetime);
+		dr->expire = time_uptime + dr->rtlifetime;
+		dr->raflags &= ~ND_OPT_RTI_FLAG_PRF_MASK;
+		dr->raflags |= pref;
+		nd6log((LOG_INFO,
+		    "%s: override default route with route information option\n",
+		    __func__));
+		return;
+	}
+
+	memset(&rt, 0, sizeof(rt));
+	rt.ndrt_ifp = ifp;
+	rt.ndrt_prefix.sin6_len = sizeof(struct sockaddr_in6);
+	rt.ndrt_prefix.sin6_family = AF_INET6;
+	rt.ndrt_prefix.sin6_addr = prefix;
+	rt.ndrt_gateway.sin6_len = sizeof(struct sockaddr_in6);
+	rt.ndrt_gateway.sin6_family = AF_INET6;
+	rt.ndrt_gateway.sin6_addr = saddr6;
+	rt.ndrt_mask.sin6_len = sizeof(struct sockaddr_in6);
+	rt.ndrt_mask.sin6_family = AF_INET6;
+	rt.ndrt_mask.sin6_addr = netmask;
+	rt.ndrt_plen = rti->nd_opt_rti_prefixlen;
+	rt.ndrt_flags = rti->nd_opt_rti_flags;
+	rt.ndrt_lifetime = ntohl(rti->nd_opt_rti_lifetime);
+	(void)nd6_routelist_update(&rt);
+}
+
 static void
 nd6_ra_opt_mtu(struct nd_opt_mtu *optmtu, struct ifnet *ifp,
     struct in6_addr saddr6)
@@ -385,7 +507,7 @@ nd6_ra_input(struct mbuf *m, int off, int icmp6len)
 	struct in6_ifextra *ndi;
 	struct ip6_hdr *ip6;
 	struct nd_router_advert *nd_ra;
-	struct nd_opt_hdr *pt;
+	struct nd_opt_hdr *pt, *rti;
 	struct in6_addr saddr6;
 	struct nd_defrouter dr0, *dr;
 	union nd_opts ndopts;
@@ -493,6 +615,15 @@ nd6_ra_input(struct mbuf *m, int off, int icmp6len)
 			    nd_ra->nd_ra_curhoplimit);
 		}
 	}
+	/* Route Information */
+	if (ndopts.nd_opts_rti != NULL) {
+		for (rti = (struct nd_opt_hdr *)ndopts.nd_opts_rti;
+		     rti <= (struct nd_opt_hdr *)ndopts.nd_opts_rti_end;
+		     rti = (struct nd_opt_hdr *)((char *)rti +
+						(rti->nd_opt_len << 3))) {
+			nd6_ra_opt_rti(rti, ifp, saddr6, &dr0);
+		}
+	}
 	dr = defrtrlist_update(&dr0);
 	/* Prefix Information */
 	if (ndopts.nd_opts_pi != NULL) {
@@ -2167,6 +2298,210 @@ restart:
 	return (error);
 }
 
+/*
+ * Install advertised route via default router.
+ */
+static int
+nd6_route_rtrequest(struct nd_routectl *key)
+{
+	struct rib_cmd_info rc;
+	struct rib_head *rnh;
+	struct nhop_object *nh;
+	struct ifaddr *ifa;
+	struct ifnet *ifp;
+	struct sockaddr *dst, *gw, *mask;
+	int flags, error;
+	time_t expire;
+
+	NET_EPOCH_ASSERT();
+
+	ifp = key->ndrt_ifp;
+	dst = (struct sockaddr *)&key->ndrt_prefix;
+	gw = (struct sockaddr *)&key->ndrt_gateway;
+	mask = (struct sockaddr *)&key->ndrt_mask;
+	flags = RTF_DYNAMIC | RTF_GATEWAY;
+	if (key->ndrt_plen == 128) {
+		flags |= RTF_HOST;
+		mask = NULL;
+	}
+
+	rnh = rt_tables_get_rnh_safe(key->ndrt_ifp->if_fib, AF_INET6);
+	if (rnh == NULL)
+		return (EINVAL);
+
+	/* Get the best ifa for the given interface and gateway. */
+	if ((ifa = ifaof_ifpforaddr(gw, ifp)) == NULL)
+		return (ENETUNREACH);
+
+	expire = time_second + key->ndrt_lifetime;
+	struct rt_metrics rmx = {
+		.rmx_expire = expire,
+	};
+	struct rt_addrinfo info = {
+		.rti_flags = flags,
+		.rti_info = {
+			[RTAX_DST] = dst,
+			[RTAX_GATEWAY] = gw,
+			[RTAX_NETMASK] = mask,
+			[RTAX_AUTHOR] = gw,
+		},
+		.rti_ifa = ifa,
+		.rti_ifp = ifp,
+		.rti_mflags = RTV_EXPIRE,
+		.rti_rmx = &rmx,
+	};
+	/* XXX: route preference */
+	struct route_nhop_data rnd = {
+		.rnd_weight = RT_DEFAULT_WEIGHT,
+	};
+
+	error = nhop_create_from_info(rnh, &info, &nh);
+	if (error == 0) {
+		nhop_set_origin(nh, NH_ORIGIN_REDIRECT);
+		rnd.rnd_nhop = nh;
+		error = rib_add_route_px(ifp->if_fib, dst, key->ndrt_plen, &rnd,
+		    RTM_F_CREATE, &rc);
+	}
+	if (error != 0)
+		return (error);
+	RTSTAT_INC(rts_dynamic);
+
+	/* Send notification of a route addition to userland. */
+	rt_missmsg_fib(RTM_REDIRECT, &info, flags | RTF_UP, error, ifp->if_fib);
+
+	return (error);
+}
+
+/*
+ * Update lifetime of advertised route.
+ */
+static int
+nd6_route_rtupdate(struct nd_routectl *key)
+{
+	struct rib_cmd_info rc;
+	time_t expire;
+
+	expire = time_second + key->ndrt_lifetime;
+	struct rt_metrics rmx = {
+		.rmx_expire = expire,
+	};
+	struct rt_addrinfo info = {
+		.rti_info[RTAX_DST] = (struct sockaddr *)&key->ndrt_prefix,
+		.rti_mflags = RTV_EXPIRE,
+		.rti_rmx = &rmx,
+	};
+	if (key->ndrt_plen != 128)
+		info.rti_info[RTAX_NETMASK] = (struct sockaddr *)&key->ndrt_mask;
+	return (rib_change_route(key->ndrt_ifp->if_fib, &info, &rc));
+}
+
+/*
+ * Delete advertised route.
+ */
+static int
+nd6_route_rtdelete(struct nd_routectl *key)
+{
+	struct sockaddr *dst, *gw;
+	struct rib_cmd_info rc;
+	struct nhop_object *nh;
+	struct ifnet *ifp;
+	int error;
+
+	ifp = key->ndrt_ifp;
+	dst = (struct sockaddr *)&key->ndrt_prefix;
+	gw = (struct sockaddr *)&key->ndrt_gateway;
+	error = rib_del_route_px(ifp->if_fib, dst, key->ndrt_plen,
+	    rib_match_gw, gw, 0, &rc);
+	if (error == 0) {
+		nh = nhop_select_func(rc.rc_nh_old, 0);
+		rt_routemsg(RTM_DELETE, rc.rc_rt, nh, ifp->if_fib);
+	}
+
+	return (error);
+}
+
+/*
+ * Lookup for exact match of advertised route with gateway
+ * as its nexthop address.
+ */
+static bool
+nd6_route_rtlookup(struct nd_routectl *key)
+{
+	RIB_RLOCK_TRACKER;
+	struct rib_head *rnh;
+	struct route_nhop_data rnd;
+	struct sockaddr *dst, *gw;
+
+	dst = (struct sockaddr *)&key->ndrt_prefix;
+	gw = (struct sockaddr *)&key->ndrt_gateway;
+	rnh = rt_tables_get_rnh_safe(key->ndrt_ifp->if_fib, AF_INET6);
+	if (rnh == NULL)
+		return (false);
+
+	RIB_RLOCK(rnh);
+	rib_lookup_prefix_plen(rnh, dst, key->ndrt_plen, &rnd);
+	RIB_RUNLOCK(rnh);
+	if (rnd.rnd_nhop == NULL)
+		return (false);
+
+	return (match_nhop_gw(rnd.rnd_nhop, gw));
+}
+
+static int
+nd6_routelist_update(struct nd_routectl *new)
+{
+	char ip6buf[INET6_ADDRSTRLEN];
+	int error = 0;
+
+	NET_EPOCH_ASSERT();
+
+	/*
+	 * RFC 4191 section 3.1: If the received route's
+	 * lifetime is zero, the route is removed from the Routing
+	 * Table if present.
+	 */
+	if (new->ndrt_lifetime == 0) {
+		error = nd6_route_rtdelete(new);
+		if (error != 0) {
+			nd6log((LOG_DEBUG,
+			    "%s: failed to delete the route %s/%d on %s (errno=%d)\n",
+			    __func__, ip6_sprintf(ip6buf, &new->ndrt_prefix.sin6_addr),
+			    new->ndrt_plen, if_name(new->ndrt_ifp), error));
+		}
+		return (error);
+	}
+
+	if (nd6_route_rtlookup(new)) {
+		/*
+		 * RFC 4191 section 3.1: the route's lifetime and
+		 * preference is updated if the route is already present.
+		 * XXX: preference on routing table? (ndrt_flags)
+		 */
+		error = nd6_route_rtupdate(new);
+		if (error != 0) {
+			nd6log((LOG_DEBUG,
+			    "%s: failed to update route lifetime of "
+			    "%s/%d from RA on %s (errno=%d)\n",
+			    __func__, ip6_sprintf(ip6buf, &new->ndrt_prefix.sin6_addr),
+			    new->ndrt_plen, if_name(new->ndrt_ifp), error));
+		}
+	} else {
+		/*
+		 * RFC 4191 section 3.1: If a route's lifetime is non-zero,
+		 * the route is added to the Routing Table if not present.
+		 */
+		error = nd6_route_rtrequest(new);
+		if (error != 0) {
+			nd6log((LOG_NOTICE,
+			    "%s: failed to add route %s/%d from RA on %s (errno=%d)\n",
+			    __func__, ip6_sprintf(ip6buf, &new->ndrt_prefix.sin6_addr),
+			    new->ndrt_plen, if_name(new->ndrt_ifp), error));
+		}
+	}
+
+	return (error);
+}
+
 /*
  * Get a randomized interface identifier for a temporary address
  * Based on RFC 8981, Section 3.3.1.


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?69e13a98.19c1c.5140b765>