diff --git a/ovn/TODO b/ovn/TODO
index 4f134a4..cbe6a5c 100644
--- a/ovn/TODO
+++ b/ovn/TODO
@@ -46,11 +46,24 @@ we can use parse_int_string() to support larger integers.
 
 ** IPv6
 
-*** ND versus ARP
+*** Drop invalid source IPv6 addresses
 
-*** IPv6 routing
+*** Don't forward non-routable addresses
 
-*** ICMPv6
+*** ICMPv6 action
+
+Similar to the ICMPv4 action, ICMPv6 messages should be generated.
+
+*** Neighbor Discovery
+
+**** ND Router Advertisements
+
+The router ports should periodically send out ND Router Advertisements
+and respond to Router Solicitations.
+
+**** Learn MAC bindings on ND Solicitations
+
+**** Properly set RSO flags
 
 ** Dynamic IP to MAC bindings
 
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index b99c183..e0e40d0 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -1866,37 +1866,34 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                               ds_cstr(&match), ds_cstr(&actions));
             }
 
-            if (op->lsp_addrs[i].n_ipv6_addrs > 0) {
+            /* For ND solicitations, we need to listen for both the
+             * unicast IPv6 address and its all-nodes multicast address,
+             * but always respond with the unicast IPv6 address. */
+            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
                 ds_clear(&match);
-                ds_put_cstr(&match, "icmp6 && icmp6.type == 135 && ");
-                if (op->lsp_addrs[i].n_ipv6_addrs == 1) {
-                    ds_put_format(&match, "nd.target == %s",
-                                  op->lsp_addrs[i].ipv6_addrs[0].addr_s);
-                } else {
-                    ds_put_cstr(&match, "(");
-                    for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
-                        ds_put_format(&match, "nd.target == %s || ",
-                                      op->lsp_addrs[i].ipv6_addrs[j].addr_s);
-                    }
-                    ds_chomp(&match, ' ');
-                    ds_chomp(&match, '|');
-                    ds_chomp(&match, '|');
-                    ds_chomp(&match, ' ');
-                    ds_put_cstr(&match, ")");
-                }
+                ds_put_format(&match,
+                        "nd_sol && ip6.dst == {%s, %s} && nd.target == %s",
+                        op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+                        op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
+                        op->lsp_addrs[i].ipv6_addrs[j].addr_s);
+
                 ds_clear(&actions);
                 ds_put_format(&actions,
-                    "nd_adv { eth.src = %s; "
-                    "nd.tll = %s; "
-                    "outport = inport; "
-                    "inport = \"\"; /* Allow sending out inport. */ "
-                    "output; };",
-                    op->lsp_addrs[i].ea_s,
-                    op->lsp_addrs[i].ea_s);
-
+                        "nd_adv { "
+                        "eth.src = %s; "
+                        "ip6.src = %s; "
+                        "nd.target = %s; "
+                        "nd.tll = %s; "
+                        "outport = inport; "
+                        "inport = \"\"; /* Allow sending out inport. */ "
+                        "output; "
+                        "};",
+                        op->lsp_addrs[i].ea_s,
+                        op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+                        op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+                        op->lsp_addrs[i].ea_s);
                 ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
                               ds_cstr(&match), ds_cstr(&actions));
-
             }
         }
     }
@@ -2037,23 +2034,49 @@ lrport_is_enabled(const struct nbrec_logical_router_port *lrport)
 static const char *
 find_lrp_member_ip(const struct ovn_port *op, const char *ip_s)
 {
-    uint32_t ip;
+    bool is_ipv4 = strchr(ip_s, '.') ? true : false;
 
-    if (!ip_parse(ip_s, &ip)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "bad ip address %s", ip_s);
-        return NULL;
-    }
+    if (is_ipv4) {
+        uint32_t ip;
 
-    for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-        const struct ipv4_netaddr *na = &op->lrp_networks.ipv4_addrs[i];
+        if (!ip_parse(ip_s, &ip)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad ip address %s", ip_s);
+            return NULL;
+        }
+
+        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
+            const struct ipv4_netaddr *na = &op->lrp_networks.ipv4_addrs[i];
+
+            if (!((na->network ^ ip) & na->mask)) {
+                /* There should be only 1 interface that matches the
+                 * supplied IP.  Otherwise, it's a configuration error,
+                 * because subnets of a router's interfaces should NOT
+                 * overlap. */
+                return na->addr_s;
+            }
+        }
+    } else {
+        struct in6_addr ip6;
 
-        if (!((na->network ^ ip) & na->mask)) {
-            /* There should be only 1 interface that matches the
-             * next hop.  Otherwise, it's a configuration error,
-             * because subnets of router's interfaces should NOT
-             * overlap. */
-            return na->addr_s;
+        if (!ipv6_parse(ip_s, &ip6)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad ipv6 address %s", ip_s);
+            return NULL;
+        }
+
+        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
+            const struct ipv6_netaddr *na = &op->lrp_networks.ipv6_addrs[i];
+            struct in6_addr xor_addr = ipv6_addr_bitxor(&na->network, &ip6);
+            struct in6_addr and_addr = ipv6_addr_bitand(&xor_addr, &na->mask);
+
+            if (ipv6_is_zero(&and_addr)) {
+                /* There should be only 1 interface that matches the
+                 * supplied IP.  Otherwise, it's a configuration error,
+                 * because subnets of a router's interfaces should NOT
+                 * overlap. */
+                return na->addr_s;
+            }
         }
     }
 
@@ -2065,21 +2088,26 @@ add_route(struct hmap *lflows, const struct ovn_port *op,
           const char *lrp_addr_s, const char *network_s, int plen,
           const char *gateway)
 {
-    char *match = xasprintf("ip4.dst == %s/%d", network_s, plen);
+    bool is_ipv4 = strchr(network_s, '.') ? true : false;
+
+    char *match = xasprintf("ip%s.dst == %s/%d", is_ipv4 ? "4" : "6",
+                            network_s, plen);
 
     struct ds actions = DS_EMPTY_INITIALIZER;
-    ds_put_cstr(&actions, "ip.ttl--; reg0 = ");
+    ds_put_format(&actions, "ip.ttl--; %sreg0 = ", is_ipv4 ? "" : "xx");
+
     if (gateway) {
         ds_put_cstr(&actions, gateway);
     } else {
-        ds_put_cstr(&actions, "ip4.dst");
+        ds_put_format(&actions, "ip%s.dst", is_ipv4 ? "4" : "6");
     }
     ds_put_format(&actions, "; "
-                  "reg1 = %s; "
+                  "%sreg1 = %s; "
                   "eth.src = %s; "
                   "outport = %s; "
                   "inport = \"\"; /* Allow sending out inport. */ "
                   "next;",
+                  is_ipv4 ? "" : "xx",
                   lrp_addr_s,
                   op->lrp_networks.ea_s,
                   op->json_key);
@@ -2097,26 +2125,68 @@ build_static_route_flow(struct hmap *lflows, struct ovn_datapath *od,
                         struct hmap *ports,
                         const struct nbrec_logical_router_static_route *route)
 {
-    ovs_be32 prefix, nexthop, mask;
+    ovs_be32 nexthop;
     const char *lrp_addr_s;
+    unsigned int plen;
+    bool is_ipv4;
 
-    /* Verify that next hop is an IP address with 32 bits mask. */
-    char *error = ip_parse_masked(route->nexthop, &nexthop, &mask);
-    if (error || mask != OVS_BE32_MAX) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "bad next hop ip address %s", route->nexthop);
+    /* Verify that the next hop is an IP address with an all-ones mask. */
+    char *error = ip_parse_cidr(route->nexthop, &nexthop, &plen);
+    if (!error) {
+        if (plen != 32) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad next hop mask %s", route->nexthop);
+            return;
+        }
+        is_ipv4 = true;
+    } else {
         free(error);
-        return;
+
+        struct in6_addr ip6;
+        char *error = ipv6_parse_cidr(route->nexthop, &ip6, &plen);
+        if (!error) {
+            if (plen != 128) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "bad next hop mask %s", route->nexthop);
+                return;
+            }
+            is_ipv4 = false;
+        } else {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad next hop ip address %s", route->nexthop);
+            free(error);
+            return;
+        }
     }
 
-    /* Verify that ip prefix is a valid CIDR address. */
-    error = ip_parse_masked(route->ip_prefix, &prefix, &mask);
-    if (error || !ip_is_cidr(mask)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "bad 'ip_prefix' in static routes %s",
-                     route->ip_prefix);
-        free(error);
-        return;
+    char *prefix_s;
+    if (is_ipv4) {
+        ovs_be32 prefix;
+        /* Verify that ip prefix is a valid IPv4 address. */
+        error = ip_parse_cidr(route->ip_prefix, &prefix, &plen);
+        if (error) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad 'ip_prefix' in static routes %s",
+                         route->ip_prefix);
+            free(error);
+            return;
+        }
+        prefix_s = xasprintf(IP_FMT, IP_ARGS(prefix & be32_prefix_mask(plen)));
+    } else {
+        /* Verify that ip prefix is a valid IPv6 address. */
+        struct in6_addr prefix;
+        error = ipv6_parse_cidr(route->ip_prefix, &prefix, &plen);
+        if (error) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad 'ip_prefix' in static routes %s",
+                         route->ip_prefix);
+            free(error);
+            return;
+        }
+        struct in6_addr mask = ipv6_create_mask(plen);
+        struct in6_addr network = ipv6_addr_bitand(&prefix, &mask);
+        prefix_s = xmalloc(INET6_ADDRSTRLEN);
+        inet_ntop(AF_INET6, &network, prefix_s, INET6_ADDRSTRLEN);
     }
 
     /* Find the outgoing port. */
@@ -2127,7 +2197,7 @@ build_static_route_flow(struct hmap *lflows, struct ovn_datapath *od,
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
             VLOG_WARN_RL(&rl, "Bad out port %s for static route %s",
                          route->output_port, route->ip_prefix);
-            return;
+            goto free_prefix_s;
         }
         lrp_addr_s = find_lrp_member_ip(out_port, route->nexthop);
     } else {
@@ -2154,17 +2224,17 @@ build_static_route_flow(struct hmap *lflows, struct ovn_datapath *od,
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
         VLOG_WARN_RL(&rl, "No path for static route %s; next hop %s",
                      route->ip_prefix, route->nexthop);
-        return;
+        goto free_prefix_s;
     }
 
-    char *prefix_s = xasprintf(IP_FMT, IP_ARGS(prefix & mask));
-    add_route(lflows, out_port, lrp_addr_s, prefix_s,
-              ip_count_cidr_bits(mask), route->nexthop);
+    add_route(lflows, out_port, lrp_addr_s, prefix_s, plen, route->nexthop);
+
+free_prefix_s:
     free(prefix_s);
 }
 
 static void
-op_put_networks(struct ds *ds, const struct ovn_port *op, bool add_bcast)
+op_put_v4_networks(struct ds *ds, const struct ovn_port *op, bool add_bcast)
 {
     if (!add_bcast && op->lrp_networks.n_ipv4_addrs == 1) {
         ds_put_format(ds, "%s", op->lrp_networks.ipv4_addrs[0].addr_s);
@@ -2184,6 +2254,23 @@ op_put_networks(struct ds *ds, const struct ovn_port *op, bool add_bcast)
 }
 
 static void
+op_put_v6_networks(struct ds *ds, const struct ovn_port *op)
+{
+    if (op->lrp_networks.n_ipv6_addrs == 1) {
+        ds_put_format(ds, "%s", op->lrp_networks.ipv6_addrs[0].addr_s);
+        return;
+    }
+
+    ds_put_cstr(ds, "{");
+    for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
+        ds_put_format(ds, "%s, ", op->lrp_networks.ipv6_addrs[i].addr_s);
+    }
+    ds_chomp(ds, ' ');
+    ds_chomp(ds, ',');
+    ds_put_cstr(ds, "}");
+}
+
+static void
 build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     struct hmap *lflows)
 {
@@ -2271,39 +2358,43 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 0, "1", "next;");
     }
 
+    /* Logical router ingress table 1: IP Input for IPv4. */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbr) {
             continue;
         }
 
-        /* L3 admission control: drop packets that originate from an IP address
-         * owned by the router or a broadcast address known to the router
-         * (priority 100). */
-        ds_clear(&match);
-        ds_put_cstr(&match, "ip4.src == ");
-        op_put_networks(&match, op, true);
-        ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 100,
-                      ds_cstr(&match), "drop;");
 
-        /* ICMP echo reply.  These flows reply to ICMP echo requests
-         * received for the router's IP address. Since packets only
-         * get here as part of the logical router datapath, the inport
-         * (i.e. the incoming locally attached net) does not matter.
-         * The ip.ttl also does not matter (RFC1812 section 4.2.2.9) */
-        ds_clear(&match);
-        ds_put_cstr(&match, "ip4.dst == ");
-        op_put_networks(&match, op, false);
-        ds_put_cstr(&match, " && icmp4.type == 8 && icmp4.code == 0");
-
-        ds_clear(&actions);
-        ds_put_format(&actions,
-            "ip4.dst <-> ip4.src; "
-            "ip.ttl = 255; "
-            "icmp4.type = 0; "
-            "inport = \"\"; /* Allow sending out inport. */ "
-            "next; ");
-        ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
-                      ds_cstr(&match), ds_cstr(&actions));
+        if (op->lrp_networks.n_ipv4_addrs) {
+            /* L3 admission control: drop packets that originate from an
+             * IPv4 address owned by the router or a broadcast address
+             * known to the router (priority 100). */
+            ds_clear(&match);
+            ds_put_cstr(&match, "ip4.src == ");
+            op_put_v4_networks(&match, op, true);
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 100,
+                          ds_cstr(&match), "drop;");
+
+            /* ICMP echo reply.  These flows reply to ICMP echo requests
+             * received for the router's IP address. Since packets only
+             * get here as part of the logical router datapath, the inport
+             * (i.e. the incoming locally attached net) does not matter.
+             * The ip.ttl also does not matter (RFC1812 section 4.2.2.9) */
+            ds_clear(&match);
+            ds_put_cstr(&match, "ip4.dst == ");
+            op_put_v4_networks(&match, op, false);
+            ds_put_cstr(&match, " && icmp4.type == 8 && icmp4.code == 0");
+
+            ds_clear(&actions);
+            ds_put_format(&actions,
+                "ip4.dst <-> ip4.src; "
+                "ip.ttl = 255; "
+                "icmp4.type = 0; "
+                "inport = \"\"; /* Allow sending out inport. */ "
+                "next; ");
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
+                          ds_cstr(&match), ds_cstr(&actions));
+        }
 
         /* ARP reply.  These flows reply to ARP requests for the router's own
          * IP address. */
@@ -2428,6 +2519,77 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         free(nat_ips);
     }
 
+    /* Logical router ingress table 1: IP Input for IPv6. */
+    HMAP_FOR_EACH (op, key_node, ports) {
+        if (!op->nbr) {
+            continue;
+        }
+
+        if (op->lrp_networks.n_ipv6_addrs) {
+            /* L3 admission control: drop packets that originate from an
+             * IPv6 address owned by the router or a broadcast address
+             * known to the router (priority 100). */
+            ds_clear(&match);
+            ds_put_cstr(&match, "ip6.src == ");
+            op_put_v6_networks(&match, op);
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 100,
+                          ds_cstr(&match), "drop;");
+
+            /* ICMPv6 echo reply.  These flows reply to echo requests
+             * received for the router's IP address. */
+            ds_clear(&match);
+            ds_put_cstr(&match, "ip6.dst == ");
+            op_put_v6_networks(&match, op);
+            ds_put_cstr(&match, " && icmp6.type == 128 && icmp6.code == 0");
+
+            ds_clear(&actions);
+            ds_put_cstr(&actions,
+                        "ip6.dst <-> ip6.src; "
+                        "ip.ttl = 255; "
+                        "icmp6.type = 129; "
+                        "inport = \"\"; /* Allow sending out inport. */ "
+                        "next; ");
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
+                          ds_cstr(&match), ds_cstr(&actions));
+
+            /* Drop IPv6 traffic to this router. */
+            ds_clear(&match);
+            ds_put_cstr(&match, "ip6.dst == ");
+            op_put_v6_networks(&match, op);
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 60,
+                          ds_cstr(&match), "drop;");
+        }
+
+        /* ND reply.  These flows reply to ND solicitations for the
+         * router's own IP address. */
+        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
+            ds_clear(&match);
+            ds_put_format(&match,
+                    "nd_sol && ip6.dst == {%s, %s} && nd.target == %s",
+                    op->lrp_networks.ipv6_addrs[i].addr_s,
+                    op->lrp_networks.ipv6_addrs[i].sn_addr_s,
+                    op->lrp_networks.ipv6_addrs[i].addr_s);
+
+            ds_clear(&actions);
+            ds_put_format(&actions,
+                          "nd_adv { "
+                          "eth.src = %s; "
+                          "ip6.src = %s; "
+                          "nd.target = %s; "
+                          "nd.tll = %s; "
+                          "outport = inport; "
+                          "inport = \"\"; /* Allow sending out inport. */ "
+                          "output; "
+                          "};",
+                          op->lrp_networks.ea_s,
+                          op->lrp_networks.ipv6_addrs[i].addr_s,
+                          op->lrp_networks.ipv6_addrs[i].addr_s,
+                          op->lrp_networks.ea_s);
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
+                          ds_cstr(&match), ds_cstr(&actions));
+        }
+    }
+
     /* NAT in Gateway routers. */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbr) {
@@ -2558,10 +2720,11 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
     /* Logical router ingress table 4: IP Routing.
      *
      * A packet that arrives at this table is an IP packet that should be
-     * routed to the address in ip4.dst. This table sets outport to the correct
-     * output port, eth.src to the output port's MAC address, and reg0 to the
-     * next-hop IP address (leaving ip4.dst, the packet’s final destination,
-     * unchanged), and advances to the next table for ARP resolution. */
+     * routed to the address in 'ip[46].dst'. This table sets outport to
+     * the correct output port, eth.src to the output port's MAC
+     * address, and '[xx]reg0' to the next-hop IP address (leaving
+     * 'ip[46].dst', the packet’s final destination, unchanged), and
+     * advances to the next table for ARP/ND resolution. */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbr) {
             continue;
@@ -2572,14 +2735,20 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                       op->lrp_networks.ipv4_addrs[i].network_s,
                       op->lrp_networks.ipv4_addrs[i].plen, NULL);
         }
+
+        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
+            add_route(lflows, op, op->lrp_networks.ipv6_addrs[i].addr_s,
+                      op->lrp_networks.ipv6_addrs[i].network_s,
+                      op->lrp_networks.ipv6_addrs[i].plen, NULL);
+        }
     }
 
+    /* Convert the static routes to flows. */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbr) {
             continue;
         }
 
-        /* Convert the static routes to flows. */
         for (int i = 0; i < od->nbr->n_static_routes; i++) {
             const struct nbrec_logical_router_static_route *route;
 
@@ -2587,6 +2756,7 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             build_static_route_flow(lflows, od, ports, route);
         }
     }
+
     /* XXX destination unreachable */
 
     /* Local router ingress table 5: ARP Resolution.
@@ -2597,10 +2767,11 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
      * Ethernet address in eth.dst. */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (op->nbr) {
-            /* This is a logical router port. If next-hop IP address in 'reg0'
-             * matches ip address of this router port, then the packet is
-             * intended to eventually be sent to this logical port. Set the
-             * destination mac address using this port's mac address.
+            /* This is a logical router port. If next-hop IP address in
+             * '[xx]reg0' matches IP address of this router port, then
+             * the packet is intended to eventually be sent to this
+             * logical port. Set the destination mac address using this
+             * port's mac address.
              *
              * The packet is still in peer's logical pipeline. So the match
              * should be on peer's outport. */
@@ -2610,23 +2781,38 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     continue;
                 }
 
-                ds_clear(&match);
-                ds_put_format(&match, "outport == %s && reg0 == ",
-                              peer->json_key);
-                op_put_networks(&match, op, false);
+                if (op->lrp_networks.n_ipv4_addrs) {
+                    ds_clear(&match);
+                    ds_put_format(&match, "outport == %s && reg0 == ",
+                                  peer->json_key);
+                    op_put_v4_networks(&match, op, false);
+
+                    ds_clear(&actions);
+                    ds_put_format(&actions, "eth.dst = %s; next;",
+                                  op->lrp_networks.ea_s);
+                    ovn_lflow_add(lflows, peer->od, S_ROUTER_IN_ARP_RESOLVE,
+                                  100, ds_cstr(&match), ds_cstr(&actions));
+                }
 
-                ds_clear(&actions);
-                ds_put_format(&actions, "eth.dst = %s; next;",
-                              op->lrp_networks.ea_s);
-                ovn_lflow_add(lflows, peer->od, S_ROUTER_IN_ARP_RESOLVE,
-                              100, ds_cstr(&match), ds_cstr(&actions));
+                if (op->lrp_networks.n_ipv6_addrs) {
+                    ds_clear(&match);
+                    ds_put_format(&match, "outport == %s && xxreg0 == ",
+                                  peer->json_key);
+                    op_put_v6_networks(&match, op);
+
+                    ds_clear(&actions);
+                    ds_put_format(&actions, "eth.dst = %s; next;",
+                                  op->lrp_networks.ea_s);
+                    ovn_lflow_add(lflows, peer->od, S_ROUTER_IN_ARP_RESOLVE,
+                                  100, ds_cstr(&match), ds_cstr(&actions));
+                }
             }
         } else if (op->od->n_router_ports && strcmp(op->nbs->type, "router")) {
             /* This is a logical switch port that backs a VM or a container.
              * Extract its addresses. For each of the address, go through all
              * the router ports attached to the switch (to which this port
              * connects) and if the address in question is reachable from the
-             * router port, add an ARP entry in that router's pipeline. */
+             * router port, add an ARP/ND entry in that router's pipeline. */
 
             for (size_t i = 0; i < op->n_lsp_addrs; i++) {
                 const char *ea_s = op->lsp_addrs[i].ea_s;
@@ -2663,6 +2849,40 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                                       ds_cstr(&match), ds_cstr(&actions));
                     }
                 }
+
+                for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
+                    const char *ip_s = op->lsp_addrs[i].ipv6_addrs[j].addr_s;
+                    for (size_t k = 0; k < op->od->n_router_ports; k++) {
+                        /* Get the Logical_Router_Port that the
+                         * Logical_Switch_Port is connected to, as
+                         * 'peer'. */
+                        const char *peer_name = smap_get(
+                            &op->od->router_ports[k]->nbs->options,
+                            "router-port");
+                        if (!peer_name) {
+                            continue;
+                        }
+
+                        struct ovn_port *peer = ovn_port_find(ports, peer_name);
+                        if (!peer || !peer->nbr) {
+                            continue;
+                        }
+
+                        if (!find_lrp_member_ip(peer, ip_s)) {
+                            continue;
+                        }
+
+                        ds_clear(&match);
+                        ds_put_format(&match, "outport == %s && xxreg0 == %s",
+                                      peer->json_key, ip_s);
+
+                        ds_clear(&actions);
+                        ds_put_format(&actions, "eth.dst = %s; next;", ea_s);
+                        ovn_lflow_add(lflows, peer->od,
+                                      S_ROUTER_IN_ARP_RESOLVE, 100,
+                                      ds_cstr(&match), ds_cstr(&actions));
+                    }
+                }
             }
         } else if (!strcmp(op->nbs->type, "router")) {
             /* This is a logical switch port that connects to a router. */
@@ -2698,16 +2918,31 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                    continue;
                 }
 
-                ds_clear(&match);
-                ds_put_format(&match, "outport == %s && reg0 == ",
-                              peer->json_key);
-                op_put_networks(&match, router_port, false);
+                if (router_port->lrp_networks.n_ipv4_addrs) {
+                    ds_clear(&match);
+                    ds_put_format(&match, "outport == %s && reg0 == ",
+                                  peer->json_key);
+                    op_put_v4_networks(&match, router_port, false);
+
+                    ds_clear(&actions);
+                    ds_put_format(&actions, "eth.dst = %s; next;",
+                                              router_port->lrp_networks.ea_s);
+                    ovn_lflow_add(lflows, peer->od, S_ROUTER_IN_ARP_RESOLVE,
+                                  100, ds_cstr(&match), ds_cstr(&actions));
+                }
 
-                ds_clear(&actions);
-                ds_put_format(&actions, "eth.dst = %s; next;",
-                              router_port->lrp_networks.ea_s);
-                ovn_lflow_add(lflows, peer->od, S_ROUTER_IN_ARP_RESOLVE,
-                              100, ds_cstr(&match), ds_cstr(&actions));
+                if (router_port->lrp_networks.n_ipv6_addrs) {
+                    ds_clear(&match);
+                    ds_put_format(&match, "outport == %s && xxreg0 == ",
+                                  peer->json_key);
+                    op_put_v6_networks(&match, router_port);
+
+                    ds_clear(&actions);
+                    ds_put_format(&actions, "eth.dst = %s; next;",
+                                  router_port->lrp_networks.ea_s);
+                    ovn_lflow_add(lflows, peer->od, S_ROUTER_IN_ARP_RESOLVE,
+                                  100, ds_cstr(&match), ds_cstr(&actions));
+                }
             }
         }
     }
