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.

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(-)

diff --git a/Documentation/networking/ip-sysctl.rst 
b/Documentation/networking/ip-sysctl.rst
index 208f46967ee5..4cc57a6be99b 100644
--- a/Documentation/networking/ip-sysctl.rst
+++ b/Documentation/networking/ip-sysctl.rst
@@ -3223,18 +3223,19 @@ drop_unsolicited_na - BOOLEAN
        Default: 0 (disabled).
 
 accept_untracked_na - INTEGER
-       Define behavior for accepting neighbor advertisements from devices that
-       are absent in the neighbor cache:
+       Define behavior for accepting neighbor advertisements for IPv6 addresses
+       that are absent from the neighbor cache or whose entries are in FAILED
+       state:
 
-       - 0 - (default) Do not accept unsolicited and untracked neighbor
-         advertisements.
+       - 0 - (default) Do not create new neighbor cache entries or update
+         FAILED entries from neighbor advertisements.
 
-       - 1 - Add a new neighbor cache entry in STALE state for routers on
-         receiving a neighbor advertisement (either solicited or unsolicited)
-         with target link-layer address option specified if no neighbor entry
-         is already present for the advertised IPv6 address. Without this knob,
-         NAs received for untracked addresses (absent in neighbor cache) are
-         silently ignored.
+       - 1 - For routers, add a new neighbor cache entry or update an existing
+         FAILED entry to STALE upon receiving a neighbor advertisement (either
+         solicited or unsolicited) with the target link-layer address option
+         specified. Without this knob, NAs received for untracked addresses
+         (absent from the neighbor cache or in FAILED state) are silently
+         ignored.
 
          This is as per router-side behavior documented in RFC9131.
 
@@ -3249,9 +3250,10 @@ accept_untracked_na - INTEGER
          used in conjunction with the ndisc_notify setting on the host to
          satisfy this prerequisite.
 
-       - 2 - Extend option (1) to add a new neighbor cache entry only if the
-         source IP address is in the same subnet as an address configured on
-         the interface that received the neighbor advertisement.
+       - 2 - Extend option (1) to add a new neighbor cache entry or update a
+         FAILED entry only if the source IP address is in the same subnet as
+         an address configured on the interface that received the neighbor
+         advertisement.
 
 enhanced_dad - BOOLEAN
        Include a nonce option in the IPv6 neighbor solicitation messages used 
for
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);
 }
 
 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,
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);
 }
 
 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
+     */
        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;
        u8 *lladdr = NULL;
        SKB_DR(reason);
        u8 new_state;
@@ -1067,32 +1084,41 @@ static enum skb_drop_reason ndisc_recv_na(struct 
sk_buff *skb)
        neigh = neigh_lookup(tbl, &msg->target, dev);
 
        /* RFC 9131 updates original Neighbour Discovery RFC 4861.
-        * NAs with Target LL Address option without a corresponding
-        * entry in the neighbour cache can now create a STALE neighbour
-        * cache entry on routers.
+        * NAs with Target LL Address option can now create a STALE neighbor
+        * cache entry on routers if the NA does not have a corresponding entry
+        * in the neighbour cache or has a corresponding FAILED entry.
         *
-        *   entry accept  fwding  solicited        behaviour
-        * ------- ------  ------  ---------    ----------------------
-        * present      X       X         0     Set state to STALE
-        * present      X       X         1     Set state to REACHABLE
-        *  absent      0       X         X     Do nothing
-        *  absent      1       0         X     Do nothing
-        *  absent      1       1         X     Add a new STALE entry
+        *       entry accept  fwding  solicited        behaviour
+        * ----------- ------  ------  ---------    ----------------------
+        *  non-FAILED      X       X         0     Set state to STALE
+        *  non-FAILED      X       X         1     Set state to REACHABLE
+        *      FAILED      0       X         X     Do nothing
+        *      FAILED      1       0         X     Do nothing
+        *      FAILED      1       1         X     Set state to STALE
+        *      absent      0       X         X     Do nothing
+        *      absent      1       0         X     Do nothing
+        *      absent      1       1         X     Add a new STALE entry
         *
         * 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;
-               }
-       }
+       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;
 
                /*
@@ -1108,19 +1134,23 @@ static enum skb_drop_reason ndisc_recv_na(struct 
sk_buff *skb)
                        goto out;
                }
 
-               ndisc_update(dev, neigh, lladdr,
-                            new_state,
-                            NEIGH_UPDATE_F_WEAK_OVERRIDE|
-                            (msg->icmph.icmp6_override ? 
NEIGH_UPDATE_F_OVERRIDE : 0)|
-                            NEIGH_UPDATE_F_OVERRIDE_ISROUTER|
-                            (msg->icmph.icmp6_router ? NEIGH_UPDATE_F_ISROUTER 
: 0),
-                            NDISC_NEIGHBOUR_ADVERTISEMENT, &ndopts);
+               update_flags = NEIGH_UPDATE_F_WEAK_OVERRIDE |
+                              (msg->icmph.icmp6_override ?
+                               NEIGH_UPDATE_F_OVERRIDE : 0) |
+                              NEIGH_UPDATE_F_OVERRIDE_ISROUTER |
+                              (msg->icmph.icmp6_router ?
+                               NEIGH_UPDATE_F_ISROUTER : 0);
+
+               __ndisc_update(dev, neigh, lladdr,
+                              new_state, update_flags, neigh_failed,
+                              NDISC_NEIGHBOUR_ADVERTISEMENT, &ndopts);
 
                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);
                }
                reason = SKB_CONSUMED;
 out:
-- 
2.43.0

Reply via email to