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)

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.
    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.
    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).

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);
-- 
2.48.1

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

Reply via email to