@@ -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');
@@ -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]);
@@ -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) {
@@ -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;
@@ -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
@@ -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
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 <trozet@nvidia.com> --- 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(-)