diff mbox

[ovs-dev,RFC,v1] ovn: add SLAAC support for IPv6

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

Commit Message

Zong Kai LI July 27, 2016, 8:02 a.m. UTC
This patch introduces(from north to south):
- OVN NB DB:
 * mtu(integer) column for Logical_Switch, used by Router Advertisement
   (RA) message MTU option.
 * slaac(bool) column for Logical_Router_Port to specify the IPv6 networks
   on a rotuer port is in SLAAC mode or not, if it has IPv6 networks. [1]
- ovn-northd:
   New table ls_in_ra_rsp to install RA responder flows, which matching
   Router Solicitation (RS) packets and have actions such as:
   action=(ra(fd80:1234::/64,2001:db8::/64,1450,fa:16:3e:12:34:56);
           outport = inport; inport = ""; output;) [2]
   while 'ra' is new action. The RA responder flows are built on logical
   switch pipeline per each logical switch. [3]
- ovn-controller:
   New action 'ra' has ordered parameters and uses 'continuation'.[4]
   The ordered parameters including: at least 1 IPv6 prefix, 1 MTU,
   and 1 ether address. They will be used in prefix information option(s),
   MTU option, SLL option for RA packet.
- lib/packet:
 * compose_ra to compose a RA packet with 1 prefix information
   option, 1 MTU option and 1 SLL option.
 * insert_ra_prefix_opt insert a prefix information option into a RA
   packet.

[1] A logical switch may have different IPv6 subnets using DHCPv6 or SLAAC,
so set 'slaac' as Logical_Switch column will not work well. Adding 'slaac'
as Logical_Router_Port may not be a good solution, but it works for now.

[2] SLAAC is the first aim of this patch, but not RA responder. So it wont
take care of lots of flags or options in RA packet.
A further work will try to support those flags or options.

[3] This works for case (a) all logical switch IPv6 subnets attached onto the
same router port, which OpenStack Neutron using this way. And it won't need
flows to route RS packets to router ports which is necessary for installing
RA responder flows on logical router pipeline.
A further work will replace this by installing RA responder flows on logical
router pipeline, which will support case (b) logical switch have IPv6 subnets
in SLAAC mode attached onto different router ports.

[4] Ordered parameters isn't a friendly way to use 'ra' action, it will be
fixed with [2].
From my side, it's fine to use 'continuation' for case (a) in [3]. But for
case (b) in [3], since RS packet will be dispatched several router ports,
which means ovn-controller will compose several RA packets for one RS packet,
I'm not sure whether it's good to keep use 'continuation'.

Signed-off-by: Zong Kai LI <zealokii@gmail.com>
---
 lib/packets.c            | 105 +++++++++++++++++++++++++++++++++++++++++++++++
 lib/packets.h            |  58 ++++++++++++++++++++++++++
 ovn/controller/pinctrl.c |  87 +++++++++++++++++++++++++++++++++++++++
 ovn/lib/actions.c        |  70 +++++++++++++++++++++++++++++++
 ovn/lib/actions.h        |   9 ++++
 ovn/lib/expr.c           |  11 +----
 ovn/lib/expr.h           |  11 +++++
 ovn/northd/ovn-northd.c  |  56 +++++++++++++++++++++++--
 ovn/ovn-nb.ovsschema     |   6 ++-
 ovn/ovn-nb.xml           |  11 +++++
 ovn/ovn-sb.xml           |  24 +++++++++++
 tests/ovn.at             |  79 +++++++++++++++++++++++++++++++++++
 12 files changed, 512 insertions(+), 15 deletions(-)

Comments

Ben Pfaff July 28, 2016, 6:04 a.m. UTC | #1
On Wed, Jul 27, 2016 at 04:02:11PM +0800, Zong Kai LI wrote:
> This patch introduces(from north to south):
> - OVN NB DB:
>  * mtu(integer) column for Logical_Switch, used by Router Advertisement
>    (RA) message MTU option.
>  * slaac(bool) column for Logical_Router_Port to specify the IPv6 networks
>    on a rotuer port is in SLAAC mode or not, if it has IPv6 networks. [1]
> - ovn-northd:
>    New table ls_in_ra_rsp to install RA responder flows, which matching
>    Router Solicitation (RS) packets and have actions such as:
>    action=(ra(fd80:1234::/64,2001:db8::/64,1450,fa:16:3e:12:34:56);
>            outport = inport; inport = ""; output;) [2]
>    while 'ra' is new action. The RA responder flows are built on logical
>    switch pipeline per each logical switch. [3]
> - ovn-controller:
>    New action 'ra' has ordered parameters and uses 'continuation'.[4]
>    The ordered parameters including: at least 1 IPv6 prefix, 1 MTU,
>    and 1 ether address. They will be used in prefix information option(s),
>    MTU option, SLL option for RA packet.
> - lib/packet:
>  * compose_ra to compose a RA packet with 1 prefix information
>    option, 1 MTU option and 1 SLL option.
>  * insert_ra_prefix_opt insert a prefix information option into a RA
>    packet.
> 
> [1] A logical switch may have different IPv6 subnets using DHCPv6 or SLAAC,
> so set 'slaac' as Logical_Switch column will not work well. Adding 'slaac'
> as Logical_Router_Port may not be a good solution, but it works for now.
> 
> [2] SLAAC is the first aim of this patch, but not RA responder. So it wont
> take care of lots of flags or options in RA packet.
> A further work will try to support those flags or options.
> 
> [3] This works for case (a) all logical switch IPv6 subnets attached onto the
> same router port, which OpenStack Neutron using this way. And it won't need
> flows to route RS packets to router ports which is necessary for installing
> RA responder flows on logical router pipeline.
> A further work will replace this by installing RA responder flows on logical
> router pipeline, which will support case (b) logical switch have IPv6 subnets
> in SLAAC mode attached onto different router ports.
> 
> [4] Ordered parameters isn't a friendly way to use 'ra' action, it will be
> fixed with [2].
> From my side, it's fine to use 'continuation' for case (a) in [3]. But for
> case (b) in [3], since RS packet will be dispatched several router ports,
> which means ovn-controller will compose several RA packets for one RS packet,
> I'm not sure whether it's good to keep use 'continuation'.
> 
> Signed-off-by: Zong Kai LI <zealokii@gmail.com>

The commit message here is so detailed that I can't figure out what the
actual goal is.  What is SLAAC and how does it benefit the user or
administrator?

Justin has been working on IPv6 support so I'm adding him in case he has
comments.

Thanks,

Ben.
Zong Kai LI July 29, 2016, 9:21 a.m. UTC | #2
On Thu, Jul 28, 2016 at 2:04 PM, Ben Pfaff <blp@ovn.org> wrote:
> On Wed, Jul 27, 2016 at 04:02:11PM +0800, Zong Kai LI wrote:
>> This patch introduces(from north to south):
>> - OVN NB DB:
>>  * mtu(integer) column for Logical_Switch, used by Router Advertisement
>>    (RA) message MTU option.
>>  * slaac(bool) column for Logical_Router_Port to specify the IPv6 networks
>>    on a rotuer port is in SLAAC mode or not, if it has IPv6 networks. [1]
>> - ovn-northd:
>>    New table ls_in_ra_rsp to install RA responder flows, which matching
>>    Router Solicitation (RS) packets and have actions such as:
>>    action=(ra(fd80:1234::/64,2001:db8::/64,1450,fa:16:3e:12:34:56);
>>            outport = inport; inport = ""; output;) [2]
>>    while 'ra' is new action. The RA responder flows are built on logical
>>    switch pipeline per each logical switch. [3]
>> - ovn-controller:
>>    New action 'ra' has ordered parameters and uses 'continuation'.[4]
>>    The ordered parameters including: at least 1 IPv6 prefix, 1 MTU,
>>    and 1 ether address. They will be used in prefix information option(s),
>>    MTU option, SLL option for RA packet.
>> - lib/packet:
>>  * compose_ra to compose a RA packet with 1 prefix information
>>    option, 1 MTU option and 1 SLL option.
>>  * insert_ra_prefix_opt insert a prefix information option into a RA
>>    packet.
>>
>> [1] A logical switch may have different IPv6 subnets using DHCPv6 or SLAAC,
>> so set 'slaac' as Logical_Switch column will not work well. Adding 'slaac'
>> as Logical_Router_Port may not be a good solution, but it works for now.
>>
>> [2] SLAAC is the first aim of this patch, but not RA responder. So it wont
>> take care of lots of flags or options in RA packet.
>> A further work will try to support those flags or options.
>>
>> [3] This works for case (a) all logical switch IPv6 subnets attached onto the
>> same router port, which OpenStack Neutron using this way. And it won't need
>> flows to route RS packets to router ports which is necessary for installing
>> RA responder flows on logical router pipeline.
>> A further work will replace this by installing RA responder flows on logical
>> router pipeline, which will support case (b) logical switch have IPv6 subnets
>> in SLAAC mode attached onto different router ports.
>>
>> [4] Ordered parameters isn't a friendly way to use 'ra' action, it will be
>> fixed with [2].
>> From my side, it's fine to use 'continuation' for case (a) in [3]. But for
>> case (b) in [3], since RS packet will be dispatched several router ports,
>> which means ovn-controller will compose several RA packets for one RS packet,
>> I'm not sure whether it's good to keep use 'continuation'.
>>
>> Signed-off-by: Zong Kai LI <zealokii@gmail.com>
>
> The commit message here is so detailed that I can't figure out what the
> actual goal is.  What is SLAAC and how does it benefit the user or
> administrator?
>
> Justin has been working on IPv6 support so I'm adding him in case he has
> comments.
>
> Thanks,
>
> Ben.

Hi, Ben.
Sorry for unclear commit message, I've fixed some details of this RFC
patch, and will try to rewrite the commit message, to make it clear
and easy to get.

SLAAC is short for stateless address autoconfiguration, it's one the
address configuration method for IPv6 (another is DHCPv6).

SLAAC is simpler than DHCPv6. After configure an IPv6 prefix/CIDR for
a subnet, people can attach the subnet onto a router, then all
VMs/VIFs on subnet can get IPv6 address under the IPv6 prefix/CIDR. By
this people may not need DHCPv6. This is helpful and friendly for
people who is not familiar with IPv6.

SLAAC need router advertisement, so I implement it in this patch, but
just a basic version, not like what Numan did in native DHCP, many
options get supported there.

Thanks,
Zongkai, LI
Ben Pfaff July 29, 2016, 5:57 p.m. UTC | #3
On Fri, Jul 29, 2016 at 05:21:32PM +0800, Zong Kai Li wrote:
> On Thu, Jul 28, 2016 at 2:04 PM, Ben Pfaff <blp@ovn.org> wrote:
> > On Wed, Jul 27, 2016 at 04:02:11PM +0800, Zong Kai LI wrote:
> >> This patch introduces(from north to south):
> >> - OVN NB DB:
> >>  * mtu(integer) column for Logical_Switch, used by Router Advertisement
> >>    (RA) message MTU option.
> >>  * slaac(bool) column for Logical_Router_Port to specify the IPv6 networks
> >>    on a rotuer port is in SLAAC mode or not, if it has IPv6 networks. [1]
> >> - ovn-northd:
> >>    New table ls_in_ra_rsp to install RA responder flows, which matching
> >>    Router Solicitation (RS) packets and have actions such as:
> >>    action=(ra(fd80:1234::/64,2001:db8::/64,1450,fa:16:3e:12:34:56);
> >>            outport = inport; inport = ""; output;) [2]
> >>    while 'ra' is new action. The RA responder flows are built on logical
> >>    switch pipeline per each logical switch. [3]
> >> - ovn-controller:
> >>    New action 'ra' has ordered parameters and uses 'continuation'.[4]
> >>    The ordered parameters including: at least 1 IPv6 prefix, 1 MTU,
> >>    and 1 ether address. They will be used in prefix information option(s),
> >>    MTU option, SLL option for RA packet.
> >> - lib/packet:
> >>  * compose_ra to compose a RA packet with 1 prefix information
> >>    option, 1 MTU option and 1 SLL option.
> >>  * insert_ra_prefix_opt insert a prefix information option into a RA
> >>    packet.
> >>
> >> [1] A logical switch may have different IPv6 subnets using DHCPv6 or SLAAC,
> >> so set 'slaac' as Logical_Switch column will not work well. Adding 'slaac'
> >> as Logical_Router_Port may not be a good solution, but it works for now.
> >>
> >> [2] SLAAC is the first aim of this patch, but not RA responder. So it wont
> >> take care of lots of flags or options in RA packet.
> >> A further work will try to support those flags or options.
> >>
> >> [3] This works for case (a) all logical switch IPv6 subnets attached onto the
> >> same router port, which OpenStack Neutron using this way. And it won't need
> >> flows to route RS packets to router ports which is necessary for installing
> >> RA responder flows on logical router pipeline.
> >> A further work will replace this by installing RA responder flows on logical
> >> router pipeline, which will support case (b) logical switch have IPv6 subnets
> >> in SLAAC mode attached onto different router ports.
> >>
> >> [4] Ordered parameters isn't a friendly way to use 'ra' action, it will be
> >> fixed with [2].
> >> From my side, it's fine to use 'continuation' for case (a) in [3]. But for
> >> case (b) in [3], since RS packet will be dispatched several router ports,
> >> which means ovn-controller will compose several RA packets for one RS packet,
> >> I'm not sure whether it's good to keep use 'continuation'.
> >>
> >> Signed-off-by: Zong Kai LI <zealokii@gmail.com>
> >
> > The commit message here is so detailed that I can't figure out what the
> > actual goal is.  What is SLAAC and how does it benefit the user or
> > administrator?
> >
> > Justin has been working on IPv6 support so I'm adding him in case he has
> > comments.
> >
> > Thanks,
> >
> > Ben.
> 
> Hi, Ben.
> Sorry for unclear commit message, I've fixed some details of this RFC
> patch, and will try to rewrite the commit message, to make it clear
> and easy to get.
> 
> SLAAC is short for stateless address autoconfiguration, it's one the
> address configuration method for IPv6 (another is DHCPv6).
> 
> SLAAC is simpler than DHCPv6. After configure an IPv6 prefix/CIDR for
> a subnet, people can attach the subnet onto a router, then all
> VMs/VIFs on subnet can get IPv6 address under the IPv6 prefix/CIDR. By
> this people may not need DHCPv6. This is helpful and friendly for
> people who is not familiar with IPv6.
> 
> SLAAC need router advertisement, so I implement it in this patch, but
> just a basic version, not like what Numan did in native DHCP, many
> options get supported there.

Thanks.  I'll look forward to the next version.
Alexey I. Froloff Oct. 19, 2016, 1:05 p.m. UTC | #4
On Wed, Jul 27, 2016 at 11:04:55PM -0700, Ben Pfaff wrote:
> The commit message here is so detailed that I can't figure out what the
> actual goal is.  What is SLAAC and how does it benefit the user or
> administrator?
I apologies for necro-posting, but Router Advertisement in not
only about address autoconfiguration.  This is an advertisement
of a router (Link-Local router address) in the first place.  In
terms of OVN - Logical_Router_Port.  Besides router, RA message
also contains information about IPv6 Prefixes (subnets),
available on this link.

Since DHCPv6 does not provide prefix length to client (assigned
addresses will have /128 prefix), communication between VMs in
one Logical_Switch will be done via configured "router"
(options:server_id).  If OS receives RA message, it will set
routing to announced prefix via network interface:

$ ip -6 ro
2001:470:XXX:XXX::/64 dev eth0  proto kernel  metric 256  expires 7035sec mtu 1280 advmss 1220 hoplimit 0
fd4b:XXX:XXX::/64 dev eth0  proto kernel  metric 256  expires 7035sec mtu 1280 advmss 1220 hoplimit 0
fe80::/64 dev eth0  proto kernel  metric 256  mtu 1280 advmss 1220 hoplimit 0
default via fe80::1 dev eth0  proto kernel  metric 1024  expires 1653sec mtu 1280 advmss 1220 hoplimit 0

So, even with DHCPv6, it would be great if OVN provide RA
support.
diff mbox

Patch

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