[ovs-dev,v9,4/4] ovn: Generate Neighbor Solicitation packet for unknown MAC IPv6 packets

Message ID 20171002162550.1923-1-nusiddiq@redhat.com
State Superseded
Headers show
Series
  • ovn IPv6: Add Router Solicitation responder support and generate Neighbor Solicitation request for unknown
Related show

Commit Message

Numan Siddique Oct. 2, 2017, 4:25 p.m.
From: Numan Siddique <nusiddiq@redhat.com>

In the router ingress pipeline, if the destination mac is unresolved
by the time the packet reaches the ARP_REQUEST stage, OVN should generate an
IPv6 Neighbor Solicitation packet to learn the MAC address. This feature is
presently missing. This patch adds this feature.  A new action "nd_ns" is
added  which replaces an IPv6 packet being processed with an IPv6 Neighbor
Solicitation packet. ovn-northd adds a flow in the ARP_REQUEST router ingress
pipeline stage if the eth.dst is zero which applies this action. This action is
similar to the IPv4 counterpart "arp" action.

OVN already has the support to learn the MAC from the IPv6 Neighbor Advertisement
packets and storing in the south bound MAC_Binding table.

Signed-off-by: Numan Siddique <nusiddiq@redhat.com>
---
 include/ovn/actions.h       |   9 +++-
 ovn/controller/pinctrl.c    | 122 +++++++++++++++++++++++---------------------
 ovn/lib/actions.c           |  22 ++++++++
 ovn/northd/ovn-northd.8.xml |  24 ++++++---
 ovn/northd/ovn-northd.c     |   8 ++-
 ovn/ovn-sb.xml              |  37 ++++++++++++++
 ovn/utilities/ovn-trace.c   |  30 +++++++++++
 tests/ovn.at                | 116 +++++++++++++++++++++++++++++++++++++++++
 8 files changed, 302 insertions(+), 66 deletions(-)

Comments

Mark Michelson Oct. 4, 2017, 6:01 p.m. | #1
On Mon, Oct 2, 2017 at 11:28 AM <nusiddiq@redhat.com> wrote:

> From: Numan Siddique <nusiddiq@redhat.com>
>
> In the router ingress pipeline, if the destination mac is unresolved
> by the time the packet reaches the ARP_REQUEST stage, OVN should generate
> an
> IPv6 Neighbor Solicitation packet to learn the MAC address. This feature is
> presently missing. This patch adds this feature.  A new action "nd_ns" is
> added  which replaces an IPv6 packet being processed with an IPv6 Neighbor
> Solicitation packet. ovn-northd adds a flow in the ARP_REQUEST router
> ingress
> pipeline stage if the eth.dst is zero which applies this action. This
> action is
> similar to the IPv4 counterpart "arp" action.
>
> OVN already has the support to learn the MAC from the IPv6 Neighbor
> Advertisement
> packets and storing in the south bound MAC_Binding table.
>
> Signed-off-by: Numan Siddique <nusiddiq@redhat.com>
>

Acked-by: Mark Michelson <mmichels@redhat.com>


> ---
>  include/ovn/actions.h       |   9 +++-
>  ovn/controller/pinctrl.c    | 122
> +++++++++++++++++++++++---------------------
>  ovn/lib/actions.c           |  22 ++++++++
>  ovn/northd/ovn-northd.8.xml |  24 ++++++---
>  ovn/northd/ovn-northd.c     |   8 ++-
>  ovn/ovn-sb.xml              |  37 ++++++++++++++
>  ovn/utilities/ovn-trace.c   |  30 +++++++++++
>  tests/ovn.at                | 116
> +++++++++++++++++++++++++++++++++++++++++
>  8 files changed, 302 insertions(+), 66 deletions(-)
>
> diff --git a/include/ovn/actions.h b/include/ovn/actions.h
> index 15cee478d..8c7208ffc 100644
> --- a/include/ovn/actions.h
> +++ b/include/ovn/actions.h
> @@ -73,7 +73,8 @@ struct simap;
>      OVNACT(SET_QUEUE,         ovnact_set_queue)       \
>      OVNACT(DNS_LOOKUP,        ovnact_dns_lookup)      \
>      OVNACT(LOG,               ovnact_log)             \
> -    OVNACT(PUT_ND_RA_OPTS,    ovnact_put_opts)
> +    OVNACT(PUT_ND_RA_OPTS,    ovnact_put_opts)        \
> +    OVNACT(ND_NS,             ovnact_nest)
>
>  /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
>  enum OVS_PACKED_ENUM ovnact_type {
> @@ -427,6 +428,12 @@ enum action_opcode {
>       *   - Any number of ICMPv6 options.
>       */
>      ACTION_OPCODE_PUT_ND_RA_OPTS,
> +
> +    /* "nd_ns { ...actions... }".
> +     *
> +     * The actions, in OpenFlow 1.3 format, follow the action_header.
> +     */
> +    ACTION_OPCODE_ND_NS,
>  };
>
>  /* Header. */
> diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
> index 3a1348937..5aedf7d0d 100644
> --- a/ovn/controller/pinctrl.c
> +++ b/ovn/controller/pinctrl.c
> @@ -85,6 +85,9 @@ static void pinctrl_handle_put_nd_ra_opts(
>      const struct flow *ip_flow, struct dp_packet *pkt_in,
>      struct ofputil_packet_in *pin, struct ofpbuf *userdata,
>      struct ofpbuf *continuation);
> +static void pinctrl_handle_nd_ns(const struct flow *ip_flow,
> +                                 const struct match *md,
> +                                 struct ofpbuf *userdata);
>
>  COVERAGE_DEFINE(pinctrl_drop_put_mac_binding);
>
> @@ -132,6 +135,43 @@ set_switch_config(struct rconn *swconn,
>  }
>
>  static void
> +set_actions_and_enqueue_msg(const struct dp_packet *packet,
> +                           const struct match *md,
> +                           struct ofpbuf *userdata)
> +{
> +    /* Copy metadata from 'md' into the packet-out via "set_field"
> +     * actions, then add actions from 'userdata'.
> +     */
> +    uint64_t ofpacts_stub[4096 / 8];
> +    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
> +    enum ofp_version version = rconn_get_version(swconn);
> +
> +    reload_metadata(&ofpacts, md);
> +    enum ofperr error = ofpacts_pull_openflow_actions(userdata,
> userdata->size,
> +                                                      version, NULL, NULL,
> +                                                      &ofpacts);
> +    if (error) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "failed to parse actions from userdata (%s)",
> +                     ofperr_to_string(error));
> +        ofpbuf_uninit(&ofpacts);
> +        return;
> +    }
> +
> +    struct ofputil_packet_out po = {
> +        .packet = dp_packet_data(packet),
> +        .packet_len = dp_packet_size(packet),
> +        .buffer_id = UINT32_MAX,
> +        .ofpacts = ofpacts.data,
> +        .ofpacts_len = ofpacts.size,
> +    };
> +    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
> +    enum ofputil_protocol proto =
> ofputil_protocol_from_ofp_version(version);
> +    queue_msg(ofputil_encode_packet_out(&po, proto));
> +    ofpbuf_uninit(&ofpacts);
> +}
> +
> +static void
>  pinctrl_handle_arp(const struct flow *ip_flow, const struct match *md,
>                     struct ofpbuf *userdata)
>  {
> @@ -166,40 +206,8 @@ pinctrl_handle_arp(const struct flow *ip_flow, const
> struct match *md,
>                        ip_flow->vlans[0].tci);
>      }
>
> -    /* Compose actions.
> -     *
> -     * First, copy metadata from 'md' into the packet-out via "set_field"
> -     * actions, then add actions from 'userdata'.
> -     */
> -    uint64_t ofpacts_stub[4096 / 8];
> -    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
> -    enum ofp_version version = rconn_get_version(swconn);
> -
> -    reload_metadata(&ofpacts, md);
> -    enum ofperr error = ofpacts_pull_openflow_actions(userdata,
> userdata->size,
> -                                                      version, NULL, NULL,
> -                                                      &ofpacts);
> -    if (error) {
> -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> -        VLOG_WARN_RL(&rl, "failed to parse arp actions (%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,
> -        .ofpacts = ofpacts.data,
> -        .ofpacts_len = ofpacts.size,
> -    };
> -    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
> -    enum ofputil_protocol proto =
> ofputil_protocol_from_ofp_version(version);
> -    queue_msg(ofputil_encode_packet_out(&po, proto));
> -
> -exit:
> +    set_actions_and_enqueue_msg(&packet, md, userdata);
>      dp_packet_uninit(&packet);
> -    ofpbuf_uninit(&ofpacts);
>  }
>
>  static void
> @@ -994,6 +1002,10 @@ process_packet_in(const struct ofp_header *msg,
> struct controller_ctx *ctx)
>                                        &continuation);
>          break;
>
> +    case ACTION_OPCODE_ND_NS:
> +        pinctrl_handle_nd_ns(&headers, &pin.flow_metadata, &userdata);
> +        break;
> +
>      default:
>          VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
>                       ntohl(ah->opcode));
> @@ -1812,9 +1824,6 @@ pinctrl_handle_nd_na(const struct flow *ip_flow,
> const struct match *md,
>          return;
>      }
>
> -    enum ofp_version version = rconn_get_version(swconn);
> -    enum ofputil_protocol proto =
> ofputil_protocol_from_ofp_version(version);
> -
>      uint64_t packet_stub[128 / 8];
>      struct dp_packet packet;
>      dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
> @@ -1827,35 +1836,32 @@ pinctrl_handle_nd_na(const struct flow *ip_flow,
> const struct match *md,
>                    &ip_flow->nd_target, &ip_flow->ipv6_src,
>                    htonl(ND_RSO_SOLICITED | ND_RSO_OVERRIDE));
>
> -    /* Reload previous packet metadata. */
> -    uint64_t ofpacts_stub[4096 / 8];
> -    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
> -    reload_metadata(&ofpacts, md);
> +    /* Reload previous packet metadata and set actions from userdata. */
> +    set_actions_and_enqueue_msg(&packet, md, userdata);
> +    dp_packet_uninit(&packet);
> +}
>
> -    enum ofperr error = ofpacts_pull_openflow_actions(userdata,
> userdata->size,
> -                                                      version, NULL, NULL,
> -                                                      &ofpacts);
> -    if (error) {
> +static void
> +pinctrl_handle_nd_ns(const struct flow *ip_flow, const struct match *md,
> +                     struct ofpbuf *userdata)
> +{
> +    /* This action only works for IPv6 packets. */
> +    if (get_dl_type(ip_flow) != htons(ETH_TYPE_IPV6)) {
>          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> -        VLOG_WARN_RL(&rl, "failed to parse actions for 'na' (%s)",
> -                     ofperr_to_string(error));
> -        goto exit;
> +        VLOG_WARN_RL(&rl, "NS action on non-IPv6 packet");
> +        return;
>      }
>
> -    struct ofputil_packet_out po = {
> -        .packet = dp_packet_data(&packet),
> -        .packet_len = dp_packet_size(&packet),
> -        .buffer_id = UINT32_MAX,
> -        .ofpacts = ofpacts.data,
> -        .ofpacts_len = ofpacts.size,
> -    };
> -    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
> +    uint64_t packet_stub[128 / 8];
> +    struct dp_packet packet;
> +    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
>
> -    queue_msg(ofputil_encode_packet_out(&po, proto));
> +    compose_nd_ns(&packet, ip_flow->dl_src, &ip_flow->ipv6_src,
> +                  &ip_flow->ipv6_dst);
>
> -exit:
> +    /* Reload previous packet metadata and set actions from userdata. */
> +    set_actions_and_enqueue_msg(&packet, md, userdata);
>      dp_packet_uninit(&packet);
> -    ofpbuf_uninit(&ofpacts);
>  }
>
>  static void
> diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
> index 8d5863c0a..d0a4d7753 100644
> --- a/ovn/lib/actions.c
> +++ b/ovn/lib/actions.c
> @@ -1134,6 +1134,12 @@ parse_ND_NA(struct action_context *ctx)
>  }
>
>  static void
> +parse_ND_NS(struct action_context *ctx)
> +{
> +    parse_nested_action(ctx, OVNACT_ND_NS, "ip6");
> +}
> +
> +static void
>  parse_CLONE(struct action_context *ctx)
>  {
>      parse_nested_action(ctx, OVNACT_CLONE, NULL);
> @@ -1161,6 +1167,12 @@ format_ND_NA(const struct ovnact_nest *nest, struct
> ds *s)
>  }
>
>  static void
> +format_ND_NS(const struct ovnact_nest *nest, struct ds *s)
> +{
> +    format_nested_action(nest, "nd_ns", s);
> +}
> +
> +static void
>  format_CLONE(const struct ovnact_nest *nest, struct ds *s)
>  {
>      format_nested_action(nest, "clone", s);
> @@ -1207,6 +1219,14 @@ encode_ND_NA(const struct ovnact_nest *on,
>  }
>
>  static void
> +encode_ND_NS(const struct ovnact_nest *on,
> +             const struct ovnact_encode_params *ep,
> +             struct ofpbuf *ofpacts)
> +{
> +    encode_nested_neighbor_actions(on, ep, ACTION_OPCODE_ND_NS, ofpacts);
> +}
> +
> +static void
>  encode_CLONE(const struct ovnact_nest *on,
>               const struct ovnact_encode_params *ep,
>               struct ofpbuf *ofpacts)
> @@ -2146,6 +2166,8 @@ parse_action(struct action_context *ctx)
>          parse_ARP(ctx);
>      } else if (lexer_match_id(ctx->lexer, "nd_na")) {
>          parse_ND_NA(ctx);
> +    } else if (lexer_match_id(ctx->lexer, "nd_ns")) {
> +        parse_ND_NS(ctx);
>      } else if (lexer_match_id(ctx->lexer, "get_arp")) {
>          parse_get_mac_bind(ctx, 32, ovnact_put_GET_ARP(ctx->ovnacts));
>      } else if (lexer_match_id(ctx->lexer, "put_arp")) {
> diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
> index a994abf78..17123c690 100644
> --- a/ovn/northd/ovn-northd.8.xml
> +++ b/ovn/northd/ovn-northd.8.xml
> @@ -1915,15 +1915,15 @@ next;
>
>      <p>
>        In the common case where the Ethernet destination has been
> resolved, this
> -      table outputs the packet.  Otherwise, it composes and sends an ARP
> -      request.  It holds the following flows:
> +      table outputs the packet.  Otherwise, it composes and sends an ARP
> or
> +      IPv6 Neighbor Solicitation request.  It holds the following flows:
>      </p>
>
>      <ul>
>        <li>
>          <p>
> -          Unknown MAC address.  A priority-100 flow with match
> <code>eth.dst ==
> -          00:00:00:00:00:00</code> has the following actions:
> +          Unknown MAC address.  A priority-100 flow for IPv4 packets with
> match
> +          <code>eth.dst == 00:00:00:00:00:00</code> has the following
> actions:
>          </p>
>
>          <pre>
> @@ -1937,13 +1937,25 @@ arp {
>          </pre>
>
>          <p>
> +          Unknown MAC address.  A priority-100 flow for IPv6 packets with
> match
> +          <code>eth.dst == 00:00:00:00:00:00</code> has the following
> actions:
> +        </p>
> +
> +        <pre>
> +nd_ns {
> +    nd.target = xxreg0;
> +    output;
> +};
> +        </pre>
> +
> +        <p>
>            (Ingress table <code>IP Routing</code> initialized
> <code>reg1</code>
>            with the IP address owned by <code>outport</code> and
> -          <code>reg0</code> with the next-hop IP address)
> +          <code>(xx)reg0</code> with the next-hop IP address)
>          </p>
>
>          <p>
> -          The IP packet that triggers the ARP request is dropped.
> +          The IP packet that triggers the ARP/IPv6 NS request is dropped.
>          </p>
>        </li>
>
> diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
> index 3da20d25b..b4ea34bc6 100644
> --- a/ovn/northd/ovn-northd.c
> +++ b/ovn/northd/ovn-northd.c
> @@ -5703,7 +5703,7 @@ build_lrouter_flows(struct hmap *datapaths, struct
> hmap *ports,
>       *
>       * In the common case where the Ethernet destination has been
> resolved,
>       * this table outputs the packet (priority 0).  Otherwise, it composes
> -     * and sends an ARP request (priority 100). */
> +     * and sends an ARP/IPv6 NA request (priority 100). */
>      HMAP_FOR_EACH (od, key_node, datapaths) {
>          if (!od->nbr) {
>              continue;
> @@ -5718,6 +5718,12 @@ build_lrouter_flows(struct hmap *datapaths, struct
> hmap *ports,
>                        "arp.op = 1; " /* ARP request */
>                        "output; "
>                        "};");
> +        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 100,
> +                      "eth.dst == 00:00:00:00:00:00",
> +                      "nd_ns { "
> +                      "nd.target = xxreg0; "
> +                      "output; "
> +                      "};");
>          ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 0, "1",
> "output;");
>      }
>
> diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
> index 2e4f28b96..ca8cbecdd 100644
> --- a/ovn/ovn-sb.xml
> +++ b/ovn/ovn-sb.xml
> @@ -1258,6 +1258,43 @@
>            <p><b>Example:</b> <code>put_arp(inport, arp.spa,
> arp.sha);</code></p>
>          </dd>
>
> +        <dt><code>nd_ns { <var>action</var>; </code>...<code>
> };</code></dt>
> +        <dd>
> +          <p>
> +            Temporarily replaces the IPv6 packet being processed by an
> IPv6
> +            Neighbor Solicitation packet and executes each nested
> +            <var>action</var> on the IPv6 NS packet.  Actions following
> the
> +            <var>nd_ns</var> action, if any, apply to the original,
> unmodified
> +            packet.
> +          </p>
> +
> +          <p>
> +            The IPv6 NS packet that this action operates on is initialized
> +            based on the IPv6 packet being processed, as follows.  These
> are
> +            default values that the nested actions will probably want to
> +            change:
> +          </p>
> +
> +          <ul>
> +            <li><code>eth.src</code> unchanged</li>
> +            <li><code>eth.dst</code> set to IPv6 multicast MAC
> address</li>
> +            <li><code>eth.type = 0x86dd</code></li>
> +            <li><code>ip6.src</code> copied from <code>ip6.src</code></li>
> +            <li>
> +              <code>ip6.dst</code> set to IPv6 Solicited-Node multicast
> address
> +            </li>
> +            <li><code>icmp6.type = 135</code> (Neighbor Solicitation)</li>
> +            <li><code>nd.target</code> copied from
> <code>ip6.dst</code></li>
> +          </ul>
> +
> +          <p>
> +            The IPv6 NS packet has the same VLAN header, if any, as the IP
> +            packet it replaces.
> +          </p>
> +
> +          <p><b>Prerequisite:</b> <code>ip6</code></p>
> +        </dd>
> +
>          <dt>
>            <code>nd_na { <var>action</var>; </code>...<code> };</code>
>          </dt>
> diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
> index 211148b8b..e457284fc 100644
> --- a/ovn/utilities/ovn-trace.c
> +++ b/ovn/utilities/ovn-trace.c
> @@ -1510,6 +1510,31 @@ execute_nd_na(const struct ovnact_nest *on, const
> struct ovntrace_datapath *dp,
>  }
>
>  static void
> +execute_nd_ns(const struct ovnact_nest *on, const struct
> ovntrace_datapath *dp,
> +              const struct flow *uflow, uint8_t table_id,
> +              enum ovnact_pipeline pipeline, struct ovs_list *super)
> +{
> +    struct flow na_flow = *uflow;
> +
> +    /* Update fields for NA. */
> +    na_flow.dl_src = uflow->dl_src;
> +    na_flow.ipv6_src = uflow->ipv6_src;
> +    na_flow.ipv6_dst = uflow->ipv6_dst;
> +    struct in6_addr sn_addr;
> +    in6_addr_solicited_node(&sn_addr, &uflow->ipv6_dst);
> +    ipv6_multicast_to_ethernet(&na_flow.dl_dst, &sn_addr);
> +    na_flow.tp_src = htons(135);
> +    na_flow.arp_sha = eth_addr_zero;
> +    na_flow.arp_tha = uflow->dl_dst;
> +
> +    struct ovntrace_node *node = ovntrace_node_append(
> +        super, OVNTRACE_NODE_TRANSFORMATION, "nd_ns");
> +
> +    trace_actions(on->nested, on->nested_len, dp, &na_flow,
> +                  table_id, pipeline, &node->subs);
> +}
> +
> +static void
>  execute_get_mac_bind(const struct ovnact_get_mac_bind *bind,
>                       const struct ovntrace_datapath *dp,
>                       struct flow *uflow, struct ovs_list *super)
> @@ -1811,6 +1836,11 @@ trace_actions(const struct ovnact *ovnacts, size_t
> ovnacts_len,
>                            super);
>              break;
>
> +        case OVNACT_ND_NS:
> +            execute_nd_ns(ovnact_get_ND_NS(a), dp, uflow, table_id,
> pipeline,
> +                          super);
> +            break;
> +
>          case OVNACT_GET_ARP:
>              execute_get_mac_bind(ovnact_get_GET_ARP(a), dp, uflow, super);
>              break;
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 3aa4e5e22..13cdc1679 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -993,6 +993,16 @@ reg1[0] = put_dhcp_opts(offerip="xyzzy");
>  reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4);
>      DHCPv4 option domain requires string value.
>
> +# nd_ns
> +nd_ns { nd.target = xxreg0; output; };
> +    encodes as
> controller(userdata=00.00.00.09.00.00.00.00.ff.ff.00.18.00.00.23.20.00.06.00.80.00.00.00.00.00.01.de.10.00.01.2e.10.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
> +    has prereqs ip6
> +
> +nd_ns { };
> +    formats as nd_ns { drop; };
> +    encodes as controller(userdata=00.00.00.09.00.00.00.00)
> +    has prereqs ip6
> +
>  # nd_na
>  nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport
> = inport; inport = ""; /* Allow sending out inport. */ output; };
>      formats as nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll =
> 12:34:56:78:9a:bc; outport = inport; inport = ""; output; };
> @@ -8795,6 +8805,112 @@ OVN_CLEANUP([gw1],[gw2],[hv1])
>
>  AT_CLEANUP
>
> +AT_SETUP([ovn -- IPv6 Neighbor Solicitation for unknown MAC])
> +AT_KEYWORDS([ovn-nd_ns for unknown mac])
> +AT_SKIP_IF([test $HAVE_PYTHON = no])
> +ovn_start
> +
> +ovn-nbctl ls-add sw0_ip6
> +ovn-nbctl lsp-add sw0_ip6 sw0_ip6-port1
> +ovn-nbctl lsp-set-addresses sw0_ip6-port1 \
> +"50:64:00:00:00:02 aef0::5264:00ff:fe00:0002"
> +
> +ovn-nbctl lsp-set-port-security sw0_ip6-port1 \
> +"50:64:00:00:00:02 aef0::5264:00ff:fe00:0002"
> +
> +ovn-nbctl lr-add lr0_ip6
> +ovn-nbctl lrp-add lr0_ip6 lrp0_ip6 00:00:00:00:af:01 aef0::/64
> +ovn-nbctl lsp-add sw0_ip6 lrp0_ip6-attachment
> +ovn-nbctl lsp-set-type lrp0_ip6-attachment router
> +ovn-nbctl lsp-set-addresses lrp0_ip6-attachment 00:00:00:00:af:01
> +ovn-nbctl lsp-set-options lrp0_ip6-attachment router-port=lrp0_ip6
> +ovn-nbctl set logical_router_port lrp0_ip6
> ipv6_ra_configs:address_mode=slaac
> +
> +ovn-nbctl ls-add public
> +ovn-nbctl lsp-add public ln-public
> +ovn-nbctl lsp-set-addresses ln-public unknown
> +ovn-nbctl lsp-set-type ln-public localnet
> +ovn-nbctl lsp-set-options ln-public network_name=phys
> +
> +ovn-nbctl lrp-add lr0_ip6 ip6_public 00:00:02:01:02:04 \
> +2001:db8:1:0:200:02ff:fe01:0204/64 \
> +-- set Logical_Router_port ip6_public options:redirect-chassis="hv1"
> +
> +
> +ovn-nbctl lsp-add public rp-ip6_public -- set Logical_Switch_Port \
> +rp-ip6_public  type=router options:router-port=ip6_public \
> +-- lsp-set-addresses rp-ip6_public router
> +
> +net_add n1
> +sim_add hv1
> +as hv1
> +ovs-vsctl add-br br-phys
> +ovn_attach n1 br-phys 192.168.0.2
> +
> +ovs-vsctl -- add-port br-int hv1-vif1 -- \
> +    set interface hv1-vif1 external-ids:iface-id=sw0_ip6-port1 \
> +    options:tx_pcap=hv1/vif1-tx.pcap \
> +    options:rxq_pcap=hv1/vif1-rx.pcap \
> +    ofport-request=1
> +ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
> +
> +# Allow some time for ovn-northd and ovn-controller to catch up.
> +# XXX This should be more systematic.
> +sleep 1
> +
> +trim_zeros() {
> +    sed 's/\(00\)\{1,\}$//'
> +}
> +
> +# Test the IPv6 Neighbor Solicitation (NS) - nd_ns action for unknown MAC
> +# addresses. ovn-controller should generate an IPv6 NS request for IPv6
> +# packets whose MAC is unknown (in the ARP_REQUEST router pipeline stage.
> +# test_ipv6 INPORT SRC_MAC DST_MAC SRC_IP DST_IP OUTPORT...
> +# This function sends ipv6 packet
> +test_ipv6() {
> +    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4
> +    dst_ip=20010db800010000020002fffe010205
> +
> +    local
> packet=${dst_mac}${src_mac}86dd6000000000083aff${src_ip}${dst_ip}
> +    packet=${packet}8000000000000000
> +    shift; shift; shift; shift
> +
> +    dst_mac=3333ff010205
> +    src_mac=000002010204
> +    mcast_node_ip=ff0200000000000000000001ff010205
> +    expected_packet=${dst_mac}${src_mac}86dd6000000000203aff${src_ip}
> +
> expected_packet=${expected_packet}${mcast_node_ip}8700XXXX00000000${dst_ip}
> +    expected_packet=${expected_packet}0101${src_mac}
> +
> +    as hv1 ovs-appctl netdev-dummy/receive hv1-vif${inport} $packet
> +    echo $expected_packet >> ipv6_ns.expected
> +}
> +
> +src_mac=506400000002
> +dst_mac=00000000af01
> +src_ip=aef0000000000000526400fffe000002
> +# Send an IPv6 packet. Generated IPv6 Neighbor solicitation packet
> +# should be received by the ports attached to br-phys.
> +test_ipv6 1 $src_mac $dst_mac $src_ip 2
> +
> +$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/br-phys_n1-tx.pcap | \
> +trim_zeros > 1.packets
> +$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/br-phys-tx.pcap | \
> +trim_zeros > 2.packets
> +
> +cat ipv6_ns.expected | cut -c -112 > expout
> +AT_CHECK([cat 1.packets | cut -c -112], [0], [expout])
> +AT_CHECK([cat 2.packets | cut -c -112], [0], [expout])
> +
> +# Skipping the ICMPv6 checksum
> +cat ipv6_ns.expected | cut -c 117- > expout
> +AT_CHECK([cat 1.packets | cut -c 117-], [0], [expout])
> +AT_CHECK([cat 2.packets | cut -c 117-], [0], [expout])
> +
> +OVN_CLEANUP([hv1])
> +
> +AT_CLEANUP
> +
>  AT_SETUP([ovn -- options:requested-chassis for logical port])
>  ovn_start
>
> --
> 2.13.5
>
> _______________________________________________
> dev mailing list
> dev@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>

Patch

diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 15cee478d..8c7208ffc 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -73,7 +73,8 @@  struct simap;
     OVNACT(SET_QUEUE,         ovnact_set_queue)       \
     OVNACT(DNS_LOOKUP,        ovnact_dns_lookup)      \
     OVNACT(LOG,               ovnact_log)             \
-    OVNACT(PUT_ND_RA_OPTS,    ovnact_put_opts)
+    OVNACT(PUT_ND_RA_OPTS,    ovnact_put_opts)        \
+    OVNACT(ND_NS,             ovnact_nest)
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -427,6 +428,12 @@  enum action_opcode {
      *   - Any number of ICMPv6 options.
      */
     ACTION_OPCODE_PUT_ND_RA_OPTS,
+
+    /* "nd_ns { ...actions... }".
+     *
+     * The actions, in OpenFlow 1.3 format, follow the action_header.
+     */
+    ACTION_OPCODE_ND_NS,
 };
 
 /* Header. */
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index 3a1348937..5aedf7d0d 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -85,6 +85,9 @@  static void pinctrl_handle_put_nd_ra_opts(
     const struct flow *ip_flow, struct dp_packet *pkt_in,
     struct ofputil_packet_in *pin, struct ofpbuf *userdata,
     struct ofpbuf *continuation);
+static void pinctrl_handle_nd_ns(const struct flow *ip_flow,
+                                 const struct match *md,
+                                 struct ofpbuf *userdata);
 
 COVERAGE_DEFINE(pinctrl_drop_put_mac_binding);
 
@@ -132,6 +135,43 @@  set_switch_config(struct rconn *swconn,
 }
 
 static void
+set_actions_and_enqueue_msg(const struct dp_packet *packet,
+                           const struct match *md,
+                           struct ofpbuf *userdata)
+{
+    /* Copy metadata from 'md' into the packet-out via "set_field"
+     * actions, then add actions from 'userdata'.
+     */
+    uint64_t ofpacts_stub[4096 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+    enum ofp_version version = rconn_get_version(swconn);
+
+    reload_metadata(&ofpacts, md);
+    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
+                                                      version, NULL, NULL,
+                                                      &ofpacts);
+    if (error) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "failed to parse actions from userdata (%s)",
+                     ofperr_to_string(error));
+        ofpbuf_uninit(&ofpacts);
+        return;
+    }
+
+    struct ofputil_packet_out po = {
+        .packet = dp_packet_data(packet),
+        .packet_len = dp_packet_size(packet),
+        .buffer_id = UINT32_MAX,
+        .ofpacts = ofpacts.data,
+        .ofpacts_len = ofpacts.size,
+    };
+    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    queue_msg(ofputil_encode_packet_out(&po, proto));
+    ofpbuf_uninit(&ofpacts);
+}
+
+static void
 pinctrl_handle_arp(const struct flow *ip_flow, const struct match *md,
                    struct ofpbuf *userdata)
 {
@@ -166,40 +206,8 @@  pinctrl_handle_arp(const struct flow *ip_flow, const struct match *md,
                       ip_flow->vlans[0].tci);
     }
 
-    /* Compose actions.
-     *
-     * First, copy metadata from 'md' into the packet-out via "set_field"
-     * actions, then add actions from 'userdata'.
-     */
-    uint64_t ofpacts_stub[4096 / 8];
-    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
-    enum ofp_version version = rconn_get_version(swconn);
-
-    reload_metadata(&ofpacts, md);
-    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
-                                                      version, NULL, NULL,
-                                                      &ofpacts);
-    if (error) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-        VLOG_WARN_RL(&rl, "failed to parse arp actions (%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,
-        .ofpacts = ofpacts.data,
-        .ofpacts_len = ofpacts.size,
-    };
-    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
-    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
-    queue_msg(ofputil_encode_packet_out(&po, proto));
-
-exit:
+    set_actions_and_enqueue_msg(&packet, md, userdata);
     dp_packet_uninit(&packet);
-    ofpbuf_uninit(&ofpacts);
 }
 
 static void
@@ -994,6 +1002,10 @@  process_packet_in(const struct ofp_header *msg, struct controller_ctx *ctx)
                                       &continuation);
         break;
 
+    case ACTION_OPCODE_ND_NS:
+        pinctrl_handle_nd_ns(&headers, &pin.flow_metadata, &userdata);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -1812,9 +1824,6 @@  pinctrl_handle_nd_na(const struct flow *ip_flow, const struct match *md,
         return;
     }
 
-    enum ofp_version version = rconn_get_version(swconn);
-    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
-
     uint64_t packet_stub[128 / 8];
     struct dp_packet packet;
     dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
@@ -1827,35 +1836,32 @@  pinctrl_handle_nd_na(const struct flow *ip_flow, const struct match *md,
                   &ip_flow->nd_target, &ip_flow->ipv6_src,
                   htonl(ND_RSO_SOLICITED | ND_RSO_OVERRIDE));
 
-    /* Reload previous packet metadata. */
-    uint64_t ofpacts_stub[4096 / 8];
-    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
-    reload_metadata(&ofpacts, md);
+    /* Reload previous packet metadata and set actions from userdata. */
+    set_actions_and_enqueue_msg(&packet, md, userdata);
+    dp_packet_uninit(&packet);
+}
 
-    enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
-                                                      version, NULL, NULL,
-                                                      &ofpacts);
-    if (error) {
+static void
+pinctrl_handle_nd_ns(const struct flow *ip_flow, const struct match *md,
+                     struct ofpbuf *userdata)
+{
+    /* This action only works for IPv6 packets. */
+    if (get_dl_type(ip_flow) != htons(ETH_TYPE_IPV6)) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-        VLOG_WARN_RL(&rl, "failed to parse actions for 'na' (%s)",
-                     ofperr_to_string(error));
-        goto exit;
+        VLOG_WARN_RL(&rl, "NS action on non-IPv6 packet");
+        return;
     }
 
-    struct ofputil_packet_out po = {
-        .packet = dp_packet_data(&packet),
-        .packet_len = dp_packet_size(&packet),
-        .buffer_id = UINT32_MAX,
-        .ofpacts = ofpacts.data,
-        .ofpacts_len = ofpacts.size,
-    };
-    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
+    uint64_t packet_stub[128 / 8];
+    struct dp_packet packet;
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
 
-    queue_msg(ofputil_encode_packet_out(&po, proto));
+    compose_nd_ns(&packet, ip_flow->dl_src, &ip_flow->ipv6_src,
+                  &ip_flow->ipv6_dst);
 
-exit:
+    /* Reload previous packet metadata and set actions from userdata. */
+    set_actions_and_enqueue_msg(&packet, md, userdata);
     dp_packet_uninit(&packet);
-    ofpbuf_uninit(&ofpacts);
 }
 
 static void
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 8d5863c0a..d0a4d7753 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -1134,6 +1134,12 @@  parse_ND_NA(struct action_context *ctx)
 }
 
 static void
+parse_ND_NS(struct action_context *ctx)
+{
+    parse_nested_action(ctx, OVNACT_ND_NS, "ip6");
+}
+
+static void
 parse_CLONE(struct action_context *ctx)
 {
     parse_nested_action(ctx, OVNACT_CLONE, NULL);
@@ -1161,6 +1167,12 @@  format_ND_NA(const struct ovnact_nest *nest, struct ds *s)
 }
 
 static void
+format_ND_NS(const struct ovnact_nest *nest, struct ds *s)
+{
+    format_nested_action(nest, "nd_ns", s);
+}
+
+static void
 format_CLONE(const struct ovnact_nest *nest, struct ds *s)
 {
     format_nested_action(nest, "clone", s);
@@ -1207,6 +1219,14 @@  encode_ND_NA(const struct ovnact_nest *on,
 }
 
 static void
+encode_ND_NS(const struct ovnact_nest *on,
+             const struct ovnact_encode_params *ep,
+             struct ofpbuf *ofpacts)
+{
+    encode_nested_neighbor_actions(on, ep, ACTION_OPCODE_ND_NS, ofpacts);
+}
+
+static void
 encode_CLONE(const struct ovnact_nest *on,
              const struct ovnact_encode_params *ep,
              struct ofpbuf *ofpacts)
@@ -2146,6 +2166,8 @@  parse_action(struct action_context *ctx)
         parse_ARP(ctx);
     } else if (lexer_match_id(ctx->lexer, "nd_na")) {
         parse_ND_NA(ctx);
+    } else if (lexer_match_id(ctx->lexer, "nd_ns")) {
+        parse_ND_NS(ctx);
     } else if (lexer_match_id(ctx->lexer, "get_arp")) {
         parse_get_mac_bind(ctx, 32, ovnact_put_GET_ARP(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "put_arp")) {
diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
index a994abf78..17123c690 100644
--- a/ovn/northd/ovn-northd.8.xml
+++ b/ovn/northd/ovn-northd.8.xml
@@ -1915,15 +1915,15 @@  next;
 
     <p>
       In the common case where the Ethernet destination has been resolved, this
-      table outputs the packet.  Otherwise, it composes and sends an ARP
-      request.  It holds the following flows:
+      table outputs the packet.  Otherwise, it composes and sends an ARP or
+      IPv6 Neighbor Solicitation request.  It holds the following flows:
     </p>
 
     <ul>
       <li>
         <p>
-          Unknown MAC address.  A priority-100 flow with match <code>eth.dst ==
-          00:00:00:00:00:00</code> has the following actions:
+          Unknown MAC address.  A priority-100 flow for IPv4 packets with match
+          <code>eth.dst == 00:00:00:00:00:00</code> has the following actions:
         </p>
 
         <pre>
@@ -1937,13 +1937,25 @@  arp {
         </pre>
 
         <p>
+          Unknown MAC address.  A priority-100 flow for IPv6 packets with match
+          <code>eth.dst == 00:00:00:00:00:00</code> has the following actions:
+        </p>
+
+        <pre>
+nd_ns {
+    nd.target = xxreg0;
+    output;
+};
+        </pre>
+
+        <p>
           (Ingress table <code>IP Routing</code> initialized <code>reg1</code>
           with the IP address owned by <code>outport</code> and
-          <code>reg0</code> with the next-hop IP address)
+          <code>(xx)reg0</code> with the next-hop IP address)
         </p>
 
         <p>
-          The IP packet that triggers the ARP request is dropped.
+          The IP packet that triggers the ARP/IPv6 NS request is dropped.
         </p>
       </li>
 
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 3da20d25b..b4ea34bc6 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -5703,7 +5703,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
      *
      * In the common case where the Ethernet destination has been resolved,
      * this table outputs the packet (priority 0).  Otherwise, it composes
-     * and sends an ARP request (priority 100). */
+     * and sends an ARP/IPv6 NA request (priority 100). */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbr) {
             continue;
@@ -5718,6 +5718,12 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                       "arp.op = 1; " /* ARP request */
                       "output; "
                       "};");
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 100,
+                      "eth.dst == 00:00:00:00:00:00",
+                      "nd_ns { "
+                      "nd.target = xxreg0; "
+                      "output; "
+                      "};");
         ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 0, "1", "output;");
     }
 
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 2e4f28b96..ca8cbecdd 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1258,6 +1258,43 @@ 
           <p><b>Example:</b> <code>put_arp(inport, arp.spa, arp.sha);</code></p>
         </dd>
 
+        <dt><code>nd_ns { <var>action</var>; </code>...<code> };</code></dt>
+        <dd>
+          <p>
+            Temporarily replaces the IPv6 packet being processed by an IPv6
+            Neighbor Solicitation packet and executes each nested
+            <var>action</var> on the IPv6 NS packet.  Actions following the
+            <var>nd_ns</var> action, if any, apply to the original, unmodified
+            packet.
+          </p>
+
+          <p>
+            The IPv6 NS packet that this action operates on is initialized
+            based on the IPv6 packet being processed, as follows.  These are
+            default values that the nested actions will probably want to
+            change:
+          </p>
+
+          <ul>
+            <li><code>eth.src</code> unchanged</li>
+            <li><code>eth.dst</code> set to IPv6 multicast MAC address</li>
+            <li><code>eth.type = 0x86dd</code></li>
+            <li><code>ip6.src</code> copied from <code>ip6.src</code></li>
+            <li>
+              <code>ip6.dst</code> set to IPv6 Solicited-Node multicast address
+            </li>
+            <li><code>icmp6.type = 135</code> (Neighbor Solicitation)</li>
+            <li><code>nd.target</code> copied from <code>ip6.dst</code></li>
+          </ul>
+
+          <p>
+            The IPv6 NS packet has the same VLAN header, if any, as the IP
+            packet it replaces.
+          </p>
+
+          <p><b>Prerequisite:</b> <code>ip6</code></p>
+        </dd>
+
         <dt>
           <code>nd_na { <var>action</var>; </code>...<code> };</code>
         </dt>
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index 211148b8b..e457284fc 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -1510,6 +1510,31 @@  execute_nd_na(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
 }
 
 static void
+execute_nd_ns(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
+              const struct flow *uflow, uint8_t table_id,
+              enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    struct flow na_flow = *uflow;
+
+    /* Update fields for NA. */
+    na_flow.dl_src = uflow->dl_src;
+    na_flow.ipv6_src = uflow->ipv6_src;
+    na_flow.ipv6_dst = uflow->ipv6_dst;
+    struct in6_addr sn_addr;
+    in6_addr_solicited_node(&sn_addr, &uflow->ipv6_dst);
+    ipv6_multicast_to_ethernet(&na_flow.dl_dst, &sn_addr);
+    na_flow.tp_src = htons(135);
+    na_flow.arp_sha = eth_addr_zero;
+    na_flow.arp_tha = uflow->dl_dst;
+
+    struct ovntrace_node *node = ovntrace_node_append(
+        super, OVNTRACE_NODE_TRANSFORMATION, "nd_ns");
+
+    trace_actions(on->nested, on->nested_len, dp, &na_flow,
+                  table_id, pipeline, &node->subs);
+}
+
+static void
 execute_get_mac_bind(const struct ovnact_get_mac_bind *bind,
                      const struct ovntrace_datapath *dp,
                      struct flow *uflow, struct ovs_list *super)
@@ -1811,6 +1836,11 @@  trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
                           super);
             break;
 
+        case OVNACT_ND_NS:
+            execute_nd_ns(ovnact_get_ND_NS(a), dp, uflow, table_id, pipeline,
+                          super);
+            break;
+
         case OVNACT_GET_ARP:
             execute_get_mac_bind(ovnact_get_GET_ARP(a), dp, uflow, super);
             break;
diff --git a/tests/ovn.at b/tests/ovn.at
index 3aa4e5e22..13cdc1679 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -993,6 +993,16 @@  reg1[0] = put_dhcp_opts(offerip="xyzzy");
 reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4);
     DHCPv4 option domain requires string value.
 
+# nd_ns
+nd_ns { nd.target = xxreg0; output; };
+    encodes as controller(userdata=00.00.00.09.00.00.00.00.ff.ff.00.18.00.00.23.20.00.06.00.80.00.00.00.00.00.01.de.10.00.01.2e.10.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
+    has prereqs ip6
+
+nd_ns { };
+    formats as nd_ns { drop; };
+    encodes as controller(userdata=00.00.00.09.00.00.00.00)
+    has prereqs ip6
+
 # nd_na
 nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; /* Allow sending out inport. */ output; };
     formats as nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; output; };
@@ -8795,6 +8805,112 @@  OVN_CLEANUP([gw1],[gw2],[hv1])
 
 AT_CLEANUP
 
+AT_SETUP([ovn -- IPv6 Neighbor Solicitation for unknown MAC])
+AT_KEYWORDS([ovn-nd_ns for unknown mac])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+ovn-nbctl ls-add sw0_ip6
+ovn-nbctl lsp-add sw0_ip6 sw0_ip6-port1
+ovn-nbctl lsp-set-addresses sw0_ip6-port1 \
+"50:64:00:00:00:02 aef0::5264:00ff:fe00:0002"
+
+ovn-nbctl lsp-set-port-security sw0_ip6-port1 \
+"50:64:00:00:00:02 aef0::5264:00ff:fe00:0002"
+
+ovn-nbctl lr-add lr0_ip6
+ovn-nbctl lrp-add lr0_ip6 lrp0_ip6 00:00:00:00:af:01 aef0::/64
+ovn-nbctl lsp-add sw0_ip6 lrp0_ip6-attachment
+ovn-nbctl lsp-set-type lrp0_ip6-attachment router
+ovn-nbctl lsp-set-addresses lrp0_ip6-attachment 00:00:00:00:af:01
+ovn-nbctl lsp-set-options lrp0_ip6-attachment router-port=lrp0_ip6
+ovn-nbctl set logical_router_port lrp0_ip6 ipv6_ra_configs:address_mode=slaac
+
+ovn-nbctl ls-add public
+ovn-nbctl lsp-add public ln-public
+ovn-nbctl lsp-set-addresses ln-public unknown
+ovn-nbctl lsp-set-type ln-public localnet
+ovn-nbctl lsp-set-options ln-public network_name=phys
+
+ovn-nbctl lrp-add lr0_ip6 ip6_public 00:00:02:01:02:04 \
+2001:db8:1:0:200:02ff:fe01:0204/64 \
+-- set Logical_Router_port ip6_public options:redirect-chassis="hv1"
+
+
+ovn-nbctl lsp-add public rp-ip6_public -- set Logical_Switch_Port \
+rp-ip6_public  type=router options:router-port=ip6_public \
+-- lsp-set-addresses rp-ip6_public router
+
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+
+ovs-vsctl -- add-port br-int hv1-vif1 -- \
+    set interface hv1-vif1 external-ids:iface-id=sw0_ip6-port1 \
+    options:tx_pcap=hv1/vif1-tx.pcap \
+    options:rxq_pcap=hv1/vif1-rx.pcap \
+    ofport-request=1
+ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+
+# Allow some time for ovn-northd and ovn-controller to catch up.
+# XXX This should be more systematic.
+sleep 1
+
+trim_zeros() {
+    sed 's/\(00\)\{1,\}$//'
+}
+
+# Test the IPv6 Neighbor Solicitation (NS) - nd_ns action for unknown MAC
+# addresses. ovn-controller should generate an IPv6 NS request for IPv6
+# packets whose MAC is unknown (in the ARP_REQUEST router pipeline stage.
+# test_ipv6 INPORT SRC_MAC DST_MAC SRC_IP DST_IP OUTPORT...
+# This function sends ipv6 packet
+test_ipv6() {
+    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4
+    dst_ip=20010db800010000020002fffe010205
+
+    local packet=${dst_mac}${src_mac}86dd6000000000083aff${src_ip}${dst_ip}
+    packet=${packet}8000000000000000
+    shift; shift; shift; shift
+
+    dst_mac=3333ff010205
+    src_mac=000002010204
+    mcast_node_ip=ff0200000000000000000001ff010205
+    expected_packet=${dst_mac}${src_mac}86dd6000000000203aff${src_ip}
+    expected_packet=${expected_packet}${mcast_node_ip}8700XXXX00000000${dst_ip}
+    expected_packet=${expected_packet}0101${src_mac}
+
+    as hv1 ovs-appctl netdev-dummy/receive hv1-vif${inport} $packet
+    echo $expected_packet >> ipv6_ns.expected
+}
+
+src_mac=506400000002
+dst_mac=00000000af01
+src_ip=aef0000000000000526400fffe000002
+# Send an IPv6 packet. Generated IPv6 Neighbor solicitation packet
+# should be received by the ports attached to br-phys.
+test_ipv6 1 $src_mac $dst_mac $src_ip 2
+
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/br-phys_n1-tx.pcap | \
+trim_zeros > 1.packets
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/br-phys-tx.pcap | \
+trim_zeros > 2.packets
+
+cat ipv6_ns.expected | cut -c -112 > expout
+AT_CHECK([cat 1.packets | cut -c -112], [0], [expout])
+AT_CHECK([cat 2.packets | cut -c -112], [0], [expout])
+
+# Skipping the ICMPv6 checksum
+cat ipv6_ns.expected | cut -c 117- > expout
+AT_CHECK([cat 1.packets | cut -c 117-], [0], [expout])
+AT_CHECK([cat 2.packets | cut -c 117-], [0], [expout])
+
+OVN_CLEANUP([hv1])
+
+AT_CLEANUP
+
 AT_SETUP([ovn -- options:requested-chassis for logical port])
 ovn_start