roster-host: transport host for the FUT roster update, lifecycle-matched

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>
This commit is contained in:
funman300
2026-08-11 17:41:11 +00:00
parent 84e81f2037
commit c9ae914910
9 changed files with 939 additions and 1 deletions
+15
View File
@@ -0,0 +1,15 @@
//! Entry point for the FUT roster-update host.
fn main() -> std::io::Result<()> {
// `--identity` so a lifecycle script can ask the binary which commit it was
// built from, rather than inferring it from a file's mtime. Checking the
// artifact on disk instead of the running process has already produced one
// confidently wrong refusal on this project.
if std::env::args().any(|a| a == "--identity") {
println!("{}", openfut_roster_host::identity());
return Ok(());
}
let cfg = openfut_roster_host::RosterConfig::from_env()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?;
openfut_roster_host::serve(cfg)
}