diff --git a/ovn/controller/binding.c b/ovn/controller/binding.c
index d7b175e..801b70a 100644
--- a/ovn/controller/binding.c
+++ b/ovn/controller/binding.c
@@ -68,7 +68,8 @@ static void
 get_local_iface_ids(const struct ovsrec_bridge *br_int,
                     struct shash *lport_to_iface,
                     struct sset *all_lports,
-                    struct sset *egress_ifaces)
+                    struct sset *egress_ifaces,
+                    struct shash *chassisredirect_ports)
 {
     int i;
 
@@ -81,10 +82,20 @@ get_local_iface_ids(const struct ovsrec_bridge *br_int,
             continue;
         }
 
+        const char *chassisredirect_port = smap_get(&port_rec->external_ids,
+                                                   "ovn-chassisredirect-port");
+
         for (j = 0; j < port_rec->n_interfaces; j++) {
             const struct ovsrec_interface *iface_rec;
 
             iface_rec = port_rec->interfaces[j];
+
+            if (chassisredirect_port) {
+                shash_add(chassisredirect_ports, chassisredirect_port,
+                          port_rec);
+                break;
+            }
+
             iface_id = smap_get(&iface_rec->external_ids, "iface-id");
 
             if (iface_id) {
@@ -277,13 +288,83 @@ setup_qos(const char *egress_iface, struct hmap *queue_map)
 }
 
 static void
+create_port_from_sb(struct controller_ctx *ctx,
+                    const struct ovsrec_bridge *br_int,
+                    const struct sbrec_port_binding *binding_rec,
+                    struct hmap *local_datapaths,
+                    const char *ovs_port_type,
+                    const char *iface_type)
+{
+    /* This function only handles ports with a single interface */
+    /* If local datapath does not exist return */
+    struct local_datapath *ld = get_local_datapath(local_datapaths,
+                                    binding_rec->datapath->tunnel_key);
+    if (!ld || !ctx->ovs_idl_txn || !binding_rec->logical_port) {
+        return;
+    }
+    struct ovsrec_interface *iface;
+    iface = ovsrec_interface_insert(ctx->ovs_idl_txn);
+    ovsrec_interface_set_name(iface, binding_rec->logical_port);
+    ovsrec_interface_set_type(iface, iface_type);
+
+    struct smap ids;
+    smap_clone(&ids, &binding_rec->options);
+    ovsrec_interface_set_external_ids(iface, &ids);
+    smap_destroy(&ids);
+
+    struct ovsrec_port *port;
+    port = ovsrec_port_insert(ctx->ovs_idl_txn);
+    ovsrec_port_set_name(port, binding_rec->logical_port);
+    ovsrec_port_set_interfaces(port, &iface, 1);
+    const struct smap port_ids = SMAP_CONST1(&port_ids,
+                                             ovs_port_type,
+                                             binding_rec->logical_port);
+    ovsrec_port_set_external_ids(port, &port_ids);
+
+    struct ovsrec_port **ports;
+    ports = xmalloc(sizeof *ports * (br_int->n_ports + 1));
+    memcpy(ports, br_int->ports, sizeof *ports * br_int->n_ports);
+    ports[br_int->n_ports] = port;
+    ovsrec_bridge_verify_ports(br_int);
+    ovsrec_bridge_set_ports(br_int, ports, br_int->n_ports + 1);
+
+    free(ports);
+}
+
+static void
+remove_ovs_port(const struct ovsrec_bridge *bridge,
+                const struct ovsrec_port *port)
+{
+    size_t i;
+    for (i = 0; i < bridge->n_ports; i++) {
+        if (bridge->ports[i] != port) {
+            continue;
+        }
+        struct ovsrec_port **new_ports;
+        new_ports = xmemdup(bridge->ports,
+                sizeof *new_ports * (bridge->n_ports - 1));
+        if (i != bridge->n_ports - 1) {
+            /* Removed port was not last */
+            new_ports[i] = bridge->ports[bridge->n_ports - 1];
+        }
+        ovsrec_bridge_verify_ports(bridge);
+        ovsrec_bridge_set_ports(bridge, new_ports, bridge->n_ports - 1);
+        free(new_ports);
+        ovsrec_port_delete(port);
+        return;
+    }
+}
+
+static void
 consider_local_datapath(struct controller_ctx *ctx,
+                        const struct ovsrec_bridge *br_int,
                         const struct sbrec_chassis *chassis_rec,
                         const struct sbrec_port_binding *binding_rec,
                         struct hmap *qos_map,
                         struct hmap *local_datapaths,
                         struct shash *lport_to_iface,
-                        struct sset *all_lports)
+                        struct sset *all_lports,
+                        struct shash *chassisredirect_ports)
 {
     const struct ovsrec_interface *iface_rec
         = shash_find_data(lport_to_iface, binding_rec->logical_port);
@@ -340,6 +421,47 @@ consider_local_datapath(struct controller_ctx *ctx,
                       binding_rec->logical_port);
             sbrec_port_binding_set_chassis(binding_rec, chassis_rec);
         }
+    } else if (chassis_rec && !strcmp(binding_rec->type,
+                                      "chassisredirect")) {
+        const char *chassis_options = smap_get(&binding_rec->options,
+                                               "redirect-chassis");
+        const struct ovsrec_port *port_rec
+            = shash_find_and_delete(chassisredirect_ports,
+                                    binding_rec->logical_port);
+        if (chassis_options && !strcmp(chassis_options, chassis_rec->name)) {
+            add_local_datapath(local_datapaths, binding_rec);
+            if (!port_rec) {
+                create_port_from_sb(ctx, br_int, binding_rec,
+                                    local_datapaths,
+                                    "ovn-chassisredirect-port",
+                                    "internal");
+            }
+            if (ctx->ovnsb_idl_txn
+                && binding_rec->chassis != chassis_rec) {
+                if (binding_rec->chassis) {
+                    VLOG_INFO("Changing chassis for redirect port %s "
+                              "from %s to %s.",
+                              binding_rec->logical_port,
+                              binding_rec->chassis->name,
+                              chassis_rec->name);
+                } else {
+                    VLOG_INFO("Claiming redirect port %s for this "
+                              "chassis.",
+                              binding_rec->logical_port);
+                }
+                sbrec_port_binding_set_chassis(binding_rec, chassis_rec);
+            }
+        } else {
+            if (ctx->ovnsb_idl_txn
+                && binding_rec->chassis == chassis_rec) {
+                VLOG_INFO("Releasing redirect port %s from this chassis.",
+                          binding_rec->logical_port);
+                sbrec_port_binding_set_chassis(binding_rec, NULL);
+            }
+            if (ctx->ovs_idl && port_rec) {
+                remove_ovs_port(br_int, port_rec);
+            }
+        }
     } else if (!strcmp(binding_rec->type, "l3gateway")) {
         const char *chassis = smap_get(&binding_rec->options,
                                        "l3gateway-chassis");
@@ -374,6 +496,8 @@ binding_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
     struct shash lport_to_iface = SHASH_INITIALIZER(&lport_to_iface);
     struct sset egress_ifaces = SSET_INITIALIZER(&egress_ifaces);
     struct hmap qos_map;
+    struct shash chassisredirect_ports =
+                                  SHASH_INITIALIZER(&chassisredirect_ports);
 
     chassis_rec = get_chassis(ctx->ovnsb_idl, chassis_id);
     if (!chassis_rec) {
@@ -383,17 +507,17 @@ binding_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
     hmap_init(&qos_map);
     if (br_int) {
         get_local_iface_ids(br_int, &lport_to_iface, all_lports,
-                            &egress_ifaces);
+                            &egress_ifaces, &chassisredirect_ports);
     }
 
     /* Run through each binding record to see if it is resident on this
      * chassis and update the binding accordingly.  This includes both
      * directly connected logical ports and children of those ports. */
     SBREC_PORT_BINDING_FOR_EACH(binding_rec, ctx->ovnsb_idl) {
-        consider_local_datapath(ctx, chassis_rec, binding_rec,
+        consider_local_datapath(ctx, br_int, chassis_rec, binding_rec,
                                 sset_is_empty(&egress_ifaces) ? NULL :
                                 &qos_map, local_datapaths, &lport_to_iface,
-                                all_lports);
+                                all_lports, &chassisredirect_ports);
 
     }
 
@@ -408,6 +532,15 @@ binding_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
     shash_destroy(&lport_to_iface);
     sset_destroy(&egress_ifaces);
     hmap_destroy(&qos_map);
+
+    /* At this point, 'chassisredirect_ports' only contains OVS ports with
+     * no corresponding SB port binding.  Delete these ports from OVS. */
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &chassisredirect_ports) {
+        remove_ovs_port(br_int, node->data);
+    }
+
+    shash_destroy(&chassisredirect_ports);
 }
 
 /* Returns true if the database is all cleaned up, false if more work is
diff --git a/ovn/controller/ovn-controller.8.xml b/ovn/controller/ovn-controller.8.xml
index 5f51cb1..fd6b9e5 100644
--- a/ovn/controller/ovn-controller.8.xml
+++ b/ovn/controller/ovn-controller.8.xml
@@ -288,6 +288,21 @@
           logical patch port that it implements.
         </p>
       </dd>
+
+      <dt>
+        <code>external-ids:ovn-chassisredirect-port</code> in the
+        <code>Port</code> table
+      </dt>
+
+      <dd>
+        <p>
+          The presence of this key identifies a port as one created by
+          <code>ovn-controller</code> to implement a
+          <code>chassisredirect</code> logical port.  Its value is the name
+          of the logical port with type=chassisredirect that the port
+          implements.
+        </p>
+      </dd>
     </dl>
 
     <h1>Runtime Management Commands</h1>
diff --git a/ovn/controller/ovn-controller.c b/ovn/controller/ovn-controller.c
index 944c403..0fb4187 100644
--- a/ovn/controller/ovn-controller.c
+++ b/ovn/controller/ovn-controller.c
@@ -544,7 +544,8 @@ main(int argc, char *argv[])
                           &flow_table);
 
                 physical_run(&ctx, mff_ovn_geneve,
-                             br_int, chassis_id, &ct_zones, &flow_table,
+                             br_int, chassis_id,
+                             &lports, &ct_zones, &flow_table,
                              &local_datapaths, &patched_datapaths);
 
                 ofctrl_put(&flow_table, &pending_ct_zones,
diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c
index 1a91531..a2a48db 100644
--- a/ovn/controller/physical.c
+++ b/ovn/controller/physical.c
@@ -19,6 +19,7 @@
 #include "flow.h"
 #include "lflow.h"
 #include "lib/poll-loop.h"
+#include "lport.h"
 #include "ofctrl.h"
 #include "openvswitch/hmap.h"
 #include "openvswitch/match.h"
@@ -159,6 +160,7 @@ consider_port_binding(enum mf_field_id mff_ovn_geneve,
                       struct hmap *local_datapaths,
                       struct hmap *patched_datapaths,
                       const struct sbrec_port_binding *binding,
+                      const struct lport_index *lports,
                       struct ofpbuf *ofpacts_p,
                       struct hmap *flow_table)
 {
@@ -199,6 +201,9 @@ consider_port_binding(enum mf_field_id mff_ovn_geneve,
      *       The same logic handles logical patch ports, as well as
      *       localnet patch ports.
      *
+     *       If the port is a chassisredirect port on the chassis we are
+     *       managing, the OpenFlow port for the chassisredirect port.
+     *
      *       For a container nested inside a VM and accessible via a VLAN,
      *       'tag' is the VLAN ID; otherwise 'tag' is 0.
      *
@@ -351,18 +356,54 @@ consider_port_binding(enum mf_field_id mff_ovn_geneve,
         match_set_metadata(&match, htonll(dp_key));
         match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, port_key);
 
-        if (zone_id) {
-            put_load(zone_id, MFF_LOG_CT_ZONE, 0, 32, ofpacts_p);
-        }
-        if (zone_id_dnat) {
-            put_load(zone_id_dnat, MFF_LOG_DNAT_ZONE, 0, 32, ofpacts_p);
+        /* If this is a chassisredirect port, reset the outport to
+         * the distributed port */
+        bool chassisredirect_error = false;
+        if (!strcmp(binding->type, "chassisredirect")) {
+            const char *distributed_port = smap_get(&binding->options,
+                                                    "distributed-port");
+            const struct sbrec_port_binding *distributed_binding;
+            distributed_binding = lport_lookup_by_name(lports,
+                                                       distributed_port);
+            if (!distributed_binding) {
+                chassisredirect_error = true; /* Packet will be dropped. */
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+                VLOG_WARN_RL(&rl, "No port binding record for distributed "
+                         "port %s referred by chassisredirect port %s",
+                         distributed_port,
+                         binding->logical_port);
+            } else if (binding->datapath !=
+                       distributed_binding->datapath) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+                VLOG_WARN_RL(&rl,
+                             "chassisredirect port %s refers to "
+                             "distributed port %s in wrong datapath",
+                             binding->logical_port,
+                             distributed_port);
+                chassisredirect_error = true; /* Packet will be dropped. */
+            } else {
+                put_load(distributed_binding->tunnel_key,
+                         MFF_LOG_OUTPORT, 0, 32, ofpacts_p);
+                zone_id = simap_get(ct_zones, distributed_port);
+            }
         }
-        if (zone_id_snat) {
-            put_load(zone_id_snat, MFF_LOG_SNAT_ZONE, 0, 32, ofpacts_p);
+
+        if (!chassisredirect_error) {
+        /* Normal handling if not chassisredirect port or no error. */
+            if (zone_id) {
+                put_load(zone_id, MFF_LOG_CT_ZONE, 0, 32, ofpacts_p);
+            }
+            if (zone_id_dnat) {
+                put_load(zone_id_dnat, MFF_LOG_DNAT_ZONE, 0, 32, ofpacts_p);
+            }
+            if (zone_id_snat) {
+                put_load(zone_id_snat, MFF_LOG_SNAT_ZONE, 0, 32, ofpacts_p);
+            }
+
+            /* Resubmit to table 34. */
+            put_resubmit(OFTABLE_CHECK_LOOPBACK, ofpacts_p);
         }
 
-        /* Resubmit to table 34. */
-        put_resubmit(OFTABLE_CHECK_LOOPBACK, ofpacts_p);
         ofctrl_add_flow(flow_table, OFTABLE_LOCAL_OUTPUT, 100,
                         &match, ofpacts_p);
 
@@ -627,6 +668,7 @@ consider_mc_group(enum mf_field_id mff_ovn_geneve,
 void
 physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
              const struct ovsrec_bridge *br_int, const char *this_chassis_id,
+             const struct lport_index *lports,
              const struct simap *ct_zones, struct hmap *flow_table,
              struct hmap *local_datapaths, struct hmap *patched_datapaths)
 {
@@ -658,6 +700,8 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
                                         "ovn-l3gateway-port");
         const char *logpatch = smap_get(&port_rec->external_ids,
                                         "ovn-logical-patch-port");
+        const char *chassisredirect = smap_get(&port_rec->external_ids,
+                                               "ovn-chassisredirect-port");
 
         for (int j = 0; j < port_rec->n_interfaces; j++) {
             const struct ovsrec_interface *iface_rec = port_rec->interfaces[j];
@@ -690,6 +734,10 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
                 /* Logical patch ports can be handled just like VIFs. */
                 simap_put(&new_localvif_to_ofport, logpatch, ofport);
                 break;
+            } else if (!strcmp(iface_rec->type, "internal") &&
+                       chassisredirect) {
+                simap_put(&new_localvif_to_ofport, chassisredirect, ofport);
+                break;
             } else if (chassis_id) {
                 enum chassis_tunnel_type tunnel_type;
                 if (!strcmp(iface_rec->type, "geneve")) {
@@ -782,7 +830,7 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
          * for loop without having cleared the flow table first, we
          * should clear the old flows to avoid collisions. */
         consider_port_binding(mff_ovn_geneve, ct_zones, local_datapaths,
-                              patched_datapaths, binding, &ofpacts,
+                              patched_datapaths, binding, lports, &ofpacts,
                               flow_table);
     }
 
diff --git a/ovn/controller/physical.h b/ovn/controller/physical.h
index 86ce93c..8f66f62 100644
--- a/ovn/controller/physical.h
+++ b/ovn/controller/physical.h
@@ -32,6 +32,7 @@ struct hmap;
 struct ovsdb_idl;
 struct ovsrec_bridge;
 struct simap;
+struct lport_index;
 
 /* OVN Geneve option information.
  *
@@ -43,6 +44,7 @@ struct simap;
 void physical_register_ovs_idl(struct ovsdb_idl *);
 void physical_run(struct controller_ctx *, enum mf_field_id mff_ovn_geneve,
                   const struct ovsrec_bridge *br_int, const char *chassis_id,
+                  const struct lport_index *lports,
                   const struct simap *ct_zones, struct hmap *flow_table,
                   struct hmap *local_datapaths, struct hmap *patched_datapaths);
 
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index ad0739c..89f04d0 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -382,6 +382,15 @@ struct ovn_datapath {
 
     /* IPAM data. */
     struct hmap ipam;
+
+    /* OVN northd only needs to know about the logical router gateway port for
+     * NAT on a distributed router.  This is populated only when there is a
+     * "redirect-chassis" specified for one of the ports on the logical router.
+     * Otherwise this will be NULL. */
+    struct ovn_port *l3gateway_port;
+    /* The "derived" OVN port representing the instance of l3gateway_port on
+     * the "redirect-chassis". */
+    struct ovn_port *l3redirect_port;
 };
 
 struct macam_node {
@@ -658,6 +667,9 @@ struct ovn_port {
 
     struct lport_addresses lrp_networks;
 
+    bool derived; /* Indicates whether this is an additional port
+                   * derived from nbsp or nbrp. */
+
     /* The port's peer:
      *
      *     - A switch port S of type "router" has a router port R as a peer,
@@ -687,6 +699,7 @@ ovn_port_create(struct hmap *ports, const char *key,
     op->sb = sb;
     op->nbsp = nbsp;
     op->nbrp = nbrp;
+    op->derived = false;
     hmap_insert(ports, &op->key_node, hash_string(op->key, 0));
     return op;
 }
@@ -737,6 +750,16 @@ ovn_port_allocate_key(struct ovn_datapath *od)
                           (1u << 15) - 1, &od->port_key_hint);
 }
 
+static char *
+chassis_redirect_name(const char *port_name)
+/* XXX Need to derive unique name of max length 15 bytes since chassisredirect
+ * ports use interface type "internal".  If name is greater than 15 bytes, no
+ * ofport is assigned, so chassisredirect port is never treated as local and
+ * the necessary flows are not created. */
+{
+    return xasprintf("cr-%s", port_name);
+}
+
 static bool
 ipam_is_duplicate_mac(struct eth_addr *ea, uint64_t mac64, bool warn)
 {
@@ -1298,6 +1321,52 @@ join_logical_ports(struct northd_context *ctx,
                 op->lrp_networks = lrp_networks;
                 op->od = od;
                 ipam_add_port_addresses(op->od, op);
+
+                const char *redirect_chassis = smap_get(&op->nbrp->options,
+                                                        "redirect-chassis");
+                if (redirect_chassis) {
+                    /* Additional "derived" ovn_port crp represents the
+                     * instance of op on the "redirect-chassis". */
+                    const char *gw_chassis = smap_get(&op->od->nbr->options,
+                                                   "chassis");
+                    if (gw_chassis) {
+                        static struct vlog_rate_limit rl
+                            = VLOG_RATE_LIMIT_INIT(1, 1);
+                        VLOG_WARN_RL(&rl, "Bad configuration: "
+                                     "redirect-chassis configured on port %s "
+                                     "on L3 gateway router", nbrp->name);
+                        continue;
+                    }
+                    const char *redirect_name
+                                         = chassis_redirect_name(nbrp->name);
+                    struct ovn_port *crp = ovn_port_find(ports, redirect_name);
+                    if (crp) {
+                        crp->derived = true;
+                        crp->nbrp = 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->od = od;
+
+                    /* Set l3gateway_port and l3redirect_port in od, for later
+                     * use during flow creation. */
+                    if (od->l3gateway_port || od->l3redirect_port) {
+                        static struct vlog_rate_limit rl
+                            = VLOG_RATE_LIMIT_INIT(1, 1);
+                        VLOG_WARN_RL(&rl, "Bad configuration: multiple ports "
+                                     "with redirect-chassis on same logical "
+                                     "router %s", od->nbr->name);
+                        continue;
+                    } else {
+                        od->l3gateway_port = op;
+                        od->l3redirect_port = crp;
+                    }
+                }
             }
         }
     }
@@ -1306,7 +1375,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 && !strcmp(op->nbsp->type, "router")) {
+        if (op->nbsp && !strcmp(op->nbsp->type, "router") && !op->derived) {
             const char *peer_name = smap_get(&op->nbsp->options, "router-port");
             if (!peer_name) {
                 continue;
@@ -1323,7 +1392,7 @@ join_logical_ports(struct northd_context *ctx,
                 op->od->router_ports,
                 sizeof *op->od->router_ports * (op->od->n_router_ports + 1));
             op->od->router_ports[op->od->n_router_ports++] = op;
-        } else if (op->nbrp && op->nbrp->peer) {
+        } else if (op->nbrp && op->nbrp->peer && !op->derived) {
             struct ovn_port *peer = ovn_port_find(ports, op->nbrp->peer);
             if (peer) {
                 if (peer->nbrp) {
@@ -1353,18 +1422,29 @@ ovn_port_update_sbrec(const struct ovn_port *op,
         /* If the router is for l3 gateway, it resides on a chassis
          * and its port type is "l3gateway". */
         const char *chassis = smap_get(&op->od->nbr->options, "chassis");
-        if (chassis) {
+        if (op->derived) {
+            sbrec_port_binding_set_type(op->sb, "chassisredirect");
+        } else if (chassis) {
             sbrec_port_binding_set_type(op->sb, "l3gateway");
         } else {
             sbrec_port_binding_set_type(op->sb, "patch");
         }
 
-        const char *peer = op->peer ? op->peer->key : "<error>";
         struct smap new;
         smap_init(&new);
-        smap_add(&new, "peer", peer);
-        if (chassis) {
-            smap_add(&new, "l3gateway-chassis", chassis);
+        if (op->derived) {
+            const char *redirect_chassis = smap_get(&op->nbrp->options,
+                                                    "redirect-chassis");
+            if (redirect_chassis) {
+                smap_add(&new, "redirect-chassis", redirect_chassis);
+            }
+            smap_add(&new, "distributed-port", op->nbrp->name);
+        } else {
+            const char *peer = op->peer ? op->peer->key : "<error>";
+            smap_add(&new, "peer", peer);
+            if (chassis) {
+                smap_add(&new, "l3gateway-chassis", chassis);
+            }
         }
         sbrec_port_binding_set_options(op->sb, &new);
         smap_destroy(&new);
@@ -3475,6 +3555,12 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
+        if (op->derived) {
+            /* No ingress packets should be received on a chassisredirect
+             * port. */
+            continue;
+        }
+
         ds_clear(&match);
         ds_put_format(&match, "(eth.mcast || eth.dst == %s) && inport == %s",
                       op->lrp_networks.ea_s, op->json_key);
@@ -3540,6 +3626,11 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
+        if (op->derived) {
+            /* No ingress packets are accepted on a chassisredirect
+             * port, so no need to program flows for that port. */
+            continue;
+        }
 
         if (op->lrp_networks.n_ipv4_addrs) {
             /* L3 admission control: drop packets that originate from an
@@ -3747,6 +3838,12 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
+        if (op->derived) {
+            /* No ingress packets are accepted on a chassisredirect
+             * port, so no need to program flows for that port. */
+            continue;
+        }
+
         if (op->lrp_networks.n_ipv6_addrs) {
             /* L3 admission control: drop packets that originate from an
              * IPv6 address owned by the router (priority 100). */
@@ -4287,6 +4384,14 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
+        if (op->derived) {
+            /* No egress packets should be processed in the context of
+             * a chassisredirect port.  The chassisredirect port should
+             * be replaced by the l3gateway port in the local output
+             * pipeline stage before egress processing. */
+            continue;
+        }
+
         ds_clear(&match);
         ds_put_format(&match, "outport == %s", op->json_key);
         ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_DELIVERY, 100,
diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
index 865dd34..6d779f7 100644
--- a/ovn/ovn-nb.ovsschema
+++ b/ovn/ovn-nb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Northbound",
-    "version": "5.4.0",
-    "cksum": "4176761817 11225",
+    "version": "5.5.0",
+    "cksum": "44118209 11438",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -182,6 +182,11 @@
         "Logical_Router_Port": {
             "columns": {
                 "name": {"type": "string"},
+                "options": {
+                    "type": {"key": "string",
+                             "value": "string",
+                             "min": 0,
+                             "max": "unlimited"}},
                 "networks": {"type": {"key": "string",
                                       "min": 1,
                                       "max": "unlimited"}},
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index e1b3136..55efe1f 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -1034,6 +1034,32 @@
       port has all ingress and egress traffic dropped.
     </column>
 
+    <group title="Options">
+      <p>
+        Additional options for the logical router port.
+      </p>
+
+      <column name="options" key="redirect-chassis">
+        <p>
+          If set, this indicates a desire to create (in the southbound
+          database) an additional port that represents a particular instance,
+          bound to a specific chassis, of this otherwise distributed logical
+          router port.  This additional port can then be specified as an
+          <code>outport</code> in ingress pipeline flows.  This will cause
+          matching packets to be directed to a specific chassis to carry out
+          the egress pipeline.  At the beginning of the egress pipeline, the
+          <code>outport</code> will be reset to the value of the distributed
+          port.
+        </p>
+
+        <p>
+          This option specifies the name of the <code>chassis</code> to which
+          the additional southbound port binding of type
+          <code>chassisredirect</code> will be bound.
+        </p>
+      </column>
+    </group>
+
     <group title="Attachment">
       <p>
         A given router port serves one of two purposes:
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 4e0047a..b0f5635 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1748,6 +1748,21 @@ tcp.flags = RST;
             table="Port_Binding"/>:<code>vtep-logical-switch</code> must also
             be defined.
           </dd>
+
+          <dt><code>chassisredirect</code></dt>
+          <dd>
+            A logical port that represents a particular instance, bound
+            to a specific chassis, of an otherwise distributed parent
+            port (e.g. of type <code>patch</code>).  A
+            <code>chassisredirect</code> port should never be used as an
+            <code>inport</code>.  When an ingress pipeline sets the
+            <code>outport</code>, it may set the value to a logical port
+            of type <code>chassisredirect</code>.  This will cause the
+            packet to be directed to a specific chassis to carry out the
+            egress pipeline.  At the beginning of the egress pipeline,
+            the <code>outport</code> will be reset to the value of the
+            distributed port.
+          </dd>
         </dl>
       </column>
     </group>
@@ -1904,6 +1919,26 @@ tcp.flags = RST;
       </column>
     </group>
 
+    <group title="Chassis Redirect Options">
+      <p>
+        These options apply to logical ports with <ref column="type"/>
+        of <code>chassisredirect</code>.
+      </p>
+
+      <column name="options" key="distributed-port">
+        The name of the distributed port for which this
+        <code>chassisredirect</code> port represents a particular instance.
+      </column>
+
+      <column name="options" key="redirect-chassis">
+        The <code>chassis</code> that this <code>chassisredirect</code> port
+        is bound to.  This is taken from <ref table="Logical_Router_Port"
+        column="options" key="redirect-chassis" db="OVN_Northbound"/>
+        in the OVN_Northbound database's <ref table="Logical_Router_Port"
+        db="OVN_Northbound"/> table.
+      </column>
+    </group>
+
     <group title="Nested Containers">
       <p>
         These columns support containers nested within a VM.  Specifically,
