summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/packets.c29
-rw-r--r--lib/packets.h4
-rw-r--r--ovn/controller/pinctrl.c126
-rw-r--r--ovn/lib/actions.c21
-rw-r--r--ovn/lib/actions.h6
-rw-r--r--ovn/northd/ovn-northd.c65
-rw-r--r--ovn/ovn-sb.xml40
-rw-r--r--tests/ovn.at101
-rw-r--r--tutorial/OVN-Tutorial.md6
9 files changed, 353 insertions, 45 deletions
diff --git a/lib/packets.c b/lib/packets.c
index 43b5a70f3..a27264c93 100644
--- a/lib/packets.c
+++ b/lib/packets.c
@@ -1355,6 +1355,35 @@ compose_nd(struct dp_packet *b, const struct eth_addr eth_src,
ND_MSG_LEN + ND_OPT_LEN));
}
+void
+compose_na(struct dp_packet *b,
+ 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 rco_flags)
+{
+ struct ovs_nd_msg *na;
+ struct ovs_nd_opt *nd_opt;
+ uint32_t icmp_csum;
+
+ eth_compose(b, eth_dst, eth_src, ETH_TYPE_IPV6, IPV6_HEADER_LEN);
+ na = compose_ipv6(b, IPPROTO_ICMPV6, ipv6_src, ipv6_dst, 0, 0, 255,
+ ND_MSG_LEN + ND_OPT_LEN);
+
+ na->icmph.icmp6_type = ND_NEIGHBOR_ADVERT;
+ na->icmph.icmp6_code = 0;
+ put_16aligned_be32(&na->rco_flags, rco_flags);
+
+ nd_opt = &na->options[0];
+ nd_opt->nd_opt_type = ND_OPT_TARGET_LINKADDR;
+ nd_opt->nd_opt_len = 1;
+
+ packet_set_nd(b, ipv6_src, eth_addr_zero, eth_src);
+ na->icmph.icmp6_cksum = 0;
+ icmp_csum = packet_csum_pseudoheader6(dp_packet_l3(b));
+ na->icmph.icmp6_cksum = csum_finish(csum_continue(icmp_csum, na,
+ ND_MSG_LEN + ND_OPT_LEN));
+}
+
uint32_t
packet_csum_pseudoheader(const struct ip_header *ip)
{
diff --git a/lib/packets.h b/lib/packets.h
index 594594053..077ccfafa 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -1069,6 +1069,10 @@ void compose_arp(struct dp_packet *, uint16_t arp_op,
ovs_be32 arp_spa, ovs_be32 arp_tpa);
void compose_nd(struct dp_packet *, const struct eth_addr eth_src,
struct in6_addr *, struct in6_addr *);
+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 rco_flags);
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 200505a66..f960ea9e1 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -25,6 +25,7 @@
#include "lport.h"
#include "nx-match.h"
#include "ovn-controller.h"
+#include "lib/packets.h"
#include "lib/sset.h"
#include "openvswitch/ofp-actions.h"
#include "openvswitch/ofp-msgs.h"
@@ -68,6 +69,11 @@ static void send_garp_run(const struct ovsrec_bridge *,
const char *chassis_id,
const struct lport_index *lports,
struct hmap *local_datapaths);
+static void pinctrl_handle_na(const struct flow *ip_flow,
+ const struct match *md,
+ struct ofpbuf *userdata);
+static void reload_metadata(struct ofpbuf *ofpacts,
+ const struct match *md);
COVERAGE_DEFINE(pinctrl_drop_put_arp);
@@ -157,31 +163,7 @@ pinctrl_handle_arp(const struct flow *ip_flow, const struct match *md,
struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
enum ofp_version version = rconn_get_version(swconn);
- enum mf_field_id md_fields[] = {
-#if FLOW_N_REGS == 8
- MFF_REG0,
- MFF_REG1,
- MFF_REG2,
- MFF_REG3,
- MFF_REG4,
- MFF_REG5,
- MFF_REG6,
- MFF_REG7,
-#else
-#error
-#endif
- MFF_METADATA,
- };
- for (size_t i = 0; i < ARRAY_SIZE(md_fields); i++) {
- const struct mf_field *field = mf_from_id(md_fields[i]);
- if (!mf_is_all_wild(field, &md->wc)) {
- struct ofpact_set_field *sf = ofpact_put_SET_FIELD(&ofpacts);
- sf->field = field;
- sf->flow_has_vlan = false;
- mf_get_value(field, &md->flow, &sf->value);
- memset(&sf->mask, 0xff, field->n_bytes);
- }
- }
+ reload_metadata(&ofpacts, md);
enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
version, &ofpacts);
if (error) {
@@ -428,6 +410,10 @@ process_packet_in(const struct ofp_header *msg)
pinctrl_handle_put_dhcp_opts(&packet, &pin, &userdata, &continuation);
break;
+ case ACTION_OPCODE_NA:
+ pinctrl_handle_na(&headers, &pin.flow_metadata, &userdata);
+ break;
+
default:
VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
ntohl(ah->opcode));
@@ -920,3 +906,93 @@ send_garp_run(const struct ovsrec_bridge *br_int, const char *chassis_id,
sset_destroy(&localnet_vifs);
simap_destroy(&localnet_ofports);
}
+
+static void
+reload_metadata(struct ofpbuf *ofpacts, const struct match *md)
+{
+ enum mf_field_id md_fields[] = {
+#if FLOW_N_REGS == 8
+ MFF_REG0,
+ MFF_REG1,
+ MFF_REG2,
+ MFF_REG3,
+ MFF_REG4,
+ MFF_REG5,
+ MFF_REG6,
+ MFF_REG7,
+#else
+#error
+#endif
+ MFF_METADATA,
+ };
+ for (size_t i = 0; i < ARRAY_SIZE(md_fields); i++) {
+ const struct mf_field *field = mf_from_id(md_fields[i]);
+ if (!mf_is_all_wild(field, &md->wc)) {
+ struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+ sf->field = field;
+ sf->flow_has_vlan = false;
+ mf_get_value(field, &md->flow, &sf->value);
+ memset(&sf->mask, 0xff, field->n_bytes);
+ }
+ }
+}
+
+static void
+pinctrl_handle_na(const struct flow *ip_flow,
+ const struct match *md,
+ struct ofpbuf *userdata)
+{
+ /* This action only works for IPv6 ND packets, and the switch should only
+ * send us ND packets this way, but check here just to be sure. */
+ if (!is_nd(ip_flow, NULL)) {
+ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+ VLOG_WARN_RL(&rl, "NA action on non-ND packet");
+ 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);
+
+ ovs_be32 ipv6_src[4], ipv6_dst[4];
+ memcpy(ipv6_dst, &ip_flow->ipv6_src, sizeof ipv6_src);
+ memcpy(ipv6_src, &ip_flow->nd_target, sizeof ipv6_dst);
+
+ /* Frame the NA packet with RSO=011. */
+ compose_na(&packet,
+ ip_flow->dl_dst, ip_flow->dl_src,
+ ipv6_src, ipv6_dst,
+ htonl(0x60000000));
+
+ /* Reload previous packet metadata. */
+ uint64_t ofpacts_stub[4096 / 8];
+ struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+ reload_metadata(&ofpacts, md);
+
+ enum ofperr error = ofpacts_pull_openflow_actions(userdata, userdata->size,
+ version, &ofpacts);
+ if (error) {
+ 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;
+ }
+
+ struct ofputil_packet_out po = {
+ .packet = dp_packet_data(&packet),
+ .packet_len = dp_packet_size(&packet),
+ .buffer_id = UINT32_MAX,
+ .in_port = OFPP_CONTROLLER,
+ .ofpacts = ofpacts.data,
+ .ofpacts_len = ofpacts.size,
+ };
+
+ queue_msg(ofputil_encode_packet_out(&po, proto));
+
+exit:
+ dp_packet_uninit(&packet);
+ ofpbuf_uninit(&ofpacts);
+}
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 52c74e6e9..550bb87b7 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -246,8 +246,11 @@ put_controller_op(struct ofpbuf *ofpacts, enum action_opcode opcode)
finish_controller_op(ofpacts, ofs);
}
+/* Implements the "arp" and "na" actions, which execute nested actions on a
+ * packet derived from the one being processed. */
static void
-parse_arp_action(struct action_context *ctx)
+parse_nested_action(struct action_context *ctx, enum action_opcode opcode,
+ const char *prereq)
{
if (!lexer_match(ctx->lexer, LEX_T_LCURLY)) {
action_syntax_error(ctx, "expecting `{'");
@@ -272,13 +275,11 @@ parse_arp_action(struct action_context *ctx)
ctx->ofpacts = outer_ofpacts;
- /* Add a "controller" action with the actions nested inside "arp {...}",
+ /* Add a "controller" action with the actions nested inside "{...}",
* converted to OpenFlow, as its userdata. ovn-controller will convert the
- * packet to an ARP and then send the packet and actions back to the switch
- * inside an OFPT_PACKET_OUT message. */
- /* controller. */
- size_t oc_offset = start_controller_op(ctx->ofpacts, ACTION_OPCODE_ARP,
- false);
+ * packet to ARP or NA and then send the packet and actions back to the
+ * switch inside an OFPT_PACKET_OUT message. */
+ size_t oc_offset = start_controller_op(ctx->ofpacts, opcode, false);
ofpacts_put_openflow_actions(inner_ofpacts.data, inner_ofpacts.size,
ctx->ofpacts, OFP13_VERSION);
finish_controller_op(ctx->ofpacts, oc_offset);
@@ -286,7 +287,7 @@ parse_arp_action(struct action_context *ctx)
/* Restore prerequisites. */
expr_destroy(ctx->prereqs);
ctx->prereqs = outer_prereqs;
- add_prerequisite(ctx, "ip4");
+ add_prerequisite(ctx, prereq);
/* Free memory. */
ofpbuf_uninit(&inner_ofpacts);
@@ -908,7 +909,9 @@ parse_action(struct action_context *ctx)
} else if (lexer_match_id(ctx->lexer, "ct_snat")) {
parse_ct_nat(ctx, true);
} else if (lexer_match_id(ctx->lexer, "arp")) {
- parse_arp_action(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, "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 f49e15ec6..6b9cd248e 100644
--- a/ovn/lib/actions.h
+++ b/ovn/lib/actions.h
@@ -54,6 +54,12 @@ enum action_opcode {
* - Any number of DHCP options.
*/
ACTION_OPCODE_PUT_DHCP_OPTS,
+
+ /* "na { ...actions... }".
+ *
+ * The actions, in OpenFlow 1.3 format, follow the action_header.
+ */
+ ACTION_OPCODE_NA,
};
/* Header. */
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index c2cf15eba..d36398e10 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -93,7 +93,7 @@ enum ovn_stage {
PIPELINE_STAGE(SWITCH, IN, PORT_SEC_ND, 2, "ls_in_port_sec_nd") \
PIPELINE_STAGE(SWITCH, IN, PRE_ACL, 3, "ls_in_pre_acl") \
PIPELINE_STAGE(SWITCH, IN, ACL, 4, "ls_in_acl") \
- PIPELINE_STAGE(SWITCH, IN, ARP_RSP, 5, "ls_in_arp_rsp") \
+ PIPELINE_STAGE(SWITCH, IN, ARP_ND_RSP, 5, "ls_in_arp_nd_rsp") \
PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 6, "ls_in_l2_lkup") \
\
/* Logical switch egress stages. */ \
@@ -1386,6 +1386,12 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports)
ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 100, "ip", "ct_next;");
ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 100, "ip", "ct_next;");
+ /* Ingress and Egress Pre-ACL Table (Priority 110).
+ *
+ * Not to do conntrack on ND packets. */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 110, "nd", "next;");
+ ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110, "nd", "next;");
+
/* Ingress and Egress ACL Table (Priority 1).
*
* By default, traffic is allowed. This is partially handled by
@@ -1436,6 +1442,12 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports)
ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
"!ct.est && ct.rel && !ct.new && !ct.inv",
"next;");
+
+ /* Ingress and Egress ACL Table (Priority 65535).
+ *
+ * Not to do conntrack on ND packets. */
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX, "nd", "next;");
+ ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX, "nd", "next;");
}
/* Ingress or Egress ACL Table (Various priorities). */
@@ -1569,13 +1581,13 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
if (!strcmp(op->nbs->type, "localnet")) {
char *match = xasprintf("inport == %s", op->json_key);
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_RSP, 100,
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
match, "next;");
free(match);
}
}
- /* Ingress table 5: ARP responder, reply for known IPs.
+ /* Ingress table 5: ARP/ND responder, reply for known IPs.
* (priority 50). */
HMAP_FOR_EACH (op, key_node, ports) {
if (!op->nbs) {
@@ -1583,7 +1595,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
}
/*
- * Add ARP reply flows if either the
+ * Add ARP/ND reply flows if either the
* - port is up or
* - port type is router
*/
@@ -1594,7 +1606,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
for (size_t i = 0; i < op->nbs->n_addresses; i++) {
struct lport_addresses laddrs;
if (!extract_lsp_addresses(op->nbs->addresses[i], &laddrs,
- false)) {
+ true)) {
continue;
}
for (size_t j = 0; j < laddrs.n_ipv4_addrs; j++) {
@@ -1615,24 +1627,61 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
ETH_ADDR_ARGS(laddrs.ea),
ETH_ADDR_ARGS(laddrs.ea),
IP_ARGS(laddrs.ipv4_addrs[j].addr));
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_RSP, 50,
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
match, actions);
free(match);
free(actions);
}
+ if (laddrs.n_ipv6_addrs > 0) {
+ char ip6_str[INET6_ADDRSTRLEN + 1];
+ struct ds match = DS_EMPTY_INITIALIZER;
+ ds_put_cstr(&match, "icmp6 && icmp6.type == 135 && ");
+ if (laddrs.n_ipv6_addrs == 1) {
+ ipv6_string_mapped(ip6_str,
+ &(laddrs.ipv6_addrs[0].addr));
+ ds_put_format(&match, "nd.target == %s", ip6_str);
+ } else {
+ ds_put_cstr(&match, "(");
+ for (size_t j = 0; j < laddrs.n_ipv6_addrs; j++) {
+ ipv6_string_mapped(ip6_str,
+ &(laddrs.ipv6_addrs[j].addr));
+ ds_put_format(&match, "nd.target == %s || ", ip6_str);
+ }
+ ds_chomp(&match, ' ');
+ ds_chomp(&match, '|');
+ ds_chomp(&match, '|');
+ ds_chomp(&match, ' ');
+ ds_put_cstr(&match, ")");
+ }
+ char *actions = xasprintf(
+ "na { eth.src = "ETH_ADDR_FMT"; "
+ "nd.tll = "ETH_ADDR_FMT"; "
+ "outport = inport; "
+ "inport = \"\"; /* Allow sending out inport. */ "
+ "output; };",
+ ETH_ADDR_ARGS(laddrs.ea),
+ ETH_ADDR_ARGS(laddrs.ea));
+
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+ ds_cstr(&match), actions);
+
+ ds_destroy(&match);
+ }
+
free(laddrs.ipv4_addrs);
+ free(laddrs.ipv6_addrs);
}
}
- /* Ingress table 5: ARP responder, by default goto next.
+ /* Ingress table 5: ARP/ND responder, by default goto next.
* (priority 0)*/
HMAP_FOR_EACH (od, key_node, datapaths) {
if (!od->nbs) {
continue;
}
- ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_RSP, 0, "1", "next;");
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_RSP, 0, "1", "next;");
}
/* Ingress table 6: Destination lookup, broadcast and multicast handling
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 4814b0a6c..ebc5cf06d 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1042,6 +1042,46 @@
<p><b>Prerequisite:</b> <code>ip4</code></p>
</dd>
+ <dt>
+ <code>na { <var>action</var>; </code>...<code> };</code>
+ </dt>
+
+ <dd>
+ <p>
+ Temporarily replaces the IPv6 packet being processed by an IPv6
+ neighbor advertisement (NA) packet and executes each nested
+ <var>action</var> on the NA packet. Actions following the
+ <var>na</var> action, if any, apply to the original, unmodified
+ packet.
+ </p>
+
+ <p>
+ The NA 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.dst</code> exchanged with <code>eth.src</code></li>
+ <li><code>eth.type = 0x86dd</code></li>
+ <li><code>ip6.dst</code> copied from <code>ip6.src</code></li>
+ <li><code>ip6.src</code> copied from <code>nd.target</code></li>
+ <li><code>icmp6.type = 136</code> (Neighbor Advertisement)</li>
+ <li><code>nd.target</code> unchanged</li>
+ <li><code>nd.sll = 00:00:00:00:00:00</code></li>
+ <li><code>nd.tll</code> copied from <code>eth.dst</code></li>
+ </ul>
+
+ <p>
+ The ND packet has the same VLAN header, if any, as the IPv6 packet
+ it replaces.
+ </p>
+
+ <p>
+ <b>Prerequisite:</b> <code>nd</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 37888bfa8..778cac7bc 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -573,6 +573,9 @@ reg1[0] = put_dhcp_opts(offerip=1.2.3.4, xyzzy); => Syntax error at `xyzzy' expe
reg1[0] = put_dhcp_opts(offerip="xyzzy"); => DHCP option offerip requires numeric value.
reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4); => DHCP option domain requires string value.
+# 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.0c.04.00.01.0e.04.00.19.00.10.00.01.0c.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
+
# 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
@@ -3393,3 +3396,101 @@ as main
OVS_APP_EXIT_AND_WAIT([ovs-vswitchd])
OVS_APP_EXIT_AND_WAIT([ovsdb-server])
AT_CLEANUP
+
+AT_SETUP([ovn -- nd ])
+AT_KEYWORDS([ovn-nd])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+#TODO: since patch port for IPv6 logical router port is not ready not,
+# so we are not going to test vifs on different lswitches cases. Try
+# to update for that once relevant stuff implemented.
+
+# 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.
+ovn-nbctl ls-add lsw0
+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 lsp-set-addresses lp1 "fa:16:3e:94:05:98 192.168.0.3 fd81:ce49:a948:0:f816:3eff:fe94:598"
+ovn-nbctl lsp-set-port-security lp1 "fa:16:3e:94:05:98 192.168.0.3 fd81:ce49:a948:0:f816:3eff:fe94:598"
+
+# Add vif2 to hv1 and lsw0, turn on l2 port security on vif2.
+ovs-vsctl add-port br-int vif2 -- set Interface vif2 external-ids:iface-id=lp2 options:tx_pcap=hv1/vif2-tx.pcap options:rxq_pcap=hv1/vif2-rx.pcap ofport-request=2
+ovn-nbctl lsp-add lsw0 lp2
+ovn-nbctl lsp-set-addresses lp2 "fa:16:3e:a1:f9:ae 192.168.0.4 fd81:ce49:a948:0:f816:3eff:fea1:f9ae"
+ovn-nbctl lsp-set-port-security lp2 "fa:16:3e:a1:f9:ae 192.168.0.4 fd81:ce49:a948:0:f816:3eff:fea1:f9ae"
+
+# 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
+ovn-nbctl acl-add lsw0 to-lport 1002 'outport == "lp2" && 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 Neighbor Solicitation packet and Neighbor Advertisement packet
+# vif1 -> NS -> vif2. vif1 <- NA <- ovn-controller.
+# vif2 will not receive NS packet, since ovn-controller will reply for it.
+ns_packet=3333ffa1f9aefa163e94059886dd6000000000203afffd81ce49a9480000f8163efffe940598fd81ce49a9480000f8163efffea1f9ae8700e01160000000fd81ce49a9480000f8163efffea1f9ae0101fa163e940598
+na_packet=fa163e940598fa163ea1f9ae86dd6000000000203afffd81ce49a9480000f8163efffea1f9aefd81ce49a9480000f8163efffe9405988800e9ed60000000fd81ce49a9480000f8163efffea1f9ae0201fa163ea1f9ae
+
+as hv1 ovs-appctl netdev-dummy/receive vif1 $ns_packet
+echo $na_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
+
+for i in 1 2; do
+ file=hv1/vif$i-tx.pcap
+ echo $file
+ $PYTHON "$top_srcdir/utilities/ovs-pcap.in" $file | trim_zeros > $i.packets
+ cat $i.expected > expout
+ AT_CHECK([cat $i.packets], [0], [expout])
+done
+
+as hv1
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+OVS_APP_EXIT_AND_WAIT([ovs-vswitchd])
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as main
+OVS_APP_EXIT_AND_WAIT([ovs-vswitchd])
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+AT_CLEANUP
diff --git a/tutorial/OVN-Tutorial.md b/tutorial/OVN-Tutorial.md
index c4bcbae42..811224db3 100644
--- a/tutorial/OVN-Tutorial.md
+++ b/tutorial/OVN-Tutorial.md
@@ -104,7 +104,7 @@ show the logical flows.
table=2(ls_in_port_sec_nd), priority= 0, match=(1), action=(next;)
table=3( ls_in_pre_acl), priority= 0, match=(1), action=(next;)
table=4( ls_in_acl), priority= 0, match=(1), action=(next;)
- table=5( ls_in_arp_rsp), priority= 0, match=(1), action=(next;)
+ table=5(ls_in_arp_nd_rsp), priority= 0, match=(1), action=(next;)
table=6( ls_in_l2_lkup), priority= 100, match=(eth.mcast), action=(outport = "_MC_flood"; output;)
table=6( ls_in_l2_lkup), priority= 50, match=(eth.dst == 00:00:00:00:00:01), action=(outport = "sw0-port1"; output;)
table=6( ls_in_l2_lkup), priority= 50, match=(eth.dst == 00:00:00:00:00:02), action=(outport = "sw0-port2"; output;)
@@ -277,7 +277,7 @@ OVN creates separate logical flows for each logical switch.
table=2(ls_in_port_sec_nd), priority= 0, match=(1), action=(next;)
table=3( ls_in_pre_acl), priority= 0, match=(1), action=(next;)
table=4( ls_in_acl), priority= 0, match=(1), action=(next;)
- table=5( ls_in_arp_rsp), priority= 0, match=(1), action=(next;)
+ table=5(ls_in_arp_nd_rsp), priority= 0, match=(1), action=(next;)
table=6( ls_in_l2_lkup), priority= 100, match=(eth.mcast), action=(outport = "_MC_flood"; output;)
table=6( ls_in_l2_lkup), priority= 50, match=(eth.dst == 00:00:00:00:00:03), action=(outport = "sw1-port1"; output;)
table=6( ls_in_l2_lkup), priority= 50, match=(eth.dst == 00:00:00:00:00:04), action=(outport = "sw1-port2"; output;)
@@ -303,7 +303,7 @@ OVN creates separate logical flows for each logical switch.
table=2(ls_in_port_sec_nd), priority= 0, match=(1), action=(next;)
table=3( ls_in_pre_acl), priority= 0, match=(1), action=(next;)
table=4( ls_in_acl), priority= 0, match=(1), action=(next;)
- table=5( ls_in_arp_rsp), priority= 0, match=(1), action=(next;)
+ table=5(ls_in_arp_nd_rsp), priority= 0, match=(1), action=(next;)
table=6( ls_in_l2_lkup), priority= 100, match=(eth.mcast), action=(outport = "_MC_flood"; output;)
table=6( ls_in_l2_lkup), priority= 50, match=(eth.dst == 00:00:00:00:00:01), action=(outport = "sw0-port1"; output;)
table=6( ls_in_l2_lkup), priority= 50, match=(eth.dst == 00:00:00:00:00:02), action=(outport = "sw0-port2"; output;)