Skip to content

Commit 6df9df0

Browse files
committed
added first basic port scanning for non-root
1 parent 9a667f2 commit 6df9df0

11 files changed

Lines changed: 375 additions & 93 deletions

File tree

cli/src/commands.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ impl From<&CommandLine> for ZondConfig {
113113
Self {
114114
no_banner: cmd.no_banner,
115115
no_dns: cmd.no_dns,
116-
ports: cmd.ports.clone(),
117116
redact: cmd.redact,
118117
quiet: cmd.quiet,
119118
disable_input: false,

cli/src/commands/scan.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,56 @@
44
// If a copy of the MPL was not distributed with this file, You can obtain one at
55
// https://mozilla.org/MPL/2.0/.
66

7-
use zond_common::{config::ZondConfig, parse};
7+
use std::time::Instant;
88

9+
use colored::*;
10+
use tracing::info_span;
11+
12+
use crate::terminal::colors;
913
use crate::terminal::print::Print;
14+
use crate::terminal::spinner::SpinnerGuard;
15+
16+
use zond_common::{config::ZondConfig, models::port::PortSet, parse};
1017

11-
pub async fn scan(targets: &[String], _cfg: &ZondConfig) -> anyhow::Result<()> {
12-
let _ips = parse::to_ipset(targets)?;
18+
pub async fn scan(
19+
targets: &[String],
20+
global_ports: PortSet,
21+
cfg: &ZondConfig,
22+
) -> anyhow::Result<()> {
1323
Print::header("starting scanner");
14-
anyhow::bail!("'scan' subcommand not implemented yet");
24+
25+
let _guard: SpinnerGuard = run_spinner();
26+
27+
let target_map = parse::to_target_map(targets, global_ports)?;
28+
let start_time = Instant::now();
29+
30+
let mut hosts = zond_core::scanner::scan(target_map, cfg).await?;
31+
32+
if hosts.is_empty() {
33+
Print::no_results();
34+
return Ok(());
35+
}
36+
37+
Print::header("Network Scanner");
38+
39+
hosts.sort_by_key(|host| *host.ips.iter().next().unwrap_or(&host.primary_ip));
40+
41+
Print::hosts(&hosts)?;
42+
Print::discovery_summary(hosts.len(), start_time.elapsed());
43+
44+
Ok(())
45+
}
46+
47+
fn run_spinner() -> SpinnerGuard {
48+
let span = info_span!("scan", indicatif.pb_show = true);
49+
let _enter = span.enter();
50+
51+
SpinnerGuard::with_status(span.clone(), || {
52+
let count = zond_core::scanner::get_host_count();
53+
let count_str = count.to_string().green().bold();
54+
let label = if count == 1 { "host" } else { "hosts" };
55+
format!("Scanned {} {} so far...", count_str, label)
56+
.color(colors::TEXT_DEFAULT)
57+
.italic()
58+
})
1559
}

cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async fn main() -> ExitCode {
5050
Commands::Info => info::info(&cfg),
5151
Commands::Listen => listen::listen(&cfg),
5252
Commands::Discover { targets } => discover::discover(targets, &cfg).await,
53-
Commands::Scan { targets } => scan::scan(targets, &cfg).await,
53+
Commands::Scan { targets } => scan::scan(targets, commands.ports.clone(), &cfg).await,
5454
};
5555

5656
let exit_code = match result {

cli/src/terminal/host.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{net::IpAddr, time::Duration};
99
use colored::*;
1010
use unicode_width::UnicodeWidthStr;
1111
use zond_common::models::host::Host;
12+
use zond_common::models::port::{Port, PortState, Protocol};
1213

1314
use crate::{
1415
terminal::{
@@ -54,6 +55,10 @@ impl PrintableHost for Host {
5455
}
5556

5657
print::as_tree(details);
58+
59+
if !self.ports().is_empty() {
60+
print_services(self.ports());
61+
}
5762
}
5863
}
5964

@@ -119,3 +124,75 @@ fn rtt_to_string(host: &Host) -> String {
119124

120125
format!("⌛ {}ms - {}ms", min_rtt.as_millis(), max_rtt.as_millis())
121126
}
127+
128+
fn print_services(ports: &[Port]) {
129+
let mut open_c = 0;
130+
let mut ghosted_c = 0;
131+
let mut blocked_c = 0;
132+
for p in ports {
133+
match p.state {
134+
PortState::Open => open_c += 1,
135+
PortState::Ghosted => ghosted_c += 1,
136+
PortState::Blocked => blocked_c += 1,
137+
PortState::Closed => (),
138+
_ => (),
139+
}
140+
}
141+
142+
let mut stats = Vec::new();
143+
if open_c > 0 {
144+
stats.push(format!("{} OPEN", open_c).green().bold().to_string());
145+
}
146+
if ghosted_c > 0 {
147+
stats.push(format!("{} GHOSTED", ghosted_c).cyan().bold().to_string());
148+
}
149+
if blocked_c > 0 {
150+
stats.push(format!("{} BLOCKED", blocked_c).yellow().bold().to_string());
151+
}
152+
153+
let stats_str = if stats.is_empty() {
154+
"ALL CHECKS CLOSED".dimmed().to_string()
155+
} else {
156+
stats.join(&format!("{}", " / ".bright_black().bold()))
157+
};
158+
159+
zprint!(
160+
" {} {}{}{} {}",
161+
"└─".bright_black(),
162+
"SERVICES".color(colors::TEXT_DEFAULT),
163+
".".repeat(2).color(colors::SEPARATOR),
164+
":".color(colors::SEPARATOR),
165+
stats_str
166+
);
167+
168+
for (i, p) in ports.iter().enumerate() {
169+
let last = i + 1 == ports.len();
170+
let branch = if !last { "├─" } else { "└─" }.bright_black();
171+
172+
let proto_str = match p.protocol {
173+
Protocol::Tcp => "tcp",
174+
Protocol::Udp => "udp",
175+
};
176+
let port_spec = format!("{}/{}", p.number, proto_str);
177+
let port_spec_padded = format!("{:width$}", port_spec, width = 9);
178+
179+
let (state_str, state_color) = match p.state {
180+
PortState::Open => ("OPEN ", colored::Color::Green),
181+
PortState::Ghosted => ("GHOSTED", colored::Color::Cyan),
182+
PortState::Blocked => ("BLOCKED", colored::Color::Yellow),
183+
PortState::Closed => ("CLOSED ", colored::Color::Red),
184+
_ => ("UNKNOWN", colored::Color::White),
185+
};
186+
187+
let state_fmt = format!("[ {} ]", state_str.color(state_color));
188+
let svc_name = p.service_info.as_deref().unwrap_or("???");
189+
190+
zprint!(
191+
" {} {} {} {}",
192+
branch,
193+
port_spec_padded.color(colors::PRIMARY),
194+
state_fmt,
195+
svc_name.color(colors::TEXT_DEFAULT)
196+
);
197+
}
198+
}

common/src/config.rs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// If a copy of the MPL was not distributed with this file, You can obtain one at
55
// https://mozilla.org/MPL/2.0/.
66

7-
use crate::models::port::PortSet;
7+
88

99
/// Global configuration options for the scanner execution.
1010
///
@@ -30,24 +30,7 @@ pub struct ZondConfig {
3030
/// processing incoming DNS packets if they were initiated elsewhere.
3131
pub no_dns: bool,
3232

33-
/// The collection of network ports targeted during discovery and scanning.
34-
///
35-
/// This field defines the breadth of the reconnaissance. It supports complex
36-
/// specifications including individual ports, inclusive ranges, and
37-
/// transport-layer protocol overrides.
38-
///
39-
/// # Protocol Handling
40-
/// * **TCP** (Default): Ports are assigned to TCP unless prefixed.
41-
/// * **UDP**: Ports prefixed with `u:` (e.g., `u:53`) are assigned to UDP.
42-
///
43-
/// # Default Behavior
44-
/// If not explicitly provided via the CLI, this defaults to a "Greatest Hits"
45-
/// list of common TCP services (**22, 80, 443, 445, 3389**). This ensures
46-
/// high-probability discovery with minimal network noise.
47-
///
48-
/// # Example Syntax
49-
/// Input string: `"22, 80-443, u:53, u:67-68"`
50-
pub ports: PortSet,
33+
5134

5235
/// Enables privacy mode for sensitive data in the output.
5336
///

common/src/models/target.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::net::IpAddr;
1717
/// Represents a single, atomic connection attempt.
1818
///
1919
/// This is the pixel of the scan. Scanners (TCP, SYN, UDP) ingest these
20-
/// objects to perform individual probes.
20+
/// objects to perform individual targets.
2121
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2222
pub struct Target {
2323
pub ip: IpAddr,
@@ -27,7 +27,7 @@ pub struct Target {
2727

2828
/// A blueprint pairing a set of IP addresses with a set of ports.
2929
///
30-
/// `TargetSet` does not store individual probes in memory; instead, it
30+
/// `TargetSet` does not store individual targets in memory; instead, it
3131
/// defines the boundaries of a Scan Area. This allows Zond to handle
3232
/// massive ranges without significant RAM overhead.
3333
#[derive(Debug, Clone, Default)]
@@ -42,10 +42,10 @@ impl TargetSet {
4242
Self { ips, ports }
4343
}
4444

45-
/// Returns the total number of probes defined by this set.
45+
/// Returns the total number of targets defined by this set.
4646
///
4747
/// Calculated as (Number of IPs) × (Number of Ports).
48-
pub fn total_probes(&self) -> u128 {
48+
pub fn total_targets(&self) -> u128 {
4949
(self.ips.len() * (self.ports.len()) as u64) as u128
5050
}
5151

@@ -83,7 +83,7 @@ impl TargetMap {
8383

8484
/// Returns the total targets across all defined units.
8585
pub fn total_targets(&self) -> u128 {
86-
self.units.iter().map(|u| u.total_probes()).sum()
86+
self.units.iter().map(|u| u.total_targets()).sum()
8787
}
8888

8989
/// Returns the total number of unique IP addresses targeted.
@@ -111,12 +111,10 @@ mod tests {
111111
use super::*;
112112
use crate::models::port::Protocol;
113113

114-
// Helper to build a simple IpSet from a string
115114
fn mock_ip_set(input: &str) -> IpSet {
116115
IpSet::try_from(input).expect("Valid IP input for tests")
117116
}
118117

119-
// Helper to build a simple PortSet from a string
120118
fn mock_port_set(input: &str) -> PortSet {
121119
PortSet::try_from(input).expect("Valid Port input for tests")
122120
}
@@ -127,7 +125,7 @@ mod tests {
127125
let ports = mock_port_set("80, 443, 1000-1007"); // 10 Ports
128126
let ts = TargetSet::new(ips, ports);
129127

130-
assert_eq!(ts.total_probes(), 2560);
128+
assert_eq!(ts.total_targets(), 2560);
131129
}
132130

133131
#[test]
@@ -158,13 +156,13 @@ mod tests {
158156
fn test_target_map_aggregation() {
159157
let mut map = TargetMap::new();
160158

161-
// Unit 1: 5 IPs, 2 Ports = 10 Probes
159+
// Unit 1: 5 IPs, 2 Ports = 10 targets
162160
map.add_unit(TargetSet::new(
163161
mock_ip_set("10.0.0.1-10.0.0.5"),
164162
mock_port_set("80, 443"),
165163
));
166164

167-
// Unit 2: 1 IP, 5 Ports = 5 Probes
165+
// Unit 2: 1 IP, 5 Ports = 5 targets
168166
map.add_unit(TargetSet::new(
169167
mock_ip_set("1.1.1.1"),
170168
mock_port_set("22, 80, 443, 8080, 8443"),
@@ -181,7 +179,7 @@ mod tests {
181179
assert!(map.is_empty());
182180

183181
let ts_empty = TargetSet::new(mock_ip_set(""), mock_port_set(""));
184-
assert_eq!(ts_empty.total_probes(), 0);
182+
assert_eq!(ts_empty.total_targets(), 0);
185183
}
186184

187185
#[test]

common/src/parse.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,35 @@ pub mod ip;
1717

1818
pub use ip::{IS_LAN_SCAN, IpParseError, to_set as to_ipset};
1919

20+
use crate::models::ip::set::IpSet;
21+
use crate::models::port::PortSet;
22+
use crate::models::target::{TargetMap, TargetSet};
23+
24+
/// Parses a list of target strings (e.g. `["1.1.1.1:80,443", "8.8.8.8"]`) into a `TargetMap`.
25+
/// Combines per-target specified ports, or falls back to `global_ports`.
26+
pub fn to_target_map(
27+
targets: &[String],
28+
global_ports: PortSet,
29+
) -> Result<TargetMap, anyhow::Error> {
30+
let mut map = TargetMap::new();
31+
32+
for target in targets {
33+
if let Some((ip_str, port_str)) = target.split_once(':') {
34+
let ip_set = IpSet::try_from(ip_str)
35+
.map_err(|e| anyhow::anyhow!("Invalid IP in '{}': {}", ip_str, e))?;
36+
let port_set = PortSet::try_from(port_str)
37+
.map_err(|e| anyhow::anyhow!("Invalid Port in '{}': {}", port_str, e))?;
38+
map.add_unit(TargetSet::new(ip_set, port_set));
39+
} else {
40+
let ip_set = IpSet::try_from(target.as_str())
41+
.map_err(|e| anyhow::anyhow!("Invalid IP '{}': {}", target, e))?;
42+
map.add_unit(TargetSet::new(ip_set, global_ports.clone()));
43+
}
44+
}
45+
46+
Ok(map)
47+
}
48+
2049
// ╔════════════════════════════════════════════╗
2150
// ║ ████████╗███████╗███████╗████████╗███████╗ ║
2251
// ║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝ ║

core/src/scanner.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ use std::time::Duration;
2121
use async_trait::async_trait;
2222
use is_root::is_root;
2323
use zond_common::config::ZondConfig;
24-
use zond_common::net::interface;
2524
use zond_common::models::host::Host;
2625
use zond_common::models::ip::set::IpSet;
26+
use zond_common::models::target::TargetMap;
27+
use zond_common::net::interface;
2728
use zond_common::utils::input::InputHandle;
2829
use zond_common::{error, info, success, warn};
2930

3031
mod connect;
32+
pub mod dispatcher;
3133
mod local;
3234
mod resolver;
3335
mod routed;
@@ -57,9 +59,18 @@ trait NetworkExplorer {
5759
async fn discover_hosts(&mut self) -> anyhow::Result<Vec<Host>>;
5860
}
5961

60-
pub async fn scan(cfg: &ZondConfig) {
62+
pub async fn scan(target_map: TargetMap, cfg: &ZondConfig) -> anyhow::Result<Vec<Host>> {
6163
let use_raw_sockets = preflight_check(cfg);
62-
if !use_raw_sockets {}
64+
65+
// Non-root TCP connect scan
66+
if !use_raw_sockets {
67+
let dispatcher = dispatcher::Dispatcher::new(target_map);
68+
let rx = dispatcher.run_shuffled();
69+
return connect::scan(rx, 50).await;
70+
}
71+
72+
// Root privileged scan logic not implemented yet
73+
Ok(Vec::new())
6374
}
6475

6576
/// The primary entry point for network discovery.
@@ -75,7 +86,7 @@ pub async fn scan(cfg: &ZondConfig) {
7586
pub async fn discover(targets: IpSet, cfg: &ZondConfig) -> anyhow::Result<Vec<Host>> {
7687
let use_raw_sockets = preflight_check(cfg);
7788
if !use_raw_sockets {
78-
return connect::range_discovery(targets, connect::prober).await;
89+
return connect::discover(targets).await;
7990
}
8091

8192
let (dns_tx, resolver_task) = if !cfg.no_dns {
@@ -149,10 +160,7 @@ async fn spawn_explorers(
149160
verbosity = 1,
150161
"Spawning FALLBACK scanner for unmapped targets"
151162
);
152-
let handle =
153-
tokio::spawn(
154-
async move { connect::range_discovery(unmapped_ips, connect::prober).await },
155-
);
163+
let handle = tokio::spawn(async move { connect::discover(unmapped_ips).await });
156164
handles.push(handle);
157165
}
158166

0 commit comments

Comments
 (0)