From 6dc2ea7cef14277cb4ff5f5437c7a47e5a4c6870 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 2 Apr 2025 11:02:55 -0400 Subject: [PATCH 01/33] WIP --- crates/network-scanner/examples/ping_range.rs | 4 +- crates/network-scanner/src/ip_utils.rs | 348 ++++++++++-------- crates/network-scanner/src/mdns.rs | 4 +- crates/network-scanner/src/netbios.rs | 40 +- crates/network-scanner/src/ping.rs | 58 ++- crates/network-scanner/src/scanner.rs | 63 ++-- crates/network-scanner/src/task_utils.rs | 24 +- 7 files changed, 314 insertions(+), 227 deletions(-) diff --git a/crates/network-scanner/examples/ping_range.rs b/crates/network-scanner/examples/ping_range.rs index 262ac0821..83725286f 100644 --- a/crates/network-scanner/examples/ping_range.rs +++ b/crates/network-scanner/examples/ping_range.rs @@ -44,8 +44,8 @@ pub async fn main() -> anyhow::Result<()> { TaskManager::new(), )?; - while let Some(ip) = receiver.recv().await { - tracing::info!("{} is alive", ip); + while let Some(ping_event) = receiver.recv().await { + tracing::info!(?ping_event); } tracing::info!("Elapsed time: {:?}", now.elapsed()); diff --git a/crates/network-scanner/src/ip_utils.rs b/crates/network-scanner/src/ip_utils.rs index 1384e839a..1fca4a685 100644 --- a/crates/network-scanner/src/ip_utils.rs +++ b/crates/network-scanner/src/ip_utils.rs @@ -4,144 +4,197 @@ use anyhow::Context; use network_interface::{Addr, NetworkInterfaceConfig, V4IfAddr}; #[derive(Debug, Clone)] -pub struct IpAddrRange { - lower: IpAddr, - upper: IpAddr, +pub enum IpAddrRange { + V4(IpV4AddrRange), + V6(IpV6AddrRange), } -pub struct IpAddrRangeIter { - range: IpAddrRange, - current: Option, +#[derive(Debug, Clone)] +pub struct IpV4AddrRange { + lower: Ipv4Addr, + upper: Ipv4Addr, + current: Option, +} + +#[derive(Debug, Clone)] +pub struct IpV6AddrRange { + lower: Ipv6Addr, + upper: Ipv6Addr, + current: Option, } -impl IpAddrRangeIter { - pub fn new(range: IpAddrRange) -> Self { +impl IpV4AddrRange { + pub fn new(lower: Ipv4Addr, upper: Ipv4Addr) -> Self { + let (lower, upper) = if u32::from(lower) > u32::from(upper) { + (upper, lower) + } else { + (lower, upper) + }; Self { - current: Some(range.lower), - range, + lower, + upper, + current: Some(lower), } } } -impl Iterator for IpAddrRangeIter { - type Item = IpAddr; +impl IpV6AddrRange { + pub fn new(lower: Ipv6Addr, upper: Ipv6Addr) -> Self { + let (lower, upper) = if u128::from(lower) > u128::from(upper) { + (upper, lower) + } else { + (lower, upper) + }; + Self { + lower, + upper, + current: Some(lower), + } + } +} + +// Implement Iterator for IPv4 range +impl Iterator for IpV4AddrRange { + type Item = Ipv4Addr; fn next(&mut self) -> Option { - let current = self.current.take()?; - if current > self.range.upper { + let current = self.current?; + if u32::from(current) > u32::from(self.upper) { return None; } - self.current = Some(increment_ip(current)); + let next = increment_ipv4(current); + self.current = Some(next); Some(current) } } -fn increment_ip(ip: IpAddr) -> IpAddr { - match ip { - IpAddr::V4(ip) => { - let mut octets = ip.octets(); - for i in (0..4).rev() { - if octets[i] < 255 { - octets[i] += 1; - break; - } else { - octets[i] = 0; - } - } - IpAddr::V4(Ipv4Addr::from(octets)) - } - IpAddr::V6(ip) => { - let mut segments = ip.segments(); - for i in (0..8).rev() { - if segments[i] < 0xffff { - segments[i] += 1; - break; - } else { - segments[i] = 0; - } - } - IpAddr::V6(Ipv6Addr::from(segments)) +// Implement Iterator for IPv6 range +impl Iterator for IpV6AddrRange { + type Item = Ipv6Addr; + + fn next(&mut self) -> Option { + let current = self.current?; + if u128::from(current) > u128::from(self.upper) { + return None; } + let next = increment_ipv6(current); + self.current = Some(next); + Some(current) } } +// Helper to create the appropriate enum variant impl IpAddrRange { - pub fn is_ipv6(&self) -> bool { - self.lower.is_ipv6() && self.upper.is_ipv6() - } - - pub fn is_ipv4(&self) -> bool { - self.lower.is_ipv4() && self.upper.is_ipv4() - } - pub fn new(lower: IpAddr, upper: IpAddr) -> anyhow::Result { - if lower.is_ipv4() != upper.is_ipv4() { - anyhow::bail!("IP range needs to be the same type"); - } - - if lower > upper { - return Ok(Self { - lower: upper, - upper: lower, - }); + match (lower, upper) { + (IpAddr::V4(l), IpAddr::V4(u)) => Ok(IpAddrRange::V4(IpV4AddrRange::new(l, u))), + (IpAddr::V6(l), IpAddr::V6(u)) => Ok(IpAddrRange::V6(IpV6AddrRange::new(l, u))), + _ => anyhow::bail!("IP range needs to be the same type (both IPv4 or both IPv6)"), } - - Ok(Self { lower, upper }) - } - - fn into_iter_inner(self) -> IpAddrRangeIter { - IpAddrRangeIter::new(self) } pub fn has_overlap(&self, other: &Self) -> bool { - self.lower <= other.upper && self.upper >= other.lower + match (self, other) { + (IpAddrRange::V4(a), IpAddrRange::V4(b)) => { + u32::from(a.lower) <= u32::from(b.upper) && u32::from(a.upper) >= u32::from(b.lower) + } + (IpAddrRange::V6(a), IpAddrRange::V6(b)) => { + u128::from(a.lower) <= u128::from(b.upper) && u128::from(a.upper) >= u128::from(b.lower) + } + _ => false, + } } } +// Implement IntoIterator for the enum so that it returns an IpAddr impl IntoIterator for IpAddrRange { type Item = IpAddr; type IntoIter = IpAddrRangeIter; fn into_iter(self) -> Self::IntoIter { - self.into_iter_inner() + match self { + IpAddrRange::V4(range) => IpAddrRangeIter::V4(range), + IpAddrRange::V6(range) => IpAddrRangeIter::V6(range), + } } } -impl TryFrom for IpAddrRange { - type Error = anyhow::Error; +// Enum for the iterator +pub enum IpAddrRangeIter { + V4(IpV4AddrRange), + V6(IpV6AddrRange), +} - fn try_from(value: V4IfAddr) -> Result { - let V4IfAddr { - ip, - broadcast: _, - netmask, - } = value; - - let Some(netmask) = netmask else { - anyhow::bail!("Network interface does not have a netmask"); - }; +impl Iterator for IpAddrRangeIter { + type Item = IpAddr; - let (lower, upper) = calculate_subnet_bounds(ip, netmask); + fn next(&mut self) -> Option { + match self { + IpAddrRangeIter::V4(r) => r.next().map(IpAddr::V4), + IpAddrRangeIter::V6(r) => r.next().map(IpAddr::V6), + } + } +} - Self::new(lower.into(), upper.into()) +// Helper to increment an Ipv4Addr +fn increment_ipv4(ip: Ipv4Addr) -> Ipv4Addr { + let mut octets = ip.octets(); + for i in (0..4).rev() { + if octets[i] < 255 { + octets[i] += 1; + return Ipv4Addr::from(octets); + } + octets[i] = 0; } + Ipv4Addr::from(octets) +} + +// Helper to increment an Ipv6Addr +fn increment_ipv6(ip: Ipv6Addr) -> Ipv6Addr { + let mut segments = ip.segments(); + for i in (0..8).rev() { + if segments[i] < 0xffff { + segments[i] += 1; + return Ipv6Addr::from(segments); + } + segments[i] = 0; + } + Ipv6Addr::from(segments) +} + +// Subnet as before +#[derive(Debug, Clone)] +pub struct Subnet { + pub ip: Ipv4Addr, + pub netmask: Ipv4Addr, + pub broadcast: Ipv4Addr, } impl From for IpAddrRange { fn from(value: Subnet) -> Self { - let Subnet { ip, netmask, .. } = value; - - let (lower, upper) = calculate_subnet_bounds(ip, netmask); - Self::new(lower.into(), upper.into()).expect("calculate_subnet_bounds returns valid lower and upper values") + let (lower, upper) = calculate_subnet_bounds(value.ip, value.netmask); + IpAddrRange::new(lower.into(), upper.into()) + .expect("Subnet bounds must be valid IPv4 addresses") } } impl From<&Subnet> for IpAddrRange { fn from(value: &Subnet) -> Self { - let Subnet { ip, netmask, .. } = value; + let (lower, upper) = calculate_subnet_bounds(value.ip, value.netmask); + IpAddrRange::new(lower.into(), upper.into()) + .expect("Subnet bounds must be valid IPv4 addresses") + } +} + +// Converting from V4IfAddr to IPv4 range +impl TryFrom for IpAddrRange { + type Error = anyhow::Error; - let (lower, upper) = calculate_subnet_bounds(*ip, *netmask); - Self::new(lower.into(), upper.into()).unwrap() + fn try_from(value: V4IfAddr) -> Result { + let V4IfAddr { ip, netmask, .. } = value; + let netmask = netmask.ok_or_else(|| anyhow::anyhow!("No netmask found"))?; + let (lower, upper) = calculate_subnet_bounds(ip, netmask); + IpAddrRange::new(lower.into(), upper.into()) } } @@ -149,109 +202,80 @@ fn calculate_subnet_bounds(ip: Ipv4Addr, netmask: Ipv4Addr) -> (Ipv4Addr, Ipv4Ad let ip_u32 = u32::from(ip); let netmask_u32 = u32::from(netmask); - // Calculate Network Address (Lower IP) + // Network Address let network_address = Ipv4Addr::from(ip_u32 & netmask_u32); - // Calculate Broadcast Address (Upper IP) - let wildcard_mask = !netmask_u32; - let broadcast_address = Ipv4Addr::from(ip_u32 | wildcard_mask); + // Broadcast Address + let broadcast_address = Ipv4Addr::from(ip_u32 | !netmask_u32); (network_address, broadcast_address) } -#[derive(Debug, Clone)] -pub struct Subnet { - pub ip: Ipv4Addr, - pub netmask: Ipv4Addr, - pub broadcast: Ipv4Addr, -} - pub fn get_subnets() -> anyhow::Result> { - let interfaces = network_interface::NetworkInterface::show().context("failed to get network interfaces")?; + let interfaces = network_interface::NetworkInterface::show() + .context("failed to get network interfaces")?; - let subnet: Vec<_> = interfaces + let subnets: Vec<_> = interfaces .into_iter() - .map(|interface| { - interface - .addr - .into_iter() - .filter_map(|addr| { - let addr = match addr { - Addr::V4(v4) => { - if v4.ip.is_loopback() || v4.ip.is_link_local() { - return None; - } - v4 + .flat_map(|interface| { + interface.addr.into_iter().filter_map(|addr| { + match addr { + Addr::V4(v4) => { + // Skip loopback or link-local + if v4.ip.is_loopback() || v4.ip.is_link_local() { + return None; + } + // Only keep if broadcast is present + if v4.broadcast.is_some() { + let netmask = v4.netmask?; + let broadcast = v4.broadcast?; + Some(Subnet { + ip: v4.ip, + netmask, + broadcast, + }) + } else { + None } - Addr::V6(_) => return None, - }; - - if addr.broadcast.is_some() { - Some(addr) - } else { - None } - }) - .collect::>() - }) - .flat_map(|addrs| addrs.into_iter()) - .map(|addr| { - let ip = addr.ip; - let netmask = addr.netmask.unwrap(); - let broadcast = addr.broadcast.unwrap(); - Subnet { ip, netmask, broadcast } + Addr::V6(_) => None, + } + }) }) .collect(); - Ok(subnet) + Ok(subnets) } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_from_v4_if_addr() { - let ip = Ipv4Addr::new(192, 168, 1, 50); - let netmask = Ipv4Addr::new(255, 255, 255, 0); - let broadcast = Ipv4Addr::new(192, 168, 1, 255); - let v4_if_addr = V4IfAddr { - ip, - broadcast: Some(broadcast), - netmask: Some(netmask), - }; - - let range = IpAddrRange::try_from(v4_if_addr).unwrap(); - - assert_eq!(range.lower, "192.168.1.0".parse::().unwrap()); - assert_eq!(range.upper, broadcast); - } - - #[test] - fn test_from_bad_v4_if_addr() { - let ip = Ipv4Addr::new(192, 168, 1, 50); - let bad_v4_if_addr = V4IfAddr { - ip, - broadcast: None, - netmask: None, - }; - - let range = IpAddrRange::try_from(bad_v4_if_addr); - assert!(range.is_err()); - } - #[test] fn test_iter_ipv4() { let lower = "10.10.0.0".parse::().unwrap(); let upper = "10.10.0.2".parse::().unwrap(); - let range = IpAddrRange::new(lower.into(), upper.into()).unwrap(); let mut iter = range.into_iter(); - - assert_eq!(iter.next(), Some(IpAddr::V4(Ipv4Addr::new(10, 10, 0, 0)))); - assert_eq!(iter.next(), Some(IpAddr::V4(Ipv4Addr::new(10, 10, 0, 1)))); - assert_eq!(iter.next(), Some(IpAddr::V4(Ipv4Addr::new(10, 10, 0, 2)))); + assert_eq!(iter.next(), Some("10.10.0.0".parse::().unwrap())); + assert_eq!(iter.next(), Some("10.10.0.1".parse::().unwrap())); + assert_eq!(iter.next(), Some("10.10.0.2".parse::().unwrap())); assert_eq!(iter.next(), None); } + + #[test] + fn test_has_overlap() { + let r1 = IpAddrRange::new( + "192.168.1.0".parse().unwrap(), + "192.168.1.255".parse().unwrap(), + ) + .unwrap(); + let r2 = IpAddrRange::new( + "192.168.1.100".parse().unwrap(), + "192.168.2.10".parse().unwrap(), + ) + .unwrap(); + assert!(r1.has_overlap(&r2)); + } } diff --git a/crates/network-scanner/src/mdns.rs b/crates/network-scanner/src/mdns.rs index f05396058..5aba828df 100644 --- a/crates/network-scanner/src/mdns.rs +++ b/crates/network-scanner/src/mdns.rs @@ -86,7 +86,7 @@ pub fn mdns_query_scan( let receiver_clone = receiver.clone(); task_manager .with_timeout(query_duration) - .when_finish(move || { + .after_finish(move |_| { debug!(service_name = ?service_name_clone, "Stopping browse for service"); if let Err(e) = service_daemon_clone.stop_browse(service_name_clone.as_ref()) { warn!(error = %e, "Failed to stop browsing for service"); @@ -107,7 +107,7 @@ pub fn mdns_query_scan( let port = msg.get_port(); for ip in msg.get_addresses() { - let entry = ScanEntry { + let entry = ScanEntry::Regular { addr: *ip, hostname: Some(device_name.clone()), port, diff --git a/crates/network-scanner/src/netbios.rs b/crates/network-scanner/src/netbios.rs index 170704d25..2095cfbaa 100644 --- a/crates/network-scanner/src/netbios.rs +++ b/crates/network-scanner/src/netbios.rs @@ -1,13 +1,14 @@ use std::mem::MaybeUninit; -use std::net::{IpAddr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use network_scanner_net::runtime::Socket2Runtime; use network_scanner_net::socket::AsyncRawSocket; use network_scanner_proto::netbios::NetBiosPacket; use socket2::{Domain, SockAddr, Type}; +use tokio::sync::mpsc::{Receiver, Sender}; -use crate::ip_utils::IpAddrRange; +use crate::ip_utils::IpV4AddrRange; use crate::task_utils::IpReceiver; use crate::{assume_init, ScannerError}; @@ -17,18 +18,22 @@ const MESSAGE: [u8; 50] = [ 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, ]; +#[derive(Debug, Clone)] +pub struct NetBiosResult { + ip: Ipv4Addr, + name: String, + mac_address: String, + group: Option, +} + const NET_BIOS_PORT: u16 = 137; pub fn netbios_query_scan( runtime: Arc, - ip_range: IpAddrRange, + ip_range: IpV4AddrRange, single_query_duration: std::time::Duration, netbios_scan_interval: std::time::Duration, task_manager: crate::task_utils::TaskManager, -) -> Result { - if ip_range.is_ipv6() { - return Err(ScannerError::DoesNotSupportIpv6("netbios".to_owned())); - } - +) -> Result, ScannerError> { let (sender, receiver) = tokio::sync::mpsc::channel(255); task_manager.spawn(move |task_manager: crate::task_utils::TaskManager| async move { for ip in ip_range.into_iter() { @@ -44,9 +49,9 @@ pub fn netbios_query_scan( } pub(crate) fn netbios_query_one( - ip: IpAddr, + ip: Ipv4Addr, mut socket: AsyncRawSocket, - result_sender: crate::task_utils::IpSender, + result_sender: Sender, duration: std::time::Duration, task_manager: crate::task_utils::TaskManager, ) { @@ -58,16 +63,19 @@ pub(crate) fn netbios_query_one( let mut buf: [MaybeUninit; 1024] = [MaybeUninit::::uninit(); 1024]; socket.recv(&mut buf).await?; - let IpAddr::V4(ipv4) = ip else { - anyhow::bail!("unreachable"); - }; - // SAFETY: TODO: explain why it’s safe. let buf = unsafe { assume_init(&buf) }; - let packet = NetBiosPacket::from(ipv4, buf); + let packet = NetBiosPacket::from(ip, buf); - result_sender.send((ipv4.into(), Some(packet.name()))).await?; + result_sender + .send(NetBiosResult { + ip, + name: packet.name(), + mac_address: packet.mac_address(), + group: packet.group() + }) + .await?; anyhow::Result::<()>::Ok(()) }); diff --git a/crates/network-scanner/src/ping.rs b/crates/network-scanner/src/ping.rs index 6ae9f66f7..db9287999 100644 --- a/crates/network-scanner/src/ping.rs +++ b/crates/network-scanner/src/ping.rs @@ -12,6 +12,19 @@ use tokio::time::timeout; use crate::create_echo_request; use crate::ip_utils::IpAddrRange; +#[derive(Debug)] +pub enum FailedReason { + Rejected, + TimedOut, +} + +#[derive(Debug)] +pub enum PingEvent { + Start { ip_addr: IpAddr }, + Success { ip_addr: IpAddr, time: u128 }, + Failed { reason: FailedReason }, +} + pub fn ping_range( runtime: Arc, range: IpAddrRange, @@ -19,7 +32,7 @@ pub fn ping_range( ping_wait_time: Duration, should_ping: impl Fn(IpAddr) -> bool + Send + Sync + 'static + Clone, task_manager: crate::task_utils::TaskManager, -) -> anyhow::Result> { +) -> anyhow::Result> { let (sender, receiver) = tokio::sync::mpsc::channel(255); let mut futures = vec![]; @@ -30,17 +43,25 @@ pub fn ping_range( Some(socket2::Protocol::ICMPV4), )?; let addr = SocketAddr::new(ip, 0); - let sender = sender.clone(); let should_ping = should_ping.clone(); + if !should_ping(ip) { + continue; + } - let ping_future = async move { - if !should_ping(ip) { - return anyhow::Ok(()); - } - if try_ping(addr.into(), socket).await.is_ok() { - sender.send(ip).await?; + let sender_clone = sender.clone(); + + let ping_future = move || async move { + let _ = sender_clone.send(PingEvent::Start { ip_addr: addr.ip() }).await; + let start_time = std::time::Instant::now(); + match try_ping(addr.into(), socket).await { + Err(_) => PingEvent::Failed { + reason: FailedReason::Rejected, + }, + Ok(_) => PingEvent::Success { + ip_addr: ip, + time: start_time.elapsed().as_millis(), + }, } - anyhow::Ok(()) }; futures.push(ping_future); @@ -48,7 +69,24 @@ pub fn ping_range( task_manager.spawn(move |task_manager| async move { for future in futures { - task_manager.with_timeout(ping_wait_time).spawn(|_| future); + let sender = sender.clone(); + + task_manager + .with_timeout(ping_wait_time) + .after_finish(move |result| { + match result { + Ok(event) => { + let _ = sender.try_send(event); + } + Err(_) => { + let _ = sender.try_send(PingEvent::Failed { + reason: FailedReason::TimedOut, + }); + } + } + }) + .spawn(|_| future()); + tokio::time::sleep(ping_interval).await; } anyhow::Ok(()) diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 39bb16087..ce741bf4f 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -1,8 +1,8 @@ use crate::broadcast::asynchronous::broadcast; use crate::ip_utils::IpAddrRange; use crate::mdns::{self, MdnsDaemon}; -use crate::netbios::netbios_query_scan; -use crate::ping::ping_range; +use crate::netbios::{netbios_query_scan, NetBiosResult}; +use crate::ping::{ping_range, PingEvent}; use crate::port_discovery::{scan_ports, PortScanResult}; use crate::task_utils::{ScanEntryReceiver, TaskExecutionContext, TaskExecutionRunner, TaskManager}; use anyhow::Context; @@ -91,7 +91,7 @@ impl NetworkScanner { let dns = ip_cache.read().get(&ip).cloned().flatten(); port_sender - .send(ScanEntry { + .send(ScanEntry::Regular { addr: ip, hostname: dns, port: socket_addr.port(), @@ -120,9 +120,11 @@ impl NetworkScanner { for subnet in subnets { debug!(broadcasting_to_subnet = ?subnet); let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); + task_manager.spawn(move |task_manager: TaskManager| async move { let mut receiver = broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; + while let Some(ip) = receiver.recv().await { trace!(broadcast_sent_ip = ?ip); ip_sender.send((ip.into(), None)).await?; @@ -148,13 +150,17 @@ impl NetworkScanner { debug!(netbios_query_ip_ranges = ?ip_ranges); for ip_range in ip_ranges { + let IpAddrRange::V4(ip_range) = ip_range else { + continue; + }; let (runtime, ip_sender, task_manager) = (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); + let mut receiver = netbios_query_scan(runtime, ip_range, netbios_timeout, netbios_interval, task_manager)?; while let Some(res) = receiver.recv().await { - debug!(netbios_query_sent_ip = ?res.0); - ip_sender.send(res).await?; + todo!() + // ip_sender.send(res).await?; } } anyhow::Ok(()) @@ -180,6 +186,7 @@ impl NetworkScanner { for ip_range in ip_ranges { let (task_manager, runtime, ip_sender) = (task_manager.clone(), Arc::clone(&runtime), ip_sender.clone()); + let should_ping = should_ping.clone(); let mut receiver = ping_range( runtime, @@ -192,7 +199,8 @@ impl NetworkScanner { while let Some(ip) = receiver.recv().await { debug!(ping_sent_ip = ?ip); - ip_sender.send((ip, None)).await?; + todo!() + // ip_sender.send(ScanEntry::Ping(ip)).await?; } } anyhow::Ok(()) @@ -211,18 +219,19 @@ impl NetworkScanner { task_manager| async move { let mut receiver = mdns::mdns_query_scan(mdns_daemon, task_manager, mdns_query_timeout)?; - while let Some(mut entry) = receiver.recv().await { - if ip_cache.read().get(&entry.addr).is_none() { - ip_cache.write().insert(entry.addr, entry.hostname.clone()); - } + todo!(); + // while let Some(mut entry) = receiver.recv().await { + // if ip_cache.read().get(&entry.addr).is_none() { + // ip_cache.write().insert(entry.addr, entry.hostname.clone()); + // } - let dns_name = ip_cache.read().get(&entry.addr).cloned().flatten(); - entry.hostname = dns_name; + // let dns_name = ip_cache.read().get(&entry.addr).cloned().flatten(); + // entry.hostname = dns_name; - if ports.contains(&entry.port) || entry.service_type.is_some() { - port_sender.send(entry).await?; - } - } + // if ports.contains(&entry.port) || entry.service_type.is_some() { + // port_sender.send(entry).await?; + // } + // } anyhow::Ok(()) }, @@ -296,15 +305,19 @@ impl NetworkScanner { } #[derive(Debug)] -pub struct ScanEntry { - // IP address of the device - pub addr: IpAddr, - // Hostname of the device - pub hostname: Option, - // Port number - pub port: u16, - // The protocol / service type listening on the port - pub service_type: Option, +pub enum ScanEntry { + Ping(PingEvent), + Netbios(NetBiosResult), + Regular { + // IP address of the device + addr: IpAddr, + // Hostname of the device + hostname: Option, + // Port number + port: u16, + // The protocol / service type listening on the port + service_type: Option, + }, } pub struct NetworkScannerStream { diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 95f756179..56fe9c014 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -166,7 +166,7 @@ impl TaskManager { self.spawn(|_| task); } - pub(crate) fn with_timeout(&self, duration: Duration) -> TimeoutManager { + pub(crate) fn with_timeout(&self, duration: Duration) -> TimeoutManager { TimeoutManager { task_manager: self.clone(), duration, @@ -184,22 +184,26 @@ impl TaskManager { } } -pub(crate) struct TimeoutManager { +type TimeoutResult = Result; +pub(crate) struct TimeoutManager { task_manager: TaskManager, duration: Duration, - when_finish: Option>, + when_finish: Option) + Send + 'static>>, } -impl TimeoutManager { - pub(crate) fn when_finish(self, f: F) -> Self +impl TimeoutManager +where + R: Send + 'static, +{ + pub(crate) fn after_finish(self, f: F) -> Self where - F: FnOnce() + Send + 'static, + F: FnOnce(TimeoutResult) + Send + 'static, { let Self { task_manager, duration, .. } = self; - let when_finish = Some(Box::new(f) as Box); + let when_finish = Some(Box::new(f) as Box) + Send + 'static>); Self { task_manager, @@ -211,7 +215,7 @@ impl TimeoutManager { pub(crate) fn spawn(self, task: T) where T: FnOnce(TaskManager) -> F + Send + 'static, - F: Future> + Send + 'static, + F: Future + Send + 'static, { let Self { task_manager, @@ -221,9 +225,9 @@ impl TimeoutManager { task_manager.spawn(move |task_manager| async move { let future = task(task_manager); - let _ = tokio::time::timeout(duration, future).await; + let result = tokio::time::timeout(duration, future).await; if let Some(when_finish) = when_finish { - when_finish(); + when_finish(result); } anyhow::Ok(()) }); From 57ad2b39594186abf2b841abdcff000974ab7046 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 8 Apr 2025 21:31:54 -0400 Subject: [PATCH 02/33] WIP --- crates/network-scanner/examples/netbios.rs | 5 +- crates/network-scanner/src/mdns.rs | 4 +- crates/network-scanner/src/netbios.rs | 24 +----- crates/network-scanner/src/ping.rs | 63 ++++++++------- crates/network-scanner/src/scanner.rs | 90 ++++++++++++++-------- crates/network-scanner/src/task_utils.rs | 32 ++++---- 6 files changed, 117 insertions(+), 101 deletions(-) diff --git a/crates/network-scanner/examples/netbios.rs b/crates/network-scanner/examples/netbios.rs index 6d130be2b..dcbf6e441 100644 --- a/crates/network-scanner/examples/netbios.rs +++ b/crates/network-scanner/examples/netbios.rs @@ -16,8 +16,9 @@ pub async fn main() -> anyhow::Result<()> { let lower: Ipv4Addr = "10.10.0.0".parse()?; let upper: Ipv4Addr = "10.10.0.125".parse()?; - let ip_range = - network_scanner::ip_utils::IpAddrRange::new(std::net::IpAddr::V4(lower), std::net::IpAddr::V4(upper))?; + + let ip_range = network_scanner::ip_utils::IpV4AddrRange::new(lower, upper); + let single_query_duration = Duration::from_secs(1); let interval = Duration::from_millis(20); let mut receiver = network_scanner::netbios::netbios_query_scan( diff --git a/crates/network-scanner/src/mdns.rs b/crates/network-scanner/src/mdns.rs index 5aba828df..d3472835f 100644 --- a/crates/network-scanner/src/mdns.rs +++ b/crates/network-scanner/src/mdns.rs @@ -86,7 +86,7 @@ pub fn mdns_query_scan( let receiver_clone = receiver.clone(); task_manager .with_timeout(query_duration) - .after_finish(move |_| { + .when_finish(move || { debug!(service_name = ?service_name_clone, "Stopping browse for service"); if let Err(e) = service_daemon_clone.stop_browse(service_name_clone.as_ref()) { warn!(error = %e, "Failed to stop browsing for service"); @@ -107,7 +107,7 @@ pub fn mdns_query_scan( let port = msg.get_port(); for ip in msg.get_addresses() { - let entry = ScanEntry::Regular { + let entry = ScanEntry::Result { addr: *ip, hostname: Some(device_name.clone()), port, diff --git a/crates/network-scanner/src/netbios.rs b/crates/network-scanner/src/netbios.rs index 2095cfbaa..21815ef71 100644 --- a/crates/network-scanner/src/netbios.rs +++ b/crates/network-scanner/src/netbios.rs @@ -6,9 +6,8 @@ use network_scanner_net::runtime::Socket2Runtime; use network_scanner_net::socket::AsyncRawSocket; use network_scanner_proto::netbios::NetBiosPacket; use socket2::{Domain, SockAddr, Type}; -use tokio::sync::mpsc::{Receiver, Sender}; -use crate::ip_utils::IpV4AddrRange; +use crate::ip_utils::{IpAddrRange, IpV4AddrRange}; use crate::task_utils::IpReceiver; use crate::{assume_init, ScannerError}; @@ -18,14 +17,6 @@ const MESSAGE: [u8; 50] = [ 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, ]; -#[derive(Debug, Clone)] -pub struct NetBiosResult { - ip: Ipv4Addr, - name: String, - mac_address: String, - group: Option, -} - const NET_BIOS_PORT: u16 = 137; pub fn netbios_query_scan( runtime: Arc, @@ -33,7 +24,7 @@ pub fn netbios_query_scan( single_query_duration: std::time::Duration, netbios_scan_interval: std::time::Duration, task_manager: crate::task_utils::TaskManager, -) -> Result, ScannerError> { +) -> Result { let (sender, receiver) = tokio::sync::mpsc::channel(255); task_manager.spawn(move |task_manager: crate::task_utils::TaskManager| async move { for ip in ip_range.into_iter() { @@ -51,7 +42,7 @@ pub fn netbios_query_scan( pub(crate) fn netbios_query_one( ip: Ipv4Addr, mut socket: AsyncRawSocket, - result_sender: Sender, + result_sender: crate::task_utils::IpSender, duration: std::time::Duration, task_manager: crate::task_utils::TaskManager, ) { @@ -68,14 +59,7 @@ pub(crate) fn netbios_query_one( let packet = NetBiosPacket::from(ip, buf); - result_sender - .send(NetBiosResult { - ip, - name: packet.name(), - mac_address: packet.mac_address(), - group: packet.group() - }) - .await?; + result_sender.send((ip.into(), Some(packet.name()))).await?; anyhow::Result::<()>::Ok(()) }); diff --git a/crates/network-scanner/src/ping.rs b/crates/network-scanner/src/ping.rs index db9287999..e5cd6e760 100644 --- a/crates/network-scanner/src/ping.rs +++ b/crates/network-scanner/src/ping.rs @@ -13,7 +13,7 @@ use crate::create_echo_request; use crate::ip_utils::IpAddrRange; #[derive(Debug)] -pub enum FailedReason { +pub enum PingFailedReason { Rejected, TimedOut, } @@ -22,7 +22,7 @@ pub enum FailedReason { pub enum PingEvent { Start { ip_addr: IpAddr }, Success { ip_addr: IpAddr, time: u128 }, - Failed { reason: FailedReason }, + Failed { ip_addr: IpAddr, reason: PingFailedReason }, } pub fn ping_range( @@ -53,15 +53,37 @@ pub fn ping_range( let ping_future = move || async move { let _ = sender_clone.send(PingEvent::Start { ip_addr: addr.ip() }).await; let start_time = std::time::Instant::now(); - match try_ping(addr.into(), socket).await { - Err(_) => PingEvent::Failed { - reason: FailedReason::Rejected, - }, - Ok(_) => PingEvent::Success { - ip_addr: ip, - time: start_time.elapsed().as_millis(), - }, - } + let ping_future = try_ping(addr.into(), socket); + let ping_future = timeout(ping_wait_time, ping_future); + match ping_future.await { + Ok(Ok(_)) => { + let elapsed = start_time.elapsed().as_millis(); + let _ = sender_clone + .send(PingEvent::Success { + ip_addr: addr.ip(), + time: elapsed, + }) + .await; + } + Ok(Err(_)) => { + let _ = sender_clone + .send(PingEvent::Failed { + ip_addr: addr.ip(), + reason: PingFailedReason::Rejected, + }) + .await; + } + Err(_) => { + let _ = sender_clone + .send(PingEvent::Failed { + ip_addr: addr.ip(), + reason: PingFailedReason::TimedOut, + }) + .await; + } + }; + + anyhow::Ok(()) }; futures.push(ping_future); @@ -69,24 +91,7 @@ pub fn ping_range( task_manager.spawn(move |task_manager| async move { for future in futures { - let sender = sender.clone(); - - task_manager - .with_timeout(ping_wait_time) - .after_finish(move |result| { - match result { - Ok(event) => { - let _ = sender.try_send(event); - } - Err(_) => { - let _ = sender.try_send(PingEvent::Failed { - reason: FailedReason::TimedOut, - }); - } - } - }) - .spawn(|_| future()); - + task_manager.spawn_no_sub_task(future()); tokio::time::sleep(ping_interval).await; } anyhow::Ok(()) diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index ce741bf4f..4c8269e24 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -1,8 +1,8 @@ use crate::broadcast::asynchronous::broadcast; use crate::ip_utils::IpAddrRange; use crate::mdns::{self, MdnsDaemon}; -use crate::netbios::{netbios_query_scan, NetBiosResult}; -use crate::ping::{ping_range, PingEvent}; +use crate::netbios::netbios_query_scan; +use crate::ping::{ping_range, PingFailedReason}; use crate::port_discovery::{scan_ports, PortScanResult}; use crate::task_utils::{ScanEntryReceiver, TaskExecutionContext, TaskExecutionRunner, TaskManager}; use anyhow::Context; @@ -51,7 +51,7 @@ impl NetworkScanner { ports, runtime, port_scan_timeout, - port_sender, + result_sender: port_sender, .. }: TaskExecutionContext, task_manager| async move { @@ -91,7 +91,7 @@ impl NetworkScanner { let dns = ip_cache.read().get(&ip).cloned().flatten(); port_sender - .send(ScanEntry::Regular { + .send(ScanEntry::Result { addr: ip, hostname: dns, port: socket_addr.port(), @@ -120,11 +120,9 @@ impl NetworkScanner { for subnet in subnets { debug!(broadcasting_to_subnet = ?subnet); let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); - task_manager.spawn(move |task_manager: TaskManager| async move { let mut receiver = broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; - while let Some(ip) = receiver.recv().await { trace!(broadcast_sent_ip = ?ip); ip_sender.send((ip.into(), None)).await?; @@ -150,17 +148,19 @@ impl NetworkScanner { debug!(netbios_query_ip_ranges = ?ip_ranges); for ip_range in ip_ranges { + let (runtime, ip_sender, task_manager) = + (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); + let IpAddrRange::V4(ip_range) = ip_range else { continue; }; - let (runtime, ip_sender, task_manager) = - (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); let mut receiver = netbios_query_scan(runtime, ip_range, netbios_timeout, netbios_interval, task_manager)?; + while let Some(res) = receiver.recv().await { - todo!() - // ip_sender.send(res).await?; + debug!(netbios_query_sent_ip = ?res.0); + ip_sender.send(res).await?; } } anyhow::Ok(()) @@ -175,6 +175,7 @@ impl NetworkScanner { ip_sender, subnets, ip_cache, + result_sender, .. }: TaskExecutionContext, task_manager| async move { @@ -186,7 +187,6 @@ impl NetworkScanner { for ip_range in ip_ranges { let (task_manager, runtime, ip_sender) = (task_manager.clone(), Arc::clone(&runtime), ip_sender.clone()); - let should_ping = should_ping.clone(); let mut receiver = ping_range( runtime, @@ -199,8 +199,22 @@ impl NetworkScanner { while let Some(ip) = receiver.recv().await { debug!(ping_sent_ip = ?ip); - todo!() - // ip_sender.send(ScanEntry::Ping(ip)).await?; + match ip { + crate::ping::PingEvent::Success { ip_addr, time } => { + ip_sender.send((ip_addr, None)).await?; + debug!(ping_success_ip = ?ip_addr, time = ?time); + } + crate::ping::PingEvent::Start { ip_addr } => { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::PingStart { ip_addr })) + .await?; + } + crate::ping::PingEvent::Failed { ip_addr, reason } => { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::PingFailed { ip_addr, reason })) + .await?; + } + } } } anyhow::Ok(()) @@ -210,7 +224,7 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { mdns_daemon, - port_sender, + result_sender: port_sender, ip_cache, ports, mdns_query_timeout, @@ -219,19 +233,30 @@ impl NetworkScanner { task_manager| async move { let mut receiver = mdns::mdns_query_scan(mdns_daemon, task_manager, mdns_query_timeout)?; - todo!(); - // while let Some(mut entry) = receiver.recv().await { - // if ip_cache.read().get(&entry.addr).is_none() { - // ip_cache.write().insert(entry.addr, entry.hostname.clone()); - // } - - // let dns_name = ip_cache.read().get(&entry.addr).cloned().flatten(); - // entry.hostname = dns_name; + while let Some(ScanEntry::Result { + addr, + hostname, + port, + service_type, + }) = receiver.recv().await + { + if ip_cache.read().get(&addr).is_none() { + ip_cache.write().insert(addr, hostname.clone()); + } - // if ports.contains(&entry.port) || entry.service_type.is_some() { - // port_sender.send(entry).await?; - // } - // } + let dns_name = ip_cache.read().get(&addr).cloned().flatten(); + + if ports.contains(&port) || service_type.is_some() { + port_sender + .send(ScanEntry::Result { + addr, + hostname: dns_name, + port, + service_type, + }) + .await?; + } + } anyhow::Ok(()) }, @@ -240,7 +265,7 @@ impl NetworkScanner { let TaskExecutionRunner { context: TaskExecutionContext { - port_receiver, + result_receiver: port_receiver, mdns_daemon, .. }, @@ -304,11 +329,16 @@ impl NetworkScanner { } } +#[derive(Debug)] +pub enum ScanEvent { + PingStart { ip_addr: IpAddr }, + PingFailed { ip_addr: IpAddr, reason: PingFailedReason }, +} + #[derive(Debug)] pub enum ScanEntry { - Ping(PingEvent), - Netbios(NetBiosResult), - Regular { + ScanEvent(ScanEvent), + Result { // IP address of the device addr: IpAddr, // Hostname of the device diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 56fe9c014..9dbcc0730 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -22,8 +22,8 @@ pub(crate) struct TaskExecutionContext { pub(crate) ip_sender: IpSender, pub(crate) ip_receiver: Arc>, - pub(crate) port_sender: ScanEntrySender, - pub(crate) port_receiver: Arc>, + pub(crate) result_sender: ScanEntrySender, + pub(crate) result_receiver: Arc>, pub(crate) ip_cache: Arc>>>, @@ -77,8 +77,8 @@ impl TaskExecutionContext { let res = Self { ip_sender, ip_receiver, - port_sender, - port_receiver, + result_sender: port_sender, + result_receiver: port_receiver, ip_cache: Arc::new(parking_lot::RwLock::new(HashMap::new())), ports, runtime, @@ -166,7 +166,7 @@ impl TaskManager { self.spawn(|_| task); } - pub(crate) fn with_timeout(&self, duration: Duration) -> TimeoutManager { + pub(crate) fn with_timeout(&self, duration: Duration) -> TimeoutManager { TimeoutManager { task_manager: self.clone(), duration, @@ -184,26 +184,22 @@ impl TaskManager { } } -type TimeoutResult = Result; -pub(crate) struct TimeoutManager { +pub(crate) struct TimeoutManager { task_manager: TaskManager, duration: Duration, - when_finish: Option) + Send + 'static>>, + when_finish: Option>, } -impl TimeoutManager -where - R: Send + 'static, -{ - pub(crate) fn after_finish(self, f: F) -> Self +impl TimeoutManager { + pub(crate) fn when_finish(self, f: F) -> Self where - F: FnOnce(TimeoutResult) + Send + 'static, + F: FnOnce() + Send + 'static, { let Self { task_manager, duration, .. } = self; - let when_finish = Some(Box::new(f) as Box) + Send + 'static>); + let when_finish = Some(Box::new(f) as Box); Self { task_manager, @@ -215,7 +211,7 @@ where pub(crate) fn spawn(self, task: T) where T: FnOnce(TaskManager) -> F + Send + 'static, - F: Future + Send + 'static, + F: Future> + Send + 'static, { let Self { task_manager, @@ -225,9 +221,9 @@ where task_manager.spawn(move |task_manager| async move { let future = task(task_manager); - let result = tokio::time::timeout(duration, future).await; + let _ = tokio::time::timeout(duration, future).await; if let Some(when_finish) = when_finish { - when_finish(result); + when_finish(); } anyhow::Ok(()) }); From 7c1dd6a8faf6dfe7682c4fff9c37f6c381587327 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 9 Apr 2025 15:13:32 -0400 Subject: [PATCH 03/33] feat: improvement on net scanner --- Cargo.lock | 1 + crates/network-scanner/examples/ping_range.rs | 12 +- crates/network-scanner/examples/scan.rs | 2 + crates/network-scanner/src/ip_utils.rs | 73 +++++++++---- crates/network-scanner/src/netbios.rs | 4 +- crates/network-scanner/src/ping.rs | 27 +++-- crates/network-scanner/src/port_discovery.rs | 4 + crates/network-scanner/src/scanner.rs | 37 ++++--- crates/network-scanner/src/task_utils.rs | 24 +++- devolutions-gateway/Cargo.toml | 1 + devolutions-gateway/src/api/net.rs | 103 +++++++++++++++--- devolutions-gateway/src/middleware/auth.rs | 1 - 12 files changed, 216 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e791b5454..74513a356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,6 +1222,7 @@ dependencies = [ "rustls-cng", "serde", "serde_json", + "serde_qs", "serde_urlencoded", "smol_str", "sysinfo", diff --git a/crates/network-scanner/examples/ping_range.rs b/crates/network-scanner/examples/ping_range.rs index 83725286f..8b7b7257d 100644 --- a/crates/network-scanner/examples/ping_range.rs +++ b/crates/network-scanner/examples/ping_range.rs @@ -24,16 +24,7 @@ pub async fn main() -> anyhow::Result<()> { let ping_wait_time = Duration::from_secs(1); - let should_ping = |ip: IpAddr| { - // if ip is even, ping it - ip.is_ipv4() && { - if let IpAddr::V4(v4) = ip { - v4.octets()[3] % 2 == 0 - } else { - false - } - } - }; + let should_ping = |_: IpAddr| true; let now = std::time::Instant::now(); let mut receiver = ping_range( runtime, @@ -42,6 +33,7 @@ pub async fn main() -> anyhow::Result<()> { ping_wait_time, should_ping, TaskManager::new(), + false, )?; while let Some(ping_event) = receiver.recv().await { diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 450ece0de..7911035b9 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -29,6 +29,8 @@ fn main() -> anyhow::Result<()> { mdns_query_timeout: 5 * 1000, max_wait_time: 10 * 1000, + + ..Default::default() }; let rt = tokio::runtime::Runtime::new()?; rt.block_on(async move { diff --git a/crates/network-scanner/src/ip_utils.rs b/crates/network-scanner/src/ip_utils.rs index 1fca4a685..b6ace8bc7 100644 --- a/crates/network-scanner/src/ip_utils.rs +++ b/crates/network-scanner/src/ip_utils.rs @@ -9,6 +9,28 @@ pub enum IpAddrRange { V6(IpV6AddrRange), } +impl TryFrom<&str> for IpAddrRange { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let parts: Vec<&str> = value.split('-').collect(); + if parts.len() != 2 { + anyhow::bail!("Invalid IP range format, expected 'lower-upper', got '{}'", value); + } + let lower = parts[0].parse::()?; + let upper = parts[1].parse::()?; + IpAddrRange::new(lower, upper) + } +} + +impl TryFrom<&String> for IpAddrRange { + type Error = anyhow::Error; + + fn try_from(value: &String) -> Result { + Self::try_from(value.as_str()) + } +} + #[derive(Debug, Clone)] pub struct IpV4AddrRange { lower: Ipv4Addr, @@ -173,16 +195,14 @@ pub struct Subnet { impl From for IpAddrRange { fn from(value: Subnet) -> Self { let (lower, upper) = calculate_subnet_bounds(value.ip, value.netmask); - IpAddrRange::new(lower.into(), upper.into()) - .expect("Subnet bounds must be valid IPv4 addresses") + IpAddrRange::new(lower.into(), upper.into()).expect("Subnet bounds must be valid IPv4 addresses") } } impl From<&Subnet> for IpAddrRange { fn from(value: &Subnet) -> Self { let (lower, upper) = calculate_subnet_bounds(value.ip, value.netmask); - IpAddrRange::new(lower.into(), upper.into()) - .expect("Subnet bounds must be valid IPv4 addresses") + IpAddrRange::new(lower.into(), upper.into()).expect("Subnet bounds must be valid IPv4 addresses") } } @@ -212,8 +232,7 @@ fn calculate_subnet_bounds(ip: Ipv4Addr, netmask: Ipv4Addr) -> (Ipv4Addr, Ipv4Ad } pub fn get_subnets() -> anyhow::Result> { - let interfaces = network_interface::NetworkInterface::show() - .context("failed to get network interfaces")?; + let interfaces = network_interface::NetworkInterface::show().context("failed to get network interfaces")?; let subnets: Vec<_> = interfaces .into_iter() @@ -224,8 +243,7 @@ pub fn get_subnets() -> anyhow::Result> { // Skip loopback or link-local if v4.ip.is_loopback() || v4.ip.is_link_local() { return None; - } - // Only keep if broadcast is present + } // Only keep if broadcast is present if v4.broadcast.is_some() { let netmask = v4.netmask?; let broadcast = v4.broadcast?; @@ -254,28 +272,39 @@ mod tests { #[test] fn test_iter_ipv4() { let lower = "10.10.0.0".parse::().unwrap(); - let upper = "10.10.0.2".parse::().unwrap(); + let upper = "10.10.0.30".parse::().unwrap(); let range = IpAddrRange::new(lower.into(), upper.into()).unwrap(); let mut iter = range.into_iter(); - assert_eq!(iter.next(), Some("10.10.0.0".parse::().unwrap())); - assert_eq!(iter.next(), Some("10.10.0.1".parse::().unwrap())); - assert_eq!(iter.next(), Some("10.10.0.2".parse::().unwrap())); + for i in 0..31 { + let expected = format!("10.10.0.{i}").parse::().unwrap(); + assert_eq!(iter.next(), Some(IpAddr::V4(expected))); + } assert_eq!(iter.next(), None); } #[test] fn test_has_overlap() { - let r1 = IpAddrRange::new( - "192.168.1.0".parse().unwrap(), - "192.168.1.255".parse().unwrap(), - ) - .unwrap(); - let r2 = IpAddrRange::new( - "192.168.1.100".parse().unwrap(), - "192.168.2.10".parse().unwrap(), - ) - .unwrap(); + let r1 = IpAddrRange::new("192.168.1.0".parse().unwrap(), "192.168.1.255".parse().unwrap()).unwrap(); + let r2 = IpAddrRange::new("192.168.1.100".parse().unwrap(), "192.168.2.10".parse().unwrap()).unwrap(); assert!(r1.has_overlap(&r2)); } + + #[test] + fn test_subnet_to_ip_range() { + let subnet = Subnet { + ip: Ipv4Addr::new(192, 168, 1, 0), + netmask: Ipv4Addr::new(255, 255, 255, 0), + broadcast: Ipv4Addr::new(192, 168, 1, 255), + }; + + let ip_range = IpAddrRange::from(subnet); + + let mut iter = ip_range.into_iter(); + + for i in 0..256 { + let expected = format!("192.168.1.{i}").parse::().unwrap(); + assert_eq!(iter.next(), Some(IpAddr::V4(expected))); + } + } } diff --git a/crates/network-scanner/src/netbios.rs b/crates/network-scanner/src/netbios.rs index 21815ef71..f3e6fa0b8 100644 --- a/crates/network-scanner/src/netbios.rs +++ b/crates/network-scanner/src/netbios.rs @@ -1,5 +1,5 @@ use std::mem::MaybeUninit; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{Ipv4Addr, SocketAddr}; use std::sync::Arc; use network_scanner_net::runtime::Socket2Runtime; @@ -7,7 +7,7 @@ use network_scanner_net::socket::AsyncRawSocket; use network_scanner_proto::netbios::NetBiosPacket; use socket2::{Domain, SockAddr, Type}; -use crate::ip_utils::{IpAddrRange, IpV4AddrRange}; +use crate::ip_utils::IpV4AddrRange; use crate::task_utils::IpReceiver; use crate::{assume_init, ScannerError}; diff --git a/crates/network-scanner/src/ping.rs b/crates/network-scanner/src/ping.rs index e5cd6e760..6472fd54e 100644 --- a/crates/network-scanner/src/ping.rs +++ b/crates/network-scanner/src/ping.rs @@ -18,6 +18,15 @@ pub enum PingFailedReason { TimedOut, } +impl std::fmt::Display for PingFailedReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PingFailedReason::Rejected => write!(f, "Ping rejected"), + PingFailedReason::TimedOut => write!(f, "Ping timed out"), + } + } +} + #[derive(Debug)] pub enum PingEvent { Start { ip_addr: IpAddr }, @@ -32,6 +41,7 @@ pub fn ping_range( ping_wait_time: Duration, should_ping: impl Fn(IpAddr) -> bool + Send + Sync + 'static + Clone, task_manager: crate::task_utils::TaskManager, + disable_ping_event: bool, ) -> anyhow::Result> { let (sender, receiver) = tokio::sync::mpsc::channel(255); let mut futures = vec![]; @@ -65,7 +75,7 @@ pub fn ping_range( }) .await; } - Ok(Err(_)) => { + Ok(Err(_)) if !disable_ping_event => { let _ = sender_clone .send(PingEvent::Failed { ip_addr: addr.ip(), @@ -74,13 +84,16 @@ pub fn ping_range( .await; } Err(_) => { - let _ = sender_clone - .send(PingEvent::Failed { - ip_addr: addr.ip(), - reason: PingFailedReason::TimedOut, - }) - .await; + if !disable_ping_event { + let _ = sender_clone + .send(PingEvent::Failed { + ip_addr: addr.ip(), + reason: PingFailedReason::TimedOut, + }) + .await; + } } + _ => {} }; anyhow::Ok(()) diff --git a/crates/network-scanner/src/port_discovery.rs b/crates/network-scanner/src/port_discovery.rs index fb9f30f00..e77d59d18 100644 --- a/crates/network-scanner/src/port_discovery.rs +++ b/crates/network-scanner/src/port_discovery.rs @@ -23,6 +23,10 @@ pub async fn scan_ports( sockets.push((socket, addr)); } + if port.is_empty() { + anyhow::bail!("No ports to scan"); + } + let (sender, receiver) = tokio::sync::mpsc::channel(port.len()); for (socket, addr) in sockets { let sender = sender.clone(); diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 4c8269e24..9cf9a4522 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -38,6 +38,10 @@ pub struct NetworkScanner { pub mdns_query_timeout: Duration, /// The overall maximum duration to wait for the entire scanning process to complete. pub max_wait_time: Duration, + /// The ranges of IP addresses to ping. + pub ip_ranges: Vec, + /// The subnets to broadcast to. + pub disable_ping_event: bool, } impl NetworkScanner { @@ -51,7 +55,7 @@ impl NetworkScanner { ports, runtime, port_scan_timeout, - result_sender: port_sender, + result_sender, .. }: TaskExecutionContext, task_manager| async move { @@ -66,10 +70,10 @@ impl NetworkScanner { ip_cache.write().insert(ip, host); - let (runtime, ports, port_sender, ip_cache) = ( + let (runtime, ports, result_sender, ip_cache) = ( Arc::clone(&runtime), ports.clone(), - port_sender.clone(), + result_sender.clone(), Arc::clone(&ip_cache), ); @@ -90,7 +94,7 @@ impl NetworkScanner { if let PortScanResult::Open(socket_addr) = res { let dns = ip_cache.read().get(&ip).cloned().flatten(); - port_sender + result_sender .send(ScanEntry::Result { addr: ip, hostname: dns, @@ -112,12 +116,12 @@ impl NetworkScanner { move |TaskExecutionContext { runtime, ip_sender, - subnets, + boardcast_subnet, broadcast_timeout, .. }: TaskExecutionContext, task_manager| async move { - for subnet in subnets { + for subnet in boardcast_subnet { debug!(broadcasting_to_subnet = ?subnet); let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); task_manager.spawn(move |task_manager: TaskManager| async move { @@ -136,7 +140,7 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { - subnets, + boardcast_subnet: subnets, netbios_timeout, netbios_interval, runtime, @@ -173,18 +177,16 @@ impl NetworkScanner { ping_timeout, runtime, ip_sender, - subnets, + range_to_ping, ip_cache, result_sender, + disable_ping_event, .. }: TaskExecutionContext, task_manager| async move { - let ip_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); - debug!(ping_ip_ranges = ?ip_ranges); - let should_ping = move |ip: IpAddr| -> bool { !ip_cache.read().contains_key(&ip) }; - for ip_range in ip_ranges { + for ip_range in range_to_ping { let (task_manager, runtime, ip_sender) = (task_manager.clone(), Arc::clone(&runtime), ip_sender.clone()); let should_ping = should_ping.clone(); @@ -195,6 +197,7 @@ impl NetworkScanner { ping_timeout, should_ping, task_manager, + disable_ping_event, )?; while let Some(ip) = receiver.recv().await { @@ -224,7 +227,7 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { mdns_daemon, - result_sender: port_sender, + result_sender, ip_cache, ports, mdns_query_timeout, @@ -247,7 +250,7 @@ impl NetworkScanner { let dns_name = ip_cache.read().get(&addr).cloned().flatten(); if ports.contains(&port) || service_type.is_some() { - port_sender + result_sender .send(ScanEntry::Result { addr, hostname: dns_name, @@ -300,6 +303,8 @@ impl NetworkScanner { netbios_timeout, netbios_interval, mdns_query_timeout, + ip_ranges, + disable_ping_event, }: NetworkScannerParams, ) -> anyhow::Result { let runtime = network_scanner_net::runtime::Socket2Runtime::new(None)?; @@ -325,6 +330,8 @@ impl NetworkScanner { mdns_query_timeout, max_wait_time: max_wait, mdns_daemon: MdnsDaemon::new()?, + ip_ranges, + disable_ping_event, }) } } @@ -437,6 +444,8 @@ pub struct NetworkScannerParams { pub netbios_interval: u64, pub mdns_query_timeout: u64, pub max_wait_time: u64, // max_wait for entire scan duration in milliseconds, suggested! + pub ip_ranges: Vec, + pub disable_ping_event: bool, } #[derive(Debug, Clone, Copy)] diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 9dbcc0730..e22d5944c 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -8,7 +8,7 @@ use std::future::Future; use tokio::sync::Mutex; -use crate::ip_utils::{get_subnets, Subnet}; +use crate::ip_utils::{get_subnets, IpAddrRange, Subnet}; use crate::mdns::MdnsDaemon; use crate::scanner::{NetworkScanner, ScanEntry}; @@ -40,7 +40,11 @@ pub(crate) struct TaskExecutionContext { pub(crate) netbios_interval: Duration, // in milliseconds pub(crate) mdns_query_timeout: Duration, // in milliseconds - pub(crate) subnets: Vec, + pub(crate) boardcast_subnet: Vec, // The subnet that have a broadcast address + + pub(crate) range_to_ping: Vec, + + pub(crate) disable_ping_event: bool, } type HandlesReceiver = crossbeam::channel::Receiver>>; @@ -59,7 +63,7 @@ impl TaskExecutionContext { let (port_sender, port_receiver) = tokio::sync::mpsc::channel(100); let port_receiver = Arc::new(Mutex::new(port_receiver)); - let subnets = get_subnets()?; + let boardcast_subnet = get_subnets()?; let NetworkScanner { ports, ping_timeout, @@ -71,9 +75,19 @@ impl TaskExecutionContext { netbios_interval, mdns_daemon, mdns_query_timeout, + ip_ranges, + disable_ping_event, .. } = network_scanner; + let ping_range = match ip_ranges.len() { + 0 => boardcast_subnet + .iter() + .map(IpAddrRange::from) + .collect::>(), + _ => ip_ranges, + }; + let res = Self { ip_sender, ip_receiver, @@ -89,8 +103,10 @@ impl TaskExecutionContext { port_scan_timeout, netbios_timeout, netbios_interval, - subnets, + boardcast_subnet, + range_to_ping: ping_range, mdns_query_timeout, + disable_ping_event, }; Ok(res) diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index bc864275a..a3296e525 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -109,6 +109,7 @@ etherparse = "0.15" # For KDC proxy portpicker = "0.1" +serde_qs = "0.14.0" [target.'cfg(windows)'.dependencies] rustls-cng = { version = "0.5", default-features = false, features = ["logging", "tls12", "ring"] } diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index cbb6f378a..d2870ea76 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -2,7 +2,7 @@ use crate::http::HttpError; use crate::token::{ApplicationProtocol, Protocol}; use crate::DgwState; use axum::extract::ws::{Message, Utf8Bytes}; -use axum::extract::WebSocketUpgrade; +use axum::extract::{RawQuery, WebSocketUpgrade}; use axum::response::Response; use axum::{Json, Router}; use network_scanner::interfaces; @@ -27,9 +27,21 @@ pub fn make_router(state: DgwState) -> Router { pub async fn handle_network_scan( _token: crate::extract::NetScanToken, ws: WebSocketUpgrade, - query_params: axum::extract::Query, + RawQuery(query): RawQuery, ) -> Result { - let scanner_params: NetworkScannerParams = query_params.0.into(); + let query = query.unwrap_or_default(); + + // Use serde_qs to parse array parameters + // As suggested by serde_urlencoded author https://github.com/nox/serde_urlencoded/issues/85 + let query_params = match serde_qs::from_str::(&query) { + Ok(params) => Ok(params), + Err(e) => Err(HttpError::bad_request().build(e)), + }?; + + let scanner_params: NetworkScannerParams = query_params.try_into().map_err(|e| { + error!(error = format!("{e:#}"), "Failed to parse query parameters"); + HttpError::bad_request().build(e) + })?; let scanner = scanner::NetworkScanner::new(scanner_params).map_err(|e| { error!(error = format!("{e:#}"), "Failed to create network scanner"); @@ -61,7 +73,7 @@ pub async fn handle_network_scan( break; }; - let response = NetworkScanResponse::new(entry.addr, entry.port, entry.hostname, entry.service_type); + let response: NetworkScanResponse = entry.into(); let Ok(response) = serde_json::to_string(&response) else { warn!("Failed to serialize response"); @@ -125,14 +137,32 @@ pub struct NetworkScanQueryParams { pub mdns_query_timeout: Option, /// The maximum duration for whole networking scan in milliseconds. Highly suggested! pub max_wait: Option, + /// The start and end IP address of the range to scan. + /// for example: 10.10.0.0-10.10.0.255 + #[serde(default)] + pub ranges: Vec, + /// The ports to scan. If not specified, the default ports will be used. + #[serde(default)] + pub ports: Vec, + + #[serde(default)] + pub disable_ping_events: bool, } const COMMON_PORTS: [u16; 11] = [22, 23, 80, 443, 389, 636, 3283, 3389, 5900, 5985, 5986]; -impl From for NetworkScannerParams { - fn from(val: NetworkScanQueryParams) -> Self { - NetworkScannerParams { - ports: COMMON_PORTS.to_vec(), +impl TryFrom for NetworkScannerParams { + type Error = anyhow::Error; + fn try_from(val: NetworkScanQueryParams) -> Result { + warn!(query=?val, "Network scan query parameters"); + + let ports = match val.ports.len() { + 0 => COMMON_PORTS.to_vec(), + _ => val.ports, + }; + + Ok(NetworkScannerParams { + ports, ping_interval: val.ping_interval.unwrap_or(200), ping_timeout: val.ping_timeout.unwrap_or(500), broadcast_timeout: val.broadcast_timeout.unwrap_or(1000), @@ -141,15 +171,48 @@ impl From for NetworkScannerParams { max_wait_time: val.max_wait.unwrap_or(120 * 1000), netbios_interval: val.netbios_interval.unwrap_or(200), mdns_query_timeout: val.mdns_query_timeout.unwrap_or(5 * 1000), // in milliseconds + ip_ranges: val + .ranges + .iter() + .map(IpAddrRange::try_from) + .collect::, anyhow::Error>>()?, + disable_ping_event: val.disable_ping_events, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ScanEvent { + #[serde(rename_all = "lowercase")] + PingStart { ip_addr: IpAddr }, + #[serde(rename_all = "lowercase")] + PingFailed { ip_addr: IpAddr, reason: String }, +} + +impl From for ScanEvent { + fn from(event: scanner::ScanEvent) -> Self { + match event { + scanner::ScanEvent::PingStart { ip_addr } => Self::PingStart { ip_addr }, + scanner::ScanEvent::PingFailed { ip_addr, reason } => Self::PingFailed { + ip_addr, + reason: reason.to_string(), + }, } } } #[derive(Debug, Serialize)] -pub struct NetworkScanResponse { - pub ip: IpAddr, - pub hostname: Option, - pub protocol: ApplicationProtocol, +#[serde(rename_all = "lowercase")] +pub enum NetworkScanResponse { + #[serde(rename_all = "lowercase")] + Event(ScanEvent), + #[serde(rename_all = "lowercase")] + Entry { + ip: IpAddr, + hostname: Option, + protocol: ApplicationProtocol, + }, } impl NetworkScanResponse { @@ -185,7 +248,21 @@ impl NetworkScanResponse { _ => ApplicationProtocol::unknown(), } }; - Self { ip, hostname, protocol } + Self::Entry { ip, hostname, protocol } + } +} + +impl From for NetworkScanResponse { + fn from(entry: scanner::ScanEntry) -> Self { + match entry { + scanner::ScanEntry::ScanEvent(event) => Self::Event(event.into()), + scanner::ScanEntry::Result { + addr, + hostname, + port, + service_type, + } => Self::new(addr, port, hostname, service_type), + } } } diff --git a/devolutions-gateway/src/middleware/auth.rs b/devolutions-gateway/src/middleware/auth.rs index 416051cbf..6958d2b77 100644 --- a/devolutions-gateway/src/middleware/auth.rs +++ b/devolutions-gateway/src/middleware/auth.rs @@ -102,7 +102,6 @@ pub async fn auth_middleware( struct TokenQueryParam<'a> { token: &'a str, } - let method = request.method(); let uri_path = request.uri().path(); From 5c4f04645d638feeb1a8e2c8520bd936db527c86 Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 14:44:09 -0400 Subject: [PATCH 04/33] add a few more thing according to doc --- crates/network-scanner/examples/ping_range.rs | 1 - .../network-scanner/examples/reverse_dns.rs | 25 + crates/network-scanner/examples/scan.rs | 37 +- crates/network-scanner/src/ping.rs | 18 +- crates/network-scanner/src/scanner.rs | 610 ++++++++++-------- crates/network-scanner/src/task_utils.rs | 94 ++- devolutions-gateway/src/api/net.rs | 105 ++- 7 files changed, 508 insertions(+), 382 deletions(-) create mode 100644 crates/network-scanner/examples/reverse_dns.rs diff --git a/crates/network-scanner/examples/ping_range.rs b/crates/network-scanner/examples/ping_range.rs index 8b7b7257d..fbaa8a69a 100644 --- a/crates/network-scanner/examples/ping_range.rs +++ b/crates/network-scanner/examples/ping_range.rs @@ -33,7 +33,6 @@ pub async fn main() -> anyhow::Result<()> { ping_wait_time, should_ping, TaskManager::new(), - false, )?; while let Some(ping_event) = receiver.recv().await { diff --git a/crates/network-scanner/examples/reverse_dns.rs b/crates/network-scanner/examples/reverse_dns.rs new file mode 100644 index 000000000..14623e731 --- /dev/null +++ b/crates/network-scanner/examples/reverse_dns.rs @@ -0,0 +1,25 @@ +use dns_lookup::lookup_addr; +use std::env; +use std::net::IpAddr; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() != 2 || args[1] == "-h" || args[1] == "--help" { + println!("Usage: {} ", args[0]); + return; + } + + let ip: IpAddr = match args[1].parse() { + Ok(ip) => ip, + Err(_) => { + eprintln!("Invalid IP address."); + return; + } + }; + + match lookup_addr(&ip) { + Ok(hostname) => println!("{}", hostname), + Err(e) => eprintln!("Lookup failed: {}", e), + } +} diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 7911035b9..24ae7286b 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::Context; -use network_scanner::scanner::{NetworkScanner, NetworkScannerParams}; +use network_scanner::scanner::{NetworkScanner, NetworkScannerParams, ScannerConfig, ScannerToggles}; use tokio::time::timeout; fn main() -> anyhow::Result<()> { @@ -15,22 +15,25 @@ fn main() -> anyhow::Result<()> { .init(); let params = NetworkScannerParams { - ports: vec![22, 80, 443, 389, 636], - ping_interval: 20, - ping_timeout: 1000, - - broadcast_timeout: 2000, - - port_scan_timeout: 2000, - - netbios_timeout: 1000, - netbios_interval: 20, - - mdns_query_timeout: 5 * 1000, - - max_wait_time: 10 * 1000, - - ..Default::default() + configs: ScannerConfig { + ip_ranges: vec![], + ports: vec![22, 80, 443, 389, 636], + ping_interval: 20, + ping_timeout: 1000, + broadcast_timeout: 2000, + port_scan_timeout: 2000, + netbios_timeout: 1000, + netbios_interval: 20, + mdns_query_timeout: 5 * 1000, + max_wait_time: 10 * 1000, + }, + toggles: ScannerToggles { + disable_boardcast: false, + disable_subnet_scan: false, + disable_ping_event: false, + disable_resolve_dns: false, + disable_zeroconf: false, + }, }; let rt = tokio::runtime::Runtime::new()?; rt.block_on(async move { diff --git a/crates/network-scanner/src/ping.rs b/crates/network-scanner/src/ping.rs index 6472fd54e..22be8f017 100644 --- a/crates/network-scanner/src/ping.rs +++ b/crates/network-scanner/src/ping.rs @@ -41,7 +41,6 @@ pub fn ping_range( ping_wait_time: Duration, should_ping: impl Fn(IpAddr) -> bool + Send + Sync + 'static + Clone, task_manager: crate::task_utils::TaskManager, - disable_ping_event: bool, ) -> anyhow::Result> { let (sender, receiver) = tokio::sync::mpsc::channel(255); let mut futures = vec![]; @@ -75,7 +74,7 @@ pub fn ping_range( }) .await; } - Ok(Err(_)) if !disable_ping_event => { + Ok(Err(_)) => { let _ = sender_clone .send(PingEvent::Failed { ip_addr: addr.ip(), @@ -84,16 +83,13 @@ pub fn ping_range( .await; } Err(_) => { - if !disable_ping_event { - let _ = sender_clone - .send(PingEvent::Failed { - ip_addr: addr.ip(), - reason: PingFailedReason::TimedOut, - }) - .await; - } + let _ = sender_clone + .send(PingEvent::Failed { + ip_addr: addr.ip(), + reason: PingFailedReason::TimedOut, + }) + .await; } - _ => {} }; anyhow::Ok(()) diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 9cf9a4522..151c50cf2 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -16,254 +16,34 @@ use typed_builder::TypedBuilder; /// Represents a network scanner for discovering devices and their services over a network. #[derive(Clone)] pub struct NetworkScanner { - /// A list of ports to scan on discovered devices. - pub ports: Vec, /// The runtime environment for socket operations, wrapped in an `Arc` for thread-safe sharing. pub(crate) runtime: Arc, /// A daemon for Multicast DNS (mDNS) operations, handling service discovery. - pub(crate) mdns_daemon: MdnsDaemon, - /// The interval between ping operations. - pub ping_interval: Duration, - /// The maximum amount of time to wait for a ping response. - pub ping_timeout: Duration, - /// The maximum amount of time to wait for responses to a broadcast request. - pub broadcast_timeout: Duration, - /// The maximum amount of time to wait for a tcp port scan response. - pub port_scan_timeout: Duration, - /// The maximum amount of time to wait for a NetBIOS query response. - pub netbios_timeout: Duration, - /// The interval between successive NetBIOS query attempts. - pub netbios_interval: Duration, - /// The maximum amount of time to wait for individual mDNS query response. - pub mdns_query_timeout: Duration, - /// The overall maximum duration to wait for the entire scanning process to complete. - pub max_wait_time: Duration, - /// The ranges of IP addresses to ping. - pub ip_ranges: Vec, - /// The subnets to broadcast to. - pub disable_ping_event: bool, + pub(crate) mdns_daemon: Option, + + /// Configuration settings for the network scanner + pub(crate) configs: ScannerConfig, + /// Toggles for enabling or disabling specific features of the scanner. + pub(crate) toggles: ScannerToggles, } impl NetworkScanner { pub fn start(&self) -> anyhow::Result> { let mut task_executor = TaskExecutionRunner::new(self.clone())?; - task_executor.run( - move |TaskExecutionContext { - ip_cache, - ip_receiver, - ports, - runtime, - port_scan_timeout, - result_sender, - .. - }: TaskExecutionContext, - task_manager| async move { - let ip_cache = Arc::clone(&ip_cache); - while let Some((ip, host)) = ip_receiver.lock().await.recv().await { - if ip_cache.read().get(&ip).is_some() { - if host.is_some() { - ip_cache.write().insert(ip, host); - } - continue; - } - - ip_cache.write().insert(ip, host); - - let (runtime, ports, result_sender, ip_cache) = ( - Arc::clone(&runtime), - ports.clone(), - result_sender.clone(), - Arc::clone(&ip_cache), - ); - - task_manager.spawn(move |task_manager| async move { - debug!(scanning_ip = ?ip); - - let dns_look_up_res = tokio::task::spawn_blocking(move || dns_lookup::lookup_addr(&ip).ok()); - - let mut port_scan_receiver = - scan_ports(ip, &ports, runtime, port_scan_timeout, task_manager).await?; - - let dns = dns_look_up_res.await?; + start_port_scan(&mut task_executor); - ip_cache.write().insert(ip, dns.clone()); + if !self.toggles.disable_boardcast { + start_boardcast(&mut task_executor); + } - while let Some(res) = port_scan_receiver.recv().await { - trace!(port_scan_result = ?res); - if let PortScanResult::Open(socket_addr) = res { - let dns = ip_cache.read().get(&ip).cloned().flatten(); + start_netbios(&mut task_executor); - result_sender - .send(ScanEntry::Result { - addr: ip, - hostname: dns, - port: socket_addr.port(), - service_type: None, - }) - .await?; - } - } - anyhow::Ok(()) - }); - } - - anyhow::Ok(()) - }, - ); - - task_executor.run( - move |TaskExecutionContext { - runtime, - ip_sender, - boardcast_subnet, - broadcast_timeout, - .. - }: TaskExecutionContext, - task_manager| async move { - for subnet in boardcast_subnet { - debug!(broadcasting_to_subnet = ?subnet); - let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); - task_manager.spawn(move |task_manager: TaskManager| async move { - let mut receiver = - broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; - while let Some(ip) = receiver.recv().await { - trace!(broadcast_sent_ip = ?ip); - ip_sender.send((ip.into(), None)).await?; - } - anyhow::Ok(()) - }); - } - anyhow::Ok(()) - }, - ); - - task_executor.run( - move |TaskExecutionContext { - boardcast_subnet: subnets, - netbios_timeout, - netbios_interval, - runtime, - ip_sender, - .. - }: TaskExecutionContext, - task_manager| async move { - let ip_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); - debug!(netbios_query_ip_ranges = ?ip_ranges); - - for ip_range in ip_ranges { - let (runtime, ip_sender, task_manager) = - (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); - - let IpAddrRange::V4(ip_range) = ip_range else { - continue; - }; + start_ping(&mut task_executor); - let mut receiver = - netbios_query_scan(runtime, ip_range, netbios_timeout, netbios_interval, task_manager)?; - - while let Some(res) = receiver.recv().await { - debug!(netbios_query_sent_ip = ?res.0); - ip_sender.send(res).await?; - } - } - anyhow::Ok(()) - }, - ); - - task_executor.run( - move |TaskExecutionContext { - ping_interval, - ping_timeout, - runtime, - ip_sender, - range_to_ping, - ip_cache, - result_sender, - disable_ping_event, - .. - }: TaskExecutionContext, - task_manager| async move { - let should_ping = move |ip: IpAddr| -> bool { !ip_cache.read().contains_key(&ip) }; - - for ip_range in range_to_ping { - let (task_manager, runtime, ip_sender) = - (task_manager.clone(), Arc::clone(&runtime), ip_sender.clone()); - let should_ping = should_ping.clone(); - let mut receiver = ping_range( - runtime, - ip_range, - ping_interval, - ping_timeout, - should_ping, - task_manager, - disable_ping_event, - )?; - - while let Some(ip) = receiver.recv().await { - debug!(ping_sent_ip = ?ip); - match ip { - crate::ping::PingEvent::Success { ip_addr, time } => { - ip_sender.send((ip_addr, None)).await?; - debug!(ping_success_ip = ?ip_addr, time = ?time); - } - crate::ping::PingEvent::Start { ip_addr } => { - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::PingStart { ip_addr })) - .await?; - } - crate::ping::PingEvent::Failed { ip_addr, reason } => { - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::PingFailed { ip_addr, reason })) - .await?; - } - } - } - } - anyhow::Ok(()) - }, - ); - - task_executor.run( - move |TaskExecutionContext { - mdns_daemon, - result_sender, - ip_cache, - ports, - mdns_query_timeout, - .. - }, - task_manager| async move { - let mut receiver = mdns::mdns_query_scan(mdns_daemon, task_manager, mdns_query_timeout)?; - - while let Some(ScanEntry::Result { - addr, - hostname, - port, - service_type, - }) = receiver.recv().await - { - if ip_cache.read().get(&addr).is_none() { - ip_cache.write().insert(addr, hostname.clone()); - } - - let dns_name = ip_cache.read().get(&addr).cloned().flatten(); - - if ports.contains(&port) || service_type.is_some() { - result_sender - .send(ScanEntry::Result { - addr, - hostname: dns_name, - port, - service_type, - }) - .await?; - } - } - - anyhow::Ok(()) - }, - ); + if !self.toggles.disable_zeroconf { + start_mdns(&mut task_executor); + } let TaskExecutionRunner { context: @@ -282,56 +62,310 @@ impl NetworkScanner { }); let scanner_stream_clone = Arc::clone(&scanner_stream); - let max_wait_time = self.max_wait_time; + let max_wait_time = Duration::from_millis(self.configs.max_wait_time); tokio::spawn(async move { tokio::time::sleep(max_wait_time).await; scanner_stream_clone.stop(); }); - Ok(scanner_stream) + return Ok(scanner_stream); + + fn start_port_scan(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + ip_cache, + ip_receiver, + runtime, + result_sender, + configs, + toggles, + .. + }: TaskExecutionContext, + task_manager| async move { + let ip_cache = Arc::clone(&ip_cache); + + let ports = configs.ports.clone(); + let disable_dns_resolve = toggles.disable_resolve_dns; + + while let Some((ip, host)) = ip_receiver.lock().await.recv().await { + // ==========================Check cache and dns resolve ============================== + // The host is optional, it can be sent from a netbios query or a mDNS query that have hostname + // Or if can be None if it's from a ping or broadcast scan + + // Check if the IP is already in the cache (if there is, that means we had it scanned before) + let is_new = ip_cache.read().get(&ip).is_none(); + let updated_dns = if is_new { + let resolve_dns = if !disable_dns_resolve { + tokio::task::spawn_blocking(move || dns_lookup::lookup_addr(&ip)) + .await? + .ok() + } else { + None + }; + let final_dns = resolve_dns.or(host); + ip_cache.write().insert(ip, final_dns.clone()); + final_dns + } else if host.is_some() { + // If the host is not None, we should update the cache with the new hostname + ip_cache.write().insert(ip, host.clone()); + host + } else { + None + }; + + if let Some(dns) = updated_dns { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::Dns { + ip_addr: ip, + hostname: dns, + })) + .await?; + } + + if !is_new { + continue; + } + + // ======================end of check cache and dns resolve ========================= + + let (runtime, ports, result_sender, ip_cache, port_scan_timeout) = ( + Arc::clone(&runtime), + ports.clone(), + result_sender.clone(), + Arc::clone(&ip_cache), + configs.port_scan_timeout, + ); + + task_manager.spawn(move |task_manager| async move { + debug!(scanning_ip = ?ip); + + let mut port_scan_receiver = + scan_ports(ip, &ports, runtime, port_scan_timeout, task_manager).await?; + + while let Some(res) = port_scan_receiver.recv().await { + trace!(port_scan_result = ?res); + if let PortScanResult::Open(socket_addr) = res { + let dns = ip_cache.read().get(&ip).cloned().flatten(); + + result_sender + .send(ScanEntry::Result { + addr: ip, + hostname: dns, + port: socket_addr.port(), + service_type: None, + }) + .await?; + } + } + anyhow::Ok(()) + }); + } + + anyhow::Ok(()) + }, + ); + } + + fn start_boardcast(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + runtime, + ip_sender, + configs, + .. + }: TaskExecutionContext, + task_manager| async move { + let boardcast_subnet = configs.boardcast_subnet; + let boardcast_timeout = configs.broadcast_timeout; + for subnet in boardcast_subnet { + debug!(broadcasting_to_subnet = ?subnet); + let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); + task_manager.spawn(move |task_manager: TaskManager| async move { + let mut receiver = + broadcast(subnet.broadcast, boardcast_timeout, runtime, task_manager).await?; + while let Some(ip) = receiver.recv().await { + trace!(broadcast_sent_ip = ?ip); + ip_sender.send((ip.into(), None)).await?; + } + anyhow::Ok(()) + }); + } + anyhow::Ok(()) + }, + ); + } + + fn start_netbios(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + runtime, + ip_sender, + configs, + .. + }: TaskExecutionContext, + task_manager| async move { + let netbios_timeout = configs.netbios_timeout; + let netbios_interval = configs.netbios_interval; + let subnets = configs.boardcast_subnet; + + let ip_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); + debug!(netbios_query_ip_ranges = ?ip_ranges); + + for ip_range in ip_ranges { + let (runtime, ip_sender, task_manager) = + (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); + + let IpAddrRange::V4(ip_range) = ip_range else { + continue; + }; + + let mut receiver = + netbios_query_scan(runtime, ip_range, netbios_timeout, netbios_interval, task_manager)?; + + while let Some(res) = receiver.recv().await { + debug!(netbios_query_sent_ip = ?res.0); + ip_sender.send(res).await?; + } + } + anyhow::Ok(()) + }, + ); + } + + fn start_ping(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + runtime, + ip_sender, + ip_cache, + result_sender, + toggles, + configs, + .. + }: TaskExecutionContext, + task_manager| async move { + let should_ping = move |ip: IpAddr| -> bool { !ip_cache.read().contains_key(&ip) }; + + let ping_interval = configs.ping_interval; + let ping_timeout = configs.ping_timeout; + let range_to_ping = configs.range_to_ping; + let disable_ping_event = toggles.disable_ping_event; + + for ip_range in range_to_ping { + let (task_manager, runtime, ip_sender) = + (task_manager.clone(), Arc::clone(&runtime), ip_sender.clone()); + let should_ping = should_ping.clone(); + let mut receiver = ping_range( + runtime, + ip_range, + ping_interval, + ping_timeout, + should_ping, + task_manager, + )?; + + while let Some(ping_event) = receiver.recv().await { + debug!(ping_sent_ip = ?ping_event); + if let crate::ping::PingEvent::Success { ip_addr, .. } = ping_event { + ip_sender.send((ip_addr, None)).await?; + }; + + if disable_ping_event { + continue; + } + + match ping_event { + crate::ping::PingEvent::Success { ip_addr, time } => { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::PingSuccess { ip_addr, time })) + .await?; + } + crate::ping::PingEvent::Start { ip_addr } => { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::PingStart { ip_addr })) + .await?; + } + crate::ping::PingEvent::Failed { ip_addr, reason } => { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::PingFailed { ip_addr, reason })) + .await?; + } + } + } + } + anyhow::Ok(()) + }, + ); + } + + fn start_mdns(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + mdns_daemon, + result_sender, + ip_cache, + configs, + .. + }, + task_manager| async move { + // Since mDNS deamon is started at the point it's created, we set it to None in order to avoid resouce waste + // Caller of the start_mdns function should garentee that the deamon exists + let mdns_daemon = match mdns_daemon { + Some(daemon) => daemon, + None => anyhow::bail!("mDNS daemon is not available but mDNS is enabled"), + }; + + let mdns_query_timeout = configs.mdns_query_timeout; + let ports = configs.ports.clone(); + + let mut receiver = mdns::mdns_query_scan(mdns_daemon, task_manager, mdns_query_timeout)?; + + while let Some(ScanEntry::Result { + addr, + hostname, + port, + service_type, + }) = receiver.recv().await + { + if ip_cache.read().get(&addr).is_none() { + ip_cache.write().insert(addr, hostname.clone()); + } + + let dns_name = ip_cache.read().get(&addr).cloned().flatten(); + + if ports.contains(&port) || service_type.is_some() { + result_sender + .send(ScanEntry::Result { + addr, + hostname: dns_name, + port, + service_type, + }) + .await?; + } + } + + anyhow::Ok(()) + }, + ); + } } - pub fn new( - NetworkScannerParams { - ports, - ping_timeout, - max_wait_time: max_wait, - ping_interval, - broadcast_timeout, - port_scan_timeout, - netbios_timeout, - netbios_interval, - mdns_query_timeout, - ip_ranges, - disable_ping_event, - }: NetworkScannerParams, - ) -> anyhow::Result { + pub fn new(NetworkScannerParams { configs, toggles }: NetworkScannerParams) -> anyhow::Result { let runtime = network_scanner_net::runtime::Socket2Runtime::new(None)?; - let ping_timeout = Duration::from_millis(ping_timeout); - let ping_interval = Duration::from_millis(ping_interval); - let broadcast_timeout = Duration::from_millis(broadcast_timeout); - let port_scan_timeout = Duration::from_millis(port_scan_timeout); - let netbios_timeout = Duration::from_millis(netbios_timeout); - let netbios_interval = Duration::from_millis(netbios_interval); - let mdns_query_timeout = Duration::from_millis(mdns_query_timeout); - let max_wait = Duration::from_millis(max_wait); + let mdns_daemon = if toggles.disable_zeroconf { + None + } else { + Some(MdnsDaemon::new()?) + }; Ok(Self { + configs, + toggles, + mdns_daemon, runtime, - ports, - ping_interval, - ping_timeout, - broadcast_timeout, - port_scan_timeout, - netbios_timeout, - netbios_interval, - mdns_query_timeout, - max_wait_time: max_wait, - mdns_daemon: MdnsDaemon::new()?, - ip_ranges, - disable_ping_event, }) } } @@ -340,6 +374,8 @@ impl NetworkScanner { pub enum ScanEvent { PingStart { ip_addr: IpAddr }, PingFailed { ip_addr: IpAddr, reason: PingFailedReason }, + PingSuccess { ip_addr: IpAddr, time: u128 }, + Dns { ip_addr: IpAddr, hostname: String }, } #[derive(Debug)] @@ -360,7 +396,7 @@ pub enum ScanEntry { pub struct NetworkScannerStream { result_receiver: Arc>, task_manager: TaskManager, - mdns_daemon: MdnsDaemon, + mdns_daemon: Option, } impl NetworkScannerStream { @@ -377,7 +413,9 @@ impl NetworkScannerStream { pub fn stop(self: Arc) { self.task_manager.stop(); - self.mdns_daemon.stop(); + if let Some(deamon) = &self.mdns_daemon { + deamon.stop(); + }; } } @@ -432,9 +470,17 @@ impl Display for ScanMethod { } } -/// The parameters for configuring a network scanner. All fields are in milliseconds. -#[derive(Debug, Clone, TypedBuilder, Default)] -pub struct NetworkScannerParams { +#[derive(Debug, Clone)] +pub struct ScannerToggles { + pub disable_ping_event: bool, + pub disable_boardcast: bool, + pub disable_subnet_scan: bool, + pub disable_zeroconf: bool, + pub disable_resolve_dns: bool, +} + +#[derive(Debug, Clone)] +pub struct ScannerConfig { pub ports: Vec, pub ping_interval: u64, pub ping_timeout: u64, @@ -443,9 +489,15 @@ pub struct NetworkScannerParams { pub netbios_timeout: u64, pub netbios_interval: u64, pub mdns_query_timeout: u64, - pub max_wait_time: u64, // max_wait for entire scan duration in milliseconds, suggested! + pub max_wait_time: u64, pub ip_ranges: Vec, - pub disable_ping_event: bool, +} + +/// The parameters for configuring a network scanner. All fields are in milliseconds. +#[derive(Debug, Clone, TypedBuilder)] +pub struct NetworkScannerParams { + pub configs: ScannerConfig, + pub toggles: ScannerToggles, } #[derive(Debug, Clone, Copy)] diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index e22d5944c..7804e03c4 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -10,13 +10,49 @@ use tokio::sync::Mutex; use crate::ip_utils::{get_subnets, IpAddrRange, Subnet}; use crate::mdns::MdnsDaemon; -use crate::scanner::{NetworkScanner, ScanEntry}; +use crate::scanner::{NetworkScanner, ScanEntry, ScannerConfig, ScannerToggles}; pub(crate) type IpSender = tokio::sync::mpsc::Sender<(IpAddr, Option)>; pub(crate) type IpReceiver = tokio::sync::mpsc::Receiver<(IpAddr, Option)>; pub(crate) type ScanEntrySender = tokio::sync::mpsc::Sender; pub(crate) type ScanEntryReceiver = tokio::sync::mpsc::Receiver; +#[derive(Debug, Clone)] +pub(crate) struct ContextConfig { + pub(crate) boardcast_subnet: Vec, // The subnet that have a broadcast address + pub(crate) range_to_ping: Vec, + pub ports: Vec, + pub ping_interval: Duration, + pub ping_timeout: Duration, + pub broadcast_timeout: Duration, + pub port_scan_timeout: Duration, + pub netbios_timeout: Duration, + pub netbios_interval: Duration, + pub mdns_query_timeout: Duration, +} + +impl ContextConfig { + pub(crate) fn new(configs: ScannerConfig, toggles: &ScannerToggles, subnet: Vec) -> Self { + let range_to_ping = match configs.ip_ranges.len() { + 0 if !toggles.disable_subnet_scan => subnet.iter().map(IpAddrRange::from).collect::>(), + _ => configs.ip_ranges.clone(), + }; + + Self { + boardcast_subnet: subnet, + range_to_ping, + ports: configs.ports, + ping_interval: Duration::from_millis(configs.ping_interval), + ping_timeout: Duration::from_millis(configs.ping_timeout), + broadcast_timeout: Duration::from_millis(configs.broadcast_timeout), + port_scan_timeout: Duration::from_millis(configs.port_scan_timeout), + netbios_timeout: Duration::from_millis(configs.netbios_timeout), + netbios_interval: Duration::from_millis(configs.netbios_interval), + mdns_query_timeout: Duration::from_millis(configs.mdns_query_timeout), + } + } +} + #[derive(Clone)] pub(crate) struct TaskExecutionContext { pub(crate) ip_sender: IpSender, @@ -27,24 +63,11 @@ pub(crate) struct TaskExecutionContext { pub(crate) ip_cache: Arc>>>, - pub(crate) ports: Vec, - pub(crate) runtime: Arc, - pub(crate) mdns_daemon: MdnsDaemon, - - pub(crate) ping_interval: Duration, // in milliseconds - pub(crate) ping_timeout: Duration, // in milliseconds - pub(crate) broadcast_timeout: Duration, // in milliseconds - pub(crate) port_scan_timeout: Duration, // in milliseconds - pub(crate) netbios_timeout: Duration, // in milliseconds - pub(crate) netbios_interval: Duration, // in milliseconds - pub(crate) mdns_query_timeout: Duration, // in milliseconds - - pub(crate) boardcast_subnet: Vec, // The subnet that have a broadcast address - - pub(crate) range_to_ping: Vec, + pub(crate) mdns_daemon: Option, - pub(crate) disable_ping_event: bool, + pub(crate) configs: ContextConfig, + pub(crate) toggles: ScannerToggles, } type HandlesReceiver = crossbeam::channel::Receiver>>; @@ -63,50 +86,25 @@ impl TaskExecutionContext { let (port_sender, port_receiver) = tokio::sync::mpsc::channel(100); let port_receiver = Arc::new(Mutex::new(port_receiver)); - let boardcast_subnet = get_subnets()?; let NetworkScanner { - ports, - ping_timeout, - ping_interval, - broadcast_timeout, - port_scan_timeout, - netbios_timeout, - runtime, - netbios_interval, mdns_daemon, - mdns_query_timeout, - ip_ranges, - disable_ping_event, + runtime, + configs, + toggles, .. } = network_scanner; - let ping_range = match ip_ranges.len() { - 0 => boardcast_subnet - .iter() - .map(IpAddrRange::from) - .collect::>(), - _ => ip_ranges, - }; - + let boardcast_subnet = get_subnets()?; let res = Self { ip_sender, ip_receiver, result_sender: port_sender, result_receiver: port_receiver, ip_cache: Arc::new(parking_lot::RwLock::new(HashMap::new())), - ports, runtime, mdns_daemon, - ping_interval, - ping_timeout, - broadcast_timeout, - port_scan_timeout, - netbios_timeout, - netbios_interval, - boardcast_subnet, - range_to_ping: ping_range, - mdns_query_timeout, - disable_ping_event, + configs: ContextConfig::new(configs, &toggles, boardcast_subnet), + toggles, }; Ok(res) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index d2870ea76..972c630ab 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -7,6 +7,7 @@ use axum::response::Response; use axum::{Json, Router}; use network_scanner::interfaces; use network_scanner::scanner::{self, NetworkScannerParams}; +use network_scanner::ip_utils::IpAddrRange; use serde::Serialize; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -145,8 +146,29 @@ pub struct NetworkScanQueryParams { #[serde(default)] pub ports: Vec, - #[serde(default)] + /// Disable the emission of ScanEvent::Ping + #[serde(default = "default_true")] pub disable_ping_events: bool, + + /// Disable the execution of broadcast scan + #[serde(default)] + pub disable_boardcast: bool, + + /// Disable the ping scan on subnet + #[serde(default)] + pub disable_subnet_scan: bool, + + /// Disable ZeroConf/mDNS + #[serde(default)] + pub disable_zeroconf: bool, + + /// Disable resolve dns + #[serde(default)] + pub disable_resolve_dns: bool, +} + +fn default_true() -> bool { + true } const COMMON_PORTS: [u16; 11] = [22, 23, 80, 443, 389, 636, 3283, 3389, 5900, 5985, 5986]; @@ -162,52 +184,83 @@ impl TryFrom for NetworkScannerParams { }; Ok(NetworkScannerParams { - ports, - ping_interval: val.ping_interval.unwrap_or(200), - ping_timeout: val.ping_timeout.unwrap_or(500), - broadcast_timeout: val.broadcast_timeout.unwrap_or(1000), - port_scan_timeout: val.port_scan_timeout.unwrap_or(1000), - netbios_timeout: val.netbios_timeout.unwrap_or(1000), - max_wait_time: val.max_wait.unwrap_or(120 * 1000), - netbios_interval: val.netbios_interval.unwrap_or(200), - mdns_query_timeout: val.mdns_query_timeout.unwrap_or(5 * 1000), // in milliseconds - ip_ranges: val - .ranges - .iter() - .map(IpAddrRange::try_from) - .collect::, anyhow::Error>>()?, - disable_ping_event: val.disable_ping_events, + configs: ScannerConfig { + ports, + ping_interval: val.ping_interval.unwrap_or(200), + ping_timeout: val.ping_timeout.unwrap_or(500), + broadcast_timeout: val.broadcast_timeout.unwrap_or(1000), + port_scan_timeout: val.port_scan_timeout.unwrap_or(1000), + netbios_timeout: val.netbios_timeout.unwrap_or(1000), + max_wait_time: val.max_wait.unwrap_or(120 * 1000), + netbios_interval: val.netbios_interval.unwrap_or(200), + mdns_query_timeout: val.mdns_query_timeout.unwrap_or(5 * 1000), // in milliseconds + ip_ranges: val + .ranges + .iter() + .map(IpAddrRange::try_from) + .collect::, anyhow::Error>>()?, + }, + toggles: scanner::ScannerToggles { + disable_ping_event: val.disable_ping_events, + disable_boardcast: val.disable_boardcast, + disable_subnet_scan: val.disable_subnet_scan, + disable_zeroconf: val.disable_zeroconf, + disable_resolve_dns: val.disable_resolve_dns, + }, }) } } #[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] +pub enum Status { + Start, + Failed, + Success, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "protocol", rename_all = "lowercase")] pub enum ScanEvent { - #[serde(rename_all = "lowercase")] - PingStart { ip_addr: IpAddr }, - #[serde(rename_all = "lowercase")] - PingFailed { ip_addr: IpAddr, reason: String }, + Ping { + ip_addr: IpAddr, + status: Status, + #[serde(skip_serializing_if = "Option::is_none")] + time: Option, + }, + Dns { + ip_addr: IpAddr, + hostname: String, + }, } impl From for ScanEvent { fn from(event: scanner::ScanEvent) -> Self { match event { - scanner::ScanEvent::PingStart { ip_addr } => Self::PingStart { ip_addr }, - scanner::ScanEvent::PingFailed { ip_addr, reason } => Self::PingFailed { + scanner::ScanEvent::PingStart { ip_addr } => Self::Ping { ip_addr, - reason: reason.to_string(), + status: Status::Start, + time: None, }, + scanner::ScanEvent::PingSuccess { ip_addr, time } => Self::Ping { + ip_addr, + status: Status::Success, + time: Some(time), + }, + scanner::ScanEvent::PingFailed { ip_addr, .. } => Self::Ping { + ip_addr, + status: Status::Failed, + time: None, + }, + scanner::ScanEvent::Dns { ip_addr, hostname } => Self::Dns { ip_addr, hostname }, } } } #[derive(Debug, Serialize)] -#[serde(rename_all = "lowercase")] +#[serde(untagged, rename_all = "lowercase")] pub enum NetworkScanResponse { - #[serde(rename_all = "lowercase")] Event(ScanEvent), - #[serde(rename_all = "lowercase")] Entry { ip: IpAddr, hostname: Option, From 5c84c708fb2607970921204fb4488c7ab5d4a74b Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 15:12:26 -0400 Subject: [PATCH 05/33] fmt typo and review fixes --- crates/network-scanner/examples/ping_range.rs | 6 +- crates/network-scanner/examples/scan.rs | 2 +- crates/network-scanner/src/ip_utils.rs | 96 ++++++++++++------- crates/network-scanner/src/mdns.rs | 2 +- crates/network-scanner/src/scanner.rs | 26 ++--- crates/network-scanner/src/task_utils.rs | 8 +- 6 files changed, 84 insertions(+), 56 deletions(-) diff --git a/crates/network-scanner/examples/ping_range.rs b/crates/network-scanner/examples/ping_range.rs index fbaa8a69a..94c82b368 100644 --- a/crates/network-scanner/examples/ping_range.rs +++ b/crates/network-scanner/examples/ping_range.rs @@ -13,10 +13,10 @@ pub async fn main() -> anyhow::Result<()> { .with_max_level(tracing::Level::INFO) .init(); - let lower = IpAddr::V4(Ipv4Addr::new(10, 10, 0, 0)); - let upper = IpAddr::V4(Ipv4Addr::new(10, 10, 0, 125)); + let lower = Ipv4Addr::new(10, 10, 0, 0); + let upper = Ipv4Addr::new(10, 10, 0, 125); - let range = network_scanner::ip_utils::IpAddrRange::new(lower, upper)?; + let range = network_scanner::ip_utils::IpAddrRange::new_ipv4(lower, upper); let runtime = network_scanner_net::runtime::Socket2Runtime::new(None)?; diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 24ae7286b..27334bb73 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -28,7 +28,7 @@ fn main() -> anyhow::Result<()> { max_wait_time: 10 * 1000, }, toggles: ScannerToggles { - disable_boardcast: false, + disable_broadcast: false, disable_subnet_scan: false, disable_ping_event: false, disable_resolve_dns: false, diff --git a/crates/network-scanner/src/ip_utils.rs b/crates/network-scanner/src/ip_utils.rs index b6ace8bc7..74290cfdc 100644 --- a/crates/network-scanner/src/ip_utils.rs +++ b/crates/network-scanner/src/ip_utils.rs @@ -19,7 +19,12 @@ impl TryFrom<&str> for IpAddrRange { } let lower = parts[0].parse::()?; let upper = parts[1].parse::()?; - IpAddrRange::new(lower, upper) + + match (lower, upper) { + (IpAddr::V4(lower), IpAddr::V4(upper)) => Ok(IpAddrRange::new_ipv4(lower, upper)), + (IpAddr::V6(lower), IpAddr::V6(upper)) => Ok(IpAddrRange::new_ipv6(lower, upper)), + _ => anyhow::bail!("IP address types do not match"), + } } } @@ -35,14 +40,38 @@ impl TryFrom<&String> for IpAddrRange { pub struct IpV4AddrRange { lower: Ipv4Addr, upper: Ipv4Addr, - current: Option, +} + +impl IntoIterator for IpV4AddrRange { + type Item = Ipv4Addr; + + type IntoIter = IpV4RangeIterator; + + fn into_iter(self) -> Self::IntoIter { + IpV4RangeIterator { + current: Some(self.lower), + upper: self.upper, + } + } } #[derive(Debug, Clone)] pub struct IpV6AddrRange { lower: Ipv6Addr, upper: Ipv6Addr, - current: Option, +} + +impl IntoIterator for IpV6AddrRange { + type Item = Ipv6Addr; + + type IntoIter = IpV6RangeIterator; + + fn into_iter(self) -> Self::IntoIter { + IpV6RangeIterator { + current: Some(self.lower), + upper: self.upper, + } + } } impl IpV4AddrRange { @@ -52,11 +81,7 @@ impl IpV4AddrRange { } else { (lower, upper) }; - Self { - lower, - upper, - current: Some(lower), - } + Self { lower, upper } } } @@ -67,16 +92,20 @@ impl IpV6AddrRange { } else { (lower, upper) }; - Self { - lower, - upper, - current: Some(lower), - } + Self { lower, upper } } } -// Implement Iterator for IPv4 range -impl Iterator for IpV4AddrRange { +pub struct IpV4RangeIterator { + current: Option, + upper: Ipv4Addr, +} +pub struct IpV6RangeIterator { + current: Option, + upper: Ipv6Addr, +} + +impl Iterator for IpV4RangeIterator { type Item = Ipv4Addr; fn next(&mut self) -> Option { @@ -90,8 +119,8 @@ impl Iterator for IpV4AddrRange { } } -// Implement Iterator for IPv6 range -impl Iterator for IpV6AddrRange { +// // Implement Iterator for IPv6 range +impl Iterator for IpV6RangeIterator { type Item = Ipv6Addr; fn next(&mut self) -> Option { @@ -107,12 +136,12 @@ impl Iterator for IpV6AddrRange { // Helper to create the appropriate enum variant impl IpAddrRange { - pub fn new(lower: IpAddr, upper: IpAddr) -> anyhow::Result { - match (lower, upper) { - (IpAddr::V4(l), IpAddr::V4(u)) => Ok(IpAddrRange::V4(IpV4AddrRange::new(l, u))), - (IpAddr::V6(l), IpAddr::V6(u)) => Ok(IpAddrRange::V6(IpV6AddrRange::new(l, u))), - _ => anyhow::bail!("IP range needs to be the same type (both IPv4 or both IPv6)"), - } + pub fn new_ipv4(lower: Ipv4Addr, upper: Ipv4Addr) -> Self { + IpAddrRange::V4(IpV4AddrRange::new(Ipv4Addr::from(upper), Ipv4Addr::from(lower))) + } + + pub fn new_ipv6(lower: Ipv6Addr, upper: Ipv6Addr) -> Self { + IpAddrRange::V6(IpV6AddrRange::new(Ipv6Addr::from(upper), Ipv6Addr::from(lower))) } pub fn has_overlap(&self, other: &Self) -> bool { @@ -135,16 +164,16 @@ impl IntoIterator for IpAddrRange { fn into_iter(self) -> Self::IntoIter { match self { - IpAddrRange::V4(range) => IpAddrRangeIter::V4(range), - IpAddrRange::V6(range) => IpAddrRangeIter::V6(range), + IpAddrRange::V4(range) => IpAddrRangeIter::V4(range.into_iter()), + IpAddrRange::V6(range) => IpAddrRangeIter::V6(range.into_iter()), } } } // Enum for the iterator pub enum IpAddrRangeIter { - V4(IpV4AddrRange), - V6(IpV6AddrRange), + V4(IpV4RangeIterator), + V6(IpV6RangeIterator), } impl Iterator for IpAddrRangeIter { @@ -194,15 +223,14 @@ pub struct Subnet { impl From for IpAddrRange { fn from(value: Subnet) -> Self { - let (lower, upper) = calculate_subnet_bounds(value.ip, value.netmask); - IpAddrRange::new(lower.into(), upper.into()).expect("Subnet bounds must be valid IPv4 addresses") + Self::from(&value) } } impl From<&Subnet> for IpAddrRange { fn from(value: &Subnet) -> Self { let (lower, upper) = calculate_subnet_bounds(value.ip, value.netmask); - IpAddrRange::new(lower.into(), upper.into()).expect("Subnet bounds must be valid IPv4 addresses") + IpAddrRange::new_ipv4(lower.into(), upper.into()) } } @@ -214,7 +242,7 @@ impl TryFrom for IpAddrRange { let V4IfAddr { ip, netmask, .. } = value; let netmask = netmask.ok_or_else(|| anyhow::anyhow!("No netmask found"))?; let (lower, upper) = calculate_subnet_bounds(ip, netmask); - IpAddrRange::new(lower.into(), upper.into()) + Ok(IpAddrRange::new_ipv4(lower.into(), upper.into())) } } @@ -273,7 +301,7 @@ mod tests { fn test_iter_ipv4() { let lower = "10.10.0.0".parse::().unwrap(); let upper = "10.10.0.30".parse::().unwrap(); - let range = IpAddrRange::new(lower.into(), upper.into()).unwrap(); + let range = IpAddrRange::new_ipv4(lower.into(), upper.into()); let mut iter = range.into_iter(); for i in 0..31 { @@ -285,8 +313,8 @@ mod tests { #[test] fn test_has_overlap() { - let r1 = IpAddrRange::new("192.168.1.0".parse().unwrap(), "192.168.1.255".parse().unwrap()).unwrap(); - let r2 = IpAddrRange::new("192.168.1.100".parse().unwrap(), "192.168.2.10".parse().unwrap()).unwrap(); + let r1 = IpAddrRange::new_ipv4("192.168.1.0".parse().unwrap(), "192.168.1.255".parse().unwrap()); + let r2 = IpAddrRange::new_ipv4("192.168.1.100".parse().unwrap(), "192.168.2.10".parse().unwrap()); assert!(r1.has_overlap(&r2)); } diff --git a/crates/network-scanner/src/mdns.rs b/crates/network-scanner/src/mdns.rs index d3472835f..20f114d29 100644 --- a/crates/network-scanner/src/mdns.rs +++ b/crates/network-scanner/src/mdns.rs @@ -91,7 +91,7 @@ pub fn mdns_query_scan( if let Err(e) = service_daemon_clone.stop_browse(service_name_clone.as_ref()) { warn!(error = %e, "Failed to stop browsing for service"); } - // Receive the last event (StopBrowse), preventing the receiver from being dropped,this will satisfy the sender side to avoid loging an error + // Receive the last event (StopBrowse), preventing the receiver from being dropped,this will satisfy the sender side to avoid logging an error let _ = receiver_clone.recv_timeout(std::time::Duration::from_millis(10)); }) .spawn(move |_| async move { diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 151c50cf2..65edec2a6 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -33,8 +33,8 @@ impl NetworkScanner { start_port_scan(&mut task_executor); - if !self.toggles.disable_boardcast { - start_boardcast(&mut task_executor); + if !self.toggles.disable_broadcast { + start_broadcast(&mut task_executor); } start_netbios(&mut task_executor); @@ -167,7 +167,7 @@ impl NetworkScanner { ); } - fn start_boardcast(task_executor: &mut TaskExecutionRunner) { + fn start_broadcast(task_executor: &mut TaskExecutionRunner) { task_executor.run( move |TaskExecutionContext { runtime, @@ -176,14 +176,14 @@ impl NetworkScanner { .. }: TaskExecutionContext, task_manager| async move { - let boardcast_subnet = configs.boardcast_subnet; - let boardcast_timeout = configs.broadcast_timeout; - for subnet in boardcast_subnet { + let broadcast_subnet = configs.broadcast_subnet; + let broadcast_timeout = configs.broadcast_timeout; + for subnet in broadcast_subnet { debug!(broadcasting_to_subnet = ?subnet); let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); task_manager.spawn(move |task_manager: TaskManager| async move { let mut receiver = - broadcast(subnet.broadcast, boardcast_timeout, runtime, task_manager).await?; + broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; while let Some(ip) = receiver.recv().await { trace!(broadcast_sent_ip = ?ip); ip_sender.send((ip.into(), None)).await?; @@ -207,7 +207,7 @@ impl NetworkScanner { task_manager| async move { let netbios_timeout = configs.netbios_timeout; let netbios_interval = configs.netbios_interval; - let subnets = configs.boardcast_subnet; + let subnets = configs.broadcast_subnet; let ip_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); debug!(netbios_query_ip_ranges = ?ip_ranges); @@ -309,8 +309,8 @@ impl NetworkScanner { .. }, task_manager| async move { - // Since mDNS deamon is started at the point it's created, we set it to None in order to avoid resouce waste - // Caller of the start_mdns function should garentee that the deamon exists + // Since mDNS daemon is started at the point it's created, we set it to None in order to avoid resource waste + // Caller of the start_mdns function should guarantee that the daemon exists let mdns_daemon = match mdns_daemon { Some(daemon) => daemon, None => anyhow::bail!("mDNS daemon is not available but mDNS is enabled"), @@ -413,8 +413,8 @@ impl NetworkScannerStream { pub fn stop(self: Arc) { self.task_manager.stop(); - if let Some(deamon) = &self.mdns_daemon { - deamon.stop(); + if let Some(daemon) = &self.mdns_daemon { + daemon.stop(); }; } } @@ -473,7 +473,7 @@ impl Display for ScanMethod { #[derive(Debug, Clone)] pub struct ScannerToggles { pub disable_ping_event: bool, - pub disable_boardcast: bool, + pub disable_broadcast: bool, pub disable_subnet_scan: bool, pub disable_zeroconf: bool, pub disable_resolve_dns: bool, diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 7804e03c4..02c384884 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -19,7 +19,7 @@ pub(crate) type ScanEntryReceiver = tokio::sync::mpsc::Receiver; #[derive(Debug, Clone)] pub(crate) struct ContextConfig { - pub(crate) boardcast_subnet: Vec, // The subnet that have a broadcast address + pub(crate) broadcast_subnet: Vec, // The subnet that have a broadcast address pub(crate) range_to_ping: Vec, pub ports: Vec, pub ping_interval: Duration, @@ -39,7 +39,7 @@ impl ContextConfig { }; Self { - boardcast_subnet: subnet, + broadcast_subnet: subnet, range_to_ping, ports: configs.ports, ping_interval: Duration::from_millis(configs.ping_interval), @@ -94,7 +94,7 @@ impl TaskExecutionContext { .. } = network_scanner; - let boardcast_subnet = get_subnets()?; + let broadcast_subnet = get_subnets()?; let res = Self { ip_sender, ip_receiver, @@ -103,7 +103,7 @@ impl TaskExecutionContext { ip_cache: Arc::new(parking_lot::RwLock::new(HashMap::new())), runtime, mdns_daemon, - configs: ContextConfig::new(configs, &toggles, boardcast_subnet), + configs: ContextConfig::new(configs, &toggles, broadcast_subnet), toggles, }; From f0abb6726d81407a6daa30eacb3c43d0bb9f4854 Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 15:12:41 -0400 Subject: [PATCH 06/33] fmt typo and review fixes --- devolutions-gateway/src/api/net.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 972c630ab..21e142ea4 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -202,7 +202,7 @@ impl TryFrom for NetworkScannerParams { }, toggles: scanner::ScannerToggles { disable_ping_event: val.disable_ping_events, - disable_boardcast: val.disable_boardcast, + disable_broadcast: val.disable_boardcast, disable_subnet_scan: val.disable_subnet_scan, disable_zeroconf: val.disable_zeroconf, disable_resolve_dns: val.disable_resolve_dns, From c61c247ea7c2aa513be1a1566e5ac1d545e9b7af Mon Sep 17 00:00:00 2001 From: "irvingouj@Devolutions" Date: Thu, 10 Apr 2025 14:45:03 -0400 Subject: [PATCH 07/33] Update crates/network-scanner/src/port_discovery.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Benoît Cortier <3809077+CBenoit@users.noreply.github.com> --- crates/network-scanner/src/port_discovery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/network-scanner/src/port_discovery.rs b/crates/network-scanner/src/port_discovery.rs index e77d59d18..32c010205 100644 --- a/crates/network-scanner/src/port_discovery.rs +++ b/crates/network-scanner/src/port_discovery.rs @@ -24,7 +24,7 @@ pub async fn scan_ports( } if port.is_empty() { - anyhow::bail!("No ports to scan"); + anyhow::bail!("no ports to scan"); } let (sender, receiver) = tokio::sync::mpsc::channel(port.len()); From e2c847de41c364ef5cdca75aedb717b78fc206fa Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 15:26:54 -0400 Subject: [PATCH 08/33] fix unnecessary Arc Mutex on stream --- crates/network-scanner/examples/scan.rs | 18 +++++------ crates/network-scanner/src/scanner.rs | 39 ++++++++++++------------ crates/network-scanner/src/task_utils.rs | 25 +++++++-------- devolutions-gateway/src/api/net.rs | 2 +- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 27334bb73..812ffc33e 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -38,25 +38,21 @@ fn main() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async move { let scanner = NetworkScanner::new(params).unwrap(); - let stream = scanner.start()?; - let stream_clone = stream.clone(); + let mut stream = scanner.start()?; let now = std::time::Instant::now(); tokio::task::spawn(async move { if tokio::signal::ctrl_c().await.is_ok() { tracing::info!("Ctrl-C received, stopping network scan"); - stream.stop(); } }); - while let Ok(Some(res)) = timeout(Duration::from_secs(120), stream_clone.recv()) - .await - .with_context(|| { - tracing::error!("Failed to receive from stream"); - "Failed to receive from stream" - }) - { + while let Ok(Some(res)) = timeout(Duration::from_secs(120), stream.recv()).await.with_context(|| { + tracing::error!("Failed to receive from stream"); + "Failed to receive from stream" + }) { tracing::warn!("Result: {:?}", res); } - stream_clone.stop(); + + stream.stop(); tracing::warn!("Network Scan finished. elapsed: {:?}", now.elapsed()); anyhow::Result::<()>::Ok(()) })?; diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 65edec2a6..4201e8623 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -10,7 +10,6 @@ use std::fmt::Display; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; -use tokio::sync::Mutex; use typed_builder::TypedBuilder; /// Represents a network scanner for discovering devices and their services over a network. @@ -28,8 +27,8 @@ pub struct NetworkScanner { } impl NetworkScanner { - pub fn start(&self) -> anyhow::Result> { - let mut task_executor = TaskExecutionRunner::new(self.clone())?; + pub fn start(&self) -> anyhow::Result { + let (mut task_executor, result_receiver) = TaskExecutionRunner::new(self.clone())?; start_port_scan(&mut task_executor); @@ -46,27 +45,27 @@ impl NetworkScanner { } let TaskExecutionRunner { - context: - TaskExecutionContext { - result_receiver: port_receiver, - mdns_daemon, - .. - }, + context: TaskExecutionContext { mdns_daemon, .. }, task_manager, } = task_executor; - let scanner_stream = Arc::new(NetworkScannerStream { - result_receiver: port_receiver, + let task_manager_clone = task_manager.clone(); + let mdns_daemon_clone = mdns_daemon.clone(); + + let scanner_stream = NetworkScannerStream { + result_receiver, task_manager, mdns_daemon, - }); + }; - let scanner_stream_clone = Arc::clone(&scanner_stream); let max_wait_time = Duration::from_millis(self.configs.max_wait_time); tokio::spawn(async move { tokio::time::sleep(max_wait_time).await; - scanner_stream_clone.stop(); + task_manager_clone.stop(); + if let Some(daemon) = &mdns_daemon_clone { + daemon.stop(); + } }); return Ok(scanner_stream); @@ -394,24 +393,24 @@ pub enum ScanEntry { } pub struct NetworkScannerStream { - result_receiver: Arc>, + result_receiver: ScanEntryReceiver, task_manager: TaskManager, mdns_daemon: Option, } impl NetworkScannerStream { - pub async fn recv(self: &Arc) -> Option { + pub async fn recv(self: &mut Self) -> Option { // The caller sometimes require Send, hence the Arc is necessary for socket_addr_receiver. - self.result_receiver.lock().await.recv().await + self.result_receiver.recv().await } - pub async fn recv_timeout(self: &Arc, duration: Duration) -> anyhow::Result> { - tokio::time::timeout(duration, self.result_receiver.lock().await.recv()) + pub async fn recv_timeout(self: &mut Self, duration: Duration) -> anyhow::Result> { + tokio::time::timeout(duration, self.result_receiver.recv()) .await .context("recv_timeout timed out") } - pub fn stop(self: Arc) { + pub fn stop(self: &Self) { self.task_manager.stop(); if let Some(daemon) = &self.mdns_daemon { daemon.stop(); diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 02c384884..fcb0ca511 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -59,7 +59,6 @@ pub(crate) struct TaskExecutionContext { pub(crate) ip_receiver: Arc>, pub(crate) result_sender: ScanEntrySender, - pub(crate) result_receiver: Arc>, pub(crate) ip_cache: Arc>>>, @@ -79,12 +78,11 @@ pub(crate) struct TaskExecutionRunner { } impl TaskExecutionContext { - pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result { + pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result<(Self, ScanEntryReceiver)> { let (ip_sender, ip_receiver) = tokio::sync::mpsc::channel(5); let ip_receiver = Arc::new(Mutex::new(ip_receiver)); - let (port_sender, port_receiver) = tokio::sync::mpsc::channel(100); - let port_receiver = Arc::new(Mutex::new(port_receiver)); + let (result_sender, result_receiver) = tokio::sync::mpsc::channel(100); let NetworkScanner { mdns_daemon, @@ -98,8 +96,7 @@ impl TaskExecutionContext { let res = Self { ip_sender, ip_receiver, - result_sender: port_sender, - result_receiver: port_receiver, + result_sender, ip_cache: Arc::new(parking_lot::RwLock::new(HashMap::new())), runtime, mdns_daemon, @@ -107,7 +104,7 @@ impl TaskExecutionContext { toggles, }; - Ok(res) + Ok((res, result_receiver)) } } @@ -122,11 +119,15 @@ impl TaskExecutionRunner { .spawn_no_sub_task(task(context, self.task_manager.clone())); } - pub(crate) fn new(scanner: NetworkScanner) -> anyhow::Result { - Ok(Self { - context: TaskExecutionContext::new(scanner)?, - task_manager: TaskManager::new(), - }) + pub(crate) fn new(scanner: NetworkScanner) -> anyhow::Result<(Self, ScanEntryReceiver)> { + let (context, receiver) = TaskExecutionContext::new(scanner)?; + Ok(( + Self { + context, + task_manager: TaskManager::new(), + }, + receiver, + )) } } diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 21e142ea4..2d820bb48 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -50,7 +50,7 @@ pub async fn handle_network_scan( })?; let res = ws.on_upgrade(move |mut websocket| async move { - let stream = match scanner.start() { + let mut stream = match scanner.start() { Ok(stream) => stream, Err(e) => { error!(error = format!("{e:#}"), "Failed to start network scan"); From f0f58c7e584c2d75889352fe6f26306cfc34355e Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 16:11:50 -0400 Subject: [PATCH 09/33] fix ping query params --- crates/network-scanner/examples/scan.rs | 2 +- crates/network-scanner/src/scanner.rs | 15 +++++---------- devolutions-gateway/src/api/net.rs | 6 +++--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 812ffc33e..8f3d5f1bc 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -30,7 +30,7 @@ fn main() -> anyhow::Result<()> { toggles: ScannerToggles { disable_broadcast: false, disable_subnet_scan: false, - disable_ping_event: false, + disable_ping_start: false, disable_resolve_dns: false, disable_zeroconf: false, }, diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 4201e8623..155a70c3c 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -249,7 +249,7 @@ impl NetworkScanner { let ping_interval = configs.ping_interval; let ping_timeout = configs.ping_timeout; let range_to_ping = configs.range_to_ping; - let disable_ping_event = toggles.disable_ping_event; + let disable_ping_start = toggles.disable_ping_start; for ip_range in range_to_ping { let (task_manager, runtime, ip_sender) = @@ -266,21 +266,15 @@ impl NetworkScanner { while let Some(ping_event) = receiver.recv().await { debug!(ping_sent_ip = ?ping_event); - if let crate::ping::PingEvent::Success { ip_addr, .. } = ping_event { - ip_sender.send((ip_addr, None)).await?; - }; - - if disable_ping_event { - continue; - } match ping_event { crate::ping::PingEvent::Success { ip_addr, time } => { + ip_sender.send((ip_addr, None)).await?; result_sender .send(ScanEntry::ScanEvent(ScanEvent::PingSuccess { ip_addr, time })) .await?; } - crate::ping::PingEvent::Start { ip_addr } => { + crate::ping::PingEvent::Start { ip_addr } if !disable_ping_start => { result_sender .send(ScanEntry::ScanEvent(ScanEvent::PingStart { ip_addr })) .await?; @@ -290,6 +284,7 @@ impl NetworkScanner { .send(ScanEntry::ScanEvent(ScanEvent::PingFailed { ip_addr, reason })) .await?; } + _ => {} } } } @@ -471,7 +466,7 @@ impl Display for ScanMethod { #[derive(Debug, Clone)] pub struct ScannerToggles { - pub disable_ping_event: bool, + pub disable_ping_start: bool, pub disable_broadcast: bool, pub disable_subnet_scan: bool, pub disable_zeroconf: bool, diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 2d820bb48..017d036ab 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -146,9 +146,9 @@ pub struct NetworkScanQueryParams { #[serde(default)] pub ports: Vec, - /// Disable the emission of ScanEvent::Ping + /// Disable the emission of ScanEvent::Ping for status start #[serde(default = "default_true")] - pub disable_ping_events: bool, + pub disable_ping_start: bool, /// Disable the execution of broadcast scan #[serde(default)] @@ -201,7 +201,7 @@ impl TryFrom for NetworkScannerParams { .collect::, anyhow::Error>>()?, }, toggles: scanner::ScannerToggles { - disable_ping_event: val.disable_ping_events, + disable_ping_start: val.disable_ping_start, disable_broadcast: val.disable_boardcast, disable_subnet_scan: val.disable_subnet_scan, disable_zeroconf: val.disable_zeroconf, From 7dc2a6e75742662ce19e169878a79d63758d8d24 Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 16:19:14 -0400 Subject: [PATCH 10/33] rebase master --- devolutions-gateway/src/api/net.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 017d036ab..9c095735a 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -6,7 +6,7 @@ use axum::extract::{RawQuery, WebSocketUpgrade}; use axum::response::Response; use axum::{Json, Router}; use network_scanner::interfaces; -use network_scanner::scanner::{self, NetworkScannerParams}; +use network_scanner::scanner::{self, NetworkScannerParams, ScannerConfig}; use network_scanner::ip_utils::IpAddrRange; use serde::Serialize; use std::fmt; From dc444259f0148acaf2d5dc69a11dac68c79d778a Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 16:30:02 -0400 Subject: [PATCH 11/33] fmt --- devolutions-gateway/src/api/net.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 9c095735a..6a55f10d6 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -6,8 +6,8 @@ use axum::extract::{RawQuery, WebSocketUpgrade}; use axum::response::Response; use axum::{Json, Router}; use network_scanner::interfaces; -use network_scanner::scanner::{self, NetworkScannerParams, ScannerConfig}; use network_scanner::ip_utils::IpAddrRange; +use network_scanner::scanner::{self, NetworkScannerParams, ScannerConfig}; use serde::Serialize; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; From 4fda2b08a335c60ea83adea7a28a6ee788cdb3c9 Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 10 Apr 2025 17:01:04 -0400 Subject: [PATCH 12/33] review fix --- Cargo.lock | 91 +++++++++++++++++++++++++++++- devolutions-gateway/Cargo.toml | 2 +- devolutions-gateway/src/api/net.rs | 13 +++-- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74513a356..52666dd6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1221,8 +1221,8 @@ dependencies = [ "rstest", "rustls-cng", "serde", + "serde-querystring", "serde_json", - "serde_qs", "serde_urlencoded", "smol_str", "sysinfo", @@ -3005,6 +3005,79 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lexical" +version = "7.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ed980ff02623721dc334b9105150b66d0e1f246a92ab5a2eca0335d54c48f6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.171" @@ -5296,6 +5369,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-querystring" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae1940bc2612f641456fc715125e4002cbd235d040188a1994e64b734054c2e" +dependencies = [ + "lexical", + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.17" @@ -5604,6 +5687,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.5" diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index a3296e525..68fc22d00 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -109,7 +109,7 @@ etherparse = "0.15" # For KDC proxy portpicker = "0.1" -serde_qs = "0.14.0" +serde-querystring = "0.3.0" [target.'cfg(windows)'.dependencies] rustls-cng = { version = "0.5", default-features = false, features = ["logging", "tls12", "ring"] } diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 6a55f10d6..31daa53e1 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -9,6 +9,7 @@ use network_scanner::interfaces; use network_scanner::ip_utils::IpAddrRange; use network_scanner::scanner::{self, NetworkScannerParams, ScannerConfig}; use serde::Serialize; +use serde_querystring::{from_str, ParseMode}; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -34,7 +35,7 @@ pub async fn handle_network_scan( // Use serde_qs to parse array parameters // As suggested by serde_urlencoded author https://github.com/nox/serde_urlencoded/issues/85 - let query_params = match serde_qs::from_str::(&query) { + let query_params = match from_str::(&query, ParseMode::Duplicate) { Ok(params) => Ok(params), Err(e) => Err(HttpError::bad_request().build(e)), }?; @@ -141,10 +142,10 @@ pub struct NetworkScanQueryParams { /// The start and end IP address of the range to scan. /// for example: 10.10.0.0-10.10.0.255 #[serde(default)] - pub ranges: Vec, + pub range: Vec, /// The ports to scan. If not specified, the default ports will be used. #[serde(default)] - pub ports: Vec, + pub port: Vec, /// Disable the emission of ScanEvent::Ping for status start #[serde(default = "default_true")] @@ -178,9 +179,9 @@ impl TryFrom for NetworkScannerParams { fn try_from(val: NetworkScanQueryParams) -> Result { warn!(query=?val, "Network scan query parameters"); - let ports = match val.ports.len() { + let ports = match val.port.len() { 0 => COMMON_PORTS.to_vec(), - _ => val.ports, + _ => val.port, }; Ok(NetworkScannerParams { @@ -195,7 +196,7 @@ impl TryFrom for NetworkScannerParams { netbios_interval: val.netbios_interval.unwrap_or(200), mdns_query_timeout: val.mdns_query_timeout.unwrap_or(5 * 1000), // in milliseconds ip_ranges: val - .ranges + .range .iter() .map(IpAddrRange::try_from) .collect::, anyhow::Error>>()?, From 02958f8b87ac2c27d30b7941668ff8112af90e5c Mon Sep 17 00:00:00 2001 From: irving ou Date: Fri, 11 Apr 2025 13:11:17 -0400 Subject: [PATCH 13/33] review fix --- crates/network-scanner/examples/scan.rs | 30 ++-- crates/network-scanner/src/netbios.rs | 6 +- crates/network-scanner/src/scanner.rs | 192 ++++++++++++++--------- crates/network-scanner/src/task_utils.rs | 59 ++++--- devolutions-gateway/src/api/net.rs | 110 ++++++------- devolutions-gateway/src/extract.rs | 25 +++ 6 files changed, 253 insertions(+), 169 deletions(-) diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 8f3d5f1bc..681320e60 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -15,24 +15,24 @@ fn main() -> anyhow::Result<()> { .init(); let params = NetworkScannerParams { - configs: ScannerConfig { + config: ScannerConfig { ip_ranges: vec![], ports: vec![22, 80, 443, 389, 636], - ping_interval: 20, - ping_timeout: 1000, - broadcast_timeout: 2000, - port_scan_timeout: 2000, - netbios_timeout: 1000, - netbios_interval: 20, - mdns_query_timeout: 5 * 1000, - max_wait_time: 10 * 1000, + ping_interval: Duration::from_millis(20), + ping_timeout: Duration::from_millis(1000), + broadcast_timeout: Duration::from_millis(2000), + port_scan_timeout: Duration::from_millis(2000), + netbios_timeout: Duration::from_millis(1000), + netbios_interval: Duration::from_millis(20), + mdns_query_timeout: Duration::from_millis(5 * 1000), + max_wait_time: Duration::from_millis(10 * 1000), }, - toggles: ScannerToggles { - disable_broadcast: false, - disable_subnet_scan: false, - disable_ping_start: false, - disable_resolve_dns: false, - disable_zeroconf: false, + toggle: ScannerToggles { + enable_broadcast: true, + enable_ping_start: true, + enable_resolve_dns: true, + enable_subnet_scan: true, + enable_zeroconf: true, }, }; let rt = tokio::runtime::Runtime::new()?; diff --git a/crates/network-scanner/src/netbios.rs b/crates/network-scanner/src/netbios.rs index f3e6fa0b8..000fdcc90 100644 --- a/crates/network-scanner/src/netbios.rs +++ b/crates/network-scanner/src/netbios.rs @@ -8,7 +8,7 @@ use network_scanner_proto::netbios::NetBiosPacket; use socket2::{Domain, SockAddr, Type}; use crate::ip_utils::IpV4AddrRange; -use crate::task_utils::IpReceiver; +use crate::task_utils::IpHostReceiver; use crate::{assume_init, ScannerError}; const MESSAGE: [u8; 50] = [ @@ -24,7 +24,7 @@ pub fn netbios_query_scan( single_query_duration: std::time::Duration, netbios_scan_interval: std::time::Duration, task_manager: crate::task_utils::TaskManager, -) -> Result { +) -> Result { let (sender, receiver) = tokio::sync::mpsc::channel(255); task_manager.spawn(move |task_manager: crate::task_utils::TaskManager| async move { for ip in ip_range.into_iter() { @@ -42,7 +42,7 @@ pub fn netbios_query_scan( pub(crate) fn netbios_query_one( ip: Ipv4Addr, mut socket: AsyncRawSocket, - result_sender: crate::task_utils::IpSender, + result_sender: crate::task_utils::IpHostSender, duration: std::time::Duration, task_manager: crate::task_utils::TaskManager, ) { diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 155a70c3c..b63bfb3ca 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -31,8 +31,9 @@ impl NetworkScanner { let (mut task_executor, result_receiver) = TaskExecutionRunner::new(self.clone())?; start_port_scan(&mut task_executor); + start_dns_look_up(&mut task_executor); - if !self.toggles.disable_broadcast { + if self.toggles.enable_broadcast { start_broadcast(&mut task_executor); } @@ -40,7 +41,7 @@ impl NetworkScanner { start_ping(&mut task_executor); - if !self.toggles.disable_zeroconf { + if self.toggles.enable_zeroconf { start_mdns(&mut task_executor); } @@ -58,7 +59,7 @@ impl NetworkScanner { mdns_daemon, }; - let max_wait_time = Duration::from_millis(self.configs.max_wait_time); + let max_wait_time = self.configs.max_wait_time; tokio::spawn(async move { tokio::time::sleep(max_wait_time).await; @@ -70,64 +71,100 @@ impl NetworkScanner { return Ok(scanner_stream); + fn start_dns_look_up(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + ip_cache, + ip_sender, + result_sender, + toggles, + .. + }: TaskExecutionContext, + task_executor| async move { + let enable_dns_resolve = toggles.enable_resolve_dns; + let mut ip_receiver = ip_sender.subscribe(); + + while let Ok((ip, host)) = ip_receiver.recv().await { + let ip_cache = Arc::clone(&ip_cache); + let result_sender = result_sender.clone(); + let existing_dns = { + let binding = ip_cache.read(); + binding.get(&ip).cloned() + }; + // Write first, to aviod new incoming same IP address, + // The host will be updated later to the correct one anyway + // Put in it's now scope to avoid holding the lock for too long + { + ip_cache.write().insert(ip, host.clone()); + } + + // Spawn a new task for each IP address + task_executor.spawn_no_sub_task(async move { + let (update_dns, ip, dns) = match existing_dns { + Some(None) if host.is_some() => { + // If the IP address is in the cache but DNS resolution was not successful before and we have a new host coming in, update + (true, ip, host) + } + None if enable_dns_resolve => { + // If the IP address is not in the cache, resolve the DNS + let resolve_dns = tokio::task::spawn_blocking(move || { + dns_lookup::lookup_addr(&ip).context("Failed to resolve DNS").ok() + }) + .await + .context("Failed to spawn blocking task")?; + + let one_or_the_other = resolve_dns.or(host); + (true, ip, one_or_the_other) + } + None => { + // If the IP address is not in the cache, just update + (true, ip, host) + } + _ => { + // If the IP address is already in the cache, do nothing + // Already exists DNS/ or DNS not exists but new host is None as well + (false, ip, host) + } + }; + + if update_dns { + if let Some(dns) = &dns { + result_sender + .send(ScanEntry::ScanEvent(ScanEvent::Dns { + ip_addr: ip, + hostname: dns.to_owned(), + })) + .await?; + } + ip_cache.write().insert(ip, dns); + } + + anyhow::Ok(()) + }); + } + + anyhow::Ok(()) + }, + ); + } + fn start_port_scan(task_executor: &mut TaskExecutionRunner) { task_executor.run( move |TaskExecutionContext { ip_cache, - ip_receiver, runtime, result_sender, + ip_sender, configs, - toggles, .. }: TaskExecutionContext, task_manager| async move { let ip_cache = Arc::clone(&ip_cache); let ports = configs.ports.clone(); - let disable_dns_resolve = toggles.disable_resolve_dns; - - while let Some((ip, host)) = ip_receiver.lock().await.recv().await { - // ==========================Check cache and dns resolve ============================== - // The host is optional, it can be sent from a netbios query or a mDNS query that have hostname - // Or if can be None if it's from a ping or broadcast scan - - // Check if the IP is already in the cache (if there is, that means we had it scanned before) - let is_new = ip_cache.read().get(&ip).is_none(); - let updated_dns = if is_new { - let resolve_dns = if !disable_dns_resolve { - tokio::task::spawn_blocking(move || dns_lookup::lookup_addr(&ip)) - .await? - .ok() - } else { - None - }; - let final_dns = resolve_dns.or(host); - ip_cache.write().insert(ip, final_dns.clone()); - final_dns - } else if host.is_some() { - // If the host is not None, we should update the cache with the new hostname - ip_cache.write().insert(ip, host.clone()); - host - } else { - None - }; - - if let Some(dns) = updated_dns { - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::Dns { - ip_addr: ip, - hostname: dns, - })) - .await?; - } - - if !is_new { - continue; - } - - // ======================end of check cache and dns resolve ========================= + let mut ip_receiver = ip_sender.subscribe(); + while let Ok((ip, _)) = ip_receiver.recv().await { let (runtime, ports, result_sender, ip_cache, port_scan_timeout) = ( Arc::clone(&runtime), ports.clone(), @@ -185,7 +222,7 @@ impl NetworkScanner { broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; while let Some(ip) = receiver.recv().await { trace!(broadcast_sent_ip = ?ip); - ip_sender.send((ip.into(), None)).await?; + ip_sender.send((ip.into(), None))?; } anyhow::Ok(()) }); @@ -208,14 +245,14 @@ impl NetworkScanner { let netbios_interval = configs.netbios_interval; let subnets = configs.broadcast_subnet; - let ip_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); - debug!(netbios_query_ip_ranges = ?ip_ranges); + let subnet_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); + debug!(netbios_query_ip_ranges = ?subnet_ranges); - for ip_range in ip_ranges { + for subnet_range in subnet_ranges { let (runtime, ip_sender, task_manager) = (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); - let IpAddrRange::V4(ip_range) = ip_range else { + let IpAddrRange::V4(ip_range) = subnet_range else { continue; }; @@ -224,7 +261,7 @@ impl NetworkScanner { while let Some(res) = receiver.recv().await { debug!(netbios_query_sent_ip = ?res.0); - ip_sender.send(res).await?; + ip_sender.send(res)?; } } anyhow::Ok(()) @@ -249,7 +286,7 @@ impl NetworkScanner { let ping_interval = configs.ping_interval; let ping_timeout = configs.ping_timeout; let range_to_ping = configs.range_to_ping; - let disable_ping_start = toggles.disable_ping_start; + let enable_ping_start = toggles.enable_ping_start; for ip_range in range_to_ping { let (task_manager, runtime, ip_sender) = @@ -269,12 +306,12 @@ impl NetworkScanner { match ping_event { crate::ping::PingEvent::Success { ip_addr, time } => { - ip_sender.send((ip_addr, None)).await?; + ip_sender.send((ip_addr, None))?; result_sender .send(ScanEntry::ScanEvent(ScanEvent::PingSuccess { ip_addr, time })) .await?; } - crate::ping::PingEvent::Start { ip_addr } if !disable_ping_start => { + crate::ping::PingEvent::Start { ip_addr } if enable_ping_start => { result_sender .send(ScanEntry::ScanEvent(ScanEvent::PingStart { ip_addr })) .await?; @@ -346,13 +383,18 @@ impl NetworkScanner { } } - pub fn new(NetworkScannerParams { configs, toggles }: NetworkScannerParams) -> anyhow::Result { + pub fn new( + NetworkScannerParams { + config: configs, + toggle: toggles, + }: NetworkScannerParams, + ) -> anyhow::Result { let runtime = network_scanner_net::runtime::Socket2Runtime::new(None)?; - let mdns_daemon = if toggles.disable_zeroconf { - None - } else { + let mdns_daemon = if toggles.enable_zeroconf { Some(MdnsDaemon::new()?) + } else { + None }; Ok(Self { @@ -466,32 +508,32 @@ impl Display for ScanMethod { #[derive(Debug, Clone)] pub struct ScannerToggles { - pub disable_ping_start: bool, - pub disable_broadcast: bool, - pub disable_subnet_scan: bool, - pub disable_zeroconf: bool, - pub disable_resolve_dns: bool, + pub enable_ping_start: bool, + pub enable_broadcast: bool, + pub enable_subnet_scan: bool, + pub enable_zeroconf: bool, + pub enable_resolve_dns: bool, } #[derive(Debug, Clone)] pub struct ScannerConfig { pub ports: Vec, - pub ping_interval: u64, - pub ping_timeout: u64, - pub broadcast_timeout: u64, - pub port_scan_timeout: u64, - pub netbios_timeout: u64, - pub netbios_interval: u64, - pub mdns_query_timeout: u64, - pub max_wait_time: u64, + pub ping_interval: Duration, + pub ping_timeout: Duration, + pub broadcast_timeout: Duration, + pub port_scan_timeout: Duration, + pub netbios_timeout: Duration, + pub netbios_interval: Duration, + pub mdns_query_timeout: Duration, + pub max_wait_time: Duration, pub ip_ranges: Vec, } /// The parameters for configuring a network scanner. All fields are in milliseconds. #[derive(Debug, Clone, TypedBuilder)] pub struct NetworkScannerParams { - pub configs: ScannerConfig, - pub toggles: ScannerToggles, + pub config: ScannerConfig, + pub toggle: ScannerToggles, } #[derive(Debug, Clone, Copy)] diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index fcb0ca511..899d33f67 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -6,14 +6,15 @@ use std::time::Duration; use std::future::Future; -use tokio::sync::Mutex; - use crate::ip_utils::{get_subnets, IpAddrRange, Subnet}; use crate::mdns::MdnsDaemon; use crate::scanner::{NetworkScanner, ScanEntry, ScannerConfig, ScannerToggles}; -pub(crate) type IpSender = tokio::sync::mpsc::Sender<(IpAddr, Option)>; -pub(crate) type IpReceiver = tokio::sync::mpsc::Receiver<(IpAddr, Option)>; +pub(crate) type BoardcastSender = tokio::sync::broadcast::Sender<(IpAddr, Option)>; + +pub(crate) type IpHostSender = tokio::sync::mpsc::Sender<(IpAddr, Option)>; +pub(crate) type IpHostReceiver = tokio::sync::mpsc::Receiver<(IpAddr, Option)>; + pub(crate) type ScanEntrySender = tokio::sync::mpsc::Sender; pub(crate) type ScanEntryReceiver = tokio::sync::mpsc::Receiver; @@ -32,32 +33,48 @@ pub(crate) struct ContextConfig { } impl ContextConfig { - pub(crate) fn new(configs: ScannerConfig, toggles: &ScannerToggles, subnet: Vec) -> Self { - let range_to_ping = match configs.ip_ranges.len() { - 0 if !toggles.disable_subnet_scan => subnet.iter().map(IpAddrRange::from).collect::>(), - _ => configs.ip_ranges.clone(), + pub(crate) fn new( + ScannerConfig { + broadcast_timeout, + mdns_query_timeout, + netbios_timeout, + netbios_interval, + ping_timeout, + ping_interval, + port_scan_timeout, + ports, + ip_ranges, + .. + }: ScannerConfig, + toggles: &ScannerToggles, + subnet: Vec, + ) -> Self { + let range_to_ping = match ip_ranges.len() { + 0 if toggles.enable_subnet_scan => subnet.iter().map(IpAddrRange::from).collect::>(), + _ => ip_ranges.clone(), }; Self { broadcast_subnet: subnet, range_to_ping, - ports: configs.ports, - ping_interval: Duration::from_millis(configs.ping_interval), - ping_timeout: Duration::from_millis(configs.ping_timeout), - broadcast_timeout: Duration::from_millis(configs.broadcast_timeout), - port_scan_timeout: Duration::from_millis(configs.port_scan_timeout), - netbios_timeout: Duration::from_millis(configs.netbios_timeout), - netbios_interval: Duration::from_millis(configs.netbios_interval), - mdns_query_timeout: Duration::from_millis(configs.mdns_query_timeout), + ports, + ping_interval, + ping_timeout, + broadcast_timeout, + port_scan_timeout, + netbios_timeout, + netbios_interval, + mdns_query_timeout, } } } #[derive(Clone)] pub(crate) struct TaskExecutionContext { - pub(crate) ip_sender: IpSender, - pub(crate) ip_receiver: Arc>, + // The sender that gathers all the IP addresses from Ping, Boradcast, Netbios etc.. and send to port scanner and DNS resolver + pub(crate) ip_sender: BoardcastSender, + // The final result sender that sends the result to the main thread pub(crate) result_sender: ScanEntrySender, pub(crate) ip_cache: Arc>>>, @@ -79,9 +96,8 @@ pub(crate) struct TaskExecutionRunner { impl TaskExecutionContext { pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result<(Self, ScanEntryReceiver)> { - let (ip_sender, ip_receiver) = tokio::sync::mpsc::channel(5); - let ip_receiver = Arc::new(Mutex::new(ip_receiver)); - + // Since the boarcast receiver does not implement Clone, we'll subscribe to the channel using the sender when we need it + let (ip_sender, _ip_receiver) = tokio::sync::broadcast::channel(100); let (result_sender, result_receiver) = tokio::sync::mpsc::channel(100); let NetworkScanner { @@ -95,7 +111,6 @@ impl TaskExecutionContext { let broadcast_subnet = get_subnets()?; let res = Self { ip_sender, - ip_receiver, result_sender, ip_cache: Arc::new(parking_lot::RwLock::new(HashMap::new())), runtime, diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 31daa53e1..bdd458951 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -1,3 +1,4 @@ +use crate::extract::RepeatQuery; use crate::http::HttpError; use crate::token::{ApplicationProtocol, Protocol}; use crate::DgwState; @@ -9,9 +10,9 @@ use network_scanner::interfaces; use network_scanner::ip_utils::IpAddrRange; use network_scanner::scanner::{self, NetworkScannerParams, ScannerConfig}; use serde::Serialize; -use serde_querystring::{from_str, ParseMode}; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::Duration; pub fn make_router(state: DgwState) -> Router { let router = Router::new().route("/scan", axum::routing::get(handle_network_scan)); @@ -29,18 +30,9 @@ pub fn make_router(state: DgwState) -> Router { pub async fn handle_network_scan( _token: crate::extract::NetScanToken, ws: WebSocketUpgrade, - RawQuery(query): RawQuery, + RepeatQuery(query): RepeatQuery, ) -> Result { - let query = query.unwrap_or_default(); - - // Use serde_qs to parse array parameters - // As suggested by serde_urlencoded author https://github.com/nox/serde_urlencoded/issues/85 - let query_params = match from_str::(&query, ParseMode::Duplicate) { - Ok(params) => Ok(params), - Err(e) => Err(HttpError::bad_request().build(e)), - }?; - - let scanner_params: NetworkScannerParams = query_params.try_into().map_err(|e| { + let scanner_params: NetworkScannerParams = query.try_into().map_err(|e| { error!(error = format!("{e:#}"), "Failed to parse query parameters"); HttpError::bad_request().build(e) })?; @@ -141,31 +133,31 @@ pub struct NetworkScanQueryParams { pub max_wait: Option, /// The start and end IP address of the range to scan. /// for example: 10.10.0.0-10.10.0.255 - #[serde(default)] - pub range: Vec, + #[serde(default, rename = "range")] + pub ranges: Vec, /// The ports to scan. If not specified, the default ports will be used. + #[serde(default, rename = "port")] + pub ports: Vec, + + /// Enable the emission of ScanEvent::Ping for status start #[serde(default)] - pub port: Vec, + pub enable_ping_start: bool, - /// Disable the emission of ScanEvent::Ping for status start + /// Enable the execution of broadcast scan #[serde(default = "default_true")] - pub disable_ping_start: bool, + pub enable_broadcast: bool, - /// Disable the execution of broadcast scan - #[serde(default)] - pub disable_boardcast: bool, - - /// Disable the ping scan on subnet - #[serde(default)] - pub disable_subnet_scan: bool, + /// Enable the ping scan on subnet + #[serde(default = "default_true")] + pub enable_subnet_scan: bool, - /// Disable ZeroConf/mDNS - #[serde(default)] - pub disable_zeroconf: bool, + /// Enable ZeroConf/mDNS + #[serde(default = "default_true")] + pub enable_zeroconf: bool, - /// Disable resolve dns - #[serde(default)] - pub disable_resolve_dns: bool, + /// Enable resolve dns + #[serde(default = "default_true")] + pub enable_resolve_dns: bool, } fn default_true() -> bool { @@ -179,34 +171,44 @@ impl TryFrom for NetworkScannerParams { fn try_from(val: NetworkScanQueryParams) -> Result { warn!(query=?val, "Network scan query parameters"); - let ports = match val.port.len() { + let ports = match val.ports.len() { 0 => COMMON_PORTS.to_vec(), - _ => val.port, + _ => val.ports, }; + let ping_interval = Duration::from_millis(val.ping_interval.unwrap_or(200)); + let ping_timeout = Duration::from_millis(val.ping_timeout.unwrap_or(500)); + let broadcast_timeout = Duration::from_millis(val.broadcast_timeout.unwrap_or(1000)); + let port_scan_timeout = Duration::from_millis(val.port_scan_timeout.unwrap_or(1000)); + let netbios_timeout = Duration::from_millis(val.netbios_timeout.unwrap_or(1000)); + let netbios_interval = Duration::from_millis(val.netbios_interval.unwrap_or(200)); + let mdns_query_timeout = Duration::from_millis(val.mdns_query_timeout.unwrap_or(5 * 1000)); + let max_wait_time = Duration::from_millis(val.max_wait.unwrap_or(120 * 1000)); + let ip_ranges = val + .ranges + .iter() + .map(IpAddrRange::try_from) + .collect::, anyhow::Error>>()?; + Ok(NetworkScannerParams { - configs: ScannerConfig { + config: ScannerConfig { ports, - ping_interval: val.ping_interval.unwrap_or(200), - ping_timeout: val.ping_timeout.unwrap_or(500), - broadcast_timeout: val.broadcast_timeout.unwrap_or(1000), - port_scan_timeout: val.port_scan_timeout.unwrap_or(1000), - netbios_timeout: val.netbios_timeout.unwrap_or(1000), - max_wait_time: val.max_wait.unwrap_or(120 * 1000), - netbios_interval: val.netbios_interval.unwrap_or(200), - mdns_query_timeout: val.mdns_query_timeout.unwrap_or(5 * 1000), // in milliseconds - ip_ranges: val - .range - .iter() - .map(IpAddrRange::try_from) - .collect::, anyhow::Error>>()?, + ping_interval, + ping_timeout, + broadcast_timeout, + port_scan_timeout, + netbios_timeout, + max_wait_time, + netbios_interval, + mdns_query_timeout, + ip_ranges, }, - toggles: scanner::ScannerToggles { - disable_ping_start: val.disable_ping_start, - disable_broadcast: val.disable_boardcast, - disable_subnet_scan: val.disable_subnet_scan, - disable_zeroconf: val.disable_zeroconf, - disable_resolve_dns: val.disable_resolve_dns, + toggle: scanner::ScannerToggles { + enable_ping_start: val.enable_ping_start, + enable_broadcast: val.enable_broadcast, + enable_subnet_scan: val.enable_subnet_scan, + enable_zeroconf: val.enable_zeroconf, + enable_resolve_dns: val.enable_resolve_dns, }, }) } @@ -415,12 +417,12 @@ impl From for NetworkInterface { interfaces::Addr::V4(v4) => Addr::V4(V4IfAddr { ip: v4.ip, broadcast: v4.broadcast, - netmask: v4.netmask.map(|netmask| Netmask(netmask)), + netmask: v4.netmask.map(Netmask), }), interfaces::Addr::V6(v6) => Addr::V6(V6IfAddr { ip: v6.ip, broadcast: v6.broadcast, - netmask: v6.netmask.map(|netmask| Netmask(netmask)), + netmask: v6.netmask.map(Netmask), }), }) .collect(); diff --git a/devolutions-gateway/src/extract.rs b/devolutions-gateway/src/extract.rs index 003d8a164..ab1201303 100644 --- a/devolutions-gateway/src/extract.rs +++ b/devolutions-gateway/src/extract.rs @@ -8,6 +8,8 @@ use crate::token::{ JrlTokenClaims, ScopeTokenClaims, WebAppTokenClaims, }; +use axum::extract::{FromRequest, RawQuery, Request}; + #[derive(Clone)] pub struct AccessToken(pub AccessTokenClaims); @@ -367,3 +369,26 @@ where } } } + +pub struct RepeatQuery(pub(crate) T); + +#[async_trait] +impl FromRequest for RepeatQuery +where + T: serde::de::DeserializeOwned, + S: Send + Sync, +{ + type Rejection = HttpError; + + async fn from_request(req: Request, state: &S) -> Result { + let RawQuery(query) = RawQuery::from_request(req, state) + .await + .map_err(|e| HttpError::bad_request().build(e))?; + + let query = query.unwrap_or_default(); + let parsed_query = serde_querystring::from_str::(&query, serde_querystring::ParseMode::Duplicate) + .map_err(|e| HttpError::bad_request().build(e))?; + + Ok(RepeatQuery(parsed_query)) + } +} From 3e57907ba69e2a74cec7a36c86b3eea7b2f26b56 Mon Sep 17 00:00:00 2001 From: irving ou Date: Fri, 11 Apr 2025 13:30:43 -0400 Subject: [PATCH 14/33] more fixes --- crates/network-scanner/src/ip_utils.rs | 2 +- crates/network-scanner/src/mdns.rs | 23 +++++++--- crates/network-scanner/src/scanner.rs | 54 ++++++++++++------------ crates/network-scanner/src/task_utils.rs | 27 ++++++------ devolutions-gateway/src/api/net.rs | 2 +- 5 files changed, 57 insertions(+), 51 deletions(-) diff --git a/crates/network-scanner/src/ip_utils.rs b/crates/network-scanner/src/ip_utils.rs index 74290cfdc..d026f74a5 100644 --- a/crates/network-scanner/src/ip_utils.rs +++ b/crates/network-scanner/src/ip_utils.rs @@ -242,7 +242,7 @@ impl TryFrom for IpAddrRange { let V4IfAddr { ip, netmask, .. } = value; let netmask = netmask.ok_or_else(|| anyhow::anyhow!("No netmask found"))?; let (lower, upper) = calculate_subnet_bounds(ip, netmask); - Ok(IpAddrRange::new_ipv4(lower.into(), upper.into())) + Ok(IpAddrRange::new_ipv4(lower, upper)) } } diff --git a/crates/network-scanner/src/mdns.rs b/crates/network-scanner/src/mdns.rs index 20f114d29..39db5b908 100644 --- a/crates/network-scanner/src/mdns.rs +++ b/crates/network-scanner/src/mdns.rs @@ -1,8 +1,9 @@ use anyhow::Context; use mdns_sd::ServiceEvent; +use tokio::sync::mpsc; -use crate::scanner::{ScanEntry, ServiceType}; -use crate::task_utils::{ScanEntryReceiver, TaskManager}; +use crate::scanner::ServiceType; +use crate::task_utils::TaskManager; use crate::ScannerError; #[derive(Clone)] @@ -60,13 +61,21 @@ const SERVICE_TYPES_INTERESTED: [ServiceType; 10] = [ ServiceType::Vnc, ]; +#[derive(Debug, Clone)] +pub struct MdnsResult { + pub addr: std::net::IpAddr, + pub hostname: Option, + pub port: u16, + pub service_type: Option, +} + pub fn mdns_query_scan( service_daemon: MdnsDaemon, task_manager: TaskManager, query_duration: std::time::Duration, -) -> Result { +) -> Result, ScannerError> { let service_daemon = service_daemon.get_service_daemon(); - let (result_sender, result_receiver) = tokio::sync::mpsc::channel(255); + let (result_sender, result_receiver) = mpsc::channel(255); for service in SERVICE_TYPES_INTERESTED { let service_name: &str = service.into(); @@ -106,9 +115,9 @@ pub fn mdns_query_scan( let port = msg.get_port(); - for ip in msg.get_addresses() { - let entry = ScanEntry::Result { - addr: *ip, + for addr in msg.get_addresses() { + let entry = MdnsResult { + addr: *addr, hostname: Some(device_name.clone()), port, service_type: protocol, diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index b63bfb3ca..28bc2bb76 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -1,15 +1,16 @@ use crate::broadcast::asynchronous::broadcast; use crate::ip_utils::IpAddrRange; -use crate::mdns::{self, MdnsDaemon}; +use crate::mdns::{self, MdnsDaemon, MdnsResult}; use crate::netbios::netbios_query_scan; use crate::ping::{ping_range, PingFailedReason}; use crate::port_discovery::{scan_ports, PortScanResult}; -use crate::task_utils::{ScanEntryReceiver, TaskExecutionContext, TaskExecutionRunner, TaskManager}; +use crate::task_utils::{TaskExecutionContext, TaskExecutionRunner, TaskManager}; use anyhow::Context; use std::fmt::Display; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; +use tokio::sync::mpsc; use typed_builder::TypedBuilder; /// Represents a network scanner for discovering devices and their services over a network. @@ -21,9 +22,9 @@ pub struct NetworkScanner { pub(crate) mdns_daemon: Option, /// Configuration settings for the network scanner - pub(crate) configs: ScannerConfig, + pub(crate) config: ScannerConfig, /// Toggles for enabling or disabling specific features of the scanner. - pub(crate) toggles: ScannerToggles, + pub(crate) toggle: ScannerToggles, } impl NetworkScanner { @@ -33,7 +34,7 @@ impl NetworkScanner { start_port_scan(&mut task_executor); start_dns_look_up(&mut task_executor); - if self.toggles.enable_broadcast { + if self.toggle.enable_broadcast { start_broadcast(&mut task_executor); } @@ -41,7 +42,7 @@ impl NetworkScanner { start_ping(&mut task_executor); - if self.toggles.enable_zeroconf { + if self.toggle.enable_zeroconf { start_mdns(&mut task_executor); } @@ -59,7 +60,7 @@ impl NetworkScanner { mdns_daemon, }; - let max_wait_time = self.configs.max_wait_time; + let max_wait_time = self.config.max_wait_time; tokio::spawn(async move { tokio::time::sleep(max_wait_time).await; @@ -100,6 +101,7 @@ impl NetworkScanner { // Spawn a new task for each IP address task_executor.spawn_no_sub_task(async move { + trace!(ip = ?ip, host = ?host, "DNS lookup"); let (update_dns, ip, dns) = match existing_dns { Some(None) if host.is_some() => { // If the IP address is in the cache but DNS resolution was not successful before and we have a new host coming in, update @@ -215,11 +217,12 @@ impl NetworkScanner { let broadcast_subnet = configs.broadcast_subnet; let broadcast_timeout = configs.broadcast_timeout; for subnet in broadcast_subnet { - debug!(broadcasting_to_subnet = ?subnet); + trace!(broadcasting_to_subnet = ?subnet); let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); task_manager.spawn(move |task_manager: TaskManager| async move { let mut receiver = broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; + while let Some(ip) = receiver.recv().await { trace!(broadcast_sent_ip = ?ip); ip_sender.send((ip.into(), None))?; @@ -260,7 +263,7 @@ impl NetworkScanner { netbios_query_scan(runtime, ip_range, netbios_timeout, netbios_interval, task_manager)?; while let Some(res) = receiver.recv().await { - debug!(netbios_query_sent_ip = ?res.0); + trace!(netbios_query_sent_ip = ?res.0); ip_sender.send(res)?; } } @@ -302,7 +305,7 @@ impl NetworkScanner { )?; while let Some(ping_event) = receiver.recv().await { - debug!(ping_sent_ip = ?ping_event); + trace!(ping_sent_ip = ?ping_event); match ping_event { crate::ping::PingEvent::Success { ip_addr, time } => { @@ -335,8 +338,8 @@ impl NetworkScanner { move |TaskExecutionContext { mdns_daemon, result_sender, - ip_cache, configs, + ip_sender, .. }, task_manager| async move { @@ -352,24 +355,22 @@ impl NetworkScanner { let mut receiver = mdns::mdns_query_scan(mdns_daemon, task_manager, mdns_query_timeout)?; - while let Some(ScanEntry::Result { + while let Some(MdnsResult { addr, hostname, port, service_type, }) = receiver.recv().await { - if ip_cache.read().get(&addr).is_none() { - ip_cache.write().insert(addr, hostname.clone()); - } - - let dns_name = ip_cache.read().get(&addr).cloned().flatten(); + // Let the DNS resolver to figure it out + // We can send the result directly from here, but there's no harm to let port scanner to check wanted ports for that machine anyway + ip_sender.send((addr, hostname.clone()))?; if ports.contains(&port) || service_type.is_some() { result_sender .send(ScanEntry::Result { addr, - hostname: dns_name, + hostname, port, service_type, }) @@ -383,23 +384,20 @@ impl NetworkScanner { } } - pub fn new( - NetworkScannerParams { - config: configs, - toggle: toggles, - }: NetworkScannerParams, - ) -> anyhow::Result { + pub fn new(NetworkScannerParams { config, toggle }: NetworkScannerParams) -> anyhow::Result { let runtime = network_scanner_net::runtime::Socket2Runtime::new(None)?; - let mdns_daemon = if toggles.enable_zeroconf { + let mdns_daemon = if toggle.enable_zeroconf { Some(MdnsDaemon::new()?) } else { None }; + debug!(?config, ?toggle, "Starting network scanner"); + Ok(Self { - configs, - toggles, + config, + toggle, mdns_daemon, runtime, }) @@ -430,7 +428,7 @@ pub enum ScanEntry { } pub struct NetworkScannerStream { - result_receiver: ScanEntryReceiver, + result_receiver: mpsc::Receiver, task_manager: TaskManager, mdns_daemon: Option, } diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 899d33f67..5d776580d 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -6,17 +6,16 @@ use std::time::Duration; use std::future::Future; +use tokio::sync::{broadcast, mpsc}; + use crate::ip_utils::{get_subnets, IpAddrRange, Subnet}; use crate::mdns::MdnsDaemon; use crate::scanner::{NetworkScanner, ScanEntry, ScannerConfig, ScannerToggles}; -pub(crate) type BoardcastSender = tokio::sync::broadcast::Sender<(IpAddr, Option)>; - -pub(crate) type IpHostSender = tokio::sync::mpsc::Sender<(IpAddr, Option)>; -pub(crate) type IpHostReceiver = tokio::sync::mpsc::Receiver<(IpAddr, Option)>; +pub(crate) type BoardcastSender = broadcast::Sender<(IpAddr, Option)>; -pub(crate) type ScanEntrySender = tokio::sync::mpsc::Sender; -pub(crate) type ScanEntryReceiver = tokio::sync::mpsc::Receiver; +pub(crate) type IpHostSender = mpsc::Sender<(IpAddr, Option)>; +pub(crate) type IpHostReceiver = mpsc::Receiver<(IpAddr, Option)>; #[derive(Debug, Clone)] pub(crate) struct ContextConfig { @@ -51,7 +50,7 @@ impl ContextConfig { ) -> Self { let range_to_ping = match ip_ranges.len() { 0 if toggles.enable_subnet_scan => subnet.iter().map(IpAddrRange::from).collect::>(), - _ => ip_ranges.clone(), + _ => ip_ranges, }; Self { @@ -75,7 +74,7 @@ pub(crate) struct TaskExecutionContext { pub(crate) ip_sender: BoardcastSender, // The final result sender that sends the result to the main thread - pub(crate) result_sender: ScanEntrySender, + pub(crate) result_sender: mpsc::Sender, pub(crate) ip_cache: Arc>>>, @@ -95,16 +94,16 @@ pub(crate) struct TaskExecutionRunner { } impl TaskExecutionContext { - pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result<(Self, ScanEntryReceiver)> { + pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result<(Self, mpsc::Receiver)> { // Since the boarcast receiver does not implement Clone, we'll subscribe to the channel using the sender when we need it - let (ip_sender, _ip_receiver) = tokio::sync::broadcast::channel(100); - let (result_sender, result_receiver) = tokio::sync::mpsc::channel(100); + let (ip_sender, _ip_receiver) = broadcast::channel(100); + let (result_sender, result_receiver) = mpsc::channel(100); let NetworkScanner { mdns_daemon, runtime, - configs, - toggles, + config: configs, + toggle: toggles, .. } = network_scanner; @@ -134,7 +133,7 @@ impl TaskExecutionRunner { .spawn_no_sub_task(task(context, self.task_manager.clone())); } - pub(crate) fn new(scanner: NetworkScanner) -> anyhow::Result<(Self, ScanEntryReceiver)> { + pub(crate) fn new(scanner: NetworkScanner) -> anyhow::Result<(Self, mpsc::Receiver)> { let (context, receiver) = TaskExecutionContext::new(scanner)?; Ok(( Self { diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index bdd458951..42a368417 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -169,7 +169,7 @@ const COMMON_PORTS: [u16; 11] = [22, 23, 80, 443, 389, 636, 3283, 3389, 5900, 59 impl TryFrom for NetworkScannerParams { type Error = anyhow::Error; fn try_from(val: NetworkScanQueryParams) -> Result { - warn!(query=?val, "Network scan query parameters"); + debug!(query=?val, "Network scan query parameters"); let ports = match val.ports.len() { 0 => COMMON_PORTS.to_vec(), From 14803407b83e4f784f3cd4abf1825629d31e2392 Mon Sep 17 00:00:00 2001 From: irving ou Date: Fri, 11 Apr 2025 13:32:50 -0400 Subject: [PATCH 15/33] more fixes --- crates/network-scanner/src/port_discovery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/network-scanner/src/port_discovery.rs b/crates/network-scanner/src/port_discovery.rs index 32c010205..5c67aad39 100644 --- a/crates/network-scanner/src/port_discovery.rs +++ b/crates/network-scanner/src/port_discovery.rs @@ -24,7 +24,7 @@ pub async fn scan_ports( } if port.is_empty() { - anyhow::bail!("no ports to scan"); + anyhow::bail!("no port to scan"); } let (sender, receiver) = tokio::sync::mpsc::channel(port.len()); From e68b38c393f49975c7942a94a284138ae231ddb6 Mon Sep 17 00:00:00 2001 From: irving ou Date: Fri, 11 Apr 2025 13:33:32 -0400 Subject: [PATCH 16/33] more fixes --- devolutions-gateway/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index 68fc22d00..ff3f650d7 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -39,6 +39,7 @@ terminal-streamer = { path = "../crates/terminal-streamer" } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" +serde-querystring = "0.3.0" # Utils, misc hostname = "0.4" @@ -109,7 +110,6 @@ etherparse = "0.15" # For KDC proxy portpicker = "0.1" -serde-querystring = "0.3.0" [target.'cfg(windows)'.dependencies] rustls-cng = { version = "0.5", default-features = false, features = ["logging", "tls12", "ring"] } From 195b1fd9e150b84e2f6530a7248d53dc1e262eb7 Mon Sep 17 00:00:00 2001 From: irving ou Date: Fri, 11 Apr 2025 13:34:10 -0400 Subject: [PATCH 17/33] more fixes --- crates/network-scanner/src/port_discovery.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/network-scanner/src/port_discovery.rs b/crates/network-scanner/src/port_discovery.rs index 5c67aad39..ead9aa440 100644 --- a/crates/network-scanner/src/port_discovery.rs +++ b/crates/network-scanner/src/port_discovery.rs @@ -10,24 +10,24 @@ use crate::task_utils::TaskManager; pub async fn scan_ports( ip: impl Into, - port: &[u16], + ports: &[u16], runtime: Arc, timeout: Duration, task_manager: TaskManager, ) -> anyhow::Result> { let ip = ip.into(); let mut sockets = vec![]; - for p in port { + for p in ports { let addr = SockAddr::from(SocketAddr::from((ip, *p))); let socket = runtime.new_socket(socket2::Domain::IPV4, socket2::Type::STREAM, None)?; sockets.push((socket, addr)); } - if port.is_empty() { + if ports.is_empty() { anyhow::bail!("no port to scan"); } - let (sender, receiver) = tokio::sync::mpsc::channel(port.len()); + let (sender, receiver) = tokio::sync::mpsc::channel(ports.len()); for (socket, addr) in sockets { let sender = sender.clone(); task_manager.spawn_no_sub_task(async move { From d85530da3cd7843cfae6b8665b76eb4319e5ab52 Mon Sep 17 00:00:00 2001 From: irving ou Date: Fri, 11 Apr 2025 13:40:00 -0400 Subject: [PATCH 18/33] rename ip_addr to ip --- devolutions-gateway/src/api/net.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 42a368417..848dd3622 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -226,13 +226,13 @@ pub enum Status { #[serde(tag = "protocol", rename_all = "lowercase")] pub enum ScanEvent { Ping { - ip_addr: IpAddr, + ip: IpAddr, status: Status, #[serde(skip_serializing_if = "Option::is_none")] time: Option, }, Dns { - ip_addr: IpAddr, + ip: IpAddr, hostname: String, }, } @@ -241,21 +241,21 @@ impl From for ScanEvent { fn from(event: scanner::ScanEvent) -> Self { match event { scanner::ScanEvent::PingStart { ip_addr } => Self::Ping { - ip_addr, + ip: ip_addr, status: Status::Start, time: None, }, scanner::ScanEvent::PingSuccess { ip_addr, time } => Self::Ping { - ip_addr, + ip: ip_addr, status: Status::Success, time: Some(time), }, scanner::ScanEvent::PingFailed { ip_addr, .. } => Self::Ping { - ip_addr, + ip: ip_addr, status: Status::Failed, time: None, }, - scanner::ScanEvent::Dns { ip_addr, hostname } => Self::Dns { ip_addr, hostname }, + scanner::ScanEvent::Dns { ip_addr, hostname } => Self::Dns { ip: ip_addr, hostname }, } } } @@ -265,6 +265,8 @@ impl From for ScanEvent { pub enum NetworkScanResponse { Event(ScanEvent), Entry { + /// for backward compatibility + #[serde(rename = "ip")] ip: IpAddr, hostname: Option, protocol: ApplicationProtocol, From 1fc255eaad02f6112170c51bb138cf56693d0e51 Mon Sep 17 00:00:00 2001 From: "irvingouj@Devolutions" Date: Fri, 11 Apr 2025 13:34:29 -0400 Subject: [PATCH 19/33] Update crates/network-scanner/src/ping.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Benoît Cortier <3809077+CBenoit@users.noreply.github.com> --- crates/network-scanner/src/ping.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/network-scanner/src/ping.rs b/crates/network-scanner/src/ping.rs index 22be8f017..860910b40 100644 --- a/crates/network-scanner/src/ping.rs +++ b/crates/network-scanner/src/ping.rs @@ -21,8 +21,8 @@ pub enum PingFailedReason { impl std::fmt::Display for PingFailedReason { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - PingFailedReason::Rejected => write!(f, "Ping rejected"), - PingFailedReason::TimedOut => write!(f, "Ping timed out"), + PingFailedReason::Rejected => write!(f, "ping rejected"), + PingFailedReason::TimedOut => write!(f, "ping timed out"), } } } From 6b76f2cbaab3eab76a0fb2bd5037bdf41d64a46e Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:05:10 -0400 Subject: [PATCH 20/33] refractoring done --- Cargo.lock | 21 ++ crates/network-scanner/Cargo.toml | 1 + crates/network-scanner/examples/mdns.rs | 2 +- crates/network-scanner/examples/netbios.rs | 5 +- .../examples/port_discovery.rs | 21 +- crates/network-scanner/examples/scan.rs | 46 ++- crates/network-scanner/mdns.log | 38 +++ .../src/broadcast/asynchronous.rs | 14 +- crates/network-scanner/src/broadcast/mod.rs | 8 + crates/network-scanner/src/event_bus.rs | 191 +++++++++++ crates/network-scanner/src/lib.rs | 2 + crates/network-scanner/src/mdns.rs | 68 ++-- crates/network-scanner/src/named_port.rs | 171 ++++++++++ crates/network-scanner/src/netbios.rs | 47 ++- crates/network-scanner/src/ping.rs | 18 +- crates/network-scanner/src/port_discovery.rs | 84 +++-- crates/network-scanner/src/scanner.rs | 299 +++++++----------- crates/network-scanner/src/task_utils.rs | 54 ++-- devolutions-gateway/src/api/net.rs | 274 +++++++++++----- 19 files changed, 973 insertions(+), 391 deletions(-) create mode 100644 crates/network-scanner/mdns.log create mode 100644 crates/network-scanner/src/event_bus.rs create mode 100644 crates/network-scanner/src/named_port.rs diff --git a/Cargo.lock b/Cargo.lock index 52666dd6e..ea58744c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,6 +1112,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.100", +] + [[package]] name = "des" version = "0.8.1" @@ -3573,6 +3593,7 @@ version = "0.0.0" dependencies = [ "anyhow", "crossbeam", + "derive_more", "dns-lookup", "futures-util", "ipconfig", diff --git a/crates/network-scanner/Cargo.toml b/crates/network-scanner/Cargo.toml index f90885861..bacd4a9cc 100644 --- a/crates/network-scanner/Cargo.toml +++ b/crates/network-scanner/Cargo.toml @@ -23,6 +23,7 @@ tokio = { version = "1.44", features = ["rt", "sync", "time", "fs"] } tracing = "0.1" typed-builder = "0.19" serde = "1" +derive_more = { version = "2.0.1", features = ["from"] } [target.'cfg(target_os = "windows")'.dependencies] ipconfig = "0.3" diff --git a/crates/network-scanner/examples/mdns.rs b/crates/network-scanner/examples/mdns.rs index da5a101e0..a7dec5af2 100644 --- a/crates/network-scanner/examples/mdns.rs +++ b/crates/network-scanner/examples/mdns.rs @@ -8,7 +8,7 @@ use network_scanner::task_utils::TaskManager; #[tokio::main] pub async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::SubscriberBuilder::default() - .with_max_level(tracing::Level::DEBUG) + .with_max_level(tracing::Level::INFO) .with_thread_names(true) .init(); diff --git a/crates/network-scanner/examples/netbios.rs b/crates/network-scanner/examples/netbios.rs index dcbf6e441..4806672c3 100644 --- a/crates/network-scanner/examples/netbios.rs +++ b/crates/network-scanner/examples/netbios.rs @@ -4,6 +4,7 @@ use std::net::Ipv4Addr; use std::time::Duration; use network_scanner::task_utils::TaskManager; +use tracing::info; #[tokio::main] pub async fn main() -> anyhow::Result<()> { @@ -29,8 +30,8 @@ pub async fn main() -> anyhow::Result<()> { TaskManager::new(), )?; - while let Some((ip, name)) = receiver.recv().await { - println!("{}: {:?}", ip, name); + while let Some(event) = receiver.recv().await { + info!(?event) } Ok(()) diff --git a/crates/network-scanner/examples/port_discovery.rs b/crates/network-scanner/examples/port_discovery.rs index a6fab2c36..b89d67cce 100644 --- a/crates/network-scanner/examples/port_discovery.rs +++ b/crates/network-scanner/examples/port_discovery.rs @@ -2,7 +2,11 @@ use std::time::Duration; -use network_scanner::task_utils::TaskManager; +use network_scanner::{ + named_port::{MaybeNamedPort, NamedPort}, + task_utils::TaskManager, +}; +use tracing::info; #[tokio::main] pub async fn main() { @@ -14,14 +18,19 @@ pub async fn main() { let runtime = network_scanner_net::runtime::Socket2Runtime::new(None).expect("failed to create runtime"); let ip = std::net::Ipv4Addr::new(127, 0, 0, 1); - // let port = 22,80,443,12345,3399,88 - let port = vec![22, 80, 443, 12345, 3399, 88]; + let ports: Vec = vec![ + NamedPort::Ssh.into(), + 80.into(), + NamedPort::Https.into(), + 389.into(), + 636.into(), + ]; let mut res = - network_scanner::port_discovery::scan_ports(ip, &port, runtime, Duration::from_secs(5), TaskManager::new()) + network_scanner::port_discovery::scan_ports(ip, &ports, runtime, Duration::from_secs(5), TaskManager::new()) .await .expect("failed to scan ports"); - while let Some(res) = res.recv().await { - tracing::warn!("Port scan result: {:?}", res); + while let Some(event) = res.recv().await { + info!(?event); } } diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index 681320e60..d15070d6a 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -3,21 +3,34 @@ use std::time::Duration; use anyhow::Context; -use network_scanner::scanner::{NetworkScanner, NetworkScannerParams, ScannerConfig, ScannerToggles}; +use network_scanner::{ + event_bus::{ScannerEvent, TypedReceiver}, + named_port::{MaybeNamedPort, NamedPort}, + port_discovery::TcpKnockEvent, + scanner::{DnsEvent, NetworkScanner, NetworkScannerParams, ScannerConfig, ScannerToggles}, +}; use tokio::time::timeout; fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::SubscriberBuilder::default() .with_max_level(tracing::Level::INFO) - .with_file(true) - .with_line_number(true) + .with_file(false) .with_thread_names(true) + .with_ansi(false) .init(); + let ports: Vec = vec![ + NamedPort::Ssh.into(), + 80.into(), + NamedPort::Https.into(), + 389.into(), + 636.into(), + ]; + let params = NetworkScannerParams { config: ScannerConfig { ip_ranges: vec![], - ports: vec![22, 80, 443, 389, 636], + ports, ping_interval: Duration::from_millis(20), ping_timeout: Duration::from_millis(1000), broadcast_timeout: Duration::from_millis(2000), @@ -29,7 +42,6 @@ fn main() -> anyhow::Result<()> { }, toggle: ScannerToggles { enable_broadcast: true, - enable_ping_start: true, enable_resolve_dns: true, enable_subnet_scan: true, enable_zeroconf: true, @@ -45,11 +57,13 @@ fn main() -> anyhow::Result<()> { tracing::info!("Ctrl-C received, stopping network scan"); } }); - while let Ok(Some(res)) = timeout(Duration::from_secs(120), stream.recv()).await.with_context(|| { + + let mut receiver: TypedReceiver = stream.subscribe().await; + while let Ok(event) = receiver.recv().await.with_context(|| { tracing::error!("Failed to receive from stream"); "Failed to receive from stream" }) { - tracing::warn!("Result: {:?}", res); + tracing::info!(?event); } stream.stop(); @@ -60,3 +74,21 @@ fn main() -> anyhow::Result<()> { tracing::info!("Done"); Ok(()) } + +#[derive(Debug)] +pub enum InterestedEvent { + Dns(DnsEvent), + TcpKnock(TcpKnockEvent), +} + +impl TryFrom for InterestedEvent { + type Error = (); + + fn try_from(event: ScannerEvent) -> Result { + match event { + ScannerEvent::Dns(dns) => Ok(InterestedEvent::Dns(dns)), + ScannerEvent::TcpKnock(tcp_knock) => Ok(InterestedEvent::TcpKnock(tcp_knock)), + _ => Err(()), + } + } +} diff --git a/crates/network-scanner/mdns.log b/crates/network-scanner/mdns.log new file mode 100644 index 000000000..4560dadbc --- /dev/null +++ b/crates/network-scanner/mdns.log @@ -0,0 +1,38 @@ +2025-04-14T18:03:28.995637Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:28.997965Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:28.999188Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.000660Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.001829Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.003185Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.004304Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.005551Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.006921Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.008455Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:29.320413Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.52.205, device_name: "NA-FABUHAMDAN", port: 22, protocol: Some(Ssh) } +2025-04-14T18:03:29.320458Z  INFO main mdns: Found: ServiceResolved { addr: fe80::ca8:e1e3:49ec:4ee9, device_name: "NA-FABUHAMDAN", port: 22, protocol: Some(Ssh) } +2025-04-14T18:03:29.729909Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.53.8, device_name: "Bhuvnesh's Macbook Air", port: 22, protocol: Some(Ssh) } +2025-04-14T18:03:29.729942Z  INFO main mdns: Found: ServiceResolved { addr: fe80::1835:f3e6:f77:76b0, device_name: "Bhuvnesh's Macbook Air", port: 22, protocol: Some(Ssh) } +2025-04-14T18:03:29.999995Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.001023Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.002262Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.003417Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.004401Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.005544Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.006423Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.007592Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.008504Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:30.009507Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.013439Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.014888Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.016450Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.018087Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.019591Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.021033Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.022369Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.023713Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.025334Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.027121Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) +2025-04-14T18:03:32.184383Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.52.240, device_name: "GCDWebServer", port: 37848, protocol: Some(Http) } +2025-04-14T18:03:32.184444Z  INFO main mdns: Found: ServiceResolved { addr: fe80::40a:3b61:c55a:f2ff, device_name: "GCDWebServer", port: 37848, protocol: Some(Http) } +2025-04-14T18:03:32.391895Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.52.240, device_name: "JaisonΓÇÖs MacBook Pro", port: 5900, protocol: Some(Vnc) } +2025-04-14T18:03:32.391979Z  INFO main mdns: Found: ServiceResolved { addr: fe80::40a:3b61:c55a:f2ff, device_name: "JaisonΓÇÖs MacBook Pro", port: 5900, protocol: Some(Vnc) } diff --git a/crates/network-scanner/src/broadcast/asynchronous.rs b/crates/network-scanner/src/broadcast/asynchronous.rs index ff76bcd2c..6ce8e9351 100644 --- a/crates/network-scanner/src/broadcast/asynchronous.rs +++ b/crates/network-scanner/src/broadcast/asynchronous.rs @@ -10,6 +10,8 @@ use socket2::SockAddr; use crate::create_echo_request; +use super::BroadcastEvent; + /// Broadcasts a ping to the given ip address /// caller need to make sure that the ip address is a broadcast address /// The timeout is for the read, if for more than given time no packet is received, the stream will end @@ -18,7 +20,7 @@ pub async fn broadcast( read_time_out: Duration, runtime: Arc, task_manager: crate::task_utils::TaskManager, -) -> anyhow::Result> { +) -> anyhow::Result> { let socket = runtime.new_socket( socket2::Domain::IPV4, socket2::Type::RAW, @@ -32,6 +34,11 @@ pub async fn broadcast( let (sender, receiver) = tokio::sync::mpsc::channel(255); task_manager.spawn_no_sub_task(async move { + sender + .send(BroadcastEvent::Start { brodcast_ip: ip }) + .await + .context("failed to send broadcast start event")?; + socket .send_to(&packet.to_bytes(true), &SockAddr::from(SocketAddr::new(ip.into(), 0))) .await?; @@ -43,7 +50,7 @@ pub async fn broadcast( async fn loop_receive( mut socket: network_scanner_net::socket::AsyncRawSocket, - sender: tokio::sync::mpsc::Sender, + sender: tokio::sync::mpsc::Sender, ) -> anyhow::Result<()> { let mut buffer = [MaybeUninit::uninit(); icmp_v4::ICMPV4_MTU]; loop { @@ -52,7 +59,8 @@ pub async fn broadcast( .as_socket_ipv4() .context("unreachable: we only use ipv4 for broadcast")? .ip(); - sender.send(ip).await?; + + sender.send(BroadcastEvent::Entry { ip }).await?; }; } } diff --git a/crates/network-scanner/src/broadcast/mod.rs b/crates/network-scanner/src/broadcast/mod.rs index 25321dd01..db910f194 100644 --- a/crates/network-scanner/src/broadcast/mod.rs +++ b/crates/network-scanner/src/broadcast/mod.rs @@ -6,6 +6,14 @@ use anyhow::Context; pub mod asynchronous; pub mod blocking; +pub use asynchronous::broadcast; + +#[derive(Debug, Clone)] +pub enum BroadcastEvent { + Start { brodcast_ip: Ipv4Addr }, + Entry { ip: Ipv4Addr }, +} + #[derive(Debug)] pub struct BroadcastResponseEntry { pub addr: Ipv4Addr, diff --git a/crates/network-scanner/src/event_bus.rs b/crates/network-scanner/src/event_bus.rs new file mode 100644 index 000000000..3b8f68485 --- /dev/null +++ b/crates/network-scanner/src/event_bus.rs @@ -0,0 +1,191 @@ +use std::net::IpAddr; + +use derive_more::From; +use tokio::sync::{broadcast, mpsc}; + +use crate::broadcast::BroadcastEvent; +use crate::mdns::MdnsEvent; +use crate::netbios::NetBiosEvent; +use crate::ping::PingEvent; +use crate::port_discovery::TcpKnockEvent; +use crate::scanner::{DnsEvent, TcpKnockWithHost}; + +macro_rules! define_scanner_event { + ( + $( $typ:ty => $variant:ident ),* $(,)? + ) => { + pub trait Sendable {} + + #[derive(Debug, Clone, From)] + pub enum ScannerEvent { + $( + $variant($typ), + )* + } + + $( + impl Sendable for $typ {} + )* + + } +} + +define_scanner_event! { + PingEvent => Ping, + MdnsEvent => Mdns, + NetBiosEvent => NetBios, + TcpKnockEvent => TcpKnock, + TcpKnockWithHost => TcpKnockWithHost, + BroadcastEvent => Broadcast, + DnsEvent => Dns, +} + +pub trait Splitable {} +macro_rules! define_splitable { + ( + $recv_event:ident, + $($typ:ty),* $(,)? + ) => { + + $( + impl Splitable<$typ> for $recv_event {} + )* + } +} + +#[derive(Debug, Clone)] +pub enum RawIpEvent { + Ping(PingEvent), + Boardcast(BroadcastEvent), +} + +impl TryFrom for TcpKnockEvent { + type Error = (); + + fn try_from(value: ScannerEvent) -> Result { + match value { + ScannerEvent::TcpKnock(tcp_knock_event) => Ok(tcp_knock_event), + _ => Err(()), + } + } +} + +define_splitable! {RawIpEvent, DnsEvent, TcpKnockEvent } +define_splitable! {TcpKnockEvent, TcpKnockWithHost} + +impl TryFrom for RawIpEvent { + type Error = (); + + fn try_from(value: ScannerEvent) -> Result { + match value { + ScannerEvent::Ping(ping_event) => Ok(RawIpEvent::Ping(ping_event)), + ScannerEvent::Broadcast(boardcast_event) => Ok(RawIpEvent::Boardcast(boardcast_event)), + _ => Err(()), + } + } +} + +impl RawIpEvent { + pub fn success(&self) -> Option { + match self { + RawIpEvent::Ping(PingEvent::Success { ip, .. }) => Some(*ip), + RawIpEvent::Boardcast(BroadcastEvent::Entry { ip }) => Some(IpAddr::V4(*ip)), + _ => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct EventBus { + sender: broadcast::Sender, +} + +impl EventBus { + pub fn new() -> Self { + let (sender, _) = broadcast::channel(255); + Self { sender } + } + + pub fn sender(&self) -> TypedSender { + let sender = self.sender.clone(); + TypedSender::new(sender) + } + + pub fn subscribe(&self) -> TypedReceiver { + let receiver = self.sender.subscribe(); + TypedReceiver::new(receiver) + } + + // For not making mistakes, (e.g. send the same event that we receive) + pub fn split(&self) -> (TypedSender, TypedReceiver) + where + R: Splitable, + { + let sender = self.sender.clone(); + let receiver = self.sender.subscribe(); + (TypedSender::new(sender), TypedReceiver::new(receiver)) + } +} + +pub struct TypedReceiver { + receiver: broadcast::Receiver, + _marker: std::marker::PhantomData, +} + +impl TypedReceiver { + fn new(receiver: broadcast::Receiver) -> Self { + Self { + receiver, + _marker: std::marker::PhantomData, + } + } +} + +impl TypedReceiver +where + T: TryFrom, +{ + pub async fn recv(&mut self) -> Result { + loop { + match self.receiver.recv().await { + Ok(event) => { + if let Ok(typed_event) = T::try_from(event) { + return Ok(typed_event); + } + } + Err(e) => return Err(e), + } + } + } +} + +#[derive(Clone)] +pub struct TypedSender { + sender: broadcast::Sender, + _marker: std::marker::PhantomData, +} + +impl TypedSender { + fn new(sender: broadcast::Sender) -> Self { + Self { + sender, + _marker: std::marker::PhantomData, + } + } +} + +impl TypedSender +where + T: Into + Sendable, +{ + pub fn send(&self, event: T) -> Result> { + self.sender.send(event.into()) + } + + pub async fn send_from(&self, mut receiver: mpsc::Receiver) -> anyhow::Result<()> { + while let Some(event) = receiver.recv().await { + self.send(event)?; + } + Ok(()) + } +} diff --git a/crates/network-scanner/src/lib.rs b/crates/network-scanner/src/lib.rs index e14b7becf..083fd8b0f 100644 --- a/crates/network-scanner/src/lib.rs +++ b/crates/network-scanner/src/lib.rs @@ -11,9 +11,11 @@ use network_interface::Addr; use network_scanner_proto::icmp_v4; pub mod broadcast; +pub mod event_bus; pub mod interfaces; pub mod ip_utils; pub mod mdns; +pub mod named_port; pub mod netbios; pub mod ping; pub mod port_discovery; diff --git a/crates/network-scanner/src/mdns.rs b/crates/network-scanner/src/mdns.rs index 39db5b908..c7cbb0dbc 100644 --- a/crates/network-scanner/src/mdns.rs +++ b/crates/network-scanner/src/mdns.rs @@ -8,26 +8,39 @@ use crate::ScannerError; #[derive(Clone)] pub struct MdnsDaemon { - service_daemon: mdns_sd::ServiceDaemon, + service_daemon: Option, } impl MdnsDaemon { pub fn new() -> Result { - let service_daemon = mdns_sd::ServiceDaemon::new()?; - Ok(Self { service_daemon }) + Ok(Self { service_daemon: None }) } - pub fn get_service_daemon(&self) -> mdns_sd::ServiceDaemon { - self.service_daemon.clone() + // Lazy initialization of the service daemon + pub fn get_service_daemon(&mut self) -> Result { + Ok(match &self.service_daemon { + Some(deamon) => deamon.clone(), + None => { + let service_daemon = + mdns_sd::ServiceDaemon::new().with_context(|| "Failed to create service daemon")?; + self.service_daemon = Some(service_daemon.clone()); + service_daemon + } + }) } pub fn stop(&self) { - let receiver = match self.service_daemon.shutdown() { + let service_daemon = match &self.service_daemon { + Some(deamon) => deamon.clone(), + None => return, + }; + + let receiver = match service_daemon.shutdown() { Ok(receiver) => receiver, Err(e) => { // if e is try again, we should try again, but only once let result = if matches!(e, mdns_sd::Error::Again) { - self.service_daemon.shutdown() + service_daemon.shutdown() } else { Err(e) }; @@ -62,31 +75,32 @@ const SERVICE_TYPES_INTERESTED: [ServiceType; 10] = [ ]; #[derive(Debug, Clone)] -pub struct MdnsResult { - pub addr: std::net::IpAddr, - pub hostname: Option, - pub port: u16, - pub service_type: Option, +pub enum MdnsEvent { + ServiceResolved { + addr: std::net::IpAddr, + device_name: String, + port: u16, + protocol: Option, + }, + Start { + service_type: ServiceType, + }, } pub fn mdns_query_scan( - service_daemon: MdnsDaemon, + mut service_daemon: MdnsDaemon, task_manager: TaskManager, query_duration: std::time::Duration, -) -> Result, ScannerError> { - let service_daemon = service_daemon.get_service_daemon(); +) -> Result, ScannerError> { + let daemon = service_daemon.get_service_daemon()?; let (result_sender, result_receiver) = mpsc::channel(255); for service in SERVICE_TYPES_INTERESTED { let service_name: &str = service.into(); let service_name = format!("{}.local.", service_name); - let (result_sender, service_daemon, service_daemon_clone, service_name_clone) = ( - result_sender.clone(), - service_daemon.clone(), - service_daemon.clone(), - service_name.clone(), - ); - let receiver = service_daemon.browse(service_name.as_ref()).with_context(|| { + let (result_sender, daemon_clone, service_name_clone) = + (result_sender.clone(), daemon.clone(), service_name.clone()); + let receiver = daemon.browse(service_name.as_ref()).with_context(|| { let err_msg = format!("failed to browse for service: {}", service_name); error!(error = err_msg); err_msg @@ -96,8 +110,8 @@ pub fn mdns_query_scan( task_manager .with_timeout(query_duration) .when_finish(move || { - debug!(service_name = ?service_name_clone, "Stopping browse for service"); - if let Err(e) = service_daemon_clone.stop_browse(service_name_clone.as_ref()) { + debug!("Stopping browse for mDns service"); + if let Err(e) = daemon_clone.stop_browse(service_name_clone.as_ref()) { warn!(error = %e, "Failed to stop browsing for service"); } // Receive the last event (StopBrowse), preventing the receiver from being dropped,this will satisfy the sender side to avoid logging an error @@ -116,11 +130,11 @@ pub fn mdns_query_scan( let port = msg.get_port(); for addr in msg.get_addresses() { - let entry = MdnsResult { + let entry = MdnsEvent::ServiceResolved { addr: *addr, - hostname: Some(device_name.clone()), + device_name: device_name.clone(), port, - service_type: protocol, + protocol, }; if let Err(e) = result_sender.send(entry).await { diff --git a/crates/network-scanner/src/named_port.rs b/crates/network-scanner/src/named_port.rs new file mode 100644 index 000000000..c3d403fba --- /dev/null +++ b/crates/network-scanner/src/named_port.rs @@ -0,0 +1,171 @@ +use std::net::{IpAddr, SocketAddr}; + +use derive_more::From; +use socket2::SockAddr; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NamedPort { + Wayk, + Rdp, + Ard, + Vnc, + Ssh, + Sshpwsh, + Sftp, + Scp, + Telnet, + Winrmhttppwsh, + Winrmhttpspwsh, + Http, + Https, + Ldap, + Ldaps, +} + +impl Into for &NamedPort { + fn into(self) -> u16 { + match self { + NamedPort::Wayk => 12876, + NamedPort::Rdp => 3389, + NamedPort::Ard => 5900, + NamedPort::Vnc => 5900, + NamedPort::Ssh => 22, + NamedPort::Sshpwsh => 22, + NamedPort::Sftp => 22, + NamedPort::Scp => 22, + NamedPort::Telnet => 23, + NamedPort::Winrmhttppwsh => 5985, + NamedPort::Winrmhttpspwsh => 5986, + NamedPort::Http => 80, + NamedPort::Https => 443, + NamedPort::Ldap => 389, + NamedPort::Ldaps => 636, + } + } +} + +impl TryFrom for NamedPort { + type Error = anyhow::Error; + + fn try_from(value: u16) -> Result { + match value { + 12876 => Ok(NamedPort::Wayk), + 3389 => Ok(NamedPort::Rdp), + 5900 => Ok(NamedPort::Ard), // Note: Same as VNC, will return Ard by convention + 22 => Ok(NamedPort::Ssh), // Note: Same as Sshpwsh/Sftp/Scp, will return Ssh by convention + 23 => Ok(NamedPort::Telnet), + 5985 => Ok(NamedPort::Winrmhttppwsh), + 5986 => Ok(NamedPort::Winrmhttpspwsh), + 80 => Ok(NamedPort::Http), + 443 => Ok(NamedPort::Https), + 389 => Ok(NamedPort::Ldap), + 636 => Ok(NamedPort::Ldaps), + _ => Err(anyhow::anyhow!("Unknown port number: {}", value)), + } + } +} + +impl TryFrom<&str> for NamedPort { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "wayk" => Ok(NamedPort::Wayk), + "rdp" => Ok(NamedPort::Rdp), + "ard" => Ok(NamedPort::Ard), + "vnc" => Ok(NamedPort::Vnc), + "ssh" => Ok(NamedPort::Ssh), + "sshpwsh" => Ok(NamedPort::Sshpwsh), + "sftp" => Ok(NamedPort::Sftp), + "scp" => Ok(NamedPort::Scp), + "telnet" => Ok(NamedPort::Telnet), + "winrmhttppwsh" => Ok(NamedPort::Winrmhttppwsh), + "winrmhttpspwsh" => Ok(NamedPort::Winrmhttpspwsh), + "http" => Ok(NamedPort::Http), + "https" => Ok(NamedPort::Https), + "ldap" => Ok(NamedPort::Ldap), + "ldaps" => Ok(NamedPort::Ldaps), + _ => Err(anyhow::anyhow!("Unknown named port: {}", value)), + } + } +} + +#[derive(Debug, Clone, From)] +pub enum MaybeNamedPort { + Named(NamedPort), + Port(u16), +} + +impl TryFrom<&str> for MaybeNamedPort { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + if let Ok(port) = value.parse::() { + return Ok(MaybeNamedPort::Port(port)); + } + + if let Ok(named_port) = NamedPort::try_from(value) { + return Ok(MaybeNamedPort::Named(named_port)); + } + + Err(anyhow::anyhow!("Unknown port or named port: {}", value)) + } +} + +impl PartialEq for MaybeNamedPort { + fn eq(&self, other: &Self) -> bool { + let raw_port = u16::from(self); + let raw_other_port = u16::from(other); + + raw_port == raw_other_port + } +} + +impl From<&MaybeNamedPort> for u16 { + fn from(tcp_knock: &MaybeNamedPort) -> Self { + match tcp_knock { + MaybeNamedPort::Named(named_port) => named_port.into(), + MaybeNamedPort::Port(port) => *port, + } + } +} + +#[derive(Debug, Clone)] +pub enum Probe { + Ping, + TcpKnock, +} + +#[derive(Debug, Clone)] +pub struct NamedAddress { + pub ip: IpAddr, + pub port: MaybeNamedPort, +} + +impl AsRef for NamedAddress { + fn as_ref(&self) -> &NamedAddress { + return self; + } +} + +impl NamedAddress { + pub fn new(ip: IpAddr, port: MaybeNamedPort) -> Self { + Self { ip, port } + } +} + +impl From<&NamedAddress> for SockAddr { + fn from(named_address: &NamedAddress) -> Self { + let port: u16 = (&named_address.port).into(); + SockAddr::from(SocketAddr::from((named_address.ip, port))) + } +} + +impl From<&SocketAddr> for NamedAddress { + fn from(addr: &SocketAddr) -> Self { + Self { + ip: addr.ip(), + port: MaybeNamedPort::Port(addr.port()), + } + } +} diff --git a/crates/network-scanner/src/netbios.rs b/crates/network-scanner/src/netbios.rs index 000fdcc90..0a69c59e0 100644 --- a/crates/network-scanner/src/netbios.rs +++ b/crates/network-scanner/src/netbios.rs @@ -2,13 +2,14 @@ use std::mem::MaybeUninit; use std::net::{Ipv4Addr, SocketAddr}; use std::sync::Arc; +use anyhow::Context; use network_scanner_net::runtime::Socket2Runtime; use network_scanner_net::socket::AsyncRawSocket; use network_scanner_proto::netbios::NetBiosPacket; use socket2::{Domain, SockAddr, Type}; +use tokio::sync::mpsc; use crate::ip_utils::IpV4AddrRange; -use crate::task_utils::IpHostReceiver; use crate::{assume_init, ScannerError}; const MESSAGE: [u8; 50] = [ @@ -17,6 +18,13 @@ const MESSAGE: [u8; 50] = [ 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, ]; +#[derive(Debug, Clone)] +pub enum NetBiosEvent { + Start { ip: Ipv4Addr }, + Success { ip: Ipv4Addr, name: String }, + Failed { ip: Ipv4Addr }, +} + const NET_BIOS_PORT: u16 = 137; pub fn netbios_query_scan( runtime: Arc, @@ -24,12 +32,18 @@ pub fn netbios_query_scan( single_query_duration: std::time::Duration, netbios_scan_interval: std::time::Duration, task_manager: crate::task_utils::TaskManager, -) -> Result { - let (sender, receiver) = tokio::sync::mpsc::channel(255); +) -> Result, ScannerError> { + let (sender, receiver) = mpsc::channel(255); task_manager.spawn(move |task_manager: crate::task_utils::TaskManager| async move { for ip in ip_range.into_iter() { let socket = runtime.new_socket(Domain::IPV4, Type::DGRAM, None)?; let (sender, task_manager) = (sender.clone(), task_manager.clone()); + + sender + .send(NetBiosEvent::Start { ip }) + .await + .context("failed to send netbios start event")?; + netbios_query_one(ip, socket, sender, single_query_duration, task_manager); tokio::time::sleep(netbios_scan_interval).await; } @@ -42,7 +56,7 @@ pub fn netbios_query_scan( pub(crate) fn netbios_query_one( ip: Ipv4Addr, mut socket: AsyncRawSocket, - result_sender: crate::task_utils::IpHostSender, + result_sender: mpsc::Sender, duration: std::time::Duration, task_manager: crate::task_utils::TaskManager, ) { @@ -50,16 +64,35 @@ pub(crate) fn netbios_query_one( let socket_addr: SocketAddr = (ip, NET_BIOS_PORT).into(); let addr = SockAddr::from(socket_addr); - socket.send_to(&MESSAGE, &addr).await?; let mut buf: [MaybeUninit; 1024] = [MaybeUninit::::uninit(); 1024]; - socket.recv(&mut buf).await?; + + let result: anyhow::Result<()> = async { + socket.send_to(&MESSAGE, &addr).await?; + socket.recv(&mut buf).await?; + Ok(()) + } + .await; + + if result.is_err() { + return result_sender + .send(NetBiosEvent::Failed { ip }) + .await + .context("failed to send netbios failed event"); + } // SAFETY: TODO: explain why it’s safe. let buf = unsafe { assume_init(&buf) }; let packet = NetBiosPacket::from(ip, buf); - result_sender.send((ip.into(), Some(packet.name()))).await?; + result_sender + .send({ + NetBiosEvent::Success { + ip: packet.ip, + name: packet.name(), + } + }) + .await?; anyhow::Result::<()>::Ok(()) }); diff --git a/crates/network-scanner/src/ping.rs b/crates/network-scanner/src/ping.rs index 860910b40..44e25716f 100644 --- a/crates/network-scanner/src/ping.rs +++ b/crates/network-scanner/src/ping.rs @@ -12,7 +12,7 @@ use tokio::time::timeout; use crate::create_echo_request; use crate::ip_utils::IpAddrRange; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum PingFailedReason { Rejected, TimedOut, @@ -27,11 +27,11 @@ impl std::fmt::Display for PingFailedReason { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum PingEvent { - Start { ip_addr: IpAddr }, - Success { ip_addr: IpAddr, time: u128 }, - Failed { ip_addr: IpAddr, reason: PingFailedReason }, + Start { ip: IpAddr }, + Success { ip: IpAddr, time: u128 }, + Failed { ip: IpAddr, reason: PingFailedReason }, } pub fn ping_range( @@ -60,7 +60,7 @@ pub fn ping_range( let sender_clone = sender.clone(); let ping_future = move || async move { - let _ = sender_clone.send(PingEvent::Start { ip_addr: addr.ip() }).await; + let _ = sender_clone.send(PingEvent::Start { ip: addr.ip() }).await; let start_time = std::time::Instant::now(); let ping_future = try_ping(addr.into(), socket); let ping_future = timeout(ping_wait_time, ping_future); @@ -69,7 +69,7 @@ pub fn ping_range( let elapsed = start_time.elapsed().as_millis(); let _ = sender_clone .send(PingEvent::Success { - ip_addr: addr.ip(), + ip: addr.ip(), time: elapsed, }) .await; @@ -77,7 +77,7 @@ pub fn ping_range( Ok(Err(_)) => { let _ = sender_clone .send(PingEvent::Failed { - ip_addr: addr.ip(), + ip: addr.ip(), reason: PingFailedReason::Rejected, }) .await; @@ -85,7 +85,7 @@ pub fn ping_range( Err(_) => { let _ = sender_clone .send(PingEvent::Failed { - ip_addr: addr.ip(), + ip: addr.ip(), reason: PingFailedReason::TimedOut, }) .await; diff --git a/crates/network-scanner/src/port_discovery.rs b/crates/network-scanner/src/port_discovery.rs index ead9aa440..eb4c0fbd6 100644 --- a/crates/network-scanner/src/port_discovery.rs +++ b/crates/network-scanner/src/port_discovery.rs @@ -6,21 +6,22 @@ use anyhow::Context; use network_scanner_net::runtime::Socket2Runtime; use socket2::SockAddr; +use crate::named_port::{MaybeNamedPort, NamedAddress}; use crate::task_utils::TaskManager; pub async fn scan_ports( ip: impl Into, - ports: &[u16], + ports: &[MaybeNamedPort], runtime: Arc, timeout: Duration, task_manager: TaskManager, -) -> anyhow::Result> { +) -> anyhow::Result> { let ip = ip.into(); let mut sockets = vec![]; - for p in ports { - let addr = SockAddr::from(SocketAddr::from((ip, *p))); + for port in ports { + let named_addr = NamedAddress::new(ip, port.clone()); let socket = runtime.new_socket(socket2::Domain::IPV4, socket2::Type::STREAM, None)?; - sockets.push((socket, addr)); + sockets.push((socket, named_addr)); } if ports.is_empty() { @@ -28,26 +29,38 @@ pub async fn scan_ports( } let (sender, receiver) = tokio::sync::mpsc::channel(ports.len()); - for (socket, addr) in sockets { + for (socket, named_addr) in sockets { let sender = sender.clone(); task_manager.spawn_no_sub_task(async move { - let connect_future = socket.connect(&addr); - let addr = addr - .as_socket() - .context("failed to scan port: only IPv4/IPv6 addresses are supported")?; + let sock_addr: SockAddr = named_addr.as_ref().into(); + let connect_future = socket.connect(&sock_addr); + + let NamedAddress { ip, port } = named_addr; match tokio::time::timeout(timeout, connect_future).await { Ok(Ok(())) => { // Successfully connected to the port - sender.send(PortScanResult::Open(addr)).await?; + sender.send(TcpKnockEvent::Start { ip, port }).await?; } Ok(Err(_)) => { // Failed to connect, but not due to a timeout (e.g., port is closed) - sender.send(PortScanResult::Closed(addr)).await?; + sender + .send(TcpKnockEvent::Failed { + ip, + port, + reason: PortScanFailedReason::Rejected, + }) + .await?; } Err(_) => { // Operation timed out - sender.send(PortScanResult::Timeout(addr)).await?; + sender + .send(TcpKnockEvent::Failed { + ip, + port, + reason: PortScanFailedReason::TimedOut, + }) + .await?; } } @@ -58,30 +71,35 @@ pub async fn scan_ports( Ok(receiver) } -#[derive(Debug)] -pub enum PortScanResult { - Open(SocketAddr), - Closed(SocketAddr), - Timeout(SocketAddr), +#[derive(Debug, Clone)] +pub enum PortScanFailedReason { + Rejected, + TimedOut, } -impl PortScanResult { - pub fn is_open(&self) -> bool { - matches!(self, PortScanResult::Open(_)) - } - - pub fn is_closed(&self) -> bool { - matches!(self, PortScanResult::Closed(_)) - } - - pub fn is_timeout(&self) -> bool { - matches!(self, PortScanResult::Timeout(_)) - } +#[derive(Debug, Clone)] +pub enum TcpKnockEvent { + Start { + ip: IpAddr, + port: MaybeNamedPort, + }, + Success { + ip: IpAddr, + port: MaybeNamedPort, + }, + Failed { + ip: IpAddr, + port: MaybeNamedPort, + reason: PortScanFailedReason, + }, +} - pub fn unwrap_open(self) -> SocketAddr { +impl TcpKnockEvent { + pub fn ip(&self) -> IpAddr { match self { - PortScanResult::Open(addr) => addr, - _ => panic!("unwrap_open called on non-open result"), + TcpKnockEvent::Start { ip, .. } => *ip, + TcpKnockEvent::Success { ip, .. } => *ip, + TcpKnockEvent::Failed { ip, .. } => *ip, } } } diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 28bc2bb76..29a52e5b1 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -1,16 +1,19 @@ use crate::broadcast::asynchronous::broadcast; +use crate::event_bus::{EventBus, RawIpEvent, ScannerEvent, TypedReceiver}; use crate::ip_utils::IpAddrRange; -use crate::mdns::{self, MdnsDaemon, MdnsResult}; +use crate::mdns::{self, MdnsDaemon, MdnsEvent}; +use crate::named_port::MaybeNamedPort; use crate::netbios::netbios_query_scan; -use crate::ping::{ping_range, PingFailedReason}; -use crate::port_discovery::{scan_ports, PortScanResult}; +use crate::ping::ping_range; +use crate::port_discovery::{scan_ports, TcpKnockEvent}; use crate::task_utils::{TaskExecutionContext, TaskExecutionRunner, TaskManager}; use anyhow::Context; use std::fmt::Display; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; -use tokio::sync::mpsc; +use tokio::sync::broadcast; +use tracing::subscriber; use typed_builder::TypedBuilder; /// Represents a network scanner for discovering devices and their services over a network. @@ -20,7 +23,6 @@ pub struct NetworkScanner { pub(crate) runtime: Arc, /// A daemon for Multicast DNS (mDNS) operations, handling service discovery. pub(crate) mdns_daemon: Option, - /// Configuration settings for the network scanner pub(crate) config: ScannerConfig, /// Toggles for enabling or disabling specific features of the scanner. @@ -29,8 +31,9 @@ pub struct NetworkScanner { impl NetworkScanner { pub fn start(&self) -> anyhow::Result { - let (mut task_executor, result_receiver) = TaskExecutionRunner::new(self.clone())?; + let mut task_executor = TaskExecutionRunner::new(self.clone())?; + start_try_get_dns_name_for_serivce(&mut task_executor); start_port_scan(&mut task_executor); start_dns_look_up(&mut task_executor); @@ -47,7 +50,9 @@ impl NetworkScanner { } let TaskExecutionRunner { - context: TaskExecutionContext { mdns_daemon, .. }, + context: TaskExecutionContext { + mdns_daemon, event_bus, .. + }, task_manager, } = task_executor; @@ -55,7 +60,7 @@ impl NetworkScanner { let mdns_daemon_clone = mdns_daemon.clone(); let scanner_stream = NetworkScannerStream { - result_receiver, + event_bus, task_manager, mdns_daemon, }; @@ -72,73 +77,70 @@ impl NetworkScanner { return Ok(scanner_stream); + fn start_try_get_dns_name_for_serivce(task_executor: &mut TaskExecutionRunner) { + task_executor.run( + move |TaskExecutionContext { + event_bus, ip_cache, .. + }: TaskExecutionContext, + _| async move { + let (result_sender, mut receiver) = event_bus.split::<_, TcpKnockEvent>(); + + while let Ok(event) = receiver.recv().await { + let ip = event.ip(); + let host = ip_cache.read().get(&ip).cloned().flatten(); + result_sender.send(TcpKnockWithHost { tcp_knock: event, host })?; + } + anyhow::Ok(()) + }, + ); + } + fn start_dns_look_up(task_executor: &mut TaskExecutionRunner) { task_executor.run( move |TaskExecutionContext { ip_cache, - ip_sender, - result_sender, + event_bus, toggles, .. }: TaskExecutionContext, task_executor| async move { let enable_dns_resolve = toggles.enable_resolve_dns; - let mut ip_receiver = ip_sender.subscribe(); - while let Ok((ip, host)) = ip_receiver.recv().await { - let ip_cache = Arc::clone(&ip_cache); - let result_sender = result_sender.clone(); - let existing_dns = { - let binding = ip_cache.read(); - binding.get(&ip).cloned() + let (event_sender, mut ip_receiver) = event_bus.split::<_, RawIpEvent>(); + + while let Ok(event) = ip_receiver.recv().await { + let Some(ip) = event.success() else { + continue; }; - // Write first, to aviod new incoming same IP address, + + let ip_cache = Arc::clone(&ip_cache); + if ip_cache.read().contains_key(&ip) { + continue; + } + + // Write first, to avoid new incoming same IP address, // The host will be updated later to the correct one anyway // Put in it's now scope to avoid holding the lock for too long { - ip_cache.write().insert(ip, host.clone()); + ip_cache.write().insert(ip, None); } + let event_sender = event_sender.clone(); + // Spawn a new task for each IP address task_executor.spawn_no_sub_task(async move { - trace!(ip = ?ip, host = ?host, "DNS lookup"); - let (update_dns, ip, dns) = match existing_dns { - Some(None) if host.is_some() => { - // If the IP address is in the cache but DNS resolution was not successful before and we have a new host coming in, update - (true, ip, host) - } - None if enable_dns_resolve => { - // If the IP address is not in the cache, resolve the DNS - let resolve_dns = tokio::task::spawn_blocking(move || { - dns_lookup::lookup_addr(&ip).context("Failed to resolve DNS").ok() - }) - .await - .context("Failed to spawn blocking task")?; - - let one_or_the_other = resolve_dns.or(host); - (true, ip, one_or_the_other) - } - None => { - // If the IP address is not in the cache, just update - (true, ip, host) - } - _ => { - // If the IP address is already in the cache, do nothing - // Already exists DNS/ or DNS not exists but new host is None as well - (false, ip, host) - } - }; - - if update_dns { - if let Some(dns) = &dns { - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::Dns { - ip_addr: ip, - hostname: dns.to_owned(), - })) - .await?; - } - ip_cache.write().insert(ip, dns); + trace!(ip = ?ip, "DNS lookup"); + if enable_dns_resolve { + event_sender.send(DnsEvent::Start { ip })?; + + // If the IP address is not in the cache, resolve the DNS + let resolve_dns = tokio::task::spawn_blocking(move || { + dns_lookup::lookup_addr(&ip).context("Failed to resolve DNS").ok() + }) + .await + .context("Failed to spawn blocking task")?; + + ip_cache.write().insert(ip, resolve_dns.clone()); } anyhow::Ok(()) @@ -153,50 +155,35 @@ impl NetworkScanner { fn start_port_scan(task_executor: &mut TaskExecutionRunner) { task_executor.run( move |TaskExecutionContext { - ip_cache, runtime, - result_sender, - ip_sender, + event_bus, configs, .. }: TaskExecutionContext, task_manager| async move { - let ip_cache = Arc::clone(&ip_cache); - let ports = configs.ports.clone(); - let mut ip_receiver = ip_sender.subscribe(); - while let Ok((ip, _)) = ip_receiver.recv().await { - let (runtime, ports, result_sender, ip_cache, port_scan_timeout) = ( + let (result_sender, mut receiver) = event_bus.split::<_, RawIpEvent>(); + + while let Ok(event) = receiver.recv().await { + let Some(ip) = event.success() else { + continue; + }; + + let (runtime, ports, result_sender, port_scan_timeout) = ( Arc::clone(&runtime), ports.clone(), result_sender.clone(), - Arc::clone(&ip_cache), configs.port_scan_timeout, ); task_manager.spawn(move |task_manager| async move { debug!(scanning_ip = ?ip); - let mut port_scan_receiver = + let port_scan_receiver = scan_ports(ip, &ports, runtime, port_scan_timeout, task_manager).await?; - while let Some(res) = port_scan_receiver.recv().await { - trace!(port_scan_result = ?res); - if let PortScanResult::Open(socket_addr) = res { - let dns = ip_cache.read().get(&ip).cloned().flatten(); - - result_sender - .send(ScanEntry::Result { - addr: ip, - hostname: dns, - port: socket_addr.port(), - service_type: None, - }) - .await?; - } - } - anyhow::Ok(()) + result_sender.send_from(port_scan_receiver).await }); } @@ -209,23 +196,25 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { runtime, - ip_sender, configs, + event_bus, .. }: TaskExecutionContext, task_manager| async move { let broadcast_subnet = configs.broadcast_subnet; let broadcast_timeout = configs.broadcast_timeout; + let ip_sender = event_bus.sender(); + for subnet in broadcast_subnet { trace!(broadcasting_to_subnet = ?subnet); - let (runtime, ip_sender) = (Arc::clone(&runtime), ip_sender.clone()); + let (runtime, sender) = (Arc::clone(&runtime), ip_sender.clone()); task_manager.spawn(move |task_manager: TaskManager| async move { let mut receiver = broadcast(subnet.broadcast, broadcast_timeout, runtime, task_manager).await?; - while let Some(ip) = receiver.recv().await { - trace!(broadcast_sent_ip = ?ip); - ip_sender.send((ip.into(), None))?; + while let Some(event) = receiver.recv().await { + trace!(broadcast_event = ?event); + sender.send(event)?; } anyhow::Ok(()) }); @@ -239,8 +228,8 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { runtime, - ip_sender, configs, + event_bus, .. }: TaskExecutionContext, task_manager| async move { @@ -249,23 +238,20 @@ impl NetworkScanner { let subnets = configs.broadcast_subnet; let subnet_ranges: Vec = subnets.iter().map(|subnet| subnet.into()).collect(); + let sender = event_bus.sender(); debug!(netbios_query_ip_ranges = ?subnet_ranges); for subnet_range in subnet_ranges { - let (runtime, ip_sender, task_manager) = - (Arc::clone(&runtime), ip_sender.clone(), task_manager.clone()); + let (runtime, task_manager) = (Arc::clone(&runtime), task_manager.clone()); let IpAddrRange::V4(ip_range) = subnet_range else { continue; }; - let mut receiver = + let receiver = netbios_query_scan(runtime, ip_range, netbios_timeout, netbios_interval, task_manager)?; - while let Some(res) = receiver.recv().await { - trace!(netbios_query_sent_ip = ?res.0); - ip_sender.send(res)?; - } + sender.send_from(receiver).await?; } anyhow::Ok(()) }, @@ -276,11 +262,9 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { runtime, - ip_sender, ip_cache, - result_sender, - toggles, configs, + event_bus, .. }: TaskExecutionContext, task_manager| async move { @@ -289,13 +273,13 @@ impl NetworkScanner { let ping_interval = configs.ping_interval; let ping_timeout = configs.ping_timeout; let range_to_ping = configs.range_to_ping; - let enable_ping_start = toggles.enable_ping_start; + + let sender = event_bus.sender(); for ip_range in range_to_ping { - let (task_manager, runtime, ip_sender) = - (task_manager.clone(), Arc::clone(&runtime), ip_sender.clone()); + let (task_manager, runtime) = (task_manager.clone(), Arc::clone(&runtime)); let should_ping = should_ping.clone(); - let mut receiver = ping_range( + let receiver = ping_range( runtime, ip_range, ping_interval, @@ -304,29 +288,7 @@ impl NetworkScanner { task_manager, )?; - while let Some(ping_event) = receiver.recv().await { - trace!(ping_sent_ip = ?ping_event); - - match ping_event { - crate::ping::PingEvent::Success { ip_addr, time } => { - ip_sender.send((ip_addr, None))?; - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::PingSuccess { ip_addr, time })) - .await?; - } - crate::ping::PingEvent::Start { ip_addr } if enable_ping_start => { - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::PingStart { ip_addr })) - .await?; - } - crate::ping::PingEvent::Failed { ip_addr, reason } => { - result_sender - .send(ScanEntry::ScanEvent(ScanEvent::PingFailed { ip_addr, reason })) - .await?; - } - _ => {} - } - } + sender.send_from(receiver).await?; } anyhow::Ok(()) }, @@ -337,9 +299,9 @@ impl NetworkScanner { task_executor.run( move |TaskExecutionContext { mdns_daemon, - result_sender, configs, - ip_sender, + event_bus, + ip_cache, .. }, task_manager| async move { @@ -350,32 +312,20 @@ impl NetworkScanner { None => anyhow::bail!("mDNS daemon is not available but mDNS is enabled"), }; + let result_sender = event_bus.sender(); + let mdns_query_timeout = configs.mdns_query_timeout; - let ports = configs.ports.clone(); let mut receiver = mdns::mdns_query_scan(mdns_daemon, task_manager, mdns_query_timeout)?; - while let Some(MdnsResult { - addr, - hostname, - port, - service_type, - }) = receiver.recv().await - { + while let Some(result) = receiver.recv().await { // Let the DNS resolver to figure it out // We can send the result directly from here, but there's no harm to let port scanner to check wanted ports for that machine anyway - ip_sender.send((addr, hostname.clone()))?; - - if ports.contains(&port) || service_type.is_some() { - result_sender - .send(ScanEntry::Result { - addr, - hostname, - port, - service_type, - }) - .await?; + if let MdnsEvent::ServiceResolved { addr, device_name, .. } = &result { + ip_cache.write().insert(*addr, Some(device_name.to_owned())); } + + result_sender.send(result)?; } anyhow::Ok(()) @@ -404,45 +354,15 @@ impl NetworkScanner { } } -#[derive(Debug)] -pub enum ScanEvent { - PingStart { ip_addr: IpAddr }, - PingFailed { ip_addr: IpAddr, reason: PingFailedReason }, - PingSuccess { ip_addr: IpAddr, time: u128 }, - Dns { ip_addr: IpAddr, hostname: String }, -} - -#[derive(Debug)] -pub enum ScanEntry { - ScanEvent(ScanEvent), - Result { - // IP address of the device - addr: IpAddr, - // Hostname of the device - hostname: Option, - // Port number - port: u16, - // The protocol / service type listening on the port - service_type: Option, - }, -} - pub struct NetworkScannerStream { - result_receiver: mpsc::Receiver, + event_bus: EventBus, task_manager: TaskManager, mdns_daemon: Option, } impl NetworkScannerStream { - pub async fn recv(self: &mut Self) -> Option { - // The caller sometimes require Send, hence the Arc is necessary for socket_addr_receiver. - self.result_receiver.recv().await - } - - pub async fn recv_timeout(self: &mut Self, duration: Duration) -> anyhow::Result> { - tokio::time::timeout(duration, self.result_receiver.recv()) - .await - .context("recv_timeout timed out") + pub async fn subscribe(&self) -> TypedReceiver { + self.event_bus.subscribe::() } pub fn stop(self: &Self) { @@ -506,7 +426,6 @@ impl Display for ScanMethod { #[derive(Debug, Clone)] pub struct ScannerToggles { - pub enable_ping_start: bool, pub enable_broadcast: bool, pub enable_subnet_scan: bool, pub enable_zeroconf: bool, @@ -515,7 +434,7 @@ pub struct ScannerToggles { #[derive(Debug, Clone)] pub struct ScannerConfig { - pub ports: Vec, + pub ports: Vec, pub ping_interval: Duration, pub ping_timeout: Duration, pub broadcast_timeout: Duration, @@ -559,3 +478,19 @@ pub enum ServiceType { /// Secure LDAP Protocol Ldaps, } + +#[derive(Debug, Clone)] +pub enum DnsEvent { + /// DNS query start + Start { ip: IpAddr }, + /// DNS query success + Success { ip: IpAddr, hostname: String }, + /// DNS query failed + Failed { ip: IpAddr }, +} + +#[derive(Debug, Clone)] +pub struct TcpKnockWithHost { + pub tcp_knock: TcpKnockEvent, + pub host: Option, +} diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index 5d776580d..cdc4ae49a 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -8,20 +8,17 @@ use std::future::Future; use tokio::sync::{broadcast, mpsc}; +use crate::event_bus::{EventBus, ScannerEvent}; use crate::ip_utils::{get_subnets, IpAddrRange, Subnet}; use crate::mdns::MdnsDaemon; -use crate::scanner::{NetworkScanner, ScanEntry, ScannerConfig, ScannerToggles}; - -pub(crate) type BoardcastSender = broadcast::Sender<(IpAddr, Option)>; - -pub(crate) type IpHostSender = mpsc::Sender<(IpAddr, Option)>; -pub(crate) type IpHostReceiver = mpsc::Receiver<(IpAddr, Option)>; +use crate::named_port::MaybeNamedPort; +use crate::scanner::{NetworkScanner, ScannerConfig, ScannerToggles}; #[derive(Debug, Clone)] pub(crate) struct ContextConfig { pub(crate) broadcast_subnet: Vec, // The subnet that have a broadcast address pub(crate) range_to_ping: Vec, - pub ports: Vec, + pub ports: Vec, pub ping_interval: Duration, pub ping_timeout: Duration, pub broadcast_timeout: Duration, @@ -70,11 +67,7 @@ impl ContextConfig { #[derive(Clone)] pub(crate) struct TaskExecutionContext { - // The sender that gathers all the IP addresses from Ping, Boradcast, Netbios etc.. and send to port scanner and DNS resolver - pub(crate) ip_sender: BoardcastSender, - - // The final result sender that sends the result to the main thread - pub(crate) result_sender: mpsc::Sender, + pub(crate) event_bus: EventBus, pub(crate) ip_cache: Arc>>>, @@ -88,16 +81,10 @@ pub(crate) struct TaskExecutionContext { type HandlesReceiver = crossbeam::channel::Receiver>>; type HandlesSender = crossbeam::channel::Sender>>; -pub(crate) struct TaskExecutionRunner { - pub(crate) context: TaskExecutionContext, - pub(crate) task_manager: TaskManager, -} - impl TaskExecutionContext { - pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result<(Self, mpsc::Receiver)> { + pub(crate) fn new(network_scanner: NetworkScanner) -> anyhow::Result { // Since the boarcast receiver does not implement Clone, we'll subscribe to the channel using the sender when we need it - let (ip_sender, _ip_receiver) = broadcast::channel(100); - let (result_sender, result_receiver) = mpsc::channel(100); + let event_bus = EventBus::new(); let NetworkScanner { mdns_daemon, @@ -108,9 +95,8 @@ impl TaskExecutionContext { } = network_scanner; let broadcast_subnet = get_subnets()?; - let res = Self { - ip_sender, - result_sender, + let context = Self { + event_bus: event_bus.clone(), ip_cache: Arc::new(parking_lot::RwLock::new(HashMap::new())), runtime, mdns_daemon, @@ -118,10 +104,15 @@ impl TaskExecutionContext { toggles, }; - Ok((res, result_receiver)) + Ok(context) } } +pub(crate) struct TaskExecutionRunner { + pub(crate) context: TaskExecutionContext, + pub(crate) task_manager: TaskManager, +} + impl TaskExecutionRunner { pub(crate) fn run(&mut self, task: T) where @@ -133,15 +124,12 @@ impl TaskExecutionRunner { .spawn_no_sub_task(task(context, self.task_manager.clone())); } - pub(crate) fn new(scanner: NetworkScanner) -> anyhow::Result<(Self, mpsc::Receiver)> { - let (context, receiver) = TaskExecutionContext::new(scanner)?; - Ok(( - Self { - context, - task_manager: TaskManager::new(), - }, - receiver, - )) + pub(crate) fn new(scanner: NetworkScanner) -> anyhow::Result { + let context = TaskExecutionContext::new(scanner)?; + Ok(Self { + context, + task_manager: TaskManager::new(), + }) } } diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 848dd3622..b68bd0bcf 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -1,14 +1,20 @@ use crate::extract::RepeatQuery; use crate::http::HttpError; -use crate::token::{ApplicationProtocol, Protocol}; +use crate::token::Protocol; use crate::DgwState; use axum::extract::ws::{Message, Utf8Bytes}; use axum::extract::{RawQuery, WebSocketUpgrade}; use axum::response::Response; use axum::{Json, Router}; +use network_scanner::event_bus::ScannerEvent; use network_scanner::interfaces; use network_scanner::ip_utils::IpAddrRange; -use network_scanner::scanner::{self, NetworkScannerParams, ScannerConfig}; +use network_scanner::mdns::MdnsEvent; +use network_scanner::named_port::{MaybeNamedPort, NamedPort}; +use network_scanner::netbios::NetBiosEvent; +use network_scanner::ping::PingEvent; +use network_scanner::port_discovery::TcpKnockEvent; +use network_scanner::scanner::{self, DnsEvent, NetworkScannerParams, ScannerConfig, TcpKnockWithHost}; use serde::Serialize; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -32,7 +38,7 @@ pub async fn handle_network_scan( ws: WebSocketUpgrade, RepeatQuery(query): RepeatQuery, ) -> Result { - let scanner_params: NetworkScannerParams = query.try_into().map_err(|e| { + let (scanner_params, filter) = query.try_into().map_err(|e| { error!(error = format!("{e:#}"), "Failed to parse query parameters"); HttpError::bad_request().build(e) })?; @@ -43,7 +49,7 @@ pub async fn handle_network_scan( })?; let res = ws.on_upgrade(move |mut websocket| async move { - let mut stream = match scanner.start() { + let stream = match scanner.start() { Ok(stream) => stream, Err(e) => { error!(error = format!("{e:#}"), "Failed to start network scan"); @@ -53,10 +59,12 @@ pub async fn handle_network_scan( info!("Network scan started"); + let mut receiver = stream.subscribe::().await; + loop { tokio::select! { - result = stream.recv() => { - let Some(entry) = result else { + result = receiver.recv() => { + let Ok(response) = result else { let _ = websocket .send(Message::Close(Some(axum::extract::ws::CloseFrame { code: axum::extract::ws::close_code::NORMAL, @@ -67,7 +75,9 @@ pub async fn handle_network_scan( break; }; - let response: NetworkScanResponse = entry.into(); + if !filter.should_emit(&response) { + continue; + } let Ok(response) = serde_json::to_string(&response) else { warn!("Failed to serialize response"); @@ -136,8 +146,8 @@ pub struct NetworkScanQueryParams { #[serde(default, rename = "range")] pub ranges: Vec, /// The ports to scan. If not specified, the default ports will be used. - #[serde(default, rename = "port")] - pub ports: Vec, + #[serde(default, rename = "probe")] + pub probes: Vec, /// Enable the emission of ScanEvent::Ping for status start #[serde(default)] @@ -158,24 +168,62 @@ pub struct NetworkScanQueryParams { /// Enable resolve dns #[serde(default = "default_true")] pub enable_resolve_dns: bool, + + #[serde(default = "default_true")] + pub enable_failure: bool, } fn default_true() -> bool { true } +#[derive(Debug, Clone)] +pub enum Probe { + Ping, + Port(MaybeNamedPort), +} + +impl TryFrom<&str> for Probe { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() { + "ping" => Ok(Probe::Ping), + _ => MaybeNamedPort::try_from(value).map(Probe::Port), + } + } +} + const COMMON_PORTS: [u16; 11] = [22, 23, 80, 443, 389, 636, 3283, 3389, 5900, 5985, 5986]; -impl TryFrom for NetworkScannerParams { +impl TryFrom for (NetworkScannerParams, EventFilter) { type Error = anyhow::Error; fn try_from(val: NetworkScanQueryParams) -> Result { debug!(query=?val, "Network scan query parameters"); - let ports = match val.ports.len() { - 0 => COMMON_PORTS.to_vec(), - _ => val.ports, + let probe: Vec = match val.probes.len() { + 0 => COMMON_PORTS.iter().map(|port| Probe::Port((*port).into())).collect(), + _ => val + .probes + .iter() + .map(|probe| { + Probe::try_from(probe.as_str()).map_err(|e| { + error!(error = format!("{e:#}"), "Failed to parse probe"); + anyhow::anyhow!("Failed to parse probe") + }) + }) + .collect::, anyhow::Error>>()?, }; + let enable_ping_event = probe.iter().any(|probe| matches!(probe, Probe::Ping)); + let ports = probe + .into_iter() + .filter_map(|probe| match probe { + Probe::Ping => None, + Probe::Port(port) => Some(port), + }) + .collect::>(); + let ping_interval = Duration::from_millis(val.ping_interval.unwrap_or(200)); let ping_timeout = Duration::from_millis(val.ping_timeout.unwrap_or(500)); let broadcast_timeout = Duration::from_millis(val.broadcast_timeout.unwrap_or(1000)); @@ -190,7 +238,7 @@ impl TryFrom for NetworkScannerParams { .map(IpAddrRange::try_from) .collect::, anyhow::Error>>()?; - Ok(NetworkScannerParams { + let scanner_param = NetworkScannerParams { config: ScannerConfig { ports, ping_interval, @@ -204,13 +252,19 @@ impl TryFrom for NetworkScannerParams { ip_ranges, }, toggle: scanner::ScannerToggles { - enable_ping_start: val.enable_ping_start, enable_broadcast: val.enable_broadcast, enable_subnet_scan: val.enable_subnet_scan, enable_zeroconf: val.enable_zeroconf, enable_resolve_dns: val.enable_resolve_dns, }, - }) + }; + + let event_filter = EventFilter { + enable_ping_event, + enable_failure: val.enable_failure, + }; + + Ok((scanner_param, event_filter)) } } @@ -231,31 +285,46 @@ pub enum ScanEvent { #[serde(skip_serializing_if = "Option::is_none")] time: Option, }, - Dns { + Host { ip: IpAddr, hostname: String, }, } -impl From for ScanEvent { - fn from(event: scanner::ScanEvent) -> Self { - match event { - scanner::ScanEvent::PingStart { ip_addr } => Self::Ping { - ip: ip_addr, - status: Status::Start, - time: None, - }, - scanner::ScanEvent::PingSuccess { ip_addr, time } => Self::Ping { - ip: ip_addr, - status: Status::Success, - time: Some(time), - }, - scanner::ScanEvent::PingFailed { ip_addr, .. } => Self::Ping { - ip: ip_addr, - status: Status::Failed, - time: None, - }, - scanner::ScanEvent::Dns { ip_addr, hostname } => Self::Dns { ip: ip_addr, hostname }, +#[derive(Debug, Serialize)] +#[serde(untagged, rename_all = "lowercase")] +pub enum TcpKockProbe { + Number(u16), + NamedApplication(Protocol), +} + +impl From for TcpKockProbe { + fn from(port: MaybeNamedPort) -> Self { + match port { + MaybeNamedPort::Port(port) => TcpKockProbe::Number(port), + MaybeNamedPort::Named(named_port) => TcpKockProbe::NamedApplication(named_port.into()), + } + } +} + +impl From for Protocol { + fn from(named_port: NamedPort) -> Self { + match named_port { + NamedPort::Wayk => Protocol::Wayk, + NamedPort::Rdp => Protocol::Rdp, + NamedPort::Ard => Protocol::Ard, + NamedPort::Vnc => Protocol::Vnc, + NamedPort::Ssh => Protocol::Ssh, + NamedPort::Sshpwsh => Protocol::SshPwsh, + NamedPort::Sftp => Protocol::Sftp, + NamedPort::Scp => Protocol::Scp, + NamedPort::Telnet => Protocol::Telnet, + NamedPort::Winrmhttppwsh => Protocol::WinrmHttpPwsh, + NamedPort::Winrmhttpspwsh => Protocol::WinrmHttpsPwsh, + NamedPort::Http => Protocol::Http, + NamedPort::Https => Protocol::Https, + NamedPort::Ldap => Protocol::Ldap, + NamedPort::Ldaps => Protocol::Ldaps, } } } @@ -265,61 +334,104 @@ impl From for ScanEvent { pub enum NetworkScanResponse { Event(ScanEvent), Entry { - /// for backward compatibility - #[serde(rename = "ip")] ip: IpAddr, + #[serde(skip_serializing_if = "Option::is_none")] hostname: Option, - protocol: ApplicationProtocol, + protocol: TcpKockProbe, + status: Status, }, } -impl NetworkScanResponse { - fn new(ip: IpAddr, port: u16, dns: Option, service_type: Option) -> Self { - let hostname = dns; - - let protocol = if let Some(protocol) = service_type { - match protocol { - scanner::ServiceType::Ssh => ApplicationProtocol::Known(Protocol::Ssh), - scanner::ServiceType::Telnet => ApplicationProtocol::Known(Protocol::Telnet), - scanner::ServiceType::Http => ApplicationProtocol::Known(Protocol::Http), - scanner::ServiceType::Https => ApplicationProtocol::Known(Protocol::Https), - scanner::ServiceType::Ldap => ApplicationProtocol::Known(Protocol::Ldap), - scanner::ServiceType::Ldaps => ApplicationProtocol::Known(Protocol::Ldaps), - scanner::ServiceType::Rdp => ApplicationProtocol::Known(Protocol::Rdp), - scanner::ServiceType::Vnc => ApplicationProtocol::Known(Protocol::Vnc), - scanner::ServiceType::Ard => ApplicationProtocol::Known(Protocol::Ard), - scanner::ServiceType::Sftp => ApplicationProtocol::Known(Protocol::Sftp), - scanner::ServiceType::Scp => ApplicationProtocol::Known(Protocol::Scp), +impl TryFrom for NetworkScanResponse { + type Error = (); + + fn try_from(event: ScannerEvent) -> Result { + match event { + ScannerEvent::Ping(PingEvent::Success { ip, time }) => Ok(NetworkScanResponse::Event(ScanEvent::Ping { + ip, + status: Status::Success, + time: Some(time), + })), + ScannerEvent::Ping(PingEvent::Failed { ip, .. }) => Ok(NetworkScanResponse::Event(ScanEvent::Ping { + ip, + status: Status::Failed, + time: None, + })), + ScannerEvent::Ping(PingEvent::Start { ip }) => Ok(NetworkScanResponse::Event(ScanEvent::Ping { + ip, + status: Status::Start, + time: None, + })), + ScannerEvent::Dns(DnsEvent::Success { ip, hostname }) => { + Ok(NetworkScanResponse::Event(ScanEvent::Host { ip, hostname })) } - } else { - match port { - 22 => ApplicationProtocol::Known(Protocol::Ssh), - 23 => ApplicationProtocol::Known(Protocol::Telnet), - 80 => ApplicationProtocol::Known(Protocol::Http), - 443 => ApplicationProtocol::Known(Protocol::Https), - 389 => ApplicationProtocol::Known(Protocol::Ldap), - 636 => ApplicationProtocol::Known(Protocol::Ldaps), - 3389 => ApplicationProtocol::Known(Protocol::Rdp), - 5900 => ApplicationProtocol::Known(Protocol::Vnc), - 5985 => ApplicationProtocol::Known(Protocol::WinrmHttpPwsh), - 5986 => ApplicationProtocol::Known(Protocol::WinrmHttpsPwsh), - _ => ApplicationProtocol::unknown(), + ScannerEvent::Mdns(MdnsEvent::ServiceResolved { addr, device_name, .. }) => { + Ok(NetworkScanResponse::Event(ScanEvent::Host { + ip: addr, + hostname: device_name, + })) } - }; - Self::Entry { ip, hostname, protocol } + ScannerEvent::NetBios(NetBiosEvent::Success { ip, name }) => { + Ok(NetworkScanResponse::Event(ScanEvent::Host { + ip: ip.into(), + hostname: name, + })) + } + ScannerEvent::TcpKnockWithHost(TcpKnockWithHost { tcp_knock, host }) => match tcp_knock { + TcpKnockEvent::Success { ip, port } => { + let protocol = port.into(); + Ok(NetworkScanResponse::Entry { + ip, + hostname: host, + protocol, + status: Status::Success, + }) + } + TcpKnockEvent::Failed { ip, port, .. } => { + let protocol = port.into(); + Ok(NetworkScanResponse::Entry { + ip, + hostname: host, + protocol, + status: Status::Failed, + }) + } + _ => Err(()), + }, + _ => Err(()), + } } } -impl From for NetworkScanResponse { - fn from(entry: scanner::ScanEntry) -> Self { - match entry { - scanner::ScanEntry::ScanEvent(event) => Self::Event(event.into()), - scanner::ScanEntry::Result { - addr, - hostname, - port, - service_type, - } => Self::new(addr, port, hostname, service_type), +#[derive(Debug, Clone, Copy)] +pub struct EventFilter { + // Emit ping event or not, we'll need ping event for tcp knock anyway, so we don't turn it off, instead, we just ignore the event + enable_ping_event: bool, + // Emit ping/tcp knock event failure or not + enable_failure: bool, +} + +impl EventFilter { + fn should_emit(&self, response: &NetworkScanResponse) -> bool { + match response { + NetworkScanResponse::Event(scan_event) => match scan_event { + ScanEvent::Ping { status, .. } if self.enable_ping_event => { + if matches!(status, Status::Failed) && !self.enable_failure { + return false; + } + + return true; + } + ScanEvent::Host { .. } => return true, + _ => return false, + }, + NetworkScanResponse::Entry { status, .. } => { + if matches!(status, Status::Failed) && !self.enable_failure { + return false; + } + + return true; + } } } } From ecb77c9335a9ab9505fafc7ee9659fb268aefa0a Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:07:03 -0400 Subject: [PATCH 21/33] clippy --- devolutions-gateway/src/api/net.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index b68bd0bcf..7de2a1746 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -420,17 +420,17 @@ impl EventFilter { return false; } - return true; + true } - ScanEvent::Host { .. } => return true, - _ => return false, + ScanEvent::Host { .. } => true, + _ => false, }, NetworkScanResponse::Entry { status, .. } => { if matches!(status, Status::Failed) && !self.enable_failure { return false; } - return true; + true } } } From 6fc5bf4a007a449e19d867dc555ca9dbeddcd2c5 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:18:11 -0400 Subject: [PATCH 22/33] remove unused import --- crates/network-scanner/src/port_discovery.rs | 3 +-- crates/network-scanner/src/scanner.rs | 4 +--- crates/network-scanner/src/task_utils.rs | 4 +--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/crates/network-scanner/src/port_discovery.rs b/crates/network-scanner/src/port_discovery.rs index eb4c0fbd6..91e90fb81 100644 --- a/crates/network-scanner/src/port_discovery.rs +++ b/crates/network-scanner/src/port_discovery.rs @@ -1,8 +1,7 @@ -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; -use anyhow::Context; use network_scanner_net::runtime::Socket2Runtime; use socket2::SockAddr; diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index 29a52e5b1..af10655aa 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -1,5 +1,5 @@ use crate::broadcast::asynchronous::broadcast; -use crate::event_bus::{EventBus, RawIpEvent, ScannerEvent, TypedReceiver}; +use crate::event_bus::{EventBus, RawIpEvent, TypedReceiver}; use crate::ip_utils::IpAddrRange; use crate::mdns::{self, MdnsDaemon, MdnsEvent}; use crate::named_port::MaybeNamedPort; @@ -12,8 +12,6 @@ use std::fmt::Display; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; -use tokio::sync::broadcast; -use tracing::subscriber; use typed_builder::TypedBuilder; /// Represents a network scanner for discovering devices and their services over a network. diff --git a/crates/network-scanner/src/task_utils.rs b/crates/network-scanner/src/task_utils.rs index cdc4ae49a..f1f12c1f0 100644 --- a/crates/network-scanner/src/task_utils.rs +++ b/crates/network-scanner/src/task_utils.rs @@ -6,9 +6,7 @@ use std::time::Duration; use std::future::Future; -use tokio::sync::{broadcast, mpsc}; - -use crate::event_bus::{EventBus, ScannerEvent}; +use crate::event_bus::EventBus; use crate::ip_utils::{get_subnets, IpAddrRange, Subnet}; use crate::mdns::MdnsDaemon; use crate::named_port::MaybeNamedPort; From fa8ef7503eca0bd0d1af3fdd83f9270f9f9f46dc Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:21:37 -0400 Subject: [PATCH 23/33] remove unwanted change --- crates/network-scanner/mdns.log | 38 --------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 crates/network-scanner/mdns.log diff --git a/crates/network-scanner/mdns.log b/crates/network-scanner/mdns.log deleted file mode 100644 index 4560dadbc..000000000 --- a/crates/network-scanner/mdns.log +++ /dev/null @@ -1,38 +0,0 @@ -2025-04-14T18:03:28.995637Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:28.997965Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:28.999188Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.000660Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.001829Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.003185Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.004304Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.005551Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.006921Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.008455Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:29.320413Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.52.205, device_name: "NA-FABUHAMDAN", port: 22, protocol: Some(Ssh) } -2025-04-14T18:03:29.320458Z  INFO main mdns: Found: ServiceResolved { addr: fe80::ca8:e1e3:49ec:4ee9, device_name: "NA-FABUHAMDAN", port: 22, protocol: Some(Ssh) } -2025-04-14T18:03:29.729909Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.53.8, device_name: "Bhuvnesh's Macbook Air", port: 22, protocol: Some(Ssh) } -2025-04-14T18:03:29.729942Z  INFO main mdns: Found: ServiceResolved { addr: fe80::1835:f3e6:f77:76b0, device_name: "Bhuvnesh's Macbook Air", port: 22, protocol: Some(Ssh) } -2025-04-14T18:03:29.999995Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.001023Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.002262Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.003417Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.004401Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.005544Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.006423Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.007592Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.008504Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:30.009507Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.013439Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.014888Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.016450Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.018087Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.019591Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.021033Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.022369Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.023713Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.025334Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.027121Z ERROR mDNS_daemon mdns_sd::service_daemon: Failed to send to [ff02::fb%70]:5353 via Interface { name: "Tailscale", addr: V6(Ifv6Addr { ip: fe80::d8ff:5060:f230:9168, netmask: ::, prefixlen: 64, broadcast: None }), index: Some(70), adapter_name: "{37217669-42DA-4657-A55B-0D995D328250}" }: A socket operation was attempted to an unreachable host. (os error 10065) -2025-04-14T18:03:32.184383Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.52.240, device_name: "GCDWebServer", port: 37848, protocol: Some(Http) } -2025-04-14T18:03:32.184444Z  INFO main mdns: Found: ServiceResolved { addr: fe80::40a:3b61:c55a:f2ff, device_name: "GCDWebServer", port: 37848, protocol: Some(Http) } -2025-04-14T18:03:32.391895Z  INFO main mdns: Found: ServiceResolved { addr: 10.1.52.240, device_name: "JaisonΓÇÖs MacBook Pro", port: 5900, protocol: Some(Vnc) } -2025-04-14T18:03:32.391979Z  INFO main mdns: Found: ServiceResolved { addr: fe80::40a:3b61:c55a:f2ff, device_name: "JaisonΓÇÖs MacBook Pro", port: 5900, protocol: Some(Vnc) } From d7a11bf7a33943516c264f375080043268d702d0 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:25:14 -0400 Subject: [PATCH 24/33] fmt --- crates/network-scanner/examples/port_discovery.rs | 6 ++---- crates/network-scanner/examples/scan.rs | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/network-scanner/examples/port_discovery.rs b/crates/network-scanner/examples/port_discovery.rs index b89d67cce..470b007d4 100644 --- a/crates/network-scanner/examples/port_discovery.rs +++ b/crates/network-scanner/examples/port_discovery.rs @@ -2,10 +2,8 @@ use std::time::Duration; -use network_scanner::{ - named_port::{MaybeNamedPort, NamedPort}, - task_utils::TaskManager, -}; +use network_scanner::named_port::{MaybeNamedPort, NamedPort}; +use network_scanner::task_utils::TaskManager; use tracing::info; #[tokio::main] diff --git a/crates/network-scanner/examples/scan.rs b/crates/network-scanner/examples/scan.rs index d15070d6a..a00dc0601 100644 --- a/crates/network-scanner/examples/scan.rs +++ b/crates/network-scanner/examples/scan.rs @@ -3,12 +3,10 @@ use std::time::Duration; use anyhow::Context; -use network_scanner::{ - event_bus::{ScannerEvent, TypedReceiver}, - named_port::{MaybeNamedPort, NamedPort}, - port_discovery::TcpKnockEvent, - scanner::{DnsEvent, NetworkScanner, NetworkScannerParams, ScannerConfig, ScannerToggles}, -}; +use network_scanner::event_bus::{ScannerEvent, TypedReceiver}; +use network_scanner::named_port::{MaybeNamedPort, NamedPort}; +use network_scanner::port_discovery::TcpKnockEvent; +use network_scanner::scanner::{DnsEvent, NetworkScanner, NetworkScannerParams, ScannerConfig, ScannerToggles}; use tokio::time::timeout; fn main() -> anyhow::Result<()> { From d80e6f3d5a065c6174f68cfb2715715f67665d82 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:27:57 -0400 Subject: [PATCH 25/33] typos --- Cargo.lock | 1 - crates/network-scanner/Cargo.toml | 1 - crates/network-scanner/src/broadcast/asynchronous.rs | 2 +- crates/network-scanner/src/broadcast/mod.rs | 2 +- crates/network-scanner/src/event_bus.rs | 6 +++--- crates/network-scanner/src/mdns.rs | 4 ++-- crates/network-scanner/src/scanner.rs | 4 ++-- 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea58744c4..0b58cb4cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3604,7 +3604,6 @@ dependencies = [ "network-scanner-proto", "parking_lot", "rtnetlink", - "serde", "socket2", "thiserror 1.0.69", "tokio 1.44.2", diff --git a/crates/network-scanner/Cargo.toml b/crates/network-scanner/Cargo.toml index bacd4a9cc..2ab937a61 100644 --- a/crates/network-scanner/Cargo.toml +++ b/crates/network-scanner/Cargo.toml @@ -22,7 +22,6 @@ thiserror = "1" tokio = { version = "1.44", features = ["rt", "sync", "time", "fs"] } tracing = "0.1" typed-builder = "0.19" -serde = "1" derive_more = { version = "2.0.1", features = ["from"] } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/network-scanner/src/broadcast/asynchronous.rs b/crates/network-scanner/src/broadcast/asynchronous.rs index 6ce8e9351..2f9ffc4d7 100644 --- a/crates/network-scanner/src/broadcast/asynchronous.rs +++ b/crates/network-scanner/src/broadcast/asynchronous.rs @@ -35,7 +35,7 @@ pub async fn broadcast( task_manager.spawn_no_sub_task(async move { sender - .send(BroadcastEvent::Start { brodcast_ip: ip }) + .send(BroadcastEvent::Start { broadcast_ip: ip }) .await .context("failed to send broadcast start event")?; diff --git a/crates/network-scanner/src/broadcast/mod.rs b/crates/network-scanner/src/broadcast/mod.rs index db910f194..65b1e4278 100644 --- a/crates/network-scanner/src/broadcast/mod.rs +++ b/crates/network-scanner/src/broadcast/mod.rs @@ -10,7 +10,7 @@ pub use asynchronous::broadcast; #[derive(Debug, Clone)] pub enum BroadcastEvent { - Start { brodcast_ip: Ipv4Addr }, + Start { broadcast_ip: Ipv4Addr }, Entry { ip: Ipv4Addr }, } diff --git a/crates/network-scanner/src/event_bus.rs b/crates/network-scanner/src/event_bus.rs index 3b8f68485..ebe5042c8 100644 --- a/crates/network-scanner/src/event_bus.rs +++ b/crates/network-scanner/src/event_bus.rs @@ -56,7 +56,7 @@ macro_rules! define_splitable { #[derive(Debug, Clone)] pub enum RawIpEvent { Ping(PingEvent), - Boardcast(BroadcastEvent), + Broadcast(BroadcastEvent), } impl TryFrom for TcpKnockEvent { @@ -79,7 +79,7 @@ impl TryFrom for RawIpEvent { fn try_from(value: ScannerEvent) -> Result { match value { ScannerEvent::Ping(ping_event) => Ok(RawIpEvent::Ping(ping_event)), - ScannerEvent::Broadcast(boardcast_event) => Ok(RawIpEvent::Boardcast(boardcast_event)), + ScannerEvent::Broadcast(broadcast_event) => Ok(RawIpEvent::Broadcast(broadcast_event)), _ => Err(()), } } @@ -89,7 +89,7 @@ impl RawIpEvent { pub fn success(&self) -> Option { match self { RawIpEvent::Ping(PingEvent::Success { ip, .. }) => Some(*ip), - RawIpEvent::Boardcast(BroadcastEvent::Entry { ip }) => Some(IpAddr::V4(*ip)), + RawIpEvent::Broadcast(BroadcastEvent::Entry { ip }) => Some(IpAddr::V4(*ip)), _ => None, } } diff --git a/crates/network-scanner/src/mdns.rs b/crates/network-scanner/src/mdns.rs index c7cbb0dbc..94682f0ae 100644 --- a/crates/network-scanner/src/mdns.rs +++ b/crates/network-scanner/src/mdns.rs @@ -19,7 +19,7 @@ impl MdnsDaemon { // Lazy initialization of the service daemon pub fn get_service_daemon(&mut self) -> Result { Ok(match &self.service_daemon { - Some(deamon) => deamon.clone(), + Some(daemon) => daemon.clone(), None => { let service_daemon = mdns_sd::ServiceDaemon::new().with_context(|| "Failed to create service daemon")?; @@ -31,7 +31,7 @@ impl MdnsDaemon { pub fn stop(&self) { let service_daemon = match &self.service_daemon { - Some(deamon) => deamon.clone(), + Some(daemon) => daemon.clone(), None => return, }; diff --git a/crates/network-scanner/src/scanner.rs b/crates/network-scanner/src/scanner.rs index af10655aa..66d4c1fc8 100644 --- a/crates/network-scanner/src/scanner.rs +++ b/crates/network-scanner/src/scanner.rs @@ -31,7 +31,7 @@ impl NetworkScanner { pub fn start(&self) -> anyhow::Result { let mut task_executor = TaskExecutionRunner::new(self.clone())?; - start_try_get_dns_name_for_serivce(&mut task_executor); + start_try_get_dns_name_for_service(&mut task_executor); start_port_scan(&mut task_executor); start_dns_look_up(&mut task_executor); @@ -75,7 +75,7 @@ impl NetworkScanner { return Ok(scanner_stream); - fn start_try_get_dns_name_for_serivce(task_executor: &mut TaskExecutionRunner) { + fn start_try_get_dns_name_for_service(task_executor: &mut TaskExecutionRunner) { task_executor.run( move |TaskExecutionContext { event_bus, ip_cache, .. From 139c728fe9ffd4accb0192e2b22894e12ce40a88 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:29:45 -0400 Subject: [PATCH 26/33] typo --- devolutions-gateway/src/api/net.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 7de2a1746..8d3a3c99c 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -293,16 +293,16 @@ pub enum ScanEvent { #[derive(Debug, Serialize)] #[serde(untagged, rename_all = "lowercase")] -pub enum TcpKockProbe { +pub enum TcpKnockProbe { Number(u16), NamedApplication(Protocol), } -impl From for TcpKockProbe { +impl From for TcpKnockProbe { fn from(port: MaybeNamedPort) -> Self { match port { - MaybeNamedPort::Port(port) => TcpKockProbe::Number(port), - MaybeNamedPort::Named(named_port) => TcpKockProbe::NamedApplication(named_port.into()), + MaybeNamedPort::Port(port) => TcpKnockProbe::Number(port), + MaybeNamedPort::Named(named_port) => TcpKnockProbe::NamedApplication(named_port.into()), } } } @@ -337,7 +337,7 @@ pub enum NetworkScanResponse { ip: IpAddr, #[serde(skip_serializing_if = "Option::is_none")] hostname: Option, - protocol: TcpKockProbe, + protocol: TcpKnockProbe, status: Status, }, } From dc35d0886e6847b3735aa33ffe769d46a29553d9 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:32:39 -0400 Subject: [PATCH 27/33] remove unrelated modification --- devolutions-gateway/src/middleware/auth.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/devolutions-gateway/src/middleware/auth.rs b/devolutions-gateway/src/middleware/auth.rs index 6958d2b77..416051cbf 100644 --- a/devolutions-gateway/src/middleware/auth.rs +++ b/devolutions-gateway/src/middleware/auth.rs @@ -102,6 +102,7 @@ pub async fn auth_middleware( struct TokenQueryParam<'a> { token: &'a str, } + let method = request.method(); let uri_path = request.uri().path(); From eff06527bba64d49fc87123f73ef57d9aaee727f Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:40:15 -0400 Subject: [PATCH 28/33] enable_falure default to false --- devolutions-gateway/src/api/net.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index 8d3a3c99c..fe9d6a08a 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -169,7 +169,7 @@ pub struct NetworkScanQueryParams { #[serde(default = "default_true")] pub enable_resolve_dns: bool, - #[serde(default = "default_true")] + #[serde(default)] pub enable_failure: bool, } From 1028e7538c87cca529a4477f5e11ab44334631a1 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 14 Apr 2025 14:57:18 -0400 Subject: [PATCH 29/33] fix mdns event --- devolutions-gateway/src/api/net.rs | 37 ++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index fe9d6a08a..ce1f26a76 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -365,11 +365,40 @@ impl TryFrom for NetworkScanResponse { ScannerEvent::Dns(DnsEvent::Success { ip, hostname }) => { Ok(NetworkScanResponse::Event(ScanEvent::Host { ip, hostname })) } - ScannerEvent::Mdns(MdnsEvent::ServiceResolved { addr, device_name, .. }) => { - Ok(NetworkScanResponse::Event(ScanEvent::Host { + ScannerEvent::Mdns(MdnsEvent::ServiceResolved { + addr, + device_name, + protocol, + port, + }) => { + let protocol = match protocol { + None => None, + Some(protocol) => Some(match protocol { + scanner::ServiceType::Rdp => Protocol::Rdp, + scanner::ServiceType::Ard => Protocol::Ard, + scanner::ServiceType::Vnc => Protocol::Vnc, + scanner::ServiceType::Ssh => Protocol::Ssh, + scanner::ServiceType::Sftp => Protocol::Sftp, + scanner::ServiceType::Scp => Protocol::Scp, + scanner::ServiceType::Telnet => Protocol::Telnet, + scanner::ServiceType::Http => Protocol::Http, + scanner::ServiceType::Https => Protocol::Https, + scanner::ServiceType::Ldap => Protocol::Ldap, + scanner::ServiceType::Ldaps => Protocol::Ldaps, + }), + }; + + let protocol = match protocol { + Some(protocol) => TcpKnockProbe::NamedApplication(protocol), + None => TcpKnockProbe::Number(port), + }; + + Ok(NetworkScanResponse::Entry { ip: addr, - hostname: device_name, - })) + hostname: Some(device_name), + protocol, + status: Status::Success, + }) } ScannerEvent::NetBios(NetBiosEvent::Success { ip, name }) => { Ok(NetworkScanResponse::Event(ScanEvent::Host { From 8bcf5eb63b88182cfb04e272ba9218db88f19d54 Mon Sep 17 00:00:00 2001 From: Irving Ou Date: Wed, 16 Apr 2025 17:12:01 -0400 Subject: [PATCH 30/33] fix build --- devolutions-gateway/src/api/net.rs | 2 +- devolutions-gateway/src/extract.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index ce1f26a76..ebf60d2c3 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -3,7 +3,7 @@ use crate::http::HttpError; use crate::token::Protocol; use crate::DgwState; use axum::extract::ws::{Message, Utf8Bytes}; -use axum::extract::{RawQuery, WebSocketUpgrade}; +use axum::extract::WebSocketUpgrade; use axum::response::Response; use axum::{Json, Router}; use network_scanner::event_bus::ScannerEvent; diff --git a/devolutions-gateway/src/extract.rs b/devolutions-gateway/src/extract.rs index ab1201303..07d13f308 100644 --- a/devolutions-gateway/src/extract.rs +++ b/devolutions-gateway/src/extract.rs @@ -372,7 +372,6 @@ where pub struct RepeatQuery(pub(crate) T); -#[async_trait] impl FromRequest for RepeatQuery where T: serde::de::DeserializeOwned, @@ -380,7 +379,7 @@ where { type Rejection = HttpError; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req:Request, state: &S) -> Result { let RawQuery(query) = RawQuery::from_request(req, state) .await .map_err(|e| HttpError::bad_request().build(e))?; From 99737eb4d790123045b4744b64aa71b26d888568 Mon Sep 17 00:00:00 2001 From: Irving Ou Date: Wed, 16 Apr 2025 17:34:58 -0400 Subject: [PATCH 31/33] fmt --- devolutions-gateway/src/extract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devolutions-gateway/src/extract.rs b/devolutions-gateway/src/extract.rs index 07d13f308..d61256fc9 100644 --- a/devolutions-gateway/src/extract.rs +++ b/devolutions-gateway/src/extract.rs @@ -379,7 +379,7 @@ where { type Rejection = HttpError; - async fn from_request(req:Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { let RawQuery(query) = RawQuery::from_request(req, state) .await .map_err(|e| HttpError::bad_request().build(e))?; From 891d613513ddb4b259167f6366f456769efe41df Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 17 Apr 2025 12:04:35 -0400 Subject: [PATCH 32/33] review fixes --- crates/network-scanner/src/named_port.rs | 20 +++++------- devolutions-gateway/src/api/net.rs | 40 +++++++++++++----------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/crates/network-scanner/src/named_port.rs b/crates/network-scanner/src/named_port.rs index c3d403fba..2dfc2ab1e 100644 --- a/crates/network-scanner/src/named_port.rs +++ b/crates/network-scanner/src/named_port.rs @@ -5,7 +5,6 @@ use socket2::SockAddr; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NamedPort { - Wayk, Rdp, Ard, Vnc, @@ -14,8 +13,8 @@ pub enum NamedPort { Sftp, Scp, Telnet, - Winrmhttppwsh, - Winrmhttpspwsh, + WinrmHttpPwsh, + WinrmHttpsPwsh, Http, Https, Ldap, @@ -25,7 +24,6 @@ pub enum NamedPort { impl Into for &NamedPort { fn into(self) -> u16 { match self { - NamedPort::Wayk => 12876, NamedPort::Rdp => 3389, NamedPort::Ard => 5900, NamedPort::Vnc => 5900, @@ -34,8 +32,8 @@ impl Into for &NamedPort { NamedPort::Sftp => 22, NamedPort::Scp => 22, NamedPort::Telnet => 23, - NamedPort::Winrmhttppwsh => 5985, - NamedPort::Winrmhttpspwsh => 5986, + NamedPort::WinrmHttpPwsh => 5985, + NamedPort::WinrmHttpsPwsh => 5986, NamedPort::Http => 80, NamedPort::Https => 443, NamedPort::Ldap => 389, @@ -49,13 +47,12 @@ impl TryFrom for NamedPort { fn try_from(value: u16) -> Result { match value { - 12876 => Ok(NamedPort::Wayk), 3389 => Ok(NamedPort::Rdp), 5900 => Ok(NamedPort::Ard), // Note: Same as VNC, will return Ard by convention 22 => Ok(NamedPort::Ssh), // Note: Same as Sshpwsh/Sftp/Scp, will return Ssh by convention 23 => Ok(NamedPort::Telnet), - 5985 => Ok(NamedPort::Winrmhttppwsh), - 5986 => Ok(NamedPort::Winrmhttpspwsh), + 5985 => Ok(NamedPort::WinrmHttpPwsh), + 5986 => Ok(NamedPort::WinrmHttpsPwsh), 80 => Ok(NamedPort::Http), 443 => Ok(NamedPort::Https), 389 => Ok(NamedPort::Ldap), @@ -70,7 +67,6 @@ impl TryFrom<&str> for NamedPort { fn try_from(value: &str) -> Result { match value { - "wayk" => Ok(NamedPort::Wayk), "rdp" => Ok(NamedPort::Rdp), "ard" => Ok(NamedPort::Ard), "vnc" => Ok(NamedPort::Vnc), @@ -79,8 +75,8 @@ impl TryFrom<&str> for NamedPort { "sftp" => Ok(NamedPort::Sftp), "scp" => Ok(NamedPort::Scp), "telnet" => Ok(NamedPort::Telnet), - "winrmhttppwsh" => Ok(NamedPort::Winrmhttppwsh), - "winrmhttpspwsh" => Ok(NamedPort::Winrmhttpspwsh), + "winrmhttppwsh" => Ok(NamedPort::WinrmHttpPwsh), + "winrmhttpspwsh" => Ok(NamedPort::WinrmHttpsPwsh), "http" => Ok(NamedPort::Http), "https" => Ok(NamedPort::Https), "ldap" => Ok(NamedPort::Ldap), diff --git a/devolutions-gateway/src/api/net.rs b/devolutions-gateway/src/api/net.rs index ebf60d2c3..dbdef54af 100644 --- a/devolutions-gateway/src/api/net.rs +++ b/devolutions-gateway/src/api/net.rs @@ -15,7 +15,7 @@ use network_scanner::netbios::NetBiosEvent; use network_scanner::ping::PingEvent; use network_scanner::port_discovery::TcpKnockEvent; use network_scanner::scanner::{self, DnsEvent, NetworkScannerParams, ScannerConfig, TcpKnockWithHost}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::Duration; @@ -38,10 +38,11 @@ pub async fn handle_network_scan( ws: WebSocketUpgrade, RepeatQuery(query): RepeatQuery, ) -> Result { - let (scanner_params, filter) = query.try_into().map_err(|e| { - error!(error = format!("{e:#}"), "Failed to parse query parameters"); - HttpError::bad_request().build(e) - })?; + let (scanner_params, filter) = query.try_into().map_err( + HttpError::bad_request() + .with_msg("failed to parse query parameters") + .err(), + )?; let scanner = scanner::NetworkScanner::new(scanner_params).map_err(|e| { error!(error = format!("{e:#}"), "Failed to create network scanner"); @@ -147,7 +148,7 @@ pub struct NetworkScanQueryParams { pub ranges: Vec, /// The ports to scan. If not specified, the default ports will be used. #[serde(default, rename = "probe")] - pub probes: Vec, + pub probes: Vec, /// Enable the emission of ScanEvent::Ping for status start #[serde(default)] @@ -169,6 +170,7 @@ pub struct NetworkScanQueryParams { #[serde(default = "default_true")] pub enable_resolve_dns: bool, + /// Enable Tcp port knocking and ping failure event #[serde(default)] pub enable_failure: bool, } @@ -194,6 +196,16 @@ impl TryFrom<&str> for Probe { } } +impl<'de> Deserialize<'de> for Probe { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Probe::try_from(value.as_str()).map_err(serde::de::Error::custom) + } +} + const COMMON_PORTS: [u16; 11] = [22, 23, 80, 443, 389, 636, 3283, 3389, 5900, 5985, 5986]; impl TryFrom for (NetworkScannerParams, EventFilter) { @@ -203,16 +215,7 @@ impl TryFrom for (NetworkScannerParams, EventFilter) { let probe: Vec = match val.probes.len() { 0 => COMMON_PORTS.iter().map(|port| Probe::Port((*port).into())).collect(), - _ => val - .probes - .iter() - .map(|probe| { - Probe::try_from(probe.as_str()).map_err(|e| { - error!(error = format!("{e:#}"), "Failed to parse probe"); - anyhow::anyhow!("Failed to parse probe") - }) - }) - .collect::, anyhow::Error>>()?, + _ => val.probes, }; let enable_ping_event = probe.iter().any(|probe| matches!(probe, Probe::Ping)); @@ -310,7 +313,6 @@ impl From for TcpKnockProbe { impl From for Protocol { fn from(named_port: NamedPort) -> Self { match named_port { - NamedPort::Wayk => Protocol::Wayk, NamedPort::Rdp => Protocol::Rdp, NamedPort::Ard => Protocol::Ard, NamedPort::Vnc => Protocol::Vnc, @@ -319,8 +321,8 @@ impl From for Protocol { NamedPort::Sftp => Protocol::Sftp, NamedPort::Scp => Protocol::Scp, NamedPort::Telnet => Protocol::Telnet, - NamedPort::Winrmhttppwsh => Protocol::WinrmHttpPwsh, - NamedPort::Winrmhttpspwsh => Protocol::WinrmHttpsPwsh, + NamedPort::WinrmHttpPwsh => Protocol::WinrmHttpPwsh, + NamedPort::WinrmHttpsPwsh => Protocol::WinrmHttpsPwsh, NamedPort::Http => Protocol::Http, NamedPort::Https => Protocol::Https, NamedPort::Ldap => Protocol::Ldap, From 48e6480aaf25943b62b28955106215857e00309d Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 17 Apr 2025 12:10:00 -0400 Subject: [PATCH 33/33] review fix --- Cargo.lock | 21 --------------------- crates/network-scanner/Cargo.toml | 1 - crates/network-scanner/src/event_bus.rs | 9 +++++++-- crates/network-scanner/src/named_port.rs | 15 +++++++++++++-- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b58cb4cf..d2ba878cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,26 +1112,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2 1.0.94", - "quote 1.0.40", - "syn 2.0.100", -] - [[package]] name = "des" version = "0.8.1" @@ -3593,7 +3573,6 @@ version = "0.0.0" dependencies = [ "anyhow", "crossbeam", - "derive_more", "dns-lookup", "futures-util", "ipconfig", diff --git a/crates/network-scanner/Cargo.toml b/crates/network-scanner/Cargo.toml index 2ab937a61..e0700be1f 100644 --- a/crates/network-scanner/Cargo.toml +++ b/crates/network-scanner/Cargo.toml @@ -22,7 +22,6 @@ thiserror = "1" tokio = { version = "1.44", features = ["rt", "sync", "time", "fs"] } tracing = "0.1" typed-builder = "0.19" -derive_more = { version = "2.0.1", features = ["from"] } [target.'cfg(target_os = "windows")'.dependencies] ipconfig = "0.3" diff --git a/crates/network-scanner/src/event_bus.rs b/crates/network-scanner/src/event_bus.rs index ebe5042c8..c2d75ed82 100644 --- a/crates/network-scanner/src/event_bus.rs +++ b/crates/network-scanner/src/event_bus.rs @@ -1,6 +1,5 @@ use std::net::IpAddr; -use derive_more::From; use tokio::sync::{broadcast, mpsc}; use crate::broadcast::BroadcastEvent; @@ -16,7 +15,7 @@ macro_rules! define_scanner_event { ) => { pub trait Sendable {} - #[derive(Debug, Clone, From)] + #[derive(Debug, Clone)] pub enum ScannerEvent { $( $variant($typ), @@ -25,6 +24,12 @@ macro_rules! define_scanner_event { $( impl Sendable for $typ {} + + impl From<$typ> for ScannerEvent { + fn from(event: $typ) -> Self { + ScannerEvent::$variant(event) + } + } )* } diff --git a/crates/network-scanner/src/named_port.rs b/crates/network-scanner/src/named_port.rs index 2dfc2ab1e..70ebca45b 100644 --- a/crates/network-scanner/src/named_port.rs +++ b/crates/network-scanner/src/named_port.rs @@ -1,6 +1,5 @@ use std::net::{IpAddr, SocketAddr}; -use derive_more::From; use socket2::SockAddr; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -86,12 +85,24 @@ impl TryFrom<&str> for NamedPort { } } -#[derive(Debug, Clone, From)] +#[derive(Debug, Clone)] pub enum MaybeNamedPort { Named(NamedPort), Port(u16), } +impl From for MaybeNamedPort { + fn from(named_port: NamedPort) -> Self { + MaybeNamedPort::Named(named_port) + } +} + +impl From for MaybeNamedPort { + fn from(port: u16) -> Self { + MaybeNamedPort::Port(port) + } +} + impl TryFrom<&str> for MaybeNamedPort { type Error = anyhow::Error;