Commit Graph

10 Commits

Author SHA1 Message Date
funman300 55b4e54d5f gate-evidence: add the Python-side positive/negative observation
'Rust did not receive it' is weaker than 'Python did'. The redirector always
runs on Python and is never switched, so it advertises the Blaze endpoint on
every run; whether Python then receives the Blaze CONNECT it just advertised
says where the hop actually went.

This is already visible in the existing logs and settles gate 5-6 more firmly
than the sidecar record alone:

  02:02:13  Python REDIR SENT -> 10.10.0.120:42130  (to .105)
  02:02:13  Rust  conn-0005 CONNECT from 10.10.0.105
            Python received NO Blaze CONNECT

Same second, both sides: Python advertised the endpoint and did not get the
connection; Rust did. It is also the mechanism gate 8 needs in reverse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:21:21 +00:00
funman300 fafa2f1858 blaze-host: document capture, sanitization, and what the tests do not cover 2026-08-11 02:16:11 +00:00
funman300 c84fd14cac blaze-host: make the build stamp trustworthy for evidence attribution
Committing updates refs/heads/<branch>, not the HEAD file, so watching HEAD
alone left the stamp one commit behind -- observed live, the banner read
a84a72e immediately after 2337431 was committed. build.rs now also watches the
resolved branch ref.

Belt and braces, since cargo still cannot see every source change: sidecar.sh
compares the binary's stamped commit against the tree's real HEAD at launch and
says so loudly on a mismatch. An evidence artefact that names the WRONG commit
is worse than one that names none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:15:11 +00:00
funman300 23374312bc blaze-host: opt-in raw frame capture + auditable sanitizer
Evidence infrastructure, not protocol functionality. Built before gates 7-10
because those sessions cannot be reproduced -- a later run is a different
session, and the migration-validation runs happen once. Gates 5-6 already went
past without their bytes being recorded.

TWO LAYERS

  live FIFA traffic
    ├── raw capture      exact RX/TX bytes, mode 0600, gitignored
    └── blaze-sanitize   → repository-safe, replayable fixtures

CAPTURE. Off unless OPENFUT_BLAZE_CAPTURE names a file. Deterministic
big-endian container: 20-byte file header, then per-frame records carrying
connection id, a global monotonic sequence, timestamp, direction and the EXACT
frame bytes. RX is recorded as received; TX only AFTER a successful write, so a
record means the bytes were sent rather than intended.

Component/command/msgNum/msgType/payload length are deliberately NOT stored
beside the frame: they are already in its 16-byte header, and a redundant copy
can disagree with the bytes, leaving a reader unable to tell which is true.
Record::header() derives them, so every field the requirements name is
available without duplicating it.

SANITIZER. Redacts only the named tags in SENSITIVE_TAGS (KEY, AUTH, SESS,
MAIL, PML) and reports every substitution with path, kind and length.
Replacement is LENGTH-PRESERVING, so the TDF varint, payload length and Fire2
header are unchanged and the sanitized frame is exactly the size of the
captured one -- asserted per frame, failing rather than emitting a subtly
different conversation. Frames with nothing sensitive keep their exact wire
bytes. Payloads that will not decode are passed through and REPORTED, so a
reader knows they were never inspected rather than assuming they were checked.

TESTS. 39 in this crate. All nine required cases: capture disabled produces no
artefact; RX and TX captured exactly; ordering preserved; fragmented input
(one byte at a time) reconstructs the same frames as a single write; coalesced
input is captured as separate frames, not per-read; capture does not alter wire
output; sanitization removes a real session key from a real captured login;
malformed/truncated/wrong-version captures fail clearly; every listed sensitive
tag is provably reachable.

MUTATION TESTED. Dropping TX capture, truncating captured frames to their
header, and removing KEY from the sensitive list were each verified to turn the
suite red. One mutation was NOT caught: moving the TX capture above the write.
It is indistinguishable while writes succeed and only diverges when one fails.
That invariant is held by code placement and a comment saying so, not by a
test, and the code says as much rather than implying coverage it does not have.

Python oracle unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:14:25 +00:00
funman300 a84a72e0c0 blaze-host: A/B the RPC routes a real FIFA session actually used
The gate 5-6 live run exercised 14 RPCs the recorded fixtures never covered --
Stats, Clubs, OSDKSettings, SponsoredEvents, Messaging::fetchMessages,
Util::getTelemetryServer, Util::userSettingsLoadAll, UserSessions cmd 0x0008,
and the transport PING -- every one taking the empty-reply fallback.

That Python does the same was an inference from reading its dispatch table.
This sends those exact routes to both backends and diffs the replies:
14/14 byte-identical.

Worth keeping: the fixtures were built from what the responder implements, so
they could never have covered what the client asks for and the responder does
not. Only a live session reveals that surface, and this makes it checkable
afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:05:18 +00:00
funman300 48aa955212 blaze-host: per-gate evidence capture, separating asserted from observed
For the live FIFA gates. Records switch rules, sidecar status, log, trace and
the Python contract result into a timestamped bundle, and reports CONFIGURED
and OBSERVED state as two distinct sections.

The separation is the whole point. 'blaze-switch.sh status = ON' is an
assertion produced by the same tooling that performs the switch, and that
tooling reported a successful rollback once when none had happened. The
observed half comes from an unrelated source: the sidecar's own record of
which peers connected to it. A non-loopback peer in that log proves the
client's Blaze traffic landed on Rust without depending on reading an iptables
rule correctly.

Verified both ways: loopback-only traffic reports 'a FIFA session did NOT land
here'; a non-loopback peer reports that it observably did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:00:31 +00:00
funman300 cf3ddde3a6 blaze-host: move the dirty-tree safeguard to launch and evidence time
The compiled-in dirty flag cannot be trusted for this job. Cargo does not
re-run a build script when another crate's source changes, so editing the
adapter and rebuilding the host leaves it reading 'clean' -- verified by
appending a line to the adapter and watching the flag not move.

So the stamp now only names the commit, and the real safeguards run at the
moment they matter and cannot go stale:

  * sidecar.sh checks the working tree at LAUNCH and warns.
  * check-live-parity.sh REFUSES on a dirty tree, since it produces the
    artefact a migration decision is made from. ALLOW_DIRTY=1 overrides for a
    throwaway check.

Both scope to the three migration crates, so unrelated submodule dirt does not
trigger them -- a warning that is always on is a warning nobody reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:56:43 +00:00
funman300 468b006008 blaze-host: scope the dirty-tree check to the crates the binary is built from
A whole-repo check read DIRTY permanently, because unrelated submodules carry
pre-existing modifications. A warning that is always on is a warning nobody
reads, which defeats the point: the flag exists so a mutated build announces
itself before it can be mistaken for parity evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:55:20 +00:00
funman300 e091921b18 blaze-host: safe sidecar lifecycle, Blaze switch, build identity
Prerequisites for the live FIFA A/B. Two safeguards here exist because the
corresponding failure actually happened, not because it was imagined.

BUILD IDENTITY. build.rs stamps commit + working-tree cleanliness; the host
prints commit, tree state, profile and a fingerprint of the bundled config
table at startup, into both the log and the trace. A dirty tree prints an
explicit "do NOT treat results from this binary as parity evidence" warning.
The previous step left four sidecars running, two serving mutated builds, and
nothing in their output said so.

SIDECAR LIFECYCLE (sidecar.sh). start/stop/status/check-orphans/with. Start
refuses when any sidecar is already running or the port is busy. Stop kills,
waits, then PROVES it: PID gone AND port free AND no stray processes, failing
if any check does not hold. `with -- CMD` traps EXIT/INT/TERM so cleanup runs
however the command exits.

  Bug found and fixed while testing it: orphan detection used `pgrep -f`,
  which matched any process whose command line merely mentioned the name --
  including the shell running the test script. It now matches the resolved
  executable via /proc/PID/exe. `pgrep -x` is unusable because Linux truncates
  the process name to "openfut-blaze-h".

BLAZE SWITCH (blaze-switch.sh). Redirects Blaze to the sidecar with a scoped
NAT rule instead of editing the frozen Python oracle, whose redirector
advertises a hardcoded BLAZE_PORT = 42130. Rules match only <LAN_IP>:42130;
127.0.0.1:42130 is deliberately left alone so Python stays reachable on
loopback and the A/B compares real Python against real Rust. Verified both
directions live: LAN->Rust with the switch on, LAN->Python with it off.

  Bug found and fixed: `off` reported success while two rules remained active
  and rollback had NOT happened. It matched `--comment "tag"` with quotes this
  iptables does not emit -- and the verification used the SAME broken matcher,
  so it confirmed its own failure. A rollback that lies is worse than one that
  fails. Now matched on the bare tag, verified with iptables-save plus a
  tag-independent check that nothing still redirects the port.

  Second flaw fixed: `sidecar.sh stop` originally warned about a live switch
  and then stopped anyway, creating the exact broken state it warned about. It
  now REFUSES, with --force as the deliberate override.

The general rule this all converges on, now stated in the README: a
verification must not share the failure mode of the thing it verifies.

116 tests still passing; clippy clean; Python backend untouched and contract
suite 446/446. NAT table left clean, no orphan processes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:54:40 +00:00
funman300 a9eb54ae9c openfut-blaze-host: thin Blaze sidecar, live-parity with Python
Third migration step, and the one that turns fixture parity into transport
parity. A TCP host that frames a Fire2 stream, keeps one Session per
connection, calls openfut-adapter-fifa17::dispatch(), and writes the returned
frames in order. It owns a socket, a buffer, a session and diagnostics --
that is the complete list. No coins, club, packs, profiles or UTAS logic:
those belong to Core, reached through the adapter later.

NO TLS, and that is evidence-based rather than an omission. The Blaze main
port is plaintext: sending a raw Fire2 Util::ping to the running backend
returns a plaintext PingResponse, blaze_handle uses the raw socket, and only
redir_handle wraps ssl. TLS belongs to the redirector phase.

LIVE A/B AGAINST THE RUNNING PYTHON BACKEND: 101 frames across three
conversations, identical normalized traces. This is the first result in the
migration that is not purely offline. check-live-parity.sh replays the
recorded conversations against both endpoints over real sockets and diffs
volatile-masked traces; session keys and clocks are masked, so anything that
differs is behavioural.

Transport tests cover what fixtures cannot: byte-for-byte replay over a
socket, requests dribbled one byte at a time, several requests in one write,
the four-frame login burst ordered on the wire, session state persisting
across frames and NOT leaking between connections, an absurd payload length
closing the connection instead of allocating, and an undecodable body still
getting a reply. 18 tests here, 116 across the three migration crates.

MUTATION TESTED, including the comparison itself. Dropping a post-login
notification is caught by the probe (frame count) AND the diff; a same-length
content change deep inside a notification body (CTY "US"->"GB", payload 116
both sides) is caught ONLY by the trace digest. So the probe's exit code is
not the test -- the diff is, and the README says so. check-live-parity.sh was
itself verified to exit 1 under mutation.

The listen port is required configuration with no default, so the sidecar
cannot silently collide with the working container. OPENFUT_BIND stays the
advertised-config bind (the adapter derives nucleusConnect from it,
reproducing the oracle) and the listener gets its own setting, so the two are
not conflated.

Gates 1-4 pass and are re-runnable. Gates 5-10 need a FIFA client and are
listed in the README, including the Python -> Rust -> Python -> Rust
back-and-forth that proves the rollback path rather than asserting it.

Python backend untouched and still the live runtime; contract suite 446/446
after this work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:42:23 +00:00