use super::{AtomicU32, Ordering, TorrentPeerModel}; use crate::tracker::{ TrackerParseError, TrackerPeerListModel, TrackerResponseModel, TrackerScrapeFileModel, TrackerScrapeModel, }; /// UDP tracker protocol identifier from BEP 15. pub const UDP_TRACKER_PROTOCOL_ID: u64 = 0x0417_2710_1980; /// Process-local counter used to allocate monotonic UDP tracker transaction ids. static UDP_TRACKER_TRANSACTION_COUNTER: AtomicU32 = AtomicU32::new(0x6d69_0000); /// Monotonic transaction identifier used for UDP tracker requests. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct UdpTrackerTransactionId(u32); impl UdpTrackerTransactionId { /// Creates a transaction identifier from a raw integer value. #[must_use] pub const fn new(value: u32) -> Self { Self(value) } /// Allocates the next process-local UDP tracker transaction identifier. #[must_use] pub fn next() -> Self { Self(UDP_TRACKER_TRANSACTION_COUNTER.fetch_add(1, Ordering::Relaxed)) } /// Returns the raw integer value sent on the wire. #[must_use] pub const fn get(self) -> u32 { self.0 } } /// UDP tracker actions defined by BEP 15. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum UdpTrackerAction { /// Connection-id bootstrap request/response. Connect, /// Announce request/response. Announce, /// Scrape request/response. Scrape, /// Error response. Error, } impl UdpTrackerAction { /// Returns the wire value associated with the action. #[must_use] pub const fn wire_value(self) -> u32 { match self { Self::Connect => 0, Self::Announce => 1, Self::Scrape => 2, Self::Error => 3, } } /// Decodes a raw BEP 15 action code into the typed tracker action. fn decode(value: u32) -> Result { match value { 0 => Ok(Self::Connect), 1 => Ok(Self::Announce), 2 => Ok(Self::Scrape), 3 => Ok(Self::Error), _ => Err(TrackerParseError::InvalidUdpAction(value)), } } #[must_use] /// Returns a short diagnostic label for the tracker action. const fn label(self) -> &'static str { match self { Self::Connect => "connect", Self::Announce => "announce", Self::Scrape => "scrape", Self::Error => "error", } } } /// Announce lifecycle values defined by the UDP tracker protocol. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum UdpTrackerAnnounceEvent { /// No explicit lifecycle event. None, /// Download completed. Completed, /// Download started. Started, /// Download stopped. Stopped, } impl UdpTrackerAnnounceEvent { /// Returns the wire value associated with the announce event. #[must_use] pub const fn wire_value(self) -> u32 { match self { Self::None => 0, Self::Completed => 1, Self::Started => 2, Self::Stopped => 3, } } } /// Header shared by all UDP tracker responses. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct UdpTrackerResponseHeader { /// Action encoded by the response. pub action: UdpTrackerAction, /// Transaction identifier associated with the request/response pair. pub transaction_id: UdpTrackerTransactionId, } impl UdpTrackerResponseHeader { /// Decodes a UDP tracker response header from raw bytes. /// /// # Errors /// /// Returns an error when the payload is truncated or the action code is invalid. pub fn decode(input: &[u8]) -> Result { ensure_udp_payload_len(input, 8, "tracker response header")?; Ok(Self { action: UdpTrackerAction::decode(read_u32_be(input, 0, "tracker action id")?)?, transaction_id: UdpTrackerTransactionId::new(read_u32_be( input, 4, "tracker transaction id", )?), }) } /// Verifies that the header action matches the expected value. /// /// # Errors /// /// Returns an error when the action differs from `expected`. pub fn expect_action(self, expected: UdpTrackerAction) -> Result { if self.action == expected { Ok(self) } else { Err(TrackerParseError::InvalidUdpPacket(format!( "expected tracker action {} but got {}", expected.label(), self.action.label() ))) } } /// Verifies that the header transaction identifier matches the expected value. /// /// # Errors /// /// Returns an error when the transaction identifier differs from `expected`. pub fn expect_transaction_id( self, expected: UdpTrackerTransactionId, ) -> Result { if self.transaction_id == expected { Ok(self) } else { Err(TrackerParseError::TransactionIdMismatch { expected: expected.get(), actual: self.transaction_id.get(), }) } } } /// UDP tracker connect request payload. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct UdpTrackerConnectRequest { /// Transaction identifier to match in the response. pub transaction_id: UdpTrackerTransactionId, } impl UdpTrackerConnectRequest { /// Encodes the request into BEP 15 wire bytes. #[must_use] pub fn encode(&self) -> Vec { let mut out = Vec::with_capacity(16); out.extend_from_slice(&UDP_TRACKER_PROTOCOL_ID.to_be_bytes()); out.extend_from_slice(&UdpTrackerAction::Connect.wire_value().to_be_bytes()); out.extend_from_slice(&self.transaction_id.get().to_be_bytes()); out } } /// UDP tracker connect response payload. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct UdpTrackerConnectResponse { /// Transaction identifier echoed by the tracker. pub transaction_id: UdpTrackerTransactionId, /// Connection identifier used by later requests. pub connection_id: u64, } impl UdpTrackerConnectResponse { /// Decodes a connect response from BEP 15 wire bytes. /// /// # Errors /// /// Returns an error when the payload is truncated or malformed. pub fn decode(input: &[u8]) -> Result { ensure_udp_payload_len(input, 16, "connect response")?; let header = UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Connect)?; Ok(Self { transaction_id: header.transaction_id, connection_id: read_u64_be(input, 8, "tracker connection id")?, }) } } /// UDP tracker announce request payload. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct UdpTrackerAnnounceRequest { /// Connection identifier previously returned by the tracker. pub connection_id: u64, /// Transaction identifier to match in the response. pub transaction_id: UdpTrackerTransactionId, /// Raw 20-byte torrent info hash. pub info_hash: [u8; 20], /// Raw 20-byte local peer identifier. pub peer_id: [u8; 20], /// Uploaded byte count. pub downloaded: u64, /// Remaining byte count. pub left: u64, /// Uploaded byte count. pub uploaded: u64, /// Announce lifecycle event. pub event: UdpTrackerAnnounceEvent, /// Optional explicit IPv4 address encoded as a `u32`. pub ip_address: u32, /// Opaque tracker key. pub key: u32, /// Desired peer count or `-1` for tracker default. pub numwant: i32, /// Listening port exposed to peers. pub port: u16, } impl UdpTrackerAnnounceRequest { /// Encodes the announce request into BEP 15 wire bytes. #[must_use] pub fn encode(&self) -> Vec { let mut out = Vec::with_capacity(98); out.extend_from_slice(&self.connection_id.to_be_bytes()); out.extend_from_slice(&UdpTrackerAction::Announce.wire_value().to_be_bytes()); out.extend_from_slice(&self.transaction_id.get().to_be_bytes()); out.extend_from_slice(&self.info_hash); out.extend_from_slice(&self.peer_id); out.extend_from_slice(&self.downloaded.to_be_bytes()); out.extend_from_slice(&self.left.to_be_bytes()); out.extend_from_slice(&self.uploaded.to_be_bytes()); out.extend_from_slice(&self.event.wire_value().to_be_bytes()); out.extend_from_slice(&self.ip_address.to_be_bytes()); out.extend_from_slice(&self.key.to_be_bytes()); out.extend_from_slice(&self.numwant.to_be_bytes()); out.extend_from_slice(&self.port.to_be_bytes()); out } } /// UDP tracker announce response payload. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UdpTrackerAnnounceResponse { /// Transaction identifier echoed by the tracker. pub transaction_id: UdpTrackerTransactionId, /// Recommended announce interval in seconds. pub interval_sec: u32, /// Number of incomplete peers. pub leechers: u32, /// Number of complete peers. pub seeders: u32, /// Parsed compact IPv4 peer list. pub peers: Vec, } impl UdpTrackerAnnounceResponse { /// Decodes an announce response from BEP 15 wire bytes. /// /// # Errors /// /// Returns an error when the payload is truncated or malformed. pub fn decode(input: &[u8]) -> Result { ensure_udp_payload_len(input, 20, "announce response")?; let header = UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Announce)?; Ok(Self { transaction_id: header.transaction_id, interval_sec: read_u32_be(input, 8, "announce interval")?, leechers: read_u32_be(input, 12, "announce leechers")?, seeders: read_u32_be(input, 16, "announce seeders")?, peers: super::parsing::parse_compact_peers_ipv4(&input[20..])?, }) } /// Converts the announce response into the higher-level tracker response model. #[must_use] pub fn into_tracker_response(self) -> TrackerResponseModel { TrackerResponseModel { peers: TrackerPeerListModel { interval_sec: self.interval_sec, peers: self.peers, min_interval_sec: None, tracker_id: None, }, scrape: None, } } } /// UDP tracker scrape request payload. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UdpTrackerScrapeRequest { /// Connection identifier previously returned by the tracker. pub connection_id: u64, /// Transaction identifier to match in the response. pub transaction_id: UdpTrackerTransactionId, /// Raw 20-byte info hashes to scrape. pub info_hashes: Vec<[u8; 20]>, } impl UdpTrackerScrapeRequest { /// Encodes the scrape request into BEP 15 wire bytes. /// /// # Errors /// /// Returns an error when no info hashes were supplied. pub fn encode(&self) -> Result, TrackerParseError> { if self.info_hashes.is_empty() { return Err(TrackerParseError::InvalidUdpPacket( "udp tracker scrape request requires at least one info hash".to_owned(), )); } let mut out = Vec::with_capacity(16 + self.info_hashes.len() * 20); out.extend_from_slice(&self.connection_id.to_be_bytes()); out.extend_from_slice(&UdpTrackerAction::Scrape.wire_value().to_be_bytes()); out.extend_from_slice(&self.transaction_id.get().to_be_bytes()); for info_hash in &self.info_hashes { out.extend_from_slice(info_hash); } Ok(out) } } /// Per-torrent counters returned by a UDP tracker scrape response. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct UdpTrackerScrapeStats { /// Number of completed downloads. pub complete: u32, /// Number of completed client downloads. pub downloaded: u32, /// Number of incomplete peers. pub incomplete: u32, } /// UDP tracker scrape response payload. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UdpTrackerScrapeResponse { /// Transaction identifier echoed by the tracker. pub transaction_id: UdpTrackerTransactionId, /// Per-info-hash scrape statistics. pub files: Vec, } impl UdpTrackerScrapeResponse { /// Decodes a scrape response from BEP 15 wire bytes. /// /// # Errors /// /// Returns an error when the payload is truncated or malformed. pub fn decode(input: &[u8]) -> Result { ensure_udp_payload_len(input, 8, "scrape response header")?; let header = UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Scrape)?; let payload = &input[8..]; if !payload.len().is_multiple_of(12) { return Err(TrackerParseError::InvalidUdpPacket( "scrape response payload length must be divisible by 12".to_owned(), )); } let mut files = Vec::with_capacity(payload.len() / 12); for offset in (0..payload.len()).step_by(12) { files.push(UdpTrackerScrapeStats { complete: read_u32_be(payload, offset, "scrape complete count")?, downloaded: read_u32_be(payload, offset + 4, "scrape downloaded count")?, incomplete: read_u32_be(payload, offset + 8, "scrape incomplete count")?, }); } Ok(Self { transaction_id: header.transaction_id, files, }) } /// Converts the response into the higher-level scrape model. /// /// # Errors /// /// Returns an error when the info-hash list does not match the response entry count. pub fn to_scrape_model( &self, info_hashes: &[[u8; 20]], ) -> Result { if info_hashes.len() != self.files.len() { return Err(TrackerParseError::InvalidUdpPacket(format!( "scrape response entry count {} does not match info-hash count {}", self.files.len(), info_hashes.len() ))); } let files = info_hashes .iter() .zip(self.files.iter()) .map(|(info_hash, stats)| TrackerScrapeFileModel { info_hash: super::parsing::hex_encode(info_hash), complete: Some(stats.complete), downloaded: Some(stats.downloaded), incomplete: Some(stats.incomplete), }) .collect::>(); let (complete, downloaded, incomplete) = if self.files.len() == 1 { let stats = self.files[0]; ( Some(stats.complete), Some(stats.downloaded), Some(stats.incomplete), ) } else { (None, None, None) }; Ok(TrackerScrapeModel { complete, downloaded, incomplete, files, }) } } /// Verifies that a UDP tracker payload is at least `min_len` bytes long. fn ensure_udp_payload_len( input: &[u8], min_len: usize, label: &str, ) -> Result<(), TrackerParseError> { if input.len() < min_len { return Err(TrackerParseError::InvalidUdpPacket(format!( "{label} truncated: expected at least {min_len} bytes but got {}", input.len() ))); } Ok(()) } /// Reads one big-endian `u32` from a UDP tracker payload. fn read_u32_be(input: &[u8], offset: usize, field: &str) -> Result { let end = offset.saturating_add(4); let bytes = input .get(offset..end) .ok_or_else(|| TrackerParseError::InvalidUdpPacket(format!("{field} truncated")))?; let mut out = [0_u8; 4]; out.copy_from_slice(bytes); Ok(u32::from_be_bytes(out)) } /// Reads one big-endian `u64` from a UDP tracker payload. fn read_u64_be(input: &[u8], offset: usize, field: &str) -> Result { let end = offset.saturating_add(8); let bytes = input .get(offset..end) .ok_or_else(|| TrackerParseError::InvalidUdpPacket(format!("{field} truncated")))?; let mut out = [0_u8; 8]; out.copy_from_slice(bytes); Ok(u64::from_be_bytes(out)) }