//! Milestone 0 — Blaze transport reachability observation. //! //! PURE LOGGING, NO NEW DETOURS. This module does not hook anything itself. It is //! called from the three Winsock detours the hook ALREADY installs — getaddrinfo //! (`hooks.rs`), connect/WSAConnect (`connect_hook.rs`) and ConnectEx //! (`connectex_hook.rs`) — and, when armed, emits a single grep-friendly //! `TRANSPORT_WATCH:` line per resolution/connect so we can answer one question: //! //! Does the FIFA 23 client attempt ANY Blaze-flavored transport activity across a //! full menu+FUT session, or none at all? //! //! Everything here is READ-ONLY: we parse the hostname / sockaddr the game passed //! only to describe it in the log. We never change a resolution result or a //! connection target — that redirect logic lives in the detours themselves and is //! untouched. The env kill switch `OPENFUT_TRANSPORT_WATCH=1` gates all output; //! disarmed (default) this module is inert (each entry point returns immediately). //! //! Future-reference note (beyond-beginner, deliberately NOT done here): a //! types-first design would model a `ConnectTarget` enum (Inet{ip,port} / NonInet / //! Short) and a `TransportEvent` and route them through the `tracing` crate with //! structured fields, instead of hand-formatting strings into a flat log file. That //! buys machine-parseable logs and log levels. For a one-shot observation gate, //! flat `write_log` lines that `grep` cleanly are the lower-ceremony choice. use core::sync::atomic::{AtomicBool, Ordering}; /// Armed once at DLL load from `OPENFUT_TRANSPORT_WATCH`. `AtomicBool` (not a plain /// `static mut bool`) because the detours that read it run on arbitrary game threads; /// an atomic gives race-free reads with no `unsafe`. `Relaxed` is enough — this is a /// standalone flag with no ordering relationship to other memory. static ARMED: AtomicBool = AtomicBool::new(false); /// Read the env var once, at DLL load, and log the arm state. Called from `DllMain` /// (`install_hooks`). Reading the env in-process (rather than as a command prefix) is /// what makes the switch actually propagate through the umu/Proton launch — the same /// gotcha the probe switches hit; it works because the launch script `export`s it. pub fn arm_from_env() { let on = std::env::var("OPENFUT_TRANSPORT_WATCH") .map(|v| v == "1") .unwrap_or(false); ARMED.store(on, Ordering::Relaxed); crate::write_log(&format!( "TRANSPORT_WATCH: {} (env OPENFUT_TRANSPORT_WATCH)\n", if on { "ARMED" } else { "disarmed" } )); } fn armed() -> bool { ARMED.load(Ordering::Relaxed) } /// True if `host` looks like EA/Blaze infrastructure. Broad on purpose: this is a log /// classifier that makes a hit visually pop (`<-- BLAZE/EA-FLAVORED`), NOT a routing /// decision. The actual redirect decision stays in `hooks::is_ea_host`, which is /// deliberately narrower and unchanged. fn is_blaze_flavored(host: &str) -> bool { let h = host.to_ascii_lowercase(); [ "redirector", "gosredirector", "blaze", "gosca", "easfc", "utas", "fut", "ea.com", "easports", ] .iter() .any(|k| h.contains(k)) } /// Log one getaddrinfo hostname. Self-gates on the arm flag, so the call site can be /// unconditional. The existing `openfut_hook: getaddrinfo(...)` line stays; this adds /// the tagged, classified line so `grep TRANSPORT_WATCH` sees the full resolution set /// and a Blaze host stands out. pub fn note_getaddrinfo(host: &str) { if !armed() { return; } let tag = if is_blaze_flavored(host) { " <-- BLAZE/EA-FLAVORED" } else { "" }; crate::write_log(&format!("TRANSPORT_WATCH: getaddrinfo host=\"{host}\"{tag}\n")); } const AF_INET: u16 = 2; // IPv4 const AF_INET6: u16 = 23; // IPv6 (Windows value; Linux uses 10 — we're in Wine/Win ABI) /// Minimal view of a `sockaddr_in`; the first `u16` is the address family for ANY /// sockaddr, so reading this layout is safe enough to classify the family even when /// the real struct is a `sockaddr_un` or larger — we only trust the rest once we've /// confirmed `sin_family == AF_INET`. #[repr(C)] struct SockaddrIn { sin_family: u16, sin_port: u16, sin_addr: u32, sin_zero: [u8; 8], } /// Minimal view of a `sockaddr_in6` (Win32 layout). `sin6_port` is network byte order; /// `sin6_addr` is the 16 raw address bytes in network order. We ignore flowinfo/scope. #[repr(C)] struct SockaddrIn6 { sin6_family: u16, sin6_port: u16, sin6_flowinfo: u32, sin6_addr: [u8; 16], sin6_scope_id: u32, } /// Is `port` a known/suspected Blaze port? SHAPE — public general knowledge; the exact /// port for FIFA23's Blaze version is UNKNOWN. 42127 main, 10041/10744 redirector /// variants, 3659 classic redirector. fn is_blaze_port(port: u16) -> bool { matches!(port, 42127 | 10744 | 3659 | 10041) } /// Log one outbound connect attempt. `api` names the call path (`connect` / /// `WSAConnect` / `ConnectEx`) so we can tell which Winsock entry the client used. /// /// SAFETY: `name` must point to at least `namelen` readable bytes — it's the sockaddr /// the game just handed to a Winsock connect API, so that always holds at the call /// sites. We read it read-only and never write through it. `s` is the socket handle, /// used only to query `SO_TYPE` (TCP=1 / UDP=2) so a real Blaze TCP dial is /// distinguishable from UDP game/voice traffic. pub unsafe fn note_connect(api: &str, name: *const u8, namelen: i32, s: usize) { if !armed() { return; } if name.is_null() || namelen < 8 { crate::write_log(&format!( "TRANSPORT_WATCH: {api} (no/short sockaddr, namelen={namelen})\n" )); return; } // SAFE: name is non-null and >= 8 bytes (checked above); the first u16 is the // address family for ANY sockaddr, so reading it is valid regardless of the real // struct type. We only trust family-specific fields after matching the family. let family = *(name as *const u16); // SAFE: getsockopt is a read-only Winsock query on a valid socket handle; a bad // handle just leaves ty=-1, which we log verbatim. TCP=1 / UDP=2. 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 as i32, SO_TYPE, &mut ty as *mut i32 as *mut u8, &mut len, ); ty }; match family { AF_INET => { // SAFE: family is AF_INET and namelen >= 8 == sizeof(sockaddr_in) fields we read. let sa = &*(name as *const SockaddrIn); // sin_addr holds the address in NETWORK byte order; on little-endian x86, // to_le_bytes reproduces those 4 bytes in memory order, which IS the dotted // quad. So b[0].b[1].b[2].b[3] is correct. (The legacy connect_hook log line // prints these reversed — a cosmetic bug there; this M0 line is the correct // one to trust.) let b = sa.sin_addr.to_le_bytes(); let port = u16::from_be(sa.sin_port); let is_loopback = b[0] == 127; let is_lsx = matches!(port, 3216 | 3217); // known-good LSX channel; not Blaze let mut tag = String::new(); if is_blaze_port(port) { tag.push_str(" <-- BLAZE-PORT"); } // A loopback connect on anything other than LSX is the situation-(a) signal. if is_loopback && !is_lsx { tag.push_str(" <-- LOOPBACK non-LSX"); } crate::write_log(&format!( "TRANSPORT_WATCH: {api} target={}.{}.{}.{}:{port} sock_type={sock_type}{tag}\n", b[0], b[1], b[2], b[3] )); } AF_INET6 => { if namelen < 28 { crate::write_log(&format!( "TRANSPORT_WATCH: {api} family=INET6 (short sockaddr, namelen={namelen})\n" )); return; } // SAFE: family is AF_INET6 and namelen >= 28 == sizeof(sockaddr_in6). let sa = &*(name as *const SockaddrIn6); let a = sa.sin6_addr; // 16 bytes, network order let port = u16::from_be(sa.sin6_port); // Format as 8 colon-separated hex groups (not compressed — clarity over // brevity for a log meant to be grepped). let hex = (0..8) .map(|i| format!("{:02x}{:02x}", a[i * 2], a[i * 2 + 1])) .collect::>() .join(":"); // ::1 = loopback: first 15 bytes zero, last byte 1. let is_loopback = a[..15].iter().all(|&x| x == 0) && a[15] == 1; let mut tag = String::new(); if is_blaze_port(port) { tag.push_str(" <-- BLAZE-PORT"); } if is_loopback { tag.push_str(" <-- IPv6 LOOPBACK (::1)"); } crate::write_log(&format!( "TRANSPORT_WATCH: {api} target=[{hex}]:{port} sock_type={sock_type} (IPv6){tag}\n" )); } other => { // AF_UNIX=1 or anything else — where a named-pipe/unix-socket-style local // Blaze transport would surface. crate::write_log(&format!( "TRANSPORT_WATCH: {api} family={other} (non-INET — possible AF_UNIX/pipe-like)\n" )); } } }