Defend against entry node path bias attacks

The defense counts the circuit failure rate for each guard for the past N
circuits. Failure is defined as the ability to complete a first hop, but not
finish completing the circuit all the way to the exit.

If the failure rate exceeds a certain amount, a notice is emitted.

If it exceeds a greater amount, a warn is emitted and the guard is disabled.

These values are governed by consensus parameters which we intend to tune as
we perform experiments and statistical simulations.
This commit is contained in:
Mike Perry 2012-05-03 20:15:34 -07:00
parent 75706527c1
commit 8d59690033
4 changed files with 254 additions and 8 deletions

View File

@ -1096,6 +1096,34 @@ The following options are useful only for clients (that is, if
"auto" (recommended) then it is on for all clients that do not set
FetchUselessDescriptors. (Default: auto)
**PathBiasCircTheshold** __NUM__ +
**PathBiasNoticeRate** __NUM.__ +
**PathBiasDisableRate** __NUM__ +
**PathBiasScaleThreshold** __NUM__ +
**PathBiasScaleFactor** __NUM__::
These options override the default behavior of Tor's (**currently
experimental**) path bias detection algorithm. To try to find broken or
misbehaving guard nodes, Tor looks for nodes where more than a certain
fraction of circuits through that node fail after the first hop. The
PathBiasCircThreshold option controls how many circuits we need to build
through a guard before we make these checks. The PathBiasNoticeRate and
PathBiasDisableRate options control what fraction of circuits must
succeed through a guard before we will warn about it or disable it,
respectively. When we have seen more than PathBiasScaleThreshold
circuits through a guard, we divide our observations by
PathBiasScaleFactor, so that new observations don't get swamped by old
ones. +
+
By default, or if a negative value is provided for one of these options,
Tor uses reasonable defaults from the networkstatus consensus document.
If no defaults are available there, these options default to 20, .70,
.50, 200, and 4 respectively.
SERVER OPTIONS
--------------

View File

@ -70,6 +70,10 @@ typedef struct {
* router, 1 if we have. */
unsigned int can_retry : 1; /**< Should we retry connecting to this entry,
* in spite of having it marked as unreachable?*/
unsigned int path_bias_notice : 1; /**< Did we alert the user about path bias
* for this node already? */
unsigned int path_bias_disabled : 1; /**< Have we disabled this node because
* of path bias issues? */
time_t bad_since; /**< 0 if this guard is currently usable, or the time at
* which it was observed to become (according to the
* directory or the user configuration) unusable. */
@ -78,6 +82,10 @@ typedef struct {
* connect to it. */
time_t last_attempted; /**< 0 if we can connect to this guard, or the time
* at which we last failed to connect to it. */
unsigned first_hops; /**< Number of first hops this guard has completed */
unsigned circuit_successes; /**< Number of successfully built circuits using
* this guard as first hop. */
} entry_guard_t;
/** Information about a configured bridge. Currently this just matches the
@ -123,6 +131,7 @@ static int count_acceptable_nodes(smartlist_t *routers);
static int onion_append_hop(crypt_path_t **head_ptr, extend_info_t *choice);
static void entry_guards_changed(void);
static entry_guard_t *entry_guard_get_by_id_digest(const char *digest);
static void bridge_free(bridge_info_t *bridge);
@ -2276,8 +2285,28 @@ circuit_send_next_onion_skin(origin_circuit_t *circ)
}
log_info(LD_CIRC,"circuit built!");
circuit_reset_failure_count(0);
if (circ->build_state->onehop_tunnel)
/* Don't count cannibalized or onehop circs for path bias */
if (circ->build_state->onehop_tunnel || circ->has_opened) {
control_event_bootstrap(BOOTSTRAP_STATUS_REQUESTING_STATUS, 0);
} else {
entry_guard_t *guard =
entry_guard_get_by_id_digest(circ->_base.n_conn->identity_digest);
if (guard) {
guard->circuit_successes++;
log_info(LD_PROTOCOL, "Got success count %u/%u for guard %s",
guard->circuit_successes, guard->first_hops,
guard->nickname);
if (guard->first_hops < guard->circuit_successes) {
log_warn(LD_BUG, "Unexpectedly high circuit_successes (%u/%u) "
"for guard %s",
guard->circuit_successes, guard->first_hops,
guard->nickname);
}
}
}
if (!can_complete_circuit && !circ->build_state->onehop_tunnel) {
const or_options_t *options = get_options();
can_complete_circuit=1;
@ -2532,6 +2561,115 @@ circuit_init_cpath_crypto(crypt_path_t *cpath, const char *key_data,
return 0;
}
/** The minimum number of first hop completions before we start
* thinking about warning about path bias and dropping guards */
static int
pathbias_get_min_circs(const or_options_t *options)
{
#define DFLT_PATH_BIAS_MIN_CIRC 20
if (options->PathBiasCircThreshold >= 5)
return options->PathBiasCircThreshold;
else
return networkstatus_get_param(NULL, "pb_mincircs",
DFLT_PATH_BIAS_MIN_CIRC,
5, INT32_MAX);
}
static double
pathbias_get_notice_rate(const or_options_t *options)
{
#define DFLT_PATH_BIAS_NOTICE_PCT 70
if (options->PathBiasNoticeRate >= 0.0)
return options->PathBiasNoticeRate;
else
return networkstatus_get_param(NULL, "pb_noticepct",
DFLT_PATH_BIAS_NOTICE_PCT, 0, 100)/100.0;
}
static double
pathbias_get_disable_rate(const or_options_t *options)
{
#define DFLT_PATH_BIAS_DISABLE_PCT 50
if (options->PathBiasDisableRate >= 0.0)
return options->PathBiasDisableRate;
else
return networkstatus_get_param(NULL, "pb_disablepct",
DFLT_PATH_BIAS_DISABLE_PCT, 0, 100)/100.0;
}
static int
pathbias_get_scale_threshold(const or_options_t *options)
{
#define DFLT_PATH_BIAS_SCALE_THRESHOLD 200
if (options->PathBiasScaleThreshold >= 2)
return options->PathBiasScaleThreshold;
else
return networkstatus_get_param(NULL, "pb_scalecircs",
DFLT_PATH_BIAS_SCALE_THRESHOLD, 10,
INT32_MAX);
}
static int
pathbias_get_scale_factor(const or_options_t *options)
{
#define DFLT_PATH_BIAS_SCALE_FACTOR 4
if (options->PathBiasScaleFactor >= 1)
return options->PathBiasScaleFactor;
else
return networkstatus_get_param(NULL, "pb_scalefactor",
DFLT_PATH_BIAS_SCALE_THRESHOLD, 1, INT32_MAX);
}
/** Increment the number of times we successfully extended a circuit to
* 'guard', first checking if the failure rate is high enough that we should
* eliminate the guard. Return -1 if the guard looks no good; return 0 if the
* guard looks fine. */
static int
entry_guard_inc_first_hop_count(entry_guard_t *guard)
{
const or_options_t *options = get_options();
entry_guards_changed();
if (guard->first_hops > (unsigned)pathbias_get_min_circs(options)) {
/* Note: We rely on the < comparison here to allow us to set a 0
* rate and disable the feature entirely. If refactoring, don't
* change to <= */
if (guard->circuit_successes/((double)guard->first_hops)
< pathbias_get_disable_rate(options)) {
log_warn(LD_PROTOCOL,
"Extremely low circuit success rate %u/%u for guard %s=%s. "
"This might indicate an attack, or a bug.",
guard->circuit_successes, guard->first_hops, guard->nickname,
hex_str(guard->identity, DIGEST_LEN));
guard->path_bias_disabled = 1;
guard->bad_since = approx_time();
return -1;
} else if (guard->circuit_successes/((double)guard->first_hops)
< pathbias_get_notice_rate(options)
&& !guard->path_bias_notice) {
guard->path_bias_notice = 1;
log_notice(LD_PROTOCOL,
"Low circuit success rate %u/%u for guard %s=%s.",
guard->circuit_successes, guard->first_hops, guard->nickname,
hex_str(guard->identity, DIGEST_LEN));
}
}
/* If we get a ton of circuits, just scale everything down */
if (guard->first_hops > (unsigned)pathbias_get_scale_threshold(options)) {
const int scale_factor = pathbias_get_scale_factor(options);
guard->first_hops /= scale_factor;
guard->circuit_successes /= scale_factor;
}
guard->first_hops++;
log_info(LD_PROTOCOL, "Got success count %u/%u for guard %s",
guard->circuit_successes, guard->first_hops, guard->nickname);
return 0;
}
/** A created or extended cell came back to us on the circuit, and it included
* <b>reply</b> as its body. (If <b>reply_type</b> is CELL_CREATED, the body
* contains (the second DH key, plus KH). If <b>reply_type</b> is
@ -2549,9 +2687,22 @@ circuit_finish_handshake(origin_circuit_t *circ, uint8_t reply_type,
char keys[CPATH_KEY_MATERIAL_LEN];
crypt_path_t *hop;
if (circ->cpath->state == CPATH_STATE_AWAITING_KEYS)
if (circ->cpath->state == CPATH_STATE_AWAITING_KEYS) {
hop = circ->cpath;
else {
/* Don't count cannibalized or onehop circs for path bias */
if (!circ->has_opened && !circ->build_state->onehop_tunnel) {
entry_guard_t *guard;
guard = entry_guard_get_by_id_digest(
circ->_base.n_conn->identity_digest);
if (guard) {
if (entry_guard_inc_first_hop_count(guard) < 0) {
/* Bogus guard; we already warned. */
return -END_CIRC_REASON_TORPROTOCOL;
}
}
}
} else {
hop = onion_next_hop_in_cpath(circ->cpath);
if (!hop) { /* got an extended when we're all done? */
log_warn(LD_PROTOCOL,"got extended when circ already built? Closing.");
@ -3630,6 +3781,8 @@ entry_guard_set_status(entry_guard_t *e, const node_t *node,
*reason = "not recommended as a guard";
else if (routerset_contains_node(options->ExcludeNodes, node))
*reason = "excluded";
else if (e->path_bias_disabled)
*reason = "path-biased";
if (*reason && ! e->bad_since) {
/* Router is newly bad. */
@ -3694,6 +3847,10 @@ entry_is_live(entry_guard_t *e, int need_uptime, int need_capacity,
const or_options_t *options = get_options();
tor_assert(msg);
if (e->path_bias_disabled) {
*msg = "path-biased";
return NULL;
}
if (e->bad_since) {
*msg = "bad";
return NULL;
@ -3757,8 +3914,8 @@ num_live_entry_guards(void)
/** If <b>digest</b> matches the identity of any node in the
* entry_guards list, return that node. Else return NULL. */
static INLINE entry_guard_t *
is_an_entry_guard(const char *digest)
static entry_guard_t *
entry_guard_get_by_id_digest(const char *digest)
{
SMARTLIST_FOREACH(entry_guards, entry_guard_t *, entry,
if (tor_memeq(digest, entry->identity, DIGEST_LEN))
@ -3844,7 +4001,7 @@ add_an_entry_guard(const node_t *chosen, int reset_status, int prepend)
if (chosen) {
node = chosen;
entry = is_an_entry_guard(node->identity);
entry = entry_guard_get_by_id_digest(node->identity);
if (entry) {
if (reset_status) {
entry->bad_since = 0;
@ -3988,6 +4145,7 @@ remove_dead_entry_guards(time_t now)
for (i = 0; i < smartlist_len(entry_guards); ) {
entry_guard_t *entry = smartlist_get(entry_guards, i);
if (entry->bad_since &&
! entry->path_bias_disabled &&
entry->bad_since + ENTRY_GUARD_REMOVE_AFTER < now) {
base16_encode(dbuf, sizeof(dbuf), entry->identity, DIGEST_LEN);
@ -4253,7 +4411,7 @@ entry_guards_set_from_config(const or_options_t *options)
/* Remove all currently configured guard nodes, excluded nodes, unreachable
* nodes, or non-Guard nodes from entry_nodes. */
SMARTLIST_FOREACH_BEGIN(entry_nodes, const node_t *, node) {
if (is_an_entry_guard(node->identity)) {
if (entry_guard_get_by_id_digest(node->identity)) {
SMARTLIST_DEL_CURRENT(entry_nodes, node);
continue;
} else if (routerset_contains_node(options->ExcludeNodes, node)) {
@ -4539,6 +4697,31 @@ entry_guards_parse_state(or_state_t *state, int set, char **msg)
continue;
}
digestmap_set(added_by, d, tor_strdup(line->value+HEX_DIGEST_LEN+1));
} else if (!strcasecmp(line->key, "EntryGuardPathBias")) {
const or_options_t *options = get_options();
unsigned hop_cnt, success_cnt;
if (tor_sscanf(line->value, "%u %u", &success_cnt, &hop_cnt) != 2) {
log_warn(LD_GENERAL, "Unable to parse guard path bias info: "
"Misformated EntryGuardPathBias %s", escaped(line->value));
continue;
}
node->first_hops = hop_cnt;
node->circuit_successes = success_cnt;
log_info(LD_GENERAL, "Read %u/%u path bias for node %s",
node->circuit_successes, node->first_hops, node->nickname);
/* Note: We rely on the < comparison here to allow us to set a 0
* rate and disable the feature entirely. If refactoring, don't
* change to <= */
if (node->circuit_successes/((double)node->first_hops)
< pathbias_get_disable_rate(options)) {
node->path_bias_disabled = 1;
log_info(LD_GENERAL,
"Path bias is too high (%u/%u); disabling node %s",
node->circuit_successes, node->first_hops, node->nickname);
}
} else {
log_warn(LD_BUG, "Unexpected key %s", line->key);
}
@ -4563,6 +4746,8 @@ entry_guards_parse_state(or_state_t *state, int set, char **msg)
e->chosen_on_date = time(NULL) - crypto_rand_int(3600*24*30);
}
}
if (node->path_bias_disabled && !node->bad_since)
node->bad_since = time(NULL);
});
if (*msg || !set) {
@ -4658,6 +4843,14 @@ entry_guards_update_state(or_state_t *state)
d, e->chosen_by_version, t);
next = &(line->next);
}
if (e->first_hops) {
*next = line = tor_malloc_zero(sizeof(config_line_t));
line->key = tor_strdup("EntryGuardPathBias");
tor_asprintf(&line->value, "%u %u",
e->circuit_successes, e->first_hops);
next = &(line->next);
}
});
if (!get_options()->AvoidDiskWrites)
or_state_mark_dirty(get_or_state(), 0);

View File

@ -50,6 +50,7 @@ typedef enum config_type_t {
CONFIG_TYPE_STRING = 0, /**< An arbitrary string. */
CONFIG_TYPE_FILENAME, /**< A filename: some prefixes get expanded. */
CONFIG_TYPE_UINT, /**< A non-negative integer less than MAX_INT */
CONFIG_TYPE_INT, /**< Any integer. */
CONFIG_TYPE_PORT, /**< A port from 1...65535, 0 for "not set", or
* "auto". */
CONFIG_TYPE_INTERVAL, /**< A number of seconds, with optional units*/
@ -354,6 +355,13 @@ static config_var_t _option_vars[] = {
V(ORListenAddress, LINELIST, NULL),
V(ORPort, LINELIST, NULL),
V(OutboundBindAddress, STRING, NULL),
V(PathBiasCircThreshold, INT, "-1"),
V(PathBiasNoticeRate, DOUBLE, "-1"),
V(PathBiasDisableRate, DOUBLE, "-1"),
V(PathBiasScaleThreshold, INT, "-1"),
V(PathBiasScaleFactor, INT, "-1"),
OBSOLETE("PathlenCoinWeight"),
V(PerConnBWBurst, MEMUNIT, "0"),
V(PerConnBWRate, MEMUNIT, "0"),
@ -498,6 +506,7 @@ static config_var_t _state_vars[] = {
VAR("EntryGuardDownSince", LINELIST_S, EntryGuards, NULL),
VAR("EntryGuardUnlistedSince", LINELIST_S, EntryGuards, NULL),
VAR("EntryGuardAddedBy", LINELIST_S, EntryGuards, NULL),
VAR("EntryGuardPathBias", LINELIST_S, EntryGuards, NULL),
V(EntryGuards, LINELIST_V, NULL),
VAR("TransportProxy", LINELIST_S, TransportProxies, NULL),
@ -2114,8 +2123,10 @@ config_assign_value(const config_format_t *fmt, or_options_t *options,
break;
}
/* fall through */
case CONFIG_TYPE_INT:
case CONFIG_TYPE_UINT:
i = (int)tor_parse_long(c->value, 10, 0,
i = (int)tor_parse_long(c->value, 10,
var->type==CONFIG_TYPE_INT ? INT_MIN : 0,
var->type==CONFIG_TYPE_PORT ? 65535 : INT_MAX,
&ok, NULL);
if (!ok) {
@ -2498,6 +2509,7 @@ get_assigned_option(const config_format_t *fmt, const void *options,
case CONFIG_TYPE_INTERVAL:
case CONFIG_TYPE_MSEC_INTERVAL:
case CONFIG_TYPE_UINT:
case CONFIG_TYPE_INT:
/* This means every or_options_t uint or bool element
* needs to be an int. Not, say, a uint16_t or char. */
tor_asprintf(&result->value, "%d", *(int*)value);
@ -2741,6 +2753,7 @@ option_clear(const config_format_t *fmt, or_options_t *options,
case CONFIG_TYPE_INTERVAL:
case CONFIG_TYPE_MSEC_INTERVAL:
case CONFIG_TYPE_UINT:
case CONFIG_TYPE_INT:
case CONFIG_TYPE_PORT:
case CONFIG_TYPE_BOOL:
*(int*)lvalue = 0;
@ -7142,6 +7155,7 @@ getinfo_helper_config(control_connection_t *conn,
case CONFIG_TYPE_STRING: type = "String"; break;
case CONFIG_TYPE_FILENAME: type = "Filename"; break;
case CONFIG_TYPE_UINT: type = "Integer"; break;
case CONFIG_TYPE_INT: type = "SignedInteger"; break;
case CONFIG_TYPE_PORT: type = "Port"; break;
case CONFIG_TYPE_INTERVAL: type = "TimeInterval"; break;
case CONFIG_TYPE_MSEC_INTERVAL: type = "TimeMsecInterval"; break;

View File

@ -3550,6 +3550,17 @@ typedef struct {
* control ports. */
int DisableNetwork;
/**
* Parameters for path-bias detection.
* @{
*/
int PathBiasCircThreshold;
double PathBiasNoticeRate;
double PathBiasDisableRate;
int PathBiasScaleThreshold;
int PathBiasScaleFactor;
/** @} */
} or_options_t;
/** Persistent state for an onion router, as saved to disk. */