Attention is currently required from: Arne Schwabe.

Hello Arne Schwabe,

I'd like you to do a code review.
Please visit

    https://gerrit.openvpn.net/c/openvpn/+/1981?usp=email

to review the following change.


Change subject: dco: detect local kernel capabilities through netlink policies
......................................................................

dco: detect local kernel capabilities through netlink policies

The ovpn module lives in the kernel, so the attributes it accepts may
differ from what we were built against. CTRL_CMD_GETPOLICY dumps the
validation policy of a family: which attributes each command takes, of
what type, and for a nested one the sub-policy that validates it.

Add a helper to query it, wrapped in dco_probe_capabilities() so the
rest of the tree never talks to netlink itself. Answering a requirement
means following a chain - which policy validates the command, where its
nested attribute points if it has one, what type the target attribute
has there - and CTRL_ATTR_OP narrows the dump enough to collect all
three in one pass.

The answer has three values rather than two: without "could not ask", a
netlink error would look the same as an old kernel.

No capability is defined yet, so the bitmap comes back empty; the rows
that fill it arrive with the features that need them. The unit test
drives the pass and the decision separately, on dumps built by hand.

Change-Id: I0051effe7f0636e30557d9ce92c50c458002f9e6
Signed-off-by: Gianmarco De Gregori <[email protected]>
---
M configure.ac
M src/openvpn/dco.h
M src/openvpn/dco_freebsd.c
M src/openvpn/dco_linux.c
M src/openvpn/dco_win.c
M tests/unit_tests/openvpn/Makefile.am
A tests/unit_tests/openvpn/mock_dco_dependencies.c
A tests/unit_tests/openvpn/test_dco.c
8 files changed, 862 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/81/1981/1

diff --git a/configure.ac b/configure.ac
index 56109af..0758ad9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1313,6 +1313,7 @@
 AC_SUBST([PLUGIN_AUTH_PAM_LIBS])

 AM_CONDITIONAL([WIN32], [test "${WIN32}" = "yes"])
+AM_CONDITIONAL([DCO_UNIT_TESTS], [test "${enable_dco}" != "no"])
 AM_CONDITIONAL([GIT_CHECKOUT], [test "${GIT_CHECKOUT}" = "yes"])
 AM_CONDITIONAL([ENABLE_PLUGIN_AUTH_PAM], [test "${enable_plugin_auth_pam}" = 
"yes"])
 AM_CONDITIONAL([ENABLE_PLUGIN_DOWN_ROOT], [test "${enable_plugin_down_root}" = 
"yes"])
diff --git a/src/openvpn/dco.h b/src/openvpn/dco.h
index 7ef75fee..d074257 100644
--- a/src/openvpn/dco.h
+++ b/src/openvpn/dco.h
@@ -254,6 +254,17 @@
  */
 bool
 dco_supports_epoch_data(struct context *c);
+
+/**
+ * Return a bitmap of DCO_CAP_* bits describing the negotiable features the
+ * local kernel supports. Everything DCO does without negotiating is available
+ * regardless. Resolved once and reused; platforms without capability probing
+ * return 0.
+ *
+ * @return  bitmask of DCO_CAP_* flags; 0 both when the kernel negotiates none
+ *          of them and when it could not be asked
+ */
+unsigned int dco_probe_capabilities(void);
 #else  /* if defined(ENABLE_DCO) */

 typedef void *dco_context_t;
@@ -388,5 +399,11 @@
 {
     return false;
 }
+
+static inline unsigned int
+dco_probe_capabilities(void)
+{
+    return 0;
+}
 #endif /* defined(ENABLE_DCO) */
 #endif /* ifndef DCO_H */
diff --git a/src/openvpn/dco_freebsd.c b/src/openvpn/dco_freebsd.c
index 87eea55..9ace240 100644
--- a/src/openvpn/dco_freebsd.c
+++ b/src/openvpn/dco_freebsd.c
@@ -702,6 +702,12 @@
     return 0;
 }

+unsigned int
+dco_probe_capabilities(void)
+{
+    return 0;
+}
+
 bool
 dco_available(msglvl_t msglevel)
 {
diff --git a/src/openvpn/dco_linux.c b/src/openvpn/dco_linux.c
index a397fb8..eabb3ea 100644
--- a/src/openvpn/dco_linux.c
+++ b/src/openvpn/dco_linux.c
@@ -103,6 +103,308 @@
     return ret;
 }

+/*
+ * Netlink policy introspection: the ovpn module lives in the kernel, so the
+ * attributes it accepts may differ from what we were built against.
+ * CTRL_CMD_GETPOLICY says which attributes a command takes and of what type,
+ * and CTRL_ATTR_OP narrows the dump to a single command.
+ */
+
+/* one row per capability; a row on a command of its own costs one more dump */
+struct dco_capability_req
+{
+    unsigned int cap;
+    uint16_t cmd;
+    uint16_t nested_attr; /* 0 if target_attr is not nested */
+    uint16_t target_attr;
+    int expected_type;
+};
+
+/** the outcome of resolving one capability requirement */
+enum ovpn_cap_status
+{
+    OVPN_CAP_UNKNOWN,     /**< the question could not be asked */
+    OVPN_CAP_SUPPORTED,   /**< the kernel accepts the attribute */
+    OVPN_CAP_UNSUPPORTED, /**< the kernel answered, and does not accept it */
+};
+
+struct ovpn_cap_query
+{
+    const struct dco_capability_req *req;
+
+    int cmd_policy; /* policy validating the doit of req->cmd, -1 until seen */
+    int sub_policy; /* where nested_attr points, -1 until seen */
+    int type;       /* type of target_attr in the sub-policy, -1 until seen */
+
+    /* policies walked past before their role was known */
+    int unclassified;
+};
+
+/* What the local kernel supports. It cannot change while we run, so the first
+ * caller resolves it and the rest just read it. */
+static unsigned int dco_caps = 0;
+static bool dco_caps_resolved = false;
+
+/*
+ * The command map arrives before any policy and a nested policy after the one
+ * nesting it, so one pass suffices. That order is not promised, so policies
+ * walked past unclassified are counted: an incomplete chain then means we 
could
+ * not tell, not that the attribute is missing.
+ */
+static int
+ovpn_cap_query_cb(struct nl_msg *msg, void *arg)
+{
+    struct ovpn_cap_query *query = arg;
+    struct nlattr *tb[CTRL_ATTR_MAX + 1];
+    const struct genlmsghdr *ghdr = nlmsg_data(nlmsg_hdr(msg));
+
+    /* one message of the dump: it carries either the command map or a slice of
+     * one policy, and fills in whichever link of the chain it happens to hold 
*/
+
+    if (nla_parse(tb, CTRL_ATTR_MAX, genlmsg_attrdata(ghdr, 0), 
genlmsg_attrlen(ghdr, 0), NULL))
+    {
+        return NL_SKIP;
+    }
+
+    /* CTRL_ATTR_OP_POLICY: which policy validates the doit of each command */
+    if (tb[CTRL_ATTR_OP_POLICY])
+    {
+        const struct nlattr *nla_cmd;
+        int rem_cmd;
+
+        nla_for_each_nested(nla_cmd, tb[CTRL_ATTR_OP_POLICY], rem_cmd)
+        {
+            struct nlattr *tb_cmd[CTRL_ATTR_POLICY_DUMP_MAX + 1];
+
+            /* the dump is filtered, but do not rely on that */
+            if (nla_type(nla_cmd) != query->req->cmd)
+            {
+                continue;
+            }
+
+            if (nla_parse(tb_cmd, CTRL_ATTR_POLICY_DUMP_MAX, 
nla_data(nla_cmd), nla_len(nla_cmd),
+                          NULL))
+            {
+                continue;
+            }
+
+            if (tb_cmd[CTRL_ATTR_POLICY_DO])
+            {
+                query->cmd_policy = 
(int)nla_get_u32(tb_cmd[CTRL_ATTR_POLICY_DO]);
+
+                /* a requirement with no container looks for its target in the
+                 * command's own policy, so the search ends where it starts */
+                if (!query->req->nested_attr)
+                {
+                    query->sub_policy = query->cmd_policy;
+                }
+            }
+        }
+    }
+
+    /* CTRL_ATTR_POLICY: which attributes each policy accepts */
+    if (!tb[CTRL_ATTR_POLICY])
+    {
+        return NL_OK;
+    }
+
+    const struct nlattr *nla_policy;
+    int rem_policy;
+
+    nla_for_each_nested(nla_policy, tb[CTRL_ATTR_POLICY], rem_policy)
+    {
+        struct nlattr *nla_attr;
+        int rem_attr;
+        int policy_id = nla_type(nla_policy);
+
+        /* only the two policies of the chain can hold anything we need. One we
+         * cannot place yet is counted, because skipping it may be why the
+         * chain ends up incomplete */
+        if (policy_id != query->cmd_policy && policy_id != query->sub_policy)
+        {
+            if (query->sub_policy < 0)
+            {
+                query->unclassified++;
+            }
+            continue;
+        }
+
+        nla_for_each_nested(nla_attr, nla_policy, rem_attr)
+        {
+            struct nlattr *tb_attr[NL_POLICY_TYPE_ATTR_MAX + 1];
+            int attr_id = nla_type(nla_attr);
+
+            if (attr_id != query->req->nested_attr && attr_id != 
query->req->target_attr)
+            {
+                continue;
+            }
+
+            if (nla_parse_nested(tb_attr, NL_POLICY_TYPE_ATTR_MAX, nla_attr, 
NULL))
+            {
+                continue;
+            }
+
+            /* An attribute id only means something inside its own policy, so
+             * each link is read where it belongs. First link: the nested
+             * attribute of the command's policy names the sub-policy. */
+            if (policy_id == query->cmd_policy && attr_id == 
query->req->nested_attr
+                && tb_attr[NL_POLICY_TYPE_ATTR_POLICY_IDX])
+            {
+                query->sub_policy = 
(int)nla_get_u32(tb_attr[NL_POLICY_TYPE_ATTR_POLICY_IDX]);
+            }
+
+            /* Second link: the type the target has in that sub-policy. */
+            if (policy_id == query->sub_policy && attr_id == 
query->req->target_attr
+                && tb_attr[NL_POLICY_TYPE_ATTR_TYPE])
+            {
+                query->type = 
(int)nla_get_u32(tb_attr[NL_POLICY_TYPE_ATTR_TYPE]);
+            }
+        }
+    }
+
+    return NL_OK;
+}
+
+/* Dump the policy of req->cmd and let the callback pick the chain out of it.
+ * Returns false if we could not ask at all, with the netlink error in *perr. 
*/
+static bool
+ovpn_cap_ask(const struct dco_capability_req *req, struct ovpn_cap_query 
*query, int *perr)
+{
+    struct nl_msg *nl_msg = NULL;
+    int ret = 0;
+
+    struct nl_sock *nl_sock = nl_socket_alloc();
+    if (!nl_sock)
+    {
+        *perr = -NLE_NOMEM;
+        return false;
+    }
+
+    ret = genl_connect(nl_sock);
+    if (ret < 0)
+    {
+        goto out;
+    }
+    set_cloexec(nl_socket_get_fd(nl_sock));
+
+    nl_msg = nlmsg_alloc();
+    if (!nl_msg)
+    {
+        ret = -NLE_NOMEM;
+        goto out;
+    }
+
+    /* nlctrl has a fixed family id, so there is nothing to resolve first */
+    if (!genlmsg_put(nl_msg, NL_AUTO_PORT, NL_AUTO_SEQ, GENL_ID_CTRL, 0,
+                     NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST, 
CTRL_CMD_GETPOLICY, 0))
+    {
+        ret = -NLE_MSGSIZE;
+        goto out;
+    }
+
+    ret = nla_put_string(nl_msg, CTRL_ATTR_FAMILY_NAME, OVPN_FAMILY_NAME);
+    if (ret < 0)
+    {
+        goto out;
+    }
+
+    ret = nla_put_u32(nl_msg, CTRL_ATTR_OP, req->cmd);
+    if (ret < 0)
+    {
+        goto out;
+    }
+
+    ret = nl_socket_modify_cb(nl_sock, NL_CB_VALID, NL_CB_CUSTOM, 
ovpn_cap_query_cb, query);
+    if (ret < 0)
+    {
+        goto out;
+    }
+
+    ret = nl_send_auto(nl_sock, nl_msg);
+    if (ret < 0)
+    {
+        goto out;
+    }
+
+    ret = nl_recvmsgs_default(nl_sock);
+
+out:
+    if (ret < 0)
+    {
+        *perr = ret;
+    }
+    nlmsg_free(nl_msg);
+    nl_socket_free(nl_sock);
+
+    return ret >= 0;
+}
+
+/*
+ * Decide whether the kernel supports the requirement, from the links the pass
+ * collected. A link the kernel never mentioned is a no; a chain we could not
+ * follow is not an answer either, and saying so is what keeps a transient
+ * failure from turning a capability off for good.
+ */
+static enum ovpn_cap_status
+ovpn_cap_resolve(const struct ovpn_cap_query *query)
+{
+    /* the dump never named the policy that validates the command */
+    if (query->cmd_policy < 0)
+    {
+        return OVPN_CAP_UNKNOWN;
+    }
+
+    /* a link is missing: a plain no, unless policies were walked past before
+     * they could be placed, in which case one of them may have held it */
+    if (query->sub_policy < 0 || query->type < 0)
+    {
+        return query->unclassified ? OVPN_CAP_UNKNOWN : OVPN_CAP_UNSUPPORTED;
+    }
+
+    /* the whole chain is there: a yes only if the attribute has the type we
+     * would be writing into it */
+    return (query->type == query->req->expected_type) ? OVPN_CAP_SUPPORTED : 
OVPN_CAP_UNSUPPORTED;
+}
+
+/* Resolve one capability requirement against the running kernel. */
+static enum ovpn_cap_status
+ovpn_cap_probe_one(const struct dco_capability_req *req, int *perr)
+{
+    struct ovpn_cap_query query = {
+        .req = req, .cmd_policy = -1, .sub_policy = -1, .type = -1, 
.unclassified = 0
+    };
+
+    if (!ovpn_cap_ask(req, &query, perr))
+    {
+        return OVPN_CAP_UNKNOWN;
+    }
+
+    return ovpn_cap_resolve(&query);
+}
+
+unsigned int
+dco_probe_capabilities(void)
+{
+    if (dco_caps_resolved)
+    {
+        return dco_caps;
+    }
+
+    /* no capability is defined yet: resolve one whose answer we already know 
*/
+    static const struct dco_capability_req self_test = { 0, OVPN_CMD_PEER_NEW, 
OVPN_A_PEER,
+                                                         OVPN_A_PEER_ID, 
NL_ATTR_TYPE_U32 };
+    int err = 0;
+    enum ovpn_cap_status status = ovpn_cap_probe_one(&self_test, &err);
+
+    msg(D_DCO_DEBUG, "%s: policy introspection self-test: %s (status %d, err 
%d)", __func__,
+        status == OVPN_CAP_SUPPORTED ? "ok" : "unexpected", (int)status, err);
+
+    dco_caps_resolved = true;
+    msg(D_DCO_DEBUG, "%s: DCO local capabilities: 0x%x", __func__, dco_caps);
+
+    return dco_caps;
+}
+
 static struct nl_msg *
 ovpn_dco_nlmsg_create(dco_context_t *dco, uint8_t cmd)
 {
@@ -417,6 +719,12 @@
 {
     dco->ovpn_dco_id = resolve_ovpn_netlink_id(M_FATAL);

+    /* nothing consumes the bitmap yet; resolve it here to exercise the path */
+    if (check_debug_level(D_DCO_DEBUG))
+    {
+        dco_probe_capabilities();
+    }
+
     dco->nl_sock = nl_socket_alloc();

     if (!dco->nl_sock)
diff --git a/src/openvpn/dco_win.c b/src/openvpn/dco_win.c
index c230a90..dbe8b78 100644
--- a/src/openvpn/dco_win.c
+++ b/src/openvpn/dco_win.c
@@ -606,6 +606,12 @@
     return 0;
 }

+unsigned int
+dco_probe_capabilities(void)
+{
+    return 0;
+}
+
 bool
 dco_available(msglvl_t msglevel)
 {
diff --git a/tests/unit_tests/openvpn/Makefile.am 
b/tests/unit_tests/openvpn/Makefile.am
index 5954902..52f18b5 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -40,6 +40,12 @@
 endif
 endif

+if HAVE_SITNL
+if DCO_UNIT_TESTS
+test_binaries += dco_testdriver
+endif
+endif
+
 if !CROSS_COMPILING
 TESTS = $(test_binaries)
 endif
@@ -424,3 +430,14 @@
        $(top_srcdir)/src/openvpn/env_set.c \
        $(top_srcdir)/src/openvpn/run_command.c \
        $(top_srcdir)/src/openvpn/socket_util.c
+
+dco_testdriver_CFLAGS  = \
+       -I$(top_srcdir)/include -I$(top_srcdir)/src/compat 
-I$(top_srcdir)/src/openvpn \
+       @TEST_CFLAGS@ $(OPTIONAL_CRYPTO_CFLAGS)
+dco_testdriver_LDFLAGS = @TEST_LDFLAGS@ $(OPTIONAL_CRYPTO_LIBS)
+dco_testdriver_SOURCES = test_dco.c \
+       mock_msg.c mock_msg.h mock_dco_dependencies.c mock_get_random.c 
test_common.h \
+       $(top_srcdir)/src/openvpn/buffer.c \
+       $(top_srcdir)/src/openvpn/fdmisc.c \
+       $(top_srcdir)/src/openvpn/platform.c \
+       $(top_srcdir)/src/openvpn/win32-util.c
diff --git a/tests/unit_tests/openvpn/mock_dco_dependencies.c 
b/tests/unit_tests/openvpn/mock_dco_dependencies.c
new file mode 100644
index 0000000..c609e77
--- /dev/null
+++ b/tests/unit_tests/openvpn/mock_dco_dependencies.c
@@ -0,0 +1,74 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2026 Gianmarco De Gregori <[email protected]>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*
+ *  Stubs for what dco_linux.c drags in but capability probing never reaches:
+ *  the data path, the event loop and the signal machinery.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+#include "dco.h"
+
+void
+multi_process_incoming_dco(dco_context_t *dco)
+{
+}
+
+void
+process_incoming_dco(dco_context_t *dco)
+{
+}
+
+void
+register_signal(struct signal_info *si, int signum, const char *signal_text)
+{
+}
+
+int
+net_iface_new(openvpn_net_ctx_t *ctx, const char *iface, const char *type, 
void *arg)
+{
+    return 0;
+}
+
+int
+net_iface_del(openvpn_net_ctx_t *ctx, const char *iface)
+{
+    return 0;
+}
+
+int
+cipher_kt_key_size(const char *ciphername)
+{
+    return 0;
+}
+
+const char *
+print_sockaddr_ex(const struct sockaddr *addr, const char *separator, const 
unsigned int flags,
+                  struct gc_arena *gc)
+{
+    return "[mock]";
+}
diff --git a/tests/unit_tests/openvpn/test_dco.c 
b/tests/unit_tests/openvpn/test_dco.c
new file mode 100644
index 0000000..ef3fe44
--- /dev/null
+++ b/tests/unit_tests/openvpn/test_dco.c
@@ -0,0 +1,433 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2002-2026 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2026 Gianmarco De Gregori <[email protected]>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*
+ *  Unit tests for the DCO capability queries.
+ *
+ *  Resolving a requirement is a single pass over a policy dump followed by a
+ *  look at what the pass collected. The pass is fed dumps built by hand and
+ *  the decision is fed a query filled in directly, which between them cover
+ *  what would otherwise need a particular kernel.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+
+#include <stdarg.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+#include "dco_linux.c"
+
+#define CMD_POLICY   4
+#define SUB_POLICY   9
+#define OTHER_POLICY 7
+
+/* the bit is never read here, only the chain that would set it */
+static const struct dco_capability_req test_req = { .cap = 1u,
+                                                    .cmd = OVPN_CMD_PEER_NEW,
+                                                    .nested_attr = OVPN_A_PEER,
+                                                    .target_attr = 
OVPN_A_PEER_KEEPALIVE_INTERVAL,
+                                                    .expected_type = 
NL_ATTR_TYPE_U32 };
+
+/* the same requirement without a container: the target sits in the command's
+ * own policy */
+static const struct dco_capability_req flat_req = { .cap = 1u,
+                                                    .cmd = OVPN_CMD_PEER_NEW,
+                                                    .nested_attr = 0,
+                                                    .target_attr = 
OVPN_A_IFINDEX,
+                                                    .expected_type = 
NL_ATTR_TYPE_U32 };
+
+static struct ovpn_cap_query
+new_query_for(const struct dco_capability_req *req)
+{
+    struct ovpn_cap_query query = {
+        .req = req, .cmd_policy = -1, .sub_policy = -1, .type = -1, 
.unclassified = 0
+    };
+
+    return query;
+}
+
+static struct ovpn_cap_query
+new_query(void)
+{
+    return new_query_for(&test_req);
+}
+
+/* a CTRL_ATTR_OP_POLICY message saying which policy validates `cmd` */
+static struct nl_msg *
+msg_op_policy(uint16_t cmd, uint32_t do_policy)
+{
+    struct nl_msg *msg = nlmsg_alloc();
+    struct nlattr *ops, *op;
+
+    assert_non_null(msg);
+    assert_non_null(genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, 0, 0, 0, 
CTRL_CMD_GETPOLICY, 0));
+
+    ops = nla_nest_start(msg, CTRL_ATTR_OP_POLICY);
+    op = nla_nest_start(msg, cmd);
+    assert_int_equal(nla_put_u32(msg, CTRL_ATTR_POLICY_DO, do_policy), 0);
+    nla_nest_end(msg, op);
+    nla_nest_end(msg, ops);
+
+    return msg;
+}
+
+/* one attribute of a policy: its id, which field of its descriptor to set,
+ * and the value to put there */
+struct attr_desc
+{
+    uint16_t attr_id;
+    int field;
+    uint32_t value;
+};
+
+/* a CTRL_ATTR_POLICY message describing `n` attributes of one policy */
+static struct nl_msg *
+msg_policy_attrs(uint32_t policy_id, const struct attr_desc *descs, size_t n)
+{
+    struct nl_msg *msg = nlmsg_alloc();
+    struct nlattr *policies, *policy, *attr;
+
+    assert_non_null(msg);
+    assert_non_null(genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, 0, 0, 0, 
CTRL_CMD_GETPOLICY, 0));
+
+    policies = nla_nest_start(msg, CTRL_ATTR_POLICY);
+    policy = nla_nest_start(msg, policy_id);
+    for (size_t i = 0; i < n; i++)
+    {
+        attr = nla_nest_start(msg, descs[i].attr_id);
+        assert_int_equal(nla_put_u32(msg, descs[i].field, descs[i].value), 0);
+        nla_nest_end(msg, attr);
+    }
+    nla_nest_end(msg, policy);
+    nla_nest_end(msg, policies);
+
+    return msg;
+}
+
+/* the shape the kernel actually sends: one attribute per message */
+static struct nl_msg *
+msg_policy_attr(uint32_t policy_id, uint16_t attr_id, int field, uint32_t 
value)
+{
+    const struct attr_desc one = { attr_id, field, value };
+
+    return msg_policy_attrs(policy_id, &one, 1);
+}
+
+static void
+feed(struct ovpn_cap_query *query, struct nl_msg *msg)
+{
+    assert_int_equal(ovpn_cap_query_cb(msg, query), NL_OK);
+    nlmsg_free(msg);
+}
+
+/* Walk the whole chain, with both policies carrying attributes we are not
+ * after and the ones we are neither first nor last, so that the pass has to
+ * skip past them rather than stop at the first mismatch. */
+static void
+feed_full_chain(struct ovpn_cap_query *query)
+{
+    const struct attr_desc cmd_attrs[] = {
+        { OVPN_A_IFINDEX, NL_POLICY_TYPE_ATTR_TYPE, NL_ATTR_TYPE_U32 },
+        { OVPN_A_PEER, NL_POLICY_TYPE_ATTR_POLICY_IDX, SUB_POLICY },
+        { OVPN_A_KEYCONF, NL_POLICY_TYPE_ATTR_TYPE, NL_ATTR_TYPE_NESTED },
+    };
+    const struct attr_desc sub_attrs[] = {
+        { OVPN_A_PEER_ID, NL_POLICY_TYPE_ATTR_TYPE, NL_ATTR_TYPE_U32 },
+        { OVPN_A_PEER_REMOTE_PORT, NL_POLICY_TYPE_ATTR_TYPE, NL_ATTR_TYPE_U16 
},
+        { test_req.target_attr, NL_POLICY_TYPE_ATTR_TYPE, NL_ATTR_TYPE_U32 },
+        { OVPN_A_PEER_LOCAL_PORT, NL_POLICY_TYPE_ATTR_TYPE, NL_ATTR_TYPE_U16 },
+    };
+
+    feed(query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(query, msg_policy_attrs(CMD_POLICY, cmd_attrs, SIZE(cmd_attrs)));
+    feed(query, msg_policy_attrs(SUB_POLICY, sub_attrs, SIZE(sub_attrs)));
+}
+
+/* the map tells us which policy validates the command we asked about */
+static void
+test_reads_command_policy(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    assert_int_equal(query.cmd_policy, CMD_POLICY);
+}
+
+/* and not about another one, even though the dump is supposed to be filtered 
*/
+static void
+test_ignores_other_command(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_DEL, CMD_POLICY));
+    assert_int_equal(query.cmd_policy, -1);
+}
+
+/* the nested attribute of that policy points at the sub-policy */
+static void
+test_follows_nested_attribute(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    assert_int_equal(query.sub_policy, SUB_POLICY);
+}
+
+/* an attribute id only means something inside its own policy, so the same
+ * number elsewhere says nothing about our question */
+static void
+test_ignores_nested_attribute_elsewhere(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(OTHER_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    assert_int_equal(query.sub_policy, -1);
+}
+
+/* the type of the target attribute is read in the sub-policy */
+static void
+test_reads_target_type(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed_full_chain(&query);
+    assert_int_equal(query.type, NL_ATTR_TYPE_U32);
+}
+
+/* and the same holds for the target attribute: a type read off some other
+ * policy is not an answer to what we asked */
+static void
+test_ignores_target_attribute_elsewhere(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    feed(&query, msg_policy_attr(OTHER_POLICY, test_req.target_attr, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(query.type, -1);
+}
+
+/* the command's own policy does get read, so the target attribute has to be
+ * turned away there too: the same id there is a different attribute */
+static void
+test_ignores_target_attribute_in_command_policy(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, test_req.target_attr, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(query.type, -1);
+}
+
+/* the sub-policy is read too, so the nested attribute has to be turned away
+ * there as well */
+static void
+test_ignores_nested_attribute_in_sub_policy(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    feed(&query, msg_policy_attr(SUB_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 OTHER_POLICY));
+    assert_int_equal(query.sub_policy, SUB_POLICY);
+}
+
+/* an attribute that is there but not as a nest has nothing to follow */
+static void
+test_nested_attribute_without_policy_idx(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(query.sub_policy, -1);
+}
+
+/* and one that is a nest says nothing about the type we are after */
+static void
+test_target_attribute_without_type(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    feed(&query, msg_policy_attr(SUB_POLICY, test_req.target_attr,
+                                 NL_POLICY_TYPE_ATTR_POLICY_IDX, 
OTHER_POLICY));
+    assert_int_equal(query.type, -1);
+}
+
+/* a policy we cannot place yet is counted, one we walk past afterwards is not 
*/
+static void
+test_counts_only_what_it_could_not_place(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(OTHER_POLICY, OVPN_A_PEER_ID, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(query.unclassified, 1);
+
+    feed(&query, msg_policy_attr(CMD_POLICY, OVPN_A_PEER, 
NL_POLICY_TYPE_ATTR_POLICY_IDX,
+                                 SUB_POLICY));
+    feed(&query, msg_policy_attr(OTHER_POLICY, OVPN_A_PEER_ID, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(query.unclassified, 1);
+}
+
+/* a flat requirement needs no hop: the command's policy is where to look */
+static void
+test_flat_requirement_reads_command_policy(void **state)
+{
+    struct ovpn_cap_query query = new_query_for(&flat_req);
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    assert_int_equal(query.sub_policy, CMD_POLICY);
+
+    feed(&query, msg_policy_attr(CMD_POLICY, flat_req.target_attr, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_SUPPORTED);
+}
+
+/* and an absent one is a plain no, with nothing left unclassified */
+static void
+test_flat_requirement_absent_is_unsupported(void **state)
+{
+    struct ovpn_cap_query query = new_query_for(&flat_req);
+
+    feed(&query, msg_op_policy(OVPN_CMD_PEER_NEW, CMD_POLICY));
+    feed(&query, msg_policy_attr(OTHER_POLICY, flat_req.target_attr, 
NL_POLICY_TYPE_ATTR_TYPE,
+                                 NL_ATTR_TYPE_U32));
+    assert_int_equal(query.unclassified, 0);
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_UNSUPPORTED);
+}
+
+/* nothing to read an answer out of is not the same as a "no" */
+static void
+test_resolve_without_command_policy(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_UNKNOWN);
+}
+
+/* a kernel that does not nest the attribute simply never mentions it */
+static void
+test_resolve_nested_absent(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    query.cmd_policy = CMD_POLICY;
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_UNSUPPORTED);
+}
+
+/* unless the dump went by in an order we could not follow, in which case we
+ * did not find out either way */
+static void
+test_resolve_incomplete_after_skipping(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    query.cmd_policy = CMD_POLICY;
+    query.unclassified = 1;
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_UNKNOWN);
+}
+
+/* the sub-policy is there but does not define the attribute: the ordinary no 
*/
+static void
+test_resolve_target_absent(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    query.cmd_policy = CMD_POLICY;
+    query.sub_policy = SUB_POLICY;
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_UNSUPPORTED);
+}
+
+/* right name, wrong type: not the attribute we would be writing */
+static void
+test_resolve_wrong_type(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    query.cmd_policy = CMD_POLICY;
+    query.sub_policy = SUB_POLICY;
+    query.type = NL_ATTR_TYPE_U8;
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_UNSUPPORTED);
+}
+
+/* and the whole chain, from a dump to a yes */
+static void
+test_resolve_complete_chain(void **state)
+{
+    struct ovpn_cap_query query = new_query();
+
+    feed_full_chain(&query);
+    assert_int_equal(query.unclassified, 0);
+    assert_int_equal(ovpn_cap_resolve(&query), OVPN_CAP_SUPPORTED);
+}
+
+int
+main(void)
+{
+    const struct CMUnitTest tests[] = {
+        cmocka_unit_test(test_reads_command_policy),
+        cmocka_unit_test(test_ignores_other_command),
+        cmocka_unit_test(test_follows_nested_attribute),
+        cmocka_unit_test(test_ignores_nested_attribute_elsewhere),
+        cmocka_unit_test(test_reads_target_type),
+        cmocka_unit_test(test_ignores_target_attribute_elsewhere),
+        cmocka_unit_test(test_ignores_target_attribute_in_command_policy),
+        cmocka_unit_test(test_ignores_nested_attribute_in_sub_policy),
+        cmocka_unit_test(test_nested_attribute_without_policy_idx),
+        cmocka_unit_test(test_target_attribute_without_type),
+        cmocka_unit_test(test_counts_only_what_it_could_not_place),
+        cmocka_unit_test(test_flat_requirement_reads_command_policy),
+        cmocka_unit_test(test_flat_requirement_absent_is_unsupported),
+        cmocka_unit_test(test_resolve_without_command_policy),
+        cmocka_unit_test(test_resolve_nested_absent),
+        cmocka_unit_test(test_resolve_incomplete_after_skipping),
+        cmocka_unit_test(test_resolve_target_absent),
+        cmocka_unit_test(test_resolve_wrong_type),
+        cmocka_unit_test(test_resolve_complete_chain),
+    };
+
+    return cmocka_run_group_tests_name("dco", tests, NULL, NULL);
+}

--
To view, visit https://gerrit.openvpn.net/c/openvpn/+/1981?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.openvpn.net/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: I0051effe7f0636e30557d9ce92c50c458002f9e6
Gerrit-Change-Number: 1981
Gerrit-PatchSet: 1
Gerrit-Owner: Gianmarco De Gregori <[email protected]>
Gerrit-Reviewer: Arne Schwabe <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: Arne Schwabe <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Reply via email to