wip: checkpoint Blaze server work for Windows migration
This commit is contained in:
@@ -1,112 +1,198 @@
|
||||
# fifa-blaze
|
||||
|
||||
EA Blaze protocol server emulator for FIFA 23 offline FUT.
|
||||
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 TLS listeners start and log every Blaze packet. No FIFA 23 component/command IDs are known yet — the capture log is how we discover them.
|
||||
**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 23 (via openfut-hook DLL / /etc/hosts)
|
||||
FIFA 17 (hosts file, or the openfut-hook DLL under Proton)
|
||||
│
|
||||
▼ gosredirector.ea.com → 127.0.0.1:42127
|
||||
blaze-server redirector listener (TLS, Fire2 framing)
|
||||
blaze-server redirector listener (SSLv3, Fire2 framing)
|
||||
│ replies: "connect to 127.0.0.1:10041"
|
||||
▼
|
||||
blaze-server Blaze listener (TLS, Fire2 framing)
|
||||
│ every packet → JSONL capture + pretty-print
|
||||
└─ captures/<timestamp>.jsonl
|
||||
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
|
||||
```
|
||||
|
||||
## Framing note
|
||||
## Transport: SSLv3, not TLS
|
||||
|
||||
Two Blaze wire framings exist: **Fire** and **Fire2**. FIFA 23's exact variant is unknown — Fire2 is attempted first because it is used by all post-2012 EA titles (ME3, BF3, etc.). If captures show malformed frame sizes, switch the `FramingVariant` in `main.rs` to `Raw` to bypass framing and capture raw bytes for manual analysis.
|
||||
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
|
||||
|
||||
### 1. Generate a self-signed TLS cert (RSA-2048)
|
||||
|
||||
DirtySDK (FIFA's network library) rejects ECDSA — RSA-2048 is required.
|
||||
|
||||
```bash
|
||||
mkdir certs
|
||||
openssl req -x509 -newkey rsa:2048 \
|
||||
-keyout certs/key.pem -out certs/cert.pem \
|
||||
-days 3650 -nodes \
|
||||
-subj "/CN=gosredirector.ea.com"
|
||||
cp config.example.toml config.toml # edit if needed
|
||||
cargo run --release --bin blaze-server -- config.toml
|
||||
```
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cp config.example.toml config.toml
|
||||
# edit if needed
|
||||
```
|
||||
|
||||
### 3. /etc/hosts redirect
|
||||
Then redirect the hostname:
|
||||
|
||||
```
|
||||
127.0.0.1 gosredirector.ea.com
|
||||
```
|
||||
|
||||
Or deploy `openfut-hook` DLL which redirects at the DNS level inside Proton.
|
||||
…or deploy `openfut-hook` to redirect inside Proton, and launch FIFA 17 and go online.
|
||||
|
||||
### 4. Run
|
||||
### 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
|
||||
cd fifa-blaze
|
||||
cargo run --release --bin blaze-server -- config.toml
|
||||
cargo test
|
||||
```
|
||||
|
||||
### 5. Start FIFA 23 and go to FUT
|
||||
|
||||
The capture log at `captures/<timestamp>.jsonl` will contain every packet.
|
||||
Inspect with jq:
|
||||
## 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
|
||||
# Find all unique component/command pairs:
|
||||
jq -r '[.component, .command] | @tsv' captures/*.jsonl | sort -u
|
||||
```
|
||||
|
||||
## Capture log format
|
||||
### Replaying a capture offline
|
||||
|
||||
Each line is a JSON object:
|
||||
`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:
|
||||
|
||||
```json
|
||||
{
|
||||
"n": 1,
|
||||
"ts": "2026-06-26T12:00:00.000Z",
|
||||
"peer": "127.0.0.1:54321",
|
||||
"dir": "IN",
|
||||
"component": "0x0001",
|
||||
"command": "0x0001",
|
||||
"type": "REQUEST",
|
||||
"seq": 1,
|
||||
"error": 0,
|
||||
"body_len": 42,
|
||||
"raw_hex": "deadbeef...",
|
||||
"tdf": "{\n \"VRSN\": 0,\n ...}"
|
||||
}
|
||||
```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. Identify the redirector request/response tag layout
|
||||
2. Identify Util `preAuth` / `postAuth` / `ping` commands
|
||||
3. Identify Authentication `login` command
|
||||
4. Implement handlers and test with FIFA 23
|
||||
|
||||
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
|
||||
# Build
|
||||
cargo build
|
||||
|
||||
# Run with verbose logging
|
||||
cargo test
|
||||
RUST_LOG=debug cargo run --bin blaze-server -- config.toml
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user