On Wed, Feb 4, 2026 at 12:08 PM Alexandra Rukomoinikova
<[email protected]> wrote:

> 1) Added new option "distributed" for load balancers.
>    With this feature, balancers will work distributedly across compute
> nodes,
>    balancing to only local backends (excluded east-west traffic)
>
> 2) If load balancer is running on a router with dgp, the router will no
>    longer be centralized on gateway - this means that access to physical
> network will also be available from hosts where the distributed balancer
> backends are located.
>
> 3) Configuration requirement for distributed load balancers:
>     1) ip_port_mapping must be specified
>     2) Balancing in underlay fabric between hosts with backends
>
> Example:
> Load Balancer: lb1 with VIP 1.1.1.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:
> 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.
>
> Suggested-by: Vladislav Odintsov <[email protected]>
> Signed-off-by: Alexandra Rukomoinikova <[email protected]>
> --
> v6 --> v7: fixed Ales comments
> ---
>

Hi Alexandra,

thank you for v7. I have some small nits down below.



>  NEWS                      |   3 +
>  TODO.rst                  |   7 +
>  northd/en-lb-data.c       |   9 +
>  northd/en-lb-data.h       |   3 +
>  northd/en-lr-stateful.c   |   3 +
>  northd/en-lr-stateful.h   |   2 +
>  northd/lb.c               | 105 ++++++-----
>  northd/lb.h               |   7 +-
>  northd/northd.c           | 186 ++++++++++++-------
>  northd/northd.h           |  17 ++
>  ovn-nb.xml                |  21 ++-
>  ovn-sb.xml                |  11 ++
>  tests/multinode-macros.at |  15 ++
>  tests/multinode.at        | 144 +++++++++++++++
>  tests/ovn-northd.at       | 370 ++++++++++++++++++++++++++++++++++++--
>  tests/server.py           |   7 +-
>  16 files changed, 782 insertions(+), 128 deletions(-)
>
> diff --git a/NEWS b/NEWS
> index 2a2b5e12d..bb550fe59 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -98,6 +98,9 @@ Post v25.09.0
>       reserving an unused IP from the backend's subnet. This change allows
>       using LRP IPs directly, eliminating the need to reserve additional
> IPs
>       per backend port.
> +  - Add "distributed" option for load balancer, that forces traffic to be
> +    routed only to backend instances running locally on the same chassis
> +    it arrives on.
>
>  OVN v25.09.0 - xxx xx xxxx
>  --------------------------
> diff --git a/TODO.rst b/TODO.rst
> index 9f5e0976d..75bd2d13b 100644
> --- a/TODO.rst
> +++ b/TODO.rst
> @@ -184,6 +184,13 @@ OVN To-do List
>      OVN_ENABLE_INTERCONNECT=true and potentially more of the CI lanes
>      ovn-kubernetes/ovn-kubernetes defines in its GitHub project.
>
> +* Distributed Load Balancers
> +
> +  * Currently, this feature works when in OVN topology physical network is
> +    connected via a switch directly connected to the DGR.
> +    We need to make it work for a topology with two levels of routers:
> +    first the GW router, then the DGR.
> +
>  ==============
>  OVN Deprecation plan
>  ==============
> diff --git a/northd/en-lb-data.c b/northd/en-lb-data.c
> index 6d52d465e..a64c06bfd 100644
> --- a/northd/en-lb-data.c
> +++ b/northd/en-lb-data.c
> @@ -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->has_distributed_lb |= lb->is_distributed;
>              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->has_distributed_lb |= lb->is_distributed;
>          } else {
>              /* Load balancer updated. */
>              bool health_checks = lb->health_checks;
> @@ -189,11 +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->is_distributed;
>              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->has_distributed_lb |= lb->is_distributed;
>
>              /* Determine the inserted and deleted vips and store them in
>               * the tracked data. */
> @@ -226,6 +230,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->is_distributed) {
> +                /* If distributed_mode is updated trigger a full
> recompute. */
> +                return EN_UNHANDLED;
> +            }
>          }
>      }
>
> @@ -687,6 +695,7 @@ handle_od_lb_changes(struct nbrec_load_balancer
> **nbrec_lbs,
>                                                                lb_uuid);
>                  ovs_assert(lb);
>                  trk_lb_data->has_routable_lb |= lb->routable;
> +                trk_lb_data->has_distributed_lb |= lb->is_distributed;
>              }
>          }
>
> diff --git a/northd/en-lb-data.h b/northd/en-lb-data.h
> index 1da087656..90e85b8c4 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 any lb (in the tracked data) has 'distibuted' flag
> set. */
> +    bool has_distributed_lb;
>  };
>
>  /* Datapath (logical switch) to lb/lbgrp association data. */
> diff --git a/northd/en-lr-stateful.c b/northd/en-lr-stateful.c
> index 212c0641c..5a738f4c3 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 = od->is_distributed;
>          }
>
>          return EN_HANDLED_UPDATED;
> @@ -527,7 +528,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 = od->is_distributed;
>
>      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 146f768c3..3b0c54521 100644
> --- a/northd/en-lr-stateful.h
> +++ b/northd/en-lr-stateful.h
> @@ -59,6 +59,8 @@ 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 cc5cc1ea5..a2bb37b45 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,19 +101,23 @@ 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 :
> - * "ip:logical_port:src_ip[:az_name]".
> + * Parses ip_port_mappings in the format :
> + * "ip:logical_port[:src_ip][:az_name]".
> + * src_ip parameter is optional when distributed mode is enabled,
> + * without health checks configured.
>   * 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.
>   */
>  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)
>  {
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
>      struct ds key = DS_EMPTY_INITIALIZER;
> +    bool allow_without_src_ip = lb->is_distributed
> +                                && !lb_vip_nb->lb_health_check;
>
>      for (size_t j = 0; j < vector_len(&lb_vip->backends); j++) {
>          const struct ovn_lb_backend *backend =
> @@ -127,26 +131,37 @@ ovn_lb_vip_backends_health_check_init(const struct
> ovn_northd_lb *lb,
>              continue;
>          }
>
> +        struct ovn_northd_lb_backend *backend_nb = NULL;
>          char *svc_mon_src_ip = NULL;
>          char *az_name = NULL;
> -        bool is_remote = false;
> -        char *port_name = xstrdup(s);
>          char *src_ip = NULL;
> +        char *port_name = NULL;
> +        char *first_colon = NULL;
> +        bool is_remote = false;
>
> -        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_RL(&rl, "Empty port name in distributed mode
> for IP %s",
> +                             ds_cstr(&key));
> +                goto cleanup;
> +            }
> +            is_remote = false;
> +            goto init_backend;
> +        } else if (!first_colon) {
> +            VLOG_WARN_RL(&rl, "Expected ':' separator for: %s",
> port_name);
> +            goto cleanup;
>          }
> -        *first_colon = '\0';
>
> +        *first_colon = '\0';
>          if (first_colon[1] == '[') {
>              /* IPv6 case - format: port:[ipv6]:az or port:[ipv6] */
>              char *ip_end = strchr(first_colon + 2, ']');
>              if (!ip_end) {
> -                VLOG_WARN("Malformed IPv6 address in backend %s", s);
> -                free(port_name);
> -                continue;
> +                VLOG_WARN_RL(&rl, "Malformed IPv6 address in backend %s",
> s);
> +                goto cleanup;
>              }
>
>              src_ip = first_colon + 2;
> @@ -155,10 +170,9 @@ ovn_lb_vip_backends_health_check_init(const struct
> ovn_northd_lb *lb,
>              if (ip_end[1] == ':') {
>                  az_name = ip_end + 2;
>                  if (!*az_name) {
> -                    VLOG_WARN("Empty AZ name specified for backend %s",
> -                              port_name);
> -                    free(port_name);
> -                    continue;
> +                    VLOG_WARN_RL(&rl, "Empty AZ name specified for
> backend %s",
> +                                 port_name);
> +                    goto cleanup;
>                  }
>                  is_remote = true;
>              }
> @@ -170,34 +184,34 @@ ovn_lb_vip_backends_health_check_init(const struct
> ovn_northd_lb *lb,
>                  *az_colon = '\0';
>                  az_name = az_colon + 1;
>                  if (!*az_name) {
> -                    VLOG_WARN("Empty AZ name specified for backend %s",
> -                              port_name);
> -                    free(port_name);
> -                    continue;
> +                    VLOG_WARN_RL(&rl, "Empty AZ name specified for
> backend %s",
> +                                 port_name);
> +                    goto 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");
> +            VLOG_WARN_RL(&rl, "Invalid svc mon src IP %s", src_ip ?
> src_ip : "NULL");
> +            goto 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->remote_backend = is_remote;
> -            backend_nb->svc_mon_lrp = NULL;
> -        }
> +init_backend:
> +        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->remote_backend = is_remote;
> +        backend_nb->svc_mon_lrp = NULL;
> +        backend_nb->distributed_backend = lb->is_distributed;
> +cleanup:
>          free(port_name);
>      }
>
> @@ -368,6 +382,9 @@ ovn_northd_lb_init(struct ovn_northd_lb *lb,
>          lb->hairpin_snat_ip = xstrdup(snat_ip);
>      }
>
> +    lb->is_distributed = smap_get_bool(&nbrec_lb->options, "distributed",
> +                                       false);
> +
>      sset_init(&lb->ips_v4);
>      sset_init(&lb->ips_v6);
>      struct smap_node *node;
> @@ -407,8 +424,8 @@ 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_vip_nb->lb_health_check || lb->is_distributed) {
> +            ovn_lb_vip_backends_ip_port_mappings_init(lb, lb_vip,
> lb_vip_nb);
>          }
>      }
>
> diff --git a/northd/lb.h b/northd/lb.h
> index fca35fa6d..c1f0c95da 100644
> --- a/northd/lb.h
> +++ b/northd/lb.h
> @@ -74,8 +74,12 @@ struct ovn_northd_lb {
>      /* Indicates if the load balancer has health checks configured. */
>      bool health_checks;
>
> -    char *hairpin_snat_ip;
> +    /* Indicates if distributed option is enabled for load balancer. */
> +    bool is_distributed;
> +
>      bool use_stateless_nat;
> +
> +    char *hairpin_snat_ip;
>  };
>
>  /* ovn-northd specific backend information. */
> @@ -91,6 +95,7 @@ struct ovn_northd_lb_backend {
>      bool health_check;
>       /* Set to true if port does not locate in local AZ. */
>      bool remote_backend;
> +    bool distributed_backend;
>      /* Logical port to which the ip belong to. */
>      char *logical_port;
>      /* Source IP address to be used for service monitoring. */
> diff --git a/northd/northd.c b/northd/northd.c
> index b4bb4ba6d..2df205ec2 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -566,6 +566,7 @@ ovn_datapath_create(struct hmap *datapaths, const
> struct uuid *key,
>      od->localnet_ports = VECTOR_EMPTY_INITIALIZER(struct ovn_port *);
>      od->lb_with_stateless_mode = false;
>      od->ipam_info_initialized = false;
> +    od->is_distributed = false;
>      od->tunnel_key = sdp->sb_dp->tunnel_key;
>      init_mcast_info_for_datapath(od);
>      return od;
> @@ -3335,6 +3336,36 @@ ovn_lb_svc_create(struct ovsdb_idl_txn *ovnsb_txn,
>      }
>  }
>
> +static bool
> +backend_is_available(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)
> +{
> +    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);
> +
> +    return (mon_info->sbrec_mon->status &&
> +           strcmp(mon_info->sbrec_mon->status, "online")) ?
> +           false : true;
> +}
> +
>  static bool
>  build_lb_vip_actions(const struct ovn_northd_lb *lb,
>                       const struct ovn_lb_vip *lb_vip,
> @@ -3360,55 +3391,53 @@ 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->is_distributed
> +                  ? "ct_lb_mark_local(backends="
> +                  : "ct_lb_mark(backends=");
>
> +    if (lb_vip_nb->lb_health_check || lb->is_distributed) {
>          size_t i = 0;
>          size_t n_active_backends = 0;
>          const struct ovn_lb_backend *backend;
>          VECTOR_FOR_EACH_PTR (&lb_vip->backends, backend) {
>              struct ovn_northd_lb_backend *backend_nb =
>                  &lb_vip_nb->backends_nb[i++];
> +            bool ipv6_backend = !IN6_IS_ADDR_V4MAPPED(&backend->ip);
>
> -            if (!backend_nb->health_check) {
> +            /* XXX: Remove these checks: by changing the iteration
> +             * only for selected backends. */
> +            if (lb_vip_nb->lb_health_check &&
> +                !backend_nb->health_check) {
>                  continue;
>              }
>
> -            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) {
> +            if (lb->is_distributed &&
> +                !backend_nb->distributed_backend) {
>                  continue;
>              }
>
> -            ovs_assert(mon_info->sbrec_mon);
> -            if (mon_info->sbrec_mon->status &&
> -                    strcmp(mon_info->sbrec_mon->status, "online")) {
> +            if (backend_nb->health_check &&
> +                !backend_is_available(lb,
> +                                      backend,
> +                                      backend_nb,
> +                                      svc_mons_data)) {
>                  continue;
>              }
>
> -            n_active_backends++;
> -            bool ipv6 = !IN6_IS_ADDR_V4MAPPED(&backend->ip);
> -            ds_put_format(action, ipv6 ? "[%s]:%"PRIu16"," :
> "%s:%"PRIu16",",
> +            if (backend_nb->distributed_backend) {
> +                ds_put_format(action, "\"%s\":",
> backend_nb->logical_port);
> +            }
> +            ds_put_format(action,
> +                          ipv6_backend ? "[%s]:%"PRIu16"," :
> "%s:%"PRIu16",",
>                            backend->ip_str, backend->port);
> +            n_active_backends++;
>          }
>          ds_chomp(action, ',');
>
>          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);
> +        ds_put_format(action, "%s", lb_vip_nb->backend_ips);
>      }
>
>      if (reject) {
> @@ -3445,6 +3474,19 @@ build_lb_vip_actions(const struct ovn_northd_lb *lb,
>      return reject;
>  }
>
> +static inline void
> +handle_od_lb_datapath_modes(struct ovn_datapath *od,
> +                            struct ovn_lb_datapaths *lb_dps)
> +{
> +    if (od->nbs && od->lb_with_stateless_mode) {
> +        hmapx_add(&lb_dps->ls_lb_with_stateless_mode, od);
> +    }
> +
> +    if (od->nbr && lb_dps->lb->is_distributed) {
> +        od->is_distributed = true;
> +    }
> +}
> +
>  static void
>  build_lb_datapaths(const struct hmap *lbs, const struct hmap *lb_groups,
>                     struct ovn_datapaths *ls_datapaths,
> @@ -3487,9 +3529,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,
> ods_size(ls_datapaths));
> -            if (od->lb_with_stateless_mode) {
> -                hmapx_add(&lb_dps->ls_lb_with_stateless_mode, od);
> -            }
> +            handle_od_lb_datapath_modes(od, lb_dps);
>          }
>
>          for (size_t i = 0; i < od->nbs->n_load_balancer_group; i++) {
> @@ -3523,6 +3563,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,
> ods_size(lr_datapaths));
> +            handle_od_lb_datapath_modes(od, lb_dps);
>          }
>      }
>
> @@ -3875,6 +3916,7 @@ sync_pb_for_lrp(struct ovn_port *op,
>          smap_add(&new, "distributed-port", op->primary_port->key);
>
>          bool always_redirect =
> +            !lr_stateful_rec->has_distributed_lb &&
>              !lr_stateful_rec->lrnat_rec->has_distributed_nat &&
>              !l3dgw_port_has_associated_vtep_lports(op->primary_port);
>
> @@ -5473,10 +5515,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,
> ods_size(ls_datapaths));
> -
> -            if (od->lb_with_stateless_mode) {
> -                hmapx_add(&lb_dps->ls_lb_with_stateless_mode, od);
> -            }
> +            handle_od_lb_datapath_modes(od, lb_dps);
>
>              /* Add the lb to the northd tracked data. */
>              hmapx_add(&nd_changes->trk_lbs.crupdated, lb_dps);
> @@ -5515,6 +5554,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,
> ods_size(lr_datapaths));
> +            handle_od_lb_datapath_modes(od, lb_dps);
>
>              /* Add the lb to the northd tracked data. */
>              hmapx_add(&nd_changes->trk_lbs.crupdated, lb_dps);
> @@ -9833,22 +9873,15 @@ build_lswitch_arp_chassis_resident(const struct
> ovn_datapath *od,
>  {
>      struct sset distributed_nat_ports =
>          SSET_INITIALIZER(&distributed_nat_ports);
> -    struct sset resident_ports = SSET_INITIALIZER(&resident_ports);
> -    struct sset inports = SSET_INITIALIZER(&inports);
> +    struct hmapx resident_ports = HMAPX_INITIALIZER(&resident_ports);
>      struct ds match = DS_EMPTY_INITIALIZER;
>
> -    struct hmapx_node *node;
> -    HMAPX_FOR_EACH (node, &od->phys_ports) {
> -        struct ovn_port *op = node->data;
> -        sset_add(&inports, op->json_key);
> -    }
> -
>      struct ovn_port *op;
>      VECTOR_FOR_EACH (&od->router_ports, op) {
>          struct ovn_port *op_r = op->peer;
>
>          if (lrp_is_l3dgw(op_r)) {
> -            sset_add(&resident_ports, op_r->cr_port->json_key);
> +            hmapx_add(&resident_ports, op_r);
>          }
>      }
>
> @@ -9864,23 +9897,30 @@ build_lswitch_arp_chassis_resident(const struct
> ovn_datapath *od,
>          }
>      }
>
> -    if (!sset_is_empty(&inports) && !sset_is_empty(&resident_ports)) {
> +    if (!hmapx_is_empty(&od->phys_ports) &&
> !hmapx_is_empty(&resident_ports)) {
> +        struct hmapx_node *node;
>          const char *port_name;
>
> -        SSET_FOR_EACH (port_name, &inports) {
> +        HMAPX_FOR_EACH (node, &od->phys_ports) {
> +            op = node->data;
> +
>              ds_clear(&match);
>              ds_put_format(&match, "arp.op == 1 && inport == %s",
> -                          port_name);
> +                          op->json_key);
>              ovn_lflow_add(lflows, od, S_SWITCH_IN_CHECK_PORT_SEC, 75,
>                            ds_cstr(&match), REGBIT_EXT_ARP " = 1; next;",
>                            ar->lflow_ref);
>          }
>
> -        SSET_FOR_EACH (port_name, &resident_ports) {
> +        HMAPX_FOR_EACH (node, &resident_ports) {
> +            op = node->data;
> +
>              ds_clear(&match);
> -            ds_put_format(&match, REGBIT_EXT_ARP" == 1 "
> -                                  "&& is_chassis_resident(%s)",
> -                          port_name);
> +            ds_put_format(&match, REGBIT_EXT_ARP" == 1");
> +            if (od_is_centralized(op->od)) {
> +                ds_put_format(&match, " && is_chassis_resident(%s)",
> +                              op->cr_port->json_key);
> +            }
>              ovn_lflow_add(lflows, od, S_SWITCH_IN_APPLY_PORT_SEC, 75,
>                            ds_cstr(&match), "next;", ar->lflow_ref);
>          }
> @@ -9899,8 +9939,7 @@ build_lswitch_arp_chassis_resident(const struct
> ovn_datapath *od,
>      }
>
>      sset_destroy(&distributed_nat_ports);
> -    sset_destroy(&resident_ports);
> -    sset_destroy(&inports);
> +    hmapx_destroy(&resident_ports);
>      ds_destroy(&match);
>  }
>
> @@ -10919,8 +10958,13 @@ build_lswitch_ip_unicast_lookup(struct ovn_port
> *op,
>                           : debug_drop_action();
>
>      if (lsp_is_router(op->nbsp) && op->peer && op->peer->nbrp) {
> +        /* Distributed gateway ports default to centralized mode.
> +         * They operate in distributed mode only when configured
> +         * on their bound router. */
> +        bool peer_lrp_is_centralized = od_is_centralized(op->peer->od);
> +
>          /* For ports connected to logical routers add flows to bypass the
> -         * broadcast flooding of ARP/ND requests in table 19. We direct
> the
> +         * broadcast flooding of ARP/ND requests in table 22. We direct
> the
>           * requests only to the router port that owns the IP address.
>           */
>          build_lswitch_rport_arp_req_flows(op->peer, op->od, op, lflows,
> @@ -10935,7 +10979,9 @@ build_lswitch_ip_unicast_lookup(struct ovn_port
> *op,
>              ds_put_format(match, "eth.dst == %s",
> op->peer->lrp_networks.ea_s);
>          }
>
> -        if (!vector_is_empty(&op->peer->od->l3dgw_ports) &&
> +
> +        if (peer_lrp_is_centralized &&
> +            !vector_is_empty(&op->peer->od->l3dgw_ports) &&
>              !vector_is_empty(&op->od->localnet_ports)) {
>              add_lrp_chassis_resident_check(op->peer, match);
>          } else if (op->cr_port) {
> @@ -12785,6 +12831,13 @@ build_distr_lrouter_nat_flows_for_lb(struct
> lrouter_nat_lb_flows_ctx *ctx,
>      size_t new_match_len = ctx->new_match->length;
>      size_t undnat_match_len = ctx->undnat_match->length;
>
> +    bool lb_is_centralized = !ctx->lb->is_distributed;
> +
> +    /* If load balancer is distributed, then the response traffic
> +     * must be returned through the distributed port.*/
> +    const char *gw_outport = lb_is_centralized ? dgp->cr_port->json_key
> +                                               : dgp->json_key;
> +
>      const char *meter = NULL;
>
>      if (ctx->reject) {
> @@ -12796,8 +12849,9 @@ build_distr_lrouter_nat_flows_for_lb(struct
> lrouter_nat_lb_flows_ctx *ctx,
>                                                      dgp, meter);
>      }
>
> -    if (!vector_is_empty(&ctx->lb_vip->backends) ||
> -        !ctx->lb_vip->empty_backend_rej) {
> +    if (lb_is_centralized &&
> +        (!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);
>      }
> @@ -12834,18 +12888,21 @@ build_distr_lrouter_nat_flows_for_lb(struct
> lrouter_nat_lb_flows_ctx *ctx,
>       * the undnat stage.
>       */
>      ds_put_format(ctx->undnat_match, ") && outport == %s", dgp->json_key);
> -    ds_clear(ctx->gw_redir_action);
> -    ds_put_format(ctx->gw_redir_action, "outport = %s; next;",
> -                  dgp->cr_port->json_key);
> +    ds_put_format(ctx->gw_redir_action,
> +                  "outport = %s; next;", gw_outport);
>
>      ovn_lflow_add(ctx->lflows, od, S_ROUTER_IN_GW_REDIRECT, 200,
>                    ds_cstr(ctx->undnat_match),
> ds_cstr(ctx->gw_redir_action),
>                    lflow_ref, WITH_HINT(&ctx->lb->nlb->header_));
>      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 (lb_is_centralized) {
> +        ds_put_format(ctx->undnat_match, " && is_chassis_resident(%s)",
> +                      dgp->cr_port->json_key);
> +    }
>      ovn_lflow_add(ctx->lflows, od, S_ROUTER_OUT_UNDNAT, 120,
>                    ds_cstr(ctx->undnat_match), ds_cstr(&undnat_action),
>                    lflow_ref, WITH_HINT(&ctx->lb->nlb->header_));
> @@ -14176,6 +14233,10 @@ build_gateway_mtu_flow(struct lflow_table
> *lflows, struct ovn_port *op,
>  static bool
>  consider_l3dgw_port_is_centralized(struct ovn_port *op)
>  {
> +    if (!od_is_centralized(op->od)) {
> +        return false;
> +    }
> +
>      if (l3dgw_port_has_associated_vtep_lports(op)) {
>          return false;
>      }
> @@ -16526,7 +16587,7 @@ build_ipv6_input_flows_for_lrouter_port(
>       * router's own IP address. */
>      for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
>          ds_clear(match);
> -        if (lrp_is_l3dgw(op)) {
> +        if (lrp_is_l3dgw(op) && od_is_centralized(op->od)) {
>              /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
>               * should only be sent from the gateway chassi, so that
>               * upstream MAC learning points to the gateway chassis.
> @@ -16738,7 +16799,8 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
>                        op->lrp_networks.ipv4_addrs[i].network_s,
>                        op->lrp_networks.ipv4_addrs[i].plen);
>
> -        if (!vector_is_empty(&op->od->l3dgw_ports) && op->peer
> +        if (od_is_centralized(op->od) &&
> +            !vector_is_empty(&op->od->l3dgw_ports) && op->peer
>              && !vector_is_empty(&op->peer->od->localnet_ports)) {
>              add_lrp_chassis_resident_check(op, match);
>          }
> diff --git a/northd/northd.h b/northd/northd.h
> index eb5c15f34..f812656af 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -451,6 +451,11 @@ struct ovn_datapath {
>      /* Indicates that the LS has valid vni associated with it. */
>      bool has_evpn_vni;
>
> +    /* True if datapath has some distributed dependencies.
> +     * Currently, this only applies to load balancers attached to datapath
> +     * with distributed mode enabled. */
> +    bool is_distributed;
> +
>      /* OVN northd only needs to know about logical router gateway ports
> for
>       * NAT/LB on a distributed router.  The "distributed gateway ports"
> are
>       * populated only when there is a gateway chassis or ha chassis group
> @@ -1152,6 +1157,18 @@ ovn_port_must_learn_route(const struct ovn_port *op,
>      return true;
>  }
>
> + /* Returns true if datapath 'od' operates in centralized mode on gateway.
> + *
> + * Returns false when datapath is distributed. A datapath is distributed
> + * only when configured with the 'distributed' option enabled. In
> distributed
> + * mode, ARP/ND processing is handled locally on each node.
> + */
> +static inline bool
> +od_is_centralized(const struct ovn_datapath *od)
> +{
> +    return !od->is_distributed;
> +}
> +
>  struct ovn_port *ovn_port_find(const struct hmap *ports, const char
> *name);
>
>  void build_igmp_lflows(struct hmap *igmp_groups,
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index 1acbf202b..aab091883 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -2399,16 +2399,18 @@
>          <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>.  The
> source
> +          <code><var>port_name</var>:<var>[source_ip]</var></code>.  The
> source
>            IP must be from the subnet of the monitored endpoint.  It can be
>            either an unused IP from the subnet, or an IP of one of the
> Logical
>            Router Ports connected to the same switch.
>            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>
> @@ -2611,6 +2613,21 @@ 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">
> +        Option enables distributed load balancing across compute nodes,
> +        ensuring traffic is always routed to local backends — eliminating
> +        east-west traffic between nodes.
> +        Required configuration: <ref column="ip_port_mappings"/>.
> +        NOTE: The addressing of the underlay network must not overlap
> with the
> +        addressing of Load Balancer VIP. If the Load Balancer is attached
> to a
> +        router that is directly connected to the underlay network and the
> VIP
> +        belongs to the same subnet as used on the underlay network, the
> traffic
> +        won't be spread across all chassis. Instead, it will be
> concentrated
> +        only on the chassis that hosts the Distributed Gateway Port of the
> +        router.
> +      </column>
> +
>      </group>
>    </table>
>
> diff --git a/ovn-sb.xml b/ovn-sb.xml
> index 00bae26bf..d294a96ea 100644
> --- a/ovn-sb.xml
> +++ b/ovn-sb.xml
> @@ -2130,6 +2130,17 @@
>            </p>
>          </dd>
>
> +        <dt><code>ct_lb_mark_local;</code></dt>
> +
> <dt><code>ct_lb_mark_local(backends=<var>lport_name</var>[<var>ip</var>[:<var>port</var>][,...][;
> hash_fields=<var>field1</var>,<var>field2</var>,...][;
> ct_flag]);</code></dt>
> +        <dd>
> +          <p>
> +              Same as <code>ct_lb_mark</code>, with the key difference
> that it
> +              implements local-only load balancing. This mode selects
> backends
> +              only from those running on the current chassis, preventing
> +              traffic from being forwarded to backends on remote nodes.
> +          </p>
> +        </dd>
> +
>          <dt>
>            <code><var>R</var> = dns_lookup();</code>
>          </dt>
> diff --git a/tests/multinode-macros.at b/tests/multinode-macros.at
> index ad09ac562..31dc00fe6 100644
> --- a/tests/multinode-macros.at
> +++ b/tests/multinode-macros.at
> @@ -470,4 +470,19 @@ m_is_fedora() {
>      m_central_as grep -qi fedora /etc/os-release
>  }
>
> +# M_START_L4_SERVER([fake_node], [namespace], [ip_addr], [port],
> [reply_string], [pidfile])
> +#
> +# Helper to properly start l4 server in inside 'fake_node''s namespace'.
> +m4_define([M_START_L4_SERVER],
> +    [podman cp $srcdir/server.py $1:/data/metadata_server.py || exit 1
> +     M_NS_DAEMONIZE([$1], [$2],
> +                    [$PYTHON /data/metadata_server.py --bind-host $3 \
> +                                                       --bind-port $4 \
> +                                                       --reply-string $5],
> +                     [$6])
> +     pid=$(podman exec $1 ip netns exec $2 ps aux | grep
> metadata_server.py | grep $5 | tr -s ' ' | cut -d' ' -f2)
> +     on_exit "podman exec $1 ip netns exec $2 kill $pid"
> +    ]
> +)
> +
>  OVS_END_SHELL_HELPERS
> diff --git a/tests/multinode.at b/tests/multinode.at
> index a7b8eafed..b5331af24 100644
> --- a/tests/multinode.at
> +++ b/tests/multinode.at
> @@ -4770,3 +4770,147 @@ M_NS_CHECK_EXEC([ovn-chassis-2], [ovn-ext2],
> [ping6 -q -c 3 -i 0.3 -w 2 6812:86:
>  m_wait_row_count mac_binding 1 ip="6812\:86\:\:102" logical_port="lr1-pub"
>
>  AT_CLEANUP
> +
> +AT_SETUP([Distribute load balancing: IPv4])
> +#
> +# ┌──────────────────────┐
> +# │ fabric (leaf switch) │
> +# │       gw-1           │
> +# └─────────┬────────────┘
> +#           │
> +#    ┌──────┴──────┐
> +#    │             │
> +#    ▼             ▼
> +#  route          route
> +#  weight 1      weight 2 (2 backends)
> +#    │             │
> +#    ▼             ▼
> +# ┌───────┐      ┌───────┐
> +# │Chassis│      │Chassis│
> +# │   1   │      │   2   │
> +# └───-───┘      └───-───┘
> +#
> +
> +check_fake_multinode_setup
> +cleanup_multinode_resources
> +
> +OVS_WAIT_UNTIL([m_as ovn-chassis-1 ip link show | grep -q genev_sys])
> +OVS_WAIT_UNTIL([m_as ovn-chassis-2 ip link show | grep -q genev_sys])
> +
> +check multinode_nbctl ls-add pub                \
> +    -- lsp-add-router-port pub pub-lr1 lr1-pub  \
> +    -- lsp-add-localnet-port pub pub-ln public
> +
> +check multinode_nbctl lr-add lr1 \
> +    -- lrp-add lr1 lr1-pub 00:00:00:00:00:01 169.254.1.254/24 \
> +    -- lrp-add lr1 lr1-down 00:00:00:00:00:02 192.168.1.254/24
> +
> +check multinode_nbctl ls-add ls1
> +check multinode_nbctl lsp-add ls1 ls1p1
> +check multinode_nbctl lsp-set-addresses ls1p1 "00:00:00:01:01:02
> 192.168.1.1"
> +check multinode_nbctl lsp-add ls1 ls1p2
> +check multinode_nbctl lsp-set-addresses ls1p2 "00:00:00:01:02:02
> 192.168.1.2"
> +check multinode_nbctl lsp-add ls1 ls1p3
> +check multinode_nbctl lsp-set-addresses ls1p3 "00:00:00:01:03:02
> 192.168.1.3"
> +check multinode_nbctl lsp-add-router-port ls1 ls1-lr1 lr1-down
> +
> +check multinode_nbctl lrp-set-gateway-chassis lr1-pub ovn-chassis-2
> +
> +# Create default route
> +check multinode_nbctl lr-route-add lr1 0.0.0.0/0 169.254.1.253 lr1-pub
> +
> +m_as ovn-chassis-1 /data/create_fake_vm.sh ls1p1 ls1p1 00:00:00:01:01:02
> 1500 192.168.1.1 24 192.168.1.254 2001::1/64 2001::a
> +m_as ovn-chassis-2 /data/create_fake_vm.sh ls1p2 ls1p2 00:00:00:01:02:02
> 1500 192.168.1.2 24 192.168.1.254 2001::2/64 2001::a
> +m_as ovn-chassis-2 /data/create_fake_vm.sh ls1p3 ls1p3 00:00:00:01:03:02
> 1500 192.168.1.3 24 192.168.1.254 2001::3/64 2001::a
> +m_wait_for_ports_up
> +
> +# Create load balancer
> +lb_vip="172.31.0.1"
> +check multinode_nbctl lb-add lb1 $lb_vip:80 192.168.1.1:10880,
> 192.168.1.2:10880,192.168.1.3:10880
> +check multinode_nbctl lr-lb-add lr1 lb1
> +check multinode_nbctl set Load_Balancer lb1
> ip_port_mappings:192.168.1.1=ls1p1:192.168.1.199
> +check multinode_nbctl set Load_Balancer lb1
> ip_port_mappings:192.168.1.2=ls1p2:192.168.1.199
> +check multinode_nbctl set Load_Balancer lb1
> ip_port_mappings:192.168.1.3=ls1p3:192.168.1.199
> +check multinode_nbctl set load_balancer lb1 options:distributed=true
> +
> +ip_ch1=$(m_as ovn-chassis-1 ip a show dev eth1 | grep "inet " | awk
> '{print $2}'| cut -d '/' -f1)
> +ip_ch2=$(m_as ovn-chassis-2 ip a show dev eth1 | grep "inet " | awk
> '{print $2}'| cut -d '/' -f1)
> +
> +# Add multipath route to load balancer VIP with weighted nexthops on
> "fabric" host:
> +check m_as ovn-gw-1 ip route flush $lb_vip
> +check m_as ovn-gw-1 ip route add $lb_vip  nexthop via $ip_ch1 dev eth1
> weight 1 \
> +                                          nexthop via $ip_ch2 dev eth1
> weight 2
> +
> +# Set kernel multipath hash policy to L3/L4 (source/destination IP+port)
> +# Policy 1 = Layer 3/4 hash (src/dst IP+port)
> +AT_CHECK([m_as ovn-gw-1 sysctl -w net.ipv4.fib_multipath_hash_policy=1],
> [0], [dnl
> +net.ipv4.fib_multipath_hash_policy = 1
> +])
> +
> +# Check OpenFlow group filling: it should only contain local backends
> +AT_CHECK([m_as ovn-chassis-1 ovs-ofctl dump-groups br-int | sed -e
> 's/table=[[0-9]]*/table=<cleared>/g'], [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.1.1:10880),exec(load:0x1->NXM_NX_CT_MARK[[1]]))
> +])
> +
> +AT_CHECK([m_as ovn-chassis-2 ovs-ofctl dump-groups br-int | sed -e
> 's/table=[[0-9]]*/table=<cleared>/g'], [0], [dnl
> +NXST_GROUP_DESC reply (xid=0x2):
> +
> group_id=2,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.1.2:10880
> ),exec(load:0x1->NXM_NX_CT_MARK[[1]])),bucket=bucket_id:2,weight:100,actions=ct(commit,table=<cleared>,zone=NXM_NX_REG11[[0..15]],nat(dst=
> 192.168.1.3:10880),exec(load:0x1->NXM_NX_CT_MARK[[1]]))
> +])
> +
> +physicl_gw_mac_address="30:42:f5:a7:46:65"
> +
> +# Configure infrastructure on chassis hosts:
> +# lb-host (physicl_gw_mac_address) - (veth) lb-ovs - br-lb - br-int
> +for c in ovn-chassis-1 ovn-chassis-2
> +do
> +    check m_as $c ip link add lb-host type veth peer lb-ovs
> +    on_exit "m_as $c ip link del lb-host"
> +
> +    check m_as $c ip link set dev lb-host address $physicl_gw_mac_address
> +    check m_as $c ip addr add 169.254.1.253/24 dev lb-host
> +    check m_as $c ip link set lb-host up
> +    check m_as $c ip link set lb-ovs up
> +
> +    check m_as $c ovs-vsctl add-br br-lb
> +    on_exit "m_as $c ovs-vsctl del-br br-lb"
> +    check m_as $c ovs-vsctl add-port br-lb lb-ovs
> +    on_exit "m_as $c ovs-vsctl del-port lb-ovs"
> +    check m_as $c ovs-vsctl set open .
> external-ids:ovn-bridge-mappings=public:br-lb
> +
> +    check m_as $c ip route flush $lb_vip
> +    check m_as $c ip r add $lb_vip via 169.254.1.254 dev lb-host
> +    on_exit "m_as $c ip route flush $lb_vip"
> +done
> +
> +OVS_WAIT_UNTIL([m_as ovn-chassis-1 ovs-vsctl show | grep -q
> patch-pub-ln-to-br-int])
> +OVS_WAIT_UNTIL([m_as ovn-chassis-2 ovs-vsctl show | grep -q
> patch-pub-ln-to-br-int])
> +
> +M_START_L4_SERVER([ovn-chassis-1], [ls1p1], [192.168.1.1], [10880],
> [ls1p1], [ls1p1.pid])
> +M_START_L4_SERVER([ovn-chassis-2], [ls1p2], [192.168.1.2], [10880],
> [ls1p2], [ls1p2.pid])
> +M_START_L4_SERVER([ovn-chassis-2], [ls1p3], [192.168.1.3], [10880],
> [ls1p3], [ls1p3.pid])
> +
> +# Capture traffic to verify load balancing occurs locally without
> east-west traffic
> +for i in {1..2}; do
> +    node_name="ovn-chassis-$i"
> +    M_START_TCPDUMP([$node_name], [-c 2 -neei genev_sys_6081 port 10880],
> [ch${i}_genev])
> +    M_START_TCPDUMP([$node_name], [-c 2 -neei eth2 port 10880],
> [ch${i}_eth2])
> +done
> +
> +AT_CHECK([m_as ovn-gw-1 /bin/bash -c 'for i in $(seq 500); \
> +                        do echo "" | nc 172.31.0.1 80 2>/dev/null ; \
> +                        echo ; done | sort | uniq -c' > reply], [0], [])
> +
> +# Check that requests are distributed among all backends.
> +AT_CHECK([grep -q ls1p1 reply && grep -q ls1p2 reply && grep -q ls1p3
> reply], [0], [])
> +
> +AT_CHECK([cat ch1_genev.tcpdump], [0], [dnl
> +])
> +AT_CHECK([cat ch1_eth2.tcpdump], [0], [dnl
> +])
> +AT_CHECK([cat ch2_genev.tcpdump], [0], [dnl
> +])
> +AT_CHECK([cat ch2_eth2.tcpdump], [0], [dnl
> +])

+
> +AT_CLEANUP
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 512e42036..4458b94f7 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -18286,16 +18286,18 @@ AT_CLEANUP
>  ])
>
>  OVN_FOR_EACH_NORTHD_NO_HV([
> -AT_SETUP([ip_port_mappings validation])
> +AT_SETUP([ip_port_mappings validation: IPv4])
>

There are missing --wait=sb everywhere.


>  ovn_start
>
>  # ip_port_mappings syntax: ip:lport_name:src_ip:<az_name>(for remote
> lports)
>
>  check ovn-nbctl ls-add ls1
> +check ovn-nbctl lr-add lr1
> +
> +check as northd 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 \
> -          -- --id=@hc create Load_Balancer_Health_Check vip="1.1.1.1\:80"
> \
> +AT_CHECK([ovn-nbctl --id=@hc create Load_Balancer_Health_Check
> vip="1.1.1.1\:80" \
>               options:failure_count=100 \
>            -- add Load_Balancer lb1_ipv4 health_check @hc | uuidfilt],
> [0], [<0>
>  ])
> @@ -18318,22 +18320,16 @@ check_column false sb:Service_Monitor remote
> logical_port=lport1
>  # Empty src_ip.
>  check ovn-nbctl clear load_balancer lb1_ipv4 ip_port_mappings
>  check ovn-nbctl set load_balancer lb1_ipv4
> ip_port_mappings:192.168.0.1=lport1:
> -OVS_WAIT_UNTIL([grep "Invalid svc mon src IP" northd/ovn-northd.log])
>  check_row_count sb:Service_Monitor 0
> -echo > northd/ovn-northd.log
>
> -# Uncorrect ip_address.
> +# Incorrect ip_address.
>  check ovn-nbctl set load_balancer lb1_ipv4
> ip_port_mappings:invalid=lport2_az1:2.2.2.9
> -OVS_WAIT_UNTIL([grep "bad IP address" northd/ovn-northd.log])
> -echo > northd/ovn-northd.log
> -
> +check_row_count sb:Service_Monitor 0
>  check ovn-nbctl set load_balancer lb1_ipv4
> ip_port_mappings:2.2.2.1=lport2_az1:invalid
> -OVS_WAIT_UNTIL([grep "bad IP address" northd/ovn-northd.log])
> -echo > northd/ovn-northd.log
> -
> +check_row_count sb:Service_Monitor 0
>  check ovn-nbctl set load_balancer lb1_ipv4
> ip_port_mappings:2.2.2.1=:2.2.2.9
> -OVS_WAIT_UNTIL([grep "bad IP address" northd/ovn-northd.log])
> -echo > northd/ovn-northd.log
> +check_row_count sb:Service_Monitor 0
> +OVS_WAIT_UNTIL([test $(grep -c "Invalid svc mon src IP"
> northd/ovn-northd.log) -eq 4])
>
>  check ovn-nbctl set load_balancer lb1_ipv4
> ip_port_mappings:192.168.0.1=lport1:192.168.0.99:az_name
>
> @@ -18345,22 +18341,171 @@ check_column "192.168.0.99" sb:Service_Monitor
> src_ip logical_port=lport1
>  check_column false sb:Service_Monitor ic_learned logical_port=lport1
>  check_column true sb:Service_Monitor remote logical_port=lport1
>
> -uuid=$(ovn-sbctl -d bare --no-headings --columns _uuid find
> Service_Monitor logical_port=lport1)
> +hc_uuid=$(fetch_column sb:Service_Monitor _uuid logical_port=lport1)
>
>  # Check az_name presence in options.
> -AT_CHECK([ovn-sbctl get Service_Monitor ${uuid} options:az-name],
> +AT_CHECK([ovn-sbctl get Service_Monitor ${hc_uuid} options:az-name],
>  [0], [az_name
>  ])
>
> -AT_CHECK([ovn-sbctl get Service_Monitor ${uuid} options:failure_count],
> +AT_CHECK([ovn-sbctl get Service_Monitor ${hc_uuid} options:failure_count],
>  [0], ["100"
>  ])
>
>  # Empty availability zone name.
>  check ovn-nbctl set load_balancer lb1_ipv4
> ip_port_mappings:192.168.0.1=lport1:192.168.0.99:
>  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.
> +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
> +
> +# Check that load balancer does not work in a distributed mode - there is
> no ip_port_mappings setting
> +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=(drop;)
> +])
> +
> +# Check if load balancer has only one backend available since the only
> one backend has ip_port_mappings
> +check ovn-nbctl set load_balancer lb_distubuted
> ip_port_mappings:192.168.0.1=lport1
> +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);)
> +])
> +
> +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);)
> +])
> +
> +# Check if health check is configured, ip_port_mappings must be provided.
> +AT_CHECK([ovn-nbctl --id=@hc create Load_Balancer_Health_Check
> vip="1.1.1.1\:80" \
> +             options:failure_count=100 \
> +          -- add Load_Balancer . health_check @hc | uuidfilt], [0], [<0>
> +])
> +
> +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=(drop;)
> +])
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([ip_port_mappings validation: IPv6])
> +ovn_start
> +
> +# ip_port_mappings syntax: ip:lport_name:src_ip:<az_name>(for remote
> lports)
> +
> +check ovn-nbctl ls-add ls1
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lsp-add ls1 lport1
> +
> +check as northd ovn-appctl -t ovn-northd vlog/disable-rate-limit
> +check ovn-nbctl lb-add lb1 [[2001::a]]:80 [[2001::3]]:80,[[2002::3]]:80
> +
> +AT_CHECK([ovn-nbctl --id=@hc create Load_Balancer_Health_Check
> vip="\[\[2001\:\:a\]\]\:80" \
> +             options:failure_count=100 \
> +          -- add Load_Balancer . health_check @hc | uuidfilt], [0], [<0>
> +])
> +
> +check_row_count sb:Service_Monitor 0
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1:[[2001::2]]\"
> +
> +check_row_count sb:Service_Monitor 1
> +ovn-sbctl list service_monitor
> +check_column "2001::3" sb:Service_Monitor ip logical_port=lport1
> +check_column 80 sb:Service_Monitor port logical_port=lport1
> +check_column tcp sb:Service_Monitor protocol logical_port=lport1
> +check_column "2001::2" sb:Service_Monitor src_ip logical_port=lport1
> +check_column false sb:Service_Monitor ic_learned logical_port=lport1
> +check_column false sb:Service_Monitor remote logical_port=lport1
> +check_column "" sb:Service_Monitor logical_input_port logical_port=lport1
> +
> +# Empty src_ip.
> +check ovn-nbctl clear load_balancer lb1 ip_port_mappings
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1:\"
> +check_row_count sb:Service_Monitor 0
> +
> +# Incorrect ip_address.
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[invalid]]\"=\"lport1:\"
> +check_row_count sb:Service_Monitor 0
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1:invalid\"
> +check_row_count sb:Service_Monitor 0
> +OVS_WAIT_UNTIL([test $(grep -c "bad IP address" northd/ovn-northd.log)
> -eq 3])
> +
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1:[[2001::2]]:az_name\"
> +check_row_count sb:Service_Monitor 1
> +ovn-sbctl list service_monitor
> +check_column "2001::3" sb:Service_Monitor ip logical_port=lport1
> +check_column 80 sb:Service_Monitor port logical_port=lport1
> +check_column tcp sb:Service_Monitor protocol logical_port=lport1
> +check_column "2001::2" sb:Service_Monitor src_ip logical_port=lport1
> +check_column false sb:Service_Monitor ic_learned logical_port=lport1
> +check_column true sb:Service_Monitor remote logical_port=lport1
> +check_column "" sb:Service_Monitor logical_input_port logical_port=lport1
> +
> +hc_uuid=$(fetch_column sb:Service_Monitor _uuid logical_port=lport1)
>
> +# Check az_name presence in options.
> +AT_CHECK([ovn-sbctl get Service_Monitor ${hc_uuid} options:az-name],
> +[0], [az_name
> +])
> +
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1:[[2001::2]]:\"
> +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
> +
> +# Check correct setup of distributed load balancers.
> +check ovn-nbctl lb-add lb_distubuted [[2001::a]]:80
> [[2001::3]]:80,[[2002::3]]:80
> +check ovn-nbctl lr-lb-add lr1 lb_distubuted
> +check ovn-nbctl set load_balancer lb_distubuted options:distributed=true
> +
> +# Check that load balancer does not work in a distributed mode - there is
> no ip_port_mappings setting
> +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 && ip6 && ip6.dst == 2001::a && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80), action=(drop;)
> +])
> +
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1\"
> +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 && ip6 && ip6.dst == 2001::a && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80),
> action=(ct_lb_mark_local(backends="lport1":[[2001::3]]:80);)
> +])
> +
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2002::3]]\"=\"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 && ip6 && ip6.dst == 2001::a && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80),
> action=(ct_lb_mark_local(backends="lport1":[[2001::3]]:80,"lport2":[[2002::3]]:80);)
> +])
> +
> +AT_CHECK([ovn-nbctl --id=@hc create Load_Balancer_Health_Check
> vip="\[\[2001\:\:a\]\]\:80" \
> +             options:failure_count=100 \
> +          -- add Load_Balancer . health_check @hc | uuidfilt], [0], [<0>
> +])
> +
> +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 && ip6 && ip6.dst == 2001::a && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80), action=(drop;)
> +])
> +
> +check ovn-nbctl set load_balancer .
> ip_port_mappings:\"[[2001::3]]\"=\"lport1:[[2001::2]]\"
> +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 && ip6 && ip6.dst == 2001::a && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80),
> action=(ct_lb_mark_local(backends="lport1":[[2001::3]]:80);)
> +])
> +
> +check ovn-nbctl lb-del lb_distubuted
> +
>  OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
> @@ -19325,6 +19470,197 @@ OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
>
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([Distributed load balancers: logical-flow test - IPv4/IPv6])
> +ovn_start
> +
> +check ovn-nbctl ls-add outside
> +check ovn-nbctl lsp-add-localnet-port outside outside phnet
> +check ovn-nbctl lsp-add-router-port outside outside-down lr1-up
> +
> +check ovn-nbctl lr-add lr1 \
> +      -- lrp-add lr1 lr1-up 11:11:11:11:11:11 169.254.0.1/24
> 2001:db8:abcd:0002::bad/64 \
> +      -- lrp-add lr1 lr1-down 12:12:12:12:12:12 192.168.0.1/24
> 2001:db8:abcd:0001::c0fe/64
> +
> +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-router-port ls1 ls1-up lr1-down
> +
> +check ovn-nbctl ha-chassis-group-add gateway
> +check ovn-nbctl ha-chassis-group-add-chassis gateway hv1 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 lb-add lb1_ipv4 172.31.0.1:80 192.168.0.101:10880,
> 192.168.0.102:10880
> +check ovn-nbctl set Load_Balancer lb1_ipv4
> ip_port_mappings:192.168.0.101=lport1:192.168.0.199
> +check ovn-nbctl set Load_Balancer lb1_ipv4
> ip_port_mappings:192.168.0.102=lport2:192.168.0.199
> +check ovn-nbctl lr-lb-add lr1 lb1_ipv4
> +
> +check ovn-nbctl lb-add lb1_ipv6 [[2000::1]]:80
> [[2001:db8:abcd:1::2]]:10882
> +check ovn-nbctl set Load_Balancer lb1_ipv6
> ip_port_mappings:\"[[2001:db8:abcd:1::2]]\"=\"lport1\"
> +check ovn-nbctl lr-lb-add lr1 lb1_ipv6
> +
> +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_ip_input | grep priority=90
> | grep 2001:db8:abcd:2::bad | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_ip_input     ), priority=90   , match=(inport ==
> "lr1-up" && ip6.dst == {2001:db8:abcd:2::bad, ff02::1:ff00:bad} && nd_ns &&
> nd.target == 2001:db8:abcd:2::bad && is_chassis_resident("cr-lr1-up")),
> action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = nd.target;
> nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
> +  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst ==
> {2001:db8:abcd:2::bad, fe80::1311:11ff:fe11:1111} && icmp6.type == 128 &&
> icmp6.code == 0), action=(ip6.dst <-> ip6.src; ip.ttl = 255; icmp6.type =
> 129; 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_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;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src
> == 2001:db8:abcd:1::2 && tcp.src == 10882)) && (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=200  , match=(ip6 && ((ip6.src
> == 2001:db8:abcd:1::2 && tcp.src == 10882)) && 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;)
> +])
> +
> +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 == 172.31.0.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);)
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new &&
> !ct.rel && ip6 && ip6.dst == 2000::1 && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80 && is_chassis_resident("cr-lr1-up")),
> action=(ct_lb_mark(backends=[[2001:db8:abcd:1::2]]:10882);)
> +])
> +
> +AT_CHECK([cat outside_lflows_before | grep ls_in_check_port_sec | grep
> priority=75 | ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_check_port_sec), priority=75   , match=(arp.op == 1 &&
> inport == "outside"), action=(reg0[[22]] = 1; next;)
> +])
> +
> +AT_CHECK([cat outside_lflows_before | grep ls_in_apply_port_sec | grep
> priority=75 | ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] == 1
> && is_chassis_resident("cr-lr1-up")), action=(next;)
> +])
> +
> +check ovn-nbctl clear logical_router_port $lr1_up_uuid ha_chassis_group
> +check ovn-nbctl ha-chassis-group-del gateway
> +check ovn-nbctl ha-chassis-group-add gateway2
> +check ovn-nbctl ha-chassis-group-add-chassis gateway2 test 1
> +ha_g_uuid=$(fetch_column nb:HA_Chassis_Group _uuid name=gateway2)
> +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 set load_balancer lb1_ipv4 options:distributed=true
> +
> +ovn-sbctl lflow-list outside > outside_lflows_after
> +ovn-sbctl lflow-list lr1 > lr1_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;)
> +])
> +
> +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_ip_input | grep priority=90 |
> grep 2001:db8:abcd:2::bad | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_ip_input     ), priority=90   , match=(inport ==
> "lr1-up" && ip6.dst == {2001:db8:abcd:2::bad, ff02::1:ff00:bad} && nd_ns &&
> nd.target == 2001:db8:abcd:2::bad), action=(nd_na_router { eth.src =
> xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport =
> inport; flags.loopback = 1; output; };)
> +  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst ==
> {2001:db8:abcd:2::bad, fe80::1311:11ff:fe11:1111} && icmp6.type == 128 &&
> icmp6.code == 0), action=(ip6.dst <-> ip6.src; ip.ttl = 255; icmp6.type =
> 129; 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;)
> +])
> +
> +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;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src
> == 2001:db8:abcd:1::2 && tcp.src == 10882)) && (inport == "lr1-up" ||
> outport == "lr1-up") && is_chassis_resident("cr-lr1-up")), action=(ct_dnat;)
> +])
> +
> +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=200  , match=(ip6 && ((ip6.src
> == 2001:db8:abcd:1::2 && tcp.src == 10882)) && 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;)
> +])
> +
> +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 == 172.31.0.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);)
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new &&
> !ct.rel && ip6 && ip6.dst == 2000::1 && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80 && is_chassis_resident("cr-lr1-up")),
> action=(ct_lb_mark(backends=[[2001:db8:abcd:1::2]]:10882);)
> +])
> +
> +AT_CHECK([cat outside_lflows_after | grep ls_in_check_port_sec | grep
> priority=75 | ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_check_port_sec), priority=75   , match=(arp.op == 1 &&
> inport == "outside"), action=(reg0[[22]] = 1; next;)
> +])
> +
> +AT_CHECK([cat outside_lflows_after | grep ls_in_apply_port_sec | grep
> priority=75 | ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_apply_port_sec), priority=75   , match=(reg0[[22]] ==
> 1), action=(next;)
> +])
> +
> +check ovn-nbctl set load_balancer lb1_ipv6 options:distributed=true
> +
> +ovn-sbctl lflow-list outside > outside_lflows_after
> +ovn-sbctl lflow-list lr1 > lr1_lflows_after
> +
> +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;)
> +  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src
> == 2001:db8:abcd:1::2 && tcp.src == 10882)) && (inport == "lr1-up" ||
> outport == "lr1-up")), action=(ct_dnat;)
> +])
> +
> +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=200  , match=(ip6 && ((ip6.src
> == 2001:db8:abcd:1::2 && tcp.src == 10882)) && 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;)
> +])
> +
> +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 == 172.31.0.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);)
> +  table=??(lr_in_dnat         ), priority=120  , match=(ct.new &&
> !ct.rel && ip6 && ip6.dst == 2000::1 && reg1[[16..23]] == 6 &&
> reg1[[0..15]] == 80),
> action=(ct_lb_mark_local(backends="lport1":[[2001:db8:abcd:1::2]]:10882);)
> +])
> +
> +check ovn-nbctl set load_balancer lb1_ipv6 options:distributed=false
> +
> +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;)
> +])
> +
> +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_ip_input | grep priority=90 |
> grep 2001:db8:abcd:2::bad | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_ip_input     ), priority=90   , match=(inport ==
> "lr1-up" && ip6.dst == {2001:db8:abcd:2::bad, ff02::1:ff00:bad} && nd_ns &&
> nd.target == 2001:db8:abcd:2::bad), action=(nd_na_router { eth.src =
> xreg0[[0..47]]; ip6.src = nd.target; nd.tll = xreg0[[0..47]]; outport =
> inport; flags.loopback = 1; output; };)
> +  table=??(lr_in_ip_input     ), priority=90   , match=(ip6.dst ==
> {2001:db8:abcd:2::bad, fe80::1311:11ff:fe11:1111} && icmp6.type == 128 &&
> icmp6.code == 0), action=(ip6.dst <-> ip6.src; ip.ttl = 255; icmp6.type =
> 129; 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;)
> +])
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
>  AT_SETUP([Conntrack skip for switch ports connected to spine switch])
>  ovn_start
>
> diff --git a/tests/server.py b/tests/server.py
> index b4aa4b188..c26646c36 100755
> --- a/tests/server.py
> +++ b/tests/server.py
> @@ -35,7 +35,7 @@ def get_socket_family(host):
>              raise
>
>
> -def start_server(host='127.0.0.1', port=10000):
> +def start_server(host='127.0.0.1', port=10000, reply_string=None):
>      # Determine socket family based on host address
>      family = get_socket_family(host)
>
> @@ -86,6 +86,8 @@ def start_server(host='127.0.0.1', port=10000):
>                  # Receive the data from the client in chunks and write
>                  # to a file
>                  data = client_socket.recv(1024)
> +                if reply_string:
> +                    client_socket.sendall(reply_string.encode())
>                  while data:
>                      with open("output.txt", "a") as f:
>                          f.write(data.decode())
> @@ -97,6 +99,7 @@ if __name__ == "__main__":
>      group = parser.add_argument_group()
>      group.add_argument("-i", "--bind-host")
>      group.add_argument("-p", "--bind-port", type=int)
> +    group.add_argument("-s", "--reply-string")
>      args = parser.parse_args()
>
> -    start_server(args.bind_host, args.bind_port)
> +    start_server(args.bind_host, args.bind_port, args.reply_string)
> --
> 2.48.1
>
> _______________________________________________
> dev mailing list
> [email protected]
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev


I took care of the nits and merged this into main.

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

Reply via email to