Skip to content

Commit 2882705

Browse files
committed
refactor: [torrust#1495] use SocketAddr as key for peers in Swarm
This change prevents duplicate peers with the same address but different IDs, ensuring more accurate peer tracking.
1 parent 15c14c5 commit 2882705

File tree

9 files changed

+109
-42
lines changed

9 files changed

+109
-42
lines changed

packages/axum-http-tracker-server/tests/server/asserts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub fn assert_bencoded_error(response_text: &String, expected_failure_reason: &s
2222
);
2323
}
2424

25+
#[allow(dead_code)]
2526
pub async fn assert_empty_announce_response(response: Response) {
2627
assert_eq!(response.status(), 200);
2728
let announce_response: Announce = serde_bencode::from_str(&response.text().await.unwrap()).unwrap();

packages/axum-http-tracker-server/tests/server/requests/announce.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ impl QueryBuilder {
126126
self
127127
}
128128

129+
pub fn with_port(mut self, port: u16) -> Self {
130+
self.announce_query.port = port;
131+
self
132+
}
133+
129134
pub fn without_compact(mut self) -> Self {
130135
self.announce_query.compact = None;
131136
self

packages/axum-http-tracker-server/tests/server/v1/contract.rs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ mod for_all_config_modes {
105105
use crate::common::fixtures::invalid_info_hashes;
106106
use crate::server::asserts::{
107107
assert_announce_response, assert_bad_announce_request_error_response, assert_cannot_parse_query_param_error_response,
108-
assert_cannot_parse_query_params_error_response, assert_compact_announce_response, assert_empty_announce_response,
109-
assert_is_announce_response, assert_missing_query_params_for_announce_request_error_response,
108+
assert_cannot_parse_query_params_error_response, assert_compact_announce_response, assert_is_announce_response,
109+
assert_missing_query_params_for_announce_request_error_response,
110110
};
111111
use crate::server::client::Client;
112112
use crate::server::requests::announce::{Compact, QueryBuilder};
@@ -559,27 +559,53 @@ mod for_all_config_modes {
559559
}
560560

561561
#[tokio::test]
562-
async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() {
562+
async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_socket_address_even_if_the_peer_id_is_different(
563+
) {
563564
logging::setup();
564565

565566
let env = Started::new(&configuration::ephemeral_public().into()).await;
566567

567568
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); // DevSkim: ignore DS173237
568569
let peer = PeerBuilder::default().build();
569570

570-
// Add a peer
571-
env.add_torrent_peer(&info_hash, &peer);
572-
573-
let announce_query = QueryBuilder::default()
571+
let announce_query_1 = QueryBuilder::default()
574572
.with_info_hash(&info_hash)
575573
.with_peer_id(&peer.peer_id)
574+
.with_peer_addr(&peer.peer_addr.ip())
575+
.with_port(peer.peer_addr.port())
576+
.query();
577+
578+
let announce_query_2 = QueryBuilder::default()
579+
.with_info_hash(&info_hash)
580+
.with_peer_id(&PeerId(*b"-qB00000000000000002")) // Different peer ID
581+
.with_peer_addr(&peer.peer_addr.ip())
582+
.with_port(peer.peer_addr.port())
576583
.query();
577584

578-
assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr);
585+
// Same peer socket address
586+
assert_eq!(announce_query_1.peer_addr, announce_query_2.peer_addr);
587+
assert_eq!(announce_query_1.port, announce_query_2.port);
588+
589+
// Different peer ID
590+
assert_ne!(announce_query_1.peer_id, announce_query_2.peer_id);
579591

580-
let response = Client::new(*env.bind_address()).announce(&announce_query).await;
592+
let _response = Client::new(*env.bind_address()).announce(&announce_query_1).await;
593+
let response = Client::new(*env.bind_address()).announce(&announce_query_2).await;
581594

582-
assert_empty_announce_response(response).await;
595+
let announce_policy = env.container.tracker_core_container.core_config.announce_policy;
596+
597+
// The response should contain only the first peer.
598+
assert_announce_response(
599+
response,
600+
&Announce {
601+
complete: 1,
602+
incomplete: 0,
603+
interval: announce_policy.interval,
604+
min_interval: announce_policy.interval_min,
605+
peers: vec![],
606+
},
607+
)
608+
.await;
583609

584610
env.stop().await;
585611
}

packages/torrent-repository/src/entry/swarm.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@ use std::collections::BTreeMap;
33
use std::net::SocketAddr;
44
use std::sync::Arc;
55

6-
use aquatic_udp_protocol::PeerId;
76
use torrust_tracker_primitives::peer::{self, Peer};
87
use torrust_tracker_primitives::DurationSinceUnixEpoch;
98

10-
// code-review: the current implementation uses the peer Id as the ``BTreeMap``
11-
// key. That would allow adding two identical peers except for the Id.
12-
// For example, two peers with the same socket address but a different peer Id
13-
// would be allowed. That would lead to duplicated peers in the tracker responses.
14-
159
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1610
pub struct Swarm {
17-
peers: BTreeMap<PeerId, Arc<Peer>>,
11+
peers: BTreeMap<SocketAddr, Arc<Peer>>,
1812
}
1913

2014
impl Swarm {
@@ -28,12 +22,12 @@ impl Swarm {
2822
self.peers.is_empty()
2923
}
3024

31-
pub fn upsert(&mut self, value: Arc<peer::Peer>) -> Option<Arc<peer::Peer>> {
32-
self.peers.insert(value.peer_id, value)
25+
pub fn upsert(&mut self, peer: Arc<Peer>) -> Option<Arc<Peer>> {
26+
self.peers.insert(peer.peer_addr, peer)
3327
}
3428

35-
pub fn remove(&mut self, key: &PeerId) -> Option<Arc<peer::Peer>> {
36-
self.peers.remove(key)
29+
pub fn remove(&mut self, peer: &Peer) -> Option<Arc<Peer>> {
30+
self.peers.remove(&peer.peer_addr)
3731
}
3832

3933
pub fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) {
@@ -42,12 +36,12 @@ impl Swarm {
4236
}
4337

4438
#[must_use]
45-
pub fn get(&self, peer_id: &PeerId) -> Option<&Arc<peer::Peer>> {
46-
self.peers.get(peer_id)
39+
pub fn get(&self, peer_addr: &SocketAddr) -> Option<&Arc<Peer>> {
40+
self.peers.get(peer_addr)
4741
}
4842

4943
#[must_use]
50-
pub fn get_all(&self, limit: Option<usize>) -> Vec<Arc<peer::Peer>> {
44+
pub fn get_all(&self, limit: Option<usize>) -> Vec<Arc<Peer>> {
5145
match limit {
5246
Some(limit) => self.peers.values().take(limit).cloned().collect(),
5347
None => self.peers.values().cloned().collect(),
@@ -151,7 +145,7 @@ mod tests {
151145

152146
swarm.upsert(peer.into());
153147

154-
assert_eq!(swarm.get(&peer.peer_id), Some(Arc::new(peer)).as_ref());
148+
assert_eq!(swarm.get(&peer.peer_addr), Some(Arc::new(peer)).as_ref());
155149
}
156150

157151
#[test]
@@ -173,7 +167,7 @@ mod tests {
173167

174168
swarm.upsert(peer.into());
175169

176-
swarm.remove(&peer.peer_id);
170+
swarm.remove(&peer);
177171

178172
assert!(swarm.is_empty());
179173
}
@@ -186,9 +180,9 @@ mod tests {
186180

187181
swarm.upsert(peer.into());
188182

189-
swarm.remove(&peer.peer_id);
183+
swarm.remove(&peer);
190184

191-
assert_eq!(swarm.get(&peer.peer_id), None);
185+
assert_eq!(swarm.get(&peer.peer_addr), None);
192186
}
193187

194188
#[test]
@@ -273,16 +267,42 @@ mod tests {
273267
}
274268

275269
#[test]
276-
fn allow_inserting_two_identical_peers_except_for_the_id() {
270+
fn allow_inserting_two_identical_peers_except_for_the_socket_address() {
277271
let mut swarm = Swarm::default();
278272

279-
let peer1 = PeerBuilder::default().with_peer_id(&PeerId(*b"-qB00000000000000001")).build();
273+
let peer1 = PeerBuilder::default()
274+
.with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6969))
275+
.build();
280276
swarm.upsert(peer1.into());
281277

282-
let peer2 = PeerBuilder::default().with_peer_id(&PeerId(*b"-qB00000000000000002")).build();
278+
let peer2 = PeerBuilder::default()
279+
.with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 6969))
280+
.build();
283281
swarm.upsert(peer2.into());
284282

285283
assert_eq!(swarm.len(), 2);
286284
}
285+
286+
#[test]
287+
fn not_allow_inserting_two_peers_with_different_peer_id_but_the_same_socket_address() {
288+
let mut swarm = Swarm::default();
289+
290+
// When that happens the peer ID will be changed in the swarm.
291+
// In practice, it's like if the peer had changed its ID.
292+
293+
let peer1 = PeerBuilder::default()
294+
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
295+
.with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6969))
296+
.build();
297+
swarm.upsert(peer1.into());
298+
299+
let peer2 = PeerBuilder::default()
300+
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
301+
.with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6969))
302+
.build();
303+
swarm.upsert(peer2.into());
304+
305+
assert_eq!(swarm.len(), 1);
306+
}
287307
}
288308
}

packages/torrent-repository/src/entry/torrent.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ impl TrackedTorrent {
7575

7676
match peer::ReadInfo::get_event(peer) {
7777
AnnounceEvent::Stopped => {
78-
drop(self.swarm.remove(&peer::ReadInfo::get_id(peer)));
78+
drop(self.swarm.remove(peer));
7979
}
8080
AnnounceEvent::Completed => {
8181
let previous = self.swarm.upsert(Arc::new(*peer));

packages/torrent-repository/tests/common/torrent_peer_builder.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::net::SocketAddr;
1+
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
22

33
use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId};
44
use torrust_tracker_clock::clock::Time;
@@ -67,24 +67,40 @@ impl TorrentPeerBuilder {
6767

6868
/// A torrent seeder is a peer with 0 bytes left to download which
6969
/// has not announced it has stopped
70+
#[allow(clippy::cast_sign_loss)]
71+
#[allow(clippy::cast_possible_truncation)]
7072
#[must_use]
7173
pub fn a_completed_peer(id: i32) -> peer::Peer {
7274
let peer_id = peer::Id::new(id);
75+
let peer_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), id as u16);
76+
7377
TorrentPeerBuilder::new()
7478
.with_number_of_bytes_left(0)
7579
.with_event_completed()
7680
.with_peer_id(*peer_id)
81+
.with_peer_address(peer_addr)
7782
.into()
7883
}
7984

8085
/// A torrent leecher is a peer that is not a seeder.
8186
/// Leecher: left > 0 OR event = Stopped
87+
///
88+
/// # Panics
89+
///
90+
/// This function panics if proved id can't be converted into a valid socket address port.
91+
///
92+
/// The `id` argument is used to identify the peer in both the `peer_id` and the `peer_addr`.
93+
#[allow(clippy::cast_sign_loss)]
94+
#[allow(clippy::cast_possible_truncation)]
8295
#[must_use]
8396
pub fn a_started_peer(id: i32) -> peer::Peer {
8497
let peer_id = peer::Id::new(id);
98+
let peer_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), id as u16);
99+
85100
TorrentPeerBuilder::new()
86101
.with_number_of_bytes_left(1)
87102
.with_event_started()
88103
.with_peer_id(*peer_id)
104+
.with_peer_address(peer_addr)
89105
.into()
90106
}

packages/torrent-repository/tests/entry/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,7 @@ async fn it_should_limit_the_number_of_peers_returned(
370370

371371
// We add one more peer than the scrape limit
372372
for peer_number in 1..=74 + 1 {
373-
let mut peer = a_started_peer(1);
374-
peer.peer_id = *peer::Id::new(peer_number);
373+
let peer = a_started_peer(peer_number);
375374
torrent.upsert_peer(&peer);
376375
}
377376

packages/tracker-core/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,14 @@ mod tests {
224224
// Scrape
225225
let scrape_data = scrape_handler.scrape(&vec![info_hash]).await.unwrap();
226226

227-
// The expected swarm metadata for the file
227+
// The expected swarm metadata for the torrent
228228
let mut expected_scrape_data = ScrapeData::empty();
229229
expected_scrape_data.add_file(
230230
&info_hash,
231231
SwarmMetadata {
232-
complete: 0, // the "complete" peer does not count because it was not previously known
233-
downloaded: 0,
234-
incomplete: 1, // the "incomplete" peer we have just announced
232+
complete: 1, // the "incomplete" announced
233+
downloaded: 0, // the "complete" peer download does not count because it was not previously known
234+
incomplete: 1, // the "incomplete" peer announced
235235
},
236236
);
237237

packages/tracker-core/src/test_helpers.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub(crate) mod tests {
104104
#[must_use]
105105
pub fn complete_peer() -> Peer {
106106
Peer {
107-
peer_id: PeerId(*b"-qB00000000000000000"),
107+
peer_id: PeerId(*b"-qB00000000000000001"),
108108
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080),
109109
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
110110
uploaded: NumberOfBytes::new(0),
@@ -118,8 +118,8 @@ pub(crate) mod tests {
118118
#[must_use]
119119
pub fn incomplete_peer() -> Peer {
120120
Peer {
121-
peer_id: PeerId(*b"-qB00000000000000000"),
122-
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080),
121+
peer_id: PeerId(*b"-qB00000000000000002"),
122+
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 2)), 8080),
123123
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
124124
uploaded: NumberOfBytes::new(0),
125125
downloaded: NumberOfBytes::new(0),

0 commit comments

Comments
 (0)