Skip to main content

git_ark/
scan.rs

1//! Bounded-parallel TCP port probe — the LAN discovery scanner.
2
3use std::net::{Ipv4Addr, SocketAddr, TcpStream};
4use std::sync::Mutex;
5use std::time::Duration;
6
7/// Probe `port` on each of `ips` with `connect_timeout`, across at most
8/// `workers` threads. Returns the reachable addresses, sorted. A bare TCP
9/// connect — no data, no auth.
10pub fn scan_port(ips: &[Ipv4Addr], port: u16, timeout: Duration, workers: usize) -> Vec<Ipv4Addr> {
11    if ips.is_empty() {
12        return Vec::new();
13    }
14    let workers = workers.clamp(1, ips.len());
15    let chunk = ips.len().div_ceil(workers);
16    let found = Mutex::new(Vec::new());
17    std::thread::scope(|s| {
18        for slice in ips.chunks(chunk) {
19            let found = &found;
20            s.spawn(move || {
21                for &ip in slice {
22                    let addr = SocketAddr::from((ip, port));
23                    if TcpStream::connect_timeout(&addr, timeout).is_ok() {
24                        found.lock().unwrap().push(ip);
25                    }
26                }
27            });
28        }
29    });
30    let mut v = found.into_inner().unwrap();
31    v.sort();
32    v
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use std::net::{Ipv4Addr, TcpListener};
39    use std::time::Duration;
40
41    #[test]
42    fn finds_a_listening_port_and_skips_closed() {
43        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
44        let open = listener.local_addr().unwrap().port();
45        let ips = [Ipv4Addr::LOCALHOST];
46        let found = scan_port(&ips, open, Duration::from_millis(300), 4);
47        assert_eq!(found, vec![Ipv4Addr::LOCALHOST]);
48
49        // A port with nothing bound: bind then drop to get a free one.
50        let tmp = TcpListener::bind("127.0.0.1:0").unwrap();
51        let closed = tmp.local_addr().unwrap().port();
52        drop(tmp);
53        let none = scan_port(&ips, closed, Duration::from_millis(200), 4);
54        assert!(
55            none.is_empty(),
56            "closed port should not be reported: {none:?}"
57        );
58    }
59}