fbc0da2a1b
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.
92 lines
4.0 KiB
Bash
92 lines
4.0 KiB
Bash
#!/usr/bin/env bash
|
|
# ============================================================================
|
|
# OpenFUT FIFA-17 FUT backend — in-CONTAINER orchestrator.
|
|
#
|
|
# Runs the 5 network responders that the game dials. Unlike the host-based
|
|
# openfut-fut.sh, this does NO host arming (no pkexec / iptables / /etc/hosts /
|
|
# ptrace) — those are client-side concerns handled on the game machine (105).
|
|
# autopatch.py is NOT run here: it patches the FIFA17.exe process memory and must
|
|
# run on the box the game runs on.
|
|
#
|
|
# Address behaviour is driven by two env vars (see each responder):
|
|
# OPENFUT_BIND bind address for every listener (container: 0.0.0.0)
|
|
# OPENFUT_ADVERTISE address handed to the client for the next hop
|
|
# (the server's LAN IP, e.g. 203.0.113.10)
|
|
# ============================================================================
|
|
set -uo pipefail
|
|
cd "$(dirname "$(readlink -f "$0")")/tools"
|
|
|
|
BIND="${OPENFUT_BIND:-0.0.0.0}"
|
|
ADV="${OPENFUT_ADVERTISE:?OPENFUT_ADVERTISE must be set to the server LAN IP (e.g. 203.0.113.10)}"
|
|
export OPENFUT_BIND="$BIND"
|
|
export OPENFUT_ADVERTISE="$ADV"
|
|
# POW keys advertised by blaze must also point at the server, not loopback.
|
|
export POW_HOST="${POW_HOST:-$ADV:8094}"
|
|
export POW_CONTENT_HOST="${POW_CONTENT_HOST:-$ADV:8080}"
|
|
export POW_ADDR="${POW_ADDR:-$BIND:8094}"
|
|
export POW_CONTENT_ADDR="${POW_CONTENT_ADDR:-$BIND:8080}"
|
|
|
|
echo "[openfut] bind=$BIND advertise=$ADV"
|
|
|
|
# The TLS cert every responder serves must carry the ADVERTISED IP in its SAN.
|
|
# The client dials the roster (:8081) and redirector by that IP, and that path
|
|
# validates the cert's SAN against the dialed address — it is NOT covered by the
|
|
# two client-side ProtoSSL gates autopatch patches, so a cert lacking IP:$ADV is
|
|
# rejected with fatal certificate_unknown and the FUT hub fails with "An error
|
|
# occurred downloading the FUT Squad Update" (docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md).
|
|
# The advertised IP is unknown at image-build time, so reconcile it here: reissue
|
|
# only when the current cert does not already carry it, so a restart reuses the
|
|
# same cert (no per-start fingerprint churn) and this self-heals if $ADV changes.
|
|
CERT=redir_cert.pem KEY=redir_key.pem
|
|
if ! openssl x509 -in "$CERT" -noout -ext subjectAltName 2>/dev/null | grep -qF "IP Address:$ADV"; then
|
|
echo "[openfut] reissuing TLS cert with SAN IP:$ADV (was missing it)"
|
|
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:$ADV,IP:127.0.0.1" \
|
|
>/dev/null 2>&1 \
|
|
&& echo "[openfut] cert SAN now: $(openssl x509 -in "$CERT" -noout -ext subjectAltName 2>/dev/null | tail -1 | tr -s ' ')" \
|
|
|| { echo "[openfut] FATAL: could not reissue TLS cert" >&2; exit 1; }
|
|
fi
|
|
|
|
# name script extra-env
|
|
declare -a SERVERS=(
|
|
"lsx|lsx_responder_v2.py|OPENFUT_LSX_EVENT_COUNT=100000"
|
|
"blaze|blaze_responder_v3b.py|-"
|
|
"roster|roster_server.py|-"
|
|
"utas|utas_server.py|FUT_TRADING=1 FUT_PILESIZES=1 FUT_TRADEABLE=1 FUT_DISCARD_TABLE=1 FUT_DISCARD_SEND=1"
|
|
"pow|pow_server.py|-"
|
|
)
|
|
|
|
pids=()
|
|
names=()
|
|
for entry in "${SERVERS[@]}"; do
|
|
IFS='|' read -r name script env <<<"$entry"
|
|
envprefix=""; [ "$env" != "-" ] && envprefix="env $env"
|
|
echo "[openfut] starting $name ($script)"
|
|
# shellcheck disable=SC2086
|
|
$envprefix python3 -u "$script" &
|
|
pids+=($!)
|
|
names+=("$name")
|
|
done
|
|
|
|
# Propagate SIGTERM/SIGINT to children so `docker stop` is clean.
|
|
term() {
|
|
echo "[openfut] shutting down…"
|
|
for p in "${pids[@]}"; do kill "$p" 2>/dev/null || true; done
|
|
wait
|
|
exit 0
|
|
}
|
|
trap term TERM INT
|
|
|
|
# If ANY responder dies, take the whole container down so the failure is visible
|
|
# (they all bind ports the game needs — a partial stack is a broken stack).
|
|
while true; do
|
|
for i in "${!pids[@]}"; do
|
|
if ! kill -0 "${pids[$i]}" 2>/dev/null; then
|
|
echo "[openfut] responder ${names[$i]} (pid ${pids[$i]}) exited - bringing container down"
|
|
term
|
|
fi
|
|
done
|
|
sleep 2
|
|
done
|