//! A bare port probe must not be reported as a TLS fault on the roster host. //! //! `TLS HANDSHAKE FAILED: ... unexpected EOF` is the exact signature the //! certificate mismatch produced — the defect that cost three live gate attempts //! and was invisible everywhere else. A reachability probe forges it trivially: //! `TcpStream::connect` then drop opens the connection and closes it without //! sending a byte, which the acceptor reports as `unexpected EOF`. The launcher's //! preflight makes such probes, so the roster host — like the redirector — must //! classify the opening before the acceptor sees it. The policy is shared via //! `openfut-tls`; this proves the roster host actually applies it. use std::io::{Read, Write}; use std::net::TcpStream; use std::sync::atomic::Ordering; use std::time::Duration; use openfut_roster_host::{bind, RosterConfig}; fn cert_pair() -> (String, String) { let base = concat!(env!("CARGO_MANIFEST_DIR"), "/../fifa17-recon/tools"); ( format!("{base}/redir_cert.pem"), format!("{base}/redir_key.pem"), ) } /// Start a host on an ephemeral port; hand back its address and its counters. fn start() -> ( std::net::SocketAddr, openfut_roster_host::ProbeCount, openfut_roster_host::Outcomes, ) { let (c, k) = cert_pair(); let server = bind(RosterConfig::for_test(&c, &k)).expect("host binds"); let addr = server.local_addr; let (probes, outcomes) = (server.probes(), server.outcomes()); std::thread::spawn(move || { let _ = server.run(); }); (addr, probes, outcomes) } /// A FIFA-like client: legacy suites, no certificate checking. Sends a GET and /// reads the response to EOF. fn tls_get(addr: std::net::SocketAddr) -> Vec { use openfut_tls::{SslConnector, SslMethod, SslVerifyMode}; let mut b = SslConnector::builder(SslMethod::tls()).expect("connector"); b.set_cipher_list(openfut_adapter_fifa17::tls::OBSERVED_CLIENT_SUITES) .expect("ciphers"); b.set_verify(SslVerifyMode::NONE); let sock = TcpStream::connect(addr).expect("connect"); let ssl = b .build() .configure() .and_then(|c| c.verify_hostname(false).into_ssl("roster-test")) .expect("ssl"); let mut s = openfut_tls::SslStream::new(ssl, sock).expect("stream"); s.connect().expect("handshake"); s.write_all( b"GET /fifa17/fut/rosterupdate.xml HTTP/1.1\r\nHost: roster-test\r\nAccept: */*\r\n\r\n", ) .expect("write"); s.flush().ok(); let mut out = Vec::new(); let _ = s.read_to_end(&mut out); out } /// The whole point: connect, send nothing, close — classified as a probe rather /// than reaching the acceptor. Asserting on the counter (not on client-visible /// behaviour) is deliberate: a probe produces no response either way, so a test /// on what the client sees would pass with the classification deleted. #[test] fn a_bare_connect_and_close_is_classified_as_a_probe() { let (addr, probes, outcomes) = start(); drop(TcpStream::connect(addr).expect("probe connects")); std::thread::sleep(Duration::from_millis(200)); assert_eq!( probes.load(Ordering::Relaxed), 1, "a connect-and-close was not classified as a probe" ); assert!( outcomes.lock().expect("lock").is_empty(), "a probe must not be recorded as a served connection" ); } /// A probe must not disturb the host: the next real client still gets served. #[test] fn a_probe_does_not_break_the_connection_that_follows_it() { let (addr, probes, _outcomes) = start(); drop(TcpStream::connect(addr).expect("probe connects")); std::thread::sleep(Duration::from_millis(100)); let response = tls_get(addr); assert!( response.starts_with(b"HTTP/1.0 200"), "real client after a probe got: {:?}", String::from_utf8_lossy(response.get(..64).unwrap_or(&response)) ); assert_eq!( probes.load(Ordering::Relaxed), 1, "the real client was miscounted as a probe" ); } /// The dangerous direction: a client that DOES speak and then fails must still /// reach the acceptor and be reported as a fault, not silently reclassified as a /// benign probe. #[test] fn a_client_that_speaks_then_fails_is_not_a_probe() { let (addr, probes, _outcomes) = start(); let mut sock = TcpStream::connect(addr).expect("connect"); // One byte of nonsense: the peer has spoken, but it is not a ClientHello, so // the handshake genuinely fails. sock.write_all(&[0x16]).expect("write"); sock.flush().ok(); drop(sock); std::thread::sleep(Duration::from_millis(200)); assert_eq!( probes.load(Ordering::Relaxed), 0, "a failing handshake was silently reclassified as a benign probe" ); }