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 | expand |
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 >
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