diff --git a/acinclude.m4 b/acinclude.m4
index 991a627..d6e0d33 100644
--- a/acinclude.m4
+++ b/acinclude.m4
@@ -178,10 +178,10 @@ dnl Configure Linux tc compat.
 AC_DEFUN([OVS_CHECK_LINUX_TC], [
   AC_COMPILE_IFELSE([
     AC_LANG_PROGRAM([#include <linux/pkt_cls.h>], [
-        int x = TCA_FLOWER_KEY_FLAGS_FRAG_IS_FIRST;
+        int x = TCA_FLOWER_KEY_CVLAN_PRIO;
     ])],
-    [AC_DEFINE([HAVE_TCA_FLOWER_KEY_FLAGS_FRAG_IS_FIRST], [1],
-               [Define to 1 if TCA_FLOWER_KEY_FLAGS_FRAG_IS_FIRST is avaiable.])])
+    [AC_DEFINE([HAVE_TCA_FLOWER_KEY_CVLAN_PRIO], [1],
+               [Define to 1 if TCA_FLOWER_KEY_CVLAN_PRIO is avaiable.])])
 
   AC_COMPILE_IFELSE([
     AC_LANG_PROGRAM([#include <linux/tc_act/tc_vlan.h>], [
diff --git a/include/linux/pkt_cls.h b/include/linux/pkt_cls.h
index 60976f3..442323d 100644
--- a/include/linux/pkt_cls.h
+++ b/include/linux/pkt_cls.h
@@ -1,7 +1,7 @@
 #ifndef __LINUX_PKT_CLS_WRAPPER_H
 #define __LINUX_PKT_CLS_WRAPPER_H 1
 
-#if defined(__KERNEL__) || defined(HAVE_TCA_FLOWER_KEY_FLAGS_FRAG_IS_FIRST)
+#if defined(__KERNEL__) || defined(HAVE_TCA_FLOWER_KEY_CVLAN_PRIO)
 #include_next <linux/pkt_cls.h>
 #else
 
@@ -196,6 +196,10 @@ enum {
 	TCA_FLOWER_KEY_IP_TTL,		/* u8 */
 	TCA_FLOWER_KEY_IP_TTL_MASK,	/* u8 */
 
+	TCA_FLOWER_KEY_CVLAN_ID,	/* be16 */
+	TCA_FLOWER_KEY_CVLAN_PRIO,	/* u8   */
+	TCA_FLOWER_KEY_CVLAN_ETH_TYPE,	/* be16 */
+
 	__TCA_FLOWER_MAX,
 };
 
diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index aa9bbd9..62c7120 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -1709,7 +1709,7 @@ dpif_netlink_netdev_match_to_dpif_flow(struct match *match,
         .flow = &match->flow,
         .mask = &match->wc.masks,
         .support = {
-            .max_vlan_headers = 1,
+            .max_vlan_headers = 2,
         },
     };
     size_t offset;
diff --git a/lib/netdev-tc-offloads.c b/lib/netdev-tc-offloads.c
index 5e8d078..272e86e 100644
--- a/lib/netdev-tc-offloads.c
+++ b/lib/netdev-tc-offloads.c
@@ -431,9 +431,20 @@ parse_tc_flower_to_match(struct tc_flower *flower,
     match_set_dl_dst_masked(match, key->dst_mac, mask->dst_mac);
 
     if (eth_type_vlan(key->eth_type)) {
-        match_set_dl_vlan(match, htons(key->vlan_id), 0);
-        match_set_dl_vlan_pcp(match, key->vlan_prio, 0);
-        match_set_dl_type(match, key->encap_eth_type);
+        match->flow.vlans[0].tpid = key->eth_type;
+        match->wc.masks.vlans[0].tpid = OVS_BE16_MAX;
+        match_set_dl_vlan(match, htons(key->vlan_id[0]), 0);
+        match_set_dl_vlan_pcp(match, key->vlan_prio[0], 0);
+
+        if (eth_type_vlan(key->encap_eth_type[0])) {
+            match_set_dl_vlan(match, htons(key->vlan_id[1]), 1);
+            match_set_dl_vlan_pcp(match, key->vlan_prio[1], 1);
+            match_set_dl_type(match, key->encap_eth_type[1]);
+            match->flow.vlans[1].tpid = key->encap_eth_type[0];
+            match->wc.masks.vlans[1].tpid = OVS_BE16_MAX;
+        } else {
+            match_set_dl_type(match, key->encap_eth_type[0]);
+        }
         flow_fix_vlan_tpid(&match->flow);
     } else {
         match_set_dl_type(match, key->eth_type);
@@ -954,14 +965,14 @@ netdev_tc_flow_put(struct netdev *netdev, struct match *match,
             && (!pcp_mask || pcp_mask == htons(VLAN_PCP_MASK))
             && (vid_mask || pcp_mask)) {
             if (vid_mask) {
-                flower.key.vlan_id = vlan_tci_to_vid(key->vlans[0].tci);
-                VLOG_DBG_RL(&rl, "vlan_id: %d\n", flower.key.vlan_id);
+                flower.key.vlan_id[0] = vlan_tci_to_vid(key->vlans[0].tci);
+                VLOG_DBG_RL(&rl, "vlan_id[0]: %d\n", flower.key.vlan_id[0]);
             }
             if (pcp_mask) {
-                flower.key.vlan_prio = vlan_tci_to_pcp(key->vlans[0].tci);
-                VLOG_DBG_RL(&rl, "vlan_prio: %d\n", flower.key.vlan_prio);
+                flower.key.vlan_prio[0] = vlan_tci_to_pcp(key->vlans[0].tci);
+                VLOG_DBG_RL(&rl, "vlan_prio[0]: %d\n", flower.key.vlan_prio[0]);
             }
-            flower.key.encap_eth_type = flower.key.eth_type;
+            flower.key.encap_eth_type[0] = flower.key.eth_type;
             flower.key.eth_type = key->vlans[0].tpid;
         } else if (mask->vlans[0].tci == htons(0xffff) &&
                    ntohs(key->vlans[0].tci) == 0) {
@@ -970,8 +981,34 @@ netdev_tc_flow_put(struct netdev *netdev, struct match *match,
             /* partial mask */
             return EOPNOTSUPP;
         }
-    } else if (mask->vlans[1].tci) {
-        return EOPNOTSUPP;
+    }
+
+    if (mask->vlans[1].tci) {
+        ovs_be16 vid_mask = mask->vlans[1].tci & htons(VLAN_VID_MASK);
+        ovs_be16 pcp_mask = mask->vlans[1].tci & htons(VLAN_PCP_MASK);
+        ovs_be16 cfi = mask->vlans[1].tci & htons(VLAN_CFI);
+
+        if (cfi && key->vlans[1].tci & htons(VLAN_CFI)
+            && (!vid_mask || vid_mask == htons(VLAN_VID_MASK))
+            && (!pcp_mask || pcp_mask == htons(VLAN_PCP_MASK))
+            && (vid_mask || pcp_mask)) {
+            if (vid_mask) {
+                flower.key.vlan_id[1] = vlan_tci_to_vid(key->vlans[1].tci);
+                VLOG_DBG_RL(&rl, "vlan_id[1]: %d", flower.key.vlan_id[1]);
+            }
+            if (pcp_mask) {
+                flower.key.vlan_prio[1] = vlan_tci_to_pcp(key->vlans[1].tci);
+                VLOG_DBG_RL(&rl, "vlan_prio[1]: %d", flower.key.vlan_prio[1]);
+            }
+            flower.key.encap_eth_type[1] = flower.key.encap_eth_type[0];
+            flower.key.encap_eth_type[0] = key->vlans[1].tpid;
+        } else if (mask->vlans[1].tci == htons(0xffff) &&
+                   ntohs(key->vlans[1].tci) == 0) {
+            /* exact && no vlan */
+        } else {
+            /* partial mask */
+            return EOPNOTSUPP;
+        }
     }
     memset(mask->vlans, 0, sizeof mask->vlans);
 
diff --git a/lib/tc.c b/lib/tc.c
index 5030399..2157135 100644
--- a/lib/tc.c
+++ b/lib/tc.c
@@ -306,6 +306,9 @@ static const struct nl_policy tca_flower_policy[] = {
                                    .optional = true, },
     [TCA_FLOWER_KEY_TCP_FLAGS_MASK] = { .type = NL_A_U16,
                                         .optional = true, },
+    [TCA_FLOWER_KEY_CVLAN_ID] = { .type = NL_A_U16, .optional = true, },
+    [TCA_FLOWER_KEY_CVLAN_PRIO] = { .type = NL_A_U8, .optional = true, },
+    [TCA_FLOWER_KEY_CVLAN_ETH_TYPE] = { .type = NL_A_U16, .optional = true, },
 };
 
 static void
@@ -332,21 +335,44 @@ nl_parse_flower_eth(struct nlattr **attrs, struct tc_flower *flower)
 static void
 nl_parse_flower_vlan(struct nlattr **attrs, struct tc_flower *flower)
 {
+    ovs_be16 encap_ethtype;
+
     if (!eth_type_vlan(flower->key.eth_type)) {
         return;
     }
 
-    flower->key.encap_eth_type =
+    flower->key.encap_eth_type[0] =
         nl_attr_get_be16(attrs[TCA_FLOWER_KEY_ETH_TYPE]);
 
     if (attrs[TCA_FLOWER_KEY_VLAN_ID]) {
-        flower->key.vlan_id =
+        flower->key.vlan_id[0] =
             nl_attr_get_u16(attrs[TCA_FLOWER_KEY_VLAN_ID]);
     }
     if (attrs[TCA_FLOWER_KEY_VLAN_PRIO]) {
-        flower->key.vlan_prio =
+        flower->key.vlan_prio[0] =
             nl_attr_get_u8(attrs[TCA_FLOWER_KEY_VLAN_PRIO]);
     }
+
+    if (!attrs[TCA_FLOWER_KEY_VLAN_ETH_TYPE]) {
+        return;
+    }
+
+    encap_ethtype = nl_attr_get_be16(attrs[TCA_FLOWER_KEY_VLAN_ETH_TYPE]);
+    if (!eth_type_vlan(encap_ethtype)) {
+        return;
+    }
+
+    flower->key.encap_eth_type[1] = flower->key.encap_eth_type[0];
+    flower->key.encap_eth_type[0] = encap_ethtype;
+
+    if (attrs[TCA_FLOWER_KEY_CVLAN_ID]) {
+        flower->key.vlan_id[1] =
+            nl_attr_get_u16(attrs[TCA_FLOWER_KEY_CVLAN_ID]);
+    }
+    if (attrs[TCA_FLOWER_KEY_CVLAN_PRIO]) {
+        flower->key.vlan_prio[1] =
+            nl_attr_get_u8(attrs[TCA_FLOWER_KEY_CVLAN_PRIO]);
+    }
 }
 
 static void
@@ -1577,6 +1603,7 @@ nl_msg_put_flower_options(struct ofpbuf *request, struct tc_flower *flower)
 
     uint16_t host_eth_type = ntohs(flower->key.eth_type);
     bool is_vlan = eth_type_vlan(flower->key.eth_type);
+    bool is_qinq = is_vlan && eth_type_vlan(flower->key.encap_eth_type[0]);
     int err;
 
     /* need to parse acts first as some acts require changing the matching
@@ -1587,7 +1614,11 @@ nl_msg_put_flower_options(struct ofpbuf *request, struct tc_flower *flower)
     }
 
     if (is_vlan) {
-        host_eth_type = ntohs(flower->key.encap_eth_type);
+        if (is_qinq) {
+            host_eth_type = ntohs(flower->key.encap_eth_type[1]);
+        } else {
+            host_eth_type = ntohs(flower->key.encap_eth_type[0]);
+        }
     }
 
     FLOWER_PUT_MASKED_VALUE(dst_mac, TCA_FLOWER_KEY_ETH_DST);
@@ -1631,15 +1662,28 @@ nl_msg_put_flower_options(struct ofpbuf *request, struct tc_flower *flower)
     nl_msg_put_be16(request, TCA_FLOWER_KEY_ETH_TYPE, flower->key.eth_type);
 
     if (is_vlan) {
-        if (flower->key.vlan_id || flower->key.vlan_prio) {
+        if (flower->key.vlan_id[0] || flower->key.vlan_prio[0]) {
             nl_msg_put_u16(request, TCA_FLOWER_KEY_VLAN_ID,
-                           flower->key.vlan_id);
+                           flower->key.vlan_id[0]);
             nl_msg_put_u8(request, TCA_FLOWER_KEY_VLAN_PRIO,
-                          flower->key.vlan_prio);
+                          flower->key.vlan_prio[0]);
         }
-        if (flower->key.encap_eth_type) {
+        if (flower->key.encap_eth_type[0]) {
             nl_msg_put_be16(request, TCA_FLOWER_KEY_VLAN_ETH_TYPE,
-                            flower->key.encap_eth_type);
+                            flower->key.encap_eth_type[0]);
+        }
+
+        if (is_qinq) {
+            if (flower->key.vlan_id[1] || flower->key.vlan_prio[1]) {
+                nl_msg_put_u16(request, TCA_FLOWER_KEY_CVLAN_ID,
+                               flower->key.vlan_id[1]);
+                nl_msg_put_u8(request, TCA_FLOWER_KEY_CVLAN_PRIO,
+                              flower->key.vlan_prio[1]);
+            }
+            if (flower->key.encap_eth_type[1]) {
+                nl_msg_put_be16(request, TCA_FLOWER_KEY_CVLAN_ETH_TYPE,
+                                flower->key.encap_eth_type[1]);
+            }
         }
     }
 
diff --git a/lib/tc.h b/lib/tc.h
index d954819..447d85f 100644
--- a/lib/tc.h
+++ b/lib/tc.h
@@ -26,6 +26,7 @@
 #include "netlink-socket.h"
 #include "odp-netlink.h"
 #include "openvswitch/ofpbuf.h"
+#include "openvswitch/flow.h"
 
 /* For backwards compatability with older kernels */
 #ifndef TC_H_CLSACT
@@ -87,10 +88,10 @@ struct tc_flower_key {
     ovs_be16 sctp_src;
     ovs_be16 sctp_dst;
 
-    uint16_t vlan_id;
-    uint8_t vlan_prio;
+    uint16_t vlan_id[FLOW_MAX_VLAN_HEADERS];
+    uint8_t vlan_prio[FLOW_MAX_VLAN_HEADERS];
 
-    ovs_be16 encap_eth_type;
+    ovs_be16 encap_eth_type[FLOW_MAX_VLAN_HEADERS];
 
     uint8_t flags;
     uint8_t ip_ttl;
