diff --git a/Documentation/faq/configuration.rst b/Documentation/faq/configuration.rst
index ff3b71a5d4ef..4a98740c5d4d 100644
--- a/Documentation/faq/configuration.rst
+++ b/Documentation/faq/configuration.rst
@@ -225,6 +225,19 @@ Q: Does Open vSwitch support IPv6 GRE?
                 options:remote_ip=fc00:100::1 \
                 options:packet_type=legacy_l2
 
+Q: Does Open vSwitch support GTP-U?
+
+    A: Yes. Starting with version 2.13, the Open vSwitch userspace
+    datapath supports GTP-U (GPRS Tunnelling Protocol User Plane
+    (GTPv1-U)). TEID is set by using tunnel key field.
+
+    ::
+
+        $ ovs-vsctl add-br br0
+        $ ovs-vsctl add-port br0 gtpu0 -- \
+                set int gtpu0 type=gtpu options:key=<teid> \
+                options:remote_ip=172.31.1.1
+
 Q: How do I connect two bridges?
 
     A: First, why do you want to do this?  Two connected bridges are not much
diff --git a/Documentation/faq/releases.rst b/Documentation/faq/releases.rst
index 748540b91634..b3507bd1c7fa 100644
--- a/Documentation/faq/releases.rst
+++ b/Documentation/faq/releases.rst
@@ -131,6 +131,7 @@ Q: Are all features available with all datapaths?
     Tunnel - Geneve-IPv6            4.4            2.6          2.6      NO
     Tunnel - ERSPAN                 4.18           2.10         2.10     NO
     Tunnel - ERSPAN-IPv6            4.18           2.10         2.10     NO
+    Tunnel - GTP-U                  NO             NO           2.13     NO
     QoS - Policing                  YES            1.1          2.6      NO
     QoS - Shaping                   YES            1.1          NO       NO
     sFlow                           YES            1.0          1.0      NO
diff --git a/NEWS b/NEWS
index 32ca2e0c6642..70bd17584594 100644
--- a/NEWS
+++ b/NEWS
@@ -57,6 +57,9 @@ v2.13.0 - 14 Feb 2020
    - 'ovs-appctl dpctl/dump-flows' can now show offloaded=partial for
      partially offloaded flows, dp:dpdk for fully offloaded by dpdk, and
      type filter supports new filters: "dpdk" and "partially-offloaded".
+   - GTP-U Tunnel Protocol
+     * Add two new fields: tun_gtpu_flags, tun_gtpu_msgtype.
+     * Only support for userspace datapath.
 
 v2.12.0 - 03 Sep 2019
 ---------------------
diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index 2f0c6559eaf5..f7c3b2e99a78 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -245,6 +245,7 @@ enum ovs_vport_type {
 	OVS_VPORT_TYPE_ERSPAN = 107, /* ERSPAN tunnel. */
 	OVS_VPORT_TYPE_IP6ERSPAN = 108, /* ERSPAN tunnel. */
 	OVS_VPORT_TYPE_IP6GRE = 109,
+	OVS_VPORT_TYPE_GTPU = 110,
 	__OVS_VPORT_TYPE_MAX
 };
 
@@ -404,6 +405,7 @@ enum ovs_tunnel_key_attr {
 	OVS_TUNNEL_KEY_ATTR_IPV6_DST,		/* struct in6_addr dst IPv6 address. */
 	OVS_TUNNEL_KEY_ATTR_PAD,
 	OVS_TUNNEL_KEY_ATTR_ERSPAN_OPTS,	/* struct erspan_metadata */
+	OVS_TUNNEL_KEY_ATTR_GTPU_OPTS,		/* struct gtpu_metadata */
 	__OVS_TUNNEL_KEY_ATTR_MAX
 };
 
diff --git a/include/openvswitch/flow.h b/include/openvswitch/flow.h
index 57b6c925c7bc..3054015d93c7 100644
--- a/include/openvswitch/flow.h
+++ b/include/openvswitch/flow.h
@@ -27,7 +27,7 @@ extern "C" {
 /* 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 41
+#define FLOW_WC_SEQ 42
 
 /* Number of Open vSwitch extension 32-bit registers. */
 #define FLOW_N_REGS 16
@@ -168,7 +168,7 @@ BUILD_ASSERT_DECL(sizeof(struct ovs_key_nsh) % sizeof(uint64_t) == 0);
 /* 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) + sizeof(struct ovs_key_nsh) + 300
-                  && FLOW_WC_SEQ == 41);
+                  && FLOW_WC_SEQ == 42);
 
 /* Incremental points at which flow classification may be performed in
  * segments.
diff --git a/include/openvswitch/match.h b/include/openvswitch/match.h
index eeabd5f470a7..8af3b74ed3e0 100644
--- a/include/openvswitch/match.h
+++ b/include/openvswitch/match.h
@@ -121,6 +121,12 @@ void match_set_tun_erspan_dir_masked(struct match *match, uint8_t dir,
 void match_set_tun_erspan_hwid(struct match *match, uint8_t hwid);
 void match_set_tun_erspan_hwid_masked(struct match *match, uint8_t hwid,
                                       uint8_t mask);
+void match_set_tun_gtpu_flags(struct match *match, uint8_t flags);
+void match_set_tun_gtpu_flags_masked(struct match *match, uint8_t flags,
+                                     uint8_t mask);
+void match_set_tun_gtpu_msgtype(struct match *match, uint8_t msgtype);
+void match_set_tun_gtpu_msgtype_masked(struct match *match, uint8_t msgtype,
+                                       uint8_t mask);
 void match_set_in_port(struct match *, ofp_port_t ofp_port);
 void match_set_pkt_mark(struct match *, uint32_t pkt_mark);
 void match_set_pkt_mark_masked(struct match *, uint32_t pkt_mark, uint32_t mask);
diff --git a/include/openvswitch/meta-flow.h b/include/openvswitch/meta-flow.h
index 1f81d830e70f..d529a9f0d21c 100644
--- a/include/openvswitch/meta-flow.h
+++ b/include/openvswitch/meta-flow.h
@@ -506,6 +506,34 @@ enum OVS_PACKED_ENUM mf_field_id {
      */
     MFF_TUN_ERSPAN_HWID,
 
+    /* "tun_gtpu_flags".
+     *
+     * GTP-U tunnel flags.
+     *
+     * Type: u8.
+     * Maskable: bitwise.
+     * Formatting: hexadecimal.
+     * Prerequisites: none.
+     * Access: read-only.
+     * NXM: none.
+     * OXM: NXOXM_ET_GTPU_FLAGS(15) since v2.13.
+     */
+    MFF_TUN_GTPU_FLAGS,
+
+    /* "tun_gtpu_msgtype".
+     *
+     * GTP-U tunnel message type.
+     *
+     * Type: u8.
+     * Maskable: bitwise.
+     * Formatting: decimal.
+     * Prerequisites: none.
+     * Access: read-only.
+     * NXM: none.
+     * OXM: NXOXM_ET_GTPU_MSGTYPE(16) since v2.13.
+     */
+    MFF_TUN_GTPU_MSGTYPE,
+
 #if TUN_METADATA_NUM_OPTS == 64
     /* "tun_metadata<N>".
      *
diff --git a/include/openvswitch/packets.h b/include/openvswitch/packets.h
index 925844edae6a..a65cb0d04e77 100644
--- a/include/openvswitch/packets.h
+++ b/include/openvswitch/packets.h
@@ -43,7 +43,9 @@ struct flow_tnl {
     uint32_t erspan_idx;
     uint8_t erspan_dir;
     uint8_t erspan_hwid;
-    uint8_t pad1[6];     /* Pad to 64 bits. */
+    uint8_t gtpu_flags;
+    uint8_t gtpu_msgtype;
+    uint8_t pad1[4];     /* Pad to 64 bits. */
     struct tun_metadata metadata;
 };
 
diff --git a/lib/dpif-netlink-rtnl.c b/lib/dpif-netlink-rtnl.c
index 582274c46774..fd157ce2d918 100644
--- a/lib/dpif-netlink-rtnl.c
+++ b/lib/dpif-netlink-rtnl.c
@@ -111,6 +111,8 @@ vport_type_to_kind(enum ovs_vport_type type,
         } else {
             return NULL;
         }
+    case OVS_VPORT_TYPE_GTPU:
+        return NULL;
     case OVS_VPORT_TYPE_NETDEV:
     case OVS_VPORT_TYPE_INTERNAL:
     case OVS_VPORT_TYPE_LISP:
@@ -277,6 +279,7 @@ dpif_netlink_rtnl_verify(const struct netdev_tunnel_config *tnl_cfg,
     case OVS_VPORT_TYPE_INTERNAL:
     case OVS_VPORT_TYPE_LISP:
     case OVS_VPORT_TYPE_STT:
+    case OVS_VPORT_TYPE_GTPU:
     case OVS_VPORT_TYPE_UNSPEC:
     case __OVS_VPORT_TYPE_MAX:
     default:
@@ -358,6 +361,7 @@ dpif_netlink_rtnl_create(const struct netdev_tunnel_config *tnl_cfg,
     case OVS_VPORT_TYPE_INTERNAL:
     case OVS_VPORT_TYPE_LISP:
     case OVS_VPORT_TYPE_STT:
+    case OVS_VPORT_TYPE_GTPU:
     case OVS_VPORT_TYPE_UNSPEC:
     case __OVS_VPORT_TYPE_MAX:
     default:
@@ -471,6 +475,7 @@ dpif_netlink_rtnl_port_destroy(const char *name, const char *type)
     case OVS_VPORT_TYPE_INTERNAL:
     case OVS_VPORT_TYPE_LISP:
     case OVS_VPORT_TYPE_STT:
+    case OVS_VPORT_TYPE_GTPU:
     case OVS_VPORT_TYPE_UNSPEC:
     case __OVS_VPORT_TYPE_MAX:
     default:
diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index 6d9427b57c7e..dc642100fc58 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -745,6 +745,9 @@ get_vport_type(const struct dpif_netlink_vport *vport)
     case OVS_VPORT_TYPE_IP6GRE:
         return "ip6gre";
 
+    case OVS_VPORT_TYPE_GTPU:
+        return "gtpu";
+
     case OVS_VPORT_TYPE_UNSPEC:
     case __OVS_VPORT_TYPE_MAX:
         break;
@@ -778,6 +781,8 @@ netdev_to_ovs_vport_type(const char *type)
         return OVS_VPORT_TYPE_IP6GRE;
     } else if (!strcmp(type, "gre")) {
         return OVS_VPORT_TYPE_GRE;
+    } else if (!strcmp(type, "gtpu")) {
+        return OVS_VPORT_TYPE_GTPU;
     } else {
         return OVS_VPORT_TYPE_UNSPEC;
     }
diff --git a/lib/flow.c b/lib/flow.c
index 5c32b4a01053..cc1b3f2db25f 100644
--- a/lib/flow.c
+++ b/lib/flow.c
@@ -129,7 +129,7 @@ struct mf_ctx {
  * away.  Some GCC versions gave warnings on ALWAYS_INLINE, so these are
  * defined as macros. */
 
-#if (FLOW_WC_SEQ != 41)
+#if (FLOW_WC_SEQ != 42)
 #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 "
@@ -731,7 +731,7 @@ void
 miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
 {
     /* Add code to this function (or its callees) to extract new fields. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     const struct pkt_metadata *md = &packet->md;
     const void *data = dp_packet_data(packet);
@@ -1187,7 +1187,7 @@ flow_get_metadata(const struct flow *flow, struct match *flow_metadata)
 {
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     match_init_catchall(flow_metadata);
     if (flow->tunnel.tun_id != htonll(0)) {
@@ -1227,6 +1227,12 @@ flow_get_metadata(const struct flow *flow, struct match *flow_metadata)
     if (flow->tunnel.erspan_hwid) {
         match_set_tun_erspan_hwid(flow_metadata, flow->tunnel.erspan_hwid);
     }
+    if (flow->tunnel.gtpu_flags) {
+        match_set_tun_gtpu_flags(flow_metadata, flow->tunnel.gtpu_flags);
+    }
+    if (flow->tunnel.gtpu_msgtype) {
+        match_set_tun_gtpu_msgtype(flow_metadata, flow->tunnel.gtpu_msgtype);
+    }
     tun_metadata_get_fmd(&flow->tunnel, flow_metadata);
     if (flow->metadata != htonll(0)) {
         match_set_metadata(flow_metadata, flow->metadata);
@@ -1767,7 +1773,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     if (flow_tnl_dst_is_set(&flow->tunnel)) {
         if (flow->tunnel.flags & FLOW_TNL_F_KEY) {
@@ -1788,6 +1794,8 @@ flow_wildcards_init_for_packet(struct flow_wildcards *wc,
         WC_MASK_FIELD(wc, tunnel.erspan_idx);
         WC_MASK_FIELD(wc, tunnel.erspan_dir);
         WC_MASK_FIELD(wc, tunnel.erspan_hwid);
+        WC_MASK_FIELD(wc, tunnel.gtpu_flags);
+        WC_MASK_FIELD(wc, tunnel.gtpu_msgtype);
 
         if (!(flow->tunnel.flags & FLOW_TNL_F_UDPIF)) {
             if (flow->tunnel.metadata.present.map) {
@@ -1918,7 +1926,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     flowmap_init(map);
 
@@ -2021,7 +2029,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     memset(&wc->masks.metadata, 0, sizeof wc->masks.metadata);
     memset(&wc->masks.regs, 0, sizeof wc->masks.regs);
@@ -2165,7 +2173,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
     uint32_t hash = basis;
 
     if (flow) {
@@ -2212,7 +2220,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
     uint32_t hash = basis;
 
     if (flow) {
@@ -2890,7 +2898,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 == 41);
+            BUILD_ASSERT(FLOW_WC_SEQ == 42);
             memset((char *) flow + FLOW_SEGMENT_2_ENDS_AT, 0,
                    sizeof(struct flow) - FLOW_SEGMENT_2_ENDS_AT);
             flow->dp_hash = 0;
@@ -3188,7 +3196,7 @@ flow_compose(struct dp_packet *p, const struct flow *flow,
     /* Add code to this function (or its callees) for emitting new fields or
      * protocols.  (This isn't essential, so it can be skipped for initial
      * testing.) */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     uint32_t pseudo_hdr_csum;
     size_t l4_len;
diff --git a/lib/flow.h b/lib/flow.h
index 75751763c81a..b32f0b27754a 100644
--- a/lib/flow.h
+++ b/lib/flow.h
@@ -964,7 +964,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     md->recirc_id = flow->recirc_id;
     md->dp_hash = flow->dp_hash;
diff --git a/lib/match.c b/lib/match.c
index 0d1ec31ef843..25c277cc670b 100644
--- a/lib/match.c
+++ b/lib/match.c
@@ -375,6 +375,34 @@ match_set_tun_erspan_hwid(struct match *match, uint8_t hwid)
 }
 
 void
+match_set_tun_gtpu_flags_masked(struct match *match, uint8_t flags,
+                                uint8_t mask)
+{
+    match->wc.masks.tunnel.gtpu_flags = flags;
+    match->flow.tunnel.gtpu_flags = flags & mask;
+}
+
+void
+match_set_tun_gtpu_flags(struct match *match, uint8_t flags)
+{
+    match_set_tun_gtpu_flags_masked(match, flags, UINT8_MAX);
+}
+
+void
+match_set_tun_gtpu_msgtype_masked(struct match *match, uint8_t msgtype,
+                                  uint8_t mask)
+{
+    match->wc.masks.tunnel.gtpu_msgtype = msgtype;
+    match->flow.tunnel.gtpu_msgtype = msgtype & mask;
+}
+
+void
+match_set_tun_gtpu_msgtype(struct match *match, uint8_t msgtype)
+{
+    match_set_tun_gtpu_msgtype_masked(match, msgtype, UINT8_MAX);
+}
+
+void
 match_set_in_port(struct match *match, ofp_port_t ofp_port)
 {
     match->wc.masks.in_port.ofp_port = u16_to_ofp(UINT16_MAX);
@@ -1325,6 +1353,12 @@ format_flow_tunnel(struct ds *s, const struct match *match)
     if (wc->masks.tunnel.erspan_hwid && tnl->erspan_ver == 2) {
         ds_put_format(s, "tun_erspan_hwid=%#"PRIx8",", tnl->erspan_hwid);
     }
+    if (wc->masks.tunnel.gtpu_flags) {
+        ds_put_format(s, "gtpu_flags=%#"PRIx8",", tnl->gtpu_flags);
+    }
+    if (wc->masks.tunnel.gtpu_msgtype) {
+        ds_put_format(s, "gtpu_msgtype=%"PRIu8",", tnl->gtpu_msgtype);
+    }
     if (wc->masks.tunnel.flags & FLOW_TNL_F_MASK) {
         format_flags_masked(s, "tun_flags", flow_tun_flag_to_string,
                             tnl->flags & FLOW_TNL_F_MASK,
@@ -1396,7 +1430,7 @@ match_format(const struct match *match,
     bool is_megaflow = false;
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     if (priority != OFP_DEFAULT_PRIORITY) {
         ds_put_format(s, "%spriority=%s%d,",
diff --git a/lib/meta-flow.c b/lib/meta-flow.c
index 8b62e6d96835..9ab82460bfc4 100644
--- a/lib/meta-flow.c
+++ b/lib/meta-flow.c
@@ -391,6 +391,10 @@ mf_is_all_wild(const struct mf_field *mf, const struct flow_wildcards *wc)
     case MFF_NSH_C3:
     case MFF_NSH_C4:
         return !wc->masks.nsh.context[mf->id - MFF_NSH_C1];
+    case MFF_TUN_GTPU_FLAGS:
+        return !wc->masks.tunnel.gtpu_flags;
+    case MFF_TUN_GTPU_MSGTYPE:
+        return !wc->masks.tunnel.gtpu_msgtype;
 
     case MFF_N_IDS:
     default:
@@ -530,6 +534,8 @@ mf_is_value_valid(const struct mf_field *mf, const union mf_value *value)
     case MFF_TUN_ERSPAN_VER:
     case MFF_TUN_ERSPAN_DIR:
     case MFF_TUN_ERSPAN_HWID:
+    case MFF_TUN_GTPU_FLAGS:
+    case MFF_TUN_GTPU_MSGTYPE:
     CASE_MFF_TUN_METADATA:
     case MFF_METADATA:
     case MFF_IN_PORT:
@@ -711,6 +717,12 @@ mf_get_value(const struct mf_field *mf, const struct flow *flow,
     case MFF_TUN_ERSPAN_HWID:
         value->u8 = flow->tunnel.erspan_hwid;
         break;
+    case MFF_TUN_GTPU_FLAGS:
+        value->u8 = flow->tunnel.gtpu_flags;
+        break;
+    case MFF_TUN_GTPU_MSGTYPE:
+        value->u8 = flow->tunnel.gtpu_msgtype;
+        break;
     CASE_MFF_TUN_METADATA:
         tun_metadata_read(&flow->tunnel, mf, value);
         break;
@@ -1042,6 +1054,12 @@ mf_set_value(const struct mf_field *mf,
     case MFF_TUN_ERSPAN_HWID:
         match_set_tun_erspan_hwid(match, value->u8);
         break;
+    case MFF_TUN_GTPU_FLAGS:
+        match_set_tun_gtpu_flags(match, value->u8);
+        break;
+    case MFF_TUN_GTPU_MSGTYPE:
+        match_set_tun_gtpu_msgtype(match, value->u8);
+        break;
     CASE_MFF_TUN_METADATA:
         tun_metadata_set_match(mf, value, NULL, match, err_str);
         break;
@@ -1459,6 +1477,12 @@ mf_set_flow_value(const struct mf_field *mf,
     case MFF_TUN_ERSPAN_HWID:
         flow->tunnel.erspan_hwid = value->u8;
         break;
+    case MFF_TUN_GTPU_FLAGS:
+        flow->tunnel.gtpu_flags = value->u8;
+        break;
+    case MFF_TUN_GTPU_MSGTYPE:
+        flow->tunnel.gtpu_msgtype = value->u8;
+        break;
     CASE_MFF_TUN_METADATA:
         tun_metadata_write(&flow->tunnel, mf, value);
         break;
@@ -1780,6 +1804,8 @@ mf_is_pipeline_field(const struct mf_field *mf)
     case MFF_TUN_ERSPAN_IDX:
     case MFF_TUN_ERSPAN_DIR:
     case MFF_TUN_ERSPAN_HWID:
+    case MFF_TUN_GTPU_FLAGS:
+    case MFF_TUN_GTPU_MSGTYPE:
     CASE_MFF_TUN_METADATA:
     case MFF_METADATA:
     case MFF_IN_PORT:
@@ -1970,6 +1996,12 @@ mf_set_wild(const struct mf_field *mf, struct match *match, char **err_str)
     case MFF_TUN_ERSPAN_HWID:
         match_set_tun_erspan_hwid_masked(match, 0, 0);
         break;
+    case MFF_TUN_GTPU_FLAGS:
+        match_set_tun_gtpu_flags_masked(match, 0, 0);
+        break;
+    case MFF_TUN_GTPU_MSGTYPE:
+        match_set_tun_gtpu_msgtype_masked(match, 0, 0);
+        break;
     CASE_MFF_TUN_METADATA:
         tun_metadata_set_match(mf, NULL, NULL, match, err_str);
         break;
@@ -2377,6 +2409,12 @@ mf_set(const struct mf_field *mf,
     case MFF_TUN_ERSPAN_HWID:
         match_set_tun_erspan_hwid_masked(match, value->u8, mask->u8);
         break;
+    case MFF_TUN_GTPU_FLAGS:
+        match_set_tun_gtpu_flags_masked(match, value->u8, mask->u8);
+        break;
+    case MFF_TUN_GTPU_MSGTYPE:
+        match_set_tun_gtpu_msgtype_masked(match, value->u8, mask->u8);
+        break;
     CASE_MFF_TUN_METADATA:
         tun_metadata_set_match(mf, value, mask, match, err_str);
         break;
diff --git a/lib/meta-flow.xml b/lib/meta-flow.xml
index 2f9c5ee16342..d4495552b3c2 100644
--- a/lib/meta-flow.xml
+++ b/lib/meta-flow.xml
@@ -1456,7 +1456,8 @@ ovs-ofctl add-flow br-int 'in_port=3,tun_src=192.168.1.1,tun_id=5001 actions=1'
         <li>LISP has a 24-bit instance ID.</li>
         <li>GRE has an optional 32-bit key.</li>
         <li>STT has a 64-bit key.</li>
-    <li>ERSPAN has a 10-bit key (Session ID).</li>
+        <li>ERSPAN has a 10-bit key (Session ID).</li>
+        <li>GTPU has a 32-bit key (Tunnel Endpoint ID).</li>
       </ul>
 
       <p>
@@ -1797,6 +1798,82 @@ ovs-ofctl add-flow br-int 'in_port=3,tun_src=192.168.1.1,tun_id=5001 actions=1'
       A 6-bit unique identifier of an ERSPAN v2 engine within a system.
     </field>
 
+    <h2>GTP-U Metadata Fields</h2>
+
+    <p>
+      These fields provide access to set-up GPRS Tunnelling Protocol
+      for User Plane (GTPv1-U), based on 3GPP TS 29.281.  A GTP-U
+      header has the following format:
+    </p>
+
+    <diagram>
+      <header>
+        <bits name="flags" above="8" width="0.6"/>
+        <bits name="msg type" above="8" width="0.6"/>
+        <bits name="length" above="16" width="0.9"/>
+        <bits name="TEID" above="32" width="1.3"/>
+      </header>
+      <dots/>
+    </diagram>
+
+    <p>
+      The flags and message type have the Open vSwitch GTP-U specific fields
+      described below.  Open vSwitch makes the TEID (Tunnel Endpoint
+      Identifier), which identifies a tunnel endpoint in the receiving GTP-U
+      protocol entity, available via <ref field="tun_id"/>.
+    </p>
+
+    <field id="MFF_TUN_GTPU_FLAGS" title="GTP-U Flags">
+      <p>
+        This field holds the 8-bit GTP-U flags, encoded as:
+      </p>
+
+      <diagram>
+        <header name="GTP-U Tunnel Flags">
+          <bits name="version" above="3" below="1" width="0.5"/>
+          <bits name="PT" above="1" width="0.3"/>
+          <bits name="rsv" above="1" below="0" width="0.3"/>
+          <bits name="E" above="1" width="0.3"/>
+          <bits name="S" above="1" width="0.3"/>
+          <bits name="PN" above="1" width="0.3"/>
+        </header>
+      </diagram>
+
+      <p>
+        The flags are:
+      </p>
+      <dl>
+        <dt>version</dt>
+        <dd>Used to determine the version of the GTP-U protocol, which should
+        be set to 1.</dd>
+
+        <dt>PT</dt>
+        <dd>Protocol type, used as a protocol discriminator
+        between GTP (1) and GTP' (0).</dd>
+
+        <dt>rsv</dt>
+        <dd>Reserved.  Must be zero.</dd>
+
+        <dt>E</dt>
+        <dd>If 1, indicates the presence of a meaningful value of the Next
+        Extension Header field.</dd>
+
+        <dt>S</dt>
+        <dd>If 1, indicates the presence of a meaningful value of the Sequence
+        Number field.</dd>
+
+        <dt>PN</dt>
+        <dd>If 1, indicates the presence of a meaningful value of the N-PDU
+        Number field.</dd>
+      </dl>
+    </field>
+
+    <field id="MFF_TUN_GTPU_MSGTYPE" title="GTP-U Message Type">
+      This field indicates whether it's a signalling message used for path
+      management, or a user plane message which carries the original packet.
+      The complete range of message types can be referred to [3GPP TS 29.281].
+    </field>
+
     <h2>Geneve Fields</h2>
 
     <p>
diff --git a/lib/netdev-native-tnl.c b/lib/netdev-native-tnl.c
index a78972888e33..0acc8795360c 100644
--- a/lib/netdev-native-tnl.c
+++ b/lib/netdev-native-tnl.c
@@ -55,6 +55,9 @@ static struct vlog_rate_limit err_rl = VLOG_RATE_LIMIT_INIT(60, 5);
 #define GENEVE_BASE_HLEN   (sizeof(struct udp_header) +         \
                             sizeof(struct genevehdr))
 
+#define GTPU_HLEN   (sizeof(struct udp_header) +        \
+                     sizeof(struct gtpuhdr))
+
 uint16_t tnl_udp_port_min = 32768;
 uint16_t tnl_udp_port_max = 61000;
 
@@ -213,6 +216,27 @@ udp_extract_tnl_md(struct dp_packet *packet, struct flow_tnl *tnl,
     return udp + 1;
 }
 
+static void
+netdev_tnl_calc_udp_csum(struct udp_header *udp, struct dp_packet *packet,
+                         int ip_tot_size)
+{
+    uint32_t csum;
+
+    if (netdev_tnl_is_header_ipv6(dp_packet_data(packet))) {
+        csum = packet_csum_pseudoheader6(netdev_tnl_ipv6_hdr(
+                                         dp_packet_data(packet)));
+    } else {
+        csum = packet_csum_pseudoheader(netdev_tnl_ip_hdr(
+                                        dp_packet_data(packet)));
+    }
+
+    csum = csum_continue(csum, udp, ip_tot_size);
+    udp->udp_csum = csum_finish(csum);
+
+    if (!udp->udp_csum) {
+        udp->udp_csum = htons(0xffff);
+    }
+}
 
 void
 netdev_tnl_push_udp_header(const struct netdev *netdev OVS_UNUSED,
@@ -229,19 +253,7 @@ netdev_tnl_push_udp_header(const struct netdev *netdev OVS_UNUSED,
     udp->udp_len = htons(ip_tot_size);
 
     if (udp->udp_csum) {
-        uint32_t csum;
-        if (netdev_tnl_is_header_ipv6(dp_packet_data(packet))) {
-            csum = packet_csum_pseudoheader6(netdev_tnl_ipv6_hdr(dp_packet_data(packet)));
-        } else {
-            csum = packet_csum_pseudoheader(netdev_tnl_ip_hdr(dp_packet_data(packet)));
-        }
-
-        csum = csum_continue(csum, udp, ip_tot_size);
-        udp->udp_csum = csum_finish(csum);
-
-        if (!udp->udp_csum) {
-            udp->udp_csum = htons(0xffff);
-        }
+        netdev_tnl_calc_udp_csum(udp, packet, ip_tot_size);
     }
 }
 
@@ -708,6 +720,133 @@ netdev_erspan_build_header(const struct netdev *netdev,
 }
 
 struct dp_packet *
+netdev_gtpu_pop_header(struct dp_packet *packet)
+{
+    struct pkt_metadata *md = &packet->md;
+    struct flow_tnl *tnl = &md->tunnel;
+    struct gtpuhdr *gtph;
+    unsigned int gtpu_hlen;
+    unsigned int hlen;
+
+    ovs_assert(packet->l3_ofs > 0);
+    ovs_assert(packet->l4_ofs > 0);
+
+    pkt_metadata_init_tnl(md);
+    if (GTPU_HLEN > dp_packet_l4_size(packet)) {
+        goto err;
+    }
+
+    gtph = udp_extract_tnl_md(packet, tnl, &hlen);
+    if (!gtph) {
+        goto err;
+    }
+
+    tnl->gtpu_flags = gtph->md.flags;
+    tnl->gtpu_msgtype = gtph->md.msgtype;
+    tnl->tun_id = be32_to_be64(get_16aligned_be32(&gtph->teid));
+
+    if (tnl->gtpu_msgtype == GTPU_MSGTYPE_GPDU) {
+        struct ip_header *ip;
+
+        if (gtph->md.flags & GTPU_S_MASK) {
+            gtpu_hlen = GTPU_HLEN + sizeof(struct gtpuhdr_opt);
+        } else {
+            gtpu_hlen = GTPU_HLEN;
+        }
+        ip = ALIGNED_CAST(struct ip_header *, (char *)gtph + gtpu_hlen);
+
+        if (IP_VER(ip->ip_ihl_ver) == 4) {
+            packet->packet_type = htonl(PT_IPV4);
+        } else if (IP_VER(ip->ip_ihl_ver) == 6) {
+            packet->packet_type = htonl(PT_IPV6);
+        } else {
+            VLOG_WARN_RL(&err_rl, "GTP-U: Receive non-IP packet.");
+        }
+        dp_packet_reset_packet(packet, hlen + gtpu_hlen);
+    } else {
+        /* non-GPDU GTP-U messages, ex: echo request, end marker.
+         * Users should redirect these packets to controller, or.
+         * any application that handles GTP-U messages, so keep
+         * the original packet.
+         */
+        packet->packet_type = htonl(PT_ETH);
+        VLOG_WARN_ONCE("Receive non-GPDU msgtype: %"PRIu8,
+                       gtph->md.msgtype);
+    }
+
+    return packet;
+
+err:
+    dp_packet_delete(packet);
+    return NULL;
+}
+
+void
+netdev_gtpu_push_header(const struct netdev *netdev,
+                        struct dp_packet *packet,
+                        const struct ovs_action_push_tnl *data)
+{
+    struct netdev_vport *dev = netdev_vport_cast(netdev);
+    struct netdev_tunnel_config *tnl_cfg;
+    struct udp_header *udp;
+    struct gtpuhdr *gtpuh;
+    int ip_tot_size;
+    unsigned int payload_len;
+
+    payload_len = dp_packet_size(packet);
+    udp = netdev_tnl_push_ip_header(packet, data->header,
+                                    data->header_len, &ip_tot_size);
+    udp->udp_src = netdev_tnl_get_src_port(packet);
+    udp->udp_len = htons(ip_tot_size);
+    netdev_tnl_calc_udp_csum(udp, packet, ip_tot_size);
+
+    gtpuh = ALIGNED_CAST(struct gtpuhdr *, udp + 1);
+
+    tnl_cfg = &dev->tnl_cfg;
+    if (tnl_cfg->set_seq) {
+        ovs_be16 *seqno = ALIGNED_CAST(ovs_be16 *, gtpuh + 1);
+        *seqno = htons(tnl_cfg->seqno++);
+        payload_len += sizeof(struct gtpuhdr_opt);
+    }
+    gtpuh->len = htons(payload_len);
+}
+
+int
+netdev_gtpu_build_header(const struct netdev *netdev,
+                         struct ovs_action_push_tnl *data,
+                         const struct netdev_tnl_build_header_params *params)
+{
+    struct netdev_vport *dev = netdev_vport_cast(netdev);
+    struct netdev_tunnel_config *tnl_cfg;
+    struct gtpuhdr *gtph;
+    unsigned int gtpu_hlen;
+
+    ovs_mutex_lock(&dev->mutex);
+    tnl_cfg = &dev->tnl_cfg;
+    gtph = udp_build_header(tnl_cfg, data, params);
+
+    /* Set to default if not set in flow. */
+    gtph->md.flags = params->flow->tunnel.gtpu_flags ?
+                     params->flow->tunnel.gtpu_flags : GTPU_FLAGS_DEFAULT;
+    gtph->md.msgtype = params->flow->tunnel.gtpu_msgtype ?
+                       params->flow->tunnel.gtpu_msgtype : GTPU_MSGTYPE_GPDU;
+    put_16aligned_be32(&gtph->teid,
+                       be64_to_be32(params->flow->tunnel.tun_id));
+
+    gtpu_hlen = sizeof *gtph;
+    if (tnl_cfg->set_seq) {
+        gtph->md.flags |= GTPU_S_MASK;
+        gtpu_hlen += sizeof(struct gtpuhdr_opt);
+    }
+    ovs_mutex_unlock(&dev->mutex);
+
+    data->header_len += gtpu_hlen;
+    data->tnl_type = OVS_VPORT_TYPE_GTPU;
+
+    return 0;
+}
+
+struct dp_packet *
 netdev_vxlan_pop_header(struct dp_packet *packet)
 {
     struct pkt_metadata *md = &packet->md;
diff --git a/lib/netdev-native-tnl.h b/lib/netdev-native-tnl.h
index 5dc00122d93e..22ae2ce5369b 100644
--- a/lib/netdev-native-tnl.h
+++ b/lib/netdev-native-tnl.h
@@ -52,6 +52,19 @@ netdev_erspan_push_header(const struct netdev *netdev,
 struct dp_packet *
 netdev_erspan_pop_header(struct dp_packet *packet);
 
+struct dp_packet *
+netdev_gtpu_pop_header(struct dp_packet *packet);
+
+void
+netdev_gtpu_push_header(const struct netdev *netdev,
+                        struct dp_packet *packet,
+                        const struct ovs_action_push_tnl *data);
+
+int
+netdev_gtpu_build_header(const struct netdev *netdev,
+                         struct ovs_action_push_tnl *data,
+                         const struct netdev_tnl_build_header_params *p);
+
 void
 netdev_tnl_push_udp_header(const struct netdev *netdev,
                            struct dp_packet *packet,
diff --git a/lib/netdev-vport.c b/lib/netdev-vport.c
index b57d21ff8d41..8efd1eee8302 100644
--- a/lib/netdev-vport.c
+++ b/lib/netdev-vport.c
@@ -111,7 +111,8 @@ netdev_vport_needs_dst_port(const struct netdev *dev)
 
     return (class->get_config == get_tunnel_config &&
             (!strcmp("geneve", type) || !strcmp("vxlan", type) ||
-             !strcmp("lisp", type) || !strcmp("stt", type)) );
+             !strcmp("lisp", type) || !strcmp("stt", type) ||
+             !strcmp("gtpu", type)));
 }
 
 const char *
@@ -216,6 +217,8 @@ netdev_vport_construct(struct netdev *netdev_)
         dev->tnl_cfg.dst_port = port ? htons(port) : htons(LISP_DST_PORT);
     } else if (!strcmp(type, "stt")) {
         dev->tnl_cfg.dst_port = port ? htons(port) : htons(STT_DST_PORT);
+    } else if (!strcmp(type, "gtpu")) {
+        dev->tnl_cfg.dst_port = port ? htons(port) : htons(GTPU_DST_PORT);
     }
 
     dev->tnl_cfg.dont_fragment = true;
@@ -433,6 +436,8 @@ tunnel_supported_layers(const char *type,
     } else if (!strcmp(type, "vxlan")
                && tnl_cfg->exts & (1 << OVS_VXLAN_EXT_GPE)) {
         return TNL_L2 | TNL_L3;
+    } else if (!strcmp(type, "gtpu")) {
+        return TNL_L3;
     } else {
         return TNL_L2;
     }
@@ -589,6 +594,10 @@ set_tunnel_config(struct netdev *dev_, const struct smap *args, char **errp)
         tnl_cfg.dst_port = htons(STT_DST_PORT);
     }
 
+    if (!strcmp(type, "gtpu")) {
+        tnl_cfg.dst_port = htons(GTPU_DST_PORT);
+    }
+
     needs_dst_port = netdev_vport_needs_dst_port(dev_);
     tnl_cfg.dont_fragment = true;
 
@@ -907,7 +916,8 @@ get_tunnel_config(const struct netdev *dev, struct smap *args)
         if ((!strcmp("geneve", type) && dst_port != GENEVE_DST_PORT) ||
             (!strcmp("vxlan", type) && dst_port != VXLAN_DST_PORT) ||
             (!strcmp("lisp", type) && dst_port != LISP_DST_PORT) ||
-            (!strcmp("stt", type) && dst_port != STT_DST_PORT)) {
+            (!strcmp("stt", type) && dst_port != STT_DST_PORT) ||
+            (!strcmp("gtpu", type) && dst_port != GTPU_DST_PORT)) {
             smap_add_format(args, "dst_port", "%d", dst_port);
         }
     }
@@ -1223,6 +1233,17 @@ netdev_vport_tunnel_register(void)
           },
           {{NULL, NULL, 0, 0}}
         },
+        { "gtpu_sys",
+          {
+              TUNNEL_FUNCTIONS_COMMON,
+              .type = "gtpu",
+              .build_header = netdev_gtpu_build_header,
+              .push_header = netdev_gtpu_push_header,
+              .pop_header = netdev_gtpu_pop_header,
+          },
+          {{NULL, NULL, 0, 0}}
+        },
+
     };
     static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
 
diff --git a/lib/nx-match.c b/lib/nx-match.c
index 0432ad4de6a7..058816c7b88f 100644
--- a/lib/nx-match.c
+++ b/lib/nx-match.c
@@ -1051,7 +1051,7 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
     ovs_be32 spi_mask;
     int match_len;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     struct nxm_put_ctx ctx = { .output = b, .implied_ethernet = false };
 
@@ -1191,6 +1191,12 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
     nxm_put_8m(&ctx, MFF_TUN_ERSPAN_HWID, oxm,
                 flow->tunnel.erspan_hwid, match->wc.masks.tunnel.erspan_hwid);
 
+    /* GTP-U */
+    nxm_put_8m(&ctx, MFF_TUN_GTPU_FLAGS, oxm, flow->tunnel.gtpu_flags,
+               match->wc.masks.tunnel.gtpu_flags);
+    nxm_put_8m(&ctx, MFF_TUN_GTPU_MSGTYPE, oxm, flow->tunnel.gtpu_msgtype,
+               match->wc.masks.tunnel.gtpu_msgtype);
+
     /* Network Service Header */
     nxm_put_8m(&ctx, MFF_NSH_FLAGS, oxm, flow->nsh.flags,
             match->wc.masks.nsh.flags);
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 746d1e97d474..b66d266cca1d 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -756,7 +756,17 @@ format_odp_tnl_push_header(struct ds *ds, struct ovs_action_push_tnl *data)
         } else {
             VLOG_WARN("%s Invalid ERSPAN version %d\n", __func__, ersh->ver);
         }
+    } else if (data->tnl_type == OVS_VPORT_TYPE_GTPU) {
+        const struct gtpuhdr *gtph;
+
+        gtph = format_udp_tnl_push_header(ds, udp);
+
+        ds_put_format(ds, "gtpu(flags=0x%"PRIx8
+                          ",msgtype=%"PRIu8",teid=0x%"PRIx32")",
+                      gtph->md.flags, gtph->md.msgtype,
+                      ntohl(get_16aligned_be32(&gtph->teid)));
     }
+
     ds_put_format(ds, ")");
 }
 
@@ -1500,6 +1510,8 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
     void *l3, *l4;
     int n = 0;
     uint8_t hwid, dir;
+    uint32_t teid;
+    uint8_t gtpu_flags, gtpu_msgtype;
 
     if (!ovs_scan_len(s, &n, "tnl_push(tnl_port(%"SCNi32"),", &data->tnl_port)) {
         return -EINVAL;
@@ -1729,6 +1741,18 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
 
         header_len = sizeof *eth + ip_len + ERSPAN_GREHDR_LEN +
                      sizeof *ersh + ERSPAN_V2_MDSIZE;
+
+    } else if (ovs_scan_len(s, &n, "gtpu(flags=%"SCNi8",msgtype=%"
+                SCNu8",teid=0x%"SCNx32"))",
+                &gtpu_flags, &gtpu_msgtype, &teid)) {
+        struct gtpuhdr *gtph = (struct gtpuhdr *) (udp + 1);
+
+        gtph->md.flags = gtpu_flags;
+        gtph->md.msgtype = gtpu_msgtype;
+        put_16aligned_be32(&gtph->teid, htonl(teid));
+        tnl_type = OVS_VPORT_TYPE_GTPU;
+        header_len = sizeof *eth + ip_len +
+                     sizeof *udp + sizeof *gtph;
     } else {
         return -EINVAL;
     }
@@ -2630,6 +2654,7 @@ static const struct attr_len_tbl ovs_tun_key_attr_lens[OVS_TUNNEL_KEY_ATTR_MAX +
     [OVS_TUNNEL_KEY_ATTR_IPV6_SRC]      = { .len = 16 },
     [OVS_TUNNEL_KEY_ATTR_IPV6_DST]      = { .len = 16 },
     [OVS_TUNNEL_KEY_ATTR_ERSPAN_OPTS]   = { .len = ATTR_LEN_VARIABLE },
+    [OVS_TUNNEL_KEY_ATTR_GTPU_OPTS]   = { .len = ATTR_LEN_VARIABLE },
 };
 
 const struct attr_len_tbl ovs_flow_key_attr_lens[OVS_KEY_ATTR_MAX + 1] = {
@@ -3035,6 +3060,13 @@ odp_tun_key_from_attr__(const struct nlattr *attr, bool is_mask,
             }
             break;
         }
+        case OVS_TUNNEL_KEY_ATTR_GTPU_OPTS: {
+            const struct gtpu_metadata *opts = nl_attr_get(a);
+
+            tun->gtpu_flags = opts->flags;
+            tun->gtpu_msgtype = opts->msgtype;
+            break;
+        }
 
         default:
             /* Allow this to show up as unexpected, if there are unknown
@@ -3149,6 +3181,15 @@ tun_key_to_attr(struct ofpbuf *a, const struct flow_tnl *tun_key,
                           &opts, sizeof(opts));
     }
 
+    if ((!tnl_type || !strcmp(tnl_type, "gtpu")) &&
+        (tun_key->gtpu_flags && tun_key->gtpu_msgtype)) {
+        struct gtpu_metadata opts;
+
+        opts.flags = tun_key->gtpu_flags;
+        opts.msgtype = tun_key->gtpu_msgtype;
+        nl_msg_put_unspec(a, OVS_TUNNEL_KEY_ATTR_GENEVE_OPTS,
+                          &opts, sizeof(opts));
+    }
     nl_msg_end_nested(a, tun_key_ofs);
 }
 
@@ -3645,6 +3686,22 @@ format_odp_tun_erspan_opt(const struct nlattr *attr,
     ds_chomp(ds, ',');
 }
 
+static void
+format_odp_tun_gtpu_opt(const struct nlattr *attr,
+                        const struct nlattr *mask_attr, struct ds *ds,
+                        bool verbose)
+{
+    const struct gtpu_metadata *opts, *mask;
+
+    opts = nl_attr_get(attr);
+    mask = mask_attr ? nl_attr_get(mask_attr) : NULL;
+
+    format_u8x(ds, "flags", opts->flags, mask ? &mask->flags : NULL, verbose);
+    format_u8u(ds, "msgtype", opts->msgtype, mask ? &mask->msgtype : NULL,
+               verbose);
+    ds_chomp(ds, ',');
+}
+
 #define MASK(PTR, FIELD) PTR ? &PTR->FIELD : NULL
 
 static void
@@ -3897,6 +3954,11 @@ format_odp_tun_attr(const struct nlattr *attr, const struct nlattr *mask_attr,
             format_odp_tun_erspan_opt(a, ma, ds, verbose);
             ds_put_cstr(ds, "),");
             break;
+        case OVS_TUNNEL_KEY_ATTR_GTPU_OPTS:
+            ds_put_cstr(ds, "gtpu(");
+            format_odp_tun_gtpu_opt(a, ma, ds, verbose);
+            ds_put_cstr(ds, ")");
+            break;
         case __OVS_TUNNEL_KEY_ATTR_MAX:
         default:
             format_unknown_key(ds, a, ma);
@@ -5105,6 +5167,50 @@ scan_vxlan_gbp(const char *s, uint32_t *key, uint32_t *mask)
 }
 
 static int
+scan_gtpu_metadata(const char *s,
+                   struct gtpu_metadata *key,
+                   struct gtpu_metadata *mask)
+{
+    const char *s_base = s;
+    uint8_t flags, flags_ma;
+    uint8_t msgtype, msgtype_ma;
+    int len;
+
+    if (!strncmp(s, "flags=", 6)) {
+        s += 6;
+        len = scan_u8(s, &flags, mask ? &flags_ma : NULL);
+        if (len == 0) {
+            return 0;
+        }
+        s += len;
+    }
+
+    if (s[0] == ',') {
+        s++;
+    }
+
+    if (!strncmp(s, "msgtype=", 8)) {
+        s += 8;
+        len = scan_u8(s, &msgtype, mask ? &msgtype_ma : NULL);
+        if (len == 0) {
+            return 0;
+        }
+        s += len;
+    }
+
+    if (!strncmp(s, ")", 1)) {
+        s += 1;
+        key->flags = flags;
+        key->msgtype = msgtype;
+        if (mask) {
+            mask->flags = flags_ma;
+            mask->msgtype = msgtype_ma;
+        }
+    }
+    return s - s_base;
+}
+
+static int
 scan_erspan_metadata(const char *s,
                      struct erspan_metadata *key,
                      struct erspan_metadata *mask)
@@ -5344,6 +5450,15 @@ erspan_to_attr(struct ofpbuf *a, const void *data_)
                       sizeof *md);
 }
 
+static void
+gtpu_to_attr(struct ofpbuf *a, const void *data_)
+{
+    const struct gtpu_metadata *md = data_;
+
+    nl_msg_put_unspec(a, OVS_TUNNEL_KEY_ATTR_GTPU_OPTS, md,
+                      sizeof *md);
+}
+
 #define SCAN_PUT_ATTR(BUF, ATTR, DATA, FUNC)                      \
     {                                                             \
         unsigned long call_fn = (unsigned long)FUNC;              \
@@ -5730,6 +5845,8 @@ parse_odp_key_mask_attr__(struct parse_odp_context *context, const char *s,
         SCAN_FIELD_NESTED_FUNC("vxlan(gbp(", uint32_t, vxlan_gbp, vxlan_gbp_to_attr);
         SCAN_FIELD_NESTED_FUNC("geneve(", struct geneve_scan, geneve,
                                geneve_to_attr);
+        SCAN_FIELD_NESTED_FUNC("gtpu(", struct gtpu_metadata, gtpu_metadata,
+                               gtpu_to_attr);
         SCAN_FIELD_NESTED_FUNC("flags(", uint16_t, tun_flags, tun_flags_to_attr);
     } SCAN_END_NESTED();
 
@@ -5997,7 +6114,7 @@ odp_flow_key_from_flow__(const struct odp_flow_key_parms *parms,
     /* New "struct flow" fields that are visible to the datapath (including all
      * data fields) should be translated into equivalent datapath flow fields
      * here (you will have to add a OVS_KEY_ATTR_* for them). */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     struct ovs_key_ethernet *eth_key;
     size_t encap[FLOW_MAX_VLAN_HEADERS] = {0};
@@ -7096,7 +7213,7 @@ odp_flow_key_to_flow__(const struct nlattr *key, size_t key_len,
     /* New "struct flow" fields that are visible to the datapath (including all
      * data fields) should be translated from equivalent datapath flow fields
      * here (you will have to add a OVS_KEY_ATTR_* for them).  */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     enum odp_key_fitness fitness = ODP_FIT_ERROR;
     if (errorp) {
@@ -8445,7 +8562,7 @@ commit_odp_actions(const struct flow *flow, struct flow *base,
     /* If you add a field that OpenFlow actions can change, and that is visible
      * to the datapath (including all data fields), then you should also add
      * code here to commit changes to the field. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     enum slow_path_reason slow1, slow2;
     bool mpls_done = false;
diff --git a/lib/odp-util.h b/lib/odp-util.h
index 4ecce1aac5d6..623a66aa28f4 100644
--- a/lib/odp-util.h
+++ b/lib/odp-util.h
@@ -147,7 +147,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 == 41);
+BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
 /* 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-match.c b/lib/ofp-match.c
index 2ec28f8036c0..86a082dde141 100644
--- a/lib/ofp-match.c
+++ b/lib/ofp-match.c
@@ -65,7 +65,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
     /* Initialize most of wc. */
     flow_wildcards_init_catchall(wc);
diff --git a/lib/packets.h b/lib/packets.h
index 4c1e91dee254..447e6f6fafa5 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -1447,6 +1447,74 @@ static inline ovs_be32 get_erspan_ts(enum erspan_ts_gra gra)
     return ts;
 }
 
+/*
+ * GTP-U protocol header and metadata
+ * See:
+ *   User Plane Protocol and Architectural Analysis on 3GPP 5G System
+ *                 draft-hmm-dmm-5g-uplane-analysis-00
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Ver |P|R|E|S|N| Message Type|             Length              |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                Tunnel Endpoint Identifier                     |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |      Sequence Number        |   N-PDU Number  |  Next-Ext-Hdr |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * GTP-U Flags:
+ *   P: Protocol Type (Set to '1')
+ *   R: Reserved Bit (Set to '0')
+ *   E: Extension Header Flag (Set to '1' if extension header exists)
+ *   S: Sequence Number Flag (Set to '1' if sequence number exists)
+ *   N: N-PDU Number Flag (Set to '1' if N-PDU number exists)
+ *
+ * GTP-U Message Type:
+ *   Indicates the type of GTP-U message.
+ *
+ * GTP-U Length:
+ *   Indicates the length in octets of the payload.
+ *
+ * User payload is transmitted in G-PDU packets.
+ */
+
+#define GTPU_VER_MASK   0xe0
+#define GTPU_P_MASK     0x10
+#define GTPU_E_MASK     0x04
+#define GTPU_S_MASK     0x02
+
+/* GTP-U UDP port. */
+#define GTPU_DST_PORT   2152
+
+/* Default GTP-U flags: Ver = 1 and P = 1. */
+#define GTPU_FLAGS_DEFAULT  0x30
+
+/* GTP-U message type for normal user plane PDU. */
+#define GTPU_MSGTYPE_REQ    1   /* Echo Request. */
+#define GTPU_MSGTYPE_REPL   2   /* Echo Reply. */
+#define GTPU_MSGTYPE_GPDU   255 /* User Payload. */
+
+struct gtpu_metadata {
+    uint8_t flags;
+    uint8_t msgtype;
+};
+BUILD_ASSERT_DECL(sizeof(struct gtpu_metadata) == 2);
+
+struct gtpuhdr {
+    struct gtpu_metadata md;
+    ovs_be16 len;
+    ovs_16aligned_be32 teid;
+};
+BUILD_ASSERT_DECL(sizeof(struct gtpuhdr) == 8);
+
+struct gtpuhdr_opt {
+    ovs_be16 seqno;
+    uint8_t pdu_number;
+    uint8_t next_ext_type;
+};
+BUILD_ASSERT_DECL(sizeof(struct gtpuhdr_opt) == 4);
+
 /* VXLAN protocol header */
 struct vxlanhdr {
     union {
diff --git a/lib/tnl-ports.c b/lib/tnl-ports.c
index 17353046cc6e..446b40763035 100644
--- a/lib/tnl-ports.c
+++ b/lib/tnl-ports.c
@@ -178,6 +178,9 @@ tnl_type_to_nw_proto(const char type[])
     if (!strcmp(type, "vxlan")) {
         return IPPROTO_UDP;
     }
+    if (!strcmp(type, "gtpu")) {
+        return IPPROTO_UDP;
+    }
     return 0;
 }
 
diff --git a/ofproto/ofproto-dpif-rid.h b/ofproto/ofproto-dpif-rid.h
index 147ef9c33348..e5d02caf28a3 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 == 41);
+BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
 
 struct frozen_metadata {
     /* Metadata in struct flow. */
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index 28dcc67ddac3..042c50a6346c 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3572,6 +3572,7 @@ propagate_tunnel_data_to_flow(struct xlate_ctx *ctx, struct eth_addr dmac,
         break;
     case OVS_VPORT_TYPE_VXLAN:
     case OVS_VPORT_TYPE_GENEVE:
+    case OVS_VPORT_TYPE_GTPU:
         nw_proto = IPPROTO_UDP;
         break;
     case OVS_VPORT_TYPE_LISP:
@@ -4123,7 +4124,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 == 41);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 42);
     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 23a5e150510a..76a3be44dd66 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -2352,7 +2352,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,src,dst,ipv6_{src,dst},flags,gbp_{id,flags},erspan_{idx,ver,dir,hwid},metadata0...metadata63} metadata in_{port,port_oxm} pkt_mark ct_{mark,label} reg0...reg15 xreg0...xreg7 xxreg0...xxreg3 eth_{src,dst} vlan_{tci,vid,pcp} mpls_{label,tc,ttl} ip_{src,dst} ipv6_{src,dst,label} nw_tos ip_dscp nw_{ecn,ttl} arp_{op,spa,tpa,sha,tha} tcp_{src,dst} udp_{src,dst} sctp_{src,dst} icmp_{type,code} icmpv6_{type,code} nd_{target,sll,tll,reserved,options_type} nsh_{flags,spi,si,c1...c4,ttl}
     matching:
-      arbitrary mask: dp_hash tun_{id,src,dst,ipv6_{src,dst},flags,gbp_{id,flags},erspan_{idx,ver,dir,hwid},metadata0...metadata63} metadata pkt_mark ct_{state,mark,label,nw_{src,dst},ipv6_{src,dst},tp_{src,dst}} reg0...reg15 xreg0...xreg7 xxreg0...xxreg3 eth_{src,dst} vlan_{tci,vid} ip_{src,dst} ipv6_{src,dst,label} ip_frag arp_{spa,tpa,sha,tha} tcp_{src,dst,flags} udp_{src,dst} sctp_{src,dst} nd_{target,sll,tll} nsh_{flags,c1...c4}
+      arbitrary mask: dp_hash tun_{id,src,dst,ipv6_{src,dst},flags,gbp_{id,flags},erspan_{idx,ver,dir,hwid},gtpu_{flags,msgtype},metadata0...metadata63} metadata pkt_mark ct_{state,mark,label,nw_{src,dst},ipv6_{src,dst},tp_{src,dst}} reg0...reg15 xreg0...xreg7 xxreg0...xxreg3 eth_{src,dst} vlan_{tci,vid} ip_{src,dst} ipv6_{src,dst,label} ip_frag arp_{spa,tpa,sha,tha} tcp_{src,dst,flags} udp_{src,dst} sctp_{src,dst} nd_{target,sll,tll} nsh_{flags,c1...c4}
       exact match or wildcard: recirc_id packet_type conj_id in_{port,port_oxm} actset_output ct_{zone,nw_proto} eth_type vlan_pcp mpls_{label,tc,bos,ttl} nw_{proto,tos} ip_dscp nw_{ecn,ttl} arp_op icmp_{type,code} icmpv6_{type,code} nd_{reserved,options_type} nsh_{mdtype,np,spi,si,ttl}
 
 ' "$1"
diff --git a/tests/tunnel-push-pop.at b/tests/tunnel-push-pop.at
index b92c23fde8f7..48c5de9d1907 100644
--- a/tests/tunnel-push-pop.at
+++ b/tests/tunnel-push-pop.at
@@ -216,6 +216,8 @@ AT_CHECK([ovs-vsctl add-port int-br t2 -- set Interface t2 type=vxlan \
                        options:remote_ip=1.1.2.92 options:key=456 options:packet_type=legacy_l3 ofport_request=7\
                     -- add-port int-br t7 -- set Interface t7 type=vxlan \
                        options:remote_ip=1.1.2.92 options:key=345 options:exts=gpe ofport_request=8\
+                    -- add-port int-br t8 -- set Interface t8 type=gtpu \
+                       options:remote_ip=1.1.2.92 options:key=123 ofport_request=9\
                        ], [0])
 
 AT_CHECK([ovs-appctl dpif/show], [0], [dnl
@@ -232,6 +234,7 @@ dummy@ovs-dummy: hit:0 missed:0
     t5 6/6081: (geneve: egress_pkt_mark=1234, out_key=flow, remote_ip=1.1.2.93)
     t6 7/3: (gre: key=456, packet_type=legacy_l3, remote_ip=1.1.2.92)
     t7 8/4789: (vxlan: key=345, remote_ip=1.1.2.92)
+    t8 9/2152: (gtpu: key=123, remote_ip=1.1.2.92)
 ])
 
 dnl First setup dummy interface IP address, then add the route
@@ -342,6 +345,7 @@ AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
 Listening ports:
 genev_sys_6081 (6081) ref_cnt=2
 gre_sys (3) ref_cnt=2
+gtpu_sys_2152 (2152) ref_cnt=1
 vxlan_sys_4789 (4789) ref_cnt=3
 ])
 
@@ -369,6 +373,13 @@ AT_CHECK([tail -1 stdout], [0],
   [Datapath actions: tnl_pop(6081)
 ])
 
+dnl Check GTP-U tunnel pop
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(1),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:00),eth_type(0x0800),ipv4(src=1.1.2.92,dst=1.1.2.88,proto=17,tos=0,ttl=64,frag=no),udp(src=51283,dst=2152)'],
+[0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: tnl_pop(2152)
+])
+
 dnl Check VXLAN tunnel push
 AT_CHECK([ovs-ofctl add-flow int-br action=2])
 AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(2),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:00),eth_type(0x0800),ipv4(src=1.1.3.88,dst=1.1.3.112,proto=47,tos=0,ttl=64,frag=no)'], [0], [stdout])
@@ -426,6 +437,15 @@ AT_CHECK([tail -1 stdout], [0],
   [Datapath actions: clone(tnl_push(tnl_port(6081),header(size=58,type=5,eth(dst=f8:bc:12:44:34:b6,src=aa:55:aa:55:00:00,dl_type=0x0800),ipv4(src=1.1.2.88,dst=1.1.2.92,proto=17,tos=0,ttl=64,frag=0x4000),udp(src=0,dst=6081,csum=0x0),geneve(crit,vni=0x7b,options({class=0xffff,type=0x80,len=4,0xa}))),out_port(100)),1)
 ])
 
+dnl Check GTP-U tunnel push
+AT_CHECK([ovs-ofctl add-flow int-br "actions=9"])
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(2),eth(src=f8:bc:12:44:34:b6,dst=aa:55:aa:55:00:00),eth_type(0x0800),ipv4(src=1.1.3.88,dst=1.1.3.112,proto=47,tos=0,ttl=64,frag=no)'],
+[0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: pop_eth,clone(tnl_push(tnl_port(2152),header(size=50,type=110,eth(dst=f8:bc:12:44:34:b6,src=aa:55:aa:55:00:00,dl_type=0x0800),ipv4(src=1.1.2.88,dst=1.1.2.92,proto=17,tos=0,ttl=64,frag=0x4000),udp(src=0,dst=2152,csum=0x0),gtpu(flags=0x30,msgtype=255,teid=0x7b)),out_port(100)),1)
+])
+AT_CHECK([ovs-ofctl del-flows int-br])
+
 dnl Check decapsulation of GRE packet
 AT_CHECK([ovs-appctl netdev-dummy/receive p0 'aa55aa550000001b213cab6408004500007e79464000402fba550101025c0101025820006558000001c8fe71d883724fbeb6f4e1494a080045000054ba200000400184861e0000011e00000200004227e75400030af3195500000000f265010000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637'])
 AT_CHECK([ovs-appctl netdev-dummy/receive p0 'aa55aa550000001b213cab6408004500007e79464000402fba550101025c0101025820006558000001c8fe71d883724fbeb6f4e1494a080045000054ba200000400184861e0000011e00000200004227e75400030af3195500000000f265010000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637'])
@@ -515,6 +535,7 @@ AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
 Listening ports:
 genev_sys_6081 (6081) ref_cnt=1
 gre_sys (3) ref_cnt=1
+gtpu_sys_2152 (2152) ref_cnt=1
 vxlan_sys_4789 (4789) ref_cnt=2
 vxlan_sys_4790 (4790) ref_cnt=1
 ])
@@ -524,6 +545,7 @@ AT_CHECK([ovs-vsctl del-port int-br t1 \
                     -- del-port int-br t4 \
                     -- del-port int-br t6 \
                     -- del-port int-br t7 \
+                    -- del-port int-br t8 \
                        ], [0])
 
 dnl Check tunnel lookup entries after deleting all remaining tunnel ports
diff --git a/tests/tunnel.at b/tests/tunnel.at
index ce000a25e6b6..d65bf4412aa9 100644
--- a/tests/tunnel.at
+++ b/tests/tunnel.at
@@ -1041,3 +1041,79 @@ AT_CHECK([ovs-appctl tnl/neigh/show | tail -n+3 | sort], [0], [dnl
 
 OVS_VSWITCHD_STOP
 AT_CLEANUP
+
+AT_SETUP([tunnel - GTP-U basic])
+OVS_VSWITCHD_START([add-port br0 p1 -- set Interface p1 type=gtpu \
+                    options:remote_ip=1.1.1.1 \
+                    options:key=123 ofport_request=1])
+
+AT_CHECK([ovs-appctl dpif/show | tail -n +3], [0], [dnl
+    br0 65534/100: (dummy-internal)
+    p1 1/2152: (gtpu: key=123, remote_ip=1.1.1.1)
+])
+
+AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
+Listening ports:
+gtpu_sys_2152 (2152) ref_cnt=1
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([tunnel - GTP-U push and pop])
+OVS_VSWITCHD_START([add-port br0 p1 -- set Interface p1 type=dummy \
+                    ofport_request=1 \
+                    -- add-port br0 p2 -- set Interface p2 type=dummy \
+                    ofport_request=2])
+
+# Add these ports separately to ensure that they get the datapath port
+# number expected below.
+ovs-vsctl -- add-port br0 p3 \
+          -- set Interface p3 type=gtpu \
+                              ofport_request=3 \
+                              options:remote_ip=1.1.1.1 \
+                              options:key=3 \
+                              options:packet_type=legacy_l3
+ovs-vsctl -- add-port br0 p4 \
+          -- set Interface p4 type=gtpu \
+                              ofport_request=4 \
+                              options:remote_ip=1.1.1.2 \
+                              options:key=4 \
+                              options:packet_type=legacy_l3
+OVS_VSWITCHD_DISABLE_TUNNEL_PUSH_POP
+
+dnl AT_CHECK([ovs-appctl dpif/show | tail -n +4], [0], [dnl
+AT_CHECK([ovs-appctl dpif/show | tail -n +4], [0], [dnl
+    p1 1/1: (dummy)
+    p2 2/2: (dummy)
+    p3 3/2152: (gtpu: key=3, remote_ip=1.1.1.1)
+    p4 4/2152: (gtpu: key=4, remote_ip=1.1.1.2)
+])
+
+AT_DATA([flows.txt], [dnl
+in_port=1,actions=3
+in_port=2,actions=4
+in_port=3,tun_gtpu_flags=0x30,tun_gtpu_msgtype=255,actions=1
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+
+AT_CHECK([ovs-appctl tnl/ports/show |sort], [0], [dnl
+Listening ports:
+gtpu_sys_2152 (2152) ref_cnt=2
+])
+
+dnl Encap: in_port=1,actions=3
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout])
+AT_CHECK([tail -1 stdout], [0],
+  [Datapath actions: set(tunnel(tun_id=0x3,dst=1.1.1.1,ttl=64,tp_dst=2152,flags(df|key))),pop_eth,2152
+])
+
+dnl receive packet from GTP-U port, match it, and output to layer3 GRE
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy 'recirc_id(0),tunnel(tun_id=0x3,src=1.1.1.1,dst=2.2.2.2,ttl=64,gtpu(flags=0x30,msgtype=255),flags(df|key)),in_port(2152),packet_type(ns=1,id=0),skb_mark(0),ipv4(frag=no)'], [0], [stdout])
+AT_CHECK([tail -2 stdout], [0],
+  [Megaflow: recirc_id=0,packet_type=(1,0),tun_id=0x3,tun_src=1.1.1.1,tun_dst=2.2.2.2,tun_tos=0,gtpu_flags=0x30,gtpu_msgtype=255,tun_flags=+df-csum+key,in_port=3,dl_type=0x0000
+Datapath actions: push_eth(src=00:00:00:00:00:00,dst=00:00:00:00:00:00),1
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml
index 4a74ed3ef24e..f9339af85edd 100644
--- a/vswitchd/vswitch.xml
+++ b/vswitchd/vswitch.xml
@@ -2647,6 +2647,30 @@
           <dd>
             A pair of virtual devices that act as a patch cable.
           </dd>
+
+          <dt><code>gtpu</code></dt>
+          <dd>
+            <p>
+            GPRS Tunneling Protocol (GTP) is a group of IP-based communications
+            protocols used to carry general packet radio service (GPRS) within
+            GSM, UMTS and LTE networks. GTP-U is used for carrying user data
+            within the GPRS core network and between the radio access network
+            and the core network. The user data transported can be packets in
+            any of IPv4, IPv6, or PPP formats.
+            </p>
+
+            <p>
+            The protocol is documented at
+            http://www.3gpp.org/DynaReport/29281.htm
+            </p>
+
+            <p>
+            Open vSwitch uses UDP destination port 2152. The source port used
+            for GTP traffic varies on a per-flow basis and is in the ephemeral
+            port range.
+            </p>
+          </dd>
+
         </dl>
       </column>
     </group>
