Native tunnels snoop the outer Ethernet source MAC of every IPv4/IPv6 data packet in terminate_native_tunnel() and call tnl_neigh_set(). That lets BFD recover when the remote VTEP NIC MAC changes, so the default stays enabled.
The outer source MAC is the last-hop MAC, not necessarily the remote VTEP MAC. Under ECMP or MLAG, packets from one peer can flap last-hop MACs. Each mismatch deletes the tnl-neigh entry and seq_change()s tnl_conf_seq, which revalidates every datapath flow. Add Interface option neigh_snoop (default true). When false on the OpenFlow tunnel port that receive matching selects for the packet, skip the data-packet tnl_neigh_set() path. At terminate time the packet is still on the physical port, so build receive tunnel metadata from the outer IP and the encapsulation key (parsed from the native vport type: vxlan, geneve, or gtpu) and reuse tnl_find() the same way tnl_port_receive() does. ARP/ND and GARP snooping via tnl_neigh_snoop() is unchanged. This option is userspace native tunneling only. Signed-off-by: Girish Kumar <[email protected]> --- v2: Resend via git send-email. v1 failed git am (Gmail linkified tests/tunnel-push-pop*.at paths). NEWS | 6 ++ lib/netdev-vport.c | 10 +++ lib/netdev.h | 1 + lib/tnl-ports.c | 27 +++++++- lib/tnl-ports.h | 1 + ofproto/ofproto-dpif-xlate.c | 116 +++++++++++++++++++++++++++++++++- ofproto/tunnel.c | 36 +++++++++++ ofproto/tunnel.h | 3 + tests/tunnel-push-pop-ipv6.at | 48 ++++++++++++++ tests/tunnel-push-pop.at | 48 ++++++++++++++ vswitchd/vswitch.xml | 13 ++++ 11 files changed, 306 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index de1a030..a57914f 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,11 @@ Post-v4.0.0 -------------------- + - Userspace datapath: + * Native tunnels accept a per-tunnel Interface 'neigh_snoop' option + (default true). Setting 'options:neigh_snoop=false' skips updating + the tunnel neighbor cache from the outer source MAC of encapsulated + data packets on that Interface. Userspace native tunneling only. + ARP/ND snooping is unchanged. v4.0.0 - 17 Aug 2026 diff --git a/lib/netdev-vport.c b/lib/netdev-vport.c index d11269d..a28af1c 100644 --- a/lib/netdev-vport.c +++ b/lib/netdev-vport.c @@ -228,6 +228,7 @@ netdev_vport_construct(struct netdev *netdev_) } tnl_cfg->dont_fragment = true; + tnl_cfg->neigh_snoop = true; tnl_cfg->ttl = DEFAULT_TTL; ovsrcu_set(&dev->tnl_cfg, tnl_cfg); @@ -638,6 +639,7 @@ set_tunnel_config(struct netdev *dev_, const struct smap *args, char **errp) needs_dst_port = netdev_vport_needs_dst_port(dev_); tnl_cfg.dont_fragment = true; + tnl_cfg.neigh_snoop = true; SMAP_FOR_EACH (node, args) { if (!strcmp(node->key, "remote_ip")) { @@ -697,6 +699,10 @@ set_tunnel_config(struct netdev *dev_, const struct smap *args, char **errp) if (!strcmp(node->value, "false")) { tnl_cfg.dont_fragment = false; } + } else if (!strcmp(node->key, "neigh_snoop")) { + if (!strcmp(node->value, "false")) { + tnl_cfg.neigh_snoop = false; + } } else if (!strcmp(node->key, "key") || !strcmp(node->key, "in_key") || !strcmp(node->key, "out_key") || @@ -1045,6 +1051,10 @@ get_tunnel_config(const struct netdev *dev, struct smap *args) smap_add(args, "df_default", "false"); } + if (!tnl_cfg->neigh_snoop) { + smap_add(args, "neigh_snoop", "false"); + } + if (tnl_cfg->set_egress_pkt_mark) { smap_add_format(args, "egress_pkt_mark", "%"PRIu32, tnl_cfg->egress_pkt_mark); diff --git a/lib/netdev.h b/lib/netdev.h index 5e093b0..42c82bd 100644 --- a/lib/netdev.h +++ b/lib/netdev.h @@ -159,6 +159,7 @@ struct netdev_tunnel_config { enum netdev_tnl_csum csum; bool dont_fragment; + bool neigh_snoop; enum netdev_pt_mode pt_mode; bool set_seq; diff --git a/lib/tnl-ports.c b/lib/tnl-ports.c index 56119b3..2e9030d 100644 --- a/lib/tnl-ports.c +++ b/lib/tnl-ports.c @@ -53,6 +53,7 @@ struct tnl_port { struct ovs_refcount ref_cnt; ovs_be16 tp_port; uint8_t nw_proto; + const char *type; char dev_name[IFNAMSIZ]; struct ovs_list node; }; @@ -180,7 +181,8 @@ tnl_type_to_nw_proto(const char type[], uint8_t nw_protos[2]) static void tnl_port_map_insert__(odp_port_t port, ovs_be16 tp_port, - const char dev_name[], uint8_t nw_proto) + const char dev_name[], uint8_t nw_proto, + const char type[]) { struct tnl_port *p; struct ip_device *ip_dev; @@ -197,6 +199,7 @@ tnl_port_map_insert__(odp_port_t port, ovs_be16 tp_port, p->port = port; p->tp_port = tp_port; p->nw_proto = nw_proto; + p->type = type; ovs_strlcpy(p->dev_name, dev_name, sizeof p->dev_name); ovs_refcount_init(&p->ref_cnt); ovs_list_insert(&port_list, &p->node); @@ -220,7 +223,8 @@ tnl_port_map_insert(odp_port_t port, ovs_be16 tp_port, for (i = 0; i < 2; i++) { if (nw_protos[i]) { - tnl_port_map_insert__(port, tp_port, dev_name, nw_protos[i]); + tnl_port_map_insert__(port, tp_port, dev_name, nw_protos[i], + type); } } } @@ -308,6 +312,25 @@ tnl_port_map_lookup(struct flow *flow, struct flow_wildcards *wc) return (cr) ? tnl_port_cast(cr)->portno : ODPP_NONE; } +/* Returns the native vport type ("vxlan", "geneve", "gtpu", ...) for + * 'port', or NULL if 'port' is not in the map. */ +const char * +tnl_port_map_get_type(odp_port_t port) +{ + const char *type = NULL; + struct tnl_port *p; + + ovs_mutex_lock(&mutex); + LIST_FOR_EACH (p, node, &port_list) { + if (p->port == port) { + type = p->type; + break; + } + } + ovs_mutex_unlock(&mutex); + return type; +} + static void tnl_port_show_v(struct ds *ds) { diff --git a/lib/tnl-ports.h b/lib/tnl-ports.h index 61ca0f8..284b117 100644 --- a/lib/tnl-ports.h +++ b/lib/tnl-ports.h @@ -25,6 +25,7 @@ #include "util.h" odp_port_t tnl_port_map_lookup(struct flow *flow, struct flow_wildcards *wc); +const char *tnl_port_map_get_type(odp_port_t); void tnl_port_map_insert(odp_port_t, ovs_be16 tp_port, const char dev_name[], const char type[]); diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c index 764dbd6..11de7a3 100644 --- a/ofproto/ofproto-dpif-xlate.c +++ b/ofproto/ofproto-dpif-xlate.c @@ -4435,6 +4435,119 @@ static bool check_neighbor_reply(struct xlate_ctx *ctx, struct flow *flow) return false; } +/* Encapsulation key from a native-tunnel packet still on the phy port. + * 'type' is the native vport type for tnl_odp_port (vxlan, geneve, gtpu). + * Geneve stores the VNI in the same 4 bytes as VXLAN; GTP does not. + * GRE uses the optional key. Returns 0 if the type is unknown or the + * key is absent. */ +static ovs_be64 +native_tnl_recv_key(const struct dp_packet *packet, const struct flow *flow, + const char *type) +{ + const void *l4; + size_t l4_size; + + if (!packet) { + return 0; + } + + l4 = dp_packet_l4(packet); + if (!l4) { + return 0; + } + l4_size = dp_packet_l4_size(packet); + + if (flow->nw_proto == IPPROTO_UDP) { + if (!type) { + return 0; + } + + if (!strcmp(type, "vxlan")) { + const struct vxlanhdr *vxh; + + if (l4_size < UDP_HEADER_LEN + sizeof *vxh) { + return 0; + } + vxh = ALIGNED_CAST(const struct vxlanhdr *, + (const char *) l4 + UDP_HEADER_LEN); + return htonll(ntohl(get_16aligned_be32(&vxh->vx_vni)) >> 8); + } + + if (!strcmp(type, "geneve")) { + const struct genevehdr *gnh; + + if (l4_size < UDP_HEADER_LEN + sizeof *gnh) { + return 0; + } + gnh = ALIGNED_CAST(const struct genevehdr *, + (const char *) l4 + UDP_HEADER_LEN); + return htonll(ntohl(get_16aligned_be32(&gnh->vni)) >> 8); + } + + if (!strcmp(type, "gtpu")) { + const struct gtpuhdr *gtph; + + if (l4_size < UDP_HEADER_LEN + sizeof *gtph) { + return 0; + } + gtph = ALIGNED_CAST(const struct gtpuhdr *, + (const char *) l4 + UDP_HEADER_LEN); + return be32_to_be64(get_16aligned_be32(>ph->teid)); + } + + return 0; + } + + if (flow->nw_proto == IPPROTO_GRE) { + const struct gre_base_hdr *greh = l4; + const ovs_16aligned_be32 *options; + size_t hlen = sizeof *greh; + + if (l4_size < hlen) { + return 0; + } + if (greh->flags & htons(GRE_CSUM)) { + hlen += 4; + } + if (!(greh->flags & htons(GRE_KEY))) { + return 0; + } + hlen += 4; + if (l4_size < hlen) { + return 0; + } + options = ALIGNED_CAST(const ovs_16aligned_be32 *, + (const char *) greh + hlen - 4); + return be32_to_be64(get_16aligned_be32(options)); + } + + return 0; +} + +/* options:neigh_snoop from the OpenFlow tunnel port that receive matching + * would select for this packet. Default on. */ +static bool +native_tnl_neigh_snoop(const struct xlate_ctx *ctx, const struct flow *flow, + odp_port_t tnl_odp_port) +{ + const struct netdev_tunnel_config *cfg; + const struct ofport_dpif *ofport; + const struct xport *xport; + const char *type; + ovs_be64 tun_id; + + type = tnl_port_map_get_type(tnl_odp_port); + tun_id = native_tnl_recv_key(ctx->xin->packet, flow, type); + ofport = tnl_port_receive_native(flow, tnl_odp_port, tun_id); + xport = xport_lookup(ctx->xcfg, ofport); + if (!xport || !xport->netdev) { + return true; + } + + cfg = netdev_get_tunnel_config(xport->netdev); + return !cfg || cfg->neigh_snoop; +} + static bool terminate_native_tunnel(struct xlate_ctx *ctx, const struct xport *xport, struct flow *flow, struct flow_wildcards *wc, @@ -4460,7 +4573,8 @@ terminate_native_tunnel(struct xlate_ctx *ctx, const struct xport *xport, ctx->xin->allow_side_effects); } else if (*tnl_port != ODPP_NONE && ctx->xin->allow_side_effects && - dl_type_is_ip_any(flow->dl_type)) { + dl_type_is_ip_any(flow->dl_type) && + native_tnl_neigh_snoop(ctx, flow, *tnl_port)) { struct eth_addr mac = flow->dl_src; struct in6_addr s_ip6; diff --git a/ofproto/tunnel.c b/ofproto/tunnel.c index d8a2895..6001d50 100644 --- a/ofproto/tunnel.c +++ b/ofproto/tunnel.c @@ -335,6 +335,42 @@ out: return ofport; } +/* Like tnl_port_receive(), but 'flow' still has the outer headers from the + * physical port. 'tnl_odp_port' is the shared native vport from + * tnl_port_map_lookup() and 'tun_id' is the encapsulation key. */ +const struct ofport_dpif * +tnl_port_receive_native(const struct flow *flow, odp_port_t tnl_odp_port, + ovs_be64 tun_id) OVS_EXCLUDED(rwlock) +{ + const struct ofport_dpif *ofport = NULL; + struct tnl_port *tnl_port; + struct flow recv_flow; + + memset(&recv_flow, 0, sizeof recv_flow); + recv_flow.packet_type = flow->packet_type; + recv_flow.in_port.odp_port = tnl_odp_port; + recv_flow.tunnel.tun_id = tun_id; + recv_flow.tunnel.flags = FLOW_TNL_F_KEY; + + if (flow->dl_type == htons(ETH_TYPE_IP)) { + recv_flow.tunnel.ip_src = flow->nw_src; + recv_flow.tunnel.ip_dst = flow->nw_dst; + } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) { + recv_flow.tunnel.ipv6_src = flow->ipv6_src; + recv_flow.tunnel.ipv6_dst = flow->ipv6_dst; + } else { + return NULL; + } + + fat_rwlock_rdlock(&rwlock); + tnl_port = tnl_find(&recv_flow); + if (tnl_port) { + ofport = tnl_port->ofport; + } + fat_rwlock_unlock(&rwlock); + return ofport; +} + /* Should be called at the beginning of action translation to initialize * wildcards and perform any actions based on receiving on tunnel port. * diff --git a/ofproto/tunnel.h b/ofproto/tunnel.h index 323f3fa..c70e899 100644 --- a/ofproto/tunnel.h +++ b/ofproto/tunnel.h @@ -40,6 +40,9 @@ int tnl_port_add(const struct ofport_dpif *, const struct netdev *, void tnl_port_del(const struct ofport_dpif *, odp_port_t); const struct ofport_dpif *tnl_port_receive(const struct flow *); +const struct ofport_dpif *tnl_port_receive_native(const struct flow *, + odp_port_t tnl_odp_port, + ovs_be64 tun_id); void tnl_wc_init(struct flow *, struct flow_wildcards *); bool tnl_process_ecn(struct flow *); odp_port_t tnl_port_send(const struct ofport_dpif *, struct flow *, diff --git a/tests/tunnel-push-pop-ipv6.at b/tests/tunnel-push-pop-ipv6.at index ec757f7..38dd077 100644 --- a/tests/tunnel-push-pop-ipv6.at +++ b/tests/tunnel-push-pop-ipv6.at @@ -656,6 +656,54 @@ AT_CHECK([ovs-appctl tnl/arp/show | tail -n+3 | sort], [0], [dnl 2001:cafe::93 f8:bc:12:44:34:b7 br0 ]) +dnl Disable data-packet neigh snoop on t2 only. A different outer MAC +dnl on that Interface must not replace the complete tnl-neigh entry. +AT_CHECK([ovs-vsctl set Interface t2 options:neigh_snoop=false]) +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'aa55aa550000f8bc1244cafe86dd60000000003a11402001cafe0000000000000000000000922001cafe000000000000000000000088c85312b5003abc700c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/arp/show | tail -n+3 | sort], [0], [dnl +2001:cafe::92 f8:bc:12:44:34:b6 br0 +2001:cafe::93 f8:bc:12:44:34:b7 br0 +]) + +dnl t3 stays default-on. A data packet for 2001:cafe::93 must still update. +dnl Use a different outer MAC so this misses the t2 megaflow and re-xlates. +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'aa55aa550000f8bc1244caff86dd60000000003a11402001cafe0000000000000000000000932001cafe000000000000000000000088c85312b5003abc700c00000300000000ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/arp/show | tail -n+3 | sort], [0], [dnl +2001:cafe::92 f8:bc:12:44:34:b6 br0 +2001:cafe::93 f8:bc:12:44:ca:ff br0 +]) + +dnl ND snooping still updates the cache when neigh_snoop is false. +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'in_port(1),eth(src=f8:bc:12:44:ca:fe,dst=aa:55:aa:55:00:00),eth_type(0x86dd),ipv6(src=2001:cafe::92,dst=2001:cafe::88,label=0,proto=58,tclass=0,hlimit=255,frag=no),icmpv6(type=136,code=0),nd(target=2001:cafe::92,sll=00:00:00:00:00:00,tll=f8:bc:12:44:ca:fe)']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/arp/show | tail -n+3 | sort], [0], [dnl +2001:cafe::92 f8:bc:12:44:ca:fe br0 +2001:cafe::93 f8:bc:12:44:ca:ff br0 +]) + +dnl Restore the complete entries used by later push checks. +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'in_port(1),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:00),eth_type(0x86dd),ipv6(src=2001:cafe::92,dst=2001:cafe::88,label=0,proto=58,tclass=0,hlimit=255,frag=no),icmpv6(type=136,code=0),nd(target=2001:cafe::92,sll=00:00:00:00:00:00,tll=f8:bc:12:44:34:b6)']) +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'in_port(1),eth(src=f8:bc:12:44:34:b7,dst=aa:55:aa:55:00:00),eth_type(0x86dd),ipv6(src=2001:cafe::93,dst=2001:cafe::88,label=0,proto=58,tclass=0,hlimit=255,frag=no),icmpv6(type=136,code=0),nd(target=2001:cafe::93,sll=00:00:00:00:00:00,tll=f8:bc:12:44:34:b7)']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/arp/show | tail -n+3 | sort], [0], [dnl +2001:cafe::92 f8:bc:12:44:34:b6 br0 +2001:cafe::93 f8:bc:12:44:34:b7 br0 +]) + dnl Disable checksum from VXLAN port. AT_CHECK([ovs-vsctl set Interface t3 options:csum=false]) AT_CHECK([ovs-ofctl del-flows int-br]) diff --git a/tests/tunnel-push-pop.at b/tests/tunnel-push-pop.at index ab393cf..79cec86 100644 --- a/tests/tunnel-push-pop.at +++ b/tests/tunnel-push-pop.at @@ -759,6 +759,54 @@ AT_CHECK([ovs-appctl tnl/neigh/show | tail -n+3 | sort], [0], [dnl 1.1.2.93 f8:bc:12:44:34:b7 br0 ]) +dnl Disable data-packet neigh snoop on t2 only. A different outer MAC +dnl on that Interface must not replace the complete tnl-neigh entry. +AT_CHECK([ovs-vsctl set Interface t2 options:neigh_snoop=false]) +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'aa55aa550000f8bc1244cafe08004500004e00010000401173e90101025c01010258c85312b5003a8cd40c00000300007b00ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/neigh/show | tail -n+3 | sort], [0], [dnl +1.1.2.92 f8:bc:12:44:34:b6 br0 +1.1.2.93 f8:bc:12:44:34:b7 br0 +]) + +dnl t3 stays default-on. A data packet for 1.1.2.93 must still update. +dnl Use a different outer MAC so this misses the t2 megaflow and re-xlates. +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'aa55aa550000f8bc1244caff08004500004e00010000401173e80101025d01010258c85312b5003a00000c00000300000000ffffffffffff00000000000008004500001c0001000040117cce7f0000017f0000010035003500080172']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/neigh/show | tail -n+3 | sort], [0], [dnl +1.1.2.92 f8:bc:12:44:34:b6 br0 +1.1.2.93 f8:bc:12:44:ca:ff br0 +]) + +dnl ARP snooping still updates the cache when neigh_snoop is false. +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'recirc_id(0),in_port(100),eth(src=f8:bc:12:44:ca:fe,dst=ff:ff:ff:ff:ff:ff),eth_type(0x0806),arp(sip=1.1.2.92,tip=1.1.2.88,op=2,sha=f8:bc:12:44:ca:fe,tha=00:00:00:00:00:00)']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/neigh/show | tail -n+3 | sort], [0], [dnl +1.1.2.92 f8:bc:12:44:ca:fe br0 +1.1.2.93 f8:bc:12:44:ca:ff br0 +]) + +dnl Restore the complete entries used by later push checks. +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'recirc_id(0),in_port(100),eth(src=f8:bc:12:44:34:b6,dst=ff:ff:ff:ff:ff:ff),eth_type(0x0806),arp(sip=1.1.2.92,tip=1.1.2.88,op=2,sha=f8:bc:12:44:34:b6,tha=00:00:00:00:00:00)']) +AT_CHECK([ovs-appctl netdev-dummy/receive p0 'recirc_id(0),in_port(100),eth(src=f8:bc:12:44:34:b7,dst=ff:ff:ff:ff:ff:ff),eth_type(0x0806),arp(sip=1.1.2.93,tip=1.1.2.88,op=2,sha=f8:bc:12:44:34:b7,tha=00:00:00:00:00:00)']) + +ovs-appctl time/warp 1000 +ovs-appctl time/warp 1000 + +AT_CHECK([ovs-appctl tnl/neigh/show | tail -n+3 | sort], [0], [dnl +1.1.2.92 f8:bc:12:44:34:b6 br0 +1.1.2.93 f8:bc:12:44:34:b7 br0 +]) + ovs-appctl time/warp 10000 AT_CHECK([ovs-vsctl del-port int-br t3 \ diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml index 4eec70f..acd1bd0 100644 --- a/vswitchd/vswitch.xml +++ b/vswitchd/vswitch.xml @@ -3102,6 +3102,19 @@ to <code>false</code> to disable. </column> + <column name="options" key="neigh_snoop" + type='{"type": "boolean"}'> + Optional. Userspace native tunneling only. If enabled (the + default), the userspace datapath updates the native-tunnel neighbor + cache from the outer Ethernet source MAC of encapsulated IPv4 and + IPv6 data packets received on this tunnel Interface. That lets + BFD recover after the remote VTEP NIC MAC changes. Set to + <code>false</code> to skip that data-packet snooping, which can + otherwise flap the neighbor entry when ECMP or MLAG changes the + last-hop MAC. ARP, GARP, and Neighbor Discovery snooping is not + affected. + </column> + <column name="options" key="egress_pkt_mark"> Optional. The pkt_mark to be set on the encapsulating packet. This option sets packet mark for the tunnel endpoint for all tunnel packets -- 2.47.3 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
