From 1ceb7142a131bd8706663d2b3c27d66a2dcb2a46 Mon Sep 17 00:00:00 2001 From: Matthew Finkel Date: Tue, 28 Oct 2014 22:01:06 +0000 Subject: A relay now advertises "tunnelled-dir-server" in its descriptor When a relay does not have an open directory port but it has an orport configured and is accepting client connections then it can now service tunnelled directory requests, too. This was already true of relays with an dirport configured. We also conditionally stop advertising this functionality if the relay is nearing its bandwidth usage limit - same as how dirport advertisement is determined. Partial implementation of prop 237, ticket 12538 --- src/or/router.c | 99 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 32 deletions(-) (limited to 'src/or/router.c') diff --git a/src/or/router.c b/src/or/router.c index 49e2e318f5..1927cfd38b 100644 --- a/src/or/router.c +++ b/src/or/router.c @@ -1099,39 +1099,25 @@ check_whether_dirport_reachable(void) can_reach_dir_port; } -/** Look at a variety of factors, and return 0 if we don't want to - * advertise the fact that we have a DirPort open. Else return the - * DirPort we want to advertise. - * - * Log a helpful message if we change our mind about whether to publish - * a DirPort. +/** The lower threshold of remaining bandwidth required to advertise directory + * services */ +/* XXX Should this be increased? */ +#define MIN_BW_TO_ADVERTISE_DIRSERVER 51200 + +/** Helper: Return 1 if we have sufficient resources for serving directory + * requests, return 0 otherwise. + * dir_port is either 0 or the configured DirPort number. + * If AccountingMax is set less than our advertised bandwidth, then don't + * serve requests. Likewise, if our advertised bandwidth is less than + * MIN_BW_TO_ADVERTISE_DIRSERVER, don't bother trying to serve requests. */ static int -decide_to_advertise_dirport(const or_options_t *options, uint16_t dir_port) +router_should_be_directory_server(const or_options_t *options, int dir_port) { static int advertising=1; /* start out assuming we will advertise */ int new_choice=1; const char *reason = NULL; - /* Section one: reasons to publish or not publish that aren't - * worth mentioning to the user, either because they're obvious - * or because they're normal behavior. */ - - if (!dir_port) /* short circuit the rest of the function */ - return 0; - if (authdir_mode(options)) /* always publish */ - return dir_port; - if (net_is_disabled()) - return 0; - if (!check_whether_dirport_reachable()) - return 0; - if (!router_get_advertised_dir_port(options, dir_port)) - return 0; - - /* Section two: reasons to publish or not publish that the user - * might find surprising. These are generally config options that - * make us choose not to publish. */ - if (accounting_is_enabled(options)) { /* Don't spend bytes for directory traffic if we could end up hibernating, * but allow DirPort otherwise. Some people set AccountingMax because @@ -1158,10 +1144,9 @@ decide_to_advertise_dirport(const or_options_t *options, uint16_t dir_port) new_choice = 0; reason = "AccountingMax enabled"; } -#define MIN_BW_TO_ADVERTISE_DIRPORT 51200 - } else if (options->BandwidthRate < MIN_BW_TO_ADVERTISE_DIRPORT || + } else if (options->BandwidthRate < MIN_BW_TO_ADVERTISE_DIRSERVER || (options->RelayBandwidthRate > 0 && - options->RelayBandwidthRate < MIN_BW_TO_ADVERTISE_DIRPORT)) { + options->RelayBandwidthRate < MIN_BW_TO_ADVERTISE_DIRSERVER)) { /* if we're advertising a small amount */ new_choice = 0; reason = "BandwidthRate under 50KB"; @@ -1169,15 +1154,61 @@ decide_to_advertise_dirport(const or_options_t *options, uint16_t dir_port) if (advertising != new_choice) { if (new_choice == 1) { - log_notice(LD_DIR, "Advertising DirPort as %d", dir_port); + if (dir_port > 0) + log_notice(LD_DIR, "Advertising DirPort as %d", dir_port); + else + log_notice(LD_DIR, "Advertising directory service support"); } else { tor_assert(reason); - log_notice(LD_DIR, "Not advertising DirPort (Reason: %s)", reason); + log_notice(LD_DIR, "Not advertising Dir%s (Reason: %s)", + dir_port ? "Port" : "ectory Service support", reason); } advertising = new_choice; } - return advertising ? dir_port : 0; + return advertising; +} + +/** Return 1 if we are configured to accept either relay or directory requests + * from clients and we aren't at risk of exceeding our bandwidth limits, thus + * we should be a directory server. If not, return 0. + */ +int +dir_server_mode(const or_options_t *options) +{ + return (server_mode(options) || options->DirPort_set) && + router_should_be_directory_server(options, 0); +} + +/** Look at a variety of factors, and return 0 if we don't want to + * advertise the fact that we have a DirPort open, else return the + * DirPort we want to advertise. + * + * Log a helpful message if we change our mind about whether to publish + * a DirPort. + */ +static int +decide_to_advertise_dirport(const or_options_t *options, uint16_t dir_port) +{ + /* Part one: reasons to publish or not publish that aren't + * worth mentioning to the user, either because they're obvious + * or because they're normal behavior. */ + + if (!dir_port) /* short circuit the rest of the function */ + return 0; + if (authdir_mode(options)) /* always publish */ + return dir_port; + if (net_is_disabled()) + return 0; + if (!check_whether_dirport_reachable()) + return 0; + if (!router_get_advertised_dir_port(options, dir_port)) + return 0; + + /* Part two: reasons to publish or not publish that the user + * might find surprising. router_should_be_directory_server() + * considers config options that make us choose not to publish. */ + return router_should_be_directory_server(options, dir_port) ? dir_port : 0; } /** Allocate and return a new extend_info_t that can be used to build @@ -2642,6 +2673,10 @@ router_dump_router_to_string(routerinfo_t *router, tor_free(p6); } + if (dir_server_mode(options)) { + smartlist_add(chunks, tor_strdup("tunnelled-dir-server\n")); + } + /* Sign the descriptor with Ed25519 */ if (emit_ed_sigs) { smartlist_add(chunks, tor_strdup("router-sig-ed25519 ")); -- cgit v1.2.3-54-g00ecf From 997f779a7f05540e5f564b4d121706c4a7069fb2 Mon Sep 17 00:00:00 2001 From: Matthew Finkel Date: Sun, 8 Feb 2015 06:51:51 +0000 Subject: Add new DirCache configuration option This will give relay operators the ability of disabling the caching of directory data. In general, this should not be necessary, but on some lower-resource systems it may beneficial. --- doc/tor.1.txt | 6 +++ src/or/config.c | 61 +++++++++++++++++++++++++ src/or/config.h | 2 + src/or/or.h | 4 ++ src/or/router.c | 4 +- src/test/test_options.c | 115 +++++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 186 insertions(+), 6 deletions(-) (limited to 'src/or/router.c') diff --git a/doc/tor.1.txt b/doc/tor.1.txt index f173a97aa3..021353dcb0 100644 --- a/doc/tor.1.txt +++ b/doc/tor.1.txt @@ -1987,6 +1987,12 @@ if DirPort is non-zero): except that port specifiers are ignored. Any address not matched by some entry in the policy is accepted. +[[DirCache]] **DirCache** **0**|**1**:: + When this option is set, Tor caches all current directory documents and + accepts client requests for them. Setting DirPort is not required for this, + because clients connect via the ORPort by default. Setting either DirPort + or BridgeRelay and setting DirCache to 0 is not supported. (Default: 1) + DIRECTORY AUTHORITY SERVER OPTIONS ---------------------------------- diff --git a/src/or/config.c b/src/or/config.c index 9ec47d2459..f9cab9f670 100644 --- a/src/or/config.c +++ b/src/or/config.c @@ -222,6 +222,7 @@ static config_var_t option_vars_[] = { V(DirPortFrontPage, FILENAME, NULL), VAR("DirReqStatistics", BOOL, DirReqStatistics_option, "1"), VAR("DirAuthority", LINELIST, DirAuthorities, NULL), + V(DirCache, BOOL, "1"), V(DirAuthorityFallbackRate, DOUBLE, "1.0"), V(DisableAllSwap, BOOL, "0"), V(DisableDebuggerAttachment, BOOL, "1"), @@ -3457,6 +3458,24 @@ options_validate(or_options_t *old_options, or_options_t *options, REJECT("AccountingRule must be 'sum' or 'max'"); } + if (options->DirPort_set && !options->DirCache) { + REJECT("DirPort configured but DirCache disabled. DirPort requires " + "DirCache."); + } + + if (options->BridgeRelay && !options->DirCache) { + REJECT("We're a bridge but DirCache is disabled. BridgeRelay requires " + "DirCache."); + } + + if (server_mode(options)) { + char *msg = NULL; + if (have_enough_mem_for_dircache(options, 0, &msg)) { + log_warn(LD_CONFIG, "%s", msg); + tor_free(msg); + } + } + if (options->HTTPProxy) { /* parse it now */ if (tor_addr_port_lookup(options->HTTPProxy, &options->HTTPProxyAddr, &options->HTTPProxyPort) < 0) @@ -4065,6 +4084,48 @@ compute_real_max_mem_in_queues(const uint64_t val, int log_guess) } } +/* If we have less than 300 MB suggest disabling dircache */ +#define DIRCACHE_MIN_MB_BANDWIDTH 300 +#define DIRCACHE_MIN_BANDWIDTH (DIRCACHE_MIN_MB_BANDWIDTH*ONE_MEGABYTE) +#define STRINGIFY(val) #val + +/** Create a warning message for emitting if we are a dircache but may not have + * enough system memory, or if we are not a dircache but probably should be. + * Return -1 when a message is returned in *msg*, else return 0. */ +STATIC int +have_enough_mem_for_dircache(const or_options_t *options, size_t total_mem, + char **msg) +{ + *msg = NULL; + if (total_mem == 0) { + if (get_total_system_memory(&total_mem) < 0) + total_mem = options->MaxMemInQueues; + } + if (options->DirCache) { + if (total_mem < DIRCACHE_MIN_BANDWIDTH) { + if (options->BridgeRelay) { + *msg = strdup("Running a Bridge with less than " + STRINGIFY(DIRCACHE_MIN_MB_BANDWIDTH) " MB of memory is " + "not recommended."); + } else { + *msg = strdup("Being a directory cache (default) with less than " + STRINGIFY(DIRCACHE_MIN_MB_BANDWIDTH) " MB of memory is " + "not recommended and may consume most of the available " + "resources, consider disabling this functionality by " + "setting the DirCache option to 0."); + } + } + } else { + if (total_mem >= DIRCACHE_MIN_BANDWIDTH) { + *msg = strdup("DirCache is disabled and we are configured as a " + "relay. This may disqualify us from becoming a guard in the " + "future."); + } + } + return *msg == NULL ? 0 : -1; +} +#undef STRINGIFY + /** Helper: return true iff s1 and s2 are both NULL, or both non-NULL * equal strings. */ static int diff --git a/src/or/config.h b/src/or/config.h index bfdd1694eb..6e08f9d178 100644 --- a/src/or/config.h +++ b/src/or/config.h @@ -158,6 +158,8 @@ STATIC int parse_dir_authority_line(const char *line, dirinfo_type_t required_type, int validate_only); STATIC int parse_dir_fallback_line(const char *line, int validate_only); +STATIC int have_enough_mem_for_dircache(const or_options_t *options, + size_t total_mem, char **msg); #endif #endif diff --git a/src/or/or.h b/src/or/or.h index 3cb1e7d7ef..89c539817f 100644 --- a/src/or/or.h +++ b/src/or/or.h @@ -3969,6 +3969,10 @@ typedef struct { /** Should we fetch our dir info at the start of the consensus period? */ int FetchDirInfoExtraEarly; + int DirCache; /**< Cache all directory documents and accept requests via + * tunnelled dir conns from clients. If 1, enabled (default); + * If 0, disabled. */ + char *VirtualAddrNetworkIPv4; /**< Address and mask to hand out for virtual * MAPADDRESS requests for IPv4 addresses */ char *VirtualAddrNetworkIPv6; /**< Address and mask to hand out for virtual diff --git a/src/or/router.c b/src/or/router.c index 1927cfd38b..5e4f855410 100644 --- a/src/or/router.c +++ b/src/or/router.c @@ -1176,7 +1176,9 @@ router_should_be_directory_server(const or_options_t *options, int dir_port) int dir_server_mode(const or_options_t *options) { - return (server_mode(options) || options->DirPort_set) && + if (!options->DirCache) + return 0; + return (server_mode(options) || options->DirPort_set) && router_should_be_directory_server(options, 0); } diff --git a/src/test/test_options.c b/src/test/test_options.c index a8ebadb14b..7e47f33de6 100644 --- a/src/test/test_options.c +++ b/src/test/test_options.c @@ -69,22 +69,29 @@ clear_log_messages(void) messages = NULL; } +#define setup_options(opt,dflt) \ + do { \ + opt = options_new(); \ + opt->command = CMD_RUN_TOR; \ + options_init(opt); \ + \ + dflt = config_dup(&options_format, opt); \ + clear_log_messages(); \ + } while (0) + static void test_options_validate_impl(const char *configuration, const char *expect_errmsg, int expect_log_severity, const char *expect_log) { - or_options_t *opt = options_new(); + or_options_t *opt=NULL; or_options_t *dflt; config_line_t *cl=NULL; char *msg=NULL; int r; - opt->command = CMD_RUN_TOR; - options_init(opt); - dflt = config_dup(&options_format, opt); - clear_log_messages(); + setup_options(opt, dflt); r = config_get_lines(configuration, &cl, 1); tt_int_op(r, OP_EQ, 0); @@ -159,12 +166,110 @@ test_options_validate(void *arg) "ServerTransportOptions did not parse", LOG_WARN, "\"slingsnappy\" is not a k=v"); + WANT_ERR("DirPort 8080\nDirCache 0", + "DirPort configured but DirCache disabled."); + WANT_ERR("BridgeRelay 1\nDirCache 0", + "We're a bridge but DirCache is disabled."); + clear_log_messages(); return; } +#define MEGABYTEIFY(mb) (U64_LITERAL(mb) << 20) +static void +test_have_enough_mem_for_dircache(void *arg) +{ + (void)arg; + or_options_t *opt=NULL; + or_options_t *dflt; + config_line_t *cl=NULL; + char *msg=NULL;; + int r; + const char *configuration = "ORPort 8080\nDirCache 1", *expect_errmsg; + + setup_options(opt, dflt); + setup_log_callback(); + (void)dflt; + + r = config_get_lines(configuration, &cl, 1); + tt_int_op(r, OP_EQ, 0); + + r = config_assign(&options_format, opt, cl, 0, 0, &msg); + tt_int_op(r, OP_EQ, 0); + + /* 300 MB RAM available, DirCache enabled */ + r = have_enough_mem_for_dircache(opt, MEGABYTEIFY(300), &msg); + tt_int_op(r, OP_EQ, 0); + tt_assert(!msg); + + /* 200 MB RAM available, DirCache enabled */ + r = have_enough_mem_for_dircache(opt, MEGABYTEIFY(200), &msg); + tt_int_op(r, OP_EQ, -1); + expect_errmsg = "Being a directory cache (default) with less than "; + if (!strstr(msg, expect_errmsg)) { + TT_DIE(("Expected error message <%s> from <%s>, but got <%s>.", + expect_errmsg, configuration, msg)); + } + tor_free(msg); + + configuration = "ORPort 8080\nDirCache 1\nBridgeRelay 1"; + r = config_get_lines(configuration, &cl, 1); + tt_int_op(r, OP_EQ, 0); + + r = config_assign(&options_format, opt, cl, 0, 0, &msg); + tt_int_op(r, OP_EQ, 0); + + /* 300 MB RAM available, DirCache enabled, Bridge */ + r = have_enough_mem_for_dircache(opt, MEGABYTEIFY(300), &msg); + tt_int_op(r, OP_EQ, 0); + tt_assert(!msg); + + /* 200 MB RAM available, DirCache enabled, Bridge */ + r = have_enough_mem_for_dircache(opt, MEGABYTEIFY(200), &msg); + tt_int_op(r, OP_EQ, -1); + expect_errmsg = "Running a Bridge with less than "; + if (!strstr(msg, expect_errmsg)) { + TT_DIE(("Expected error message <%s> from <%s>, but got <%s>.", + expect_errmsg, configuration, msg)); + } + tor_free(msg); + + configuration = "ORPort 8080\nDirCache 0"; + r = config_get_lines(configuration, &cl, 1); + tt_int_op(r, OP_EQ, 0); + + r = config_assign(&options_format, opt, cl, 0, 0, &msg); + tt_int_op(r, OP_EQ, 0); + + /* 200 MB RAM available, DirCache disabled */ + r = have_enough_mem_for_dircache(opt, MEGABYTEIFY(200), &msg); + tt_int_op(r, OP_EQ, 0); + tt_assert(!msg); + + /* 300 MB RAM available, DirCache disabled */ + r = have_enough_mem_for_dircache(opt, MEGABYTEIFY(300), &msg); + tt_int_op(r, OP_EQ, -1); + expect_errmsg = "DirCache is disabled and we are configured as a "; + if (!strstr(msg, expect_errmsg)) { + TT_DIE(("Expected error message <%s> from <%s>, but got <%s>.", + expect_errmsg, configuration, msg)); + } + tor_free(msg); + + clear_log_messages(); + + done: + if (msg) + tor_free(msg); + tor_free(dflt); + tor_free(opt); + tor_free(cl); + return; +} + struct testcase_t options_tests[] = { { "validate", test_options_validate, TT_FORK, NULL, NULL }, + { "mem_dircache", test_have_enough_mem_for_dircache, TT_FORK, NULL, NULL }, END_OF_TESTCASES }; -- cgit v1.2.3-54-g00ecf From 54406f78b84103ed7fd4610a89dda9d37f0f033e Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Tue, 3 Nov 2015 09:29:31 -0500 Subject: Change dataflow on generating 'dir-cache' flag. Convention is that router_dump_router_to_string() should look at its input "router", which should be generated by router_build_fresh_descirptor(). --- src/or/router.c | 3 ++- src/test/test_dir.c | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'src/or/router.c') diff --git a/src/or/router.c b/src/or/router.c index 5e4f855410..5c680ce875 100644 --- a/src/or/router.c +++ b/src/or/router.c @@ -1899,6 +1899,7 @@ router_build_fresh_descriptor(routerinfo_t **r, extrainfo_t **e) ri->addr = addr; ri->or_port = router_get_advertised_or_port(options); ri->dir_port = router_get_advertised_dir_port(options, 0); + ri->supports_tunnelled_dir_requests = dir_server_mode(options); ri->cache_info.published_on = time(NULL); ri->onion_pkey = crypto_pk_dup_key(get_onion_key()); /* must invoke from * main thread */ @@ -2675,7 +2676,7 @@ router_dump_router_to_string(routerinfo_t *router, tor_free(p6); } - if (dir_server_mode(options)) { + if (router->supports_tunnelled_dir_requests) { smartlist_add(chunks, tor_strdup("tunnelled-dir-server\n")); } diff --git a/src/test/test_dir.c b/src/test/test_dir.c index 97a9200bdb..cf95df5100 100644 --- a/src/test/test_dir.c +++ b/src/test/test_dir.c @@ -111,6 +111,7 @@ test_dir_formats(void *arg) r1->cache_info.published_on = 0; r1->or_port = 9000; r1->dir_port = 9003; + r1->supports_tunnelled_dir_requests = 1; tor_addr_parse(&r1->ipv6_addr, "1:2:3:4::"); r1->ipv6_orport = 9999; r1->onion_pkey = crypto_pk_dup_key(pk1); @@ -155,6 +156,7 @@ test_dir_formats(void *arg) r2->cache_info.published_on = 5; r2->or_port = 9005; r2->dir_port = 0; + r2->supports_tunnelled_dir_requests = 1; r2->onion_pkey = crypto_pk_dup_key(pk2); curve25519_keypair_t r2_onion_keypair; curve25519_keypair_generate(&r2_onion_keypair, 0); @@ -176,12 +178,9 @@ test_dir_formats(void *arg) options->ContactInfo = tor_strdup("Magri White " ""); - options->ORPort_set = options->DirPort_set = options->AssumeReachable = 1; buf = router_dump_router_to_string(r1, pk2, NULL, NULL, NULL); tor_free(options->ContactInfo); - /* Reset for later */ - options->ORPort_set = options->DirPort_set = options->AssumeReachable = 0; tt_assert(buf); strlcpy(buf2, "router Magri 192.168.0.1 9000 0 9003\n" @@ -301,8 +300,6 @@ test_dir_formats(void *arg) strlcat(buf2, "tunnelled-dir-server\n", sizeof(buf2)); strlcat(buf2, "router-sig-ed25519 ", sizeof(buf2)); - options->ORPort_set = 1; - buf = router_dump_router_to_string(r2, pk1, pk2, &r2_onion_keypair, &kp2); tt_assert(buf); buf[strlen(buf2)] = '\0'; /* Don't compare the sig; it's never the same @@ -314,7 +311,6 @@ test_dir_formats(void *arg) buf = router_dump_router_to_string(r2, pk1, NULL, NULL, NULL); /* Reset for later */ - options->ORPort_set = 0; cp = buf; rp2 = router_parse_entry_from_string((const char*)cp,NULL,1,0,NULL,NULL); tt_assert(rp2); -- cgit v1.2.3-54-g00ecf From 0c8e042c30946faa564f03251bb3663e6204df9a Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Wed, 11 Nov 2015 13:34:05 -0500 Subject: Restore semantics of advertise vs serve on directory cacheing When we are low on accounted bandwidth, we stop advertising that we're a directory, but we will continue to answer directory requests, just as before. --- src/or/router.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) (limited to 'src/or/router.c') diff --git a/src/or/router.c b/src/or/router.c index 5c680ce875..c1d970dbe1 100644 --- a/src/or/router.c +++ b/src/or/router.c @@ -1099,11 +1099,26 @@ check_whether_dirport_reachable(void) can_reach_dir_port; } -/** The lower threshold of remaining bandwidth required to advertise directory - * services */ +/** The lower threshold of remaining bandwidth required to advertise (or + * automatically provide) directory services */ /* XXX Should this be increased? */ #define MIN_BW_TO_ADVERTISE_DIRSERVER 51200 +/** Return true iff we have enough configured bandwidth to cache directory + * information. */ +static int +router_has_bandwidth_to_be_dirserver(const or_options_t *options) +{ + if (options->BandwidthRate < MIN_BW_TO_ADVERTISE_DIRSERVER) { + return 0; + } + if (options->RelayBandwidthRate > 0 && + options->RelayBandwidthRate < MIN_BW_TO_ADVERTISE_DIRSERVER) { + return 0; + } + return 1; +} + /** Helper: Return 1 if we have sufficient resources for serving directory * requests, return 0 otherwise. * dir_port is either 0 or the configured DirPort number. @@ -1144,9 +1159,7 @@ router_should_be_directory_server(const or_options_t *options, int dir_port) new_choice = 0; reason = "AccountingMax enabled"; } - } else if (options->BandwidthRate < MIN_BW_TO_ADVERTISE_DIRSERVER || - (options->RelayBandwidthRate > 0 && - options->RelayBandwidthRate < MIN_BW_TO_ADVERTISE_DIRSERVER)) { + } else if (! router_has_bandwidth_to_be_dirserver(options)) { /* if we're advertising a small amount */ new_choice = 0; reason = "BandwidthRate under 50KB"; @@ -1178,8 +1191,8 @@ dir_server_mode(const or_options_t *options) { if (!options->DirCache) return 0; - return (server_mode(options) || options->DirPort_set) && - router_should_be_directory_server(options, 0); + return options->DirPort_set || + (server_mode(options) && router_has_bandwidth_to_be_dirserver(options)); } /** Look at a variety of factors, and return 0 if we don't want to @@ -1899,7 +1912,8 @@ router_build_fresh_descriptor(routerinfo_t **r, extrainfo_t **e) ri->addr = addr; ri->or_port = router_get_advertised_or_port(options); ri->dir_port = router_get_advertised_dir_port(options, 0); - ri->supports_tunnelled_dir_requests = dir_server_mode(options); + ri->supports_tunnelled_dir_requests = dir_server_mode(options) && + router_should_be_directory_server(options, ri->dir_port); ri->cache_info.published_on = time(NULL); ri->onion_pkey = crypto_pk_dup_key(get_onion_key()); /* must invoke from * main thread */ -- cgit v1.2.3-54-g00ecf