diff mbox

[ovs-dev,v2,15/22] odp: Support conntrack orig tuple key.

Message ID 1488331058-40038-16-git-send-email-jarno@ovn.org
State Superseded
Headers show

Commit Message

Jarno Rajahalme March 1, 2017, 1:17 a.m. UTC
Userspace support for datapath original direction conntrack tuple.

Signed-off-by: Jarno Rajahalme <jarno@ovn.org>
---
 build-aux/extract-ofp-fields    |   3 +
 include/openvswitch/flow.h      |  15 ++-
 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, 1115 insertions(+), 119 deletions(-)

Comments

Joe Stringer March 3, 2017, 6:09 p.m. UTC | #1
On 28 February 2017 at 17:17, Jarno Rajahalme <jarno@ovn.org> wrote:
> Userspace support for datapath original direction conntrack tuple.
>
> Signed-off-by: Jarno Rajahalme <jarno@ovn.org>

Thanks for the submission. Some feedback below.

<snip>

> 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

"Tracked" in the current API documentation refers to whether the
packet was submitted to the connection tracker during the current
pipeline processing, and not connection state. To refer to connections
which have been committed, we call that "committed". See the
"Connection Tracking Fields" section of ovs-fields(7) for more
details.

<snip>

> @@ -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;
>  }

It is strange for this parse_icmpv6() function to return one of two
values, but only populate arp_buf[] in some of the cases that it
returns true -- while the caller does initialize the buffer, it'd be
more robust if this function consistently initialized the buffer.

> 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)

So these bits indicate valid? (...)

> +            || (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)

(...) But for these bits, we define a macro to identify a different
set of valid bits?

> 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

Picking nits a bit, but this states that the ct 5tuple is generally
different from the packet headers, but this depends on your traffic
pattern and your actions. If the majority of the traffic for the
connection is in the forward direction and there's no NAT involved,
this statement doesn't really hold.

> +      actual packet headers may in reverse direction (for reply packets),

"may in"

> @@ -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.

The original comment defines return value/early exit semantics from
this function, but the new comment just defines the side-effects /
inner operation of the function. This is a bit more natural to read if
it's integrated into the overall description of this function in the
previous comment --- just like you did for nx_pull_match_loose().


> @@ -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:

Previously we would immediately reject unknown OXM headers; now we
don't due to the above, right? Is this intentional? This kind of
behavioural change would be easier to validate if there was a separate
patch outlining why, what it affects, etc.

> 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

Should there be a comment that if ct_state is populated, then
ct_orig_tuple is populated? If "ct_orig_tuple_ipv6" is true, then
ct_orig_tuple.ipv6 is used, otherwise ct_orig_tuple.ipv4 is used.
Jarno Rajahalme March 3, 2017, 10:36 p.m. UTC | #2
Thanks for the review Joe!

> On Mar 3, 2017, at 10:09 AM, Joe Stringer <joe@ovn.org> wrote:
> 
> On 28 February 2017 at 17:17, Jarno Rajahalme <jarno@ovn.org> wrote:
>> Userspace support for datapath original direction conntrack tuple.
>> 
>> Signed-off-by: Jarno Rajahalme <jarno@ovn.org>
> 
> Thanks for the submission. Some feedback below.
> 
> <snip>
> 
>> 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
> 
> "Tracked" in the current API documentation refers to whether the
> packet was submitted to the connection tracker during the current
> pipeline processing, and not connection state. To refer to connections
> which have been committed, we call that "committed". See the
> "Connection Tracking Fields" section of ovs-fields(7) for more
> details.
> 

The intent is to not require the connection to be committed, as the value is properly populated also for the “new” packets. In the above, I was using the term “tracked connection” in a more general sense, and did not intend to refer to the “packet is tracked” ct_state bit. Maybe I should change this to “valid connection”?

> <snip>
> 
>> @@ -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;
>> }
> 
> It is strange for this parse_icmpv6() function to return one of two
> values, but only populate arp_buf[] in some of the cases that it
> returns true -- while the caller does initialize the buffer, it'd be
> more robust if this function consistently initialized the buffer.
> 

Right, I’ll refactor this.

>> 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)
> 
> So these bits indicate valid? (...)
> 
>> +            || (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)
> 
> (...) But for these bits, we define a macro to identify a different
> set of valid bits?
> 

Right. The first case is for validating OpenFlow rule matches. Rules are free to not match on all the ct_state bits, se we accept any of them as evidence that matching packet is associated with a valid connection tracking entry, and therefore that the orig tuple fields are populated.

The second case is for validating extracted flows, which do not have a (match) mask, but which generate a wildcard pattern. In this case all ct_state bits are present (either as ones or zeros), and we can minimize the number of bits to add to the wildcard mask. For example, a related packet is always either new, established, or reply(I recall ICMP response being a related reply, but not new or established). same goes for NATted packets.

>> 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
> 
> Picking nits a bit, but this states that the ct 5tuple is generally
> different from the packet headers, but this depends on your traffic
> pattern and your actions. If the majority of the traffic for the
> connection is in the forward direction and there's no NAT involved,
> this statement doesn't really hold.
> 

Replaced “, and generally are different from the actual packet headers,”
with “, and thus may be different from the actual packet headers,”

>> +      actual packet headers may in reverse direction (for reply packets),
> 
> "may in”
> 

replaced with “may be in"

>> @@ -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.
> 
> The original comment defines return value/early exit semantics from
> this function, but the new comment just defines the side-effects /
> inner operation of the function. This is a bit more natural to read if
> it's integrated into the overall description of this function in the
> previous comment --- just like you did for nx_pull_match_loose().
> 

nx_pull_match_loose() has the non-loose version we refer to the in the commit message, but in this case where is no non-loose version. I changed the comment to:

/* Parses the OXM match description in the 'oxm_len' bytes in 'oxm'.  Stores
 * the result in 'match'.
 *
 * Returns 0 if successful, otherwise an OpenFlow error code.
 *
 * Encountering unknown OXM headers or missing field prerequisites are not
 * considered as error conditions.
 */


> 
>> @@ -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:
> 
> Previously we would immediately reject unknown OXM headers; now we
> don't due to the above, right? Is this intentional? This kind of
> behavioural change would be easier to validate if there was a separate
> patch outlining why, what it affects, etc.
> 

Previously packet metadata fields did not have prerequisites, but the new orig tuple fields do, for the purposes of accepting OpenFlow rules. Here I did not want to party the packet again to check the prerequisites, as the packet_in2 is generated by OVS itself.

I had added this comment on top of nx_pull_raw():

/* 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. */

The skipping of unknown OXMs here is not strictly needed, but “came in” with the use of the existing “_loose” functions. Checking in with Ben on how OVN used this we figured out that this is actually what we want to do. Generally, the controller should ignore unknown fields, so that the switch can add new functionality without requiring synchronous controller upgrades. When packet_in2 is used for a continuation, the controller can copy the metadata from the original packet_in2 message, including all the unknown fields, to the resume message it is sending to the switch. OVN does this already.

I’ll move this to a separate patch to highlight this change.

>> 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
> 
> Should there be a comment that if ct_state is populated, then
> ct_orig_tuple is populated? If "ct_orig_tuple_ipv6" is true, then
> ct_orig_tuple.ipv6 is used, otherwise ct_orig_tuple.ipv4 is used.

Comments added.

  Jarno
Joe Stringer March 3, 2017, 10:46 p.m. UTC | #3
On 3 March 2017 at 14:36, Jarno Rajahalme <jarno@ovn.org> wrote:
> Thanks for the review Joe!
>
>> On Mar 3, 2017, at 10:09 AM, Joe Stringer <joe@ovn.org> wrote:
>>
>> On 28 February 2017 at 17:17, Jarno Rajahalme <jarno@ovn.org> wrote:
>>> Userspace support for datapath original direction conntrack tuple.
>>>
>>> Signed-off-by: Jarno Rajahalme <jarno@ovn.org>
>>
>> Thanks for the submission. Some feedback below.
>>
>> <snip>
>>
>>> 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
>>
>> "Tracked" in the current API documentation refers to whether the
>> packet was submitted to the connection tracker during the current
>> pipeline processing, and not connection state. To refer to connections
>> which have been committed, we call that "committed". See the
>> "Connection Tracking Fields" section of ovs-fields(7) for more
>> details.
>>
>
> The intent is to not require the connection to be committed, as the value is properly populated also for the “new” packets. In the above, I was using the term “tracked connection” in a more general sense, and did not intend to refer to the “packet is tracked” ct_state bit. Maybe I should change this to “valid connection”?

Yes, I think that "valid connection" would be more clear - I don't
think there's anything in the docs that might confuse the meaning of
that.

>>> @@ -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:
>>
>> Previously we would immediately reject unknown OXM headers; now we
>> don't due to the above, right? Is this intentional? This kind of
>> behavioural change would be easier to validate if there was a separate
>> patch outlining why, what it affects, etc.
>>
>
> Previously packet metadata fields did not have prerequisites, but the new orig tuple fields do, for the purposes of accepting OpenFlow rules. Here I did not want to party the packet again to check the prerequisites, as the packet_in2 is generated by OVS itself.
>
> I had added this comment on top of nx_pull_raw():
>
> /* 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. */
>
> The skipping of unknown OXMs here is not strictly needed, but “came in” with the use of the existing “_loose” functions. Checking in with Ben on how OVN used this we figured out that this is actually what we want to do. Generally, the controller should ignore unknown fields, so that the switch can add new functionality without requiring synchronous controller upgrades. When packet_in2 is used for a continuation, the controller can copy the metadata from the original packet_in2 message, including all the unknown fields, to the resume message it is sending to the switch. OVN does this already.
>
> I’ll move this to a separate patch to highlight this change.

OK, that sounds good thanks!
diff mbox

Patch

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..5cd78e4 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,8 +130,11 @@  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. */
+    ovs_be32 pad4;              /* Pad to 64 bits. */
 };
 BUILD_ASSERT_DECL(sizeof(struct flow) % sizeof(uint64_t) == 0);
 BUILD_ASSERT_DECL(sizeof(struct flow_tnl) % sizeof(uint64_t) == 0);
@@ -136,8 +143,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 89fc3a4..0a6b730 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -3061,6 +3061,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
@@ -3095,7 +3106,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 e595f3b..682b084 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()