diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c
index 576c695..df19680 100644
--- a/ovn/controller/physical.c
+++ b/ovn/controller/physical.c
@@ -707,6 +707,23 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
     put_resubmit(OFTABLE_LOCAL_OUTPUT, &ofpacts);
     ofctrl_add_flow(flow_table, OFTABLE_REMOTE_OUTPUT, 0, &match, &ofpacts);
 
+    /* Table 32, Priority 100.
+     * =======================
+     *
+     * Directly output NA packets replied by ovn-controller for NS packets
+     * come from local VMs.
+     *   - match: ip6 && icmp6 && icmp6.type == 136 && reg0 == 0x1
+     *   - action: resubmit(,48)
+     */
+    match_init_catchall(&match);
+    match_set_dl_type(&match, htons(0x86dd));
+    match_set_nw_proto(&match, (uint8_t)58);
+    match_set_icmp_type(&match, (uint8_t)136);
+    match_set_reg(&match, 0, (uint32_t)1);
+    ofpbuf_clear(&ofpacts);
+    put_resubmit(OFTABLE_LOG_EGRESS_PIPELINE, &ofpacts);
+    ofctrl_add_flow(flow_table, OFTABLE_REMOTE_OUTPUT, 100, &match, &ofpacts);
+
     /* Table 34, Priority 0.
      * =======================
      *
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index a8dd2bb..0263d5c 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -93,7 +93,7 @@ enum ovn_stage {
     PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_ND,    2, "ls_in_port_sec_nd")     \
     PIPELINE_STAGE(SWITCH, IN,  PRE_ACL,        3, "ls_in_pre_acl")      \
     PIPELINE_STAGE(SWITCH, IN,  ACL,            4, "ls_in_acl")          \
-    PIPELINE_STAGE(SWITCH, IN,  ARP_RSP,        5, "ls_in_arp_rsp")      \
+    PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,     5, "ls_in_arp_nd_rsp")      \
     PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,        6, "ls_in_l2_lkup")      \
                                                                       \
     /* Logical switch egress stages. */                               \
@@ -1475,6 +1475,64 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports)
                           acl->match, "drop;");
         }
     }
+
+    /* Egress Pre-ACL Table (Priority 110).
+     *
+     * Directly output RA packets replied by ovn-controller, they dont
+     * need go through conntrack.
+     */
+    bool ra_allowed = false;
+    /* Consider the following cases are RA allowed:
+     * - has:
+     *     match: "... ip6 && icmp6",
+     *     action: "allow"/"allow-related".
+     *   and has no:
+     *     match: "... ip6 && icmp6 && icmp6.type == 136",
+     *     action: "drop"/"reject".
+     * - has:
+     *     match: "... ip6 && icmp6 && icmp6.type == 136",
+     *     action: "allow"/"allow-related".
+     *   and has no:
+     *     match: "... ip6 && icmp6",
+     *     action: "drop"/"reject".
+     */
+    for (size_t i = 0; i < od->nbs->n_acls; i++) {
+        struct nbrec_acl *acl = od->nbs->acls[i];
+        if (!strcmp(acl->direction, "from-lport")) {
+            continue;
+        }
+        char *icmp6 = strstr(acl->match, "ip6 && icmp6");
+        if (!icmp6) {
+            continue;
+        }
+        /* strlen("ip6 && icmp6") == 12 */
+        if (*(icmp6 + 12) == '\0') {
+            if (!strcmp(acl->action, "drop")
+                || !strcmp(acl->action, "reject")) {
+                ra_allowed = false;
+                break;
+            } else {
+                ra_allowed = true;
+            }
+        } else {
+            char *ra = strstr(icmp6 + 12, "icmp6.type == 136");
+            /* strlen("icmp6.type == 136") == 17 */
+            if (ra && *(ra + 17) == '\0') {
+                if (!strcmp(acl->action, "drop")
+                    || !strcmp(acl->action, "reject")) {
+                    ra_allowed = false;
+                    break;
+                } else {
+                    ra_allowed = true;
+                }
+            }
+        }
+    }
+    if (ra_allowed) {
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
+                      "ip6 && icmp6 && icmp6.type == 136 && reg0 == 0x1",
+                      "output;");
+    }
 }
 
 static void
@@ -1566,13 +1624,13 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
 
         if (!strcmp(op->nbs->type, "localnet")) {
             char *match = xasprintf("inport == %s", op->json_key);
-            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_RSP, 100,
+            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
                           match, "next;");
             free(match);
         }
     }
 
-    /* Ingress table 5: ARP responder, reply for known IPs.
+    /* Ingress table 5: ARP/ND responder, reply for known IPs.
      * (priority 50). */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbs) {
@@ -1580,7 +1638,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
 
         /*
-         * Add ARP reply flows if either the
+         * Add ARP/ND reply flows if either the
          *  - port is up or
          *  - port type is router
          */
@@ -1591,7 +1649,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         for (size_t i = 0; i < op->nbs->n_addresses; i++) {
             struct lport_addresses laddrs;
             if (!extract_lport_addresses(op->nbs->addresses[i], &laddrs,
-                                         false)) {
+                                         true)) {
                 continue;
             }
             for (size_t j = 0; j < laddrs.n_ipv4_addrs; j++) {
@@ -1612,13 +1670,34 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                     ETH_ADDR_ARGS(laddrs.ea),
                     ETH_ADDR_ARGS(laddrs.ea),
                     IP_ARGS(laddrs.ipv4_addrs[j].addr));
-                ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_RSP, 50,
+                ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
                               match, actions);
                 free(match);
                 free(actions);
             }
+            char ip6_str[INET6_ADDRSTRLEN + 1];
+            for (size_t j = 0; j < laddrs.n_ipv6_addrs; j++) {
+                ipv6_string_mapped(ip6_str, &(laddrs.ipv6_addrs[i].addr));
+
+                struct ds match = DS_EMPTY_INITIALIZER;
+                ds_put_cstr(&match, "ip6 && nd && icmp6.type == 135 "
+                                    "&& nd.target == ");
+                ds_put_format(&match, "%s", ip6_str);
+                struct ds actions = DS_EMPTY_INITIALIZER;
+                ds_put_cstr(&actions, "na{");
+                ds_put_format(&actions, ETH_ADDR_FMT, ETH_ADDR_ARGS(laddrs.ea));
+                ds_put_cstr(&actions, "; reg0 = 0x1; outport = inport; "
+                                      "inport = \"\"; output;};");
+
+                ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+                              ds_cstr(&match), ds_cstr(&actions));
+
+                ds_destroy(&actions);
+                ds_destroy(&match);
+            }
 
             free(laddrs.ipv4_addrs);
+            free(laddrs.ipv6_addrs);
         }
     }
 
@@ -1629,7 +1708,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
             continue;
         }
 
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_RSP, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_RSP, 0, "1", "next;");
     }
 
     /* Ingress table 6: Destination lookup, broadcast and multicast handling
diff --git a/tutorial/OVN-Tutorial.md b/tutorial/OVN-Tutorial.md
index 7b31fe2..bb3cdae 100644
--- a/tutorial/OVN-Tutorial.md
+++ b/tutorial/OVN-Tutorial.md
@@ -104,7 +104,7 @@ show the logical flows.
       table=2(ls_in_port_sec_nd), priority=    0, match=(1), action=(next;)
       table=3(   ls_in_pre_acl), priority=    0, match=(1), action=(next;)
       table=4(       ls_in_acl), priority=    0, match=(1), action=(next;)
-      table=5(   ls_in_arp_rsp), priority=    0, match=(1), action=(next;)
+      table=5(ls_in_arp_nd_rsp), priority=    0, match=(1), action=(next;)
       table=6(   ls_in_l2_lkup), priority=  100, match=(eth.mcast), action=(outport = "_MC_flood"; output;)
       table=6(   ls_in_l2_lkup), priority=   50, match=(eth.dst == 00:00:00:00:00:01), action=(outport = "sw0-port1"; output;)
       table=6(   ls_in_l2_lkup), priority=   50, match=(eth.dst == 00:00:00:00:00:02), action=(outport = "sw0-port2"; output;)
@@ -277,7 +277,7 @@ OVN creates separate logical flows for each logical switch.
       table=2(ls_in_port_sec_nd), priority=    0, match=(1), action=(next;)
       table=3(   ls_in_pre_acl), priority=    0, match=(1), action=(next;)
       table=4(       ls_in_acl), priority=    0, match=(1), action=(next;)
-      table=5(   ls_in_arp_rsp), priority=    0, match=(1), action=(next;)
+      table=5(ls_in_arp_nd_rsp), priority=    0, match=(1), action=(next;)
       table=6(   ls_in_l2_lkup), priority=  100, match=(eth.mcast), action=(outport = "_MC_flood"; output;)
       table=6(   ls_in_l2_lkup), priority=   50, match=(eth.dst == 00:00:00:00:00:03), action=(outport = "sw1-port1"; output;)
       table=6(   ls_in_l2_lkup), priority=   50, match=(eth.dst == 00:00:00:00:00:04), action=(outport = "sw1-port2"; output;)
@@ -303,7 +303,7 @@ OVN creates separate logical flows for each logical switch.
       table=2(ls_in_port_sec_nd), priority=    0, match=(1), action=(next;)
       table=3(   ls_in_pre_acl), priority=    0, match=(1), action=(next;)
       table=4(       ls_in_acl), priority=    0, match=(1), action=(next;)
-      table=5(   ls_in_arp_rsp), priority=    0, match=(1), action=(next;)
+      table=5(ls_in_arp_nd_rsp), priority=    0, match=(1), action=(next;)
       table=6(   ls_in_l2_lkup), priority=  100, match=(eth.mcast), action=(outport = "_MC_flood"; output;)
       table=6(   ls_in_l2_lkup), priority=   50, match=(eth.dst == 00:00:00:00:00:01), action=(outport = "sw0-port1"; output;)
       table=6(   ls_in_l2_lkup), priority=   50, match=(eth.dst == 00:00:00:00:00:02), action=(outport = "sw0-port2"; output;)
