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

Reply via email to