Files
fifa-blaze/README.md
T

199 lines
7.3 KiB
Markdown

# 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`](https://github.com/jacobtread/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:
```toml
[blaze]
framing = "raw" # bypass framing entirely, capture byte streams
```
## Quick start
```bash
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:
```bash
cargo test
```
## Reading captures
```bash
# 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:
```bash
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](https://github.com/PocketRelay/Server), 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:
1. Confirm the framing variant from the raw bytes
2. Identify the redirector request/response tag layout
3. Identify Util `preAuth` / `postAuth` / `ping`
4. Identify Authentication `login`
5. Implement handlers via `Dispatcher::register` and test against the client
Prior art worth reading before guessing: [`PocketRelay/Server`](https://github.com/PocketRelay/Server)
(ME3, Rust), [`openBlase`](https://github.com/openBlase/openBlase), and
[`Tratos/New-Blaze-Emulator`](https://github.com/Tratos/New-Blaze-Emulator) (BF3). For the
FUT REST layer that follows, [`futapi/fut`](https://github.com/futapi/fut) documents real
`fut.ea.com` endpoint shapes.
## Development
```bash
cargo build
cargo test
RUST_LOG=debug cargo run --bin blaze-server -- config.toml
```