[ovs-dev,PATCHv2,4/6] Add connection tracking label support.
diff mbox

Message ID 1442531068-41423-5-git-send-email-joestringer@nicira.com
State Changes Requested
Headers show

Commit Message

Joe Stringer Sept. 17, 2015, 11:04 p.m. UTC
This patch adds a new 128-bit metadata field to the connection tracking
interface. When a label is specified as part of the ct action and the
connection is committed, the value is saved with the current connection.
Subsequent ct lookups with the table specified will expose this metadata
as the "ct_label" field in the flow.

For example, to allow new connections from port 1->2 and only allow
established connections from port 2->1, and to associate a label with
those connections:

    priority=1,action=drop
    priority=10,arp,action=normal
    priority=10,icmp,action=normal
    in_port=1,tcp,action=ct(commit,exec(set_field:1->ct_label)),2
    in_port=2,ct_state=-trk,tcp,action=ct(table=1)
    table=1,in_port=2,ct_state=+trk,ct_label=1,tcp,action=1

Signed-off-by: Joe Stringer <joestringer@nicira.com>
Acked-by: Jarno Rajahalme <jrajahalme@nicira.com>
---
v2: Address feedback from v1
---
 NEWS                                              |   4 +-
 build-aux/extract-ofp-fields                      |   2 +
 datapath/flow_netlink.c                           |   2 +-
 datapath/linux/compat/include/linux/openvswitch.h |  10 ++
 lib/dpif-netdev.c                                 |   3 +-
 lib/flow.c                                        |  13 +++
 lib/flow.h                                        |  12 ++-
 lib/match.c                                       |  37 +++++++
 lib/match.h                                       |   2 +
 lib/meta-flow.c                                   |  71 ++++++++++++++
 lib/meta-flow.h                                   |  21 ++++
 lib/nx-match.c                                    |  12 +++
 lib/odp-execute.c                                 |   2 +
 lib/odp-util.c                                    | 113 +++++++++++++++++++++-
 lib/odp-util.h                                    |   4 +-
 lib/ofp-actions.c                                 |  11 ++-
 lib/packets.h                                     |   1 +
 ofproto/ofproto-dpif-sflow.c                      |   1 +
 ofproto/ofproto-dpif-xlate.c                      |  28 ++++++
 ofproto/ofproto-dpif.c                            |  14 ++-
 ofproto/ofproto-unixctl.man                       |   2 +
 tests/dpif-netdev.at                              |   2 +-
 tests/odp.at                                      |   6 +-
 tests/ofproto-dpif.at                             |   4 +-
 tests/ofproto.at                                  |   3 +-
 tests/system-traffic.at                           |  40 ++++++++
 tests/test-odp.c                                  |   1 +
 utilities/ovs-ofctl.8.in                          |   8 ++
 28 files changed, 410 insertions(+), 19 deletions(-)

Comments

Ben Pfaff Sept. 18, 2015, 5:49 p.m. UTC | #1
On Thu, Sep 17, 2015 at 04:04:26PM -0700, Joe Stringer wrote:
> This patch adds a new 128-bit metadata field to the connection tracking
> interface. When a label is specified as part of the ct action and the
> connection is committed, the value is saved with the current connection.
> Subsequent ct lookups with the table specified will expose this metadata
> as the "ct_label" field in the flow.
> 
> For example, to allow new connections from port 1->2 and only allow
> established connections from port 2->1, and to associate a label with
> those connections:
> 
>     priority=1,action=drop
>     priority=10,arp,action=normal
>     priority=10,icmp,action=normal
>     in_port=1,tcp,action=ct(commit,exec(set_field:1->ct_label)),2
>     in_port=2,ct_state=-trk,tcp,action=ct(table=1)
>     table=1,in_port=2,ct_state=+trk,ct_label=1,tcp,action=1
> 
> Signed-off-by: Joe Stringer <joestringer@nicira.com>
> Acked-by: Jarno Rajahalme <jrajahalme@nicira.com>
> ---
> v2: Address feedback from v1

MINIFLOW_GET_U128_PTR seems risky.  How you can be sure that both 64-bit
components of the u128 are present?

Is the formatting and parsing of ovs_u128 in lib/meta-flow.[ch]
consistent with what we're doing elsewhere?  It essentially treats the
bits in an ovs_u128 as if they were a (putative) ovs_be128; that is,
it's always big-endian.  I guess that's OK as long as we're consistent
throughout the code.

The wire format name though (in meta-flow.h etc) should probably not be
u128, because that implies that it's in host byte order.

I wonder whether ovs_u128 should actually be ovs_be128, with each of the
components defined as big-endian too.  Do we ever actually use an
ovs_u128 as an integer?  If not, then having it as big-endian would make
it a lot easier to think about (at least for me).

Thanks,

Ben.
Joe Stringer Sept. 23, 2015, 12:34 a.m. UTC | #2
On 18 September 2015 at 10:49, Ben Pfaff <blp@nicira.com> wrote:
> On Thu, Sep 17, 2015 at 04:04:26PM -0700, Joe Stringer wrote:
>> This patch adds a new 128-bit metadata field to the connection tracking
>> interface. When a label is specified as part of the ct action and the
>> connection is committed, the value is saved with the current connection.
>> Subsequent ct lookups with the table specified will expose this metadata
>> as the "ct_label" field in the flow.
>>
>> For example, to allow new connections from port 1->2 and only allow
>> established connections from port 2->1, and to associate a label with
>> those connections:
>>
>>     priority=1,action=drop
>>     priority=10,arp,action=normal
>>     priority=10,icmp,action=normal
>>     in_port=1,tcp,action=ct(commit,exec(set_field:1->ct_label)),2
>>     in_port=2,ct_state=-trk,tcp,action=ct(table=1)
>>     table=1,in_port=2,ct_state=+trk,ct_label=1,tcp,action=1
>>
>> Signed-off-by: Joe Stringer <joestringer@nicira.com>
>> Acked-by: Jarno Rajahalme <jrajahalme@nicira.com>
>> ---
>> v2: Address feedback from v1
>
> MINIFLOW_GET_U128_PTR seems risky.  How you can be sure that both 64-bit
> components of the u128 are present?

Currently we only check the first 64-bit component. Perhaps we could
expand the following:

MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD)) ? ....

to check both pieces:

(MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD))
 && MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD))) ?  ...

> Is the formatting and parsing of ovs_u128 in lib/meta-flow.[ch]
> consistent with what we're doing elsewhere?  It essentially treats the
> bits in an ovs_u128 as if they were a (putative) ovs_be128; that is,
> it's always big-endian.  I guess that's OK as long as we're consistent
> throughout the code.

I don't think there's an "elsewhere" to be consistent with, as such.
This patch introduces the u128 type into meta-flow. In terms of the
kernel interface, the structure for this field is host byte-order;
it's not as though this is actually a field in a packet we will see on
the wire. To be more consistent with the other parts of
meta-flow.[ch], I suppose that we would introduce an ovs_be128 type
rather than using the existing ovs_u128.

> The wire format name though (in meta-flow.h etc) should probably not be
> u128, because that implies that it's in host byte order.
>
> I wonder whether ovs_u128 should actually be ovs_be128, with each of the
> components defined as big-endian too.  Do we ever actually use an
> ovs_u128 as an integer?  If not, then having it as big-endian would make
> it a lot easier to think about (at least for me).

Makes sense. I can follow up on adding an ovs_be128 definition and
translating to host byte order in the usual places to interface with
the kernel.
Joe Stringer Sept. 23, 2015, 12:37 a.m. UTC | #3
On 22 September 2015 at 17:34, Joe Stringer <joestringer@nicira.com> wrote:
> On 18 September 2015 at 10:49, Ben Pfaff <blp@nicira.com> wrote:
>> On Thu, Sep 17, 2015 at 04:04:26PM -0700, Joe Stringer wrote:
>>> This patch adds a new 128-bit metadata field to the connection tracking
>>> interface. When a label is specified as part of the ct action and the
>>> connection is committed, the value is saved with the current connection.
>>> Subsequent ct lookups with the table specified will expose this metadata
>>> as the "ct_label" field in the flow.
>>>
>>> For example, to allow new connections from port 1->2 and only allow
>>> established connections from port 2->1, and to associate a label with
>>> those connections:
>>>
>>>     priority=1,action=drop
>>>     priority=10,arp,action=normal
>>>     priority=10,icmp,action=normal
>>>     in_port=1,tcp,action=ct(commit,exec(set_field:1->ct_label)),2
>>>     in_port=2,ct_state=-trk,tcp,action=ct(table=1)
>>>     table=1,in_port=2,ct_state=+trk,ct_label=1,tcp,action=1
>>>
>>> Signed-off-by: Joe Stringer <joestringer@nicira.com>
>>> Acked-by: Jarno Rajahalme <jrajahalme@nicira.com>
>>> ---
>>> v2: Address feedback from v1
>>
>> MINIFLOW_GET_U128_PTR seems risky.  How you can be sure that both 64-bit
>> components of the u128 are present?
>
> Currently we only check the first 64-bit component. Perhaps we could
> expand the following:
>
> MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD)) ? ....
>
> to check both pieces:
>
> (MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD))
>  && MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD))) ?  ...

I mean..
(MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD))
 && MINIFLOW_IN_MAP(FLOW, FLOW_U64_OFFSET(FIELD)) + 1) ?  ...

Patch
diff mbox

diff --git a/NEWS b/NEWS
index 6eeccdc..7045d72 100644
--- a/NEWS
+++ b/NEWS
@@ -22,8 +22,8 @@  Post-v2.4.0
      a Vagrant box.  See INSTALL.md for details
    - Dropped support for GRE64 tunnel.
    - 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/build-aux/extract-ofp-fields b/build-aux/extract-ofp-fields
index b37f0b8..dfff8ee 100755
--- a/build-aux/extract-ofp-fields
+++ b/build-aux/extract-ofp-fields
@@ -20,11 +20,13 @@  TYPES = {"u8":       (1,   False),
          "MAC":      (6,   False),
          "be64":     (8,   False),
          "IPv6":     (16,  False),
+         "u128":     (16,  False),
          "tunnelMD": (124, True)}
 
 FORMATTING = {"decimal":            ("MFS_DECIMAL",      1,   8),
               "hexadecimal":        ("MFS_HEXADECIMAL",  1, 127),
               "conn state":         ("MFS_CT_STATE",     2,   2),
+              "conn label":         ("MFS_CT_LABEL",    16,  16),
               "Ethernet":           ("MFS_ETHERNET",     6,   6),
               "IPv4":               ("MFS_IPV4",         4,   4),
               "IPv6":               ("MFS_IPV6",        16,  16),
diff --git a/datapath/flow_netlink.c b/datapath/flow_netlink.c
index 4e9547a..3f78589 100644
--- a/datapath/flow_netlink.c
+++ b/datapath/flow_netlink.c
@@ -281,7 +281,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 f6fb3a1..ddbeb32 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,	/* u8 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. */
@@ -660,12 +666,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_FLAGS,      /* u32 bitmask of OVS_CT_F_*. */
 	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 37608bd..5887508 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 3cbf35b..9e0278d 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);
+    }
 }
 
 char *
@@ -1131,6 +1139,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]);
@@ -1208,6 +1219,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);
@@ -1314,6 +1326,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 ceda11b..740bad8 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
@@ -786,6 +787,14 @@  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))                        \
+     ? &((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)           \
@@ -987,6 +996,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 bab0a9a..aada884 100644
--- a/lib/match.c
+++ b/lib/match.c
@@ -319,6 +319,25 @@  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.u64.lo = mask.u64.lo;
+    match->wc.masks.ct_label.u64.hi = mask.u64.hi;
+}
+
+void
 match_set_dl_type(struct match *match, ovs_be16 dl_type)
 {
     match->wc.masks.dl_type = OVS_BE16_MAX;
@@ -957,6 +976,20 @@  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)) {
+        ds_put_format(s, "ct_label=");
+        ds_put_hex(s, key, sizeof(*key));
+        if (!is_all_ones(mask, sizeof(*mask))) {
+            ds_put_char(s, '/');
+            ds_put_hex(s, mask, sizeof(*mask));
+        }
+        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
@@ -1026,6 +1059,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 2b969c4..51dd998 100644
--- a/lib/match.h
+++ b/lib/match.h
@@ -88,6 +88,8 @@  void match_set_ct_state_masked(struct match *, uint16_t ct_state, uint16_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 455b14f..83f89a7 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:
@@ -506,6 +508,7 @@  mf_is_value_valid(const struct mf_field *mf, const union mf_value *value)
     case MFF_CT_STATE:
     case MFF_CT_ZONE:
     case MFF_CT_MARK:
+    case MFF_CT_LABEL:
     CASE_MFF_REGS:
     CASE_MFF_XREGS:
     case MFF_ETH_SRC:
@@ -665,6 +668,10 @@  mf_get_value(const struct mf_field *mf, const struct flow *flow,
         value->be32 = htonl(flow->ct_mark);
         break;
 
+    case MFF_CT_LABEL:
+        value->u128 = flow->ct_label;
+        break;
+
     CASE_MFF_REGS:
         value->be32 = htonl(flow->regs[mf->id - MFF_REG0]);
         break;
@@ -909,6 +916,10 @@  mf_set_value(const struct mf_field *mf,
         match_set_ct_mark(match, ntohl(value->be32));
         break;
 
+    case MFF_CT_LABEL:
+        match_set_ct_label(match, value->u128);
+        break;
+
     CASE_MFF_REGS:
         match_set_reg(match, mf->id - MFF_REG0, ntohl(value->be32));
         break;
@@ -1205,6 +1216,10 @@  mf_set_flow_value(const struct mf_field *mf,
         flow->ct_mark = ntohl(value->be32);
         break;
 
+    case MFF_CT_LABEL:
+        flow->ct_label = value->u128;
+        break;
+
     CASE_MFF_REGS:
         flow->regs[mf->id - MFF_REG0] = ntohl(value->be32);
         break;
@@ -1509,6 +1524,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;
@@ -1780,6 +1800,10 @@  mf_set(const struct mf_field *mf,
         match_set_ct_mark_masked(match, ntohl(value->be32), ntohl(mask->be32));
         break;
 
+    case MFF_CT_LABEL:
+        match_set_ct_label_masked(match, value->u128, mask->u128);
+        break;
+
     case MFF_ETH_DST:
         match_set_dl_dst_masked(match, value->mac, mask->mac);
         break;
@@ -1971,6 +1995,32 @@  syntax_error:
 }
 
 static char *
+mf_from_u128_string(const struct mf_field *mf, const char *s_,
+                    ovs_u128 *valuep, ovs_u128 *maskp)
+{
+    char *s = CONST_CAST(char *, s_);
+
+    ovs_assert(mf->n_bytes == sizeof(*valuep));
+
+    if (!parse_int_string(s, (uint8_t *)valuep, sizeof(*valuep), &s)) {
+        if (strlen(s)) {
+            if (*s == '/'
+                && !parse_int_string(s + 1, (uint8_t *)maskp, sizeof(*maskp),
+                                     &s)) {
+                return NULL;
+            } else {
+                /* parse error */
+            }
+        } else {
+            memset(maskp, 0xff, sizeof(*maskp));
+            return NULL;
+        }
+    }
+
+    return xasprintf("%s: invalid u128 for %s", s, mf->name);
+}
+
+static char *
 mf_from_ethernet_string(const struct mf_field *mf, const char *s,
                         struct eth_addr *mac, struct eth_addr *mask)
 {
@@ -2214,6 +2264,11 @@  mf_parse(const struct mf_field *mf, const char *s,
         error = mf_from_ct_state_string(s, &value->be16, &mask->be16);
         break;
 
+    case MFS_CT_LABEL:
+        ovs_assert(mf->n_bytes == sizeof(ovs_u128));
+        error = mf_from_u128_string(mf, s, &value->u128, &mask->u128);
+        break;
+
     case MFS_ETHERNET:
         error = mf_from_ethernet_string(mf, s, &value->mac, &mask->mac);
         break;
@@ -2341,6 +2396,18 @@  mf_format_ct_state_string(ovs_be16 value, ovs_be16 mask, struct ds *s)
                         ntohs(mask), UINT16_MAX);
 }
 
+static void
+mf_format_ct_label_string(const ovs_u128 *value, const ovs_u128 *mask,
+                            struct ds *s)
+{
+    ds_put_format(s, "ct_label=");
+    ds_put_hex(s, value, sizeof(*value));
+    if (mask) {
+        ds_put_char(s, '/');
+        ds_put_hex(s, mask, sizeof(*mask));
+    }
+}
+
 /* Appends to 's' a string representation of field 'mf' whose value is in
  * 'value' and 'mask'.  'mask' may be NULL to indicate an exact match. */
 void
@@ -2382,6 +2449,10 @@  mf_format(const struct mf_field *mf,
                                   mask ? mask->be16 : OVS_BE16_MAX, s);
         break;
 
+    case MFS_CT_LABEL:
+        mf_format_ct_label_string(&value->u128, (ovs_u128 *)mask, s);
+        break;
+
     case MFS_ETHERNET:
         eth_format_masked(value->mac, mask ? &mask->mac : NULL, s);
         break;
diff --git a/lib/meta-flow.h b/lib/meta-flow.h
index 4423dd1..55e4e49 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: u128.
+     * Maskable: bitwise.
+     * Formatting: conn label.
+     * 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>".
      *
@@ -1764,6 +1783,7 @@  enum OVS_PACKED_ENUM mf_string {
 
     /* Other formats. */
     MFS_CT_STATE,               /* Connection tracking state */
+    MFS_CT_LABEL,               /* Connection tracking label */
     MFS_ETHERNET,
     MFS_IPV4,
     MFS_IPV6,
@@ -1831,6 +1851,7 @@  union mf_value {
     ovs_be32 be32;
     ovs_be16 be16;
     uint8_t u8;
+    ovs_u128 u128;
 };
 BUILD_ASSERT_DECL(sizeof(union mf_value) == 128);
 BUILD_ASSERT_DECL(sizeof(union mf_value) >= GENEVE_MAX_OPT_SIZE);
diff --git a/lib/nx-match.c b/lib/nx-match.c
index 4b384de..abe10c3 100644
--- a/lib/nx-match.c
+++ b/lib/nx-match.c
@@ -781,6 +781,14 @@  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)
+{
+    nxm_put(b, field, version, &value, &mask, sizeof(value));
+}
+
 /* Appends to 'b' a set of OXM or NXM matches for the IPv4 or IPv6 fields in
  * 'match'.  */
 static void
@@ -1049,6 +1057,10 @@  nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
         nxm_put_32m(b, MFF_CT_MARK, oxm, htonl(flow->ct_mark),
                     htonl(match->wc.masks.ct_mark));
     }
+    if (!ovs_u128_is_zero(&match->wc.masks.ct_label)) {
+        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 3784b0f..2dd23b0 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -138,6 +138,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";
@@ -543,12 +544,15 @@  static const struct nl_policy ovs_conntrack_policy[] = {
                             .min_len = sizeof(uint16_t)},
     [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;
     uint32_t flags;
     uint16_t zone;
@@ -561,9 +565,10 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     flags = a[OVS_CT_ATTR_FLAGS] ? nl_attr_get_u32(a[OVS_CT_ATTR_FLAGS]) : 0;
     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 (flags || zone || mark) {
+    if (flags || zone || mark || label) {
         ds_put_cstr(ds, "(");
         if (flags & OVS_CT_F_COMMIT) {
             ds_put_format(ds, "commit");
@@ -581,6 +586,15 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
             ds_put_format(ds, "mark=%"PRIx32"/%"PRIx32, *mark,
                           *(mark + 1));
         }
+        if (label) {
+            if (ds_last(ds) != '(') {
+                ds_put_char(ds, ',');
+            }
+            ds_put_format(ds, "label=");
+            ds_put_hex(ds, label, sizeof(*label));
+            ds_put_char(ds, '/');
+            ds_put_hex(ds, (label + 1), sizeof(*label));
+        }
         ds_put_cstr(ds, ")");
     }
 }
@@ -1016,6 +1030,23 @@  ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
 }
 
 static int
+parse_ct_label(const char *s, ovs_u128 *value, ovs_u128 *mask, char **tail)
+{
+    if (!parse_int_string(s, (uint8_t *)value, sizeof(*value), tail)) {
+        if (**tail == '/') {
+            if (parse_int_string(s + 1, (uint8_t *)mask, sizeof(mask), tail)) {
+                return -EINVAL;
+            }
+        } else {
+            memset(mask, 0xff, sizeof(*mask));
+        }
+        return 0;
+    }
+
+    return -EINVAL;
+}
+
+static int
 parse_conntrack_action(const char *s_, struct ofpbuf *actions)
 {
     const char *s = s_;
@@ -1027,9 +1058,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++;
@@ -1039,6 +1076,7 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             }
 
             while (s != end) {
+                char *tail;
                 int n = -1;
 
                 s += strspn(s, delimiters);
@@ -1061,6 +1099,18 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
                     }
                     continue;
                 }
+                if (ovs_scan(s, "label=%n", &n)) {
+                    int error;
+
+                    s += n;
+                    error = parse_ct_label(s, &ct_label.value, &ct_label.mask,
+                                           &tail);
+                    if (error) {
+                        return error;
+                    }
+                    s = tail;
+                    continue;
+                }
 
                 if (n < 0) {
                     return -EINVAL;
@@ -1080,6 +1130,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);
     }
 
@@ -1350,6 +1404,7 @@  static const struct attr_len_tbl ovs_flow_key_attr_lens[OVS_KEY_ATTR_MAX + 1] =
     [OVS_KEY_ATTR_CT_STATE]  = { .len = 1 },
     [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
@@ -2237,6 +2292,18 @@  format_odp_key_attr(const struct nlattr *a, const struct nlattr *ma,
         }
         break;
 
+    case OVS_KEY_ATTR_CT_LABEL: {
+        const struct ovs_key_ct_label *mask = ma ? nl_attr_get(ma) : NULL;
+
+        if (verbose || (mask && !is_all_zeros(mask, sizeof(*mask)))) {
+            ds_put_hex(ds, nl_attr_get(a), nl_attr_get_size(a));
+            if (mask && !is_exact) {
+                ds_put_char(ds, '/');
+                ds_put_hex(ds, MASK(mask, ct_label), sizeof(*mask));
+            }
+        }
+        break;
+    }
 
     case OVS_KEY_ATTR_TUNNEL:
         format_odp_tun_attr(a, ma, ds, verbose);
@@ -2445,6 +2512,28 @@  generate_all_wildcard_mask(const struct attr_len_tbl tbl[], int max,
     return ofp->base;
 }
 
+static int
+scan_u128(const char *s_, ovs_u128 *key, ovs_u128 *mask)
+{
+    char *s = CONST_CAST(char *, s_);
+    int n;
+
+    if (parse_int_string(s, (uint8_t *)key, sizeof(*key), &s)) {
+        return 0;
+    }
+
+    if (ovs_scan(s, "/%n", &n)) {
+        s += n;
+        if (parse_int_string(s, (uint8_t *)mask, sizeof(*mask), &s)) {
+            return 0;
+        }
+    } else {
+        mask->u64.hi = mask->u64.lo = UINT64_MAX;
+    }
+
+    return s - s_;
+}
+
 int
 odp_ufid_from_string(const char *s_, ovs_u128 *ufid)
 {
@@ -3350,6 +3439,7 @@  parse_odp_key_mask_attr(const char *s, const struct simap *port_names,
     SCAN_SINGLE("ct_state(", uint8_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);
@@ -3594,6 +3684,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);
@@ -3784,6 +3878,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
@@ -3845,6 +3943,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;
 
@@ -4411,6 +4516,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 74b07d8..11dbc25 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_CONN_STATE              2     2     4      8
  *  OVS_KEY_ATTR_CONN_ZONE               2     2     4      8
  *  OVS_KEY_ATTR_CONN_MARK               4    --     4      8
+ *  OVS_KEY_ATTR_CONN_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 21d900d..36fa2c3 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -6142,6 +6142,12 @@  unsupported_nesting(enum ofpact_type action, enum ofpact_type outer_action)
     return OFPERR_OFPBAC_BAD_ARGUMENT;
 }
 
+static bool
+field_requires_ct(enum mf_field_id field)
+{
+    return field == MFF_CT_MARK || field == MFF_CT_LABEL;
+}
+
 static enum ofperr
 ofpacts_verify_nested(const struct ofpact *a, enum ofpact_type outer_action)
 {
@@ -6153,12 +6159,11 @@  ofpacts_verify_nested(const struct ofpact *a, enum ofpact_type outer_action)
 
     field = ofpact_get_mf_field(a->type, a);
     if (outer_action == OFPACT_CT
-        && (!field
-            || (field && field->id != MFF_CT_MARK))) {
+        && (!field || (field && !field_requires_ct(field->id)))) {
         return unsupported_nesting(a->type, outer_action);
     }
 
-    if (field && outer_action != OFPACT_CT && field->id == MFF_CT_MARK) {
+    if (outer_action != OFPACT_CT && field && field_requires_ct(field->id)) {
         VLOG_WARN("cannot set CT fields outside of \"ct\" action");
         return OFPERR_OFPBAC_BAD_SET_ARGUMENT;
     }
diff --git a/lib/packets.h b/lib/packets.h
index 1061ee9..cb7ea84 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -130,6 +130,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. */
     struct flow_tnl tunnel;     /* Encapsulating tunnel parameters. Note that
                                  * if 'ip_dst' == 0, the rest of the fields may
                                  * be uninitialized. */
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 6027ffd..49e1b77 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
@@ -4167,8 +4168,33 @@  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)
+{
+    const ovs_u128 *key;
+    ovs_u128 *mask, *base;
+
+    key = &flow->ct_label;
+    base = &base_flow->ct_label;
+    mask = &wc->masks.ct_label;
+
+    if (!ovs_u128_is_zero(mask) && !ovs_u128_equals(key, base)) {
+        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 = *key;
+        odp_ct_label->mask = *mask;
+    }
+}
+
+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;
     uint32_t flags = 0;
     size_t ct_offset;
@@ -4194,11 +4220,13 @@  compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
     nl_msg_put_u32(ctx->odp_actions, OVS_CT_ATTR_FLAGS, flags);
     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 f837543..7129c29 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -1264,6 +1264,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__
@@ -1283,6 +1284,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
@@ -3982,19 +3984,27 @@  static enum ofperr
 rule_check(struct rule *rule)
 {
     uint16_t ct_state_mask, ct_zone_mask;
+    const ovs_u128 *labelp;
+    ovs_u128 ct_label_mask = { { 0, 0 } };
     uint32_t ct_mark_mask;
 
     ct_state_mask = MINIFLOW_GET_U16(&rule->cr.match.mask->masks, ct_state);
     ct_zone_mask = MINIFLOW_GET_U16(&rule->cr.match.mask->masks, ct_zone);
     ct_mark_mask = MINIFLOW_GET_U32(&rule->cr.match.mask->masks, ct_mark);
+    labelp = MINIFLOW_GET_U128_PTR(&rule->cr.match.mask->masks, ct_label);
+    if (labelp) {
+        ct_label_mask = *labelp;
+    }
 
-    if (ct_state_mask || ct_zone_mask || ct_mark_mask) {
+    if (ct_state_mask || ct_zone_mask || ct_mark_mask
+        || !ovs_u128_is_zero(&ct_label_mask)) {
         struct ofproto_dpif *ofproto = ofproto_dpif_cast(rule->ofproto);
         const struct odp_support *support = &ofproto_dpif_get_support(ofproto)->odp;
 
         if ((ct_state_mask && !support->ct_state)
             || (ct_zone_mask && !support->ct_zone)
-            || (ct_mark_mask && !support->ct_mark)) {
+            || (ct_mark_mask && !support->ct_mark)
+            || (!ovs_u128_is_zero(&ct_label_mask) && !support->ct_label)) {
             return OFPERR_OFPBMC_BAD_FIELD;
         }
         if (ct_state_mask & 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 bf3fa8a..702d44c 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_mark(0x12345678\/0xFOFOFOFO),/' odp-base.txt
+ sed 's/\(eth([[^)]]*),?\)/\1,ct_state(+trk),ct_mark(0x12345678\/0xFOFOFOFO),ct_label(0x1234567890ABCDEF1234567890ABCDEF\/0x102030405060708090A0B0C0D0E0F0),/' odp-base.txt
 
  echo
  echo '# Valid forms with IP first fragment.'
diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
index a6a6ee9..4a212be 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 8cdca9c..5fa1e84 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -1538,7 +1538,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 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
-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
@@ -1621,6 +1621,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/system-traffic.at b/tests/system-traffic.at
index ccdaa2a..03b9747 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:1->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=000000000000000001,tcp,action=1
+priority=100,in_port=3,tcp,action=ct(commit,exec(set_field:2->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=000000000000000001,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 afeeaeb..5058ab2 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -1368,6 +1368,10 @@  Matches connection zone \fIvalue\fR exactly.
 Matches connection mark \fIvalue\fR either exactly or with optional
 \fImask\fR.
 .
+.IP \fBct_label=\fIvalue\fR[\fB/\fImask\fR]
+Matches connection label \fIvalue\fR either exactly or with optional
+\fImask\fR.
+.
 .PP
 Defining IPv6 flows (those with \fBdl_type\fR equal to 0x86dd) requires
 support for NXM.  The following shorthand notations are available for
@@ -1641,6 +1645,10 @@  field:
 Store a 32-bit metadata value with the connection. This will populate the
 \fBct_mark\fR flow field if the \fBtable\fR is specified in the \fBct\fR
 action.
+.IP \fBset_field:\fIvalue\fR->ct_label\fR
+Store a 128-bit metadata value with the connection. This will populate the
+\fBct_label\fR flow field if the \fBtable\fR is specified in the \fBct\fR
+action.
 .RE
 .
 .RE