diff mbox

[ovs-dev,IPv6,v2,10/10] ovn: Add support for IPv6 dynamic bindings.

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

Commit Message

Justin Pettit July 29, 2016, 6:26 a.m. UTC
This commit also introduces "get_nd" and "put_nd" logical actions.

Signed-off-by: Justin Pettit <jpettit@ovn.org>
---
 include/ovn/actions.h       |  13 +++-
 ovn/controller/lflow.c      |  27 +++++--
 ovn/controller/pinctrl.c    | 170 ++++++++++++++++++++++++--------------------
 ovn/lib/actions.c           |  60 +++++++++++++++-
 ovn/northd/ovn-northd.8.xml |  56 ++++++++++-----
 ovn/northd/ovn-northd.c     |  17 +++++
 ovn/ovn-architecture.7.xml  |   2 +
 ovn/ovn-sb.xml              |  70 +++++++++++++-----
 tests/ovn.at                |  15 ++++
 tests/test-ovn.c            |   2 +-
 10 files changed, 311 insertions(+), 121 deletions(-)

Comments

Ben Pfaff July 29, 2016, 5:53 p.m. UTC | #1
On Thu, Jul 28, 2016 at 11:26:20PM -0700, Justin Pettit wrote:
> This commit also introduces "get_nd" and "put_nd" logical actions.
> 
> Signed-off-by: Justin Pettit <jpettit@ovn.org>

struct put_mac_binding might include a string buffer directly; not sure
there's value in the extra allocation here.

In pinctrl_handle_put_mac_binding(), the parentheses around the call
to hash_2words() look funny to me here:
+    uint32_t hash = hash_string(ip_s, (hash_2words(dp_key, port_key)));

s/Solictation/Solicitation/ in ovn-northd.8.xml.

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

Patch

diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 2ae9cd2..a395ce9 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -78,6 +78,16 @@  enum action_opcode {
      * The actions, in OpenFlow 1.3 format, follow the action_header.
      */
     ACTION_OPCODE_ND_NA,
+
+    /* "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 mac_bind_ptable;    /* OpenFlow table for 'get_arp'/'get_nd' to
+                                   resubmit. */
 };
 
 char *actions_parse(struct lexer *, const struct action_params *,
diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index 674b756..fda10eb 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -528,7 +528,7 @@  consider_logical_flow(const struct lport_index *lports,
         .first_ptable = first_ptable,
         .cur_ltable = lflow->table_id,
         .output_ptable = output_ptable,
-        .arp_ptable = OFTABLE_MAC_BINDING,
+        .mac_bind_ptable = OFTABLE_MAC_BINDING,
     };
     error = actions_parse_string(lflow->actions, &ap, &ofpacts, &prereqs);
     if (error) {
@@ -638,16 +638,29 @@  consider_neighbor_flow(const struct lport_index *lports,
         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 c03e2cf..ed12dd3 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -53,14 +53,15 @@  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 init_put_arps(void);
-static void destroy_put_arps(void);
-static void run_put_arps(struct controller_ctx *,
-                         const struct lport_index *lports);
-static void wait_put_arps(struct controller_ctx *);
-static void flush_put_arps(void);
+static void pinctrl_handle_put_mac_binding(const struct flow *md,
+                                           const struct flow *headers,
+                                           bool is_arp);
+static void init_put_mac_bindings(void);
+static void destroy_put_mac_bindings(void);
+static void run_put_mac_bindings(struct controller_ctx *,
+                                 const struct lport_index *lports);
+static void wait_put_mac_bindings(struct controller_ctx *);
+static void flush_put_mac_bindings(void);
 
 static void init_send_garps(void);
 static void destroy_send_garps(void);
@@ -75,14 +76,14 @@  static void pinctrl_handle_nd_na(const struct flow *ip_flow,
 static void reload_metadata(struct ofpbuf *ofpacts,
                             const struct match *md);
 
-COVERAGE_DEFINE(pinctrl_drop_put_arp);
+COVERAGE_DEFINE(pinctrl_drop_put_mac_binding);
 
 void
 pinctrl_init(void)
 {
     swconn = rconn_create(5, 0, DSCP_DEFAULT, 1 << OFP13_VERSION);
     conn_seq_no = 0;
-    init_put_arps();
+    init_put_mac_bindings();
     init_send_garps();
 }
 
@@ -403,7 +404,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:
@@ -414,6 +416,11 @@  process_packet_in(const struct ofp_header *msg)
         pinctrl_handle_nd_na(&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));
@@ -473,7 +480,7 @@  pinctrl_run(struct controller_ctx *ctx, const struct lport_index *lports,
         if (conn_seq_no != rconn_get_connection_seqno(swconn)) {
             pinctrl_setup(swconn);
             conn_seq_no = rconn_get_connection_seqno(swconn);
-            flush_put_arps();
+            flush_put_mac_bindings();
         }
 
         /* Process a limited number of messages per call. */
@@ -492,14 +499,14 @@  pinctrl_run(struct controller_ctx *ctx, const struct lport_index *lports,
         }
     }
 
-    run_put_arps(ctx, lports);
+    run_put_mac_bindings(ctx, lports);
     send_garp_run(br_int, chassis_id, lports, local_datapaths);
 }
 
 void
 pinctrl_wait(struct controller_ctx *ctx)
 {
-    wait_put_arps(ctx);
+    wait_put_mac_bindings(ctx);
     rconn_run_wait(swconn);
     rconn_recv_wait(swconn);
     send_garp_wait();
@@ -509,60 +516,61 @@  void
 pinctrl_destroy(void)
 {
     rconn_destroy(swconn);
-    destroy_put_arps();
+    destroy_put_mac_bindings();
     destroy_send_garps();
 }
 
-/* Implementation of the "put_arp" OVN action.  This action sends a packet to
- * ovn-controller, using the flow as an API (see actions.h for details).  This
- * code implements the action by updating the MAC_Binding table in the
- * southbound database.
+/* Implementation of the "put_arp" and "put_nd" OVN actions.  These
+ * actions send a packet to ovn-controller, using the flow as an API
+ * (see actions.h for details).  This code implements the actions by
+ * updating the MAC_Binding table in the southbound database.
  *
  * This code could be a lot simpler if the database could always be updated,
  * but in fact we can only update it when ctx->ovnsb_idl_txn is nonnull.  Thus,
- * we buffer up a few put_arps (but we don't keep them longer than 1 second)
- * and apply them whenever a database transaction is available. */
+ * we buffer up a few put_mac_bindings (but we don't keep them longer
+ * than 1 second) and apply them whenever a database transaction is
+ * available. */
 
-/* Buffered "put_arp" operation. */
-struct put_arp {
-    struct hmap_node hmap_node; /* In 'put_arps'. */
+/* Buffered "put_mac_binding" operation. */
+struct put_mac_binding {
+    struct hmap_node hmap_node; /* In 'put_mac_bindings'. */
 
     long long int timestamp;    /* In milliseconds. */
 
     /* Key. */
     uint32_t dp_key;
     uint32_t port_key;
-    ovs_be32 ip;
+    char *ip_s;
 
     /* Value. */
     struct eth_addr mac;
 };
 
-/* Contains "struct put_arp"s. */
-static struct hmap put_arps;
+/* Contains "struct put_mac_binding"s. */
+static struct hmap put_mac_bindings;
 
 static void
-init_put_arps(void)
+init_put_mac_bindings(void)
 {
-    hmap_init(&put_arps);
+    hmap_init(&put_mac_bindings);
 }
 
 static void
-destroy_put_arps(void)
+destroy_put_mac_bindings(void)
 {
-    flush_put_arps();
-    hmap_destroy(&put_arps);
+    flush_put_mac_bindings();
+    hmap_destroy(&put_mac_bindings);
 }
 
-static struct put_arp *
-pinctrl_find_put_arp(uint32_t dp_key, uint32_t port_key, ovs_be32 ip,
-                     uint32_t hash)
+static struct put_mac_binding *
+pinctrl_find_put_mac_binding(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) {
+    struct put_mac_binding *pa;
+    HMAP_FOR_EACH_WITH_HASH (pa, hmap_node, hash, &put_mac_bindings) {
         if (pa->dp_key == dp_key
             && pa->port_key == port_key
-            && pa->ip == ip) {
+            && !strcmp(pa->ip_s, ip_s)) {
             return pa;
         }
     }
@@ -570,64 +578,72 @@  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_arp)
 {
     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);
-    if (!pa) {
-        if (hmap_count(&put_arps) >= 1000) {
-            COVERAGE_INC(pinctrl_drop_put_arp);
+    char ip_s[INET6_ADDRSTRLEN];
+
+    if (is_arp) {
+        ovs_be32 ip = htonl(md->regs[0]);
+        inet_ntop(AF_INET, &ip, 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_mac_binding *pmb
+        = pinctrl_find_put_mac_binding(dp_key, port_key, ip_s, hash);
+    if (!pmb) {
+        if (hmap_count(&put_mac_bindings) >= 1000) {
+            COVERAGE_INC(pinctrl_drop_put_mac_binding);
             return;
         }
 
-        pa = xmalloc(sizeof *pa);
-        hmap_insert(&put_arps, &pa->hmap_node, hash);
-        pa->dp_key = dp_key;
-        pa->port_key = port_key;
-        pa->ip = ip;
+        pmb = xmalloc(sizeof *pmb);
+        hmap_insert(&put_mac_bindings, &pmb->hmap_node, hash);
+        pmb->dp_key = dp_key;
+        pmb->port_key = port_key;
+        pmb->ip_s = xstrdup(ip_s);
     }
-    pa->timestamp = time_msec();
-    pa->mac = headers->dl_src;
+    pmb->timestamp = time_msec();
+    pmb->mac = headers->dl_src;
 }
 
 static void
-run_put_arp(struct controller_ctx *ctx, const struct lport_index *lports,
-            const struct put_arp *pa)
+run_put_mac_binding(struct controller_ctx *ctx,
+                    const struct lport_index *lports,
+                    const struct put_mac_binding *pmb)
 {
-    if (time_msec() > pa->timestamp + 1000) {
+    if (time_msec() > pmb->timestamp + 1000) {
         return;
     }
 
     /* Convert logical datapath and logical port key into lport. */
     const struct sbrec_port_binding *pb
-        = lport_lookup_by_key(lports, pa->dp_key, pa->port_key);
+        = lport_lookup_by_key(lports, pmb->dp_key, pmb->port_key);
     if (!pb) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
 
         VLOG_WARN_RL(&rl, "unknown logical port with datapath %"PRIu32" "
-                     "and port %"PRIu32, pa->dp_key, pa->port_key);
+                     "and port %"PRIu32, pmb->dp_key, pmb->port_key);
         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));
+             ETH_ADDR_FMT, ETH_ADDR_ARGS(pmb->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, pmb->ip_s)) {
             if (strcmp(b->mac, mac_string)) {
                 sbrec_mac_binding_set_mac(b, mac_string);
             }
@@ -638,39 +654,41 @@  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, pmb->ip_s);
     sbrec_mac_binding_set_mac(b, mac_string);
     sbrec_mac_binding_set_datapath(b, pb->datapath);
 }
 
 static void
-run_put_arps(struct controller_ctx *ctx, const struct lport_index *lports)
+run_put_mac_bindings(struct controller_ctx *ctx,
+                     const struct lport_index *lports)
 {
     if (!ctx->ovnsb_idl_txn) {
         return;
     }
 
-    const struct put_arp *pa;
-    HMAP_FOR_EACH (pa, hmap_node, &put_arps) {
-        run_put_arp(ctx, lports, pa);
+    const struct put_mac_binding *pmb;
+    HMAP_FOR_EACH (pmb, hmap_node, &put_mac_bindings) {
+        run_put_mac_binding(ctx, lports, pmb);
     }
-    flush_put_arps();
+    flush_put_mac_bindings();
 }
 
 static void
-wait_put_arps(struct controller_ctx *ctx)
+wait_put_mac_bindings(struct controller_ctx *ctx)
 {
-    if (ctx->ovnsb_idl_txn && !hmap_is_empty(&put_arps)) {
+    if (ctx->ovnsb_idl_txn && !hmap_is_empty(&put_mac_bindings)) {
         poll_immediate_wake();
     }
 }
 
 static void
-flush_put_arps(void)
+flush_put_mac_bindings(void)
 {
-    struct put_arp *pa;
-    HMAP_FOR_EACH_POP (pa, hmap_node, &put_arps) {
-        free(pa);
+    struct put_mac_binding *pmb;
+    HMAP_FOR_EACH_POP (pmb, hmap_node, &put_mac_bindings) {
+        free(pmb->ip_s);
+        free(pmb);
     }
 }
 
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 6b8ea3f..b9d1205 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -424,7 +424,7 @@  parse_get_arp_action(struct action_context *ctx)
     setup_args(ctx, args, ARRAY_SIZE(args));
 
     put_load(0, MFF_ETH_DST, 0, 48, ctx->ofpacts);
-    emit_resubmit(ctx, ctx->ap->arp_ptable);
+    emit_resubmit(ctx, ctx->ap->mac_bind_ptable);
 
     restore_args(ctx, args, ARRAY_SIZE(args));
 }
@@ -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->mac_bind_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)
@@ -1065,12 +1115,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_na")) {
-        parse_nested_action(ctx, ACTION_OPCODE_ND_NA, "nd_ns");
     } 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_na")) {
+        parse_nested_action(ctx, ACTION_OPCODE_ND_NA, "nd_ns");
+    } 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/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
index bfcaeb5..9bee3a3 100644
--- a/ovn/northd/ovn-northd.8.xml
+++ b/ovn/northd/ovn-northd.8.xml
@@ -843,7 +843,7 @@  output;
       </li>
 
       <li>
-        ARP reply handling.  These flows use ARP replies to populate the
+        ARP reply handling.  This flow uses ARP replies to populate the
         logical router's ARP table.  A priority-90 flow with match <code>arp.op
         == 2</code> has actions <code>put_arp(inport, arp.spa,
         arp.sha);</code>.
@@ -851,21 +851,19 @@  output;
 
       <li>
         <p>
-          Reply to IPv6 Neighbor Solicitations.
-        </p>
-
-        <p>
-          These flows reply to Neighbor Solictation requests for the
-          router's own IPv6 address.  For each router port <var>P</var>
-          that owns IPv6 address <var>A</var>, solicited node address
-          <var>S</var>, and Ethernet address
-          <var>E</var>, a priority-90 flow matches <code>inport ==
-          <var>P</var> &amp;&amp; nd_ns &amp;&amp; ip6.dst == {<var>A</var>,
-          <var>E</var>} &amp;&amp; nd.target == <var>A</var></code>
-          with the following actions:
+          Reply to IPv6 Neighbor Solicitations.  These flows reply to
+          Neighbor Solictation requests for the router's own IPv6
+          address and populate the logical router's mac binding table.
+          For each router port <var>P</var> that owns IPv6 address
+          <var>A</var>, solicited node address <var>S</var>, and
+          Ethernet address <var>E</var>, a priority-90 flow matches
+          <code>inport == <var>P</var> &amp;&amp; nd_ns &amp;&amp;
+          ip6.dst == {<var>A</var>, <var>E</var>} &amp;&amp; nd.target
+          == <var>A</var></code> with the following actions:
         </p>
 
         <pre>
+put_nd(inport, ip6.src, nd.sll);
 nd_na {
     eth.src = <var>E</var>;
     ip6.src = <var>A</var>;
@@ -879,6 +877,23 @@  nd_na {
       </li>
 
       <li>
+        IPv6 neighbor advertisement handling.  This flow uses neighbor
+        advertisements to populate the logical router's mac binding
+        table.  A priority-90 flow with match <code>nd_na</code>
+        has actions <code>put_nd(inport, nd.target, nd.tll);</code>.
+      </li>
+
+      <li>
+        IPv6 neighbor solicitation for non-hosted addresses handling.
+        This flow uses neighbor solicitations to populate the logical
+        router's mac binding table (ones that were directed at the
+        logical router would have matched the priority-90 neighbor
+        solicitation flow already).  A priority-80 flow with match
+        <code>nd_ns</code> has actions
+        <code>put_nd(inport, ip6.src, nd.sll);</code>.
+      </li>
+
+      <li>
         <p>
           UDP port unreachable.  Priority-80 flows generate ICMP port
           unreachable messages in reply to UDP datagrams directed to the
@@ -1234,15 +1249,22 @@  icmp4 {
 
       <li>
         <p>
-          Dynamic MAC bindings.  This flows resolves MAC-to-IP bindings that
-          have become known dynamically through ARP.  (The next table will
-          issue an ARP request for cases where the binding is not yet known.)
+          Dynamic MAC bindings.  These flows resolve MAC-to-IP bindings
+          that have become known dynamically through ARP or neighbor
+          discovery.  (The next table will issue an ARP or neighbor
+          solicitation request for cases where the binding is not yet
+          known.)
         </p>
 
         <p>
-          A priority-0 logical flow with match <code>1</code> has actions
+          A priority-0 logical flow with match <code>ip4</code> has actions
           <code>get_arp(outport, reg0); next;</code>.
         </p>
+
+        <p>
+          A priority-0 logical flow with match <code>ip6</code> has actions
+          <code>get_nd(outport, xxreg0); next;</code>.
+        </p>
       </li>
     </ul>
 
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index f0f72f0..f9d9823 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -3042,6 +3042,17 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 30,
                       ds_cstr(&match), "drop;");
 
+        /* ND advertisement handling.  Use advertisements to populate
+         * the logical router's ARP/ND table. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 90, "nd_na",
+                      "put_nd(inport, nd.target, nd.tll);");
+
+        /* Lean from neighbor solicitations that were not directed at
+         * us.  (A priority-90 flow will respond to requests to us and
+         * learn the sender's mac address. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 80, "nd_ns",
+                      "put_nd(inport, ip6.src, nd.sll);");
+
         /* 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;");
@@ -3248,6 +3259,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_na { "
                           "eth.src = %s; "
                           "ip6.src = %s; "
@@ -3625,6 +3637,11 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
         ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 0, "1",
                       "get_arp(outport, reg0); next;");
+        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/ovn/ovn-architecture.7.xml b/ovn/ovn-architecture.7.xml
index a9258e7..bf90dac 100644
--- a/ovn/ovn-architecture.7.xml
+++ b/ovn/ovn-architecture.7.xml
@@ -857,6 +857,7 @@ 
         </dd>
 
         <dt><code>get_arp(<var>P</var>, <var>A</var>);</code></dt>
+        <dt><code>get_nd(<var>P</var>, <var>A</var>);</code></dt>
         <dd>
           <p>
             Implemented by storing arguments into OpenFlow fields, then
@@ -875,6 +876,7 @@ 
         </dd>
 
         <dt><code>put_arp(<var>P</var>, <var>A</var>, <var>E</var>);</code></dt>
+        <dt><code>put_nd(<var>P</var>, <var>A</var>, <var>E</var>);</code></dt>
         <dd>
           <p>
             Implemented by storing the arguments into OpenFlow fields, then
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index d134e74..28ab0ae 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1109,6 +1109,44 @@ 
           <p><b>Prerequisite:</b> <code>ip4</code></p>
         </dd>
 
+        <dt><code>get_arp(<var>P</var>, <var>A</var>);</code></dt>
+
+        <dd>
+          <p>
+            <b>Parameters</b>: logical port string field <var>P</var>, 32-bit
+            IP address field <var>A</var>.
+          </p>
+
+          <p>
+            Looks up <var>A</var> in <var>P</var>'s mac binding table.
+            If an entry is found, stores its Ethernet address in
+            <code>eth.dst</code>, otherwise stores
+            <code>00:00:00:00:00:00</code> in <code>eth.dst</code>.
+          </p>
+
+          <p><b>Example:</b> <code>get_arp(outport, ip4.dst);</code></p>
+        </dd>
+
+        <dt>
+          <code>put_arp(<var>P</var>, <var>A</var>, <var>E</var>);</code>
+        </dt>
+
+        <dd>
+          <p>
+            <b>Parameters</b>: logical port string field <var>P</var>, 32-bit
+            IP address field <var>A</var>, 48-bit Ethernet address field
+            <var>E</var>.
+          </p>
+
+          <p>
+            Adds or updates the entry for IP address <var>A</var> in
+            logical port <var>P</var>'s mac binding table, setting its
+            Ethernet address to <var>E</var>.
+          </p>
+
+          <p><b>Example:</b> <code>put_arp(inport, arp.spa, arp.sha);</code></p>
+        </dd>
+
         <dt>
           <code>nd_na { <var>action</var>; </code>...<code> };</code>
         </dt>
@@ -1149,42 +1187,42 @@ 
           </p>
         </dd>
 
-        <dt><code>get_arp(<var>P</var>, <var>A</var>);</code></dt>
+        <dt><code>get_nd(<var>P</var>, <var>A</var>);</code></dt>
 
         <dd>
           <p>
-            <b>Parameters</b>: logical port string field <var>P</var>, 32-bit
-            IP address field <var>A</var>.
+            <b>Parameters</b>: logical port string field <var>P</var>, 128-bit
+            IPv6 address field <var>A</var>.
           </p>
 
           <p>
-            Looks up <var>A</var> in <var>P</var>'s ARP table.  If an entry is
-            found, stores its Ethernet address in <code>eth.dst</code>,
-            otherwise stores <code>00:00:00:00:00:00</code> in
-            <code>eth.dst</code>.
+            Looks up <var>A</var> in <var>P</var>'s mac binding table.
+            If an entry is found, stores its Ethernet address in
+            <code>eth.dst</code>, otherwise stores
+            <code>00:00:00:00:00:00</code> in <code>eth.dst</code>.
           </p>
 
-          <p><b>Example:</b> <code>get_arp(outport, ip4.dst);</code></p>
+          <p><b>Example:</b> <code>get_nd(outport, ip6.dst);</code></p>
         </dd>
 
         <dt>
-          <code>put_arp(<var>P</var>, <var>A</var>, <var>E</var>);</code>
+          <code>put_nd(<var>P</var>, <var>A</var>, <var>E</var>);</code>
         </dt>
 
         <dd>
           <p>
-            <b>Parameters</b>: logical port string field <var>P</var>, 32-bit
-            IP address field <var>A</var>, 48-bit Ethernet address field
-            <var>E</var>.
+            <b>Parameters</b>: logical port string field <var>P</var>,
+            128-bit IPv6 address field <var>A</var>, 48-bit Ethernet
+            address field <var>E</var>.
           </p>
 
           <p>
-            Adds or updates the entry for IP address <var>A</var> in logical
-            port <var>P</var>'s ARP table, setting its Ethernet address to
-            <var>E</var>.
+            Adds or updates the entry for IPv6 address <var>A</var> in
+            logical port <var>P</var>'s mac binding table, setting its
+            Ethernet address to <var>E</var>.
           </p>
 
-          <p><b>Example:</b> <code>put_arp(inport, arp.spa, arp.sha);</code></p>
+          <p><b>Example:</b> <code>put_nd(inport, nd.target, nd.tll);</code></p>
         </dd>
 
         <dt>
diff --git a/tests/ovn.at b/tests/ovn.at
index 72d63f2..3d2ebe5 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -699,6 +699,21 @@  reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4); => DHCP option domain
 # 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.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=nd_ns
 
+# 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.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd)
+
 # 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
 ip4.src <-> ip6.src[0..31]; => actions=push:NXM_NX_IPV6_SRC[0..31],push:NXM_OF_IP_SRC[],pop:NXM_NX_IPV6_SRC[0..31],pop:NXM_OF_IP_SRC[], prereqs=eth.type == 0x800 && eth.type == 0x86dd
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index e226b3b..9a0101b 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1320,7 +1320,7 @@  test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
             .first_ptable = 16,
             .cur_ltable = 10,
             .output_ptable = 64,
-            .arp_ptable = 65,
+            .mac_bind_ptable = 65,
         };
         error = actions_parse_string(ds_cstr(&input), &ap, &ofpacts, &prereqs);
         if (!error) {