1use std::collections::{BTreeMap, BTreeSet};
8use std::net::{IpAddr, Ipv6Addr};
9use std::sync::Arc;
10
11use tokio::sync::Semaphore;
12use tokio::task::JoinSet;
13
14mod util;
15
16use util::{
17 DiscoveredAddress, InterfaceAddress, PingBackend, get_addresses, hostname_resolution_supported,
18 resolve_hostname, select_ping_backend, socket_ipv6_multicast_ping, socket_ping,
19 system_ipv6_multicast_ping, system_ping,
20};
21
22#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct ScanOptions {
25 pub interface: Option<String>,
27 pub resolve_hostnames: bool,
29 pub raw_socket: bool,
31 pub timeout: usize,
33 pub ipv4: bool,
35 pub ipv6: bool,
37}
38
39impl Default for ScanOptions {
40 fn default() -> Self {
41 Self {
42 interface: None,
43 resolve_hostnames: true,
44 raw_socket: false,
45 timeout: 1,
46 ipv4: true,
47 ipv6: true,
48 }
49 }
50}
51
52pub async fn scan(options: ScanOptions) -> Result<Vec<String>, Box<dyn std::error::Error>> {
57 let mut results = Vec::new();
58 scan_each(options, |result| results.push(result)).await?;
59 Ok(results)
60}
61
62pub async fn scan_each<F>(
67 options: ScanOptions,
68 mut on_result: F,
69) -> Result<(), Box<dyn std::error::Error>>
70where
71 F: FnMut(String),
72{
73 let resolve = options.resolve_hostnames && hostname_resolution_supported();
74 let system_ping_exists = util::command_exists("ping");
75
76 let ping_backend = select_ping_backend(options.raw_socket, system_ping_exists)?;
77 let addresses = get_addresses(options.interface);
78 let semaphore = Arc::new(Semaphore::new(150));
79
80 let mut tasks = JoinSet::new();
81 let mut ipv6_tasks = JoinSet::new();
82 let mut ipv6_interfaces = BTreeMap::new();
83 for address in addresses {
84 match address {
85 InterfaceAddress::V4(address) if options.ipv4 => {
86 run_ipv4_subnet(
87 &mut tasks,
88 address,
89 resolve,
90 ping_backend,
91 options.timeout,
92 semaphore.clone(),
93 );
94 }
95 InterfaceAddress::V4(_) => {}
96 InterfaceAddress::V6 {
97 ip,
98 interface,
99 index,
100 } if options.ipv6 => {
101 let source = ipv6_interfaces.entry((interface, index)).or_insert(ip);
102 if ipv6_source_preferred(*source, ip) {
103 *source = ip;
104 }
105 }
106 InterfaceAddress::V6 { .. } => {}
107 }
108 }
109
110 let ipv6_config = Ipv6ScanConfig {
111 resolve_hostnames: resolve,
112 ping_backend,
113 system_ping_exists,
114 timeout: options.timeout,
115 };
116
117 for ((interface, index), source) in ipv6_interfaces {
118 ipv6_tasks.spawn(collect_ipv6_interface(
119 interface,
120 index,
121 source,
122 ipv6_config,
123 ));
124 }
125
126 while let Some(result) = ipv6_tasks.join_next().await {
127 let Ok(addresses) = result else {
128 continue;
129 };
130
131 for address in addresses {
132 tasks.spawn(format_successful_address(
133 address,
134 ipv6_config.resolve_hostnames,
135 semaphore.clone(),
136 ));
137 }
138 }
139
140 let mut seen = BTreeSet::new();
141 while let Some(result) = tasks.join_next().await {
142 if let Ok(Some(result)) = result
143 && seen.insert(result.clone())
144 {
145 on_result(result);
146 }
147 }
148
149 Ok(())
150}
151
152fn run_ipv4_subnet(
154 tasks: &mut JoinSet<Option<String>>,
155 address: std::net::Ipv4Addr,
156 resolve_hostnames: bool,
157 ping_backend: PingBackend,
158 timeout: usize,
159 semaphore: Arc<Semaphore>,
160) {
161 let octets = address.octets();
162
163 for i in 1..255 {
164 let ip_addr = IpAddr::V4(std::net::Ipv4Addr::new(octets[0], octets[1], octets[2], i));
165 tasks.spawn(ping_address(
166 ip_addr,
167 Some(IpAddr::V4(address)),
168 resolve_hostnames,
169 ping_backend,
170 timeout,
171 semaphore.clone(),
172 ));
173 }
174}
175
176#[derive(Clone, Copy)]
177struct Ipv6ScanConfig {
178 resolve_hostnames: bool,
179 ping_backend: PingBackend,
180 system_ping_exists: bool,
181 timeout: usize,
182}
183
184async fn collect_ipv6_interface(
185 interface: String,
186 index: Option<u32>,
187 source: Ipv6Addr,
188 config: Ipv6ScanConfig,
189) -> Vec<DiscoveredAddress> {
190 match socket_ipv6_multicast_ping(
191 &interface,
192 index,
193 source,
194 config.timeout,
195 config.ping_backend,
196 )
197 .await
198 {
199 Ok(addresses) => addresses,
200 Err(()) if config.system_ping_exists => {
201 system_ipv6_multicast_ping(&interface, index, config.timeout).await
202 }
203 Err(()) => Vec::new(),
204 }
205}
206
207async fn ping_address(
208 ip_addr: IpAddr,
209 source: Option<IpAddr>,
210 resolve_hostnames: bool,
211 ping_backend: PingBackend,
212 timeout: usize,
213 semaphore: Arc<Semaphore>,
214) -> Option<String> {
215 let _permit = match semaphore.acquire().await {
216 Ok(permit) => permit,
217 Err(_) => return None,
218 };
219
220 let success = match ping_backend {
221 PingBackend::RawSocket => socket_ping(&ip_addr, source, timeout).await,
222 PingBackend::System => system_ping(&ip_addr, timeout).await,
223 };
224
225 match (success, resolve_hostnames) {
226 (true, true) => resolve_hostname(&ip_addr)
227 .await
228 .or_else(|| Some(ip_addr.to_string())),
229 (true, false) => Some(ip_addr.to_string()),
230 _ => None,
231 }
232}
233
234fn ipv6_source_preferred(current: Ipv6Addr, candidate: Ipv6Addr) -> bool {
235 !current.is_unicast_link_local() && candidate.is_unicast_link_local()
236}
237
238async fn format_successful_address(
239 address: DiscoveredAddress,
240 resolve_hostnames: bool,
241 semaphore: Arc<Semaphore>,
242) -> Option<String> {
243 let _permit = match semaphore.acquire().await {
244 Ok(permit) => permit,
245 Err(_) => return None,
246 };
247
248 if resolve_hostnames {
249 resolve_hostname(&address.ip_addr)
250 .await
251 .or(Some(address.display_addr))
252 } else {
253 Some(address.display_addr)
254 }
255}
256
257#[doc(hidden)]
258pub mod cli_support {
259 pub use super::util::{
260 PingBackend, can_open_raw_socket, command_exists, hostname_resolution_supported,
261 raw_socket_supported, select_ping_backend,
262 };
263}
264
265#[cfg(test)]
266mod tests {
267 use super::ipv6_source_preferred;
268
269 #[test]
270 fn ipv6_source_selection_prefers_link_local_for_multicast() {
271 assert!(ipv6_source_preferred(
272 "2001:db8::1".parse().unwrap(),
273 "fe80::1".parse().unwrap(),
274 ));
275 assert!(!ipv6_source_preferred(
276 "fe80::1".parse().unwrap(),
277 "2001:db8::1".parse().unwrap(),
278 ));
279 }
280}