aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Goulet <dgoulet@torproject.org>2018-01-30 09:33:12 -0500
committerDavid Goulet <dgoulet@torproject.org>2018-01-30 09:33:12 -0500
commitcd81403cc0d73d53cb7f3650b38d49c54100af25 (patch)
tree5ea4f0c626ad082fb195a478614f67dff1a7b386
parent03ab24b44cd148263cfb2b801cba35489e3852c1 (diff)
parent9aca7d47306222f2870ec16a7291a8215d6c3316 (diff)
downloadtor-cd81403cc0d73d53cb7f3650b38d49c54100af25.tar.gz
tor-cd81403cc0d73d53cb7f3650b38d49c54100af25.zip
Merge branch 'ticket24902_029_05' into ticket24902_033_02
-rw-r--r--changes/ticket2490213
-rw-r--r--doc/tor.1.txt88
-rw-r--r--src/common/log.c2
-rw-r--r--src/common/torlog.h4
-rw-r--r--src/or/channel.c9
-rw-r--r--src/or/channel.h3
-rw-r--r--src/or/command.c13
-rw-r--r--src/or/config.c25
-rw-r--r--src/or/connection.c16
-rw-r--r--src/or/dos.c737
-rw-r--r--src/or/dos.h140
-rw-r--r--src/or/geoip.c63
-rw-r--r--src/or/geoip.h27
-rw-r--r--src/or/include.am2
-rw-r--r--src/or/main.c2
-rw-r--r--src/or/networkstatus.c2
-rw-r--r--src/or/or.h33
-rw-r--r--src/or/rendmid.c12
-rw-r--r--src/or/status.c2
-rw-r--r--src/test/include.am1
-rw-r--r--src/test/test.c1
-rw-r--r--src/test/test.h1
-rw-r--r--src/test/test_dos.c248
23 files changed, 1410 insertions, 34 deletions
diff --git a/changes/ticket24902 b/changes/ticket24902
new file mode 100644
index 0000000000..1a2ef95cc9
--- /dev/null
+++ b/changes/ticket24902
@@ -0,0 +1,13 @@
+ o Major features (denial of service mitigation):
+ - Give relays some defenses against the recent network overload. We start
+ with three defenses (default parameters in parentheses). First: if a
+ single client address makes too many concurrent connections (>100), hang
+ up on further connections. Second: if a single client address makes
+ circuits too quickly (more than 3 per second, with an allowed burst of
+ 90) while also having too many connections open (3), refuse new create
+ cells for the next while (1-2 hours). Third: if a client asks to
+ establish a rendezvous point to you directly, ignore the request. These
+ defenses can be manually controlled by new torrc options, but relays
+ will also take guidance from consensus parameters, so there's no need to
+ configure anything manually. Implements ticket 24902.
+
diff --git a/doc/tor.1.txt b/doc/tor.1.txt
index ef3d1eb9ee..5ad8183650 100644
--- a/doc/tor.1.txt
+++ b/doc/tor.1.txt
@@ -2752,6 +2752,94 @@ The following options are used to configure a hidden service.
including setting SOCKSPort to "0". Can not be changed while tor is
running. (Default: 0)
+DENIAL OF SERVICE MITIGATION OPTIONS
+------------------------------------
+
+The following options are useful only for a public relay. They control the
+Denial of Service mitigation subsystem.
+
+[[DoSCircuitCreationEnabled]] **DoSCircuitCreationEnabled** **0**|**1**|**auto**::
+
+ Enable circuit creation DoS mitigation. If enabled, tor will cache client
+ IPs along with statistics in order to detect circuit DoS attacks. If an
+ address is positively identified, tor will activate defenses against the
+ address. See the DoSCircuitCreationDefenseType option for more details.
+ This is a client to relay detection only. "auto" means use the consensus
+ parameter.
+ (Default: auto)
+
+[[DoSCircuitCreationMinConnections]] **DoSCircuitCreationMinConnections** __NUM__::
+
+ Minimum threshold of concurrent connections before a client address can be
+ flagged as executing a circuit creation DoS. In other words, once a client
+ address reaches the circuit rate and has a minimum of NUM concurrent
+ connections, a detection is positive. "0" means use the consensus
+ parameter.
+ (Default: 0)
+
+[[DoSCircuitCreationRate]] **DoSCircuitCreationRate** __NUM__::
+
+ The allowed circuit creation rate per second applied per client IP
+ address. If this option is 0, it obeys a consensus parameter. (Default: 0)
+
+[[DoSCircuitCreationBurst]] **DoSCircuitCreationBurst** __NUM__::
+
+ The allowed circuit creation burst per client IP address. If the circuit
+ rate and the burst are reached, a client is marked as executing a circuit
+ creation DoS. "0" means use the consensus parameter.
+ (Default: 0)
+
+[[DoSCircuitCreationDefenseType]] **DoSCircuitCreationDefenseType** __NUM__::
+
+ This is the type of defense applied to a detected client address. The
+ possible values are:
+
+ 1: No defense.
+ 2: Refuse circuit creation for the DoSCircuitCreationDefenseTimePeriod period of time.
++
+ "0" means use the consensus parameter.
+ (Default: 0)
+
+[[DoSCircuitCreationDefenseTimePeriod]] **DoSCircuitCreationDefenseTimePeriod** __NUM__::
+
+ The base time period that the DoS defense is activated for. The actual
+ value is selected randomly for each activation from NUM+1 to 3/2 * NUM.
+ "0" means use the consensus parameter.
+ (Default: 0)
+
+[[DoSConnectionEnabled]] **DoSConnectionEnabled** **0**|**1**|**auto**::
+
+ Enable the connection DoS mitigation. For client address only, this allows
+ tor to mitigate against large number of concurrent connections made by a
+ single IP address. "auto" means use the consensus parameter.
+ (Default: auto)
+
+[[DoSConnectionMaxConcurrentCount]] **DoSConnectionMaxConcurrentCount** __NUM__::
+
+ The maximum threshold of concurrent connection from a client IP address.
+ Above this limit, a defense selected by DoSConnectionDefenseType is
+ applied. "0" means use the consensus parameter.
+ (Default: 0)
+
+[[DoSConnectionDefenseType]] **DoSConnectionDefenseType** __NUM__::
+
+ This is the type of defense applied to a detected client address for the
+ connection mitigation. The possible values are:
+
+ 1: No defense.
+ 2: Immediately close new connections.
++
+ "0" means use the consensus parameter.
+ (Default: 0)
+
+[[DoSRefuseSingleHopClientRendezvous]] **DoSRefuseSingleHopClientRendezvous** **0**|**1**|**auto**::
+
+ Refuse establishment of rendezvous points for single hop clients. In other
+ words, if a client directly connects to the relay and sends an
+ ESTABLISH_RENDEZVOUS cell, it is silently dropped. "auto" means use the
+ consensus parameter.
+ (Default: auto)
+
TESTING NETWORK OPTIONS
-----------------------
diff --git a/src/common/log.c b/src/common/log.c
index ac6d07a929..d59e5a4036 100644
--- a/src/common/log.c
+++ b/src/common/log.c
@@ -1263,7 +1263,7 @@ static const char *domain_list[] = {
"GENERAL", "CRYPTO", "NET", "CONFIG", "FS", "PROTOCOL", "MM",
"HTTP", "APP", "CONTROL", "CIRC", "REND", "BUG", "DIR", "DIRSERV",
"OR", "EDGE", "ACCT", "HIST", "HANDSHAKE", "HEARTBEAT", "CHANNEL",
- "SCHED", "GUARD", "CONSDIFF", NULL
+ "SCHED", "GUARD", "CONSDIFF", "DOS", NULL
};
/** Return a bitmask for the log domain for which <b>domain</b> is the name,
diff --git a/src/common/torlog.h b/src/common/torlog.h
index b7d033adb9..cadfe3b879 100644
--- a/src/common/torlog.h
+++ b/src/common/torlog.h
@@ -103,8 +103,10 @@
#define LD_GUARD (1u<<23)
/** Generation and application of consensus diffs. */
#define LD_CONSDIFF (1u<<24)
+/** Denial of Service mitigation. */
+#define LD_DOS (1u<<25)
/** Number of logging domains in the code. */
-#define N_LOGGING_DOMAINS 25
+#define N_LOGGING_DOMAINS 26
/** This log message is not safe to send to a callback-based logger
* immediately. Used as a flag, not a log domain. */
diff --git a/src/or/channel.c b/src/or/channel.c
index 345a90a004..094bf93e66 100644
--- a/src/or/channel.c
+++ b/src/or/channel.c
@@ -1888,6 +1888,7 @@ channel_do_open_actions(channel_t *chan)
if (!connection_or_digest_is_known_relay(chan->identity_digest)) {
if (channel_get_addr_if_possible(chan, &remote_addr)) {
char *transport_name = NULL;
+ channel_tls_t *tlschan = BASE_CHAN_TO_TLS(chan);
if (chan->get_transport_name(chan, &transport_name) < 0)
transport_name = NULL;
@@ -1895,6 +1896,10 @@ channel_do_open_actions(channel_t *chan)
&remote_addr, transport_name,
now);
tor_free(transport_name);
+ /* Notify the DoS subsystem of a new client. */
+ if (tlschan && tlschan->conn) {
+ dos_new_client_conn(tlschan->conn);
+ }
}
/* Otherwise the underlying transport can't tell us this, so skip it */
}
@@ -2914,8 +2919,8 @@ channel_get_canonical_remote_descr(channel_t *chan)
* supports this operation, and return 1. Return 0 if the underlying transport
* doesn't let us do this.
*/
-int
-channel_get_addr_if_possible(channel_t *chan, tor_addr_t *addr_out)
+MOCK_IMPL(int,
+channel_get_addr_if_possible,(channel_t *chan, tor_addr_t *addr_out))
{
tor_assert(chan);
tor_assert(addr_out);
diff --git a/src/or/channel.h b/src/or/channel.h
index 4b60b7a7d6..9128403ce7 100644
--- a/src/or/channel.h
+++ b/src/or/channel.h
@@ -590,7 +590,8 @@ MOCK_DECL(void, channel_dump_statistics, (channel_t *chan, int severity));
void channel_dump_transport_statistics(channel_t *chan, int severity);
const char * channel_get_actual_remote_descr(channel_t *chan);
const char * channel_get_actual_remote_address(channel_t *chan);
-int channel_get_addr_if_possible(channel_t *chan, tor_addr_t *addr_out);
+MOCK_DECL(int, channel_get_addr_if_possible, (channel_t *chan,
+ tor_addr_t *addr_out));
const char * channel_get_canonical_remote_descr(channel_t *chan);
int channel_has_queued_writes(channel_t *chan);
int channel_is_bad_for_new_circs(channel_t *chan);
diff --git a/src/or/command.c b/src/or/command.c
index bd70e37a07..185596a65a 100644
--- a/src/or/command.c
+++ b/src/or/command.c
@@ -46,6 +46,7 @@
#include "config.h"
#include "control.h"
#include "cpuworker.h"
+#include "dos.h"
#include "hibernate.h"
#include "nodelist.h"
#include "onion.h"
@@ -247,6 +248,11 @@ command_process_create_cell(cell_t *cell, channel_t *chan)
(unsigned)cell->circ_id,
U64_PRINTF_ARG(chan->global_identifier), chan);
+ /* First thing we do, even though the cell might be invalid, is inform the
+ * DoS mitigation subsystem layer of this event. Validation is done by this
+ * function. */
+ dos_cc_new_create_cell(chan);
+
/* We check for the conditions that would make us drop the cell before
* we check for the conditions that would make us send a DESTROY back,
* since those conditions would make a DESTROY nonsensical. */
@@ -284,6 +290,13 @@ command_process_create_cell(cell_t *cell, channel_t *chan)
return;
}
+ /* Check if we should apply a defense for this channel. */
+ if (dos_cc_get_defense_type(chan) == DOS_CC_DEFENSE_REFUSE_CELL) {
+ channel_send_destroy(cell->circ_id, chan,
+ END_CIRC_REASON_RESOURCELIMIT);
+ return;
+ }
+
if (!server_mode(options) ||
(!public_server_mode(options) && channel_is_outgoing(chan))) {
log_fn(LOG_PROTOCOL_WARN, LD_PROTOCOL,
diff --git a/src/or/config.c b/src/or/config.c
index afaf867851..f6875b7ed5 100644
--- a/src/or/config.c
+++ b/src/or/config.c
@@ -81,6 +81,7 @@
#include "dirserv.h"
#include "dirvote.h"
#include "dns.h"
+#include "dos.h"
#include "entrynodes.h"
#include "git_revision.h"
#include "geoip.h"
@@ -316,6 +317,19 @@ static config_var_t option_vars_[] = {
OBSOLETE("DynamicDHGroups"),
VPORT(DNSPort),
OBSOLETE("DNSListenAddress"),
+ /* DoS circuit creation options. */
+ V(DoSCircuitCreationEnabled, AUTOBOOL, "auto"),
+ V(DoSCircuitCreationMinConnections, UINT, "0"),
+ V(DoSCircuitCreationRate, UINT, "0"),
+ V(DoSCircuitCreationBurst, UINT, "0"),
+ V(DoSCircuitCreationDefenseType, INT, "0"),
+ V(DoSCircuitCreationDefenseTimePeriod, INTERVAL, "0"),
+ /* DoS connection options. */
+ V(DoSConnectionEnabled, AUTOBOOL, "auto"),
+ V(DoSConnectionMaxConcurrentCount, UINT, "0"),
+ V(DoSConnectionDefenseType, INT, "0"),
+ /* DoS single hop client options. */
+ V(DoSRefuseSingleHopClientRendezvous, AUTOBOOL, "auto"),
V(DownloadExtraInfo, BOOL, "0"),
V(TestingEnableConnBwEvent, BOOL, "0"),
V(TestingEnableCellStatsEvent, BOOL, "0"),
@@ -2323,6 +2337,17 @@ options_act(const or_options_t *old_options)
}
}
+ /* DoS mitigation subsystem only applies to public relay. */
+ if (public_server_mode(options)) {
+ /* If we are configured as a relay, initialize the subsystem. Even on HUP,
+ * this is safe to call as it will load data from the current options
+ * or/and the consensus. */
+ dos_init();
+ } else if (old_options && public_server_mode(old_options)) {
+ /* Going from relay to non relay, clean it up. */
+ dos_free_all();
+ }
+
/* Load the webpage we're going to serve every time someone asks for '/' on
our DirPort. */
tor_free(global_dirfrontpagecontents);
diff --git a/src/or/connection.c b/src/or/connection.c
index a020bef775..5bbb61dfa9 100644
--- a/src/or/connection.c
+++ b/src/or/connection.c
@@ -80,6 +80,7 @@
#include "dirserv.h"
#include "dns.h"
#include "dnsserv.h"
+#include "dos.h"
#include "entrynodes.h"
#include "ext_orport.h"
#include "geoip.h"
@@ -703,6 +704,13 @@ connection_free_,(connection_t *conn))
"connection_free");
}
#endif /* 1 */
+
+ /* Notify the circuit creation DoS mitigation subsystem that an OR client
+ * connection has been closed. And only do that if we track it. */
+ if (conn->type == CONN_TYPE_OR) {
+ dos_close_client_conn(TO_OR_CONN(conn));
+ }
+
connection_unregister_events(conn);
connection_free_minimal(conn);
}
@@ -1608,6 +1616,14 @@ connection_handle_listener_read(connection_t *conn, int new_type)
return 0;
}
}
+ if (new_type == CONN_TYPE_OR) {
+ /* Assess with the connection DoS mitigation subsystem if this address
+ * can open a new connection. */
+ if (dos_conn_addr_get_defense_type(&addr) == DOS_CONN_DEFENSE_CLOSE) {
+ tor_close_socket(news);
+ return 0;
+ }
+ }
newconn = connection_new(new_type, conn->socket_family);
newconn->s = news;
diff --git a/src/or/dos.c b/src/or/dos.c
new file mode 100644
index 0000000000..a614d12314
--- /dev/null
+++ b/src/or/dos.c
@@ -0,0 +1,737 @@
+/* Copyright (c) 2018, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+/*
+ * \file dos.c
+ * \brief Implement Denial of Service mitigation subsystem.
+ */
+
+#define DOS_PRIVATE
+
+#include "or.h"
+#include "channel.h"
+#include "config.h"
+#include "geoip.h"
+#include "main.h"
+#include "networkstatus.h"
+#include "router.h"
+
+#include "dos.h"
+
+/*
+ * Circuit creation denial of service mitigation.
+ *
+ * Namespace used for this mitigation framework is "dos_cc_" where "cc" is for
+ * Circuit Creation.
+ */
+
+/* Is the circuit creation DoS mitigation enabled? */
+static unsigned int dos_cc_enabled = 0;
+
+/* Consensus parameters. They can be changed when a new consensus arrives.
+ * They are initialized with the hardcoded default values. */
+static uint32_t dos_cc_min_concurrent_conn;
+static uint32_t dos_cc_circuit_rate;
+static uint32_t dos_cc_circuit_burst;
+static dos_cc_defense_type_t dos_cc_defense_type;
+static int32_t dos_cc_defense_time_period;
+
+/* Keep some stats for the heartbeat so we can report out. */
+static uint64_t cc_num_rejected_cells;
+static uint32_t cc_num_marked_addrs;
+
+/*
+ * Concurrent connection denial of service mitigation.
+ *
+ * Namespace used for this mitigation framework is "dos_conn_".
+ */
+
+/* Is the connection DoS mitigation enabled? */
+static unsigned int dos_conn_enabled = 0;
+
+/* Consensus parameters. They can be changed when a new consensus arrives.
+ * 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;
+
+/* Keep some stats for the heartbeat so we can report out. */
+static uint64_t conn_num_addr_rejected;
+
+/*
+ * General interface of the denial of service mitigation subsystem.
+ */
+
+/* Keep stats for the heartbeat. */
+static uint64_t num_single_hop_client_refused;
+
+/* Return true iff the circuit creation mitigation is enabled. We look at the
+ * consensus for this else a default value is returned. */
+MOCK_IMPL(STATIC unsigned int,
+get_param_cc_enabled, (const networkstatus_t *ns))
+{
+ if (get_options()->DoSCircuitCreationEnabled != -1) {
+ return get_options()->DoSCircuitCreationEnabled;
+ }
+
+ return !!networkstatus_get_param(ns, "DoSCircuitCreationEnabled",
+ DOS_CC_ENABLED_DEFAULT, 0, 1);
+}
+
+/* Return the parameter for the minimum concurrent connection at which we'll
+ * start counting circuit for a specific client address. */
+STATIC uint32_t
+get_param_cc_min_concurrent_connection(const networkstatus_t *ns)
+{
+ if (get_options()->DoSCircuitCreationMinConnections) {
+ return get_options()->DoSCircuitCreationMinConnections;
+ }
+ return networkstatus_get_param(ns, "DoSCircuitCreationMinConnections",
+ DOS_CC_MIN_CONCURRENT_CONN_DEFAULT,
+ 1, INT32_MAX);
+}
+
+/* Return the parameter for the time rate that is how many circuits over this
+ * time span. */
+static uint32_t
+get_param_cc_circuit_rate(const networkstatus_t *ns)
+{
+ /* This is in seconds. */
+ if (get_options()->DoSCircuitCreationRate) {
+ return get_options()->DoSCircuitCreationRate;
+ }
+ return networkstatus_get_param(ns, "DoSCircuitCreationRate",
+ DOS_CC_CIRCUIT_RATE_DEFAULT,
+ 1, INT32_MAX);
+}
+
+/* Return the parameter for the maximum circuit count for the circuit time
+ * rate. */
+STATIC uint32_t
+get_param_cc_circuit_burst(const networkstatus_t *ns)
+{
+ if (get_options()->DoSCircuitCreationBurst) {
+ return get_options()->DoSCircuitCreationBurst;
+ }
+ return networkstatus_get_param(ns, "DoSCircuitCreationBurst",
+ DOS_CC_CIRCUIT_BURST_DEFAULT,
+ 1, INT32_MAX);
+}
+
+/* Return the consensus parameter of the circuit creation defense type. */
+static uint32_t
+get_param_cc_defense_type(const networkstatus_t *ns)
+{
+ if (get_options()->DoSCircuitCreationDefenseType) {
+ return get_options()->DoSCircuitCreationDefenseType;
+ }
+ return networkstatus_get_param(ns, "DoSCircuitCreationDefenseType",
+ DOS_CC_DEFENSE_TYPE_DEFAULT,
+ DOS_CC_DEFENSE_NONE, DOS_CC_DEFENSE_MAX);
+}
+
+/* Return the consensus parameter of the defense time period which is how much
+ * time should we defend against a malicious client address. */
+static int32_t
+get_param_cc_defense_time_period(const networkstatus_t *ns)
+{
+ /* Time in seconds. */
+ if (get_options()->DoSCircuitCreationDefenseTimePeriod) {
+ return get_options()->DoSCircuitCreationDefenseTimePeriod;
+ }
+ return networkstatus_get_param(ns, "DoSCircuitCreationDefenseTimePeriod",
+ DOS_CC_DEFENSE_TIME_PERIOD_DEFAULT,
+ 0, INT32_MAX);
+}
+
+/* Return true iff connection mitigation is enabled. We look at the consensus
+ * for this else a default value is returned. */
+MOCK_IMPL(STATIC unsigned int,
+get_param_conn_enabled, (const networkstatus_t *ns))
+{
+ if (get_options()->DoSConnectionEnabled != -1) {
+ return get_options()->DoSConnectionEnabled;
+ }
+ return !!networkstatus_get_param(ns, "DoSConnectionEnabled",
+ DOS_CONN_ENABLED_DEFAULT, 0, 1);
+}
+
+/* Return the consensus parameter for the maximum concurrent connection
+ * allowed. */
+STATIC uint32_t
+get_param_conn_max_concurrent_count(const networkstatus_t *ns)
+{
+ if (get_options()->DoSConnectionMaxConcurrentCount) {
+ return get_options()->DoSConnectionMaxConcurrentCount;
+ }
+ return networkstatus_get_param(ns, "DoSConnectionMaxConcurrentCount",
+ DOS_CONN_MAX_CONCURRENT_COUNT_DEFAULT,
+ 1, INT32_MAX);
+}
+
+/* Return the consensus parameter of the connection defense type. */
+static uint32_t
+get_param_conn_defense_type(const networkstatus_t *ns)
+{
+ if (get_options()->DoSConnectionDefenseType) {
+ return get_options()->DoSConnectionDefenseType;
+ }
+ return networkstatus_get_param(ns, "DoSConnectionDefenseType",
+ DOS_CONN_DEFENSE_TYPE_DEFAULT,
+ DOS_CONN_DEFENSE_NONE, DOS_CONN_DEFENSE_MAX);
+}
+
+/* Set circuit creation parameters located in the consensus or their default
+ * if none are present. Called at initialization or when the consensus
+ * changes. */
+static void
+set_dos_parameters(const networkstatus_t *ns)
+{
+ /* Get the default consensus param values. */
+ dos_cc_enabled = get_param_cc_enabled(ns);
+ dos_cc_min_concurrent_conn = get_param_cc_min_concurrent_connection(ns);
+ dos_cc_circuit_rate = get_param_cc_circuit_rate(ns);
+ dos_cc_circuit_burst = get_param_cc_circuit_burst(ns);
+ dos_cc_defense_time_period = get_param_cc_defense_time_period(ns);
+ dos_cc_defense_type = get_param_cc_defense_type(ns);
+
+ /* Connection detection. */
+ 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);
+}
+
+/* Free everything for the circuit creation DoS mitigation subsystem. */
+static void
+cc_free_all(void)
+{
+ /* If everything is freed, the circuit creation subsystem is not enabled. */
+ dos_cc_enabled = 0;
+}
+
+/* Called when the consensus has changed. Do appropriate actions for the
+ * circuit creation subsystem. */
+static void
+cc_consensus_has_changed(const networkstatus_t *ns)
+{
+ /* Looking at the consensus, is the circuit creation subsystem enabled? If
+ * not and it was enabled before, clean it up. */
+ if (dos_cc_enabled && !get_param_cc_enabled(ns)) {
+ cc_free_all();
+ }
+}
+
+/** Return the number of circuits we allow per second under the current
+ * configuration. */
+STATIC uint32_t
+get_circuit_rate_per_second(void)
+{
+ return dos_cc_circuit_rate;
+}
+
+/* Given the circuit creation client statistics object, refill the circuit
+ * bucket if needed. This also works if the bucket was never filled in the
+ * first place. The addr is only used for logging purposes. */
+STATIC void
+cc_stats_refill_bucket(cc_client_stats_t *stats, const tor_addr_t *addr)
+{
+ uint32_t new_circuit_bucket_count, circuit_rate = 0, num_token;
+ time_t now, elapsed_time_last_refill;
+
+ tor_assert(stats);
+ tor_assert(addr);
+
+ now = approx_time();
+
+ /* We've never filled the bucket so fill it with the maximum being the burst
+ * and we are done. */
+ if (stats->last_circ_bucket_refill_ts == 0) {
+ num_token = dos_cc_circuit_burst;
+ goto end;
+ }
+
+ /* At this point, we know we might need to add token to the bucket. We'll
+ * first compute the circuit rate that is how many circuit are we allowed to
+ * do per second. */
+ circuit_rate = get_circuit_rate_per_second();
+
+ /* How many seconds have elapsed between now and the last refill? */
+ elapsed_time_last_refill = now - stats->last_circ_bucket_refill_ts;
+
+ /* If the elapsed time is below 0 it means our clock jumped backward so in
+ * that case, lets be safe and fill it up to the maximum. Not filling it
+ * could trigger a detection for a valid client. Also, if the clock jumped
+ * negative but we didn't notice until the elapsed time became positive
+ * again, then we potentially spent many seconds not refilling the bucket
+ * when we should have been refilling it. But the fact that we didn't notice
+ * until now means that no circuit creation requests came in during that
+ * time, so the client doesn't end up punished that much from this hopefully
+ * rare situation.*/
+ if (elapsed_time_last_refill < 0) {
+ /* Dividing the burst by the circuit rate gives us the time span that will
+ * give us the maximum allowed value of token. */
+ elapsed_time_last_refill = (dos_cc_circuit_burst / circuit_rate);
+ }
+
+ /* Compute how many circuits we are allowed in that time frame which we'll
+ * add to the bucket. This can be big but it is cap to a maximum after. */
+ num_token = elapsed_time_last_refill * circuit_rate;
+
+ end:
+ /* We cap the bucket to the burst value else this could grow to infinity
+ * over time. */
+ new_circuit_bucket_count = MIN(stats->circuit_bucket + num_token,
+ dos_cc_circuit_burst);
+ log_debug(LD_DOS, "DoS address %s has its circuit bucket value: %" PRIu32
+ ". Filling it to %" PRIu32 ". Circuit rate is %" PRIu32,
+ fmt_addr(addr), stats->circuit_bucket, new_circuit_bucket_count,
+ circuit_rate);
+
+ stats->circuit_bucket = new_circuit_bucket_count;
+ stats->last_circ_bucket_refill_ts = now;
+ return;
+}
+
+/* Return true iff the circuit bucket is down to 0 and the number of
+ * concurrent connections is greater or equal the minimum threshold set the
+ * consensus parameter. */
+static int
+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;
+}
+
+/* Mark client address by setting a timestamp in the stats object which tells
+ * us until when it is marked as positively detected. */
+static void
+cc_mark_client(cc_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. */
+ stats->marked_until_ts =
+ approx_time() + dos_cc_defense_time_period +
+ crypto_rand_int_range(1, dos_cc_defense_time_period / 2);
+}
+
+/* Return true iff the given channel address is marked as malicious. This is
+ * called a lot and part of the fast path of handling cells. It has to remain
+ * as fast as we can. */
+static int
+cc_channel_addr_is_marked(channel_t *chan)
+{
+ time_t now;
+ tor_addr_t addr;
+ clientmap_entry_t *entry;
+ cc_client_stats_t *stats = NULL;
+
+ if (chan == NULL) {
+ goto end;
+ }
+ /* Must be a client connection else we ignore. */
+ if (!channel_is_client(chan)) {
+ goto end;
+ }
+ /* Without an IP address, nothing can work. */
+ if (!channel_get_addr_if_possible(chan, &addr)) {
+ goto end;
+ }
+
+ /* We are only interested in client connection from the geoip cache. */
+ entry = geoip_lookup_client(&addr, NULL, GEOIP_CLIENT_CONNECT);
+ if (entry == NULL) {
+ /* We can have a connection creating circuits but not tracked by the geoip
+ * cache. Once this DoS subsystem is enabled, we can end up here with no
+ * entry for the channel. */
+ goto end;
+ }
+ now = approx_time();
+ stats = &entry->dos_stats.cc_stats;
+
+ end:
+ return stats && stats->marked_until_ts >= now;
+}
+
+/* Concurrent connection private API. */
+
+/* Free everything for the connection DoS mitigation subsystem. */
+static void
+conn_free_all(void)
+{
+ dos_conn_enabled = 0;
+}
+
+/* Called when the consensus has changed. Do appropriate actions for the
+ * connection mitigation subsystem. */
+static void
+conn_consensus_has_changed(const networkstatus_t *ns)
+{
+ /* Looking at the consensus, is the connection mitigation subsystem enabled?
+ * If not and it was enabled before, clean it up. */
+ if (dos_conn_enabled && !get_param_conn_enabled(ns)) {
+ conn_free_all();
+ }
+}
+
+/* General private API */
+
+/* Return true iff we have at least one DoS detection enabled. This is used to
+ * decide if we need to allocate any kind of high level DoS object. */
+static inline int
+dos_is_enabled(void)
+{
+ return (dos_cc_enabled || dos_conn_enabled);
+}
+
+/* Circuit creation public API. */
+
+/* Called when a CREATE cell is received from the given channel. */
+void
+dos_cc_new_create_cell(channel_t *chan)
+{
+ tor_addr_t addr;
+ clientmap_entry_t *entry;
+
+ tor_assert(chan);
+
+ /* Skip everything if not enabled. */
+ if (!dos_cc_enabled) {
+ goto end;
+ }
+
+ /* Must be a client connection else we ignore. */
+ if (!channel_is_client(chan)) {
+ goto end;
+ }
+ /* Without an IP address, nothing can work. */
+ if (!channel_get_addr_if_possible(chan, &addr)) {
+ goto end;
+ }
+
+ /* We are only interested in client connection from the geoip cache. */
+ entry = geoip_lookup_client(&addr, NULL, GEOIP_CLIENT_CONNECT);
+ if (entry == NULL) {
+ /* We can have a connection creating circuits but not tracked by the geoip
+ * cache. Once this DoS subsystem is enabled, we can end up here with no
+ * entry for the channel. */
+ goto end;
+ }
+
+ /* General comment. Even though the client can already be marked as
+ * malicious, we continue to track statistics. If it keeps going above
+ * threshold while marked, the defense period time will grow longer. There
+ * is really no point at unmarking a client that keeps DoSing us. */
+
+ /* First of all, we'll try to refill the circuit bucket opportunistically
+ * before we assess. */
+ cc_stats_refill_bucket(&entry->dos_stats.cc_stats, &addr);
+
+ /* Take a token out of the circuit bucket if we are above 0 so we don't
+ * underflow the bucket. */
+ if (entry->dos_stats.cc_stats.circuit_bucket > 0) {
+ entry->dos_stats.cc_stats.circuit_bucket--;
+ }
+
+ /* This is the detection. Assess at every CREATE cell if the client should
+ * get marked as malicious. This should be kept as fast as possible. */
+ if (cc_has_exhausted_circuits(&entry->dos_stats)) {
+ /* If this is the first time we mark this entry, log it a info level.
+ * Under heavy DDoS, logging each time we mark would results in lots and
+ * lots of logs. */
+ if (entry->dos_stats.cc_stats.marked_until_ts == 0) {
+ log_debug(LD_DOS, "Detected circuit creation DoS by address: %s",
+ fmt_addr(&addr));
+ cc_num_marked_addrs++;
+ }
+ cc_mark_client(&entry->dos_stats.cc_stats);
+ }
+
+ end:
+ return;
+}
+
+/* Return the defense type that should be used for this circuit.
+ *
+ * This is part of the fast path and called a lot. */
+dos_cc_defense_type_t
+dos_cc_get_defense_type(channel_t *chan)
+{
+ tor_assert(chan);
+
+ /* Skip everything if not enabled. */
+ if (!dos_cc_enabled) {
+ goto end;
+ }
+
+ /* On an OR circuit, we'll check if the previous channel is a marked client
+ * connection detected by our DoS circuit creation mitigation subsystem. */
+ if (cc_channel_addr_is_marked(chan)) {
+ /* We've just assess that this circuit should trigger a defense for the
+ * cell it just seen. Note it down. */
+ cc_num_rejected_cells++;
+ return dos_cc_defense_type;
+ }
+
+ end:
+ return DOS_CC_DEFENSE_NONE;
+}
+
+/* Concurrent connection detection public API. */
+
+/* Return true iff the given address is permitted to open another connection.
+ * A defense value is returned for the caller to take appropriate actions. */
+dos_conn_defense_type_t
+dos_conn_addr_get_defense_type(const tor_addr_t *addr)
+{
+ clientmap_entry_t *entry;
+
+ tor_assert(addr);
+
+ /* Skip everything if not enabled. */
+ if (!dos_conn_enabled) {
+ goto end;
+ }
+
+ /* We are only interested in client connection from the geoip cache. */
+ entry = geoip_lookup_client(addr, NULL, GEOIP_CLIENT_CONNECT);
+ if (entry == NULL) {
+ goto end;
+ }
+
+ /* Need to be above the maximum concurrent connection count to trigger a
+ * defense. */
+ if (entry->dos_stats.concurrent_count > dos_conn_max_concurrent_count) {
+ conn_num_addr_rejected++;
+ return dos_conn_defense_type;
+ }
+
+ end:
+ return DOS_CONN_DEFENSE_NONE;
+}
+
+/* General API */
+
+/* Take any appropriate actions for the given geoip entry that is about to get
+ * freed. This is called for every entry that is being freed.
+ *
+ * This function will clear out the connection tracked flag if the concurrent
+ * count of the entry is above 0 so if those connections end up being seen by
+ * this subsystem, we won't try to decrement the counter for a new geoip entry
+ * that might have been added after this call for the same address. */
+void
+dos_geoip_entry_about_to_free(const clientmap_entry_t *geoip_ent)
+{
+ tor_assert(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) {
+ goto end;
+ }
+
+ /* For each connection matching the geoip entry address, we'll clear the
+ * tracked flag because the entry is about to get removed from the geoip
+ * cache. We do not try to decrement if the flag is not set. */
+ SMARTLIST_FOREACH_BEGIN(get_connection_array(), connection_t *, conn) {
+ if (conn->type == CONN_TYPE_OR) {
+ or_connection_t *or_conn = TO_OR_CONN(conn);
+ if (!tor_addr_compare(&geoip_ent->addr, &or_conn->real_addr,
+ CMP_EXACT)) {
+ or_conn->tracked_for_dos_mitigation = 0;
+ }
+ }
+ } SMARTLIST_FOREACH_END(conn);
+
+ end:
+ return;
+}
+
+/* Note down that we've just refused a single hop client. This increments a
+ * counter later used for the heartbeat. */
+void
+dos_note_refuse_single_hop_client(void)
+{
+ num_single_hop_client_refused++;
+}
+
+/* Return true iff single hop client connection (ESTABLISH_RENDEZVOUS) should
+ * be refused. */
+int
+dos_should_refuse_single_hop_client(void)
+{
+ /* If we aren't a public relay, this shouldn't apply to anything. */
+ if (!public_server_mode(get_options())) {
+ return 0;
+ }
+
+ if (get_options()->DoSRefuseSingleHopClientRendezvous != -1) {
+ return get_options()->DoSRefuseSingleHopClientRendezvous;
+ }
+
+ return (int) networkstatus_get_param(NULL,
+ "DoSRefuseSingleHopClientRendezvous",
+ 0 /* default */, 0, 1);
+}
+
+/* Log a heartbeat message with some statistics. */
+void
+dos_log_heartbeat(void)
+{
+ char *conn_msg = NULL;
+ char *cc_msg = NULL;
+ char *single_hop_client_msg = NULL;
+
+ if (!dos_is_enabled()) {
+ goto end;
+ }
+
+ if (dos_cc_enabled) {
+ tor_asprintf(&cc_msg,
+ " %" PRIu64 " circuits rejected,"
+ " %" PRIu32 " marked addresses.",
+ cc_num_rejected_cells, cc_num_marked_addrs);
+ }
+
+ if (dos_conn_enabled) {
+ tor_asprintf(&conn_msg,
+ " %" PRIu64 " connections closed.",
+ conn_num_addr_rejected);
+ }
+
+ if (dos_should_refuse_single_hop_client()) {
+ tor_asprintf(&single_hop_client_msg,
+ " %" PRIu64 " single hop clients refused.",
+ num_single_hop_client_refused);
+ }
+
+ log_notice(LD_HEARTBEAT,
+ "DoS mitigation since startup:%s%s%s",
+ (cc_msg != NULL) ? cc_msg : " [cc not enabled]",
+ (conn_msg != NULL) ? conn_msg : " [conn not enabled]",
+ (single_hop_client_msg != NULL) ? single_hop_client_msg : "");
+
+ tor_free(conn_msg);
+ tor_free(cc_msg);
+ tor_free(single_hop_client_msg);
+
+ end:
+ return;
+}
+
+/* Called when a new client connection has been established on the given
+ * address. */
+void
+dos_new_client_conn(or_connection_t *or_conn)
+{
+ clientmap_entry_t *entry;
+
+ tor_assert(or_conn);
+
+ /* Past that point, we know we have at least one DoS detection subsystem
+ * enabled so we'll start allocating stuff. */
+ if (!dos_is_enabled()) {
+ goto end;
+ }
+
+ /* We are only interested in client connection from the geoip cache. */
+ entry = geoip_lookup_client(&or_conn->real_addr, NULL,
+ GEOIP_CLIENT_CONNECT);
+ if (BUG(entry == NULL)) {
+ /* Should never happen because we note down the address in the geoip
+ * cache before this is called. */
+ goto end;
+ }
+
+ entry->dos_stats.concurrent_count++;
+ or_conn->tracked_for_dos_mitigation = 1;
+ log_debug(LD_DOS, "Client address %s has now %u concurrent connections.",
+ fmt_addr(&or_conn->real_addr),
+ entry->dos_stats.concurrent_count);
+
+ end:
+ return;
+}
+
+/* Called when a client connection for the given IP address has been closed. */
+void
+dos_close_client_conn(const or_connection_t *or_conn)
+{
+ clientmap_entry_t *entry;
+
+ tor_assert(or_conn);
+
+ /* We have to decrement the count on tracked connection only even if the
+ * subsystem has been disabled at runtime because it might be re-enabled
+ * after and we need to keep a synchronized counter at all time. */
+ if (!or_conn->tracked_for_dos_mitigation) {
+ goto end;
+ }
+
+ /* We are only interested in client connection from the geoip cache. */
+ entry = geoip_lookup_client(&or_conn->real_addr, NULL,
+ GEOIP_CLIENT_CONNECT);
+ if (entry == NULL) {
+ /* This can happen because we can close a connection before the channel
+ * got to be noted down in the geoip cache. */
+ 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(&or_conn->real_addr),
+ entry->dos_stats.concurrent_count);
+
+ end:
+ return;
+}
+
+/* Called when the consensus has changed. We might have new consensus
+ * parameters to look at. */
+void
+dos_consensus_has_changed(const networkstatus_t *ns)
+{
+ cc_consensus_has_changed(ns);
+ conn_consensus_has_changed(ns);
+
+ /* We were already enabled or we just became enabled but either way, set the
+ * consensus parameters for all subsystems. */
+ set_dos_parameters(ns);
+}
+
+/* Return true iff the DoS mitigation subsystem is enabled. */
+int
+dos_enabled(void)
+{
+ return dos_is_enabled();
+}
+
+/* Free everything from the Denial of Service subsystem. */
+void
+dos_free_all(void)
+{
+ /* Free the circuit creation mitigation subsystem. It is safe to do this
+ * even if it wasn't initialized. */
+ cc_free_all();
+
+ /* Free the connection mitigation subsystem. It is safe to do this even if
+ * it wasn't initialized. */
+ conn_free_all();
+}
+
+/* Initialize the Denial of Service subsystem. */
+void
+dos_init(void)
+{
+ /* To initialize, we only need to get the parameters. */
+ set_dos_parameters(NULL);
+}
+
diff --git a/src/or/dos.h b/src/or/dos.h
new file mode 100644
index 0000000000..8695512ea6
--- /dev/null
+++ b/src/or/dos.h
@@ -0,0 +1,140 @@
+/* Copyright (c) 2018, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+/*
+ * \file dos.h
+ * \brief Header file for dos.c
+ */
+
+#ifndef TOR_DOS_H
+#define TOR_DOS_H
+
+/* Structure that keeps stats of client connection per-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
+ * if the count goes to 0, we have a positive detection. */
+ uint32_t circuit_bucket;
+
+ /* When was the last time we've refilled the circuit bucket? This is used to
+ * know if we need to refill the bucket when a new circuit is seen. It is
+ * synchronized using approx_time(). */
+ time_t last_circ_bucket_refill_ts;
+
+ /* This client address was detected to be above the circuit creation rate
+ * and this timestamp indicates until when it should remain marked as
+ * detected so we can apply a defense for the address. It is synchronized
+ * using the approx_time(). */
+ time_t marked_until_ts;
+} cc_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;
+
+ /* Circuit creation statistics. This is only used if the circuit creation
+ * subsystem has been enabled (dos_cc_enabled). */
+ cc_client_stats_t cc_stats;
+} dos_client_stats_t;
+
+/* General API. */
+
+/* Stub. */
+struct clientmap_entry_t;
+
+void dos_init(void);
+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_about_to_free(const struct clientmap_entry_t *geoip_ent);
+
+void dos_new_client_conn(or_connection_t *or_conn);
+void dos_close_client_conn(const or_connection_t *or_conn);
+
+int dos_should_refuse_single_hop_client(void);
+void dos_note_refuse_single_hop_client(void);
+
+/*
+ * Circuit creation DoS mitigation subsystemn interface.
+ */
+
+/* DoSCircuitCreationEnabled default. Disabled by default. */
+#define DOS_CC_ENABLED_DEFAULT 0
+/* DoSCircuitCreationDefenseType maps to the dos_cc_defense_type_t enum. */
+#define DOS_CC_DEFENSE_TYPE_DEFAULT DOS_CC_DEFENSE_REFUSE_CELL
+/* DoSCircuitCreationMinConnections default */
+#define DOS_CC_MIN_CONCURRENT_CONN_DEFAULT 3
+/* DoSCircuitCreationRateTenths is 3 per seconds. */
+#define DOS_CC_CIRCUIT_RATE_DEFAULT 3
+/* DoSCircuitCreationBurst default. */
+#define DOS_CC_CIRCUIT_BURST_DEFAULT 90
+/* DoSCircuitCreationDefenseTimePeriod in seconds. */
+#define DOS_CC_DEFENSE_TIME_PERIOD_DEFAULT (60 * 60)
+
+/* Type of defense that we can use for the circuit creation DoS mitigation. */
+typedef enum dos_cc_defense_type_t {
+ /* No defense used. */
+ DOS_CC_DEFENSE_NONE = 1,
+ /* Refuse any cells which means a DESTROY cell will be sent back. */
+ DOS_CC_DEFENSE_REFUSE_CELL = 2,
+
+ /* Maximum value that can be used. Useful for the boundaries of the
+ * consensus parameter. */
+ DOS_CC_DEFENSE_MAX = 2,
+} dos_cc_defense_type_t;
+
+void dos_cc_new_create_cell(channel_t *channel);
+dos_cc_defense_type_t dos_cc_get_defense_type(channel_t *chan);
+
+/*
+ * Concurrent connection DoS mitigation interface.
+ */
+
+/* DoSConnectionEnabled default. Disabled by default. */
+#define DOS_CONN_ENABLED_DEFAULT 0
+/* DoSConnectionMaxConcurrentCount default. */
+#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
+
+/* Type of defense that we can use for the concurrent connection DoS
+ * mitigation. */
+typedef enum dos_conn_defense_type_t {
+ /* No defense used. */
+ DOS_CONN_DEFENSE_NONE = 1,
+ /* Close immediately the connection meaning refuse it. */
+ DOS_CONN_DEFENSE_CLOSE = 2,
+
+ /* Maximum value that can be used. Useful for the boundaries of the
+ * consensus parameter. */
+ DOS_CONN_DEFENSE_MAX = 2,
+} dos_conn_defense_type_t;
+
+dos_conn_defense_type_t dos_conn_addr_get_defense_type(const tor_addr_t *addr);
+
+#ifdef DOS_PRIVATE
+
+STATIC uint32_t get_param_conn_max_concurrent_count(
+ const networkstatus_t *ns);
+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_circuit_rate_per_second(void);
+STATIC void cc_stats_refill_bucket(cc_client_stats_t *stats,
+ const tor_addr_t *addr);
+
+MOCK_DECL(STATIC unsigned int, get_param_cc_enabled,
+ (const networkstatus_t *ns));
+MOCK_DECL(STATIC unsigned int, get_param_conn_enabled,
+ (const networkstatus_t *ns));
+
+#endif /* TOR_DOS_PRIVATE */
+
+#endif /* TOR_DOS_H */
+
diff --git a/src/or/geoip.c b/src/or/geoip.c
index d7411e6aaa..5b954979b9 100644
--- a/src/or/geoip.c
+++ b/src/or/geoip.c
@@ -34,6 +34,7 @@
#include "config.h"
#include "control.h"
#include "dnsserv.h"
+#include "dos.h"
#include "geoip.h"
#include "routerlist.h"
@@ -473,24 +474,6 @@ geoip_db_digest(sa_family_t family)
return hex_str(geoip6_digest, DIGEST_LEN);
}
-/** Entry in a map from IP address to the last time we've seen an incoming
- * connection from that IP address. Used by bridges only, to track which
- * countries have them blocked. */
-typedef struct clientmap_entry_t {
- HT_ENTRY(clientmap_entry_t) node;
- tor_addr_t addr;
- /* Name of pluggable transport used by this client. NULL if no
- pluggable transport was used. */
- char *transport_name;
-
- /** Time when we last saw this IP address, in MINUTES since the epoch.
- *
- * (This will run out of space around 4011 CE. If Tor is still in use around
- * 4000 CE, please remember to add more bits to last_seen_in_minutes.) */
- unsigned int last_seen_in_minutes:30;
- unsigned int action:2;
-} clientmap_entry_t;
-
/** Largest allowable value for last_seen_in_minutes. (It's a 30-bit field,
* so it can hold up to (1u<<30)-1, or 0x3fffffffu.
*/
@@ -537,6 +520,10 @@ clientmap_entry_free_(clientmap_entry_t *ent)
if (!ent)
return;
+ /* This entry is about to be freed so pass it to the DoS subsystem to see if
+ * any actions can be taken about it. */
+ dos_geoip_entry_about_to_free(ent);
+
tor_free(ent->transport_name);
tor_free(ent);
}
@@ -568,14 +555,17 @@ geoip_note_client_seen(geoip_client_action_t action,
time_t now)
{
const or_options_t *options = get_options();
- clientmap_entry_t lookup, *ent;
- memset(&lookup, 0, sizeof(clientmap_entry_t));
+ clientmap_entry_t *ent;
if (action == GEOIP_CLIENT_CONNECT) {
- /* Only remember statistics as entry guard or as bridge. */
- if (!options->EntryStatistics &&
- (!(options->BridgeRelay && options->BridgeRecordUsageByCountry)))
- return;
+ /* Only remember statistics if the DoS mitigation subsystem is enabled. If
+ * not, only if as entry guard or as bridge. */
+ if (!dos_enabled()) {
+ if (!options->EntryStatistics &&
+ (!(options->BridgeRelay && options->BridgeRecordUsageByCountry))) {
+ return;
+ }
+ }
} else {
/* Only gather directory-request statistics if configured, and
* forcibly disable them on bridge authorities. */
@@ -587,11 +577,7 @@ geoip_note_client_seen(geoip_client_action_t action,
safe_str_client(fmt_addr((addr))),
transport_name ? transport_name : "<no transport>");
- tor_addr_copy(&lookup.addr, addr);
- lookup.action = (int)action;
- lookup.transport_name = (char*) transport_name;
- ent = HT_FIND(clientmap, &client_history, &lookup);
-
+ ent = geoip_lookup_client(addr, transport_name, action);
if (! ent) {
ent = tor_malloc_zero(sizeof(clientmap_entry_t));
tor_addr_copy(&ent->addr, addr);
@@ -639,6 +625,25 @@ geoip_remove_old_clients(time_t cutoff)
&cutoff);
}
+/* Return a client entry object matching the given address, transport name and
+ * geoip action from the clientmap. NULL if not found. The transport_name can
+ * be NULL. */
+clientmap_entry_t *
+geoip_lookup_client(const tor_addr_t *addr, const char *transport_name,
+ geoip_client_action_t action)
+{
+ clientmap_entry_t lookup;
+
+ tor_assert(addr);
+
+ /* We always look for a client connection with no transport. */
+ tor_addr_copy(&lookup.addr, addr);
+ lookup.action = action;
+ lookup.transport_name = (char *) transport_name;
+
+ return HT_FIND(clientmap, &client_history, &lookup);
+}
+
/** How many responses are we giving to clients requesting v3 network
* statuses? */
static uint32_t ns_v3_responses[GEOIP_NS_RESPONSE_NUM];
diff --git a/src/or/geoip.h b/src/or/geoip.h
index acf61b97ae..ccedc1bc1f 100644
--- a/src/or/geoip.h
+++ b/src/or/geoip.h
@@ -13,6 +13,7 @@
#define TOR_GEOIP_H
#include "testsupport.h"
+#include "dos.h"
#ifdef GEOIP_PRIVATE
STATIC int geoip_parse_entry(const char *line, sa_family_t family);
@@ -20,6 +21,29 @@ STATIC int geoip_get_country_by_ipv4(uint32_t ipaddr);
STATIC int geoip_get_country_by_ipv6(const struct in6_addr *addr);
STATIC void clear_geoip_db(void);
#endif /* defined(GEOIP_PRIVATE) */
+
+/** Entry in a map from IP address to the last time we've seen an incoming
+ * connection from that IP address. Used by bridges only to track which
+ * countries have them blocked, or the DoS mitigation subsystem if enabled. */
+typedef struct clientmap_entry_t {
+ HT_ENTRY(clientmap_entry_t) node;
+ tor_addr_t addr;
+ /* Name of pluggable transport used by this client. NULL if no
+ pluggable transport was used. */
+ char *transport_name;
+
+ /** Time when we last saw this IP address, in MINUTES since the epoch.
+ *
+ * (This will run out of space around 4011 CE. If Tor is still in use around
+ * 4000 CE, please remember to add more bits to last_seen_in_minutes.) */
+ unsigned int last_seen_in_minutes:30;
+ unsigned int action:2;
+
+ /* This object is used to keep some statistics per client address for the
+ * DoS mitigation subsystem. */
+ dos_client_stats_t dos_stats;
+} clientmap_entry_t;
+
int should_record_bridge_info(const or_options_t *options);
int geoip_load_file(sa_family_t family, const char *filename);
MOCK_DECL(int, geoip_get_country_by_addr, (const tor_addr_t *addr));
@@ -33,6 +57,9 @@ void geoip_note_client_seen(geoip_client_action_t action,
const tor_addr_t *addr, const char *transport_name,
time_t now);
void geoip_remove_old_clients(time_t cutoff);
+clientmap_entry_t *geoip_lookup_client(const tor_addr_t *addr,
+ const char *transport_name,
+ geoip_client_action_t action);
void geoip_note_ns_response(geoip_ns_response_t response);
char *geoip_get_transport_history(void);
diff --git a/src/or/include.am b/src/or/include.am
index 09be125617..e0366a0cac 100644
--- a/src/or/include.am
+++ b/src/or/include.am
@@ -47,6 +47,7 @@ LIBTOR_A_SOURCES = \
src/or/dirvote.c \
src/or/dns.c \
src/or/dnsserv.c \
+ src/or/dos.c \
src/or/fp_pair.c \
src/or/geoip.c \
src/or/entrynodes.c \
@@ -189,6 +190,7 @@ ORHEADERS = \
src/or/dns.h \
src/or/dns_structs.h \
src/or/dnsserv.h \
+ src/or/dos.h \
src/or/ext_orport.h \
src/or/fallback_dirs.inc \
src/or/fp_pair.h \
diff --git a/src/or/main.c b/src/or/main.c
index 10e606f3ac..96c7e77c79 100644
--- a/src/or/main.c
+++ b/src/or/main.c
@@ -75,6 +75,7 @@
#include "dirvote.h"
#include "dns.h"
#include "dnsserv.h"
+#include "dos.h"
#include "entrynodes.h"
#include "geoip.h"
#include "hibernate.h"
@@ -3486,6 +3487,7 @@ tor_free_all(int postfork)
bridges_free_all();
consdiffmgr_free_all();
hs_free_all();
+ dos_free_all();
if (!postfork) {
config_free_all();
or_state_free_all();
diff --git a/src/or/networkstatus.c b/src/or/networkstatus.c
index aa0a8d15c3..77898445d9 100644
--- a/src/or/networkstatus.c
+++ b/src/or/networkstatus.c
@@ -51,6 +51,7 @@
#include "directory.h"
#include "dirserv.h"
#include "dirvote.h"
+#include "dos.h"
#include "entrynodes.h"
#include "hibernate.h"
#include "main.h"
@@ -1606,6 +1607,7 @@ notify_networkstatus_changed(const networkstatus_t *old_c,
{
notify_control_networkstatus_changed(old_c, new_c);
scheduler_notify_networkstatus_changed(old_c, new_c);
+ dos_consensus_has_changed(new_c);
}
/** Copy all the ancillary information (like router download status and so on)
diff --git a/src/or/or.h b/src/or/or.h
index c81e29c95c..0436533a96 100644
--- a/src/or/or.h
+++ b/src/or/or.h
@@ -1636,6 +1636,10 @@ typedef struct or_connection_t {
/** True iff this connection has had its bootstrap failure logged with
* control_event_bootstrap_problem. */
unsigned int have_noted_bootstrap_problem:1;
+ /** True iff this is a client connection and its address has been put in the
+ * geoip cache and handled by the DoS mitigation subsystem. We use this to
+ * insure we have a coherent count of concurrent connection. */
+ unsigned int tracked_for_dos_mitigation : 1;
uint16_t link_proto; /**< What protocol version are we using? 0 for
* "none negotiated yet." */
@@ -4701,6 +4705,35 @@ typedef struct {
* running embedded inside another process.
*/
int DisableSignalHandlers;
+
+ /** Autobool: Is the circuit creation DoS mitigation subsystem enabled? */
+ int DoSCircuitCreationEnabled;
+ /** Minimum concurrent connection needed from one single address before any
+ * defense is used. */
+ int DoSCircuitCreationMinConnections;
+ /** Circuit rate used to refill the token bucket. */
+ int DoSCircuitCreationRate;
+ /** Maximum allowed burst of circuits. Reaching that value, the address is
+ * detected as malicious and a defense might be used. */
+ int DoSCircuitCreationBurst;
+ /** When an address is marked as malicous, what defense should be used
+ * against it. See the dos_cc_defense_type_t enum. */
+ int DoSCircuitCreationDefenseType;
+ /** For how much time (in seconds) the 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. */
+ int DoSCircuitCreationDefenseTimePeriod;
+
+ /** Autobool: Is the DoS connection mitigation subsystem enabled? */
+ int DoSConnectionEnabled;
+ /** Maximum concurrent connection allowed per address. */
+ int DoSConnectionMaxConcurrentCount;
+ /** When an address is reaches the maximum count, what defense should be
+ * used against it. See the dos_conn_defense_type_t enum. */
+ int DoSConnectionDefenseType;
+
+ /** Autobool: Do we refuse single hop client rendezvous? */
+ int DoSRefuseSingleHopClientRendezvous;
} or_options_t;
#define LOG_PROTOCOL_WARN (get_protocol_warning_severity_level())
diff --git a/src/or/rendmid.c b/src/or/rendmid.c
index 66d2f93113..c4a34ca62c 100644
--- a/src/or/rendmid.c
+++ b/src/or/rendmid.c
@@ -8,10 +8,12 @@
**/
#include "or.h"
+#include "channel.h"
#include "circuitlist.h"
#include "circuituse.h"
#include "config.h"
#include "crypto.h"
+#include "dos.h"
#include "relay.h"
#include "rendmid.h"
#include "rephist.h"
@@ -231,6 +233,16 @@ rend_mid_establish_rendezvous(or_circuit_t *circ, const uint8_t *request,
goto err;
}
+ /* Check if we are configured to accept established rendezvous cells from
+ * client or in other words tor2web clients. */
+ if (channel_is_client(circ->p_chan) &&
+ dos_should_refuse_single_hop_client()) {
+ /* Note it down for the heartbeat log purposes. */
+ dos_note_refuse_single_hop_client();
+ /* Silent drop so the client has to time out before moving on. */
+ return 0;
+ }
+
if (circ->base_.n_chan) {
log_warn(LD_PROTOCOL,
"Tried to establish rendezvous on non-edge circuit");
diff --git a/src/or/status.c b/src/or/status.c
index 187dc4ad22..3b4c605853 100644
--- a/src/or/status.c
+++ b/src/or/status.c
@@ -29,6 +29,7 @@
#include "statefile.h"
#include "hs_stats.h"
#include "hs_service.h"
+#include "dos.h"
static void log_accounting(const time_t now, const or_options_t *options);
#include "geoip.h"
@@ -167,6 +168,7 @@ log_heartbeat(time_t now)
if (public_server_mode(options)) {
rep_hist_log_circuit_handshake_stats(now);
rep_hist_log_link_protocol_counts();
+ dos_log_heartbeat();
}
circuit_log_ancient_one_hop_circuits(1800);
diff --git a/src/test/include.am b/src/test/include.am
index a4b9705fdd..9783f93d57 100644
--- a/src/test/include.am
+++ b/src/test/include.am
@@ -114,6 +114,7 @@ src_test_test_SOURCES = \
src/test/test_dir.c \
src/test/test_dir_common.c \
src/test/test_dir_handle_get.c \
+ src/test/test_dos.c \
src/test/test_entryconn.c \
src/test/test_entrynodes.c \
src/test/test_guardfraction.c \
diff --git a/src/test/test.c b/src/test/test.c
index f225fd277d..cba7465179 100644
--- a/src/test/test.c
+++ b/src/test/test.c
@@ -1193,6 +1193,7 @@ struct testgroup_t testgroups[] = {
{ "dir/", dir_tests },
{ "dir_handle_get/", dir_handle_get_tests },
{ "dir/md/", microdesc_tests },
+ { "dos/", dos_tests },
{ "entryconn/", entryconn_tests },
{ "entrynodes/", entrynodes_tests },
{ "guardfraction/", guardfraction_tests },
diff --git a/src/test/test.h b/src/test/test.h
index 93d527ba17..b41f0e54bb 100644
--- a/src/test/test.h
+++ b/src/test/test.h
@@ -202,6 +202,7 @@ extern struct testcase_t crypto_tests[];
extern struct testcase_t crypto_openssl_tests[];
extern struct testcase_t dir_tests[];
extern struct testcase_t dir_handle_get_tests[];
+extern struct testcase_t dos_tests[];
extern struct testcase_t entryconn_tests[];
extern struct testcase_t entrynodes_tests[];
extern struct testcase_t guardfraction_tests[];
diff --git a/src/test/test_dos.c b/src/test/test_dos.c
new file mode 100644
index 0000000000..5a8474ad8b
--- /dev/null
+++ b/src/test/test_dos.c
@@ -0,0 +1,248 @@
+/* Copyright (c) 2018, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#define DOS_PRIVATE
+#define TOR_CHANNEL_INTERNAL_
+#define CIRCUITLIST_PRIVATE
+
+#include "or.h"
+#include "dos.h"
+#include "circuitlist.h"
+#include "geoip.h"
+#include "channel.h"
+#include "test.h"
+#include "log_test_helpers.h"
+
+static unsigned int
+mock_enable_dos_protection(const networkstatus_t *ns)
+{
+ (void) ns;
+ return 1;
+}
+
+/** Test that the connection tracker of the DoS subsystem will block clients
+ * who try to establish too many connections */
+static void
+test_dos_conn_creation(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(&or_conn.real_addr,
+ "18.0.0.1"));
+ tor_addr_t *addr = &or_conn.real_addr;
+
+ /* Get DoS subsystem limits */
+ dos_init();
+ uint32_t max_concurrent_conns = get_param_conn_max_concurrent_count(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 < max_concurrent_conns; i++) {
+ dos_new_client_conn(&or_conn);
+ }
+ }
+
+ /* 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 */
+ dos_new_client_conn(&or_conn);
+ tt_int_op(DOS_CONN_DEFENSE_CLOSE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ /* Close a client conn and see that a new conn will be permitted again */
+ dos_close_client_conn(&or_conn);
+ tt_int_op(DOS_CONN_DEFENSE_NONE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ /* Register another conn and see that defense measures get reactivated */
+ dos_new_client_conn(&or_conn);
+ tt_int_op(DOS_CONN_DEFENSE_CLOSE, OP_EQ,
+ dos_conn_addr_get_defense_type(addr));
+
+ done:
+ dos_free_all();
+}
+
+/** Helper mock: Place a fake IP addr for this channel in <b>addr_out</b> */
+static int
+mock_channel_get_addr_if_possible(channel_t *chan, tor_addr_t *addr_out)
+{
+ (void)chan;
+ tt_int_op(AF_INET,OP_EQ, tor_addr_parse(addr_out, "18.0.0.1"));;
+ return 1;
+
+ done:
+ return 0;
+}
+
+/** Test that the circuit tracker of the DoS subsystem will block clients who
+ * try to establish too many circuits. */
+static void
+test_dos_circuit_creation(void *arg)
+{
+ (void) arg;
+ unsigned int i;
+
+ MOCK(get_param_cc_enabled, mock_enable_dos_protection);
+ MOCK(get_param_conn_enabled, mock_enable_dos_protection);
+ MOCK(channel_get_addr_if_possible,
+ mock_channel_get_addr_if_possible);
+
+ /* Initialize channels/conns/circs that will be used */
+ channel_t *chan = tor_malloc_zero(sizeof(channel_t));
+ channel_init(chan);
+ chan->is_client = 1;
+
+ /* 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(&or_conn.real_addr,
+ "18.0.0.1"));
+ tor_addr_t *addr = &or_conn.real_addr;
+
+ /* Get DoS subsystem limits */
+ dos_init();
+ uint32_t max_circuit_count = get_param_cc_circuit_burst(NULL);
+ uint32_t min_conc_conns_for_cc =
+ get_param_cc_min_concurrent_connection(NULL);
+
+ /* Introduce new client and establish enough connections to activate the
+ * circuit counting subsystem */
+ geoip_note_client_seen(GEOIP_CLIENT_CONNECT, addr, NULL, now);
+ for (i = 0; i < min_conc_conns_for_cc ; i++) {
+ dos_new_client_conn(&or_conn);
+ }
+
+ /* Register new circuits for this client and conn, but not enough to get
+ * detected as dos */
+ for (i=0; i < max_circuit_count-1; i++) {
+ dos_cc_new_create_cell(chan);
+ }
+ /* see that we didn't get detected for dosing */
+ tt_int_op(DOS_CC_DEFENSE_NONE, OP_EQ, dos_cc_get_defense_type(chan));
+
+ /* Register another CREATE cell that will push us over the limit. Check that
+ * the cell gets refused. */
+ dos_cc_new_create_cell(chan);
+ tt_int_op(DOS_CC_DEFENSE_REFUSE_CELL, OP_EQ, dos_cc_get_defense_type(chan));
+
+ /* TODO: Wait a few seconds before sending the cell, and check that the
+ buckets got refilled properly. */
+ /* TODO: Actually send a Tor cell (instead of calling the DoS function) and
+ * check that it will get refused */
+
+ done:
+ tor_free(chan);
+ dos_free_all();
+}
+
+/** Test that the DoS subsystem properly refills the circuit token buckets. */
+static void
+test_dos_bucket_refill(void *arg)
+{
+ (void) arg;
+ int i;
+ /* For this test, this variable is set to the current circ count of the token
+ * bucket. */
+ uint32_t current_circ_count;
+
+ MOCK(get_param_cc_enabled, mock_enable_dos_protection);
+ MOCK(get_param_conn_enabled, mock_enable_dos_protection);
+ MOCK(channel_get_addr_if_possible,
+ mock_channel_get_addr_if_possible);
+
+ time_t now = 1281533250; /* 2010-08-11 13:27:30 UTC */
+ update_approx_time(now);
+
+ /* Initialize channels/conns/circs that will be used */
+ channel_t *chan = tor_malloc_zero(sizeof(channel_t));
+ channel_init(chan);
+ chan->is_client = 1;
+ or_connection_t or_conn;
+ tt_int_op(AF_INET,OP_EQ, tor_addr_parse(&or_conn.real_addr,
+ "18.0.0.1"));
+ tor_addr_t *addr = &or_conn.real_addr;
+
+ /* Initialize DoS subsystem and get relevant limits */
+ dos_init();
+ uint32_t max_circuit_count = get_param_cc_circuit_burst(NULL);
+ int circ_rate = tor_lround(get_circuit_rate_per_second());
+ /* Check that the circuit rate is a positive number and smaller than the max
+ * circuit count */
+ tt_int_op(circ_rate, OP_GT, 1);
+ tt_int_op(circ_rate, OP_LT, max_circuit_count);
+
+ /* Register this client */
+ geoip_note_client_seen(GEOIP_CLIENT_CONNECT, addr, NULL, now);
+ dos_new_client_conn(&or_conn);
+
+ /* Fetch this client from the geoip cache and get its DoS structs */
+ clientmap_entry_t *entry = geoip_lookup_client(addr, NULL,
+ GEOIP_CLIENT_CONNECT);
+ tt_assert(entry);
+ dos_client_stats_t* dos_stats = &entry->dos_stats;
+ /* Check that the circuit bucket is still uninitialized */
+ tt_uint_op(dos_stats->cc_stats.circuit_bucket, OP_EQ, 0);
+
+ /* Send a create cell: then check that the circ token bucket got initialized
+ * and one circ was subtracted. */
+ dos_cc_new_create_cell(chan);
+ current_circ_count = max_circuit_count - 1;
+ tt_uint_op(dos_stats->cc_stats.circuit_bucket, OP_EQ, current_circ_count);
+
+ /* Now send 29 more CREATEs and ensure that the bucket is missing 30
+ * tokens */
+ for (i=0; i < 29; i++) {
+ dos_cc_new_create_cell(chan);
+ current_circ_count--;
+ }
+ tt_uint_op(dos_stats->cc_stats.circuit_bucket, OP_EQ, current_circ_count);
+
+ /* OK! Progress time forward one sec, refill the bucket and check that the
+ * refill happened correctly. */
+ now += 1;
+ update_approx_time(now);
+ cc_stats_refill_bucket(&dos_stats->cc_stats, addr);
+ /* check refill */
+ current_circ_count += circ_rate;
+ tt_uint_op(dos_stats->cc_stats.circuit_bucket, OP_EQ, current_circ_count);
+
+ /* Now send as many CREATE cells as needed to deplete our token bucket
+ * completely */
+ for (; current_circ_count != 0; current_circ_count--) {
+ dos_cc_new_create_cell(chan);
+ }
+ tt_uint_op(current_circ_count, OP_EQ, 0);
+ tt_uint_op(dos_stats->cc_stats.circuit_bucket, OP_EQ, current_circ_count);
+
+ /* Now progress time a week forward, and check that the token bucket does not
+ * have more than max_circs allowance, even tho we let it simmer for so
+ * long. */
+ now += 604800; /* a week */
+ update_approx_time(now);
+ cc_stats_refill_bucket(&dos_stats->cc_stats, addr);
+ current_circ_count += max_circuit_count;
+ tt_uint_op(dos_stats->cc_stats.circuit_bucket, OP_EQ, current_circ_count);
+
+ done:
+ tor_free(chan);
+ 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 },
+ END_OF_TESTCASES
+};
+