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
38const PACKET_LEN: usize = 40;
40const RECEIVE_BUFFER_LEN: usize = 1 << 20;
49
50const MAX_PROBES: usize = 16_777_214;
52const MAX_TIMEOUT: Duration = Duration::from_hours(24);
54
55const ONE_MINUTE: Duration = Duration::from_mins(1);
57const TEN_MINUTES: Duration = Duration::from_mins(10);
59const THIRTY_MINUTES: Duration = Duration::from_mins(30);
61const ONE_HOUR: Duration = Duration::from_hours(1);
63
64pub type Port = u16;
66type SeqNum = u32;
68type ExpectedResponses = HashMap<(Ipv4Addr, Port), SeqNum>;
70
71pub type CallbackError = Box<dyn Error + Send + Sync>;
77
78pub use ipnet::{AddrParseError, Ipv4Net};
83
84#[derive(Debug, thiserror::Error)]
98#[non_exhaustive]
99pub enum InterfaceError {
100 #[error("network interface {name:?} does not exist")]
102 NotFound {
103 name: String,
105 },
106 #[error("network interface {name:?} has no IPv4 address")]
108 NoIpv4 {
109 name: String,
111 },
112}
113
114#[derive(Debug, thiserror::Error)]
127#[non_exhaustive]
128pub enum TargetsError {
129 #[error("invalid IPv4 network")]
131 InvalidNetwork(#[source] AddrParseError),
132 #[error("invalid IPv4 address")]
134 InvalidAddress(#[source] AddrParseError),
135 #[error("{network} contains more than {max} usable addresses; split networks larger than a /8")]
137 TooLarge {
138 network: Ipv4Net,
140 max: usize,
142 },
143}
144
145#[derive(Debug, thiserror::Error)]
155#[non_exhaustive]
156pub enum PortsError {
157 #[error("empty port in {input:?}")]
159 EmptyItem {
160 input: String,
162 },
163 #[error("invalid port range {item:?}")]
165 InvalidRange {
166 item: String,
168 },
169 #[error("reversed port range {item:?}")]
171 ReversedRange {
172 item: String,
174 },
175 #[error("invalid TCP port {input:?}")]
177 InvalidPort {
178 input: String,
180 #[source]
182 source: ParseIntError,
183 },
184 #[error("TCP port zero is not supported")]
186 PortZero,
187 #[error("failed to read {}", path.display())]
189 ServicesFileRead {
190 path: PathBuf,
192 #[source]
194 source: io::Error,
195 },
196 #[error("{} contains no TCP services", path.display())]
198 NoTcpServices {
199 path: PathBuf,
201 },
202}
203
204#[derive(Debug, thiserror::Error)]
216#[non_exhaustive]
217pub enum ScanError {
218 #[error("at least one target and one port are required")]
220 EmptyScan,
221 #[error("bandwidth must be greater than zero")]
223 ZeroBandwidth,
224 #[error("bandwidth is too large")]
226 BandwidthOverflow,
227 #[error("timeout of {timeout:?} exceeds the maximum of {max:?}")]
229 TimeoutTooLarge {
230 timeout: Duration,
232 max: Duration,
234 },
235 #[error("scan size overflow")]
237 ScanSizeOverflow,
238 #[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 probe_count: usize,
246 max: usize,
248 },
249 #[error("duplicate host/port pair {host}:{port}; ScanConfig targets and ports must be unique")]
251 DuplicatePair {
252 host: Ipv4Addr,
254 port: Port,
256 },
257 #[error("failed to create raw socket (run as root or grant CAP_NET_RAW)")]
259 SocketCreation(#[source] io::Error),
260 #[error("failed to receive raw packet")]
262 Receive(#[source] io::Error),
263 #[error("packet receiver thread panicked: {0}")]
265 ReceiverPanicked(String),
266 #[error(transparent)]
268 Incomplete(IncompleteScanError),
269 #[error("callback failed")]
271 Callback(#[source] CallbackError),
272}
273
274#[derive(Debug, thiserror::Error)]
276#[non_exhaustive]
277pub enum SendError {
278 #[error("failed to construct IPv4 packet")]
280 PacketConstruction,
281 #[error("failed to send SYN to {host}:{port}")]
283 Io {
284 host: Ipv4Addr,
286 port: Port,
288 #[source]
290 source: io::Error,
291 },
292 #[error("callback failed")]
294 Callback(#[source] CallbackError),
295}
296
297#[derive(Debug, thiserror::Error)]
321#[error("scan stopped after sending {probes_sent} of {total_probes} probes")]
322pub struct IncompleteScanError {
323 #[source]
325 source: SendError,
326 partial_results: Vec<ScanResult>,
328 probes_sent: usize,
330 total_probes: usize,
332}
333
334impl IncompleteScanError {
335 #[must_use]
337 pub fn partial_results(&self) -> &[ScanResult] {
338 &self.partial_results
339 }
340
341 #[must_use]
343 pub const fn probes_sent(&self) -> usize {
344 self.probes_sent
345 }
346
347 #[must_use]
349 pub const fn total_probes(&self) -> usize {
350 self.total_probes
351 }
352}
353
354#[derive(Clone, Debug, Eq, Hash, PartialEq)]
356#[non_exhaustive]
357pub struct ScanConfig {
358 pub targets: Vec<Ipv4Addr>,
362 pub ports: Vec<Port>,
366 pub source: Ipv4Addr,
368 pub bandwidth_kib: u64,
373 pub timeout: Duration,
377 pub show_closed: bool,
379}
380
381impl ScanConfig {
382 #[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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
413#[non_exhaustive]
414pub struct ScanProgress {
415 pub probes_sent: usize,
417 pub total_probes: usize,
419 pub elapsed: Duration,
421}
422
423impl ScanProgress {
424 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
474#[non_exhaustive]
475pub enum PortState {
476 Open,
478 Closed,
480}
481
482#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
484#[non_exhaustive]
485pub struct ScanResult {
486 pub host: Ipv4Addr,
488 pub port: Port,
490 pub state: PortState,
492}
493
494impl ScanResult {
495 #[must_use]
497 pub const fn new(host: Ipv4Addr, port: Port, state: PortState) -> Self {
498 Self { host, port, state }
499 }
500}
501
502pub 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
537pub 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
579pub 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 (parse_port(start)?, parse_port(end)?)
613 } else {
614 let port = parse_port(item)?;
616 (port, port)
617 };
618
619 if start > end {
621 return Err(PortsError::ReversedRange {
622 item: item.to_owned(),
623 });
624 }
625
626 ports.extend(start..=end);
628 }
629
630 Ok(ports.into_iter().collect())
631}
632
633pub 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 let mut fields = line
667 .split('#')
668 .next()
669 .unwrap_or_default()
670 .split_whitespace();
671
672 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
691pub fn scan(config: &ScanConfig) -> Result<Vec<ScanResult>, ScanError> {
718 scan_with_callbacks(config, |_| Ok(()), |_| Ok(()))
719}
720
721pub 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
752pub 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 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 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 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 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 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 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 next_send += interval;
864 if let Some(delay) = next_send.checked_duration_since(Instant::now()) {
865 thread::sleep(delay);
866 }
867
868 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 done.store(true, Ordering::Release);
894
895 let mut results = receive_thread.join().map_err(|payload| {
900 ScanError::ReceiverPanicked(describe_panic_payload(&*payload).to_owned())
901 })??;
902
903 results.sort_unstable_by_key(|result| (u32::from(result.host), result.port));
905
906 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
919fn 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
933fn 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
947fn 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
957fn 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#[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
1002fn nonce() -> u32 {
1006 SystemTime::now()
1007 .duration_since(UNIX_EPOCH)
1008 .unwrap_or_default()
1009 .subsec_nanos()
1010}
1011
1012fn 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
1036fn 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
1049fn 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
1094fn advance_progress_deadline(mut deadline: Duration, elapsed: Duration) -> Duration {
1096 while deadline <= elapsed {
1097 deadline = next_progress_deadline(deadline);
1098 }
1099 deadline
1100}
1101
1102fn 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
1117struct ReceiveConfig<'a> {
1119 expected: &'a ExpectedResponses,
1121 source: Ipv4Addr,
1123 source_port: Port,
1125 show_closed: bool,
1127 done: &'a AtomicBool,
1129 timeout: Duration,
1131}
1132
1133fn 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 if config.done.load(Ordering::Acquire) && deadline.is_none() {
1148 deadline = Some(Instant::now() + config.timeout);
1149 }
1150 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
1152 break;
1153 }
1154
1155 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 let Some((ipv4, _)) = iterator
1166 .next_with_timeout(wait)
1167 .map_err(ScanError::Receive)?
1168 else {
1169 continue;
1170 };
1171
1172 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 on_result(result).map_err(ScanError::Callback)?;
1190 results.push(result);
1191 }
1192
1193 Ok(results)
1194}
1195
1196fn 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 if !seen.insert(key) {
1233 return None;
1234 }
1235
1236 Some(ScanResult { host, port, state })
1237}
1238
1239fn 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}