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.
This commit is contained in:
funman300
2026-08-11 04:12:32 +00:00
parent 288d990821
commit e2c4ca6d56
3 changed files with 287 additions and 13 deletions
+27
View File
@@ -5,11 +5,23 @@
//! 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 crate::tls::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);
#[derive(Debug, Clone)]
pub struct RedirectorConfig {
/// BIND: where this listener binds. Never client-visible.
@@ -20,6 +32,10 @@ pub struct RedirectorConfig {
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 {
@@ -51,11 +67,21 @@ impl RedirectorConfig {
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,
})
}
@@ -76,6 +102,7 @@ impl RedirectorConfig {
format!("{base}/redir_key.pem"),
),
adapter: AdapterConfig::advertising(advertise),
close_dwell: ORACLE_CLOSE_DWELL,
}
}
}