diff mbox series

[ovs-dev,v3,2/2] openflow: Add extension to flush CT by generic match

Message ID 20221215124557.358665-3-amusil@redhat.com
State Superseded
Headers show
Series Add extension for partial CT flush | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/intel-ovs-compilation fail test: fail

Commit Message

Ales Musil Dec. 15, 2022, 12:45 p.m. UTC
Add extension that allows to flush connections from CT
by specifying fields that the connections should be
matched against. This allows to match only some fields
of the connection e.g. source address for orig direrction.

Reported-at: https://bugzilla.redhat.com/2120546
Signed-off-by: Ales Musil <amusil@redhat.com>
---
v3: Rebase on top of master.
v2: Rebase on top of master.
    Use suggestion from Ilya.
---
 NEWS                           |   3 +
 include/openflow/nicira-ext.h  |  30 +++++++
 include/openvswitch/ofp-msgs.h |   4 +
 include/openvswitch/ofp-util.h |   4 +
 lib/ofp-bundle.c               |   1 +
 lib/ofp-ct-util.c              | 146 +++++++++++++++++++++++++++++++++
 lib/ofp-ct-util.h              |   9 ++
 lib/ofp-print.c                |  20 +++++
 lib/ofp-util.c                 |  25 ++++++
 lib/rconn.c                    |   1 +
 ofproto/ofproto-dpif.c         |   8 +-
 ofproto/ofproto-provider.h     |   7 +-
 ofproto/ofproto.c              |  30 ++++++-
 tests/ofp-print.at             |  93 +++++++++++++++++++++
 tests/ovs-ofctl.at             |  12 +++
 tests/system-traffic.at        | 116 ++++++++++++++------------
 utilities/ovs-ofctl.c          |  38 +++++++++
 17 files changed, 489 insertions(+), 58 deletions(-)

Comments

Paolo Valerio Dec. 15, 2022, 4:44 p.m. UTC | #1
Ales Musil <amusil@redhat.com> writes:

> Add extension that allows to flush connections from CT
> by specifying fields that the connections should be
> matched against. This allows to match only some fields
> of the connection e.g. source address for orig direrction.
>
> Reported-at: https://bugzilla.redhat.com/2120546
> Signed-off-by: Ales Musil <amusil@redhat.com>
> ---
> v3: Rebase on top of master.
> v2: Rebase on top of master.
>     Use suggestion from Ilya.
> ---

Although a second opinion would be nice to have here,
the patch LGTM and the tests succeeded.

Acked-by: Paolo Valerio <pvalerio@redhat.com>

>  NEWS                           |   3 +
>  include/openflow/nicira-ext.h  |  30 +++++++
>  include/openvswitch/ofp-msgs.h |   4 +
>  include/openvswitch/ofp-util.h |   4 +
>  lib/ofp-bundle.c               |   1 +
>  lib/ofp-ct-util.c              | 146 +++++++++++++++++++++++++++++++++
>  lib/ofp-ct-util.h              |   9 ++
>  lib/ofp-print.c                |  20 +++++
>  lib/ofp-util.c                 |  25 ++++++
>  lib/rconn.c                    |   1 +
>  ofproto/ofproto-dpif.c         |   8 +-
>  ofproto/ofproto-provider.h     |   7 +-
>  ofproto/ofproto.c              |  30 ++++++-
>  tests/ofp-print.at             |  93 +++++++++++++++++++++
>  tests/ovs-ofctl.at             |  12 +++
>  tests/system-traffic.at        | 116 ++++++++++++++------------
>  utilities/ovs-ofctl.c          |  38 +++++++++
>  17 files changed, 489 insertions(+), 58 deletions(-)
>
> diff --git a/NEWS b/NEWS
> index ff8904b02..46b8faa41 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -16,6 +16,9 @@ Post-v3.0.0
>       by specifying 'max-rate' or '[r]stp-path-cost' accordingly.
>     - ovs-dpctl and related ovs-appctl commands:
>       * "flush-conntrack" is capable of handling partial 5-tuple.
> +   - OpenFlow:
> +      * New OpenFlow extension NXT_CT_FLUSH to flush connections matching
> +        the specified fields.
>  
>  
>  v3.0.0 - 15 Aug 2022
> diff --git a/include/openflow/nicira-ext.h b/include/openflow/nicira-ext.h
> index b68804991..32ce56d31 100644
> --- a/include/openflow/nicira-ext.h
> +++ b/include/openflow/nicira-ext.h
> @@ -1064,4 +1064,34 @@ struct nx_zone_id {
>  };
>  OFP_ASSERT(sizeof(struct nx_zone_id) == 8);
>  
> +/* CT flush available TLVs. */
> +enum nx_ct_flush_tlv_type {
> +    /* Outer types. */
> +    NXT_CT_ORIG_DIRECTION,    /* CT orig direction outer type. */
> +    NXT_CT_REPLY_DIRECTION,   /* CT reply direction outer type. */
> +
> +    /* Nested types. */
> +    NXT_CT_SRC,               /* CT source IPv6 or mapped IPv4 address. */
> +    NXT_CT_DST,               /* CT destination IPv6 or mapped IPv4 address. */
> +    NXT_CT_SRC_PORT,          /* CT source port. */
> +    NXT_CT_DST_PORT,          /* CT destination port. */
> +    NXT_CT_ICMP_ID,           /* CT ICMP id. */
> +    NXT_CT_ICMP_TYPE,         /* CT ICMP type. */
> +    NXT_CT_ICMP_CODE,         /* CT ICMP code. */
> +
> +    /* Primitive types. */
> +    NXT_CT_ZONE_ID,           /* CT zone id. */
> +};
> +
> +/* NXT_CT_FLUSH.
> + *
> + * Flushes the connection tracking specified by 5-tuple.
> + * The struct should be followed by TLVs specifying the matching parameters. */
> +struct nx_ct_flush {
> +    uint8_t ip_proto;          /* IP protocol. */
> +    uint8_t family;            /* L3 address family. */
> +    uint8_t zero[6];           /* Must be zero. */
> +};
> +OFP_ASSERT(sizeof(struct nx_ct_flush) == 8);
> +
>  #endif /* openflow/nicira-ext.h */
> diff --git a/include/openvswitch/ofp-msgs.h b/include/openvswitch/ofp-msgs.h
> index 921a937e5..659b0a3e7 100644
> --- a/include/openvswitch/ofp-msgs.h
> +++ b/include/openvswitch/ofp-msgs.h
> @@ -526,6 +526,9 @@ enum ofpraw {
>  
>      /* NXST 1.0+ (4): struct nx_ipfix_stats_reply[]. */
>      OFPRAW_NXST_IPFIX_FLOW_REPLY,
> +
> +    /* NXT 1.0+ (32): struct nx_ct_flush, uint8_t[8][]. */
> +    OFPRAW_NXT_CT_FLUSH,
>  };
>  
>  /* Decoding messages into OFPRAW_* values. */
> @@ -772,6 +775,7 @@ enum ofptype {
>      OFPTYPE_IPFIX_FLOW_STATS_REQUEST, /* OFPRAW_NXST_IPFIX_FLOW_REQUEST */
>      OFPTYPE_IPFIX_FLOW_STATS_REPLY,   /* OFPRAW_NXST_IPFIX_FLOW_REPLY */
>      OFPTYPE_CT_FLUSH_ZONE,            /* OFPRAW_NXT_CT_FLUSH_ZONE. */
> +    OFPTYPE_CT_FLUSH,           /* OFPRAW_NXT_CT_FLUSH. */
>  
>      /* Flow monitor extension. */
>      OFPTYPE_FLOW_MONITOR_CANCEL,  /* OFPRAW_NXT_FLOW_MONITOR_CANCEL.
> diff --git a/include/openvswitch/ofp-util.h b/include/openvswitch/ofp-util.h
> index 84937ae26..e10d90b9f 100644
> --- a/include/openvswitch/ofp-util.h
> +++ b/include/openvswitch/ofp-util.h
> @@ -65,6 +65,10 @@ struct ofpbuf *ofputil_encode_echo_reply(const struct ofp_header *);
>  
>  struct ofpbuf *ofputil_encode_barrier_request(enum ofp_version);
>  
> +struct ofpbuf *ofputil_ct_match_encode(const struct ofputil_ct_match *match,
> +                                       uint16_t *zone_id,
> +                                       enum ofp_version version);
> +
>  #ifdef __cplusplus
>  }
>  #endif
> diff --git a/lib/ofp-bundle.c b/lib/ofp-bundle.c
> index 0161c2bc6..941a8370e 100644
> --- a/lib/ofp-bundle.c
> +++ b/lib/ofp-bundle.c
> @@ -292,6 +292,7 @@ ofputil_is_bundlable(enum ofptype type)
>      case OFPTYPE_IPFIX_FLOW_STATS_REQUEST:
>      case OFPTYPE_IPFIX_FLOW_STATS_REPLY:
>      case OFPTYPE_CT_FLUSH_ZONE:
> +    case OFPTYPE_CT_FLUSH:
>          break;
>      }
>  
> diff --git a/lib/ofp-ct-util.c b/lib/ofp-ct-util.c
> index 9112305cc..276932ea6 100644
> --- a/lib/ofp-ct-util.c
> +++ b/lib/ofp-ct-util.c
> @@ -23,8 +23,12 @@
>  
>  #include "ct-dpif.h"
>  #include "ofp-ct-util.h"
> +#include "openflow/nicira-ext.h"
>  #include "openvswitch/dynamic-string.h"
> +#include "openvswitch/ofp-msgs.h"
>  #include "openvswitch/ofp-parse.h"
> +#include "openvswitch/ofp-errors.h"
> +#include "openvswitch/ofp-prop.h"
>  #include "openvswitch/ofp-util.h"
>  #include "openvswitch/packets.h"
>  
> @@ -309,3 +313,145 @@ error:
>      free(copy);
>      return false;
>  }
> +
> +static enum ofperr
> +ofpprop_pull_value(struct ofpbuf *property, void *value, size_t len)
> +{
> +    if (ofpbuf_msgsize(property) < len) {
> +        return OFPERR_OFPBPC_BAD_LEN;
> +    }
> +
> +    memcpy(value, property->msg, len);
> +
> +    return 0;
> +}
> +
> +static enum ofperr
> +ofputil_ct_tuple_decode_nested(struct ofpbuf *property,
> +                               struct ofputil_ct_tuple *tuple)
> +{
> +    struct ofpbuf nested;
> +    enum ofperr error = ofpprop_parse_nested(property, &nested);
> +    if (error) {
> +        return error;
> +    }
> +
> +    while (nested.size) {
> +        struct ofpbuf inner;
> +        uint64_t type;
> +
> +        error = ofpprop_pull(&nested, &inner, &type);
> +        if (error) {
> +            return error;
> +        }
> +        switch (type) {
> +            case NXT_CT_SRC:
> +                ofpprop_pull_value(&inner, &tuple->src, sizeof tuple->src);
> +                break;
> +            case NXT_CT_DST:
> +                ofpprop_pull_value(&inner, &tuple->dst, sizeof tuple->dst);
> +                break;
> +            case NXT_CT_SRC_PORT:
> +                ofpprop_parse_be16(&inner, &tuple->src_port);
> +                break;
> +            case NXT_CT_DST_PORT:
> +                ofpprop_parse_be16(&inner, &tuple->dst_port);
> +                break;
> +            case NXT_CT_ICMP_ID:
> +                ofpprop_parse_be16(&inner, &tuple->icmp_id);
> +                break;
> +            case NXT_CT_ICMP_TYPE:
> +                ofpprop_parse_u8(&inner, &tuple->icmp_type);
> +                break;
> +            case NXT_CT_ICMP_CODE:
> +                ofpprop_parse_u8(&inner, &tuple->icmp_code);
> +        }
> +    }
> +
> +    return 0;
> +}
> +
> +enum ofperr
> +ofputil_ct_match_decode(struct ofputil_ct_match *match, bool *with_zone,
> +                        uint16_t *zone_id, const struct ofp_header *oh)
> +{
> +    struct ofpbuf msg = ofpbuf_const_initializer(oh, ntohs(oh->length));
> +    ofpraw_pull_assert(&msg);
> +
> +    const struct nx_ct_flush *nx_flush = ofpbuf_pull(&msg, sizeof *nx_flush);
> +
> +    if (!is_all_zeros(nx_flush->zero, sizeof nx_flush->zero)) {
> +        return OFPERR_NXBRC_MUST_BE_ZERO;
> +    }
> +
> +    match->l3_type = nx_flush->family;
> +    match->ip_proto = nx_flush->ip_proto;
> +
> +    struct ofputil_ct_tuple *orig = &match->tuple_orig;
> +    struct ofputil_ct_tuple *reply = &match->tuple_reply;
> +
> +    while (msg.size) {
> +        struct ofpbuf property;
> +        uint64_t type;
> +
> +        enum ofperr error = ofpprop_pull(&msg, &property, &type);
> +        if (error) {
> +            return error;
> +        }
> +
> +        switch (type) {
> +            case NXT_CT_ORIG_DIRECTION:
> +                ofputil_ct_tuple_decode_nested(&property, orig);
> +                break;
> +            case NXT_CT_REPLY_DIRECTION:
> +                ofputil_ct_tuple_decode_nested(&property, reply);
> +                break;
> +            case NXT_CT_ZONE_ID:
> +                if (with_zone) {
> +                    *with_zone = true;
> +                }
> +                ofpprop_parse_u16(&property, zone_id);
> +                break;
> +        }
> +    }
> +
> +    return 0;
> +}
> +
> +void
> +ofputil_ct_tuple_encode(const struct ofputil_ct_tuple *tuple,
> +                        struct ofpbuf *buf, enum nx_ct_flush_tlv_type type,
> +                        uint8_t ip_proto)
> +{
> +    /* 128 B is enough to hold the whole tuple. */
> +    uint8_t stub[128];
> +    struct ofpbuf nested = OFPBUF_STUB_INITIALIZER(stub);
> +
> +    if (!ipv6_is_zero(&tuple->src)) {
> +        ofpprop_put(&nested, NXT_CT_SRC, &tuple->src, sizeof tuple->src);
> +    }
> +
> +    if (!ipv6_is_zero(&tuple->dst)) {
> +        ofpprop_put(&nested, NXT_CT_DST, &tuple->dst, sizeof tuple->dst);
> +    }
> +
> +    if (ip_proto == IPPROTO_ICMP || ip_proto == IPPROTO_ICMPV6) {
> +        ofpprop_put_be16(&nested, NXT_CT_ICMP_ID, tuple->icmp_id);
> +        ofpprop_put_u8(&nested, NXT_CT_ICMP_TYPE, tuple->icmp_type);
> +        ofpprop_put_u8(&nested, NXT_CT_ICMP_CODE, tuple->icmp_code);
> +    } else {
> +        if (tuple->src_port) {
> +            ofpprop_put_be16(&nested, NXT_CT_SRC_PORT, tuple->src_port);
> +        }
> +
> +        if (tuple->dst_port) {
> +            ofpprop_put_be16(&nested, NXT_CT_DST_PORT, tuple->dst_port);
> +        }
> +    }
> +
> +    if (nested.size) {
> +        ofpprop_put_nested(buf, type, &nested);
> +    }
> +
> +    ofpbuf_uninit(&nested);
> +}
> diff --git a/lib/ofp-ct-util.h b/lib/ofp-ct-util.h
> index a53d73cb0..5d862500f 100644
> --- a/lib/ofp-ct-util.h
> +++ b/lib/ofp-ct-util.h
> @@ -17,6 +17,7 @@
>  #define OVS_OFP_CT_UTIL_H
>  
>  #include "ct-dpif.h"
> +#include "openflow/nicira-ext.h"
>  #include "openvswitch/ofp-util.h"
>  
>  bool ofputil_ct_match_cmp(const struct ofputil_ct_match *match,
> @@ -31,4 +32,12 @@ void ofputil_ct_match_format(struct ds *ds,
>  bool ofputil_ct_match_parse(struct ofputil_ct_match *match, const char *s,
>                              struct ds *ds);
>  
> +enum ofperr ofputil_ct_match_decode(struct ofputil_ct_match *match,
> +                                    bool *with_zone, uint16_t *zone_id,
> +                                    const struct ofp_header *oh);
> +
> +void ofputil_ct_tuple_encode(const struct ofputil_ct_tuple *tuple,
> +                             struct ofpbuf *buf,
> +                             enum nx_ct_flush_tlv_type type, uint8_t ip_proto);
> +
>  #endif /* lib/ofp-ct-util.h */
> diff --git a/lib/ofp-print.c b/lib/ofp-print.c
> index bd37fa17a..94bfa7070 100644
> --- a/lib/ofp-print.c
> +++ b/lib/ofp-print.c
> @@ -36,6 +36,7 @@
>  #include "learn.h"
>  #include "multipath.h"
>  #include "netdev.h"
> +#include "ofp-ct-util.h"
>  #include "nx-match.h"
>  #include "odp-util.h"
>  #include "openflow/nicira-ext.h"
> @@ -949,6 +950,23 @@ ofp_print_nxt_ct_flush_zone(struct ds *string, const struct nx_zone_id *nzi)
>      return 0;
>  }
>  
> +static enum ofperr
> +ofp_print_nxt_ct_flush(struct ds *string, const struct ofp_header *oh)
> +{
> +    uint16_t zone_id = 0;
> +    struct ofputil_ct_match match = {0};
> +
> +    enum ofperr error = ofputil_ct_match_decode(&match, NULL, &zone_id, oh);
> +    if (error) {
> +        return error;
> +    }
> +
> +    ofputil_ct_match_format(string, &match);
> +    ds_put_format(string, ",zone_id=%"PRIu16, zone_id);
> +
> +    return 0;
> +}
> +
>  static enum ofperr
>  ofp_to_string__(const struct ofp_header *oh,
>                  const struct ofputil_port_map *port_map,
> @@ -1184,6 +1202,8 @@ ofp_to_string__(const struct ofp_header *oh,
>  
>      case OFPTYPE_CT_FLUSH_ZONE:
>          return ofp_print_nxt_ct_flush_zone(string, ofpmsg_body(oh));
> +    case OFPTYPE_CT_FLUSH:
> +        return ofp_print_nxt_ct_flush(string, oh);
>      }
>  
>      return 0;
> diff --git a/lib/ofp-util.c b/lib/ofp-util.c
> index a324ceeea..a14cb6860 100644
> --- a/lib/ofp-util.c
> +++ b/lib/ofp-util.c
> @@ -31,6 +31,7 @@
>  #include "multipath.h"
>  #include "netdev.h"
>  #include "nx-match.h"
> +#include "ofp-ct-util.h"
>  #include "id-pool.h"
>  #include "openflow/netronome-ext.h"
>  #include "openvswitch/dynamic-string.h"
> @@ -237,3 +238,27 @@ ofputil_encode_barrier_request(enum ofp_version ofp_version)
>  
>      return ofpraw_alloc(type, ofp_version, 0);
>  }
> +
> +struct ofpbuf *
> +ofputil_ct_match_encode(const struct ofputil_ct_match *match,
> +                        uint16_t *zone_id, enum ofp_version version)
> +{
> +    struct ofpbuf *msg = ofpraw_alloc(OFPRAW_NXT_CT_FLUSH, version, 0);
> +    struct nx_ct_flush *nx_flush = ofpbuf_put_zeros(msg, sizeof *nx_flush);
> +    const struct ofputil_ct_tuple *orig = &match->tuple_orig;
> +    const struct ofputil_ct_tuple *reply = &match->tuple_reply;
> +
> +    nx_flush->ip_proto = match->ip_proto;
> +    nx_flush->family = match->l3_type;
> +
> +    ofputil_ct_tuple_encode(orig, msg, NXT_CT_ORIG_DIRECTION,
> +                            match->ip_proto);
> +    ofputil_ct_tuple_encode(reply, msg, NXT_CT_REPLY_DIRECTION,
> +                            match->ip_proto);
> +
> +    if (zone_id) {
> +        ofpprop_put_u16(msg, NXT_CT_ZONE_ID, *zone_id);
> +    }
> +
> +    return msg;
> +}
> diff --git a/lib/rconn.c b/lib/rconn.c
> index a96b2eb8b..4afa21515 100644
> --- a/lib/rconn.c
> +++ b/lib/rconn.c
> @@ -1426,6 +1426,7 @@ is_admitted_msg(const struct ofpbuf *b)
>      case OFPTYPE_IPFIX_FLOW_STATS_REQUEST:
>      case OFPTYPE_IPFIX_FLOW_STATS_REPLY:
>      case OFPTYPE_CT_FLUSH_ZONE:
> +    case OFPTYPE_CT_FLUSH:
>      default:
>          return true;
>      }
> diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
> index f9562dee8..29174a585 100644
> --- a/ofproto/ofproto-dpif.c
> +++ b/ofproto/ofproto-dpif.c
> @@ -5358,11 +5358,12 @@ type_set_config(const char *type, const struct smap *other_config)
>  }
>  
>  static void
> -ct_flush(const struct ofproto *ofproto_, const uint16_t *zone)
> +ct_flush(const struct ofproto *ofproto_, const uint16_t *zone,
> +         const struct ofputil_ct_match *match)
>  {
>      struct ofproto_dpif *ofproto = ofproto_dpif_cast(ofproto_);
>  
> -    ct_dpif_flush(ofproto->backer->dpif, zone, NULL);
> +    ct_dpif_flush(ofproto->backer->dpif, zone, match);
>  }
>  
>  static struct ct_timeout_policy *
> @@ -5674,6 +5675,9 @@ get_datapath_cap(const char *datapath_type, struct smap *cap)
>      smap_add(cap, "lb_output_action", s.lb_output_action ? "true" : "false");
>      smap_add(cap, "ct_zero_snat", s.ct_zero_snat ? "true" : "false");
>      smap_add(cap, "add_mpls", s.add_mpls ? "true" : "false");
> +    /* The ct_tuple_flush is implemented on dpif level, so it is supported
> +     * for all backers. */
> +    smap_add(cap, "ct_flush", "true");
>  }
>  
>  /* Gets timeout policy name in 'backer' based on 'zone', 'dl_type' and
> diff --git a/ofproto/ofproto-provider.h b/ofproto/ofproto-provider.h
> index 7e3fb6698..5e39234f9 100644
> --- a/ofproto/ofproto-provider.h
> +++ b/ofproto/ofproto-provider.h
> @@ -49,6 +49,7 @@
>  #include "openvswitch/ofp-port.h"
>  #include "openvswitch/ofp-switch.h"
>  #include "openvswitch/ofp-table.h"
> +#include "openvswitch/ofp-util.h"
>  #include "ovs-atomic.h"
>  #include "ovs-rcu.h"
>  #include "ovs-thread.h"
> @@ -1902,8 +1903,10 @@ struct ofproto_class {
>  /* ## Connection tracking ## */
>  /* ## ------------------- ## */
>      /* Flushes the connection tracking tables. If 'zone' is not NULL,
> -     * only deletes connections in '*zone'. */
> -    void (*ct_flush)(const struct ofproto *, const uint16_t *zone);
> +     * only deletes connections in '*zone'. If 'match' is not NULL,
> +     * deletes connections specified by the match. */
> +    void (*ct_flush)(const struct ofproto *, const uint16_t *zone,
> +                     const struct ofputil_ct_match *match);
>  
>      /* Sets conntrack timeout policy specified by 'timeout_policy' to 'zone'
>       * in datapath type 'dp_type'. */
> diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
> index 3a527683c..c9b222994 100644
> --- a/ofproto/ofproto.c
> +++ b/ofproto/ofproto.c
> @@ -34,6 +34,7 @@
>  #include "openvswitch/hmap.h"
>  #include "netdev.h"
>  #include "nx-match.h"
> +#include "ofp-ct-util.h"
>  #include "ofproto.h"
>  #include "ofproto-provider.h"
>  #include "openflow/nicira-ext.h"
> @@ -934,7 +935,31 @@ handle_nxt_ct_flush_zone(struct ofconn *ofconn, const struct ofp_header *oh)
>  
>      uint16_t zone = ntohs(nzi->zone_id);
>      if (ofproto->ofproto_class->ct_flush) {
> -        ofproto->ofproto_class->ct_flush(ofproto, &zone);
> +        ofproto->ofproto_class->ct_flush(ofproto, &zone, NULL);
> +    } else {
> +        return EOPNOTSUPP;
> +    }
> +
> +    return 0;
> +}
> +
> +static enum ofperr
> +handle_nxt_ct_flush(struct ofconn *ofconn, const struct ofp_header *oh)
> +{
> +    struct ofproto *ofproto = ofconn_get_ofproto(ofconn);
> +    struct ofputil_ct_match match = {0};
> +    bool with_zone = false;
> +    uint16_t zone_id = 0;
> +
> +    enum ofperr error =
> +        ofputil_ct_match_decode(&match, &with_zone, &zone_id, oh);
> +    if (error) {
> +        return error;
> +    }
> +
> +    if (ofproto->ofproto_class->ct_flush) {
> +        ofproto->ofproto_class->ct_flush(ofproto, with_zone ? &zone_id : NULL,
> +                                         &match);
>      } else {
>          return EOPNOTSUPP;
>      }
> @@ -8787,6 +8812,9 @@ handle_single_part_openflow(struct ofconn *ofconn, const struct ofp_header *oh,
>      case OFPTYPE_CT_FLUSH_ZONE:
>          return handle_nxt_ct_flush_zone(ofconn, oh);
>  
> +    case OFPTYPE_CT_FLUSH:
> +        return handle_nxt_ct_flush(ofconn, oh);
> +
>      case OFPTYPE_HELLO:
>      case OFPTYPE_ERROR:
>      case OFPTYPE_FEATURES_REPLY:
> diff --git a/tests/ofp-print.at b/tests/ofp-print.at
> index fe41cc42c..7c6a86133 100644
> --- a/tests/ofp-print.at
> +++ b/tests/ofp-print.at
> @@ -4073,3 +4073,96 @@ AT_CHECK([ovs-ofctl ofp-print "\
>  NXT_CT_FLUSH_ZONE (xid=0x3): zone_id=13
>  ])
>  AT_CLEANUP
> +
> +AT_SETUP([NXT_CT_FLUSH])
> +AT_KEYWORDS([ofp-print])
> +AT_CHECK([ovs-ofctl ofp-print "\
> +01 04 00 18 00 00 00 03 00 00 23 20 00 00 00 20 \
> +06 \
> +02 \
> +00 00 00 00 00 00 \
> +"], [0], [dnl
> +NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=::,dst=::,src_port=0,dst_port=0),reply=(src=::,dst=::,src_port=0,dst_port=0),zone_id=0
> +])
> +
> +AT_CHECK([ovs-ofctl ofp-print "\
> +01 04 00 20 00 00 00 03 00 00 23 20 00 00 00 20 \
> +06 \
> +02 \
> +00 00 00 00 00 00 \
> +00 09 00 08 00 0d 00 00 \
> +"], [0], [dnl
> +NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=::,dst=::,src_port=0,dst_port=0),reply=(src=::,dst=::,src_port=0,dst_port=0),zone_id=13
> +])
> +
> +AT_CHECK([ovs-ofctl ofp-print "\
> +01 04 00 68 00 00 00 03 00 00 23 20 00 00 00 20 \
> +06 \
> +02 \
> +00 00 00 00 00 00 \
> +00 09 00 08 00 0d 00 00 \
> +00 00 00 48 00 00 00 00 \
> +00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
> +00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
> +00 04 00 08 00 50 00 00 \
> +00 05 00 08 1f 90 00 00 \
> +"], [0], [dnl
> +NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=10.10.0.1,dst=10.10.0.2,src_port=80,dst_port=8080),reply=(src=::,dst=::,src_port=0,dst_port=0),zone_id=13
> +])
> +
> +AT_CHECK([ovs-ofctl ofp-print "\
> +01 04 00 68 00 00 00 03 00 00 23 20 00 00 00 20 \
> +06 \
> +02 \
> +00 00 00 00 00 00 \
> +00 09 00 08 00 0d 00 00 \
> +00 01 00 48 00 00 00 00 \
> +00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
> +00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
> +00 05 00 08 00 50 00 00 \
> +00 04 00 08 1f 90 00 00 \
> +"], [0], [dnl
> +NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=::,dst=::,src_port=0,dst_port=0),reply=(src=10.10.0.2,dst=10.10.0.1,src_port=8080,dst_port=80),zone_id=13
> +])
> +
> +AT_CHECK([ovs-ofctl ofp-print "\
> +01 04 00 b0 00 00 00 03 00 00 23 20 00 00 00 20 \
> +06 \
> +02 \
> +00 00 00 00 00 00 \
> +00 09 00 08 00 0d 00 00 \
> +00 00 00 48 00 00 00 00 \
> +00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
> +00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
> +00 04 00 08 00 50 00 00 \
> +00 05 00 08 1f 90 00 00 \
> +00 01 00 48 00 00 00 00 \
> +00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
> +00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
> +00 05 00 08 00 50 00 00 \
> +00 04 00 08 1f 90 00 00 \
> +"], [0], [dnl
> +NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=10.10.0.1,dst=10.10.0.2,src_port=80,dst_port=8080),reply=(src=10.10.0.2,dst=10.10.0.1,src_port=8080,dst_port=80),zone_id=13
> +])
> +
> +AT_CHECK([ovs-ofctl ofp-print "\
> +01 04 00 b8 00 00 00 03 00 00 23 20 00 00 00 20 \
> +01 \
> +0a \
> +00 00 00 00 00 00 \
> +00 00 00 50 00 00 00 00 \
> +00 02 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 01 00 00 00 00 \
> +00 03 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 02 00 00 00 00 \
> +00 06 00 08 00 0a 00 00 \
> +00 07 00 05 01 00 00 00 \
> +00 08 00 05 02 00 00 00 \
> +00 01 00 50 00 00 00 00 \
> +00 02 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 02 00 00 00 00 \
> +00 02 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 01 00 00 00 00 \
> +00 06 00 08 00 0a 00 00 \
> +00 07 00 05 03 00 00 00 \
> +00 08 00 05 04 00 00 00 \
> +"], [0], [dnl
> +NXT_CT_FLUSH (xid=0x3): l3_type=10,ip_proto=1,orig=(src=fd18::ffff:abcd:1,dst=fd18::ffff:abcd:2,icmp_id=10,icmp_type=1,icmp_code=2),reply=(src=fd18::ffff:abcd:1,dst=::,icmp_id=10,icmp_type=3,icmp_code=4),zone_id=0
> +])
> +AT_CLEANUP
> diff --git a/tests/ovs-ofctl.at b/tests/ovs-ofctl.at
> index a8934051e..ad94e75be 100644
> --- a/tests/ovs-ofctl.at
> +++ b/tests/ovs-ofctl.at
> @@ -3271,3 +3271,15 @@ AT_CHECK([ovs-ofctl -O OpenFlow15 dump-flows br0 | ofctl_strip | sed '/OFPST_FLO
>  
>  OVS_VSWITCHD_STOP(["/Flow exceeded the maximum flow statistics reply size and was excluded from the response set/d"])
>  AT_CLEANUP
> +
> +AT_SETUP([ovs-ofctl ct - flush-conntrack])
> +OVS_VSWITCHD_START
> +
> +AT_CHECK([ovs-appctl vlog/set ct_dpif:dbg])
> +AT_CHECK([ovs-ofctl flush-conntrack br0 zone=5 'ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1'])
> +
> +OVS_WAIT_UNTIL([grep -q "|ct_dpif|DBG|.*ct_flush" ovs-vswitchd.log])
> +AT_CHECK([grep -q "ct_dpif|DBG|.*ct_flush: l3_type=2,ip_proto=17,orig=(src=10.1.1.1,dst=10.1.1.2,src_port=1,dst_port=0),reply=(src=::,dst=::,src_port=0,dst_port=0) in zone 5" ovs-vswitchd.log])
> +
> +OVS_VSWITCHD_STOP
> +AT_CLEANUP
> diff --git a/tests/system-traffic.at b/tests/system-traffic.at
> index 51903a658..396d81ad9 100644
> --- a/tests/system-traffic.at
> +++ b/tests/system-traffic.at
> @@ -2250,126 +2250,136 @@ priority=100,in_port=2,icmp,action=ct(zone=5,commit),1
>  
>  AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
>  
> -dnl Test UDP from port 1
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> +flush_conntrack() {
> +    if [[ "$1" == "dpctl" ]]; then
> +        AT_CHECK([ovs-appctl dpctl/flush-conntrack ${@:2}])
> +    else
> +        AT_CHECK([ovs-ofctl flush-conntrack br0 ${@:2}])
> +    fi
> +}
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [], [dnl
> +for type in dpctl ofctl; do
> +    AS_BOX([Testing with $type command])
> +
> +    dnl Test UDP from port 1
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> +
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [], [dnl
>  udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=17,ct_tp_src=2,ct_tp_dst=1'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=17,ct_tp_src=2,ct_tp_dst=1'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [1], [dnl
> -])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [1])
>  
> -dnl Test UDP from port 2
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [dnl
> +    dnl Test UDP from port 2
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
> +
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [dnl
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack zone=5 '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'])
> +    AT_CHECK([flush_conntrack $type zone=5 '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'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.1.1.2)], [0], [dnl
> -])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.1.1.2)], [0])
>  
> -dnl Test ICMP traffic
> -NS_CHECK_EXEC([at_ns1], [ping -q -c 3 -i 0.3 -w 2 10.1.1.1 | FORMAT_PING], [0], [dnl
> +    dnl Test ICMP traffic
> +    NS_CHECK_EXEC([at_ns1], [ping -q -c 3 -i 0.3 -w 2 10.1.1.1 | FORMAT_PING], [0], [dnl
>  3 packets transmitted, 3 received, 0% packet loss, time 0ms
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [stdout])
> -AT_CHECK([cat stdout | FORMAT_CT(10.1.1.1)], [0],[dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [stdout])
> +    AT_CHECK([cat stdout | FORMAT_CT(10.1.1.1)], [0],[dnl
>  icmp,orig=(src=10.1.1.2,dst=10.1.1.1,id=<cleared>,type=8,code=0),reply=(src=10.1.1.1,dst=10.1.1.2,id=<cleared>,type=0,code=0),zone=5
>  ])
>  
> -ICMP_ID=`cat stdout | cut -d ',' -f4 | cut -d '=' -f2`
> -ICMP_TUPLE=ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=1,icmp_id=$ICMP_ID,icmp_type=8,icmp_code=0
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack zone=5 $ICMP_TUPLE])
> +    ICMP_ID=`cat stdout | cut -d ',' -f4 | cut -d '=' -f2`
> +    ICMP_TUPLE=ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=1,icmp_id=$ICMP_ID,icmp_type=8,icmp_code=0
> +    AT_CHECK([flush_conntrack $type zone=5 $ICMP_TUPLE])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [1], [dnl
> -])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [1])
>  
> -dnl Test UDP from port 1 and 2, partial flush by src port
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
> +    dnl Test UDP from port 1 and 2, partial flush by src port
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
>  
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
>  udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_src=1'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_src=1'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_src=2'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_src=2'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
>  
> -dnl Test UDP from port 1 and 2, partial flush by dst port
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
> +    dnl Test UDP from port 1 and 2, partial flush by dst port
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
>  
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
>  udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_dst=2'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_dst=2'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_dst=1'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_dst=1'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
>  
> -dnl Test UDP from port 1 and 2, partial flush by src address
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
> +    dnl Test UDP from port 1 and 2, partial flush by src address
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
>  
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
>  udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_src=10.1.1.1'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_src=10.1.1.1'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_src=10.1.1.2'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_src=10.1.1.2'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
>  
> -dnl Test UDP from port 1 and 2, partial flush by dst address
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> -AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
> +    dnl Test UDP from port 1 and 2, partial flush by dst address
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
> +    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
>  
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
>  udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_dst=10.1.1.2'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_dst=10.1.1.2'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
>  udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
>  ])
>  
> -AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_dst=10.1.1.1'])
> +    AT_CHECK([flush_conntrack $type 'ct_nw_dst=10.1.1.1'])
>  
> -AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
> +    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
> +done
>  
>  OVS_TRAFFIC_VSWITCHD_STOP
>  AT_CLEANUP
> diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
> index fe9114580..26fc28746 100644
> --- a/utilities/ovs-ofctl.c
> +++ b/utilities/ovs-ofctl.c
> @@ -40,6 +40,7 @@
>  #include "fatal-signal.h"
>  #include "nx-match.h"
>  #include "odp-util.h"
> +#include "ofp-ct-util.h"
>  #include "ofp-version-opt.h"
>  #include "ofproto/ofproto.h"
>  #include "openflow/nicira-ext.h"
> @@ -3050,6 +3051,40 @@ ofctl_ct_flush_zone(struct ovs_cmdl_context *ctx)
>      vconn_close(vconn);
>  }
>  
> +static void
> +ofctl_ct_flush_conntrack(struct ovs_cmdl_context *ctx)
> +{
> +    struct vconn *vconn;
> +    struct ofputil_ct_match match = {0};
> +    struct ds ds = DS_EMPTY_INITIALIZER;
> +    uint16_t zone;
> +    bool with_zone = false;
> +    int args = ctx->argc - 1;
> +
> +    /* Parse ct tuple */
> +    if (args) {
> +        if (!ofputil_ct_match_parse(&match, ctx->argv[args], &ds)) {
> +            ovs_fatal(0, "Failed to parse ct-tuple: %s", ds_cstr(&ds));
> +        }
> +        args--;
> +    }
> +
> +    /* Parse zone */
> +    if (args && ovs_scan(ctx->argv[args], "zone=%"SCNu16, &zone)) {
> +        with_zone = true;
> +    }
> +
> +    open_vconn(ctx->argv[1], &vconn);
> +    enum ofp_version version = vconn_get_version(vconn);
> +
> +    struct ofpbuf *msg =
> +        ofputil_ct_match_encode(&match, with_zone ? &zone : NULL, version);
> +
> +    ds_destroy(&ds);
> +    transact_noreply(vconn, msg);
> +    vconn_close(vconn);
> +}
> +
>  static void
>  ofctl_dump_ipfix_flow(struct ovs_cmdl_context *ctx)
>  {
> @@ -5063,6 +5098,9 @@ static const struct ovs_cmdl_command all_commands[] = {
>      { "ct-flush-zone", "switch zone",
>        2, 2, ofctl_ct_flush_zone, OVS_RO },
>  
> +    { "flush-conntrack", "switch [zone=N] [ct-tuple]",
> +      1, 3, ofctl_ct_flush_conntrack, OVS_RO },
> +
>      { "ofp-parse", "file",
>        1, 1, ofctl_ofp_parse, OVS_RW },
>      { "ofp-parse-pcap", "pcap",
> -- 
> 2.38.1
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index ff8904b02..46b8faa41 100644
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,9 @@  Post-v3.0.0
      by specifying 'max-rate' or '[r]stp-path-cost' accordingly.
    - ovs-dpctl and related ovs-appctl commands:
      * "flush-conntrack" is capable of handling partial 5-tuple.
+   - OpenFlow:
+      * New OpenFlow extension NXT_CT_FLUSH to flush connections matching
+        the specified fields.
 
 
 v3.0.0 - 15 Aug 2022
diff --git a/include/openflow/nicira-ext.h b/include/openflow/nicira-ext.h
index b68804991..32ce56d31 100644
--- a/include/openflow/nicira-ext.h
+++ b/include/openflow/nicira-ext.h
@@ -1064,4 +1064,34 @@  struct nx_zone_id {
 };
 OFP_ASSERT(sizeof(struct nx_zone_id) == 8);
 
+/* CT flush available TLVs. */
+enum nx_ct_flush_tlv_type {
+    /* Outer types. */
+    NXT_CT_ORIG_DIRECTION,    /* CT orig direction outer type. */
+    NXT_CT_REPLY_DIRECTION,   /* CT reply direction outer type. */
+
+    /* Nested types. */
+    NXT_CT_SRC,               /* CT source IPv6 or mapped IPv4 address. */
+    NXT_CT_DST,               /* CT destination IPv6 or mapped IPv4 address. */
+    NXT_CT_SRC_PORT,          /* CT source port. */
+    NXT_CT_DST_PORT,          /* CT destination port. */
+    NXT_CT_ICMP_ID,           /* CT ICMP id. */
+    NXT_CT_ICMP_TYPE,         /* CT ICMP type. */
+    NXT_CT_ICMP_CODE,         /* CT ICMP code. */
+
+    /* Primitive types. */
+    NXT_CT_ZONE_ID,           /* CT zone id. */
+};
+
+/* NXT_CT_FLUSH.
+ *
+ * Flushes the connection tracking specified by 5-tuple.
+ * The struct should be followed by TLVs specifying the matching parameters. */
+struct nx_ct_flush {
+    uint8_t ip_proto;          /* IP protocol. */
+    uint8_t family;            /* L3 address family. */
+    uint8_t zero[6];           /* Must be zero. */
+};
+OFP_ASSERT(sizeof(struct nx_ct_flush) == 8);
+
 #endif /* openflow/nicira-ext.h */
diff --git a/include/openvswitch/ofp-msgs.h b/include/openvswitch/ofp-msgs.h
index 921a937e5..659b0a3e7 100644
--- a/include/openvswitch/ofp-msgs.h
+++ b/include/openvswitch/ofp-msgs.h
@@ -526,6 +526,9 @@  enum ofpraw {
 
     /* NXST 1.0+ (4): struct nx_ipfix_stats_reply[]. */
     OFPRAW_NXST_IPFIX_FLOW_REPLY,
+
+    /* NXT 1.0+ (32): struct nx_ct_flush, uint8_t[8][]. */
+    OFPRAW_NXT_CT_FLUSH,
 };
 
 /* Decoding messages into OFPRAW_* values. */
@@ -772,6 +775,7 @@  enum ofptype {
     OFPTYPE_IPFIX_FLOW_STATS_REQUEST, /* OFPRAW_NXST_IPFIX_FLOW_REQUEST */
     OFPTYPE_IPFIX_FLOW_STATS_REPLY,   /* OFPRAW_NXST_IPFIX_FLOW_REPLY */
     OFPTYPE_CT_FLUSH_ZONE,            /* OFPRAW_NXT_CT_FLUSH_ZONE. */
+    OFPTYPE_CT_FLUSH,           /* OFPRAW_NXT_CT_FLUSH. */
 
     /* Flow monitor extension. */
     OFPTYPE_FLOW_MONITOR_CANCEL,  /* OFPRAW_NXT_FLOW_MONITOR_CANCEL.
diff --git a/include/openvswitch/ofp-util.h b/include/openvswitch/ofp-util.h
index 84937ae26..e10d90b9f 100644
--- a/include/openvswitch/ofp-util.h
+++ b/include/openvswitch/ofp-util.h
@@ -65,6 +65,10 @@  struct ofpbuf *ofputil_encode_echo_reply(const struct ofp_header *);
 
 struct ofpbuf *ofputil_encode_barrier_request(enum ofp_version);
 
+struct ofpbuf *ofputil_ct_match_encode(const struct ofputil_ct_match *match,
+                                       uint16_t *zone_id,
+                                       enum ofp_version version);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/ofp-bundle.c b/lib/ofp-bundle.c
index 0161c2bc6..941a8370e 100644
--- a/lib/ofp-bundle.c
+++ b/lib/ofp-bundle.c
@@ -292,6 +292,7 @@  ofputil_is_bundlable(enum ofptype type)
     case OFPTYPE_IPFIX_FLOW_STATS_REQUEST:
     case OFPTYPE_IPFIX_FLOW_STATS_REPLY:
     case OFPTYPE_CT_FLUSH_ZONE:
+    case OFPTYPE_CT_FLUSH:
         break;
     }
 
diff --git a/lib/ofp-ct-util.c b/lib/ofp-ct-util.c
index 9112305cc..276932ea6 100644
--- a/lib/ofp-ct-util.c
+++ b/lib/ofp-ct-util.c
@@ -23,8 +23,12 @@ 
 
 #include "ct-dpif.h"
 #include "ofp-ct-util.h"
+#include "openflow/nicira-ext.h"
 #include "openvswitch/dynamic-string.h"
+#include "openvswitch/ofp-msgs.h"
 #include "openvswitch/ofp-parse.h"
+#include "openvswitch/ofp-errors.h"
+#include "openvswitch/ofp-prop.h"
 #include "openvswitch/ofp-util.h"
 #include "openvswitch/packets.h"
 
@@ -309,3 +313,145 @@  error:
     free(copy);
     return false;
 }
+
+static enum ofperr
+ofpprop_pull_value(struct ofpbuf *property, void *value, size_t len)
+{
+    if (ofpbuf_msgsize(property) < len) {
+        return OFPERR_OFPBPC_BAD_LEN;
+    }
+
+    memcpy(value, property->msg, len);
+
+    return 0;
+}
+
+static enum ofperr
+ofputil_ct_tuple_decode_nested(struct ofpbuf *property,
+                               struct ofputil_ct_tuple *tuple)
+{
+    struct ofpbuf nested;
+    enum ofperr error = ofpprop_parse_nested(property, &nested);
+    if (error) {
+        return error;
+    }
+
+    while (nested.size) {
+        struct ofpbuf inner;
+        uint64_t type;
+
+        error = ofpprop_pull(&nested, &inner, &type);
+        if (error) {
+            return error;
+        }
+        switch (type) {
+            case NXT_CT_SRC:
+                ofpprop_pull_value(&inner, &tuple->src, sizeof tuple->src);
+                break;
+            case NXT_CT_DST:
+                ofpprop_pull_value(&inner, &tuple->dst, sizeof tuple->dst);
+                break;
+            case NXT_CT_SRC_PORT:
+                ofpprop_parse_be16(&inner, &tuple->src_port);
+                break;
+            case NXT_CT_DST_PORT:
+                ofpprop_parse_be16(&inner, &tuple->dst_port);
+                break;
+            case NXT_CT_ICMP_ID:
+                ofpprop_parse_be16(&inner, &tuple->icmp_id);
+                break;
+            case NXT_CT_ICMP_TYPE:
+                ofpprop_parse_u8(&inner, &tuple->icmp_type);
+                break;
+            case NXT_CT_ICMP_CODE:
+                ofpprop_parse_u8(&inner, &tuple->icmp_code);
+        }
+    }
+
+    return 0;
+}
+
+enum ofperr
+ofputil_ct_match_decode(struct ofputil_ct_match *match, bool *with_zone,
+                        uint16_t *zone_id, const struct ofp_header *oh)
+{
+    struct ofpbuf msg = ofpbuf_const_initializer(oh, ntohs(oh->length));
+    ofpraw_pull_assert(&msg);
+
+    const struct nx_ct_flush *nx_flush = ofpbuf_pull(&msg, sizeof *nx_flush);
+
+    if (!is_all_zeros(nx_flush->zero, sizeof nx_flush->zero)) {
+        return OFPERR_NXBRC_MUST_BE_ZERO;
+    }
+
+    match->l3_type = nx_flush->family;
+    match->ip_proto = nx_flush->ip_proto;
+
+    struct ofputil_ct_tuple *orig = &match->tuple_orig;
+    struct ofputil_ct_tuple *reply = &match->tuple_reply;
+
+    while (msg.size) {
+        struct ofpbuf property;
+        uint64_t type;
+
+        enum ofperr error = ofpprop_pull(&msg, &property, &type);
+        if (error) {
+            return error;
+        }
+
+        switch (type) {
+            case NXT_CT_ORIG_DIRECTION:
+                ofputil_ct_tuple_decode_nested(&property, orig);
+                break;
+            case NXT_CT_REPLY_DIRECTION:
+                ofputil_ct_tuple_decode_nested(&property, reply);
+                break;
+            case NXT_CT_ZONE_ID:
+                if (with_zone) {
+                    *with_zone = true;
+                }
+                ofpprop_parse_u16(&property, zone_id);
+                break;
+        }
+    }
+
+    return 0;
+}
+
+void
+ofputil_ct_tuple_encode(const struct ofputil_ct_tuple *tuple,
+                        struct ofpbuf *buf, enum nx_ct_flush_tlv_type type,
+                        uint8_t ip_proto)
+{
+    /* 128 B is enough to hold the whole tuple. */
+    uint8_t stub[128];
+    struct ofpbuf nested = OFPBUF_STUB_INITIALIZER(stub);
+
+    if (!ipv6_is_zero(&tuple->src)) {
+        ofpprop_put(&nested, NXT_CT_SRC, &tuple->src, sizeof tuple->src);
+    }
+
+    if (!ipv6_is_zero(&tuple->dst)) {
+        ofpprop_put(&nested, NXT_CT_DST, &tuple->dst, sizeof tuple->dst);
+    }
+
+    if (ip_proto == IPPROTO_ICMP || ip_proto == IPPROTO_ICMPV6) {
+        ofpprop_put_be16(&nested, NXT_CT_ICMP_ID, tuple->icmp_id);
+        ofpprop_put_u8(&nested, NXT_CT_ICMP_TYPE, tuple->icmp_type);
+        ofpprop_put_u8(&nested, NXT_CT_ICMP_CODE, tuple->icmp_code);
+    } else {
+        if (tuple->src_port) {
+            ofpprop_put_be16(&nested, NXT_CT_SRC_PORT, tuple->src_port);
+        }
+
+        if (tuple->dst_port) {
+            ofpprop_put_be16(&nested, NXT_CT_DST_PORT, tuple->dst_port);
+        }
+    }
+
+    if (nested.size) {
+        ofpprop_put_nested(buf, type, &nested);
+    }
+
+    ofpbuf_uninit(&nested);
+}
diff --git a/lib/ofp-ct-util.h b/lib/ofp-ct-util.h
index a53d73cb0..5d862500f 100644
--- a/lib/ofp-ct-util.h
+++ b/lib/ofp-ct-util.h
@@ -17,6 +17,7 @@ 
 #define OVS_OFP_CT_UTIL_H
 
 #include "ct-dpif.h"
+#include "openflow/nicira-ext.h"
 #include "openvswitch/ofp-util.h"
 
 bool ofputil_ct_match_cmp(const struct ofputil_ct_match *match,
@@ -31,4 +32,12 @@  void ofputil_ct_match_format(struct ds *ds,
 bool ofputil_ct_match_parse(struct ofputil_ct_match *match, const char *s,
                             struct ds *ds);
 
+enum ofperr ofputil_ct_match_decode(struct ofputil_ct_match *match,
+                                    bool *with_zone, uint16_t *zone_id,
+                                    const struct ofp_header *oh);
+
+void ofputil_ct_tuple_encode(const struct ofputil_ct_tuple *tuple,
+                             struct ofpbuf *buf,
+                             enum nx_ct_flush_tlv_type type, uint8_t ip_proto);
+
 #endif /* lib/ofp-ct-util.h */
diff --git a/lib/ofp-print.c b/lib/ofp-print.c
index bd37fa17a..94bfa7070 100644
--- a/lib/ofp-print.c
+++ b/lib/ofp-print.c
@@ -36,6 +36,7 @@ 
 #include "learn.h"
 #include "multipath.h"
 #include "netdev.h"
+#include "ofp-ct-util.h"
 #include "nx-match.h"
 #include "odp-util.h"
 #include "openflow/nicira-ext.h"
@@ -949,6 +950,23 @@  ofp_print_nxt_ct_flush_zone(struct ds *string, const struct nx_zone_id *nzi)
     return 0;
 }
 
+static enum ofperr
+ofp_print_nxt_ct_flush(struct ds *string, const struct ofp_header *oh)
+{
+    uint16_t zone_id = 0;
+    struct ofputil_ct_match match = {0};
+
+    enum ofperr error = ofputil_ct_match_decode(&match, NULL, &zone_id, oh);
+    if (error) {
+        return error;
+    }
+
+    ofputil_ct_match_format(string, &match);
+    ds_put_format(string, ",zone_id=%"PRIu16, zone_id);
+
+    return 0;
+}
+
 static enum ofperr
 ofp_to_string__(const struct ofp_header *oh,
                 const struct ofputil_port_map *port_map,
@@ -1184,6 +1202,8 @@  ofp_to_string__(const struct ofp_header *oh,
 
     case OFPTYPE_CT_FLUSH_ZONE:
         return ofp_print_nxt_ct_flush_zone(string, ofpmsg_body(oh));
+    case OFPTYPE_CT_FLUSH:
+        return ofp_print_nxt_ct_flush(string, oh);
     }
 
     return 0;
diff --git a/lib/ofp-util.c b/lib/ofp-util.c
index a324ceeea..a14cb6860 100644
--- a/lib/ofp-util.c
+++ b/lib/ofp-util.c
@@ -31,6 +31,7 @@ 
 #include "multipath.h"
 #include "netdev.h"
 #include "nx-match.h"
+#include "ofp-ct-util.h"
 #include "id-pool.h"
 #include "openflow/netronome-ext.h"
 #include "openvswitch/dynamic-string.h"
@@ -237,3 +238,27 @@  ofputil_encode_barrier_request(enum ofp_version ofp_version)
 
     return ofpraw_alloc(type, ofp_version, 0);
 }
+
+struct ofpbuf *
+ofputil_ct_match_encode(const struct ofputil_ct_match *match,
+                        uint16_t *zone_id, enum ofp_version version)
+{
+    struct ofpbuf *msg = ofpraw_alloc(OFPRAW_NXT_CT_FLUSH, version, 0);
+    struct nx_ct_flush *nx_flush = ofpbuf_put_zeros(msg, sizeof *nx_flush);
+    const struct ofputil_ct_tuple *orig = &match->tuple_orig;
+    const struct ofputil_ct_tuple *reply = &match->tuple_reply;
+
+    nx_flush->ip_proto = match->ip_proto;
+    nx_flush->family = match->l3_type;
+
+    ofputil_ct_tuple_encode(orig, msg, NXT_CT_ORIG_DIRECTION,
+                            match->ip_proto);
+    ofputil_ct_tuple_encode(reply, msg, NXT_CT_REPLY_DIRECTION,
+                            match->ip_proto);
+
+    if (zone_id) {
+        ofpprop_put_u16(msg, NXT_CT_ZONE_ID, *zone_id);
+    }
+
+    return msg;
+}
diff --git a/lib/rconn.c b/lib/rconn.c
index a96b2eb8b..4afa21515 100644
--- a/lib/rconn.c
+++ b/lib/rconn.c
@@ -1426,6 +1426,7 @@  is_admitted_msg(const struct ofpbuf *b)
     case OFPTYPE_IPFIX_FLOW_STATS_REQUEST:
     case OFPTYPE_IPFIX_FLOW_STATS_REPLY:
     case OFPTYPE_CT_FLUSH_ZONE:
+    case OFPTYPE_CT_FLUSH:
     default:
         return true;
     }
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index f9562dee8..29174a585 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -5358,11 +5358,12 @@  type_set_config(const char *type, const struct smap *other_config)
 }
 
 static void
-ct_flush(const struct ofproto *ofproto_, const uint16_t *zone)
+ct_flush(const struct ofproto *ofproto_, const uint16_t *zone,
+         const struct ofputil_ct_match *match)
 {
     struct ofproto_dpif *ofproto = ofproto_dpif_cast(ofproto_);
 
-    ct_dpif_flush(ofproto->backer->dpif, zone, NULL);
+    ct_dpif_flush(ofproto->backer->dpif, zone, match);
 }
 
 static struct ct_timeout_policy *
@@ -5674,6 +5675,9 @@  get_datapath_cap(const char *datapath_type, struct smap *cap)
     smap_add(cap, "lb_output_action", s.lb_output_action ? "true" : "false");
     smap_add(cap, "ct_zero_snat", s.ct_zero_snat ? "true" : "false");
     smap_add(cap, "add_mpls", s.add_mpls ? "true" : "false");
+    /* The ct_tuple_flush is implemented on dpif level, so it is supported
+     * for all backers. */
+    smap_add(cap, "ct_flush", "true");
 }
 
 /* Gets timeout policy name in 'backer' based on 'zone', 'dl_type' and
diff --git a/ofproto/ofproto-provider.h b/ofproto/ofproto-provider.h
index 7e3fb6698..5e39234f9 100644
--- a/ofproto/ofproto-provider.h
+++ b/ofproto/ofproto-provider.h
@@ -49,6 +49,7 @@ 
 #include "openvswitch/ofp-port.h"
 #include "openvswitch/ofp-switch.h"
 #include "openvswitch/ofp-table.h"
+#include "openvswitch/ofp-util.h"
 #include "ovs-atomic.h"
 #include "ovs-rcu.h"
 #include "ovs-thread.h"
@@ -1902,8 +1903,10 @@  struct ofproto_class {
 /* ## Connection tracking ## */
 /* ## ------------------- ## */
     /* Flushes the connection tracking tables. If 'zone' is not NULL,
-     * only deletes connections in '*zone'. */
-    void (*ct_flush)(const struct ofproto *, const uint16_t *zone);
+     * only deletes connections in '*zone'. If 'match' is not NULL,
+     * deletes connections specified by the match. */
+    void (*ct_flush)(const struct ofproto *, const uint16_t *zone,
+                     const struct ofputil_ct_match *match);
 
     /* Sets conntrack timeout policy specified by 'timeout_policy' to 'zone'
      * in datapath type 'dp_type'. */
diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
index 3a527683c..c9b222994 100644
--- a/ofproto/ofproto.c
+++ b/ofproto/ofproto.c
@@ -34,6 +34,7 @@ 
 #include "openvswitch/hmap.h"
 #include "netdev.h"
 #include "nx-match.h"
+#include "ofp-ct-util.h"
 #include "ofproto.h"
 #include "ofproto-provider.h"
 #include "openflow/nicira-ext.h"
@@ -934,7 +935,31 @@  handle_nxt_ct_flush_zone(struct ofconn *ofconn, const struct ofp_header *oh)
 
     uint16_t zone = ntohs(nzi->zone_id);
     if (ofproto->ofproto_class->ct_flush) {
-        ofproto->ofproto_class->ct_flush(ofproto, &zone);
+        ofproto->ofproto_class->ct_flush(ofproto, &zone, NULL);
+    } else {
+        return EOPNOTSUPP;
+    }
+
+    return 0;
+}
+
+static enum ofperr
+handle_nxt_ct_flush(struct ofconn *ofconn, const struct ofp_header *oh)
+{
+    struct ofproto *ofproto = ofconn_get_ofproto(ofconn);
+    struct ofputil_ct_match match = {0};
+    bool with_zone = false;
+    uint16_t zone_id = 0;
+
+    enum ofperr error =
+        ofputil_ct_match_decode(&match, &with_zone, &zone_id, oh);
+    if (error) {
+        return error;
+    }
+
+    if (ofproto->ofproto_class->ct_flush) {
+        ofproto->ofproto_class->ct_flush(ofproto, with_zone ? &zone_id : NULL,
+                                         &match);
     } else {
         return EOPNOTSUPP;
     }
@@ -8787,6 +8812,9 @@  handle_single_part_openflow(struct ofconn *ofconn, const struct ofp_header *oh,
     case OFPTYPE_CT_FLUSH_ZONE:
         return handle_nxt_ct_flush_zone(ofconn, oh);
 
+    case OFPTYPE_CT_FLUSH:
+        return handle_nxt_ct_flush(ofconn, oh);
+
     case OFPTYPE_HELLO:
     case OFPTYPE_ERROR:
     case OFPTYPE_FEATURES_REPLY:
diff --git a/tests/ofp-print.at b/tests/ofp-print.at
index fe41cc42c..7c6a86133 100644
--- a/tests/ofp-print.at
+++ b/tests/ofp-print.at
@@ -4073,3 +4073,96 @@  AT_CHECK([ovs-ofctl ofp-print "\
 NXT_CT_FLUSH_ZONE (xid=0x3): zone_id=13
 ])
 AT_CLEANUP
+
+AT_SETUP([NXT_CT_FLUSH])
+AT_KEYWORDS([ofp-print])
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 18 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+02 \
+00 00 00 00 00 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=::,dst=::,src_port=0,dst_port=0),reply=(src=::,dst=::,src_port=0,dst_port=0),zone_id=0
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 20 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+02 \
+00 00 00 00 00 00 \
+00 09 00 08 00 0d 00 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=::,dst=::,src_port=0,dst_port=0),reply=(src=::,dst=::,src_port=0,dst_port=0),zone_id=13
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 68 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+02 \
+00 00 00 00 00 00 \
+00 09 00 08 00 0d 00 00 \
+00 00 00 48 00 00 00 00 \
+00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
+00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
+00 04 00 08 00 50 00 00 \
+00 05 00 08 1f 90 00 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=10.10.0.1,dst=10.10.0.2,src_port=80,dst_port=8080),reply=(src=::,dst=::,src_port=0,dst_port=0),zone_id=13
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 68 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+02 \
+00 00 00 00 00 00 \
+00 09 00 08 00 0d 00 00 \
+00 01 00 48 00 00 00 00 \
+00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
+00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
+00 05 00 08 00 50 00 00 \
+00 04 00 08 1f 90 00 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=::,dst=::,src_port=0,dst_port=0),reply=(src=10.10.0.2,dst=10.10.0.1,src_port=8080,dst_port=80),zone_id=13
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 b0 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+02 \
+00 00 00 00 00 00 \
+00 09 00 08 00 0d 00 00 \
+00 00 00 48 00 00 00 00 \
+00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
+00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
+00 04 00 08 00 50 00 00 \
+00 05 00 08 1f 90 00 00 \
+00 01 00 48 00 00 00 00 \
+00 03 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 00 00 00 00 \
+00 02 00 14 00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 00 00 00 00 \
+00 05 00 08 00 50 00 00 \
+00 04 00 08 1f 90 00 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): l3_type=2,ip_proto=6,orig=(src=10.10.0.1,dst=10.10.0.2,src_port=80,dst_port=8080),reply=(src=10.10.0.2,dst=10.10.0.1,src_port=8080,dst_port=80),zone_id=13
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 b8 00 00 00 03 00 00 23 20 00 00 00 20 \
+01 \
+0a \
+00 00 00 00 00 00 \
+00 00 00 50 00 00 00 00 \
+00 02 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 01 00 00 00 00 \
+00 03 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 02 00 00 00 00 \
+00 06 00 08 00 0a 00 00 \
+00 07 00 05 01 00 00 00 \
+00 08 00 05 02 00 00 00 \
+00 01 00 50 00 00 00 00 \
+00 02 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 02 00 00 00 00 \
+00 02 00 14 fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 01 00 00 00 00 \
+00 06 00 08 00 0a 00 00 \
+00 07 00 05 03 00 00 00 \
+00 08 00 05 04 00 00 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): l3_type=10,ip_proto=1,orig=(src=fd18::ffff:abcd:1,dst=fd18::ffff:abcd:2,icmp_id=10,icmp_type=1,icmp_code=2),reply=(src=fd18::ffff:abcd:1,dst=::,icmp_id=10,icmp_type=3,icmp_code=4),zone_id=0
+])
+AT_CLEANUP
diff --git a/tests/ovs-ofctl.at b/tests/ovs-ofctl.at
index a8934051e..ad94e75be 100644
--- a/tests/ovs-ofctl.at
+++ b/tests/ovs-ofctl.at
@@ -3271,3 +3271,15 @@  AT_CHECK([ovs-ofctl -O OpenFlow15 dump-flows br0 | ofctl_strip | sed '/OFPST_FLO
 
 OVS_VSWITCHD_STOP(["/Flow exceeded the maximum flow statistics reply size and was excluded from the response set/d"])
 AT_CLEANUP
+
+AT_SETUP([ovs-ofctl ct - flush-conntrack])
+OVS_VSWITCHD_START
+
+AT_CHECK([ovs-appctl vlog/set ct_dpif:dbg])
+AT_CHECK([ovs-ofctl flush-conntrack br0 zone=5 'ct_nw_src=10.1.1.1,ct_nw_dst=10.1.1.2,ct_nw_proto=17,ct_tp_src=1'])
+
+OVS_WAIT_UNTIL([grep -q "|ct_dpif|DBG|.*ct_flush" ovs-vswitchd.log])
+AT_CHECK([grep -q "ct_dpif|DBG|.*ct_flush: l3_type=2,ip_proto=17,orig=(src=10.1.1.1,dst=10.1.1.2,src_port=1,dst_port=0),reply=(src=::,dst=::,src_port=0,dst_port=0) in zone 5" ovs-vswitchd.log])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 51903a658..396d81ad9 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -2250,126 +2250,136 @@  priority=100,in_port=2,icmp,action=ct(zone=5,commit),1
 
 AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
 
-dnl Test UDP from port 1
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
+flush_conntrack() {
+    if [[ "$1" == "dpctl" ]]; then
+        AT_CHECK([ovs-appctl dpctl/flush-conntrack ${@:2}])
+    else
+        AT_CHECK([ovs-ofctl flush-conntrack br0 ${@:2}])
+    fi
+}
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [], [dnl
+for type in dpctl ofctl; do
+    AS_BOX([Testing with $type command])
+
+    dnl Test UDP from port 1
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
+
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [], [dnl
 udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=17,ct_tp_src=2,ct_tp_dst=1'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=17,ct_tp_src=2,ct_tp_dst=1'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [1], [dnl
-])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], [1])
 
-dnl Test UDP from port 2
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [dnl
+    dnl Test UDP from port 2
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
+
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [dnl
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack zone=5 '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'])
+    AT_CHECK([flush_conntrack $type zone=5 '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'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.1.1.2)], [0], [dnl
-])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.1.1.2)], [0])
 
-dnl Test ICMP traffic
-NS_CHECK_EXEC([at_ns1], [ping -q -c 3 -i 0.3 -w 2 10.1.1.1 | FORMAT_PING], [0], [dnl
+    dnl Test ICMP traffic
+    NS_CHECK_EXEC([at_ns1], [ping -q -c 3 -i 0.3 -w 2 10.1.1.1 | FORMAT_PING], [0], [dnl
 3 packets transmitted, 3 received, 0% packet loss, time 0ms
 ])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [stdout])
-AT_CHECK([cat stdout | FORMAT_CT(10.1.1.1)], [0],[dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [0], [stdout])
+    AT_CHECK([cat stdout | FORMAT_CT(10.1.1.1)], [0],[dnl
 icmp,orig=(src=10.1.1.2,dst=10.1.1.1,id=<cleared>,type=8,code=0),reply=(src=10.1.1.1,dst=10.1.1.2,id=<cleared>,type=0,code=0),zone=5
 ])
 
-ICMP_ID=`cat stdout | cut -d ',' -f4 | cut -d '=' -f2`
-ICMP_TUPLE=ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=1,icmp_id=$ICMP_ID,icmp_type=8,icmp_code=0
-AT_CHECK([ovs-appctl dpctl/flush-conntrack zone=5 $ICMP_TUPLE])
+    ICMP_ID=`cat stdout | cut -d ',' -f4 | cut -d '=' -f2`
+    ICMP_TUPLE=ct_nw_src=10.1.1.2,ct_nw_dst=10.1.1.1,ct_nw_proto=1,icmp_id=$ICMP_ID,icmp_type=8,icmp_code=0
+    AT_CHECK([flush_conntrack $type zone=5 $ICMP_TUPLE])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [1], [dnl
-])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.2,"], [1])
 
-dnl Test UDP from port 1 and 2, partial flush by src port
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
+    dnl Test UDP from port 1 and 2, partial flush by src port
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
 
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
 udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_src=1'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_src=1'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_src=2'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_src=2'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
 
-dnl Test UDP from port 1 and 2, partial flush by dst port
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
+    dnl Test UDP from port 1 and 2, partial flush by dst port
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
 
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
 udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_dst=2'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_dst=2'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_proto=17,ct_tp_dst=1'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_proto=17,ct_tp_dst=1'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
 
-dnl Test UDP from port 1 and 2, partial flush by src address
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
+    dnl Test UDP from port 1 and 2, partial flush by src address
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
 
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
 udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_src=10.1.1.1'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_src=10.1.1.1'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_src=10.1.1.2'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_src=10.1.1.2'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
 
-dnl Test UDP from port 1 and 2, partial flush by dst address
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
-AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
+    dnl Test UDP from port 1 and 2, partial flush by dst address
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=1 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101010a0101020001000200080000 actions=resubmit(,0)"])
+    AT_CHECK([ovs-ofctl -O OpenFlow13 packet-out br0 "in_port=2 packet=50540000000a50540000000908004500001c000000000011a4cd0a0101020a0101010002000100080000 actions=resubmit(,0)"])
 
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1" | sort], [0], [dnl
 udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),reply=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1)
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_dst=10.1.1.2'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_dst=10.1.1.2'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [0], [dnl
 udp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=2,dport=1),reply=(src=10.1.1.1,dst=10.1.1.2,sport=1,dport=2),zone=5
 ])
 
-AT_CHECK([ovs-appctl dpctl/flush-conntrack 'ct_nw_dst=10.1.1.1'])
+    AT_CHECK([flush_conntrack $type 'ct_nw_dst=10.1.1.1'])
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
+    AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "10\.1\.1\.1"], [1])
+done
 
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
index fe9114580..26fc28746 100644
--- a/utilities/ovs-ofctl.c
+++ b/utilities/ovs-ofctl.c
@@ -40,6 +40,7 @@ 
 #include "fatal-signal.h"
 #include "nx-match.h"
 #include "odp-util.h"
+#include "ofp-ct-util.h"
 #include "ofp-version-opt.h"
 #include "ofproto/ofproto.h"
 #include "openflow/nicira-ext.h"
@@ -3050,6 +3051,40 @@  ofctl_ct_flush_zone(struct ovs_cmdl_context *ctx)
     vconn_close(vconn);
 }
 
+static void
+ofctl_ct_flush_conntrack(struct ovs_cmdl_context *ctx)
+{
+    struct vconn *vconn;
+    struct ofputil_ct_match match = {0};
+    struct ds ds = DS_EMPTY_INITIALIZER;
+    uint16_t zone;
+    bool with_zone = false;
+    int args = ctx->argc - 1;
+
+    /* Parse ct tuple */
+    if (args) {
+        if (!ofputil_ct_match_parse(&match, ctx->argv[args], &ds)) {
+            ovs_fatal(0, "Failed to parse ct-tuple: %s", ds_cstr(&ds));
+        }
+        args--;
+    }
+
+    /* Parse zone */
+    if (args && ovs_scan(ctx->argv[args], "zone=%"SCNu16, &zone)) {
+        with_zone = true;
+    }
+
+    open_vconn(ctx->argv[1], &vconn);
+    enum ofp_version version = vconn_get_version(vconn);
+
+    struct ofpbuf *msg =
+        ofputil_ct_match_encode(&match, with_zone ? &zone : NULL, version);
+
+    ds_destroy(&ds);
+    transact_noreply(vconn, msg);
+    vconn_close(vconn);
+}
+
 static void
 ofctl_dump_ipfix_flow(struct ovs_cmdl_context *ctx)
 {
@@ -5063,6 +5098,9 @@  static const struct ovs_cmdl_command all_commands[] = {
     { "ct-flush-zone", "switch zone",
       2, 2, ofctl_ct_flush_zone, OVS_RO },
 
+    { "flush-conntrack", "switch [zone=N] [ct-tuple]",
+      1, 3, ofctl_ct_flush_conntrack, OVS_RO },
+
     { "ofp-parse", "file",
       1, 1, ofctl_ofp_parse, OVS_RW },
     { "ofp-parse-pcap", "pcap",