]> git.ipfire.org Git - thirdparty/openvpn.git/commitdiff
Add connect-freq-initial option to limit initial connection responses
authorArne Schwabe <arne@rfc2549.org>
Tue, 10 Jan 2023 01:59:01 +0000 (02:59 +0100)
committerGert Doering <gert@greenie.muc.de>
Tue, 10 Jan 2023 07:04:05 +0000 (08:04 +0100)
This limits the number of packets OpenVPN will respond to. This avoids
OpenVPN servers being abused for refelection attacks in a large scale
as we gotten a lot more efficient with the cookie approach in our
initial connection handling.

The defaults of 100 attempts per 10s should work for most people,
esepcially since completed three way handshakes are not counted. So
the default will throttle connection attempts on server with high packet
loss or that are actually under a DOS.

The 100 per 10s are similar in size to the old 2.5 and earlier behaviour
where every initial connection attempt would take up a slot of the
max-clients sessions and those would only expire after the TLS timeout.
This roughly translates to 1024 connection attempts in 60s on an
empty server.

OpenVPN will announce once per period when starting to drop packets and
ultimatively how many packets it dropped:

    Connection Attempt Note: --connect-freq-initial 100 10 rate limit
    exceeded, dropping initial handshake packets for the next 10 seconds

    Connection Attempt Dropped 217 initial handshake packets due to
    --connect-freq-initial 100 10

to inform an admin about the consequences of this feature.

Patch v2: use strtol instead of atoi to be able to differentiate between
          an error parsing and parsing 0. Use int64_t instead int to
          avoid overflow errors.

Patch v3: Add message when we start dropping. Add a few fixes to the logic.
          improve docs

Patch v4: missing missing return statement.
Patch v5: add build files for msvc build

Signed-off-by: Arne Schwabe <arne@rfc2549.org>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <20230110015901.933522-1-arne@rfc2549.org>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg25938.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
Changes.rst
doc/man-sections/server-options.rst
src/openvpn/Makefile.am
src/openvpn/mudp.c
src/openvpn/multi.c
src/openvpn/multi.h
src/openvpn/openvpn.vcxproj
src/openvpn/options.c
src/openvpn/options.h
src/openvpn/reflect_filter.c [new file with mode: 0644]
src/openvpn/reflect_filter.h [new file with mode: 0644]

index 47933ae09f028daf445f5349724b23a36192f6e6..187d03fcf30eda199f9c40638688b2c3a72d7dce 100644 (file)
@@ -92,6 +92,10 @@ Cookie based handshake for UDP server
     shake. The tls-crypt-v2 option allows controlling if older clients are
     accepted.
 
+    By default the rate of initial packet responses is limited to 100 per 10s
+    interval to avoid OpenVPN servers being abused in reflection attacks
+    (see ``--connect-freq-initial``).
+
 Data channel offloading with ovpn-dco
     2.6.0+ implements support for data-channel offloading where the data packets
     are directly processed and forwarded in kernel space thanks to the ovpn-dco
index 99263fff3ab980a03177ba45e63448cfb247ac97..dbe35d6e1581491258616f14e7a0467394baebd2 100644 (file)
@@ -178,12 +178,36 @@ fast hardware. SSL/TLS authentication must be used in this mode.
   with connection requests using certificates which will ultimately fail
   to authenticate.
 
+  This limit applies after ``--connect-freq-initial`` and
+  only applies to client that have completed the three-way handshake
+  or client that use ``--tls-crypt-v2`` without cookie support
+  (``allow-noncookie`` argument to ``--tls-crypt-v2``).
+
   This is an imperfect solution however, because in a real DoS scenario,
   legitimate connections might also be refused.
 
   For the best protection against DoS attacks in server mode, use
   ``--proto udp`` and either ``--tls-auth`` or ``--tls-crypt``.
 
+--connect-freq-initial args
+  (UDP only) Allow a maximum of ``n`` initial connection packet responses
+  per ``sec`` seconds from the OpenVPN server to clients.
+
+  Valid syntax:
+  ::
+
+     connect-freq-initial n sec
+
+  OpenVPN starting at 2.6 is very efficient in responding to initial
+  connection packets. When not limiting the initial responses
+  an OpenVPN daemon can be abused in reflection attacks.
+  This option is designed to limit the rate OpenVPN will respond to initial
+  attacks.
+
+  Connection attempts that complete the initial three-way handshake
+  will not be counted against the limit. The default is to allow
+  100 initial connection per 10s.
+
 --duplicate-cn
   Allow multiple clients with the same common name to concurrently
   connect. In the absence of this option, OpenVPN will disconnect a client
index e80a35abd51696fe89339c47a6fbd046b38b7609..35d60a65ba8ecc2fc5a05109949d0f701713b9bd 100644 (file)
@@ -113,6 +113,7 @@ openvpn_SOURCES = \
        ps.c ps.h \
        push.c push.h \
        pushlist.h \
+       reflect_filter.c reflect_filter.h \
        reliable.c reliable.h \
        route.c route.h \
        run_command.c run_command.h \
index c27c6da5bfdda94ef6c3853a567f32bbd00c8c33..77560425fe7d1be4d9ae89b7236e081c6b1ca0a5 100644 (file)
@@ -82,6 +82,16 @@ do_pre_decrypt_check(struct multi_context *m,
     struct openvpn_sockaddr *from = &m->top.c2.from.dest;
     int handwindow = m->top.options.handshake_window;
 
+    if (verdict == VERDICT_VALID_RESET_V3 || verdict == VERDICT_VALID_RESET_V2)
+    {
+        /* Check if we are still below our limit for sending out
+         * responses */
+        if (!reflect_filter_rate_limit_check(m->initial_rate_limiter))
+        {
+            return false;
+        }
+    }
+
     if (verdict == VERDICT_VALID_RESET_V3)
     {
         /* Extract the packet id to check if it has the special format that
@@ -244,6 +254,10 @@ multi_get_create_instance_udp(struct multi_context *m, bool *floated)
 
                 if (frequency_limit_event_allowed(m->new_connection_limiter))
                 {
+                    /* a successful three-way handshake only counts against
+                     * connect-freq but not against connect-freq-initial */
+                    reflect_filter_rate_limit_decrease(m->initial_rate_limiter);
+
                     mi = multi_create_instance(m, &real);
                     if (mi)
                     {
index 186e881927ef5897485b9c8e7cdd9bd672dcd4d7..26904859f2853318beb8d4fe7e7212f8ade40517 100644 (file)
@@ -52,6 +52,7 @@
 #include "crypto_backend.h"
 #include "ssl_util.h"
 #include "dco.h"
+#include "reflect_filter.h"
 
 /*#define MULTI_DEBUG_EVENT_LOOP*/
 
@@ -368,6 +369,8 @@ multi_init(struct multi_context *m, struct context *t, bool tcp_mode)
      */
     m->new_connection_limiter = frequency_limit_init(t->options.cf_max,
                                                      t->options.cf_per);
+    m->initial_rate_limiter = initial_rate_limit_init(t->options.cf_initial_max,
+                                                      t->options.cf_initial_per);
 
     /*
      * Allocate broadcast/multicast buffer list
@@ -729,6 +732,7 @@ multi_uninit(struct multi_context *m)
         mbuf_free(m->mbuf);
         ifconfig_pool_free(m->ifconfig_pool);
         frequency_limit_free(m->new_connection_limiter);
+        initial_rate_limit_free(m->initial_rate_limiter);
         multi_reap_free(m->reaper);
         mroute_helper_free(m->route_helper);
         multi_tcp_free(m->mtcp);
index 370d795c9d5e130a45b90e6edef1d374016edc81..713c63eee46c6cc2a123e1252d170464795bd366 100644 (file)
@@ -39,6 +39,7 @@
 #include "mtcp.h"
 #include "perf.h"
 #include "vlan.h"
+#include "reflect_filter.h"
 
 #define MULTI_PREFIX_MAX_LENGTH 256
 
@@ -170,6 +171,7 @@ struct multi_context {
                                  *   as external transport. */
     struct ifconfig_pool *ifconfig_pool;
     struct frequency_limit *new_connection_limiter;
+    struct initial_packet_rate_limit *initial_rate_limiter;
     struct mroute_helper *route_helper;
     struct multi_reap *reaper;
     struct mroute_addr local;
index 13755dae995039b2bf857d733d124eb588d2327e..97baf678c4620c0da8569d8e04ed3b5126746dae 100644 (file)
     <ClCompile Include="proxy.c" />
     <ClCompile Include="ps.c" />
     <ClCompile Include="push.c" />
+    <ClCompile Include="reflect_filter.c" />
     <ClCompile Include="reliable.c" />
     <ClCompile Include="route.c" />
     <ClCompile Include="run_command.c" />
     <ClInclude Include="ps.h" />
     <ClInclude Include="push.h" />
     <ClInclude Include="pushlist.h" />
+    <ClInclude Include="reflect_filter.h" />
     <ClInclude Include="reliable.h" />
     <ClInclude Include="ring_buffer.h" />
     <ClInclude Include="route.h" />
index ee3783046e214c8d4a09cdf647424ebfef65d0b6..e756af94840cf0a746718daffdd0ba7d31c17abf 100644 (file)
@@ -480,6 +480,7 @@ static const char usage_message[] =
     "                  as well as pushes it to connecting clients.\n"
     "--learn-address cmd : Run command cmd to validate client virtual addresses.\n"
     "--connect-freq n s : Allow a maximum of n new connections per s seconds.\n"
+    "--connect-freq-initial n s : Allow a maximum of n replies for initial connections attempts per s seconds.\n"
     "--max-clients n : Allow a maximum of n simultaneously connected clients.\n"
     "--max-routes-per-client n : Allow a maximum of n internal routes per client.\n"
     "--stale-routes-check n [t] : Remove routes with a last activity timestamp\n"
@@ -864,6 +865,8 @@ init_options(struct options *o, const bool init_gc)
     o->n_bcast_buf = 256;
     o->tcp_queue_limit = 64;
     o->max_clients = 1024;
+    o->cf_initial_per = 10;
+    o->cf_initial_max = 100;
     o->max_routes_per_client = 256;
     o->stale_routes_check_interval = 0;
     o->ifconfig_pool_persist_refresh_freq = 600;
@@ -1555,6 +1558,8 @@ show_p2mp_parms(const struct options *o)
     SHOW_BOOL(duplicate_cn);
     SHOW_INT(cf_max);
     SHOW_INT(cf_per);
+    SHOW_INT(cf_initial_max);
+    SHOW_INT(cf_initial_per);
     SHOW_INT(max_clients);
     SHOW_INT(max_routes_per_client);
     SHOW_STR(auth_user_pass_verify_script);
@@ -7452,6 +7457,22 @@ add_option(struct options *options,
         options->cf_max = cf_max;
         options->cf_per = cf_per;
     }
+    else if (streq(p[0], "connect-freq-initial") && p[1] && p[2] && !p[3])
+    {
+        long cf_max, cf_per;
+
+        VERIFY_PERMISSION(OPT_P_GENERAL);
+        char *e1, *e2;
+        cf_max = strtol(p[1], &e1, 10);
+        cf_per = strtol(p[2], &e2, 10);
+        if (cf_max < 0 || cf_per < 0 || *e1 != '\0' || *e2 != '\0')
+        {
+            msg(msglevel, "--connect-freq-initial parameters must be integers and >= 0");
+            goto err;
+        }
+        options->cf_initial_max = cf_max;
+        options->cf_initial_per = cf_per;
+    }
     else if (streq(p[0], "max-clients") && p[1] && !p[2])
     {
         int max_clients;
index fec1eace5f30e544f2215e3599ade9459b46cd2f..48315b10ed002ae5895f60337e9a106c00d850ce 100644 (file)
@@ -513,8 +513,13 @@ struct options
     bool push_ifconfig_ipv6_blocked;                    /* IPv6 */
     bool enable_c2c;
     bool duplicate_cn;
+
     int cf_max;
     int cf_per;
+
+    int cf_initial_max;
+    int cf_initial_per;
+
     int max_clients;
     int max_routes_per_client;
     int stale_routes_check_interval;
diff --git a/src/openvpn/reflect_filter.c b/src/openvpn/reflect_filter.c
new file mode 100644 (file)
index 0000000..323184c
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ *  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) 2022 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.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#elif defined(_MSC_VER)
+#include "config-msvc.h"
+#endif
+
+#include "syshead.h"
+
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <memory.h>
+
+#include "crypto.h"
+#include "reflect_filter.h"
+
+
+bool
+reflect_filter_rate_limit_check(struct initial_packet_rate_limit *irl)
+{
+    if (now > irl->last_period_reset + irl->period_length)
+    {
+        int64_t dropped = irl->curr_period_counter - irl->max_per_period;
+        if (dropped > 0)
+        {
+            msg(D_TLS_DEBUG_LOW, "Dropped %" PRId64 " initial handshake packets"
+                " due to --connect-freq-initial %" PRId64 " %d", dropped,
+                irl->max_per_period, irl->period_length);
+
+        }
+        irl->last_period_reset = now;
+        irl->curr_period_counter = 0;
+        irl->warning_displayed = false;
+    }
+
+    irl->curr_period_counter++;
+
+    bool over_limit = irl->curr_period_counter > irl->max_per_period;
+
+    if (over_limit && !irl->warning_displayed)
+    {
+        msg(M_WARN, "Note: --connect-freq-initial %" PRId64 " %d rate limit "
+            "exceeded, dropping initial handshake packets for the next %d "
+            "seconds", irl->max_per_period, irl->period_length,
+            (int)(irl->last_period_reset + irl->period_length - now));
+        irl->warning_displayed = true;
+    }
+    return !over_limit;
+}
+
+void
+reflect_filter_rate_limit_decrease(struct initial_packet_rate_limit *irl)
+{
+    if (irl->curr_period_counter > 0)
+    {
+        irl->curr_period_counter--;
+    }
+}
+
+
+struct initial_packet_rate_limit *
+initial_rate_limit_init(int max_per_period, int period_length)
+{
+    struct initial_packet_rate_limit *irl;
+
+
+    ALLOC_OBJ(irl, struct initial_packet_rate_limit);
+
+    irl->max_per_period = max_per_period;
+    irl->period_length = period_length;
+    irl->curr_period_counter = 0;
+    irl->last_period_reset = 0;
+
+    return irl;
+}
+
+void
+initial_rate_limit_free(struct initial_packet_rate_limit *irl)
+{
+    free(irl);
+}
diff --git a/src/openvpn/reflect_filter.h b/src/openvpn/reflect_filter.h
new file mode 100644 (file)
index 0000000..6dfa875
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ *  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) 2022 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.
+ */
+#ifndef REFLECT_FILTER_H
+#define REFLECT_FILTER_H
+
+#include <limits.h>
+
+/** struct that handles all the rate limiting logic for initial
+ * responses */
+struct initial_packet_rate_limit {
+    /** This is a hard limit for packets per seconds. */
+    int64_t max_per_period;
+
+    /** period length in seconds */
+    int period_length;
+
+    /** Number of packets in the current period. We use int64_t here
+     * to avoid any potiential issues with overflow */
+    int64_t curr_period_counter;
+
+    /* Last time we reset our timer */
+    time_t last_period_reset;
+
+    /* we want to warn once per period that packets are being started to
+     * be dropped */
+    bool warning_displayed;
+};
+
+
+/**
+ * checks if the connection is still allowed to connect under the rate
+ * limit. This also increases the internal counter at the same time
+ */
+bool
+reflect_filter_rate_limit_check(struct initial_packet_rate_limit *irl);
+
+/**
+ * decreases the counter of initial packets seen, so connections that
+ * successfully completed the three-way handshake do not count against
+ * the counter of initial connection attempts
+ */
+void
+reflect_filter_rate_limit_decrease(struct initial_packet_rate_limit *irl);
+
+/**
+ * allocate and initialize the initial-packet rate limiter structure
+ */
+struct initial_packet_rate_limit *
+initial_rate_limit_init(int max_per_period, int period_length);
+
+/**
+ * free the initial-packet rate limiter structure
+ */
+void initial_rate_limit_free(struct initial_packet_rate_limit *irl);
+#endif /* ifndef REFLECT_FILTER_H */