/* 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 "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_tenths; 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; /* * General interface of the denial of service mitigation subsystem. */ /* 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_tenths(const networkstatus_t *ns) { /* This is in seconds. */ if (get_options()->DoSCircuitCreationRateTenths) { return get_options()->DoSCircuitCreationRateTenths; } return networkstatus_get_param(ns, "DoSCircuitCreationRateTenths", DOS_CC_CIRCUIT_RATE_TENTHS_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_tenths = get_param_cc_circuit_rate_tenths(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) { 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); } /* 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. */ /* General API */ /* 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); }