Skip to main content

singsing_rs/
lib.rs

1#![doc = env!("CARGO_PKG_DESCRIPTION")]
2#![doc = ""]
3#![cfg_attr(doc, doc = include_str!("../README.md"))]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/0xdea/singsing-rs/master/.img/logo_singsing.png"
6)]
7#![expect(
8    clippy::pub_use,
9    reason = "the crate's one `pub use` re-exports a foreign `ipnet` type that already appears \
10              in our public API (`TargetsError`), the deliberate exception this lint warns \
11              against as a module-layout anti-pattern; `use` items can't carry the attribute \
12              themselves, so it's set here instead"
13)]
14
15#[cfg(not(target_os = "linux"))]
16compile_error!("singsing-rs only supports Linux (see the Compatibility section in README.md)");
17
18use std::any::Any;
19use std::collections::{BTreeSet, HashMap, HashSet};
20use std::error::Error;
21use std::net::{IpAddr, Ipv4Addr};
22use std::num::ParseIntError;
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
27use std::{fs, io, thread};
28
29use pnet::datalink;
30use pnet::packet::ip::IpNextHeaderProtocols;
31use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet, checksum};
32use pnet::packet::tcp::{MutableTcpPacket, TcpFlags, TcpPacket, ipv4_checksum};
33use pnet::packet::{MutablePacket as _, Packet as _};
34use pnet::transport::{
35    TransportChannelType, TransportReceiver, ipv4_packet_iter, transport_channel,
36};
37
38/// The packet length used for scanning.
39const PACKET_LEN: usize = 40;
40/// The receive buffer's length, reused for every packet read on the raw socket.
41///
42/// The socket is a `Layer3` raw socket, so the kernel delivers every matching IPv4/TCP packet on
43/// the host to it, not just replies to this scan's own probes (unrelated packets are filtered out
44/// in userspace by `classify_response`). The buffer must therefore be large enough for the
45/// largest packet any such traffic could deliver, not just this scan's own `PACKET_LEN`-sized
46/// probes and replies: a too-small buffer silently truncates an oversized read rather than
47/// erroring. `1 MiB` comfortably exceeds the largest possible IPv4 packet (65,535 bytes).
48const RECEIVE_BUFFER_LEN: usize = 1 << 20;
49
50/// The maximum number of probes to send during a scan.
51const MAX_PROBES: usize = 16_777_214;
52/// The maximum time to listen for late replies after the final probe.
53const MAX_TIMEOUT: Duration = Duration::from_hours(24);
54
55/// One minute duration.
56const ONE_MINUTE: Duration = Duration::from_mins(1);
57/// Ten minute duration.
58const TEN_MINUTES: Duration = Duration::from_mins(10);
59/// Thirty minute duration.
60const THIRTY_MINUTES: Duration = Duration::from_mins(30);
61/// One hour duration.
62const ONE_HOUR: Duration = Duration::from_hours(1);
63
64/// A TCP port number.
65pub type Port = u16;
66/// A TCP sequence or acknowledgement number.
67type SeqNum = u32;
68/// Maps each target host/port pair to its expected TCP sequence number.
69type ExpectedResponses = HashMap<(Ipv4Addr, Port), SeqNum>;
70
71/// The error type returned by `scan`/`scan_with_callback`/`scan_with_callbacks`'s `on_result` and
72/// `on_progress` callbacks.
73///
74/// Callbacks are caller-defined and can fail for reasons this crate can't enumerate in advance, so
75/// their error is boxed rather than typed.
76pub type CallbackError = Box<dyn Error + Send + Sync>;
77
78/// Foreign types from `ipnet` that appear in this crate's public API (see [`TargetsError`]).
79///
80/// Re-exported so callers can name them without adding `ipnet` as a separate direct dependency,
81/// and so that a semver-breaking `ipnet` upgrade shows up as a `singsing-rs` API change too.
82pub use ipnet::{AddrParseError, Ipv4Net};
83
84/// An error resolving a network interface's IPv4 address.
85///
86/// # Examples
87///
88/// ```
89/// use singsing_rs::{InterfaceError, interface_ipv4};
90///
91/// let name = "singsing-rs-example-missing-interface";
92/// match interface_ipv4(name) {
93///     Err(InterfaceError::NotFound { name: n }) => assert_eq!(n, name),
94///     other => panic!("unexpected result: {other:?}"),
95/// }
96/// ```
97#[derive(Debug, thiserror::Error)]
98#[non_exhaustive]
99pub enum InterfaceError {
100    /// No interface with the given name exists.
101    #[error("network interface {name:?} does not exist")]
102    NotFound {
103        /// The requested interface name.
104        name: String,
105    },
106    /// The interface exists but has no IPv4 address.
107    #[error("network interface {name:?} has no IPv4 address")]
108    NoIpv4 {
109        /// The requested interface name.
110        name: String,
111    },
112}
113
114/// An error parsing scan targets.
115///
116/// # Examples
117///
118/// ```
119/// use singsing_rs::{TargetsError, parse_targets};
120///
121/// assert!(matches!(
122///     parse_targets("10.0.0.0/7"),
123///     Err(TargetsError::TooLarge { .. })
124/// ));
125/// ```
126#[derive(Debug, thiserror::Error)]
127#[non_exhaustive]
128pub enum TargetsError {
129    /// The input contained `/` but was not a valid IPv4 network.
130    #[error("invalid IPv4 network")]
131    InvalidNetwork(#[source] AddrParseError),
132    /// The input was not a valid IPv4 address.
133    #[error("invalid IPv4 address")]
134    InvalidAddress(#[source] AddrParseError),
135    /// The network contains more usable addresses than the scan limit allows.
136    #[error("{network} contains more than {max} usable addresses; split networks larger than a /8")]
137    TooLarge {
138        /// The oversized network.
139        network: Ipv4Net,
140        /// The maximum number of usable addresses.
141        max: usize,
142    },
143}
144
145/// An error parsing or reading scan ports.
146///
147/// # Examples
148///
149/// ```
150/// use singsing_rs::{PortsError, parse_ports};
151///
152/// assert!(matches!(parse_ports("0"), Err(PortsError::PortZero)));
153/// ```
154#[derive(Debug, thiserror::Error)]
155#[non_exhaustive]
156pub enum PortsError {
157    /// A comma-separated item was empty.
158    #[error("empty port in {input:?}")]
159    EmptyItem {
160        /// The full port list that contained the empty item.
161        input: String,
162    },
163    /// A range contained more than one `-`.
164    #[error("invalid port range {item:?}")]
165    InvalidRange {
166        /// The malformed range item.
167        item: String,
168    },
169    /// A range's start was greater than its end.
170    #[error("reversed port range {item:?}")]
171    ReversedRange {
172        /// The reversed range item.
173        item: String,
174    },
175    /// A port was not a valid `u16`.
176    #[error("invalid TCP port {input:?}")]
177    InvalidPort {
178        /// The unparsable port text.
179        input: String,
180        /// The underlying integer parse error.
181        #[source]
182        source: ParseIntError,
183    },
184    /// Port zero was requested, which is not supported.
185    #[error("TCP port zero is not supported")]
186    PortZero,
187    /// The services file could not be read.
188    #[error("failed to read {}", path.display())]
189    ServicesFileRead {
190        /// The services file path.
191        path: PathBuf,
192        /// The underlying I/O error.
193        #[source]
194        source: io::Error,
195    },
196    /// The services file contained no TCP services.
197    #[error("{} contains no TCP services", path.display())]
198    NoTcpServices {
199        /// The services file path.
200        path: PathBuf,
201    },
202}
203
204/// An error running a scan.
205///
206/// # Examples
207///
208/// ```
209/// use singsing_rs::{ScanConfig, ScanError, scan};
210/// use std::net::Ipv4Addr;
211///
212/// let config = ScanConfig::new(Vec::new(), vec![80], Ipv4Addr::LOCALHOST);
213/// assert!(matches!(scan(&config), Err(ScanError::EmptyScan)));
214/// ```
215#[derive(Debug, thiserror::Error)]
216#[non_exhaustive]
217pub enum ScanError {
218    /// The scan had no targets or no ports.
219    #[error("at least one target and one port are required")]
220    EmptyScan,
221    /// The configured bandwidth was zero.
222    #[error("bandwidth must be greater than zero")]
223    ZeroBandwidth,
224    /// The configured bandwidth overflowed while converting to a packet rate.
225    #[error("bandwidth is too large")]
226    BandwidthOverflow,
227    /// The configured late-reply timeout exceeds the maximum.
228    #[error("timeout of {timeout:?} exceeds the maximum of {max:?}")]
229    TimeoutTooLarge {
230        /// The requested timeout.
231        timeout: Duration,
232        /// The maximum allowed timeout.
233        max: Duration,
234    },
235    /// Multiplying the target and port counts overflowed `usize`.
236    #[error("scan size overflow")]
237    ScanSizeOverflow,
238    /// The scan exceeds the maximum number of probes.
239    #[error(
240        "scan contains {probe_count} probes; maximum is {max} \
241         (one port on a /8 or all 65,535 ports on a /24); split larger scans"
242    )]
243    TooManyProbes {
244        /// The requested probe count.
245        probe_count: usize,
246        /// The maximum allowed probe count.
247        max: usize,
248    },
249    /// `ScanConfig` contained a duplicate target/port pair.
250    #[error("duplicate host/port pair {host}:{port}; ScanConfig targets and ports must be unique")]
251    DuplicatePair {
252        /// The duplicated target.
253        host: Ipv4Addr,
254        /// The duplicated port.
255        port: Port,
256    },
257    /// Creating the raw transport socket failed.
258    #[error("failed to create raw socket (run as root or grant CAP_NET_RAW)")]
259    SocketCreation(#[source] io::Error),
260    /// Receiving a raw packet failed.
261    #[error("failed to receive raw packet")]
262    Receive(#[source] io::Error),
263    /// The packet receiver thread panicked.
264    #[error("packet receiver thread panicked: {0}")]
265    ReceiverPanicked(String),
266    /// Transmission stopped after part of the scan was sent.
267    #[error(transparent)]
268    Incomplete(IncompleteScanError),
269    /// The `on_result` callback returned an error.
270    #[error("callback failed")]
271    Callback(#[source] CallbackError),
272}
273
274/// An error that stopped probe transmission mid-scan.
275#[derive(Debug, thiserror::Error)]
276#[non_exhaustive]
277pub enum SendError {
278    /// The fixed-size SYN packet buffer could not be parsed back into an IPv4 packet.
279    #[error("failed to construct IPv4 packet")]
280    PacketConstruction,
281    /// Sending a probe failed.
282    #[error("failed to send SYN to {host}:{port}")]
283    Io {
284        /// The probe's destination host.
285        host: Ipv4Addr,
286        /// The probe's destination port.
287        port: Port,
288        /// The underlying I/O error.
289        #[source]
290        source: io::Error,
291    },
292    /// The `on_progress` callback returned an error.
293    #[error("callback failed")]
294    Callback(#[source] CallbackError),
295}
296
297/// An error that stopped transmission after part of a scan was executed.
298///
299/// `IncompleteScanError` has no public constructor; callers only ever obtain one from
300/// [`ScanError::Incomplete`], returned by [`scan`]/[`scan_with_callback`]/[`scan_with_callbacks`].
301///
302/// # Examples
303///
304/// ```no_run
305/// use singsing_rs::{ScanConfig, ScanError, scan};
306/// use std::net::Ipv4Addr;
307///
308/// let config = ScanConfig::new(vec![Ipv4Addr::LOCALHOST], vec![80], Ipv4Addr::LOCALHOST);
309/// if let Err(ScanError::Incomplete(incomplete)) = scan(&config) {
310///     eprintln!(
311///         "sent {} of {} probes before stopping",
312///         incomplete.probes_sent(),
313///         incomplete.total_probes()
314///     );
315///     for result in incomplete.partial_results() {
316///         println!("{result:?}");
317///     }
318/// }
319/// ```
320#[derive(Debug, thiserror::Error)]
321#[error("scan stopped after sending {probes_sent} of {total_probes} probes")]
322pub struct IncompleteScanError {
323    /// The error that caused the incomplete scan.
324    #[source]
325    source: SendError,
326    /// The results received from probes sent before transmission stopped.
327    partial_results: Vec<ScanResult>,
328    /// The number of probes successfully sent before the error.
329    probes_sent: usize,
330    /// The total number of probes requested by the scan.
331    total_probes: usize,
332}
333
334impl IncompleteScanError {
335    /// Returns results received from probes sent before transmission stopped.
336    #[must_use]
337    pub fn partial_results(&self) -> &[ScanResult] {
338        &self.partial_results
339    }
340
341    /// Returns the number of probes successfully sent before the error.
342    #[must_use]
343    pub const fn probes_sent(&self) -> usize {
344        self.probes_sent
345    }
346
347    /// Returns the total number of probes requested by the scan.
348    #[must_use]
349    pub const fn total_probes(&self) -> usize {
350        self.total_probes
351    }
352}
353
354/// Configuration for one SYN scan.
355#[derive(Clone, Debug, Eq, Hash, PartialEq)]
356#[non_exhaustive]
357pub struct ScanConfig {
358    /// IPv4 addresses to scan.
359    ///
360    /// Addresses must be unique.
361    pub targets: Vec<Ipv4Addr>,
362    /// TCP ports to scan.
363    ///
364    /// Ports must be unique.
365    pub ports: Vec<Port>,
366    /// Source IPv4 address assigned to the selected interface.
367    pub source: Ipv4Addr,
368    /// Approximate maximum packet bandwidth in KiB/s.
369    ///
370    /// [`ScanConfig::new`] defaults this to 15 KiB/s, or approximately 384 probes per second
371    /// with the scanner's 40-byte packet accounting.
372    pub bandwidth_kib: u64,
373    /// Time to listen for late replies after the final probe.
374    ///
375    /// Capped at 24 hours; [`ScanConfig::new`] defaults this to 30 seconds.
376    pub timeout: Duration,
377    /// Whether RST responses should be returned.
378    pub show_closed: bool,
379}
380
381impl ScanConfig {
382    /// Creates a configuration with 15 KiB/s bandwidth and a 30-second late-reply timeout.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use singsing_rs::ScanConfig;
388    /// use std::net::Ipv4Addr;
389    ///
390    /// let target: Ipv4Addr = "192.168.2.10".parse()?;
391    /// let mut config = ScanConfig::new(vec![target], vec![22, 80, 443], Ipv4Addr::LOCALHOST);
392    /// config.show_closed = true;
393    ///
394    /// assert_eq!(config.bandwidth_kib, 15);
395    /// assert!(config.show_closed);
396    /// # Ok::<(), std::net::AddrParseError>(())
397    /// ```
398    #[must_use]
399    pub const fn new(targets: Vec<Ipv4Addr>, ports: Vec<Port>, source: Ipv4Addr) -> Self {
400        Self {
401            targets,
402            ports,
403            source,
404            bandwidth_kib: 15,
405            timeout: Duration::from_secs(30),
406            show_closed: false,
407        }
408    }
409}
410
411/// Probe sending progress reported during a scan.
412#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
413#[non_exhaustive]
414pub struct ScanProgress {
415    /// Number of probes sent so far.
416    pub probes_sent: usize,
417    /// Total number of probes in the scan.
418    pub total_probes: usize,
419    /// Time elapsed since sending began.
420    pub elapsed: Duration,
421}
422
423impl ScanProgress {
424    /// Creates a progress snapshot from the given probe counts and elapsed time.
425    #[must_use]
426    pub const fn new(probes_sent: usize, total_probes: usize, elapsed: Duration) -> Self {
427        Self {
428            probes_sent,
429            total_probes,
430            elapsed,
431        }
432    }
433
434    /// Returns the integer completion percentage.
435    #[must_use]
436    pub const fn percent(self) -> usize {
437        if self.total_probes == 0 {
438            return 0;
439        }
440        self.probes_sent.saturating_mul(100) / self.total_probes
441    }
442
443    /// Estimates the time required to send the remaining probes.
444    ///
445    /// Returns `None` before the first probe is sent, since no rate can be estimated yet.
446    #[must_use]
447    pub fn estimated_remaining(self) -> Option<Duration> {
448        let sent = u32::try_from(self.probes_sent).ok()?;
449        let remaining = u32::try_from(self.total_probes.saturating_sub(self.probes_sent)).ok()?;
450
451        if sent == 0 {
452            return None;
453        }
454        self.elapsed.checked_mul(remaining)?.checked_div(sent)
455    }
456}
457
458/// The state inferred from a TCP response.
459///
460/// # Examples
461///
462/// ```
463/// use singsing_rs::{PortState, ScanResult};
464/// use std::net::Ipv4Addr;
465///
466/// let result = ScanResult::new(Ipv4Addr::LOCALHOST, 443, PortState::Open);
467/// match result.state {
468///     PortState::Open => println!("{}:{} is open", result.host, result.port),
469///     PortState::Closed => println!("{}:{} is closed", result.host, result.port),
470///     _ => println!("{}:{} is some other state", result.host, result.port),
471/// }
472/// ```
473#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
474#[non_exhaustive]
475pub enum PortState {
476    /// A SYN/ACK was received.
477    Open,
478    /// A RST was received.
479    Closed,
480}
481
482/// One response produced by a scan.
483#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
484#[non_exhaustive]
485pub struct ScanResult {
486    /// The responding host.
487    pub host: Ipv4Addr,
488    /// The responding TCP port.
489    pub port: Port,
490    /// The inferred port state.
491    pub state: PortState,
492}
493
494impl ScanResult {
495    /// Creates a scan result for the given host, port, and inferred state.
496    #[must_use]
497    pub const fn new(host: Ipv4Addr, port: Port, state: PortState) -> Self {
498        Self { host, port, state }
499    }
500}
501
502/// Resolves the first IPv4 address assigned to a network interface.
503///
504/// # Errors
505///
506/// Returns an error if the interface does not exist or has no IPv4 address.
507///
508/// # Examples
509///
510/// ```
511/// use singsing_rs::{InterfaceError, interface_ipv4};
512/// use std::net::Ipv4Addr;
513///
514/// assert_eq!(interface_ipv4("lo")?, Ipv4Addr::LOCALHOST);
515/// # Ok::<(), InterfaceError>(())
516/// ```
517pub fn interface_ipv4(name: &str) -> Result<Ipv4Addr, InterfaceError> {
518    let interface = datalink::interfaces()
519        .into_iter()
520        .find(|interface| interface.name == name)
521        .ok_or_else(|| InterfaceError::NotFound {
522            name: name.to_owned(),
523        })?;
524
525    interface
526        .ips
527        .into_iter()
528        .find_map(|network| match network.ip() {
529            IpAddr::V4(address) => Some(address),
530            IpAddr::V6(_) => None,
531        })
532        .ok_or_else(|| InterfaceError::NoIpv4 {
533            name: name.to_owned(),
534        })
535}
536
537/// Expands an IPv4 address or CIDR into scan targets.
538///
539/// Network and broadcast addresses are omitted for prefixes from `/0` through `/30`. Both
540/// addresses of a `/31` are included, as is the single address of a `/32`, matching
541/// [`Ipv4Net::hosts`].
542///
543/// # Errors
544///
545/// Returns an error for malformed IPv4/CIDR input or a network containing more usable addresses
546/// than a `/8`.
547///
548/// # Examples
549///
550/// ```
551/// use singsing_rs::parse_targets;
552/// use std::net::Ipv4Addr;
553///
554/// assert_eq!(
555///     parse_targets("192.168.2.0/30")?,
556///     ["192.168.2.1".parse::<Ipv4Addr>()?, "192.168.2.2".parse()?]
557/// );
558/// # Ok::<(), Box<dyn std::error::Error>>(())
559/// ```
560pub fn parse_targets(input: &str) -> Result<Vec<Ipv4Addr>, TargetsError> {
561    let network = if input.contains('/') {
562        input.parse().map_err(TargetsError::InvalidNetwork)?
563    } else {
564        format!("{input}/32")
565            .parse()
566            .map_err(TargetsError::InvalidAddress)?
567    };
568
569    if usable_target_count(network).is_none_or(|count| count > MAX_PROBES) {
570        return Err(TargetsError::TooLarge {
571            network,
572            max: MAX_PROBES,
573        });
574    }
575
576    Ok(network.hosts().collect())
577}
578
579/// Parses comma-separated ports and inclusive ranges such as `21-23,80,443`.
580///
581/// Duplicate ports are removed and the result is returned in ascending order.
582///
583/// # Errors
584///
585/// Returns an error for empty items, reversed ranges, port zero, or values larger than 65535.
586///
587/// # Examples
588///
589/// ```
590/// use singsing_rs::{PortsError, parse_ports};
591///
592/// assert_eq!(parse_ports("22,80,79-81")?, [22, 79, 80, 81]);
593/// # Ok::<(), PortsError>(())
594/// ```
595pub fn parse_ports(input: &str) -> Result<Vec<Port>, PortsError> {
596    let mut ports = BTreeSet::new();
597
598    for item in input.split(',') {
599        if item.is_empty() {
600            return Err(PortsError::EmptyItem {
601                input: input.to_owned(),
602            });
603        }
604
605        let (start, end) = if let Some((start, end)) = item.split_once('-') {
606            if end.contains('-') {
607                return Err(PortsError::InvalidRange {
608                    item: item.to_owned(),
609                });
610            }
611            // Port range.
612            (parse_port(start)?, parse_port(end)?)
613        } else {
614            // Single port.
615            let port = parse_port(item)?;
616            (port, port)
617        };
618
619        // After both halves are known to be valid ports, check for a reversed range.
620        if start > end {
621            return Err(PortsError::ReversedRange {
622                item: item.to_owned(),
623            });
624        }
625
626        // Insert the whole range at once.
627        ports.extend(start..=end);
628    }
629
630    Ok(ports.into_iter().collect())
631}
632
633/// Reads TCP ports from a services file (normally `/etc/services`).
634///
635/// Duplicate ports are removed and the result is returned in ascending order.
636///
637/// # Errors
638///
639/// Returns an error when the file cannot be read or contains no TCP services.
640///
641/// # Examples
642///
643/// ```
644/// use singsing_rs::ports_from_services;
645/// use std::fs;
646///
647/// let path = std::env::temp_dir().join("singsing-rs-doctest-services");
648/// fs::write(&path, "ssh 22/tcp\ndomain 53/udp\nhttp 80/tcp\n")?;
649///
650/// let ports = ports_from_services(&path)?;
651/// fs::remove_file(&path)?;
652///
653/// assert_eq!(ports, [22, 80]);
654/// # Ok::<(), Box<dyn std::error::Error>>(())
655/// ```
656pub fn ports_from_services(path: impl AsRef<Path>) -> Result<Vec<Port>, PortsError> {
657    let contents =
658        fs::read_to_string(path.as_ref()).map_err(|source| PortsError::ServicesFileRead {
659            path: path.as_ref().to_path_buf(),
660            source,
661        })?;
662    let mut ports = BTreeSet::new();
663
664    for line in contents.lines() {
665        // Break lines into fields, skipping comments and empty lines.
666        let mut fields = line
667            .split('#')
668            .next()
669            .unwrap_or_default()
670            .split_whitespace();
671
672        // Skip service names and extract valid TCP ports to insert into the set.
673        let _service = fields.next();
674        if let Some(port_protocol) = fields.next()
675            && let Some((port, "tcp")) = port_protocol.split_once('/')
676            && let Ok(port) = parse_port(port)
677        {
678            ports.insert(port);
679        }
680    }
681
682    if ports.is_empty() {
683        return Err(PortsError::NoTcpServices {
684            path: path.as_ref().to_path_buf(),
685        });
686    }
687
688    Ok(ports.into_iter().collect())
689}
690
691/// Executes a Linux IPv4 SYN scan.
692///
693/// No reply means filtered or unreachable and therefore produces no result. Raw sockets require
694/// root or `CAP_NET_RAW`.
695///
696/// # Errors
697///
698/// Returns an error for an empty or excessively large scan, invalid bandwidth, an excessive
699/// timeout, duplicate targets or ports, raw socket permission failures, packet send failures,
700/// or receiver failures. A transmission-phase failure is returned as [`IncompleteScanError`],
701/// which retains results received for successfully sent probes.
702///
703/// # Examples
704///
705/// ```no_run
706/// use singsing_rs::{ScanConfig, interface_ipv4, parse_targets, scan};
707///
708/// let source = interface_ipv4("eth0")?;
709/// let targets = parse_targets("192.168.2.10")?;
710/// let config = ScanConfig::new(targets, vec![22, 80, 443], source);
711///
712/// for result in scan(&config)? {
713///     println!("{result:?}");
714/// }
715/// # Ok::<(), Box<dyn std::error::Error>>(())
716/// ```
717pub fn scan(config: &ScanConfig) -> Result<Vec<ScanResult>, ScanError> {
718    scan_with_callbacks(config, |_| Ok(()), |_| Ok(()))
719}
720
721/// Executes a SYN scan and calls `on_result` as each response arrives.
722///
723/// Results are still returned in sorted order after the scan. The callback is useful for
724/// interactive clients that need immediate per-result feedback.
725///
726/// # Errors
727///
728/// Returns the same errors as [`scan`], along with errors returned by `on_result`.
729///
730/// # Examples
731///
732/// ```no_run
733/// use singsing_rs::{ScanConfig, interface_ipv4, parse_targets, scan_with_callback};
734///
735/// let source = interface_ipv4("eth0")?;
736/// let targets = parse_targets("192.168.2.10")?;
737/// let config = ScanConfig::new(targets, vec![22, 80, 443], source);
738///
739/// scan_with_callback(&config, |result| {
740///     println!("{result:?}");
741///     Ok(())
742/// })?;
743/// # Ok::<(), Box<dyn std::error::Error>>(())
744/// ```
745pub fn scan_with_callback(
746    config: &ScanConfig,
747    on_result: impl FnMut(ScanResult) -> Result<(), CallbackError> + Send + 'static,
748) -> Result<Vec<ScanResult>, ScanError> {
749    scan_with_callbacks(config, on_result, |_| Ok(()))
750}
751
752/// Executes a SYN scan with callbacks for results and sending progress.
753///
754/// `on_result` runs as each response arrives. It needs to be `Send + 'static` because it gets
755/// moved into the spawned receiver thread. While probes are being sent, `on_progress` runs every
756/// minute for the first ten minutes, every ten minutes through the first hour, and every
757/// thirty minutes thereafter.
758///
759/// # Errors
760///
761/// Returns the same errors as [`scan`], along with errors returned by either callback.
762///
763/// # Examples
764///
765/// ```no_run
766/// use singsing_rs::{ScanConfig, interface_ipv4, parse_targets, scan_with_callbacks};
767///
768/// let source = interface_ipv4("eth0")?;
769/// let targets = parse_targets("192.168.2.10")?;
770/// let config = ScanConfig::new(targets, vec![22, 80, 443], source);
771///
772/// scan_with_callbacks(
773///     &config,
774///     |result| {
775///         println!("{result:?}");
776///         Ok(())
777///     },
778///     |progress| {
779///         eprintln!("{}% complete", progress.percent());
780///         Ok(())
781///     },
782/// )?;
783/// # Ok::<(), Box<dyn std::error::Error>>(())
784/// ```
785pub fn scan_with_callbacks(
786    config: &ScanConfig,
787    mut on_result: impl FnMut(ScanResult) -> Result<(), CallbackError> + Send + 'static,
788    mut on_progress: impl FnMut(ScanProgress) -> Result<(), CallbackError>,
789) -> Result<Vec<ScanResult>, ScanError> {
790    // Validate the scan configuration and build the expected responses table.
791    let probe_count = validate_scan(config)?;
792    let source_port = source_port();
793    let nonce = nonce();
794    let expected = Arc::new(expected_responses(config, nonce, probe_count)?);
795
796    // Create the transport channel (`Layer3` raw socket).
797    let protocol = TransportChannelType::Layer3(IpNextHeaderProtocols::Tcp);
798    let (mut sender, mut receiver) =
799        transport_channel(RECEIVE_BUFFER_LEN, protocol).map_err(ScanError::SocketCreation)?;
800
801    // Spawn the receiver thread.
802    let done = Arc::new(AtomicBool::new(false));
803    let receiver_done = Arc::clone(&done);
804    let receiver_expected = Arc::clone(&expected);
805    let source = config.source;
806    let timeout = config.timeout;
807    let show_closed = config.show_closed;
808    let receive_thread = thread::spawn(move || {
809        let receive_config = ReceiveConfig {
810            expected: &receiver_expected,
811            source,
812            source_port,
813            show_closed,
814            done: &receiver_done,
815            timeout,
816        };
817        receive(&mut receiver, &receive_config, &mut on_result)
818    });
819
820    // Compute the send interval based on the requested bandwidth.
821    //
822    // Unlike `timeout`, `bandwidth_kib` has no upper sanity limit, only the overflow guard below
823    // (~u64::MAX / 1024 KiB/s). An extreme but non-overflowing value drives `packets_per_second`
824    // high enough that this division floors to zero, making `interval` `Duration::ZERO`; the send
825    // loop then never sleeps, so the practical effect is unthrottled sending rather than a panic
826    // or incorrect behavior, so no cap is needed.
827    let bytes_per_second = config
828        .bandwidth_kib
829        .checked_mul(1024)
830        .ok_or(ScanError::BandwidthOverflow)?;
831    let packets_per_second = (bytes_per_second / 40).max(1);
832    let interval = Duration::from_nanos(1_000_000_000_u64 / packets_per_second);
833
834    // Send loop.
835    //
836    // Runs as an inline closure so a mid-loop error can be captured without immediately returning
837    // from the outer function. This way, a send failure doesn't abort the receiver early, but just
838    // gets folded into the final error once both sides are done, so partial results are not lost.
839    let mut next_send = Instant::now();
840    let started = next_send;
841    let mut next_progress = ONE_MINUTE;
842    let mut probes_sent = 0;
843    let send_result = (|| -> Result<(), SendError> {
844        #[expect(
845            clippy::iter_over_hash_type,
846            reason = "randomized `HashMap` iteration order is deliberate; see README's Transmission order section"
847        )]
848        for (&(host, port), &sequence) in expected.iter() {
849            // Build one TCP SYN packet and send it.
850            let packet = syn_packet(config.source, host, source_port, port, sequence);
851            let ipv4_packet =
852                MutableIpv4Packet::owned(packet).ok_or(SendError::PacketConstruction)?;
853            sender
854                .send_to(ipv4_packet, IpAddr::V4(host))
855                .map_err(|io_error| SendError::Io {
856                    host,
857                    port,
858                    source: io_error,
859                })?;
860            probes_sent += 1;
861
862            // Throttle the send loop to the requested bandwidth.
863            next_send += interval;
864            if let Some(delay) = next_send.checked_duration_since(Instant::now()) {
865                thread::sleep(delay);
866            }
867
868            // Track progress and invoke the callback if necessary.
869            //
870            // Unlike a failing `on_result` on the receive side, a failing `on_progress` here stops
871            // the send loop like any other send-loop error, so it's preserved as `SendError::Callback`
872            // inside `IncompleteScanError` (with partial results and counts), not discarded.
873            let now = Instant::now();
874            let elapsed = now.duration_since(started);
875            if elapsed >= next_progress {
876                on_progress(ScanProgress {
877                    probes_sent,
878                    total_probes: probe_count,
879                    elapsed,
880                })
881                .map_err(SendError::Callback)?;
882                next_progress = advance_progress_deadline(next_progress, elapsed);
883            }
884        }
885
886        Ok(())
887    })();
888
889    // Whatever happens, unconditionally mark the send loop as done.
890    //
891    // Everything this thread wrote to memory before this store is guaranteed to be visible to the
892    // receive thread that later does an `Acquire` load on `done`.
893    done.store(true, Ordering::Release);
894
895    // Join the receive thread and collect the results.
896    //
897    // The first `?` (via `map_err`) converts the panic payload to a `ScanError::ReceiverPanicked`.
898    // The second `?` propagates any other error from the receive thread.
899    let mut results = receive_thread.join().map_err(|payload| {
900        ScanError::ReceiverPanicked(describe_panic_payload(&*payload).to_owned())
901    })??;
902
903    // Sort the results by host and port.
904    results.sort_unstable_by_key(|result| (u32::from(result.host), result.port));
905
906    // If the send loop failed mid-scan, return an `IncompleteScanError` with the collected results.
907    if let Err(error) = send_result {
908        return Err(ScanError::Incomplete(IncompleteScanError {
909            source: error,
910            partial_results: results,
911            probes_sent,
912            total_probes: probe_count,
913        }));
914    }
915
916    Ok(results)
917}
918
919/// Returns the number of usable addresses in an IPv4 network.
920///
921/// Both addresses of a `/31` count as usable and a `/32` counts as one; otherwise the network
922/// and broadcast addresses are excluded. Returns `None` on prefix-length arithmetic overflow.
923fn usable_target_count(network: Ipv4Net) -> Option<usize> {
924    let host_bits = 32_u32.checked_sub(u32::from(network.prefix_len()))?;
925
926    match host_bits {
927        0 => Some(1),
928        1 => Some(2),
929        bits => 1_usize.checked_shl(bits)?.checked_sub(2),
930    }
931}
932
933/// Parses a single TCP port, rejecting port zero.
934fn parse_port(input: &str) -> Result<Port, PortsError> {
935    let port = input.parse().map_err(|source| PortsError::InvalidPort {
936        input: input.to_owned(),
937        source,
938    })?;
939
940    if port == 0 {
941        return Err(PortsError::PortZero);
942    }
943
944    Ok(port)
945}
946
947/// Validates a scan configuration and returns its total probe count.
948fn validate_scan(config: &ScanConfig) -> Result<usize, ScanError> {
949    validate_probe_count(
950        config.targets.len(),
951        config.ports.len(),
952        config.bandwidth_kib,
953        config.timeout,
954    )
955}
956
957/// Validates scan size and configuration limits, returning the total probe count.
958///
959/// Rejects an empty target or port list, zero bandwidth, a late-reply timeout above
960/// [`MAX_TIMEOUT`], and a target * port product above [`MAX_PROBES`].
961fn validate_probe_count(
962    target_count: usize,
963    port_count: usize,
964    bandwidth_kib: u64,
965    timeout: Duration,
966) -> Result<usize, ScanError> {
967    if target_count == 0 || port_count == 0 {
968        return Err(ScanError::EmptyScan);
969    }
970    if bandwidth_kib == 0 {
971        return Err(ScanError::ZeroBandwidth);
972    }
973    if timeout > MAX_TIMEOUT {
974        return Err(ScanError::TimeoutTooLarge {
975            timeout,
976            max: MAX_TIMEOUT,
977        });
978    }
979
980    let probe_count = target_count
981        .checked_mul(port_count)
982        .ok_or(ScanError::ScanSizeOverflow)?;
983    if probe_count > MAX_PROBES {
984        return Err(ScanError::TooManyProbes {
985            probe_count,
986            max: MAX_PROBES,
987        });
988    }
989
990    Ok(probe_count)
991}
992
993/// Picks a random ephemeral TCP source port in the `49152..=65535` range, reused for every probe in the scan.
994#[expect(
995    clippy::as_conversions,
996    reason = "`nonce() % 16384` is always in `0..16384`, so it always fits in a `u16`"
997)]
998fn source_port() -> Port {
999    49152 + (nonce() % 16384) as u16
1000}
1001
1002/// Returns a per-scan random nonce derived from the current sub-second time.
1003///
1004/// This nonce is not cryptographically robust, but it is sufficient for our purposes.
1005fn nonce() -> u32 {
1006    SystemTime::now()
1007        .duration_since(UNIX_EPOCH)
1008        .unwrap_or_default()
1009        .subsec_nanos()
1010}
1011
1012/// Builds the expected-response table mapping each target/port pair to its deterministic sequence
1013/// number.
1014///
1015/// Returns [`ScanError::DuplicatePair`] for a duplicate target/port pair.
1016fn expected_responses(
1017    config: &ScanConfig,
1018    nonce: u32,
1019    probe_count: usize,
1020) -> Result<ExpectedResponses, ScanError> {
1021    let mut expected = HashMap::with_capacity(probe_count);
1022
1023    for &host in &config.targets {
1024        for &port in &config.ports {
1025            if expected
1026                .insert((host, port), sequence(host, port, nonce))
1027                .is_some()
1028            {
1029                return Err(ScanError::DuplicatePair { host, port });
1030            }
1031        }
1032    }
1033    Ok(expected)
1034}
1035
1036/// Derives the deterministic expected TCP sequence number for a host/port pair, given the nonce.
1037///
1038/// This allows to correlate a reply with a probe, without the need to track live per-connection
1039/// state in memory. This classic stateless-SYN-scanning trick is robust against accidental
1040/// misclassification, but it does not provide any protection against a deliberately hostile
1041/// target trying to defeat correlation.
1042fn sequence(host: Ipv4Addr, port: Port, nonce: u32) -> SeqNum {
1043    u32::from(host)
1044        .rotate_left(13)
1045        .wrapping_add(u32::from(port).rotate_left(3))
1046        ^ nonce
1047}
1048
1049/// Builds a raw 40-byte IPv4/TCP SYN packet for one probe.
1050fn syn_packet(
1051    source: Ipv4Addr,
1052    destination: Ipv4Addr,
1053    source_port: Port,
1054    destination_port: Port,
1055    sequence: SeqNum,
1056) -> Vec<u8> {
1057    let mut bytes = vec![0_u8; PACKET_LEN];
1058
1059    #[expect(
1060        clippy::expect_used,
1061        reason = "`bytes` is exactly `PACKET_LEN`, sized to fit one IPv4 header and one TCP header, so packet construction cannot fail"
1062    )]
1063    let mut ipv4 = MutableIpv4Packet::new(&mut bytes).expect("fixed-size IPv4 packet");
1064    ipv4.set_version(4);
1065    ipv4.set_header_length(5);
1066    ipv4.set_total_length(40);
1067    #[expect(
1068        clippy::as_conversions,
1069        reason = "`sequence >> 16` keeps only the top 16 bits, so it always fits in a `u16`"
1070    )]
1071    ipv4.set_identification((sequence >> 16) as u16);
1072    ipv4.set_ttl(64);
1073    ipv4.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
1074    ipv4.set_source(source);
1075    ipv4.set_destination(destination);
1076
1077    #[expect(
1078        clippy::expect_used,
1079        reason = "`bytes` is exactly `PACKET_LEN`, sized to fit one IPv4 header and one TCP header, so packet construction cannot fail"
1080    )]
1081    let mut tcp = MutableTcpPacket::new(ipv4.payload_mut()).expect("fixed-size TCP packet");
1082    tcp.set_source(source_port);
1083    tcp.set_destination(destination_port);
1084    tcp.set_sequence(sequence);
1085    tcp.set_data_offset(5);
1086    tcp.set_flags(TcpFlags::SYN);
1087    tcp.set_window(64240);
1088    tcp.set_checksum(ipv4_checksum(&tcp.to_immutable(), &source, &destination));
1089    ipv4.set_checksum(checksum(&ipv4.to_immutable()));
1090
1091    bytes
1092}
1093
1094/// Advances a progress deadline past `elapsed`, skipping any missed intervals.
1095fn advance_progress_deadline(mut deadline: Duration, elapsed: Duration) -> Duration {
1096    while deadline <= elapsed {
1097        deadline = next_progress_deadline(deadline);
1098    }
1099    deadline
1100}
1101
1102/// Returns the next progress deadline after `previous`.
1103///
1104/// Follows a growing schedule: every minute for the first ten minutes, every ten minutes through
1105/// the first hour, then every thirty minutes thereafter.
1106fn next_progress_deadline(previous: Duration) -> Duration {
1107    let interval = if previous < TEN_MINUTES {
1108        ONE_MINUTE
1109    } else if previous < ONE_HOUR {
1110        TEN_MINUTES
1111    } else {
1112        THIRTY_MINUTES
1113    };
1114    previous + interval
1115}
1116
1117/// Configuration for receiving packets.
1118struct ReceiveConfig<'a> {
1119    /// Map of expected (source, port) pairs to sequence numbers.
1120    expected: &'a ExpectedResponses,
1121    /// Source IP address to filter packets by.
1122    source: Ipv4Addr,
1123    /// Source port to filter packets by.
1124    source_port: Port,
1125    /// Whether to show closed connections.
1126    show_closed: bool,
1127    /// Atomic flag indicating when to stop receiving.
1128    done: &'a AtomicBool,
1129    /// Timeout duration for receiving packets.
1130    timeout: Duration,
1131}
1132
1133/// Reads and classifies raw packets until sending is done and the late-reply timeout elapses,
1134/// returning accepted results in arrival order.
1135fn receive(
1136    receiver: &mut TransportReceiver,
1137    config: &ReceiveConfig<'_>,
1138    on_result: &mut impl FnMut(ScanResult) -> Result<(), CallbackError>,
1139) -> Result<Vec<ScanResult>, ScanError> {
1140    let mut iterator = ipv4_packet_iter(receiver);
1141    let mut results = Vec::new();
1142    let mut seen = HashSet::new();
1143    let mut deadline = None;
1144
1145    loop {
1146        // Check the done flag and set the deadline only once.
1147        if config.done.load(Ordering::Acquire) && deadline.is_none() {
1148            deadline = Some(Instant::now() + config.timeout);
1149        }
1150        // Break the loop if the deadline has passed.
1151        if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
1152            break;
1153        }
1154
1155        // Calculate the wait duration based on the deadline (capped at 100ms).
1156        let wait = deadline
1157            .and_then(|deadline| deadline.checked_duration_since(Instant::now()))
1158            .unwrap_or(Duration::from_millis(100))
1159            .min(Duration::from_millis(100));
1160
1161        // Try to read a packet, blocking for at most `wait`.
1162        //
1163        // A genuine I/O error becomes a `ScanError::Receive` and ends the whole scan.
1164        // If no packet is received within `wait`, continue to the next iteration.
1165        let Some((ipv4, _)) = iterator
1166            .next_with_timeout(wait)
1167            .map_err(ScanError::Receive)?
1168        else {
1169            continue;
1170        };
1171
1172        // If a packet is received, classify it and add it to the results.
1173        //
1174        // If the packet is not a valid response, it is ignored and the scan continues.
1175        let Some(result) = classify_response(
1176            &ipv4,
1177            config.expected,
1178            config.source,
1179            config.source_port,
1180            config.show_closed,
1181            &mut seen,
1182        ) else {
1183            continue;
1184        };
1185
1186        // Call the user-defined callback with the accepted result and then add it to the results
1187        // in raw arrival order. Unlike a send-loop failure, a failing callback here currently
1188        // discards `results` entirely rather than preserving it via `IncompleteScanError`.
1189        on_result(result).map_err(ScanError::Callback)?;
1190        results.push(result);
1191    }
1192
1193    Ok(results)
1194}
1195
1196/// Correlates one received IPv4 packet against the expected-response table.
1197///
1198/// Returns `Some` only for a not-yet-seen reply whose destination address and port match the
1199/// scan's source, whose source host/port matches an actual probe, and whose acknowledgement
1200/// number matches the expected sequence.
1201fn classify_response(
1202    ipv4: &Ipv4Packet<'_>,
1203    expected: &ExpectedResponses,
1204    source: Ipv4Addr,
1205    source_port: Port,
1206    show_closed: bool,
1207    seen: &mut HashSet<(Ipv4Addr, Port)>,
1208) -> Option<ScanResult> {
1209    if ipv4.get_destination() != source {
1210        return None;
1211    }
1212
1213    let tcp = TcpPacket::new(ipv4.payload())?;
1214    let key = (ipv4.get_source(), tcp.get_source());
1215    let (host, port) = key;
1216    let sequence = expected.get(&key)?;
1217    if tcp.get_destination() != source_port || tcp.get_acknowledgement() != sequence.wrapping_add(1)
1218    {
1219        return None;
1220    }
1221
1222    let flags = tcp.get_flags();
1223    let state = if flags == TcpFlags::SYN | TcpFlags::ACK {
1224        PortState::Open
1225    } else if show_closed && (flags == TcpFlags::RST || flags == TcpFlags::RST | TcpFlags::ACK) {
1226        PortState::Closed
1227    } else {
1228        return None;
1229    };
1230
1231    // Return `None` if the key is already seen, to avoid duplicate results.
1232    if !seen.insert(key) {
1233        return None;
1234    }
1235
1236    Some(ScanResult { host, port, state })
1237}
1238
1239/// Extracts a human-readable message from a thread panic payload.
1240///
1241/// Falls back to a generic message when the payload isn't the common `&str` or `String` shape
1242/// produced by `panic!`.
1243fn describe_panic_payload(payload: &(dyn Any + Send)) -> &str {
1244    payload
1245        .downcast_ref::<&str>()
1246        .copied()
1247        .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
1248        .unwrap_or("unknown panic payload")
1249}
1250
1251#[cfg(test)]
1252#[expect(clippy::panic_in_result_fn, reason = "panics are allowed in test code")]
1253#[expect(clippy::unwrap_used, reason = "tests can use `unwrap`")]
1254mod tests {
1255    use std::path::PathBuf;
1256    use std::sync::atomic::AtomicUsize;
1257    use std::{env, fs, io, process};
1258
1259    use super::*;
1260
1261    fn response_packet(
1262        remote: Ipv4Addr,
1263        local: Ipv4Addr,
1264        remote_port: Port,
1265        local_port: Port,
1266        acknowledgement: SeqNum,
1267        flags: u8,
1268    ) -> Vec<u8> {
1269        let mut bytes = vec![0_u8; PACKET_LEN];
1270        let mut ipv4 = MutableIpv4Packet::new(&mut bytes).unwrap();
1271        ipv4.set_version(4);
1272        ipv4.set_header_length(5);
1273        ipv4.set_total_length(40);
1274        ipv4.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
1275        ipv4.set_source(remote);
1276        ipv4.set_destination(local);
1277
1278        let mut tcp = MutableTcpPacket::new(ipv4.payload_mut()).unwrap();
1279        tcp.set_source(remote_port);
1280        tcp.set_destination(local_port);
1281        tcp.set_acknowledgement(acknowledgement);
1282        tcp.set_data_offset(5);
1283        tcp.set_flags(flags);
1284        bytes
1285    }
1286
1287    fn classify_packet(
1288        bytes: &[u8],
1289        expected: &ExpectedResponses,
1290        source: Ipv4Addr,
1291        source_port: Port,
1292        show_closed: bool,
1293        seen: &mut HashSet<(Ipv4Addr, Port)>,
1294    ) -> Option<ScanResult> {
1295        let ipv4 = Ipv4Packet::new(bytes)?;
1296        classify_response(&ipv4, expected, source, source_port, show_closed, seen)
1297    }
1298
1299    fn services_path() -> PathBuf {
1300        static NEXT_FILE: AtomicUsize = AtomicUsize::new(0);
1301
1302        let number = NEXT_FILE.fetch_add(1, Ordering::Relaxed);
1303        env::temp_dir().join(format!("singsing-rs-services-{}-{number}", process::id()))
1304    }
1305
1306    fn services_from(contents: &str) -> anyhow::Result<Vec<u16>> {
1307        let path = services_path();
1308        fs::write(&path, contents)?;
1309        let result = ports_from_services(&path).map_err(anyhow::Error::from);
1310        fs::remove_file(path)?;
1311        result
1312    }
1313
1314    #[test]
1315    fn parses_ports_ranges_and_duplicates() {
1316        assert_eq!(parse_ports("22,80,79-81").unwrap(), [22, 79, 80, 81]);
1317    }
1318
1319    #[test]
1320    fn rejects_invalid_ports() {
1321        assert!(matches!(parse_ports("0"), Err(PortsError::PortZero)));
1322        assert!(matches!(
1323            parse_ports("80-79"),
1324            Err(PortsError::ReversedRange { item }) if item == "80-79"
1325        ));
1326        assert!(matches!(
1327            parse_ports("65536"),
1328            Err(PortsError::InvalidPort { input, .. }) if input == "65536"
1329        ));
1330        assert!(matches!(
1331            parse_ports("22,"),
1332            Err(PortsError::EmptyItem { input }) if input == "22,"
1333        ));
1334        assert!(matches!(
1335            parse_ports("1-2-3"),
1336            Err(PortsError::InvalidRange { item }) if item == "1-2-3"
1337        ));
1338    }
1339
1340    #[test]
1341    fn parses_host_and_network() {
1342        assert_eq!(
1343            parse_targets("192.168.2.9").unwrap(),
1344            ["192.168.2.9".parse::<Ipv4Addr>().unwrap()]
1345        );
1346        assert_eq!(
1347            parse_targets("192.168.2.0/30").unwrap(),
1348            [
1349                "192.168.2.1".parse::<Ipv4Addr>().unwrap(),
1350                "192.168.2.2".parse::<Ipv4Addr>().unwrap()
1351            ]
1352        );
1353        assert_eq!(
1354            parse_targets("192.168.2.0/31").unwrap(),
1355            [
1356                "192.168.2.0".parse::<Ipv4Addr>().unwrap(),
1357                "192.168.2.1".parse::<Ipv4Addr>().unwrap()
1358            ]
1359        );
1360        assert_eq!(
1361            parse_targets("192.168.2.7/32").unwrap(),
1362            ["192.168.2.7".parse::<Ipv4Addr>().unwrap()]
1363        );
1364    }
1365
1366    #[test]
1367    fn normalizes_host_bits_and_rejects_invalid_targets() {
1368        assert_eq!(
1369            parse_targets("192.168.2.7/30").unwrap(),
1370            [
1371                "192.168.2.5".parse::<Ipv4Addr>().unwrap(),
1372                "192.168.2.6".parse::<Ipv4Addr>().unwrap()
1373            ]
1374        );
1375        assert!(matches!(
1376            parse_targets(""),
1377            Err(TargetsError::InvalidAddress(_))
1378        ));
1379        assert!(matches!(
1380            parse_targets("not-an-address"),
1381            Err(TargetsError::InvalidAddress(_))
1382        ));
1383        assert!(matches!(
1384            parse_targets("192.168.2.1/33"),
1385            Err(TargetsError::InvalidNetwork(_))
1386        ));
1387    }
1388
1389    #[test]
1390    fn rejects_oversized_cidr_before_expansion() {
1391        let slash_8 = "10.0.0.0/8".parse::<Ipv4Net>().unwrap();
1392        let slash_31 = "192.168.2.0/31".parse::<Ipv4Net>().unwrap();
1393        let slash_32 = "192.168.2.1/32".parse::<Ipv4Net>().unwrap();
1394
1395        assert_eq!(usable_target_count(slash_8), Some(MAX_PROBES));
1396        assert_eq!(usable_target_count(slash_31), Some(2));
1397        assert_eq!(usable_target_count(slash_32), Some(1));
1398        assert!(matches!(
1399            parse_targets("10.0.0.0/7"),
1400            Err(TargetsError::TooLarge { max, .. }) if max == MAX_PROBES
1401        ));
1402        assert!(matches!(
1403            parse_targets("0.0.0.0/0"),
1404            Err(TargetsError::TooLarge { max, .. }) if max == MAX_PROBES
1405        ));
1406    }
1407
1408    #[test]
1409    fn resolves_loopback_interface_address() {
1410        assert_eq!(interface_ipv4("lo").unwrap(), Ipv4Addr::LOCALHOST);
1411    }
1412
1413    #[test]
1414    fn rejects_unknown_interface() {
1415        let name = "singsing-rs-interface-does-not-exist";
1416
1417        assert!(matches!(
1418            interface_ipv4(name),
1419            Err(InterfaceError::NotFound { name: n }) if n == name
1420        ));
1421    }
1422
1423    #[test]
1424    #[expect(
1425        clippy::as_conversions,
1426        reason = "`sequence >> 16` keeps only the top 16 bits, so it always fits in a `u16`"
1427    )]
1428    fn builds_valid_syn_packet() {
1429        let source = "192.168.2.1".parse().unwrap();
1430        let destination = "172.16.100.2".parse().unwrap();
1431        let sequence = 0x1234_5678;
1432        let bytes = syn_packet(source, destination, 50000, 443, sequence);
1433        let ipv4 = Ipv4Packet::new(&bytes).unwrap();
1434        let tcp = TcpPacket::new(ipv4.payload()).unwrap();
1435
1436        assert_eq!(bytes.len(), PACKET_LEN);
1437        assert_eq!(ipv4.get_version(), 4);
1438        assert_eq!(ipv4.get_header_length(), 5);
1439        assert_eq!(ipv4.get_total_length(), 40);
1440        assert_eq!(ipv4.get_identification(), (sequence >> 16) as u16);
1441        assert_eq!(ipv4.get_ttl(), 64);
1442        assert_eq!(ipv4.get_next_level_protocol(), IpNextHeaderProtocols::Tcp);
1443        assert_eq!(ipv4.get_source(), source);
1444        assert_eq!(ipv4.get_destination(), destination);
1445        let mut ip_for_checksum = MutableIpv4Packet::owned(bytes.clone()).unwrap();
1446        ip_for_checksum.set_checksum(0);
1447        assert_eq!(
1448            ipv4.get_checksum(),
1449            checksum(&ip_for_checksum.to_immutable())
1450        );
1451        let mut tcp_for_checksum = MutableTcpPacket::owned(tcp.packet().to_vec()).unwrap();
1452        tcp_for_checksum.set_checksum(0);
1453        assert_eq!(
1454            tcp.get_checksum(),
1455            ipv4_checksum(&tcp_for_checksum.to_immutable(), &source, &destination)
1456        );
1457        assert_eq!(tcp.packet().len(), 20);
1458        assert!(tcp.payload().is_empty());
1459        assert_eq!(tcp.get_source(), 50000);
1460        assert_eq!(tcp.get_destination(), 443);
1461        assert_eq!(tcp.get_sequence(), sequence);
1462        assert_eq!(tcp.get_acknowledgement(), 0);
1463        assert_eq!(tcp.get_data_offset(), 5);
1464        assert_eq!(tcp.get_flags(), TcpFlags::SYN);
1465        assert_eq!(tcp.get_window(), 64240);
1466        assert_eq!(tcp.get_urgent_ptr(), 0);
1467    }
1468
1469    #[test]
1470    fn accepts_open_response_once() {
1471        let source = "192.168.2.1".parse().unwrap();
1472        let target = "172.16.100.2".parse().unwrap();
1473        let source_port = 50000;
1474        let target_port = 443;
1475        let sequence = 0x1234_5678_u32;
1476        let expected = HashMap::from([((target, target_port), sequence)]);
1477        let open = ScanResult {
1478            host: target,
1479            port: target_port,
1480            state: PortState::Open,
1481        };
1482
1483        let valid_open = response_packet(
1484            target,
1485            source,
1486            target_port,
1487            source_port,
1488            sequence.wrapping_add(1),
1489            TcpFlags::SYN | TcpFlags::ACK,
1490        );
1491        let mut seen = HashSet::new();
1492        assert_eq!(
1493            classify_packet(
1494                &valid_open,
1495                &expected,
1496                source,
1497                source_port,
1498                false,
1499                &mut seen
1500            ),
1501            Some(open)
1502        );
1503        assert_eq!(
1504            classify_packet(
1505                &valid_open,
1506                &expected,
1507                source,
1508                source_port,
1509                false,
1510                &mut seen
1511            ),
1512            None
1513        );
1514    }
1515
1516    #[test]
1517    fn rejects_uncorrelated_responses() {
1518        let source = "192.168.2.1".parse().unwrap();
1519        let target = "172.16.100.2".parse().unwrap();
1520        let other_target = "172.16.100.3".parse().unwrap();
1521        let source_port = 50000;
1522        let target_port = 443;
1523        let sequence = 0x1234_5678_u32;
1524        let expected = HashMap::from([((target, target_port), sequence)]);
1525        let invalid_packets = [
1526            response_packet(
1527                target,
1528                "192.168.2.2".parse().unwrap(),
1529                target_port,
1530                source_port,
1531                sequence.wrapping_add(1),
1532                TcpFlags::SYN | TcpFlags::ACK,
1533            ),
1534            response_packet(
1535                other_target,
1536                source,
1537                target_port,
1538                source_port,
1539                sequence.wrapping_add(1),
1540                TcpFlags::SYN | TcpFlags::ACK,
1541            ),
1542            response_packet(
1543                target,
1544                source,
1545                80,
1546                source_port,
1547                sequence.wrapping_add(1),
1548                TcpFlags::SYN | TcpFlags::ACK,
1549            ),
1550            response_packet(
1551                target,
1552                source,
1553                target_port,
1554                source_port + 1,
1555                sequence.wrapping_add(1),
1556                TcpFlags::SYN | TcpFlags::ACK,
1557            ),
1558            response_packet(
1559                target,
1560                source,
1561                target_port,
1562                source_port,
1563                sequence,
1564                TcpFlags::SYN | TcpFlags::ACK,
1565            ),
1566        ];
1567        for packet in invalid_packets {
1568            assert_eq!(
1569                classify_packet(
1570                    &packet,
1571                    &expected,
1572                    source,
1573                    source_port,
1574                    false,
1575                    &mut HashSet::new()
1576                ),
1577                None
1578            );
1579        }
1580    }
1581
1582    #[test]
1583    fn reports_closed_responses_only_when_requested() {
1584        let source = "192.168.2.1".parse().unwrap();
1585        let target = "172.16.100.2".parse().unwrap();
1586        let source_port = 50000;
1587        let target_port = 443;
1588        let sequence = 0x1234_5678_u32;
1589        let expected = HashMap::from([((target, target_port), sequence)]);
1590        let closed_packet = response_packet(
1591            target,
1592            source,
1593            target_port,
1594            source_port,
1595            sequence.wrapping_add(1),
1596            TcpFlags::RST | TcpFlags::ACK,
1597        );
1598        let mut closed_seen = HashSet::new();
1599        assert_eq!(
1600            classify_packet(
1601                &closed_packet,
1602                &expected,
1603                source,
1604                source_port,
1605                false,
1606                &mut closed_seen
1607            ),
1608            None
1609        );
1610        assert_eq!(
1611            classify_packet(
1612                &closed_packet,
1613                &expected,
1614                source,
1615                source_port,
1616                true,
1617                &mut closed_seen
1618            ),
1619            Some(ScanResult {
1620                host: target,
1621                port: target_port,
1622                state: PortState::Closed,
1623            })
1624        );
1625    }
1626
1627    #[test]
1628    fn ignores_truncated_and_unexpected_responses() {
1629        let source = "192.168.2.1".parse().unwrap();
1630        let target = "172.16.100.2".parse().unwrap();
1631        let source_port = 50000;
1632        let target_port = 443;
1633        let sequence = 0x1234_5678_u32;
1634        let expected = HashMap::from([((target, target_port), sequence)]);
1635        let mut truncated = vec![0_u8; 20];
1636        let mut ipv4 = MutableIpv4Packet::new(&mut truncated).unwrap();
1637        ipv4.set_version(4);
1638        ipv4.set_header_length(5);
1639        ipv4.set_total_length(20);
1640        ipv4.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
1641        ipv4.set_source(target);
1642        ipv4.set_destination(source);
1643
1644        let mut seen = HashSet::new();
1645        assert_eq!(
1646            classify_packet(&truncated, &expected, source, source_port, false, &mut seen),
1647            None
1648        );
1649        for flags in [TcpFlags::ACK, TcpFlags::SYN | TcpFlags::ACK | TcpFlags::RST] {
1650            let packet = response_packet(
1651                target,
1652                source,
1653                target_port,
1654                source_port,
1655                sequence.wrapping_add(1),
1656                flags,
1657            );
1658            assert_eq!(
1659                classify_packet(&packet, &expected, source, source_port, true, &mut seen),
1660                None
1661            );
1662        }
1663
1664        let valid = response_packet(
1665            target,
1666            source,
1667            target_port,
1668            source_port,
1669            sequence.wrapping_add(1),
1670            TcpFlags::SYN | TcpFlags::ACK,
1671        );
1672        assert!(
1673            classify_packet(&valid, &expected, source, source_port, false, &mut seen).is_some()
1674        );
1675    }
1676
1677    #[test]
1678    fn accepts_wrapped_acknowledgement_number() {
1679        let source = "192.168.2.1".parse().unwrap();
1680        let target = "172.16.100.2".parse().unwrap();
1681        let source_port = 50000;
1682        let target_port = 443;
1683        let expected = HashMap::from([((target, target_port), u32::MAX)]);
1684        let response = response_packet(
1685            target,
1686            source,
1687            target_port,
1688            source_port,
1689            0,
1690            TcpFlags::SYN | TcpFlags::ACK,
1691        );
1692
1693        assert!(
1694            classify_packet(
1695                &response,
1696                &expected,
1697                source,
1698                source_port,
1699                false,
1700                &mut HashSet::new()
1701            )
1702            .is_some()
1703        );
1704    }
1705
1706    #[test]
1707    fn validates_scan_limits_and_configuration() {
1708        let timeout = Duration::from_secs(30);
1709
1710        assert_eq!(
1711            validate_probe_count(254, 65_535, 15, timeout).unwrap(),
1712            16_645_890
1713        );
1714        assert_eq!(
1715            validate_probe_count(256, 65_535, 15, timeout).unwrap(),
1716            16_776_960
1717        );
1718        assert_eq!(
1719            validate_probe_count(MAX_PROBES, 1, 15, timeout).unwrap(),
1720            MAX_PROBES
1721        );
1722        assert_eq!(validate_probe_count(1, 1, 15, MAX_TIMEOUT).unwrap(), 1);
1723        assert!(matches!(
1724            validate_probe_count(257, 65_535, 15, timeout),
1725            Err(ScanError::TooManyProbes { probe_count: 16_842_495, max }) if max == MAX_PROBES
1726        ));
1727        assert!(matches!(
1728            validate_probe_count(MAX_PROBES + 1, 1, 15, timeout),
1729            Err(ScanError::TooManyProbes { max, .. }) if max == MAX_PROBES
1730        ));
1731        assert!(matches!(
1732            validate_probe_count(usize::MAX, 2, 15, timeout),
1733            Err(ScanError::ScanSizeOverflow)
1734        ));
1735        assert!(matches!(
1736            validate_probe_count(0, 1, 15, timeout),
1737            Err(ScanError::EmptyScan)
1738        ));
1739        assert!(matches!(
1740            validate_probe_count(1, 0, 15, timeout),
1741            Err(ScanError::EmptyScan)
1742        ));
1743        assert!(matches!(
1744            validate_probe_count(1, 1, 0, timeout),
1745            Err(ScanError::ZeroBandwidth)
1746        ));
1747        assert!(matches!(
1748            validate_probe_count(1, 1, 15, MAX_TIMEOUT + Duration::from_secs(1)),
1749            Err(ScanError::TimeoutTooLarge { max, .. }) if max == MAX_TIMEOUT
1750        ));
1751    }
1752
1753    #[test]
1754    fn timeout_too_large_reports_both_durations() {
1755        let timeout = MAX_TIMEOUT + Duration::from_secs(1);
1756
1757        let error = validate_probe_count(1, 1, 15, timeout).unwrap_err();
1758
1759        assert_eq!(
1760            error.to_string(),
1761            format!("timeout of {timeout:?} exceeds the maximum of {MAX_TIMEOUT:?}")
1762        );
1763    }
1764
1765    #[test]
1766    fn rejects_duplicate_scan_config_entries() {
1767        let host = "192.168.2.1".parse().unwrap();
1768        let duplicate_targets = ScanConfig::new(vec![host, host], vec![443], host);
1769        let duplicate_ports = ScanConfig::new(vec![host], vec![443, 443], host);
1770        let unique = ScanConfig::new(vec![host], vec![80, 443], host);
1771
1772        assert!(
1773            expected_responses(&duplicate_targets, 1, 2)
1774                .unwrap_err()
1775                .to_string()
1776                .contains("must be unique")
1777        );
1778        assert!(
1779            expected_responses(&duplicate_ports, 1, 2)
1780                .unwrap_err()
1781                .to_string()
1782                .contains("must be unique")
1783        );
1784        assert_eq!(expected_responses(&unique, 1, 2).unwrap().len(), 2);
1785    }
1786
1787    #[test]
1788    fn parses_tcp_services_and_ignores_other_entries() -> anyhow::Result<()> {
1789        let ports = services_from(
1790            "\
1791# comment
1792ssh             22/tcp
1793domain          53/udp
1794http            80/tcp  www # inline comment
1795http-alt        80/tcp
1796malformed
1797invalid         nope/tcp
1798zero            0/tcp
1799",
1800        )?;
1801
1802        assert_eq!(ports, [22, 80]);
1803        Ok(())
1804    }
1805
1806    #[test]
1807    fn rejects_services_file_without_tcp_ports() {
1808        let path = services_path();
1809        fs::write(&path, "domain 53/udp\n# comment\nmalformed\n").unwrap();
1810        let error = ports_from_services(&path).unwrap_err();
1811        fs::remove_file(&path).unwrap();
1812
1813        assert!(matches!(error, PortsError::NoTcpServices { path: p } if p == path));
1814    }
1815
1816    #[test]
1817    fn reports_missing_services_file_path() {
1818        let path = services_path();
1819        let error = ports_from_services(&path).unwrap_err();
1820
1821        assert!(matches!(
1822            &error,
1823            PortsError::ServicesFileRead { path: p, .. } if p == &path
1824        ));
1825        assert!(format!("{error:#}").contains(&path.display().to_string()));
1826    }
1827
1828    #[test]
1829    fn incomplete_scan_error_preserves_context() {
1830        let host = "172.16.100.2".parse().unwrap();
1831        let partial_result = ScanResult {
1832            host,
1833            port: 443,
1834            state: PortState::Open,
1835        };
1836        let incomplete = IncompleteScanError {
1837            source: SendError::Io {
1838                host,
1839                port: 443,
1840                source: io::Error::other("send failed"),
1841            },
1842            partial_results: vec![partial_result],
1843            probes_sent: 7,
1844            total_probes: 10,
1845        };
1846
1847        assert_eq!(incomplete.partial_results(), [partial_result]);
1848        assert_eq!(incomplete.probes_sent(), 7);
1849        assert_eq!(incomplete.total_probes(), 10);
1850        assert_eq!(
1851            incomplete.to_string(),
1852            "scan stopped after sending 7 of 10 probes"
1853        );
1854        assert_eq!(
1855            Error::source(&incomplete).unwrap().to_string(),
1856            "failed to send SYN to 172.16.100.2:443"
1857        );
1858
1859        let error: anyhow::Error = incomplete.into();
1860        assert!(error.downcast_ref::<IncompleteScanError>().is_some());
1861        assert_eq!(
1862            format!("{error:#}"),
1863            "scan stopped after sending 7 of 10 probes: \
1864             failed to send SYN to 172.16.100.2:443: send failed"
1865        );
1866    }
1867
1868    #[test]
1869    fn describes_str_panic_payload() {
1870        let payload: Box<dyn Any + Send> = Box::new("boom");
1871        assert_eq!(describe_panic_payload(&*payload), "boom");
1872    }
1873
1874    #[test]
1875    fn describes_string_panic_payload() {
1876        let payload: Box<dyn Any + Send> = Box::new(String::from("boom"));
1877        assert_eq!(describe_panic_payload(&*payload), "boom");
1878    }
1879
1880    #[test]
1881    fn describes_unrecognized_panic_payload() {
1882        let payload: Box<dyn Any + Send> = Box::new(42_i32);
1883        assert_eq!(describe_panic_payload(&*payload), "unknown panic payload");
1884    }
1885
1886    #[test]
1887    fn estimates_scan_progress() {
1888        let progress = ScanProgress {
1889            probes_sent: 25,
1890            total_probes: 100,
1891            elapsed: Duration::from_secs(60),
1892        };
1893
1894        assert_eq!(progress.percent(), 25);
1895        assert_eq!(
1896            progress.estimated_remaining(),
1897            Some(Duration::from_secs(180))
1898        );
1899    }
1900
1901    #[test]
1902    fn handles_scan_progress_boundaries() {
1903        let no_probes = ScanProgress {
1904            probes_sent: 0,
1905            total_probes: 0,
1906            elapsed: Duration::from_secs(60),
1907        };
1908        let not_started = ScanProgress {
1909            total_probes: 100,
1910            ..no_probes
1911        };
1912        let complete = ScanProgress {
1913            probes_sent: 100,
1914            ..not_started
1915        };
1916        let over_complete = ScanProgress {
1917            probes_sent: 101,
1918            ..complete
1919        };
1920
1921        assert_eq!(no_probes.percent(), 0);
1922        assert_eq!(no_probes.estimated_remaining(), None);
1923        assert_eq!(not_started.percent(), 0);
1924        assert_eq!(not_started.estimated_remaining(), None);
1925        assert_eq!(complete.percent(), 100);
1926        assert_eq!(complete.estimated_remaining(), Some(Duration::ZERO));
1927        assert_eq!(over_complete.estimated_remaining(), Some(Duration::ZERO));
1928    }
1929
1930    #[test]
1931    fn probe_limit_accommodates_single_port_slash_8() {
1932        assert_eq!(MAX_PROBES, 16_777_214);
1933    }
1934
1935    #[test]
1936    fn progress_schedule_uses_increasing_intervals() {
1937        assert_eq!(next_progress_deadline(Duration::from_mins(9)), TEN_MINUTES);
1938        assert_eq!(next_progress_deadline(TEN_MINUTES), Duration::from_mins(20));
1939        assert_eq!(next_progress_deadline(Duration::from_mins(50)), ONE_HOUR);
1940        assert_eq!(next_progress_deadline(ONE_HOUR), Duration::from_mins(90));
1941    }
1942
1943    #[test]
1944    fn progress_schedule_skips_missed_deadlines() {
1945        assert_eq!(
1946            advance_progress_deadline(ONE_MINUTE, Duration::from_mins(35)),
1947            Duration::from_mins(40)
1948        );
1949    }
1950}