The native userspace tunnel neighbor cache removes a complete entry
as soon as its aging timer expires. The next packet is treated as a
cache miss and dropped while OVS sends an ARP or neighbor discovery
request.

A kernel neighbor entry can instead enter the stale state while still
retaining a usable link-layer address. Traffic continues using that
address while neighbor reachability is refreshed.

Add reachable, stale, and incomplete states to the userspace cache.
When a reachable entry expires, retain its MAC indefinitely and send
traffic while issuing rate-limited refresh probes. Avoid caching
translations made with a stale entry so that OVS can retry probes and
promptly use a refreshed binding.

Remove a stale entry after three unanswered probes and one retransmit
interval. Limit retained stale entries to 4096 and evict the
oldest-used entries when the limit is exceeded.

This fixes the case where OVS previously learned the neighbor MAC but
the entry became stale during an idle period.

A genuinely cold entry—restart, flush, eviction, or never learned—
still drops its triggering packet. That remains the future buffering
case.

Add IPv4 and IPv6 tests covering long-idle retention, stale MAC
forwarding, rate-limited refresh probes, and removal after three
unanswered probes.

Assisted-by: GPT-5, OpenAI Codex
Signed-off-by: Tim Rozet <[email protected]>
---
 lib/tnl-neigh-cache.c              | 245 ++++++++++++++++++++++++-----
 lib/tnl-neigh-cache.h              |   3 +-
 ofproto/ofproto-dpif-xlate-cache.c |   3 +-
 ofproto/ofproto-dpif-xlate.c       |  60 ++++---
 tests/tunnel-push-pop-ipv6.at      |  44 +++++-
 tests/tunnel-push-pop.at           |  41 ++++-
 6 files changed, 330 insertions(+), 66 deletions(-)

diff --git a/lib/tnl-neigh-cache.c b/lib/tnl-neigh-cache.c
index fbefc3d51..2c791af12 100644
--- a/lib/tnl-neigh-cache.c
+++ b/lib/tnl-neigh-cache.c
@@ -48,14 +48,25 @@
 #define NEIGH_ENTRY_DEFAULT_IDLE_TIME_MS (15 * 60 * 1000)
 #define NEIGH_ENTRY_MAX_AGING_TIME_S     3600
 #define NEIGH_ENTRY_LOOKUP_RETRANS_TIME  1000
+#define NEIGH_ENTRY_MAX_PROBES           3
+#define NEIGH_ENTRY_MAX_STALE            4096
+
+enum tnl_neigh_state {
+    TNL_NEIGH_INCOMPLETE,
+    TNL_NEIGH_REACHABLE,
+    TNL_NEIGH_STALE,
+};
 
 struct tnl_neigh_entry {
     struct cmap_node cmap_node;
     struct in6_addr ip;
     struct eth_addr mac;
-    atomic_llong expires;       /* Expiration time in ms. */
+    atomic_llong expires;       /* Reachable/incomplete deadline in ms. */
+    atomic_llong used;          /* Last successful lookup in ms. */
+    atomic_llong probe_expires; /* Next allowed stale probe in ms. */
     char br_name[IFNAMSIZ];
-    atomic_bool complete;
+    atomic_uint n_probes;       /* Unanswered probes while stale. */
+    atomic_uint state;          /* enum tnl_neigh_state. */
 };
 
 static struct cmap table = CMAP_INITIALIZER;
@@ -70,15 +81,24 @@ tnl_neigh_hash(const struct in6_addr *ip)
 }
 
 static bool
-tnl_neigh_expired(struct tnl_neigh_entry *neigh)
+tnl_neigh_deadline_expired(atomic_llong *deadline)
 {
     long long expires;
 
-    atomic_read_explicit(&neigh->expires, &expires, memory_order_acquire);
+    atomic_read_explicit(deadline, &expires, memory_order_acquire);
 
     return expires <= time_msec();
 }
 
+static enum tnl_neigh_state
+tnl_neigh_get_state(struct tnl_neigh_entry *neigh)
+{
+    unsigned int state;
+
+    atomic_read_explicit(&neigh->state, &state, memory_order_acquire);
+    return state;
+}
+
 static uint32_t
 tnl_neigh_get_aging(void)
 {
@@ -98,15 +118,6 @@ tnl_neigh_get_retrans_time(void)
     return retrans_time;
 }
 
-static bool
-tnl_neigh_is_complete(struct tnl_neigh_entry *neigh)
-{
-    bool complete;
-
-    atomic_read_explicit(&neigh->complete, &complete, memory_order_acquire);
-    return complete;
-}
-
 static struct tnl_neigh_entry *
 tnl_neigh_lookup__(const char br_name[IFNAMSIZ], const struct in6_addr *dst)
 {
@@ -116,16 +127,6 @@ tnl_neigh_lookup__(const char br_name[IFNAMSIZ], const 
struct in6_addr *dst)
     hash = tnl_neigh_hash(dst);
     CMAP_FOR_EACH_WITH_HASH (neigh, cmap_node, hash, &table) {
         if (ipv6_addr_equals(&neigh->ip, dst) && !strcmp(neigh->br_name, 
br_name)) {
-            if (tnl_neigh_expired(neigh)) {
-                return NULL;
-            }
-
-            if (tnl_neigh_is_complete(neigh)) {
-                atomic_store_explicit(&neigh->expires,
-                                      time_msec() + tnl_neigh_get_aging(),
-                                      memory_order_release);
-            }
-
             return neigh;
         }
     }
@@ -146,9 +147,12 @@ tnl_neigh_set_partial(const char name[IFNAMSIZ], const 
struct in6_addr *dst)
     neigh = xmalloc(sizeof *neigh);
 
     neigh->ip = *dst;
-    atomic_store_relaxed(&neigh->complete, false);
     atomic_store_relaxed(&neigh->expires,
                          time_msec() + tnl_neigh_get_retrans_time());
+    atomic_store_relaxed(&neigh->used, 0);
+    atomic_store_relaxed(&neigh->probe_expires, 0);
+    atomic_store_relaxed(&neigh->n_probes, 0);
+    atomic_store_relaxed(&neigh->state, TNL_NEIGH_INCOMPLETE);
     ovs_strlcpy(neigh->br_name, name, sizeof neigh->br_name);
     cmap_insert(&table, &neigh->cmap_node, tnl_neigh_hash(&neigh->ip));
 
@@ -158,18 +162,79 @@ tnl_neigh_set_partial(const char name[IFNAMSIZ], const 
struct in6_addr *dst)
 
 int
 tnl_neigh_lookup(const char br_name[IFNAMSIZ], const struct in6_addr *dst,
-                 struct eth_addr *mac, bool insert_partial)
+                 struct eth_addr *mac, bool insert_partial, bool *stale,
+                 bool *probe)
 {
     struct tnl_neigh_entry *neigh;
+    enum tnl_neigh_state state;
     int res = ENOENT;
 
+    if (stale) {
+        *stale = false;
+    }
+    if (probe) {
+        *probe = false;
+    }
+
     neigh = tnl_neigh_lookup__(br_name, dst);
     if (neigh) {
-        if (tnl_neigh_is_complete(neigh)) {
+        state = tnl_neigh_get_state(neigh);
+        if (state == TNL_NEIGH_REACHABLE) {
+            long long now = time_msec();
+
             *mac = neigh->mac;
+            atomic_store_explicit(&neigh->expires,
+                                  now + tnl_neigh_get_aging(),
+                                  memory_order_release);
+            atomic_store_explicit(&neigh->used, now,
+                                  memory_order_release);
             res = 0;
-        } else {
+        } else if (state == TNL_NEIGH_STALE) {
+            long long expires;
+            long long now = time_msec();
+            uint32_t retrans_time = tnl_neigh_get_retrans_time();
+            unsigned int n_probes;
+
+            *mac = neigh->mac;
+            if (stale) {
+                *stale = true;
+            }
+            atomic_store_explicit(&neigh->used, now,
+                                  memory_order_release);
+            atomic_read_explicit(&neigh->n_probes, &n_probes,
+                                 memory_order_acquire);
+            atomic_read_explicit(&neigh->probe_expires, &expires,
+                                 memory_order_acquire);
+            if (probe && n_probes < NEIGH_ENTRY_MAX_PROBES &&
+                (!retrans_time || expires <= now)) {
+                long long next = now + retrans_time;
+
+                if (!retrans_time ||
+                    atomic_compare_exchange_strong_explicit(
+                        &neigh->probe_expires, &expires, next,
+                        memory_order_acq_rel, memory_order_acquire)) {
+                    atomic_add_explicit(&neigh->n_probes, 1, &n_probes,
+                                        memory_order_acq_rel);
+                    if (n_probes < NEIGH_ENTRY_MAX_PROBES) {
+                        *probe = true;
+                    }
+                }
+            }
+            res = 0;
+        } else if (!tnl_neigh_deadline_expired(&neigh->expires)) {
             res = EINPROGRESS;
+        } else if (insert_partial && tnl_neigh_get_retrans_time()) {
+            long long expires;
+            long long now = time_msec();
+            long long next = now + tnl_neigh_get_retrans_time();
+
+            atomic_read_explicit(&neigh->expires, &expires,
+                                 memory_order_acquire);
+            if (!atomic_compare_exchange_strong_explicit(
+                    &neigh->expires, &expires, next,
+                    memory_order_acq_rel, memory_order_acquire)) {
+                res = EINPROGRESS;
+            }
         }
     } else if (insert_partial && tnl_neigh_get_retrans_time()) {
         /* Insert a partial entry only if there is a retransmit timer set. */
@@ -193,22 +258,75 @@ tnl_neigh_delete(struct tnl_neigh_entry *neigh)
     ovsrcu_postpone(neigh_entry_free, neigh);
 }
 
+static int
+tnl_neigh_compare_used(const void *a_, const void *b_)
+{
+    struct tnl_neigh_entry *const *a = a_;
+    struct tnl_neigh_entry *const *b = b_;
+    long long int a_used;
+    long long int b_used;
+
+    atomic_read_explicit(&(*a)->used, &a_used, memory_order_acquire);
+    atomic_read_explicit(&(*b)->used, &b_used, memory_order_acquire);
+
+    return a_used > b_used ? 1 : a_used < b_used ? -1 : 0;
+}
+
+/* Retain stale entries so that they remain usable after long idle periods,
+ * but limit how much memory they can consume. */
+static bool
+tnl_neigh_evict_stale(void)
+{
+    struct tnl_neigh_entry **entries;
+    struct tnl_neigh_entry *neigh;
+    size_t n_stale = 0;
+    size_t i = 0;
+
+    CMAP_FOR_EACH (neigh, cmap_node, &table) {
+        if (tnl_neigh_get_state(neigh) == TNL_NEIGH_STALE) {
+            n_stale++;
+        }
+    }
+    if (n_stale <= NEIGH_ENTRY_MAX_STALE) {
+        return false;
+    }
+
+    entries = xmalloc(n_stale * sizeof *entries);
+    CMAP_FOR_EACH (neigh, cmap_node, &table) {
+        if (tnl_neigh_get_state(neigh) == TNL_NEIGH_STALE) {
+            entries[i++] = neigh;
+        }
+    }
+    ovs_assert(i == n_stale);
+    qsort(entries, n_stale, sizeof *entries, tnl_neigh_compare_used);
+
+    for (i = 0; i < n_stale - NEIGH_ENTRY_MAX_STALE; i++) {
+        tnl_neigh_delete(entries[i]);
+    }
+    free(entries);
+
+    return true;
+}
+
 void
 tnl_neigh_set(const char name[IFNAMSIZ], const struct in6_addr *dst,
               const struct eth_addr mac)
 {
     ovs_mutex_lock(&mutex);
     struct tnl_neigh_entry *neigh = tnl_neigh_lookup__(name, dst);
+    enum tnl_neigh_state state;
     bool insert = true;
+    bool changed = true;
+    bool update_mac = true;
 
     if (neigh) {
-        if (!tnl_neigh_is_complete(neigh)) {
+        state = tnl_neigh_get_state(neigh);
+        if (state == TNL_NEIGH_INCOMPLETE) {
             insert = false;
         } else if (eth_addr_equals(neigh->mac, mac)) {
-            atomic_store_relaxed(&neigh->expires,
-                                 time_msec() + tnl_neigh_get_aging());
-            ovs_mutex_unlock(&mutex);
-            return;
+            insert = false;
+            update_mac = false;
+            changed = state != TNL_NEIGH_REACHABLE;
         } else {
             tnl_neigh_delete(neigh);
         }
@@ -221,18 +339,28 @@ tnl_neigh_set(const char name[IFNAMSIZ], const struct 
in6_addr *dst,
         ovs_strlcpy(neigh->br_name, name, sizeof neigh->br_name);
     }
 
-    neigh->mac = mac;
+    if (update_mac) {
+        neigh->mac = mac;
+    }
+    long long now = time_msec();
+
     atomic_store_explicit(&neigh->expires,
-                          time_msec() + tnl_neigh_get_aging(),
+                          now + tnl_neigh_get_aging(),
+                          memory_order_release);
+    atomic_store_explicit(&neigh->used, now, memory_order_release);
+    atomic_store_relaxed(&neigh->probe_expires, 0);
+    atomic_store_relaxed(&neigh->n_probes, 0);
+    atomic_store_explicit(&neigh->state, TNL_NEIGH_REACHABLE,
                           memory_order_release);
-    atomic_store_explicit(&neigh->complete, true, memory_order_release);
 
     if (insert) {
         cmap_insert(&table, &neigh->cmap_node, tnl_neigh_hash(&neigh->ip));
     }
 
     ovs_mutex_unlock(&mutex);
-    seq_change(tnl_conf_seq);
+    if (changed) {
+        seq_change(tnl_conf_seq);
+    }
 }
 
 static void
@@ -310,11 +438,34 @@ tnl_neigh_cache_run(void)
 
     ovs_mutex_lock(&mutex);
     CMAP_FOR_EACH(neigh, cmap_node, &table) {
-        if (tnl_neigh_expired(neigh)) {
+        enum tnl_neigh_state state = tnl_neigh_get_state(neigh);
+
+        if (state == TNL_NEIGH_REACHABLE &&
+            tnl_neigh_deadline_expired(&neigh->expires)) {
+            long long now = time_msec();
+
+            atomic_store_relaxed(&neigh->probe_expires, now);
+            atomic_store_relaxed(&neigh->n_probes, 0);
+            atomic_store_explicit(&neigh->state, TNL_NEIGH_STALE,
+                                  memory_order_release);
+            changed = true;
+        } else if (state == TNL_NEIGH_INCOMPLETE &&
+                   tnl_neigh_deadline_expired(&neigh->expires)) {
             tnl_neigh_delete(neigh);
             changed = true;
+        } else if (state == TNL_NEIGH_STALE) {
+            unsigned int n_probes;
+
+            atomic_read_explicit(&neigh->n_probes, &n_probes,
+                                 memory_order_acquire);
+            if (n_probes >= NEIGH_ENTRY_MAX_PROBES &&
+                tnl_neigh_deadline_expired(&neigh->probe_expires)) {
+                tnl_neigh_delete(neigh);
+                changed = true;
+            }
         }
     }
+    changed |= tnl_neigh_evict_stale();
     ovs_mutex_unlock(&mutex);
 
     if (changed) {
@@ -396,6 +547,9 @@ tnl_neigh_cache_aging(struct unixctl_conn *conn, int argc,
     new_exp = time_msec() + aging;
 
     CMAP_FOR_EACH (neigh, cmap_node, &table) {
+        if (tnl_neigh_get_state(neigh) != TNL_NEIGH_REACHABLE) {
+            continue;
+        }
         atomic_read_explicit(&neigh->expires, &curr_exp,
                              memory_order_acquire);
         if (new_exp < curr_exp) {
@@ -439,13 +593,18 @@ tnl_neigh_cache_retrans_time(struct unixctl_conn *conn, 
int argc,
     new_exp = time_msec() + retrans_time;
 
     CMAP_FOR_EACH (neigh, cmap_node, &table) {
-        if (tnl_neigh_is_complete(neigh)) {
+        atomic_llong *deadline;
+        enum tnl_neigh_state state = tnl_neigh_get_state(neigh);
+
+        if (state == TNL_NEIGH_REACHABLE) {
             continue;
         }
-        atomic_read_explicit(&neigh->expires, &curr_exp,
+        deadline = state == TNL_NEIGH_STALE
+                   ? &neigh->probe_expires : &neigh->expires;
+        atomic_read_explicit(deadline, &curr_exp,
                              memory_order_acquire);
         if (new_exp < curr_exp) {
-            atomic_store_explicit(&neigh->expires, new_exp,
+            atomic_store_explicit(deadline, new_exp,
                                   memory_order_release);
         }
     }
@@ -511,14 +670,16 @@ tnl_neigh_cache_show(struct unixctl_conn *conn, int argc 
OVS_UNUSED,
         need_ws = INET6_ADDRSTRLEN - (ds.length - start_len);
         ds_put_char_multiple(&ds, ' ', need_ws);
 
-        if (tnl_neigh_is_complete(neigh)) {
+        enum tnl_neigh_state state = tnl_neigh_get_state(neigh);
+
+        if (state != TNL_NEIGH_INCOMPLETE) {
             ds_put_format(&ds, ETH_ADDR_FMT"   %s",
                           ETH_ADDR_ARGS(neigh->mac), neigh->br_name);
         } else {
             ds_put_format(&ds, "                    %s INCOMPLETE",
                           neigh->br_name);
         }
-        if (tnl_neigh_expired(neigh)) {
+        if (state == TNL_NEIGH_STALE) {
             ds_put_format(&ds, " STALE");
         }
         ds_put_char(&ds, '\n');
diff --git a/lib/tnl-neigh-cache.h b/lib/tnl-neigh-cache.h
index e16155b4d..f98707964 100644
--- a/lib/tnl-neigh-cache.h
+++ b/lib/tnl-neigh-cache.h
@@ -36,7 +36,8 @@ int tnl_neigh_snoop(const struct flow *flow, struct 
flow_wildcards *wc,
 void tnl_neigh_set(const char name[IFNAMSIZ], const struct in6_addr *dst,
                    const struct eth_addr mac);
 int tnl_neigh_lookup(const char dev_name[IFNAMSIZ], const struct in6_addr *dst,
-                     struct eth_addr *mac, bool insert_partial);
+                     struct eth_addr *mac, bool insert_partial, bool *stale,
+                     bool *probe);
 void tnl_neigh_cache_init(void);
 void tnl_neigh_cache_run(void);
 void tnl_neigh_flush(const char dev_name[IFNAMSIZ]);
diff --git a/ofproto/ofproto-dpif-xlate-cache.c 
b/ofproto/ofproto-dpif-xlate-cache.c
index cb37e2462..54e9e7bd1 100644
--- a/ofproto/ofproto-dpif-xlate-cache.c
+++ b/ofproto/ofproto-dpif-xlate-cache.c
@@ -152,7 +152,8 @@ xlate_push_stats_entry(struct xc_entry *entry,
     case XC_TNL_NEIGH:
         /* Lookup neighbor to avoid timeout. */
         tnl_neigh_lookup(entry->tnl_neigh_cache.br_name,
-                         &entry->tnl_neigh_cache.d_ipv6, &dmac, false);
+                         &entry->tnl_neigh_cache.d_ipv6, &dmac, false,
+                         NULL, NULL);
         break;
     case XC_TUNNEL_HEADER:
         if (entry->tunnel_hdr.operation == ADD) {
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index 764dbd662..b3a65f676 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3792,6 +3792,30 @@ tnl_send_arp_request(struct xlate_ctx *ctx, const struct 
xport *out_dev,
     dp_packet_uninit(&packet);
 }
 
+static void
+tnl_send_neigh_request(struct xlate_ctx *ctx, const struct xport *out_dev,
+                       const struct eth_addr eth_src,
+                       const struct in6_addr *ip_src,
+                       const struct in6_addr *ip_dst)
+{
+    struct in6_addr nh_src = in6addr_any;
+    struct in6_addr nh_dst = *ip_dst;
+    ovs_be32 ip4_dst = in6_addr_get_mapped_ipv4(ip_dst);
+
+    COVERAGE_INC(xlate_actions_neigh_sent);
+    if (ovs_router_get_netdev_source_address(
+            ip_dst, netdev_get_name(out_dev->netdev), &nh_src)) {
+        nh_src = *ip_src;
+    }
+
+    if (ip4_dst) {
+        tnl_send_arp_request(ctx, out_dev, eth_src,
+                             in6_addr_get_mapped_ipv4(&nh_src), ip4_dst);
+    } else {
+        tnl_send_nd_request(ctx, out_dev, eth_src, &nh_src, &nh_dst);
+    }
+}
+
 static void
 propagate_tunnel_data_to_flow__(struct flow *dst_flow,
                                 const struct flow *src_flow,
@@ -3894,6 +3918,8 @@ native_tunnel_output(struct xlate_ctx *ctx, const struct 
xport *xport,
     struct in6_addr d_ip6 = in6addr_any;
     struct eth_addr smac;
     struct eth_addr dmac;
+    bool stale;
+    bool probe;
     int err;
     char buf_sip6[INET6_ADDRSTRLEN];
     char buf_dip6[INET6_ADDRSTRLEN];
@@ -3943,10 +3969,9 @@ native_tunnel_output(struct xlate_ctx *ctx, const struct 
xport *xport,
         s_ip = in6_addr_get_mapped_ipv4(&s_ip6);
     }
 
-    err = tnl_neigh_lookup(out_dev->xbridge->name, &d_ip6, &dmac, true);
+    err = tnl_neigh_lookup(out_dev->xbridge->name, &d_ip6, &dmac, true,
+                           &stale, &probe);
     if (err) {
-        struct in6_addr nh_s_ip6 = in6addr_any;
-
         put_cloned_drop_action(ctx->xbridge->ofproto, ctx->odp_actions,
                                XLATE_TUNNEL_NEIGH_CACHE_MISS,
                                !is_last_action);
@@ -3962,26 +3987,21 @@ native_tunnel_output(struct xlate_ctx *ctx, const 
struct xport *xport,
                      "neighbor cache miss for %s on bridge %s, "
                      "sending %s request",
                      buf_dip6, out_dev->xbridge->name, d_ip ? "ARP" : "ND");
-        COVERAGE_INC(xlate_actions_neigh_sent);
-
-        err = ovs_router_get_netdev_source_address(
-            &d_ip6, netdev_get_name(out_dev->netdev), &nh_s_ip6);
-
-        if (err) {
-            nh_s_ip6 = s_ip6;
-        }
-
-        if (d_ip) {
-            ovs_be32 nh_s_ip;
-
-            nh_s_ip = in6_addr_get_mapped_ipv4(&nh_s_ip6);
-            tnl_send_arp_request(ctx, out_dev, smac, nh_s_ip, d_ip);
-        } else {
-            tnl_send_nd_request(ctx, out_dev, smac, &nh_s_ip6, &d_ip6);
-        }
+        tnl_send_neigh_request(ctx, out_dev, smac, &s_ip6, &d_ip6);
         return err;
     }
 
+    if (stale) {
+        ctx->xout->avoid_caching = true;
+    }
+    if (probe) {
+        xlate_report(ctx, OFT_DETAIL,
+                     "neighbor cache stale for %s on bridge %s, "
+                     "sending %s request",
+                     buf_dip6, out_dev->xbridge->name, d_ip ? "ARP" : "ND");
+        tnl_send_neigh_request(ctx, out_dev, smac, &s_ip6, &d_ip6);
+    }
+
     if (ctx->xin->xcache) {
         struct xc_entry *entry;
 
diff --git a/tests/tunnel-push-pop-ipv6.at b/tests/tunnel-push-pop-ipv6.at
index ec757f7d7..65a66745e 100644
--- a/tests/tunnel-push-pop-ipv6.at
+++ b/tests/tunnel-push-pop-ipv6.at
@@ -393,9 +393,51 @@ AT_CHECK([ovs-appctl tnl/neigh/show | grep br0 | sort], 
[0], [dnl
 
 ovs-appctl time/warp 5000
 
-dnl Check the entry has been removed
+dnl Check that an idle stale entry is retained indefinitely.
 AT_CHECK([ovs-appctl tnl/neigh/show | grep br0 | sort], [0], [dnl
+2001:cafe::92                                 aa:bb:cc:00:00:01   br0 STALE
 ])
+ovs-appctl time/warp 15000
+AT_CHECK([ovs-appctl tnl/neigh/show | grep br0 | sort], [0], [dnl
+2001:cafe::92                                 aa:bb:cc:00:00:01   br0 STALE
+])
+
+dnl Check that the MAC remains usable while OVS refreshes the stale entry.
+AT_CHECK([ovs-ofctl add-flow int-br \
+          "priority=100,in_port=LOCAL,actions=2"])
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [stdout])
+AT_CHECK([tail -1 stdout | grep -q '^Datapath actions: tnl_push'], [0])
+AT_CHECK([grep -q 'aa:bb:cc:00:00:01' stdout], [0])
+
+dnl A second lookup uses the stale MAC without sending another ND request.
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [ignore])
+OVS_WAIT_UNTIL([test `ovs-pcap p0.pcap | grep -c \
+  92aa55aa55000086dd6000000000203aff2001cafe` -ge 2])
+AT_CHECK([ovs-pcap p0.pcap | grep -c \
+  92aa55aa55000086dd6000000000203aff2001cafe], [0], [2
+])
+
+dnl Remove the stale entry after three unanswered ND probes.
+ovs-appctl time/warp 5000
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [ignore])
+ovs-appctl time/warp 5000
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [ignore])
+OVS_WAIT_UNTIL([test `ovs-pcap p0.pcap | grep -c \
+  92aa55aa55000086dd6000000000203aff2001cafe` -ge 4])
+ovs-appctl time/warp 5000
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 2001:cafe::92], [1], [])
 
 dnl Restore the aging time to 900s (default)
 AT_CHECK([ovs-appctl tnl/neigh/aging 900], [0], [OK
diff --git a/tests/tunnel-push-pop.at b/tests/tunnel-push-pop.at
index ab393cfc7..56d2cc324 100644
--- a/tests/tunnel-push-pop.at
+++ b/tests/tunnel-push-pop.at
@@ -346,9 +346,48 @@ AT_CHECK([ovs-appctl tnl/neigh/show | grep br0 | sort], 
[0], [dnl
 
 ovs-appctl time/warp 5000
 
-dnl Check the entry has been removed
+dnl Check that an idle stale entry is retained indefinitely.
 AT_CHECK([ovs-appctl tnl/neigh/show | grep br0 | sort], [0], [dnl
+1.1.2.92                                      aa:bb:cc:00:00:01   br0 STALE
 ])
+ovs-appctl time/warp 15000
+AT_CHECK([ovs-appctl tnl/neigh/show | grep br0 | sort], [0], [dnl
+1.1.2.92                                      aa:bb:cc:00:00:01   br0 STALE
+])
+
+dnl Check that the MAC remains usable while OVS refreshes the stale entry.
+AT_CHECK([ovs-ofctl add-flow int-br \
+          "priority=100,in_port=LOCAL,actions=2"])
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [stdout])
+AT_CHECK([tail -1 stdout | grep -q '^Datapath actions: tnl_push'], [0])
+AT_CHECK([grep -q 'aa:bb:cc:00:00:01' stdout], [0])
+
+dnl A second lookup uses the stale MAC without sending another ARP request.
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [ignore])
+OVS_WAIT_UNTIL([test `ovs-pcap p0.pcap | grep -c 101025c` -ge 2])
+AT_CHECK([ovs-pcap p0.pcap | grep -c 101025c], [0], [2
+])
+
+dnl Remove the stale entry after three unanswered ARP probes.
+ovs-appctl time/warp 5000
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [ignore])
+ovs-appctl time/warp 5000
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(int-br),]dnl
+  [eth(src=aa:55:aa:55:00:00,dst=f8:bc:12:ff:ff:ff),eth_type(0x0800),]dnl
+  [ipv4(src=1.1.3.92,dst=1.1.3.88,proto=1,tos=0,ttl=64,frag=no),]dnl
+  [icmp(type=0,code=0)"], [0], [ignore])
+OVS_WAIT_UNTIL([test `ovs-pcap p0.pcap | grep -c 101025c` -ge 4])
+ovs-appctl time/warp 5000
+AT_CHECK([ovs-appctl tnl/neigh/show | grep br0], [1], [])
 
 dnl Restore the aging time to 900s (default)
 AT_CHECK([ovs-appctl tnl/neigh/aging 900], [0], [OK
-- 
2.55.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to