diff --git a/include/linux/pkt_cls.h b/include/linux/pkt_cls.h
index defbde2..2f80fa0 100644
--- a/include/linux/pkt_cls.h
+++ b/include/linux/pkt_cls.h
@@ -333,6 +333,11 @@ enum {
 	FLOW_KEY_SKGID,
 	FLOW_KEY_VLAN_TAG,
 	FLOW_KEY_RXHASH,
+	FLOW_KEY_TUNNEL_SRC,
+	FLOW_KEY_TUNNEL_DST,
+	FLOW_KEY_TUNNEL_PROTO,
+	FLOW_KEY_TUNNEL_PROTO_SRC,
+	FLOW_KEY_TUNNEL_PROTO_DST,
 	__FLOW_KEY_MAX,
 };
 
diff --git a/net/sched/cls_flow.c b/net/sched/cls_flow.c
index 6994214..f0bd3ad 100644
--- a/net/sched/cls_flow.c
+++ b/net/sched/cls_flow.c
@@ -311,6 +311,301 @@ static u32 flow_get_rxhash(struct sk_buff *skb)
 	return skb_get_rxhash(skb);
 }
 
+static u32 tunnel_inner_ip_src(struct sk_buff *skb)
+{
+	if (pskb_network_may_pull(skb, skb_network_header_len(skb) +
+							sizeof(struct iphdr))) {
+		return ntohl(ipip_hdr(skb)->saddr);
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ipv6_src(struct sk_buff *skb)
+{
+	if (pskb_network_may_pull(skb, skb_network_header_len(skb) +
+						sizeof(struct ipv6hdr))) {
+		struct ipv6hdr *iph = (struct ipv6hdr *)
+					skb_transport_header(skb);
+		return ntohl(iph->saddr.s6_addr32[3]);
+	}
+
+	return 0;
+}
+
+static u32 flow_get_tunnel_src(struct sk_buff *skb)
+{
+	switch (skb->protocol) {
+	case htons(ETH_P_IP):
+		if (pskb_network_may_pull(skb, sizeof(struct iphdr))) {
+			if (ip_hdr(skb)->protocol == IPPROTO_IPIP) {
+				return tunnel_inner_ip_src(skb);
+			} else if (ip_hdr(skb)->protocol == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_src(skb);
+			}
+		}
+		break;
+	case htons(ETH_P_IPV6):
+		if (pskb_network_may_pull(skb, sizeof(struct ipv6hdr))) {
+			if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPIP) {
+				return tunnel_inner_ip_src(skb);
+			} else if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_src(skb);
+			}
+		}
+		break;
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ip_dst(struct sk_buff *skb)
+{
+	if (pskb_network_may_pull(skb, skb_network_header_len(skb) +
+							sizeof(struct iphdr))) {
+		return ntohl(ipip_hdr(skb)->daddr);
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ipv6_dst(struct sk_buff *skb)
+{
+	if (pskb_network_may_pull(skb, skb_network_header_len(skb) +
+						sizeof(struct ipv6hdr))) {
+		struct ipv6hdr *iph = (struct ipv6hdr *)
+					skb_transport_header(skb);
+		return ntohl(iph->daddr.s6_addr32[3]);
+	}
+
+	return 0;
+}
+
+static u32 flow_get_tunnel_dst(struct sk_buff *skb)
+{
+	switch (skb->protocol) {
+	case htons(ETH_P_IP):
+		if (pskb_network_may_pull(skb, sizeof(struct iphdr))) {
+			if (ip_hdr(skb)->protocol == IPPROTO_IPIP) {
+				return tunnel_inner_ip_dst(skb);
+			} else if (ip_hdr(skb)->protocol == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_dst(skb);
+			}
+		}
+		break;
+	case htons(ETH_P_IPV6):
+		if (pskb_network_may_pull(skb, sizeof(struct ipv6hdr))) {
+			if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPIP) {
+				return tunnel_inner_ip_dst(skb);
+			} else if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_dst(skb);
+			}
+		}
+		break;
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ip_proto(struct sk_buff *skb)
+{
+	struct iphdr *iph;
+
+	if (!pskb_network_may_pull(skb, skb_network_header_len(skb) +
+							sizeof(struct iphdr))) {
+		return 0;
+	}
+
+	iph = ipip_hdr(skb);
+
+	return iph->protocol;
+}
+
+static u32 tunnel_inner_ipv6_proto(struct sk_buff *skb)
+{
+	struct ipv6hdr *ipv6h;
+
+	if (!pskb_network_may_pull(skb, skb_network_header_len(skb) +
+						sizeof(struct ipv6hdr))) {
+		return 0;
+	}
+
+	ipv6h = (struct ipv6hdr *)skb_transport_header(skb);
+
+	return ipv6h->nexthdr;
+}
+
+static u32 flow_get_tunnel_proto(struct sk_buff *skb)
+{
+	switch (skb->protocol) {
+	case htons(ETH_P_IP):
+		if (pskb_network_may_pull(skb, sizeof(struct iphdr))) {
+			if (ip_hdr(skb)->protocol == IPPROTO_IPIP) {
+				return tunnel_inner_ip_proto(skb);
+			} else if (ip_hdr(skb)->protocol == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_proto(skb);
+			}
+		}
+		break;
+	case htons(ETH_P_IPV6):
+		if (pskb_network_may_pull(skb, sizeof(struct ipv6hdr))) {
+			if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPIP) {
+				return tunnel_inner_ip_proto(skb);
+			} else if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_proto(skb);
+			}
+		}
+		break;
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ip_proto_src(struct sk_buff *skb)
+{
+	struct iphdr *iph;
+	int poff;
+
+	if (!pskb_network_may_pull(skb, skb_network_header_len(skb) +
+							sizeof(struct iphdr))) {
+		return 0;
+	}
+
+	iph = ipip_hdr(skb);
+
+	if (ip_is_fragment(iph))
+		return 0;
+
+	poff = proto_ports_offset(iph->protocol);
+	if (poff >= 0 && pskb_network_may_pull(skb, skb_network_header_len(skb)
+						+ iph->ihl * 4 + 2 + poff)) {
+		return ntohs(*(__be16 *)((void *)iph + iph->ihl * 4 + poff));
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ipv6_proto_src(struct sk_buff *skb)
+{
+	struct ipv6hdr *ipv6h;
+	int poff;
+
+	if (!pskb_network_may_pull(skb, skb_network_header_len(skb) +
+						sizeof(struct ipv6hdr))) {
+		return 0;
+	}
+
+	ipv6h = (struct ipv6hdr *)skb_transport_header(skb);
+
+	poff = proto_ports_offset(ipv6h->nexthdr);
+	if (poff >= 0 &&
+		    pskb_network_may_pull(skb, sizeof(*ipv6h) + poff + 2)) {
+		return ntohs(*(__be16 *)((void *)ipv6h + sizeof(*ipv6h) +
+						 			poff));
+	}
+
+	return 0;
+}
+
+static u32 flow_get_tunnel_proto_src(struct sk_buff *skb)
+{
+	switch (skb->protocol) {
+	case htons(ETH_P_IP):
+		if (pskb_network_may_pull(skb, sizeof(struct iphdr))) {
+			if (ip_hdr(skb)->protocol == IPPROTO_IPIP) {
+				return tunnel_inner_ip_proto_src(skb);
+			} else if (ip_hdr(skb)->protocol == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_proto_src(skb);
+			}
+			return 0;
+		}
+		break;
+	case htons(ETH_P_IPV6):
+		if (pskb_network_may_pull(skb, sizeof(struct ipv6hdr))) {
+			if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPIP) {
+				return tunnel_inner_ip_proto_src(skb);
+			} else if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_proto_src(skb);
+			}
+		}
+		break;
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ip_proto_dst(struct sk_buff *skb)
+{
+	struct iphdr *iph;
+	int poff;
+
+	if (!pskb_network_may_pull(skb, skb_network_header_len(skb) +
+							sizeof(struct iphdr))) {
+		return 0;
+	}
+
+	iph = ipip_hdr(skb);
+
+	if (ip_is_fragment(iph))
+		return 0;
+
+	poff = proto_ports_offset(iph->protocol);
+	if (poff >= 0 && pskb_network_may_pull(skb, skb_network_header_len(skb)
+						+ iph->ihl * 4 + 4 + poff)) {
+		return ntohs(*(__be16 *)((void *)iph + iph->ihl * 4 + 2 + poff));
+	}
+
+	return 0;
+}
+
+static u32 tunnel_inner_ipv6_proto_dst(struct sk_buff *skb)
+{
+	struct ipv6hdr *ipv6h;
+	int poff;
+
+	if (!pskb_network_may_pull(skb, skb_network_header_len(skb) +
+						sizeof(struct ipv6hdr))) {
+		return 0;
+	}
+
+	ipv6h = (struct ipv6hdr *)skb_transport_header(skb);
+
+	poff = proto_ports_offset(ipv6h->nexthdr);
+	if (poff >= 0 &&
+		    pskb_network_may_pull(skb, sizeof(*ipv6h) + poff + 4)) {
+		return ntohs(*(__be16 *)((void *)ipv6h + sizeof(*ipv6h) +
+						 		poff + 2));
+	}
+
+	return 0;
+}
+
+static u32 flow_get_tunnel_proto_dst(struct sk_buff *skb)
+{
+	switch (skb->protocol) {
+	case htons(ETH_P_IP):
+		if (pskb_network_may_pull(skb, sizeof(struct iphdr))) {
+			if (ip_hdr(skb)->protocol == IPPROTO_IPIP) {
+				return tunnel_inner_ip_proto_dst(skb);
+			} else if (ip_hdr(skb)->protocol == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_proto_dst(skb);
+			}
+		}
+		break;
+	case htons(ETH_P_IPV6):
+		if (pskb_network_may_pull(skb, sizeof(struct ipv6hdr))) {
+			if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPIP) {
+				return tunnel_inner_ip_proto_dst(skb);
+			} else if (ipv6_hdr(skb)->nexthdr == IPPROTO_IPV6) {
+				return tunnel_inner_ipv6_proto_dst(skb);
+			}
+		}
+		break;
+	}
+
+	return 0;
+}
+
 static u32 flow_key_get(struct sk_buff *skb, int key)
 {
 	switch (key) {
@@ -350,6 +645,16 @@ static u32 flow_key_get(struct sk_buff *skb, int key)
 		return flow_get_vlan_tag(skb);
 	case FLOW_KEY_RXHASH:
 		return flow_get_rxhash(skb);
+	case FLOW_KEY_TUNNEL_SRC:
+		return flow_get_tunnel_src(skb);
+	case FLOW_KEY_TUNNEL_DST:
+		return flow_get_tunnel_dst(skb);
+	case FLOW_KEY_TUNNEL_PROTO:
+		return flow_get_tunnel_proto(skb);
+	case FLOW_KEY_TUNNEL_PROTO_SRC:
+		return flow_get_tunnel_proto_src(skb);
+	case FLOW_KEY_TUNNEL_PROTO_DST:
+		return flow_get_tunnel_proto_dst(skb);
 	default:
 		WARN_ON(1);
 		return 0;
