diff mbox series

[ovs-dev,RFC,v2] northd: Commit all traffic when there is stateful NAT/LB.

Message ID 20241120163721.2611818-1-amusil@redhat.com
State Deferred
Headers show
Series [ovs-dev,RFC,v2] northd: Commit all traffic when there is stateful NAT/LB. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test fail github build: failed
ovsrobot/github-robot-_ovn-kubernetes fail github build: failed

Commit Message

Ales Musil Nov. 20, 2024, 4:37 p.m. UTC
Commit all traffic that is not already commit by either NAT or LB. This
ensures that the traffic is tracked, and we don't erroneously commit
reply traffic, or reply traffic is not marked as invalid.

To achieve the commit we need to perform lookup on every packet
that goes through LR pipeline whenever there is stateful NAT.

The SNAT lookup requires additional flag as the unSNAT is happening
in ingress pipeline and at that point we need to know if the packet
is reply or not. This is not required for DNAT, because unDNAT stage
happens in egress.

Signed-off-by: Ales Musil <amusil@redhat.com>
---
There are few failing system tests with userspace datapath, that's due
to the recirculation limit that is being hit due to additional
lookups.
---
 include/ovn/logical-fields.h  |   4 +
 lib/logical-fields.c          |   4 +
 northd/northd.c               |  84 +++++++++++-------
 northd/northd.h               |  39 ++++----
 tests/ovn-macros.at           |   6 ++
 tests/ovn-northd.at           | 102 ++++++++++++++-------
 tests/system-common-macros.at |   8 ++
 tests/system-ovn.at           | 161 ++++++++++++++++++++++++++--------
 8 files changed, 288 insertions(+), 120 deletions(-)

Comments

Dumitru Ceara Dec. 18, 2024, 1:08 p.m. UTC | #1
On 11/20/24 5:37 PM, Ales Musil wrote:
> Commit all traffic that is not already commit by either NAT or LB. This
> ensures that the traffic is tracked, and we don't erroneously commit
> reply traffic, or reply traffic is not marked as invalid.
> 
> To achieve the commit we need to perform lookup on every packet
> that goes through LR pipeline whenever there is stateful NAT.
> 
> The SNAT lookup requires additional flag as the unSNAT is happening
> in ingress pipeline and at that point we need to know if the packet
> is reply or not. This is not required for DNAT, because unDNAT stage
> happens in egress.
> 
> Signed-off-by: Ales Musil <amusil@redhat.com>
> ---

Hi Han,

I was wondering if you had time to evaluate the performance impact of
this patch in your lab.

Thanks,
Dumitru
Ales Musil Jan. 7, 2025, 3:43 p.m. UTC | #2
On Wed, Dec 18, 2024 at 2:08 PM Dumitru Ceara <dceara@redhat.com> wrote:

> On 11/20/24 5:37 PM, Ales Musil wrote:
> > Commit all traffic that is not already commit by either NAT or LB. This
> > ensures that the traffic is tracked, and we don't erroneously commit
> > reply traffic, or reply traffic is not marked as invalid.
> >
> > To achieve the commit we need to perform lookup on every packet
> > that goes through LR pipeline whenever there is stateful NAT.
> >
> > The SNAT lookup requires additional flag as the unSNAT is happening
> > in ingress pipeline and at that point we need to know if the packet
> > is reply or not. This is not required for DNAT, because unDNAT stage
> > happens in egress.
> >
> > Signed-off-by: Ales Musil <amusil@redhat.com>
> > ---
>
> Hi Han,
>
> I was wondering if you had time to evaluate the performance impact of
> this patch in your lab.
>
> Thanks,
> Dumitru
>
>
Hi Han,

by any chance did you have some time to run the performance tests?
ovn-kubernetes is really interested in this as it will fix one of the use
cases
that they have.

Thank you,
Ales
Mark Michelson Jan. 9, 2025, 10:22 p.m. UTC | #3
I know from other emails in this thread that we don't want to merge this 
without some performance testing.

However, from a code correctness perspective, I had a look through the 
change, and

Acked-by: Mark Michelson <mmichels@redhat.com>

I have a couple of nitpicks of comments in updated tests below.

On 11/20/24 11:37, Ales Musil wrote:
> Commit all traffic that is not already commit by either NAT or LB. This
> ensures that the traffic is tracked, and we don't erroneously commit
> reply traffic, or reply traffic is not marked as invalid.
> 
> To achieve the commit we need to perform lookup on every packet
> that goes through LR pipeline whenever there is stateful NAT.
> 
> The SNAT lookup requires additional flag as the unSNAT is happening
> in ingress pipeline and at that point we need to know if the packet
> is reply or not. This is not required for DNAT, because unDNAT stage
> happens in egress.
> 
> Signed-off-by: Ales Musil <amusil@redhat.com>
> ---
> There are few failing system tests with userspace datapath, that's due
> to the recirculation limit that is being hit due to additional
> lookups.
> ---
>   include/ovn/logical-fields.h  |   4 +
>   lib/logical-fields.c          |   4 +
>   northd/northd.c               |  84 +++++++++++-------
>   northd/northd.h               |  39 ++++----
>   tests/ovn-macros.at           |   6 ++
>   tests/ovn-northd.at           | 102 ++++++++++++++-------
>   tests/system-common-macros.at |   8 ++
>   tests/system-ovn.at           | 161 ++++++++++++++++++++++++++--------
>   8 files changed, 288 insertions(+), 120 deletions(-)
> 
> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> index d563e044c..05bc18eab 100644
> --- a/include/ovn/logical-fields.h
> +++ b/include/ovn/logical-fields.h
> @@ -86,6 +86,7 @@ enum mff_log_flags_bits {
>       MLF_LOCALNET_BIT = 15,
>       MLF_RX_FROM_TUNNEL_BIT = 16,
>       MLF_ICMP_SNAT_BIT = 17,
> +    MLF_UNSNAT_NEW_BIT = 18,
>   };
>   
>   /* MFF_LOG_FLAGS_REG flag assignments */
> @@ -141,6 +142,9 @@ enum mff_log_flags {
>       MLF_RX_FROM_TUNNEL = (1 << MLF_RX_FROM_TUNNEL_BIT),
>   
>       MLF_ICMP_SNAT = (1 << MLF_ICMP_SNAT_BIT),
> +
> +    /* Indicate that the packet didn't go through unSNAT. */
> +    MLF_UNSNAT_NEW = (1 << MLF_UNSNAT_NEW_BIT),
>   };
>   
>   /* OVN logical fields
> diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> index 5a8b53f2b..081faad5a 100644
> --- a/lib/logical-fields.c
> +++ b/lib/logical-fields.c
> @@ -139,6 +139,10 @@ ovn_init_symtab(struct shash *symtab)
>                                flags_str);
>       snprintf(flags_str, sizeof flags_str, "flags[%d]", MLF_RX_FROM_TUNNEL_BIT);
>       expr_symtab_add_subfield(symtab, "flags.tunnel_rx", NULL, flags_str);
> +    snprintf(flags_str, sizeof flags_str, "flags[%d]",
> +             MLF_UNSNAT_NEW_BIT);
> +    expr_symtab_add_subfield(symtab, "flags.unsnat_new", NULL,
> +                             flags_str);
>   
>       /* Connection tracking state. */
>       expr_symtab_add_field_scoped(symtab, "ct_mark", MFF_CT_MARK, NULL, false,
> diff --git a/northd/northd.c b/northd/northd.c
> index aed1d425f..95d2d5bde 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -16207,8 +16207,7 @@ build_lrouter_out_snat_flow(struct lflow_table *lflows,
>                               struct ds *actions, bool distributed_nat,
>                               struct eth_addr mac, int cidr_bits, bool is_v6,
>                               struct ovn_port *l3dgw_port,
> -                            struct lflow_ref *lflow_ref,
> -                            const struct chassis_features *features)
> +                            struct lflow_ref *lflow_ref)
>   {
>       if (!(nat_entry->type == SNAT || nat_entry->type == DNAT_AND_SNAT)) {
>           return;
> @@ -16239,34 +16238,6 @@ build_lrouter_out_snat_flow(struct lflow_table *lflows,
>                               priority, ds_cstr(match),
>                               ds_cstr(actions), &nat->header_,
>                               lflow_ref);
> -
> -    /* For the SNAT networks, we need to make sure that connections are
> -     * properly tracked so we can decide whether to perform SNAT on traffic
> -     * exiting the network. */
> -    if (features->ct_commit_to_zone && features->ct_next_zone &&
> -        nat_entry->type == SNAT && !od->is_gw_router) {
> -        /* For traffic that comes from SNAT network, initiate CT state before
> -         * entering S_ROUTER_OUT_SNAT to allow matching on various CT states.
> -         */
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 70,
> -                      ds_cstr(match), "ct_next(snat);",
> -                      lflow_ref);
> -
> -        build_lrouter_out_snat_match(lflows, od, nat, match,
> -                                     distributed_nat, cidr_bits, is_v6,
> -                                     l3dgw_port, lflow_ref, true);
> -
> -        /* New traffic that goes into SNAT network is committed to CT to avoid
> -         * SNAT-ing replies.*/
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority,
> -                      ds_cstr(match), "ct_snat;",
> -                      lflow_ref);
> -
> -        ds_put_cstr(match, " && ct.new");
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_SNAT, priority,
> -                      ds_cstr(match), "ct_commit_to_zone(snat);",
> -                      lflow_ref);
> -    }
>   }
>   
>   static void
> @@ -16548,6 +16519,8 @@ static void build_lr_nat_defrag_and_lb_default_flows(
>       /* Packets are allowed by default. */
>       ovn_lflow_add(lflows, od, S_ROUTER_IN_DEFRAG, 0, "1", "next;", lflow_ref);
>       ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 0, "1", "next;", lflow_ref);
> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_POST_UNSNAT, 0, "1", "next;",
> +                  lflow_ref);
>       ovn_lflow_add(lflows, od, S_ROUTER_OUT_CHECK_DNAT_LOCAL, 0, "1",
>                     REGBIT_DST_NAT_IP_LOCAL" = 0; next;", lflow_ref);
>       ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 0, "1", "next;", lflow_ref);
> @@ -16659,9 +16632,6 @@ build_lrouter_nat_defrag_and_lb(
>           ovn_lflow_add(lflows, od, S_ROUTER_OUT_UNDNAT, 50,
>                         "ip", "flags.loopback = 1; ct_dnat;",
>                         lflow_ref);
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 50,
> -                      "ip && ct.new", "ct_commit { } ; next; ",
> -                      lflow_ref);
>       }
>   
>       /* NAT rules are only valid on Gateway routers and routers with
> @@ -16679,6 +16649,9 @@ build_lrouter_nat_defrag_and_lb(
>           !lport_addresses_is_empty(&lrnat_rec->dnat_force_snat_addrs);
>       bool lb_force_snat_ip =
>           !lport_addresses_is_empty(&lrnat_rec->lb_force_snat_addrs);
> +    bool stateful_dnat = lr_stateful_rec->has_lb_vip;
> +    bool stateful_snat = (dnat_force_snat_ip || lb_force_snat_ip ||
> +                          lrnat_rec->lb_force_snat_router_ip);
>   
>       for (size_t i = 0; i < lrnat_rec->n_nat_entries; i++) {
>           struct ovn_nat *nat_entry = &lrnat_rec->nat_entries[i];
> @@ -16697,6 +16670,21 @@ build_lrouter_nat_defrag_and_lb(
>               continue;
>           }
>   
> +        if (!stateless) {
> +            switch (nat_entry->type) {
> +            case DNAT:
> +                stateful_dnat = true;
> +                break;
> +            case SNAT:
> +                stateful_snat = true;
> +                break;
> +            case DNAT_AND_SNAT:
> +                stateful_snat = true;
> +                stateful_dnat = true;
> +                break;
> +            }
> +        }
> +
>           /* S_ROUTER_IN_UNSNAT
>            * Ingress UNSNAT table: It is for already established connections'
>            * reverse traffic. i.e., SNAT has already been done in egress
> @@ -16819,7 +16807,7 @@ build_lrouter_nat_defrag_and_lb(
>           } else {
>               build_lrouter_out_snat_flow(lflows, od, nat_entry, match, actions,
>                                           distributed_nat, mac, cidr_bits, is_v6,
> -                                        l3dgw_port, lflow_ref, features);
> +                                        l3dgw_port, lflow_ref);
>           }
>   
>           /* S_ROUTER_IN_ADMISSION - S_ROUTER_IN_IP_INPUT */
> @@ -16909,6 +16897,34 @@ build_lrouter_nat_defrag_and_lb(
>           }
>       }
>   
> +
> +    bool can_commit = features->ct_commit_to_zone && features->ct_next_zone;
> +    if (can_commit && stateful_dnat) {
> +        ovn_lflow_add(lflows, od, S_ROUTER_IN_DEFRAG, 10,
> +                      "ip && (!ct.trk || !ct.rpl)",
> +                      "ct_next(dnat);", lflow_ref);
> +        ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 10,
> +                      "ip && ct.new", "ct_commit_to_zone(dnat);", lflow_ref);
> +    }
> +
> +    if (can_commit && stateful_snat) {
> +        /* We would lose the CT state especially the ct.new flag if we have
> +         * mixed SNAT and DNAT on single LR. In order to know if we actually
> +         * can commit into SNAT zone keep the flag in register. The SNAT flows
> +         * in the egress pipeline can then check the flag and commit
> +         * based on that. */
> +        ovn_lflow_add(lflows, od, S_ROUTER_IN_POST_UNSNAT, 10,
> +                      "ip && (!ct.trk || ct.new)",
> +                      "flags.unsnat_new = 1; next;", lflow_ref);
> +        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 10,
> +                      "ip && (!ct.trk || !ct.rpl) && "
> +                      "flags.unsnat_new == 1", "ct_next(snat);",
> +                      lflow_ref);
> +        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 10,
> +                      "ip && ct.new && flags.unsnat_new == 1",
> +                      "ct_commit_to_zone(snat);", lflow_ref);
> +    }
> +
>       if (use_common_zone && od->nbr->n_nat) {
>           ds_clear(match);
>           ds_put_cstr(match, "ip && ct_mark.natted == 1");
> diff --git a/northd/northd.h b/northd/northd.h
> index c1442ff40..162097018 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -481,27 +481,28 @@ enum ovn_stage {
>       PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
>       PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_REQ,  4, "lr_in_dhcp_relay_req") \
>       PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          5, "lr_in_unsnat")       \
> -    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          6, "lr_in_defrag")       \
> -    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    7, "lr_in_lb_aff_check") \
> -    PIPELINE_STAGE(ROUTER, IN,  DNAT,            8, "lr_in_dnat")         \
> -    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    9, "lr_in_lb_aff_learn") \
> -    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   10, "lr_in_ecmp_stateful") \
> -    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   11, "lr_in_nd_ra_options") \
> -    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  12, "lr_in_nd_ra_response") \
> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  13, "lr_in_ip_routing_pre")  \
> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      14, "lr_in_ip_routing")      \
> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 15, "lr_in_ip_routing_ecmp") \
> -    PIPELINE_STAGE(ROUTER, IN,  POLICY,          16, "lr_in_policy")          \
> -    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     17, "lr_in_policy_ecmp")     \
> -    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 18,                      \
> +    PIPELINE_STAGE(ROUTER, IN,  POST_UNSNAT,     6, "lr_in_post_unsnat")  \
> +    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          7, "lr_in_defrag")       \
> +    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    8, "lr_in_lb_aff_check") \
> +    PIPELINE_STAGE(ROUTER, IN,  DNAT,            9, "lr_in_dnat")         \
> +    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    10, "lr_in_lb_aff_learn") \
> +    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   11, "lr_in_ecmp_stateful") \
> +    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   12, "lr_in_nd_ra_options") \
> +    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  13, "lr_in_nd_ra_response") \
> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  14, "lr_in_ip_routing_pre")  \
> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      15, "lr_in_ip_routing")      \
> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 16, "lr_in_ip_routing_ecmp") \
> +    PIPELINE_STAGE(ROUTER, IN,  POLICY,          17, "lr_in_policy")          \
> +    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     18, "lr_in_policy_ecmp")     \
> +    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 19,                      \
>                     "lr_in_dhcp_relay_resp_chk")                                \
> -    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 19,                          \
> +    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 20,                          \
>                     "lr_in_dhcp_relay_resp")                                    \
> -    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     20, "lr_in_arp_resolve")     \
> -    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     21, "lr_in_chk_pkt_len")     \
> -    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     22, "lr_in_larger_pkts")     \
> -    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     23, "lr_in_gw_redirect")     \
> -    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     24, "lr_in_arp_request")     \
> +    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     21, "lr_in_arp_resolve")     \
> +    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     22, "lr_in_chk_pkt_len")     \
> +    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     23, "lr_in_larger_pkts")     \
> +    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     24, "lr_in_gw_redirect")     \
> +    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     25, "lr_in_arp_request")     \
>                                                                         \
>       /* Logical router egress stages. */                               \
>       PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       \
> diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
> index efb333a47..beaeef167 100644
> --- a/tests/ovn-macros.at
> +++ b/tests/ovn-macros.at
> @@ -1315,6 +1315,12 @@ ovn_strip_collector_set() {
>       sed 's/collector_set=[[0-9]]*,\?/collector_set=??,/g'
>   }
>   
> +get_zone_num() {
> +    output=$1
> +    name=$2
> +    printf "$output" | grep $name | cut -d ' ' -f 2
> +}
> +
>   OVS_END_SHELL_HELPERS
>   
>   m4_define([OVN_POPULATE_ARP], [AT_CHECK(ovn_populate_arp__, [0], [ignore])])
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 8477e4250..4b936f1c6 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -1188,18 +1188,18 @@ AT_CAPTURE_FILE([crflows])
>   
>   AT_CHECK([grep -e "lr_out_snat" drflows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.src == $allowed_range), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
>   ])
>   
>   AT_CHECK([grep -e "lr_out_post_snat" drflows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.src == $allowed_range && ct.new), action=(ct_commit_to_zone(snat);)
>   ])
>   
>   AT_CHECK([grep -e "lr_out_snat" crflows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
>   ])
> @@ -1227,19 +1227,19 @@ AT_CAPTURE_FILE([crflows2])
>   
>   AT_CHECK([grep -e "lr_out_snat" drflows2 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
>     table=??(lr_out_snat        ), priority=163  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $disallowed_range), action=(next;)
>   ])
>   
>   AT_CHECK([grep -e "lr_out_post_snat" drflows2 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ct.new), action=(ct_commit_to_zone(snat);)
>   ])
>   
>   AT_CHECK([grep -e "lr_out_snat" crflows2 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
>     table=??(lr_out_snat        ), priority=35   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $disallowed_range), action=(next;)
> @@ -1266,6 +1266,7 @@ AT_CAPTURE_FILE([crflows2])
>   
>   AT_CHECK([grep -e "lr_out_snat" drflows3 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
>   ])
> @@ -1276,6 +1277,7 @@ AT_CHECK([grep -e "lr_out_post_snat" drflows3 | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep -e "lr_out_snat" crflows3 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
>   ])
> @@ -1301,6 +1303,7 @@ AT_CAPTURE_FILE([crflows2])
>   
>   AT_CHECK([grep -e "lr_out_snat" drflows4 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
>     table=??(lr_out_snat        ), priority=163  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $disallowed_range), action=(next;)
> @@ -1308,6 +1311,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows4 | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep -e "lr_out_snat" crflows4 | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
>     table=??(lr_out_snat        ), priority=35   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $disallowed_range), action=(next;)
> @@ -4284,12 +4288,14 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.4:8080);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.40:8080);)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
> @@ -4315,12 +4321,14 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.40:8080; force_snat);)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
> @@ -4333,6 +4341,7 @@ AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=100  , match=(flags.force_snat_for_lb == 1 && ip4), action=(ct_snat(20.0.0.4);)
>     table=??(lr_out_snat        ), priority=100  , match=(flags.force_snat_for_lb == 1 && ip6), action=(ct_snat(aef0::4);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> @@ -4346,7 +4355,7 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="router_ip"
> @@ -4366,12 +4375,14 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.40:8080; force_snat);)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
> @@ -4384,6 +4395,7 @@ AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
> @@ -4398,7 +4410,7 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   check ovn-nbctl --wait=sb remove logical_router lr0 options chassis
> @@ -4431,12 +4443,14 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.40:8080; force_snat);)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
> @@ -4449,6 +4463,7 @@ AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
> @@ -4464,7 +4479,7 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   check ovn-nbctl --wait=sb lb-add lb2 10.0.0.20:80 10.0.0.40:8080
> @@ -4483,6 +4498,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.20), action=(ct_dnat;)
>   ])
> @@ -4505,7 +4521,7 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CLEANUP
> @@ -5767,10 +5783,12 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat_in_czone(10.0.0.3);)
>   ])
>   
> @@ -5789,10 +5807,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.10);)
>     table=??(lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
> @@ -5819,10 +5839,12 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat(10.0.0.3);)
>   ])
>   
> @@ -5837,24 +5859,20 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
> -  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> -  table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
>   ])
>   
>   AT_CHECK([grep "lr_out_post_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_snat   ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
>   ])
>   
>   # Associate load balancer to lr0
> @@ -5889,6 +5907,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
> @@ -5897,6 +5916,7 @@ AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat_in_czone(10.0.0.3);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.80,10.0.0.81);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.4:8080);)
> @@ -5929,10 +5949,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.10);)
>     table=??(lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
> @@ -5959,6 +5981,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
> @@ -5967,6 +5990,7 @@ AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat(10.0.0.3);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.80,10.0.0.81);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.4:8080);)
> @@ -5995,24 +6019,20 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
> -  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> -  table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
>   ])
>   
>   AT_CHECK([grep "lr_out_post_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_snat   ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
>   ])
>   
>   # Make the logical router as Gateway router
> @@ -6033,6 +6053,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
> @@ -6041,6 +6062,7 @@ AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(ct_lb_mark(backends=10.0.0.80,10.0.0.81);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.4:8080);)
> @@ -6066,11 +6088,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>     table=??(lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
>     table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
> @@ -6096,6 +6119,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
> @@ -6104,6 +6128,7 @@ AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.80,10.0.0.81; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
> @@ -6129,11 +6154,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> @@ -6160,6 +6186,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
> @@ -6169,6 +6196,7 @@ AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.80,10.0.0.81; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
> @@ -6195,11 +6223,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
> @@ -6236,6 +6265,7 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.10), action=(ct_dnat;)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
> @@ -6246,6 +6276,7 @@ AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.80,10.0.0.81; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
> @@ -6273,11 +6304,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>     table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-public"), action=(ct_snat(def0::10);)
> @@ -6308,11 +6340,13 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
>     table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.210), action=(ct_dnat;)
>   ])
>   
>   AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.210 && tcp && tcp.dst == 60), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.50:6062,10.0.0.60:6062; force_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.210 && udp && udp.dst == 60), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.50:6062,10.0.0.60:6062; force_snat);)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
> @@ -6335,11 +6369,12 @@ AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
>   ])
>   
>   AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
>     table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
>   ])
>   
> @@ -6371,6 +6406,7 @@ check ovn-nbctl --wait=sb sync
>   
>   AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.10), action=(reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=??);};)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
> @@ -6385,6 +6421,7 @@ check ovn-nbctl --wait=sb set load_balancer lb5 options:skip_snat=true
>   
>   AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.10), action=(flags.skip_snat_for_lb = 1; reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=??);};)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
> @@ -6401,6 +6438,7 @@ check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="route
>   
>   AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.10), action=(flags.force_snat_for_lb = 1; reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=??);};)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
> @@ -6418,6 +6456,7 @@ check ovn-nbctl --wait=sb lr-lb-add lr0 lb6
>   
>   AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.30), action=(drop;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
> @@ -6432,6 +6471,7 @@ check ovn-nbctl --wait=sb set load_balancer lb6 options:skip_snat=true
>   
>   AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.30), action=(flags.skip_snat_for_lb = 1; drop;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
> @@ -6448,6 +6488,7 @@ check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="route
>   
>   AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.30), action=(flags.force_snat_for_lb = 1; drop;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
>     table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
> @@ -7997,9 +8038,6 @@ AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | ovn_strip_lflows], [0], [dn
>   ])
>   
>   AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | ovn_strip_lflows], [0], [dnl
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat;)
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_snat;)
> -  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_snat;)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.10);)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2") && (!ct.trk || !ct.rpl)), action=(ct_snat(10.0.0.10);)
>     table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3") && (!ct.trk || !ct.rpl)), action=(ct_snat(192.168.0.10);)
> @@ -8007,9 +8045,6 @@ AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | ovn_strip_lflows], [0], [dnl
>   
>   AT_CHECK([grep lr_out_post_snat lrflows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ct.new), action=(ct_commit_to_zone(snat);)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S2" && is_chassis_resident("cr-DR-S2") && ct.new), action=(ct_commit_to_zone(snat);)
> -  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3") && ct.new), action=(ct_commit_to_zone(snat);)
>   ])
>   
>   check ovn-nbctl --wait=sb lr-nat-del DR snat 20.0.0.10
> @@ -9435,6 +9470,7 @@ AT_CHECK([grep "lr_in_lb_aff_check" R1flows | ovn_strip_lflows], [0], [dnl
>   ])
>   AT_CHECK([grep "lr_in_dnat " R1flows | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; ct_lb_mark(backends=10.0.0.2:80);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; ct_lb_mark(backends=20.0.0.2:80);)
> @@ -9459,6 +9495,7 @@ AT_CAPTURE_FILE([R1flows_skip_snat])
>   
>   AT_CHECK([grep "lr_in_dnat " R1flows_skip_snat | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; skip_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; skip_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=20.0.0.2:80; skip_snat);)
> @@ -9480,6 +9517,7 @@ AT_CAPTURE_FILE([R1flows_force_snat])
>   
>   AT_CHECK([grep "lr_in_dnat " R1flows_force_snat | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; force_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; force_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.force_snat_for_lb = 1; ct_lb_mark(backends=20.0.0.2:80; force_snat);)
> @@ -9500,6 +9538,7 @@ AT_CAPTURE_FILE([R1flows_force_skip_snat])
>   
>   AT_CHECK([grep "lr_in_dnat " R1flows_force_skip_snat | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; skip_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; skip_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=20.0.0.2:80; skip_snat);)
> @@ -9524,6 +9563,7 @@ AT_CAPTURE_FILE([R1flows_2lbs])
>   
>   AT_CHECK([grep "lr_in_dnat " R1flows_2lbs | ovn_strip_lflows], [0], [dnl
>     table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; skip_snat);)
>     table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.20 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; force_snat);)
>     table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; skip_snat);)
> diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
> index c59556173..ffc69d67c 100644
> --- a/tests/system-common-macros.at
> +++ b/tests/system-common-macros.at
> @@ -237,6 +237,14 @@ m4_define([STRIP_MONITOR_CSUM], [grep "csum:" | sed 's/csum:.*/csum: <skip>/'])
>   m4_define([FORMAT_CT],
>       [[grep -F "dst=$1," | sed -e 's/port=[0-9]*/port=<cleared>/g' -e 's/id=[0-9]*/id=<cleared>/g' -e 's/state=[0-9_A-Z]*/state=<cleared>/g' | sort | uniq]])
>   
> +# FORMAT_CT_WITH_ZONE([ip-addr], [zone])
> +#
> +# Strip content from the piped input which would differ from test to test
> +# and limit the output to the rows containing 'ip-addr' and 'zone'.
> +#
> +m4_define([FORMAT_CT_WITH_ZONE],
> +    [[grep -F "dst=$1," | grep -F "zone=$2" | sed -e 's/port=[0-9]*/port=<cleared>/g' -e 's/id=[0-9]*/id=<cleared>/g' -e 's/state=[0-9_A-Z]*/state=<cleared>/g' | sort]])
> +
>   # DAEMONIZE([command], [pidfile])
>   #
>   # Run 'command' as a background process and record its pid to 'pidfile' to
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 145399ded..599e5ff50 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -89,37 +89,62 @@ ADD_VETH(bar1, bar1, br-int, "192.168.2.2/24", "f0:00:00:01:02:05", \
>   ovn-nbctl lsp-add bar bar1 \
>   -- lsp-set-addresses bar1 "f0:00:00:01:02:05 192.168.2.2"
>   
> -# Add a DNAT rule.
> -ovn-nbctl -- --id=@nat create nat type="dnat" logical_ip=192.168.1.2 \
> -    external_ip=30.0.0.2 -- add logical_router R2 nat @nat
> +# Add a DNAT and SNAT rule.
> +check ovn-nbctl lr-nat-add R2 dnat_and_snat 30.0.0.2 192.168.1.2
>   
>   # Add a SNAT rule
> -ovn-nbctl -- --id=@nat create nat type="snat" logical_ip=192.168.2.2 \
> -    external_ip=30.0.0.1 -- add logical_router R2 nat @nat
> +check ovn-nbctl lr-nat-add R2 snat 30.0.0.1 192.168.2.2
>   
>   # wait for ovn-controller to catch up.
>   ovn-nbctl --wait=hv sync
>   OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=30.0.0.1)'])
>   
> -# 'alice1' should be able to ping 'foo1' directly.
> -NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 192.168.1.2 | FORMAT_PING], \
> -[0], [dnl
> +ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
> +r2_dnat=$(get_zone_num "$ct_zones" R2_dnat)
> +r2_snat=$(get_zone_num "$ct_zones" R2_snat)
> +
> +test_alice1_to_foo1() {
> +    check ovn-nbctl --wait=hv sync
> +
> +    check ovs-appctl dpctl/flush-conntrack
> +
> +    # 'alice1' should be able to ping 'foo1' directly.
> +    NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 192.168.1.2 | FORMAT_PING], \
> +    [0], [dnl
>   3 packets transmitted, 3 received, 0% packet loss, time 0ms
>   ])
>   
> -# North-South DNAT: 'alice1' should also be able to ping 'foo1' via 30.0.0.2
> -NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 30.0.0.2 | FORMAT_PING], \
> -[0], [dnl
> +    # North-South DNAT: 'alice1' should also be able to ping 'foo1' via 30.0.0.2
> +    NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 30.0.0.2 | FORMAT_PING], \
> +    [0], [dnl
>   3 packets transmitted, 3 received, 0% packet loss, time 0ms
>   ])
>   
> -# Check conntrack entries.
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.2) | \
> -sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +    # Check conntrack entries.
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.2) | \
> +    sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
>   icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
>   icmp,orig=(src=172.16.1.2,dst=30.0.0.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
>   ])
>   
> +    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.2], [$r2_snat])], [0], [dnl
> +icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_snat
> +icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_snat
> +])
> +
> +    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.2], [$r2_dnat])], [0], [dnl
> +icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_dnat
> +icmp,orig=(src=172.16.1.2,dst=30.0.0.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_dnat
> +])
> +}
> +
> +test_alice1_to_foo1
> +
> +check ovn-nbctl lr-nat-del R2 dnat_and_snat
> +check ovn-nbctl lr-nat-add R2 dnat 30.0.0.2 192.168.1.2
> +test_alice1_to_foo1
> +
>   # South-North SNAT: 'bar1' pings 'alice1'. But 'alice1' receives traffic
>   # from 30.0.0.1
>   NS_CHECK_EXEC([bar1], [ping -q -c 3 -i 0.3 -w 2 172.16.1.2 | FORMAT_PING], \
> @@ -269,37 +294,62 @@ ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:05", \
>   ovn-nbctl lsp-add bar bar1 \
>   -- lsp-set-addresses bar1 "f0:00:00:01:02:05 fd12::2"
>   
> -# Add a DNAT rule.
> -ovn-nbctl -- --id=@nat create nat type="dnat" logical_ip=\"fd11::2\" \
> -    external_ip=\"fd30::2\" -- add logical_router R2 nat @nat
> +# Add a DNAT and SNAT rule.
> +check ovn-nbctl lr-nat-add R2 dnat_and_snat fd30::2 fd11::2
>   
>   # Add a SNAT rule
> -ovn-nbctl -- --id=@nat create nat type="snat" logical_ip=\"fd12::2\" \
> -    external_ip=\"fd30::1\" -- add logical_router R2 nat @nat
> +check ovn-nbctl lr-nat-add R2 snat fd30::1 fd12::2
>   
>   # wait for ovn-controller to catch up.
>   ovn-nbctl --wait=hv sync
>   OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=fd30::1)'])
>   
> -# 'alice1' should be able to ping 'foo1' directly.
> -NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd11::2 | FORMAT_PING], \
> -[0], [dnl
> +ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
> +r2_snat=$(get_zone_num "$ct_zones" R2_snat)
> +r2_dnat=$(get_zone_num "$ct_zones" R2_dnat)
> +
> +test_alice1_to_foo1() {
> +    check ovn-nbctl --wait=hv sync
> +
> +    check ovs-appctl dpctl/flush-conntrack
> +
> +    # 'alice1' should be able to ping 'foo1' directly.
> +    NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd11::2 | FORMAT_PING], \
> +    [0], [dnl
>   3 packets transmitted, 3 received, 0% packet loss, time 0ms
>   ])
>   
> -# North-South DNAT: 'alice1' should also be able to ping 'foo1' via fd30::2
> -NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd30::2 | FORMAT_PING], \
> -[0], [dnl
> +    # North-South DNAT: 'alice1' should also be able to ping 'foo1' via fd30::2
> +    NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd30::2 | FORMAT_PING], \
> +    [0], [dnl
>   3 packets transmitted, 3 received, 0% packet loss, time 0ms
>   ])
>   
> -# Check conntrack entries.
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd21::2) | \
> -sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +    # Check conntrack entries.
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd21::2) | \
> +    sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=<cleared>
>   icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=<cleared>
>   icmpv6,orig=(src=fd21::2,dst=fd30::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=<cleared>
>   ])
>   
> +    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd21::2], [$r2_snat])], [0], [dnl
> +icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_snat
> +icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_snat
> +])
> +
> +    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd21::2], [$r2_dnat])], [0], [dnl
> +icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_dnat
> +icmpv6,orig=(src=fd21::2,dst=fd30::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_dnat
> +])
> +}
> +
> +test_alice1_to_foo1
> +
> +check ovn-nbctl lr-nat-del R2 dnat_and_snat
> +check ovn-nbctl lr-nat-add R2 dnat_and_snat fd30::2 fd11::2
> +test_alice1_to_foo1
> +
>   # South-North SNAT: 'bar1' pings 'alice1'. But 'alice1' receives traffic
>   # from fd30::1
>   NS_CHECK_EXEC([bar1], [ping6 -q -c 3 -i 0.3 -w 2 fd21::2 | FORMAT_PING], \
> @@ -3753,6 +3803,7 @@ NS_CHECK_EXEC([foo2], [ping6 -q -c 3 -i 0.3 -w 2 fd20::2 | FORMAT_PING], \
>   ovs-appctl dpctl/dump-conntrack | grep icmpv6
>   AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd11::3) | \
>   sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmpv6,orig=(src=fd11::3,dst=fd20::2,id=<cleared>,type=128,code=0),reply=(src=fd20::2,dst=fd11::3,id=<cleared>,type=129,code=0),zone=<cleared>
>   ])
>   
>   # We verify that SNAT indeed happened via 'dump-conntrack' command.
> @@ -3924,6 +3975,10 @@ AT_CHECK([ovn-nbctl lr-nat-add R1 snat 172.16.1.1 0.0.0.0/0])
>   ovn-nbctl --wait=hv sync
>   OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=172.16.1.1)'])
>   
> +ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
> +r1_snat=$(get_zone_num "$ct_zones" R1_snat)
> +r1_dnat=$(get_zone_num "$ct_zones" R1_dnat)
> +
>   echo "------ hv dump ------"
>   ovs-ofctl show br-int
>   ovs-ofctl dump-flows br-int
> @@ -3938,6 +3993,8 @@ NS_CHECK_EXEC([foo1], [ping -q -c 3 -i 0.3 -w 2 192.168.2.2 | FORMAT_PING], \
>   # We verify that the connection is not tracked.

I think "not" needs to be removed from the comment above.

>   AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(192.168.2.2) | \
>   sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmp,orig=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
> +icmp,orig=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
>   ])
>   
>   AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> @@ -3950,6 +4007,8 @@ NS_CHECK_EXEC([foo2], [ping -q -c 3 -i 0.3 -w 2 192.168.2.2 | FORMAT_PING], \
>   # We verify that the connection is not tracked.

Like my previous finding, I believe "not" needs to be removed here.

>   AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(192.168.2.2) | \
>   sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmp,orig=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
> +icmp,orig=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
>   ])
>   
>   AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> @@ -3959,9 +4018,11 @@ NS_CHECK_EXEC([bar1], [ping -q -c 3 -i 0.3 -w 2 192.168.1.3 | FORMAT_PING], \
>   3 packets transmitted, 3 received, 0% packet loss, time 0ms
>   ])
>   
> -# We verify that the connection is not tracked.
> +# We verify that the connection is tracked.
>   AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(192.168.2.2) | \
>   sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmp,orig=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=8,code=0),reply=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
> +icmp,orig=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=8,code=0),reply=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
>   ])
>   
>   AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> @@ -3978,6 +4039,7 @@ AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(172.16.1.4) |
>   sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
>   icmp,orig=(src=172.16.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=172.16.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
>   icmp,orig=(src=192.168.1.2,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=172.16.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
> +icmp,orig=(src=192.168.1.2,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=192.168.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
>   ])
>   
>   AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> @@ -3997,6 +4059,16 @@ icmp,orig=(src=172.16.1.1,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src
>   icmp,orig=(src=192.168.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=<cleared>
>   ])
>   
> +AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.1], [$r1_snat])], [0], [dnl
> +icmp,orig=(src=172.16.1.1,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=$r1_snat
> +icmp,orig=(src=192.168.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=$r1_snat
> +])
> +
> +AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.4], [$r1_dnat])], [0], [dnl
> +icmp,orig=(src=172.16.1.1,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=$r1_dnat
> +icmp,orig=(src=192.168.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=192.168.1.3,id=<cleared>,type=0,code=0),zone=$r1_dnat
> +])
> +
>   OVS_APP_EXIT_AND_WAIT([ovn-controller])
>   
>   as ovn-sb
> @@ -4108,6 +4180,10 @@ AT_CHECK([ovn-nbctl lr-nat-add R1 snat fd20::1 ::/0])
>   ovn-nbctl --wait=hv sync
>   OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=fd20::1)'])
>   
> +ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
> +r1_snat=$(get_zone_num "$ct_zones" R1_snat)
> +r1_dnat=$(get_zone_num "$ct_zones" R1_dnat)
> +
>   echo "------ hv dump ------"
>   ovs-ofctl show br-int
>   ovs-ofctl dump-flows br-int
> @@ -4144,6 +4220,7 @@ NS_CHECK_EXEC([foo1], [ping -q -c 3 -i 0.3 -w 2 fd20::4 | FORMAT_PING], \
>   # Then DNAT of 'bar1' address happens (listed first below).
>   AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd20::4) | \
>   sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmpv6,orig=(src=fd11::2,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd11::2,id=<cleared>,type=129,code=0),zone=<cleared>
>   icmpv6,orig=(src=fd11::2,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd20::3,id=<cleared>,type=129,code=0),zone=<cleared>
>   icmpv6,orig=(src=fd20::3,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::3,id=<cleared>,type=129,code=0),zone=<cleared>
>   ])
> @@ -4165,6 +4242,16 @@ icmpv6,orig=(src=fd20::1,dst=fd12::2,id=<cleared>,type=128,code=0),reply=(src=fd
>   icmpv6,orig=(src=fd20::1,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::1,id=<cleared>,type=129,code=0),zone=<cleared>
>   ])
>   
> +AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd20::1], [$r1_snat])], [0], [dnl
> +icmpv6,orig=(src=fd11::3,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd20::1,id=<cleared>,type=129,code=0),zone=$r1_snat
> +icmpv6,orig=(src=fd20::1,dst=fd12::2,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::1,id=<cleared>,type=129,code=0),zone=$r1_snat
> +])
> +
> +AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd20::4], [$r1_dnat])], [0], [dnl
> +icmpv6,orig=(src=fd11::3,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd11::3,id=<cleared>,type=129,code=0),zone=$r1_dnat
> +icmpv6,orig=(src=fd20::1,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::1,id=<cleared>,type=129,code=0),zone=$r1_dnat
> +])
> +
>   OVS_APP_EXIT_AND_WAIT([ovn-controller])
>   
>   as ovn-sb
> @@ -8682,10 +8769,10 @@ test_ping sw11 192.168.1.2
>   OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep -v "n_packets=0" | grep 'nat(src=172.16.1.21)'])
>   # Ensure conntrack entry is present
>   OVS_WAIT_FOR_OUTPUT([
> -    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
> +    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.1.2) | \
>         sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> -icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
> -tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
> +icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.21,id=<cleared>,type=0,code=0),zone=<cleared>
> +tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.21,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
>   ])
>   
>   AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> @@ -8697,9 +8784,11 @@ test_ping sw11 192.168.1.2
>   
>   # Ensure conntrack entry is present
>   OVS_WAIT_FOR_OUTPUT([
> -    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
> +    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.1.2) | \
>         sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.21,id=<cleared>,type=0,code=0),zone=<cleared>
>   icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
> +tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.21,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
>   tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
>   ])
>   
> @@ -8711,10 +8800,10 @@ test_ping sw11 172.16.1.2
>   
>   # Ensure conntrack entry is present
>   OVS_WAIT_FOR_OUTPUT([
> -    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
> +    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.1.2) | \
>         sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> -icmp,orig=(src=192.168.2.2,dst=172.16.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
> -tcp,orig=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
> +icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.21,id=<cleared>,type=0,code=0),zone=<cleared>
> +tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.21,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
>   ])
>   
>   AT_CHECK([ovs-appctl dpctl/flush-conntrack])
Han Zhou Jan. 23, 2025, 5:01 p.m. UTC | #4
On Tue, Jan 7, 2025 at 7:44 AM Ales Musil <amusil@redhat.com> wrote:
>
>
>
> On Wed, Dec 18, 2024 at 2:08 PM Dumitru Ceara <dceara@redhat.com> wrote:
>>
>> On 11/20/24 5:37 PM, Ales Musil wrote:
>> > Commit all traffic that is not already commit by either NAT or LB. This
>> > ensures that the traffic is tracked, and we don't erroneously commit
>> > reply traffic, or reply traffic is not marked as invalid.
>> >
>> > To achieve the commit we need to perform lookup on every packet
>> > that goes through LR pipeline whenever there is stateful NAT.
>> >
>> > The SNAT lookup requires additional flag as the unSNAT is happening
>> > in ingress pipeline and at that point we need to know if the packet
>> > is reply or not. This is not required for DNAT, because unDNAT stage
>> > happens in egress.
>> >
>> > Signed-off-by: Ales Musil <amusil@redhat.com>
>> > ---
>>
>> Hi Han,
>>
>> I was wondering if you had time to evaluate the performance impact of
>> this patch in your lab.
>>
>> Thanks,
>> Dumitru
>>
>
> Hi Han,
>
> by any chance did you have some time to run the performance tests?
> ovn-kubernetes is really interested in this as it will fix one of the use
cases
> that they have.
>
> Thank you,
> Ales

Hi Folks, I am sorry that the test got delayed. Adding Alin here because he
is willing to help on this. Hopefully we will get some results soon.

Thanks,
Han
diff mbox series

Patch

diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
index d563e044c..05bc18eab 100644
--- a/include/ovn/logical-fields.h
+++ b/include/ovn/logical-fields.h
@@ -86,6 +86,7 @@  enum mff_log_flags_bits {
     MLF_LOCALNET_BIT = 15,
     MLF_RX_FROM_TUNNEL_BIT = 16,
     MLF_ICMP_SNAT_BIT = 17,
+    MLF_UNSNAT_NEW_BIT = 18,
 };
 
 /* MFF_LOG_FLAGS_REG flag assignments */
@@ -141,6 +142,9 @@  enum mff_log_flags {
     MLF_RX_FROM_TUNNEL = (1 << MLF_RX_FROM_TUNNEL_BIT),
 
     MLF_ICMP_SNAT = (1 << MLF_ICMP_SNAT_BIT),
+
+    /* Indicate that the packet didn't go through unSNAT. */
+    MLF_UNSNAT_NEW = (1 << MLF_UNSNAT_NEW_BIT),
 };
 
 /* OVN logical fields
diff --git a/lib/logical-fields.c b/lib/logical-fields.c
index 5a8b53f2b..081faad5a 100644
--- a/lib/logical-fields.c
+++ b/lib/logical-fields.c
@@ -139,6 +139,10 @@  ovn_init_symtab(struct shash *symtab)
                              flags_str);
     snprintf(flags_str, sizeof flags_str, "flags[%d]", MLF_RX_FROM_TUNNEL_BIT);
     expr_symtab_add_subfield(symtab, "flags.tunnel_rx", NULL, flags_str);
+    snprintf(flags_str, sizeof flags_str, "flags[%d]",
+             MLF_UNSNAT_NEW_BIT);
+    expr_symtab_add_subfield(symtab, "flags.unsnat_new", NULL,
+                             flags_str);
 
     /* Connection tracking state. */
     expr_symtab_add_field_scoped(symtab, "ct_mark", MFF_CT_MARK, NULL, false,
diff --git a/northd/northd.c b/northd/northd.c
index aed1d425f..95d2d5bde 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -16207,8 +16207,7 @@  build_lrouter_out_snat_flow(struct lflow_table *lflows,
                             struct ds *actions, bool distributed_nat,
                             struct eth_addr mac, int cidr_bits, bool is_v6,
                             struct ovn_port *l3dgw_port,
-                            struct lflow_ref *lflow_ref,
-                            const struct chassis_features *features)
+                            struct lflow_ref *lflow_ref)
 {
     if (!(nat_entry->type == SNAT || nat_entry->type == DNAT_AND_SNAT)) {
         return;
@@ -16239,34 +16238,6 @@  build_lrouter_out_snat_flow(struct lflow_table *lflows,
                             priority, ds_cstr(match),
                             ds_cstr(actions), &nat->header_,
                             lflow_ref);
-
-    /* For the SNAT networks, we need to make sure that connections are
-     * properly tracked so we can decide whether to perform SNAT on traffic
-     * exiting the network. */
-    if (features->ct_commit_to_zone && features->ct_next_zone &&
-        nat_entry->type == SNAT && !od->is_gw_router) {
-        /* For traffic that comes from SNAT network, initiate CT state before
-         * entering S_ROUTER_OUT_SNAT to allow matching on various CT states.
-         */
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 70,
-                      ds_cstr(match), "ct_next(snat);",
-                      lflow_ref);
-
-        build_lrouter_out_snat_match(lflows, od, nat, match,
-                                     distributed_nat, cidr_bits, is_v6,
-                                     l3dgw_port, lflow_ref, true);
-
-        /* New traffic that goes into SNAT network is committed to CT to avoid
-         * SNAT-ing replies.*/
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority,
-                      ds_cstr(match), "ct_snat;",
-                      lflow_ref);
-
-        ds_put_cstr(match, " && ct.new");
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_SNAT, priority,
-                      ds_cstr(match), "ct_commit_to_zone(snat);",
-                      lflow_ref);
-    }
 }
 
 static void
@@ -16548,6 +16519,8 @@  static void build_lr_nat_defrag_and_lb_default_flows(
     /* Packets are allowed by default. */
     ovn_lflow_add(lflows, od, S_ROUTER_IN_DEFRAG, 0, "1", "next;", lflow_ref);
     ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 0, "1", "next;", lflow_ref);
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_POST_UNSNAT, 0, "1", "next;",
+                  lflow_ref);
     ovn_lflow_add(lflows, od, S_ROUTER_OUT_CHECK_DNAT_LOCAL, 0, "1",
                   REGBIT_DST_NAT_IP_LOCAL" = 0; next;", lflow_ref);
     ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 0, "1", "next;", lflow_ref);
@@ -16659,9 +16632,6 @@  build_lrouter_nat_defrag_and_lb(
         ovn_lflow_add(lflows, od, S_ROUTER_OUT_UNDNAT, 50,
                       "ip", "flags.loopback = 1; ct_dnat;",
                       lflow_ref);
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 50,
-                      "ip && ct.new", "ct_commit { } ; next; ",
-                      lflow_ref);
     }
 
     /* NAT rules are only valid on Gateway routers and routers with
@@ -16679,6 +16649,9 @@  build_lrouter_nat_defrag_and_lb(
         !lport_addresses_is_empty(&lrnat_rec->dnat_force_snat_addrs);
     bool lb_force_snat_ip =
         !lport_addresses_is_empty(&lrnat_rec->lb_force_snat_addrs);
+    bool stateful_dnat = lr_stateful_rec->has_lb_vip;
+    bool stateful_snat = (dnat_force_snat_ip || lb_force_snat_ip ||
+                          lrnat_rec->lb_force_snat_router_ip);
 
     for (size_t i = 0; i < lrnat_rec->n_nat_entries; i++) {
         struct ovn_nat *nat_entry = &lrnat_rec->nat_entries[i];
@@ -16697,6 +16670,21 @@  build_lrouter_nat_defrag_and_lb(
             continue;
         }
 
+        if (!stateless) {
+            switch (nat_entry->type) {
+            case DNAT:
+                stateful_dnat = true;
+                break;
+            case SNAT:
+                stateful_snat = true;
+                break;
+            case DNAT_AND_SNAT:
+                stateful_snat = true;
+                stateful_dnat = true;
+                break;
+            }
+        }
+
         /* S_ROUTER_IN_UNSNAT
          * Ingress UNSNAT table: It is for already established connections'
          * reverse traffic. i.e., SNAT has already been done in egress
@@ -16819,7 +16807,7 @@  build_lrouter_nat_defrag_and_lb(
         } else {
             build_lrouter_out_snat_flow(lflows, od, nat_entry, match, actions,
                                         distributed_nat, mac, cidr_bits, is_v6,
-                                        l3dgw_port, lflow_ref, features);
+                                        l3dgw_port, lflow_ref);
         }
 
         /* S_ROUTER_IN_ADMISSION - S_ROUTER_IN_IP_INPUT */
@@ -16909,6 +16897,34 @@  build_lrouter_nat_defrag_and_lb(
         }
     }
 
+
+    bool can_commit = features->ct_commit_to_zone && features->ct_next_zone;
+    if (can_commit && stateful_dnat) {
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_DEFRAG, 10,
+                      "ip && (!ct.trk || !ct.rpl)",
+                      "ct_next(dnat);", lflow_ref);
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 10,
+                      "ip && ct.new", "ct_commit_to_zone(dnat);", lflow_ref);
+    }
+
+    if (can_commit && stateful_snat) {
+        /* We would lose the CT state especially the ct.new flag if we have
+         * mixed SNAT and DNAT on single LR. In order to know if we actually
+         * can commit into SNAT zone keep the flag in register. The SNAT flows
+         * in the egress pipeline can then check the flag and commit
+         * based on that. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_POST_UNSNAT, 10,
+                      "ip && (!ct.trk || ct.new)",
+                      "flags.unsnat_new = 1; next;", lflow_ref);
+        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 10,
+                      "ip && (!ct.trk || !ct.rpl) && "
+                      "flags.unsnat_new == 1", "ct_next(snat);",
+                      lflow_ref);
+        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 10,
+                      "ip && ct.new && flags.unsnat_new == 1",
+                      "ct_commit_to_zone(snat);", lflow_ref);
+    }
+
     if (use_common_zone && od->nbr->n_nat) {
         ds_clear(match);
         ds_put_cstr(match, "ip && ct_mark.natted == 1");
diff --git a/northd/northd.h b/northd/northd.h
index c1442ff40..162097018 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -481,27 +481,28 @@  enum ovn_stage {
     PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
     PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_REQ,  4, "lr_in_dhcp_relay_req") \
     PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          5, "lr_in_unsnat")       \
-    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          6, "lr_in_defrag")       \
-    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    7, "lr_in_lb_aff_check") \
-    PIPELINE_STAGE(ROUTER, IN,  DNAT,            8, "lr_in_dnat")         \
-    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    9, "lr_in_lb_aff_learn") \
-    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   10, "lr_in_ecmp_stateful") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   11, "lr_in_nd_ra_options") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  12, "lr_in_nd_ra_response") \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  13, "lr_in_ip_routing_pre")  \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      14, "lr_in_ip_routing")      \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 15, "lr_in_ip_routing_ecmp") \
-    PIPELINE_STAGE(ROUTER, IN,  POLICY,          16, "lr_in_policy")          \
-    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     17, "lr_in_policy_ecmp")     \
-    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 18,                      \
+    PIPELINE_STAGE(ROUTER, IN,  POST_UNSNAT,     6, "lr_in_post_unsnat")  \
+    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          7, "lr_in_defrag")       \
+    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    8, "lr_in_lb_aff_check") \
+    PIPELINE_STAGE(ROUTER, IN,  DNAT,            9, "lr_in_dnat")         \
+    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    10, "lr_in_lb_aff_learn") \
+    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   11, "lr_in_ecmp_stateful") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   12, "lr_in_nd_ra_options") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  13, "lr_in_nd_ra_response") \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  14, "lr_in_ip_routing_pre")  \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      15, "lr_in_ip_routing")      \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 16, "lr_in_ip_routing_ecmp") \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY,          17, "lr_in_policy")          \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     18, "lr_in_policy_ecmp")     \
+    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 19,                      \
                   "lr_in_dhcp_relay_resp_chk")                                \
-    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 19,                          \
+    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 20,                          \
                   "lr_in_dhcp_relay_resp")                                    \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     20, "lr_in_arp_resolve")     \
-    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     21, "lr_in_chk_pkt_len")     \
-    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     22, "lr_in_larger_pkts")     \
-    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     23, "lr_in_gw_redirect")     \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     24, "lr_in_arp_request")     \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     21, "lr_in_arp_resolve")     \
+    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     22, "lr_in_chk_pkt_len")     \
+    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     23, "lr_in_larger_pkts")     \
+    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     24, "lr_in_gw_redirect")     \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     25, "lr_in_arp_request")     \
                                                                       \
     /* Logical router egress stages. */                               \
     PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       \
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index efb333a47..beaeef167 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -1315,6 +1315,12 @@  ovn_strip_collector_set() {
     sed 's/collector_set=[[0-9]]*,\?/collector_set=??,/g'
 }
 
+get_zone_num() {
+    output=$1
+    name=$2
+    printf "$output" | grep $name | cut -d ' ' -f 2
+}
+
 OVS_END_SHELL_HELPERS
 
 m4_define([OVN_POPULATE_ARP], [AT_CHECK(ovn_populate_arp__, [0], [ignore])])
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 8477e4250..4b936f1c6 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -1188,18 +1188,18 @@  AT_CAPTURE_FILE([crflows])
 
 AT_CHECK([grep -e "lr_out_snat" drflows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.src == $allowed_range), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
 ])
 
 AT_CHECK([grep -e "lr_out_post_snat" drflows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.src == $allowed_range && ct.new), action=(ct_commit_to_zone(snat);)
 ])
 
 AT_CHECK([grep -e "lr_out_snat" crflows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
 ])
@@ -1227,19 +1227,19 @@  AT_CAPTURE_FILE([crflows2])
 
 AT_CHECK([grep -e "lr_out_snat" drflows2 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
   table=??(lr_out_snat        ), priority=163  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $disallowed_range), action=(next;)
 ])
 
 AT_CHECK([grep -e "lr_out_post_snat" drflows2 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ct.new), action=(ct_commit_to_zone(snat);)
 ])
 
 AT_CHECK([grep -e "lr_out_snat" crflows2 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.1);)
   table=??(lr_out_snat        ), priority=35   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $disallowed_range), action=(next;)
@@ -1266,6 +1266,7 @@  AT_CAPTURE_FILE([crflows2])
 
 AT_CHECK([grep -e "lr_out_snat" drflows3 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
 ])
@@ -1276,6 +1277,7 @@  AT_CHECK([grep -e "lr_out_post_snat" drflows3 | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep -e "lr_out_snat" crflows3 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $allowed_range && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
 ])
@@ -1301,6 +1303,7 @@  AT_CAPTURE_FILE([crflows2])
 
 AT_CHECK([grep -e "lr_out_snat" drflows4 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
   table=??(lr_out_snat        ), priority=163  , match=(ip && ip4.src == 50.0.0.11 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.dst == $disallowed_range), action=(next;)
@@ -1308,6 +1311,7 @@  AT_CHECK([grep -e "lr_out_snat" drflows4 | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep -e "lr_out_snat" crflows4 | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 50.0.0.11 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.2);)
   table=??(lr_out_snat        ), priority=35   , match=(ip && ip4.src == 50.0.0.11 && ip4.dst == $disallowed_range), action=(next;)
@@ -4284,12 +4288,14 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.4:8080);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.40:8080);)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
@@ -4315,12 +4321,14 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.40:8080; force_snat);)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
@@ -4333,6 +4341,7 @@  AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=100  , match=(flags.force_snat_for_lb == 1 && ip4), action=(ct_snat(20.0.0.4);)
   table=??(lr_out_snat        ), priority=100  , match=(flags.force_snat_for_lb == 1 && ip6), action=(ct_snat(aef0::4);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
@@ -4346,7 +4355,7 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="router_ip"
@@ -4366,12 +4375,14 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.40:8080; force_snat);)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
@@ -4384,6 +4395,7 @@  AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
@@ -4398,7 +4410,7 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 check ovn-nbctl --wait=sb remove logical_router lr0 options chassis
@@ -4431,12 +4443,14 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.100 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.40:8080; force_snat);)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
@@ -4449,6 +4463,7 @@  AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
@@ -4464,7 +4479,7 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 check ovn-nbctl --wait=sb lb-add lb2 10.0.0.20:80 10.0.0.40:8080
@@ -4483,6 +4498,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.100), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.20), action=(ct_dnat;)
 ])
@@ -4505,7 +4521,7 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CLEANUP
@@ -5767,10 +5783,12 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat_in_czone(10.0.0.3);)
 ])
 
@@ -5789,10 +5807,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.10);)
   table=??(lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
@@ -5819,10 +5839,12 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat(10.0.0.3);)
 ])
 
@@ -5837,24 +5859,20 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
-  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 AT_CHECK([grep "lr_out_post_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_snat   ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
 ])
 
 # Associate load balancer to lr0
@@ -5889,6 +5907,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
@@ -5897,6 +5916,7 @@  AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat_in_czone(10.0.0.3);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.80,10.0.0.81);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.4:8080);)
@@ -5929,10 +5949,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat_in_czone(172.168.0.10);)
   table=??(lr_out_snat        ), priority=154  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl) && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.168.0.10);)
@@ -5959,6 +5981,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
@@ -5967,6 +5990,7 @@  AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_dnat(10.0.0.3);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.80,10.0.0.81);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80 && is_chassis_resident("cr-lr0-public")), action=(ct_lb_mark(backends=10.0.0.4:8080);)
@@ -5995,24 +6019,20 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
-  table=??(lr_out_post_undnat ), priority=70   , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
-  table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 10.0.0.0/24 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public")), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.10 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 10.0.0.3 && outport == "lr0-public" && is_chassis_resident("cr-lr0-public") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.20);)
 ])
 
 AT_CHECK([grep "lr_out_post_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_snat   ), priority=153  , match=(ip && ip4.dst == 10.0.0.0/24 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 10.0.0.10 && inport == "lr0-public" && is_chassis_resident("cr-lr0-public") && ct.new), action=(ct_commit_to_zone(snat);)
 ])
 
 # Make the logical router as Gateway router
@@ -6033,6 +6053,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
@@ -6041,6 +6062,7 @@  AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(ct_lb_mark(backends=10.0.0.80,10.0.0.81);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.4:8080);)
@@ -6066,11 +6088,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
   table=??(lr_out_snat        ), priority=25   , match=(ip && ip4.src == 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
   table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
@@ -6096,6 +6119,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.200), action=(ct_dnat;)
@@ -6104,6 +6128,7 @@  AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.80,10.0.0.81; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
@@ -6129,11 +6154,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
@@ -6160,6 +6186,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
@@ -6169,6 +6196,7 @@  AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.80,10.0.0.81; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
@@ -6195,11 +6223,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
@@ -6236,6 +6265,7 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.10), action=(ct_dnat;)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.100), action=(ct_dnat;)
@@ -6246,6 +6276,7 @@  AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.168.0.20), action=(flags.loopback = 1; ct_dnat(10.0.0.3);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.200), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.80,10.0.0.81; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.4:8080; force_snat);)
@@ -6273,11 +6304,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=??(lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-public"), action=(ct_snat(def0::10);)
@@ -6308,11 +6340,13 @@  AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_in_defrag" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_defrag       ), priority=10   , match=(ip && (!ct.trk || !ct.rpl)), action=(ct_next(dnat);)
   table=??(lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 172.168.0.210), action=(ct_dnat;)
 ])
 
 AT_CHECK([grep "lr_in_dnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.210 && tcp && tcp.dst == 60), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.50:6062,10.0.0.60:6062; force_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.0.210 && udp && udp.dst == 60), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.50:6062,10.0.0.60:6062; force_snat);)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
@@ -6335,11 +6369,12 @@  AT_CHECK([grep "lr_out_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep "lr_out_post_undnat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_undnat ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_undnat ), priority=50   , match=(ip && ct.new), action=(ct_commit { } ; next; )
+  table=??(lr_out_post_undnat ), priority=10   , match=(ip && (!ct.trk || !ct.rpl) && flags.unsnat_new == 1), action=(ct_next(snat);)
 ])
 
 AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && flags.unsnat_new == 1), action=(ct_commit_to_zone(snat);)
   table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
 ])
 
@@ -6371,6 +6406,7 @@  check ovn-nbctl --wait=sb sync
 
 AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.10), action=(reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=??);};)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
@@ -6385,6 +6421,7 @@  check ovn-nbctl --wait=sb set load_balancer lb5 options:skip_snat=true
 
 AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.10), action=(flags.skip_snat_for_lb = 1; reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=??);};)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
@@ -6401,6 +6438,7 @@  check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="route
 
 AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.10), action=(flags.force_snat_for_lb = 1; reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=??);};)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
@@ -6418,6 +6456,7 @@  check ovn-nbctl --wait=sb lr-lb-add lr0 lb6
 
 AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.30), action=(drop;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
@@ -6432,6 +6471,7 @@  check ovn-nbctl --wait=sb set load_balancer lb6 options:skip_snat=true
 
 AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.30), action=(flags.skip_snat_for_lb = 1; drop;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
@@ -6448,6 +6488,7 @@  check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="route
 
 AT_CHECK([ovn-sbctl dump-flows lr0 | grep "lr_in_dnat" | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.168.10.30), action=(flags.force_snat_for_lb = 1; drop;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.est && !ct.rel && !ct.new && ct_mark.natted), action=(next;)
   table=??(lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
@@ -7997,9 +8038,6 @@  AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | ovn_strip_lflows], [0], [dn
 ])
 
 AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | ovn_strip_lflows], [0], [dnl
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat;)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_snat;)
-  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_snat;)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.10);)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2") && (!ct.trk || !ct.rpl)), action=(ct_snat(10.0.0.10);)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3") && (!ct.trk || !ct.rpl)), action=(ct_snat(192.168.0.10);)
@@ -8007,9 +8045,6 @@  AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | ovn_strip_lflows], [0], [dnl
 
 AT_CHECK([grep lr_out_post_snat lrflows | ovn_strip_lflows], [0], [dnl
   table=??(lr_out_post_snat   ), priority=0    , match=(1), action=(next;)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ct.new), action=(ct_commit_to_zone(snat);)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S2" && is_chassis_resident("cr-DR-S2") && ct.new), action=(ct_commit_to_zone(snat);)
-  table=??(lr_out_post_snat   ), priority=161  , match=(ip && ip4.dst == 20.0.0.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3") && ct.new), action=(ct_commit_to_zone(snat);)
 ])
 
 check ovn-nbctl --wait=sb lr-nat-del DR snat 20.0.0.10
@@ -9435,6 +9470,7 @@  AT_CHECK([grep "lr_in_lb_aff_check" R1flows | ovn_strip_lflows], [0], [dnl
 ])
 AT_CHECK([grep "lr_in_dnat " R1flows | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; ct_lb_mark(backends=10.0.0.2:80);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; ct_lb_mark(backends=20.0.0.2:80);)
@@ -9459,6 +9495,7 @@  AT_CAPTURE_FILE([R1flows_skip_snat])
 
 AT_CHECK([grep "lr_in_dnat " R1flows_skip_snat | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; skip_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; skip_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=20.0.0.2:80; skip_snat);)
@@ -9480,6 +9517,7 @@  AT_CAPTURE_FILE([R1flows_force_snat])
 
 AT_CHECK([grep "lr_in_dnat " R1flows_force_snat | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; force_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; force_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.force_snat_for_lb = 1; ct_lb_mark(backends=20.0.0.2:80; force_snat);)
@@ -9500,6 +9538,7 @@  AT_CAPTURE_FILE([R1flows_force_skip_snat])
 
 AT_CHECK([grep "lr_in_dnat " R1flows_force_skip_snat | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; skip_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; skip_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 20.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=20.0.0.2:80; skip_snat);)
@@ -9524,6 +9563,7 @@  AT_CAPTURE_FILE([R1flows_2lbs])
 
 AT_CHECK([grep "lr_in_dnat " R1flows_2lbs | ovn_strip_lflows], [0], [dnl
   table=??(lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=??(lr_in_dnat         ), priority=10   , match=(ip && ct.new), action=(ct_commit_to_zone(dnat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.10 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; skip_snat);)
   table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && ip4 && ip4.dst == 172.16.0.20 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80,20.0.0.2:80; force_snat);)
   table=??(lr_in_dnat         ), priority=150  , match=(reg9[[6]] == 1 && ct.new && ip4.dst == 172.16.0.10 && reg4 == 10.0.0.2 && reg8[[0..15]] == 80), action=(reg0 = 172.16.0.10; flags.skip_snat_for_lb = 1; ct_lb_mark(backends=10.0.0.2:80; skip_snat);)
diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
index c59556173..ffc69d67c 100644
--- a/tests/system-common-macros.at
+++ b/tests/system-common-macros.at
@@ -237,6 +237,14 @@  m4_define([STRIP_MONITOR_CSUM], [grep "csum:" | sed 's/csum:.*/csum: <skip>/'])
 m4_define([FORMAT_CT],
     [[grep -F "dst=$1," | sed -e 's/port=[0-9]*/port=<cleared>/g' -e 's/id=[0-9]*/id=<cleared>/g' -e 's/state=[0-9_A-Z]*/state=<cleared>/g' | sort | uniq]])
 
+# FORMAT_CT_WITH_ZONE([ip-addr], [zone])
+#
+# Strip content from the piped input which would differ from test to test
+# and limit the output to the rows containing 'ip-addr' and 'zone'.
+#
+m4_define([FORMAT_CT_WITH_ZONE],
+    [[grep -F "dst=$1," | grep -F "zone=$2" | sed -e 's/port=[0-9]*/port=<cleared>/g' -e 's/id=[0-9]*/id=<cleared>/g' -e 's/state=[0-9_A-Z]*/state=<cleared>/g' | sort]])
+
 # DAEMONIZE([command], [pidfile])
 #
 # Run 'command' as a background process and record its pid to 'pidfile' to
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 145399ded..599e5ff50 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -89,37 +89,62 @@  ADD_VETH(bar1, bar1, br-int, "192.168.2.2/24", "f0:00:00:01:02:05", \
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:05 192.168.2.2"
 
-# Add a DNAT rule.
-ovn-nbctl -- --id=@nat create nat type="dnat" logical_ip=192.168.1.2 \
-    external_ip=30.0.0.2 -- add logical_router R2 nat @nat
+# Add a DNAT and SNAT rule.
+check ovn-nbctl lr-nat-add R2 dnat_and_snat 30.0.0.2 192.168.1.2
 
 # Add a SNAT rule
-ovn-nbctl -- --id=@nat create nat type="snat" logical_ip=192.168.2.2 \
-    external_ip=30.0.0.1 -- add logical_router R2 nat @nat
+check ovn-nbctl lr-nat-add R2 snat 30.0.0.1 192.168.2.2
 
 # wait for ovn-controller to catch up.
 ovn-nbctl --wait=hv sync
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=30.0.0.1)'])
 
-# 'alice1' should be able to ping 'foo1' directly.
-NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 192.168.1.2 | FORMAT_PING], \
-[0], [dnl
+ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
+r2_dnat=$(get_zone_num "$ct_zones" R2_dnat)
+r2_snat=$(get_zone_num "$ct_zones" R2_snat)
+
+test_alice1_to_foo1() {
+    check ovn-nbctl --wait=hv sync
+
+    check ovs-appctl dpctl/flush-conntrack
+
+    # 'alice1' should be able to ping 'foo1' directly.
+    NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 192.168.1.2 | FORMAT_PING], \
+    [0], [dnl
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
 
-# North-South DNAT: 'alice1' should also be able to ping 'foo1' via 30.0.0.2
-NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 30.0.0.2 | FORMAT_PING], \
-[0], [dnl
+    # North-South DNAT: 'alice1' should also be able to ping 'foo1' via 30.0.0.2
+    NS_CHECK_EXEC([alice1], [ping -q -c 3 -i 0.3 -w 2 30.0.0.2 | FORMAT_PING], \
+    [0], [dnl
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
 
-# Check conntrack entries.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.2) | \
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+    # Check conntrack entries.
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.2) | \
+    sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
 icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
 icmp,orig=(src=172.16.1.2,dst=30.0.0.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
 ])
 
+    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.2], [$r2_snat])], [0], [dnl
+icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_snat
+icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_snat
+])
+
+    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.2], [$r2_dnat])], [0], [dnl
+icmp,orig=(src=172.16.1.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_dnat
+icmp,orig=(src=172.16.1.2,dst=30.0.0.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.2,id=<cleared>,type=0,code=0),zone=$r2_dnat
+])
+}
+
+test_alice1_to_foo1
+
+check ovn-nbctl lr-nat-del R2 dnat_and_snat
+check ovn-nbctl lr-nat-add R2 dnat 30.0.0.2 192.168.1.2
+test_alice1_to_foo1
+
 # South-North SNAT: 'bar1' pings 'alice1'. But 'alice1' receives traffic
 # from 30.0.0.1
 NS_CHECK_EXEC([bar1], [ping -q -c 3 -i 0.3 -w 2 172.16.1.2 | FORMAT_PING], \
@@ -269,37 +294,62 @@  ADD_VETH(bar1, bar1, br-int, "fd12::2/64", "f0:00:00:01:02:05", \
 ovn-nbctl lsp-add bar bar1 \
 -- lsp-set-addresses bar1 "f0:00:00:01:02:05 fd12::2"
 
-# Add a DNAT rule.
-ovn-nbctl -- --id=@nat create nat type="dnat" logical_ip=\"fd11::2\" \
-    external_ip=\"fd30::2\" -- add logical_router R2 nat @nat
+# Add a DNAT and SNAT rule.
+check ovn-nbctl lr-nat-add R2 dnat_and_snat fd30::2 fd11::2
 
 # Add a SNAT rule
-ovn-nbctl -- --id=@nat create nat type="snat" logical_ip=\"fd12::2\" \
-    external_ip=\"fd30::1\" -- add logical_router R2 nat @nat
+check ovn-nbctl lr-nat-add R2 snat fd30::1 fd12::2
 
 # wait for ovn-controller to catch up.
 ovn-nbctl --wait=hv sync
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=fd30::1)'])
 
-# 'alice1' should be able to ping 'foo1' directly.
-NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd11::2 | FORMAT_PING], \
-[0], [dnl
+ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
+r2_snat=$(get_zone_num "$ct_zones" R2_snat)
+r2_dnat=$(get_zone_num "$ct_zones" R2_dnat)
+
+test_alice1_to_foo1() {
+    check ovn-nbctl --wait=hv sync
+
+    check ovs-appctl dpctl/flush-conntrack
+
+    # 'alice1' should be able to ping 'foo1' directly.
+    NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd11::2 | FORMAT_PING], \
+    [0], [dnl
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
 
-# North-South DNAT: 'alice1' should also be able to ping 'foo1' via fd30::2
-NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd30::2 | FORMAT_PING], \
-[0], [dnl
+    # North-South DNAT: 'alice1' should also be able to ping 'foo1' via fd30::2
+    NS_CHECK_EXEC([alice1], [ping6 -q -c 3 -i 0.3 -w 2 fd30::2 | FORMAT_PING], \
+    [0], [dnl
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
 
-# Check conntrack entries.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd21::2) | \
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+    # Check conntrack entries.
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd21::2) | \
+    sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=<cleared>
 icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=<cleared>
 icmpv6,orig=(src=fd21::2,dst=fd30::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=<cleared>
 ])
 
+    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd21::2], [$r2_snat])], [0], [dnl
+icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_snat
+icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_snat
+])
+
+    AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd21::2], [$r2_dnat])], [0], [dnl
+icmpv6,orig=(src=fd21::2,dst=fd11::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_dnat
+icmpv6,orig=(src=fd21::2,dst=fd30::2,id=<cleared>,type=128,code=0),reply=(src=fd11::2,dst=fd21::2,id=<cleared>,type=129,code=0),zone=$r2_dnat
+])
+}
+
+test_alice1_to_foo1
+
+check ovn-nbctl lr-nat-del R2 dnat_and_snat
+check ovn-nbctl lr-nat-add R2 dnat_and_snat fd30::2 fd11::2
+test_alice1_to_foo1
+
 # South-North SNAT: 'bar1' pings 'alice1'. But 'alice1' receives traffic
 # from fd30::1
 NS_CHECK_EXEC([bar1], [ping6 -q -c 3 -i 0.3 -w 2 fd21::2 | FORMAT_PING], \
@@ -3753,6 +3803,7 @@  NS_CHECK_EXEC([foo2], [ping6 -q -c 3 -i 0.3 -w 2 fd20::2 | FORMAT_PING], \
 ovs-appctl dpctl/dump-conntrack | grep icmpv6
 AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd11::3) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmpv6,orig=(src=fd11::3,dst=fd20::2,id=<cleared>,type=128,code=0),reply=(src=fd20::2,dst=fd11::3,id=<cleared>,type=129,code=0),zone=<cleared>
 ])
 
 # We verify that SNAT indeed happened via 'dump-conntrack' command.
@@ -3924,6 +3975,10 @@  AT_CHECK([ovn-nbctl lr-nat-add R1 snat 172.16.1.1 0.0.0.0/0])
 ovn-nbctl --wait=hv sync
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=172.16.1.1)'])
 
+ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
+r1_snat=$(get_zone_num "$ct_zones" R1_snat)
+r1_dnat=$(get_zone_num "$ct_zones" R1_dnat)
+
 echo "------ hv dump ------"
 ovs-ofctl show br-int
 ovs-ofctl dump-flows br-int
@@ -3938,6 +3993,8 @@  NS_CHECK_EXEC([foo1], [ping -q -c 3 -i 0.3 -w 2 192.168.2.2 | FORMAT_PING], \
 # We verify that the connection is not tracked.
 AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(192.168.2.2) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
+icmp,orig=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
 ])
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])
@@ -3950,6 +4007,8 @@  NS_CHECK_EXEC([foo2], [ping -q -c 3 -i 0.3 -w 2 192.168.2.2 | FORMAT_PING], \
 # We verify that the connection is not tracked.
 AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(192.168.2.2) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
+icmp,orig=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
 ])
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])
@@ -3959,9 +4018,11 @@  NS_CHECK_EXEC([bar1], [ping -q -c 3 -i 0.3 -w 2 192.168.1.3 | FORMAT_PING], \
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
 
-# We verify that the connection is not tracked.
+# We verify that the connection is tracked.
 AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(192.168.2.2) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=8,code=0),reply=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
+icmp,orig=(src=192.168.2.2,dst=192.168.1.3,id=<cleared>,type=8,code=0),reply=(src=192.168.1.3,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
 ])
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])
@@ -3978,6 +4039,7 @@  AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep icmp | FORMAT_CT(172.16.1.4) |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 icmp,orig=(src=172.16.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=172.16.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
 icmp,orig=(src=192.168.1.2,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=172.16.1.3,id=<cleared>,type=0,code=0),zone=<cleared>
+icmp,orig=(src=192.168.1.2,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=192.168.1.2,id=<cleared>,type=0,code=0),zone=<cleared>
 ])
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])
@@ -3997,6 +4059,16 @@  icmp,orig=(src=172.16.1.1,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src
 icmp,orig=(src=192.168.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=<cleared>
 ])
 
+AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.1], [$r1_snat])], [0], [dnl
+icmp,orig=(src=172.16.1.1,dst=192.168.2.2,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=$r1_snat
+icmp,orig=(src=192.168.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=$r1_snat
+])
+
+AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([172.16.1.4], [$r1_dnat])], [0], [dnl
+icmp,orig=(src=172.16.1.1,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=192.168.2.2,dst=172.16.1.1,id=<cleared>,type=0,code=0),zone=$r1_dnat
+icmp,orig=(src=192.168.1.3,dst=172.16.1.4,id=<cleared>,type=8,code=0),reply=(src=172.16.1.4,dst=192.168.1.3,id=<cleared>,type=0,code=0),zone=$r1_dnat
+])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -4108,6 +4180,10 @@  AT_CHECK([ovn-nbctl lr-nat-add R1 snat fd20::1 ::/0])
 ovn-nbctl --wait=hv sync
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep 'nat(src=fd20::1)'])
 
+ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
+r1_snat=$(get_zone_num "$ct_zones" R1_snat)
+r1_dnat=$(get_zone_num "$ct_zones" R1_dnat)
+
 echo "------ hv dump ------"
 ovs-ofctl show br-int
 ovs-ofctl dump-flows br-int
@@ -4144,6 +4220,7 @@  NS_CHECK_EXEC([foo1], [ping -q -c 3 -i 0.3 -w 2 fd20::4 | FORMAT_PING], \
 # Then DNAT of 'bar1' address happens (listed first below).
 AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd20::4) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmpv6,orig=(src=fd11::2,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd11::2,id=<cleared>,type=129,code=0),zone=<cleared>
 icmpv6,orig=(src=fd11::2,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd20::3,id=<cleared>,type=129,code=0),zone=<cleared>
 icmpv6,orig=(src=fd20::3,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::3,id=<cleared>,type=129,code=0),zone=<cleared>
 ])
@@ -4165,6 +4242,16 @@  icmpv6,orig=(src=fd20::1,dst=fd12::2,id=<cleared>,type=128,code=0),reply=(src=fd
 icmpv6,orig=(src=fd20::1,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::1,id=<cleared>,type=129,code=0),zone=<cleared>
 ])
 
+AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd20::1], [$r1_snat])], [0], [dnl
+icmpv6,orig=(src=fd11::3,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd20::1,id=<cleared>,type=129,code=0),zone=$r1_snat
+icmpv6,orig=(src=fd20::1,dst=fd12::2,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::1,id=<cleared>,type=129,code=0),zone=$r1_snat
+])
+
+AT_CHECK_UNQUOTED([ovs-appctl dpctl/dump-conntrack | FORMAT_CT_WITH_ZONE([fd20::4], [$r1_dnat])], [0], [dnl
+icmpv6,orig=(src=fd11::3,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd20::4,dst=fd11::3,id=<cleared>,type=129,code=0),zone=$r1_dnat
+icmpv6,orig=(src=fd20::1,dst=fd20::4,id=<cleared>,type=128,code=0),reply=(src=fd12::2,dst=fd20::1,id=<cleared>,type=129,code=0),zone=$r1_dnat
+])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -8682,10 +8769,10 @@  test_ping sw11 192.168.1.2
 OVS_WAIT_UNTIL([ovs-ofctl dump-flows br-int | grep -v "n_packets=0" | grep 'nat(src=172.16.1.21)'])
 # Ensure conntrack entry is present
 OVS_WAIT_FOR_OUTPUT([
-    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.1.2) | \
       sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
-tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.21,id=<cleared>,type=0,code=0),zone=<cleared>
+tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.21,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
 ])
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])
@@ -8697,9 +8784,11 @@  test_ping sw11 192.168.1.2
 
 # Ensure conntrack entry is present
 OVS_WAIT_FOR_OUTPUT([
-    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.1.2) | \
       sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.21,id=<cleared>,type=0,code=0),zone=<cleared>
 icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
+tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.21,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
 tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
 ])
 
@@ -8711,10 +8800,10 @@  test_ping sw11 172.16.1.2
 
 # Ensure conntrack entry is present
 OVS_WAIT_FOR_OUTPUT([
-    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.1.2) | \
       sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-icmp,orig=(src=192.168.2.2,dst=172.16.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=192.168.2.2,id=<cleared>,type=0,code=0),zone=<cleared>
-tcp,orig=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+icmp,orig=(src=192.168.2.2,dst=192.168.1.2,id=<cleared>,type=8,code=0),reply=(src=192.168.1.2,dst=172.16.1.21,id=<cleared>,type=0,code=0),zone=<cleared>
+tcp,orig=(src=192.168.2.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.21,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
 ])
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])