199 lines
7.3 KiB
Markdown
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
|
|
```
|