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:
+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