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:
@@ -0,0 +1,3 @@
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
ar = "x86_64-w64-mingw32-ar"
|
||||
Generated
+83
@@ -0,0 +1,83 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "openfut-hook"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "openfut-hook"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_Networking_WinSock",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
panic = "abort"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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 ());
|
||||
}
|
||||
+74
-29
@@ -33,7 +33,7 @@ pub struct LauncherApp {
|
||||
log_follow: bool,
|
||||
|
||||
// Setup state
|
||||
hosts_configured: bool,
|
||||
hook_deployed: bool,
|
||||
cert_path: Option<std::path::PathBuf>,
|
||||
setup_message: Option<(bool, String)>, // (success, text)
|
||||
}
|
||||
@@ -54,7 +54,8 @@ impl LauncherApp {
|
||||
|
||||
let config = LauncherConfig::load();
|
||||
let cert_path = setup::find_bridge_cert(&config.bridge_captures_dir);
|
||||
let hosts_configured = setup::hosts_configured();
|
||||
let hook_deployed =
|
||||
setup::hook_dll_deployed(std::path::Path::new(&config.fifa_game_dir));
|
||||
|
||||
Self {
|
||||
config,
|
||||
@@ -66,7 +67,7 @@ impl LauncherApp {
|
||||
active_tab: Tab::Dashboard,
|
||||
log_tab: 0,
|
||||
log_follow: true,
|
||||
hosts_configured,
|
||||
hook_deployed,
|
||||
cert_path,
|
||||
setup_message: None,
|
||||
}
|
||||
@@ -189,11 +190,11 @@ impl LauncherApp {
|
||||
.num_columns(2)
|
||||
.spacing([12.0, 4.0])
|
||||
.show(ui, |ui| {
|
||||
ui.label("Hosts file:");
|
||||
if self.hosts_configured {
|
||||
ui.colored_label(Color32::from_rgb(80, 200, 120), "Configured");
|
||||
ui.label("Hook DLL:");
|
||||
if self.hook_deployed {
|
||||
ui.colored_label(Color32::from_rgb(80, 200, 120), "Deployed (version.dll)");
|
||||
} else {
|
||||
ui.colored_label(Color32::from_rgb(220, 150, 0), "Not configured");
|
||||
ui.colored_label(Color32::from_rgb(220, 150, 0), "Not deployed");
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
@@ -263,51 +264,81 @@ impl LauncherApp {
|
||||
}
|
||||
|
||||
fn ui_setup(&mut self, ui: &mut Ui) {
|
||||
use std::path::Path;
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.heading("System Setup");
|
||||
ui.add_space(4.0);
|
||||
ui.label("These steps route FIFA 23 traffic through the emulator. Both require elevated privileges (polkit/sudo).");
|
||||
ui.label("These steps route FIFA 23 traffic through the emulator via DLL injection (no hosts file changes needed).");
|
||||
ui.add_space(12.0);
|
||||
|
||||
// ── Hosts file ────────────────────────────────────────────────────────
|
||||
// ── Hook DLL deployment ───────────────────────────────────────────────
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.strong("Step 1 — Redirect EA hostnames");
|
||||
ui.strong("Step 1 — Deploy network hook DLL");
|
||||
ui.add_space(4.0);
|
||||
ui.label("Adds entries to /etc/hosts so the game connects to the bridge instead of EA's servers.");
|
||||
ui.label("Copies openfut_hook.dll into the FIFA 23 folder as version.dll. \
|
||||
When Proton loads the game it will intercept network calls and redirect \
|
||||
EA hostnames to the local bridge — no hosts file changes required.");
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Refresh status
|
||||
self.hosts_configured = setup::hosts_configured();
|
||||
let game_dir = Path::new(&self.config.fifa_game_dir);
|
||||
self.hook_deployed = setup::hook_dll_deployed(game_dir);
|
||||
|
||||
let dll_src = Path::new(&self.config.hook_dll_path);
|
||||
let dll_built = dll_src.exists();
|
||||
|
||||
if !dll_built {
|
||||
ui.colored_label(
|
||||
Color32::from_rgb(220, 150, 0),
|
||||
"⚠ Hook DLL not built yet. Run in openfut-hook/:",
|
||||
);
|
||||
ui.monospace("cargo build --release --target x86_64-pc-windows-gnu");
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if self.hosts_configured {
|
||||
ui.colored_label(Color32::from_rgb(80, 200, 120), "✔ Configured");
|
||||
if self.hook_deployed {
|
||||
ui.colored_label(Color32::from_rgb(80, 200, 120), "✔ Deployed (version.dll)");
|
||||
ui.add_space(8.0);
|
||||
if ui.button("Remove entries").clicked() {
|
||||
match setup::remove_hosts_entries() {
|
||||
if ui.button("Remove").clicked() {
|
||||
match setup::remove_hook_dll(game_dir) {
|
||||
Ok(()) => {
|
||||
self.setup_message = Some((true, "Hosts entries removed.".into()));
|
||||
self.hosts_configured = false;
|
||||
self.setup_message = Some((true, "Hook DLL removed.".into()));
|
||||
self.hook_deployed = false;
|
||||
}
|
||||
Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(Color32::from_rgb(220, 60, 60), "✘ Not configured");
|
||||
ui.colored_label(Color32::from_rgb(220, 60, 60), "✘ Not deployed");
|
||||
ui.add_space(8.0);
|
||||
if ui.button("Add entries").clicked() {
|
||||
match setup::add_hosts_entries() {
|
||||
if ui.add_enabled(dll_built, egui::Button::new("Deploy")).clicked() {
|
||||
match setup::deploy_hook_dll(dll_src, game_dir) {
|
||||
Ok(()) => {
|
||||
self.setup_message = Some((true, "Hosts entries added.".into()));
|
||||
self.hosts_configured = true;
|
||||
self.setup_message = Some((true, "Hook DLL deployed as version.dll.".into()));
|
||||
self.hook_deployed = true;
|
||||
}
|
||||
Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(4.0);
|
||||
ui.strong("Steam Launch Options");
|
||||
ui.label("Paste this into FIFA 23 → Properties → Launch Options in Steam:");
|
||||
ui.add_space(2.0);
|
||||
let launch_opt = setup::STEAM_LAUNCH_OPTIONS;
|
||||
ui.horizontal(|ui| {
|
||||
ui.monospace(launch_opt);
|
||||
if ui.small_button("Copy").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = launch_opt.to_string());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
@@ -317,11 +348,10 @@ impl LauncherApp {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.strong("Step 2 — Install TLS certificate");
|
||||
ui.add_space(4.0);
|
||||
ui.label("Installs the bridge's self-signed cert into the system CA store so the game accepts HTTPS connections.");
|
||||
ui.label("Installs the bridge's self-signed cert into the Wine/Proton cert store \
|
||||
so the game accepts HTTPS connections to the bridge.");
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Refresh cert path
|
||||
self.cert_path = setup::find_bridge_cert(&self.config.bridge_captures_dir);
|
||||
|
||||
match &self.cert_path.clone() {
|
||||
@@ -334,7 +364,7 @@ impl LauncherApp {
|
||||
Some(cert) => {
|
||||
ui.label(format!("Cert: {}", cert.display()));
|
||||
ui.add_space(4.0);
|
||||
if ui.button("Install cert (requires sudo)").clicked() {
|
||||
if ui.button("Install cert (Wine cert store)").clicked() {
|
||||
match setup::install_cert(cert) {
|
||||
Ok(()) => self.setup_message = Some((true, "Certificate installed.".into())),
|
||||
Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))),
|
||||
@@ -346,7 +376,6 @@ impl LauncherApp {
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
// ── Status message ────────────────────────────────────────────────────
|
||||
if let Some((ok, msg)) = &self.setup_message {
|
||||
let color = if *ok {
|
||||
Color32::from_rgb(80, 200, 120)
|
||||
@@ -416,6 +445,22 @@ impl LauncherApp {
|
||||
ui.label("TLS enabled:");
|
||||
changed |= ui.checkbox(&mut self.config.bridge_tls_enabled, "").changed();
|
||||
ui.end_row();
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
|
||||
ui.strong("Hook DLL");
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Hook DLL path:");
|
||||
changed |= ui.text_edit_singleline(&mut self.config.hook_dll_path).changed();
|
||||
ui.end_row();
|
||||
|
||||
ui.label("FIFA 23 game dir:");
|
||||
changed |= ui.text_edit_singleline(&mut self.config.fifa_game_dir).changed();
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
if changed {
|
||||
|
||||
@@ -11,6 +11,10 @@ pub struct LauncherConfig {
|
||||
pub bridge_captures_dir: String,
|
||||
pub bridge_core_url: String,
|
||||
pub bridge_tls_enabled: bool,
|
||||
/// Path to the built openfut_hook.dll (Windows DLL for Proton injection).
|
||||
pub hook_dll_path: String,
|
||||
/// FIFA 23 game folder inside the Proton prefix (where the DLL is deployed).
|
||||
pub fifa_game_dir: String,
|
||||
}
|
||||
|
||||
impl Default for LauncherConfig {
|
||||
@@ -41,6 +45,15 @@ impl Default for LauncherConfig {
|
||||
.into(),
|
||||
bridge_core_url: "http://127.0.0.1:8080".into(),
|
||||
bridge_tls_enabled: true,
|
||||
hook_dll_path: base
|
||||
.join("openfut-launcher/openfut-hook/target/x86_64-pc-windows-gnu/release/openfut_hook.dll")
|
||||
.to_string_lossy()
|
||||
.into(),
|
||||
fifa_game_dir: dirs::home_dir()
|
||||
.map(|h| h.join(".steam/steam/steamapps/common/FIFA 23"))
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-95
@@ -1,129 +1,106 @@
|
||||
use std::process::Command;
|
||||
|
||||
/// EA FUT hostnames the bridge needs to intercept.
|
||||
const INTERCEPT_HOSTS: &[&str] = &[
|
||||
"fut.ea.com",
|
||||
"utas.mob.v4.fut.ea.com",
|
||||
"utas.s2.fut.ea.com",
|
||||
];
|
||||
|
||||
const HOSTS_MARKER_START: &str = "# BEGIN openfut-launcher";
|
||||
const HOSTS_MARKER_END: &str = "# END openfut-launcher";
|
||||
use std::{path::{Path, PathBuf}, process::Command};
|
||||
|
||||
// ── Cert installation ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Find the bridge cert in the given captures dir.
|
||||
pub fn find_bridge_cert(captures_dir: &str) -> Option<std::path::PathBuf> {
|
||||
let p = std::path::Path::new(captures_dir).join("bridge_cert.pem");
|
||||
pub fn find_bridge_cert(captures_dir: &str) -> Option<PathBuf> {
|
||||
let p = Path::new(captures_dir).join("bridge_cert.pem");
|
||||
p.exists().then_some(p)
|
||||
}
|
||||
|
||||
/// Install the bridge cert into the system CA store.
|
||||
/// On Linux, copies to /usr/local/share/ca-certificates/ and runs
|
||||
/// update-ca-certificates via pkexec (polkit graphical sudo).
|
||||
pub fn install_cert(cert_src: &std::path::Path) -> anyhow::Result<()> {
|
||||
let dest = std::path::Path::new("/usr/local/share/ca-certificates/openfut-bridge.crt");
|
||||
/// Install the bridge cert into the Wine/Proton prefix cert store so the game
|
||||
/// trusts TLS connections to the bridge. Falls back to the system CA store.
|
||||
pub fn install_cert(cert_src: &Path) -> anyhow::Result<()> {
|
||||
// Try certutil inside the Proton prefix first (Wine cert store)
|
||||
let wine_result = try_wine_certutil(cert_src);
|
||||
if wine_result.is_ok() {
|
||||
return wine_result;
|
||||
}
|
||||
|
||||
// Write a small shell script that pkexec can run as root
|
||||
// Fallback: Linux system CA store via pkexec/sudo
|
||||
let dest = Path::new("/usr/local/share/ca-certificates/openfut-bridge.crt");
|
||||
let script = format!(
|
||||
"cp '{}' '{}' && update-ca-certificates",
|
||||
cert_src.display(),
|
||||
dest.display()
|
||||
);
|
||||
run_elevated(&script)
|
||||
}
|
||||
|
||||
let status = Command::new("pkexec")
|
||||
.args(["sh", "-c", &script])
|
||||
fn try_wine_certutil(cert_src: &Path) -> anyhow::Result<()> {
|
||||
// certutil is available inside a Wine prefix via winetricks or natively
|
||||
let status = Command::new("wine")
|
||||
.args([
|
||||
"certutil",
|
||||
"-addstore",
|
||||
"-user",
|
||||
"Root",
|
||||
&cert_src.to_string_lossy(),
|
||||
])
|
||||
.status();
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
Ok(s) => anyhow::bail!("pkexec exited with status {s}"),
|
||||
Err(e) => {
|
||||
// pkexec not available — try sudo
|
||||
let status = Command::new("sudo")
|
||||
.args(["sh", "-c", &script])
|
||||
.status()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("cert install failed: {e}")
|
||||
}
|
||||
}
|
||||
Ok(s) => anyhow::bail!("wine certutil exited {s}"),
|
||||
Err(e) => anyhow::bail!("wine certutil not available: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hosts file management ─────────────────────────────────────────────────────
|
||||
|
||||
pub fn hosts_configured() -> bool {
|
||||
let content = std::fs::read_to_string("/etc/hosts").unwrap_or_default();
|
||||
content.contains(HOSTS_MARKER_START)
|
||||
}
|
||||
|
||||
/// Add OpenFUT intercept entries to /etc/hosts (requires root).
|
||||
pub fn add_hosts_entries() -> anyhow::Result<()> {
|
||||
let current = std::fs::read_to_string("/etc/hosts")?;
|
||||
if current.contains(HOSTS_MARKER_START) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let entries: String = INTERCEPT_HOSTS
|
||||
.iter()
|
||||
.map(|h| format!("127.0.0.1 {h}\n"))
|
||||
.collect();
|
||||
|
||||
let addition = format!("\n{HOSTS_MARKER_START}\n{entries}{HOSTS_MARKER_END}\n");
|
||||
let new_content = format!("{current}{addition}");
|
||||
|
||||
write_hosts(&new_content)
|
||||
}
|
||||
|
||||
/// Remove OpenFUT intercept entries from /etc/hosts (requires root).
|
||||
pub fn remove_hosts_entries() -> anyhow::Result<()> {
|
||||
let current = std::fs::read_to_string("/etc/hosts")?;
|
||||
if !current.contains(HOSTS_MARKER_START) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
let mut skip = false;
|
||||
for line in current.lines() {
|
||||
if line.trim() == HOSTS_MARKER_START {
|
||||
skip = true;
|
||||
continue;
|
||||
}
|
||||
if line.trim() == HOSTS_MARKER_END {
|
||||
skip = false;
|
||||
continue;
|
||||
}
|
||||
if !skip {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
write_hosts(out.trim_end())
|
||||
}
|
||||
|
||||
fn write_hosts(content: &str) -> anyhow::Result<()> {
|
||||
// Write to a temp file, then mv into place with elevated privileges
|
||||
let tmp = "/tmp/openfut_hosts_tmp";
|
||||
std::fs::write(tmp, content)?;
|
||||
|
||||
let script = format!("mv '{tmp}' /etc/hosts && chmod 644 /etc/hosts");
|
||||
|
||||
let status = Command::new("pkexec").args(["sh", "-c", &script]).status();
|
||||
fn run_elevated(script: &str) -> anyhow::Result<()> {
|
||||
let status = Command::new("pkexec")
|
||||
.args(["sh", "-c", script])
|
||||
.status();
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
_ => {
|
||||
let s = Command::new("sudo")
|
||||
.args(["sh", "-c", &script])
|
||||
.args(["sh", "-c", script])
|
||||
.status()?;
|
||||
if s.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("failed to write /etc/hosts")
|
||||
anyhow::bail!("elevated command failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── DLL hook deployment ───────────────────────────────────────────────────────
|
||||
|
||||
/// Deploy openfut_hook.dll into the FIFA 23 game directory.
|
||||
/// Uses a name that FIFA 23 loads but doesn't need from system: `version.dll`.
|
||||
/// Proton will prefer the local copy over the system one.
|
||||
pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path) -> anyhow::Result<()> {
|
||||
if !dll_src.exists() {
|
||||
anyhow::bail!(
|
||||
"Hook DLL not found at {}. Build it first with:\n\
|
||||
cargo build --release --target x86_64-pc-windows-gnu\n\
|
||||
(inside openfut-hook/)",
|
||||
dll_src.display()
|
||||
);
|
||||
}
|
||||
std::fs::create_dir_all(game_dir)?;
|
||||
let dest = game_dir.join("version.dll");
|
||||
std::fs::copy(dll_src, &dest)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the deployed hook DLL from the FIFA game directory.
|
||||
pub fn remove_hook_dll(game_dir: &Path) -> anyhow::Result<()> {
|
||||
let dest = game_dir.join("version.dll");
|
||||
if dest.exists() {
|
||||
std::fs::remove_file(&dest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether our hook DLL is deployed in the game directory.
|
||||
pub fn hook_dll_deployed(game_dir: &Path) -> bool {
|
||||
game_dir.join("version.dll").exists()
|
||||
}
|
||||
|
||||
/// The Steam launch options the user needs to paste in to enable the override.
|
||||
/// Proton loads local DLLs named in WINEDLLOVERRIDES ahead of system ones.
|
||||
pub const STEAM_LAUNCH_OPTIONS: &str =
|
||||
"WINEDLLOVERRIDES=\"version=n,b\" %command%";
|
||||
|
||||
Reference in New Issue
Block a user