/* Copyright (c) 2001 Matej Pfajfar. * Copyright (c) 2001-2004, Roger Dingledine. * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. * Copyright (c) 2007-2021, The Tor Project, Inc. */ /* See LICENSE for licensing information */ /** * @file connstats.c * @brief Count bidirectional vs one-way connections. * * Connection statistics, use to track one-way and bidirectional connections. * * Note that this code counts concurrent connections in each * BIDI_INTERVAL-second interval, not total connections. It can tell you what * fraction of connections are bidirectional at each time, not necessarily * what number are bidirectional. **/ #include "orconfig.h" #include "core/or/or.h" #include "feature/stats/connstats.h" #include "app/config/config.h" /** 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 conn_stats_init(time_t now) { start_of_conn_stats_interval = now; } /** Count connections on which we read and wrote less than this many bytes * as "below threshold." */ #define BIDI_THRESHOLD 20480 /** Count connections that we read or wrote at least this factor as many * bytes from/to than we wrote or read to/from as mostly reading or * writing. */ #define BIDI_FACTOR 10 /** Interval length in seconds for considering read and written bytes for * connection stats. */ #define BIDI_INTERVAL 10 /** Start of next BIDI_INTERVAL second interval. */ static time_t bidi_next_interval = 0; /** A single grouped set of connection type counts. */ typedef struct conn_counts_t { /** Number of connections that we read and wrote less than BIDI_THRESHOLD * bytes from/to in BIDI_INTERVAL seconds. */ uint32_t below_threshold; /** Number of connections that we read at least BIDI_FACTOR times more * bytes from than we wrote to in BIDI_INTERVAL seconds. */ uint32_t mostly_read; /** Number of connections that we wrote at least BIDI_FACTOR times more * bytes to than we read from in BIDI_INTERVAL seconds. */ uint32_t mostly_written; /** 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. */ uint32_t both_read_and_written; } conn_counts_t ; /** A collection of connection counts, over all OR connections. */ static conn_counts_t counts; /** A collection of connection counts, over IPv6 OR connections only. */ static conn_counts_t counts_ipv6; /** 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 */ bool is_ipv6; /**< True if this is an IPv6 connection */ } 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(); /** Hashtable helper: return true if @a a and @a b have the same key. */ 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; } /** Hashtable helper: compute a digest for the key of @a entry. */ 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_GENERATE2(bidimap, bidi_map_entry_t, node, bidi_map_ent_hash, bidi_map_ent_eq, 0.6, tor_reallocarray_, tor_free_); /** Release all storage held in connstats.c */ void conn_stats_free_all(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); } /** Reset counters for conn statistics. */ void conn_stats_reset(time_t now) { start_of_conn_stats_interval = now; memset(&counts, 0, sizeof(counts)); memset(&counts_ipv6, 0, sizeof(counts_ipv6)); conn_stats_free_all(); } /** Stop collecting connection stats in a way that we can re-start doing * so in conn_stats_init(). */ void conn_stats_terminate(void) { conn_stats_reset(0); } /** * Record a single entry @a ent in the counts structure @a cnt. */ static void add_entry_to_count(conn_counts_t *cnt, const bidi_map_entry_t *ent) { if (ent->read + ent->written < BIDI_THRESHOLD) cnt->below_threshold++; else if (ent->read >= ent->written * BIDI_FACTOR) cnt->mostly_read++; else if (ent->written >= ent->read * BIDI_FACTOR) cnt->mostly_written++; else cnt->both_read_and_written++; } /** * Count all the connection information we've received during the current * period in 'bidimap', and store that information in the appropriate count * structures. **/ static void collect_period_statistics(void) { bidi_map_entry_t **ptr, **next, *ent; for (ptr = HT_START(bidimap, &bidi_map); ptr; ptr = next) { ent = *ptr; add_entry_to_count(&counts, ent); if (ent->is_ipv6) add_entry_to_count(&counts_ipv6, ent); next = HT_NEXT_RMV(bidimap, &bidi_map, ptr); tor_free(ent); } log_info(LD_GENERAL, "%d below threshold, %d mostly read, " "%d mostly written, %d both read and written.", counts.below_threshold, counts.mostly_read, counts.mostly_written, counts.both_read_and_written); } /** 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 conn_stats_note_or_conn_bytes(uint64_t conn_id, size_t num_read, size_t num_written, time_t when, bool is_ipv6) { 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) { collect_period_statistics(); while (when >= bidi_next_interval) bidi_next_interval += BIDI_INTERVAL; } /* 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; entry->is_ipv6 |= is_ipv6; } else { entry = tor_malloc_zero(sizeof(bidi_map_entry_t)); entry->conn_id = conn_id; entry->written = num_written; entry->read = num_read; entry->is_ipv6 = is_ipv6; HT_INSERT(bidimap, &bidi_map, entry); } } } /** Return a newly allocated string containing the connection statistics * until now, or NULL if we're not collecting conn stats. Caller must * ensure start_of_conn_stats_interval is in the past. */ char * conn_stats_format(time_t now) { char *result, written_at[ISO_TIME_LEN+1]; if (!start_of_conn_stats_interval) return NULL; /* Not initialized. */ tor_assert(now >= start_of_conn_stats_interval); format_iso_time(written_at, now); tor_asprintf(&result, "conn-bi-direct %s (%d s) " "%"PRIu32",%"PRIu32",%"PRIu32",%"PRIu32"\n" "ipv6-conn-bi-direct %s (%d s) " "%"PRIu32",%"PRIu32",%"PRIu32",%"PRIu32"\n", written_at, (unsigned) (now - start_of_conn_stats_interval), counts.below_threshold, counts.mostly_read, counts.mostly_written, counts.both_read_and_written, written_at, (unsigned) (now - start_of_conn_stats_interval), counts_ipv6.below_threshold, counts_ipv6.mostly_read, counts_ipv6.mostly_written, counts_ipv6.both_read_and_written); return result; } /** If 24 hours have passed since the beginning of the current conn stats * period, write conn stats to $DATADIR/stats/conn-stats (possibly * overwriting an existing file) and reset counters. Return when we would * next want to write conn stats or 0 if we never want to write. */ time_t conn_stats_save(time_t now) { char *str = NULL; 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 */ /* Generate history string. */ str = conn_stats_format(now); /* Reset counters. */ conn_stats_reset(now); /* Try to write to disk. */ if (!check_or_create_data_subdir("stats")) { write_to_data_subdir("stats", "conn-stats", str, "connection statistics"); } done: tor_free(str); return start_of_conn_stats_interval + WRITE_STATS_INTERVAL; }