[tor-commits] [tor/master] Defend against entry node path bias attacks
nickm at torproject.org
nickm at torproject.org
Fri Jun 15 04:43:56 UTC 2012
commit 8d59690033b1cc2b462b2b9ed61ddeaebb6f9ab0
Author: Mike Perry <mikeperry-git at fscked.org>
Date: Thu May 3 20:15:34 2012 -0700
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.
---
doc/tor.1.txt | 28 +++++++
src/or/circuitbuild.c | 207 +++++++++++++++++++++++++++++++++++++++++++++++--
src/or/config.c | 16 ++++-
src/or/or.h | 11 +++
4 files changed, 254 insertions(+), 8 deletions(-)
diff --git a/doc/tor.1.txt b/doc/tor.1.txt
index 468ed63..65bdcfd 100644
--- a/doc/tor.1.txt
+++ b/doc/tor.1.txt
@@ -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
--------------
diff --git a/src/or/circuitbuild.c b/src/or/circuitbuild.c
index 3c72204..9969f7e 100644
--- a/src/or/circuitbuild.c
+++ b/src/or/circuitbuild.c
@@ -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);
diff --git a/src/or/config.c b/src/or/config.c
index 782a6e9..03e93e0 100644
--- a/src/or/config.c
+++ b/src/or/config.c
@@ -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;
diff --git a/src/or/or.h b/src/or/or.h
index 7ff6284..3a53e5e 100644
--- a/src/or/or.h
+++ b/src/or/or.h
@@ -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. */
More information about the tor-commits
mailing list