677 lines
23 KiB
Bash
677 lines
23 KiB
Bash
#!/bin/sh
|
|
# connstats.sh
|
|
# Log-derived Wi-Fi connection counters per run (intended for cron every 10 minutes).
|
|
# Writes one summary line per run to /root/connstats.log
|
|
# Optional: --debug for chatty troubleshooting output in the same log.
|
|
# Optional: --always-find-offset to calibrate syslog header time via a logger marker
|
|
# and (on fresh start / rotation / truncation) build a true last-10-min slice.
|
|
|
|
LOG_SRC="/var/log/messages"
|
|
OUT_LOG="/root/connstats.log"
|
|
STATE="/tmp/connstats.state"
|
|
TMPDIR="/tmp"
|
|
|
|
# How many tail lines to scan when we have no state (first run / after reboot / rotation)
|
|
BOOTSTRAP_BACKFILL_LINES=2000
|
|
|
|
# Used only with --always-find-offset during bootstrap-like runs
|
|
BOOTSTRAP_TIME_WINDOW_SEC=600
|
|
BOOTSTRAP_TIME_TAIL_LINES=10000
|
|
|
|
DEBUG=0
|
|
ALWAYS_FIND_OFFSET=0
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--debug) DEBUG=1 ;;
|
|
--always-find-offset) ALWAYS_FIND_OFFSET=1 ;;
|
|
-h|--help)
|
|
echo "Usage: $0 [--debug] [--always-find-offset]"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown arg: $arg" >&2
|
|
echo "Usage: $0 [--debug] [--always-find-offset]" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
umask 077
|
|
|
|
ts_iso() {
|
|
# ISO-ish timestamp; works on BusyBox and GNU date
|
|
date "+%Y-%m-%dT%H:%M:%S%z"
|
|
}
|
|
|
|
log() {
|
|
# Always append to OUT_LOG
|
|
echo "[$(ts_iso)] $*" >> "$OUT_LOG"
|
|
}
|
|
|
|
# Generate a short random token (8 chars) for marker grep
|
|
rand_token() {
|
|
if [ -r /dev/urandom ]; then
|
|
tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 8
|
|
else
|
|
echo "$(date -u +%s 2>/dev/null)$$" | tr -dc 'a-z0-9' | tail -c 8
|
|
fi
|
|
}
|
|
|
|
# Parse syslog header timestamp (Mon DD HH:MM:SS) into epoch seconds (UTC) using awk mktime()
|
|
# Expects typical line like: "<13> Jan 31 12:31:00 root[-] [notice]: message"
|
|
syslog_header_to_epoch_utc() {
|
|
echo "$1" | TZ=UTC awk '
|
|
function mon2num(m) {
|
|
if (m=="Jan") return 1
|
|
if (m=="Feb") return 2
|
|
if (m=="Mar") return 3
|
|
if (m=="Apr") return 4
|
|
if (m=="May") return 5
|
|
if (m=="Jun") return 6
|
|
if (m=="Jul") return 7
|
|
if (m=="Aug") return 8
|
|
if (m=="Sep") return 9
|
|
if (m=="Oct") return 10
|
|
if (m=="Nov") return 11
|
|
if (m=="Dec") return 12
|
|
return 0
|
|
}
|
|
{
|
|
mon=$2; day=$3; tod=$4
|
|
year=strftime("%Y")
|
|
m=mon2num(mon)
|
|
if (m==0) { print ""; exit 1 }
|
|
split(tod, t, ":")
|
|
if (length(t) != 3) { print ""; exit 1 }
|
|
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
|
|
d=day+0
|
|
print mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, d, hh, mm, ss))
|
|
}
|
|
'
|
|
}
|
|
|
|
# Compute offset_sec = payload_epoch_utc - syslog_header_epoch_utc
|
|
# Returns offset seconds on stdout (blank on failure)
|
|
find_time_offset_sec() {
|
|
token="$(rand_token)"
|
|
now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')"
|
|
[ -z "$now_epoch" ] && now_epoch=0
|
|
|
|
logger "connstat calculation started. issueing time marker $token epoch=$now_epoch" 2>/dev/null
|
|
|
|
found_line=""
|
|
i=0
|
|
while [ $i -lt 3 ]; do
|
|
found_line="$(tail -n 300 "$LOG_SRC" 2>/dev/null | grep "$token" | tail -n 1)"
|
|
[ -n "$found_line" ] && break
|
|
sleep 1
|
|
i=$((i + 1))
|
|
done
|
|
|
|
[ -z "$found_line" ] && { echo ""; return; }
|
|
|
|
hdr_epoch="$(syslog_header_to_epoch_utc "$found_line" | tr -d ' ')"
|
|
[ -z "$hdr_epoch" ] && { echo ""; return; }
|
|
|
|
payload_epoch="$(echo "$found_line" | awk '{
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i ~ /^epoch=[0-9]+$/) { sub(/^epoch=/,"",$i); print $i; exit }
|
|
}
|
|
}' | tr -d ' ')"
|
|
[ -z "$payload_epoch" ] && payload_epoch="$now_epoch"
|
|
|
|
echo $((payload_epoch - hdr_epoch))
|
|
}
|
|
|
|
# Basic sanity checks
|
|
if [ ! -r "$LOG_SRC" ]; then
|
|
log "ERROR log_src_unreadable path=$LOG_SRC"
|
|
exit 1
|
|
fi
|
|
|
|
# Get current inode + line count (line-based incremental read is most portable)
|
|
cur_inode="$(ls -i "$LOG_SRC" 2>/dev/null | awk '{print $1}')"
|
|
cur_lines="$(wc -l < "$LOG_SRC" 2>/dev/null | tr -d ' ')"
|
|
|
|
if [ -z "$cur_inode" ] || [ -z "$cur_lines" ]; then
|
|
log "ERROR cannot_stat_log inode='$cur_inode' lines='$cur_lines'"
|
|
exit 1
|
|
fi
|
|
|
|
last_inode=""
|
|
last_line=""
|
|
|
|
if [ -f "$STATE" ]; then
|
|
# STATE format:
|
|
# inode=<num>
|
|
# line=<num>
|
|
last_inode="$(grep '^inode=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)"
|
|
last_line="$(grep '^line=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)"
|
|
fi
|
|
|
|
# Decide where to start reading
|
|
start_line=""
|
|
|
|
backfill_start() {
|
|
start_line=$((cur_lines - BOOTSTRAP_BACKFILL_LINES + 1))
|
|
[ "$start_line" -lt 1 ] && start_line=1
|
|
}
|
|
|
|
# Extract the slice
|
|
tmp_slice="$TMPDIR/connstats.slice.$$"
|
|
|
|
# Grep patterns (hostapd transition events)
|
|
re_auth='hostapd.*IEEE 802\.11: authenticated'
|
|
re_assoc='hostapd.*IEEE 802\.11: associated'
|
|
re_disassoc='hostapd.*IEEE 802\.11: disassociated'
|
|
|
|
# Kernel extras for "seen_any" breadth (optional but useful)
|
|
re_kernel_seen='kernel.*(station kicked out|station timed out)'
|
|
|
|
# Are we in a bootstrap-like condition (no state OR rotation/truncation)?
|
|
bootstrap_like=0
|
|
if [ -z "$last_inode" ] || [ -z "$last_line" ]; then
|
|
bootstrap_like=1
|
|
else
|
|
if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then
|
|
bootstrap_like=1
|
|
fi
|
|
fi
|
|
|
|
# Bootstrap handling
|
|
if [ "$ALWAYS_FIND_OFFSET" -eq 1 ] && [ "$bootstrap_like" -eq 1 ]; then
|
|
offset_sec="$(find_time_offset_sec)"
|
|
|
|
if [ -n "$offset_sec" ]; then
|
|
now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')"
|
|
[ -z "$now_epoch" ] && now_epoch=0
|
|
cutoff_epoch=$((now_epoch - BOOTSTRAP_TIME_WINDOW_SEC))
|
|
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG bootstrap_time_window_last_10m offset_sec=$offset_sec cutoff_epoch=$cutoff_epoch tail_lines=$BOOTSTRAP_TIME_TAIL_LINES"
|
|
fi
|
|
|
|
# Build slice = last-10-min relevant lines (hostapd transitions + kernel station kicked/timed out)
|
|
# using (syslog_header_epoch + offset_sec) >= cutoff_epoch
|
|
tail -n "$BOOTSTRAP_TIME_TAIL_LINES" "$LOG_SRC" 2>/dev/null \
|
|
| TZ=UTC awk -v cutoff="$cutoff_epoch" -v off="$offset_sec" '
|
|
function mon2num(m) {
|
|
if (m=="Jan") return 1
|
|
if (m=="Feb") return 2
|
|
if (m=="Mar") return 3
|
|
if (m=="Apr") return 4
|
|
if (m=="May") return 5
|
|
if (m=="Jun") return 6
|
|
if (m=="Jul") return 7
|
|
if (m=="Aug") return 8
|
|
if (m=="Sep") return 9
|
|
if (m=="Oct") return 10
|
|
if (m=="Nov") return 11
|
|
if (m=="Dec") return 12
|
|
return 0
|
|
}
|
|
function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) {
|
|
mon=$2; day=$3; tod=$4
|
|
year=strftime("%Y")
|
|
m=mon2num(mon)
|
|
if (m==0) return -1
|
|
split(tod, t, ":")
|
|
if (length(t) != 3) return -1
|
|
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
|
|
return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss))
|
|
}
|
|
{
|
|
line=$0
|
|
if (line ~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/ ||
|
|
line ~ /kernel.*(station kicked out|station timed out)/) {
|
|
he=header_epoch()
|
|
if (he < 0) next
|
|
corr=he + off
|
|
if (corr >= cutoff) print line
|
|
}
|
|
}
|
|
' > "$tmp_slice" 2>/dev/null
|
|
|
|
start_line="(time_window_last_10m)"
|
|
else
|
|
# Offset calc failed -> fall back to old bootstrap behavior
|
|
backfill_start
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG time_offset_failed_fallback_to_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
|
|
fi
|
|
sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null
|
|
fi
|
|
|
|
else
|
|
# Original behavior (unchanged)
|
|
if [ -z "$last_inode" ] || [ -z "$last_line" ]; then
|
|
backfill_start
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG bootstrap_no_state_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
|
|
fi
|
|
else
|
|
# Rotation / truncation detection
|
|
if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then
|
|
backfill_start
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG bootstrap_rotation_or_truncate_backfill last_inode=$last_inode cur_inode=$cur_inode last_line=$last_line cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
|
|
fi
|
|
else
|
|
start_line=$((last_line + 1))
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG incremental last_line=$last_line cur_lines=$cur_lines start_line=$start_line"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null
|
|
fi
|
|
|
|
slice_lines="$(wc -l < "$tmp_slice" 2>/dev/null | tr -d ' ')"
|
|
[ -z "$slice_lines" ] && slice_lines=0
|
|
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG slice_path=$tmp_slice slice_lines=$slice_lines"
|
|
fi
|
|
|
|
# Totals (MAC repeats allowed)
|
|
auth_events_total="$(grep -E "$re_auth" "$tmp_slice" | wc -l | tr -d ' ')"
|
|
assoc_events_total="$(grep -E "$re_assoc" "$tmp_slice" | wc -l | tr -d ' ')"
|
|
disassoc_events_total="$(grep -E "$re_disassoc" "$tmp_slice" | wc -l | tr -d ' ')"
|
|
|
|
# Extract MAC right after token "STA" (from hostapd lines)
|
|
extract_sta_macs() {
|
|
awk '
|
|
{
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i=="STA") {
|
|
print $(i+1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
# Extract MACs in brackets: [aa:bb:cc:dd:ee:ff]
|
|
extract_bracket_macs() {
|
|
grep -oE '\[[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}\]' | tr -d '[]'
|
|
}
|
|
|
|
# Count unique valid MACs from stdin
|
|
uniq_count() {
|
|
grep -E '^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$' \
|
|
| sort -u \
|
|
| wc -l | tr -d ' '
|
|
}
|
|
|
|
# Unique MACs per event type (hostapd)
|
|
unique_macs_authenticated="$(
|
|
grep -E "$re_auth" "$tmp_slice" \
|
|
| extract_sta_macs \
|
|
| uniq_count
|
|
)"
|
|
|
|
unique_macs_associated="$(
|
|
grep -E "$re_assoc" "$tmp_slice" \
|
|
| extract_sta_macs \
|
|
| uniq_count
|
|
)"
|
|
|
|
unique_macs_disassociated="$(
|
|
grep -E "$re_disassoc" "$tmp_slice" \
|
|
| extract_sta_macs \
|
|
| uniq_count
|
|
)"
|
|
|
|
# Unique MACs seen in "any relevant activity" (union: auth/assoc/disassoc + selected kernel)
|
|
unique_macs_seen_any="$(
|
|
(
|
|
grep -E 'hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)' "$tmp_slice" \
|
|
| extract_sta_macs
|
|
|
|
grep -E "$re_kernel_seen" "$tmp_slice" \
|
|
| extract_bracket_macs
|
|
) | uniq_count
|
|
)"
|
|
|
|
########################################################################
|
|
# B-class metrics (ephemeral state in /tmp; reboot loss accepted)
|
|
########################################################################
|
|
|
|
SESSION_STATE="/tmp/connstats.sessions.state"
|
|
SESSION_STATE_TMP="$TMPDIR/connstats.sessions.state.$$"
|
|
SESSION_STATE_TTL_SEC=21600
|
|
AUTH_ASSOC_MAX_SAMPLES=50
|
|
|
|
now_epoch_b="$(date -u +%s 2>/dev/null | tr -d ' ')"
|
|
[ -z "$now_epoch_b" ] && now_epoch_b=0
|
|
|
|
# Output file (temporary, removed each run)
|
|
B_OUT_TMP="$TMPDIR/connstats.b.out.$$"
|
|
|
|
TZ=UTC awk -v state_in="$SESSION_STATE" \
|
|
-v state_out="$SESSION_STATE_TMP" \
|
|
-v now="$now_epoch_b" \
|
|
-v ttl="$SESSION_STATE_TTL_SEC" \
|
|
-v maxs="$AUTH_ASSOC_MAX_SAMPLES" '
|
|
function mon2num(m) {
|
|
if (m=="Jan") return 1
|
|
if (m=="Feb") return 2
|
|
if (m=="Mar") return 3
|
|
if (m=="Apr") return 4
|
|
if (m=="May") return 5
|
|
if (m=="Jun") return 6
|
|
if (m=="Jul") return 7
|
|
if (m=="Aug") return 8
|
|
if (m=="Sep") return 9
|
|
if (m=="Oct") return 10
|
|
if (m=="Nov") return 11
|
|
if (m=="Dec") return 12
|
|
return 0
|
|
}
|
|
function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) {
|
|
mon=$2; day=$3; tod=$4
|
|
year=strftime("%Y")
|
|
m=mon2num(mon)
|
|
if (m==0) return -1
|
|
split(tod, t, ":")
|
|
if (length(t) != 3) return -1
|
|
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
|
|
return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss))
|
|
}
|
|
function extract_sta_mac( i) {
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i=="STA") return $(i+1)
|
|
}
|
|
return ""
|
|
}
|
|
function mac_valid(m) {
|
|
return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/)
|
|
}
|
|
|
|
BEGIN {
|
|
if (state_in != "") {
|
|
while ((getline line < state_in) > 0) {
|
|
n = split(line, a, " ")
|
|
if (n >= 2) {
|
|
m=a[1]; e=a[2]+0
|
|
if (mac_valid(m) && e > 0) last_auth[m]=e
|
|
}
|
|
}
|
|
close(state_in)
|
|
}
|
|
}
|
|
|
|
{
|
|
line=$0
|
|
if (line !~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/) next
|
|
|
|
e = header_epoch()
|
|
if (e < 0) next
|
|
|
|
mac = extract_sta_mac()
|
|
if (!mac_valid(mac)) next
|
|
|
|
if (line ~ /IEEE 802\.11: authenticated/) {
|
|
last_auth[mac]=e
|
|
next
|
|
}
|
|
|
|
if (line ~ /IEEE 802\.11: associated/) {
|
|
if (mac in last_auth) {
|
|
d = e - last_auth[mac]
|
|
if (d >= 0) {
|
|
ms = d * 1000
|
|
if (sample_count < maxs) {
|
|
if (samples == "") samples = ms
|
|
else samples = samples "," ms
|
|
sample_count++
|
|
}
|
|
}
|
|
}
|
|
next
|
|
}
|
|
|
|
if (line ~ /IEEE 802\.11: disassociated/) {
|
|
if (mac in last_auth) {
|
|
d = e - last_auth[mac]
|
|
if (d >= 0) {
|
|
full_sessions_total++
|
|
sum_len += d
|
|
if (d < 60) early_drop_sessions_total++
|
|
delete last_auth[mac]
|
|
}
|
|
}
|
|
next
|
|
}
|
|
}
|
|
|
|
END {
|
|
if (full_sessions_total > 0) avg_session_length = int((sum_len / full_sessions_total) + 0.5)
|
|
else avg_session_length = 0
|
|
|
|
if (state_out != "") {
|
|
for (m in last_auth) {
|
|
e = last_auth[m] + 0
|
|
if (e > 0 && (now <= 0 || (e >= (now - ttl)))) {
|
|
print m " " e > state_out
|
|
}
|
|
}
|
|
close(state_out)
|
|
}
|
|
|
|
print full_sessions_total "\t" avg_session_length "\t" early_drop_sessions_total "\t" samples
|
|
}
|
|
' "$tmp_slice" > "$B_OUT_TMP" 2>/dev/null
|
|
|
|
# Default B fields
|
|
full_sessions_total=0
|
|
avg_session_length=0
|
|
early_drop_sessions_total=0
|
|
auth_to_assoc_time_ms_samples=""
|
|
|
|
# Parse awk output
|
|
if [ -s "$B_OUT_TMP" ]; then
|
|
b_out_line="$(head -n 1 "$B_OUT_TMP" 2>/dev/null)"
|
|
full_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')"
|
|
avg_session_length="$(echo "$b_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')"
|
|
early_drop_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')"
|
|
auth_to_assoc_time_ms_samples="$(echo "$b_out_line" | awk -F'\t' '{print $4}')"
|
|
fi
|
|
|
|
rm -f "$B_OUT_TMP" 2>/dev/null
|
|
|
|
[ -z "$full_sessions_total" ] && full_sessions_total=0
|
|
[ -z "$avg_session_length" ] && avg_session_length=0
|
|
[ -z "$early_drop_sessions_total" ] && early_drop_sessions_total=0
|
|
|
|
# Move state into place (ephemeral but persistent across runs until reboot)
|
|
if [ -f "$SESSION_STATE_TMP" ]; then
|
|
mv -f "$SESSION_STATE_TMP" "$SESSION_STATE" 2>/dev/null
|
|
else
|
|
rm -f "$SESSION_STATE_TMP" 2>/dev/null
|
|
fi
|
|
|
|
########################################################################
|
|
# C-class metrics (kernel kick reason codes 1..8)
|
|
########################################################################
|
|
|
|
# Only count kernel "station kicked out ..." (avoids PRS DRIVER_LOG "reason" lines)
|
|
re_kick_reason_base='kernel.*station kicked out.*reason[[:space:]]+'
|
|
|
|
reason_code_events_1="$(grep -E "${re_kick_reason_base}1([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_2="$(grep -E "${re_kick_reason_base}2([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_3="$(grep -E "${re_kick_reason_base}3([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_4="$(grep -E "${re_kick_reason_base}4([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_5="$(grep -E "${re_kick_reason_base}5([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_6="$(grep -E "${re_kick_reason_base}6([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_7="$(grep -E "${re_kick_reason_base}7([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
reason_code_events_8="$(grep -E "${re_kick_reason_base}8([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
|
|
|
|
[ -z "$reason_code_events_1" ] && reason_code_events_1=0
|
|
[ -z "$reason_code_events_2" ] && reason_code_events_2=0
|
|
[ -z "$reason_code_events_3" ] && reason_code_events_3=0
|
|
[ -z "$reason_code_events_4" ] && reason_code_events_4=0
|
|
[ -z "$reason_code_events_5" ] && reason_code_events_5=0
|
|
[ -z "$reason_code_events_6" ] && reason_code_events_6=0
|
|
[ -z "$reason_code_events_7" ] && reason_code_events_7=0
|
|
[ -z "$reason_code_events_8" ] && reason_code_events_8=0
|
|
|
|
########################################################################
|
|
# D-class metrics (flaps between ath0 and ath1 within the bucket)
|
|
########################################################################
|
|
|
|
D_OUT_TMP="$TMPDIR/connstats.d.out.$$"
|
|
|
|
awk '
|
|
function extract_sta_mac( i) {
|
|
for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1)
|
|
return ""
|
|
}
|
|
function mac_valid(m) {
|
|
return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/)
|
|
}
|
|
function extract_iface( i, v) {
|
|
for (i=1; i<=NF; i++) {
|
|
v = $i
|
|
if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v }
|
|
}
|
|
return ""
|
|
}
|
|
|
|
{
|
|
line=$0
|
|
if (line !~ /hostapd.*IEEE 802\.11: associated/) next
|
|
|
|
iface = extract_iface()
|
|
if (iface != "ath0" && iface != "ath1") next
|
|
|
|
mac = extract_sta_mac()
|
|
if (!mac_valid(mac)) next
|
|
|
|
if (mac in last_iface) {
|
|
prev = last_iface[mac]
|
|
if (prev != iface) {
|
|
if (prev=="ath0" && iface=="ath1") {
|
|
flap_events_0_to_1++
|
|
uniq_0_to_1[mac]=1
|
|
} else if (prev=="ath1" && iface=="ath0") {
|
|
flap_events_1_to_0++
|
|
uniq_1_to_0[mac]=1
|
|
}
|
|
}
|
|
}
|
|
|
|
last_iface[mac]=iface
|
|
}
|
|
|
|
END {
|
|
for (m in uniq_0_to_1) unique_0_to_1++
|
|
for (m in uniq_1_to_0) unique_1_to_0++
|
|
|
|
if (unique_0_to_1+0 < 0) unique_0_to_1=0
|
|
if (flap_events_0_to_1+0 < 0) flap_events_0_to_1=0
|
|
if (unique_1_to_0+0 < 0) unique_1_to_0=0
|
|
if (flap_events_1_to_0+0 < 0) flap_events_1_to_0=0
|
|
|
|
print unique_0_to_1 "\t" flap_events_0_to_1 "\t" unique_1_to_0 "\t" flap_events_1_to_0
|
|
}
|
|
' "$tmp_slice" > "$D_OUT_TMP" 2>/dev/null
|
|
|
|
unique_macs_flap_ath0_to_ath1=0
|
|
flap_events_total_ath0_to_ath1=0
|
|
unique_macs_flap_ath1_to_ath0=0
|
|
flap_events_total_ath1_to_ath0=0
|
|
|
|
if [ -s "$D_OUT_TMP" ]; then
|
|
d_out_line="$(head -n 1 "$D_OUT_TMP" 2>/dev/null)"
|
|
unique_macs_flap_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')"
|
|
flap_events_total_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')"
|
|
unique_macs_flap_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')"
|
|
flap_events_total_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $4}' | tr -d ' ')"
|
|
fi
|
|
|
|
rm -f "$D_OUT_TMP" 2>/dev/null
|
|
|
|
[ -z "$unique_macs_flap_ath0_to_ath1" ] && unique_macs_flap_ath0_to_ath1=0
|
|
[ -z "$flap_events_total_ath0_to_ath1" ] && flap_events_total_ath0_to_ath1=0
|
|
[ -z "$unique_macs_flap_ath1_to_ath0" ] && unique_macs_flap_ath1_to_ath0=0
|
|
[ -z "$flap_events_total_ath1_to_ath0" ] && flap_events_total_ath1_to_ath0=0
|
|
|
|
########################################################################
|
|
# Emit one summary line
|
|
########################################################################
|
|
log "connstats bucket_run slice_lines=$slice_lines unique_macs_seen_any=$unique_macs_seen_any unique_macs_authenticated=$unique_macs_authenticated auth_events_total=$auth_events_total unique_macs_associated=$unique_macs_associated assoc_events_total=$assoc_events_total unique_macs_disassociated=$unique_macs_disassociated disassoc_events_total=$disassoc_events_total full_sessions_total=$full_sessions_total avg_session_length=$avg_session_length early_drop_sessions_total=$early_drop_sessions_total auth_to_assoc_time_ms_samples=$auth_to_assoc_time_ms_samples reason_code_events_1=$reason_code_events_1 reason_code_events_2=$reason_code_events_2 reason_code_events_3=$reason_code_events_3 reason_code_events_4=$reason_code_events_4 reason_code_events_5=$reason_code_events_5 reason_code_events_6=$reason_code_events_6 reason_code_events_7=$reason_code_events_7 reason_code_events_8=$reason_code_events_8 unique_macs_flap_ath0_to_ath1=$unique_macs_flap_ath0_to_ath1 flap_events_total_ath0_to_ath1=$flap_events_total_ath0_to_ath1 unique_macs_flap_ath1_to_ath0=$unique_macs_flap_ath1_to_ath0 flap_events_total_ath1_to_ath0=$flap_events_total_ath1_to_ath0"
|
|
|
|
if [ $DEBUG -eq 1 ]; then
|
|
log "DEBUG dump_auth_lines_begin"
|
|
grep -E "$re_auth" "$tmp_slice" | sed 's/^/DEBUG AUTH: /' >> "$OUT_LOG"
|
|
log "DEBUG dump_assoc_lines_begin"
|
|
grep -E "$re_assoc" "$tmp_slice" | sed 's/^/DEBUG ASSOC: /' >> "$OUT_LOG"
|
|
log "DEBUG dump_disassoc_lines_begin"
|
|
grep -E "$re_disassoc" "$tmp_slice" | sed 's/^/DEBUG DISASSOC: /' >> "$OUT_LOG"
|
|
|
|
log "DEBUG dump_kick_reason_lines_begin"
|
|
grep -E "${re_kick_reason_base}[1-8]([^0-9]|$)" "$tmp_slice" | sed 's/^/DEBUG KICK: /' >> "$OUT_LOG"
|
|
|
|
log "DEBUG dump_flap_assoc_lines_begin"
|
|
grep -E 'hostapd.*IEEE 802\.11: associated' "$tmp_slice" | sed 's/^/DEBUG FLAP_ASSOC: /' >> "$OUT_LOG"
|
|
|
|
log "DEBUG dump_flap_events_begin"
|
|
awk '
|
|
function extract_sta_mac( i) { for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1); return "" }
|
|
function mac_valid(m) { return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/) }
|
|
function extract_iface( i, v) {
|
|
for (i=1; i<=NF; i++) { v=$i; if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v } }
|
|
return ""
|
|
}
|
|
{
|
|
if ($0 !~ /hostapd.*IEEE 802\.11: associated/) next
|
|
iface=extract_iface()
|
|
if (iface!="ath0" && iface!="ath1") next
|
|
mac=extract_sta_mac()
|
|
if (!mac_valid(mac)) next
|
|
if (mac in last_iface) {
|
|
prev=last_iface[mac]
|
|
if (prev!=iface) {
|
|
if ((prev=="ath0" && iface=="ath1") || (prev=="ath1" && iface=="ath0")) {
|
|
print "mac=" mac " from=" prev " to=" iface
|
|
}
|
|
}
|
|
}
|
|
last_iface[mac]=iface
|
|
}
|
|
' "$tmp_slice" | sed 's/^/DEBUG FLAP: /' >> "$OUT_LOG"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Send one syslog message (wifidebug-style tag), after random delay
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Hostname (as in wifidebug)
|
|
HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null)"
|
|
[ -z "$HOSTNAME" ] && HOSTNAME="$(hostname 2>/dev/null)"
|
|
[ -z "$HOSTNAME" ] && HOSTNAME="unknown"
|
|
|
|
# Random delay 3..20 seconds (right before sending)
|
|
delay="$(hexdump -n2 -e '/2 "%u"' /dev/urandom 2>/dev/null)"
|
|
case "$delay" in (''|*[!0-9]*) delay=0;; esac
|
|
delay=$(( (delay % 18) + 3 ))
|
|
sleep "$delay"
|
|
|
|
# Exact stats payload (same fields as file line, no extra prefixes)
|
|
MSG="slice_lines=$slice_lines unique_macs_seen_any=$unique_macs_seen_any unique_macs_authenticated=$unique_macs_authenticated auth_events_total=$auth_events_total unique_macs_associated=$unique_macs_associated assoc_events_total=$assoc_events_total unique_macs_disassociated=$unique_macs_disassociated disassoc_events_total=$disassoc_events_total full_sessions_total=$full_sessions_total avg_session_length=$avg_session_length early_drop_sessions_total=$early_drop_sessions_total auth_to_assoc_time_ms_samples=$auth_to_assoc_time_ms_samples reason_code_events_1=$reason_code_events_1 reason_code_events_2=$reason_code_events_2 reason_code_events_3=$reason_code_events_3 reason_code_events_4=$reason_code_events_4 reason_code_events_5=$reason_code_events_5 reason_code_events_6=$reason_code_events_6 reason_code_events_7=$reason_code_events_7 reason_code_events_8=$reason_code_events_8 unique_macs_flap_ath0_to_ath1=$unique_macs_flap_ath0_to_ath1 flap_events_total_ath0_to_ath1=$flap_events_total_ath0_to_ath1 unique_macs_flap_ath1_to_ath0=$unique_macs_flap_ath1_to_ath0 flap_events_total_ath1_to_ath0=$flap_events_total_ath1_to_ath0"
|
|
|
|
logger -t "connstats|${HOSTNAME}" "$MSG" 2>/dev/null
|
|
|
|
# Update state to current end-of-file line count and inode
|
|
{
|
|
echo "inode=$cur_inode"
|
|
echo "line=$cur_lines"
|
|
} > "$STATE"
|
|
|
|
rm -f "$tmp_slice" 2>/dev/null
|
|
exit 0
|