pub mod sweep;
pub mod tls;
use std::io::ErrorKind;
use std::net::IpAddr;
use crate::config::Config;
use crate::model::{Endpoint, ProbeStatus};
#[derive(Debug)]
pub enum Failure {
TcpRefused,
TcpUnreachable,
Timeout,
Handshake(String),
ClosedNoCert(String),
Unknown(String),
}
pub async fn probe_one(host: &str, ip: Option<IpAddr>, port: u16, cfg: &Config) -> Endpoint {
let mut endpoint =
match tokio::time::timeout(cfg.probe_timeout, tls::attempt(host, ip, port, cfg)).await {
Err(_) => Endpoint::failed(ProbeStatus::Timeout {
secs: cfg.probe_timeout.as_secs(),
}),
Ok(Ok((served, negotiated))) => Endpoint {
status: ProbeStatus::Ok,
ip,
served: Some(served),
served_not_in_ct: false,
negotiated: Some(negotiated),
observed_at: None,
},
Ok(Err(failure)) => handle_failure(host, ip, port, cfg, failure).await,
};
endpoint.ip = ip;
if endpoint.observed_at.is_none() {
endpoint.observed_at = Some(chrono::Utc::now());
}
endpoint
}
async fn handle_failure(
host: &str,
ip: Option<IpAddr>,
port: u16,
cfg: &Config,
failure: Failure,
) -> Endpoint {
match failure {
Failure::TcpRefused => Endpoint::failed(ProbeStatus::TcpRefused),
Failure::TcpUnreachable => Endpoint::failed(ProbeStatus::TcpUnreachable),
Failure::Timeout => Endpoint::failed(ProbeStatus::Timeout {
secs: cfg.probe_timeout.as_secs(),
}),
Failure::ClosedNoCert(_) => Endpoint::failed(ProbeStatus::ConnectedNoCert),
Failure::Unknown(reason) => Endpoint::failed(ProbeStatus::Unknown { reason }),
Failure::Handshake(_) => openssl_fallback(host, ip, port, cfg).await,
}
}
async fn openssl_fallback(host: &str, ip: Option<IpAddr>, port: u16, cfg: &Config) -> Endpoint {
let owned = host.to_string();
let timeout = cfg.probe_timeout;
let outcome = tokio::time::timeout(
timeout,
tokio::task::spawn_blocking(move || tls::openssl_attempt(&owned, ip, port, timeout)),
)
.await;
match outcome {
Err(_) => Endpoint::failed(ProbeStatus::Timeout {
secs: timeout.as_secs(),
}),
Ok(Err(join)) => Endpoint::failed(ProbeStatus::Unknown {
reason: join.to_string(),
}),
Ok(Ok(Ok((served, negotiated)))) => Endpoint {
status: ProbeStatus::Ok,
ip,
served: Some(served),
served_not_in_ct: false,
negotiated: Some(negotiated),
observed_at: None,
},
Ok(Ok(Err(_reason))) => Endpoint::failed(ProbeStatus::TlsHandshakeFailed),
}
}
pub(super) fn classify_io(e: &std::io::Error) -> Failure {
match e.kind() {
ErrorKind::ConnectionRefused => Failure::TcpRefused,
ErrorKind::NetworkUnreachable
| ErrorKind::HostUnreachable
| ErrorKind::AddrNotAvailable
| ErrorKind::NotConnected
| ErrorKind::NetworkDown => Failure::TcpUnreachable,
ErrorKind::TimedOut | ErrorKind::WouldBlock => Failure::Timeout,
_ => Failure::Unknown(e.to_string()),
}
}
pub(super) fn classify_handshake(e: std::io::Error) -> Failure {
let text = e.to_string();
match e.kind() {
ErrorKind::UnexpectedEof => Failure::ClosedNoCert(text),
ErrorKind::TimedOut | ErrorKind::WouldBlock => Failure::Timeout,
_ => Failure::Handshake(text),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn io_classification() {
assert!(matches!(
classify_io(&std::io::Error::from(ErrorKind::ConnectionRefused)),
Failure::TcpRefused
));
assert!(matches!(
classify_io(&std::io::Error::from(ErrorKind::NetworkUnreachable)),
Failure::TcpUnreachable
));
assert!(matches!(
classify_io(&std::io::Error::from(ErrorKind::AddrNotAvailable)),
Failure::TcpUnreachable
));
assert!(matches!(
classify_io(&std::io::Error::from(ErrorKind::TimedOut)),
Failure::Timeout
));
assert!(matches!(
classify_io(&std::io::Error::other("boom")),
Failure::Unknown(_)
));
}
#[test]
fn handshake_eof_is_closed_no_cert() {
assert!(matches!(
classify_handshake(std::io::Error::new(ErrorKind::UnexpectedEof, "eof")),
Failure::ClosedNoCert(_)
));
assert!(matches!(
classify_handshake(std::io::Error::other("alert")),
Failure::Handshake(_)
));
}
}