@@ -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 \
new file mode 100644
@@ -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
+ * ----------------------------------------------------------------------- */
+
+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.
+ */
+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.
+ */
+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;
+}
new file mode 100644
@@ -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 */
@@ -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.
@@ -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 *);
@@ -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 //
@@ -3709,3 +3717,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
@@ -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
@@ -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},
};
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. Signed-off-by: Aaron Conole <aconole@redhat.com> --- 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