[ovs-dev] [PATCH 1/6] userspace: Add support for NSH MD1 match fields

Yi Yang yi.y.yang at intel.com
Wed Jul 5 03:45:38 UTC 2017


From: Jan Scheurich <jan.scheurich at ericsson.com>

This patch adds support for NSH packet header fields to the OVS
control plane and the userspace datapath. Initially we support the
fields of the NSH base header as defined in
https://www.ietf.org/id/draft-ietf-sfc-nsh-12.txt
and the fixed context headers specified for metadata format MD1.
The variable length MD2 format is parsed but the TLV context headers
are not yet available for matching.

The NSH fields are modelled as OXM fields with the dedicated OXM
class 0x8004 proposed for NSH in ONF. The following fields are defined:

OXM code            ofctl name    Size      Comment
----------------------------------------------------------------------
OXM_NSH_FLAGS       nsh_flags       8       Bits 2-9 of 1st NSH word
(0x8004,1)
OXM_NSH_MDTYPE      nsh_mdtype      8       Bits 16-23
(0x8004,2)
OXM_NSH_NEXTPROTO   nsh_np          8       Bits 24-31
(0x8004,3)
OXM_NSH_SPI         nsh_spi         24      Bits 0-23 of 2nd NSH word
(0x8004,4)
OXM_NSH_SI          nsh_si          8       Bits 24-31
(0x8004,5)
OXM_NSH_C1          nsh_c1          32      Maskable, nsh_mdtype==1
(0x8004,6)
OXM_NSH_C2          nsh_c2          32      Maskable, nsh_mdtype==1
(0x8004,7)
OXM_NSH_C3          nsh_c3          32      Maskable, nsh_mdtype==1
(0x8004,8)
OXM_NSH_C4          nsh_c4          32      Maskable, nsh_mdtype==1
(0x8004,9)

Signed-off-by: Mengke Liu <mengke.liu at intel.com>
Signed-off-by: Ricky Li <ricky.li at intel.com>
Signed-off-by: Johnson Li <johnson.li at intel.com>
Signed-off-by: Yi Yang <yi.y.yang at intel.com>
Signed-off-by: Jan Scheurich <jan.scheurich at ericsson.com>
---
 build-aux/extract-ofp-fields                      |   2 +
 datapath/flow_netlink.c                           |   5 +-
 datapath/linux/compat/include/linux/openvswitch.h |  13 ++
 include/openvswitch/automake.mk                   |   3 +-
 include/openvswitch/flow.h                        |   8 +-
 include/openvswitch/match.h                       |  12 ++
 include/openvswitch/meta-flow.h                   | 132 ++++++++++++++
 include/openvswitch/nsh.h                         | 122 +++++++++++++
 include/openvswitch/packets.h                     |  19 ++
 lib/flow.c                                        | 100 ++++++++++-
 lib/flow.h                                        |   3 +-
 lib/match.c                                       |  63 ++++++-
 lib/meta-flow.c                                   | 181 +++++++++++++++++++
 lib/meta-flow.xml                                 |  21 +++
 lib/nx-match.c                                    |  20 ++-
 lib/odp-execute.c                                 |   4 +
 lib/odp-util.c                                    | 208 ++++++++++++++++++++++
 lib/odp-util.h                                    |   2 +-
 lib/ofp-util.c                                    |   2 +-
 lib/packets.h                                     |   3 +
 ofproto/ofproto-dpif-rid.h                        |   2 +-
 ofproto/ofproto-dpif-sflow.c                      |   1 +
 ofproto/ofproto-dpif-xlate.c                      |   2 +-
 tests/ofproto.at                                  |  11 +-
 24 files changed, 915 insertions(+), 24 deletions(-)
 create mode 100644 include/openvswitch/nsh.h

diff --git a/build-aux/extract-ofp-fields b/build-aux/extract-ofp-fields
index 24dd756..7d978b9 100755
--- a/build-aux/extract-ofp-fields
+++ b/build-aux/extract-ofp-fields
@@ -46,6 +46,7 @@ PREREQS = {"none": "MFP_NONE",
            "IPv4": "MFP_IPV4",
            "IPv6": "MFP_IPV6",
            "IPv4/IPv6": "MFP_IP_ANY",
+           "NSH": "MFP_NSH",
            "CT": "MFP_CT_VALID",
            "MPLS": "MFP_MPLS",
            "TCP": "MFP_TCP",
@@ -68,6 +69,7 @@ OXM_CLASSES = {"NXM_OF_":        (0,          0x0000),
                "NXM_NX_":        (0,          0x0001),
                "OXM_OF_":        (0,          0x8000),
                "OXM_OF_PKT_REG": (0,          0x8001),
+               "OXM_NSH_":       (0,          0x8004),
                "ONFOXM_ET_":     (0x4f4e4600, 0xffff),
 
                # This is the experimenter OXM class for Nicira, which is the
diff --git a/datapath/flow_netlink.c b/datapath/flow_netlink.c
index 07ab8e9..d7a54db 100644
--- a/datapath/flow_netlink.c
+++ b/datapath/flow_netlink.c
@@ -329,7 +329,7 @@ size_t ovs_key_attr_size(void)
 	/* Whenever adding new OVS_KEY_ FIELDS, we should consider
 	 * updating this function.
 	 */
-	BUILD_BUG_ON(OVS_KEY_ATTR_TUNNEL_INFO != 28);
+	BUILD_BUG_ON(OVS_KEY_ATTR_TUNNEL_INFO != 29);
 
 	return    nla_total_size(4)   /* OVS_KEY_ATTR_PRIORITY */
 		+ nla_total_size(0)   /* OVS_KEY_ATTR_TUNNEL */
@@ -350,7 +350,8 @@ size_t ovs_key_attr_size(void)
 		+ nla_total_size(2)   /* OVS_KEY_ATTR_ETHERTYPE */
 		+ nla_total_size(40)  /* OVS_KEY_ATTR_IPV6 */
 		+ nla_total_size(2)   /* OVS_KEY_ATTR_ICMPV6 */
-		+ nla_total_size(28); /* OVS_KEY_ATTR_ND */
+		+ nla_total_size(28)  /* OVS_KEY_ATTR_ND */
+		+ nla_total_size(24); /* OVS_KEY_ATTR_NSH */
 }
 
 static const struct ovs_len_tbl ovs_vxlan_ext_key_lens[OVS_VXLAN_EXT_MAX + 1] = {
diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index 91d3140..f5814c5 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -359,6 +359,7 @@ enum ovs_key_attr {
 	OVS_KEY_ATTR_CT_LABELS,	/* 16-octet connection tracking labels */
 	OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4,   /* struct ovs_key_ct_tuple_ipv4 */
 	OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6,   /* struct ovs_key_ct_tuple_ipv6 */
+	OVS_KEY_ATTR_NSH,       /* struct ovs_key_nsh */
 
 #ifdef __KERNEL__
 	/* Only used within kernel data path. */
@@ -490,6 +491,18 @@ struct ovs_key_ct_labels {
 	};
 };
 
+struct ovs_key_nsh {
+    __u8 flags;
+    __u8 mdtype;
+    __u8 np;
+    __u8 pad;
+    __be32 path_hdr;
+    __be32 c1;
+    __be32 c2;
+    __be32 c3;
+    __be32 c4;
+};
+
 /* OVS_KEY_ATTR_CT_STATE flags */
 #define OVS_CS_F_NEW               0x01 /* Beginning of a new connection. */
 #define OVS_CS_F_ESTABLISHED       0x02 /* Part of an existing connection. */
diff --git a/include/openvswitch/automake.mk b/include/openvswitch/automake.mk
index c8e67a2..25ac9a7 100644
--- a/include/openvswitch/automake.mk
+++ b/include/openvswitch/automake.mk
@@ -30,4 +30,5 @@ openvswitchinclude_HEADERS = \
 	include/openvswitch/uuid.h \
 	include/openvswitch/version.h \
 	include/openvswitch/vconn.h \
-	include/openvswitch/vlog.h
+	include/openvswitch/vlog.h \
+	include/openvswitch/nsh.h
diff --git a/include/openvswitch/flow.h b/include/openvswitch/flow.h
index 36a2a85..91966df 100644
--- a/include/openvswitch/flow.h
+++ b/include/openvswitch/flow.h
@@ -23,7 +23,7 @@
 /* This sequence number should be incremented whenever anything involving flows
  * or the wildcarding of flows changes.  This will cause build assertion
  * failures in places which likely need to be updated. */
-#define FLOW_WC_SEQ 39
+#define FLOW_WC_SEQ 40
 
 /* Number of Open vSwitch extension 32-bit registers. */
 #define FLOW_N_REGS 16
@@ -142,6 +142,7 @@ struct flow {
     struct eth_addr arp_tha;    /* ARP/ND target hardware address. */
     ovs_be16 tcp_flags;         /* TCP flags. With L3 to avoid matching L4. */
     ovs_be16 pad2;              /* Pad to 64 bits. */
+    struct flow_nsh nsh;        /* Network Service Header keys */
 
     /* L4 (64-bit aligned) */
     ovs_be16 tp_src;            /* TCP/UDP/SCTP source port/ICMP type. */
@@ -154,13 +155,14 @@ struct flow {
 };
 BUILD_ASSERT_DECL(sizeof(struct flow) % sizeof(uint64_t) == 0);
 BUILD_ASSERT_DECL(sizeof(struct flow_tnl) % sizeof(uint64_t) == 0);
+BUILD_ASSERT_DECL(sizeof(struct flow_nsh) % sizeof(uint64_t) == 0);
 
 #define FLOW_U64S (sizeof(struct flow) / sizeof(uint64_t))
 
 /* Remember to update FLOW_WC_SEQ when changing 'struct flow'. */
 BUILD_ASSERT_DECL(offsetof(struct flow, igmp_group_ip4) + sizeof(uint32_t)
-                  == sizeof(struct flow_tnl) + 300
-                  && FLOW_WC_SEQ == 39);
+                  == sizeof(struct flow_tnl) + sizeof(struct flow_nsh) + 300
+                  && FLOW_WC_SEQ == 40);
 
 /* Incremental points at which flow classification may be performed in
  * segments.
diff --git a/include/openvswitch/match.h b/include/openvswitch/match.h
index aca7252..f2afd4a 100644
--- a/include/openvswitch/match.h
+++ b/include/openvswitch/match.h
@@ -42,6 +42,18 @@ struct match {
 /* Initializer for a "struct match" that matches every packet. */
 #define MATCH_CATCHALL_INITIALIZER { .flow = { .dl_type = 0 } }
 
+#define MATCH_SET_FIELD_MASKED(match, field, value, msk)      \
+    do {                                                      \
+        (match)->wc.masks.field = (msk);                      \
+        (match)->flow.field = (value) & (msk);                \
+    } while (0)
+
+#define MATCH_SET_FIELD_UINT8(match, field, value)            \
+    MATCH_SET_FIELD_MASKED(match, field, value, UINT8_MAX)
+
+#define MATCH_SET_FIELD_BE32(match, field, value)             \
+    MATCH_SET_FIELD_MASKED(match, field, value, OVS_BE32_MAX)
+
 void match_init(struct match *,
                 const struct flow *, const struct flow_wildcards *);
 void match_wc_init(struct match *match, const struct flow *flow);
diff --git a/include/openvswitch/meta-flow.h b/include/openvswitch/meta-flow.h
index fc10950..9b777db 100644
--- a/include/openvswitch/meta-flow.h
+++ b/include/openvswitch/meta-flow.h
@@ -1735,6 +1735,137 @@ enum OVS_PACKED_ENUM mf_field_id {
      */
     MFF_ND_TLL,
 
+/* ## ---- ## */
+/* ## NSH  ## */
+/* ## ---- ## */
+
+    /* "nsh_flags".
+     *
+     * flags field in NSH base header (8 bits).
+     *
+     * Type: u8.
+     * Maskable: bitwise.
+     * Formatting: decimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_FLAGS(1) since OF1.3 and v2.8.
+     */
+    MFF_NSH_FLAGS,
+
+    /* "nsh_mdtype".
+     *
+     * mdtype field in NSH base header (8 bits).
+     *
+     * Type: u8.
+     * Maskable: no.
+     * Formatting: decimal.
+     * Prerequisites: NSH.
+     * Access: read-only.
+     * NXM: none.
+     * OXM: OXM_NSH_MDTYPE(2) since OF1.3 and v2.8.
+     */
+    MFF_NSH_MDTYPE,
+
+    /* "nsh_np".
+     *
+     * np (next protocol) field in NSH base header (8 bits)
+     *
+     * Type: u8.
+     * Maskable: no.
+     * Formatting: decimal.
+     * Prerequisites: NSH.
+     * Access: read-only.
+     * NXM: none.
+     * OXM: OXM_NSH_NP(3) since OF1.3 and v2.8.
+     */
+    MFF_NSH_NP,
+
+    /* "nsh_spi" (aka "nsp").
+     *
+     * spi (service path identifier) field in NSH base
+     * header (24 bits).
+     *
+     * Type: be32.
+     * Maskable: no.
+     * Formatting: hexadecimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_SPI(4) since OF1.3 and v2.8.
+     */
+    MFF_NSH_SPI,
+
+    /* "nsh_si" (aka "nsi").
+     *
+     * si (service index) field in NSH base header (8 bits).
+     *
+     * Type: u8.
+     * Maskable: no.
+     * Formatting: decimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_SI(5) since OF1.3 and v2.8.
+     */
+    MFF_NSH_SI,
+
+    /* "nsh_c1" (aka "nshc1").
+     *
+     * c1 (Network Platform Context) field in NSH context header (32 bits)
+     *
+     * Type: be32.
+     * Maskable: bitwise.
+     * Formatting: hexadecimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_C1(6) since OF1.3 and v2.8.
+     */
+    MFF_NSH_C1,
+
+    /* "nsh_c2" (aka "nshc2").
+     *
+     * c2 (Network Shared Context) field in NSH context header (32 bits)
+     *
+     * Type: be32.
+     * Maskable: bitwise.
+     * Formatting: hexadecimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_C2(7) since OF1.3 and v2.8.
+     */
+    MFF_NSH_C2,
+
+    /* "nsh_c3" (aka "nshc3").
+     *
+     * c3 (Service Platform Context) field in NSH context header (32 bits)
+     *
+     * Type: be32.
+     * Maskable: bitwise.
+     * Formatting: hexadecimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_C3(8) since OF1.3 and v2.8.
+     */
+    MFF_NSH_C3,
+
+    /* "nsh_c4" (aka "nshc4").
+     *
+     * c4 (Service Shared Context) field in NSH context header (32 bits)
+     *
+     * Type: be32.
+     * Maskable: bitwise.
+     * Formatting: hexadecimal.
+     * Prerequisites: NSH.
+     * Access: read/write.
+     * NXM: none.
+     * OXM: OXM_NSH_C4(9) since OF1.3 and v2.8.
+     */
+    MFF_NSH_C4,
+
     MFF_N_IDS
 };
 
@@ -1834,6 +1965,7 @@ enum OVS_PACKED_ENUM mf_prereqs {
     MFP_IPV4,
     MFP_IPV6,
     MFP_IP_ANY,
+    MFP_NSH,
 
     /* L2.5 requirements. */
     MFP_MPLS,
diff --git a/include/openvswitch/nsh.h b/include/openvswitch/nsh.h
new file mode 100644
index 0000000..532438b
--- /dev/null
+++ b/include/openvswitch/nsh.h
@@ -0,0 +1,122 @@
+#ifndef __OPENVSWITCH_NSH_H
+#define __OPENVSWITCH_NSH_H 1
+
+#include "openvswitch/types.h"
+
+/*
+ * Network Service Header:
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |Ver|O|C|R|R|R|R|R|R|    Length   |   MD Type   |  Next Proto   |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                Service Path ID                | Service Index |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * ~               Mandatory/Optional Context Header               ~
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * Ver = The version field is used to ensure backward compatibility
+ *       going forward with future NSH updates.  It MUST be set to 0x0
+ *       by the sender, in this first revision of NSH.
+ *
+ * O = OAM. when set to 0x1 indicates that this packet is an operations
+ *     and management (OAM) packet.  The receiving SFF and SFs nodes
+ *     MUST examine the payload and take appropriate action.
+ *
+ * C = context. Indicates that a critical metadata TLV is present.
+ *
+ * Length : total length, in 4-byte words, of NSH including the Base
+ *          Header, the Service Path Header and the optional variable
+ *          TLVs.
+ * MD Type: indicates the format of NSH beyond the mandatory Base Header
+ *          and the Service Path Header.
+ *
+ * Next Protocol: indicates the protocol type of the original packet. A
+ *          new IANA registry will be created for protocol type.
+ *
+ * Service Path Identifier (SPI): identifies a service path.
+ *          Participating nodes MUST use this identifier for Service
+ *          Function Path selection.
+ *
+ * Service Index (SI): provides location within the SFP.
+ *
+ * [0] https://tools.ietf.org/html/draft-ietf-sfc-nsh-13
+ */
+
+/**
+ * struct nsh_md1_ctx - Keeps track of NSH context data
+ * @nshc<1-4>: NSH Contexts.
+ */
+struct nsh_md1_ctx {
+    ovs_be32 c1;
+    ovs_be32 c2;
+    ovs_be32 c3;
+    ovs_be32 c4;
+};
+
+struct nsh_md2_tlv {
+    ovs_be16 md_class;
+    uint8_t type;
+    uint8_t length;
+    uint8_t md_value[];
+};
+
+struct nsh_hdr {
+    ovs_be16 ver_flags_len;
+    uint8_t md_type;
+    uint8_t next_proto;
+    ovs_be32 path_hdr;
+    union {
+        struct nsh_md1_ctx md1;
+        struct nsh_md2_tlv md2[0];
+    };
+};
+
+/* Masking NSH header fields. */
+#define NSH_VER_MASK       0xc000
+#define NSH_VER_SHIFT      12
+#define NSH_FLAGS_MASK     0x3fc0
+#define NSH_FLAGS_SHIFT    6
+#define NSH_LEN_MASK       0x003f
+#define NSH_LEN_SHIFT      0
+
+#define NSH_SPI_MASK       0xffffff00
+#define NSH_SPI_SHIFT      8
+#define NSH_SI_MASK        0x000000ff
+#define NSH_SI_SHIFT       0
+
+#define NSH_DST_PORT    4790     /* UDP Port for NSH on VXLAN. */
+#define ETH_P_NSH       0x894F   /* Ethertype for NSH. */
+
+/* NSH Base Header Next Protocol. */
+#define NSH_P_IPV4        0x01
+#define NSH_P_IPV6        0x02
+#define NSH_P_ETHERNET    0x03
+
+/* MD Type Registry. */
+#define NSH_M_TYPE1     0x01
+#define NSH_M_TYPE2     0x02
+#define NSH_M_EXP1      0xFE
+#define NSH_M_EXP2      0xFF
+
+/* sizeof(struct nsh_hdr) + sizeof(struct nsh_md1_ctx). */
+#define NSH_M_TYPE1_LEN  24
+
+static inline uint16_t
+nsh_hdr_len(const struct nsh_hdr *nsh)
+{
+    return 4 * (ntohs(nsh->ver_flags_len) & NSH_LEN_MASK) >> NSH_LEN_SHIFT;
+}
+
+static inline struct nsh_md1_ctx *
+nsh_md1_ctx(struct nsh_hdr *nsh)
+{
+    return &nsh->md1;
+}
+
+static inline struct nsh_md2_tlv *
+nsh_md2_ctx(struct nsh_hdr *nsh)
+{
+    return nsh->md2;
+}
+
+#endif
diff --git a/include/openvswitch/packets.h b/include/openvswitch/packets.h
index f13d634..1592847 100644
--- a/include/openvswitch/packets.h
+++ b/include/openvswitch/packets.h
@@ -69,4 +69,23 @@ union flow_vlan_hdr {
     };
 };
 
+/* Network Service Header keys */
+struct flow_nsh {
+    uint8_t flags;
+    uint8_t mdtype;
+    uint8_t np;
+    uint8_t si;
+    ovs_be32 spi;
+    ovs_be32 c1;
+    ovs_be32 c2;
+    ovs_be32 c3;
+    ovs_be32 c4;
+};
+
+/* NSH flags */
+#define FLOW_NSH_F_OAM (1 << 0)
+#define FLOW_NSH_F_CTX (1 << 1)
+
+#define FLOW_NSH_F_MASK ((1 << 2) - 1)
+
 #endif /* packets.h */
diff --git a/lib/flow.c b/lib/flow.c
index 75a91cc..3309936 100644
--- a/lib/flow.c
+++ b/lib/flow.c
@@ -40,6 +40,7 @@
 #include "random.h"
 #include "unaligned.h"
 #include "util.h"
+#include "openvswitch/nsh.h"
 
 COVERAGE_DEFINE(flow_extract);
 COVERAGE_DEFINE(miniflow_malloc);
@@ -125,7 +126,7 @@ struct mf_ctx {
  * away.  Some GCC versions gave warnings on ALWAYS_INLINE, so these are
  * defined as macros. */
 
-#if (FLOW_WC_SEQ != 39)
+#if (FLOW_WC_SEQ != 40)
 #define MINIFLOW_ASSERT(X) ovs_assert(X)
 BUILD_MESSAGE("FLOW_WC_SEQ changed: miniflow_extract() will have runtime "
                "assertions enabled. Consider updating FLOW_WC_SEQ after "
@@ -528,6 +529,56 @@ parse_ipv6_ext_hdrs(const void **datap, size_t *sizep, uint8_t *nw_proto,
     return parse_ipv6_ext_hdrs__(datap, sizep, nw_proto, nw_frag);
 }
 
+int
+parse_nsh(const void **datap, size_t *sizep, struct flow_nsh *key)
+{
+    const struct nsh_hdr *nsh = (const struct nsh_hdr *) *datap;
+    uint16_t ver_flags_len;
+    uint8_t version, length, flags;
+    uint32_t path_hdr;
+
+    memset(key, 0, sizeof(struct flow_nsh));
+
+    ver_flags_len = ntohs(nsh->ver_flags_len);
+    version = (ver_flags_len & NSH_VER_MASK) >> NSH_VER_SHIFT;
+    flags = (ver_flags_len & NSH_FLAGS_MASK) >> NSH_FLAGS_SHIFT;
+    length = (ver_flags_len & NSH_LEN_MASK) >> NSH_LEN_SHIFT;
+
+    if (version != 0) {
+        return false;
+    }
+
+    key->flags = flags;
+    key->mdtype = nsh->md_type;
+    key->np = nsh->next_proto;
+
+    path_hdr = ntohl(nsh->path_hdr);
+    key->si = (path_hdr & NSH_SI_MASK) >> NSH_SI_SHIFT;
+    key->spi = htonl((path_hdr & NSH_SPI_MASK) >> NSH_SPI_SHIFT);
+
+    switch (key->mdtype) {
+        case NSH_M_TYPE1:
+            if (length != 6) {
+                return -EINVAL;
+            }
+            key->c1 = nsh->md1.c1;
+            key->c2 = nsh->md1.c2;
+            key->c3 = nsh->md1.c3;
+            key->c4 = nsh->md1.c4;
+            break;
+        case NSH_M_TYPE2:
+            /* TODO */
+            break;
+        default:
+            return false;
+    }
+
+    /* NSH header length is in 4 byte words. */
+    data_pull(datap, sizep, 4 * length);
+
+    return true;
+}
+
 /* Initializes 'flow' members from 'packet' and 'md', taking the packet type
  * into account.
  *
@@ -817,6 +868,19 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
                 miniflow_push_macs(mf, arp_sha, arp_buf);
                 miniflow_pad_to_64(mf, arp_tha);
             }
+        } else if (dl_type == htons(ETH_TYPE_NSH)) {
+            struct flow_nsh nsh;
+
+            if (OVS_LIKELY(parse_nsh(&data, &size, &nsh))) {
+                if (nsh.mdtype == NSH_M_TYPE1) {
+                    miniflow_push_words(mf, nsh, &nsh,
+                                        sizeof(struct flow_nsh) /
+                                        sizeof(uint64_t));
+                }
+                else if (nsh.mdtype == NSH_M_TYPE2) {
+                    /* TODO */
+                }
+            }
         }
         goto out;
     }
@@ -950,7 +1014,7 @@ flow_get_metadata(const struct flow *flow, struct match *flow_metadata)
 {
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     match_init_catchall(flow_metadata);
     if (flow->tunnel.tun_id != htonll(0)) {
@@ -1446,7 +1510,7 @@ flow_wildcards_init_for_packet(struct flow_wildcards *wc,
     memset(&wc->masks, 0x0, sizeof wc->masks);
 
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     if (flow_tnl_dst_is_set(&flow->tunnel)) {
         if (flow->tunnel.flags & FLOW_TNL_F_KEY) {
@@ -1545,6 +1609,16 @@ flow_wildcards_init_for_packet(struct flow_wildcards *wc,
             }
         }
         return;
+    } else if (flow->dl_type == htons(ETH_TYPE_NSH)) {
+        WC_MASK_FIELD(wc, nsh.flags);
+        WC_MASK_FIELD(wc, nsh.mdtype);
+        WC_MASK_FIELD(wc, nsh.np);
+        WC_MASK_FIELD(wc, nsh.spi);
+        WC_MASK_FIELD(wc, nsh.si);
+        WC_MASK_FIELD(wc, nsh.c1);
+        WC_MASK_FIELD(wc, nsh.c2);
+        WC_MASK_FIELD(wc, nsh.c3);
+        WC_MASK_FIELD(wc, nsh.c4);
     } else {
         return; /* Unknown ethertype. */
     }
@@ -1586,7 +1660,7 @@ void
 flow_wc_map(const struct flow *flow, struct flowmap *map)
 {
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     flowmap_init(map);
 
@@ -1672,6 +1746,16 @@ flow_wc_map(const struct flow *flow, struct flowmap *map)
         FLOWMAP_SET(map, nw_proto);
         FLOWMAP_SET(map, arp_sha);
         FLOWMAP_SET(map, arp_tha);
+    } else if (flow->dl_type == htons(ETH_TYPE_NSH)) {
+        FLOWMAP_SET(map, nsh.flags);
+        FLOWMAP_SET(map, nsh.mdtype);
+        FLOWMAP_SET(map, nsh.np);
+        FLOWMAP_SET(map, nsh.spi);
+        FLOWMAP_SET(map, nsh.si);
+        FLOWMAP_SET(map, nsh.c1);
+        FLOWMAP_SET(map, nsh.c2);
+        FLOWMAP_SET(map, nsh.c3);
+        FLOWMAP_SET(map, nsh.c4);
     }
 }
 
@@ -1681,7 +1765,7 @@ void
 flow_wildcards_clear_non_packet_fields(struct flow_wildcards *wc)
 {
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     memset(&wc->masks.metadata, 0, sizeof wc->masks.metadata);
     memset(&wc->masks.regs, 0, sizeof wc->masks.regs);
@@ -1825,7 +1909,7 @@ flow_wildcards_set_xxreg_mask(struct flow_wildcards *wc, int idx,
 uint32_t
 miniflow_hash_5tuple(const struct miniflow *flow, uint32_t basis)
 {
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
     uint32_t hash = basis;
 
     if (flow) {
@@ -1872,7 +1956,7 @@ ASSERT_SEQUENTIAL(ipv6_src, ipv6_dst);
 uint32_t
 flow_hash_5tuple(const struct flow *flow, uint32_t basis)
 {
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
     uint32_t hash = basis;
 
     if (flow) {
@@ -2461,7 +2545,7 @@ flow_push_mpls(struct flow *flow, int n, ovs_be16 mpls_eth_type,
 
         if (clear_flow_L3) {
             /* Clear all L3 and L4 fields and dp_hash. */
-            BUILD_ASSERT(FLOW_WC_SEQ == 39);
+            BUILD_ASSERT(FLOW_WC_SEQ == 40);
             memset((char *) flow + FLOW_SEGMENT_2_ENDS_AT, 0,
                    sizeof(struct flow) - FLOW_SEGMENT_2_ENDS_AT);
             flow->dp_hash = 0;
diff --git a/lib/flow.h b/lib/flow.h
index f61d6c3..e76db6e 100644
--- a/lib/flow.h
+++ b/lib/flow.h
@@ -126,6 +126,7 @@ void flow_compose(struct dp_packet *, const struct flow *);
 bool parse_ipv6_ext_hdrs(const void **datap, size_t *sizep, uint8_t *nw_proto,
                          uint8_t *nw_frag);
 ovs_be16 parse_dl_type(const struct eth_header *data_, size_t size);
+int parse_nsh(const void **datap, size_t *sizep, struct flow_nsh *key);
 
 static inline uint64_t
 flow_get_xreg(const struct flow *flow, int idx)
@@ -911,7 +912,7 @@ static inline void
 pkt_metadata_from_flow(struct pkt_metadata *md, const struct flow *flow)
 {
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     md->recirc_id = flow->recirc_id;
     md->dp_hash = flow->dp_hash;
diff --git a/lib/match.c b/lib/match.c
index f5288e3..5858cd1 100644
--- a/lib/match.c
+++ b/lib/match.c
@@ -24,6 +24,7 @@
 #include "openvswitch/ofp-util.h"
 #include "packets.h"
 #include "tun-metadata.h"
+#include "openvswitch/nsh.h"
 
 /* Converts the flow in 'flow' into a match in 'match', with the given
  * 'wildcards'. */
@@ -1089,6 +1090,21 @@ format_ipv6_netmask(struct ds *s, const char *name,
 }
 
 static void
+format_uint8_masked(struct ds *s, const char *name,
+                   uint8_t value, uint8_t mask)
+{
+    if (mask != 0) {
+        ds_put_format(s, "%s%s=%s", colors.param, name, colors.end);
+        if (mask == UINT8_MAX) {
+            ds_put_format(s, "%"PRIu8, value);
+        } else {
+            ds_put_format(s, "0x%02"PRIx8"/0x%02"PRIx8, value, mask);
+        }
+        ds_put_char(s, ',');
+    }
+}
+
+static void
 format_uint16_masked(struct ds *s, const char *name,
                    uint16_t value, uint16_t mask)
 {
@@ -1128,7 +1144,23 @@ format_be32_masked(struct ds *s, const char *name,
         if (mask == OVS_BE32_MAX) {
             ds_put_format(s, "%"PRIu32, ntohl(value));
         } else {
-            ds_put_format(s, "0x%"PRIx32"/0x%"PRIx32,
+            ds_put_format(s, "0x%08"PRIx32"/0x%08"PRIx32,
+                          ntohl(value), ntohl(mask));
+        }
+        ds_put_char(s, ',');
+    }
+}
+
+static void
+format_be32_masked_hex(struct ds *s, const char *name,
+                       ovs_be32 value, ovs_be32 mask)
+{
+    if (mask != htonl(0)) {
+        ds_put_format(s, "%s%s=%s", colors.param, name, colors.end);
+        if (mask == OVS_BE32_MAX) {
+            ds_put_format(s, "0x%08"PRIx32, ntohl(value));
+        } else {
+            ds_put_format(s, "0x%08"PRIx32"/0x%08"PRIx32,
                           ntohl(value), ntohl(mask));
         }
         ds_put_char(s, ',');
@@ -1218,6 +1250,30 @@ format_ct_label_masked(struct ds *s, const ovs_u128 *key, const ovs_u128 *mask)
     }
 }
 
+static void
+format_nsh_masked(struct ds *s, const struct flow *f, const struct flow *m)
+{
+    if (m->nsh.flags)
+        format_uint8_masked(s, "nsh_flags", f->nsh.flags, m->nsh.flags);
+    if (m->nsh.mdtype)
+        format_uint8_masked(s, "nsh_mdtype", f->nsh.mdtype, m->nsh.mdtype);
+    if (m->nsh.np)
+        format_uint8_masked(s, "nsh_np", f->nsh.np, m->nsh.np);
+    if (m->nsh.spi)
+        format_be32_masked(s, "nsh_spi", f->nsh.spi, m->nsh.spi);
+    if (m->nsh.si)
+        format_uint8_masked(s, "nsh_si", f->nsh.si, m->nsh.si);
+
+    if (m->nsh.c1)
+        format_be32_masked_hex(s, "nsh_c1", f->nsh.c1, m->nsh.c1);
+    if (m->nsh.c2)
+        format_be32_masked_hex(s, "nsh_c2", f->nsh.c2, m->nsh.c2);
+    if (m->nsh.c3)
+        format_be32_masked_hex(s, "nsh_c3", f->nsh.c3, m->nsh.c3);
+    if (m->nsh.c4)
+        format_be32_masked_hex(s, "nsh_c4", f->nsh.c4, m->nsh.c4);
+}
+
 /* Appends a string representation of 'match' to 's'.  If 'priority' is
  * different from OFP_DEFAULT_PRIORITY, includes it in 's'.  If 'port_map' is
  * nonnull, uses it to translate port numbers to names in output. */
@@ -1235,7 +1291,7 @@ match_format(const struct match *match,
 
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     if (priority != OFP_DEFAULT_PRIORITY) {
         ds_put_format(s, "%spriority=%s%d,",
@@ -1321,7 +1377,6 @@ match_format(const struct match *match,
     }
 
     if (wc->masks.dl_type) {
-        dl_type = f->dl_type;
         skip_type = true;
         if (dl_type == htons(ETH_TYPE_IP)) {
             if (wc->masks.nw_proto) {
@@ -1459,6 +1514,8 @@ match_format(const struct match *match,
                dl_type == htons(ETH_TYPE_RARP)) {
         format_ip_netmask(s, "arp_spa", f->nw_src, wc->masks.nw_src);
         format_ip_netmask(s, "arp_tpa", f->nw_dst, wc->masks.nw_dst);
+    } else if (dl_type == htons(ETH_TYPE_NSH)) {
+        format_nsh_masked(s, f, &wc->masks);
     } else {
         format_ip_netmask(s, "nw_src", f->nw_src, wc->masks.nw_src);
         format_ip_netmask(s, "nw_dst", f->nw_dst, wc->masks.nw_dst);
diff --git a/lib/meta-flow.c b/lib/meta-flow.c
index d098081..53c8ed0 100644
--- a/lib/meta-flow.c
+++ b/lib/meta-flow.c
@@ -359,6 +359,25 @@ mf_is_all_wild(const struct mf_field *mf, const struct flow_wildcards *wc)
     case MFF_TCP_FLAGS:
         return !wc->masks.tcp_flags;
 
+    case MFF_NSH_FLAGS:
+        return !wc->masks.nsh.flags;
+    case MFF_NSH_MDTYPE:
+        return !wc->masks.nsh.mdtype;
+    case MFF_NSH_NP:
+        return !wc->masks.nsh.np;
+    case MFF_NSH_SPI:
+        return !wc->masks.nsh.spi;
+    case MFF_NSH_SI:
+        return !wc->masks.nsh.si;
+    case MFF_NSH_C1:
+        return !wc->masks.nsh.c1;
+    case MFF_NSH_C2:
+        return !wc->masks.nsh.c2;
+    case MFF_NSH_C3:
+        return !wc->masks.nsh.c3;
+    case MFF_NSH_C4:
+        return !wc->masks.nsh.c4;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
@@ -423,6 +442,8 @@ mf_are_prereqs_ok__(const struct mf_field *mf, const struct flow *flow,
         return eth_type_mpls(dl_type);
     case MFP_IP_ANY:
         return is_ip_any(flow);
+    case MFP_NSH:
+        return dl_type == htons(ETH_TYPE_NSH);
     case MFP_CT_VALID:
         return is_ct_valid(flow, mask, wc);
     case MFP_TCP:
@@ -586,6 +607,17 @@ mf_is_value_valid(const struct mf_field *mf, const union mf_value *value)
     case MFF_CT_STATE:
         return !(value->be32 & ~htonl(CS_SUPPORTED_MASK));
 
+    case MFF_NSH_FLAGS:
+    case MFF_NSH_MDTYPE:
+    case MFF_NSH_NP:
+    case MFF_NSH_SPI:
+    case MFF_NSH_SI:
+    case MFF_NSH_C1:
+    case MFF_NSH_C2:
+    case MFF_NSH_C3:
+    case MFF_NSH_C4:
+        return true;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
@@ -863,6 +895,34 @@ mf_get_value(const struct mf_field *mf, const struct flow *flow,
         value->ipv6 = flow->nd_target;
         break;
 
+    case MFF_NSH_FLAGS:
+        value->u8 = flow->nsh.flags;
+        break;
+    case MFF_NSH_MDTYPE:
+        value->u8 = flow->nsh.mdtype;
+        break;
+    case MFF_NSH_NP:
+        value->u8 = flow->nsh.np;
+        break;
+    case MFF_NSH_SPI:
+        value->be32 = flow->nsh.spi;
+        break;
+    case MFF_NSH_SI:
+        value->u8 = flow->nsh.si;
+        break;
+    case MFF_NSH_C1:
+        value->be32 = flow->nsh.c1;
+        break;
+    case MFF_NSH_C2:
+        value->be32 = flow->nsh.c2;
+        break;
+    case MFF_NSH_C3:
+        value->be32 = flow->nsh.c3;
+        break;
+    case MFF_NSH_C4:
+        value->be32 = flow->nsh.c4;
+        break;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
@@ -1156,6 +1216,34 @@ mf_set_value(const struct mf_field *mf,
         match_set_nd_target(match, &value->ipv6);
         break;
 
+    case MFF_NSH_FLAGS:
+        MATCH_SET_FIELD_UINT8(match, nsh.flags, value->u8);
+        break;
+    case MFF_NSH_MDTYPE:
+        MATCH_SET_FIELD_UINT8(match, nsh.mdtype, value->u8);
+        break;
+    case MFF_NSH_NP:
+        MATCH_SET_FIELD_UINT8(match, nsh.np, value->u8);
+        break;
+    case MFF_NSH_SPI:
+        MATCH_SET_FIELD_BE32(match, nsh.spi, value->be32);
+        break;
+    case MFF_NSH_SI:
+        MATCH_SET_FIELD_UINT8(match, nsh.si, value->u8);
+        break;
+    case MFF_NSH_C1:
+        MATCH_SET_FIELD_BE32(match, nsh.c1, value->be32);
+        break;
+    case MFF_NSH_C2:
+        MATCH_SET_FIELD_BE32(match, nsh.c2, value->be32);
+        break;
+    case MFF_NSH_C3:
+        MATCH_SET_FIELD_BE32(match, nsh.c3, value->be32);
+        break;
+    case MFF_NSH_C4:
+        MATCH_SET_FIELD_BE32(match, nsh.c4, value->be32);
+        break;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
@@ -1525,6 +1613,34 @@ mf_set_flow_value(const struct mf_field *mf,
         flow->nd_target = value->ipv6;
         break;
 
+    case MFF_NSH_FLAGS:
+        flow->nsh.flags = value->u8;
+        break;
+    case MFF_NSH_MDTYPE:
+        flow->nsh.mdtype = value->u8;
+        break;
+    case MFF_NSH_NP:
+        flow->nsh.np = value->u8;
+        break;
+    case MFF_NSH_SPI:
+        flow->nsh.spi = value->be32;
+        break;
+    case MFF_NSH_SI:
+        flow->nsh.si = value->u8;
+        break;
+    case MFF_NSH_C1:
+        flow->nsh.c1 = value->be32;
+        break;
+    case MFF_NSH_C2:
+        flow->nsh.c2 = value->be32;
+        break;
+    case MFF_NSH_C3:
+        flow->nsh.c3 = value->be32;
+        break;
+    case MFF_NSH_C4:
+        flow->nsh.c4 = value->be32;
+        break;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
@@ -1651,6 +1767,15 @@ mf_is_pipeline_field(const struct mf_field *mf)
     case MFF_ND_TARGET:
     case MFF_ND_SLL:
     case MFF_ND_TLL:
+    case MFF_NSH_FLAGS:
+    case MFF_NSH_MDTYPE:
+    case MFF_NSH_NP:
+    case MFF_NSH_SPI:
+    case MFF_NSH_SI:
+    case MFF_NSH_C1:
+    case MFF_NSH_C2:
+    case MFF_NSH_C3:
+    case MFF_NSH_C4:
         return false;
 
     case MFF_N_IDS:
@@ -1985,6 +2110,34 @@ mf_set_wild(const struct mf_field *mf, struct match *match, char **err_str)
         memset(&match->flow.nd_target, 0, sizeof match->flow.nd_target);
         break;
 
+    case MFF_NSH_FLAGS:
+        MATCH_SET_FIELD_MASKED(match, nsh.flags, 0, 0);
+        break;
+    case MFF_NSH_MDTYPE:
+        MATCH_SET_FIELD_MASKED(match, nsh.mdtype, 0, 0);
+        break;
+    case MFF_NSH_NP:
+        MATCH_SET_FIELD_MASKED(match, nsh.np, 0, 0);
+        break;
+    case MFF_NSH_SPI:
+        MATCH_SET_FIELD_MASKED(match, nsh.spi, htonl(0), htonl(0));
+        break;
+    case MFF_NSH_SI:
+        MATCH_SET_FIELD_MASKED(match, nsh.si, 0, 0);
+        break;
+    case MFF_NSH_C1:
+        MATCH_SET_FIELD_MASKED(match, nsh.c1, htonl(0), htonl(0));
+        break;
+    case MFF_NSH_C2:
+        MATCH_SET_FIELD_MASKED(match, nsh.c2, htonl(0), htonl(0));
+        break;
+    case MFF_NSH_C3:
+        MATCH_SET_FIELD_MASKED(match, nsh.c3, htonl(0), htonl(0));
+        break;
+    case MFF_NSH_C4:
+        MATCH_SET_FIELD_MASKED(match, nsh.c4, htonl(0), htonl(0));
+        break;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
@@ -2222,6 +2375,34 @@ mf_set(const struct mf_field *mf,
         match_set_tcp_flags_masked(match, value->be16, mask->be16);
         break;
 
+    case MFF_NSH_FLAGS:
+        MATCH_SET_FIELD_MASKED(match, nsh.flags, value->u8, mask->u8);
+        break;
+    case MFF_NSH_MDTYPE:
+        MATCH_SET_FIELD_MASKED(match, nsh.mdtype, value->u8, mask->u8);
+        break;
+    case MFF_NSH_NP:
+        MATCH_SET_FIELD_MASKED(match, nsh.np, value->u8, mask->u8);
+        break;
+    case MFF_NSH_SPI:
+        MATCH_SET_FIELD_MASKED(match, nsh.spi, value->be32, mask->be32);
+        break;
+    case MFF_NSH_SI:
+        MATCH_SET_FIELD_MASKED(match, nsh.si, value->u8, mask->u8);
+        break;
+    case MFF_NSH_C1:
+        MATCH_SET_FIELD_MASKED(match, nsh.c1, value->be32, mask->be32);
+        break;
+    case MFF_NSH_C2:
+        MATCH_SET_FIELD_MASKED(match, nsh.c2, value->be32, mask->be32);
+        break;
+    case MFF_NSH_C3:
+        MATCH_SET_FIELD_MASKED(match, nsh.c3, value->be32, mask->be32);
+        break;
+    case MFF_NSH_C4:
+        MATCH_SET_FIELD_MASKED(match, nsh.c4, value->be32, mask->be32);
+        break;
+
     case MFF_N_IDS:
     default:
         OVS_NOT_REACHED();
diff --git a/lib/meta-flow.xml b/lib/meta-flow.xml
index 634ab69..c6dc0e6 100644
--- a/lib/meta-flow.xml
+++ b/lib/meta-flow.xml
@@ -1292,6 +1292,27 @@ tcp,tp_src=0x07c0/0xfff0
     </field>
   </group>
 
+  <group title="Network Service Header">
+    <field id="MFF_NSH_FLAGS"
+        title="flags field in NSH base header (8 bits)"/>
+    <field id="MFF_NSH_MDTYPE"
+        title="mdtype field in NSH base header (8 bits)"/>
+    <field id="MFF_NSH_NP"
+        title="np (next protocol) field in NSH base header (8 bits)"/>
+    <field id="MFF_NSH_SPI"
+        title="spi (service path identifier) field in NSH base header (24 bits)"/>
+    <field id="MFF_NSH_SI"
+        title="si (service index) field in NSH base header (8 bits)"/>
+    <field id="MFF_NSH_C1"
+        title="c1 (Network Platform Context) field in NSH context header (32 bits)"/>
+    <field id="MFF_NSH_C2"
+        title="c2 (Network Shared Context) field in NSH context header (32 bits)"/>
+    <field id="MFF_NSH_C3"
+        title="c3 (Service Platform Context) field in NSH context header (32 bits)"/>
+    <field id="MFF_NSH_C4"
+        title="c4 (Service Shared Context) field in NSH context header (32 bits)"/>
+  </group>
+
   <group title="Tunnel">
     <p>
       The fields in this group relate to tunnels, which Open vSwitch
diff --git a/lib/nx-match.c b/lib/nx-match.c
index cb0cad8..5d7ab1d 100644
--- a/lib/nx-match.c
+++ b/lib/nx-match.c
@@ -1025,7 +1025,7 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
     int match_len;
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     struct nxm_put_ctx ctx = { .output = b, .implied_ethernet = false };
 
@@ -1154,6 +1154,24 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
                flow->tunnel.gbp_flags, match->wc.masks.tunnel.gbp_flags);
     tun_metadata_to_nx_match(b, oxm, match);
 
+    /* Network Service Header */
+    nxm_put_8m(&ctx, MFF_NSH_FLAGS, oxm, flow->nsh.flags,
+            match->wc.masks.nsh.flags);
+    nxm_put_8m(&ctx, MFF_NSH_MDTYPE, oxm, flow->nsh.mdtype,
+            match->wc.masks.nsh.mdtype);
+    nxm_put_8m(&ctx, MFF_NSH_NP, oxm, flow->nsh.np,
+            match->wc.masks.nsh.np);
+    nxm_put_32m(&ctx, MFF_NSH_SPI, oxm, flow->nsh.spi, match->wc.masks.nsh.spi);
+    nxm_put_8m(&ctx, MFF_NSH_SI, oxm, flow->nsh.si, match->wc.masks.nsh.si);
+    nxm_put_32m(&ctx, MFF_NSH_C1, oxm, flow->nsh.c1,
+            match->wc.masks.nsh.c1);
+    nxm_put_32m(&ctx, MFF_NSH_C2, oxm, flow->nsh.c2,
+            match->wc.masks.nsh.c2);
+    nxm_put_32m(&ctx, MFF_NSH_C3, oxm, flow->nsh.c3,
+            match->wc.masks.nsh.c3);
+    nxm_put_32m(&ctx, MFF_NSH_C4, oxm, flow->nsh.c4,
+            match->wc.masks.nsh.c4);
+
     /* Registers. */
     if (oxm < OFP15_VERSION) {
         for (i = 0; i < FLOW_N_REGS; i++) {
diff --git a/lib/odp-execute.c b/lib/odp-execute.c
index d656334..4eb5f40 100644
--- a/lib/odp-execute.c
+++ b/lib/odp-execute.c
@@ -295,6 +295,8 @@ odp_execute_set_action(struct dp_packet *packet, const struct nlattr *a)
         odp_eth_set_addrs(packet, nl_attr_get(a), NULL);
         break;
 
+    case OVS_KEY_ATTR_NSH:
+        break;
     case OVS_KEY_ATTR_IPV4:
         ipv4_key = nl_attr_get_unspec(a, sizeof(struct ovs_key_ipv4));
         packet_set_ipv4(packet, ipv4_key->ipv4_src,
@@ -419,6 +421,8 @@ odp_execute_masked_set_action(struct dp_packet *packet,
                           get_mask(a, struct ovs_key_ethernet));
         break;
 
+    case OVS_KEY_ATTR_NSH:
+        break;
     case OVS_KEY_ATTR_IPV4:
         odp_set_ipv4(packet, nl_attr_get(a),
                      get_mask(a, struct ovs_key_ipv4));
diff --git a/lib/odp-util.c b/lib/odp-util.c
index d9fee4f..b4427f0 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -175,6 +175,7 @@ ovs_key_attr_to_string(enum ovs_key_attr attr, char *namebuf, size_t bufsize)
     case OVS_KEY_ATTR_DP_HASH: return "dp_hash";
     case OVS_KEY_ATTR_RECIRC_ID: return "recirc_id";
     case OVS_KEY_ATTR_PACKET_TYPE: return "packet_type";
+    case OVS_KEY_ATTR_NSH: return "nsh";
 
     case __OVS_KEY_ATTR_MAX:
     default:
@@ -247,6 +248,95 @@ format_odp_clone_action(struct ds *ds, const struct nlattr *attr,
     ds_put_format(ds, ")");
 }
 
+static void
+format_nsh_key(struct ds *ds, const struct ovs_key_nsh *key)
+{
+    ds_put_format(ds, "flags=%d", key->flags);
+    ds_put_format(ds, ",mdtype=%d", key->mdtype);
+    ds_put_format(ds, ",np=%d", key->np);
+    ds_put_format(ds, ",spi=%d", (ntohl(key->path_hdr) & NSH_SPI_MASK) >> NSH_SPI_SHIFT);
+    ds_put_format(ds, ",si=%d", (ntohl(key->path_hdr) & NSH_SI_MASK) >> NSH_SI_SHIFT);
+
+    switch (key->mdtype) {
+        case NSH_M_TYPE1:
+            ds_put_format(ds, ",c1=0x%08x", ntohl(key->c1));
+            ds_put_format(ds, ",c2=0x%08x", ntohl(key->c2));
+            ds_put_format(ds, ",c3=0x%08x", ntohl(key->c3));
+            ds_put_format(ds, ",c4=0x%08x", ntohl(key->c4));
+            break;
+        case NSH_M_TYPE2:
+            /* TODO */
+            break;
+        default:
+            OVS_NOT_REACHED();
+    }
+}
+
+static void
+format_uint8_masked(struct ds *s, bool *first, const char *name,
+                    uint8_t value, uint8_t mask)
+{
+    if (mask != 0) {
+        if (!*first) {
+            ds_put_char(s, ',');
+        }
+        ds_put_format(s, "%s=", name);
+        if (mask == UINT8_MAX) {
+            ds_put_format(s, "%"PRIu8, value);
+        } else {
+            ds_put_format(s, "0x%02"PRIx8"/0x%02"PRIx8, value, mask);
+        }
+        *first = false;
+    }
+}
+
+static void
+format_be32_masked(struct ds *s, bool *first, const char *name,
+                   ovs_be32 value, ovs_be32 mask)
+{
+    if (mask != htonl(0)) {
+        if (!*first) {
+            ds_put_char(s, ',');
+        }
+        ds_put_format(s, "%s=", name);
+        if (mask == OVS_BE32_MAX) {
+            ds_put_format(s, "0x%08"PRIx32, ntohl(value));
+        } else {
+            ds_put_format(s, "0x%08"PRIx32"/0x%08"PRIx32,
+                          ntohl(value), ntohl(mask));
+        }
+        *first = false;
+    }
+}
+
+static void
+format_nsh_key_mask(struct ds *ds, const struct ovs_key_nsh *key,
+                    const struct ovs_key_nsh *mask)
+{
+    if (!mask) {
+        format_nsh_key(ds, key);
+    } else {
+        bool first = true;
+        uint32_t spi = (ntohl(key->path_hdr) & NSH_SPI_MASK) >> NSH_SPI_SHIFT;
+        uint32_t spi_mask = (ntohl(mask->path_hdr) & NSH_SPI_MASK) >> NSH_SPI_SHIFT;
+        if (spi_mask == 0x00ffffff) {
+            spi_mask = UINT32_MAX;
+        }
+        uint8_t si = (ntohl(key->path_hdr) & NSH_SI_MASK) >> NSH_SI_SHIFT;
+        uint8_t si_mask = (ntohl(mask->path_hdr) & NSH_SI_MASK) >> NSH_SI_SHIFT;
+
+        format_uint8_masked(ds, &first, "flags", key->flags, mask->flags);
+        format_uint8_masked(ds, &first, "mdtype", key->mdtype, mask->mdtype);
+        format_uint8_masked(ds, &first, "np", key->np, mask->np);
+        format_be32_masked(ds, &first, "spi", htonl(spi), htonl(spi_mask));
+        format_uint8_masked(ds, &first, "si", si, si_mask);
+        format_be32_masked(ds, &first, "c1", key->c1, mask->c1);
+        format_be32_masked(ds, &first, "c2", key->c2, mask->c2);
+        format_be32_masked(ds, &first, "c3", key->c3, mask->c3);
+        format_be32_masked(ds, &first, "c4", key->c4, mask->c4);
+    }
+}
+
 static const char *
 slow_path_reason_to_string(uint32_t reason)
 {
@@ -1970,6 +2060,7 @@ static const struct attr_len_tbl ovs_flow_key_attr_lens[OVS_KEY_ATTR_MAX + 1] =
     [OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4] = { .len = sizeof(struct ovs_key_ct_tuple_ipv4) },
     [OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6] = { .len = sizeof(struct ovs_key_ct_tuple_ipv6) },
     [OVS_KEY_ATTR_PACKET_TYPE] = { .len = 4  },
+    [OVS_KEY_ATTR_NSH]       = { .len = sizeof(struct ovs_key_nsh) },
 };
 
 /* Returns the correct length of the payload for a flow key attribute of the
@@ -3144,6 +3235,12 @@ format_odp_key_attr(const struct nlattr *a, const struct nlattr *ma,
         ds_chomp(ds, ',');
         break;
     }
+    case OVS_KEY_ATTR_NSH: {
+        const struct ovs_key_nsh *mask = ma ? nl_attr_get(ma) : NULL;
+        const struct ovs_key_nsh *key = nl_attr_get(a);
+        format_nsh_key_mask(ds, key, mask);
+        break;
+    }
     case OVS_KEY_ATTR_UNSPEC:
     case __OVS_KEY_ATTR_MAX:
     default:
@@ -3542,6 +3639,29 @@ scan_be16(const char *s, ovs_be16 *key, ovs_be16 *mask)
 }
 
 static int
+scan_be32(const char *s, ovs_be32 *key, ovs_be32 *mask)
+{
+    uint32_t key_, mask_;
+    int n;
+
+    if (ovs_scan(s, "%"SCNi32"%n", &key_, &n)) {
+        int len = n;
+
+        *key = htonl(key_);
+        if (mask) {
+            if (ovs_scan(s + len, "/%"SCNi32"%n", &mask_, &n)) {
+                len += n;
+                *mask = htonl(mask_);
+            } else {
+                *mask = OVS_BE32_MAX;
+            }
+        }
+        return len;
+    }
+    return 0;
+}
+
+static int
 scan_be64(const char *s, ovs_be64 *key, ovs_be64 *mask)
 {
     uint64_t key_, mask_;
@@ -4344,6 +4464,17 @@ parse_odp_key_mask_attr(const char *s, const struct simap *port_names,
         SCAN_FIELD("id=", be16, id);
     } SCAN_END(OVS_KEY_ATTR_PACKET_TYPE);
 
+    SCAN_BEGIN("nsh(", struct ovs_key_nsh) {
+        SCAN_FIELD("flags=", u8, flags);
+        SCAN_FIELD("mdtype=", u8, mdtype);
+        SCAN_FIELD("np=", u8, np);
+        SCAN_FIELD("path_hdr=", be32, path_hdr);
+        SCAN_FIELD("c1=", be32, c1);
+        SCAN_FIELD("c2=", be32, c2);
+        SCAN_FIELD("c3=", be32, c3);
+        SCAN_FIELD("c4=", be32, c4);
+    } SCAN_END(OVS_KEY_ATTR_NSH);
+
     /* Encap open-coded. */
     if (!strncmp(s, "encap(", 6)) {
         const char *start = s;
@@ -4452,6 +4583,10 @@ static void get_arp_key(const struct flow *, struct ovs_key_arp *);
 static void put_arp_key(const struct ovs_key_arp *, struct flow *);
 static void get_nd_key(const struct flow *, struct ovs_key_nd *);
 static void put_nd_key(const struct ovs_key_nd *, struct flow *);
+static void get_nsh_key(const struct flow *flow, struct ovs_key_nsh *nsh,
+                        bool is_mask);
+static void put_nsh_key(const struct ovs_key_nsh *nsh, struct flow *flow,
+                        bool is_mask);
 
 /* These share the same layout. */
 union ovs_key_tp {
@@ -4626,6 +4761,12 @@ odp_flow_key_from_flow__(const struct odp_flow_key_parms *parms,
         for (i = 0; i < n; i++) {
             mpls_key[i].mpls_lse = data->mpls_lse[i];
         }
+    } else if (flow->dl_type == htons(ETH_TYPE_NSH)) {
+        struct ovs_key_nsh *nsh_key;
+
+        nsh_key = nl_msg_put_unspec_uninit(buf, OVS_KEY_ATTR_NSH,
+                                            sizeof *nsh_key);
+        get_nsh_key(data, nsh_key, export_mask);
     }
 
     if (is_ip_any(flow) && !(flow->nw_frag & FLOW_NW_FRAG_LATER)) {
@@ -5186,6 +5327,21 @@ parse_l2_5_onward(const struct nlattr *attrs[OVS_KEY_ATTR_MAX + 1],
                 expected_bit = OVS_KEY_ATTR_ARP;
             }
         }
+    } else if (src_flow->dl_type == htons(ETH_TYPE_NSH)) {
+        if (!is_mask) {
+            expected_attrs |= UINT64_C(1) << OVS_KEY_ATTR_NSH;
+        }
+        if (present_attrs & (UINT64_C(1) << OVS_KEY_ATTR_NSH)) {
+            const struct ovs_key_nsh *nsh_key;
+
+            nsh_key = nl_attr_get(attrs[OVS_KEY_ATTR_NSH]);
+            put_nsh_key(nsh_key, flow, false);
+            if (is_mask) {
+                check_start = nsh_key;
+                check_len = sizeof *nsh_key;
+                expected_bit = OVS_KEY_ATTR_NSH;
+            }
+        }
     } else {
         goto done;
     }
@@ -6227,6 +6383,58 @@ commit_set_nw_action(const struct flow *flow, struct flow *base,
     return 0;
 }
 
+static void
+get_nsh_key(const struct flow *flow, struct ovs_key_nsh *nsh, bool is_mask)
+{
+    nsh->flags = flow->nsh.flags;
+    nsh->mdtype = flow->nsh.mdtype;
+    nsh->np = flow->nsh.np;
+    nsh->path_hdr = htonl(ntohl(flow->nsh.spi) << NSH_SPI_SHIFT | flow->nsh.si);
+    if (is_mask) {
+        nsh->c1 = flow->nsh.c1;
+        nsh->c2 = flow->nsh.c2;
+        nsh->c3 = flow->nsh.c3;
+        nsh->c4 = flow->nsh.c4;
+    } else {
+        switch (nsh->mdtype) {
+        case NSH_M_TYPE1:
+            nsh->c1 = flow->nsh.c1;
+            nsh->c2 = flow->nsh.c2;
+            nsh->c3 = flow->nsh.c3;
+            nsh->c4 = flow->nsh.c4;
+            break;
+        case NSH_M_TYPE2:
+            /* TODO: MD type 2 */
+            break;
+        }
+    }
+}
+
+static void
+put_nsh_key(const struct ovs_key_nsh *nsh, struct flow *flow, bool is_mask OVS_UNUSED)
+{
+    flow->nsh.flags = nsh->flags;
+    flow->nsh.mdtype = nsh->mdtype;
+    flow->nsh.np = nsh->np;
+    flow->nsh.spi = htonl((ntohl(nsh->path_hdr) & NSH_SPI_MASK) >> NSH_SPI_SHIFT);
+    flow->nsh.si = (ntohl(nsh->path_hdr) & NSH_SI_MASK) >> NSH_SI_SHIFT;
+    switch (nsh->mdtype) {
+        case NSH_M_TYPE1:
+            flow->nsh.c1 = nsh->c1;
+            flow->nsh.c2 = nsh->c2;
+            flow->nsh.c3 = nsh->c3;
+            flow->nsh.c4 = nsh->c4;
+            break;
+        case NSH_M_TYPE2:
+            /* TODO: MD type 2 */
+            flow->nsh.c1 = 0;
+            flow->nsh.c2 = 0;
+            flow->nsh.c3 = 0;
+            flow->nsh.c4 = 0;
+            break;
+    }
+}
+
 /* TCP, UDP, and SCTP keys have the same layout. */
 BUILD_ASSERT_DECL(sizeof(struct ovs_key_tcp) == sizeof(struct ovs_key_udp) &&
                   sizeof(struct ovs_key_tcp) == sizeof(struct ovs_key_sctp));
diff --git a/lib/odp-util.h b/lib/odp-util.h
index f01c069..ed16f96 100644
--- a/lib/odp-util.h
+++ b/lib/odp-util.h
@@ -146,7 +146,7 @@ void odp_portno_name_format(const struct hmap *portno_names,
  * add another field and forget to adjust this value.
  */
 #define ODPUTIL_FLOW_KEY_BYTES 640
-BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
 /* A buffer with sufficient size and alignment to hold an nlattr-formatted flow
  * key.  An array of "struct nlattr" might not, in theory, be sufficiently
diff --git a/lib/ofp-util.c b/lib/ofp-util.c
index 0c768ae..e30acf1 100644
--- a/lib/ofp-util.c
+++ b/lib/ofp-util.c
@@ -102,7 +102,7 @@ ofputil_netmask_to_wcbits(ovs_be32 netmask)
 void
 ofputil_wildcard_from_ofpfw10(uint32_t ofpfw, struct flow_wildcards *wc)
 {
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
     /* Initialize most of wc. */
     flow_wildcards_init_catchall(wc);
diff --git a/lib/packets.h b/lib/packets.h
index 8287ca3..9a85675 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -25,6 +25,7 @@
 #include "openvswitch/geneve.h"
 #include "openvswitch/packets.h"
 #include "openvswitch/types.h"
+#include "openvswitch/nsh.h"
 #include "odp-netlink.h"
 #include "random.h"
 #include "hash.h"
@@ -370,6 +371,7 @@ ovs_be32 set_mpls_lse_values(uint8_t ttl, uint8_t tc, uint8_t bos,
 #define ETH_TYPE_RARP          0x8035
 #define ETH_TYPE_MPLS          0x8847
 #define ETH_TYPE_MPLS_MCAST    0x8848
+#define ETH_TYPE_NSH           0x894f
 
 static inline bool eth_type_mpls(ovs_be16 eth_type)
 {
@@ -1271,6 +1273,7 @@ enum packet_type {
     PT_IPV6 = PACKET_TYPE(OFPHTN_ETHERTYPE, ETH_TYPE_IPV6),
     PT_MPLS = PACKET_TYPE(OFPHTN_ETHERTYPE, ETH_TYPE_MPLS),
     PT_MPLS_MC = PACKET_TYPE(OFPHTN_ETHERTYPE, ETH_TYPE_MPLS_MCAST),
+    PT_NSH  = PACKET_TYPE(OFPHTN_ETHERTYPE, ETH_TYPE_NSH),
     PT_UNKNOWN = PACKET_TYPE(0xffff, 0xffff),   /* Unknown packet type. */
 };
 
diff --git a/ofproto/ofproto-dpif-rid.h b/ofproto/ofproto-dpif-rid.h
index ab9b1b7..14b346d 100644
--- a/ofproto/ofproto-dpif-rid.h
+++ b/ofproto/ofproto-dpif-rid.h
@@ -99,7 +99,7 @@ struct rule;
 /* Metadata for restoring pipeline context after recirculation.  Helpers
  * are inlined below to keep them together with the definition for easier
  * updates. */
-BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
 
 struct frozen_metadata {
     /* Metadata in struct flow. */
diff --git a/ofproto/ofproto-dpif-sflow.c b/ofproto/ofproto-dpif-sflow.c
index fc665a6..e4cca65 100644
--- a/ofproto/ofproto-dpif-sflow.c
+++ b/ofproto/ofproto-dpif-sflow.c
@@ -1050,6 +1050,7 @@ sflow_read_set_action(const struct nlattr *attr,
     case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6:
     case OVS_KEY_ATTR_UNSPEC:
     case OVS_KEY_ATTR_PACKET_TYPE:
+    case OVS_KEY_ATTR_NSH:
     case __OVS_KEY_ATTR_MAX:
     default:
         break;
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index a157fc9..cdab421 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3362,7 +3362,7 @@ compose_output_action__(struct xlate_ctx *ctx, ofp_port_t ofp_port,
 
     /* If 'struct flow' gets additional metadata, we'll need to zero it out
      * before traversing a patch port. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 39);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 40);
     memset(&flow_tnl, 0, sizeof flow_tnl);
 
     if (!check_output_prerequisites(ctx, xport, flow, check_stp)) {
diff --git a/tests/ofproto.at b/tests/ofproto.at
index 9e6acfa..e12cf43 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -2386,7 +2386,7 @@ head_table () {
         actions: output group set_field strip_vlan push_vlan mod_nw_ttl dec_ttl set_mpls_ttl dec_mpls_ttl push_mpls pop_mpls set_queue
         supported on Set-Field: tun_id tun_src tun_dst tun_ipv6_src tun_ipv6_dst tun_flags tun_gbp_id tun_gbp_flags tun_metadata0 dnl
 tun_metadata1 tun_metadata2 tun_metadata3 tun_metadata4 tun_metadata5 tun_metadata6 tun_metadata7 tun_metadata8 tun_metadata9 tun_metadata10 tun_metadata11 tun_metadata12 tun_metadata13 tun_metadata14 tun_metadata15 tun_metadata16 tun_metadata17 tun_metadata18 tun_metadata19 tun_metadata20 tun_metadata21 tun_metadata22 tun_metadata23 tun_metadata24 tun_metadata25 tun_metadata26 tun_metadata27 tun_metadata28 tun_metadata29 tun_metadata30 tun_metadata31 tun_metadata32 tun_metadata33 tun_metadata34 tun_metadata35 tun_metadata36 tun_metadata37 tun_metadata38 tun_metadata39 tun_metadata40 tun_metadata41 tun_metadata42 tun_metadata43 tun_metadata44 tun_metadata45 tun_metadata46 tun_metadata47 tun_metadata48 tun_metadata49 tun_metadata50 tun_metadata51 tun_metadata52 tun_metadata53 tun_metadata54 tun_metadata55 tun_metadata56 tun_metadata57 tun_metadata58 tun_metadata59 tun_metadata60 tun_metadata61 tun_metadata62 tun_metadata63 dnl
-metadata in_port in_port_oxm pkt_mark ct_mark ct_label reg0 reg1 reg2 reg3 reg4 reg5 reg6 reg7 reg8 reg9 reg10 reg11 reg12 reg13 reg14 reg15 xreg0 xreg1 xreg2 xreg3 xreg4 xreg5 xreg6 xreg7 xxreg0 xxreg1 xxreg2 xxreg3 eth_src eth_dst vlan_tci vlan_vid vlan_pcp mpls_label mpls_tc mpls_ttl ip_src ip_dst ipv6_src ipv6_dst ipv6_label nw_tos ip_dscp nw_ecn nw_ttl arp_op arp_spa arp_tpa arp_sha arp_tha tcp_src tcp_dst udp_src udp_dst sctp_src sctp_dst icmp_type icmp_code icmpv6_type icmpv6_code nd_target nd_sll nd_tll
+metadata in_port in_port_oxm pkt_mark ct_mark ct_label reg0 reg1 reg2 reg3 reg4 reg5 reg6 reg7 reg8 reg9 reg10 reg11 reg12 reg13 reg14 reg15 xreg0 xreg1 xreg2 xreg3 xreg4 xreg5 xreg6 xreg7 xxreg0 xxreg1 xxreg2 xxreg3 eth_src eth_dst vlan_tci vlan_vid vlan_pcp mpls_label mpls_tc mpls_ttl ip_src ip_dst ipv6_src ipv6_dst ipv6_label nw_tos ip_dscp nw_ecn nw_ttl arp_op arp_spa arp_tpa arp_sha arp_tha tcp_src tcp_dst udp_src udp_dst sctp_src sctp_dst icmp_type icmp_code icmpv6_type icmpv6_code nd_target nd_sll nd_tll nsh_flags nsh_spi nsh_si nsh_c1 nsh_c2 nsh_c3 nsh_c4
     matching:
       dp_hash: arbitrary mask
       recirc_id: exact match or wildcard
@@ -2548,6 +2548,15 @@ metadata in_port in_port_oxm pkt_mark ct_mark ct_label reg0 reg1 reg2 reg3 reg4
       nd_target: arbitrary mask
       nd_sll: arbitrary mask
       nd_tll: arbitrary mask
+      nsh_flags: arbitrary mask
+      nsh_mdtype: exact match or wildcard
+      nsh_np: exact match or wildcard
+      nsh_spi: exact match or wildcard
+      nsh_si: exact match or wildcard
+      nsh_c1: arbitrary mask
+      nsh_c2: arbitrary mask
+      nsh_c3: arbitrary mask
+      nsh_c4: arbitrary mask
 
 ' $1
 }
-- 
2.1.0



More information about the dev mailing list