review feedback from sashiko is correct https://sashiko.dev/#/patchset/20260723104403.7970-1-sigefriedhyy%40gmail.com
I will send another revision to fix them. On Thu, Jul 23, 2026 at 7:44 PM Yuyang Huang <[email protected]> wrote: > > Add RTA_DEL_REASON (u8) to RTM_DELROUTE notifications to report the > cause of kernel-initiated IPv6 route deletions, so user space can tell > routes that expired from routes explicitly withdrawn by Router > Advertisements. > > The two causes call for different treatment in user space. An expired > RA route means the router failed to refresh it before its lifetime ran > out, which points at a misconfigured or unreliable router; user space > may want to react, for example by disabling IPv6 on that network > entirely. An RA withdrawal (zero lifetime) is valid, RFC-compliant > operation and needs no such recovery. Today both surface as an > identical RTM_DELROUTE. > > Plumb the deletion reason through the IPv6 FIB and notification paths: > - Update `fib6_del`, `fib6_del_route`, and `inet6_rt_notify` signatures > to explicitly pass the `del_reason` (u8) down the call chain. > - Update `rt6_fill_node()` to emit `RTA_DEL_REASON` attribute when the > reason is not UNSPEC. > - Record the cause in the kernel-initiated IPv6 deletion paths: > - RTA_DEL_REASON_EXPIRED: for routes cleaned up by the FIB6 garbage > collector. > - RTA_DEL_REASON_RA_WITHDRAWN: for default routes, prefix routes, or > route information option routes withdrawn by Router Advertisements. > - Update the `rt-route` Netlink YAML spec to define the new attribute, > notifications, and multicast groups. > > Signed-off-by: Yuyang Huang <[email protected]> > --- > 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 +++++++++++++++-------- > 8 files changed, 116 insertions(+), 35 deletions(-) > > diff --git a/Documentation/netlink/specs/rt-route.yaml > b/Documentation/netlink/specs/rt-route.yaml > index 33195db96746..234f502fb35d 100644 > --- a/Documentation/netlink/specs/rt-route.yaml > +++ b/Documentation/netlink/specs/rt-route.yaml > @@ -78,6 +78,15 @@ definitions: > - > name: rta-used > type: u32 > + - > + name: del-reason > + type: enum > + name-prefix: rta-del-reason- > + enum-name: rta-del-reason > + entries: > + - unspec > + - expired > + - ra-withdrawn > > attribute-sets: > - > @@ -185,6 +194,10 @@ attribute-sets: > type: u32 > byte-order: big-endian > display-hint: hex > + - > + name: del-reason > + type: u32 > + enum: del-reason > - > name: metrics > name-prefix: rtax- > @@ -299,6 +312,7 @@ operations: > - dport > - nh-id > - flowlabel > + - del-reason > dump: > request: > value: 26 > @@ -322,3 +336,22 @@ operations: > request: > value: 25 > attributes: *all-route-attrs > + - > + name: newroute-ntf > + doc: Notification about a created route. > + value: 24 > + notify: getroute > + - > + name: delroute-ntf > + doc: Notification about a deleted route. > + value: 25 > + notify: getroute > + > +mcast-groups: > + list: > + - > + name: rtnlgrp-ipv4-route > + value: 7 > + - > + name: rtnlgrp-ipv6-route > + value: 11 > diff --git a/include/net/ip6_fib.h b/include/net/ip6_fib.h > index 9cd27e1b9b69..7ca0e2124e79 100644 > --- a/include/net/ip6_fib.h > +++ b/include/net/ip6_fib.h > @@ -468,7 +468,7 @@ void fib6_clean_all_skip_notify(struct net *net, > > int fib6_add(struct fib6_node *root, struct fib6_info *rt, > struct nl_info *info, struct netlink_ext_ack *extack); > -int fib6_del(struct fib6_info *rt, struct nl_info *info); > +int fib6_del(struct fib6_info *rt, struct nl_info *info, u32 del_reason); > > static inline > void rt6_get_prefsrc(const struct rt6_info *rt, struct in6_addr *addr) > @@ -531,7 +531,7 @@ static inline void fib6_rt_update(struct net *net, struct > fib6_info *rt, > } > #endif > void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info, > - unsigned int flags); > + unsigned int flags, u32 del_reason); > > void fib6_age_exceptions(struct fib6_info *rt, struct fib6_gc_args *gc_args, > unsigned long now); > diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h > index 09ffe0f13ce7..98f1684fe378 100644 > --- a/include/net/ip6_route.h > +++ b/include/net/ip6_route.h > @@ -128,6 +128,8 @@ int ip6_route_add(struct fib6_config *cfg, gfp_t > gfp_flags, > int ip6_ins_rt(struct net *net, struct fib6_info *f6i); > #if IS_ENABLED(CONFIG_IPV6) > int ip6_del_rt(struct net *net, struct fib6_info *f6i, bool skip_notify); > +int ip6_del_rt_reason(struct net *net, struct fib6_info *f6i, bool > skip_notify, > + u32 del_reason); > #else > static inline int ip6_del_rt(struct net *net, struct fib6_info *f6i, > bool skip_notify) > diff --git a/include/uapi/linux/rtnetlink.h b/include/uapi/linux/rtnetlink.h > index 27265fd31e5f..fe00e624f3c3 100644 > --- a/include/uapi/linux/rtnetlink.h > +++ b/include/uapi/linux/rtnetlink.h > @@ -399,6 +399,7 @@ enum rtattr_type_t { > RTA_DPORT, > RTA_NH_ID, > RTA_FLOWLABEL, > + RTA_DEL_REASON, > __RTA_MAX > }; > > @@ -407,6 +408,22 @@ enum rtattr_type_t { > #define RTM_RTA(r) ((struct rtattr*)(((char*)(r)) + > NLMSG_ALIGN(sizeof(struct rtmsg)))) > #define RTM_PAYLOAD(n) NLMSG_PAYLOAD(n,sizeof(struct rtmsg)) > > +/* RTA_DEL_REASON: why the kernel deleted the route. u32. > + * Emitted only on RTM_DELROUTE notifications, and only when the deletion > + * path records a cause. Absence means either an older kernel or a > + * deletion path that does not (yet) record its cause - consumers must > + * treat "absent" and "unspec" identically. New causes may be appended. > + * Currently only IPv6 deletion paths record a cause. > + */ > +enum rta_del_reason { > + RTA_DEL_REASON_UNSPEC, /* cause not recorded */ > + RTA_DEL_REASON_EXPIRED, /* RTF_EXPIRES lifetime ran out (GC) > */ > + RTA_DEL_REASON_RA_WITHDRAWN, /* zero-lifetime RA / PIO / RIO */ > + __RTA_DEL_REASON_MAX > +}; > + > +#define RTA_DEL_REASON_MAX (__RTA_DEL_REASON_MAX - 1) > + > /* RTM_MULTIPATH --- array of struct rtnexthop. > * > * "struct rtnexthop" describes all necessary nexthop information, > diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c > index f1fe9ede1edb..1ca9d82b5721 100644 > --- a/net/ipv6/addrconf.c > +++ b/net/ipv6/addrconf.c > @@ -2874,7 +2874,8 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 > *opt, int len, bool sllao) > if (rt) { > /* Autoconf prefix route */ > if (valid_lft == 0) { > - ip6_del_rt(net, rt, false); > + ip6_del_rt_reason(net, rt, false, > + > RTA_DEL_REASON_RA_WITHDRAWN); > rt = NULL; > } else { > table = rt->fib6_table; > diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c > index a130cdfaebfb..32b9a00956a0 100644 > --- a/net/ipv6/ip6_fib.c > +++ b/net/ipv6/ip6_fib.c > @@ -1286,7 +1286,8 @@ static int fib6_add_rt2node(struct fib6_node *fn, > struct fib6_info *rt, > rcu_assign_pointer(rt->fib6_node, fn); > rcu_assign_pointer(*ins, rt); > if (!info->skip_notify) > - inet6_rt_notify(RTM_NEWROUTE, rt, info, nlflags); > + inet6_rt_notify(RTM_NEWROUTE, rt, info, nlflags, > + RTA_DEL_REASON_UNSPEC); > info->nl_net->ipv6.rt6_stats->fib_rt_entries++; > > if (!(fn->fn_flags & RTN_RTINFO)) { > @@ -1317,7 +1318,8 @@ static int fib6_add_rt2node(struct fib6_node *fn, > struct fib6_info *rt, > rt->fib6_next = iter->fib6_next; > rcu_assign_pointer(*ins, rt); > if (!info->skip_notify) > - inet6_rt_notify(RTM_NEWROUTE, rt, info, > NLM_F_REPLACE); > + inet6_rt_notify(RTM_NEWROUTE, rt, info, NLM_F_REPLACE, > + RTA_DEL_REASON_UNSPEC); > if (!(fn->fn_flags & RTN_RTINFO)) { > info->nl_net->ipv6.rt6_stats->fib_route_nodes++; > fn->fn_flags |= RTN_RTINFO; > @@ -1966,7 +1968,8 @@ static struct fib6_node *fib6_repair_tree(struct net > *net, > } > > static void fib6_del_route(struct fib6_table *table, struct fib6_node *fn, > - struct fib6_info __rcu **rtp, struct nl_info *info) > + struct fib6_info __rcu **rtp, struct nl_info *info, > + u32 del_reason) > { > struct fib6_info *leaf, *replace_rt = NULL; > struct fib6_walker *w; > @@ -2055,13 +2058,13 @@ static void fib6_del_route(struct fib6_table *table, > struct fib6_node *fn, > call_fib6_entry_notifiers_replace(net, replace_rt); > } > if (!info->skip_notify) > - inet6_rt_notify(RTM_DELROUTE, rt, info, 0); > + inet6_rt_notify(RTM_DELROUTE, rt, info, 0, del_reason); > > fib6_info_release(rt); > } > > /* Need to own table->tb6_lock */ > -int fib6_del(struct fib6_info *rt, struct nl_info *info) > +int fib6_del(struct fib6_info *rt, struct nl_info *info, u32 del_reason) > { > struct net *net = info->nl_net; > struct fib6_info __rcu **rtp; > @@ -2090,7 +2093,7 @@ int fib6_del(struct fib6_info *rt, struct nl_info *info) > if (rt == cur) { > if (fib6_requires_src(cur)) > fib6_routes_require_src_dec(info->nl_net); > - fib6_del_route(table, fn, rtp, info); > + fib6_del_route(table, fn, rtp, info, del_reason); > return 0; > } > rtp_next = &cur->fib6_next; > @@ -2252,7 +2255,7 @@ static int fib6_clean_node(struct fib6_walker *w) > res = c->func(rt, c->arg); > if (res == -1) { > w->leaf = rt; > - res = fib6_del(rt, &info); > + res = fib6_del(rt, &info, RTA_DEL_REASON_UNSPEC); > if (res) { > #if RT6_DEBUG >= 2 > pr_debug("%s: del failed: rt=%p@%p err=%d\n", > @@ -2400,7 +2403,7 @@ static void fib6_gc_table(struct net *net, > > hlist_for_each_entry_safe(rt, n, &tb6->tb6_gc_hlist, gc_link) > if (fib6_age(rt, gc_args) == -1) > - fib6_del(rt, &info); > + fib6_del(rt, &info, RTA_DEL_REASON_EXPIRED); > } > > static void fib6_gc_all(struct net *net, struct fib6_gc_args *gc_args) > diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c > index f867ec8d3d90..c3537fc0ec63 100644 > --- a/net/ipv6/ndisc.c > +++ b/net/ipv6/ndisc.c > @@ -1361,7 +1361,9 @@ static enum skb_drop_reason > ndisc_router_discovery(struct sk_buff *skb) > defrtr_usr_metric = in6_dev->cnf.ra_defrtr_metric; > /* delete the route if lifetime is 0 or if metric needs change */ > if (rt && (lifetime == 0 || rt->fib6_metric != defrtr_usr_metric)) { > - ip6_del_rt(net, rt, false); > + ip6_del_rt_reason(net, rt, false, > + lifetime == 0 ? RTA_DEL_REASON_RA_WITHDRAWN > : > + RTA_DEL_REASON_UNSPEC); > rt = NULL; > } > > @@ -1396,7 +1398,8 @@ static enum skb_drop_reason > ndisc_router_discovery(struct sk_buff *skb) > .nl_net = net, > }; > rt->fib6_flags = (rt->fib6_flags & ~RTF_PREF_MASK) | > RTF_PREF(pref); > - inet6_rt_notify(RTM_NEWROUTE, rt, &nlinfo, NLM_F_REPLACE); > + inet6_rt_notify(RTM_NEWROUTE, rt, &nlinfo, NLM_F_REPLACE, > + RTA_DEL_REASON_UNSPEC); > } > > if (rt) { > diff --git a/net/ipv6/route.c b/net/ipv6/route.c > index a1301334da48..e8ed95dc5665 100644 > --- a/net/ipv6/route.c > +++ b/net/ipv6/route.c > @@ -111,7 +111,7 @@ static int rt6_fill_node(struct net *net, struct sk_buff > *skb, > struct fib6_info *rt, struct dst_entry *dst, > struct in6_addr *dest, struct in6_addr *src, > int iif, int type, u32 portid, u32 seq, > - unsigned int flags); > + unsigned int flags, u32 del_reason); > static struct rt6_info *rt6_find_cached_rt(const struct fib6_result *res, > const struct in6_addr *daddr, > const struct in6_addr *saddr); > @@ -1020,7 +1020,7 @@ int rt6_route_rcv(struct net_device *dev, u8 *opt, int > len, > gwaddr, dev); > > if (rt && !lifetime) { > - ip6_del_rt(net, rt, false); > + ip6_del_rt_reason(net, rt, false, > RTA_DEL_REASON_RA_WITHDRAWN); > rt = NULL; > } > > @@ -3973,7 +3973,8 @@ int ip6_route_add(struct fib6_config *cfg, gfp_t > gfp_flags, > return err; > } > > -static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info) > +static int __ip6_del_rt(struct fib6_info *rt, struct nl_info *info, > + u32 del_reason) > { > struct net *net = info->nl_net; > struct fib6_table *table; > @@ -3986,7 +3987,7 @@ static int __ip6_del_rt(struct fib6_info *rt, struct > nl_info *info) > > table = rt->fib6_table; > spin_lock_bh(&table->tb6_lock); > - err = fib6_del(rt, info); > + err = fib6_del(rt, info, del_reason); > spin_unlock_bh(&table->tb6_lock); > > out: > @@ -3994,14 +3995,20 @@ static int __ip6_del_rt(struct fib6_info *rt, struct > nl_info *info) > return err; > } > > -int ip6_del_rt(struct net *net, struct fib6_info *rt, bool skip_notify) > +int ip6_del_rt_reason(struct net *net, struct fib6_info *rt, bool > skip_notify, > + u32 del_reason) > { > struct nl_info info = { > .nl_net = net, > - .skip_notify = skip_notify > + .skip_notify = skip_notify, > }; > > - return __ip6_del_rt(rt, &info); > + return __ip6_del_rt(rt, &info, del_reason); > +} > + > +int ip6_del_rt(struct net *net, struct fib6_info *rt, bool skip_notify) > +{ > + return ip6_del_rt_reason(net, rt, skip_notify, RTA_DEL_REASON_UNSPEC); > } > > static int __ip6_del_rt_siblings(struct fib6_info *rt, struct fib6_config > *cfg) > @@ -4028,7 +4035,8 @@ static int __ip6_del_rt_siblings(struct fib6_info *rt, > struct fib6_config *cfg) > > if (rt6_fill_node(net, skb, rt, NULL, > NULL, NULL, 0, RTM_DELROUTE, > - info->portid, seq, 0) < 0) { > + info->portid, seq, 0, > + RTA_DEL_REASON_UNSPEC) < 0) { > kfree_skb(skb); > skb = NULL; > } else > @@ -4064,13 +4072,13 @@ static int __ip6_del_rt_siblings(struct fib6_info > *rt, struct fib6_config *cfg) > list_for_each_entry_safe(sibling, next_sibling, > &rt->fib6_siblings, > fib6_siblings) { > - err = fib6_del(sibling, info); > + err = fib6_del(sibling, info, RTA_DEL_REASON_UNSPEC); > if (err) > goto out_unlock; > } > } > > - err = fib6_del(rt, info); > + err = fib6_del(rt, info, RTA_DEL_REASON_UNSPEC); > out_unlock: > spin_unlock_bh(&table->tb6_lock); > out_put: > @@ -4196,7 +4204,8 @@ static int ip6_route_del(struct fib6_config *cfg, > if (!fib6_info_hold_safe(rt)) > continue; > > - err = __ip6_del_rt(rt, &cfg->fc_nlinfo); > + err = __ip6_del_rt(rt, &cfg->fc_nlinfo, > + RTA_DEL_REASON_UNSPEC); > break; > } > if (cfg->fc_nh_id) > @@ -4215,7 +4224,8 @@ static int ip6_route_del(struct fib6_config *cfg, > > /* if gateway was specified only delete the one hop */ > if (cfg->fc_flags & RTF_GATEWAY) > - err = __ip6_del_rt(rt, &cfg->fc_nlinfo); > + err = __ip6_del_rt(rt, &cfg->fc_nlinfo, > + RTA_DEL_REASON_UNSPEC); > else > err = __ip6_del_rt_siblings(rt, cfg); > break; > @@ -5381,7 +5391,8 @@ static void ip6_route_mpath_notify(struct fib6_info *rt, > } > > if (rt) > - inet6_rt_notify(RTM_NEWROUTE, rt, info, nlflags); > + inet6_rt_notify(RTM_NEWROUTE, rt, info, nlflags, > + RTA_DEL_REASON_UNSPEC); > > rcu_read_unlock(); > } > @@ -5739,6 +5750,7 @@ static size_t rt6_nlmsg_size(struct fib6_info *f6i) > + nla_total_size(sizeof(struct rta_cacheinfo)) > + nla_total_size(TCP_CA_NAME_MAX) /* RTAX_CC_ALGO */ > + nla_total_size(1) /* RTA_PREF */ > + + nla_total_size(4) /* RTA_DEL_REASON */ > + nexthop_len; > } > > @@ -5775,7 +5787,7 @@ static int rt6_fill_node(struct net *net, struct > sk_buff *skb, > struct fib6_info *rt, struct dst_entry *dst, > struct in6_addr *dest, struct in6_addr *src, > int iif, int type, u32 portid, u32 seq, > - unsigned int flags) > + unsigned int flags, u32 del_reason) > { > struct rt6_info *rt6 = dst_rt6_info(dst); > struct rt6key *rt6_dst, *rt6_src; > @@ -5954,6 +5966,10 @@ static int rt6_fill_node(struct net *net, struct > sk_buff *skb, > if (rtnl_put_cacheinfo(skb, dst, 0, expires, dst ? dst->error : 0) < > 0) > goto nla_put_failure; > > + if (type == RTM_DELROUTE && del_reason != RTA_DEL_REASON_UNSPEC && > + nla_put_u32(skb, RTA_DEL_REASON, del_reason)) > + goto nla_put_failure; > + > if (nla_put_u8(skb, RTA_PREF, IPV6_EXTRACT_PREF(rt6_flags))) > goto nla_put_failure; > > @@ -6055,7 +6071,8 @@ static int rt6_nh_dump_exceptions(struct fib6_nh *nh, > void *arg) > &rt6_ex->rt6i->dst, NULL, NULL, 0, > RTM_NEWROUTE, > NETLINK_CB(dump->cb->skb).portid, > - dump->cb->nlh->nlmsg_seq, > w->flags); > + dump->cb->nlh->nlmsg_seq, > w->flags, > + RTA_DEL_REASON_UNSPEC); > if (err) > return err; > > @@ -6103,7 +6120,8 @@ int rt6_dump_route(struct fib6_info *rt, void *p_arg, > unsigned int skip) > if (rt6_fill_node(net, arg->skb, rt, NULL, NULL, NULL, > 0, RTM_NEWROUTE, > NETLINK_CB(arg->cb->skb).portid, > - arg->cb->nlh->nlmsg_seq, flags)) { > + arg->cb->nlh->nlmsg_seq, flags, > + RTA_DEL_REASON_UNSPEC)) { > return 0; > } > count++; > @@ -6336,12 +6354,14 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, > struct nlmsghdr *nlh, > err = rt6_fill_node(net, skb, from, NULL, NULL, NULL, > iif, RTM_NEWROUTE, > NETLINK_CB(in_skb).portid, > - nlh->nlmsg_seq, 0); > + nlh->nlmsg_seq, 0, > + RTA_DEL_REASON_UNSPEC); > else > err = rt6_fill_node(net, skb, from, dst, &fl6.daddr, > &fl6.saddr, iif, RTM_NEWROUTE, > NETLINK_CB(in_skb).portid, > - nlh->nlmsg_seq, 0); > + nlh->nlmsg_seq, 0, > + RTA_DEL_REASON_UNSPEC); > } else { > err = -ENETUNREACH; > } > @@ -6358,7 +6378,7 @@ static int inet6_rtm_getroute(struct sk_buff *in_skb, > struct nlmsghdr *nlh, > } > > void inet6_rt_notify(int event, struct fib6_info *rt, struct nl_info *info, > - unsigned int nlm_flags) > + unsigned int nlm_flags, u32 del_reason) > { > struct net *net = info->nl_net; > struct sk_buff *skb; > @@ -6377,7 +6397,8 @@ void inet6_rt_notify(int event, struct fib6_info *rt, > struct nl_info *info, > goto errout; > > err = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0, > - event, info->portid, seq, nlm_flags); > + event, info->portid, seq, nlm_flags, > + del_reason); > if (err < 0) { > kfree_skb(skb); > /* -EMSGSIZE implies needed space grew under us. */ > @@ -6410,7 +6431,8 @@ void fib6_rt_update(struct net *net, struct fib6_info > *rt, > goto errout; > > err = rt6_fill_node(net, skb, rt, NULL, NULL, NULL, 0, > - RTM_NEWROUTE, info->portid, seq, NLM_F_REPLACE); > + RTM_NEWROUTE, info->portid, seq, NLM_F_REPLACE, > + RTA_DEL_REASON_UNSPEC); > if (err < 0) { > /* -EMSGSIZE implies BUG in rt6_nlmsg_size() */ > WARN_ON(err == -EMSGSIZE); > @@ -6463,7 +6485,7 @@ void fib6_info_hw_flags_set(struct net *net, struct > fib6_info *f6i, > } > > err = rt6_fill_node(net, skb, f6i, NULL, NULL, NULL, 0, RTM_NEWROUTE, > 0, > - 0, 0); > + 0, 0, RTA_DEL_REASON_UNSPEC); > if (err < 0) { > /* -EMSGSIZE implies BUG in rt6_nlmsg_size() */ > WARN_ON(err == -EMSGSIZE); > -- > 2.43.0 >

