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:
funman300
2026-08-20 20:38:10 +00:00
parent 966e92b304
commit 16f3452990
5 changed files with 249 additions and 1 deletions
+89
View File
@@ -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);
}
}