feat: include crate version in peer_id

This commit is contained in:
Alex Galvin 2025-05-24 03:57:23 -04:00
parent e83da0b194
commit 610140cff6
8 changed files with 164 additions and 18 deletions

View file

@ -29,8 +29,9 @@ use futures::{
use leaky_bucket::RateLimiter; use leaky_bucket::RateLimiter;
use librqbit_core::{ use librqbit_core::{
crate_version,
hash_id::Id20, hash_id::Id20,
peer_id::generate_peer_id, peer_id::generate_azereus_style,
spawn_utils::{spawn, spawn_with_cancel}, spawn_utils::{spawn, spawn_with_cancel},
}; };
use parking_lot::RwLock; use parking_lot::RwLock;
@ -1167,7 +1168,9 @@ impl DhtState {
.context("cannot determine UDP listen addr")?; .context("cannot determine UDP listen addr")?;
info!("DHT listening on {:?}", 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); info!("starting up DHT with peer id {:?}", peer_id);
let bootstrap_addrs = config let bootstrap_addrs = config
.bootstrap_addrs .bootstrap_addrs

View file

@ -131,7 +131,7 @@ mod tests {
let dht = DhtBuilder::new().await.unwrap(); let dht = DhtBuilder::new().await.unwrap();
let peer_rx = dht.get_peers(info_hash, None); 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( match read_metainfo_from_peer_receiver(
peer_id, peer_id,
info_hash, info_hash,

View file

@ -270,7 +270,7 @@ mod tests {
init_logging(); init_logging();
let addr = SocketAddr::from_str("127.0.0.1:27311").unwrap(); 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(); let info_hash = Id20::from_str("9905f844e5d8787ecd5e08fb46b2eb0a42c131d7").unwrap();
dbg!(read_metainfo_from_peer( dbg!(read_metainfo_from_peer(
addr, addr,

View file

@ -46,9 +46,10 @@ use futures::{
use itertools::Itertools; use itertools::Itertools;
use librqbit_core::{ use librqbit_core::{
constants::CHUNK_SIZE, constants::CHUNK_SIZE,
crate_version,
directories::get_configuration_directory, directories::get_configuration_directory,
magnet::Magnet, magnet::Magnet,
peer_id::generate_peer_id, peer_id::generate_azereus_style,
spawn_utils::spawn_with_cancel, spawn_utils::spawn_with_cancel,
torrent_metainfo::{TorrentMetaV1Info, TorrentMetaV1Owned}, torrent_metainfo::{TorrentMetaV1Info, TorrentMetaV1Owned},
}; };
@ -498,7 +499,9 @@ impl Session {
mut opts: SessionOptions, mut opts: SessionOptions,
) -> BoxFuture<'static, anyhow::Result<Arc<Self>>> { ) -> BoxFuture<'static, anyhow::Result<Arc<Self>>> {
async move { 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(); let token = opts.cancellation_token.take().unwrap_or_default();
#[cfg(feature = "disable-upload")] #[cfg(feature = "disable-upload")]

View file

@ -20,7 +20,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
hex = "0.4" hex = "0.4"
anyhow = "1" anyhow = "1"
url = { version = "2", default-features = false } url = { version = "2", default-features = false }
uuid = { version = "1", features = ["v4"] } rand = "0.8"
parking_lot = "0.12" parking_lot = "0.12"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
buffers = { path = "../buffers", package = "librqbit-buffers", version = "4.2" } buffers = { path = "../buffers", package = "librqbit-buffers", version = "4.2" }

View file

@ -1,4 +1,18 @@
use crate::hash_id::Id20; 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::<u8>().unwrap_or(0),
env!("CARGO_PKG_VERSION_MINOR").parse::<u8>().unwrap_or(0),
env!("CARGO_PKG_VERSION_PATCH").parse::<u8>().unwrap_or(0),
env!("CARGO_PKG_VERSION_PRE").parse::<u8>().unwrap_or(0),
)
};
}
#[derive(Debug)] #[derive(Debug)]
pub enum AzureusStyleKind { pub enum AzureusStyleKind {
@ -8,13 +22,13 @@ pub enum AzureusStyleKind {
QBittorrent, QBittorrent,
UTorrent, UTorrent,
RQBit, RQBit,
Other([char; 2]), Other([u8; 2]),
} }
#[derive(Debug)] #[derive(Debug)]
pub struct AzureusStyle { pub struct AzureusStyle {
pub kind: AzureusStyleKind, pub kind: AzureusStyleKind,
pub version: [char; 4], pub version: [u8; 4],
} }
impl AzureusStyleKind { impl AzureusStyleKind {
@ -26,7 +40,7 @@ impl AzureusStyleKind {
b"qB" => AzureusStyleKind::QBittorrent, b"qB" => AzureusStyleKind::QBittorrent,
b"UT" => AzureusStyleKind::UTorrent, b"UT" => AzureusStyleKind::UTorrent,
b"rQ" => AzureusStyleKind::RQBit, 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<AzureusStyle> {
if !(p[0] == b'-' && p[7] == b'-') { if !(p[0] == b'-' && p[7] == b'-') {
return None; return None;
} }
let mut version = ['0'; 4]; let mut version = [b'0'; 4];
for (i, c) in p[3..7].iter().copied().enumerate() { 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]); let kind = AzureusStyleKind::from_bytes(p[1], p[2]);
Some(AzureusStyle { kind, version }) Some(AzureusStyle { kind, version })
@ -53,13 +67,62 @@ pub fn try_decode_peer_id(p: Id20) -> Option<PeerId> {
Some(PeerId::AzureusStyle(try_decode_azureus_style(&p)?)) 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<u8> {
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<u8> {
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 mut peer_id = [0u8; 20];
let u = uuid::Uuid::new_v4(); peer_id[..8].copy_from_slice(fingerprint);
peer_id[4..20].copy_from_slice(&u.as_bytes()[..]); rand::thread_rng().fill_bytes(&mut peer_id[8..]);
peer_id[..8].copy_from_slice(b"-rQ7000-");
Id20::new(peer_id) 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);
}
}
}

View file

@ -447,7 +447,7 @@ mod tests {
connection_id, connection_id,
AnnounceFields { AnnounceFields {
info_hash: hash, info_hash: hash,
peer_id: generate_peer_id(), peer_id: generate_peer_id(b"-xx1234-"),
downloaded: 0, downloaded: 0,
left: 0, left: 0,
uploaded: 0, uploaded: 0,

77
flake.lock generated Normal file
View file

@ -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
}