//! Redirector host configuration. //! //! Endpoints resolve through `openfut-host-config`, the single environment //! reader, so this host and the Blaze sidecar build client-visible addresses by //! the same construction path. Only the transport settings — listener, cert, //! key, TLS knobs — are parsed here, and none of them are client-visible. use std::time::Duration; use openfut_adapter_fifa17::blaze::AdapterConfig; use openfut_host_config::{self as hostcfg, ConfigError}; use openfut_tls::{ProtocolVersion, TlsConfig}; /// How long the oracle holds a redirector connection open after responding /// (`time.sleep(0.3)` in `blaze_responder_v3b.redir_handle`). /// /// Measured, not guessed: a differential lifecycle probe records the Python /// redirector holding the socket ~300ms and the first version of this host /// closing at 0ms. FIFA 17 was proven against the former, so it is the default /// here. Not a tuning knob — changing it changes what this host is a /// reimplementation OF. pub const ORACLE_CLOSE_DWELL: Duration = Duration::from_millis(300); /// Compose the shared TLS listener from the FIFA 17 adapter's observed profile. /// /// The split is deliberate: `openfut-tls` knows how to build an acceptor, /// `openfut-adapter-fifa17` knows what FIFA 17 offers, and this host knows only /// how to join them. A second host serving the same client uses this same pair /// rather than repeating either half — one configuration path is the structural /// guard against the 2026-08-11 certificate mismatch, where two services /// configured TLS separately and disagreed. fn fifa17_tls(cert: impl Into, key: impl Into) -> Result { use openfut_adapter_fifa17::tls as profile; let ver = |s: &str| { ProtocolVersion::parse(s).map_err(|e| ConfigError(format!("FIFA 17 TLS profile: {e}"))) }; Ok(TlsConfig::new( cert, key, profile::CIPHER_LIST, ver(profile::MIN_VERSION)?, ver(profile::MAX_VERSION)?, )) } #[derive(Debug, Clone)] pub struct RedirectorConfig { /// BIND: where this listener binds. Never client-visible. pub listen_addr: String, /// Required, with no default, so the host can never collide with the /// Python redirector it runs beside. pub listen_port: u16, pub tls: TlsConfig, /// ADVERTISE and everything derived from it. pub adapter: AdapterConfig, /// Dwell before closing an answered connection. Overridable ONLY so the /// causal experiment — set it to 0 and confirm the failure returns — can be /// run without rebuilding. pub close_dwell: Duration, } impl RedirectorConfig { pub fn from_env() -> Result { let adapter = hostcfg::adapter_from_env()?; let listen_port = hostcfg::required_port( "OPENFUT_REDIRECTOR_HOST_PORT", "this host runs beside the working Python redirector and must not \ collide with it, so the port is explicit and has no default", )?; let listen_addr = hostcfg::optional("OPENFUT_REDIRECTOR_HOST_BIND", &adapter.endpoints.bind); let cert = hostcfg::required( "OPENFUT_REDIRECTOR_CERT", "path to the RSA certificate; reuse the proven redirector's so TLS \ implementation stays the only variable in an A/B", )?; let key = hostcfg::required("OPENFUT_REDIRECTOR_KEY", "path to the matching private key")?; // FIFA 17's observed profile, supplied by the adapter. The transport // host chooses no cipher and no protocol window of its own: those are // facts about the client, and belong with the game they describe. let mut tls = fifa17_tls(cert, key)?; if let Some(list) = hostcfg::optional_opt("OPENFUT_REDIRECTOR_CIPHERS") { tls.cipher_list = list; } // Overridable so a failed retail handshake is a configuration change, // not a code change — but never lowered pre-emptively. if let Some(level) = hostcfg::optional_opt("OPENFUT_REDIRECTOR_SECURITY_LEVEL") { tls.security_level = level.trim().parse().ok(); } let close_dwell = match hostcfg::optional_opt("OPENFUT_REDIRECTOR_CLOSE_DWELL_MS") { None => ORACLE_CLOSE_DWELL, Some(v) => Duration::from_millis(v.trim().parse().map_err(|_| { ConfigError(format!( "OPENFUT_REDIRECTOR_CLOSE_DWELL_MS is not a number of milliseconds: {v:?}" )) })?), }; Ok(RedirectorConfig { listen_addr, listen_port, tls, adapter, close_dwell, }) } pub fn listen_on(&self) -> String { format!("{}:{}", self.listen_addr, self.listen_port) } /// Test fixture: the oracle's certificate, an ephemeral port, and an /// explicit advertised address. #[doc(hidden)] pub fn for_test(advertise: &str) -> RedirectorConfig { let base = concat!(env!("CARGO_MANIFEST_DIR"), "/../fifa17-recon/tools"); RedirectorConfig { listen_addr: "127.0.0.1".into(), listen_port: 0, tls: fifa17_tls( format!("{base}/redir_cert.pem"), format!("{base}/redir_key.pem"), ) .expect("the adapter's TLS profile must be valid"), adapter: AdapterConfig::advertising(advertise), close_dwell: ORACLE_CLOSE_DWELL, } } }