openfut-hook: RE instrumentation for the Blaze dial-gate investigation
In-process, read-only probes and transport observation built while closing the online/FUT route from both the memory and network sides. - probe.rs / dial_notification.rs: menu-time ctx dump, connMgr enumerator, synthetic dial-notification + direct-call dial trigger, and the [element+0x40] container write-watchpoint. All env-gated, one-shot, VirtualQuery-guarded; none alter game state by default. - transport_watch.rs + connect/connectex/hooks/lib: M0 transport observation (grep-friendly TRANSPORT_WATCH logging on the existing getaddrinfo/connect/ WSAConnect/ConnectEx detours) and an IPv6 (v4-mapped) EA-redirect so the game's IPv6 :443 dials land on the bridge instead of the dead servers. Findings: the game never initiates a Blaze connection offline; the dial handler is registered by a self-registering, message-driven state machine whose container stays empty with no Blaze exchange. See openfut-bridge docs/closure-and-preservation.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,27 @@ struct SockaddrIn {
|
||||
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,
|
||||
}
|
||||
|
||||
/// IPv4-mapped IPv6 loopback: `::ffff:127.0.0.1`. An `AF_INET6` socket connecting to
|
||||
/// this sends real IPv4 packets to 127.0.0.1, so the connection lands on the bridge's
|
||||
/// existing IPv4 listener on :8443 — no separate IPv6 listener needed. The game's own
|
||||
/// EA dials already use v4-mapped addresses (`::ffff:x.x.x.x`), so its sockets are not
|
||||
/// `IPV6_V6ONLY` and will accept this target.
|
||||
const V4MAPPED_LOOPBACK: [u8; 16] =
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1];
|
||||
|
||||
// Address of ws2_32!connect (set at hook installation)
|
||||
static CONNECT_ADDR: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
@@ -61,40 +82,87 @@ unsafe fn restore_original(target: *mut u8) {
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 16], i32)> {
|
||||
if namelen < 8 { return None; }
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
if sa.sin_family != AF_INET { return None; }
|
||||
/// If `name` is an EA-relevant connect target, return a rewritten sockaddr pointing at
|
||||
/// the local bridge (plus its byte length). Handles BOTH `AF_INET` and `AF_INET6`: the
|
||||
/// game's Blaze/DirtySDK stack dials EA over IPv6 (v4-mapped) on :443, and the old
|
||||
/// IPv4-only path let those slip straight past us to the real (dead) servers.
|
||||
///
|
||||
/// The returned buffer is 28 bytes (enough for a `sockaddr_in6`); the second value is
|
||||
/// how many of those bytes are meaningful (16 for v4, 28 for v6). `pub(crate)` so the
|
||||
/// ConnectEx path can share this one implementation.
|
||||
pub(crate) unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 28], i32)> {
|
||||
if namelen < 8 || name.is_null() {
|
||||
return None;
|
||||
}
|
||||
// The first u16 of any sockaddr is the address family.
|
||||
let family = *(name as *const u16);
|
||||
let mut buf = [0u8; 28];
|
||||
|
||||
let orig = sa.sin_addr.to_le_bytes();
|
||||
let orig_port = u16::from_be(sa.sin_port);
|
||||
|
||||
let new_port_nbo = match sa.sin_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
#[cfg(not(feature = "capture_baseline"))]
|
||||
PORT_LSX_NBO => PORT_LSX_TARGET_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
||||
orig[3], orig[2], orig[1], orig[0], orig_port,
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
|
||||
let mut buf = [0u8; 16];
|
||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
||||
out.sin_family = AF_INET;
|
||||
out.sin_port = new_port_nbo;
|
||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
||||
Some((buf, 16))
|
||||
match family {
|
||||
AF_INET => {
|
||||
// SAFE: family is AF_INET and namelen >= 8 == the sockaddr_in fields we read.
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
let new_port_nbo = match sa.sin_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
#[cfg(not(feature = "capture_baseline"))]
|
||||
PORT_LSX_NBO => PORT_LSX_TARGET_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => return None,
|
||||
};
|
||||
// sin_addr is network order; to_le_bytes gives memory order = the dotted
|
||||
// quad, so b[0].b[1].b[2].b[3] is correct (the old code printed it reversed).
|
||||
let o = sa.sin_addr.to_le_bytes();
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: v4 {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
||||
o[0], o[1], o[2], o[3], u16::from_be(sa.sin_port),
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
// SAFE: buf is 28 bytes, larger than the 16-byte sockaddr_in we write.
|
||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
||||
out.sin_family = AF_INET;
|
||||
out.sin_port = new_port_nbo;
|
||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
||||
Some((buf, 16))
|
||||
}
|
||||
AF_INET6 => {
|
||||
if namelen < 28 {
|
||||
return None;
|
||||
}
|
||||
// SAFE: family is AF_INET6 and namelen >= 28 == sizeof(sockaddr_in6).
|
||||
let sa6 = &*(name as *const SockaddrIn6);
|
||||
// LSX is IPv4-only (anadius keys on it), so it is intentionally omitted here.
|
||||
let new_port_nbo = match sa6.sin6_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => return None,
|
||||
};
|
||||
let a = sa6.sin6_addr;
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: v6 [{:02x}{:02x}:..:{:02x}{:02x}]:{} → ::ffff:127.0.0.1:{}\n",
|
||||
a[0], a[1], a[14], a[15], u16::from_be(sa6.sin6_port),
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
// SAFE: buf is exactly 28 bytes == sizeof(sockaddr_in6).
|
||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn6);
|
||||
out.sin6_family = AF_INET6;
|
||||
out.sin6_port = new_port_nbo;
|
||||
out.sin6_flowinfo = 0;
|
||||
out.sin6_addr = V4MAPPED_LOOPBACK;
|
||||
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;
|
||||
|
||||
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
|
||||
crate::transport_watch::note_connect("connect", name, namelen, s);
|
||||
|
||||
// Log every call so we can confirm the hook fires at all
|
||||
if namelen >= 8 {
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
@@ -157,6 +225,8 @@ pub unsafe extern "system" fn hooked_wsa_connect(
|
||||
caller: *const (), callee: *const (),
|
||||
sqos: *const (), gqos: *const (),
|
||||
) -> i32 {
|
||||
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
|
||||
crate::transport_watch::note_connect("WSAConnect", name, namelen, s);
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user