use std::fmt;
use zond_engine::model::parse::ip::{Keyword, ResolverFn, ZoneResolverFn, names_keyword};
use zond_engine::model::parse::target::{self as engine_parse, TargetContext, TargetParseError};
use zond_engine::resolve;
use zond_engine::system::interface;
use zond_engine::{IpSet, PortSet, Resolver, TargetMap, ZondConfig};
pub(crate) const MAX_IPV4_ADDRESSES: u128 = 1 << 20;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub(crate) enum TargetError {
#[error("{0}")]
Parse(#[from] TargetParseError),
#[error(
"'{expression}' is a hostname, and this run may not send DNS — from \
--no-dns, or `no_dns` in engine.toml. Give its address instead, or \
allow DNS."
)]
NameNeedsDns {
expression: String,
},
#[error(
"{expression} covers {requested} IPv4 addresses, more than one run will \
sweep (the limit is {limit}). Give a smaller prefix, a range, or the \
specific addresses you mean."
)]
TooLarge {
expression: String,
requested: u128,
limit: u128,
},
#[error(
"{expression} comes to {requested} probes, more than one scan will spend \
(the limit is {limit}). Narrow the addresses, the ports, or both."
)]
TooManyProbes {
expression: String,
requested: u128,
limit: u128,
},
}
#[derive(Debug, Clone)]
struct Asked {
expressions: Vec<String>,
segment_sweep: bool,
}
impl Asked {
fn from_expressions<S: AsRef<str>>(expressions: &[S]) -> Self {
Self::new(expressions, names_keyword(expressions, Keyword::Lan))
}
fn new<S: AsRef<str>>(expressions: &[S], segment_sweep: bool) -> Self {
Self {
expressions: expressions
.iter()
.map(|expression| expression.as_ref().trim().to_owned())
.collect(),
segment_sweep,
}
}
fn apply_to(&self, cfg: &mut ZondConfig) {
cfg.segment_sweep = self.segment_sweep;
}
}
impl fmt::Display for Asked {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.expressions.join(", "))
}
}
#[derive(Debug, Clone)]
pub(crate) struct Targets {
asked: Asked,
ips: IpSet,
}
impl Targets {
#[must_use]
pub(crate) fn into_ips(self) -> IpSet {
self.ips
}
#[must_use]
pub(crate) fn len(&self) -> u128 {
self.ips.len()
}
pub(crate) fn apply_to(&self, cfg: &mut ZondConfig) {
self.asked.apply_to(cfg);
}
}
impl fmt::Display for Targets {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.asked.fmt(f)
}
}
pub(crate) async fn resolve<S: AsRef<str>>(
expressions: &[S],
resolve_names: bool,
) -> Result<Targets, TargetError> {
let resolver = resolve_names.then(Resolver::from_system);
let discovery = resolve::for_discovery(expressions, resolver.as_ref())
.await
.map_err(name_needs_dns)?;
let asked = Asked::new(expressions, discovery.segment_sweep());
let requested = discovery.ips().v4_len();
if requested > MAX_IPV4_ADDRESSES {
return Err(TargetError::TooLarge {
expression: asked.to_string(),
requested,
limit: MAX_IPV4_ADDRESSES,
});
}
Ok(Targets {
asked,
ips: discovery.into_ips(),
})
}
fn name_needs_dns(error: TargetParseError) -> TargetError {
match error {
TargetParseError::NoHostLookup(expression) => TargetError::NameNeedsDns { expression },
other => TargetError::Parse(other),
}
}
pub(crate) const MAX_PROBES: u128 = 1 << 22;
#[derive(Debug, Clone)]
pub(crate) struct ScanTargets {
asked: Asked,
map: TargetMap,
}
impl ScanTargets {
#[must_use]
pub(crate) fn into_map(self) -> TargetMap {
self.map
}
#[must_use]
pub(crate) fn probes(&self) -> u128 {
self.map.gross_targets().unwrap_or(u128::MAX)
}
#[must_use]
pub(crate) fn hosts(&self) -> u128 {
self.map.gross_ips().unwrap_or(u128::MAX)
}
pub(crate) fn apply_to(&self, cfg: &mut ZondConfig) {
self.asked.apply_to(cfg);
}
}
impl fmt::Display for ScanTargets {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.asked.fmt(f)
}
}
fn host_context() -> TargetContext<'static> {
const KEYWORDS: ResolverFn<'static> = &interface::resolve_keyword;
const ZONES: ZoneResolverFn<'static> = &interface::resolve_zone;
TargetContext {
keywords: Some(KEYWORDS),
zones: Some(ZONES),
hosts: None,
}
}
pub(crate) async fn resolve_ports<S: AsRef<str>>(
expressions: &[S],
ports: PortSet,
resolve_names: bool,
) -> Result<ScanTargets, TargetError> {
let context = host_context();
let map = match resolve_names.then(Resolver::from_system) {
Some(resolver) => resolve::to_target_map(expressions, ports, &context, &resolver)
.await
.map_err(name_needs_dns)?,
None => {
engine_parse::to_target_map(expressions, ports, &context).map_err(name_needs_dns)?
}
};
let targets = ScanTargets {
asked: Asked::from_expressions(expressions),
map,
};
let requested = targets.probes();
if requested > MAX_PROBES {
return Err(TargetError::TooManyProbes {
expression: targets.to_string(),
requested,
limit: MAX_PROBES,
});
}
Ok(targets)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
async fn offline<S: AsRef<str>>(expressions: &[S]) -> Result<Targets, TargetError> {
resolve(expressions, false).await
}
#[tokio::test]
async fn a_cidr_block_covers_every_address_in_it() {
let targets = offline(&["192.168.0.0/24"])
.await
.expect("a well-formed block");
assert_eq!(targets.len(), 256);
assert!(
targets
.into_ips()
.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 255)))
);
}
#[tokio::test]
async fn a_shortened_range_ends_where_its_last_octets_say() {
let targets = offline(&["192.168.0.1-50"])
.await
.expect("a well-formed range");
assert_eq!(targets.len(), 50);
let ips = targets.into_ips();
assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 50))));
assert!(!ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 51))));
}
#[tokio::test]
async fn several_expressions_and_comma_separated_lists_are_one_set() {
let targets = offline(&["10.0.0.1,10.0.0.2", "10.0.0.3"])
.await
.expect("well-formed");
assert_eq!(targets.len(), 3);
}
#[tokio::test]
async fn an_ipv6_address_and_prefix_are_targets_like_any_other() {
let single = offline(&["2001:db8::1"])
.await
.expect("a well-formed address");
assert_eq!(single.len(), 1);
let prefix = offline(&["2001:db8::/120"])
.await
.expect("a well-formed prefix");
assert_eq!(prefix.len(), 256);
}
#[test]
fn both_phases_write_the_same_settings() {
for sweep in [false, true] {
let asked = Asked::new(&["lan"], sweep);
let discovery = Targets {
asked: asked.clone(),
ips: IpSet::new(),
};
let port_scan = ScanTargets {
asked,
map: TargetMap::default(),
};
let mut from_discovery = ZondConfig::default();
let mut from_port_scan = ZondConfig::default();
discovery.apply_to(&mut from_discovery);
port_scan.apply_to(&mut from_port_scan);
assert_eq!(from_discovery.segment_sweep, sweep);
assert_eq!(from_port_scan.segment_sweep, sweep);
}
}
#[test]
fn a_port_scan_reads_the_sweep_out_of_the_expressions() {
assert!(Asked::from_expressions(&["lan"]).segment_sweep);
assert!(Asked::from_expressions(&["LAN"]).segment_sweep, "case");
assert!(
Asked::from_expressions(&["10.0.0.1,lan"]).segment_sweep,
"a comma-separated list is still a list of targets"
);
assert!(!Asked::from_expressions(&["10.0.0.0/24"]).segment_sweep);
}
#[test]
fn targets_that_name_no_network_turn_the_sweep_off_again() {
let mut config = ZondConfig {
segment_sweep: true,
..ZondConfig::default()
};
Asked::new(&["10.0.0.1"], false).apply_to(&mut config);
assert!(!config.segment_sweep);
}
#[tokio::test]
async fn a_message_quotes_the_expressions_back() {
let targets = offline(&[" 10.0.0.1 ", "192.168.0.0/30"])
.await
.expect("well-formed");
assert_eq!(targets.to_string(), "10.0.0.1, 192.168.0.0/30");
assert_eq!(targets.len(), 5, "one address plus a /30");
}
#[tokio::test]
async fn something_that_is_not_a_target_is_refused() {
assert!(matches!(
offline(&["192.168.0.300"]).await,
Err(TargetError::Parse(_))
));
}
#[tokio::test]
async fn a_hostname_under_no_dns_names_the_flag_responsible() {
let refused = offline(&["one.one.one.one"]).await;
let Err(error @ TargetError::NameNeedsDns { .. }) = refused else {
panic!("a hostname cannot be resolved with DNS forbidden");
};
let message = error.to_string();
assert!(message.contains("one.one.one.one"), "got {message:?}");
assert!(message.contains("--no-dns"), "got {message:?}");
assert!(message.contains("engine.toml"), "got {message:?}");
}
#[tokio::test]
async fn the_largest_accepted_ipv4_range_is_accepted() {
let accepted = offline(&["10.0.0.0/12"])
.await
.expect("a /12 is within the limit");
assert_eq!(accepted.len(), MAX_IPV4_ADDRESSES);
}
#[tokio::test]
async fn an_ipv4_range_beyond_the_limit_is_refused() {
let refused = offline(&["10.0.0.0/8"]).await;
let Err(TargetError::TooLarge { requested, .. }) = refused else {
panic!("a /8 is more than one run will sweep");
};
assert_eq!(requested, 1 << 24);
}
#[tokio::test]
async fn a_large_ipv6_prefix_is_left_for_the_engine_to_judge() {
let accepted = offline(&["2001:db8::/64"])
.await
.expect("this is the engine's call, not this module's");
assert_eq!(accepted.len(), 1u128 << 64);
}
}