use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use crate::constants;
pub use constants::{
LPD_DEFAULT_ANNOUNCE_INTERVAL_SECS as DEFAULT_ANNOUNCE_INTERVAL_SECS,
LPD_MULTICAST_ADDRESS as LPD_MULTICAST_ADDR, LPD_PORT,
};
pub const MAX_PEERS_PER_HASH: usize = 50;
#[derive(Debug, Clone)]
pub struct LpdPeer {
pub info_hash: String,
pub port: u16,
pub addr: IpAddr,
pub last_seen: Instant,
pub token: Option<u32>,
}
impl LpdPeer {
pub fn new(info_hash: impl Into<String>, port: u16, addr: IpAddr) -> Self {
Self {
info_hash: info_hash.into(),
port,
addr,
last_seen: Instant::now(),
token: None,
}
}
pub fn with_token(info_hash: impl Into<String>, port: u16, addr: IpAddr, token: u32) -> Self {
let mut p = Self::new(info_hash, port, addr);
p.token = Some(token);
p
}
pub fn socket_addr(&self) -> SocketAddr {
SocketAddr::new(self.addr, self.port)
}
pub fn is_expired(&self, max_age: Duration) -> bool {
self.last_seen.elapsed() > max_age
}
}
impl PartialEq for LpdPeer {
fn eq(&self, other: &Self) -> bool {
self.info_hash == other.info_hash && self.addr == other.addr
}
}
impl Eq for LpdPeer {}
impl std::hash::Hash for LpdPeer {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.info_hash.hash(state);
self.addr.hash(state);
}
}
pub struct LpdAnnouncer {
socket: UdpSocket,
multicast_addr: SocketAddr,
enabled: bool,
announce_interval: Duration,
}
impl LpdAnnouncer {
pub fn new() -> Result<Self, String> {
Self::with_config(constants::LPD_DEFAULT_ANNOUNCE_INTERVAL_SECS)
}
pub fn with_config(announce_interval_secs: u64) -> Result<Self, String> {
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| format!("Failed to bind UDP socket: {}", e))?;
socket
.set_broadcast(true)
.map_err(|e| format!("Failed to enable broadcast: {}", e))?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = socket.as_raw_fd();
unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_REUSEADDR,
&1i32 as *const i32 as *const libc::c_void,
std::mem::size_of::<i32>() as libc::socklen_t,
);
}
}
let multicast_addr: SocketAddr = format!(
"{}:{}",
constants::LPD_MULTICAST_ADDRESS,
constants::LPD_PORT
)
.parse()
.map_err(|e| format!("Invalid LPD multicast address: {}", e))?;
let multicast_ip: Ipv4Addr = constants::LPD_MULTICAST_ADDRESS
.parse()
.map_err(|e| format!("Invalid LPD multicast IP: {}", e))?;
socket
.join_multicast_v4(&multicast_ip, &Ipv4Addr::UNSPECIFIED)
.map_err(|e| format!("Failed to join LPD multicast group: {}", e))?;
debug!(
local = ?socket.local_addr().ok(),
multicast = %multicast_addr,
"LpdAnnouncer created successfully"
);
Ok(Self {
socket,
multicast_addr,
enabled: true,
announce_interval: Duration::from_secs(announce_interval_secs),
})
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn local_addr(&self) -> Result<SocketAddr, String> {
self.socket
.local_addr()
.map_err(|e| format!("Failed to get local address: {}", e))
}
pub fn announce(&self, info_hash: &str, port: u16) -> Result<(), String> {
if !self.enabled {
return Ok(()); }
if info_hash.len() != 40 || !info_hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!(
"Invalid info_hash format: expected 40 hex chars, got {} chars",
info_hash.len()
));
}
let token: u32 = rand::random::<u32>();
let msg = format!(
"Hash: {}\nPort: {}\nToken: {:08x}\n",
info_hash, port, token
);
debug!(
info_hash = %&info_hash[..8],
port,
token = format!("{:08x}", token),
"Sending LPD announcement"
);
self.socket
.send_to(msg.as_bytes(), self.multicast_addr)
.map_err(|e| format!("LPD announce send failed: {}", e))?;
Ok(())
}
pub fn receive_announcements(&self, timeout: Duration) -> Vec<LpdPeer> {
let mut buf = [0u8; constants::LPD_RECEIVE_BUFFER_SIZE];
let mut peers = Vec::new();
let mut seen: HashSet<(String, IpAddr)> = HashSet::new();
self.socket
.set_read_timeout(Some(timeout))
.expect("set_read_timeout should not fail");
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
match self.socket.recv_from(&mut buf) {
Ok((len, src_addr)) => {
if len == 0 {
continue;
}
if let Some(peer) = parse_lpd_announcement(&buf[..len], src_addr.ip()) {
let key = (peer.info_hash.clone(), peer.addr);
if seen.insert(key) {
debug!(
info_hash = %peer.info_hash[..8.min(peer.info_hash.len())],
addr = %src_addr.ip(),
"Received valid LPD announcement"
);
peers.push(peer);
} else {
debug!(
addr = %src_addr.ip(),
"Duplicate LPD announcement suppressed"
);
}
}
}
Err(e) => {
let kind = e.kind();
if kind == std::io::ErrorKind::TimedOut
|| kind == std::io::ErrorKind::WouldBlock
{
break;
}
warn!(error = %e, "LPD receive error, continuing");
std::thread::sleep(Duration::from_millis(10));
}
}
}
debug!(count = peers.len(), "LPD receive completed");
peers
}
pub fn announce_and_discover(
&self,
info_hash: &str,
port: u16,
discover_timeout: Duration,
) -> Result<Vec<LpdPeer>, String> {
self.announce(info_hash, port)?;
std::thread::sleep(Duration::from_millis(500));
let peers = self.receive_announcements(discover_timeout);
let peers: Vec<LpdPeer> = peers
.into_iter()
.filter(|p| !(p.info_hash == info_hash && p.port == port))
.collect();
Ok(peers)
}
}
pub struct LpdManager {
announcer: Arc<LpdAnnouncer>,
peers: Arc<RwLock<HashMap<String, HashSet<LpdPeer>>>>,
pub active_hashes: Arc<RwLock<HashSet<String>>>,
_announce_task: Option<tokio::task::JoinHandle<()>>,
}
impl Default for LpdManager {
fn default() -> Self {
Self::new()
}
}
impl LpdManager {
pub fn new() -> Self {
let announcer = LpdAnnouncer::new().unwrap_or_else(|_| {
warn!("Could not create LPD announcer, LPD will be disabled");
LpdAnnouncer::with_config(constants::LPD_DEFAULT_ANNOUNCE_INTERVAL_SECS)
.unwrap_or_else(|_| panic!("Fatal: cannot create LPD announcer"))
});
Self {
announcer: Arc::new(announcer),
peers: Arc::new(RwLock::new(HashMap::new())),
active_hashes: Arc::new(RwLock::new(HashSet::new())),
_announce_task: None,
}
}
pub fn with_interval(announce_interval_secs: u64) -> Result<Self, String> {
let announcer = LpdAnnouncer::with_config(announce_interval_secs)?;
Ok(Self {
announcer: Arc::new(announcer),
peers: Arc::new(RwLock::new(HashMap::new())),
active_hashes: Arc::new(RwLock::new(HashSet::new())),
_announce_task: None,
})
}
pub async fn register_torrent(&self, info_hash: &str) -> Result<(), String> {
let mut active = self.active_hashes.write().await;
active.insert(info_hash.to_string());
let mut peers_map = self.peers.write().await;
peers_map.entry(info_hash.to_string()).or_default();
info!(info_hash = %&info_hash[..8], "Torrent registered for LPD");
Ok(())
}
pub async fn unregister_torrent(&self, info_hash: &str) {
let mut active = self.active_hashes.write().await;
active.remove(info_hash);
info!(info_hash = %&info_hash[..8], "Torrent unregistered from LPD");
}
pub async fn announce_torrent(&self, info_hash: &str, port: u16) -> Result<(), String> {
self.announcer.announce(info_hash, port)?;
Ok(())
}
pub async fn discover_peers(&self, _info_hash: &str, timeout_ms: Option<u64>) -> Vec<LpdPeer> {
let timeout =
Duration::from_millis(timeout_ms.unwrap_or(constants::LPD_DEFAULT_RECEIVE_TIMEOUT_MS));
self.announcer.receive_announcements(timeout)
}
pub async fn get_peers_for(&self, info_hash: &str) -> Vec<LpdPeer> {
let peers_map = self.peers.read().await;
peers_map
.get(info_hash)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default()
}
pub fn start_background_announce(&mut self, port: u16) -> Option<tokio::task::JoinHandle<()>> {
if !self.announcer.is_enabled() {
debug!("LPD is disabled, not starting background announce");
return None;
}
let announcer = Arc::clone(&self.announcer);
let active_hashes = Arc::clone(&self.active_hashes);
info!(
interval_secs = self.announcer.announce_interval.as_secs(),
port, "Starting LPD background announce task"
);
let handle = tokio::spawn(async move {
let mut ticker = tokio::time::interval(announcer.announce_interval);
loop {
ticker.tick().await;
let hashes: Vec<String> = {
let active = active_hashes.read().await;
active.iter().cloned().collect()
};
for info_hash in &hashes {
if let Err(e) = announcer.announce(info_hash, port) {
warn!(
info_hash = %&info_hash[..8.min(info_hash.len())],
error = %e,
"Background LPD announce failed"
);
}
}
debug!(
count = hashes.len(),
"LPD background announce cycle completed"
);
}
});
self._announce_task = Some(handle);
None
}
pub fn stop_background_announce(&mut self) {
if let Some(handle) = self._announce_task.take() {
handle.abort();
info!("LPD background announce task stopped");
}
}
pub async fn update_peers(&self, info_hash: &str, new_peers: Vec<LpdPeer>) {
let mut peers_map = self.peers.write().await;
let entry = peers_map.entry(info_hash.to_string()).or_default();
for peer in new_peers {
if entry.len() >= MAX_PEERS_PER_HASH {
let oldest = entry.iter().max_by_key(|p| p.last_seen.elapsed()).cloned();
if let Some(oldest_peer) = oldest {
entry.remove(&oldest_peer);
}
}
entry.insert(peer);
}
}
pub async fn cleanup_expired_peers(&self, max_age: Duration) -> usize {
let mut peers_map = self.peers.write().await;
let mut removed = 0usize;
for peers in peers_map.values_mut() {
let before = peers.len();
peers.retain(|p| !p.is_expired(max_age));
removed += before - peers.len();
}
if removed > 0 {
debug!(removed, "Cleaned up expired LPD peers");
}
removed
}
pub fn is_available(&self) -> bool {
self.announcer.is_enabled()
}
}
pub fn parse_lpd_announcement(data: &[u8], sender_ip: IpAddr) -> Option<LpdPeer> {
let text = std::str::from_utf8(data).ok()?;
let mut info_hash = String::new();
let mut port = 0u16;
let mut token: Option<u32> = None;
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("Hash:") {
let val = rest.trim();
if val.len() == 40 && val.chars().all(|c| c.is_ascii_hexdigit()) {
info_hash = val.to_lowercase(); } else {
return None; }
} else if let Some(rest) = line.strip_prefix("Port:") {
port = rest.trim().parse().ok()?;
if port == 0 {
return None; }
} else if let Some(rest) = line.strip_prefix("Token:") {
let token_str = rest.trim();
token = u32::from_str_radix(token_str, 16).ok();
}
}
if !info_hash.is_empty() && port > 0 && token.is_some() {
let mut peer = LpdPeer::new(info_hash, port, sender_ip);
peer.token = token;
Some(peer)
} else {
debug!(
text_len = data.len(),
has_hash = !info_hash.is_empty(),
has_port = port > 0,
has_token = token.is_some(),
"Incomplete/malformed LPD announcement ignored"
);
None
}
}