On 11/14/25 7:54 PM, Lucas Vargas Dias via dev wrote:
> FIN or FIN-ACK packets from client was dropping because the client
> side was using conntrack. Connection is in SYN_SENT state because the
> response from backends bypass the conntrack, when client sends a FIN
> or FIN-ACK, the conntrack is invalid and packet is dropped.
> To fix it, remove the client side from conntrack, calculating the hash
> from packet to choice the backend.
> REG_IDX_LB_STATELESS is used to store the index from backend.
> 
> Signed-off-by: Lucas Vargas Dias <[email protected]>
> ---

Hi Lucas,

Sorry for the very long delay in reviewing this patch.

It looks good to me aside for some very small comments I shared below.
In my opinion the implementation is way more clear now with these changes.

Therefore, I rebased the patch on the current main branch, fixed up the
nits below and then pushed it to main.

I guess we should backport this to 25.09 and 25.03.  Would you mind
sending backport patches if you have time?  Some of the refactors that
went in since on the main branch make a cherry pick impossible as is.

Regards,
Dumitru


>  northd/lb.c         |   3 +
>  northd/lb.h         |   1 +
>  northd/northd.c     | 298 +++++++++++++++++++++++++++++++-------------
>  tests/multinode.at  |  99 +++++++++------
>  tests/ovn-northd.at | 217 ++++++++++++++++++++++++++++++--
>  5 files changed, 481 insertions(+), 137 deletions(-)
> 
> diff --git a/northd/lb.c b/northd/lb.c
> index 919557ec4..2266d76e8 100644
> --- a/northd/lb.c
> +++ b/northd/lb.c
> @@ -358,6 +358,9 @@ ovn_northd_lb_init(struct ovn_northd_lb *lb,
>      }
>      lb->affinity_timeout = affinity_timeout;
>  
> +    lb->use_stateless_nat = smap_get_bool(&nbrec_lb->options,
> +                                          "use_stateless_nat", false);
> +
>      const char *snat_ip = smap_get(&nbrec_lb->options, "hairpin_snat_ip");
>  
>      if (snat_ip && validate_snap_ip_address(snat_ip)) {
> diff --git a/northd/lb.h b/northd/lb.h
> index 43a8a1850..53dc4abf0 100644
> --- a/northd/lb.h
> +++ b/northd/lb.h
> @@ -75,6 +75,7 @@ struct ovn_northd_lb {
>      bool health_checks;
>  
>      char *hairpin_snat_ip;
> +    bool use_stateless_nat;
>  };
>  
>  /* ovn-northd specific backend information. */
> diff --git a/northd/northd.c b/northd/northd.c
> index cdf12ec86..4defff1cc 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -157,6 +157,7 @@ static bool vxlan_mode;
>  #define REG_LB_PORT "reg2[0..15]"
>  #define REG_CT_TP_DST "reg1[0..15]"
>  #define REG_CT_PROTO "reg1[16..23]"
> +#define REG_IDX_LB_STATELESS "reg1[0..15]"
>  
>  /* Registers for ACL evaluation */
>  #define REGBIT_ACL_VERDICT_ALLOW "reg8[16]"
> @@ -12439,6 +12440,155 @@ lrouter_use_common_zone(const struct ovn_datapath 
> *od)
>      return !od->is_gw_router && use_common_zone;
>  }
>  
> +static void
> +build_lrouter_flows_for_lb_stateless(struct lrouter_nat_lb_flows_ctx *ctx,
> +                                     struct ovn_datapath *od,
> +                                     struct lflow_ref *lflow_ref,
> +                                     struct ovn_port *dgp,
> +                                     const char *meter)
> +{
> +    /* (NOTE) dnat_action: Add a new rule lr_in_dnat with backend IP
> +     * and port action to skip conntrack completely. It is based on
> +     * REG_IDX_LB_STATELESS which was calculated in lr_in_ct_extract.
> +     * So, if the packet has VIP IP destination and port
> +     * (if port was configured), it selecs a backend based on

Typo: selecs

> +     * REG_IDX_LB_STATELESS. It works to multi-chassis and avoid to
> +     * sync conntrack betweem them.

Typo: betweem

> +     */
> +    struct ds new_action_stateless_nat = DS_EMPTY_INITIALIZER;
> +    struct ds new_match_stateless_nat = DS_EMPTY_INITIALIZER;
> +    if (!vector_is_empty(&ctx->lb_vip->backends) ||
> +        !ctx->lb_vip->empty_backend_rej) {
> +        ds_put_format(&new_match_stateless_nat, "is_chassis_resident(%s)",
> +                      dgp->cr_port->json_key);
> +    }
> +
> +    bool ipv4 = ctx->lb_vip->address_family == AF_INET;
> +    const char *ip_match = ipv4 ? "ip4" : "ip6";
> +    ds_put_format(&new_match_stateless_nat, " && %s && %s.dst == %s",
> +                  ip_match, ip_match, ctx->lb_vip->vip_str);
> +    if (ctx->lb_vip->port_str) {
> +        ds_put_format(&new_match_stateless_nat,
> +                      " && %s && %s.dst == %s",
> +                      ctx->lb->proto, ctx->lb->proto,
> +                      ctx->lb_vip->port_str);
> +    }
> +
> +    const struct ovn_lb_backend *backend;
> +    if (vector_len(&ctx->lb_vip->backends) == 1) {
> +        backend = vector_get_ptr(&ctx->lb_vip->backends, 0);
> +        ds_put_format(&new_action_stateless_nat, "%s.dst = %s; ",
> +                      ip_match, backend->ip_str);
> +        if (ctx->lb_vip->port_str) {
> +            ds_put_format(&new_action_stateless_nat, "%s.dst = %s; ",
> +                          ctx->lb->proto, backend->port_str);
> +        }
> +        ds_put_format(&new_action_stateless_nat, "next;");
> +        ovn_lflow_add_with_hint__(ctx->lflows, od, S_ROUTER_IN_DNAT,
> +                                  ctx->prio,
> +                                  ds_cstr(&new_match_stateless_nat),
> +                                  ds_cstr(&new_action_stateless_nat),
> +                                  NULL, meter, &ctx->lb->nlb->header_,
> +                                  lflow_ref);
> +    }
> +    size_t match_len = new_match_stateless_nat.length;
> +    size_t i = 0;
> +    VECTOR_FOR_EACH_PTR (&ctx->lb_vip->backends, backend) {
> +        if (vector_len(&ctx->lb_vip->backends) <= 1) {
> +            break;
> +        }
> +        ds_put_format(&new_match_stateless_nat, " && "
> +            REG_IDX_LB_STATELESS" == ""%"PRIuSIZE, i++);
> +        ds_put_format(&new_action_stateless_nat, "%s.dst = %s; ",
> +                      ip_match, backend->ip_str);
> +        if (ctx->lb_vip->port_str) {
> +            ds_put_format(&new_action_stateless_nat, "%s.dst = %s; ",
> +                          ctx->lb->proto, backend->port_str);
> +        }
> +        ds_put_format(&new_action_stateless_nat, "next;");
> +        ovn_lflow_add_with_hint__(ctx->lflows, od, S_ROUTER_IN_DNAT,
> +                                  ctx->prio,
> +                                  ds_cstr(&new_match_stateless_nat),
> +                                  ds_cstr(&new_action_stateless_nat),
> +                                  NULL, meter, &ctx->lb->nlb->header_,
> +                                  lflow_ref);
> +        ds_clear(&new_action_stateless_nat);
> +        ds_truncate(&new_match_stateless_nat, match_len);
> +    }
> +
> +    ds_destroy(&new_match_stateless_nat);
> +    ds_destroy(&new_action_stateless_nat);
> +
> +    if (vector_is_empty(&ctx->lb_vip->backends)) {
> +        return;
> +    }
> +
> +    size_t undnat_match_len = ctx->undnat_match->length;
> +    struct ds snat_action = DS_EMPTY_INITIALIZER;
> +
> +
> +    /* We need to centralize the LB traffic to properly perform
> +     * the undnat stage.
> +     */
> +    ds_put_format(ctx->undnat_match, ") && outport == %s", dgp->json_key);
> +    ds_clear(ctx->gw_redir_action);
> +    ds_put_format(ctx->gw_redir_action, "outport = %s; next;",
> +                  dgp->cr_port->json_key);
> +
> +    ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_IN_GW_REDIRECT,
> +                            200, ds_cstr(ctx->undnat_match),
> +                            ds_cstr(ctx->gw_redir_action),
> +                            &ctx->lb->nlb->header_,
> +                            lflow_ref);
> +    ds_truncate(ctx->undnat_match, undnat_match_len);
> +
> +    ds_put_format(ctx->undnat_match, ") && (inport == %s || outport == %s)"
> +                                     " && is_chassis_resident(%s)",
> +                  dgp->json_key, dgp->json_key,
> +                  dgp->cr_port->json_key);
> +
> +    /* Use the LB protocol as matching criteria for out undnat and snat when
> +     * creating LBs with stateless NAT. */
> +    ds_put_format(ctx->undnat_match, " && %s", ctx->lb->proto);
> +
> +

Nit: one newline too many.

> +    /* undnat_action: Just follows the pipeline in the lr_out_undenat NAT 
> rule.
> +     */
> +    ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_OUT_UNDNAT, 120,
> +                            ds_cstr(ctx->undnat_match),
> +                            "next;", &ctx->lb->nlb->header_,
> +                            lflow_ref);
> +
> +    /* (NOTE) snat_action: Add a new rule lr_out_snat with LB VIP as source
> +     * IP action to perform stateless NAT pipeline completely when the
> +     * outgoing packet is redirected to a chassis that does not have an
> +     * active conntrack entry. Otherwise, it will not be SNATed by the
> +     * ct_lb action because it does not refer to a valid created flow. The
> +     * use case for responding to a packet in different chassis is multipath
> +     * via ECMP. So, the LB lr_out_snat is created with a lower priority than
> +     * the other router pipeline entries, in this case, if the packet is not
> +     * SNATed by ct_lb (conntrack lost), it will be SNATed by the LB
> +     * stateless NAT rule. Also, SNAT is performed only when the packet
> +     * matches the configured LB backend IPs, ports and protocols. Otherwise,
> +     * the packet will be forwarded without SNAted interference.

I think I'd say "without being SNATed" instead.

> +     */
> +    if (ctx->lb_vip->port_str) {
> +        ds_put_format(&snat_action, "%s.src = %s; %s.src = %s; next;",
> +                      ip_match, ctx->lb_vip->vip_str, ctx->lb->proto,
> +                      ctx->lb_vip->port_str);
> +    } else {
> +        ds_put_format(&snat_action, "%s.src = %s; next;",
> +                      ip_match, ctx->lb_vip->vip_str);
> +    }
> +    ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_OUT_SNAT, 160,
> +                            ds_cstr(ctx->undnat_match),
> +                            ds_cstr(&snat_action), &ctx->lb->nlb->header_,
> +                            lflow_ref);
> +
> +    ds_truncate(ctx->undnat_match, undnat_match_len);
> +    ds_destroy(&snat_action);
> +}
> +
>  static void
>  build_distr_lrouter_nat_flows_for_lb(struct lrouter_nat_lb_flows_ctx *ctx,
>                                       enum lrouter_nat_lb_flow_type type,
> @@ -12447,66 +12597,39 @@ build_distr_lrouter_nat_flows_for_lb(struct 
> lrouter_nat_lb_flows_ctx *ctx,
>                                       struct ovn_port *dgp,
>                                       bool stateless_nat)
>  {
> -    struct ds dnat_action = DS_EMPTY_INITIALIZER;
> -
>      /* Store the match lengths, so we can reuse the ds buffer. */
>      size_t new_match_len = ctx->new_match->length;
>      size_t undnat_match_len = ctx->undnat_match->length;
>  
> -    /* (NOTE) dnat_action: Add the first LB backend IP as a destination
> -     * action of the lr_in_dnat NAT rule. Including the backend IP is useful
> -     * for accepting packets coming from a chassis that does not have
> -     * previously established conntrack entries. This means that the actions
> -     * (ip4.dst + ct_lb_mark) are executed in addition and ip4.dst is not
> -     * useful when traffic passes through the same chassis for ingress/egress
> -     * packets. However, the actions are complementary in cases where traffic
> -     * enters from one chassis, the ack response comes from another chassis,
> -     * and the final ack step of the TCP handshake comes from the first
> -     * chassis used. Without using stateless NAT, the connection will not be
> -     * established because the return packet followed a path through another
> -     * chassis and only ct_lb_mark will not be able to receive the ack and
> -     * forward it to the right backend. With using stateless NAT, the packet
> -     * will be accepted and forwarded to the same backend that corresponds to
> -     * the previous conntrack entry that is in the SYN_SENT state
> -     * (created by ct_lb_mark for the first rcv packet in this flow).
> -     */
> -    if (stateless_nat) {
> -        if (!vector_is_empty(&ctx->lb_vip->backends)) {
> -            const struct ovn_lb_backend *backend =
> -                vector_get_ptr(&ctx->lb_vip->backends, 0);
> -            bool ipv6 = !IN6_IS_ADDR_V4MAPPED(&backend->ip);
> -            ds_put_format(&dnat_action, "%s.dst = %s; ", ipv6 ? "ip6" : 
> "ip4",
> -                          backend->ip_str);
> -        }
> -    }
> -    ds_put_format(&dnat_action, "%s", ctx->new_action[type]);
> -
>      const char *meter = NULL;
>  
>      if (ctx->reject) {
>          meter = copp_meter_get(COPP_REJECT, od->nbr->copp, 
> ctx->meter_groups);
>      }
>  
> +    if (stateless_nat) {
> +        return build_lrouter_flows_for_lb_stateless(ctx, od, lflow_ref,
> +                                                    dgp, meter);
> +    }
> +
>      if (!vector_is_empty(&ctx->lb_vip->backends) ||
>          !ctx->lb_vip->empty_backend_rej) {
>          ds_put_format(ctx->new_match, " && is_chassis_resident(%s)",
>                        dgp->cr_port->json_key);
>      }
>  
> -    ovn_lflow_add_with_hint__(ctx->lflows, od, S_ROUTER_IN_DNAT, ctx->prio,
> -                              ds_cstr(ctx->new_match), ds_cstr(&dnat_action),
> -                              NULL, meter, &ctx->lb->nlb->header_,
> -                              lflow_ref);
> +    ovn_lflow_add_with_hint__(ctx->lflows, od, S_ROUTER_IN_DNAT,
> +                              ctx->prio, ds_cstr(ctx->new_match),
> +                              ctx->new_action[type], NULL, meter,
> +                              &ctx->lb->nlb->header_, lflow_ref);
>  
>      ds_truncate(ctx->new_match, new_match_len);
>  
> -    ds_destroy(&dnat_action);
>      if (vector_is_empty(&ctx->lb_vip->backends)) {
>          return;
>      }
>  
>      struct ds undnat_action = DS_EMPTY_INITIALIZER;
> -    struct ds snat_action = DS_EMPTY_INITIALIZER;
>  
>      switch (type) {
>      case LROUTER_NAT_LB_FLOW_FORCE_SNAT:
> @@ -12523,13 +12646,6 @@ build_distr_lrouter_nat_flows_for_lb(struct 
> lrouter_nat_lb_flows_ctx *ctx,
>          break;
>      }
>  
> -    /* undnat_action: Remove the ct action from the lr_out_undenat NAT rule.
> -     */
> -    if (stateless_nat) {
> -        ds_clear(&undnat_action);
> -        ds_put_format(&undnat_action, "next;");
> -    }
> -
>      /* We need to centralize the LB traffic to properly perform
>       * the undnat stage.
>       */
> @@ -12546,53 +12662,16 @@ build_distr_lrouter_nat_flows_for_lb(struct 
> lrouter_nat_lb_flows_ctx *ctx,
>      ds_truncate(ctx->undnat_match, undnat_match_len);
>  
>      ds_put_format(ctx->undnat_match, ") && (inport == %s || outport == %s)"
> -                  " && is_chassis_resident(%s)", dgp->json_key, 
> dgp->json_key,
> -                  dgp->cr_port->json_key);
> -    /* Use the LB protocol as matching criteria for out undnat and snat when
> -     * creating LBs with stateless NAT. */
> -    if (stateless_nat) {
> -        ds_put_format(ctx->undnat_match, " && %s", ctx->lb->proto);
> -    }
> +                        " && is_chassis_resident(%s)",
> +                        dgp->json_key, dgp->json_key,
> +                        dgp->cr_port->json_key);
>      ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_OUT_UNDNAT, 120,
>                              ds_cstr(ctx->undnat_match),
>                              ds_cstr(&undnat_action), &ctx->lb->nlb->header_,
>                              lflow_ref);
>  
> -    /* (NOTE) snat_action: Add a new rule lr_out_snat with LB VIP as source
> -     * IP action to perform stateless NAT pipeline completely when the
> -     * outgoing packet is redirected to a chassis that does not have an
> -     * active conntrack entry. Otherwise, it will not be SNATed by the
> -     * ct_lb action because it does not refer to a valid created flow. The
> -     * use case for responding to a packet in different chassis is multipath
> -     * via ECMP. So, the LB lr_out_snat is created with a lower priority than
> -     * the other router pipeline entries, in this case, if the packet is not
> -     * SNATed by ct_lb (conntrack lost), it will be SNATed by the LB
> -     * stateless NAT rule. Also, SNAT is performed only when the packet
> -     * matches the configured LB backend IPs, ports and protocols. Otherwise,
> -     * the packet will be forwarded without SNAted interference.
> -     */
> -    if (stateless_nat) {
> -        if (ctx->lb_vip->port_str) {
> -            ds_put_format(&snat_action, "%s.src = %s; %s.src = %s; next;",
> -                          ctx->lb_vip->address_family == AF_INET6 ?
> -                          "ip6" : "ip4",
> -                          ctx->lb_vip->vip_str, ctx->lb->proto,
> -                          ctx->lb_vip->port_str);
> -        } else {
> -            ds_put_format(&snat_action, "%s.src = %s; next;",
> -                          ctx->lb_vip->address_family == AF_INET6 ?
> -                          "ip6" : "ip4",
> -                          ctx->lb_vip->vip_str);
> -        }
> -        ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_OUT_SNAT, 160,
> -                                ds_cstr(ctx->undnat_match),
> -                                ds_cstr(&snat_action), 
> &ctx->lb->nlb->header_,
> -                                lflow_ref);
> -    }
> -
>      ds_truncate(ctx->undnat_match, undnat_match_len);
>      ds_destroy(&undnat_action);
> -    ds_destroy(&snat_action);
>  }
>  
>  static void
> @@ -12749,8 +12828,7 @@ build_lrouter_nat_flows_for_lb(
>       * lflow generation for them.
>       */
>      size_t index;
> -    bool use_stateless_nat = smap_get_bool(&lb->nlb->options,
> -                                           "use_stateless_nat", false);
> +    bool use_stateless_nat = lb->use_stateless_nat;
>      DYNAMIC_BITMAP_FOR_EACH_1 (index, &lb_dps->nb_lr_map) {
>          struct ovn_datapath *od = vector_get(&lr_datapaths->dps, index,
>                                               struct ovn_datapath *);
> @@ -12883,13 +12961,45 @@ build_lswitch_flows_for_lb(struct ovn_lb_datapaths 
> *lb_dps,
>      build_lb_rules_for_stateless_acl(lflows, lb_dps);
>  }
>  
> +static void
> +build_lrouter_defrag_flows_for_lb_stateless(const struct ovn_northd_lb *lb,
> +                                            const struct ovn_lb_vip *lb_vip,
> +                                            struct ds *action)
> +{
> +    ovs_assert(lb && lb_vip && action);
> +
> +    if (vector_len(&lb_vip->backends) > 1) {
> +        ds_put_format(action, REG_IDX_LB_STATELESS" = select(");
> +        if (lb->selection_fields) {
> +            ds_put_format(action, "values=(");
> +        }
> +        for (size_t idx_backend = 0; idx_backend <
> +             vector_len(&lb_vip->backends);
> +             idx_backend++) {
> +            ds_put_format(action, "%"PRIuSIZE",", idx_backend);
> +        }
> +        ds_truncate(action, action->length - 1);
> +        if (lb->selection_fields) {
> +            ds_put_format(action, "); hash_fields=\"%s\"",
> +                          lb->selection_fields);
> +        }
> +        ds_put_format(action,");");

Nit: missing space after "action,".

> +    } else {
> +        ds_put_format(action, "next;");
> +    }
> +}
> +
>  /* If there are any load balancing rules, we should send the packet to
> - * conntrack for defragmentation and tracking.  This helps with two things.
> + * conntrack for defragmentation and tracking, unless LB is stateless.
> + * This helps with two things.
>   *
>   * 1. With tracking, we can send only new connections to pick a DNAT ip 
> address
>   *    from a group.
>   * 2. If there are L4 ports in load balancing rules, we need the
>   *    defragmentation to match on L4 ports.
> + *
> + * If load balancer is stateless, conntrack must not be used.
> + * Here, basicaly, it's calculated a hash to select the backend.

Nit: I'd rephrase it to "A hash is calculated then to select the backend.".

>   */
>  static void
>  build_lrouter_defrag_flows_for_lb(struct ovn_lb_datapaths *lb_dps,
> @@ -12900,21 +13010,31 @@ build_lrouter_defrag_flows_for_lb(struct 
> ovn_lb_datapaths *lb_dps,
>      if (dynamic_bitmap_is_empty(&lb_dps->nb_lr_map)) {
>          return;
>      }
> +    struct ds action = DS_EMPTY_INITIALIZER;
>  
>      for (size_t i = 0; i < lb_dps->lb->n_vips; i++) {
>          struct ovn_lb_vip *lb_vip = &lb_dps->lb->vips[i];
>          bool ipv6 = lb_vip->address_family == AF_INET6;
>          int prio = 100;
> -
> +        enum ovn_stage stage = S_ROUTER_IN_DEFRAG;
>          ds_clear(match);
>          ds_put_format(match, "ip && ip%c.dst == %s", ipv6 ? '6' : '4',
>                        lb_vip->vip_str);
> -
> +        if (lb_dps->lb->use_stateless_nat) {
> +            stage = S_ROUTER_IN_CT_EXTRACT;
> +            prio = 120;
> +            build_lrouter_defrag_flows_for_lb_stateless(lb_dps->lb, lb_vip,
> +                                                        &action);
> +        } else {

Nit: I'd move "stage = S_ROUTER_IN_DEFRAG" here to make it more explicit.

> +            ds_put_format(&action, "ct_dnat;");
> +        }
>          ovn_lflow_add_with_dp_group(
>              lflows, lb_dps->nb_lr_map.map, ods_size(lr_datapaths),
> -            S_ROUTER_IN_DEFRAG, prio, ds_cstr(match), "ct_dnat;",
> +            stage, prio, ds_cstr(match), ds_cstr(&action),
>              &lb_dps->lb->nlb->header_, lb_dps->lflow_ref);
> +        ds_clear(&action);
>      }
> +    ds_destroy(&action);
>  }
>  
>  static void
> diff --git a/tests/multinode.at b/tests/multinode.at
> index 2f74487c8..5b001994d 100644
> --- a/tests/multinode.at
> +++ b/tests/multinode.at
> @@ -1514,6 +1514,7 @@ check multinode_nbctl ls-lb-add sw0 lb0
>  
>  # Set use_stateless_nat to true
>  check multinode_nbctl set load_balancer lb0 options:use_stateless_nat=true
> +check multinode_nbctl set load_balancer lb0 
> selection_fields="ip_src,tp_src,ip_dst,tp_dst"
>  
>  # Start backend http services
>  M_NS_DAEMONIZE([ovn-chassis-1], [sw0p1], [python3 -m http.server --bind 
> 10.0.1.3 80 >/dev/null 2>&1], [http1.pid])
> @@ -1545,14 +1546,12 @@ m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
>  M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v 172.16.0.100:80 
> --retry 0 --connect-timeout 1 --max-time 1 --local-port 59001'])
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | 
> M_FORMAT_CT(20.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
> -tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59001,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59001),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  
> tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59001,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59001),zone=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
>  M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v 172.16.0.100:80 
> --retry 0 --connect-timeout 1 --max-time 1 --local-port 59000'])
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | 
> M_FORMAT_CT(30.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
> -tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59000,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59000),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  
> tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59000,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59000),zone=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
> @@ -1583,7 +1582,6 @@ Connected to 172.16.0.100 (172.16.0.100) port 80
>  # Check if we have only one backend for the same connection - orig + dest 
> ports
>  OVS_WAIT_FOR_OUTPUT([echo -e $gw1_ct | M_FORMAT_CT(20.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
> -tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59004,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59004),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  
> tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59004,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59004),zone=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
> @@ -1649,7 +1647,6 @@ Connected to 172.16.0.100 (172.16.0.100) port 80
>  # Check if we have only one backend for the same connection - orig + dest 
> ports
>  OVS_WAIT_FOR_OUTPUT([echo -e $gw1_ct | M_FORMAT_CT(20.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
> -tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59005,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59005),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  
> tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59005,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59005),zone=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
> @@ -1705,7 +1702,6 @@ Connected to 172.16.0.100 (172.16.0.100) port 80
>  # Check if we have only one backend for the same connection - orig + dest 
> ports
>  OVS_WAIT_FOR_OUTPUT([echo -e $gw2_ct | M_FORMAT_CT(30.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
> -tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59006,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59006),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  
> tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59006,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59006),zone=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
> @@ -1771,7 +1767,6 @@ Connected to 172.16.0.100 (172.16.0.100) port 80
>  # Check if we have only one backend for the same connection - orig + dest 
> ports
>  OVS_WAIT_FOR_OUTPUT([echo -e $gw2_ct | M_FORMAT_CT(30.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
> -tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59007,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59007),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  
> tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59007,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59007),zone=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
> @@ -1858,7 +1853,6 @@ M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v 
> -O 172.16.0.100:9000/down
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | 
> M_FORMAT_CT(20.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
>  
> tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59008,dport=80),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59008),zone=<cleared>,protoinfo=(state=<cleared>)
> -tcp,orig=(src=20.0.1.3,dst=<cleared>,sport=59008,dport=9000),reply=(src=<cleared>,dst=20.0.1.3,sport=80,dport=59008),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat -v curl.out | 
> M_FORMAT_CURL([172.16.0.100], [9000])], [0], [dnl
> @@ -1874,7 +1868,6 @@ M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v 
> -O 172.16.0.100:9000/down
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | 
> M_FORMAT_CT(30.0.1.3) | \
>  grep tcp | sed -E -e 's/10.0.1.3|10.0.1.4/<cleared>/g' | sort], [0], [dnl
>  
> tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59008,dport=80),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59008),zone=<cleared>,protoinfo=(state=<cleared>)
> -tcp,orig=(src=30.0.1.3,dst=<cleared>,sport=59008,dport=9000),reply=(src=<cleared>,dst=30.0.1.3,sport=80,dport=59008),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
>  ])
>  
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat -v curl.out | 
> M_FORMAT_CURL([172.16.0.100], [9000])], [0], [dnl
> @@ -1907,14 +1900,14 @@ done
>  #                                                   |
>  #                     
> +.............................|.............................+
>  #                     |                                                      
>      |
> -#      DGP publicp3 (ovn-gw-3) (20.0.2.3/24)                     DGP 
> publicp4 (ovn-gw-4) (20.0.2.4/24)
> +#      DGP publicp3 (ovn-gw-3) (20.0.1.2/24)                     DGP 
> publicp4 (ovn-gw-4) (20.0.2.4/24)
>  #                     |                                                      
>      |
>  #                     
> +.............................+.............................+
> -#                                                   |
> -#                                                   | (overlay)
> +#                     |                                                      
>      |
> +#                     | (public_right)                               
> (public_left)|
>  #                     
> +.............................+.............................+
>  #                     |                                                      
>      |
> -#      DGP public1 (ovn-gw-1) (20.0.2.1/24)                      DGP public2 
> (ovn-gw-2) (20.0.2.2/24)
> +#      DGP public1 (ovn-gw-1) (20.0.1.1/24)                      DGP public2 
> (ovn-gw-2) (20.0.2.2/24)
>  #                     |                                                      
>      |
>  #                     
> +.............................+.............................+
>  #                                                   |
> @@ -1968,14 +1961,15 @@ check multinode_nbctl lrp-add lr0 lr0-sw0 
> 00:00:00:00:ff:01 10.0.2.1/24 1000::a/
>  check multinode_nbctl lsp-add-router-port sw0 sw0-lr0 lr0-sw0
>  
>  # create external connection for N/S traffic using multiple DGPs
> -check multinode_nbctl ls-add public
> +check multinode_nbctl ls-add public_right
> +check multinode_nbctl ls-add public_left
>  
>  # create external connection for N/S traffic
>  # DGP public1
> -check multinode_nbctl lsp-add-localnet-port public ln-public-1 public1
> +check multinode_nbctl lsp-add-localnet-port public_right ln-public-1 public1
>  
>  # DGP public2
> -check multinode_nbctl lsp-add-localnet-port public ln-public-2 public2
> +check multinode_nbctl lsp-add-localnet-port public_left ln-public-2 public2
>  
>  # Attach DGP public1 to GW-1 public1 (overlay connectivity)
>  m_as ovn-gw-1 ovs-vsctl set open . 
> external-ids:ovn-bridge-mappings=public1:br-ex
> @@ -1983,19 +1977,19 @@ m_as ovn-gw-1 ovs-vsctl set open . 
> external-ids:ovn-bridge-mappings=public1:br-e
>  # Attach DGP public2 to GW-2 public2 (overlay connectivity)
>  m_as ovn-gw-2 ovs-vsctl set open . 
> external-ids:ovn-bridge-mappings=public2:br-ex
>  
> -check multinode_nbctl lrp-add lr0 lr0-public-p1 40:54:00:00:00:01 
> 20.0.2.1/24 2000::1/64
> -check multinode_nbctl lsp-add-router-port public public-lr0-p1 lr0-public-p1
> +check multinode_nbctl lrp-add lr0 lr0-public-p1 40:54:00:00:00:01 
> 20.0.1.1/24 2000::1/64
> +check multinode_nbctl lsp-add-router-port public_right public-lr0-p1 
> lr0-public-p1
>  check multinode_nbctl lrp-set-gateway-chassis lr0-public-p1 ovn-gw-1 10
>  
>  m_wait_for_ports_up
>  
> -M_NS_CHECK_EXEC([ovn-chassis-1], [sw0p1], [ping -q -c 3 -i 0.3 -w 2 20.0.2.1 
> | FORMAT_PING], \
> +M_NS_CHECK_EXEC([ovn-chassis-1], [sw0p1], [ping -q -c 3 -i 0.3 -w 2 20.0.1.1 
> | FORMAT_PING], \
>  [0], [dnl
>  3 packets transmitted, 3 received, 0% packet loss, time 0ms
>  ])
>  
>  check multinode_nbctl lrp-add lr0 lr0-public-p2 40:54:00:00:00:02 
> 20.0.2.2/24 2000::2/64
> -check multinode_nbctl lsp-add-router-port public public-lr0-p2 lr0-public-p2
> +check multinode_nbctl lsp-add-router-port public_left public-lr0-p2 
> lr0-public-p2
>  check multinode_nbctl lrp-set-gateway-chassis lr0-public-p2 ovn-gw-2 10
>  
>  M_NS_CHECK_EXEC([ovn-chassis-2], [sw0p2], [ping -q -c 3 -i 0.3 -w 2 20.0.2.2 
> | FORMAT_PING], \
> @@ -2010,13 +2004,13 @@ check multinode_nbctl lsp-add-router-port sw1 sw1-lr1 
> lr1-sw1
>  
>  # create external connection for N/S traffic
>  # DGP public3
> -check multinode_nbctl lsp-add-localnet-port public ln-public-3 public3
> +check multinode_nbctl lsp-add-localnet-port public_left ln-public-3 public3
>  
>  # Attach DGP public3 to GW-3 public3 (overlay connectivity)
>  m_as ovn-gw-3 ovs-vsctl set open . 
> external-ids:ovn-bridge-mappings=public3:br-ex
>  
> -check multinode_nbctl lrp-add lr1 lr1-public-p3 40:54:00:00:00:03 
> 20.0.2.3/24 2000::3/64
> -check multinode_nbctl lsp-add-router-port public public-lr1-p3 lr1-public-p3
> +check multinode_nbctl lrp-add lr1 lr1-public-p3 40:54:00:00:00:03 
> 20.0.1.2/24 2000::3/64
> +check multinode_nbctl lsp-add-router-port public_left public-lr1-p3 
> lr1-public-p3
>  check multinode_nbctl lrp-set-gateway-chassis lr1-public-p3 ovn-gw-3 10
>  
>  M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 -i 0.3 -w 2 40.0.2.1 
> | FORMAT_PING], \
> @@ -2024,21 +2018,16 @@ M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 
> 3 -i 0.3 -w 2 40.0.2.1 | F
>  3 packets transmitted, 3 received, 0% packet loss, time 0ms
>  ])
>  
> -M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 -i 0.3 -w 2 20.0.2.3 
> | FORMAT_PING], \
> +M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 -i 0.3 -w 2 20.0.1.2 
> | FORMAT_PING], \
>  [0], [dnl
>  3 packets transmitted, 3 received, 0% packet loss, time 0ms
>  ])
>  
>  # Add a default route for multiple DGPs using ECMP - first step
> -check multinode_nbctl --ecmp lr-route-add lr0 0.0.0.0/0 20.0.2.3
> -check multinode_nbctl --ecmp lr-route-add lr1 0.0.0.0/0 20.0.2.1
> -
> -# Add SNAT rules using gateway-port
> -check multinode_nbctl --gateway-port lr0-public-p1 lr-nat-add lr0 snat 
> 20.0.2.1 10.0.2.0/24
> -check multinode_nbctl --gateway-port lr0-public-p2 lr-nat-add lr0 snat 
> 20.0.2.2 10.0.2.0/24
> -check multinode_nbctl --gateway-port lr1-public-p3 lr-nat-add lr1 snat 
> 20.0.2.3 40.0.2.0/24
> +check multinode_nbctl --ecmp lr-route-add lr0 0.0.0.0/0 20.0.1.2
> +check multinode_nbctl --ecmp lr-route-add lr1 0.0.0.0/0 20.0.1.1
>  
> -M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 -i 0.3 -w 2 20.0.2.1 
> | FORMAT_PING], \
> +M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 -i 0.3 -w 2 20.0.1.1 
> | FORMAT_PING], \
>  [0], [dnl
>  3 packets transmitted, 3 received, 0% packet loss, time 0ms
>  ])
> @@ -2050,13 +2039,13 @@ M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 
> 3 -i 0.3 -w 2 20.0.2.2 | F
>  
>  # Configure the second DGP for the lr1
>  # DGP public4
> -check multinode_nbctl lsp-add-localnet-port public ln-public-4 public4
> +check multinode_nbctl lsp-add-localnet-port public_right ln-public-4 public4
>  
>  # Attach DGP public4 to GW-2 public4 (overlay connectivity)
>  m_as ovn-gw-4 ovs-vsctl set open . 
> external-ids:ovn-bridge-mappings=public4:br-ex
>  
>  check multinode_nbctl lrp-add lr1 lr1-public-p4 40:54:00:00:00:04 
> 20.0.2.4/24 2000::4/64
> -check multinode_nbctl lsp-add-router-port public public-lr1-p4 lr1-public-p4
> +check multinode_nbctl lsp-add-router-port public_right public-lr1-p4 
> lr1-public-p4
>  check multinode_nbctl lrp-set-gateway-chassis lr1-public-p4 ovn-gw-4 10
>  
>  M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 -i 0.3 -w 2 20.0.2.4 
> | FORMAT_PING], \
> @@ -2064,9 +2053,6 @@ M_NS_CHECK_EXEC([ovn-chassis-3], [sw1p1], [ping -q -c 3 
> -i 0.3 -w 2 20.0.2.4 | F
>  3 packets transmitted, 3 received, 0% packet loss, time 0ms
>  ])
>  
> -# Add SNAT rules using gateway-port
> -check multinode_nbctl --gateway-port lr1-public-p4 lr-nat-add lr1 snat 
> 20.0.2.4 40.0.2.0/24
> -
>  # Add a default route for multiple DGPs using ECMP - second step (multipath)
>  check multinode_nbctl --ecmp lr-route-add lr0 0.0.0.0/0 20.0.2.4
>  check multinode_nbctl --ecmp lr-route-add lr1 0.0.0.0/0 20.0.2.2
> @@ -2200,36 +2186,77 @@ fi
>  # Set use_stateless_nat to true
>  # Now, if the traffic passes through both gateways (GW-1 and GW-2) it will 
> be forwarded successfully
>  check multinode_nbctl set load_balancer lb0 options:use_stateless_nat=true
> +check multinode_nbctl --wait=sb set load_balancer lb0 
> selection_fields="ip_src,tp_src,ip_dst,tp_dst"
>  
>  # Check the flows again for the LB VIP - always needs to be successful 
> regardless of the datapath (one or two gw chassis)
>  M_NS_EXEC([ovn-chassis-3], [sw1p1], [sh -c 'curl -v -O 
> 172.16.0.100:80/download_file --retry 0 --connect-timeout 1 --max-time 1 
> 2>curl.out'])
> +M_NS_EXEC([ovn-chassis-1], [sw0p1], [sh -c 'ss -nn >connections.out'])
> +M_NS_EXEC([ovn-chassis-2], [sw0p2], [sh -c 'ss -nn >connections.out'])
>  
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat -v curl.out | 
> M_FORMAT_CURL([172.16.0.100], [80])], [0], [dnl
>  Connected to 172.16.0.100 (172.16.0.100) port 80
>  200 OK
>  ])
>  
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-1 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-2 cat connections.out | grep  
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
>  M_NS_EXEC([ovn-chassis-3], [sw1p1], [sh -c 'curl -v -O 
> 172.16.0.100:80/download_file --retry 0 --connect-timeout 1 --max-time 1 
> 2>curl.out'])
> +M_NS_EXEC([ovn-chassis-1], [sw0p1], [sh -c 'ss -nn >connections.out'])
> +M_NS_EXEC([ovn-chassis-2], [sw0p2], [sh -c 'ss -nn >connections.out'])
>  
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat -v curl.out | 
> M_FORMAT_CURL([172.16.0.100], [80])], [0], [dnl
>  Connected to 172.16.0.100 (172.16.0.100) port 80
>  200 OK
>  ])
>  
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-1 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-2 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
>  M_NS_EXEC([ovn-chassis-3], [sw1p1], [sh -c 'curl -v -O 
> 172.16.0.100:80/download_file --retry 0 --connect-timeout 1 --max-time 1 
> 2>curl.out'])
> +M_NS_EXEC([ovn-chassis-1], [sw0p1], [sh -c 'ss -nn >connections.out'])
> +M_NS_EXEC([ovn-chassis-2], [sw0p2], [sh -c 'ss -nn >connections.out'])
>  
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat -v curl.out | 
> M_FORMAT_CURL([172.16.0.100], [80])], [0], [dnl
>  Connected to 172.16.0.100 (172.16.0.100) port 80
>  200 OK
>  ])
>  
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-1 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-2 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
>  M_NS_EXEC([ovn-chassis-3], [sw1p1], [sh -c 'curl -v -O 
> 172.16.0.100:80/download_file --retry 0 --connect-timeout 1 --max-time 1 
> 2>curl.out'])
> +M_NS_EXEC([ovn-chassis-1], [sw0p1], [sh -c 'ss -nn >connections.out'])
> +M_NS_EXEC([ovn-chassis-2], [sw0p2], [sh -c 'ss -nn >connections.out'])
>  
>  OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat -v curl.out | 
> M_FORMAT_CURL([172.16.0.100], [80])], [0], [dnl
>  Connected to 172.16.0.100 (172.16.0.100) port 80
>  200 OK
>  ])
>  
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-1 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
> +OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-2 cat connections.out | grep 
> "FIN-WAIT-2" | wc -l], [0], [dnl
> +0
> +])
> +
>  # Direct backend traffic using the same LB ports needs to be dropped
>  M_NS_EXEC([ovn-chassis-3], [sw1p1], [sh -c 'curl -v -O 
> 10.0.2.3:80/download_file --retry 0 --connect-timeout 1 --max-time 1 
> 2>curl.out'])
>  
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 448bc66ae..1551df17f 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -14300,14 +14300,66 @@ ovn-sbctl dump-flows lr1 > lr1flows
>  AT_CAPTURE_FILE([lr1flows])
>  
>  # Check stateless NAT rules for load balancer with multiple DGP
> -# 1. Check if the backend IPs are in the ipX.dst action
> +# 1. Check if the reg1[0..15] will select one of backends

Nit: missing period at the end of the sentence (in multiple places in
these test changes).

> +AT_CHECK([grep "lr_in_ct_extract" lr1flows | ovn_strip_lflows | grep 
> "30.0.0.1"], [0], [dnl
> +  table=??(lr_in_ct_extract   ), priority=120  , match=(ip && ip4.dst == 
> 30.0.0.1), action=(reg1[[0..15]] = select(0,1,2);)
> +])
> +
> +# 2. Check if the backend IPs are in the ipX.dst action
>  AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep "30.0.0.1"], 
> [0], [dnl
> -  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 30.0.0.1 && is_chassis_resident("cr-lr1-ts1")), 
> action=(ip4.dst = 172.16.0.103; 
> ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
> -  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 30.0.0.1 && is_chassis_resident("cr-lr1-ts2")), 
> action=(ip4.dst = 172.16.0.103; 
> ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
> -  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 30.0.0.1 && is_chassis_resident("cr-lr1_public")), 
> action=(ip4.dst = 172.16.0.103; 
> ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 0), action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 1), action=(ip4.dst = 172.16.0.102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 2), action=(ip4.dst = 172.16.0.101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 0), action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 1), action=(ip4.dst = 172.16.0.102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 2), action=(ip4.dst = 172.16.0.101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 0), action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 1), action=(ip4.dst = 172.16.0.102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 2), action=(ip4.dst = 172.16.0.101; next;)
> +])
> +
> +# 3. Check if the DGP ports are in the match with action next
> +AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1_public" || outport == "lr1_public") && 
> is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
>  ])
>  
> -# 2. Check if the DGP ports are in the match with action next
> +# 4. Check if the VIP IP is in the ipX.src action
> +AT_CHECK([grep "lr_out_snat" lr1flows | 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=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(ip4.src = 30.0.0.1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(ip4.src = 30.0.0.1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1_public" || outport == "lr1_public") && 
> is_chassis_resident("cr-lr1_public") && tcp), action=(ip4.src = 30.0.0.1; 
> next;)
> +])
> +
> +# Set selection fields
> +check ovn-nbctl --wait=sb set load_balancer lb1 
> selection_fields="ip_src,tp_src,ip_dst,tp_dst"
> +
> +ovn-sbctl dump-flows lr1 > lr1flows
> +AT_CAPTURE_FILE([lr1flows])
> +
> +# 1. Check if the reg1[0..15] will select one of backends using hash_fields
> +AT_CHECK([grep "lr_in_ct_extract" lr1flows | ovn_strip_lflows | grep 
> "30.0.0.1"], [0], [dnl
> +  table=??(lr_in_ct_extract   ), priority=120  , match=(ip && ip4.dst == 
> 30.0.0.1), action=(reg1[[0..15]] = select(values=(0,1,2); 
> hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
> +])
> +
> +# 2. Check if the backend IPs are in the ipX.dst action
> +AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep "30.0.0.1"], 
> [0], [dnl
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 0), action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 1), action=(ip4.dst = 172.16.0.102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 2), action=(ip4.dst = 172.16.0.101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 0), action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 1), action=(ip4.dst = 172.16.0.102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 2), action=(ip4.dst = 172.16.0.101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 0), action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 1), action=(ip4.dst = 172.16.0.102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1 && 
> reg1[[0..15]] == 2), action=(ip4.dst = 172.16.0.101; next;)
> +])
> +
> +# 3. Check if the DGP ports are in the match with action next
>  AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
>    table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
> @@ -14315,7 +14367,7 @@ AT_CHECK([grep "lr_out_undnat" lr1flows | 
> ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1_public" || outport == "lr1_public") && 
> is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
>  ])
>  
> -# 3. Check if the VIP IP is in the ipX.src action
> +# 4. Check if the VIP IP is in the ipX.src action
>  AT_CHECK([grep "lr_out_snat" lr1flows | 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;)
> @@ -14324,6 +14376,50 @@ AT_CHECK([grep "lr_out_snat" lr1flows | 
> ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
> (inport == "lr1_public" || outport == "lr1_public") && 
> is_chassis_resident("cr-lr1_public") && tcp), action=(ip4.src = 30.0.0.1; 
> next;)
>  ])
>  
> +# Delete LB and create with one backend
> +check ovn-nbctl --wait=sb lb-del lb1
> +check ovn-nbctl --wait=sb lb-add lb1 "30.0.0.1" "172.16.0.103"
> +
> +# Set use_stateless_nat to true
> +check ovn-nbctl --wait=sb set load_balancer lb1 
> options:use_stateless_nat=true
> +
> +# Associate load balancer to s1
> +check ovn-nbctl ls-lb-add s1 lb1
> +check ovn-nbctl lr-lb-add lr1 lb1
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl dump-flows lr1 > lr1flows
> +AT_CAPTURE_FILE([lr1flows])
> +
> +# 1. Check if the reg1[0..15] will select one of backends using hash_fields
> +AT_CHECK([grep "lr_in_ct_extract" lr1flows | ovn_strip_lflows | grep 
> "30.0.0.1"], [0], [dnl
> +  table=??(lr_in_ct_extract   ), priority=120  , match=(ip && ip4.dst == 
> 30.0.0.1), action=(next;)
> +])
> +
> +# 2. Check if the backend IPs are in the ipX.dst action
> +AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep "30.0.0.1"], 
> [0], [dnl
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip4 && ip4.dst == 30.0.0.1), 
> action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip4 && ip4.dst == 30.0.0.1), 
> action=(ip4.dst = 172.16.0.103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip4 && ip4.dst == 30.0.0.1), 
> action=(ip4.dst = 172.16.0.103; next;)
> +])
> +
> +# 3. Check if the DGP ports are in the match with action next
> +AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 172.16.0.103)) && (inport == "lr1_public" || outport == "lr1_public") && 
> is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
> +])
> +
> +# 4. Check if the VIP IP is in the ipX.src action
> +AT_CHECK([grep "lr_out_snat" lr1flows | 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=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(ip4.src = 30.0.0.1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(ip4.src = 30.0.0.1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
> 172.16.0.103)) && (inport == "lr1_public" || outport == "lr1_public") && 
> is_chassis_resident("cr-lr1_public") && tcp), action=(ip4.src = 30.0.0.1; 
> next;)
> +])
> +
>  AT_CLEANUP
>  ])
>  
> @@ -14405,14 +14501,67 @@ ovn-sbctl dump-flows lr1 > lr1flows
>  AT_CAPTURE_FILE([lr1flows])
>  
>  # Check stateless NAT rules for load balancer with multiple DGP
> -# 1. Check if the backend IPs are in the ipX.dst action
> +# 1. Check if the reg1[0..15] will select one of backends
> +AT_CHECK([grep "lr_in_ct_extract" lr1flows | ovn_strip_lflows | grep 
> "2001:db8:cccc::1"], [0], [dnl
> +  table=??(lr_in_ct_extract   ), priority=120  , match=(ip && ip6.dst == 
> 2001:db8:cccc::1), action=(reg1[[0..15]] = select(0,1,2);)
> +])
> +
> +# 2. Check if the backend IPs are in the ipX.dst action
>  AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep 
> "2001:db8:cccc::1"], [0], [dnl
> -  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
> ip6 && ip6.dst == 2001:db8:cccc::1 && is_chassis_resident("cr-lr1-ts1")), 
> action=(ip6.dst = 2001:db8:aaaa:3::103; 
> ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
> -  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
> ip6 && ip6.dst == 2001:db8:cccc::1 && is_chassis_resident("cr-lr1-ts2")), 
> action=(ip6.dst = 2001:db8:aaaa:3::103; 
> ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
> -  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
> ip6 && ip6.dst == 2001:db8:cccc::1 && is_chassis_resident("cr-lr1_public")), 
> action=(ip6.dst = 2001:db8:aaaa:3::103; 
> ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 0), action=(ip6.dst = 
> 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 1), action=(ip6.dst = 
> 2001:db8:aaaa:3::102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 2), action=(ip6.dst = 
> 2001:db8:aaaa:3::101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 0), action=(ip6.dst = 
> 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 1), action=(ip6.dst = 
> 2001:db8:aaaa:3::102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 2), action=(ip6.dst = 
> 2001:db8:aaaa:3::101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 0), action=(ip6.dst = 
> 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 1), action=(ip6.dst = 
> 2001:db8:aaaa:3::102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 2), action=(ip6.dst = 
> 2001:db8:aaaa:3::101; next;)
> +])
> +
> +# 3. Check if the DGP ports are in the match with action next
> +AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1_public" || outport == "lr1_public") 
> && is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
>  ])
>  
> -# 2. Check if the DGP ports are in the match with action next
> +# 4. Check if the VIP IP is in the ipX.src action
> +AT_CHECK([grep "lr_out_snat" lr1flows | 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=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1_public" || outport == "lr1_public") 
> && is_chassis_resident("cr-lr1_public") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
> +])
> +
> +# Set selection fields
> +check ovn-nbctl --wait=sb set load_balancer lb1 
> selection_fields="ip_src,tp_src,ip_dst,tp_dst"
> +
> +ovn-sbctl dump-flows lr1 > lr1flows
> +AT_CAPTURE_FILE([lr1flows])
> +
> +# Check stateless NAT rules for load balancer with multiple DGP
> +# 1. Check if the reg1[0..15] will select one of backends
> +AT_CHECK([grep "lr_in_ct_extract" lr1flows | ovn_strip_lflows | grep 
> "2001:db8:cccc::1"], [0], [dnl
> +  table=??(lr_in_ct_extract   ), priority=120  , match=(ip && ip6.dst == 
> 2001:db8:cccc::1), action=(reg1[[0..15]] = select(values=(0,1,2); 
> hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
> +])
> +
> +# 2. Check if the backend IPs are in the ipX.dst action
> +AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep 
> "2001:db8:cccc::1"], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 0), action=(ip6.dst = 
> 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 1), action=(ip6.dst = 
> 2001:db8:aaaa:3::102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 2), action=(ip6.dst = 
> 2001:db8:aaaa:3::101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 0), action=(ip6.dst = 
> 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 1), action=(ip6.dst = 
> 2001:db8:aaaa:3::102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 2), action=(ip6.dst = 
> 2001:db8:aaaa:3::101; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 0), action=(ip6.dst = 
> 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 1), action=(ip6.dst = 
> 2001:db8:aaaa:3::102; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1 && reg1[[0..15]] == 2), action=(ip6.dst = 
> 2001:db8:aaaa:3::101; next;)
> +])
> +
> +# 3. Check if the DGP ports are in the match with action next
>  AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
>    table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
> @@ -14420,7 +14569,7 @@ AT_CHECK([grep "lr_out_undnat" lr1flows | 
> ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1_public" || outport == "lr1_public") 
> && is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
>  ])
>  
> -# 3. Check if the VIP IP is in the ipX.src action
> +# 4. Check if the VIP IP is in the ipX.src action
>  AT_CHECK([grep "lr_out_snat" lr1flows | 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;)
> @@ -14429,6 +14578,50 @@ AT_CHECK([grep "lr_out_snat" lr1flows | 
> ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
> 2001:db8:aaaa:3::101)) && (inport == "lr1_public" || outport == "lr1_public") 
> && is_chassis_resident("cr-lr1_public") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
>  ])
>  
> +# Delete LB and create with one backend
> +check ovn-nbctl --wait=sb lb-del lb1
> +check ovn-nbctl --wait=sb lb-add lb1 "2001:db8:cccc::1" 
> "2001:db8:aaaa:3::103"
> +
> +# Set use_stateless_nat to true
> +check ovn-nbctl --wait=sb set load_balancer lb1 
> options:use_stateless_nat=true
> +
> +# Associate load balancer to s1
> +check ovn-nbctl ls-lb-add s1 lb1
> +check ovn-nbctl lr-lb-add lr1 lb1
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl dump-flows lr1 > lr1flows
> +AT_CAPTURE_FILE([lr1flows])
> +# Check stateless NAT rules for load balancer with multiple DGP
> +# 1. Check if the reg1[0..15] will select one of backends
> +AT_CHECK([grep "lr_in_ct_extract" lr1flows | ovn_strip_lflows | grep 
> "2001:db8:cccc::1"], [0], [dnl
> +  table=??(lr_in_ct_extract   ), priority=120  , match=(ip && ip6.dst == 
> 2001:db8:cccc::1), action=(next;)
> +])
> +
> +# 2. Check if the backend IPs are in the ipX.dst action
> +AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep 
> "2001:db8:cccc::1"], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts1") && ip6 && ip6.dst == 
> 2001:db8:cccc::1), action=(ip6.dst = 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1-ts2") && ip6 && ip6.dst == 
> 2001:db8:cccc::1), action=(ip6.dst = 2001:db8:aaaa:3::103; next;)
> +  table=??(lr_in_dnat         ), priority=110  , 
> match=(is_chassis_resident("cr-lr1_public") && ip6 && ip6.dst == 
> 2001:db8:cccc::1), action=(ip6.dst = 2001:db8:aaaa:3::103; next;)
> +])
> +
> +# 3. Check if the DGP ports are in the match with action next
> +AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(next;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103)) && (inport == "lr1_public" || outport == "lr1_public") 
> && is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
> +])
> +
> +# 4. Check if the VIP IP is in the ipX.src action
> +AT_CHECK([grep "lr_out_snat" lr1flows | 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=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
> is_chassis_resident("cr-lr1-ts1") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
> is_chassis_resident("cr-lr1-ts2") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
> +  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
> 2001:db8:aaaa:3::103)) && (inport == "lr1_public" || outport == "lr1_public") 
> && is_chassis_resident("cr-lr1_public") && tcp), action=(ip6.src = 
> 2001:db8:cccc::1; next;)
> +])
> +
>  AT_CLEANUP
>  ])
>  

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

Reply via email to