Procházet zdrojové kódy

Merge branch 'prop293_squashed'

Nick Mathewson před 5 roky
rodič
revize
1f95e80351

+ 8 - 0
changes/ticket26770

@@ -0,0 +1,8 @@
+  o Minor features (directory authority, relay):
+    - Authorities now vote on a "StaleDesc" flag to indicate that a relay's
+      descriptor is so old that the relay should upload again soon.  Relays
+      understand this flag, and treat it as a signal to upload a new
+      descriptor.  This flag will eventually let us remove the 'published'
+      date from routerstatus entries, and save a great deal of space in our
+      consensus diffs. Closes ticket 26770; implements proposal 293.
+

+ 18 - 1
src/feature/dirauth/dirvote.c

@@ -4372,6 +4372,23 @@ clear_status_flags_on_sybil(routerstatus_t *rs)
    * forget to add it to this clause. */
 }
 
+/** Space-separated list of all the flags that we will always vote on. */
+const char DIRVOTE_UNIVERSAL_FLAGS[] =
+  "Authority "
+  "Exit "
+  "Fast "
+  "Guard "
+  "HSDir "
+  "Stable "
+  "StaleDesc "
+  "V2Dir "
+  "Valid";
+/** Space-separated list of all flags that we may or may not vote on,
+ * depending on our configuration. */
+const char DIRVOTE_OPTIONAL_FLAGS[] =
+  "BadExit "
+  "Running";
+
 /** Return a new networkstatus_t* containing our current opinion. (For v3
  * authorities) */
 networkstatus_t *
@@ -4620,7 +4637,7 @@ dirserv_generate_networkstatus_vote_obj(crypto_pk_t *private_key,
 
   v3_out->known_flags = smartlist_new();
   smartlist_split_string(v3_out->known_flags,
-                         "Authority Exit Fast Guard Stable V2Dir Valid HSDir",
+                         DIRVOTE_UNIVERSAL_FLAGS,
                          0, SPLIT_SKIP_SPACE|SPLIT_IGNORE_BLANK, 0);
   if (vote_on_reachability)
     smartlist_add_strdup(v3_out->known_flags, "Running");

+ 3 - 0
src/feature/dirauth/dirvote.h

@@ -92,6 +92,9 @@
 /** Maximum size of a line in a vote. */
 #define MAX_BW_FILE_HEADERS_LINE_LEN 1024
 
+extern const char DIRVOTE_UNIVERSAL_FLAGS[];
+extern const char DIRVOTE_OPTIONAL_FLAGS[];
+
 /*
  * Public API. Used outside of the dirauth subsystem.
  *

+ 6 - 2
src/feature/dirauth/voteflags.c

@@ -95,7 +95,7 @@ real_uptime(const routerinfo_t *router, time_t now)
  */
 static int
 dirserv_thinks_router_is_unreliable(time_t now,
-                                    routerinfo_t *router,
+                                    const routerinfo_t *router,
                                     int need_uptime, int need_capacity)
 {
   if (need_uptime) {
@@ -541,7 +541,7 @@ dirserv_set_router_is_running(routerinfo_t *router, time_t now)
 void
 set_routerstatus_from_routerinfo(routerstatus_t *rs,
                                  node_t *node,
-                                 routerinfo_t *ri,
+                                 const routerinfo_t *ri,
                                  time_t now,
                                  int listbadexits)
 {
@@ -593,6 +593,10 @@ set_routerstatus_from_routerinfo(routerstatus_t *rs,
   rs->or_port = ri->or_port;
   rs->dir_port = ri->dir_port;
   rs->is_v2_dir = ri->supports_tunnelled_dir_requests;
+
+  rs->is_staledesc =
+    (ri->cache_info.published_on + DESC_IS_STALE_INTERVAL) < now;
+
   if (options->AuthDirHasIPv6Connectivity == 1 &&
       !tor_addr_is_null(&ri->ipv6_addr) &&
       node->last_reachable6 >= now - REACHABLE_TIMEOUT) {

+ 5 - 1
src/feature/dirauth/voteflags.h

@@ -19,12 +19,16 @@ int running_long_enough_to_decide_unreachable(void);
 
 void set_routerstatus_from_routerinfo(routerstatus_t *rs,
                                       node_t *node,
-                                      routerinfo_t *ri, time_t now,
+                                      const routerinfo_t *ri,
+                                      time_t now,
                                       int listbadexits);
 
 void dirserv_compute_performance_thresholds(digestmap_t *omit_as_sybil);
 
 #ifdef VOTEFLAGS_PRIVATE
+/** Any descriptor older than this age causes the authorities to set the
+ * StaleDesc flag. */
+#define DESC_IS_STALE_INTERVAL (18*60*60)
 STATIC void dirserv_set_routerstatus_testing(routerstatus_t *rs);
 #endif
 

+ 2 - 0
src/feature/dirparse/ns_parse.c

@@ -434,6 +434,8 @@ routerstatus_parse_entry_from_string(memarea_t *area,
         rs->is_hs_dir = 1;
       } else if (!strcmp(tok->args[i], "V2Dir")) {
         rs->is_v2_dir = 1;
+      } else if (!strcmp(tok->args[i], "StaleDesc")) {
+        rs->is_staledesc = 1;
       }
     }
     /* These are implied true by having been included in a consensus made

+ 2 - 1
src/feature/nodelist/fmt_routerstatus.c

@@ -135,7 +135,7 @@ routerstatus_format_entry(const routerstatus_t *rs, const char *version,
     goto done;
 
   smartlist_add_asprintf(chunks,
-                   "s%s%s%s%s%s%s%s%s%s%s\n",
+                   "s%s%s%s%s%s%s%s%s%s%s%s\n",
                   /* These must stay in alphabetical order. */
                    rs->is_authority?" Authority":"",
                    rs->is_bad_exit?" BadExit":"",
@@ -145,6 +145,7 @@ routerstatus_format_entry(const routerstatus_t *rs, const char *version,
                    rs->is_hs_dir?" HSDir":"",
                    rs->is_flagged_running?" Running":"",
                    rs->is_stable?" Stable":"",
+                   rs->is_staledesc?" StaleDesc":"",
                    rs->is_v2_dir?" V2Dir":"",
                    rs->is_valid?" Valid":"");
 

+ 2 - 2
src/feature/nodelist/networkstatus.c

@@ -724,8 +724,8 @@ networkstatus_vote_find_mutable_entry(networkstatus_t *ns, const char *digest)
 
 /** Return the entry in <b>ns</b> for the identity digest <b>digest</b>, or
  * NULL if none was found. */
-const routerstatus_t *
-networkstatus_vote_find_entry(networkstatus_t *ns, const char *digest)
+MOCK_IMPL(const routerstatus_t *,
+networkstatus_vote_find_entry,(networkstatus_t *ns, const char *digest))
 {
   return networkstatus_vote_find_mutable_entry(ns, digest);
 }

+ 3 - 2
src/feature/nodelist/networkstatus.h

@@ -40,8 +40,9 @@ int compare_digest_to_routerstatus_entry(const void *_key,
                                          const void **_member);
 int compare_digest_to_vote_routerstatus_entry(const void *_key,
                                               const void **_member);
-const routerstatus_t *networkstatus_vote_find_entry(networkstatus_t *ns,
-                                              const char *digest);
+MOCK_DECL(const routerstatus_t *,networkstatus_vote_find_entry,(
+                                              networkstatus_t *ns,
+                                              const char *digest));
 routerstatus_t *networkstatus_vote_find_mutable_entry(networkstatus_t *ns,
                                               const char *digest);
 int networkstatus_vote_find_entry_idx(networkstatus_t *ns,

+ 2 - 0
src/feature/nodelist/routerstatus_st.h

@@ -47,6 +47,8 @@ struct routerstatus_t {
   unsigned int is_v2_dir:1; /** True iff this router publishes an open DirPort
                              * or it claims to accept tunnelled dir requests.
                              */
+  unsigned int is_staledesc:1; /** True iff the authorities think this router
+                                * should upload a new descriptor soon. */
 
   unsigned int has_bandwidth:1; /**< The vote/consensus had bw info */
   unsigned int has_exitsummary:1; /**< The vote/consensus had exit summaries */

+ 8 - 3
src/feature/relay/router.c

@@ -59,6 +59,7 @@
 #include "feature/dircommon/dir_connection_st.h"
 #include "feature/nodelist/authority_cert_st.h"
 #include "feature/nodelist/extrainfo_st.h"
+#include "feature/nodelist/networkstatus_st.h"
 #include "feature/nodelist/node_st.h"
 #include "feature/nodelist/routerinfo_st.h"
 #include "feature/nodelist/routerstatus_st.h"
@@ -1470,9 +1471,9 @@ static extrainfo_t *desc_extrainfo = NULL;
 static const char *desc_gen_reason = "uninitialized reason";
 /** Since when has our descriptor been "clean"?  0 if we need to regenerate it
  * now. */
-static time_t desc_clean_since = 0;
+STATIC time_t desc_clean_since = 0;
 /** Why did we mark the descriptor dirty? */
-static const char *desc_dirty_reason = "Tor just started";
+STATIC const char *desc_dirty_reason = "Tor just started";
 /** Boolean: do we need to regenerate the above? */
 static int desc_needs_upload = 0;
 
@@ -2134,7 +2135,9 @@ mark_my_descriptor_dirty_if_too_old(time_t now)
   /* Now we see whether we want to be retrying frequently or no.  The
    * rule here is that we'll retry frequently if we aren't listed in the
    * live consensus we have, or if the publication time of the
-   * descriptor listed for us in the consensus is very old. */
+   * descriptor listed for us in the consensus is very old, or if the
+   * consensus lists us as "stale" and we haven't regenerated since the
+   * consensus was published. */
   ns = networkstatus_get_live_consensus(now);
   if (ns) {
     rs = networkstatus_vote_find_entry(ns, server_identitykey_digest);
@@ -2142,6 +2145,8 @@ mark_my_descriptor_dirty_if_too_old(time_t now)
       retry_fast_reason = "not listed in consensus";
     else if (rs->published_on < slow_cutoff)
       retry_fast_reason = "version listed in consensus is quite old";
+    else if (rs->is_staledesc && ns->valid_after > desc_clean_since)
+      retry_fast_reason = "listed as stale in consensus";
   }
 
   if (retry_fast_reason && desc_clean_since < fast_cutoff)

+ 4 - 0
src/feature/relay/router.h

@@ -117,6 +117,10 @@ void router_free_all(void);
 /* Used only by router.c and test.c */
 STATIC void get_platform_str(char *platform, size_t len);
 STATIC int router_write_fingerprint(int hashed);
+#ifdef TOR_UNIT_TESTS
+extern time_t desc_clean_since;
+extern const char *desc_dirty_reason;
+#endif
 #endif
 
 #endif /* !defined(TOR_ROUTER_H) */

+ 6 - 2
src/test/fuzz/fuzz_vrs.c

@@ -3,6 +3,7 @@
 #define NS_PARSE_PRIVATE
 #define NETWORKSTATUS_PRIVATE
 #include "core/or/or.h"
+#include "feature/dirauth/dirvote.h"
 #include "feature/dirparse/ns_parse.h"
 #include "feature/dirparse/unparseable.h"
 #include "lib/memarea/memarea.h"
@@ -35,9 +36,12 @@ fuzz_init(void)
   dummy_vote = tor_malloc_zero(sizeof(*dummy_vote));
   dummy_vote->known_flags = smartlist_new();
   smartlist_split_string(dummy_vote->known_flags,
-                         "Authority BadExit Exit Fast Guard HSDir "
-                         "NoEdConsensus Running Stable V2Dir Valid",
+                         DIRVOTE_UNIVERSAL_FLAGS,
                          " ", 0, 0);
+  smartlist_split_string(dummy_vote->known_flags,
+                         DIRVOTE_OPTIONAL_FLAGS,
+                         " ", 0, 0);
+  smartlist_sort_strings(dummy_vote->known_flags);
   return 0;
 }
 

+ 1 - 0
src/test/include.am

@@ -177,6 +177,7 @@ src_test_test_SOURCES += \
 	src/test/test_util.c \
 	src/test/test_util_format.c \
 	src/test/test_util_process.c \
+	src/test/test_voting_flags.c \
 	src/test/test_voting_schedule.c \
 	src/test/test_x509.c \
 	src/test/test_helpers.c \

+ 2 - 1
src/test/test.c

@@ -866,7 +866,8 @@ struct testgroup_t testgroups[] = {
   { "crypto/pem/", pem_tests },
   { "dir/", dir_tests },
   { "dir/md/", microdesc_tests },
-  { "dir/voting-schedule/", voting_schedule_tests },
+  { "dir/voting/flags/", voting_flags_tests },
+  { "dir/voting/schedule/", voting_schedule_tests },
   { "dir_handle_get/", dir_handle_get_tests },
   { "dns/", dns_tests },
   { "dos/", dos_tests },

+ 1 - 0
src/test/test.h

@@ -265,6 +265,7 @@ extern struct testcase_t tortls_tests[];
 extern struct testcase_t util_format_tests[];
 extern struct testcase_t util_process_tests[];
 extern struct testcase_t util_tests[];
+extern struct testcase_t voting_flags_tests[];
 extern struct testcase_t voting_schedule_tests[];
 extern struct testcase_t x509_tests[];
 

+ 75 - 0
src/test/test_dir.c

@@ -6052,6 +6052,80 @@ test_dir_find_dl_min_delay(void* data)
   mock_options = NULL;
 }
 
+static void
+test_dir_matching_flags(void *arg)
+{
+  (void) arg;
+  routerstatus_t *rs_noflags = NULL;
+  routerstatus_t *rs = NULL;
+  char *s = NULL;
+
+  smartlist_t *tokens = smartlist_new();
+  memarea_t *area = memarea_new();
+
+  int expected_val_when_unused = 0;
+
+  const char *ex_noflags =
+    "r example hereiswhereyouridentitygoes 2015-08-30 12:00:00 "
+       "192.168.0.1 9001 0\n"
+    "m thisoneislongerbecauseitisa256bitmddigest33\n"
+    "s\n";
+  const char *cp = ex_noflags;
+  rs_noflags = routerstatus_parse_entry_from_string(
+         area, &cp,
+         cp + strlen(cp),
+         tokens, NULL, NULL,
+         MAX_SUPPORTED_CONSENSUS_METHOD, FLAV_MICRODESC);
+  tt_assert(rs_noflags);
+
+#define FLAG(string, field) STMT_BEGIN {        \
+    tor_asprintf(&s,\
+                 "r example hereiswhereyouridentitygoes 2015-08-30 12:00:00 " \
+                 "192.168.0.1 9001 0\n"                                 \
+                 "m thisoneislongerbecauseitisa256bitmddigest33\n"      \
+                 "s %s\n", string);                                     \
+    cp = s;                                                             \
+    rs =  routerstatus_parse_entry_from_string(                         \
+      area, &cp,                                                        \
+      cp + strlen(cp),                                                  \
+      tokens, NULL, NULL,                                               \
+      MAX_SUPPORTED_CONSENSUS_METHOD, FLAV_MICRODESC);                  \
+    /* the field should usually be 0 when no flags are listed */        \
+    tt_int_op(rs_noflags->field, OP_EQ, expected_val_when_unused);      \
+    /* the field should be 1 when this flags islisted */                \
+    tt_int_op(rs->field, OP_EQ, 1);                                     \
+    tor_free(s);                                                        \
+    routerstatus_free(rs);                                              \
+} STMT_END
+
+  FLAG("Authority", is_authority);
+  FLAG("BadExit", is_bad_exit);
+  FLAG("Exit", is_exit);
+  FLAG("Fast", is_fast);
+  FLAG("Guard", is_possible_guard);
+  FLAG("HSDir", is_hs_dir);
+  FLAG("Stable", is_stable);
+  FLAG("StaleDesc", is_staledesc);
+  FLAG("V2Dir", is_v2_dir);
+
+  // These flags are assumed to be set whether they're declared or not.
+  expected_val_when_unused = 1;
+  FLAG("Running", is_flagged_running);
+  FLAG("Valid", is_valid);
+  expected_val_when_unused = 0;
+
+  // These flags are no longer used, but still parsed.
+  FLAG("Named", is_named);
+  FLAG("Unnamed", is_unnamed);
+
+ done:
+  tor_free(s);
+  routerstatus_free(rs);
+  routerstatus_free(rs_noflags);
+  memarea_drop_all(area);
+  smartlist_free(tokens);
+}
+
 static void
 test_dir_assumed_flags(void *arg)
 {
@@ -6377,6 +6451,7 @@ struct testcase_t dir_tests[] = {
   DIR_ARG(find_dl_min_delay, TT_FORK, "cfr"),
   DIR_ARG(find_dl_min_delay, TT_FORK, "car"),
   DIR(assumed_flags, 0),
+  DIR(matching_flags, 0),
   DIR(networkstatus_compute_bw_weights_v10, 0),
   DIR(platform_str, 0),
   DIR(networkstatus_consensus_has_ipv6, TT_FORK),

+ 98 - 0
src/test/test_router.c

@@ -7,12 +7,17 @@
  * \brief Unittests for code in router.c
  **/
 
+#define ROUTER_PRIVATE
+
 #include "core/or/or.h"
 #include "app/config/config.h"
 #include "core/mainloop/mainloop.h"
 #include "feature/hibernate/hibernate.h"
+#include "feature/nodelist/networkstatus.h"
+#include "feature/nodelist/networkstatus_st.h"
 #include "feature/nodelist/routerinfo_st.h"
 #include "feature/nodelist/routerlist.h"
+#include "feature/nodelist/routerstatus_st.h"
 #include "feature/relay/router.h"
 #include "feature/stats/rephist.h"
 #include "lib/crypt_ops/crypto_curve25519.h"
@@ -231,11 +236,104 @@ test_router_check_descriptor_bandwidth_changed(void *arg)
   UNMOCK(we_are_hibernating);
 }
 
+static networkstatus_t *mock_ns = NULL;
+static networkstatus_t *
+mock_networkstatus_get_live_consensus(time_t now)
+{
+  (void)now;
+  return mock_ns;
+}
+
+static routerstatus_t *mock_rs = NULL;
+static const routerstatus_t *
+mock_networkstatus_vote_find_entry(networkstatus_t *ns, const char *digest)
+{
+  (void)ns;
+  (void)digest;
+  return mock_rs;
+}
+
+static void
+test_router_mark_if_too_old(void *arg)
+{
+  (void)arg;
+  time_t now = approx_time();
+  MOCK(networkstatus_get_live_consensus,
+       mock_networkstatus_get_live_consensus);
+  MOCK(networkstatus_vote_find_entry, mock_networkstatus_vote_find_entry);
+
+  routerstatus_t rs;
+  networkstatus_t ns;
+  memset(&rs, 0, sizeof(rs));
+  memset(&ns, 0, sizeof(ns));
+  mock_ns = &ns;
+  mock_ns->valid_after = now-3600;
+  mock_rs = &rs;
+  mock_rs->published_on = now - 10;
+
+  // no reason to mark this time.
+  desc_clean_since = now-10;
+  desc_dirty_reason = NULL;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, now-10);
+
+  // Doesn't appear in consensus?  Still don't mark it.
+  mock_ns = NULL;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, now-10);
+  mock_ns = &ns;
+
+  // No new descriptor in a long time?  Mark it.
+  desc_clean_since = now - 3600 * 96;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, 0);
+  tt_str_op(desc_dirty_reason, OP_EQ, "time for new descriptor");
+
+  // Version in consensus published a long time ago?  We won't mark it
+  // if it's been clean for only a short time.
+  desc_clean_since = now - 10;
+  desc_dirty_reason = NULL;
+  mock_rs->published_on = now - 3600 * 96;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, now - 10);
+
+  // ... but if it's been clean a while, we mark.
+  desc_clean_since = now - 2 * 3600;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, 0);
+  tt_str_op(desc_dirty_reason, OP_EQ,
+            "version listed in consensus is quite old");
+
+  // same deal if we're marked stale.
+  desc_clean_since = now - 2 * 3600;
+  desc_dirty_reason = NULL;
+  mock_rs->published_on = now - 10;
+  mock_rs->is_staledesc = 1;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, 0);
+  tt_str_op(desc_dirty_reason, OP_EQ,
+            "listed as stale in consensus");
+
+  // same deal if we're absent from the consensus.
+  desc_clean_since = now - 2 * 3600;
+  desc_dirty_reason = NULL;
+  mock_rs = NULL;
+  mark_my_descriptor_dirty_if_too_old(now);
+  tt_i64_op(desc_clean_since, OP_EQ, 0);
+  tt_str_op(desc_dirty_reason, OP_EQ,
+            "not listed in consensus");
+
+ done:
+  UNMOCK(networkstatus_get_live_consensus);
+  UNMOCK(networkstatus_vote_find_entry);
+}
+
 #define ROUTER_TEST(name, flags)                          \
   { #name, test_router_ ## name, flags, NULL, NULL }
 
 struct testcase_t router_tests[] = {
   ROUTER_TEST(check_descriptor_bandwidth_changed, TT_FORK),
   ROUTER_TEST(dump_router_to_string_no_bridge_distribution_method, TT_FORK),
+  ROUTER_TEST(mark_if_too_old, TT_FORK),
   END_OF_TESTCASES
 };

+ 192 - 0
src/test/test_voting_flags.c

@@ -0,0 +1,192 @@
+/* Copyright (c) 2018, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#include "orconfig.h"
+
+#define VOTEFLAGS_PRIVATE
+
+#include "core/or/or.h"
+
+#include "feature/dirauth/voteflags.h"
+#include "feature/nodelist/node_st.h"
+#include "feature/nodelist/routerstatus_st.h"
+#include "feature/nodelist/routerinfo_st.h"
+
+#include "app/config/config.h"
+
+#include "test/test.h"
+
+typedef struct {
+  time_t now;
+  routerinfo_t ri;
+  node_t node;
+
+  routerstatus_t expected;
+} flag_vote_test_cfg_t;
+
+static void
+setup_cfg(flag_vote_test_cfg_t *c)
+{
+  memset(c, 0, sizeof(*c));
+
+  c->now = approx_time();
+
+  c->ri.nickname = (char *) "testing100";
+  strlcpy(c->expected.nickname, "testing100", sizeof(c->expected.nickname));
+
+  memset(c->ri.cache_info.identity_digest, 0xff, DIGEST_LEN);
+  memset(c->ri.cache_info.signed_descriptor_digest, 0xee, DIGEST256_LEN);
+
+  c->ri.cache_info.published_on = c->now - 100;
+  c->expected.published_on = c->now - 100;
+
+  c->ri.addr = 0x7f010105;
+  c->expected.addr = 0x7f010105;
+  c->ri.or_port = 9090;
+  c->expected.or_port = 9090;
+
+  tor_addr_make_null(&c->ri.ipv6_addr, AF_INET6);
+  tor_addr_make_null(&c->expected.ipv6_addr, AF_INET6);
+
+  // By default we have no loaded information about stability or speed,
+  // so we'll default to voting "yeah sure." on these two.
+  c->expected.is_fast = 1;
+  c->expected.is_stable = 1;
+}
+
+static bool
+check_result(flag_vote_test_cfg_t *c)
+{
+  bool result = false;
+  routerstatus_t rs;
+  memset(&rs, 0, sizeof(rs));
+  set_routerstatus_from_routerinfo(&rs, &c->node, &c->ri, c->now, 0);
+
+  tt_i64_op(rs.published_on, OP_EQ, c->expected.published_on);
+  tt_str_op(rs.nickname, OP_EQ, c->expected.nickname);
+
+  // identity_digest and descriptor_digest are not set here.
+
+  tt_uint_op(rs.addr, OP_EQ, c->expected.addr);
+  tt_uint_op(rs.or_port, OP_EQ, c->expected.or_port);
+  tt_uint_op(rs.dir_port, OP_EQ, c->expected.dir_port);
+
+  tt_assert(tor_addr_eq(&rs.ipv6_addr, &c->expected.ipv6_addr));
+  tt_uint_op(rs.ipv6_orport, OP_EQ, c->expected.ipv6_orport);
+
+#define FLAG(flagname) \
+  tt_uint_op(rs.flagname, OP_EQ, c->expected.flagname)
+
+  FLAG(is_authority);
+  FLAG(is_exit);
+  FLAG(is_stable);
+  FLAG(is_fast);
+  FLAG(is_flagged_running);
+  FLAG(is_named);
+  FLAG(is_unnamed);
+  FLAG(is_valid);
+  FLAG(is_possible_guard);
+  FLAG(is_bad_exit);
+  FLAG(is_hs_dir);
+  FLAG(is_v2_dir);
+  FLAG(is_staledesc);
+  FLAG(has_bandwidth);
+  FLAG(has_exitsummary);
+  FLAG(bw_is_unmeasured);
+
+  result = true;
+
+ done:
+  return result;
+}
+
+static void
+test_voting_flags_minimal(void *arg)
+{
+  flag_vote_test_cfg_t *cfg = arg;
+  check_result(cfg);
+}
+
+static void
+test_voting_flags_ipv6(void *arg)
+{
+  flag_vote_test_cfg_t *cfg = arg;
+
+  tt_assert(tor_addr_parse(&cfg->ri.ipv6_addr, "f00::b42") == AF_INET6);
+  cfg->ri.ipv6_orport = 9091;
+  // no change in expected results, since we aren't set up with ipv6
+  // connectivity.
+  if (!check_result(cfg))
+    goto done;
+
+  get_options_mutable()->AuthDirHasIPv6Connectivity = 1;
+  // no change in expected results, since last_reachable6 won't be set.
+  if (!check_result(cfg))
+    goto done;
+
+  cfg->node.last_reachable6 = cfg->now - 10;
+  // now that lastreachable6 is set, we expect to see the result.
+  tt_assert(tor_addr_parse(&cfg->expected.ipv6_addr, "f00::b42") == AF_INET6);
+  cfg->expected.ipv6_orport = 9091;
+  if (!check_result(cfg))
+    goto done;
+ done:
+  ;
+}
+
+static void
+test_voting_flags_staledesc(void *arg)
+{
+  flag_vote_test_cfg_t *cfg = arg;
+  time_t now = cfg->now;
+
+  cfg->ri.cache_info.published_on = now - DESC_IS_STALE_INTERVAL + 10;
+  cfg->expected.published_on = now - DESC_IS_STALE_INTERVAL + 10;
+  // no change in expectations for is_staledesc
+  if (!check_result(cfg))
+    goto done;
+
+  cfg->ri.cache_info.published_on = now - DESC_IS_STALE_INTERVAL - 10;
+  cfg->expected.published_on = now - DESC_IS_STALE_INTERVAL - 10;
+  cfg->expected.is_staledesc = 1;
+  if (!check_result(cfg))
+    goto done;
+
+ done:
+  ;
+}
+
+static void *
+setup_voting_flags_test(const struct testcase_t *testcase)
+{
+  (void)testcase;
+  flag_vote_test_cfg_t *cfg = tor_malloc_zero(sizeof(*cfg));
+  setup_cfg(cfg);
+  return cfg;
+}
+
+static int
+teardown_voting_flags_test(const struct testcase_t *testcase, void *arg)
+{
+  (void)testcase;
+  flag_vote_test_cfg_t *cfg = arg;
+  tor_free(cfg);
+  return 1;
+}
+
+static const struct testcase_setup_t voting_flags_setup = {
+  .setup_fn = setup_voting_flags_test,
+  .cleanup_fn = teardown_voting_flags_test,
+};
+
+#define T(name,flags)                                   \
+  { #name, test_voting_flags_##name, (flags), &voting_flags_setup, NULL }
+
+struct testcase_t voting_flags_tests[] = {
+  T(minimal, 0),
+  T(ipv6, TT_FORK),
+  // TODO: Add more of these tests.
+  T(staledesc, TT_FORK),
+  END_OF_TESTCASES
+};
+