feat(fifa17-hook): redirect FIFA17 sockets from shared config
The fifa17 hook path installed no connection redirect (only a module-map dump), so FIFA17 relied entirely on Linux iptables DNAT / hosts (and had no Windows equivalent). Add an in-process, config-driven redirect for the fifa17 build: - openfut-common gains ResolvedServer::redirect_for_ea_port(): the single shared decision (EA source-port signature -> configured OpenFUT host+port, network byte order), reused by the hook so it and openfut.cfg agree by construction. Covers 443->https, 10041/42230->blaze_redirector, 42127->blaze_main. - connect_hook: redirect_if_ea now dispatches to a config-driven rewrite when armed (rewrites to the CONFIGURED, possibly remote, server -- not hardcoded 127.0.0.1), matching by EA source port so a hardcoded EA IP (159.153.51.20 redirector) and a DNS-resolved one both land on the server. Legacy loopback path retained only for the not-yet-retired FIFA23 build. - fifa17::install() reads openfut.cfg next to FIFA17.exe via openfut-common and installs connect + WSAConnect (IAT) + ConnectEx (shared redirect). Fail-safe: missing/invalid config installs NO redirect (traffic untouched), never a corrupt sockaddr. Tests: openfut-common redirect map/sockaddr/endian/remote-host/unknown-port. Cross-builds x86_64-pc-windows-gnu --features fifa17; clippy -D warnings clean. No Linux fallback removed (migration gate).
This commit is contained in:
@@ -115,6 +115,41 @@ pub struct ResolvedServer {
|
||||
pub ports: OpenFutPorts,
|
||||
}
|
||||
|
||||
/// Where one matched EA connection is rewritten to, in the exact WinSock
|
||||
/// on-the-wire representation the socket hooks need. Produced by
|
||||
/// [`ResolvedServer::redirect_for_ea_port`] so the hook and the launcher's
|
||||
/// `openfut.cfg` share one decision by construction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Redirect {
|
||||
/// Rewritten IPv4 for `sockaddr_in.sin_addr` (network byte order in memory).
|
||||
pub addr_nbo: u32,
|
||||
/// Rewritten port for `sin_port` / `sin6_port` (network byte order).
|
||||
pub port_nbo: u16,
|
||||
/// The resolved server IPv4, for callers building an IPv6 v4-mapped address.
|
||||
pub redirect_ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl ResolvedServer {
|
||||
/// Decide the redirect for an outbound EA connection whose destination port
|
||||
/// is `ea_port_nbo` (network byte order, as read straight from the sockaddr).
|
||||
///
|
||||
/// Returns `None` when the port is not a recognised OpenFUT route — the hook
|
||||
/// then leaves the connection untouched. The original destination IP is
|
||||
/// intentionally ignored: matching is by the fixed EA source-port signature
|
||||
/// ([`ea_ports`]), so a hardcoded EA IP (e.g. FIFA17's `159.153.51.20`
|
||||
/// redirector) and a DNS-resolved one are treated identically and both land
|
||||
/// on the configured server — no `/etc/hosts`, DNAT, or portproxy required.
|
||||
pub fn redirect_for_ea_port(&self, ea_port_nbo: u16) -> Option<Redirect> {
|
||||
let ea_port = u16::from_be(ea_port_nbo);
|
||||
let dest_port = self.ports.map_source_port(ea_port)?;
|
||||
Some(Redirect {
|
||||
addr_nbo: sin_addr_from_ipv4(self.redirect_ip),
|
||||
port_nbo: sin_port_nbo(dest_port),
|
||||
redirect_ip: self.redirect_ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors loading/validating OpenFUT server configuration. Every one of these
|
||||
/// must BLOCK operation — none of them may fall back to loopback.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -476,4 +511,58 @@ mod tests {
|
||||
};
|
||||
assert_eq!(c.resolve().unwrap_err(), ConfigError::ServerMissing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_maps_every_fifa17_route_to_configured_server() {
|
||||
// The canonical staging cfg. Ports come from the file, not constants.
|
||||
let resolved = ServerConfig::parse(
|
||||
"host=10.10.0.120\nhttps_port=8443\nblaze_redirector_port=42127\nblaze_main_port=42130\n",
|
||||
)
|
||||
.unwrap()
|
||||
.resolve()
|
||||
.unwrap();
|
||||
let server = Ipv4Addr::new(10, 10, 0, 120);
|
||||
// (EA source port [host order], expected OpenFUT dest port)
|
||||
for (ea, dest) in [
|
||||
(443u16, 8443u16),
|
||||
(10041, 42127),
|
||||
(42230, 42127),
|
||||
(42127, 42130),
|
||||
] {
|
||||
let r = resolved
|
||||
.redirect_for_ea_port(ea.to_be())
|
||||
.unwrap_or_else(|| panic!("EA port {ea} should be a route"));
|
||||
assert_eq!(u16::from_be(r.port_nbo), dest, "EA {ea} -> dest");
|
||||
assert_eq!(r.redirect_ip, server, "EA {ea} -> server ip");
|
||||
assert_eq!(
|
||||
r.addr_nbo,
|
||||
sin_addr_from_ipv4(server),
|
||||
"EA {ea} -> sin_addr"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_leaves_unknown_ports_untouched() {
|
||||
let resolved = ServerConfig::parse("host=10.10.0.120\n")
|
||||
.unwrap()
|
||||
.resolve()
|
||||
.unwrap();
|
||||
assert!(resolved.redirect_for_ea_port(8080u16.to_be()).is_none());
|
||||
assert!(resolved.redirect_for_ea_port(22u16.to_be()).is_none());
|
||||
assert!(resolved.redirect_for_ea_port(443u16.to_be()).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_targets_configured_remote_host_not_loopback() {
|
||||
let resolved = ServerConfig::parse("host=10.10.0.120\n")
|
||||
.unwrap()
|
||||
.resolve()
|
||||
.unwrap();
|
||||
// FIFA17 redirector (hardcoded EA IP 159.153.51.20:42230) must be rewritten
|
||||
// to the configured REMOTE server, never 127.0.0.1.
|
||||
let r = resolved.redirect_for_ea_port(42230u16.to_be()).unwrap();
|
||||
assert_eq!(r.redirect_ip, Ipv4Addr::new(10, 10, 0, 120));
|
||||
assert_ne!(r.redirect_ip, Ipv4Addr::LOCALHOST);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user