From 610140cff6bac7a058964dfe3a9dfe3c880bf794 Mon Sep 17 00:00:00 2001 From: Alex Galvin Date: Sat, 24 May 2025 03:57:23 -0400 Subject: [PATCH 1/2] feat: include crate version in peer_id --- crates/dht/src/dht.rs | 7 +- crates/librqbit/src/dht_utils.rs | 2 +- crates/librqbit/src/peer_info_reader/mod.rs | 2 +- crates/librqbit/src/session.rs | 7 +- crates/librqbit_core/Cargo.toml | 2 +- crates/librqbit_core/src/peer_id.rs | 83 ++++++++++++++++--- crates/tracker_comms/src/tracker_comms_udp.rs | 2 +- flake.lock | 77 +++++++++++++++++ 8 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 flake.lock diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 72d6297..98ff51b 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -29,8 +29,9 @@ use futures::{ use leaky_bucket::RateLimiter; use librqbit_core::{ + crate_version, hash_id::Id20, - peer_id::generate_peer_id, + peer_id::generate_azereus_style, spawn_utils::{spawn, spawn_with_cancel}, }; use parking_lot::RwLock; @@ -1167,7 +1168,9 @@ impl DhtState { .context("cannot determine UDP listen addr")?; info!("DHT listening on {:?}", listen_addr); - let peer_id = config.peer_id.unwrap_or_else(generate_peer_id); + let peer_id = config + .peer_id + .unwrap_or_else(|| (generate_azereus_style(*b"rQ", crate_version!()))); info!("starting up DHT with peer id {:?}", peer_id); let bootstrap_addrs = config .bootstrap_addrs diff --git a/crates/librqbit/src/dht_utils.rs b/crates/librqbit/src/dht_utils.rs index 3ac4cdc..86395da 100644 --- a/crates/librqbit/src/dht_utils.rs +++ b/crates/librqbit/src/dht_utils.rs @@ -131,7 +131,7 @@ mod tests { let dht = DhtBuilder::new().await.unwrap(); let peer_rx = dht.get_peers(info_hash, None); - let peer_id = generate_peer_id(); + let peer_id = generate_peer_id(b"-xx1234-"); match read_metainfo_from_peer_receiver( peer_id, info_hash, diff --git a/crates/librqbit/src/peer_info_reader/mod.rs b/crates/librqbit/src/peer_info_reader/mod.rs index d311a96..b91e6d9 100644 --- a/crates/librqbit/src/peer_info_reader/mod.rs +++ b/crates/librqbit/src/peer_info_reader/mod.rs @@ -270,7 +270,7 @@ mod tests { init_logging(); let addr = SocketAddr::from_str("127.0.0.1:27311").unwrap(); - let peer_id = generate_peer_id(); + let peer_id = generate_peer_id(b"-xx1234-"); let info_hash = Id20::from_str("9905f844e5d8787ecd5e08fb46b2eb0a42c131d7").unwrap(); dbg!(read_metainfo_from_peer( addr, diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 683c7ed..49715e9 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -46,9 +46,10 @@ use futures::{ use itertools::Itertools; use librqbit_core::{ constants::CHUNK_SIZE, + crate_version, directories::get_configuration_directory, magnet::Magnet, - peer_id::generate_peer_id, + peer_id::generate_azereus_style, spawn_utils::spawn_with_cancel, torrent_metainfo::{TorrentMetaV1Info, TorrentMetaV1Owned}, }; @@ -498,7 +499,9 @@ impl Session { mut opts: SessionOptions, ) -> BoxFuture<'static, anyhow::Result>> { async move { - let peer_id = opts.peer_id.unwrap_or_else(generate_peer_id); + let peer_id = opts + .peer_id + .unwrap_or_else(|| generate_azereus_style(*b"rQ", crate_version!())); let token = opts.cancellation_token.take().unwrap_or_default(); #[cfg(feature = "disable-upload")] diff --git a/crates/librqbit_core/Cargo.toml b/crates/librqbit_core/Cargo.toml index 6ab064a..61c9315 100644 --- a/crates/librqbit_core/Cargo.toml +++ b/crates/librqbit_core/Cargo.toml @@ -20,7 +20,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } hex = "0.4" anyhow = "1" url = { version = "2", default-features = false } -uuid = { version = "1", features = ["v4"] } +rand = "0.8" parking_lot = "0.12" serde = { version = "1", features = ["derive"] } buffers = { path = "../buffers", package = "librqbit-buffers", version = "4.2" } diff --git a/crates/librqbit_core/src/peer_id.rs b/crates/librqbit_core/src/peer_id.rs index 6f2ce60..94cfdb4 100644 --- a/crates/librqbit_core/src/peer_id.rs +++ b/crates/librqbit_core/src/peer_id.rs @@ -1,4 +1,18 @@ use crate::hash_id::Id20; +use rand::{self, RngCore}; + +/// Return the version of the invoking crate as a tuple +#[macro_export] +macro_rules! crate_version { + () => { + ( + env!("CARGO_PKG_VERSION_MAJOR").parse::().unwrap_or(0), + env!("CARGO_PKG_VERSION_MINOR").parse::().unwrap_or(0), + env!("CARGO_PKG_VERSION_PATCH").parse::().unwrap_or(0), + env!("CARGO_PKG_VERSION_PRE").parse::().unwrap_or(0), + ) + }; +} #[derive(Debug)] pub enum AzureusStyleKind { @@ -8,13 +22,13 @@ pub enum AzureusStyleKind { QBittorrent, UTorrent, RQBit, - Other([char; 2]), + Other([u8; 2]), } #[derive(Debug)] pub struct AzureusStyle { pub kind: AzureusStyleKind, - pub version: [char; 4], + pub version: [u8; 4], } impl AzureusStyleKind { @@ -26,7 +40,7 @@ impl AzureusStyleKind { b"qB" => AzureusStyleKind::QBittorrent, b"UT" => AzureusStyleKind::UTorrent, b"rQ" => AzureusStyleKind::RQBit, - _ => AzureusStyleKind::Other([b1 as char, b2 as char]), + other => AzureusStyleKind::Other(*other), } } } @@ -36,9 +50,9 @@ fn try_decode_azureus_style(p: &Id20) -> Option { if !(p[0] == b'-' && p[7] == b'-') { return None; } - let mut version = ['0'; 4]; + let mut version = [b'0'; 4]; for (i, c) in p[3..7].iter().copied().enumerate() { - version[i] = c as char; + version[i] = version_digit_from_id(c)?; } let kind = AzureusStyleKind::from_bytes(p[1], p[2]); Some(AzureusStyle { kind, version }) @@ -53,13 +67,62 @@ pub fn try_decode_peer_id(p: Id20) -> Option { Some(PeerId::AzureusStyle(try_decode_azureus_style(&p)?)) } -pub fn generate_peer_id() -> Id20 { +/// Returns `None` for bytes greater than 64 +fn version_digit_to_id(d: u8) -> Option { + let version_map = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-"; + version_map.get(d as usize).copied() +} + +/// Returns `None` for bytes that aren't alphanumeric, `.` or `-`. +fn version_digit_from_id(d: u8) -> Option { + match d { + b'0'..=b'9' => Some(d - b'0'), + b'A'..=b'Z' => Some(d - b'0' - 10), + b'a'..=b'z' => Some(d - b'0' - 10 - 26), + b'.' => Some(62), + b'-' => Some(63), + _ => None, + } +} + +/// Generate a client fingerprint in the Azereus format, where `b"-xx1234-"` corresponds to version `1.2.3.4`` of the torrent client abbreviated by `xx` +pub fn generate_azereus_style(client: [u8; 2], version: (u8, u8, u8, u8)) -> Id20 { + let mut fingerprint = [b'-'; 8]; + + fingerprint[1..3].copy_from_slice(&client); + fingerprint[3] = version_digit_to_id(version.0).unwrap(); + fingerprint[4] = version_digit_to_id(version.1).unwrap(); + fingerprint[5] = version_digit_to_id(version.2).unwrap(); + fingerprint[6] = version_digit_to_id(version.3).unwrap(); + generate_peer_id(&fingerprint) +} + +/// Panics if the `fingerprint` slice isn't eight bytes long +pub fn generate_peer_id(fingerprint: &[u8]) -> Id20 { let mut peer_id = [0u8; 20]; - let u = uuid::Uuid::new_v4(); - peer_id[4..20].copy_from_slice(&u.as_bytes()[..]); - - peer_id[..8].copy_from_slice(b"-rQ7000-"); + peer_id[..8].copy_from_slice(fingerprint); + rand::thread_rng().fill_bytes(&mut peer_id[8..]); Id20::new(peer_id) } + +#[cfg(test)] +mod tests { + use crate::peer_id::generate_azereus_style; + + #[test] + fn test_azereus_peer_id_generation() { + for (client, version, correct_fingerprint) in [ + (*b"xx", (1, 2, 3, 4), *b"-xx1234-"), + (*b"00", (10, 0, 0, 0), *b"-00A000-"), + (*b"\xFF\xFF", (36, 37, 62, 63), *b"-\xFF\xFFab.--"), + ] { + let id1 = generate_azereus_style(client, version); + let id2 = generate_azereus_style(client, version); + assert_ne!(id1, id2); + assert_eq!(id1.0[..8], id2.0[..8]); + assert_eq!(id1.0[..8], correct_fingerprint); + } + } +} diff --git a/crates/tracker_comms/src/tracker_comms_udp.rs b/crates/tracker_comms/src/tracker_comms_udp.rs index 30fc65d..0b05227 100644 --- a/crates/tracker_comms/src/tracker_comms_udp.rs +++ b/crates/tracker_comms/src/tracker_comms_udp.rs @@ -447,7 +447,7 @@ mod tests { connection_id, AnnounceFields { info_hash: hash, - peer_id: generate_peer_id(), + peer_id: generate_peer_id(b"-xx1234-"), downloaded: 0, left: 0, uploaded: 0, diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..48de183 --- /dev/null +++ b/flake.lock @@ -0,0 +1,77 @@ +{ + "nodes": { + "flake-compat": { + "locked": { + "lastModified": 1748460212, + "narHash": "sha256-RBUseGlYAKOd8hnKVujiGzpdJoZWj5e3A+Ds2mKsv28=", + "ref": "refs/heads/main", + "rev": "88e58d66efad1b3e0edf8633ea0774f7105d37c9", + "revCount": 86, + "type": "git", + "url": "https://git.lix.systems/lix-project/flake-compat" + }, + "original": { + "type": "git", + "url": "https://git.lix.systems/lix-project/flake-compat" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1748302896, + "narHash": "sha256-ixMT0a8mM091vSswlTORZj93WQAJsRNmEvqLL+qwTFM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7848cd8c982f7740edf76ddb3b43d234cb80fc4d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} From efad73dbe12e8a2cc29020603bd12e9ab67603cb Mon Sep 17 00:00:00 2001 From: Alex Galvin Date: Sun, 25 May 2025 22:49:44 -0400 Subject: [PATCH 2/2] chore: bump librqbit-core to 5.0.0 --- crates/dht/Cargo.toml | 2 +- crates/librqbit/Cargo.toml | 2 +- crates/librqbit_core/Cargo.toml | 2 +- crates/peer_binary_protocol/Cargo.toml | 2 +- crates/tracker_comms/Cargo.toml | 2 +- crates/upnp-serve/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/dht/Cargo.toml b/crates/dht/Cargo.toml index 225e476..64f05da 100644 --- a/crates/dht/Cargo.toml +++ b/crates/dht/Cargo.toml @@ -38,7 +38,7 @@ rand = "0.8" indexmap = "2" dashmap = { version = "6", features = ["serde"] } clone_to_owned = { path = "../clone_to_owned", package = "librqbit-clone-to-owned", version = "3" } -librqbit-core = { path = "../librqbit_core", default-features = false, version = "4.1" } +librqbit-core = { path = "../librqbit_core", default-features = false, version = "5" } chrono = { version = "0.4.31", features = ["serde"] } tokio-util = "0.7.10" bytes = "1.7.1" diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 1f8c4ee..ee130be 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -52,7 +52,7 @@ home = { version = "=0.5.5", optional = true } bencode = { path = "../bencode", default-features = false, package = "librqbit-bencode", version = "3" } tracker_comms = { path = "../tracker_comms", default-features = false, package = "librqbit-tracker-comms", version = "2.1" } buffers = { path = "../buffers", package = "librqbit-buffers", version = "4.2" } -librqbit-core = { path = "../librqbit_core", default-features = false, version = "4.1" } +librqbit-core = { path = "../librqbit_core", default-features = false, version = "5" } clone_to_owned = { path = "../clone_to_owned", package = "librqbit-clone-to-owned", version = "3" } peer_binary_protocol = { path = "../peer_binary_protocol", default-features = false, package = "librqbit-peer-protocol", version = "4.2" } sha1w = { path = "../sha1w", default-features = false, package = "librqbit-sha1-wrapper", version = "4.1" } diff --git a/crates/librqbit_core/Cargo.toml b/crates/librqbit_core/Cargo.toml index 61c9315..8767809 100644 --- a/crates/librqbit_core/Cargo.toml +++ b/crates/librqbit_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-core" -version = "4.1.0" +version = "5.0.0" edition = "2021" description = "Important utilities used throughout librqbit useful for working with torrents." license = "Apache-2.0" diff --git a/crates/peer_binary_protocol/Cargo.toml b/crates/peer_binary_protocol/Cargo.toml index 85f5862..9c4bd80 100644 --- a/crates/peer_binary_protocol/Cargo.toml +++ b/crates/peer_binary_protocol/Cargo.toml @@ -24,7 +24,7 @@ byteorder = "1" buffers = { path = "../buffers", package = "librqbit-buffers", version = "4.2" } bencode = { path = "../bencode", default-features = false, package = "librqbit-bencode", version = "3" } clone_to_owned = { path = "../clone_to_owned", package = "librqbit-clone-to-owned", version = "3" } -librqbit-core = { path = "../librqbit_core", default-features = false, version = "4.1" } +librqbit-core = { path = "../librqbit_core", default-features = false, version = "5" } bitvec = "1" anyhow = "1" bytes = "1.7.1" diff --git a/crates/tracker_comms/Cargo.toml b/crates/tracker_comms/Cargo.toml index 704894e..d212f43 100644 --- a/crates/tracker_comms/Cargo.toml +++ b/crates/tracker_comms/Cargo.toml @@ -23,7 +23,7 @@ anyhow = "1" futures = "0.3" async-stream = "0.3.5" buffers = { path = "../buffers", package = "librqbit-buffers", version = "4.2" } -librqbit-core = { path = "../librqbit_core", default-features = false, version = "4.1" } +librqbit-core = { path = "../librqbit_core", default-features = false, version = "5" } byteorder = "1.5" serde = { version = "1", features = ["derive"] } urlencoding = "2" diff --git a/crates/upnp-serve/Cargo.toml b/crates/upnp-serve/Cargo.toml index 17b1295..797e847 100644 --- a/crates/upnp-serve/Cargo.toml +++ b/crates/upnp-serve/Cargo.toml @@ -29,7 +29,7 @@ uuid = { version = "1.10.0", features = ["v4"] } librqbit-upnp = { version = "1", path = "../upnp", default-features = false } gethostname = "0.5.0" librqbit-sha1-wrapper = { path = "../sha1w", version = "4", default-features = false } -librqbit-core = { version = "4.1", path = "../librqbit_core", default-features = false } +librqbit-core = { version = "5", path = "../librqbit_core", default-features = false } mime_guess = "2.0.5" url = { version = "2", default-features = false } parking_lot = "0.12.3"