use std::collections::HashSet;
use std::io::ErrorKind as IoErrorKind;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::time::Duration;
use anyhow::{anyhow, bail, Result};
use futures::stream::{self, StreamExt};
use tokio::io::ErrorKind as TokioErrorKind;
use tokio::net::{TcpStream, UdpSocket};
use crate::cli::Args;
use crate::ip_proto::MAX_IP_PROTO_PARALLEL_SHARDS;
use crate::ping::ping_hosts;
use crate::ports::parse_port_spec;
use crate::scan::PortLine;
use crate::sctp::MAX_SCTP_PARALLEL_SHARDS;
use crate::syn::MAX_SYN_PARALLEL_SHARDS;
const DEFAULT_TCP_DISCOVERY_PORTS: &[u16] = &[443, 80];
const TCP_SYN_DEFAULT: u16 = 80;
const TCP_ACK_DEFAULT: u16 = 80;
const UDP_PING_DEFAULT: u16 = 40_125;
const SCTP_PING_DEFAULT: u16 = 80;
fn has_explicit_discovery_flags(args: &Args) -> bool {
args.ping_echo
|| args.ping_syn.is_some()
|| args.ping_ack.is_some()
|| args.ping_udp.is_some()
|| args.ping_sctp.is_some()
|| args.ping_timestamp
|| args.ping_mask
|| args.ping_ip_proto.is_some()
}
fn has_implemented_explicit_probes(args: &Args) -> bool {
args.ping_echo
|| args.ping_syn.is_some()
|| args.ping_ack.is_some()
|| args.ping_udp.is_some()
|| args.ping_sctp.is_some()
|| args.ping_ip_proto.is_some()
|| args.ping_timestamp
|| args.ping_mask
}
fn ports_from_ping_tcp(opt: &Option<Option<String>>, default: u16) -> Result<Vec<u16>> {
match opt {
None => Ok(vec![]),
Some(None) => Ok(vec![default]),
Some(Some(s)) => parse_port_spec(s.trim()).map_err(|e| anyhow!("{e}")),
}
}
fn ports_from_ping_udp(opt: &Option<Option<String>>) -> Result<Vec<u16>> {
match opt {
None => Ok(vec![]),
Some(None) => Ok(vec![UDP_PING_DEFAULT]),
Some(Some(s)) => parse_port_spec(s.trim()).map_err(|e| anyhow!("{e}")),
}
}
fn ports_from_ping_sctp(opt: &Option<Option<String>>) -> Result<Vec<u16>> {
match opt {
None => Ok(vec![]),
Some(None) => Ok(vec![SCTP_PING_DEFAULT]),
Some(Some(s)) => parse_port_spec(s.trim()).map_err(|e| anyhow!("{e}")),
}
}
fn parse_ping_ip_proto_list(s: &str) -> Result<Vec<u16>> {
let s = s.trim();
if s.is_empty() {
return Ok(vec![1, 2, 4]);
}
let mut out = Vec::new();
for part in s.split(',') {
let p = part.trim();
if p.is_empty() {
continue;
}
let n: u16 = p
.parse()
.map_err(|_| anyhow!("invalid protocol number in -PO: {p}"))?;
if n > 255 {
bail!("protocol number in -PO must be 0..=255, got {n}");
}
out.push(n);
}
if out.is_empty() {
bail!("-PO protocol list is empty");
}
out.sort_unstable();
out.dedup();
Ok(out)
}
fn port_lines_to_alive_hosts(lines: Vec<PortLine>) -> HashSet<IpAddr> {
lines
.into_iter()
.filter(|l| l.state == "open" || l.state == "closed")
.map(|l| l.host)
.collect()
}
async fn syn_raw_discovery_collect(
hosts: &[IpAddr],
ports: &[u16],
skip: Option<&HashSet<IpAddr>>,
timeout: Duration,
max_shards: usize,
) -> std::io::Result<HashSet<IpAddr>> {
let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
for &h in hosts {
if skip.map(|s| s.contains(&h)).unwrap_or(false) {
continue;
}
for &p in ports {
match h {
IpAddr::V4(a) => v4.push((a, p)),
IpAddr::V6(a) => v6.push((a, p)),
}
}
}
let v4_fut = async {
if v4.is_empty() {
return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
}
tokio::task::spawn_blocking(move || {
let lines = crate::syn::parallel_syn_scan_ipv4(
v4, timeout, None, None, None, None, None, 0, max_shards,
)?;
Ok(port_lines_to_alive_hosts(lines))
})
.await
.map_err(|e| std::io::Error::other(format!("{e}")))?
};
let v6_fut = async {
if v6.is_empty() {
return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
}
tokio::task::spawn_blocking(move || {
let lines = crate::syn::parallel_syn_scan_ipv6(
v6, timeout, None, None, None, None, None, 0, max_shards,
)?;
Ok(port_lines_to_alive_hosts(lines))
})
.await
.map_err(|e| std::io::Error::other(format!("{e}")))?
};
let (r4, r6) = tokio::join!(v4_fut, v6_fut);
let mut out = r4?;
out.extend(r6?);
Ok(out)
}
async fn ack_raw_discovery_collect(
hosts: &[IpAddr],
ports: &[u16],
skip: Option<&HashSet<IpAddr>>,
timeout: Duration,
max_shards: usize,
) -> std::io::Result<HashSet<IpAddr>> {
let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
for &h in hosts {
if skip.map(|s| s.contains(&h)).unwrap_or(false) {
continue;
}
for &p in ports {
match h {
IpAddr::V4(a) => v4.push((a, p)),
IpAddr::V6(a) => v6.push((a, p)),
}
}
}
let v4_fut = async {
if v4.is_empty() {
return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
}
tokio::task::spawn_blocking(move || {
let lines = crate::syn::parallel_ack_ping_scan_ipv4(
v4, timeout, None, None, None, None, None, 0, max_shards,
)?;
Ok(port_lines_to_alive_hosts(lines))
})
.await
.map_err(|e| std::io::Error::other(format!("{e}")))?
};
let v6_fut = async {
if v6.is_empty() {
return Ok::<HashSet<IpAddr>, std::io::Error>(HashSet::new());
}
tokio::task::spawn_blocking(move || {
let lines = crate::syn::parallel_ack_ping_scan_ipv6(
v6, timeout, None, None, None, None, None, 0, max_shards,
)?;
Ok(port_lines_to_alive_hosts(lines))
})
.await
.map_err(|e| std::io::Error::other(format!("{e}")))?
};
let (r4, r6) = tokio::join!(v4_fut, v6_fut);
let mut out = r4?;
out.extend(r6?);
Ok(out)
}
async fn sctp_raw_discovery_collect(
hosts: &[IpAddr],
ports: &[u16],
skip: Option<&HashSet<IpAddr>>,
timeout: Duration,
max_shards: usize,
) -> HashSet<IpAddr> {
let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
for &h in hosts {
if skip.map(|s| s.contains(&h)).unwrap_or(false) {
continue;
}
for &p in ports {
match h {
IpAddr::V4(a) => v4.push((a, p)),
IpAddr::V6(a) => v6.push((a, p)),
}
}
}
let v4_fut = async {
if v4.is_empty() {
return Ok(HashSet::new());
}
let r = tokio::task::spawn_blocking(move || {
crate::sctp::parallel_sctp_scan_ipv4(
v4,
crate::sctp::SctpProbeKind::Init,
timeout,
None,
None,
None,
None,
None,
0,
max_shards,
)
.map(port_lines_to_alive_hosts)
})
.await;
match r {
Ok(Ok(s)) => Ok(s),
Ok(Err(e)) => Err(e),
Err(e) => Err(std::io::Error::other(format!("{e}"))),
}
};
let v6_fut = async {
if v6.is_empty() {
return Ok(HashSet::new());
}
let r = tokio::task::spawn_blocking(move || {
crate::sctp::parallel_sctp_scan_ipv6(
v6,
crate::sctp::SctpProbeKind::Init,
timeout,
None,
None,
None,
None,
None,
0,
max_shards,
)
.map(port_lines_to_alive_hosts)
})
.await;
match r {
Ok(Ok(s)) => Ok(s),
Ok(Err(e)) => Err(e),
Err(e) => Err(std::io::Error::other(format!("{e}"))),
}
};
let (r4, r6) = tokio::join!(v4_fut, v6_fut);
let mut out = HashSet::new();
match r4 {
Ok(s) => out.extend(s),
Err(e) => tracing::warn!(error = %e, "SCTP discovery IPv4 (-PY) failed"),
}
match r6 {
Ok(s) => out.extend(s),
Err(e) => tracing::warn!(error = %e, "SCTP discovery IPv6 (-PY) failed"),
}
out
}
async fn ip_proto_ping_discovery_collect(
hosts: &[IpAddr],
protos: &[u16],
skip: Option<&HashSet<IpAddr>>,
timeout: Duration,
max_shards: usize,
) -> HashSet<IpAddr> {
let mut v4: Vec<(Ipv4Addr, u16)> = Vec::new();
let mut v6: Vec<(Ipv6Addr, u16)> = Vec::new();
for &h in hosts {
if skip.map(|s| s.contains(&h)).unwrap_or(false) {
continue;
}
for &p in protos {
match h {
IpAddr::V4(a) => v4.push((a, p)),
IpAddr::V6(a) => v6.push((a, p)),
}
}
}
let v4_fut = async {
if v4.is_empty() {
return Ok(HashSet::new());
}
let r = tokio::task::spawn_blocking(move || {
crate::ip_proto::parallel_ip_proto_scan_ipv4(
v4, timeout, None, None, None, None, None, 0, max_shards,
)
.map(port_lines_to_alive_hosts)
})
.await;
match r {
Ok(Ok(s)) => Ok(s),
Ok(Err(e)) => Err(e),
Err(e) => Err(std::io::Error::other(format!("{e}"))),
}
};
let v6_fut = async {
if v6.is_empty() {
return Ok(HashSet::new());
}
let r = tokio::task::spawn_blocking(move || {
crate::ip_proto::parallel_ip_proto_scan_ipv6(
v6, timeout, None, None, None, None, None, 0, max_shards,
)
.map(port_lines_to_alive_hosts)
})
.await;
match r {
Ok(Ok(s)) => Ok(s),
Ok(Err(e)) => Err(e),
Err(e) => Err(std::io::Error::other(format!("{e}"))),
}
};
let (r4, r6) = tokio::join!(v4_fut, v6_fut);
let mut out = HashSet::new();
match r4 {
Ok(s) => out.extend(s),
Err(e) => tracing::warn!(error = %e, "IP protocol discovery IPv4 (-PO) failed"),
}
match r6 {
Ok(s) => out.extend(s),
Err(e) => tracing::warn!(error = %e, "IP protocol discovery IPv6 (-PO) failed"),
}
out
}
async fn ip_proto_ping_discovery_merge(
hosts: &[IpAddr],
protos: &[u16],
alive: &mut HashSet<IpAddr>,
connect_timeout: Duration,
max_shards: usize,
) {
let s =
ip_proto_ping_discovery_collect(hosts, protos, Some(&*alive), connect_timeout, max_shards)
.await;
alive.extend(s);
}
async fn sctp_discovery_merge(
hosts: &[IpAddr],
ports: &[u16],
alive: &mut HashSet<IpAddr>,
connect_timeout: Duration,
max_shards: usize,
) {
let s =
sctp_raw_discovery_collect(hosts, ports, Some(&*alive), connect_timeout, max_shards).await;
alive.extend(s);
}
async fn tcp_ps_discovery_or_connect(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
skip: Option<&HashSet<IpAddr>>,
connect_timeout: Duration,
max_shards: usize,
) -> HashSet<IpAddr> {
match syn_raw_discovery_collect(hosts, ports, skip, connect_timeout, max_shards).await {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "raw TCP SYN discovery failed; using TCP connect");
tcp_connect_discovery_collect(hosts, ports, concurrency, skip, connect_timeout).await
}
}
}
async fn tcp_ps_discovery_merge(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
alive: &mut HashSet<IpAddr>,
connect_timeout: Duration,
max_shards: usize,
) {
let new_up = tcp_ps_discovery_or_connect(
hosts,
ports,
concurrency,
Some(&*alive),
connect_timeout,
max_shards,
)
.await;
alive.extend(new_up);
}
async fn tcp_pa_discovery_or_connect(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
skip: Option<&HashSet<IpAddr>>,
connect_timeout: Duration,
max_shards: usize,
) -> HashSet<IpAddr> {
match ack_raw_discovery_collect(hosts, ports, skip, connect_timeout, max_shards).await {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "raw TCP ACK discovery failed; using TCP connect");
tcp_connect_discovery_collect(hosts, ports, concurrency, skip, connect_timeout).await
}
}
}
async fn tcp_pa_discovery_merge(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
alive: &mut HashSet<IpAddr>,
connect_timeout: Duration,
max_shards: usize,
) {
let new_up = tcp_pa_discovery_or_connect(
hosts,
ports,
concurrency,
Some(&*alive),
connect_timeout,
max_shards,
)
.await;
alive.extend(new_up);
}
async fn tcp_probe(host: IpAddr, port: u16, timeout: Duration) -> bool {
let addr = SocketAddr::new(host, port);
match tokio::time::timeout(timeout, TcpStream::connect(addr)).await {
Ok(Ok(_)) => true,
Ok(Err(e)) => e.kind() == TokioErrorKind::ConnectionRefused,
Err(_) => false,
}
}
async fn tcp_connect_discovery_collect(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
skip: Option<&HashSet<IpAddr>>,
connect_timeout: Duration,
) -> HashSet<IpAddr> {
let pairs: Vec<(IpAddr, u16)> = hosts
.iter()
.copied()
.filter(|h| !skip.map(|s| s.contains(h)).unwrap_or(false))
.flat_map(|h| ports.iter().map(move |&p| (h, p)))
.collect();
let results: Vec<(IpAddr, bool)> = stream::iter(pairs)
.map(|(host, port)| {
let t = connect_timeout;
async move { (host, tcp_probe(host, port, t).await) }
})
.buffer_unordered(concurrency)
.collect()
.await;
results
.into_iter()
.filter(|(_, ok)| *ok)
.map(|(h, _)| h)
.collect()
}
async fn udp_ping_probe(host: IpAddr, port: u16, wait: Duration) -> bool {
let bind_addr: SocketAddr = match host {
IpAddr::V4(_) => "0.0.0.0:0".parse().unwrap(),
IpAddr::V6(_) => "[::]:0".parse().unwrap(),
};
let Ok(socket) = UdpSocket::bind(bind_addr).await else {
return false;
};
let dst = SocketAddr::new(host, port);
if socket.send_to(&[0u8], dst).await.is_err() {
return false;
}
let mut buf = [0u8; 2048];
match tokio::time::timeout(wait, socket.recv_from(&mut buf)).await {
Ok(Ok((n, _))) => n > 0,
Ok(Err(e)) => matches!(
e.kind(),
IoErrorKind::ConnectionRefused
| IoErrorKind::NetworkUnreachable
| IoErrorKind::HostUnreachable
),
Err(_) => false,
}
}
async fn udp_discovery_collect(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
skip: Option<&HashSet<IpAddr>>,
icmp_wait: Duration,
) -> HashSet<IpAddr> {
let pairs: Vec<(IpAddr, u16)> = hosts
.iter()
.copied()
.filter(|h| !skip.map(|s| s.contains(h)).unwrap_or(false))
.flat_map(|h| ports.iter().map(move |&p| (h, p)))
.collect();
let results: Vec<(IpAddr, bool)> = stream::iter(pairs)
.map(|(host, port)| {
let w = icmp_wait;
async move { (host, udp_ping_probe(host, port, w).await) }
})
.buffer_unordered(concurrency)
.collect()
.await;
results
.into_iter()
.filter(|(_, ok)| *ok)
.map(|(h, _)| h)
.collect()
}
async fn udp_discovery(
hosts: &[IpAddr],
ports: &[u16],
concurrency: usize,
alive: &mut HashSet<IpAddr>,
icmp_wait: Duration,
) {
let new_up = udp_discovery_collect(hosts, ports, concurrency, Some(&*alive), icmp_wait).await;
alive.extend(new_up);
}
#[cfg(unix)]
async fn icmp_timestamp_discovery_merge(
hosts: &[IpAddr],
alive: &mut HashSet<IpAddr>,
timeout: Duration,
concurrency: usize,
) {
let v4: Vec<Ipv4Addr> = hosts
.iter()
.copied()
.filter(|h| !alive.contains(h))
.filter_map(|h| match h {
IpAddr::V4(a) => Some(a),
IpAddr::V6(_) => None,
})
.collect();
if v4.is_empty() {
return;
}
let results: Vec<(IpAddr, bool)> = stream::iter(v4)
.map(|dst| async move {
let ok = tokio::task::spawn_blocking(move || {
crate::icmp_ping::icmp_timestamp_probe_v4(dst, timeout)
})
.await
.unwrap_or(false);
(IpAddr::V4(dst), ok)
})
.buffer_unordered(concurrency)
.collect()
.await;
for (ip, ok) in results {
if ok {
alive.insert(ip);
}
}
}
#[cfg(unix)]
async fn icmp_mask_discovery_merge(
hosts: &[IpAddr],
alive: &mut HashSet<IpAddr>,
timeout: Duration,
concurrency: usize,
) {
let v4: Vec<Ipv4Addr> = hosts
.iter()
.copied()
.filter(|h| !alive.contains(h))
.filter_map(|h| match h {
IpAddr::V4(a) => Some(a),
IpAddr::V6(_) => None,
})
.collect();
if v4.is_empty() {
return;
}
let results: Vec<(IpAddr, bool)> = stream::iter(v4)
.map(|dst| async move {
let ok = tokio::task::spawn_blocking(move || {
crate::icmp_ping::icmp_address_mask_probe_v4(dst, timeout)
})
.await
.unwrap_or(false);
(IpAddr::V4(dst), ok)
})
.buffer_unordered(concurrency)
.collect()
.await;
for (ip, ok) in results {
if ok {
alive.insert(ip);
}
}
}
fn local_subnet_v4_hosts(hosts: &[IpAddr]) -> Vec<Ipv4Addr> {
let ifaces = match if_addrs::get_if_addrs() {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let mut locals: Vec<(Ipv4Addr, u32)> = Vec::new();
for iface in &ifaces {
if let if_addrs::IfAddr::V4(ref v4) = iface.addr {
let ip = v4.ip;
let mask = v4.netmask;
let mask_u32 = u32::from(mask);
locals.push((ip, mask_u32));
}
}
let mut out = Vec::new();
for h in hosts {
if let IpAddr::V4(target) = h {
let t = u32::from(*target);
for &(local_ip, mask) in &locals {
let l = u32::from(local_ip);
if (t & mask) == (l & mask) {
out.push(*target);
break;
}
}
}
}
out
}
#[cfg(unix)]
fn arp_ping_hosts(
targets: &[Ipv4Addr],
timeout: Duration,
spoof_mac: Option<[u8; 6]>,
) -> HashSet<IpAddr> {
use pnet::datalink::{self, Channel};
use pnet::packet::arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket};
use pnet::packet::ethernet::{EtherTypes, MutableEthernetPacket};
use pnet::packet::Packet;
use pnet::util::MacAddr;
let mut alive = HashSet::new();
if targets.is_empty() {
return alive;
}
let interfaces = datalink::interfaces();
let iface = match interfaces
.iter()
.find(|i| i.is_up() && !i.is_loopback() && i.ips.iter().any(|ip| ip.is_ipv4()))
{
Some(i) => i.clone(),
None => return alive,
};
let src_mac = if let Some(mac) = spoof_mac {
MacAddr::new(mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
} else {
match iface.mac {
Some(m) => m,
None => return alive,
}
};
let src_ip = match iface.ips.iter().find_map(|ip| match ip.ip() {
IpAddr::V4(a) => Some(a),
_ => None,
}) {
Some(ip) => ip,
None => return alive,
};
let (mut tx, mut rx) = match datalink::channel(&iface, Default::default()) {
Ok(Channel::Ethernet(tx, rx)) => (tx, rx),
_ => return alive,
};
let target_set: HashSet<Ipv4Addr> = targets.iter().copied().collect();
for &target in targets {
let mut eth_buf = vec![0u8; 42]; if let Some(mut eth) = MutableEthernetPacket::new(&mut eth_buf) {
eth.set_destination(MacAddr::broadcast());
eth.set_source(src_mac);
eth.set_ethertype(EtherTypes::Arp);
let mut arp_buf = vec![0u8; 28];
if let Some(mut arp) = MutableArpPacket::new(&mut arp_buf) {
arp.set_hardware_type(ArpHardwareTypes::Ethernet);
arp.set_protocol_type(EtherTypes::Ipv4);
arp.set_hw_addr_len(6);
arp.set_proto_addr_len(4);
arp.set_operation(ArpOperations::Request);
arp.set_sender_hw_addr(src_mac);
arp.set_sender_proto_addr(src_ip);
arp.set_target_hw_addr(MacAddr::zero());
arp.set_target_proto_addr(target);
eth.set_payload(arp.packet());
}
let _ = tx.send_to(eth.packet(), None);
}
}
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
match rx.next() {
Ok(frame) => {
if let Some(eth) = pnet::packet::ethernet::EthernetPacket::new(frame) {
if eth.get_ethertype() == EtherTypes::Arp {
if let Some(arp) = ArpPacket::new(eth.payload()) {
if arp.get_operation() == ArpOperations::Reply {
let sender = arp.get_sender_proto_addr();
if target_set.contains(&sender) {
alive.insert(IpAddr::V4(sender));
if alive.len() == target_set.len() {
break;
}
}
}
}
}
}
}
Err(_) => break,
}
}
alive
}
#[cfg(not(unix))]
fn arp_ping_hosts(
_targets: &[Ipv4Addr],
_timeout: Duration,
_spoof_mac: Option<[u8; 6]>,
) -> HashSet<IpAddr> {
HashSet::new()
}
pub async fn hosts_after_discovery(
hosts: Vec<IpAddr>,
args: &Args,
concurrency: usize,
connect_timeout: Duration,
spoof_mac: Option<[u8; 6]>,
) -> Result<Vec<IpAddr>> {
if args.no_ping {
return Ok(hosts);
}
if hosts.is_empty() {
return Ok(hosts);
}
let c = concurrency.max(1);
let max_shards = c.clamp(1, MAX_SYN_PARALLEL_SHARDS);
let max_sctp_shards = c.clamp(1, MAX_SCTP_PARALLEL_SHARDS);
let max_ip_proto_shards = c.clamp(1, MAX_IP_PROTO_PARALLEL_SHARDS);
if (args.ping_timestamp || args.ping_mask) && hosts.iter().any(|h| matches!(h, IpAddr::V6(_))) {
let n = hosts.iter().filter(|h| matches!(h, IpAddr::V6(_))).count();
tracing::warn!(
count = n,
"ICMP timestamp (-PP) and netmask (-PM) are IPv4-only; skipping IPv6 targets"
);
}
if args.ping_timestamp && cfg!(not(unix)) {
tracing::warn!("-PP (ICMP timestamp) requires Unix raw ICMP; skipping");
}
if args.ping_mask && cfg!(not(unix)) {
tracing::warn!("-PM (ICMP netmask) requires Unix raw ICMP; skipping");
}
let explicit = has_explicit_discovery_flags(args);
let implemented = has_implemented_explicit_probes(args);
if explicit && !implemented {
tracing::warn!(
"only unimplemented -P* discovery probes were given; using default discovery (ICMP + TCP {})",
DEFAULT_TCP_DISCOVERY_PORTS
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(",")
);
}
let mut alive: HashSet<IpAddr> = HashSet::new();
if !args.disable_arp_ping {
let local_v4 = local_subnet_v4_hosts(&hosts);
if !local_v4.is_empty() {
let arp_alive = tokio::task::spawn_blocking({
let local_v4 = local_v4.clone();
let ct = connect_timeout;
move || arp_ping_hosts(&local_v4, ct.min(Duration::from_secs(2)), spoof_mac)
})
.await
.unwrap_or_default();
alive.extend(arp_alive);
}
}
let use_default = !(explicit && implemented);
if use_default {
let hosts_icmp = hosts.clone();
let hosts_tcp = hosts.clone();
let ct = connect_timeout;
let (icmp_alive, tcp_alive) = tokio::join!(
async move {
let mut s = HashSet::new();
for o in ping_hosts(&hosts_icmp, c).await {
if o.up {
s.insert(o.host);
}
}
s
},
async move {
tcp_ps_discovery_or_connect(
&hosts_tcp,
DEFAULT_TCP_DISCOVERY_PORTS,
c,
None,
ct,
max_shards,
)
.await
}
);
alive.extend(icmp_alive);
alive.extend(tcp_alive);
} else {
if args.ping_echo {
for o in ping_hosts(&hosts, c).await {
if o.up {
alive.insert(o.host);
}
}
}
if args.ping_syn.is_some() {
let ports = ports_from_ping_tcp(&args.ping_syn, TCP_SYN_DEFAULT)?;
tcp_ps_discovery_merge(&hosts, &ports, c, &mut alive, connect_timeout, max_shards)
.await;
}
if args.ping_ack.is_some() {
let ports = ports_from_ping_tcp(&args.ping_ack, TCP_ACK_DEFAULT)?;
tcp_pa_discovery_merge(&hosts, &ports, c, &mut alive, connect_timeout, max_shards)
.await;
}
if args.ping_udp.is_some() {
let ports = ports_from_ping_udp(&args.ping_udp)?;
udp_discovery(&hosts, &ports, c, &mut alive, connect_timeout).await;
}
if args.ping_sctp.is_some() {
let ports = ports_from_ping_sctp(&args.ping_sctp)?;
sctp_discovery_merge(&hosts, &ports, &mut alive, connect_timeout, max_sctp_shards)
.await;
}
if let Some(opt) = &args.ping_ip_proto {
let protos = parse_ping_ip_proto_list(opt.as_deref().unwrap_or(""))?;
ip_proto_ping_discovery_merge(
&hosts,
&protos,
&mut alive,
connect_timeout,
max_ip_proto_shards,
)
.await;
}
if args.ping_timestamp {
#[cfg(unix)]
icmp_timestamp_discovery_merge(&hosts, &mut alive, connect_timeout, c).await;
}
if args.ping_mask {
#[cfg(unix)]
icmp_mask_discovery_merge(&hosts, &mut alive, connect_timeout, c).await;
}
}
let out: Vec<IpAddr> = hosts.into_iter().filter(|h| alive.contains(h)).collect();
if out.is_empty() {
bail!("no hosts responded to discovery (try -Pn to skip host discovery, or adjust -P* probes)");
}
Ok(out)
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
#[test]
fn ports_from_ping_tcp_variants() {
assert_eq!(ports_from_ping_tcp(&Some(None), 80).unwrap(), vec![80]);
assert_eq!(
ports_from_ping_tcp(&Some(Some("443".into())), 80).unwrap(),
vec![443]
);
assert_eq!(
ports_from_ping_tcp(&Some(Some("80,443".into())), 80).unwrap(),
vec![80, 443]
);
assert!(ports_from_ping_tcp(&None, 80).unwrap().is_empty());
}
#[test]
fn ports_from_ping_udp_variants() {
assert_eq!(ports_from_ping_udp(&Some(None)).unwrap(), vec![40_125]);
assert_eq!(
ports_from_ping_udp(&Some(Some("53".into()))).unwrap(),
vec![53]
);
assert!(ports_from_ping_udp(&None).unwrap().is_empty());
}
#[test]
fn ports_from_ping_sctp_variants() {
assert_eq!(ports_from_ping_sctp(&Some(None)).unwrap(), vec![80]);
assert_eq!(
ports_from_ping_sctp(&Some(Some("38412".into()))).unwrap(),
vec![38412]
);
assert!(ports_from_ping_sctp(&None).unwrap().is_empty());
}
#[test]
fn parse_ping_ip_proto_list_variants() {
assert_eq!(parse_ping_ip_proto_list("").unwrap(), vec![1, 2, 4]);
assert_eq!(parse_ping_ip_proto_list(" ").unwrap(), vec![1, 2, 4]);
assert_eq!(parse_ping_ip_proto_list("6").unwrap(), vec![6]);
assert_eq!(parse_ping_ip_proto_list("17,6").unwrap(), vec![6, 17]);
assert!(parse_ping_ip_proto_list("256").is_err());
assert!(parse_ping_ip_proto_list(",,").is_err());
}
#[test]
fn port_lines_to_alive_filters_timeouts() {
let lines = vec![
PortLine::new(
IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1)),
80,
"tcp",
"closed",
crate::scan::PortReason::ConnRefused,
None,
),
PortLine::new(
IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 2)),
80,
"tcp",
"filtered",
crate::scan::PortReason::Timeout,
None,
),
];
let a = port_lines_to_alive_hosts(lines);
assert_eq!(a.len(), 1);
assert!(a.contains(&IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1))));
}
#[test]
fn has_explicit_discovery_flags_false_by_default() {
let args = crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "127.0.0.1"]).unwrap();
assert!(!has_explicit_discovery_flags(&args));
}
#[test]
fn has_explicit_discovery_flags_ping_echo() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-E", "127.0.0.1"])
.unwrap();
assert!(has_explicit_discovery_flags(&args));
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn has_explicit_discovery_flags_ping_syn_some() {
let args = crate::cli::Args::try_parse_from([
"nmaprs",
"-p",
"80",
"--ping-S",
"443",
"127.0.0.1",
])
.unwrap();
assert!(has_explicit_discovery_flags(&args));
}
#[test]
fn has_explicit_discovery_flags_ip_proto_without_timestamp_mask_gap() {
let args = crate::cli::Args::try_parse_from([
"nmaprs",
"-p",
"80",
"--ping-ip-proto",
"6",
"127.0.0.1",
])
.unwrap();
assert!(has_explicit_discovery_flags(&args));
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn parse_ping_ip_proto_list_sorts_and_dedupes() {
assert_eq!(
parse_ping_ip_proto_list("17,1,17,6").unwrap(),
vec![1, 6, 17]
);
}
#[test]
fn port_lines_to_alive_includes_open_and_closed() {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 5));
let open = PortLine::new(
ip,
443,
"tcp",
"open",
crate::scan::PortReason::SynAck,
None,
);
let closed = PortLine::new(
ip,
444,
"tcp",
"closed",
crate::scan::PortReason::ConnRefused,
None,
);
let filtered = PortLine::new(
ip,
445,
"tcp",
"filtered",
crate::scan::PortReason::Timeout,
None,
);
let a = port_lines_to_alive_hosts(vec![open, closed, filtered]);
assert_eq!(a.len(), 1);
assert!(a.contains(&ip));
}
#[test]
fn has_explicit_discovery_flags_false_for_no_ping_only() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "--no-ping", "-p", "80", "127.0.0.1"])
.unwrap();
assert!(!has_explicit_discovery_flags(&args));
}
#[test]
fn ports_from_ping_tcp_empty_some_none_uses_default_port() {
assert_eq!(ports_from_ping_tcp(&Some(None), 1234).unwrap(), vec![1234]);
}
#[test]
fn parse_ping_ip_proto_list_rejects_non_numeric() {
assert!(parse_ping_ip_proto_list("1,foo").is_err());
}
#[test]
fn port_lines_to_alive_empty_input() {
assert!(port_lines_to_alive_hosts(vec![]).is_empty());
}
#[test]
fn has_implemented_explicit_probes_false_when_only_no_ping() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "--no-ping", "-p", "80", "127.0.0.1"])
.unwrap();
assert!(!has_implemented_explicit_probes(&args));
}
#[test]
fn ports_from_ping_sctp_default_port_80() {
assert_eq!(ports_from_ping_sctp(&Some(None)).unwrap(), vec![80]);
}
#[test]
fn ports_from_ping_ack_uses_tcp_helper_with_default() {
use super::TCP_ACK_DEFAULT;
assert_eq!(
ports_from_ping_tcp(&Some(None), TCP_ACK_DEFAULT).unwrap(),
vec![TCP_ACK_DEFAULT]
);
}
#[test]
fn parse_ping_ip_proto_list_single_zero() {
assert_eq!(parse_ping_ip_proto_list("0").unwrap(), vec![0]);
}
#[test]
fn port_lines_to_alive_excludes_only_filtered() {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 8));
let filtered = PortLine::new(
ip,
1,
"tcp",
"filtered",
crate::scan::PortReason::Timeout,
None,
);
assert!(port_lines_to_alive_hosts(vec![filtered]).is_empty());
}
#[test]
fn has_implemented_explicit_probes_ping_timestamp() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-P", "127.0.0.1"])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn has_implemented_explicit_probes_ping_mask() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-M", "127.0.0.1"])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn parse_ping_ip_proto_list_dedupes() {
assert_eq!(parse_ping_ip_proto_list("1,1,2,2").unwrap(), vec![1, 2]);
}
#[test]
fn ports_from_ping_udp_default_port_40125() {
assert_eq!(ports_from_ping_udp(&Some(None)).unwrap(), vec![40_125]);
}
#[test]
fn port_lines_to_alive_dedupes_same_host() {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 9));
let a = PortLine::new(
ip,
80,
"tcp",
"closed",
crate::scan::PortReason::ConnRefused,
None,
);
let b = PortLine::new(
ip,
443,
"tcp",
"open",
crate::scan::PortReason::SynAck,
None,
);
let alive = port_lines_to_alive_hosts(vec![a, b]);
assert_eq!(alive.len(), 1);
assert!(alive.contains(&ip));
}
#[test]
fn has_explicit_discovery_flags_ping_syn_short_flag() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-S", "127.0.0.1"])
.unwrap();
assert!(has_explicit_discovery_flags(&args));
}
#[test]
fn has_explicit_discovery_flags_ping_ip_proto() {
let args = crate::cli::Args::try_parse_from([
"nmaprs",
"-p",
"80",
"--ping-ip-proto",
"1,6",
"127.0.0.1",
])
.unwrap();
assert!(has_explicit_discovery_flags(&args));
}
#[test]
fn parse_ping_ip_proto_list_whitespace_only_defaults() {
assert_eq!(parse_ping_ip_proto_list(" ").unwrap(), vec![1, 2, 4]);
}
#[test]
fn parse_ping_ip_proto_list_max_255_ok() {
assert_eq!(parse_ping_ip_proto_list("255").unwrap(), vec![255]);
}
#[test]
fn ports_from_ping_tcp_invalid_spec_errors() {
assert!(ports_from_ping_tcp(&Some(Some("abc".into())), 80).is_err());
}
#[test]
fn ports_from_ping_udp_invalid_spec_errors() {
assert!(ports_from_ping_udp(&Some(Some("99999".into()))).is_err());
}
#[test]
fn port_lines_to_alive_open_only_counts() {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 20));
let open = PortLine::new(ip, 80, "tcp", "open", crate::scan::PortReason::SynAck, None);
let alive = port_lines_to_alive_hosts(vec![open]);
assert_eq!(alive.len(), 1);
}
#[test]
fn has_implemented_explicit_probes_ping_echo() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-E", "127.0.0.1"])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn has_implemented_explicit_probes_ping_udp() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-U", "127.0.0.1"])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn parse_ping_ip_proto_list_sorts_unsorted_input() {
assert_eq!(parse_ping_ip_proto_list("17,6,1").unwrap(), vec![1, 6, 17]);
}
#[test]
fn parse_ping_ip_proto_list_256_errors() {
assert!(parse_ping_ip_proto_list("256").is_err());
}
#[test]
fn has_explicit_discovery_flags_no_ping_syn() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--no-ping", "127.0.0.1"])
.unwrap();
assert!(!has_explicit_discovery_flags(&args));
}
#[test]
fn port_lines_to_alive_closed_state_counts_as_up() {
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 31));
let closed = PortLine::new(
ip,
80,
"tcp",
"closed",
crate::scan::PortReason::ConnRefused,
None,
);
let alive = port_lines_to_alive_hosts(vec![closed]);
assert_eq!(alive.len(), 1);
assert!(alive.contains(&ip));
}
#[test]
fn has_implemented_explicit_probes_ping_ip_proto() {
let args = crate::cli::Args::try_parse_from([
"nmaprs",
"-p",
"80",
"--ping-ip-proto",
"1",
"127.0.0.1",
])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn has_implemented_explicit_probes_ping_ack() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-A", "127.0.0.1"])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn has_implemented_explicit_probes_ping_sctp() {
let args =
crate::cli::Args::try_parse_from(["nmaprs", "-p", "80", "--ping-Y", "127.0.0.1"])
.unwrap();
assert!(has_implemented_explicit_probes(&args));
}
#[test]
fn has_explicit_discovery_flags_ping_ack() {
let args = crate::cli::Args::try_parse_from([
"nmaprs",
"-p",
"80",
"--ping-A",
"443",
"127.0.0.1",
])
.unwrap();
assert!(has_explicit_discovery_flags(&args));
}
#[test]
fn ports_from_ping_tcp_range_spec() {
assert_eq!(
ports_from_ping_tcp(&Some(Some("22-24".into())), 80).unwrap(),
vec![22, 23, 24]
);
}
#[test]
fn ports_from_ping_sctp_multi_port() {
assert_eq!(
ports_from_ping_sctp(&Some(Some("80,443".into()))).unwrap(),
vec![80, 443]
);
}
#[test]
fn parse_ping_ip_proto_list_leading_comma_skips_empty_token() {
assert_eq!(parse_ping_ip_proto_list(",1").unwrap(), vec![1]);
}
#[test]
fn port_lines_to_alive_ipv6_host() {
let ip: IpAddr = "2001:db8::1".parse().unwrap();
let open = PortLine::new(
ip,
443,
"tcp",
"open",
crate::scan::PortReason::SynAck,
None,
);
let alive = port_lines_to_alive_hosts(vec![open]);
assert!(alive.contains(&ip));
}
#[test]
fn local_subnet_v4_includes_loopback_when_on_subnet() {
let hosts = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
let local = local_subnet_v4_hosts(&hosts);
assert!(local.contains(&Ipv4Addr::LOCALHOST) || local.is_empty());
}
}