Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [x] Good Performance in Busy Conditions.
- [x] Support for `UDP`, `HTTP`, and `TLS` Sockets.
- [x] Native `IPv4` and `IPv6` support.
- [x] [I2P] peer announces and matchmaking over HTTP.
- [x] Private & Whitelisted mode.
- [x] Tracker Management API.
- [x] Support [newTrackon][newtrackon] checks.
Expand Down Expand Up @@ -296,3 +297,4 @@ This project was a joint effort by [Nautilus Cyberneering GmbH][nautilus] and [D
[Power2All]: https://github.com/power2all
[torrust-demo]: https://github.com/torrust/torrust-demo
[prometheus]: https://prometheus.io/
[I2P]: https://i2p.net/en/docs/applications/bittorrent/
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
],
"ignorePaths": [
".tmp/**",
"storage/**",
"target",
"docs/media/*.svg",
"contrib/bencode/benches/*.bencode",
Expand All @@ -33,4 +34,4 @@
"contrib/dev-tools/git/github-merge.py",
"docs/issues/**/evidence/*.html"
]
}
}
7 changes: 7 additions & 0 deletions docs/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ Packages that have been extracted to their own standalone repositories.
- Response bencoding
- Error code mapping
- Compact peer formatting
- I2P Destination parsing and compact Destination-hash formatting

HTTP swarms keep I2P peers isolated from clearnet peers. An I2P announce may provide its full
Base64 Destination, with or without the `.i2p` suffix, in the `ip` query parameter. Non-compact
responses return that Destination, while compact responses return its 32-byte SHA-256 hash.
See the [I2P BitTorrent specification](https://i2p.net/en/docs/applications/bittorrent/) for the
wire-format details.

### UDP Tracker (BEP 15)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ mod tests {
use std::str::FromStr;

use torrust_info_hash::InfoHash;
use torrust_tracker_http_protocol::v1::requests::announce::{Announce, Compact, Event, NumberOfBytes};
use torrust_tracker_http_protocol::v1::requests::announce::{Announce, AnnounceAddress, Compact, Event, NumberOfBytes};
use torrust_tracker_http_protocol::v1::responses::error::Error;
use torrust_tracker_primitives::PeerId;

Expand All @@ -113,7 +113,7 @@ mod tests {
info_hash: InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(), // DevSkim: ignore DS173237
peer_id: PeerId(*b"-qB00000000000000001"),
port: 17548,
ip: Some(IpAddr::V4(Ipv4Addr::new(2, 137, 87, 41))),
ip: Some(AnnounceAddress::Ip(IpAddr::V4(Ipv4Addr::new(2, 137, 87, 41)))),
downloaded: Some(NumberOfBytes::new(0)),
uploaded: Some(NumberOfBytes::new(0)),
left: Some(NumberOfBytes::new(0)),
Expand Down
18 changes: 14 additions & 4 deletions packages/axum-http-server/src/v1/handlers/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use torrust_tracker_http_core::services::announce::{AnnounceService, HttpAnnounc
use torrust_tracker_http_protocol::v1::requests::announce::{Announce, Compact};
use torrust_tracker_http_protocol::v1::responses::{self};
use torrust_tracker_http_protocol::v1::services::peer_ip_resolver::ClientIpSources;
use torrust_tracker_primitives::AnnounceData as DomainAnnounceData;
use torrust_tracker_primitives::{AnnounceData as DomainAnnounceData, PeerAddress as DomainPeerAddress};

use crate::v1::extractors::announce_request::ExtractRequest;
use crate::v1::extractors::authentication_key::Extract as ExtractKey;
Expand Down Expand Up @@ -106,9 +106,19 @@ fn to_protocol_announce_data(domain_data: DomainAnnounceData) -> responses::anno
peers: domain_data
.peers
.into_iter()
.map(|peer| responses::announce::Peer {
peer_id: peer.peer_id,
peer_addr: peer.peer_addr,
.map(|peer| {
let peer_addr = match &peer.peer_addr {
DomainPeerAddress::Clearnet(address) => responses::announce::PeerAddress::Clearnet(*address),
DomainPeerAddress::I2p(address) => responses::announce::PeerAddress::I2p {
destination: address.destination.to_string(),
destination_hash: *address.destination.hash(),
},
};

responses::announce::Peer {
peer_id: peer.peer_id,
peer_addr,
}
})
.collect(),
stats: responses::announce::SwarmMetadata {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ use torrust_tracker_client::http::client::Client;
use torrust_tracker_http_protocol::percent_encoding::percent_encode_byte_array;
use torrust_tracker_http_protocol::v1::requests::announce::{AnnounceBuilder, Compact};
use torrust_tracker_http_protocol::v1::responses::announce::deserialization::{
CompactPeer, CompactPeerList, DeserializedNormal, DictionaryPeer,
CompactPeer, CompactPeerList, DeserializedCompact, DeserializedNormal, DictionaryPeer,
};
use torrust_tracker_primitives::PeerId as DomainPeerId;
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
use torrust_tracker_primitives::{I2pDestination, PeerId as DomainPeerId};
use torrust_tracker_test_helpers::{configuration, logging};

use crate::common::fixtures::invalid_info_hashes;
Expand Down Expand Up @@ -105,15 +105,15 @@ async fn should_fail_when_url_query_parameters_are_invalid() {
let http_tracker_config = Arc::new(cfg.http_trackers.unwrap()[0].clone());
let env = Started::new(&core_config, &http_tracker_config).await;

let invalid_query_param = "a=b=c";
let invalid_query_param = "missing-value-separator";

let response = Client::new(env.base_url(), Duration::from_secs(5))
.unwrap()
.get(&format!("announce?{invalid_query_param}"))
.await
.unwrap();

assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await;
assert_cannot_parse_query_param_error_response(response, "invalid param missing-value-separator").await;

env.stop().await;
}
Expand Down Expand Up @@ -594,7 +594,7 @@ async fn should_return_the_list_of_previously_announced_peers() {
min_interval: announce_policy.interval_min,
peers: vec![DictionaryPeer {
peer_id: previously_announced_peer.peer_id.as_bytes().to_vec(),
ip: previously_announced_peer.peer_addr.ip().to_string(),
ip: previously_announced_peer.peer_addr.ip().unwrap().to_string(),
port: previously_announced_peer.peer_addr.port(),
}],
},
Expand Down Expand Up @@ -658,12 +658,12 @@ async fn should_return_the_list_of_previously_announced_peers_including_peers_us
peers: vec![
DictionaryPeer {
peer_id: peer_using_ipv4.peer_id.as_bytes().to_vec(),
ip: peer_using_ipv4.peer_addr.ip().to_string(),
ip: peer_using_ipv4.peer_addr.ip().unwrap().to_string(),
port: peer_using_ipv4.peer_addr.port(),
},
DictionaryPeer {
peer_id: peer_using_ipv6.peer_id.as_bytes().to_vec(),
ip: peer_using_ipv6.peer_addr.ip().to_string(),
ip: peer_using_ipv6.peer_addr.ip().unwrap().to_string(),
port: peer_using_ipv6.peer_addr.port(),
},
],
Expand All @@ -689,14 +689,14 @@ async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_socket
let announce_query_1 = AnnounceBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(peer.peer_id.0))
.with_ip(peer.peer_addr.ip())
.with_ip(peer.peer_addr.ip().unwrap())
.with_port(peer.peer_addr.port())
.query();

let announce_query_2 = AnnounceBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000002")) // Different peer ID
.with_ip(peer.peer_addr.ip())
.with_ip(peer.peer_addr.ip().unwrap())
.with_port(peer.peer_addr.port())
.query();

Expand Down Expand Up @@ -776,14 +776,68 @@ async fn should_return_the_compact_response() {
incomplete: 0,
interval: 120,
min_interval: 120,
peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()),
peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr.socket_addr().unwrap())].to_vec()),
};

assert_compact_announce_response(response, &expected_response).await;

env.stop().await;
}

#[tokio::test]
async fn it_should_return_i2p_destination_hashes_in_a_compact_response() {
logging::setup();

let cfg = configuration::ephemeral_public();
let core_config = Arc::new(cfg.core.clone());
let http_tracker_config = Arc::new(cfg.http_trackers.unwrap()[0].clone());
let env = Started::new(&core_config, &http_tracker_config).await;
let client = Client::new(env.base_url(), Duration::from_secs(5)).unwrap();
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); // DevSkim: ignore DS173237
// cspell:disable-next-line
let first_destination = format!("{}BQAEAAAAAA==.i2p", "A".repeat(512))
.parse::<I2pDestination>()
.unwrap();

client
.announce(
&AnnounceBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_port(1)
.with_i2p_destination(first_destination.clone())
.with_compact(Compact::Accepted)
.query(),
)
.await
.unwrap();

let response = client
.announce(
&AnnounceBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
.with_port(1)
.with_i2p_destination(
// cspell:disable-next-line
format!("B{}BQAEAAAAAA==.i2p", "A".repeat(511))
.parse::<I2pDestination>()
.unwrap(),
)
.with_compact(Compact::Accepted)
.query(),
)
.await
.unwrap();
let bytes = response.bytes().await.unwrap();
let announce = DeserializedCompact::from_bytes(&bytes).unwrap();

assert_eq!(announce.peers, *first_destination.hash());
assert!(announce.peers6.is_empty());

env.stop().await;
}

#[tokio::test]
async fn should_return_the_compact_response_by_default() {
logging::setup();
Expand Down Expand Up @@ -942,10 +996,10 @@ async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_a
.in_memory_torrent_repository
.get_torrent_peers(&info_hash, usize::MAX)
.await;
let peer_addr = peers[0].peer_addr;
let peer_addr = &peers[0].peer_addr;

assert_eq!(peer_addr.ip(), client_ip);
assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
assert_eq!(peer_addr.ip(), Some(client_ip));
assert_ne!(peer_addr.ip(), Some(IpAddr::from_str("2.2.2.2").unwrap()));

env.stop().await;
}
Expand Down Expand Up @@ -986,7 +1040,7 @@ async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_t
.in_memory_torrent_repository
.get_torrent_peers(&info_hash, usize::MAX)
.await;
let peer_addr = peers[0].peer_addr;
let peer_addr = &peers[0].peer_addr;

let ext_ip: IpAddr = env
.container
Expand All @@ -996,8 +1050,8 @@ async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_t
.external_ip
.unwrap()
.into();
assert_eq!(peer_addr.ip(), ext_ip);
assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
assert_eq!(peer_addr.ip(), Some(ext_ip));
assert_ne!(peer_addr.ip(), Some(IpAddr::from_str("2.2.2.2").unwrap()));

env.stop().await;
}
Expand Down Expand Up @@ -1039,7 +1093,7 @@ async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_t
.in_memory_torrent_repository
.get_torrent_peers(&info_hash, usize::MAX)
.await;
let peer_addr = peers[0].peer_addr;
let peer_addr = &peers[0].peer_addr;

let ext_ip: IpAddr = env
.container
Expand All @@ -1049,8 +1103,8 @@ async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_t
.external_ip
.unwrap()
.into();
assert_eq!(peer_addr.ip(), ext_ip);
assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
assert_eq!(peer_addr.ip(), Some(ext_ip));
assert_ne!(peer_addr.ip(), Some(IpAddr::from_str("2.2.2.2").unwrap()));

env.stop().await;
}
Expand Down Expand Up @@ -1095,9 +1149,9 @@ async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer
.in_memory_torrent_repository
.get_torrent_peers(&info_hash, usize::MAX)
.await;
let peer_addr = peers[0].peer_addr;
let peer_addr = &peers[0].peer_addr;

assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap());
assert_eq!(peer_addr.ip(), Some(IpAddr::from_str("150.172.238.178").unwrap()));

env.stop().await;
}
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ async fn should_allow_getting_a_torrent_info() {
seeders: 1,
completed: 0,
leechers: 0,
peers: Some(vec![conversion::from_domain_peer(peer)]),
peers: Some(vec![conversion::from_domain_peer(&peer)]),
},
)
.await;
Expand Down
2 changes: 1 addition & 1 deletion packages/http-core/benches/helpers/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub async fn return_announce_data_once(samples: u64) -> Duration {

let peer = sample_peer();

let (announce_request, client_ip_sources) = sample_announce_request_for_peer(peer);
let (announce_request, client_ip_sources) = sample_announce_request_for_peer(&peer);

let announce_service = AnnounceService::new(
core_tracker_services.core_config.clone(),
Expand Down
6 changes: 3 additions & 3 deletions packages/http-core/benches/helpers/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub async fn initialize_core_tracker_services_with_config(
pub fn sample_peer() -> peer::Peer {
peer::Peer {
peer_id: PeerId(*b"-qB00000000000000000"),
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080),
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080).into(),
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
uploaded: NumberOfBytes::new(0),
downloaded: NumberOfBytes::new(0),
Expand All @@ -102,7 +102,7 @@ pub fn sample_peer() -> peer::Peer {
}
}

pub fn sample_announce_request_for_peer(peer: Peer) -> (Announce, ClientIpSources) {
pub fn sample_announce_request_for_peer(peer: &Peer) -> (Announce, ClientIpSources) {
let announce_request = Announce {
info_hash: sample_info_hash(),
peer_id: peer.peer_id,
Expand All @@ -123,7 +123,7 @@ pub fn sample_announce_request_for_peer(peer: Peer) -> (Announce, ClientIpSource

let client_ip_sources = ClientIpSources {
right_most_x_forwarded_for: None,
connection_info_socket_address: Some(SocketAddr::new(peer.peer_addr.ip(), 8080)),
connection_info_socket_address: Some(SocketAddr::new(peer.peer_addr.ip().unwrap(), 8080)),
};

(announce_request, client_ip_sources)
Expand Down
5 changes: 3 additions & 2 deletions packages/http-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ pub(crate) mod tests {
peer.peer_addr = SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)),
8080,
);
)
.into();
peer
}

pub fn sample_peer() -> peer::Peer {
peer::Peer {
peer_id: PeerId(*b"-qB00000000000000000"),
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080),
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080).into(),
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
uploaded: NumberOfBytes::new(0),
downloaded: NumberOfBytes::new(0),
Expand Down
Loading