northd currently generates flows that restrict ARP request handling.

If an ARP request comes from an lswitch that has either localnet or
l2gateway ports, and routers attachment port is an L3 dgw port,
then the request is only allowed on resident chassis.

However, the same lswitch can also have VIF ports, and for those ports
the restrictions above should not be appled - to fix this, the 
is_chassis_resident
check was moved up to the switch level.

As a result, the intended behavior is that for all packets coming from physical 
ports:
1) flags.localnet flag is set. Previously, this only applied to localnet ports,
   but now it also includes L2 gateway ports. The flag name was kept unchanged 
for
   backward compatibility.

2) For IPs belonging to the switch, there was already a check in
   build_lswitch_arp_nd_local_resp_match that ensures processing happens only on
   HA chassis when the packet comes from localnet ports; otherwise, the packet
   is allowed on all chassis. This logic is the same for ND and ARP packets.

2) The same logical for router-related flows in
   build_lswitch_rport_arp_req_flow, but only for ARP packets.
   The expectation is that for router/NAT ports, packets arrive on HA
   chassis acting as gateway router port (or selected gateway port for NAT).
   For distributed NAT, the packet should arrive on chassis where the
   logical VM port resides.

4) On the router side, the is_chassis_resident check for ARP requests was 
removed
   for MAC_Binding learning. All other router-side ARP response logic remains
   unchanged and still relies on is_chassis_resident. This is for case: in a 
centralized
   routing setup (without localnet/l2gw ports), if a switch is attached to two 
routers,
   MAC learning will happen on both, but the ARP response will only be 
generated from one.

5) The logic for centralized routing has not changed.

Signed-off-by: Alexandra Rukomoinikova <[email protected]>
---
 northd/en-lr-nat.c            |   2 +-
 northd/northd.c               | 455 ++++++++++++++++++++++++----------
 northd/northd.h               |  16 ++
 tests/ovn-northd.at           | 323 ++++++++++++++++++++----
 tests/ovn.at                  | 162 ++++++++++++
 tests/system-common-macros.at |   3 +
 tests/system-ovn.at           | 127 +++++++---
 7 files changed, 868 insertions(+), 220 deletions(-)

diff --git a/northd/en-lr-nat.c b/northd/en-lr-nat.c
index f0596d254..f965b4155 100644
--- a/northd/en-lr-nat.c
+++ b/northd/en-lr-nat.c
@@ -354,7 +354,7 @@ lr_nat_entry_set_dgw_port(const struct ovn_datapath *od,
      * on the gateway chassis for the DGP's networks/subnets.)
      */
     struct ovn_port *l3dgw_port = nat_entry->l3dgw_port;
-    if (l3dgw_port && l3dgw_port->peer && l3dgw_port->peer->cr_port) {
+    if (l3dgw_port && is_router_with_centralized_routing(l3dgw_port)) {
         return true;
     }
 
diff --git a/northd/northd.c b/northd/northd.c
index 0dbf17426..d86a6593d 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -1255,6 +1255,12 @@ lsp_is_vtep(const struct nbrec_logical_switch_port *nbsp)
     return !strcmp(nbsp->type, "vtep");
 }
 
+static inline bool
+lsp_is_l2gw(const struct nbrec_logical_switch_port *nbsp)
+{
+    return !strcmp(nbsp->type, "l2gateway");
+}
+
 static bool
 localnet_can_learn_mac(const struct nbrec_logical_switch_port *nbsp)
 {
@@ -9578,132 +9584,255 @@ lrouter_port_ipv6_reachable(const struct ovn_port *op,
     return false;
 }
 
-/*
- * Ingress table 30: Flows that forward ARP/ND requests only to the routers
- * that own the addresses. Other ARP/ND packets are still flooded in the
- * switching domain as regular broadcast.
- */
+static inline void
+build_lswitch_rport_arp_forward_action(const char *port_name,
+                                       bool has_non_router_ports,
+                                       struct ds *actions)
+{
+    if (has_non_router_ports) {
+        ds_put_format(actions, "clone {outport = %s; output; }; "
+                                "outport = \""MC_UNKNOWN"\"; output;",
+                      port_name);
+    } else {
+        ds_put_format(actions, "outport = %s; output;", port_name);
+    }
+}
+
+static inline void
+build_distributed_routing_arp_restrictions(
+    const struct ovn_port *op,
+    const char *nat_port,
+    struct ds *match)
+{
+    const char *crp_key = nat_port
+                          ? nat_port
+                          : op->peer->cr_port->json_key;
+    ds_put_format(match,
+                  " && " LOCALNET_CHASSIS_RESIDENT_MATCH,
+                  crp_key);
+}
+
 static void
-build_lswitch_rport_arp_req_flow(
-    const char *ips, int addr_family, struct ovn_port *patch_op,
-    const struct ovn_datapath *od, uint32_t priority,
-    struct lflow_table *lflows, const struct ovsdb_idl_row *stage_hint,
+build_lswitch_rport_garp_flow(const char *arp_ip,
+                              const struct ovn_port *switch_op,
+                              const char *nat_port,
+                              bool distributed_routing,
+                              struct ds *arp_nd_base_match,
+                              struct lflow_table *lflows,
+                              struct lflow_ref *lflow_ref)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    ds_clone(&match, arp_nd_base_match);
+    ds_put_format(&match, " && arp.spa == %s", arp_ip);
+    if (distributed_routing) {
+        build_distributed_routing_arp_restrictions(switch_op,
+                                                   nat_port,
+                                                   &match);
+    }
+    ovn_lflow_add(lflows, switch_op->od, S_SWITCH_IN_L2_LKUP, 90,
+                  ds_cstr(&match),
+                  "outport = \""MC_FLOOD_L2"\"; output;",
+                  lflow_ref, WITH_HINT(&switch_op->nbsp->header_));
+
+    ds_destroy(&match);
+}
+
+static void
+buils_lswitch_rport_arp_req_flow_centralized_routing(
+    struct ovn_port *op, bool has_non_router_ports,
+    struct ds *arp_nd_base_match, struct lflow_table *lflows,
     struct lflow_ref *lflow_ref)
 {
-    struct ds match   = DS_EMPTY_INITIALIZER;
-    struct ds m       = DS_EMPTY_INITIALIZER;
-    struct ds m_garp  = DS_EMPTY_INITIALIZER;
     struct ds actions = DS_EMPTY_INITIALIZER;
+    struct ds match = DS_EMPTY_INITIALIZER;
 
-    arp_nd_ns_match(ips, addr_family, &m);
-    ds_clone(&match, &m);
+    ds_put_format(&match, "%s && is_chassis_resident(%s)",
+                  ds_cstr(arp_nd_base_match), op->cr_port->json_key);
+    build_lswitch_rport_arp_forward_action(op->json_key,
+                                           has_non_router_ports,
+                                           &actions);
+    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_L2_LKUP, 80,
+                  ds_cstr(&match), ds_cstr(&actions), lflow_ref,
+                  WITH_HINT(&op->nbsp->header_));
+    ds_clear(&match);
+    ds_put_format(&match, "%s && !is_chassis_resident(%s)",
+                  ds_cstr(arp_nd_base_match), op->cr_port->json_key);
+    ds_clear(&actions);
+    build_lswitch_rport_arp_forward_action(op->cr_port->json_key,
+                                           has_non_router_ports,
+                                           &actions);
+    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_L2_LKUP, 80,
+                  ds_cstr(&match), ds_cstr(&actions), lflow_ref,
+                  WITH_HINT(&op->nbsp->header_));
 
-    bool has_cr_port = patch_op->cr_port;
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
 
-    /* If the patch_op has a chassis resident port, it means
-     *    - its peer is a distributed gateway port (DGP) and
-     *    - routing is centralized for the DGP's networks on
-     *      the configured gateway chassis.
-     *
-     * If that's the case, make sure that the packets destined to
-     * the DGP's MAC are sent to the chassis where the DGP resides.
-     * */
+static void
+buils_lswitch_rport_arp_req_flow_distributed_routing(
+    struct ovn_port *op, const char *nat_port,
+    bool has_non_router_ports, struct ds *arp_nd_base_match,
+    struct lflow_table *lflows, struct lflow_ref *lflow_ref)
+{
+    struct ds actions = DS_EMPTY_INITIALIZER;
+    struct ds match = DS_EMPTY_INITIALIZER;
 
-    if (has_cr_port) {
-        ds_put_format(&match, " && is_chassis_resident(%s)",
-                      patch_op->cr_port->json_key);
-    }
+    ds_clone(&match, arp_nd_base_match);
+    build_distributed_routing_arp_restrictions(op, nat_port, &match);
+    build_lswitch_rport_arp_forward_action(op->json_key,
+                                           has_non_router_ports,
+                                           &actions);
+    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_L2_LKUP, 80,
+                  ds_cstr(&match), ds_cstr(&actions), lflow_ref,
+                  WITH_HINT(&op->nbsp->header_));
 
-    if (addr_family == AF_INET) {
-        ds_clone(&m_garp, &m);
-        ds_put_format(&m_garp, " && arp.spa == %s", ips);
-    }
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
 
-    /* Send a the packet to the router pipeline.  If the switch has non-router
-     * ports then flood it there as well.
-     */
-    if (vector_len(&od->router_ports) != od->nbs->n_ports) {
-        ds_put_format(&actions, "clone {outport = %s; output; }; "
-                                "outport = \""MC_UNKNOWN"\"; output;",
-                      patch_op->json_key);
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, priority,
-                      ds_cstr(&match), ds_cstr(&actions), lflow_ref,
-                      WITH_HINT(stage_hint));
-        if (addr_family == AF_INET) {
-            ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, priority + 10,
-                          ds_cstr(&m_garp),
-                          "outport = \""MC_FLOOD_L2"\"; output;",
-                          lflow_ref, WITH_HINT(stage_hint));
-        }
-    } else {
-        ds_put_format(&actions, "outport = %s; output;", patch_op->json_key);
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, priority,
-                      ds_cstr(&match), ds_cstr(&actions), lflow_ref,
-                      WITH_HINT(stage_hint));
-    }
+static void
+build_lswitch_rport_arp_req_flow_default_routing(
+    struct ovn_port *op, bool switch_non_router_ports,
+    struct ds *arp_nd_base_match, struct lflow_table *lflows,
+    struct lflow_ref *lflow_ref)
+{
+    struct ds actions = DS_EMPTY_INITIALIZER;
+    struct ds match   = DS_EMPTY_INITIALIZER;
 
-    if (has_cr_port) {
-        ds_clear(&match);
-        ds_put_format(&match, "%s && !is_chassis_resident(%s)", ds_cstr(&m),
-                      patch_op->cr_port->json_key);
-        ds_clear(&actions);
-        if (vector_len(&od->router_ports) != od->nbs->n_ports) {
-            ds_put_format(&actions, "clone {outport = %s; output; }; "
-                                    "outport = \""MC_UNKNOWN"\"; output;",
-                          patch_op->cr_port->json_key);
-            ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, priority,
-                          ds_cstr(&match), ds_cstr(&actions), lflow_ref,
-                          WITH_HINT(stage_hint));
-        } else {
-            ds_put_format(&actions, "outport = %s; output;",
-                          patch_op->cr_port->json_key);
-            ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, priority,
-                          ds_cstr(&match), ds_cstr(&actions), lflow_ref,
-                          WITH_HINT(stage_hint));
-        }
-    }
+    ds_put_format(&match, "%s", ds_cstr(arp_nd_base_match));
+    build_lswitch_rport_arp_forward_action(op->json_key,
+                                           switch_non_router_ports,
+                                           &actions);
+    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_L2_LKUP, 80,
+                  ds_cstr(&match), ds_cstr(&actions), lflow_ref,
+                  WITH_HINT(&op->nbsp->header_));
 
-    ds_destroy(&m);
     ds_destroy(&match);
-    ds_destroy(&m_garp);
     ds_destroy(&actions);
 }
 
 /*
- * Ingress table 30: Flows that forward ARP/ND requests only to the routers
- * that own the addresses.
- * Priorities:
- * - 80: self originated GARPs that need to follow regular processing.
- * - 75: ARP requests to router owned IPs (interface IP/LB/NAT).
+ * Ingress table IN_L2_LKUP:
+ *
+ * This table contains flows that forward ARP/ND requests only to the
+ * router instances that own the corresponding addresses.
+ *
+ * Other ARP/ND packets are still flooded within the switching domain
+ * as regular broadcast traffic, subject to the rules described below.
+ *
+ * If a switch has physical ports (localnet/l2gateway) and a router port
+ * is associated with a chassisredirect (cr-port), routing is no longer
+ * strictly centralized on a single router chassis.
+ * Instead, routing may occur on all chassis, subject to the following rules:
+ *
+ * 1) Packets from localnet ports addressed to router-owned IPs:
+ *    ARP/ND requests are allowed only on the HA (gateway) chassis.
+ *
+ * 2) Packets from localnet ports addressed to switch ports:
+ *    ARP/ND requests are allowed only on the chassis where the target
+ *    logical port resides.
+ *
+ *    (Implemented in build_lswitch_arp_nd_local_resp_match.)
+ *
+ * 3) Packets addressed to a distributed NAT IP:
+ *    ARP/ND requests are allowed only on the chassis where the logical
+ *    port owning the corresponding private IP is bound.
+ *
+ * 4) No restrictions are imposed on internal ARP/ND traffic.
+ *
+ * 5) All other packets are dropped. For arp packets - they are dropped
+ *    in switch pipeline, for ND packets - they are dropped on router.
+ *
+ * ----------------------------------------------------------------------
+ *
+ * If the switch has no physical (localnet/l2gateway) ports, routing is
+ * centralized on the HA chassis:
+ *
+ * 1) Packets arriving on the HA chassis are delivered directly to the
+ *    router port and flooded to unknown multicast group.
+ *
+ * 2) Packets arriving on non-HA chassis are forwarded to the HA chassis
+ *    via the chassisredirect (cr-port) and unknown multicast group.
+ *
+ * ----------------------------------------------------------------------
+ *
+ * In the default routing mode (when no router port has a cr-port),
+ * packets are simply forwarded directly to the router port and
+ * flooded to unknown multicast group.
  */
 static void
-build_lswitch_rport_arp_req_flows(struct ovn_port *op,
-                                  struct ovn_datapath *sw_od,
-                                  struct ovn_port *sw_op,
-                                  struct lflow_table *lflows,
-                                  const struct ovsdb_idl_row *stage_hint)
+build_lswitch_rport_arp_req_flow(const char *arp_ip,
+                                 int addr_family,
+                                 struct ovn_port *switch_op,
+                                 const char *nat_port,
+                                 struct lflow_table *lflows,
+                                 struct lflow_ref *lflow_ref)
 {
-    if (!op || !op->nbrp) {
+    struct ds arp_nd_base_match = DS_EMPTY_INITIALIZER;
+    bool peer_has_cr_port = switch_op->peer && switch_op->peer->cr_port;
+    bool is_centralized_routing =
+        is_peer_router_with_centralized_routing(switch_op);
+    VLOG_WARN("is_centralized_routing = %d", is_centralized_routing);
+    bool distributed_routing = peer_has_cr_port &&
+                               !is_centralized_routing &&
+                               addr_family == AF_INET;
+    bool switch_non_router_ports = vector_len(&switch_op->od->router_ports)
+                                   != switch_op->od->nbs->n_ports;
+
+    arp_nd_ns_match(arp_ip, addr_family, &arp_nd_base_match);
+
+    if (is_centralized_routing) {
+        buils_lswitch_rport_arp_req_flow_centralized_routing(
+            switch_op, switch_non_router_ports, &arp_nd_base_match,
+            lflows, lflow_ref);
+    } else if (distributed_routing) {
+        buils_lswitch_rport_arp_req_flow_distributed_routing(
+            switch_op, nat_port, switch_non_router_ports,
+            &arp_nd_base_match, lflows, lflow_ref);
+    } else {
+        build_lswitch_rport_arp_req_flow_default_routing(
+            switch_op, switch_non_router_ports, &arp_nd_base_match,
+            lflows, lflow_ref);
+    }
+
+    /* For IPv4, also add a GARP flow at higher priority so that
+     * self-originated GARPs are flooded to the entire L2 domain.
+     * Only needed when there are non-router ports.
+     */
+    if (addr_family == AF_INET && switch_non_router_ports) {
+        build_lswitch_rport_garp_flow(arp_ip, switch_op, nat_port,
+                                      distributed_routing,
+                                      &arp_nd_base_match,
+                                      lflows, lflow_ref);
+    }
+
+    ds_destroy(&arp_nd_base_match);
+}
+
+static void
+build_lswitch_rport_arp_req_flows(struct ovn_port *router_op,
+                                  struct ovn_port *switch_op,
+                                  struct lflow_table *lflows)
+{
+    if (!router_op || !router_op->nbrp) {
         return;
     }
 
-    if (!lrport_is_enabled(op->nbrp)) {
+    if (!lrport_is_enabled(router_op->nbrp)) {
         return;
     }
 
-    /* Forward ARP requests for owned IP addresses (L3, VIP, NAT) only to this
-     * router port.
-     * Priority: 80.
-     */
-    for (size_t i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
+    for (size_t i = 0; i < router_op->lrp_networks.n_ipv4_addrs; i++) {
         build_lswitch_rport_arp_req_flow(
-            op->lrp_networks.ipv4_addrs[i].addr_s, AF_INET, sw_op, sw_od, 80,
-            lflows, stage_hint, sw_op->lflow_ref);
+            router_op->lrp_networks.ipv4_addrs[i].addr_s, AF_INET, switch_op,
+            NULL, lflows, switch_op->lflow_ref);
     }
-    for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
+    for (size_t i = 0; i < router_op->lrp_networks.n_ipv6_addrs; i++) {
         build_lswitch_rport_arp_req_flow(
-            op->lrp_networks.ipv6_addrs[i].addr_s, AF_INET6, sw_op, sw_od, 80,
-            lflows, stage_hint, sw_op->lflow_ref);
+            router_op->lrp_networks.ipv6_addrs[i].addr_s, AF_INET6, switch_op,
+            NULL,  lflows, switch_op->lflow_ref);
     }
 }
 
@@ -9718,8 +9847,7 @@ static void
 build_lswitch_rport_arp_req_flows_for_lbnats(
     struct ovn_port *op, const struct lr_stateful_record *lr_stateful_rec,
     const struct ovn_datapath *sw_od, struct ovn_port *sw_op,
-    struct lflow_table *lflows, const struct ovsdb_idl_row *stage_hint,
-    struct lflow_ref *lflow_ref)
+    struct lflow_table *lflows, struct lflow_ref *lflow_ref)
 {
     if (!op || !op->nbrp) {
         return;
@@ -9746,9 +9874,8 @@ build_lswitch_rport_arp_req_flows_for_lbnats(
              */
             if (ip_parse(ip_addr, &ipv4_addr) &&
                 lrouter_port_ipv4_reachable(op, ipv4_addr)) {
-                build_lswitch_rport_arp_req_flow(
-                    ip_addr, AF_INET, sw_op, sw_od, 80, lflows,
-                    stage_hint, lflow_ref);
+                build_lswitch_rport_arp_req_flow(ip_addr, AF_INET, sw_op,
+                                                 NULL, lflows, lflow_ref);
             }
         }
         SSET_FOR_EACH (ip_addr, &lr_stateful_rec->lb_ips->ips_v6_reachable) {
@@ -9759,9 +9886,8 @@ build_lswitch_rport_arp_req_flows_for_lbnats(
              */
             if (ipv6_parse(ip_addr, &ipv6_addr) &&
                 lrouter_port_ipv6_reachable(op, &ipv6_addr)) {
-                build_lswitch_rport_arp_req_flow(
-                    ip_addr, AF_INET6, sw_op, sw_od, 80, lflows,
-                    stage_hint, lflow_ref);
+                build_lswitch_rport_arp_req_flow(ip_addr, AF_INET6, sw_op,
+                                                 NULL, lflows, lflow_ref);
             }
         }
     }
@@ -9792,24 +9918,38 @@ build_lswitch_rport_arp_req_flows_for_lbnats(
             continue;
         }
 
+        struct ds json_key = DS_EMPTY_INITIALIZER;
+        char *nat_logical_port;
+        if (nat_entry->is_distributed) {
+            json_string_escape(nat->logical_port, &json_key);
+            nat_logical_port = ds_steal_cstr(&json_key);
+        } else if (nat_entry->l3dgw_port) {
+           nat_logical_port = nat_entry->l3dgw_port->cr_port->json_key;
+        } else {
+            nat_logical_port = NULL;
+        }
+
         /* Check if the ovn port has a network configured on which we could
          * expect ARP requests/NS for the DNAT external_ip.
          */
         if (nat_entry_is_v6(nat_entry)) {
             if (!sset_contains(&lr_stateful_rec->lb_ips->ips_v6,
                                nat->external_ip)) {
-                build_lswitch_rport_arp_req_flow(
-                    nat->external_ip, AF_INET6, sw_op, sw_od, 80, lflows,
-                    stage_hint, lflow_ref);
+                build_lswitch_rport_arp_req_flow(nat->external_ip, AF_INET6,
+                                                 sw_op, nat_logical_port,
+                                                 lflows, lflow_ref);
             }
         } else {
             if (!sset_contains(&lr_stateful_rec->lb_ips->ips_v4,
                                nat->external_ip)) {
-                build_lswitch_rport_arp_req_flow(
-                    nat->external_ip, AF_INET, sw_op, sw_od, 80, lflows,
-                    stage_hint, lflow_ref);
+                build_lswitch_rport_arp_req_flow(nat->external_ip, AF_INET,
+                                                 sw_op, nat_logical_port,
+                                                 lflows, lflow_ref);
             }
         }
+        if (nat_entry->is_distributed) {
+            free(nat_logical_port);
+        }
     }
 
     struct shash_node *snat_snode;
@@ -9831,23 +9971,25 @@ build_lswitch_rport_arp_req_flows_for_lbnats(
         }
 
         const struct nbrec_nat *nat = nat_entry->nb;
-
+        char *nat_logical_port = nat_entry->l3dgw_port ?
+                                 nat_entry->l3dgw_port->cr_port->json_key :
+                                 NULL;
         /* Check if the ovn port has a network configured on which we could
          * expect ARP requests/NS for the SNAT external_ip.
          */
         if (nat_entry_is_v6(nat_entry)) {
             if (!sset_contains(&lr_stateful_rec->lb_ips->ips_v6,
                                nat->external_ip)) {
-                build_lswitch_rport_arp_req_flow(
-                    nat->external_ip, AF_INET6, sw_op, sw_od, 80, lflows,
-                    stage_hint, lflow_ref);
+                build_lswitch_rport_arp_req_flow(nat->external_ip, AF_INET6,
+                                                 sw_op, nat_logical_port,
+                                                 lflows, lflow_ref);
             }
         } else {
             if (!sset_contains(&lr_stateful_rec->lb_ips->ips_v4,
                                nat->external_ip)) {
-                build_lswitch_rport_arp_req_flow(
-                    nat->external_ip, AF_INET, sw_op, sw_od, 80, lflows,
-                    stage_hint, lflow_ref);
+                build_lswitch_rport_arp_req_flow(nat->external_ip, AF_INET,
+                                                 sw_op, nat_logical_port,
+                                                 lflows, lflow_ref);
             }
         }
     }
@@ -10248,6 +10390,58 @@ build_lswitch_lflows_l2_unknown(struct ovn_datapath 
*od,
                   "output;", lflow_ref);
 }
 
+static void
+build_lswitch_arp_restrictions_default_flows(struct ovn_datapath *od,
+                                             struct lflow_table *lflows,
+                                             struct lflow_ref *lflow_ref)
+{
+    struct sset ha_ports = SSET_INITIALIZER(&ha_ports);
+
+    struct ovn_port *switch_op;
+    VECTOR_FOR_EACH (&od->router_ports, switch_op) {
+        if (is_peer_router_with_centralized_routing(switch_op)) {
+            continue;
+        }
+
+        if (switch_op->peer->cr_port) {
+            sset_add(&ha_ports, switch_op->peer->cr_port->json_key);
+        }
+    }
+
+    if (sset_is_empty(&ha_ports)) {
+        sset_destroy(&ha_ports);
+        return;
+    }
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+    ds_put_cstr(&match, "flags.localnet == 1 && arp.op == 1 && ");
+
+    size_t n_ports = sset_count(&ha_ports);
+    if (n_ports > 1) {
+        ds_put_cstr(&match, "(");
+    }
+
+    const char *cr_port;
+    bool first = true;
+    SSET_FOR_EACH (cr_port, &ha_ports) {
+        if (!first) {
+            ds_put_cstr(&match, " && ");
+        }
+        ds_put_format(&match, "!is_chassis_resident(%s)", cr_port);
+        first = false;
+    }
+
+    if (n_ports > 1) {
+        ds_put_cstr(&match, ")");
+    }
+
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, 75,
+                  ds_cstr(&match), "drop;", lflow_ref);
+
+    ds_destroy(&match);
+    sset_destroy(&ha_ports);
+}
+
 /* Build LB, ACL, QOS related flows for logical switch pipeline stages:
  * - Ingress: tables 5-12, 17-19, 23
  * - Egress: tables 2-8, 10-11
@@ -10361,7 +10555,7 @@ build_lswitch_from_localnet_op(struct ovn_port *op,
                                struct ds *match)
 {
     ovs_assert(op->nbsp);
-    if (!lsp_is_localnet(op->nbsp)) {
+    if (!lsp_is_localnet(op->nbsp) && !lsp_is_l2gw(op->nbsp)) {
         return;
     }
     ds_clear(match);
@@ -10384,9 +10578,8 @@ build_lswitch_arp_nd_local_resp_match(struct ds *match,
         return;
     }
 
-    ds_put_format(match,
-        " && ((flags.localnet == 1 && is_chassis_resident(%s))"
-            " || flags.localnet == 0)", op->json_key);
+    ds_put_format(match, " && " LOCALNET_CHASSIS_RESIDENT_MATCH,
+                  op->json_key);
 }
 
 /* Ingress table 24: ARP/ND responder, reply for known IPs.
@@ -11322,8 +11515,7 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
          * broadcast flooding of ARP/ND requests in table 22. We direct the
          * requests only to the router port that owns the IP address.
          */
-        build_lswitch_rport_arp_req_flows(op->peer, op->od, op, lflows,
-                                          &op->nbsp->header_);
+        build_lswitch_rport_arp_req_flows(op->peer, op, lflows);
 
         ds_clear(match);
         if (!eth_addr_is_zero(op->proxy_arp_addrs.ea)) {
@@ -14120,7 +14312,7 @@ build_lrouter_port_nat_arp_nd_flow(struct ovn_port *op,
         return;
     }
 
-    if (op->peer && op->peer->cr_port) {
+    if (is_router_with_centralized_routing(op)) {
         /* We don't add the below flows if the router port's peer has
          * a chassisredirect port.  That's because routing is centralized on
          * the gateway chassis for the router port networks/subnets.
@@ -14894,10 +15086,6 @@ build_neigh_learning_flows_for_lrouter_port(
                           op->lrp_networks.ipv4_addrs[i].network_s,
                           op->lrp_networks.ipv4_addrs[i].plen,
                           op->lrp_networks.ipv4_addrs[i].addr_s);
-            if (lrp_is_l3dgw(op)) {
-                ds_put_format(match, " && is_chassis_resident(%s)",
-                              op->cr_port->json_key);
-            }
             const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
                               " = lookup_arp(inport, arp.spa, arp.sha); "
                               REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1;"
@@ -14912,10 +15100,6 @@ build_neigh_learning_flows_for_lrouter_port(
                       op->json_key,
                       op->lrp_networks.ipv4_addrs[i].network_s,
                       op->lrp_networks.ipv4_addrs[i].plen);
-        if (lrp_is_l3dgw(op)) {
-            ds_put_format(match, " && is_chassis_resident(%s)",
-                          op->cr_port->json_key);
-        }
         ds_clear(actions);
         ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
                       " = lookup_arp(inport, arp.spa, arp.sha); %snext;",
@@ -17396,7 +17580,6 @@ build_lrouter_ipv4_ip_input_for_lbnats(
             ds_put_format(match, "is_chassis_resident(%s)",
                           op->cr_port->json_key);
         }
-
         /* Create a single ARP rule for all IPs that are used as VIPs. */
         char *lb_ips_v4_as = lr_lb_address_set_ref(op->od->tunnel_key,
                                                    AF_INET);
@@ -18692,8 +18875,7 @@ build_lsp_lflows_for_lbnats(struct ovn_port *lsp,
     ovs_assert(lsp->nbsp);
     ovs_assert(lsp->peer);
     build_lswitch_rport_arp_req_flows_for_lbnats(
-        lsp->peer, lr_stateful_rec, lsp->od, lsp,
-        lflows, &lsp->nbsp->header_, lflow_ref);
+        lsp->peer, lr_stateful_rec, lsp->od, lsp, lflows, lflow_ref);
     build_lswitch_ip_unicast_lookup_for_nats(lsp, lr_stateful_rec, lflows,
                                              match, actions, lflow_ref);
 }
@@ -19540,6 +19722,7 @@ build_lswitch_and_lrouter_iterate_by_ls(struct 
ovn_datapath *od,
         build_lswitch_lflows_l2_unknown(od, lsi->lflows, NULL);
     }
     build_mcast_flood_lswitch(od, lsi->lflows, &lsi->actions, NULL);
+    build_lswitch_arp_restrictions_default_flows(od, lsi->lflows, NULL);
 }
 
 /* Helper function to combine all lflow generation which is iterated by
diff --git a/northd/northd.h b/northd/northd.h
index 74fb58848..97a7e8833 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -388,6 +388,10 @@ enum dynamic_routing_redistribute_mode {
 DRR_MODES
 #undef DRR_MODE
 
+
+#define LOCALNET_CHASSIS_RESIDENT_MATCH \
+    "((flags.localnet == 1 && is_chassis_resident(%s)) || flags.localnet == 0)"
+
 /* The 'key' comes from nbs->header_.uuid or nbr->header_.uuid or
  * sb->header_.uuid. */
 struct ovn_datapath {
@@ -1175,6 +1179,18 @@ od_is_centralized(const struct ovn_datapath *od)
     return !od->is_distributed;
 }
 
+static inline bool
+is_router_with_centralized_routing(struct ovn_port *router_op)
+{
+   return router_op->peer && router_op->peer->cr_port;
+}
+
+static inline bool
+is_peer_router_with_centralized_routing(struct ovn_port *switch_op)
+{
+    return switch_op->cr_port;
+}
+
 struct ovn_port *ovn_port_find(const struct hmap *ports, const char *name);
 
 void build_igmp_lflows(struct hmap *igmp_groups,
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index f87b14c9a..8642a6651 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -2338,17 +2338,10 @@ action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = 
nd.target; nd.tll = xreg0[[0
 
 # Priority 91 drop flows (per distributed gw port), if port is not resident.
 AT_CHECK([ovn-sbctl lflow-list | grep -E "lr_in_ip_input.*priority=91" | grep 
"arp\|nd" | ovn_strip_lflows], [0], [dnl
-  table=??(lr_in_ip_input     ), priority=91   , dnl
-match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.150), 
action=(drop;)
-  table=??(lr_in_ip_input     ), priority=91   , dnl
-match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.2), dnl
-action=(drop;)
-  table=??(lr_in_ip_input     ), priority=91   , dnl
-match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.3), dnl
-action=(drop;)
-  table=??(lr_in_ip_input     ), priority=91   , dnl
-match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.4), dnl
-action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "lrp-public" 
&& arp.op == 1 && arp.tpa == 43.43.43.150), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "lrp-public" 
&& arp.op == 1 && arp.tpa == 43.43.43.2), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "lrp-public" 
&& arp.op == 1 && arp.tpa == 43.43.43.3), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "lrp-public" 
&& arp.op == 1 && arp.tpa == 43.43.43.4), action=(drop;)
 ])
 
 # Priority 92 ARP/NS responders (per distributed gw port), if port is resident.
@@ -6192,19 +6185,20 @@ AT_CHECK([grep "ls_in_l2_lkup" ls1_lflows | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_l2_lkup      ), priority=71   , match=(eth.mcast && ip), 
action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=72   , match=(eth.mcast && (nd_na || 
nd_rs || nd_ra)), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{00:00:00:00:01:01} && eth.dst == ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op 
== 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.100), action=(clone {outport = "ls1-ro1"; 
output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.200), action=(clone {outport = "ls1-ro1"; 
output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "ls1-ro1"; 
output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.100), action=(clone {outport = "ls1-ro1"; 
output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.100), action=(clone {outport = "ls1-ro1"; 
output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.200), action=(clone {outport = "ls1-ro1"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(flags.localnet == 1 && 
arp.op == 1 && !is_chassis_resident("cr-ro1-ls1")), action=(drop;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.100 && ((flags.localnet == 1 && 
is_chassis_resident("cr-ro1-ls1")) || flags.localnet == 0)), action=(clone 
{outport = "ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.200 && ((flags.localnet == 1 && 
is_chassis_resident("cr-ro1-ls1")) || flags.localnet == 0)), action=(clone 
{outport = "ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && ((flags.localnet == 1 && 
is_chassis_resident("cr-ro1-ls1")) || flags.localnet == 0)), action=(clone 
{outport = "ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.100 && ((flags.localnet == 1 && 
is_chassis_resident("cr-ro1-ls1")) || flags.localnet == 0)), action=(clone 
{outport = "ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.100 && ((flags.localnet == 1 && 
is_chassis_resident("cr-ro1-ls1")) || flags.localnet == 0)), action=(clone 
{outport = "ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.200 && ((flags.localnet == 1 && 
is_chassis_resident("cr-ro1-ls1")) || flags.localnet == 0)), action=(clone 
{outport = "ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::200:ff:fe00:101), action=(clone {outport = 
"ls1-ro1"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.100 && arp.spa == 10.0.0.100), action=(outport 
= "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.200 && arp.spa == 10.0.0.200), action=(outport 
= "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.1), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.100 && arp.spa == 192.168.1.100), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.100 && arp.spa == 30.0.0.100), action=(outport 
= "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.200 && arp.spa == 30.0.0.200), action=(outport 
= "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.100 && arp.spa == 10.0.0.100 && 
((flags.localnet == 1 && is_chassis_resident("cr-ro1-ls1")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.0.0.200 && arp.spa == 10.0.0.200 && 
((flags.localnet == 1 && is_chassis_resident("cr-ro1-ls1")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.1 && 
((flags.localnet == 1 && is_chassis_resident("cr-ro1-ls1")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.100 && arp.spa == 192.168.1.100 && 
((flags.localnet == 1 && is_chassis_resident("cr-ro1-ls1")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.100 && arp.spa == 30.0.0.100 && 
((flags.localnet == 1 && is_chassis_resident("cr-ro1-ls1")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 30.0.0.200 && arp.spa == 30.0.0.200 && 
((flags.localnet == 1 && is_chassis_resident("cr-ro1-ls1")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
 ])
 
 
@@ -7689,9 +7683,6 @@ AT_CHECK([grep lr_in_admission lrflows | grep cr-DR | 
ovn_strip_lflows], [0], [d
 ])
 # Check the flows in lr_in_lookup_neighbor stage
 AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep cr-DR | ovn_strip_lflows], 
[0], [dnl
-  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == "DR-S1" && 
arp.spa == 172.16.1.0/24 && arp.op == 1 && is_chassis_resident("cr-DR-S1")), 
action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
-  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == "DR-S2" && 
arp.spa == 172.16.2.0/24 && arp.op == 1 && is_chassis_resident("cr-DR-S2")), 
action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
-  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == "DR-S3" && 
arp.spa == 172.16.3.0/24 && arp.op == 1 && is_chassis_resident("cr-DR-S3")), 
action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
   table=??(lr_in_lookup_neighbor), priority=120  , match=(inport == "DR-S1" && 
(nd_na || nd_ns) && eth.mcast && !is_chassis_resident("cr-DR-S1")), 
action=(reg9[[2]] = 1; next;)
   table=??(lr_in_lookup_neighbor), priority=120  , match=(inport == "DR-S2" && 
(nd_na || nd_ns) && eth.mcast && !is_chassis_resident("cr-DR-S2")), 
action=(reg9[[2]] = 1; next;)
   table=??(lr_in_lookup_neighbor), priority=120  , match=(inport == "DR-S3" && 
(nd_na || nd_ns) && eth.mcast && !is_chassis_resident("cr-DR-S3")), 
action=(reg9[[2]] = 1; next;)
@@ -14637,15 +14628,16 @@ AT_CHECK([grep "ls_in_l2_lkup" publicflows | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_l2_lkup      ), priority=71   , match=(eth.mcast && ip), 
action=(outport = "_MC_flood_l2"; output;)
   table=??(ls_in_l2_lkup      ), priority=72   , match=(eth.mcast && (nd_na || 
nd_rs || nd_ra)), action=(outport = "_MC_flood"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{00:00:00:00:ff:02, 30:54:00:00:00:03} && eth.dst == ff:ff:ff:ff:ff:ff && 
(arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; 
output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.10), action=(clone {outport = "public-lr0"; 
output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.100), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(flags.localnet == 1 && 
arp.op == 1 && !is_chassis_resident("cr-lr0-public")), action=(drop;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.10 && ((flags.localnet == 1 && 
is_chassis_resident("cr-lr0-public")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.100 && ((flags.localnet == 1 && 
is_chassis_resident("cr-lr0-public")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && ((flags.localnet == 1 && 
is_chassis_resident("sw0-port1")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && ((flags.localnet == 1 && 
is_chassis_resident("cr-lr0-public")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
   table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::200:ff:fe00:ff02), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.10 && arp.spa == 172.168.0.10), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.100 && arp.spa == 172.168.0.100), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && arp.spa == 172.168.0.110), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && arp.spa == 172.168.0.120), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.10 && arp.spa == 172.168.0.10 && 
((flags.localnet == 1 && is_chassis_resident("cr-lr0-public")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.100 && arp.spa == 172.168.0.100 && 
((flags.localnet == 1 && is_chassis_resident("cr-lr0-public")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && arp.spa == 172.168.0.110 && 
((flags.localnet == 1 && is_chassis_resident("sw0-port1")) || flags.localnet == 
0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && arp.spa == 172.168.0.120 && 
((flags.localnet == 1 && is_chassis_resident("cr-lr0-public")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
 ])
 
 AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" -e "10.0.0.3" -e 
"20.0.0.3" -e "30:54:00:00:00:03"  -e "sw0-port1" lr0flows | ovn_strip_lflows], 
[0], [dnl
@@ -14676,10 +14668,10 @@ AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" 
-e "10.0.0.3" -e "20.0.0.3
 AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" -e "10.0.0.3" -e 
"20.0.0.3" -e "30:54:00:00:00:03"  -e "sw0-port1" publicflows | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_l2_lkup      ), priority=50   , match=(eth.dst == 
30:54:00:00:00:03 && is_chassis_resident("sw0-port1")), action=(outport = 
"public-lr0"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{00:00:00:00:ff:02, 30:54:00:00:00:03} && eth.dst == ff:ff:ff:ff:ff:ff && 
(arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; 
output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && arp.spa == 172.168.0.110), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && arp.spa == 172.168.0.120), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && ((flags.localnet == 1 && 
is_chassis_resident("sw0-port1")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && ((flags.localnet == 1 && 
is_chassis_resident("cr-lr0-public")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && arp.spa == 172.168.0.110 && 
((flags.localnet == 1 && is_chassis_resident("sw0-port1")) || flags.localnet == 
0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && arp.spa == 172.168.0.120 && 
((flags.localnet == 1 && is_chassis_resident("cr-lr0-public")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
 ])
 }
 
@@ -14930,10 +14922,10 @@ AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" 
-e "10.0.0.3" -e "20.0.0.3
 AT_CHECK([grep -Fe "172.168.0.110" -e "172.168.0.120" -e "10.0.0.3" -e 
"20.0.0.3" -e "30:54:00:00:00:03"  -e "sw0-port1" publicflows | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_l2_lkup      ), priority=50   , match=(eth.dst == 
30:54:00:00:00:03 && is_chassis_resident("sw0-port1")), action=(outport = 
"public-lr0"; output;)
   table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{00:00:00:00:ff:02, 30:54:00:00:00:03} && eth.dst == ff:ff:ff:ff:ff:ff && 
(arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = "_MC_flood_l2"; 
output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120), action=(clone {outport = 
"public-lr0"; output; }; outport = "_MC_unknown"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && arp.spa == 172.168.0.110), 
action=(outport = "_MC_flood_l2"; output;)
-  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && arp.spa == 172.168.0.120), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && ((flags.localnet == 1 && 
is_chassis_resident("sw0-port1")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && ((flags.localnet == 1 && 
is_chassis_resident("cr-lr0-public")) || flags.localnet == 0)), action=(clone 
{outport = "public-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.110 && arp.spa == 172.168.0.110 && 
((flags.localnet == 1 && is_chassis_resident("sw0-port1")) || flags.localnet == 
0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.168.0.120 && arp.spa == 172.168.0.120 && 
((flags.localnet == 1 && is_chassis_resident("cr-lr0-public")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
 ])
 
 OVN_CLEANUP_NORTHD
@@ -20933,3 +20925,250 @@ check_column "$global_svc_mon_mac" sb:Service_Monitor 
src_mac port=2
 OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([Logical Switch ARP filtering])
+ovn_start
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lr-add lr2
+check ovn-nbctl lrp-add lr1 down_link f0:00:00:00:00:f1 192.168.1.1/24 
aef0::200:ff:fe00:3/64
+check ovn-nbctl lrp-add lr1 private_link f0:00:00:00:00:f2 172.31.1.1/24 
fd11::1/64
+
+check ovn-nbctl ls-add ls1
+check ovn-nbctl ls-add private
+
+check ovn-nbctl lsp-add ls1 down_vif1
+check ovn-nbctl lsp-add ls1 down_ext
+check ovn-nbctl lsp-add-router-port ls1 up_link down_link
+
+check ovn-nbctl lsp-add private private_vif1_ipv4
+check ovn-nbctl lsp-add private private_vif1_ipv6
+check ovn-nbctl lsp-add-router-port private pr_uplink private_link
+
+check ovn-nbctl lsp-set-addresses down_vif1 'f0:00:00:00:00:01 192.168.1.101'
+check ovn-nbctl lsp-set-addresses private_vif1_ipv4 'f0:00:00:00:00:05 
172.31.0.101'
+check ovn-nbctl lsp-set-addresses private_vif1_ipv6 'f0:00:00:00:00:05 fd11::2'
+
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat 172.31.1.2 172.31.0.101 \
+    private_vif1_ipv4 f0:00:00:00:00:07
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat fd12::2 fd11::2 \
+    private_vif1_ipv6 f0:00:00:00:00:06
+
+check ovn-nbctl lb-add lb1 99.99.99.99:80 172.31.0.9:10880
+check ovn-nbctl lr-lb-add lr1 lb1
+
+# Create two NATs: one of them is part of the router subnet;
+# for the second one, a gateway (GW) will need to be added later
+# when testing with multiple DGPs
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat 10.10.10.13 172.31.0.103
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat fd12::3 fd11::3
+check ovn-nbctl lr-nat-add lr1 snat 192.168.1.14 172.31.0.0/24
+check ovn-nbctl lr-nat-add lr1 snat aef0::200:ff:fe00:4 fd11::/64
+
+check ovn-nbctl --wait=sb sync
+
+# Check base routing (without any gateways): ARP/ND in ls_in_l2_lkup should be 
redirected to the router port and flooded to the unknown multicast group
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_arp_rsp' | grep 
'priority=50'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 192.168.1.1 
&& arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff), action=(eth.dst = eth.src; 
eth.src = f0:00:00:00:00:f1; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; 
arp.sha = f0:00:00:00:00:f1; arp.tpa = arp.spa; arp.spa = 192.168.1.1; outport 
= inport; flags.loopback = 1; output;)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 
192.168.1.101 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff), action=(eth.dst 
= eth.src; eth.src = f0:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = 
arp.sha; arp.sha = f0:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 
192.168.1.101; outport = inport; flags.loopback = 1; output;)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:3 && nd.target == aef0::200:ff:fe00:3), action=(nd_na_router { 
eth.src = f0:00:00:00:00:f1; ip6.src = aef0::200:ff:fe00:3; nd.target = 
aef0::200:ff:fe00:3; nd.tll = f0:00:00:00:00:f1; outport = inport; 
flags.loopback = 1; output; };)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:f1 && nd.target == fe80::f200:ff:fe00:f1), action=(nd_na_router 
{ eth.src = f0:00:00:00:00:f1; ip6.src = fe80::f200:ff:fe00:f1; nd.target = 
fe80::f200:ff:fe00:f1; nd.tll = f0:00:00:00:00:f1; outport = inport; 
flags.loopback = 1; output; };)
+])
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_l2_lkup' | grep -E 
'priority=75|priority=80|priority=90'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{f0:00:00:00:00:f1, f0:00:00:00:00:06, f0:00:00:00:00:07} && eth.dst == 
ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = 
"_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:3), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:4), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::2), action=(clone {outport = "up_link"; output; }; 
outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::3), action=(clone {outport = "up_link"; output; }; 
outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::f200:ff:fe00:f1), action=(clone {outport = 
"up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && arp.spa == 10.10.10.13), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2 && arp.spa == 172.31.1.2), action=(outport 
= "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.1), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && arp.spa == 192.168.1.14), 
action=(outport = "_MC_flood_l2"; output;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_lookup_neighbor' | grep 
lookup_arp | ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(arp.op == 2), 
action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == 
"down_link" && arp.spa == 192.168.1.0/24 && arp.op == 1), action=(reg9[[2]] = 
lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == 
"private_link" && arp.spa == 172.31.1.0/24 && arp.op == 1), action=(reg9[[2]] = 
lookup_arp(inport, arp.spa, arp.sha); next;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_ip_input' | grep -E 
'priority=92|priority=91|priority=90'| ovn_strip_lflows | grep -E 'arp|nd'], 
[0], [dnl
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa 
== 10.10.10.13), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa 
== 172.31.1.2), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa 
== 192.168.1.14), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.0/24), 
action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply 
*/ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = 
inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "down_link" 
&& ip6.dst == {aef0::200:ff:fe00:3, ff02::1:ff00:3} && nd_ns && nd.target == 
aef0::200:ff:fe00:3), action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src 
= nd.target; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; 
output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "down_link" 
&& ip6.dst == {fe80::f200:ff:fe00:f1, ff02::1:ff00:f1} && nd_ns && nd.target == 
fe80::f200:ff:fe00:f1), action=(nd_na_router { eth.src = xreg0[[0..47]]; 
ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback 
= 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == 
"private_link" && arp.op == 1 && arp.tpa == 172.31.1.1 && arp.spa == 
172.31.1.0/24), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == 
"private_link" && ip6.dst == {fd11::1, ff02::1:ff00:1} && nd_ns && nd.target == 
fd11::1), action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = nd.target; 
nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == 
"private_link" && ip6.dst == {fe80::f200:ff:fe00:f2, ff02::1:ff00:f2} && nd_ns 
&& nd.target == fe80::f200:ff:fe00:f2), action=(nd_na_router { eth.src = 
xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; 
flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst == 
{aef0::200:ff:fe00:4, ff02::1:ff00:4} && nd_ns && nd.target == 
aef0::200:ff:fe00:4), action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = 
nd.target; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; 
output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst == {fd12::2, 
ff02::1:ff00:2} && nd_ns && nd.target == fd12::2), action=(nd_na { eth.src = 
xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; 
flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst == {fd12::3, 
ff02::1:ff00:3} && nd_ns && nd.target == fd12::3), action=(nd_na { eth.src = 
xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; 
flags.loopback = 1; output; };)
+])
+
+# Create a gateway port to centralize routing on hv1
+check ovn-nbctl lrp-set-gateway-chassis down_link hv1
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_arp_rsp' | grep 
'priority=50'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 192.168.1.1 
&& arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff), action=(eth.dst = eth.src; 
eth.src = f0:00:00:00:00:f1; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; 
arp.sha = f0:00:00:00:00:f1; arp.tpa = arp.spa; arp.spa = 192.168.1.1; outport 
= inport; flags.loopback = 1; output;)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 
192.168.1.101 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff), action=(eth.dst 
= eth.src; eth.src = f0:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = 
arp.sha; arp.sha = f0:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 
192.168.1.101; outport = inport; flags.loopback = 1; output;)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:3 && nd.target == aef0::200:ff:fe00:3), action=(nd_na_router { 
eth.src = f0:00:00:00:00:f1; ip6.src = aef0::200:ff:fe00:3; nd.target = 
aef0::200:ff:fe00:3; nd.tll = f0:00:00:00:00:f1; outport = inport; 
flags.loopback = 1; output; };)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:f1 && nd.target == fe80::f200:ff:fe00:f1), action=(nd_na_router 
{ eth.src = f0:00:00:00:00:f1; ip6.src = fe80::f200:ff:fe00:f1; nd.target = 
fe80::f200:ff:fe00:f1; nd.tll = f0:00:00:00:00:f1; outport = inport; 
flags.loopback = 1; output; };)
+])
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_l2_lkup' | grep -E 
'priority=75|priority=80|priority=90'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{f0:00:00:00:00:f1, f0:00:00:00:00:06, f0:00:00:00:00:07} && eth.dst == 
ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = 
"_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && !is_chassis_resident("cr-up_link")), 
action=(clone {outport = "cr-up_link"; output; }; outport = "_MC_unknown"; 
output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && is_chassis_resident("cr-up_link")), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2 && !is_chassis_resident("cr-up_link")), 
action=(clone {outport = "cr-up_link"; output; }; outport = "_MC_unknown"; 
output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2 && is_chassis_resident("cr-up_link")), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && !is_chassis_resident("cr-up_link")), 
action=(clone {outport = "cr-up_link"; output; }; outport = "_MC_unknown"; 
output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && is_chassis_resident("cr-up_link")), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && !is_chassis_resident("cr-up_link")), 
action=(clone {outport = "cr-up_link"; output; }; outport = "_MC_unknown"; 
output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && is_chassis_resident("cr-up_link")), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:3 && 
!is_chassis_resident("cr-up_link")), action=(clone {outport = "cr-up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:3 && 
is_chassis_resident("cr-up_link")), action=(clone {outport = "up_link"; output; 
}; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:4 && 
!is_chassis_resident("cr-up_link")), action=(clone {outport = "cr-up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:4 && 
is_chassis_resident("cr-up_link")), action=(clone {outport = "up_link"; output; 
}; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::2 && !is_chassis_resident("cr-up_link")), 
action=(clone {outport = "cr-up_link"; output; }; outport = "_MC_unknown"; 
output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::2 && is_chassis_resident("cr-up_link")), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::3 && !is_chassis_resident("cr-up_link")), 
action=(clone {outport = "cr-up_link"; output; }; outport = "_MC_unknown"; 
output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::3 && is_chassis_resident("cr-up_link")), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::f200:ff:fe00:f1 && 
!is_chassis_resident("cr-up_link")), action=(clone {outport = "cr-up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::f200:ff:fe00:f1 && 
is_chassis_resident("cr-up_link")), action=(clone {outport = "up_link"; output; 
}; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && arp.spa == 10.10.10.13), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2 && arp.spa == 172.31.1.2), action=(outport 
= "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.1), 
action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && arp.spa == 192.168.1.14), 
action=(outport = "_MC_flood_l2"; output;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_lookup_neighbor' | grep 
lookup_arp | ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(arp.op == 2), 
action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == 
"down_link" && arp.spa == 192.168.1.0/24 && arp.op == 1), action=(reg9[[2]] = 
lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == 
"private_link" && arp.spa == 172.31.1.0/24 && arp.op == 1), action=(reg9[[2]] = 
lookup_arp(inport, arp.spa, arp.sha); next;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_ip_input' | grep -E 
'priority=92|priority=91|priority=90'| ovn_strip_lflows | grep -E 'arp|nd'], 
[0], [dnl
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa 
== 10.10.10.13), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa 
== 172.31.1.2), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa 
== 192.168.1.14), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.0/24), 
action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply 
*/ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = 
inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "down_link" 
&& ip6.dst == {aef0::200:ff:fe00:3, ff02::1:ff00:3} && nd_ns && nd.target == 
aef0::200:ff:fe00:3 && is_chassis_resident("cr-down_link")), 
action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = nd.target; nd.tll = 
xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "down_link" 
&& ip6.dst == {fe80::f200:ff:fe00:f1, ff02::1:ff00:f1} && nd_ns && nd.target == 
fe80::f200:ff:fe00:f1 && is_chassis_resident("cr-down_link")), 
action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = nd.target; nd.tll = 
xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == 
"private_link" && arp.op == 1 && arp.tpa == 172.31.1.1 && arp.spa == 
172.31.1.0/24), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 
2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> 
arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == 
"private_link" && ip6.dst == {fd11::1, ff02::1:ff00:1} && nd_ns && nd.target == 
fd11::1), action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = nd.target; 
nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(inport == 
"private_link" && ip6.dst == {fe80::f200:ff:fe00:f2, ff02::1:ff00:f2} && nd_ns 
&& nd.target == fe80::f200:ff:fe00:f2), action=(nd_na_router { eth.src = 
xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; 
flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst == 
{aef0::200:ff:fe00:4, ff02::1:ff00:4} && nd_ns && nd.target == 
aef0::200:ff:fe00:4), action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = 
nd.target; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; 
output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst == {fd12::2, 
ff02::1:ff00:2} && nd_ns && nd.target == fd12::2), action=(nd_na { eth.src = 
xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; 
flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst == {fd12::3, 
ff02::1:ff00:3} && nd_ns && nd.target == fd12::3), action=(nd_na { eth.src = 
xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; 
flags.loopback = 1; output; };)
+])
+
+# Check routing become distributed
+check ovn-nbctl lsp-add-localnet-port ls1 localnet physical
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_arp_rsp' | grep 
'priority=50'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 192.168.1.1 
&& arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
is_chassis_resident("up_link")) || flags.localnet == 0)), action=(eth.dst = 
eth.src; eth.src = f0:00:00:00:00:f1; arp.op = 2; /* ARP reply */ arp.tha = 
arp.sha; arp.sha = f0:00:00:00:00:f1; arp.tpa = arp.spa; arp.spa = 192.168.1.1; 
outport = inport; flags.loopback = 1; output;)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 
192.168.1.101 && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && 
((flags.localnet == 1 && is_chassis_resident("down_vif1")) || flags.localnet == 
0)), action=(eth.dst = eth.src; eth.src = f0:00:00:00:00:01; arp.op = 2; /* ARP 
reply */ arp.tha = arp.sha; arp.sha = f0:00:00:00:00:01; arp.tpa = arp.spa; 
arp.spa = 192.168.1.101; outport = inport; flags.loopback = 1; output;)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:3 && nd.target == aef0::200:ff:fe00:3 && ((flags.localnet == 1 
&& is_chassis_resident("up_link")) || flags.localnet == 0)), 
action=(nd_na_router { eth.src = f0:00:00:00:00:f1; ip6.src = 
aef0::200:ff:fe00:3; nd.target = aef0::200:ff:fe00:3; nd.tll = 
f0:00:00:00:00:f1; outport = inport; flags.loopback = 1; output; };)
+  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:f1 && nd.target == fe80::f200:ff:fe00:f1 && ((flags.localnet == 
1 && is_chassis_resident("up_link")) || flags.localnet == 0)), 
action=(nd_na_router { eth.src = f0:00:00:00:00:f1; ip6.src = 
fe80::f200:ff:fe00:f1; nd.target = fe80::f200:ff:fe00:f1; nd.tll = 
f0:00:00:00:00:f1; outport = inport; flags.loopback = 1; output; };)
+])
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_l2_lkup' | grep -E 
'priority=75|priority=80|priority=90'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{f0:00:00:00:00:f1, f0:00:00:00:00:06, f0:00:00:00:00:07} && eth.dst == 
ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op == 3 || nd_ns)), action=(outport = 
"_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(flags.localnet == 1 && 
arp.op == 1 && !is_chassis_resident("cr-down_link")), action=(drop;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && ((flags.localnet == 1 && 
is_chassis_resident("cr-down_link")) || flags.localnet == 0)), action=(clone 
{outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2 && ((flags.localnet == 1 && 
is_chassis_resident("private_vif1_ipv4")) || flags.localnet == 0)), 
action=(clone {outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && ((flags.localnet == 1 && 
is_chassis_resident("cr-down_link")) || flags.localnet == 0)), action=(clone 
{outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && ((flags.localnet == 1 && 
is_chassis_resident("cr-down_link")) || flags.localnet == 0)), action=(clone 
{outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:3), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:4), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::2), action=(clone {outport = "up_link"; output; }; 
outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::3), action=(clone {outport = "up_link"; output; }; 
outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::f200:ff:fe00:f1), action=(clone {outport = 
"up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && arp.spa == 10.10.10.13 && 
((flags.localnet == 1 && is_chassis_resident("cr-down_link")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 172.31.1.2 && arp.spa == 172.31.1.2 && 
((flags.localnet == 1 && is_chassis_resident("private_vif1_ipv4")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.1 && 
((flags.localnet == 1 && is_chassis_resident("cr-down_link")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && arp.spa == 192.168.1.14 && 
((flags.localnet == 1 && is_chassis_resident("cr-down_link")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_l2_lkup' | grep priority=75 | 
grep flags.localnet | ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(flags.localnet == 1 && 
arp.op == 1 && !is_chassis_resident("cr-down_link")), action=(drop;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_lookup_neighbor' | grep 
lookup_arp | ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(arp.op == 2), 
action=(reg9[[2]] = lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == 
"down_link" && arp.spa == 192.168.1.0/24 && arp.op == 1), action=(reg9[[2]] = 
lookup_arp(inport, arp.spa, arp.sha); next;)
+  table=??(lr_in_lookup_neighbor), priority=100  , match=(inport == 
"private_link" && arp.spa == 172.31.1.0/24 && arp.op == 1), action=(reg9[[2]] = 
lookup_arp(inport, arp.spa, arp.sha); next;)
+])
+
+# expect drop only for ipv6
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_ip_input' | grep -E 
'priority=91'| ovn_strip_lflows | grep -E 'arp|nd'], [0], [dnl
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 10.10.10.13), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 172.31.1.2), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 192.168.1.14), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "down_link" 
&& ip6.dst == {aef0::200:ff:fe00:4, ff02::1:ff00:4} && nd_ns && nd.target == 
aef0::200:ff:fe00:4), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "down_link" 
&& ip6.dst == {fd12::2, ff02::1:ff00:2} && nd_ns && nd.target == fd12::2), 
action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "down_link" 
&& ip6.dst == {fd12::3, ff02::1:ff00:3} && nd_ns && nd.target == fd12::3), 
action=(drop;)
+])
+
+AT_CHECK([ovn-sbctl lflow-list lr1 | grep 'lr_in_ip_input' | grep -E 
'priority=92'| ovn_strip_lflows | grep -E 'arp|nd'], [0], [dnl
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 10.10.10.13 && 
is_chassis_resident("cr-down_link")), action=(eth.dst = eth.src; eth.src = 
xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 
xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; 
output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 172.31.1.2 && 
is_chassis_resident("private_vif1_ipv4")), action=(eth.dst = eth.src; eth.src = 
f0:00:00:00:00:07; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 
f0:00:00:00:00:07; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; 
output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "down_link" 
&& arp.op == 1 && arp.tpa == 192.168.1.14 && 
is_chassis_resident("cr-down_link")), action=(eth.dst = eth.src; eth.src = 
xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 
xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; 
output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "down_link" 
&& ip6.dst == {aef0::200:ff:fe00:4, ff02::1:ff00:4} && nd_ns && nd.target == 
aef0::200:ff:fe00:4 && is_chassis_resident("cr-down_link")), action=(nd_na { 
eth.src = xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport 
= inport; flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "down_link" 
&& ip6.dst == {fd12::2, ff02::1:ff00:2} && nd_ns && nd.target == fd12::2 && 
is_chassis_resident("private_vif1_ipv6")), action=(nd_na { eth.src = 
f0:00:00:00:00:06; ip6.src = nd.target; nd.tll = f0:00:00:00:00:06; outport = 
inport; flags.loopback = 1; output; };)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "down_link" 
&& ip6.dst == {fd12::3, ff02::1:ff00:3} && nd_ns && nd.target == fd12::3 && 
is_chassis_resident("cr-down_link")), action=(nd_na { eth.src = xreg0[[0..47]]; 
ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback 
= 1; output; };)
+])
+
+# Add one more dgp port: check drop flow
+check ovn-nbctl lrp-add lr2 down_link_2 f0:00:00:00:00:f8 192.168.1.8/24 
aef0::200:ff:fe00:8
+check ovn-nbctl lrp-set-gateway-chassis down_link_2 hv2
+check ovn-nbctl lsp-add-router-port ls1 pr_uplink2 down_link_2
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_l2_lkup' | grep priority=75 | 
grep flags.localnet | ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(flags.localnet == 1 && 
arp.op == 1 && (!is_chassis_resident("cr-down_link_2") && 
!is_chassis_resident("cr-down_link"))), action=(drop;)
+])
+
+# Create another DGP on the router: verify that flows in ls_in_l2_lkup
+# will have the correct CR port - those in the router subnets use the DGP in 
the subnet,
+# while others are NATed with the specified DGP port
+check ovn-nbctl lrp-add lr1 lrp-gw2 f0:00:00:00:00:f1 11.11.11.1/24 
aeff::200:ff:fe00:3/64
+check ovn-nbctl lrp-set-gateway-chassis lrp-gw2 hv2
+
+lrp_uuid=$(fetch_column nb:logical_router_port _uuid name=lrp-gw2)
+uuid=$(fetch_column nb:nat _uuid external_ip=10.10.10.13)
+check ovn-nbctl set nat $uuid gateway_port=$lrp_uuid
+uuid=$(fetch_column nb:nat _uuid external_ip='"fd12::3"')
+check ovn-nbctl set nat $uuid gateway_port=$lrp_uuid
+
+AT_CHECK([ovn-sbctl lflow-list ls1 | grep 'ls_in_l2_lkup' | grep -E 
'priority=75|priority=80|priority=90'| ovn_strip_lflows], [0], [dnl
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{f0:00:00:00:00:f1} && eth.dst == ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op 
== 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(eth.src == 
{f0:00:00:00:00:f8} && eth.dst == ff:ff:ff:ff:ff:ff && (arp.op == 1 || rarp.op 
== 3 || nd_ns)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=75   , match=(flags.localnet == 1 && 
arp.op == 1 && (!is_chassis_resident("cr-down_link_2") && 
!is_chassis_resident("cr-down_link"))), action=(drop;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && ((flags.localnet == 1 && 
is_chassis_resident("cr-lrp-gw2")) || flags.localnet == 0)), action=(clone 
{outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && ((flags.localnet == 1 && 
is_chassis_resident("cr-down_link")) || flags.localnet == 0)), action=(clone 
{outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && ((flags.localnet == 1 && 
is_chassis_resident("cr-down_link")) || flags.localnet == 0)), action=(clone 
{outport = "up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.8 && ((flags.localnet == 1 && 
is_chassis_resident("cr-down_link_2")) || flags.localnet == 0)), action=(clone 
{outport = "pr_uplink2"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:3), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:4), action=(clone {outport = "up_link"; 
output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == aef0::200:ff:fe00:8), action=(clone {outport = 
"pr_uplink2"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fd12::3), action=(clone {outport = "up_link"; output; }; 
outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::f200:ff:fe00:f1), action=(clone {outport = 
"up_link"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
nd_ns && nd.target == fe80::f200:ff:fe00:f8), action=(clone {outport = 
"pr_uplink2"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 10.10.10.13 && arp.spa == 10.10.10.13 && 
((flags.localnet == 1 && is_chassis_resident("cr-lrp-gw2")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.1 && arp.spa == 192.168.1.1 && 
((flags.localnet == 1 && is_chassis_resident("cr-down_link")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.14 && arp.spa == 192.168.1.14 && 
((flags.localnet == 1 && is_chassis_resident("cr-down_link")) || flags.localnet 
== 0)), action=(outport = "_MC_flood_l2"; output;)
+  table=??(ls_in_l2_lkup      ), priority=90   , match=(flags[[1]] == 0 && 
arp.op == 1 && arp.tpa == 192.168.1.8 && arp.spa == 192.168.1.8 && 
((flags.localnet == 1 && is_chassis_resident("cr-down_link_2")) || 
flags.localnet == 0)), action=(outport = "_MC_flood_l2"; output;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
diff --git a/tests/ovn.at b/tests/ovn.at
index 522c1c90d..72d265b42 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -27971,6 +27971,8 @@ match_send_rtr2="load:0x${r2_tnl_key}->NXM_NX_REG15"
 
 as hv1
 OVS_WAIT_UNTIL([
+    ovs-ofctl dump-flows br-int | \
+    grep -E "${match_arp_req}" | grep "${match_send_rtr1}"
     pkts_to_rtr1=$(ovs-ofctl dump-flows br-int | \
     grep -E "${match_arp_req}" | grep "${match_send_rtr1}" | \
     grep n_packets=1 -c)
@@ -28035,12 +28037,19 @@ AT_CAPTURE_FILE([sbflows2])
 as hv1
 AT_CHECK([ovs-ofctl dump-flows br-int | grep -E 
"priority=80,.*${match_sw_metadata}" | grep -oE "arp_tpa=[[0-9.]]+" | sort], 
[0], [dnl
 arp_tpa=10.0.0.1
+arp_tpa=10.0.0.1
+arp_tpa=10.0.0.11
 arp_tpa=10.0.0.11
 arp_tpa=10.0.0.111
+arp_tpa=10.0.0.111
+arp_tpa=10.0.0.121
 arp_tpa=10.0.0.121
 arp_tpa=10.0.0.122
 arp_tpa=10.0.0.2
+arp_tpa=10.0.0.2
 arp_tpa=10.0.0.22
+arp_tpa=10.0.0.22
+arp_tpa=10.0.0.222
 arp_tpa=10.0.0.222
 ])
 AT_CHECK([ovs-ofctl dump-flows br-int | grep -E 
"priority=80,.*${match_sw_metadata}" | grep -oE "nd_target=[[0-9a-f:]]+" | 
sort], [0], [dnl
@@ -46220,3 +46229,156 @@ OVN_CLEANUP([hv])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([GARP delivery: gw and external ports])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
+ovn_start
+
+# Configure initial environment
+# LR1: down_link <-> LS1: up_link
+# set lr_down: gateway port (chassis redirect) bound to hv1
+# LS1: down_vif1 - vif port bound to hv1
+#      down_vif2 - vif port bound to hv2
+#      down_ext - outer (port will be iterated as localnet, l2gateway)
+#
+# Test: send GARP request from virtual ports (down_vif1, down_vif2)
+#       ensure mac_binding is always updated.
+#       (Fixing the issue: mac_binding is only updated for packets came from
+#        down_link's resident chassis)
+#       send GARP request from from localnet.
+#       ensure mac_binding is updated only if localnet bound to same hv as 
l3dgw
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 down_link f0:00:00:00:00:f1 192.168.1.1/24
+
+check ovn-nbctl ls-add ls1
+check ovn-nbctl lsp-add ls1 up_link
+check ovn-nbctl lsp-add ls1 down_vif1
+check ovn-nbctl lsp-add ls1 down_vif2
+check ovn-nbctl lsp-add ls1 down_ext
+
+check ovn-nbctl set Logical_Switch_Port up_link \
+    type=router \
+    options:router-port=down_link \
+    addresses=router
+
+check ovn-nbctl lsp-set-addresses down_vif1 'f0:00:00:00:00:01 192.168.1.101'
+check ovn-nbctl lsp-set-addresses down_vif2 'f0:00:00:00:00:02 192.168.1.102'
+
+check ovn-nbctl lsp-set-type down_ext localnet
+check ovn-nbctl lsp-set-options down_ext network_name=physnet1
+check ovn-nbctl lrp-set-gateway-chassis down_link hv1
+
+net_add n1
+
+# Create hypervisor hv1 connected to n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl add-port br-int vif1 -- \
+    set Interface vif1 external-ids:iface-id=down_vif1 \
+    options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap
+
+# Create hypervisor hv2 connected to n1, add localnet here
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovs-vsctl add-br br-eth0
+ovn_attach n1 br-phys 192.168.0.2
+ovs-vsctl add-port br-int vif2 -- \
+    set Interface vif2 external-ids:iface-id=down_vif2 \
+    options:tx_pcap=hv2/vif2-tx.pcap options:rxq_pcap=hv2/vif2-rx.pcap
+
+ovs-vsctl set Open_vSwitch . 
external_ids:ovn-bridge-mappings="physnet1:br-eth0"
+
+ovs-vsctl add-port br-eth0 vif_ext -- \
+    set Interface vif_ext options:tx_pcap=hv2/vif_ext-tx.pcap \
+    options:rxq_pcap=hv2/vif_ext-rx.pcap
+
+# Pre-populate the hypervisors' ARP tables so that we don't lose any
+# packets for ARP resolution (native tunneling doesn't queue packets
+# for ARP resolution).
+OVN_POPULATE_ARP
+
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+# Annonce 192.168.1.222 from localnet in hv2
+# result: drop, hv2 is not gateway chassis for down_link
+sha=02:00:00:00:00:ee
+tha=00:00:00:00:00:00
+spa=192.168.1.222
+tpa=$spa
+garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', 
pdst='${tpa}')")
+as hv2 ovs-appctl netdev-dummy/receive vif_ext $garp
+
+# Make hv2 gateway chassis
+# Annonce 192.168.1.223 from localnet in hv2
+# result: ok, hv2 is gateway chassis for down_link
+#
+check ovn-nbctl lrp-set-gateway-chassis down_link hv2
+
+wait_row_count Port_Binding 1 logical_port=cr-down_link 'chassis!=[[]]'
+check ovn-nbctl --wait=hv sync
+
+sha=02:00:00:00:00:ee
+tha=00:00:00:00:00:00
+spa=192.168.1.223
+tpa=$spa
+garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', 
pdst='${tpa}')")
+as hv2 ovs-appctl netdev-dummy/receive vif_ext $garp
+
+# Annonce 192.168.1.111, 112 from vif1, vif2 in hv1, hv2
+# result: ok, vif1, vif2 are virtual ports, restrictions are not applied.
+sha=f0:00:00:00:00:01
+tha=00:00:00:00:00:00
+spa=192.168.1.111
+tpa=0.0.0.0
+garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', 
pdst='${tpa}')")
+as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
+
+sha=f0:00:00:00:00:02
+tha=00:00:00:00:00:00
+spa=192.168.1.112
+tpa=0.0.0.0
+garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', 
pdst='${tpa}')")
+as hv2 ovs-appctl netdev-dummy/receive vif2 $garp
+
+# Set down_ext type to l2gateway
+# Annonce 192.168.1.113, 114 from vif1, vif2 in hv1, hv2
+# result: ok, vif1, vif2 are virtual ports, restrictions are not applied.
+check ovn-nbctl --wait=hv lsp-set-type down_ext l2gateway
+
+sha=f0:00:00:00:00:01
+tha=00:00:00:00:00:00
+spa=192.168.1.113
+tpa=0.0.0.0
+garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', 
pdst='${tpa}')")
+as hv1 ovs-appctl netdev-dummy/receive vif1 $garp
+
+sha=f0:00:00:00:00:02
+tha=00:00:00:00:00:00
+spa=192.168.1.114
+tpa=0.0.0.0
+garp=$(fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
+                ARP(hwsrc='${sha}', hwdst='${tha}', psrc='${spa}', 
pdst='${tpa}')")
+as hv2 ovs-appctl netdev-dummy/receive vif2 $garp
+
+wait_row_count MAC_Binding 1 ip="192.168.1.111" mac='"f0:00:00:00:00:01"' 
logical_port='"down_link"'
+wait_row_count MAC_Binding 1 ip="192.168.1.112" mac='"f0:00:00:00:00:02"' 
logical_port='"down_link"'
+wait_row_count MAC_Binding 1 ip="192.168.1.113" mac='"f0:00:00:00:00:01"' 
logical_port='"down_link"'
+wait_row_count MAC_Binding 1 ip="192.168.1.114" mac='"f0:00:00:00:00:02"' 
logical_port='"down_link"'
+wait_row_count MAC_Binding 1 ip="192.168.1.223" mac='"02:00:00:00:00:ee"' 
logical_port='"down_link"'
+wait_row_count MAC_Binding 0 ip="192.168.1.222" mac='"02:00:00:00:00:ee"' 
logical_port='"down_link"'
+
+check ovn-nbctl --wait=hv lsp-set-type down_ext localnet
+
+OVN_CLEANUP([hv1],[hv2])
+AT_CLEANUP
+])
diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
index 6206bbb96..ecf632fe8 100644
--- a/tests/system-common-macros.at
+++ b/tests/system-common-macros.at
@@ -93,6 +93,9 @@ m4_define([ADD_VETH],
 #
 m4_define([FORMAT_PING], [grep "transmitted" | sed 's/time.*ms$/time 0ms/'])
 
+m4_define([FORMAT_ARPING],
+[[sed -n 's/^Unicast reply from \([0-9.][0-9.]*\) \[\([^][]*\)\].*/\1 \2/p']])
+
 # FORMAT_CT([ip-addr])
 #
 # Strip content from the piped input which would differ from test to test
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index cfcc3f197..e46cf986b 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -21805,75 +21805,120 @@ AT_CLEANUP
 ])
 
 OVN_FOR_EACH_NORTHD([
-AT_SETUP([VIF port connected to localnet network])
-#
-# Topology:
-#    (fabric) -- localnet-port -- LS --- DGP(chassis2) -- LR
-#                                 |
-#                                 |
-#                               VM (chassis1)
-#
-# It is expected that ARP requests to this port are allowed on the chassis 
that hosts this port.
-
+AT_SETUP([ARP restrictions: GARPs and arp to router ips])
 ovn_start
 OVS_TRAFFIC_VSWITCHD_START()
 ADD_BR([br-int])
-ADD_BR([br-ext])
+ADD_BR([br-ext], [set Bridge br-ext fail-mode=standalone])
 
-ovs-ofctl add-flow br-ext action=normal
-# Set external-ids in br-int needed for ovn-controller
-ovs-vsctl \
+check ovs-vsctl \
         -- set Open_vSwitch . external-ids:system-id=hv1 \
         -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
         -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
         -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
-        -- set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext \
         -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
 
-# Start ovn-controller
 start_daemon ovn-controller
 
 check ovn-nbctl lr-add lr1
-check ovn-nbctl ls-add public
+check ovn-nbctl lrp-add lr1 lr1-outside f0:00:00:00:00:f1 192.168.0.1/24 
2001:db8::1/64
+check ovn-nbctl lrp-add lr1 lr1-private f0:00:00:00:00:f2 172.31.0.1/24 
2002:db8::1/64
 
-check ovn-nbctl lrp-add lr1 rp-public 00:00:02:01:02:03 172.31.1.1/24
-check ovn-nbctl lsp-add-router-port public public-rp rp-public
-check ovn-nbctl lsp-add-localnet-port public localnet phynet
-check ovn-nbctl lrp-set-gateway-chassis rp-public hv2
+check ovn-nbctl ls-add outside
+check ovn-nbctl ls-add private
 
-ADD_NAMESPACES(ext)
-ADD_VETH(ext, ext, br-ext, "172.31.1.2/24", "f0:00:00:01:02:02", \
-         "172.31.1.1")
-ADD_NAMESPACES(lsp1)
-ADD_VETH(lsp1, lsp1, br-int, "172.31.1.3/24", "f0:00:00:01:02:03", \
-         "172.31.1.1")
-ADD_NAMESPACES(lsp2)
-ADD_VETH(lsp2, lsp2, br-int, "172.31.1.4/24", "f0:00:00:01:02:04", \
-         "172.31.1.1")
+check ovn-nbctl lsp-add-router-port outside outside-lr1 lr1-outside
+check ovn-nbctl lsp-add-router-port private private-lr1 lr1-private
+
+check ovn-nbctl lsp-add outside outside_vif
+check ovn-nbctl lsp-add private private_vif
+check ovn-nbctl lsp-add private private_nat
+
+check ovn-nbctl lsp-set-addresses outside_vif "02:ac:10:01:01:03 192.168.0.3 
2001:db8::3"
+ADD_NAMESPACES(outside_vif)
+ADD_VETH(outside_vif, outside_vif, br-int, "2001:db8::3/64", 
"02:ac:10:01:01:03", \
+         "2001:db8::1", "nodad", "192.168.0.3/24", "192.168.0.1")
+
+check ovn-nbctl lsp-set-addresses private_vif "02:ac:10:01:02:03 172.31.0.3 
2002:db8::3"
+ADD_NAMESPACES(private_vif)
+ADD_VETH(private_vif, private_vif, br-int, "2002:db8::3/64", 
"02:ac:10:01:02:03", \
+         "2002:db8::1", "nodad", "172.31.0.3/24", "172.31.0.1")
+
+check ovn-nbctl lsp-set-addresses private_nat "02:ac:10:01:02:04 172.31.0.4 
2002:db8::4"
+ADD_NAMESPACES(private_nat)
+ADD_VETH(private_nat, private_nat, br-int, "2002:db8::4/64", 
"02:ac:10:01:02:04", \
+         "2002:db8::1", "nodad", "172.31.0.4/24", "172.31.0.1")
+
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat 10.10.10.1 172.31.0.3 private_nat 
f0:00:00:00:00:07
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat 10.10.10.2 172.31.0.4
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat fd12::1 2002:db8::3 private_nat 
f0:00:00:00:00:07
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat fd12::2 2002:db8::4
 
-check ovn-nbctl lsp-add public lsp1
-check ovn-nbctl lsp-set-addresses lsp1 "f0:00:00:01:02:03 172.31.1.3"
-check ovn-nbctl lsp-add public lsp2
-check ovn-nbctl lsp-set-addresses lsp2 "f0:00:00:01:02:04 172.31.1.4"
+check ovn-nbctl lrp-set-gateway-chassis lr1-outside hv1
+check ovn-nbctl lsp-add-localnet-port outside localnet phynet
+check ovs-vsctl set Open_vSwitch . 
external-ids:ovn-bridge-mappings=phynet:br-ext
+
+ADD_NAMESPACES(external)
+ADD_VETH(external, external, br-ext, "2001:db8::2/64", "02:ac:10:01:01:02", \
+         "2001:db8::1", "nodad", "192.168.0.2/24", "192.168.0.1")
 
+OVN_POPULATE_ARP
+wait_for_ports_up
 check ovn-nbctl --wait=hv sync
 
-NS_CHECK_EXEC([ext], [ping -q -c 3 -i 0.3 -w 2 172.31.1.3 | FORMAT_PING], \
-[0], [dnl
-3 packets transmitted, 3 received, 0% packet loss, time 0ms
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 192.168.0.1 | 
FORMAT_ARPING], [0],
+[dnl
+192.168.0.1 F0:00:00:00:00:F1
+])
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 192.168.0.3 | 
FORMAT_ARPING], [0],
+[dnl
+192.168.0.3 02:AC:10:01:01:03
+])
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 10.10.10.1 | 
FORMAT_ARPING], [0],
+[dnl
+10.10.10.1 F0:00:00:00:00:07
+])
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 10.10.10.2 | 
FORMAT_ARPING], [0],
+[dnl
+10.10.10.2 F0:00:00:00:00:F1
 ])
 
-NS_CHECK_EXEC([lsp1], [ping -q -c 3 -i 0.3 -w 2 172.31.1.4 | FORMAT_PING], \
-[0], [dnl
+NS_CHECK_EXEC([external], [ping6 -q -c 3 -i 0.3 -w 2 2001:db8::1 | 
FORMAT_PING], [0],
+[dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+NS_CHECK_EXEC([external], [ping6 -q -c 3 -i 0.3 -w 2 2001:db8::3 | 
FORMAT_PING], [0],
+[dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+NS_CHECK_EXEC([external], [ping6 -q -c 3 -i 0.3 -w 2 fd12::1 | FORMAT_PING], 
[0],
+[dnl
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
+NS_CHECK_EXEC([external], [ping6 -q -c 3 -i 0.3 -w 2 fd12::2 | FORMAT_PING], 
[0],
+[dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+check ovn-nbctl clear logical_router_port lr1-outside gateway_chassis
+check ovn-nbctl lrp-set-gateway-chassis lr1-outside hv2
+check ovn-nbctl --wait=hv sync
+
+NS_CHECK_EXEC([external], [ip neigh flush dev external], [0])
+NS_CHECK_EXEC([external], [ip -6 neigh flush dev external], [0])
+
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 192.168.0.1 >/dev/null 
2>&1], [1])
+NS_CHECK_EXEC([external], [ping6 -q -c 3 -i 0.3 -w 2 2001:db8::1 >/dev/null 
2>&1], [1])
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 10.10.10.2 >/dev/null 
2>&1], [1])
+NS_CHECK_EXEC([external], [ping6 -q -c 3 -i 0.3 -w 2 fd12::2 >/dev/null 2>&1], 
[1])
+
+# Distributed NAT ips should be available
+NS_CHECK_EXEC([external], [arping -f -c 3 -I external 10.10.10.1  >/dev/null 
2>&1], [0])
 
 OVN_CLEANUP_CONTROLLER([hv1])
 OVN_CLEANUP_NORTHD
 
-as
 OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
 /connection dropped.*/d"])
-
 AT_CLEANUP
 ])
-- 
2.48.1

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to