1use std::collections::{HashMap, HashSet};
26use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29
30use tokio::sync::RwLock;
31use tracing::{debug, info, warn};
32
33use crate::constants;
34
35pub use constants::{
37 LPD_DEFAULT_ANNOUNCE_INTERVAL_SECS as DEFAULT_ANNOUNCE_INTERVAL_SECS,
38 LPD_MULTICAST_ADDRESS as LPD_MULTICAST_ADDR, LPD_PORT,
39};
40
41pub const MAX_PEERS_PER_HASH: usize = 50;
47
48#[derive(Debug, Clone)]
54pub struct LpdPeer {
55 pub info_hash: String,
57 pub port: u16,
59 pub addr: IpAddr,
61 pub last_seen: Instant,
63 pub token: Option<u32>,
65}
66
67impl LpdPeer {
68 pub fn new(info_hash: impl Into<String>, port: u16, addr: IpAddr) -> Self {
70 Self {
71 info_hash: info_hash.into(),
72 port,
73 addr,
74 last_seen: Instant::now(),
75 token: None,
76 }
77 }
78
79 pub fn with_token(info_hash: impl Into<String>, port: u16, addr: IpAddr, token: u32) -> Self {
81 let mut p = Self::new(info_hash, port, addr);
82 p.token = Some(token);
83 p
84 }
85
86 pub fn socket_addr(&self) -> SocketAddr {
88 SocketAddr::new(self.addr, self.port)
89 }
90
91 pub fn is_expired(&self, max_age: Duration) -> bool {
93 self.last_seen.elapsed() > max_age
94 }
95}
96
97impl PartialEq for LpdPeer {
98 fn eq(&self, other: &Self) -> bool {
99 self.info_hash == other.info_hash && self.addr == other.addr
101 }
102}
103
104impl Eq for LpdPeer {}
105
106impl std::hash::Hash for LpdPeer {
107 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
108 self.info_hash.hash(state);
109 self.addr.hash(state);
110 }
111}
112
113pub struct LpdAnnouncer {
127 socket: UdpSocket,
129 multicast_addr: SocketAddr,
131 enabled: bool,
133 announce_interval: Duration,
135}
136
137impl LpdAnnouncer {
138 pub fn new() -> Result<Self, String> {
150 Self::with_config(constants::LPD_DEFAULT_ANNOUNCE_INTERVAL_SECS)
151 }
152
153 pub fn with_config(announce_interval_secs: u64) -> Result<Self, String> {
155 let socket = UdpSocket::bind("0.0.0.0:0")
157 .map_err(|e| format!("Failed to bind UDP socket: {}", e))?;
158
159 socket
161 .set_broadcast(true)
162 .map_err(|e| format!("Failed to enable broadcast: {}", e))?;
163
164 #[cfg(unix)]
166 {
167 use std::os::unix::io::AsRawFd;
168 let fd = socket.as_raw_fd();
169 unsafe {
170 libc::setsockopt(
171 fd,
172 libc::SOL_SOCKET,
173 libc::SO_REUSEADDR,
174 &1i32 as *const i32 as *const libc::c_void,
175 std::mem::size_of::<i32>() as libc::socklen_t,
176 );
177 }
178 }
179
180 let multicast_addr: SocketAddr = format!(
182 "{}:{}",
183 constants::LPD_MULTICAST_ADDRESS,
184 constants::LPD_PORT
185 )
186 .parse()
187 .map_err(|e| format!("Invalid LPD multicast address: {}", e))?;
188
189 let multicast_ip: Ipv4Addr = constants::LPD_MULTICAST_ADDRESS
191 .parse()
192 .map_err(|e| format!("Invalid LPD multicast IP: {}", e))?;
193
194 socket
195 .join_multicast_v4(&multicast_ip, &Ipv4Addr::UNSPECIFIED)
196 .map_err(|e| format!("Failed to join LPD multicast group: {}", e))?;
197
198 debug!(
199 local = ?socket.local_addr().ok(),
200 multicast = %multicast_addr,
201 "LpdAnnouncer created successfully"
202 );
203
204 Ok(Self {
205 socket,
206 multicast_addr,
207 enabled: true,
208 announce_interval: Duration::from_secs(announce_interval_secs),
209 })
210 }
211
212 pub fn disable(&mut self) {
214 self.enabled = false;
215 }
216
217 pub fn enable(&mut self) {
219 self.enabled = true;
220 }
221
222 pub fn is_enabled(&self) -> bool {
224 self.enabled
225 }
226
227 pub fn local_addr(&self) -> Result<SocketAddr, String> {
229 self.socket
230 .local_addr()
231 .map_err(|e| format!("Failed to get local address: {}", e))
232 }
233
234 pub fn announce(&self, info_hash: &str, port: u16) -> Result<(), String> {
250 if !self.enabled {
251 return Ok(()); }
253
254 if info_hash.len() != 40 || !info_hash.chars().all(|c| c.is_ascii_hexdigit()) {
256 return Err(format!(
257 "Invalid info_hash format: expected 40 hex chars, got {} chars",
258 info_hash.len()
259 ));
260 }
261
262 let token: u32 = rand::random::<u32>();
264
265 let msg = format!(
267 "Hash: {}\nPort: {}\nToken: {:08x}\n",
268 info_hash, port, token
269 );
270
271 debug!(
272 info_hash = %&info_hash[..8],
273 port,
274 token = format!("{:08x}", token),
275 "Sending LPD announcement"
276 );
277
278 self.socket
279 .send_to(msg.as_bytes(), self.multicast_addr)
280 .map_err(|e| format!("LPD announce send failed: {}", e))?;
281
282 Ok(())
283 }
284
285 pub fn receive_announcements(&self, timeout: Duration) -> Vec<LpdPeer> {
298 let mut buf = [0u8; constants::LPD_RECEIVE_BUFFER_SIZE];
299 let mut peers = Vec::new();
300 let mut seen: HashSet<(String, IpAddr)> = HashSet::new();
301
302 self.socket
303 .set_read_timeout(Some(timeout))
304 .expect("set_read_timeout should not fail");
305
306 let deadline = Instant::now() + timeout;
307
308 loop {
309 let remaining = deadline.saturating_duration_since(Instant::now());
310 if remaining.is_zero() {
311 break;
312 }
313
314 match self.socket.recv_from(&mut buf) {
315 Ok((len, src_addr)) => {
316 if len == 0 {
317 continue;
318 }
319
320 if let Some(peer) = parse_lpd_announcement(&buf[..len], src_addr.ip()) {
321 let key = (peer.info_hash.clone(), peer.addr);
323 if seen.insert(key) {
324 debug!(
325 info_hash = %peer.info_hash[..8.min(peer.info_hash.len())],
326 addr = %src_addr.ip(),
327 "Received valid LPD announcement"
328 );
329 peers.push(peer);
330 } else {
331 debug!(
332 addr = %src_addr.ip(),
333 "Duplicate LPD announcement suppressed"
334 );
335 }
336 }
337 }
338 Err(e) => {
339 let kind = e.kind();
340 if kind == std::io::ErrorKind::TimedOut
341 || kind == std::io::ErrorKind::WouldBlock
342 {
343 break;
344 }
345 warn!(error = %e, "LPD receive error, continuing");
347 std::thread::sleep(Duration::from_millis(10));
349 }
350 }
351 }
352
353 debug!(count = peers.len(), "LPD receive completed");
354 peers
355 }
356
357 pub fn announce_and_discover(
362 &self,
363 info_hash: &str,
364 port: u16,
365 discover_timeout: Duration,
366 ) -> Result<Vec<LpdPeer>, String> {
367 self.announce(info_hash, port)?;
369
370 std::thread::sleep(Duration::from_millis(500));
372
373 let peers = self.receive_announcements(discover_timeout);
375
376 let peers: Vec<LpdPeer> = peers
378 .into_iter()
379 .filter(|p| !(p.info_hash == info_hash && p.port == port))
380 .collect();
381
382 Ok(peers)
383 }
384}
385
386pub struct LpdManager {
395 announcer: Arc<LpdAnnouncer>,
397 peers: Arc<RwLock<HashMap<String, HashSet<LpdPeer>>>>,
399 pub active_hashes: Arc<RwLock<HashSet<String>>>,
401 _announce_task: Option<tokio::task::JoinHandle<()>>,
403}
404
405impl Default for LpdManager {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411impl LpdManager {
412 pub fn new() -> Self {
417 let announcer = LpdAnnouncer::new().unwrap_or_else(|_| {
418 warn!("Could not create LPD announcer, LPD will be disabled");
421 LpdAnnouncer::with_config(constants::LPD_DEFAULT_ANNOUNCE_INTERVAL_SECS)
422 .unwrap_or_else(|_| panic!("Fatal: cannot create LPD announcer"))
423 });
424
425 Self {
426 announcer: Arc::new(announcer),
427 peers: Arc::new(RwLock::new(HashMap::new())),
428 active_hashes: Arc::new(RwLock::new(HashSet::new())),
429 _announce_task: None,
430 }
431 }
432
433 pub fn with_interval(announce_interval_secs: u64) -> Result<Self, String> {
435 let announcer = LpdAnnouncer::with_config(announce_interval_secs)?;
436
437 Ok(Self {
438 announcer: Arc::new(announcer),
439 peers: Arc::new(RwLock::new(HashMap::new())),
440 active_hashes: Arc::new(RwLock::new(HashSet::new())),
441 _announce_task: None,
442 })
443 }
444
445 pub async fn register_torrent(&self, info_hash: &str) -> Result<(), String> {
449 let mut active = self.active_hashes.write().await;
450 active.insert(info_hash.to_string());
451
452 let mut peers_map = self.peers.write().await;
454 peers_map.entry(info_hash.to_string()).or_default();
455
456 info!(info_hash = %&info_hash[..8], "Torrent registered for LPD");
457 Ok(())
458 }
459
460 pub async fn unregister_torrent(&self, info_hash: &str) {
462 let mut active = self.active_hashes.write().await;
463 active.remove(info_hash);
464
465 info!(info_hash = %&info_hash[..8], "Torrent unregistered from LPD");
466 }
467
468 pub async fn announce_torrent(&self, info_hash: &str, port: u16) -> Result<(), String> {
470 self.announcer.announce(info_hash, port)?;
471 Ok(())
472 }
473
474 pub async fn discover_peers(&self, _info_hash: &str, timeout_ms: Option<u64>) -> Vec<LpdPeer> {
476 let timeout =
477 Duration::from_millis(timeout_ms.unwrap_or(constants::LPD_DEFAULT_RECEIVE_TIMEOUT_MS));
478 self.announcer.receive_announcements(timeout)
479 }
480
481 pub async fn get_peers_for(&self, info_hash: &str) -> Vec<LpdPeer> {
483 let peers_map = self.peers.read().await;
484 peers_map
485 .get(info_hash)
486 .map(|set| set.iter().cloned().collect())
487 .unwrap_or_default()
488 }
489
490 pub fn start_background_announce(&mut self, port: u16) -> Option<tokio::task::JoinHandle<()>> {
503 if !self.announcer.is_enabled() {
504 debug!("LPD is disabled, not starting background announce");
505 return None;
506 }
507
508 let announcer = Arc::clone(&self.announcer);
509 let active_hashes = Arc::clone(&self.active_hashes);
510
511 info!(
512 interval_secs = self.announcer.announce_interval.as_secs(),
513 port, "Starting LPD background announce task"
514 );
515
516 let handle = tokio::spawn(async move {
517 let mut ticker = tokio::time::interval(announcer.announce_interval);
518
519 loop {
520 ticker.tick().await;
521
522 let hashes: Vec<String> = {
523 let active = active_hashes.read().await;
524 active.iter().cloned().collect()
525 };
526
527 for info_hash in &hashes {
528 if let Err(e) = announcer.announce(info_hash, port) {
529 warn!(
530 info_hash = %&info_hash[..8.min(info_hash.len())],
531 error = %e,
532 "Background LPD announce failed"
533 );
534 }
535 }
536
537 debug!(
538 count = hashes.len(),
539 "LPD background announce cycle completed"
540 );
541 }
542 });
543
544 self._announce_task = Some(handle);
545 None
548 }
549
550 pub fn stop_background_announce(&mut self) {
552 if let Some(handle) = self._announce_task.take() {
553 handle.abort();
554 info!("LPD background announce task stopped");
555 }
556 }
557
558 pub async fn update_peers(&self, info_hash: &str, new_peers: Vec<LpdPeer>) {
560 let mut peers_map = self.peers.write().await;
561 let entry = peers_map.entry(info_hash.to_string()).or_default();
562
563 for peer in new_peers {
564 if entry.len() >= MAX_PEERS_PER_HASH {
566 let oldest = entry.iter().max_by_key(|p| p.last_seen.elapsed()).cloned();
568 if let Some(oldest_peer) = oldest {
569 entry.remove(&oldest_peer);
570 }
571 }
572 entry.insert(peer);
573 }
574 }
575
576 pub async fn cleanup_expired_peers(&self, max_age: Duration) -> usize {
578 let mut peers_map = self.peers.write().await;
579 let mut removed = 0usize;
580
581 for peers in peers_map.values_mut() {
582 let before = peers.len();
583 peers.retain(|p| !p.is_expired(max_age));
584 removed += before - peers.len();
585 }
586
587 if removed > 0 {
588 debug!(removed, "Cleaned up expired LPD peers");
589 }
590 removed
591 }
592
593 pub fn is_available(&self) -> bool {
595 self.announcer.is_enabled()
596 }
597}
598
599pub fn parse_lpd_announcement(data: &[u8], sender_ip: IpAddr) -> Option<LpdPeer> {
622 let text = std::str::from_utf8(data).ok()?;
623 let mut info_hash = String::new();
624 let mut port = 0u16;
625 let mut token: Option<u32> = None;
626
627 for line in text.lines() {
628 let line = line.trim();
629 if let Some(rest) = line.strip_prefix("Hash:") {
630 let val = rest.trim();
631 if val.len() == 40 && val.chars().all(|c| c.is_ascii_hexdigit()) {
633 info_hash = val.to_lowercase(); } else {
635 return None; }
637 } else if let Some(rest) = line.strip_prefix("Port:") {
638 port = rest.trim().parse().ok()?;
639 if port == 0 {
640 return None; }
642 } else if let Some(rest) = line.strip_prefix("Token:") {
643 let token_str = rest.trim();
644 token = u32::from_str_radix(token_str, 16).ok();
645 }
646 }
647
648 if !info_hash.is_empty() && port > 0 && token.is_some() {
650 let mut peer = LpdPeer::new(info_hash, port, sender_ip);
651 peer.token = token;
652 Some(peer)
653 } else {
654 debug!(
655 text_len = data.len(),
656 has_hash = !info_hash.is_empty(),
657 has_port = port > 0,
658 has_token = token.is_some(),
659 "Incomplete/malformed LPD announcement ignored"
660 );
661 None
662 }
663}