diff mbox series

[ovs-dev] conntrack: Allow to dump userspace conntrack expectations.

Message ID 168751642384.719785.16745960299941143091.stgit@rawp
State Accepted
Commit 9b4d2ad8e8cf720e150ea038600cc85b6f79d465
Headers show
Series [ovs-dev] conntrack: Allow to dump userspace conntrack expectations. | 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

Paolo Valerio June 23, 2023, 10:33 a.m. UTC
The patch introduces a new commands ovs-appctl dpctl/dump-conntrack-exp
that allows to dump the existing expectations for the userspace ct.

Signed-off-by: Paolo Valerio <pvalerio@redhat.com>
---
 NEWS                             |    2 +
 lib/conntrack.c                  |   66 +++++++++++++++++++++++++++++
 lib/conntrack.h                  |   10 ++++
 lib/ct-dpif.c                    |   87 ++++++++++++++++++++++++++++++++++++++
 lib/ct-dpif.h                    |   15 +++++++
 lib/dpctl.c                      |   49 +++++++++++++++++++++
 lib/dpctl.man                    |    6 +++
 lib/dpif-netdev.c                |   50 ++++++++++++++++++++++
 lib/dpif-netlink.c               |    3 +
 lib/dpif-provider.h              |   11 +++++
 tests/system-kmod-macros.at      |    9 ++++
 tests/system-traffic.at          |   44 +++++++++++++++++++
 tests/system-userspace-macros.at |    6 +++
 13 files changed, 357 insertions(+), 1 deletion(-)

Comments

Ilya Maximets June 29, 2023, 10:39 p.m. UTC | #1
On 6/23/23 12:33, Paolo Valerio wrote:
> The patch introduces a new commands ovs-appctl dpctl/dump-conntrack-exp
> that allows to dump the existing expectations for the userspace ct.
> 
> Signed-off-by: Paolo Valerio <pvalerio@redhat.com>
> ---
>  NEWS                             |    2 +
>  lib/conntrack.c                  |   66 +++++++++++++++++++++++++++++
>  lib/conntrack.h                  |   10 ++++
>  lib/ct-dpif.c                    |   87 ++++++++++++++++++++++++++++++++++++++
>  lib/ct-dpif.h                    |   15 +++++++
>  lib/dpctl.c                      |   49 +++++++++++++++++++++
>  lib/dpctl.man                    |    6 +++
>  lib/dpif-netdev.c                |   50 ++++++++++++++++++++++
>  lib/dpif-netlink.c               |    3 +
>  lib/dpif-provider.h              |   11 +++++
>  tests/system-kmod-macros.at      |    9 ++++
>  tests/system-traffic.at          |   44 +++++++++++++++++++
>  tests/system-userspace-macros.at |    6 +++
>  13 files changed, 357 insertions(+), 1 deletion(-)
> 
> diff --git a/NEWS b/NEWS
> index 66d5a4ea3..16cdb6933 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -24,6 +24,8 @@ Post-v3.1.0
>       * New commands "dpctl/{ct-get-sweep-interval,ct-set-sweep-interval}" that
>         allow to get and set, for the userspace datapath, the sweep interval
>         for the conntrack garbage collector.
> +     * New commands "dpctl/dump-conntrack-exp" that allows to dump
> +       conntrack's expectations for the userspace datapath.

Applied.  Thanks!

Best regards, Ilya Maximets.
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 66d5a4ea3..16cdb6933 100644
--- a/NEWS
+++ b/NEWS
@@ -24,6 +24,8 @@  Post-v3.1.0
      * New commands "dpctl/{ct-get-sweep-interval,ct-set-sweep-interval}" that
        allow to get and set, for the userspace datapath, the sweep interval
        for the conntrack garbage collector.
+     * New commands "dpctl/dump-conntrack-exp" that allows to dump
+       conntrack's expectations for the userspace datapath.
    - ovs-ctl:
      * Added new options --[ovsdb-server|ovs-vswitchd]-umask=MODE to set umask
        value when starting OVS daemons.  E.g., use --ovsdb-server-umask=0002
diff --git a/lib/conntrack.c b/lib/conntrack.c
index f5ebfa05b..4375c03e2 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -2670,6 +2670,72 @@  conntrack_dump_done(struct conntrack_dump *dump OVS_UNUSED)
     return 0;
 }
 
+static void
+exp_node_to_ct_dpif_exp(const struct alg_exp_node *exp,
+                        struct ct_dpif_exp *entry)
+{
+    memset(entry, 0, sizeof *entry);
+
+    conn_key_to_tuple(&exp->key, &entry->tuple_orig);
+    conn_key_to_tuple(&exp->parent_key, &entry->tuple_parent);
+    entry->zone = exp->key.zone;
+    entry->mark = exp->parent_mark;
+    memcpy(&entry->labels, &exp->parent_label, sizeof entry->labels);
+    entry->protoinfo.proto = exp->key.nw_proto;
+}
+
+int
+conntrack_exp_dump_start(struct conntrack *ct, struct conntrack_dump *dump,
+                         const uint16_t *pzone)
+{
+    memset(dump, 0, sizeof(*dump));
+
+    if (pzone) {
+        dump->zone = *pzone;
+        dump->filter_zone = true;
+    }
+
+    dump->ct = ct;
+
+    return 0;
+}
+
+int
+conntrack_exp_dump_next(struct conntrack_dump *dump, struct ct_dpif_exp *entry)
+{
+    struct conntrack *ct = dump->ct;
+    struct alg_exp_node *enode;
+    int ret = EOF;
+
+    ovs_rwlock_rdlock(&ct->resources_lock);
+
+    for (;;) {
+        struct hmap_node *node = hmap_at_position(&ct->alg_expectations,
+                                                  &dump->hmap_pos);
+        if (!node) {
+            break;
+        }
+
+        enode = CONTAINER_OF(node, struct alg_exp_node, node);
+
+        if (!dump->filter_zone || enode->key.zone == dump->zone) {
+            ret = 0;
+            exp_node_to_ct_dpif_exp(enode, entry);
+            break;
+        }
+    }
+
+    ovs_rwlock_unlock(&ct->resources_lock);
+
+    return ret;
+}
+
+int
+conntrack_exp_dump_done(struct conntrack_dump *dump OVS_UNUSED)
+{
+    return 0;
+}
+
 int
 conntrack_flush(struct conntrack *ct, const uint16_t *zone)
 {
diff --git a/lib/conntrack.h b/lib/conntrack.h
index 524ec0acb..57d5159b6 100644
--- a/lib/conntrack.h
+++ b/lib/conntrack.h
@@ -100,7 +100,10 @@  void conntrack_clear(struct dp_packet *packet);
 struct conntrack_dump {
     struct conntrack *ct;
     unsigned bucket;
-    struct cmap_position cm_pos;
+    union {
+        struct cmap_position cm_pos;
+        struct hmap_position hmap_pos;
+    };
     bool filter_zone;
     uint16_t zone;
 };
@@ -132,6 +135,11 @@  int conntrack_dump_start(struct conntrack *, struct conntrack_dump *,
 int conntrack_dump_next(struct conntrack_dump *, struct ct_dpif_entry *);
 int conntrack_dump_done(struct conntrack_dump *);
 
+int conntrack_exp_dump_start(struct conntrack *, struct conntrack_dump *,
+                             const uint16_t *);
+int conntrack_exp_dump_next(struct conntrack_dump *, struct ct_dpif_exp *);
+int conntrack_exp_dump_done(struct conntrack_dump *);
+
 int conntrack_flush(struct conntrack *, const uint16_t *zone);
 int conntrack_flush_tuple(struct conntrack *, const struct ct_dpif_tuple *,
                           uint16_t zone);
diff --git a/lib/ct-dpif.c b/lib/ct-dpif.c
index 0c4b2964f..f59c6e560 100644
--- a/lib/ct-dpif.c
+++ b/lib/ct-dpif.c
@@ -101,6 +101,65 @@  ct_dpif_dump_done(struct ct_dpif_dump_state *dump)
             ? dpif->dpif_class->ct_dump_done(dpif, dump)
             : EOPNOTSUPP);
 }
+
+/* Start dumping the expectations from the connection tracker.
+ *
+ * 'dump' must be the address of a pointer to a struct ct_dpif_dump_state,
+ * which should be passed (unaltered) to ct_exp_dpif_dump_{next,done}().
+ *
+ * If 'zone' is not NULL, it should point to an integer identifing a
+ * conntrack zone to which the dump will be limited.  If it is NULL,
+ * conntrack entries from all zones will be dumped.
+ *
+ * If there has been a problem the function returns a non-zero value
+ * that represents the error.  Otherwise it returns zero. */
+int
+ct_exp_dpif_dump_start(struct dpif *dpif, struct ct_dpif_dump_state **dump,
+                       const uint16_t *zone)
+{
+    int err;
+
+    err = (dpif->dpif_class->ct_exp_dump_start
+           ? dpif->dpif_class->ct_exp_dump_start(dpif, dump, zone)
+           : EOPNOTSUPP);
+
+    if (!err) {
+        (*dump)->dpif = dpif;
+    }
+
+    return err;
+}
+
+/* Dump one expectation and put it in 'entry'.
+ *
+ * 'dump' should have been initialized by ct_exp_dpif_dump_start().
+ *
+ * The function returns 0, if an entry has been dumped succesfully.
+ * Otherwise it returns a non-zero value which can be:
+ * - EOF: meaning that there are no more entries to dump.
+ * - an error value.
+ * In both cases, the user should call ct_exp_dpif_dump_done(). */
+int
+ct_exp_dpif_dump_next(struct ct_dpif_dump_state *dump,
+                      struct ct_dpif_exp *entry)
+{
+    struct dpif *dpif = dump->dpif;
+
+    return (dpif->dpif_class->ct_exp_dump_next
+            ? dpif->dpif_class->ct_exp_dump_next(dpif, dump, entry)
+            : EOPNOTSUPP);
+}
+
+/* Free resources used by 'dump', if any. */
+int
+ct_exp_dpif_dump_done(struct ct_dpif_dump_state *dump)
+{
+    struct dpif *dpif = dump->dpif;
+
+    return (dpif->dpif_class->ct_exp_dump_done
+            ? dpif->dpif_class->ct_exp_dump_done(dpif, dump)
+            : EOPNOTSUPP);
+}
 
 /* Flushing. */
 
@@ -462,6 +521,34 @@  ct_dpif_status_flags(uint32_t flags)
     }
 }
 
+void
+ct_dpif_format_exp_entry(const struct ct_dpif_exp *entry, struct ds *ds)
+{
+    ct_dpif_format_ipproto(ds, entry->tuple_orig.ip_proto);
+
+    ds_put_cstr(ds, ",orig=(");
+    ct_dpif_format_tuple(ds, &entry->tuple_orig);
+    ds_put_cstr(ds, ")");
+
+    if (entry->zone) {
+        ds_put_format(ds, ",zone=%"PRIu16, entry->zone);
+    }
+    if (entry->mark) {
+        ds_put_format(ds, ",mark=%"PRIu32, entry->mark);
+    }
+    if (!ovs_u128_is_zero(entry->labels)) {
+        ovs_be128 value;
+
+        ds_put_cstr(ds, ",labels=");
+        value = hton128(entry->labels);
+        ds_put_hex(ds, &value, sizeof value);
+    }
+
+    ds_put_cstr(ds, ",parent=(");
+    ct_dpif_format_tuple(ds, &entry->tuple_parent);
+    ds_put_cstr(ds, ")");
+}
+
 void
 ct_dpif_format_entry(const struct ct_dpif_entry *entry, struct ds *ds,
                      bool verbose, bool print_stats)
diff --git a/lib/ct-dpif.h b/lib/ct-dpif.h
index 5579ac925..0b728b529 100644
--- a/lib/ct-dpif.h
+++ b/lib/ct-dpif.h
@@ -179,6 +179,16 @@  enum ct_dpif_status_flags {
 
 #define CT_DPIF_STATUS_MASK ((CT_DPIF_STATUS_UNTRACKED << 1) - 1)
 
+struct ct_dpif_exp {
+    struct ct_dpif_tuple tuple_orig;
+    struct ct_dpif_tuple tuple_parent;
+    uint16_t zone;
+    struct ct_dpif_protoinfo protoinfo;
+    ovs_u128 labels;
+    uint32_t status;
+    uint32_t mark;
+};
+
 struct ct_dpif_entry {
     /* Const members. */
     struct ct_dpif_tuple tuple_orig;
@@ -286,6 +296,10 @@  int ct_dpif_dump_start(struct dpif *, struct ct_dpif_dump_state **,
                        const uint16_t *zone, int *);
 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_exp_dpif_dump_start(struct dpif *, struct ct_dpif_dump_state **,
+                           const uint16_t *zone);
+int ct_exp_dpif_dump_next(struct ct_dpif_dump_state *, struct ct_dpif_exp *);
+int ct_exp_dpif_dump_done(struct ct_dpif_dump_state *);
 int ct_dpif_flush(struct dpif *, const uint16_t *zone,
                   const struct ofp_ct_match *);
 int ct_dpif_set_maxconns(struct dpif *dpif, uint32_t maxconns);
@@ -310,6 +324,7 @@  int ct_dpif_ipf_dump_done(struct dpif *dpif, void *);
 void ct_dpif_entry_uninit(struct ct_dpif_entry *);
 void ct_dpif_format_entry(const struct ct_dpif_entry *, struct ds *,
                           bool verbose, bool print_stats);
+void ct_dpif_format_exp_entry(const struct ct_dpif_exp *, struct ds *);
 void ct_dpif_format_ipproto(struct ds *ds, uint16_t ipproto);
 void ct_dpif_format_tuple(struct ds *, const struct ct_dpif_tuple *);
 uint8_t ct_dpif_coalesce_tcp_state(uint8_t state);
diff --git a/lib/dpctl.c b/lib/dpctl.c
index 15950bd50..4394653ab 100644
--- a/lib/dpctl.c
+++ b/lib/dpctl.c
@@ -1707,6 +1707,53 @@  dpctl_dump_conntrack(int argc, const char *argv[],
     return error;
 }
 
+static int
+dpctl_dump_conntrack_exp(int argc, const char *argv[],
+                         struct dpctl_params *dpctl_p)
+{
+    struct ct_dpif_dump_state *dump;
+    uint16_t zone, *pzone = NULL;
+    struct ct_dpif_exp cte;
+    struct dpif *dpif;
+    int error;
+
+    if (argc > 1 && ovs_scan(argv[argc - 1], "zone=%"SCNu16, &zone)) {
+        pzone = &zone;
+        argc--;
+    }
+
+    error = opt_dpif_open(argc, argv, dpctl_p, 2, &dpif);
+    if (error) {
+        return error;
+    }
+
+    error = ct_exp_dpif_dump_start(dpif, &dump, pzone);
+    if (error) {
+        dpctl_error(dpctl_p, error, "starting conntrack expectations dump");
+        dpif_close(dpif);
+        return error;
+    }
+
+    while (!(error = ct_exp_dpif_dump_next(dump, &cte))) {
+        struct ds s = DS_EMPTY_INITIALIZER;
+
+        ct_dpif_format_exp_entry(&cte, &s);
+
+        dpctl_print(dpctl_p, "%s\n", ds_cstr(&s));
+        ds_destroy(&s);
+    }
+    if (error == EOF) {
+        error = 0;
+    } else if (error) {
+        dpctl_error(dpctl_p, error, "dumping conntrack expectation");
+    }
+
+    ct_exp_dpif_dump_done(dump);
+    dpif_close(dpif);
+
+    return error;
+}
+
 static int
 dpctl_flush_conntrack(int argc, const char *argv[],
                       struct dpctl_params *dpctl_p)
@@ -2951,6 +2998,8 @@  static const struct dpctl_command all_commands[] = {
       0, 1, dpctl_offload_stats_show, DP_RO },
     { "dump-conntrack", "[-m] [-s] [dp] [zone=N]",
       0, 4, dpctl_dump_conntrack, DP_RO },
+    { "dump-conntrack-exp", "[dp] [zone=N]",
+      0, 2, dpctl_dump_conntrack_exp, DP_RO },
     { "flush-conntrack", "[dp] [zone=N] [ct-orig-tuple] [ct-reply-tuple]",
       0, 4, dpctl_flush_conntrack, DP_RW },
     { "cache-get-size", "[dp]", 0, 1, dpctl_cache_get_size, DP_RO },
diff --git a/lib/dpctl.man b/lib/dpctl.man
index d448596d3..66fc50903 100644
--- a/lib/dpctl.man
+++ b/lib/dpctl.man
@@ -302,6 +302,12 @@  are included. With \fB\-\-statistics\fR timeouts and timestamps are
 added to the output.
 .
 .TP
+\*(DX\fBdump\-conntrack\-exp\fR [\fIdp\fR] [\fBzone=\fIzone\fR]
+Prints to the console all the expectation entries in the tracker used by
+\fIdp\fR.  If \fBzone=\fIzone\fR is specified, only shows the expectations
+in \fIzone\fR. Only supported for userspace datapath.
+.
+.TP
 \*(DX\fBflush\-conntrack\fR [\fIdp\fR] [\fBzone=\fIzone\fR] [\fIct-origin-tuple\fR [\fIct-reply-tuple\fR]]
 Flushes the connection entries in the tracker used by \fIdp\fR based on
 \fIzone\fR and connection tracking tuple \fIct-origin-tuple\fR.
diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c
index abe63412e..feab15d21 100644
--- a/lib/dpif-netdev.c
+++ b/lib/dpif-netdev.c
@@ -9267,6 +9267,53 @@  dpif_netdev_ct_dump_done(struct dpif *dpif OVS_UNUSED,
     return err;
 }
 
+static int
+dpif_netdev_ct_exp_dump_start(struct dpif *dpif,
+                              struct ct_dpif_dump_state **dump_,
+                              const uint16_t *pzone)
+{
+    struct dp_netdev *dp = get_dp_netdev(dpif);
+    struct dp_netdev_ct_dump *dump;
+
+    dump = xzalloc(sizeof *dump);
+    dump->dp = dp;
+    dump->ct = dp->conntrack;
+
+    conntrack_exp_dump_start(dp->conntrack, &dump->dump, pzone);
+
+    *dump_ = &dump->up;
+
+    return 0;
+}
+
+static int
+dpif_netdev_ct_exp_dump_next(struct dpif *dpif OVS_UNUSED,
+                             struct ct_dpif_dump_state *dump_,
+                             struct ct_dpif_exp *entry)
+{
+    struct dp_netdev_ct_dump *dump;
+
+    INIT_CONTAINER(dump, dump_, up);
+
+    return conntrack_exp_dump_next(&dump->dump, entry);
+}
+
+static int
+dpif_netdev_ct_exp_dump_done(struct dpif *dpif OVS_UNUSED,
+                             struct ct_dpif_dump_state *dump_)
+{
+    struct dp_netdev_ct_dump *dump;
+    int err;
+
+    INIT_CONTAINER(dump, dump_, up);
+
+    err = conntrack_exp_dump_done(&dump->dump);
+
+    free(dump);
+
+    return err;
+}
+
 static int
 dpif_netdev_ct_flush(struct dpif *dpif, const uint16_t *zone,
                      const struct ct_dpif_tuple *tuple)
@@ -9679,6 +9726,9 @@  const struct dpif_class dpif_netdev_class = {
     dpif_netdev_ct_dump_start,
     dpif_netdev_ct_dump_next,
     dpif_netdev_ct_dump_done,
+    dpif_netdev_ct_exp_dump_start,
+    dpif_netdev_ct_exp_dump_next,
+    dpif_netdev_ct_exp_dump_done,
     dpif_netdev_ct_flush,
     dpif_netdev_ct_set_maxconns,
     dpif_netdev_ct_get_maxconns,
diff --git a/lib/dpif-netlink.c b/lib/dpif-netlink.c
index 60bd39643..9194971d3 100644
--- a/lib/dpif-netlink.c
+++ b/lib/dpif-netlink.c
@@ -4566,6 +4566,9 @@  const struct dpif_class dpif_netlink_class = {
     dpif_netlink_ct_dump_start,
     dpif_netlink_ct_dump_next,
     dpif_netlink_ct_dump_done,
+    NULL,                       /* ct_exp_dump_start */
+    NULL,                       /* ct_exp_dump_next */
+    NULL,                       /* ct_exp_dump_done */
     dpif_netlink_ct_flush,
     NULL,                       /* ct_set_maxconns */
     NULL,                       /* ct_get_maxconns */
diff --git a/lib/dpif-provider.h b/lib/dpif-provider.h
index a33c6ec30..db1f063e0 100644
--- a/lib/dpif-provider.h
+++ b/lib/dpif-provider.h
@@ -79,6 +79,7 @@  dpif_flow_dump_thread_init(struct dpif_flow_dump_thread *thread,
 
 struct ct_dpif_dump_state;
 struct ct_dpif_entry;
+struct ct_dpif_exp;
 struct ct_dpif_tuple;
 struct ct_dpif_timeout_policy;
 enum ct_features;
@@ -471,6 +472,16 @@  struct dpif_class {
                         struct ct_dpif_entry *entry);
     int (*ct_dump_done)(struct dpif *, struct ct_dpif_dump_state *state);
 
+    /* Starts the dump initializing the structures involved and the zone
+     * filter. */
+    int (*ct_exp_dump_start)(struct dpif *, struct ct_dpif_dump_state **state,
+                             const uint16_t *zone);
+    /* Fill the expectation 'entry' with the related informations. */
+    int (*ct_exp_dump_next)(struct dpif *, struct ct_dpif_dump_state *state,
+                            struct ct_dpif_exp *entry);
+    /* Ends the dump cleaning up any potential pending state, if any. */
+    int (*ct_exp_dump_done)(struct dpif *, struct ct_dpif_dump_state *state);
+
     /* Flushes the connection tracking tables.  The arguments have the
      * following behavior:
      *
diff --git a/tests/system-kmod-macros.at b/tests/system-kmod-macros.at
index 712925ded..81601390d 100644
--- a/tests/system-kmod-macros.at
+++ b/tests/system-kmod-macros.at
@@ -123,6 +123,15 @@  m4_define([CHECK_CONNTRACK_TIMEOUT],
     on_exit 'modprobe -r nfnetlink_cttimeout'
 ])
 
+# CHECK_CONNTRACK_DUMP_EXPECTATIONS()
+#
+# Perform requirements checks for dumping conntrack expectations.
+#
+m4_define([CHECK_CONNTRACK_DUMP_EXPECTATIONS],
+[
+    AT_SKIP_IF([:])
+])
+
 # CHECK_CT_DPIF_SET_GET_MAXCONNS()
 #
 # Perform requirements checks for running ovs-dpctl ct-set-maxconns or
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 4c378e1d0..a05ca311c 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -5195,6 +5195,50 @@  tcp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=<cleared>,dport=<cleared>),reply=(src=
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([conntrack - FTP with expectation dump])
+AT_SKIP_IF([test $HAVE_FTP = no])
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_ALG()
+CHECK_CONNTRACK_DUMP_EXPECTATIONS()
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+AT_DATA([flows.txt], [dnl
+table=0,priority=1,action=drop
+table=0,priority=10,arp,action=normal
+table=0,priority=10,icmp,action=normal
+table=0,priority=100,in_port=1,tcp,action=ct(alg=ftp,commit),2
+table=0,priority=100,in_port=2,tcp,action=ct(table=1)
+table=1,in_port=2,tcp,ct_state=+trk+est,action=1
+table=1,in_port=2,tcp,ct_state=+trk+rel,action=1
+])
+
+AT_CHECK([ovs-ofctl --bundle replace-flows br0 flows.txt])
+
+OVS_START_L7([at_ns1], [ftp])
+
+dnl FTP requests from p0->p1 should work fine.
+NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.1.1.2)], [0], [dnl
+tcp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=<cleared>,dport=<cleared>),reply=(src=10.1.1.2,dst=10.1.1.1,sport=<cleared>,dport=<cleared>),protoinfo=(state=<cleared>),helper=ftp
+])
+
+dnl Verify that a dump with zero entries in a zone doesn't return any entry.
+AT_CHECK([ovs-appctl dpctl/dump-conntrack-exp zone=42], [0], [dnl
+])
+
+AT_CHECK([ovs-appctl dpctl/dump-conntrack-exp | FORMAT_CT(10.1.1.2)], [0], [dnl
+tcp,orig=(src=10.1.1.2,dst=10.1.1.1,sport=<cleared>,dport=<cleared>),parent=(src=10.1.1.1,dst=10.1.1.2,sport=<cleared>,dport=<cleared>)
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_SETUP([conntrack - FTP over IPv6])
 AT_SKIP_IF([test $HAVE_FTP = no])
 CHECK_CONNTRACK()
diff --git a/tests/system-userspace-macros.at b/tests/system-userspace-macros.at
index c1855cbc5..73e0e843b 100644
--- a/tests/system-userspace-macros.at
+++ b/tests/system-userspace-macros.at
@@ -112,6 +112,12 @@  m4_define([CHECK_CONNTRACK_ZEROIP_SNAT])
 #
 m4_define([CHECK_CONNTRACK_TIMEOUT])
 
+# CHECK_CONNTRACK_DUMP_EXPECTATIONS()
+#
+# Perform requirements checks for dumping conntrack expectations.
+#
+m4_define([CHECK_CONNTRACK_DUMP_EXPECTATIONS])
+
 # CHECK_CT_DPIF_SET_GET_MAXCONNS()
 #
 # Perform requirements checks for running ovs-dpctl ct-set-maxconns or