get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/1.1/patches/2221014/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2221014,
    "url": "http://patchwork.ozlabs.org/api/1.1/patches/2221014/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/openvswitch/patch/20260408170613.587902-12-aconole@redhat.com/",
    "project": {
        "id": 47,
        "url": "http://patchwork.ozlabs.org/api/1.1/projects/47/?format=api",
        "name": "Open vSwitch",
        "link_name": "openvswitch",
        "list_id": "ovs-dev.openvswitch.org",
        "list_email": "ovs-dev@openvswitch.org",
        "web_url": "http://openvswitch.org/",
        "scm_url": "git@github.com:openvswitch/ovs.git",
        "webscm_url": "https://github.com/openvswitch/ovs"
    },
    "msgid": "<20260408170613.587902-12-aconole@redhat.com>",
    "date": "2026-04-08T17:06:07",
    "name": "[ovs-dev,RFC,11/12] ct-offload-dummy: Introduce dummy ct offload.",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "6bed7e6977f782c61893d2187e7eee13b6845887",
    "submitter": {
        "id": 67184,
        "url": "http://patchwork.ozlabs.org/api/1.1/people/67184/?format=api",
        "name": "Aaron Conole",
        "email": "aconole@redhat.com"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/openvswitch/patch/20260408170613.587902-12-aconole@redhat.com/mbox/",
    "series": [
        {
            "id": 499163,
            "url": "http://patchwork.ozlabs.org/api/1.1/series/499163/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/openvswitch/list/?series=499163",
            "date": "2026-04-08T17:05:56",
            "name": "ct-offload: Introduce a conntrack offload infrastructure.",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/499163/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2221014/comments/",
    "check": "success",
    "checks": "http://patchwork.ozlabs.org/api/patches/2221014/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<ovs-dev-bounces@openvswitch.org>",
        "X-Original-To": [
            "incoming@patchwork.ozlabs.org",
            "dev@openvswitch.org"
        ],
        "Delivered-To": [
            "patchwork-incoming@legolas.ozlabs.org",
            "ovs-dev@lists.linuxfoundation.org"
        ],
        "Authentication-Results": [
            "legolas.ozlabs.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=QxQ2m/xL;\n\tdkim-atps=neutral",
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=140.211.166.138; helo=smtp1.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org)",
            "smtp1.osuosl.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key)\n header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=QxQ2m/xL",
            "smtp3.osuosl.org; dmarc=pass (p=quarantine dis=none)\n header.from=redhat.com",
            "smtp3.osuosl.org;\n dkim=fail reason=\"signature verification failed\" (1024-bit key)\n header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=QxQ2m/xL"
        ],
        "Received": [
            "from smtp1.osuosl.org (smtp1.osuosl.org [140.211.166.138])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4frTyV3rVVz1xv0\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 09 Apr 2026 03:07:30 +1000 (AEST)",
            "from localhost (localhost [127.0.0.1])\n\tby smtp1.osuosl.org (Postfix) with ESMTP id 683FC824DF;\n\tWed,  8 Apr 2026 17:07:28 +0000 (UTC)",
            "from smtp1.osuosl.org ([127.0.0.1])\n by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id 8dGeEhqDOKzP; Wed,  8 Apr 2026 17:07:23 +0000 (UTC)",
            "from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56])\n\tby smtp1.osuosl.org (Postfix) with ESMTPS id 0FF7F82861;\n\tWed,  8 Apr 2026 17:07:20 +0000 (UTC)",
            "from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id 05F1BC054A;\n\tWed,  8 Apr 2026 17:07:20 +0000 (UTC)",
            "from smtp3.osuosl.org (smtp3.osuosl.org [IPv6:2605:bc80:3010::136])\n by lists.linuxfoundation.org (Postfix) with ESMTP id 04850C0902\n for <dev@openvswitch.org>; Wed,  8 Apr 2026 17:07:19 +0000 (UTC)",
            "from localhost (localhost [127.0.0.1])\n by smtp3.osuosl.org (Postfix) with ESMTP id ADD8F60F9D\n for <dev@openvswitch.org>; Wed,  8 Apr 2026 17:06:47 +0000 (UTC)",
            "from smtp3.osuosl.org ([127.0.0.1])\n by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id XOzoG9zeYnL9 for <dev@openvswitch.org>;\n Wed,  8 Apr 2026 17:06:46 +0000 (UTC)",
            "from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.129.124])\n by smtp3.osuosl.org (Postfix) with ESMTPS id D0C1060FA7\n for <dev@openvswitch.org>; Wed,  8 Apr 2026 17:06:45 +0000 (UTC)",
            "from mx-prod-mc-05.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-561-HaIZrK0LPtGnRsKFo3ZWKg-1; Wed,\n 08 Apr 2026 13:06:41 -0400",
            "from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com\n (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4])\n (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest\n SHA256)\n (No client certificate requested)\n by mx-prod-mc-05.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id 69DA7195608A; Wed,  8 Apr 2026 17:06:40 +0000 (UTC)",
            "from RHTRH0061144.redhat.com (unknown [10.22.89.172])\n by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id 9DB94300019F; Wed,  8 Apr 2026 17:06:38 +0000 (UTC)"
        ],
        "X-Virus-Scanned": [
            "amavis at osuosl.org",
            "amavis at osuosl.org"
        ],
        "X-Comment": "SPF check N/A for local connections - client-ip=140.211.9.56;\n helo=lists.linuxfoundation.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN> ",
        "DKIM-Filter": [
            "OpenDKIM Filter v2.11.0 smtp1.osuosl.org 0FF7F82861",
            "OpenDKIM Filter v2.11.0 smtp3.osuosl.org D0C1060FA7"
        ],
        "Received-SPF": "Pass (mailfrom) identity=mailfrom; client-ip=170.10.129.124;\n helo=us-smtp-delivery-124.mimecast.com; envelope-from=aconole@redhat.com;\n receiver=<UNKNOWN>",
        "DMARC-Filter": "OpenDMARC Filter v1.4.2 smtp3.osuosl.org D0C1060FA7",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1775668004;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=967KTdKt3UxhOnjgK316UNFyip0Iv+PUu9GdP6AK710=;\n b=QxQ2m/xL6jdoHiFLA+6tmhJcpxy4PgNXKL9V8d6cZmBBgLsdOuWAWybWuQ/eGZDPHocvVN\n syygfhNth5KpLtqdVZIFjLKftfXSBqhNwTb3VxM7dRoJFMEoFnFLoZdP4hDwMtCGmFoAwy\n Mnw6D1yCo6TRYVQ2ksuGdhIiJOcCHA0=",
        "X-MC-Unique": "HaIZrK0LPtGnRsKFo3ZWKg-1",
        "X-Mimecast-MFC-AGG-ID": "HaIZrK0LPtGnRsKFo3ZWKg_1775668000",
        "To": "dev@openvswitch.org",
        "Date": "Wed,  8 Apr 2026 13:06:07 -0400",
        "Message-ID": "<20260408170613.587902-12-aconole@redhat.com>",
        "In-Reply-To": "<20260408170613.587902-1-aconole@redhat.com>",
        "References": "<20260408170613.587902-1-aconole@redhat.com>",
        "MIME-Version": "1.0",
        "X-Scanned-By": "MIMEDefang 3.4.1 on 10.30.177.4",
        "X-Mimecast-Spam-Score": "0",
        "X-Mimecast-MFC-PROC-ID": "_gnBeOEOZqn-xnQvSG32QhfsIyNCOanT4muWwHG0ZEM_1775668000",
        "X-Mimecast-Originator": "redhat.com",
        "Subject": "[ovs-dev] [RFC 11/12] ct-offload-dummy: Introduce dummy ct offload.",
        "X-BeenThere": "ovs-dev@openvswitch.org",
        "X-Mailman-Version": "2.1.30",
        "Precedence": "list",
        "List-Id": "<ovs-dev.openvswitch.org>",
        "List-Unsubscribe": "<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>",
        "List-Archive": "<http://mail.openvswitch.org/pipermail/ovs-dev/>",
        "List-Post": "<mailto:ovs-dev@openvswitch.org>",
        "List-Help": "<mailto:ovs-dev-request@openvswitch.org?subject=help>",
        "List-Subscribe": "<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>",
        "From": "Aaron Conole via dev <ovs-dev@openvswitch.org>",
        "Reply-To": "Aaron Conole <aconole@redhat.com>",
        "Cc": "Eli Britstein <elibr@nvidia.com>, Florian Westphal <fwestpha@redhat.com>,\n Flavio Leitner <fbl@redhat.com>",
        "Content-Type": "text/plain; charset=\"utf-8\"",
        "Content-Transfer-Encoding": "base64",
        "Errors-To": "ovs-dev-bounces@openvswitch.org",
        "Sender": "\"dev\" <ovs-dev-bounces@openvswitch.org>"
    },
    "content": "This includes a test netdev offload an a suite of unit tests to\nensure functionality.  To facilitate the testing, some special\noffload APIs are added that force offload to true.  It is expected\nthat these are not called unless within a testing environment.\n\nSigned-off-by: Aaron Conole <aconole@redhat.com>\n---\n lib/automake.mk        |   2 +\n lib/ct-offload-dummy.c | 253 +++++++++++++++++++++++++++++++++\n lib/ct-offload-dummy.h |  64 +++++++++\n lib/ct-offload.c       |  12 +-\n lib/ct-offload.h       |  10 ++\n tests/dpif-netdev.at   |  72 ++++++++++\n tests/library.at       |  36 +++++\n tests/test-conntrack.c | 314 +++++++++++++++++++++++++++++++++++++++++\n 8 files changed, 762 insertions(+), 1 deletion(-)\n create mode 100644 lib/ct-offload-dummy.c\n create mode 100644 lib/ct-offload-dummy.h",
    "diff": "diff --git a/lib/automake.mk b/lib/automake.mk\nindex f11e3de27c..b9dc5118fa 100644\n--- a/lib/automake.mk\n+++ b/lib/automake.mk\n@@ -99,6 +99,8 @@ lib_libopenvswitch_la_SOURCES = \\\n \tlib/conntrack.h \\\n \tlib/ct-offload.c \\\n \tlib/ct-offload.h \\\n+\tlib/ct-offload-dummy.c \\\n+\tlib/ct-offload-dummy.h \\\n \tlib/cooperative-multitasking.c \\\n \tlib/cooperative-multitasking.h \\\n \tlib/cooperative-multitasking-private.h \\\ndiff --git a/lib/ct-offload-dummy.c b/lib/ct-offload-dummy.c\nnew file mode 100644\nindex 0000000000..c85f478e6c\n--- /dev/null\n+++ b/lib/ct-offload-dummy.c\n@@ -0,0 +1,253 @@\n+/*\n+ * Copyright (c) 2026 Red Hat, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at:\n+ *\n+ *     http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+#include <config.h>\n+\n+#include \"ct-offload-dummy.h\"\n+#include \"ct-offload.h\"\n+#include \"hash.h\"\n+#include \"openvswitch/list.h\"\n+#include \"openvswitch/vlog.h\"\n+#include \"ovs-thread.h\"\n+#include \"timeval.h\"\n+#include \"util.h\"\n+\n+VLOG_DEFINE_THIS_MODULE(ct_offload_dummy);\n+\n+/* -----------------------------------------------------------------------\n+ * Per-connection tracking\n+ * ----------------------------------------------------------------------- */\n+\n+struct ct_dummy_entry {\n+    struct ovs_list   list_node;\n+    const struct conn *conn;\n+    struct netdev     *netdev_fwd_in;\n+    struct netdev     *netdev_rev_in;\n+};\n+\n+/* ct-offload infrastructure guarantees that we get called under the offload\n+ * mutex, but the counters that we have are simple ints that can be erased\n+ * at any time from any thread, so we have this extra mutex for consistency.\n+ */\n+static struct ovs_mutex    dummy_mutex    = OVS_MUTEX_INITIALIZER;\n+\n+/* Since this is a testing interface, we can use the above mutex when checking\n+ * the fake list of offloaded connections for other properties (like the\n+ * bidireactionality, etc).  A proper hardware offload implementation shouldn't\n+ * generally need this amount of critical sections.\n+ */\n+static struct ovs_list     dummy_conns    OVS_GUARDED_BY(dummy_mutex)\n+    = OVS_LIST_INITIALIZER(&dummy_conns);\n+\n+static unsigned int n_added       = 0;\n+static unsigned int n_deleted     = 0;\n+static unsigned int n_updated     = 0;\n+static unsigned int n_established = 0;\n+\n+/* Lookup must be called with dummy_mutex held. */\n+static struct ct_dummy_entry *\n+dummy_find__(const struct conn *conn)\n+    OVS_REQUIRES(dummy_mutex)\n+{\n+    struct ct_dummy_entry *e;\n+\n+    LIST_FOR_EACH (e, list_node, &dummy_conns) {\n+        if (e->conn == conn) {\n+            return e;\n+        }\n+    }\n+    return NULL;\n+}\n+\n+static bool\n+dummy_can_offload(const struct ct_offload_ctx *ctx OVS_UNUSED)\n+{\n+    /* Always accept that we can offload in the dummy provider */\n+    return true;\n+}\n+\n+static int\n+dummy_conn_add(const struct ct_offload_ctx *ctx)\n+{\n+    struct ct_dummy_entry *e = xmalloc(sizeof *e);\n+\n+    e->conn = ctx->conn;\n+    e->netdev_fwd_in = ctx->netdev_in;\n+    e->netdev_rev_in = NULL;\n+\n+    ovs_mutex_lock(&dummy_mutex);\n+    ovs_list_push_back(&dummy_conns, &e->list_node);\n+    n_added++;\n+    ovs_mutex_unlock(&dummy_mutex);\n+\n+    VLOG_DBG(\"ct_offload_dummy: conn add: conn=%p, netdev_fwd_in=%p\",\n+             ctx->conn, ctx->netdev_in);\n+    return 0;\n+}\n+\n+static void\n+dummy_conn_del(const struct ct_offload_ctx *ctx)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    struct ct_dummy_entry *e = dummy_find__(ctx->conn);\n+\n+    if (e) {\n+        ovs_list_remove(&e->list_node);\n+        n_deleted++;\n+        free(e);\n+    }\n+    ovs_mutex_unlock(&dummy_mutex);\n+\n+    VLOG_DBG(\"ct_offload_dummy: conn del: conn=%p\", ctx->conn);\n+}\n+\n+static void\n+dummy_conn_established(const struct ct_offload_ctx *ctx)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    struct ct_dummy_entry *e = dummy_find__(ctx->conn);\n+\n+    if (e && !e->netdev_rev_in) {\n+        e->netdev_rev_in = ctx->netdev_in;\n+        n_established++;\n+        VLOG_DBG(\"ct_offload_dummy: conn established: conn=%p \"\n+                 \"netdev_fwd_in=%p netdev_rev_in=%p\",\n+                 ctx->conn, e->netdev_fwd_in, e->netdev_rev_in);\n+    }\n+    ovs_mutex_unlock(&dummy_mutex);\n+}\n+\n+static long long\n+dummy_conn_update(const struct ct_offload_ctx *ctx)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    struct ct_dummy_entry *e = dummy_find__(ctx->conn);\n+\n+    if (!e) {\n+        ovs_mutex_unlock(&dummy_mutex);\n+        return 0;\n+    }\n+\n+    n_updated++;\n+    ovs_mutex_unlock(&dummy_mutex);\n+\n+    VLOG_DBG(\"ct_offload_dummy: conn update: conn=%p\", ctx->conn);\n+    return time_msec();\n+}\n+\n+static void\n+dummy_flush(void)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    struct ct_dummy_entry *e;\n+    LIST_FOR_EACH_POP (e, list_node, &dummy_conns) {\n+        n_deleted++;\n+        free(e);\n+    }\n+    ovs_mutex_unlock(&dummy_mutex);\n+}\n+\n+/* -----------------------------------------------------------------------\n+ * Provider class\n+ * ----------------------------------------------------------------------- */\n+\n+const struct ct_offload_class ct_offload_dummy_class = {\n+    .name             = \"dummy\",\n+    .init             = NULL,\n+    .batch_submit     = NULL,\n+    .conn_add         = dummy_conn_add,\n+    .conn_del         = dummy_conn_del,\n+    .conn_update      = dummy_conn_update,\n+    .conn_established = dummy_conn_established,\n+    .can_offload      = dummy_can_offload,\n+    .flush            = dummy_flush,\n+};\n+\n+/* -----------------------------------------------------------------------\n+ * Public API\n+ * ----------------------------------------------------------------------- */\n+\n+void\n+ct_offload_dummy_register(void)\n+{\n+    ct_offload_dummy_reset_counters();\n+    ct_offload_register(&ct_offload_dummy_class);\n+}\n+\n+void\n+ct_offload_dummy_unregister(void)\n+{\n+    /* Flush any leftover entries before unregistering so we do not leak. */\n+    dummy_flush();\n+    ct_offload_unregister(&ct_offload_dummy_class);\n+}\n+\n+unsigned int\n+ct_offload_dummy_n_added(void)\n+{\n+    return n_added;\n+}\n+\n+unsigned int\n+ct_offload_dummy_n_deleted(void)\n+{\n+    return n_deleted;\n+}\n+\n+unsigned int\n+ct_offload_dummy_n_updated(void)\n+{\n+    return n_updated;\n+}\n+\n+unsigned int\n+ct_offload_dummy_n_established(void)\n+{\n+    return n_established;\n+}\n+\n+void\n+ct_offload_dummy_reset_counters(void)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    n_added       = 0;\n+    n_deleted     = 0;\n+    n_updated     = 0;\n+    n_established = 0;\n+    ovs_mutex_unlock(&dummy_mutex);\n+}\n+\n+bool\n+ct_offload_dummy_contains(const struct conn *conn)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    bool found = dummy_find__(conn) != NULL;\n+    ovs_mutex_unlock(&dummy_mutex);\n+    return found;\n+}\n+\n+/* Returns true if the dummy provider has seen both the forward-direction\n+ * input netdev (recorded at conn_add) and the reply-direction input netdev\n+ * (recorded at conn_established) for 'conn'. */\n+bool\n+ct_offload_dummy_is_bidirectional(const struct conn *conn)\n+{\n+    ovs_mutex_lock(&dummy_mutex);\n+    struct ct_dummy_entry *e = dummy_find__(conn);\n+    bool bidi = e && e->netdev_fwd_in && e->netdev_rev_in;\n+    ovs_mutex_unlock(&dummy_mutex);\n+    return bidi;\n+}\ndiff --git a/lib/ct-offload-dummy.h b/lib/ct-offload-dummy.h\nnew file mode 100644\nindex 0000000000..1e7ecfdb04\n--- /dev/null\n+++ b/lib/ct-offload-dummy.h\n@@ -0,0 +1,64 @@\n+/*\n+ * Copyright (c) 2026 Red Hat, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at:\n+ *\n+ *     http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+#ifndef CT_OFFLOAD_DUMMY_H\n+#define CT_OFFLOAD_DUMMY_H 1\n+\n+/* Dummy CT offload provider\n+ * =========================\n+ *\n+ * A software-only implementation of the ct_offload_class interface used for\n+ * unit testing.  It records every conn_add/conn_del/conn_update call and\n+ * exposes inspection helpers so tests can verify that the correct hooks are\n+ * reached without requiring any hardware.\n+ *\n+ * Typical usage:\n+ *\n+ *   ct_offload_dummy_register();   // activate the provider\n+ *   conntrack_execute(...);        // exercises conn_add\n+ *   ovs_assert(ct_offload_dummy_n_added() == 1);\n+ *   conntrack_flush(...);          // exercises conn_del\n+ *   ovs_assert(ct_offload_dummy_n_deleted() == 1);\n+ *   ct_offload_dummy_unregister(); // tear down after test\n+ */\n+\n+#include <stdbool.h>\n+\n+struct conn;\n+\n+/* Register (or unregister) the dummy provider.\n+ *\n+ * ct_offload_dummy_register() also marks CT offload as \"enabled\" within the\n+ * dummy so that the guards in conntrack.c fire even without hardware offload\n+ * being configured globally.  Call ct_offload_dummy_unregister() to undo. */\n+void ct_offload_dummy_register(void);\n+void ct_offload_dummy_unregister(void);\n+\n+/* Counters.  Initialized to zero and can be reset. */\n+unsigned int ct_offload_dummy_n_added(void);\n+unsigned int ct_offload_dummy_n_deleted(void);\n+unsigned int ct_offload_dummy_n_updated(void);\n+unsigned int ct_offload_dummy_n_established(void);\n+\n+/* Reset all counters without changing registered state. */\n+void ct_offload_dummy_reset_counters(void);\n+\n+/* Returns true if 'conn' is currently tracked by the dummy (was added but\n+ * not yet deleted or flushed). */\n+bool ct_offload_dummy_contains(const struct conn *conn);\n+bool ct_offload_dummy_is_bidirectional(const struct conn *conn);\n+\n+#endif /* CT_OFFLOAD_DUMMY_H */\ndiff --git a/lib/ct-offload.c b/lib/ct-offload.c\nindex b777801ab9..707e71c03f 100644\n--- a/lib/ct-offload.c\n+++ b/lib/ct-offload.c\n@@ -57,6 +57,10 @@ static struct ovs_list  ct_offload_classes\n  * registered dpif offload class will be activated by ct_offload_module_init().\n  */\n static const struct ct_offload_class *base_ct_offload_classes[] = {\n+    /* Dummy provider: activated whenever the \"dummy\" dpif offload class is\n+     * registered (hw-offload=true with a dummy datapath).  Also used directly\n+     * by unit tests via ct_offload_dummy_register(). */\n+    &ct_offload_dummy_class,\n };\n \n \n@@ -166,6 +170,12 @@ ct_offload_module_init(void)\n     }\n }\n \n+static bool ct_offload_forced = false;\n+void ct_offload_force_enable(bool value)\n+{\n+    ct_offload_forced = value;\n+}\n+\n /* ct_offload_enabled() - returns true when hardware offload is active.\n  *\n  * Delegates to dpif_offload_enabled() so CT offload shares the same global\n@@ -173,7 +183,7 @@ ct_offload_module_init(void)\n bool\n ct_offload_enabled(void)\n {\n-    return dpif_offload_enabled();\n+    return dpif_offload_enabled() || ct_offload_forced;\n }\n \n /* ct_offload_set_global_cfg() - configure CT offload from OVSDB.\ndiff --git a/lib/ct-offload.h b/lib/ct-offload.h\nindex fe4ecd33b8..3836852703 100644\n--- a/lib/ct-offload.h\n+++ b/lib/ct-offload.h\n@@ -83,6 +83,12 @@ struct ct_offload_class {\n     void (*flush)(void);\n };\n \n+/* Dummy (software-only) CT offload provider, always compiled in.\n+ * Registered automatically when the \"dummy\" dpif offload class is active\n+ * (e.g. hw-offload=true with a dummy datapath), and available directly for\n+ * unit tests via ct_offload_dummy_register() in ct-offload-dummy.h. */\n+extern const struct ct_offload_class ct_offload_dummy_class;\n+\n /* Register/unregister a provider.  Must be called at module init, before\n  * any connections are created. */\n int  ct_offload_register(const struct ct_offload_class *);\n@@ -100,6 +106,10 @@ void ct_offload_set_global_cfg(const struct ovsrec_open_vswitch *);\n  */\n bool ct_offload_enabled(void);\n \n+/* Used for testing.  Forces an additional parameter for the offload enable\n+ * check.  Set to 'true' to always enable the offloads. */\n+void ct_offload_force_enable(bool);\n+\n /* Per-connection offload API that dispatches to all registered providers. */\n int       ct_offload_conn_add(const struct ct_offload_ctx *);\n void      ct_offload_conn_del(const struct ct_offload_ctx *);\ndiff --git a/tests/dpif-netdev.at b/tests/dpif-netdev.at\nindex 2311979709..ae890f72fb 100644\n--- a/tests/dpif-netdev.at\n+++ b/tests/dpif-netdev.at\n@@ -50,6 +50,14 @@ filter_hw_packet_netdev_dummy () {\n         | sort | uniq\n }\n \n+filter_ct_offload_dummy_conn_add () {\n+    grep 'ct_offload_dummy.*conn add:' | sed 's/.*|DBG|//' | sort | uniq\n+}\n+\n+filter_ct_offload_dummy_conn_del () {\n+    grep 'ct_offload_dummy.*conn del:' | sed 's/.*|DBG|//' | sort | uniq\n+}\n+\n filter_flow_dump () {\n     grep 'flow_dump ' | sed '\n         s/.*flow_dump //\n@@ -3709,3 +3717,67 @@ AT_CHECK_UNQUOTED([tail -n 1 p1.pcap.txt], [0], [${good_expected_v6}\n \n OVS_VSWITCHD_STOP\n AT_CLEANUP\n+\n+dnl Test that the CT offload dummy provider receives conn_add and conn_del\n+dnl callbacks when packets are processed through a conntrack commit flow on a\n+dnl dummy datapath with hw-offload enabled.\n+AT_SETUP([dpif-netdev - conntrack offload dummy])\n+AT_KEYWORDS([conntrack offload])\n+OVS_VSWITCHD_START(\n+  [add-port br0 p1 -- \\\n+   set interface p1 type=dummy ofport_request=1 \\\n+                    options:pstream=punix:$OVS_RUNDIR/p1.sock \\\n+                    options:ifindex=1100 -- \\\n+   add-port br0 p2 -- \\\n+   set interface p2 type=dummy ofport_request=2 \\\n+                    options:pstream=punix:$OVS_RUNDIR/p2.sock \\\n+                    options:ifindex=1101 -- \\\n+   set bridge br0 datapath-type=dummy \\\n+                  other-config:datapath-id=1234 fail-mode=secure], [], [], [])\n+\n+dnl Enable debug logging for the dpif offload and CT offload dummy modules so\n+dnl the test can detect hook calls via log grep.\n+AT_CHECK([ovs-appctl vlog/set dpif_offload_dummy:file:dbg ct_offload_dummy:file:dbg])\n+\n+dnl Enable hardware offload — this registers the \"dummy\" dpif offload class\n+dnl and automatically activates the CT offload dummy provider.\n+AT_CHECK([ovs-vsctl set Open_vSwitch . other_config:hw-offload=true])\n+OVS_WAIT_UNTIL([grep \"Flow HW offload is enabled\" ovs-vswitchd.log])\n+\n+dnl Add a two-table conntrack flow:\n+dnl  table 0: untracked packets → ct(commit) recirculate to table 1\n+dnl  table 1: tracked packets   → output on p2\n+AT_CHECK([ovs-ofctl add-flow br0 \\\n+  'table=0,priority=100,in_port=p1,ip,ct_state=-trk,actions=ct(commit,table=1)'])\n+AT_CHECK([ovs-ofctl add-flow br0 \\\n+  'table=1,priority=100,in_port=p1,ip,ct_state=+trk,actions=output:p2'])\n+\n+dnl Compose and inject a UDP packet on p1.  The first packet misses the\n+dnl datapath, causes an upcall, executes ct(commit) to create a conntrack\n+dnl entry, and triggers the ct_offload_dummy conn_add callback.\n+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\"\n+pkt=$(ovs-ofctl compose-packet --bare \"${flow_s}\")\n+AT_CHECK([ovs-appctl netdev-dummy/receive p1 \"${pkt}\"])\n+\n+dnl Wait for the CT offload dummy conn_add hook to fire.\n+OVS_WAIT_UNTIL([grep 'ct_offload_dummy.*conn add:' ovs-vswitchd.log])\n+\n+dnl Verify exactly one connection was added.\n+AT_CHECK([filter_ct_offload_dummy_conn_add < ovs-vswitchd.log | wc -l | tr -d ' '],\n+  [0], [1\n+])\n+\n+dnl Flush all conntrack entries — conn_clean is called for every tracked\n+dnl connection, which invokes ct_offload_conn_del on each registered provider.\n+AT_CHECK([ovs-appctl dpctl/flush-conntrack])\n+\n+dnl Wait for the CT offload dummy conn_del hook to fire.\n+OVS_WAIT_UNTIL([grep 'ct_offload_dummy.*conn del:' ovs-vswitchd.log])\n+\n+dnl Verify exactly one connection was deleted.\n+AT_CHECK([filter_ct_offload_dummy_conn_del < ovs-vswitchd.log | wc -l | tr -d ' '],\n+  [0], [1\n+])\n+\n+OVS_VSWITCHD_STOP\n+AT_CLEANUP\ndiff --git a/tests/library.at b/tests/library.at\nindex 6c5b55f045..2d5b02f75b 100644\n--- a/tests/library.at\n+++ b/tests/library.at\n@@ -325,3 +325,39 @@ AT_KEYWORDS([conntrack])\n AT_CHECK([ovstest test-conntrack private-destructor], [0], [.\n ])\n AT_CLEANUP\n+\n+AT_SETUP([conntrack offload dummy - conn add hook])\n+AT_KEYWORDS([conntrack offload])\n+AT_CHECK([ovstest test-conntrack offload-conn-add], [0], [.\n+])\n+AT_CLEANUP\n+\n+AT_SETUP([conntrack offload dummy - conn del hook])\n+AT_KEYWORDS([conntrack offload])\n+AT_CHECK([ovstest test-conntrack offload-conn-del], [0], [.\n+])\n+AT_CLEANUP\n+\n+AT_SETUP([conntrack offload dummy - conn update hook])\n+AT_KEYWORDS([conntrack offload])\n+AT_CHECK([ovstest test-conntrack offload-conn-update], [0], [.\n+])\n+AT_CLEANUP\n+\n+AT_SETUP([conntrack offload dummy - multiple connections])\n+AT_KEYWORDS([conntrack offload])\n+AT_CHECK([ovstest test-conntrack offload-multi-conn], [0], [.\n+])\n+AT_CLEANUP\n+\n+AT_SETUP([conntrack offload dummy - conn established hook (end-to-end)])\n+AT_KEYWORDS([conntrack offload])\n+AT_CHECK([ovstest test-conntrack offload-conn-established], [0], [.\n+])\n+AT_CLEANUP\n+\n+AT_SETUP([conntrack offload dummy - conn established fires exactly once (API)])\n+AT_KEYWORDS([conntrack offload])\n+AT_CHECK([ovstest test-conntrack offload-conn-established-api], [0], [.\n+])\n+AT_CLEANUP\ndiff --git a/tests/test-conntrack.c b/tests/test-conntrack.c\nindex 3c409b373b..86f1f36d3f 100644\n--- a/tests/test-conntrack.c\n+++ b/tests/test-conntrack.c\n@@ -17,6 +17,8 @@\n #include <config.h>\n #include \"conntrack.h\"\n #include \"conntrack-private.h\"\n+#include \"ct-offload.h\"\n+#include \"ct-offload-dummy.h\"\n \n #include \"dp-packet.h\"\n #include \"fatal-signal.h\"\n@@ -691,6 +693,304 @@ test_private_destructor(struct ovs_cmdl_context *ctx OVS_UNUSED)\n     printf(\".\\n\");\n }\n \n+\f\n+/* ===========================================================================\n+ * CT offload dummy provider tests\n+ *\n+ * These tests exercise the ct_offload provider API directly without going\n+ * through conntrack_execute.  The offload global-enable flag is deliberately\n+ * not set here: the unit tests own the provider list and call the API\n+ * functions directly.  End-to-end enablement (hw-offload=true via DB config)\n+ * is covered by the dpif-netdev integration test.\n+ *\n+ * Each test must be run as a separate ovstest invocation so that the\n+ * process-global provider list starts empty.\n+ * ===========================================================================\n+ */\n+\n+/* The dummy only compares pointer addresses and never dereferences them, so a\n+ * small integer cast is sufficient. */\n+#define FAKE_CONN(n)   ((struct conn *)(uintptr_t)(n))\n+#define FAKE_NETDEV(n) ((struct netdev *)(uintptr_t)(n))\n+\n+/* Test: offload-conn-add\n+ * ----------------------\n+ * Register the dummy provider, call ct_offload_conn_add() directly, and\n+ * verify that the conn_add hook was invoked and the connection is tracked.\n+ */\n+static void\n+test_offload_conn_add(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+    ct_offload_force_enable(true);\n+    ct_offload_dummy_register();\n+\n+    struct conn *fake = FAKE_CONN(1);\n+    struct ct_offload_ctx offload_ctx = {\n+        .conn = fake, .netdev_in = NULL,\n+    };\n+    ct_offload_conn_add(&offload_ctx);\n+\n+    ovs_assert(ct_offload_dummy_n_added() == 1);\n+    ovs_assert(ct_offload_dummy_contains(fake));\n+\n+    ct_offload_dummy_unregister();\n+    ct_offload_force_enable(false);\n+    printf(\".\\n\");\n+}\n+\n+/* Test: offload-conn-del\n+ * ----------------------\n+ * Register the dummy, add then delete a connection via the API, and verify\n+ * that conn_del was called and the connection is no longer tracked.\n+ */\n+static void\n+test_offload_conn_del(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+    ct_offload_force_enable(true);\n+    ct_offload_dummy_register();\n+\n+    struct conn *fake = FAKE_CONN(1);\n+    struct ct_offload_ctx offload_ctx = {\n+        .conn = fake, .netdev_in = NULL,\n+    };\n+\n+    ct_offload_conn_add(&offload_ctx);\n+    ovs_assert(ct_offload_dummy_n_added() == 1);\n+\n+    ct_offload_conn_del(&offload_ctx);\n+    ovs_assert(ct_offload_dummy_n_deleted() == 1);\n+    ovs_assert(!ct_offload_dummy_contains(fake));\n+\n+    ct_offload_dummy_unregister();\n+    ct_offload_force_enable(false);\n+    printf(\".\\n\");\n+}\n+\n+/* Test: offload-conn-update\n+ * -------------------------\n+ * Register the dummy, add a connection, call ct_offload_conn_update()\n+ * directly, and verify that a non-zero last-used timestamp is returned.\n+ */\n+static void\n+test_offload_conn_update(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+    ct_offload_force_enable(true);\n+    ct_offload_dummy_register();\n+\n+    struct conn *fake = FAKE_CONN(1);\n+    struct ct_offload_ctx offload_ctx = {\n+        .conn = fake, .netdev_in = NULL,\n+    };\n+\n+    ct_offload_conn_add(&offload_ctx);\n+\n+    long long ts = ct_offload_conn_update(&offload_ctx);\n+    ovs_assert(ts != 0);\n+    ovs_assert(ct_offload_dummy_n_updated() == 1);\n+\n+    ct_offload_dummy_unregister();\n+    ct_offload_force_enable(false);\n+    printf(\".\\n\");\n+}\n+\n+/* Test: offload-multi-conn\n+ * ------------------------\n+ * Register the dummy, add N connections via the API, and verify that each\n+ * is tracked independently.\n+ */\n+#define OFFLOAD_MULTI_N 4\n+\n+static void\n+test_offload_multi_conn(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+    ct_offload_force_enable(true);\n+    ct_offload_dummy_register();\n+\n+    for (unsigned i = 1; i <= OFFLOAD_MULTI_N; i++) {\n+        struct ct_offload_ctx offload_ctx = {\n+            .conn = FAKE_CONN(i), .netdev_in = NULL,\n+        };\n+        ct_offload_conn_add(&offload_ctx);\n+    }\n+\n+    ovs_assert(ct_offload_dummy_n_added() == OFFLOAD_MULTI_N);\n+    for (unsigned i = 1; i <= OFFLOAD_MULTI_N; i++) {\n+        ovs_assert(ct_offload_dummy_contains(FAKE_CONN(i)));\n+    }\n+\n+    ct_offload_dummy_unregister();\n+    ct_offload_force_enable(false);\n+    printf(\".\\n\");\n+}\n+\n+/* Test: offload-conn-established\n+ * --------------------------------\n+ * Drive a TCP three-way handshake through conntrack_execute() with the dummy\n+ * offload provider registered.  Verifies three properties:\n+ *\n+ *  (a) conn_add fires on the SYN (new connection created, forward netdev\n+ *      recorded); conn_established does NOT fire yet.\n+ *  (b) conn_established fires exactly once on the first ESTABLISHED reply\n+ *      (SYN-ACK), recording the reply-direction netdev so that the dummy\n+ *      entry is fully bidirectional.\n+ *  (c) A subsequent reply packet (ACK) does NOT cause a second\n+ *      conn_established call the \"exactly once\" guarantee holds.\n+ *\n+ * ct_offload_dummy_register() calls ct_offload_force_enable(true), which\n+ * makes ct_offload_enabled() return true so the guards in conntrack.c fire\n+ * without a real hardware offload backend.\n+ */\n+static void\n+test_offload_conn_established(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+    /* Allocate the per-connection private slot before registering so that the\n+     * ADD/ESTABLISHED state transitions are tracked in conn->private[].\n+     * The simple FAKE_CONN tests skip this step because they do not exercise\n+     * the private-slot code path. */\n+    ct_offload_alloc_private_slot();\n+    ct_offload_force_enable(true);\n+    ct_offload_dummy_register();\n+\n+    struct conntrack *lct = conntrack_init();\n+    /* Disable TCP sequence-number checking so test packets with seq=0 are\n+     * accepted by the state machine. */\n+    conntrack_set_tcp_seq_chk(lct, false);\n+\n+    long long now = time_msec();\n+\n+    struct eth_addr eth_a = ETH_ADDR_C(00, 00, 00, 00, 00, 01);\n+    struct eth_addr eth_b = ETH_ADDR_C(00, 00, 00, 00, 00, 02);\n+    ovs_be32 ip_a = inet_addr(\"10.0.0.1\");\n+    ovs_be32 ip_b = inet_addr(\"10.0.0.2\");\n+    uint16_t sport = 1234;\n+    uint16_t dport = 80;\n+\n+    /* --- (a) SYN: forward direction, creates the connection entry. --- */\n+    struct dp_packet *syn = build_eth_ip_packet(NULL, eth_a, eth_b,\n+                                                ip_a, ip_b,\n+                                                IPPROTO_TCP, 0);\n+    build_tcp_packet(syn, sport, dport, TCP_SYN, NULL, 0);\n+\n+    struct dp_packet_batch syn_batch;\n+    dp_packet_batch_init_packet(&syn_batch, syn);\n+    conntrack_execute(lct, &syn_batch, htons(ETH_TYPE_IP), false, true, 0,\n+                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(1));\n+\n+    /* conn_add must have fired; conn_established must not have. */\n+    ovs_assert(ct_offload_dummy_n_added() == 1);\n+    ovs_assert(ct_offload_dummy_n_established() == 0);\n+\n+    /* The packet carries the conn pointer after commit. */\n+    struct conn *conn = syn->md.conn;\n+    ovs_assert(conn != NULL);\n+    ovs_assert(ct_offload_conn_is_offloaded(conn));\n+    ovs_assert(!ct_offload_conn_is_established(conn));\n+\n+    dp_packet_delete_batch(&syn_batch, true);\n+\n+    /* --- (b) SYN-ACK: reply direction, transitions to ESTABLISHED. --- */\n+    struct dp_packet *synack = build_eth_ip_packet(NULL, eth_b, eth_a,\n+                                                   ip_b, ip_a,\n+                                                   IPPROTO_TCP, 0);\n+    build_tcp_packet(synack, dport, sport, TCP_SYN | TCP_ACK, NULL, 0);\n+\n+    struct dp_packet_batch synack_batch;\n+    dp_packet_batch_init_packet(&synack_batch, synack);\n+    conntrack_execute(lct, &synack_batch, htons(ETH_TYPE_IP), false, true, 0,\n+                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(2));\n+\n+    /* conn_established fires exactly once on the first ESTABLISHED reply. */\n+    ovs_assert(ct_offload_dummy_n_established() == 1);\n+    ovs_assert(ct_offload_conn_is_established(conn));\n+    /* Both netdev pointers are now known: the entry is fully bidirectional. */\n+    ovs_assert(ct_offload_dummy_is_bidirectional(conn));\n+\n+    dp_packet_delete_batch(&synack_batch, true);\n+\n+    /* --- (c) ACK: another reply packet must NOT trigger conn_established\n+     *             again.  The private-slot guard enforces this. --- */\n+    struct dp_packet *ack = build_eth_ip_packet(NULL, eth_b, eth_a,\n+                                                ip_b, ip_a,\n+                                                IPPROTO_TCP, 0);\n+    build_tcp_packet(ack, dport, sport, TCP_ACK, NULL, 0);\n+\n+    struct dp_packet_batch ack_batch;\n+    dp_packet_batch_init_packet(&ack_batch, ack);\n+    conntrack_execute(lct, &ack_batch, htons(ETH_TYPE_IP), false, true, 0,\n+                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(2));\n+\n+    /* Counter must still be 1 - conn_established must not have fired again. */\n+    ovs_assert(ct_offload_dummy_n_established() == 1);\n+\n+    dp_packet_delete_batch(&ack_batch, true);\n+\n+    conntrack_destroy(lct);\n+    ct_offload_dummy_unregister();\n+    ct_offload_force_enable(false);\n+    printf(\".\\n\");\n+}\n+\n+/* Test: offload-conn-established-api\n+ * ------------------------------------\n+ * Exercise ct_offload_conn_established() directly (not through\n+ * conntrack_execute) to verify that the \"exactly once\" guarantee in the\n+ * dispatch layer holds independently of the conntrack state machine.\n+ *\n+ * Sequence:\n+ *   1. conn_add() - transitions the private slot to CT_OFFLOAD_STATE_ADDED.\n+ *   2. conn_established() - should dispatch to the provider exactly once and\n+ *      advance the slot to CT_OFFLOAD_STATE_EST.\n+ *   3. A second conn_established() call with the same conn must be a no-op\n+ *      (provider not called again, counter unchanged).\n+ */\n+static void\n+test_offload_conn_established_api(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+    ct_offload_alloc_private_slot();\n+    ct_offload_force_enable(true);\n+    ct_offload_dummy_register();\n+\n+    /* We need a real conn with a live private-data slot, so spin up a minimal\n+     * conntrack instance and commit one UDP packet to get a conn. */\n+    struct conntrack *lct = conntrack_init();\n+    long long now = time_msec();\n+\n+    ovs_be16 dl_type;\n+    struct dp_packet *pkt = build_packet(1, 2, &dl_type);\n+    struct dp_packet_batch batch;\n+    dp_packet_batch_init_packet(&batch, pkt);\n+    conntrack_execute(lct, &batch, dl_type, false, true, 0,\n+                      NULL, NULL, NULL, NULL, now, 0, FAKE_NETDEV(1));\n+    struct conn *conn = pkt->md.conn;\n+    ovs_assert(conn != NULL);\n+    dp_packet_delete_batch(&batch, true);\n+\n+    /* conn_add should have fired (via conntrack_execute). */\n+    ovs_assert(ct_offload_dummy_n_added() == 1);\n+    ovs_assert(ct_offload_dummy_n_established() == 0);\n+    ovs_assert(ct_offload_conn_is_offloaded(conn));\n+    ovs_assert(!ct_offload_conn_is_established(conn));\n+\n+    /* First call: must dispatch to the provider. */\n+    struct ct_offload_ctx ctx1 = {\n+        .conn = conn, .netdev_in = FAKE_NETDEV(2),\n+    };\n+    ct_offload_conn_established(&ctx1);\n+    ovs_assert(ct_offload_dummy_n_established() == 1);\n+    ovs_assert(ct_offload_conn_is_established(conn));\n+    ovs_assert(ct_offload_dummy_is_bidirectional(conn));\n+\n+    /* Second call with the same conn: must be a no-op. */\n+    ct_offload_conn_established(&ctx1);\n+\n+    ovs_assert(ct_offload_dummy_n_established() == 1);  /* unchanged */\n+\n+    conntrack_destroy(lct);\n+    ct_offload_dummy_unregister();\n+    ct_offload_force_enable(false);\n+    printf(\".\\n\");\n+}\n+\n \f\n static const struct ovs_cmdl_command commands[] = {\n     /* Connection tracker tests. */\n@@ -725,6 +1025,20 @@ static const struct ovs_cmdl_command commands[] = {\n      test_private_id_exhaustion, OVS_RO},\n     {\"private-destructor\", \"\", 0, 0,\n      test_private_destructor, OVS_RO},\n+    /* CT offload dummy provider tests.\n+     * Each must be run as a separate ovstest invocation. */\n+    {\"offload-conn-add\", \"\", 0, 0,\n+     test_offload_conn_add, OVS_RO},\n+    {\"offload-conn-del\", \"\", 0, 0,\n+     test_offload_conn_del, OVS_RO},\n+    {\"offload-conn-update\", \"\", 0, 0,\n+     test_offload_conn_update, OVS_RO},\n+    {\"offload-multi-conn\", \"\", 0, 0,\n+     test_offload_multi_conn, OVS_RO},\n+    {\"offload-conn-established\", \"\", 0, 0,\n+     test_offload_conn_established, OVS_RO},\n+    {\"offload-conn-established-api\", \"\", 0, 0,\n+     test_offload_conn_established_api, OVS_RO},\n \n     {NULL, NULL, 0, 0, NULL, OVS_RO},\n };\n",
    "prefixes": [
        "ovs-dev",
        "RFC",
        "11/12"
    ]
}