From: Yuyang Huang <sigefriedhyy@gmail.com>
To: Yuyang Huang <sigefriedhyy@gmail.com>
Cc: "David S. Miller" <davem@davemloft.net>,
Bobby Eshleman <bobbyeshleman@meta.com>,
Chris J Arges <carges@cloudflare.com>,
Daniel Zahka <daniel.zahka@gmail.com>,
David Ahern <dsahern@kernel.org>, David Wei <dw@davidwei.uk>,
Dimitri Daskalakis <dimitri.daskalakis1@gmail.com>,
Donald Hunter <donald.hunter@gmail.com>,
Eric Dumazet <edumazet@google.com>, Gal Pressman <gal@nvidia.com>,
Ido Schimmel <idosch@nvidia.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Shuah Khan <shuah@kernel.org>, Simon Horman <horms@kernel.org>,
Stanislav Fomichev <sdf@fomichev.me>,
Willem de Bruijn <willemb@google.com>,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
netdev@vger.kernel.org
Subject: [PATCH net-next 2/2] selftests: net: verify RTA_DEL_REASON on route deletion
Date: Sat, 18 Jul 2026 07:23:27 +0900 [thread overview]
Message-ID: <20260717222327.7398-3-sigefriedhyy@gmail.com> (raw)
In-Reply-To: <20260717222327.7398-1-sigefriedhyy@gmail.com>
Extend rtnetlink.py to check the reason reported in RTM_DELROUTE:
- expired: route with a 2s lifetime collected by the fib6 GC
(gc_interval lowered like fib_tests.sh fib6_gc_test does);
- ra-withdrawn: a single RA advertises a default route (router
lifetime), an on-link prefix route (RFC 4861 prefix information
option) and a route information option route (RFC 4191), then a
second RA withdraws all three with zero lifetimes; the RAs are
crafted over a raw ICMPv6 socket so the test does not depend on an
external RA tool;
- absence: a userspace deletion request records no cause and must not
carry the attribute at all.
Signed-off-by: Yuyang Huang <sigefriedhyy@gmail.com>
---
.../testing/selftests/net/lib/py/__init__.py | 4 +-
tools/testing/selftests/net/lib/py/ynl.py | 7 +-
tools/testing/selftests/net/rtnetlink.py | 181 +++++++++++++++++-
3 files changed, 187 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index e58bdbdc58ee..34935886b6ad 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -17,7 +17,7 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
wait_file, tool, tc
from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
from .ynl import NlError, NlctrlFamily, YnlFamily, \
- EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily
+ EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily
from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
__all__ = ["KSRC",
@@ -34,4 +34,4 @@ __all__ = ["KSRC",
"NetdevSim", "NetdevSimDev",
"NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
"YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
- "NlctrlFamily", "RtnlAddrFamily", "Netlink"]
+ "NlctrlFamily", "RtnlAddrFamily", "RtnlRouteFamily", "Netlink"]
diff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py
index 2e567062aa6c..08deff756f29 100644
--- a/tools/testing/selftests/net/lib/py/ynl.py
+++ b/tools/testing/selftests/net/lib/py/ynl.py
@@ -29,7 +29,7 @@ except ModuleNotFoundError as e:
__all__ = [
"NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH",
- "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily",
+ "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily", "RtnlRouteFamily",
"NetdevFamily", "NetshaperFamily", "NlctrlFamily", "DevlinkFamily",
"PSPFamily",
]
@@ -54,6 +54,11 @@ class RtnlAddrFamily(YnlFamily):
super().__init__((SPEC_PATH / Path('rt-addr.yaml')).as_posix(),
schema='', recv_size=recv_size)
+class RtnlRouteFamily(YnlFamily):
+ def __init__(self, recv_size=0):
+ super().__init__((SPEC_PATH / Path('rt-route.yaml')).as_posix(),
+ schema='', recv_size=recv_size)
+
class NetdevFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(),
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index 0c67c7c00d84..8773e88b934b 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -5,7 +5,8 @@ import socket
import struct
import time
from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx
-from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily
+from lib.py import ksft_not_in, ksft_not_none
+from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily
IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01'
@@ -134,8 +135,184 @@ def ipv4_devconf_notify() -> None:
ksft_true(f"inet {ifname} forwarding on" in cmd_obj.stdout,
f"No 'forwarding on' notificiation found for interface {ifname}")
+def _rtnl_route_subscribe(ns):
+ with NetNSEnter(str(ns)):
+ rtnl = RtnlRouteFamily()
+ rtnl.ntf_subscribe("rtnlgrp-ipv6-route")
+ return rtnl
+
+
+def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10):
+ """Return the attrs of the first matching notification, None on timeout."""
+
+ for msg in rtnl.poll_ntf(duration=deadline):
+ if msg['name'] != name:
+ continue
+ attrs = msg['msg']
+ if attrs['rtm-dst-len'] != dst_len:
+ continue
+ if dst is not None and attrs.get('dst') != dst:
+ continue
+ return attrs
+ return None
+
+
+def _collect_route_ntfs(rtnl, name, want, deadline=10):
+ """Gather attrs of matching notifications, keyed by (dst_len, dst)."""
+
+ seen = {}
+ for msg in rtnl.poll_ntf(duration=deadline):
+ if msg['name'] != name:
+ continue
+ attrs = msg['msg']
+ key = (attrs['rtm-dst-len'], attrs.get('dst'))
+ if key in want:
+ seen[key] = attrs
+ if len(seen) == len(want):
+ break
+ return seen
+
+
+def _write_ipv6_sysctl(name, value):
+ with open(f"/proc/sys/net/ipv6/{name}", "w") as f:
+ f.write(f"{value}\n")
+
+
+def ipv6_route_del_reason_expired() -> None:
+ """An expired route reports RTA_DEL_REASON == expired."""
+
+ with NetNS() as ns:
+ rtnl = _rtnl_route_subscribe(ns)
+ with NetNSEnter(str(ns)):
+ _write_ipv6_sysctl("route/gc_interval", 2)
+ ip("link add name dummy1 type dummy", ns=str(ns))
+ ip("link set dev dummy1 up", ns=str(ns))
+ ip("-6 route add 2001:db8:2::/64 dev dummy1 expires 2", ns=str(ns))
+
+ attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:2::',
+ deadline=15)
+ ksft_not_none(attrs, "no RTM_DELROUTE for the expired route")
+ if attrs is not None:
+ ksft_eq(attrs.get('del-reason'), 'expired')
+
+
+def _send_ra(sock, ifindex, lifetime, rio=None, pio=None):
+ """The kernel fills in the ICMPv6 checksum on raw ICMPv6 sockets."""
+
+ # type, code, cksum, hop limit, flags, router lifetime,
+ # reachable time, retrans timer
+ ra = struct.pack('!BBHBBHII', 134, 0, 0, 64, 0, lifetime, 0, 0)
+ if rio is not None:
+ prefix, plen, rio_lifetime = rio
+ # RFC 4191 route information option, /64 prefix (8 bytes)
+ ra += struct.pack('!BBBBI', 24, 2, plen, 0, rio_lifetime)
+ ra += socket.inet_pton(socket.AF_INET6, prefix)[:8]
+ if pio is not None:
+ prefix, plen, valid_lft = pio
+ # RFC 4861 prefix information option, on-link only (L set, A clear)
+ ra += struct.pack('!BBBBIII', 3, 4, plen, 0x80, valid_lft, 0, 0)
+ ra += socket.inet_pton(socket.AF_INET6, prefix)
+ sock.sendto(ra, ('ff02::1', 0, 0, ifindex))
+
+
+def _ra_router_sock(ns_r, ifname):
+ with NetNSEnter(str(ns_r)):
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
+ socket.IPPROTO_ICMPV6)
+ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
+ return sock, socket.if_nametoindex(ifname)
+
+
+def _ra_advertise_routes(rtnl, sock, ifindex, want, **ra_opts):
+ """
+ Sending fails with EADDRNOTAVAIL until the router's link-local
+ address passes DAD, so retry.
+ """
+
+ seen = {}
+ for _ in range(10):
+ try:
+ _send_ra(sock, ifindex, **ra_opts)
+ except OSError:
+ time.sleep(0.2)
+ continue
+ seen.update(_collect_route_ntfs(rtnl, 'newroute-ntf',
+ want - seen.keys(), deadline=2))
+ if len(seen) == len(want):
+ break
+ return seen
+
+
+def ipv6_route_del_reason_ra_withdrawn() -> None:
+ """
+ Routes withdrawn by a zero-lifetime RA (router lifetime, RFC 4861
+ PIO, RFC 4191 RIO) report RTA_DEL_REASON == ra-withdrawn.
+ """
+
+ # (rtm-dst-len, dst); the default route carries no RTA_DST
+ routes = {(0, None), (64, '2001:db8:6::'), (64, '2001:db8:5::')}
+
+ with NetNS() as ns_h, NetNS() as ns_r:
+ ip(f"link add veth0 netns {ns_h} type veth peer name veth1 netns {ns_r}")
+ with NetNSEnter(str(ns_h)):
+ _write_ipv6_sysctl("conf/veth0/accept_ra", 2)
+ _write_ipv6_sysctl("conf/veth0/forwarding", 0)
+ try:
+ _write_ipv6_sysctl("conf/veth0/accept_ra_rt_info_max_plen", 64)
+ except FileNotFoundError:
+ raise KsftSkipEx("no CONFIG_IPV6_ROUTE_INFO")
+ with NetNSEnter(str(ns_r)):
+ # skip DAD so the router's link-local source is usable right away
+ _write_ipv6_sysctl("conf/veth1/accept_dad", 0)
+ ip("link set dev veth0 up", ns=str(ns_h))
+ ip("link set dev veth1 up", ns=str(ns_r))
+
+ rtnl = _rtnl_route_subscribe(ns_h)
+ sock, ifindex = _ra_router_sock(ns_r, "veth1")
+
+ seen = _ra_advertise_routes(rtnl, sock, ifindex, routes,
+ lifetime=1800,
+ rio=('2001:db8:5::', 64, 600),
+ pio=('2001:db8:6::', 64, 600))
+ ksft_eq(set(seen), routes, "not all RA routes were installed")
+ if set(seen) != routes:
+ return
+
+ _send_ra(sock, ifindex, 0, rio=('2001:db8:5::', 64, 0),
+ pio=('2001:db8:6::', 64, 0))
+ seen = _collect_route_ntfs(rtnl, 'delroute-ntf', routes)
+ for key in routes:
+ attrs = seen.get(key)
+ ksft_not_none(attrs, f"no RTM_DELROUTE for {key}")
+ if attrs is not None:
+ ksft_eq(attrs.get('del-reason'), 'ra-withdrawn')
+
+
+def ipv6_route_del_reason_absent() -> None:
+ """
+ A deletion path that records no cause (here a userspace request)
+ must not carry RTA_DEL_REASON at all.
+ """
+
+ with NetNS() as ns:
+ rtnl = _rtnl_route_subscribe(ns)
+ ip("link add name dummy1 type dummy", ns=str(ns))
+ ip("link set dev dummy1 up", ns=str(ns))
+ ip("-6 route add 2001:db8:1::/64 dev dummy1", ns=str(ns))
+ ip("-6 route del 2001:db8:1::/64 dev dummy1", ns=str(ns))
+
+ attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:1::')
+ ksft_not_none(attrs, "no RTM_DELROUTE for 2001:db8:1::/64")
+ if attrs is not None:
+ ksft_not_in('del-reason', attrs,
+ "user deletion must not carry del-reason")
+
+
def main() -> None:
- ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify])
+ 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])
ksft_exit()
if __name__ == "__main__":
--
2.43.0
prev parent reply other threads:[~2026-07-17 22:23 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-17 22:23 [PATCH net-next 0/2] ipv6: report why a route was deleted in RTM_DELROUTE Yuyang Huang
2026-07-17 22:23 ` [PATCH net-next 1/2] " Yuyang Huang
2026-07-17 22:23 ` Yuyang Huang [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260717222327.7398-3-sigefriedhyy@gmail.com \
--to=sigefriedhyy@gmail.com \
--cc=bobbyeshleman@meta.com \
--cc=carges@cloudflare.com \
--cc=daniel.zahka@gmail.com \
--cc=davem@davemloft.net \
--cc=dimitri.daskalakis1@gmail.com \
--cc=donald.hunter@gmail.com \
--cc=dsahern@kernel.org \
--cc=dw@davidwei.uk \
--cc=edumazet@google.com \
--cc=gal@nvidia.com \
--cc=horms@kernel.org \
--cc=idosch@nvidia.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=sdf@fomichev.me \
--cc=shuah@kernel.org \
--cc=willemb@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox