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 v2 2/2] selftests: net: verify RTA_DEL_REASON on route deletion
Date: Thu, 23 Jul 2026 19:44:03 +0900 [thread overview]
Message-ID: <20260723104403.7970-3-sigefriedhyy@gmail.com> (raw)
In-Reply-To: <20260723104403.7970-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..f74ccd5c2d13 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 - set(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-23 10:44 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-23 10:44 [PATCH net-next v2 0/2] ipv6: report why a route was deleted in RTM_DELROUTE Yuyang Huang
2026-07-23 10:44 ` [PATCH net-next v2 1/2] " Yuyang Huang
2026-07-23 10:44 ` 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=20260723104403.7970-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.