diff --git a/include/linux/pkt_cls.h b/include/linux/pkt_cls.h
index a3300418e..1384d71f9 100644
--- a/include/linux/pkt_cls.h
+++ b/include/linux/pkt_cls.h
@@ -204,10 +204,35 @@ enum {
 	TCA_FLOWER_KEY_ENC_IP_TOS_MASK,	/* u8 */
 	TCA_FLOWER_KEY_ENC_IP_TTL,		/* u8 */
 	TCA_FLOWER_KEY_ENC_IP_TTL_MASK,	/* u8 */
+	TCA_FLOWER_KEY_ENC_OPTS,
+	TCA_FLOWER_KEY_ENC_OPTS_MASK,
 
 	__TCA_FLOWER_MAX,
 };
 
+enum {
+	TCA_FLOWER_KEY_ENC_OPTS_UNSPEC,
+	TCA_FLOWER_KEY_ENC_OPTS_GENEVE, /* Nested
+					 * TCA_TUNNEL_KEY_ENC_OPTS_GENEVE
+					 * attributes
+					 */
+	__TCA_FLOWER_KEY_ENC_OPTS_MAX,
+};
+
+#define TCA_FLOWER_KEY_ENC_OPTS_MAX (__TCA_FLOWER_KEY_ENC_OPTS_MAX - 1)
+
+enum {
+	TCA_FLOWER_KEY_ENC_OPT_GENEVE_UNSPEC,
+	TCA_FLOWER_KEY_ENC_OPT_GENEVE_CLASS,            /* u16 */
+	TCA_FLOWER_KEY_ENC_OPT_GENEVE_TYPE,             /* u8 */
+	TCA_FLOWER_KEY_ENC_OPT_GENEVE_DATA,             /* 4 to 128 bytes */
+
+	__TCA_FLOWER_KEY_ENC_OPT_GENEVE_MAX,
+};
+
+#define TCA_FLOWER_KEY_ENC_OPT_GENEVE_MAX \
+		(__TCA_FLOWER_KEY_ENC_OPT_GENEVE_MAX - 1)
+
 enum {
 	TCA_FLOWER_KEY_FLAGS_IS_FRAGMENT = (1 << 0),
 	TCA_FLOWER_KEY_FLAGS_FRAG_IS_FIRST = (1 << 1),
diff --git a/lib/netdev-tc-offloads.c b/lib/netdev-tc-offloads.c
index 097893584..4894c0d0f 100644
--- a/lib/netdev-tc-offloads.c
+++ b/lib/netdev-tc-offloads.c
@@ -432,6 +432,38 @@ static void parse_tc_flower_geneve_opts(struct tc_action *action,
     nl_msg_end_nested(buf, geneve_off);
 }
 
+static void
+flower_tun_opt_to_match(struct match *match, struct tc_flower *flower)
+{
+    struct geneve_opt *opt, *opt_mask;
+    int len, cnt = 0;
+
+    memcpy(match->flow.tunnel.metadata.opts.gnv,
+           flower->key.tunnel.metadata.opts.gnv,
+           flower->key.tunnel.metadata.present.len);
+    match->flow.tunnel.metadata.present.len =
+           flower->key.tunnel.metadata.present.len;
+    match->flow.tunnel.flags |= FLOW_TNL_F_UDPIF;
+    memcpy(match->wc.masks.tunnel.metadata.opts.gnv,
+           flower->mask.tunnel.metadata.opts.gnv,
+           flower->mask.tunnel.metadata.present.len);
+
+    len = flower->key.tunnel.metadata.present.len;
+    while (len) {
+        opt = &match->flow.tunnel.metadata.opts.gnv[cnt];
+        opt_mask = &match->wc.masks.tunnel.metadata.opts.gnv[cnt];
+
+        opt_mask->length = 0x1f;
+
+        cnt += sizeof(struct geneve_opt) / 4 + opt->length;
+        len -= sizeof(struct geneve_opt) + opt->length * 4;
+    }
+
+    match->wc.masks.tunnel.metadata.present.len =
+           flower->mask.tunnel.metadata.present.len;
+    match->wc.masks.tunnel.flags |= FLOW_TNL_F_UDPIF;
+}
+
 static int
 parse_tc_flower_to_match(struct tc_flower *flower,
                          struct match *match,
@@ -548,6 +580,9 @@ parse_tc_flower_to_match(struct tc_flower *flower,
         if (flower->key.tunnel.tp_dst) {
             match_set_tun_tp_dst(match, flower->key.tunnel.tp_dst);
         }
+        if (flower->key.tunnel.metadata.present.len) {
+            flower_tun_opt_to_match(match, flower);
+        }
     }
 
     act_off = nl_msg_start_nested(buf, OVS_FLOW_ATTR_ACTIONS);
@@ -968,6 +1003,34 @@ test_key_and_mask(struct match *match)
     return 0;
 }
 
+static void
+flower_match_to_tun_opt(struct tc_flower *flower, const struct flow_tnl *tnl,
+                        const struct flow_tnl *tnl_mask)
+{
+    struct geneve_opt *opt, *opt_mask;
+    int len, cnt = 0;
+
+    memcpy(flower->key.tunnel.metadata.opts.gnv, tnl->metadata.opts.gnv,
+           tnl->metadata.present.len);
+    flower->key.tunnel.metadata.present.len = tnl->metadata.present.len;
+
+    memcpy(flower->mask.tunnel.metadata.opts.gnv, tnl_mask->metadata.opts.gnv,
+           tnl->metadata.present.len);
+
+    len = flower->key.tunnel.metadata.present.len;
+    while (len) {
+        opt = &flower->key.tunnel.metadata.opts.gnv[cnt];
+        opt_mask = &flower->mask.tunnel.metadata.opts.gnv[cnt];
+
+        opt_mask->length = opt->length;
+
+        cnt += sizeof(struct geneve_opt) / 4 + opt->length;
+        len -= sizeof(struct geneve_opt) + opt->length * 4;
+    }
+
+    flower->mask.tunnel.metadata.present.len = tnl->metadata.present.len;
+}
+
 int
 netdev_tc_flow_put(struct netdev *netdev, struct match *match,
                    struct nlattr *actions, size_t actions_len,
@@ -1016,6 +1079,7 @@ netdev_tc_flow_put(struct netdev *netdev, struct match *match,
         flower.key.tunnel.tp_dst = tnl->tp_dst;
         flower.mask.tunnel.tos = tnl_mask->ip_tos;
         flower.mask.tunnel.ttl = tnl_mask->ip_ttl;
+        flower_match_to_tun_opt(&flower, tnl, tnl_mask);
         flower.tunnel = true;
     }
     memset(&mask->tunnel, 0, sizeof mask->tunnel);
diff --git a/lib/tc.c b/lib/tc.c
index 45e59e4d7..1cf305487 100644
--- a/lib/tc.c
+++ b/lib/tc.c
@@ -325,6 +325,9 @@ static const struct nl_policy tca_flower_policy[] = {
                                     .optional = true, },
     [TCA_FLOWER_KEY_ENC_IP_TTL_MASK] = { .type = NL_A_U8,
                                          .optional = true, },
+    [TCA_FLOWER_KEY_ENC_OPTS] = { .type = NL_A_NESTED, .optional = true, },
+    [TCA_FLOWER_KEY_ENC_OPTS_MASK] = { .type = NL_A_NESTED,
+                                       .optional = true, },
 };
 
 static void
@@ -431,9 +434,130 @@ nl_parse_flower_vlan(struct nlattr **attrs, struct tc_flower *flower)
     }
 }
 
-static void
+static int
+nl_parse_geneve_key(const struct nlattr *in_nlattr,
+                    struct tun_metadata *metadata)
+{
+    struct geneve_opt *opt = NULL;
+    const struct ofpbuf *msg;
+    uint16_t last_opt_type;
+    struct nlattr *nla;
+    struct ofpbuf buf;
+    size_t left;
+    int cnt;
+
+    nl_attr_get_nested(in_nlattr, &buf);
+    msg = &buf;
+
+    last_opt_type = TCA_FLOWER_KEY_ENC_OPT_GENEVE_UNSPEC;
+    cnt = 0;
+    NL_ATTR_FOR_EACH (nla, left, ofpbuf_at(msg, 0, 0), msg->size) {
+        uint16_t type = nl_attr_type(nla);
+
+        switch (type) {
+        case TCA_FLOWER_KEY_ENC_OPT_GENEVE_CLASS:
+            if (cnt && last_opt_type != TCA_FLOWER_KEY_ENC_OPT_GENEVE_DATA) {
+                VLOG_ERR_RL(&error_rl, "failed to parse tun options class");
+                return EINVAL;
+            }
+
+            opt = &metadata->opts.gnv[cnt];
+            opt->opt_class = nl_attr_get_be16(nla);
+            cnt += sizeof(struct geneve_opt) / 4;
+            metadata->present.len += sizeof(struct geneve_opt);
+            last_opt_type = TCA_FLOWER_KEY_ENC_OPT_GENEVE_CLASS;
+            break;
+        case TCA_FLOWER_KEY_ENC_OPT_GENEVE_TYPE:
+            if (last_opt_type != TCA_FLOWER_KEY_ENC_OPT_GENEVE_CLASS) {
+                VLOG_ERR_RL(&error_rl, "failed to parse tun options type");
+                return EINVAL;
+            }
+
+            opt->type = nl_attr_get_u8(nla);
+            last_opt_type = TCA_FLOWER_KEY_ENC_OPT_GENEVE_TYPE;
+            break;
+        case TCA_FLOWER_KEY_ENC_OPT_GENEVE_DATA:
+            if (last_opt_type != TCA_FLOWER_KEY_ENC_OPT_GENEVE_TYPE) {
+                VLOG_ERR_RL(&error_rl, "failed to parse tun options data");
+                return EINVAL;
+            }
+
+            opt->length = nl_attr_get_size(nla) / 4;
+            memcpy(opt + 1, nl_attr_get_unspec(nla, 1), opt->length * 4);
+            cnt += opt->length;
+            metadata->present.len += opt->length * 4;
+            last_opt_type = TCA_FLOWER_KEY_ENC_OPT_GENEVE_DATA;
+            break;
+        }
+    }
+
+    if (last_opt_type != TCA_FLOWER_KEY_ENC_OPT_GENEVE_DATA) {
+        VLOG_ERR_RL(&error_rl, "failed to parse tun options without data");
+        return EINVAL;
+    }
+
+    return 0;
+}
+
+static int
+nl_parse_flower_tunnel_opts(struct nlattr *options,
+                            struct tun_metadata *metadata)
+{
+    const struct ofpbuf *msg;
+    struct nlattr *nla;
+    struct ofpbuf buf;
+    size_t left;
+    int err;
+
+    nl_attr_get_nested(options, &buf);
+    msg = &buf;
+
+    NL_ATTR_FOR_EACH (nla, left, ofpbuf_at(msg, 0, 0), msg->size) {
+        uint16_t type = nl_attr_type(nla);
+        switch (type) {
+        case TCA_FLOWER_KEY_ENC_OPTS_GENEVE:
+            err = nl_parse_geneve_key(nla, metadata);
+            if (err) {
+                return err;
+            }
+
+            break;
+        }
+    }
+
+    return 0;
+}
+
+static int
+flower_tun_geneve_opt_check_len(struct tun_metadata *key,
+                                struct tun_metadata *mask)
+{
+    const struct geneve_opt *opt, *opt_mask;
+    int len, cnt = 0;
+
+    len = key->present.len;
+    while (len) {
+        opt = &key->opts.gnv[cnt];
+        opt_mask = &mask->opts.gnv[cnt];
+
+        if (opt->length != opt_mask->length) {
+            VLOG_ERR_RL(&error_rl,
+                        "failed to parse tun options; key/mask length differ");
+            return EINVAL;
+        }
+
+        cnt += sizeof(struct geneve_opt) / 4 + opt->length;
+        len -= sizeof(struct geneve_opt) + opt->length * 4;
+    }
+
+    return 0;
+}
+
+static int
 nl_parse_flower_tunnel(struct nlattr **attrs, struct tc_flower *flower)
 {
+    int err;
+
     if (attrs[TCA_FLOWER_KEY_ENC_KEY_ID]) {
         ovs_be32 id = nl_attr_get_be32(attrs[TCA_FLOWER_KEY_ENC_KEY_ID]);
 
@@ -471,6 +595,35 @@ nl_parse_flower_tunnel(struct nlattr **attrs, struct tc_flower *flower)
         flower->mask.tunnel.ttl =
             nl_attr_get_u8(attrs[TCA_FLOWER_KEY_ENC_IP_TTL_MASK]);
     }
+    if (attrs[TCA_FLOWER_KEY_ENC_OPTS] &&
+        attrs[TCA_FLOWER_KEY_ENC_OPTS_MASK]) {
+         err = nl_parse_flower_tunnel_opts(attrs[TCA_FLOWER_KEY_ENC_OPTS],
+                                           &flower->key.tunnel.metadata);
+         if (err) {
+             return err;
+         }
+
+         err = nl_parse_flower_tunnel_opts(attrs[TCA_FLOWER_KEY_ENC_OPTS_MASK],
+                                           &flower->mask.tunnel.metadata);
+         if (err) {
+             return err;
+         }
+
+         err = flower_tun_geneve_opt_check_len(&flower->key.tunnel.metadata,
+                                               &flower->mask.tunnel.metadata);
+         if (err) {
+             return err;
+         }
+    } else if (attrs[TCA_FLOWER_KEY_ENC_OPTS]) {
+        VLOG_ERR_RL(&error_rl,
+                    "failed to parse tun options; no mask supplied");
+        return EINVAL;
+    } else if (attrs[TCA_FLOWER_KEY_ENC_OPTS_MASK]) {
+        VLOG_ERR_RL(&error_rl, "failed to parse tun options; no key supplied");
+        return EINVAL;
+    }
+
+    return 0;
 }
 
 static void
@@ -1184,6 +1337,7 @@ static int
 nl_parse_flower_options(struct nlattr *nl_options, struct tc_flower *flower)
 {
     struct nlattr *attrs[ARRAY_SIZE(tca_flower_policy)];
+    int err;
 
     if (!nl_parse_nested(nl_options, tca_flower_policy,
                          attrs, ARRAY_SIZE(tca_flower_policy))) {
@@ -1195,7 +1349,11 @@ nl_parse_flower_options(struct nlattr *nl_options, struct tc_flower *flower)
     nl_parse_flower_mpls(attrs, flower);
     nl_parse_flower_vlan(attrs, flower);
     nl_parse_flower_ip(attrs, flower);
-    nl_parse_flower_tunnel(attrs, flower);
+    err = nl_parse_flower_tunnel(attrs, flower);
+    if (err) {
+        return err;
+    }
+
     nl_parse_flower_flags(attrs, flower);
     return nl_parse_flower_actions(attrs, flower);
 }
@@ -1806,6 +1964,38 @@ nl_msg_put_masked_value(struct ofpbuf *request, uint16_t type,
     nl_msg_put_unspec(request, type, data, len);
 }
 
+static void
+nl_msg_put_flower_tunnel_opts(struct ofpbuf *request, uint16_t type,
+                              struct tun_metadata metadata)
+{
+    struct geneve_opt *opt;
+    size_t outer, inner;
+    int len, cnt = 0;
+
+    len = metadata.present.len;
+    if (!len) {
+        return;
+    }
+
+    outer = nl_msg_start_nested(request, type);
+    while (len) {
+        opt = &metadata.opts.gnv[cnt];
+        inner = nl_msg_start_nested(request, TCA_FLOWER_KEY_ENC_OPTS_GENEVE);
+
+        nl_msg_put_be16(request, TCA_FLOWER_KEY_ENC_OPT_GENEVE_CLASS,
+                        opt->opt_class);
+        nl_msg_put_u8(request, TCA_FLOWER_KEY_ENC_OPT_GENEVE_TYPE, opt->type);
+        nl_msg_put_unspec(request, TCA_FLOWER_KEY_ENC_OPT_GENEVE_DATA, opt + 1,
+                          opt->length * 4);
+
+        cnt += sizeof(struct geneve_opt) / 4 + opt->length;
+        len -= sizeof(struct geneve_opt) + opt->length * 4;
+
+        nl_msg_end_nested(request, inner);
+    }
+    nl_msg_end_nested(request, outer);
+}
+
 static void
 nl_msg_put_flower_tunnel(struct ofpbuf *request, struct tc_flower *flower)
 {
@@ -1837,6 +2027,10 @@ nl_msg_put_flower_tunnel(struct ofpbuf *request, struct tc_flower *flower)
     }
     nl_msg_put_be16(request, TCA_FLOWER_KEY_ENC_UDP_DST_PORT, tp_dst);
     nl_msg_put_be32(request, TCA_FLOWER_KEY_ENC_KEY_ID, id);
+    nl_msg_put_flower_tunnel_opts(request, TCA_FLOWER_KEY_ENC_OPTS,
+                                  flower->key.tunnel.metadata);
+    nl_msg_put_flower_tunnel_opts(request, TCA_FLOWER_KEY_ENC_OPTS_MASK,
+                                  flower->mask.tunnel.metadata);
 }
 
 #define FLOWER_PUT_MASKED_VALUE(member, type) \
diff --git a/lib/tc.h b/lib/tc.h
index 6c17ee391..cff96c5c2 100644
--- a/lib/tc.h
+++ b/lib/tc.h
@@ -123,6 +123,7 @@ struct tc_flower_key {
         ovs_be16 tp_src;
         ovs_be16 tp_dst;
         ovs_be64 id;
+        struct tun_metadata metadata;
     } tunnel;
 };
 
