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:
@@ -43,7 +43,7 @@
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -52,7 +52,7 @@ use openfut_adapter_fifa17::roster::{self, Method};
|
||||
use openfut_http::drain_body;
|
||||
pub use openfut_http::BodyRead;
|
||||
// The acceptor comes from the shared crate, so this host never names OpenSSL.
|
||||
use openfut_tls::SslAcceptor;
|
||||
use openfut_tls::{peer_opening, PeerOpening, SslAcceptor};
|
||||
|
||||
pub mod config;
|
||||
pub use config::RosterConfig;
|
||||
@@ -112,6 +112,15 @@ pub struct ConnOutcome {
|
||||
|
||||
pub type Outcomes = Arc<std::sync::Mutex<Vec<ConnOutcome>>>;
|
||||
|
||||
/// How many bare port probes have been classified before reaching the acceptor.
|
||||
///
|
||||
/// Counted, not only logged, for the same reason [`ConnOutcome`] exists: a test
|
||||
/// that asserts on client-visible symptoms cannot tell a probe that was
|
||||
/// classified from one that merely failed quietly, so removing the
|
||||
/// classification would leave the suite green. This makes it assertable — and
|
||||
/// mirrors the redirector, the host that first needed the distinction.
|
||||
pub type ProbeCount = Arc<AtomicUsize>;
|
||||
|
||||
/// Bounded: a host polled every few seconds must not accumulate forever.
|
||||
const OUTCOME_HISTORY: usize = 64;
|
||||
|
||||
@@ -130,6 +139,7 @@ pub struct Server {
|
||||
acceptor: Arc<SslAcceptor>,
|
||||
cfg: Arc<RosterConfig>,
|
||||
outcomes: Outcomes,
|
||||
probes: ProbeCount,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
@@ -137,6 +147,11 @@ impl Server {
|
||||
self.outcomes.clone()
|
||||
}
|
||||
|
||||
/// A handle to the bare-probe counter, obtainable before [`Server::run`].
|
||||
pub fn probes(&self) -> ProbeCount {
|
||||
self.probes.clone()
|
||||
}
|
||||
|
||||
pub fn run(self) -> std::io::Result<()> {
|
||||
let counter = AtomicU64::new(0);
|
||||
for incoming in self.listener.incoming() {
|
||||
@@ -144,7 +159,8 @@ impl Server {
|
||||
let id = counter.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let (acceptor, cfg) = (self.acceptor.clone(), self.cfg.clone());
|
||||
let outcomes = self.outcomes.clone();
|
||||
std::thread::spawn(move || handle(stream, id, &acceptor, &cfg, &outcomes));
|
||||
let probes = self.probes.clone();
|
||||
std::thread::spawn(move || handle(stream, id, &acceptor, &cfg, &outcomes, &probes));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -184,6 +200,7 @@ pub fn bind(cfg: RosterConfig) -> std::io::Result<Server> {
|
||||
acceptor: Arc::new(acceptor),
|
||||
cfg: Arc::new(cfg),
|
||||
outcomes: Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||
probes: ProbeCount::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -207,12 +224,25 @@ fn handle(
|
||||
acceptor: &SslAcceptor,
|
||||
cfg: &RosterConfig,
|
||||
outcomes: &Outcomes,
|
||||
probes: &ProbeCount,
|
||||
) {
|
||||
let peer = stream
|
||||
.peer_addr()
|
||||
.map(|a| a.to_string())
|
||||
.unwrap_or_else(|_| "?".into());
|
||||
|
||||
// Classify a bare port probe BEFORE the acceptor sees it. A `connect` then
|
||||
// drop (the launcher's preflight makes one per run) otherwise reaches the
|
||||
// acceptor as `unexpected EOF` — byte-identical to the certificate mismatch
|
||||
// that cost three live gates. Shared with the redirector via openfut-tls so
|
||||
// the two hosts can never diverge on this.
|
||||
if peer_opening(&stream) == PeerOpening::ClosedWithoutSpeaking {
|
||||
probes.fetch_add(1, Ordering::Relaxed);
|
||||
log(&format!(
|
||||
"conn-{id:04} {peer} PROBE: closed before sending a ClientHello (not a TLS fault)"
|
||||
));
|
||||
return;
|
||||
}
|
||||
let mut tls = match acceptor.accept(stream) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user