diff options
Diffstat (limited to 'src/or/dos.c')
-rw-r--r-- | src/or/dos.c | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/src/or/dos.c b/src/or/dos.c index d1a2c6a281..b83ea60298 100644 --- a/src/or/dos.c +++ b/src/or/dos.c @@ -35,6 +35,9 @@ 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 uint32_t cc_num_marked_addrs; + /* * Concurrent connection denial of service mitigation. * @@ -209,6 +212,117 @@ cc_consensus_has_changed(const networkstatus_t *ns) } } +/** Return the number of circuits we allow per second under the current + * configuration. */ +STATIC uint32_t +get_circuit_rate_per_second(void) +{ + int64_t circ_rate; + + /* We take the burst divided by the rate which is in tenths of a second so + * convert to get a circuit rate per second. */ + circ_rate = dos_cc_circuit_rate_tenths / 10; + if (circ_rate < 0) { + /* Safety check, never allow it to go below 0 else the bucket will always + * be empty resulting in every address to be detected. */ + circ_rate = 1; + } + + /* Clamp it down to a 32 bit value because a rate of 2^32 circuits per + * second is just too much in any circumstances. */ + if (circ_rate > UINT32_MAX) { + circ_rate = UINT32_MAX; + } + return (uint32_t) circ_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); +} + /* Concurrent connection private API. */ /* Free everything for the connection DoS mitigation subsystem. */ @@ -242,6 +356,71 @@ dos_is_enabled(void) /* 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; +} + /* Concurrent connection detection public API. */ /* General API */ |