1pub mod sweep;
2pub mod tls;
3
4use std::io::ErrorKind;
5use std::net::IpAddr;
6
7use crate::config::Config;
8use crate::model::{Endpoint, ProbeStatus};
9
10#[derive(Debug)]
11pub enum Failure {
12 TcpRefused,
13 TcpUnreachable,
14 Timeout,
15 Handshake(String),
16 ClosedNoCert(String),
17 Unknown(String),
18}
19
20pub async fn probe_one(host: &str, ip: Option<IpAddr>, port: u16, cfg: &Config) -> Endpoint {
23 let mut endpoint =
24 match tokio::time::timeout(cfg.probe_timeout, tls::attempt(host, ip, port, cfg)).await {
25 Err(_) => Endpoint::failed(ProbeStatus::Timeout {
26 secs: cfg.probe_timeout.as_secs(),
27 }),
28 Ok(Ok((served, negotiated))) => Endpoint {
29 status: ProbeStatus::Ok,
30 ip,
31 served: Some(served),
32 served_not_in_ct: false,
33 negotiated: Some(negotiated),
34 observed_at: None,
35 },
36 Ok(Err(failure)) => handle_failure(host, ip, port, cfg, failure).await,
37 };
38 endpoint.ip = ip;
39 if endpoint.observed_at.is_none() {
40 endpoint.observed_at = Some(chrono::Utc::now());
41 }
42 endpoint
43}
44
45async fn handle_failure(
46 host: &str,
47 ip: Option<IpAddr>,
48 port: u16,
49 cfg: &Config,
50 failure: Failure,
51) -> Endpoint {
52 match failure {
53 Failure::TcpRefused => Endpoint::failed(ProbeStatus::TcpRefused),
54 Failure::TcpUnreachable => Endpoint::failed(ProbeStatus::TcpUnreachable),
55 Failure::Timeout => Endpoint::failed(ProbeStatus::Timeout {
56 secs: cfg.probe_timeout.as_secs(),
57 }),
58 Failure::ClosedNoCert(_) => Endpoint::failed(ProbeStatus::ConnectedNoCert),
59 Failure::Unknown(reason) => Endpoint::failed(ProbeStatus::Unknown { reason }),
60 Failure::Handshake(_) => openssl_fallback(host, ip, port, cfg).await,
61 }
62}
63
64async fn openssl_fallback(host: &str, ip: Option<IpAddr>, port: u16, cfg: &Config) -> Endpoint {
65 let owned = host.to_string();
66 let timeout = cfg.probe_timeout;
67 let outcome = tokio::time::timeout(
68 timeout,
69 tokio::task::spawn_blocking(move || tls::openssl_attempt(&owned, ip, port, timeout)),
70 )
71 .await;
72
73 match outcome {
74 Err(_) => Endpoint::failed(ProbeStatus::Timeout {
75 secs: timeout.as_secs(),
76 }),
77 Ok(Err(join)) => Endpoint::failed(ProbeStatus::Unknown {
78 reason: join.to_string(),
79 }),
80 Ok(Ok(Ok((served, negotiated)))) => Endpoint {
81 status: ProbeStatus::Ok,
82 ip,
83 served: Some(served),
84 served_not_in_ct: false,
85 negotiated: Some(negotiated),
86 observed_at: None,
87 },
88 Ok(Ok(Err(_reason))) => Endpoint::failed(ProbeStatus::TlsHandshakeFailed),
89 }
90}
91
92pub(super) fn classify_io(e: &std::io::Error) -> Failure {
93 match e.kind() {
94 ErrorKind::ConnectionRefused => Failure::TcpRefused,
95 ErrorKind::NetworkUnreachable
96 | ErrorKind::HostUnreachable
97 | ErrorKind::AddrNotAvailable
98 | ErrorKind::NotConnected
99 | ErrorKind::NetworkDown => Failure::TcpUnreachable,
100 ErrorKind::TimedOut | ErrorKind::WouldBlock => Failure::Timeout,
101 _ => Failure::Unknown(e.to_string()),
102 }
103}
104
105pub(super) fn classify_handshake(e: std::io::Error) -> Failure {
108 let text = e.to_string();
109 match e.kind() {
110 ErrorKind::UnexpectedEof => Failure::ClosedNoCert(text),
112 ErrorKind::TimedOut | ErrorKind::WouldBlock => Failure::Timeout,
113 _ => Failure::Handshake(text),
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn io_classification() {
124 assert!(matches!(
125 classify_io(&std::io::Error::from(ErrorKind::ConnectionRefused)),
126 Failure::TcpRefused
127 ));
128 assert!(matches!(
129 classify_io(&std::io::Error::from(ErrorKind::NetworkUnreachable)),
130 Failure::TcpUnreachable
131 ));
132 assert!(matches!(
133 classify_io(&std::io::Error::from(ErrorKind::AddrNotAvailable)),
134 Failure::TcpUnreachable
135 ));
136 assert!(matches!(
137 classify_io(&std::io::Error::from(ErrorKind::TimedOut)),
138 Failure::Timeout
139 ));
140 assert!(matches!(
141 classify_io(&std::io::Error::other("boom")),
142 Failure::Unknown(_)
143 ));
144 }
145
146 #[test]
147 fn handshake_eof_is_closed_no_cert() {
148 assert!(matches!(
149 classify_handshake(std::io::Error::new(ErrorKind::UnexpectedEof, "eof")),
150 Failure::ClosedNoCert(_)
151 ));
152 assert!(matches!(
153 classify_handshake(std::io::Error::other("alert")),
154 Failure::Handshake(_)
155 ));
156 }
157}