1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
|
/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
It is considered beerware. Prost. Skol. Cheers or whatever. */
#include <arpa/inet.h>
#include <signal.h>
#include <pthread.h>
#include <string.h>
#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();
}
|