Files
OpenFUT-Bridge/src/tls.rs
T
funman300 0138a39f3e LSX gate: FIFA reaches "connecting to EA Servers" via bridge
Implement the LSX (EA App bootstrap, TCP :3216→:3217) server in the bridge
and clear the loading-screen crash gate. FIFA 23 now completes the full LSX
handshake + session against OpenFUT and advances to the online/Blaze phase
("<persona> is connecting to the EA Servers…") — verified live, not a cached
session (the displayed persona "fun" comes from our GetProfile).

Root cause of the invariant loading-screen crash: get_config() omitted the
PROGRESSIVE_INSTALLATION and PROGRESSIVE_INSTALLATION_EVENT services (Name="PI").
FIFA builds its service registry from GetConfig and resolves that handle during
online-init; the missing service left a null handle it dereferenced at
FIFA23.exe+0x133dfc5 (mov rax,[r13+0x18], r13=NULL) ~2s after LSX — identical
across every prior run. Found via FIFA's own CrashReport_*.json (stable RIP) +
diffing anadius64.dll's service-registration table against ours.

Also in this change:
- process_frame(): handle FIFA's pipelined (batched) LSX messages per read,
  eliminating the ~15s handshake hang.
- Real anadius values (RE'd from anadius64.dll + anadius.cfg): PersonaId/UserId/
  persona, locale list, GetGameInfo fixed `GameInfo="<value>"` attribute,
  GetAllGameInfo 14-attr schema, GetSetting `Setting=` attribute.
- IsProgressiveInstallationAvailable served from sender="PI".
- docs/connection-gate-findings.md: full RE trail; relocate stale openfut-hook
  copy under docs/ (canonical hook lives in openfut-launcher).

Next gate: Blaze/online (M2), ridden over ProtoSSL in-process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:37:33 -07:00

82 lines
3.3 KiB
Rust

use std::{path::Path, sync::Arc};
use tokio_rustls::TlsAcceptor;
/// Load a previously persisted cert/key pair, or generate and save a new one.
///
/// Reusing the same cert across restarts means clients only need to trust it once.
pub fn load_or_generate_cert(cert_path: &Path, key_path: &Path) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
if cert_path.exists() && key_path.exists() {
let cert_pem = std::fs::read(cert_path)?;
let key_pem = std::fs::read(key_path)?;
tracing::info!("Loaded existing TLS cert from {:?}", cert_path);
return Ok((cert_pem, key_pem));
}
let (cert_pem, key_pem) = generate_self_signed_cert()?;
if let Some(parent) = cert_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(cert_path, &cert_pem)?;
std::fs::write(key_path, &key_pem)?;
tracing::info!("Generated new TLS cert, saved to {:?}", cert_path);
Ok((cert_pem, key_pem))
}
/// Generate a self-signed certificate covering EA FUT hostnames.
/// CN is set to `fut.ea.com` so ProtoSSL's CN hostname check passes.
/// SANs include wildcard entries for all `*.ea.com` and `*.easports.com` subdomains
/// so connections to any EA subdomain are covered.
pub fn generate_self_signed_cert() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, SanType};
use std::net::{IpAddr, Ipv4Addr};
let mut params = CertificateParams::new(vec![
"fut.ea.com".to_string(),
"utas.mob.v4.fut.ea.com".to_string(),
"accounts.ea.com".to_string(),
"pin.data.ea.com".to_string(),
"gateway.ea.com".to_string(),
"localhost".to_string(),
]);
// Wildcard SANs cover every EA subdomain the game might contact
params.subject_alt_names.push(SanType::DnsName("*.ea.com".to_string()));
params.subject_alt_names.push(SanType::DnsName("*.easports.com".to_string()));
params.subject_alt_names.push(SanType::DnsName("*.ugc.footapi.com".to_string()));
params.subject_alt_names.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
// CN must match the primary hostname so ProtoSSL's CN check passes
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "fut.ea.com");
params.distinguished_name = dn;
let cert = Certificate::from_params(params)?;
let cert_pem = cert.serialize_pem()?.into_bytes();
let key_pem = cert.serialize_private_key_pem().into_bytes();
Ok((cert_pem, key_pem))
}
/// Build a TlsAcceptor from PEM-encoded certificate and private key bytes.
pub fn make_tls_acceptor(cert_pem: &[u8], key_pem: &[u8]) -> anyhow::Result<TlsAcceptor> {
use rustls::{Certificate, PrivateKey, ServerConfig};
let certs: Vec<Certificate> = rustls_pemfile::certs(&mut std::io::Cursor::new(cert_pem))?
.into_iter()
.map(Certificate)
.collect();
let mut keys = rustls_pemfile::pkcs8_private_keys(&mut std::io::Cursor::new(key_pem))?;
if keys.is_empty() {
anyhow::bail!("no PKCS8 private keys found in key PEM");
}
let config = Arc::new(
ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, PrivateKey(keys.remove(0)))?,
);
Ok(TlsAcceptor::from(config))
}