[Openvpn-devel] [XL] Change in openvpn[master]: Implement sending/receiving app custom control protocol

"plaisthos \(Code Review\) via Openvpn-devel" <[email protected]>
Newsgroups net.sourceforge.lists.openvpn-devel
Message-ID <39296fa9eb4336d0403e35db2eeb4c72241b4dad-EmailReplacePatchSet-HTML@gerrit.openvpn.net>
plaisthos has uploaded a new patch set (#3). ( http://gerrit.openvpn.net/c/openvpn/+/1858?usp=email )


Change subject: Implement sending/receiving app custom control protocol
......................................................................

Implement sending/receiving app custom control protocol

Change-Id: I4a6a6ef41e15896b8b79d795b2a6de86b8ae4c84
Signed-off-by: Arne Schwabe <[email protected]>
---
M CMakeLists.txt
M doc/man-sections/client-options.rst
M doc/management-notes.txt
M src/openvpn/Makefile.am
A src/openvpn/acc.c
A src/openvpn/acc.h
M src/openvpn/forward.c
M src/openvpn/init.c
M src/openvpn/manage.c
M src/openvpn/manage.h
M src/openvpn/multi.c
M src/openvpn/options.c
M src/openvpn/options.h
M src/openvpn/push.c
M src/openvpn/push.h
M src/openvpn/push_util.c
A src/openvpn/push_util.h
M src/openvpn/ssl.c
M src/openvpn/ssl_common.h
M tests/unit_tests/openvpn/Makefile.am
M tests/unit_tests/openvpn/mock_management.c
A tests/unit_tests/openvpn/mock_management.h
M tests/unit_tests/openvpn/mock_push_dependencies.c
A tests/unit_tests/openvpn/test_acc.c
A tests/unit_tests/openvpn/test_acc.h
M tests/unit_tests/openvpn/test_misc.c
M tests/unit_tests/openvpn/test_ssl.c
27 files changed, 1,319 insertions(+), 23 deletions(-)


  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/58/1858/3

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6eb5954..8f52e44 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -424,6 +424,8 @@
     src/compat/compat-gettimeofday.c
     src/compat/compat-strsep.c
     src/compat/compat-strtok_r.c
+    src/openvpn/acc.c
+    src/openvpn/acc.h
     src/openvpn/argv.c
     src/openvpn/argv.h
     src/openvpn/base64.c
@@ -560,8 +562,9 @@
     src/openvpn/ps.c
     src/openvpn/ps.h
     src/openvpn/push.c
-    src/openvpn/push_util.c
     src/openvpn/push.h
+    src/openvpn/push_util.c
+    src/openvpn/push_util.h
     src/openvpn/pushlist.h
     src/openvpn/reflect_filter.c
     src/openvpn/reflect_filter.h
@@ -812,7 +815,10 @@
             tests/unit_tests/openvpn/mock_management.c
             tests/unit_tests/openvpn/mock_ssl_dependencies.c
             tests/unit_tests/openvpn/mock_win32_execve.c
+            tests/unit_tests/openvpn/test_acc.c
+            src/openvpn/acc.c
             src/openvpn/argv.c
+            src/openvpn/auth_token.c
             src/openvpn/base64.c
             src/openvpn/crypto_epoch.c
             src/openvpn/crypto_mbedtls.c
@@ -826,8 +832,11 @@
             src/openvpn/options_util.c
             src/openvpn/otime.c
             src/openvpn/packet_id.c
+            src/openvpn/reliable.c
+            src/openvpn/session_id.c
             src/openvpn/run_command.c
             src/openvpn/ssl_mbedtls.c
+            src/openvpn/ssl_ncp.c
             src/openvpn/ssl_openssl.c
             src/openvpn/ssl_util.c
             src/openvpn/ssl_verify_mbedtls.c
@@ -844,6 +853,8 @@
     target_sources(test_misc PRIVATE
         tests/unit_tests/openvpn/mock_get_random.c
         tests/unit_tests/openvpn/test_schedule.c
+        tests/unit_tests/openvpn/mock_management.c
+        src/openvpn/base64.c
         src/openvpn/options_util.c
         src/openvpn/otime.c
         src/openvpn/ssl_util.c
diff --git a/doc/man-sections/client-options.rst b/doc/man-sections/client-options.rst
index 87c138a..89ba7cf 100644
--- a/doc/man-sections/client-options.rst
+++ b/doc/man-sections/client-options.rst
@@ -12,6 +12,17 @@
   When this option is set, OpenVPN will not drop incoming tun packets with
   same destination as host.
 
+--app-custom-control protocollist
+  Sets the list of supported app custom control protocols.
+  These protocols are announced to the server when this option is used
+  as a client. If the option is set on a server it will negotiate the
+  common protocols with the client.
+
+  The protocollist is a : separated list of protocol identifiers.
+
+  See management-notes.txt in the OpenVPN distribution  and the OpenVPN RFC
+  draft for a description of app custom control protocol support.
+
 --auth-token token
   This is not an option to be used directly in any configuration files,
   but rather push this option from a ``--client-connect`` script or a
diff --git a/doc/management-notes.txt b/doc/management-notes.txt
index d6b3fbb..b2dcc0a 100644
--- a/doc/management-notes.txt
+++ b/doc/management-notes.txt
@@ -798,6 +798,56 @@
 The client should return the response to the crtext challenge
 using the cr-response command.
 
+app custom control
+==================
+If the special string "ACC" is passed as {EXTRA} OpenVPN will determine
+the common app custom protocols between the client and the sever and insruct
+the client that the server will send control messages to the client using
+the ACC protocol.
+
+The server should then use acc-msg command to send app custom control messages
+to the client to faciliate the pending authentication.
+
+COMMAND -- client-acc-msg (OpenVPN 2.8 or higher)
+------------------------------------------
+
+This commands sends an app custom control message to a client,
+the format of the command is:
+
+  client-acc-msg {CID} {KID}
+  {protocol}
+  {flags}
+  {message}
+  END
+
+CID,KID -- client ID and Key ID.  See documentation for ">CLIENT:"
+notification for more info.
+
+protocol -- the ACC protocol identified that this message should be
+            tagged with.
+
+flags    -- a colon seperated list of the encoding and the flag if
+            the message is a fragment. E.g. "6:F" for a base64 encoded
+            message that is also a fragment and not complete yet.
+
+message  -- The message encoded in the encoding specified in flags
+
+
+COMMAND -- acc-msg (OpenVPN 2.8 or higher)
+------------------------------------------
+
+This commands sends an app custom command to the peer, the format of the
+command is
+
+  acc-msg
+  {protocol}
+  {flags}
+  {message}
+  END
+
+See the client-acc-msg command for more details about the format of the message.
+
+
 COMMAND -- client-deny  (OpenVPN 2.1 or higher)
 -----------------------------------------------
 
@@ -1247,6 +1297,23 @@
    response. Mechanisms that need multiple rounds or more complex answers
    should implement a different response type than CR_RESPONSE.
 
+(6) App custom control message
+
+    This message is an application specific payload message that is transported
+    over the OpenVPN control channel.
+
+    In server mode:
+
+    >CLIENT:ACC,{CID},{KID},{protocol},{fragment},{msg_base64}
+
+    In client mode:
+
+    >ACC:{protocol},{fragment},{msg_base64}
+
+    The protocol is a short ascii string identifying the app specific custom
+    protocol and identifies the protocol. If fragment is set to 1, the message
+    is not complete and the next message should be concatenated to this message
+    to form a complete message.
 
 Variables:
 
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index 7fd12b4..4fcccf1 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -42,6 +42,7 @@
 sbin_PROGRAMS = openvpn
 
 openvpn_SOURCES = \
+	acc.c acc.h \
 	argv.c argv.h \
 	auth_token.c auth_token.h \
 	base64.c base64.h \
@@ -121,7 +122,8 @@
 	proto.c proto.h \
 	proxy.c proxy.h \
 	ps.c ps.h \
-	push.c push_util.c push.h \
+	push.c push.h \
+	push_util.c push_util.h \
 	pushlist.h \
 	reflect_filter.c reflect_filter.h \
 	reliable.c reliable.h \
diff --git a/src/openvpn/acc.c b/src/openvpn/acc.c
new file mode 100644
index 0000000..2b7e65f
--- /dev/null
+++ b/src/openvpn/acc.c
@@ -0,0 +1,390 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2025 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2025 Arne Schwabe <[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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+
+#include <string.h>
+#include "buffer.h"
+#include "push_util.h"
+#include "error.h"
+#include "acc.h"
+#include "forward.h"
+#include "options_util.h"
+#include "ssl_util.h"
+#include "ssl_ncp.h"
+
+enum acc_message_flag
+{
+    ACC_MESSAGE_FLAGS_ASCII = 1,
+    ACC_MESSAGE_FLAGS_BASE64 = 2,
+    ACC_MESSAGE_FLAGS_FRAGMENT = 4,
+};
+
+
+static void
+log_acc_message(struct context *c, const struct buffer *buf, int payload_len,
+                const char *protocol, int flags)
+{
+    bool fragment = flags & ACC_MESSAGE_FLAGS_FRAGMENT;
+#ifdef ENABLE_MANAGEMENT
+    struct gc_arena gc = gc_new();
+    const char *payload_msg = NULL;
+
+    /* For simplicity, we always encode payload to be base64 encoded
+     * if not already in base64 format */
+    if (flags & ACC_MESSAGE_FLAGS_ASCII)
+    {
+        char *b64out = NULL;
+        ASSERT(openvpn_base64_encode(BPTR(buf), payload_len, &b64out) >= 0);
+        gc_addspecial(b64out, free, &gc);
+        payload_msg = b64out;
+    }
+    else
+    {
+        payload_msg = BSTR(buf);
+    }
+
+    if (management)
+    {
+        struct tls_session *session = &c->c2.tls_multi->session[TM_ACTIVE];
+
+        struct man_def_auth_context *mda = session->opt->mda_context;
+        if (c->options.mode == MODE_SERVER)
+        {
+            /* server mode. Report client context */
+            unsigned int mda_key_id = get_primary_key(c->c2.tls_multi)->mda_key_id;
+            msg(M_CLIENT, ">CLIENT:ACC,%lu,%u,%s,%d,%s",
+                mda->cid, mda_key_id, protocol, fragment, payload_msg);
+        }
+        else
+        {
+            /* Client mode. Do not a report a CLIENT id */
+            msg(M_CLIENT, ">ACC:%s,%d,%s", protocol, fragment, payload_msg);
+        }
+    }
+    gc_free(&gc);
+#endif /* ifdef ENABLE_MANAGEMENT */
+    msg(D_PUSH, "Received custom app control message (protocol '%s'%s)",
+        protocol, fragment ? ", fragment" : "");
+}
+
+
+/**
+ *  Parses the flags field of an app control message and moves the
+ *  buffer past the flags string. If there is an
+ *  error parsing the flags field the method returns -1 and
+ *  puts the error reason in err_reason
+ */
+static int
+parse_acc_message_flags(struct buffer *buf, const char **err_reason, struct gc_arena *gc)
+{
+    const char *flags = extract_field(buf, ',', gc);
+
+    if (!flags)
+    {
+        *err_reason = "could not extract flags field";
+        return -1;
+    }
+
+
+    int acc_flags = 0;
+
+    for (const char *flag = flags; *flag != '\0'; flag++)
+    {
+        if (*flag == 'A')
+        {
+            acc_flags |= ACC_MESSAGE_FLAGS_ASCII;
+        }
+        else if (*flag == '6')
+        {
+            acc_flags |= ACC_MESSAGE_FLAGS_BASE64;
+        }
+        else if (*flag == 'F')
+        {
+            acc_flags |= ACC_MESSAGE_FLAGS_FRAGMENT;
+        }
+        else
+        {
+            *err_reason = "Unknown flag in flags";
+            return -1;
+        }
+    }
+    /* The message should be encoded with exactly one encoding. Ensure that
+     * only one of the flags is present */
+    bool asciienc = (bool)(acc_flags & ACC_MESSAGE_FLAGS_ASCII);
+    bool base64enc = (bool)(acc_flags & ACC_MESSAGE_FLAGS_BASE64);
+
+    if (base64enc + asciienc != 1)
+    {
+        char *tmp = gc_malloc(512, 1, gc);
+        snprintf(tmp, 512, "number of encodings must be exactly one "
+                           "(B64=%d, ASCII=%d)",
+                 base64enc, asciienc);
+        *err_reason = tmp;
+        return -1;
+    }
+
+    return acc_flags;
+}
+
+
+void
+receive_acc_message(struct context *c, const struct buffer *buffer)
+{
+    struct gc_arena gc = gc_new();
+    const char *err_reason = "";
+
+    /* Example message: ACC,muppets,15,A,I am Miss Piggy */
+    struct buffer buf = *buffer;
+
+    if (!buf_advance(&buf, strlen("ACC")) || buf_read_u8(&buf) != ',')
+    {
+        err_reason = "missing , after ACC";
+        goto err;
+    }
+
+    /* extract protocol, payload length, flags substrings */
+    char *protocol = extract_field(&buf, ',', &gc);
+    if (!protocol)
+    {
+        err_reason = "could not extract protocol field";
+        goto err;
+    }
+
+    int payload_len = 0;
+    if (!buffer_read_int(&buf, &payload_len))
+    {
+        err_reason = "could not extract payload length field";
+        goto err;
+    }
+
+    /* comma after the length */
+    if (buf_read_u8(&buf) != ',')
+    {
+        err_reason = "missing , after payload len";
+        goto err;
+    }
+
+    int flags = parse_acc_message_flags(&buf, &err_reason, &gc);
+
+    if (flags < 0)
+    {
+        goto err;
+    }
+
+    /* We should have a final NUL byte in the control message buffer
+     * and thus the length of the buffer should be payload_len + 1 */
+    if (buf_len(&buf) != payload_len + 1)
+    {
+        char *tmp = gc_malloc(512, 1, &gc);
+        snprintf(tmp, 512, "field length %d, payload length %d mismatch",
+                 payload_len, buf_len(&buf) - 1);
+        err_reason = tmp;
+        goto err;
+    }
+
+
+    log_acc_message(c, &buf, payload_len, protocol, flags);
+    gc_free(&gc);
+    return;
+
+err:
+    msg(D_PUSH_ERRORS, "WARNING: Received malformed custom app control channel "
+                       "(%s) message control message: %s",
+        err_reason,
+        format_hex(BPTR(&buf), BLEN(&buf), 80, &gc));
+    gc_free(&gc);
+}
+
+
+bool
+send_acc_message(struct context *c,
+                 struct tls_multi *tls_multi,
+                 struct tls_session *session,
+                 const char *protocol, bool fragment,
+                 const char *msg, bool base64)
+{
+    /* TODO check client capabilities */
+    /* 3 for the encoding, potential F, and , 1 for the final flag, 5 for the message size itself */
+    const size_t max_header_size = strlen("ACC,") + 3 + strlen(protocol) + 1 + 5;
+
+    size_t len = max_header_size + strlen(msg);
+
+    if (len > PUSH_BUNDLE_SIZE)
+    {
+        msg(M_CLIENT, "message length (%zu) exceeds maximum possible message length (%d)", len, PUSH_BUNDLE_SIZE);
+        return false;
+    }
+
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(len, &gc);
+
+    /* Example message: ACC,muppets,15,A,I am Miss Piggy */
+    buf_printf(&buf, "ACC,%s,%zu,%s%s,%s", protocol,
+               strlen(msg),
+               base64 ? "6" : "A",
+               fragment ? "F" : "",
+               msg);
+
+    send_control_channel_string_dowork(session, BSTR(&buf), D_PUSH);
+    reschedule_multi_process(c);
+    return true;
+}
+
+char *
+parse_acc_parameters(const char *capabilities,
+                     const char *delim,
+                     int *max_acc_len,
+                     bool server,
+                     struct gc_arena *gc)
+{
+    char *tmp_message = string_alloc(capabilities, NULL);
+    char *tmp_message_orig = tmp_message;
+    char *lasts = NULL;
+
+    const char *token = strtok_r(tmp_message, delim, &lasts);
+
+    int p = 0;
+    int acclen = 0;
+    const char *protocols = NULL;
+    char *ret = NULL;
+
+    /* In server mode we allow more capabilities than our own, in client
+     * mode we reject if we should use capabilities that we do not support */
+    int allowed_acc_len = server ? INT_MAX : ACC_MAX_MSG_LEN;
+
+    while (token)
+    {
+        if (p == 0 && !atoi_constrained(token, &acclen, "ACC message length", ACC_MIN_MSG_LEN, allowed_acc_len, D_PUSH_ERRORS))
+        {
+            goto done;
+        }
+
+        if (p == 1 && (!tls_item_in_cipher_list("A", token) || !tls_item_in_cipher_list("6", token)))
+        {
+            msg(D_PUSH_ERRORS, "App custom control encoding must include base64 and ascii");
+            goto done;
+        }
+
+        if (p == 1 && !server && strcmp(token, "A:6") && strcmp(token, "6:A"))
+        {
+            msg(D_PUSH_ERRORS, "App custom control encoding must exactly base64 and ascii");
+            goto done;
+        }
+
+        if (p == 2)
+        {
+            protocols = token;
+        }
+
+        if (p == 3)
+        {
+            msg(D_PUSH_ERRORS, "Too many parameters for INFO ACC message/IV_SSO.");
+            goto done;
+        }
+
+        token = strtok_r(NULL, delim, &lasts);
+        p++;
+    }
+
+    *max_acc_len = min_int(acclen, ACC_MAX_MSG_LEN);
+    ret = string_alloc(protocols, gc);
+
+done:
+    free(tmp_message_orig);
+    return ret;
+}
+
+
+void
+determine_common_acc_protocols(struct context *c)
+{
+    if (!c->options.acc_protocols)
+    {
+        /* No app custom protocols have been configured server-side. */
+        return;
+    }
+    if (c->options.acc_negotiated_protocols)
+    {
+        /* We already have determined the common protocols. Doing this
+         * again would just add an extra string allocation to options->gc */
+        return;
+    }
+
+    struct gc_arena gc = gc_new();
+
+    /* example IV_ACC: IV_ACC=2048,6:A,flower:power */
+    const char *peer_info = c->c2.tls_multi->peer_info;
+    char *iv_acc = extract_var_peer_info(peer_info, "IV_ACC=", &gc);
+    if (!iv_acc)
+    {
+        gc_free(&gc);
+        return;
+    }
+
+    int max_acc_len = 0;
+    char *protocols = parse_acc_parameters(iv_acc, ",", &max_acc_len, true, &gc);
+
+    if (!protocols)
+    {
+        gc_free(&gc);
+        msg(D_PUSH_ERRORS, "WARNING: IV_ACC peer info variable of peer is malformed");
+        return;
+    }
+
+    char *lasts = NULL;
+    const char *token = strtok_r(protocols, ":", &lasts);
+
+    struct buffer common_protocols = alloc_buf_gc(1024, &gc);
+
+    while (token)
+    {
+        if (tls_item_in_cipher_list(token, c->options.acc_protocols))
+        {
+            /* protocol is in both server and client list */
+            if (buf_len(&common_protocols) > 0)
+            {
+                buf_write_u8(&common_protocols, ':');
+            }
+            buf_printf(&common_protocols, "%s", token);
+        }
+        token = strtok_r(NULL, ":", &lasts);
+    }
+
+    if (buf_len(&common_protocols) == 0)
+    {
+        gc_free(&gc);
+        msg(D_PUSH_DEBUG, "WARNING: No common app custom control protocol.");
+        return;
+    }
+
+    c->options.acc_negotiated_protocols = string_alloc(buf_str(&common_protocols), &c->options.gc);
+    c->options.app_custom_protocol_len = max_acc_len;
+    gc_free(&gc);
+}
\ No newline at end of file
diff --git a/src/openvpn/acc.h b/src/openvpn/acc.h
new file mode 100644
index 0000000..c86eb5d
--- /dev/null
+++ b/src/openvpn/acc.h
@@ -0,0 +1,92 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2025 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2025 Arne Schwabe <[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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef ACC_H
+#define ACC_H
+
+#include "openvpn.h"
+
+#define ACC_MIN_MSG_LEN 64
+#define ACC_MAX_MSG_LEN 1280
+
+/**
+ * This method parses an app custom control message and delivers it to the
+ * management interface. We leave reassembly of fragmented messages to the
+ * management interface.
+ *
+ * @param c            The context struct
+ * @param buffer       Buffer containing the control message with ACC
+ */
+void
+receive_acc_message(struct context *c, const struct buffer *buffer);
+
+
+bool
+send_acc_message(struct context *c,
+                 struct tls_multi *tls_multi,
+                 struct tls_session *session,
+                 const char *protocol, bool fragment,
+                 const char *msg, bool base64);
+
+
+/**
+ * Parses an info message from the server that contains app custom control info
+ * information prior to the PUSH_REPLY to be able to use app custom control
+ * protocols before sending
+ *
+ * In client mode only allow what we support ourselves. In server mode
+ * allow also capabilities that we not support ourselves.
+ *
+ * @param capabilities      The string to parse
+ * @param delim             The delimiter to use when parsing the string.
+ *                          IV_ACC uses "," as delimiter while the
+ *                          custom-control PUSH message uses " " as "," is
+ *                          already used to separate different push directives
+ * @param max_acc_len       Set to the maximum ACC message length
+ * @param server            Whether we are server or client
+ * @param gc                The gc arena to use for allocating the resulting string
+ * @return                  The resulting protocol string or NULL on error
+ */
+char *
+parse_acc_parameters(const char *capabilities,
+                     const char *delim,
+                     int *max_acc_len,
+                     bool server,
+                     struct gc_arena *gc);
+
+/**
+ * Parses the IV_ACC of the peer and sets c->options.acc_negotiated_protocols
+ * to the common protocols of server and client and
+ * c->options.app_custom_protocol_len  to the maximum length of the ACC message
+ * support by both server and client.
+ *
+ * If there is no common protocol or the client does not support any app custom
+ * protocol c->options.acc_negotiated_protocols will be set to NULL.
+ *
+ * @param c     the client context to use the set the app custom protocols.
+ */
+void
+determine_common_acc_protocols(struct context *c);
+#endif
diff --git a/src/openvpn/forward.c b/src/openvpn/forward.c
index 46e1a53..48a9b7c 100644
--- a/src/openvpn/forward.c
+++ b/src/openvpn/forward.c
@@ -42,6 +42,7 @@
 #include "dco.h"
 #include "auth_token.h"
 #include "tun_afunix.h"
+#include "acc.h"
 
 #include "memdbg.h"
 
@@ -251,11 +252,11 @@
     }
     else if (buf_string_match_head_str(buf, "INFO_PRE"))
     {
-        server_pushed_info(buf, 8);
+        server_pushed_info(c, buf, 8);
     }
     else if (buf_string_match_head_str(buf, "INFO"))
     {
-        server_pushed_info(buf, 4);
+        server_pushed_info(c, buf, 4);
     }
     else if (buf_string_match_head_str(buf, "CR_RESPONSE"))
     {
@@ -265,6 +266,10 @@
     {
         receive_auth_pending(c, buf);
     }
+    else if (buf_string_match_head_str(buf, "ACC"))
+    {
+        receive_acc_message(c, buf);
+    }
     else if (buf_string_match_head_str(buf, "EXIT"))
     {
         receive_exit_message(c);
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index 101fa64..fc7e9b4 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -2283,6 +2283,10 @@
             buf_printf(&out, " aead-epoch");
         }
     }
+    if (c->options.acc_negotiated_protocols)
+    {
+        buf_printf(&out, "custom-control %d,A:6,%s", c->options.app_custom_protocol_len, c->options.acc_negotiated_protocols);
+    }
 
     if (BLENZ(&out) > strlen(header))
     {
@@ -2527,6 +2531,11 @@
         flags |= (OPT_P_ROUTE | OPT_P_DHCPDNS);
     }
 
+    if (c->options.acc_protocols)
+    {
+        flags |= OPT_P_ACC;
+    }
+
     return flags;
 }
 
@@ -3342,6 +3351,7 @@
     to.verify_hash_depth = options->verify_hash_depth;
     to.verify_hash_no_ca = options->verify_hash_no_ca;
     memcpy(to.x509_username_field, options->x509_username_field, sizeof(to.x509_username_field));
+    to.acc_protocols = options->acc_protocols;
     to.es = c->c2.es;
     to.net_ctx = &c->net_ctx;
 
@@ -4300,6 +4310,27 @@
 }
 #endif /* ifdef TARGET_ANDROID */
 
+static bool
+management_acc_msg(void *arg,
+                   struct buffer_list *input) /* ownership transferred */
+{
+    struct gc_arena gc = gc_new();
+    struct context *c = arg;
+
+    bool ret = true;
+    if (buffer_list_defined(input))
+    {
+        struct tls_multi *multi = c->c2.tls_multi;
+        struct tls_session *session = &multi->session[TM_ACTIVE];
+        struct options *opt = &c->options;
+
+        ret = management_send_acc_message(c, multi, session, opt->acc_negotiated_protocols, input);
+    }
+
+    buffer_list_free(input);
+    gc_free(&gc);
+    return ret;
+}
 #endif /* ifdef ENABLE_MANAGEMENT */
 
 void
@@ -4316,6 +4347,7 @@
         cb.proxy_cmd = management_callback_proxy_cmd;
         cb.remote_cmd = management_callback_remote_cmd;
         cb.send_cc_message = management_callback_send_cc_message;
+        cb.acc_msg = management_acc_msg;
 #ifdef TARGET_ANDROID
         cb.network_change = management_callback_network_change;
 #endif
diff --git a/src/openvpn/manage.c b/src/openvpn/manage.c
index bfc2aa2..a9f2463 100644
--- a/src/openvpn/manage.c
+++ b/src/openvpn/manage.c
@@ -44,8 +44,10 @@
 #include "dco.h"
 #include "push.h"
 #include "multi.h"
+#include "acc.h"
 
 #include "memdbg.h"
+#include "ssl_ncp.h"
 
 #ifdef ENABLE_PKCS11
 #include "pkcs11.h"
@@ -1080,6 +1082,35 @@
             }
             break;
 
+        case IEC_CLIENT_ACC_MSG:
+            if (man->persist.callback.client_acc_msg)
+            {
+                bool status = (*man->persist.callback.client_acc_msg)(man->persist.callback.arg,
+                                                                      man->connection.in_extra_cid,
+                                                                      man->connection.in_extra_kid,
+                                                                      man->connection.in_extra);
+                man->connection.in_extra = NULL;
+                report_command_status(status, "client-acc-msg");
+            }
+            else
+            {
+                man_command_unsupported("client-acc-msg");
+            }
+            break;
+
+        case IEC_ACC_MSG:
+            if (man->persist.callback.acc_msg)
+            {
+                bool status = (*man->persist.callback.acc_msg)(man->persist.callback.arg,
+                                                               man->connection.in_extra);
+                man->connection.in_extra = NULL;
+                report_command_status(status, "acc-msg");
+            }
+            else
+            {
+                man_command_unsupported("acc-msg");
+            }
+            break;
         case IEC_PK_SIGN:
             man->connection.ext_key_state = EKS_READY;
             buffer_list_free(man->connection.ext_key_input);
@@ -1204,6 +1235,29 @@
 }
 
 static void
+man_client_acc_msg(struct management *man, const char *cid_str,
+                   const char *kid_str)
+{
+    struct man_connection *mc = &man->connection;
+    mc->in_extra_cid = 0;
+    mc->in_extra_kid = 0;
+    if (parse_cid(cid_str, &mc->in_extra_cid)
+        && parse_uint(kid_str, "KID", &mc->in_extra_kid))
+    {
+        mc->in_extra_cmd = IEC_CLIENT_ACC_MSG;
+        in_extra_reset(mc, IER_NEW);
+    }
+}
+
+static void
+man_acc_msg(struct management *man)
+{
+    struct man_connection *mc = &man->connection;
+    mc->in_extra_cmd = IEC_ACC_MSG;
+    in_extra_reset(mc, IER_NEW);
+}
+
+static void
 man_client_auth(struct management *man, const char *cid_str, const char *kid_str, const bool extra)
 {
     struct man_connection *mc = &man->connection;
@@ -1760,6 +1814,17 @@
             man_client_pending_auth(man, p[1], p[2], p[3], p[4]);
         }
     }
+    else if (streq(p[0], "client-acc-msg"))
+    {
+        if (man_need(man, p, 2, 0))
+        {
+            man_client_acc_msg(man, p[1], p[2]);
+        }
+    }
+    else if (streq(p[0], "acc-msg"))
+    {
+        man_acc_msg(man);
+    }
     else if (streq(p[0], "rsa-sig"))
     {
         man_pk_sig(man, "rsa-sig");
@@ -3121,6 +3186,7 @@
     }
 }
 
+
 void
 management_connection_established(struct management *management, struct man_def_auth_context *mdac,
                                   const struct env_set *es)
@@ -4352,6 +4418,65 @@
     }
 }
 
+bool
+management_send_acc_message(struct context *c, struct tls_multi *multi, struct tls_session *session,
+                            const char *negotiated_protocols, struct buffer_list *input)
+{
+    /* ensure that are enough inputs in the list */
+    if (!input || input->size < 3)
+    {
+        msg(M_CLIENT, "not enough input lines for acc message");
+        return false;
+    }
+
+    struct gc_arena gc = gc_new();
+
+    const struct buffer *proto_buf = buffer_list_peek(input);
+    const char *protocol = string_alloc(BSTR(proto_buf), &gc);
+    buffer_list_pop(input);
+
+    const struct buffer *encoding_buf = buffer_list_peek(input);
+    char *encoding = buf_str(encoding_buf);
+    char *flag = NULL;
+    bool b64encoding = false;
+    bool fragment = false;
+
+    while ((flag = strsep(&encoding, ":")))
+    {
+        if (!strcmp(flag, "6"))
+        {
+            b64encoding = true;
+        }
+        else if (!strcmp(flag, "F"))
+        {
+            fragment = true;
+        }
+    }
+    buffer_list_pop(input);
+
+    buffer_list_aggregate_separator(input, 10000, "");
+    const struct buffer *acc_msg_buf = buffer_list_peek(input);
+
+    if (!proto_buf || !acc_msg_buf)
+    {
+        msg(M_CLIENT, "missing protocol or message");
+        goto error;
+    }
+    if (!negotiated_protocols || !tls_item_in_cipher_list(protocol, negotiated_protocols))
+    {
+        msg(M_CLIENT, "protocol not in the list of negotiated protocols: %s", np(negotiated_protocols));
+        goto error;
+    }
+
+    bool ret = send_acc_message(c, multi, session, protocol,
+                                fragment, buf_str(acc_msg_buf),
+                                b64encoding);
+    gc_free(&gc);
+    return ret;
+error:
+    gc_free(&gc);
+    return false;
+}
 #else /* ifdef ENABLE_MANAGEMENT */
 
 #include "win32.h"
diff --git a/src/openvpn/manage.h b/src/openvpn/manage.h
index 71e88d0..39bdf99 100644
--- a/src/openvpn/manage.h
+++ b/src/openvpn/manage.h
@@ -184,12 +184,19 @@
     int (*n_clients)(void *arg);
     bool (*send_cc_message)(void *arg, const char *message, const char *parameter);
     bool (*kill_by_cid)(void *arg, const unsigned long cid, const char *kill_msg);
+
     bool (*client_auth)(void *arg, const unsigned long cid, const unsigned int mda_key_id,
                         const bool auth, const char *reason, const char *client_reason,
                         struct buffer_list *cc_config); /* ownership transferred */
     bool (*client_pending_auth)(void *arg, const unsigned long cid, const unsigned int kid,
                                 const char *extra, unsigned int timeout);
     char *(*get_peer_info)(void *arg, const unsigned long cid);
+    bool (*client_acc_msg)(void *arg,
+                           const unsigned long cid,
+                           const unsigned int mda_key_id,
+                           struct buffer_list *msg);
+    bool (*acc_msg)(void *arg,
+                    struct buffer_list *msg);
     bool (*proxy_cmd)(void *arg, const char **p);
     bool (*remote_cmd)(void *arg, const char **p);
 #ifdef TARGET_ANDROID
@@ -292,13 +299,15 @@
     struct command_line *in;
     struct buffer_list *out;
 
-#define IEC_UNDEF       0
-#define IEC_CLIENT_AUTH 1
+#define IEC_UNDEF          0
+#define IEC_CLIENT_AUTH    1
 /* #define IEC_CLIENT_PF   2 *REMOVED FEATURE* */
-#define IEC_RSA_SIGN    3
-#define IEC_CERTIFICATE 4
-#define IEC_PK_SIGN     5
-#define IEC_PASSWORD    6
+#define IEC_RSA_SIGN       3
+#define IEC_CERTIFICATE    4
+#define IEC_PK_SIGN        5
+#define IEC_PASSWORD       6
+#define IEC_CLIENT_ACC_MSG 7
+#define IEC_ACC_MSG        8
     int in_extra_cmd;
     struct buffer_list *in_extra;
     unsigned long in_extra_cid;
@@ -511,4 +520,13 @@
  */
 void management_sleep(const int n);
 
+/** Parses the ACC message from the input buffer_list and send to the
+ * session identified by session and multi
+ */
+struct tls_session;
+struct tls_multi;
+bool
+management_send_acc_message(struct context *c, struct tls_multi *multi, struct tls_session *session,
+                            const char *negotiated_protocols, struct buffer_list *input);
+
 #endif /* ifndef MANAGE_H */
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index dda5c6b..ae2b80d 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -52,6 +52,7 @@
 #include "ssl_util.h"
 #include "dco.h"
 #include "reflect_filter.h"
+#include "acc.h"
 
 /*#define MULTI_DEBUG_EVENT_LOOP*/
 
@@ -1773,6 +1774,13 @@
         o->imported_protocol_flags |= CO_USE_CC_EXIT_NOTIFY;
     }
 
+    if (o->acc_protocols)
+    {
+        /* If ACC is enabled server side call to find common acc protocols,
+         * will be pushed during push message */
+        determine_common_acc_protocols(c);
+    }
+
     /* Select cipher if client supports Negotiable Crypto Parameters */
 
     /* if we have already created our key, we cannot *change* our own
@@ -4032,6 +4040,24 @@
     }
 }
 
+static const char *
+calculate_app_control_extra(struct context *c, struct gc_arena *gc)
+{
+    determine_common_acc_protocols(c);
+
+    if (!c->options.acc_negotiated_protocols)
+    {
+        msg(M_CLIENT, "ACC: No negotiated protocols");
+        return NULL;
+    }
+
+    struct buffer buf = alloc_buf_gc(1024, gc);
+
+    buf_printf(&buf, "ACC:%d A:6 %s", c->options.app_custom_protocol_len, c->options.acc_negotiated_protocols);
+    return buf_str(&buf);
+}
+
+
 static bool
 management_client_pending_auth(void *arg, const unsigned long cid, const unsigned int mda_key_id,
                                const char *extra, unsigned int timeout)
@@ -4042,17 +4068,29 @@
     if (mi)
     {
         struct tls_multi *multi = mi->context.c2.tls_multi;
+
+        struct gc_arena gc = gc_new();
+        if (!strcmp(extra, "ACC"))
+        {
+            extra = calculate_app_control_extra(&mi->context, &gc);
+            if (!extra)
+            {
+                gc_free(&gc);
+                return false;
+            }
+        }
         struct tls_session *session = lookup_session_by_mda_key_id(multi, mda_key_id);
 
         if (!session)
         {
             return false;
         }
-        
+
         /* sends INFO_PRE and AUTH_PENDING messages to client */
         bool ret = send_auth_pending_messages(multi, session, extra, timeout);
         reschedule_multi_process(&mi->context);
         multi_schedule_context_wakeup(m, mi);
+        gc_free(&gc);
         return ret;
     }
     return false;
@@ -4096,6 +4134,39 @@
     return ret;
 }
 
+
+static bool
+management_client_acc_msg(void *arg,
+                          const unsigned long cid,
+                          const unsigned int mda_key_id,
+                          struct buffer_list *input) /* ownership transferred */
+{
+    struct multi_context *m = (struct multi_context *)arg;
+    struct multi_instance *mi = lookup_by_cid(m, cid);
+
+    bool ret = false;
+    if (mi && buffer_list_defined(input))
+    {
+        struct tls_multi *multi = mi->context.c2.tls_multi;
+        struct tls_session *session = lookup_session_by_mda_key_id(multi, mda_key_id);
+        if (!session)
+        {
+            msg(M_CLIENT, "client session not found");
+            buffer_list_free(input);
+            return false;
+        }
+
+        struct options *opt = &mi->context.options;
+
+        ret = management_send_acc_message(&mi->context, multi, session, opt->acc_negotiated_protocols, input);
+        multi_schedule_context_wakeup(m, mi);
+    }
+
+    buffer_list_free(input);
+    return ret;
+}
+
+
 static char *
 management_get_peer_info(void *arg, const unsigned long cid)
 {
@@ -4132,6 +4203,7 @@
         cb.n_clients = management_callback_n_clients;
         cb.kill_by_cid = management_kill_by_cid;
         cb.client_auth = management_client_auth;
+        cb.client_acc_msg = management_client_acc_msg;
         cb.client_pending_auth = management_client_pending_auth;
         cb.get_peer_info = management_get_peer_info;
         cb.push_update_broadcast = management_callback_send_push_update_broadcast;
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index 588d75d..30bd72c 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -35,6 +35,7 @@
 
 #include "syshead.h"
 
+#include "acc.h"
 #include "buffer.h"
 #include "error.h"
 #include "common.h"
@@ -7100,6 +7101,28 @@
         VERIFY_PERMISSION(OPT_P_GENERAL);
         options->force_key_material_export = true;
     }
+    else if (streq(p[0], "app-custom-control") && p[1] && !p[2])
+    {
+        VERIFY_PERMISSION(OPT_P_GENERAL);
+        options->acc_protocols = p[1];
+    }
+    else if (streq(p[0], "custom-control") && p[1] && p[2] && p[3] && !p[4])
+    {
+        VERIFY_PERMISSION(OPT_P_ACC);
+
+        if (!streq(p[2], "A:6") && !streq(p[2], "6:A"))
+        {
+            msg(msglevel, "App custom control encoding must be base64 and ascii");
+            goto err;
+        }
+
+        if (!atoi_constrained(p[1], &options->app_custom_protocol_len,
+                              "app custom message message length", ACC_MIN_MSG_LEN, ACC_MAX_MSG_LEN, msglevel))
+        {
+            goto err;
+        }
+        options->acc_negotiated_protocols = p[3];
+    }
     else if (streq(p[0], "prng") && p[1] && !p[3])
     {
         msg(M_WARN, "NOTICE: --prng option ignored (SSL library PRNG is used)");
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index 131513e..3151494 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -711,6 +711,16 @@
     /* force using TLS key material export for data channel key generation */
     bool force_key_material_export;
 
+    /** The app custom control protocols we should announce as supported */
+    const char *acc_protocols;
+
+    /** the protocols that are negotiated with the peer. As a client we
+     * trust the server pushed list here */
+    const char *acc_negotiated_protocols;
+
+    /** the maximum length of an ACC control channel message */
+    int app_custom_protocol_len;
+
     bool vlan_tagging;
     enum vlan_acceptable_frames vlan_accept;
     uint16_t vlan_pvid;
@@ -762,8 +772,10 @@
 #define OPT_P_INLINE          (1u << 29)
 #define OPT_P_PUSH_MTU        (1u << 30)
 #define OPT_P_ROUTE_TABLE     (1u << 31)
+#define OPT_P_ACC             (1ull << 32)
 
-#define OPT_P_DEFAULT (~(OPT_P_INSTANCE | OPT_P_PULL_MODE | OPT_P_PEER_ID | 0x0ull))
+/* Options allowed in the normal configuration file (vs pushed options or in ccd files) */
+#define OPT_P_DEFAULT (~(OPT_P_INSTANCE | OPT_P_PULL_MODE | OPT_P_PEER_ID | OPT_P_ACC))
 
 #define PULL_DEFINED(opt) ((opt)->pull)
 
diff --git a/src/openvpn/push.c b/src/openvpn/push.c
index 5b15f71..7697ec2 100644
--- a/src/openvpn/push.c
+++ b/src/openvpn/push.c
@@ -27,6 +27,9 @@
 #include "syshead.h"
 
 #include "push.h"
+
+#include "acc.h"
+#include "push_util.h"
 #include "options.h"
 #include "crypto.h"
 #include "ssl.h"
@@ -220,9 +223,8 @@
 #endif
 }
 
-
 void
-server_pushed_info(const struct buffer *buffer, const int adv)
+server_pushed_info(struct context *c, const struct buffer *buffer, const int adv)
 {
     const char *m = "";
     struct buffer buf = *buffer;
@@ -232,6 +234,27 @@
         m = BSTR(&buf);
     }
 
+    if (strncmp(m, "ACC:", 4) == 0)
+    {
+        int max_acc_len = 0;
+
+
+        /* example string to parse: ACC:1400 A:6 flower:happy */
+        /* Skip over the ACC: prefix */
+        m += 4;
+
+        char *protocols = parse_acc_parameters(m, " ", &max_acc_len, false, &c->options.gc);
+        if (protocols)
+        {
+            c->options.acc_negotiated_protocols = protocols;
+            c->options.app_custom_protocol_len = max_acc_len;
+        }
+        else
+        {
+            msg(D_PUSH_ERRORS, "WARNING: Received ACC info command with invalid parameters");
+        }
+    }
+
 #ifdef ENABLE_MANAGEMENT
     struct gc_arena gc;
     if (management)
@@ -284,6 +307,7 @@
     msg(D_PUSH, "CR response was sent by client ('%s')", m);
 }
 
+
 /**
  * Parse the keyword for the AUTH_PENDING request
  * @param buffer                buffer containing the keywords, the buffer's
@@ -440,7 +464,6 @@
     const char *const peer_info = tls_multi->peer_info;
     unsigned int proto = extract_iv_proto(peer_info);
 
-
     /* Calculate the maximum timeout and subtract the time we already waited */
     unsigned int max_timeout =
         max_uint(tls_multi->opt.renegotiate_seconds / 2, tls_multi->opt.handshake_window);
@@ -731,6 +754,11 @@
                 client_max_mtu, o->ce.tun_mtu, o->ce.tun_mtu);
         }
     }
+
+    if (c->options.acc_negotiated_protocols)
+    {
+        push_option_fmt(gc, push_list, M_USAGE, "custom-control %d A:6 %s", c->options.app_custom_protocol_len, c->options.acc_negotiated_protocols);
+    }
 }
 
 #if defined(__GNUC__) || defined(__clang__)
diff --git a/src/openvpn/push.h b/src/openvpn/push.h
index dff945f..3aca8a7 100644
--- a/src/openvpn/push.h
+++ b/src/openvpn/push.h
@@ -52,6 +52,7 @@
 
 int process_incoming_push_request(struct context *c);
 
+
 /**
  * @brief Handles the receiving of a push-update message and applies updates to the specified
  * options.
@@ -92,7 +93,7 @@
 
 void receive_exit_message(struct context *c);
 
-void server_pushed_info(const struct buffer *buffer, const int adv);
+void server_pushed_info(struct context *c, const struct buffer *buffer, const int adv);
 
 void receive_cr_response(struct context *c, const struct buffer *buffer);
 
diff --git a/src/openvpn/push_util.c b/src/openvpn/push_util.c
index fcc5411..6462a97 100644
--- a/src/openvpn/push_util.c
+++ b/src/openvpn/push_util.c
@@ -370,4 +370,4 @@
 
     RETURN_UPDATE_STATUS(n_sent);
 }
-#endif /* ifdef ENABLE_MANAGEMENT */
+#endif /* ifdef ENABLE_MANAGEMENT */
\ No newline at end of file
diff --git a/src/openvpn/push_util.h b/src/openvpn/push_util.h
new file mode 100644
index 0000000..c9cc680
--- /dev/null
+++ b/src/openvpn/push_util.h
@@ -0,0 +1,34 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2025 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2025 Arne Schwabe <[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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "buffer.h"
+
+/**
+ * Extract a field from buf that end with the \c sep character. The
+ * returned string is allocated in the gc_arena. If the seperator character
+ * is not found, the function returns the nullptr.
+ */
+char *
+extract_field(struct buffer *buf, char sep, struct gc_arena *gc);
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index d5bd602..12e19aa 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -55,6 +55,7 @@
 
 #include "crypto_epoch.h"
 #include "ssl.h"
+#include "acc.h"
 #include "ssl_verify.h"
 #include "ssl_backend.h"
 #include "ssl_ncp.h"
@@ -2054,6 +2055,11 @@
                     }
                 }
             }
+
+            if (session->opt->acc_protocols)
+            {
+                buf_printf(&out, "IV_ACC=%d,6:A,%s\n", ACC_MAX_MSG_LEN, session->opt->acc_protocols);
+            }
         }
     }
 
diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h
index db61198..3b326a7 100644
--- a/src/openvpn/ssl_common.h
+++ b/src/openvpn/ssl_common.h
@@ -452,6 +452,9 @@
     size_t ekm_size;
 
     bool dco_enabled; /**< Whether keys have to be installed in DCO or not */
+
+    /** The app custom control protocols we should announce as supported */
+    const char *acc_protocols;
 };
 
 /** @addtogroup control_processor
diff --git a/tests/unit_tests/openvpn/Makefile.am b/tests/unit_tests/openvpn/Makefile.am
index 5954902..cfa5efc 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -103,9 +103,11 @@
 	@TEST_CFLAGS@
 ssl_testdriver_LDFLAGS = @TEST_LDFLAGS@  $(OPTIONAL_CRYPTO_LIBS)
 ssl_testdriver_SOURCES = test_ssl.c \
+    test_acc.c test_acc.h \
 	mock_msg.c mock_msg.h test_common.h \
-	mock_management.c \
+	mock_management.c mock_management.h \
 	mock_ssl_dependencies.c mock_win32_execve.c \
+	$(top_srcdir)/src/openvpn/acc.c \
 	$(top_srcdir)/src/openvpn/argv.c \
 	$(top_srcdir)/src/openvpn/base64.c \
 	$(top_srcdir)/src/openvpn/buffer.c \
@@ -127,6 +129,7 @@
 	$(top_srcdir)/src/openvpn/run_command.c \
 	$(top_srcdir)/src/openvpn/ssl_openssl.c \
 	$(top_srcdir)/src/openvpn/ssl_mbedtls.c \
+	$(top_srcdir)/src/openvpn/ssl_ncp.c \
 	$(top_srcdir)/src/openvpn/ssl_util.c \
 	$(top_srcdir)/src/openvpn/ssl_verify_mbedtls.c \
 	$(top_srcdir)/src/openvpn/ssl_verify_openssl.c \
@@ -375,8 +378,10 @@
 
 misc_testdriver_SOURCES = test_misc.c \
 	test_schedule.c test_schedule.h \
+	mock_management.c mock_management.h \
 	mock_msg.c test_common.h  \
 	mock_get_random.c \
+	$(top_srcdir)/src/openvpn/base64.c \
 	$(top_srcdir)/src/openvpn/buffer.c \
 	$(top_srcdir)/src/openvpn/options_util.c \
 	$(top_srcdir)/src/openvpn/ssl_util.c \
@@ -388,6 +393,7 @@
 	$(top_srcdir)/src/openvpn/siphash.c \
 	$(top_srcdir)/src/openvpn/siphash_reference.c
 
+
 push_update_msg_testdriver_CFLAGS = -I$(top_srcdir)/src/openvpn \
 	-I$(top_srcdir)/src/compat \
 	-I$(top_srcdir)/tests/unit_tests/openvpn \
diff --git a/tests/unit_tests/openvpn/mock_management.c b/tests/unit_tests/openvpn/mock_management.c
index 77f6ce2..e3bf828 100644
--- a/tests/unit_tests/openvpn/mock_management.c
+++ b/tests/unit_tests/openvpn/mock_management.c
@@ -29,8 +29,11 @@
 
 #include "syshead.h"
 
+#include <stdlib.h>
 #include "manage.h"
 
+#include "mock_management.h"
+
 #ifdef ENABLE_MANAGEMENT
 
 struct management *management; /* GLOBAL */
@@ -61,3 +64,15 @@
 management_sleep(const int n)
 {
 }
+
+void
+init_mock_management(void)
+{
+    ALLOC_OBJ_CLEAR(management, struct management);
+}
+
+void
+uninit_mock_management(void)
+{
+    free(management);
+}
\ No newline at end of file
diff --git a/tests/unit_tests/openvpn/mock_management.h b/tests/unit_tests/openvpn/mock_management.h
new file mode 100644
index 0000000..e21aec43
--- /dev/null
+++ b/tests/unit_tests/openvpn/mock_management.h
@@ -0,0 +1,32 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2025 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2025 Arne Schwabe <[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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MOCK_MANAGEMENT_H
+#define MOCK_MANAGEMENT_H
+void init_mock_management(void);
+
+void uninit_mock_management(void);
+
+#endif
diff --git a/tests/unit_tests/openvpn/mock_push_dependencies.c b/tests/unit_tests/openvpn/mock_push_dependencies.c
index 112bcfd..c16d0cb 100644
--- a/tests/unit_tests/openvpn/mock_push_dependencies.c
+++ b/tests/unit_tests/openvpn/mock_push_dependencies.c
@@ -43,7 +43,8 @@
     uint64_t flags = OPT_P_UP | OPT_P_ROUTE_EXTRAS | OPT_P_SOCKBUF | OPT_P_SOCKFLAGS
                      | OPT_P_SETENV | OPT_P_SHAPER | OPT_P_TIMER | OPT_P_COMP | OPT_P_PERSIST
                      | OPT_P_MESSAGES | OPT_P_EXPLICIT_NOTIFY | OPT_P_ECHO | OPT_P_PULL_MODE
-                     | OPT_P_PEER_ID | OPT_P_NCP | OPT_P_PUSH_MTU | OPT_P_ROUTE | OPT_P_DHCPDNS;
+                     | OPT_P_PEER_ID | OPT_P_NCP | OPT_P_PUSH_MTU | OPT_P_ROUTE | OPT_P_DHCPDNS
+                     | OPT_P_ACC;
     return flags;
 }
 
diff --git a/tests/unit_tests/openvpn/test_acc.c b/tests/unit_tests/openvpn/test_acc.c
new file mode 100644
index 0000000..2b3e2dc
--- /dev/null
+++ b/tests/unit_tests/openvpn/test_acc.c
@@ -0,0 +1,236 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2025 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2025 Arne Schwabe <[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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "syshead.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+#include "acc.h"
+#include "test_acc.h"
+
+#include "mock_msg.h"
+
+/* Mock functions */
+bool
+send_control_channel_string_dowork(struct tls_session *session,
+                                   const char *str, int msglevel)
+{
+    assert_false(true);
+    return false;
+}
+
+void
+send_push_reply_auth_token(struct tls_multi *multi)
+{
+}
+
+void
+auth_set_client_reason(struct tls_multi *multi, const char *reason)
+{
+}
+
+#define MSG_PAYLOAD "FgMBARkBAAEVAwNanUalkEDDxLBpZw9JnjI3tBM"                \
+                    "r4engpg/cDgS48DMaxyCdbzofNjQJU9Y0DD66z/notFT0cF1d60nTS" \
+                    "4md4OJwoQAwEwITAxMBwCzAMACfzKnMqMyqwCvALwCewCTAKABrwCP" \
+                    "AJwBnwArAFAA5wAnAEwAzAQAAnP8BAAEAAAsABAMAAQIACgAWABQAH" \
+                    "QAXAB4AGQAYAQABAQECAQMBBAAWAAAAFwAAAA0AMAAuBAMFAwYDCAc" \
+                    "ICAgaCBsIHAgJCAoICwgECAUIBgQBBQEGAQMDAwEDAgQCBQIGAgArA" \
+                    "AUEAwQDAwAtAAIBAQAzACYAJAAdACCk4Tz2PB/2FsxOfOIV4gn/NyJ" \
+                    "93rQwityP9eKSijXgcQ=="
+
+void
+test_acc_parse_client_messages(void **state)
+{
+    /* ensure that the messages get to mock_msg */
+    x_debug_level = 3;
+    management = (void *)0x12345678;
+    struct gc_arena gc = gc_new();
+
+    /* contains a TLS 1.3 ClientHello */
+    const char msg[] = "ACC,cck1,384,6," MSG_PAYLOAD;
+
+    struct buffer msg_buf;
+    struct context c = { 0 };
+    c.options.mode = MODE_SERVER;
+
+    ALLOC_OBJ_CLEAR_GC(c.c2.tls_multi, struct tls_multi, &gc);
+    ALLOC_OBJ_CLEAR_GC(c.c2.tls_multi, struct tls_multi, &gc);
+
+    c.c2.tls_multi->session[TM_ACTIVE].opt = &c.c2.tls_multi->opt;
+    c.c2.tls_multi->session[TM_ACTIVE].opt->mda_context = &c.c2.mda_context;
+    buf_set_read(&msg_buf, (void *)msg, sizeof(msg));
+    receive_acc_message(&c, &msg_buf);
+
+    assert_string_equal(mock_msg_buf, "Received custom app control message (protocol 'cck1')");
+    assert_string_equal(mock_managment_buf, ">CLIENT:ACC,0,0,cck1,0," MSG_PAYLOAD);
+
+    const char flower_msg[] = "ACC,flower,48,6,SSBhbSAAIEtlcm1pdCD//SB0aGUg77+94oCPZnJvZyEAAA==";
+    buf_set_read(&msg_buf, (void *)flower_msg, sizeof(flower_msg));
+    receive_acc_message(&c, &msg_buf);
+    assert_string_equal(mock_msg_buf, "Received custom app control message (protocol 'flower')");
+    assert_string_equal(mock_managment_buf, ">CLIENT:ACC,0,0,flower,0,SSBhbSAAIEtlcm1pdCD//SB0aGUg77+94oCPZnJvZyEAAA==");
+
+    CLEAR(mock_managment_buf);
+    const char msg_incorrect_len[] = "ACC,fortune,62,6,InsgIm1lIjogImZyb2ciLCAAeGZm/SJtc2ciOiAiSSBhbSAAS2VybWl0IiB9Ig==";
+    buf_set_read(&msg_buf, (void *)msg_incorrect_len, sizeof(msg_incorrect_len));
+    receive_acc_message(&c, &msg_buf);
+    const char *error = "WARNING: Received malformed custom app control channel "
+                        "(field length 62, payload length 64 mismatch) message "
+                        "control message: 496e7367 496d316c 496a6f67 496d5a79 "
+                        "62326369 4c434141 65475a6d 2f534a7[more...]";
+
+    assert_string_equal(mock_msg_buf, error);
+    assert_string_equal(mock_managment_buf, "");
+
+    CLEAR(mock_managment_buf);
+    const char msg_fragment[] = "ACC,power,12,6F,aGVsbG8gdw==";
+    buf_set_read(&msg_buf, (void *)msg_fragment, sizeof(msg_fragment));
+    receive_acc_message(&c, &msg_buf);
+    assert_string_equal(mock_msg_buf, "Received custom app control message (protocol 'power', fragment)");
+    assert_string_equal(mock_managment_buf, ">CLIENT:ACC,0,0,power,1,aGVsbG8gdw==");
+
+    CLEAR(mock_managment_buf);
+    /* check two encoding present */
+    const char flower_msg_two_encs[] = "ACC,flower,48,6A,SSBhbSAAIEtlcm1pdCD//SB0aGUg77+94oCPZnJvZyEAAA==";
+    buf_set_read(&msg_buf, (void *)flower_msg_two_encs, sizeof(flower_msg_two_encs));
+    receive_acc_message(&c, &msg_buf);
+
+    error = "WARNING: Received malformed custom app control channel (number of "
+            "encodings must be exactly one (B64=1, ASCII=1)) message control "
+            "message: 53534268 62534141 4945746c 636d3170 6443442f 2f534230 "
+            "61475567 37372b3[more...]";
+    assert_string_equal(mock_msg_buf, error);
+    assert_string_equal(mock_managment_buf, "");
+
+    /* no encoding present */
+    const char flower_msg_no_encs[] = "ACC,flower,48,,SSBhbSAAIEtlcm1pdCD//SB0aGUg77+94oCPZnJvZyEAAA==";
+    buf_set_read(&msg_buf, (void *)flower_msg_no_encs, sizeof(flower_msg_no_encs));
+    receive_acc_message(&c, &msg_buf);
+
+    error = "WARNING: Received malformed custom app control channel (number of "
+            "encodings must be exactly one (B64=0, ASCII=0)) message control "
+            "message: 53534268 62534141 4945746c 636d3170 6443442f 2f534230 "
+            "61475567 37372b3[more...]";
+    assert_string_equal(mock_msg_buf, error);
+    assert_string_equal(mock_managment_buf, "");
+
+    x_debug_level = 0;
+    management = NULL;
+    gc_free(&gc);
+}
+
+void
+test_parse_acc_parameters(void **state)
+{
+    struct gc_arena gc = gc_new();
+    /* parameters to INFO message */
+    const char *info_acc_msg = "4096 A:6 flower:happy";
+
+    int max_len = 0;
+    const char *protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, true, &gc);
+    assert_string_equal(protocols, "flower:happy");
+    assert_int_equal(max_len, 1280);
+
+    /* a client does not accept a length longer than it supports */
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, false, &gc);
+    assert_null(protocols);
+
+    /* message with lower limit than our, should work for client and server */
+    info_acc_msg = "1111 A:6 flower:happy";
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, false, &gc);
+    assert_string_equal(protocols, "flower:happy");
+    assert_int_equal(max_len, 1111);
+
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, true, &gc);
+    assert_string_equal(protocols, "flower:happy");
+    assert_int_equal(max_len, 1111);
+
+    /* more encodings than supported. Server should be okay, client should fail */
+    info_acc_msg = "1200 A:6:B flower:happy";
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, false, &gc);
+    assert_null(protocols);
+
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, true, &gc);
+    assert_string_equal(protocols, "flower:happy");
+    assert_int_equal(max_len, 1200);
+
+    /* extra parameter */
+    info_acc_msg = "1400 A:6 flower:happy 77";
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, true, &gc);
+    assert_null(protocols);
+
+    protocols = parse_acc_parameters(info_acc_msg, " ", &max_len, false, &gc);
+    assert_null(protocols);
+
+
+    const char *iv_acc_capabilities = "1400,A:6,flower:happy:power";
+    protocols = parse_acc_parameters(iv_acc_capabilities, ",", &max_len, true, &gc);
+    assert_string_equal(protocols, "flower:happy:power");
+
+    /* too small protocol length */
+    info_acc_msg = "32 A:6 flower:happy";
+    protocols = parse_acc_parameters(info_acc_msg, ",", &max_len, true, &gc);
+    assert_null(protocols);
+
+    /* non-numerical protocol length */
+    info_acc_msg = "abff A:6 flower:happy 77";
+    protocols = parse_acc_parameters(info_acc_msg, ",", &max_len, true, &gc);
+    assert_null(protocols);
+
+    /* non-numerical protocol length */
+    info_acc_msg = "abff A:6 flower:happy 77";
+    protocols = parse_acc_parameters(info_acc_msg, ",", &max_len, true, &gc);
+    assert_null(protocols);
+
+    gc_free(&gc);
+}
+
+void
+test_acc_common_protocols(void **state)
+{
+    struct context c = { 0 };
+    c.options.gc = gc_new();
+
+    ALLOC_OBJ_CLEAR_GC(c.c2.tls_multi, struct tls_multi, &c.options.gc);
+
+    c.c2.tls_multi->peer_info = "IV_PROTO=4321\nIV_ACC=1400,A:6,flower:happy:power";
+    c.options.acc_protocols = "power:flower:star";
+
+    determine_common_acc_protocols(&c);
+
+    assert_int_equal(c.options.app_custom_protocol_len, ACC_MAX_MSG_LEN);
+    assert_string_equal(c.options.acc_negotiated_protocols, "flower:power");
+
+    gc_free(&c.options.gc);
+}
\ No newline at end of file
diff --git a/tests/unit_tests/openvpn/test_acc.h b/tests/unit_tests/openvpn/test_acc.h
new file mode 100644
index 0000000..11ecf32
--- /dev/null
+++ b/tests/unit_tests/openvpn/test_acc.h
@@ -0,0 +1,33 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2025 OpenVPN Inc <[email protected]>
+ *  Copyright (C) 2025 Arne Schwabe <[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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef TEST_ACC_H
+#define TEST_ACC_H
+void test_acc_parse_client_messages(void **state);
+
+void test_parse_acc_parameters(void **state);
+
+void test_acc_common_protocols(void **state);
+#endif
diff --git a/tests/unit_tests/openvpn/test_misc.c b/tests/unit_tests/openvpn/test_misc.c
index 5f03860..18e4356 100644
--- a/tests/unit_tests/openvpn/test_misc.c
+++ b/tests/unit_tests/openvpn/test_misc.c
@@ -40,11 +40,35 @@
 #include "list.h"
 #include "mock_msg.h"
 #include "crypto.h"
+#include "test_schedule.h"
+#include "mock_management.h"
+#include "multi.h"
+
 #ifdef _WIN32
 #include "win32-util.h"
 #endif
-#include "test_schedule.h"
 
+/* extra mocks */
+bool
+apply_push_options(struct context *c, struct options *options, struct buffer *buf,
+                   uint64_t permission_mask, uint64_t *option_types_found,
+                   struct env_set *es, bool is_update)
+{
+    return true;
+}
+
+bool
+send_control_channel_string(struct context *c, const char *str, msglvl_t msglevel)
+{
+    check_expected_ptr(str);
+    return true;
+}
+
+struct multi_instance *
+lookup_by_cid(struct multi_context *m, const unsigned long cid)
+{
+    return *(m->instances);
+}
 
 static void
 test_compat_lzo_string(void **state)
@@ -492,5 +516,8 @@
 main(void)
 {
     openvpn_unit_test_setup();
-    return cmocka_run_group_tests(misc_tests, NULL, NULL);
+    init_mock_management();
+    int ret = cmocka_run_group_tests(misc_tests, NULL, NULL);
+    uninit_mock_management();
+    return ret;
 }
diff --git a/tests/unit_tests/openvpn/test_ssl.c b/tests/unit_tests/openvpn/test_ssl.c
index d6b63d0..b3889cd 100644
--- a/tests/unit_tests/openvpn/test_ssl.c
+++ b/tests/unit_tests/openvpn/test_ssl.c
@@ -52,6 +52,8 @@
 #include "ssl_verify.h"
 #include "openvpn.h"
 
+#include "test_acc.h"
+
 /* Mock function to be allowed to include win32.c which is required for
  * getting the temp directory */
 #ifdef _WIN32
@@ -87,6 +89,16 @@
     return;
 }
 
+/* Define a dummy dco cipher option to avoid linking against all the DCO
+ * units */
+#if defined(ENABLE_DCO)
+const char *
+dco_get_supported_ciphers(void)
+{
+    return "AES-192-GCM:AES-128-CBC:AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305";
+}
+#endif
+
 /* generated using
  * openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -keyout - \
  * -noenc -sha256 -days 3650 -subj '/CN=ovpn-test-secp384r1'  -nodes \
@@ -960,8 +972,10 @@
         cmocka_unit_test(test_data_channel_known_vectors_epoch),
         cmocka_unit_test(test_data_channel_known_vectors_shortpktid),
         cmocka_unit_test(crypto_test_print_cert_details),
-        cmocka_unit_test(ssl_test_extract_peer_info)
-
+        cmocka_unit_test(ssl_test_extract_peer_info),
+        cmocka_unit_test(test_acc_parse_client_messages),
+        cmocka_unit_test(test_parse_acc_parameters),
+        cmocka_unit_test(test_acc_common_protocols)
     };
 
 #if defined(ENABLE_CRYPTO_OPENSSL)

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

Gerrit-MessageType: newpatchset
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: I4a6a6ef41e15896b8b79d795b2a6de86b8ae4c84
Gerrit-Change-Number: 1858
Gerrit-PatchSet: 3
Gerrit-Owner: plaisthos <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>

_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.