fifa-blaze
EA Blaze protocol server emulator — FIFA 17 target.
Why FIFA 17 and not FIFA 23
FIFA 23 was the original target. It does not work, for a structural reason documented in
../openfut-bridge/docs/closure-and-preservation.md: the FIFA 23 client never dials.
The functions that register the redirector dial are Blaze message handlers, dispatched
only once Blaze messages are already flowing — so the dial requires the connection it is
supposed to create. Offline that handler container is empty and nothing can break the
circle. Two further walls sit behind it (ProtoSSL cert pinning, and an anti-tamper VM that
faults on forced state).
FIFA 17 differs categorically, not by degree:
| FIFA 17 (2016) | FIFA 23 (2022) | |
|---|---|---|
| Online entry | Client dials gosredirector directly |
Gated behind an EA-App handshake with no counterpart |
| Anti-tamper | None | VM faults deterministically on forced state |
| Transport | ProtoSSL — SSLv3 + RC4 | Pinned modern TLS (CertificateUnknown) |
| Feedback loop | You get a wire on day one | Never dials → nothing to iterate against |
That last row is the one that matters. Redirect the hostname, and the client connects and starts talking — which restores the oracle the whole method depends on: send a frame, watch the client react, correct, repeat.
openfut-core is unaffected by any of this. It is game-independent and already complete;
only the bridge layer is title-specific.
Status
Milestone 1 — capture stub. Two SSLv3 listeners start and log every byte. No FIFA 17 component/command IDs are known yet — the capture log is how they are discovered.
Milestone 1 goal: one real frame in captures/. The FIFA 23 effort produced zero in a
month.
Architecture
FIFA 17 (hosts file, or the openfut-hook DLL under Proton)
│
▼ gosredirector.ea.com → 127.0.0.1:42127
blaze-server redirector listener (SSLv3, Fire2 framing)
│ replies: "connect to 127.0.0.1:10041"
▼
blaze-server Blaze listener (SSLv3, Fire2 framing)
│
├─ raw byte tee → captures/<label>-<port>.raw ← always, before framing
└─ framed packet → captures/<timestamp>.jsonl ← only if framing is right
Transport: SSLv3, not TLS
EA's DirtySDK speaks ProtoSSL, a homegrown SSLv3 restricted to RC4-SHA / RC4-MD5.
A modern TLS stack cannot negotiate with it at all — no shared version, let alone a shared
cipher. This server uses blaze-ssl-async,
which implements that dialect and ships a certificate built to satisfy old ProtoSSL
verification.
There are no certificates to generate. The certs/ directory and the old openssl
step are obsolete; the [tls] config section is gone.
The raw tee
Every byte is mirrored to captures/<label>-<port>.raw before framing is attempted:
IN 12 0000000900000001...
OUT 30 0000001e00090001...
This exists because the FIFA 23 run ended with six capture files containing zero bytes — captures were only written after a packet decoded, so a wrong framing guess destroyed the very evidence needed to fix the guess. With the tee, a bad guess still yields bytes.
Disable with raw_tee = false in [capture]. Don't.
Framing
Fire and Fire2 both exist. FIFA 17 is post-2012 so Fire2 is the likely variant, but
this is UNCONFIRMED. It is a config value, not a constant — flip it without rebuilding:
[blaze]
framing = "raw" # bypass framing entirely, capture byte streams
Quick start
cp config.example.toml config.toml # edit if needed
cargo run --release --bin blaze-server -- config.toml
Then redirect the hostname:
127.0.0.1 gosredirector.ea.com
…or deploy openfut-hook to redirect inside Proton, and launch FIFA 17 and go online.
Verifying without FIFA
The SSLv3 path is covered by a test that completes a real handshake and round-trips application bytes through the tee — so the server can be validated before touching the game:
cargo test
Reading captures
# Raw bytes (always present)
cat captures/blaze-*.raw
# Framed packets (only if the framing guess is right)
jq '.' captures/capture-*.jsonl | less
jq -r '[.component, .command] | @tsv' captures/*.jsonl | sort -u
Replaying a capture offline
blaze-replay feeds a recorded .raw back through the codec, so a framing hypothesis is
tested against real bytes in a second — no game, no rebuild:
blaze-replay captures/blaze-54321.raw # test fire2
blaze-replay captures/blaze-54321.raw --framing raw # dump unparsed, read the header by hand
blaze-replay captures/blaze-54321.raw --dir out # decode our replies instead
It concatenates records before decoding (TCP does not preserve message boundaries) and ends with a verdict:
#1 component=0x0009 command=0x0007 type=REQUEST seq=1 error=0 body=0B
2 packet(s) decoded, 0 byte(s) undecoded
VERDICT: fire2 is a clean fit — every byte accounted for.
What the server answers today
src/components.rs carries the Blaze component/command tables and turns capture lines
from raw hex into names like Util.preAuth, so an unknown ID stands out as unknown.
src/routes.rs holds the response bodies. Three commands are answered; everything else
falls through to an empty response so the client keeps talking and the log keeps growing.
| Command | Status |
|---|---|
Redirector.getServerInstance |
ADDR union + VALU{HOST,IP,PORT}, SECU, XDNS |
Util.ping |
STIM server clock |
Util.preAuth |
minimal scaffold — the gate that must be right before auth is attempted |
All of it is TODO/CONFIRM. The tags and structure come from open-source emulators for
other EA titles (chiefly PocketRelay, ME3, MIT);
no EA source is involved. Component IDs are framework-level and stable across titles;
command IDs are per-title and may well differ for FIFA 17. When a capture disagrees
with anything in these files, the capture wins.
The point of answering at all is that an empty body is guaranteed wrong and teaches nothing, whereas a structurally plausible one either advances the client — a result — or is rejected with an error code that is itself information.
A note on TDF tags
Tags are not ASCII on the wire. TDF packs each character into 6 bits across 3 bytes, so
ADDR never appears literally in a capture. Grepping bytes for tag names will always fail;
parse instead (blaze-replay, or TdfStringifier as the tests in routes.rs do).
Next steps (Milestone 2)
Once captures reveal component/command IDs:
- Confirm the framing variant from the raw bytes
- Identify the redirector request/response tag layout
- Identify Util
preAuth/postAuth/ping - Identify Authentication
login - Implement handlers via
Dispatcher::registerand test against the client
Prior art worth reading before guessing: PocketRelay/Server
(ME3, Rust), openBlase, and
Tratos/New-Blaze-Emulator (BF3). For the
FUT REST layer that follows, futapi/fut documents real
fut.ea.com endpoint shapes.
Development
cargo build
cargo test
RUST_LOG=debug cargo run --bin blaze-server -- config.toml