Merge branch 'vanguards-lite-dev-rebased'

This commit is contained in:
George Kadianakis 2021-07-28 12:00:37 +03:00
commit 4f68fe3e6c
12 changed files with 383 additions and 53 deletions

9
changes/ticket40363 Normal file
View file

@ -0,0 +1,9 @@
o Major features (Proposal 332, onion services, guard selection algorithm):
- Clients and onion services now choose four long-lived "layer 2" guard
relays for use as the middle hop in all onion circuits. These relays are
kept in place for a randomized duration averaging 1 week each. This
mitigates guard discovery attacks against clients and short-lived onion
services such as OnionShare. Long-lived onion services that need high
security should still use the Vanguards addon
(https://github.com/mikeperry-tor/vanguards). Closes ticket 40363;
implements proposal 333.

View file

@ -1749,6 +1749,13 @@ The following options are useful only for clients (that is, if
the guard-n-primary-guards consensus parameter, and default to 3 if the
consensus parameter isn't set. (Default: 0)
[[VanguardsLiteEnabled]] **VanguardsLiteEnabled** **0**|**1**|**auto**::
This option specifies whether clients should use the vanguards-lite
subsystem to protect against guard discovery attacks. If it's set to
'auto', clients will do what the vanguards-lite-enabled consensus parameter
tells them to do, and will default to enable the subsystem if the consensus
parameter isn't set. (Default: auto)
[[UseMicrodescriptors]] **UseMicrodescriptors** **0**|**1**|**auto**::
Microdescriptors are a smaller version of the information that Tor needs
in order to build its circuits. Using microdescriptors makes Tor clients

View file

@ -669,6 +669,7 @@ static const config_var_t option_vars_[] = {
VAR("UseEntryGuards", BOOL, UseEntryGuards_option, "1"),
OBSOLETE("UseEntryGuardsAsDirGuards"),
V(UseGuardFraction, AUTOBOOL, "auto"),
V(VanguardsLiteEnabled, AUTOBOOL, "auto"),
V(UseMicrodescriptors, AUTOBOOL, "auto"),
OBSOLETE("UseNTorHandshake"),
V_IMMUTABLE(User, STRING, NULL),

View file

@ -594,6 +594,9 @@ struct or_options_t {
* If 0, use value from NumEntryGuards. */
int NumPrimaryGuards; /**< How many primary guards do we want? */
/** Boolean: Switch to toggle the vanguards-lite subsystem */
int VanguardsLiteEnabled;
int RephistTrackTime; /**< How many seconds do we keep rephist info? */
/** Should we always fetch our dir info on the mirror schedule (which
* means directly from the authorities) no matter our other config? */

View file

@ -1293,6 +1293,7 @@ signewnym_impl(time_t now)
circuit_mark_all_dirty_circs_as_unusable();
addressmap_clear_transient();
hs_client_purge_state();
purge_vanguards_lite();
time_of_last_signewnym = now;
signewnym_is_pending = 0;
@ -1370,6 +1371,7 @@ CALLBACK(save_state);
CALLBACK(write_stats_file);
CALLBACK(control_per_second_events);
CALLBACK(second_elapsed);
CALLBACK(manage_vglite);
#undef CALLBACK
@ -1392,6 +1394,9 @@ STATIC periodic_event_item_t mainloop_periodic_events[] = {
CALLBACK(second_elapsed, NET_PARTICIPANT,
FL(RUN_ON_DISABLE)),
/* Update vanguards-lite once per hour, if we have networking */
CALLBACK(manage_vglite, NET_PARTICIPANT, FL(NEED_NET)),
/* XXXX Do we have a reason to do this on a callback? Does it do any good at
* all? For now, if we're dormant, we can let our listeners decay. */
CALLBACK(retry_listeners, NET_PARTICIPANT, FL(NEED_NET)),
@ -1662,6 +1667,21 @@ mainloop_schedule_shutdown(int delay_sec)
mainloop_event_schedule(scheduled_shutdown_ev, &delay_tv);
}
/**
* Update vanguards-lite layer2 nodes, once every 15 minutes
*/
static int
manage_vglite_callback(time_t now, const or_options_t *options)
{
(void)now;
(void)options;
#define VANGUARDS_LITE_INTERVAL (15*60)
maintain_layer2_guards();
return VANGUARDS_LITE_INTERVAL;
}
/** Perform regular maintenance tasks. This function gets run once per
* second.
*/

View file

@ -1359,7 +1359,9 @@ route_len_for_purpose(uint8_t purpose, extend_info_t *exit_ei)
int routelen = DEFAULT_ROUTE_LEN;
int known_purpose = 0;
if (circuit_should_use_vanguards(purpose)) {
/* If we're using L3 vanguards, we need longer paths for onion services */
if (circuit_purpose_is_hidden_service(purpose) &&
get_options()->HSLayer3Nodes) {
/* Clients want an extra hop for rends to avoid linkability.
* Services want it for intro points to avoid publishing their
* layer3 guards. They want it for hsdir posts to use
@ -1374,14 +1376,6 @@ route_len_for_purpose(uint8_t purpose, extend_info_t *exit_ei)
purpose == CIRCUIT_PURPOSE_S_ESTABLISH_INTRO)
return routelen+1;
/* If we only have Layer2 vanguards, then we do not need
* the extra hop for linkabilty reasons (see below).
* This means all hops can be of the form:
* S/C - G - L2 - M - R/HSDir/I
*/
if (get_options()->HSLayer2Nodes && !get_options()->HSLayer3Nodes)
return routelen+1;
/* For connections to hsdirs, clients want two extra hops
* when using layer3 guards, to avoid linkability.
* Same goes for intro points. Note that the route len
@ -1400,16 +1394,14 @@ route_len_for_purpose(uint8_t purpose, extend_info_t *exit_ei)
return routelen;
switch (purpose) {
/* These two purposes connect to a router that we chose, so
* DEFAULT_ROUTE_LEN is safe. */
case CIRCUIT_PURPOSE_S_ESTABLISH_INTRO:
/* hidden service connecting to introduction point */
/* These purposes connect to a router that we chose, so DEFAULT_ROUTE_LEN
* is safe: */
case CIRCUIT_PURPOSE_TESTING:
/* router reachability testing */
known_purpose = 1;
break;
/* These three purposes connect to a router that someone else
/* These purposes connect to a router that someone else
* might have chosen, so add an extra hop to protect anonymity. */
case CIRCUIT_PURPOSE_C_GENERAL:
case CIRCUIT_PURPOSE_C_HSDIR_GET:
@ -1419,6 +1411,9 @@ route_len_for_purpose(uint8_t purpose, extend_info_t *exit_ei)
/* client connecting to introduction point */
case CIRCUIT_PURPOSE_S_CONNECT_REND:
/* hidden service connecting to rendezvous point */
case CIRCUIT_PURPOSE_S_ESTABLISH_INTRO:
/* hidden service connecting to intro point. In this case we want an extra
hop to avoid linkability attacks by the introduction point. */
known_purpose = 1;
routelen++;
break;
@ -2259,8 +2254,14 @@ middle_node_must_be_vanguard(const or_options_t *options,
return 0;
}
/* If we have sticky L2 nodes, and this is an L2 pick, use vanguards */
if (options->HSLayer2Nodes && cur_len == 1) {
/* Don't even bother if the feature is disabled */
if (!vanguards_lite_is_enabled()) {
return 0;
}
/* If we are a hidden service circuit, always use either vanguards-lite
* or HSLayer2Nodes for 2nd hop. */
if (cur_len == 1) {
return 1;
}
@ -2284,7 +2285,8 @@ pick_vanguard_middle_node(const or_options_t *options,
/* Pick the right routerset based on the current hop */
if (cur_len == 1) {
vanguard_routerset = options->HSLayer2Nodes;
vanguard_routerset = options->HSLayer2Nodes ?
options->HSLayer2Nodes : get_layer2_guards();
} else if (cur_len == 2) {
vanguard_routerset = options->HSLayer3Nodes;
} else {
@ -2293,6 +2295,10 @@ pick_vanguard_middle_node(const or_options_t *options,
return NULL;
}
if (BUG(!vanguard_routerset)) {
return NULL;
}
node = pick_restricted_middle_node(flags, vanguard_routerset,
options->ExcludeNodes, excluded,
cur_len+1);

View file

@ -1204,25 +1204,6 @@ needs_circuits_for_build(int num)
return 0;
}
/**
* Launch the appropriate type of predicted circuit for hidden
* services, depending on our options.
*/
static void
circuit_launch_predicted_hs_circ(int flags)
{
/* K.I.S.S. implementation of bug #23101: If we are using
* vanguards or pinned middles, pre-build a specific purpose
* for HS circs. */
if (circuit_should_use_vanguards(CIRCUIT_PURPOSE_HS_VANGUARDS)) {
circuit_launch(CIRCUIT_PURPOSE_HS_VANGUARDS, flags);
} else {
/* If no vanguards, then no HS-specific prebuilt circuits are needed.
* Normal GENERAL circs are fine */
circuit_launch(CIRCUIT_PURPOSE_C_GENERAL, flags);
}
}
/** Determine how many circuits we have open that are clean,
* Make sure it's enough for all the upcoming behaviors we predict we'll have.
* But put an upper bound on the total number of circuits.
@ -1276,7 +1257,7 @@ circuit_predict_and_launch_new(void)
"Have %d clean circs (%d internal), need another internal "
"circ for my hidden service.",
num, num_internal);
circuit_launch_predicted_hs_circ(flags);
circuit_launch(CIRCUIT_PURPOSE_HS_VANGUARDS, flags);
return;
}
@ -1295,7 +1276,10 @@ circuit_predict_and_launch_new(void)
" another hidden service circ.",
num, num_uptime_internal, num_internal);
circuit_launch_predicted_hs_circ(flags);
/* Always launch vanguards purpose circuits for HS clients,
* for vanguards-lite. This prevents us from cannibalizing
* to build these circuits (and thus not use vanguards). */
circuit_launch(CIRCUIT_PURPOSE_HS_VANGUARDS, flags);
return;
}
@ -2022,16 +2006,12 @@ circuit_is_hs_v3(const circuit_t *circ)
int
circuit_should_use_vanguards(uint8_t purpose)
{
const or_options_t *options = get_options();
/* Only hidden service circuits use vanguards */
if (!circuit_purpose_is_hidden_service(purpose))
return 0;
/* Pinned middles are effectively vanguards */
if (options->HSLayer2Nodes || options->HSLayer3Nodes)
/* All hidden service circuits use either vanguards or
* vanguards-lite. */
if (circuit_purpose_is_hidden_service(purpose))
return 1;
/* Everything else is a normal circuit */
return 0;
}
@ -2069,13 +2049,11 @@ circuit_should_cannibalize_to_build(uint8_t purpose_to_build,
return 0;
}
/* For vanguards, the server-side intro circ is not cannibalized
* because we pre-build 4 hop HS circuits, and it only needs a 3 hop
* circuit. It is also long-lived, so it is more important that
* it have lower latency than get built fast.
/* The server-side intro circ is not cannibalized because it only
* needs a 3 hop circuit. It is also long-lived, so it is more
* important that it have lower latency than get built fast.
*/
if (circuit_should_use_vanguards(purpose_to_build) &&
purpose_to_build == CIRCUIT_PURPOSE_S_ESTABLISH_INTRO) {
if (purpose_to_build == CIRCUIT_PURPOSE_S_ESTABLISH_INTRO) {
return 0;
}

View file

@ -3930,6 +3930,253 @@ guard_selection_free_(guard_selection_t *gs)
tor_free(gs);
}
/**********************************************************************/
/** Layer2 guard subsystem (vanguards-lite) used for onion service circuits */
/** A simple representation of a layer2 guard. We just need its identity so
* that we feed it into a routerset, and a sampled timestamp to do expiration
* checks. */
typedef struct layer2_guard_t {
/** Identity of the guard */
char identity[DIGEST_LEN];
/** When does this guard expire? (randomized timestamp) */
time_t expire_on_date;
} layer2_guard_t;
#define layer2_guard_free(val) \
FREE_AND_NULL(layer2_guard_t, layer2_guard_free_, (val))
/** Return true if the vanguards-lite subsystem is enabled */
bool
vanguards_lite_is_enabled(void)
{
/* First check torrc option and then maybe also the consensus parameter. */
const or_options_t *options = get_options();
/* If the option is explicitly disabled, that's the final word here */
if (options->VanguardsLiteEnabled == 0) {
return false;
}
/* If the option is set to auto, then check the consensus parameter */
if (options->VanguardsLiteEnabled == -1) {
return networkstatus_get_param(NULL, "vanguards-lite-enabled",
1, /* default to "on" */
0, 1);
}
/* else it's enabled */
tor_assert_nonfatal(options->VanguardsLiteEnabled == 1);
return options->VanguardsLiteEnabled;
}
static void
layer2_guard_free_(layer2_guard_t *l2)
{
if (!l2) {
return;
}
tor_free(l2);
}
/** Global list and routerset of L2 guards. They are both synced and they get
* updated periodically. We need both the list and the routerset: we use the
* smartlist to keep track of expiration times and the routerset is what we
* return to the users of this subsystem. */
static smartlist_t *layer2_guards = NULL;
static routerset_t *layer2_routerset = NULL;
/** Number of L2 guards */
#define NUMBER_SECOND_GUARDS 4
/** Make sure that the number of L2 guards is less than the number of
* MAX_SANE_RESTRICTED_NODES */
CTASSERT(NUMBER_SECOND_GUARDS < 20);
/** Lifetime of L2 guards:
* 1 to 12 days, for an average of a week using the max(x,x) distribution */
#define MIN_SECOND_GUARD_LIFETIME (3600*24)
#define MAX_SECOND_GUARD_LIFETIME (3600*24*12)
/** Return the number of guards our L2 guardset should have */
static int
get_number_of_layer2_hs_guards(void)
{
return (int) networkstatus_get_param(NULL,
"guard-hs-l2-number",
NUMBER_SECOND_GUARDS,
1, 19);
}
/** Return the minimum lifetime of L2 guards */
static int
get_min_lifetime_of_layer2_hs_guards(void)
{
return (int) networkstatus_get_param(NULL,
"guard-hs-l2-lifetime-min",
MIN_SECOND_GUARD_LIFETIME,
1, INT32_MAX);
}
/** Return the maximum lifetime of L2 guards */
static int
get_max_lifetime_of_layer2_hs_guards(void)
{
return (int) networkstatus_get_param(NULL,
"guard-hs-l2-lifetime-max",
MAX_SECOND_GUARD_LIFETIME,
1, INT32_MAX);
}
/**
* Sample and return a lifetime for an L2 guard.
*
* Lifetime randomized uniformly between min and max consensus params.
*/
static int
get_layer2_hs_guard_lifetime(void)
{
int min = get_min_lifetime_of_layer2_hs_guards();
int max = get_max_lifetime_of_layer2_hs_guards();
if (BUG(min >= max)) {
return min;
}
return crypto_rand_int_range(min, max);
}
/** Maintain the L2 guard list. Make sure the list contains enough guards, do
* expirations as necessary, and keep all the data structures of this
* subsystem synchronized */
void
maintain_layer2_guards(void)
{
if (!router_have_minimum_dir_info()) {
return;
}
/* Create the list if it doesn't exist */
if (!layer2_guards) {
layer2_guards = smartlist_new();
}
/* Go through the list and perform any needed expirations */
SMARTLIST_FOREACH_BEGIN(layer2_guards, layer2_guard_t *, g) {
/* Expire based on expiration date */
if (g->expire_on_date <= approx_time()) {
log_info(LD_GENERAL, "Removing expired Layer2 guard %s",
safe_str_client(hex_str(g->identity, DIGEST_LEN)));
// Nickname may be gone from consensus and doesn't matter anyway
control_event_guard("None", g->identity, "BAD_L2");
layer2_guard_free(g);
SMARTLIST_DEL_CURRENT_KEEPORDER(layer2_guards, g);
continue;
}
/* Expire if relay has left consensus */
if (router_get_consensus_status_by_id(g->identity) == NULL) {
log_info(LD_GENERAL, "Removing missing Layer2 guard %s",
safe_str_client(hex_str(g->identity, DIGEST_LEN)));
// Nickname may be gone from consensus and doesn't matter anyway
control_event_guard("None", g->identity, "BAD_L2");
layer2_guard_free(g);
SMARTLIST_DEL_CURRENT_KEEPORDER(layer2_guards, g);
continue;
}
} SMARTLIST_FOREACH_END(g);
/* Find out how many guards we need to add */
int new_guards_needed_n =
get_number_of_layer2_hs_guards() - smartlist_len(layer2_guards);
if (new_guards_needed_n <= 0) {
return;
}
log_info(LD_GENERAL, "Adding %d guards to Layer2 routerset",
new_guards_needed_n);
/* Add required guards to the list */
smartlist_t *excluded = smartlist_new();
for (int i = 0; i < new_guards_needed_n; i++) {
const node_t *choice = NULL;
const or_options_t *options = get_options();
/* Pick Stable nodes */
router_crn_flags_t flags = CRN_NEED_DESC|CRN_NEED_UPTIME;
choice = router_choose_random_node(excluded, options->ExcludeNodes, flags);
if (!choice) {
break;
}
/* We found our node: create an L2 guard out of it */
layer2_guard_t *layer2_guard = tor_malloc_zero(sizeof(layer2_guard_t));
memcpy(layer2_guard->identity, choice->identity, DIGEST_LEN);
layer2_guard->expire_on_date = approx_time() +
get_layer2_hs_guard_lifetime();
smartlist_add(layer2_guards, layer2_guard);
log_info(LD_GENERAL, "Adding Layer2 guard %s",
safe_str_client(hex_str(layer2_guard->identity, DIGEST_LEN)));
// Nickname can also be None here because it is looked up later
control_event_guard("None", layer2_guard->identity,
"GOOD_L2");
/* Exclude this node and its family so that we don't double-pick. */
nodelist_add_node_and_family(excluded, choice);
}
/* Some cleanup */
smartlist_free(excluded);
/* Now that the list is up to date, synchronize the routerset */
routerset_free(layer2_routerset);
layer2_routerset = routerset_new();
SMARTLIST_FOREACH_BEGIN (layer2_guards, layer2_guard_t *, g) {
routerset_parse(layer2_routerset,
hex_str(g->identity, DIGEST_LEN),
"l2 guards");
} SMARTLIST_FOREACH_END(g);
}
/**
* Reset vanguards-lite list(s).
*
* Used for SIGNAL NEWNYM.
*/
void
purge_vanguards_lite(void)
{
if (!layer2_guards)
return;
/* Go through the list and perform any needed expirations */
SMARTLIST_FOREACH_BEGIN(layer2_guards, layer2_guard_t *, g) {
layer2_guard_free(g);
} SMARTLIST_FOREACH_END(g);
smartlist_clear(layer2_guards);
/* Pick new l2 guards */
maintain_layer2_guards();
}
/** Return a routerset containing the L2 guards or NULL if it's not yet
* initialized. Callers must not free the routerset. Designed for use in
* pick_vanguard_middle_node() and should not be used anywhere else. Do not
* store this pointer -- any future calls to maintain_layer2_guards() and
* purge_vanguards_lite() can invalidate it. */
const routerset_t *
get_layer2_guards(void)
{
if (!layer2_guards) {
maintain_layer2_guards();
}
return layer2_routerset;
}
/*****************************************************************************/
/** Release all storage held by the list of entry guards and related
* memory structs. */
void
@ -3946,4 +4193,15 @@ entry_guards_free_all(void)
guard_contexts = NULL;
}
circuit_build_times_free_timeouts(get_circuit_build_times_mutable());
if (!layer2_guards) {
return;
}
SMARTLIST_FOREACH_BEGIN(layer2_guards, layer2_guard_t *, g) {
layer2_guard_free(g);
} SMARTLIST_FOREACH_END(g);
smartlist_free(layer2_guards);
routerset_free(layer2_routerset);
}

View file

@ -651,4 +651,9 @@ guard_get_guardfraction_bandwidth(guardfraction_bandwidth_t *guardfraction_bw,
int orig_bandwidth,
uint32_t guardfraction_percentage);
bool vanguards_lite_is_enabled(void);
const routerset_t *get_layer2_guards(void);
void maintain_layer2_guards(void);
void purge_vanguards_lite(void);
#endif /* !defined(TOR_ENTRYNODES_H) */

View file

@ -1699,6 +1699,9 @@ notify_after_networkstatus_changes(void)
channelpadding_new_consensus_params(c);
circpad_new_consensus_params(c);
router_new_consensus_params(c);
/* Maintenance of our L2 guard list */
maintain_layer2_guards();
}
/** Copy all the ancillary information (like router download status and so on)

View file

@ -113,7 +113,7 @@ test_new_route_len_safe_exit(void *arg)
/* hidden service connecting to introduction point */
r = new_route_len(CIRCUIT_PURPOSE_S_ESTABLISH_INTRO, &dummy_ei,
&dummy_nodes);
tt_int_op(DEFAULT_ROUTE_LEN, OP_EQ, r);
tt_int_op(DEFAULT_ROUTE_LEN+1, OP_EQ, r);
/* router testing its own reachability */
r = new_route_len(CIRCUIT_PURPOSE_TESTING, &dummy_ei, &dummy_nodes);

View file

@ -92,6 +92,12 @@ bfn_mock_node_get_by_id(const char *id)
return NULL;
}
static int
mock_router_have_minimum_dir_info(void)
{
return 1;
}
/* Helper function to free a test node. */
static void
test_node_free(node_t *n)
@ -3087,6 +3093,38 @@ test_entry_guard_vanguard_path_selection(void *arg)
circuit_free_(circ);
}
static void
test_entry_guard_layer2_guards(void *arg)
{
(void) arg;
MOCK(router_have_minimum_dir_info, mock_router_have_minimum_dir_info);
/* First check the enable/disable switch */
get_options_mutable()->VanguardsLiteEnabled = 0;
tt_int_op(vanguards_lite_is_enabled(), OP_EQ, 0);
get_options_mutable()->VanguardsLiteEnabled = 1;
tt_int_op(vanguards_lite_is_enabled(), OP_EQ, 1);
get_options_mutable()->VanguardsLiteEnabled = -1;
tt_int_op(vanguards_lite_is_enabled(), OP_EQ, 1);
/* OK now let's move to actual testing */
/* Remove restrictions to route around Big Fake Network restrictions */
get_options_mutable()->EnforceDistinctSubnets = 0;
/* Create the L2 guardset */
maintain_layer2_guards();
const routerset_t *l2_guards = get_layer2_guards();
tt_assert(l2_guards);
tt_int_op(routerset_len(l2_guards), OP_EQ, 4);
done:
UNMOCK(router_have_minimum_dir_info);
}
static const struct testcase_setup_t big_fake_network = {
big_fake_network_setup, big_fake_network_cleanup
};
@ -3152,6 +3190,8 @@ struct testcase_t entrynodes_tests[] = {
BFN_TEST(manage_primary),
BFN_TEST(correct_cascading_order),
BFN_TEST(layer2_guards),
EN_TEST_FORK(guard_preferred),
BFN_TEST(select_for_circuit_no_confirmed),