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:
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user