refactor(launcher): retire FIFA 23; keep the hook game-generic by feature
FIFA 23 is not in development and was never a valid template for FIFA 17
(different game, different in-memory layout). Remove it as a build target and
as scaffolding, while preserving the per-game feature architecture so future
games plug in as new modules — never by copying retired reverse-engineering.
Hook (openfut-hook):
- Delete install_hooks_fifa23 and every FIFA23-only module: config, hooks,
transport_watch, ssl_patch, origin_spy, tls_bypass, dial_notification, probe
(+ probe feature), recv_hook (+ capture_baseline feature), plus the orphan
FIFA23 LSX/Origin files lsx.rs and ea_stub.rs. ~3.6k lines; git + Vault retain
the research.
- lib.rs is now game-generic: a per-game feature selects that game's module and
install_hooks dispatches to it. No game feature => compile_error!("select a
game, e.g. --features fifa17"). --features fifa17 remains the build invariant.
- Drop the crate-wide blanket (it existed only
to hide the compiled-but-unused FIFA23 modules). Replace with narrow, justified
#[allow(dead_code)] on the three FIFA17 SBC RE-scaffolding items it was masking,
so the candidate stays behavior-identical.
- connect_hook: the redirect is now always the config-driven path (openfut-common
target from openfut.cfg); the hardcoded-loopback rewrite and its dead consts are
gone. Removed the FIFA23-era transport_watch diagnostics from the shared
connect/WSAConnect/ConnectEx detours. Deleted unused iat::patch_iat_in.
Launcher:
- fifa_game_dir no longer defaults to a hardcoded '.../FIFA 23' Steam path; it is
empty by default, matching the launcher's own rule that it never invents a path
to somebody's game install (like openfut_server_host and game_profile).
- Generalise the remaining 'FIFA 23' doc literals in config.rs / setup.rs.
Proof: fifa17 clippy -D warnings clean; no-game build fails with the documented
compile_error; launcher 75 tests pass unchanged; launcher + hook cross-build
x86_64-pc-windows-gnu; cargo fmt --check clean; zero FIFA23 symbols/literals
remain. FIFA17 armed-module set unchanged (redirect + SBC/store/season).
This commit is contained in:
@@ -5,20 +5,6 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const AF_INET: u16 = 2;
|
||||
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
||||
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
||||
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
||||
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
||||
// EA App LSX. anadius handles :3216 in-process before it reaches the host TCP
|
||||
// stack (keyed on port 3216 specifically), so redirecting FIFA's LSX connect to a
|
||||
// *different* host port (:3217) slips past that interception and lands on the
|
||||
// native openfut-bridge LSX server. This is the load-bearing redirect that routes
|
||||
// LSX to our bridge; without it FIFA uses anadius's in-process emu instead.
|
||||
#[allow(dead_code)] // unused when built with the `capture_baseline` feature
|
||||
const PORT_LSX_NBO: u16 = 0x900C; // 3216 big-endian (EA App LSX)
|
||||
#[allow(dead_code)]
|
||||
const PORT_LSX_TARGET_NBO: u16 = 0x910C; // 3217 big-endian (bridge LSX target)
|
||||
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
||||
|
||||
#[repr(C)]
|
||||
struct SockaddrIn {
|
||||
@@ -41,13 +27,6 @@ struct SockaddrIn6 {
|
||||
sin6_scope_id: u32,
|
||||
}
|
||||
|
||||
/// IPv4-mapped IPv6 loopback: `::ffff:127.0.0.1`. An `AF_INET6` socket connecting to
|
||||
/// this sends real IPv4 packets to 127.0.0.1, so the connection lands on the bridge's
|
||||
/// existing IPv4 listener on :8443 — no separate IPv6 listener needed. The game's own
|
||||
/// EA dials already use v4-mapped addresses (`::ffff:x.x.x.x`), so its sockets are not
|
||||
/// `IPV6_V6ONLY` and will accept this target.
|
||||
const V4MAPPED_LOOPBACK: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1];
|
||||
|
||||
// Address of ws2_32!connect (set at hook installation)
|
||||
static CONNECT_ADDR: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
@@ -89,92 +68,9 @@ unsafe fn restore_original(target: *mut u8) {
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
/// If `name` is an EA-relevant connect target, return a rewritten sockaddr pointing at
|
||||
/// the local bridge (plus its byte length). Handles BOTH `AF_INET` and `AF_INET6`: the
|
||||
/// game's Blaze/DirtySDK stack dials EA over IPv6 (v4-mapped) on :443, and the old
|
||||
/// IPv4-only path let those slip straight past us to the real (dead) servers.
|
||||
///
|
||||
/// The returned buffer is 28 bytes (enough for a `sockaddr_in6`); the second value is
|
||||
/// how many of those bytes are meaningful (16 for v4, 28 for v6). `pub(crate)` so the
|
||||
/// ConnectEx path can share this one implementation.
|
||||
unsafe fn redirect_loopback(name: *const u8, namelen: i32) -> Option<([u8; 28], i32)> {
|
||||
if namelen < 8 || name.is_null() {
|
||||
return None;
|
||||
}
|
||||
// The first u16 of any sockaddr is the address family.
|
||||
let family = *(name as *const u16);
|
||||
let mut buf = [0u8; 28];
|
||||
|
||||
match family {
|
||||
AF_INET => {
|
||||
// SAFE: family is AF_INET and namelen >= 8 == the sockaddr_in fields we read.
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
let new_port_nbo = match sa.sin_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
#[cfg(not(feature = "capture_baseline"))]
|
||||
PORT_LSX_NBO => PORT_LSX_TARGET_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => return None,
|
||||
};
|
||||
// sin_addr is network order; to_le_bytes gives memory order = the dotted
|
||||
// quad, so b[0].b[1].b[2].b[3] is correct (the old code printed it reversed).
|
||||
let o = sa.sin_addr.to_le_bytes();
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: v4 {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
||||
o[0],
|
||||
o[1],
|
||||
o[2],
|
||||
o[3],
|
||||
u16::from_be(sa.sin_port),
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
// SAFE: buf is 28 bytes, larger than the 16-byte sockaddr_in we write.
|
||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
||||
out.sin_family = AF_INET;
|
||||
out.sin_port = new_port_nbo;
|
||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
||||
Some((buf, 16))
|
||||
}
|
||||
AF_INET6 => {
|
||||
if namelen < 28 {
|
||||
return None;
|
||||
}
|
||||
// SAFE: family is AF_INET6 and namelen >= 28 == sizeof(sockaddr_in6).
|
||||
let sa6 = &*(name as *const SockaddrIn6);
|
||||
// LSX is IPv4-only (anadius keys on it), so it is intentionally omitted here.
|
||||
let new_port_nbo = match sa6.sin6_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => return None,
|
||||
};
|
||||
let a = sa6.sin6_addr;
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: v6 [{:02x}{:02x}:..:{:02x}{:02x}]:{} → ::ffff:127.0.0.1:{}\n",
|
||||
a[0],
|
||||
a[1],
|
||||
a[14],
|
||||
a[15],
|
||||
u16::from_be(sa6.sin6_port),
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
// SAFE: buf is exactly 28 bytes == sizeof(sockaddr_in6).
|
||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn6);
|
||||
out.sin6_family = AF_INET6;
|
||||
out.sin6_port = new_port_nbo;
|
||||
out.sin6_flowinfo = 0;
|
||||
out.sin6_addr = V4MAPPED_LOOPBACK;
|
||||
out.sin6_scope_id = 0;
|
||||
Some((buf, 28))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The armed FIFA17 redirect target, resolved once from `openfut.cfg` via
|
||||
/// `openfut-common`. When set, `redirect_if_ea` rewrites matched EA connections
|
||||
/// to this configured server; when unset, the legacy loopback path is used.
|
||||
/// The armed redirect target, resolved once from `openfut.cfg` via `openfut-common`.
|
||||
/// When set, `redirect_if_ea` rewrites matched EA connections to this configured
|
||||
/// server; when unset, matched connections are left untouched (no redirect).
|
||||
static REDIRECT: OnceLock<openfut_common::ResolvedServer> = OnceLock::new();
|
||||
|
||||
/// Arm the config-driven redirect (FIFA17). Idempotent: the first call wins.
|
||||
@@ -182,16 +78,16 @@ pub fn set_redirect(server: openfut_common::ResolvedServer) {
|
||||
let _ = REDIRECT.set(server);
|
||||
}
|
||||
|
||||
/// Dispatch: config-driven (FIFA17, shared `openfut-common` map + configured
|
||||
/// host) when armed, else the legacy hardcoded-loopback rewrite.
|
||||
/// If `name` is a matched EA connect target, return a rewritten sockaddr pointing
|
||||
/// at the configured OpenFUT server (plus its meaningful byte length: 16 for v4,
|
||||
/// 28 for v6). The target is armed once from `openfut.cfg` via `set_redirect`;
|
||||
/// when unset — or when the port is not a known EA route — the connection is left
|
||||
/// untouched. Shared by the connect / WSAConnect / ConnectEx detours.
|
||||
pub(crate) unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 28], i32)> {
|
||||
if namelen < 8 || name.is_null() {
|
||||
return None;
|
||||
}
|
||||
match REDIRECT.get() {
|
||||
Some(server) => redirect_configured(server, name, namelen),
|
||||
None => redirect_loopback(name, namelen),
|
||||
}
|
||||
redirect_configured(REDIRECT.get()?, name, namelen)
|
||||
}
|
||||
|
||||
/// FIFA17 config-driven rewrite. Destination host+port come from `openfut.cfg`
|
||||
@@ -258,9 +154,6 @@ unsafe fn redirect_configured(
|
||||
pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen: i32) -> i32 {
|
||||
let addr = CONNECT_ADDR.load(Ordering::Relaxed) as *mut u8;
|
||||
|
||||
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
|
||||
crate::transport_watch::note_connect("connect", name, namelen, s);
|
||||
|
||||
// Log every call so we can confirm the hook fires at all
|
||||
if namelen >= 8 {
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
@@ -334,8 +227,6 @@ pub unsafe extern "system" fn hooked_wsa_connect(
|
||||
sqos: *const (),
|
||||
gqos: *const (),
|
||||
) -> i32 {
|
||||
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
|
||||
crate::transport_watch::note_connect("WSAConnect", name, namelen, s);
|
||||
let real = REAL_WSA.get().copied().unwrap();
|
||||
if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
||||
real(s, buf.as_ptr(), len, caller, callee, sqos, gqos)
|
||||
|
||||
Reference in New Issue
Block a user