From c6732a03cbf0efa815e8a857af8118878c053af0 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Thu, 5 Nov 2026 18:18:45 +0100 Subject: Answer blacklisted torrent announces with an empty packet, that carries a re-announce timeout --- src/ot_udp.c | 8 +++++--- src/trackerlogic.c | 6 ++++++ src/trackerlogic.h | 3 +++ tests/integration/test_accesslist.py | 18 +++++++++++------- 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) { ws->reply = ws->outbuf + 8; ws->reply_size = add_peer_to_torrent_and_return_peers(FLAG_UDP, ws, numwant); + /* The announce path answers a refusal itself, so nothing is left here + but a tracker that could not get the memory to track the torrent */ if (!ws->reply_size) { - const char err_unauth[] = "Requested download is not authorized for use with this tracker."; - size_t s = sizeof(err_unauth); + const char err_resources[] = "Tracker is out of resources, try again later."; + size_t s = sizeof(err_resources); outpacket[0] = htonl(3); - memcpy(&outpacket[2], &err_unauth[0], s); + memcpy(&outpacket[2], &err_resources[0], s); socket_send(serversocket, ws->outbuf, 8 + s, remoteip, remoteport); return 1; } 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 memcpy(ws->reply, invalid_hash, strlen(invalid_hash)); return strlen(invalid_hash); } + if (proto == FLAG_UDP) { + ((uint32_t *)ws->reply)[0] = htonl(OT_CLIENT_REJECT_INTERVAL_RANDOM); + ((uint32_t *)ws->reply)[1] = 0; + ((uint32_t *)ws->reply)[2] = 0; + return 12; + } return 0; } 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 { #define OT_CLIENT_TIMEOUT_SEND (60 * 15) #define OT_CLIENT_REQUEST_INTERVAL (60 * 30) #define OT_CLIENT_REQUEST_VARIATION (60 * 6) +#define OT_CLIENT_REJECT_INTERVAL (60 * 120) #define OT_TORRENT_TIMEOUT_HOURS 24 #define OT_TORRENT_TIMEOUT (60 * OT_TORRENT_TIMEOUT_HOURS) #define OT_CLIENT_REQUEST_INTERVAL_RANDOM \ (OT_CLIENT_REQUEST_INTERVAL - OT_CLIENT_REQUEST_VARIATION / 2 + (int)(nrand48(ws->rand48_state) % OT_CLIENT_REQUEST_VARIATION)) +#define OT_CLIENT_REJECT_INTERVAL_RANDOM \ + (OT_CLIENT_REJECT_INTERVAL - OT_CLIENT_REQUEST_VARIATION / 2 + (int)(nrand48(ws->rand48_state) % OT_CLIENT_REQUEST_VARIATION)) /* If WANT_MODEST_FULLSCRAPES is on, ip addresses may not 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): self.assertNotIn(b"failure reason", answer) self.assertEqual(answer[b"complete"], 1) - def test_the_block_is_an_error_packet_over_udp(self): - """BEP 15 has an error action, which is what a refusal should use. - It used to answer the announce header with no body behind it, a - packet too short for any client to read.""" + def test_the_block_is_an_empty_announce_over_udp(self): + """An empty swarm with a long interval, not an error. BEP 15 binds a + client to the interval and says nothing about what to do with an + error, so an error puts the client on its own retry schedule, which + starts in the tens of seconds.""" sock, connection_id = self.tracker.udp_connect() sock.close() request = struct.pack( @@ -49,10 +50,13 @@ class Blacklist(TrackerTestCase): ) answer = self.tracker.udp_raw(request) self.assertIsNotNone(answer) - action, transaction = struct.unpack(">II", answer[:8]) - self.assertEqual(action, 3, "action 3 is the error packet") + self.assertEqual(len(answer), 20, "header and counts, no peers behind them") + action, transaction, interval, leechers, seeders = struct.unpack(">IIIII", answer) + self.assertEqual(action, 1, "action 1 is the announce packet") self.assertEqual(transaction, 7) - self.assertIn(b"not authorized", answer[8:]) + self.assertEqual((leechers, seeders), (0, 0)) + self.assertGreaterEqual(interval, 60 * 60, "far enough out to be worth the trouble") + self.assertLessEqual(interval, 60 * 60 * 6, "not so far out that a client clamps it") def test_a_blocked_hash_is_not_stored_when_it_arrives_over_udp(self): sock, connection_id = self.tracker.udp_connect() -- cgit v1.3