Thread (12 messages) 12 messages, 1 author, 2d ago
WARM2d
Revisions (2)
  1. v1 [diff vs current]
  2. v2 current

[PATCH net-next v2 09/10] selftests: net: RFC6724 rule 5.5 tests

From: David 'equinox' Lamparter <hidden>
Date: 2026-07-23 12:24:22
Also in: linux-doc, linux-kselftest
Subsystem: kernel selftest framework, networking [general], the rest · Maintainers: Shuah Khan, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds

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 <redacted>
---
 tools/testing/selftests/net/Makefile          |   1 +
 tools/testing/selftests/net/config            |   1 +
 .../net/ipv6_saddr_rfc6724rule5p5.py          | 285 ++++++++++++++++++
 3 files changed, 287 insertions(+)
 create mode 100755 tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index ab890e6f79dd..07872f838c50 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -55,6 +55,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 96fffca6547c..cc9c0f38a9d1 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -50,6 +50,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 100755
index 000000000000..21dfffc750ad
--- /dev/null
+++ b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
@@ -0,0 +1,285 @@
+#!/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")
+
+
+@in_netns
+def test_multipath() -> None:
+    """
+    Test with an ECMP (but non-nexthop-object) dst-src route
+
+    Multiple nexthops for the non-subtree route can't make a difference since
+    the rule 5.5 code is called with a dst, not a route.  Nexthop objects on
+    subtree routes are rejected on (attempted) install by the kernel.
+    """
+    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"
+        + " nexthop via fe80::1 dev veth0"
+        + " nexthop via fe80::101 dev veth0"
+    )
+    ip(
+        "-6 route add default from 2001:db8:1000::/48"
+        + " nexthop via fe80::2 dev veth0"
+        + " nexthop via fe80::102 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")
+
+    # flip order
+    ip(
+        "-6 route replace default from 2001:db8:10::/48"
+        + " nexthop via fe80::101 dev veth0"
+        + " nexthop via fe80::1 dev veth0"
+    )
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "reorder")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "reorder, 5.5 > 8")
+
+    # try the other one
+    ip("-6 route replace default via fe80::101 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "other NH")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "other NH, 5.5 > 8")
+
+    ip("-6 route del default via fe80::101 dev veth0 metric 100")
+
+    ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "delete/baseline")
+    ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "delete/5.5 > 8")
+
+
+def main() -> None:
+    """
+    RFC6724 rule 5.5 test driver
+    """
+    ksft_run(
+        [
+            test_basic,
+            test_nh_obj,
+            test_low_metric,
+            test_no_subtree,
+            test_longer,
+            test_multipath,
+        ]
+    )
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.53.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help