f451406058
Mandatory OpenFUT architecture audit. Two real defects found and fixed, plus
the config surface tightened so neither class can recur.
DEFECT 1 -- hidden localhost fallback. The Rust host defaulted POW hosts to
127.0.0.1 while every other URL followed OPENFUT_ADVERTISE, so a remote
deployment would emit loopback POW URLs and fail far from the cause. It also
diverged from the deployed Python entrypoint, which derives them
(POW_HOST="${POW_HOST:-$ADV:8094}"). POW endpoints now derive from the
advertised address; explicit overrides still win.
DEFECT 2 -- Default gave loopback silently. `Endpoints::default()` and
`AdapterConfig::default()` supplied 127.0.0.1, so anything constructing a
config by omission got loopback with no signal. Both `Default` impls are
REMOVED. Loopback is now `Endpoints::loopback()` / `AdapterConfig::loopback()`:
an explicit, greppable decision. Production uses `advertising(host)`.
CONFIGURABILITY. `blaze_port` and `utas_port` are now config, not literals.
The advertised Blaze port is our choice -- the client goes wherever
<serverinstanceinfo> sends it -- and 8099 is the client's own built-in default
but still deployment config. A bad port value is an error, not a silent
fallback to the previous one.
TEST-NET EVERYWHERE. Committed fixtures and tests used the lab's real LAN
address; a test that passes because its constant matches the current lab
proves nothing about relocatability. Redirector fixtures regenerated on
RFC 5737 TEST-NET-1/2/3 plus loopback. Harness scripts no longer default the
client IP to the lab address -- client-state.sh now requires it.
SEVEN REQUIRED TESTS in tests/deployment_config.rs plus host-side coverage:
remote config never silently becomes localhost; missing advertise fails
clearly; bind may differ from advertise; changing the Blaze port changes the
redirect; changing the host updates all 200+ generated URLs with no
stragglers; no helper bypasses central config; mutations are detectable.
MUTATION TESTED, and it found a hole in the audit tests themselves. Hardcoding
utas_base, reverting the POW derivation and re-hardcoding the Blaze port were
all caught. Making the redirector read `bind` instead of `advertise` was NOT:
`advertising()` sets bind == advertise, so the two sources were
indistinguishable. That is the single most likely bypass -- the oracle really
does read bind for nucleusConnect -- so the test now forces bind != advertise
and asserts the bind address never reaches the wire. Re-mutated: caught.
Wire behaviour unchanged: oracle fixtures still current, 153 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
2.8 KiB
Bash
Executable File
88 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Is the FIFA client currently connected to anything?
|
|
#
|
|
# client-state.sh [client-ip]
|
|
#
|
|
# WHY THIS EXISTS
|
|
#
|
|
# Host-side `ss` CANNOT see the Python backend's connections. The Python
|
|
# responders run inside a container, so a client session terminates at
|
|
# 172.20.0.2:42130 in the container's network namespace; the host only ever sees
|
|
# the NAT'd flow, and a plain `ss | grep <client>` on the host reports nothing.
|
|
#
|
|
# That produced a wrong precondition check: "no .105 Blaze session — closed" was
|
|
# reported while FIFA was mid-session on Python, and gate 9 was armed against a
|
|
# client that had never exited. Python's own log gave it away — it logs closes
|
|
# reliably (45 of them) and there was no close for that session.
|
|
#
|
|
# So this looks in BOTH namespaces, and reports the Rust sidecar and the Python
|
|
# container separately.
|
|
#
|
|
# Exit 0 when the client has no live session anywhere (safe to start a gate),
|
|
# 1 when it does.
|
|
set -uo pipefail
|
|
|
|
CLIENT="${1:-${OPENFUT_CLIENT_IP:-}}"
|
|
if [[ -z "$CLIENT" ]]; then
|
|
echo "usage: client-state.sh <client-ip> (or set OPENFUT_CLIENT_IP)" >&2
|
|
echo " no default: the lab's address is deployment config, not architecture," >&2
|
|
echo " and a default that matches the current lab hides the coupling." >&2
|
|
exit 2
|
|
fi
|
|
CONTAINER="${OPENFUT_PY_CONTAINER:-openfut-fut-backend}"
|
|
live=0
|
|
|
|
decode_tcp() {
|
|
# /proc/net/tcp rows -> "local remote state", little-endian hex addresses.
|
|
python3 -c "
|
|
import sys
|
|
def d(x):
|
|
ip, port = x.split(':')
|
|
return '.'.join(str(int(ip[i:i+2], 16)) for i in (6, 4, 2, 0)) + ':' + str(int(port, 16))
|
|
for line in sys.stdin:
|
|
f = line.split()
|
|
if len(f) < 4 or not f[0].endswith(':'):
|
|
continue
|
|
try:
|
|
print(d(f[1]), d(f[2]), f[3])
|
|
except Exception:
|
|
pass
|
|
"
|
|
}
|
|
|
|
echo "client: $CLIENT"
|
|
|
|
# ---- Rust sidecar (host namespace)
|
|
rust="$(ss -tn state established 2>/dev/null | grep -F "$CLIENT" | grep -E ':42230' || true)"
|
|
if [[ -n "$rust" ]]; then
|
|
echo " RUST sidecar : LIVE session(s)"
|
|
sed 's/^/ /' <<<"$rust"
|
|
live=1
|
|
else
|
|
echo " RUST sidecar : none"
|
|
fi
|
|
|
|
# ---- Python backend (container namespace)
|
|
if docker exec "$CONTAINER" true 2>/dev/null; then
|
|
py="$(docker exec "$CONTAINER" cat /proc/net/tcp 2>/dev/null | decode_tcp \
|
|
| awk '$3=="01"' | grep -F "$CLIENT" || true)"
|
|
if [[ -n "$py" ]]; then
|
|
echo " PYTHON backend: LIVE session(s)"
|
|
awk '{printf " %-22s <- %-22s ESTABLISHED\n", $1, $2}' <<<"$py"
|
|
live=1
|
|
else
|
|
echo " PYTHON backend: none"
|
|
fi
|
|
else
|
|
echo " PYTHON backend: container '$CONTAINER' not reachable — CANNOT confirm"
|
|
live=1 # unknown is not the same as clear
|
|
fi
|
|
|
|
echo
|
|
if [[ $live -eq 0 ]]; then
|
|
echo "RESULT: no live client session — safe to begin a gate"
|
|
else
|
|
echo "RESULT: client still connected — close FIFA before starting a gate"
|
|
fi
|
|
exit $live
|