[PATCH net-next 0/2] ipv6: report why a route was deleted in RTM_DELROUTE

Yuyang Huang posted 2 patches 1 week ago
There is a newer version of this series
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(-)
[PATCH net-next 0/2] ipv6: report why a route was deleted in RTM_DELROUTE
Posted by Yuyang Huang 1 week ago
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
Re: [PATCH net-next 0/2] ipv6: report why a route was deleted in RTM_DELROUTE
Posted by Ido Schimmel 5 days, 21 hours ago
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.
Re: [PATCH net-next 0/2] ipv6: report why a route was deleted in RTM_DELROUTE
Posted by Yuyang Huang 5 days, 17 hours ago
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