From fbc0da2a1b6da20e47a0746313a0ff0d3d9ae749 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 18 Aug 2026 15:57:19 +0000 Subject: [PATCH] fix(fifa17-tls): carry the advertised IP in the roster/redirector cert SAN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FUT hub failed to load with "An error occurred downloading the FUT Squad Update" because the client dials the roster (https://: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. --- fifa17-recon/docker/fifa17-python/Dockerfile | 22 +++++++++++++------ .../docker/fifa17-python/entrypoint.sh | 20 +++++++++++++++++ fifa17-recon/tools/openfut-fut.sh | 21 +++++++++++++++--- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/fifa17-recon/docker/fifa17-python/Dockerfile b/fifa17-recon/docker/fifa17-python/Dockerfile index 1795178..5218184 100644 --- a/fifa17-recon/docker/fifa17-python/Dockerfile +++ b/fifa17-recon/docker/fifa17-python/Dockerfile @@ -37,17 +37,25 @@ RUN set -eu; \ COPY data/ /app/data/ -# Redirector TLS cert (CN/SAN = winter15.gosredirector.ea.com). ProtoSSL -# cert-verify is patched client-side, so a self-signed cert is fine. The pair is -# git-ignored (*.pem/*.key); regenerate if absent so a fresh checkout builds -# without extra steps. +# Redirector/roster TLS cert (CN/SAN = winter15.gosredirector.ea.com). ProtoSSL +# cert-verify is patched client-side, so a self-signed cert is fine — but the +# client dials the roster and redirector BY IP, and that path still checks the +# SAN against the dialed address (it is NOT covered by the two patched gates), so +# a cert without a matching IP SAN is rejected with fatal certificate_unknown +# (docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md). The advertised LAN IP is a RUNTIME value, +# unknown here, so this bakes only a loopback-IP baseline and the entrypoint +# reissues with IP:$OPENFUT_ADVERTISE at start. +# +# openssl therefore has to remain in the image for the entrypoint, not be dropped +# with the apt lists. The pair is git-ignored (*.pem/*.key); regenerate if absent +# so a fresh checkout builds without extra steps. +RUN apt-get update && apt-get install -y --no-install-recommends openssl && \ + rm -rf /var/lib/apt/lists/* RUN if [ ! -s tools/redir_cert.pem ] || [ ! -s tools/redir_key.pem ]; then \ - apt-get update && apt-get install -y --no-install-recommends openssl && \ openssl req -x509 -newkey rsa:2048 -nodes \ -keyout tools/redir_key.pem -out tools/redir_cert.pem \ -days 3650 -subj "/CN=winter15.gosredirector.ea.com" \ - -addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com" && \ - rm -rf /var/lib/apt/lists/*; \ + -addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com,IP:127.0.0.1"; \ fi # Bake a dataset manifest so every image is self-identifying. diff --git a/fifa17-recon/docker/fifa17-python/entrypoint.sh b/fifa17-recon/docker/fifa17-python/entrypoint.sh index faf8caf..3ba37a8 100644 --- a/fifa17-recon/docker/fifa17-python/entrypoint.sh +++ b/fifa17-recon/docker/fifa17-python/entrypoint.sh @@ -28,6 +28,26 @@ 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" diff --git a/fifa17-recon/tools/openfut-fut.sh b/fifa17-recon/tools/openfut-fut.sh index b4a78b8..e54b194 100755 --- a/fifa17-recon/tools/openfut-fut.sh +++ b/fifa17-recon/tools/openfut-fut.sh @@ -32,11 +32,26 @@ c() { printf ' %s\n' "$*"; } up() { ss -tlnp 2>/dev/null | grep -q ":$1 "; } ensure_cert() { - [ -s "$CERT" ] && [ -s "$KEY" ] && return 0 - echo "[*] generating self-signed TLS cert (redirector MITM; ProtoSSL verify is patched)" + # 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:127.0.0.1" \ + -addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com,$ip_sans" \ >/dev/null 2>&1 }