roster: redirect the FIFA 17 roster dial at the socket
FIFA 17's ProtoSSL verifies the roster certificate by dNSName only, so an IP-addressed roster host is refused even with the IP in the SANs. The hostname therefore has to survive into SNI while the connection lands on our server. The connect/WSAConnect/ConnectEx detour already intercepted the dial; it just did not rewrite it, because 8081 was absent from the EA port table. Adding ea_ports::FIFA17_ROSTER plus an OpenFutPorts.roster destination makes the existing, proven redirect handle it with no new hook surface, and removes the need for any client-side DNS change. to_cfg_string writes roster_port ONLY when it differs from the default: the parser rejects unknown keys, so emitting it unconditionally would make an already-deployed older hook reject the whole config and install no redirect at all -- breaking the game instead of degrading.
This commit is contained in:
@@ -40,6 +40,21 @@ pub mod ea_ports {
|
||||
pub const FIFA17_BLAZE_REDIRECTOR: u16 = 42230;
|
||||
/// EA Blaze main server source port.
|
||||
pub const BLAZE_MAIN: u16 = 42127;
|
||||
/// The roster / "FUT Squad Update" port.
|
||||
///
|
||||
/// Unlike the others this number is OURS: the client only dials it because
|
||||
/// our Blaze hands it `ROSTERUPDATE_URL = https://<roster-host>:8081/...`.
|
||||
/// It is still a *signature* in exactly the same sense, because the IP the
|
||||
/// client dials is whatever the roster hostname resolved to — in practice
|
||||
/// EA's live `159.153.51.20` record — and we rewrite that to the configured
|
||||
/// server while leaving the hostname (and therefore SNI) untouched.
|
||||
///
|
||||
/// Keeping the hostname is the whole point: FIFA 17's ProtoSSL verifies the
|
||||
/// roster certificate by **dNSName only**, so redirecting at the socket
|
||||
/// preserves certificate validity in a way an IP-addressed URL cannot. This
|
||||
/// is what removes the need for a client hosts entry, an NRPT rule, or an
|
||||
/// external DNS responder.
|
||||
pub const FIFA17_ROSTER: u16 = 8081;
|
||||
}
|
||||
|
||||
/// Default OpenFUT *destination* ports, derived from the current OpenFUT server
|
||||
@@ -64,6 +79,11 @@ pub mod default_ports {
|
||||
/// which serves `/fut/packs/loc/storepackdescriptions.en_us.xml`. Verified
|
||||
/// live: that path returns 200 with a 180-byte XLIFF document.
|
||||
pub const FUT_CONTENT: u16 = 8085;
|
||||
/// OpenFUT roster / "FUT Squad Update" listener. Same number as the
|
||||
/// [`ea_ports::FIFA17_ROSTER`] signature because we advertise that port
|
||||
/// ourselves; it is a separate constant so a deployment can move the roster
|
||||
/// service without changing what the client dials.
|
||||
pub const ROSTER: u16 = 8081;
|
||||
}
|
||||
|
||||
/// OpenFUT destination ports. Each field is where an intercepted EA source port
|
||||
@@ -80,6 +100,8 @@ pub struct OpenFutPorts {
|
||||
/// FUT web-file content server. Not a redirect destination — see
|
||||
/// [`default_ports::FUT_CONTENT`].
|
||||
pub fut_content: u16,
|
||||
/// Destination for roster traffic ([`ea_ports::FIFA17_ROSTER`]).
|
||||
pub roster: u16,
|
||||
}
|
||||
|
||||
impl Default for OpenFutPorts {
|
||||
@@ -89,6 +111,7 @@ impl Default for OpenFutPorts {
|
||||
blaze_redirector: default_ports::BLAZE_REDIRECTOR,
|
||||
blaze_main: default_ports::BLAZE_MAIN,
|
||||
fut_content: default_ports::FUT_CONTENT,
|
||||
roster: default_ports::ROSTER,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +127,7 @@ impl OpenFutPorts {
|
||||
Some(self.blaze_redirector)
|
||||
}
|
||||
ea_ports::BLAZE_MAIN => Some(self.blaze_main),
|
||||
ea_ports::FIFA17_ROSTER => Some(self.roster),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -260,6 +284,7 @@ impl ServerConfig {
|
||||
"blaze_redirector_port" => ports.blaze_redirector = parse_port(value)?,
|
||||
"blaze_main_port" => ports.blaze_main = parse_port(value)?,
|
||||
"fut_content_port" => ports.fut_content = parse_port(value)?,
|
||||
"roster_port" => ports.roster = parse_port(value)?,
|
||||
other => {
|
||||
return Err(ConfigError::MalformedConfig(format!(
|
||||
"line {}: unknown key '{other}'",
|
||||
@@ -274,15 +299,26 @@ impl ServerConfig {
|
||||
}
|
||||
|
||||
/// Serialize to the structured `openfut.cfg` format.
|
||||
///
|
||||
/// `roster_port` is emitted ONLY when it differs from the default. The
|
||||
/// parser rejects unknown keys, so a config written by a newer launcher and
|
||||
/// read by an older hook would fail to parse and install NO redirect at all
|
||||
/// — breaking the game rather than degrading. Withholding the default keeps
|
||||
/// the common case byte-identical to what every deployed hook already
|
||||
/// accepts, while still round-tripping a deliberately changed port.
|
||||
pub fn to_cfg_string(&self) -> String {
|
||||
format!(
|
||||
let mut out = format!(
|
||||
"host={}\nhttps_port={}\nblaze_redirector_port={}\nblaze_main_port={}\nfut_content_port={}\n",
|
||||
self.host,
|
||||
self.ports.https,
|
||||
self.ports.blaze_redirector,
|
||||
self.ports.blaze_main,
|
||||
self.ports.fut_content
|
||||
)
|
||||
);
|
||||
if self.ports.roster != default_ports::ROSTER {
|
||||
out.push_str(&format!("roster_port={}\n", self.ports.roster));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Base URL the FUT web-file (CDN) prefix is built from, e.g.
|
||||
@@ -485,6 +521,7 @@ mod tests {
|
||||
blaze_redirector: 10041,
|
||||
blaze_main: 42127,
|
||||
fut_content: 8085,
|
||||
roster: default_ports::ROSTER,
|
||||
},
|
||||
};
|
||||
let s = c.to_cfg_string();
|
||||
@@ -555,6 +592,46 @@ mod tests {
|
||||
assert_eq!(p.map_source_port(12345), None);
|
||||
}
|
||||
|
||||
/// The roster dial is the whole point of the FIFA17_ROSTER signature: the
|
||||
/// client resolves `winter15.gosredirector.ea.com` to EA's live record and
|
||||
/// dials THAT ip on 8081, so the socket layer is the only place we can send
|
||||
/// it to ourselves without touching the client's DNS.
|
||||
#[test]
|
||||
fn roster_port_is_redirected_to_the_configured_server() {
|
||||
let server = ServerConfig::parse("host=10.10.0.120\n")
|
||||
.unwrap()
|
||||
.resolve()
|
||||
.unwrap();
|
||||
let redirect = server
|
||||
.redirect_for_ea_port(sin_port_nbo(ea_ports::FIFA17_ROSTER))
|
||||
.expect("roster dial must be recognised");
|
||||
assert_eq!(redirect.redirect_ip, Ipv4Addr::new(10, 10, 0, 120));
|
||||
// Port is preserved: we advertise 8081 and serve 8081.
|
||||
assert_eq!(redirect.port_nbo, sin_port_nbo(8081));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roster_destination_port_is_configurable() {
|
||||
let c = ServerConfig::parse("host=10.10.0.120\nroster_port=9443\n").unwrap();
|
||||
assert_eq!(c.ports.roster, 9443);
|
||||
assert_eq!(c.ports.map_source_port(ea_ports::FIFA17_ROSTER), Some(9443));
|
||||
}
|
||||
|
||||
/// A default roster port must NOT appear in the written config: the parser
|
||||
/// rejects unknown keys, so emitting it unconditionally would make every
|
||||
/// already-deployed hook reject the whole file and install no redirect.
|
||||
#[test]
|
||||
fn default_roster_port_is_not_emitted_but_a_custom_one_round_trips() {
|
||||
let default_cfg = ServerConfig::parse("host=10.10.0.120\n").unwrap();
|
||||
assert!(!default_cfg.to_cfg_string().contains("roster_port"));
|
||||
|
||||
let mut custom = default_cfg.clone();
|
||||
custom.ports.roster = 9443;
|
||||
let reparsed = ServerConfig::parse(&custom.to_cfg_string()).unwrap();
|
||||
assert_eq!(reparsed.ports.roster, 9443);
|
||||
assert_eq!(reparsed, custom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_literal_ipv4_no_dns() {
|
||||
let c = ServerConfig::parse("host=127.0.0.1\n").unwrap();
|
||||
|
||||
@@ -326,6 +326,7 @@ impl LauncherConfig {
|
||||
blaze_redirector: self.openfut_blaze_redirector_port,
|
||||
blaze_main: self.openfut_blaze_main_port,
|
||||
fut_content: openfut_common::default_ports::FUT_CONTENT,
|
||||
roster: openfut_common::default_ports::ROSTER,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user