Skip to main content

irontide_tracker/
udp.rs

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