On 18 May 2026, at 19:13, Aaron Conole wrote:

> This includes a test netdev offload an a suite of unit tests to
> ensure functionality.  To facilitate the testing, some special
> offload APIs are added that force offload to true.  It is expected
> that these are not called unless within a testing environment.

Hi Aaron,

I just looked over this patch, not a real review, as I feel
like we need a proper dpif-offload-dummy integration to
pinpoint the integration issues that I think exist.  This
way we can also run general unit tests.

What I also noticed is that there is no way to see from a
user perspective that a flow is offloaded, and by which
providers.

//Eelco

> Signed-off-by: Aaron Conole <[email protected]>
> ---
>  lib/automake.mk        |   2 +
>  lib/ct-offload-dummy.c | 253 +++++++++++++++++++++++++++++++++
>  lib/ct-offload-dummy.h |  64 +++++++++
>  lib/ct-offload.c       |  12 +-
>  lib/ct-offload.h       |  10 ++
>  tests/dpif-netdev.at   |  72 ++++++++++
>  tests/library.at       |  36 +++++
>  tests/test-conntrack.c | 314 +++++++++++++++++++++++++++++++++++++++++
>  8 files changed, 762 insertions(+), 1 deletion(-)
>  create mode 100644 lib/ct-offload-dummy.c
>  create mode 100644 lib/ct-offload-dummy.h
>
> diff --git a/lib/automake.mk b/lib/automake.mk
> index f11e3de27c..b9dc5118fa 100644
> --- a/lib/automake.mk
> +++ b/lib/automake.mk
> @@ -99,6 +99,8 @@ lib_libopenvswitch_la_SOURCES = \
>       lib/conntrack.h \
>       lib/ct-offload.c \
>       lib/ct-offload.h \
> +     lib/ct-offload-dummy.c \
> +     lib/ct-offload-dummy.h \
>       lib/cooperative-multitasking.c \
>       lib/cooperative-multitasking.h \
>       lib/cooperative-multitasking-private.h \
> diff --git a/lib/ct-offload-dummy.c b/lib/ct-offload-dummy.c
> new file mode 100644
> index 0000000000..c85f478e6c
> --- /dev/null
> +++ b/lib/ct-offload-dummy.c
> @@ -0,0 +1,253 @@
> +/*
> + * Copyright (c) 2026 Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "ct-offload-dummy.h"
> +#include "ct-offload.h"
> +#include "hash.h"
> +#include "openvswitch/list.h"
> +#include "openvswitch/vlog.h"
> +#include "ovs-thread.h"
> +#include "timeval.h"
> +#include "util.h"
> +
> +VLOG_DEFINE_THIS_MODULE(ct_offload_dummy);
> +
> +/* -----------------------------------------------------------------------
> + * Per-connection tracking
> + * ----------------------------------------------------------------------- */

These look like AI generated sections.  The coding style
says to use form feeds (control+L) to divide source files,
with a single-line comment if needed.

> +
> +struct ct_dummy_entry {
> +    struct ovs_list   list_node;
> +    const struct conn *conn;
> +    struct netdev     *netdev_fwd_in;
> +    struct netdev     *netdev_rev_in;
> +};
> +
> +/* ct-offload infrastructure guarantees that we get called under the offload
> + * mutex, but the counters that we have are simple ints that can be erased
> + * at any time from any thread, so we have this extra mutex for consistency.
> + */

Comments should end on the same line as the closing */.

> +static struct ovs_mutex    dummy_mutex    = OVS_MUTEX_INITIALIZER;
> +
> +/* Since this is a testing interface, we can use the above mutex when 
> checking
> + * the fake list of offloaded connections for other properties (like the
> + * bidireactionality, etc).  A proper hardware offload implementation 
> shouldn't
> + * generally need this amount of critical sections.
> + */

Same here.  Also "bidireactionality" -> "bidirectionality"?

> +static struct ovs_list     dummy_conns    OVS_GUARDED_BY(dummy_mutex)
> +    = OVS_LIST_INITIALIZER(&dummy_conns);
> +
> +static unsigned int n_added       = 0;
> +static unsigned int n_deleted     = 0;
> +static unsigned int n_updated     = 0;
> +static unsigned int n_established = 0;
> +
> +/* Lookup must be called with dummy_mutex held. */
> +static struct ct_dummy_entry *
> +dummy_find__(const struct conn *conn)
> +    OVS_REQUIRES(dummy_mutex)
> +{
> +    struct ct_dummy_entry *e;
> +
> +    LIST_FOR_EACH (e, list_node, &dummy_conns) {
> +        if (e->conn == conn) {
> +            return e;
> +        }
> +    }
> +    return NULL;
> +}
> +
> +static bool
> +dummy_can_offload(const struct ct_offload_ctx *ctx OVS_UNUSED)
> +{
> +    /* Always accept that we can offload in the dummy provider */
> +    return true;
> +}
> +
> +static int
> +dummy_conn_add(const struct ct_offload_ctx *ctx)
> +{
> +    struct ct_dummy_entry *e = xmalloc(sizeof *e);
> +
> +    e->conn = ctx->conn;
> +    e->netdev_fwd_in = ctx->netdev_in;
> +    e->netdev_rev_in = NULL;
> +
> +    ovs_mutex_lock(&dummy_mutex);
> +    ovs_list_push_back(&dummy_conns, &e->list_node);
> +    n_added++;
> +    ovs_mutex_unlock(&dummy_mutex);
> +
> +    VLOG_DBG("ct_offload_dummy: conn add: conn=%p, netdev_fwd_in=%p",
> +             ctx->conn, ctx->netdev_in);
> +    return 0;
> +}
> +
> +static void
> +dummy_conn_del(const struct ct_offload_ctx *ctx)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    struct ct_dummy_entry *e = dummy_find__(ctx->conn);
> +
> +    if (e) {
> +        ovs_list_remove(&e->list_node);
> +        n_deleted++;
> +        free(e);
> +    }
> +    ovs_mutex_unlock(&dummy_mutex);
> +
> +    VLOG_DBG("ct_offload_dummy: conn del: conn=%p", ctx->conn);
> +}
> +
> +static void
> +dummy_conn_established(const struct ct_offload_ctx *ctx)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    struct ct_dummy_entry *e = dummy_find__(ctx->conn);
> +
> +    if (e && !e->netdev_rev_in) {
> +        e->netdev_rev_in = ctx->netdev_in;
> +        n_established++;
> +        VLOG_DBG("ct_offload_dummy: conn established: conn=%p "
> +                 "netdev_fwd_in=%p netdev_rev_in=%p",
> +                 ctx->conn, e->netdev_fwd_in, e->netdev_rev_in);
> +    }
> +    ovs_mutex_unlock(&dummy_mutex);
> +}
> +
> +static long long
> +dummy_conn_update(const struct ct_offload_ctx *ctx)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    struct ct_dummy_entry *e = dummy_find__(ctx->conn);
> +
> +    if (!e) {
> +        ovs_mutex_unlock(&dummy_mutex);
> +        return 0;
> +    }
> +
> +    n_updated++;
> +    ovs_mutex_unlock(&dummy_mutex);
> +
> +    VLOG_DBG("ct_offload_dummy: conn update: conn=%p", ctx->conn);
> +    return time_msec();
> +}
> +
> +static void
> +dummy_flush(void)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    struct ct_dummy_entry *e;
> +    LIST_FOR_EACH_POP (e, list_node, &dummy_conns) {
> +        n_deleted++;
> +        free(e);
> +    }
> +    ovs_mutex_unlock(&dummy_mutex);
> +}
> +
> +/* -----------------------------------------------------------------------
> + * Provider class
> + * ----------------------------------------------------------------------- */
> +
> +const struct ct_offload_class ct_offload_dummy_class = {
> +    .name             = "dummy",
> +    .init             = NULL,
> +    .batch_submit     = NULL,
> +    .conn_add         = dummy_conn_add,
> +    .conn_del         = dummy_conn_del,
> +    .conn_update      = dummy_conn_update,
> +    .conn_established = dummy_conn_established,
> +    .can_offload      = dummy_can_offload,
> +    .flush            = dummy_flush,
> +};
> +
> +/* -----------------------------------------------------------------------
> + * Public API
> + * ----------------------------------------------------------------------- */
> +
> +void
> +ct_offload_dummy_register(void)
> +{
> +    ct_offload_dummy_reset_counters();
> +    ct_offload_register(&ct_offload_dummy_class);
> +}
> +
> +void
> +ct_offload_dummy_unregister(void)
> +{
> +    /* Flush any leftover entries before unregistering so we do not leak. */
> +    dummy_flush();
> +    ct_offload_unregister(&ct_offload_dummy_class);
> +}
> +
> +unsigned int
> +ct_offload_dummy_n_added(void)
> +{
> +    return n_added;
> +}
> +
> +unsigned int
> +ct_offload_dummy_n_deleted(void)
> +{
> +    return n_deleted;
> +}
> +
> +unsigned int
> +ct_offload_dummy_n_updated(void)
> +{
> +    return n_updated;
> +}
> +
> +unsigned int
> +ct_offload_dummy_n_established(void)
> +{
> +    return n_established;
> +}
> +
> +void
> +ct_offload_dummy_reset_counters(void)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    n_added       = 0;
> +    n_deleted     = 0;
> +    n_updated     = 0;
> +    n_established = 0;
> +    ovs_mutex_unlock(&dummy_mutex);
> +}
> +
> +bool
> +ct_offload_dummy_contains(const struct conn *conn)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    bool found = dummy_find__(conn) != NULL;
> +    ovs_mutex_unlock(&dummy_mutex);
> +    return found;
> +}
> +
> +/* Returns true if the dummy provider has seen both the forward-direction
> + * input netdev (recorded at conn_add) and the reply-direction input netdev
> + * (recorded at conn_established) for 'conn'. */
> +bool
> +ct_offload_dummy_is_bidirectional(const struct conn *conn)
> +{
> +    ovs_mutex_lock(&dummy_mutex);
> +    struct ct_dummy_entry *e = dummy_find__(conn);
> +    bool bidi = e && e->netdev_fwd_in && e->netdev_rev_in;
> +    ovs_mutex_unlock(&dummy_mutex);
> +    return bidi;
> +}
> diff --git a/lib/ct-offload-dummy.h b/lib/ct-offload-dummy.h
> new file mode 100644
> index 0000000000..1e7ecfdb04
> --- /dev/null
> +++ b/lib/ct-offload-dummy.h
> @@ -0,0 +1,64 @@
> +/*
> + * Copyright (c) 2026 Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef CT_OFFLOAD_DUMMY_H
> +#define CT_OFFLOAD_DUMMY_H 1
> +
> +/* Dummy CT offload provider
> + * =========================
> + *
> + * A software-only implementation of the ct_offload_class interface used for
> + * unit testing.  It records every conn_add/conn_del/conn_update call and
> + * exposes inspection helpers so tests can verify that the correct hooks are
> + * reached without requiring any hardware.
> + *
> + * Typical usage:
> + *
> + *   ct_offload_dummy_register();   // activate the provider
> + *   conntrack_execute(...);        // exercises conn_add
> + *   ovs_assert(ct_offload_dummy_n_added() == 1);
> + *   conntrack_flush(...);          // exercises conn_del
> + *   ovs_assert(ct_offload_dummy_n_deleted() == 1);
> + *   ct_offload_dummy_unregister(); // tear down after test
> + */
> +
> +#include <stdbool.h>
> +
> +struct conn;
> +
> +/* Register (or unregister) the dummy provider.
> + *
> + * ct_offload_dummy_register() also marks CT offload as "enabled" within the
> + * dummy so that the guards in conntrack.c fire even without hardware offload
> + * being configured globally.  Call ct_offload_dummy_unregister() to undo. */
> +void ct_offload_dummy_register(void);
> +void ct_offload_dummy_unregister(void);
> +
> +/* Counters.  Initialized to zero and can be reset. */
> +unsigned int ct_offload_dummy_n_added(void);
> +unsigned int ct_offload_dummy_n_deleted(void);
> +unsigned int ct_offload_dummy_n_updated(void);
> +unsigned int ct_offload_dummy_n_established(void);
> +
> +/* Reset all counters without changing registered state. */
> +void ct_offload_dummy_reset_counters(void);
> +
> +/* Returns true if 'conn' is currently tracked by the dummy (was added but
> + * not yet deleted or flushed). */
> +bool ct_offload_dummy_contains(const struct conn *conn);
> +bool ct_offload_dummy_is_bidirectional(const struct conn *conn);
> +
> +#endif /* CT_OFFLOAD_DUMMY_H */
> diff --git a/lib/ct-offload.c b/lib/ct-offload.c
> index 1b4d230b80..34710cf57b 100644
> --- a/lib/ct-offload.c
> +++ b/lib/ct-offload.c
> @@ -57,6 +57,10 @@ static struct ovs_list  ct_offload_classes
>   * registered dpif offload class will be activated by 
> ct_offload_module_init().
>   */
>  static const struct ct_offload_class *base_ct_offload_classes[] = {
> +    /* Dummy provider: activated whenever the "dummy" dpif offload class is
> +     * registered (hw-offload=true with a dummy datapath).  Also used 
> directly
> +     * by unit tests via ct_offload_dummy_register(). */
> +    &ct_offload_dummy_class,
>  };
>
>
> @@ -166,6 +170,12 @@ ct_offload_module_init(void)
>      }
>  }
>
> +static bool ct_offload_forced = false;
> +void ct_offload_force_enable(bool value)
> +{
> +    ct_offload_forced = value;
> +}
> +
>  /* ct_offload_enabled() - returns true when hardware offload is active.
>   *
>   * Delegates to dpif_offload_enabled() so CT offload shares the same global
> @@ -173,7 +183,7 @@ ct_offload_module_init(void)
>  bool
>  ct_offload_enabled(void)
>  {
> -    return dpif_offload_enabled();
> +    return dpif_offload_enabled() || ct_offload_forced;
>  }
>
>  /* ct_offload_set_global_cfg() - configure CT offload from OVSDB.
> diff --git a/lib/ct-offload.h b/lib/ct-offload.h
> index fe4ecd33b8..3836852703 100644
> --- a/lib/ct-offload.h
> +++ b/lib/ct-offload.h
> @@ -83,6 +83,12 @@ struct ct_offload_class {
>      void (*flush)(void);
>  };
>
> +/* Dummy (software-only) CT offload provider, always compiled in.
> + * Registered automatically when the "dummy" dpif offload class is active
> + * (e.g. hw-offload=true with a dummy datapath), and available directly for
> + * unit tests via ct_offload_dummy_register() in ct-offload-dummy.h. */
> +extern const struct ct_offload_class ct_offload_dummy_class;
> +
>  /* Register/unregister a provider.  Must be called at module init, before
>   * any connections are created. */
>  int  ct_offload_register(const struct ct_offload_class *);
> @@ -100,6 +106,10 @@ void ct_offload_set_global_cfg(const struct 
> ovsrec_open_vswitch *);
>   */
>  bool ct_offload_enabled(void);
>
> +/* Used for testing.  Forces an additional parameter for the offload enable
> + * check.  Set to 'true' to always enable the offloads. */
> +void ct_offload_force_enable(bool);
> +
>  /* Per-connection offload API that dispatches to all registered providers. */
>  int       ct_offload_conn_add(const struct ct_offload_ctx *);
>  void      ct_offload_conn_del(const struct ct_offload_ctx *);
> diff --git a/tests/dpif-netdev.at b/tests/dpif-netdev.at
> index b0eb9ea63f..552a269455 100644
> --- a/tests/dpif-netdev.at
> +++ b/tests/dpif-netdev.at
> @@ -50,6 +50,14 @@ filter_hw_packet_netdev_dummy () {
>          | sort | uniq
>  }
>
> +filter_ct_offload_dummy_conn_add () {
> +    grep 'ct_offload_dummy.*conn add:' | sed 's/.*|DBG|//' | sort | uniq
> +}
> +
> +filter_ct_offload_dummy_conn_del () {
> +    grep 'ct_offload_dummy.*conn del:' | sed 's/.*|DBG|//' | sort | uniq
> +}
> +
>  filter_flow_dump () {
>      grep 'flow_dump ' | sed '
>          s/.*flow_dump //
> @@ -3734,3 +3742,67 @@ AT_CHECK_UNQUOTED([tail -n 1 p1.pcap.txt], [0], 
> [${good_expected_v6}
>
>  OVS_VSWITCHD_STOP
>  AT_CLEANUP
> +
> +dnl Test that the CT offload dummy provider receives conn_add and conn_del
> +dnl callbacks when packets are processed through a conntrack commit flow on a
> +dnl dummy datapath with hw-offload enabled.
> +AT_SETUP([dpif-netdev - conntrack offload dummy])
> +AT_KEYWORDS([conntrack offload])
> +OVS_VSWITCHD_START(
> +  [add-port br0 p1 -- \
> +   set interface p1 type=dummy ofport_request=1 \
> +                    options:pstream=punix:$OVS_RUNDIR/p1.sock \
> +                    options:ifindex=1100 -- \
> +   add-port br0 p2 -- \
> +   set interface p2 type=dummy ofport_request=2 \
> +                    options:pstream=punix:$OVS_RUNDIR/p2.sock \
> +                    options:ifindex=1101 -- \
> +   set bridge br0 datapath-type=dummy \
> +                  other-config:datapath-id=1234 fail-mode=secure], [], [], 
> [])
> +
> +dnl Enable debug logging for the dpif offload and CT offload dummy modules so
> +dnl the test can detect hook calls via log grep.
> +AT_CHECK([ovs-appctl vlog/set dpif_offload_dummy:file:dbg 
> ct_offload_dummy:file:dbg])
> +
> +dnl Enable hardware offload — this registers the "dummy" dpif offload class
> +dnl and automatically activates the CT offload dummy provider.
> +AT_CHECK([ovs-vsctl set Open_vSwitch . other_config:hw-offload=true])
> +OVS_WAIT_UNTIL([grep "Flow HW offload is enabled" ovs-vswitchd.log])
> +
> +dnl Add a two-table conntrack flow:
> +dnl  table 0: untracked packets → ct(commit) recirculate to table 1
> +dnl  table 1: tracked packets   → output on p2
> +AT_CHECK([ovs-ofctl add-flow br0 \
> +  
> 'table=0,priority=100,in_port=p1,ip,ct_state=-trk,actions=ct(commit,table=1)'])
> +AT_CHECK([ovs-ofctl add-flow br0 \
> +  'table=1,priority=100,in_port=p1,ip,ct_state=+trk,actions=output:p2'])
> +
> +dnl Compose and inject a UDP packet on p1.  The first packet misses the
> +dnl datapath, causes an upcall, executes ct(commit) to create a conntrack
> +dnl entry, and triggers the ct_offload_dummy conn_add callback.
> +flow_s="eth_src=50:54:00:00:00:01,eth_dst=50:54:00:00:00:02,udp,ip_src=10.0.0.1,ip_dst=10.0.0.2,ip_frag=no,udp_src=1000,udp_dst=2000"
> +pkt=$(ovs-ofctl compose-packet --bare "${flow_s}")
> +AT_CHECK([ovs-appctl netdev-dummy/receive p1 "${pkt}"])
> +
> +dnl Wait for the CT offload dummy conn_add hook to fire.
> +OVS_WAIT_UNTIL([grep 'ct_offload_dummy.*conn add:' ovs-vswitchd.log])
> +
> +dnl Verify exactly one connection was added.
> +AT_CHECK([filter_ct_offload_dummy_conn_add < ovs-vswitchd.log | wc -l | tr 
> -d ' '],
> +  [0], [1
> +])
> +
> +dnl Flush all conntrack entries — conn_clean is called for every tracked
> +dnl connection, which invokes ct_offload_conn_del on each registered 
> provider.
> +AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> +
> +dnl Wait for the CT offload dummy conn_del hook to fire.
> +OVS_WAIT_UNTIL([grep 'ct_offload_dummy.*conn del:' ovs-vswitchd.log])
> +
> +dnl Verify exactly one connection was deleted.
> +AT_CHECK([filter_ct_offload_dummy_conn_del < ovs-vswitchd.log | wc -l | tr 
> -d ' '],
> +  [0], [1
> +])
> +
> +OVS_VSWITCHD_STOP
> +AT_CLEANUP
> diff --git a/tests/library.at b/tests/library.at
> index 6c5b55f045..2d5b02f75b 100644
> --- a/tests/library.at
> +++ b/tests/library.at
> @@ -325,3 +325,39 @@ AT_KEYWORDS([conntrack])
>  AT_CHECK([ovstest test-conntrack private-destructor], [0], [.
>  ])
>  AT_CLEANUP
> +
> +AT_SETUP([conntrack offload dummy - conn add hook])
> +AT_KEYWORDS([conntrack offload])
> +AT_CHECK([ovstest test-conntrack offload-conn-add], [0], [.
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack offload dummy - conn del hook])
> +AT_KEYWORDS([conntrack offload])
> +AT_CHECK([ovstest test-conntrack offload-conn-del], [0], [.
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack offload dummy - conn update hook])
> +AT_KEYWORDS([conntrack offload])
> +AT_CHECK([ovstest test-conntrack offload-conn-update], [0], [.
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack offload dummy - multiple connections])
> +AT_KEYWORDS([conntrack offload])
> +AT_CHECK([ovstest test-conntrack offload-multi-conn], [0], [.
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack offload dummy - conn established hook (end-to-end)])
> +AT_KEYWORDS([conntrack offload])
> +AT_CHECK([ovstest test-conntrack offload-conn-established], [0], [.
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack offload dummy - conn established fires exactly once 
> (API)])
> +AT_KEYWORDS([conntrack offload])
> +AT_CHECK([ovstest test-conntrack offload-conn-established-api], [0], [.
> +])
> +AT_CLEANUP
> diff --git a/tests/test-conntrack.c b/tests/test-conntrack.c
> index 3c409b373b..86f1f36d3f 100644
> --- a/tests/test-conntrack.c
> +++ b/tests/test-conntrack.c
> @@ -17,6 +17,8 @@
>  #include <config.h>
>  #include "conntrack.h"
>  #include "conntrack-private.h"
> +#include "ct-offload.h"
> +#include "ct-offload-dummy.h"
>
>  #include "dp-packet.h"
>  #include "fatal-signal.h"
> @@ -691,6 +693,304 @@ test_private_destructor(struct ovs_cmdl_context *ctx 
> OVS_UNUSED)
>      printf(".\n");
>  }
>
> +
> +/* 
> ===========================================================================
> + * CT offload dummy provider tests
> + *
> + * These tests exercise the ct_offload provider API directly without going
> + * through conntrack_execute.  The offload global-enable flag is deliberately
> + * not set here: the unit tests own the provider list and call the API
> + * functions directly.  End-to-end enablement (hw-offload=true via DB config)
> + * is covered by the dpif-netdev integration test.
> + *
> + * Each test must be run as a separate ovstest invocation so that the
> + * process-global provider list starts empty.
> + * 
> ===========================================================================
> + */
> +
> +/* The dummy only compares pointer addresses and never dereferences them, so 
> a
> + * small integer cast is sufficient. */
> +#define FAKE_CONN(n)   ((struct conn *)(uintptr_t)(n))
> +#define FAKE_NETDEV(n) ((struct netdev *)(uintptr_t)(n))
> +
> +/* Test: offload-conn-add
> + * ----------------------
> + * Register the dummy provider, call ct_offload_conn_add() directly, and
> + * verify that the conn_add hook was invoked and the connection is tracked.
> + */
> +static void
> +test_offload_conn_add(struct ovs_cmdl_context *ctx OVS_UNUSED)
> +{
> +    ct_offload_force_enable(true);
> +    ct_offload_dummy_register();
> +
> +    struct conn *fake = FAKE_CONN(1);
> +    struct ct_offload_ctx offload_ctx = {
> +        .conn = fake, .netdev_in = NULL,
> +    };
> +    ct_offload_conn_add(&offload_ctx);
> +
> +    ovs_assert(ct_offload_dummy_n_added() == 1);
> +    ovs_assert(ct_offload_dummy_contains(fake));
> +
> +    ct_offload_dummy_unregister();
> +    ct_offload_force_enable(false);
> +    printf(".\n");
> +}
> +
> +/* Test: offload-conn-del
> + * ----------------------
> + * Register the dummy, add then delete a connection via the API, and verify
> + * that conn_del was called and the connection is no longer tracked.
> + */
> +static void
> +test_offload_conn_del(struct ovs_cmdl_context *ctx OVS_UNUSED)
> +{
> +    ct_offload_force_enable(true);
> +    ct_offload_dummy_register();
> +
> +    struct conn *fake = FAKE_CONN(1);
> +    struct ct_offload_ctx offload_ctx = {
> +        .conn = fake, .netdev_in = NULL,
> +    };
> +
> +    ct_offload_conn_add(&offload_ctx);
> +    ovs_assert(ct_offload_dummy_n_added() == 1);
> +
> +    ct_offload_conn_del(&offload_ctx);
> +    ovs_assert(ct_offload_dummy_n_deleted() == 1);
> +    ovs_assert(!ct_offload_dummy_contains(fake));
> +
> +    ct_offload_dummy_unregister();
> +    ct_offload_force_enable(false);
> +    printf(".\n");
> +}
> +
> +/* Test: offload-conn-update
> + * -------------------------
> + * Register the dummy, add a connection, call ct_offload_conn_update()
> + * directly, and verify that a non-zero last-used timestamp is returned.
> + */
> +static void
> +test_offload_conn_update(struct ovs_cmdl_context *ctx OVS_UNUSED)
> +{
> +    ct_offload_force_enable(true);
> +    ct_offload_dummy_register();
> +
> +    struct conn *fake = FAKE_CONN(1);
> +    struct ct_offload_ctx offload_ctx = {
> +        .conn = fake, .netdev_in = NULL,
> +    };
> +
> +    ct_offload_conn_add(&offload_ctx);
> +
> +    long long ts = ct_offload_conn_update(&offload_ctx);
> +    ovs_assert(ts != 0);
> +    ovs_assert(ct_offload_dummy_n_updated() == 1);
> +
> +    ct_offload_dummy_unregister();
> +    ct_offload_force_enable(false);
> +    printf(".\n");
> +}
> +
> +/* Test: offload-multi-conn
> + * ------------------------
> + * Register the dummy, add N connections via the API, and verify that each
> + * is tracked independently.
> + */
> +#define OFFLOAD_MULTI_N 4
> +
> +static void
> +test_offload_multi_conn(struct ovs_cmdl_context *ctx OVS_UNUSED)
> +{
> +    ct_offload_force_enable(true);
> +    ct_offload_dummy_register();
> +
> +    for (unsigned i = 1; i <= OFFLOAD_MULTI_N; i++) {
> +        struct ct_offload_ctx offload_ctx = {
> +            .conn = FAKE_CONN(i), .netdev_in = NULL,
> +        };
> +        ct_offload_conn_add(&offload_ctx);
> +    }
> +
> +    ovs_assert(ct_offload_dummy_n_added() == OFFLOAD_MULTI_N);
> +    for (unsigned i = 1; i <= OFFLOAD_MULTI_N; i++) {
> +        ovs_assert(ct_offload_dummy_contains(FAKE_CONN(i)));
> +    }
> +
> +    ct_offload_dummy_unregister();
> +    ct_offload_force_enable(false);
> +    printf(".\n");
> +}
> +
> +/* Test: offload-conn-established
> + * --------------------------------
> + * Drive a TCP three-way handshake through conntrack_execute() with the dummy
> + * offload provider registered.  Verifies three properties:
> + *
> + *  (a) conn_add fires on the SYN (new connection created, forward netdev
> + *      recorded); conn_established does NOT fire yet.
> + *  (b) conn_established fires exactly once on the first ESTABLISHED reply
> + *      (SYN-ACK), recording the reply-direction netdev so that the dummy
> + *      entry is fully bidirectional.
> + *  (c) A subsequent reply packet (ACK) does NOT cause a second
> + *      conn_established call the "exactly once" guarantee holds.
> + *
> + * ct_offload_dummy_register() calls ct_offload_force_enable(true), which
> + * makes ct_offload_enabled() return true so the guards in conntrack.c fire
> + * without a real hardware offload backend.
> + */
> +static void
> +test_offload_conn_established(struct ovs_cmdl_context *ctx OVS_UNUSED)
> +{
> +    /* Allocate the per-connection private slot before registering so that 
> the
> +     * ADD/ESTABLISHED state transitions are tracked in conn->private[].
> +     * The simple FAKE_CONN tests skip this step because they do not exercise
> +     * the private-slot code path. */
> +    ct_offload_alloc_private_slot();
> +    ct_offload_force_enable(true);
> +    ct_offload_dummy_register();
> +
> +    struct conntrack *lct = conntrack_init();
> +    /* Disable TCP sequence-number checking so test packets with seq=0 are
> +     * accepted by the state machine. */
> +    conntrack_set_tcp_seq_chk(lct, false);
> +
> +    long long now = time_msec();
> +
> +    struct eth_addr eth_a = ETH_ADDR_C(00, 00, 00, 00, 00, 01);
> +    struct eth_addr eth_b = ETH_ADDR_C(00, 00, 00, 00, 00, 02);
> +    ovs_be32 ip_a = inet_addr("10.0.0.1");
> +    ovs_be32 ip_b = inet_addr("10.0.0.2");
> +    uint16_t sport = 1234;
> +    uint16_t dport = 80;
> +
> +    /* --- (a) SYN: forward direction, creates the connection entry. --- */
> +    struct dp_packet *syn = build_eth_ip_packet(NULL, eth_a, eth_b,
> +                                                ip_a, ip_b,
> +                                                IPPROTO_TCP, 0);
> +    build_tcp_packet(syn, sport, dport, TCP_SYN, NULL, 0);
> +
> +    struct dp_packet_batch syn_batch;
> +    dp_packet_batch_init_packet(&syn_batch, syn);
> +    conntrack_execute(lct, &syn_batch, htons(ETH_TYPE_IP), false, true, 0,
> +                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(1));
> +
> +    /* conn_add must have fired; conn_established must not have. */
> +    ovs_assert(ct_offload_dummy_n_added() == 1);
> +    ovs_assert(ct_offload_dummy_n_established() == 0);
> +
> +    /* The packet carries the conn pointer after commit. */
> +    struct conn *conn = syn->md.conn;
> +    ovs_assert(conn != NULL);
> +    ovs_assert(ct_offload_conn_is_offloaded(conn));
> +    ovs_assert(!ct_offload_conn_is_established(conn));
> +
> +    dp_packet_delete_batch(&syn_batch, true);
> +
> +    /* --- (b) SYN-ACK: reply direction, transitions to ESTABLISHED. --- */
> +    struct dp_packet *synack = build_eth_ip_packet(NULL, eth_b, eth_a,
> +                                                   ip_b, ip_a,
> +                                                   IPPROTO_TCP, 0);
> +    build_tcp_packet(synack, dport, sport, TCP_SYN | TCP_ACK, NULL, 0);
> +
> +    struct dp_packet_batch synack_batch;
> +    dp_packet_batch_init_packet(&synack_batch, synack);
> +    conntrack_execute(lct, &synack_batch, htons(ETH_TYPE_IP), false, true, 0,
> +                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(2));
> +
> +    /* conn_established fires exactly once on the first ESTABLISHED reply. */
> +    ovs_assert(ct_offload_dummy_n_established() == 1);
> +    ovs_assert(ct_offload_conn_is_established(conn));
> +    /* Both netdev pointers are now known: the entry is fully bidirectional. 
> */
> +    ovs_assert(ct_offload_dummy_is_bidirectional(conn));
> +
> +    dp_packet_delete_batch(&synack_batch, true);
> +
> +    /* --- (c) ACK: another reply packet must NOT trigger conn_established
> +     *             again.  The private-slot guard enforces this. --- */
> +    struct dp_packet *ack = build_eth_ip_packet(NULL, eth_b, eth_a,
> +                                                ip_b, ip_a,
> +                                                IPPROTO_TCP, 0);
> +    build_tcp_packet(ack, dport, sport, TCP_ACK, NULL, 0);
> +
> +    struct dp_packet_batch ack_batch;
> +    dp_packet_batch_init_packet(&ack_batch, ack);
> +    conntrack_execute(lct, &ack_batch, htons(ETH_TYPE_IP), false, true, 0,
> +                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(2));
> +
> +    /* Counter must still be 1 - conn_established must not have fired again. 
> */
> +    ovs_assert(ct_offload_dummy_n_established() == 1);
> +
> +    dp_packet_delete_batch(&ack_batch, true);
> +
> +    conntrack_destroy(lct);
> +    ct_offload_dummy_unregister();
> +    ct_offload_force_enable(false);
> +    printf(".\n");
> +}
> +
> +/* Test: offload-conn-established-api
> + * ------------------------------------
> + * Exercise ct_offload_conn_established() directly (not through
> + * conntrack_execute) to verify that the "exactly once" guarantee in the
> + * dispatch layer holds independently of the conntrack state machine.
> + *
> + * Sequence:
> + *   1. conn_add() - transitions the private slot to CT_OFFLOAD_STATE_ADDED.
> + *   2. conn_established() - should dispatch to the provider exactly once and
> + *      advance the slot to CT_OFFLOAD_STATE_EST.
> + *   3. A second conn_established() call with the same conn must be a no-op
> + *      (provider not called again, counter unchanged).
> + */
> +static void
> +test_offload_conn_established_api(struct ovs_cmdl_context *ctx OVS_UNUSED)
> +{
> +    ct_offload_alloc_private_slot();
> +    ct_offload_force_enable(true);
> +    ct_offload_dummy_register();
> +
> +    /* We need a real conn with a live private-data slot, so spin up a 
> minimal
> +     * conntrack instance and commit one UDP packet to get a conn. */
> +    struct conntrack *lct = conntrack_init();
> +    long long now = time_msec();
> +
> +    ovs_be16 dl_type;
> +    struct dp_packet *pkt = build_packet(1, 2, &dl_type);
> +    struct dp_packet_batch batch;
> +    dp_packet_batch_init_packet(&batch, pkt);
> +    conntrack_execute(lct, &batch, dl_type, false, true, 0,
> +                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(1));
> +    struct conn *conn = pkt->md.conn;
> +    ovs_assert(conn != NULL);
> +    dp_packet_delete_batch(&batch, true);
> +
> +    /* conn_add should have fired (via conntrack_execute). */
> +    ovs_assert(ct_offload_dummy_n_added() == 1);
> +    ovs_assert(ct_offload_dummy_n_established() == 0);
> +    ovs_assert(ct_offload_conn_is_offloaded(conn));
> +    ovs_assert(!ct_offload_conn_is_established(conn));
> +
> +    /* First call: must dispatch to the provider. */
> +    struct ct_offload_ctx ctx1 = {
> +        .conn = conn, .netdev_in = FAKE_NETDEV(2),
> +    };
> +    ct_offload_conn_established(&ctx1);
> +    ovs_assert(ct_offload_dummy_n_established() == 1);
> +    ovs_assert(ct_offload_conn_is_established(conn));
> +    ovs_assert(ct_offload_dummy_is_bidirectional(conn));
> +
> +    /* Second call with the same conn: must be a no-op. */
> +    ct_offload_conn_established(&ctx1);
> +
> +    ovs_assert(ct_offload_dummy_n_established() == 1);  /* unchanged */
> +
> +    conntrack_destroy(lct);
> +    ct_offload_dummy_unregister();
> +    ct_offload_force_enable(false);
> +    printf(".\n");
> +}
> +
>  
>  static const struct ovs_cmdl_command commands[] = {
>      /* Connection tracker tests. */
> @@ -725,6 +1025,20 @@ static const struct ovs_cmdl_command commands[] = {
>       test_private_id_exhaustion, OVS_RO},
>      {"private-destructor", "", 0, 0,
>       test_private_destructor, OVS_RO},
> +    /* CT offload dummy provider tests.
> +     * Each must be run as a separate ovstest invocation. */
> +    {"offload-conn-add", "", 0, 0,
> +     test_offload_conn_add, OVS_RO},
> +    {"offload-conn-del", "", 0, 0,
> +     test_offload_conn_del, OVS_RO},
> +    {"offload-conn-update", "", 0, 0,
> +     test_offload_conn_update, OVS_RO},
> +    {"offload-multi-conn", "", 0, 0,
> +     test_offload_multi_conn, OVS_RO},
> +    {"offload-conn-established", "", 0, 0,
> +     test_offload_conn_established, OVS_RO},
> +    {"offload-conn-established-api", "", 0, 0,
> +     test_offload_conn_established_api, OVS_RO},
>
>      {NULL, NULL, 0, 0, NULL, OVS_RO},
>  };
> -- 
> 2.53.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to