[PATCH net-next v2] selftests: net: add IPv4 and IPv6 address order check
From: Martin Jabůrek <hidden>
Date: 2026-09-07 12:57:42
Subsystem:
kernel selftest framework, networking [general], the rest · Maintainers:
Shuah Khan, Shuah Khan, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
Introduce the following new tests: `ipv4_verify_same_scope_addr_order`, `ipv4_verify_inter_scope_addr_order`, `ipv6_verify_same_scope_addr_order`, `ipv6_verify_inter_scope_addr_order` to check the ordering of a set of IP addresses after being inserted. The implementations of these protocols are inconsistent in regard to address ordering. IPv4 addresses stay in the same order as inserted while IPv6 addresses appear in reverse order. This inconsistency has prompted attempts to unify the ordering, so both protocols act the same (as IPv4). This however caused user-space regressions in certain applications, which relied on the order as it was prior to the change (particularly NetworkManager). A similar inconsistency occurs when inserting different scope addresses, where IPv4 puts link local ones before global and IPv6 does the opposite. Attempts were already made to unify this also, risking further regressions. The addition of these tests aims to consolidate current behaviour to prevent regressions in the future. The expected behaviour is the initial one, where each protocol acts differently. The same goes for inter scope addresses. Conversations detailing the decision processes for creating these tests are linked below. Link: https://lore.kernel.org/netdev/20260521135310.GC977@cmadams.net/ (local) Link: https://lore.kernel.org/netdev/20260529112357.5079-1-fmancera@suse.de/ (local) Link: https://lore.kernel.org/netdev/20260721090114.GA2510713@shredder/ (local) Suggested-by: Fernando Fernandez Mancera <redacted> Signed-off-by: Martin Jabůrek <redacted> --- v2: added tests for inter-scope addresses, created helper functions, fixed linter errors for new code --- tools/testing/selftests/net/rtnetlink.py | 115 ++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index 5cc3ebdcf08d..5605be411f1a 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py@@ -314,11 +314,124 @@ def ipv6_route_del_reason_absent() -> None: "user deletion must not carry del-reason") +def _insert_and_get_addrs_ipv4(test_addrs: list[str], scopes: list[str]) -> list[str]: + with NetNS() as ns, NetNSEnter(str(ns)): + dev_name = "dummy_dev" + + ip(f"link add name {dev_name} type dummy", ns=str(ns)) + for test_addr, scope in zip(test_addrs, scopes): + ip(f"address add {test_addr}/24 dev {dev_name} scope {scope}", ns=str(ns)) + ip(f"link set dev {dev_name} up", ns=str(ns)) + + rtnl = RtnlAddrFamily() + addrs = rtnl.getaddr({"ifa-family": socket.AF_INET}, dump=True) + return [addr["address"] for addr in addrs] + + +def ipv4_verify_same_scope_addr_order() -> None: + """ + After inserting multiple same scope IPv4 addresses, their order + must be the same as the insertion order. + """ + + # Primary addresses of test subnets are inserted first. + test_addresses = [ + "192.0.2.1", "203.0.113.1", + "192.0.2.2", "203.0.113.2", "192.0.2.3" + ] + scopes = ["global"] * 5 + + address_list = _insert_and_get_addrs_ipv4(test_addresses, scopes) + ksft_eq(test_addresses, address_list, "Unexpected IPv4 address order") + + +def ipv4_verify_inter_scope_addr_order() -> None: + """ + Checks the ordering of inter-scope addresses. This has also + been attempted to be patched, bringing in a new risk of a user-space + regression, similar to the same scope equivalent. This will further + consolidate the implementation differences of both protocols. + """ + + test_addresses = [ + "192.0.2.1", "203.0.113.1", + "192.0.2.2", "203.0.113.2", "192.0.2.3" + ] + # IPv4 puts link local addresses before global ones. + scopes = [ + "link", "global", + "link", "global", "link" + ] + + address_list = _insert_and_get_addrs_ipv4(test_addresses, scopes) + ksft_eq(test_addresses, address_list, "Unexpected IPv4 address order across scopes") + + +def _insert_and_get_addrs_ipv6(test_addrs: list[str], scopes: list[str]) -> list[str]: + with NetNS() as ns, NetNSEnter(str(ns)): + dev_name = "dummy_dev" + + ip(f"link add name {dev_name} type dummy", ns=str(ns)) + for test_addr, scope in zip(test_addrs, scopes): + ip(f"address add {test_addr}/64 dev {dev_name} scope {scope}", ns=str(ns)) + ip(f"link set dev {dev_name} up", ns=str(ns)) + + rtnl = RtnlAddrFamily() + addrs = rtnl.getaddr({"ifa-family": socket.AF_INET6}, dump=True) + return [addr["address"] for addr in addrs] + + +def ipv6_verify_same_scope_addr_order() -> None: + """ + After inserting multiple same scope IPv6 addresses, their order + must be the _reverse_ of the insertion order. + + While this behaviour is different from how IPv4 acts, + updating the IPv6 implementation to act the same way + has proved to cause user-space application regressions + (particularly in NetworkManager). This behaviour is being + tested for to consolidate it as being expected and correct. + """ + + test_addresses = [ + "2001:db8:a::1", "2001:db8:b::1", + "2001:db8:a::2", "2001:db8:b::2", "2001:db8:a::3" + ] + scopes = ["global"] * 5 + + address_list = _insert_and_get_addrs_ipv6(test_addresses, scopes) + # We ignore the link-local address present by default. + ksft_eq(test_addresses[::-1], address_list[:5], "Unexpected IPv6 address order") + + +def ipv6_verify_inter_scope_addr_order() -> None: + """ + To prevent potential user-space regressions with IPv6 + addresses, the inter-scope insertion order is also being tested. + + This again differs from IPv4, here global scope addresses have priority + against link local ones - the behaviours are opposite. + """ + test_addresses = [ + "2001:db8:a::1", "2001:db8:b::1", + "2001:db8:a::2", "2001:db8:b::2", "2001:db8:a::3" + ] + scopes = [ + "global", "link", + "global", "link", "global" + ] + + address_list = _insert_and_get_addrs_ipv6(test_addresses, scopes) + ksft_eq(test_addresses[::-1], address_list[:5], "Unexpected IPv6 address order across scopes") + + def main() -> None: ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify, ipv6_route_del_reason_expired, ipv6_route_del_reason_ra_withdrawn, - ipv6_route_del_reason_absent]) + ipv6_route_del_reason_absent, + ipv4_verify_same_scope_addr_order, ipv4_verify_inter_scope_addr_order, + ipv6_verify_same_scope_addr_order, ipv6_verify_inter_scope_addr_order]) ksft_exit() if __name__ == "__main__":
--
2.55.0