diff --git a/openfut-blaze-host/openfut-observe.sh b/openfut-blaze-host/openfut-observe.sh new file mode 100755 index 0000000..07af41f --- /dev/null +++ b/openfut-blaze-host/openfut-observe.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# Passive connection-attempt observer for one client address. +# +# openfut-observe.sh on +# 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 " + 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" +} + +# "portpackets", 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 diff --git a/scripts/verify-build-identity.sh b/scripts/verify-build-identity.sh index 957ea56..6699568 100755 --- a/scripts/verify-build-identity.sh +++ b/scripts/verify-build-identity.sh @@ -21,6 +21,16 @@ cd "$(dirname "$(readlink -f "$0")")/.." STAMPED="${1:-}" [[ -n "$STAMPED" ]] || { echo "usage: verify-build-identity.sh " >&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)