diff --git a/doc/spec/dir-spec.txt b/doc/spec/dir-spec.txt index 04e73c4676..4c22771882 100644 --- a/doc/spec/dir-spec.txt +++ b/doc/spec/dir-spec.txt @@ -848,6 +848,30 @@ Mean number of circuits that are included in any of the deciles, rounded up to the next integer. + "conn-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL + [At most once] + + YYYY-MM-DD HH:MM:SS defines the end of the included connection + statistics measurement interval of length NSEC seconds (86400 + seconds by default). + + A "conn-stats-end" line, as well as any other "conn-*" line, + is first added after the relay has been running for at least 24 + hours. + + "conn-bi-direct" BELOW,READ,WRITE,BOTH NL + [At most once] + + Number of connections, split into 10-second intervals, that are + used uni-directionally or bi-directionally. Every 10 seconds, + we determine for every connection whether we read and wrote less + than a threshold of 20 KiB (BELOW), read at least 10 times more + than we wrote (READ), wrote at least 10 times more than we read + (WRITE), or read and wrote more than the threshold, but not 10 + times more in either direction (BOTH). After classifying a + connection, read and write counters are reset for the next + 10-second interval. + "exit-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL [At most once.] diff --git a/src/or/config.c b/src/or/config.c index d371276b18..5ab4b46d5f 100644 --- a/src/or/config.c +++ b/src/or/config.c @@ -200,6 +200,7 @@ static config_var_t _option_vars[] = { V(ClientOnly, BOOL, "0"), V(ConsensusParams, STRING, NULL), V(ConnLimit, UINT, "1000"), + V(ConnStatistics, BOOL, "0"), V(ConstrainedSockets, BOOL, "0"), V(ConstrainedSockSize, MEMUNIT, "8192"), V(ContactInfo, STRING, NULL), @@ -1391,7 +1392,8 @@ options_act(or_options_t *old_options) } if (options->CellStatistics || options->DirReqStatistics || - options->EntryStatistics || options->ExitPortStatistics) { + options->EntryStatistics || options->ExitPortStatistics || + options->ConnStatistics) { time_t now = time(NULL); if ((!old_options || !old_options->CellStatistics) && options->CellStatistics) @@ -1405,6 +1407,9 @@ options_act(or_options_t *old_options) if ((!old_options || !old_options->ExitPortStatistics) && options->ExitPortStatistics) rep_hist_exit_stats_init(now); + if ((!old_options || !old_options->ConnStatistics) && + options->ConnStatistics) + rep_hist_conn_stats_init(now); if (!old_options) log_notice(LD_CONFIG, "Configured to measure statistics. Look for " "the *-stats files that will first be written to the " @@ -1423,6 +1428,9 @@ options_act(or_options_t *old_options) if (old_options && old_options->ExitPortStatistics && !options->ExitPortStatistics) rep_hist_exit_stats_term(); + if (old_options && old_options->ConnStatistics && + !options->ConnStatistics) + rep_hist_conn_stats_term(); /* Check if we need to parse and add the EntryNodes config option. */ if (options->EntryNodes && diff --git a/src/or/connection.c b/src/or/connection.c index 14883157a9..6f9ae20a78 100644 --- a/src/or/connection.c +++ b/src/or/connection.c @@ -2185,6 +2185,11 @@ connection_buckets_decrement(connection_t *conn, time_t now, if (!connection_is_rate_limited(conn)) return; /* local IPs are free */ + + if (conn->type == CONN_TYPE_OR) + rep_hist_note_or_conn_bytes(conn->global_identifier, num_read, + num_written, now); + if (num_read > 0) { rep_hist_note_bytes_read(num_read, now); } diff --git a/src/or/main.c b/src/or/main.c index 823290aa2d..8d98d3f732 100644 --- a/src/or/main.c +++ b/src/or/main.c @@ -1204,6 +1204,11 @@ run_scheduled_events(time_t now) if (next_write && next_write < next_time_to_write_stats_files) next_time_to_write_stats_files = next_write; } + if (options->ConnStatistics) { + time_t next_write = rep_hist_conn_stats_write(time_to_write_stats_files); + if (next_write && next_write < next_time_to_write_stats_files) + next_time_to_write_stats_files = next_write; + } time_to_write_stats_files = next_time_to_write_stats_files; } diff --git a/src/or/or.h b/src/or/or.h index 8110500b26..c9e43fa15d 100644 --- a/src/or/or.h +++ b/src/or/or.h @@ -2894,6 +2894,9 @@ typedef struct { /** If true, the user wants us to collect statistics on port usage. */ int ExitPortStatistics; + /** If true, the user wants us to collect connection statistics. */ + int ConnStatistics; + /** If true, the user wants us to collect cell statistics. */ int CellStatistics; diff --git a/src/or/rephist.c b/src/or/rephist.c index 175ea01756..f63e9519e4 100644 --- a/src/or/rephist.c +++ b/src/or/rephist.c @@ -7,7 +7,7 @@ * \brief Basic history and "reputation" functionality to remember * which servers have worked in the past, how much bandwidth we've * been using, which ports we tend to want, and so on; further, - * exit port statistics and cell statistics. + * exit port statistics, cell statistics, and connection statistics. **/ #include "or.h" @@ -2418,6 +2418,201 @@ rep_hist_buffer_stats_write(time_t now) return start_of_buffer_stats_interval + WRITE_STATS_INTERVAL; } +/*** Connection statistics ***/ + +/** Start of the current connection stats interval or 0 if we're not + * collecting connection statistics. */ +static time_t start_of_conn_stats_interval; + +/** Initialize connection stats. */ +void +rep_hist_conn_stats_init(time_t now) +{ + start_of_conn_stats_interval = now; +} + +#define BIDI_THRESHOLD 20480 + +#define BIDI_FACTOR 10 + +#define BIDI_INTERVAL 10 + +/* Start of next BIDI_INTERVAL second interval. */ +static time_t bidi_next_interval = 0; + +/* Number of connections that we read and wrote less than BIDI_THRESHOLD + * bytes from/to in BIDI_INTERVAL seconds. */ +static uint32_t below_threshold = 0; + +/* Number of connections that we read at least BIDI_FACTOR times more + * bytes from than we wrote to in BIDI_INTERVAL seconds. */ +static uint32_t mostly_read = 0; + +/* Number of connections that we wrote at least BIDI_FACTOR times more + * bytes to than we read from in BIDI_INTERVAL seconds. */ +static uint32_t mostly_written = 0; + +/* Number of connections that we read and wrote at least BIDI_THRESHOLD + * bytes from/to, but not BIDI_FACTOR times more in either direction in + * BIDI_INTERVAL seconds. */ +static uint32_t both_read_and_written = 0; + +/* Entry in a map from connection ID to the number of read and written + * bytes on this connection in a BIDI_INTERVAL second interval. */ +typedef struct bidi_map_entry_t { + HT_ENTRY(bidi_map_entry_t) node; + uint64_t conn_id; /**< Connection ID */ + size_t read; /**< Number of read bytes */ + size_t written; /**< Number of written bytes */ +} bidi_map_entry_t; + +/** Map of OR connections together with the number of read and written + * bytes in the current BIDI_INTERVAL second interval. */ +static HT_HEAD(bidimap, bidi_map_entry_t) bidi_map = + HT_INITIALIZER(); + +static int +bidi_map_ent_eq(const bidi_map_entry_t *a, const bidi_map_entry_t *b) +{ + return a->conn_id == b->conn_id; +} + +static unsigned +bidi_map_ent_hash(const bidi_map_entry_t *entry) +{ + return (unsigned) entry->conn_id; +} + +HT_PROTOTYPE(bidimap, bidi_map_entry_t, node, bidi_map_ent_hash, + bidi_map_ent_eq); +HT_GENERATE(bidimap, bidi_map_entry_t, node, bidi_map_ent_hash, + bidi_map_ent_eq, 0.6, malloc, realloc, free); + +static void +bidi_map_free(void) +{ + bidi_map_entry_t **ptr, **next, *ent; + for (ptr = HT_START(bidimap, &bidi_map); ptr; ptr = next) { + ent = *ptr; + next = HT_NEXT_RMV(bidimap, &bidi_map, ptr); + tor_free(ent); + } + HT_CLEAR(bidimap, &bidi_map); +} + +/** Stop collecting connection stats in a way that we can re-start doing + * so in rep_hist_conn_stats_init(). */ +void +rep_hist_conn_stats_term(void) +{ + below_threshold = 0; + mostly_read = 0; + mostly_written = 0; + both_read_and_written = 0; + start_of_conn_stats_interval = 0; + bidi_map_free(); +} + + +/** We read num_read bytes and wrote num_written from/to OR + * connection conn_id in second when. If this is the first + * observation in a new interval, sum up the last observations. Add bytes + * for this connection. */ +void +rep_hist_note_or_conn_bytes(uint64_t conn_id, size_t num_read, + size_t num_written, time_t when) +{ + if (!start_of_conn_stats_interval) + return; + /* Initialize */ + if (bidi_next_interval == 0) + bidi_next_interval = when + BIDI_INTERVAL; + /* Sum up last period's statistics */ + if (when >= bidi_next_interval) { + bidi_map_entry_t **ptr, **next, *ent; + for (ptr = HT_START(bidimap, &bidi_map); ptr; ptr = next) { + ent = *ptr; + if (ent->read + ent->written < BIDI_THRESHOLD) + below_threshold++; + else if (ent->read >= ent->written * BIDI_FACTOR) + mostly_read++; + else if (ent->written >= ent->read * BIDI_FACTOR) + mostly_written++; + else + both_read_and_written++; + next = HT_NEXT_RMV(bidimap, &bidi_map, ptr); + tor_free(ent); + } + while (when >= bidi_next_interval) + bidi_next_interval += BIDI_INTERVAL; + log_info(LD_GENERAL, "%d below threshold, %d mostly read, " + "%d mostly written, %d both read and written.", + below_threshold, mostly_read, mostly_written, + both_read_and_written); + } + /* Add this connection's bytes. */ + if (num_read > 0 || num_written > 0) { + bidi_map_entry_t *entry, lookup; + lookup.conn_id = conn_id; + entry = HT_FIND(bidimap, &bidi_map, &lookup); + if (entry) { + entry->written += num_written; + entry->read += num_read; + } else { + entry = tor_malloc_zero(sizeof(bidi_map_entry_t)); + entry->conn_id = conn_id; + entry->written = num_written; + entry->read = num_read; + HT_INSERT(bidimap, &bidi_map, entry); + } + } +} + +/** Write conn statistics to $DATADIR/stats/conn-stats and return when + * we would next want to write conn stats. */ +time_t +rep_hist_conn_stats_write(time_t now) +{ + char *statsdir = NULL, *filename = NULL; + char written[ISO_TIME_LEN+1]; + open_file_t *open_file = NULL; + FILE *out; + + if (!start_of_conn_stats_interval) + return 0; /* Not initialized. */ + if (start_of_conn_stats_interval + WRITE_STATS_INTERVAL > now) + goto done; /* Not ready to write */ + + /* write to file */ + statsdir = get_datadir_fname("stats"); + if (check_private_dir(statsdir, CPD_CREATE) < 0) + goto done; + filename = get_datadir_fname2("stats", "conn-stats"); + out = start_writing_to_stdio_file(filename, OPEN_FLAGS_APPEND, + 0600, &open_file); + if (!out) + goto done; + format_iso_time(written, now); + if (fprintf(out, "conn-stats-end %s (%d s)\n", written, + (unsigned) (now - start_of_conn_stats_interval)) < 0) + goto done; + + if (fprintf(out, "conn-bi-direct %d,%d,%d,%d\n", + below_threshold, mostly_read, mostly_written, + both_read_and_written) < 0) + goto done; + + finish_writing_to_file(open_file); + open_file = NULL; + start_of_conn_stats_interval = now; + done: + if (open_file) + abort_writing_to_file(open_file); + tor_free(filename); + tor_free(statsdir); + return start_of_conn_stats_interval + WRITE_STATS_INTERVAL; +} + /** Free all storage held by the OR/link history caches, by the * bandwidth history arrays, by the port history, or by statistics . */ void @@ -2432,5 +2627,6 @@ rep_hist_free_all(void) tor_free(exit_streams); built_last_stability_doc_at = 0; predicted_ports_free(); + bidi_map_free(); } diff --git a/src/or/rephist.h b/src/or/rephist.h index 8f5a34dacf..efc8531ac0 100644 --- a/src/or/rephist.h +++ b/src/or/rephist.h @@ -76,5 +76,11 @@ void rep_hist_buffer_stats_add_circ(circuit_t *circ, time_t rep_hist_buffer_stats_write(time_t now); void rep_hist_buffer_stats_term(void); +void rep_hist_conn_stats_init(time_t now); +void rep_hist_note_or_conn_bytes(uint64_t conn_id, size_t num_read, + size_t num_written, time_t when); +time_t rep_hist_conn_stats_write(time_t now); +void rep_hist_conn_stats_term(void); + #endif diff --git a/src/or/router.c b/src/or/router.c index 1f3967d3d5..b612e9a7fb 100644 --- a/src/or/router.c +++ b/src/or/router.c @@ -2051,6 +2051,19 @@ extrainfo_dump_to_string(char **s_out, extrainfo_t *extrainfo, "exit-stats-end", now, &contents) > 0) { smartlist_add(chunks, contents); } + if (options->ConnStatistics && + load_stats_file("stats"PATH_SEPARATOR"conn-stats", + "conn-stats-end", now, &contents) > 0) { + size_t pos = strlen(s); + if (strlcpy(s + pos, contents, maxlen - strlen(s)) != + strlen(contents)) { + log_warn(LD_DIR, "Could not write conn-stats to extra-info " + "descriptor."); + s[pos] = '\0'; + write_stats_to_extrainfo = 0; + } + tor_free(contents); + } } if (should_record_bridge_info(options) && write_stats_to_extrainfo) {