feat: replace hosts file with DLL injection hook

Adds openfut-hook/, a Windows DLL (cdylib, x86_64-pc-windows-gnu) that
patches the IAT of FIFA 23 at load time to redirect getaddrinfo calls
for fut.ea.com / utas.*.fut.ea.com to 127.0.0.1, sending all FUT
traffic to the local bridge — no /etc/hosts changes needed.

Deployment: the launcher copies openfut_hook.dll into the FIFA 23 game
folder as version.dll (a DLL FIFA loads but delegates to system).
Proton picks up the local copy automatically when you set:
  WINEDLLOVERRIDES="version=n,b" %command%
in Steam launch options.

Also updates cert install to try the Wine/Proton cert store (wine
certutil) before falling back to the Linux system CA store, and removes
all hosts file code from setup.rs / app.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 21:05:35 -07:00
parent fc894a7f77
commit 9b5da57f12
9 changed files with 482 additions and 124 deletions
+44
View File
@@ -0,0 +1,44 @@
/// Network hooks — redirect EA FUT hostnames to localhost.
use std::{ffi::CStr, sync::OnceLock};
use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo};
const INTERCEPT: &[&str] = &[
"fut.ea.com",
"utas.mob.v4.fut.ea.com",
"utas.s2.fut.ea.com",
];
type GetaddrinfoFn = unsafe extern "system" fn(
*const u8,
*const u8,
*const ADDRINFOA,
*mut *mut ADDRINFOA,
) -> i32;
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
pub fn set_real(f: GetaddrinfoFn) {
let _ = REAL.set(f);
}
pub unsafe extern "system" fn hooked_getaddrinfo(
node_name: *const u8,
service_name: *const u8,
hints: *const ADDRINFOA,
result: *mut *mut ADDRINFOA,
) -> i32 {
if !node_name.is_null() {
if let Ok(host) = CStr::from_ptr(node_name as *const i8).to_str() {
for target in INTERCEPT {
if host.eq_ignore_ascii_case(target) {
let local = b"127.0.0.1\0";
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
return real(local.as_ptr(), service_name, hints, result);
}
}
}
}
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
real(node_name, service_name, hints, result)
}
+135
View File
@@ -0,0 +1,135 @@
/// IAT (Import Address Table) patching.
///
/// We define the PE structs ourselves rather than pulling in windows-sys PE
/// headers (which are in a different crate / feature path).
use windows_sys::Win32::{
Foundation::HMODULE,
System::{
LibraryLoader::{GetModuleHandleA, GetProcAddress},
Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE},
},
};
// ── Minimal PE struct definitions ─────────────────────────────────────────────
#[repr(C)]
struct ImageDosHeader {
e_magic: u16,
_pad: [u16; 29],
e_lfanew: i32,
}
#[repr(C)]
struct ImageFileHeader {
machine: u16,
number_of_sections: u16,
time_date_stamp: u32,
pointer_to_symbol_table: u32,
number_of_symbols: u32,
size_of_optional_header: u16,
characteristics: u16,
}
#[repr(C)]
struct ImageDataDirectory {
virtual_address: u32,
size: u32,
}
#[repr(C)]
struct ImageOptionalHeader64 {
magic: u16,
_pad: [u8; 106],
data_directory: [ImageDataDirectory; 16],
}
#[repr(C)]
struct ImageNtHeaders64 {
signature: u32,
file_header: ImageFileHeader,
optional_header: ImageOptionalHeader64,
}
#[repr(C)]
struct ImageImportDescriptor {
original_first_thunk: u32,
time_date_stamp: u32,
forwarder_chain: u32,
name: u32,
first_thunk: u32,
}
// ── IAT patching ──────────────────────────────────────────────────────────────
/// Replace every IAT slot in the main module that currently holds
/// `original_fn` with `hook_fn`.
///
/// # Safety
/// Caller must ensure hook_fn has the same calling convention and signature.
pub unsafe fn patch_iat(original_fn: *const (), hook_fn: *const ()) -> usize {
let module = GetModuleHandleA(std::ptr::null());
patch_module(module, original_fn, hook_fn)
}
unsafe fn patch_module(
module: HMODULE,
original_fn: *const (),
hook_fn: *const (),
) -> usize {
if module.is_null() {
return 0;
}
let base = module as usize;
let dos = base as *const ImageDosHeader;
if (*dos).e_magic != 0x5A4D {
return 0;
}
let nt = (base + (*dos).e_lfanew as usize) as *const ImageNtHeaders64;
let import_rva = (*nt).optional_header.data_directory[1].virtual_address as usize;
if import_rva == 0 {
return 0;
}
let mut desc = (base + import_rva) as *const ImageImportDescriptor;
let mut count = 0usize;
while (*desc).name != 0 {
let ft = (*desc).first_thunk as usize;
let iat_slot = (base + ft) as *mut usize;
let mut i = 0usize;
loop {
let val = *iat_slot.add(i);
if val == 0 {
break;
}
if val == original_fn as usize {
let target = iat_slot.add(i) as *const std::ffi::c_void;
let mut old: u32 = 0;
VirtualProtect(target, std::mem::size_of::<usize>(), PAGE_EXECUTE_READWRITE, &mut old);
*iat_slot.add(i) = hook_fn as usize;
VirtualProtect(target, std::mem::size_of::<usize>(), old, &mut old);
count += 1;
}
i += 1;
}
desc = desc.add(1);
}
count
}
/// Resolve the address of an exported function from an already-loaded DLL.
pub unsafe fn resolve(dll: &[u8], fn_name: &[u8]) -> *const () {
let module = GetModuleHandleA(dll.as_ptr());
if module.is_null() {
return std::ptr::null();
}
match GetProcAddress(module, fn_name.as_ptr()) {
Some(f) => f as *const (),
None => std::ptr::null(),
}
}
+37
View File
@@ -0,0 +1,37 @@
mod hooks;
mod iat;
use windows_sys::Win32::{
Foundation::{BOOL, HMODULE, TRUE},
System::SystemServices::DLL_PROCESS_ATTACH,
Networking::WinSock::ADDRINFOA,
};
#[no_mangle]
pub unsafe extern "system" fn DllMain(
_module: HMODULE,
reason: u32,
_reserved: *mut (),
) -> BOOL {
if reason == DLL_PROCESS_ATTACH {
install_hooks();
}
TRUE
}
unsafe fn install_hooks() {
let real_ptr = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0");
if real_ptr.is_null() {
return;
}
let real_fn: unsafe extern "system" fn(
*const u8,
*const u8,
*const ADDRINFOA,
*mut *mut ADDRINFOA,
) -> i32 = std::mem::transmute(real_ptr);
hooks::set_real(real_fn);
iat::patch_iat(real_ptr, hooks::hooked_getaddrinfo as *const ());
}