diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index 10a7e18..5010579 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -591,16 +591,29 @@ consider_neighbor_flow(struct hmap *flow_table,
         return;
     }
 
-    ovs_be32 ip;
-    if (!ip_parse(b->ip, &ip)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "bad 'ip' %s", b->ip);
-        return;
+
+    if (strchr(b->ip, '.')) {
+        ovs_be32 ip;
+        if (!ip_parse(b->ip, &ip)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad 'ip' %s", b->ip);
+            return;
+        }
+        match_set_reg(match_p, 0, ntohl(ip));
+    } else {
+        struct in6_addr ip6;
+        if (!ipv6_parse(b->ip, &ip6)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad 'ip' %s", b->ip);
+            return;
+        }
+        ovs_be128 value;
+        memcpy(&value, &ip6, sizeof(value));
+        match_set_xxreg(match_p, 0, ntoh128(value));
     }
 
     match_set_metadata(match_p, htonll(pb->datapath->tunnel_key));
     match_set_reg(match_p, MFF_LOG_OUTPORT - MFF_REG0, pb->tunnel_key);
-    match_set_reg(match_p, 0, ntohl(ip));
 
     ofpbuf_clear(ofpacts_p);
     put_load(mac.ea, sizeof mac.ea, MFF_ETH_DST, 0, 48, ofpacts_p);
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index 77e5dc6..7a92e80 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -53,8 +53,9 @@ static struct rconn *swconn;
  * rconn_get_connection_seqno(rconn), 'swconn' has reconnected. */
 static unsigned int conn_seq_no;
 
-static void pinctrl_handle_put_arp(const struct flow *md,
-                                   const struct flow *headers);
+static void pinctrl_handle_put_mac_binding(const struct flow *md,
+                                           const struct flow *headers,
+                                           bool is_ipv4);
 static void init_put_arps(void);
 static void destroy_put_arps(void);
 static void run_put_arps(struct controller_ctx *,
@@ -402,7 +403,8 @@ process_packet_in(const struct ofp_header *msg)
         break;
 
     case ACTION_OPCODE_PUT_ARP:
-        pinctrl_handle_put_arp(&pin.flow_metadata.flow, &headers);
+        pinctrl_handle_put_mac_binding(&pin.flow_metadata.flow, &headers,
+                                       true);
         break;
 
     case ACTION_OPCODE_PUT_DHCP_OPTS:
@@ -413,6 +415,11 @@ process_packet_in(const struct ofp_header *msg)
         pinctrl_handle_nd_adv(&headers, &pin.flow_metadata, &userdata);
         break;
 
+    case ACTION_OPCODE_PUT_ND:
+        pinctrl_handle_put_mac_binding(&pin.flow_metadata.flow, &headers,
+                                       false);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -523,7 +530,7 @@ pinctrl_destroy(void)
  * and apply them whenever a database transaction is available. */
 
 /* Buffered "put_arp" operation. */
-struct put_arp {
+struct put_arp {    /* xxx Rename this to "mac_binding" or something. */
     struct hmap_node hmap_node; /* In 'put_arps'. */
 
     long long int timestamp;    /* In milliseconds. */
@@ -531,7 +538,7 @@ struct put_arp {
     /* Key. */
     uint32_t dp_key;
     uint32_t port_key;
-    ovs_be32 ip;
+    char *ip_s;
 
     /* Value. */
     struct eth_addr mac;
@@ -554,14 +561,14 @@ destroy_put_arps(void)
 }
 
 static struct put_arp *
-pinctrl_find_put_arp(uint32_t dp_key, uint32_t port_key, ovs_be32 ip,
+pinctrl_find_put_arp(uint32_t dp_key, uint32_t port_key, const char *ip_s,
                      uint32_t hash)
 {
     struct put_arp *pa;
     HMAP_FOR_EACH_WITH_HASH (pa, hmap_node, hash, &put_arps) {
         if (pa->dp_key == dp_key
             && pa->port_key == port_key
-            && pa->ip == ip) {
+            && !strcmp(pa->ip_s, ip_s)) {
             return pa;
         }
     }
@@ -569,13 +576,22 @@ pinctrl_find_put_arp(uint32_t dp_key, uint32_t port_key, ovs_be32 ip,
 }
 
 static void
-pinctrl_handle_put_arp(const struct flow *md, const struct flow *headers)
+pinctrl_handle_put_mac_binding(const struct flow *md,
+                               const struct flow *headers, bool is_ipv4)
 {
     uint32_t dp_key = ntohll(md->metadata);
     uint32_t port_key = md->regs[MFF_LOG_INPORT - MFF_REG0];
-    ovs_be32 ip = htonl(md->regs[0]);
-    uint32_t hash = hash_3words(dp_key, port_key, (OVS_FORCE uint32_t) ip);
-    struct put_arp *pa = pinctrl_find_put_arp(dp_key, port_key, ip, hash);
+    char ip_s[INET6_ADDRSTRLEN];
+
+    /* xxx Kinda hoakey argument. */
+    if (is_ipv4) {
+        inet_ntop(AF_INET, &md->regs[0], ip_s, sizeof(ip_s));
+    } else {
+        ovs_be128 ip6 = hton128(flow_get_xxreg(md, 0));
+        inet_ntop(AF_INET6, &ip6, ip_s, sizeof(ip_s));
+    }
+    uint32_t hash = hash_string(ip_s, (hash_2words(dp_key, port_key)));
+    struct put_arp *pa = pinctrl_find_put_arp(dp_key, port_key, ip_s, hash);
     if (!pa) {
         if (hmap_count(&put_arps) >= 1000) {
             COVERAGE_INC(pinctrl_drop_put_arp);
@@ -586,12 +602,14 @@ pinctrl_handle_put_arp(const struct flow *md, const struct flow *headers)
         hmap_insert(&put_arps, &pa->hmap_node, hash);
         pa->dp_key = dp_key;
         pa->port_key = port_key;
-        pa->ip = ip;
+        pa->ip_s = xstrdup(ip_s);
     }
     pa->timestamp = time_msec();
     pa->mac = headers->dl_src;
 }
 
+/* xxx All these functions should change from put_arp to bind_macs or
+ * xxx something. */
 static void
 run_put_arp(struct controller_ctx *ctx, const struct lport_index *lports,
             const struct put_arp *pa)
@@ -611,22 +629,19 @@ run_put_arp(struct controller_ctx *ctx, const struct lport_index *lports,
         return;
     }
 
-    /* Convert arguments to string form for database. */
-    char ip_string[INET_ADDRSTRLEN + 1];
-    snprintf(ip_string, sizeof ip_string, IP_FMT, IP_ARGS(pa->ip));
-
+    /* Convert ethernet argument to string form for database. */
     char mac_string[ETH_ADDR_STRLEN + 1];
     snprintf(mac_string, sizeof mac_string,
              ETH_ADDR_FMT, ETH_ADDR_ARGS(pa->mac));
 
-    /* Check for and update an existing IP-MAC binding for this logical
+    /* Check for an update an existing IP-MAC binding for this logical
      * port.
      *
      * XXX This is not very efficient. */
     const struct sbrec_mac_binding *b;
     SBREC_MAC_BINDING_FOR_EACH (b, ctx->ovnsb_idl) {
         if (!strcmp(b->logical_port, pb->logical_port)
-            && !strcmp(b->ip, ip_string)) {
+            && !strcmp(b->ip, pa->ip_s)) {
             if (strcmp(b->mac, mac_string)) {
                 sbrec_mac_binding_set_mac(b, mac_string);
             }
@@ -637,7 +652,7 @@ run_put_arp(struct controller_ctx *ctx, const struct lport_index *lports,
     /* Add new IP-MAC binding for this logical port. */
     b = sbrec_mac_binding_insert(ctx->ovnsb_idl_txn);
     sbrec_mac_binding_set_logical_port(b, pb->logical_port);
-    sbrec_mac_binding_set_ip(b, ip_string);
+    sbrec_mac_binding_set_ip(b, pa->ip_s);
     sbrec_mac_binding_set_mac(b, mac_string);
 }
 
@@ -668,6 +683,7 @@ flush_put_arps(void)
 {
     struct put_arp *pa;
     HMAP_FOR_EACH_POP (pa, hmap_node, &put_arps) {
+        free(pa->ip_s);
         free(pa);
     }
 }
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 57ca580..f65c877 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -776,6 +776,56 @@ parse_ct_lb_action(struct action_context *ctx)
 }
 
 static void
+parse_get_nd_action(struct action_context *ctx)
+{
+    struct mf_subfield port, ip6;
+
+    if (!action_force_match(ctx, LEX_T_LPAREN)
+        || !action_parse_field(ctx, 0, &port)
+        || !action_force_match(ctx, LEX_T_COMMA)
+        || !action_parse_field(ctx, 128, &ip6)
+        || !action_force_match(ctx, LEX_T_RPAREN)) {
+        return;
+    }
+
+    const struct arg args[] = {
+        { &port, MFF_LOG_OUTPORT },
+        { &ip6, MFF_XXREG0 },
+    };
+    setup_args(ctx, args, ARRAY_SIZE(args));
+
+    put_load(0, MFF_ETH_DST, 0, 48, ctx->ofpacts);
+    emit_resubmit(ctx, ctx->ap->arp_ptable);
+
+    restore_args(ctx, args, ARRAY_SIZE(args));
+}
+
+static void
+parse_put_nd_action(struct action_context *ctx)
+{
+    struct mf_subfield port, ip6, mac;
+
+    if (!action_force_match(ctx, LEX_T_LPAREN)
+        || !action_parse_field(ctx, 0, &port)
+        || !action_force_match(ctx, LEX_T_COMMA)
+        || !action_parse_field(ctx, 128, &ip6)
+        || !action_force_match(ctx, LEX_T_COMMA)
+        || !action_parse_field(ctx, 48, &mac)
+        || !action_force_match(ctx, LEX_T_RPAREN)) {
+        return;
+    }
+
+    const struct arg args[] = {
+        { &port, MFF_LOG_INPORT },
+        { &ip6, MFF_XXREG0 },
+        { &mac, MFF_ETH_SRC }
+    };
+    setup_args(ctx, args, ARRAY_SIZE(args));
+    put_controller_op(ctx->ofpacts, ACTION_OPCODE_PUT_ND);
+    restore_args(ctx, args, ARRAY_SIZE(args));
+}
+
+static void
 emit_ct(struct action_context *ctx, bool recirc_next, bool commit,
         int *ct_mark, int *ct_mark_mask,
         ovs_be128 *ct_label, ovs_be128 *ct_label_mask)
@@ -1063,12 +1113,16 @@ parse_action(struct action_context *ctx)
         parse_ct_lb_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "arp")) {
         parse_nested_action(ctx, ACTION_OPCODE_ARP, "ip4");
-    } else if (lexer_match_id(ctx->lexer, "nd_adv")) {
-        parse_nested_action(ctx, ACTION_OPCODE_ND_ADV, "nd_sol");
     } else if (lexer_match_id(ctx->lexer, "get_arp")) {
         parse_get_arp_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "put_arp")) {
         parse_put_arp_action(ctx);
+    } else if (lexer_match_id(ctx->lexer, "nd_adv")) {
+        parse_nested_action(ctx, ACTION_OPCODE_ND_ADV, "nd_sol");
+    } else if (lexer_match_id(ctx->lexer, "get_nd")) {
+        parse_get_nd_action(ctx);
+    } else if (lexer_match_id(ctx->lexer, "put_nd")) {
+        parse_put_nd_action(ctx);
     } else {
         action_syntax_error(ctx, "expecting action");
     }
diff --git a/ovn/lib/actions.h b/ovn/lib/actions.h
index 2268302..4f0a243 100644
--- a/ovn/lib/actions.h
+++ b/ovn/lib/actions.h
@@ -78,6 +78,16 @@ enum action_opcode {
      * The actions, in OpenFlow 1.3 format, follow the action_header.
      */
     ACTION_OPCODE_ND_ADV,
+
+    /* "put_nd(port, ip6, mac)"
+     *
+     * Arguments are passed through the packet metadata and data, as follows:
+     *
+     *     MFF_XXREG0 = ip6
+     *     MFF_LOG_INPORT = port
+     *     MFF_ETH_SRC = mac
+     */
+    ACTION_OPCODE_PUT_ND,
 };
 
 /* Header. */
@@ -128,7 +138,8 @@ struct action_params {
     uint8_t first_ptable;       /* First OpenFlow table. */
     uint8_t cur_ltable;         /* 0 <= cur_ltable < n_tables. */
     uint8_t output_ptable;      /* OpenFlow table for 'output' to resubmit. */
-    uint8_t arp_ptable;         /* OpenFlow table for 'get_arp' to resubmit. */
+    uint8_t arp_ptable;         /* OpenFlow table for 'get_arp'/'get_nd' to
+                                   resubmit. */
 };
 
 char *actions_parse(struct lexer *, const struct action_params *,
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 991018d..b4f5529 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -2362,6 +2362,14 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         /* Pass other traffic not already handled to the next table for
          * routing. */
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 0, "1", "next;");
+
+        /* ND advertisement handling.  Use advertisements to populate
+         * the logical router's ARP/ND table. */
+        /* xxx We're also supposed to learn on solicitations. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 90, "nd_adv",
+                      "put_nd(inport, nd.target, nd.tll);");
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 80, "nd_sol",
+                      "put_nd(inport, ip6.src, nd.sll);");
     }
 
     /* Logical router ingress table 1: IP Input for IPv4. */
@@ -2586,6 +2594,7 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
             ds_clear(&actions);
             ds_put_format(&actions,
+                          "put_nd(inport, ip6.src, nd.sll); "
                           "nd_adv { "
                           "eth.src = %s; "
                           "ip6.src = %s; "
@@ -2973,8 +2982,11 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 0, "1",
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 0, "ip4",
                       "get_arp(outport, reg0); next;");
+
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 0, "ip6",
+                      "get_nd(outport, xxreg0); next;");
     }
 
     /* Local router ingress table 6: ARP request.
diff --git a/tests/ovn.at b/tests/ovn.at
index fb2d3a3..3d7e995 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -650,6 +650,21 @@ reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4); => DHCP option domain
 # nd_adv
 nd_adv { 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_sol
 
+# get_nd
+get_nd(outport, ip6.dst); => actions=push:NXM_NX_XXREG0[],push:NXM_NX_IPV6_DST[],pop:NXM_NX_XXREG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_XXREG0[], prereqs=eth.type == 0x86dd
+get_nd(inport, xxreg0); => actions=push:NXM_NX_REG15[],push:NXM_NX_REG14[],pop:NXM_NX_REG15[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG15[], prereqs=1
+get_nd; => Syntax error at `;' expecting `('.
+get_nd(); => Syntax error at `)' expecting field name.
+get_nd(inport); => Syntax error at `)' expecting `,'.
+get_nd(inport ip6.dst); => Syntax error at `ip6.dst' expecting `,'.
+get_nd(inport, ip6.dst; => Syntax error at `;' expecting `)'.
+get_nd(inport, eth.dst); => Cannot use 48-bit field eth.dst[0..47] where 128-bit field is required.
+get_nd(inport, outport); => Cannot use string field outport where numeric field is required.
+get_nd(xxreg0, ip6.dst); => Cannot use numeric field xxreg0 where string field is required.
+
+# put_nd
+put_nd(inport, nd.target, nd.sll); => actions=push:NXM_NX_XXREG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ND_SLL[],push:NXM_NX_ND_TARGET[],pop:NXM_NX_XXREG0[],pop:NXM_OF_ETH_SRC[],controller(userdata=00.00.00.04.00.00.00.00),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_XXREG0[], prereqs=((icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x88 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd))) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd) && ((icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x88 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd))) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)
 
  && ip.t
 tl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)
+
