[tor-commits] [tor/main] Introduce vanguards-lite subsystem and some of its entry points

asn at torproject.org asn at torproject.org
Wed Jul 28 09:04:17 UTC 2021


commit 314a6b42c59c7d9ea240b758ccffd796963efd0f
Author: George Kadianakis <desnacked at riseup.net>
Date:   Thu Jul 1 17:42:34 2021 +0300

    Introduce vanguards-lite subsystem and some of its entry points
    
    Co-authored-by: Mike Perry <mikeperry-git at torproject.org>
---
 changes/ticket40363             |   9 ++
 src/core/mainloop/mainloop.c    |  20 ++++
 src/feature/client/entrynodes.c | 202 ++++++++++++++++++++++++++++++++++++++++
 src/feature/client/entrynodes.h |   4 +
 src/test/test_entrynodes.c      |  25 +++++
 5 files changed, 260 insertions(+)

diff --git a/changes/ticket40363 b/changes/ticket40363
new file mode 100644
index 0000000000..240cbf8680
--- /dev/null
+++ b/changes/ticket40363
@@ -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 332.
diff --git a/src/core/mainloop/mainloop.c b/src/core/mainloop/mainloop.c
index 69606c0d53..68e8ee8f20 100644
--- a/src/core/mainloop/mainloop.c
+++ b/src/core/mainloop/mainloop.c
@@ -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 per hour
+ */
+static int
+manage_vglite_callback(time_t now, const or_options_t *options)
+{
+ (void)now;
+ (void)options;
+#define VANGUARDS_LITE_INTERVAL (60*60)
+
+  maintain_layer2_guards();
+
+  return VANGUARDS_LITE_INTERVAL;
+}
+
 /** Perform regular maintenance tasks.  This function gets run once per
  * second.
  */
diff --git a/src/feature/client/entrynodes.c b/src/feature/client/entrynodes.c
index 502cb99690..58faa8033c 100644
--- a/src/feature/client/entrynodes.c
+++ b/src/feature/client/entrynodes.c
@@ -3930,6 +3930,197 @@ guard_selection_free_(guard_selection_t *gs)
   tor_free(gs);
 }
 
+/**********************************************************************/
+
+/** Layer2 guard subsystem used for client-side 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;
+
+/** 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
+/** 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, INT32_MAX);
+}
+
+/** 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)
+{
+  return crypto_rand_int_range(get_min_lifetime_of_layer2_hs_guards(),
+                               get_max_lifetime_of_layer2_hs_guards());
+}
+
+/** 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");
+      tor_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");
+      tor_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 */
+  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(NULL, options->ExcludeNodes, flags);
+    if (choice) {
+      /* 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");
+    }
+  }
+
+  /* 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) {
+    tor_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 (because
+ *  the routerset pointer can dangle under your feet) */
+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 +4137,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) {
+    tor_free(g);
+  } SMARTLIST_FOREACH_END(g);
+
+  smartlist_free(layer2_guards);
+  routerset_free(layer2_routerset);
 }
diff --git a/src/feature/client/entrynodes.h b/src/feature/client/entrynodes.h
index 88ed8f649e..c1bc5b41c1 100644
--- a/src/feature/client/entrynodes.h
+++ b/src/feature/client/entrynodes.h
@@ -651,4 +651,8 @@ guard_get_guardfraction_bandwidth(guardfraction_bandwidth_t *guardfraction_bw,
                                   int orig_bandwidth,
                                   uint32_t guardfraction_percentage);
 
+routerset_t *get_layer2_guards(void);
+void maintain_layer2_guards(void);
+void purge_vanguards_lite(void);
+
 #endif /* !defined(TOR_ENTRYNODES_H) */
diff --git a/src/test/test_entrynodes.c b/src/test/test_entrynodes.c
index c94b5d6a23..785ce296d3 100644
--- a/src/test/test_entrynodes.c
+++ b/src/test/test_entrynodes.c
@@ -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,23 @@ 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);
+
+  /* Create the guardset */
+  maintain_layer2_guards();
+
+  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 +3175,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),





More information about the tor-commits mailing list