Files
OpenFUT/fifa17-recon/tools/openfut-fut.sh
T
funman300 fbc0da2a1b fix(fifa17-tls): carry the advertised IP in the roster/redirector cert SAN
The FUT hub failed to load with "An error occurred downloading the FUT Squad
Update" because the client dials the roster (https://<advertise>:8081) and the
redirector BY IP, while the served certificate carried DNS SANs only
(winter15.gosredirector.ea.com + wildcards). The client aborts that handshake with
fatal certificate_unknown. Root cause and evidence in
docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md (commit 082246c): a wire capture shows the client
offering TLS1.2 with RSA suites, the server selecting them, then rejecting the cert —
and autopatch demonstrably patched both ProtoSSL gates in that process, so this
validation path is NOT one of the two the client-side patch covers. The SAN is the fix.

Three generators produced the cert and none put the advertised IP in the SAN:

* docker entrypoint.sh — the production path. The advertised IP is a RUNTIME value
  (OPENFUT_ADVERTISE), unknown at image-build time, so the cert is now reconciled at
  startup: reissued with IP:$ADV,IP:127.0.0.1 in the SAN only when the current cert
  lacks it. That makes a restart reuse the same cert (no per-start fingerprint churn,
  which would otherwise recreate the Aug-13 surprise) and self-heal if $ADV changes.
* Dockerfile — installs openssl unconditionally so the entrypoint can reissue at
  runtime (previously it was dropped with the apt lists), and bakes a loopback-IP
  baseline cert so a plain `docker build` still yields a usable image.
* openfut-fut.sh — the local orchestrator. ensure_cert now defaults the SAN IP to this
  host's primary LAN IP (OPENFUT_ADVERTISE overrides) and reissues when the cert lacks
  it, instead of only generating when the file is absent.

Verified without the client, which is the strongest evidence obtainable here: a
verifying TLS client checking the cert BY IP rejects the old DNS-only cert ("IP address
mismatch, certificate is not valid for '10.10.0.120'") and accepts the new
IP-bearing cert; and the entrypoint reconcile is idempotent end to end — an old cert is
reissued to carry IP:$ADV, a simulated restart leaves the fingerprint unchanged, and
the final SAN carries both the advertised and loopback IPs.

Live confirmation needs the production container rebuilt with OPENFUT_ADVERTISE set
(operator-gated); production is otherwise untouched.

entrypoint.sh carries unrelated pre-existing uncommitted work (env-based component
selection) that is not on any branch; only the cert-reconcile block is committed here,
and that work is left intact in the working tree.
2026-08-18 15:57:19 +00:00

124 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# OpenFUT — FIFA 17 offline FUT backend orchestrator
#
# ./openfut-fut.sh start bring up the whole harness (arm host + all servers)
# ./openfut-fut.sh stop shut the servers down
# ./openfut-fut.sh restart stop + start
# ./openfut-fut.sh status show what's up
#
# After `start`, launch FIFA 17 and select Ultimate Team. See RUNBOOK below (status).
# Volatile host state (sysctls/iptables/cert) does NOT survive a reboot; `start`
# re-arms everything, so just re-run it after booting. /etc/hosts persists.
# ============================================================================
set -uo pipefail
cd "$(dirname "$(readlink -f "$0")")" # tools/
CERT=redir_cert.pem; KEY=redir_key.pem
# name script "port[,port...]" extra-env
SERVERS=(
"lsx lsx_responder_v2.py 4216 OPENFUT_LSX_EVENT_COUNT=100000"
"blaze blaze_responder_v3b.py 42127,42130,42131 -"
"roster roster_server.py 8081 -"
"utas utas_server.py 8099 FUT_TRADING=1 FUT_PILESIZES=1 FUT_TRADEABLE=1 FUT_DISCARD_TABLE=1 FUT_DISCARD_SEND=1"
"autopatch autopatch.py - -"
# POW/EASFC — the "EA FC servers unreachable" layer. Harmless when idle: it just
# binds 8094/8080 and nothing points at it unless FUT_POW=1 makes blaze serve the
# FIFA_POW_URL redirect keys. See pow_server.py for the powdll evidence.
"pow pow_server.py 8094,8080 -"
)
c() { printf ' %s\n' "$*"; }
up() { ss -tlnp 2>/dev/null | grep -q ":$1 "; }
ensure_cert() {
# The cert MUST carry the address the client dials in its SAN, or the roster
# HTTPS handshake is rejected with fatal certificate_unknown and the FUT hub
# fails to load (docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md): the client dials the
# roster/redirector by IP and that path validates the SAN against it. Default to
# this host's primary LAN IP so a client on another machine works;
# OPENFUT_ADVERTISE overrides. Reissue when absent OR when the current cert lacks
# that IP, so this self-heals rather than serving a stale DNS-only cert.
local adv ip_sans regen=0
adv="${OPENFUT_ADVERTISE:-$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')}"
ip_sans="IP:127.0.0.1"; [ -n "$adv" ] && ip_sans="IP:$adv,IP:127.0.0.1"
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
regen=1
elif [ -n "$adv" ] && ! openssl x509 -in "$CERT" -noout -ext subjectAltName 2>/dev/null | grep -qF "IP Address:$adv"; then
regen=1
fi
[ "$regen" = 0 ] && return 0
echo "[*] issuing self-signed TLS cert (SAN includes $ip_sans; redirector MITM; ProtoSSL verify is patched)"
openssl req -x509 -newkey rsa:2048 -nodes -keyout "$KEY" -out "$CERT" -days 3650 \
-subj "/CN=winter15.gosredirector.ea.com" \
-addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com,$ip_sans" \
>/dev/null 2>&1
}
armed() {
[ "$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null)" = 0 ] \
&& [ "$(cat /proc/sys/net/ipv4/conf/lo/route_localnet 2>/dev/null)" = 1 ] \
&& grep -q '[[:space:]]easw\.easports\.com\b' /etc/hosts 2>/dev/null
}
ensure_armed() {
if armed; then c "host already armed (ptrace_scope=0, route_localnet=1, /etc/hosts ok)"; return 0; fi
echo "[*] arming host state (needs root — a password dialog will appear)"
pkexec sh "$PWD/root_arm.sh" || { echo "!! root_arm failed. Run manually: pkexec sh $PWD/root_arm.sh"; return 1; }
}
start() {
ensure_cert
ensure_armed || exit 1
echo "[*] starting servers (detached)…"
for s in "${SERVERS[@]}"; do
read -r name script ports env <<<"$s"
pkill -9 -f "$script" 2>/dev/null; :
done
sleep 1
for s in "${SERVERS[@]}"; do
read -r name script ports env <<<"$s"
local envprefix=""; [ "$env" != "-" ] && envprefix="env $env"
: > "/tmp/${name}.log" 2>/dev/null || true
setsid bash -c "exec $envprefix python3 -u $script" </dev/null >"/tmp/${name}.log" 2>&1 &
disown
done
sleep 2
status
}
stop() {
echo "[*] stopping servers…"
for s in "${SERVERS[@]}"; do read -r name script _ <<<"$s"; pkill -9 -f "$script" 2>/dev/null; :; done
sleep 1; c "servers stopped (host arm + /etc/hosts left intact)"
}
status() {
echo "== OpenFUT FIFA17 FUT backend =="
armed && c "host: ARMED ✓" || c "host: NOT armed (run: pkexec sh $PWD/root_arm.sh)"
for s in "${SERVERS[@]}"; do
read -r name script ports env <<<"$s"
if pgrep -f "$script" >/dev/null; then
if [ "$ports" = "-" ]; then c "$name ✓ (running)"
else
local ok=1; IFS=',' read -ra ps <<<"$ports"
for p in "${ps[@]}"; do up "$p" || ok=0; done
[ $ok = 1 ] && c "$name ✓ ($ports)" || c "$name ⚠ running but a port is down ($ports)"
fi
else c "$name ✗ DOWN"; fi
done
echo "-- RUNBOOK --"
c "1. (this must be done BEFORE launching FIFA — servers bind the ports the game needs)"
c "2. Launch FIFA 17 fresh: ~/Desktop/launch-fifa17.sh (a FRESH launch avoids the live-DB error)"
c "3. In-game: select Ultimate Team. At the 'security question', type ANY answer -> Continue -> OK."
c "4. -> the FUT hub. Logs: /tmp/{lsx,blaze,roster,utas,autopatch}.log"
}
case "${1:-status}" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
*) echo "usage: $0 {start|stop|restart|status}"; exit 1 ;;
esac