On Wed Sep 16, 2026 at 12:02 PM UTC, Ido Schimmel wrote:
> On Tue, Sep 15, 2026 at 05:01:31AM +0000, Lawrence Lee wrote:
> > Transition a FAILED neighbor entry to STALE upon receipt of an NA
> > message on routers when accept_untracked_na is enabled. This extends the
> > RFC 9131 accept_untracked_na behavior so that FAILED entries are treated
> > the same as non-existent entries. In the context of RFC 4861 which
> > introduced NDP, both non-existent and FAILED entries are considered
> > untracked since they do not have a valid neighbor cache entry.
>
> RFC 4861 didn't introduce NDP (RFC 1970 did), so please omit this bit.
> But if you're going to mention RFC 4861, then cite 7.3.3 which says that
> "If address resolution fails, the entry SHOULD be deleted". The fact
> that Linux keeps it as FAILED is an implementation detail and treating
> it as untracked is correct from RFC perspective.
>

That's my mistake, I'll update this to mention that 4861 is just the 
most recent NDP standard and will cite 7.3.3.

> > 
> > Trying to resolve FAILED neighbors via periodic probing (e.g. using
> > NTF_EXT_MANAGED) is more work compared to this approach which uses
> > information in NAs that the kernel may already be receiving. Note that
> > because this behavior in IPv6 is dependent on the accept_untracked_na
> > sysctl setting, this approach is more conservative than IPv4 which
> > transitions FAILED neighbors to STALE by default upon receiving GARPs.
> > 
> > Link: https://lore.kernel.org/r/[email protected]
> > Assisted-by: LLM Sashiko sparse
> > Signed-off-by: Lawrence Lee <[email protected]>
> > ---
> >  Documentation/networking/ip-sysctl.rst | 28 ++++----
> >  include/net/ndisc.h                    | 15 ++--
> >  net/6lowpan/ndisc.c                    | 15 ++--
> >  net/ipv6/ndisc.c                       | 94 +++++++++++++++++---------
> >  4 files changed, 96 insertions(+), 56 deletions(-)
>
> The RFC was:
>
> 1 file changed, 11 insertions(+), 2 deletions(-)
>
> I'm not sure how this ballooned to this size...
>

Some of the additional size can be attributed to doc/comment changes, 
but a lot of it comes from changes I implemented to address issues found 
by the local Sashiko review I ran. It was definitely an oversight on my 
part to not mention these in either the commit message or comments, 
sorry about that.

> > diff --git a/include/net/ndisc.h b/include/net/ndisc.h
> > index 96e3bb6e83af..7fb3f10eca6c 100644
> > --- a/include/net/ndisc.h
> > +++ b/include/net/ndisc.h
> > @@ -154,11 +154,13 @@ void __ndisc_fill_addr_option(struct sk_buff *skb, 
> > int type, const void *data,
> >   *     option parser will take care about that option.
> >   *
> >   * void (*update)(const struct net_device *dev, struct neighbour *n,
> > - *           u32 flags, u8 icmp6_type,
> > + *           u32 flags, bool failed_recovery, u8 icmp6_type,
> >   *           const struct ndisc_options *ndopts):
> >   *     This function is called when IPv6 ndisc updates the neighbour cache
> >   *     entry. Additional options which can be updated may be previously
> >   *     parsed by parse_opts callback and accessible over ndopts parameter.
> > + *     failed_recovery indicates that ndisc accepted the packet to recover
> > + *     an entry observed in NUD_FAILED.
> >   *
> >   * int (*opt_addr_space)(const struct net_device *dev, u8 icmp6_type,
> >   *                  struct neighbour *neigh, u8 *ha_buf,
> > @@ -197,7 +199,7 @@ struct ndisc_ops {
> >                              struct nd_opt_hdr *nd_opt,
> >                              struct ndisc_options *ndopts);
> >     void    (*update)(const struct net_device *dev, struct neighbour *n,
> > -                     u32 flags, u8 icmp6_type,
> > +                     u32 flags, bool failed_recovery, u8 icmp6_type,
> >                       const struct ndisc_options *ndopts);
> >     int     (*opt_addr_space)(const struct net_device *dev, u8 icmp6_type,
> >                               struct neighbour *neigh, u8 *ha_buf,
> > @@ -227,12 +229,13 @@ static inline int ndisc_ops_parse_options(const 
> > struct net_device *dev,
> >  }
> >  
> >  static inline void ndisc_ops_update(const struct net_device *dev,
> > -                                     struct neighbour *n, u32 flags,
> > -                                     u8 icmp6_type,
> > -                                     const struct ndisc_options *ndopts)
> > +                               struct neighbour *n, u32 flags,
> > +                               bool failed_recovery, u8 icmp6_type,
> > +                               const struct ndisc_options *ndopts)
> >  {
> >     if (dev->ndisc_ops && dev->ndisc_ops->update)
> > -           dev->ndisc_ops->update(dev, n, flags, icmp6_type, ndopts);
> > +           dev->ndisc_ops->update(dev, n, flags, failed_recovery,
> > +                                  icmp6_type, ndopts);
> >  }
>
> All the changes in this file can be dropped. See below.
>

This is tied to the lowpan changes below.

> >  
> >  static inline int ndisc_ops_opt_addr_space(const struct net_device *dev,
> > diff --git a/net/6lowpan/ndisc.c b/net/6lowpan/ndisc.c
> > index 868d28583c0a..8fedfef93740 100644
> > --- a/net/6lowpan/ndisc.c
> > +++ b/net/6lowpan/ndisc.c
> > @@ -47,7 +47,8 @@ static int lowpan_ndisc_parse_options(const struct 
> > net_device *dev,
> >     }
> >  }
> >  
> > -static void lowpan_ndisc_802154_update(struct neighbour *n, u32 flags,
> > +static void lowpan_ndisc_802154_update(struct neighbour *n,
> > +                                  bool failed_recovery,
> >                                    u8 icmp6_type,
> >                                    const struct ndisc_options *ndopts)
> >  {
> > @@ -87,20 +88,24 @@ static void lowpan_ndisc_802154_update(struct neighbour 
> > *n, u32 flags,
> >             ieee802154_be16_to_le16(&neigh->short_addr, lladdr_short);
> >             if (!lowpan_802154_is_valid_src_short_addr(neigh->short_addr))
> >                     neigh->short_addr = 
> > cpu_to_le16(IEEE802154_ADDR_SHORT_UNSPEC);
> > +   } else if (failed_recovery) {
> > +           neigh->short_addr = cpu_to_le16(IEEE802154_ADDR_SHORT_UNSPEC);
> >     }
> >     write_unlock_bh(&n->lock);
> >  }
> >  
> >  static void lowpan_ndisc_update(const struct net_device *dev,
> > -                           struct neighbour *n, u32 flags, u8 icmp6_type,
> > +                           struct neighbour *n, u32 flags,
> > +                           bool failed_recovery, u8 icmp6_type,
> >                             const struct ndisc_options *ndopts)
> >  {
> >     if (!lowpan_is_ll(dev, LOWPAN_LLTYPE_IEEE802154))
> >             return;
> >  
> > -   /* react on overrides only. TODO check if this is really right. */
> > -   if (flags & NEIGH_UPDATE_F_OVERRIDE)
> > -           lowpan_ndisc_802154_update(n, flags, icmp6_type, ndopts);
> > +   /* React to overrides or accepted FAILED-entry recovery. */
> > +   if ((flags & NEIGH_UPDATE_F_OVERRIDE) || failed_recovery)
> > +           lowpan_ndisc_802154_update(n, failed_recovery, icmp6_type,
> > +                                      ndopts);
> >  }
> >  
> >  static int lowpan_ndisc_opt_addr_space(const struct net_device *dev,
>
> I'm not sure why you added these lowpan changes to the patch. They are
> not described in the commit message. Given that lowpan_ndisc_update()
> already has a TODO comment about only handling overrides, I suggest to
> ignore it. If needed, it can be modified in the future by someone who
> can explain the use case and test the change.
>

I added the lowpan changes after my local Sashiko review run identified 
an issue where a FAILED neighbor can retain an outdated private short 
address if it's moved to STALE by a non-override NA. Happy to drop all 
lowpan-related changes or update comments/commit message to reflect the 
changes, please let me know your preference.

> > diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
> > index 90cd5d852569..84d70c09205a 100644
> > --- a/net/ipv6/ndisc.c
> > +++ b/net/ipv6/ndisc.c
> > @@ -778,13 +778,23 @@ static int pndisc_is_router(const void *pkey,
> >     return ret;
> >  }
> >  
> > +static void __ndisc_update(const struct net_device *dev,
> > +                      struct neighbour *neigh, const u8 *lladdr, u8 new,
> > +                      u32 flags, bool failed_recovery, u8 icmp6_type,
> > +                      struct ndisc_options *ndopts)
> > +{
> > +   neigh_update(neigh, lladdr, new, flags, 0);
> > +   /* report ndisc ops about neighbour update */
> > +   ndisc_ops_update(dev, neigh, flags, failed_recovery, icmp6_type,
> > +                    ndopts);
> > +}
> > +
> >  void ndisc_update(const struct net_device *dev, struct neighbour *neigh,
> >               const u8 *lladdr, u8 new, u32 flags, u8 icmp6_type,
> >               struct ndisc_options *ndopts)
> >  {
> > -   neigh_update(neigh, lladdr, new, flags, 0);
> > -   /* report ndisc ops about neighbour update */
> > -   ndisc_ops_update(dev, neigh, flags, icmp6_type, ndopts);
> > +   __ndisc_update(dev, neigh, lladdr, new, flags, false, icmp6_type,
> > +                  ndopts);
> >  }
>
> This hunk can be dropped.
>

Tied to the lowpan changes above.

> >  
> >  static enum skb_drop_reason ndisc_recv_ns(struct sk_buff *skb)
> > @@ -972,14 +982,18 @@ static enum skb_drop_reason ndisc_recv_ns(struct 
> > sk_buff *skb)
> >  
> >  static int accept_untracked_na(struct inet6_dev *idev, struct in6_addr 
> > *saddr)
> >  {
> > +    /* For any given neighbor IP address, consider it an untracked 
> > neighbor if
> > +     * it is absent from the neighbor cache or if it has a NUD_FAILED 
> > entry in
> > +     * the neighbor cache
> > +     */
>
> Redundant given the comments in the caller and the sysctl documentation.
> Simply modify the existing comments below to mention FAILED case.
>

Will update.

> >     switch (READ_ONCE(idev->cnf.accept_untracked_na)) {
> > -   case 0: /* Don't accept untracked na (absent in neighbor cache) */
> > +   case 0: /* Reject NAs for untracked neighbours */
> >             return 0;
> > -   case 1: /* Create new entries from na if currently untracked */
> > +   case 1: /* Accept NAs for untracked neighbours */
> >             return 1;
> > -   case 2: /* Create new entries from untracked na only if saddr is in the
> > +   case 2: /* Accept NAs for untracked neighbours only if saddr is in the
> >              * same subnet as an address configured on the interface that
> > -            * received the na
> > +            * received the NA
> >              */
> >             return !!ipv6_chk_prefix(saddr, idev->dev);
> >     default:
> > @@ -1001,6 +1015,9 @@ static enum skb_drop_reason ndisc_recv_na(struct 
> > sk_buff *skb)
> >     struct neigh_table *tbl;
> >     struct neighbour *neigh;
> >     struct inet6_dev *idev;
> > +   bool neigh_failed = false;
> > +   bool neigh_untracked = false;
> > +   bool accept_untracked = false;
>
> Try to maintain reverse xmas tree:
>
> https://docs.kernel.org/next/process/maintainer-netdev.html#local-variable-ordering-reverse-xmas-tree-rcs
>

Will fix.

> >     new_state = msg->icmph.icmp6_solicited ? NUD_REACHABLE : 
> >     NUD_STALE;
> > -   if (!neigh && lladdr && idev && READ_ONCE(idev->cnf.forwarding)) {
> > -           if (accept_untracked_na(idev, saddr)) {
> > -                   neigh = neigh_create(tbl, &msg->target, dev);
> > -                   new_state = NUD_STALE;
> > -           }
> > -   }
> > +   neigh_failed = neigh &&
> > +                  (READ_ONCE(neigh->nud_state) & NUD_FAILED);
> > +   neigh_untracked = !neigh || neigh_failed;
> > +   if (neigh_untracked) {
> > +           accept_untracked = lladdr && idev &&
> > +                              READ_ONCE(idev->cnf.forwarding) &&
> > +                              accept_untracked_na(idev, saddr);
> > +           new_state = NUD_STALE;
> > +   }
> > +   if (!neigh && accept_untracked)
> > +           neigh = neigh_create(tbl, &msg->target, dev);
> >  
> >     if (neigh && !IS_ERR(neigh)) {
> > +           u32 update_flags;
> >             u8 old_flags = neigh->flags;
> >  
> > -           if (READ_ONCE(neigh->nud_state) & NUD_FAILED)
> > +           if (neigh_untracked && !accept_untracked)
> >                     goto out;
> >  
> >             /*
>
> This can be simplified to:
>
> diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
> index 90cd5d852569..7d114bdb263e 100644
> --- a/net/ipv6/ndisc.c
> +++ b/net/ipv6/ndisc.c
> @@ -1082,19 +1082,21 @@ static enum skb_drop_reason ndisc_recv_na(struct 
> sk_buff *skb)
>        * Note that we don't do a (daddr == all-routers-mcast) check.
>        */
>       new_state = msg->icmph.icmp6_solicited ? NUD_REACHABLE : NUD_STALE;
> -     if (!neigh && lladdr && idev && READ_ONCE(idev->cnf.forwarding)) {
> -             if (accept_untracked_na(idev, saddr)) {
> -                     neigh = neigh_create(tbl, &msg->target, dev);
> -                     new_state = NUD_STALE;
> +     if (!neigh || (READ_ONCE(neigh->nud_state) & NUD_FAILED)) {
> +             if (!lladdr || !idev || !READ_ONCE(idev->cnf.forwarding) ||
> +                 !accept_untracked_na(idev, saddr)) {
> +                     if (neigh)
> +                             neigh_release(neigh);
> +                     return reason;
>               }
> +             if (!neigh)
> +                     neigh = neigh_create(tbl, &msg->target, dev);
> +             new_state = NUD_STALE;
>       }
>  
>       if (neigh && !IS_ERR(neigh)) {
>               u8 old_flags = neigh->flags;
>  
> -             if (READ_ONCE(neigh->nud_state) & NUD_FAILED)
> -                     goto out;
> -
>               /*
>                * Don't update the neighbor cache entry on a proxy NA from
>                * ourselves because either the proxied node is off link or it
>

Was originally unsure if I should modify the existing logical structure.  
Thanks for the suggestion, will implement this. Is it appropriate to 
credit you with a commit tag?

> >  
> >             if ((old_flags & ~neigh->flags) & NTF_ROUTER) {
> >                     /*
> >                      * Change: router to host
> >                      */
> > -                   rt6_clean_tohost(dev_net(dev),  saddr);
> > +                   rt6_clean_tohost(net,
> > +                                    neigh_failed ? &msg->target : saddr);
> >             }
>
> What is the reason for this change? It's also not explained in the
> commit message and I suspect it's not needed.

This was added in response to another Sashiko local review finding.  
Let's say we have some FAILED neighbor T with NTF_ROUTER set. If we get 
an NA from source address S with target address T and with the Router 
bit clear, existing kernel code will cleanup routes with gateway S, but 
IMO we should clean routes with gateway T instead since that is the 
neighbor which was updated by the NA. I can either update 
comments/commit message to reflect this or remove the change entirely, 
please let me know your preference.


Reply via email to