Userspace native tunneling snoops the outer Ethernet source of an
encapsulated IP packet into tnl_neigh_set(), keyed by the remote VTEP
IP. That MAC is the last hop. When the VTEP is routed
(ovs/route/lookup reports a gateway), the last hop is a router or MLAG
MAC. Learning it flaps the neighbor cache, changes tnl_conf_seq, and
storms the revalidator. The same happens if the remote is on-link on
a different bridge than the one that received the packet.
When the VTEP is on-link through the receiving bridge, keep snooping
so BFD can recover after a real remote NIC MAC change.
On the data-packet tnl_neigh_set path only, look up the underlay route
for the outer source IP, using the outer destination as the return-
route source so source-based routing is honored. Learn only when the
lookup succeeds, the route has no gateway, and the output device
belongs to the receiving bridge (resolving member-port names, not just
the bridge name). A failed lookup does not snoop. ARP/GARP and IPv6
Neighbor Advertisement snooping is unchanged.
Learning still happens only on slow-path translation of an encapsulated
packet, not on every packet.
Fixes: ec2aa2ab4689 ("ofproto-dpif-xlate: Snoop ingress packets and update
neigh cache if needed.")
Signed-off-by: Girish Kumar <[email protected]>
---
v3:
- Drop the neigh_snoop knob from v2.
- Skip data-packet tnl_neigh_set unless the remote is on-link
through the receiving bridge (no gateway; out_dev maps to
ctx->xbridge, including member-port names).
- Pass the outer destination as ovs_router_lookup src so
source-based routing matches tunnel output.
- Failed lookup does not snoop. ARP/ND unchanged.
- Tests: on-link vs routed, other-bridge, policy-routed
(IPv4 and IPv6).
v2: Resend via git send-email. v1 failed git am (Gmail
linkified tests/tunnel-push-pop*.at paths).
AUTHORS.rst | 1 +
NEWS | 10 +++
ofproto/ofproto-dpif-xlate.c | 64 ++++++++++++++++++-
tests/tunnel-push-pop-ipv6.at | 115 ++++++++++++++++++++++++++++++++++
tests/tunnel-push-pop.at | 114 +++++++++++++++++++++++++++++++++
5 files changed, 301 insertions(+), 3 deletions(-)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 0f7445c80fe..79a69ae13e2 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -201,6 +201,7 @@ Gal Sagie [email protected]
Genevieve LEsperance [email protected]
Geoffrey Wossum [email protected]
Gianluca Merlo [email protected]
+Girish Kumar [email protected]
Giuseppe Lettieri [email protected]
Glen Gibb [email protected]
Gowrishankar Muthukrishnan [email protected]
diff --git a/NEWS b/NEWS
index de1a030adfc..e389c5c3f01 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,15 @@
Post-v4.0.0
--------------------
+ - Userspace datapath:
+ * Native tunnel neighbor learning from encapsulated data packets
+ now ignores the outer Ethernet source unless the remote VTEP is
+ on-link through the receiving bridge. Routed last-hop MACs,
+ and remotes on-link on a different bridge, caused cache flaps
+ and revalidator storms. The lookup uses the packet's outer
+ destination as the return-route source so source-based routing
+ is honored. On-link VTEPs on the receiving bridge still
+ update the cache so BFD can recover after a remote NIC MAC
+ change.
v4.0.0 - 17 Aug 2026
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index 764dbd6627a..2f13f9b1d25 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -4435,6 +4435,55 @@ static bool check_neighbor_reply(struct xlate_ctx *ctx,
struct flow *flow)
return false;
}
+/* Map a router output device name to the xbridge that owns it.
+ * ovs_router_lookup() may return a bridge name or a member port name
+ * (system-route fallback). Walk xports, the same resolution
+ * tnl_route_lookup_flow() uses after the bridge-name match. */
+static const struct xbridge *
+xbridge_from_dev_name(const struct xlate_ctx *ctx, const char *dev_name)
+{
+ struct xbridge *xbridge;
+ struct xport *port;
+
+ HMAP_FOR_EACH (xbridge, hmap_node, &ctx->xcfg->xbridges) {
+ HMAP_FOR_EACH (port, ofp_node, &xbridge->xports) {
+ if (!strncmp(netdev_get_name(port->netdev),
+ dev_name, IFNAMSIZ)) {
+ return xbridge;
+ }
+ }
+ }
+ return NULL;
+}
+
+/* True if the underlay route for remote 'ip' is on-link through the
+ * receiving bridge. 'src' is the outer destination (local VTEP), the
+ * return-path source for policy routing; never NULL. Failed lookup,
+ * a next-hop gateway, or an output device owned by another xbridge
+ * is not on-link. IPv4 stores a missing gateway as ::ffff:0.0.0.0,
+ * which ipv6_addr_is_set() treats as set; treat that as no gateway. */
+static bool
+is_onlink(const struct xlate_ctx *ctx, uint32_t mark,
+ const struct in6_addr *ip, const struct in6_addr *src)
+{
+ const struct xbridge *out_xbridge;
+ struct in6_addr src6 = *src;
+ char out_dev[IFNAMSIZ];
+ struct in6_addr gw;
+
+ if (!ovs_router_lookup(mark, ip, out_dev, &src6, &gw)) {
+ return false;
+ }
+
+ if (ipv6_addr_is_set(&gw)
+ && (!IN6_IS_ADDR_V4MAPPED(&gw) || in6_addr_get_mapped_ipv4(&gw))) {
+ return false;
+ }
+
+ out_xbridge = xbridge_from_dev_name(ctx, out_dev);
+ return out_xbridge == ctx->xbridge;
+}
+
static bool
terminate_native_tunnel(struct xlate_ctx *ctx, const struct xport *xport,
struct flow *flow, struct flow_wildcards *wc,
@@ -4461,16 +4510,25 @@ terminate_native_tunnel(struct xlate_ctx *ctx, const
struct xport *xport,
} else if (*tnl_port != ODPP_NONE &&
ctx->xin->allow_side_effects &&
dl_type_is_ip_any(flow->dl_type)) {
- struct eth_addr mac = flow->dl_src;
- struct in6_addr s_ip6;
+ struct in6_addr s_ip6, d_ip6;
if (flow->dl_type == htons(ETH_TYPE_IP)) {
in6_addr_set_mapped_ipv4(&s_ip6, flow->nw_src);
+ in6_addr_set_mapped_ipv4(&d_ip6, flow->nw_dst);
} else {
s_ip6 = flow->ipv6_src;
+ d_ip6 = flow->ipv6_dst;
}
- tnl_neigh_set(ctx->xbridge->name, &s_ip6, mac);
+ /* Learn the outer Ethernet source only when the remote is
+ * on-link through this bridge. A routed last hop is a
+ * router/MLAG MAC; a remote on-link on another bridge is
+ * not this bridge's neighbor. Either would flap
+ * tnl_conf_seq. A failed route lookup does not snoop.
+ * ARP/ND snooping above is unchanged. */
+ if (is_onlink(ctx, flow->pkt_mark, &s_ip6, &d_ip6)) {
+ tnl_neigh_set(ctx->xbridge->name, &s_ip6, flow->dl_src);
+ }
}
}
diff --git a/tests/tunnel-push-pop-ipv6.at b/tests/tunnel-push-pop-ipv6.at
index ec757f7d7e0..8fd82a1f9d0 100644
--- a/tests/tunnel-push-pop-ipv6.at
+++ b/tests/tunnel-push-pop-ipv6.at
@@ -957,3 +957,118 @@ AT_CHECK([grep -q "GENEVE_ACT" stdout])
OVS_VSWITCHD_STOP
AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop_ipv6 - neigh snoop on-link vs routed])
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy
ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=vxlan \
+ options:remote_ip=2001:cafe::92 options:key=123
ofport_request=2 \
+ -- add-port int-br t2 -- set Interface t2 type=vxlan \
+ options:remote_ip=2001:db8:1::92 options:key=123
ofport_request=3])
+
+dnl On-link underlay for 2001:cafe::/64. Routed VTEP 2001:db8:1::92 via
+dnl 2001:cafe::1.
+AT_CHECK([ovs-appctl netdev-dummy/ip6addr br0 2001:cafe::88/64], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/add 2001:db8:1::/64 br0 2001:cafe::1], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 2001:cafe::92], [0], [src 2001:cafe::88
+gateway ::
+dev br0
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 2001:db8:1::92], [0], [src 2001:cafe::88
+gateway 2001:cafe::1
+dev br0
+])
+
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 2001:cafe::92 aa:bb:cc:00:00:01], [0],
[OK
+])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 2001:db8:1::92 aa:bb:cc:00:00:02], [0],
[OK
+])
+
+dnl On-link: data packet must still update tnl-neigh (BFD after NIC MAC
change).
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe86dd60000000003a11402001cafe0000000000000000000000922001cafe000000000000000000000088c85312b5003abc700c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 2001:cafe::92], [0], [dnl
+2001:cafe::92 f8:bc:12:44:ca:fe br0
+])
+
+dnl Routed: flapping last-hop MAC must not overwrite the VTEP neigh entry.
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe86dd60000000003a114020010db80001000000000000000000922001cafe000000000000000000000088c85312b5003a79b60c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 2001:db8:1::92], [0], [dnl
+2001:db8:1::92 aa:bb:cc:00:00:02 br0
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop_ipv6 - neigh snoop on-link other bridge])
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy
ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ovs-vsctl add-br br1 -- set bridge br1 datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=vxlan \
+ options:remote_ip=2001:db8:2::92 options:key=123
ofport_request=2])
+
+dnl Remote 2001:db8:2::92 is on-link on br1, not on receiving bridge br0.
+AT_CHECK([ovs-appctl netdev-dummy/ip6addr br0 2001:cafe::88/64], [0], [OK
+])
+AT_CHECK([ovs-appctl netdev-dummy/ip6addr br1 2001:db8:2::88/64], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 2001:db8:2::92 src=2001:cafe::88], [0],
[src 2001:cafe::88
+gateway ::
+dev br1
+])
+
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 2001:db8:2::92 aa:bb:cc:00:00:03], [0],
[OK
+])
+
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe86dd60000000003a114020010db80002000000000000000000922001cafe000000000000000000000088c85312b5003a79b50c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 2001:db8:2::92], [0], [dnl
+2001:db8:2::92 aa:bb:cc:00:00:03 br0
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop_ipv6 - neigh snoop policy routed])
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy
ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=vxlan \
+ options:remote_ip=2001:db8:3::92 options:key=123
ofport_request=2])
+
+dnl Main table: 2001:db8:3::/64 on-link (second address on br0). From
+dnl the local VTEP 2001:cafe::88, table 10 reaches it via a gateway.
+dnl Learning must use the outer destination as src.
+AT_CHECK([ovs-appctl netdev-dummy/ip6addr br0 2001:cafe::88/64], [0], [OK
+])
+AT_CHECK([ovs-appctl netdev-dummy/ip6addr br0 2001:db8:3::88/64], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/add 2001:db8:3::/64 br0 2001:cafe::1 table=10],
[0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/rule/add -6 from=2001:cafe::88/128 table=10],
[0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 2001:db8:3::92], [0], [src 2001:db8:3::88
+gateway ::
+dev br0
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 2001:db8:3::92 src=2001:cafe::88], [0],
[src 2001:cafe::88
+gateway 2001:cafe::1
+dev br0
+])
+
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 2001:db8:3::92 aa:bb:cc:00:00:04], [0],
[OK
+])
+
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe86dd60000000003a114020010db80003000000000000000000922001cafe000000000000000000000088c85312b5003a79b40c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 2001:db8:3::92], [0], [dnl
+2001:db8:3::92 aa:bb:cc:00:00:04 br0
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/tests/tunnel-push-pop.at b/tests/tunnel-push-pop.at
index ab393cfc718..2cc20cf7c80 100644
--- a/tests/tunnel-push-pop.at
+++ b/tests/tunnel-push-pop.at
@@ -1616,3 +1616,117 @@ AT_CHECK([ovs-appctl ofproto/trace ovs-dummy
'in_port(int-br),dnl
OVS_VSWITCHD_STOP
AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop - neigh snoop on-link vs routed])
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy
ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=vxlan \
+ options:remote_ip=1.1.2.92 options:key=123
ofport_request=2 \
+ -- add-port int-br t2 -- set Interface t2 type=vxlan \
+ options:remote_ip=10.1.2.92 options:key=123
ofport_request=3])
+
+dnl On-link underlay for 1.1.2.0/24. Routed VTEP 10.1.2.92 via 1.1.2.1.
+AT_CHECK([ovs-appctl netdev-dummy/ip4addr br0 1.1.2.88/24], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/add 10.1.2.0/24 br0 1.1.2.1], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 1.1.2.92], [0], [src 1.1.2.88
+gateway ::
+dev br0
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 10.1.2.92], [0], [src 1.1.2.88
+gateway 1.1.2.1
+dev br0
+])
+
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 1.1.2.92 aa:bb:cc:00:00:01], [0], [OK
+])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 10.1.2.92 aa:bb:cc:00:00:02], [0], [OK
+])
+
+dnl On-link: data packet must still update tnl-neigh (BFD after NIC MAC
change).
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe08004500004e00010000401173e90101025c01010258c85312b5003a8cd40c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 1.1.2.92], [0], [dnl
+1.1.2.92 f8:bc:12:44:ca:fe br0
+])
+
+dnl Routed: flapping last-hop MAC must not overwrite the VTEP neigh entry.
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe08004500004e0001000040116ae90a01025c01010258c85312b5003a00000c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 10.1.2.92], [0], [dnl
+10.1.2.92 aa:bb:cc:00:00:02 br0
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop - neigh snoop on-link other bridge])
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy
ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ovs-vsctl add-br br1 -- set bridge br1 datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=vxlan \
+ options:remote_ip=10.3.3.92 options:key=123
ofport_request=2])
+
+dnl Remote 10.3.3.92 is on-link on br1, not on the receiving bridge br0.
+AT_CHECK([ovs-appctl netdev-dummy/ip4addr br0 1.1.2.88/24], [0], [OK
+])
+AT_CHECK([ovs-appctl netdev-dummy/ip4addr br1 10.3.3.88/24], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 10.3.3.92 src=1.1.2.88], [0], [src
1.1.2.88
+gateway ::
+dev br1
+])
+
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 10.3.3.92 aa:bb:cc:00:00:03], [0], [OK
+])
+
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe08004500004e00010000401169e70a03035c01010258c85312b5003a00000c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 10.3.3.92], [0], [dnl
+10.3.3.92 aa:bb:cc:00:00:03 br0
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([tunnel_push_pop - neigh snoop policy routed])
+OVS_VSWITCHD_START([add-port br0 p0 -- set Interface p0 type=dummy
ofport_request=1 other-config:hwaddr=aa:55:aa:55:00:00])
+AT_CHECK([ovs-vsctl add-br int-br -- set bridge int-br datapath_type=dummy])
+AT_CHECK([ovs-vsctl add-port int-br t1 -- set Interface t1 type=vxlan \
+ options:remote_ip=10.2.2.92 options:key=123
ofport_request=2])
+
+dnl Main table: 10.2.2.0/24 on-link (second address on br0). From the
+dnl local VTEP 1.1.2.88, table 10 reaches it via a gateway. Learning
+dnl must use the outer destination as src.
+AT_CHECK([ovs-appctl netdev-dummy/ip4addr br0 1.1.2.88/24], [0], [OK
+])
+AT_CHECK([ovs-appctl netdev-dummy/ip4addr br0 10.2.2.88/24], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/add 10.2.2.0/24 br0 1.1.2.1 table=10], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/rule/add from=1.1.2.88/32 table=10], [0], [OK
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 10.2.2.92], [0], [src 10.2.2.88
+gateway ::
+dev br0
+])
+AT_CHECK([ovs-appctl ovs/route/lookup 10.2.2.92 src=1.1.2.88], [0], [src
1.1.2.88
+gateway 1.1.2.1
+dev br0
+])
+
+AT_CHECK([ovs-ofctl add-flow br0 action=normal])
+AT_CHECK([ovs-appctl tnl/neigh/set br0 10.2.2.92 aa:bb:cc:00:00:04], [0], [OK
+])
+
+AT_CHECK([ovs-appctl revalidator/purge])
+AT_CHECK([ovs-appctl netdev-dummy/receive p0
'aa55aa550000f8bc1244cafe08004500004e0001000040116ae80a02025c01010258c85312b5003a00000c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172'])
+AT_CHECK([ovs-appctl tnl/neigh/show | grep 10.2.2.92], [0], [dnl
+10.2.2.92 aa:bb:cc:00:00:04 br0
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev