This commit fixes the build_distr_lrouter_nat_flows_for_lb function to
include one NAT stateless flow entry for each DGP in use. Since we have
added support to create multiple gateway ports per logical router, it's
necessary to include in the LR nat rules pipeline a specific entry for each
attached DGP. Otherwise, the ingress traffic is only redirected when the
incoming LRP matches the chassis_resident field.

Considering that DNAT rules for DGPs were implemented with the need to
configure the DGP-related gateway-port column, the load-balancer NAT rule
configuration can use a similar idea. In this case, we don't know the LRP
responsible for the incoming traffic, and therefore we need to automatically
apply a stateless NAT rule to the load-balancer on all DGPs to allow inbound
traffic.

After applying this patch, the incoming and/or outgoing traffic can pass
through any chassis where the DGP resides without having problems with CT
state.

Reported-at: https://bugs.launchpad.net/ubuntu/+source/ovn/+bug/2054322
Fixes: 15348b7b806f ("ovn-northd: Multiple distributed gateway port support.")
Signed-off-by: Roberto Bartzen Acosta <roberto.aco...@luizalabs.com>
---
 northd/en-lr-stateful.c   |  12 -
 northd/northd.c           | 110 ++++++--
 tests/multinode-macros.at |  40 +++
 tests/multinode.at        | 550 ++++++++++++++++++++++++++++++++++++++
 tests/ovn-northd.at       | 222 +++++++++++++++
 5 files changed, 897 insertions(+), 37 deletions(-)

diff --git a/northd/en-lr-stateful.c b/northd/en-lr-stateful.c
index baf1bd2f8..f09691af6 100644
--- a/northd/en-lr-stateful.c
+++ b/northd/en-lr-stateful.c
@@ -516,18 +516,6 @@ lr_stateful_record_create(struct lr_stateful_table *table,
 
     table->array[od->index] = lr_stateful_rec;
 
-    /* Load balancers are not supported (yet) if a logical router has multiple
-     * distributed gateway port.  Log a warning. */
-    if (lr_stateful_rec->has_lb_vip && lr_has_multiple_gw_ports(od)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_WARN_RL(&rl, "Load-balancers are configured on logical "
-                     "router %s, which has %"PRIuSIZE" distributed "
-                     "gateway ports. Load-balancer is not supported "
-                     "yet when there is more than one distributed "
-                     "gateway port on the router.",
-                     od->nbr->name, od->n_l3dgw_ports);
-    }
-
     return lr_stateful_rec;
 }
 
diff --git a/northd/northd.c b/northd/northd.c
index 7ceed63dd..e3fc16258 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -11878,31 +11878,30 @@ static void
 build_distr_lrouter_nat_flows_for_lb(struct lrouter_nat_lb_flows_ctx *ctx,
                                      enum lrouter_nat_lb_flow_type type,
                                      struct ovn_datapath *od,
-                                     struct lflow_ref *lflow_ref)
+                                     struct lflow_ref *lflow_ref,
+                                     struct ovn_port *dgp)
 {
-    struct ovn_port *dgp = od->l3dgw_ports[0];
-
-    const char *undnat_action;
-
-    switch (type) {
-    case LROUTER_NAT_LB_FLOW_FORCE_SNAT:
-        undnat_action = "flags.force_snat_for_lb = 1; next;";
-        break;
-    case LROUTER_NAT_LB_FLOW_SKIP_SNAT:
-        undnat_action = "flags.skip_snat_for_lb = 1; next;";
-        break;
-    case LROUTER_NAT_LB_FLOW_NORMAL:
-    case LROUTER_NAT_LB_FLOW_MAX:
-        undnat_action = lrouter_use_common_zone(od)
-                        ? "ct_dnat_in_czone;"
-                        : "ct_dnat;";
-        break;
-    }
+    struct ds dnat_action = DS_EMPTY_INITIALIZER;
 
     /* Store the match lengths, so we can reuse the ds buffer. */
     size_t new_match_len = ctx->new_match->length;
     size_t undnat_match_len = ctx->undnat_match->length;
 
+    /* Create stateless LB NAT rules when using DGPs.
+     * dnat_action: Add the LB backend IPs as a destination action of the
+     *              lr_in_dnat NAT rule with cumulative effect because any
+     *              backend dst IP used in the action list will redirect the
+     *              packet to the ct_lb pipeline.
+     */
+    if (od->n_l3dgw_ports > 1) {
+        for (size_t i = 0; i < ctx->lb_vip->n_backends; i++) {
+            struct ovn_lb_backend *backend = &ctx->lb_vip->backends[i];
+            bool ipv6 = !IN6_IS_ADDR_V4MAPPED(&backend->ip);
+            ds_put_format(&dnat_action, "%s.dst=%s;", ipv6 ? "ip6" : "ip4",
+                          backend->ip_str);
+        }
+    }
+    ds_put_format(&dnat_action, "%s", ctx->new_action[type]);
 
     const char *meter = NULL;
 
@@ -11912,20 +11911,47 @@ build_distr_lrouter_nat_flows_for_lb(struct 
lrouter_nat_lb_flows_ctx *ctx,
 
     if (ctx->lb_vip->n_backends || !ctx->lb_vip->empty_backend_rej) {
         ds_put_format(ctx->new_match, " && is_chassis_resident(%s)",
-                      od->l3dgw_ports[0]->cr_port->json_key);
+                      dgp->cr_port->json_key);
     }
 
     ovn_lflow_add_with_hint__(ctx->lflows, od, S_ROUTER_IN_DNAT, ctx->prio,
-                              ds_cstr(ctx->new_match), ctx->new_action[type],
+                              ds_cstr(ctx->new_match), ds_cstr(&dnat_action),
                               NULL, meter, &ctx->lb->nlb->header_,
                               lflow_ref);
 
     ds_truncate(ctx->new_match, new_match_len);
 
+    ds_destroy(&dnat_action);
     if (!ctx->lb_vip->n_backends) {
         return;
     }
 
+    struct ds undnat_action = DS_EMPTY_INITIALIZER;
+    struct ds snat_action = DS_EMPTY_INITIALIZER;
+
+    switch (type) {
+    case LROUTER_NAT_LB_FLOW_FORCE_SNAT:
+        ds_put_format(&undnat_action, "flags.force_snat_for_lb = 1; next;");
+        break;
+    case LROUTER_NAT_LB_FLOW_SKIP_SNAT:
+        ds_put_format(&undnat_action, "flags.skip_snat_for_lb = 1; next;");
+        break;
+    case LROUTER_NAT_LB_FLOW_NORMAL:
+    case LROUTER_NAT_LB_FLOW_MAX:
+        ds_put_format(&undnat_action, "%s",
+                      lrouter_use_common_zone(od) ? "ct_dnat_in_czone;"
+                      : "ct_dnat;");
+        break;
+    }
+
+    /* Create stateless LB NAT rules when using DGPs.
+     * undnat_action: Remove the ct action from the lr_out_undenat NAT rule.
+     */
+    if (od->n_l3dgw_ports > 1) {
+        ds_clear(&undnat_action);
+        ds_put_format(&undnat_action, "next;");
+    }
+
     /* We need to centralize the LB traffic to properly perform
      * the undnat stage.
      */
@@ -11944,11 +11970,42 @@ build_distr_lrouter_nat_flows_for_lb(struct 
lrouter_nat_lb_flows_ctx *ctx,
     ds_put_format(ctx->undnat_match, ") && (inport == %s || outport == %s)"
                   " && is_chassis_resident(%s)", dgp->json_key, dgp->json_key,
                   dgp->cr_port->json_key);
+    /* Use the LB protocol as matching criteria for out undnat and snat when
+     * creating LBs with multiple DGPs. */
+    if (od->n_l3dgw_ports > 1) {
+        ds_put_format(ctx->undnat_match, " && %s", ctx->lb->proto);
+    }
     ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_OUT_UNDNAT, 120,
-                            ds_cstr(ctx->undnat_match), undnat_action,
-                            &ctx->lb->nlb->header_,
+                            ds_cstr(ctx->undnat_match),
+                            ds_cstr(&undnat_action), &ctx->lb->nlb->header_,
                             lflow_ref);
+
+    /* Create stateless LB NAT rules when using DGPs.
+     * snat_action: Add a new lr_out_snat rule with the LB VIP as source IP
+     *              action to perform the NAT stateless pipeline completely.
+     */
+    if (od->n_l3dgw_ports > 1) {
+        if (ctx->lb_vip->port_str) {
+            ds_put_format(&snat_action, "%s.src=%s; %s.src=%s; next;",
+                          ctx->lb_vip->address_family == AF_INET6 ?
+                          "ip6" : "ip4",
+                          ctx->lb_vip->vip_str, ctx->lb->proto,
+                          ctx->lb_vip->port_str);
+        } else {
+            ds_put_format(&snat_action, "%s.src=%s; next;",
+                          ctx->lb_vip->address_family == AF_INET6 ?
+                          "ip6" : "ip4",
+                          ctx->lb_vip->vip_str);
+        }
+        ovn_lflow_add_with_hint(ctx->lflows, od, S_ROUTER_OUT_SNAT, 160,
+                                ds_cstr(ctx->undnat_match),
+                                ds_cstr(&snat_action), &ctx->lb->nlb->header_,
+                                lflow_ref);
+    }
+
     ds_truncate(ctx->undnat_match, undnat_match_len);
+    ds_destroy(&undnat_action);
+    ds_destroy(&snat_action);
 }
 
 static void
@@ -12114,8 +12171,11 @@ build_lrouter_nat_flows_for_lb(
         if (!od->n_l3dgw_ports) {
             bitmap_set1(gw_dp_bitmap[type], index);
         } else {
-            build_distr_lrouter_nat_flows_for_lb(&ctx, type, od,
-                                                 lb_dps->lflow_ref);
+            for (size_t i = 0; i < od->n_l3dgw_ports; i++) {
+                struct ovn_port *dgp = od->l3dgw_ports[i];
+                build_distr_lrouter_nat_flows_for_lb(&ctx, type, od,
+                                                     lb_dps->lflow_ref, dgp);
+            }
         }
 
         if (lb->affinity_timeout) {
diff --git a/tests/multinode-macros.at b/tests/multinode-macros.at
index 757917626..2f69433fc 100644
--- a/tests/multinode-macros.at
+++ b/tests/multinode-macros.at
@@ -40,6 +40,27 @@ m4_define([M_START_TCPDUMP],
     ]
 )
 
+# M_EXEC([fake_node], [command])
+#
+# Execute 'command' in 'fakenode'
+m4_define([M_EXEC],
+    [podman exec $1 $2])
+
+# M_CHECK_EXEC([fake_node], [command], other_params...)
+#
+# Wrapper for AT_CHECK that executes 'command' inside 'fake_node''s'.
+# 'other_params' as passed as they are to AT_CHECK.
+m4_define([M_CHECK_EXEC],
+    [ AT_CHECK([M_EXEC([$1], [$2])], m4_shift(m4_shift(m4_shift($@)))) ]
+)
+
+# M_FORMAT_CT([ip-addr])
+#
+# Strip content from the piped input which would differ from test to test
+# and limit the output to the rows containing 'ip-addr'.
+#
+m4_define([M_FORMAT_CT],
+    [[grep -F "dst=$1," | sed -e 's/id=[0-9]*/id=<cleared>/g' -e 
's/state=[0-9_A-Z]*/state=<cleared>/g' | sort | uniq | sed -e 
's/zone=[[0-9]]*/zone=<cleared>/' -e 's/mark=[[0-9]]*/mark=<cleared>/' ]])
 
 OVS_START_SHELL_HELPERS
 
@@ -76,6 +97,25 @@ multinode_nbctl () {
     m_as ovn-central ovn-nbctl "$@"
 }
 
+check_fake_multinode_setup_by_nodes() {
+    check m_as ovn-central ovn-nbctl --wait=sb sync
+    for c in $1
+    do
+        AT_CHECK([m_as $c ovn-appctl -t ovn-controller version], [0], [ignore])
+    done
+}
+
+cleanup_multinode_resources_by_nodes() {
+    m_as ovn-central rm -f /etc/ovn/ovnnb_db.db
+    m_as ovn-central /usr/share/ovn/scripts/ovn-ctl restart_northd
+    check m_as ovn-central ovn-nbctl --wait=sb sync
+    for c in $1
+    do
+        m_as $c ovs-vsctl del-br br-int
+        m_as $c ip --all netns delete
+    done
+}
+
 # m_count_rows TABLE [CONDITION...]
 #
 # Prints the number of rows in TABLE (that satisfy CONDITION).
diff --git a/tests/multinode.at b/tests/multinode.at
index a0eb8fc67..88a46f3b2 100644
--- a/tests/multinode.at
+++ b/tests/multinode.at
@@ -1591,3 +1591,553 @@ AT_CHECK([cat ch1_eth2.tcpdump], [0], [dnl
 ])
 
 AT_CLEANUP
+
+AT_SETUP([ovn multinode load-balancer with multiple DGPs and multiple chassis])
+
+# Check that ovn-fake-multinode setup is up and running - requires additional 
nodes
+check_fake_multinode_setup_by_nodes 'ovn-chassis-1 ovn-chassis-2 ovn-chassis-3 
ovn-chassis-4 ovn-gw-1 ovn-gw-2'
+
+# Delete the multinode NB and OVS resources before starting the test.
+cleanup_multinode_resources_by_nodes 'ovn-chassis-1 ovn-chassis-2 
ovn-chassis-3 ovn-chassis-4 ovn-gw-1 ovn-gw-2'
+
+# Network topology
+#
+#             publicp1 (ovn-chassis-3) (20.0.0.3/24)
+#                |
+#              overlay
+#                |
+#      DGP public1 (ovn-gw-1) (20.0.0.1/24)
+#                |
+#                |
+#                |
+#               lr0 ------- sw0 --- sw0p1 (ovn-chassis-1) 10.0.0.3/24
+#                |           |
+#                |           + ---  sw0p2 (ovn-chassis-2) 10.0.0.4/24
+#                |
+#      DGP public2 (ovn-gw-2) (30.0.0.1/24)
+#                |
+#              overlay
+#                |
+#             publicp2 (ovn-chassis-4) (30.0.0.3/24)
+
+# Delete already used ovs-ports
+m_as ovn-chassis-1 ovs-vsctl del-port br-int sw0p1-p
+m_as ovn-chassis-2 ovs-vsctl del-port br-int sw0p2-p
+m_as ovn-chassis-1 ip link del sw0p1-p
+m_as ovn-chassis-2 ip link del sw0p2-p
+m_as ovn-chassis-3 ovs-vsctl del-port br-int publicp1-p
+m_as ovn-chassis-4 ovs-vsctl del-port br-int publicp2-p
+m_as ovn-chassis-3 ip link del publicp1-p
+m_as ovn-chassis-4 ip link del publicp2-p
+
+# Create East-West switch for LB backends
+check multinode_nbctl ls-add sw0
+check multinode_nbctl lsp-add sw0 sw0-port1
+check multinode_nbctl lsp-set-addresses sw0-port1 "50:54:00:00:00:03 10.0.0.3 
1000::3"
+check multinode_nbctl lsp-add sw0 sw0-port2
+check multinode_nbctl lsp-set-addresses sw0-port2 "50:54:00:00:00:04 10.0.0.4 
1000::4"
+
+m_as ovn-chassis-1 /data/create_fake_vm.sh sw0-port1 sw0p1 50:54:00:00:00:03 
1400 10.0.0.3 24 10.0.0.1 1000::3/64 1000::a
+m_as ovn-chassis-2 /data/create_fake_vm.sh sw0-port2 sw0p2 50:54:00:00:00:04 
1400 10.0.0.4 24 10.0.0.1 1000::4/64 1000::a
+
+m_wait_for_ports_up
+
+M_NS_CHECK_EXEC([ovn-chassis-1], [sw0p1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.4 | 
FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+M_NS_CHECK_EXEC([ovn-chassis-2], [sw0p2], [ping -q -c 3 -i 0.3 -w 2 10.0.0.3 | 
FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+# Create a logical router and attach to sw0
+check multinode_nbctl lr-add lr0
+check multinode_nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24 
1000::a/64
+check multinode_nbctl lsp-add sw0 sw0-lr0
+check multinode_nbctl lsp-set-type sw0-lr0 router
+check multinode_nbctl lsp-set-addresses sw0-lr0 router
+check multinode_nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
+
+# create external connection for N/S traffic using multiple DGPs
+check multinode_nbctl ls-add public
+
+# DGP public1
+check multinode_nbctl lsp-add public ln-public-1
+check multinode_nbctl lsp-set-type ln-public-1 localnet
+check multinode_nbctl lsp-set-addresses ln-public-1 unknown
+check multinode_nbctl lsp-set-options ln-public-1 network_name=public1
+
+# DGP public2
+# create exteranl connection for N/S traffic
+check multinode_nbctl lsp-add public ln-public-2
+check multinode_nbctl lsp-set-type ln-public-2 localnet
+check multinode_nbctl lsp-set-addresses ln-public-2 unknown
+check multinode_nbctl lsp-set-options ln-public-2 network_name=public2
+
+# Attach DGP public1 to GW-1 and chassis-3 (overlay connectivity)
+m_as ovn-gw-1 ovs-vsctl set open . 
external-ids:ovn-bridge-mappings=public1:br-ex
+m_as ovn-chassis-3 ovs-vsctl set open . 
external-ids:ovn-bridge-mappings=public1:br-ex
+
+# Attach DGP public2 to GW-2 and chassis-4 (overlay connectivity)
+m_as ovn-gw-2 ovs-vsctl set open . 
external-ids:ovn-bridge-mappings=public2:br-ex
+m_as ovn-chassis-4 ovs-vsctl set open . 
external-ids:ovn-bridge-mappings=public2:br-ex
+
+# Create the external LR0 port to the DGP public1
+check multinode_nbctl lsp-add public public-port1
+check multinode_nbctl lsp-set-addresses public-port1 "40:54:00:00:00:03 
20.0.0.3 2000::3"
+
+check multinode_nbctl lrp-add lr0 lr0-public-p1 00:00:00:00:ff:02 20.0.0.1/24 
2000::a/64
+check multinode_nbctl lsp-add public public-lr0-p1
+check multinode_nbctl lsp-set-type public-lr0-p1 router
+check multinode_nbctl lsp-set-addresses public-lr0-p1 router
+check multinode_nbctl lsp-set-options public-lr0-p1 router-port=lr0-public-p1
+check multinode_nbctl lrp-set-gateway-chassis lr0-public-p1 ovn-gw-1 10
+
+# Create a VM on ovn-chassis-3 in the same public1 overlay
+m_as ovn-chassis-3 /data/create_fake_vm.sh public-port1 publicp1 
40:54:00:00:00:03 1400 20.0.0.3 24 20.0.0.1 2000::4/64 2000::a
+
+m_wait_for_ports_up public-port1
+
+M_NS_CHECK_EXEC([ovn-chassis-3], [publicp1], [ping -q -c 3 -i 0.3 -w 2 
20.0.0.1 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+# Create the external LR0 port to the DGP public2
+check multinode_nbctl lsp-add public public-port2
+check multinode_nbctl lsp-set-addresses public-port2 "60:54:00:00:00:03 
30.0.0.3 3000::3"
+
+check multinode_nbctl lrp-add lr0 lr0-public-p2 00:00:00:00:ff:03 30.0.0.1/24 
3000::a/64
+check multinode_nbctl lsp-add public public-lr0-p2
+check multinode_nbctl lsp-set-type public-lr0-p2 router
+check multinode_nbctl lsp-set-addresses public-lr0-p2 router
+check multinode_nbctl lsp-set-options public-lr0-p2 router-port=lr0-public-p2
+check multinode_nbctl lrp-set-gateway-chassis lr0-public-p2 ovn-gw-2 10
+
+# Create a VM on ovn-chassis-4 in the same public2 overlay
+m_as ovn-chassis-4 /data/create_fake_vm.sh public-port2 publicp2 
60:54:00:00:00:03 1400 30.0.0.3 24 30.0.0.1 3000::4/64 3000::a
+
+m_wait_for_ports_up public-port2
+
+M_NS_CHECK_EXEC([ovn-chassis-4], [publicp2], [ping -q -c 3 -i 0.3 -w 2 
30.0.0.1 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+# Add a default route for multiple DGPs - using ECMP
+####check multinode_nbctl --ecmp lr-route-add lr0 0.0.0.0/0 20.0.0.3
+####check multinode_nbctl --ecmp lr-route-add lr0 0.0.0.0/0 30.0.0.3
+
+# Add SNAT rules using gateway-port
+check multinode_nbctl --gateway-port lr0-public-p1 lr-nat-add lr0 snat 
20.0.0.1 10.0.0.0/24
+check multinode_nbctl --gateway-port lr0-public-p2 lr-nat-add lr0 snat 
30.0.0.1 10.0.0.0/24
+
+M_NS_CHECK_EXEC([ovn-chassis-1], [sw0p1], [ping -q -c 3 -i 0.3 -w 2 20.0.0.3 | 
FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+M_NS_CHECK_EXEC([ovn-chassis-2], [sw0p2], [ping -q -c 3 -i 0.3 -w 2 30.0.0.3 | 
FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+# create LB
+check multinode_nbctl lb-add lb0 "172.16.0.100:80" "10.0.0.3:80,10.0.0.4:80"
+check multinode_nbctl lr-lb-add lr0 lb0
+check multinode_nbctl ls-lb-add sw0 lb0
+
+# Start backend http services
+M_NS_DAEMONIZE([ovn-chassis-1], [sw0p1], [$PYTHON -m http.server --bind 
10.0.0.3 80 >/dev/null 2>&1], [http1.pid])
+M_NS_DAEMONIZE([ovn-chassis-2], [sw0p2], [$PYTHON -m http.server --bind 
10.0.0.4 80 >/dev/null 2>&1], [http2.pid])
+
+# wait for http server be ready
+sleep 2
+
+# Flush conntrack entries for easier output parsing of next test.
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v 172.16.0.100:80 --retry 
3 --max-time 1 --local-port 59002 2> curl.out'])
+M_NS_CHECK_EXEC([ovn-chassis-3], [publicp1], [sh -c 'cat curl.out | grep -i -e 
connect | grep -v 'Server:''], \
+[0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+* Closing connection
+])
+
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v 172.16.0.100:80 --retry 
3 --max-time 1 --local-port 59003 2> curl.out'])
+M_NS_CHECK_EXEC([ovn-chassis-4], [publicp2], [sh -c 'cat curl.out | grep -i -e 
connect | grep -v 'Server:''], \
+[0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+* Closing connection
+])
+
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v 172.16.0.100:80 --retry 
3 --max-time 1 --local-port 59001'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | 
M_FORMAT_CT(20.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59001,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59001),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59001,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59001),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v 172.16.0.100:80 --retry 
3 --max-time 1 --local-port 59000'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | 
M_FORMAT_CT(30.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59000,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59000),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59000,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59000),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# create a big file on web servers for download
+M_NS_EXEC([ovn-chassis-1], [sw0p1], [dd bs=512 count=200000 if=/dev/urandom 
of=download_file])
+M_NS_EXEC([ovn-chassis-2], [sw0p2], [dd bs=512 count=200000 if=/dev/urandom 
of=download_file])
+
+# Flush conntrack entries for easier output parsing of next test.
+m_as ovn-chassis-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-chassis-2 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 --local-port 59004 
2>curl.out'])
+
+gw1_ct=$(m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+gw2_ct=$(m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_ct=$(m_as ovn-chassis-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_ct=$(m_as ovn-chassis-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_flow=$(m_as ovn-chassis-1 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_flow=$(m_as ovn-chassis-2 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 ip netns exec publicp1 cat curl.out | \
+grep -i -e connect | grep -v 'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+* Closing connection
+])
+
+# Check if we have only one backend for the same connection - orig + dest ports
+OVS_WAIT_FOR_OUTPUT([echo -e $gw1_ct | M_FORMAT_CT(20.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59004,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59004),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59004,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59004),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Check if gw-2 is empty to ensure that the traffic only come from/to the 
originator chassis via DGP public1
+AT_CHECK([echo -e $gw2_ct | grep "20.0.0.3" -c], [1], [dnl
+0
+])
+
+# Check the backend IP from ct entries on gw-1 (DGP public1)
+backend_check=$(echo -e $gw1_ct | grep "10.0.0.3" | grep "dport=80" -c)
+
+if [[ $backend_check -gt 0 ]]; then
+# Backend resides on ovn-chassis-1
+AT_CHECK([echo -e $chassis1_ct | M_FORMAT_CT(20.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=10.0.0.3,sport=59004,dport=80),reply=(src=10.0.0.3,dst=20.0.0.3,sport=80,dport=59004),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-1
+AT_CHECK([echo -e $chassis2_ct | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis2_flow | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+else
+# Backend resides on ovn-chassis-2
+AT_CHECK([echo -e $chassis2_ct | M_FORMAT_CT(20.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=10.0.0.4,sport=59004,dport=80),reply=(src=10.0.0.4,dst=20.0.0.3,sport=80,dport=59004),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-2
+AT_CHECK([echo -e $chassis1_ct | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis1_flow | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+fi
+
+# Flush conntrack entries for easier output parsing of next test.
+m_as ovn-chassis-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-chassis-2 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+# Check the flows again for a new source port
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 --local-port 59005 
2>curl.out'])
+
+gw1_ct=$(m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+gw2_ct=$(m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_ct=$(m_as ovn-chassis-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_ct=$(m_as ovn-chassis-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_flow=$(m_as ovn-chassis-1 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_flow=$(m_as ovn-chassis-2 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 ip netns exec publicp1 cat curl.out | \
+grep -i -e connect | grep -v 'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+* Closing connection
+])
+
+# Check if we have only one backend for the same connection - orig + dest ports
+OVS_WAIT_FOR_OUTPUT([echo -e $gw1_ct | M_FORMAT_CT(20.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59005,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59005),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59005,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59005),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Check if gw-2 is empty to ensure that the traffic only come from/to the 
originator chassis via DGP public1
+AT_CHECK([echo -e $gw2_ct | grep "20.0.0.3" -c], [1], [dnl
+0
+])
+
+# Check the backend IP from ct entries on gw-1 (DGP public1)
+backend_check=$(echo -e $gw1_ct | grep "10.0.0.3" | grep "dport=80" -c)
+
+if [[ $backend_check -gt 0 ]]; then
+# Backend resides on ovn-chassis-1
+AT_CHECK([echo -e $chassis1_ct | M_FORMAT_CT(20.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=10.0.0.3,sport=59005,dport=80),reply=(src=10.0.0.3,dst=20.0.0.3,sport=80,dport=59005),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-1
+AT_CHECK([echo -e $chassis2_ct | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis2_flow | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+else
+# Backend resides on ovn-chassis-2
+AT_CHECK([echo -e $chassis2_ct | M_FORMAT_CT(20.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=10.0.0.4,sport=59005,dport=80),reply=(src=10.0.0.4,dst=20.0.0.3,sport=80,dport=59005),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-2
+AT_CHECK([echo -e $chassis1_ct | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis1_flow | grep "20.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+fi
+
+# Flush conntrack entries for easier output parsing of next test.
+m_as ovn-chassis-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-chassis-2 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+# Start a new test using the second DGP as origin (public2)
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 --local-port 59006 
2>curl.out'])
+
+gw1_ct=$(m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+gw2_ct=$(m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_ct=$(m_as ovn-chassis-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_ct=$(m_as ovn-chassis-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_flow=$(m_as ovn-chassis-1 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_flow=$(m_as ovn-chassis-2 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-4 ip netns exec publicp2 cat curl.out | \
+grep -i -e connect | grep -v 'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+* Closing connection
+])
+
+# Check if we have only one backend for the same connection - orig + dest ports
+OVS_WAIT_FOR_OUTPUT([echo -e $gw2_ct | M_FORMAT_CT(30.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59006,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59006),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59006,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59006),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Check if gw-1 is empty to ensure that the traffic only come from/to the 
originator chassis via DGP public2
+AT_CHECK([echo -e $gw1_ct | grep "30.0.0.3" -c], [1], [dnl
+0
+])
+
+# Check the backend IP from ct entries on gw-2 (DGP public2)
+backend_check=$(echo -e $gw2_ct | grep "10.0.0.3" | grep "dport=80" -c)
+
+if [[ $backend_check -gt 0 ]]; then
+# Backend resides on ovn-chassis-1
+AT_CHECK([echo -e $chassis1_ct | M_FORMAT_CT(30.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=10.0.0.3,sport=59006,dport=80),reply=(src=10.0.0.3,dst=30.0.0.3,sport=80,dport=59006),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-1
+AT_CHECK([echo -e $chassis2_ct | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis2_flow | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+else
+# Backend resides on ovn-chassis-2
+AT_CHECK([echo -e $chassis2_ct | M_FORMAT_CT(30.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=10.0.0.4,sport=59006,dport=80),reply=(src=10.0.0.4,dst=30.0.0.3,sport=80,dport=59006),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-2
+AT_CHECK([echo -e $chassis1_ct | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis1_flow | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+fi
+
+# Flush conntrack entries for easier output parsing of next test.
+m_as ovn-chassis-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-chassis-2 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+# Check the flows again for a new source port using the second DGP as origin 
(public2)
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 --local-port 59007 
2>curl.out'])
+
+gw1_ct=$(m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+gw2_ct=$(m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_ct=$(m_as ovn-chassis-1 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_ct=$(m_as ovn-chassis-2 ovs-appctl dpctl/dump-conntrack | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis1_flow=$(m_as ovn-chassis-1 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+chassis2_flow=$(m_as ovn-chassis-2 ovs-dpctl dump-flows | sed 
':a;N;$!ba;s/\n/\\n/g')
+
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-4 ip netns exec publicp2 cat curl.out | \
+grep -i -e connect | grep -v 'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+* Closing connection
+])
+
+# Check if we have only one backend for the same connection - orig + dest ports
+OVS_WAIT_FOR_OUTPUT([echo -e $gw2_ct | M_FORMAT_CT(30.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59007,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59007),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59007,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59007),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Check if gw-1 is empty to ensure that the traffic only come from/to the 
originator chassis via DGP public2
+AT_CHECK([echo -e $gw1_ct | grep "30.0.0.3" -c], [1], [dnl
+0
+])
+
+# Check the backend IP from ct entries on gw-1 (DGP public1)
+backend_check=$(echo -e $gw2_ct | grep "10.0.0.3" | grep "dport=80" -c)
+
+if [[ $backend_check -gt 0 ]]; then
+# Backend resides on ovn-chassis-1
+AT_CHECK([echo -e $chassis1_ct | M_FORMAT_CT(30.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=10.0.0.3,sport=59007,dport=80),reply=(src=10.0.0.3,dst=30.0.0.3,sport=80,dport=59007),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-1
+AT_CHECK([echo -e $chassis2_ct | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis2_flow | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+else
+# Backend resides on ovn-chassis-2
+AT_CHECK([echo -e $chassis2_ct | M_FORMAT_CT(30.0.0.3) | \
+grep tcp], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=10.0.0.4,sport=59007,dport=80),reply=(src=10.0.0.4,dst=30.0.0.3,sport=80,dport=59007),zone=<cleared>,protoinfo=(state=<cleared>)
+])
+
+# Ensure that the traffic only come from ovn-chassis-2
+AT_CHECK([echo -e $chassis1_ct | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+AT_CHECK([echo -e $chassis1_flow | grep "30.0.0.3" | grep "dport=80" -c], [1], 
[dnl
+0
+])
+fi
+
+# Check multiple requests coming from DGP's public1 and public2
+
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 2>curl.out'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-4 cat curl.out | \
+sed 's/\(.*\)200 OK/200 OK\n/' | grep -i -e connect -e "200 OK" | grep -v 
'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+200 OK
+* Closing connection
+])
+
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 2>curl.out'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat curl.out | \
+sed 's/\(.*\)200 OK/200 OK\n/' | grep -i -e connect -e "200 OK" | grep -v 
'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+200 OK
+* Closing connection
+])
+
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 2>curl.out'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-4 cat curl.out | \
+sed 's/\(.*\)200 OK/200 OK\n/' | grep -i -e connect -e "200 OK" | grep -v 
'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+200 OK
+* Closing connection
+])
+
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v -O 
172.16.0.100:80/download_file --retry 3 --max-time 1 2>curl.out'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat curl.out | \
+sed 's/\(.*\)200 OK/200 OK\n/' | grep -i -e connect -e "200 OK" | grep -v 
'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 80
+200 OK
+* Closing connection
+])
+
+# Remove the LB and change the VIP port - different from the backend ports
+check multinode_nbctl lb-del lb0
+
+# create LB again
+check multinode_nbctl lb-add lb0 "172.16.0.100:9000" "10.0.0.3:80,10.0.0.4:80"
+check multinode_nbctl lr-lb-add lr0 lb0
+check multinode_nbctl ls-lb-add sw0 lb0
+
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+# Check end-to-end request using a new port for VIP
+M_NS_EXEC([ovn-chassis-3], [publicp1], [sh -c 'curl -v -O 
172.16.0.100:9000/download_file --retry 3 --max-time 1 --local-port 59008 
2>curl.out'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-1 ovs-appctl dpctl/dump-conntrack | 
M_FORMAT_CT(20.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59008,dport=80),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59008),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=20.0.0.3,dst=<cleared>,sport=59008,dport=9000),reply=(src=<cleared>,dst=20.0.0.3,sport=80,dport=59008),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+])
+
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat curl.out | \
+sed 's/\(.*\)200 OK/200 OK\n/' | grep -i -e connect -e "200 OK" | grep -v 
'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 9000
+200 OK
+* Closing connection
+])
+
+m_as ovn-gw-1 ovs-appctl dpctl/flush-conntrack
+m_as ovn-gw-2 ovs-appctl dpctl/flush-conntrack
+
+# Check end-to-end request using a new port for VIP
+M_NS_EXEC([ovn-chassis-4], [publicp2], [sh -c 'curl -v -O 
172.16.0.100:9000/download_file --retry 3 --max-time 1 --local-port 59008 
2>curl.out'])
+OVS_WAIT_FOR_OUTPUT([m_as ovn-gw-2 ovs-appctl dpctl/dump-conntrack | 
M_FORMAT_CT(30.0.0.3) | \
+grep tcp | sed -E -e 's/10.0.0.3|10.0.0.4/<cleared>/g' | sort], [0], [dnl
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59008,dport=80),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59008),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=30.0.0.3,dst=<cleared>,sport=59008,dport=9000),reply=(src=<cleared>,dst=30.0.0.3,sport=80,dport=59008),zone=<cleared>,mark=<cleared>,protoinfo=(state=<cleared>)
+])
+
+OVS_WAIT_FOR_OUTPUT([m_as ovn-chassis-3 cat curl.out | \
+sed 's/\(.*\)200 OK/200 OK\n/' | grep -i -e connect -e "200 OK" | grep -v 
'Server:'], [0], [dnl
+* Connected to 172.16.0.100 (172.16.0.100) port 9000
+200 OK
+* Closing connection
+])
+
+AT_CLEANUP
\ No newline at end of file
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 93ccbce6b..1acc6f9a2 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -13867,3 +13867,225 @@ check_no_redirect
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD_NO_HV_PARALLELIZATION([
+AT_SETUP([Load balancer with Distributed Gateway Ports (LB + DGP + NAT 
Stateless)])
+ovn_start
+
+check ovn-nbctl ls-add public
+check ovn-nbctl lr-add lr1
+
+# lr1 DGP ts1
+check ovn-nbctl ls-add ts1
+check ovn-nbctl lrp-add lr1 lr1-ts1 00:00:01:02:03:04 172.16.10.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-ts1 chassis-2
+
+# lr1 DGP ts2
+check ovn-nbctl ls-add ts2
+check ovn-nbctl lrp-add lr1 lr1-ts2 00:00:01:02:03:05 172.16.20.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-ts2 chassis-3
+
+# lr1 DGP public
+check ovn-nbctl lrp-add lr1 lr1_public 00:de:ad:ff:00:01 173.16.0.1/16
+check ovn-nbctl lrp-add lr1 lr1_s1 00:de:ad:fe:00:02 172.16.0.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1_public chassis-1
+
+check ovn-nbctl ls-add s1
+# s1 - lr1
+check ovn-nbctl lsp-add s1 s1_lr1
+check ovn-nbctl lsp-set-type s1_lr1 router
+check ovn-nbctl lsp-set-addresses s1_lr1 "00:de:ad:fe:00:02 172.16.0.1"
+check ovn-nbctl lsp-set-options s1_lr1 router-port=lr1_s1
+
+# s1 - backend vm1
+check ovn-nbctl lsp-add s1 vm1
+check ovn-nbctl lsp-set-addresses vm1 "00:de:ad:01:00:01 172.16.0.101"
+
+# s1 - backend vm2
+check ovn-nbctl lsp-add s1 vm2
+check ovn-nbctl lsp-set-addresses vm2 "00:de:ad:01:00:02 172.16.0.102"
+
+# s1 - backend vm3
+check ovn-nbctl lsp-add s1 vm3
+check ovn-nbctl lsp-set-addresses vm3 "00:de:ad:01:00:03 172.16.0.103"
+
+# Add the lr1 DGP ts1 to the public switch
+check ovn-nbctl lsp-add public public_lr1_ts1
+check ovn-nbctl lsp-set-type public_lr1_ts1 router
+check ovn-nbctl lsp-set-addresses public_lr1_ts1 router
+check ovn-nbctl lsp-set-options public_lr1_ts1 router-port=lr1-ts1 
nat-addresses=router
+
+# Add the lr1 DGP ts2 to the public switch
+check ovn-nbctl lsp-add public public_lr1_ts2
+check ovn-nbctl lsp-set-type public_lr1_ts2 router
+check ovn-nbctl lsp-set-addresses public_lr1_ts2 router
+check ovn-nbctl lsp-set-options public_lr1_ts2 router-port=lr1-ts2 
nat-addresses=router
+
+# Add the lr1 DGP public to the public switch
+check ovn-nbctl lsp-add public public_lr1
+check ovn-nbctl lsp-set-type public_lr1 router
+check ovn-nbctl lsp-set-addresses public_lr1 router
+check ovn-nbctl lsp-set-options public_lr1 router-port=lr1_public 
nat-addresses=router
+
+# Create the Load Balancer lb1
+check ovn-nbctl --wait=sb lb-add lb1 "30.0.0.1" 
"172.16.0.103,172.16.0.102,172.16.0.101"
+
+# Associate load balancer to s1
+check ovn-nbctl ls-lb-add s1 lb1
+check ovn-nbctl --wait=sb sync
+
+ovn-sbctl dump-flows s1 > s1flows
+AT_CAPTURE_FILE([s1flows])
+
+AT_CHECK([grep "ls_in_pre_stateful" s1flows | ovn_strip_lflows | grep 
"30.0.0.1"], [0], [dnl
+  table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && 
ip4.dst == 30.0.0.1), action=(reg1 = 30.0.0.1; ct_lb_mark;)
+])
+AT_CHECK([grep "ls_in_lb" s1flows | ovn_strip_lflows | grep "30.0.0.1"], [0], 
[dnl
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip4.dst == 
30.0.0.1), action=(ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
+])
+
+# Associate load balancer to lr1 with DGP
+check ovn-nbctl lr-lb-add lr1 lb1
+check ovn-nbctl --wait=sb sync
+
+ovn-sbctl dump-flows lr1 > lr1flows
+AT_CAPTURE_FILE([lr1flows])
+
+# Check stateless NAT rules for load balancer with multiple DGP
+# 1. Check if the backend IPs are in the ipX.dst action
+AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep "30.0.0.1"], 
[0], [dnl
+  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
ip4 && ip4.dst == 30.0.0.1 && is_chassis_resident("cr-lr1-ts1")), 
action=(ip4.dst=172.16.0.103;ip4.dst=172.16.0.102;ip4.dst=172.16.0.101;ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
+  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
ip4 && ip4.dst == 30.0.0.1 && is_chassis_resident("cr-lr1-ts2")), 
action=(ip4.dst=172.16.0.103;ip4.dst=172.16.0.102;ip4.dst=172.16.0.101;ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
+  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
ip4 && ip4.dst == 30.0.0.1 && is_chassis_resident("cr-lr1_public")), 
action=(ip4.dst=172.16.0.103;ip4.dst=172.16.0.102;ip4.dst=172.16.0.101;ct_lb_mark(backends=172.16.0.103,172.16.0.102,172.16.0.101);)
+])
+
+# 2. Check if the DGP ports are in the match with action next
+AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
+  table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
(inport == "lr1-ts1" || outport == "lr1-ts1") && 
is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
+  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
(inport == "lr1-ts2" || outport == "lr1-ts2") && 
is_chassis_resident("cr-lr1-ts2") && tcp), action=(next;)
+  table=??(lr_out_undnat      ), priority=120  , match=(ip4 && ((ip4.src == 
172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
(inport == "lr1_public" || outport == "lr1_public") && 
is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
+])
+
+# 3. Check if the VIP IP is in the ipX.src action
+AT_CHECK([grep "lr_out_snat" lr1flows | ovn_strip_lflows], [0], [dnl
+  table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
+  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
(inport == "lr1-ts1" || outport == "lr1-ts1") && 
is_chassis_resident("cr-lr1-ts1") && tcp), action=(ip4.src=30.0.0.1; next;)
+  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
(inport == "lr1-ts2" || outport == "lr1-ts2") && 
is_chassis_resident("cr-lr1-ts2") && tcp), action=(ip4.src=30.0.0.1; next;)
+  table=??(lr_out_snat        ), priority=160  , match=(ip4 && ((ip4.src == 
172.16.0.103) || (ip4.src == 172.16.0.102) || (ip4.src == 172.16.0.101)) && 
(inport == "lr1_public" || outport == "lr1_public") && 
is_chassis_resident("cr-lr1_public") && tcp), action=(ip4.src=30.0.0.1; next;)
+])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV_PARALLELIZATION([
+AT_SETUP([Load balancer with Distributed Gateway Ports (LB + DGP + NAT 
Stateless) - IPv6])
+ovn_start
+
+check ovn-nbctl ls-add public
+check ovn-nbctl lr-add lr1
+
+# lr1 DGP ts1
+check ovn-nbctl ls-add ts1
+check ovn-nbctl lrp-add lr1 lr1-ts1 00:00:01:02:03:04 2001:db8:aaaa:1::1/64
+check ovn-nbctl lrp-set-gateway-chassis lr1-ts1 chassis-2
+
+# lr1 DGP ts2
+check ovn-nbctl ls-add ts2
+check ovn-nbctl lrp-add lr1 lr1-ts2 00:00:01:02:03:05 2001:db8:aaaa:2::1/64
+check ovn-nbctl lrp-set-gateway-chassis lr1-ts2 chassis-3
+
+# lr1 DGP public
+check ovn-nbctl lrp-add lr1 lr1_public 00:de:ad:ff:00:01 2001:db8:bbbb::1/64
+check ovn-nbctl lrp-add lr1 lr1_s1 00:de:ad:fe:00:02 2001:db8:aaaa:3::1/64
+check ovn-nbctl lrp-set-gateway-chassis lr1_public chassis-1
+
+check ovn-nbctl ls-add s1
+# s1 - lr1
+check ovn-nbctl lsp-add s1 s1_lr1
+check ovn-nbctl lsp-set-type s1_lr1 router
+check ovn-nbctl lsp-set-addresses s1_lr1 "00:de:ad:fe:00:02 2001:db8:aaaa:3::1"
+check ovn-nbctl lsp-set-options s1_lr1 router-port=lr1_s1
+
+# s1 - backend vm1
+check ovn-nbctl lsp-add s1 vm1
+check ovn-nbctl lsp-set-addresses vm1 "00:de:ad:01:00:01 2001:db8:aaaa:3::101"
+
+# s1 - backend vm2
+check ovn-nbctl lsp-add s1 vm2
+check ovn-nbctl lsp-set-addresses vm2 "00:de:ad:01:00:02 2001:db8:aaaa:3::102"
+
+# s1 - backend vm3
+check ovn-nbctl lsp-add s1 vm3
+check ovn-nbctl lsp-set-addresses vm3 "00:de:ad:01:00:03 2001:db8:aaaa:3::103"
+
+# Add the lr1 DGP ts1 to the public switch
+check ovn-nbctl lsp-add public public_lr1_ts1
+check ovn-nbctl lsp-set-type public_lr1_ts1 router
+check ovn-nbctl lsp-set-addresses public_lr1_ts1 router
+check ovn-nbctl lsp-set-options public_lr1_ts1 router-port=lr1-ts1 
nat-addresses=router
+
+# Add the lr1 DGP ts2 to the public switch
+check ovn-nbctl lsp-add public public_lr1_ts2
+check ovn-nbctl lsp-set-type public_lr1_ts2 router
+check ovn-nbctl lsp-set-addresses public_lr1_ts2 router
+check ovn-nbctl lsp-set-options public_lr1_ts2 router-port=lr1-ts2 
nat-addresses=router
+
+# Add the lr1 DGP public to the public switch
+check ovn-nbctl lsp-add public public_lr1
+check ovn-nbctl lsp-set-type public_lr1 router
+check ovn-nbctl lsp-set-addresses public_lr1 router
+check ovn-nbctl lsp-set-options public_lr1 router-port=lr1_public 
nat-addresses=router
+
+# Create the Load Balancer lb1
+check ovn-nbctl --wait=sb lb-add lb1 "2001:db8:cccc::1" 
"2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101"
+
+# Associate load balancer to s1
+check ovn-nbctl ls-lb-add s1 lb1
+check ovn-nbctl --wait=sb sync
+
+ovn-sbctl dump-flows s1 > s1flows
+AT_CAPTURE_FILE([s1flows])
+
+AT_CHECK([grep "ls_in_pre_stateful" s1flows | ovn_strip_lflows | grep 
"2001:db8:cccc::1"], [0], [dnl
+  table=??(ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && 
ip6.dst == 2001:db8:cccc::1), action=(xxreg1 = 2001:db8:cccc::1; ct_lb_mark;)
+])
+AT_CHECK([grep "ls_in_lb" s1flows | ovn_strip_lflows | grep 
"2001:db8:cccc::1"], [0], [dnl
+  table=??(ls_in_lb           ), priority=110  , match=(ct.new && ip6.dst == 
2001:db8:cccc::1), 
action=(ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
+])
+
+# Associate load balancer to lr1 with DGP
+check ovn-nbctl lr-lb-add lr1 lb1
+check ovn-nbctl --wait=sb sync
+
+ovn-sbctl dump-flows lr1 > lr1flows
+AT_CAPTURE_FILE([lr1flows])
+
+# Check stateless NAT rules for load balancer with multiple DGP
+# 1. Check if the backend IPs are in the ipX.dst action
+AT_CHECK([grep "lr_in_dnat" lr1flows | ovn_strip_lflows | grep 
"2001:db8:cccc::1"], [0], [dnl
+  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
ip6 && ip6.dst == 2001:db8:cccc::1 && is_chassis_resident("cr-lr1-ts1")), 
action=(ip6.dst=2001:db8:aaaa:3::103;ip6.dst=2001:db8:aaaa:3::102;ip6.dst=2001:db8:aaaa:3::101;ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
+  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
ip6 && ip6.dst == 2001:db8:cccc::1 && is_chassis_resident("cr-lr1-ts2")), 
action=(ip6.dst=2001:db8:aaaa:3::103;ip6.dst=2001:db8:aaaa:3::102;ip6.dst=2001:db8:aaaa:3::101;ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
+  table=??(lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && 
ip6 && ip6.dst == 2001:db8:cccc::1 && is_chassis_resident("cr-lr1_public")), 
action=(ip6.dst=2001:db8:aaaa:3::103;ip6.dst=2001:db8:aaaa:3::102;ip6.dst=2001:db8:aaaa:3::101;ct_lb_mark(backends=2001:db8:aaaa:3::103,2001:db8:aaaa:3::102,2001:db8:aaaa:3::101);)
+])
+
+# 2. Check if the DGP ports are in the match with action next
+AT_CHECK([grep "lr_out_undnat" lr1flows | ovn_strip_lflows], [0], [dnl
+  table=??(lr_out_undnat      ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
2001:db8:aaaa:3::101)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
is_chassis_resident("cr-lr1-ts1") && tcp), action=(next;)
+  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
2001:db8:aaaa:3::101)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
is_chassis_resident("cr-lr1-ts2") && tcp), action=(next;)
+  table=??(lr_out_undnat      ), priority=120  , match=(ip6 && ((ip6.src == 
2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
2001:db8:aaaa:3::101)) && (inport == "lr1_public" || outport == "lr1_public") 
&& is_chassis_resident("cr-lr1_public") && tcp), action=(next;)
+])
+
+# 3. Check if the VIP IP is in the ipX.src action
+AT_CHECK([grep "lr_out_snat" lr1flows | ovn_strip_lflows], [0], [dnl
+  table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=??(lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
+  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
2001:db8:aaaa:3::101)) && (inport == "lr1-ts1" || outport == "lr1-ts1") && 
is_chassis_resident("cr-lr1-ts1") && tcp), action=(ip6.src=2001:db8:cccc::1; 
next;)
+  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
2001:db8:aaaa:3::101)) && (inport == "lr1-ts2" || outport == "lr1-ts2") && 
is_chassis_resident("cr-lr1-ts2") && tcp), action=(ip6.src=2001:db8:cccc::1; 
next;)
+  table=??(lr_out_snat        ), priority=160  , match=(ip6 && ((ip6.src == 
2001:db8:aaaa:3::103) || (ip6.src == 2001:db8:aaaa:3::102) || (ip6.src == 
2001:db8:aaaa:3::101)) && (inport == "lr1_public" || outport == "lr1_public") 
&& is_chassis_resident("cr-lr1_public") && tcp), 
action=(ip6.src=2001:db8:cccc::1; next;)
+])
+
+AT_CLEANUP
+])
-- 
2.34.1


-- 




_'Esta mensagem é direcionada apenas para os endereços constantes no 
cabeçalho inicial. Se você não está listado nos endereços constantes no 
cabeçalho, pedimos-lhe que desconsidere completamente o conteúdo dessa 
mensagem e cuja cópia, encaminhamento e/ou execução das ações citadas estão 
imediatamente anuladas e proibidas'._


* **'Apesar do Magazine Luiza tomar 
todas as precauções razoáveis para assegurar que nenhum vírus esteja 
presente nesse e-mail, a empresa não poderá aceitar a responsabilidade por 
quaisquer perdas ou danos causados por esse e-mail ou por seus anexos'.*



_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to