[tor-commits] [tor/master] dos: New client connect rate detection

ahf at torproject.org ahf at torproject.org
Mon Feb 22 21:03:05 UTC 2021


commit 94b56eaa7597e4a091a5b51d2c9032ea046631e3
Author: David Goulet <dgoulet at torproject.org>
Date:   Tue Jan 26 11:42:52 2021 -0500

    dos: New client connect rate detection
    
    This is a new detection type which is that a relay can now control the rate of
    client connections from a single address.
    
    The mechanism is pretty simple, if the rate/burst is reached, the address is
    marked for a period of time and any connection from that address is denied.
    
    Closes #40253
    
    Signed-off-by: David Goulet <dgoulet at torproject.org>
---
 changes/ticket40253             |   3 +
 doc/man/tor.1.txt               |  24 ++++++++
 src/core/or/dos.c               | 122 ++++++++++++++++++++++++++++++++++++++++
 src/core/or/dos.h               |  30 +++++++++-
 src/core/or/dos_options.inc     |  12 ++++
 src/feature/stats/geoip_stats.c |   2 +
 6 files changed, 192 insertions(+), 1 deletion(-)

diff --git a/changes/ticket40253 b/changes/ticket40253
new file mode 100644
index 0000000000..ca7c207bb3
--- /dev/null
+++ b/changes/ticket40253
@@ -0,0 +1,3 @@
+  o Major feature (relay, denial of service):
+    - Add a new DoS subsystem feature to control the rate of client connections
+      for relays. Closes ticket 40253.
diff --git a/doc/man/tor.1.txt b/doc/man/tor.1.txt
index 3538d94b8e..3756d26522 100644
--- a/doc/man/tor.1.txt
+++ b/doc/man/tor.1.txt
@@ -2936,6 +2936,30 @@ Denial of Service mitigation subsystem described above.
     consensus, the value is 100.
     (Default: 0)
 
+[[DoSConnectionConnectRate]] **DoSConnectionConnectRate** __NUM__::
+
+    The allowed rate of client connection from a single address per second.
+    Coupled with the burst (see below), if the limit is reached, the address
+    is marked and a defense is applied (DoSConnectionDefenseType) for a period
+    of time defined by DoSConnectionConnectDefenseTimePeriod. If not defined
+    or set to 0, it is controlled by a consensus parameter.
+    (Default: 0)
+
+[[DoSConnectionConnectBurst]] **DoSConnectionConnectBurst** __NUM__::
+
+    The allowed burst of client connection from a single address per second.
+    See the DoSConnectionConnectRate for more details on this detection. If
+    not defined or set to 0, it is controlled by a consensus parameter.
+    (Default: 0)
+
+[[DoSConnectionConnectDefenseTimePeriod]] **DoSConnectionConnectDefenseTimePeriod** __N__ **seconds**|**minutes**|**hours**::
+
+    The base time period in seconds that the client connection defense is
+    activated for. The actual value is selected randomly for each activation
+    from N+1 to 3/2 * N. If not defined or set to 0, it is controlled by a
+    consensus parameter.
+    (Default: 24 hours)
+
 [[DoSRefuseSingleHopClientRendezvous]] **DoSRefuseSingleHopClientRendezvous** **0**|**1**|**auto**::
 
     Refuse establishment of rendezvous points for single hop clients. In other
diff --git a/src/core/or/dos.c b/src/core/or/dos.c
index a761082be0..8b3dccc871 100644
--- a/src/core/or/dos.c
+++ b/src/core/or/dos.c
@@ -63,9 +63,14 @@ static unsigned int dos_conn_enabled = 0;
  * They are initialized with the hardcoded default values. */
 static uint32_t dos_conn_max_concurrent_count;
 static dos_conn_defense_type_t dos_conn_defense_type;
+static uint32_t dos_conn_connect_rate = DOS_CONN_CONNECT_RATE_DEFAULT;
+static uint32_t dos_conn_connect_burst = DOS_CONN_CONNECT_BURST_DEFAULT;
+static int32_t dos_conn_connect_defense_time_period =
+  DOS_CONN_CONNECT_DEFENSE_TIME_PERIOD_DEFAULT;
 
 /* Keep some stats for the heartbeat so we can report out. */
 static uint64_t conn_num_addr_rejected;
+static uint64_t conn_num_addr_connect_rejected;
 
 /*
  * General interface of the denial of service mitigation subsystem.
@@ -190,6 +195,47 @@ get_param_conn_defense_type(const networkstatus_t *ns)
                                  DOS_CONN_DEFENSE_NONE, DOS_CONN_DEFENSE_MAX);
 }
 
+/* Return the connection connect rate parameters either from the configuration
+ * file or, if not found, consensus parameter. */
+static uint32_t
+get_param_conn_connect_rate(const networkstatus_t *ns)
+{
+  if (dos_get_options()->DoSConnectionConnectRate) {
+    return dos_get_options()->DoSConnectionConnectRate;
+  }
+  return networkstatus_get_param(ns, "DoSConnectionConnectRate",
+                                 DOS_CONN_CONNECT_RATE_DEFAULT,
+                                 1, INT32_MAX);
+}
+
+/* Return the connection connect burst parameters either from the
+ * configuration file or, if not found, consensus parameter. */
+static uint32_t
+get_param_conn_connect_burst(const networkstatus_t *ns)
+{
+  if (dos_get_options()->DoSConnectionConnectBurst) {
+    return dos_get_options()->DoSConnectionConnectBurst;
+  }
+  return networkstatus_get_param(ns, "DoSConnectionConnectBurst",
+                                 DOS_CONN_CONNECT_BURST_DEFAULT,
+                                 1, INT32_MAX);
+}
+
+/* Return the connection connect defense time period from the configuration
+ * file or, if not found, the consensus parameter. */
+static int32_t
+get_param_conn_connect_defense_time_period(const networkstatus_t *ns)
+{
+  /* Time in seconds. */
+  if (dos_get_options()->DoSConnectionConnectDefenseTimePeriod) {
+    return dos_get_options()->DoSConnectionConnectDefenseTimePeriod;
+  }
+  return networkstatus_get_param(ns, "DoSConnectionConnectDefenseTimePeriod",
+                                 DOS_CONN_CONNECT_DEFENSE_TIME_PERIOD_DEFAULT,
+                                 DOS_CONN_CONNECT_DEFENSE_TIME_PERIOD_MIN,
+                                 INT32_MAX);
+}
+
 /* Set circuit creation parameters located in the consensus or their default
  * if none are present. Called at initialization or when the consensus
  * changes. */
@@ -208,6 +254,10 @@ set_dos_parameters(const networkstatus_t *ns)
   dos_conn_enabled = get_param_conn_enabled(ns);
   dos_conn_max_concurrent_count = get_param_conn_max_concurrent_count(ns);
   dos_conn_defense_type = get_param_conn_defense_type(ns);
+  dos_conn_connect_rate = get_param_conn_connect_rate(ns);
+  dos_conn_connect_burst = get_param_conn_connect_burst(ns);
+  dos_conn_connect_defense_time_period =
+    get_param_conn_connect_defense_time_period(ns);
 }
 
 /* Free everything for the circuit creation DoS mitigation subsystem. */
@@ -405,6 +455,20 @@ cc_channel_addr_is_marked(channel_t *chan)
 
 /* Concurrent connection private API. */
 
+/* Mark client connection stats by setting a timestamp which tells us until
+ * when it is marked as positively detected. */
+static void
+conn_mark_client(conn_client_stats_t *stats)
+{
+  tor_assert(stats);
+
+  /* We add a random offset of a maximum of half the defense time so it is
+   * less predictable and thus more difficult to game. */
+  stats->marked_until_ts =
+    approx_time() + dos_conn_connect_defense_time_period +
+    crypto_rand_int_range(1, dos_conn_connect_defense_time_period / 2);
+}
+
 /* Free everything for the connection DoS mitigation subsystem. */
 static void
 conn_free_all(void)
@@ -424,6 +488,32 @@ conn_consensus_has_changed(const networkstatus_t *ns)
   }
 }
 
+/** Called when a new client connection has arrived. The following will update
+ * the client connection statistics.
+ *
+ * If the connect counter reaches its limit, it is marked. */
+static void
+conn_update_on_connect(conn_client_stats_t *stats)
+{
+  tor_assert(stats);
+
+  /* Refill connect connection count. */
+  token_bucket_ctr_refill(&stats->connect_count, (uint32_t) approx_time());
+
+  /* Decrement counter for this new connection. */
+  if (token_bucket_ctr_get(&stats->connect_count) > 0) {
+    token_bucket_ctr_dec(&stats->connect_count, 1);
+  }
+
+  /* Assess connect counter. Mark it if counter is down to 0 and we haven't
+   * marked it before or it was reset. This is to avoid to re-mark it over and
+   * over again extending continously the blocked time. */
+  if (token_bucket_ctr_get(&stats->connect_count) == 0 &&
+      stats->marked_until_ts == 0) {
+    conn_mark_client(stats);
+  }
+}
+
 /* General private API */
 
 /* Return true iff we have at least one DoS detection enabled. This is used to
@@ -549,6 +639,16 @@ dos_conn_addr_get_defense_type(const tor_addr_t *addr)
     goto end;
   }
 
+  /* Is this address marked as making too many client connections? */
+  if (entry->dos_stats.conn_stats.marked_until_ts >= approx_time()) {
+    conn_num_addr_connect_rejected++;
+    return dos_conn_defense_type;
+  }
+  /* Reset it to 0 here so that if the marked timestamp has expired that is
+   * we've gone beyond it, we have to reset it so the detection can mark it
+   * again in the future. */
+  entry->dos_stats.conn_stats.marked_until_ts = 0;
+
   /* Need to be above the maximum concurrent connection count to trigger a
    * defense. */
   if (entry->dos_stats.concurrent_count > dos_conn_max_concurrent_count) {
@@ -597,6 +697,22 @@ dos_geoip_entry_about_to_free(const clientmap_entry_t *geoip_ent)
   return;
 }
 
+/** A new geoip client entry has been allocated, initialize its DoS object. */
+void
+dos_geoip_entry_init(clientmap_entry_t *geoip_ent)
+{
+  tor_assert(geoip_ent);
+
+  /* Initialize the connection count counter with the rate and burst
+   * parameters taken either from configuration or consensus.
+   *
+   * We do this even if the DoS connection detection is not enabled because it
+   * can be enabled at runtime and these counters need to be valid. */
+  token_bucket_ctr_init(&geoip_ent->dos_stats.conn_stats.connect_count,
+                        dos_conn_connect_rate, dos_conn_connect_burst,
+                        (uint32_t) approx_time());
+}
+
 /* Note down that we've just refused a single hop client. This increments a
  * counter later used for the heartbeat. */
 void
@@ -650,6 +766,9 @@ dos_log_heartbeat(void)
     tor_asprintf(&conn_msg,
                  " %" PRIu64 " connections closed.",
                  conn_num_addr_rejected);
+    tor_asprintf(&conn_msg,
+                 " %" PRIu64 " connect() connections closed.",
+                 conn_num_addr_connect_rejected);
   }
 
   if (dos_should_refuse_single_hop_client()) {
@@ -711,6 +830,9 @@ dos_new_client_conn(or_connection_t *or_conn, const char *transport_name)
     goto end;
   }
 
+  /* Update stats from this new connect. */
+  conn_update_on_connect(&entry->dos_stats.conn_stats);
+
   entry->dos_stats.concurrent_count++;
   or_conn->tracked_for_dos_mitigation = 1;
   log_debug(LD_DOS, "Client address %s has now %u concurrent connections.",
diff --git a/src/core/or/dos.h b/src/core/or/dos.h
index 62c3857409..9dba63531b 100644
--- a/src/core/or/dos.h
+++ b/src/core/or/dos.h
@@ -11,7 +11,9 @@
 
 #include "core/or/or.h"
 
-/* Structure that keeps stats of client connection per-IP. */
+#include "lib/evloop/token_bucket.h"
+
+/* Structure that keeps stats of circuit creation per client connection IP. */
 typedef struct cc_client_stats_t {
   /* Number of allocated circuits remaining for this address.  It is
    * decremented every time a new circuit is seen for this client address and
@@ -30,6 +32,18 @@ typedef struct cc_client_stats_t {
   time_t marked_until_ts;
 } cc_client_stats_t;
 
+/* Structure that keeps stats of client connection per-IP. */
+typedef struct conn_client_stats_t {
+  /* Connect count from the specific address. We use a token bucket here to
+   * track the rate and burst of connections from the same IP address.*/
+  token_bucket_ctr_t connect_count;
+
+  /* The client address attempted too many connections, per the connect_count
+   * rules, and thus is marked so defense(s) can be applied. It is
+   * synchronized using the approx_time(). */
+  time_t marked_until_ts;
+} conn_client_stats_t;
+
 /* This object is a top level object that contains everything related to the
  * per-IP client DoS mitigation. Because it is per-IP, it is used in the geoip
  * clientmap_entry_t object. */
@@ -38,6 +52,9 @@ typedef struct dos_client_stats_t {
    * likely way too big for the amount of allowed file descriptors. */
   uint32_t concurrent_count;
 
+  /* Client connection statistics. */
+  conn_client_stats_t conn_stats;
+
   /* Circuit creation statistics. This is only used if the circuit creation
    * subsystem has been enabled (dos_cc_enabled). */
   cc_client_stats_t cc_stats;
@@ -53,6 +70,7 @@ void dos_free_all(void);
 void dos_consensus_has_changed(const networkstatus_t *ns);
 int dos_enabled(void);
 void dos_log_heartbeat(void);
+void dos_geoip_entry_init(struct clientmap_entry_t *geoip_ent);
 void dos_geoip_entry_about_to_free(const struct clientmap_entry_t *geoip_ent);
 
 void dos_new_client_conn(or_connection_t *or_conn,
@@ -104,6 +122,16 @@ dos_cc_defense_type_t dos_cc_get_defense_type(channel_t *chan);
 #define DOS_CONN_MAX_CONCURRENT_COUNT_DEFAULT 100
 /* DoSConnectionDefenseType maps to the dos_conn_defense_type_t enum. */
 #define DOS_CONN_DEFENSE_TYPE_DEFAULT DOS_CONN_DEFENSE_CLOSE
+/* DoSConnectionConnectRate default. Per second. */
+#define DOS_CONN_CONNECT_RATE_DEFAULT 20
+/* DoSConnectionConnectBurst default. Per second. */
+#define DOS_CONN_CONNECT_BURST_DEFAULT 40
+/* DoSConnectionConnectDefenseTimePeriod default. Set to 24 hours. */
+#define DOS_CONN_CONNECT_DEFENSE_TIME_PERIOD_DEFAULT (24 * 60 * 60)
+/* DoSCircuitCreationDefenseTimePeriod minimum value. Because we add a random
+ * offset to the marked timestamp, we need the minimum value to be non zero.
+ * We consider that 10 seconds is an acceptable lower bound. */
+#define DOS_CONN_CONNECT_DEFENSE_TIME_PERIOD_MIN (10)
 
 /* Type of defense that we can use for the concurrent connection DoS
  * mitigation. */
diff --git a/src/core/or/dos_options.inc b/src/core/or/dos_options.inc
index 063a739939..9baa7a35b8 100644
--- a/src/core/or/dos_options.inc
+++ b/src/core/or/dos_options.inc
@@ -44,4 +44,16 @@ CONF_VAR(DoSConnectionDefenseType, INT, 0, "0")
 /** Autobool: Do we refuse single hop client rendezvous? */
 CONF_VAR(DoSRefuseSingleHopClientRendezvous, AUTOBOOL, 0, "auto")
 
+/** Allowed burst of client connection allowed per address. */
+CONF_VAR(DoSConnectionConnectBurst, POSINT, 0, "0")
+
+/** Allowed rate of client connection allowed per address. */
+CONF_VAR(DoSConnectionConnectRate, POSINT, 0, "0")
+
+/** For how much time (in seconds) the connection connect rate defense is
+* applicable for a malicious address. A random time delta is added to the
+* defense time of an address which will be between 1 second and half of this
+* value. */
+CONF_VAR(DoSConnectionConnectDefenseTimePeriod, INTERVAL, 0, "0")
+
 END_CONF_STRUCT(dos_options_t)
diff --git a/src/feature/stats/geoip_stats.c b/src/feature/stats/geoip_stats.c
index a733653dde..aa9f91c136 100644
--- a/src/feature/stats/geoip_stats.c
+++ b/src/feature/stats/geoip_stats.c
@@ -196,6 +196,8 @@ clientmap_entry_new(geoip_client_action_t action, const tor_addr_t *addr,
   if (transport_name) {
     entry->transport_name = tor_strdup(transport_name);
   }
+  /* Initialize the DoS object. */
+  dos_geoip_entry_init(entry);
 
   /* Allocated and initialized, note down its size for the OOM handler. */
   geoip_increment_client_history_cache_size(clientmap_entry_size(entry));





More information about the tor-commits mailing list