c65e9c54ce
It is a TOTAL -- every packet reaching the chain counts there, including ones already counted by a named-port rule. The old wording invited reading the number as a remainder, which is how 15 unexplained attempts got misread earlier.
143 lines
5.8 KiB
Bash
Executable File
143 lines
5.8 KiB
Bash
Executable File
#!/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". The final catch-all is reported as TOTAL, not "other":
|
|
# every packet reaching the chain counts there, including ones already counted
|
|
# by a named-port rule above it.
|
|
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
|