An ICMP echo request sent to a logical router port's own IP is
answered by the in-pipeline echo responder in the logical router's
IP input stage, and the generated reply re-enters the connected
logical switch with inport set to the router port.  Router port
traffic is exempt from conntrack (skip_port_from_conntrack()),
so the reply arrives at the ACL eval stage untracked, never
matches the built-in est-allow flow, and is dropped by a stateful
ACL set that only allows egress traffic.  A VM with such a
security group therefore cannot ping its router interface.

The exemption exists for transit traffic, where the request and
the reply are processed on different chassis and per-chassis
conntrack zones cannot correlate them.  Router-terminated echo
traffic is different: the reply is generated with
flags.loopback = 1 in the logical router pipeline, so both the
request and the reply are processed on the same chassis and the
router port's zone is local.  Add per-router-port flows at
priority 111 (above the priority-110 skip flows) in the pre-ACL
stages that send echo requests destined to the router port's own
address and echo replies sourced from it through conntrack in
that zone.  The reply then matches the existing est-allow flow
and is allowed, and pings to the router can still be blocked by
dropping the echo request direction with a higher-priority ACL,
like any other established connection.  Transit traffic is not
matched and keeps the exemption.

Router ports, however, had no conntrack zone allocated: the
zone lookups in the physical pipeline (the egress zone load for
the router port and the logical switch ingress zone load at the
patch handoff) key off the switch-side patch port name, and
ovn-controller only allocated zones for bound logical ports and
per-datapath NAT zones.  Extend ct_zones_update() to also assign
a zone for the switch-side port of every router port on a local
logical switch (one additional zone per router port per chassis
with a local datapath).  The ct_zones incremental processing
handler cannot track those zones incrementally, so make it fall
back to a full ct_zones recompute for patch ports instead of
deleting their zones.

Reported-at: https://redhat.atlassian.net/browse/FDP-4509
Assisted-by: GLM 5.3, OpenCode
Signed-off-by: Jacob Tanenbaum <[email protected]>

diff --git a/NEWS b/NEWS
index 4176f8d7f..11d79c0c2 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,14 @@
 Post v26.09.0
 --------------
+   - ICMP echo replies generated by the logical router are now tracked
+     by conntrack in the router port's zone, so they are established
+     reply traffic instead of untracked packets.  Stateful ACLs with
+     only egress allow-related rules no longer drop a VM's ping to its
+     router port's IP address.  Pings to the router can still be
+     blocked by dropping the echo request direction with a
+     higher-priority ACL.  To support this, ovn-controller now
+     allocates a conntrack zone for the router port of every local
+     logical switch (one additional zone per router port per chassis).
    - Learn ARP sender hardware address (arp.sha) and IPv6 Neighbor
      Discovery link-layer addresses (nd.tll, nd.sll) into the FDB
      table in addition to eth.src.  This allows VRRP virtual MACs to
diff --git a/controller/ct-zone.c b/controller/ct-zone.c
index a4f94375f..b54011d82 100644
--- a/controller/ct-zone.c
+++ b/controller/ct-zone.c
@@ -210,6 +210,16 @@ ct_zones_update(const struct simap *local_lports,
         }
         free(dnat);
         free(snat);
+
+        /* The router ports of a local logical switch need zones assigned
+         * so that traffic terminated by the connected logical router can
+         * be tracked in the router port's zone on this chassis. */
+        if (ld->is_switch) {
+            const struct peer_ports *peers;
+            VECTOR_FOR_EACH_PTR (&ld->peer_ports, peers) {
+                sset_add(&all_users, peers->local->logical_port);
+            }
+        }
     }
 
     ct_zones_parse_range(ovs_table, &min_ct_zone, &max_ct_zone);
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 7bc3139ab..56c917e64 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -2873,16 +2873,15 @@ ct_zones_runtime_data_handler(struct engine_node *node, 
void *data)
                 continue;
             }
 
-            /* For patch ports without ACL flag, delete the ct_zone. */
+            /* The CT zone of a patch port without the ACL flag depends
+             * on the local datapaths of the connected switches, which is
+             * not tracked incrementally.  Fall back to a full recompute
+             * of ct_zones so that zones for router ports are assigned
+             * and released consistently with ct_zones_update(). */
             if (!strcmp(t_lport->pb->type, "patch") &&
                 !smap_get_bool(&t_lport->pb->options,
                                "enable_router_port_acl", false)) {
-                updated |= ct_zone_handle_port_update(&ct_zones_data->ctx,
-                                                      t_lport->pb,
-                                                      false, &scan_start,
-                                                      min_ct_zone,
-                                                      max_ct_zone);
-                continue;
+                return EN_UNHANDLED;
             }
             bool port_updated =
                     t_lport->tracked_type == TRACKED_RESOURCE_NEW ||
diff --git a/northd/northd.c b/northd/northd.c
index 3f59894bd..169da9c34 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -6928,6 +6928,75 @@ skip_port_from_conntrack(const struct ovn_datapath *od, 
struct ovn_port *op,
     free(egress_match);
 }
 
+static void op_put_v4_networks(struct ds *ds, const struct ovn_port *op,
+                               bool add_bcast);
+static void op_put_v6_networks(struct ds *ds, const struct ovn_port *op);
+
+/* Router-terminated ICMP echo request/reply pairs must be tracked by
+ * conntrack even though other traffic to and from a router port is
+ * exempt from it (see skip_port_from_conntrack()).
+ *
+ * The flows match only echo requests whose destination is the router
+ * port's own address and echo replies whose source is the router
+ * port's own address, so transit traffic through the router port is
+ * not affected.  They run at priority 111, above the priority-110
+ * skip flows installed by skip_port_from_conntrack(). */
+static void
+build_rport_echo_conntrack_flows(const struct ovn_datapath *od,
+                                 struct ovn_port *op,
+                                 struct lflow_table *lflows,
+                                 struct lflow_ref *lflow_ref)
+{
+    ovs_assert(op->peer && op->peer->nbrp);
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    if (op->peer->lrp_networks.n_ipv4_addrs) {
+        ds_put_format(&match, "inport == %s && ip4 && ip4.src == ",
+                      op->json_key);
+        op_put_v4_networks(&match, op->peer, false);
+        ds_put_cstr(&match, " && icmp4.type == 0 && icmp4.code == 0");
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 111, ds_cstr(&match),
+                      REGBIT_CONNTRACK_DEFRAG" = 1; next;", lflow_ref,
+                      WITH_IO_PORT(op->key),
+                      WITH_HINT(&op->nbsp->header_));
+
+        ds_clear(&match);
+        ds_put_format(&match, "outport == %s && ip4 && ip4.dst == ",
+                      op->json_key);
+        op_put_v4_networks(&match, op->peer, false);
+        ds_put_cstr(&match, " && icmp4.type == 8 && icmp4.code == 0");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 111, ds_cstr(&match),
+                      REGBIT_CONNTRACK_DEFRAG" = 1; next;", lflow_ref,
+                      WITH_IO_PORT(op->key),
+                      WITH_HINT(&op->nbsp->header_));
+    }
+
+    if (op->peer->lrp_networks.n_ipv6_addrs) {
+        ds_clear(&match);
+        ds_put_format(&match, "inport == %s && ip6 && ip6.src == ",
+                      op->json_key);
+        op_put_v6_networks(&match, op->peer);
+        ds_put_cstr(&match, " && icmp6.type == 129 && icmp6.code == 0");
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 111, ds_cstr(&match),
+                      REGBIT_CONNTRACK_DEFRAG" = 1; next;", lflow_ref,
+                      WITH_IO_PORT(op->key),
+                      WITH_HINT(&op->nbsp->header_));
+
+        ds_clear(&match);
+        ds_put_format(&match, "outport == %s && ip6 && ip6.dst == ",
+                      op->json_key);
+        op_put_v6_networks(&match, op->peer);
+        ds_put_cstr(&match, " && icmp6.type == 128 && icmp6.code == 0");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 111, ds_cstr(&match),
+                      REGBIT_CONNTRACK_DEFRAG" = 1; next;", lflow_ref,
+                      WITH_IO_PORT(op->key),
+                      WITH_HINT(&op->nbsp->header_));
+    }
+
+    ds_destroy(&match);
+}
+
 /* Skip conntrack for traffic from/to EVPN remote VTEPs.
  * Remote VTEPs do not have conntrack zones assigned, so
  * conntrack lookups would return +trk+inv and cause drops. */
@@ -7042,6 +7111,9 @@ build_ls_stateful_rec_pre_acls(
             skip_port_from_conntrack(od, op, true,
                                      S_SWITCH_IN_PRE_ACL, S_SWITCH_OUT_PRE_ACL,
                                      110, lflows, lflow_ref);
+            if (op->peer && op->peer->nbrp) {
+                build_rport_echo_conntrack_flows(od, op, lflows, lflow_ref);
+            }
         }
         VECTOR_FOR_EACH (&od->switch_ports, op) {
             skip_port_from_conntrack(od, op, true,
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 93af177ab..a8e870ffc 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -13947,12 +13947,16 @@ AT_CHECK([ovn-sbctl dump-flows S1 | grep pre_acl | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.mcast), 
action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(ip && inport == 
"S1-R1"), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(nd || nd_rs || nd_ra 
|| mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)), action=(next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "S1-R1" && 
ip4 && ip4.src == 172.16.1.1 && icmp4.type == 0 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "S1-R1" && 
ip6 && ip6.src == fe80::ac:10ff:fe01:1 && icmp6.type == 129 && icmp6.code == 
0), action=(reg0[[0]] = 1; next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=100  , match=(ip), action=(reg0[[0]] 
= 1; next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.mcast), 
action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == 
$svc_monitor_mac), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
"S1-R1"), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(nd || nd_rs || nd_ra 
|| mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)), action=(next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "S1-R1" && 
ip4 && ip4.dst == 172.16.1.1 && icmp4.type == 8 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "S1-R1" && 
ip6 && ip6.dst == fe80::ac:10ff:fe01:1 && icmp6.type == 128 && icmp6.code == 
0), action=(reg0[[0]] = 1; next;)
 ])
 
 # Enable 'enable_router_port_acl' and check the flows
@@ -13994,12 +13998,16 @@ AT_CHECK([ovn-sbctl dump-flows S1 | grep pre_acl | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.mcast), 
action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(ip && inport == 
"S1-R1"), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(nd || nd_rs || nd_ra 
|| mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)), action=(next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "S1-R1" && 
ip4 && ip4.src == 172.16.1.1 && icmp4.type == 0 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "S1-R1" && 
ip6 && ip6.src == fe80::ac:10ff:fe01:1 && icmp6.type == 129 && icmp6.code == 
0), action=(reg0[[0]] = 1; next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=100  , match=(ip), action=(reg0[[0]] 
= 1; next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.mcast), 
action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == 
$svc_monitor_mac), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
"S1-R1"), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(nd || nd_rs || nd_ra 
|| mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)), action=(next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "S1-R1" && 
ip4 && ip4.dst == 172.16.1.1 && icmp4.type == 8 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "S1-R1" && 
ip6 && ip6.dst == fe80::ac:10ff:fe01:1 && icmp6.type == 128 && icmp6.code == 
0), action=(reg0[[0]] = 1; next;)
 ])
 
 # Clear the option 'enable_router_port_acl' and check the flows.  Before that 
enable the option.
@@ -14027,12 +14035,16 @@ AT_CHECK([ovn-sbctl dump-flows S1 | grep pre_acl | 
ovn_strip_lflows], [0], [dnl
   table=??(ls_in_pre_acl      ), priority=110  , match=(eth.mcast), 
action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(ip && inport == 
"S1-R1"), action=(next;)
   table=??(ls_in_pre_acl      ), priority=110  , match=(nd || nd_rs || nd_ra 
|| mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)), action=(next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "S1-R1" && 
ip4 && ip4.src == 172.16.1.1 && icmp4.type == 0 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "S1-R1" && 
ip6 && ip6.src == fe80::ac:10ff:fe01:1 && icmp6.type == 129 && icmp6.code == 
0), action=(reg0[[0]] = 1; next;)
   table=??(ls_out_pre_acl     ), priority=0    , match=(1), action=(next;)
   table=??(ls_out_pre_acl     ), priority=100  , match=(ip), action=(reg0[[0]] 
= 1; next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.mcast), 
action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(eth.src == 
$svc_monitor_mac), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
"S1-R1"), action=(next;)
   table=??(ls_out_pre_acl     ), priority=110  , match=(nd || nd_rs || nd_ra 
|| mldv1 || mldv2 || (udp && udp.src == 546 && udp.dst == 547)), action=(next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "S1-R1" && 
ip4 && ip4.dst == 172.16.1.1 && icmp4.type == 8 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "S1-R1" && 
ip6 && ip6.dst == fe80::ac:10ff:fe01:1 && icmp6.type == 128 && icmp6.code == 
0), action=(reg0[[0]] = 1; next;)
 ])
 
 OVN_CLEANUP_NORTHD
@@ -24122,6 +24134,54 @@ AT_CHECK([grep "ls1-to-spine" ls1flows | 
ovn_strip_lflows | sort], [0], [dnl
 OVN_CLEANUP_NORTHD
 AT_CLEANUP
 
+AT_SETUP([Conntrack flows for router port ICMP echo])
+ovn_start
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-ls0 00:00:00:00:00:01 10.0.0.1/24 fd00::1/64
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add-router-port ls0 ls0-lr0 lr0-ls0
+check ovn-nbctl lsp-add ls0 vm1 -- \
+    lsp-set-addresses vm1 "f0:00:00:00:00:01 10.0.0.10 fd00::10"
+
+# Stateful security group with only egress allow-all rules:
+# egress is allowed, ingress is dropped.
+check ovn-nbctl acl-add ls0 from-lport 100 "inport == \"vm1\" && ip" 
allow-related
+check ovn-nbctl acl-add ls0 to-lport 100 "outport == \"vm1\" && ip" drop
+
+check ovn-nbctl --wait=sb sync
+
+AT_CHECK([ovn-sbctl dump-flows ls0 > ls0flows])
+AT_CAPTURE_FILE([ls0flows])
+
+AT_CHECK([grep "ls0-lr0" ls0flows | ovn_strip_lflows | sort], [0], [dnl
+  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 && 
arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "ls0-lr0"), 
action=(next;)
+  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:1 && nd.target == fd00::1 && inport == "ls0-lr0"), 
action=(next;)
+  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && ip6.dst 
== ff02::1:ff00:1 && nd.target == fe80::200:ff:fe00:1 && inport == "ls0-lr0"), 
action=(next;)
+  table=??(ls_in_check_port_sec), priority=70   , match=(inport == "ls0-lr0"), 
action=(reg0[[18]] = 1; next;)
+  table=??(ls_in_l2_lkup      ), priority=50   , match=(eth.dst == 
00:00:00:00:00:01), action=(outport = "ls0-lr0"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
eth.dst == ff:ff:ff:ff:ff:ff && arp.op == 1 && arp.tpa == 10.0.0.1), 
action=(clone {outport = "ls0-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
eth.mcast && nd_ns && nd.target == fd00::1), action=(clone {outport = 
"ls0-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_l2_lkup      ), priority=80   , match=(flags[[1]] == 0 && 
eth.mcast && nd_ns && nd.target == fe80::200:ff:fe00:1), action=(clone {outport 
= "ls0-lr0"; output; }; outport = "_MC_unknown"; output;)
+  table=??(ls_in_pre_acl      ), priority=110  , match=(ip && inport == 
"ls0-lr0"), action=(next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "ls0-lr0" && 
ip4 && ip4.src == 10.0.0.1 && icmp4.type == 0 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_in_pre_acl      ), priority=111  , match=(inport == "ls0-lr0" && 
ip6 && ip6.src == {fd00::1, fe80::200:ff:fe00:1} && icmp6.type == 129 && 
icmp6.code == 0), action=(reg0[[0]] = 1; next;)
+  table=??(ls_in_pre_lb       ), priority=110  , match=(ip && inport == 
"ls0-lr0"), action=(next;)
+  table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
"ls0-lr0"), action=(next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "ls0-lr0" 
&& ip4 && ip4.dst == 10.0.0.1 && icmp4.type == 8 && icmp4.code == 0), 
action=(reg0[[0]] = 1; next;)
+  table=??(ls_out_pre_acl     ), priority=111  , match=(outport == "ls0-lr0" 
&& ip6 && ip6.dst == {fd00::1, fe80::200:ff:fe00:1} && icmp6.type == 128 && 
icmp6.code == 0), action=(reg0[[0]] = 1; next;)
+  table=??(ls_out_pre_lb      ), priority=110  , match=(ip && outport == 
"ls0-lr0"), action=(next;)
+])
+
+# There must be no special-case allow flows for router echo replies in the
+# ACL eval tables; the built-in est-allow flow must still be present.
+AT_CHECK([grep 'ls_in_acl_eval' ls0flows | grep 'ls0-lr0' | grep -q icmp], [1])
+AT_CHECK([grep 'ls_out_acl_eval' ls0flows | grep 'ls0-lr0' | grep -q icmp], 
[1])
+AT_CHECK([grep 'ls_in_acl_eval' ls0flows | grep -q 'ct.est && !ct.rel && 
ct.rpl && ct_mark.blocked == 0'])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+
 AT_SETUP([Logical Switch Port Health Check - lflow/service monitor 
synchronization])
 ovn_start
 
diff --git a/tests/ovn.at b/tests/ovn.at
index 780149467..4f80ecc1e 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -37204,9 +37204,11 @@ AT_CHECK([as hv1 ovn-appctl -t ovn-controller 
ct-zone-list | \
 grep sw0-port2 -c], [0], [1
 ])
 
-# There should be no ct-zone id allocated for sw0-lr0 and lr0-sw0
+# There should be a ct zone for the switch-side patch port sw0-lr0, but
+# not for the router-side patch port lr0-sw0.
 AT_CHECK([as hv1 ovn-appctl -t ovn-controller ct-zone-list | \
-grep sw0-lr0], [1], [])
+grep sw0-lr0 -c], [0], [1
+])
 
 AT_CHECK([as hv1 ovn-appctl -t ovn-controller ct-zone-list | \
 grep lr0-sw0], [1], [])
@@ -37225,12 +37227,17 @@ grep sw1-port1 -c], [0], [1
 # Attach sw1 to lr0
 check ovn-nbctl lsp-add-router-port sw1 sw1-lr0 lr0-sw1
 
-# There should be no ct-zone id allocated for sw1-lr0
+# There is no ct-zone id allocated for the switch-side patch port
+# sw1-lr0 while its router port does not exist yet.
 AT_CHECK([as hv1 ovn-appctl -t ovn-controller ct-zone-list | \
 grep sw1-lr0], [1], [])
 
 check ovn-nbctl lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 20.0.0.1/24
 
+# There should be a ct zone for the switch-side patch port sw1-lr0
+OVS_WAIT_UNTIL([test `as hv1 ovn-appctl -t ovn-controller ct-zone-list | \
+grep sw1-lr0 -c` -eq 1])
+
 # There should be no ct-zone id allocated for lr0-sw1
 AT_CHECK([as hv1 ovn-appctl -t ovn-controller ct-zone-list | \
 grep lr0-sw1], [1], [])
@@ -43878,8 +43885,10 @@ check ovn-nbctl --wait=hv sync
 AS_BOX([With patch ports])
 zone_list_hv1=$(as hv1 ovn-appctl -t ovn-controller ct-zone-list | sort)
 AT_CHECK([echo "$zone_list_hv1" | sed "s/ [[0-9]]*/ ??/" | sort], [0], [dnl
+ls1-r1 ??
 ls1_dnat ??
 ls1_snat ??
+ls2-r1 ??
 ls2_dnat ??
 ls2_snat ??
 lsp1 ??
@@ -43889,8 +43898,10 @@ r1_snat ??
 
 zone_list_hv2=$(as hv2 ovn-appctl -t ovn-controller ct-zone-list | sort)
 AT_CHECK([echo "$zone_list_hv2" | sed "s/ [[0-9]]*/ ??/" | sort], [0], [dnl
+ls1-r1 ??
 ls1_dnat ??
 ls1_snat ??
+ls2-r1 ??
 ls2_dnat ??
 ls2_snat ??
 lsp2 ??
@@ -43956,8 +43967,10 @@ AS_BOX([With patch ports again])
 check ovn-nbctl --wait=hv remove Logical_Router r1 options chassis=hv2
 zone_list_hv1=$(as hv1 ovn-appctl -t ovn-controller ct-zone-list | sort)
 AT_CHECK([echo "$zone_list_hv1" | sed "s/ [[0-9]]*/ ??/" | sort], [0], [dnl
+ls1-r1 ??
 ls1_dnat ??
 ls1_snat ??
+ls2-r1 ??
 ls2_dnat ??
 ls2_snat ??
 lsp1 ??
@@ -43967,8 +43980,10 @@ r1_snat ??
 
 zone_list_hv2=$(as hv2 ovn-appctl -t ovn-controller ct-zone-list | sort)
 AT_CHECK([echo "$zone_list_hv2" | sed "s/ [[0-9]]*/ ??/" | sort], [0], [dnl
+ls1-r1 ??
 ls1_dnat ??
 ls1_snat ??
+ls2-r1 ??
 ls2_dnat ??
 ls2_snat ??
 lsp2 ??
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 3cb4b858a..b041d6385 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -21242,8 +21242,11 @@ AT_CHECK([grep "ext_lr" stdout], [0], [dnl
 ext_lr ??
 ])
 
-# Check no CT zone for ls_lr.
-AT_CHECK([grep "ls_lr" stdout], [1], [])
+# Check that the switch-side patch port ls_lr has a CT zone allocated.
+AT_CHECK([ovn-appctl -t ovn-controller ct-zone-list | sed "s/ [[0-9]]*/ ??/"], 
[0], [stdout])
+AT_CHECK([grep "ls_lr" stdout], [0], [dnl
+ls_lr ??
+])
 
 # Generate ICMP traffic to create conntrack entries.
 NS_CHECK_EXEC([vm1], [ping -q -c 2 -W 1 172.16.0.100 | FORMAT_PING], \
@@ -21261,10 +21264,12 @@ 
icmp,orig=(src=192.168.100.11,dst=172.16.0.100,id=<cleared>,type=8,code=0),reply
 NETNS_DAEMONIZE([external], [ncat -l -k 172.16.0.100 80], [external.pid])
 NS_CHECK_EXEC([vm1], [ncat -z 172.16.0.100 80], [1])
 
-# Remove enable_router_port_acl and verify CT zone release.
+# Remove enable_router_port_acl.  The CT zone for ext_lr is kept: with
+# router echo traffic tracked in the router port's zone, switch-side
+# patch ports of local logical switches always have a CT zone.
 check ovn-nbctl lsp-set-options ext_lr router-port=lr_ext
 check ovn-nbctl --wait=sb sync
-AT_CHECK([ovn-appctl -t ovn-controller ct-zone-list | grep "ext_lr"], [1], [])
+AT_CHECK([ovn-appctl -t ovn-controller ct-zone-list | grep "ext_lr"], [0], 
[ignore])
 
 # Re-enable enable_router_port_acl and verify CT zone reallocation.
 check ovn-nbctl lsp-set-options ext_lr router-port=lr_ext 
enable_router_port_acl=true
@@ -24110,3 +24115,80 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
patch-.*/d
 /connection dropped.*/d"])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ping to router port with stateful ACL])
+AT_KEYWORDS([acl])
+
+CHECK_CONNTRACK()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+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 bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+start_daemon ovn-controller
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-ls0 00:00:00:00:00:01 10.0.0.1/24 fd00::1/64
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add-router-port ls0 ls0-lr0 lr0-ls0
+check ovn-nbctl lsp-add ls0 vm1
+check ovn-nbctl lsp-set-addresses vm1 "f0:00:00:00:00:01 10.0.0.10 fd00::10"
+
+dnl Stateful security group with only egress allow-all rules:
+dnl egress is allowed, ingress is dropped.
+check ovn-nbctl acl-add ls0 from-lport 100 "inport == \"vm1\" && ip" 
allow-related
+check ovn-nbctl acl-add ls0 to-lport 100 "outport == \"vm1\" && ip" drop
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "10.0.0.10/24", "f0:00:00:00:00:01", "10.0.0.1")
+NS_CHECK_EXEC([vm1], [ip -6 addr add fd00::10/64 dev vm1 nodad])
+
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+# The router port must have a conntrack zone allocated on this chassis:
+# the router echo request and reply are tracked in that zone.
+OVS_WAIT_UNTIL([ovn-appctl -t ovn-controller ct-zone-list | grep -q ls0-lr0])
+
+dnl The router echo reply is conntrack-established in the router port's
+dnl zone, so the stateful SG's built-in est-allow lets it through even
+dnl though ingress has no allow rule.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.1 | FORMAT_PING], [0], 
[dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+NS_CHECK_EXEC([vm1], [ping -6 -q -c 3 -i 0.3 -w 2 fd00::1 | FORMAT_PING], [0], 
[dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+dnl A higher-priority ACL that drops the echo request direction must
+dnl block pings to the router, like any other established connection.
+check ovn-nbctl --wait=hv acl-add ls0 from-lport 101 "inport == \"vm1\" && ip4 
&& icmp4.type == 8 && ip4.dst == 10.0.0.1" drop
+
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.1 | FORMAT_PING], [0], 
[dnl
+7 packets transmitted, 0 received, 100% packet loss, time 0ms
+])
+
+check ovn-nbctl --wait=hv acl-add ls0 from-lport 101 "inport == \"vm1\" && ip6 
&& icmp6.type == 128 && ip6.dst == fd00::1" drop
+
+NS_CHECK_EXEC([vm1], [ping -6 -q -c 3 -i 0.3 -w 2 fd00::1 | FORMAT_PING], [0], 
[dnl
+7 packets transmitted, 0 received, 100% packet loss, time 0ms
+])
+
+OVN_CLEANUP_CONTROLLER([hv1])
+
+OVN_CLEANUP_NORTHD
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+
+AT_CLEANUP
+])
-- 
2.56.0

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

Reply via email to