diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c
index 2ec0ba9..ba2cddf 100644
--- a/ovn/controller/physical.c
+++ b/ovn/controller/physical.c
@@ -23,7 +23,9 @@
 #include "ovn-controller.h"
 #include "ovn/lib/ovn-sb-idl.h"
 #include "openvswitch/vlog.h"
+#include "shash.h"
 #include "simap.h"
+#include "smap.h"
 #include "sset.h"
 #include "vswitch-idl.h"
 
@@ -138,6 +140,8 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
 {
     struct simap lport_to_ofport = SIMAP_INITIALIZER(&lport_to_ofport);
     struct hmap tunnels = HMAP_INITIALIZER(&tunnels);
+    struct simap localnet_to_ofport = SIMAP_INITIALIZER(&localnet_to_ofport);
+
     for (int i = 0; i < br_int->n_ports; i++) {
         const struct ovsrec_port *port_rec = br_int->ports[i];
         if (!strcmp(port_rec->name, br_int->name)) {
@@ -150,6 +154,9 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
             continue;
         }
 
+        const char *localnet = smap_get(&port_rec->external_ids,
+                                        "ovn-patch-port");
+
         for (int j = 0; j < port_rec->n_interfaces; j++) {
             const struct ovsrec_interface *iface_rec = port_rec->interfaces[j];
 
@@ -162,8 +169,11 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
                 continue;
             }
 
-            /* Record as chassis or local logical port. */
-            if (chassis_id) {
+            /* Record as patch to local net, chassis, or local logical port. */
+            if (!strcmp(iface_rec->type, "patch") && localnet) {
+                simap_put(&localnet_to_ofport, localnet, ofport);
+                break;
+            } else if (chassis_id) {
                 enum chassis_tunnel_type tunnel_type;
                 if (!strcmp(iface_rec->type, "geneve")) {
                     tunnel_type = GENEVE;
@@ -196,6 +206,20 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
     struct ofpbuf ofpacts;
     ofpbuf_init(&ofpacts, 0);
 
+    struct binding_elem {
+        struct ovs_list list_elem;
+        const struct sbrec_port_binding *binding;
+    };
+    struct localnet_bindings {
+        ofp_port_t ofport;
+        struct ovs_list bindings;
+    };
+    /* A list of localnet port bindings hashed by network name */
+    struct shash localnet_inputs = SHASH_INITIALIZER(&localnet_inputs);
+
+    /* Datapaths with at least one local port binding */
+    struct hmap local_datapaths = HMAP_INITIALIZER(&local_datapaths);
+
     /* Set up flows in table 0 for physical-to-logical translation and in table
      * 64 for logical-to-physical translation. */
     const struct sbrec_port_binding *binding;
@@ -210,7 +234,13 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
 
         int tag = 0;
         ofp_port_t ofport;
-        if (binding->parent_port) {
+        if (!strcmp(binding->type, "localnet")) {
+            const char *network = smap_get(&binding->options, "network_name");
+            if (!network) {
+                continue;
+            }
+            ofport = u16_to_ofp(simap_get(&localnet_to_ofport, network));
+        } else if (binding->parent_port) {
             ofport = u16_to_ofp(simap_get(&lport_to_ofport,
                                           binding->parent_port));
             if (ofport && binding->tag) {
@@ -245,33 +275,67 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
              *
              * Priority 150 is for traffic belonging to containers. For such
              * traffic, match on the tags and then strip the tag.
-             * Priority 100 is for traffic belonging to VMs.
+             * Priority 100 is for traffic belonging to VMs or locally connected
+             * networks.
              *
              * For both types of traffic: set MFF_LOG_INPORT to the logical
              * input port, MFF_LOG_DATAPATH to the logical datapath, and
              * resubmit into the logical ingress pipeline starting at table
              * 16. */
-            match_init_catchall(&match);
-            ofpbuf_clear(&ofpacts);
-            match_set_in_port(&match, ofport);
-            if (tag) {
-                match_set_dl_vlan(&match, htons(tag));
-            }
+            if (!strcmp(binding->type, "localnet")) {
+                /* The same OpenFlow port may correspond to localnet ports
+                 * attached to more than one logical datapath, so keep track of
+                 * all associated bindings and add a flow at the end. */
+
+                const char *network = smap_get(&binding->options, "network_name");
+                struct shash_node *node;
+                struct localnet_bindings *ln_bindings;
+
+                node = shash_find(&localnet_inputs, network);
+                if (!node) {
+                    ln_bindings = xmalloc(sizeof *ln_bindings);
+                    ln_bindings->ofport = ofport;
+                    list_init(&ln_bindings->bindings);
+                    node = shash_add(&localnet_inputs, network, ln_bindings);
+                }
+                ln_bindings = node->data;
 
-            /* Set MFF_LOG_DATAPATH and MFF_LOG_INPORT. */
-            put_load(binding->datapath->tunnel_key, MFF_LOG_DATAPATH, 0, 64,
-                     &ofpacts);
-            put_load(binding->tunnel_key, MFF_LOG_INPORT, 0, 32, &ofpacts);
+                struct binding_elem *b = xmalloc(sizeof *b);
+                b->binding = binding;
+                list_insert(&ln_bindings->bindings, &b->list_elem);
+            } else {
+                struct hmap_node *ld;
+                ld = hmap_first_with_hash(&local_datapaths,
+                                          binding->datapath->tunnel_key);
+                if (!ld) {
+                    ld = xmalloc(sizeof *ld);
+                    hmap_insert(&local_datapaths, ld,
+                                binding->datapath->tunnel_key);
+                }
 
-            /* Strip vlans. */
-            if (tag) {
-                ofpact_put_STRIP_VLAN(&ofpacts);
-            }
+                ofpbuf_clear(&ofpacts);
+                match_init_catchall(&match);
+                match_set_in_port(&match, ofport);
+                if (tag) {
+                    match_set_dl_vlan(&match, htons(tag));
+                }
 
-            /* Resubmit to first logical ingress pipeline table. */
-            put_resubmit(OFTABLE_LOG_INGRESS_PIPELINE, &ofpacts);
-            ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG, tag ? 150 : 100,
-                            &match, &ofpacts);
+                /* Set MFF_LOG_DATAPATH and MFF_LOG_INPORT. */
+                put_load(binding->datapath->tunnel_key, MFF_LOG_DATAPATH, 0, 64,
+                         &ofpacts);
+                put_load(binding->tunnel_key, MFF_LOG_INPORT, 0, 32,
+                         &ofpacts);
+
+                /* Strip vlans. */
+                if (tag) {
+                    ofpact_put_STRIP_VLAN(&ofpacts);
+                }
+
+                /* Resubmit to first logical ingress pipeline table. */
+                put_resubmit(OFTABLE_LOG_INGRESS_PIPELINE, &ofpacts);
+                ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG,
+                                tag ? 150 : 100, &match, &ofpacts);
+            }
 
             /* Table 33, priority 100.
              * =======================
@@ -401,6 +465,16 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
                 put_resubmit(OFTABLE_DROP_LOOPBACK, &ofpacts);
             } else if (port->chassis) {
                 sset_add(&remote_chassis, port->chassis->name);
+            } else if (!strcmp(port->type, "localnet")) {
+                const char *network = smap_get(&port->options, "network_name");
+                if (!network) {
+                    continue;
+                }
+                if (!simap_contains(&localnet_to_ofport, network)) {
+                    continue;
+                }
+                put_load(port->tunnel_key, MFF_LOG_OUTPORT, 0, 32, &ofpacts);
+                put_resubmit(OFTABLE_DROP_LOOPBACK, &ofpacts);
             }
         }
 
@@ -516,4 +590,64 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
         free(tun);
     }
     hmap_destroy(&tunnels);
+
+    /* Table 0, Priority 100
+     * =====================
+     *
+     * We have now determined the full set of port bindings associated with each
+     * "localnet" network.  Only create flows for datapaths that have another
+     * local binding.  Otherwise, we know it would just be dropped.
+     */
+    struct shash_node *ln_bindings_node, *ln_bindings_node_next;
+    struct localnet_bindings *ln_bindings;
+    SHASH_FOR_EACH_SAFE (ln_bindings_node, ln_bindings_node_next, &localnet_inputs) {
+        ln_bindings = ln_bindings_node->data;
+
+        struct match match;
+        match_init_catchall(&match);
+        match_set_in_port(&match, ln_bindings->ofport);
+
+        struct ofpbuf ofpacts;
+        ofpbuf_init(&ofpacts, 0);
+
+        bool found_local = false;
+        struct binding_elem *b;
+        LIST_FOR_EACH_POP (b, list_elem, &ln_bindings->bindings) {
+            struct hmap_node *ld;
+            ld = hmap_first_with_hash(&local_datapaths,
+                                      b->binding->datapath->tunnel_key);
+            if (ld) {
+                /* This datapath has local port bindings. */
+                found_local = true;
+
+                /* Set MFF_LOG_DATAPATH and MFF_LOG_INPORT. */
+                put_load(b->binding->datapath->tunnel_key, MFF_LOG_DATAPATH,
+                         0, 64, &ofpacts);
+                put_load(b->binding->tunnel_key, MFF_LOG_INPORT, 0, 32,
+                         &ofpacts);
+                put_resubmit(OFTABLE_LOG_INGRESS_PIPELINE, &ofpacts);
+            }
+
+            free(b);
+        }
+
+        if (found_local) {
+            ofctrl_add_flow(flow_table, 0, 100, &match, &ofpacts);
+        }
+
+        ofpbuf_uninit(&ofpacts);
+
+        shash_delete(&localnet_inputs, ln_bindings_node);
+        free(ln_bindings);
+    }
+    shash_destroy(&localnet_inputs);
+
+    struct hmap_node *node;
+    while ((node = hmap_first(&local_datapaths))) {
+        hmap_remove(&local_datapaths, node);
+        free(node);
+    }
+    hmap_destroy(&local_datapaths);
+
+    simap_destroy(&localnet_to_ofport);
 }
diff --git a/ovn/ovn-architecture.7.xml b/ovn/ovn-architecture.7.xml
index 31488bd..b4e6d10 100644
--- a/ovn/ovn-architecture.7.xml
+++ b/ovn/ovn-architecture.7.xml
@@ -679,6 +679,13 @@
       </p>
 
       <p>
+        It's possible that a single ingress physical port maps to multiple
+        logical ports with a type of <code>localnet</code>. The logical datapath
+        and logical input port fields will be reset and the packet will be
+        resubmitted to table 16 multiple times.
+      </p>
+
+      <p>
         Packets that originate from a container nested within a VM are treated
         in a slightly different way.  The originating container can be
         distinguished based on the VIF-specific VLAN ID, so the
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index ade8164..0dfdab5 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -116,13 +116,25 @@
       </p>
 
       <p>
-      There are no other logical port types implemented yet.
+      When this column is set to <em>localnet</em>, this logical port represents a
+      connection to a locally accessible network from each ovn-controller instance.
+      A logical switch can only have a single <em>localnet</em> port attached
+      and at most one regular logical port.  This is used to model direct
+      connectivity to an existing network.
       </p>
     </column>
 
     <column name="options">
-        This column provides key/value settings specific to the logical port
-        <ref column="type"/>.
+      <p>
+      This column provides key/value settings specific to the logical port
+      <ref column="type"/>.
+      </p>
+
+      <p>
+      When <ref column="type"/> is set to <em>localnet</em>, you must set the option
+      <em>network_name</em>.  ovn-controller uses local configuration to determine
+      exactly how to connect to this locally accessible network.
+      </p>
     </column>
 
     <column name="parent_name">
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 8102eb3..844150c 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -943,13 +943,40 @@
       </p>
 
       <p>
-      There are no other logical port types implemented yet.
+      When this column is set to <em>localnet</em>, this logical port represents a
+      connection to a locally accessible network from each ovn-controller instance.
+      A logical switch can only have a single <em>localnet</em> port attached
+      and at most one regular logical port.  This is used to model direct
+      connectivity to an existing network.
       </p>
     </column>
 
     <column name="options">
-        This column provides key/value settings specific to the logical port
-        <ref column="type"/>.
+      <p>
+      This column provides key/value settings specific to the logical port
+      <ref column="type"/>.
+      </p>
+
+      <p>
+      When <ref column="type"/> is set to <em>localnet</em>, you must set the option
+      <em>network_name</em>.  ovn-controller uses the configuration entry
+      <em>ovn-bridge-mappings</em> to determine how to connect to this network.
+      <em>ovn-bridge-mappings</em> is a list of network names mapped to a local
+      OVS bridge that provides access to that network.  An example of configuring
+      <em>ovn-bridge-mappings</em> would be:
+      </p>
+
+      <p>
+      <em>$ ovs-vsctl set open . external-ids:ovn-bridge-mappings=physnet1:br-eth0,physnet2:br-eth1</em>
+      </p>
+
+      <p>
+      Also note that when a logical switch has a <em>localnet</em> port attached,
+      every chassis that may have a local vif attached to that logical switch
+      must have a bridge mapping configured to reach that <em>localnet</em>.
+      Traffic that arrives on a <em>localnet</em> port is never forwarded over a tunnel
+      to another chassis.
+      </p>
     </column>
 
     <column name="tunnel_key">
