summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/ticket402533
-rw-r--r--doc/man/tor.1.txt24
-rw-r--r--src/core/or/dos.c179
-rw-r--r--src/core/or/dos.h37
-rw-r--r--src/core/or/dos_options.inc12
-rw-r--r--src/feature/stats/geoip_stats.c2
-rw-r--r--src/test/test_dos.c65
7 files changed, 297 insertions, 25 deletions
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 d51183e04a..9bc7efc788 100644
--- a/doc/man/tor.1.txt
+++ b/doc/man/tor.1.txt
@@ -2941,6 +2941,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..ba4e5442d6 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. */
@@ -349,7 +399,7 @@ cc_has_exhausted_circuits(const dos_client_stats_t *stats)
{
tor_assert(stats);
return stats->cc_stats.circuit_bucket == 0 &&
- stats->concurrent_count >= dos_cc_min_concurrent_conn;
+ stats->conn_stats.concurrent_count >= dos_cc_min_concurrent_conn;
}
/* Mark client address by setting a timestamp in the stats object which tells
@@ -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,63 @@ conn_consensus_has_changed(const networkstatus_t *ns)
}
}
+/** Called when a new client connection has arrived. The following will update
+ * the client connection statistics.
+ *
+ * The addr is used for logging purposes only.
+ *
+ * If the connect counter reaches its limit, it is marked. */
+static void
+conn_update_on_connect(conn_client_stats_t *stats, const tor_addr_t *addr)
+{
+ tor_assert(stats);
+ tor_assert(addr);
+
+ /* Update concurrent count for this new connect. */
+ stats->concurrent_count++;
+
+ /* 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);
+ }
+
+ log_debug(LD_DOS, "Client address %s has now %u concurrent connections. "
+ "Remaining %lu/sec connections are allowed.",
+ fmt_addr(addr), stats->concurrent_count,
+ token_bucket_ctr_get(&stats->connect_count));
+}
+
+/** Called when a client connection is closed. The following will update
+ * the client connection statistics.
+ *
+ * The addr is used for logging purposes only. */
+static void
+conn_update_on_close(conn_client_stats_t *stats, const tor_addr_t *addr)
+{
+ /* Extra super duper safety. Going below 0 means an underflow which could
+ * lead to most likely a false positive. In theory, this should never happen
+ * but lets be extra safe. */
+ if (BUG(stats->concurrent_count == 0)) {
+ return;
+ }
+
+ stats->concurrent_count--;
+ log_debug(LD_DOS, "Client address %s has lost a connection. Concurrent "
+ "connections are now at %u",
+ fmt_addr(addr), stats->concurrent_count);
+}
+
/* General private API */
/* Return true iff we have at least one DoS detection enabled. This is used to
@@ -549,9 +670,20 @@ 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) {
+ if (entry->dos_stats.conn_stats.concurrent_count >
+ dos_conn_max_concurrent_count) {
conn_num_addr_rejected++;
return dos_conn_defense_type;
}
@@ -576,7 +708,7 @@ dos_geoip_entry_about_to_free(const clientmap_entry_t *geoip_ent)
/* The count is down to 0 meaning no connections right now, we can safely
* clear the geoip entry from the cache. */
- if (geoip_ent->dos_stats.concurrent_count == 0) {
+ if (geoip_ent->dos_stats.conn_stats.concurrent_count == 0) {
goto end;
}
@@ -597,6 +729,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 +798,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,11 +862,11 @@ dos_new_client_conn(or_connection_t *or_conn, const char *transport_name)
goto end;
}
- entry->dos_stats.concurrent_count++;
+ /* Update stats from this new connect. */
+ conn_update_on_connect(&entry->dos_stats.conn_stats,
+ &TO_CONN(or_conn)->addr);
+
or_conn->tracked_for_dos_mitigation = 1;
- log_debug(LD_DOS, "Client address %s has now %u concurrent connections.",
- fmt_addr(&TO_CONN(or_conn)->addr),
- entry->dos_stats.concurrent_count);
end:
return;
@@ -745,18 +896,8 @@ dos_close_client_conn(const or_connection_t *or_conn)
goto end;
}
- /* Extra super duper safety. Going below 0 means an underflow which could
- * lead to most likely a false positive. In theory, this should never happen
- * but lets be extra safe. */
- if (BUG(entry->dos_stats.concurrent_count == 0)) {
- goto end;
- }
-
- entry->dos_stats.concurrent_count--;
- log_debug(LD_DOS, "Client address %s has lost a connection. Concurrent "
- "connections are now at %u",
- fmt_addr(&TO_CONN(or_conn)->addr),
- entry->dos_stats.concurrent_count);
+ /* Update stats from this new close. */
+ conn_update_on_close(&entry->dos_stats.conn_stats, &TO_CONN(or_conn)->addr);
end:
return;
diff --git a/src/core/or/dos.h b/src/core/or/dos.h
index 62c3857409..3153a1fc5f 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,13 +32,28 @@ 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 {
+ /* Concurrent connection count from the specific address. 2^32 - 1 is most
+ * likely way too big for the amount of allowed file descriptors. */
+ uint32_t concurrent_count;
+
+ /* 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. */
typedef struct dos_client_stats_t {
- /* Concurrent connection count from the specific address. 2^32 is most
- * 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). */
@@ -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. */
@@ -127,6 +155,7 @@ STATIC uint32_t get_param_conn_max_concurrent_count(
STATIC uint32_t get_param_cc_circuit_burst(const networkstatus_t *ns);
STATIC uint32_t get_param_cc_min_concurrent_connection(
const networkstatus_t *ns);
+STATIC uint32_t get_param_conn_connect_burst(const networkstatus_t *ns);
STATIC uint64_t get_circuit_rate_per_second(void);
STATIC void cc_stats_refill_bucket(cc_client_stats_t *stats,
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));
diff --git a/src/test/test_dos.c b/src/test/test_dos.c
index 850bbef59b..d9ddaec108 100644
--- a/src/test/test_dos.c
+++ b/src/test/test_dos.c
@@ -79,6 +79,9 @@ test_dos_conn_creation(void *arg)
{ /* Register many conns from this client but not enough to get it blocked */
unsigned int i;
for (i = 0; i < max_concurrent_conns; i++) {
+ /* Don't trigger the connect() rate limitation so advance the clock 1
+ * second for each connection. */
+ update_approx_time(++now);
dos_new_client_conn(&or_conn, NULL);
}
}
@@ -470,7 +473,7 @@ test_known_relay(void *arg)
GEOIP_CLIENT_CONNECT);
tt_assert(entry);
/* We should have a count of 0. */
- tt_uint_op(entry->dos_stats.concurrent_count, OP_EQ, 0);
+ tt_uint_op(entry->dos_stats.conn_stats.concurrent_count, OP_EQ, 0);
/* To make sure that his is working properly, make a unknown client
* connection and see if we do get it. */
@@ -483,7 +486,7 @@ test_known_relay(void *arg)
GEOIP_CLIENT_CONNECT);
tt_assert(entry);
/* We should have a count of 2. */
- tt_uint_op(entry->dos_stats.concurrent_count, OP_EQ, 2);
+ tt_uint_op(entry->dos_stats.conn_stats.concurrent_count, OP_EQ, 2);
done:
routerstatus_free(rs); routerinfo_free(ri); microdesc_free(md);
@@ -496,11 +499,69 @@ test_known_relay(void *arg)
UNMOCK(get_param_cc_enabled);
}
+/** Test that the connection tracker of the DoS subsystem will block clients
+ * who try to establish too many connections */
+static void
+test_dos_conn_rate(void *arg)
+{
+ (void) arg;
+
+ MOCK(get_param_cc_enabled, mock_enable_dos_protection);
+ MOCK(get_param_conn_enabled, mock_enable_dos_protection);
+
+ /* Initialize test data */
+ or_connection_t or_conn;
+ time_t now = 1281533250; /* 2010-08-11 13:27:30 UTC */
+ tt_int_op(AF_INET,OP_EQ, tor_addr_parse(&TO_CONN(&or_conn)->addr,
+ "18.0.0.1"));
+ tor_addr_t *addr = &TO_CONN(&or_conn)->addr;
+ update_approx_time(now);
+
+ /* Get DoS subsystem limits */
+ dos_init();
+ uint32_t burst_conn = get_param_conn_connect_burst(NULL);
+
+ /* Introduce new client */
+ geoip_note_client_seen(GEOIP_CLIENT_CONNECT, addr, NULL, now);
+ { /* Register many conns from this client but not enough to get it blocked */
+ unsigned int i;
+ for (i = 0; i < burst_conn - 1; i++) {
+ dos_new_client_conn(&or_conn, NULL);
+ }
+ }
+
+ /* Check that new conns are still permitted */
+ tt_int_op(DOS_CONN_DEFENSE_NONE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ /* Register another conn and check that new conns are not allowed anymore.
+ * We should have reached our burst. */
+ dos_new_client_conn(&or_conn, NULL);
+ tt_int_op(DOS_CONN_DEFENSE_CLOSE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ /* Advance the time 12 hours. It should still be blocked. */
+ update_approx_time(now + (12 * 60 * 60));
+ tt_int_op(DOS_CONN_DEFENSE_CLOSE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ /* Advance the time 24 hours plus 13 hours. It should be unblocked.
+ * Remember, we had a random value between 24 hours and rand(24/2) thus
+ * adding 13 hours is safe. */
+ update_approx_time(now + (37 * 60 * 60));
+ tt_int_op(DOS_CONN_DEFENSE_NONE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ done:
+ dos_free_all();
+}
+
struct testcase_t dos_tests[] = {
{ "conn_creation", test_dos_conn_creation, TT_FORK, NULL, NULL },
{ "circuit_creation", test_dos_circuit_creation, TT_FORK, NULL, NULL },
{ "bucket_refill", test_dos_bucket_refill, TT_FORK, NULL, NULL },
{ "known_relay" , test_known_relay, TT_FORK,
NULL, NULL },
+ { "conn_rate", test_dos_conn_rate, TT_FORK, NULL, NULL },
END_OF_TESTCASES
};