СC. Martin Kalcok

On 06.10.2026 11:51, Alexandra Rukomoinikova wrote:
> On distributed routers, the flows that track traffic of an SNAT network
> also matched traffic of a stateless dnat_and_snat inside that network,
> so it was committed to the SNAT CT zone.  Stateless NAT must bypass
> conntrack.
>
> Add higher priority flows for stateless dnat_and_snat that just skip
> the tracking, reusing the same helper as stateful NAT.
>
> Fixes: 40136a2f2c84 ("northd: Fix direct access to SNAT network.")
> Fixes: 79f4cfd9c5d4 ("northd: Avoid committing DNAT traffic to SNAT zone.")
> Signed-off-by: Alexandra Rukomoinikova <[email protected]>
> ---
>   Documentation/ref/ovn-logical-flows.7.rst |  34 +++++-
>   northd/northd.c                           | 128 +++++++++++++---------
>   tests/ovn-northd.at                       |  68 ++++++++++++
>   tests/system-ovn.at                       |  16 +++
>   4 files changed, 191 insertions(+), 55 deletions(-)
>
> diff --git a/Documentation/ref/ovn-logical-flows.7.rst 
> b/Documentation/ref/ovn-logical-flows.7.rst
> index 44fd560bf..194074642 100644
> --- a/Documentation/ref/ovn-logical-flows.7.rst
> +++ b/Documentation/ref/ovn-logical-flows.7.rst
> @@ -3784,7 +3784,19 @@ Egress Table 2: Post UNDNAT
>   
>   - A priority-70 logical flow is added that initiates CT state for traffic 
> that
>     is configured to be SNATed on Distributed routers. This allows the next 
> table,
> -  ``lr_out_snat``, to effectively match on various CT states.
> +  ``lr_out_snat``, to effectively match on various CT states. The flow 
> matches
> +  on ``ip && ip4.src == A && outport == GW && (!ct.trk || !ct.rpl)`` with an
> +  action ``ct_next(snat);``, where *A* is the logical IP or network of the 
> NAT
> +  rule and *GW* is the logical router gateway port.
> +
> +  If the NAT rule is of type dnat_and_snat, the flow is added with priority 
> 75
> +  instead, so that the traffic of its logical IP is not tracked by a SNAT 
> rule
> +  covering the same network. The action is ``ct_next(dnat);``. If the
> +  dnat_and_snat rule has ``stateless=true`` in the options, the flow does not
> +  match on CT state and its action is ``next;``, so that the traffic bypasses
> +  conntrack.
> +
> +  These flows are not added if ``options:ct-commit-all`` is set to ``true``.
>   
>   - A priority-50 logical flow is added that commits any untracked flows from 
> the
>     previous table :ref:`UNDNAT <lr-out-1>` for Gateway routers.  This flow
> @@ -3923,7 +3935,17 @@ based on the configuration in the OVN Northbound 
> database.
>     ``ip4.src == A && outport == GW``, this flow matches on ``ip4.dst == A &&
>     inport == GW``. A CT state is initiated for this traffic so that the 
> following
>     table, ``lr_out_post_snat``, can identify whether the traffic flow was
> -  initiated from the internal or external network.
> +  initiated from the internal or external network. The flow has priority *P*,
> +  an additional match ``(!ct.trk || !ct.rpl)`` and an action ``ct_snat;``.
> +
> +  If the NAT rule is of type dnat_and_snat, the flow is added with priority
> +  ``P + 5`` and an action ``ct_dnat;``, so that the traffic of its logical IP
> +  is tracked in the DNAT CT zone instead of the SNAT CT zone of a SNAT rule
> +  covering the same network. If the dnat_and_snat rule has ``stateless=true``
> +  in the options, the flow does not match on CT state and its action is
> +  ``next;``, so that the traffic bypasses conntrack.
> +
> +  This flow is not added if ``options:ct-commit-all`` is set to ``true``.
>   
>   - If the ``options:ct-commit-all`` is set to ``true`` the following two 
> flows
>     are configured matching on ``ip && (!ct.trk || !ct.rpl) && 
> flags.unsnat_new ==
> @@ -3945,8 +3967,12 @@ Packets reaching this table are processed according to 
> the flows below:
>     routers, and was initiated from an external network (i.e. it matches
>     ``ct.new``), is committed to the SNAT CT zone. This ensures that replies
>     returning from the SNATed network do not have their source address 
> translated.
> -  For details about match rules and priority see section :ref:`SNAT on
> -  Distributed Routers <lr-out-3>`.
> +  The action is ``ct_commit_to_zone(snat);``. If the NAT rule is of type
> +  dnat_and_snat, the traffic is committed to the DNAT CT zone with an action
> +  ``ct_commit_to_zone(dnat);`` instead. Traffic of a dnat_and_snat rule that
> +  has ``stateless=true`` in the options is not committed. For details about
> +  match rules and priority see section :ref:`SNAT on Distributed Routers
> +  <lr-out-3>`.
>   
>   - A priority-0 logical flow that matches all packets not already handled 
> (match
>     ``1``) and action ``next;``.
> diff --git a/northd/northd.c b/northd/northd.c
> index f37040b57..4e4849f8c 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -18373,6 +18373,69 @@ build_lrouter_out_snat_match(struct lflow_table 
> *lflows,
>       }
>   }
>   
> +static void
> +build_lrouter_out_snat_track_flows(struct lflow_table *lflows,
> +                                   const struct ovn_datapath *od,
> +                                   const struct ovn_nat *nat_entry,
> +                                   struct ds *match, struct ds *actions,
> +                                   bool distributed_nat, int cidr_bits,
> +                                   bool is_v6, struct ovn_port *l3dgw_port,
> +                                   struct lflow_ref *lflow_ref,
> +                                   bool commit_all,
> +                                   const struct chassis_features *features,
> +                                   bool stateless)
> +{
> +    if (!features->ct_commit_to_zone || !features->ct_next_zone ||
> +        od->is_gw_router || commit_all || lrouter_use_common_zone(od)) {
> +        return;
> +    }
> +
> +    const struct nbrec_nat *nat = nat_entry->nb;
> +    uint16_t priority = lrouter_nat_get_priority(od, nat, false, cidr_bits);
> +    const char *zone = nat_entry->type == SNAT ? "snat" : "dnat";
> +    uint16_t prio_offset = nat_entry->type == SNAT ? 0 : 5;
> +
> +    build_lrouter_out_snat_match(lflows, od, nat, match, distributed_nat,
> +                                 cidr_bits, is_v6, l3dgw_port, lflow_ref,
> +                                 false);
> +    ds_clear(actions);
> +    if (stateless) {
> +        ds_put_cstr(actions, "next;");
> +    } else {
> +        ds_put_cstr(match, " && (!ct.trk || !ct.rpl)");
> +        ds_put_format(actions, "ct_next(%s);", zone);
> +    }
> +    ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 70 + prio_offset,
> +                  ds_cstr(match), ds_cstr(actions), lflow_ref,
> +                  WITH_HINT(&nat->header_));
> +
> +    build_lrouter_out_snat_match(lflows, od, nat, match, distributed_nat,
> +                                 cidr_bits, is_v6, l3dgw_port, lflow_ref,
> +                                 true);
> +    if (stateless) {
> +        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority + prio_offset,
> +                      ds_cstr(match), "next;", lflow_ref,
> +                      WITH_HINT(&nat->header_));
> +        return;
> +    }
> +
> +    size_t match_any_state_len = match->length;
> +    ds_put_cstr(match, " && (!ct.trk || !ct.rpl)");
> +    ds_clear(actions);
> +    ds_put_format(actions, "ct_%s;", zone);
> +    ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority + prio_offset,
> +                  ds_cstr(match), ds_cstr(actions), lflow_ref,
> +                  WITH_HINT(&nat->header_));
> +
> +    ds_truncate(match, match_any_state_len);
> +    ds_put_cstr(match, " && ct.new");
> +    ds_clear(actions);
> +    ds_put_format(actions, "ct_commit_to_zone(%s);", zone);
> +    ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_SNAT, priority + prio_offset,
> +                  ds_cstr(match), ds_cstr(actions), lflow_ref,
> +                  WITH_HINT(&nat->header_));
> +}
> +
>   static void
>   build_lrouter_out_snat_stateless_flow(struct lflow_table *lflows,
>                                         const struct ovn_datapath *od,
> @@ -18381,7 +18444,9 @@ build_lrouter_out_snat_stateless_flow(struct 
> lflow_table *lflows,
>                                         bool distributed_nat,
>                                         struct eth_addr mac, int cidr_bits,
>                                         bool is_v6, struct ovn_port 
> *l3dgw_port,
> -                                      struct lflow_ref *lflow_ref)
> +                                      struct lflow_ref *lflow_ref,
> +                                      bool commit_all,
> +                                      const struct chassis_features 
> *features)
>   {
>       if (!(nat_entry->type == SNAT || nat_entry->type == DNAT_AND_SNAT)) {
>           return;
> @@ -18405,6 +18470,11 @@ build_lrouter_out_snat_stateless_flow(struct 
> lflow_table *lflows,
>   
>       ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority, ds_cstr(match),
>                     ds_cstr(actions), lflow_ref, WITH_HINT(&nat->header_));
> +
> +    build_lrouter_out_snat_track_flows(lflows, od, nat_entry, match, actions,
> +                                       distributed_nat, cidr_bits, is_v6,
> +                                       l3dgw_port, lflow_ref, commit_all,
> +                                       features, true);
>   }
>   
>   static void
> @@ -18501,55 +18571,10 @@ build_lrouter_out_snat_flow(struct lflow_table 
> *lflows,
>       ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority, ds_cstr(match),
>                     ds_cstr(actions), lflow_ref, WITH_HINT(&nat->header_));
>   
> -    /* 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 &&
> -        !od->is_gw_router && !commit_all) {
> -        const char *zone;
> -        uint16_t prio_offset;
> -        if (nat_entry->type == SNAT) {
> -            /* Traffic to/from hosts behind SNAT is tracked through the
> -             * SNAT CT zone.*/
> -            zone = "snat";
> -            prio_offset = 0;
> -        } else {
> -            /* Traffic to/from hosts behind DNAT_AND_SNAT is tracked through
> -             * the DNAT CT zone with slightly higher priority flows.*/
> -            zone = "dnat";
> -            prio_offset = 5;
> -        }
> -
> -        /* For traffic that comes from the SNAT network, initiate CT state
> -         * from the correct zone, before entering S_ROUTER_OUT_SNAT to allow
> -         * matching on various CT states.*/
> -        ds_clear(actions);
> -        ds_put_format(actions, "ct_next(%s);", zone);
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_UNDNAT, 70 + prio_offset,
> -                      ds_cstr(match), ds_cstr(actions),
> -                      lflow_ref);
> -
> -        build_lrouter_out_snat_match(lflows, od, nat, match,
> -                                     distributed_nat, cidr_bits, is_v6,
> -                                     l3dgw_port, lflow_ref, true);
> -        size_t match_any_state_len = match->length;
> -        ds_put_cstr(match, " && (!ct.trk || !ct.rpl)");
> -        ds_clear(actions);
> -        ds_put_format(actions, "ct_%s;", zone);
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, priority + prio_offset,
> -                      ds_cstr(match), ds_cstr(actions),
> -                      lflow_ref);
> -
> -        /* New traffic that goes into the SNAT network is committed to the
> -         * correct CT zone to avoid SNAT-ing replies.*/
> -        ds_truncate(match, match_any_state_len);
> -        ds_put_cstr(match, " && ct.new");
> -        ds_clear(actions);
> -        ds_put_format(actions, "ct_commit_to_zone(%s);", zone);
> -        ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_SNAT,
> -                      priority + prio_offset, ds_cstr(match), 
> ds_cstr(actions),
> -                      lflow_ref);
> -    }
> +    build_lrouter_out_snat_track_flows(lflows, od, nat_entry, match, actions,
> +                                       distributed_nat, cidr_bits, is_v6,
> +                                       l3dgw_port, lflow_ref, commit_all,
> +                                       features, false);
>   }
>   
>   static void
> @@ -19072,7 +19097,8 @@ build_lrouter_nat_defrag_and_lb(
>                                                     nat_entry->is_distributed,
>                                                     nat_entry->mac, cidr_bits,
>                                                     is_v6, 
> nat_entry->l3dgw_port,
> -                                                  lflow_ref);
> +                                                  lflow_ref, commit_all,
> +                                                  features);
>           } else if (lrouter_use_common_zone(od)) {
>               build_lrouter_out_snat_in_czone_flow(lflows, od, nat_entry, 
> match,
>                                                    actions,
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index f8c144918..c41617fd4 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -1431,6 +1431,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows5 | 
> ovn_strip_lflows], [0], [dnl
>     table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
>     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), action=(ip4.src=172.16.1.2; next;)
> +  table=??(lr_out_snat        ), priority=166  , match=(ip && ip4.dst == 
> 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1") && ip4.src 
> == $allowed_range), action=(next;)
>   ])
>   
>   AT_CHECK([grep -e "lr_out_snat" crflows5 | ovn_strip_lflows], [0], [dnl
> @@ -1461,6 +1462,7 @@ AT_CHECK([grep -e "lr_out_snat" drflows6 | 
> ovn_strip_lflows], [0], [dnl
>     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")), 
> action=(ip4.src=172.16.1.2; next;)
>     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;)
> +  table=??(lr_out_snat        ), priority=166  , match=(ip && ip4.dst == 
> 50.0.0.11 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), 
> action=(next;)
>   ])
>   
>   AT_CHECK([grep -e "lr_out_snat" crflows6 | ovn_strip_lflows], [0], [dnl
> @@ -1474,6 +1476,72 @@ OVN_CLEANUP_NORTHD
>   AT_CLEANUP
>   ])
>   
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([Stateless dnat_and_snat inside SNAT network on distributed router])
> +ovn_start
> +
> +check ovn-sbctl chassis-add gw1 geneve 127.0.0.1 \
> +  -- set chassis gw1 other_config:ct-commit-to-zone="true" \
> +  -- set chassis gw1 other_config:ct-next-zone="true"
> +
> +check ovn-nbctl lr-add DR
> +check ovn-nbctl lrp-add DR DR-public 02:ac:10:01:00:01 172.16.1.1/24
> +check ovn-nbctl lrp-add DR DR-S1 02:ac:10:01:00:02 50.0.0.1/24
> +check ovn-nbctl lrp-set-gateway-chassis DR-public gw1
> +
> +check ovn-nbctl lr-nat-add DR snat 172.16.1.10 50.0.0.0/24
> +check ovn-nbctl --stateless lr-nat-add DR dnat_and_snat 172.16.1.2 50.0.0.11
> +check ovn-nbctl --wait=sb sync
> +
> +dnl Traffic to/from the stateless NAT logical IP must not be tracked by
> +dnl the flows of the SNAT covering its network.
> +ovn-sbctl dump-flows DR > drflows
> +AT_CAPTURE_FILE([drflows])
> +
> +AT_CHECK([grep -e "lr_out_post_undnat" drflows | 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 == 
> 50.0.0.0/24 && outport == "DR-public" && is_chassis_resident("cr-DR-public") 
> && (!ct.trk || !ct.rpl)), action=(ct_next(snat);)
> +  table=??(lr_out_post_undnat ), priority=75   , match=(ip && ip4.src == 
> 50.0.0.11 && outport == "DR-public" && is_chassis_resident("cr-DR-public")), 
> action=(next;)
> +])
> +
> +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=120  , match=(nd_ns), 
> action=(next;)
> +  table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.dst == 
> 50.0.0.0/24 && inport == "DR-public" && is_chassis_resident("cr-DR-public") 
> && (!ct.trk || !ct.rpl)), action=(ct_snat;)
> +  table=??(lr_out_snat        ), priority=153  , match=(ip && ip4.src == 
> 50.0.0.0/24 && outport == "DR-public" && is_chassis_resident("cr-DR-public") 
> && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.10);)
> +  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 
> 50.0.0.11 && outport == "DR-public" && is_chassis_resident("cr-DR-public")), 
> action=(ip4.src=172.16.1.2; next;)
> +  table=??(lr_out_snat        ), priority=166  , match=(ip && ip4.dst == 
> 50.0.0.11 && inport == "DR-public" && is_chassis_resident("cr-DR-public")), 
> action=(next;)
> +])
> +
> +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=153  , match=(ip && ip4.dst == 
> 50.0.0.0/24 && inport == "DR-public" && is_chassis_resident("cr-DR-public") 
> && ct.new), action=(ct_commit_to_zone(snat);)
> +])
> +
> +dnl With ct-commit-all the SNAT network is not tracked by these flows.
> +check ovn-nbctl --wait=sb set logical_router DR options:ct-commit-all="true"
> +ovn-sbctl dump-flows DR > drflows2
> +AT_CAPTURE_FILE([drflows2])
> +
> +AT_CHECK([grep -e "lr_out_post_undnat" drflows2 | 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_not_tracked == 1 && outport == "DR-public" && 
> is_chassis_resident("cr-DR-public")), action=(ct_next(snat);)
> +  table=??(lr_out_post_undnat ), priority=10   , match=(ip && 
> flags.unsnat_new == 1 && outport == "DR-public" && 
> is_chassis_resident("cr-DR-public")), action=(next;)
> +])
> +
> +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.trk || 
> !ct.rpl) && flags.unsnat_new == 1 && outport == "DR-public" && 
> is_chassis_resident("cr-DR-public")), action=(ct_commit_to_zone(snat);)
> +  table=??(lr_out_snat        ), priority=10   , match=(ip && ct.new && 
> outport == "DR-public" && is_chassis_resident("cr-DR-public")), 
> 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 == 
> 50.0.0.0/24 && outport == "DR-public" && is_chassis_resident("cr-DR-public") 
> && (!ct.trk || !ct.rpl)), action=(ct_snat(172.16.1.10);)
> +  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 
> 50.0.0.11 && outport == "DR-public" && is_chassis_resident("cr-DR-public")), 
> action=(ip4.src=172.16.1.2; next;)
> +])
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
>   OVN_FOR_EACH_NORTHD_NO_HV([
>   AT_SETUP([check Load balancer health check and Service Monitor sync])
>   ovn_start
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 13e62bf9b..d9c33b03a 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -3677,6 +3677,22 @@ NS_CHECK_CONNECTIVITY([alice1], [foo1], [192.168.1.2])
>   # North-South Direct (Bypassing SNAT): 'alice1' reaches 'bar1' using 
> 192.168.2.2
>   NS_CHECK_CONNECTIVITY([alice1], [bar1], [192.168.2.2])
>   
> +AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> +
> +# Stateless DNAT_AND_SNAT inside the SNAT network: its traffic must not be
> +# tracked by the flows of the SNAT covering the network.
> +check ovn-nbctl --wait=hv --stateless lr-nat-add R1 dnat_and_snat 172.16.1.5 
> 192.168.2.2
> +
> +# North-South stateless DNAT: 'alice1' reaches 'bar1' on 172.16.1.5.
> +NS_CHECK_CONNECTIVITY([alice1], [bar1], [172.16.1.5])
> +
> +# South-North stateless SNAT: 'bar1' reaches 'alice1' from 172.16.1.5.
> +NS_CHECK_CONNECTIVITY([bar1], [alice1], [172.16.1.2])
> +
> +AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(192.168.2.2) | \
> +sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
> +])
> +
>   # Try to ping external network
>   NETNS_START_TCPDUMP([ext-net], [-n -c 3 -i ext-veth dst 172.16.1.3 and 
> icmp], [ext-net])
>   AT_CHECK([ovn-nbctl lr-nat-del R1 snat])


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

Reply via email to