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
