anti-ping 0.1.2

A library for ICMP, UDP, and TCP ping functionality
Documentation
//! TCP ping implementation
//!
//! This module provides TCP-based connectivity testing by attempting to establish
//! TCP connections to specific ports. This is useful for testing connectivity to
//! services and can work through firewalls that might block ICMP.

use anti_common::{ports, PingConfig, PingError, PingReply, PingResult};
use std::net::{IpAddr, SocketAddr, TcpStream};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};

/// TCP pinger that tests connectivity by attempting TCP connections
pub struct TcpPinger {
    config: PingConfig,
    target_port: u16,
}

impl TcpPinger {
    /// Create a new TCP pinger with default port (HTTP - 80)
    pub fn new(config: PingConfig) -> Self {
        Self {
            config,
            target_port: ports::HTTP,
        }
    }

    /// Create a new TCP pinger with a specific port
    pub fn new_with_port(config: PingConfig, port: u16) -> Self {
        Self {
            config,
            target_port: port,
        }
    }

    /// Attempt a TCP connection and measure the time
    pub fn ping(&self, sequence: u16) -> PingResult<PingReply> {
        self.ping_port(sequence, self.target_port)
    }

    /// Attempt a TCP connection to a specific port
    pub fn ping_port(&self, sequence: u16, port: u16) -> PingResult<PingReply> {
        let target_addr = SocketAddr::new(IpAddr::V4(self.config.target), port);
        let start_time = Instant::now();

        match TcpStream::connect_timeout(&target_addr, self.config.timeout) {
            Ok(_stream) => {
                // Successfully connected - measure actual connection time
                let rtt = start_time.elapsed();
                Ok(PingReply {
                    sequence,
                    rtt,
                    bytes_received: 0, // TCP handshake doesn't return data
                    from: self.config.target,
                    ttl: None, // TTL not available from TCP connection
                })
            }
            Err(e) => {
                let elapsed = start_time.elapsed();

                match e.kind() {
                    std::io::ErrorKind::ConnectionRefused => {
                        // Connection refused means host is reachable but port is closed
                        // This is still a successful "ping" in terms of host reachability
                        if elapsed < Duration::from_millis(100) {
                            Ok(PingReply {
                                sequence,
                                rtt: elapsed,
                                bytes_received: 0,
                                from: self.config.target,
                                ttl: None,
                            })
                        } else {
                            Err(PingError::PortUnreachable)
                        }
                    }
                    std::io::ErrorKind::TimedOut => Err(PingError::Timeout {
                        duration: self.config.timeout,
                    }),
                    std::io::ErrorKind::PermissionDenied => Err(PingError::PermissionDenied {
                        context: format!(
                            "Permission denied connecting to {}:{}",
                            self.config.target, port
                        ),
                    }),
                    _ => {
                        // Map other connection errors
                        if elapsed >= self.config.timeout {
                            Err(PingError::Timeout {
                                duration: self.config.timeout,
                            })
                        } else {
                            Err(PingError::NetworkUnreachable)
                        }
                    }
                }
            }
        }
    }

    /// Test multiple common ports in sequence
    pub fn ping_common_ports(&self, sequence: u16) -> Vec<(u16, PingResult<PingReply>)> {
        let common_ports = [
            ports::HTTP,
            ports::HTTPS,
            ports::SSH,
            ports::DNS,
            80,  // HTTP
            443, // HTTPS
            22,  // SSH
            21,  // FTP
            25,  // SMTP
            110, // POP3
            143, // IMAP
            993, // IMAPS
            995, // POP3S
        ];

        common_ports
            .iter()
            .map(|&port| (port, self.ping_port(sequence, port)))
            .collect()
    }

    /// Test multiple common ports in parallel and return the first successful one
    pub fn ping_first_open_port(&self, sequence: u16) -> Option<(u16, PingReply)> {
        let common_ports = [
            ports::HTTP,  // 80
            ports::HTTPS, // 443
            ports::SSH,   // 22
            21,           // FTP
            25,           // SMTP
            ports::DNS,   // 53
            110,          // POP3
            143,          // IMAP
            993,          // IMAPS
            995,          // POP3S
            3389,         // RDP
            5432,         // PostgreSQL
            3306,         // MySQL
        ];

        let (tx, rx) = mpsc::channel::<(u16, PingReply)>();
        let target = self.config.target;
        let timeout = self.config.timeout;

        // Spawn a thread for each port
        for &port in &common_ports {
            let tx = tx.clone();
            thread::spawn(move || {
                let target_addr = SocketAddr::new(IpAddr::V4(target), port);
                let start_time = Instant::now();

                match TcpStream::connect_timeout(&target_addr, timeout) {
                    Ok(_stream) => {
                        let rtt = start_time.elapsed();
                        let reply = PingReply {
                            sequence,
                            rtt,
                            bytes_received: 0,
                            from: target,
                            ttl: None,
                        };
                        let _ = tx.send((port, reply));
                    }
                    Err(e) => {
                        let elapsed = start_time.elapsed();
                        // Only consider quick connection refused as reachable
                        if e.kind() == std::io::ErrorKind::ConnectionRefused
                            && elapsed < Duration::from_millis(100)
                        {
                            let reply = PingReply {
                                sequence,
                                rtt: elapsed,
                                bytes_received: 0,
                                from: target,
                                ttl: None,
                            };
                            let _ = tx.send((port, reply));
                        }
                        // For other errors, we don't send anything (port is not useful)
                    }
                }
            });
        }

        // Drop the original sender so the channel can close when all threads are done
        drop(tx);

        // Wait for the first successful result
        if let Ok((port, reply)) = rx.recv() {
            Some((port, reply))
        } else {
            None
        }
    }

    /// Get the target configuration
    pub fn config(&self) -> &PingConfig {
        &self.config
    }

    /// Get the target port
    pub fn target_port(&self) -> u16 {
        self.target_port
    }

    /// Set a new target port
    pub fn set_target_port(&mut self, port: u16) {
        self.target_port = port;
    }
}

/// Check if a TCP connection attempt indicates host reachability
pub fn connection_indicates_reachability(
    result: &Result<TcpStream, std::io::Error>,
    elapsed: Duration,
) -> bool {
    match result {
        Ok(_) => true, // Successful connection
        Err(e) => match e.kind() {
            std::io::ErrorKind::ConnectionRefused => {
                // Quick connection refused indicates host is reachable
                elapsed < Duration::from_millis(100)
            }
            _ => false,
        },
    }
}

/// Get a list of commonly tested TCP ports
pub fn get_common_tcp_ports() -> Vec<u16> {
    vec![
        ports::HTTP,  // 80
        ports::HTTPS, // 443
        ports::SSH,   // 22
        ports::DNS,   // 53
        21,           // FTP
        25,           // SMTP
        110,          // POP3
        143,          // IMAP
        993,          // IMAPS
        995,          // POP3S
        3389,         // RDP
        5432,         // PostgreSQL
        3306,         // MySQL
        6379,         // Redis
        27017,        // MongoDB
    ]
}

/// Classify a port by its common service
pub fn classify_port(port: u16) -> &'static str {
    match port {
        21 => "FTP",
        22 => "SSH",
        25 => "SMTP",
        53 => "DNS",
        80 => "HTTP",
        110 => "POP3",
        143 => "IMAP",
        443 => "HTTPS",
        993 => "IMAPS",
        995 => "POP3S",
        3306 => "MySQL",
        3389 => "RDP",
        5432 => "PostgreSQL",
        6379 => "Redis",
        27017 => "MongoDB",
        _ => "Unknown",
    }
}

/// TCP connection result with additional metadata
#[derive(Debug)]
pub struct TcpConnectionResult {
    /// The port that was tested
    pub port: u16,
    /// The ping result
    pub result: PingResult<PingReply>,
    /// Service classification
    pub service: &'static str,
    /// Whether the connection indicates host reachability
    pub indicates_reachability: bool,
}

impl TcpConnectionResult {
    /// Create a new TCP connection result
    pub fn new(port: u16, result: PingResult<PingReply>) -> Self {
        let service = classify_port(port);
        let indicates_reachability = result.is_ok();

        Self {
            port,
            result,
            service,
            indicates_reachability,
        }
    }

    /// Check if the connection was successful
    pub fn is_successful(&self) -> bool {
        self.result.is_ok()
    }

    /// Get the RTT if available
    pub fn rtt(&self) -> Option<Duration> {
        self.result.as_ref().ok().map(|reply| reply.rtt)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::Ipv4Addr;
    use std::time::Duration;

    #[test]
    fn test_tcp_pinger_creation() {
        let config = PingConfig {
            target: Ipv4Addr::new(127, 0, 0, 1),
            timeout: Duration::from_secs(1),
            ..Default::default()
        };

        let pinger = TcpPinger::new(config);
        assert_eq!(pinger.target_port(), ports::HTTP);
        assert_eq!(pinger.config().target, Ipv4Addr::new(127, 0, 0, 1));
    }

    #[test]
    fn test_tcp_pinger_with_custom_port() {
        let config = PingConfig::default();
        let pinger = TcpPinger::new_with_port(config, 8080);
        assert_eq!(pinger.target_port(), 8080);
    }

    #[test]
    fn test_port_classification() {
        assert_eq!(classify_port(80), "HTTP");
        assert_eq!(classify_port(443), "HTTPS");
        assert_eq!(classify_port(22), "SSH");
        assert_eq!(classify_port(9999), "Unknown");
    }

    #[test]
    fn test_common_ports_list() {
        let ports = get_common_tcp_ports();
        assert!(ports.contains(&80));
        assert!(ports.contains(&443));
        assert!(ports.contains(&22));
        assert!(!ports.is_empty());
    }

    #[test]
    fn test_connection_reachability_quick_refused() {
        let quick_error =
            std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused");
        let result: Result<TcpStream, std::io::Error> = Err(quick_error);
        let quick_elapsed = Duration::from_millis(10);

        assert!(connection_indicates_reachability(&result, quick_elapsed));
    }

    #[test]
    fn test_connection_reachability_slow_timeout() {
        let timeout_error = std::io::Error::new(std::io::ErrorKind::TimedOut, "Timed out");
        let result: Result<TcpStream, std::io::Error> = Err(timeout_error);
        let slow_elapsed = Duration::from_secs(5);

        assert!(!connection_indicates_reachability(&result, slow_elapsed));
    }

    #[test]
    fn test_tcp_connection_result() {
        let reply = PingReply {
            sequence: 1,
            rtt: Duration::from_millis(50),
            bytes_received: 0,
            from: Ipv4Addr::new(127, 0, 0, 1),
            ttl: None,
        };

        let result = TcpConnectionResult::new(80, Ok(reply));
        assert_eq!(result.port, 80);
        assert_eq!(result.service, "HTTP");
        assert!(result.is_successful());
        assert!(result.indicates_reachability);
        assert_eq!(result.rtt(), Some(Duration::from_millis(50)));
    }

    #[test]
    fn test_set_target_port() {
        let config = PingConfig::default();
        let mut pinger = TcpPinger::new(config);
        assert_eq!(pinger.target_port(), ports::HTTP);

        pinger.set_target_port(8080);
        assert_eq!(pinger.target_port(), 8080);
    }
}