packets: Define RSO flags.
[cascardo/ovs.git] / ovn / controller / pinctrl.c
index e440a88..62f4748 100644 (file)
 #include "pinctrl.h"
 
 #include "coverage.h"
+#include "csum.h"
 #include "dirs.h"
 #include "dp-packet.h"
 #include "flow.h"
 #include "lport.h"
-#include "ofp-actions.h"
-#include "ovn/lib/actions.h"
-#include "ovn/lib/logical-fields.h"
-#include "ofp-msgs.h"
-#include "ofp-print.h"
+#include "nx-match.h"
+#include "ovn-controller.h"
+#include "lib/packets.h"
+#include "lib/sset.h"
+#include "openvswitch/ofp-actions.h"
+#include "openvswitch/ofp-msgs.h"
+#include "openvswitch/ofp-print.h"
 #include "openvswitch/ofp-util.h"
-#include "ovn/lib/actions.h"
-#include "rconn.h"
 #include "openvswitch/vlog.h"
+
+#include "lib/dhcp.h"
 #include "ovn-controller.h"
+#include "ovn/lib/actions.h"
+#include "ovn/lib/logical-fields.h"
+#include "ovn/lib/ovn-util.h"
 #include "poll-loop.h"
+#include "rconn.h"
 #include "socket-util.h"
 #include "timeval.h"
 #include "vswitch-idl.h"
@@ -55,6 +62,19 @@ static void run_put_arps(struct controller_ctx *,
 static void wait_put_arps(struct controller_ctx *);
 static void flush_put_arps(void);
 
+static void init_send_garps(void);
+static void destroy_send_garps(void);
+static void send_garp_wait(void);
+static void send_garp_run(const struct ovsrec_bridge *,
+                          const char *chassis_id,
+                          const struct lport_index *lports,
+                          struct hmap *local_datapaths);
+static void pinctrl_handle_na(const struct flow *ip_flow,
+                              const struct match *md,
+                              struct ofpbuf *userdata);
+static void reload_metadata(struct ofpbuf *ofpacts,
+                            const struct match *md);
+
 COVERAGE_DEFINE(pinctrl_drop_put_arp);
 
 void
@@ -63,6 +83,7 @@ pinctrl_init(void)
     swconn = rconn_create(5, 0, DSCP_DEFAULT, 1 << OFP13_VERSION);
     conn_seq_no = 0;
     init_put_arps();
+    init_send_garps();
 }
 
 static ovs_be32
@@ -142,33 +163,8 @@ pinctrl_handle_arp(const struct flow *ip_flow, const struct match *md,
     struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
     enum ofp_version version = rconn_get_version(swconn);
 
-    enum mf_field_id md_fields[] = {
-#if FLOW_N_REGS == 8
-        MFF_REG0,
-        MFF_REG1,
-        MFF_REG2,
-        MFF_REG3,
-        MFF_REG4,
-        MFF_REG5,
-        MFF_REG6,
-        MFF_REG7,
-#else
-#error
-#endif
-        MFF_METADATA,
-    };
-    for (size_t i = 0; i < ARRAY_SIZE(md_fields); i++) {
-        const struct mf_field *field = mf_from_id(md_fields[i]);
-        if (!mf_is_all_wild(field, &md->wc)) {
-            struct ofpact_set_field *sf = ofpact_put_SET_FIELD(&ofpacts);
-            sf->field = field;
-            sf->flow_has_vlan = false;
-            mf_get_value(field, &md->flow, &sf->value);
-            memset(&sf->mask, 0xff, field->n_bytes);
-        }
-    }
-    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
-                                                      version, &ofpacts);
+    reload_metadata(&ofpacts, md);
+    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size, version, &ofpacts);
     if (error) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
         VLOG_WARN_RL(&rl, "failed to parse arp actions (%s)",
@@ -192,14 +188,192 @@ exit:
     ofpbuf_uninit(&ofpacts);
 }
 
+static void
+pinctrl_handle_put_dhcp_opts(
+    struct dp_packet *pkt_in, struct ofputil_packet_in *pin,
+    struct ofpbuf *userdata, struct ofpbuf *continuation)
+{
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    struct dp_packet *pkt_out_ptr = NULL;
+    uint32_t success = 0;
+
+    /* Parse result field. */
+    const struct mf_field *f;
+    enum ofperr ofperr = nx_pull_header(userdata, &f, NULL);
+    if (ofperr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "bad result OXM (%s)", ofperr_to_string(ofperr));
+        goto exit;
+    }
+
+    /* Parse result offset and offer IP. */
+    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
+    ovs_be32 *offer_ip = ofpbuf_try_pull(userdata, sizeof *offer_ip);
+    if (!ofsp || !offer_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "offset or offer_ip not present in the userdata");
+        goto exit;
+    }
+
+    /* Check that the result is valid and writable. */
+    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
+    ofperr = mf_check_dst(&dst, NULL);
+    if (ofperr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "bad result bit (%s)", ofperr_to_string(ofperr));
+        goto exit;
+    }
+
+    if (!userdata->size) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP options not present in the userdata");
+        goto exit;
+    }
+
+    /* Validate the DHCP request packet.
+     * Format of the DHCP packet is
+     * ------------------------------------------------------------------------
+     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
+     * ------------------------------------------------------------------------
+     */
+    if (dp_packet_l4_size(pkt_in) < (UDP_HEADER_LEN +
+        sizeof (struct dhcp_header) + sizeof(uint32_t) + 3)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "Invalid or incomplete DHCP packet recieved");
+        goto exit;
+    }
+
+    struct dhcp_header const *in_dhcp_data = dp_packet_get_udp_payload(pkt_in);
+    if (in_dhcp_data->op != DHCP_OP_REQUEST) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "Invalid opcode in the DHCP packet : %d",
+                     in_dhcp_data->op);
+        goto exit;
+    }
+
+    /* DHCP options follow the DHCP header. The first 4 bytes of the DHCP
+     * options is the DHCP magic cookie followed by the actual DHCP options.
+     */
+    const uint8_t *in_dhcp_opt =
+        (const uint8_t *)dp_packet_get_udp_payload(pkt_in) +
+        sizeof (struct dhcp_header);
+
+    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
+    if (memcmp(in_dhcp_opt, &magic_cookie, sizeof(ovs_be32))) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP magic cookie not present in the DHCP packet");
+        goto exit;
+    }
+
+    in_dhcp_opt += 4;
+    /* Check that the DHCP Message Type (opt 53) is present or not with
+     * valid values - DHCP_MSG_DISCOVER or DHCP_MSG_REQUEST as the first
+     * DHCP option.
+     */
+    if (!(in_dhcp_opt[0] == DHCP_OPT_MSG_TYPE && in_dhcp_opt[1] == 1 && (
+            in_dhcp_opt[2] == DHCP_MSG_DISCOVER ||
+            in_dhcp_opt[2] == DHCP_MSG_REQUEST))) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "Invalid DHCP message type : opt code = %d,"
+                     " opt value = %d", in_dhcp_opt[0], in_dhcp_opt[2]);
+        goto exit;
+    }
+
+    uint8_t msg_type;
+    if (in_dhcp_opt[2] == DHCP_MSG_DISCOVER) {
+        msg_type = DHCP_MSG_OFFER;
+    } else {
+        msg_type = DHCP_MSG_ACK;
+    }
+
+    /* Frame the DHCP reply packet
+     * Total DHCP options length will be options stored in the userdata +
+     * 16 bytes.
+     *
+     * --------------------------------------------------------------
+     *| 4 Bytes (dhcp cookie) | 3 Bytes (option type) | DHCP options |
+     * --------------------------------------------------------------
+     *| 4 Bytes padding | 1 Byte (option end 0xFF ) | 4 Bytes padding|
+     * --------------------------------------------------------------
+     */
+    uint16_t new_l4_size = UDP_HEADER_LEN + DHCP_HEADER_LEN + \
+                           userdata->size + 16;
+    size_t new_packet_size = pkt_in->l4_ofs + new_l4_size;
+
+    struct dp_packet pkt_out;
+    dp_packet_init(&pkt_out, new_packet_size);
+    dp_packet_clear(&pkt_out);
+    dp_packet_prealloc_tailroom(&pkt_out, new_packet_size);
+    pkt_out_ptr = &pkt_out;
+
+    /* Copy the L2 and L3 headers from the pkt_in as they would remain same*/
+    dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, pkt_in->l4_ofs), pkt_in->l4_ofs);
+
+    pkt_out.l2_5_ofs = pkt_in->l2_5_ofs;
+    pkt_out.l2_pad_size = pkt_in->l2_pad_size;
+    pkt_out.l3_ofs = pkt_in->l3_ofs;
+    pkt_out.l4_ofs = pkt_in->l4_ofs;
+
+    struct udp_header *udp = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, UDP_HEADER_LEN), UDP_HEADER_LEN);
+
+    struct dhcp_header *dhcp_data = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, DHCP_HEADER_LEN), DHCP_HEADER_LEN);
+    dhcp_data->op = DHCP_OP_REPLY;
+    dhcp_data->yiaddr = *offer_ip;
+    dp_packet_put(&pkt_out, &magic_cookie, sizeof(ovs_be32));
+
+    uint8_t *out_dhcp_opts = dp_packet_put_zeros(&pkt_out,
+                                                 userdata->size + 12);
+    /* DHCP option - type */
+    out_dhcp_opts[0] = DHCP_OPT_MSG_TYPE;
+    out_dhcp_opts[1] = 1;
+    out_dhcp_opts[2] = msg_type;
+    out_dhcp_opts += 3;
+
+    memcpy(out_dhcp_opts, userdata->data, userdata->size);
+    out_dhcp_opts += userdata->size;
+    /* Padding */
+    out_dhcp_opts += 4;
+    /* End */
+    out_dhcp_opts[0] = DHCP_OPT_END;
+
+    udp->udp_len = htons(new_l4_size);
+
+    struct ip_header *out_ip = dp_packet_l3(&pkt_out);
+    out_ip->ip_tot_len = htons(pkt_out.l4_ofs - pkt_out.l3_ofs + new_l4_size);
+    udp->udp_csum = 0;
+    out_ip->ip_csum = 0;
+    out_ip->ip_csum = csum(out_ip, sizeof *out_ip);
+
+    pin->packet = dp_packet_data(&pkt_out);
+    pin->packet_len = dp_packet_size(&pkt_out);
+
+    success = 1;
+exit:
+    if (!ofperr) {
+        union mf_subvalue sv;
+        sv.u8_val = success;
+        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
+    }
+    queue_msg(ofputil_encode_resume(pin, continuation, proto));
+    if (pkt_out_ptr) {
+        dp_packet_uninit(pkt_out_ptr);
+    }
+}
+
 static void
 process_packet_in(const struct ofp_header *msg)
 {
     static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
 
     struct ofputil_packet_in pin;
+    struct ofpbuf continuation;
     enum ofperr error = ofputil_decode_packet_in(msg, true, &pin,
-                                                 NULL, NULL, NULL);
+                                                 NULL, NULL, &continuation);
+
     if (error) {
         VLOG_WARN_RL(&rl, "error decoding packet-in: %s",
                      ofperr_to_string(error));
@@ -231,6 +405,14 @@ process_packet_in(const struct ofp_header *msg)
         pinctrl_handle_put_arp(&pin.flow_metadata.flow, &headers);
         break;
 
+    case ACTION_OPCODE_PUT_DHCP_OPTS:
+        pinctrl_handle_put_dhcp_opts(&packet, &pin, &userdata, &continuation);
+        break;
+
+    case ACTION_OPCODE_NA:
+        pinctrl_handle_na(&headers, &pin.flow_metadata, &userdata);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -267,7 +449,9 @@ pinctrl_recv(const struct ofp_header *oh, enum ofptype type)
 
 void
 pinctrl_run(struct controller_ctx *ctx, const struct lport_index *lports,
-            const struct ovsrec_bridge *br_int)
+            const struct ovsrec_bridge *br_int,
+            const char *chassis_id,
+            struct hmap *local_datapaths)
 {
     if (br_int) {
         char *target;
@@ -308,6 +492,7 @@ pinctrl_run(struct controller_ctx *ctx, const struct lport_index *lports,
     }
 
     run_put_arps(ctx, lports);
+    send_garp_run(br_int, chassis_id, lports, local_datapaths);
 }
 
 void
@@ -316,6 +501,7 @@ pinctrl_wait(struct controller_ctx *ctx)
     wait_put_arps(ctx);
     rconn_run_wait(swconn);
     rconn_recv_wait(swconn);
+    send_garp_wait();
 }
 
 void
@@ -323,6 +509,7 @@ pinctrl_destroy(void)
 {
     rconn_destroy(swconn);
     destroy_put_arps();
+    destroy_send_garps();
 }
 \f
 /* Implementation of the "put_arp" OVN action.  This action sends a packet to
@@ -452,6 +639,7 @@ run_put_arp(struct controller_ctx *ctx, const struct lport_index *lports,
     sbrec_mac_binding_set_logical_port(b, pb->logical_port);
     sbrec_mac_binding_set_ip(b, ip_string);
     sbrec_mac_binding_set_mac(b, mac_string);
+    sbrec_mac_binding_set_datapath(b, pb->datapath);
 }
 
 static void
@@ -479,9 +667,343 @@ wait_put_arps(struct controller_ctx *ctx)
 static void
 flush_put_arps(void)
 {
-    struct put_arp *pa, *next;
-    HMAP_FOR_EACH_SAFE (pa, next, hmap_node, &put_arps) {
-        hmap_remove(&put_arps, &pa->hmap_node);
+    struct put_arp *pa;
+    HMAP_FOR_EACH_POP (pa, hmap_node, &put_arps) {
         free(pa);
     }
 }
+\f
+/*
+ * Send gratuitous ARP for vif on localnet.
+ *
+ * When a new vif on localnet is added, gratuitous ARPs are sent announcing
+ * the port's mac,ip mapping.  On localnet, such announcements are needed for
+ * switches and routers on the broadcast segment to update their port-mac
+ * and ARP tables.
+ */
+struct garp_data {
+    struct eth_addr ea;          /* Ethernet address of port. */
+    ovs_be32 ipv4;               /* Ipv4 address of port. */
+    long long int announce_time; /* Next announcement in ms. */
+    int backoff;                 /* Backoff for the next announcement. */
+    ofp_port_t ofport;           /* ofport used to output this GARP. */
+};
+
+/* Contains GARPs to be sent. */
+static struct shash send_garp_data;
+
+/* Next GARP announcement in ms. */
+static long long int send_garp_time;
+
+static void
+init_send_garps(void)
+{
+    shash_init(&send_garp_data);
+    send_garp_time = LLONG_MAX;
+}
+
+static void
+destroy_send_garps(void)
+{
+    shash_destroy_free_data(&send_garp_data);
+}
+
+/* Add or update a vif for which GARPs need to be announced. */
+static void
+send_garp_update(const struct sbrec_port_binding *binding_rec,
+                 struct simap *localnet_ofports, struct hmap *local_datapaths)
+{
+    /* Find the localnet ofport to send this GARP. */
+    struct local_datapath *ld
+        = get_local_datapath(local_datapaths,
+                             binding_rec->datapath->tunnel_key);
+    if (!ld || !ld->localnet_port) {
+        return;
+    }
+    ofp_port_t ofport = u16_to_ofp(simap_get(localnet_ofports,
+                                             ld->localnet_port->logical_port));
+
+    /* Update GARP if it exists. */
+    struct garp_data *garp = shash_find_data(&send_garp_data,
+                                             binding_rec->logical_port);
+    if (garp) {
+        garp->ofport = ofport;
+        return;
+    }
+
+    /* Add GARP for new vif. */
+    int i;
+    for (i = 0; i < binding_rec->n_mac; i++) {
+        struct lport_addresses laddrs;
+        if (!extract_lsp_addresses(binding_rec->mac[i], &laddrs)
+            || !laddrs.n_ipv4_addrs) {
+            continue;
+        }
+
+        struct garp_data *garp = xmalloc(sizeof *garp);
+        garp->ea = laddrs.ea;
+        garp->ipv4 = laddrs.ipv4_addrs[0].addr;
+        garp->announce_time = time_msec() + 1000;
+        garp->backoff = 1;
+        garp->ofport = ofport;
+        shash_add(&send_garp_data, binding_rec->logical_port, garp);
+
+        destroy_lport_addresses(&laddrs);
+        break;
+    }
+}
+
+/* Remove a vif from GARP announcements. */
+static void
+send_garp_delete(const char *lport)
+{
+    struct garp_data *garp = shash_find_and_delete(&send_garp_data, lport);
+    free(garp);
+}
+
+static long long int
+send_garp(struct garp_data *garp, long long int current_time)
+{
+    if (current_time < garp->announce_time) {
+        return garp->announce_time;
+    }
+
+    /* Compose a GARP request packet. */
+    uint64_t packet_stub[128 / 8];
+    struct dp_packet packet;
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
+    compose_arp(&packet, ARP_OP_REQUEST, garp->ea, eth_addr_zero,
+                true, garp->ipv4, garp->ipv4);
+
+    /* Compose actions.  The garp request is output on localnet ofport. */
+    uint64_t ofpacts_stub[4096 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+    enum ofp_version version = rconn_get_version(swconn);
+    ofpact_put_OUTPUT(&ofpacts)->port = garp->ofport;
+
+    struct ofputil_packet_out po = {
+        .packet = dp_packet_data(&packet),
+        .packet_len = dp_packet_size(&packet),
+        .buffer_id = UINT32_MAX,
+        .in_port = OFPP_CONTROLLER,
+        .ofpacts = ofpacts.data,
+        .ofpacts_len = ofpacts.size,
+    };
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    queue_msg(ofputil_encode_packet_out(&po, proto));
+    dp_packet_uninit(&packet);
+    ofpbuf_uninit(&ofpacts);
+
+    /* Set the next announcement.  At most 5 announcements are sent for a
+     * vif. */
+    if (garp->backoff < 16) {
+        garp->backoff *= 2;
+        garp->announce_time = current_time + garp->backoff * 1000;
+    } else {
+        garp->announce_time = LLONG_MAX;
+    }
+    return garp->announce_time;
+}
+
+/* Get localnet vifs, and ofport for localnet patch ports. */
+static void
+get_localnet_vifs(const struct ovsrec_bridge *br_int,
+                  const char *this_chassis_id,
+                  const struct lport_index *lports,
+                  struct hmap *local_datapaths,
+                  struct sset *localnet_vifs,
+                  struct simap *localnet_ofports)
+{
+    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)) {
+            continue;
+        }
+        const char *chassis_id = smap_get(&port_rec->external_ids,
+                                          "ovn-chassis-id");
+        if (chassis_id && !strcmp(chassis_id, this_chassis_id)) {
+            continue;
+        }
+        const char *localnet = smap_get(&port_rec->external_ids,
+                                        "ovn-localnet-port");
+        for (int j = 0; j < port_rec->n_interfaces; j++) {
+            const struct ovsrec_interface *iface_rec = port_rec->interfaces[j];
+            if (!iface_rec->n_ofport) {
+                continue;
+            }
+            if (localnet) {
+                int64_t ofport = iface_rec->ofport[0];
+                if (ofport < 1 || ofport > ofp_to_u16(OFPP_MAX)) {
+                    continue;
+                }
+                simap_put(localnet_ofports, localnet, ofport);
+                continue;
+            }
+            const char *iface_id = smap_get(&iface_rec->external_ids,
+                                            "iface-id");
+            if (!iface_id) {
+                continue;
+            }
+            const struct sbrec_port_binding *pb
+                = lport_lookup_by_name(lports, iface_id);
+            if (!pb) {
+                continue;
+            }
+            struct local_datapath *ld
+                = get_local_datapath(local_datapaths,
+                                     pb->datapath->tunnel_key);
+            if (ld && ld->localnet_port) {
+                sset_add(localnet_vifs, iface_id);
+            }
+        }
+    }
+}
+
+static void
+send_garp_wait(void)
+{
+    poll_timer_wait_until(send_garp_time);
+}
+
+static void
+send_garp_run(const struct ovsrec_bridge *br_int, const char *chassis_id,
+              const struct lport_index *lports,
+              struct hmap *local_datapaths)
+{
+    struct sset localnet_vifs = SSET_INITIALIZER(&localnet_vifs);
+    struct simap localnet_ofports = SIMAP_INITIALIZER(&localnet_ofports);
+
+    get_localnet_vifs(br_int, chassis_id, lports, local_datapaths,
+                      &localnet_vifs, &localnet_ofports);
+
+    /* For deleted ports, remove from send_garp_data. */
+    struct shash_node *iter, *next;
+    SHASH_FOR_EACH_SAFE (iter, next, &send_garp_data) {
+        if (!sset_contains(&localnet_vifs, iter->name)) {
+            send_garp_delete(iter->name);
+        }
+    }
+
+    /* Update send_garp_data. */
+    const char *iface_id;
+    SSET_FOR_EACH (iface_id, &localnet_vifs) {
+        const struct sbrec_port_binding *pb = lport_lookup_by_name(lports,
+                                                                   iface_id);
+        if (pb) {
+            send_garp_update(pb, &localnet_ofports, local_datapaths);
+        }
+    }
+
+    /* Send GARPs, and update the next announcement. */
+    long long int current_time = time_msec();
+    send_garp_time = LLONG_MAX;
+    SHASH_FOR_EACH (iter, &send_garp_data) {
+        long long int next_announce = send_garp(iter->data, current_time);
+        if (send_garp_time > next_announce) {
+            send_garp_time = next_announce;
+        }
+    }
+    sset_destroy(&localnet_vifs);
+    simap_destroy(&localnet_ofports);
+}
+
+static void
+reload_metadata(struct ofpbuf *ofpacts, const struct match *md)
+{
+    enum mf_field_id md_fields[] = {
+#if FLOW_N_REGS == 16
+        MFF_REG0,
+        MFF_REG1,
+        MFF_REG2,
+        MFF_REG3,
+        MFF_REG4,
+        MFF_REG5,
+        MFF_REG6,
+        MFF_REG7,
+        MFF_REG8,
+        MFF_REG9,
+        MFF_REG10,
+        MFF_REG11,
+        MFF_REG12,
+        MFF_REG13,
+        MFF_REG14,
+        MFF_REG15,
+#else
+#error
+#endif
+        MFF_METADATA,
+    };
+    for (size_t i = 0; i < ARRAY_SIZE(md_fields); i++) {
+        const struct mf_field *field = mf_from_id(md_fields[i]);
+        if (!mf_is_all_wild(field, &md->wc)) {
+            struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+            sf->field = field;
+            sf->flow_has_vlan = false;
+            mf_get_value(field, &md->flow, &sf->value);
+            memset(&sf->mask, 0xff, field->n_bytes);
+        }
+    }
+}
+
+static void
+pinctrl_handle_na(const struct flow *ip_flow,
+                  const struct match *md,
+                  struct ofpbuf *userdata)
+{
+    /* This action only works for IPv6 ND packets, and the switch should only
+     * send us ND packets this way, but check here just to be sure. */
+    if (!is_nd(ip_flow, NULL)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "NA action on non-ND packet");
+        return;
+    }
+
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+
+    uint64_t packet_stub[128 / 8];
+    struct dp_packet packet;
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
+
+    ovs_be32 ipv6_src[4], ipv6_dst[4];
+    memcpy(ipv6_dst, &ip_flow->ipv6_src, sizeof ipv6_src);
+    memcpy(ipv6_src, &ip_flow->nd_target, sizeof ipv6_dst);
+
+    /* xxx These flags are not exactly correct.  Look at section 7.2.4
+     * xxx of RFC 4861.  For example, we need to set ND_RSO_ROUTER for
+     * xxx router's interfaces and ND_RSO_SOLICITED only if it was
+     * xxx requested. */
+    compose_na(&packet,
+               ip_flow->dl_dst, ip_flow->dl_src,
+               ipv6_src, ipv6_dst,
+               htonl(ND_RSO_SOLICITED | ND_RSO_OVERRIDE));
+
+    /* Reload previous packet metadata. */
+    uint64_t ofpacts_stub[4096 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+    reload_metadata(&ofpacts, md);
+
+    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
+                                                      version, &ofpacts);
+    if (error) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "failed to parse actions for 'na' (%s)",
+                     ofperr_to_string(error));
+        goto exit;
+    }
+
+    struct ofputil_packet_out po = {
+        .packet = dp_packet_data(&packet),
+        .packet_len = dp_packet_size(&packet),
+        .buffer_id = UINT32_MAX,
+        .in_port = OFPP_CONTROLLER,
+        .ofpacts = ofpacts.data,
+        .ofpacts_len = ofpacts.size,
+    };
+
+    queue_msg(ofputil_encode_packet_out(&po, proto));
+
+exit:
+    dp_packet_uninit(&packet);
+    ofpbuf_uninit(&ofpacts);
+}