diff --git a/lib/packets.c b/lib/packets.c
index 1bf887e..f4e5e4d 100644
--- a/lib/packets.c
+++ b/lib/packets.c
@@ -35,6 +35,7 @@
 
 const struct in6_addr in6addr_exact = IN6ADDR_EXACT_INIT;
 const struct in6_addr in6addr_all_hosts = IN6ADDR_ALL_HOSTS_INIT;
+const struct in6_addr in6addr_all_routers = IN6ADDR_ALL_ROUTERS_INIT;
 
 struct in6_addr
 flow_tnl_dst(const struct flow_tnl *tnl)
@@ -1404,6 +1405,110 @@ compose_na(struct dp_packet *b,
                                                       ND_MSG_LEN + ND_OPT_LEN));
 }
 
+void
+compose_ra(struct dp_packet *b,
+           const struct eth_addr eth_src, const ovs_be32 ipv6_src[4],
+           const ovs_be32 router_prefix[4], uint8_t plen,
+           ovs_be16 hlmo_flags, ovs_be16 router_lifetime,
+           ovs_be32 reachable_time,ovs_be32 retrans_timer,
+           uint8_t la_flags, ovs_be32 valid_lifetime,
+           ovs_be32 preferred_lifetime, ovs_be16 mtu)
+{
+    struct ovs_ra_msg *ra;
+    struct ovs_ra_prefix *ra_prefix;
+    struct ovs_ra_mtu *ra_mtu;
+    struct ovs_nd_opt *ra_sll;
+    uint32_t icmp_csum;
+
+    eth_compose(b, eth_addr_ip6_all_hosts, eth_src,
+                ETH_TYPE_IPV6, IPV6_HEADER_LEN);
+    ra = compose_ipv6(b, IPPROTO_ICMPV6, ipv6_src,
+                      ALIGNED_CAST(ovs_be32 *, in6addr_all_hosts.s6_addr),
+                      0, 0, 255,
+                      RA_MSG_LEN + RA_PREFIX_LEN + RA_MTU_LEN + ND_OPT_LEN);
+
+    ra->icmph.icmp6_type = ND_ROUTER_ADVERT;
+    ra->icmph.icmp6_code = 0;
+    ra->hlmo_flags = hlmo_flags;
+    ra->router_lifetime = router_lifetime;
+    ra->reachable_time = reachable_time;
+    ra->retrans_timer = retrans_timer;
+
+    /* Set prefix information option. */
+    ra_prefix = &ra->prefix_info[0];
+    ra_prefix->ra_opt_type = ND_OPT_PREFIX_INFORMATION;
+    ra_prefix->ra_opt_len = 4;
+    ra_prefix->ra_opt_plen = plen;
+    ra_prefix->ra_opt_la = la_flags;
+    ra_prefix->ra_opt_valid_lifetime = valid_lifetime;
+    ra_prefix->ra_opt_preferred_lifetime = preferred_lifetime;
+    ra_prefix->ra_opt_reserved = 0;
+    packet_update_csum128(b, IPPROTO_ICMPV6,
+                          ra_prefix->prefix.be32, router_prefix);
+    memcpy(ra_prefix->prefix.be32, router_prefix, sizeof(ovs_be32[4]));
+
+    /* Set MTU option. */
+    ra_mtu = (struct ovs_ra_mtu *)(ra_prefix + 1);
+    ra_mtu->mtu_opt_type = ND_OPT_MTU;
+    ra_mtu->mtu_opt_len = 1;
+    ra_mtu->mtu_opt_reserved = 0;
+    ovs_be16 *csum = &(ra->icmph.icmp6_cksum);
+    *csum = recalc_csum32(*csum, ra_mtu->mtu, mtu);
+    ra_mtu->mtu = mtu;
+
+    /* Set Source Link-Layer. */
+    ra_sll = (struct ovs_nd_opt *)(ra_mtu + 1);
+    ra_sll->nd_opt_type = ND_OPT_SOURCE_LINKADDR;
+    ra_sll->nd_opt_len = 1;
+    csum = &(ra->icmph.icmp6_cksum);
+    *csum = recalc_csum48(*csum, ra_sll->nd_opt_mac, eth_src);
+    ra_sll->nd_opt_mac = eth_src;
+
+    ra->icmph.icmp6_cksum = 0;
+    icmp_csum = packet_csum_pseudoheader6(dp_packet_l3(b));
+    ra->icmph.icmp6_cksum = csum_finish(csum_continue(
+        icmp_csum, ra, RA_MSG_LEN + RA_PREFIX_LEN + RA_MTU_LEN + ND_OPT_LEN));
+}
+
+void
+insert_ra_prefix_opt(struct dp_packet *b,
+                     const ovs_be32 router_prefix[4], uint8_t plen,
+                     uint8_t la_flags, ovs_be32 valid_lifetime,
+                     ovs_be32 preferred_lifetime)
+{
+    struct ip6_hdr *nh = dp_packet_l3(b);
+    struct ovs_ra_msg *ra = dp_packet_l4(b);
+    struct ovs_ra_prefix *ra_prefix;
+    uint32_t icmp_csum;
+    size_t prev_l4_size = dp_packet_l4_size(b);
+
+    /* Insert new prefix information option before previous ones. */
+    dp_packet_put_uninit(b, sizeof(struct ovs_ra_prefix));
+    nh->ip6_plen = htons(prev_l4_size + RA_PREFIX_LEN);
+    char *src = (char *) dp_packet_l4(b) + RA_MSG_LEN;
+    char *dst = src + RA_PREFIX_LEN;
+    memmove(dst, src, prev_l4_size - RA_MSG_LEN);
+    ra_prefix = &ra->prefix_info[0];
+
+    /* Set prefix information option. */
+    ra_prefix->ra_opt_type = ND_OPT_PREFIX_INFORMATION;
+    ra_prefix->ra_opt_len = 4;
+    ra_prefix->ra_opt_plen = plen;
+    ra_prefix->ra_opt_la = la_flags;
+    ra_prefix->ra_opt_valid_lifetime = valid_lifetime;
+    ra_prefix->ra_opt_preferred_lifetime = preferred_lifetime;
+    ra_prefix->ra_opt_reserved = 0;
+    packet_update_csum128(b, IPPROTO_ICMPV6,
+                          ra_prefix->prefix.be32, router_prefix);
+    memcpy(ra_prefix->prefix.be32, router_prefix, sizeof(ovs_be32[4]));
+
+    /* Recalculate checksum. */
+    ra->icmph.icmp6_cksum = 0;
+    icmp_csum = packet_csum_pseudoheader6(dp_packet_l3(b));
+    ra->icmph.icmp6_cksum = csum_finish(csum_continue(
+        icmp_csum, ra, prev_l4_size + RA_PREFIX_LEN));
+}
+
 uint32_t
 packet_csum_pseudoheader(const struct ip_header *ip)
 {
diff --git a/lib/packets.h b/lib/packets.h
index 5fd1e51..7fb0473 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -161,6 +161,12 @@ static const struct eth_addr eth_addr_lacp OVS_UNUSED
 static const struct eth_addr eth_addr_bfd OVS_UNUSED
     = { { { 0x00, 0x23, 0x20, 0x00, 0x00, 0x01 } } };
 
+static const struct eth_addr eth_addr_ip6_all_hosts OVS_UNUSED
+    = { { { 0x33, 0x33, 0x00, 0x00, 0x00, 0x01 } } };
+
+static const struct eth_addr eth_addr_ip6_all_routers OVS_UNUSED
+    = { { { 0x33, 0x33, 0x00, 0x00, 0x00, 0x02 } } };
+
 static inline bool eth_addr_is_broadcast(const struct eth_addr a)
 {
     return (a.be16[0] & a.be16[1] & a.be16[2]) == htons(0xffff);
@@ -844,6 +850,43 @@ BUILD_ASSERT_DECL(ND_MSG_LEN == sizeof(struct ovs_nd_msg));
 #define ND_RSO_SOLICITED 0x40000000
 #define ND_RSO_OVERRIDE  0x20000000
 
+/* Router Advertisment Prefix Information option field. */
+#define RA_PREFIX_LEN 32
+struct ovs_ra_prefix {
+    uint8_t  ra_opt_type;      /* Values defined in icmp6.h */
+    uint8_t  ra_opt_len;
+    uint8_t  ra_opt_plen;
+    /* on-link flag and autonomous address-configuration flag */
+    uint8_t ra_opt_la;
+    ovs_be32 ra_opt_valid_lifetime;
+    ovs_be32 ra_opt_preferred_lifetime;
+    ovs_be32 ra_opt_reserved;
+    union ovs_16aligned_in6_addr prefix;
+};
+BUILD_ASSERT_DECL(RA_PREFIX_LEN == sizeof(struct ovs_ra_prefix));
+
+#define RA_MTU_LEN 8
+struct ovs_ra_mtu {
+    uint8_t  mtu_opt_type;      /* Values defined in icmp6.h */
+    uint8_t  mtu_opt_len;
+    ovs_be16 mtu_opt_reserved;
+    ovs_be32 mtu;
+};
+BUILD_ASSERT_DECL(RA_MTU_LEN == sizeof(struct ovs_ra_mtu));
+
+#define RA_MSG_LEN 16
+struct ovs_ra_msg {
+    struct icmp6_header icmph;
+    ovs_be16 hlmo_flags;
+    ovs_be16 router_lifetime;
+    ovs_be32 reachable_time;
+    ovs_be32 retrans_timer;
+    struct ovs_ra_prefix prefix_info[0];
+    struct ovs_ra_mtu mtu[0];
+    struct ovs_nd_opt sll[0];
+};
+BUILD_ASSERT_DECL(RA_MSG_LEN == sizeof(struct ovs_ra_msg));
+
 /*
  * Use the same struct for MLD and MLD2, naming members as the defined fields in
  * in the corresponding version of the protocol, though they are reserved in the
@@ -898,6 +941,10 @@ extern const struct in6_addr in6addr_all_hosts;
 #define IN6ADDR_ALL_HOSTS_INIT { { { 0xff,0x02,0x00,0x00,0x00,0x00,0x00,0x00, \
                                      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01 } } }
 
+extern const struct in6_addr in6addr_all_routers;
+#define IN6ADDR_ALL_ROUTERS_INIT { { { 0xff,0x02,0x00,0x00,0x00,0x00,0x00,0x00, \
+                                       0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02 } } }
+
 static inline bool ipv6_addr_equals(const struct in6_addr *a,
                                     const struct in6_addr *b)
 {
@@ -1093,6 +1140,17 @@ void compose_na(struct dp_packet *,
                 const struct eth_addr eth_src, const struct eth_addr eth_dst,
                 const ovs_be32 ipv6_src[4], const ovs_be32 ipv6_dst[4],
                 ovs_be32 rso_flags);
+void compose_ra(struct dp_packet *,
+                const struct eth_addr eth_src, const ovs_be32 ipv6_src[4],
+                const ovs_be32 router_prefix[4], uint8_t plen,
+                ovs_be16 hlmo_flags, ovs_be16 router_lifetime,
+                ovs_be32 reachable_time,ovs_be32 retrans_timer,
+                uint8_t la_flags, ovs_be32 valid_lifetime,
+                ovs_be32 preferred_lifetime, ovs_be16 mtu);
+void insert_ra_prefix_opt(struct dp_packet *,
+                          const ovs_be32 router_prefix[4], uint8_t plen,
+                          uint8_t la_flags, ovs_be32 valid_lifetime,
+                          ovs_be32 preferred_lifetime);
 uint32_t packet_csum_pseudoheader(const struct ip_header *);
 void IP_ECN_set_ce(struct dp_packet *pkt, bool is_ipv6);
 
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index 0ae6501..b9a71e4 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -72,6 +72,9 @@ static void send_garp_run(const struct ovsrec_bridge *,
 static void pinctrl_handle_na(const struct flow *ip_flow,
                               const struct match *md,
                               struct ofpbuf *userdata);
+static void pinctrl_handle_ra(struct ofputil_packet_in *pin,
+                              struct ofpbuf *userdata,
+                              struct ofpbuf *continuation);
 static void reload_metadata(struct ofpbuf *ofpacts,
                             const struct match *md);
 
@@ -414,6 +417,10 @@ process_packet_in(const struct ofp_header *msg)
         pinctrl_handle_na(&headers, &pin.flow_metadata, &userdata);
         break;
 
+    case ACTION_OPCODE_RA:
+        pinctrl_handle_ra(&pin, &userdata, &continuation);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -1008,3 +1015,83 @@ exit:
     dp_packet_uninit(&packet);
     ofpbuf_uninit(&ofpacts);
 }
+
+static void
+pinctrl_handle_ra(struct ofputil_packet_in *pin,
+                  struct ofpbuf *userdata,
+                  struct ofpbuf *continuation)
+{
+    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);
+
+    /* 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.");
+        return;
+    }
+
+    /* Get prefixes. */
+    size_t prefixes_size = sizeof(struct ipv6_netaddr) * (*n_prefixes);
+    struct ipv6_netaddr *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;
+    }
+
+    /* Get MTU. */
+    ovs_be16 *mtu = ofpbuf_try_pull(userdata, sizeof(ovs_be16));
+
+    /* Get SLL. */
+    struct eth_addr *sll = ofpbuf_try_pull(userdata,
+                                           sizeof(struct eth_addr));
+
+    struct in6_addr lla;
+    in6_generate_lla(*sll, &lla);
+    ovs_be32 ipv6_src[4], router_prefix[4];
+    memcpy(ipv6_src, &lla, sizeof ipv6_src);
+    memcpy(router_prefix, &prefixes[0].addr, sizeof router_prefix);
+
+    /* Flags: hop-limit = 64, M=0, O=0. */
+    ovs_be16 hlmo_flags = htons(0x4000);
+    ovs_be16 router_lifetime = htons(0x2a30);  // 3 hours.
+    uint8_t la_flags = 0xc0;
+    ovs_be32 valid_lifetime = htonl(0x2a30);
+    ovs_be32 preferred_lifetime = htonl(0x2a30);
+    /* Frame the RA packet. */
+    uint64_t packet_stub[256 / 8];
+    struct dp_packet packet;
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
+    compose_ra(&packet,
+               *sll, ipv6_src,
+               router_prefix, prefixes[0].plen,
+               hlmo_flags, router_lifetime, 0, 0,
+               la_flags, valid_lifetime, preferred_lifetime,
+               *mtu);
+    for (size_t i = 1; i < *n_prefixes; i++) {
+        memcpy(router_prefix, &prefixes[i].addr, sizeof router_prefix);
+        insert_ra_prefix_opt(&packet, router_prefix, prefixes[i].plen,
+                             la_flags, valid_lifetime, preferred_lifetime);
+    }
+
+    pin->packet = dp_packet_data(&packet);
+    pin->packet_len = dp_packet_size(&packet);
+    queue_msg(ofputil_encode_resume(pin, continuation, proto));
+
+exit:
+    dp_packet_uninit(&packet);
+    free(prefixes);
+}
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 6e2bf93..e411517 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -1029,6 +1029,74 @@ parse_ct_nat(struct action_context *ctx, bool snat)
     ofpbuf_push_uninit(ctx->ofpacts, ct_offset);
 }
 
+static void
+parse_ra_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_RA,
+                                           true);
+    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.be16[63],
+                             sizeof(ovs_be16));
+    /* 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);
+
+    expr_destroy(ctx->prereqs);
+    add_prerequisite(ctx, "icmp6 && icmp6.type == 133 && icmp6.code == 0");
+
+exit:
+    expr_constant_set_destroy(&cs);
+}
+
 static bool
 parse_action(struct action_context *ctx)
 {
@@ -1066,6 +1134,8 @@ parse_action(struct action_context *ctx)
         parse_nested_action(ctx, ACTION_OPCODE_ARP, "ip4");
     } else if (lexer_match_id(ctx->lexer, "na")) {
         parse_nested_action(ctx, ACTION_OPCODE_NA, "nd");
+    } else if (lexer_match_id(ctx->lexer, "ra")) {
+        parse_ra_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "get_arp")) {
         parse_get_arp_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "put_arp")) {
diff --git a/ovn/lib/actions.h b/ovn/lib/actions.h
index 114c71e..c77c463 100644
--- a/ovn/lib/actions.h
+++ b/ovn/lib/actions.h
@@ -78,6 +78,15 @@ enum action_opcode {
      * The actions, in OpenFlow 1.3 format, follow the action_header.
      */
     ACTION_OPCODE_NA,
+
+    /* ra (prefix0, [prefix1, ...,] mtu, sll)".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - One or more IPv6 prefixes.
+     *   - A 16-bit MTU.
+     *   - A 48-bit Ether address.
+     */
+    ACTION_OPCODE_RA,
 };
 
 /* Header. */
diff --git a/ovn/lib/expr.c b/ovn/lib/expr.c
index 1c38b99..e210faf 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/lib/expr.h b/ovn/lib/expr.h
index d790c49..150f2e7 100644
--- a/ovn/lib/expr.h
+++ b/ovn/lib/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/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 17fbf29..0c3f336 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -102,7 +102,8 @@ enum ovn_stage {
     PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,     9, "ls_in_arp_rsp")      \
     PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,   10, "ls_in_dhcp_options") \
     PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE,  11, "ls_in_dhcp_response") \
-    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,        12, "ls_in_l2_lkup")      \
+    PIPELINE_STAGE(SWITCH, IN,  RA_RSP,         12, "ls_in_ra_rsp")      \
+    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,        13, "ls_in_l2_lkup")      \
                                                                       \
     /* Logical switch egress stages. */                               \
     PIPELINE_STAGE(SWITCH, OUT, PRE_LB,       0, "ls_out_pre_lb")     \
@@ -2218,7 +2219,54 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP_RESPONSE, 0, "1", "next;");
     }
 
-    /* Ingress table 12: Destination lookup, broadcast and multicast handling
+    /* Ingress table 12: RA responder. */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (!od->nbs) {
+            continue;
+        }
+
+        /* By default goto next. (priority 0)*/
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_RA_RSP, 0, "1", "next;");
+
+        /* Reply for known router ports. (priority 50) */
+        if (!od->n_router_ports) {
+            continue;
+        }
+        ds_clear(&match);
+        ds_put_cstr(&match, "eth.dst == 33:33:00:00:00:02"
+                            " && ip6.dst == ff02::2 && icmp6"
+                            " && icmp6.type == 133 && icmp6.code == 0");
+        ds_clear(&actions);
+        ds_put_format(&actions, "ra(");
+        size_t actions_len = actions.length;
+        for (size_t i = 0; i != od->n_router_ports; i++) {
+            op = od->router_ports[i];
+            if (!op->peer) {
+                continue;
+            }
+            op = op->peer;
+            if (!op->lrp_networks.n_ipv6_addrs
+                || !op->nbrp->slaac
+                || !*op->nbrp->slaac) {
+                continue;
+            }
+            for (size_t j = 0; j != op->lrp_networks.n_ipv6_addrs; j++) {
+                ds_put_format(&actions, "%s/%u,",
+                              op->lrp_networks.ipv6_addrs[j].network_s,
+                              op->lrp_networks.ipv6_addrs[j].plen);
+            }
+            ds_put_format(&actions, "%ld,", od->nbs->mtu);
+            ds_put_cstr(&actions, op->lrp_networks.ea_s);
+        }
+        if (actions.length != actions_len) {
+            ds_put_cstr(&actions, "); outport = inport; inport = \"\"; "
+                                  "output;");
+            ovn_lflow_add(lflows, od, S_SWITCH_IN_RA_RSP, 50,
+                          ds_cstr(&match), ds_cstr(&actions));
+        }
+    }
+
+    /* Ingress table 13: Destination lookup, broadcast and multicast handling
      * (priority 100). */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbsp) {
@@ -2238,7 +2286,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                       "outport = \""MC_FLOOD"\"; output;");
     }
 
-    /* Ingress table 12: Destination lookup, unicast handling (priority 50), */
+    /* Ingress table 13: Destination lookup, unicast handling (priority 50), */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbsp) {
             continue;
@@ -2271,7 +2319,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
     }
 
-    /* Ingress table 12: Destination lookup for unknown MACs (priority 0). */
+    /* Ingress table 13: Destination lookup for unknown MACs (priority 0). */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
             continue;
diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
index 3cf07c1..7ef23d3 100644
--- a/ovn/ovn-nb.ovsschema
+++ b/ovn/ovn-nb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Northbound",
     "version": "5.1.0",
-    "cksum": "2201958537 8295",
+    "cksum": "912529793 8553",
     "tables": {
         "Logical_Switch": {
             "columns": {
@@ -21,6 +21,9 @@
                                                   "refType": "strong"},
                                            "min": 0,
                                            "max": 1}},
+                "mtu": {"type": {"key": {"type": "integer",
+                                         "minInteger": 0,
+                                         "maxInteger": 65535}}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
@@ -134,6 +137,7 @@
                 "mac": {"type": "string"},
                 "peer": {"type": {"key": "string", "min": 0, "max": 1}},
                 "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}},
+                "slaac": {"type": {"key": "boolean", "min": 0, "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index abd0340..d01c9f1 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -78,6 +78,10 @@
       Access control rules that apply to packets within the logical switch.
     </column>
 
+    <column name="mtu">
+      Logical Switch MTU.
+    </column>
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
@@ -820,6 +824,13 @@
       port has all ingress and egress traffic dropped.
     </column>
 
+    <column name="slaac">
+      Setting <code>true</code> specifies the logical switch subnets behind
+      this router port IPv6 networks are going to use SLAAC as IPv6 address
+      configuration and RA mode. Otherwise set this column to
+      <code>false</code>, or this router port has no IPv6 networks.
+    </column>
+
     <group title="Attachment">
       <p>
         A given router port serves one of two purposes:
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 3d26e65..bde036d 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1112,6 +1112,30 @@
           </p>
         </dd>
 
+        <dt>
+          <code>ra (<var>p</var>, <var>m</var>, <var>s</var>);</code>
+        </dt>
+
+        <dd>
+          <p>
+            Generate an IPv6 router advertisement (RA) packet per 'ra'
+            action ordered parameters.
+            <b>Parameters</b>: One or more IPv6 prefixes <var>p</var>,
+            16-bit MUT <var>m</var>, 48-bit source link-layer
+            address <var>s</var>.
+          </p>
+
+          <p>
+            The ND packet has the same VLAN header, if any, as the IPv6 packet
+            it replaces.
+          </p>
+
+          <p>
+            <b>Prerequisite:</b>
+              <code>icmp6 &amp;&amp; icmp6.type == 133 &amp;&amp; icmp6.code == 0</code>
+          </p>
+        </dd>
+
         <dt><code>get_arp(<var>P</var>, <var>A</var>);</code></dt>
 
         <dd>
diff --git a/tests/ovn.at b/tests/ovn.at
index 86efcf5..f315747 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -655,6 +655,9 @@ reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4); => DHCP option domain
 # na
 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.00.19.00.10.00.00.00.02.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
 
+# ra
+ra(fdad:a0f9:a012::/64,fdad:a0f9:c593::/64,1422,fa:16:3e:32:3c:e0); => actions=controller(userdata=00.00.00.04.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.05.8e.fa.16.3e.32.3c.e0,pause), prereqs=icmp6 && icmp6.type == 0x85 && icmp6.code == 0
+
 # 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
 ip4.src <-> ip6.src[0..31]; => actions=push:NXM_NX_IPV6_SRC[0..31],push:NXM_OF_IP_SRC[],pop:NXM_NX_IPV6_SRC[0..31],pop:NXM_OF_IP_SRC[], prereqs=eth.type == 0x800 && eth.type == 0x86dd
@@ -3791,6 +3794,82 @@ OVN_CLEANUP([hv1])
 
 AT_CLEANUP
 
+AT_SETUP([ovn -- router advertisement responder])
+AT_KEYWORDS([ovn-ra])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+# In this test cases we create 1 lswitch, it has 2 VIF ports attached
+# with. NS packet we test, from one VIF for another VIF, will be replied
+# by local ovn-controller, but not by target VIF.
+
+# Create hypervisors and logical switch lsw0, logical router lr0, attach lsw0 onto lr0.
+ovn-nbctl ls-add lsw0
+ovn-nbctl lr-add lr0
+ovn-nbctl lrp-add lr0 lrp0 fa:16:3e:32:3c:e0 fdad:a0f9:a012::1/64 fdad:a0f9:c593::1/64
+ovn-nbctl set Logical_Router_Port lrp0 slaac="true"
+ovn-nbctl \
+    -- lsp-add lsw0 lsp0 \
+    -- set Logical_Switch_Port lsp0 type=router \
+                     options:router-port=lrp0 \
+                     addresses='"fa:16:3e:32:3c:e0 fdad:a0f9:a012::1 fdad:a0f9:c593::1"'
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+
+# Add vif1 to hv1 and lsw0, turn on l2 port security on vif1.
+ovs-vsctl add-port br-int vif1 -- set Interface vif1 external-ids:iface-id=lp1 options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap ofport-request=1
+ovn-nbctl lsp-add lsw0 lp1
+ovn-nbctl set logical-switch lsw0 mtu=1450
+ovn-nbctl lsp-set-addresses lp1 "fa:16:3e:6e:a1:42 10.0.0.12 fdad:a0f9:a012:0:f816:3eff:fe6e:a142 fdad:a0f9:c593:0:f816:3eff:fe6e:a142"
+ovn-nbctl lsp-set-port-security lp1 "fa:16:3e:6e:a1:42 10.0.0.12 fdad:a0f9:a012:0:f816:3eff:fe6e:a142 fdad:a0f9:c593:0:f816:3eff:fe6e:a142"
+
+# Add ACL rule for ICMPv6 on lsw0
+ovn-nbctl acl-add lsw0 from-lport 1002 'ip6 && icmp6'  allow-related
+ovn-nbctl acl-add lsw0 to-lport 1002 'outport == "lp1" && ip6 && icmp6'  allow-related
+
+# Allow some time for ovn-northd and ovn-controller to catch up.
+# XXX This should be more systematic.
+sleep 1
+
+# Given the name of a logical port, prints the name of the hypervisor
+# on which it is located.
+vif_to_hv() {
+    echo hv1${1%?}
+}
+trim_zeros() {
+    sed 's/\(00\)\{1,\}$//'
+}
+for i in 1 2; do
+    : > $i.expected
+done
+
+# Complete Router Solicitation packet and Router Advertisement packet.
+rs_packet=333300000002fa163e6ea14286dd6000000000103afffe80000000000000f8163efffe6ea142ff0200000000000000000000000000028500cb9e000000000101fa163e6ea142
+ra_packet=333300000001fa163e323ce086dd6000000000603afffe80000000000000f8163efffe323ce0ff02000000000000000000000000000186004b7240002a300000000000000000030440c000002a3000002a3000000000fdada0f9c59300000000000000000000030440c000002a3000002a3000000000fdada0f9a012000000000000000000000501000005aa00000101fa163e323ce0
+
+as hv1 ovs-appctl netdev-dummy/receive vif1 $rs_packet
+echo $ra_packet | trim_zeros >> 1.expected
+
+sleep 1
+
+echo "------ hv1 dump ------"
+as hv1 ovs-vsctl show
+as hv1 ovs-ofctl -O OpenFlow13 show br-int
+as hv1 ovs-ofctl -O OpenFlow13 dump-flows br-int
+
+file=hv1/vif1-tx.pcap
+echo $file
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" $file | trim_zeros > 1.packets
+cat 1.expected > expout
+AT_CHECK([cat 1.packets], [0], [expout])
+
+OVN_CLEANUP([hv1])
+
+AT_CLEANUP
+
 AT_SETUP([ovn -- address sets modification/removal smoke test])
 AT_KEYWORDS([ovn-addr])
 ovn_start
