Skip to content

Commit 481d413

Browse files
committed
feat: [#569] allow UDP clients to limit peers in response
The UDP tracker announce response always include all peers available up to a maxium of 74 peers, ignoring the `num_want` param in the request described in: https://www.bittorrent.org/beps/bep_0015.html This change applies that limit only when is lower than then TORRENT_PEERS_LIMIT (74).
1 parent 1e437f7 commit 481d413

File tree

5 files changed

+167
-30
lines changed

5 files changed

+167
-30
lines changed

src/core/mod.rs

Lines changed: 157 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ pub mod torrent;
448448

449449
pub mod peer_tests;
450450

451+
use std::cmp::max;
451452
use std::collections::HashMap;
452453
use std::net::IpAddr;
453454
use std::panic::Location;
@@ -520,6 +521,38 @@ pub struct AnnounceData {
520521
pub policy: AnnouncePolicy,
521522
}
522523

524+
/// How many peers the peer announcing wants in the announce response.
525+
#[derive(Clone, Debug, PartialEq, Default)]
526+
pub enum PeersWanted {
527+
/// The peer wants as many peers as possible in the announce response.
528+
#[default]
529+
All,
530+
/// The peer only wants a certain amount of peers in the announce response.
531+
Only { amount: usize },
532+
}
533+
534+
impl PeersWanted {
535+
fn limit(&self) -> usize {
536+
match self {
537+
PeersWanted::All => TORRENT_PEERS_LIMIT,
538+
PeersWanted::Only { amount } => *amount,
539+
}
540+
}
541+
}
542+
543+
impl From<i32> for PeersWanted {
544+
fn from(value: i32) -> Self {
545+
if value > 0 {
546+
match value.try_into() {
547+
Ok(peers_wanted) => Self::Only { amount: peers_wanted },
548+
Err(_) => Self::All,
549+
}
550+
} else {
551+
Self::All
552+
}
553+
}
554+
}
555+
523556
/// Structure that holds the data returned by the `scrape` request.
524557
#[derive(Debug, PartialEq, Default)]
525558
pub struct ScrapeData {
@@ -639,7 +672,13 @@ impl Tracker {
639672
/// # Context: Tracker
640673
///
641674
/// BEP 03: [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html).
642-
pub fn announce(&self, info_hash: &InfoHash, peer: &mut peer::Peer, remote_client_ip: &IpAddr) -> AnnounceData {
675+
pub fn announce(
676+
&self,
677+
info_hash: &InfoHash,
678+
peer: &mut peer::Peer,
679+
remote_client_ip: &IpAddr,
680+
peers_wanted: &PeersWanted,
681+
) -> AnnounceData {
643682
// code-review: maybe instead of mutating the peer we could just return
644683
// a tuple with the new peer and the announce data: (Peer, AnnounceData).
645684
// It could even be a different struct: `StoredPeer` or `PublicPeer`.
@@ -661,7 +700,7 @@ impl Tracker {
661700

662701
let stats = self.upsert_peer_and_get_stats(info_hash, peer);
663702

664-
let peers = self.get_peers_for(info_hash, peer);
703+
let peers = self.get_peers_for(info_hash, peer, peers_wanted.limit());
665704

666705
AnnounceData {
667706
peers,
@@ -713,16 +752,21 @@ impl Tracker {
713752
Ok(())
714753
}
715754

716-
fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer) -> Vec<Arc<peer::Peer>> {
755+
/// # Context: Tracker
756+
///
757+
/// Get torrent peers for a given torrent and client.
758+
///
759+
/// It filters out the client making the request.
760+
fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer, limit: usize) -> Vec<Arc<peer::Peer>> {
717761
match self.torrents.get(info_hash) {
718762
None => vec![],
719-
Some(entry) => entry.get_peers_for_client(&peer.peer_addr, Some(TORRENT_PEERS_LIMIT)),
763+
Some(entry) => entry.get_peers_for_client(&peer.peer_addr, Some(max(limit, TORRENT_PEERS_LIMIT))),
720764
}
721765
}
722766

723767
/// # Context: Tracker
724768
///
725-
/// Get all torrent peers for a given torrent
769+
/// Get torrent peers for a given torrent.
726770
pub fn get_torrent_peers(&self, info_hash: &InfoHash) -> Vec<Arc<peer::Peer>> {
727771
match self.torrents.get(info_hash) {
728772
None => vec![],
@@ -1199,6 +1243,7 @@ mod tests {
11991243
use std::sync::Arc;
12001244

12011245
use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId};
1246+
use torrust_tracker_configuration::TORRENT_PEERS_LIMIT;
12021247
use torrust_tracker_primitives::info_hash::InfoHash;
12031248
use torrust_tracker_primitives::DurationSinceUnixEpoch;
12041249
use torrust_tracker_test_helpers::configuration;
@@ -1328,7 +1373,7 @@ mod tests {
13281373
}
13291374

13301375
#[tokio::test]
1331-
async fn it_should_return_all_the_peers_for_a_given_torrent() {
1376+
async fn it_should_return_the_peers_for_a_given_torrent() {
13321377
let tracker = public_tracker();
13331378

13341379
let info_hash = sample_info_hash();
@@ -1341,20 +1386,93 @@ mod tests {
13411386
assert_eq!(peers, vec![Arc::new(peer)]);
13421387
}
13431388

1389+
/// It generates a peer id from a number where the number is the last
1390+
/// part of the peer ID. For example, for `12` it returns
1391+
/// `-qB00000000000000012`.
1392+
fn numeric_peer_id(two_digits_value: i32) -> PeerId {
1393+
// Format idx as a string with leading zeros, ensuring it has exactly 2 digits
1394+
let idx_str = format!("{two_digits_value:02}");
1395+
1396+
// Create the base part of the peer ID.
1397+
let base = b"-qB00000000000000000";
1398+
1399+
// Concatenate the base with idx bytes, ensuring the total length is 20 bytes.
1400+
let mut peer_id_bytes = [0u8; 20];
1401+
peer_id_bytes[..base.len()].copy_from_slice(base);
1402+
peer_id_bytes[base.len() - idx_str.len()..].copy_from_slice(idx_str.as_bytes());
1403+
1404+
PeerId(peer_id_bytes)
1405+
}
1406+
13441407
#[tokio::test]
1345-
async fn it_should_return_all_the_peers_for_a_given_torrent_excluding_a_given_peer() {
1408+
async fn it_should_return_74_peers_at_the_most_for_a_given_torrent() {
1409+
let tracker = public_tracker();
1410+
1411+
let info_hash = sample_info_hash();
1412+
1413+
for idx in 1..=75 {
1414+
let peer = Peer {
1415+
peer_id: numeric_peer_id(idx),
1416+
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, idx.try_into().unwrap())), 8080),
1417+
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
1418+
uploaded: NumberOfBytes::new(0),
1419+
downloaded: NumberOfBytes::new(0),
1420+
left: NumberOfBytes::new(0), // No bytes left to download
1421+
event: AnnounceEvent::Completed,
1422+
};
1423+
1424+
tracker.upsert_peer_and_get_stats(&info_hash, &peer);
1425+
}
1426+
1427+
let peers = tracker.get_torrent_peers(&info_hash);
1428+
1429+
assert_eq!(peers.len(), 74);
1430+
}
1431+
1432+
#[tokio::test]
1433+
async fn it_should_return_the_peers_for_a_given_torrent_excluding_a_given_peer() {
13461434
let tracker = public_tracker();
13471435

13481436
let info_hash = sample_info_hash();
13491437
let peer = sample_peer();
13501438

13511439
tracker.upsert_peer_and_get_stats(&info_hash, &peer);
13521440

1353-
let peers = tracker.get_peers_for(&info_hash, &peer);
1441+
let peers = tracker.get_peers_for(&info_hash, &peer, TORRENT_PEERS_LIMIT);
13541442

13551443
assert_eq!(peers, vec![]);
13561444
}
13571445

1446+
#[tokio::test]
1447+
async fn it_should_return_74_peers_at_the_most_for_a_given_torrent_when_it_filters_out_a_given_peer() {
1448+
let tracker = public_tracker();
1449+
1450+
let info_hash = sample_info_hash();
1451+
1452+
let excluded_peer = sample_peer();
1453+
1454+
tracker.upsert_peer_and_get_stats(&info_hash, &excluded_peer);
1455+
1456+
// Add 74 peers
1457+
for idx in 2..=75 {
1458+
let peer = Peer {
1459+
peer_id: numeric_peer_id(idx),
1460+
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, idx.try_into().unwrap())), 8080),
1461+
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
1462+
uploaded: NumberOfBytes::new(0),
1463+
downloaded: NumberOfBytes::new(0),
1464+
left: NumberOfBytes::new(0), // No bytes left to download
1465+
event: AnnounceEvent::Completed,
1466+
};
1467+
1468+
tracker.upsert_peer_and_get_stats(&info_hash, &peer);
1469+
}
1470+
1471+
let peers = tracker.get_peers_for(&info_hash, &excluded_peer, TORRENT_PEERS_LIMIT);
1472+
1473+
assert_eq!(peers.len(), 74);
1474+
}
1475+
13581476
#[tokio::test]
13591477
async fn it_should_return_the_torrent_metrics() {
13601478
let tracker = public_tracker();
@@ -1409,6 +1527,7 @@ mod tests {
14091527
use crate::core::tests::the_tracker::{
14101528
peer_ip, public_tracker, sample_info_hash, sample_peer, sample_peer_1, sample_peer_2,
14111529
};
1530+
use crate::core::PeersWanted;
14121531

14131532
mod should_assign_the_ip_to_the_peer {
14141533

@@ -1514,7 +1633,7 @@ mod tests {
15141633

15151634
let mut peer = sample_peer();
15161635

1517-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1636+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15181637

15191638
assert_eq!(announce_data.peers, vec![]);
15201639
}
@@ -1524,10 +1643,15 @@ mod tests {
15241643
let tracker = public_tracker();
15251644

15261645
let mut previously_announced_peer = sample_peer_1();
1527-
tracker.announce(&sample_info_hash(), &mut previously_announced_peer, &peer_ip());
1646+
tracker.announce(
1647+
&sample_info_hash(),
1648+
&mut previously_announced_peer,
1649+
&peer_ip(),
1650+
&PeersWanted::All,
1651+
);
15281652

15291653
let mut peer = sample_peer_2();
1530-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1654+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15311655

15321656
assert_eq!(announce_data.peers, vec![Arc::new(previously_announced_peer)]);
15331657
}
@@ -1537,14 +1661,15 @@ mod tests {
15371661
use crate::core::tests::the_tracker::{
15381662
completed_peer, leecher, peer_ip, public_tracker, sample_info_hash, seeder, started_peer,
15391663
};
1664+
use crate::core::PeersWanted;
15401665

15411666
#[tokio::test]
15421667
async fn when_the_peer_is_a_seeder() {
15431668
let tracker = public_tracker();
15441669

15451670
let mut peer = seeder();
15461671

1547-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1672+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15481673

15491674
assert_eq!(announce_data.stats.complete, 1);
15501675
}
@@ -1555,7 +1680,7 @@ mod tests {
15551680

15561681
let mut peer = leecher();
15571682

1558-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1683+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15591684

15601685
assert_eq!(announce_data.stats.incomplete, 1);
15611686
}
@@ -1566,10 +1691,11 @@ mod tests {
15661691

15671692
// We have to announce with "started" event because peer does not count if peer was not previously known
15681693
let mut started_peer = started_peer();
1569-
tracker.announce(&sample_info_hash(), &mut started_peer, &peer_ip());
1694+
tracker.announce(&sample_info_hash(), &mut started_peer, &peer_ip(), &PeersWanted::All);
15701695

15711696
let mut completed_peer = completed_peer();
1572-
let announce_data = tracker.announce(&sample_info_hash(), &mut completed_peer, &peer_ip());
1697+
let announce_data =
1698+
tracker.announce(&sample_info_hash(), &mut completed_peer, &peer_ip(), &PeersWanted::All);
15731699

15741700
assert_eq!(announce_data.stats.downloaded, 1);
15751701
}
@@ -1583,7 +1709,7 @@ mod tests {
15831709
use torrust_tracker_primitives::info_hash::InfoHash;
15841710

15851711
use crate::core::tests::the_tracker::{complete_peer, incomplete_peer, public_tracker};
1586-
use crate::core::{ScrapeData, SwarmMetadata};
1712+
use crate::core::{PeersWanted, ScrapeData, SwarmMetadata};
15871713

15881714
#[tokio::test]
15891715
async fn it_should_return_a_zeroed_swarm_metadata_for_the_requested_file_if_the_tracker_does_not_have_that_torrent(
@@ -1609,11 +1735,21 @@ mod tests {
16091735

16101736
// Announce a "complete" peer for the torrent
16111737
let mut complete_peer = complete_peer();
1612-
tracker.announce(&info_hash, &mut complete_peer, &IpAddr::V4(Ipv4Addr::new(126, 0, 0, 10)));
1738+
tracker.announce(
1739+
&info_hash,
1740+
&mut complete_peer,
1741+
&IpAddr::V4(Ipv4Addr::new(126, 0, 0, 10)),
1742+
&PeersWanted::All,
1743+
);
16131744

16141745
// Announce an "incomplete" peer for the torrent
16151746
let mut incomplete_peer = incomplete_peer();
1616-
tracker.announce(&info_hash, &mut incomplete_peer, &IpAddr::V4(Ipv4Addr::new(126, 0, 0, 11)));
1747+
tracker.announce(
1748+
&info_hash,
1749+
&mut incomplete_peer,
1750+
&IpAddr::V4(Ipv4Addr::new(126, 0, 0, 11)),
1751+
&PeersWanted::All,
1752+
);
16171753

16181754
// Scrape
16191755
let scrape_data = tracker.scrape(&vec![info_hash]).await;
@@ -1740,7 +1876,7 @@ mod tests {
17401876
use crate::core::tests::the_tracker::{
17411877
complete_peer, incomplete_peer, peer_ip, sample_info_hash, whitelisted_tracker,
17421878
};
1743-
use crate::core::ScrapeData;
1879+
use crate::core::{PeersWanted, ScrapeData};
17441880

17451881
#[test]
17461882
fn it_should_be_able_to_build_a_zeroed_scrape_data_for_a_list_of_info_hashes() {
@@ -1761,11 +1897,11 @@ mod tests {
17611897
let info_hash = "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap();
17621898

17631899
let mut peer = incomplete_peer();
1764-
tracker.announce(&info_hash, &mut peer, &peer_ip());
1900+
tracker.announce(&info_hash, &mut peer, &peer_ip(), &PeersWanted::All);
17651901

17661902
// Announce twice to force non zeroed swarm metadata
17671903
let mut peer = complete_peer();
1768-
tracker.announce(&info_hash, &mut peer, &peer_ip());
1904+
tracker.announce(&info_hash, &mut peer, &peer_ip(), &PeersWanted::All);
17691905

17701906
let scrape_data = tracker.scrape(&vec![info_hash]).await;
17711907

src/servers/http/v1/services/announce.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::sync::Arc;
1414
use torrust_tracker_primitives::info_hash::InfoHash;
1515
use torrust_tracker_primitives::peer;
1616

17-
use crate::core::{statistics, AnnounceData, Tracker};
17+
use crate::core::{statistics, AnnounceData, PeersWanted, Tracker};
1818

1919
/// The HTTP tracker `announce` service.
2020
///
@@ -30,7 +30,7 @@ pub async fn invoke(tracker: Arc<Tracker>, info_hash: InfoHash, peer: &mut peer:
3030
let original_peer_ip = peer.peer_addr.ip();
3131

3232
// The tracker could change the original peer ip
33-
let announce_data = tracker.announce(&info_hash, peer, &original_peer_ip);
33+
let announce_data = tracker.announce(&info_hash, peer, &original_peer_ip, &PeersWanted::All);
3434

3535
match original_peer_ip {
3636
IpAddr::V4(_) => {

src/servers/http/v1/services/scrape.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ mod tests {
103103
use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
104104
use torrust_tracker_test_helpers::configuration;
105105

106-
use crate::core::{statistics, ScrapeData, Tracker};
106+
use crate::core::{statistics, PeersWanted, ScrapeData, Tracker};
107107
use crate::servers::http::v1::services::scrape::invoke;
108108
use crate::servers::http::v1::services::scrape::tests::{
109109
public_tracker, sample_info_hash, sample_info_hashes, sample_peer,
@@ -119,7 +119,7 @@ mod tests {
119119
// Announce a new peer to force scrape data to contain not zeroed data
120120
let mut peer = sample_peer();
121121
let original_peer_ip = peer.ip();
122-
tracker.announce(&info_hash, &mut peer, &original_peer_ip);
122+
tracker.announce(&info_hash, &mut peer, &original_peer_ip, &PeersWanted::All);
123123

124124
let scrape_data = invoke(&tracker, &info_hashes, &original_peer_ip).await;
125125

@@ -194,7 +194,7 @@ mod tests {
194194
use mockall::predicate::eq;
195195
use torrust_tracker_test_helpers::configuration;
196196

197-
use crate::core::{statistics, ScrapeData, Tracker};
197+
use crate::core::{statistics, PeersWanted, ScrapeData, Tracker};
198198
use crate::servers::http::v1::services::scrape::fake;
199199
use crate::servers::http::v1::services::scrape::tests::{
200200
public_tracker, sample_info_hash, sample_info_hashes, sample_peer,
@@ -210,7 +210,7 @@ mod tests {
210210
// Announce a new peer to force scrape data to contain not zeroed data
211211
let mut peer = sample_peer();
212212
let original_peer_ip = peer.ip();
213-
tracker.announce(&info_hash, &mut peer, &original_peer_ip);
213+
tracker.announce(&info_hash, &mut peer, &original_peer_ip, &PeersWanted::All);
214214

215215
let scrape_data = fake(&tracker, &info_hashes, &original_peer_ip).await;
216216

0 commit comments

Comments
 (0)