diff mbox series

[ovs-dev,RFC,2/2] openflow: Add extension to flush CT by 5-tuple

Message ID 20221006062559.7876-2-amusil@redhat.com
State RFC
Headers show
Series [ovs-dev,RFC,1/2] dpif: Add support for CT flush with partial tuple | 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 success test: success

Commit Message

Ales Musil Oct. 6, 2022, 6:25 a.m. UTC
Curently there was only extension to flush CT
by zone. Add another extension that will
allow CT flush by full or partial 5-tuple.
The extension can also specify the direction
orig or reply.

Reported-at: https://bugzilla.redhat.com/2120546
Signed-off-by: Ales Musil <amusil@redhat.com>
---
I'm mainly unsure about the overlap between 
struct ct_dpif_tuple and struct ofputil_ct_tuple,
however I was not able to find a good way how to
"merge" them because the usage is sligthly different.
That's the main reason for RFC, when we come to an 
agreement what is the best way I'll make it an official
series. 

 NEWS                           |  3 ++
 include/openflow/nicira-ext.h  | 17 ++++++++++
 include/openvswitch/ofp-msgs.h |  4 +++
 include/openvswitch/ofp-util.h | 36 +++++++++++++++++++++
 lib/ct-dpif.c                  | 26 ++++++++++++---
 lib/ct-dpif.h                  |  4 ++-
 lib/dpctl.c                    |  3 +-
 lib/ofp-bundle.c               |  1 +
 lib/ofp-print.c                | 41 +++++++++++++++++++++++
 lib/ofp-util.c                 | 59 ++++++++++++++++++++++++++++++++++
 lib/rconn.c                    |  1 +
 ofproto/ofproto-dpif.c         | 58 +++++++++++++++++++++++++++++++--
 ofproto/ofproto-provider.h     |  7 ++--
 ofproto/ofproto.c              | 26 ++++++++++++++-
 tests/ofp-print.at             | 55 +++++++++++++++++++++++++++++++
 15 files changed, 330 insertions(+), 11 deletions(-)
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 81909812e..b87e0ac62 100644
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,9 @@  Post-v3.0.0
        DPDK 21.11.2.
    - 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 conntrack by full
+       or partial 5-tuple.
 
 
 v3.0.0 - 15 Aug 2022
diff --git a/include/openflow/nicira-ext.h b/include/openflow/nicira-ext.h
index b68804991..fcfd0e8db 100644
--- a/include/openflow/nicira-ext.h
+++ b/include/openflow/nicira-ext.h
@@ -1064,4 +1064,21 @@  struct nx_zone_id {
 };
 OFP_ASSERT(sizeof(struct nx_zone_id) == 8);
 
+/* NXT_CT_FLUSH.
+ *
+ * Flushes the connection tracking specified by 5-tuple. */
+struct nx_ct_flush {
+    uint8_t ip_proto;          /* IP protocol. */
+    uint8_t direction;         /* The CT direction specified by enum
+                                * ofputil_ct_direction. */
+    ovs_be16 zone_id;          /* CT zone id. */
+    ovs_be32 src[4];           /* CT source IPv6 or mapped IPv4 address. */
+    ovs_be32 dst[4];           /* CT destination IPv6 or mapped IPv4
+                                * address. */
+    ovs_be16 src_port;         /* CT source port or ICMP id. */
+    ovs_be16 dst_port;         /* CT destination port or ICMP type and ICMP
+                                * code. */
+};
+OFP_ASSERT(sizeof(struct nx_ct_flush) == 40);
+
 #endif /* openflow/nicira-ext.h */
diff --git a/include/openvswitch/ofp-msgs.h b/include/openvswitch/ofp-msgs.h
index 921a937e5..80f12481c 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. */
+    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 091a09cad..3081c88d8 100644
--- a/include/openvswitch/ofp-util.h
+++ b/include/openvswitch/ofp-util.h
@@ -19,6 +19,9 @@ 
 
 #include <stdbool.h>
 #include <stdint.h>
+#include <sys/types.h>
+#include <netinet/in.h>
+
 #include "openvswitch/ofp-protocol.h"
 
 struct ofp_header;
@@ -27,6 +30,31 @@  struct ofp_header;
 extern "C" {
 #endif
 
+enum ofputil_ct_direction {
+    OFPUTIL_CT_DIRECTION_ORIG = 1,
+    OFPUTIL_CT_DIRECTION_REPLY,
+};
+
+struct ofputil_ct_tuple {
+    uint8_t ip_proto;
+    uint8_t direction;
+
+    struct in6_addr src;
+    struct in6_addr dst;
+
+    union {
+        ovs_be16 src_port;
+        ovs_be16 icmp_id;
+    };
+    union {
+        ovs_be16 dst_port;
+        struct {
+            uint8_t icmp_type;
+            uint8_t icmp_code;
+        };
+    };
+};
+
 bool ofputil_decode_hello(const struct ofp_header *,
                           uint32_t *allowed_versions);
 struct ofpbuf *ofputil_encode_hello(uint32_t version_bitmap);
@@ -37,6 +65,14 @@  struct ofpbuf *ofputil_encode_echo_reply(const struct ofp_header *);
 
 struct ofpbuf *ofputil_encode_barrier_request(enum ofp_version);
 
+struct ofpbuf *ofp_ct_tuple_encode(struct ofputil_ct_tuple *tuple,
+                                   uint16_t zone_id,
+                                   enum ofputil_ct_direction dir,
+                                   enum ofp_version version);
+enum ofperr ofp_ct_tuple_decode(struct ofputil_ct_tuple *tuple,
+                                uint16_t *zone_id,
+                                const struct ofp_header *oh);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/ct-dpif.c b/lib/ct-dpif.c
index 57995f5e5..14737bcf7 100644
--- a/lib/ct-dpif.c
+++ b/lib/ct-dpif.c
@@ -23,6 +23,7 @@ 
 
 #include "ct-dpif.h"
 #include "openvswitch/ofp-parse.h"
+#include "openvswitch/ofp-util.h"
 #include "openvswitch/vlog.h"
 
 VLOG_DEFINE_THIS_MODULE(ct_dpif);
@@ -176,7 +177,8 @@  ct_dpif_tuple_cmp_partial(const struct ct_dpif_tuple *partial,
 
 static int
 ct_dpif_flush_tuple(struct dpif *dpif, const uint16_t *zone,
-                    const struct ct_dpif_tuple *tuple) {
+                    const struct ct_dpif_tuple *tuple,
+                    enum ofputil_ct_direction direction) {
     struct ct_dpif_dump_state *dump;
     struct ct_dpif_entry cte;
     int error;
@@ -204,7 +206,22 @@  ct_dpif_flush_tuple(struct dpif *dpif, const uint16_t *zone,
             continue;
         }
 
-        if (ct_dpif_tuple_cmp_partial(tuple, &cte.tuple_orig)) {
+        struct ct_dpif_tuple *ct_tuple;
+        switch (direction) {
+            case OFPUTIL_CT_DIRECTION_ORIG:
+                ct_tuple = &cte.tuple_orig;
+                break;
+            case OFPUTIL_CT_DIRECTION_REPLY:
+                ct_tuple = &cte.tuple_reply;
+                break;
+            default:
+                error = EOPNOTSUPP;
+        }
+        if (error) {
+            break;
+        }
+
+        if (ct_dpif_tuple_cmp_partial(tuple, ct_tuple)) {
             error = dpif->dpif_class->ct_flush(dpif, &cte.zone,
                                                &cte.tuple_orig);
             if (error) {
@@ -228,10 +245,11 @@  ct_dpif_flush_tuple(struct dpif *dpif, const uint16_t *zone,
  *     in '*zone'. If 'zone' is NULL, use the default zone (zone 0). */
 int
 ct_dpif_flush(struct dpif *dpif, const uint16_t *zone,
-              const struct ct_dpif_tuple *tuple)
+              const struct ct_dpif_tuple *tuple,
+              enum ofputil_ct_direction direction)
 {
     if (tuple) {
-        return ct_dpif_flush_tuple(dpif, zone, tuple);
+        return ct_dpif_flush_tuple(dpif, zone, tuple, direction);
     } else if (zone) {
         VLOG_DBG("%s: ct_flush: zone %"PRIu16, dpif_name(dpif), *zone);
     } else {
diff --git a/lib/ct-dpif.h b/lib/ct-dpif.h
index b59cba962..2d32bdca4 100644
--- a/lib/ct-dpif.h
+++ b/lib/ct-dpif.h
@@ -17,6 +17,7 @@ 
 #ifndef CT_DPIF_H
 #define CT_DPIF_H
 
+#include "openvswitch/ofp-util.h"
 #include "openvswitch/types.h"
 #include "packets.h"
 
@@ -281,7 +282,8 @@  int ct_dpif_dump_start(struct dpif *, struct ct_dpif_dump_state **,
 int ct_dpif_dump_next(struct ct_dpif_dump_state *, struct ct_dpif_entry *);
 int ct_dpif_dump_done(struct ct_dpif_dump_state *);
 int ct_dpif_flush(struct dpif *, const uint16_t *zone,
-                  const struct ct_dpif_tuple *);
+                  const struct ct_dpif_tuple *,
+                  enum ofputil_ct_direction direction);
 int ct_dpif_set_maxconns(struct dpif *dpif, uint32_t maxconns);
 int ct_dpif_get_maxconns(struct dpif *dpif, uint32_t *maxconns);
 int ct_dpif_get_nconns(struct dpif *dpif, uint32_t *nconns);
diff --git a/lib/dpctl.c b/lib/dpctl.c
index 29041fa3e..89760576d 100644
--- a/lib/dpctl.c
+++ b/lib/dpctl.c
@@ -35,6 +35,7 @@ 
 #include "dpif.h"
 #include "dpif-provider.h"
 #include "openvswitch/dynamic-string.h"
+#include "openvswitch/ofp-util.h"
 #include "flow.h"
 #include "openvswitch/match.h"
 #include "netdev.h"
@@ -1737,7 +1738,7 @@  dpctl_flush_conntrack(int argc, const char *argv[],
         return error;
     }
 
-    error = ct_dpif_flush(dpif, pzone, ptuple);
+    error = ct_dpif_flush(dpif, pzone, ptuple, OFPUTIL_CT_DIRECTION_ORIG);
     if (!error) {
         dpif_close(dpif);
         return 0;
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-print.c b/lib/ofp-print.c
index bd37fa17a..904672f9e 100644
--- a/lib/ofp-print.c
+++ b/lib/ofp-print.c
@@ -949,6 +949,44 @@  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)
+{
+    struct ofputil_ct_tuple tuple;
+    uint16_t zone_id;
+    enum ofperr err = ofp_ct_tuple_decode(&tuple, &zone_id, oh);
+    if (err) {
+        return err;
+    }
+
+    switch (tuple.direction) {
+        case OFPUTIL_CT_DIRECTION_ORIG:
+            ds_put_cstr(string, " direction=orig,");
+            break;
+        case OFPUTIL_CT_DIRECTION_REPLY:
+            ds_put_cstr(string, " direction=reply,");
+            break;
+    }
+
+    ds_put_format(string, "proto=%"PRIu8",zone_id=%"PRIu16,
+                  tuple.ip_proto, zone_id);
+    ds_put_cstr(string, ",src=");
+    ipv6_format_mapped(&tuple.src, string);
+    ds_put_cstr(string, ",dst=");
+    ipv6_format_mapped(&tuple.dst, string);
+
+    if (tuple.ip_proto == IPPROTO_ICMP ||
+        tuple.ip_proto == IPPROTO_ICMPV6) {
+        ds_put_format(string, ",id=%"PRIu16",type=%"PRIu8",code=%"PRIu8,
+                      ntohs(tuple.icmp_id), tuple.icmp_type, tuple.icmp_code);
+    } else {
+        ds_put_format(string, ",src_port=%"PRIu16",dst_port=%"PRIu16,
+                      ntohs(tuple.src_port), ntohs(tuple.dst_port));
+    }
+
+    return 0;
+}
+
 static enum ofperr
 ofp_to_string__(const struct ofp_header *oh,
                 const struct ofputil_port_map *port_map,
@@ -1184,6 +1222,9 @@  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..e9d845098 100644
--- a/lib/ofp-util.c
+++ b/lib/ofp-util.c
@@ -237,3 +237,62 @@  ofputil_encode_barrier_request(enum ofp_version ofp_version)
 
     return ofpraw_alloc(type, ofp_version, 0);
 }
+
+struct ofpbuf *
+ofp_ct_tuple_encode(struct ofputil_ct_tuple *tuple, uint16_t zone_id,
+                    enum ofputil_ct_direction dir, 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);
+
+    memcpy(&nx_flush->src, &tuple->src, sizeof tuple->src);
+    memcpy(&nx_flush->dst, &tuple->dst, sizeof tuple->dst);
+    nx_flush->ip_proto = tuple->ip_proto;
+    nx_flush->direction = dir;
+    nx_flush->zone_id = htons(zone_id);
+    nx_flush->src_port = tuple->src_port;
+
+    if (tuple->ip_proto == IPPROTO_ICMP || tuple->ip_proto == IPPROTO_ICMPV6) {
+        nx_flush->dst_port = htons(tuple->icmp_type << 8 | tuple->icmp_code);
+    } else {
+        nx_flush->dst_port = tuple->dst_port;
+    }
+
+    return msg;
+}
+
+enum ofperr
+ofp_ct_tuple_decode(struct ofputil_ct_tuple *tuple, uint16_t *zone_id,
+                    const struct ofp_header *oh)
+{
+
+    const struct nx_ct_flush *nx_flush = ofpmsg_body(oh);
+
+    switch (nx_flush->direction) {
+        case OFPUTIL_CT_DIRECTION_ORIG:
+        case OFPUTIL_CT_DIRECTION_REPLY:
+            break;
+        default:
+            return EOPNOTSUPP;
+    }
+
+    *zone_id = ntohs(nx_flush->zone_id);
+
+    tuple->ip_proto = nx_flush->ip_proto;
+    tuple->direction = nx_flush->direction;
+
+    memcpy(&tuple->src, &nx_flush->src, sizeof tuple->src);
+    memcpy(&tuple->dst, &nx_flush->dst, sizeof tuple->dst);
+
+    tuple->src_port = nx_flush->src_port;
+
+    if (tuple->ip_proto == IPPROTO_ICMP || tuple->ip_proto == IPPROTO_ICMPV6) {
+        uint16_t icmp = ntohs(nx_flush->dst_port);
+        tuple->icmp_type = icmp >> 8 & 0xff;
+        tuple->icmp_code = icmp & 0xff;
+    } else {
+        tuple->dst_port = nx_flush->dst_port;
+    }
+
+    return 0;
+}
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..b493a4104 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -5358,11 +5358,65 @@  type_set_config(const char *type, const struct smap *other_config)
 }
 
 static void
-ct_flush(const struct ofproto *ofproto_, const uint16_t *zone)
+ofp_ct_mapped_ipv6_to_ct_dpif_inet_addr(const struct in6_addr *ipv6,
+                                        union ct_dpif_inet_addr *addr)
 {
+    memset(addr, 0, sizeof *addr);
+
+    if (ipv6_is_zero(ipv6)) {
+        return;
+    }
+
+    if (IN6_IS_ADDR_V4MAPPED(ipv6)) {
+        addr->ip = in6_addr_get_mapped_ipv4(ipv6);
+    } else {
+        addr->in6 = *ipv6;
+    }
+}
+
+static int
+ofputil_ct_tuple_to_ct_dpif_tuple(const struct ofputil_ct_tuple *ofp_tuple,
+                                  struct ct_dpif_tuple *tuple)
+{
+    tuple->l3_type = AF_INET;
+    if (!ipv6_is_zero(&ofp_tuple->src) &&
+        !IN6_IS_ADDR_V4MAPPED(&ofp_tuple->src)) {
+        tuple->l3_type = AF_INET6;
+    }
+
+
+    ofp_ct_mapped_ipv6_to_ct_dpif_inet_addr(&ofp_tuple->src, &tuple->src);
+    ofp_ct_mapped_ipv6_to_ct_dpif_inet_addr(&ofp_tuple->dst, &tuple->dst);
+
+    tuple->ip_proto = ofp_tuple->ip_proto;
+    tuple->src_port = ofp_tuple->src_port;
+
+    if (tuple->ip_proto == IPPROTO_ICMP ||
+        tuple->ip_proto == IPPROTO_ICMPV6) {
+        tuple->icmp_code = ofp_tuple->icmp_code;
+        tuple->icmp_type = ofp_tuple->icmp_type;
+    } else {
+        tuple->dst_port = ofp_tuple->dst_port;
+    }
+
+    return 0;
+}
+
+static void
+ct_flush(const struct ofproto *ofproto_, const uint16_t *zone,
+         const struct ofputil_ct_tuple *ofp_tuple)
+{
+    struct ct_dpif_tuple tuple;
+    enum ofputil_ct_direction direction =
+        ofp_tuple ? ofp_tuple->direction : OFPUTIL_CT_DIRECTION_ORIG;
     struct ofproto_dpif *ofproto = ofproto_dpif_cast(ofproto_);
 
-    ct_dpif_flush(ofproto->backer->dpif, zone, NULL);
+    if (ofp_tuple) {
+        ofputil_ct_tuple_to_ct_dpif_tuple(ofp_tuple, &tuple);
+    }
+
+    ct_dpif_flush(ofproto->backer->dpif, zone, ofp_tuple ? &tuple : NULL,
+                  direction);
 }
 
 static struct ct_timeout_policy *
diff --git a/ofproto/ofproto-provider.h b/ofproto/ofproto-provider.h
index 7e3fb6698..c79478395 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 'tuple' is not NULL,
+     * deletes connections specified by the tuple. */
+    void (*ct_flush)(const struct ofproto *, const uint16_t *zone,
+                     const struct ofputil_ct_tuple *tuple);
 
     /* 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..cd6598df8 100644
--- a/ofproto/ofproto.c
+++ b/ofproto/ofproto.c
@@ -934,7 +934,28 @@  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_tuple tuple;
+    uint16_t zone_id;
+
+    int err = ofp_ct_tuple_decode(&tuple, &zone_id, oh);
+    if (err) {
+        return err;
+    }
+
+    if (ofproto->ofproto_class->ct_flush) {
+        ofproto->ofproto_class->ct_flush(ofproto, &zone_id, &tuple);
     } else {
         return EOPNOTSUPP;
     }
@@ -8787,6 +8808,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..a30e04ad1 100644
--- a/tests/ofp-print.at
+++ b/tests/ofp-print.at
@@ -4073,3 +4073,58 @@  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 38 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+01 \
+00 0d \
+00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 \
+00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 \
+00 50 \
+1f 90 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): direction=orig,proto=6,zone_id=13,src=10.10.0.1,dst=10.10.0.2,src_port=80,dst_port=8080
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 38 00 00 00 03 00 00 23 20 00 00 00 20 \
+06 \
+01 \
+00 0d \
+fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 01 \
+fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 02 \
+00 50 \
+1f 90 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): direction=orig,proto=6,zone_id=13,src=fd18::ffff:abcd:1,dst=fd18::ffff:abcd:2,src_port=80,dst_port=8080
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 38 00 00 00 03 00 00 23 20 00 00 00 20 \
+01 \
+01 \
+00 0d \
+00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 01 \
+00 00 00 00 00 00 00 00 00 00 ff ff 0a 0a 00 02 \
+00 01 \
+00 0a \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): direction=orig,proto=1,zone_id=13,src=10.10.0.1,dst=10.10.0.2,id=1,type=0,code=10
+])
+
+AT_CHECK([ovs-ofctl ofp-print "\
+01 04 00 38 00 00 00 03 00 00 23 20 00 00 00 20 \
+01 \
+01 \
+00 0d \
+fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 01 \
+fd 18 00 00 00 00 00 00 00 00 ff ff ab cd 00 02 \
+00 01 \
+0a 00 \
+"], [0], [dnl
+NXT_CT_FLUSH (xid=0x3): direction=orig,proto=1,zone_id=13,src=fd18::ffff:abcd:1,dst=fd18::ffff:abcd:2,id=1,type=10,code=0
+])
+AT_CLEANUP