netpulse-cli 0.1.1

A zero-config, single-binary network quality monitor with percentile stats, jitter, and MTR-style traceroute
Documentation
// src/probers/tcp.rs — TCP Handshake Latency Prober
//
// Measures the time to complete a TCP three-way handshake.
// This is what HTTP clients actually feel — unlike ICMP which
// is often treated differently by network equipment.
//
// No data is sent after the connection is established;
// the socket is immediately dropped (sends RST or FIN).

use super::{ProbeResult, Prober};
use crate::error::NetPulseError;
use async_trait::async_trait;
use std::time::Instant;
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};

/// TCP handshake latency prober.
pub struct TcpProber {
    /// Default target port if none is embedded in the target string.
    port: u16,
    /// Probe timeout in milliseconds.
    timeout_ms: u64,
}

impl TcpProber {
    pub fn new(port: u16, timeout_ms: u64) -> Self {
        Self { port, timeout_ms }
    }
}

#[async_trait]
impl Prober for TcpProber {
    fn name(&self) -> &'static str {
        "tcp"
    }

    async fn probe(
        &self,
        target: &str,
        seq: u64,
        _ttl: Option<u32>,
    ) -> Result<ProbeResult, NetPulseError> {
        // Support "host:port" format; fall back to self.port otherwise
        let addr = if target.contains(':') {
            target.to_string()
        } else {
            format!("{}:{}", target, self.port)
        };

        let start = Instant::now();
        let connect_result = timeout(
            Duration::from_millis(self.timeout_ms),
            TcpStream::connect(&addr),
        )
        .await;

        match connect_result {
            Ok(Ok(_stream)) => {
                // Connection succeeded — stream is dropped here, closing the TCP connection
                let rtt_us = start.elapsed().as_micros() as u64;
                Ok(ProbeResult::success(target, seq, rtt_us, None))
            }
            Ok(Err(e)) => {
                // Connection refused or other OS error — still produces a valid RTT
                // (the host responded, just with RST) unless it's a routing failure
                match e.kind() {
                    std::io::ErrorKind::ConnectionRefused => {
                        // Host is live but port is closed — we still got an RTT
                        let rtt_us = start.elapsed().as_micros() as u64;
                        Ok(ProbeResult::success(target, seq, rtt_us, None))
                    }
                    _ => Ok(ProbeResult::loss(target, seq)),
                }
            }
            Err(_elapsed) => {
                // Timed out — count as loss
                Ok(ProbeResult::loss(target, seq))
            }
        }
    }
}