]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
receiver: Add per-IP cookie threshold
authorTobias Brunner <tobias@strongswan.org>
Fri, 18 Mar 2022 14:13:06 +0000 (15:13 +0100)
committerTobias Brunner <tobias@strongswan.org>
Thu, 14 Apr 2022 13:28:07 +0000 (15:28 +0200)
Because the global cookie threshold is higher than the per-IP block
threshold, it was previously possible for an attacker to block a legitimate
user by sending spoofed IKE_SA_INIT packets from that user's IP.

The timespan for requiring cookies is now also not extended anymore with
every IKE_SA_INIT received during the calm down period.  Because this
allowed an attacker, after initially triggering the global cookie threshold,
to force cookies for all clients by sending just a single spoofed
IKE_SA_INIT every 10 seconds.

We keep track of reaching the per-IP threshold in segments of the hashed
IP addresses, so only a (random, due to chunk_hash()'s random key) subset
of clients will receive cookies, if single IPs are targeted.

The default global threshold is increased a bit.

conf/options/charon.opt
src/libcharon/network/receiver.c

index df323304695b07f73af5527c67424e20c59fdae2..8126f990f0344d774878b92df8389b946ccc7198 100644 (file)
@@ -69,9 +69,13 @@ charon.cisco_unity = no
 charon.close_ike_on_child_failure = no
        Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed.
 
-charon.cookie_threshold = 10
+charon.cookie_threshold = 30
        Number of half-open IKE_SAs that activate the cookie mechanism.
 
+charon.cookie_threshold_ip = 3
+       Number of half-open IKE_SAs for a single peer IP that activate the cookie
+       mechanism.
+
 charon.crypto_test.bench = no
        Benchmark crypto algorithms and order them by efficiency.
 
index 8ab413e64aa5d662ec9f0b9827082876a3fec151..9a786bc34e3f9b258c7942fd61f4dd8163907478 100644 (file)
 #define COOKIE_LIFETIME 10
 /** time we wait before disabling cookies */
 #define COOKIE_CALMDOWN_DELAY 10
+/** number of per-IP timestamps we keep track of (must be a power of 2) */
+#define COOKIE_CALMDOWN_BUCKETS 32
+/** mask applied to IP address hashes to determine the timestamp index */
+#define COOKIE_CALMDOWN_MASK (COOKIE_CALMDOWN_BUCKETS-1)
 /** time in seconds we use a secret at most (since we keep two secrets, must
  * be at least COOKIE_LIFETIME to process all outstanding valid cookies)  */
 #define COOKIE_SECRET_SWITCH 120
 /** default value for private_receiver_t.cookie_threshold */
-#define COOKIE_THRESHOLD_DEFAULT 10
+#define COOKIE_THRESHOLD_DEFAULT 30
+/** default value for private_receiver_t.cookie_threshold_ip (must be lower
+ * than BLOCK_THRESHOLD_DEFAULT) */
+#define COOKIE_THRESHOLD_IP_DEFAULT 3
 /** default value for private_receiver_t.block_threshold */
 #define BLOCK_THRESHOLD_DEFAULT 5
 /** length of the secret to use for cookie calculation */
@@ -102,17 +109,23 @@ struct private_receiver_t {
        /**
         * require cookies after this many half open IKE_SAs
         */
-       uint32_t cookie_threshold;
+       u_int cookie_threshold;
 
        /**
-        * timestamp of last cookie requested
+        * require cookies for a specific IP after this many half open IKE_SAs
         */
-       time_t last_cookie;
+       u_int cookie_threshold_ip;
+
+       /**
+        * global (0) and per-IP hash (1..n) timestamps of when the threshold was
+        * last reached
+        */
+       time_t last_threshold[COOKIE_CALMDOWN_BUCKETS+1];
 
        /**
         * how many half open IKE_SAs per peer before blocking
         */
-       uint32_t block_threshold;
+       u_int block_threshold;
 
        /**
         * Drop IKE_SA_INIT requests if processor job load exceeds this limit
@@ -283,30 +296,67 @@ static bool check_cookie(private_receiver_t *this, message_t *message)
 }
 
 /**
- * Check if we currently require cookies
+ * Struct to keep track of half-open SA counts
+ */
+typedef struct {
+       u_int count;
+       bool determined;
+} half_open_count_t;
+
+/**
+ * Get the number of half-open SAs, cached or from the manager, either global
+ * or for a single IP.
+ */
+static u_int get_half_open_count(half_open_count_t *this, host_t *src)
+{
+       if (!this->determined)
+       {
+               this->determined = TRUE;
+               this->count = charon->ike_sa_manager->get_half_open_count(
+                                                                                       charon->ike_sa_manager, src, TRUE);
+       }
+       return this->count;
+}
+
+/**
+ * Check if we currently require cookies either globally or for the given IP
  */
 static bool cookie_required(private_receiver_t *this,
-                                                       u_int half_open, uint32_t now)
+                                                       half_open_count_t *half_open, host_t *src,
+                                                       uint32_t now)
 {
-       if (this->cookie_threshold && half_open >= this->cookie_threshold)
+       u_int threshold = src ? this->cookie_threshold_ip : this->cookie_threshold;
+       u_int idx = 0;
+
+       if (!threshold)
+       {
+               return FALSE;
+       }
+       if (src)
        {
-               this->last_cookie = now;
+               /* keep track of IPs in segments so not all are affected if a single
+                * IP is targeted */
+               idx = 1 + (chunk_hash(src->get_address(src)) & COOKIE_CALMDOWN_MASK);
+       }
+       if (get_half_open_count(half_open, src) >= threshold)
+       {
+               this->last_threshold[idx] = now;
                return TRUE;
        }
-       if (this->last_cookie && now < this->last_cookie + COOKIE_CALMDOWN_DELAY)
+       if (this->last_threshold[idx] &&
+               now < this->last_threshold[idx] + COOKIE_CALMDOWN_DELAY)
        {
-               /* We don't disable cookies unless we haven't seen IKE_SA_INITs
-                * for COOKIE_CALMDOWN_DELAY seconds. This avoids jittering between
+               /* We don't disable cookies unless the threshold was not reached for
+                * COOKIE_CALMDOWN_DELAY seconds. This avoids jittering between
                 * cookie on / cookie off states, which is problematic. Consider the
-                * following: A legitimate initiator sends a IKE_SA_INIT while we
+                * following: A legitimate initiator sends an IKE_SA_INIT while we
                 * are under a DoS attack. If we toggle our cookie behavior,
                 * multiple retransmits of this IKE_SA_INIT might get answered with
                 * and without cookies. The initiator goes on and retries with
                 * a cookie, but it can't know if the completing IKE_SA_INIT response
                 * is to its IKE_SA_INIT request with or without cookies. This is
-                * problematic, as the cookie is part of AUTH payload data.
+                * problematic, as the cookie is part of the AUTH payload data.
                 */
-               this->last_cookie = now;
                return TRUE;
        }
        return FALSE;
@@ -317,22 +367,23 @@ static bool cookie_required(private_receiver_t *this,
  */
 static bool drop_ike_sa_init(private_receiver_t *this, message_t *message)
 {
-       u_int half_open;
+       half_open_count_t half_open = {}, half_open_ip = {};
+       host_t *src;
        uint32_t now;
 
+       src = message->get_source(message);
        now = time_monotonic(NULL);
-       half_open = charon->ike_sa_manager->get_half_open_count(
-                                                                               charon->ike_sa_manager, NULL, TRUE);
 
        /* check for cookies in IKEv2 */
        if (message->get_major_version(message) == IKEV2_MAJOR_VERSION &&
-               cookie_required(this, half_open, now) && !check_cookie(this, message))
+               (cookie_required(this, &half_open, NULL, now) ||
+                cookie_required(this, &half_open_ip, src, now)) &&
+               !check_cookie(this, message))
        {
                chunk_t cookie;
 
                DBG2(DBG_NET, "received packet: from %#H to %#H (%zu bytes)",
-                        message->get_source(message),
-                        message->get_destination(message),
+                        src, message->get_destination(message),
                         message->get_packet_data(message).len);
 
                if (!this->secret_first_use)
@@ -362,8 +413,7 @@ static bool drop_ike_sa_init(private_receiver_t *this, message_t *message)
                {
                        return TRUE;
                }
-               DBG2(DBG_NET, "sending COOKIE notify to %H",
-                        message->get_source(message));
+               DBG2(DBG_NET, "sending COOKIE notify to %H", src);
                send_notify(message, IKEV2_MAJOR_VERSION, IKE_SA_INIT, COOKIE, cookie);
                chunk_free(&cookie);
                return TRUE;
@@ -371,21 +421,20 @@ static bool drop_ike_sa_init(private_receiver_t *this, message_t *message)
 
        /* check if peer has too many IKE_SAs half open */
        if (this->block_threshold &&
-               charon->ike_sa_manager->get_half_open_count(charon->ike_sa_manager,
-                               message->get_source(message), TRUE) >= this->block_threshold)
+               get_half_open_count(&half_open_ip, src) >= this->block_threshold)
+
        {
-               DBG1(DBG_NET, "ignoring IKE_SA setup from %H, "
-                        "peer too aggressive", message->get_source(message));
+               DBG1(DBG_NET, "ignoring IKE_SA setup from %H, per-IP half-open IKE_SA "
+                        "limit of %d reached", src, this->block_threshold);
                return TRUE;
        }
 
        /* check if global half open IKE_SA limit reached */
        if (this->init_limit_half_open &&
-           half_open >= this->init_limit_half_open)
+           get_half_open_count(&half_open, NULL) >= this->init_limit_half_open)
        {
-               DBG1(DBG_NET, "ignoring IKE_SA setup from %H, half open IKE_SA "
-                        "count of %d exceeds limit of %d", message->get_source(message),
-                        half_open, this->init_limit_half_open);
+               DBG1(DBG_NET, "ignoring IKE_SA setup from %H, half-open IKE_SA "
+                        "limit of %d reached", src, this->init_limit_half_open);
                return TRUE;
        }
 
@@ -401,8 +450,7 @@ static bool drop_ike_sa_init(private_receiver_t *this, message_t *message)
                if (jobs > this->init_limit_job_load)
                {
                        DBG1(DBG_NET, "ignoring IKE_SA setup from %H, job load of %d "
-                                "exceeds limit of %d", message->get_source(message),
-                                jobs, this->init_limit_job_load);
+                                "exceeds limit of %d", src, jobs, this->init_limit_job_load);
                        return TRUE;
                }
        }
@@ -487,8 +535,7 @@ static job_requeue_t receive_packets(private_receiver_t *this)
        message = message_create_from_packet(packet);
        if (message->parse_header(message) != SUCCESS)
        {
-               DBG1(DBG_NET, "received invalid IKE header from %H - ignored",
-                        packet->get_source(packet));
+               DBG1(DBG_NET, "received invalid IKE header from %H - ignored", src);
                charon->bus->alert(charon->bus, ALERT_PARSE_ERROR_HEADER, message);
                message->destroy(message);
                return JOB_REQUEUE_DIRECT;
@@ -535,7 +582,7 @@ static job_requeue_t receive_packets(private_receiver_t *this)
        {
                DBG1(DBG_NET, "received unsupported IKE version %d.%d from %H, sending "
                         "INVALID_MAJOR_VERSION", message->get_major_version(message),
-                        message->get_minor_version(message), packet->get_source(packet));
+                        message->get_minor_version(message), src);
                message->destroy(message);
                return JOB_REQUEUE_DIRECT;
        }
@@ -637,8 +684,18 @@ receiver_t *receiver_create()
        {
                this->cookie_threshold = lib->settings->get_int(lib->settings,
                                        "%s.cookie_threshold", COOKIE_THRESHOLD_DEFAULT, lib->ns);
+               this->cookie_threshold_ip = lib->settings->get_int(lib->settings,
+                                       "%s.cookie_threshold_ip", COOKIE_THRESHOLD_IP_DEFAULT, lib->ns);
                this->block_threshold = lib->settings->get_int(lib->settings,
                                        "%s.block_threshold", BLOCK_THRESHOLD_DEFAULT, lib->ns);
+
+               if (this->cookie_threshold_ip >= this->block_threshold)
+               {
+                       this->block_threshold = this->cookie_threshold_ip + 1;
+                       DBG1(DBG_NET, "increasing block threshold to %u due to per-IP "
+                                "cookie threshold of %u", this->block_threshold,
+                                this->cookie_threshold_ip);
+               }
        }
        this->init_limit_job_load = lib->settings->get_int(lib->settings,
                                        "%s.init_limit_job_load", 0, lib->ns);