#!/usr/bin/env bash # Switch the Blaze hop between the Python backend and the Rust sidecar, # WITHOUT modifying the Python backend. # # blaze-switch.sh status # blaze-switch.sh on Blaze -> Rust # blaze-switch.sh off Blaze -> Python (rollback) # # WHY NAT RATHER THAN RECONFIGURING THE REDIRECTOR # # The Python redirector advertises a hardcoded `BLAZE_PORT = 42130` # (blaze_responder_v3b.py:173), so pointing the client at another port would # mean editing the frozen oracle and rebuilding the container. A scoped NAT rule # changes nothing in Python, applies instantly, and rolls back with one command # — exactly the property the A/B needs. # # SCOPE, deliberately narrow # # Rules match ONLY traffic to :42130: # * PREROUTING — the FIFA client on the game machine (the real path) # * OUTPUT — this host's own connections, so the switch can be smoke # tested locally before FIFA is involved # # Traffic to 127.0.0.1:42130 is deliberately NOT matched, so Python stays # directly reachable on loopback while the switch is on. That is what lets # check-live-parity.sh compare real Python against real Rust rather than # accidentally comparing Rust against itself. # # MATCHING RULES BY THE BARE TAG, ON PURPOSE # # An earlier version matched `--comment "tag"` with quotes. This iptables emits # the comment unquoted, so removal silently found nothing — and because the # post-removal verification used the SAME matcher, it confirmed its own failure # and reported a successful rollback that had not happened. A rollback that lies # is worse than one that fails. # # Two lessons are baked in below: match the bare tag string so no output-format # assumption can be wrong, and verify with a predicate that does not share the # removal's failure mode. set -uo pipefail TAG="openfut-blaze-switch" PY_PORT=42130 SUDO="" [[ $EUID -eq 0 ]] || SUDO=sudo die() { echo "blaze-switch: $*" >&2; exit 1; } # Full rule specs carrying our tag, as `-A CHAIN ...` lines. tagged_specs() { $SUDO iptables -t nat -S 2>/dev/null | grep -F -- "$TAG" || true } # Independent verifier: a different command and a different output format from # the one used to build delete commands, so a parsing bug cannot hide itself. count_tagged() { $SUDO iptables-save -t nat 2>/dev/null | grep -cF -- "$TAG" || true } # Behavioural check: is anything still redirecting our port? redirects_to() { $SUDO iptables -t nat -S 2>/dev/null \ | grep -E -- "--dport ${PY_PORT}\b" \ | grep -F -- "REDIRECT" || true } cmd_status() { local specs count specs="$(tagged_specs)" count="$(count_tagged)" if [[ -z "$specs" && "$count" == "0" ]]; then echo "Blaze is served by PYTHON (no switch rules)" else echo "Blaze is redirected to the RUST sidecar:" [[ -n "$specs" ]] && echo "$specs" | sed 's/^/ /' fi # Disagreement between the two views means one of them is parsing wrongly — # report it rather than trusting either. local n_specs n_specs="$(printf '%s' "$specs" | grep -c . || true)" if [[ "$n_specs" != "$count" ]]; then echo " WARNING: rule views disagree (specs=$n_specs, save=$count)" >&2 fi local other other="$(redirects_to | grep -vF -- "$TAG" || true)" if [[ -n "$other" ]]; then echo " note: other REDIRECT rules also touch port $PY_PORT:" >&2 echo "$other" | sed 's/^/ /' >&2 fi return 0 } remove_rules() { local removed=0 spec while IFS= read -r spec; do [[ -n "$spec" ]] || continue # `-A CHAIN args…` -> `-D CHAIN args…` # shellcheck disable=SC2086 if $SUDO iptables -t nat -D ${spec#-A } 2>/dev/null; then removed=$((removed + 1)) else echo "blaze-switch: failed to delete: $spec" >&2 fi done < <(tagged_specs) echo "$removed" } cmd_on() { local ip="${1:-}" port="${2:-}" [[ -n "$ip" && -n "$port" ]] || die "usage: blaze-switch.sh on " # Never stack rules: start from a known state. remove_rules >/dev/null $SUDO iptables -t nat -I PREROUTING 1 -p tcp -d "$ip" --dport "$PY_PORT" \ -m comment --comment "$TAG" -j REDIRECT --to-ports "$port" \ || die "failed to add PREROUTING rule" $SUDO iptables -t nat -I OUTPUT 1 -p tcp -d "$ip" --dport "$PY_PORT" \ -m comment --comment "$TAG" -j REDIRECT --to-ports "$port" \ || die "failed to add OUTPUT rule" local count count="$(count_tagged)" [[ "$count" == "2" ]] || die "expected 2 rules after 'on', found $count" echo "Blaze -> RUST: $ip:$PY_PORT now lands on local port $port" echo " 127.0.0.1:$PY_PORT still reaches PYTHON (unmatched by design)" echo " roll back with: $0 off" echo echo " NOTE: while this is on, the sidecar MUST stay up. Stopping it without" echo " switching off leaves Blaze pointing at a dead port." } cmd_off() { local before removed after before="$(count_tagged)" removed="$(remove_rules)" after="$(count_tagged)" if [[ "$after" != "0" ]]; then echo "FAILED: $after switch rule(s) still present after removing $removed" >&2 tagged_specs | sed 's/^/ /' >&2 return 1 fi # Independent of the tag entirely: nothing should still be redirecting the # Blaze port. Catches a rule that lost its comment somehow. local stray stray="$(redirects_to)" if [[ -n "$stray" ]]; then echo "FAILED: a REDIRECT rule still targets port $PY_PORT:" >&2 echo "$stray" | sed 's/^/ /' >&2 return 1 fi echo "Blaze -> PYTHON: removed $removed rule(s) (was $before), verified none remain" return 0 } case "${1:-}" in status) shift; cmd_status "$@" ;; on) shift; cmd_on "$@" ;; off) shift; cmd_off "$@" ;; *) sed -n '2,10p' "$0" | sed 's/^# \?//'; exit 2 ;; esac