/* This software was written by Dirk Engling It is considered beerware. Prost. Skol. Cheers or whatever. */ #include #include #include #include #include "ot_clean.h" #include "ot_mutex.h" #include "trackerlogic.h" #include "test.h" /* A clock stepped backwards used to leave the torrent's base in the future, so the span the cleaner works from came out negative. It was then added to a peer's age and stored in the single byte that age lives in, where -1 reads as 255 and the next pass threw the whole swarm away. */ static void announce(struct ot_workstruct *ws, ot_hash *hash, uint8_t last_octet, int seeding) { static const uint8_t v4mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; /* The peer list is written into the reply, which workstruct_init leaves for its caller to point somewhere */ ws->reply = ws->outbuf; memset(&ws->peer, 0, sizeof(ws->peer)); memcpy(&ws->peer, v4mapped, sizeof(v4mapped)); ((uint8_t *)&ws->peer)[15] = last_octet; OT_SETPORT(&ws->peer, &(uint16_t){6881}); if (seeding) OT_PEERFLAG(ws->peer) |= PEER_FLAG_SEEDING; ws->hash = hash; add_peer_to_torrent_and_return_peers(FLAG_TCP, ws, 0); } /* A distinct v4 peer addressed by two octets, to build swarms past 256 */ static size_t announce_wide(struct ot_workstruct *ws, ot_hash *hash, unsigned int which, size_t amount) { static const uint8_t v4mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; ws->reply = ws->outbuf; memset(&ws->peer, 0, sizeof(ws->peer)); memcpy(&ws->peer, v4mapped, sizeof(v4mapped)); ((uint8_t *)&ws->peer)[14] = (uint8_t)(which >> 8); ((uint8_t *)&ws->peer)[15] = (uint8_t)which; OT_SETPORT(&ws->peer, &(uint16_t){6881}); ws->hash = hash; return add_peer_to_torrent_and_return_peers(FLAG_TCP, ws, amount); } static size_t peers_in(ot_hash *hash) { char reply[1024]; size_t len = return_udp_scrape_for_torrent(*hash, reply); uint32_t seeders, leechers; if (len < 12) return 0; memcpy(&seeders, reply, 4); memcpy(&leechers, reply + 8, 4); return (size_t)ntohl(seeders) + (size_t)ntohl(leechers); } static ot_torrent *torrent_of(ot_hash *hash) { ot_vector *bucket = mutex_bucket_lock_by_hash(*hash); ot_torrent *t = (ot_torrent *)bucket->data; ot_torrent *found = NULL; size_t i; for (i = 0; i < bucket->size; ++i) if (!memcmp(t[i].hash, hash, sizeof(ot_hash))) found = t + i; mutex_bucket_unlock_by_hash(*hash, 0); return found; } /* What a clock stepped back by `minutes` leaves behind */ static void step_clock_back(ot_torrent *torrent, int minutes) { torrent->base += minutes; } /* What the next minute of a well behaved clock looks like */ static void age_by_a_minute(ot_torrent *torrent) { torrent->base -= 1; } static void case_a_clock_that_went_back_keeps_the_swarm(void) { struct ot_workstruct ws; ot_hash hash; ot_torrent *torrent; test_case("a swarm survives the clock being stepped backwards"); memset(hash, 0x31, sizeof(hash)); CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); announce(&ws, &hash, 1, 1); announce(&ws, &hash, 2, 0); CHECK_INT(peers_in(&hash), 2); torrent = torrent_of(&hash); CHECK(torrent != NULL); step_clock_back(torrent, 5); /* The pass that sees the step keeps everybody either way: unclamped it writes a negative span into each peer's age byte, where it reads as 251 rather than as a removal. */ CHECK_INT(clean_single_torrent(torrent), 0); CHECK_INT(peers_in(&hash), 2); /* A minute later, and this is where a poisoned age byte took the swarm: 251 plus one minute is past any timeout. */ age_by_a_minute(torrent); CHECK_INT(clean_single_torrent(torrent), 0); CHECK_INT(peers_in(&hash), 2); workstruct_deinit(&ws); } static void case_an_idle_swarm_still_ages_out(void) { struct ot_workstruct ws; ot_hash hash; ot_torrent *torrent; test_case("a swarm nobody touched is still cleaned away"); memset(hash, 0x32, sizeof(hash)); CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); announce(&ws, &hash, 3, 1); CHECK_INT(peers_in(&hash), 1); torrent = torrent_of(&hash); CHECK(torrent != NULL); step_clock_back(torrent, -OT_PEER_TIMEOUT); CHECK_INT(clean_single_torrent(torrent), 0); CHECK_INT(peers_in(&hash), 0); workstruct_deinit(&ws); } static void case_empty_torrent_timeout_bounds_a_counter_only_torrent(void) { struct ot_workstruct ws; ot_hash hash; ot_torrent *torrent; ot_time saved = g_empty_torrent_timeout; test_case("the empty torrent timeout bounds a counter-only torrent's life"); memset(hash, 0x33, sizeof(hash)); CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); announce(&ws, &hash, 7, 1); torrent = torrent_of(&hash); CHECK(torrent != NULL); torrent->down_count = 1; /* one completed download to remember it by */ /* Age the only peer out: the torrent falls empty but keeps its count */ step_clock_back(torrent, -OT_PEER_TIMEOUT); CHECK_INT(clean_single_torrent(torrent), 0); CHECK_INT(peers_in(&hash), 0); /* Carry it a little past a short empty-torrent timeout */ step_clock_back(torrent, -10); /* The default, matching the torrent timeout, still holds it for the count */ g_empty_torrent_timeout = OT_TORRENT_TIMEOUT; CHECK_INT(clean_single_torrent(torrent), 0); /* A timeout it has now exceeded lets the next sweep drop it */ g_empty_torrent_timeout = OT_PEER_TIMEOUT + 5; CHECK_INT(clean_single_torrent(torrent), 1); g_empty_torrent_timeout = saved; workstruct_deinit(&ws); } static void case_a_bucketed_swarm_survives_clean_and_serves(void) { struct ot_workstruct ws; ot_hash hash; ot_torrent *torrent; unsigned int i; const unsigned int n = 600; /* past OT_PEER_BUCKET_MINCOUNT (512) */ test_case("a swarm large enough to be bucketed cleans, splits and serves without loss"); memset(hash, 0x34, sizeof(hash)); CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); for (i = 0; i < n; ++i) announce_wide(&ws, &hash, i, 0); CHECK_INT(peers_in(&hash), n); torrent = torrent_of(&hash); CHECK(torrent != NULL); CHECK(torrent->peer_list != NULL); /* A clean with a minute elapsed splits the flat vector into buckets */ step_clock_back(torrent, -1); CHECK_INT(clean_single_torrent(torrent), 0); CHECK(torrent->peer_list != NULL); CHECK_INT(OT_VECTOR_HASBUCKETS(&torrent->peer_list->peers4), 1); CHECK_INT(peers_in(&hash), n); /* Serve a selection back, which walks the bucket list */ CHECK(announce_wide(&ws, &hash, 1, 200) > 0); CHECK_INT(peers_in(&hash), n); workstruct_deinit(&ws); } void test_clean(void) { sigset_t hup; /* accesslist_deinit sends SIGHUP to its reader, trackerlogic_init starts the accesslist, and the test process must not take it */ sigemptyset(&hup); sigaddset(&hup, SIGHUP); pthread_sigmask(SIG_BLOCK, &hup, NULL); trackerlogic_init(); case_a_clock_that_went_back_keeps_the_swarm(); case_an_idle_swarm_still_ages_out(); case_empty_torrent_timeout_bounds_a_counter_only_torrent(); case_a_bucketed_swarm_survives_clean_and_serves(); trackerlogic_deinit(); }