summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-11-05 18:18:45 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-11-05 18:18:45 +0100
commitc6732a03cbf0efa815e8a857af8118878c053af0 (patch)
tree6e774fd381c845f38de21f9abbfe1debe53dc56d
parent194fc0b2f7aa37ca25a9ea058eac699b2824e95a (diff)
Answer blacklisted torrent announces with an empty packet, that carries a re-announce timeout
-rw-r--r--src/ot_udp.c8
-rw-r--r--src/trackerlogic.c6
-rw-r--r--src/trackerlogic.h3
-rw-r--r--tests/integration/test_accesslist.py18
4 files changed, 25 insertions, 10 deletions
diff --git a/src/ot_udp.c b/src/ot_udp.c
index e67ca1f..9e8e3c4 100644
--- a/src/ot_udp.c
+++ b/src/ot_udp.c
@@ -176,11 +176,13 @@ int handle_udp6(int64_t serversocket, struct ot_workstruct *ws) {
176 ws->reply = ws->outbuf + 8; 176 ws->reply = ws->outbuf + 8;
177 ws->reply_size = add_peer_to_torrent_and_return_peers(FLAG_UDP, ws, numwant); 177 ws->reply_size = add_peer_to_torrent_and_return_peers(FLAG_UDP, ws, numwant);
178 178
179 /* The announce path answers a refusal itself, so nothing is left here
180 but a tracker that could not get the memory to track the torrent */
179 if (!ws->reply_size) { 181 if (!ws->reply_size) {
180 const char err_unauth[] = "Requested download is not authorized for use with this tracker."; 182 const char err_resources[] = "Tracker is out of resources, try again later.";
181 size_t s = sizeof(err_unauth); 183 size_t s = sizeof(err_resources);
182 outpacket[0] = htonl(3); 184 outpacket[0] = htonl(3);
183 memcpy(&outpacket[2], &err_unauth[0], s); 185 memcpy(&outpacket[2], &err_resources[0], s);
184 socket_send(serversocket, ws->outbuf, 8 + s, remoteip, remoteport); 186 socket_send(serversocket, ws->outbuf, 8 + s, remoteip, remoteport);
185 return 1; 187 return 1;
186 } 188 }
diff --git a/src/trackerlogic.c b/src/trackerlogic.c
index 7e8d969..2313379 100644
--- a/src/trackerlogic.c
+++ b/src/trackerlogic.c
@@ -94,6 +94,12 @@ size_t add_peer_to_torrent_and_return_peers(PROTO_FLAG proto, struct ot_workstru
94 memcpy(ws->reply, invalid_hash, strlen(invalid_hash)); 94 memcpy(ws->reply, invalid_hash, strlen(invalid_hash));
95 return strlen(invalid_hash); 95 return strlen(invalid_hash);
96 } 96 }
97 if (proto == FLAG_UDP) {
98 ((uint32_t *)ws->reply)[0] = htonl(OT_CLIENT_REJECT_INTERVAL_RANDOM);
99 ((uint32_t *)ws->reply)[1] = 0;
100 ((uint32_t *)ws->reply)[2] = 0;
101 return 12;
102 }
97 return 0; 103 return 0;
98 } 104 }
99 105
diff --git a/src/trackerlogic.h b/src/trackerlogic.h
index f6aa8cc..4f970ba 100644
--- a/src/trackerlogic.h
+++ b/src/trackerlogic.h
@@ -39,12 +39,15 @@ typedef struct {
39#define OT_CLIENT_TIMEOUT_SEND (60 * 15) 39#define OT_CLIENT_TIMEOUT_SEND (60 * 15)
40#define OT_CLIENT_REQUEST_INTERVAL (60 * 30) 40#define OT_CLIENT_REQUEST_INTERVAL (60 * 30)
41#define OT_CLIENT_REQUEST_VARIATION (60 * 6) 41#define OT_CLIENT_REQUEST_VARIATION (60 * 6)
42#define OT_CLIENT_REJECT_INTERVAL (60 * 120)
42 43
43#define OT_TORRENT_TIMEOUT_HOURS 24 44#define OT_TORRENT_TIMEOUT_HOURS 24
44#define OT_TORRENT_TIMEOUT (60 * OT_TORRENT_TIMEOUT_HOURS) 45#define OT_TORRENT_TIMEOUT (60 * OT_TORRENT_TIMEOUT_HOURS)
45 46
46#define OT_CLIENT_REQUEST_INTERVAL_RANDOM \ 47#define OT_CLIENT_REQUEST_INTERVAL_RANDOM \
47 (OT_CLIENT_REQUEST_INTERVAL - OT_CLIENT_REQUEST_VARIATION / 2 + (int)(nrand48(ws->rand48_state) % OT_CLIENT_REQUEST_VARIATION)) 48 (OT_CLIENT_REQUEST_INTERVAL - OT_CLIENT_REQUEST_VARIATION / 2 + (int)(nrand48(ws->rand48_state) % OT_CLIENT_REQUEST_VARIATION))
49#define OT_CLIENT_REJECT_INTERVAL_RANDOM \
50 (OT_CLIENT_REJECT_INTERVAL - OT_CLIENT_REQUEST_VARIATION / 2 + (int)(nrand48(ws->rand48_state) % OT_CLIENT_REQUEST_VARIATION))
48 51
49/* If WANT_MODEST_FULLSCRAPES is on, ip addresses may not 52/* If WANT_MODEST_FULLSCRAPES is on, ip addresses may not
50 fullscrape more frequently than this amount in seconds */ 53 fullscrape more frequently than this amount in seconds */
diff --git a/tests/integration/test_accesslist.py b/tests/integration/test_accesslist.py
index 1c84ccf..7d01264 100644
--- a/tests/integration/test_accesslist.py
+++ b/tests/integration/test_accesslist.py
@@ -38,10 +38,11 @@ class Blacklist(TrackerTestCase):
38 self.assertNotIn(b"failure reason", answer) 38 self.assertNotIn(b"failure reason", answer)
39 self.assertEqual(answer[b"complete"], 1) 39 self.assertEqual(answer[b"complete"], 1)
40 40
41 def test_the_block_is_an_error_packet_over_udp(self): 41 def test_the_block_is_an_empty_announce_over_udp(self):
42 """BEP 15 has an error action, which is what a refusal should use. 42 """An empty swarm with a long interval, not an error. BEP 15 binds a
43 It used to answer the announce header with no body behind it, a 43 client to the interval and says nothing about what to do with an
44 packet too short for any client to read.""" 44 error, so an error puts the client on its own retry schedule, which
45 starts in the tens of seconds."""
45 sock, connection_id = self.tracker.udp_connect() 46 sock, connection_id = self.tracker.udp_connect()
46 sock.close() 47 sock.close()
47 request = struct.pack( 48 request = struct.pack(
@@ -49,10 +50,13 @@ class Blacklist(TrackerTestCase):
49 ) 50 )
50 answer = self.tracker.udp_raw(request) 51 answer = self.tracker.udp_raw(request)
51 self.assertIsNotNone(answer) 52 self.assertIsNotNone(answer)
52 action, transaction = struct.unpack(">II", answer[:8]) 53 self.assertEqual(len(answer), 20, "header and counts, no peers behind them")
53 self.assertEqual(action, 3, "action 3 is the error packet") 54 action, transaction, interval, leechers, seeders = struct.unpack(">IIIII", answer)
55 self.assertEqual(action, 1, "action 1 is the announce packet")
54 self.assertEqual(transaction, 7) 56 self.assertEqual(transaction, 7)
55 self.assertIn(b"not authorized", answer[8:]) 57 self.assertEqual((leechers, seeders), (0, 0))
58 self.assertGreaterEqual(interval, 60 * 60, "far enough out to be worth the trouble")
59 self.assertLessEqual(interval, 60 * 60 * 6, "not so far out that a client clamps it")
56 60
57 def test_a_blocked_hash_is_not_stored_when_it_arrives_over_udp(self): 61 def test_a_blocked_hash_is_not_stored_when_it_arrives_over_udp(self):
58 sock, connection_id = self.tracker.udp_connect() 62 sock, connection_id = self.tracker.udp_connect()