> 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]>

Acked-by: Lorenzo Bianconi <[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
> 
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to