Skip to main content

zond_engine/
scanner.rs

1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7//! Orchestration logic for network discovery.
8//!
9//! This module coordinates the execution of various scanning strategies:
10//! - **Privileged**: High-speed raw socket scans ([`LocalScanner`] for ARP/ICMP, [`RoutedScanner`] for TCP SYN).
11//! - **Unprivileged**: Standard TCP handshake fallback via [`handshake`].
12//!
13//! It manages the lifecycle of a scan by partitioning targets by interface,
14//! spawning concurrent explorers, and piping results through a background [`HostnameResolver`]
15
16use std::net::IpAddr;
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use dashmap::DashMap;
21use is_root::is_root;
22use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
23use tokio::task::JoinHandle;
24
25use crate::core::config::ZondConfig;
26use crate::core::handle::ScanHandle;
27use crate::core::models::{host::Host, ip::set::IpSet, target::TargetMap};
28use crate::core::session::{ScanEvent, ScanSession};
29use crate::scanner::resolver::HostnameResolver;
30use crate::system::interface;
31use crate::{error, info, success, warn};
32use local::LocalScanner;
33use routed::RoutedScanner;
34
35mod connect;
36pub mod dispatcher;
37mod local;
38mod resolver;
39mod routed;
40
41#[async_trait]
42trait NetworkExplorer {
43    async fn discover_hosts(&mut self) -> anyhow::Result<()>;
44}
45
46pub async fn scan(
47    target_map: TargetMap,
48) -> anyhow::Result<(ScanSession, JoinHandle<anyhow::Result<()>>)> {
49    if not_root() {
50        // Future: Remove this fallback once SYN scanner is ready
51        warn!("Privileged port scanning (SYN) not yet implemented; using TCP connect fallback");
52    }
53
54    let (session, scan_handle, events_tx) = ScanSession::new();
55    let store = session.store.clone();
56
57    let join_handle = tokio::spawn(async move {
58        let dispatcher = dispatcher::Dispatcher::new(target_map);
59        let rx = dispatcher.run_shuffled(&scan_handle);
60        connect::scan(rx, &scan_handle, 50, store, events_tx).await
61    });
62
63    Ok((session, join_handle))
64}
65
66/// The primary entry point for network discovery.
67///
68/// ### Capabilities
69/// - **Privilege Aware**: Uses raw sockets (ARP/TCP SYN) if root; falls back to standard TCP handshakes if not.
70/// - **Multi-Interface**: Automatically partitions targets across available network adapters.
71/// - **Parallel Resolver**: Streams found IPs to a background DNS task for zero-latency lookups.
72///
73/// ### Integration Notes
74/// - **State**: Emits [`ScanEvent`]s to `ScanSession` and reacts to [`ScanHandle::should_stop`].
75/// - **Concurrency**: Spawns multiple Tokio tasks; ensure the caller is within a multithreaded runtime.
76pub async fn discover(
77    targets: IpSet,
78    cfg: &ZondConfig,
79) -> anyhow::Result<(ScanSession, JoinHandle<anyhow::Result<()>>)> {
80    let with_dns: bool = !cfg.no_dns;
81    let (session, scan_handle, events_tx) = ScanSession::new();
82    let store = session.store.clone();
83
84    if not_root() {
85        let store_clone = store.clone();
86        let events_tx_clone = events_tx.clone();
87        let handle_clone = scan_handle.clone();
88
89        let join_handle = tokio::spawn(async move {
90            connect::discover(targets, &handle_clone, store_clone.clone(), events_tx_clone).await?;
91            if with_dns {
92                resolver::resolve_hosts_async(store_clone).await;
93            }
94            Ok(())
95        });
96        return Ok((session, join_handle));
97    }
98
99    let (dns_tx, resolver_task) = if with_dns {
100        let (tx, rx) = mpsc::unbounded_channel();
101        let task = spawn_resolver(rx).await;
102        (Some(tx), Some(task))
103    } else {
104        info!("DNS resolution skipped by user flag");
105        (None, None)
106    };
107
108    let scanner_handles = spawn_explorers(
109        targets,
110        &scan_handle,
111        dns_tx,
112        store.clone(),
113        events_tx.clone(),
114    )
115    .await;
116
117    let join_handle = tokio::spawn(async move {
118        for handle in scanner_handles {
119            match handle.await {
120                Ok(Ok(())) => {}
121                Ok(Err(e)) => error!("Scanner task failed: {e}"),
122                Err(e) => error!("Task panicked: {e}"),
123            }
124        }
125
126        if let Some(task) = resolver_task
127            && let Ok(Some(mut resolver)) = task.await
128        {
129            resolver.resolve_hosts(store);
130        }
131
132        Ok(())
133    });
134
135    Ok((session, join_handle))
136}
137
138async fn spawn_explorers(
139    targets: IpSet,
140    scan_handle: &ScanHandle,
141    dns_tx: Option<UnboundedSender<IpAddr>>,
142    store: Arc<DashMap<IpAddr, Host>>,
143    events_tx: UnboundedSender<ScanEvent>,
144) -> Vec<JoinHandle<anyhow::Result<()>>> {
145    let mut handles = Vec::new();
146
147    let (interface_map, unmapped_ips) = interface::map_ips_to_interfaces(targets);
148
149    for (intf, (local_ips, routed_ips)) in interface_map {
150        // Local Scanner (ARP/ICMP)
151        if !local_ips.is_empty() {
152            info!(verbosity = 1, "Spawning LOCAL scanner for {}", intf.name);
153            let tx = dns_tx.clone();
154            let intf_c = intf.clone();
155
156            let scan_handle_clone = scan_handle.clone();
157            let store_clone = store.clone();
158            let events_tx_clone = events_tx.clone();
159            let handle = tokio::spawn(async move {
160                let mut scanner = LocalScanner::new(
161                    intf_c,
162                    local_ips,
163                    scan_handle_clone,
164                    tx,
165                    store_clone,
166                    events_tx_clone,
167                )?;
168                scanner.discover_hosts().await
169            });
170            handles.push(handle);
171        }
172
173        // Routed Scanner (TCP Syn Scan)
174        if !routed_ips.is_empty() {
175            info!(verbosity = 1, "Spawning ROUTED scanner for {}", intf.name);
176            let tx = dns_tx.clone();
177            let intf_c = intf.clone();
178
179            let scan_handle_clone = scan_handle.clone();
180            let store_clone = store.clone();
181            let events_tx_clone = events_tx.clone();
182            let handle = tokio::spawn(async move {
183                let mut scanner = RoutedScanner::new(
184                    intf_c,
185                    routed_ips,
186                    scan_handle_clone,
187                    tx,
188                    store_clone,
189                    events_tx_clone,
190                )?;
191                scanner.discover_hosts().await
192            });
193            handles.push(handle);
194        }
195    }
196
197    // Fallback Scanner (Unprivileged TCP Handshake) for unmapped IPs (e.g. localhost)
198    if !unmapped_ips.is_empty() {
199        info!(
200            verbosity = 1,
201            "Spawning FALLBACK scanner for unmapped targets"
202        );
203        let scan_handle_clone = scan_handle.clone();
204        let store_clone = store.clone();
205        let events_tx_clone = events_tx.clone();
206        let handle = tokio::spawn(async move {
207            connect::discover(
208                unmapped_ips,
209                &scan_handle_clone,
210                store_clone,
211                events_tx_clone,
212            )
213            .await
214        });
215        handles.push(handle);
216    }
217
218    handles
219}
220
221async fn spawn_resolver(dns_rx: UnboundedReceiver<IpAddr>) -> JoinHandle<Option<HostnameResolver>> {
222    tokio::spawn(async move {
223        match HostnameResolver::new(dns_rx) {
224            Ok(resolver) => {
225                success!("Successfully initialized hostname resolver");
226                Some(resolver.run().await)
227            }
228            Err(e) => {
229                error!("Resolver failed to start: {e}");
230                None
231            }
232        }
233    })
234}
235
236fn not_root() -> bool {
237    if !is_root() {
238        warn!("Root privileges missing, defaulting to unprivileged TCP scan");
239        return true;
240    }
241
242    success!("Root privileges detected, raw socket scan enabled");
243    false
244}