Skip to main content

pq_map/probe/
mod.rs

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
20/// Probe one endpoint within PROBE_TIMEOUT: rustls first, OpenSSL fallback
21/// only for handshake-class failures (parity with ct-inv2.sh --probe).
22pub 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
105/// Map a mid-handshake I/O failure to a probe classification:
106/// clean close without certificate vs. broken TLS (alerts, garbage, EOF with data).
107pub(super) fn classify_handshake(e: std::io::Error) -> Failure {
108    let text = e.to_string();
109    match e.kind() {
110        // Clean close before the certificate arrived (e.g. plain-TCP service).
111        ErrorKind::UnexpectedEof => Failure::ClosedNoCert(text),
112        ErrorKind::TimedOut | ErrorKind::WouldBlock => Failure::Timeout,
113        // Alerts, protocol garbage, record overflows: everything else.
114        _ => 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}