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};
pub struct TcpPinger {
config: PingConfig,
target_port: u16,
}
impl TcpPinger {
pub fn new(config: PingConfig) -> Self {
Self {
config,
target_port: ports::HTTP,
}
}
pub fn new_with_port(config: PingConfig, port: u16) -> Self {
Self {
config,
target_port: port,
}
}
pub fn ping(&self, sequence: u16) -> PingResult<PingReply> {
self.ping_port(sequence, self.target_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) => {
let rtt = start_time.elapsed();
Ok(PingReply {
sequence,
rtt,
bytes_received: 0, from: self.config.target,
ttl: None, })
}
Err(e) => {
let elapsed = start_time.elapsed();
match e.kind() {
std::io::ErrorKind::ConnectionRefused => {
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
),
}),
_ => {
if elapsed >= self.config.timeout {
Err(PingError::Timeout {
duration: self.config.timeout,
})
} else {
Err(PingError::NetworkUnreachable)
}
}
}
}
}
}
pub fn ping_common_ports(&self, sequence: u16) -> Vec<(u16, PingResult<PingReply>)> {
let common_ports = [
ports::HTTP,
ports::HTTPS,
ports::SSH,
ports::DNS,
80, 443, 22, 21, 25, 110, 143, 993, 995, ];
common_ports
.iter()
.map(|&port| (port, self.ping_port(sequence, port)))
.collect()
}
pub fn ping_first_open_port(&self, sequence: u16) -> Option<(u16, PingReply)> {
let common_ports = [
ports::HTTP, ports::HTTPS, ports::SSH, 21, 25, ports::DNS, 110, 143, 993, 995, 3389, 5432, 3306, ];
let (tx, rx) = mpsc::channel::<(u16, PingReply)>();
let target = self.config.target;
let timeout = self.config.timeout;
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();
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));
}
}
}
});
}
drop(tx);
if let Ok((port, reply)) = rx.recv() {
Some((port, reply))
} else {
None
}
}
pub fn config(&self) -> &PingConfig {
&self.config
}
pub fn target_port(&self) -> u16 {
self.target_port
}
pub fn set_target_port(&mut self, port: u16) {
self.target_port = port;
}
}
pub fn connection_indicates_reachability(
result: &Result<TcpStream, std::io::Error>,
elapsed: Duration,
) -> bool {
match result {
Ok(_) => true, Err(e) => match e.kind() {
std::io::ErrorKind::ConnectionRefused => {
elapsed < Duration::from_millis(100)
}
_ => false,
},
}
}
pub fn get_common_tcp_ports() -> Vec<u16> {
vec![
ports::HTTP, ports::HTTPS, ports::SSH, ports::DNS, 21, 25, 110, 143, 993, 995, 3389, 5432, 3306, 6379, 27017, ]
}
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",
}
}
#[derive(Debug)]
pub struct TcpConnectionResult {
pub port: u16,
pub result: PingResult<PingReply>,
pub service: &'static str,
pub indicates_reachability: bool,
}
impl TcpConnectionResult {
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,
}
}
pub fn is_successful(&self) -> bool {
self.result.is_ok()
}
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);
}
}