Skip to content

Commit fd2a124

Browse files
committed
Added more integration tests
1 parent f98b51e commit fd2a124

10 files changed

Lines changed: 511 additions & 151 deletions

File tree

tests/src/discovery/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2026 OverTheFlow and Contributors
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4+
// If a copy of the MPL was not distributed with this file, You can obtain one at
5+
// https://mozilla.org/MPL/2.0/.
6+
7+
//! High-level host and service discovery tests.
8+
//!
9+
//! These tests verify Zond's ability to identify live hosts across various
10+
//! network environments using both unprivileged (TCP sweeps) and
11+
//! privileged (ARP/ICMP) techniques.
12+
13+
pub mod unprivileged;
14+
pub mod privileged;

tests/src/discovery/privileged.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) 2026 OverTheFlow and Contributors
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4+
// If a copy of the MPL was not distributed with this file, You can obtain one at
5+
// https://mozilla.org/MPL/2.0/.
6+
7+
use std::net::{IpAddr, Ipv4Addr};
8+
use zond_common::config::ZondConfig;
9+
use zond_common::models::ip::set::IpSet;
10+
use zond_core::scanner;
11+
12+
#[cfg(target_os = "linux")]
13+
use crate::utils::NetnsContext;
14+
15+
#[tokio::test]
16+
#[cfg(target_os = "linux")]
17+
async fn test_privileged_discovery_netns() {
18+
let _ctx: NetnsContext = match NetnsContext::new("test1") {
19+
Some(c) => c,
20+
None => {
21+
eprintln!("Skipping netns test: Requires root privileges or 'ip' command.");
22+
return;
23+
}
24+
};
25+
26+
let target_ip: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 200, 0, 2));
27+
28+
let config: ZondConfig = ZondConfig {
29+
no_banner: true,
30+
no_dns: true,
31+
redact: false,
32+
quiet: 0,
33+
disable_input: true,
34+
};
35+
36+
let mut collection = IpSet::new();
37+
collection.insert(target_ip);
38+
39+
let result = scanner::discover(collection, &config).await;
40+
41+
match result {
42+
Ok(hosts) => {
43+
assert!(!hosts.is_empty(), "Should find the target in the namespace");
44+
let host = hosts
45+
.iter()
46+
.find(|h| h.primary_ip == target_ip)
47+
.expect("Target IP not found in results");
48+
49+
assert!(
50+
host.mac.is_some(),
51+
"Should resolve MAC address for local neighbor"
52+
);
53+
println!("Found host: {:?} with MAC {:?}", host.primary_ip, host.mac);
54+
}
55+
Err(e) => panic!("Discovery failed: {}", e),
56+
}
57+
}
58+
59+
#[tokio::test]
60+
#[cfg(target_os = "linux")]
61+
async fn test_privileged_discovery_hostname_resolution() {
62+
let ctx: NetnsContext = match NetnsContext::new("res-test") {
63+
Some(c) => c,
64+
None => return,
65+
};
66+
67+
// Set a custom hostname inside the namespace
68+
// Note: This requires the machine running the test to support 'ip netns exec'
69+
let _ = crate::utils::netns::run_ns_cmd(&ctx.ns_name, "hostname", &["zond-target-host"]);
70+
71+
// Start a listener on 443 inside the namespace so 'discover' finds it
72+
// We'll use a sidecar thread running 'nc' since it's easier than async-pipe logic for a quick mock.
73+
let ns_name = ctx.ns_name.clone();
74+
std::thread::spawn(move || {
75+
let _ = crate::utils::netns::run_ns_cmd(&ns_name, "nc", &["-l", "-p", "443", "-e", "echo hello"]);
76+
});
77+
78+
let target_ip: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 200, 0, 2));
79+
let config: ZondConfig = ZondConfig {
80+
no_banner: true,
81+
no_dns: false, // Enable DNS
82+
redact: false,
83+
quiet: 0,
84+
disable_input: true,
85+
};
86+
87+
let mut collection = IpSet::new();
88+
collection.insert(target_ip);
89+
90+
let result = scanner::discover(collection, &config).await;
91+
assert!(result.is_ok());
92+
let hosts = result.unwrap();
93+
94+
if !hosts.is_empty() {
95+
let host = &hosts[0];
96+
println!("Resolved host: {:?} - Hostname: {:?}", host.primary_ip, host.hostname);
97+
// On many systems, the namespace hostname won't resolve unless /etc/hosts is updated or a DNS server is present.
98+
// For now, we verify that the scan COMPLETE safely with resolution enabled.
99+
}
100+
}
101+
102+
#[tokio::test]
103+
#[cfg(target_os = "linux")]
104+
async fn test_privileged_discovery_stress_multi_alias() {
105+
let ctx: NetnsContext = match NetnsContext::new("stress-test") {
106+
Some(c) => c,
107+
None => return,
108+
};
109+
110+
// Add 20 alias IPs to the namespace interface (v-targ-stress-test)
111+
for i in 3..23 {
112+
let ip = format!("10.200.0.{}", i);
113+
let _ = crate::utils::netns::run_ns_cmd(&ctx.ns_name, "ip", &["addr", "add", &format!("{}/24", ip), "dev", "v-targ-stress-test"]);
114+
}
115+
116+
let config: ZondConfig = ZondConfig {
117+
no_banner: true,
118+
no_dns: true,
119+
redact: false,
120+
quiet: 0,
121+
disable_input: true,
122+
};
123+
124+
let mut collection = IpSet::new();
125+
collection.insert_range("10.200.0.1-10.200.0.30".parse().unwrap());
126+
127+
// Privileged scanner should use ARP to find all 21 active IPs (host + 20 aliases)
128+
let result = scanner::discover(collection, &config).await;
129+
assert!(result.is_ok());
130+
let hosts = result.unwrap();
131+
132+
// Should find at least 21 hosts (the target + aliases)
133+
// Note: 10.200.0.1 is the host side, so it might also be found depending on routing.
134+
assert!(hosts.len() >= 21, "Failed to find all aliased IPs in namespace. Found only: {}", hosts.len());
135+
}

tests/src/discovery/routed.rs

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
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-
#![cfg(test)]
87
use std::net::{IpAddr, Ipv4Addr};
98
use std::sync::atomic::Ordering;
109
use std::time::Duration;
@@ -13,9 +12,6 @@ use zond_common::models::host::Host;
1312
use zond_common::models::ip::{range::Ipv4Range, set::IpSet};
1413
use zond_core::scanner::{self, STOP_SIGNAL};
1514

16-
#[cfg(target_os = "linux")]
17-
use crate::utils::NetnsContext;
18-
1915
#[tokio::test]
2016
async fn test_discovery_single_loopback() {
2117
let config: ZondConfig = ZondConfig {
@@ -107,71 +103,70 @@ async fn test_stop_signal_aborts() {
107103
}
108104

109105
#[tokio::test]
110-
#[cfg(target_os = "linux")]
111-
async fn test_privileged_discovery_netns() {
112-
let _ctx: NetnsContext = match NetnsContext::new("test1") {
113-
Some(c) => c,
114-
None => {
115-
eprintln!("Skipping netns test: Requires root privileges or 'ip' command.");
116-
return;
117-
}
106+
async fn test_discovery_empty_set() {
107+
let cfg: ZondConfig = ZondConfig {
108+
no_banner: true,
109+
no_dns: true,
110+
redact: false,
111+
quiet: 0,
112+
disable_input: true,
118113
};
119114

120-
let target_ip: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 200, 0, 2));
115+
let targets = IpSet::new();
116+
let result = scanner::discover(targets, &cfg).await;
121117

122-
let config: ZondConfig = ZondConfig {
118+
assert!(result.is_ok());
119+
let hosts = result.unwrap();
120+
assert!(
121+
hosts.is_empty(),
122+
"Scanning empty set should return no hosts"
123+
);
124+
}
125+
126+
#[tokio::test]
127+
async fn test_discovery_redundant_ranges() {
128+
let cfg: ZondConfig = ZondConfig {
123129
no_banner: true,
124130
no_dns: true,
125131
redact: false,
126132
quiet: 0,
127133
disable_input: true,
128134
};
129135

130-
let mut collection = IpSet::new();
131-
collection.insert(target_ip);
132-
133-
let result = scanner::discover(collection, &config).await;
134-
135-
match result {
136-
Ok(hosts) => {
137-
assert!(!hosts.is_empty(), "Should find the target in the namespace");
138-
let host = hosts
139-
.iter()
140-
.find(|h| h.primary_ip == target_ip)
141-
.expect("Target IP not found in results");
142-
143-
assert!(
144-
host.mac.is_some(),
145-
"Should resolve MAC address for local neighbor"
146-
);
147-
println!("Found host: {:?} with MAC {:?}", host.primary_ip, host.mac);
148-
}
149-
Err(e) => panic!("Discovery failed: {}", e),
150-
}
151-
}
136+
let mut targets = IpSet::new();
137+
// Overlapping ranges: 127.0.0.1/31 (1, 2) and 127.0.0.1-5 (1, 2, 3, 4, 5)
138+
targets.insert_range("127.0.0.1/31".parse().unwrap());
139+
targets.insert_range("127.0.0.1-127.0.0.5".parse().unwrap());
152140

153-
#[test]
154-
fn test_lan_network_resolution() {
155-
// Assert that the machine running the integration test has at least 1 viable interface
156-
// that resolves via our platform agnostics hooks (macOS networksetup, Linux sysfs, Windows GetIfTable2).
157-
let result = zond_common::net::interface::get_lan_network();
158-
assert!(
159-
result.is_ok(),
160-
"Expected no OS or Viability errors during interface parsing"
161-
);
141+
// IpSet should have merged these into a single range of 6 IPs (0-5)
142+
assert_eq!(targets.len(), 6);
162143

163-
// Virtualized headless CI runners might return None here since they use virtual bridges,
164-
// but the FFI/Syscalls must execute safely regardless!
165-
println!("Resolved LAN network: {:?}", result.unwrap());
144+
let result = scanner::discover(targets, &cfg).await;
145+
assert!(result.is_ok());
166146
}
167147

168-
#[test]
169-
fn test_prioritized_interfaces_resolution() {
170-
let interfaces_res = zond_common::net::interface::get_prioritized_interfaces(10);
171-
assert!(interfaces_res.is_ok());
172-
let interfaces = interfaces_res.unwrap();
148+
#[tokio::test]
149+
async fn test_discovery_loopback_stress() {
150+
let cfg: ZondConfig = ZondConfig {
151+
no_banner: true,
152+
no_dns: true,
153+
redact: false,
154+
quiet: 0,
155+
disable_input: true,
156+
};
157+
158+
let mut targets = IpSet::new();
159+
targets.insert_range("127.0.0.0/24".parse().unwrap());
160+
161+
let start = std::time::Instant::now();
162+
let result = scanner::discover(targets, &cfg).await;
163+
let elapsed = start.elapsed();
164+
165+
assert!(result.is_ok());
166+
println!("Sweep of 256 IPs took {}ms", elapsed.as_millis());
167+
173168
assert!(
174-
!interfaces.is_empty(),
175-
"Expected at least 1 UP, non-loopback network interface on the host natively"
169+
elapsed < Duration::from_secs(5),
170+
"Sweep took too long, concurrency might be broken"
176171
);
177172
}

0 commit comments

Comments
 (0)