[ovs-dev,IPv6,v2,08/10] ovn: Don't require clearing inport to hair-pin packets.
diff mbox

Message ID 1469773580-33112-8-git-send-email-jpettit@ovn.org
State Accepted
Headers show

Commit Message

Justin Pettit July 29, 2016, 6:26 a.m. UTC
Introduce the "flags.loopback" symbol to allow packets to be sent back
on their ingress ports.  Previously, one needed to clear "inport" to
hair-pin packets, but this made "inport" not available for future
matching.  This approach should be more intuitive, but it will also be
needed in future patches.

This patch also removes functionality from the OVN expression library
that clears the OpenFlow ingress port when the logical input port is
zeroed.

Signed-off-by: Justin Pettit <jpettit@ovn.org>
---
 ovn/controller/lflow.c      |  8 +++++++-
 ovn/controller/lflow.h      |  7 ++++---
 ovn/controller/physical.c   | 46 +++++++++++++++++++++++++++++++++++++++------
 ovn/lib/expr.c              | 10 ----------
 ovn/lib/logical-fields.h    | 10 ++++++++++
 ovn/northd/ovn-northd.8.xml | 26 ++++++++++++-------------
 ovn/northd/ovn-northd.c     | 24 +++++++++++------------
 ovn/ovn-architecture.7.xml  | 36 ++++++++++++++++++++++++++++-------
 ovn/ovn-sb.xml              | 29 ++++++++++++++--------------
 tests/ovn.at                |  4 ++--
 10 files changed, 132 insertions(+), 68 deletions(-)

Comments

Ben Pfaff July 29, 2016, 5:43 p.m. UTC | #1
On Thu, Jul 28, 2016 at 11:26:18PM -0700, Justin Pettit wrote:
> Introduce the "flags.loopback" symbol to allow packets to be sent back
> on their ingress ports.  Previously, one needed to clear "inport" to
> hair-pin packets, but this made "inport" not available for future
> matching.  This approach should be more intuitive, but it will also be
> needed in future patches.
> 
> This patch also removes functionality from the OVN expression library
> that clears the OpenFlow ingress port when the logical input port is
> zeroed.
> 
> Signed-off-by: Justin Pettit <jpettit@ovn.org>

I found the description of OpenFlow table 64 in ovn-architecture(7) to
be somewhat confusing.  Here's a rewrite:

      <p>
        Table 64 bypasses OpenFlow loopback when MLF_ALLOW_LOOPBACK is set.
        Logical loopback was handled in table 34, but OpenFlow by default also
        prevents loopback to the OpenFlow ingress port.  Thus, when
        MLF_ALLOW_LOOPBACK is set, OpenFlow table 64 saves the OpenFlow ingress
        port, sets it to zero, resubmits to table 65 for logical-to-physical
        transformation, and then restores the OpenFlow ingress port,
        effectively disabling OpenFlow loopback prevents.  When
        MLF_ALLOW_LOOPBACK is unset, table 64 flow simply resubmits to table
        65.
      </p>

Also, I think that tables 64 and 65 should be in different <li>...</li>
blocks instead of the same one.

Acked-by: Ben Pfaff <blp@ovn.org>

Patch
diff mbox

diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index c06bf18..674b756 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -81,6 +81,12 @@  lflow_init(void)
     expr_symtab_add_field(&symtab, "xxreg0", MFF_XXREG0, NULL, false);
     expr_symtab_add_field(&symtab, "xxreg1", MFF_XXREG1, NULL, false);
 
+    /* Flags used in logical to physical transformation. */
+    expr_symtab_add_field(&symtab, "flags", MFF_LOG_FLAGS, NULL, false);
+    char flags_str[16];
+    snprintf(flags_str, sizeof flags_str, "flags[%d]", MLF_ALLOW_LOOPBACK_BIT);
+    expr_symtab_add_subfield(&symtab, "flags.loopback", NULL, flags_str);
+
     /* Connection tracking state. */
     expr_symtab_add_field(&symtab, "ct_mark", MFF_CT_MARK, NULL, false);
     expr_symtab_add_field(&symtab, "ct_label", MFF_CT_LABEL, NULL, false);
@@ -494,7 +500,7 @@  consider_logical_flow(const struct lport_index *lports,
     uint8_t ptable = first_ptable + lflow->table_id;
     uint8_t output_ptable = (ingress
                              ? OFTABLE_REMOTE_OUTPUT
-                             : OFTABLE_LOG_TO_PHY);
+                             : OFTABLE_SAVE_INPORT);
 
     /* Translate OVN actions into OpenFlow actions.
      *
diff --git a/ovn/controller/lflow.h b/ovn/controller/lflow.h
index c512a26..ac058ff 100644
--- a/ovn/controller/lflow.h
+++ b/ovn/controller/lflow.h
@@ -51,10 +51,11 @@  struct uuid;
 #define OFTABLE_LOG_INGRESS_PIPELINE 16 /* First of LOG_PIPELINE_LEN tables. */
 #define OFTABLE_REMOTE_OUTPUT        32
 #define OFTABLE_LOCAL_OUTPUT         33
-#define OFTABLE_DROP_LOOPBACK        34
+#define OFTABLE_CHECK_LOOPBACK       34
 #define OFTABLE_LOG_EGRESS_PIPELINE  48 /* First of LOG_PIPELINE_LEN tables. */
-#define OFTABLE_LOG_TO_PHY           64
-#define OFTABLE_MAC_BINDING          65
+#define OFTABLE_SAVE_INPORT          64
+#define OFTABLE_LOG_TO_PHY           65
+#define OFTABLE_MAC_BINDING          66
 
 /* The number of tables for the ingress and egress pipelines. */
 #define LOG_PIPELINE_LEN 16
diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c
index ee1da4f..15d4e00 100644
--- a/ovn/controller/physical.c
+++ b/ovn/controller/physical.c
@@ -375,25 +375,49 @@  consider_port_binding(enum mf_field_id mff_ovn_geneve,
         }
 
         /* Resubmit to table 34. */
-        put_resubmit(OFTABLE_DROP_LOOPBACK, ofpacts_p);
+        put_resubmit(OFTABLE_CHECK_LOOPBACK, ofpacts_p);
         ofctrl_add_flow(OFTABLE_LOCAL_OUTPUT, 100,
                         &match, ofpacts_p, &binding->header_.uuid);
 
         /* Table 34, Priority 100.
          * =======================
          *
-         * Drop packets whose logical inport and outport are the same. */
+         * Drop packets whose logical inport and outport are the same
+         * and the MLF_ALLOW_LOOPBACK flag is not set. */
         match_init_catchall(&match);
         ofpbuf_clear(ofpacts_p);
         match_set_metadata(&match, htonll(dp_key));
+        match_set_reg_masked(&match, MFF_LOG_FLAGS - MFF_REG0,
+                             0, MLF_ALLOW_LOOPBACK);
         match_set_reg(&match, MFF_LOG_INPORT - MFF_REG0, port_key);
         match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, port_key);
-        ofctrl_add_flow(OFTABLE_DROP_LOOPBACK, 100,
+        ofctrl_add_flow(OFTABLE_CHECK_LOOPBACK, 100,
                         &match, ofpacts_p, &binding->header_.uuid);
 
         /* Table 64, Priority 100.
          * =======================
          *
+         * If the packet is supposed to hair-pin because the "loopback"
+         * flag is set, temporarily set the in_port to zero, resubmit to
+         * table 65 for logical-to-physical translation, then restore
+         * the port number. */
+        match_init_catchall(&match);
+        ofpbuf_clear(ofpacts_p);
+        match_set_metadata(&match, htonll(dp_key));
+        match_set_reg_masked(&match, MFF_LOG_FLAGS - MFF_REG0,
+                             MLF_ALLOW_LOOPBACK, MLF_ALLOW_LOOPBACK);
+        match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, port_key);
+
+        put_stack(MFF_IN_PORT, ofpact_put_STACK_PUSH(ofpacts_p));
+        put_load(0, MFF_IN_PORT, 0, 16, ofpacts_p);
+        put_resubmit(OFTABLE_LOG_TO_PHY, ofpacts_p);
+        put_stack(MFF_IN_PORT, ofpact_put_STACK_POP(ofpacts_p));
+        ofctrl_add_flow(OFTABLE_SAVE_INPORT, 100,
+                        &match, ofpacts_p, &binding->header_.uuid);
+
+        /* Table 65, Priority 100.
+         * =======================
+         *
          * Deliver the packet to the local vif. */
         match_init_catchall(&match);
         ofpbuf_clear(ofpacts_p);
@@ -523,12 +547,12 @@  consider_mc_group(enum mf_field_id mff_ovn_geneve,
         if (!strcmp(port->type, "patch")) {
             put_load(port->tunnel_key, MFF_LOG_OUTPORT, 0, 32,
                      remote_ofpacts_p);
-            put_resubmit(OFTABLE_DROP_LOOPBACK, remote_ofpacts_p);
+            put_resubmit(OFTABLE_CHECK_LOOPBACK, remote_ofpacts_p);
         } else if (simap_contains(&localvif_to_ofport,
                            (port->parent_port && *port->parent_port)
                            ? port->parent_port : port->logical_port)) {
             put_load(port->tunnel_key, MFF_LOG_OUTPORT, 0, 32, ofpacts_p);
-            put_resubmit(OFTABLE_DROP_LOOPBACK, ofpacts_p);
+            put_resubmit(OFTABLE_CHECK_LOOPBACK, ofpacts_p);
         } else if (port->chassis && !get_localnet_port(local_datapaths,
                                          mc->datapath->tunnel_key)) {
             /* Add remote chassis only when localnet port not exist,
@@ -888,7 +912,17 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
     MFF_LOG_REGS;
 #undef MFF_LOG_REGS
     put_resubmit(OFTABLE_LOG_EGRESS_PIPELINE, &ofpacts);
-    ofctrl_add_flow(OFTABLE_DROP_LOOPBACK, 0, &match, &ofpacts, hc_uuid);
+    ofctrl_add_flow(OFTABLE_CHECK_LOOPBACK, 0, &match, &ofpacts, hc_uuid);
+
+    /* Table 64, Priority 0.
+     * =======================
+     *
+     * Resubmit packets that do not have the MLF_ALLOW_LOOPBACK flag set
+     * to table 65 for logical-to-physical translation. */
+    match_init_catchall(&match);
+    ofpbuf_clear(&ofpacts);
+    put_resubmit(OFTABLE_LOG_TO_PHY, &ofpacts);
+    ofctrl_add_flow(OFTABLE_SAVE_INPORT, 0, &match, &ofpacts, hc_uuid);
 
     ofpbuf_uninit(&ofpacts);
 
diff --git a/ovn/lib/expr.c b/ovn/lib/expr.c
index 288aae2..1649c05 100644
--- a/ovn/lib/expr.c
+++ b/ovn/lib/expr.c
@@ -2872,16 +2872,6 @@  parse_assignment(struct lexer *lexer, struct expr_field *dst,
             bitwise_put(port, &sf->value,
                         sf->field->n_bytes, 0, sf->field->n_bits);
             bitwise_one(&sf->mask, sf->field->n_bytes, 0, sf->field->n_bits);
-
-            /* If the logical input port is being zeroed, clear the OpenFlow
-             * ingress port also, to allow a packet to be sent back to its
-             * origin. */
-            if (!port && sf->field->id == MFF_LOG_INPORT) {
-                sf = ofpact_put_SET_FIELD(ofpacts);
-                sf->field = mf_from_id(MFF_IN_PORT);
-                bitwise_one(&sf->mask,
-                            sf->field->n_bytes, 0, sf->field->n_bits);
-            }
         }
 
     exit_destroy_cs:
diff --git a/ovn/lib/logical-fields.h b/ovn/lib/logical-fields.h
index 24c15c7..94b3995 100644
--- a/ovn/lib/logical-fields.h
+++ b/ovn/lib/logical-fields.h
@@ -18,11 +18,21 @@ 
 
 #include "openvswitch/meta-flow.h"
 
+enum {
+    MLF_ALLOW_LOOPBACK_BIT = 0
+};
+
+enum {
+    MLF_ALLOW_LOOPBACK = (1 << MLF_ALLOW_LOOPBACK_BIT) /* Allow outputting
+                                                          back to inport. */
+};
+
 /* Logical fields.
  *
  * These values are documented in ovn-architecture(7), please update the
  * documentation if you change any of them. */
 #define MFF_LOG_DATAPATH MFF_METADATA /* Logical datapath (64 bits). */
+#define MFF_LOG_FLAGS      MFF_REG10  /* One of MLF_* (32 bits). */
 #define MFF_LOG_DNAT_ZONE  MFF_REG11  /* conntrack dnat zone for gateway router
                                        * (32 bits). */
 #define MFF_LOG_SNAT_ZONE  MFF_REG12  /* conntrack snat zone for gateway router
diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
index 6d9b6ca..790ca7e 100644
--- a/ovn/northd/ovn-northd.8.xml
+++ b/ovn/northd/ovn-northd.8.xml
@@ -441,7 +441,7 @@  arp.sha = <var>E</var>;
 arp.tpa = arp.spa;
 arp.spa = <var>A</var>;
 outport = inport;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 output;
         </pre>
 
@@ -467,7 +467,7 @@  nd_na {
     nd.target = <var>A</var>;
     nd.tll = <var>E</var>;
     outport = inport;
-    inport = ""; /* Allow sending out inport. */
+    flags.loopback = 1;
     output;
 };
         </pre>
@@ -546,7 +546,7 @@  ip4.src = <var>S</var>;
 udp.src = 67;
 udp.dst = 68;
 outport = <var>P</var>;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 output;
         </pre>
 
@@ -772,7 +772,7 @@  output;
 ip4.dst &lt;-&gt; ip4.src;
 ip.ttl = 255;
 icmp4.type = 0;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 next;
         </pre>
 
@@ -784,7 +784,7 @@  next;
 ip6.dst &lt;-&gt; ip6.src;
 ip.ttl = 255;
 icmp6.type = 129;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 next;
         </pre>
       </li>
@@ -812,7 +812,7 @@  arp.sha = <var>E</var>;
 arp.tpa = arp.spa;
 arp.spa = <var>A</var>;
 outport = <var>P</var>;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 output;
         </pre>
       </li>
@@ -837,7 +837,7 @@  arp.sha = <var>E</var>;
 arp.tpa = arp.spa;
 arp.spa = <var>A</var>;
 outport = <var>P</var>;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 output;
         </pre>
       </li>
@@ -872,7 +872,7 @@  nd_na {
     nd.target = <var>A</var>;
     nd.tll = <var>E</var>;
     outport = inport;
-    inport = \"\"; /* Allow sending out inport. */
+    flags.loopback = 1;
     output;
 };
         </pre>
@@ -1032,13 +1032,13 @@  icmp4 {
           For each configuration in the OVN Northbound database, that asks
           to change the destination IP address of a packet from <var>A</var> to
           <var>B</var>, a priority-100 flow matches <code>ip &amp;&amp;
-          ip4.dst == <var>A</var></code> with an action <code>inport = "";
-          ct_dnat(<var>B</var>);</code>.
+          ip4.dst == <var>A</var></code> with an action
+          <code>flags.loopback = 1; ct_dnat(<var>B</var>);</code>.
         </p>
 
         <p>
           For all IP packets of a Gateway router, a priority-50 flow with an
-          action <code>inport = ""; ct_dnat;</code>.
+          action <code>flags.loopback = 1; ct_dnat;</code>.
         </p>
 
         <p>
@@ -1084,7 +1084,7 @@  reg0 = <var>G</var>;
 reg1 = <var>A</var>;
 eth.src = <var>E</var>;
 outport = <var>P</var>;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 next;
         </pre>
 
@@ -1117,7 +1117,7 @@  xxreg0 = <var>G</var>;
 xxreg1 = <var>A</var>;
 eth.src = <var>E</var>;
 outport = <var>P</var>;
-inport = ""; /* Allow sending out inport. */
+flags.loopback = 1;
 next;
         </pre>
 
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 1fe73bf..4e0e072 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -1802,8 +1802,8 @@  build_dhcpv4_action(struct ovn_port *op, ovs_be32 offer_ip,
 
     ds_put_format(response_action, "eth.dst = eth.src; eth.src = %s; "
                   "ip4.dst = "IP_FMT"; ip4.src = %s; udp.src = 67; "
-                  "udp.dst = 68; outport = inport; inport = \"\";"
-                  " /* Allow sending out inport. */ output;",
+                  "udp.dst = 68; outport = inport; flags.loopback = 1; "
+                  "output;",
                   server_mac, IP_ARGS(offer_ip), server_ip);
 
     smap_destroy(&dhcpv4_options);
@@ -2470,7 +2470,7 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                     "arp.tpa = arp.spa; "
                     "arp.spa = %s; "
                     "outport = inport; "
-                    "inport = \"\"; /* Allow sending out inport. */ "
+                    "flags.loopback = 1; "
                     "output;",
                     op->lsp_addrs[i].ea_s, op->lsp_addrs[i].ea_s,
                     op->lsp_addrs[i].ipv4_addrs[j].addr_s);
@@ -2497,7 +2497,7 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                         "nd.target = %s; "
                         "nd.tll = %s; "
                         "outport = inport; "
-                        "inport = \"\"; /* Allow sending out inport. */ "
+                        "flags.loopback = 1; "
                         "output; "
                         "};",
                         op->lsp_addrs[i].ea_s,
@@ -2793,7 +2793,7 @@  add_route(struct hmap *lflows, const struct ovn_port *op,
                   "%sreg1 = %s; "
                   "eth.src = %s; "
                   "outport = %s; "
-                  "inport = \"\"; /* Allow sending out inport. */ "
+                  "flags.loopback = 1; "
                   "next;",
                   is_ipv4 ? "" : "xx",
                   lrp_addr_s,
@@ -3074,7 +3074,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 "ip4.dst <-> ip4.src; "
                 "ip.ttl = 255; "
                 "icmp4.type = 0; "
-                "inport = \"\"; /* Allow sending out inport. */ "
+                "flags.loopback = 1; "
                 "next; ");
             ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
                           ds_cstr(&match), ds_cstr(&actions));
@@ -3098,7 +3098,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 "arp.tpa = arp.spa; "
                 "arp.spa = %s; "
                 "outport = %s; "
-                "inport = \"\"; /* Allow sending out inport. */ "
+                "flags.loopback = 1; "
                 "output;",
                 op->lrp_networks.ea_s,
                 op->lrp_networks.ea_s,
@@ -3147,7 +3147,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 "arp.tpa = arp.spa; "
                 "arp.spa = "IP_FMT"; "
                 "outport = %s; "
-                "inport = \"\"; /* Allow sending out inport. */ "
+                "flags.loopback = 1; "
                 "output;",
                 op->lrp_networks.ea_s,
                 op->lrp_networks.ea_s,
@@ -3216,7 +3216,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                         "ip6.dst <-> ip6.src; "
                         "ip.ttl = 255; "
                         "icmp6.type = 129; "
-                        "inport = \"\"; /* Allow sending out inport. */ "
+                        "flags.loopback = 1; "
                         "next; ");
             ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
                           ds_cstr(&match), ds_cstr(&actions));
@@ -3249,7 +3249,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                           "nd.target = %s; "
                           "nd.tll = %s; "
                           "outport = inport; "
-                          "inport = \"\"; /* Allow sending out inport. */ "
+                          "flags.loopback = 1; "
                           "output; "
                           "};",
                           op->lrp_networks.ea_s,
@@ -3345,7 +3345,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 ds_clear(&match);
                 ds_put_format(&match, "ip && ip4.dst == %s", nat->external_ip);
                 ds_clear(&actions);
-                ds_put_format(&actions,"inport = \"\"; ct_dnat(%s);",
+                ds_put_format(&actions,"flags.loopback = 1; ct_dnat(%s);",
                               nat->logical_ip);
                 ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 100,
                               ds_cstr(&match), ds_cstr(&actions));
@@ -3385,7 +3385,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         * back the new destination IP address that is needed for
         * routing in the openflow pipeline. */
         ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 50,
-                      "ip", "inport = \"\"; ct_dnat;");
+                      "ip", "flags.loopback = 1; ct_dnat;");
     }
 
     /* Logical router ingress table 4: IP Routing.
diff --git a/ovn/ovn-architecture.7.xml b/ovn/ovn-architecture.7.xml
index 3529c99..a9258e7 100644
--- a/ovn/ovn-architecture.7.xml
+++ b/ovn/ovn-architecture.7.xml
@@ -768,6 +768,15 @@ 
       Open vSwitch extension register number 12.
     </dd>
 
+    <dt>logical flow flags</dt>
+    <dd>
+      A field used to store flags to be used when transforming logical
+        <!-- Keep the following in sync with MFF_LOG_FLAGS in
+             ovn/lib/logical-fields.h. -->
+      flows into physical flows.  OVN stores this in Open vSwitch
+      extension register number 10.
+    </dd>
+
     <dt>VLAN ID</dt>
     <dd>
       The VLAN ID is used as an interface between OVN and containers nested
@@ -851,10 +860,10 @@ 
         <dd>
           <p>
             Implemented by storing arguments into OpenFlow fields, then
-            resubmitting to table 65, which <code>ovn-controller</code>
+            resubmitting to table 66, which <code>ovn-controller</code>
             populates with flows generated from the <code>MAC_Binding</code>
             table in the OVN Southbound database.  If there is a match in table
-            65, then its actions store the bound MAC in the Ethernet
+            66, then its actions store the bound MAC in the Ethernet
             destination address field.
           </p>
 
@@ -887,8 +896,8 @@ 
         OpenFlow tables 32 through 47 implement the <code>output</code> action
         in the logical ingress pipeline.  Specifically, table 32 handles
         packets to remote hypervisors, table 33 handles packets to the local
-        hypervisor, and table 34 discards packets whose logical ingress and
-        egress port are the same.
+        hypervisor, and table 34 checks whether packets whose logical ingress
+        and egress port are the same should be discarded.
       </p>
 
       <p>
@@ -941,7 +950,8 @@ 
 
       <p>
         Table 34 matches and drops packets for which the logical input and
-        output ports are the same.  It resubmits other packets to table 48.
+        output ports are the same and the MLF_ALLOW_LOOPBACK flag is not
+        set.  It resubmits other packets to table 48.
       </p>
     </li>
 
@@ -965,7 +975,19 @@ 
 
     <li>
       <p>
-        OpenFlow table 64 performs logical-to-physical translation, the
+        OpenFlow table 64 is responsible for saving the ingress logical
+        port when the MLF_ALLOW_LOOPBACK flag is set.  If this is not
+        done, Open vSwitch will not forward the packet when the egress
+        port is the same as the ingress port.  A flow is added that
+        matches on the "allow loopback" flag with actions that push the
+        ingress logical port onto the stack, resubmit to table 65 for
+        logical-to-physical transformation, and then pop the logical
+        ingress port back from the stack.  If the flag is not set, the
+        flow simply resubmits to table 65.
+      </p>
+
+      <p>
+        OpenFlow table 65 performs logical-to-physical translation, the
         opposite of table 0.  It matches the packet's logical egress port.  Its
         actions output the packet to the port attached to the OVN integration
         bridge that represents that logical port.  If the logical egress port
@@ -974,7 +996,7 @@ 
       </p>
 
       <p>
-        If the logical egress port is a logical patch port, then table 64
+        If the logical egress port is a logical patch port, then table 65
         outputs to an OVS patch port that represents the logical patch port.
         The packet re-enters the OpenFlow flow table from the OVS patch port's
         peer in table 0, which identifies the logical datapath and logical
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 792f495..d134e74 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -375,13 +375,13 @@ 
       depends on the current value of the <code>outport</code> field.  Suppose
       <code>outport</code> designates a logical port.  First, OVN compares
       <code>inport</code> to <code>outport</code>; if they are equal, it treats
-      the <code>output</code> as a no-op.  In the common case, where they are
-      different, the packet enters the egress pipeline.  This transition to the
-      egress pipeline discards register data, e.g. <code>reg0</code> ...
-      <code>reg9</code> and connection tracking state, to achieve
-      uniform behavior regardless of whether the egress pipeline is on a
-      different hypervisor (because registers aren't preserve across
-      tunnel encapsulation).
+      the <code>output</code> as a no-op by default.  In the common
+      case, where they are different, the packet enters the egress
+      pipeline.  This transition to the egress pipeline discards
+      register data, e.g. <code>reg0</code> ...  <code>reg9</code> and
+      connection tracking state, to achieve uniform behavior regardless
+      of whether the egress pipeline is on a different hypervisor
+      (because registers aren't preserve across tunnel encapsulation).
     </p>
 
     <p>
@@ -787,6 +787,7 @@ 
         <li><code>reg0</code>...<code>reg9</code></li>
         <li><code>xxreg0</code> <code>xxreg1</code></li>
         <li><code>inport</code> <code>outport</code></li>
+        <li><code>flags.loopback</code></li>
         <li><code>eth.src</code> <code>eth.dst</code> <code>eth.type</code></li>
         <li><code>vlan.tci</code> <code>vlan.vid</code> <code>vlan.pcp</code> <code>vlan.present</code></li>
         <li><code>ip.proto</code> <code>ip.dscp</code> <code>ip.ecn</code> <code>ip.ttl</code> <code>ip.frag</code></li>
@@ -883,13 +884,13 @@ 
           </p>
 
           <p>
-            Output to the input port is implicitly dropped, that is,
-            <code>output</code> becomes a no-op if <code>outport</code> ==
-            <code>inport</code>.  Occasionally it may be useful to override
-            this behavior, e.g. to send an ARP reply to an ARP request; to do
-            so, use <code>inport = "";</code> to set the logical input port to
-            an empty string (which should not be used as the name of any
-            logical port).
+            By default, output to the input port is implicitly dropped,
+            that is, <code>output</code> becomes a no-op if
+            <code>outport</code> == <code>inport</code>.  Occasionally
+            it may be useful to override this behavior, e.g. to send an
+            ARP reply to an ARP request; to do so, use
+            <code>flags.loopback = 1</code> to allow the packet to
+            "hair-pin" back to the input port.
           </p>
         </dd>
 
diff --git a/tests/ovn.at b/tests/ovn.at
index ab42c75..72d63f2 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -568,7 +568,7 @@  tcp.dst=80; => actions=set_field:80->tcp_dst, prereqs=ip.proto == 0x6 && (eth.ty
 eth.dst[40] = 1; => actions=set_field:01:00:00:00:00:00/01:00:00:00:00:00->eth_dst, prereqs=1
 vlan.pcp = 2; => actions=set_field:0x4000/0xe000->vlan_tci, prereqs=vlan.tci[12]
 vlan.tci[13..15] = 2; => actions=set_field:0x4000/0xe000->vlan_tci, prereqs=1
-inport = ""; => actions=set_field:0->reg14,set_field:0->in_port, prereqs=1
+inport = ""; => actions=set_field:0->reg14, prereqs=1
 ip.ttl = 4; => actions=set_field:4->nw_ttl, prereqs=eth.type == 0x800 || eth.type == 0x86dd
 outport="eth0"; next; outport="LOCAL"; next; => actions=set_field:0x5->reg15,resubmit(,27),set_field:0xfffe->reg15,resubmit(,27), prereqs=1
 
@@ -697,7 +697,7 @@  reg1[0] = put_dhcp_opts(offerip="xyzzy"); => DHCP option offerip requires numeri
 reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4); => DHCP option domain requires string value.
 
 # nd_na
-nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; /* Allow sending out inport. */ output; }; => actions=controller(userdata=00.00.00.03.00.00.00.00.00.19.00.10.80.00.08.06.12.34.56.78.9a.bc.00.00.00.19.00.10.80.00.42.06.12.34.56.78.9a.bc.00.00.ff.ff.00.18.00.00.23.20.00.06.00.20.00.00.00.00.00.01.1c.04.00.01.1e.04.00.19.00.10.00.01.1c.04.00.00.00.00.00.00.00.00.00.19.00.10.00.00.00.02.00.00.00.00.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=nd_ns
+nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; /* Allow sending out inport. */ output; }; => actions=controller(userdata=00.00.00.03.00.00.00.00.00.19.00.10.80.00.08.06.12.34.56.78.9a.bc.00.00.00.19.00.10.80.00.42.06.12.34.56.78.9a.bc.00.00.ff.ff.00.18.00.00.23.20.00.06.00.20.00.00.00.00.00.01.1c.04.00.01.1e.04.00.19.00.10.00.01.1c.04.00.00.00.00.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=nd_ns
 
 # Contradictionary prerequisites (allowed but not useful):
 ip4.src = ip6.src[0..31]; => actions=move:NXM_NX_IPV6_SRC[0..31]->NXM_OF_IP_SRC[], prereqs=eth.type == 0x800 && eth.type == 0x86dd