diff --git a/NEWS b/NEWS
index fb3a20c..9b9dff2 100644
--- a/NEWS
+++ b/NEWS
@@ -25,8 +25,8 @@ Post-v2.4.0
      the next OVS release.
    - Added --user option to all daemons
    - Add support for connection tracking through the new "ct" action
-     and "ct_state"/"ct_zone"/"ct_mark" match fields.  Only available on
-     Linux kernels with the connection tracking module loaded.
+     and "ct_state"/"ct_zone"/"ct_mark"/"ct_label" match fields.  Only
+     available on Linux kernels with the connection tracking module loaded.
 
 
 v2.4.0 - 20 Aug 2015
diff --git a/datapath/flow_netlink.c b/datapath/flow_netlink.c
index f50f023..3a3492b 100644
--- a/datapath/flow_netlink.c
+++ b/datapath/flow_netlink.c
@@ -282,7 +282,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 != 25);
+	BUILD_BUG_ON(OVS_KEY_ATTR_TUNNEL_INFO != 26);
 
 	return    nla_total_size(4)   /* OVS_KEY_ATTR_PRIORITY */
 		+ nla_total_size(0)   /* OVS_KEY_ATTR_TUNNEL */
diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index fbe2467..99a1624 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -346,6 +346,7 @@ enum ovs_key_attr {
 	OVS_KEY_ATTR_CT_STATE,	/* u32 bitmask of OVS_CS_F_* */
 	OVS_KEY_ATTR_CT_ZONE,	/* u16 connection tracking zone. */
 	OVS_KEY_ATTR_CT_MARK,	/* u32 connection tracking mark */
+	OVS_KEY_ATTR_CT_LABEL,	/* 16-octet connection tracking label */
 
 #ifdef __KERNEL__
 	/* Only used within kernel data path. */
@@ -459,6 +460,11 @@ struct ovs_key_nd {
 	__u8	nd_tll[ETH_ALEN];
 };
 
+#define OVS_CT_LABEL_LEN	16
+struct ovs_key_ct_label {
+	__u8	ct_label[OVS_CT_LABEL_LEN];
+};
+
 /* 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. */
@@ -662,12 +668,16 @@ struct ovs_action_push_tnl {
  * @OVS_CT_ATTR_MARK: u32 value followed by u32 mask. For each bit set in the
  * mask, the corresponding bit in the value is copied to the connection
  * tracking mark field in the connection.
+ * @OVS_CT_ATTR_LABEL: %OVS_CT_LABEL_LEN value followed by %OVS_CT_LABEL_LEN
+ * mask. For each bit set in the mask, the corresponding bit in the value is
+ * copied to the connection tracking label field in the connection.
  */
 enum ovs_ct_attr {
 	OVS_CT_ATTR_UNSPEC,
 	OVS_CT_ATTR_COMMIT,     /* No argument, commits connection. */
 	OVS_CT_ATTR_ZONE,       /* u16 zone id. */
 	OVS_CT_ATTR_MARK,       /* mark to associate with this connection. */
+	OVS_CT_ATTR_LABEL,      /* label to associate with this connection. */
 	__OVS_CT_ATTR_MAX
 };
 
diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c
index 8769842..47fa9e2 100644
--- a/lib/dpif-netdev.c
+++ b/lib/dpif-netdev.c
@@ -1921,7 +1921,8 @@ dpif_netdev_flow_from_nlattrs(const struct nlattr *key, uint32_t key_len,
     }
 
     /* Userspace datapath doesn't support conntrack. */
-    if (flow->ct_state || flow->ct_zone || flow->ct_mark) {
+    if (flow->ct_state || flow->ct_zone || flow->ct_mark
+        || !ovs_u128_is_zero(&flow->ct_label)) {
         return EINVAL;
     }
 
diff --git a/lib/flow.c b/lib/flow.c
index 1e2d2ab..d5dcb92 100644
--- a/lib/flow.c
+++ b/lib/flow.c
@@ -486,6 +486,11 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
     if (md->ct_state) {
         miniflow_push_uint32(mf, ct_mark, md->ct_mark);
         miniflow_pad_to_64(mf, pad1);
+
+        if (!ovs_u128_is_zero(&md->ct_label)) {
+            miniflow_push_words(mf, ct_label, &md->ct_label,
+                                sizeof md->ct_label / sizeof(uint64_t));
+        }
     }
 
     /* Initialize packet's layer pointer and offsets. */
@@ -847,6 +852,9 @@ flow_get_metadata(const struct flow *flow, struct match *flow_metadata)
     if (flow->ct_mark != 0) {
         match_set_ct_mark(flow_metadata, flow->ct_mark);
     }
+    if (!ovs_u128_is_zero(&flow->ct_label)) {
+        match_set_ct_label(flow_metadata, flow->ct_label);
+    }
 }
 
 const char *ct_state_to_string(uint32_t state)
@@ -1151,6 +1159,9 @@ flow_format(struct ds *ds, const struct flow *flow)
     if (!flow->ct_mark) {
         WC_UNMASK_FIELD(wc, ct_mark);
     }
+    if (ovs_u128_is_zero(&flow->ct_label)) {
+        WC_UNMASK_FIELD(wc, ct_label);
+    }
     for (int i = 0; i < FLOW_N_REGS; i++) {
         if (!flow->regs[i]) {
             WC_UNMASK_FIELD(wc, regs[i]);
@@ -1228,6 +1239,7 @@ void flow_wildcards_init_for_packet(struct flow_wildcards *wc,
     WC_MASK_FIELD(wc, ct_state);
     WC_MASK_FIELD(wc, ct_zone);
     WC_MASK_FIELD(wc, ct_mark);
+    WC_MASK_FIELD(wc, ct_label);
     WC_MASK_FIELD(wc, recirc_id);
     WC_MASK_FIELD(wc, dp_hash);
     WC_MASK_FIELD(wc, in_port);
@@ -1334,6 +1346,7 @@ flow_wc_map(const struct flow *flow, struct flowmap *map)
     FLOWMAP_SET(map, ct_state);
     FLOWMAP_SET(map, ct_zone);
     FLOWMAP_SET(map, ct_mark);
+    FLOWMAP_SET(map, ct_label);
 
     /* Ethertype-dependent fields. */
     if (OVS_LIKELY(flow->dl_type == htons(ETH_TYPE_IP))) {
diff --git a/lib/flow.h b/lib/flow.h
index 1ca0a1d..efd34a0 100644
--- a/lib/flow.h
+++ b/lib/flow.h
@@ -106,6 +106,7 @@ struct flow {
     uint16_t ct_zone;           /* Connection tracking zone. */
     uint32_t ct_mark;           /* Connection mark.*/
     uint8_t pad1[4];            /* Pad to 64 bits. */
+    ovs_u128 ct_label;          /* Connection label. */
     uint32_t conj_id;           /* Conjunction ID. */
     ofp_port_t actset_output;   /* Output port in action set. */
     uint8_t pad2[2];            /* Pad to 64 bits. */
@@ -157,7 +158,7 @@ BUILD_ASSERT_DECL(sizeof(struct flow_tnl) % 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) + 200
+                  == sizeof(struct flow_tnl) + 216
                   && FLOW_WC_SEQ == 34);
 
 /* Incremental points at which flow classification may be performed in
@@ -787,6 +788,15 @@ miniflow_get__(const struct miniflow *mf, size_t idx)
      [FLOW_U64_OFFREM(FIELD) / sizeof(TYPE)]                            \
      : 0)
 
+/* Get a pointer to the ovs_u128 value of struct flow 'FIELD' from miniflow
+ * 'FLOW'. */
+#define MINIFLOW_GET_U128_PTR(FLOW, FIELD)                              \
+    ((MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD))                     \
+      && (MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD) + 1)))           \
+     ? &((OVS_FORCE const ovs_u128 *)miniflow_get__(FLOW, FLOW_U64_OFFSET(FIELD))) \
+     [FLOW_U64_OFFREM(FIELD) / sizeof(ovs_u128)]                        \
+     : NULL)
+
 #define MINIFLOW_GET_U8(FLOW, FIELD)            \
     MINIFLOW_GET_TYPE(FLOW, uint8_t, FIELD)
 #define MINIFLOW_GET_U16(FLOW, FIELD)           \
@@ -988,6 +998,7 @@ pkt_metadata_from_flow(struct pkt_metadata *md, const struct flow *flow)
     md->ct_state = flow->ct_state;
     md->ct_zone = flow->ct_zone;
     md->ct_mark = flow->ct_mark;
+    md->ct_label = flow->ct_label;
 }
 
 static inline bool is_ip_any(const struct flow *flow)
diff --git a/lib/match.c b/lib/match.c
index dc47168..d22772b 100644
--- a/lib/match.c
+++ b/lib/match.c
@@ -319,6 +319,24 @@ match_set_ct_mark_masked(struct match *match, uint32_t ct_mark,
 }
 
 void
+match_set_ct_label(struct match *match, ovs_u128 ct_label)
+{
+    ovs_u128 mask;
+
+    mask.u64.lo = UINT64_MAX;
+    mask.u64.hi = UINT64_MAX;
+    match_set_ct_label_masked(match, ct_label, mask);
+}
+
+void
+match_set_ct_label_masked(struct match *match, ovs_u128 value, ovs_u128 mask)
+{
+    match->flow.ct_label.u64.lo = value.u64.lo & mask.u64.lo;
+    match->flow.ct_label.u64.hi = value.u64.hi & mask.u64.hi;
+    match->wc.masks.ct_label = mask;
+}
+
+void
 match_set_dl_type(struct match *match, ovs_be16 dl_type)
 {
     match->wc.masks.dl_type = OVS_BE16_MAX;
@@ -957,6 +975,24 @@ format_flow_tunnel(struct ds *s, const struct match *match)
     tun_metadata_match_format(s, match);
 }
 
+static void
+format_ct_label_masked(struct ds *s, const ovs_u128 *key, const ovs_u128 *mask)
+{
+    if (!ovs_u128_is_zero(mask)) {
+        ovs_be128 value;
+
+        hton128(key, &value);
+        ds_put_format(s, "ct_label=");
+        ds_put_hex(s, &value, sizeof value);
+        if (!is_all_ones(mask, sizeof(*mask))) {
+            hton128(mask, &value);
+            ds_put_char(s, '/');
+            ds_put_hex(s, &value, sizeof value);
+        }
+        ds_put_char(s, ',');
+    }
+}
+
 /* Appends a string representation of 'match' to 's'.  If 'priority' is
  * different from OFP_DEFAULT_PRIORITY, includes it in 's'. */
 void
@@ -1025,6 +1061,10 @@ match_format(const struct match *match, struct ds *s, int priority)
         format_uint32_masked(s, "ct_mark", f->ct_mark, wc->masks.ct_mark);
     }
 
+    if (!ovs_u128_is_zero(&wc->masks.ct_label)) {
+        format_ct_label_masked(s, &f->ct_label, &wc->masks.ct_label);
+    }
+
     if (wc->masks.dl_type) {
         skip_type = true;
         if (f->dl_type == htons(ETH_TYPE_IP)) {
diff --git a/lib/match.h b/lib/match.h
index 17c0879..75e5925 100644
--- a/lib/match.h
+++ b/lib/match.h
@@ -88,6 +88,8 @@ void match_set_ct_state_masked(struct match *, uint32_t ct_state, uint32_t mask)
 void match_set_ct_zone(struct match *, uint16_t ct_zone);
 void match_set_ct_mark(struct match *, uint32_t ct_mark);
 void match_set_ct_mark_masked(struct match *, uint32_t ct_mark, uint32_t mask);
+void match_set_ct_label(struct match *, ovs_u128 ct_label);
+void match_set_ct_label_masked(struct match *, ovs_u128 ct_label, ovs_u128 mask);
 void match_set_skb_priority(struct match *, uint32_t skb_priority);
 void match_set_dl_type(struct match *, ovs_be16);
 void match_set_dl_src(struct match *, const struct eth_addr );
diff --git a/lib/meta-flow.c b/lib/meta-flow.c
index 084a572..6e63465 100644
--- a/lib/meta-flow.c
+++ b/lib/meta-flow.c
@@ -220,6 +220,8 @@ mf_is_all_wild(const struct mf_field *mf, const struct flow_wildcards *wc)
         return !wc->masks.ct_zone;
     case MFF_CT_MARK:
         return !wc->masks.ct_mark;
+    case MFF_CT_LABEL:
+        return ovs_u128_is_zero(&wc->masks.ct_label);
     CASE_MFF_REGS:
         return !wc->masks.regs[mf->id - MFF_REG0];
     CASE_MFF_XREGS:
@@ -505,6 +507,7 @@ mf_is_value_valid(const struct mf_field *mf, const union mf_value *value)
     case MFF_PKT_MARK:
     case MFF_CT_ZONE:
     case MFF_CT_MARK:
+    case MFF_CT_LABEL:
     CASE_MFF_REGS:
     CASE_MFF_XREGS:
     case MFF_ETH_SRC:
@@ -667,6 +670,10 @@ mf_get_value(const struct mf_field *mf, const struct flow *flow,
         value->be32 = htonl(flow->ct_mark);
         break;
 
+    case MFF_CT_LABEL:
+        hton128(&flow->ct_label, &value->be128);
+        break;
+
     CASE_MFF_REGS:
         value->be32 = htonl(flow->regs[mf->id - MFF_REG0]);
         break;
@@ -911,6 +918,14 @@ mf_set_value(const struct mf_field *mf,
         match_set_ct_mark(match, ntohl(value->be32));
         break;
 
+    case MFF_CT_LABEL: {
+        ovs_u128 label;
+
+        ntoh128(&value->be128, &label);
+        match_set_ct_label(match, label);
+        break;
+    }
+
     CASE_MFF_REGS:
         match_set_reg(match, mf->id - MFF_REG0, ntohl(value->be32));
         break;
@@ -1207,6 +1222,10 @@ mf_set_flow_value(const struct mf_field *mf,
         flow->ct_mark = ntohl(value->be32);
         break;
 
+    case MFF_CT_LABEL:
+        ntoh128(&value->be128, &flow->ct_label);
+        break;
+
     CASE_MFF_REGS:
         flow->regs[mf->id - MFF_REG0] = ntohl(value->be32);
         break;
@@ -1511,6 +1530,11 @@ mf_set_wild(const struct mf_field *mf, struct match *match, char **err_str)
         match->wc.masks.ct_mark = 0;
         break;
 
+    case MFF_CT_LABEL:
+        memset(&match->flow.ct_label, 0, sizeof(match->flow.ct_label));
+        memset(&match->wc.masks.ct_label, 0, sizeof(match->wc.masks.ct_label));
+        break;
+
     CASE_MFF_REGS:
         match_set_reg_masked(match, mf->id - MFF_REG0, 0, 0);
         break;
@@ -1782,6 +1806,19 @@ mf_set(const struct mf_field *mf,
         match_set_ct_mark_masked(match, ntohl(value->be32), ntohl(mask->be32));
         break;
 
+    case MFF_CT_LABEL: {
+        ovs_u128 hlabel, hmask;
+
+        ntoh128(&value->be128, &hlabel);
+        if (mask) {
+            ntoh128(&mask->be128, &hmask);
+        } else {
+            hmask.u64.lo = hmask.u64.hi = UINT64_MAX;
+        }
+        match_set_ct_label_masked(match, hlabel, hmask);
+        break;
+    }
+
     case MFF_ETH_DST:
         match_set_dl_dst_masked(match, value->mac, mask->mac);
         break;
diff --git a/lib/meta-flow.h b/lib/meta-flow.h
index 65c6eeb..1610abe 100644
--- a/lib/meta-flow.h
+++ b/lib/meta-flow.h
@@ -787,6 +787,25 @@ enum OVS_PACKED_ENUM mf_field_id {
      */
     MFF_CT_MARK,
 
+    /* "ct_label".
+     *
+     * Connection tracking label.  The label is carried with the
+     * connection tracking state.  On Linux this is held in the
+     * conntrack label extension but the exact implementation is
+     * platform-dependent.
+     *
+     * Writable only from nested actions within the NXAST_CT action.
+     *
+     * Type: be128.
+     * Maskable: bitwise.
+     * Formatting: hexadecimal.
+     * Prerequisites: none.
+     * Access: read/write.
+     * NXM: NXM_NX_CT_LABEL(108) since v2.5.
+     * OXM: none.
+     */
+    MFF_CT_LABEL,
+
 #if FLOW_N_REGS == 8
     /* "reg<N>".
      *
diff --git a/lib/nx-match.c b/lib/nx-match.c
index 094a975..7a343da 100644
--- a/lib/nx-match.c
+++ b/lib/nx-match.c
@@ -781,6 +781,18 @@ nxm_put_frag(struct ofpbuf *b, const struct match *match,
                nw_frag_mask == FLOW_NW_FRAG_MASK ? UINT8_MAX : nw_frag_mask);
 }
 
+static void
+nxm_put_ct_label(struct ofpbuf *b,
+                 enum mf_field_id field, enum ofp_version version,
+                 const ovs_u128 value, const ovs_u128 mask)
+{
+    ovs_be128 bevalue, bemask;
+
+    hton128(&value, &bevalue);
+    hton128(&mask, &bemask);
+    nxm_put(b, field, version, &bevalue, &bemask, sizeof(bevalue));
+}
+
 /* Appends to 'b' a set of OXM or NXM matches for the IPv4 or IPv6 fields in
  * 'match'.  */
 static void
@@ -1043,6 +1055,8 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
                 htons(match->wc.masks.ct_zone));
     nxm_put_32m(b, MFF_CT_MARK, oxm, htonl(flow->ct_mark),
                 htonl(match->wc.masks.ct_mark));
+    nxm_put_ct_label(b, MFF_CT_LABEL, oxm, flow->ct_label,
+                     match->wc.masks.ct_label);
 
     /* OpenFlow 1.1+ Metadata. */
     nxm_put_64m(b, MFF_METADATA, oxm,
diff --git a/lib/odp-execute.c b/lib/odp-execute.c
index 4d6384e..209512b 100644
--- a/lib/odp-execute.c
+++ b/lib/odp-execute.c
@@ -329,6 +329,7 @@ odp_execute_set_action(struct dp_packet *packet, const struct nlattr *a)
     case OVS_KEY_ATTR_CT_STATE:
     case OVS_KEY_ATTR_CT_ZONE:
     case OVS_KEY_ATTR_CT_MARK:
+    case OVS_KEY_ATTR_CT_LABEL:
     case __OVS_KEY_ATTR_MAX:
     default:
         OVS_NOT_REACHED();
@@ -420,6 +421,7 @@ odp_execute_masked_set_action(struct dp_packet *packet,
     case OVS_KEY_ATTR_CT_STATE:
     case OVS_KEY_ATTR_CT_ZONE:
     case OVS_KEY_ATTR_CT_MARK:
+    case OVS_KEY_ATTR_CT_LABEL:
     case OVS_KEY_ATTR_ENCAP:
     case OVS_KEY_ATTR_ETHERTYPE:
     case OVS_KEY_ATTR_IN_PORT:
diff --git a/lib/odp-util.c b/lib/odp-util.c
index fa2fe73..7f119b5 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -83,6 +83,10 @@ static void format_geneve_opts(const struct geneve_opt *opt,
 static struct nlattr *generate_all_wildcard_mask(const struct attr_len_tbl tbl[],
                                                  int max, struct ofpbuf *,
                                                  const struct nlattr *key);
+static void format_u128(struct ds *ds, const ovs_u128 *value,
+                        const ovs_u128 *mask, bool verbose);
+static int scan_u128(const char *s, ovs_u128 *value, ovs_u128 *mask);
+
 /* Returns one the following for the action with the given OVS_ACTION_ATTR_*
  * 'type':
  *
@@ -138,6 +142,7 @@ ovs_key_attr_to_string(enum ovs_key_attr attr, char *namebuf, size_t bufsize)
     case OVS_KEY_ATTR_CT_STATE: return "ct_state";
     case OVS_KEY_ATTR_CT_ZONE: return "ct_zone";
     case OVS_KEY_ATTR_CT_MARK: return "ct_mark";
+    case OVS_KEY_ATTR_CT_LABEL: return "ct_label";
     case OVS_KEY_ATTR_TUNNEL: return "tunnel";
     case OVS_KEY_ATTR_IN_PORT: return "in_port";
     case OVS_KEY_ATTR_ETHERNET: return "eth";
@@ -541,12 +546,15 @@ static const struct nl_policy ovs_conntrack_policy[] = {
     [OVS_CT_ATTR_ZONE] = { .type = NL_A_U16, .optional = true, },
     [OVS_CT_ATTR_MARK] = { .type = NL_A_UNSPEC, .optional = true,
                            .min_len = sizeof(uint32_t) * 2 },
+    [OVS_CT_ATTR_LABEL] = { .type = NL_A_UNSPEC, .optional = true,
+                            .min_len = sizeof(ovs_u128) * 2 },
 };
 
 static void
 format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
 {
     struct nlattr *a[ARRAY_SIZE(ovs_conntrack_policy)];
+    const ovs_u128 *label;
     const uint32_t *mark;
     uint16_t zone;
     bool commit;
@@ -559,9 +567,10 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     commit = a[OVS_CT_ATTR_COMMIT] ? true : false;
     zone = a[OVS_CT_ATTR_ZONE] ? nl_attr_get_u16(a[OVS_CT_ATTR_ZONE]) : 0;
     mark = a[OVS_CT_ATTR_MARK] ? nl_attr_get(a[OVS_CT_ATTR_MARK]) : NULL;
+    label = a[OVS_CT_ATTR_LABEL] ? nl_attr_get(a[OVS_CT_ATTR_LABEL]): NULL;
 
     ds_put_format(ds, "ct");
-    if (commit || zone || mark) {
+    if (commit || zone || mark || label) {
         ds_put_cstr(ds, "(");
         if (commit) {
             ds_put_format(ds, "commit,");
@@ -573,6 +582,10 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
             ds_put_format(ds, "mark=%#"PRIx32"/%#"PRIx32",", *mark,
                           *(mark + 1));
         }
+        if (label) {
+            ds_put_format(ds, "label=");
+            format_u128(ds, label, label + 1, true);
+        }
         ds_chomp(ds, ',');
         ds_put_cstr(ds, ")");
     }
@@ -1020,9 +1033,15 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             uint32_t value;
             uint32_t mask;
         } ct_mark = { 0, 0 };
+        struct {
+            ovs_u128 value;
+            ovs_u128 mask;
+        } ct_label;
         size_t start;
         char *end;
 
+        memset(&ct_label, 0, sizeof(ct_label));
+
         s += 2;
         if (ovs_scan(s, "(")) {
             s++;
@@ -1054,6 +1073,17 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
                     }
                     continue;
                 }
+                if (ovs_scan(s, "label=%n", &n)) {
+                    int retval;
+
+                    s += n;
+                    retval = scan_u128(s, &ct_label.value, &ct_label.mask);
+                    if (retval < 0) {
+                        return retval;
+                    }
+                    s += retval;
+                    continue;
+                }
 
                 return -EINVAL;
             }
@@ -1071,6 +1101,10 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             nl_msg_put_unspec(actions, OVS_CT_ATTR_MARK, &ct_mark,
                               sizeof(ct_mark));
         }
+        if (!ovs_u128_is_zero(&ct_label.mask)) {
+            nl_msg_put_unspec(actions, OVS_CT_ATTR_LABEL, &ct_label,
+                              sizeof(ct_label));
+        }
         nl_msg_end_nested(actions, start);
     }
 
@@ -1341,6 +1375,7 @@ static const struct attr_len_tbl ovs_flow_key_attr_lens[OVS_KEY_ATTR_MAX + 1] =
     [OVS_KEY_ATTR_CT_STATE]  = { .len = 4 },
     [OVS_KEY_ATTR_CT_ZONE]   = { .len = 2 },
     [OVS_KEY_ATTR_CT_MARK]   = { .len = 4 },
+    [OVS_KEY_ATTR_CT_LABEL]  = { .len = sizeof(struct ovs_key_ct_label) },
 };
 
 /* Returns the correct length of the payload for a flow key attribute of the
@@ -2258,6 +2293,13 @@ format_odp_key_attr(const struct nlattr *a, const struct nlattr *ma,
         }
         break;
 
+    case OVS_KEY_ATTR_CT_LABEL: {
+        const ovs_u128 *value = nl_attr_get(a);
+        const ovs_u128 *mask = ma ? nl_attr_get(ma) : NULL;
+
+        format_u128(ds, value, mask, verbose);
+        break;
+    }
 
     case OVS_KEY_ATTR_TUNNEL:
         format_odp_tun_attr(a, ma, ds, verbose);
@@ -2466,6 +2508,56 @@ generate_all_wildcard_mask(const struct attr_len_tbl tbl[], int max,
     return ofp->base;
 }
 
+static void
+format_u128(struct ds *ds, const ovs_u128 *key, const ovs_u128 *mask,
+            bool verbose)
+{
+    if (verbose || (mask && !ovs_u128_is_zero(mask))) {
+        ovs_be128 value;
+
+        hton128(key, &value);
+        ds_put_hex(ds, &value, sizeof value);
+        if (mask && !(ovs_u128_is_ones(mask))) {
+            hton128(mask, &value);
+            ds_put_char(ds, '/');
+            ds_put_hex(ds, &value, sizeof value);
+        }
+    }
+}
+
+static int
+scan_u128(const char *s_, ovs_u128 *value, ovs_u128 *mask)
+{
+    char *s = CONST_CAST(char *, s_);
+    ovs_be128 be_value;
+    ovs_be128 be_mask;
+
+    if (!parse_int_string(s, (uint8_t *)&be_value, sizeof be_value, &s)) {
+        ntoh128(&be_value, value);
+
+        if (mask) {
+            int n;
+
+            if (ovs_scan(s, "/%n", &n)) {
+                int error;
+
+                s += n;
+                error = parse_int_string(s, (uint8_t *)&be_mask,
+                                         sizeof be_mask, &s);
+                if (error) {
+                    return error;
+                }
+                ntoh128(&be_mask, mask);
+            } else {
+                *mask = OVS_U128_MAX;
+            }
+        }
+        return s - s_;
+    }
+
+    return 0;
+}
+
 int
 odp_ufid_from_string(const char *s_, ovs_u128 *ufid)
 {
@@ -3371,6 +3463,7 @@ parse_odp_key_mask_attr(const char *s, const struct simap *port_names,
     SCAN_SINGLE("ct_state(", uint32_t, ct_state, OVS_KEY_ATTR_CT_STATE);
     SCAN_SINGLE("ct_zone(", uint16_t, u16, OVS_KEY_ATTR_CT_ZONE);
     SCAN_SINGLE("ct_mark(", uint32_t, u32, OVS_KEY_ATTR_CT_MARK);
+    SCAN_SINGLE("ct_label(", ovs_u128, u128, OVS_KEY_ATTR_CT_LABEL);
 
     SCAN_BEGIN_NESTED("tunnel(", OVS_KEY_ATTR_TUNNEL) {
         SCAN_FIELD_NESTED("tun_id=", ovs_be64, be64, OVS_TUNNEL_KEY_ATTR_ID);
@@ -3677,6 +3770,10 @@ odp_flow_key_from_flow__(const struct odp_flow_key_parms *parms,
     if (parms->support.ct_mark) {
         nl_msg_put_u32(buf, OVS_KEY_ATTR_CT_MARK, data->ct_mark);
     }
+    if (parms->support.ct_label) {
+        nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_LABEL, &data->ct_label,
+                          sizeof(data->ct_label));
+    }
     if (parms->support.recirc) {
         nl_msg_put_u32(buf, OVS_KEY_ATTR_RECIRC_ID, data->recirc_id);
         nl_msg_put_u32(buf, OVS_KEY_ATTR_DP_HASH, data->dp_hash);
@@ -3868,6 +3965,10 @@ odp_key_from_pkt_metadata(struct ofpbuf *buf, const struct pkt_metadata *md)
         if (md->ct_mark) {
             nl_msg_put_u32(buf, OVS_KEY_ATTR_CT_MARK, md->ct_mark);
         }
+        if (!ovs_u128_is_zero(&md->ct_label)) {
+            nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_LABEL, &md->ct_label,
+                              sizeof(md->ct_label));
+        }
     }
 
     /* Add an ingress port attribute if 'odp_in_port' is not the magical
@@ -3929,6 +4030,13 @@ odp_key_to_pkt_metadata(const struct nlattr *key, size_t key_len,
             md->ct_mark = nl_attr_get_u32(nla);
             wanted_attrs &= ~(1u << OVS_KEY_ATTR_CT_MARK);
             break;
+        case OVS_KEY_ATTR_CT_LABEL: {
+            const ovs_u128 *cl = nl_attr_get(nla);
+
+            md->ct_label = *cl;
+            wanted_attrs &= ~(1u << OVS_KEY_ATTR_CT_LABEL);
+            break;
+        }
         case OVS_KEY_ATTR_TUNNEL: {
             enum odp_key_fitness res;
 
@@ -4497,6 +4605,12 @@ odp_flow_key_to_flow__(const struct nlattr *key, size_t key_len,
         flow->ct_mark = nl_attr_get_u32(attrs[OVS_KEY_ATTR_CT_MARK]);
         expected_attrs |= UINT64_C(1) << OVS_KEY_ATTR_CT_MARK;
     }
+    if (present_attrs & (UINT64_C(1) << OVS_KEY_ATTR_CT_LABEL)) {
+        const ovs_u128 *cl = nl_attr_get(attrs[OVS_KEY_ATTR_CT_LABEL]);
+
+        flow->ct_label = *cl;
+        expected_attrs |= UINT64_C(1) << OVS_KEY_ATTR_CT_LABEL;
+    }
 
     if (present_attrs & (UINT64_C(1) << OVS_KEY_ATTR_TUNNEL)) {
         enum odp_key_fitness res;
diff --git a/lib/odp-util.h b/lib/odp-util.h
index 1bb1484..1e49893 100644
--- a/lib/odp-util.h
+++ b/lib/odp-util.h
@@ -123,6 +123,7 @@ void odp_portno_names_destroy(struct hmap *portno_names);
  *  OVS_KEY_ATTR_CT_STATE                4    --     4      8
  *  OVS_KEY_ATTR_CT_ZONE                 2     2     4      8
  *  OVS_KEY_ATTR_CT_MARK                 4    --     4      8
+ *  OVS_KEY_ATTR_CT_LABEL               16    --     4     20
  *  OVS_KEY_ATTR_ETHERNET               12    --     4     16
  *  OVS_KEY_ATTR_ETHERTYPE               2     2     4      8  (outer VLAN ethertype)
  *  OVS_KEY_ATTR_VLAN                    2     2     4      8
@@ -132,7 +133,7 @@ void odp_portno_names_destroy(struct hmap *portno_names);
  *  OVS_KEY_ATTR_ICMPV6                  2     2     4      8
  *  OVS_KEY_ATTR_ND                     28    --     4     32
  *  ----------------------------------------------------------
- *  total                                                 512
+ *  total                                                 532
  *
  * We include some slack space in case the calculation isn't quite right or we
  * add another field and forget to adjust this value.
@@ -174,6 +175,7 @@ struct odp_support {
     bool ct_state;
     bool ct_zone;
     bool ct_mark;
+    bool ct_label;
 };
 
 struct odp_flow_key_parms {
diff --git a/lib/ofp-actions.c b/lib/ofp-actions.c
index 5b3c392..c2b37e8 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -6258,7 +6258,7 @@ unsupported_nesting(enum ofpact_type action, enum ofpact_type outer_action)
 static bool
 field_requires_ct(enum mf_field_id field)
 {
-    return field == MFF_CT_MARK;
+    return field == MFF_CT_MARK || field == MFF_CT_LABEL;
 }
 
 /* Apply nesting constraints for actions */
diff --git a/lib/packets.h b/lib/packets.h
index 91eb290..a2a4ef8 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -129,6 +129,7 @@ struct pkt_metadata {
     uint16_t ct_state;          /* Connection state. */
     uint16_t ct_zone;           /* Connection zone. */
     uint32_t ct_mark;           /* Connection mark. */
+    ovs_u128 ct_label;          /* Connection label. */
     union flow_in_port in_port; /* Input port. */
     struct flow_tnl tunnel;     /* Encapsulating tunnel parameters. Note that
                                  * if 'ip_dst' == 0, the rest of the fields may
diff --git a/ofproto/ofproto-dpif-sflow.c b/ofproto/ofproto-dpif-sflow.c
index 310bbc5..e21107b 100644
--- a/ofproto/ofproto-dpif-sflow.c
+++ b/ofproto/ofproto-dpif-sflow.c
@@ -1032,6 +1032,7 @@ sflow_read_set_action(const struct nlattr *attr,
     case OVS_KEY_ATTR_CT_STATE:
     case OVS_KEY_ATTR_CT_ZONE:
     case OVS_KEY_ATTR_CT_MARK:
+    case OVS_KEY_ATTR_CT_LABEL:
     case OVS_KEY_ATTR_UNSPEC:
     case __OVS_KEY_ATTR_MAX:
     default:
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index 3955b3a..16e0550 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -2808,6 +2808,7 @@ clear_conntrack(struct flow *flow)
     flow->ct_state = 0;
     flow->ct_zone = 0;
     flow->ct_mark = 0;
+    memset(&flow->ct_label, 0, sizeof flow->ct_label);
 }
 
 static void
@@ -4165,8 +4166,27 @@ put_ct_mark(const struct flow *flow, struct flow *base_flow,
 }
 
 static void
+put_ct_label(const struct flow *flow, struct flow *base_flow,
+             struct ofpbuf *odp_actions, struct flow_wildcards *wc)
+{
+    if (!ovs_u128_is_zero(&wc->masks.ct_label)
+        && !ovs_u128_equals(&flow->ct_label, &base_flow->ct_label)) {
+        struct {
+            ovs_u128 key;
+            ovs_u128 mask;
+        } *odp_ct_label;
+
+        odp_ct_label = nl_msg_put_unspec_uninit(odp_actions, OVS_CT_ATTR_LABEL,
+                                                sizeof(*odp_ct_label));
+        odp_ct_label->key = flow->ct_label;
+        odp_ct_label->mask = wc->masks.ct_label;
+    }
+}
+
+static void
 compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
 {
+    ovs_u128 old_ct_label = ctx->base_flow.ct_label;
     uint32_t old_ct_mark = ctx->base_flow.ct_mark;
     size_t ct_offset;
     uint16_t zone;
@@ -4190,11 +4210,13 @@ compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
     }
     nl_msg_put_u16(ctx->odp_actions, OVS_CT_ATTR_ZONE, zone);
     put_ct_mark(&ctx->xin->flow, &ctx->base_flow, ctx->odp_actions, ctx->wc);
+    put_ct_label(&ctx->xin->flow, &ctx->base_flow, ctx->odp_actions, ctx->wc);
     nl_msg_end_nested(ctx->odp_actions, ct_offset);
 
     /* Restore the original ct fields in the key. These should only be exposed
      * after recirculation to another table. */
     ctx->base_flow.ct_mark = old_ct_mark;
+    ctx->base_flow.ct_label = old_ct_label;
 
     if (ofc->recirc_table == NX_CT_RECIRC_NONE) {
         /* If we do not recirculate as part of this action, hide the results of
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index 6ad9e93..59ae307 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -1270,6 +1270,7 @@ check_##NAME(struct dpif_backer *backer)                                    \
 CHECK_FEATURE(ct_state)
 CHECK_FEATURE(ct_zone)
 CHECK_FEATURE(ct_mark)
+CHECK_FEATURE__(ct_label, ct_label.u64.lo)
 
 #undef CHECK_FEATURE
 #undef CHECK_FEATURE__
@@ -1289,6 +1290,7 @@ check_support(struct dpif_backer *backer)
     backer->support.odp.ct_state = check_ct_state(backer);
     backer->support.odp.ct_zone = check_ct_zone(backer);
     backer->support.odp.ct_mark = check_ct_mark(backer);
+    backer->support.odp.ct_label = check_ct_label(backer);
 }
 
 static int
@@ -4002,19 +4004,27 @@ static enum ofperr
 rule_check(struct rule *rule)
 {
     uint16_t ct_state, ct_zone;
+    const ovs_u128 *labelp;
+    ovs_u128 ct_label = { { 0, 0 } };
     uint32_t ct_mark;
 
     ct_state = MINIFLOW_GET_U16(rule->cr.match.flow, ct_state);
     ct_zone = MINIFLOW_GET_U16(rule->cr.match.flow, ct_zone);
     ct_mark = MINIFLOW_GET_U32(rule->cr.match.flow, ct_mark);
+    labelp = MINIFLOW_GET_U128_PTR(rule->cr.match.flow, ct_label);
+    if (labelp) {
+        ct_label = *labelp;
+    }
 
-    if (ct_state || ct_zone || ct_mark) {
+    if (ct_state || ct_zone || ct_mark
+        || !ovs_u128_is_zero(&ct_label)) {
         struct ofproto_dpif *ofproto = ofproto_dpif_cast(rule->ofproto);
         const struct odp_support *support = &ofproto_dpif_get_support(ofproto)->odp;
 
         if ((ct_state && !support->ct_state)
             || (ct_zone && !support->ct_zone)
-            || (ct_mark && !support->ct_mark)) {
+            || (ct_mark && !support->ct_mark)
+            || (!ovs_u128_is_zero(&ct_label) && !support->ct_label)) {
             return OFPERR_OFPBMC_BAD_FIELD;
         }
         if (ct_state & CS_UNSUPPORTED_MASK) {
diff --git a/ofproto/ofproto-unixctl.man b/ofproto/ofproto-unixctl.man
index 87ef80d..53e5549 100644
--- a/ofproto/ofproto-unixctl.man
+++ b/ofproto/ofproto-unixctl.man
@@ -109,6 +109,8 @@ Connection state of the packet.
 Connection tracking zone for packet.
 .IP \fIct_mark\fR
 Connection mark of the packet.
+.IP \fIct_label\fR
+Connection label of the packet.
 .IP \fItun_id\fR
 The tunnel ID on which the packet arrived.
 .IP \fIin_port\fR
diff --git a/tests/dpif-netdev.at b/tests/dpif-netdev.at
index 502416f..103f87c 100644
--- a/tests/dpif-netdev.at
+++ b/tests/dpif-netdev.at
@@ -82,7 +82,7 @@ AT_CHECK([cat ovs-vswitchd.log | grep -A 1 'miss upcall' | tail -n 1], [0], [dnl
 skb_priority(0),skb_mark(0),recirc_id(0),dp_hash(0),in_port(1),eth(src=50:54:00:00:00:09,dst=50:54:00:00:00:0a),eth_type(0x0800),ipv4(src=10.0.0.2,dst=10.0.0.1,proto=1,tos=0,ttl=64,frag=no),icmp(type=8,code=0)
 ])
 AT_CHECK([cat ovs-vswitchd.log | FILTER_FLOW_INSTALL | STRIP_XOUT], [0], [dnl
-pkt_mark=0,recirc_id=0,dp_hash=0,skb_priority=0,ct_state=0,ct_zone=0,ct_mark=0,icmp,in_port=1,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.0.0.2,nw_dst=10.0.0.1,nw_tos=0,nw_ecn=0,nw_ttl=64,icmp_type=8,icmp_code=0, actions: <del>
+pkt_mark=0,recirc_id=0,dp_hash=0,skb_priority=0,ct_state=0,ct_zone=0,ct_mark=0,ct_label=0,icmp,in_port=1,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.0.0.2,nw_dst=10.0.0.1,nw_tos=0,nw_ecn=0,nw_ttl=64,icmp_type=8,icmp_code=0, actions: <del>
 recirc_id=0,ip,in_port=1,vlan_tci=0x0000/0x1fff,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_frag=no, actions: <del>
 ])
 
diff --git a/tests/odp.at b/tests/odp.at
index 6e51faa..c09d24c 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -71,7 +71,7 @@ s/$/)/' odp-base.txt
 
  echo
  echo '# Valid forms with conntrack fields.'
- sed 's/^/skb_priority(0),skb_mark(0),ct_mark(0x12345678),recirc_id(0),dp_hash(0),/' odp-base.txt
+ sed 's/^/skb_priority(0),skb_mark(0),ct_mark(0x12345678),ct_label(0x1234567890abcdef1234567890abcdef),recirc_id(0),dp_hash(0),/' odp-base.txt
 
  echo
  echo '# Valid forms with IP first fragment.'
@@ -93,7 +93,7 @@ s/^/ODP_FIT_TOO_LITTLE: /
 dnl Some fields are always printed for this test, because wildcards aren't
 dnl specified. We can skip these.
 sed -i 's/\(skb_mark(0)\),\(ct\)/\1,ct_state(0),ct_zone(0),\2/' odp-out.txt
-sed -i 's/\(skb_mark([[^)]]*)\),\(recirc\)/\1,ct_state(0),ct_zone(0),ct_mark(0),\2/' odp-out.txt
+sed -i 's/\(skb_mark([[^)]]*)\),\(recirc\)/\1,ct_state(0),ct_zone(0),ct_mark(0),ct_label(0),\2/' odp-out.txt
 
 AT_CHECK_UNQUOTED([ovstest test-odp parse-keys < odp-in.txt], [0], [`cat odp-out.txt`
 ])
@@ -163,7 +163,7 @@ s/$/)/' odp-base.txt
 
  echo
  echo '# Valid forms with conntrack fields.'
- sed 's/\(eth([[^)]]*)\),/\1,ct_state(+trk),ct_zone(5/0xff),ct_mark(0x12345678\/0xFOFOFOFO),/' odp-base.txt
+ sed 's/\(eth([[^)]]*)\),/\1,ct_state(+trk),ct_zone(5/0xff),ct_mark(0x12345678\/0xFOFOFOFO),,ct_label(0x1234567890ABCDEF1234567890ABCDEF\/0x102030405060708090A0B0C0D0E0F0),/' odp-base.txt
 
  echo
  echo '# Valid forms with IP first fragment.'
@@ -310,6 +310,7 @@ ct
 ct(commit)
 ct(commit,zone=5)
 ct(commit,mark=0xa0a0a0a0/0xfefefefe)
+ct(commit,label=0x1234567890abcdef1234567890abcdef/0xf1f2f3f4f5f6f7f8f9f0fafbfcfdfeff)
 ])
 AT_CHECK_UNQUOTED([ovstest test-odp parse-actions < actions.txt], [0],
   [`cat actions.txt`
diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
index 55eab7f..bc2daf1 100644
--- a/tests/ofproto-dpif.at
+++ b/tests/ofproto-dpif.at
@@ -6523,8 +6523,8 @@ for i in 1 2 3 4; do
 done
 sleep 1
 AT_CHECK([cat ovs-vswitchd.log | STRIP_UFID | FILTER_FLOW_INSTALL | STRIP_USED], [0], [dnl
-pkt_mark=0,recirc_id=0,dp_hash=0,skb_priority=0,ct_state=0,ct_zone=0,ct_mark=0,icmp,in_port=1,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.0.0.2,nw_dst=10.0.0.1,nw_tos=0,nw_ecn=0,nw_ttl=64,icmp_type=8,icmp_code=0, actions:2
-pkt_mark=0,recirc_id=0,dp_hash=0,skb_priority=0,ct_state=0,ct_zone=0,ct_mark=0,icmp,in_port=1,vlan_tci=0x0000,dl_src=50:54:00:00:00:0b,dl_dst=50:54:00:00:00:0c,nw_src=10.0.0.4,nw_dst=10.0.0.3,nw_tos=0,nw_ecn=0,nw_ttl=64,icmp_type=8,icmp_code=0, actions:drop
+pkt_mark=0,recirc_id=0,dp_hash=0,skb_priority=0,ct_state=0,ct_zone=0,ct_mark=0,ct_label=0,icmp,in_port=1,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.0.0.2,nw_dst=10.0.0.1,nw_tos=0,nw_ecn=0,nw_ttl=64,icmp_type=8,icmp_code=0, actions:2
+pkt_mark=0,recirc_id=0,dp_hash=0,skb_priority=0,ct_state=0,ct_zone=0,ct_mark=0,ct_label=0,icmp,in_port=1,vlan_tci=0x0000,dl_src=50:54:00:00:00:0b,dl_dst=50:54:00:00:00:0c,nw_src=10.0.0.4,nw_dst=10.0.0.3,nw_tos=0,nw_ecn=0,nw_ttl=64,icmp_type=8,icmp_code=0, actions:drop
 ])
 AT_CHECK([cat ovs-vswitchd.log | STRIP_UFID | FILTER_FLOW_DUMP | grep 'packets:3'], [0], [dnl
 skb_priority(0),skb_mark(0),recirc_id(0),dp_hash(0),in_port(1),eth(src=50:54:00:00:00:09,dst=50:54:00:00:00:0a),eth_type(0x0800),ipv4(src=10.0.0.2,dst=10.0.0.1,proto=1,tos=0,ttl=64,frag=no),icmp(type=8,code=0), packets:3, bytes:180, used:0.0s, actions:2
diff --git a/tests/ofproto.at b/tests/ofproto.at
index d218256..5e4441c 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -1531,7 +1531,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_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 reg0 reg1 reg2 reg3 reg4 reg5 reg6 reg7 xreg0 xreg1 xreg2 xreg3 eth_src eth_dst vlan_tci vlan_vid vlan_pcp mpls_label mpls_tc 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 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 xreg0 xreg1 xreg2 xreg3 eth_src eth_dst vlan_tci vlan_vid vlan_pcp mpls_label mpls_tc 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 nd_target nd_sll nd_tll
     matching:
       dp_hash: arbitrary mask
       recirc_id: exact match or wildcard
@@ -1614,6 +1614,7 @@ metadata in_port in_port_oxm pkt_mark ct_mark reg0 reg1 reg2 reg3 reg4 reg5 reg6
       ct_state: arbitrary mask
       ct_zone: exact match or wildcard
       ct_mark: arbitrary mask
+      ct_label: arbitrary mask
       reg0: arbitrary mask
       reg1: arbitrary mask
       reg2: arbitrary mask
diff --git a/tests/ovs-ofctl.at b/tests/ovs-ofctl.at
index 1ffc230..33e67ed 100644
--- a/tests/ovs-ofctl.at
+++ b/tests/ovs-ofctl.at
@@ -124,7 +124,9 @@ for test_case in \
     'icmp6,icmpv6_code=2                         NXM,OXM' \
     'ct_state=+trk                               NXM,OXM' \
     'ct_zone=0                                   NXM,OXM' \
-    'ct_mark=0                                   NXM,OXM'
+    'ct_mark=0                                   NXM,OXM' \
+    'ct_label=0                                  NXM,OXM' \
+    'ct_label=0x1234567890ABCDEF12345678         NXM,OXM'
 do
     set $test_case
     echo
@@ -353,6 +355,10 @@ actions=controller(max_len=123,reason=invalid_ttl,id=555)
 actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
 ip,actions=ct(commit,zone=5)
 ip,actions=ct(commit,exec(load(1->NXM_NX_CT_MARK[])))
+ip,actions=ct(commit,exec(load(0x1->NXM_NX_CT_LABEL[])))
+ip,actions=ct(commit,exec(load(0x1234567890ABCDEF->NXM_NX_CT_LABEL[32..95])))
+ip,actions=ct(commit,exec(set_field(0x1->ct_label)))
+ip,ct_state=+trk,ct_label=0x1234567890abcdef12345678,actions=ct(commit)
 ]])
 
 AT_CHECK([ovs-ofctl parse-flows flows.txt
@@ -391,6 +397,10 @@ NXT_FLOW_MOD: ADD table:255 actions=controller(reason=invalid_ttl,max_len=123,id
 NXT_FLOW_MOD: ADD table:255 actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
 NXT_FLOW_MOD: ADD table:255 ip actions=ct(commit,zone=5)
 NXT_FLOW_MOD: ADD table:255 ip actions=ct(commit,exec(load:0x1->NXM_NX_CT_MARK[]))
+NXT_FLOW_MOD: ADD table:255 ip actions=ct(commit,exec(load:0x1->NXM_NX_CT_LABEL[0..63],load:0->NXM_NX_CT_LABEL[64..127]))
+NXT_FLOW_MOD: ADD table:255 ip actions=ct(commit,exec(load:0x1234567890abcdef->NXM_NX_CT_LABEL[32..95]))
+NXT_FLOW_MOD: ADD table:255 ip actions=ct(commit,exec(load:0x1->NXM_NX_CT_LABEL[0..63],load:0->NXM_NX_CT_LABEL[64..127]))
+NXT_FLOW_MOD: ADD table:255 ct_state=+trk,ct_label=0x1234567890abcdef12345678,ip actions=ct(commit)
 ]])
 AT_CLEANUP
 
@@ -427,6 +437,7 @@ dl_dst=aa:bb:cc:dd:ee:ff/00:00:00:00:00:00,actions=drop
 actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
 ip,actions=ct(commit,zone=5)
 ip,actions=ct(commit,exec(load(1->NXM_NX_CT_MARK[[]])))
+ip,actions=ct(commit,exec(load(0x1->NXM_NX_CT_LABEL[[]])))
 ])
 AT_CHECK([ovs-ofctl -F nxm parse-flows flows.txt], [0], [stdout])
 AT_CHECK([[sed 's/ (xid=0x[0-9a-fA-F]*)//' stdout]], [0], [dnl
@@ -462,6 +473,7 @@ NXT_FLOW_MOD: ADD actions=drop
 NXT_FLOW_MOD: ADD actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
 NXT_FLOW_MOD: ADD ip actions=ct(commit,zone=5)
 NXT_FLOW_MOD: ADD ip actions=ct(commit,exec(load:0x1->NXM_NX_CT_MARK[[]]))
+NXT_FLOW_MOD: ADD ip actions=ct(commit,exec(load:0x1->NXM_NX_CT_LABEL[[0..63]],load:0->NXM_NX_CT_LABEL[[64..127]]))
 ])
 AT_CLEANUP
 
@@ -497,6 +509,8 @@ vlan_tci=0x1123/0x1fff,actions=drop
 actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
 ip,actions=ct(commit,zone=5)
 ip,actions=ct(commit,exec(load(1->NXM_NX_CT_MARK[])))
+ip,actions=ct(commit,exec(load(1->NXM_NX_CT_LABEL[])))
+ip,actions=ct(commit,exec(set_field(1->ct_label)))
 ]])
 AT_CHECK([ovs-ofctl -F nxm -mmm parse-flows flows.txt], [0], [stdout], [stderr])
 AT_CHECK([[sed 's/ (xid=0x[0-9a-fA-F]*)//' stdout]], [0],
@@ -531,6 +545,8 @@ NXT_FLOW_MOD: ADD NXM_OF_VLAN_TCI_W(1123/1fff) actions=drop
 NXT_FLOW_MOD: ADD <any> actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
 NXT_FLOW_MOD: ADD NXM_OF_ETH_TYPE(0800) actions=ct(commit,zone=5)
 NXT_FLOW_MOD: ADD NXM_OF_ETH_TYPE(0800) actions=ct(commit,exec(load:0x1->NXM_NX_CT_MARK[]))
+NXT_FLOW_MOD: ADD NXM_OF_ETH_TYPE(0800) actions=ct(commit,exec(load:0x1->NXM_NX_CT_LABEL[0..63],load:0->NXM_NX_CT_LABEL[64..127]))
+NXT_FLOW_MOD: ADD NXM_OF_ETH_TYPE(0800) actions=ct(commit,exec(load:0x1->NXM_NX_CT_LABEL[0..63],load:0->NXM_NX_CT_LABEL[64..127]))
 ]])
 AT_CLEANUP
 
@@ -815,6 +831,8 @@ NXM_OF_ETH_TYPE(0800) NXM_NX_CT_STATE_W(00000080/000000F0)
 NXM_OF_ETH_TYPE(0800) NXM_NX_CT_ZONE(5a5a)
 NXM_OF_ETH_TYPE(0800) NXM_NX_CT_MARK(5a5a5a5a)
 NXM_OF_ETH_TYPE(0800) NXM_NX_CT_MARK_W(5a5a5a5a/fefefefe)
+NXM_OF_ETH_TYPE(0800) NXM_NX_CT_LABEL(1234567890abcdef1234567890abcdef)
+NXM_OF_ETH_TYPE(0800) NXM_NX_CT_LABEL_W(10203040506070809000a0b0c0d0e0f0/f1f2f3f4f5f6f7f8f9f0fafbfcfdfeff)
 
 # dp_hash (testing experimenter OXM).
 NXM_NX_DP_HASH(01234567)
@@ -1118,6 +1136,8 @@ NXM_OF_ETH_TYPE(0800), NXM_NX_CT_STATE_W(00000080/000000f0)
 NXM_OF_ETH_TYPE(0800), NXM_NX_CT_ZONE(5a5a)
 NXM_OF_ETH_TYPE(0800), NXM_NX_CT_MARK(5a5a5a5a)
 NXM_OF_ETH_TYPE(0800), NXM_NX_CT_MARK_W(5a5a5a5a/fefefefe)
+NXM_OF_ETH_TYPE(0800), NXM_NX_CT_LABEL(1234567890abcdef1234567890abcdef)
+NXM_OF_ETH_TYPE(0800), NXM_NX_CT_LABEL_W(10203040506070809000a0b0c0d0e0f0/f1f2f3f4f5f6f7f8f9f0fafbfcfdfeff)
 
 # dp_hash (testing experimenter OXM).
 NXM_NX_DP_HASH(01234567)
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 0b6adf7..71440cc 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -661,6 +661,46 @@ SYN_RECV src=10.1.1.3 dst=10.1.1.4 sport=<cleared> dport=<cleared> src=10.1.1.4
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([conntrack - ct_label])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1, at_ns2, at_ns3)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+ADD_VETH(p2, at_ns2, br0, "10.1.1.3/24")
+ADD_VETH(p3, at_ns3, br0, "10.1.1.4/24")
+
+dnl Allow traffic between ns0<->ns1 using the ct_label.
+dnl Check that different labels do not match for traffic between ns2<->ns3.
+AT_DATA([flows.txt], [dnl
+priority=1,action=drop
+priority=10,arp,action=normal
+priority=10,icmp,action=normal
+priority=100,in_port=1,tcp,action=ct(commit,exec(set_field:0x0a000d000005000001->ct_label)),2
+priority=100,in_port=2,ct_state=-trk,tcp,action=ct(table=0)
+priority=100,in_port=2,ct_state=+trk,ct_label=0x0a000d000005000001,tcp,action=1
+priority=100,in_port=3,tcp,action=ct(commit,exec(set_field:0x2->ct_label)),4
+priority=100,in_port=4,ct_state=-trk,tcp,action=ct(table=0)
+priority=100,in_port=4,ct_state=+trk,ct_label=0x0a000d000005000001,tcp,action=3
+])
+
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+
+dnl HTTP requests from p0->p1 should work fine.
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py]], [http0.pid])
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.2 -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+
+dnl HTTP requests from p2->p3 should fail due to network failure.
+dnl Try 3 times, in 1 second intervals.
+NETNS_DAEMONIZE([at_ns3], [[$PYTHON $srcdir/test-l7.py]], [http1.pid])
+NS_CHECK_EXEC([at_ns2], [wget 10.1.1.4 -t 3 -T 1 -v -o wget1.log], [4])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_SETUP([conntrack - ICMP related])
 CHECK_CONNTRACK()
 OVS_TRAFFIC_VSWITCHD_START(
diff --git a/tests/test-odp.c b/tests/test-odp.c
index 245e1f9..cdc761f 100644
--- a/tests/test-odp.c
+++ b/tests/test-odp.c
@@ -61,6 +61,7 @@ parse_keys(bool wc_keys)
                     .ct_state = true,
                     .ct_zone = true,
                     .ct_mark = true,
+                    .ct_label = true,
                 },
             };
 
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index f4436b8..fcb47d8 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -1396,6 +1396,11 @@ Matches the given 32-bit connection mark \fIvalue\fR either exactly or with
 optional \fImask\fR. This represents metadata associated with the connection
 that the current packet is part of. Introduced in Open vSwitch 2.5.
 .
+.IP \fBct_label=\fIvalue\fR[\fB/\fImask\fR]
+Matches the given 128-bit connection labels \fIvalue\fR either exactly or with
+optional \fImask\fR. This represents metadata associated with the connection
+that the current packet is part of. Introduced in Open vSwitch 2.5.
+.
 .PP
 Defining IPv6 flows (those with \fBdl_type\fR equal to 0x86dd) requires
 support for NXM.  The following shorthand notations are available for
@@ -1678,6 +1683,11 @@ Store a 32-bit metadata value with the connection. If the connection is
 committed, then subsequent lookups for packets in this connection will
 populate the \fBct_mark\fR flow field when the packet is sent to the
 connection tracker with the \fBtable\fR specified.
+.IP \fBset_field:\fIvalue\fR->ct_label\fR
+Store a 128-bit metadata value with the connection.  If the connection is
+committed, then subsequent lookups for packets in this connection will
+populate the \fBct_label\fR flow field when the packet is sent to the
+connection tracker with the \fBtable\fR specified.
 .RE
 .IP
 The \fBcommit\fR parameter must be specified to use \fBexec(...)\fR.
