use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::{Duration, Instant};
const MAX_PROBE_ADDRESSES: usize = 64;
pub fn probe(addr: impl ToSocketAddrs, timeout: Duration) -> bool {
let Ok(addrs) = addr.to_socket_addrs() else {
return false;
};
probe_candidates(addrs.take(MAX_PROBE_ADDRESSES), timeout)
}
#[must_use]
pub fn probe_resolved(addrs: &[SocketAddr], timeout: Duration) -> bool {
probe_candidates(addrs.iter().copied().take(MAX_PROBE_ADDRESSES), timeout)
}
fn probe_candidates(addrs: impl Iterator<Item = SocketAddr>, timeout: Duration) -> bool {
probe_candidates_with(addrs, timeout, |candidate, remaining| {
TcpStream::connect_timeout(candidate, remaining).is_ok()
})
}
fn probe_candidates_with(
addrs: impl Iterator<Item = SocketAddr>,
timeout: Duration,
mut connect: impl FnMut(&SocketAddr, Duration) -> bool,
) -> bool {
let candidates: Vec<SocketAddr> = addrs.take(MAX_PROBE_ADDRESSES).collect();
let started = Instant::now();
for (index, candidate) in candidates.iter().enumerate() {
let remaining = timeout.saturating_sub(started.elapsed());
if remaining.is_zero() {
return false;
}
let left = u32::try_from(candidates.len().saturating_sub(index)).unwrap_or(u32::MAX);
let share = remaining.checked_div(left).unwrap_or(remaining);
if connect(candidate, share) {
return true;
}
}
false
}
#[must_use]
pub fn is_online(timeout: Duration) -> bool {
let addresses = [
SocketAddr::from(([1, 1, 1, 1], 443)),
SocketAddr::from(([8, 8, 8, 8], 53)),
];
probe_resolved(&addresses, timeout)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, TcpListener};
const WAIT: Duration = Duration::from_secs(2);
#[test]
fn open_port_probes_true() -> Result<(), std::io::Error> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
assert!(probe(format!("127.0.0.1:{port}"), WAIT));
Ok(())
}
#[test]
fn closed_port_probes_false() -> Result<(), std::io::Error> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
drop(listener);
assert!(!probe(format!("127.0.0.1:{port}"), WAIT));
Ok(())
}
#[test]
fn unresolvable_host_probes_false() {
assert!(!probe("nonexistent.invalid:80", WAIT));
}
#[test]
fn resolved_probe_reaches_a_loopback_listener() -> Result<(), std::io::Error> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let address = listener.local_addr()?;
assert!(probe_resolved(&[address], WAIT));
Ok(())
}
#[test]
#[expect(
clippy::disallowed_methods,
reason = "the injected dial delay verifies that later candidates receive the shared remaining budget"
)]
fn address_candidates_share_one_remaining_budget() {
let candidates = [
SocketAddr::from(([192, 0, 2, 1], 443)),
SocketAddr::from(([198, 51, 100, 1], 443)),
];
let mut budgets = Vec::new();
let result = probe_candidates_with(
candidates.into_iter(),
Duration::from_millis(100),
|_, remaining| {
budgets.push(remaining);
if budgets.len() == 1 {
std::thread::sleep(Duration::from_millis(60));
}
false
},
);
assert!(!result, "all candidate failures report unreachable");
assert_eq!(
budgets.len(),
2,
"both addresses fit before the shared deadline"
);
assert!(
budgets[1] <= Duration::from_millis(50),
"the second candidate does not receive a restarted timeout"
);
}
#[test]
#[expect(
clippy::disallowed_methods,
reason = "the injected dial sleeps out its whole share, which is what a blackholed address does"
)]
fn a_blackholed_candidate_does_not_starve_the_next() {
let candidates = [
SocketAddr::from(([192, 0, 2, 1], 443)),
SocketAddr::from(([198, 51, 100, 1], 443)),
];
let timeout = Duration::from_millis(100);
let mut budgets = Vec::new();
let result = probe_candidates_with(candidates.into_iter(), timeout, |_, share| {
budgets.push(share);
if budgets.len() == 1 {
std::thread::sleep(share);
return false;
}
true
});
assert!(result, "the second candidate answers, so the probe is true");
assert_eq!(budgets.len(), 2, "the second candidate was dialed");
assert!(
budgets[0] <= timeout / 2,
"the first candidate is offered its share, not the whole budget"
);
assert!(
!budgets[1].is_zero(),
"the second candidate still has time left to answer"
);
}
#[test]
#[expect(
clippy::disallowed_methods,
reason = "this controlled resolver delay proves DNS work is outside the socket connection budget"
)]
fn resolver_delay_is_outside_the_connection_budget() {
struct SlowResolver;
impl ToSocketAddrs for SlowResolver {
type Iter = std::vec::IntoIter<SocketAddr>;
fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
std::thread::sleep(Duration::from_millis(80));
Ok(vec![SocketAddr::from((Ipv4Addr::LOCALHOST, 9))].into_iter())
}
}
let started = Instant::now();
assert!(!probe(SlowResolver, Duration::from_millis(10)));
assert!(started.elapsed() >= Duration::from_millis(80));
}
#[test]
#[expect(
clippy::disallowed_methods,
reason = "the injected dial sleeps out each share so the shared wall-clock bound can be observed"
)]
fn a_whole_probe_fits_one_wall_clock_budget() {
let candidates = [
SocketAddr::from(([192, 0, 2, 1], 443)),
SocketAddr::from(([198, 51, 100, 1], 443)),
SocketAddr::from(([203, 0, 113, 1], 443)),
SocketAddr::from(([192, 0, 2, 2], 443)),
];
let timeout = Duration::from_millis(120);
let started = Instant::now();
let result = probe_candidates_with(candidates.into_iter(), timeout, |_, share| {
std::thread::sleep(share);
false
});
let elapsed = started.elapsed();
assert!(
!result,
"every candidate fails, so the probe reports unreachable"
);
assert!(
elapsed <= timeout + Duration::from_millis(80),
"the four candidates share one budget of {timeout:?}, not four: {elapsed:?}"
);
}
}