diff mbox

[ovs-dev,RFC,v2,2/3] ovn: add SLAAC support for IPv6

Message ID 1470104378-32302-1-git-send-email-zealokii@gmail.com
State Superseded
Headers show

Commit Message

Zong Kai LI Aug. 2, 2016, 2:19 a.m. UTC
This patch tries to implement Router Advertisement (RA) responder for SLAAC
on ovn-controller side.

It parses lflows which have:
 - match: inport == LRP_NAME && ip6.dst == ff02::2 && nd_rs
   (nd_rs: icmp6.type == 133 && icmp6.code == 0 && ttl == 255)
 - action: nd_ra{slaac(prefix,...,MTU, mac_address);
           outport = inport; flags.loopback = 1; output;};
   (nd_ra is a new action which stands for RA responder;
    slaac is a new action which has the following parameters to tell
    ovn-controller to compose RA packet with SLAAC flags, with these
    parameters:
     - IPv6 prefix(es): such as fd80:a0f9:a012::/64.
     - MTU: logical switch MTU, such as 1450.
     - MAC address: router port mac address, such as fa:16:3e:12:34:56.
    Beside the parameters list above, nd_ra action will use eth.src and ip6.src
    from packet being processed as RA packet eth.src and ip6.src.
After a RA packet is composed, the left nested actions will make RA packet
transmitted back to the inport, where Router Solicitation (RS) packet comes.

For inner action 'slaac', as a prototype, it doesn't try to expose all RA
relevant flags and fields. As most flags are constant for in SLAAC scenario,
so it only exposed few fields user may care, such as prefixes, MTU and LLA.
It uses an ordered parameters list to hold all those values for now, but in
future, when we try to implement more feature on RA, the unexposed ones should
be exposed, and key-pair style parameter list should be used.

For outer action 'nd_ra', as a prototype, it's just a RA responder now, not a
real implement for RA, like it wont send periodic RA broadcast. This will be
harmful when routing relevant stuff changes, like user changes lrp mac,
disattach a switch from a router. However, a periodic version seems not so
necessary, but a broadcast should be sent when things change.

For lflow, this patch add prerequisites for Router Solicitation (RS) and RA
message. But it doesn't fix details of nd.target, nd.sll and nd.tll for RS
and RA, since these fields are not supported to be modified via ovs native
actions for now.
---
 include/ovn/actions.h    |  15 +++++
 include/ovn/expr.h       |  11 ++++
 ovn/controller/lflow.c   |   6 +-
 ovn/controller/pinctrl.c | 168 +++++++++++++++++++++++++++++++++++++++++++++++
 ovn/lib/actions.c        |  69 +++++++++++++++++++
 ovn/lib/expr.c           |  11 +---
 ovn/ovn-sb.xml           |  32 ++++++++-
 tests/ovn.at             |   6 +-
 tests/test-ovn.c         |   6 +-
 9 files changed, 310 insertions(+), 14 deletions(-)

Comments

Dustin Lundquist Aug. 9, 2016, 8:47 p.m. UTC | #1
On Mon, Aug 1, 2016 at 7:19 PM, Zong Kai LI <zealokii@gmail.com> wrote:

> This patch tries to implement Router Advertisement (RA) responder for SLAAC
> on ovn-controller side.
>
> It parses lflows which have:
>  - match: inport == LRP_NAME && ip6.dst == ff02::2 && nd_rs
>    (nd_rs: icmp6.type == 133 && icmp6.code == 0 && ttl == 255)
>  - action: nd_ra{slaac(prefix,...,MTU, mac_address);
>            outport = inport; flags.loopback = 1; output;};
>    (nd_ra is a new action which stands for RA responder;
>     slaac is a new action which has the following parameters to tell
>     ovn-controller to compose RA packet with SLAAC flags, with these
>     parameters:
>      - IPv6 prefix(es): such as fd80:a0f9:a012::/64.
>      - MTU: logical switch MTU, such as 1450.
>      - MAC address: router port mac address, such as fa:16:3e:12:34:56.
>     Beside the parameters list above, nd_ra action will use eth.src and
> ip6.src
>     from packet being processed as RA packet eth.src and ip6.src.
> After a RA packet is composed, the left nested actions will make RA packet
> transmitted back to the inport, where Router Solicitation (RS) packet
> comes.
>
> For inner action 'slaac', as a prototype, it doesn't try to expose all RA
> relevant flags and fields. As most flags are constant for in SLAAC
> scenario,
> so it only exposed few fields user may care, such as prefixes, MTU and LLA.
> It uses an ordered parameters list to hold all those values for now, but in
> future, when we try to implement more feature on RA, the unexposed ones
> should
> be exposed, and key-pair style parameter list should be used.
>
> For outer action 'nd_ra', as a prototype, it's just a RA responder now,
> not a
> real implement for RA, like it wont send periodic RA broadcast. This will
> be
> harmful when routing relevant stuff changes, like user changes lrp mac,
> disattach a switch from a router. However, a periodic version seems not so
> necessary, but a broadcast should be sent when things change.
>
> For lflow, this patch add prerequisites for Router Solicitation (RS) and RA
> message. But it doesn't fix details of nd.target, nd.sll and nd.tll for RS
> and RA, since these fields are not supported to be modified via ovs native
> actions for now.
> ---
>  include/ovn/actions.h    |  15 +++++
>  include/ovn/expr.h       |  11 ++++
>  ovn/controller/lflow.c   |   6 +-
>  ovn/controller/pinctrl.c | 168 ++++++++++++++++++++++++++++++
> +++++++++++++++++
>  ovn/lib/actions.c        |  69 +++++++++++++++++++
>  ovn/lib/expr.c           |  11 +---
>  ovn/ovn-sb.xml           |  32 ++++++++-
>  tests/ovn.at             |   6 +-
>  tests/test-ovn.c         |   6 +-
>  9 files changed, 310 insertions(+), 14 deletions(-)
>
> diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
>
index bd685fe..fa1e7d7 100644
> --- a/ovn/controller/pinctrl.c
> +++ b/ovn/controller/pinctrl.c
>
@@ -1019,3 +1026,164 @@ 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 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);
> +
> +    /* 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 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);
> +
> +    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);
> +    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);
> +    }
> +    packet_put_ra_mtu_opt(packet, *mtu);
> +    packet_put_ra_sll_opt(packet, *sll);

Other router advertisement sender implementations include the source
link-layer address option before prefix options, any reason for this
ordering? I'm not aware of any client incompatibilities with sending this
in this order, but could confuse some less tolerant clients.

> +    ret_val = true;
> +
> +exit:
> +    if (prefixes) {
> +        free(prefixes);
> +    }
> +    return ret_val;
> +}
> +
> +static void
> +pinctrl_handle_nd_ra(const struct flow *ip_flow, struct ofputil_packet_in
> *pin,
> +                     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, &pin->flow_metadata);
> +
> +    /* 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/ovn-sb.xml b/ovn/ovn-sb.xml
> index 13c9526..66a9009 100644
> --- a/ovn/ovn-sb.xml
> +++ b/ovn/ovn-sb.xml
> @@ -1151,8 +1151,38 @@
>            </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>: One or more IPv6 prefixes, 16-bit MUT,
> 48-bit
>
MTU?

> +            mac address.
> +            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_arp(<var>P</var>, <var>A</var>);</code></dt>
> +
> +        <dt><code>get_nd(<var>P</var>, <var>A</var>);</code></dt>
>          <dd>
>            <p>
>              <b>Parameters</b>: logical port string field <var>P</var>,
> 128-bit
>
> Is there a reason for specifying the prefixes as the first arguments to
slaac()? Usually variable number arguments are passed last after any fixed
arguments, I would recommend `slacc(sll, mtu, prefix1, ...)` instead.

Other packet in handlers like pinctrl_handle_arp() only accept the
flow_metadata rather than the entire packet, any reason for this change
since only the metadata is used?

Finally tests/test-ovn.c and tests/ovn.at didn't apply cleanly.


Thanks,


Dustin Lundquist
diff mbox

Patch

diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index a395ce9..9eaa8db 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -88,6 +88,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 d790c49..150f2e7 100644
--- a/include/ovn/expr.h
+++ b/include/ovn/expr.h
@@ -450,6 +450,17 @@  struct expr_constant_set {
     bool in_curlies;              /* Whether the constants were in {}. */
 };
 
+/* Context maintained during expr_parse(). */
+struct expr_context {
+    struct lexer *lexer;        /* Lexer for pulling more tokens. */
+    const struct shash *symtab; /* Symbol table. */
+    const struct shash *macros; /* Table of macros. */
+    char *error;                /* Error, if any, otherwise NULL. */
+    bool not;                   /* True inside odd number of NOT operators. */
+};
+
+bool parse_constant(struct expr_context *ctx, struct expr_constant_set *cs,
+                    size_t *allocated_values);
 char *expr_parse_constant_set(struct lexer *, const struct shash *symtab,
                               struct expr_constant_set *cs)
     OVS_WARN_UNUSED_RESULT;
diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index fda10eb..18afefd 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -164,7 +164,11 @@  lflow_init(void)
     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/controller/pinctrl.c b/ovn/controller/pinctrl.c
index bd685fe..fa1e7d7 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,
+                                 struct ofputil_packet_in *pin,
+                                 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, &userdata);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -1019,3 +1026,164 @@  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 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);
+
+    /* 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 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);
+
+    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);
+    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);
+    }
+    packet_put_ra_mtu_opt(packet, *mtu);
+    packet_put_ra_sll_opt(packet, *sll);
+    ret_val = true;
+
+exit:
+    if (prefixes) {
+        free(prefixes);
+    }
+    return ret_val;
+}
+
+static void
+pinctrl_handle_nd_ra(const struct flow *ip_flow, struct ofputil_packet_in *pin,
+                     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, &pin->flow_metadata);
+
+    /* 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 b9d1205..7e913ce 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -1080,6 +1080,71 @@  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)
+{
+    if (!lexer_match(ctx->lexer, LEX_T_LPAREN)) {
+        action_syntax_error(ctx, "expecting '('");
+        return;
+    }
+
+    struct expr_constant_set cs = {
+        .type = EXPR_C_INTEGER,
+        .values = NULL,
+        .n_values = 0,
+        .in_curlies = false,
+    };
+    struct expr_context expr_ctx = {
+       .lexer = ctx->lexer,
+       .symtab = NULL,
+    };
+    size_t allocated_values = 0;
+
+    do {
+        if (!parse_constant(&expr_ctx, &cs, &allocated_values)) {
+            break;
+        }
+        lexer_match(expr_ctx.lexer, LEX_T_COMMA);
+    } while(!lexer_match(expr_ctx.lexer, LEX_T_RPAREN));
+    /* Multiple prefixes, as least 1 MTU, at least 1 SLL. */
+    if (cs.n_values < 3) {
+        action_syntax_error(ctx, "not enough constants found");
+        goto exit;
+    }
+    /* Check prefixes. */
+    for (size_t i = 0; i < cs.n_values - 2; i++) {
+        if (cs.values[i].format != LEX_F_IPV6) {
+            action_syntax_error(ctx, "expecting IPv6 address format");
+            goto exit;
+        }
+    }
+
+    /* Set controller data. */
+    size_t oc_offset = start_controller_op(ctx->ofpacts, ACTION_OPCODE_SLAAC,
+                                           false);
+    cs.n_values = cs.n_values - 2;
+    /* Put prefixes number. */
+    ofpbuf_put(ctx->ofpacts, &cs.n_values, 1);
+    /* Put prefixes. */
+    int plen = 0;
+    for (size_t i = 0; 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);
+    }
+    /* Put MTU. */
+    ofpbuf_put(ctx->ofpacts, &cs.values[cs.n_values].value.be32[31],
+                             sizeof(ovs_be32));
+    /* Put SLL. */
+    ofpbuf_put(ctx->ofpacts, &cs.values[cs.n_values + 1].value.mac,
+                             sizeof(struct eth_addr));
+    finish_controller_op(ctx->ofpacts, oc_offset);
+
+exit:
+    expr_constant_set_destroy(&cs);
+}
+
 static bool
 parse_action(struct action_context *ctx)
 {
@@ -1125,6 +1190,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 1649c05..730e749 100644
--- a/ovn/lib/expr.c
+++ b/ovn/lib/expr.c
@@ -406,15 +406,6 @@  expr_print(const struct expr *e)
 
 /* Parsing. */
 
-/* Context maintained during expr_parse(). */
-struct expr_context {
-    struct lexer *lexer;        /* Lexer for pulling more tokens. */
-    const struct shash *symtab; /* Symbol table. */
-    const struct shash *macros; /* Table of macros. */
-    char *error;                /* Error, if any, otherwise NULL. */
-    bool not;                   /* True inside odd number of NOT operators. */
-};
-
 struct expr *expr_parse__(struct expr_context *);
 static void expr_not(struct expr *);
 static bool parse_field(struct expr_context *, struct expr_field *);
@@ -757,7 +748,7 @@  parse_macros(struct expr_context *ctx, struct expr_constant_set *cs,
     return true;
 }
 
-static bool
+bool
 parse_constant(struct expr_context *ctx, struct expr_constant_set *cs,
                size_t *allocated_values)
 {
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 13c9526..66a9009 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1151,8 +1151,38 @@ 
           </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>: One or more IPv6 prefixes, 16-bit MUT, 48-bit
+            mac address.
+            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_arp(<var>P</var>, <var>A</var>);</code></dt>
+
+        <dt><code>get_nd(<var>P</var>, <var>A</var>);</code></dt>
         <dd>
           <p>
             <b>Parameters</b>: logical port string field <var>P</var>, 128-bit
diff --git a/tests/ovn.at b/tests/ovn.at
index 54fa8c5..454cb24 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -701,7 +701,11 @@  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)
+
+# ra for slaac
+nd_ra{slaac(fdad:a0f9:a012::/64,fdad:a0f9:c593::/64,1450,fa:16:3e:32:3c:e0); outport = inport; flags.loopback = 1; output;}; => actions=controller(userdata=00.00.00.05.00.00.00.00.ff.ff.00.50.00.00.23.20.00.25.00.00.00.00.00.00.00.03.00.39.00.00.00.06.00.00.00.00.02.fd.ad.a0.f9.a0.12.00.00.00.00.00.00.00.00.00.00.40.fd.ad.a0.f9.c5.93.00.00.00.00.00.00.00.00.00.00.40.00.00.05.aa.fa.16.3e.32.3c.e0.00.00.00.00.00.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.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
+nd_ra{slaac(fd80:a123:b345::/64,1450,fa:16:3e:62:f1:e6); 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.01.fd.80.a1.23.b3.45.00.00.00.00.00.00.00.00.00.00.40.00.00.05.aa.fa.16.3e.62.f1.e6.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
 
 # Contradictionary prerequisites (allowed but not useful):
 ip4.src = ip6.src[0..31]; => actions=move:NXM_NX_IPV6_SRC[0..31]->NXM_OF_IP_SRC[], prereqs=eth.type == 0x800 && eth.type == 0x86dd
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index acb6a99..3a7c406 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -213,7 +213,11 @@  create_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",