From 767befab806c79c31bc75bcd5d0cc614a90837bd Mon Sep 17 00:00:00 2001 From: Juan Jose Nicola Date: Wed, 27 Nov 2024 10:23:34 -0300 Subject: [PATCH 1/4] Add: PoC for alive test. It only support icmpv4 For testing, compile with nasl-builtin-raw-ip feature and run `sudo target/debug/scannerctl alivetest --icmp -t 192.168.0.1,192.168.0.2,192.168.0.3 --timeout 5000 --verbose` --- rust/Cargo.lock | 2 + rust/Cargo.toml | 4 +- rust/src/alive_test/README.md | 7 + rust/src/alive_test/alive_test.rs | 319 +++++++++++++++++++++++++++ rust/src/alive_test/error.rs | 27 +++ rust/src/alive_test/mod.rs | 13 ++ rust/src/lib.rs | 1 + rust/src/models/target.rs | 12 + rust/src/nasl/builtin/mod.rs | 2 +- rust/src/nasl/builtin/raw_ip/mod.rs | 2 +- rust/src/nasl/mod.rs | 5 + rust/src/scannerctl/alivetest/mod.rs | 81 +++++++ rust/src/scannerctl/main.rs | 11 +- 13 files changed, 481 insertions(+), 5 deletions(-) create mode 100644 rust/src/alive_test/README.md create mode 100644 rust/src/alive_test/alive_test.rs create mode 100644 rust/src/alive_test/error.rs create mode 100644 rust/src/alive_test/mod.rs create mode 100644 rust/src/scannerctl/alivetest/mod.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9ab46418f..9d44b42eb 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2650,10 +2650,12 @@ checksum = "99e935fc73d54a89fff576526c2ccd42bbf8247aae05b358693475b14fd4ff79" dependencies = [ "bitflags 1.3.2", "errno 0.2.8", + "futures", "libc", "libloading 0.6.7", "pkg-config", "regex", + "tokio", "windows-sys 0.36.1", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2b6ac5cb7..fedcbc61e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -80,7 +80,8 @@ walkdir = "2" x509-certificate = "0.23.1" x509-parser = "0.16.0" -pcap = { version = "1.0.0", optional = true } +rayon = { version = "1.8.0", optional = true } +pcap = { version = "1.0.0", optional = true, features = ["all-features", "capture-stream"] } pnet_base = { version = "0.33.0", optional = true } pnet = { version = "0.33.0", optional = true } pnet_macros = { version = "0.33.0", optional = true } @@ -130,7 +131,6 @@ enforce-no-trailing-arguments = [] [workspace.dependencies] tokio = { version = "1.39.3", features = ["full"] } -futures = "0.3.30" [workspace.package] version = "0.1.0" diff --git a/rust/src/alive_test/README.md b/rust/src/alive_test/README.md new file mode 100644 index 000000000..7b4896e99 --- /dev/null +++ b/rust/src/alive_test/README.md @@ -0,0 +1,7 @@ +# Alive Test + +This is the rust library implementation of Boreas from https://github.com/greenbone/gvm-libs/ and https://github.com/greenbone/boreas/ + +Alive Test is a library to scan for alive hosts as well as a command line tool integrated in scannerctl, which replaces the former Boreas library and command line tool written in C. + +It supports IPv4 and IPv6 address ranges and allows to exclude certain addresses from a range. The alive ping tests support ICMP, TCP-ACK, TCP-SYN and ARP and any combination. For TCP ping an individual port list can be applied. diff --git a/rust/src/alive_test/alive_test.rs b/rust/src/alive_test/alive_test.rs new file mode 100644 index 000000000..283bd6531 --- /dev/null +++ b/rust/src/alive_test/alive_test.rs @@ -0,0 +1,319 @@ +// SPDX-FileCopyrightText: 2024 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +use crate::alive_test::AliveTestError; +use crate::models::{AliveTestMethods, Host}; + +use futures::StreamExt; +use pnet::packet::ip::IpNextHeaderProtocols; +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::time::sleep; + +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, +}; + +use pcap::{Active, Capture, Inactive, PacketCodec, PacketStream}; +use pnet::packet::{ + self, + icmp::*, + ip::IpNextHeaderProtocol, + ipv4::{checksum, Ipv4Packet}, +}; + +use socket2::{Domain, Protocol, Socket}; + +use tracing::debug; + +/// Define IPPROTO_RAW +const IPPROTO_RAW: i32 = 255; + +/// Default timeout +const DEFAULT_TIMEOUT: u64 = 5000; + +enum AliveTestCtl { + Stop, + // (IP and succesful detection method) + Alive(String, AliveTestMethods), +} + +fn new_raw_socket() -> Result { + match Socket::new_raw( + Domain::IPV4, + socket2::Type::RAW, + Some(Protocol::from(IPPROTO_RAW)), + ) { + Ok(s) => Ok(s), + Err(_) => Err(AliveTestError::NoSocket("no socket".to_string())), + } +} + +async fn forge_icmp(dst: IpAddr) -> Result, AliveTestError> { + if dst.is_ipv6() { + return Err(AliveTestError::InvalidDestinationAddr( + "Invalid destination address".to_string(), + )); + } + + let mut buf = vec![0; 8]; // icmp length + let mut icmp_pkt = packet::icmp::MutableIcmpPacket::new(&mut buf) + .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + + icmp_pkt.set_icmp_type(packet::icmp::IcmpTypes::EchoRequest); + icmp_pkt.set_icmp_code(packet::icmp::IcmpCode::new(0u8)); + let icmp_aux = IcmpPacket::new(&buf) + .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + let chksum = pnet::packet::icmp::checksum(&icmp_aux); + + let mut icmp_pkt = packet::icmp::MutableIcmpPacket::new(&mut buf) + .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + icmp_pkt.set_checksum(chksum); + + let mut ip_buf = vec![0; 20]; //IP length + ip_buf.append(&mut buf); + let total_length = ip_buf.len(); + let mut pkt = packet::ipv4::MutableIpv4Packet::new(&mut ip_buf) + .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + + pkt.set_header_length(5_u8); + pkt.set_next_level_protocol(IpNextHeaderProtocol(0x01)); + pkt.set_ttl(255_u8); + match dst.to_string().parse::() { + Ok(ip) => { + pkt.set_destination(ip); + } + Err(_) => { + return Err(AliveTestError::InvalidDestinationAddr( + "Invalid destination address".to_string(), + )); + } + }; + + pkt.set_version(4u8); + pkt.set_total_length(total_length as u16); + let chksum = checksum(&pkt.to_immutable()); + pkt.set_checksum(chksum); + + Ok(ip_buf) +} + +/// Send an icmp packet +async fn alive_test_send_icmp_packet(icmp: Vec) -> Result<(), AliveTestError> { + tracing::debug!("starting sending packet"); + let soc = new_raw_socket()?; + + if let Err(_) = soc.set_header_included_v4(true) { + return Err(AliveTestError::NoSocket("no socket".to_string())); + }; + + let icmp_raw = &icmp as &[u8]; + let packet = packet::ipv4::Ipv4Packet::new(icmp_raw).ok_or_else(|| { + AliveTestError::CreateIcmpPacket("Not possible to create icmp packet".to_string()) + })?; + + let sock_str = format!("{}:{}", &packet.get_destination().to_string().as_str(), 0); + let sockaddr = SocketAddr::from_str(&sock_str) + .map_err(|_| AliveTestError::NoSocket("no socket".to_string()))?; + let sockaddr = socket2::SockAddr::from(sockaddr); + + match soc.send_to(icmp_raw, &sockaddr) { + Ok(b) => { + debug!("Sent {} bytes", b); + } + Err(e) => { + return Err(AliveTestError::SendPacket(e.to_string())); + } + }; + Ok(()) +} + +struct PktCodec; + +impl PacketCodec for PktCodec { + type Item = Box<[u8]>; + + fn decode(&mut self, packet: pcap::Packet) -> Self::Item { + packet.data.into() + } +} + +fn pkt_stream( + capture_inactive: Capture, +) -> Result, pcap::Error> { + let cap = capture_inactive + .promisc(true) + .immediate_mode(true) + .timeout(5000) + .immediate_mode(true) + .open()? + .setnonblock()?; + cap.stream(PktCodec) +} + +enum EtherTypes { + EtherTypeIp, + #[allow(dead_code)] + EtherTypeIp6, + #[allow(dead_code)] + EtherTypeArp, +} + +impl TryFrom<&[u8]> for EtherTypes { + type Error = AliveTestError; + + fn try_from(val: &[u8]) -> Result { + match val { + &[0x08, 0x00] => Ok(EtherTypes::EtherTypeIp), + &[0x08, 0x06] => Ok(EtherTypes::EtherTypeIp), + &[0x08, 0xDD] => Ok(EtherTypes::EtherTypeIp), + _ => Err(AliveTestError::InvalidEtherType( + "Invalid EtherType".to_string(), + )), + } + } +} + +fn process_packet(packet: &[u8]) -> Result, AliveTestError> { + if packet.len() <= 16 { + return Err(AliveTestError::CreateIcmpPacket( + "Invalid IP packet".to_string(), + )); + }; + let ether_type = &packet[14..16]; + let ether_type = EtherTypes::try_from(ether_type)?; + match ether_type { + EtherTypes::EtherTypeIp => { + let pkt = Ipv4Packet::new(&packet[16..]) + .ok_or_else(|| AliveTestError::CreateIcmpPacket("Invalid IP packet".to_string()))?; + let hl = pkt.get_header_length() as usize; + if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { + let icmp_pkt = IcmpPacket::new(&packet[hl..]).ok_or_else(|| { + AliveTestError::CreateIcmpPacket("invalid icmp reply".to_string()) + })?; + if icmp_pkt.get_icmp_type() == IcmpTypes::EchoReply { + if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { + return Ok(Some(AliveTestCtl::Alive( + pkt.get_source().to_string(), + AliveTestMethods::Icmp, + ))); + } + } + } + } + EtherTypes::EtherTypeIp6 => unimplemented!(), + EtherTypes::EtherTypeArp => unimplemented!(), + } + + Ok(None) +} + +pub struct Scanner { + target: Vec, + methods: Vec, + timeout: Option, +} + +impl Scanner { + pub fn new(target: Vec, methods: Vec, timeout: Option) -> Self { + Self { + target, + methods, + timeout, + } + } + + pub async fn run_alive_test(&self) -> Result<(), AliveTestError> { + // TODO: Replace with a Storage type to store the alive host list + let mut alive = Vec::<(String, String)>::new(); + + if self.methods.contains(&AliveTestMethods::ConsiderAlive) { + self.target.iter().for_each(|t| { + alive.push((t.clone(), AliveTestMethods::ConsiderAlive.to_string())); + println!("{t} via {}", AliveTestMethods::ConsiderAlive.to_string()) + }); + + return Ok(()); + } + + let capture_inactive = Capture::from_device("any").unwrap(); + let trgt = self.target.clone(); + + let (tx_ctl, mut rx_ctl): (mpsc::Sender, mpsc::Receiver) = + mpsc::channel(1024); + let (tx_msg, mut rx_msg): (mpsc::Sender, mpsc::Receiver) = + mpsc::channel(1024); + + // spawn the process for reading packets from the interfaces. + let worker_capture_handle = tokio::spawn(async move { + let mut stream = pkt_stream(capture_inactive).expect("Failed to create stream"); + tracing::debug!("Start capture loop"); + + loop { + tokio::select! { + packet = stream.next() => { // packet is Option> + if let Some(Ok(data)) = packet { + if let Ok(Some(AliveTestCtl::Alive(addr, method))) = process_packet(&data) { + tx_msg.send(AliveTestCtl::Alive(addr, method)).await.unwrap() + } + } + }, + Some(ctl) = rx_ctl.recv() => { + match ctl { + AliveTestCtl::Stop =>{ + break; + }, + _ => () + }; + }, + } + } + tracing::debug!("leaving the capture thread"); + }); + + let timeout = if self.timeout.is_some() { + self.timeout.unwrap() + } else { + DEFAULT_TIMEOUT + }; + + let methods = self.methods.clone(); + let worker_handle = tokio::spawn(async move { + let mut count = 0; + + if methods.contains(&AliveTestMethods::Icmp) { + for t in trgt.iter() { + count += 1; + let dst_ip = IpAddr::from_str(t).unwrap(); + let icmp = forge_icmp(dst_ip).await.unwrap(); + let _ = alive_test_send_icmp_packet(icmp).await; + } + } + if methods.contains(&AliveTestMethods::TcpSyn) { + //unimplemented + } + if methods.contains(&AliveTestMethods::TcpAck) { + //unimplemented + } + if methods.contains(&AliveTestMethods::Arp) { + //unimplemented + } + + tracing::debug!("Finished sending {count} packets"); + sleep(Duration::from_millis(timeout)).await; + let _ = tx_ctl.send(AliveTestCtl::Stop).await; + }); + + while let Some(AliveTestCtl::Alive(addr, method)) = rx_msg.recv().await { + alive.push((addr.clone(), method.to_string())); + println!("{addr} via {method:?}"); + } + + let _ = worker_handle.await; + let _ = worker_capture_handle.await; + + Ok(()) + } +} diff --git a/rust/src/alive_test/error.rs b/rust/src/alive_test/error.rs new file mode 100644 index 000000000..888a6ea58 --- /dev/null +++ b/rust/src/alive_test/error.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +use thiserror::Error; + +/// Errors that might occur, when working with the alive test library. +#[derive(Debug, Error)] +pub enum Error { + /// Not possible to create a socket + #[error("Not possible to create a socket")] + NoSocket(String), + #[error("Not possible to create an ICMP packet")] + CreateIcmpPacket(String), + #[error("Invalid destination Address")] + InvalidDestinationAddr(String), + #[error("Route unavailable")] + UnavailableRoute(String), + #[error("There was an error")] + Custom(String), + #[error("pcap error")] + PcapError(String), + #[error("send_packet")] + SendPacket(String), + #[error("Invalid EtherType")] + InvalidEtherType(String), +} diff --git a/rust/src/alive_test/mod.rs b/rust/src/alive_test/mod.rs new file mode 100644 index 000000000..5f595d6bd --- /dev/null +++ b/rust/src/alive_test/mod.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +#![doc = include_str!("README.md")] + +#[cfg(feature = "nasl-builtin-raw-ip")] +pub mod alive_test; +mod error; + +#[cfg(feature = "nasl-builtin-raw-ip")] +pub use alive_test::Scanner; +pub use error::Error as AliveTestError; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f2b5d1e7b..750153761 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception +pub mod alive_test; pub mod feed; pub mod models; pub mod nasl; diff --git a/rust/src/models/target.rs b/rust/src/models/target.rs index 3aee3d597..4f947b092 100644 --- a/rust/src/models/target.rs +++ b/rust/src/models/target.rs @@ -73,3 +73,15 @@ impl TryFrom for AliveTestMethods { } } } + +impl ToString for AliveTestMethods { + fn to_string(&self) -> String { + match self { + AliveTestMethods::TcpAck => "tcp_ack".to_string(), + AliveTestMethods::Icmp => "icmp".to_string(), + AliveTestMethods::Arp => "arp".to_string(), + AliveTestMethods::ConsiderAlive => "consider_alive".to_string(), + AliveTestMethods::TcpSyn => "tcp_syn".to_string(), + } + } +} diff --git a/rust/src/nasl/builtin/mod.rs b/rust/src/nasl/builtin/mod.rs index 595e38b76..ac026261d 100644 --- a/rust/src/nasl/builtin/mod.rs +++ b/rust/src/nasl/builtin/mod.rs @@ -16,7 +16,7 @@ mod knowledge_base; mod misc; mod network; #[cfg(feature = "nasl-builtin-raw-ip")] -mod raw_ip; +pub mod raw_ip; mod regex; mod report_functions; mod ssh; diff --git a/rust/src/nasl/builtin/raw_ip/mod.rs b/rust/src/nasl/builtin/raw_ip/mod.rs index 25924f4d4..8afe8826c 100644 --- a/rust/src/nasl/builtin/raw_ip/mod.rs +++ b/rust/src/nasl/builtin/raw_ip/mod.rs @@ -4,7 +4,7 @@ mod frame_forgery; mod packet_forgery; -mod raw_ip_utils; +pub mod raw_ip_utils; use std::io; use crate::nasl::utils::{IntoFunctionSet, NaslVars, StoredFunctionSet}; diff --git a/rust/src/nasl/mod.rs b/rust/src/nasl/mod.rs index d4f3876f9..f0a590cc5 100644 --- a/rust/src/nasl/mod.rs +++ b/rust/src/nasl/mod.rs @@ -10,6 +10,11 @@ pub mod utils; #[cfg(test)] pub mod test_utils; +#[cfg(feature = "nasl-builtin-raw-ip")] +pub mod raw_ip_utils { + pub use super::builtin::raw_ip::raw_ip_utils; +} + pub mod prelude { pub use super::builtin::BuiltinError; pub use super::builtin::ContextFactory; diff --git a/rust/src/scannerctl/alivetest/mod.rs b/rust/src/scannerctl/alivetest/mod.rs new file mode 100644 index 000000000..0398904c0 --- /dev/null +++ b/rust/src/scannerctl/alivetest/mod.rs @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Greenbone AG +// +// SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +//pub mod alivetest; + +use clap::{arg, Arg, ArgAction, Command}; +// re-export to work around name conflict + +use scannerlib::alive_test::Scanner; +use scannerlib::models::{AliveTestMethods, Host}; + +use crate::CliError; + +pub fn extend_args(cmd: Command) -> Command { + cmd.subcommand( + crate::add_verbose( + Command::new("alivetest") + .about("Perform alive test using different strategies against the target") + .arg(arg!(-t --target "List of hosts to test against").required(true)) + // TODO: implement exclude host list. + .arg(arg!(-p --ports "List of ports to test. Default port list is \"80,137,587,3128,8081\"").required(false)) + .arg(Arg::new("timeout") + .long("timeout") + .value_parser(clap::value_parser!(u64)) + .value_name("MILLISECONDS") + .value_parser(clap::value_parser!(u64)) + .help("Wait time for replies")) + .arg(arg!(--icmp "ICMP ping. Default method when no method specified. Supports both IPv4 and IPv6.").required(false).action(ArgAction::SetTrue)) + .arg(arg!(--"tcpsyn" "TCP-SYN ping. Supports both IPv4 and IPv6.").required(false).action(ArgAction::SetTrue)) + .arg(arg!(--"tcpack" "TCP-ACK ping. Supports both IPv4 and IPv6.").required(false).action(ArgAction::SetTrue)) + .arg(arg!(--arp "ARP ping. Supports both IPv4 and IPv6.").required(false).action(ArgAction::SetTrue)) + ) + ) +} + +pub async fn run(root: &clap::ArgMatches) -> Option> { + let (args, _verbose) = crate::get_args_set_logging(root, "alivetest")?; + // TODO: parse target and implement exclude + let target = args.get_one::("target").unwrap(); + let target = target.split(',').map(|x| x.to_string()).collect::>(); + + // TODO: implement parse port list. + let _ports = args + .get_one::("ports") + .cloned() + .unwrap_or("80,137,587,3128,8081".to_string()); + + let timeout = args.get_one::("timeout"); + let icmp = args.get_one::("icmp").cloned().unwrap_or_default(); + let tcp_syn = args.get_one::("tcpsyn").cloned().unwrap_or_default(); + let tcp_ack = args.get_one::("tcpack").cloned().unwrap_or_default(); + let arp = args.get_one::("arp").cloned().unwrap_or_default(); + + let mut methods = vec![]; + if tcp_syn { + methods.push(AliveTestMethods::TcpSyn); + } + if tcp_ack { + methods.push(AliveTestMethods::TcpAck); + } + if arp { + methods.push(AliveTestMethods::Arp); + } + + if icmp || methods.is_empty() { + methods.push(AliveTestMethods::Icmp); + } + + Some(execute(target, timeout.copied(), methods).await) +} + +async fn execute( + target: Vec, + timeout: Option, + methods: Vec, +) -> Result<(), CliError> { + let s = Scanner::new(target, methods, timeout); + let _ = s.run_alive_test().await; + Ok(()) +} diff --git a/rust/src/scannerctl/main.rs b/rust/src/scannerctl/main.rs index 7d3835fec..86d17ea2f 100644 --- a/rust/src/scannerctl/main.rs +++ b/rust/src/scannerctl/main.rs @@ -3,6 +3,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception #![doc = include_str!("README.md")] +#[cfg(feature = "nasl-builtin-raw-ip")] +mod alivetest; mod error; mod execute; mod feed; @@ -65,7 +67,10 @@ async fn main() { let matches = osp::extend_args(matches); let matches = execute::extend_args(matches); let matches = notusupdate::scanner::extend_args(matches); - let matches = feed::extend_args(matches).get_matches(); + let matches = feed::extend_args(matches); + #[cfg(feature = "nasl-builtin-raw-ip")] + let matches = alivetest::extend_args(matches); + let matches = matches.get_matches(); let result = run(&matches).await; match result { @@ -103,6 +108,10 @@ async fn run(matches: &ArgMatches) -> Result<(), CliError> { if let Some(result) = osp::run(matches).await { return result; } + #[cfg(feature = "nasl-builtin-raw-ip")] + if let Some(result) = alivetest::run(matches).await { + return result; + } Err(CliError { filename: "".to_string(), kind: CliErrorKind::Corrupt(format!( From 24bddcc96eea05dbf0ed959f29086007d6f74b7c Mon Sep 17 00:00:00 2001 From: Juan Jose Nicola Date: Wed, 22 Jan 2025 12:40:03 -0300 Subject: [PATCH 2/4] apply suggestion and improvements --- rust/Cargo.lock | 1 + rust/src/alive_test/alive_test.rs | 315 +++++++++++++++--------------- rust/src/alive_test/error.rs | 26 +-- rust/src/lib.rs | 1 + rust/src/models/target.rs | 16 +- 5 files changed, 184 insertions(+), 175 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9d44b42eb..26899e1fd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3569,6 +3569,7 @@ dependencies = [ "pnet_macros_support", "quick-xml", "rand", + "rayon", "rc4", "redis", "regex", diff --git a/rust/src/alive_test/alive_test.rs b/rust/src/alive_test/alive_test.rs index 283bd6531..abec19b01 100644 --- a/rust/src/alive_test/alive_test.rs +++ b/rust/src/alive_test/alive_test.rs @@ -8,7 +8,7 @@ use crate::models::{AliveTestMethods, Host}; use futures::StreamExt; use pnet::packet::ip::IpNextHeaderProtocols; use std::time::Duration; -use tokio::sync::mpsc; +use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::time::sleep; use std::{ @@ -19,76 +19,78 @@ use std::{ use pcap::{Active, Capture, Inactive, PacketCodec, PacketStream}; use pnet::packet::{ self, - icmp::*, + icmp::{IcmpCode, IcmpTypes, MutableIcmpPacket, *}, ip::IpNextHeaderProtocol, - ipv4::{checksum, Ipv4Packet}, + ipv4::{checksum, Ipv4Packet, MutableIpv4Packet}, }; use socket2::{Domain, Protocol, Socket}; -use tracing::debug; - -/// Define IPPROTO_RAW const IPPROTO_RAW: i32 = 255; - -/// Default timeout -const DEFAULT_TIMEOUT: u64 = 5000; - +const DEFAULT_TIMEOUT_MS: u64 = 5000; +const ICMP_LENGTH: usize = 8; +const IP_LENGTH: usize = 20; +const HEADER_LENGTH: u8 = 5; +const DEFAULT_TTL: u8 = 255; +const MIN_ALLOWED_PACKET_LEN: usize = 16; enum AliveTestCtl { Stop, - // (IP and succesful detection method) - Alive(String, AliveTestMethods), + // (IP and successful detection method) + Alive { + ip: String, + detection_method: AliveTestMethods, + }, +} +fn make_mut_icmp_packet(buf: &mut Vec) -> Result { + MutableIcmpPacket::new(buf).ok_or_else(|| AliveTestError::CreateIcmpPacket) } fn new_raw_socket() -> Result { - match Socket::new_raw( + Socket::new_raw( Domain::IPV4, socket2::Type::RAW, Some(Protocol::from(IPPROTO_RAW)), - ) { - Ok(s) => Ok(s), - Err(_) => Err(AliveTestError::NoSocket("no socket".to_string())), - } + ) + .map_err(|e| AliveTestError::NoSocket(e.to_string())) } -async fn forge_icmp(dst: IpAddr) -> Result, AliveTestError> { +fn forge_icmp(dst: IpAddr) -> Result, AliveTestError> { if dst.is_ipv6() { - return Err(AliveTestError::InvalidDestinationAddr( - "Invalid destination address".to_string(), - )); + return Err(AliveTestError::InvalidDestinationAddr); } - let mut buf = vec![0; 8]; // icmp length - let mut icmp_pkt = packet::icmp::MutableIcmpPacket::new(&mut buf) - .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + // Create an icmp packet from a buffer and modify it. + let mut buf = vec![0; ICMP_LENGTH]; + let mut icmp_pkt = make_mut_icmp_packet(&mut buf)?; + icmp_pkt.set_icmp_type(IcmpTypes::EchoRequest); + icmp_pkt.set_icmp_code(IcmpCode::new(0u8)); - icmp_pkt.set_icmp_type(packet::icmp::IcmpTypes::EchoRequest); - icmp_pkt.set_icmp_code(packet::icmp::IcmpCode::new(0u8)); - let icmp_aux = IcmpPacket::new(&buf) - .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + // Require an unmutable ICMP packet for checksum calculation. + // We create an unmutable from the buffer for this purpose + let icmp_aux = IcmpPacket::new(&buf).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; let chksum = pnet::packet::icmp::checksum(&icmp_aux); - let mut icmp_pkt = packet::icmp::MutableIcmpPacket::new(&mut buf) - .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + // Because the buffer of original mutable icmp packet is borrowed, + // create a new mutable icmp packet to set the checksum in the original buffer. + let mut icmp_pkt = make_mut_icmp_packet(&mut buf)?; icmp_pkt.set_checksum(chksum); - let mut ip_buf = vec![0; 20]; //IP length + // We do now the same as above for the IPv4 packet, appending the icmp packet as payload + let mut ip_buf = vec![0; IP_LENGTH]; ip_buf.append(&mut buf); let total_length = ip_buf.len(); - let mut pkt = packet::ipv4::MutableIpv4Packet::new(&mut ip_buf) - .ok_or_else(|| AliveTestError::CreateIcmpPacket("No icmp packet".to_string()))?; + let mut pkt = + MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; - pkt.set_header_length(5_u8); - pkt.set_next_level_protocol(IpNextHeaderProtocol(0x01)); - pkt.set_ttl(255_u8); + pkt.set_header_length(HEADER_LENGTH); + pkt.set_next_level_protocol(IpNextHeaderProtocol(IpNextHeaderProtocols::Icmp.0)); + pkt.set_ttl(DEFAULT_TTL); match dst.to_string().parse::() { Ok(ip) => { pkt.set_destination(ip); } Err(_) => { - return Err(AliveTestError::InvalidDestinationAddr( - "Invalid destination address".to_string(), - )); + return Err(AliveTestError::InvalidDestinationAddr); } }; @@ -101,27 +103,20 @@ async fn forge_icmp(dst: IpAddr) -> Result, AliveTestError> { } /// Send an icmp packet -async fn alive_test_send_icmp_packet(icmp: Vec) -> Result<(), AliveTestError> { +fn alive_test_send_icmp_packet(icmp: Vec) -> Result<(), AliveTestError> { tracing::debug!("starting sending packet"); - let soc = new_raw_socket()?; - - if let Err(_) = soc.set_header_included_v4(true) { - return Err(AliveTestError::NoSocket("no socket".to_string())); - }; + let sock = new_raw_socket()?; + sock.set_header_included_v4(true) + .map_err(|e| AliveTestError::NoSocket(e.to_string()))?; let icmp_raw = &icmp as &[u8]; - let packet = packet::ipv4::Ipv4Packet::new(icmp_raw).ok_or_else(|| { - AliveTestError::CreateIcmpPacket("Not possible to create icmp packet".to_string()) - })?; - - let sock_str = format!("{}:{}", &packet.get_destination().to_string().as_str(), 0); - let sockaddr = SocketAddr::from_str(&sock_str) - .map_err(|_| AliveTestError::NoSocket("no socket".to_string()))?; - let sockaddr = socket2::SockAddr::from(sockaddr); + let packet = + packet::ipv4::Ipv4Packet::new(icmp_raw).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; - match soc.send_to(icmp_raw, &sockaddr) { + let sockaddr = SocketAddr::new(IpAddr::V4(packet.get_destination()), 0); + match sock.send_to(icmp_raw, &sockaddr.into()) { Ok(b) => { - debug!("Sent {} bytes", b); + tracing::debug!("Sent {} bytes", b); } Err(e) => { return Err(AliveTestError::SendPacket(e.to_string())); @@ -146,7 +141,7 @@ fn pkt_stream( let cap = capture_inactive .promisc(true) .immediate_mode(true) - .timeout(5000) + .timeout(DEFAULT_TIMEOUT_MS as i32) .immediate_mode(true) .open()? .setnonblock()?; @@ -155,9 +150,7 @@ fn pkt_stream( enum EtherTypes { EtherTypeIp, - #[allow(dead_code)] EtherTypeIp6, - #[allow(dead_code)] EtherTypeArp, } @@ -167,47 +160,42 @@ impl TryFrom<&[u8]> for EtherTypes { fn try_from(val: &[u8]) -> Result { match val { &[0x08, 0x00] => Ok(EtherTypes::EtherTypeIp), - &[0x08, 0x06] => Ok(EtherTypes::EtherTypeIp), - &[0x08, 0xDD] => Ok(EtherTypes::EtherTypeIp), - _ => Err(AliveTestError::InvalidEtherType( - "Invalid EtherType".to_string(), - )), + &[0x08, 0x06] => Ok(EtherTypes::EtherTypeArp), + &[0x08, 0xDD] => Ok(EtherTypes::EtherTypeIp6), + _ => Err(AliveTestError::InvalidEtherType), } } } +fn process_ip_packet(packet: &[u8]) -> Result, AliveTestError> { + let pkt = Ipv4Packet::new(&packet[16..]).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; + let hl = pkt.get_header_length() as usize; + if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { + let icmp_pkt = + IcmpPacket::new(&packet[hl..]).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; + if icmp_pkt.get_icmp_type() == IcmpTypes::EchoReply { + if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { + return Ok(Some(AliveTestCtl::Alive { + ip: pkt.get_source().to_string(), + detection_method: AliveTestMethods::Icmp, + })); + } + } + } + Ok(None) +} + fn process_packet(packet: &[u8]) -> Result, AliveTestError> { - if packet.len() <= 16 { - return Err(AliveTestError::CreateIcmpPacket( - "Invalid IP packet".to_string(), - )); + if packet.len() <= MIN_ALLOWED_PACKET_LEN { + return Err(AliveTestError::WrongPacketLength); }; let ether_type = &packet[14..16]; let ether_type = EtherTypes::try_from(ether_type)?; match ether_type { - EtherTypes::EtherTypeIp => { - let pkt = Ipv4Packet::new(&packet[16..]) - .ok_or_else(|| AliveTestError::CreateIcmpPacket("Invalid IP packet".to_string()))?; - let hl = pkt.get_header_length() as usize; - if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { - let icmp_pkt = IcmpPacket::new(&packet[hl..]).ok_or_else(|| { - AliveTestError::CreateIcmpPacket("invalid icmp reply".to_string()) - })?; - if icmp_pkt.get_icmp_type() == IcmpTypes::EchoReply { - if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { - return Ok(Some(AliveTestCtl::Alive( - pkt.get_source().to_string(), - AliveTestMethods::Icmp, - ))); - } - } - } - } + EtherTypes::EtherTypeIp => process_ip_packet(&packet), EtherTypes::EtherTypeIp6 => unimplemented!(), EtherTypes::EtherTypeArp => unimplemented!(), } - - Ok(None) } pub struct Scanner { @@ -216,6 +204,66 @@ pub struct Scanner { timeout: Option, } +async fn capture_task( + capture_inactive: Capture, + mut rx_ctl: Receiver, + tx_msg: Sender, +) -> Result<(), AliveTestError> { + let mut stream = pkt_stream(capture_inactive).expect("Failed to create stream"); + tracing::debug!("Start capture loop"); + + loop { + tokio::select! { + packet = stream.next() => { // packet is Option> + if let Some(Ok(data)) = packet { + if let Ok(Some(AliveTestCtl::Alive{ip: addr, detection_method: method})) = process_packet(&data) { + tx_msg.send(AliveTestCtl::Alive{ip: addr, detection_method: method}).await.unwrap() + } + } + }, + ctl = rx_ctl.recv() => { + if let Some(AliveTestCtl::Stop) = ctl { + break; + }; + }, + } + } + tracing::debug!("leaving the capture thread"); + Ok(()) +} + +async fn send_task( + methods: Vec, + trgt: Vec, + timeout: u64, + tx_ctl: Sender, +) -> Result<(), AliveTestError> { + let mut count = 0; + + if methods.contains(&AliveTestMethods::Icmp) { + for t in trgt.iter() { + count += 1; + let dst_ip = IpAddr::from_str(t).expect("Valid IP address"); + let icmp = forge_icmp(dst_ip).expect("Valid ICMP packet"); + let _ = alive_test_send_icmp_packet(icmp); + } + } + if methods.contains(&AliveTestMethods::TcpSyn) { + //unimplemented + } + if methods.contains(&AliveTestMethods::TcpAck) { + //unimplemented + } + if methods.contains(&AliveTestMethods::Arp) { + //unimplemented + } + + tracing::debug!("Finished sending {count} packets"); + sleep(Duration::from_millis(timeout)).await; + let _ = tx_ctl.send(AliveTestCtl::Stop).await; + Ok(()) +} + impl Scanner { pub fn new(target: Vec, methods: Vec, timeout: Option) -> Self { Self { @@ -230,89 +278,46 @@ impl Scanner { let mut alive = Vec::<(String, String)>::new(); if self.methods.contains(&AliveTestMethods::ConsiderAlive) { - self.target.iter().for_each(|t| { + for t in self.target.iter() { alive.push((t.clone(), AliveTestMethods::ConsiderAlive.to_string())); println!("{t} via {}", AliveTestMethods::ConsiderAlive.to_string()) - }); - + } return Ok(()); - } + }; - let capture_inactive = Capture::from_device("any").unwrap(); + let capture_inactive = Capture::from_device("any") + .map_err(|e| AliveTestError::NoValidInterface(e.to_string()))?; let trgt = self.target.clone(); - let (tx_ctl, mut rx_ctl): (mpsc::Sender, mpsc::Receiver) = - mpsc::channel(1024); - let (tx_msg, mut rx_msg): (mpsc::Sender, mpsc::Receiver) = + let (tx_ctl, rx_ctl): (Sender, Receiver) = mpsc::channel(1024); + let (tx_msg, mut rx_msg): (Sender, Receiver) = mpsc::channel(1024); - // spawn the process for reading packets from the interfaces. - let worker_capture_handle = tokio::spawn(async move { - let mut stream = pkt_stream(capture_inactive).expect("Failed to create stream"); - tracing::debug!("Start capture loop"); - - loop { - tokio::select! { - packet = stream.next() => { // packet is Option> - if let Some(Ok(data)) = packet { - if let Ok(Some(AliveTestCtl::Alive(addr, method))) = process_packet(&data) { - tx_msg.send(AliveTestCtl::Alive(addr, method)).await.unwrap() - } - } - }, - Some(ctl) = rx_ctl.recv() => { - match ctl { - AliveTestCtl::Stop =>{ - break; - }, - _ => () - }; - }, - } - } - tracing::debug!("leaving the capture thread"); - }); - - let timeout = if self.timeout.is_some() { - self.timeout.unwrap() - } else { - DEFAULT_TIMEOUT - }; + let capture_handle = tokio::spawn(capture_task(capture_inactive, rx_ctl, tx_msg)); + let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT_MS); let methods = self.methods.clone(); - let worker_handle = tokio::spawn(async move { - let mut count = 0; - - if methods.contains(&AliveTestMethods::Icmp) { - for t in trgt.iter() { - count += 1; - let dst_ip = IpAddr::from_str(t).unwrap(); - let icmp = forge_icmp(dst_ip).await.unwrap(); - let _ = alive_test_send_icmp_packet(icmp).await; - } - } - if methods.contains(&AliveTestMethods::TcpSyn) { - //unimplemented - } - if methods.contains(&AliveTestMethods::TcpAck) { - //unimplemented - } - if methods.contains(&AliveTestMethods::Arp) { - //unimplemented - } + let send_handle = tokio::spawn(send_task(methods, trgt, timeout, tx_ctl)); - tracing::debug!("Finished sending {count} packets"); - sleep(Duration::from_millis(timeout)).await; - let _ = tx_ctl.send(AliveTestCtl::Stop).await; - }); - - while let Some(AliveTestCtl::Alive(addr, method)) = rx_msg.recv().await { + while let Some(AliveTestCtl::Alive { + ip: addr, + detection_method: method, + }) = rx_msg.recv().await + { alive.push((addr.clone(), method.to_string())); println!("{addr} via {method:?}"); } - let _ = worker_handle.await; - let _ = worker_capture_handle.await; + match send_handle.await { + Ok(Ok(())) => (), + Ok(Err(e)) => return Err(e), + Err(e) => return Err(AliveTestError::JoinError(e.to_string())), + }; + match capture_handle.await { + Ok(Ok(())) => (), + Ok(Err(e)) => return Err(e), + Err(e) => return Err(AliveTestError::JoinError(e.to_string())), + }; Ok(()) } diff --git a/rust/src/alive_test/error.rs b/rust/src/alive_test/error.rs index 888a6ea58..92d47d32c 100644 --- a/rust/src/alive_test/error.rs +++ b/rust/src/alive_test/error.rs @@ -8,20 +8,20 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { /// Not possible to create a socket - #[error("Not possible to create a socket")] + #[error("Not possible to create a socket: {0}")] NoSocket(String), - #[error("Not possible to create an ICMP packet")] - CreateIcmpPacket(String), - #[error("Invalid destination Address")] - InvalidDestinationAddr(String), - #[error("Route unavailable")] - UnavailableRoute(String), - #[error("There was an error")] - Custom(String), - #[error("pcap error")] - PcapError(String), - #[error("send_packet")] + #[error("Wrong buffer size. Not possible to create an ICMP packet")] + CreateIcmpPacket, + #[error("It was not possible to parse the destination Address")] + InvalidDestinationAddr, + #[error("Error sending a packet: {0}")] SendPacket(String), #[error("Invalid EtherType")] - InvalidEtherType(String), + InvalidEtherType, + #[error("Wrong packet length")] + WrongPacketLength, + #[error("Pcap: No valid interface {0}")] + NoValidInterface(String), + #[error("Fail spawning the task {0}")] + JoinError(String), } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 750153761..9a125e6dd 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception +#[cfg(feature = "nasl-builtin-raw-ip")] pub mod alive_test; pub mod feed; pub mod models; diff --git a/rust/src/models/target.rs b/rust/src/models/target.rs index 4f947b092..60bf0bb45 100644 --- a/rust/src/models/target.rs +++ b/rust/src/models/target.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception +use std::fmt::{Display, Formatter}; + use super::{credential::Credential, port::Port}; pub type Host = String; @@ -74,14 +76,14 @@ impl TryFrom for AliveTestMethods { } } -impl ToString for AliveTestMethods { - fn to_string(&self) -> String { +impl Display for AliveTestMethods { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - AliveTestMethods::TcpAck => "tcp_ack".to_string(), - AliveTestMethods::Icmp => "icmp".to_string(), - AliveTestMethods::Arp => "arp".to_string(), - AliveTestMethods::ConsiderAlive => "consider_alive".to_string(), - AliveTestMethods::TcpSyn => "tcp_syn".to_string(), + AliveTestMethods::TcpAck => write!(f, "tcp_ack"), + AliveTestMethods::Icmp => write!(f, "icmp"), + AliveTestMethods::Arp => write!(f, "arp"), + AliveTestMethods::ConsiderAlive => write!(f, "consider_alive"), + AliveTestMethods::TcpSyn => write!(f, "tcp_syn"), } } } From 816ca7ea402451d3a9efd1fa219dfc1cdd74c906 Mon Sep 17 00:00:00 2001 From: Juan Jose Nicola Date: Wed, 5 Feb 2025 13:38:42 -0300 Subject: [PATCH 3/4] more suggestions and improvements --- rust/src/alive_test/alive_test.rs | 139 ++++++++++++++---------------- 1 file changed, 63 insertions(+), 76 deletions(-) diff --git a/rust/src/alive_test/alive_test.rs b/rust/src/alive_test/alive_test.rs index abec19b01..ff8001d84 100644 --- a/rust/src/alive_test/alive_test.rs +++ b/rust/src/alive_test/alive_test.rs @@ -6,22 +6,19 @@ use crate::alive_test::AliveTestError; use crate::models::{AliveTestMethods, Host}; use futures::StreamExt; +use pnet::packet::icmp; use pnet::packet::ip::IpNextHeaderProtocols; use std::time::Duration; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::time::sleep; -use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, - str::FromStr, -}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use pcap::{Active, Capture, Inactive, PacketCodec, PacketStream}; use pnet::packet::{ - self, icmp::{IcmpCode, IcmpTypes, MutableIcmpPacket, *}, - ip::IpNextHeaderProtocol, ipv4::{checksum, Ipv4Packet, MutableIpv4Packet}, + Packet, }; use socket2::{Domain, Protocol, Socket}; @@ -33,16 +30,23 @@ const IP_LENGTH: usize = 20; const HEADER_LENGTH: u8 = 5; const DEFAULT_TTL: u8 = 255; const MIN_ALLOWED_PACKET_LEN: usize = 16; -enum AliveTestCtl { - Stop, - // (IP and successful detection method) - Alive { +// This is the only possible code for an echo request +const ICMP_ECHO_REQ_CODE: u8 = 0; +const IP_PPRTO_VERSION_IPV4: u8 = 4; + +pub struct AliveTestCtlStop; + +pub struct AliveHostCtl { ip: String, detection_method: AliveTestMethods, - }, } -fn make_mut_icmp_packet(buf: &mut Vec) -> Result { - MutableIcmpPacket::new(buf).ok_or_else(|| AliveTestError::CreateIcmpPacket) +impl AliveHostCtl { + fn new(ip: String, detection_method: AliveTestMethods) -> Self { + Self { + ip, + detection_method + } + } } fn new_raw_socket() -> Result { @@ -54,67 +58,54 @@ fn new_raw_socket() -> Result { .map_err(|e| AliveTestError::NoSocket(e.to_string())) } -fn forge_icmp(dst: IpAddr) -> Result, AliveTestError> { - if dst.is_ipv6() { - return Err(AliveTestError::InvalidDestinationAddr); - } +fn forge_icmp_packet() -> Result, AliveTestError> { // Create an icmp packet from a buffer and modify it. let mut buf = vec![0; ICMP_LENGTH]; - let mut icmp_pkt = make_mut_icmp_packet(&mut buf)?; + // Since we control the buffer size, we can safely unwrap here. + let mut icmp_pkt = MutableIcmpPacket::new(&mut buf).unwrap(); icmp_pkt.set_icmp_type(IcmpTypes::EchoRequest); - icmp_pkt.set_icmp_code(IcmpCode::new(0u8)); - - // Require an unmutable ICMP packet for checksum calculation. - // We create an unmutable from the buffer for this purpose - let icmp_aux = IcmpPacket::new(&buf).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; - let chksum = pnet::packet::icmp::checksum(&icmp_aux); - - // Because the buffer of original mutable icmp packet is borrowed, - // create a new mutable icmp packet to set the checksum in the original buffer. - let mut icmp_pkt = make_mut_icmp_packet(&mut buf)?; - icmp_pkt.set_checksum(chksum); + icmp_pkt.set_icmp_code(IcmpCode::new(ICMP_ECHO_REQ_CODE)); + icmp_pkt.set_checksum(icmp::checksum(&icmp_pkt.to_immutable())); + Ok(buf) +} +fn forge_ipv4_packet_for_icmp (icmp_buf: &mut Vec, dst: Ipv4Addr) -> Result, AliveTestError> { // We do now the same as above for the IPv4 packet, appending the icmp packet as payload let mut ip_buf = vec![0; IP_LENGTH]; - ip_buf.append(&mut buf); + ip_buf.append(icmp_buf); let total_length = ip_buf.len(); let mut pkt = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; pkt.set_header_length(HEADER_LENGTH); - pkt.set_next_level_protocol(IpNextHeaderProtocol(IpNextHeaderProtocols::Icmp.0)); + pkt.set_next_level_protocol(IpNextHeaderProtocols::Icmp); pkt.set_ttl(DEFAULT_TTL); - match dst.to_string().parse::() { - Ok(ip) => { - pkt.set_destination(ip); - } - Err(_) => { - return Err(AliveTestError::InvalidDestinationAddr); - } - }; + pkt.set_destination(dst); - pkt.set_version(4u8); + pkt.set_version(IP_PPRTO_VERSION_IPV4); pkt.set_total_length(total_length as u16); let chksum = checksum(&pkt.to_immutable()); pkt.set_checksum(chksum); - Ok(ip_buf) + Ipv4Packet::owned(ip_buf).ok_or_else(|| AliveTestError::CreateIcmpPacket) + +} + +fn forge_icmp(dst: Ipv4Addr) -> Result, AliveTestError> { + let mut icmp_buf = forge_icmp_packet()?; + forge_ipv4_packet_for_icmp(&mut icmp_buf, dst) } /// Send an icmp packet -fn alive_test_send_icmp_packet(icmp: Vec) -> Result<(), AliveTestError> { +fn alive_test_send_icmp_packet(icmp: Ipv4Packet<'static>) -> Result<(), AliveTestError> { tracing::debug!("starting sending packet"); let sock = new_raw_socket()?; sock.set_header_included_v4(true) .map_err(|e| AliveTestError::NoSocket(e.to_string()))?; - let icmp_raw = &icmp as &[u8]; - let packet = - packet::ipv4::Ipv4Packet::new(icmp_raw).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; - - let sockaddr = SocketAddr::new(IpAddr::V4(packet.get_destination()), 0); - match sock.send_to(icmp_raw, &sockaddr.into()) { + let sockaddr = SocketAddr::new(IpAddr::V4(icmp.get_destination()), 0); + match sock.send_to(icmp.packet(), &sockaddr.into()) { Ok(b) => { tracing::debug!("Sent {} bytes", b); } @@ -167,7 +158,7 @@ impl TryFrom<&[u8]> for EtherTypes { } } -fn process_ip_packet(packet: &[u8]) -> Result, AliveTestError> { +fn process_ip_packet(packet: &[u8]) -> Result, AliveTestError> { let pkt = Ipv4Packet::new(&packet[16..]).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; let hl = pkt.get_header_length() as usize; if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { @@ -175,7 +166,7 @@ fn process_ip_packet(packet: &[u8]) -> Result, AliveTestErr IcmpPacket::new(&packet[hl..]).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; if icmp_pkt.get_icmp_type() == IcmpTypes::EchoReply { if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { - return Ok(Some(AliveTestCtl::Alive { + return Ok(Some(AliveHostCtl { ip: pkt.get_source().to_string(), detection_method: AliveTestMethods::Icmp, })); @@ -185,7 +176,7 @@ fn process_ip_packet(packet: &[u8]) -> Result, AliveTestErr Ok(None) } -fn process_packet(packet: &[u8]) -> Result, AliveTestError> { +fn process_packet(packet: &[u8]) -> Result, AliveTestError> { if packet.len() <= MIN_ALLOWED_PACKET_LEN { return Err(AliveTestError::WrongPacketLength); }; @@ -206,8 +197,8 @@ pub struct Scanner { async fn capture_task( capture_inactive: Capture, - mut rx_ctl: Receiver, - tx_msg: Sender, + mut rx_ctl: Receiver, + tx_msg: Sender, ) -> Result<(), AliveTestError> { let mut stream = pkt_stream(capture_inactive).expect("Failed to create stream"); tracing::debug!("Start capture loop"); @@ -216,13 +207,13 @@ async fn capture_task( tokio::select! { packet = stream.next() => { // packet is Option> if let Some(Ok(data)) = packet { - if let Ok(Some(AliveTestCtl::Alive{ip: addr, detection_method: method})) = process_packet(&data) { - tx_msg.send(AliveTestCtl::Alive{ip: addr, detection_method: method}).await.unwrap() + if let Ok(Some(alive_host)) = process_packet(&data) { + tx_msg.send(alive_host).await.unwrap() } } }, ctl = rx_ctl.recv() => { - if let Some(AliveTestCtl::Stop) = ctl { + if let Some(AliveTestCtlStop) = ctl { break; }; }, @@ -236,14 +227,19 @@ async fn send_task( methods: Vec, trgt: Vec, timeout: u64, - tx_ctl: Sender, + tx_ctl: Sender, ) -> Result<(), AliveTestError> { let mut count = 0; if methods.contains(&AliveTestMethods::Icmp) { for t in trgt.iter() { count += 1; - let dst_ip = IpAddr::from_str(t).expect("Valid IP address"); + let dst_ip = match t.to_string().parse::() { + Ok(ip) => ip, + Err(_) => { + continue; + } + }; let icmp = forge_icmp(dst_ip).expect("Valid ICMP packet"); let _ = alive_test_send_icmp_packet(icmp); } @@ -260,7 +256,7 @@ async fn send_task( tracing::debug!("Finished sending {count} packets"); sleep(Duration::from_millis(timeout)).await; - let _ = tx_ctl.send(AliveTestCtl::Stop).await; + let _ = tx_ctl.send(AliveTestCtlStop).await; Ok(()) } @@ -275,11 +271,11 @@ impl Scanner { pub async fn run_alive_test(&self) -> Result<(), AliveTestError> { // TODO: Replace with a Storage type to store the alive host list - let mut alive = Vec::<(String, String)>::new(); + let mut alive = Vec::::new(); if self.methods.contains(&AliveTestMethods::ConsiderAlive) { for t in self.target.iter() { - alive.push((t.clone(), AliveTestMethods::ConsiderAlive.to_string())); + alive.push(AliveHostCtl::new(t.clone(), AliveTestMethods::ConsiderAlive)); println!("{t} via {}", AliveTestMethods::ConsiderAlive.to_string()) } return Ok(()); @@ -288,9 +284,8 @@ impl Scanner { let capture_inactive = Capture::from_device("any") .map_err(|e| AliveTestError::NoValidInterface(e.to_string()))?; let trgt = self.target.clone(); - - let (tx_ctl, rx_ctl): (Sender, Receiver) = mpsc::channel(1024); - let (tx_msg, mut rx_msg): (Sender, Receiver) = + let (tx_ctl, rx_ctl): (Sender, Receiver) = mpsc::channel(1024); + let (tx_msg, mut rx_msg): (Sender, Receiver) = mpsc::channel(1024); let capture_handle = tokio::spawn(capture_task(capture_inactive, rx_ctl, tx_msg)); @@ -299,25 +294,17 @@ impl Scanner { let methods = self.methods.clone(); let send_handle = tokio::spawn(send_task(methods, trgt, timeout, tx_ctl)); - while let Some(AliveTestCtl::Alive { + while let Some(AliveHostCtl { ip: addr, detection_method: method, }) = rx_msg.recv().await { - alive.push((addr.clone(), method.to_string())); + alive.push(AliveHostCtl::new(addr.clone(), method.clone())); println!("{addr} via {method:?}"); } - match send_handle.await { - Ok(Ok(())) => (), - Ok(Err(e)) => return Err(e), - Err(e) => return Err(AliveTestError::JoinError(e.to_string())), - }; - match capture_handle.await { - Ok(Ok(())) => (), - Ok(Err(e)) => return Err(e), - Err(e) => return Err(AliveTestError::JoinError(e.to_string())), - }; + let _ = send_handle.await.unwrap(); + let _ = capture_handle.await.unwrap(); Ok(()) } From e8e68642283e4cee715c393eb2ffe66f3f35b435 Mon Sep 17 00:00:00 2001 From: Juan Jose Nicola Date: Fri, 14 Feb 2025 11:22:22 -0300 Subject: [PATCH 4/4] apply more suggestion, fix format and clippy warnings --- rust/Cargo.lock | 1 - rust/Cargo.toml | 2 - rust/src/alive_test/alive_test.rs | 78 +++++++++++++++------------- rust/src/alive_test/error.rs | 6 ++- rust/src/alive_test/mod.rs | 3 +- rust/src/scannerctl/alivetest/mod.rs | 4 +- 6 files changed, 51 insertions(+), 43 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 26899e1fd..9d44b42eb 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3569,7 +3569,6 @@ dependencies = [ "pnet_macros_support", "quick-xml", "rand", - "rayon", "rc4", "redis", "regex", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index fedcbc61e..3e6b1211c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -79,8 +79,6 @@ uuid = { version = "1", features = ["v4", "fast-rng", "serde"] } walkdir = "2" x509-certificate = "0.23.1" x509-parser = "0.16.0" - -rayon = { version = "1.8.0", optional = true } pcap = { version = "1.0.0", optional = true, features = ["all-features", "capture-stream"] } pnet_base = { version = "0.33.0", optional = true } pnet = { version = "0.33.0", optional = true } diff --git a/rust/src/alive_test/alive_test.rs b/rust/src/alive_test/alive_test.rs index ff8001d84..11904552f 100644 --- a/rust/src/alive_test/alive_test.rs +++ b/rust/src/alive_test/alive_test.rs @@ -37,14 +37,14 @@ const IP_PPRTO_VERSION_IPV4: u8 = 4; pub struct AliveTestCtlStop; pub struct AliveHostCtl { - ip: String, - detection_method: AliveTestMethods, + ip: String, + detection_method: AliveTestMethods, } impl AliveHostCtl { fn new(ip: String, detection_method: AliveTestMethods) -> Self { Self { ip, - detection_method + detection_method, } } } @@ -58,8 +58,7 @@ fn new_raw_socket() -> Result { .map_err(|e| AliveTestError::NoSocket(e.to_string())) } - -fn forge_icmp_packet() -> Result, AliveTestError> { +fn forge_icmp_packet() -> Vec { // Create an icmp packet from a buffer and modify it. let mut buf = vec![0; ICMP_LENGTH]; // Since we control the buffer size, we can safely unwrap here. @@ -67,16 +66,16 @@ fn forge_icmp_packet() -> Result, AliveTestError> { icmp_pkt.set_icmp_type(IcmpTypes::EchoRequest); icmp_pkt.set_icmp_code(IcmpCode::new(ICMP_ECHO_REQ_CODE)); icmp_pkt.set_checksum(icmp::checksum(&icmp_pkt.to_immutable())); - Ok(buf) + buf } -fn forge_ipv4_packet_for_icmp (icmp_buf: &mut Vec, dst: Ipv4Addr) -> Result, AliveTestError> { +fn forge_ipv4_packet_for_icmp(icmp_buf: &mut Vec, dst: Ipv4Addr) -> Ipv4Packet<'static> { // We do now the same as above for the IPv4 packet, appending the icmp packet as payload let mut ip_buf = vec![0; IP_LENGTH]; ip_buf.append(icmp_buf); let total_length = ip_buf.len(); - let mut pkt = - MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; + // Since we control the buffer size, we can safely unwrap here. + let mut pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap(); pkt.set_header_length(HEADER_LENGTH); pkt.set_next_level_protocol(IpNextHeaderProtocols::Icmp); @@ -88,12 +87,11 @@ fn forge_ipv4_packet_for_icmp (icmp_buf: &mut Vec, dst: Ipv4Addr) -> Result< let chksum = checksum(&pkt.to_immutable()); pkt.set_checksum(chksum); - Ipv4Packet::owned(ip_buf).ok_or_else(|| AliveTestError::CreateIcmpPacket) - + Ipv4Packet::owned(ip_buf).unwrap() } -fn forge_icmp(dst: Ipv4Addr) -> Result, AliveTestError> { - let mut icmp_buf = forge_icmp_packet()?; +fn forge_icmp(dst: Ipv4Addr) -> Ipv4Packet<'static> { + let mut icmp_buf = forge_icmp_packet(); forge_ipv4_packet_for_icmp(&mut icmp_buf, dst) } @@ -149,28 +147,31 @@ impl TryFrom<&[u8]> for EtherTypes { type Error = AliveTestError; fn try_from(val: &[u8]) -> Result { - match val { - &[0x08, 0x00] => Ok(EtherTypes::EtherTypeIp), - &[0x08, 0x06] => Ok(EtherTypes::EtherTypeArp), - &[0x08, 0xDD] => Ok(EtherTypes::EtherTypeIp6), + match *val { + [0x08, 0x00] => Ok(EtherTypes::EtherTypeIp), + [0x08, 0x06] => Ok(EtherTypes::EtherTypeArp), + [0x08, 0xDD] => Ok(EtherTypes::EtherTypeIp6), _ => Err(AliveTestError::InvalidEtherType), } } } fn process_ip_packet(packet: &[u8]) -> Result, AliveTestError> { - let pkt = Ipv4Packet::new(&packet[16..]).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; + let pkt = Ipv4Packet::new(&packet[16..]).ok_or_else(|| { + AliveTestError::CreateIpPacketFromWrongBufferSize(packet.len() as i64 - 16) + })?; let hl = pkt.get_header_length() as usize; if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { - let icmp_pkt = - IcmpPacket::new(&packet[hl..]).ok_or_else(|| AliveTestError::CreateIcmpPacket)?; - if icmp_pkt.get_icmp_type() == IcmpTypes::EchoReply { - if pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { - return Ok(Some(AliveHostCtl { - ip: pkt.get_source().to_string(), - detection_method: AliveTestMethods::Icmp, - })); - } + let icmp_pkt = IcmpPacket::new(&packet[hl..]).ok_or_else(|| { + AliveTestError::CreateIcmpPacketFromWrongBufferSize(packet[hl..].len() as i64) + })?; + if icmp_pkt.get_icmp_type() == IcmpTypes::EchoReply + && pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp + { + return Ok(Some(AliveHostCtl { + ip: pkt.get_source().to_string(), + detection_method: AliveTestMethods::Icmp, + })); } } Ok(None) @@ -183,7 +184,7 @@ fn process_packet(packet: &[u8]) -> Result, AliveTestError> let ether_type = &packet[14..16]; let ether_type = EtherTypes::try_from(ether_type)?; match ether_type { - EtherTypes::EtherTypeIp => process_ip_packet(&packet), + EtherTypes::EtherTypeIp => process_ip_packet(packet), EtherTypes::EtherTypeIp6 => unimplemented!(), EtherTypes::EtherTypeArp => unimplemented!(), } @@ -240,8 +241,8 @@ async fn send_task( continue; } }; - let icmp = forge_icmp(dst_ip).expect("Valid ICMP packet"); - let _ = alive_test_send_icmp_packet(icmp); + let icmp = forge_icmp(dst_ip); + alive_test_send_icmp_packet(icmp)?; } } if methods.contains(&AliveTestMethods::TcpSyn) { @@ -256,7 +257,8 @@ async fn send_task( tracing::debug!("Finished sending {count} packets"); sleep(Duration::from_millis(timeout)).await; - let _ = tx_ctl.send(AliveTestCtlStop).await; + // Send only returns error if the receiver is closed, which only happens when it panics. + tx_ctl.send(AliveTestCtlStop).await.unwrap(); Ok(()) } @@ -275,8 +277,11 @@ impl Scanner { if self.methods.contains(&AliveTestMethods::ConsiderAlive) { for t in self.target.iter() { - alive.push(AliveHostCtl::new(t.clone(), AliveTestMethods::ConsiderAlive)); - println!("{t} via {}", AliveTestMethods::ConsiderAlive.to_string()) + alive.push(AliveHostCtl::new( + t.clone(), + AliveTestMethods::ConsiderAlive, + )); + println!("{t} via {}", AliveTestMethods::ConsiderAlive) } return Ok(()); }; @@ -284,7 +289,8 @@ impl Scanner { let capture_inactive = Capture::from_device("any") .map_err(|e| AliveTestError::NoValidInterface(e.to_string()))?; let trgt = self.target.clone(); - let (tx_ctl, rx_ctl): (Sender, Receiver) = mpsc::channel(1024); + let (tx_ctl, rx_ctl): (Sender, Receiver) = + mpsc::channel(1024); let (tx_msg, mut rx_msg): (Sender, Receiver) = mpsc::channel(1024); @@ -303,8 +309,8 @@ impl Scanner { println!("{addr} via {method:?}"); } - let _ = send_handle.await.unwrap(); - let _ = capture_handle.await.unwrap(); + send_handle.await.unwrap().unwrap(); + capture_handle.await.unwrap().unwrap(); Ok(()) } diff --git a/rust/src/alive_test/error.rs b/rust/src/alive_test/error.rs index 92d47d32c..e33da6760 100644 --- a/rust/src/alive_test/error.rs +++ b/rust/src/alive_test/error.rs @@ -10,8 +10,10 @@ pub enum Error { /// Not possible to create a socket #[error("Not possible to create a socket: {0}")] NoSocket(String), - #[error("Wrong buffer size. Not possible to create an ICMP packet")] - CreateIcmpPacket, + #[error("Wrong buffer size {0}. Not possible to create an ICMP packet")] + CreateIcmpPacketFromWrongBufferSize(i64), + #[error("Wrong buffer size {0}. Not possible to create an IP packet")] + CreateIpPacketFromWrongBufferSize(i64), #[error("It was not possible to parse the destination Address")] InvalidDestinationAddr, #[error("Error sending a packet: {0}")] diff --git a/rust/src/alive_test/mod.rs b/rust/src/alive_test/mod.rs index 5f595d6bd..0eec94dd5 100644 --- a/rust/src/alive_test/mod.rs +++ b/rust/src/alive_test/mod.rs @@ -5,7 +5,8 @@ #![doc = include_str!("README.md")] #[cfg(feature = "nasl-builtin-raw-ip")] -pub mod alive_test; +#[allow(clippy::module_inception)] +mod alive_test; mod error; #[cfg(feature = "nasl-builtin-raw-ip")] diff --git a/rust/src/scannerctl/alivetest/mod.rs b/rust/src/scannerctl/alivetest/mod.rs index 0398904c0..3e77d5844 100644 --- a/rust/src/scannerctl/alivetest/mod.rs +++ b/rust/src/scannerctl/alivetest/mod.rs @@ -76,6 +76,8 @@ async fn execute( methods: Vec, ) -> Result<(), CliError> { let s = Scanner::new(target, methods, timeout); - let _ = s.run_alive_test().await; + if let Err(e) = s.run_alive_test().await { + tracing::warn!("{e}"); + } Ok(()) }