|
| 1 | +//! TCP connect for the port scanner, which must tell "refused" from "no |
| 2 | +//! answer". |
| 3 | +//! |
| 4 | +//! Windows retries a SYN that was answered with a RST, so a connect to a |
| 5 | +//! closed port only fails with `ConnectionRefused` after about 2 s (measured |
| 6 | +//! 2026-10-02 on Windows 11: 2025-2047 ms to a LAN host, 2043 ms to |
| 7 | +//! loopback). The port scanner gives up at 500 ms by default, so every closed |
| 8 | +//! port read as filtered. Turning SYN retransmissions off for the socket |
| 9 | +//! makes the first RST final: the same connects failed in 0-5 ms. |
| 10 | +//! |
| 11 | +//! With no retransmissions Windows abandons the connect after the initial |
| 12 | +//! retransmission timeout, 1 s by default, whatever timeout the caller asked |
| 13 | +//! for. So the initial timeout is set to the caller's timeout as well. |
| 14 | +
|
| 15 | +use std::io; |
| 16 | +use std::net::SocketAddr; |
| 17 | +use std::time::Duration; |
| 18 | + |
| 19 | +use tokio::net::{TcpSocket, TcpStream}; |
| 20 | + |
| 21 | +/// Connect to `addr`, giving up after `timeout_ms`. A timeout is reported as |
| 22 | +/// `ErrorKind::TimedOut`, the same kind the OS uses for an unanswered SYN. |
| 23 | +pub(crate) async fn connect(addr: SocketAddr, timeout_ms: u64) -> io::Result<TcpStream> { |
| 24 | + let socket = if addr.is_ipv4() { |
| 25 | + TcpSocket::new_v4()? |
| 26 | + } else { |
| 27 | + TcpSocket::new_v6()? |
| 28 | + }; |
| 29 | + #[cfg(windows)] |
| 30 | + windows::final_rst(&socket, timeout_ms); |
| 31 | + match tokio::time::timeout(Duration::from_millis(timeout_ms), socket.connect(addr)).await { |
| 32 | + Ok(result) => result, |
| 33 | + Err(_) => Err(io::Error::new(io::ErrorKind::TimedOut, "connect timed out")), |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +#[cfg(windows)] |
| 38 | +mod windows { |
| 39 | + use std::os::windows::io::AsRawSocket; |
| 40 | + |
| 41 | + use tokio::net::TcpSocket; |
| 42 | + use windows_sys::Win32::Networking::WinSock::{ |
| 43 | + WSAIoctl, SIO_TCP_INITIAL_RTO, SOCKET, TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS, |
| 44 | + TCP_INITIAL_RTO_PARAMETERS, |
| 45 | + }; |
| 46 | + |
| 47 | + /// Turn off SYN retransmissions and set the initial retransmission |
| 48 | + /// timeout to `timeout_ms`. Best effort: if the ioctl is refused the |
| 49 | + /// connect still works, it is only slow to report a closed port. |
| 50 | + pub(super) fn final_rst(socket: &TcpSocket, timeout_ms: u64) { |
| 51 | + let params = TCP_INITIAL_RTO_PARAMETERS { |
| 52 | + Rtt: rtt(timeout_ms), |
| 53 | + // The C header defines this as (USHORT)-2 and assigns it to the |
| 54 | + // UCHAR field, which keeps the low byte. |
| 55 | + MaxSynRetransmissions: TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS as u8, |
| 56 | + }; |
| 57 | + let mut returned = 0u32; |
| 58 | + // SAFETY: a valid socket handle, an input buffer of the declared size, |
| 59 | + // no output buffer, and no overlapped I/O. |
| 60 | + unsafe { |
| 61 | + WSAIoctl( |
| 62 | + socket.as_raw_socket() as SOCKET, |
| 63 | + SIO_TCP_INITIAL_RTO, |
| 64 | + ¶ms as *const _ as *const core::ffi::c_void, |
| 65 | + std::mem::size_of::<TCP_INITIAL_RTO_PARAMETERS>() as u32, |
| 66 | + std::ptr::null_mut(), |
| 67 | + 0, |
| 68 | + &mut returned, |
| 69 | + std::ptr::null_mut(), |
| 70 | + None, |
| 71 | + ); |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + /// 65535 means "unspecified" to Windows, so the largest usable value is |
| 76 | + /// 65534 ms. 0 is not a timeout either. |
| 77 | + pub(super) fn rtt(timeout_ms: u64) -> u16 { |
| 78 | + timeout_ms.clamp(1, 65_534) as u16 |
| 79 | + } |
| 80 | + |
| 81 | + #[cfg(test)] |
| 82 | + mod tests { |
| 83 | + use super::rtt; |
| 84 | + |
| 85 | + #[test] |
| 86 | + fn rtt_stays_inside_the_values_windows_accepts() { |
| 87 | + assert_eq!(rtt(0), 1); |
| 88 | + assert_eq!(rtt(500), 500); |
| 89 | + assert_eq!(rtt(65_535), 65_534); |
| 90 | + assert_eq!(rtt(u64::MAX), 65_534); |
| 91 | + } |
| 92 | + } |
| 93 | +} |
0 commit comments