redirector: Rust host on vendored OpenSSL; shared typed config extracted

TLS DEPENDENCY, as directed: the openssl crate directly with the `vendored`
feature. NOT native-tls. native-tls abstracts over whatever the platform
provides; here the requirement is the opposite -- precise, evidenced behaviour
for one legacy client -- which needs explicit control of the cipher list,
protocol floor/ceiling and security level. Vendored so a distro libssl update
cannot silently change whether FIFA 17 can connect.

Scoped to this crate alone. Neither OpenFUT Core nor the generic protocol
crates gain an OpenSSL dependency.

CIPHERS driven by the captured retail ClientHello, not by generic legacy
assumptions. The six RSA+AES suites it offers are enabled; RC4 and MD5 are
deliberately NOT, even though the client offers them -- it already negotiates
AES256-GCM-SHA384, so resurrecting RC4 for completeness would weaken the
service for nothing. TLS 1.2 floor and ceiling, matching the observed client;
the floor is not dropped to 1.0 pre-emptively because "the oracle permits it"
is not "the client requires it".

SECURITY LEVEL IS NOT LOWERED. Tried the default policy first, as directed,
and OpenSSL 3.6.3 accepts static-RSA/AES without weakening. No SECLEVEL change
was needed and none is applied; it remains overridable per-listener with
evidence.

CERTIFICATE: the proven Python redirector's material is reused, so the TLS
implementation stays the only variable in an A/B. Verified RSA-2048, CN
winter15.gosredirector.ea.com, cert/key modulus match; the key stays
gitignored.

SHARED CONFIG. New openfut-host-config is now the only crate that reads the
environment, and both hosts resolve endpoints through it. Two hosts each
parsing OPENFUT_ADVERTISE would be exactly the "separate helpers constructing
endpoints from different sources of truth" the address audit forbids.

VERIFICATION BY REAL HANDSHAKE, not by enumeration. The crate exposes no
accessor for a context's configured suites at this version, which turned out
better: the host now rehearses the retail handshake at startup with a client
restricted to exactly FIFA's eight suites and REFUSES TO SERVE if it fails, so
a cipher/version misconfiguration surfaces at boot rather than as an
unexplained failure during a live gate.

Gates 1-5 pass: TLS config unit tests; a FIFA-suite-only client negotiates
TLSv1.2/AES256-GCM-SHA384; each enabled RSA+AES suite negotiable alone; an
RC4-only client is refused; an ECDHE-only client is refused (proving no modern
policy was silently inherited); a full HTTPS round-trip returns bytes
IDENTICAL to the Python oracle's recorded response.

Cargo.lock committed for reproducibility: openssl 0.10.81, openssl-sys 0.9.117,
openssl-src 300.6.1+3.6.3 (OpenSSL 3.6.3). Updating openssl-src is NOT a
routine bump -- it requires re-running the FIFA compatibility gates.

Gates 6-14 need the retail client and are next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-08-11 03:28:04 +00:00
parent 0d576a14b7
commit 89f77470f3
10 changed files with 1242 additions and 123 deletions
+81
View File
@@ -0,0 +1,81 @@
//! 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 openfut_adapter_fifa17::blaze::AdapterConfig;
use openfut_host_config::{self as hostcfg, ConfigError};
use crate::tls::TlsConfig;
#[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,
}
impl RedirectorConfig {
pub fn from_env() -> Result<RedirectorConfig, ConfigError> {
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")?;
let mut tls = TlsConfig::new(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();
}
Ok(RedirectorConfig {
listen_addr,
listen_port,
tls,
adapter,
})
}
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: TlsConfig::new(
format!("{base}/redir_cert.pem"),
format!("{base}/redir_key.pem"),
),
adapter: AdapterConfig::advertising(advertise),
}
}
}