[ovs-dev] [PATCH 15/21] odp: Support conntrack orig tuple key.

Jarno Rajahalme jarno at ovn.org
Fri Feb 24 00:19:22 UTC 2017


Userspace support for datapath original direction conntrack tuple.

Signed-off-by: Jarno Rajahalme <jarno at ovn.org>
---
 build-aux/extract-ofp-fields    |   3 +
 include/openvswitch/flow.h      |  14 ++-
 include/openvswitch/match.h     |  16 +++
 include/openvswitch/meta-flow.h | 136 +++++++++++++++++++++++++
 lib/conntrack.c                 |  43 ++++++--
 lib/flow.c                      | 220 ++++++++++++++++++++++++++++------------
 lib/flow.h                      |  50 +++++++++
 lib/match.c                     | 110 +++++++++++++++++++-
 lib/meta-flow.c                 | 157 +++++++++++++++++++++++++++-
 lib/meta-flow.xml               |  92 +++++++++++++++++
 lib/nx-match.c                  |  40 ++++++--
 lib/nx-match.h                  |   4 +-
 lib/odp-execute.c               |   4 +
 lib/odp-util.c                  | 124 ++++++++++++++++++++++
 lib/odp-util.h                  |   8 +-
 lib/ofp-util.c                  |   7 +-
 lib/packets.h                   |   5 +
 ofproto/ofproto-dpif-rid.h      |   2 +-
 ofproto/ofproto-dpif-sflow.c    |   2 +
 ofproto/ofproto-dpif-xlate.c    |  13 ++-
 ofproto/ofproto-dpif.c          |   2 +
 tests/odp.at                    |   2 +-
 tests/ofproto-dpif.at           |  30 +++---
 tests/ofproto.at                |   7 ++
 tests/system-traffic.at         | 142 ++++++++++++++++++++++++--
 25 files changed, 1114 insertions(+), 119 deletions(-)

diff --git a/build-aux/extract-ofp-fields b/build-aux/extract-ofp-fields
index 498b887..a26d558 100755
--- a/build-aux/extract-ofp-fields
+++ b/build-aux/extract-ofp-fields
@@ -44,6 +44,9 @@ PREREQS = {"none": "MFP_NONE",
            "IPv4": "MFP_IPV4",
            "IPv6": "MFP_IPV6",
            "IPv4/IPv6": "MFP_IP_ANY",
+           "CT": "MFP_CT_VALID",
+           "CTv4": "MFP_CTV4_VALID",
+           "CTv6": "MFP_CTV6_VALID",
            "MPLS": "MFP_MPLS",
            "TCP": "MFP_TCP",
            "UDP": "MFP_UDP",
diff --git a/include/openvswitch/flow.h b/include/openvswitch/flow.h
index 9169272..68399b8 100644
--- a/include/openvswitch/flow.h
+++ b/include/openvswitch/flow.h
@@ -23,7 +23,7 @@
 /* This sequence number should be incremented whenever anything involving flows
  * or the wildcarding of flows changes.  This will cause build assertion
  * failures in places which likely need to be updated. */
-#define FLOW_WC_SEQ 36
+#define FLOW_WC_SEQ 37
 
 /* Number of Open vSwitch extension 32-bit registers. */
 #define FLOW_N_REGS 16
@@ -92,7 +92,7 @@ struct flow {
     union flow_in_port in_port; /* Input port.*/
     uint32_t recirc_id;         /* Must be exact match. */
     uint8_t ct_state;           /* Connection tracking state. */
-    uint8_t pad0;
+    uint8_t ct_nw_proto;        /* CT orig tuple IP protocol. */
     uint16_t ct_zone;           /* Connection tracking zone. */
     uint32_t ct_mark;           /* Connection mark.*/
     uint8_t pad1[4];            /* Pad to 64 bits. */
@@ -110,8 +110,12 @@ struct flow {
     /* L3 (64-bit aligned) */
     ovs_be32 nw_src;            /* IPv4 source address or ARP SPA. */
     ovs_be32 nw_dst;            /* IPv4 destination address or ARP TPA. */
+    ovs_be32 ct_nw_src;         /* CT orig tuple IPv4 source address. */
+    ovs_be32 ct_nw_dst;         /* CT orig tuple IPv4 destination address. */
     struct in6_addr ipv6_src;   /* IPv6 source address. */
     struct in6_addr ipv6_dst;   /* IPv6 destination address. */
+    struct in6_addr ct_ipv6_src; /* CT orig tuple IPv6 source address. */
+    struct in6_addr ct_ipv6_dst; /* CT orig tuple IPv6 destination address. */
     ovs_be32 ipv6_label;        /* IPv6 flow label. */
     uint8_t nw_frag;            /* FLOW_FRAG_* flags. */
     uint8_t nw_tos;             /* IP ToS (including DSCP and ECN). */
@@ -126,6 +130,8 @@ struct flow {
     /* L4 (64-bit aligned) */
     ovs_be16 tp_src;            /* TCP/UDP/SCTP source port/ICMP type. */
     ovs_be16 tp_dst;            /* TCP/UDP/SCTP destination port/ICMP code. */
+    ovs_be16 ct_tp_src;         /* CT original tuple source port/ICMP type. */
+    ovs_be16 ct_tp_dst;         /* CT original tuple dst port/ICMP code. */
     ovs_be32 igmp_group_ip4;    /* IGMP group IPv4 address.
                                  * Keep last for BUILD_ASSERT_DECL below. */
 };
@@ -136,8 +142,8 @@ 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) + 248
-                  && FLOW_WC_SEQ == 36);
+                  == sizeof(struct flow_tnl) + 292
+                  && FLOW_WC_SEQ == 37);
 
 /* Incremental points at which flow classification may be performed in
  * segments.
diff --git a/include/openvswitch/match.h b/include/openvswitch/match.h
index 0b5f050..06fa04c 100644
--- a/include/openvswitch/match.h
+++ b/include/openvswitch/match.h
@@ -99,6 +99,22 @@ 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_ct_nw_src(struct match *, ovs_be32);
+void match_set_ct_nw_src_masked(struct match *, ovs_be32, ovs_be32 mask);
+void match_set_ct_nw_dst(struct match *, ovs_be32);
+void match_set_ct_nw_dst_masked(struct match *, ovs_be32, ovs_be32 mask);
+void match_set_ct_nw_proto(struct match *, uint8_t);
+void match_set_ct_tp_src(struct match *, ovs_be16);
+void match_set_ct_tp_src_masked(struct match *, ovs_be16, ovs_be16 mask);
+void match_set_ct_tp_dst(struct match *, ovs_be16);
+void match_set_ct_tp_dst_masked(struct match *, ovs_be16, ovs_be16 mask);
+void match_set_ct_ipv6_src(struct match *, const struct in6_addr *);
+void match_set_ct_ipv6_src_masked(struct match *, const struct in6_addr *,
+                                  const struct in6_addr *);
+void match_set_ct_ipv6_dst(struct match *, const struct in6_addr *);
+void match_set_ct_ipv6_dst_masked(struct match *, const struct in6_addr *,
+                                  const struct in6_addr *);
+
 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/include/openvswitch/meta-flow.h b/include/openvswitch/meta-flow.h
index aac9945..94cee20 100644
--- a/include/openvswitch/meta-flow.h
+++ b/include/openvswitch/meta-flow.h
@@ -740,6 +740,139 @@ enum OVS_PACKED_ENUM mf_field_id {
      */
     MFF_CT_LABEL,
 
+    /* "ct_nw_proto".
+     *
+     * The "protocol" byte in the IPv4 or IPv6 header for the original
+     * direction conntrack tuple, or of the master conntrack entry, if the
+     * current connection is a related connection.
+     *
+     * The value is initially zero and populated by the CT action.  The value
+     * remains zero after the CT action only if the packet can not be
+     * associated with a tracked connection, in which case the prerequisites
+     * for matching this field ("CT") are not met.
+     *
+     * Type: u8.
+     * Maskable: no.
+     * Formatting: decimal.
+     * Prerequisites: CT.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_NW_PROTO(119) since v2.7.
+     * OXM: none.
+     */
+    MFF_CT_NW_PROTO,
+
+    /* "ct_nw_src".
+     *
+     * IPv4 source address of the original direction tuple of the conntrack
+     * entry, or of the master conntrack entry, if the current connection is a
+     * related connection.
+     *
+     * The value is populated by the CT action.
+     *
+     * Type: be32.
+     * Maskable: bitwise.
+     * Formatting: IPv4.
+     * Prerequisites: CTv4.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_NW_SRC(120) since v2.7.
+     * OXM: none.
+     * Prefix lookup member: ct_nw_src.
+     */
+    MFF_CT_NW_SRC,
+
+    /* "ct_nw_dst".
+     *
+     * IPv4 destination address of the original direction tuple of the
+     * conntrack entry, or of the master conntrack entry, if the current
+     * connection is a related connection.
+     *
+     * The value is populated by the CT action.
+     *
+     * Type: be32.
+     * Maskable: bitwise.
+     * Formatting: IPv4.
+     * Prerequisites: CTv4.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_NW_DST(121) since v2.7.
+     * OXM: none.
+     * Prefix lookup member: ct_nw_dst.
+     */
+    MFF_CT_NW_DST,
+
+    /* "ct_ipv6_src".
+     *
+     * IPv6 source address of the original direction tuple of the conntrack
+     * entry, or of the master conntrack entry, if the current connection is a
+     * related connection.
+     *
+     * The value is populated by the CT action.
+     *
+     * Type: be128.
+     * Maskable: bitwise.
+     * Formatting: IPv6.
+     * Prerequisites: CTv6.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_IPV6_SRC(122) since v2.7.
+     * OXM: none.
+     * Prefix lookup member: ct_ipv6_src.
+     */
+    MFF_CT_IPV6_SRC,
+
+    /* "ct_ipv6_dst".
+     *
+     * IPv6 destination address of the original direction tuple of the
+     * conntrack entry, or of the master conntrack entry, if the current
+     * connection is a related connection.
+     *
+     * The value is populated by the CT action.
+     *
+     * Type: be128.
+     * Maskable: bitwise.
+     * Formatting: IPv6.
+     * Prerequisites: CTv6.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_IPV6_DST(123) since v2.7.
+     * OXM: none.
+     * Prefix lookup member: ct_ipv6_dst.
+     */
+    MFF_CT_IPV6_DST,
+
+    /* "ct_tp_src".
+     *
+     * Transport layer source port of the original direction tuple of the
+     * conntrack entry, or of the master conntrack entry, if the current
+     * connection is a related connection.
+     *
+     * The value is populated by the CT action.
+     *
+     * Type: be16.
+     * Maskable: bitwise.
+     * Formatting: decimal.
+     * Prerequisites: CT.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_TP_SRC(124) since v2.7.
+     * OXM: none.
+     */
+    MFF_CT_TP_SRC,
+
+    /* "ct_tp_dst".
+     *
+     * Transport layer destination port of the original direction tuple of the
+     * conntrack entry, or of the master conntrack entry, if the current
+     * connection is a related connection.
+     *
+     * The value is populated by the CT action.
+     *
+     * Type: be16.
+     * Maskable: bitwise.
+     * Formatting: decimal.
+     * Prerequisites: CT.
+     * Access: read-only.
+     * NXM: NXM_NX_CT_TP_DST(125) since v2.7.
+     * OXM: none.
+     */
+    MFF_CT_TP_DST,
+
 #if FLOW_N_REGS == 16
     /* "reg<N>".
      *
@@ -1689,6 +1822,9 @@ enum OVS_PACKED_ENUM mf_prereqs {
     MFP_SCTP,                   /* On IPv4 or IPv6. */
     MFP_ICMPV4,
     MFP_ICMPV6,
+    MFP_CT_VALID,               /* Implies IPv4 or IPv6. */
+    MFP_CTV4_VALID,             /* MFP_CT_VALID and IPv4. */
+    MFP_CTV6_VALID,             /* MFP_CT_VALID and IPv6. */
 
     /* L2+L3+L4 requirements. */
     MFP_ND,
diff --git a/lib/conntrack.c b/lib/conntrack.c
index 9bea3d9..1b66c8d 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -159,12 +159,44 @@ static unsigned hash_to_bucket(uint32_t hash)
 
 static void
 write_ct_md(struct dp_packet *pkt, uint16_t state, uint16_t zone,
-            uint32_t mark, ovs_u128 label)
+            const struct conn *conn, const struct conn_key *key)
 {
     pkt->md.ct_state = state | CS_TRACKED;
     pkt->md.ct_zone = zone;
-    pkt->md.ct_mark = mark;
-    pkt->md.ct_label = label;
+    pkt->md.ct_mark = conn ? conn->mark : 0;
+    pkt->md.ct_label = conn ? conn->label : OVS_U128_ZERO;
+
+    /* Use the original direction tuple if we have it. */
+    if (conn) {
+        key = &conn->key;
+    }
+    pkt->md.ct_orig_tuple_ipv6 = false;
+    if (key) {
+        if (key->dl_type == htons(ETH_TYPE_IP)) {
+            pkt->md.ct_orig_tuple.ipv4 = (struct ovs_key_ct_tuple_ipv4) {
+                key->src.addr.ipv4_aligned,
+                key->dst.addr.ipv4_aligned,
+                key->nw_proto != IPPROTO_ICMP
+                ? key->src.port : htons(key->src.icmp_type),
+                key->nw_proto != IPPROTO_ICMP
+                ? key->dst.port : htons(key->src.icmp_code),
+                key->nw_proto,
+            };
+        } else if (key->dl_type == htons(ETH_TYPE_IPV6)) {
+            pkt->md.ct_orig_tuple_ipv6 = true;
+            pkt->md.ct_orig_tuple.ipv6 = (struct ovs_key_ct_tuple_ipv6) {
+                key->src.addr.ipv6_aligned,
+                key->dst.addr.ipv6_aligned,
+                key->nw_proto != IPPROTO_ICMPV6
+                ? key->src.port : htons(key->src.icmp_type),
+                key->nw_proto != IPPROTO_ICMPV6
+                ? key->dst.port : htons(key->src.icmp_code),
+                key->nw_proto,
+            };
+        }
+    } else {
+        memset(&pkt->md.ct_orig_tuple, 0, sizeof pkt->md.ct_orig_tuple);
+    }
 }
 
 static struct conn *
@@ -254,8 +286,7 @@ process_one(struct conntrack *ct, struct dp_packet *pkt,
         }
     }
 
-    write_ct_md(pkt, state, zone, conn ? conn->mark : 0,
-                conn ? conn->label : OVS_U128_ZERO);
+    write_ct_md(pkt, state, zone, conn, &ctx->key);
 
     return conn;
 }
@@ -306,7 +337,7 @@ conntrack_execute(struct conntrack *ct, struct dp_packet_batch *pkt_batch,
         unsigned bucket;
 
         if (!conn_key_extract(ct, pkts[i], dl_type, &ctxs[i], zone)) {
-            write_ct_md(pkts[i], CS_INVALID, zone, 0, OVS_U128_ZERO);
+            write_ct_md(pkts[i], CS_INVALID, zone, NULL, NULL);
             continue;
         }
 
diff --git a/lib/flow.c b/lib/flow.c
index 0c95b75..e1e9ae9 100644
--- a/lib/flow.c
+++ b/lib/flow.c
@@ -125,7 +125,7 @@ struct mf_ctx {
  * away.  Some GCC versions gave warnings on ALWAYS_INLINE, so these are
  * defined as macros. */
 
-#if (FLOW_WC_SEQ != 36)
+#if (FLOW_WC_SEQ != 37)
 #define MINIFLOW_ASSERT(X) ovs_assert(X)
 BUILD_MESSAGE("FLOW_WC_SEQ changed: miniflow_extract() will have runtime "
                "assertions enabled. Consider updating FLOW_WC_SEQ after "
@@ -312,6 +312,11 @@ BUILD_MESSAGE("FLOW_WC_SEQ changed: miniflow_extract() will have runtime "
 #define miniflow_push_macs(MF, FIELD, VALUEP)                       \
     miniflow_push_macs_(MF, offsetof(struct flow, FIELD), VALUEP)
 
+/* Return the pointer to the miniflow data when called BEFORE the corresponding
+ * push. */
+#define miniflow_pointer(MF, FIELD)                                     \
+    (void *)((uint8_t *)MF.data + ((offsetof(struct flow, FIELD)) % 8))
+
 /* Pulls the MPLS headers at '*datap' and returns the count of them. */
 static inline int
 parse_mpls(const void **datap, size_t *sizep)
@@ -383,61 +388,63 @@ parse_ethertype(const void **datap, size_t *sizep)
     return htons(FLOW_DL_TYPE_NONE);
 }
 
-static inline void
+/* Returns 'true' if the packet is an ND packet. */
+static inline bool
 parse_icmpv6(const void **datap, size_t *sizep, const struct icmp6_hdr *icmp,
              const struct in6_addr **nd_target,
              struct eth_addr arp_buf[2])
 {
-    if (icmp->icmp6_code == 0 &&
-        (icmp->icmp6_type == ND_NEIGHBOR_SOLICIT ||
-         icmp->icmp6_type == ND_NEIGHBOR_ADVERT)) {
+    if (icmp->icmp6_code != 0 ||
+        (icmp->icmp6_type != ND_NEIGHBOR_SOLICIT &&
+         icmp->icmp6_type != ND_NEIGHBOR_ADVERT)) {
+        return false;
+    }
 
-        *nd_target = data_try_pull(datap, sizep, sizeof **nd_target);
-        if (OVS_UNLIKELY(!*nd_target)) {
-            return;
-        }
+    *nd_target = data_try_pull(datap, sizep, sizeof **nd_target);
+    if (OVS_UNLIKELY(!*nd_target)) {
+        return true;
+    }
 
-        while (*sizep >= 8) {
-            /* The minimum size of an option is 8 bytes, which also is
-             * the size of Ethernet link-layer options. */
-            const struct ovs_nd_opt *nd_opt = *datap;
-            int opt_len = nd_opt->nd_opt_len * ND_OPT_LEN;
+    while (*sizep >= 8) {
+        /* The minimum size of an option is 8 bytes, which also is
+         * the size of Ethernet link-layer options. */
+        const struct ovs_nd_opt *nd_opt = *datap;
+        int opt_len = nd_opt->nd_opt_len * ND_OPT_LEN;
 
-            if (!opt_len || opt_len > *sizep) {
-                return;
-            }
+        if (!opt_len || opt_len > *sizep) {
+            return true;
+        }
 
-            /* Store the link layer address if the appropriate option is
-             * provided.  It is considered an error if the same link
-             * layer option is specified twice. */
-            if (nd_opt->nd_opt_type == ND_OPT_SOURCE_LINKADDR
-                && opt_len == 8) {
-                if (OVS_LIKELY(eth_addr_is_zero(arp_buf[0]))) {
-                    arp_buf[0] = nd_opt->nd_opt_mac;
-                } else {
-                    goto invalid;
-                }
-            } else if (nd_opt->nd_opt_type == ND_OPT_TARGET_LINKADDR
-                       && opt_len == 8) {
-                if (OVS_LIKELY(eth_addr_is_zero(arp_buf[1]))) {
-                    arp_buf[1] = nd_opt->nd_opt_mac;
-                } else {
-                    goto invalid;
-                }
+        /* Store the link layer address if the appropriate option is
+         * provided.  It is considered an error if the same link
+         * layer option is specified twice. */
+        if (nd_opt->nd_opt_type == ND_OPT_SOURCE_LINKADDR
+            && opt_len == 8) {
+            if (OVS_LIKELY(eth_addr_is_zero(arp_buf[0]))) {
+                arp_buf[0] = nd_opt->nd_opt_mac;
+            } else {
+                goto invalid;
             }
-
-            if (OVS_UNLIKELY(!data_try_pull(datap, sizep, opt_len))) {
-                return;
+        } else if (nd_opt->nd_opt_type == ND_OPT_TARGET_LINKADDR
+                   && opt_len == 8) {
+            if (OVS_LIKELY(eth_addr_is_zero(arp_buf[1]))) {
+                arp_buf[1] = nd_opt->nd_opt_mac;
+            } else {
+                goto invalid;
             }
         }
-    }
 
-    return;
+        if (OVS_UNLIKELY(!data_try_pull(datap, sizep, opt_len))) {
+            return true;
+        }
+    }
+    return true;
 
 invalid:
     *nd_target = NULL;
     arp_buf[0] = eth_addr_zero;
     arp_buf[1] = eth_addr_zero;
+    return true;
 }
 
 static inline bool
@@ -561,6 +568,8 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
     const char *l2;
     ovs_be16 dl_type;
     uint8_t nw_frag, nw_tos, nw_ttl, nw_proto;
+    uint8_t *ct_nw_proto_p = NULL;
+    ovs_be16 ct_tp_src = 0, ct_tp_dst = 0;
 
     /* Metadata. */
     if (flow_tnl_dst_is_set(&md->tunnel)) {
@@ -594,7 +603,8 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
     if (md->recirc_id || md->ct_state) {
         miniflow_push_uint32(mf, recirc_id, md->recirc_id);
         miniflow_push_uint8(mf, ct_state, md->ct_state);
-        miniflow_push_uint8(mf, pad0, 0);
+        ct_nw_proto_p = miniflow_pointer(mf, ct_nw_proto);
+        miniflow_push_uint8(mf, ct_nw_proto, 0);
         miniflow_push_uint16(mf, ct_zone, md->ct_zone);
     }
 
@@ -670,6 +680,15 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
 
         /* Push both source and destination address at once. */
         miniflow_push_words(mf, nw_src, &nh->ip_src, 1);
+        if (ct_nw_proto_p && !md->ct_orig_tuple_ipv6) {
+            *ct_nw_proto_p = md->ct_orig_tuple.ipv4.ipv4_proto;
+            if (*ct_nw_proto_p) {
+                miniflow_push_words(mf, ct_nw_src,
+                                    &md->ct_orig_tuple.ipv4.ipv4_src, 1);
+                ct_tp_src = md->ct_orig_tuple.ipv4.src_port;
+                ct_tp_dst = md->ct_orig_tuple.ipv4.dst_port;
+            }
+        }
 
         miniflow_push_be32(mf, ipv6_label, 0); /* Padding for IPv4. */
 
@@ -708,6 +727,17 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
                             sizeof nh->ip6_src / 8);
         miniflow_push_words(mf, ipv6_dst, &nh->ip6_dst,
                             sizeof nh->ip6_dst / 8);
+        if (ct_nw_proto_p && md->ct_orig_tuple_ipv6) {
+            *ct_nw_proto_p = md->ct_orig_tuple.ipv6.ipv6_proto;
+            if (*ct_nw_proto_p) {
+                miniflow_push_words(mf, ct_ipv6_src,
+                                    &md->ct_orig_tuple.ipv6.ipv6_src,
+                                    2 *
+                                    sizeof md->ct_orig_tuple.ipv6.ipv6_src / 8);
+                ct_tp_src = md->ct_orig_tuple.ipv6.src_port;
+                ct_tp_dst = md->ct_orig_tuple.ipv6.dst_port;
+            }
+        }
 
         tc_flow = get_16aligned_be32(&nh->ip6_flow);
         {
@@ -770,7 +800,8 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
                                    TCP_FLAGS_BE32(tcp->tcp_ctl));
                 miniflow_push_be16(mf, tp_src, tcp->tcp_src);
                 miniflow_push_be16(mf, tp_dst, tcp->tcp_dst);
-                miniflow_pad_to_64(mf, tp_dst);
+                miniflow_push_be16(mf, ct_tp_src, ct_tp_src);
+                miniflow_push_be16(mf, ct_tp_dst, ct_tp_dst);
             }
         } else if (OVS_LIKELY(nw_proto == IPPROTO_UDP)) {
             if (OVS_LIKELY(size >= UDP_HEADER_LEN)) {
@@ -778,7 +809,8 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
 
                 miniflow_push_be16(mf, tp_src, udp->udp_src);
                 miniflow_push_be16(mf, tp_dst, udp->udp_dst);
-                miniflow_pad_to_64(mf, tp_dst);
+                miniflow_push_be16(mf, ct_tp_src, ct_tp_src);
+                miniflow_push_be16(mf, ct_tp_dst, ct_tp_dst);
             }
         } else if (OVS_LIKELY(nw_proto == IPPROTO_SCTP)) {
             if (OVS_LIKELY(size >= SCTP_HEADER_LEN)) {
@@ -786,7 +818,8 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
 
                 miniflow_push_be16(mf, tp_src, sctp->sctp_src);
                 miniflow_push_be16(mf, tp_dst, sctp->sctp_dst);
-                miniflow_pad_to_64(mf, tp_dst);
+                miniflow_push_be16(mf, ct_tp_src, ct_tp_src);
+                miniflow_push_be16(mf, ct_tp_dst, ct_tp_dst);
             }
         } else if (OVS_LIKELY(nw_proto == IPPROTO_ICMP)) {
             if (OVS_LIKELY(size >= ICMP_HEADER_LEN)) {
@@ -794,7 +827,8 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
 
                 miniflow_push_be16(mf, tp_src, htons(icmp->icmp_type));
                 miniflow_push_be16(mf, tp_dst, htons(icmp->icmp_code));
-                miniflow_pad_to_64(mf, tp_dst);
+                miniflow_push_be16(mf, ct_tp_src, ct_tp_src);
+                miniflow_push_be16(mf, ct_tp_dst, ct_tp_dst);
             }
         } else if (OVS_LIKELY(nw_proto == IPPROTO_IGMP)) {
             if (OVS_LIKELY(size >= IGMP_HEADER_LEN)) {
@@ -802,8 +836,11 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
 
                 miniflow_push_be16(mf, tp_src, htons(igmp->igmp_type));
                 miniflow_push_be16(mf, tp_dst, htons(igmp->igmp_code));
+                miniflow_push_be16(mf, ct_tp_src, ct_tp_src);
+                miniflow_push_be16(mf, ct_tp_dst, ct_tp_dst);
                 miniflow_push_be32(mf, igmp_group_ip4,
                                    get_16aligned_be32(&igmp->group));
+                miniflow_pad_to_64(mf, igmp_group_ip4);
             }
         } else if (OVS_LIKELY(nw_proto == IPPROTO_ICMPV6)) {
             if (OVS_LIKELY(size >= sizeof(struct icmp6_hdr))) {
@@ -811,16 +848,23 @@ miniflow_extract(struct dp_packet *packet, struct miniflow *dst)
                 struct eth_addr arp_buf[2] = { { { { 0 } } } };
                 const struct icmp6_hdr *icmp = data_pull(&data, &size,
                                                          sizeof *icmp);
-                parse_icmpv6(&data, &size, icmp, &nd_target, arp_buf);
-                if (nd_target) {
-                    miniflow_push_words(mf, nd_target, nd_target,
-                                        sizeof *nd_target / sizeof(uint64_t));
+                if (parse_icmpv6(&data, &size, icmp, &nd_target, arp_buf)) {
+                    if (nd_target) {
+                        miniflow_push_words(mf, nd_target, nd_target,
+                                            sizeof *nd_target / sizeof(uint64_t));
+                    }
+                    miniflow_push_macs(mf, arp_sha, arp_buf);
+                    miniflow_pad_to_64(mf, arp_tha);
+                    miniflow_push_be16(mf, tp_src, htons(icmp->icmp6_type));
+                    miniflow_push_be16(mf, tp_dst, htons(icmp->icmp6_code));
+                    miniflow_pad_to_64(mf, tp_dst);
+                } else {
+                    /* ICMPv6 but not ND. */
+                    miniflow_push_be16(mf, tp_src, htons(icmp->icmp6_type));
+                    miniflow_push_be16(mf, tp_dst, htons(icmp->icmp6_code));
+                    miniflow_push_be16(mf, ct_tp_src, ct_tp_src);
+                    miniflow_push_be16(mf, ct_tp_dst, ct_tp_dst);
                 }
-                miniflow_push_macs(mf, arp_sha, arp_buf);
-                miniflow_pad_to_64(mf, arp_tha);
-                miniflow_push_be16(mf, tp_src, htons(icmp->icmp6_type));
-                miniflow_push_be16(mf, tp_dst, htons(icmp->icmp6_code));
-                miniflow_pad_to_64(mf, tp_dst);
             }
         }
     }
@@ -870,7 +914,7 @@ flow_get_metadata(const struct flow *flow, struct match *flow_metadata)
 {
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     match_init_catchall(flow_metadata);
     if (flow->tunnel.tun_id != htonll(0)) {
@@ -916,6 +960,21 @@ flow_get_metadata(const struct flow *flow, struct match *flow_metadata)
     match_set_in_port(flow_metadata, flow->in_port.ofp_port);
     if (flow->ct_state != 0) {
         match_set_ct_state(flow_metadata, flow->ct_state);
+        if (is_ct_valid(flow, NULL, NULL) && flow->ct_nw_proto != 0) {
+            if (flow->dl_type == htons(ETH_TYPE_IP)) {
+                match_set_ct_nw_src(flow_metadata, flow->ct_nw_src);
+                match_set_ct_nw_dst(flow_metadata, flow->ct_nw_dst);
+                match_set_ct_nw_proto(flow_metadata, flow->ct_nw_proto);
+                match_set_ct_tp_src(flow_metadata, flow->ct_tp_src);
+                match_set_ct_tp_dst(flow_metadata, flow->ct_tp_dst);
+            } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
+                match_set_ct_ipv6_src(flow_metadata, &flow->ct_ipv6_src);
+                match_set_ct_ipv6_dst(flow_metadata, &flow->ct_ipv6_dst);
+                match_set_ct_nw_proto(flow_metadata, flow->ct_nw_proto);
+                match_set_ct_tp_src(flow_metadata, flow->ct_tp_src);
+                match_set_ct_tp_dst(flow_metadata, flow->ct_tp_dst);
+            }
+        }
     }
     if (flow->ct_zone != 0) {
         match_set_ct_zone(flow_metadata, flow->ct_zone);
@@ -1237,6 +1296,18 @@ flow_format(struct ds *ds, const struct flow *flow)
     if (ovs_u128_is_zero(flow->ct_label)) {
         WC_UNMASK_FIELD(wc, ct_label);
     }
+    if (!is_ct_valid(flow, &match.wc, NULL) || !flow->ct_nw_proto) {
+        WC_UNMASK_FIELD(wc, ct_nw_proto);
+        WC_UNMASK_FIELD(wc, ct_tp_src);
+        WC_UNMASK_FIELD(wc, ct_tp_dst);
+        if (flow->dl_type == htons(ETH_TYPE_IP)) {
+            WC_UNMASK_FIELD(wc, ct_nw_src);
+            WC_UNMASK_FIELD(wc, ct_nw_dst);
+        } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
+            WC_UNMASK_FIELD(wc, ct_ipv6_src);
+            WC_UNMASK_FIELD(wc, ct_ipv6_dst);
+        }
+    }
     for (int i = 0; i < FLOW_N_REGS; i++) {
         if (!flow->regs[i]) {
             WC_UNMASK_FIELD(wc, regs[i]);
@@ -1276,7 +1347,7 @@ void flow_wildcards_init_for_packet(struct flow_wildcards *wc,
     memset(&wc->masks, 0x0, sizeof wc->masks);
 
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     if (flow_tnl_dst_is_set(&flow->tunnel)) {
         if (flow->tunnel.flags & FLOW_TNL_F_KEY) {
@@ -1332,10 +1403,20 @@ void flow_wildcards_init_for_packet(struct flow_wildcards *wc,
     if (flow->dl_type == htons(ETH_TYPE_IP)) {
         WC_MASK_FIELD(wc, nw_src);
         WC_MASK_FIELD(wc, nw_dst);
+        WC_MASK_FIELD(wc, ct_nw_src);
+        WC_MASK_FIELD(wc, ct_nw_dst);
     } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
         WC_MASK_FIELD(wc, ipv6_src);
         WC_MASK_FIELD(wc, ipv6_dst);
         WC_MASK_FIELD(wc, ipv6_label);
+        if (is_nd(flow, wc)) {
+            WC_MASK_FIELD(wc, arp_sha);
+            WC_MASK_FIELD(wc, arp_tha);
+            WC_MASK_FIELD(wc, nd_target);
+        } else {
+            WC_MASK_FIELD(wc, ct_ipv6_src);
+            WC_MASK_FIELD(wc, ct_ipv6_dst);
+        }
     } else if (flow->dl_type == htons(ETH_TYPE_ARP) ||
                flow->dl_type == htons(ETH_TYPE_RARP)) {
         WC_MASK_FIELD(wc, nw_src);
@@ -1361,6 +1442,9 @@ void flow_wildcards_init_for_packet(struct flow_wildcards *wc,
     WC_MASK_FIELD(wc, nw_tos);
     WC_MASK_FIELD(wc, nw_ttl);
     WC_MASK_FIELD(wc, nw_proto);
+    WC_MASK_FIELD(wc, ct_nw_proto);
+    WC_MASK_FIELD(wc, ct_tp_src);
+    WC_MASK_FIELD(wc, ct_tp_dst);
 
     /* No transport layer header in later fragments. */
     if (!(flow->nw_frag & FLOW_NW_FRAG_LATER) &&
@@ -1375,10 +1459,6 @@ void flow_wildcards_init_for_packet(struct flow_wildcards *wc,
 
         if (flow->nw_proto == IPPROTO_TCP) {
             WC_MASK_FIELD(wc, tcp_flags);
-        } else if (flow->nw_proto == IPPROTO_ICMPV6) {
-            WC_MASK_FIELD(wc, arp_sha);
-            WC_MASK_FIELD(wc, arp_tha);
-            WC_MASK_FIELD(wc, nd_target);
         } else if (flow->nw_proto == IPPROTO_IGMP) {
             WC_MASK_FIELD(wc, igmp_group_ip4);
         }
@@ -1394,7 +1474,7 @@ void
 flow_wc_map(const struct flow *flow, struct flowmap *map)
 {
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     flowmap_init(map);
 
@@ -1436,6 +1516,11 @@ flow_wc_map(const struct flow *flow, struct flowmap *map)
         FLOWMAP_SET(map, nw_ttl);
         FLOWMAP_SET(map, tp_src);
         FLOWMAP_SET(map, tp_dst);
+        FLOWMAP_SET(map, ct_nw_proto);
+        FLOWMAP_SET(map, ct_nw_src);
+        FLOWMAP_SET(map, ct_nw_dst);
+        FLOWMAP_SET(map, ct_tp_src);
+        FLOWMAP_SET(map, ct_tp_dst);
 
         if (OVS_UNLIKELY(flow->nw_proto == IPPROTO_IGMP)) {
             FLOWMAP_SET(map, igmp_group_ip4);
@@ -1453,11 +1538,16 @@ flow_wc_map(const struct flow *flow, struct flowmap *map)
         FLOWMAP_SET(map, tp_src);
         FLOWMAP_SET(map, tp_dst);
 
-        if (OVS_UNLIKELY(flow->nw_proto == IPPROTO_ICMPV6)) {
+        if (OVS_UNLIKELY(is_nd(flow, NULL))) {
             FLOWMAP_SET(map, nd_target);
             FLOWMAP_SET(map, arp_sha);
             FLOWMAP_SET(map, arp_tha);
         } else {
+            FLOWMAP_SET(map, ct_nw_proto);
+            FLOWMAP_SET(map, ct_ipv6_src);
+            FLOWMAP_SET(map, ct_ipv6_dst);
+            FLOWMAP_SET(map, ct_tp_src);
+            FLOWMAP_SET(map, ct_tp_dst);
             FLOWMAP_SET(map, tcp_flags);
         }
     } else if (eth_type_mpls(flow->dl_type)) {
@@ -1478,7 +1568,7 @@ void
 flow_wildcards_clear_non_packet_fields(struct flow_wildcards *wc)
 {
     /* Update this function whenever struct flow changes. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     memset(&wc->masks.metadata, 0, sizeof wc->masks.metadata);
     memset(&wc->masks.regs, 0, sizeof wc->masks.regs);
@@ -1622,7 +1712,7 @@ flow_wildcards_set_xxreg_mask(struct flow_wildcards *wc, int idx,
 uint32_t
 miniflow_hash_5tuple(const struct miniflow *flow, uint32_t basis)
 {
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
     uint32_t hash = basis;
 
     if (flow) {
@@ -1669,7 +1759,7 @@ ASSERT_SEQUENTIAL(ipv6_src, ipv6_dst);
 uint32_t
 flow_hash_5tuple(const struct flow *flow, uint32_t basis)
 {
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
     uint32_t hash = basis;
 
     if (flow) {
@@ -2137,7 +2227,7 @@ flow_push_mpls(struct flow *flow, int n, ovs_be16 mpls_eth_type,
 
         if (clear_flow_L3) {
             /* Clear all L3 and L4 fields and dp_hash. */
-            BUILD_ASSERT(FLOW_WC_SEQ == 36);
+            BUILD_ASSERT(FLOW_WC_SEQ == 37);
             memset((char *) flow + FLOW_SEGMENT_2_ENDS_AT, 0,
                    sizeof(struct flow) - FLOW_SEGMENT_2_ENDS_AT);
             flow->dp_hash = 0;
diff --git a/lib/flow.h b/lib/flow.h
index 62315bc..14a3004 100644
--- a/lib/flow.h
+++ b/lib/flow.h
@@ -862,9 +862,35 @@ flow_union_with_miniflow(struct flow *dst, const struct miniflow *src)
     flow_union_with_miniflow_subset(dst, src, src->map);
 }
 
+static inline bool is_ct_valid(const struct flow *flow,
+                               const struct flow_wildcards *mask,
+                               struct flow_wildcards *wc)
+{
+    /* Matches are checked with 'mask' and without 'wc'. */
+    if (mask && !wc) {
+        /* Must match at least one of the bits that implies a valid
+         * conntrack entry, or an explicit not-invalid. */
+        return flow->ct_state & (CS_NEW | CS_ESTABLISHED | CS_RELATED
+                                 | CS_REPLY_DIR | CS_SRC_NAT | CS_DST_NAT)
+            || (flow->ct_state & CS_TRACKED
+                && mask->masks.ct_state & CS_INVALID
+                && !(flow->ct_state & CS_INVALID));
+    }
+    /* Else we are checking a fully extracted flow, where valid CT state always
+     * has either 'new', 'established', or 'reply_dir' bit set. */
+#define CS_VALID_MASK (CS_NEW | CS_ESTABLISHED | CS_REPLY_DIR)
+    if (wc) {
+        wc->masks.ct_state |= CS_VALID_MASK;
+    }
+    return flow->ct_state & CS_VALID_MASK;
+}
+
 static inline void
 pkt_metadata_from_flow(struct pkt_metadata *md, const struct flow *flow)
 {
+    /* Update this function whenever struct flow changes. */
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
+
     md->recirc_id = flow->recirc_id;
     md->dp_hash = flow->dp_hash;
     flow_tnl_copy__(&md->tunnel, &flow->tunnel);
@@ -875,6 +901,30 @@ pkt_metadata_from_flow(struct pkt_metadata *md, const struct flow *flow)
     md->ct_zone = flow->ct_zone;
     md->ct_mark = flow->ct_mark;
     md->ct_label = flow->ct_label;
+
+    md->ct_orig_tuple_ipv6 = false;
+    if (is_ct_valid(flow, NULL, NULL)) {
+        if (flow->dl_type == htons(ETH_TYPE_IP)) {
+            md->ct_orig_tuple.ipv4 = (struct ovs_key_ct_tuple_ipv4) {
+                flow->ct_nw_src,
+                flow->ct_nw_dst,
+                flow->ct_tp_src,
+                flow->ct_tp_dst,
+                flow->ct_nw_proto,
+            };
+        } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
+            md->ct_orig_tuple_ipv6 = true;
+            md->ct_orig_tuple.ipv6 = (struct ovs_key_ct_tuple_ipv6) {
+                flow->ct_ipv6_src,
+                flow->ct_ipv6_dst,
+                flow->ct_tp_src,
+                flow->ct_tp_dst,
+                flow->ct_nw_proto,
+            };
+        }
+    } else {
+        memset(&md->ct_orig_tuple, 0, sizeof md->ct_orig_tuple);
+    }
 }
 
 /* Often, during translation we need to read a value from a flow('FLOW') and
diff --git a/lib/match.c b/lib/match.c
index 882bf0c..1a5b4ba 100644
--- a/lib/match.c
+++ b/lib/match.c
@@ -384,6 +384,99 @@ match_set_ct_label_masked(struct match *match, ovs_u128 value, ovs_u128 mask)
 }
 
 void
+match_set_ct_nw_src(struct match *match, ovs_be32 ct_nw_src)
+{
+    match->flow.ct_nw_src = ct_nw_src;
+    match->wc.masks.ct_nw_src = OVS_BE32_MAX;
+}
+
+void
+match_set_ct_nw_src_masked(struct match *match, ovs_be32 ct_nw_src,
+                           ovs_be32 mask)
+{
+    match->flow.ct_nw_src = ct_nw_src & mask;
+    match->wc.masks.ct_nw_src = mask;
+}
+
+void
+match_set_ct_nw_dst(struct match *match, ovs_be32 ct_nw_dst)
+{
+    match->flow.ct_nw_dst = ct_nw_dst;
+    match->wc.masks.ct_nw_dst = OVS_BE32_MAX;
+}
+
+void
+match_set_ct_nw_dst_masked(struct match *match, ovs_be32 ct_nw_dst,
+                           ovs_be32 mask)
+{
+    match->flow.ct_nw_dst = ct_nw_dst & mask;
+    match->wc.masks.ct_nw_dst = mask;
+}
+
+void
+match_set_ct_nw_proto(struct match *match, uint8_t ct_nw_proto)
+{
+    match->flow.ct_nw_proto = ct_nw_proto;
+    match->wc.masks.ct_nw_proto = UINT8_MAX;
+}
+
+void
+match_set_ct_tp_src(struct match *match, ovs_be16 ct_tp_src)
+{
+    match_set_ct_tp_src_masked(match, ct_tp_src, OVS_BE16_MAX);
+}
+
+void
+match_set_ct_tp_src_masked(struct match *match, ovs_be16 port, ovs_be16 mask)
+{
+    match->flow.ct_tp_src = port & mask;
+    match->wc.masks.ct_tp_src = mask;
+}
+
+void
+match_set_ct_tp_dst(struct match *match, ovs_be16 ct_tp_dst)
+{
+    match_set_ct_tp_dst_masked(match, ct_tp_dst, OVS_BE16_MAX);
+}
+
+void
+match_set_ct_tp_dst_masked(struct match *match, ovs_be16 port, ovs_be16 mask)
+{
+    match->flow.ct_tp_dst = port & mask;
+    match->wc.masks.ct_tp_dst = mask;
+}
+
+void
+match_set_ct_ipv6_src(struct match *match, const struct in6_addr *src)
+{
+    match->flow.ct_ipv6_src = *src;
+    match->wc.masks.ct_ipv6_src = in6addr_exact;
+}
+
+void
+match_set_ct_ipv6_src_masked(struct match *match, const struct in6_addr *src,
+                             const struct in6_addr *mask)
+{
+    match->flow.ct_ipv6_src = ipv6_addr_bitand(src, mask);
+    match->wc.masks.ct_ipv6_src = *mask;
+}
+
+void
+match_set_ct_ipv6_dst(struct match *match, const struct in6_addr *dst)
+{
+    match->flow.ct_ipv6_dst = *dst;
+    match->wc.masks.ct_ipv6_dst = in6addr_exact;
+}
+
+void
+match_set_ct_ipv6_dst_masked(struct match *match, const struct in6_addr *dst,
+                             const struct in6_addr *mask)
+{
+    match->flow.ct_ipv6_dst = ipv6_addr_bitand(dst, mask);
+    match->wc.masks.ct_ipv6_dst = *mask;
+}
+
+void
 match_set_dl_type(struct match *match, ovs_be16 dl_type)
 {
     match->wc.masks.dl_type = OVS_BE16_MAX;
@@ -1075,7 +1168,7 @@ match_format(const struct match *match, struct ds *s, int priority)
 
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     if (priority != OFP_DEFAULT_PRIORITY) {
         ds_put_format(s, "%spriority=%s%d,",
@@ -1137,6 +1230,21 @@ match_format(const struct match *match, struct ds *s, int priority)
         format_ct_label_masked(s, &f->ct_label, &wc->masks.ct_label);
     }
 
+    format_ip_netmask(s, "ct_nw_src", f->ct_nw_src,
+                      wc->masks.ct_nw_src);
+    format_ipv6_netmask(s, "ct_ipv6_src", &f->ct_ipv6_src,
+                        &wc->masks.ct_ipv6_src);
+    format_ip_netmask(s, "ct_nw_dst", f->ct_nw_dst,
+                      wc->masks.ct_nw_dst);
+    format_ipv6_netmask(s, "ct_ipv6_dst", &f->ct_ipv6_dst,
+                        &wc->masks.ct_ipv6_dst);
+    if (wc->masks.ct_nw_proto) {
+        ds_put_format(s, "%sct_nw_proto=%s%"PRIu8",",
+                      colors.param, colors.end, f->ct_nw_proto);
+        format_be16_masked(s, "ct_tp_src", f->ct_tp_src, wc->masks.ct_tp_src);
+        format_be16_masked(s, "ct_tp_dst", f->ct_tp_dst, wc->masks.ct_tp_dst);
+    }
+
     if (wc->masks.dl_type) {
         skip_type = true;
         if (f->dl_type == htons(ETH_TYPE_IP)) {
diff --git a/lib/meta-flow.c b/lib/meta-flow.c
index 9d635a3..38d051e 100644
--- a/lib/meta-flow.c
+++ b/lib/meta-flow.c
@@ -246,6 +246,20 @@ mf_is_all_wild(const struct mf_field *mf, const struct flow_wildcards *wc)
         return !wc->masks.ct_mark;
     case MFF_CT_LABEL:
         return ovs_u128_is_zero(wc->masks.ct_label);
+    case MFF_CT_NW_PROTO:
+        return !wc->masks.ct_nw_proto;
+    case MFF_CT_NW_SRC:
+        return !wc->masks.ct_nw_src;
+    case MFF_CT_NW_DST:
+        return !wc->masks.ct_nw_dst;
+    case MFF_CT_TP_SRC:
+        return !wc->masks.ct_tp_src;
+    case MFF_CT_TP_DST:
+        return !wc->masks.ct_tp_dst;
+    case MFF_CT_IPV6_SRC:
+        return ipv6_mask_is_any(&wc->masks.ct_ipv6_src);
+    case MFF_CT_IPV6_DST:
+        return ipv6_mask_is_any(&wc->masks.ct_ipv6_dst);
     CASE_MFF_REGS:
         return !wc->masks.regs[mf->id - MFF_REG0];
     CASE_MFF_XREGS:
@@ -383,7 +397,7 @@ mf_is_mask_valid(const struct mf_field *mf, const union mf_value *mask)
  * Sets inspected bits in 'wc', if non-NULL. */
 static bool
 mf_are_prereqs_ok__(const struct mf_field *mf, const struct flow *flow,
-                    const struct flow_wildcards *mask OVS_UNUSED,
+                    const struct flow_wildcards *mask,
                     struct flow_wildcards *wc)
 {
     switch (mf->prereqs) {
@@ -402,6 +416,14 @@ mf_are_prereqs_ok__(const struct mf_field *mf, const struct flow *flow,
         return eth_type_mpls(flow->dl_type);
     case MFP_IP_ANY:
         return is_ip_any(flow);
+    case MFP_CT_VALID:
+        return is_ct_valid(flow, mask, wc);
+    case MFP_CTV4_VALID:
+        return flow->dl_type == htons(ETH_TYPE_IP)
+            && is_ct_valid(flow, mask, wc);
+    case MFP_CTV6_VALID:
+        return flow->dl_type == htons(ETH_TYPE_IPV6)
+            && is_ct_valid(flow, mask, wc);
     case MFP_TCP:
         /* Matching !FRAG_LATER is not enforced (mask is not checked). */
         return is_tcp(flow, wc) && !(flow->nw_frag & FLOW_NW_FRAG_LATER);
@@ -475,6 +497,13 @@ mf_is_value_valid(const struct mf_field *mf, const union mf_value *value)
     case MFF_CT_ZONE:
     case MFF_CT_MARK:
     case MFF_CT_LABEL:
+    case MFF_CT_NW_PROTO:
+    case MFF_CT_NW_SRC:
+    case MFF_CT_NW_DST:
+    case MFF_CT_IPV6_SRC:
+    case MFF_CT_IPV6_DST:
+    case MFF_CT_TP_SRC:
+    case MFF_CT_TP_DST:
     CASE_MFF_REGS:
     CASE_MFF_XREGS:
     CASE_MFF_XXREGS:
@@ -649,6 +678,34 @@ mf_get_value(const struct mf_field *mf, const struct flow *flow,
         value->be128 = hton128(flow->ct_label);
         break;
 
+    case MFF_CT_NW_PROTO:
+        value->u8 = flow->ct_nw_proto;
+        break;
+
+    case MFF_CT_NW_SRC:
+        value->be32 = flow->ct_nw_src;
+        break;
+
+    case MFF_CT_NW_DST:
+        value->be32 = flow->ct_nw_dst;
+        break;
+
+    case MFF_CT_IPV6_SRC:
+        value->ipv6 = flow->ct_ipv6_src;
+        break;
+
+    case MFF_CT_IPV6_DST:
+        value->ipv6 = flow->ct_ipv6_dst;
+        break;
+
+    case MFF_CT_TP_SRC:
+        value->be16 = flow->ct_tp_src;
+        break;
+
+    case MFF_CT_TP_DST:
+        value->be16 = flow->ct_tp_dst;
+        break;
+
     CASE_MFF_REGS:
         value->be32 = htonl(flow->regs[mf->id - MFF_REG0]);
         break;
@@ -911,6 +968,34 @@ mf_set_value(const struct mf_field *mf,
         match_set_ct_label(match, ntoh128(value->be128));
         break;
 
+    case MFF_CT_NW_PROTO:
+        match_set_ct_nw_proto(match, value->u8);
+        break;
+
+    case MFF_CT_NW_SRC:
+        match_set_ct_nw_src(match, value->be32);
+        break;
+
+    case MFF_CT_NW_DST:
+        match_set_ct_nw_dst(match, value->be32);
+        break;
+
+    case MFF_CT_IPV6_SRC:
+        match_set_ct_ipv6_src(match, &value->ipv6);
+        break;
+
+    case MFF_CT_IPV6_DST:
+        match_set_ct_ipv6_dst(match, &value->ipv6);
+        break;
+
+    case MFF_CT_TP_SRC:
+        match_set_ct_tp_src(match, value->be16);
+        break;
+
+    case MFF_CT_TP_DST:
+        match_set_ct_tp_dst(match, value->be16);
+        break;
+
     CASE_MFF_REGS:
         match_set_reg(match, mf->id - MFF_REG0, ntohl(value->be32));
         break;
@@ -1242,6 +1327,34 @@ mf_set_flow_value(const struct mf_field *mf,
         flow->ct_label = ntoh128(value->be128);
         break;
 
+    case MFF_CT_NW_PROTO:
+        flow->ct_nw_proto = value->u8;
+        break;
+
+    case MFF_CT_NW_SRC:
+        flow->ct_nw_src = value->be32;
+        break;
+
+    case MFF_CT_NW_DST:
+        flow->ct_nw_dst = value->be32;
+        break;
+
+    case MFF_CT_IPV6_SRC:
+        flow->ct_ipv6_src = value->ipv6;
+        break;
+
+    case MFF_CT_IPV6_DST:
+        flow->ct_ipv6_dst = value->ipv6;
+        break;
+
+    case MFF_CT_TP_SRC:
+        flow->ct_tp_src = value->be16;
+        break;
+
+    case MFF_CT_TP_DST:
+        flow->ct_tp_dst = value->be16;
+        break;
+
     CASE_MFF_REGS:
         flow->regs[mf->id - MFF_REG0] = ntohl(value->be32);
         break;
@@ -1571,6 +1684,41 @@ mf_set_wild(const struct mf_field *mf, struct match *match, char **err_str)
         memset(&match->wc.masks.ct_label, 0, sizeof(match->wc.masks.ct_label));
         break;
 
+    case MFF_CT_NW_PROTO:
+        match->flow.ct_nw_proto = 0;
+        match->wc.masks.ct_nw_proto = 0;
+        break;
+
+    case MFF_CT_NW_SRC:
+        match->flow.ct_nw_src = 0;
+        match->wc.masks.ct_nw_src = 0;
+        break;
+
+    case MFF_CT_NW_DST:
+        match->flow.ct_nw_dst = 0;
+        match->wc.masks.ct_nw_dst = 0;
+        break;
+
+    case MFF_CT_IPV6_SRC:
+        memset(&match->flow.ct_ipv6_src, 0, sizeof(match->flow.ct_ipv6_src));
+        WC_UNMASK_FIELD(&match->wc, ct_ipv6_src);
+        break;
+
+    case MFF_CT_IPV6_DST:
+        memset(&match->flow.ct_ipv6_dst, 0, sizeof(match->flow.ct_ipv6_dst));
+        WC_UNMASK_FIELD(&match->wc, ct_ipv6_dst);
+        break;
+
+    case MFF_CT_TP_SRC:
+        match->flow.ct_tp_src = 0;
+        match->wc.masks.ct_tp_src = 0;
+        break;
+
+    case MFF_CT_TP_DST:
+        match->flow.ct_tp_dst = 0;
+        match->wc.masks.ct_tp_dst = 0;
+        break;
+
     CASE_MFF_REGS:
         match_set_reg_masked(match, mf->id - MFF_REG0, 0, 0);
         break;
@@ -1773,6 +1921,13 @@ mf_set(const struct mf_field *mf,
 
     switch (mf->id) {
     case MFF_CT_ZONE:
+    case MFF_CT_NW_PROTO:
+    case MFF_CT_NW_SRC:
+    case MFF_CT_NW_DST:
+    case MFF_CT_IPV6_SRC:
+    case MFF_CT_IPV6_DST:
+    case MFF_CT_TP_SRC:
+    case MFF_CT_TP_DST:
     case MFF_RECIRC_ID:
     case MFF_CONJ_ID:
     case MFF_IN_PORT:
diff --git a/lib/meta-flow.xml b/lib/meta-flow.xml
index 3db0f82..7a7f03d 100644
--- a/lib/meta-flow.xml
+++ b/lib/meta-flow.xml
@@ -2479,6 +2479,98 @@ actions=clone(load:0->NXM_OF_IN_PORT[],output:123)
       parameter to the <code>ct</code> action, to the connection to which the
       current packet belongs.
     </field>
+
+    <p>
+      Open vSwitch 2.8 introduced the matching support for connection
+      tracker original direction 5-tuple fields.
+    </p>
+
+    <p>
+      For non-committed non-related connections the conntrack original
+      direction tuple fields always have the same values as the
+      corresponding headers in the packet itself.  For any other packets of
+      a committed connection the conntrack original direction tuple fields
+      reflect the values from that initial non-committed non-related packet,
+      and generally are different from the actual packet headers, as the
+      actual packet headers may in reverse direction (for reply packets),
+      transformed by NAT (when \fBnat\fR option was applied to the
+      connection), or be of different protocol (i.e., when an ICMP response
+      is sent to an UDP packet).  In case of related connections, e.g., an
+      FTP data connection, the original direction tuple contains the
+      original direction headers from the master connection, e.g., an FTP
+      control connection.
+    </p>
+
+    <p>
+      The following fields are populated by the ct action, and require a
+      match to a valid connection tracking state as a prerequisite, in
+      addition to the IP or IPv6 ethertype match.  Examples of valid
+      connection tracking state matches include \fBct_state=+new\fR,
+      \fBct_state=+est\fR, \fBct_state=+rel\fR, and \fBct_state=+trk-inv\fR.
+    </p>
+
+    <field id="MFF_CT_NW_SRC" title="Connection Tracking Original Direction IPv4 Source Address">
+      Matches IPv4 conntrack original direction tuple source address.
+      See the paragraphs above for general description to the
+      conntrack original direction tuple.  Introduced in Open vSwitch
+      2.8.
+    </field>
+
+    <field id="MFF_CT_NW_DST" title="Connection Tracking Original Direction IPv4 Destination Address">
+      Matches IPv4 conntrack original direction tuple destination address.
+      See the paragraphs above for general description to the
+      conntrack original direction tuple.  Introduced in Open vSwitch
+      2.8.
+    </field>
+
+    <field id="MFF_CT_IPV6_SRC" title="Connection Tracking Original Direction IPv6 Source Address">
+      Matches IPv6 conntrack original direction tuple source address.
+      See the paragraphs above for general description to the
+      conntrack original direction tuple.  Introduced in Open vSwitch
+      2.8.
+    </field>
+
+    <field id="MFF_CT_IPV6_DST" title="Connection Tracking Original Direction IPv6 Destination Address">
+      Matches IPv6 conntrack original direction tuple destination address.
+      See the paragraphs above for general description to the
+      conntrack original direction tuple.  Introduced in Open vSwitch
+      2.8.
+    </field>
+
+    <field id="MFF_CT_NW_PROTO" title="Connection Tracking Original Direction IP Protocol">
+      Matches conntrack original direction tuple IP protocol type,
+      which is specified as a decimal number between 0 and 255,
+      inclusive (e.g. 1 to match ICMP packets or 6 to match TCP
+      packets).  In case of, for example, an ICMP response to an UDP
+      packet, this may be different from the IP protocol type of the
+      packet itself.  See the paragraphs above for general description
+      to the conntrack original direction tuple.  Introduced in Open
+      vSwitch 2.8.
+    </field>
+
+    <field id="MFF_CT_TP_SRC" title="Connection Tracking Original Direction Transport Layer Source Port">
+      Bitwise match on the conntrack original direction tuple
+      transport source, when
+      <code>MFF_CT_NW_PROTO</code> has value 6 for TCP, 17 for UDP, or
+      132 for SCTP.  When <code>MFF_CT_NW_PROTO</code> has value 1 for
+      ICMP, or 58 for ICMPv6, the lower 8 bits of
+      <code>MFF_CT_TP_SRC</code> matches the conntrack original
+      direction ICMP type.  See the paragraphs above for general
+      description to the conntrack original direction
+      tuple. Introduced in Open vSwitch 2.8.
+    </field>
+
+    <field id="MFF_CT_TP_DST" title="Connection Tracking Original Direction Transport Layer Source Port">
+      Bitwise match on the conntrack original direction tuple
+      transport destination port, when
+      <code>MFF_CT_NW_PROTO</code> has value 6 for TCP, 17 for UDP, or
+      132 for SCTP.  When <code>MFF_CT_NW_PROTO</code> has value 1 for
+      ICMP, or 58 for ICMPv6, the lower 8 bits of
+      <code>MFF_CT_TP_DST</code> matches the conntrack original
+      direction ICMP code.  See the paragraphs above for general
+      description to the conntrack original direction
+      tuple. Introduced in Open vSwitch 2.8.
+    </field>
   </group>
 
   <group title="Register">
diff --git a/lib/nx-match.c b/lib/nx-match.c
index 95516a1..92b9e12 100644
--- a/lib/nx-match.c
+++ b/lib/nx-match.c
@@ -504,6 +504,9 @@ nx_pull_match_entry(struct ofpbuf *b, bool allow_cookie,
     return 0;
 }
 
+/* Prerequisites will only be checked when 'strict' is 'true'.  This allows
+ * decoding conntrack original direction 5-tuple IP addresses without the
+ * ethertype being present, when decoding metadata only. */
 static enum ofperr
 nx_pull_raw(const uint8_t *p, unsigned int match_len, bool strict,
             struct match *match, ovs_be64 *cookie, ovs_be64 *cookie_mask,
@@ -539,7 +542,7 @@ nx_pull_raw(const uint8_t *p, unsigned int match_len, bool strict,
                 *cookie = value.be64;
                 *cookie_mask = mask.be64;
             }
-        } else if (!mf_are_match_prereqs_ok(field, match)) {
+        } else if (strict && !mf_are_match_prereqs_ok(field, match)) {
             error = OFPERR_OFPBMC_BAD_PREREQ;
         } else if (!mf_is_all_wild(field, &match->wc)) {
             error = OFPERR_OFPBMC_DUP_FIELD;
@@ -607,7 +610,8 @@ nx_pull_match(struct ofpbuf *b, unsigned int match_len, struct match *match,
 }
 
 /* Behaves the same as nx_pull_match(), but skips over unknown NXM headers,
- * instead of failing with an error. */
+ * instead of failing with an error, and does not check for field
+ * prerequisities. */
 enum ofperr
 nx_pull_match_loose(struct ofpbuf *b, unsigned int match_len,
                     struct match *match,
@@ -664,8 +668,9 @@ oxm_pull_match(struct ofpbuf *b, const struct tun_table *tun_table,
     return oxm_pull_match__(b, true, tun_table, match);
 }
 
-/* Behaves the same as oxm_pull_match() with one exception.  Skips over unknown
- * OXM headers instead of failing with an error when they are encountered. */
+/* Behaves the same as oxm_pull_match() with two exceptions.  Skips over
+ * unknown OXM headers instead of failing with an error when they are
+ * encountered, and does not check for field prerequisities. */
 enum ofperr
 oxm_pull_match_loose(struct ofpbuf *b, const struct tun_table *tun_table,
                      struct match *match)
@@ -676,14 +681,15 @@ oxm_pull_match_loose(struct ofpbuf *b, const struct tun_table *tun_table,
 /* Parses the OXM match description in the 'oxm_len' bytes in 'oxm'.  Stores
  * the result in 'match'.
  *
- * Fails with an error when encountering unknown OXM headers.
+ * Does NOT fail with an error when encountering unknown OXM headers.  Also
+ * does not check for field prerequisities.
  *
  * Returns 0 if successful, otherwise an OpenFlow error code. */
 enum ofperr
-oxm_decode_match(const void *oxm, size_t oxm_len,
-                 const struct tun_table *tun_table, struct match *match)
+oxm_decode_match_loose(const void *oxm, size_t oxm_len,
+                       const struct tun_table *tun_table, struct match *match)
 {
-    return nx_pull_raw(oxm, oxm_len, true, match, NULL, NULL, tun_table);
+    return nx_pull_raw(oxm, oxm_len, false, match, NULL, NULL, tun_table);
 }
 
 /* Verify an array of OXM TLVs treating value of each TLV as a mask,
@@ -963,7 +969,7 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
     int match_len;
     int i;
 
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     /* Metadata. */
     if (match->wc.masks.dp_hash) {
@@ -1111,7 +1117,21 @@ nx_put_raw(struct ofpbuf *b, enum ofp_version oxm, const struct match *match,
                 htonl(match->wc.masks.ct_mark));
     nxm_put_128m(b, MFF_CT_LABEL, oxm, hton128(flow->ct_label),
                  hton128(match->wc.masks.ct_label));
-
+    nxm_put_32m(b, MFF_CT_NW_SRC, oxm,
+                flow->ct_nw_src, match->wc.masks.ct_nw_src);
+    nxm_put_ipv6(b, MFF_CT_IPV6_SRC, oxm,
+                 &flow->ct_ipv6_src, &match->wc.masks.ct_ipv6_src);
+    nxm_put_32m(b, MFF_CT_NW_DST, oxm,
+                flow->ct_nw_dst, match->wc.masks.ct_nw_dst);
+    nxm_put_ipv6(b, MFF_CT_IPV6_DST, oxm,
+                 &flow->ct_ipv6_dst, &match->wc.masks.ct_ipv6_dst);
+    if (flow->ct_nw_proto) {
+        nxm_put_8(b, MFF_CT_NW_PROTO, oxm, flow->ct_nw_proto);
+        nxm_put_16m(b, MFF_CT_TP_SRC, oxm,
+                    flow->ct_tp_src, match->wc.masks.ct_tp_src);
+        nxm_put_16m(b, MFF_CT_TP_DST, oxm,
+                    flow->ct_tp_dst, match->wc.masks.ct_tp_dst);
+    }
     /* OpenFlow 1.1+ Metadata. */
     nxm_put_64m(b, MFF_METADATA, oxm,
                 flow->metadata, match->wc.masks.metadata);
diff --git a/lib/nx-match.h b/lib/nx-match.h
index 631ab48..b599731 100644
--- a/lib/nx-match.h
+++ b/lib/nx-match.h
@@ -61,8 +61,8 @@ enum ofperr oxm_pull_match(struct ofpbuf *, const struct tun_table *,
                            struct match *);
 enum ofperr oxm_pull_match_loose(struct ofpbuf *, const struct tun_table *,
                                  struct match *);
-enum ofperr oxm_decode_match(const void *, size_t, const struct tun_table *,
-                             struct match *);
+enum ofperr oxm_decode_match_loose(const void *, size_t,
+                                   const struct tun_table *, struct match *);
 enum ofperr oxm_pull_field_array(const void *, size_t fields_len,
                                  struct field_array *);
 
diff --git a/lib/odp-execute.c b/lib/odp-execute.c
index 1f6812a..50bbafa 100644
--- a/lib/odp-execute.c
+++ b/lib/odp-execute.c
@@ -381,6 +381,8 @@ odp_execute_set_action(struct dp_packet *packet, const struct nlattr *a)
     case OVS_KEY_ATTR_VLAN:
     case OVS_KEY_ATTR_TCP_FLAGS:
     case OVS_KEY_ATTR_CT_STATE:
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4:
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6:
     case OVS_KEY_ATTR_CT_ZONE:
     case OVS_KEY_ATTR_CT_MARK:
     case OVS_KEY_ATTR_CT_LABELS:
@@ -476,6 +478,8 @@ odp_execute_masked_set_action(struct dp_packet *packet,
     case OVS_KEY_ATTR_CT_ZONE:
     case OVS_KEY_ATTR_CT_MARK:
     case OVS_KEY_ATTR_CT_LABELS:
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4:
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6:
     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 4106738..b3da722 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -150,6 +150,8 @@ ovs_key_attr_to_string(enum ovs_key_attr attr, char *namebuf, size_t bufsize)
     case OVS_KEY_ATTR_CT_ZONE: return "ct_zone";
     case OVS_KEY_ATTR_CT_MARK: return "ct_mark";
     case OVS_KEY_ATTR_CT_LABELS: return "ct_label";
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4: return "ct_tuple4";
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6: return "ct_tuple6";
     case OVS_KEY_ATTR_TUNNEL: return "tunnel";
     case OVS_KEY_ATTR_IN_PORT: return "in_port";
     case OVS_KEY_ATTR_ETHERNET: return "eth";
@@ -1874,6 +1876,8 @@ static const struct attr_len_tbl ovs_flow_key_attr_lens[OVS_KEY_ATTR_MAX + 1] =
     [OVS_KEY_ATTR_CT_ZONE]   = { .len = 2 },
     [OVS_KEY_ATTR_CT_MARK]   = { .len = 4 },
     [OVS_KEY_ATTR_CT_LABELS] = { .len = sizeof(struct ovs_key_ct_labels) },
+    [OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4] = { .len = sizeof(struct ovs_key_ct_tuple_ipv4) },
+    [OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6] = { .len = sizeof(struct ovs_key_ct_tuple_ipv6) },
 };
 
 /* Returns the correct length of the payload for a flow key attribute of the
@@ -2823,6 +2827,40 @@ format_odp_key_attr(const struct nlattr *a, const struct nlattr *ma,
         break;
     }
 
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4: {
+        const struct ovs_key_ct_tuple_ipv4 *key = nl_attr_get(a);
+        const struct ovs_key_ct_tuple_ipv4 *mask = ma ? nl_attr_get(ma) : NULL;
+
+        format_ipv4(ds, "src", key->ipv4_src, MASK(mask, ipv4_src), verbose);
+        format_ipv4(ds, "dst", key->ipv4_dst, MASK(mask, ipv4_dst), verbose);
+        format_u8u(ds, "proto", key->ipv4_proto, MASK(mask, ipv4_proto),
+                      verbose);
+        format_be16(ds, "tp_src", key->src_port, MASK(mask, src_port),
+                    verbose);
+        format_be16(ds, "tp_dst", key->dst_port, MASK(mask, dst_port),
+                    verbose);
+        ds_chomp(ds, ',');
+        break;
+    }
+
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6: {
+        const struct ovs_key_ct_tuple_ipv6 *key = nl_attr_get(a);
+        const struct ovs_key_ct_tuple_ipv6 *mask = ma ? nl_attr_get(ma) : NULL;
+
+        format_in6_addr(ds, "src", &key->ipv6_src, MASK(mask, ipv6_src),
+                        verbose);
+        format_in6_addr(ds, "dst", &key->ipv6_dst, MASK(mask, ipv6_dst),
+                        verbose);
+        format_u8u(ds, "proto", key->ipv6_proto, MASK(mask, ipv6_proto),
+                      verbose);
+        format_be16(ds, "src_port", key->src_port, MASK(mask, src_port),
+                    verbose);
+        format_be16(ds, "dst_port", key->dst_port, MASK(mask, dst_port),
+                    verbose);
+        ds_chomp(ds, ',');
+        break;
+    }
+
     case OVS_KEY_ATTR_TUNNEL:
         format_odp_tun_attr(a, ma, ds, verbose);
         break;
@@ -4104,6 +4142,22 @@ parse_odp_key_mask_attr(const char *s, const struct simap *port_names,
     SCAN_SINGLE("ct_mark(", uint32_t, u32, OVS_KEY_ATTR_CT_MARK);
     SCAN_SINGLE("ct_label(", ovs_u128, u128, OVS_KEY_ATTR_CT_LABELS);
 
+    SCAN_BEGIN("ct_tuple4(", struct ovs_key_ct_tuple_ipv4) {
+        SCAN_FIELD("src=", ipv4, ipv4_src);
+        SCAN_FIELD("dst=", ipv4, ipv4_dst);
+        SCAN_FIELD("proto=", u8, ipv4_proto);
+        SCAN_FIELD("tp_src=", be16, src_port);
+        SCAN_FIELD("tp_dst=", be16, dst_port);
+    } SCAN_END(OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4);
+
+    SCAN_BEGIN("ct_tuple6(", struct ovs_key_ct_tuple_ipv6) {
+        SCAN_FIELD("src=", in6_addr, ipv6_src);
+        SCAN_FIELD("dst=", in6_addr, ipv6_dst);
+        SCAN_FIELD("proto=", u8, ipv6_proto);
+        SCAN_FIELD("tp_src=", be16, src_port);
+        SCAN_FIELD("tp_dst=", be16, dst_port);
+    } SCAN_END(OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6);
+
     SCAN_BEGIN_NESTED("tunnel(", OVS_KEY_ATTR_TUNNEL) {
         SCAN_FIELD_NESTED("tun_id=", ovs_be64, be64, OVS_TUNNEL_KEY_ATTR_ID);
         SCAN_FIELD_NESTED("src=", ovs_be32, ipv4, OVS_TUNNEL_KEY_ATTR_IPV4_SRC);
@@ -4354,6 +4408,29 @@ odp_flow_key_from_flow__(const struct odp_flow_key_parms *parms,
         nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_LABELS, &data->ct_label,
                           sizeof(data->ct_label));
     }
+    if (parms->support.ct_orig_tuple && flow->ct_nw_proto) {
+        if (flow->dl_type == htons(ETH_TYPE_IP)) {
+            struct ovs_key_ct_tuple_ipv4 ct = {
+                data->ct_nw_src,
+                data->ct_nw_dst,
+                data->ct_tp_src,
+                data->ct_tp_dst,
+                data->ct_nw_proto,
+            };
+            nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4, &ct,
+                              sizeof ct);
+        } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
+            struct ovs_key_ct_tuple_ipv6 ct = {
+                data->ct_ipv6_src,
+                data->ct_ipv6_dst,
+                data->ct_tp_src,
+                data->ct_tp_dst,
+                data->ct_nw_proto,
+            };
+            nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6, &ct,
+                              sizeof ct);
+        }
+    }
     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);
@@ -4550,6 +4627,19 @@ odp_key_from_pkt_metadata(struct ofpbuf *buf, const struct pkt_metadata *md)
             nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_LABELS, &md->ct_label,
                               sizeof(md->ct_label));
         }
+        if (md->ct_orig_tuple_ipv6) {
+            if (md->ct_orig_tuple.ipv6.ipv6_proto) {
+                nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6,
+                                  &md->ct_orig_tuple.ipv6,
+                                  sizeof md->ct_orig_tuple.ipv6);
+            }
+        } else {
+            if (md->ct_orig_tuple.ipv4.ipv4_proto) {
+                nl_msg_put_unspec(buf, OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4,
+                                  &md->ct_orig_tuple.ipv4,
+                                  sizeof md->ct_orig_tuple.ipv4);
+            }
+        }
     }
 
     /* Add an ingress port attribute if 'odp_in_port' is not the magical
@@ -4618,6 +4708,21 @@ odp_key_to_pkt_metadata(const struct nlattr *key, size_t key_len,
             wanted_attrs &= ~(1u << OVS_KEY_ATTR_CT_LABELS);
             break;
         }
+        case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4: {
+            const struct ovs_key_ct_tuple_ipv4 *ct = nl_attr_get(nla);
+            md->ct_orig_tuple.ipv4 = *ct;
+            md->ct_orig_tuple_ipv6 = false;
+            wanted_attrs &= ~(1u << OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4);
+            break;
+        }
+        case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6: {
+            const struct ovs_key_ct_tuple_ipv6 *ct = nl_attr_get(nla);
+
+            md->ct_orig_tuple.ipv6 = *ct;
+            md->ct_orig_tuple_ipv6 = true;
+            wanted_attrs &= ~(1u << OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6);
+            break;
+        }
         case OVS_KEY_ATTR_TUNNEL: {
             enum odp_key_fitness res;
 
@@ -5191,6 +5296,25 @@ odp_flow_key_to_flow__(const struct nlattr *key, size_t key_len,
         flow->ct_label = *cl;
         expected_attrs |= UINT64_C(1) << OVS_KEY_ATTR_CT_LABELS;
     }
+    if (present_attrs & (UINT64_C(1) << OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4)) {
+        const struct ovs_key_ct_tuple_ipv4 *ct = nl_attr_get(attrs[OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4]);
+        flow->ct_nw_src = ct->ipv4_src;
+        flow->ct_nw_dst = ct->ipv4_dst;
+        flow->ct_nw_proto = ct->ipv4_proto;
+        flow->ct_tp_src = ct->src_port;
+        flow->ct_tp_dst = ct->dst_port;
+        expected_attrs |= UINT64_C(1) << OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4;
+    }
+    if (present_attrs & (UINT64_C(1) << OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6)) {
+        const struct ovs_key_ct_tuple_ipv6 *ct = nl_attr_get(attrs[OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6]);
+
+        flow->ct_ipv6_src = ct->ipv6_src;
+        flow->ct_ipv6_dst = ct->ipv6_dst;
+        flow->ct_nw_proto = ct->ipv6_proto;
+        flow->ct_tp_src = ct->src_port;
+        flow->ct_tp_dst = ct->dst_port;
+        expected_attrs |= UINT64_C(1) << OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6;
+    }
 
     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 42011bc..2d00815 100644
--- a/lib/odp-util.h
+++ b/lib/odp-util.h
@@ -127,6 +127,7 @@ void odp_portno_names_destroy(struct hmap *portno_names);
  *  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_CT_ORIG_TUPLE_IPV6     40    --     4     44
  *  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
@@ -136,13 +137,13 @@ 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                                                 572
+ *  total                                                 616
  *
  * We include some slack space in case the calculation isn't quite right or we
  * add another field and forget to adjust this value.
  */
 #define ODPUTIL_FLOW_KEY_BYTES 640
-BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
 /* A buffer with sufficient size and alignment to hold an nlattr-formatted flow
  * key.  An array of "struct nlattr" might not, in theory, be sufficiently
@@ -185,6 +186,9 @@ struct odp_support {
      * 'ct_state'.  The above 'ct_state' member must be true for this
      * to make sense */
     bool ct_state_nat;
+
+    bool ct_orig_tuple;   /* Conntrack original direction tuple matching
+                           * supported. */
 };
 
 struct odp_flow_key_parms {
diff --git a/lib/ofp-util.c b/lib/ofp-util.c
index 7d40cbb..7881480 100644
--- a/lib/ofp-util.c
+++ b/lib/ofp-util.c
@@ -101,7 +101,7 @@ ofputil_netmask_to_wcbits(ovs_be32 netmask)
 void
 ofputil_wildcard_from_ofpfw10(uint32_t ofpfw, struct flow_wildcards *wc)
 {
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
     /* Initialize most of wc. */
     flow_wildcards_init_catchall(wc);
@@ -3397,8 +3397,9 @@ decode_nx_packet_in2(const struct ofp_header *oh, bool loose,
         }
 
         case NXPINT_METADATA:
-            error = oxm_decode_match(payload.msg, ofpbuf_msgsize(&payload),
-                                     tun_table, &pin->flow_metadata);
+            error = oxm_decode_match_loose(payload.msg,
+                                           ofpbuf_msgsize(&payload),
+                                           tun_table, &pin->flow_metadata);
             break;
 
         case NXPINT_USERDATA:
diff --git a/lib/packets.h b/lib/packets.h
index f7e1d82..35e5d95 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -100,9 +100,14 @@ struct pkt_metadata {
     uint32_t skb_priority;      /* Packet priority for QoS. */
     uint32_t pkt_mark;          /* Packet mark. */
     uint8_t  ct_state;          /* Connection state. */
+    bool ct_orig_tuple_ipv6;
     uint16_t ct_zone;           /* Connection zone. */
     uint32_t ct_mark;           /* Connection mark. */
     ovs_u128 ct_label;          /* Connection label. */
+    union {
+        struct ovs_key_ct_tuple_ipv4 ipv4;
+        struct ovs_key_ct_tuple_ipv6 ipv6;
+    } ct_orig_tuple;
     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-rid.h b/ofproto/ofproto-dpif-rid.h
index c357591..dfe54ff 100644
--- a/ofproto/ofproto-dpif-rid.h
+++ b/ofproto/ofproto-dpif-rid.h
@@ -99,7 +99,7 @@ struct rule;
 /* Metadata for restoring pipeline context after recirculation.  Helpers
  * are inlined below to keep them together with the definition for easier
  * updates. */
-BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
 
 struct frozen_metadata {
     /* Metadata in struct flow. */
diff --git a/ofproto/ofproto-dpif-sflow.c b/ofproto/ofproto-dpif-sflow.c
index 520b8dd..69cdf69 100644
--- a/ofproto/ofproto-dpif-sflow.c
+++ b/ofproto/ofproto-dpif-sflow.c
@@ -1025,6 +1025,8 @@ sflow_read_set_action(const struct nlattr *attr,
     case OVS_KEY_ATTR_CT_ZONE:
     case OVS_KEY_ATTR_CT_MARK:
     case OVS_KEY_ATTR_CT_LABELS:
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV4:
+    case OVS_KEY_ATTR_CT_ORIG_TUPLE_IPV6:
     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 503a347..15c18cc 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3053,6 +3053,17 @@ clear_conntrack(struct xlate_ctx *ctx)
     flow->ct_zone = 0;
     flow->ct_mark = 0;
     flow->ct_label = OVS_U128_ZERO;
+
+    flow->ct_nw_proto = 0;
+    flow->ct_tp_src = 0;
+    flow->ct_tp_dst = 0;
+    if (flow->dl_type == htons(ETH_TYPE_IP)) {
+        flow->ct_nw_src = 0;
+        flow->ct_nw_dst = 0;
+    } if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
+        memset(&flow->ct_ipv6_src, 0, sizeof flow->ct_ipv6_src);
+        memset(&flow->ct_ipv6_dst, 0, sizeof flow->ct_ipv6_dst);
+    }
 }
 
 static bool
@@ -3087,7 +3098,7 @@ compose_output_action__(struct xlate_ctx *ctx, ofp_port_t ofp_port,
 
     /* If 'struct flow' gets additional metadata, we'll need to zero it out
      * before traversing a patch port. */
-    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 36);
+    BUILD_ASSERT_DECL(FLOW_WC_SEQ == 37);
     memset(&flow_tnl, 0, sizeof flow_tnl);
 
     if (!xport) {
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index 7c7201d..a7caab8 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -1184,6 +1184,7 @@ CHECK_FEATURE(ct_zone)
 CHECK_FEATURE(ct_mark)
 CHECK_FEATURE__(ct_label, ct_label, ct_label.u64.lo, 1)
 CHECK_FEATURE__(ct_state_nat, ct_state, ct_state, CS_TRACKED|CS_SRC_NAT)
+CHECK_FEATURE__(ct_orig_tuple, ct_orig_tuple, ct_nw_proto, 1)
 
 #undef CHECK_FEATURE
 #undef CHECK_FEATURE__
@@ -1210,6 +1211,7 @@ check_support(struct dpif_backer *backer)
     backer->support.odp.ct_label = check_ct_label(backer);
 
     backer->support.odp.ct_state_nat = check_ct_state_nat(backer);
+    backer->support.odp.ct_orig_tuple = check_ct_orig_tuple(backer);
 }
 
 static int
diff --git a/tests/odp.at b/tests/odp.at
index db1e827..459ff35 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -163,7 +163,7 @@ s/$/)/' odp-base.txt
 
  echo
  echo '# Valid forms with conntrack fields.'
- sed 's/\(eth([[^)]]*)\),/\1,ct_state(+trk),ct_zone(0x5\/0xff),ct_mark(0x10305070\/0xf0f0f0f0),ct_label(0x1234567890abcdef1234567890abcdef\/0x102030405060708090a0b0c0d0e0f0),/' odp-base.txt
+ sed 's/\(eth([[^)]]*)\),/\1,ct_state(+trk),ct_zone(0x5\/0xff),ct_mark(0x10305070\/0xf0f0f0f0),ct_label(0x1234567890abcdef1234567890abcdef\/0x102030405060708090a0b0c0d0e0f0),ct_tuple4(src=10.10.10.10,dst=20.20.20.20,proto=17,tp_src=1,tp_dst=2),/' odp-base.txt
 
  echo
  echo '# Valid forms with IP first fragment.'
diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
index e861d9f..8de3142 100644
--- a/tests/ofproto-dpif.at
+++ b/tests/ofproto-dpif.at
@@ -8447,7 +8447,7 @@ AT_CHECK([cat ofctl_monitor.log], [0], [dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=42 in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=1,tp_dst=2 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 ])
 
@@ -8470,7 +8470,7 @@ AT_CHECK([cat ofctl_monitor.log], [0], [dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=42 in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=3,tp_dst=4 udp_csum:e9d2
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=3,ct_tp_dst=4,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4,tp_dst=3 udp_csum:e9d2
 ])
 
@@ -8520,7 +8520,7 @@ dnl happens because the ct_state field is available only after recirc.
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=62 in_port=1 (via action) data_len=62 (unbuffered)
 udp6,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,ipv6_src=2001:db8::1,ipv6_dst=2001:db8::2,ipv6_label=0x00000,nw_tos=112,nw_ecn=0,nw_ttl=128,tp_src=1,tp_dst=2 udp_csum:a466
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=62 ct_state=est|rpl|trk,in_port=2 (via action) data_len=62 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=62 ct_state=est|rpl|trk,ct_ipv6_src=2001:db8::1,ct_ipv6_dst=2001:db8::2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=62 (unbuffered)
 udp6,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,ipv6_src=2001:db8::2,ipv6_dst=2001:db8::1,ipv6_label=0x00000,nw_tos=112,nw_ecn=0,nw_ttl=128,tp_src=2,tp_dst=1 udp_csum:a466
 ])
 
@@ -8631,7 +8631,7 @@ OVS_WAIT_UNTIL([ovs-appctl -t ovs-ofctl exit])
 
 dnl Check this output. Only one reply must be there
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 dnl
 OFPT_ECHO_REQUEST (xid=0x0): 0 bytes of payload
@@ -8724,13 +8724,13 @@ AT_CHECK([cat ofctl_monitor.log], [0], [dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=42 in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=1,tp_dst=2 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=42 in_port=3 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=1,tp_dst=2 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_zone=1,in_port=4 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_zone=1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=4 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 ])
 
@@ -8777,10 +8777,10 @@ OVS_WAIT_UNTIL([ovs-appctl -t ovs-ofctl exit])
 
 dnl Check this output. We only see the latter two packets, not the first.
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=new|trk,in_port=1 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=new|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=1,tp_dst=2 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 ])
 
@@ -8827,10 +8827,10 @@ OVS_WAIT_UNTIL([ovs-appctl -t ovs-ofctl exit])
 
 dnl Check this output. We only see the first and the last packet
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=47 ct_state=new|trk,in_port=1 (via action) data_len=47 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=47 ct_state=new|trk,ct_nw_src=172.16.0.1,ct_nw_dst=172.16.0.2,ct_nw_proto=17,ct_tp_src=41614,ct_tp_dst=5555,in_port=1 (via action) data_len=47 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=e6:4c:47:35:28:c9,dl_dst=c6:f9:4e:cb:72:db,nw_src=172.16.0.1,nw_dst=172.16.0.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=41614,tp_dst=5555 udp_csum:2096
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=75 ct_state=rel|rpl|trk,in_port=2 (via action) data_len=75 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=75 ct_state=rel|rpl|trk,ct_nw_src=172.16.0.1,ct_nw_dst=172.16.0.2,ct_nw_proto=17,ct_tp_src=41614,ct_tp_dst=5555,in_port=2 (via action) data_len=75 (unbuffered)
 icmp,vlan_tci=0x0000,dl_src=c6:f9:4e:cb:72:db,dl_dst=e6:4c:47:35:28:c9,nw_src=172.16.0.2,nw_dst=172.16.0.1,nw_tos=192,nw_ecn=0,nw_ttl=64,icmp_type=3,icmp_code=3 icmp_csum:553f
 ])
 
@@ -8888,10 +8888,10 @@ dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=42 ct_mark=0x5,in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=5,tp_dst=6 udp_csum:e9ce
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_mark=0x1,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_mark=0x1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_mark=0x3,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_mark=0x3,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=3,ct_tp_dst=4,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4,tp_dst=3 udp_csum:e9d2
 ])
 
@@ -8936,10 +8936,10 @@ OVS_WAIT_UNTIL([ovs-appctl -t ovs-ofctl exit])
 
 dnl Check this output.
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_label=0x1,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_label=0x1,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_label=0x2,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_label=0x2,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=3,ct_tp_dst=4,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=4,tp_dst=3 udp_csum:e9d2
 ])
 
@@ -9215,7 +9215,7 @@ dnl
 NXT_PACKET_IN (xid=0x0): cookie=0x0 total_len=42 in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=1,tp_dst=2 udp_csum:e9d6
 dnl
-NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:0a,dl_dst=50:54:00:00:00:09,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=2,tp_dst=1 udp_csum:e9d6
 dnl
 NXT_PACKET_IN (xid=0x0): table_id=1 cookie=0x0 total_len=42 in_port=2 (via action) data_len=42 (unbuffered)
diff --git a/tests/ofproto.at b/tests/ofproto.at
index c899ec8..94b1149 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -2406,6 +2406,13 @@ metadata in_port in_port_oxm pkt_mark ct_mark ct_label reg0 reg1 reg2 reg3 reg4
       ct_zone: exact match or wildcard
       ct_mark: arbitrary mask
       ct_label: arbitrary mask
+      ct_nw_proto: exact match or wildcard
+      ct_nw_src: arbitrary mask
+      ct_nw_dst: arbitrary mask
+      ct_ipv6_src: arbitrary mask
+      ct_ipv6_dst: arbitrary mask
+      ct_tp_src: arbitrary mask
+      ct_tp_dst: arbitrary mask
       reg0: arbitrary mask
       reg1: arbitrary mask
       reg2: arbitrary mask
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index bb7a497..c69c526 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -612,6 +612,7 @@ AT_BANNER([conntrack])
 AT_SETUP([conntrack - controller])
 CHECK_CONNTRACK()
 OVS_TRAFFIC_VSWITCHD_START()
+AT_CHECK([ovs-appctl vlog/set dpif:dbg dpif_netdev:dbg ofproto_dpif_upcall:dbg])
 
 ADD_NAMESPACES(at_ns0, at_ns1)
 
@@ -645,7 +646,7 @@ dnl Check this output. We only see the latter two packets, not the first.
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
 NXT_PACKET_IN2 (xid=0x0): total_len=42 in_port=1 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=1,tp_dst=2 udp_csum:0
-NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=42 ct_state=est|rpl|trk,in_port=2 (via action) data_len=42 (unbuffered)
+NXT_PACKET_IN2 (xid=0x0): cookie=0x0 total_len=42 ct_state=est|rpl|trk,ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1,ct_tp_dst=2,in_port=2 (via action) data_len=42 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=50:54:00:00:00:09,dl_dst=50:54:00:00:00:0a,nw_src=10.1.1.2,nw_dst=10.1.1.1,nw_tos=0,nw_ecn=0,nw_ttl=0,tp_src=2,tp_dst=1 udp_csum:0
 ])
 
@@ -1353,9 +1354,9 @@ dnl Check this output. We only see the latter two packets, not the first.
 AT_CHECK([cat ofctl_monitor.log], [0], [dnl
 NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=75 ct_state=inv|trk,in_port=2 (via action) data_len=75 (unbuffered)
 icmp,vlan_tci=0x0000,dl_src=c6:f5:4e:cb:72:db,dl_dst=f6:4c:47:35:28:c9,nw_src=172.16.0.4,nw_dst=172.16.0.3,nw_tos=192,nw_ecn=0,nw_ttl=64,icmp_type=3,icmp_code=3 icmp_csum:da49
-NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=47 ct_state=new|trk,in_port=1 (via action) data_len=47 (unbuffered)
+NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=47 ct_state=new|trk,ct_nw_src=172.16.0.1,ct_nw_dst=172.16.0.2,ct_nw_proto=17,ct_tp_src=41614,ct_tp_dst=5555,in_port=1 (via action) data_len=47 (unbuffered)
 udp,vlan_tci=0x0000,dl_src=e6:4c:47:35:28:c9,dl_dst=c6:f9:4e:cb:72:db,nw_src=172.16.0.1,nw_dst=172.16.0.2,nw_tos=0,nw_ecn=0,nw_ttl=64,tp_src=41614,tp_dst=5555 udp_csum:2096
-NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=75 ct_state=rel|rpl|trk,in_port=2 (via action) data_len=75 (unbuffered)
+NXT_PACKET_IN2 (xid=0x0): table_id=1 cookie=0x0 total_len=75 ct_state=rel|rpl|trk,ct_nw_src=172.16.0.1,ct_nw_dst=172.16.0.2,ct_nw_proto=17,ct_tp_src=41614,ct_tp_dst=5555,in_port=2 (via action) data_len=75 (unbuffered)
 icmp,vlan_tci=0x0000,dl_src=c6:f9:4e:cb:72:db,dl_dst=e6:4c:47:35:28:c9,nw_src=172.16.0.2,nw_dst=172.16.0.1,nw_tos=192,nw_ecn=0,nw_ttl=64,icmp_type=3,icmp_code=3 icmp_csum:553f
 ])
 
@@ -1369,7 +1370,7 @@ AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.0.3)], [0], [dnl
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
-AT_SETUP([conntrack - IPv4 fragmentation ])
+AT_SETUP([conntrack - IPv4 fragmentation])
 CHECK_CONNTRACK()
 CHECK_CONNTRACK_FRAG()
 OVS_TRAFFIC_VSWITCHD_START()
@@ -2818,6 +2819,72 @@ dnl separate from the above to easier identify issues in this code on different
 dnl kernels.
 CHECK_FTP_NAT_POST_RECIRC([seqadj], [10.1.1.240], [0x0a0101f0])
 
+
+dnl CHECK_FTP_NAT_ORIG_TUPLE(TITLE, IP_ADDR, IP_ADDR_AS_HEX)
+dnl
+dnl Checks the implementation of conntrack original direction tuple matching
+dnl with FTP ALGs in combination with NAT, with flow tables that implement
+dnl the NATing before the first round of recirculation - that is, the first
+dnl flow ct(nat, table=foo) then a subsequent flow will implement the
+dnl commiting of NATed and other connections with ct(nat..),output:foo.
+dnl
+dnl IP_ADDR must specify the NAT address in standard "10.1.1.x" format,
+dnl and IP_ADDR_AS_HEX must specify the same address as hex, eg 0x0a0101xx.
+m4_define([CHECK_FTP_NAT_ORIG_TUPLE], [dnl
+    CHECK_FTP_NAT([orig tuple $1], [$2], [dnl
+dnl track all IP traffic (includes nat and helper calls to non-NEW packets.)
+table=0 ip, action=ct(nat,table=1)
+dnl
+dnl ARP
+dnl
+table=0 priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+table=0 priority=10 arp action=normal
+table=0 priority=0 action=drop
+dnl
+dnl "ACL table"
+dnl
+dnl Allow all IP traffic with conntrack original direction IP source address
+dnl '10.1.1.1'.  This should allow also reply packets and related packets in
+dnl both directions.
+table=1 ip, ct_state=+trk-inv, ct_nw_src=10.1.1.1 action=goto_table:2
+dnl Drop everything else.
+table=1 priority=0, action=drop
+dnl
+dnl "Conntrack commit table"
+dnl
+dnl Commit new outgoing FTP control connections.  Must match on 'tcp' when
+dnl setting 'alg=ftp'.
+table=2 in_port=1 priority=100 ct_state=+new, tcp, tp_dst=21, action=ct(alg=ftp,commit,nat(src=$2)),2
+dnl Commit other new outgoing IP connections.
+table=2 in_port=1 priority=20 ct_state=+new, ip, action=ct(commit,nat(src=$2)),2
+dnl Commit incoming new IP connections. 'nat' may be needed for related
+dnl connections, and is harmless for connections that do not need it.
+table=2 in_port=2 priority=10 ct_state=+new, ip, action=ct(commit,nat),1
+dnl Just forward all the rest.
+table=2 priority=0 in_port=1 action=2
+table=2 priority=0 in_port=2 action=1
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+dnl
+table=8,reg2=$3/0xffffffff,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=100 arp xreg0=0 action=normal
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+    ])
+])
+
+dnl Check that ct(nat,table=foo) works without TCP sequence adjustment with
+dnl an ACL table based on matching on conntrack original direction tuple only.
+CHECK_FTP_NAT_ORIG_TUPLE([], [10.1.1.9], [0x0a010109])
+
+dnl Check that ct(nat,table=foo) works with TCP sequence adjustment with
+dnl an ACL table based on matching on conntrack original direction tuple only.
+CHECK_FTP_NAT_ORIG_TUPLE([seqadj], [10.1.1.240], [0x0a0101f0])
+
 AT_SETUP([conntrack - IPv6 HTTP with NAT])
 CHECK_CONNTRACK()
 CHECK_CONNTRACK_NAT()
@@ -2878,9 +2945,6 @@ NS_CHECK_EXEC([at_ns1], [ip -6 neigh add fc00::240 lladdr 80:88:88:88:88:88 dev
 dnl Allow any traffic from ns0->ns1.
 dnl Only allow nd, return traffic from ns1->ns0.
 AT_DATA([flows.txt], [dnl
-dnl Allow other ICMPv6 both ways (without commit).
-table=1 priority=100 in_port=1 icmp6, action=2
-table=1 priority=100 in_port=2 icmp6, action=1
 dnl track all IPv6 traffic (this includes NAT & help to non-NEW packets.)
 table=0 priority=10 ip6, action=ct(nat,table=1)
 table=0 priority=0 action=drop
@@ -2894,6 +2958,9 @@ table=1 in_port=2 ct_state=+new+rel tcp6 ipv6_dst=fc00::240 action=ct(commit,nat
 dnl Allow established TCPv6 connections both ways, enforce NATting
 table=1 in_port=1 ct_state=+est tcp6 ipv6_src=fc00::240   action=2
 table=1 in_port=2 ct_state=+est tcp6 ipv6_dst=fc00::1     action=1
+dnl Allow other ICMPv6 both ways (without commit).
+table=1 priority=100 in_port=1 icmp6, action=2
+table=1 priority=100 in_port=2 icmp6, action=1
 dnl Drop everything else.
 table=1 priority=0, action=drop
 ])
@@ -2919,6 +2986,67 @@ tcp,orig=(src=fc00::2,dst=fc00::240,sport=<cleared>,dport=<cleared>),reply=(src=
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
+
+AT_SETUP([conntrack - IPv6 FTP with NAT - orig tuple])
+AT_SKIP_IF([test $HAVE_PYFTPDLIB = no])
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "fc00::1/96")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 80:88:88:88:88:88])
+ADD_VETH(p1, at_ns1, br0, "fc00::2/96")
+dnl Would be nice if NAT could translate neighbor discovery messages, too.
+NS_CHECK_EXEC([at_ns1], [ip -6 neigh add fc00::240 lladdr 80:88:88:88:88:88 dev p1])
+
+dnl Allow any traffic from ns0->ns1.
+dnl Only allow nd, return traffic from ns1->ns0.
+AT_DATA([flows.txt], [dnl
+dnl track all IPv6 traffic (this includes NAT & help to non-NEW packets.)
+table=0 priority=10 ip6, action=ct(nat,table=1)
+table=0 priority=0 action=drop
+dnl
+dnl Table 1
+dnl
+dnl Allow other ICMPv6 both ways (without commit).
+table=1 priority=100 in_port=1 icmp6, action=2
+table=1 priority=100 in_port=2 icmp6, action=1
+dnl Allow new TCPv6 FTP control connections.
+table=1 priority=10 in_port=1 ct_state=+new+trk-inv tcp6 ct_nw_proto=6 ct_ipv6_src=fc00::1 ct_tp_dst=21  action=ct(alg=ftp,commit,nat(src=fc00::240)),2
+dnl Allow related TCPv6 connections from port 2 to the NATted address.
+table=1 priority=10 in_port=2 ct_state=+new+rel+trk-inv ipv6 ct_nw_proto=6 ct_ipv6_src=fc00::1 ct_tp_dst=21 action=ct(commit,nat),1
+dnl Allow established TCPv6 connections both ways, enforce NATting
+table=1 priority=10 in_port=1 ct_state=+est+trk-inv ipv6 ct_nw_proto=6 ct_ipv6_src=fc00::1 ct_tp_dst=21 action=2
+table=1 priority=10 in_port=2 ct_state=+est+trk-inv ipv6 ct_nw_proto=6 ct_ipv6_src=fc00::1 ct_tp_dst=21 action=1
+dnl Drop everything else.
+table=1 priority=0, action=drop
+])
+
+AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
+
+dnl Linux seems to take a little time to get its IPv6 stack in order. Without
+dnl waiting, we get occasional failures due to the following error:
+dnl "connect: Cannot assign requested address"
+OVS_WAIT_UNTIL([ip netns exec at_ns0 ping6 -c 1 fc00::2 >/dev/null])
+
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py ftp]], [ftp0.pid])
+OVS_WAIT_UNTIL([ip netns exec at_ns1 netstat -l | grep ftp])
+
+dnl FTP requests from p0->p1 should work fine.
+NS_CHECK_EXEC([at_ns0], [wget ftp://[[fc00::2]] -6 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v --server-response --no-remove-listing -o wget0.log -d])
+
+dnl Discards CLOSE_WAIT and CLOSING
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fc00::2)], [0], [dnl
+tcp,orig=(src=fc00::1,dst=fc00::2,sport=<cleared>,dport=<cleared>),reply=(src=fc00::2,dst=fc00::240,sport=<cleared>,dport=<cleared>),protoinfo=(state=<cleared>),helper=ftp
+tcp,orig=(src=fc00::2,dst=fc00::240,sport=<cleared>,dport=<cleared>),reply=(src=fc00::1,dst=fc00::2,sport=<cleared>,dport=<cleared>),protoinfo=(state=<cleared>)
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+
 AT_SETUP([conntrack - DNAT load balancing])
 CHECK_CONNTRACK()
 CHECK_CONNTRACK_NAT()
-- 
2.1.4




More information about the dev mailing list