57 lines
2.6 KiB
Bash
Executable File
57 lines
2.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# cira-tunnel-watchdog: drop half-open Intel AMT phone-home (CIRA) tunnels on MeshCentral's MPS.
|
|
#
|
|
# Why (servers/pfi-tacticalrmm/README.md): when a host whose AMT shares the network chip power-cycles, AMT opens
|
|
# a new CIRA tunnel without closing the old one. MeshCentral 1.2.0 keeps both. Its web relay (KVM "HW Connect",
|
|
# SOL, IDE-R, MeshCommander) can pick the dead one and hang at "Setup..." forever. Its own 90 s idle timeout
|
|
# does not fire, because its writes to the dead socket keep resetting it. Seen 3 of 3 power events, at two sites
|
|
# (2026-10-02/03).
|
|
#
|
|
# What: every run, any ESTABLISHED socket on the MPS port that has received nothing for MAX_SILENT_MS is
|
|
# destroyed with `ss -K`. MeshCentral then drops it, and the live tunnel serves. Healthy tunnels hear from their
|
|
# AMT every ~6-30 s (measured), so the 180 s default leaves a wide margin. A tunnel dropped by mistake comes
|
|
# back on its own: the AMT policy reconnects every 10 s.
|
|
#
|
|
# cira-tunnel-watchdog act (root: ss -K needs CAP_NET_ADMIN)
|
|
# cira-tunnel-watchdog --dry-run report what would be dropped
|
|
# cira-tunnel-watchdog --dry-run --from-file F decide from a saved `ss -tnio` capture (tests)
|
|
set -euo pipefail
|
|
PORT="${CIRA_PORT:-4433}"
|
|
MAX_SILENT_MS="${CIRA_MAX_SILENT_MS:-180000}"
|
|
DRY=0
|
|
SRC=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dry-run) DRY=1 ;;
|
|
--from-file) SRC="${2:?--from-file needs a path}"; shift ;;
|
|
*) echo "usage: $0 [--dry-run] [--from-file <ss capture>]" >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
if [ -n "$SRC" ]; then
|
|
[ "$DRY" = 1 ] || { echo "--from-file is a test mode and needs --dry-run" >&2; exit 2; }
|
|
out="$(cat "$SRC")"
|
|
else
|
|
out="$(ss -tnio state established "( sport = :$PORT )")"
|
|
fi
|
|
# `ss -tnio state established` prints a socket line (Recv-Q Send-Q Local Peer) followed by an indented info line.
|
|
stale="$(printf '%s\n' "$out" | awk -v max="$MAX_SILENT_MS" '
|
|
/^[0-9]/ { peer = $4; next }
|
|
/lastrcv:/ {
|
|
lr = -1
|
|
for (i = 1; i <= NF; i++) if ($i ~ /^lastrcv:/) { split($i, a, ":"); lr = a[2] + 0 }
|
|
if (peer != "" && lr > max) print peer, lr
|
|
peer = ""
|
|
}')"
|
|
[ -z "$stale" ] && exit 0
|
|
while read -r peer lr; do
|
|
[[ "$peer" =~ ^\[[0-9a-fA-F:.]+\]:[0-9]+$ ]] || { echo "skip unparsable peer [$peer]" >&2; continue; }
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "would drop $peer (silent ${lr} ms > ${MAX_SILENT_MS})"
|
|
else
|
|
ss -K -tn "( sport = :$PORT ) and dst $peer" >/dev/null
|
|
logger -t cira-tunnel-watchdog "dropped stale CIRA tunnel $peer (silent ${lr} ms > ${MAX_SILENT_MS})"
|
|
echo "dropped $peer (silent ${lr} ms)"
|
|
fi
|
|
done <<<"$stale"
|