Second consumer of openfut-tls, and the reason it was extracted first.
This host contains no roster content and no cipher choice: the adapter
owns the 67 bytes and the observed TLS profile, openfut-tls owns the
acceptor, and this crate owns accept/read/drain/write/close.
Lifecycle was MEASURED, not inherited. The obvious mistake here would
have been copying the redirector's 300ms dwell because the other host has
one. A probe against the oracle says otherwise:
dwell after responding 0 ms (redirector: 300 ms)
request body drained POST answered only once it arrives
close clean FIN, never RST
keep-alive none one request per connection
The probe ran against a REPLICA of roster_server.py loaded from its own
source, not against :8081 -- http.server.HTTPServer is single-threaded
and FIFA was mid-session, so holding a connection open to measure the
close would have stalled the game's poll and could have surfaced as the
squad-update error. The replica was then confirmed byte-identical to the
live oracle under masking, the 1-byte delta being the container's Python
version in the Server header.
Differential against the live oracle, every field identical, with the
Server header compared UNMASKED:
GET 230B HEAD 163B POST 163B
drained=True reset=False answered_before_body=False
keepalive: second request accepted by the socket, never answered
Testing follows the redirector's hard-won rule: where a property is
visible both to the client and inside the host, it is asserted inside the
host via ConnOutcome. A client-side check cannot tell "drained" from "not
drained" -- it reads the buffered response either way -- and that exact
mistake let a mutation survive once already.
9 parity tests, 6 unit tests, 5/5 mutations killed, including "answer
before draining", "hold the connection open like the redirector" and
"inherit the redirector's 300ms default".
drain_body is duplicated from the redirector deliberately. Unifying it
means editing the redirector, and the roster A/B must change exactly one
thing. Extraction is scheduled for after the roster gate closes.
Not deployed and not switched: Python still serves :8081.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The redirector was the only host that spoke TLS, so its TLS lived inside
it. The roster host needs the same listener, and that made the choice
explicit: share this code or copy it.
Copying it is what already went wrong. On 2026-08-11 the Rust redirector
served one certificate while the container served another. ProtoSSL
caches the server certificate per backend, so the redirector -- the first
TLS connection of a session -- decided what the client expected, and
every later service failed its handshake. Silently: Python's socketserver
swallows ssl.SSLError as OSError. Three gates went to it. One place to
configure TLS is the structural fix, so it exists before the second host
does rather than after.
Split along the line the architecture already draws:
openfut-tls how to build an acceptor. Game-independent.
Knows nothing about which suites any client
offers.
adapter-fifa17::tls what FIFA 17 was OBSERVED to offer: the six
enabled suites, the two refused, the TLS 1.2
window, the EA SNI. Plain strings, so the
adapter keeps its lean dependencies -- reading
a card table should not build OpenSSL.
redirector-host joins the two. Chooses no cipher of its own.
Behaviour is unchanged, and shown to be:
* tests/fifa17_tls_profile.rs carries over every case from the deleted
module -- FIFA's eight suites negotiate AES256-GCM-SHA384, each enabled
suite works alone, RC4-only is refused, ECDHE-only is refused. Deleting
a module must not quietly delete its evidence.
* one test pins the composed values literally against the host as it was
when gates 1-14 passed. A "pure refactor" that cannot fail is not a
claim, it is an assumption.
* the rebuilt binary self-tests to the same TLSv1.2 / AES256-GCM-SHA384
the retail client negotiated at 17:09 today.
Two improvements fall out of having one place to look:
* the startup banner now prints cert_sha256. The mismatch above raised no
error at startup and broke the client much later with nothing logged;
it is now the first line of the log.
* tls_min/tls_max print as TLSv1.2 rather than SslVersion(771). This line
is gate evidence and gets read by people.
Nothing deployed and nothing restarted: FIFA is mid-session on the
running redirector, which is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>