Netdev List
 help / color / mirror / Atom feed
* [PATCH net-next 0/9] RFC 6724 rule 5.5 support
@ 2026-07-14  9:40 David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 1/9] net/ipv6: fix lookup for ::/0 (non-)subtree route David 'equinox' Lamparter
                   ` (8 more replies)
  0 siblings, 9 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest

Hi all,


this patchset implements RFC6724 rule 5.5.  For the unaquainted:

   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
   If SA or SA's prefix is assigned by the selected next-hop that will
   be used to send to D and SB or SB's prefix is assigned by a different
   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
   assigned by the next-hop that will be used to send to D and SA or
   SA's prefix is assigned by a different next-hop, then prefer SB.

The way this is done is through IPv6 subtree routes.  If a router
advertises some prefix in its RA/PIOs, source specific subtree routes
should be created for the default route (and RIOs) installed as a result
of processing that RA.

This may initially sound like a weird way to do it, but for one RFC8028
requires the subtree routes anyway, and also I did try the more obvious
approaches (explicitly tracking it, putting it on the address, putting
it on the neighbor entry) and all of them break in some scenarios.

I've put together a selftest, there's also a rather hacky test suite
created for an IETF hackathon: https://github.com/eqvinox/rule5p5-tests
(it's not specific to this patchset.)  I've also been dogfooding these
patches on my personal devices for more than a year.

Rule 5.5 itself has extensive history at the IETF, including changing
from optional to mandatory in the recent 6724 update.  It is immensely
useful (really: required) to make multihoming, renumbering and failover
work.

@Jakub you had previously asked me to resubmit the "prep" patches since
it was at a poor time (cf. Fri, Jul 25, 2025 at 05:39:58PM -0700).
(I had tried submitting the preparation bits on its own.)

@Paolo you had looked at the lookup fix:
On Tue, Nov 11, 2025 at 11:13:30AM +0100, Paolo Abeni wrote:
> The patch LGTM, and I agree this should go via net-next, given that it's
> really a corner case and I could miss nasty side-effects.
>
> It looks like you have some testing scenario handy: it would be great to
> include it as a paired self-test; could you please add it?

Cheers,


equi (David)


P.S.: I also happen to be around at netdevconf in Rome, in case anyone
happens to see this and have questions.  Of course being at a conference
generally means not looking at random patch mails, so this is mostly
just in case you see the Subject lines or this cover letter.  Apologies,
it wasn't possible for me to submit this ahead of the conference.


diffstat:
 Documentation/networking/ipv6-addrsel.rst                |  75 ++++++++++++++++++++++++++++++++
 MAINTAINERS                                              |   1 +
 include/net/addrconf.h                                   |   4 ++
 include/net/ip6_route.h                                  |  26 ------------
 net/ipv6/Kconfig                                         |  18 ++++++--
 net/ipv6/addrconf.c                                      | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 net/ipv6/ip6_fib.c                                       |   5 ++-
 net/ipv6/ip6_output.c                                    |  26 +++++++++---
 net/ipv6/route.c                                         |  16 +++++--
 tools/testing/selftests/net/Makefile                     |   1 +
 tools/testing/selftests/net/config                       |   1 +
 tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 497 insertions(+), 51 deletions(-)


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH net-next 1/9] net/ipv6: fix lookup for ::/0 (non-)subtree route
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 2/9] net/ipv6: flatten ip6_route_get_saddr David 'equinox' Lamparter
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

Assume a scenario with something like the following routes:
default via fe80::1 dev dummy0
2001:db8:1::/48 via fe80::10 dev dummy0
2001:db8:1::/48 from 2001:db8:1:2::/64 via fe80::12 dev dummy0

Now if a lookup happens for 2001:db8:1::2345, but with a source address
*not* covered by the third route, the expectation is to hit the second
one.  Unfortunately, this was broken since the code, on failing the
lookup in the subtree, didn't consider the node itself which the subtree
is attached to, i.e. route #2 above.

The fix is simple, check if the subtree is attached to a node that is
itself a valid route before backtracking to less specific destination
prefixes.

This case is somewhat rare for several reasons.  To begin with, subtree
routes are most commonly attached to the default destination.
Additionally, in the rare cases where a non-default destination prefix
is host to subtree routes, the fallback on not hitting any subtree route
is commonly a default route (or a subtree route on that).

(Note that this was working for the "::/0 from ::/0" case since the root
node is special-cased.  The issue was discovered during RFC 6724 rule
5.5 testing, trying to find edge cases.)

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
Cc: Paolo Abeni <pabeni@redhat.com>
Cc: Lorenzo Colitti <lorenzo@google.com>
Cc: Patrick Rohr <prohr@google.com>
Cc: Maciej Żenczykowski <maze@google.com>
---
 net/ipv6/ip6_fib.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index a130cdfaebfb..273f2bfc5286 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -1656,8 +1656,11 @@ static struct fib6_node *fib6_node_lookup_1(struct fib6_node *root,
 					struct fib6_node *sfn;
 					sfn = fib6_node_lookup_1(subtree,
 								 args + 1);
-					if (!sfn)
+					if (!sfn) {
+						if (fn->fn_flags & RTN_RTINFO)
+							return fn;
 						goto backtrack;
+					}
 					fn = sfn;
 				}
 #endif
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 2/9] net/ipv6: flatten ip6_route_get_saddr
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 1/9] net/ipv6: fix lookup for ::/0 (non-)subtree route David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 3/9] net/ipv6: create ipv6_fl_get_saddr David 'equinox' Lamparter
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

Inline ip6_route_get_saddr()'s functionality in rt6_fill_node(), to
prepare for replacing the former with a dst based function.

NB: the l3mdev handling introduced by 252442f2ae31 "ipv6: fix source
address selection with route leak" is dropped here - the l3mdev ifindex
was a constant 0 on this call site, so that code was in fact dead.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv6/route.c | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index a1301334da48..704afe2ddea7 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -5850,9 +5850,19 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
 			if (nla_put_u32(skb, RTA_IIF, iif))
 				goto nla_put_failure;
 	} else if (dest) {
-		struct in6_addr saddr_buf;
-		if (ip6_route_get_saddr(net, rt, dest, 0, 0, &saddr_buf) == 0 &&
-		    nla_put_in6_addr(skb, RTA_PREFSRC, &saddr_buf))
+		struct in6_addr saddr_buf, *saddr = NULL;
+
+		if (rt->fib6_prefsrc.plen) {
+			saddr = &rt->fib6_prefsrc.addr;
+		} else {
+			struct net_device *dev = fib6_info_nh_dev(rt);
+
+			if (ipv6_dev_get_saddr(net, dev, dest, 0,
+					       &saddr_buf) == 0)
+				saddr = &saddr_buf;
+		}
+
+		if (saddr && nla_put_in6_addr(skb, RTA_PREFSRC, saddr))
 			goto nla_put_failure;
 	}
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 3/9] net/ipv6: create ipv6_fl_get_saddr
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 1/9] net/ipv6: fix lookup for ::/0 (non-)subtree route David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 2/9] net/ipv6: flatten ip6_route_get_saddr David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 4/9] net/ipv6: use ipv6_fl_get_saddr in output David 'equinox' Lamparter
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

This adds passing the relevant flow information as well as selected
nexthop into the source address selection code, to allow the RFC6724
rule 5.5 code to look at its details.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 include/net/addrconf.h |  4 ++++
 net/ipv6/addrconf.c    | 45 +++++++++++++++++++++++++++++++-----------
 2 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/include/net/addrconf.h b/include/net/addrconf.h
index 8ced27a8229b..7bc0711b8717 100644
--- a/include/net/addrconf.h
+++ b/include/net/addrconf.h
@@ -142,6 +142,10 @@ struct inet6_ifaddr *ipv6_get_ifaddr(struct net *net,
 int ipv6_dev_get_saddr(struct net *net, const struct net_device *dev,
 		       const struct in6_addr *daddr, unsigned int srcprefs,
 		       struct in6_addr *saddr);
+int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst,
+		      const struct net_device *dst_dev,
+		      const struct sock *sk, unsigned int srcprefs,
+		      struct flowi6 *fl6);
 int ipv6_get_lladdr(struct net_device *dev, struct in6_addr *addr,
 		    u32 banned_flags);
 bool inet_rcv_saddr_equal(const struct sock *sk, const struct sock *sk2,
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index f1fe9ede1edb..0ba46ed518a9 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1542,7 +1542,9 @@ struct ipv6_saddr_score {
 };
 
 struct ipv6_saddr_dst {
-	const struct in6_addr *addr;
+	const struct flowi6 *fl6;
+	const struct dst_entry *dst;
+	const struct sock *sk;
 	int ifindex;
 	int scope;
 	int label;
@@ -1619,7 +1621,7 @@ static int ipv6_get_saddr_eval(struct net *net,
 		break;
 	case IPV6_SADDR_RULE_LOCAL:
 		/* Rule 1: Prefer same address */
-		ret = ipv6_addr_equal(&score->ifa->addr, dst->addr);
+		ret = ipv6_addr_equal(&score->ifa->addr, &dst->fl6->daddr);
 		break;
 	case IPV6_SADDR_RULE_SCOPE:
 		/* Rule 2: Prefer appropriate scope
@@ -1697,11 +1699,11 @@ static int ipv6_get_saddr_eval(struct net *net,
 		 *	    non-ORCHID vs non-ORCHID
 		 */
 		ret = !(ipv6_addr_orchid(&score->ifa->addr) ^
-			ipv6_addr_orchid(dst->addr));
+			ipv6_addr_orchid(&dst->fl6->daddr));
 		break;
 	case IPV6_SADDR_RULE_PREFIX:
 		/* Rule 8: Use longest matching prefix */
-		ret = ipv6_addr_diff(&score->ifa->addr, dst->addr);
+		ret = ipv6_addr_diff(&score->ifa->addr, &dst->fl6->daddr);
 		if (ret > score->ifa->prefix_len)
 			ret = score->ifa->prefix_len;
 		score->matchlen = ret;
@@ -1819,9 +1821,10 @@ static int ipv6_get_saddr_master(struct net *net,
 	return hiscore_idx;
 }
 
-int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
-		       const struct in6_addr *daddr, unsigned int prefs,
-		       struct in6_addr *saddr)
+int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst_entry,
+		      const struct net_device *dst_dev,
+		      const struct sock *sk, unsigned int prefs,
+		      struct flowi6 *fl6)
 {
 	struct ipv6_saddr_score scores[2], *hiscore;
 	struct ipv6_saddr_dst dst;
@@ -1832,11 +1835,13 @@ int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
 	int hiscore_idx = 0;
 	int ret = 0;
 
-	dst_type = __ipv6_addr_type(daddr);
-	dst.addr = daddr;
+	dst_type = __ipv6_addr_type(&fl6->daddr);
+	dst.fl6 = fl6;
+	dst.sk = sk;
+	dst.dst = dst_entry;
 	dst.ifindex = dst_dev ? dst_dev->ifindex : 0;
 	dst.scope = __ipv6_addr_src_scope(dst_type);
-	dst.label = ipv6_addr_label(net, daddr, dst_type, dst.ifindex);
+	dst.label = ipv6_addr_label(net, &fl6->daddr, dst_type, dst.ifindex);
 	dst.prefs = prefs;
 
 	scores[hiscore_idx].rule = -1;
@@ -1911,11 +1916,29 @@ int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
 	if (!hiscore->ifa)
 		ret = -EADDRNOTAVAIL;
 	else
-		*saddr = hiscore->ifa->addr;
+		fl6->saddr = hiscore->ifa->addr;
 
 	rcu_read_unlock();
 	return ret;
 }
+EXPORT_SYMBOL(ipv6_fl_get_saddr);
+
+int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
+		       const struct in6_addr *daddr, unsigned int prefs,
+		       struct in6_addr *saddr)
+{
+	struct flowi6 fl6;
+	int ret;
+
+	memset(&fl6, 0, sizeof(fl6));
+	fl6.daddr = *daddr;
+
+	ret = ipv6_fl_get_saddr(net, NULL, dst_dev, NULL, prefs, &fl6);
+	if (!ret)
+		*saddr = fl6.saddr;
+
+	return ret;
+}
 EXPORT_SYMBOL(ipv6_dev_get_saddr);
 
 static int __ipv6_get_lladdr(struct inet6_dev *idev, struct in6_addr *addr,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 4/9] net/ipv6: use ipv6_fl_get_saddr in output
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
                   ` (2 preceding siblings ...)
  2026-07-14  9:40 ` [PATCH net-next 3/9] net/ipv6: create ipv6_fl_get_saddr David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 5/9] net/ipv6: drop ip6_route_get_saddr David 'equinox' Lamparter
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

Flatten ip6_route_get_saddr() into ip6_dst_lookup_tail (which really
just means handling fib6_prefsrc), and then replace ipv6_dev_get_saddr
with ipv6_fl_get_saddr to pass down the flow information.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv6/ip6_output.c | 25 +++++++++++++++++++------
 1 file changed, 19 insertions(+), 6 deletions(-)

diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
index 368e4fa3b43c..03b8d42d9df9 100644
--- a/net/ipv6/ip6_output.c
+++ b/net/ipv6/ip6_output.c
@@ -1143,27 +1143,40 @@ static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
 	int flags = 0;
 
 	/* The correct way to handle this would be to do
-	 * ip6_route_get_saddr, and then ip6_route_output; however,
+	 * ipv6_fl_get_saddr, and then ip6_route_output; however,
 	 * the route-specific preferred source forces the
-	 * ip6_route_output call _before_ ip6_route_get_saddr.
+	 * ip6_route_output call _before_ ipv6_fl_get_saddr.
 	 *
 	 * In source specific routing (no src=any default route),
 	 * ip6_route_output will fail given src=any saddr, though, so
 	 * that's why we try it again later.
 	 */
 	if (ipv6_addr_any(&fl6->saddr)) {
+		struct net_device *l3mdev;
+		struct net_device *dev;
 		struct fib6_info *from;
 		struct rt6_info *rt;
+		bool same_vrf;
 
 		*dst = ip6_route_output(net, sk, fl6);
 		rt = (*dst)->error ? NULL : dst_rt6_info(*dst);
 
 		rcu_read_lock();
 		from = rt ? rcu_dereference(rt->from) : NULL;
-		err = ip6_route_get_saddr(net, from, &fl6->daddr,
-					  sk ? READ_ONCE(inet6_sk(sk)->srcprefs) : 0,
-					  fl6->flowi6_l3mdev,
-					  &fl6->saddr);
+
+		l3mdev = dev_get_by_index_rcu(net, fl6->flowi6_l3mdev);
+		if (!from || !from->fib6_prefsrc.plen || l3mdev)
+			dev = from ? fib6_info_nh_dev(from) : NULL;
+		same_vrf = !l3mdev || l3mdev_master_dev_rcu(dev) == l3mdev;
+		if (from && from->fib6_prefsrc.plen && same_vrf) {
+			fl6->saddr = from->fib6_prefsrc.addr;
+			err = 0;
+		} else
+			err = ipv6_fl_get_saddr(net, *dst,
+						same_vrf ? dev : l3mdev, sk,
+						sk ? READ_ONCE(inet6_sk(sk)->srcprefs) : 0,
+						fl6);
+
 		rcu_read_unlock();
 
 		if (err)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 5/9] net/ipv6: drop ip6_route_get_saddr
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
                   ` (3 preceding siblings ...)
  2026-07-14  9:40 ` [PATCH net-next 4/9] net/ipv6: use ipv6_fl_get_saddr in output David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 6/9] net/ipv6: flip IPV6_SUBTREES default to Y David 'equinox' Lamparter
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

It's no longer used anywhere.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 include/net/ip6_route.h | 26 --------------------------
 1 file changed, 26 deletions(-)

diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h
index 09ffe0f13ce7..fe13cb1d3257 100644
--- a/include/net/ip6_route.h
+++ b/include/net/ip6_route.h
@@ -140,32 +140,6 @@ void rt6_flush_exceptions(struct fib6_info *f6i);
 void rt6_age_exceptions(struct fib6_info *f6i, struct fib6_gc_args *gc_args,
 			unsigned long now);
 
-static inline int ip6_route_get_saddr(struct net *net, struct fib6_info *f6i,
-				      const struct in6_addr *daddr,
-				      unsigned int prefs, int l3mdev_index,
-				      struct in6_addr *saddr)
-{
-	struct net_device *l3mdev;
-	struct net_device *dev;
-	bool same_vrf;
-	int err = 0;
-
-	rcu_read_lock();
-
-	l3mdev = dev_get_by_index_rcu(net, l3mdev_index);
-	if (!f6i || !f6i->fib6_prefsrc.plen || l3mdev)
-		dev = f6i ? fib6_info_nh_dev(f6i) : NULL;
-	same_vrf = !l3mdev || l3mdev_master_dev_rcu(dev) == l3mdev;
-	if (f6i && f6i->fib6_prefsrc.plen && same_vrf)
-		*saddr = f6i->fib6_prefsrc.addr;
-	else
-		err = ipv6_dev_get_saddr(net, same_vrf ? dev : l3mdev, daddr, prefs, saddr);
-
-	rcu_read_unlock();
-
-	return err;
-}
-
 struct rt6_info *rt6_lookup(struct net *net, const struct in6_addr *daddr,
 			    const struct in6_addr *saddr, int oif,
 			    const struct sk_buff *skb, int flags);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 6/9] net/ipv6: flip IPV6_SUBTREES default to Y
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
                   ` (4 preceding siblings ...)
  2026-07-14  9:40 ` [PATCH net-next 5/9] net/ipv6: drop ip6_route_get_saddr David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 7/9] net/ipv6: support RFC6724 rule 5.5 via subtrees David 'equinox' Lamparter
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

It's needed for RFC6724 rule 5.5, which as it turns out is more
important than was initially discernible.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 net/ipv6/Kconfig | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/net/ipv6/Kconfig b/net/ipv6/Kconfig
index c3806c6ac96f..39659e9b53a3 100644
--- a/net/ipv6/Kconfig
+++ b/net/ipv6/Kconfig
@@ -243,16 +243,26 @@ config IPV6_MULTIPLE_TABLES
 config IPV6_SUBTREES
 	bool "IPv6: source address based routing"
 	depends on IPV6_MULTIPLE_TABLES
+	default y
 	help
 	  Enable routing by source address or prefix.
 
 	  The destination address is still the primary routing key, so mixing
 	  normal and source prefix specific routes in the same routing table
-	  may sometimes lead to unintended routing behavior.  This can be
-	  avoided by defining different routing tables for the normal and
-	  source prefix specific routes.
+	  may sometimes lead to unintended routing behavior if the order of
+	  lookups is misunderstood.  To achieve full separation based on source
+	  address, different routing tables should be defined for each source
+	  prefix.
 
-	  If unsure, say N.
+	  Subtrees specifically address scenarios where local connectivity
+	  between source prefixes is shared and unrestricted, but some
+	  destinations (e.g. the default route) have more fine-grained rules.
+
+	  This feature is required to correctly support RFC6724 rule 5.5 in
+	  source address selection.  Rule 5.5 is a necessity for good
+	  multihoming, renumbering and source prefix liveliness checks.
+
+	  If unsure, say Y.
 
 config IPV6_MROUTE
 	bool "IPv6: multicast routing"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 7/9] net/ipv6: support RFC6724 rule 5.5 via subtrees
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
                   ` (5 preceding siblings ...)
  2026-07-14  9:40 ` [PATCH net-next 6/9] net/ipv6: flip IPV6_SUBTREES default to Y David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 8/9] selftests: net: RFC6724 rule 5.5 tests David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation David 'equinox' Lamparter
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter,
	Jen Linkova

RFC6724 says:
   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
   If SA or SA's prefix is assigned by the selected next-hop that will
   be used to send to D and SB or SB's prefix is assigned by a different
   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
   assigned by the next-hop that will be used to send to D and SA or
   SA's prefix is assigned by a different next-hop, then prefer SB.

As it turns out, this behavior is immensely useful in handling
renumbering, multihoming, and source address liveliness checks.

That said, just implementing this as an one-shot in source address
selection is not particularly useful if output routing then meanders
between multiple nexthops.  To actually make this work (and work well),
installing source-specific routes for prefixes seen in PIOs is a good
way to go.

To actually select a source address, there's still the route lookup with
an empty source as before, which will select some particular nexthop.
Now, for the various candidate source addresses, this does another
lookup and checks if we got the same nexthop, and prefers those source
addresses - rule 5.5 behavior.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
Cc: Lorenzo Colitti <lorenzo@google.com>
Cc: Patrick Rohr <prohr@google.com>
Cc: Maciej Żenczykowski <maze@google.com>
Cc: Jen Linkova <furry13@gmail.com>
---
 net/ipv6/addrconf.c | 101 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 100 insertions(+), 1 deletion(-)

diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index 0ba46ed518a9..f5c04928aa14 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1522,6 +1522,7 @@ enum {
 	IPV6_SADDR_RULE_HOA,
 #endif
 	IPV6_SADDR_RULE_OIF,
+	IPV6_SADDR_RULE_PIO_BY_NEXTHOP,
 	IPV6_SADDR_RULE_LABEL,
 	IPV6_SADDR_RULE_PRIVACY,
 	IPV6_SADDR_RULE_ORCHID,
@@ -1542,13 +1543,14 @@ struct ipv6_saddr_score {
 };
 
 struct ipv6_saddr_dst {
-	const struct flowi6 *fl6;
+	struct flowi6 *fl6;
 	const struct dst_entry *dst;
 	const struct sock *sk;
 	int ifindex;
 	int scope;
 	int label;
 	unsigned int prefs;
+	struct net *net;
 };
 
 static inline int ipv6_saddr_preferred(int type)
@@ -1593,6 +1595,80 @@ static bool ipv6_allow_optimistic_dad(const struct net *net,
 #endif
 }
 
+/* "source address is preferable if the chosen nexthop advertised it as a PIO"
+ * => consider 'advertised as a PIO' to be 'the routes for the source's subtree
+ *    include one with the same nexthop'
+ *
+ * NB: there is no backtracking in the subtree here, this is intentional -
+ * each prefix seen (and accepted) in PIOs creates essentially a "zone" which
+ * is our search scope.
+ */
+static int ipv6_saddr_rule5p5(struct ipv6_saddr_score *score,
+			      struct ipv6_saddr_dst *saddr_dst)
+{
+	const struct rt6_info *rt, *cmp_rt;
+	struct dst_entry *cmp_dst;
+	struct fib6_info *f6i;
+	int ret = 0;
+
+	rt = container_of(saddr_dst->dst, struct rt6_info, dst);
+
+	/* fl6->saddr is ::, cf. check at the top of ipv6_common_get_saddr() */
+	saddr_dst->fl6->saddr = score->ifa->addr;
+	cmp_dst = ip6_route_output(saddr_dst->net, saddr_dst->sk,
+				   saddr_dst->fl6);
+	memset(&saddr_dst->fl6->saddr, 0, sizeof(saddr_dst->fl6->saddr));
+
+	if (cmp_dst->error)
+		goto out_release_dst;
+
+	cmp_rt = container_of(cmp_dst, struct rt6_info, dst);
+
+	/* this must work if _any_ nexthop matches; the non-subtree best may
+	 * not be in same order as subtree best
+	 */
+	for (f6i = rcu_dereference(cmp_rt->from); f6i;
+	     f6i = rcu_dereference(f6i->fib6_next)) {
+		struct fib6_nh *f6n = f6i->fib6_nh;
+		struct fib6_info *sibling;
+
+		/* non-subtree route: says nothing about router advertising this source */
+		if (f6i->fib6_src.plen == 0)
+			continue;
+
+		if (f6n->nh_common.nhc_dev != saddr_dst->dst->dev ||
+		    f6n->nh_common.nhc_gw_family != AF_INET6)
+			continue;
+
+		if (ipv6_addr_equal(&f6n->nh_common.nhc_gw.ipv6,
+				    &rt->rt6i_gateway)) {
+			ret = 1;
+			goto out_release_dst;
+		}
+
+		if (!f6i->fib6_nsiblings)
+			continue;
+
+		list_for_each_entry(sibling, &f6i->fib6_siblings, fib6_siblings) {
+			f6n = sibling->fib6_nh;
+
+			if (f6n->nh_common.nhc_dev != saddr_dst->dst->dev ||
+			    f6n->nh_common.nhc_gw_family != AF_INET6)
+				continue;
+
+			if (ipv6_addr_equal(&f6n->nh_common.nhc_gw.ipv6,
+					    &rt->rt6i_gateway)) {
+				ret = 1;
+				goto out_release_dst;
+			}
+		}
+	}
+
+out_release_dst:
+	dst_release(cmp_dst);
+	return ret;
+}
+
 static int ipv6_get_saddr_eval(struct net *net,
 			       struct ipv6_saddr_score *score,
 			       struct ipv6_saddr_dst *dst,
@@ -1677,6 +1753,24 @@ static int ipv6_get_saddr_eval(struct net *net,
 		ret = (!dst->ifindex ||
 		       dst->ifindex == score->ifa->idev->dev->ifindex);
 		break;
+	case IPV6_SADDR_RULE_PIO_BY_NEXTHOP:
+		/* Rule 5.5: Prefer sources advertised by chosen next-hop */
+
+		/* Without subtrees, the source address will make no difference
+		 * in the ip6_route_output call in rule5p5.  Therefore the rule
+		 * 5.5 check becomes useless.  This wouldn't result in any
+		 * errors, but ip6_route_output isn't free, so if subtrees are
+		 * disabled save some cycles by skipping this entirely.
+		 *
+		 * This is done through subtrees_enabled to have the code
+		 * compiled regardless.
+		 */
+		if (fib6_routes_require_src(net) && dst->dst
+		    && !dst->dst->error)
+			ret = ipv6_saddr_rule5p5(score, dst);
+		else
+			ret = 1;
+		break;
 	case IPV6_SADDR_RULE_LABEL:
 		/* Rule 6: Prefer matching label */
 		ret = ipv6_addr_label(net,
@@ -1835,7 +1929,12 @@ int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst_entry,
 	int hiscore_idx = 0;
 	int ret = 0;
 
+	/* we should never end up here with a non-empty saddr. */
+	if (WARN_ON_ONCE(!ipv6_addr_any(&fl6->saddr)))
+		return 0;
+
 	dst_type = __ipv6_addr_type(&fl6->daddr);
+	dst.net = net;
 	dst.fl6 = fl6;
 	dst.sk = sk;
 	dst.dst = dst_entry;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 8/9] selftests: net: RFC6724 rule 5.5 tests
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
                   ` (6 preceding siblings ...)
  2026-07-14  9:40 ` [PATCH net-next 7/9] net/ipv6: support RFC6724 rule 5.5 via subtrees David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14  9:40 ` [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation David 'equinox' Lamparter
  8 siblings, 0 replies; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

This throws a couple of situations at the IPv6 source address selection
logic, primarily to check that RFC6724 rule 5.5 is applied as expected.

This requires `CONFIG_IPV6_SUBTREES=y`, so that's added to
selftests/net/config.  (The patchset also changes the default on that,
but arguably better to be explicit.)

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 tools/testing/selftests/net/Makefile          |   1 +
 tools/testing/selftests/net/config            |   1 +
 .../net/ipv6_saddr_rfc6724rule5p5.py          | 231 ++++++++++++++++++
 3 files changed, 233 insertions(+)
 create mode 100644 tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py

diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 708d960ae07d..c07d2aebad69 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -53,6 +53,7 @@ TEST_PROGS := \
 	ipv6_flowlabel.sh \
 	ipv6_force_forwarding.sh \
 	ipv6_route_update_soft_lockup.sh \
+	ipv6_saddr_rfc6724rule5p5.py \
 	ipvtap_test.sh \
 	l2_tos_ttl_inherit.sh \
 	l2tp.sh \
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index e1ce35c2abbe..b0b66412b9f3 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -48,6 +48,7 @@ CONFIG_IPV6_ROUTER_PREF=y
 CONFIG_IPV6_RPL_LWTUNNEL=y
 CONFIG_IPV6_SEG6_LWTUNNEL=y
 CONFIG_IPV6_SIT=y
+CONFIG_IPV6_SUBTREES=y
 CONFIG_IPV6_VTI=y
 CONFIG_IPVLAN=m
 CONFIG_IPVTAP=m
diff --git a/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
new file mode 100644
index 000000000000..682b39ba991d
--- /dev/null
+++ b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2023-2026  David 'equinox' Lamparter
+"""
+RFC 6724 (IPv6 source address selection) rule 5.5 tests
+
+For reference:
+
+   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
+   If SA or SA's prefix is assigned by the selected next-hop that will
+   be used to send to D and SB or SB's prefix is assigned by a different
+   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
+   assigned by the next-hop that will be used to send to D and SA or
+   SA's prefix is assigned by a different next-hop, then prefer SB.
+
+(and since it provides the "counterpoint":)
+
+   Rule 8: Use longest matching prefix.
+   If CommonPrefixLen(SA, D) > CommonPrefixLen(SB, D), then prefer SA.
+   Similarly, if CommonPrefixLen(SB, D) > CommonPrefixLen(SA, D), then
+   prefer SB.
+
+Note rule 5.5 was originally optional but made mandatory by
+draft-ietf-6man-rfc6724-update (which at the point of creation of this test
+was already "done" at the IETF but waiting in the RFC editor queue due to a
+blocking dependency.)
+"""
+
+from socket import socket, AF_INET6, SOCK_DGRAM
+from functools import wraps
+from typing import Callable
+
+from lib.py import ksft_run, ksft_exit, ksft_eq
+from lib.py import NetNS, NetNSEnter
+from lib.py import ip
+
+
+def select_addr(dest):
+    """
+    connect() + getsockname() to figure out what was selected as source address
+    """
+    sock = socket(AF_INET6, SOCK_DGRAM, 0)
+    sock.connect((dest, 12345))
+    return sock.getsockname()[0]
+
+
+def in_netns(func: Callable[[], None]) -> Callable[[], None]:
+    """
+    python decorator to put test function in netns
+    """
+
+    @wraps(func)
+    def wrapped() -> None:
+        with NetNS() as testns:
+            with NetNSEnter(str(testns)):
+                func()
+
+    return wrapped
+
+
+@in_netns
+def test_basic() -> None:
+    """
+    Simple & most common case for RFC6724 rule 5.5: multiple default routes
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    ip("-6 route del default via fe80::1 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_nh_obj() -> None:
+    """
+    Same as above, but with nexthop objects for the default route
+
+    NB: The kernel doesn't currently allow nexthop objects for subtree routes.
+    """
+
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+
+    # distinct nexthop objects are used, because what matters is the nexthop
+    # itself, not the nexthop object.  To cover everything, make a group.
+    ip("nexthop add id 101 via fe80::1 dev veth0")
+    ip("nexthop add id 201 group 101")
+    ip("nexthop add id 102 via fe80::2 dev veth0")
+    ip("nexthop add id 202 group 102")
+
+    ip("-6 route add default nhid 201 metric 100")
+    ip("-6 route add default nhid 202 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    ip("-6 route del default nhid 201 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_low_metric() -> None:
+    """
+    Check that subtree routes take effect even if they are higher metric
+
+    For checking that "router advertised prefix", metric is irrelevant.  It
+    matters for the initial unspecific lookup to find a nexthop to begin with.
+    (The later source address check lookup doesn't change the nexthop, i.e.
+    the effects of metrics are already done.)
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64   dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0 metric 1000")
+    ip("-6 route add default from 2001:db8:1000::/48 via fe80::3 dev veth0 metric 50")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+    ip("-6 route del default via fe80::1 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_no_subtree() -> None:
+    """
+    Ensure that matching on a non-subtree route doesn't trigger rule 5.5
+
+    (This was non-obviously broken in earlier versions of the implementations,
+    a non-subtree route would still match.  Make sure it doesn't break again.)
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default via fe80::1 dev veth0 metric 100")
+    ip("-6 route add default via fe80::2 dev veth0 metric 200")
+    ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(
+        select_addr("2001:db8:1001::"),
+        "2001:db8:10::1",
+        "rule 5.5 > rule 8, ignoring non-SADR",
+    )
+
+
+@in_netns
+def test_longer() -> None:
+    """
+    Check functionality for non-default destination.
+
+    This is expected to be very rare in actual practice, and doesn't do
+    backtracking (also refer to kernel docs.)
+    """
+    ip("link add type veth")
+    ip("link set veth0 up")
+    ip("link set veth1 up")
+    ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+    ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+    ip("-6 route add default           via fe80::1 dev veth0 metric 100")
+    ip("-6 route add 2001:db8:500::/48 via fe80::2 dev veth0 metric 100")
+    ip("-6 route add default           from 2001:db8:10::/48   via fe80::1 dev veth0")
+    ip("-6 route add 2001:db8:500::/48 from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:500::"), "2001:db8:1000::1", "rule 5.5")
+    ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:1000::1", "rule 5.5")
+
+    ip("-6 route add 2001:db8:500:aaa::/64 via fe80::2 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:500::"), "2001:db8:1000::1", "rule 5.5")
+    ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:10::1", "no backtracking")
+
+    ip("-6 route del 2001:db8:500::/48 from 2001:db8:1000::/48 via fe80::2 dev veth0")
+    ip("-6 route add default           from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+    ksft_eq(select_addr("2001:db8:500::"), "2001:db8:10::1", "no backtracking")
+    ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:10::1", "no backtracking")
+
+
+def main() -> None:
+    """
+    RFC6724 rule 5.5 test driver
+    """
+    ksft_run(
+        [
+            test_basic,
+            test_nh_obj,
+            test_low_metric,
+            test_no_subtree,
+            test_longer,
+        ]
+    )
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
  2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
                   ` (7 preceding siblings ...)
  2026-07-14  9:40 ` [PATCH net-next 8/9] selftests: net: RFC6724 rule 5.5 tests David 'equinox' Lamparter
@ 2026-07-14  9:40 ` David 'equinox' Lamparter
  2026-07-14 13:01   ` Jonathan Corbet
  8 siblings, 1 reply; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14  9:40 UTC (permalink / raw)
  To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
	Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
	linux-doc, linux-kselftest, David 'equinox' Lamparter

RFC6724 rule 5.5 is anything but obvious, especially if trying to do it
well.  (RFC8028 and its errata kinda proves the point.)

This documents what exactly the Linux kernel does for RFC6724 rule 5.5,
especially what the routing table needs to look like for it to work.

Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
 Documentation/networking/ipv6-addrsel.rst | 75 +++++++++++++++++++++++
 MAINTAINERS                               |  1 +
 2 files changed, 76 insertions(+)
 create mode 100644 Documentation/networking/ipv6-addrsel.rst

diff --git a/Documentation/networking/ipv6-addrsel.rst b/Documentation/networking/ipv6-addrsel.rst
new file mode 100644
index 000000000000..bed032e69570
--- /dev/null
+++ b/Documentation/networking/ipv6-addrsel.rst
@@ -0,0 +1,75 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================================
+IPv6 source address selection trivia
+====================================
+
+
+RFC6724 rule 5.5 support
+------------------------
+
+RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
+out quite tricky, but also immensely useful in multihoming scenarios.  For
+reference, it says:
+
+::
+
+   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
+   If SA or SA's prefix is assigned by the selected next-hop that will
+   be used to send to D and SB or SB's prefix is assigned by a different
+   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
+   assigned by the next-hop that will be used to send to D and SA or
+   SA's prefix is assigned by a different next-hop, then prefer SB.
+
+The way this works on Linux is as follows:
+
+- prior to any source address selection happening, when receiving a RA, more
+  than the installation of a default route (or ::/128 route) needs to happen:
+  for each PIO, a source-specific (subtree) route is *additionally* installed.
+  The effect of this is that *after* a source address has been selected, one
+  of the routers that advertised it will remain in use (this is *not* RFC 6724
+  related, but rather RFC 8028.)  At the same time, these extra routes serve
+  to remember which router advertised what.
+
+- per usual, a route lookup for the IPv6 destination address in consideration
+  is done first.  This is passed around in kernel as a dst_entry.
+
+- the source address selection code iterates through the various rules in
+  RFC 6724.
+
+- if/when rule 5.5 is reached, first of all, there is a check if *any* source
+  specific routes exist in the routing table.  If there are none, the entire
+  code for 5.5 is skipped because it cannot have any effect, but is not free
+  to execute (can involve multiple routing lookups.)  **In applications that
+  use a lot of unbound (e.g. UDP) sockets, installing subtree routes should
+  therefore be avoided to not incur this cost on each source address selection
+  pass.**  Alternatively, applications should bind their sockets to a specific
+  source address such that the selection code is never hit.
+
+- if subtree routes do exist, the source address selection code now repeats
+  the routing lookup done before source address selection is entered, except
+  with the source address under consideration filled in.  This lookup will hit
+  the subtree routes that were installed (see first item), giving a fresh
+  dst_entry.  If the new dst_entry matches the original dst_entry, that means
+  the original router has in fact sent RAs with PIOs for this source address,
+  so it is preferred.  Otherwise it is not.
+
+
+There are a few caveats to consider:
+
+- the kernel currently does not create the subtree routes mentioned in the
+  first item.  This is a separate work item, partially done at the time of
+  writing this.  But this can equally well be performed in userspace processing
+  of RAs, e.g. NetworkManager or plain static configuration.
+
+- since addresses can also be acquired from DHCPv6, even RA/PIO combinations
+  that didn't result in the creation of any addresses (e.g. A=0) should have
+  subtree routes added.  Those routes *may* be relevant for DHCPv6-generated
+  addresses.
+
+- the "announce check" lookup does not backtrack.  Only the destination prefix
+  that provided the "unspecific" (::/128) match is checked for source prefixes
+  to see what routers advertised what.  This means that for e.g. RIOs, subtree
+  routes also have to be created.  (Backtracking for this case would further
+  increase the cost of source address selection, for a pretty rare situation
+  that has an easy fix/workaround.)
diff --git a/MAINTAINERS b/MAINTAINERS
index f3218abefd0c..4edf48362a07 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18912,6 +18912,7 @@ F:	Documentation/netlink/specs/rt-addr.yaml
 F:	Documentation/netlink/specs/rt-neigh.yaml
 F:	Documentation/netlink/specs/rt-route.yaml
 F:	Documentation/netlink/specs/rt-rule.yaml
+F:	Documentation/networking/ipv6-addrsel.rst
 F:	include/linux/inetdevice.h
 F:	include/linux/mroute*
 F:	include/net/addrconf.h
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
  2026-07-14  9:40 ` [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation David 'equinox' Lamparter
@ 2026-07-14 13:01   ` Jonathan Corbet
  2026-07-14 17:16     ` David 'equinox' Lamparter
  0 siblings, 1 reply; 13+ messages in thread
From: Jonathan Corbet @ 2026-07-14 13:01 UTC (permalink / raw)
  To: David 'equinox' Lamparter, Paolo Abeni, Jakub Kicinski,
	Ido Schimmel
  Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
	Shuah Khan, Fernando Fernandez Mancera, Lorenzo Colitti,
	Maciej Żenczykowski, Patrick Rohr, netdev, linux-doc,
	linux-kselftest, David 'equinox' Lamparter

David 'equinox' Lamparter <equinox@diac24.net> writes:

> RFC6724 rule 5.5 is anything but obvious, especially if trying to do it
> well.  (RFC8028 and its errata kinda proves the point.)
>
> This documents what exactly the Linux kernel does for RFC6724 rule 5.5,
> especially what the routing table needs to look like for it to work.
>
> Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
> ---
>  Documentation/networking/ipv6-addrsel.rst | 75 +++++++++++++++++++++++
>  MAINTAINERS                               |  1 +
>  2 files changed, 76 insertions(+)
>  create mode 100644 Documentation/networking/ipv6-addrsel.rst

You need to add this new document to the index.rst file or it won't be
part of the docs build...you should have seen a warning when you built
the docs.

> diff --git a/Documentation/networking/ipv6-addrsel.rst b/Documentation/networking/ipv6-addrsel.rst
> new file mode 100644
> index 000000000000..bed032e69570
> --- /dev/null
> +++ b/Documentation/networking/ipv6-addrsel.rst
> @@ -0,0 +1,75 @@
> +.. SPDX-License-Identifier: GPL-2.0
> +
> +====================================
> +IPv6 source address selection trivia
> +====================================
> +
> +
> +RFC6724 rule 5.5 support
> +------------------------

Please stick with the section markup guidelines in
Documentation/doc-guide/sphinx.rst. 

> +RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
> +out quite tricky, but also immensely useful in multihoming scenarios.  For
> +reference, it says:
> +
> +::

You'll likely get more visually pleasing results if you just leave out
the "::" here; you don't need literal formatting.

> +   Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
> +   If SA or SA's prefix is assigned by the selected next-hop that will
> +   be used to send to D and SB or SB's prefix is assigned by a different
> +   next-hop, then prefer SA.  Similarly, if SB or SB's prefix is
> +   assigned by the next-hop that will be used to send to D and SA or
> +   SA's prefix is assigned by a different next-hop, then prefer SB.
> +
> +The way this works on Linux is as follows:
> +
> +- prior to any source address selection happening, when receiving a RA, more
> +  than the installation of a default route (or ::/128 route) needs to happen:
> +  for each PIO, a source-specific (subtree) route is *additionally* installed.
> +  The effect of this is that *after* a source address has been selected, one
> +  of the routers that advertised it will remain in use (this is *not* RFC 6724
> +  related, but rather RFC 8028.)  At the same time, these extra routes serve
> +  to remember which router advertised what.
> +
> +- per usual, a route lookup for the IPv6 destination address in consideration
> +  is done first.  This is passed around in kernel as a dst_entry.
> +
> +- the source address selection code iterates through the various rules in
> +  RFC 6724.
> +
> +- if/when rule 5.5 is reached, first of all, there is a check if *any* source
> +  specific routes exist in the routing table.  If there are none, the entire
> +  code for 5.5 is skipped because it cannot have any effect, but is not free
> +  to execute (can involve multiple routing lookups.)  **In applications that
> +  use a lot of unbound (e.g. UDP) sockets, installing subtree routes should
> +  therefore be avoided to not incur this cost on each source address selection
> +  pass.**  Alternatively, applications should bind their sockets to a specific
> +  source address such that the selection code is never hit.
> +
> +- if subtree routes do exist, the source address selection code now repeats
> +  the routing lookup done before source address selection is entered, except
> +  with the source address under consideration filled in.  This lookup will hit
> +  the subtree routes that were installed (see first item), giving a fresh
> +  dst_entry.  If the new dst_entry matches the original dst_entry, that means
> +  the original router has in fact sent RAs with PIOs for this source address,
> +  so it is preferred.  Otherwise it is not.
> +
> +
> +There are a few caveats to consider:
> +
> +- the kernel currently does not create the subtree routes mentioned in the
> +  first item.  This is a separate work item, partially done at the time of
> +  writing this.  But this can equally well be performed in userspace processing
> +  of RAs, e.g. NetworkManager or plain static configuration.
> +
> +- since addresses can also be acquired from DHCPv6, even RA/PIO combinations
> +  that didn't result in the creation of any addresses (e.g. A=0) should have
> +  subtree routes added.  Those routes *may* be relevant for DHCPv6-generated
> +  addresses.
> +
> +- the "announce check" lookup does not backtrack.  Only the destination prefix
> +  that provided the "unspecific" (::/128) match is checked for source prefixes
> +  to see what routers advertised what.  This means that for e.g. RIOs, subtree
> +  routes also have to be created.  (Backtracking for this case would further
> +  increase the cost of source address selection, for a pretty rare situation
> +  that has an easy fix/workaround.)
> diff --git a/MAINTAINERS b/MAINTAINERS
> index f3218abefd0c..4edf48362a07 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -18912,6 +18912,7 @@ F:	Documentation/netlink/specs/rt-addr.yaml
>  F:	Documentation/netlink/specs/rt-neigh.yaml
>  F:	Documentation/netlink/specs/rt-route.yaml
>  F:	Documentation/netlink/specs/rt-rule.yaml
> +F:	Documentation/networking/ipv6-addrsel.rst

It seems weird to add this one file here - I wonder why there isn't just
an entry for Documentation/networking ?

Thanks,

jon

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
  2026-07-14 13:01   ` Jonathan Corbet
@ 2026-07-14 17:16     ` David 'equinox' Lamparter
  2026-07-14 17:31       ` Jonathan Corbet
  0 siblings, 1 reply; 13+ messages in thread
From: David 'equinox' Lamparter @ 2026-07-14 17:16 UTC (permalink / raw)
  To: Jonathan Corbet; +Cc: netdev, linux-doc

On Tue, Jul 14, 2026 at 07:01:13AM -0600, Jonathan Corbet wrote:
> David 'equinox' Lamparter <equinox@diac24.net> writes:
> >  create mode 100644 Documentation/networking/ipv6-addrsel.rst
>
> You need to add this new document to the index.rst file or it won't be
> part of the docs build...you should have seen a warning when you built
> the docs.

Indeed... I missed it between the other warnings.  Downside of doing
clean runs without a sphinx cache, sigh.

> > +====================================
> > +IPv6 source address selection trivia
> > +====================================
> > +
> > +
> > +RFC6724 rule 5.5 support
> > +------------------------
>
> Please stick with the section markup guidelines in
> Documentation/doc-guide/sphinx.rst.

Oh, you mean I shouldn't skip the "Chapter" level?

Now that you mention it, maybe I should include the file from ipv6.rst,
and remove the document title entirely - it looks quite weird in the TOC
with the direct reference from index.rst.  (Or rather, I've made weird
choices with the headings.)  Does that sound good to you?

(Or, I could just put the entire text into ipv6.rst...)

> > +RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
> > +out quite tricky, but also immensely useful in multihoming scenarios.  For
> > +reference, it says:
> > +
> > +::
> 
> You'll likely get more visually pleasing results if you just leave out
> the "::" here; you don't need literal formatting.

I did that because RFCs are historically shipped & cited monospaced, and
that's direct from the RFC.  My tendency is to keep it this way for
"established convention" reasons, but I don't care that much - does
anyone have stronger feelings?

> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index f3218abefd0c..4edf48362a07 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -18912,6 +18912,7 @@ F:	Documentation/netlink/specs/rt-addr.yaml
> >  F:	Documentation/netlink/specs/rt-neigh.yaml
> >  F:	Documentation/netlink/specs/rt-route.yaml
> >  F:	Documentation/netlink/specs/rt-rule.yaml
> > +F:	Documentation/networking/ipv6-addrsel.rst
> 
> It seems weird to add this one file here - I wonder why there isn't just
> an entry for Documentation/networking ?

There is, the difference is "NETWORKING [GENERAL]" vs "NETWORKING
[IPv4/IPv6]".  I wasn't sure how much this matters & decided to err on
the side of caution (and also because checkpatch complained.)  I'll go
drop the line.

Thanks,


-equi

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
  2026-07-14 17:16     ` David 'equinox' Lamparter
@ 2026-07-14 17:31       ` Jonathan Corbet
  0 siblings, 0 replies; 13+ messages in thread
From: Jonathan Corbet @ 2026-07-14 17:31 UTC (permalink / raw)
  To: David 'equinox' Lamparter; +Cc: netdev, linux-doc

David 'equinox' Lamparter <equinox@diac24.net> writes:

> On Tue, Jul 14, 2026 at 07:01:13AM -0600, Jonathan Corbet wrote:
>> David 'equinox' Lamparter <equinox@diac24.net> writes:
>> >  create mode 100644 Documentation/networking/ipv6-addrsel.rst
>>
>> You need to add this new document to the index.rst file or it won't be
>> part of the docs build...you should have seen a warning when you built
>> the docs.
>
> Indeed... I missed it between the other warnings.  Downside of doing
> clean runs without a sphinx cache, sigh.

*Sigh* ... one of these years we'll clean up all those warnings, and
have them actually stay cleaned...

>> > +====================================
>> > +IPv6 source address selection trivia
>> > +====================================
>> > +
>> > +
>> > +RFC6724 rule 5.5 support
>> > +------------------------
>>
>> Please stick with the section markup guidelines in
>> Documentation/doc-guide/sphinx.rst.
>
> Oh, you mean I shouldn't skip the "Chapter" level?
>
> Now that you mention it, maybe I should include the file from ipv6.rst,
> and remove the document title entirely - it looks quite weird in the TOC
> with the direct reference from index.rst.  (Or rather, I've made weird
> choices with the headings.)  Does that sound good to you?
>
> (Or, I could just put the entire text into ipv6.rst...)

I would take that latter approach myself.  But others may differ :)

>> > +RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
>> > +out quite tricky, but also immensely useful in multihoming scenarios.  For
>> > +reference, it says:
>> > +
>> > +::
>> 
>> You'll likely get more visually pleasing results if you just leave out
>> the "::" here; you don't need literal formatting.
>
> I did that because RFCs are historically shipped & cited monospaced, and
> that's direct from the RFC.  My tendency is to keep it this way for
> "established convention" reasons, but I don't care that much - does
> anyone have stronger feelings?

You're writing the document, it's up to you in the end.

(But if you keep it that way, you can end the previous paragraph with
"it says::" and drop the separate "::" line).

Thanks,

jon

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-07-14 17:31 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14  9:40 [PATCH net-next 0/9] RFC 6724 rule 5.5 support David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 1/9] net/ipv6: fix lookup for ::/0 (non-)subtree route David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 2/9] net/ipv6: flatten ip6_route_get_saddr David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 3/9] net/ipv6: create ipv6_fl_get_saddr David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 4/9] net/ipv6: use ipv6_fl_get_saddr in output David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 5/9] net/ipv6: drop ip6_route_get_saddr David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 6/9] net/ipv6: flip IPV6_SUBTREES default to Y David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 7/9] net/ipv6: support RFC6724 rule 5.5 via subtrees David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 8/9] selftests: net: RFC6724 rule 5.5 tests David 'equinox' Lamparter
2026-07-14  9:40 ` [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation David 'equinox' Lamparter
2026-07-14 13:01   ` Jonathan Corbet
2026-07-14 17:16     ` David 'equinox' Lamparter
2026-07-14 17:31       ` Jonathan Corbet

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox