diff --git a/NEWS b/NEWS
index eefdae9bc..2a1c56b2d 100644
--- a/NEWS
+++ b/NEWS
@@ -33,6 +33,9 @@ OVN v21.06.0 - 18 Jun 2021
     "ovn-trim-limit-lflow-cache" and "ovn-trim-wmark-perc-lflow-cache", to
     allow enforcing a lflow cache size limit and high watermark percentage
     for which automatic memory trimming is performed.
+  - Support multiple distributed gateway ports on a single logical router.
+    (NAT and load-balancer are not supported yet when there are multiple
+    distributed gateway ports).
 
 OVN v21.03.0 - 12 Mar 2021
 -------------------------
diff --git a/northd/lrouter.dl b/northd/lrouter.dl
index 4a24f3f61..d37350ab8 100644
--- a/northd/lrouter.dl
+++ b/northd/lrouter.dl
@@ -138,14 +138,14 @@ Warning[message] :-
     var message = "Bad configuration: distributed gateway port configured on "
     "port ${lrp.name} on L3 gateway router".
 
-/* DistributedGatewayPortCandidate.
+/* Distributed gateway ports.
  *
- * Each row pairs a logical router with its distributed gateway port,
- * but without checking that there is at most one DGP per LR.
+ * Each row means 'lrp' is a distributed gateway port on 'lr_uuid'.
  *
- * (Use DistributedGatewayPort instead, since it guarantees uniqueness.) */
-relation DistributedGatewayPortCandidate(lr_uuid: uuid, lrp_uuid: uuid)
-DistributedGatewayPortCandidate(lr_uuid, lrp_uuid) :-
+ * A logical router can have multiple distributed gateway ports. */
+relation DistributedGatewayPort(lrp: Intern<nb::Logical_Router_Port>,
+                                lr_uuid: uuid)
+DistributedGatewayPort(lrp, lr_uuid) :-
     lr in nb::Logical_Router(._uuid = lr_uuid),
     LogicalRouterPort(lrp_uuid, lr._uuid),
     lrp in &nb::Logical_Router_Port(._uuid = lrp_uuid),
@@ -153,30 +153,10 @@ DistributedGatewayPortCandidate(lr_uuid, lrp_uuid) :-
     var has_hcg = lrp.ha_chassis_group.is_some(),
     var has_gc = not lrp.gateway_chassis.is_empty(),
     has_hcg or has_gc.
-Warning[message] :-
-    DistributedGatewayPortCandidate(lr_uuid, lrp_uuid),
-    var lrps = lrp_uuid.group_by(lr_uuid).to_set(),
-    lrps.size() > 1,
-    lr in nb::Logical_Router(._uuid = lr_uuid),
-    var message = "Bad configuration: multiple distributed gateway ports on "
-    "logical router ${lr.name}; ignoring all of them".
-
-/* Distributed gateway ports.
- *
- * Each row means 'lrp' is the distributed gateway port on 'lr_uuid'.
- *
- * There is at most one distributed gateway port per logical router. */
-relation DistributedGatewayPort(lrp: Intern<nb::Logical_Router_Port>, lr_uuid: uuid)
-DistributedGatewayPort(lrp, lr_uuid) :-
-    DistributedGatewayPortCandidate(lr_uuid, lrp_uuid),
-    var lrps = lrp_uuid.group_by(lr_uuid).to_set(),
-    lrps.size() == 1,
-    Some{var lrp_uuid} = lrps.nth(0),
-    lrp in &nb::Logical_Router_Port(._uuid = lrp_uuid).
 
 /* HAChassis is an abstraction over nb::Gateway_Chassis and nb::HA_Chassis, which
  * are different ways to represent the same configuration.  Each row is
- * effectively one HA_Chassis record.  (Usually, we could associated each
+ * effectively one HA_Chassis record.  (Usually, we could associate each
  * row with a particular 'lr_uuid', but it's permissible for more than one
  * logical router to use a HA chassis group, so we omit it so that multiple
  * references get merged.)
@@ -236,18 +216,20 @@ HAChassisGroup(ha_chassis_group_uuid(hac_group_uuid),
                         .name = name,
                         .external_ids = external_ids).
 
-/* Each row maps from a logical router to the name of its HAChassisGroup.
- * This level of indirection is needed because multiple logical routers
- * are allowed to reference a given HAChassisGroup. */
-relation LogicalRouterHAChassisGroup(lr_uuid: uuid,
-                                     hacg_uuid: uuid)
-LogicalRouterHAChassisGroup(lr_uuid, ha_chassis_group_uuid(lrp._uuid)) :-
-    DistributedGatewayPort(lrp, lr_uuid),
+/* Each row maps from a distributed gateway logical router port to the name of
+ * its HAChassisGroup.
+ * This level of indirection is needed because multiple distributed gateway
+ * logical router ports are allowed to reference a given HAChassisGroup. */
+relation DistributedGatewayPortHAChassisGroup(
+    lrp: Intern<nb::Logical_Router_Port>,
+    hacg_uuid: uuid)
+DistributedGatewayPortHAChassisGroup(lrp, ha_chassis_group_uuid(lrp._uuid)) :-
+    DistributedGatewayPort(.lrp = lrp),
     lrp.ha_chassis_group == None,
     lrp.gateway_chassis.size() > 0.
-LogicalRouterHAChassisGroup(lr_uuid,
-                            ha_chassis_group_uuid(hac_group_uuid)) :-
-    DistributedGatewayPort(lrp, lr_uuid),
+DistributedGatewayPortHAChassisGroup(lrp,
+                                     ha_chassis_group_uuid(hac_group_uuid)) :-
+    DistributedGatewayPort(.lrp = lrp),
     Some{var hac_group_uuid} = lrp.ha_chassis_group,
     nb::HA_Chassis_Group(._uuid = hac_group_uuid).
 
@@ -259,14 +241,19 @@ RouterPortIsRedirect(lrp, false) :-
     &nb::Logical_Router_Port(._uuid = lrp),
     not DistributedGatewayPort(&nb::Logical_Router_Port{._uuid = lrp}, _).
 
-relation LogicalRouterRedirectPort(lr: uuid, has_redirect_port: Option<Intern<nb::Logical_Router_Port>>)
-
-LogicalRouterRedirectPort(lr, Some{lrp}) :-
-    DistributedGatewayPort(lrp, lr).
-
-LogicalRouterRedirectPort(lr, None) :-
-    nb::Logical_Router(._uuid = lr),
-    not DistributedGatewayPort(_, lr).
+/*
+ * LogicalRouterDGWPorts maps from each logical router UUID
+ * to the logical router's set of distributed gateway (or redirect) ports. */
+relation LogicalRouterDGWPorts(
+    lr_uuid: uuid,
+    l3dgw_ports: Vec<Intern<nb::Logical_Router_Port>>)
+LogicalRouterDGWPorts(lr_uuid, l3dgw_ports) :-
+    DistributedGatewayPort(lrp, lr_uuid),
+    var l3dgw_ports = lrp.group_by(lr_uuid).to_vec().
+LogicalRouterDGWPorts(lr_uuid, vec_empty()) :-
+    lr in nb::Logical_Router(),
+    var lr_uuid = lr._uuid,
+    not DistributedGatewayPort(_, lr_uuid).
 
 typedef ExceptionalExtIps = AllowedExtIps{ips: Intern<nb::Address_Set>}
                           | ExemptedExtIps{ips: Intern<nb::Address_Set>}
@@ -450,9 +437,7 @@ LogicalRouterCopp0(lr, meters) :-
 
 /* Router relation collects all attributes of a logical router.
  *
- * `l3dgw_port` - optional redirect port (see `DistributedGatewayPort`)
- * `redirect_port_name` - derived redirect port name (or empty string if
- *      router does not have a redirect port)
+ * `l3dgw_ports` - optional redirect ports (see `DistributedGatewayPort`)
  * `is_gateway` - true iff the router is a gateway router.  Together with
  *      `l3dgw_port`, this flag affects the generation of various flows
  *      related to NAT and load balancing.
@@ -474,8 +459,7 @@ typedef Router = Router {
     external_ids:       Map<string,string>,
 
     /* Additional computed fields. */
-    l3dgw_port:         Option<Intern<nb::Logical_Router_Port>>,
-    redirect_port_name: string,
+    l3dgw_ports:        Vec<Intern<nb::Logical_Router_Port>>,
     is_gateway:         bool,
     nats:               Vec<NAT>,
     snat_ips:           Map<v46_ip, Set<NAT>>,
@@ -498,23 +482,18 @@ Router[Router{
         .options       =    lr.options,
         .external_ids  =    lr.external_ids,
 
-        .l3dgw_port = l3dgw_port,
-        .redirect_port_name =
-            match (l3dgw_port) {
-                Some{rport} -> json_string_escape(chassis_redirect_name(rport.name)),
-                _ -> ""
-            },
-        .is_gateway = lr.options.contains_key("chassis"),
-        .nats       = nats,
-        .snat_ips   = snat_ips,
-        .lbs        = lbs,
-        .mcast_cfg  = mcast_cfg,
+        .l3dgw_ports = l3dgw_ports,
+        .is_gateway  = lr.options.contains_key("chassis"),
+        .nats        = nats,
+        .snat_ips    = snat_ips,
+        .lbs         = lbs,
+        .mcast_cfg   = mcast_cfg,
         .learn_from_arp_request = learn_from_arp_request,
         .force_lb_snat = force_lb_snat,
         .copp       = copp}.intern()] :-
     lr in nb::Logical_Router(),
     lr.is_enabled(),
-    LogicalRouterRedirectPort(lr._uuid, l3dgw_port),
+    LogicalRouterDGWPorts(lr._uuid, l3dgw_ports),
     LogicalRouterNATs(lr._uuid, nats),
     LogicalRouterLBs(lr._uuid, lbs),
     LogicalRouterSnatIPs(lr._uuid, snat_ips),
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index 99a19f853..8a368ef61 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -3764,10 +3764,10 @@ icmp6 {
     <h3>Ingress Table 17: Gateway Redirect</h3>
 
     <p>
-      For distributed logical routers where one of the logical router
+      For distributed logical routers where one or more of the logical router
       ports specifies a gateway chassis, this table redirects
-      certain packets to the distributed gateway port instance on the
-      gateway chassis.  This table has the following flows:
+      certain packets to the distributed gateway port instances on the
+      gateway chassises.  This table has the following flows:
     </p>
 
     <ul>
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index ebe12cace..87c4478fa 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -655,13 +655,12 @@ struct ovn_datapath {
     bool is_gw_router;
 
     /* OVN northd only needs to know about the logical router gateway port for
-     * NAT on a distributed router.  This "distributed gateway port" is
-     * populated only when there is a gateway chassis specified for one of
-     * the ports on the logical router.  Otherwise this will be NULL. */
-    struct ovn_port *l3dgw_port;
-    /* The "derived" OVN port representing the instance of l3dgw_port on
-     * the gateway chassis. */
-    struct ovn_port *l3redirect_port;
+     * NAT on a distributed router.  The "distributed gateway ports" are
+     * populated only when there is a gateway chassis or ha chassis group
+     * specified for some of the ports on the logical router. Otherwise this
+     * will be NULL. */
+    struct ovn_port **l3dgw_ports;
+    size_t n_l3dgw_ports;
 
     /* NAT entries configured on the router. */
     struct ovn_nat *nat_entries;
@@ -802,6 +801,16 @@ init_nat_entries(struct ovn_datapath *od)
         return;
     }
 
+    if (od->n_l3dgw_ports > 1) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "NAT is configured on logical router %s, which has %"
+                     PRIuSIZE" distributed gateway ports. NAT is not supported"
+                     " yet when there is more than one distributed gateway "
+                     "port on the router.",
+                     od->nbr->name, od->n_l3dgw_ports);
+        return;
+    }
+
     od->nat_entries = xmalloc(od->nbr->n_nat * sizeof *od->nat_entries);
 
     for (size_t i = 0; i < od->nbr->n_nat; i++) {
@@ -941,6 +950,7 @@ ovn_datapath_destroy(struct hmap *datapaths, struct ovn_datapath *od)
         destroy_lb_ips(od);
         free(od->nat_entries);
         free(od->localnet_ports);
+        free(od->l3dgw_ports);
         ovn_ls_port_group_destroy(&od->nb_pgs);
         destroy_mcast_info_for_datapath(od);
 
@@ -1489,9 +1499,18 @@ struct ovn_port {
     /* Logical port multicast data. */
     struct mcast_port_info mcast_info;
 
-    /* This is ordinarily false.  It is true if and only if this ovn_port is
-     * derived from a chassis-redirect port. */
-    bool derived;
+    /* At most one of l3dgw_port and cr_port can be not NULL. */
+
+    /* This is set to a distributed gateway port if and only if this ovn_port
+     * is "derived" from it. Otherwise this is set to NULL. The derived
+     * ovn_port represents the instance of distributed gateway port on the
+     * gateway chassis.*/
+    struct ovn_port *l3dgw_port;
+
+    /* This is set to the "derived" chassis-redirect port of this port if and
+     * only if this port is a distributed gateway port. Otherwise this is set
+     * to NULL. */
+    struct ovn_port *cr_port;
 
     bool has_unknown; /* If the addresses have 'unknown' defined. */
 
@@ -1578,7 +1597,7 @@ ovn_port_create(struct hmap *ports, const char *key,
     op->key = xstrdup(key);
     op->sb = sb;
     ovn_port_set_nb(op, nbsp, nbrp);
-    op->derived = false;
+    op->l3dgw_port = op->cr_port = NULL;
     hmap_insert(ports, &op->key_node, hash_string(op->key, 0));
     return op;
 }
@@ -1682,7 +1701,7 @@ lrport_is_enabled(const struct nbrec_logical_router_port *lrport)
 static struct ovn_port *
 ovn_port_get_peer(struct hmap *ports, struct ovn_port *op)
 {
-    if (!op->nbsp || !lsp_is_router(op->nbsp) || op->derived) {
+    if (!op->nbsp || !lsp_is_router(op->nbsp) || op->l3dgw_port) {
         return NULL;
     }
 
@@ -2426,6 +2445,7 @@ join_logical_ports(struct northd_context *ctx,
                 tag_alloc_add_existing_tags(tag_alloc_table, nbsp);
             }
         } else {
+            size_t n_allocated_l3dgw_ports = 0;
             for (size_t i = 0; i < od->nbr->n_ports; i++) {
                 const struct nbrec_logical_router_port *nbrp
                     = od->nbr->ports[i];
@@ -2481,36 +2501,32 @@ join_logical_ports(struct northd_context *ctx,
                                      "on L3 gateway router", nbrp->name);
                         continue;
                     }
-                    if (od->l3dgw_port || od->l3redirect_port) {
-                        static struct vlog_rate_limit rl
-                            = VLOG_RATE_LIMIT_INIT(1, 1);
-                        VLOG_WARN_RL(&rl, "Bad configuration: multiple "
-                                     "distributed gateway ports on logical "
-                                     "router %s", od->nbr->name);
-                        continue;
-                    }
 
                     char *redirect_name =
                         ovn_chassis_redirect_name(nbrp->name);
                     struct ovn_port *crp = ovn_port_find(ports, redirect_name);
                     if (crp && crp->sb && crp->sb->datapath == od->sb) {
-                        crp->derived = true;
                         ovn_port_set_nb(crp, NULL, nbrp);
                         ovs_list_remove(&crp->list);
                         ovs_list_push_back(both, &crp->list);
                     } else {
                         crp = ovn_port_create(ports, redirect_name,
                                               NULL, nbrp, NULL);
-                        crp->derived = true;
                         ovs_list_push_back(nb_only, &crp->list);
                     }
+                    crp->l3dgw_port = op;
+                    op->cr_port = crp;
                     crp->od = od;
                     free(redirect_name);
 
-                    /* Set l3dgw_port and l3redirect_port in od, for later
-                     * use during flow creation. */
-                    od->l3dgw_port = op;
-                    od->l3redirect_port = crp;
+                    /* Add to l3dgw_ports in od, for later use during flow
+                     * creation. */
+                    if (od->n_l3dgw_ports == n_allocated_l3dgw_ports) {
+                        od->l3dgw_ports = x2nrealloc(od->l3dgw_ports,
+                                                     &n_allocated_l3dgw_ports,
+                                                     sizeof *od->l3dgw_ports);
+                    }
+                    od->l3dgw_ports[od->n_l3dgw_ports++] = op;
 
                     assign_routable_addresses(op);
                 }
@@ -2522,7 +2538,7 @@ join_logical_ports(struct northd_context *ctx,
      * to their peers. */
     struct ovn_port *op;
     HMAP_FOR_EACH (op, key_node, ports) {
-        if (op->nbsp && lsp_is_router(op->nbsp) && !op->derived) {
+        if (op->nbsp && lsp_is_router(op->nbsp) && !op->l3dgw_port) {
             struct ovn_port *peer = ovn_port_get_peer(ports, op);
             if (!peer || !peer->nbrp) {
                 continue;
@@ -2553,7 +2569,7 @@ join_logical_ports(struct northd_context *ctx,
             if (peer->od && peer->od->mcast_info.rtr.relay) {
                 op->od->mcast_info.sw.flood_relay = true;
             }
-        } else if (op->nbrp && op->nbrp->peer && !op->derived) {
+        } else if (op->nbrp && op->nbrp->peer && !op->l3dgw_port) {
             struct ovn_port *peer = ovn_port_find(ports, op->nbrp->peer);
             if (peer) {
                 if (peer->nbrp) {
@@ -2598,7 +2614,8 @@ get_nat_addresses(const struct ovn_port *op, size_t *n, bool routable_only)
     struct eth_addr mac;
     if (!op || !op->nbrp || !op->od || !op->od->nbr
         || (!op->od->nbr->n_nat && !op->od->nbr->n_load_balancer)
-        || !eth_addr_from_string(op->nbrp->mac, &mac)) {
+        || !eth_addr_from_string(op->nbrp->mac, &mac)
+        || op->od->n_l3dgw_ports > 1) {
         *n = n_nats;
         return NULL;
     }
@@ -2629,7 +2646,7 @@ get_nat_addresses(const struct ovn_port *op, size_t *n, bool routable_only)
 
         /* Determine whether this NAT rule satisfies the conditions for
          * distributed NAT processing. */
-        if (op->od->l3redirect_port && !strcmp(nat->type, "dnat_and_snat")
+        if (op->od->l3dgw_ports && !strcmp(nat->type, "dnat_and_snat")
             && nat->logical_port && nat->external_mac) {
             /* Distributed NAT rule. */
             if (eth_addr_from_string(nat->external_mac, &mac)) {
@@ -2695,9 +2712,9 @@ get_nat_addresses(const struct ovn_port *op, size_t *n, bool routable_only)
     if (central_ip_address) {
         /* Gratuitous ARP for centralized NAT rules on distributed gateway
          * ports should be restricted to the gateway chassis. */
-        if (op->od->l3redirect_port) {
+        if (op->od->l3dgw_ports) {
             ds_put_format(&c_addresses, " is_chassis_resident(%s)",
-                          op->od->l3redirect_port->json_key);
+                          op->od->l3dgw_ports[0]->cr_port->json_key);
         }
 
         addresses[n_nats++] = ds_steal_cstr(&c_addresses);
@@ -3010,7 +3027,7 @@ ovn_port_update_sbrec(struct northd_context *ctx,
         /* If the router is for l3 gateway, it resides on a chassis
          * and its port type is "l3gateway". */
         const char *chassis_name = smap_get(&op->od->nbr->options, "chassis");
-        if (op->derived) {
+        if (op->l3dgw_port) {
             sbrec_port_binding_set_type(op->sb, "chassisredirect");
         } else if (chassis_name) {
             sbrec_port_binding_set_type(op->sb, "l3gateway");
@@ -3020,7 +3037,7 @@ ovn_port_update_sbrec(struct northd_context *ctx,
 
         struct smap new;
         smap_init(&new);
-        if (op->derived) {
+        if (op->l3dgw_port) {
             const char *redirect_type = smap_get(&op->nbrp->options,
                                                  "redirect-type");
 
@@ -3200,7 +3217,7 @@ ovn_port_update_sbrec(struct northd_context *ctx,
             char **nats = NULL;
             if (nat_addresses && !strcmp(nat_addresses, "router")) {
                 if (op->peer && op->peer->od
-                    && (chassis || op->peer->od->l3redirect_port)) {
+                    && (chassis || op->peer->od->l3dgw_ports)) {
                     nats = get_nat_addresses(op->peer, &n_nats, false);
                 }
             /* Only accept manual specification of ethernet address
@@ -3236,12 +3253,26 @@ ovn_port_update_sbrec(struct northd_context *ctx,
              * sending the GARPs for the router port IPs.
              * */
             bool add_router_port_garp = false;
-            if (op->peer && op->peer->nbrp && op->peer->od->l3dgw_port &&
-                op->peer->od->l3redirect_port &&
-                (smap_get_bool(&op->peer->nbrp->options,
-                              "reside-on-redirect-chassis", false) ||
-                op->peer == op->peer->od->l3dgw_port)) {
-                add_router_port_garp = true;
+            if (op->peer && op->peer->nbrp && op->peer->od->l3dgw_ports) {
+                if (op->peer->cr_port) {
+                    add_router_port_garp = true;
+                } else if (smap_get_bool(&op->peer->nbrp->options,
+                               "reside-on-redirect-chassis", false)) {
+                    if (op->peer->od->n_l3dgw_ports == 1) {
+                        add_router_port_garp = true;
+                    } else {
+                        static struct vlog_rate_limit rl =
+                            VLOG_RATE_LIMIT_INIT(1, 1);
+                        VLOG_WARN_RL(&rl, "\"reside-on-redirect-chassis\" is "
+                                     "set on logical router port %s, which "
+                                     "is on logical router %s, which has %"
+                                     PRIuSIZE" distributed gateway ports. This"
+                                     "option can only be used when there is "
+                                     "a single distributed gateway port.",
+                                     op->peer->key, op->peer->od->nbr->name,
+                                     op->peer->od->n_l3dgw_ports);
+                    }
+                }
             } else if (chassis && op->od->n_localnet_ports) {
                 add_router_port_garp = true;
             }
@@ -3256,9 +3287,10 @@ ovn_port_update_sbrec(struct northd_context *ctx,
                                   op->peer->lrp_networks.ipv4_addrs[i].addr_s);
                 }
 
-                if (op->peer->od->l3redirect_port) {
+                if (op->peer->od->l3dgw_ports) {
                     ds_put_format(&garp_info, " is_chassis_resident(%s)",
-                                  op->peer->od->l3redirect_port->json_key);
+                                  op->peer->od->l3dgw_ports[0]
+                                  ->cr_port->json_key);
                 }
 
                 n_nats++;
@@ -3531,7 +3563,17 @@ build_ovn_lr_lbs(struct hmap *datapaths, struct hmap *lbs)
         if (!od->nbr) {
             continue;
         }
-        if (!smap_get(&od->nbr->options, "chassis") && !od->l3dgw_port) {
+        if (!smap_get(&od->nbr->options, "chassis")
+            && od->n_l3dgw_ports != 1) {
+            if (od->n_l3dgw_ports > 1 && od->nbr->n_load_balancer) {
+                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);
+            }
             continue;
         }
 
@@ -6440,13 +6482,16 @@ build_lrouter_groups__(struct hmap *ports, struct ovn_datapath *od)
 {
     ovs_assert((od && od->nbr && od->lr_group));
 
-    if (od->l3dgw_port && od->l3redirect_port) {
-        /* It's a logical router with gateway port. If it
+    if (od->l3dgw_ports) {
+        /* It's a logical router with gateway ports. If it
          * has HA_Chassis_Group associated to it in SB DB, then store the
          * ha chassis group name. */
-        if (od->l3redirect_port->sb->ha_chassis_group) {
-            sset_add(&od->lr_group->ha_chassis_groups,
-                     od->l3redirect_port->sb->ha_chassis_group->name);
+        for (size_t i = 0; i < od->n_l3dgw_ports; i++) {
+            struct ovn_port *crp = od->l3dgw_ports[i]->cr_port;
+            if (crp->sb->ha_chassis_group) {
+                sset_add(&od->lr_group->ha_chassis_groups,
+                         crp->sb->ha_chassis_group->name);
+            }
         }
     }
 
@@ -7833,16 +7878,17 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
                 ds_clear(match);
                 ds_put_format(match, "eth.dst == "ETH_ADDR_FMT,
                               ETH_ADDR_ARGS(mac));
-                if (op->peer->od->l3dgw_port
-                    && op->peer->od->l3redirect_port
+                if (op->peer->od->l3dgw_ports
                     && op->od->n_localnet_ports) {
                     bool add_chassis_resident_check = false;
-                    if (op->peer == op->peer->od->l3dgw_port) {
+                    const char *json_key;
+                    if (op->peer->cr_port) {
                         /* The peer of this port represents a distributed
                          * gateway port. The destination lookup flow for the
                          * router's distributed gateway port MAC address should
                          * only be programmed on the gateway chassis. */
                         add_chassis_resident_check = true;
+                        json_key = op->peer->cr_port->json_key;
                     } else {
                         /* Check if the option 'reside-on-redirect-chassis'
                          * is set to true on the peer port. If set to true
@@ -7853,12 +7899,15 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
                          */
                         add_chassis_resident_check = smap_get_bool(
                             &op->peer->nbrp->options,
-                            "reside-on-redirect-chassis", false);
+                            "reside-on-redirect-chassis", false) &&
+                            op->peer->od->n_l3dgw_ports == 1;
+                        json_key =
+                            op->peer->od->l3dgw_ports[0]->cr_port->json_key;
                     }
 
                     if (add_chassis_resident_check) {
                         ds_put_format(match, " && is_chassis_resident(%s)",
-                                      op->peer->od->l3redirect_port->json_key);
+                                      json_key);
                     }
                 }
 
@@ -7871,8 +7920,7 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
 
                 /* Add ethernet addresses specified in NAT rules on
                  * distributed logical routers. */
-                if (op->peer->od->l3dgw_port
-                    && op->peer == op->peer->od->l3dgw_port) {
+                if (op->peer->cr_port) {
                     for (int j = 0; j < op->peer->od->nbr->n_nat; j++) {
                         const struct nbrec_nat *nat
                                                   = op->peer->od->nbr->nat[j];
@@ -9139,14 +9187,14 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
                                      &lb->nlb->header_);
         }
 
-        if (od->l3redirect_port &&
+        if (od->l3dgw_ports &&
             (lb_vip->n_backends || !lb_vip->empty_backend_rej)) {
             new_match_p = xasprintf("%s && is_chassis_resident(%s)",
                                     new_match,
-                                    od->l3redirect_port->json_key);
+                                    od->l3dgw_ports[0]->cr_port->json_key);
             est_match_p = xasprintf("%s && is_chassis_resident(%s)",
                                     est_match,
-                                    od->l3redirect_port->json_key);
+                                    od->l3dgw_ports[0]->cr_port->json_key);
         }
 
         if (snat_type == NO_FORCE_SNAT &&
@@ -9191,15 +9239,15 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
             free(est_match_p);
         }
 
-        if (!od->l3dgw_port || !od->l3redirect_port || !lb_vip->n_backends) {
+        if (!od->l3dgw_ports || !lb_vip->n_backends) {
             goto next;
         }
 
-        char *undnat_match_p = xasprintf("%s) && outport == %s && "
-                                         "is_chassis_resident(%s)",
-                                         ds_cstr(&undnat_match),
-                                         od->l3dgw_port->json_key,
-                                         od->l3redirect_port->json_key);
+        char *undnat_match_p = xasprintf(
+            "%s) && outport == %s && is_chassis_resident(%s)",
+            ds_cstr(&undnat_match),
+            od->l3dgw_ports[0]->json_key,
+            od->l3dgw_ports[0]->cr_port->json_key);
         if (snat_type == SKIP_SNAT) {
             ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_UNDNAT, 120,
                                     undnat_match_p, skip_snat_est_action,
@@ -9695,9 +9743,9 @@ build_lrouter_port_nat_arp_nd_flow(struct ovn_port *op,
          * upstream MAC learning points to the gateway chassis.
          * Also need to avoid generation of multiple ARP responses
          * from different chassis. */
-        if (op->od->l3redirect_port) {
+        if (op->od->l3dgw_ports) {
             ds_put_format(&match, "is_chassis_resident(%s)",
-                          op->od->l3redirect_port->json_key);
+                          op->od->l3dgw_ports[0]->cr_port->json_key);
         }
     }
 
@@ -9968,7 +10016,7 @@ build_adm_ctrl_flows_for_lrouter_port(
             return;
         }
 
-        if (op->derived) {
+        if (op->l3dgw_port) {
             /* No ingress packets should be received on a chassisredirect
              * port. */
             return;
@@ -9991,12 +10039,11 @@ build_adm_ctrl_flows_for_lrouter_port(
         ds_clear(match);
         ds_put_format(match, "eth.dst == %s && inport == %s",
                       op->lrp_networks.ea_s, op->json_key);
-        if (op->od->l3dgw_port && op == op->od->l3dgw_port
-            && op->od->l3redirect_port) {
+        if (op->cr_port) {
             /* Traffic with eth.dst = l3dgw_port->lrp_networks.ea_s
              * should only be received on the gateway chassis. */
             ds_put_format(match, " && is_chassis_resident(%s)",
-                          op->od->l3redirect_port->json_key);
+                          op->cr_port->json_key);
         }
         ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_ADMISSION, 50,
                                 ds_cstr(match),  ds_cstr(actions),
@@ -10135,10 +10182,9 @@ build_neigh_learning_flows_for_lrouter_port(
                               op->lrp_networks.ipv4_addrs[i].network_s,
                               op->lrp_networks.ipv4_addrs[i].plen,
                               op->lrp_networks.ipv4_addrs[i].addr_s);
-                if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                    && op->od->l3redirect_port) {
+                if (op->cr_port) {
                     ds_put_format(match, " && is_chassis_resident(%s)",
-                                  op->od->l3redirect_port->json_key);
+                                  op->cr_port->json_key);
                 }
                 const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
                                   " = lookup_arp(inport, arp.spa, arp.sha); "
@@ -10155,10 +10201,9 @@ build_neigh_learning_flows_for_lrouter_port(
                           op->json_key,
                           op->lrp_networks.ipv4_addrs[i].network_s,
                           op->lrp_networks.ipv4_addrs[i].plen);
-            if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                && op->od->l3redirect_port) {
+            if (op->cr_port) {
                 ds_put_format(match, " && is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
+                              op->cr_port->json_key);
             }
             ds_clear(actions);
             ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
@@ -10643,7 +10688,7 @@ build_arp_resolve_flows_for_lrouter_port(
             }
         }
 
-        if (!op->derived && op->od->l3redirect_port) {
+        if (op->cr_port) {
             const char *redirect_type = smap_get(&op->nbrp->options,
                                                  "redirect-type");
             if (redirect_type && !strcasecmp(redirect_type, "bridged")) {
@@ -10656,7 +10701,7 @@ build_arp_resolve_flows_for_lrouter_port(
                 ds_clear(match);
                 ds_put_format(match, "outport == %s && "
                               "!is_chassis_resident(%s)", op->json_key,
-                              op->od->l3redirect_port->json_key);
+                              op->cr_port->json_key);
                 ds_clear(actions);
                 ds_put_format(actions, "eth.dst = %s; next;",
                               op->lrp_networks.ea_s);
@@ -10904,8 +10949,8 @@ build_arp_resolve_flows_for_lrouter_port(
                                         &op->nbsp->header_);
             }
 
-            if (smap_get(&peer->od->nbr->options, "chassis") ||
-                (peer->od->l3dgw_port && peer == peer->od->l3dgw_port)) {
+            if (smap_get(&peer->od->nbr->options, "chassis")
+                || peer->cr_port) {
                 routable_addresses_to_lflows(lflows, router_port, peer,
                                              match, actions);
             }
@@ -10934,110 +10979,111 @@ build_check_pkt_len_flows_for_lrouter(
         struct ds *match, struct ds *actions,
         struct shash *meter_groups)
 {
-    if (od->nbr) {
-
-        /* Packets are allowed by default. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_CHK_PKT_LEN, 0, "1",
-                      "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LARGER_PKTS, 0, "1",
-                      "next;");
+    if (!od->nbr) {
+        return;
+    }
 
-        if (od->l3dgw_port && od->l3redirect_port) {
-            int gw_mtu = 0;
-            if (od->l3dgw_port->nbrp) {
-                 gw_mtu = smap_get_int(&od->l3dgw_port->nbrp->options,
-                                       "gateway_mtu", 0);
-            }
-            /* Add the flows only if gateway_mtu is configured. */
-            if (gw_mtu <= 0) {
-                return;
-            }
+    /* Packets are allowed by default. */
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_CHK_PKT_LEN, 0, "1",
+                  "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_LARGER_PKTS, 0, "1",
+                  "next;");
+    for (size_t dgp = 0; dgp < od->n_l3dgw_ports; dgp++) {
+        int gw_mtu = 0;
+        if (od->l3dgw_ports[dgp]->nbrp) {
+             gw_mtu = smap_get_int(&od->l3dgw_ports[dgp]->nbrp->options,
+                                   "gateway_mtu", 0);
+        }
+        /* Add the flows only if gateway_mtu is configured. */
+        if (gw_mtu <= 0) {
+            continue;
+        }
 
-            ds_clear(match);
-            ds_put_format(match, "outport == %s", od->l3dgw_port->json_key);
+        ds_clear(match);
+        ds_put_format(match, "outport == %s",
+                      od->l3dgw_ports[dgp]->json_key);
 
-            ds_clear(actions);
-            ds_put_format(actions,
-                          REGBIT_PKT_LARGER" = check_pkt_larger(%d);"
-                          " next;", gw_mtu + VLAN_ETH_HEADER_LEN);
-            ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_CHK_PKT_LEN, 50,
-                                    ds_cstr(match), ds_cstr(actions),
-                                    &od->l3dgw_port->nbrp->header_);
+        ds_clear(actions);
+        ds_put_format(actions,
+                      REGBIT_PKT_LARGER" = check_pkt_larger(%d);"
+                      " next;", gw_mtu + VLAN_ETH_HEADER_LEN);
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_CHK_PKT_LEN, 50,
+                                ds_cstr(match), ds_cstr(actions),
+                                &od->l3dgw_ports[dgp]->nbrp->header_);
 
-            for (size_t i = 0; i < od->nbr->n_ports; i++) {
-                struct ovn_port *rp = ovn_port_find(ports,
-                                                    od->nbr->ports[i]->name);
-                if (!rp || rp == od->l3dgw_port) {
-                    continue;
-                }
+        for (size_t i = 0; i < od->nbr->n_ports; i++) {
+            struct ovn_port *rp = ovn_port_find(ports,
+                                                od->nbr->ports[i]->name);
+            if (!rp || rp->cr_port) {
+                continue;
+            }
 
-                if (rp->lrp_networks.ipv4_addrs) {
-                    ds_clear(match);
-                    ds_put_format(match, "inport == %s && outport == %s"
-                                  " && ip4 && "REGBIT_PKT_LARGER,
-                                  rp->json_key, od->l3dgw_port->json_key);
+            if (rp->lrp_networks.ipv4_addrs) {
+                ds_clear(match);
+                ds_put_format(match, "inport == %s && outport == %s"
+                              " && ip4 && "REGBIT_PKT_LARGER,
+                              rp->json_key, od->l3dgw_ports[dgp]->json_key);
 
-                    ds_clear(actions);
-                    /* Set icmp4.frag_mtu to gw_mtu */
-                    ds_put_format(actions,
-                        "icmp4_error {"
-                        REGBIT_EGRESS_LOOPBACK" = 1; "
-                        "eth.dst = %s; "
-                        "ip4.dst = ip4.src; "
-                        "ip4.src = %s; "
-                        "ip.ttl = 255; "
-                        "icmp4.type = 3; /* Destination Unreachable. */ "
-                        "icmp4.code = 4; /* Frag Needed and DF was Set. */ "
-                        "icmp4.frag_mtu = %d; "
-                        "next(pipeline=ingress, table=%d); };",
-                        rp->lrp_networks.ea_s,
-                        rp->lrp_networks.ipv4_addrs[0].addr_s,
-                        gw_mtu,
-                        ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
-                    ovn_lflow_add_with_hint__(lflows, od,
-                                              S_ROUTER_IN_LARGER_PKTS, 50,
-                                              ds_cstr(match), ds_cstr(actions),
-                                              NULL,
-                                              copp_meter_get(
-                                                    COPP_ICMP4_ERR,
-                                                    rp->od->nbr->copp,
-                                                    meter_groups),
-                                              &rp->nbrp->header_);
-                }
+                ds_clear(actions);
+                /* Set icmp4.frag_mtu to gw_mtu */
+                ds_put_format(actions,
+                    "icmp4_error {"
+                    REGBIT_EGRESS_LOOPBACK" = 1; "
+                    "eth.dst = %s; "
+                    "ip4.dst = ip4.src; "
+                    "ip4.src = %s; "
+                    "ip.ttl = 255; "
+                    "icmp4.type = 3; /* Destination Unreachable. */ "
+                    "icmp4.code = 4; /* Frag Needed and DF was Set. */ "
+                    "icmp4.frag_mtu = %d; "
+                    "next(pipeline=ingress, table=%d); };",
+                    rp->lrp_networks.ea_s,
+                    rp->lrp_networks.ipv4_addrs[0].addr_s,
+                    gw_mtu,
+                    ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
+                ovn_lflow_add_with_hint__(lflows, od,
+                                          S_ROUTER_IN_LARGER_PKTS, 50,
+                                          ds_cstr(match), ds_cstr(actions),
+                                          NULL,
+                                          copp_meter_get(
+                                                COPP_ICMP4_ERR,
+                                                rp->od->nbr->copp,
+                                                meter_groups),
+                                          &rp->nbrp->header_);
+            }
 
-                if (rp->lrp_networks.ipv6_addrs) {
-                    ds_clear(match);
-                    ds_put_format(match, "inport == %s && outport == %s"
-                                  " && ip6 && "REGBIT_PKT_LARGER,
-                                  rp->json_key, od->l3dgw_port->json_key);
+            if (rp->lrp_networks.ipv6_addrs) {
+                ds_clear(match);
+                ds_put_format(match, "inport == %s && outport == %s"
+                              " && ip6 && "REGBIT_PKT_LARGER,
+                              rp->json_key, od->l3dgw_ports[dgp]->json_key);
 
-                    ds_clear(actions);
-                    /* Set icmp6.frag_mtu to gw_mtu */
-                    ds_put_format(actions,
-                        "icmp6_error {"
-                        REGBIT_EGRESS_LOOPBACK" = 1; "
-                        "eth.dst = %s; "
-                        "ip6.dst = ip6.src; "
-                        "ip6.src = %s; "
-                        "ip.ttl = 255; "
-                        "icmp6.type = 2; /* Packet Too Big. */ "
-                        "icmp6.code = 0; "
-                        "icmp6.frag_mtu = %d; "
-                        "next(pipeline=ingress, table=%d); };",
-                        rp->lrp_networks.ea_s,
-                        rp->lrp_networks.ipv6_addrs[0].addr_s,
-                        gw_mtu,
-                        ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
-                    ovn_lflow_add_with_hint__(lflows, od,
-                                              S_ROUTER_IN_LARGER_PKTS, 50,
-                                              ds_cstr(match), ds_cstr(actions),
-                                              NULL,
-                                              copp_meter_get(
-                                                    COPP_ICMP6_ERR,
-                                                    rp->od->nbr->copp,
-                                                    meter_groups),
-                                              &rp->nbrp->header_);
-                }
+                ds_clear(actions);
+                /* Set icmp6.frag_mtu to gw_mtu */
+                ds_put_format(actions,
+                    "icmp6_error {"
+                    REGBIT_EGRESS_LOOPBACK" = 1; "
+                    "eth.dst = %s; "
+                    "ip6.dst = ip6.src; "
+                    "ip6.src = %s; "
+                    "ip.ttl = 255; "
+                    "icmp6.type = 2; /* Packet Too Big. */ "
+                    "icmp6.code = 0; "
+                    "icmp6.frag_mtu = %d; "
+                    "next(pipeline=ingress, table=%d); };",
+                    rp->lrp_networks.ea_s,
+                    rp->lrp_networks.ipv6_addrs[0].addr_s,
+                    gw_mtu,
+                    ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
+                ovn_lflow_add_with_hint__(lflows, od,
+                                          S_ROUTER_IN_LARGER_PKTS, 50,
+                                          ds_cstr(match), ds_cstr(actions),
+                                          NULL,
+                                          copp_meter_get(
+                                                COPP_ICMP6_ERR,
+                                                rp->od->nbr->copp,
+                                                meter_groups),
+                                          &rp->nbrp->header_);
             }
         }
     }
@@ -11055,32 +11101,32 @@ build_gateway_redirect_flows_for_lrouter(
         struct ovn_datapath *od, struct hmap *lflows,
         struct ds *match, struct ds *actions)
 {
-    if (od->nbr) {
-        if (od->l3dgw_port && od->l3redirect_port) {
-            const struct ovsdb_idl_row *stage_hint = NULL;
-
-            if (od->l3dgw_port->nbrp) {
-                stage_hint = &od->l3dgw_port->nbrp->header_;
-            }
+    if (!od->nbr) {
+        return;
+    }
+    for (size_t i = 0; i < od->n_l3dgw_ports; i++) {
+        const struct ovsdb_idl_row *stage_hint = NULL;
 
-            /* For traffic with outport == l3dgw_port, if the
-             * packet did not match any higher priority redirect
-             * rule, then the traffic is redirected to the central
-             * instance of the l3dgw_port. */
-            ds_clear(match);
-            ds_put_format(match, "outport == %s",
-                          od->l3dgw_port->json_key);
-            ds_clear(actions);
-            ds_put_format(actions, "outport = %s; next;",
-                          od->l3redirect_port->json_key);
-            ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_GW_REDIRECT, 50,
-                                    ds_cstr(match), ds_cstr(actions),
-                                    stage_hint);
+        if (od->l3dgw_ports[i]->nbrp) {
+            stage_hint = &od->l3dgw_ports[i]->nbrp->header_;
         }
 
-        /* Packets are allowed by default. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_GW_REDIRECT, 0, "1", "next;");
+        /* For traffic with outport == l3dgw_port, if the
+         * packet did not match any higher priority redirect
+         * rule, then the traffic is redirected to the central
+         * instance of the l3dgw_port. */
+        ds_clear(match);
+        ds_put_format(match, "outport == %s",
+                      od->l3dgw_ports[i]->json_key);
+        ds_clear(actions);
+        ds_put_format(actions, "outport = %s; next;",
+                      od->l3dgw_ports[i]->cr_port->json_key);
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_GW_REDIRECT, 50,
+                                ds_cstr(match), ds_cstr(actions),
+                                stage_hint);
     }
+    /* Packets are allowed by default. */
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_GW_REDIRECT, 0, "1", "next;");
 }
 
 /* Local router ingress table ARP_REQUEST: ARP request.
@@ -11179,7 +11225,7 @@ build_egress_delivery_flows_for_lrouter_port(
             return;
         }
 
-        if (op->derived) {
+        if (op->l3dgw_port) {
             /* No egress packets should be processed in the context of
              * a chassisredirect port.  The chassisredirect port should
              * be replaced by the l3dgw port in the local output
@@ -11269,7 +11315,7 @@ build_dhcpv6_reply_flows_for_lrouter_port(
         struct ovn_port *op, struct hmap *lflows,
         struct ds *match)
 {
-    if (op->nbrp && (!op->derived)) {
+    if (op->nbrp && (!op->l3dgw_port)) {
         for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
             ds_clear(match);
             ds_put_format(match, "ip6.dst == %s && udp.src == 547 &&"
@@ -11289,7 +11335,7 @@ build_ipv6_input_flows_for_lrouter_port(
         struct ds *match, struct ds *actions,
         struct shash *meter_groups)
 {
-    if (op->nbrp && (!op->derived)) {
+    if (op->nbrp && (!op->l3dgw_port)) {
         /* No ingress packets are accepted on a chassisredirect
          * port, so no need to program flows for that port. */
         if (op->lrp_networks.n_ipv6_addrs) {
@@ -11315,15 +11361,14 @@ 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 (op->od->l3dgw_port && op == op->od->l3dgw_port
-                && op->od->l3redirect_port) {
+            if (op->cr_port) {
                 /* 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.
                  * Also need to avoid generation of multiple ND replies
                  * from different chassis. */
                 ds_put_format(match, "is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
+                              op->cr_port->json_key);
             }
 
             build_lrouter_nd_flow(op->od, op, "nd_na_router",
@@ -11334,7 +11379,7 @@ build_ipv6_input_flows_for_lrouter_port(
         }
 
         /* UDP/TCP/SCTP port unreachable */
-        if (!op->od->is_gw_router && !op->od->l3dgw_port) {
+        if (!op->od->is_gw_router && !op->od->l3dgw_ports) {
             for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
                 ds_clear(match);
                 ds_put_format(match,
@@ -11504,7 +11549,7 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
 {
     /* No ingress packets are accepted on a chassisredirect
      * port, so no need to program flows for that port. */
-    if (op->nbrp && (!op->derived)) {
+    if (op->nbrp && (!op->l3dgw_port)) {
         if (op->lrp_networks.n_ipv4_addrs) {
             /* L3 admission control: drop packets that originate from an
              * IPv4 address owned by the router or a broadcast address
@@ -11574,16 +11619,18 @@ 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 (op->od->l3dgw_port && op->od->l3redirect_port && op->peer
+            if (op->od->l3dgw_ports && op->peer
                 && op->peer->od->n_localnet_ports) {
                 bool add_chassis_resident_check = false;
-                if (op == op->od->l3dgw_port) {
+                const char *json_key;
+                if (op->cr_port) {
                     /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
                      * should only be sent from the gateway chassis, so that
                      * upstream MAC learning points to the gateway chassis.
                      * Also need to avoid generation of multiple ARP responses
                      * from different chassis. */
                     add_chassis_resident_check = true;
+                    json_key = op->cr_port->json_key;
                 } else {
                     /* Check if the option 'reside-on-redirect-chassis'
                      * is set to true on the router port. If set to true
@@ -11595,12 +11642,14 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
                      */
                     add_chassis_resident_check = smap_get_bool(
                         &op->nbrp->options,
-                        "reside-on-redirect-chassis", false);
+                        "reside-on-redirect-chassis", false) &&
+                        op->od->n_l3dgw_ports == 1;
+                    json_key = op->od->l3dgw_ports[0]->cr_port->json_key;
                 }
 
                 if (add_chassis_resident_check) {
                     ds_put_format(match, " && is_chassis_resident(%s)",
-                                  op->od->l3redirect_port->json_key);
+                                  json_key);
                 }
             }
 
@@ -11613,9 +11662,9 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
         const char *ip_address;
         if (sset_count(&op->od->lb_ips_v4)) {
             ds_clear(match);
-            if (op == op->od->l3dgw_port) {
+            if (op->cr_port) {
                 ds_put_format(match, "is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
+                              op->cr_port->json_key);
             }
 
             struct ds load_balancer_ips_v4 = DS_EMPTY_INITIALIZER;
@@ -11633,9 +11682,9 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
 
         SSET_FOR_EACH (ip_address, &op->od->lb_ips_v6) {
             ds_clear(match);
-            if (op == op->od->l3dgw_port) {
+            if (op->cr_port) {
                 ds_put_format(match, "is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
+                              op->cr_port->json_key);
             }
 
             build_lrouter_nd_flow(op->od, op, "nd_na",
@@ -11644,7 +11693,7 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
                                   lflows, meter_groups);
         }
 
-        if (!op->od->is_gw_router && !op->od->l3dgw_port) {
+        if (!op->od->is_gw_router && !op->od->l3dgw_ports) {
             /* UDP/TCP/SCTP port unreachable. */
             for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
                 ds_clear(match);
@@ -11741,7 +11790,7 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
          * exception is on the l3dgw_port where we might need to use a
          * different ETH address.
          */
-        if (op != op->od->l3dgw_port) {
+        if (!op->cr_port) {
             return;
         }
 
@@ -11823,12 +11872,12 @@ build_lrouter_in_unsnat_flow(struct hmap *lflows, struct ovn_datapath *od,
         ds_clear(actions);
         ds_put_format(match, "ip && ip%s.dst == %s && inport == %s",
                       is_v6 ? "6" : "4", nat->external_ip,
-                      od->l3dgw_port->json_key);
-        if (!distributed && od->l3redirect_port) {
+                      od->l3dgw_ports[0]->json_key);
+        if (!distributed && od->l3dgw_ports) {
             /* Flows for NAT rules that are centralized are only
             * programmed on the gateway chassis. */
             ds_put_format(match, " && is_chassis_resident(%s)",
-                          od->l3redirect_port->json_key);
+                          od->l3dgw_ports[0]->cr_port->json_key);
         }
 
         if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
@@ -11900,12 +11949,12 @@ build_lrouter_in_dnat_flow(struct hmap *lflows, struct ovn_datapath *od,
             ds_clear(match);
             ds_put_format(match, "ip && ip%s.dst == %s && inport == %s",
                           is_v6 ? "6" : "4", nat->external_ip,
-                          od->l3dgw_port->json_key);
-            if (!distributed && od->l3redirect_port) {
+                          od->l3dgw_ports[0]->json_key);
+            if (!distributed && od->l3dgw_ports) {
                 /* Flows for NAT rules that are centralized are only
                 * programmed on the gateway chassis. */
                 ds_put_format(match, " && is_chassis_resident(%s)",
-                              od->l3redirect_port->json_key);
+                              od->l3dgw_ports[0]->cr_port->json_key);
             }
             ds_clear(actions);
             if (nat->allowed_ext_ips || nat->exempted_ext_ips) {
@@ -11944,7 +11993,7 @@ build_lrouter_out_undnat_flow(struct hmap *lflows, struct ovn_datapath *od,
     *
     * Note that this only applies for NAT on a distributed router.
     */
-    if (!od->l3dgw_port ||
+    if (!od->l3dgw_ports ||
         (strcmp(nat->type, "dnat") && strcmp(nat->type, "dnat_and_snat"))) {
         return;
     }
@@ -11952,12 +12001,12 @@ build_lrouter_out_undnat_flow(struct hmap *lflows, struct ovn_datapath *od,
     ds_clear(match);
     ds_put_format(match, "ip && ip%s.src == %s && outport == %s",
                   is_v6 ? "6" : "4", nat->logical_ip,
-                  od->l3dgw_port->json_key);
-    if (!distributed && od->l3redirect_port) {
+                  od->l3dgw_ports[0]->json_key);
+    if (!distributed && od->l3dgw_ports) {
         /* Flows for NAT rules that are centralized are only
         * programmed on the gateway chassis. */
         ds_put_format(match, " && is_chassis_resident(%s)",
-                      od->l3redirect_port->json_key);
+                      od->l3dgw_ports[0]->cr_port->json_key);
     }
     ds_clear(actions);
     if (distributed) {
@@ -12030,13 +12079,13 @@ build_lrouter_out_snat_flow(struct hmap *lflows, struct ovn_datapath *od,
         ds_clear(match);
         ds_put_format(match, "ip && ip%s.src == %s && outport == %s",
                       is_v6 ? "6" : "4", nat->logical_ip,
-                      od->l3dgw_port->json_key);
-        if (!distributed && od->l3redirect_port) {
+                      od->l3dgw_ports[0]->json_key);
+        if (!distributed && od->l3dgw_ports) {
             /* Flows for NAT rules that are centralized are only
             * programmed on the gateway chassis. */
             priority += 128;
             ds_put_format(match, " && is_chassis_resident(%s)",
-                          od->l3redirect_port->json_key);
+                          od->l3dgw_ports[0]->cr_port->json_key);
         }
         ds_clear(actions);
 
@@ -12077,11 +12126,11 @@ build_lrouter_ingress_flow(struct hmap *lflows, struct ovn_datapath *od,
                            struct ds *actions, struct eth_addr mac,
                            bool distributed, bool is_v6)
 {
-    if (od->l3dgw_port && !strcmp(nat->type, "snat")) {
+    if (od->l3dgw_ports && !strcmp(nat->type, "snat")) {
         ds_clear(match);
         ds_put_format(
             match, "inport == %s && %s == %s",
-            od->l3dgw_port->json_key,
+            od->l3dgw_ports[0]->json_key,
             is_v6 ? "ip6.src" : "ip4.src", nat->external_ip);
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_IP_INPUT,
                                 120, ds_cstr(match), "next;",
@@ -12099,14 +12148,14 @@ build_lrouter_ingress_flow(struct hmap *lflows, struct ovn_datapath *od,
         */
         ds_clear(actions);
         ds_put_format(actions, REG_INPORT_ETH_ADDR " = %s; next;",
-                      od->l3dgw_port->lrp_networks.ea_s);
+                      od->l3dgw_ports[0]->lrp_networks.ea_s);
 
         ds_clear(match);
         ds_put_format(match,
                       "eth.dst == "ETH_ADDR_FMT" && inport == %s"
                       " && is_chassis_resident(\"%s\")",
                       ETH_ADDR_ARGS(mac),
-                      od->l3dgw_port->json_key,
+                      od->l3dgw_ports[0]->json_key,
                       nat->logical_port);
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_ADMISSION, 50,
                                 ds_cstr(match), ds_cstr(actions),
@@ -12188,7 +12237,7 @@ lrouter_check_nat_entry(struct ovn_datapath *od, const struct nbrec_nat *nat,
     /* For distributed router NAT, determine whether this NAT rule
      * satisfies the conditions for distributed NAT processing. */
     *distributed = false;
-    if (od->l3dgw_port && !strcmp(nat->type, "dnat_and_snat") &&
+    if (od->l3dgw_ports && !strcmp(nat->type, "dnat_and_snat") &&
         nat->logical_port && nat->external_mac) {
         if (eth_addr_from_string(nat->external_mac, mac)) {
             *distributed = true;
@@ -12233,7 +12282,7 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
      * not committed, it would produce ongoing datapath flows with the ct.new
      * flag set. Some NICs are unable to offload these flows.
      */
-    if ((od->is_gw_router || od->l3dgw_port) &&
+    if ((od->is_gw_router || od->l3dgw_ports) &&
         (od->nbr->n_nat || od->nbr->n_load_balancer)) {
         ovn_lflow_add(lflows, od, S_ROUTER_OUT_UNDNAT, 50,
                       "ip", "flags.loopback = 1; ct_dnat;");
@@ -12249,7 +12298,7 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
     /* NAT rules are only valid on Gateway routers and routers with
      * l3dgw_port (router has a port with gateway chassis
      * specified). */
-    if (!od->is_gw_router && !od->l3dgw_port) {
+    if (!od->is_gw_router && !od->l3dgw_ports) {
         return;
     }
 
@@ -12290,14 +12339,14 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
                 ds_clear(match);
                 ds_put_format(
                     match, "outport == %s && %s == %s",
-                    od->l3dgw_port->json_key,
+                    od->l3dgw_ports[0]->json_key,
                     is_v6 ? REG_NEXT_HOP_IPV6 : REG_NEXT_HOP_IPV4,
                     nat->external_ip);
                 ds_clear(actions);
                 ds_put_format(
                     actions, "eth.dst = %s; next;",
                     distributed ? nat->external_mac :
-                    od->l3dgw_port->lrp_networks.ea_s);
+                    od->l3dgw_ports[0]->lrp_networks.ea_s);
                 ovn_lflow_add_with_hint(lflows, od,
                                         S_ROUTER_IN_ARP_RESOLVE,
                                         100, ds_cstr(match),
@@ -12333,7 +12382,7 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
             ds_put_format(match,
                           "ip%s.src == %s && outport == %s",
                           is_v6 ? "6" : "4", nat->logical_ip,
-                          od->l3dgw_port->json_key);
+                          od->l3dgw_ports[0]->json_key);
             /* Add a rule to drop traffic from a distributed NAT if
              * the virtual port has not claimed yet becaused otherwise
              * the traffic will be centralized misconfiguring the TOR switch.
@@ -12360,16 +12409,16 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
          * gateway port have ip.dst matching a NAT external IP, then
          * loop a clone of the packet back to the beginning of the
          * ingress pipeline with inport = outport. */
-        if (od->l3dgw_port) {
+        if (od->l3dgw_ports) {
             /* Distributed router. */
             ds_clear(match);
             ds_put_format(match, "ip%s.dst == %s && outport == %s",
                           is_v6 ? "6" : "4",
                           nat->external_ip,
-                          od->l3dgw_port->json_key);
+                          od->l3dgw_ports[0]->json_key);
             if (!distributed) {
                 ds_put_format(match, " && is_chassis_resident(%s)",
-                              od->l3redirect_port->json_key);
+                              od->l3dgw_ports[0]->cr_port->json_key);
             } else {
                 ds_put_format(match, " && is_chassis_resident(\"%s\")",
                               nat->logical_port);
diff --git a/northd/ovn_northd.dl b/northd/ovn_northd.dl
index 7bfaae992..c6ebecb81 100644
--- a/northd/ovn_northd.dl
+++ b/northd/ovn_northd.dl
@@ -183,7 +183,7 @@ OutProxy_Port_Binding(._uuid              = lsp._uuid,
     },
     Some{var router_port} = lsp.options.get("router-port"),
     var opt_chassis = peer.and_then(|p| p.router.options.get("chassis")),
-    var l3dgw_port = peer.and_then(|p| p.router.l3dgw_port),
+    var l3dgw_port = peer.and_then(|p| p.router.l3dgw_ports.nth(0)),
     (var __type, var options) = {
         var options = ["peer" -> router_port];
         match (opt_chassis) {
@@ -239,7 +239,7 @@ OutProxy_Port_Binding(._uuid              = lsp._uuid,
         Some{rport} -> match (
             (rport.lrp.options.get_bool_def("reside-on-redirect-chassis", false)
              and l3dgw_port.is_some()) or
-            Some{rport.lrp} == l3dgw_port or
+            rport.is_redirect or
             (rport.router.options.contains_key("chassis") and
              not sw.localnet_ports.is_empty())) {
             false -> set_empty(),
@@ -333,7 +333,7 @@ function get_router_load_balancer_ips(router: Intern<Router>,
 function get_nat_addresses(rport: Intern<RouterPort>, routable_only: bool): Set<string> =
 {
     var addresses = set_empty();
-    var has_redirect = rport.router.l3dgw_port.is_some();
+    var has_redirect = not rport.router.l3dgw_ports.is_empty();
     match (eth_addr_from_string(rport.lrp.mac)) {
         None -> addresses,
         Some{mac} -> {
@@ -400,7 +400,10 @@ function get_nat_addresses(rport: Intern<RouterPort>, routable_only: bool): Set<
                 /* Gratuitous ARP for centralized NAT rules on distributed gateway
                  * ports should be restricted to the gateway chassis. */
                 if (has_redirect) {
-                    c_addresses = c_addresses ++ " is_chassis_resident(${rport.router.redirect_port_name})"
+                    c_addresses = c_addresses ++ match (rport.router.l3dgw_ports.nth(0)) {
+                        None -> "",
+                        Some {var gw_port} -> " is_chassis_resident(${json_string_escape(chassis_redirect_name(gw_port.name))})"
+                    }
                 } else ();
 
                 addresses.insert(c_addresses)
@@ -415,8 +418,10 @@ function get_garp_nat_addresses(rport: Intern<RouterPort>): string = {
     for (ipv4_addr in rport.networks.ipv4_addrs) {
         garp_info.push("${ipv4_addr.addr}")
     };
-    if (rport.router.redirect_port_name != "") {
-        garp_info.push("is_chassis_resident(${rport.router.redirect_port_name})")
+    match (rport.router.l3dgw_ports.nth(0)) {
+        None -> (),
+        Some {var gw_port} -> garp_info.push(
+            "is_chassis_resident(${json_string_escape(chassis_redirect_name(gw_port.name))})")
     };
     garp_info.join(" ")
 }
@@ -453,7 +458,7 @@ OutProxy_Port_Binding(// lrp._uuid is already in use; generate a new UUID by
                       .nat_addresses      = set_empty(),
                       .external_ids       = lrp.external_ids) :-
     DistributedGatewayPort(lrp, lr_uuid),
-    LogicalRouterHAChassisGroup(lr_uuid, hacg_uuid),
+    DistributedGatewayPortHAChassisGroup(lrp, hacg_uuid),
     var redirect_type = match (lrp.options.get("redirect-type")) {
         Some{var value} -> ["redirect-type" -> value],
         _ -> map_empty()
@@ -509,7 +514,8 @@ sb::Out_Port_Binding(._uuid              = pbinding._uuid,
  * chassis.  RefChassisSet has a row for every logical router. */
 relation RefChassis(lr_uuid: uuid, chassis_uuid: uuid)
 RefChassis(lr_uuid, chassis_uuid) :-
-    LogicalRouterHAChassisGroup(lr_uuid, _),
+    DistributedGatewayPortHAChassisGroup(lrp, _),
+    DistributedGatewayPort(lrp, lr_uuid),
     ConnectedLogicalRouter[(lr_uuid, set_uuid)],
     ConnectedLogicalRouter[(lr2_uuid, set_uuid)],
     FirstHopLogicalRouter(lr2_uuid, ls_uuid),
@@ -536,7 +542,8 @@ RefChassisSet(lr_uuid, set_empty()) :-
 relation HAChassisGroupRefChassisSet(hacg_uuid: uuid,
                                      chassis_uuids: Set<uuid>)
 HAChassisGroupRefChassisSet(hacg_uuid, chassis_uuids) :-
-    LogicalRouterHAChassisGroup(lr_uuid, hacg_uuid),
+    DistributedGatewayPortHAChassisGroup(lrp, hacg_uuid),
+    DistributedGatewayPort(lrp, lr_uuid),
     RefChassisSet(lr_uuid, chassis_uuids),
     var chassis_uuids = chassis_uuids.group_by(hacg_uuid).union().
 
@@ -4453,7 +4460,7 @@ for (&SwitchPort(.lsp = lsp,
                  .peer = Some{&RouterPort{.lrp = lrp,
                                           .is_redirect = is_redirect,
                                           .router = &Router{._uuid = lr_uuid,
-                                                            .redirect_port_name = redirect_port_name}}})
+                                                            .l3dgw_ports = l3dgw_ports}}})
      if (lsp.addresses.contains("router") and lsp.__type != "external"))
 {
     Some{var mac} = scan_eth_addr(lrp.mac) in {
@@ -4473,6 +4480,14 @@ for (&SwitchPort(.lsp = lsp,
               */
               lrp.options.get_bool_def("reside-on-redirect-chassis", false)) in
         var __match = if (add_chassis_resident_check) {
+            var redirect_port_name = if (is_redirect) {
+                json_string_escape(chassis_redirect_name(lrp.name))
+            } else {
+                match (l3dgw_ports.nth(0)) {
+                    Some {var gw_port} -> json_string_escape(chassis_redirect_name(gw_port.name)),
+                    None -> ""
+                }
+            };
             /* The destination lookup flow for the router's
              * distributed gateway port MAC address should only be
              * programmed on the "redirect-chassis". */
@@ -4872,13 +4887,8 @@ var rLNIR = rEGBIT_LOOKUP_NEIGHBOR_IP_RESULT() in
 
 /* Check if we need to learn mac-binding from ARP requests. */
 for (RouterPortNetworksIPv4Addr(rp@&RouterPort{.router = router}, addr)) {
-    var is_l3dgw_port = match (router.l3dgw_port) {
-        Some{l3dgw_lrp} -> l3dgw_lrp._uuid == rp.lrp._uuid,
-        None -> false
-    } in
-    var has_redirect_port = router.redirect_port_name != "" in
-    var chassis_residence = match (is_l3dgw_port and has_redirect_port) {
-        true -> " && is_chassis_resident(${router.redirect_port_name})",
+    var chassis_residence = match (rp.is_redirect) {
+        true -> " && is_chassis_resident(${json_string_escape(chassis_redirect_name(rp.lrp.name))})",
         false -> ""
     } in
     var rLNR = rEGBIT_LOOKUP_NEIGHBOR_RESULT() in
@@ -5038,7 +5048,7 @@ relation AddChassisResidentCheck_(lrp: uuid, add_check: bool)
 AddChassisResidentCheck_(lrp._uuid, res) :-
     &SwitchPort(.peer = Some{&RouterPort{.lrp = lrp, .router = router, .is_redirect = is_redirect}},
                 .sw = sw),
-    router.l3dgw_port.is_some(),
+    not router.l3dgw_ports.is_empty(),
     not sw.localnet_ports.is_empty(),
     var res = if (is_redirect) {
         /* Traffic with eth.src = l3dgw_port->lrp_networks.ea
@@ -5143,7 +5153,8 @@ LogicalRouterArpNdFlow(router, nat, None, rEG_INPORT_ETH_ADDR(), None, false, 90
  * different ETH address.
  */
 LogicalRouterPortNatArpNdFlow(router, nat, l3dgw_port) :-
-    router in &Router(._uuid = lr_uuid, .l3dgw_port = Some{l3dgw_port}),
+    router in &Router(._uuid = lr_uuid, .l3dgw_ports = l3dgw_ports),
+    Some {var l3dgw_port} = l3dgw_ports.nth(0),
     LogicalRouterNAT(lr_uuid, nat),
     /* Skip SNAT entries for now, we handle unique SNAT IPs separately
      * below.
@@ -5151,7 +5162,8 @@ LogicalRouterPortNatArpNdFlow(router, nat, l3dgw_port) :-
     nat.nat.__type != "snat".
 /* Now handle SNAT entries too, one per unique SNAT IP. */
 LogicalRouterPortNatArpNdFlow(router, nat, l3dgw_port) :-
-    router in &Router(.l3dgw_port = Some{l3dgw_port}, .snat_ips = snat_ips),
+    router in &Router(.l3dgw_ports = l3dgw_ports, .snat_ips = snat_ips),
+    Some {var l3dgw_port} = l3dgw_ports.nth(0),
     var snat_ip = FlatMap(snat_ips),
     (var ip, var nats) = snat_ip,
     Some{var nat} = nats.nth(0).
@@ -5181,9 +5193,9 @@ LogicalRouterArpNdFlow(router, nat, Some{lrp}, mac, None, true, 91) :-
              * upstream MAC learning points to the gateway chassis.
              * Also need to avoid generation of multiple ARP responses
              * from different chassis. */
-            match (router.redirect_port_name) {
-                "" -> "",
-                s -> "is_chassis_resident(${s})"
+            match (router.l3dgw_ports.nth(0)) {
+                None -> "",
+                Some {var gw_port} -> "is_chassis_resident(${json_string_escape(chassis_redirect_name(gw_port.name))})"
             }
         )
     }.
@@ -5328,7 +5340,15 @@ for (RouterPortNetworksIPv4Addr(.port = &RouterPort{.lrp = lrp,
         var __match =
             "arp.spa == ${addr.match_network()}" ++
             if (add_chassis_resident_check) {
-                " && is_chassis_resident(${router.redirect_port_name})"
+                var redirect_port_name = if (is_redirect) {
+                    json_string_escape(chassis_redirect_name(lrp.name))
+                } else {
+                    match (router.l3dgw_ports.nth(0)) {
+                        None -> "",
+                        Some {var gw_port} -> json_string_escape(chassis_redirect_name(gw_port.name))
+                    }
+                };
+                " && is_chassis_resident(${redirect_port_name})"
             } else "" in
         LogicalRouterArpFlow(.lr = router,
                              .lrp = Some{lrp},
@@ -5347,7 +5367,7 @@ for (&RouterPort(.lrp = lrp,
                  .networks = networks,
                  .is_redirect = is_redirect))
 var residence_check = match (is_redirect) {
-    true -> Some{"is_chassis_resident(${router.redirect_port_name})"},
+    true -> Some{"is_chassis_resident(${json_string_escape(chassis_redirect_name(lrp.name))})"},
     false -> None
 } in {
     (var all_ips_v4, _) = get_router_load_balancer_ips(router, false) in {
@@ -5417,7 +5437,7 @@ Flow(.logical_datapath = lr_uuid,
 for (RouterPortNetworksIPv4Addr(
         .port = &RouterPort{
             .router = &Router{._uuid = lr_uuid,
-                              .l3dgw_port = None,
+                              .l3dgw_ports = vec_empty(),
                               .is_gateway = false,
                               .copp = copp},
             .lrp = lrp},
@@ -5553,7 +5573,7 @@ for (RouterPortNetworksIPv6Addr(.port = &RouterPort{.lrp = lrp,
 /* UDP/TCP/SCTP port unreachable */
 for (RouterPortNetworksIPv6Addr(
         .port = &RouterPort{.router = &Router{._uuid = lr_uuid,
-                                              .l3dgw_port = None,
+                                              .l3dgw_ports = vec_empty(),
                                               .is_gateway = false,
                                               .copp = copp},
                             .lrp = lrp,
@@ -5681,11 +5701,11 @@ for (r in &Router(._uuid = lr_uuid)) {
 }
 
 for (r in &Router(._uuid = lr_uuid,
-                  .l3dgw_port = l3dgw_port,
+                  .l3dgw_ports = l3dgw_ports,
                   .is_gateway = is_gateway,
                   .nat = nat,
                   .load_balancer = load_balancer)
-     if (l3dgw_port.is_some() or is_gateway) and (not is_empty(nat) or not is_empty(load_balancer))) {
+     if (l3dgw_ports.len() > 0 or is_gateway) and (not is_empty(nat) or not is_empty(load_balancer))) {
     /* If the router has load balancer or DNAT rules, re-circulate every packet
      * through the DNAT zone so that packets that need to be unDNATed in the
      * reverse direction get unDNATed.
@@ -5768,7 +5788,7 @@ function lrouter_nat_add_ext_ip_match(
                 },
                 false -> {
                     /* S_ROUTER_OUT_SNAT uses priority (mask + 1 + 128 + 1) */
-                    var is_gw_router = router.l3dgw_port == None;
+                    var is_gw_router = router.l3dgw_ports.is_empty();
                     var mask_1bits = mask.cidr_bits().unwrap_or(8'd0) as integer;
                     mask_1bits + 2 + { if (not is_gw_router) 128 else 0 }
                 }
@@ -5873,10 +5893,9 @@ VirtualLogicalPort(Some{logical_port}) :-
  * l3dgw_port (router has a port with "redirect-chassis"
  * specified). */
 for (r in &Router(._uuid = lr_uuid,
-                  .l3dgw_port = l3dgw_port,
-                  .redirect_port_name = redirect_port_name,
+                  .l3dgw_ports = l3dgw_ports,
                   .is_gateway = is_gateway)
-     if l3dgw_port.is_some() or is_gateway)
+     if not l3dgw_ports.is_empty() or is_gateway)
 {
     for (LogicalRouterNAT(.lr = lr_uuid, .nat = nat)) {
         var ipX = nat.external_ip.ipX() in
@@ -5894,7 +5913,7 @@ for (r in &Router(._uuid = lr_uuid,
         } in
         /* For distributed router NAT, determine whether this NAT rule
          * satisfies the conditions for distributed NAT processing. */
-        var mac = match ((l3dgw_port.is_some() and nat.nat.__type == "dnat_and_snat",
+        var mac = match ((not l3dgw_ports.is_empty() and nat.nat.__type == "dnat_and_snat",
                           nat.nat.logical_port, nat.external_mac)) {
             (true, Some{_}, Some{mac}) -> Some{mac},
             _ -> None
@@ -5912,7 +5931,7 @@ for (r in &Router(._uuid = lr_uuid,
              * not know about the possibility of eventual additional SNAT in
              * egress pipeline. */
             if (nat.nat.__type == "snat" or nat.nat.__type == "dnat_and_snat") {
-                if (l3dgw_port == None) {
+                if (l3dgw_ports.is_empty()) {
                     /* Gateway router. */
                     var actions = if (stateless) {
                         "${ipX}.dst=${nat.nat.logical_ip}; next;"
@@ -5926,7 +5945,7 @@ for (r in &Router(._uuid = lr_uuid,
                          .actions          = actions,
                          .external_ids     = stage_hint(nat.nat._uuid))
                 };
-                Some{var gwport} = l3dgw_port in {
+                Some {var gwport} = l3dgw_ports.nth(0) in {
                     /* Distributed router. */
 
                     /* Traffic received on l3dgw_port is subject to NAT. */
@@ -5936,7 +5955,7 @@ for (r in &Router(._uuid = lr_uuid,
                         if (mac == None) {
                             /* Flows for NAT rules that are centralized are only
                              * programmed on the "redirect-chassis". */
-                            " && is_chassis_resident(${redirect_port_name})"
+                            " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gwport.name))})"
                         } else { "" } in
                     var actions = if (stateless) {
                         "${ipX}.dst=${nat.nat.logical_ip}; next;"
@@ -5962,7 +5981,7 @@ for (r in &Router(._uuid = lr_uuid,
                                    ""
                                } in
             if (nat.nat.__type == "dnat" or nat.nat.__type == "dnat_and_snat") {
-                None = l3dgw_port in
+                l3dgw_ports.is_empty() in
                 var __match = "ip && ${ipX}.dst == ${nat.nat.external_ip}" in
                 (var ext_ip_match, var ext_flow) = lrouter_nat_add_ext_ip_match(
                     r, nat, __match, ipX, true, mask) in
@@ -5994,14 +6013,14 @@ for (r in &Router(._uuid = lr_uuid,
                          .external_ids     = stage_hint(nat.nat._uuid))
                 };
 
-                Some{var gwport} = l3dgw_port in
+                Some {var gwport} = l3dgw_ports.nth(0) in
                 var __match =
                     "ip && ${ipX}.dst == ${nat.nat.external_ip}"
                     " && inport == ${json_string_escape(gwport.name)}" ++
                     if (mac == None) {
                         /* Flows for NAT rules that are centralized are only
                          * programmed on the "redirect-chassis". */
-                        " && is_chassis_resident(${redirect_port_name})"
+                        " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gwport.name))})"
                     } else { "" } in
                 (var ext_ip_match, var ext_flow) = lrouter_nat_add_ext_ip_match(
                     r, nat, __match, ipX, true, mask) in
@@ -6025,7 +6044,7 @@ for (r in &Router(._uuid = lr_uuid,
             };
 
             /* ARP resolve for NAT IPs. */
-            Some{var gwport} = l3dgw_port in {
+            Some {var gwport} = l3dgw_ports.nth(0) in {
             var gwport_name = json_string_escape(gwport.name) in {
                 if (nat.nat.__type == "snat") {
                     var __match = "inport == ${gwport_name} && "
@@ -6062,14 +6081,14 @@ for (r in &Router(._uuid = lr_uuid,
              * Note that this only applies for NAT on a distributed router.
              */
             if ((nat.nat.__type == "dnat" or nat.nat.__type == "dnat_and_snat")) {
-                Some{var gwport} = l3dgw_port in
+                Some {var gwport} = l3dgw_ports.nth(0) in
                 var __match =
                     "ip && ${ipX}.src == ${nat.nat.logical_ip}"
                     " && outport == ${json_string_escape(gwport.name)}" ++
                     if (mac == None) {
                         /* Flows for NAT rules that are centralized are only
                          * programmed on the "redirect-chassis". */
-                        " && is_chassis_resident(${redirect_port_name})"
+                        " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gwport.name))})"
                     } else { "" } in
                 var actions =
                     match (mac) {
@@ -6099,7 +6118,7 @@ for (r in &Router(._uuid = lr_uuid,
                                    ""
                                } in
             if (nat.nat.__type == "snat" or nat.nat.__type == "dnat_and_snat") {
-                None = l3dgw_port in
+                l3dgw_ports.is_empty() in
                 var __match = "ip && ${ipX}.src == ${nat.nat.logical_ip}" in
                 (var ext_ip_match, var ext_flow) = lrouter_nat_add_ext_ip_match(
                     r, nat, __match, ipX, false, mask) in
@@ -6124,14 +6143,14 @@ for (r in &Router(._uuid = lr_uuid,
                          .external_ids     = stage_hint(nat.nat._uuid))
                 };
 
-                Some{var gwport} = l3dgw_port in
+                Some {var gwport} = l3dgw_ports.nth(0) in
                 var __match =
                     "ip && ${ipX}.src == ${nat.nat.logical_ip}"
                     " && outport == ${json_string_escape(gwport.name)}" ++
                     if (mac == None) {
                         /* Flows for NAT rules that are centralized are only
                          * programmed on the "redirect-chassis". */
-                        " && is_chassis_resident(${redirect_port_name})"
+                        " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gwport.name))})"
                     } else { "" } in
                 (var ext_ip_match, var ext_flow) = lrouter_nat_add_ext_ip_match(
                     r, nat, __match, ipX, false, mask) in
@@ -6169,7 +6188,7 @@ for (r in &Router(._uuid = lr_uuid,
              * on the l3dgw_port instance where nat->logical_port is
              * resident. */
             Some{var mac_addr} = mac in
-            Some{var gwport} = l3dgw_port in
+            Some{var gwport} = l3dgw_ports.nth(0) in
             Some{var logical_port} = nat.nat.logical_port in
             var __match =
                 "eth.dst == ${mac_addr} && inport == ${json_string_escape(gwport.name)}"
@@ -6195,7 +6214,7 @@ for (r in &Router(._uuid = lr_uuid,
              * stage is sent out with proper IP/MAC src addresses
              */
             Some{var mac_addr} = mac in
-            Some{var gwport} = l3dgw_port in
+            Some{var gwport} = l3dgw_ports.nth(0) in
             Some{var logical_port} = nat.nat.logical_port in
             Some{var external_mac} = nat.nat.external_mac in
             var __match =
@@ -6214,7 +6233,7 @@ for (r in &Router(._uuid = lr_uuid,
                  .external_ids     = stage_hint(nat.nat._uuid));
 
             for (VirtualLogicalPort(nat.nat.logical_port)) {
-                Some{var gwport} = l3dgw_port in
+                Some{var gwport} = l3dgw_ports.nth(0) in
                 Flow(.logical_datapath = lr_uuid,
                      .stage            = s_ROUTER_IN_GW_REDIRECT(),
                     .priority         = 80,
@@ -6229,14 +6248,14 @@ for (r in &Router(._uuid = lr_uuid,
              * gateway port have ip.dst matching a NAT external IP, then
              * loop a clone of the packet back to the beginning of the
              * ingress pipeline with inport = outport. */
-            Some{var gwport} = l3dgw_port in
+            Some{var gwport} = l3dgw_ports.nth(0) in
             /* Distributed router. */
             Some{var port} = match (mac) {
                 Some{_} -> match (nat.nat.logical_port) {
                                Some{name} -> Some{json_string_escape(name)},
                                None -> None: Option<string>
                            },
-                None -> Some{redirect_port_name}
+                None -> Some{json_string_escape(chassis_redirect_name(gwport.name))}
             } in
             var __match = "${ipX}.dst == ${nat.nat.external_ip} && outport == ${json_string_escape(gwport.name)} && is_chassis_resident(${port})" in
             var regs = {
@@ -6264,7 +6283,7 @@ for (r in &Router(._uuid = lr_uuid,
     };
 
     /* Handle force SNAT options set in the gateway router. */
-    if (l3dgw_port == None) {
+    if (l3dgw_ports.is_empty()) {
         var dnat_force_snat_ips = get_force_snat_ip(r.options, "dnat") in
         if (not dnat_force_snat_ips.is_empty())
         LogicalRouterForceSnatFlows(.logical_router = lr_uuid,
@@ -6292,14 +6311,13 @@ function nats_contain_vip(nats: Vec<NAT>, vip: v46_ip): bool {
  * Gateway routers or router with gateway port. */
 for (RouterLBVIP(
         .router = r@&Router{._uuid = lr_uuid,
-                            .l3dgw_port = l3dgw_port,
-                            .redirect_port_name = redirect_port_name,
+                            .l3dgw_ports = l3dgw_ports,
                             .is_gateway = is_gateway,
                             .nats = nats},
         .lb = lb,
         .vip = vip,
         .backends = backends)
-     if l3dgw_port.is_some() or is_gateway)
+     if not l3dgw_ports.is_empty() or is_gateway)
 {
     if (backends == "" and not lb.options.get_bool_def("reject", false)) {
         for (LoadBalancerEmptyEvents(lb)) {
@@ -6368,8 +6386,8 @@ for (RouterLBVIP(
                 (110, "")
             } in
         var __match = match1 ++ match2 ++
-            match ((l3dgw_port, backends != "" or lb.options.get_bool_def("reject", false))) {
-                (Some{gwport}, true) -> " && is_chassis_resident(${redirect_port_name})",
+            match ((l3dgw_ports.nth(0), backends != "" or lb.options.get_bool_def("reject", false))) {
+                (Some{gw_port}, true) -> " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gw_port.name))})",
                 _ -> ""
             } in
         var snat_for_lb = snat_for_lb(r.options, lb) in
@@ -6381,8 +6399,8 @@ for (RouterLBVIP(
                 } else {
                     ""
                 } ++
-                match ((l3dgw_port, backends != "" or lb.options.get_bool_def("reject", false))) {
-                    (Some{gwport}, true) -> " && is_chassis_resident(${redirect_port_name})",
+                match ((l3dgw_ports.nth(0), backends != "" or lb.options.get_bool_def("reject", false))) {
+                    (Some {var gw_port}, true) -> " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gw_port.name))})",
                     _ -> ""
                 } in
             var actions =
@@ -6421,7 +6439,7 @@ for (RouterLBVIP(
                      .external_ids     = stage_hint(lb._uuid))
             };
 
-            Some{var gwport} = l3dgw_port in
+            Some{var gwport} = l3dgw_ports.nth(0) in
             /* Add logical flows to UNDNAT the load balanced reverse traffic in
              * the router egress pipleine stage - S_ROUTER_OUT_UNDNAT if the logical
              * router has a gateway router port associated.
@@ -6446,7 +6464,7 @@ for (RouterLBVIP(
             var undnat_match =
                 "${ip_address.ipX()} && (" ++ conds.join(" || ") ++
                 ") && outport == ${json_string_escape(gwport.name)} && "
-                "is_chassis_resident(${redirect_port_name})" in
+                "is_chassis_resident(${json_string_escape(chassis_redirect_name(gwport.name))})" in
             var action =
                 match (snat_for_lb) {
                     SkipSNAT -> "flags.skip_snat_for_lb = 1; ct_dnat;",
@@ -6477,14 +6495,14 @@ MeteredFlow(.logical_datapath = r._uuid,
             .controller_meter = meter,
             .external_ids     = stage_hint(lb._uuid)) :-
     r in &Router(),
-    r.l3dgw_port.is_some() or r.is_gateway,
+    r.l3dgw_ports.len() > 0 or r.is_gateway,
     LBVIPWithStatus[lbvip@&LBVIPWithStatus{.lb = lb}],
     r.load_balancer.contains(lb._uuid),
     var __match
         = "ct.new && " ++
           get_match_for_lb_key(lbvip.vip_addr, lbvip.vip_port, lb.protocol, true, true) ++
-          match (r.l3dgw_port) {
-              Some{gwport} -> " && is_chassis_resident(${r.redirect_port_name})",
+          match (r.l3dgw_ports.nth(0)) {
+              Some{gw_port} -> " && is_chassis_resident(${json_string_escape(chassis_redirect_name(gw_port.name))})",
               _ -> ""
           },
     var priority = if (lbvip.vip_port != 0) 120 else 110,
@@ -7290,11 +7308,11 @@ Flow(.logical_datapath = router._uuid,
      .stage            = s_ROUTER_IN_ARP_RESOLVE(),
      .priority         = 50,
      .__match          = "outport == ${rp.json_name} && "
-                         "!is_chassis_resident(${router.redirect_port_name})",
+                         "!is_chassis_resident(${json_string_escape(chassis_redirect_name(l3dgw_port.name))})",
      .actions          = "eth.dst = ${rp.networks.ea}; next;",
      .external_ids     = stage_hint(lrp._uuid)) :-
     rp in &RouterPort(.lrp = lrp, .router = router),
-    router.redirect_port_name != "",
+    Some{var l3dgw_port} = router.l3dgw_ports.nth(0),
     Some{"bridged"} = lrp.options.get("redirect-type").
 
 
@@ -7537,9 +7555,8 @@ Flow(.logical_datapath = lr_uuid,
                          "next;",
      .external_ids     = stage_hint(l3dgw_port._uuid)) :-
     r in &Router(._uuid = lr_uuid),
-    Some{var l3dgw_port} = r.l3dgw_port,
+    Some{var l3dgw_port} = r.l3dgw_ports.nth(0),
     var l3dgw_port_json_name = json_string_escape(l3dgw_port.name),
-    r.redirect_port_name != "",
     var gw_mtu = l3dgw_port.options.get_int_def("gateway_mtu", 0),
     gw_mtu > 0,
     var mtu = gw_mtu + vLAN_ETH_HEADER_LEN().
@@ -7564,9 +7581,8 @@ MeteredFlow(.logical_datapath = lr_uuid,
             .controller_meter = r.copp.get(cOPP_ICMP4_ERR()),
             .external_ids     = stage_hint(rp.lrp._uuid)) :-
     r in &Router(._uuid = lr_uuid),
-    Some{var l3dgw_port} = r.l3dgw_port,
+    Some{var l3dgw_port} = r.l3dgw_ports.nth(0),
     var l3dgw_port_json_name = json_string_escape(l3dgw_port.name),
-    r.redirect_port_name != "",
     var gw_mtu = l3dgw_port.options.get_int_def("gateway_mtu", 0),
     gw_mtu > 0,
     rp in &RouterPort(.router = r),
@@ -7593,9 +7609,8 @@ MeteredFlow(.logical_datapath = lr_uuid,
             .controller_meter = r.copp.get(cOPP_ICMP6_ERR()),
             .external_ids     = stage_hint(rp.lrp._uuid)) :-
     r in &Router(._uuid = lr_uuid),
-    Some{var l3dgw_port} = r.l3dgw_port,
+    Some{var l3dgw_port} = r.l3dgw_ports.nth(0),
     var l3dgw_port_json_name = json_string_escape(l3dgw_port.name),
-    r.redirect_port_name != "",
     var gw_mtu = l3dgw_port.options.get_int_def("gateway_mtu", 0),
     gw_mtu > 0,
     rp in &RouterPort(.router = r),
@@ -7609,21 +7624,20 @@ MeteredFlow(.logical_datapath = lr_uuid,
  * of the traffic to the l3redirect_port which represents
  * the central instance of the l3dgw_port.
  */
-for (&Router(._uuid = lr_uuid,
-             .l3dgw_port = l3dgw_port,
-             .redirect_port_name = redirect_port_name))
+for (&Router(._uuid = lr_uuid))
 {
     /* For traffic with outport == l3dgw_port, if the
      * packet did not match any higher priority redirect
      * rule, then the traffic is redirected to the central
      * instance of the l3dgw_port. */
-    Some{var gwport} = l3dgw_port in
-    Flow(.logical_datapath = lr_uuid,
-         .stage            = s_ROUTER_IN_GW_REDIRECT(),
-         .priority         = 50,
-         .__match          = "outport == ${json_string_escape(gwport.name)}",
-         .actions          = "outport = ${redirect_port_name}; next;",
-         .external_ids     = stage_hint(gwport._uuid));
+    for (DistributedGatewayPort(lrp, lr_uuid)) {
+        Flow(.logical_datapath = lr_uuid,
+             .stage            = s_ROUTER_IN_GW_REDIRECT(),
+             .priority         = 50,
+             .__match          = "outport == ${json_string_escape(lrp.name)}",
+             .actions          = "outport = ${json_string_escape(chassis_redirect_name(lrp.name))}; next;",
+             .external_ids     = stage_hint(lrp._uuid))
+    };
 
     /* Packets are allowed by default. */
     Flow(.logical_datapath = lr_uuid,
diff --git a/ovn-architecture.7.xml b/ovn-architecture.7.xml
index 0eef9b739..3598b5073 100644
--- a/ovn-architecture.7.xml
+++ b/ovn-architecture.7.xml
@@ -731,6 +731,13 @@
     highest-priority gateway that is online.
   </p>
 
+  <p>
+    A logical router can have multiple distributed gateway ports, each
+    connecting different external networks. However, some features, such as NAT
+    and load balancers, are not supported yet for logical routers with more
+    than one distributed gateway port configured.
+  </p>
+
   <h4>Physical VLAN MTU Issues</h4>
 
   <p>
@@ -1968,8 +1975,9 @@
 
   <p>
     If the logical router doesn't have a distributed gateway port connecting
-    to the localnet logical switch which provides external connectivity,
-    then this option is ignored by <code>OVN</code>.
+    to the localnet logical switch which provides external connectivity, or
+    if it has more than one distributed gateway ports, then this option is
+    ignored by <code>OVN</code>.
   </p>
 
   <p>
@@ -2086,6 +2094,13 @@
     a tunnel.
   </p>
 
+  <p>
+    If the logical router doesn't have a distributed gateway port connecting
+    to the localnet logical switch which provides external connectivity, or
+    if it has more than one distributed gateway ports, then this option is
+    ignored by <code>OVN</code>.
+  </p>
+
   <p>
     Following happens for bridged redirection:
   </p>
diff --git a/ovn-nb.xml b/ovn-nb.xml
index c1176e81f..ec51b5608 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -2032,13 +2032,14 @@
 
     <column name="nat">
       One or more NAT rules for the router.  NAT rules only work on
-      Gateway routers, and on distributed routers with logical gateway ports.
+      Gateway routers, and on distributed routers with one and only one
+      distributed gateway port.
     </column>
 
     <column name="load_balancer">
       Load balance a virtual ip address to a set of logical port ip
       addresses.  Load balancer rules only work on the Gateway routers or
-      routers with distributed gateway ports.
+      routers with one and only one distributed gateway port.
     </column>
 
     <group title="Naming">
@@ -2453,8 +2454,7 @@
         If either of these are set, this logical router port represents a
         distributed gateway port that connects this router to a
         logical switch with a <code>localnet</code> port or a
-        connection to another OVN deployment.  There may be at most
-        one such logical router port on each logical router.
+        connection to another OVN deployment.
       </p>
 
       <p>
@@ -2476,8 +2476,16 @@
       </p>
 
       <p>
-        When more than one gateway chassis is specified, OVN only uses
-        one at a time. OVN can rely on OVS BFD implementation to monitor
+        There can be more than one distributed gateway ports configured
+        on each logical router, each connecting to different L2 segments.
+        However, features such as NAT and load-balancer are not supported
+        on logical routers with more than one distributed gateway ports.
+      </p>
+
+      <p>
+        For each distributed gateway port, it may have more than one gateway
+        chassises. When more than one gateway chassis is specified, OVN only
+        uses one at a time.  OVN can rely on OVS BFD implementation to monitor
         gateway connectivity, preferring the highest-priority gateway
         that is online.  Priorities are specified in the <code>priority</code>
         column of <ref table="Gateway_Chassis"/> or <ref table="HA_Chassis"/>.
@@ -2563,8 +2571,8 @@
           </p>
 
           <p>
-            OVN honors this option only if the logical router has a distributed
-            gateway port and if the LRP's peer switch has a
+            OVN honors this option only if the logical router has one and only
+            one distributed gateway port and if the LRP's peer switch has a
             <code>localnet</code> port.
           </p>
         </column>
@@ -2588,7 +2596,8 @@
           <p>
             Setting this option to <code>overlay</code> or leaving it unset has
             no effect.  This option may usefully be set only on a distributed
-            gateway port.  It is otherwise ignored.
+            gateway port when there is one and only one distributed gateway
+            port on the logical router.  It is otherwise ignored.
           </p>
         </column>
       </group>
diff --git a/ovs b/ovs
index daf627f45..e6ad4d8d9 160000
--- a/ovs
+++ b/ovs
@@ -1 +1 @@
-Subproject commit daf627f459ffbc7171d42a2c01f80754bfd54edc
+Subproject commit e6ad4d8d9c9273f226ec9a993b64fccfb50bdf4c
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 94405349e..dfb5b70f9 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -4815,3 +4815,95 @@ AT_CHECK([ovn-sbctl --columns=tags list logical_flow | grep lsp0 -c], [0], [dnl
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn-northd -- lr multiple gw ports])
+AT_KEYWORDS([multiple-l3dgw-ports])
+ovn_start
+
+# Logical network:
+# 1 Logical Router, 3 bridged Logical Switches,
+# 1 gateway chassis attached to each corresponding LRP.
+#
+#                | S1 (gw1)
+#                |
+#      ls  ----  DR -- S3 (gw3)
+# (20.0.0.0/24)  |
+#                | S2 (gw2)
+#
+# Validate basic LR logical flows.
+
+check ovn-sbctl chassis-add gw1 geneve 127.0.0.1
+check ovn-sbctl chassis-add gw2 geneve 128.0.0.1
+check ovn-sbctl chassis-add gw3 geneve 129.0.0.1
+
+check ovn-nbctl lr-add DR
+check ovn-nbctl lrp-add DR DR-S1 02:ac:10:01:00:01 172.16.1.1/24
+check ovn-nbctl lrp-add DR DR-S2 03:ac:10:01:00:01 10.0.0.1/24
+check ovn-nbctl lrp-add DR DR-S3 04:ac:10:01:00:01 192.168.0.1/24
+check ovn-nbctl lrp-add DR DR-ls 05:ac:10:01:00:01 20.0.0.1/24
+
+check ovn-nbctl ls-add S1
+check ovn-nbctl lsp-add S1 S1-DR
+check ovn-nbctl lsp-set-type S1-DR router
+check ovn-nbctl lsp-set-addresses S1-DR router
+check ovn-nbctl --wait=sb lsp-set-options S1-DR router-port=DR-S1
+
+check ovn-nbctl ls-add S2
+check ovn-nbctl lsp-add S2 S2-DR
+check ovn-nbctl lsp-set-type S2-DR router
+check ovn-nbctl lsp-set-addresses S2-DR router
+check ovn-nbctl --wait=sb lsp-set-options S2-DR router-port=DR-S2
+
+check ovn-nbctl ls-add S3
+check ovn-nbctl lsp-add S3 S3-DR
+check ovn-nbctl lsp-set-type S3-DR router
+check ovn-nbctl lsp-set-addresses S3-DR router
+check ovn-nbctl --wait=sb lsp-set-options S3-DR router-port=DR-S3
+
+check ovn-nbctl ls-add  ls
+check ovn-nbctl lsp-add ls ls-DR
+check ovn-nbctl lsp-set-type ls-DR router
+check ovn-nbctl lsp-set-addresses ls-DR router
+check ovn-nbctl --wait=sb lsp-set-options ls-DR router-port=DR-ls
+
+check ovn-nbctl lrp-set-gateway-chassis DR-S1 gw1
+check ovn-nbctl lrp-set-gateway-chassis DR-S2 gw2
+check ovn-nbctl lrp-set-gateway-chassis DR-S3 gw3
+
+check ovn-nbctl --wait=sb sync
+
+ovn-sbctl dump-flows DR > lrflows
+AT_CAPTURE_FILE([lrflows])
+
+# Check the flows in lr_in_admission stage
+AT_CHECK([grep lr_in_admission lrflows | grep cr-DR | wc -l], [0], [3
+])
+AT_CHECK([grep lr_in_admission lrflows | grep cr-DR-S1 | wc -l], [0], [1
+])
+AT_CHECK([grep lr_in_admission lrflows | grep cr-DR-S2 | wc -l], [0], [1
+])
+AT_CHECK([grep lr_in_admission lrflows | grep cr-DR-S3 | wc -l], [0], [1
+])
+
+# Check the flows in lr_in_lookup_neighbor stage
+AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep cr-DR | wc -l], [0], [3
+])
+AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep cr-DR-S1 | wc -l], [0], [1
+])
+AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep cr-DR-S2 | wc -l], [0], [1
+])
+AT_CHECK([grep lr_in_lookup_neighbor lrflows | grep cr-DR-S3 | wc -l], [0], [1
+])
+
+# Check the flows in lr_in_gw_redirect stage
+AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR | wc -l], [0], [3
+])
+AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR-S1 | wc -l], [0], [1
+])
+AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR-S2 | wc -l], [0], [1
+])
+AT_CHECK([grep lr_in_gw_redirect lrflows | grep cr-DR-S3 | wc -l], [0], [1
+])
+AT_CLEANUP
+])
diff --git a/tests/ovn.at b/tests/ovn.at
index 13d860f10..13f2a4990 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -27351,3 +27351,310 @@ AT_CHECK([ovs-ofctl dump-flows br-int table=44 | grep 10.0.0.144], [0], [ignore]
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- lr multiple gw ports])
+AT_KEYWORDS([multiple-l3dgw-ports])
+ovn_start
+
+# Logical network:
+# 1 LR, 3 Logical Switches,
+# 1 gateway chassis attached to each corresponding LRP.
+#
+#                | S1 (gw1)
+#                |
+#      ls  ----  DR -- S3 (gw3)
+# (20.0.0.0/24)  |
+#                | S2 (gw2)
+#
+# S1 - VLAN 1000
+# S2 - VLAN 2000
+# S3 - VLAN 3000
+#
+# 5 chassis(s), HV1----HV5
+#
+# HV1 - VIF11
+# HV2 - Gateway chassis gw1
+# HV3 - Gateway chassis gw2
+# HV4 - Gateway chassis gw3
+# HV5 - North endpoint
+
+ovn-nbctl lr-add DR
+ovn-nbctl lrp-add DR DR-S1 02:ac:10:01:00:01 172.16.1.1/24
+ovn-nbctl lrp-add DR DR-S2 08:ac:10:01:00:01 10.0.0.1/24
+ovn-nbctl lrp-add DR DR-S3 04:ac:10:01:00:01 192.168.0.1/24
+ovn-nbctl lrp-add DR DR-ls 06:ac:10:01:00:01 20.0.0.1/24
+
+ovn-nbctl ls-add S1
+ovn-nbctl lsp-add S1 S1-DR
+ovn-nbctl lsp-set-type S1-DR router
+ovn-nbctl lsp-set-addresses S1-DR router
+ovn-nbctl --wait=sb lsp-set-options S1-DR router-port=DR-S1
+ovn-nbctl lsp-add S1 ln1 "" 1000
+ovn-nbctl lsp-set-addresses ln1 unknown
+ovn-nbctl lsp-set-type ln1 localnet
+ovn-nbctl lsp-set-options ln1 network_name=phys
+
+ovn-nbctl ls-add S2
+ovn-nbctl lsp-add S2 S2-DR
+ovn-nbctl lsp-set-type S2-DR router
+ovn-nbctl lsp-set-addresses S2-DR router
+ovn-nbctl --wait=sb lsp-set-options S2-DR router-port=DR-S2
+ovn-nbctl lsp-add S2 ln2 "" 2000
+ovn-nbctl lsp-set-addresses ln2 unknown
+ovn-nbctl lsp-set-type ln2 localnet
+ovn-nbctl lsp-set-options ln2 network_name=phys
+
+ovn-nbctl ls-add S3
+ovn-nbctl lsp-add S3 S3-DR
+ovn-nbctl lsp-set-type S3-DR router
+ovn-nbctl lsp-set-addresses S3-DR router
+ovn-nbctl --wait=sb lsp-set-options S3-DR router-port=DR-S3
+ovn-nbctl lsp-add S3 ln3 "" 3000
+ovn-nbctl lsp-set-addresses ln3 unknown
+ovn-nbctl lsp-set-type ln3 localnet
+ovn-nbctl lsp-set-options ln3 network_name=phys
+
+ovn-nbctl ls-add ls
+ovn-nbctl lsp-add ls ls-DR
+ovn-nbctl lsp-set-type ls-DR router
+ovn-nbctl lsp-set-addresses ls-DR router
+ovn-nbctl --wait=sb lsp-set-options ls-DR router-port=DR-ls
+
+# Add the lsp lp11 to ls. This will map to VIF11.
+ovn-nbctl lsp-add ls lp11
+ovn-nbctl lsp-set-addresses lp11 "f0:00:00:00:00:10 20.0.0.10"
+ovn-nbctl lsp-set-port-security lp11 f0:00:00:00:00:10
+
+# Add the Northbound endpoint, lp-north1
+ovn-nbctl ls-add ls-north1
+ovn-nbctl lsp-add ls-north1 ln4 "" 1000
+ovn-nbctl lsp-set-addresses ln4 unknown
+ovn-nbctl lsp-set-type ln4 localnet
+ovn-nbctl lsp-set-options ln4 network_name=phys
+
+ovn-nbctl lsp-add ls-north1 lp-north1
+ovn-nbctl lsp-set-addresses lp-north1 "f0:f0:00:00:00:11 172.16.1.10"
+ovn-nbctl lsp-set-port-security lp-north1 f0:f0:00:00:00:11
+
+# Add the Northbound endpoint, lp-north2
+ovn-nbctl ls-add ls-north2
+ovn-nbctl lsp-add ls-north2 ln5 "" 2000
+ovn-nbctl lsp-set-addresses ln5 unknown
+ovn-nbctl lsp-set-type ln5 localnet
+ovn-nbctl lsp-set-options ln5 network_name=phys
+
+ovn-nbctl lsp-add ls-north2 lp-north2
+ovn-nbctl lsp-set-addresses lp-north2 "f0:f0:00:00:00:22 10.0.0.10"
+ovn-nbctl lsp-set-port-security lp-north2 f0:f0:00:00:00:22
+
+# Add the Northbound endpoint, lp-north3
+ovn-nbctl ls-add ls-north3
+ovn-nbctl lsp-add ls-north3 ln6 "" 3000
+ovn-nbctl lsp-set-addresses ln6 unknown
+ovn-nbctl lsp-set-type ln6 localnet
+ovn-nbctl lsp-set-options ln6 network_name=phys
+
+ovn-nbctl lsp-add ls-north3 lp-north3
+ovn-nbctl lsp-set-addresses lp-north3 "f0:f0:00:00:00:33 192.168.0.10"
+ovn-nbctl lsp-set-port-security lp-north3 f0:f0:00:00:00:33
+
+# Add 5 chassis
+net_add n1
+for i in 1 2 3 4 5; do
+    sim_add hv$i
+    as hv$i
+    ovs-vsctl add-br br-phys
+    ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+    ovn_attach n1 br-phys 192.168.0.$i 24 $encap
+done
+
+# Add a vif on HV1
+as hv1 ovs-vsctl add-port br-int vif11 -- \
+    set Interface vif11 external-ids:iface-id=lp11 \
+                              options:tx_pcap=hv1/vif11-tx.pcap \
+                              options:rxq_pcap=hv1/vif11-rx.pcap \
+                              ofport-request=11
+OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up lp11` = xup])
+
+as hv5 ovs-vsctl add-port br-int vif-north1 -- \
+        set Interface vif-north1 external-ids:iface-id=lp-north1 \
+                              options:tx_pcap=hv5/vif-north1-tx.pcap \
+                              options:rxq_pcap=hv5/vif-north1-rx.pcap \
+                              ofport-request=44
+
+as hv5 ovs-vsctl add-port br-int vif-north2 -- \
+        set Interface vif-north2 external-ids:iface-id=lp-north2 \
+                              options:tx_pcap=hv5/vif-north2-tx.pcap \
+                              options:rxq_pcap=hv5/vif-north2-rx.pcap \
+                              ofport-request=45
+
+as hv5 ovs-vsctl add-port br-int vif-north3 -- \
+        set Interface vif-north3 external-ids:iface-id=lp-north3 \
+                              options:tx_pcap=hv5/vif-north3-tx.pcap \
+                              options:rxq_pcap=hv5/vif-north3-rx.pcap \
+                              ofport-request=46
+
+ovn-nbctl lrp-set-gateway-chassis DR-S1 hv2
+ovn-nbctl lrp-set-gateway-chassis DR-S2 hv3
+ovn-nbctl lrp-set-gateway-chassis DR-S3 hv4
+
+ovn-nbctl --wait=sb sync
+OVN_POPULATE_ARP
+
+vif_to_ls () {
+    case ${1} in dnl (
+        vif?[[11]]) echo ls ;; dnl (
+        vif-north1) echo ls-north1 ;; dnl (
+        vif-north2) echo ls-north2 ;; dnl (
+        vif-north3) echo ls-north3 ;; dnl (
+        *) AT_FAIL_IF([:]) ;;
+    esac
+}
+
+vif_to_hv () {
+    case ${1} in dnl (
+        vif[[1]]?) echo hv1 ;; dnl (
+        vif-north1) echo hv5 ;; dnl (
+        vif-north2) echo hv5 ;; dnl (
+        vif-north3) echo hv5 ;; dnl (
+        *) AT_FAIL_IF([:]) ;;
+    esac
+}
+
+vif_to_lrp () {
+    case ${1} in dnl (
+        vif?[[11]]) echo DR-ls ;; dnl (
+        *) AT_FAIL_IF([:]) ;;
+    esac
+
+}
+
+ip_to_hex() {
+       printf "%02x%02x%02x%02x" "${@}"
+}
+
+# test_arp INPORT SHA SPA TPA
+#
+# Causes a packet to be received on INPORT.  The packet is an ARP
+# request with SHA, SPA, and TPA as specified.
+test_arp() {
+    local inport=$1 sha=$2 spa=$3 tpa=$4
+    local request=ffffffffffff${sha}08060001080006040001${sha}${spa}ffffffffffff${tpa}
+    hv=`vif_to_hv $inport`
+    as $hv ovs-appctl netdev-dummy/receive $inport $request
+}
+
+
+test_ip() {
+        # This packet has bad checksums but logical L3 routing doesn't check.
+        local inport=${1} src_mac=${2} dst_mac=${3} src_ip=${4} dst_ip=${5} outport=${6}
+        local packet=${dst_mac}${src_mac}08004500001c0000000040110000${src_ip}${dst_ip}0035111100080000
+        shift; shift; shift; shift; shift
+        hv=`vif_to_hv $inport`
+        as $hv ovs-appctl netdev-dummy/receive $inport $packet
+        in_ls=`vif_to_ls $inport`
+        for outport; do
+            out_ls=`vif_to_ls $outport`
+            if test $in_ls = $out_ls; then
+                # Ports on the same logical switch receive exactly the same packet.
+                echo $packet
+            else
+                # Routing decrements TTL and updates source and dest MAC
+                # (and checksum).
+                # For North-South, packet will come via gateway chassis, i.e hv3
+                if test $inport = vif-north1; then
+                    echo f0000000001006ac1001000108004500001c000000003f110100${src_ip}${dst_ip}0035111100080000 >> $outport.expected
+                fi
+                if test $outport = vif-north1; then
+                    echo f0f00000001102ac1001000108004500001c000000003f110100${src_ip}${dst_ip}0035111100080000 >> $outport.expected
+                fi
+                if test $outport = vif-north2; then
+                    echo f0f00000002208ac1001000108004500001c000000003f110100${src_ip}${dst_ip}0035111100080000 >> $outport.expected
+                fi
+                if test $outport = vif-north3; then
+                    echo f0f00000003304ac1001000108004500001c000000003f110100${src_ip}${dst_ip}0035111100080000 >> $outport.expected
+                fi
+            fi >> $outport.expected
+        done
+}
+
+echo "------ OVN dump ------"
+ovn-nbctl show
+ovn-sbctl show
+ovn-sbctl list port_binding
+ovn-sbctl list mac_binding
+ovn-sbctl list datapath_binding
+
+ovn-sbctl dump-flows DR
+ovn-sbctl dump-flows S1
+ovn-sbctl dump-flows ls
+
+echo "------ hv1 dump ------"
+as hv1 ovs-vsctl show
+as hv1 ovs-vsctl list Open_Vswitch
+as hv1 ovs-ofctl dump-flows br-int
+
+echo "------ hv2 dump ------"
+as hv2 ovs-vsctl show
+as hv2 ovs-vsctl list Open_Vswitch
+as hv2 ovs-ofctl dump-flows br-int
+
+echo "------ hv3 dump ------"
+as hv3 ovs-vsctl show
+as hv3 ovs-vsctl list Open_Vswitch
+as hv3 ovs-ofctl dump-flows br-int
+
+echo "------ hv4 dump ------"
+as hv4 ovs-vsctl show
+as hv4 ovs-vsctl list Open_Vswitch
+as hv5 ovs-ofctl dump-flows br-int
+
+# N-S with lp-north1
+echo "Send Dummy ARP"
+sip=`ip_to_hex 172 16 1 10`
+tip=`ip_to_hex 172 16 1 50`
+test_arp vif-north1 f0f000000011 $sip $tip
+
+echo "Send traffic North to South"
+sip=`ip_to_hex 172 16 1 10`
+dip=`ip_to_hex 20 0 0 10`
+test_ip vif-north1 f0f000000011 02ac10010001 $sip $dip vif11
+# Confirm that North to south traffic works fine.
+OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif11-tx.pcap], [vif11.expected])
+
+echo "Send traffic South to North1"
+sip=`ip_to_hex 20 0 0 10`
+dip=`ip_to_hex 172 16 1 10`
+test_ip vif11 f00000000010 06ac10010001 $sip $dip vif-north1
+# Confirm that South to North traffic works fine.
+OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv5/vif-north1-tx.pcap], [vif-north1.expected])
+
+# N-S with lp-north2
+echo "Send Dummy ARP"
+sip=`ip_to_hex 10 0 0 10`
+tip=`ip_to_hex 10 0 0 50`
+test_arp vif-north2 f0f000000022 $sip $tip
+
+echo "Send traffic South to North2"
+sip=`ip_to_hex 20 0 0 10`
+dip=`ip_to_hex 10 0 0 10`
+test_ip vif11 f00000000010 06ac10010001 $sip $dip vif-north2
+# Confirm that South to North traffic works fine.
+OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv5/vif-north2-tx.pcap], [vif-north2.expected])
+
+# N-S with lp-north3
+echo "Send Dummy ARP"
+sip=`ip_to_hex 192 168 0 10`
+tip=`ip_to_hex 192 168 0 50`
+test_arp vif-north3 f0f000000033 $sip $tip
+
+echo "Send traffic South to North3"
+sip=`ip_to_hex 20 0 0 10`
+dip=`ip_to_hex 192 168 0 10`
+test_ip vif11 f00000000010 06ac10010001 $sip $dip vif-north3
+# Confirm that South to North traffic works fine.
+OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv5/vif-north3-tx.pcap], [vif-north3.expected])
+
+AT_CLEANUP
+])
