Add a new pipeline stage ls_in_arp_nd_pre_lookup (table 26)
and EVPN suppression response flows in ls_in_arp_rsp for
logical switches with EVPN enabled (dynamic-routing-vni set).
The pre-lookup stage calls chk_evpn_arp(arp.tpa) for broadcast
ARP requests and chk_evpn_arp(nd.target) for multicast ND
solicitations. When the EVPN ARP side table has a matching
entry, the resolved MAC is stored in eth.dst and reg9[5] is
set.
The response flows in ls_in_arp_rsp at priority 40 match when
reg9[5] == 1 and generate proxy ARP replies or ND NA replies
using the MAC from eth.dst. This prevents unnecessary
flooding of ARP/ND requests to remote VTEPs for EVPN-learned
addresses.
Key design points:
- MAC stored in eth.dst (loaded by the EVPN ARP side table).
On a miss, eth.dst is left unchanged.
- ARP response uses eth.dst <-> eth.src swap to put the
resolved MAC into eth.src and the original sender's MAC
into eth.dst for the reply.
- ND response relies on nd_na{} reading eth.dst for the
NA source MAC (nd.tll).
- ARP match: arp.op == 1 && reg9[5] == 1.
- ND match: nd_ns && reg9[5] == 1.
- COPP_ND_NA meter on the ND NA response flow.
Reported-at: https://redhat.atlassian.net/browse/FDP-3429
Assisted-by: Claude Opus 4.6, Claude Code
Signed-off-by: Ales Musil <[email protected]>
---
Documentation/ref/ovn-logical-flows.7.rst | 72 ++++++++++++++------
NEWS | 6 ++
lib/ovn-util.c | 4 +-
lib/ovn-util.h | 2 +-
northd/northd.c | 83 +++++++++++++++++++++++
northd/northd.h | 18 ++---
ovn-sb.ovsschema | 6 +-
tests/ovn-northd.at | 39 +++++++++++
tests/ovn.at | 4 +-
tests/system-ovn.at | 77 +++++++++++++++++++++
10 files changed, 275 insertions(+), 36 deletions(-)
diff --git a/Documentation/ref/ovn-logical-flows.7.rst
b/Documentation/ref/ovn-logical-flows.7.rst
index ce4dd5355..2c13478a7 100644
--- a/Documentation/ref/ovn-logical-flows.7.rst
+++ b/Documentation/ref/ovn-logical-flows.7.rst
@@ -717,7 +717,7 @@ Ingress Table 19: Hairpin
- If logical switch has attached logical switch port of *vtep* type, then a
priority-1000 flow that matches on ``reg0[14]`` register bit for the traffic
received from HW VTEP (ramp) ports. This traffic is passed to ingress table
- :ref:`Destination Lookup <ls-in-32>`.
+ :ref:`Destination Lookup <ls-in-33>`.
- A priority-1 flow that hairpins traffic matched by non-default flows in the
:ref:`Pre-Hairpin <ls-in-17>` table. Hairpinning is done at L2, Ethernet
@@ -978,7 +978,28 @@ refer to either the parent or child ports as applicable to
this logical switch.
.. _ls-in-26:
-Ingress Table 26: ARP/ND responder
+Ingress Table 26: ARP/ND Pre-Lookup
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For logical switches with EVPN enabled (``dynamic-routing-vni`` is set),
+this table performs a pre-lookup in the EVPN ARP side table using the
+``chk_evpn_arp()`` action. If the target IP address matches an
+EVPN-learned entry, the resolved MAC is loaded into ``eth.dst``
+and a regbit is set so that the ARP/ND responder table can generate a
+proxy reply.
+
+- Priority-5 flows match broadcast ARP requests
+ (``arp.op == 1 && eth.bcast``) and multicast ND
+ solicitations (``nd_ns_mcast``), and call ``chk_evpn_arp(arp.tpa)``
+ or ``chk_evpn_arp(nd.target)`` respectively.
+
+- A priority-0 fallback flow advances to the next table.
+
+For switches without EVPN, only the priority-0 fallback flow is present.
+
+.. _ls-in-27:
+
+Ingress Table 27: ARP/ND responder
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table implements ARP/ND responder in a logical switch for known IPs. The
@@ -1208,12 +1229,23 @@ proxy ARP/ND behavior. It contains these logical flows:
These flows are required to respond to an ARP request if an ARP request is
sent for the IP *vip*.
+- For logical switches with EVPN enabled, priority-40 flows provide ARP/ND
+ suppression for EVPN-learned addresses. These flows match when the EVPN
+ ARP pre-lookup (table 26) found a hit (``reg9[5] == 1``):
+
+ - An ARP suppression flow matches ``arp.op == 1 && reg9[5] == 1`` and
+ generates an ARP reply using the MAC from ``eth.dst`` (loaded by
+ ``chk_evpn_arp()`` in the pre-lookup stage).
+
+ - An ND suppression flow matches ``nd_ns && reg9[5] == 1`` and
+ generates an ND NA reply using the MAC from ``eth.dst``.
+
- One priority-0 fallback flow that matches all packets and advances to the
next
table.
-.. _ls-in-27:
+.. _ls-in-28:
-Ingress Table 27: DHCP option processing
+Ingress Table 28: DHCP option processing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table adds the DHCPv4 options to a DHCPv4 packet from the logical ports
@@ -1246,9 +1278,9 @@ options. This table also adds flows for the logical ports
of type ``external``.
- A priority-0 flow that matches all packets to advances to table 16.
-.. _ls-in-28:
+.. _ls-in-29:
-Ingress Table 28: DHCP responses
+Ingress Table 29: DHCP responses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table implements DHCP responder for the DHCP replies generated by the
@@ -1301,9 +1333,9 @@ previous table.
- A priority-0 flow that matches all packets to advances to table 17.
-.. _ls-in-29:
+.. _ls-in-30:
-Ingress Table 29 DNS Lookup
+Ingress Table 30 DNS Lookup
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table looks up and resolves the DNS names to the corresponding configured
@@ -1321,9 +1353,9 @@ IP address(es).
other kinds of packets, it just stores 0 into reg0[4]. Either way, it
continues to the next table.
-.. _ls-in-30:
+.. _ls-in-31:
-Ingress Table 30 DNS Responses
+Ingress Table 31 DNS Responses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table implements DNS responder for the DNS replies generated by the
@@ -1346,9 +1378,9 @@ previous table.
(This terminates ingress packet processing; the packet does not go to the
next
ingress table.)
-.. _ls-in-31:
+.. _ls-in-32:
-Ingress table 31 External ports
+Ingress table 32 External ports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Traffic from the ``external`` logical ports enter the ingress datapath pipeline
@@ -1373,9 +1405,9 @@ traffic from these ports.
- A priority-0 flow that matches all packets to advances to table 20.
-.. _ls-in-32:
+.. _ls-in-33:
-Ingress Table 32 Destination Lookup
+Ingress Table 33 Destination Lookup
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table implements switching behavior. It contains these logical flows:
@@ -1532,9 +1564,9 @@ This table implements switching behavior. It contains
these logical flows:
If there is no entry for ``eth.dst`` in the MAC learning table, then it
stores
``none`` in the ``outport``.
-.. _ls-in-33:
+.. _ls-in-34:
-Ingress Table 33 Destination unknown
+Ingress Table 34 Destination unknown
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This table handles the packets whose destination was not found or and looked up
@@ -1695,12 +1727,12 @@ In addition, the following flows are added.
- A priority 34000 logical flow is added for each logical port which has DHCPv4
options defined to allow the DHCPv4 reply packet and which has DHCPv6 options
- defined to allow the DHCPv6 reply packet from :ref:`Ingress Table 28: DHCP
- responses <ls-in-28>`. This is indicated by setting the allow bit.
+ defined to allow the DHCPv6 reply packet from :ref:`Ingress Table 29: DHCP
+ responses <ls-in-29>`. This is indicated by setting the allow bit.
- A priority 34000 logical flow is added for each logical switch datapath
configured with DNS records with the match ``udp.dst = 53`` to allow the DNS
- reply packet from :ref:`Ingress Table 30: DNS responses <ls-in-30>`. This is
+ reply packet from :ref:`Ingress Table 31: DNS responses <ls-in-31>`. This is
indicated by setting the allow bit.
- A priority 34000 logical flow is added for each logical switch datapath with
@@ -1843,7 +1875,7 @@ in ``ct_label.nf_id`` during request processing.
function group, a priority-99 flow matches ``reg8[21] == 1 && reg8[22] == 1
&&
reg0[22..29] == id`` and sets ``outport=P; reg8[23] = 1;
next(pipeline=ingress, table=T)`` where *P* is the ``outport`` of that
network
- function and *T* is the ingress table :ref:`Destination Lookup <ls-in-32>`.
+ function and *T* is the ingress table :ref:`Destination Lookup <ls-in-33>`.
This redirects request packets matching ``to-lport`` ACLs with
network_function_group to the specific network function selected by the Pre
Network Function stage. The packets are injected back to the ingress pipeline
diff --git a/NEWS b/NEWS
index 748ae30eb..d426d671a 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,12 @@ Post v26.03.0
* Add ECMP/multi-homing support for EVPN FDB entries. FDB entries
backed by a kernel nexthop group are load-balanced via OpenFlow
select groups with weighted buckets.
+ * Add EVPN ARP/ND suppression for logical switches. When a
+ broadcast ARP request or multicast ND solicitation targets
+ an IP address that was learned via EVPN, ovn-northd now
+ generates proxy-reply flows using a dedicated side table
+ and the new chk_evpn_arp() action, preventing unnecessary
+ flooding to remote VTEPs.
- Added "override-connected" option to Logical Router Static Routes to mark
static routes as higher-priority than connected routes, which in turn led
to changes in administrative distance for specific route types. Please see
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index cc5431a11..90ab27fc6 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -1007,8 +1007,8 @@ ip_address_and_port_from_lb_key(const char *key, char
**ip_address,
*
* NOTE: If OVN_NORTHD_PIPELINE_CSUM is updated make sure to double check
* whether an update of OVN_INTERNAL_MINOR_VER is required. */
-#define OVN_NORTHD_PIPELINE_CSUM "951247664 11305"
-#define OVN_INTERNAL_MINOR_VER 14
+#define OVN_NORTHD_PIPELINE_CSUM "3951531131 11381"
+#define OVN_INTERNAL_MINOR_VER 15
/* Returns the OVN version. The caller must free the returned value. */
char *
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index bfca178e4..4d1761dc4 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -340,7 +340,7 @@ BUILD_ASSERT_DECL(
#define SCTP_ABORT_CHUNK_FLAG_T (1 << 0)
/* The number of tables for the ingress and egress pipelines. */
-#define LOG_PIPELINE_INGRESS_LEN 34
+#define LOG_PIPELINE_INGRESS_LEN 35
#define LOG_PIPELINE_EGRESS_LEN 16
static inline uint32_t
diff --git a/northd/northd.c b/northd/northd.c
index f5aa5cca3..a5534e89c 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -204,6 +204,7 @@ BUILD_ASSERT_DECL(ACL_OBS_STAGE_MAX < (1 << 2));
#define REGBIT_LOOKUP_NEIGHBOR_RESULT "reg9[2]"
#define REGBIT_LOOKUP_NEIGHBOR_IP_RESULT "reg9[3]"
#define REGBIT_DST_NAT_IP_LOCAL "reg9[4]"
+#define REGBIT_EVPN_LOOKUP_MAC "reg9[5]"
#define REGBIT_KNOWN_LB_SESSION "reg9[6]"
#define REGBIT_DHCP_RELAY_REQ_CHK "reg9[7]"
#define REGBIT_DHCP_RELAY_RESP_CHK "reg9[8]"
@@ -10754,6 +10755,85 @@ build_lswitch_arp_nd_responder_default(struct
ovn_datapath *od,
lflow_ref);
}
+/* Ingress table ls_in_arp_nd_pre_lookup: EVPN ARP/ND pre-lookup.
+ *
+ * For EVPN-enabled switches, calls chk_evpn_arp() to look up the
+ * IP in the EVPN ARP side table. On a hit, the resolved MAC is
+ * stored in eth.dst and REGBIT_EVPN_LOOKUP_MAC is set. The
+ * response flow in ls_in_arp_rsp reads the MAC from eth.dst.
+ */
+static void
+build_lswitch_arp_nd_evpn_lookup(struct ovn_datapath *od,
+ struct lflow_table *lflows,
+ struct lflow_ref *lflow_ref)
+{
+ ovs_assert(od->nbs);
+
+ /* Default: pass through. */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_PRE_LOOKUP, 0, "1",
+ "next;", lflow_ref);
+
+ if (!od->has_evpn_vni) {
+ return;
+ }
+
+ /* IPv4: broadcast ARP requests only. */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_PRE_LOOKUP, 5,
+ "arp.op == 1 && eth.bcast",
+ REGBIT_EVPN_LOOKUP_MAC " = chk_evpn_arp(arp.tpa); next;",
+ lflow_ref);
+
+ /* IPv6: multicast ND solicitations only. */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_PRE_LOOKUP, 5,
+ "nd_ns_mcast",
+ REGBIT_EVPN_LOOKUP_MAC " = chk_evpn_arp(nd.target); next;",
+ lflow_ref);
+}
+
+/* Ingress table ls_in_arp_rsp: EVPN ARP/ND suppression response flows.
+ *
+ * For EVPN-enabled switches, adds flows that proxy-reply to ARP/ND
+ * when chk_evpn_arp found a match (REGBIT_EVPN_LOOKUP_MAC == 1).
+ * The resolved MAC is already in eth.dst (loaded by the side table).
+ */
+static void
+build_lswitch_arp_nd_evpn_response(struct ovn_datapath *od,
+ struct lflow_table *lflows,
+ const struct shash *meter_groups,
+ struct lflow_ref *lflow_ref)
+{
+ ovs_assert(od->nbs);
+
+ if (!od->has_evpn_vni) {
+ return;
+ }
+
+ /* ARP reply (priority 40): ARP request with EVPN hit.
+ * eth.dst holds the resolved MAC (loaded by chk_evpn_arp). */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_RSP, 40,
+ "arp.op == 1 && " REGBIT_EVPN_LOOKUP_MAC " == 1",
+ "eth.dst <-> eth.src; "
+ "arp.op = 2; "
+ "arp.tha = arp.sha; "
+ "arp.sha = eth.src; "
+ "arp.tpa <-> arp.spa; "
+ "outport = inport; "
+ "flags.loopback = 1; "
+ "output;",
+ lflow_ref);
+
+ /* ND NA reply (priority 40): ND NS with EVPN hit.
+ * eth.dst holds the resolved MAC (loaded by chk_evpn_arp);
+ * compose_nd_na() uses ip_flow->dl_dst for eth.src and nd.tll. */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_RSP, 40,
+ "nd_ns && " REGBIT_EVPN_LOOKUP_MAC " == 1",
+ "nd_na { outport = inport; flags.loopback = 1; output; };",
+ lflow_ref,
+ WITH_CTRL_METER(copp_meter_get(COPP_ND_NA,
+ od->nbs->copp,
+ meter_groups)));
+}
+
/* Ingress table 24: ARP/ND responder for service monitor source ip.
* (priority 110)*/
static void
@@ -19525,7 +19605,10 @@ build_lswitch_and_lrouter_iterate_by_ls(struct
ovn_datapath *od,
build_fwd_group_lflows(od, lsi->lflows, NULL);
build_lswitch_lflows_admission_control(od, lsi->lflows, NULL);
build_lswitch_learn_fdb_od(od, lsi->lflows, NULL);
+ build_lswitch_arp_nd_evpn_lookup(od, lsi->lflows, NULL);
build_lswitch_arp_nd_responder_default(od, lsi->lflows, NULL);
+ build_lswitch_arp_nd_evpn_response(od, lsi->lflows, lsi->meter_groups,
+ NULL);
build_lswitch_dns_lookup_and_response(od, lsi->lflows, lsi->meter_groups,
NULL);
build_lswitch_dhcp_and_dns_defaults(od, lsi->lflows, NULL);
diff --git a/northd/northd.h b/northd/northd.h
index 726a416e4..f713017b5 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -541,14 +541,16 @@ ls_has_localnet_port(const struct ovn_datapath *od)
"ls_in_pre_network_function") \
PIPELINE_STAGE(SWITCH, IN, STATEFUL, 24, "ls_in_stateful") \
PIPELINE_STAGE(SWITCH, IN, NF, 25, "ls_in_network_function") \
- PIPELINE_STAGE(SWITCH, IN, ARP_ND_RSP, 26, "ls_in_arp_rsp") \
- PIPELINE_STAGE(SWITCH, IN, DHCP_OPTIONS, 27, "ls_in_dhcp_options") \
- PIPELINE_STAGE(SWITCH, IN, DHCP_RESPONSE, 28, "ls_in_dhcp_response") \
- PIPELINE_STAGE(SWITCH, IN, DNS_LOOKUP, 29, "ls_in_dns_lookup") \
- PIPELINE_STAGE(SWITCH, IN, DNS_RESPONSE, 30, "ls_in_dns_response") \
- PIPELINE_STAGE(SWITCH, IN, EXTERNAL_PORT, 31, "ls_in_external_port") \
- PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 32, "ls_in_l2_lkup") \
- PIPELINE_STAGE(SWITCH, IN, L2_UNKNOWN, 33, "ls_in_l2_unknown") \
+ PIPELINE_STAGE(SWITCH, IN, ARP_ND_PRE_LOOKUP, 26, \
+ "ls_in_arp_nd_pre_lookup") \
+ PIPELINE_STAGE(SWITCH, IN, ARP_ND_RSP, 27, "ls_in_arp_rsp") \
+ PIPELINE_STAGE(SWITCH, IN, DHCP_OPTIONS, 28, "ls_in_dhcp_options") \
+ PIPELINE_STAGE(SWITCH, IN, DHCP_RESPONSE, 29, "ls_in_dhcp_response") \
+ PIPELINE_STAGE(SWITCH, IN, DNS_LOOKUP, 30, "ls_in_dns_lookup") \
+ PIPELINE_STAGE(SWITCH, IN, DNS_RESPONSE, 31, "ls_in_dns_response") \
+ PIPELINE_STAGE(SWITCH, IN, EXTERNAL_PORT, 32, "ls_in_external_port") \
+ PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 33, "ls_in_l2_lkup") \
+ PIPELINE_STAGE(SWITCH, IN, L2_UNKNOWN, 34, "ls_in_l2_unknown") \
\
/* Logical switch egress stages. */ \
PIPELINE_STAGE(SWITCH, OUT, LOOKUP_FDB, 0, "ls_out_lookup_fdb") \
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index d9a91739c..d38992ad8 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
{
"name": "OVN_Southbound",
- "version": "21.8.0",
- "cksum": "614397313 36713",
+ "version": "21.9.0",
+ "cksum": "3029208442 36713",
"tables": {
"SB_Global": {
"columns": {
@@ -103,7 +103,7 @@
"egress"]]}}},
"table_id": {"type": {"key": {"type": "integer",
"minInteger": 0,
- "maxInteger": 33}}},
+ "maxInteger": 34}}},
"priority": {"type": {"key": {"type": "integer",
"minInteger": 0,
"maxInteger": 65535}}},
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 7f4a88d4e..763c3265b 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -19502,6 +19502,45 @@ OVN_CLEANUP_NORTHD
AT_CLEANUP
])
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([LS EVPN ARP/ND suppression flows])
+AT_KEYWORDS([dynamic-routing evpn])
+ovn_start
+
+AS_BOX([EVPN switch])
+check ovn-nbctl --wait=sb \
+ -- ls-add ls-evpn \
+ -- set logical_switch ls-evpn other_config:dynamic-routing-vni=10
+
+dnl Verify ls_in_arp_nd_pre_lookup flows use chk_evpn_arp.
+AT_CHECK([ovn-sbctl lflow-list ls-evpn | grep ls_in_arp_nd_pre_lookup |
ovn_strip_lflows], [0], [dnl
+ table=??(ls_in_arp_nd_pre_lookup), priority=0 , match=(1), action=(next;)
+ table=??(ls_in_arp_nd_pre_lookup), priority=5 , match=(arp.op == 1 &&
eth.bcast), action=(reg9[[5]] = chk_evpn_arp(arp.tpa); next;)
+ table=??(ls_in_arp_nd_pre_lookup), priority=5 , match=(nd_ns_mcast),
action=(reg9[[5]] = chk_evpn_arp(nd.target); next;)
+])
+
+dnl Verify ls_in_arp_rsp EVPN ARP suppression flow at priority 40.
+AT_CHECK([ovn-sbctl lflow-list ls-evpn | grep ls_in_arp_rsp | grep
'priority=40' | ovn_strip_lflows], [0], [dnl
+ table=??(ls_in_arp_rsp ), priority=40 , match=(arp.op == 1 &&
reg9[[5]] == 1), action=(eth.dst <-> eth.src; arp.op = 2; arp.tha = arp.sha;
arp.sha = eth.src; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1;
output;)
+ table=??(ls_in_arp_rsp ), priority=40 , match=(nd_ns && reg9[[5]] ==
1), action=(nd_na { outport = inport; flags.loopback = 1; output; };)
+])
+
+AS_BOX([Non-EVPN switch])
+check ovn-nbctl --wait=sb \
+ -- ls-add ls-plain
+
+dnl Non-EVPN switch should have only default next; in pre_lookup.
+AT_CHECK([ovn-sbctl lflow-list ls-plain | grep ls_in_arp_nd_pre_lookup |
ovn_strip_lflows], [0], [dnl
+ table=??(ls_in_arp_nd_pre_lookup), priority=0 , match=(1), action=(next;)
+])
+
+dnl Non-EVPN switch should have no EVPN suppression flows (no priority 40 with
EVPN regbit).
+AT_CHECK([ovn-sbctl lflow-list ls-plain | grep ls_in_arp_rsp | grep
'priority=40'], [1])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
OVN_FOR_EACH_NORTHD_NO_HV([
AT_SETUP([LS EVPN conntrack skip with stateful ACLs and LBs])
AT_KEYWORDS([dynamic-routing])
diff --git a/tests/ovn.at b/tests/ovn.at
index 2d2dcb385..495a6ed7f 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -867,8 +867,8 @@ next();
Syntax error at `)' expecting "pipeline" or "table".
next(10;
Syntax error at `;' expecting `)'.
-next(34);
- "next" action cannot advance beyond table 34.
+next(35);
+ "next" action cannot advance beyond table 35.
next(table=lflow_table);
formats as next;
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 1f0f17cb7..c6e6b61fd 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -18015,6 +18015,7 @@ OVN_FOR_EACH_NORTHD([
AT_SETUP([dynamic-routing - EVPN $1 naming])
AT_KEYWORDS([dynamic-routing])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
CHECK_VRF()
CHECK_CONNTRACK()
CHECK_CONNTRACK_NAT()
@@ -18588,6 +18589,82 @@ check ip -6 neigh add dev $BR_NAME 172:16::70 lladdr
f0:00:0f:16:10:70 nud noarp
OVS_WAIT_UNTIL([test "$(ovs-ofctl dump-flows br-int
table=OFTABLE_EVPN_ARP_LOOKUP | \
grep -c priority)" = "6"])
+AS_BOX([EVPN ARP/ND suppression logical flows])
+dnl Verify northd generates chk_evpn_arp flows in ls_in_arp_nd_pre_lookup.
+AT_CHECK([ovn-sbctl lflow-list ls-evpn | grep ls_in_arp_nd_pre_lookup | grep
-q 'chk_evpn_arp'])
+
+dnl Verify ls_in_arp_rsp has EVPN suppression response flows (priority 40).
+AT_CHECK([ovn-sbctl lflow-list ls-evpn | grep ls_in_arp_rsp | grep
'priority=40' | \
+ grep -q 'reg9\[[5\]] == 1'])
+
+AS_BOX([EVPN ARP/ND suppression traffic])
+
+dnl Record initial packet counts on both EVPN suppression flows.
+evpn_arp_pkt_before=$(ovs-ofctl dump-flows br-int table=$(ovn-debug
lflow-stage-to-oftable ls_in_arp_rsp) | \
+ grep "priority=40.*arp" | sed -n 's/.*n_packets=\([[0-9]]*\).*/\1/p')
+evpn_nd_pkt_before=$(ovs-ofctl dump-flows br-int table=$(ovn-debug
lflow-stage-to-oftable ls_in_arp_rsp) | \
+ grep "priority=40.*icmp6" | sed -n 's/.*n_packets=\([[0-9]]*\).*/\1/p')
+
+dnl --- Broadcast ARP: should be suppressed (local proxy reply) ---
+ip netns exec workload1 scapy -H <<-EOF
+p = Ether(src='f0:00:0f:16:01:10', dst='ff:ff:ff:ff:ff:ff') / \
+ ARP(op=1, hwsrc='f0:00:0f:16:01:10', psrc='172.16.1.10',
+ hwdst='00:00:00:00:00:00', pdst='172.16.1.50')
+sendp(p, iface='workload1', verbose=False)
+EOF
+
+dnl Verify the EVPN suppression flow handled the broadcast ARP.
+OVS_WAIT_UNTIL([
+ evpn_arp_pkt_after=$(ovs-ofctl dump-flows br-int table=$(ovn-debug
lflow-stage-to-oftable ls_in_arp_rsp) | \
+ grep "priority=40.*arp" | sed -n 's/.*n_packets=\([[0-9]]*\).*/\1/p')
+ test "$evpn_arp_pkt_after" -gt "$evpn_arp_pkt_before"
+])
+
+dnl --- Unicast ARP: must NOT be suppressed ---
+ip netns exec workload1 scapy -H <<-EOF
+p = Ether(src='f0:00:0f:16:01:10', dst='f0:00:0f:16:10:50') / \
+ ARP(op=1, hwsrc='f0:00:0f:16:01:10', psrc='172.16.1.10',
+ hwdst='00:00:00:00:00:00', pdst='172.16.1.50')
+sendp(p, iface='workload1', verbose=False)
+EOF
+
+dnl The unicast ARP must NOT hit the suppression flow.
+evpn_arp_pkt_unicast=$(ovs-ofctl dump-flows br-int table=$(ovn-debug
lflow-stage-to-oftable ls_in_arp_rsp) | \
+ grep "priority=40.*arp" | sed -n 's/.*n_packets=\([[0-9]]*\).*/\1/p')
+AT_CHECK([test "$evpn_arp_pkt_unicast" = "$evpn_arp_pkt_after"], [0])
+
+dnl --- Multicast ND NS: should be suppressed (local proxy NA) ---
+NS_CHECK_EXEC([workload1], [ip -6 neigh flush dev workload1], [0], [ignore],
[ignore])
+
+ip netns exec workload1 scapy -H <<-EOF
+p = Ether(src='f0:00:0f:16:01:10', dst='33:33:ff:00:00:50') / \
+ IPv6(src='172:16::10', dst='ff02::1:ff00:50') / \
+ ICMPv6ND_NS(tgt='172:16::50') / \
+ ICMPv6NDOptSrcLLAddr(lladdr='f0:00:0f:16:01:10')
+sendp(p, iface='workload1', verbose=False)
+EOF
+
+dnl Verify the EVPN ND suppression flow handled the multicast NS.
+OVS_WAIT_UNTIL([
+ evpn_nd_pkt_after=$(ovs-ofctl dump-flows br-int table=$(ovn-debug
lflow-stage-to-oftable ls_in_arp_rsp) | \
+ grep "priority=40.*icmp6" | sed -n 's/.*n_packets=\([[0-9]]*\).*/\1/p')
+ test "$evpn_nd_pkt_after" -gt "$evpn_nd_pkt_before"
+])
+
+dnl --- Unicast ND NS: must NOT be suppressed ---
+ip netns exec workload1 scapy -H <<-EOF
+p = Ether(src='f0:00:0f:16:01:10', dst='f0:00:0f:16:10:50') / \
+ IPv6(src='172:16::10', dst='172:16::50') / \
+ ICMPv6ND_NS(tgt='172:16::50') / \
+ ICMPv6NDOptSrcLLAddr(lladdr='f0:00:0f:16:01:10')
+sendp(p, iface='workload1', verbose=False)
+EOF
+
+dnl The unicast ND NS must NOT hit the suppression flow.
+evpn_nd_pkt_unicast=$(ovs-ofctl dump-flows br-int table=$(ovn-debug
lflow-stage-to-oftable ls_in_arp_rsp) | \
+ grep "priority=40.*icmp6" | sed -n 's/.*n_packets=\([[0-9]]*\).*/\1/p')
+AT_CHECK([test "$evpn_nd_pkt_unicast" = "$evpn_nd_pkt_after"], [0])
+
# Remove remote workload ARP entries and check ovn-controller's state.
check ip neigh del dev $BR_NAME 172.16.1.50
check ip neigh del dev $BR_NAME 172.16.1.60
--
2.54.0
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev