diff --git a/openfut-common/src/lib.rs b/openfut-common/src/lib.rs index 45db714..73cab4f 100644 --- a/openfut-common/src/lib.rs +++ b/openfut-common/src/lib.rs @@ -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://: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(); diff --git a/src/config.rs b/src/config.rs index 158607b..87a3628 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, }, } }