diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index d3e0e5ef2..f6cafbd55 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -1511,11 +1511,55 @@ output;
 
       <li>
         <p>
-          These flows reply to ARP requests for the virtual IP addresses
-          configured in the router for DNAT or load balancing.  For a
-          configured DNAT IP address or a load balancer IPv4 VIP <var>A</var>,
-          for each router port <var>P</var> with Ethernet
-          address <var>E</var>, a priority-90 flow matches
+          Reply to IPv6 Neighbor Solicitations.  These flows reply to
+          Neighbor Solicitation requests for the router's own IPv6
+          address and populate the logical router's mac binding table.
+        </p>
+
+        <p>
+          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>
+nd_na_router {
+    eth.src = <var>E</var>;
+    ip6.src = <var>A</var>;
+    nd.target = <var>A</var>;
+    nd.tll = <var>E</var>;
+    outport = inport;
+    flags.loopback = 1;
+    output;
+};
+        </pre>
+
+        <p>
+          For the gateway port on a distributed logical router (where
+          one of the logical router ports specifies a
+          <code>redirect-chassis</code>), the above flows replying to
+          IPv6 Neighbor Solicitations are only programmed on the
+          gateway port instance on the <code>redirect-chassis</code>.
+          This behavior avoids generation of multiple replies from
+          different chassis, and allows upstream MAC learning to point
+          to the <code>redirect-chassis</code>.
+        </p>
+      </li>
+
+      <li>
+        <p>
+          These flows reply to ARP requests or IPv6 neighbor solicitation
+          for the virtual IP addresses configured in the router for DNAT
+          or load balancing.
+        </p>
+
+        <p>
+          IPv4: For a configured DNAT IP address or a load balancer
+          IPv4 VIP <var>A</var>, for each router port <var>P</var> with
+          Ethernet address <var>E</var>, a priority-90 flow matches
           <code>inport == <var>P</var> &amp;&amp; arp.op == 1 &amp;&amp;
           arp.tpa == <var>A</var></code> (ARP request)
           with the following actions:
@@ -1534,6 +1578,30 @@ flags.loopback = 1;
 output;
         </pre>
 
+        <p>
+          IPv6: For a configured DNAT IP address or a load balancer
+          IPv6 VIP <var>A</var>, solicited node address <var>S</var>,
+          for each router port <var>P</var> with
+          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>S</var>} &amp;&amp;
+          nd.target == <var>A</var></code>
+          with the following actions:
+        </p>
+
+        <pre>
+eth.dst = eth.src;
+nd_na {
+    eth.src = <var>E</var>;
+    nd.tll = <var>E</var>;
+    ip6.src = <var>A</var>;
+    nd.target = <var>A</var>;
+    outport = <var>P</var>;
+    flags.loopback = 1;
+    output;
+}
+        </pre>
+
         <p>
           For the gateway port on a distributed logical router with NAT
           (where one of the logical router ports specifies a
@@ -1570,6 +1638,15 @@ eth.src = <var>external_mac</var>;
 arp.sha = <var>external_mac</var>;
             </pre>
 
+            <p>
+              or in the case of IPv6 neighbor solicition:
+            </p>
+
+            <pre>
+eth.src = <var>external_mac</var>;
+nd.tll = <var>external_mac</var>;
+            </pre>
+
             <p>
               This behavior avoids generation of multiple ARP responses
               from different chassis, and allows upstream MAC learning to
@@ -1579,68 +1656,6 @@ arp.sha = <var>external_mac</var>;
         </ul>
       </li>
 
-      <li>
-        <p>
-          Reply to IPv6 Neighbor Solicitations.  These flows reply to
-          Neighbor Solicitation requests for the router's own IPv6
-          address and load balancing IPv6 VIPs and populate the logical
-          router's mac binding table.
-        </p>
-
-        <p>
-          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>
-nd_na_router {
-    eth.src = <var>E</var>;
-    ip6.src = <var>A</var>;
-    nd.target = <var>A</var>;
-    nd.tll = <var>E</var>;
-    outport = inport;
-    flags.loopback = 1;
-    output;
-};
-        </pre>
-
-        <p>
-          For each router port <var>P</var> that has load balancing VIP
-          <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>
-nd_na {
-    eth.src = <var>E</var>;
-    ip6.src = <var>A</var>;
-    nd.target = <var>A</var>;
-    nd.tll = <var>E</var>;
-    outport = inport;
-    flags.loopback = 1;
-    output;
-};
-        </pre>
-
-        <p>
-          For the gateway port on a distributed logical router (where
-          one of the logical router ports specifies a
-          <code>redirect-chassis</code>), the above flows replying to
-          IPv6 Neighbor Solicitations are only programmed on the
-          gateway port instance on the <code>redirect-chassis</code>.
-          This behavior avoids generation of multiple replies from
-          different chassis, and allows upstream MAC learning to point
-          to the <code>redirect-chassis</code>.
-        </p>
-      </li>
-
       <li>
         Priority-85 flows which drops the ARP and IPv6 Neighbor Discovery
         packets.
@@ -1694,10 +1709,11 @@ nd_na {
         handled by one of the flows above, which amounts to ICMP (other than
         echo requests) and fragments with nonzero offsets.  For each IP address
         <var>A</var> owned by the router, a priority-60 flow matches
-        <code>ip4.dst == <var>A</var></code> and drops the traffic.  An
-        exception is made and the above flow is not added if the router
-        port's own IP address is used to SNAT packets passing through that
-        router.
+        <code>ip4.dst == <var>A</var></code> or
+        <code>ip6.dst == <var>A</var></code>
+        and drops the traffic.  An exception is made and the above flow
+        is not added if the router port's own IP address is used to SNAT
+        packets passing through that router.
       </li>
     </ul>
 
@@ -1791,23 +1807,26 @@ icmp6 {
         <p>
           If the Gateway router has been configured to force SNAT any
           previously DNATted packets to <var>B</var>, a priority-110 flow
-          matches <code>ip &amp;&amp; ip4.dst == <var>B</var></code> with
-          an action <code>ct_snat; </code>.
+          matches <code>ip &amp;&amp; ip4.dst == <var>B</var></code> or
+          <code>ip &amp;&amp; ip6.dst == <var>B</var></code>
+          with an action <code>ct_snat; </code>.
         </p>
 
         <p>
           If the Gateway router has been configured to force SNAT any
           previously load-balanced packets to <var>B</var>, a priority-100 flow
-          matches <code>ip &amp;&amp; ip4.dst == <var>B</var></code> with
-          an action <code>ct_snat; </code>.
+          matches <code>ip &amp;&amp; ip4.dst == <var>B</var></code> or
+          <code>ip &amp;&amp; ip6.dst == <var>B</var></code>
+          with an action <code>ct_snat; </code>.
         </p>
 
         <p>
           For each NAT configuration in the OVN Northbound database, that asks
           to change the source IP address of a packet from <var>A</var> to
-          <var>B</var>, a priority-90 flow matches <code>ip &amp;&amp;
-          ip4.dst == <var>B</var></code> with an action
-          <code>ct_snat; </code>.
+          <var>B</var>, a priority-90 flow matches
+          <code>ip &amp;&amp; ip4.dst == <var>B</var></code> or
+          <code>ip &amp;&amp; ip6.dst == <var>B</var></code>
+          with an action <code>ct_snat; </code>.
         </p>
 
         <p>
@@ -1825,7 +1844,9 @@ icmp6 {
           For each configuration in the OVN Northbound database, that asks
           to change the source IP address of a packet from <var>A</var> to
           <var>B</var>, a priority-100 flow matches <code>ip &amp;&amp;
-          ip4.dst == <var>B</var> &amp;&amp; inport == <var>GW</var></code>,
+          ip4.dst == <var>B</var> &amp;&amp; inport == <var>GW</var></code> or
+          <code>ip &amp;&amp;
+          ip6.dst == <var>B</var> &amp;&amp; inport == <var>GW</var></code>
           where <var>GW</var> is the logical router gateway port, with an
           action <code>ct_snat;</code>.
         </p>
@@ -1839,10 +1860,12 @@ icmp6 {
         <p>
           For each configuration in the OVN Northbound database, that asks
           to change the source IP address of a packet from <var>A</var> to
-          <var>B</var>, a priority-50 flow matches <code>ip &amp;&amp;
-          ip4.dst == <var>B</var></code> with an action
+          <var>B</var>, a priority-50 flow matches
+          <code>ip &amp;&amp; ip4.dst == <var>B</var></code> or
+          <code>ip &amp;&amp; ip6.dst == <var>B</var></code>
+          with an action
           <code>REGBIT_NAT_REDIRECT = 1; next;</code>.  This flow is for
-          east/west traffic to a NAT destination IPv4 address.  By
+          east/west traffic to a NAT destination IPv4/IPv6 address.  By
           setting the <code>REGBIT_NAT_REDIRECT</code> flag, in the
           ingress table <code>Gateway Redirect</code> this will trigger a
           redirect to the instance of the gateway port on the
@@ -1888,25 +1911,27 @@ icmp6 {
         For all the configured load balancing rules for a Gateway router or
         Router with gateway port in <code>OVN_Northbound</code> database that
         includes a L4 port <var>PORT</var> of protocol <var>P</var> and IPv4
-        address <var>VIP</var>, a priority-120 flow that matches on
+        or IPv6 address <var>VIP</var>, a priority-120 flow that matches on
         <code>ct.new &amp;&amp; ip &amp;&amp; ip4.dst == <var>VIP</var>
         &amp;&amp; <var>P</var> &amp;&amp; <var>P</var>.dst == <var>PORT
-        </var></code> with an action of <code>ct_lb(<var>args</var>)</code>,
-        where <var>args</var> contains comma separated IPv4 addresses (and
-        optional port numbers) to load balance to.  If the router is configured
-        to force SNAT any load-balanced packets, the above action will be
-        replaced by <code>flags.force_snat_for_lb = 1;
+        </var></code> (<code>ip6.dst == <var>VIP</var></code> in the IPv6 case)
+        with an action of <code>ct_lb(<var>args</var>)</code>,
+        where <var>args</var> contains comma separated IPv4 or IPv6 addresses
+        (and optional port numbers) to load balance to.  If the router is
+        configured to force SNAT any load-balanced packets, the above action
+        will be replaced by <code>flags.force_snat_for_lb = 1;
         ct_lb(<var>args</var>);</code>.
       </li>
 
       <li>
         For all the configured load balancing rules for a router in
         <code>OVN_Northbound</code> database that includes a L4 port
-        <var>PORT</var> of protocol <var>P</var> and IPv4 address
+        <var>PORT</var> of protocol <var>P</var> and IPv4 or IPv6 address
         <var>VIP</var>, a priority-120 flow that matches on
         <code>ct.est &amp;&amp; ip &amp;&amp; ip4.dst == <var>VIP</var>
         &amp;&amp; <var>P</var> &amp;&amp; <var>P</var>.dst == <var>PORT
-        </var></code> with an action of <code>ct_dnat;</code>. If the router is
+        </var></code> (<code>ip6.dst == <var>VIP</var></code> in the IPv6 case)
+        with an action of <code>ct_dnat;</code>. If the router is
         configured to force SNAT any load-balanced packets, the above action
         will be replaced by <code>flags.force_snat_for_lb = 1; ct_dnat;</code>.
       </li>
@@ -1916,11 +1941,13 @@ icmp6 {
         <code>OVN_Northbound</code> database that includes just an IP address
         <var>VIP</var> to match on, a priority-110 flow that matches on
         <code>ct.new &amp;&amp; ip &amp;&amp; ip4.dst ==
-        <var>VIP</var></code> with an action of
+        <var>VIP</var></code> (<code>ip6.dst == <var>VIP</var></code> in the
+        IPv6 case) with an action of
         <code>ct_lb(<var>args</var>)</code>, where <var>args</var> contains
-        comma separated IPv4 addresses.  If the router is configured to force
-        SNAT any load-balanced packets, the above action will be replaced by
-        <code>flags.force_snat_for_lb = 1; ct_lb(<var>args</var>);</code>.
+        comma separated IPv4 or IPv6 addresses.  If the router is configured
+        to force SNAT any load-balanced packets, the above action will be
+        replaced by <code>flags.force_snat_for_lb = 1;
+        ct_lb(<var>args</var>);</code>.
       </li>
 
       <li>
@@ -1928,7 +1955,8 @@ icmp6 {
         <code>OVN_Northbound</code> database that includes just an IP address
         <var>VIP</var> to match on, a priority-110 flow that matches on
         <code>ct.est &amp;&amp; ip &amp;&amp; ip4.dst ==
-        <var>VIP</var></code> with an action of <code>ct_dnat;</code>.
+        <var>VIP</var></code> (or <code>ip6.dst == <var>VIP</var></code>)
+        with an action of <code>ct_dnat;</code>.
         If the router is configured to force SNAT any load-balanced
         packets, the above action will be replaced by
         <code>flags.force_snat_for_lb = 1; ct_dnat;</code>.
@@ -1942,7 +1970,8 @@ icmp6 {
         For each configuration in the OVN Northbound database, that asks
         to change the destination IP address of a packet from <var>A</var> to
         <var>B</var>, a priority-100 flow matches <code>ip &amp;&amp;
-        ip4.dst == <var>A</var></code> with an action
+        ip4.dst == <var>A</var></code> or <code>ip &amp;&amp;
+        ip6.dst == <var>A</var></code> with an action
         <code>flags.loopback = 1; ct_dnat(<var>B</var>);</code>.  If the
         Gateway router is configured to force SNAT any DNATed packet,
         the above action will be replaced by
@@ -1979,7 +2008,8 @@ icmp6 {
           <var>B</var>, a priority-100 flow matches <code>ip &amp;&amp;
           ip4.dst == <var>B</var> &amp;&amp; inport == <var>GW</var></code>,
           where <var>GW</var> is the logical router gateway port, with an
-          action <code>ct_dnat(<var>B</var>);</code>.
+          action <code>ct_dnat(<var>B</var>);</code>.  The match will
+          include <code>ip6.dst == <var>B</var></code> in the IPv6 case.
         </p>
 
         <p>
@@ -1992,9 +2022,10 @@ icmp6 {
           For each configuration in the OVN Northbound database, that asks
           to change the destination IP address of a packet from <var>A</var> to
           <var>B</var>, a priority-50 flow matches <code>ip &amp;&amp;
-          ip4.dst == <var>B</var></code> with an action
+          ip4.dst == <var>B</var></code> or <code>ip &amp;&amp;
+          ip6.dst == <var>B</var></code> with an action
           <code>REGBIT_NAT_REDIRECT = 1; next;</code>.  This flow is for
-          east/west traffic to a NAT destination IPv4 address.  By
+          east/west traffic to a NAT destination IPv4/IPv6 address.  By
           setting the <code>REGBIT_NAT_REDIRECT</code> flag, in the
           ingress table <code>Gateway Redirect</code> this will trigger a
           redirect to the instance of the gateway port on the
@@ -2149,8 +2180,8 @@ logical_ip{0,1} = <var>LIP{0,1}</var>;
         <pre>
 eth.dst = <var>MAC0</var>;
 eth.src = <var>MAC1</var>;
-reg0 = ip4.dst;
-reg1 = <var>EIP1</var>;
+reg0 = ip4.dst; /* xxreg0 = ip6.dst; in the IPv6 case */
+reg1 = <var>EIP1</var>; /* xxreg1 in the IPv6 case */
 outport = <code>redirect-chassis-port</code>;
 <code>REGBIT_DISTRIBUTED_NAT = 1; next;</code>.
         </pre>
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index ae81a6944..71ee0b678 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -66,6 +66,15 @@ struct northd_context {
     struct ovsdb_idl_index *sbrec_ip_mcast_by_dp;
 };
 
+/* An IPv4 or IPv6 address */
+struct v46_ip {
+    int family;
+    union {
+        ovs_be32 ipv4;
+        struct in6_addr ipv6;
+    };
+};
+
 static const char *ovnnb_db;
 static const char *ovnsb_db;
 static const char *unixctl_path;
@@ -2273,6 +2282,15 @@ get_nat_addresses(const struct ovn_port *op, size_t *n)
                     break;
                 }
             }
+            if (!is_router_ip) {
+                for (size_t j = 0; j < op->lrp_networks.n_ipv6_addrs; j++) {
+                    if (!strcmp(nat->external_ip,
+                                op->lrp_networks.ipv6_addrs[j].addr_s)) {
+                        is_router_ip = true;
+                        break;
+                    }
+                }
+            }
 
             if (!is_router_ip) {
                 ds_put_format(&c_addresses, " %s", nat->external_ip);
@@ -6031,9 +6049,28 @@ add_distributed_nat_routes(struct hmap *lflows, const struct ovn_port *op)
             continue;
         }
 
+        /* Determine if we need to create IPv4 or IPv6 flows */
+        ovs_be32 ip;
+        struct in6_addr ipv6;
+        int family = AF_INET;
+        if (!ip_parse(nat->external_ip, &ip) || !ip) {
+            family = AF_INET6;
+            if (!ipv6_parse(nat->external_ip, &ipv6)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "bad ip address %s in nat configuration "
+                             "for router %s", nat->external_ip, op->key);
+                /* We'll create IPv6 flows anyway, but the address
+                 * is probably bogus ... */
+            }
+        }
+
         ds_put_format(&match, "inport == %s && "
-                      "ip4.src == %s && ip4.dst == %s",
-                       op->json_key, nat->logical_ip, nat->external_ip);
+                      "ip%s.src == %s && ip%s.dst == %s",
+                       op->json_key,
+                       family == AF_INET ? "4" : "6",
+                       nat->logical_ip,
+                       family == AF_INET ? "4" : "6",
+                       nat->external_ip);
         ds_put_format(&actions, "outport = %s; eth.dst = %s; "
                       REGBIT_DISTRIBUTED_NAT" = 1; "
                       REGBIT_NAT_REDIRECT" = 0; next;",
@@ -6051,17 +6088,38 @@ add_distributed_nat_routes(struct hmap *lflows, const struct ovn_port *op)
                 !nat2->external_mac || !nat2->external_ip)
                 continue;
 
+            family = AF_INET;
+            if (!ip_parse(nat2->external_ip, &ip) || !ip) {
+                family = AF_INET6;
+                if (!ipv6_parse(nat2->external_ip, &ipv6)) {
+                    static struct vlog_rate_limit rl =
+                        VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "bad ip address %s in nat configuration "
+                                 "for router %s", nat2->external_ip, op->key);
+                    /* We'll create IPv6 flows anyway, but the address
+                     * is probably bogus ... */
+                }
+            }
+
             ds_put_format(&match, "inport == %s && "
-                          "ip4.src == %s && ip4.dst == %s",
-                          op->json_key, nat->logical_ip, nat2->external_ip);
+                          "ip%s.src == %s && ip%s.dst == %s",
+                          op->json_key,
+                          family == AF_INET ? "4" : "6",
+                          nat->logical_ip,
+                          family == AF_INET ? "4" : "6",
+                          nat2->external_ip);
             ds_put_format(&actions, "outport = %s; "
                           "eth.src = %s; eth.dst = %s; "
-                          "reg0 = ip4.dst; reg1 = %s; "
+                          "%sreg0 = ip%s.dst; %sreg1 = %s; "
                           REGBIT_DISTRIBUTED_NAT" = 1; "
                           REGBIT_NAT_REDIRECT" = 0; next;",
                           op->od->l3dgw_port->json_key,
                           op->od->l3dgw_port->lrp_networks.ea_s,
-                          nat2->external_mac, nat->external_ip);
+                          nat2->external_mac,
+                          family == AF_INET ? "" : "xx",
+                          family == AF_INET ? "4" : "6",
+                          family == AF_INET ? "" : "xx",
+                          nat->external_ip);
             ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_ROUTING, 400,
                           ds_cstr(&match), ds_cstr(&actions));
             ds_clear(&match);
@@ -6298,7 +6356,8 @@ op_put_v6_networks(struct ds *ds, const struct ovn_port *op)
 }
 
 static const char *
-get_force_snat_ip(struct ovn_datapath *od, const char *key_type, ovs_be32 *ip)
+get_force_snat_ip(struct ovn_datapath *od, const char *key_type,
+                  struct v46_ip *ip)
 {
     char *key = xasprintf("%s_force_snat_ip", key_type);
     const char *ip_address = smap_get(&od->nbr->options, key);
@@ -6306,19 +6365,27 @@ get_force_snat_ip(struct ovn_datapath *od, const char *key_type, ovs_be32 *ip)
 
     if (ip_address) {
         ovs_be32 mask;
-        char *error = ip_parse_masked(ip_address, ip, &mask);
+        ip->family = AF_INET;
+        char *error = ip_parse_masked(ip_address, &ip->ipv4, &mask);
         if (error || mask != OVS_BE32_MAX) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_WARN_RL(&rl, "bad ip %s in options of router "UUID_FMT"",
-                         ip_address, UUID_ARGS(&od->key));
             free(error);
-            *ip = 0;
-            return NULL;
+            struct in6_addr mask_v6, v6_exact = IN6ADDR_EXACT_INIT;
+            ip->family = AF_INET6;
+            error = ipv6_parse_masked(ip_address, &ip->ipv6, &mask_v6);
+            if (error || memcmp(&mask_v6, &v6_exact, sizeof(mask_v6))) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "bad ip %s in options of router "UUID_FMT"",
+                             ip_address, UUID_ARGS(&od->key));
+                memset(ip, 0, sizeof *ip);
+                ip->family = AF_UNSPEC;
+                return NULL;
+            }
         }
         return ip_address;
     }
 
-    *ip = 0;
+    memset(ip, 0, sizeof *ip);
+    ip->family = AF_UNSPEC;
     return NULL;
 }
 
@@ -6884,11 +6951,11 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         /* A gateway router can have 2 SNAT IP addresses to force DNATed and
          * LBed traffic respectively to be SNATed.  In addition, there can be
          * a number of SNAT rules in the NAT table. */
-        ovs_be32 *snat_ips = xmalloc(sizeof *snat_ips *
-                                     (op->od->nbr->n_nat + 2));
+        struct v46_ip *snat_ips = xmalloc(sizeof *snat_ips
+                                          * (op->od->nbr->n_nat + 2));
         size_t n_snat_ips = 0;
 
-        ovs_be32 snat_ip;
+        struct v46_ip snat_ip;
         const char *dnat_force_snat_ip = get_force_snat_ip(op->od, "dnat",
                                                            &snat_ip);
         if (dnat_force_snat_ip) {
@@ -6907,44 +6974,85 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             nat = op->od->nbr->nat[i];
 
             ovs_be32 ip;
+            struct in6_addr ipv6;
+            bool is_v6 = false;
             if (!ip_parse(nat->external_ip, &ip) || !ip) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "bad ip address %s in nat configuration "
-                             "for router %s", nat->external_ip, op->key);
-                continue;
+                if (!ipv6_parse(nat->external_ip, &ipv6)) {
+                    static struct vlog_rate_limit rl =
+                        VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "bad ip address %s in nat configuration "
+                                 "for router %s", nat->external_ip, op->key);
+                    continue;
+                }
+                is_v6 = true;
             }
 
             if (!strcmp(nat->type, "snat")) {
-                snat_ips[n_snat_ips++] = ip;
+                if (is_v6) {
+                    snat_ips[n_snat_ips].family = AF_INET6;
+                    snat_ips[n_snat_ips++].ipv6 = ipv6;
+                } else {
+                    snat_ips[n_snat_ips].family = AF_INET;
+                    snat_ips[n_snat_ips++].ipv4 = ip;
+                }
                 continue;
             }
 
-            /* ARP handling for external IP addresses.
+            /* ARP / ND handling for external IP addresses.
              *
              * DNAT IP addresses are external IP addresses that need ARP
              * handling. */
+            char addr_s[INET6_ADDRSTRLEN + 1];
             ds_clear(&match);
-            ds_put_format(&match,
-                          "inport == %s && arp.tpa == "IP_FMT" && arp.op == 1",
-                          op->json_key, IP_ARGS(ip));
-
             ds_clear(&actions);
-            ds_put_format(&actions,
-                "eth.dst = eth.src; "
-                "arp.op = 2; /* ARP reply */ "
-                "arp.tha = arp.sha; ");
+            if (is_v6) {
+                /* 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. */
+                char sn_addr_s[INET6_ADDRSTRLEN + 1];
+                struct in6_addr sn_addr;
+                in6_addr_solicited_node(&sn_addr, &ipv6);
+                ipv6_string_mapped(sn_addr_s, &sn_addr);
+                ipv6_string_mapped(addr_s, &ipv6);
+
+                ds_put_format(&match, "inport == %s && "
+                        "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
+                        op->json_key, addr_s, sn_addr_s, addr_s);
 
+                ds_put_format(&actions,
+                    "eth.dst = eth.src; "
+                    "nd_na { ");
+            } else {
+                ds_put_format(&match,
+                              "inport == %s "
+                              "&& arp.tpa == "IP_FMT" && arp.op == 1",
+                              op->json_key, IP_ARGS(ip));
+
+                ds_put_format(&actions,
+                    "eth.dst = eth.src; "
+                    "arp.op = 2; /* ARP reply */ "
+                    "arp.tha = arp.sha; ");
+            }
             if (op->od->l3dgw_port && op == op->od->l3dgw_port) {
                 struct eth_addr mac;
                 if (nat->external_mac &&
                     eth_addr_from_string(nat->external_mac, &mac)
                     && nat->logical_port) {
                     /* distributed NAT case, use nat->external_mac */
-                    ds_put_format(&actions,
-                        "eth.src = "ETH_ADDR_FMT"; "
-                        "arp.sha = "ETH_ADDR_FMT"; ",
-                        ETH_ADDR_ARGS(mac),
-                        ETH_ADDR_ARGS(mac));
+                    if (is_v6) {
+                        ds_put_format(&actions,
+                            "eth.src = "ETH_ADDR_FMT"; "
+                            "nd.tll = "ETH_ADDR_FMT"; ",
+                            ETH_ADDR_ARGS(mac),
+                            ETH_ADDR_ARGS(mac));
+
+                    } else {
+                        ds_put_format(&actions,
+                            "eth.src = "ETH_ADDR_FMT"; "
+                            "arp.sha = "ETH_ADDR_FMT"; ",
+                            ETH_ADDR_ARGS(mac),
+                            ETH_ADDR_ARGS(mac));
+                    }
                     /* Traffic with eth.src = nat->external_mac should only be
                      * sent from the chassis where nat->logical_port is
                      * resident, so that upstream MAC learning points to the
@@ -6953,11 +7061,20 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     ds_put_format(&match, " && is_chassis_resident(\"%s\")",
                                   nat->logical_port);
                 } else {
-                    ds_put_format(&actions,
-                        "eth.src = %s; "
-                        "arp.sha = %s; ",
-                        op->lrp_networks.ea_s,
-                        op->lrp_networks.ea_s);
+                    if (is_v6) {
+                        ds_put_format(&actions,
+                            "eth.src = %s; "
+                            "nd.tll = %s; ",
+                            op->lrp_networks.ea_s,
+                            op->lrp_networks.ea_s);
+
+                    } else {
+                        ds_put_format(&actions,
+                            "eth.src = %s; "
+                            "arp.sha = %s; ",
+                            op->lrp_networks.ea_s,
+                            op->lrp_networks.ea_s);
+                    }
                     /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
                      * should only be sent from the "redirect-chassis", so that
                      * upstream MAC learning points to the "redirect-chassis".
@@ -6968,21 +7085,40 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                                       op->od->l3redirect_port->json_key);
                     }
                 }
+            } else {
+                if (is_v6) {
+                    ds_put_format(&actions,
+                        "eth.src = %s; "
+                        "nd.tll = %s; ",
+                        op->lrp_networks.ea_s,
+                        op->lrp_networks.ea_s);
+                } else {
+                    ds_put_format(&actions,
+                        "eth.src = %s; "
+                        "arp.sha = %s; ",
+                        op->lrp_networks.ea_s,
+                        op->lrp_networks.ea_s);
+                }
+            }
+            if (is_v6) {
+                ds_put_format(&actions,
+                    "ip6.src = %s; "
+                    "nd.target = %s; "
+                    "outport = %s; "
+                    "flags.loopback = 1; "
+                    "output; "
+                    "};",
+                    addr_s, addr_s, op->json_key);
             } else {
                 ds_put_format(&actions,
-                    "eth.src = %s; "
-                    "arp.sha = %s; ",
-                    op->lrp_networks.ea_s,
-                    op->lrp_networks.ea_s);
+                    "arp.tpa = arp.spa; "
+                    "arp.spa = "IP_FMT"; "
+                    "outport = %s; "
+                    "flags.loopback = 1; "
+                    "output;",
+                    IP_ARGS(ip),
+                    op->json_key);
             }
-            ds_put_format(&actions,
-                "arp.tpa = arp.spa; "
-                "arp.spa = "IP_FMT"; "
-                "outport = %s; "
-                "flags.loopback = 1; "
-                "output;",
-                IP_ARGS(ip),
-                op->json_key);
             ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
                           ds_cstr(&match), ds_cstr(&actions));
         }
@@ -7039,7 +7175,36 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             bool snat_ip_is_router_ip = false;
             for (int j = 0; j < n_snat_ips; j++) {
                 /* Packets to SNAT IPs should not be dropped. */
-                if (op->lrp_networks.ipv4_addrs[i].addr == snat_ips[j]) {
+                if (snat_ips[j].family == AF_INET
+                    && op->lrp_networks.ipv4_addrs[i].addr
+                       == snat_ips[j].ipv4) {
+                        snat_ip_is_router_ip = true;
+                        break;
+                }
+            }
+            if (snat_ip_is_router_ip) {
+                continue;
+            }
+            ds_put_format(&match, "%s, ",
+                          op->lrp_networks.ipv4_addrs[i].addr_s);
+            has_drop_ips = true;
+        }
+        if (has_drop_ips) {
+            ds_chomp(&match, ' ');
+            ds_chomp(&match, ',');
+            ds_put_cstr(&match, "} || ip6.dst == {");
+        } else {
+            ds_clear(&match);
+            ds_put_cstr(&match, "ip6.dst == {");
+        }
+
+        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
+            bool snat_ip_is_router_ip = false;
+            for (int j = 0; j < n_snat_ips; j++) {
+                /* Packets to SNAT IPs should not be dropped. */
+                if (snat_ips[j].family == AF_INET6
+                    && !memcmp(&op->lrp_networks.ipv6_addrs[i].addr,
+                               &snat_ips[j].ipv6, sizeof snat_ips[j].ipv6)) {
                     snat_ip_is_router_ip = true;
                     break;
                 }
@@ -7048,9 +7213,10 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 continue;
             }
             ds_put_format(&match, "%s, ",
-                          op->lrp_networks.ipv4_addrs[i].addr_s);
+                          op->lrp_networks.ipv6_addrs[i].addr_s);
             has_drop_ips = true;
         }
+
         ds_chomp(&match, ' ');
         ds_chomp(&match, ',');
         ds_put_cstr(&match, "}");
@@ -7077,14 +7243,6 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         }
 
         if (op->lrp_networks.n_ipv6_addrs) {
-            /* L3 admission control: drop packets that originate from an
-             * IPv6 address owned by 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);
@@ -7101,13 +7259,6 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                         "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
@@ -7249,11 +7400,11 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
-        ovs_be32 snat_ip;
+        struct v46_ip snat_ip, lb_snat_ip;
         const char *dnat_force_snat_ip = get_force_snat_ip(od, "dnat",
                                                            &snat_ip);
         const char *lb_force_snat_ip = get_force_snat_ip(od, "lb",
-                                                         &snat_ip);
+                                                         &lb_snat_ip);
 
         for (int i = 0; i < od->nbr->n_nat; i++) {
             const struct nbrec_nat *nat;
@@ -7261,21 +7412,38 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             nat = od->nbr->nat[i];
 
             ovs_be32 ip, mask;
+            struct in6_addr ipv6, mask_v6, v6_exact = IN6ADDR_EXACT_INIT;
+            bool is_v6 = false;
 
             char *error = ip_parse_masked(nat->external_ip, &ip, &mask);
             if (error || mask != OVS_BE32_MAX) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "bad external ip %s for nat",
-                             nat->external_ip);
                 free(error);
-                continue;
+                error = ipv6_parse_masked(nat->external_ip, &ipv6, &mask_v6);
+                if (error || memcmp(&mask_v6, &v6_exact, sizeof(mask_v6))) {
+                    /* Invalid for both IPv4 and IPv6 */
+                    static struct vlog_rate_limit rl =
+                        VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "bad external ip %s for nat",
+                                 nat->external_ip);
+                    free(error);
+                    continue;
+                }
+                /* It was an invalid IPv4 address, but valid IPv6.
+                 * Treat the rest of the handling of this NAT rule
+                 * as IPv6. */
+                is_v6 = true;
             }
 
             /* Check the validity of nat->logical_ip. 'logical_ip' can
              * be a subnet when the type is "snat". */
-            error = ip_parse_masked(nat->logical_ip, &ip, &mask);
+            if (is_v6) {
+                error = ipv6_parse_masked(nat->external_ip, &ipv6, &mask_v6);
+            } else {
+                error = ip_parse_masked(nat->logical_ip, &ip, &mask);
+            }
             if (!strcmp(nat->type, "snat")) {
                 if (error) {
+                    /* Invalid for both IPv4 and IPv6 */
                     static struct vlog_rate_limit rl =
                         VLOG_RATE_LIMIT_INIT(5, 1);
                     VLOG_WARN_RL(&rl, "bad ip network or ip %s for snat "
@@ -7285,7 +7453,10 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     continue;
                 }
             } else {
-                if (error || mask != OVS_BE32_MAX) {
+                if (error || (!is_v6 && mask != OVS_BE32_MAX)
+                    || (is_v6 && memcmp(&mask_v6, &v6_exact,
+                                        sizeof mask_v6))) {
+                    /* Invalid for both IPv4 and IPv6 */
                     static struct vlog_rate_limit rl =
                         VLOG_RATE_LIMIT_INIT(5, 1);
                     VLOG_WARN_RL(&rl, "bad ip %s for dnat in router "
@@ -7326,7 +7497,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 if (!od->l3dgw_port) {
                     /* Gateway router. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.dst == %s",
+                    ds_put_format(&match, "ip && ip%s.dst == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->external_ip);
                     ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 90,
                                   ds_cstr(&match), "ct_snat;");
@@ -7335,8 +7507,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
                     /* Traffic received on l3dgw_port is subject to NAT. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.dst == %s"
+                    ds_put_format(&match, "ip && ip%s.dst == %s"
                                           " && inport == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->external_ip,
                                   od->l3dgw_port->json_key);
                     if (!distributed && od->l3redirect_port) {
@@ -7352,7 +7525,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                      * redirected to the central instance of the l3dgw_port
                      * for NAT processing. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.dst == %s",
+                    ds_put_format(&match, "ip && ip%s.dst == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->external_ip);
                     ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 50,
                                   ds_cstr(&match),
@@ -7371,7 +7545,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                      * We need to set flags.loopback because the router can
                      * send the packet back through the same interface. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.dst == %s",
+                    ds_put_format(&match, "ip && ip%s.dst == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->external_ip);
                     ds_clear(&actions);
                     if (dnat_force_snat_ip) {
@@ -7390,8 +7565,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
                     /* Traffic received on l3dgw_port is subject to NAT. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.dst == %s"
+                    ds_put_format(&match, "ip && ip%s.dst == %s"
                                           " && inport == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->external_ip,
                                   od->l3dgw_port->json_key);
                     if (!distributed && od->l3redirect_port) {
@@ -7410,7 +7586,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                      * redirected to the central instance of the l3dgw_port
                      * for NAT processing. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.dst == %s",
+                    ds_put_format(&match, "ip && ip%s.dst == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->external_ip);
                     ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 50,
                                   ds_cstr(&match),
@@ -7429,8 +7606,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
             if (od->l3dgw_port && (!strcmp(nat->type, "dnat")
                 || !strcmp(nat->type, "dnat_and_snat"))) {
                 ds_clear(&match);
-                ds_put_format(&match, "ip && ip4.src == %s"
+                ds_put_format(&match, "ip && ip%s.src == %s"
                                       " && outport == %s",
+                              is_v6 ? "6" : "4",
                               nat->logical_ip,
                               od->l3dgw_port->json_key);
                 if (!distributed && od->l3redirect_port) {
@@ -7457,7 +7635,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 if (!od->l3dgw_port) {
                     /* Gateway router. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.src == %s",
+                    ds_put_format(&match, "ip && ip%s.src == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->logical_ip);
                     ds_clear(&actions);
                     ds_put_format(&actions, "ct_snat(%s);", nat->external_ip);
@@ -7473,8 +7652,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
                     /* Distributed router. */
                     ds_clear(&match);
-                    ds_put_format(&match, "ip && ip4.src == %s"
+                    ds_put_format(&match, "ip && ip%s.src == %s"
                                           " && outport == %s",
+                                  is_v6 ? "6" : "4",
                                   nat->logical_ip,
                                   od->l3dgw_port->json_key);
                     if (!distributed && od->l3redirect_port) {
@@ -7523,7 +7703,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
              * can be applied in a distributed manner. */
             if (distributed) {
                 ds_clear(&match);
-                ds_put_format(&match, "ip4.src == %s && outport == %s",
+                ds_put_format(&match, "ip%s.src == %s && outport == %s",
+                              is_v6 ? "6" : "4",
                               nat->logical_ip,
                               od->l3dgw_port->json_key);
                 ovn_lflow_add(lflows, od, S_ROUTER_IN_GW_REDIRECT, 100,
@@ -7550,9 +7731,10 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
                         ds_clear(&match);
                         ds_put_format(&match, "is_chassis_resident(\"%s\") && "
-                                      "ip4.src == %s && ip4.dst == %s",
-                                      nat->logical_port, nat2->external_ip,
-                                      nat->external_ip);
+                                      "ip%s.src == %s && ip%s.dst == %s",
+                                      nat->logical_port,
+                                      is_v6 ? "6" : "4", nat2->external_ip,
+                                      is_v6 ? "6" : "4", nat->external_ip);
                         ds_clear(&actions);
                         ds_put_format(&actions,
                                       "inport = outport; outport = \"\"; "
@@ -7564,8 +7746,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
 
                         ds_clear(&match);
                         ds_put_format(&match,
-                                      "ip4.src == %s && ip4.dst == %s",
-                                      nat2->external_ip, nat->external_ip);
+                                      "ip%s.src == %s && ip%s.dst == %s",
+                                      is_v6 ? "6" : "4", nat2->external_ip,
+                                      is_v6 ? "6" : "4", nat->external_ip);
                         ovn_lflow_add(lflows, od, S_ROUTER_OUT_EGR_LOOP, 200,
                                       ds_cstr(&match), "next;");
                         ds_clear(&match);
@@ -7573,7 +7756,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 }
 
                 ds_clear(&match);
-                ds_put_format(&match, "ip4.dst == %s && outport == %s",
+                ds_put_format(&match, "ip%s.dst == %s && outport == %s",
+                              is_v6 ? "6" : "4",
                               nat->external_ip,
                               od->l3dgw_port->json_key);
                 ds_clear(&actions);
@@ -7597,7 +7781,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
              * gateway router (as set in options:dnat_force_snat_ip) is seen,
              * UNSNAT it. */
             ds_clear(&match);
-            ds_put_format(&match, "ip && ip4.dst == %s", dnat_force_snat_ip);
+            ds_put_format(&match, "ip && ip%s.dst == %s",
+                          snat_ip.family == AF_INET ? "4" : "6",
+                          dnat_force_snat_ip);
             ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 110,
                           ds_cstr(&match), "ct_snat;");
 
@@ -7616,7 +7802,9 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
              * gateway router (as set in options:lb_force_snat_ip) is seen,
              * UNSNAT it. */
             ds_clear(&match);
-            ds_put_format(&match, "ip && ip4.dst == %s", lb_force_snat_ip);
+            ds_put_format(&match, "ip && ip%s.dst == %s",
+                          lb_snat_ip.family == AF_INET ? "4" : "6",
+                          lb_force_snat_ip);
             ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 100,
                           ds_cstr(&match), "ct_snat;");
 
@@ -7718,7 +7906,7 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                     if (addr_family == AF_INET) {
                         ds_put_format(&match, "ip && ip4.dst == %s",
                                       ip_address);
-                    } else {
+                    } else if (addr_family == AF_INET6) {
                         ds_put_format(&match, "ip && ip6.dst == %s",
                                       ip_address);
                     }
@@ -7738,7 +7926,7 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                 if (addr_family == AF_INET) {
                     ds_put_format(&match, "ip && ip4.dst == %s",
                                 ip_address);
-                } else {
+                } else if (addr_family == AF_INET6) {
                     ds_put_format(&match, "ip && ip6.dst == %s",
                                 ip_address);
                 }
