Skip to main content

irontide_tracker/
udp.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    reason = "M175: BEP 15 UDP tracker wire format — field widths fixed by spec"
6)]
7
8use std::net::SocketAddr;
9use std::os::fd::{AsFd, AsRawFd};
10use std::time::Duration;
11
12use tokio::net::UdpSocket;
13
14use irontide_core::Id20;
15
16use crate::compact::{parse_compact_peers, parse_compact_peers6};
17use crate::error::{Error, Result};
18
19/// Apply DSCP/ToS marking to a UDP socket. No-op if dscp == 0.
20fn apply_dscp_udp(socket: &UdpSocket, dscp: u8, is_ipv6: bool) {
21    if dscp == 0 {
22        return;
23    }
24    let tos = u32::from(dscp) << 2;
25    let fd = socket.as_fd().as_raw_fd();
26    let result = unsafe {
27        if is_ipv6 {
28            libc::setsockopt(
29                fd,
30                libc::IPPROTO_IPV6,
31                libc::IPV6_TCLASS,
32                std::ptr::from_ref(&(tos as libc::c_int)).cast::<libc::c_void>(),
33                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
34            )
35        } else {
36            libc::setsockopt(
37                fd,
38                libc::IPPROTO_IP,
39                libc::IP_TOS,
40                std::ptr::from_ref(&(tos as libc::c_int)).cast::<libc::c_void>(),
41                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
42            )
43        }
44    };
45    if result != 0 {
46        tracing::debug!(
47            dscp,
48            "failed to set DSCP on UDP tracker socket: {}",
49            std::io::Error::last_os_error()
50        );
51    }
52}
53use crate::{AnnounceRequest, AnnounceResponse, ScrapeInfo};
54
55/// BEP 41: UDP tracker protocol extension option.
56///
57/// Options appear as TLV (Type-Length-Value) entries after the peer list in a
58/// UDP announce response. Types 0x00 and 0x01 are single-byte (no length field).
59/// Types 0x02..0x7F use a 1-byte length. Types 0x80..0xFF use a 2-byte big-endian
60/// length.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum UdpTrackerOption {
63    /// End of options marker (type 0x00).
64    EndOfOptions,
65    /// No operation / padding (type 0x01).
66    Nop,
67    /// URL data from tracker (type 0x02).
68    UrlData(String),
69    /// Unknown extension type (forward compatible).
70    Unknown {
71        /// The option type byte.
72        option_type: u8,
73        /// Raw option payload.
74        data: Vec<u8>,
75    },
76}
77
78/// Parse BEP 41 TLV-encoded options from the trailing bytes of a UDP response.
79///
80/// Parsing stops at `EndOfOptions` (0x00) or when the data is exhausted.
81/// Malformed trailing bytes (truncated length/value) are silently ignored
82/// for forward compatibility.
83fn parse_udp_options(data: &[u8]) -> Vec<UdpTrackerOption> {
84    let mut options = Vec::new();
85    let mut pos = 0;
86    while pos < data.len() {
87        let opt_type = data[pos];
88        pos += 1;
89        match opt_type {
90            0x00 => {
91                options.push(UdpTrackerOption::EndOfOptions);
92                break;
93            }
94            0x01 => {
95                options.push(UdpTrackerOption::Nop);
96            }
97            _ => {
98                // Types 0x02..0x7F: 1-byte length. Types 0x80..0xFF: 2-byte BE length.
99                if pos >= data.len() {
100                    break;
101                }
102                let length = if opt_type < 0x80 {
103                    let l = data[pos] as usize;
104                    pos += 1;
105                    l
106                } else {
107                    if pos.checked_add(1).is_none_or(|end| end >= data.len()) {
108                        break;
109                    }
110                    let l = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
111                    pos += 2;
112                    l
113                };
114                if pos.checked_add(length).is_none_or(|end| end > data.len()) {
115                    break;
116                }
117                let value = &data[pos..pos + length];
118                pos += length;
119                match opt_type {
120                    0x02 => {
121                        if let Ok(url) = std::str::from_utf8(value) {
122                            options.push(UdpTrackerOption::UrlData(url.to_owned()));
123                        }
124                    }
125                    _ => {
126                        options.push(UdpTrackerOption::Unknown {
127                            option_type: opt_type,
128                            data: value.to_vec(),
129                        });
130                    }
131                }
132            }
133        }
134    }
135    options
136}
137
138/// Magic connection ID for UDP tracker connect (BEP 15).
139const CONNECT_MAGIC: u64 = 0x0417_2710_1980;
140const ACTION_CONNECT: u32 = 0;
141const ACTION_ANNOUNCE: u32 = 1;
142const ACTION_SCRAPE: u32 = 2;
143
144/// Default timeout for UDP tracker requests.
145const UDP_TIMEOUT: Duration = Duration::from_secs(15);
146
147/// UDP tracker client (BEP 15).
148#[derive(Clone)]
149pub struct UdpTracker {
150    timeout: Duration,
151    dscp: u8,
152}
153
154/// UDP announce response.
155#[derive(Debug, Clone)]
156pub struct UdpAnnounceResponse {
157    /// Common announce response data (interval, peers, etc.).
158    pub response: AnnounceResponse,
159    /// Transaction ID echoed from the request.
160    pub transaction_id: u32,
161    /// BEP 41: extension options parsed from trailing bytes after the peer list.
162    pub options: Vec<UdpTrackerOption>,
163}
164
165/// UDP scrape response.
166#[derive(Debug, Clone)]
167pub struct UdpScrapeResponse {
168    /// Per-torrent scrape statistics (same order as requested hashes).
169    pub results: Vec<ScrapeInfo>,
170    /// Transaction ID echoed from the request.
171    pub transaction_id: u32,
172}
173
174impl UdpTracker {
175    /// Creates a new UDP tracker client with default settings.
176    #[must_use]
177    pub fn new() -> Self {
178        Self {
179            timeout: UDP_TIMEOUT,
180            dscp: 0,
181        }
182    }
183
184    /// Sets the timeout duration for UDP tracker requests.
185    #[must_use]
186    pub fn with_timeout(mut self, timeout: Duration) -> Self {
187        self.timeout = timeout;
188        self
189    }
190
191    /// Sets the DSCP/ToS value for outbound UDP packets.
192    #[must_use]
193    pub fn with_dscp(mut self, dscp: u8) -> Self {
194        self.dscp = dscp;
195        self
196    }
197
198    /// Build a UDP connect request packet (BEP 15).
199    #[must_use]
200    pub fn build_connect_request(transaction_id: u32) -> [u8; 16] {
201        let mut buf = [0u8; 16];
202        buf[0..8].copy_from_slice(&CONNECT_MAGIC.to_be_bytes());
203        buf[8..12].copy_from_slice(&ACTION_CONNECT.to_be_bytes());
204        buf[12..16].copy_from_slice(&transaction_id.to_be_bytes());
205        buf
206    }
207
208    /// Parse a UDP connect response, returning the `connection_id`.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if the response is too short, has a wrong transaction ID, or is an error response.
213    pub fn parse_connect_response(data: &[u8], expected_transaction_id: u32) -> Result<u64> {
214        if data.len() < 16 {
215            return Err(Error::UdpProtocol(format!(
216                "connect response too short: {} bytes",
217                data.len()
218            )));
219        }
220
221        let action = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
222        if action != ACTION_CONNECT {
223            return Err(Error::UdpProtocol(format!(
224                "expected action 0 (connect), got {action}"
225            )));
226        }
227
228        let transaction_id = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
229        if transaction_id != expected_transaction_id {
230            return Err(Error::UdpProtocol(format!(
231                "transaction ID mismatch: expected {expected_transaction_id}, got {transaction_id}"
232            )));
233        }
234
235        let connection_id = u64::from_be_bytes([
236            data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
237        ]);
238
239        Ok(connection_id)
240    }
241
242    /// Build a UDP announce request packet (BEP 15).
243    #[must_use]
244    pub fn build_announce_request(
245        connection_id: u64,
246        transaction_id: u32,
247        req: &AnnounceRequest,
248    ) -> Vec<u8> {
249        let mut buf = Vec::with_capacity(98);
250        buf.extend_from_slice(&connection_id.to_be_bytes());
251        buf.extend_from_slice(&ACTION_ANNOUNCE.to_be_bytes());
252        buf.extend_from_slice(&transaction_id.to_be_bytes());
253        buf.extend_from_slice(req.info_hash.as_bytes());
254        buf.extend_from_slice(req.peer_id.as_bytes());
255        buf.extend_from_slice(&req.downloaded.to_be_bytes());
256        buf.extend_from_slice(&req.left.to_be_bytes());
257        buf.extend_from_slice(&req.uploaded.to_be_bytes());
258        buf.extend_from_slice(&(req.event as u32).to_be_bytes());
259        buf.extend_from_slice(&0u32.to_be_bytes()); // IP address (0 = default)
260        buf.extend_from_slice(&0u32.to_be_bytes()); // key (random)
261        buf.extend_from_slice(&req.num_want.unwrap_or(-1i32).to_be_bytes());
262        buf.extend_from_slice(&req.port.to_be_bytes());
263        buf
264    }
265
266    /// Parse a UDP announce response.
267    ///
268    /// # Errors
269    ///
270    /// Returns an error if the response is too short or has a wrong transaction ID.
271    pub fn parse_announce_response(
272        data: &[u8],
273        expected_transaction_id: u32,
274    ) -> Result<UdpAnnounceResponse> {
275        if data.len() < 20 {
276            return Err(Error::UdpProtocol(format!(
277                "announce response too short: {} bytes",
278                data.len()
279            )));
280        }
281
282        let action = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
283        if action != ACTION_ANNOUNCE {
284            // Check for error action (3)
285            if action == 3 && data.len() > 8 {
286                let msg = String::from_utf8_lossy(&data[8..]);
287                return Err(Error::TrackerError {
288                    message: msg.into_owned(),
289                    retry_in: None,
290                });
291            }
292            return Err(Error::UdpProtocol(format!(
293                "expected action 1 (announce), got {action}"
294            )));
295        }
296
297        let transaction_id = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
298        if transaction_id != expected_transaction_id {
299            return Err(Error::UdpProtocol(format!(
300                "transaction ID mismatch: expected {expected_transaction_id}, got {transaction_id}"
301            )));
302        }
303
304        let interval = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
305        let leechers = u32::from_be_bytes([data[12], data[13], data[14], data[15]]);
306        let seeders = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
307
308        // BEP 41: Peer list runs from byte 20 to the largest 6-byte boundary.
309        // Any remaining bytes are BEP 41 extension options.
310        let peer_data = &data[20..];
311        let peer_size = 6; // IPv4 compact: 4 IP + 2 port
312        let num_peers = peer_data.len() / peer_size;
313        let peers_end = num_peers * peer_size;
314        let peers = parse_compact_peers(&peer_data[..peers_end])?;
315        let options = if peers_end < peer_data.len() {
316            parse_udp_options(&peer_data[peers_end..])
317        } else {
318            Vec::new()
319        };
320
321        Ok(UdpAnnounceResponse {
322            response: AnnounceResponse {
323                interval,
324                seeders: Some(seeders),
325                leechers: Some(leechers),
326                peers,
327            },
328            transaction_id,
329            options,
330        })
331    }
332
333    /// Parse a UDP announce response where the tracker address is IPv6.
334    ///
335    /// Per BEP 15, the compact peer format matches the address family of the
336    /// tracker endpoint: 18-byte entries (16 IP + 2 port) for IPv6 trackers.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error on short data, action mismatch, transaction ID mismatch,
341    /// or malformed compact peer entries.
342    pub fn parse_announce_response_v6(
343        data: &[u8],
344        expected_transaction_id: u32,
345    ) -> Result<UdpAnnounceResponse> {
346        if data.len() < 20 {
347            return Err(Error::UdpProtocol(format!(
348                "announce response too short: {} bytes",
349                data.len()
350            )));
351        }
352
353        let action = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
354        if action != ACTION_ANNOUNCE {
355            if action == 3 && data.len() > 8 {
356                let msg = String::from_utf8_lossy(&data[8..]);
357                return Err(Error::TrackerError {
358                    message: msg.into_owned(),
359                    retry_in: None,
360                });
361            }
362            return Err(Error::UdpProtocol(format!(
363                "expected action 1 (announce), got {action}"
364            )));
365        }
366
367        let transaction_id = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
368        if transaction_id != expected_transaction_id {
369            return Err(Error::UdpProtocol(format!(
370                "transaction ID mismatch: expected {expected_transaction_id}, got {transaction_id}"
371            )));
372        }
373
374        let interval = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
375        let leechers = u32::from_be_bytes([data[12], data[13], data[14], data[15]]);
376        let seeders = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
377
378        // BEP 41: Peer list runs from byte 20 to the largest 18-byte boundary.
379        // Any remaining bytes are BEP 41 extension options.
380        let peer_data = &data[20..];
381        let peer_size = 18; // IPv6 compact: 16 IP + 2 port
382        let num_peers = peer_data.len() / peer_size;
383        let peers_end = num_peers * peer_size;
384        let peers = parse_compact_peers6(&peer_data[..peers_end])?;
385        let options = if peers_end < peer_data.len() {
386            parse_udp_options(&peer_data[peers_end..])
387        } else {
388            Vec::new()
389        };
390
391        Ok(UdpAnnounceResponse {
392            response: AnnounceResponse {
393                interval,
394                seeders: Some(seeders),
395                leechers: Some(leechers),
396                peers,
397            },
398            transaction_id,
399            options,
400        })
401    }
402
403    /// Perform a full UDP announce (connect + announce).
404    ///
405    /// # Errors
406    ///
407    /// Returns an error on DNS resolution failure, socket I/O errors, timeout,
408    /// or protocol-level errors in the connect/announce exchange.
409    pub async fn announce(
410        &self,
411        tracker_addr: &str,
412        req: &AnnounceRequest,
413    ) -> Result<UdpAnnounceResponse> {
414        // Resolve hostname:port — supports both "1.2.3.4:6969" and "tracker.example.com:6969"
415        let addr: SocketAddr = match tracker_addr.parse() {
416            Ok(sa) => sa,
417            Err(_) => tokio::net::lookup_host(tracker_addr)
418                .await
419                .map_err(|e| {
420                    Error::InvalidUrl(format!("DNS lookup failed for {tracker_addr}: {e}"))
421                })?
422                .next()
423                .ok_or_else(|| Error::InvalidUrl(format!("no addresses for {tracker_addr}")))?,
424        };
425
426        let bind_addr = if addr.is_ipv6() {
427            "[::]:0"
428        } else {
429            "0.0.0.0:0"
430        };
431        let socket = UdpSocket::bind(bind_addr).await?;
432        apply_dscp_udp(&socket, self.dscp, addr.is_ipv6());
433        socket.connect(addr).await?;
434
435        // Step 1: Connect
436        let txn_id = generate_transaction_id();
437        let connect_req = Self::build_connect_request(txn_id);
438        socket.send(&connect_req).await?;
439
440        let mut buf = [0u8; 2048];
441        let n = tokio::time::timeout(self.timeout, socket.recv(&mut buf))
442            .await
443            .map_err(|_| Error::Timeout)??;
444
445        let connection_id = Self::parse_connect_response(&buf[..n], txn_id)?;
446
447        // Step 2: Announce
448        let txn_id2 = generate_transaction_id();
449        let announce_req = Self::build_announce_request(connection_id, txn_id2, req);
450        socket.send(&announce_req).await?;
451
452        let n = tokio::time::timeout(self.timeout, socket.recv(&mut buf))
453            .await
454            .map_err(|_| Error::Timeout)??;
455
456        if addr.is_ipv6() {
457            Self::parse_announce_response_v6(&buf[..n], txn_id2)
458        } else {
459            Self::parse_announce_response(&buf[..n], txn_id2)
460        }
461    }
462
463    /// Build a UDP scrape request packet (BEP 15).
464    ///
465    /// Format: `connection_id(8)` + action(4, value=2) + `transaction_id(4)` + `info_hash(20)`*N
466    #[must_use]
467    pub fn build_scrape_request(
468        connection_id: u64,
469        transaction_id: u32,
470        info_hashes: &[Id20],
471    ) -> Vec<u8> {
472        let mut buf = Vec::with_capacity(16 + 20 * info_hashes.len());
473        buf.extend_from_slice(&connection_id.to_be_bytes());
474        buf.extend_from_slice(&ACTION_SCRAPE.to_be_bytes());
475        buf.extend_from_slice(&transaction_id.to_be_bytes());
476        for hash in info_hashes {
477            buf.extend_from_slice(hash.as_bytes());
478        }
479        buf
480    }
481
482    /// Parse a UDP scrape response.
483    ///
484    /// Format: action(4) + `transaction_id(4)` + [seeders(4) + completed(4) + leechers(4)]*N
485    ///
486    /// # Errors
487    ///
488    /// Returns an error on short data, action mismatch, transaction ID mismatch,
489    /// or payload length not divisible by 12.
490    pub fn parse_scrape_response(
491        data: &[u8],
492        expected_transaction_id: u32,
493    ) -> Result<UdpScrapeResponse> {
494        if data.len() < 8 {
495            return Err(Error::UdpProtocol(format!(
496                "scrape response too short: {} bytes",
497                data.len()
498            )));
499        }
500
501        let action = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
502        if action == 3 && data.len() > 8 {
503            let msg = String::from_utf8_lossy(&data[8..]);
504            return Err(Error::TrackerError {
505                message: msg.into_owned(),
506                retry_in: None,
507            });
508        }
509        if action != ACTION_SCRAPE {
510            return Err(Error::UdpProtocol(format!(
511                "expected action 2 (scrape), got {action}"
512            )));
513        }
514
515        let transaction_id = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
516        if transaction_id != expected_transaction_id {
517            return Err(Error::UdpProtocol(format!(
518                "transaction ID mismatch: expected {expected_transaction_id}, got {transaction_id}"
519            )));
520        }
521
522        let payload = &data[8..];
523        if !payload.len().is_multiple_of(12) {
524            return Err(Error::UdpProtocol(format!(
525                "scrape payload not divisible by 12: {} bytes",
526                payload.len()
527            )));
528        }
529
530        let mut results = Vec::with_capacity(payload.len() / 12);
531        for chunk in payload.chunks_exact(12) {
532            let complete = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
533            let downloaded = u32::from_be_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
534            let incomplete = u32::from_be_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]);
535            results.push(ScrapeInfo {
536                complete,
537                incomplete,
538                downloaded,
539            });
540        }
541
542        Ok(UdpScrapeResponse {
543            results,
544            transaction_id,
545        })
546    }
547
548    /// Perform a full UDP scrape (connect + scrape).
549    ///
550    /// # Errors
551    ///
552    /// Returns an error on DNS resolution failure, socket I/O errors, timeout,
553    /// or protocol-level errors in the connect/scrape exchange.
554    pub async fn scrape(
555        &self,
556        tracker_addr: &str,
557        info_hashes: &[Id20],
558    ) -> Result<UdpScrapeResponse> {
559        // Resolve hostname:port — supports both "1.2.3.4:6969" and "tracker.example.com:6969"
560        let addr: SocketAddr = match tracker_addr.parse() {
561            Ok(sa) => sa,
562            Err(_) => tokio::net::lookup_host(tracker_addr)
563                .await
564                .map_err(|e| {
565                    Error::InvalidUrl(format!("DNS lookup failed for {tracker_addr}: {e}"))
566                })?
567                .next()
568                .ok_or_else(|| Error::InvalidUrl(format!("no addresses for {tracker_addr}")))?,
569        };
570
571        let bind_addr = if addr.is_ipv6() {
572            "[::]:0"
573        } else {
574            "0.0.0.0:0"
575        };
576        let socket = UdpSocket::bind(bind_addr).await?;
577        apply_dscp_udp(&socket, self.dscp, addr.is_ipv6());
578        socket.connect(addr).await?;
579
580        // Step 1: Connect
581        let txn_id = generate_transaction_id();
582        let connect_req = Self::build_connect_request(txn_id);
583        socket.send(&connect_req).await?;
584
585        let mut buf = [0u8; 2048];
586        let n = tokio::time::timeout(self.timeout, socket.recv(&mut buf))
587            .await
588            .map_err(|_| Error::Timeout)??;
589
590        let connection_id = Self::parse_connect_response(&buf[..n], txn_id)?;
591
592        // Step 2: Scrape
593        let txn_id2 = generate_transaction_id();
594        let scrape_req = Self::build_scrape_request(connection_id, txn_id2, info_hashes);
595        socket.send(&scrape_req).await?;
596
597        let n = tokio::time::timeout(self.timeout, socket.recv(&mut buf))
598            .await
599            .map_err(|_| Error::Timeout)??;
600
601        Self::parse_scrape_response(&buf[..n], txn_id2)
602    }
603}
604
605impl Default for UdpTracker {
606    fn default() -> Self {
607        Self::new()
608    }
609}
610
611fn generate_transaction_id() -> u32 {
612    use std::time::SystemTime;
613    SystemTime::now()
614        .duration_since(SystemTime::UNIX_EPOCH)
615        .unwrap_or_default()
616        .subsec_nanos()
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::AnnounceEvent;
623    use irontide_core::Id20;
624
625    fn test_request() -> AnnounceRequest {
626        AnnounceRequest {
627            info_hash: Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
628            peer_id: Id20::from_hex("0102030405060708091011121314151617181920").unwrap(),
629            port: 6881,
630            uploaded: 0,
631            downloaded: 0,
632            left: 1_000_000,
633            event: AnnounceEvent::Started,
634            num_want: Some(50),
635            compact: true,
636            i2p_destination: None,
637        }
638    }
639
640    #[test]
641    fn connect_request_format() {
642        let req = UdpTracker::build_connect_request(12345);
643        assert_eq!(req.len(), 16);
644        // Magic connection ID
645        assert_eq!(
646            u64::from_be_bytes(req[0..8].try_into().unwrap()),
647            CONNECT_MAGIC
648        );
649        // Action = 0
650        assert_eq!(u32::from_be_bytes(req[8..12].try_into().unwrap()), 0);
651        // Transaction ID
652        assert_eq!(u32::from_be_bytes(req[12..16].try_into().unwrap()), 12345);
653    }
654
655    #[test]
656    fn connect_response_parse() {
657        let mut resp = [0u8; 16];
658        resp[0..4].copy_from_slice(&0u32.to_be_bytes()); // action = connect
659        resp[4..8].copy_from_slice(&12345u32.to_be_bytes()); // txn id
660        resp[8..16].copy_from_slice(&99999u64.to_be_bytes()); // connection id
661
662        let conn_id = UdpTracker::parse_connect_response(&resp, 12345).unwrap();
663        assert_eq!(conn_id, 99999);
664    }
665
666    #[test]
667    fn connect_response_wrong_txn() {
668        let mut resp = [0u8; 16];
669        resp[0..4].copy_from_slice(&0u32.to_be_bytes());
670        resp[4..8].copy_from_slice(&12345u32.to_be_bytes());
671        resp[8..16].copy_from_slice(&99999u64.to_be_bytes());
672
673        assert!(UdpTracker::parse_connect_response(&resp, 99999).is_err());
674    }
675
676    #[test]
677    fn announce_request_format() {
678        let req = test_request();
679        let data = UdpTracker::build_announce_request(42, 100, &req);
680        assert_eq!(data.len(), 98);
681
682        // Connection ID
683        assert_eq!(u64::from_be_bytes(data[0..8].try_into().unwrap()), 42);
684        // Action = announce
685        assert_eq!(u32::from_be_bytes(data[8..12].try_into().unwrap()), 1);
686        // Transaction ID
687        assert_eq!(u32::from_be_bytes(data[12..16].try_into().unwrap()), 100);
688        // Port at the end
689        assert_eq!(u16::from_be_bytes(data[96..98].try_into().unwrap()), 6881);
690    }
691
692    #[test]
693    fn announce_response_parse() {
694        let mut resp = Vec::new();
695        resp.extend_from_slice(&1u32.to_be_bytes()); // action = announce
696        resp.extend_from_slice(&42u32.to_be_bytes()); // txn id
697        resp.extend_from_slice(&1800u32.to_be_bytes()); // interval
698        resp.extend_from_slice(&5u32.to_be_bytes()); // leechers
699        resp.extend_from_slice(&10u32.to_be_bytes()); // seeders
700        // One peer: 192.168.1.1:6881
701        resp.extend_from_slice(&[192, 168, 1, 1, 0x1A, 0xE1]);
702
703        let parsed = UdpTracker::parse_announce_response(&resp, 42).unwrap();
704        assert_eq!(parsed.response.interval, 1800);
705        assert_eq!(parsed.response.seeders, Some(10));
706        assert_eq!(parsed.response.leechers, Some(5));
707        assert_eq!(parsed.response.peers.len(), 1);
708        assert_eq!(parsed.response.peers[0].to_string(), "192.168.1.1:6881");
709    }
710
711    #[test]
712    fn announce_response_error() {
713        let mut resp = Vec::new();
714        resp.extend_from_slice(&3u32.to_be_bytes()); // action = error
715        resp.extend_from_slice(&42u32.to_be_bytes()); // txn id
716        resp.extend_from_slice(b"torrent not found");
717
718        let result = UdpTracker::parse_announce_response(&resp, 42);
719        assert!(result.is_err());
720    }
721
722    #[test]
723    fn connect_response_too_short() {
724        assert!(UdpTracker::parse_connect_response(&[0u8; 10], 0).is_err());
725    }
726
727    #[test]
728    fn scrape_request_format() {
729        let hash1 = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
730        let hash2 = Id20::from_hex("0102030405060708091011121314151617181920").unwrap();
731        let data = UdpTracker::build_scrape_request(42, 100, &[hash1, hash2]);
732        assert_eq!(data.len(), 16 + 40); // header + 2 hashes
733        assert_eq!(u64::from_be_bytes(data[0..8].try_into().unwrap()), 42);
734        assert_eq!(u32::from_be_bytes(data[8..12].try_into().unwrap()), 2); // action=scrape
735        assert_eq!(u32::from_be_bytes(data[12..16].try_into().unwrap()), 100);
736        assert_eq!(&data[16..36], hash1.as_bytes());
737        assert_eq!(&data[36..56], hash2.as_bytes());
738    }
739
740    #[test]
741    fn scrape_response_parse() {
742        let mut resp = Vec::new();
743        resp.extend_from_slice(&2u32.to_be_bytes()); // action = scrape
744        resp.extend_from_slice(&42u32.to_be_bytes()); // txn id
745        // seeders=10, completed=50, leechers=3
746        resp.extend_from_slice(&10u32.to_be_bytes());
747        resp.extend_from_slice(&50u32.to_be_bytes());
748        resp.extend_from_slice(&3u32.to_be_bytes());
749
750        let parsed = UdpTracker::parse_scrape_response(&resp, 42).unwrap();
751        assert_eq!(parsed.results.len(), 1);
752        assert_eq!(parsed.results[0].complete, 10);
753        assert_eq!(parsed.results[0].downloaded, 50);
754        assert_eq!(parsed.results[0].incomplete, 3);
755    }
756
757    #[test]
758    fn scrape_response_multiple_hashes() {
759        let mut resp = Vec::new();
760        resp.extend_from_slice(&2u32.to_be_bytes());
761        resp.extend_from_slice(&42u32.to_be_bytes());
762        // Hash 1: seeders=10, completed=50, leechers=3
763        resp.extend_from_slice(&10u32.to_be_bytes());
764        resp.extend_from_slice(&50u32.to_be_bytes());
765        resp.extend_from_slice(&3u32.to_be_bytes());
766        // Hash 2: seeders=20, completed=100, leechers=5
767        resp.extend_from_slice(&20u32.to_be_bytes());
768        resp.extend_from_slice(&100u32.to_be_bytes());
769        resp.extend_from_slice(&5u32.to_be_bytes());
770
771        let parsed = UdpTracker::parse_scrape_response(&resp, 42).unwrap();
772        assert_eq!(parsed.results.len(), 2);
773        assert_eq!(parsed.results[1].complete, 20);
774        assert_eq!(parsed.results[1].downloaded, 100);
775        assert_eq!(parsed.results[1].incomplete, 5);
776    }
777
778    #[test]
779    fn scrape_response_wrong_action() {
780        let mut resp = Vec::new();
781        resp.extend_from_slice(&1u32.to_be_bytes()); // action = announce (wrong)
782        resp.extend_from_slice(&42u32.to_be_bytes());
783
784        let result = UdpTracker::parse_scrape_response(&resp, 42);
785        assert!(result.is_err());
786    }
787
788    #[test]
789    fn scrape_response_too_short() {
790        let result = UdpTracker::parse_scrape_response(&[0u8; 4], 0);
791        assert!(result.is_err());
792    }
793
794    #[test]
795    fn announce_response_v6_parse() {
796        use std::net::Ipv6Addr;
797        let mut resp = Vec::new();
798        resp.extend_from_slice(&1u32.to_be_bytes()); // action = announce
799        resp.extend_from_slice(&42u32.to_be_bytes()); // txn id
800        resp.extend_from_slice(&1800u32.to_be_bytes()); // interval
801        resp.extend_from_slice(&5u32.to_be_bytes()); // leechers
802        resp.extend_from_slice(&10u32.to_be_bytes()); // seeders
803        // One IPv6 peer: [2001:db8::1]:6881
804        let ip: Ipv6Addr = "2001:db8::1".parse().unwrap();
805        resp.extend_from_slice(&ip.octets());
806        resp.extend_from_slice(&6881u16.to_be_bytes());
807
808        let parsed = UdpTracker::parse_announce_response_v6(&resp, 42).unwrap();
809        assert_eq!(parsed.response.interval, 1800);
810        assert_eq!(parsed.response.peers.len(), 1);
811        assert_eq!(
812            parsed.response.peers[0],
813            "[2001:db8::1]:6881".parse::<SocketAddr>().unwrap()
814        );
815    }
816
817    #[test]
818    fn udp_tracker_dscp_builder() {
819        let tracker = UdpTracker::new().with_dscp(0x2E);
820        assert_eq!(tracker.dscp, 0x2E);
821    }
822
823    #[test]
824    fn udp_tracker_default_no_dscp() {
825        let tracker = UdpTracker::new();
826        assert_eq!(tracker.dscp, 0);
827    }
828
829    // --- BEP 41: UDP tracker protocol extension tests ---
830
831    #[test]
832    fn parse_udp_options_empty() {
833        let options = parse_udp_options(&[]);
834        assert!(options.is_empty());
835    }
836
837    #[test]
838    fn parse_udp_options_end_of_options() {
839        let options = parse_udp_options(&[0x00]);
840        assert_eq!(options, vec![UdpTrackerOption::EndOfOptions]);
841    }
842
843    #[test]
844    fn parse_udp_options_nop_and_end() {
845        let options = parse_udp_options(&[0x01, 0x00]);
846        assert_eq!(
847            options,
848            vec![UdpTrackerOption::Nop, UdpTrackerOption::EndOfOptions]
849        );
850    }
851
852    #[test]
853    fn parse_udp_options_url_data() {
854        // Type 0x02, length 11, "example.com"
855        let mut data = vec![0x02, 11];
856        data.extend_from_slice(b"example.com");
857        let options = parse_udp_options(&data);
858        assert_eq!(
859            options,
860            vec![UdpTrackerOption::UrlData("example.com".to_owned())]
861        );
862    }
863
864    #[test]
865    fn parse_udp_options_unknown_type() {
866        // Type 0x03 (unknown), length 3, payload [0xAA, 0xBB, 0xCC]
867        let data = vec![0x03, 3, 0xAA, 0xBB, 0xCC];
868        let options = parse_udp_options(&data);
869        assert_eq!(
870            options,
871            vec![UdpTrackerOption::Unknown {
872                option_type: 0x03,
873                data: vec![0xAA, 0xBB, 0xCC],
874            }]
875        );
876    }
877
878    #[test]
879    fn parse_udp_options_two_byte_length() {
880        // Type 0x80 uses 2-byte big-endian length
881        let payload = b"hello";
882        let mut data = vec![0x80];
883        data.extend_from_slice(&(payload.len() as u16).to_be_bytes());
884        data.extend_from_slice(payload);
885        let options = parse_udp_options(&data);
886        assert_eq!(
887            options,
888            vec![UdpTrackerOption::Unknown {
889                option_type: 0x80,
890                data: payload.to_vec(),
891            }]
892        );
893    }
894
895    #[test]
896    fn announce_response_with_trailing_options() {
897        // Build: 20-byte header + 12 bytes peers (2 IPv4) + BEP 41 options.
898        // The options trailer MUST be < 6 bytes (peer entry size) because the
899        // parser determines the peer/option boundary as the largest multiple of
900        // the peer entry size within the remaining data.
901        let mut resp = Vec::new();
902        resp.extend_from_slice(&1u32.to_be_bytes()); // action = announce
903        resp.extend_from_slice(&42u32.to_be_bytes()); // txn id
904        resp.extend_from_slice(&60u32.to_be_bytes()); // interval
905        resp.extend_from_slice(&1u32.to_be_bytes()); // leechers
906        resp.extend_from_slice(&2u32.to_be_bytes()); // seeders
907        // Peer 1: 10.0.0.1:8080
908        resp.extend_from_slice(&[10, 0, 0, 1, 0x1F, 0x90]);
909        // Peer 2: 192.168.1.1:6881
910        resp.extend_from_slice(&[192, 168, 1, 1, 0x1A, 0xE1]);
911        // BEP 41 options (5 bytes, < 6): URLData "te" + EndOfOptions
912        resp.extend_from_slice(&[0x02, 0x02]); // type=URLData, length=2
913        resp.extend_from_slice(b"te");
914        resp.push(0x00); // EndOfOptions
915        assert_eq!(resp.len(), 37); // 20 + 12 + 5
916
917        let parsed = UdpTracker::parse_announce_response(&resp, 42).unwrap();
918        assert_eq!(parsed.response.interval, 60);
919        assert_eq!(parsed.response.seeders, Some(2));
920        assert_eq!(parsed.response.leechers, Some(1));
921        assert_eq!(parsed.response.peers.len(), 2);
922        assert_eq!(parsed.response.peers[0].to_string(), "10.0.0.1:8080");
923        assert_eq!(parsed.response.peers[1].to_string(), "192.168.1.1:6881");
924        assert_eq!(
925            parsed.options,
926            vec![
927                UdpTrackerOption::UrlData("te".to_owned()),
928                UdpTrackerOption::EndOfOptions,
929            ]
930        );
931    }
932
933    #[test]
934    fn announce_response_no_options() {
935        // Standard response with exact peer-list boundary — no trailing bytes.
936        let mut resp = Vec::new();
937        resp.extend_from_slice(&1u32.to_be_bytes()); // action = announce
938        resp.extend_from_slice(&42u32.to_be_bytes()); // txn id
939        resp.extend_from_slice(&1800u32.to_be_bytes()); // interval
940        resp.extend_from_slice(&5u32.to_be_bytes()); // leechers
941        resp.extend_from_slice(&10u32.to_be_bytes()); // seeders
942        // One peer: 192.168.1.1:6881
943        resp.extend_from_slice(&[192, 168, 1, 1, 0x1A, 0xE1]);
944
945        let parsed = UdpTracker::parse_announce_response(&resp, 42).unwrap();
946        assert_eq!(parsed.response.peers.len(), 1);
947        assert!(parsed.options.is_empty());
948    }
949}