diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index fb2d6a9..11eaf49 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -90,6 +90,21 @@ enum action_opcode {
      *     MFF_ETH_SRC = mac
      */
     ACTION_OPCODE_PUT_ND,
+
+    /* "nd_ra { ...actions... }".
+     *
+     * The actions, in OpenFlow 1.3 format, follow the action_header.
+     */
+    ACTION_OPCODE_ND_RA,
+
+    /* slaac (prefix0, [prefix1, ...,] mtu, mac)".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - One or more IPv6 prefixes.
+     *   - A 16-bit MTU.
+     *   - A 48-bit Ether address.
+     */
+    ACTION_OPCODE_SLAAC,
 };
 
 /* Header. */
diff --git a/include/ovn/expr.h b/include/ovn/expr.h
index df22895..e1979ae 100644
--- a/include/ovn/expr.h
+++ b/include/ovn/expr.h
@@ -461,9 +461,13 @@ struct expr_constant_set {
 
 char *expr_parse_constant_set(struct lexer *, const struct shash *symtab,
                               struct expr_constant_set *cs)
+
     OVS_WARN_UNUSED_RESULT;
 void expr_constant_set_destroy(struct expr_constant_set *cs);
 
+bool expr_parse_list_integer_parameters(struct lexer *lexer,
+                                        struct expr_constant_set *cs);
+
 
 /* Address sets, aka "macros".
  *
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index bd685fe..4efaccf 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -73,6 +73,9 @@ static void send_garp_run(const struct ovsrec_bridge *,
 static void pinctrl_handle_nd_na(const struct flow *ip_flow,
                                  const struct match *md,
                                  struct ofpbuf *userdata);
+static void pinctrl_handle_nd_ra(const struct flow *ip_flow,
+                                 const struct match *md,
+                                 struct ofpbuf *userdata);
 static void reload_metadata(struct ofpbuf *ofpacts,
                             const struct match *md);
 
@@ -421,6 +424,10 @@ process_packet_in(const struct ofp_header *msg)
                                        false);
         break;
 
+    case ACTION_OPCODE_ND_RA:
+        pinctrl_handle_nd_ra(&headers, &pin.flow_metadata, &userdata);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -1019,3 +1026,165 @@ exit:
     dp_packet_uninit(&packet);
     ofpbuf_uninit(&ofpacts);
 }
+
+static bool pinctrl_handle_slaac(const struct flow *ip_flow,
+                                 struct ofpbuf *userdata,
+                                 struct dp_packet *packet,
+                                 int *left_bytes)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+    bool ret_val = false;
+    struct ipv6_netaddr *prefixes = NULL;
+
+    /* Get SLL. */
+    struct eth_addr *sll = ofpbuf_try_pull(userdata,
+                                           sizeof(struct eth_addr));
+    if (!sll) {
+        VLOG_WARN_RL(&rl, "Failed to parse link-layer address.");
+        goto exit;
+    }
+    (*left_bytes) -= sizeof(struct eth_addr);
+
+    /* Get MTU. */
+    ovs_be32 *mtu = ofpbuf_try_pull(userdata, sizeof(ovs_be32));
+    if (!mtu || *mtu == 0) {
+        VLOG_WARN_RL(&rl, "Failed to parse mtu.");
+        goto exit;
+    }
+    (*left_bytes) -= sizeof(ovs_be32);
+
+    /* Get number of prefixes. */
+    uint8_t *n_prefixes = ofpbuf_try_pull(userdata, 1);
+    if (!n_prefixes || *n_prefixes == 0) {
+        VLOG_WARN_RL(&rl, "Failed to parse prefixes number.");
+        goto exit;
+    }
+    (*left_bytes)--;
+
+    /* Get prefixes. */
+    size_t prefixes_size = sizeof(struct ipv6_netaddr) * (*n_prefixes);
+    prefixes = xmalloc(prefixes_size);
+    struct in6_addr *prefix;
+    uint8_t *prefix_len = NULL;
+    for (size_t i = 0; i < *n_prefixes; i++) {
+        prefix = ofpbuf_try_pull(userdata, sizeof(struct in6_addr));
+        if (!prefix) {
+            VLOG_WARN_RL(&rl, "Failed to parse ipv6 prefix.");
+            goto exit;
+        }
+        prefix_len = ofpbuf_try_pull(userdata, 1);
+        if (!prefix_len || *prefix_len == 0) {
+            VLOG_WARN_RL(&rl, "Failed to parse prefix len.");
+            goto exit;
+        }
+        memcpy(&prefixes[i].addr, prefix, sizeof(struct in6_addr));
+        prefixes[i].plen = *prefix_len;
+    }
+    (*left_bytes) -= *n_prefixes * (sizeof(struct in6_addr) + 1);
+
+    ovs_be32 ipv6_src[4], ipv6_dst[4], router_prefix[4];
+    struct in6_addr lla;
+    in6_generate_lla(*sll, &lla);
+    memcpy(ipv6_src, &lla, sizeof ipv6_src);
+    memcpy(ipv6_dst, &ip_flow->ipv6_src, sizeof ipv6_dst);
+
+    uint8_t cur_hop_limit = 64;
+    uint8_t mo_flags = 0;
+    ovs_be16 router_lifetime = htons(0x2a30);  // 3 hours.
+    uint8_t la_flags = ND_PREFIX_ON_LINK | ND_PREFIX_AUTONOMOUS_ADDRESS;
+    ovs_be32 valid_lifetime = htonl(0x2a30);
+    ovs_be32 preferred_lifetime = htonl(0x2a30);
+    ovs_be32 reachable_time = 0;
+    ovs_be32 retrans_timer = 0;
+
+    compose_nd_ra(packet, *sll, ip_flow->dl_src, ipv6_src, ipv6_dst,
+                  cur_hop_limit, mo_flags,
+                  router_lifetime, reachable_time, retrans_timer);
+    packet_put_ra_sll_opt(packet, *sll);
+    packet_put_ra_mtu_opt(packet, *mtu);
+    for (size_t i = 0; i < *n_prefixes; i++) {
+        memcpy(router_prefix, &prefixes[i].addr, sizeof router_prefix);
+        packet_put_ra_prefix_opt(packet, prefixes[i].plen, la_flags,
+                              valid_lifetime, preferred_lifetime,
+                              router_prefix);
+    }
+    ret_val = true;
+
+exit:
+    if (prefixes) {
+        free(prefixes);
+    }
+    return ret_val;
+}
+
+static void
+pinctrl_handle_nd_ra(const struct flow *ip_flow, const struct match *md,
+                     struct ofpbuf *userdata)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    uint64_t ofpacts_stub[4096 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+    reload_metadata(&ofpacts, md);
+
+    /* Pull inner action. */
+    struct ofpact *act = ofpbuf_try_pull(userdata, sizeof(struct ofpact));
+    if (!act) {
+        VLOG_WARN_RL(&rl, "Failed to pull ofpact.");
+        return;
+    }
+    int left_bytes = ntohs(act->len) - sizeof(struct ofpact);
+    if (!ofpbuf_try_pull(userdata, sizeof(struct ofpact_controller))) {
+        VLOG_WARN_RL(&rl, "Failed to pull ofpact_controller.");
+        return;
+    }
+    left_bytes -= sizeof(struct ofpact_controller);
+    struct action_header *ah = ofpbuf_try_pull(userdata, sizeof *ah);
+    if (!ah) {
+        VLOG_WARN_RL(&rl, "Failed to pull action_header.");
+        return;
+    }
+    if (ntohl(ah->opcode) != ACTION_OPCODE_SLAAC) {
+        VLOG_WARN_RL(&rl, "Not supported action opcode %d.", ntohl(ah->opcode));
+        return;
+    }
+    left_bytes -= sizeof *ah;
+
+    uint64_t packet_stub[256 / 8];
+    struct dp_packet packet;
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
+    if (!pinctrl_handle_slaac(ip_flow, userdata, &packet, &left_bytes)) {
+        VLOG_WARN_RL(&rl, "Failed to compose a RA packet for SLAAC.");
+        goto exit;
+    }
+    if (left_bytes < 0){
+        VLOG_WARN_RL(&rl, "Left bytes error in compose RA packet.");
+        goto exit;
+    } else if (left_bytes > 0) {
+        ofpbuf_try_pull(userdata, left_bytes);
+    }
+    /* Get left actions. */
+    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
+                                                      version, &ofpacts);
+    if (error) {
+        VLOG_WARN_RL(&rl, "Failed to parse actions for 'ra' (%s)",
+                     ofperr_to_string(error));
+        goto exit;
+    }
+
+    struct ofputil_packet_out po = {
+        .packet = dp_packet_data(&packet),
+        .packet_len = dp_packet_size(&packet),
+        .buffer_id = UINT32_MAX,
+        .in_port = OFPP_CONTROLLER,
+        .ofpacts = ofpacts.data,
+        .ofpacts_len = ofpacts.size,
+    };
+
+    queue_msg(ofputil_encode_packet_out(&po, proto));
+
+exit:
+    dp_packet_uninit(&packet);
+    ofpbuf_uninit(&ofpacts);
+}
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index ec82ec3..25f0a0c 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -1081,6 +1081,75 @@ parse_ct_nat(struct action_context *ctx, bool snat)
     ofpbuf_push_uninit(ctx->ofpacts, ct_offset);
 }
 
+static void
+parse_slaac_action(struct action_context *ctx)
+{
+    struct expr_constant_set cs = {
+        .type = EXPR_C_INTEGER,
+        .values = NULL,
+        .n_values = 0,
+        .in_curlies = false,
+    };
+
+    if (!expr_parse_list_integer_parameters(ctx->lexer, &cs)) {
+        action_syntax_error(ctx, "failed to parse list parameters"
+                                 " for 'slaac' action");
+        goto exit;
+    }
+
+    /* Parameters check:
+     * 1 ether address for SLL, 1 integer for MTU, and multiple IPv6
+     * addresses for Prefix Information options. */
+    if (cs.n_values < 3) {
+        action_syntax_error(ctx, "not enough parameters parsed in"
+                                 " 'slaac' action");
+        goto exit;
+    }
+    size_t i = 0;
+    if (cs.values[i++].format != LEX_F_ETHERNET) {
+        action_syntax_error(ctx, "the first parameter in 'slaac' action"
+                                 " should be ether address");
+        goto exit;
+    }
+    if (cs.values[i++].format != LEX_F_DECIMAL) {
+        action_syntax_error(ctx, "the second parameter in 'slaac' action"
+                                 " should be mtu, in decimal integer type");
+        goto exit;
+    }
+    for (; i != cs.n_values; i++) {
+        if (cs.values[i].format != LEX_F_IPV6) {
+            action_syntax_error(ctx, "expecting IPv6 address format in"
+                                     " 'slaac' action");
+            goto exit;
+        }
+    }
+
+    /* Set controller data. */
+    size_t oc_offset = start_controller_op(ctx->ofpacts, ACTION_OPCODE_SLAAC,
+                                           false);
+    /* Put ether address for SLL option. */
+    ofpbuf_put(ctx->ofpacts, &cs.values[0].value.mac,
+                             sizeof(struct eth_addr));
+    /* Put integer for MTU option. */
+    ofpbuf_put(ctx->ofpacts, &cs.values[1].value.be32[31],
+                             sizeof(ovs_be32));
+    /* Put number of Prefix Information options. */
+    int num = cs.n_values - 2;
+    ofpbuf_put(ctx->ofpacts, &num, 1);
+    /* Put prefixes and prefixes lengths for Prefix Information options. */
+    int plen = 0;
+    for (size_t i = 2; i != cs.n_values; i++) {
+        ofpbuf_put(ctx->ofpacts, &cs.values[i].value.ipv6,
+                   sizeof(struct in6_addr));
+        plen = ipv6_count_cidr_bits(&cs.values[i].mask.ipv6);
+        ofpbuf_put(ctx->ofpacts, &plen, 1);
+    }
+    finish_controller_op(ctx->ofpacts, oc_offset);
+
+exit:
+    expr_constant_set_destroy(&cs);
+}
+
 static bool
 parse_action(struct action_context *ctx)
 {
@@ -1126,6 +1195,10 @@ parse_action(struct action_context *ctx)
         parse_get_nd_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "put_nd")) {
         parse_put_nd_action(ctx);
+    } else if (lexer_match_id(ctx->lexer, "nd_ra")) {
+        parse_nested_action(ctx, ACTION_OPCODE_ND_RA, "nd_rs");
+    } else if (lexer_match_id(ctx->lexer, "slaac")) {
+        parse_slaac_action(ctx);
     } else {
         action_syntax_error(ctx, "expecting action");
     }
diff --git a/ovn/lib/expr.c b/ovn/lib/expr.c
index db00d7c..c261e44 100644
--- a/ovn/lib/expr.c
+++ b/ovn/lib/expr.c
@@ -3026,3 +3026,30 @@ expr_parse_constant_set(struct lexer *lexer, const struct shash *symtab,
     parse_constant_set(&ctx, cs);
     return ctx.error;
 }
+
+/* Parses a ()-enclosed list integer parameters in an action into 'cs',
+ * which the caller need not have initialized.  Returns true on success, in
+ * otherwise return false. It's always the caller's responsibility to
+ * destroy 'cs'. */
+bool
+expr_parse_list_integer_parameters(struct lexer *lexer,
+                                   struct expr_constant_set *cs)
+{
+    if (!lexer_match(lexer, LEX_T_LPAREN)) {
+        return false;
+    }
+
+    struct expr_context ctx = { .lexer = lexer, .symtab = NULL };
+    size_t allocated_values = 0;
+    bool ok = true;
+
+    do {
+        if (!parse_constant(&ctx, cs, &allocated_values)) {
+            ok = false;
+            break;
+        }
+        lexer_match(ctx.lexer, LEX_T_COMMA);
+    } while(!lexer_match(ctx.lexer, LEX_T_RPAREN));
+
+    return ok;
+}
diff --git a/ovn/lib/logical-fields.c b/ovn/lib/logical-fields.c
index 6dbb4ae..b906cf8 100644
--- a/ovn/lib/logical-fields.c
+++ b/ovn/lib/logical-fields.c
@@ -174,7 +174,12 @@ ovn_init_symtab(struct shash *symtab)
     expr_symtab_add_field(symtab, "arp.tha", MFF_ARP_THA, "arp", false);
 
     expr_symtab_add_predicate(symtab, "nd",
-              "icmp6.type == {135, 136} && icmp6.code == 0 && ip.ttl == 255");
+              "icmp6.type == {133, 134, 135, 136}"
+              " && icmp6.code == 0 && ip.ttl == 255");
+    expr_symtab_add_predicate(symtab, "nd_rs",
+              "icmp6.type == 133 && icmp6.code == 0 && ip.ttl == 255");
+    expr_symtab_add_predicate(symtab, "nd_ra",
+              "icmp6.type == 134 && icmp6.code == 0 && ip.ttl == 255");
     expr_symtab_add_predicate(symtab, "nd_ns",
               "icmp6.type == 135 && icmp6.code == 0 && ip.ttl == 255");
     expr_symtab_add_predicate(symtab, "nd_na",
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 13c9526..dce9dc7 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1151,8 +1151,36 @@
           </p>
         </dd>
 
-        <dt><code>get_nd(<var>P</var>, <var>A</var>);</code></dt>
+        <dt>
+          <code>nd_ra { <var>slaac(...)</var>; <var>action</var>; </code>...<code> };</code>
+        </dt>
+
+        <dd>
+          <p>
+            Generate an IPv6 Router Advertisement (RA) packet per inner action
+            'slaac' ordered parameters, and eth.src and ip6.src from Router
+            Solicitation (RS) packet being processed.
+            <b>Parameters</b>: 48-bit mac address for SLL option, 16-bit MTU,
+            one or more IPv6 prefixes for Prefix Information options.
+            Then executes each nested <var>action</var> on the RA packet.
+            Actions following the <var>ra</var> action, if any, apply to the
+            original, unmodified packet.
+            The composed RA packet will use RS packet eth.src as eth.dst, use
+            RS packet ip6.src as ip6.dst, and use 'slaac' action parameter mac
+            address and its lla as eth.src and ip6.src.
+          </p>
+
+          <p>
+            The RA packet has the same VLAN header, if any, as the IPv6 packet
+            it replaces.
+          </p>
+
+          <p>
+            <b>Prerequisite:</b> <code>nd_rs</code>
+          </p>
+        </dd>
 
+        <dt><code>get_nd(<var>P</var>, <var>A</var>);</code></dt>
         <dd>
           <p>
             <b>Parameters</b>: logical port string field <var>P</var>, 128-bit
diff --git a/tests/ovn.at b/tests/ovn.at
index a95e0b2..33ace16 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -732,6 +732,9 @@ 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
 
+# nd_ra
+nd_ra{slaac(12:34:56:78:9a:bc,1450,fdad:a0f9:a012::/64);outport = inport;flags.loopback = 1;output;}; => actions=controller(userdata=00.00.00.05.00.00.00.00.ff.ff.00.38.00.00.23.20.00.25.00.00.00.00.00.00.00.03.00.28.00.00.00.06.00.00.00.00.12.34.56.78.9a.bc.00.00.05.aa.01.fd.ad.a0.f9.a0.12.00.00.00.00.00.00.00.00.00.00.40.ff.ff.00.18.00.00.23.20.00.06.00.20.00.00.00.00.00.01.1c.04.00.01.1e.04.ff.ff.00.18.00.00.23.20.00.07.00.00.00.01.14.04.00.00.00.00.00.00.00.01.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=nd_rs
+
 # 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
@@ -745,7 +748,7 @@ get_nd(inport, outport); => Cannot use string field outport where numeric field
 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)
+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 == 0x85 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x86 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x88 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd))) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.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):
