fix(tls): share bare-probe classification across all FIFA-facing TLS hosts

A reachability probe (TcpStream::connect then drop; the launcher preflight makes
them) reaches a TLS acceptor as 'unexpected EOF' — byte-identical to the
certificate mismatch that cost three live gates. The redirector classified the
opening before the acceptor to keep a benign probe from forging a TLS fault, but
the roster host (the second FIFA-facing TLS host) did not, so the documented
hazard 'remains in any other TLS host that has not adopted it' was live there.

Lift the pure policy (PeerOpening + classify_opening) plus a peer_opening(&TcpStream)
peek helper into the shared openfut-tls crate (game-independent; +unit tests).
The redirector now re-exports them (public API + its probe_classification test
unchanged; behaviour identical). The roster host adopts them: a ProbeCount, a
probes() handle, and a pre-acceptor peek that logs PROBE and returns instead of
failing the handshake. New roster probe_classification integration test (3 cases:
bare probe classified, real client after a probe still served 200, speaks-then-
fails still reported as a fault). Full workspace tests green; clippy -D clean.
This commit is contained in:
funman300
2026-08-16 20:30:50 +00:00
parent 12fb9fc38b
commit ad406f21bd
4 changed files with 230 additions and 31 deletions
+6 -28
View File
@@ -233,32 +233,11 @@ pub fn serve(cfg: RedirectorConfig) -> std::io::Result<()> {
bind(cfg)?.run()
}
/// What a peer did with the connection before any TLS was attempted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerOpening {
/// Connected and closed without sending anything: a reachability probe.
ClosedWithoutSpeaking,
/// Sent at least one byte, so a real handshake is under way.
Spoke,
/// Timed out or errored. Deliberately NOT treated as a probe — a slow or
/// broken client must still reach the acceptor and produce a real
/// diagnostic, because misclassifying a fault as a probe would hide
/// precisely what this distinction exists to protect.
Undetermined,
}
/// Classify the result of peeking at the first byte.
///
/// Split out as a pure function so the policy is testable without a socket —
/// the interesting cases (EOF vs timeout) are awkward to provoke live and easy
/// to get backwards.
pub fn classify_opening(peek: &std::io::Result<usize>) -> PeerOpening {
match peek {
Ok(0) => PeerOpening::ClosedWithoutSpeaking,
Ok(_) => PeerOpening::Spoke,
Err(_) => PeerOpening::Undetermined,
}
}
/// Re-exported from the shared TLS crate. The classification policy now lives in
/// `openfut-tls` so every FIFA-facing TLS host shares one implementation; this
/// host keeps naming them here so its public API and `probe_classification`
/// integration test are unaffected.
pub use openfut_tls::{classify_opening, PeerOpening};
fn handle(
stream: TcpStream,
@@ -285,8 +264,7 @@ fn handle(
// exactly how the certificate mismatch that cost three live gates
// presented, so a benign probe forging it poisons the one channel this
// project gates on. Classified here, the two are never confused again.
let mut first = [0u8; 1];
if classify_opening(&stream.peek(&mut first)) == PeerOpening::ClosedWithoutSpeaking {
if openfut_tls::peer_opening(&stream) == PeerOpening::ClosedWithoutSpeaking {
probes.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
log(&format!(
"conn-{id:04} {peer} PROBE: closed before sending a ClientHello (not a TLS fault)"