#!/bin/sh # # Log WAN vs LAN ping quality on the Debian edge router. # Use to compare "rocketship" evenings vs slow periods (ISP/upstream vs local). # # Topology (see docs/INFRA.md): # world -> 134.17.26.161 (router 10.7.6.228) -> 10.7.0.10 (FE) -> 10.7.0.0/8 (BE, …) # # Install on router: # sudo cp wan-snapshot.sh /usr/local/bin/wan-snapshot.sh # sudo chmod +x /usr/local/bin/wan-snapshot.sh # sudo mkdir -p /var/log # # Cron (every 15 minutes): # */15 * * * * root /usr/local/bin/wan-snapshot.sh # # Env overrides: # WAN_SNAPSHOT_LOG=/var/log/wan-quality.log # WAN_PING_TARGET=1.1.1.1 # FE_PING_TARGET=10.7.0.10 # BE_PING_TARGET=10.7.16.128 (set to "none" to skip) # PING_COUNT=10 # PING_DEADLINE_S=15 # set -eu WAN_SNAPSHOT_LOG="${WAN_SNAPSHOT_LOG:-/var/log/wan-quality.log}" WAN_PING_TARGET="${WAN_PING_TARGET:-1.1.1.1}" FE_PING_TARGET="${FE_PING_TARGET:-10.7.0.10}" BE_PING_TARGET="${BE_PING_TARGET-10.7.16.128}" PING_COUNT="${PING_COUNT:-10}" PING_DEADLINE_S="${PING_DEADLINE_S:-15}" usage() { sed -n '2,22p' "$0" printf '\nCommands:\n' printf ' (default) append one snapshot line to the log\n' printf ' --report summarize today vs yesterday averages\n' printf ' --tail [N] print last N log lines (default 24)\n' printf ' --help this text\n' } need_ping() { command -v ping >/dev/null 2>&1 || { echo "wan-snapshot: missing ping" >&2 exit 1 } } # ping_summary HOST -> prints: loss_pct avg_ms (or loss_pct=-1 avg_ms=-1 on total failure) ping_summary() { _host="$1" _out="" _out="$(ping -c "$PING_COUNT" -w "$PING_DEADLINE_S" "$_host" 2>/dev/null)" || true if [ -z "$_out" ]; then printf '%s %s' "-1" "-1" return 0 fi _loss="$(printf '%s\n' "$_out" | awk '/packet loss/ { for (i = 1; i <= NF; i++) if ($i ~ /%$/) { gsub(/%/, "", $i); print $i; exit } }')" _avg="$(printf '%s\n' "$_out" | awk -F'[=/ ]' '/min\/avg\/max/ { print $8; exit }')" [ -n "$_loss" ] || _loss="-1" [ -n "$_avg" ] || _avg="-1" printf '%s %s' "$_loss" "$_avg" } snapshot_once() { need_ping _ts="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')" _wan="$(ping_summary "$WAN_PING_TARGET")" _wan_loss="${_wan%% *}" _wan_avg="${_wan#* }" _fe="$(ping_summary "$FE_PING_TARGET")" _fe_loss="${_fe%% *}" _fe_avg="${_fe#* }" _line="ts=$_ts wan=$WAN_PING_TARGET wan_loss=${_wan_loss}% wan_avg_ms=${_wan_avg} fe=$FE_PING_TARGET fe_loss=${_fe_loss}% fe_avg_ms=${_fe_avg}" if [ -n "$BE_PING_TARGET" ] && [ "$BE_PING_TARGET" != "none" ]; then _be="$(ping_summary "$BE_PING_TARGET")" _be_loss="${_be%% *}" _be_avg="${_be#* }" _line="$_line be=$BE_PING_TARGET be_loss=${_be_loss}% be_avg_ms=${_be_avg}" fi _dir="$(dirname "$WAN_SNAPSHOT_LOG")" [ -d "$_dir" ] || mkdir -p "$_dir" 2>/dev/null || true printf '%s\n' "$_line" >> "$WAN_SNAPSHOT_LOG" printf '%s\n' "$_line" } # Average wan_avg_ms / fe_avg_ms for lines matching DATE prefix (YYYY-MM-DD). day_avg() { _log="$1" _date="$2" _field="$3" [ -r "$_log" ] || return 0 awk -v d="$_date" -v f="$_field" ' $0 ~ "^ts=" d { for (i = 1; i <= NF; i++) { if ($i ~ "^" f "=") { split($i, a, "=") v = a[2] gsub(/%/, "", v) if (v + 0 == v && v >= 0) { n++; s += v } } } } END { if (n > 0) printf "%.2f", s / n else print "n/a" } ' "$_log" } day_max_loss() { _log="$1" _date="$2" _field="$3" [ -r "$_log" ] || return 0 awk -v d="$_date" -v f="$_field" ' $0 ~ "^ts=" d { for (i = 1; i <= NF; i++) { if ($i ~ "^" f "=") { split($i, a, "=") v = a[2] gsub(/%/, "", v) if (v + 0 == v && v >= 0 && v > max) max = v } } } END { if (max == "") print "n/a" else printf "%.0f", max } ' "$_log" } report() { _log="$WAN_SNAPSHOT_LOG" _today="$(date '+%Y-%m-%d')" _yesterday="$(date -d 'yesterday' '+%Y-%m-%d' 2>/dev/null || date -v-1d '+%Y-%m-%d' 2>/dev/null || echo '')" if [ ! -r "$_log" ]; then echo "wan-snapshot: no log at $_log (run default command first)" >&2 exit 1 fi _samples_today="$(grep -c "^ts=$_today" "$_log" 2>/dev/null || echo 0)" printf 'log=%s\n' "$_log" printf 'today=%s samples=%s\n' "$_today" "$_samples_today" printf ' wan_avg_ms=%s wan_max_loss%%=%s\n' \ "$(day_avg "$_log" "$_today" "wan_avg_ms")" \ "$(day_max_loss "$_log" "$_today" "wan_loss")" printf ' fe_avg_ms=%s fe_max_loss%%=%s\n' \ "$(day_avg "$_log" "$_today" "fe_avg_ms")" \ "$(day_max_loss "$_log" "$_today" "fe_loss")" if [ -n "$_yesterday" ] && grep -q "^ts=$_yesterday" "$_log" 2>/dev/null; then _samples_y="$(grep -c "^ts=$_yesterday" "$_log" 2>/dev/null || echo 0)" printf 'yesterday=%s samples=%s\n' "$_yesterday" "$_samples_y" printf ' wan_avg_ms=%s wan_max_loss%%=%s\n' \ "$(day_avg "$_log" "$_yesterday" "wan_avg_ms")" \ "$(day_max_loss "$_log" "$_yesterday" "wan_loss")" printf ' fe_avg_ms=%s fe_max_loss%%=%s\n' \ "$(day_avg "$_log" "$_yesterday" "fe_avg_ms")" \ "$(day_max_loss "$_log" "$_yesterday" "fe_loss")" fi printf '\nInterpretation:\n' printf ' wan bad + fe good -> likely ISP/upstream (world -> 134.17.26.161)\n' printf ' wan + fe bad -> router or LAN (10.7.6.228 -> 10.7.0.10)\n' printf ' wan + fe good, apps slow -> DNAT/nginx above ping path\n' } tail_log() { _n="${1:-24}" [ -r "$WAN_SNAPSHOT_LOG" ] || { echo "wan-snapshot: no log at $WAN_SNAPSHOT_LOG" >&2 exit 1 } tail -n "$_n" "$WAN_SNAPSHOT_LOG" } case "${1:-}" in -h|--help|help) usage ;; --report|report) report ;; --tail|tail) tail_log "${2:-24}" ;; "") snapshot_once ;; *) echo "wan-snapshot: unknown command: $1 (try --help)" >&2 exit 1 ;; esac