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;
|
pub const FIFA17_BLAZE_REDIRECTOR: u16 = 42230;
|
||||||
/// EA Blaze main server source port.
|
/// EA Blaze main server source port.
|
||||||
pub const BLAZE_MAIN: u16 = 42127;
|
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
|
/// 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
|
/// which serves `/fut/packs/loc/storepackdescriptions.en_us.xml`. Verified
|
||||||
/// live: that path returns 200 with a 180-byte XLIFF document.
|
/// live: that path returns 200 with a 180-byte XLIFF document.
|
||||||
pub const FUT_CONTENT: u16 = 8085;
|
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
|
/// 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
|
/// FUT web-file content server. Not a redirect destination — see
|
||||||
/// [`default_ports::FUT_CONTENT`].
|
/// [`default_ports::FUT_CONTENT`].
|
||||||
pub fut_content: u16,
|
pub fut_content: u16,
|
||||||
|
/// Destination for roster traffic ([`ea_ports::FIFA17_ROSTER`]).
|
||||||
|
pub roster: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for OpenFutPorts {
|
impl Default for OpenFutPorts {
|
||||||
@@ -89,6 +111,7 @@ impl Default for OpenFutPorts {
|
|||||||
blaze_redirector: default_ports::BLAZE_REDIRECTOR,
|
blaze_redirector: default_ports::BLAZE_REDIRECTOR,
|
||||||
blaze_main: default_ports::BLAZE_MAIN,
|
blaze_main: default_ports::BLAZE_MAIN,
|
||||||
fut_content: default_ports::FUT_CONTENT,
|
fut_content: default_ports::FUT_CONTENT,
|
||||||
|
roster: default_ports::ROSTER,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +127,7 @@ impl OpenFutPorts {
|
|||||||
Some(self.blaze_redirector)
|
Some(self.blaze_redirector)
|
||||||
}
|
}
|
||||||
ea_ports::BLAZE_MAIN => Some(self.blaze_main),
|
ea_ports::BLAZE_MAIN => Some(self.blaze_main),
|
||||||
|
ea_ports::FIFA17_ROSTER => Some(self.roster),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,6 +284,7 @@ impl ServerConfig {
|
|||||||
"blaze_redirector_port" => ports.blaze_redirector = parse_port(value)?,
|
"blaze_redirector_port" => ports.blaze_redirector = parse_port(value)?,
|
||||||
"blaze_main_port" => ports.blaze_main = parse_port(value)?,
|
"blaze_main_port" => ports.blaze_main = parse_port(value)?,
|
||||||
"fut_content_port" => ports.fut_content = parse_port(value)?,
|
"fut_content_port" => ports.fut_content = parse_port(value)?,
|
||||||
|
"roster_port" => ports.roster = parse_port(value)?,
|
||||||
other => {
|
other => {
|
||||||
return Err(ConfigError::MalformedConfig(format!(
|
return Err(ConfigError::MalformedConfig(format!(
|
||||||
"line {}: unknown key '{other}'",
|
"line {}: unknown key '{other}'",
|
||||||
@@ -274,15 +299,26 @@ impl ServerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize to the structured `openfut.cfg` format.
|
/// 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 {
|
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",
|
"host={}\nhttps_port={}\nblaze_redirector_port={}\nblaze_main_port={}\nfut_content_port={}\n",
|
||||||
self.host,
|
self.host,
|
||||||
self.ports.https,
|
self.ports.https,
|
||||||
self.ports.blaze_redirector,
|
self.ports.blaze_redirector,
|
||||||
self.ports.blaze_main,
|
self.ports.blaze_main,
|
||||||
self.ports.fut_content
|
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.
|
/// Base URL the FUT web-file (CDN) prefix is built from, e.g.
|
||||||
@@ -485,6 +521,7 @@ mod tests {
|
|||||||
blaze_redirector: 10041,
|
blaze_redirector: 10041,
|
||||||
blaze_main: 42127,
|
blaze_main: 42127,
|
||||||
fut_content: 8085,
|
fut_content: 8085,
|
||||||
|
roster: default_ports::ROSTER,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let s = c.to_cfg_string();
|
let s = c.to_cfg_string();
|
||||||
@@ -555,6 +592,46 @@ mod tests {
|
|||||||
assert_eq!(p.map_source_port(12345), None);
|
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]
|
#[test]
|
||||||
fn resolve_literal_ipv4_no_dns() {
|
fn resolve_literal_ipv4_no_dns() {
|
||||||
let c = ServerConfig::parse("host=127.0.0.1\n").unwrap();
|
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_redirector: self.openfut_blaze_redirector_port,
|
||||||
blaze_main: self.openfut_blaze_main_port,
|
blaze_main: self.openfut_blaze_main_port,
|
||||||
fut_content: openfut_common::default_ports::FUT_CONTENT,
|
fut_content: openfut_common::default_ports::FUT_CONTENT,
|
||||||
|
roster: openfut_common::default_ports::ROSTER,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user