On 8/24/25 2:02 PM, Alexandra Rukomoinikova wrote:
> Currently, load balancers using L3 gateway operate centrally. Despite being 
> distributed to compute nodes, all VIP traffic is sent to a single gateway 
> node for load balancing processing.
> 
> This RFC implementation introduces distributed load balancer that integrates 
> with physical network fabric to advertise VIPs via ECMP routing. Core concept 
> is to enable every compute node that hosting backend virtual machines to 
> locally terminate and process traffic destined for a VIP.
> 
> Implementation introduces a new distributed option for load balancer. When 
> enabled, it functions as follows (static routing example configuration):
>     1) Network Fabric Configuration: static ECMP route for Load Balancer's 
> VIP (e.g., 10.255.0.1/32) is configured on physical network. Nexthops for 
> this route are IP addresses of all compute nodes that host backends for this 
> load balancer.
>     2) Traffic Distribution: network fabric uses ECMP to balance incoming 
> client traffic for VIP across listed compute node nexthops, respecting 
> configured weights.
>     3) Local Traffic Processing: Each compute node independently terminates 
> VIP traffic it receives. Load balancer performs DNAT/UNDNAT right on the node 
> and then distributes connections only to backend servers running on that same 
> node. There is no cross-node traffic forwarding.
> 
> Example:
> Load Balancer: lb1 with VIP 10.255.0.1 and distributed option enabled.
> Fabric is configured with a static ECMP route for 10.255.0.1/32:
>     nexthop via ip_host1 weight 1 (hosts backend1)
>     nexthop via ip_host2 weight 1 (hosts backend2)
>     nexthop via ip_host3 weight 2 (hosts backend3 and backend4)
> 

Hi Alexandra,

I only skimmed through the RFC code changes so this is not a full
review, just some thoughts.

In general I think this might be a very nice addition to OVN!

> As part of testing, following estimates of distribution of requests to 
> balancers were obtained:
> [root@dev11 ~]# for i in $(seq 5000); do curl http://10.255.0.1:80 
> 2>/dev/null ; echo ; done | awk '{print $2}' | sort | uniq -c
>    1265 “backend 4",
>    1260 “backend 3",
>    1224 “backend 2",
>    1251 “backend 1",
> Thus, requests using ecmp balancing are distributed between backends 
> approximately evenly.
> 
> Key changes:
>     1) For distributed load balancers, specifying ip_port_mappings (without 
> using src_ip) becomes mandatory.
>     2) New SBDB action is introduced: ct_lb_mark_local(logical_port:ip:port). 
> Controllers on each compute node will process logical flows and populate 
> OpenFlow load balancing group with only local backends, which are identified 
> by their logical_port name.

Would it make sense to add "fallback" flows too for the case in which
none of the backends is local?  I.e.:
- prioritize local backends
- if no local backends exist fall back to any other backend

OTOH, while typing the above I realized maybe that wouldn't work because
reply traffic (from the backend) will not come back via the same chassis
so we'll fail unNATing.

>     3) This feature is exclusive to L3 mode, which requires associated 
> logical routers to operate distributively. All load balancer traffic flows 
> through router's distributed port, while load balancing logic is distributed 
> and processed locally on each compute node.

Is there any reason to not support such "distributed" load balancers on
logical switches too?

>     4) Added tests that check configuration creation interface, check 
> recalculation of logical flows, openflow groups and possibility of balancing 
> on a computer node with this option.
> 
> As part of future work on this RFC, following changes are planned:
>     1) Integrate this functionality with BGP support.
>     2) Implement incremental configuration updates.
>     3) Support following use case: not all backends are processed in a 
> distributed manner; some backends are handled centrally on gw node (this is 
> important, for example, when some backends are located on unsecured compute 
> nodes).

Instead of extending `ip_port_mappings` would it make sense to add a new
explicit Load_Balancer_Backend table for better "type safety"?  E.g.
with explicit Logical_Switch_Port references.  Maybe something like:

"Load_Balancer": {
    "columns": {
        "name": {"type": "string"},
        "typed_vips":
             {"type": {"key": {"type": "uuid",
                               "refTable": "Load_Balancer_VIP"},
                       "min": 1,
                       "max": "unlimited"}}

...
"Load_Balancer_VIP": {
    "columns": {
        "ip": {"type": "string"},
        "backends": {
            "type": {"key": {"type": "uuid",
                             "refTable": "Load_Balancer_Backend"},
                     "min": 0,
                     "max": "unlimited"}}

...
"Load_Balancer_Backend": {
    "columns": {
        "ip": {"type": "string"},
        "logical_port": {
            "type": {"key": {"type": "uuid",
                             "refTable": "Logical_Switch_Port",
                             "refType": "strong"},
                     "min": 1, "max": 1}},
...

Regards,
Dumitru

> 
> Suggested-by: Vladislav Odintsov <[email protected]>
> Signed-off-by: Alexandra Rukomoinikova <[email protected]>
> ---
>  controller/lflow.c          |  14 +++
>  controller/lflow.h          |   1 +
>  controller/ovn-controller.c |   1 +
>  include/ovn/actions.h       |  11 ++
>  lib/actions.c               | 133 +++++++++++++++++----
>  lib/ovn-util.c              |   2 +-
>  northd/en-lb-data.c         |  11 +-
>  northd/en-lb-data.h         |   3 +
>  northd/en-lr-stateful.c     |   3 +
>  northd/en-lr-stateful.h     |   1 +
>  northd/lb.c                 | 116 +++++++++++-------
>  northd/lb.h                 |   3 +
>  northd/northd.c             | 208 +++++++++++++++++++++++---------
>  northd/northd.h             |   5 +
>  ovn-nb.xml                  |  15 ++-
>  tests/ovn-northd.at         | 230 ++++++++++++++++++++++++++++++++++++
>  tests/system-ovn.at         |  83 +++++++++++++
>  utilities/ovn-trace.c       |   2 +
>  18 files changed, 716 insertions(+), 126 deletions(-)
> 
> diff --git a/controller/lflow.c b/controller/lflow.c
> index b75ae5c0d..ab4537c68 100644
> --- a/controller/lflow.c
> +++ b/controller/lflow.c
> @@ -63,6 +63,7 @@ struct lookup_port_aux {
>      const struct sbrec_logical_flow *lflow;
>      struct objdep_mgr *deps_mgr;
>      const struct hmap *chassis_tunnels;
> +    const struct shash *local_bindings;
>  };
>  
>  struct condition_aux {
> @@ -172,6 +173,17 @@ tunnel_ofport_cb(const void *aux_, const char 
> *port_name, ofp_port_t *ofport)
>      return true;
>  }
>  
> +static bool
> +lookup_local_port_cb(const void *aux_, const char *port_name)
> +{
> +    const struct lookup_port_aux *aux = aux_;
> +
> +    if (local_binding_get_primary_pb(aux->local_bindings, port_name)) {
> +        return true;
> +    }
> +    return false;
> +}
> +
>  static bool
>  is_chassis_resident_cb(const void *c_aux_, const char *port_name)
>  {
> @@ -850,6 +862,7 @@ add_matches_to_flow_table(const struct sbrec_logical_flow 
> *lflow,
>          .lflow = lflow,
>          .deps_mgr = l_ctx_out->lflow_deps_mgr,
>          .chassis_tunnels = l_ctx_in->chassis_tunnels,
> +        .local_bindings = l_ctx_in->lbinding_lports,
>      };
>  
>      /* Parse any meter to be used if this flow should punt packets to
> @@ -865,6 +878,7 @@ add_matches_to_flow_table(const struct sbrec_logical_flow 
> *lflow,
>      struct ovnact_encode_params ep = {
>          .lookup_port = lookup_port_cb,
>          .tunnel_ofport = tunnel_ofport_cb,
> +        .lookup_local_port = lookup_local_port_cb,
>          .aux = &aux,
>          .is_switch = ldp->is_switch,
>          .group_table = l_ctx_out->group_table,
> diff --git a/controller/lflow.h b/controller/lflow.h
> index c8a87c886..d58d20439 100644
> --- a/controller/lflow.h
> +++ b/controller/lflow.h
> @@ -140,6 +140,7 @@ struct lflow_ctx_in {
>      const struct smap *template_vars;
>      const struct flow_collector_ids *collector_ids;
>      const struct hmap *local_lbs;
> +    const struct shash *lbinding_lports;
>      bool localnet_learn_fdb;
>      bool localnet_learn_fdb_changed;
>      bool explicit_arp_ns_output;
> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
> index 6396fa898..308f7edd7 100644
> --- a/controller/ovn-controller.c
> +++ b/controller/ovn-controller.c
> @@ -3923,6 +3923,7 @@ init_lflow_ctx(struct engine_node *node,
>      l_ctx_in->template_vars = &template_vars->local_templates;
>      l_ctx_in->collector_ids = &fo->collector_ids;
>      l_ctx_in->local_lbs = &lb_data->local_lbs;
> +    l_ctx_in->lbinding_lports = &rt_data->lbinding_data.bindings;
>  
>      l_ctx_out->flow_table = &fo->flow_table;
>      l_ctx_out->group_table = &fo->group_table;
> diff --git a/include/ovn/actions.h b/include/ovn/actions.h
> index 0eaef9112..051d5a2c9 100644
> --- a/include/ovn/actions.h
> +++ b/include/ovn/actions.h
> @@ -75,6 +75,7 @@ struct collector_set_ids;
>      OVNACT(CT_SNAT_IN_CZONE,  ovnact_ct_nat)          \
>      OVNACT(CT_LB,             ovnact_ct_lb)           \
>      OVNACT(CT_LB_MARK,        ovnact_ct_lb)           \
> +    OVNACT(CT_LB_MARK_LOCAL,  ovnact_ct_lb)           \
>      OVNACT(SELECT,            ovnact_select)          \
>      OVNACT(CT_CLEAR,          ovnact_null)            \
>      OVNACT(CT_COMMIT_NAT,     ovnact_ct_commit_to_zone) \
> @@ -311,6 +312,12 @@ struct ovnact_ct_commit_to_zone {
>      uint8_t ltable;
>  };
>  
> +enum ovnact_ct_lb_type {
> +    OVNACT_CT_LB_TYPE_LABEL,
> +    OVNACT_CT_LB_TYPE_MARK,
> +    OVNACT_CT_LB_LOCAL_TYPE_MARK,
> +};
> +
>  enum ovnact_ct_lb_flag {
>      OVNACT_CT_LB_FLAG_NONE,
>      OVNACT_CT_LB_FLAG_SKIP_SNAT,
> @@ -324,6 +331,7 @@ struct ovnact_ct_lb_dst {
>          ovs_be32 ipv4;
>      };
>      uint16_t port;
> +    char *port_name;
>  };
>  
>  /* OVNACT_CT_LB/OVNACT_CT_LB_MARK. */
> @@ -897,6 +905,9 @@ struct ovnact_encode_params {
>      bool (*tunnel_ofport)(const void *aux, const char *port_name,
>                            ofp_port_t *ofport);
>  
> +    /* Checks if the logical port exists and is bound to this chassis. */
> +    bool (*lookup_local_port)(const void *aux, const char *port_name);
> +
>      const void *aux;
>  
>      /* 'true' if the flow is for a switch. */
> diff --git a/lib/actions.c b/lib/actions.c
> index 98ab368fc..3a6495f32 100644
> --- a/lib/actions.c
> +++ b/lib/actions.c
> @@ -1187,8 +1187,24 @@ ovnact_ct_commit_to_zone_free(struct 
> ovnact_ct_commit_to_zone *cn OVS_UNUSED)
>  {
>  }
>  
> +
> +static bool
> +parse_ct_lb_logical_port_name(struct action_context *ctx,
> +                              struct ovnact_ct_lb_dst *dst)
> +{
> +    if (ctx->lexer->token.type != LEX_T_STRING) {
> +        return false;
> +    }
> +
> +    dst->port_name = xstrdup(ctx->lexer->token.s);
> +
> +    lexer_get(ctx->lexer);
> +    return true;
> +}
> +
>  static void
> -parse_ct_lb_action(struct action_context *ctx, bool ct_lb_mark)
> +parse_ct_lb_action(struct action_context *ctx,
> +                   enum ovnact_ct_lb_type type)
>  {
>      if (ctx->pp->cur_ltable >= ctx->pp->n_tables) {
>          lexer_error(ctx->lexer, "\"ct_lb\" action not allowed in last 
> table.");
> @@ -1211,7 +1227,20 @@ parse_ct_lb_action(struct action_context *ctx, bool 
> ct_lb_mark)
>  
>          while (!lexer_match(ctx->lexer, LEX_T_SEMICOLON) &&
>                 !lexer_match(ctx->lexer, LEX_T_RPAREN)) {
> -            struct ovnact_ct_lb_dst dst;
> +            struct ovnact_ct_lb_dst dst = {0};
> +
> +            if (type == OVNACT_CT_LB_LOCAL_TYPE_MARK) {
> +
> +                if (!parse_ct_lb_logical_port_name(ctx, &dst)) {
> +                    vector_destroy(&dsts);
> +                    lexer_syntax_error(ctx->lexer,
> +                                       "expecting logicl port name "
> +                                       "for distributed load balancer");
> +                    return;
> +                }
> +                lexer_get(ctx->lexer);
> +            }
> +
>              if (lexer_match(ctx->lexer, LEX_T_LSQUARE)) {
>                  /* IPv6 address and port */
>                  if (ctx->lexer->token.type != LEX_T_INTEGER
> @@ -1298,8 +1327,19 @@ parse_ct_lb_action(struct action_context *ctx, bool 
> ct_lb_mark)
>          }
>      }
>  
> -    struct ovnact_ct_lb *cl = ct_lb_mark ? 
> ovnact_put_CT_LB_MARK(ctx->ovnacts)
> -                                         : ovnact_put_CT_LB(ctx->ovnacts);
> +    struct ovnact_ct_lb *cl;
> +    switch (type) {
> +        case OVNACT_CT_LB_TYPE_LABEL:
> +            cl = ovnact_put_CT_LB(ctx->ovnacts);
> +            break;
> +        case OVNACT_CT_LB_TYPE_MARK:
> +            cl = ovnact_put_CT_LB_MARK(ctx->ovnacts);
> +            break;
> +        case OVNACT_CT_LB_LOCAL_TYPE_MARK:
> +            cl = ovnact_put_CT_LB_MARK_LOCAL(ctx->ovnacts);
> +            break;
> +    }
> +
>      cl->ltable = ctx->pp->cur_ltable + 1;
>      cl->n_dsts = vector_len(&dsts);
>      cl->dsts = vector_steal_array(&dsts);
> @@ -1308,13 +1348,16 @@ parse_ct_lb_action(struct action_context *ctx, bool 
> ct_lb_mark)
>  }
>  
>  static void
> -format_ct_lb(const struct ovnact_ct_lb *cl, struct ds *s, bool ct_lb_mark)
> +format_ct_lb(const struct ovnact_ct_lb *cl, struct ds *s,
> +             enum ovnact_ct_lb_type type)
>  {
> -    if (ct_lb_mark) {
> -        ds_put_cstr(s, "ct_lb_mark");
> -    } else {
> -        ds_put_cstr(s, "ct_lb");
> -    }
> +    static const char *const lb_action_strings[] = {
> +        [OVNACT_CT_LB_TYPE_LABEL] = "ct_lb",
> +        [OVNACT_CT_LB_TYPE_MARK] = "ct_lb_mark",
> +        [OVNACT_CT_LB_LOCAL_TYPE_MARK] = "ct_lb_mark_local",
> +    };
> +    ds_put_cstr(s, lb_action_strings[type]);
> +
>      if (cl->n_dsts) {
>          ds_put_cstr(s, "(backends=");
>          for (size_t i = 0; i < cl->n_dsts; i++) {
> @@ -1337,6 +1380,9 @@ format_ct_lb(const struct ovnact_ct_lb *cl, struct ds 
> *s, bool ct_lb_mark)
>                      ds_put_format(s, "]:%"PRIu16, dst->port);
>                  }
>              }
> +            if (type == OVNACT_CT_LB_LOCAL_TYPE_MARK) {
> +                ds_put_format(s, ":%s", dst->port_name);
> +            }
>          }
>  
>          if (cl->hash_fields) {
> @@ -1363,20 +1409,36 @@ format_ct_lb(const struct ovnact_ct_lb *cl, struct ds 
> *s, bool ct_lb_mark)
>  static void
>  format_CT_LB(const struct ovnact_ct_lb *cl, struct ds *s)
>  {
> -    format_ct_lb(cl, s, false);
> +    format_ct_lb(cl, s, OVNACT_CT_LB_TYPE_LABEL);
>  }
>  
>  static void
>  format_CT_LB_MARK(const struct ovnact_ct_lb *cl, struct ds *s)
>  {
> -    format_ct_lb(cl, s, true);
> +    format_ct_lb(cl, s, OVNACT_CT_LB_TYPE_MARK);
> +}
> +
> +static void
> +format_CT_LB_MARK_LOCAL(const struct ovnact_ct_lb *cl, struct ds *s)
> +{
> +    format_ct_lb(cl, s, OVNACT_CT_LB_LOCAL_TYPE_MARK);
> +}
> +
> +static inline void
> +append_nat_destination(struct ds *ds, const char *ip_addr,
> +                       bool needs_brackets)
> +{
> +    ds_put_format(ds, "ct(nat(dst=%s%s%s",
> +                  needs_brackets ? "[" : "",
> +                  ip_addr,
> +                  needs_brackets ? "]" : "");
>  }
>  
>  static void
>  encode_ct_lb(const struct ovnact_ct_lb *cl,
>               const struct ovnact_encode_params *ep,
>               struct ofpbuf *ofpacts,
> -             bool ct_lb_mark)
> +             enum ovnact_ct_lb_type type)
>  {
>      uint8_t recirc_table = cl->ltable + first_ptable(ep, ep->pipeline);
>      if (!cl->n_dsts) {
> @@ -1408,7 +1470,8 @@ encode_ct_lb(const struct ovnact_ct_lb *cl,
>      struct ofpact_group *og;
>      uint32_t zone_reg = ep->is_switch ? MFF_LOG_CT_ZONE - MFF_REG0
>                              : MFF_LOG_DNAT_ZONE - MFF_REG0;
> -    const char *flag_reg = ct_lb_mark ? "ct_mark" : "ct_label";
> +    const char *flag_reg = (type == OVNACT_CT_LB_TYPE_LABEL)
> +                            ? "ct_label" : "ct_mark";
>  
>      const char *ct_flag_value;
>      switch (cl->ct_flag) {
> @@ -1443,11 +1506,14 @@ encode_ct_lb(const struct ovnact_ct_lb *cl,
>          } else {
>              inet_ntop(AF_INET6, &dst->ipv6, ip_addr, sizeof ip_addr);
>          }
> -        ds_put_format(&ds, 
> ",bucket=bucket_id=%"PRIuSIZE",weight:100,actions="
> -                      "ct(nat(dst=%s%s%s", bucket_id,
> -                      dst->family == AF_INET6 && dst->port ? "[" : "",
> -                      ip_addr,
> -                      dst->family == AF_INET6 && dst->port ? "]" : "");
> +        if (type == OVNACT_CT_LB_LOCAL_TYPE_MARK
> +            && !ep->lookup_local_port(ep->aux, dst->port_name)) {
> +            continue;
> +        }
> +        ds_put_format(&ds, 
> ",bucket=bucket_id=%"PRIuSIZE",weight:100,actions=",
> +                      bucket_id);
> +        bool needs_brackets = (dst->family == AF_INET6 && dst->port);
> +        append_nat_destination(&ds, ip_addr, needs_brackets);
>          if (dst->port) {
>              ds_put_format(&ds, ":%"PRIu16, dst->port);
>          }
> @@ -1480,7 +1546,7 @@ encode_CT_LB(const struct ovnact_ct_lb *cl,
>               const struct ovnact_encode_params *ep,
>               struct ofpbuf *ofpacts)
>  {
> -    encode_ct_lb(cl, ep, ofpacts, false);
> +    encode_ct_lb(cl, ep, ofpacts, OVNACT_CT_LB_TYPE_LABEL);
>  }
>  
>  static void
> @@ -1488,13 +1554,30 @@ encode_CT_LB_MARK(const struct ovnact_ct_lb *cl,
>                    const struct ovnact_encode_params *ep,
>                    struct ofpbuf *ofpacts)
>  {
> -    encode_ct_lb(cl, ep, ofpacts, true);
> +    encode_ct_lb(cl, ep, ofpacts, OVNACT_CT_LB_TYPE_MARK);
>  }
>  
>  static void
> -ovnact_ct_lb_free(struct ovnact_ct_lb *ct_lb)
> +encode_CT_LB_MARK_LOCAL(const struct ovnact_ct_lb *cl,
> +                        const struct ovnact_encode_params *ep,
> +                        struct ofpbuf *ofpacts)
> +{
> +    encode_ct_lb(cl, ep, ofpacts, OVNACT_CT_LB_LOCAL_TYPE_MARK);
> +}
> +
> +static void
> +ovnact_ct_lb_free_dsts(struct ovnact_ct_lb *ct_lb)
>  {
> +    for (size_t i = 0; i < ct_lb->n_dsts; i++) {
> +        free(ct_lb->dsts[i].port_name);
> +    }
>      free(ct_lb->dsts);
> +}
> +
> +static void
> +ovnact_ct_lb_free(struct ovnact_ct_lb *ct_lb)
> +{
> +    ovnact_ct_lb_free_dsts(ct_lb);
>      free(ct_lb->hash_fields);
>  }
>  
> @@ -5900,9 +5983,11 @@ parse_action(struct action_context *ctx)
>      } else if (lexer_match_id(ctx->lexer, "ct_snat_in_czone")) {
>          parse_CT_SNAT_IN_CZONE(ctx);
>      } else if (lexer_match_id(ctx->lexer, "ct_lb")) {
> -        parse_ct_lb_action(ctx, false);
> +        parse_ct_lb_action(ctx, OVNACT_CT_LB_TYPE_LABEL);
>      } else if (lexer_match_id(ctx->lexer, "ct_lb_mark")) {
> -        parse_ct_lb_action(ctx, true);
> +        parse_ct_lb_action(ctx, OVNACT_CT_LB_TYPE_MARK);
> +    } else if (lexer_match_id(ctx->lexer, "ct_lb_mark_local")) {
> +        parse_ct_lb_action(ctx, OVNACT_CT_LB_LOCAL_TYPE_MARK);
>      } else if (lexer_match_id(ctx->lexer, "ct_clear")) {
>          ovnact_put_CT_CLEAR(ctx->ovnacts);
>      } else if (lexer_match_id(ctx->lexer, "ct_commit_nat")) {
> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
> index 8b583fa6d..7eb49b553 100644
> --- a/lib/ovn-util.c
> +++ b/lib/ovn-util.c
> @@ -915,7 +915,7 @@ ip_address_and_port_from_lb_key(const char *key, char 
> **ip_address,
>   *
>   * NOTE: If OVN_NORTHD_PIPELINE_CSUM is updated make sure to double check
>   * whether an update of OVN_INTERNAL_MINOR_VER is required. */
> -#define OVN_NORTHD_PIPELINE_CSUM "2405300854 10800"
> +#define OVN_NORTHD_PIPELINE_CSUM "1693307722 10856"
>  #define OVN_INTERNAL_MINOR_VER 10
>  
>  /* Returns the OVN version. The caller must free the returned value. */
> diff --git a/northd/en-lb-data.c b/northd/en-lb-data.c
> index 6d52d465e..5591aa576 100644
> --- a/northd/en-lb-data.c
> +++ b/northd/en-lb-data.c
> @@ -136,7 +136,7 @@ en_lb_data_clear_tracked_data(void *data)
>      destroy_tracked_data(lb_data);
>  }
>  
> -
> +/* TODO: incremental processing for distributed lb. */
>  /* Handler functions. */
>  enum engine_input_handler_result
>  lb_data_load_balancer_handler(struct engine_node *node, void *data)
> @@ -166,6 +166,7 @@ lb_data_load_balancer_handler(struct engine_node *node, 
> void *data)
>              add_crupdated_lb_to_tracked_data(lb, trk_lb_data,
>                                               lb->health_checks);
>              trk_lb_data->has_routable_lb |= lb->routable;
> +            trk_lb_data->distributed_mode |= lb->distributed_mode;
>              continue;
>          }
>  
> @@ -180,6 +181,7 @@ lb_data_load_balancer_handler(struct engine_node *node, 
> void *data)
>              add_deleted_lb_to_tracked_data(lb, trk_lb_data,
>                                             lb->health_checks);
>              trk_lb_data->has_routable_lb |= lb->routable;
> +            trk_lb_data->distributed_mode |= lb->distributed_mode;
>          } else {
>              /* Load balancer updated. */
>              bool health_checks = lb->health_checks;
> @@ -189,12 +191,13 @@ lb_data_load_balancer_handler(struct engine_node *node, 
> void *data)
>              sset_swap(&lb->ips_v6, &old_ips_v6);
>              enum lb_neighbor_responder_mode neigh_mode = lb->neigh_mode;
>              bool routable = lb->routable;
> +            bool distributed_mode = lb->distributed_mode;
>              ovn_northd_lb_reinit(lb, tracked_lb);
>              health_checks |= lb->health_checks;
>              struct crupdated_lb *clb = add_crupdated_lb_to_tracked_data(
>                  lb, trk_lb_data, health_checks);
>              trk_lb_data->has_routable_lb |= lb->routable;
> -
> +            trk_lb_data->distributed_mode |= lb->distributed_mode;
>              /* Determine the inserted and deleted vips and store them in
>               * the tracked data. */
>              const char *vip;
> @@ -226,6 +229,10 @@ lb_data_load_balancer_handler(struct engine_node *node, 
> void *data)
>                  /* If neigh_mode is updated trigger a full recompute. */
>                  return EN_UNHANDLED;
>              }
> +            if (distributed_mode != lb->distributed_mode) {
> +                /* If neigh_mode is updated trigger a full recompute. */
> +                return EN_UNHANDLED;
> +            }
>          }
>      }
>  
> diff --git a/northd/en-lb-data.h b/northd/en-lb-data.h
> index 1da087656..bbc464e45 100644
> --- a/northd/en-lb-data.h
> +++ b/northd/en-lb-data.h
> @@ -82,6 +82,9 @@ struct tracked_lb_data {
>  
>      /* Indicates if any lb (in the tracked data) has 'routable' flag set. */
>      bool has_routable_lb;
> +
> +    /* Indicates if a lb is in distributed mode. */
> +    bool distributed_mode;
>  };
>  
>  /* Datapath (logical switch) to lb/lbgrp association data. */
> diff --git a/northd/en-lr-stateful.c b/northd/en-lr-stateful.c
> index 56e93f3c4..357c8bbc9 100644
> --- a/northd/en-lr-stateful.c
> +++ b/northd/en-lr-stateful.c
> @@ -326,6 +326,7 @@ lr_stateful_lb_data_handler(struct engine_node *node, 
> void *data_)
>                  ovn_datapaths_find_by_index(input_data.lr_datapaths,
>                                              lr_stateful_rec->lr_index);
>              lr_stateful_rec->has_lb_vip = od_has_lb_vip(od);
> +            lr_stateful_rec->has_distributed_lb = lr_has_distributed_lb(od);
>          }
>  
>          return EN_HANDLED_UPDATED;
> @@ -530,7 +531,9 @@ lr_stateful_record_create(struct lr_stateful_table *table,
>      if (nbr->n_nat) {
>          lr_stateful_rebuild_vip_nats(lr_stateful_rec);
>      }
> +
>      lr_stateful_rec->has_lb_vip = od_has_lb_vip(od);
> +    lr_stateful_rec->has_distributed_lb = lr_has_distributed_lb(od);
>  
>      hmap_insert(&table->entries, &lr_stateful_rec->key_node,
>                  uuid_hash(&lr_stateful_rec->nbr_uuid));
> diff --git a/northd/en-lr-stateful.h b/northd/en-lr-stateful.h
> index 75975c935..c7e30127a 100644
> --- a/northd/en-lr-stateful.h
> +++ b/northd/en-lr-stateful.h
> @@ -59,6 +59,7 @@ struct lr_stateful_record {
>  
>      bool has_lb_vip;
>  
> +    bool has_distributed_lb;
>      /* Load Balancer vIPs relevant for this datapath. */
>      struct ovn_lb_ip_set *lb_ips;
>  
> diff --git a/northd/lb.c b/northd/lb.c
> index 30726cd27..13b76a1ef 100644
> --- a/northd/lb.c
> +++ b/northd/lb.c
> @@ -85,12 +85,12 @@ ovn_lb_ip_set_clone(struct ovn_lb_ip_set *lb_ip_set)
>      return clone;
>  }
>  
> -static
> -void ovn_northd_lb_vip_init(struct ovn_northd_lb_vip *lb_vip_nb,
> -                            const struct ovn_lb_vip *lb_vip,
> -                            const struct nbrec_load_balancer *nbrec_lb,
> -                            const char *vip_port_str, const char 
> *backend_ips,
> -                            bool template)
> +static void
> +ovn_northd_lb_vip_init(struct ovn_northd_lb_vip *lb_vip_nb,
> +                       const struct ovn_lb_vip *lb_vip,
> +                       const struct nbrec_load_balancer *nbrec_lb,
> +                       const char *vip_port_str, const char *backend_ips,
> +                       bool template)
>  {
>      lb_vip_nb->backend_ips = xstrdup(backend_ips);
>      lb_vip_nb->n_backends = vector_len(&lb_vip->backends);
> @@ -101,43 +101,57 @@ void ovn_northd_lb_vip_init(struct ovn_northd_lb_vip 
> *lb_vip_nb,
>  }
>  
>  /*
> - * Initializes health check configuration for load balancer VIP
> - * backends. Parses the ip_port_mappings in the format :
> + * Initializes health check configuration for load balancer VIP backends.
> + * Parses the ip_port_mappings in the format:
>   * "ip:logical_port:src_ip[:az_name]".
> - * If az_name is present and non-empty, it indicates this is a
> - * remote service monitor (backend is in another availability zone),
> - * it should be propogated to another AZ by interconnection processing.
> + * If az_name is present and non-empty, it indicates this is a remote service
> + * monitor (backend is in another availability zone), it should be propogated
> + * to another AZ by interconnection processing.
> + * src_ip parameter becomes optional when distributed mode is enabled without
> + * health checks configured.
>   */
>  static void
> -ovn_lb_vip_backends_health_check_init(const struct ovn_northd_lb *lb,
> -                                      const struct ovn_lb_vip *lb_vip,
> -                                      struct ovn_northd_lb_vip *lb_vip_nb)
> +ovn_lb_vip_backends_ip_port_mappings_init(const struct ovn_northd_lb *lb,
> +                                          const struct ovn_lb_vip *lb_vip,
> +                                          struct ovn_northd_lb_vip 
> *lb_vip_nb,
> +                                          bool *is_lb_correctly_configured)
>  {
>      struct ds key = DS_EMPTY_INITIALIZER;
> +    bool allow_without_src_ip = lb->distributed_mode
> +                                && !lb_vip_nb->lb_health_check;
>  
>      for (size_t j = 0; j < vector_len(&lb_vip->backends); j++) {
>          const struct ovn_lb_backend *backend =
>              vector_get_ptr(&lb_vip->backends, j);
> +        struct ovn_northd_lb_backend *backend_nb = NULL;
> +        char *port_name = NULL, *az_name = NULL, *first_colon = NULL;
> +        char *svc_mon_src_ip = NULL, *src_ip = NULL;
> +        bool is_remote = false;
>          ds_clear(&key);
>          ds_put_format(&key, IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)
>                        ? "%s" : "[%s]", backend->ip_str);
> -
>          const char *s = smap_get(&lb->nlb->ip_port_mappings, ds_cstr(&key));
>          if (!s) {
> -            continue;
> +            goto mark_error_and_cleanup;
>          }
>  
> -        char *svc_mon_src_ip = NULL;
> -        char *az_name = NULL;
> -        bool is_remote = false;
> -        char *port_name = xstrdup(s);
> -        char *src_ip = NULL;
> -
> -        char *first_colon = strchr(port_name, ':');
> -        if (!first_colon) {
> -            free(port_name);
> -            continue;
> +        port_name = xstrdup(s);
> +        first_colon = strchr(port_name, ':');
> +        if (!first_colon && allow_without_src_ip) {
> +            if (!*port_name) {
> +                VLOG_WARN("Empty port name in distributed mode for IP %s",
> +                          ds_cstr(&key));
> +                goto mark_error_and_cleanup;
> +            }
> +            src_ip = NULL;
> +            az_name = NULL;
> +            is_remote = false;
> +            goto init_backend_nb;
> +        } else if (!first_colon) {
> +            VLOG_WARN("Expected ':' separator for: %s", port_name);
> +            goto mark_error_and_cleanup;
>          }
> +
>          *first_colon = '\0';
>  
>          if (first_colon[1] == '[') {
> @@ -145,8 +159,7 @@ ovn_lb_vip_backends_health_check_init(const struct 
> ovn_northd_lb *lb,
>              char *ip_end = strchr(first_colon + 2, ']');
>              if (!ip_end) {
>                  VLOG_WARN("Malformed IPv6 address in backend %s", s);
> -                free(port_name);
> -                continue;
> +                goto mark_error_and_cleanup;
>              }
>  
>              src_ip = first_colon + 2;
> @@ -157,8 +170,7 @@ ovn_lb_vip_backends_health_check_init(const struct 
> ovn_northd_lb *lb,
>                  if (!*az_name) {
>                      VLOG_WARN("Empty AZ name specified for backend %s",
>                                port_name);
> -                    free(port_name);
> -                    continue;
> +                    goto mark_error_and_cleanup;
>                  }
>                  is_remote = true;
>              }
> @@ -172,32 +184,37 @@ ovn_lb_vip_backends_health_check_init(const struct 
> ovn_northd_lb *lb,
>                  if (!*az_name) {
>                      VLOG_WARN("Empty AZ name specified for backend %s",
>                                port_name);
> -                    free(port_name);
> -                    continue;
> +                    goto mark_error_and_cleanup;
>                  }
> -            is_remote = true;
> +                is_remote = true;
>              }
>          }
>  
>          struct sockaddr_storage svc_mon_src_addr;
>          if (!src_ip || !inet_parse_address(src_ip, &svc_mon_src_addr)) {
>              VLOG_WARN("Invalid svc mon src IP %s", src_ip ? src_ip : "NULL");
> +            goto mark_error_and_cleanup;
>          } else {
>              struct ds src_ip_s = DS_EMPTY_INITIALIZER;
>              ss_format_address_nobracks(&svc_mon_src_addr, &src_ip_s);
>              svc_mon_src_ip = ds_steal_cstr(&src_ip_s);
>          }
>  
> -        if (svc_mon_src_ip) {
> -            struct ovn_northd_lb_backend *backend_nb =
> -                &lb_vip_nb->backends_nb[j];
> -            backend_nb->health_check = true;
> -            backend_nb->logical_port = xstrdup(port_name);
> -            backend_nb->svc_mon_src_ip = svc_mon_src_ip;
> -            backend_nb->az_name = is_remote ? xstrdup(az_name) : NULL;
> -            backend_nb->local_backend = !is_remote;
> -        }
> +init_backend_nb:
> +        backend_nb = &lb_vip_nb->backends_nb[j];
> +        backend_nb->health_check = lb_vip_nb->lb_health_check;
> +        backend_nb->logical_port = xstrdup(port_name);
> +        backend_nb->svc_mon_src_ip = svc_mon_src_ip;
> +        backend_nb->az_name = is_remote ? xstrdup(az_name) : NULL;
> +        backend_nb->local_backend = !is_remote;
> +        goto cleanup_and_continue;
> +
> +mark_error_and_cleanup:
> +        *is_lb_correctly_configured = false;
> +
> +cleanup_and_continue:
>          free(port_name);
> +        continue;
>      }
>  
>      ds_destroy(&key);
> @@ -364,6 +381,9 @@ ovn_northd_lb_init(struct ovn_northd_lb *lb,
>          lb->hairpin_snat_ip = xstrdup(snat_ip);
>      }
>  
> +    lb->distributed_mode = smap_get_bool(&nbrec_lb->options,
> +                                         "distributed",
> +                                         false);
>      sset_init(&lb->ips_v4);
>      sset_init(&lb->ips_v6);
>      struct smap_node *node;
> @@ -403,8 +423,16 @@ ovn_northd_lb_init(struct ovn_northd_lb *lb,
>          }
>          n_vips++;
>  
> -        if (lb_vip_nb->lb_health_check) {
> -            ovn_lb_vip_backends_health_check_init(lb, lb_vip, lb_vip_nb);
> +        if (lb->distributed_mode || lb_vip_nb->lb_health_check) {
> +            bool is_lb_correctly_configured = true;
> +            ovn_lb_vip_backends_ip_port_mappings_init(lb,
> +                lb_vip, lb_vip_nb, &is_lb_correctly_configured);
> +            if (lb->distributed_mode && !is_lb_correctly_configured) {
> +                VLOG_ERR("Proper ip_port_mappings configuration for "
> +                         "all backends is required for distributed load "
> +                         "balancer %s operation.", lb->nlb->name);
> +                lb->distributed_mode = false;
> +            }
>          }
>      }
>  
> diff --git a/northd/lb.h b/northd/lb.h
> index a0e560204..2f30cce0b 100644
> --- a/northd/lb.h
> +++ b/northd/lb.h
> @@ -74,6 +74,9 @@ struct ovn_northd_lb {
>      /* Indicates if the load balancer has health checks configured. */
>      bool health_checks;
>  
> +    /* Indicates if the load balancer is distributed. */
> +    bool distributed_mode;
> +
>      char *hairpin_snat_ip;
>  };
>  
> diff --git a/northd/northd.c b/northd/northd.c
> index 015f30a35..b0d86c741 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -473,6 +473,20 @@ od_has_lb_vip(const struct ovn_datapath *od)
>      }
>  }
>  
> +bool
> +lr_has_distributed_lb(const struct ovn_datapath *od)
> +{
> +    for (size_t i = 0; i < od->nbr->n_load_balancer; i++) {
> +        if (lb_has_vip(od->nbr->load_balancer[i]) &&
> +            smap_get_bool(&od->nbr->load_balancer[i]->options,
> +                          "distributed", false)) {
> +            return true;
> +        }
> +    }
> +
> +    return false;
> +}
> +
>  static const char *
>  ovn_datapath_name(const struct sbrec_datapath_binding *sb)
>  {
> @@ -537,6 +551,7 @@ ovn_datapath_create(struct hmap *datapaths, const struct 
> uuid *key,
>      od->router_ports = VECTOR_EMPTY_INITIALIZER(struct ovn_port *);
>      od->l3dgw_ports = VECTOR_EMPTY_INITIALIZER(struct ovn_port *);
>      od->localnet_ports = VECTOR_EMPTY_INITIALIZER(struct ovn_port *);
> +    od->distrubuted_lbs = VECTOR_EMPTY_INITIALIZER(struct ovn_northd_lb *);
>      od->lb_with_stateless_mode = false;
>      od->ipam_info_initialized = false;
>      od->tunnel_key = sdp->sb_dp->tunnel_key;
> @@ -567,6 +582,7 @@ ovn_datapath_destroy(struct hmap *datapaths, struct 
> ovn_datapath *od)
>          vector_destroy(&od->ls_peers);
>          vector_destroy(&od->localnet_ports);
>          vector_destroy(&od->l3dgw_ports);
> +        vector_destroy(&od->distrubuted_lbs);
>          destroy_mcast_info_for_datapath(od);
>          destroy_ports_for_datapath(od);
>          sset_destroy(&od->router_ips);
> @@ -3140,6 +3156,53 @@ ovn_lb_svc_create(struct ovsdb_idl_txn *ovnsb_txn,
>      }
>  }
>  
> +static inline void
> +append_lb_backend_to_action(const struct ovn_lb_backend *backend,
> +                            const struct ovn_northd_lb_backend *backend_nb,
> +                            bool distributed_mode,
> +                            struct ds *action)
> +{
> +    bool ipv6 = !IN6_IS_ADDR_V4MAPPED(&backend->ip);
> +
> +    if (distributed_mode) {
> +        ds_put_format(action, "\"%s\":", backend_nb->logical_port);
> +    }
> +    ds_put_format(action, ipv6 ? "[%s]:%"PRIu16"," : "%s:%"PRIu16",",
> +                  backend->ip_str, backend->port);
> +}
> +
> +static bool
> +is_backend_online(const struct ovn_northd_lb *lb,
> +                  const struct ovn_lb_backend *backend,
> +                  const struct ovn_northd_lb_backend *backend_nb,
> +                  const struct svc_monitors_map_data *svc_mons_data)
> +{
> +    const char *protocol = lb->nlb->protocol;
> +    if (!protocol || !protocol[0]) {
> +        protocol = "tcp";
> +    }
> +
> +    struct service_monitor_info *mon_info =
> +        get_service_mon(svc_mons_data->local_svc_monitors_map,
> +                        svc_mons_data->ic_learned_svc_monitors_map,
> +                        backend->ip_str,
> +                        backend_nb->logical_port,
> +                        backend->port,
> +                        protocol);
> +
> +    if (!mon_info) {
> +        return false;
> +    }
> +
> +    ovs_assert(mon_info->sbrec_mon);
> +    if (mon_info->sbrec_mon->status &&
> +        strcmp(mon_info->sbrec_mon->status, "online")) {
> +        return false;
> +    }
> +
> +    return true;
> +}
> +
>  static bool
>  build_lb_vip_actions(const struct ovn_northd_lb *lb,
>                       const struct ovn_lb_vip *lb_vip,
> @@ -3165,12 +3228,14 @@ build_lb_vip_actions(const struct ovn_northd_lb *lb,
>          }
>      }
>  
> -    if (lb_vip_nb->lb_health_check) {
> -        ds_put_cstr(action, "ct_lb_mark(backends=");
> +    ds_put_format(action, "%s", lb->distributed_mode
> +                  ? "ct_lb_mark_local(backends="
> +                  : "ct_lb_mark(backends=");
>  
> -        size_t i = 0;
> -        size_t n_active_backends = 0;
> -        const struct ovn_lb_backend *backend;
> +    const struct ovn_lb_backend *backend;
> +    size_t n_active_backends = 0;
> +    size_t i = 0;
> +    if (lb_vip_nb->lb_health_check) {
>          VECTOR_FOR_EACH_PTR (&lb_vip->backends, backend) {
>              struct ovn_northd_lb_backend *backend_nb =
>                  &lb_vip_nb->backends_nb[i++];
> @@ -3179,41 +3244,44 @@ build_lb_vip_actions(const struct ovn_northd_lb *lb,
>                  continue;
>              }
>  
> -            const char *protocol = lb->nlb->protocol;
> -            if (!protocol || !protocol[0]) {
> -                protocol = "tcp";
> +            if (is_backend_online(lb, backend,
> +                    backend_nb, svc_mons_data)) {
> +                n_active_backends++;
> +                append_lb_backend_to_action(backend, backend_nb,
> +                    false, action);
>              }
> +        }
> +        ds_chomp(action, ',');
> +    } else if (lb->distributed_mode) {
> +        VECTOR_FOR_EACH_PTR (&lb_vip->backends, backend) {
> +            struct ovn_northd_lb_backend *backend_nb =
> +                &lb_vip_nb->backends_nb[i++];
>  
> -            struct service_monitor_info *mon_info =
> -                get_service_mon(svc_mons_data->local_svc_monitors_map,
> -                                svc_mons_data->ic_learned_svc_monitors_map,
> -                                backend->ip_str,
> -                                backend_nb->logical_port,
> -                                backend->port,
> -                                protocol);
> -
> -            if (!mon_info) {
> -                continue;
> -            }
> +                if (lb_vip_nb->lb_health_check
> +                    && !backend_nb->health_check) {
> +                    continue;
> +                }
>  
> -            ovs_assert(mon_info->sbrec_mon);
> -            if (mon_info->sbrec_mon->status &&
> -                    strcmp(mon_info->sbrec_mon->status, "online")) {
> -                continue;
> -            }
> +                if (lb_vip_nb->lb_health_check) {
> +                    if (is_backend_online(lb, backend,
> +                            backend_nb, svc_mons_data)) {
> +                        n_active_backends++;
> +                    } else {
> +                        continue;
> +                    }
> +                }
>  
> -            n_active_backends++;
> -            bool ipv6 = !IN6_IS_ADDR_V4MAPPED(&backend->ip);
> -            ds_put_format(action, ipv6 ? "[%s]:%"PRIu16"," : "%s:%"PRIu16",",
> -                          backend->ip_str, backend->port);
> +                append_lb_backend_to_action(backend, backend_nb,
> +                    true, action);
>          }
>          ds_chomp(action, ',');
> +    } else {
> +        ds_put_format(action, "%s", lb_vip_nb->backend_ips);
> +    }
>  
> +    if (lb_vip_nb->lb_health_check) {
>          drop = !n_active_backends && !lb_vip->empty_backend_rej;
>          reject = !n_active_backends && lb_vip->empty_backend_rej;
> -    } else {
> -        ds_put_format(action, "ct_lb_mark(backends=%s",
> -                      lb_vip_nb->backend_ips);
>      }
>  
>      if (reject) {
> @@ -3250,6 +3318,20 @@ build_lb_vip_actions(const struct ovn_northd_lb *lb,
>      return reject;
>  }
>  
> +static inline void
> +handle_lb_datapath_modes(struct ovn_datapath *od,
> +                         struct ovn_lb_datapaths *lb_dps,
> +                         bool ls_datapath)
> +{
> +    if (ls_datapath && od->lb_with_stateless_mode) {
> +        hmapx_add(&lb_dps->ls_lb_with_stateless_mode, od);
> +    }
> +
> +    if (!ls_datapath && lb_dps->lb->distributed_mode) {
> +        vector_push(&od->distrubuted_lbs, &lb_dps->lb);
> +    }
> +}
> +
>  static void
>  build_lb_datapaths(const struct hmap *lbs, const struct hmap *lb_groups,
>                     struct ovn_datapaths *ls_datapaths,
> @@ -3292,9 +3374,7 @@ build_lb_datapaths(const struct hmap *lbs, const struct 
> hmap *lb_groups,
>              lb_dps = ovn_lb_datapaths_find(lb_datapaths_map, lb_uuid);
>              ovs_assert(lb_dps);
>              ovn_lb_datapaths_add_ls(lb_dps, 1, &od);
> -            if (od->lb_with_stateless_mode) {
> -                hmapx_add(&lb_dps->ls_lb_with_stateless_mode, od);
> -            }
> +            handle_lb_datapath_modes(od, lb_dps, true);
>          }
>  
>          for (size_t i = 0; i < od->nbs->n_load_balancer_group; i++) {
> @@ -3328,6 +3408,7 @@ build_lb_datapaths(const struct hmap *lbs, const struct 
> hmap *lb_groups,
>              lb_dps = ovn_lb_datapaths_find(lb_datapaths_map, lb_uuid);
>              ovs_assert(lb_dps);
>              ovn_lb_datapaths_add_lr(lb_dps, 1, &od);
> +            handle_lb_datapath_modes(od, lb_dps, false);
>          }
>      }
>  
> @@ -3664,6 +3745,7 @@ sync_pb_for_lrp(struct ovn_port *op,
>  
>          bool always_redirect =
>              !lr_stateful_rec->lrnat_rec->has_distributed_nat &&
> +            !lr_stateful_rec->has_distributed_lb &&
>              !l3dgw_port_has_associated_vtep_lports(op->primary_port);
>  
>          const char *redirect_type = smap_get(&op->nbrp->options,
> @@ -5067,11 +5149,7 @@ northd_handle_lb_data_changes(struct tracked_lb_data 
> *trk_lb_data,
>              lb_dps = ovn_lb_datapaths_find(lb_datapaths_map, 
> &uuidnode->uuid);
>              ovs_assert(lb_dps);
>              ovn_lb_datapaths_add_ls(lb_dps, 1, &od);
> -
> -            if (od->lb_with_stateless_mode) {
> -                hmapx_add(&lb_dps->ls_lb_with_stateless_mode, od);
> -            }
> -
> +            handle_lb_datapath_modes(od, lb_dps, true);
>              /* Add the lb to the northd tracked data. */
>              hmapx_add(&nd_changes->trk_lbs.crupdated, lb_dps);
>          }
> @@ -5108,7 +5186,7 @@ northd_handle_lb_data_changes(struct tracked_lb_data 
> *trk_lb_data,
>              lb_dps = ovn_lb_datapaths_find(lb_datapaths_map, 
> &uuidnode->uuid);
>              ovs_assert(lb_dps);
>              ovn_lb_datapaths_add_lr(lb_dps, 1, &od);
> -
> +            handle_lb_datapath_modes(od, lb_dps, false);
>              /* Add the lb to the northd tracked data. */
>              hmapx_add(&nd_changes->trk_lbs.crupdated, lb_dps);
>          }
> @@ -10134,6 +10212,12 @@ build_lswitch_ip_mcast_igmp_mld(struct 
> ovn_igmp_group *igmp_group,
>                    90, ds_cstr(match), ds_cstr(actions), lflow_ref);
>  }
>  
> +static inline bool
> +peer_has_distributed_lb(struct ovn_port *op)
> +{
> +    return op && op->od && !vector_is_empty(&op->od->distrubuted_lbs);
> +}
> +
>  /* Ingress table 25: Destination lookup, unicast handling (priority 50), */
>  static void
>  build_lswitch_ip_unicast_lookup(struct ovn_port *op,
> @@ -10208,7 +10292,8 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
>                                        struct ovn_port *)->cr_port->json_key;
>              }
>  
> -            if (add_chassis_resident_check) {
> +            if (add_chassis_resident_check
> +                && !peer_has_distributed_lb(op->peer)) {
>                  ds_put_format(match, " && is_chassis_resident(%s)", 
> json_key);
>              }
>          } else if (op->cr_port) {
> @@ -11922,7 +12007,8 @@ build_distr_lrouter_nat_flows_for_lb(struct 
> lrouter_nat_lb_flows_ctx *ctx,
>                                       struct ovn_datapath *od,
>                                       struct lflow_ref *lflow_ref,
>                                       struct ovn_port *dgp,
> -                                     bool stateless_nat)
> +                                     bool stateless_nat,
> +                                     bool distributed_mode)
>  {
>      struct ds dnat_action = DS_EMPTY_INITIALIZER;
>  
> @@ -11964,8 +12050,9 @@ build_distr_lrouter_nat_flows_for_lb(struct 
> lrouter_nat_lb_flows_ctx *ctx,
>          meter = copp_meter_get(COPP_REJECT, od->nbr->copp, 
> ctx->meter_groups);
>      }
>  
> -    if (!vector_is_empty(&ctx->lb_vip->backends) ||
> -        !ctx->lb_vip->empty_backend_rej) {
> +    if (!distributed_mode
> +        && (!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);
>      }
> @@ -12008,23 +12095,33 @@ build_distr_lrouter_nat_flows_for_lb(struct 
> lrouter_nat_lb_flows_ctx *ctx,
>      }
>  
>      /* We need to centralize the LB traffic to properly perform
> -     * the undnat stage.
> +     * the undnat stage in case of non distributed load balancer.
>       */
>      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);
> +    const char *outport = distributed_mode ?
> +                          dgp->json_key :
> +                          dgp->cr_port->json_key;
> +
> +    ds_put_format(ctx->gw_redir_action,
> +                  "outport = %s; next;", outport);
>  
>      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);
> +    ds_put_format(ctx->undnat_match, ") && (inport == %s || outport == %s)",
> +                  dgp->json_key, dgp->json_key);
> +
> +    if (!distributed_mode) {
> +        ds_put_format(ctx->undnat_match, " && is_chassis_resident(%s)",
> +                      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) {
> @@ -12153,6 +12250,8 @@ build_lrouter_nat_flows_for_lb(
>                                         svc_mons_data,
>                                         false);
>  
> +    bool distributed_mode = lb->distributed_mode;
> +
>      /* Higher priority rules are added for load-balancing in DNAT
>       * table.  For every match (on a VIP[:port]), we add two flows.
>       * One flow is for specific matching on ct.new with an action
> @@ -12257,7 +12356,8 @@ build_lrouter_nat_flows_for_lb(
>              VECTOR_FOR_EACH (&od->l3dgw_ports, dgp) {
>                  build_distr_lrouter_nat_flows_for_lb(&ctx, type, od,
>                                                       lb_dps->lflow_ref, dgp,
> -                                                     stateless_nat);
> +                                                     stateless_nat,
> +                                                     distributed_mode);
>              }
>          }
>  
> @@ -13403,7 +13503,8 @@ build_adm_ctrl_flows_for_lrouter_port(
>          ds_put_format(match, "%s", op->lrp_networks.ea_s);
>      }
>      ds_put_format(match, " && inport == %s", op->json_key);
> -    if (consider_l3dgw_port_is_centralized(op)) {
> +    bool l3dgw_port = consider_l3dgw_port_is_centralized(op);
> +    if (l3dgw_port && vector_is_empty(&op->od->distrubuted_lbs)) {
>          ds_put_format(match, " && is_chassis_resident(%s)",
>                        op->cr_port->json_key);
>      }
> @@ -15830,7 +15931,8 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
>                                        struct ovn_port *)->cr_port->json_key;
>              }
>  
> -            if (add_chassis_resident_check) {
> +            if (add_chassis_resident_check
> +                && vector_is_empty(&op->od->distrubuted_lbs)) {
>                  ds_put_format(match, " && is_chassis_resident(%s)",
>                                json_key);
>              }
> diff --git a/northd/northd.h b/northd/northd.h
> index 8f865e8b3..868721aca 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -106,6 +106,7 @@ struct ovn_datapath *
>  ovn_datapath_find_by_key(struct hmap *datapaths, uint32_t dp_key);
>  
>  bool od_has_lb_vip(const struct ovn_datapath *od);
> +bool lr_has_distributed_lb(const struct ovn_datapath *od);
>  
>  /* List of routing and routing-related protocols which
>   * OVN is capable of redirecting from LRP to specific LSP. */
> @@ -448,6 +449,10 @@ struct ovn_datapath {
>      /* Map of ovn_port objects belonging to this datapath.
>       * This map doesn't include derived ports. */
>      struct hmap ports;
> +
> +    /* A set of distributed load balancers associated
> +     * with this datapath. The datapath must be a router. */
> +    struct vector distrubuted_lbs;
>  };
>  
>  const struct ovn_datapath *ovn_datapath_find(const struct hmap *datapaths,
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index b7b5b5c40..0ed0a407e 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -2288,13 +2288,15 @@
>          <p>
>            Maps from endpoint IP to a colon-separated pair of logical port 
> name
>            and source IP,
> -          e.g. <code><var>port_name</var>:<var>sourc_ip</var></code> for 
> IPv4.
> +          e.g. <code><var>port_name</var>:<var>source_ip</var></code> for 
> IPv4.
>            Health checks are sent to this port with the specified source IP.
>            For IPv6 square brackets must be used around IP address, e.g:
> -          <code><var>port_name</var>:<var>[sourc_ip]</var></code>
> +          <code><var>port_name</var>:<var>[source_ip]</var></code>
>            Remote endpoint:
>            Specify :target_zone_name at the end of the above syntax to create
>            remote health checks in a specific zone.
> +          For distributed load balancers - ip_port_mappings is required.
> +          In the absence of health checks - source_ip is optional.
>          </p>
>  
>          <p>
> @@ -2497,6 +2499,15 @@ or
>          traffic may be dropped in scenarios where we have different chassis
>          for each DGP. This option is set to <code>false</code> by default.
>        </column>
> +
> +      <column name="options" key="distributed">
> +        When enabled on a load balancer associated with a distributed gateway
> +        router, load balancing operations are performed locally on chassis
> +        hosting backends. In this mode, traffic is balanced to backends
> +        located on the same chassis where the processing occurs. Enabling
> +        this option requires configuring <ref column="ip_port_mappings"/>
> +        for proper operation.
> +      </column>
>      </group>
>    </table>
>  
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 11bbb211d..6970a19ab 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -17808,8 +17808,12 @@ AT_SETUP([ip_port_mappings validation])
>  ovn_start
>  
>  # ip_port_mappings syntax: ip:lport_name:src_ip:<az_name>(for remote lports)
> +# For distributed load balancer src_ip is optional when health checks are 
> not configured.
>  
>  check ovn-nbctl ls-add ls1
> +check ovn-nbctl lr-add lr1
> +
> +ovn-appctl -t ovn-northd vlog/disable-rate-limit
>  
>  check ovn-nbctl lb-add lb1_ipv4 1.1.1.1:80 
> 192.168.0.1:10880,192.168.0.2:10880,192.168.0.3:10880
>  AT_CHECK([ovn-nbctl --wait=sb \
> @@ -17879,6 +17883,54 @@ check ovn-nbctl set load_balancer lb1_ipv4 
> ip_port_mappings:192.168.0.1=lport1:1
>  check_row_count sb:Service_Monitor 0
>  
>  OVS_WAIT_UNTIL([grep "Empty AZ name specified" northd/ovn-northd.log])
> +
> +check ovn-nbctl lb-del lb1_ipv4
> +
> +# Check correct setup of distributed load balancers.
> +echo > northd/ovn-northd.log
> +check ovn-nbctl lb-add lb_distubuted 1.1.1.1:80 
> 192.168.0.1:10880,192.168.0.2:10880
> +check ovn-nbctl lr-lb-add lr1 lb_distubuted
> +check ovn-nbctl set load_balancer lb_distubuted options:distributed=true
> +
> +OVS_WAIT_UNTIL([grep "Proper ip_port_mappings" northd/ovn-northd.log])
> +echo > northd/ovn-northd.log
> +
> +# Ensure proper ip_port_mappings configuration for all health checks is 
> required for distributed
> +# load balancer functionality. src_ip is optional when health checks are not 
> configured.
> +ovn-sbctl lflow-list lr1 > lr1_lflow
> +AT_CHECK([cat lr1_lflow | grep lr_in_dnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 1.1.1.1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80), 
> action=(ct_lb_mark(backends=192.168.0.1:10880,192.168.0.2:10880);)
> +])
> +ovn-nbctl list load_balancer
> +
> +check ovn-nbctl set load_balancer lb_distubuted 
> ip_port_mappings:192.168.0.1=lport1
> +OVS_WAIT_UNTIL([grep "Proper ip_port_mappings" northd/ovn-northd.log])
> +echo > northd/ovn-northd.log
> +ovn-sbctl lflow-list lr1 > lr1_lflow
> +AT_CHECK([cat lr1_lflow | grep lr_in_dnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 1.1.1.1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80), 
> action=(ct_lb_mark(backends=192.168.0.1:10880,192.168.0.2:10880);)
> +])
> +
> +check ovn-nbctl set load_balancer lb_distubuted 
> ip_port_mappings:192.168.0.2=lport2
> +ovn-sbctl lflow-list lr1 > lr1_lflow
> +AT_CHECK([cat lr1_lflow | grep lr_in_dnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 1.1.1.1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80), 
> action=(ct_lb_mark_local(backends="lport1":192.168.0.1:10880,"lport2":192.168.0.2:10880);)
> +])
> +echo > northd/ovn-northd.log
> +
> +# Check if health check is configured, ip_port_mappings must be provided.
> +AT_CHECK([ovn-nbctl --wait=sb \
> +          -- --id=@hc create Load_Balancer_Health_Check vip="1.1.1.1\:80" \
> +             options:failure_count=100 \
> +          -- add Load_Balancer lb_distubuted health_check @hc | uuidfilt], 
> [0], [<0>
> +])
> +
> +ovn-sbctl lflow-list lr1 > lr1_lflow
> +OVS_WAIT_UNTIL([grep "Expected ':' separator for:" northd/ovn-northd.log])
> +AT_CHECK([cat lr1_lflow | grep lr_in_dnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 1.1.1.1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80), 
> action=(drop;)
> +])
> +
>  AT_CLEANUP
>  ])
>  
> @@ -17940,3 +17992,181 @@ AT_CHECK([grep 'ls_in_l2_unknown' lflows | 
> ovn_strip_lflows], [0], [dnl
>  
>  AT_CLEANUP
>  ])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([Distributed lb: logical-flow test])
> +ovn_start
> +
> +net_add n1
> +sim_add hv1
> +as hv1
> +ovs-vsctl add-br br-phys
> +ovn_attach n1 br-phys 192.168.0.1
> +ovs-vsctl add-port br-int lport1 -- set interface lport1 
> external_ids:iface-id=lport1
> +
> +net_add n2
> +sim_add hv2
> +as hv2
> +check ovs-vsctl add-br br-phys
> +ovn_attach n1 br-phys 192.168.0.2
> +ovs-vsctl add-port br-int lport2 -- set interface lport2 
> external_ids:iface-id=lport2
> +
> +# If load balancer becomes distributed, it operates in a distributed manner 
> on
> +# separate compute nodes and balances traffic to local backends (located on 
> that compute node).
> +# Test topology: one switch, two hypervisors, to verify that OpenFlow groups 
> are created
> +# only from local backends. Logical_switch is connected to a router, a 
> distributed load
> +# balancer is added on Logical_Router, and router is connected to a switch 
> which is
> +# connected to the physical network.
> +check ovn-nbctl ls-add outside
> +
> +check ovn-nbctl lsp-add outside outside \
> +      -- lsp-set-addresses outside unknown \
> +      -- lsp-set-type outside localnet
> +
> +check ovn-nbctl --wait=sb set Logical_Switch_Port outside tag_request=2
> +
> +check ovn-nbctl lsp-add outside outside-down \
> +      -- lsp-set-type outside-down router \
> +      -- lsp-set-addresses outside-down router \
> +      -- lsp-set-options outside-down router-port=lr1-up
> +
> +check ovn-nbctl lr-add lr1 \
> +      -- lrp-add lr1 lr1-up 11:11:11:11:11:11 169.254.0.1/24 \
> +      -- lrp-add lr1 lr1-down 12:12:12:12:12:12 192.168.0.1/24
> +
> +check ovn-nbctl ls-add ls1 \
> +      -- lsp-add ls1 lport1 \
> +      -- lsp-set-addresses lport1 "13:13:13:13:13:13 192.168.0.101" \
> +      -- lsp-add ls1 lport2 \
> +      -- lsp-set-addresses lport2 "14:14:14:14:14:14 192.168.0.102"
> +
> +check ovn-nbctl lsp-add ls1 ls1-up \
> +      -- lsp-set-type ls1-up router \
> +      -- lsp-set-addresses ls1-up router \
> +      -- lsp-set-options ls1-up router-port=lr1-down
> +
> +check ovn-nbctl --wait=sb sync
> +wait_for_ports_up
> +
> +check ovn-nbctl ha-chassis-group-add gateway
> +check ovn-nbctl ha-chassis-group-add-chassis gateway test 1
> +ha_g_uuid=$(fetch_column nb:HA_Chassis_Group _uuid name=gateway)
> +lr1_up_uuid=$(fetch_column nb:Logical_Router_Port _uuid name=lr1-up)
> +check ovn-nbctl set logical_router_port $lr1_up_uuid 
> ha_chassis_group=$ha_g_uuid
> +check ovn-nbctl --wait=hv sync
> +
> +check ovn-nbctl lb-add lb1 1.1.1.1:80 192.168.0.101:10880,192.168.0.102:10880
> +check ovn-nbctl set Load_Balancer lb1 
> ip_port_mappings:192.168.0.101=lport1:192.168.0.199
> +check ovn-nbctl set Load_Balancer lb1 
> ip_port_mappings:192.168.0.102=lport2:192.168.0.199
> +check ovn-nbctl lr-lb-add lr1 lb1
> +
> +ovn-sbctl lflow-list lr1 > lr1_lflows_before
> +ovn-sbctl lflow-list outside > outside_lflows_before
> +
> +AT_CHECK([cat outside_lflows_before | grep ls_in_l2_lkup | grep priority=50 
> | ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_l2_lkup      ), priority=50   , match=(eth.dst == 
> 11:11:11:11:11:11 && is_chassis_resident("cr-lr1-up")), action=(outport = 
> "outside-down"; output;)
> +])
> +
> +AT_CHECK([cat lr1_lflows_before | grep lr_in_ip_input | grep priority=90 | 
> grep 169.254.0.1 | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "lr1-up" 
> && arp.op == 1 && arp.tpa == 169.254.0.1 && arp.spa == 169.254.0.0/24 && 
> is_chassis_resident("cr-lr1-up")), action=(eth.dst = eth.src; eth.src = 
> xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 
> xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; 
> output;)
> +  table=??(lr_in_ip_input     ), priority=90   , match=(ip4.dst == 
> 169.254.0.1 && icmp4.type == 8 && icmp4.code == 0), action=(ip4.dst <-> 
> ip4.src; ip.ttl = 255; icmp4.type = 0; flags.loopback = 1; next; )
> +])
> +
> +AT_CHECK([cat lr1_lflows_before | grep lr_in_admission | grep  priority=50 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.dst == 
> 11:11:11:11:11:11 && inport == "lr1-up" && is_chassis_resident("cr-lr1-up")), 
> action=(xreg0[[0..47]] = 11:11:11:11:11:11; next;)
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.dst == 
> 12:12:12:12:12:12 && inport == "lr1-down"), action=(xreg0[[0..47]] = 
> 12:12:12:12:12:12; next;)
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.mcast && inport 
> == "lr1-down"), action=(xreg0[[0..47]] = 12:12:12:12:12:12; next;)
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.mcast && inport 
> == "lr1-up"), action=(xreg0[[0..47]] = 11:11:11:11:11:11; next;)
> +])
> +
> +AT_CHECK([cat lr1_lflows_before | grep lr_in_dnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 1.1.1.1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80 && 
> is_chassis_resident("cr-lr1-up")), 
> action=(ct_lb_mark(backends=192.168.0.101:10880,192.168.0.102:10880);)
> +])
> +
> +AT_CHECK([cat lr1_lflows_before | grep lr_out_undnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 192.168.0.101 && tcp.src == 10880) || (ip4.src == 192.168.0.102 && tcp.src == 
> 10880)) && (inport == "lr1-up" || outport == "lr1-up") && 
> is_chassis_resident("cr-lr1-up")), action=(ct_dnat;)
> +])
> +
> +AT_CHECK([cat lr1_lflows_before | grep lr_in_gw_redirect | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_gw_redirect  ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_gw_redirect  ), priority=200  , match=(ip4 && ((ip4.src == 
> 192.168.0.101 && tcp.src == 10880) || (ip4.src == 192.168.0.102 && tcp.src == 
> 10880)) && outport == "lr1-up"), action=(outport = "cr-lr1-up"; next;)
> +  table=??(lr_in_gw_redirect  ), priority=50   , match=(outport == 
> "lr1-up"), action=(outport = "cr-lr1-up"; next;)
> +])
> +
> +# OVN controller currently does not recalculate local datapaths when the 
> 'ha_chassis_group'
> +# changes, so we reboot it.
> +as hv1
> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
> +start_daemon ovn-controller
> +wait_for_ports_up
> +
> +AT_CHECK([ovn-appctl -t ovn-controller debug/dump-local-datapaths], [0], [dnl
> +Local datapaths:
> +Datapath: lr1, type: router
> +Datapath: ls1, type: switch
> +])
> +
> +check ovn-nbctl set load_balancer lb1 options:distributed=true
> +check ovn-nbctl --wait=hv sync
> +
> +# Check that flows for router's distributed gw port are now distributed.
> +ovn-sbctl lflow-list outside > outside_lflows_after
> +AT_CHECK([cat outside_lflows_after | grep ls_in_l2_lkup | grep priority=50 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_l2_lkup      ), priority=50   , match=(eth.dst == 
> 11:11:11:11:11:11), action=(outport = "outside-down"; output;)
> +])
> +
> +ovn-sbctl lflow-list lr1 > lr1_lflows_after
> +
> +AT_CHECK([cat lr1_lflows_after | grep lr_in_ip_input | grep priority=90 | 
> grep 169.254.0.1 | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_ip_input     ), priority=90   , match=(inport == "lr1-up" 
> && arp.op == 1 && arp.tpa == 169.254.0.1 && arp.spa == 169.254.0.0/24), 
> action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply 
> */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport 
> = inport; flags.loopback = 1; output;)
> +  table=??(lr_in_ip_input     ), priority=90   , match=(ip4.dst == 
> 169.254.0.1 && icmp4.type == 8 && icmp4.code == 0), action=(ip4.dst <-> 
> ip4.src; ip.ttl = 255; icmp4.type = 0; flags.loopback = 1; next; )
> +])
> +
> +AT_CHECK([cat lr1_lflows_after | grep lr_in_admission | grep priority=50 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.dst == 
> 11:11:11:11:11:11 && inport == "lr1-up"), action=(xreg0[[0..47]] = 
> 11:11:11:11:11:11; next;)
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.dst == 
> 12:12:12:12:12:12 && inport == "lr1-down"), action=(xreg0[[0..47]] = 
> 12:12:12:12:12:12; next;)
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.mcast && inport 
> == "lr1-down"), action=(xreg0[[0..47]] = 12:12:12:12:12:12; next;)
> +  table=??(lr_in_admission    ), priority=50   , match=(eth.mcast && inport 
> == "lr1-up"), action=(xreg0[[0..47]] = 11:11:11:11:11:11; next;)
> +])
> +
> +# Check that load balancer flows are now also distributed.
> +AT_CHECK([cat lr1_lflows_after | grep lr_in_dnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new && !ct.rel && 
> ip4 && ip4.dst == 1.1.1.1 && reg1[[16..23]] == 6 && reg1[[0..15]] == 80), 
> action=(ct_lb_mark_local(backends="lport1":192.168.0.101:10880,"lport2":192.168.0.102:10880);)
> +])
> +
> +AT_CHECK([cat lr1_lflows_after | grep lr_out_undnat | grep priority=120 | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
> 192.168.0.101 && tcp.src == 10880) || (ip4.src == 192.168.0.102 && tcp.src == 
> 10880)) && (inport == "lr1-up" || outport == "lr1-up")), action=(ct_dnat;)
> +])
> +
> +# Check that distributed load balancers do not redirect traffic to gateway 
> port.
> +AT_CHECK([cat lr1_lflows_after | grep lr_in_gw_redirect | ovn_strip_lflows], 
> [0], [dnl
> +  table=??(lr_in_gw_redirect  ), priority=0    , match=(1), action=(next;)
> +  table=??(lr_in_gw_redirect  ), priority=200  , match=(ip4 && ((ip4.src == 
> 192.168.0.101 && tcp.src == 10880) || (ip4.src == 192.168.0.102 && tcp.src == 
> 10880)) && outport == "lr1-up"), action=(outport = "lr1-up"; next;)
> +  table=??(lr_in_gw_redirect  ), priority=50   , match=(outport == 
> "lr1-up"), action=(outport = "cr-lr1-up"; next;)
> +])
> +
> +# Check that external switch has been added to local datapaths on compute 
> nodes
> +# when 'distributed' option is enabled on load balancer.
> +as hv1
> +AT_CHECK([ovn-appctl -t ovn-controller debug/dump-local-datapaths], [0], [dnl
> +Local datapaths:
> +Datapath: outside, type: switch
> +Datapath: lr1, type: router
> +Datapath: ls1, type: switch
> +])
> +
> +as hv1
> +# Verify that OpenFlow groups were created only for local backends.
> +AT_CHECK([ovs-ofctl dump-groups br-int | sed -e 
> 's/table=[[0-9]]*/table=<cleared>/'], [0], [dnl
> +NXST_GROUP_DESC reply (xid=0x2):
> + 
> group_id=1,type=select,selection_method=dp_hash,bucket=bucket_id:0,weight:100,actions=ct(commit,table=<cleared>,zone=NXM_NX_REG11[[0..15]],nat(dst=192.168.0.101:10880),exec(load:0x1->NXM_NX_CT_MARK[[1]]))
> +])
> +
> +as hv2
> +AT_CHECK([ovs-ofctl dump-groups br-int | sed -e 
> 's/table=[[0-9]]*/table=<cleared>/'], [0], [dnl
> +NXST_GROUP_DESC reply (xid=0x2):
> + 
> group_id=1,type=select,selection_method=dp_hash,bucket=bucket_id:1,weight:100,actions=ct(commit,table=<cleared>,zone=NXM_NX_REG11[[0..15]],nat(dst=192.168.0.102:10880),exec(load:0x1->NXM_NX_CT_MARK[[1]]))
> +])
> +
> +AT_CLEANUP
> +])
> \ No newline at end of file
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 8e356df6f..5e2ae9ee9 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -18484,3 +18484,86 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
> patch-.*/d
>  /connection dropped.*/d"])
>  AT_CLEANUP
>  ])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Distributed lb: system test])
> +AT_KEYWORDS([ovnlb])
> +
> +# Simple test for basic functionality verification
> +# topology:
> +# client - br-ext - br-int - outside-switch - lr - backend
> +# test case:
> +# 1. create a centralized load balancer, specifying gateway chassis for 
> router.
> +# 2. enable distributed option on load balancer.
> +# 3. change the gateway to a non-existent one - we expect the distributed 
> load balancer to continue working.
> +
> +CHECK_CONNTRACK()
> +CHECK_CONNTRACK_NAT()
> +
> +ovn_start
> +OVS_TRAFFIC_VSWITCHD_START()
> +ADD_BR([br-int])
> +ADD_BR([br-ext])
> +
> +check ovs-ofctl add-flow br-ext action=normal
> +# Set external-ids in br-int needed for ovn-controller
> +check ovs-vsctl \
> +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> +        -- set Open_vSwitch . 
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> +        -- set bridge br-int fail-mode=secure 
> other-config:disable-in-band=true \
> +        -- set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext
> +
> +# Start ovn-controller
> +start_daemon ovn-controller
> +
> +check ovn-nbctl lr-add lr
> +check ovn-nbctl ls-add internal
> +check ovn-nbctl ls-add public
> +
> +check ovn-nbctl lrp-add lr lr-pub 00:00:01:01:02:03 192.168.100.1/24 
> 1000::1/64
> +check ovn-nbctl lsp-add  public pub-lr -- set Logical_Switch_Port pub-lr \
> +    type=router options:router-port=lr-pub addresses=\"00:00:01:01:02:03\"
> +
> +check ovn-nbctl lrp-add lr lr-internal 00:00:01:01:02:04 192.168.200.1/24 
> 2000::1/64
> +check ovn-nbctl lsp-add internal internal-lr -- set Logical_Switch_Port 
> internal-lr \
> +    type=router options:router-port=lr-internal 
> addresses=\"00:00:01:01:02:04\"
> +
> +check ovn-nbctl lsp-add internal server -- lsp-set-addresses server "unknown"
> +
> +check ovn-nbctl lsp-add public ln_port \
> +                -- lsp-set-addresses ln_port unknown \
> +                -- lsp-set-type ln_port localnet \
> +                -- lsp-set-options ln_port network_name=phynet
> +
> +check ovn-nbctl set logical_router lr options:chassis=hv1
> +
> +check ovn-nbctl lb-add lb1 192.168.100.20:80 192.168.200.10:10880
> +check ovn-nbctl lr-lb-add lr lb1
> +check ovn-nbctl --wait=hv sync
> +
> +ADD_NAMESPACES(client)
> +ADD_VETH(client, client, br-ext, "192.168.100.10/24", "f0:00:00:01:02:03", \
> +         "192.168.100.1")
> +
> +ADD_NAMESPACES(server)
> +ADD_VETH(server, server, br-int, "192.168.200.10/24", "f0:00:0f:01:02:03", \
> +         "192.168.200.1")
> +
> +NETNS_DAEMONIZE([server], [nc -l -u 192.168.200.10 10880 > /dev/null], 
> [serverv4.pid])
> +
> +NS_CHECK_EXEC([client], [nc -z -u 192.168.100.20 80], [ignore], [ignore], 
> [ignore])
> +
> +check ovn-nbctl set load_balancer lb1 options:distributed=true
> +check ovn-nbctl set Load_Balancer lb1 ip_port_mappings:192.168.200.10=lport1
> +check ovn-nbctl --wait=hv sync
> +
> +check ovn-nbctl set logical_router lr options:chassis=hv2
> +
> +NS_CHECK_EXEC([client], [nc -z -u 192.168.100.20 80], [ignore], [ignore], 
> [ignore])
> +
> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> +/connection dropped.*/d"])
> +AT_CLEANUP
> +])
> \ No newline at end of file
> diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
> index 9d9f915da..f288c8a8f 100644
> --- a/utilities/ovn-trace.c
> +++ b/utilities/ovn-trace.c
> @@ -3590,6 +3590,8 @@ trace_actions(const struct ovnact *ovnacts, size_t 
> ovnacts_len,
>          case OVNACT_CT_STATE_SAVE:
>              execute_ct_save_state(ovnact_get_CT_STATE_SAVE(a), uflow, super);
>              break;
> +        case OVNACT_CT_LB_MARK_LOCAL:
> +            break;
>          }
>      }
>      ofpbuf_uninit(&stack);

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

Reply via email to