[Openvpn-devel,v2] Add demo plugin that excercises "CLIENT_CONNECT" and "CLIENT_CONNECT_V2" paths

Message ID 20200811104401.31075-1-gert@greenie.muc.de
State Superseded
Headers show
Series [Openvpn-devel,v2] Add demo plugin that excercises "CLIENT_CONNECT" and "CLIENT_CONNECT_V2" paths | expand

Commit Message

Gert Doering Aug. 11, 2020, 12:44 a.m. UTC
This is a new "samples" plugin which does not do many useful things,
besides
 - show how a plugin is programmed
 - how the various messages get dispatched
 - how to pass back information from a client-connect/v2 plugin
 - how to do async-cc plugins  [not yet implemented]

the operation of the plugin is controlled by UV_WANT_* environment variables
controlled by the client ("--setenv UV_WANT_CC_FAIL 1 --push-peer-info"),
to "fail CLIENT_CONNECT" or "use async-cc for CLIENT_CONNECT_V2" or
"send 'disable' back from ...") - which is useful for automated testing
of server success/defer/fail code paths for the CLIENT_CONNECT_* functions.

See samples/sample-plugins/client-connect/README for details how to do this.

v2:
  - implement async / deferred operation both for CLIENT_CONNECT and
    CLIENT_CONNECT_V2 plugin calls
  - implement returning openvpn-controlled (setenv) config snippets
    (so the client side can verify in automated testing that the plugin
    operated correctly, without hard-coding something in the plugin code)
---
 sample/sample-plugins/client-connect/Makefile |  16 +
 sample/sample-plugins/client-connect/README   |  34 +
 .../client-connect/sample-client-connect.c    | 609 ++++++++++++++++++
 3 files changed, 659 insertions(+)
 create mode 100644 sample/sample-plugins/client-connect/Makefile
 create mode 100644 sample/sample-plugins/client-connect/README
 create mode 100644 sample/sample-plugins/client-connect/sample-client-connect.c

Comments

David Sommerseth Sept. 11, 2020, 9:03 a.m. UTC | #1
On 11/08/2020 12:44, Gert Doering wrote:
> This is a new "samples" plugin which does not do many useful things,
> besides
>  - show how a plugin is programmed
>  - how the various messages get dispatched
>  - how to pass back information from a client-connect/v2 plugin
>  - how to do async-cc plugins  [not yet implemented]
> 
> the operation of the plugin is controlled by UV_WANT_* environment variables
> controlled by the client ("--setenv UV_WANT_CC_FAIL 1 --push-peer-info"),
> to "fail CLIENT_CONNECT" or "use async-cc for CLIENT_CONNECT_V2" or
> "send 'disable' back from ...") - which is useful for automated testing
> of server success/defer/fail code paths for the CLIENT_CONNECT_* functions.
> 
> See samples/sample-plugins/client-connect/README for details how to do this.
> 
> v2:
>   - implement async / deferred operation both for CLIENT_CONNECT and
>     CLIENT_CONNECT_V2 plugin calls
>   - implement returning openvpn-controlled (setenv) config snippets
>     (so the client side can verify in automated testing that the plugin
>     operated correctly, without hard-coding something in the plugin code)
> ---
>  sample/sample-plugins/client-connect/Makefile |  16 +
>  sample/sample-plugins/client-connect/README   |  34 +
>  .../client-connect/sample-client-connect.c    | 609 ++++++++++++++++++
>  3 files changed, 659 insertions(+)
>  create mode 100644 sample/sample-plugins/client-connect/Makefile
>  create mode 100644 sample/sample-plugins/client-connect/README
>  create mode 100644 sample/sample-plugins/client-connect/sample-client-connect.c
> 
> diff --git a/sample/sample-plugins/client-connect/Makefile b/sample/sample-plugins/client-connect/Makefile
> new file mode 100644
> index 00000000..ff187c43
> --- /dev/null
> +++ b/sample/sample-plugins/client-connect/Makefile
> @@ -0,0 +1,16 @@
> +all: sample-client-connect.so
> +
> +
> +sample-client-connect.o: sample-client-connect.c
> +
> +# This directory is where we will look for openvpn-plugin.h
> +CPPFLAGS=-I../../../include
> +
> +CC=gcc

If you don't set this; CC will normally be 'cc', which is normally linked to
gcc on Linux systems.

> +CFLAGS=-O2 -Wall -Wno-unused-variable -g

I would probably skip the -Wno-unused-variable and rather not declare unused
variables.

[...snip...]

> --- /dev/null
> +++ b/sample/sample-plugins/client-connect/sample-client-connect.c

[...snip...]

> +/* use v3 functions so we can use openvpn's logging and base64 etc. */
> +OPENVPN_EXPORT int
> +openvpn_plugin_open_v3(const int v3structver,
> +                       struct openvpn_plugin_args_open_in const *args,
> +                       struct openvpn_plugin_args_open_return *ret)
> +{
> +    const char **argv = args->argv;

From a code-style point of view, I think declaring unused variables should be
avoided.  Since this is as an example, we could just comment this line out and
add a remark that we do not parse plug-in arguments in this example.

[...snip...]

> +int
> +openvpn_plugin_client_connect_v2(struct plugin_context *context,
> +                                 struct plugin_per_client_context *pcc,
> +                                 const char **envp,
> +                                 struct openvpn_plugin_string_list **return_list)
> +{

[...snip...]

> +    rl->name = strdup("config");

I'm getting a lot of "warning: implicit declaration of function ‘strdup’;" and
 "warning: assignment to ‘char *’ from ‘int’ makes pointer from integer
without a cast" compiler warning on all of these strdup() calls.

We do use strdup() in the main openvpn code, but that code includes config.h,
which contains #define _GNU_SOURCE 1.  This removes this compiler warning.

This is on RHEL-7 with both gcc-4.8 and gcc-9.3.


Otherwise, the code looks reasonable and it works.  The log file does not
include the pushed echo statement (can be enabled in options.c:5286).  The
management interface shows the pushed echo message.
Gert Doering Sept. 11, 2020, 11:39 a.m. UTC | #2
Hi,

thanks for the review.

On Fri, Sep 11, 2020 at 09:03:43PM +0200, David Sommerseth wrote:
> On 11/08/2020 12:44, Gert Doering wrote:
> > This is a new "samples" plugin which does not do many useful things,
> > besides
[..]
> > +sample-client-connect.o: sample-client-connect.c
> > +
> > +# This directory is where we will look for openvpn-plugin.h
> > +CPPFLAGS=-I../../../include
> > +
> > +CC=gcc
> 
> If you don't set this; CC will normally be 'cc', which is normally linked to
> gcc on Linux systems.
> 
> > +CFLAGS=-O2 -Wall -Wno-unused-variable -g
> 
> I would probably skip the -Wno-unused-variable and rather not declare unused
> variables.

Yeah, not sure how that one slipped in.  I *did* try to make it warning-free,
so, next version is coming :-)

> > +    rl->name = strdup("config");
> 
> I'm getting a lot of "warning: implicit declaration of function ???strdup???;" and
>  "warning: assignment to ???char *??? from ???int??? makes pointer from integer
> without a cast" compiler warning on all of these strdup() calls.
> 
> We do use strdup() in the main openvpn code, but that code includes config.h,
> which contains #define _GNU_SOURCE 1.  This removes this compiler warning.
> 
> This is on RHEL-7 with both gcc-4.8 and gcc-9.3.

Not sure what you are hitting there.  According to "man strdup()", 
inclusion of <string.h> should be fully sufficient, without any extra
declarations.  But maybe it's a -std=... thing to tell GCC what
C language level is desired?

I developed and tested this on a gentoo linux, with gcc-9.2.0, and it's 
*not* throwing strdup() warnings.

> Otherwise, the code looks reasonable and it works.  The log file does not
> include the pushed echo statement (can be enabled in options.c:5286).  The
> management interface shows the pushed echo message.

Yeah, the "echo" directive is sort of weird.  It's blanked out of PUSH
messages as well...  I do wonder what James had in mind here, or what 
AS/Connect are using it for :-)

gert
David Sommerseth Sept. 13, 2020, 11:35 p.m. UTC | #3
On 11/09/2020 23:39, Gert Doering wrote:
[...snip...]
>> I'm getting a lot of "warning: implicit declaration of function ???strdup???;" and
>>  "warning: assignment to ???char *??? from ???int??? makes pointer from integer
>> without a cast" compiler warning on all of these strdup() calls.
>>
>> We do use strdup() in the main openvpn code, but that code includes config.h,
>> which contains #define _GNU_SOURCE 1.  This removes this compiler warning.
>>
>> This is on RHEL-7 with both gcc-4.8 and gcc-9.3.
> 
> Not sure what you are hitting there.  According to "man strdup()", 
> inclusion of <string.h> should be fully sufficient, without any extra
> declarations.  But maybe it's a -std=... thing to tell GCC what
> C language level is desired?

Yes, I read somewhere that -std=c99 changes the behavior.  _POSIX_C_SOURCE is 
not defined with -std=c99 (nor c11), but is defined without -std= defined.
None of the compiler standards adds _XOPEN_SOURCE - and neither _SVID_SOURCE
nor _BSD_SOURCE (which is not really surprising)

> I developed and tested this on a gentoo linux, with gcc-9.2.0, and it's 
> *not* throwing strdup() warnings.

Without -std=c99 I get a clean compilation with GCC-9.3.1 (RHEL-7/devtoolset-9 
SCL).  But on stock RHEL-7 (GCC-4.8.5) this plugin does not compile without 
-std=c99 ...

------------------------
client-connect/sample-client-connect.c: In function ‘openvpn_plugin_client_connect’:
client-connect/sample-client-connect.c:356:9: error: ‘for’ loop initial declarations are only allowed in C99 mode
         for (int i = 0; argv[i]; i++)
------------------------

Patch

diff --git a/sample/sample-plugins/client-connect/Makefile b/sample/sample-plugins/client-connect/Makefile
new file mode 100644
index 00000000..ff187c43
--- /dev/null
+++ b/sample/sample-plugins/client-connect/Makefile
@@ -0,0 +1,16 @@ 
+all: sample-client-connect.so
+
+
+sample-client-connect.o: sample-client-connect.c
+
+# This directory is where we will look for openvpn-plugin.h
+CPPFLAGS=-I../../../include
+
+CC=gcc
+CFLAGS=-O2 -Wall -Wno-unused-variable -g
+
+.c.o:
+	$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -c $<
+
+sample-client-connect.so: sample-client-connect.o
+	$(CC) $(CFLAGS) -fPIC -shared $(LDFLAGS) -Wl,-soname,$@ -o $@ $< -lc
diff --git a/sample/sample-plugins/client-connect/README b/sample/sample-plugins/client-connect/README
new file mode 100644
index 00000000..be6ef947
--- /dev/null
+++ b/sample/sample-plugins/client-connect/README
@@ -0,0 +1,34 @@ 
+OpenVPN plugin examples.
+
+Examples provided:
+
+sample-client-connect.c
+
+  - hook to all plugin hooks that openvpn offers
+  - log which hook got called
+  - on CLIENT_CONNECT or CLIENT_CONNECT_V2 set some config variables
+    (controlled by "setenv plugin_cc_config ..." and "plugin_cc2_config"
+    in openvpn's config)
+
+  - if the environment variable UV_WANT_CC_FAIL is set, fail
+  - if the environment variable UV_WANT_CC_DISABLE is set, reject ("disable")
+  - if the environment variable UV_WANT_CC_ASYNC is set, go to
+    asynchronous/deferred mode on CLIENT_CONNECT, and sleep for
+    ${UV_WANT_CC_ASYNC} seconds
+
+  - if the environment variable UV_WANT_CC2_FAIL is set, fail CC2
+  - if the environment variable UV_WANT_CC2_DISABLE is set, reject ("disable")
+  - if the environment variable UV_WANT_CC2_ASYNC is set, go to
+    asynchronous/deferred mode on CLIENT_CONNECT_V2, and sleep for
+    ${UV_WANT_CC2_ASYNC} seconds
+
+    (this can be client-controlled with --setenv UV_WANT_CC_ASYNC nnn
+     etc. --> for easy testing server code paths)
+
+To build (not very sophisticated right now, needs gcc):
+
+  .../sample-plugins/client-connect$ make (Linux/BSD/etc.)
+
+To use in OpenVPN, add to config file:
+
+  plugin sample-client-connect.so (Linux/BSD/etc.)
diff --git a/sample/sample-plugins/client-connect/sample-client-connect.c b/sample/sample-plugins/client-connect/sample-client-connect.c
new file mode 100644
index 00000000..6e874524
--- /dev/null
+++ b/sample/sample-plugins/client-connect/sample-client-connect.c
@@ -0,0 +1,609 @@ 
+/*
+ *  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) 2002-2018 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  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.
+ */
+
+/*
+ * This file implements a simple OpenVPN plugin module which
+ * will log the calls made, and send back some config statements
+ * when called on the CLIENT_CONNECT and CLIENT_CONNECT_V2 hooks.
+ *
+ * it can be asked to fail or go to async/deferred mode by setting
+ * environment variables (UV_WANT_CC_FAIL, UV_WANT_CC_ASYNC,
+ * UV_WANT_CC2_ASYNC) - mostly used as a testing vehicle for the
+ * server side code to handle these cases
+ *
+ * See the README file for build instructions and env control variables.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/wait.h>
+
+#include "openvpn-plugin.h"
+
+/* Pointers to functions exported from openvpn */
+static plugin_log_t plugin_log = NULL;
+static plugin_secure_memzero_t plugin_secure_memzero = NULL;
+static plugin_base64_decode_t plugin_base64_decode = NULL;
+
+/* module name for plugin_log() */
+static char *MODULE = "sample-cc";
+
+/*
+ * Our context, where we keep our state.
+ */
+
+struct plugin_context {
+    int verb;                           /* logging verbosity */
+};
+
+/* this is used for the CLIENT_CONNECT_V2 async/deferred handler
+ *
+ * the "CLIENT_CONNECT_V2" handler puts per-client information into
+ * this, and the "CLIENT_CONNECT_DEFER_V2" handler looks at it to see
+ * if it's time yet to succeed/fail
+ */
+struct plugin_per_client_context {
+    time_t sleep_until;                 /* wakeup time (time() + sleep) */
+    bool want_fail;
+    bool want_disable;
+    const char *client_config;
+};
+
+/*
+ * Given an environmental variable name, search
+ * the envp array for its value, returning it
+ * if found or NULL otherwise.
+ */
+static const char *
+get_env(const char *name, const char *envp[])
+{
+    if (envp)
+    {
+        int i;
+        const int namelen = strlen(name);
+        for (i = 0; envp[i]; ++i)
+        {
+            if (!strncmp(envp[i], name, namelen))
+            {
+                const char *cp = envp[i] + namelen;
+                if (*cp == '=')
+                {
+                    return cp + 1;
+                }
+            }
+        }
+    }
+    return NULL;
+}
+
+
+static int
+atoi_null0(const char *str)
+{
+    if (str)
+    {
+        return atoi(str);
+    }
+    else
+    {
+        return 0;
+    }
+}
+
+/* use v3 functions so we can use openvpn's logging and base64 etc. */
+OPENVPN_EXPORT int
+openvpn_plugin_open_v3(const int v3structver,
+                       struct openvpn_plugin_args_open_in const *args,
+                       struct openvpn_plugin_args_open_return *ret)
+{
+    const char **argv = args->argv;
+    const char **envp = args->envp;
+
+    /* Check API compatibility -- struct version 5 or higher needed */
+    if (v3structver < 5)
+    {
+        fprintf(stderr, "sample-client-connect: this plugin is incompatible with the running version of OpenVPN\n");
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    /*
+     * Allocate our context
+     */
+    struct plugin_context *context = calloc(1, sizeof(struct plugin_context));
+    if (!context)
+    {
+        goto error;
+    }
+
+    /*
+     * Intercept just about everything...
+     */
+    ret->type_mask =
+        OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_UP)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_DOWN)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_ROUTE_UP)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_IPCHANGE)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_TLS_VERIFY)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT_V2)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_DISCONNECT)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_LEARN_ADDRESS)
+        |OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_TLS_FINAL);
+
+    /* Save global pointers to functions exported from openvpn */
+    plugin_log = args->callbacks->plugin_log;
+    plugin_secure_memzero = args->callbacks->plugin_secure_memzero;
+    plugin_base64_decode = args->callbacks->plugin_base64_decode;
+
+    /*
+     * Get verbosity level from environment
+     */
+    context->verb = atoi_null0(get_env("verb", envp));
+
+    ret->handle = (openvpn_plugin_handle_t *) context;
+    plugin_log(PLOG_NOTE, MODULE, "initialization succeeded");
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;
+
+error:
+    if (context)
+    {
+        free(context);
+    }
+    return OPENVPN_PLUGIN_FUNC_ERROR;
+}
+
+
+/* there are two possible interfaces for an openvpn plugin how
+ * to be called on "client connect", which primarily differ in the
+ * way config options are handed back to the client instance
+ * (see openvpn/multi.c, multi_client_connect_call_plugin_{v1,v2}())
+ *
+ * OPENVPN_PLUGIN_CLIENT_CONNECT
+ *   openvpn creates a temp file and passes the name to the plugin
+ *    (via argv[1] variable, argv[0] is the name of the plugin)
+ *   the plugin can write config statements to that file, and openvpn
+ *    reads it in like a "ccd/$cn" per-client config file
+ *
+ * OPENVPN_PLUGIN_CLIENT_CONNECT_V2
+ *   the caller passes in a pointer to an "openvpn_plugin_string_list"
+ *   (openvpn-plugin.h), which is a linked list of (name,value) pairs
+ *
+ *   we fill in one node with name="config" and value="our config"
+ *
+ *   both "l" and "l->name" and "l->value" are malloc()ed by the plugin
+ *   and free()ed by the caller (openvpn_plugin_string_list_free())
+ */
+
+/* helper function to write actual "here are your options" file,
+ * called from sync and sync handler
+ */
+int
+write_cc_options_file(const char *name, const char **envp)
+{
+    if (!name)
+    {
+        return OPENVPN_PLUGIN_FUNC_SUCCESS;
+    }
+
+    FILE *fp = fopen(name,"w");
+    if (!fp)
+    {
+        plugin_log(PLOG_ERR, MODULE, "fopen('%s') failed", name);
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    /* config to-be-sent can come from "setenv plugin_cc_config" in openvpn */
+    const char *p = get_env("plugin_cc_config", envp);
+    if (p)
+    {
+        fprintf(fp, "%s\n", p);
+    }
+
+    /* some generic config snippets so we know it worked */
+    fprintf(fp, "push \"echo sample-cc plugin 1 called\"\n");
+
+    /* if the caller wants, reject client by means of "disable" option */
+    if (get_env("UV_WANT_CC_DISABLE", envp))
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC_DISABLE, reject");
+        fprintf(fp, "disable\n");
+    }
+    fclose(fp);
+
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;
+}
+
+int
+cc_handle_deferred_v1(int seconds, const char *name, const char **envp)
+{
+    const char *ccd_file = get_env("client_connect_deferred_file", envp);
+    if (!ccd_file)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC_ASYNC=%d, but "
+                   "'client_connect_deferred_file' not set -> fail", seconds);
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    /* the CLIENT_CONNECT (v1) API is a bit tricky to work with, because
+     * completition can be signalled both by the "deferred_file" and by
+     * the new ...CLIENT_CONNECT_DEFER API - which is optional.
+     *
+     * For OpenVPN to be able to differenciate, we must create the file
+     * right away if we want to use that for signalling.
+     */
+    int fd = open(ccd_file, O_WRONLY);
+    if (fd < 0)
+    {
+        plugin_log(PLOG_ERR|PLOG_ERRNO, MODULE, "open('%s') failed", ccd_file);
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    if (write(fd, "2", 1) != 1)
+    {
+        plugin_log(PLOG_ERR|PLOG_ERRNO, MODULE, "write to '%s' failed", ccd_file );
+        close(fd);
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+    close(fd);
+
+    /* we do not want to complicate our lives with having to wait()
+     * for child processes (so they are not zombiefied) *and* we MUST NOT
+     * fiddle with signal handlers (= shared with openvpn main), so
+     * we use double-fork() trick.
+     */
+
+    /* fork, sleep, succeed/fail according to env vars */
+    pid_t p1 = fork();
+    if (p1 < 0)                 /* Fork failed */
+    {
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+    if (p1 > 0)                 /* parent process */
+    {
+        waitpid(p1, NULL, 0);
+        return OPENVPN_PLUGIN_FUNC_DEFERRED;
+    }
+
+    /* first gen child process, fork() again and exit() right away */
+    pid_t p2 = fork();
+    if (p2 < 0)
+    {
+        plugin_log(PLOG_ERR|PLOG_ERRNO, MODULE, "BACKGROUND: fork(2) failed");
+        exit(1);
+    }
+    if (p2 > 0)                 /* new parent: exit right away */
+    {
+        exit(0);
+    }
+
+    /* (grand-)child process
+     *  - never call "return" now (would mess up openvpn)
+     *  - return status is communicated by file
+     *  - then exit()
+     */
+
+    /* do mighty complicated work that will really take time here... */
+    plugin_log(PLOG_NOTE, MODULE, "in async/deferred handler, sleep(%d)", seconds);
+    sleep(seconds);
+
+    /* write config options to openvpn */
+    int ret = write_cc_options_file(name, envp);
+
+    /* by setting "UV_WANT_CC_FAIL" we can be triggered to fail */
+    const char *p = get_env("UV_WANT_CC_FAIL", envp);
+    if (p)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC_FAIL=%s -> fail", p);
+        ret = OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    /* now signal success/failure state to openvpn */
+    fd = open(ccd_file, O_WRONLY);
+    if (fd < 0)
+    {
+        plugin_log(PLOG_ERR|PLOG_ERRNO, MODULE, "open('%s') failed", ccd_file);
+        exit(1);
+    }
+
+    plugin_log(PLOG_NOTE, MODULE, "cc_handle_deferred_v1: done, signalling %s",
+               (ret == OPENVPN_PLUGIN_FUNC_SUCCESS) ? "success" : "fail" );
+
+    if (write(fd, (ret == OPENVPN_PLUGIN_FUNC_SUCCESS) ? "1" : "0", 1) != 1)
+    {
+        plugin_log(PLOG_ERR|PLOG_ERRNO, MODULE, "write to '%s' failed", ccd_file );
+    }
+    close(fd);
+
+    exit(0);
+}
+
+int
+openvpn_plugin_client_connect(struct plugin_context *context,
+                              const char **argv,
+                              const char **envp)
+{
+    /* log environment variables handed to us by OpenVPN, but
+     * only if "setenv verb" is 3 or higher (arbitrary number)
+     */
+    if (context->verb>=3)
+    {
+        for (int i = 0; argv[i]; i++)
+        {
+            plugin_log(PLOG_NOTE, MODULE, "per-client argv: %s", argv[i]);
+        }
+        for (int i = 0; envp[i]; i++)
+        {
+            plugin_log(PLOG_NOTE, MODULE, "per-client env: %s", envp[i]);
+        }
+    }
+
+    /* by setting "UV_WANT_CC_ASYNC" we go to async/deferred mode */
+    const char *p = get_env("UV_WANT_CC_ASYNC", envp);
+    if (p)
+    {
+        /* the return value will usually be OPENVPN_PLUGIN_FUNC_DEFERRED
+         * ("I will do my job in the background, check the status file!")
+         * but depending on env setup it might be "..._ERRROR"
+         */
+        return cc_handle_deferred_v1(atoi(p), argv[1], envp);
+    }
+
+    /* -- this is synchronous mode (openvpn waits for us) -- */
+
+    /* by setting "UV_WANT_CC_FAIL" we can be triggered to fail */
+    p = get_env("UV_WANT_CC_FAIL", envp);
+    if (p)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC_FAIL=%s -> fail", p);
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    /* does the caller want options?  give them some */
+    int ret = write_cc_options_file(argv[1], envp);
+
+    return ret;
+}
+
+int
+openvpn_plugin_client_connect_v2(struct plugin_context *context,
+                                 struct plugin_per_client_context *pcc,
+                                 const char **envp,
+                                 struct openvpn_plugin_string_list **return_list)
+{
+    /* by setting "UV_WANT_CC2_ASYNC" we go to async/deferred mode */
+    const char *want_async = get_env("UV_WANT_CC2_ASYNC", envp);
+    const char *want_fail = get_env("UV_WANT_CC2_FAIL", envp);
+    const char *want_disable = get_env("UV_WANT_CC2_DISABLE", envp);
+
+    /* config to push towards client - can be controlled by OpenVPN
+     * config ("setenv plugin_cc2_config ...") - mostly useful in a
+     * regression test environment to push stuff like routes which are
+     * then verified by t_client ping tests
+     */
+    const char *client_config = get_env("plugin_cc2_config", envp);
+    if (!client_config)
+    {
+        /* pick something meaningless which can be verified in client log */
+        client_config = "push \"setenv CC2 MOOH\"\n";
+    }
+
+    if (want_async)
+    {
+        /* we do no really useful work here, so we just tell the
+         * "CLIENT_CONNECT_DEFER_V2" handler that it should sleep
+         * and then "do things" via the per-client-context
+         */
+        pcc->sleep_until = time(NULL) + atoi(want_async);
+        pcc->want_fail = (want_fail != NULL);
+        pcc->want_disable = (want_disable != NULL);
+        pcc->client_config = client_config;
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC2_ASYNC=%s -> set up deferred handler", want_async);
+        return OPENVPN_PLUGIN_FUNC_DEFERRED;
+    }
+
+    /* by setting "UV_WANT_CC2_FAIL" we can be triggered to fail here */
+    if (want_fail)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC2_FAIL=%s -> fail", want_fail);
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    struct openvpn_plugin_string_list *rl =
+        calloc(1, sizeof(struct openvpn_plugin_string_list));
+    if (!rl)
+    {
+        plugin_log(PLOG_ERR, MODULE, "malloc(return_list) failed");
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+    rl->name = strdup("config");
+    if (want_disable)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC2_DISABLE, reject");
+        rl->value = strdup("disable\n");
+    }
+    else
+    {
+        rl->value = strdup(client_config);
+    }
+
+    if (!rl->name || !rl->value)
+    {
+        plugin_log(PLOG_ERR, MODULE, "malloc(return_list->xx) failed");
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    *return_list = rl;
+
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;
+}
+
+int
+openvpn_plugin_client_connect_defer_v2(struct plugin_context *context,
+                                       struct plugin_per_client_context *pcc,
+                                       struct openvpn_plugin_string_list
+                                       **return_list)
+{
+    time_t time_left = pcc->sleep_until - time(NULL);
+    plugin_log(PLOG_NOTE, MODULE, "defer_v2: seconds left=%d",
+               (int) time_left);
+
+    /* not yet due? */
+    if (time_left > 0)
+    {
+        return OPENVPN_PLUGIN_FUNC_DEFERRED;
+    }
+
+    /* client wants fail? */
+    if (pcc->want_fail)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC2_FAIL -> fail" );
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    /* fill in RL according to with-disable / without-disable */
+
+    /* TODO: unify this with non-deferred case */
+    struct openvpn_plugin_string_list *rl =
+        calloc(1, sizeof(struct openvpn_plugin_string_list));
+    if (!rl)
+    {
+        plugin_log(PLOG_ERR, MODULE, "malloc(return_list) failed");
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+    rl->name = strdup("config");
+    if (pcc->want_disable)
+    {
+        plugin_log(PLOG_NOTE, MODULE, "env has UV_WANT_CC2_DISABLE, reject");
+        rl->value = strdup("disable\n");
+    }
+    else
+    {
+        rl->value = strdup(pcc->client_config);
+    }
+
+    if (!rl->name || !rl->value)
+    {
+        plugin_log(PLOG_ERR, MODULE, "malloc(return_list->xx) failed");
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    *return_list = rl;
+
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;
+}
+
+OPENVPN_EXPORT int
+openvpn_plugin_func_v2(openvpn_plugin_handle_t handle,
+                       const int type,
+                       const char *argv[],
+                       const char *envp[],
+                       void *per_client_context,
+                       struct openvpn_plugin_string_list **return_list)
+{
+    struct plugin_context *context = (struct plugin_context *) handle;
+    struct plugin_per_client_context *pcc = (struct plugin_per_client_context *) per_client_context;
+
+    /* for most functions, we just "don't do anything" but log the
+     * event received (so one can follow it in the log and understand
+     * the sequence of events).  CONNECT and CONNECT_V2 are handled
+     */
+    switch (type)
+    {
+        case OPENVPN_PLUGIN_UP:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_UP");
+            break;
+
+        case OPENVPN_PLUGIN_DOWN:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_DOWN");
+            break;
+
+        case OPENVPN_PLUGIN_ROUTE_UP:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_ROUTE_UP");
+            break;
+
+        case OPENVPN_PLUGIN_IPCHANGE:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_IPCHANGE");
+            break;
+
+        case OPENVPN_PLUGIN_TLS_VERIFY:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_TLS_VERIFY");
+            break;
+
+        case OPENVPN_PLUGIN_CLIENT_CONNECT:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_CLIENT_CONNECT");
+            return openvpn_plugin_client_connect(context, argv, envp);
+
+        case OPENVPN_PLUGIN_CLIENT_CONNECT_V2:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_CLIENT_CONNECT_V2");
+            return openvpn_plugin_client_connect_v2(context, pcc, envp,
+                                                    return_list);
+
+        case OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2");
+            return openvpn_plugin_client_connect_defer_v2(context, pcc,
+                                                          return_list);
+
+        case OPENVPN_PLUGIN_CLIENT_DISCONNECT:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_CLIENT_DISCONNECT");
+            break;
+
+        case OPENVPN_PLUGIN_LEARN_ADDRESS:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_LEARN_ADDRESS");
+            break;
+
+        case OPENVPN_PLUGIN_TLS_FINAL:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_TLS_FINAL");
+            break;
+
+        default:
+            plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_? type=%d\n", type);
+    }
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;
+}
+
+OPENVPN_EXPORT void *
+openvpn_plugin_client_constructor_v1(openvpn_plugin_handle_t handle)
+{
+    printf("FUNC: openvpn_plugin_client_constructor_v1\n");
+    return calloc(1, sizeof(struct plugin_per_client_context));
+}
+
+OPENVPN_EXPORT void
+openvpn_plugin_client_destructor_v1(openvpn_plugin_handle_t handle, void *per_client_context)
+{
+    printf("FUNC: openvpn_plugin_client_destructor_v1\n");
+    free(per_client_context);
+}
+
+OPENVPN_EXPORT void
+openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
+{
+    struct plugin_context *context = (struct plugin_context *) handle;
+    printf("FUNC: openvpn_plugin_close_v1\n");
+    free(context);
+}