tooling: observe client connection ATTEMPTS; make the build guard reject bad args

openfut-observe.sh answers the one question no server log can: when a gate
fails and a service logged nothing, did the client try and fail, or never try?
Both look like silence. Two redirector gates were lost to that ambiguity --
"roster server logged nothing" was equally consistent with a broken roster
service, a wrong roster URL, and a client that never asked.

Built on iptables packet counters because this box has no tcpdump, no
conntrack, and no readable kernel log. That last one is verified rather than
assumed: an initial LOG-based version installed correctly and its rules matched
(counters proved it), but the output went nowhere -- journalctl -k has no
entries and dmesg is empty. Counters are also lower volume and record only SYNs,
so no payload can be captured even in principle.

Validated against the live client, not a loopback stand-in: an initial
self-test using this host's own address counted almost nothing, because
locally-generated packets never traverse PREROUTING. Against the real remote
client it counts 8081 at ~4/min, matching the roster server's own log.

Known gap, recorded rather than hidden: the catch-all TOTAL runs well above the
sum of the named ports, so the client makes steady background attempts to ports
not tracked here. It is present during a working session, so it is not the
failure signature, and it is not chased further here.

verify-build-identity.sh now rejects an argument that is not a commit hash.
Passing the binary path instead of its stamp previously produced a plausible
"REFUSING: binary was built from ./target/release/... but HEAD is <sha>", which
reads as a real stale-build finding rather than a caller mistake -- and a
safeguard that cries wolf is one people learn to route around. Usage error is
now exit 2, distinct from a genuine stale build (1) and success (0).
This commit is contained in:
funman300
2026-08-11 04:22:40 +00:00
parent e2c4ca6d56
commit 5bc39e902d
2 changed files with 150 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Passive connection-attempt observer for one client address.
#
# openfut-observe.sh on <CLIENT_IP>
# openfut-observe.sh off
# openfut-observe.sh status
# openfut-observe.sh mark record the current counters as a baseline
# openfut-observe.sh delta attempts since the last mark
#
# WHY THIS EXISTS
#
# A live gate can fail in two very different ways that look identical in every
# server log: the client tried to connect and could not, or the client never
# tried at all. No server log can separate those, because both produce silence.
#
# Worked example, and the reason this exists: two redirector gates failed with
# "An error occurred downloading the FUT Squad Update" while the roster server
# logged nothing at all. "Nothing" was consistent with a broken roster service,
# a wrong roster URL, and a client that never asked — three very different bugs.
#
# HOW
#
# This box has no tcpdump, no conntrack, and no readable kernel log (iptables
# LOG rules match but their output goes nowhere — verified, not assumed). What
# does work is iptables PACKET COUNTERS, so the observation is built from those:
# a dedicated raw-table chain, one counting rule per interesting port, each with
# no target so it counts and falls through.
#
# Only SYNs are counted, so one line per connection attempt, and no payload is
# recorded — this cannot see message contents even in principle.
#
# SAFETY
#
# A counting rule has no target: it cannot drop, rewrite or delay a packet. All
# state lives in one custom chain, so `off` is "unhook, flush, delete" and its
# verification re-reads the table rather than trusting the delete's exit code.
# No rule value contains a space, which is what made an earlier LOG-based
# version impossible to delete by reconstructed spec.
set -uo pipefail
TAG=openfut-observe
CHAIN=OPENFUT_OBS
TABLE=raw
STATE="${OPENFUT_OBSERVE_STATE:-${TMPDIR:-/tmp}/openfut-observe.mark}"
# Ports worth separating. The final catch-all counts EVERY attempt, so it is a
# TOTAL and not a remainder: attempts to an untracked port show up as the gap
# between TOTAL and the sum of the named ports, rather than vanishing.
PORTS=(42127 42227 42130 8081 8099 8080 8094 9988 8999 4216 80 443 17502)
die() { echo "observe: $*" >&2; exit 1; }
ipt() { sudo iptables -t "$TABLE" "$@"; }
chain_exists() { ipt -S "$CHAIN" >/dev/null 2>&1; }
hooks() { sudo iptables-save -t "$TABLE" 2>/dev/null | grep -cF -- "--comment $TAG"; }
cmd_on() {
local ip="${1:-}"
[[ -n "$ip" ]] || die "usage: openfut-observe.sh on <CLIENT_IP>"
chain_exists && die "already on — run 'off' first"
ipt -N "$CHAIN" || die "could not create $CHAIN"
local p
for p in "${PORTS[@]}"; do
ipt -A "$CHAIN" -p tcp --dport "$p" || { cmd_off >/dev/null; die "rule for $p failed"; }
done
ipt -A "$CHAIN" -p tcp || { cmd_off >/dev/null; die "catch-all rule failed"; }
# --syn is SYN without ACK: one match per connection ATTEMPT, retries included.
ipt -I PREROUTING -s "$ip" -p tcp --syn -m comment --comment "$TAG" -j "$CHAIN" \
|| { cmd_off >/dev/null; die "could not hook $CHAIN into PREROUTING"; }
[[ "$(hooks)" == "1" ]] || { cmd_off >/dev/null; die "hook not installed"; }
rm -f "$STATE"
echo "observing $ip: ${#PORTS[@]} ports + catch-all, hooked into $TABLE/PREROUTING"
}
cmd_off() {
local removed=0
# Unhook by parsed fields as argv elements, then flush and delete. No value
# here contains a space, so this round trip is safe.
while read -r ip; do
[[ -n "$ip" ]] || continue
ipt -D PREROUTING -s "$ip" -p tcp --syn -m comment --comment "$TAG" -j "$CHAIN" \
2>/dev/null && removed=$((removed + 1))
done < <(sudo iptables-save -t "$TABLE" 2>/dev/null \
| grep -F -- "--comment $TAG" \
| sed -nE 's/.* -s ([0-9.]+)(\/32)? .*/\1/p')
chain_exists && { ipt -F "$CHAIN"; ipt -X "$CHAIN"; }
local left_hooks left_chain
left_hooks="$(hooks)"; chain_exists && left_chain=yes || left_chain=no
if [[ "$left_hooks" != "0" || "$left_chain" != "no" ]]; then
echo "observe: REFUSING to report success — hooks=$left_hooks chain=$left_chain" >&2
return 1
fi
rm -f "$STATE"
echo "observing off: removed $removed hook(s) and the chain, verified none remain"
}
# "port<TAB>packets", catch-all reported as "other".
counters() {
ipt -L "$CHAIN" -v -n -x 2>/dev/null | awk '
/dpt:/ { for(i=1;i<=NF;i++) if($i ~ /^dpt:/){ sub(/dpt:/,"",$i); print $i "\t" $1 } ; next }
/^ *[0-9]+ +[0-9]+ +/ && !/dpt:/ && NR>2 { print "TOTAL\t" $1 }'
}
cmd_status() {
chain_exists || { echo "INACTIVE (no observe chain)"; return 0; }
echo "ACTIVE, hooked for: $(sudo iptables-save -t "$TABLE" | grep -F -- "--comment $TAG" \
| sed -nE 's/.* -s ([0-9.]+)(\/32)? .*/\1/p' | tr '\n' ' ')"
counters | awk -F'\t' '$2>0 {printf " %-8s %s attempt(s)\n", $1, $2}'
counters | awk -F'\t' '$2>0' | grep -q . || echo " (no connection attempts yet)"
}
cmd_mark() {
chain_exists || die "not observing"
counters > "$STATE" || die "could not write $STATE"
echo "baseline recorded ($(wc -l <"$STATE") counters)"
}
cmd_delta() {
chain_exists || die "not observing"
[[ -f "$STATE" ]] || die "no baseline — run 'mark' first"
join -t$'\t' -a2 -e 0 -o '0,1.2,2.2' <(sort "$STATE") <(counters | sort) \
| awk -F'\t' '{ d=$3-$2; if (d>0) printf " %-8s %s attempt(s)\n", $1, d }' \
| sort -k2 -rn
echo " ---"
join -t$'\t' -a2 -e 0 -o '0,1.2,2.2' <(sort "$STATE") <(counters | sort) \
| awk -F'\t' '{ if ($3-$2 > 0) n++ } END { print " ports contacted since mark: " n+0 }'
}
case "${1:-}" in
on) shift; cmd_on "$@" ;;
off) cmd_off ;;
status) cmd_status ;;
mark) cmd_mark ;;
delta) cmd_delta ;;
*) sed -n '2,9p' "$0" | sed 's/^# \?//'; exit 2 ;;
esac
+10
View File
@@ -21,6 +21,16 @@ cd "$(dirname "$(readlink -f "$0")")/.."
STAMPED="${1:-}"
[[ -n "$STAMPED" ]] || { echo "usage: verify-build-identity.sh <binary-commit>" >&2; exit 2; }
# Reject anything that is not a commit BEFORE comparing. Passing the binary
# path instead of its stamp otherwise produces a perfectly plausible-looking
# REFUSING line, which reads as a real finding rather than a caller mistake —
# and a safeguard that cries wolf is one people learn to work around.
if [[ ! "$STAMPED" =~ ^[0-9a-f]{7,40}$ ]]; then
echo "usage error: expected a commit hash, got '$STAMPED'" >&2
echo " (to check a binary: verify-build-identity.sh \"\$(BIN --identity | sed -nE 's/.*commit=([0-9a-f]+).*/\\1/p')\")" >&2
exit 2
fi
CRATES=(openfut-protocol-blaze openfut-adapter-fifa17 openfut-host-config
openfut-blaze-host openfut-redirector-host)