use std::{ collections::HashMap, net::{IpAddr, SocketAddr}, time::Duration, }; use anyhow::Result; use ipnetwork::IpNetwork; use serde::{Deserialize, Serialize}; use tokio::{net::TcpStream, sync::Semaphore, time::timeout}; use tracing::debug; use crate::{config::ScanConfig, oui}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiscoveredDevice { pub ip: String, pub mac: Option, pub hostname: Option, pub vendor: Option, pub state: String, pub services: Vec, pub open_ports: Vec, } pub async fn scan_all(cfg: &ScanConfig) -> Result> { let arp_table = read_arp_table(); let sem = std::sync::Arc::new(Semaphore::new(cfg.concurrency)); let mut handles = Vec::new(); for subnet_str in &cfg.subnets { let network: IpNetwork = subnet_str.parse()?; for ip in network.iter() { let IpAddr::V4(ipv4) = ip else { continue }; // Exclure adresse réseau et broadcast if ipv4 == network.network() || ipv4.octets()[3] == 255 { continue; } let mac = arp_table.get(&ip).cloned(); let cfg_clone = cfg.clone(); let sem_clone = sem.clone(); handles.push(tokio::spawn(async move { let _permit = sem_clone.acquire().await.unwrap(); scan_host(ip, mac, &cfg_clone).await })); } } let mut devices = Vec::new(); for h in handles { if let Ok(Some(dev)) = h.await { devices.push(dev); } } Ok(devices) } async fn scan_host(ip: IpAddr, mac: Option, cfg: &ScanConfig) -> Option { let is_alive = ping(ip, cfg.ping_timeout_ms).await || mac.is_some(); if !is_alive { return None; } let hostname = resolve_hostname(ip).await; let open_ports = scan_ports(ip, &cfg.ports, cfg.service_timeout_ms).await; let services = detect_services(&open_ports); let vendor = mac.as_deref().and_then(oui::lookup).map(str::to_string); debug!("Découvert : {} ({:?}) — ports {:?}", ip, hostname, open_ports); Some(DiscoveredDevice { ip: ip.to_string(), mac, hostname, vendor, state: "online".into(), services, open_ports, }) } // Ping via connexion ICMP simulée par TCP sur port 7 (echo), avec fallback sans réponse // Compatible sans privilèges root (TCP connect probe) async fn ping(ip: IpAddr, timeout_ms: u64) -> bool { // Probe TCP rapide sur port 80 ou 22 — si l'un répond (refuse ou accepte) l'hôte est en vie for port in [80u16, 22, 443, 8080] { let addr = SocketAddr::new(ip, port); if timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr)) .await .is_ok() { return true; } } false } // Lecture de la table ARP Linux : /proc/net/arp fn read_arp_table() -> HashMap { let mut map = HashMap::new(); let Ok(content) = std::fs::read_to_string("/proc/net/arp") else { return map; }; for line in content.lines().skip(1) { let cols: Vec<&str> = line.split_whitespace().collect(); if cols.len() >= 4 { if let Ok(ip) = cols[0].parse::() { let mac = cols[3].to_uppercase(); if mac != "00:00:00:00:00:00" { map.insert(ip, mac); } } } } map } // Résolution DNS inverse (PTR) — à implémenter avec un resolver complet en Phase 2+ async fn resolve_hostname(_ip: IpAddr) -> Option { None } // Scan TCP des ports configurés async fn scan_ports(ip: IpAddr, ports: &[u16], timeout_ms: u64) -> Vec { let mut open = Vec::new(); for &port in ports { let addr = SocketAddr::new(ip, port); if timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr)) .await .map(|r| r.is_ok()) .unwrap_or(false) { open.push(port); } } open } // Mapping port → nom de service connu fn detect_services(ports: &[u16]) -> Vec { ports .iter() .filter_map(|&p| SERVICE_NAMES.iter().find(|(port, _)| *port == p).map(|(_, name)| name.to_string())) .collect() } const SERVICE_NAMES: &[(u16, &str)] = &[ (22, "SSH"), (80, "HTTP"), (443, "HTTPS"), (445, "SMB"), (2049, "NFS"), (1883, "MQTT"), (2375, "Docker"), (8006, "Proxmox"), (8123, "HomeAssistant"), (3000, "Grafana"), (9090, "Prometheus"), (9100, "NodeExporter"), (3306, "MySQL"), (5432, "PostgreSQL"), (6379, "Redis"), (8080, "HTTP-Alt"), (8443, "HTTPS-Alt"), (5357, "WSD"), (21, "FTP"), (25, "SMTP"), (53, "DNS"), ];