Skip to contentSkip to conversation

Add 'printing of port number' to mroute_addr_print_ex() for v4-mapped v6.

PATCH In review Gert Doering 3 participants 9 messages Dec 7, 2018 – Mar 25, 2021

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 Add 'printing of port number' to mroute_addr_print_ex() for v4-mapped v6.

For whatever reason, this function never printed port numbers for IPv6 addresses (but it did for IPv4) - which creates a bit of confusion for IPv6-mapped v4 addresses on a dual stack socket, that will have ports numbers printed or not, depending on whether it's a dual-stack v6 socket or single-stack v4.

This will not(!) add printing of port numbers for "proper" v6 addresses yet, because that might have adverse side effects to address parsing elsewhere.

---
 src/openvpn/mroute.c | 7 +++++++
 1 file changed, 7 insertions(+)
src/openvpn/mroute.c +7 -0
index 28940a8d..134f4c00 100644
@@ -477,6 +477,13 @@ mroute_addr_print_ex(const struct mroute_addr *ma,
                 {
                     buf_printf(&out, "%s", print_in_addr_t(maddr.v4mappedv6.addr,
                                                            IA_NET_ORDER, gc));
+		    /* we only print port numbers for v4mapped v6 as of
+		     * today, because "v6addr:port" is too ambiguous
+		     */
+                    if (maddr.type & MR_WITH_PORT)
+                    {
+                        buf_printf(&out, ":%d", ntohs(maddr.v6.port));
+                    }
                 }
                 else
                 {
Signature · 1 line
2.19.1
#1 lore raw

PATCH v2 1/1 Stop state-exhaustion attacks from a single source address.

If an attacker sends lots of RESET packets from different source ports (but the same source address), every packet will create a new multi_instance, leading to resource waste on the server, and to lots of reply packets while OpenVPN tries to establish a TLS handshake.

This can be rate-limited with "connect-freq", but if this is set too tightly, an attacker can drown out legitimate users of the same OpenVPN server.

So, when deciding whether or not to create a new instance, iterate over all existing instances, counting all from the same source IP (ignoring source port info) that are "in TLS negotiation" state - if more than <n> instances are already active, refuse new instance.

The cutoff parameter can be configured by a newly introduced 3rd argument to "connect-freq". So something like this might be reasonable for a medium-sized server:

connect-freq 20 20 3

("permit 20 new connections per 20 seconds, but only 3 from the same source IP address")

Drawback: if many users are sitting behind a shared NAT ip address and they all reconnect at the same time, session setup will take longer for some of the users while the server is still handshaking with others.

v1:
this is really a "request for discussion" and needs removal of lots of debugging printout. Also, it needs to be configurable.

v2:
make it configurable

---
 src/openvpn/mudp.c    | 108 +++++++++++++++++++++++++++++++++++++++++-
 src/openvpn/options.c |   6 ++-
 src/openvpn/options.h |   1 +
 3 files changed, 113 insertions(+), 2 deletions(-)
src/openvpn/mudp.c +107 -1
index a604d217..ff5eb688 100644
@@ -41,6 +41,111 @@
 #include <sys/inotify.h>
 #endif
 
+/* compare two mroute_addr, ignoring the port info
+ */
+static inline bool
+mroute_addr_equal_no_port(const struct mroute_addr *a1,
+                          const struct mroute_addr *a2)
+{
+    if (a1->type != a2->type)
+    {
+        return false;
+    }
+    if (a1->netbits != a2->netbits)
+    {
+        return false;
+    }
+    if (a1->len != a2->len)
+    {
+        return false;
+    }
+    int len = a1->len;
+    if ( len == 6 || len == 18 )	/* AF_INET or AF_INET6 + Port */
+    {
+	len -= 2;
+    }
+    return memcmp(a1->raw_addr, a2->raw_addr, len) == 0;
+}
+
+/*
+ * allow no more than <n> instances for the same source IP
+ * in "key state not active yet" state - this is almost always a
+ * state exhaustion and/or reflection attack
+ *
+ * real use cases might hit "multiple instances for the same source IP"
+ * if there are multiple users behind the same NAT gateway - but those
+ * will not be in "ks->state < S_START" for more than a few seconds
+ * (so we might see a few false positives if many users behind the same
+ * NAT gateway restart their VPNs at the same time, but TLS handshake
+ * will succeed for one after another, eventually for all)
+ */
+
+
+bool
+dangling_instances_per_source_allowed(struct multi_context *m,
+				      struct mroute_addr *new_source)
+{
+    /* nothing configured -> default = allow all */
+    if ( m->top.options.cf_max_dangling <= 0 )
+    {
+	return true;
+    }
+
+    struct gc_arena gc = gc_new();
+    msg(M_INFO, "MULTI dipsa: new_source=%s (len=%d)",
+		mroute_addr_print(new_source, &gc), new_source->len );
+
+    struct hash_iterator hi;
+    struct hash_element *he;
+    int count = 0;
+
+    hash_iterator_init(m->iter, &hi);
+    while ((he = hash_iterator_next(&hi)))
+    {
+	struct multi_instance *mi = (struct multi_instance *) he->value;
+
+	msg(M_INFO, "MULTI dipsa: instance: halt=%d real=%s",
+		mi->halt, mroute_addr_print(&mi->real, &gc));
+	if (!mi->halt && mroute_addr_equal_no_port(new_source, &mi->real))
+	{
+	    msg(M_INFO, "MULTI dipsa: ip match found!");
+
+	    struct tls_multi * tls_multi = mi->context.c2.tls_multi;
+
+	    for (int i=0; i<TM_SIZE; i++ )
+	    {
+	        msg(M_INFO, "MULTI dipsa: session[%d].key[KS_PRIMARY].state=%d",
+		    i, tls_multi->session[i].key[KS_PRIMARY].state );
+	    }
+
+	    /* session not fully established yet?
+	     * (TM_ACTIVE seems to be the one that goes from "2" to "6"
+             * after successful handshake, while TM_UNTRUST sticks to "1")
+             */
+	    struct tls_session *session = &tls_multi->session[TM_ACTIVE];
+	    struct key_state *ks = &session->key[KS_PRIMARY];
+	    if ( ks->state < S_START )
+	    {
+	        msg(M_INFO, "MULTI dipsa: dangling session (ks->state=%d)",
+			    ks->state );
+	        count++;
+	    }
+	}
+    }
+    hash_iterator_free(&hi);
+
+    gc_free(&gc);
+
+    msg( M_INFO, "MULTI dipsa: count=%d", count );
+    if ( count >= m->top.options.cf_max_dangling )
+    {
+	msg( M_INFO, "MULTI dipsa: too many dangling instances (%d) for this source IP", count );
+	return false;
+    }
+
+    return true;
+}
+
 /*
  * Get a client instance based on real address.  If
  * the instance doesn't exist, create it while
@@ -100,7 +205,8 @@ multi_get_create_instance_udp(struct multi_context *m, bool *floated)
             if (!m->top.c2.tls_auth_standalone
                 || tls_pre_decrypt_lite(m->top.c2.tls_auth_standalone, &m->top.c2.from, &m->top.c2.buf))
             {
-                if (frequency_limit_event_allowed(m->new_connection_limiter))
+                if (frequency_limit_event_allowed(m->new_connection_limiter)
+                    && dangling_instances_per_source_allowed(m, &real) )
                 {
                     mi = multi_create_instance(m, &real);
                     if (mi)
src/openvpn/options.c +5 -1
index 26f056fc..b8e28df6 100644
@@ -6644,7 +6644,7 @@ add_option(struct options *options,
         options->real_hash_size = real;
         options->virtual_hash_size = real;
     }
-    else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[3])
+    else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[4])
     {
         int cf_max, cf_per;
 
@@ -6658,6 +6658,10 @@ add_option(struct options *options,
         }
         options->cf_max = cf_max;
         options->cf_per = cf_per;
+	if ( p[3] )
+	{
+	    options->cf_max_dangling = atoi(p[3]);
+	}
     }
     else if (streq(p[0], "max-clients") && p[1] && !p[2])
     {
src/openvpn/options.h +1 -0
index e2b38939..26a78878 100644
@@ -451,6 +451,7 @@ struct options
     bool duplicate_cn;
     int cf_max;
     int cf_per;
+    int cf_max_dangling;
     int max_clients;
     int max_routes_per_client;
     int stale_routes_check_interval;
Signature · 1 line
2.19.1
#2 lore raw

PATCH v3 1/1 Stop state-exhaustion attacks from a single source address.

If an attacker sends lots of RESET packets from different source ports (but the same source address), every packet will create a new multi_instance, leading to resource waste on the server, and to lots of reply packets while OpenVPN tries to establish a TLS handshake.

This can be rate-limited with "connect-freq", but if this is set too tightly, an attacker can drown out legitimate users of the same OpenVPN server.

So, when deciding whether or not to create a new instance, iterate over all existing instances, counting all from the same source IP (ignoring source port info) that are "in TLS negotiation" state - if more than <n> instances are already active, refuse new instance.

The cutoff parameter can be configured by a newly introduced 3rd argument to "connect-freq". So something like this might be reasonable for a medium-sized server:

connect-freq 20 20 3

("permit 20 new connections per 20 seconds, but only 3 from the same source IP address")

Drawback: if many users are sitting behind a shared NAT ip address and they all reconnect at the same time, session setup will take longer for some of the users while the server is still handshaking with others.

v1:
this is really a "request for discussion" and needs removal of lots of debugging printout. Also, it needs to be configurable.

v2:
make it configurable

v3:
Swap order of checks - check "per instance dangling limit" before generic "connect-freq" limiter. Otherwise a really massive flood of incoming RESET packets from a single host will trigger the global "connect-freq" limiter *first*, and thus drown out other sessions (again).
Clarify wording of refusal message.

---
 src/openvpn/mudp.c    | 108 +++++++++++++++++++++++++++++++++++++++++-
 src/openvpn/options.c |   6 ++-
 src/openvpn/options.h |   1 +
 3 files changed, 113 insertions(+), 2 deletions(-)
src/openvpn/mudp.c +107 -1
index a604d217..be8e0fe7 100644
@@ -41,6 +41,111 @@
 #include <sys/inotify.h>
 #endif
 
+/* compare two mroute_addr, ignoring the port info
+ */
+static inline bool
+mroute_addr_equal_no_port(const struct mroute_addr *a1,
+                          const struct mroute_addr *a2)
+{
+    if (a1->type != a2->type)
+    {
+        return false;
+    }
+    if (a1->netbits != a2->netbits)
+    {
+        return false;
+    }
+    if (a1->len != a2->len)
+    {
+        return false;
+    }
+    int len = a1->len;
+    if ( len == 6 || len == 18 )	/* AF_INET or AF_INET6 + Port */
+    {
+	len -= 2;
+    }
+    return memcmp(a1->raw_addr, a2->raw_addr, len) == 0;
+}
+
+/*
+ * allow no more than <n> instances for the same source IP
+ * in "key state not active yet" state - this is almost always a
+ * state exhaustion and/or reflection attack
+ *
+ * real use cases might hit "multiple instances for the same source IP"
+ * if there are multiple users behind the same NAT gateway - but those
+ * will not be in "ks->state < S_START" for more than a few seconds
+ * (so we might see a few false positives if many users behind the same
+ * NAT gateway restart their VPNs at the same time, but TLS handshake
+ * will succeed for one after another, eventually for all)
+ */
+
+
+bool
+dangling_instances_per_source_allowed(struct multi_context *m,
+				      struct mroute_addr *new_source)
+{
+    /* nothing configured -> default = allow all */
+    if ( m->top.options.cf_max_dangling <= 0 )
+    {
+	return true;
+    }
+
+    struct gc_arena gc = gc_new();
+    msg(M_INFO, "MULTI dipsa: new_source=%s (len=%d)",
+		mroute_addr_print(new_source, &gc), new_source->len );
+
+    struct hash_iterator hi;
+    struct hash_element *he;
+    int count = 0;
+
+    hash_iterator_init(m->iter, &hi);
+    while ((he = hash_iterator_next(&hi)))
+    {
+	struct multi_instance *mi = (struct multi_instance *) he->value;
+
+	msg(M_INFO, "MULTI dipsa: instance: halt=%d real=%s",
+		mi->halt, mroute_addr_print(&mi->real, &gc));
+	if (!mi->halt && mroute_addr_equal_no_port(new_source, &mi->real))
+	{
+	    msg(M_INFO, "MULTI dipsa: ip match found!");
+
+	    struct tls_multi * tls_multi = mi->context.c2.tls_multi;
+
+	    for (int i=0; i<TM_SIZE; i++ )
+	    {
+	        msg(M_INFO, "MULTI dipsa: session[%d].key[KS_PRIMARY].state=%d",
+		    i, tls_multi->session[i].key[KS_PRIMARY].state );
+	    }
+
+	    /* session not fully established yet?
+	     * (TM_ACTIVE seems to be the one that goes from "2" to "6"
+             * after successful handshake, while TM_UNTRUST sticks to "1")
+             */
+	    struct tls_session *session = &tls_multi->session[TM_ACTIVE];
+	    struct key_state *ks = &session->key[KS_PRIMARY];
+	    if ( ks->state < S_START )
+	    {
+	        msg(M_INFO, "MULTI dipsa: dangling session (ks->state=%d)",
+			    ks->state );
+	        count++;
+	    }
+	}
+    }
+    hash_iterator_free(&hi);
+
+    gc_free(&gc);
+
+    msg( M_INFO, "MULTI dipsa: count=%d", count );
+    if ( count >= m->top.options.cf_max_dangling )
+    {
+	msg( M_INFO, "MULTI dipsa: maximum number of instances doing TLS handshake for this source IP reached (%d)", count );
+	return false;
+    }
+
+    return true;
+}
+
 /*
  * Get a client instance based on real address.  If
  * the instance doesn't exist, create it while
@@ -100,7 +205,8 @@ multi_get_create_instance_udp(struct multi_context *m, bool *floated)
             if (!m->top.c2.tls_auth_standalone
                 || tls_pre_decrypt_lite(m->top.c2.tls_auth_standalone, &m->top.c2.from, &m->top.c2.buf))
             {
-                if (frequency_limit_event_allowed(m->new_connection_limiter))
+                if (dangling_instances_per_source_allowed(m, &real)
+                    && frequency_limit_event_allowed(m->new_connection_limiter))
                 {
                     mi = multi_create_instance(m, &real);
                     if (mi)
src/openvpn/options.c +5 -1
index 26f056fc..b8e28df6 100644
@@ -6644,7 +6644,7 @@ add_option(struct options *options,
         options->real_hash_size = real;
         options->virtual_hash_size = real;
     }
-    else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[3])
+    else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[4])
     {
         int cf_max, cf_per;
 
@@ -6658,6 +6658,10 @@ add_option(struct options *options,
         }
         options->cf_max = cf_max;
         options->cf_per = cf_per;
+	if ( p[3] )
+	{
+	    options->cf_max_dangling = atoi(p[3]);
+	}
     }
     else if (streq(p[0], "max-clients") && p[1] && !p[2])
     {
src/openvpn/options.h +1 -0
index e2b38939..26a78878 100644
@@ -451,6 +451,7 @@ struct options
     bool duplicate_cn;
     int cf_max;
     int cf_per;
+    int cf_max_dangling;
     int max_clients;
     int max_routes_per_client;
     int stale_routes_check_interval;
Signature · 1 line
2.19.1
#5 lore raw
In reply to Gert Doering

Hi,

Quoted 20 lines · Gert Doering

For whatever reason, this function never printed port numbers for IPv6 addresses (but it did for IPv4) - which creates a bit of confusion for IPv6-mapped v4 addresses on a dual stack socket, that will have ports numbers printed or not, depending on whether it's a dual-stack v6 socket or single-stack v4.

This will not(!) add printing of port numbers for "proper" v6 addresses yet, because that might have adverse side effects to address parsing elsewhere.

---
 src/openvpn/mroute.c | 7 +++++++
 1 file changed, 7 insertions(+)
src/openvpn/mroute.c +0 -0
index 28940a8d..134f4c00 100644

src/openvpn/mroute.c mroute_addr_print_ex(const struct mroute_addr *ma, · line 482

Quoted 6 lines
src/openvpn/mroute.c +3 -0
@@ -477,6 +477,13 @@ mroute_addr_print_ex(const struct mroute_addr *ma,
                 {
                     buf_printf(&out, "%s", print_in_addr_t(maddr.v4mappedv6.addr,
                                                            IA_NET_ORDER, gc));
+		    /* we only print port numbers for v4mapped v6 as of
+		     * today, because "v6addr:port" is too ambiguous
+		     */

The comments above are indented with tab+spaces, while the code below is just all spaces. Please use the all spaces everywhere.

src/openvpn/mroute.c mroute_addr_print_ex(const struct mroute_addr *ma, · line 486

Quoted 4 lines
src/openvpn/mroute.c +4 -0
+                    if (maddr.type & MR_WITH_PORT)
+                    {
+                        buf_printf(&out, ":%d", ntohs(maddr.v6.port));
+                    }

I don't understand how is this solving the ambiguity? Or you are just
saying: "we can't do much, let's just print the port anyway"? My suggestion would be to surround the address with [], so basically change the printf format above from %s to [%s]. Does it make sense?

Cheers,

Quoted 4 lines
src/openvpn/mroute.c +0 -0
                 }
                 else
                 {
Signature · 1 line
Antonio Quartulli
#6 lore raw

Hi,

Quoted 6 lines · Antonio Quartulli

src/openvpn/mroute.c mroute_addr_print_ex(const struct mroute_addr *ma, · line 482

Quoted 3 lines
src/openvpn/mroute.c +3 -0
+		    /* we only print port numbers for v4mapped v6 as of
+		     * today, because "v6addr:port" is too ambiguous
+		     */

The comments above are indented with tab+spaces, while the code below is just all spaces. Please use the all spaces everywhere.

Gah. That was the intention, but the comment was added later on and using the wrong editor :-)

Quoted 6 lines

src/openvpn/mroute.c mroute_addr_print_ex(const struct mroute_addr *ma, · line 486

Quoted 4 lines
src/openvpn/mroute.c +4 -0
+                    if (maddr.type & MR_WITH_PORT)
+                    {
+                        buf_printf(&out, ":%d", ntohs(maddr.v6.port));
+                    }

I don't understand how is this solving the ambiguity?

These are *v4* addresses, just masquerading as v6 in the socket structure.

So 1.2.3.4:567 is never ambiguous.

For "true v6 addresses" (the other branch) we keep on not printing the port number.

Quoted 4 lines

Or you are just
saying: "we can't do much, let's just print the port anyway"? My suggestion would be to surround the address with [], so basically change the printf format above from %s to [%s]. Does it make sense?

This is something I want to discuss. Changing mroute_print_addr_ex() for "true v6 addresses" and printing the port number in a new format will affect status file printing, management interface, etc. - so it needs to be well considered.

*This* patch just fixes the discrepancy that v4 addresses are printed "with port" if doing "proto udp4", and "without port" if a v4 connect comes in on a "proto udp / v6only=no" socket. (We do not need to discuss how many problems the v4mapped v6 address format brings with it...)

gert

Signature · 6 lines
"If was one thing all people took for granted, was conviction that if you 
 feed honest figures into a computer, honest figures come out. Never doubted 
 it myself till I met a computer with a sense of humor."
                             Robert A. Heinlein, The Moon is a Harsh Mistress

Gert Doering - Munich, Germany                             [email protected]...
#7 lore raw

Hi,

Quoted 37 lines · Gert Doering

Hi,

Quoted 6 lines · Antonio Quartulli

src/openvpn/mroute.c mroute_addr_print_ex(const struct mroute_addr *ma, · line 482

Quoted 3 lines
src/openvpn/mroute.c +3 -0
+		    /* we only print port numbers for v4mapped v6 as of
+		     * today, because "v6addr:port" is too ambiguous
+		     */

The comments above are indented with tab+spaces, while the code below is just all spaces. Please use the all spaces everywhere.

Gah. That was the intention, but the comment was added later on and using the wrong editor :-)

Quoted 6 lines

src/openvpn/mroute.c mroute_addr_print_ex(const struct mroute_addr *ma, · line 486

Quoted 4 lines
src/openvpn/mroute.c +4 -0
+                    if (maddr.type & MR_WITH_PORT)
+                    {
+                        buf_printf(&out, ":%d", ntohs(maddr.v6.port));
+                    }

I don't understand how is this solving the ambiguity?

These are *v4* addresses, just masquerading as v6 in the socket structure.

So 1.2.3.4:567 is never ambiguous.

For "true v6 addresses" (the other branch) we keep on not printing the port number.

Quoted 4 lines

Or you are just
saying: "we can't do much, let's just print the port anyway"? My suggestion would be to surround the address with [], so basically change the printf format above from %s to [%s]. Does it make sense?

This is something I want to discuss. Changing mroute_print_addr_ex() for "true v6 addresses" and printing the port number in a new format will affect status file printing, management interface, etc. - so it needs to be well considered.

Quoted 5 lines

*This* patch just fixes the discrepancy that v4 addresses are printed "with port" if doing "proto udp4", and "without port" if a v4 connect comes in on a "proto udp / v6only=no" socket. (We do not need to discuss how many problems the v4mapped v6 address format brings with it...)

Thanks for the clarification.

The patch looks good and does what it says on the lid. Tested by checking the status output (with and without the patch) and everything looked good.

Now it only needs to have the tabs fixed on the comment lines, but other than that:

Regards,

Signature · 1 line
Antonio Quartulli
#8 lore raw
In reply to Gert Doering

Patch has been applied to the master and release/2.4 branch (as I see this as a bugfix). Uncrustify applied, tabs are spaced.

"Print port numbers on v6 addresses" landed on my agenda again, need to make up my mind on address format and then come up with a proposal.

commit 4543b13b8540836f6faf67a03b5358bb8bb94a4a (master) commit c2f7058700bc12858a74c53266534c567d1b05f2 (release/2.4)

Author: Gert Doering
Date:   Fri Dec 7 13:33:03 2018 +0100
     Add 'printing of port number' to mroute_addr_print_ex() for v4-mapped v6.

     Signed-off-by: Gert Doering <[email protected]...>
     Acked-by: Antonio Quartulli <[email protected]...>
     Message-Id: <[email protected]...>
     URL: https://www.mail-archive.com/[email protected]/msg17996.html
     Signed-off-by: Gert Doering <[email protected]...>

--
kind regards,

Gert Doering

#3 lore raw

Re: [Openvpn-devel] [PATCH v3] Stop state-exhaustion attacks from a single source address.

In reply to Gert Doering

Hi,

Sorry for the terrible response time, but here goes for initial review:

Quoted 21 lines · Gert Doering

If an attacker sends lots of RESET packets from different source ports (but the same source address), every packet will create a new multi_instance, leading to resource waste on the server, and to lots of reply packets while OpenVPN tries to establish a TLS handshake.

This can be rate-limited with "connect-freq", but if this is set too tightly, an attacker can drown out legitimate users of the same OpenVPN server.

So, when deciding whether or not to create a new instance, iterate over all existing instances, counting all from the same source IP (ignoring source port info) that are "in TLS negotiation" state - if more than <n> instances are already active, refuse new instance.

The cutoff parameter can be configured by a newly introduced 3rd argument to "connect-freq". So something like this might be reasonable for a medium-sized server:

   connect-freq 20 20 3
Quoted 7 lines

("permit 20 new connections per 20 seconds, but only 3 from the same source IP address")

Drawback: if many users are sitting behind a shared NAT ip address and they all reconnect at the same time, session setup will take longer for some of the users while the server is still handshaking with others.

This seems a very reasonable way - on top of tls-auth/tls-crypt - to mitigate floods from a single IP. I'm not very familiar with this code paths, what happens when a cient gets rejected? Does it get told "try next server", or do we give it a silence treatment?

Also, I think it would be nice to have separate log messages in the server log for "generic limit reached" and "per-ip limit reached".

Finally, aside from removing the debug prints, this will need some whitespace polishing. The patch mixes tabs/spaces and styles surrounding the parentheses in ifs.

-Steffan

#4 lore raw

Re: [Openvpn-devel] [PATCH v3] Stop state-exhaustion attacks from a single source address.

In reply to Gert Doering

Hi,

Quoted 36 lines · Gert Doering

If an attacker sends lots of RESET packets from different source ports (but the same source address), every packet will create a new multi_instance, leading to resource waste on the server, and to lots of reply packets while OpenVPN tries to establish a TLS handshake.

This can be rate-limited with "connect-freq", but if this is set too tightly, an attacker can drown out legitimate users of the same OpenVPN server.

So, when deciding whether or not to create a new instance, iterate over all existing instances, counting all from the same source IP (ignoring source port info) that are "in TLS negotiation" state - if more than <n> instances are already active, refuse new instance.

The cutoff parameter can be configured by a newly introduced 3rd argument to "connect-freq". So something like this might be reasonable for a medium-sized server:

   connect-freq 20 20 3

("permit 20 new connections per 20 seconds, but only 3 from the same source IP address")

Drawback: if many users are sitting behind a shared NAT ip address and they all reconnect at the same time, session setup will take longer for some of the users while the server is still handshaking with others.

v1:
this is really a "request for discussion" and needs removal of lots of debugging printout. Also, it needs to be configurable.

v2:
make it configurable

Quoted 8 lines

v3:
Swap order of checks - check "per instance dangling limit" before generic "connect-freq" limiter. Otherwise a really massive flood of incoming RESET packets from a single host will trigger the global "connect-freq" limiter *first*, and thus drown out other sessions (again).
Clarify wording of refusal message.

I am undusting this patch in the attempts of pushing it forward.

Like Steffan already said, this patch looks interesting and offers another layer of protection against DoS attacks.

Like your descriptions says, this is basically an additional knob to make connect-freq finer grained. For this reason this change is also fairly contained and is not expected to create side effects on setups not using this option.

The case of multiple peers behind the same router needing more time to reconnect is acceptable, imho.

Quoted 89 lines
---
 src/openvpn/mudp.c    | 108 +++++++++++++++++++++++++++++++++++++++++-
 src/openvpn/options.c |   6 ++-
 src/openvpn/options.h |   1 +
 3 files changed, 113 insertions(+), 2 deletions(-)
src/openvpn/mudp.c +74 -0
index a604d217..be8e0fe7 100644
@@ -41,6 +41,111 @@
 #include <sys/inotify.h>
 #endif
 
+/* compare two mroute_addr, ignoring the port info
+ */
+static inline bool
+mroute_addr_equal_no_port(const struct mroute_addr *a1,
+                          const struct mroute_addr *a2)
+{
+    if (a1->type != a2->type)
+    {
+        return false;
+    }
+    if (a1->netbits != a2->netbits)
+    {
+        return false;
+    }
+    if (a1->len != a2->len)
+    {
+        return false;
+    }
+    int len = a1->len;
+    if ( len == 6 || len == 18 )	/* AF_INET or AF_INET6 + Port */
+    {
+	len -= 2;
+    }
+    return memcmp(a1->raw_addr, a2->raw_addr, len) == 0;
+}
+
+/*
+ * allow no more than <n> instances for the same source IP
+ * in "key state not active yet" state - this is almost always a
+ * state exhaustion and/or reflection attack
+ *
+ * real use cases might hit "multiple instances for the same source IP"
+ * if there are multiple users behind the same NAT gateway - but those
+ * will not be in "ks->state < S_START" for more than a few seconds
+ * (so we might see a few false positives if many users behind the same
+ * NAT gateway restart their VPNs at the same time, but TLS handshake
+ * will succeed for one after another, eventually for all)
+ */
+
+
+bool
+dangling_instances_per_source_allowed(struct multi_context *m,
+				      struct mroute_addr *new_source)
+{
+    /* nothing configured -> default = allow all */
+    if ( m->top.options.cf_max_dangling <= 0 )
+    {
+	return true;
+    }
+
+    struct gc_arena gc = gc_new();
+    msg(M_INFO, "MULTI dipsa: new_source=%s (len=%d)",
+		mroute_addr_print(new_source, &gc), new_source->len );
+
+    struct hash_iterator hi;
+    struct hash_element *he;
+    int count = 0;
+
+    hash_iterator_init(m->iter, &hi);
+    while ((he = hash_iterator_next(&hi)))
+    {
+	struct multi_instance *mi = (struct multi_instance *) he->value;
+
+	msg(M_INFO, "MULTI dipsa: instance: halt=%d real=%s",
+		mi->halt, mroute_addr_print(&mi->real, &gc));
+	if (!mi->halt && mroute_addr_equal_no_port(new_source, &mi->real))
+	{
+	    msg(M_INFO, "MULTI dipsa: ip match found!");
+
+	    struct tls_multi * tls_multi = mi->context.c2.tls_multi;
+
+	    for (int i=0; i<TM_SIZE; i++ )
+	    {
+	        msg(M_INFO, "MULTI dipsa: session[%d].key[KS_PRIMARY].state=%d",

src/openvpn/mudp.c line 127

Quoted 10 lines
src/openvpn/mudp.c +10 -0
+		    i, tls_multi->session[i].key[KS_PRIMARY].state );
+	    }
+
+	    /* session not fully established yet?
+	     * (TM_ACTIVE seems to be the one that goes from "2" to "6"
+             * after successful handshake, while TM_UNTRUST sticks to "1")
+             */
+	    struct tls_session *session = &tls_multi->session[TM_ACTIVE];
+	    struct key_state *ks = &session->key[KS_PRIMARY];
+	    if ( ks->state < S_START )

Why S_START? Shouldn't we rather count all sessions < S_ACTIVE?

I am looking at ssl.c:2072, where the state is moved to S_START, and it seems we still have to perform the CRL check at this point. It seems to me a bit early to assume the session is valid.

Maybe Arne can confirm or kill my argument.

Quoted 25 lines
src/openvpn/mudp.c +21 -0
+	    {
+	        msg(M_INFO, "MULTI dipsa: dangling session (ks->state=%d)",
+			    ks->state );
+	        count++;
+	    }
+	}
+    }
+    hash_iterator_free(&hi);
+
+    gc_free(&gc);
+
+    msg( M_INFO, "MULTI dipsa: count=%d", count );
+    if ( count >= m->top.options.cf_max_dangling )
+    {
+	msg( M_INFO, "MULTI dipsa: maximum number of instances doing TLS handshake for this source IP reached (%d)", count );
+	return false;
+    }
+
+    return true;
+}
+
 /*
  * Get a client instance based on real address.  If
  * the instance doesn't exist, create it while

src/openvpn/mudp.c multi_get_create_instance_udp(struct multi_context *m, bool *floated) · line 209

Quoted 6 lines
src/openvpn/mudp.c +2 -1
@@ -100,7 +205,8 @@ multi_get_create_instance_udp(struct multi_context *m, bool *floated)
             if (!m->top.c2.tls_auth_standalone
                 || tls_pre_decrypt_lite(m->top.c2.tls_auth_standalone, &m->top.c2.from, &m->top.c2.buf))
             {
-                if (frequency_limit_event_allowed(m->new_connection_limiter))
+                if (dangling_instances_per_source_allowed(m, &real)
+                    && frequency_limit_event_allowed(m->new_connection_limiter))

I agree with Steffan that it would be nice to have a different error message for DIPSA.

Any specific reason why these checks are UDP only?

Quoted 33 lines
src/openvpn/mudp.c +0 -0
                 {
                     mi = multi_create_instance(m, &real);
                     if (mi)
src/openvpn/options.c +5 -1
index 26f056fc..b8e28df6 100644
@@ -6644,7 +6644,7 @@ add_option(struct options *options,
         options->real_hash_size = real;
         options->virtual_hash_size = real;
     }
-    else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[3])
+    else if (streq(p[0], "connect-freq") && p[1] && p[2] && !p[4])
     {
         int cf_max, cf_per;
 
@@ -6658,6 +6658,10 @@ add_option(struct options *options,
         }
         options->cf_max = cf_max;
         options->cf_per = cf_per;
+	if ( p[3] )
+	{
+	    options->cf_max_dangling = atoi(p[3]);
+	}
     }
     else if (streq(p[0], "max-clients") && p[1] && !p[2])
     {
src/openvpn/options.h +0 -0
index e2b38939..26a78878 100644

src/openvpn/options.h struct options · line 457

Quoted 7 lines
src/openvpn/options.h +1 -0
@@ -451,6 +451,7 @@ struct options
     bool duplicate_cn;
     int cf_max;
     int cf_per;
+    int cf_max_dangling;
     int max_clients;
     int max_routes_per_client;
     int stale_routes_check_interval;

All in all I think this is a non-invasive patch which introduces a nice additional barrier against DoS.

[Still requires some style polishing]

Regards,

Signature · 1 line
Antonio Quartulli