Documentation/netlink/specs/rt-route.yaml | 33 ++++ include/net/ip6_fib.h | 4 +- include/net/ip6_route.h | 2 + include/uapi/linux/rtnetlink.h | 17 ++ net/ipv6/addrconf.c | 3 +- net/ipv6/ip6_fib.c | 19 +- net/ipv6/ndisc.c | 7 +- net/ipv6/route.c | 66 ++++--- .../testing/selftests/net/lib/py/__init__.py | 4 +- tools/testing/selftests/net/lib/py/ynl.py | 7 +- tools/testing/selftests/net/rtnetlink.py | 181 +++++++++++++++++- 11 files changed, 303 insertions(+), 40 deletions(-)
When the kernel deletes an IPv6 route on its own, the RTM_DELROUTE notification does not say why. User space cannot tell a route that expired from one the router explicitly withdrew, yet the two call for different reactions: an expired RA route means the router failed to refresh it in time, which points at a misconfigured or unreliable router and may warrant action such as disabling IPv6 on that network, while a zero-lifetime withdrawal is normal, RFC-compliant operation. Patch 1 adds RTA_DEL_REASON (u8) to RTM_DELROUTE notifications and records the cause in the kernel-initiated IPv6 deletion paths: RTA_DEL_REASON_EXPIRED for routes garbage collected after their RTF_EXPIRES lifetime ran out, and RTA_DEL_REASON_RA_WITHDRAWN for default routes, prefix routes and RFC 4191 route information routes withdrawn by Router Advertisements. The rt-route Netlink spec is extended with the attribute, the route notifications and their multicast groups. Only kernel-initiated deletions that user space cannot otherwise explain are attributed. User-requested deletions are self-explanatory to the requester, so they carry no reason; the UAPI documents that absence and RTA_DEL_REASON_UNSPEC must be treated identically, which keeps the door open for attributing more paths (nexthop removal cascades, device removal) later. Patch 2 adds selftests covering all three producer paths: a GC-expired route, and a default route + PIO prefix route + RIO route advertised and then withdrawn by hand-crafted RAs over a raw ICMPv6 socket (no external RA tool needed), plus a check that user-requested deletions carry no attribute. The notifications are decoded with YNL, which also exercises the rt-route spec additions. Yuyang Huang (2): ipv6: report why a route was deleted in RTM_DELROUTE selftests: net: verify RTA_DEL_REASON on route deletion Documentation/netlink/specs/rt-route.yaml | 33 ++++ include/net/ip6_fib.h | 4 +- include/net/ip6_route.h | 2 + include/uapi/linux/rtnetlink.h | 17 ++ net/ipv6/addrconf.c | 3 +- net/ipv6/ip6_fib.c | 19 +- net/ipv6/ndisc.c | 7 +- net/ipv6/route.c | 66 ++++--- .../testing/selftests/net/lib/py/__init__.py | 4 +- tools/testing/selftests/net/lib/py/ynl.py | 7 +- tools/testing/selftests/net/rtnetlink.py | 181 +++++++++++++++++- 11 files changed, 303 insertions(+), 40 deletions(-) -- 2.43.0
On Sat, Jul 18, 2026 at 07:23:25AM +0900, Yuyang Huang wrote: > When the kernel deletes an IPv6 route on its own, the RTM_DELROUTE > notification does not say why. User space cannot tell a route that > expired from one the router explicitly withdrew, yet the two call for > different reactions: an expired RA route means the router failed to > refresh it in time, which points at a misconfigured or unreliable > router and may warrant action such as disabling IPv6 on that network, > while a zero-lifetime withdrawal is normal, RFC-compliant operation. Please expand more on the motivation: Which user space application is going to consume this information and what is it going to do with it? Also, Sashiko has some valid comments. Please take a look.
On Sun, Jul 19, 2026 at 3:53 PM Ido Schimmel <idosch@nvidia.com> wrote: > Please expand more on the motivation: Which user space application is > going to consume this information and what is it going to do with it? Thanks for the review, let me add more background information. My current use case comes from Android devices, but I think it is a general problem that any consumer device running Linux will face, especially on Wi-Fi networks (multicast delivery on Wi-Fi is not guaranteed, e.g. frames can be lost around DTIM for clients in power save mode). On Android, the userspace NetworkStack process listens on RTMGRP_IPV6_ROUTE and today treats any loss of the IPv6 default route as "router lost". To prevent the device from repeatedly gaining and losing IPv6 connectivity on a badly configured network (e.g. a misconfigured RA interval), when it detects the device is on a dual-stack network with working IPv4 connectivity, it defensively clears accept_ra_defrtr and restarts IPv6. The intention is to stop userspace apps from continuing to use broken global IPv6 connectivity (while keeping link-local IPv6 working). However, if the route was withdrawn by a zero-lifetime RA (some ISPs do this intentionally for various reconfiguration reasons), that reaction is wrong - with accept_ra_defrtr off, IPv6 never recovers when the router advertises again. If the route genuinely expired, the defensive reaction is right, since the router failed to refresh it in time. We tried to fix this in userspace but found we cannot get the needed information from the kernel. RTM_NEWROUTE carries the initial route lifetime (in rta_cacheinfo), but when a later RA refreshes the lifetime, the kernel does not resend RTM_NEWROUTE. So to distinguish the cause of an RTM_DELROUTE, userspace would have to open a raw socket, listen to RAs, and track lifetimes itself. That is not ideal: this logic already exists in the kernel, and userspace should not need to replicate it. I also considered sending RTM_NEWROUTE on every RA lifetime update, but that would be spammy and is technically wrong (a lifetime update does not add a new route, so it should not trigger RTM_NEWROUTE). I think RTA_DEL_REASON is the architecturally correct fix: it tells userspace why the route was deleted so it can react accordingly. In our use case, NetworkStack should defensively disable global IPv6 only on RTA_DEL_REASON_EXPIRED, and take no action on RTA_DEL_REASON_RA_WITHDRAWN since that is RFC-compliant behavior. Feel free to let me know if more background is needed. If this use case makes sense, I will summarize it in the v2 commit message. > Also, Sashiko has some valid comments. Please take a look. Here is the reply to the sashiko's feedback. >Does this new attribute violate the Netlink uAPI guidelines for integer >types? The guidelines state we should avoid integer types smaller than 32 >bits since they save no memory due to 4-byte attribute alignment padding, >unless the value is a fixed protocol header field. >Should this be defined as a u32? ... >Since this deletion reason is a software classification and not a compact >protocol field, it has no natural requirement to be 8-bit. Would it be >better to document and serialize this as a 32-bit integer to prevent >artificially limiting the enum space for future additions? ... >If the attribute type in the YAML spec is updated to u32, this call to >nla_put_u8() in rt6_fill_node() would also need to be updated to >nla_put_u32() to match the 32-bit width. Good point, given u8 saves nothing, will change it to u32 in v2. >Will this operation cause a TypeError crash during test execution? >In Python 3, using the subtraction operator between a set and a dict_keys >object raises an unsupported operand type error. Since the caller >ipv6_route_del_reason_ra_withdrawn() passes a set for the want parameter, >want - seen.keys() will crash the test. >Should this be converted to a set first, such as want - set(seen.keys())? I don't think this is a bug (dict_keys is a set-like view) and the test passes correctly in my local test. But the suggested form reads better, so I'll apply it in v2. Thanks, Yuyang
© 2016 - 2026 Red Hat, Inc.