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
19fn 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#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum UdpTrackerOption {
63 EndOfOptions,
65 Nop,
67 UrlData(String),
69 Unknown {
71 option_type: u8,
73 data: Vec<u8>,
75 },
76}
77
78fn 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 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
138const CONNECT_MAGIC: u64 = 0x0417_2710_1980;
140const ACTION_CONNECT: u32 = 0;
141const ACTION_ANNOUNCE: u32 = 1;
142const ACTION_SCRAPE: u32 = 2;
143
144const UDP_TIMEOUT: Duration = Duration::from_secs(15);
146
147#[derive(Clone)]
149pub struct UdpTracker {
150 timeout: Duration,
151 dscp: u8,
152}
153
154#[derive(Debug, Clone)]
156pub struct UdpAnnounceResponse {
157 pub response: AnnounceResponse,
159 pub transaction_id: u32,
161 pub options: Vec<UdpTrackerOption>,
163}
164
165#[derive(Debug, Clone)]
167pub struct UdpScrapeResponse {
168 pub results: Vec<ScrapeInfo>,
170 pub transaction_id: u32,
172}
173
174impl UdpTracker {
175 #[must_use]
177 pub fn new() -> Self {
178 Self {
179 timeout: UDP_TIMEOUT,
180 dscp: 0,
181 }
182 }
183
184 #[must_use]
186 pub fn with_timeout(mut self, timeout: Duration) -> Self {
187 self.timeout = timeout;
188 self
189 }
190
191 #[must_use]
193 pub fn with_dscp(mut self, dscp: u8) -> Self {
194 self.dscp = dscp;
195 self
196 }
197
198 #[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 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 #[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()); buf.extend_from_slice(&0u32.to_be_bytes()); 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 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 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 let peer_data = &data[20..];
311 let peer_size = 6; 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 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 let peer_data = &data[20..];
381 let peer_size = 18; 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 pub async fn announce(
410 &self,
411 tracker_addr: &str,
412 req: &AnnounceRequest,
413 ) -> Result<UdpAnnounceResponse> {
414 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 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 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 #[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 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 pub async fn scrape(
555 &self,
556 tracker_addr: &str,
557 info_hashes: &[Id20],
558 ) -> Result<UdpScrapeResponse> {
559 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 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 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 assert_eq!(
646 u64::from_be_bytes(req[0..8].try_into().unwrap()),
647 CONNECT_MAGIC
648 );
649 assert_eq!(u32::from_be_bytes(req[8..12].try_into().unwrap()), 0);
651 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()); resp[4..8].copy_from_slice(&12345u32.to_be_bytes()); resp[8..16].copy_from_slice(&99999u64.to_be_bytes()); 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 assert_eq!(u64::from_be_bytes(data[0..8].try_into().unwrap()), 42);
684 assert_eq!(u32::from_be_bytes(data[8..12].try_into().unwrap()), 1);
686 assert_eq!(u32::from_be_bytes(data[12..16].try_into().unwrap()), 100);
688 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()); resp.extend_from_slice(&42u32.to_be_bytes()); resp.extend_from_slice(&1800u32.to_be_bytes()); resp.extend_from_slice(&5u32.to_be_bytes()); resp.extend_from_slice(&10u32.to_be_bytes()); 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()); resp.extend_from_slice(&42u32.to_be_bytes()); 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); 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); 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()); resp.extend_from_slice(&42u32.to_be_bytes()); 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 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 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()); 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()); resp.extend_from_slice(&42u32.to_be_bytes()); resp.extend_from_slice(&1800u32.to_be_bytes()); resp.extend_from_slice(&5u32.to_be_bytes()); resp.extend_from_slice(&10u32.to_be_bytes()); 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 #[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 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 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 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 let mut resp = Vec::new();
902 resp.extend_from_slice(&1u32.to_be_bytes()); resp.extend_from_slice(&42u32.to_be_bytes()); resp.extend_from_slice(&60u32.to_be_bytes()); resp.extend_from_slice(&1u32.to_be_bytes()); resp.extend_from_slice(&2u32.to_be_bytes()); resp.extend_from_slice(&[10, 0, 0, 1, 0x1F, 0x90]);
909 resp.extend_from_slice(&[192, 168, 1, 1, 0x1A, 0xE1]);
911 resp.extend_from_slice(&[0x02, 0x02]); resp.extend_from_slice(b"te");
914 resp.push(0x00); assert_eq!(resp.len(), 37); 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 let mut resp = Vec::new();
937 resp.extend_from_slice(&1u32.to_be_bytes()); resp.extend_from_slice(&42u32.to_be_bytes()); resp.extend_from_slice(&1800u32.to_be_bytes()); resp.extend_from_slice(&5u32.to_be_bytes()); resp.extend_from_slice(&10u32.to_be_bytes()); 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}