diff --git a/gping/src/main.rs b/gping/src/main.rs index 9bf552d5a..7d045ff97 100644 --- a/gping/src/main.rs +++ b/gping/src/main.rs @@ -96,6 +96,18 @@ struct Args { #[arg(long, default_value = "0")] horizontal_margin: u16, + /// Use TCP pings instead of ICMP + #[arg(long, short = 't', default_value_t = false)] + tcping: bool, + + /// Treat RST as a drop (default is to not consider RST as pong) + #[arg(long, short = 'r', default_value_t = false)] + no_rst: bool, + + /// TCP port (only used for TCP pings) + #[arg(long, short = 'p', default_value_t = 80)] + port: u16, + #[arg( name = "color", short = 'c', @@ -440,10 +452,16 @@ fn main() -> Result<()> { } else { PingOptions::new(host_or_cmd, interval, interface.clone()) }; + if let Some(ping_args) = &ping_args { ping_opts = ping_opts.with_raw_arguments(ping_args.clone()); } - + if args.tcping { + ping_opts = ping_opts + .with_tcping(true) + .with_port(args.port) + .with_allow_rst(args.no_rst); + } threads.push(start_ping_thread( ping_opts, host_id, diff --git a/pinger/examples/tcp_ping.rs b/pinger/examples/tcp_ping.rs new file mode 100644 index 000000000..e96cccc8b --- /dev/null +++ b/pinger/examples/tcp_ping.rs @@ -0,0 +1,42 @@ + +use pinger::{ping, PingOptions}; +use std::env; +use std::time::Duration; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: tcp_ping [port]"); + return; + } + + let host = &args[1]; + let port: u16 = if args.len() >= 3 { + args[2].parse().expect("Port must be a number") + } else { + 80 // default port + }; + + let opts = PingOptions::new(host, Duration::from_secs(1), None) + .with_tcping(true) // enable TCP ping + .with_port(port) // set port + .with_allow_rst(false); // treat RST as pong + + let rx = ping(opts).expect("Failed to start TCP ping"); + + for result in rx { + match result { + pinger::PingResult::Pong(dur, target) => { + println!("PONG {} in {:?}", target, dur); + } + pinger::PingResult::Timeout(target) => { + println!("TIMEOUT {}", target); + } + pinger::PingResult::Unknown(target) => { + println!("UNKNOWN {}", target); + } + pinger::PingResult::PingExited(_, _) => {} + } + } +} + diff --git a/pinger/src/lib.rs b/pinger/src/lib.rs index b4496abc3..f78d2ff96 100644 --- a/pinger/src/lib.rs +++ b/pinger/src/lib.rs @@ -41,12 +41,17 @@ mod target; #[cfg(test)] mod test; +mod tcp; + #[derive(Debug, Clone)] pub struct PingOptions { pub target: Target, pub interval: Duration, pub interface: Option, pub raw_arguments: Option>, + pub tcping: bool, // use TCP instead of ICMP + pub allow_rst: bool, // We can take leverage connection refused as a valid pong in some case + pub port: Option, // optional port for TCP ping } impl PingOptions { @@ -68,6 +73,9 @@ impl PingOptions { interval, interface, raw_arguments: None, + tcping: false, + allow_rst: false, + port: Some(80), } } pub fn new(target: impl ToString, interval: Duration, interface: Option) -> Self { @@ -81,6 +89,20 @@ impl PingOptions { pub fn new_ipv6(target: impl ToString, interval: Duration, interface: Option) -> Self { Self::from_target(Target::new_ipv6(target), interval, interface) } + /// Enable or disable TCP ping + pub fn with_tcping(mut self, tcping: bool) -> Self { + self.tcping = tcping; + self + } + /// Allow or disallow treating RST as a valid response + pub fn with_allow_rst(mut self, allow: bool) -> Self { + self.allow_rst = allow; + self + } + pub fn with_port(mut self, port: u16) -> Self { + self.port = Some(port); + self + } } pub fn run_ping( @@ -205,6 +227,10 @@ pub fn get_pinger(options: PingOptions) -> std::result::Result, return Ok(Arc::new(fake::FakePinger::from_options(options)?)); } + if options.tcping { + return Ok(Arc::new(crate::tcp::TcpPinger::from_options(options)?)); + } + #[cfg(windows)] { return Ok(Arc::new(windows::WindowsPinger::from_options(options)?)); diff --git a/pinger/src/tcp.rs b/pinger/src/tcp.rs new file mode 100644 index 000000000..66cf9c4f7 --- /dev/null +++ b/pinger/src/tcp.rs @@ -0,0 +1,74 @@ +use std::net::{TcpStream, ToSocketAddrs}; +use std::sync::mpsc; +use std::thread; +use std::time::{Instant}; +use std::io::ErrorKind; + + +use crate::{PingOptions, PingResult, Pinger}; + +pub struct TcpPinger { + options: PingOptions, +} + +impl Pinger for TcpPinger { + fn from_options(options: PingOptions) -> Result { + Ok(TcpPinger { options }) + } + + fn parse_fn(&self) -> fn(String) -> Option { + |_| None // TCP doesn't parse lines + } + + fn ping_args(&self) -> (&str, Vec) { + ("tcp", vec![]) // unused + } + + fn start(&self) -> Result, crate::PingCreationError> { + let (tx, rx) = mpsc::channel(); + let options = self.options.clone(); + + thread::spawn(move || { + for _ in 0.. { + let port = options.port.unwrap_or(80); + let socket_str = format!("{}:{}", options.target, port); + let addr = match socket_str.to_socket_addrs() { + Ok(mut addrs) => match addrs.next() { + Some(a) => a, + None => { + let _ = tx.send(PingResult::Unknown( + "Unable to resolve address".to_string() + )); + continue; + } + }, + Err(e) => { + let _ = tx.send(PingResult::Unknown(format!("Resolve error: {}", e))); + continue; + } + }; + + let start = Instant::now(); + match TcpStream::connect_timeout(&addr, options.interval) { + Ok(_) => { + let _ = tx.send(PingResult::Pong(start.elapsed(), addr.to_string())); + } + Err(e) => { + //println!("DEBUG: error kind for {}: {:?}", addr, e.kind()); + let is_rst = matches!(e.kind(), ErrorKind::ConnectionRefused); + if is_rst && options.allow_rst { // treat RST as pong default behavior + let _ = tx.send(PingResult::Pong(start.elapsed(), addr.to_string())); + } else { + let _ = tx.send(PingResult::Timeout(addr.to_string())); + } + } + } + + thread::sleep(options.interval); + } + }); + + Ok(rx) + } +} +