/// Hooks ws2_32!connect via inline detour (no iptables needed). /// Uses unhook/rehook pattern: restores original bytes, calls real function, re-installs hook. /// This avoids trampoline RIP-relocation issues entirely. use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::OnceLock; const AF_INET: u16 = 2; #[repr(C)] struct SockaddrIn { sin_family: u16, sin_port: u16, sin_addr: u32, sin_zero: [u8; 8], } const AF_INET6: u16 = 23; // Windows AF_INET6 value (we run under the Win ABI in Wine) /// Win32 `sockaddr_in6`. `sin6_port` is network byte order; `sin6_addr` is 16 raw /// address bytes in network order. 28 bytes total. #[repr(C)] struct SockaddrIn6 { sin6_family: u16, sin6_port: u16, sin6_flowinfo: u32, sin6_addr: [u8; 16], sin6_scope_id: u32, } // Address of ws2_32!connect (set at hook installation) static CONNECT_ADDR: AtomicUsize = AtomicUsize::new(0); // Original 14 bytes saved before we overwrite them static mut ORIGINAL_BYTES: [u8; 14] = [0u8; 14]; /// Restores the real WinSock call's thread-local last error after detour repair, /// logging, and other instrumentation have run. Callers inspect this value after /// `SOCKET_ERROR`; leaking a logger/VirtualProtect error changes connect semantics. struct WsaLastErrorGuard(i32); impl WsaLastErrorGuard { unsafe fn capture() -> Self { use windows_sys::Win32::Networking::WinSock::WSAGetLastError; Self(WSAGetLastError()) } fn value(&self) -> i32 { self.0 } } impl Drop for WsaLastErrorGuard { fn drop(&mut self) { unsafe { use windows_sys::Win32::Networking::WinSock::WSASetLastError; WSASetLastError(self.0); } } } // For WSAConnect IAT fallback type WsaConnectFn = unsafe extern "system" fn( s: usize, name: *const u8, namelen: i32, caller: *const (), callee: *const (), sqos: *const (), gqos: *const (), ) -> i32; static REAL_WSA: OnceLock = OnceLock::new(); pub fn set_real_wsa_connect(f: WsaConnectFn) { let _ = REAL_WSA.set(f); } unsafe fn write_hook(target: *mut u8, dest: u64) { use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}; let mut old: u32 = 0; VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old); // FF 25 00 00 00 00 JMP [rip+0] target.write(0xFF); target.add(1).write(0x25); (target.add(2) as *mut u32).write(0u32); (target.add(6) as *mut u64).write(dest); VirtualProtect(target as _, 14, old, &mut old); } unsafe fn restore_original(target: *mut u8) { use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}; let mut old: u32 = 0; VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old); core::ptr::copy_nonoverlapping(core::ptr::addr_of!(ORIGINAL_BYTES) as *const u8, target, 14); VirtualProtect(target as _, 14, old, &mut old); } /// 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 = OnceLock::new(); /// Arm the config-driven redirect (FIFA17). Idempotent: the first call wins. pub fn set_redirect(server: openfut_common::ResolvedServer) { let _ = REDIRECT.set(server); } /// 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; } redirect_configured(REDIRECT.get()?, name, namelen) } /// FIFA17 config-driven rewrite. Destination host+port come from `openfut.cfg` /// through `openfut-common`, so the hook and the launcher agree by construction. /// Matching is by EA source-port signature only (see `openfut_common::ea_ports`), /// so a hardcoded EA IP (e.g. the `159.153.51.20:42230` redirector) and a /// DNS-resolved one both land on the configured — possibly remote — server. An /// unrecognised port returns `None` (connection left untouched). Never corrupts /// the sockaddr: it only writes into a fresh 28-byte buffer. unsafe fn redirect_configured( server: &openfut_common::ResolvedServer, name: *const u8, namelen: i32, ) -> Option<([u8; 28], i32)> { let family = *(name as *const u16); let mut buf = [0u8; 28]; match family { AF_INET => { let sa = &*(name as *const SockaddrIn); let redir = server.redirect_for_ea_port(sa.sin_port)?; crate::write_log(&format!( "connect_hook: v4 :{} → {}:{}\n", u16::from_be(sa.sin_port), redir.redirect_ip, u16::from_be(redir.port_nbo) )); let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn); out.sin_family = AF_INET; out.sin_port = redir.port_nbo; out.sin_addr = redir.addr_nbo; Some((buf, 16)) } AF_INET6 => { if namelen < 28 { return None; } let sa6 = &*(name as *const SockaddrIn6); let redir = server.redirect_for_ea_port(sa6.sin6_port)?; // ::ffff: — a v4-mapped v6 target so a v6 socket sends // real IPv4 packets to the configured server. let o = redir.redirect_ip.octets(); let mut v4mapped = [0u8; 16]; v4mapped[10] = 0xff; v4mapped[11] = 0xff; v4mapped[12..16].copy_from_slice(&o); crate::write_log(&format!( "connect_hook: v6 :{} → ::ffff:{}:{}\n", u16::from_be(sa6.sin6_port), redir.redirect_ip, u16::from_be(redir.port_nbo) )); let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn6); out.sin6_family = AF_INET6; out.sin6_port = redir.port_nbo; out.sin6_flowinfo = 0; out.sin6_addr = v4mapped; out.sin6_scope_id = 0; Some((buf, 28)) } _ => None, } } 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; // Log every call so we can confirm the hook fires at all if namelen >= 8 { let sa = &*(name as *const SockaddrIn); if sa.sin_family == AF_INET { let b = sa.sin_addr.to_le_bytes(); let port = u16::from_be(sa.sin_port); // Log socket type (SOCK_STREAM=1, SOCK_DGRAM=2) to detect UDP connects let sock_type = { use windows_sys::Win32::Networking::WinSock::{getsockopt, SOL_SOCKET, SO_TYPE}; let mut ty: i32 = -1; let mut len: i32 = 4; getsockopt( s, SOL_SOCKET, SO_TYPE, &mut ty as *mut i32 as *mut u8, &mut len, ); ty }; crate::write_log(&format!( "connect_hook: call {}.{}.{}.{}:{} sock_type={}\n", b[3], b[2], b[1], b[0], port, sock_type )); } } // Port 3216 (EA App LSX) is now handled by the native openfut-bridge LSX // server — no in-process interception needed, just let connect() go through. let (call_name, call_len) = if let Some((buf, len)) = redirect_if_ea(name, namelen) { restore_original(addr); let r = { let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 = core::mem::transmute(addr); f(s, buf.as_ptr(), len) }; // Named binding held until `return r`: its Drop restores the WSA error after `write_hook`. let _last_error = WsaLastErrorGuard::capture(); write_hook(addr, hooked_connect as *const () as u64); return r; } else { (name, namelen) }; restore_original(addr); let r = { let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 = core::mem::transmute(addr); f(s, call_name, call_len) }; let last_error = WsaLastErrorGuard::capture(); write_hook(addr, hooked_connect as *const () as u64); if namelen >= 8 { let sa = &*(call_name as *const SockaddrIn); if sa.sin_family == AF_INET { let logged_error = if r != 0 { last_error.value() } else { 0 }; crate::write_log(&format!( "connect_hook: result={r} wsa_err={logged_error}\n" )); } } r } pub unsafe extern "system" fn hooked_wsa_connect( s: usize, name: *const u8, namelen: i32, caller: *const (), callee: *const (), sqos: *const (), gqos: *const (), ) -> i32 { 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) } else { real(s, name, namelen, caller, callee, sqos, gqos) } } /// Install inline detour on ws2_32!connect. pub unsafe fn install_inline_connect_hook() -> bool { use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress}; let ws2 = GetModuleHandleA(c"ws2_32.dll".as_ptr().cast()); if ws2.is_null() { return false; } let connect_fn = match GetProcAddress(ws2, c"connect".as_ptr().cast()) { Some(f) => f as *mut u8, None => return false, }; // Save original 14 bytes core::ptr::copy_nonoverlapping( connect_fn, core::ptr::addr_of_mut!(ORIGINAL_BYTES) as *mut u8, 14, ); CONNECT_ADDR.store(connect_fn as usize, Ordering::Relaxed); // Overwrite first 14 bytes with absolute indirect JMP to our hook write_hook(connect_fn, hooked_connect as *const () as u64); true } #[cfg(test)] mod tests { use super::WsaLastErrorGuard; use windows_sys::Win32::Networking::WinSock::{ WSAGetLastError, WSASetLastError, WSAEWOULDBLOCK, }; #[test] fn restores_winsock_last_error_after_instrumentation() { unsafe { WSASetLastError(WSAEWOULDBLOCK); { let guard = WsaLastErrorGuard::capture(); assert_eq!(guard.value(), WSAEWOULDBLOCK); WSASetLastError(0); } assert_eq!(WSAGetLastError(), WSAEWOULDBLOCK); } } }