Attention is currently required from: flichtenheld.
Hello flichtenheld,
I'd like you to reexamine a change. Please visit
http://gerrit.openvpn.net/c/openvpn/+/1727?usp=email
to look at the new patch set (#3).
Change subject: Add lookup of multi session by session id
......................................................................
Add lookup of multi session by session id
This refactors the way that we lookup control channel packets for UDP
packets from other peers. Instead of looking them up by their source
IP address, we lookup the session ids instead.
It also has the consequence that we can have multiple ongoing
sessions from the same source IP address and the new session will
go through all the connect steps like an initial session.
The check if the new session can take over the old session's IP
is now also the same as for floating.
This eliminates a whole class of bugs that we currently have that
break connection if the reconnecting client has different
capabilities as the setup and negotiation is inherited from the
previous client currently. Currently there is at least one bug
regarding the dynamic tls-crypt in this situation.
This also changes the user visible behaviour for clients
reconnecting from the same IP and port. They are now almost
behaving like clients that reconnect from a different IP
address. These now do the whole renegotiation and run
connect scripts/plugins and all the things are normally
skipped when reconnecting from the same IP and port.
The only difference is that duplicate-cn does not
allow both connection.
This will also eventually allow us to get rid of TM_INITIAL
slot as we now do no longer need to keep an ongoing and a
new session anymore. Currently the p2p mode still needs
the extra session slot for the new session so we cannot
remove it just yet.
This now allows a multiple pending session from the
same source IP and port. Previously a client would need
to use a different ports to create multiple pending
session. This change does not make it really easier to
exhaust all pending session than before.
Change-Id: Idb59ecd119331b198792ad1379bec8600211651b
Signed-off-by: Arne Schwabe <[email protected]>
---
M CMakeLists.txt
M doc/man-sections/advanced-options.rst
M src/openvpn/mtcp.c
M src/openvpn/mudp.c
M src/openvpn/multi.c
M src/openvpn/multi.h
M src/openvpn/options.c
M src/openvpn/options.h
A src/openvpn/sid_hash.h
M tests/unit_tests/openvpn/Makefile.am
M tests/unit_tests/openvpn/test_misc.c
11 files changed, 255 insertions(+), 59 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/27/1727/3
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 412fa06..0a04fcb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -832,6 +832,7 @@
src/openvpn/siphash.c
src/openvpn/siphash_reference.c
src/openvpn/siphash_openssl.c
+ src/openvpn/session_id.c
)
target_sources(test_ncp PRIVATE
diff --git a/doc/man-sections/advanced-options.rst
b/doc/man-sections/advanced-options.rst
index 3eff3085..21a0191 100644
--- a/doc/man-sections/advanced-options.rst
+++ b/doc/man-sections/advanced-options.rst
@@ -29,16 +29,18 @@
--hash-size args
Set the size of the real address hash table to ``r`` and the virtual
- address table to ``v``.
+ address table to ``v``. And if specified the session id hash table to
+ ``s``. Otherwise ``s`` be set to the same value as ``r``.
Valid syntax:
::
- hash-size r v
+ hash-size r v [s]
- By default, both tables are sized at 4 times ``--max-clients`` buckets.
+ By default, all three tables are sized at 4 times ``--max-clients`` buckets.
With the default of 1024 of ``--max-clients`` this gives 4096 buckets.
+
--bcast-buffers n
Allocate ``n`` buffers for broadcast datagrams (default :code:`256`).
diff --git a/src/openvpn/mtcp.c b/src/openvpn/mtcp.c
index f000283..eb4e944 100644
--- a/src/openvpn/mtcp.c
+++ b/src/openvpn/mtcp.c
@@ -42,36 +42,12 @@
{
struct gc_arena gc = gc_new();
struct multi_instance *mi = NULL;
- struct hash *hash = m->hash;
mi = multi_create_instance(m, NULL, sock);
if (mi)
{
mi->real.proto = sock->info.proto;
- struct hash_element *he;
- const uint64_t hv = hash_value(hash, &mi->real);
- struct hash_bucket *bucket = hash_bucket(hash, hv);
-
multi_assign_peer_id(m, mi);
-
- he = hash_lookup_fast(hash, bucket, &mi->real, hv);
-
- if (he)
- {
- struct multi_instance *oldmi = (struct multi_instance *)he->value;
- msg(D_MULTI_LOW,
- "MULTI TCP: new incoming client address matches existing
client address -- new client takes precedence");
- oldmi->did_real_hash = false;
- multi_close_instance(m, oldmi, false);
- he->key = &mi->real;
- he->value = mi;
- }
- else
- {
- hash_add_fast(hash, bucket, &mi->real, hv, mi);
- }
-
- mi->did_real_hash = true;
}
#ifdef ENABLE_DEBUG
diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
index 128afbb..288366f 100644
--- a/src/openvpn/mudp.c
+++ b/src/openvpn/mudp.c
@@ -32,6 +32,7 @@
#include "memdbg.h"
#include "ssl_pkt.h"
+#include "sid_hash.h"
#ifdef HAVE_SYS_INOTIFY_H
#include <sys/inotify.h>
@@ -180,7 +181,7 @@
}
else
{
- msg(D_MULTI_DEBUG,
+ msg(D_MULTI_MEDIUM,
"Valid packet (%s) with HMAC challenge from peer (%s), "
"accepting new connection.",
packet_opcode_name(op), peer);
@@ -206,7 +207,6 @@
struct link_socket *sock,
struct mroute_addr *real)
{
- struct hash *hash = m->hash;
struct tls_pre_decrypt_state state = { 0 };
struct multi_instance *mi = NULL;
struct gc_arena gc = gc_new();
@@ -234,11 +234,6 @@
mi = multi_create_instance(m, real, sock);
if (mi)
{
- const uint64_t hv = hash_value(hash, real);
- struct hash_bucket *bucket = hash_bucket(hash, hv);
- hash_add_fast(hash, bucket, &mi->real, hv, mi);
-
- mi->did_real_hash = true;
multi_assign_peer_id(m, mi);
/* If we have a session id already, ensure that the
@@ -250,6 +245,7 @@
struct tls_session *session =
&mi->context.c2.tls_multi->session[TM_INITIAL];
session_skip_to_pre_start(session, &state,
&m->top.c2.from);
+ multi_hash_sid_add(m, &state.peer_session_id, mi);
}
}
}
@@ -272,7 +268,7 @@
* @return instance matching the address, NULL otherwise
*/
static struct multi_instance *
-multi_get_instance_udp_real(struct multi_context *m, struct mroute_addr *real)
+multi_get_instance_udp_real(struct multi_context *m,struct mroute_addr *real)
{
struct hash *hash = m->hash;
struct hash_element *he;
@@ -286,15 +282,26 @@
return NULL;
}
-struct multi_instance *
-multi_get_instance_udp_control(struct multi_context *m, struct link_socket
*sock)
-{
- struct mroute_addr real = { 0 };
- real.proto = sock->info.proto;
- if (mroute_extract_openvpn_sockaddr(&real, &m->top.c2.from.dest, true) &&
m->top.c2.buf.len > 0)
+static struct multi_instance *
+multi_get_instance_udp_control(struct multi_context *m)
+{
+ /* Copy buffer, to a tmp buffer, so that reading the sesison does not
+ * modify the internal pointers */
+ struct buffer tmp = m->top.c2.buf;
+
+ /* op code */
+ uint8_t op = (uint8_t)buf_read_u8(&tmp) >> P_OPCODE_SHIFT;
+ (void)op;
+
+ struct session_id sid = { 0 };
+ session_id_read(&sid, &tmp);
+
+ struct hash_element *he_sid = multi_hash_sid_lookup(m, &sid);
+
+ if (he_sid)
{
- return multi_get_instance_real_udp_real(m, &real);
+ return he_sid->value;
}
return NULL;
@@ -373,7 +380,14 @@
}
else
{
- mi = multi_get_instance_udp_control(m, sock);
+ if (m->top.c2.buf.len < 9)
+ {
+ /* control packets must be at least the opcode byte + session id
+ * (8 byte) long, otherwise they are not valid packets */
+ return NULL;
+ }
+
+ mi = multi_get_instance_udp_control(m);
/* we have no existing multi instance for this connection, control
* packets can create a session. Data packets cannot */
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 78c4693..b870d89 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -23,6 +23,7 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
+#include "sid_hash.h"
#include "siphash.h"
#ifdef HAVE_SYS_INOTIFY_H
@@ -272,8 +273,8 @@
struct multi_context *m = t->multi;
int dev = DEV_TYPE_UNDEF;
- msg(D_MULTI_LOW, "MULTI: multi_init called, r=%d v=%d",
t->options.real_hash_size,
- t->options.virtual_hash_size);
+ msg(D_MULTI_LOW, "MULTI: multi_init called, r=%d v=%d s=%d",
t->options.real_hash_size,
+ t->options.virtual_hash_size, t->options.sid_hash_size);
/*
* Get tun/tap/null device type
@@ -301,6 +302,12 @@
m->vhash = hash_init(t->options.virtual_hash_size, siphash_new_context(),
siphash_free_context,
mroute_addr_hash_function,
mroute_addr_compare_function);
+ /*
+ * Peer session id hash table. Used to lookup a session by the session
+ * id of one of its active sessions */
+ m->sid_hash = hash_init(t->options.sid_hash_size, siphash_new_context(),
siphash_free_context,
+ session_id_hash_function, session_id_hash_equal);
+
#ifdef ENABLE_MANAGEMENT
m->cid_hash = hash_init(t->options.real_hash_size, NULL, free,
cid_hash_function, cid_compare_function);
#endif
@@ -431,8 +438,7 @@
buf_printf(&out, "%s/", cn);
}
buf_printf(&out, "%s", mroute_addr_print(&mi->real, gc));
- if (mi->context.c2.tls_multi && check_debug_level(D_DCO_DEBUG)
- && dco_enabled(&mi->context.options))
+ if (mi->context.c2.tls_multi && true)
{
buf_printf(&out, " rx-peer-id=%d",
mi->context.c2.tls_multi->rx_peer_id);
}
@@ -602,6 +608,12 @@
}
#endif
+
+ if (session_id_defined(&mi->sid_hashed_value))
+ {
+ multi_hash_sid_remove(m, &mi->sid_hashed_value);
+ }
+
if (mi->context.c2.tls_multi->rx_peer_id != MAX_PEER_ID)
{
m->instances[mi->context.c2.tls_multi->rx_peer_id] = NULL;
@@ -676,6 +688,7 @@
hash_free(m->hash);
hash_free(m->vhash);
+ hash_free(m->sid_hash);
#ifdef ENABLE_MANAGEMENT
hash_free(m->cid_hash);
#endif
@@ -2461,6 +2474,34 @@
multi_client_connect_setenv(mi);
}
+static bool
+multi_check_dest_addr_allowed(struct multi_context *m, struct multi_instance
*mi, struct mroute_addr *real);
+
+/**
+ * This sets up the client real address (outer tunnel addr) in the
+ * hash map for data channel packet. If the address is already taken
+ * this steps fails
+ */
+static enum client_connect_return
+multi_client_connect_real_addr(struct multi_context *m, struct multi_instance
*mi,
+ bool deferred, uint64_t *option_types_found)
+{
+ /* If the address is already taken up by another client we fail the new
+ * connection */
+ if (!multi_check_dest_addr_allowed(m, mi, &mi->real))
+ {
+ msg(D_MULTI_ERRORS,
+ "MULTI: client IP address and port already assigned to another "
+ "client, terminating connection");
+ return CC_RET_FAILED;
+ }
+
+ ASSERT(!mi->did_real_hash);
+ ASSERT(hash_add(m->hash, &mi->real, mi, false));
+ mi->did_real_hash = true;
+ return CC_RET_SUCCEEDED;
+}
+
/**
* Do the necessary modification for doing the compress migrate. This is
* implemented as a connect handler as it fits the modify config for a client
@@ -2555,6 +2596,7 @@
uint64_t *option_types_found);
static const multi_client_connect_handler client_connect_handlers[] = {
+ multi_client_connect_real_addr,
multi_client_connect_compress_migrate,
multi_client_connect_source_ccd,
multi_client_connect_call_plugin_v1,
@@ -2623,6 +2665,7 @@
}
return true;
}
+
/*
* Called as soon as the SSL/TLS connection is authenticated.
*
@@ -3109,7 +3152,7 @@
/* do not allow if target address is taken by client with another cert */
if (!cert_hash_compare(m1->locked_cert_hash_set, m2->locked_cert_hash_set))
{
- msg(D_MULTI_LOW, "Disallow float to an address taken by another client
%s",
+ msg(D_MULTI_LOW, "Disallow float/connect to an address taken by
another client %s",
multi_instance_string(ex_mi, false, &gc));
mi->context.c2.buf.len = 0;
@@ -3122,7 +3165,7 @@
if (!m1->locked_username || !m2->locked_username
|| strcmp(m1->locked_username, m2->locked_username) != 0)
{
- msg(D_MULTI_LOW, "Disallow float to an address taken by another
client %s",
+ msg(D_MULTI_LOW, "Disallow float/connect to an address taken by
another client %s",
multi_instance_string(ex_mi, false, &gc));
goto done;
}
diff --git a/src/openvpn/multi.h b/src/openvpn/multi.h
index 8b837fa..10af816 100644
--- a/src/openvpn/multi.h
+++ b/src/openvpn/multi.h
@@ -134,6 +134,10 @@
bool did_real_hash;
#ifdef ENABLE_MANAGEMENT
bool did_cid_hash;
+ /* If this is multi_insance is hashed in the sid lookup table the session
+ * id here is non-null and the hash map's key pointer uses is pointing
+ * here */
+ struct session_id sid_hashed_value;
struct buffer_list *cc_config;
#endif
bool did_iroutes;
@@ -170,6 +174,11 @@
* address of the remote peer. */
struct hash *vhash; /**< VPN tunnel instances indexed by
* virtual address of remote hosts. */
+ struct hash *sid_hash; /**< TLS sessions indexed by the peer's
+ session id. We do not care
collisions here
+ as clients should have unique ids
and
+ supporting clients with identical
SIDs
+ is not needed */
struct schedule *schedule;
struct mbuf_set *mbuf; /**< Set of buffers for passing data
* channel packets between VPN tunnel
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index aededdf..b9ffd0d 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -3754,6 +3754,10 @@
{
o->virtual_hash_size = 4 * o->max_clients;
}
+ if (!o->sid_hash_size)
+ {
+ o->sid_hash_size = o->real_hash_size;
+ }
}
static void
@@ -7355,7 +7359,7 @@
options->ifconfig_ipv6_pool_base = network;
options->ifconfig_ipv6_pool_netbits = netbits;
}
- else if (streq(p[0], "hash-size") && p[1] && p[2] && !p[3])
+ else if (streq(p[0], "hash-size") && p[1] && p[2] && !p[4])
{
int real, virtual;
@@ -7367,6 +7371,16 @@
}
options->real_hash_size = (uint32_t)real;
options->virtual_hash_size = (uint32_t)virtual;
+
+ if (p[3])
+ {
+ int sid;
+ if (!atoi_constrained(p[3], &sid, "hash-size sid", 1, INT_MAX,
msglevel))
+ {
+ goto err;
+ }
+ options->sid_hash_size = (uint32_t)sid;
+ }
}
else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[3])
{
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index a111cf8..7207701 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -496,6 +496,7 @@
uint32_t real_hash_size;
uint32_t virtual_hash_size;
+ uint32_t sid_hash_size;
const char *client_connect_script;
const char *client_disconnect_script;
const char *learn_address_script;
diff --git a/src/openvpn/sid_hash.h b/src/openvpn/sid_hash.h
new file mode 100644
index 0000000..e817e07
--- /dev/null
+++ b/src/openvpn/sid_hash.h
@@ -0,0 +1,90 @@
+/*
+ * 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) 2026 OpenVPN Inc <[email protected]>
+ * Copyright (C) 2026 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 "session_id.h"
+#include "multi.h"
+#include "list.h"
+#include "siphash.h"
+
+inline static void
+multi_hash_sid_add(struct multi_context *m, struct session_id *sid,
+ struct multi_instance *mi)
+{
+ /* This only be called if the multi instance is not already present
+ * in the hash table */
+ ASSERT(!session_id_defined(&mi->sid_hashed_value));
+
+ mi->sid_hashed_value = *sid;
+
+ const uint64_t hv = hash_value(m->sid_hash, &mi->sid_hashed_value);
+ struct hash_bucket *bucket = hash_bucket(m->sid_hash, hv);
+ hash_add_fast(m->sid_hash, bucket, &mi->sid_hashed_value, hv, mi);
+ multi_instance_inc_refcount(mi);
+}
+
+inline static bool
+multi_hash_sid_remove(struct multi_context *m, const struct session_id *sid)
+{
+ const uint64_t hv = hash_value(m->sid_hash, sid);
+ struct hash_bucket *bucket = hash_bucket(m->sid_hash, hv);
+ struct hash_element *he = hash_lookup_fast(m->sid_hash, bucket, sid, hv);
+ if (he)
+ {
+ struct multi_instance *mi = he->value;
+ ASSERT(hash_remove_fast(m->sid_hash, bucket, sid, hv));
+ CLEAR(mi->sid_hashed_value);
+ multi_instance_dec_refcount(mi);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+static inline struct hash_element *
+multi_hash_sid_lookup(struct multi_context *m, struct session_id *sid)
+{
+ const uint64_t sid_hv = hash_value(m->sid_hash, sid);
+ struct hash_bucket *sid_bucket = hash_bucket(m->sid_hash, sid_hv);
+ struct hash_element *he_sid = hash_lookup_fast(m->sid_hash, sid_bucket,
sid, sid_hv);
+ return he_sid;
+}
+
+/* hashing the session. As the struct is just an 8 byte array
+ * hashing is straight forward */
+static inline uint64_t
+session_id_hash_function(const void *key, void *ctx)
+{
+ return siphash_hash_func(key, sizeof(struct session_id), ctx);
+}
+
+/* wrapper for session_id_equal to have the void* arguments that the
+ * hash map requires */
+static inline bool
+session_id_hash_equal(const void *sid1, const void *sid2)
+{
+ return session_id_equal((struct session_id *)sid1, (struct session_id
*)sid2);
+}
diff --git a/tests/unit_tests/openvpn/Makefile.am
b/tests/unit_tests/openvpn/Makefile.am
index 5f7f692..2c8b6d4 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -382,7 +382,8 @@
$(top_srcdir)/src/openvpn/list.c \
$(top_srcdir)/src/openvpn/siphash.c \
$(top_srcdir)/src/openvpn/siphash_openssl.c \
- $(top_srcdir)/src/openvpn/siphash_reference.c
+ $(top_srcdir)/src/openvpn/siphash_reference.c \
+ $(top_srcdir)/src/openvpn/session_id.c
push_update_msg_testdriver_CFLAGS = -I$(top_srcdir)/src/openvpn \
-I$(top_srcdir)/src/compat \
diff --git a/tests/unit_tests/openvpn/test_misc.c
b/tests/unit_tests/openvpn/test_misc.c
index 576630d..52f98c3 100644
--- a/tests/unit_tests/openvpn/test_misc.c
+++ b/tests/unit_tests/openvpn/test_misc.c
@@ -39,6 +39,7 @@
#include "test_common.h"
#include "list.h"
#include "mock_msg.h"
+#include "sid_hash.h"
static void
test_compat_lzo_string(void **state)
@@ -139,13 +140,6 @@
return strcmp((const char *)key1, (const char *)key2) == 0;
}
-static uint32_t
-get_random(void)
-{
- /* rand() is not very random, but it's C99 and this is just for testing */
- return (uint32_t)rand();
-}
-
static struct hash_element *
hash_lookup_by_value(struct hash *hash, void *value)
{
@@ -445,12 +439,63 @@
mock_set_debug_level(saved_log_level);
}
+static void
+test_sid_hash_list(void **state)
+{
+ /* very simple tests to ensure the basic hash functions work */
+
+ struct multi_context m = { 0 };
+ m.sid_hash = hash_init(2048, siphash_new_context(), siphash_free_context,
session_id_hash_function, session_id_hash_equal);
+
+ struct session_id sid1;
+ struct session_id sid2;
+ struct session_id sid3;
+
+ struct multi_instance *m1, *m3;
+
+ /* multi_hash_sid_remove will call gc_free on the gc of a mi and
+ * free on the mi itself */
+ ALLOC_OBJ(m1, struct multi_instance);
+ ALLOC_OBJ(m3, struct multi_instance);
+
+ m1->gc = gc_new();
+ m3->gc = gc_new();
+
+ session_id_random(&sid1);
+ session_id_random(&sid2);
+ session_id_random(&sid3);
+
+ multi_hash_sid_add(&m, &sid1, m1);
+ multi_hash_sid_add(&m, &sid3, m3);
+
+
+ /* sid2 is not added and should not be returned */
+ struct hash_element *he_sid = multi_hash_sid_lookup(&m, &sid2);
+ assert_null(he_sid);
+
+ he_sid = multi_hash_sid_lookup(&m, &sid1);
+ assert_non_null(he_sid);
+ assert_ptr_equal(he_sid->value, m1);
+
+ /* Try removing elements, only that are in the map should return true */
+ assert_true(multi_hash_sid_remove(&m, &sid1));
+ assert_false(multi_hash_sid_remove(&m, &sid2));
+ assert_false(multi_hash_sid_remove(&m, &sid1));
+
+ /* should no longer find the element */
+ he_sid = multi_hash_sid_lookup(&m, &sid1);
+ assert_null(he_sid);
+
+ hash_free(m.sid_hash);
+}
+
const struct CMUnitTest misc_tests[] = {
cmocka_unit_test(test_compat_lzo_string),
cmocka_unit_test(test_auth_fail_temp_no_flags),
cmocka_unit_test(test_auth_fail_temp_flags),
cmocka_unit_test(test_auth_fail_temp_flags_msg),
cmocka_unit_test(test_list),
- cmocka_unit_test(test_atoi_variants)
};
+ cmocka_unit_test(test_atoi_variants),
+ cmocka_unit_test(test_sid_hash_list)
};
int
main(void)
--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1727?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: Idb59ecd119331b198792ad1379bec8600211651b
Gerrit-Change-Number: 1727
Gerrit-PatchSet: 3
Gerrit-Owner: plaisthos <[email protected]>
Gerrit-Reviewer: flichtenheld <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: flichtenheld <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel