fc00b0c6f9d67ddff5e345feb3bfabed02531789
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
05f6147433 |
http: extract the shared body drain; fix a flaky test race it exposed
Queued cleanup, run only AFTER the roster gate closed in both directions,
so the live A/B changed exactly one thing.
The two `drain_body` implementations were character-for-character
identical, so the extraction is a move. What it guards is not cosmetic:
answering while the client is still sending leaves unread data in the
receive queue and Linux turns the close into an RST rather than a FIN --
invisible in any comparison of the response, and worth two live gate
attempts to find. Behaviour that must be identical across hosts gets one
implementation, the same reasoning that produced openfut-tls.
SCOPE IS DELIBERATELY NARROW. Only the byte-identical part moved. The two
head-reading loops are NOT identical and stay where they are:
redirector roster
head cap 65536 16384
read chunk 4096 1024
on error abort proceed if any bytes arrived
Those differences are probably accidental, but each host is gate-proven
with the values it has. Unifying them would be a behaviour change wearing
a refactor's clothes -- exactly the mistake this project has already paid
for. They converge later as their own change with their own gate, or not
at all.
Purity shown, not asserted: every existing test in both hosts still
passes (426 workspace tests), and 7/7 mutations are killed, including
three in the SHARED crate that must break both hosts at once and one per
host that skips the drain call.
Three test cases neither host had now exist, because the extracted code
finally had somewhere to be tested directly: a malformed Content-Length,
an unterminated head, and a lookalike header. That last one matters --
`X-Original-Content-Length: 99` would drain 99 bytes that were never sent
if the match were `contains` rather than `starts_with`, and a mutation
confirms the test catches it.
Also fixes a race this run exposed in openfut-tls's own tests: keypair()
returned early if the certificate file existed, but wrote the certificate
BEFORE the key, so a parallel test could observe a cert whose key had not
landed. It failed one run and passed the next -- the kind of flake that
gets rerun instead of fixed. Now generated once per process via OnceLock,
key written first, and the suite was repeated five times to confirm.
Nothing deployed and nothing restarted: the running redirector and roster
are still the gate-proven binaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8f3b659c33 |
lifecycle: one host-lifecycle helper; roster.sh; ban pkill -f
redirector.sh and the coming roster.sh needed the same five rules, each
of which cost something to learn:
* resolve /proc/PID/exe; never match a command line. `pkill -f` /
`pgrep -f` match any shell whose ARGUMENTS mention the name, including
the shell running the command. That has killed this session's own
shell twice, and is now banned in migration tooling -- the helper
contains no `-f` matching and the header says why.
* `readlink`, not `readlink -f`. After a rebuild the link reads
"<path> (deleted)" and -f resolves it to nothing, so the orphan check
goes blind to exactly the long-lived processes it exists to find. Two
orphans hid there, one serving the wrong certificate.
* stop PROVES the process is gone and the port free.
* an ambiguous binary is an error for start/verify but NOT for
stop/status: rollback must never be blocked by a question about the
build tree.
* verify the RUNNING process's commit, not the artifact on disk, which
a rebuild can silently advance past.
Copying those into a second script would have been the same mistake as
copying the TLS setup. Instead scripts/host-lifecycle.sh owns them and a
service supplies four facts: name, crate, executable, port variable.
redirector.sh goes from 178 lines to 26 and roster.sh is 24, with no
behaviour change -- the refactored redirector.sh still sees the live
armed process (pid 830736, port 42227) and still refuses correctly
because HEAD has moved past it.
Paths are unchanged (rundir, pidfile, portfile, commit stamp, log), so
the currently running redirector stays manageable across this refactor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
84e81f2037 |
tls: extract a shared listener; move FIFA 17's profile into its adapter
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>
|
||
|
|
aa2679162d |
redirector.sh: never leave it ambiguous which binary is under test
Two defects, both found by the guards misfiring rather than by reading: 1. BIN preferred target/debug and fell back to release only when debug was absent. `cargo build --release` therefore produced a correct binary while the script kept inspecting a stale debug one, and the build guard refused with a message naming a commit nobody was trying to run. The guard was right that something was stale — it just pointed at the wrong artifact. Disagreement between the two is now an explicit refusal naming both, with OPENFUT_REDIRECTOR_BIN as the deliberate override. The refusal is recorded at load and raised only by `verify` and `start`. `stop` and `status` must work in any build-tree state: rollback can never be blocked by a question about which artifact would have been started. 2. `verify-running` read the stamp file without checking the process still existed. The stamp outlives the process, so after a stop it reported on a corpse — either "identity OK" or a REFUSAL naming a commit, both implying something was running when nothing was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ca63095786 |
lifecycle: stop the orphan check going blind when the binary is rebuilt
`list_procs` matched on `readlink -f /proc/PID/exe`. Once the binary is
rebuilt -- which happens constantly here, `cargo test` alone is enough -- the
link reads "<path> (deleted)" and -f resolves it to something that matches
nothing. The scan then finds zero processes, so `start`'s orphan check passes
and a second instance can be launched alongside a stray.
Not theoretical. Two orphans were running undetected tonight:
pid 592731 :42327 a stale-cert redirector left from testing check-tls-parity,
still serving F9:16:1A -- the exact certificate whose
mismatch cost three live gates
pid 542693 :42230 a Blaze sidecar debug build from 03:12
Neither was in a client path, so neither was doing harm, but a stray listener
serving the known-bad certificate is precisely what should never sit around
unnoticed.
Fixed by using plain readlink and stripping the " (deleted)" suffix. Shown both
ways: with the bug `status` reports no processes at all for a live pid; with the
fix it reports 604454. The pidfile path was unaffected, which is why `stop` kept
working and hid this.
|
||
|
|
cfb0435d96 |
redirector.sh: verify the RUNNING process's commit, not just the binary on disk
`verify` inspects `$BIN --identity`, which is the file on disk. That is not necessarily what is serving. Caught during gate 14 setup: the live process had been started from |
||
|
|
e2c4ca6d56 |
redirector-host: reproduce the oracle's connection lifecycle, not just its bytes
Two live gate attempts failed with "An error occurred downloading the FUT
Squad Update" while the redirect response was verified byte-identical to the
Python oracle. Rolling back to the Python redirector fixed it, so the response
bytes were never the whole contract.
Log archaeology found the discriminator: the client polls
/fifa17/fut/rosterupdate.xml ~4x/min in every successful FUT session, and the
only gap in 300 recorded fetches is 03:47-03:58 -- exactly the two
Rust-redirector sessions. The Blaze RPC sequence over those sessions is
identical (msgNum 0-53), so the divergence is entirely outside Blaze.
A differential lifecycle probe against both redirectors found the two
behaviours this host never reproduced:
* the oracle drains the request body per Content-Length; this host stopped
at the header terminator, leaving unread data in the receive queue, which
makes Linux close with RST rather than FIN
* the oracle holds the connection open ~300ms before closing
(time.sleep(0.3)); this host closed at 0ms
Both are now reproduced. The dwell is a named constant, ORACLE_CLOSE_DWELL,
overridable only so the causal experiment -- set it to 0, confirm the failure
returns -- can be run without a rebuild.
The suite could not have caught either: it sent Content-Length: 0, so there
was never a body to drain. It now POSTs a body, and asserts a split-write body
is fully consumed.
Testing the drain via client-visible symptoms does NOT work -- verified by
mutation: with the drain removed the client still reads the buffered response
and sees close_notify before any reset. So the host records a per-connection
ConnOutcome and the test asserts on that. Both mutations (no-dwell, no-drain)
are now each caught by exactly one test.
This does not yet prove causation for the FUT Squad Update failure; it removes
the only two measured divergences. Gate 6 is the test.
|
||
|
|
c03702707b |
redirector: commit stamp + shared build-identity verifier that REFUSES
The binary records only the commit it was built from -- no dirty-tree flag. Cargo will not re-run a build script because another crate's source changed, so a compiled-in 'clean' claim can be stale and is not a safeguard; that was verified on the Blaze host. scripts/verify-build-identity.sh establishes both facts at LAUNCH, where they cannot go stale: the stamped commit equals HEAD, and the migration crates are clean. It REFUSES rather than warns, because for a migration gate a warning on stderr is something to scroll past. --identity prints the stamp without valid configuration. The launcher must be able to establish which commit a binary came from BEFORE deciding whether to run it; requiring a correct environment first would invert the check. redirector.sh mirrors sidecar.sh: refuses to start with an orphan present or the port busy, matches the resolved executable rather than the command line (pgrep -f matches any shell mentioning the name), and stop PROVES the process is gone and the port free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
89f77470f3 |
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> |