57b5fd6b77
- Scan ping TCP multi-ports (sans root requis) - Lecture table ARP Linux (/proc/net/arp) - Détection 20 services par scan de ports TCP - Base OUI embarquée (~70 constructeurs courants) - API JSON locale Axum sur :9100 (GET /devices, GET /health) - Push automatique vers backend /api/v1/network - Enregistrement agent au démarrage - Config YAML (subnet 10.0.0.0/22, concurrence, timeouts) - ROADMAP Phase 1 et 2 marquées complètes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
4.9 KiB
Rust
172 lines
4.9 KiB
Rust
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<String>,
|
|
pub hostname: Option<String>,
|
|
pub vendor: Option<String>,
|
|
pub state: String,
|
|
pub services: Vec<String>,
|
|
pub open_ports: Vec<u16>,
|
|
}
|
|
|
|
pub async fn scan_all(cfg: &ScanConfig) -> Result<Vec<DiscoveredDevice>> {
|
|
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<String>, cfg: &ScanConfig) -> Option<DiscoveredDevice> {
|
|
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<IpAddr, String> {
|
|
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::<IpAddr>() {
|
|
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<String> {
|
|
None
|
|
}
|
|
|
|
// Scan TCP des ports configurés
|
|
async fn scan_ports(ip: IpAddr, ports: &[u16], timeout_ms: u64) -> Vec<u16> {
|
|
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<String> {
|
|
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"),
|
|
];
|