Prevent odhcpd crashes from MAC randomization causing hostname conflicts, stale lease pile-up, and lease flooding. Adds hostname dedup, stale lease cleanup, flood detection, CLI commands, RPC methods, and LuCI dashboard card. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
7.3 KiB
Bash
255 lines
7.3 KiB
Bash
#!/bin/sh
|
|
# TAP test suite for mac-guardian detection logic
|
|
# Run: sh tests/test_detection.sh
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
TEST_TMPDIR=$(mktemp -d)
|
|
trap 'rm -rf "$TEST_TMPDIR"' EXIT
|
|
|
|
# Override paths for testing
|
|
MG_RUNDIR="$TEST_TMPDIR/run"
|
|
MG_DBFILE="$MG_RUNDIR/known.db"
|
|
MG_LOCKDIR="$MG_RUNDIR/lock"
|
|
MG_LOGFILE="$TEST_TMPDIR/mac-guardian.log"
|
|
MG_EVENTS_LOG="$TEST_TMPDIR/mac-guardian.log"
|
|
MG_OUI_FILE="$SCRIPT_DIR/files/usr/lib/secubox/mac-guardian/oui.tsv"
|
|
MG_STATS_FILE="$TEST_TMPDIR/stats.json"
|
|
MG_MAX_LOG_SIZE=524288
|
|
|
|
mkdir -p "$MG_RUNDIR"
|
|
touch "$MG_DBFILE"
|
|
touch "$MG_EVENTS_LOG"
|
|
|
|
# Stub logger and UCI
|
|
logger() { :; }
|
|
config_load() { :; }
|
|
config_get() {
|
|
local var="$1" section="$2" option="$3" default="$4"
|
|
eval "$var=\"\${$var:-$default}\""
|
|
}
|
|
config_list_foreach() { :; }
|
|
|
|
# Source functions
|
|
. "$SCRIPT_DIR/files/usr/lib/secubox/mac-guardian/functions.sh"
|
|
|
|
# Re-apply path overrides (sourcing functions.sh resets them)
|
|
MG_RUNDIR="$TEST_TMPDIR/run"
|
|
MG_DBFILE="$MG_RUNDIR/known.db"
|
|
MG_LOCKDIR="$MG_RUNDIR/lock"
|
|
MG_LOGFILE="$TEST_TMPDIR/mac-guardian.log"
|
|
MG_EVENTS_LOG="$TEST_TMPDIR/mac-guardian.log"
|
|
MG_OUI_FILE="$SCRIPT_DIR/files/usr/lib/secubox/mac-guardian/oui.tsv"
|
|
MG_STATS_FILE="$TEST_TMPDIR/stats.json"
|
|
MG_MAX_LOG_SIZE=524288
|
|
|
|
# Reset config
|
|
MG_ENABLED=1
|
|
MG_DEBUG=0
|
|
MG_DETECT_RANDOM=1
|
|
MG_DETECT_OUI_DUP=1
|
|
MG_OUI_DUP_THRESHOLD=3
|
|
MG_DETECT_FLIP=1
|
|
MG_FLIP_WINDOW=300
|
|
MG_FLIP_THRESHOLD=3
|
|
MG_DETECT_SPOOF=1
|
|
MG_POLICY="alert"
|
|
MG_NOTIFY_CROWDSEC=0
|
|
MG_WL_MACS=""
|
|
MG_WL_OUIS=""
|
|
MG_START_TIME=$(date +%s)
|
|
MG_TOTAL_SCANS=0
|
|
MG_TOTAL_ALERTS=0
|
|
MG_DHCP_ENABLED=1
|
|
MG_DHCP_CLEANUP_STALE=1
|
|
MG_DHCP_DEDUP_HOSTNAMES=1
|
|
MG_DHCP_FLOOD_THRESHOLD=3
|
|
MG_DHCP_FLOOD_WINDOW=60
|
|
MG_DHCP_STALE_TIMEOUT=3600
|
|
|
|
# --- TAP output ---
|
|
TESTS=0
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
ok() {
|
|
TESTS=$((TESTS + 1))
|
|
PASS=$((PASS + 1))
|
|
echo "ok $TESTS - $1"
|
|
}
|
|
|
|
not_ok() {
|
|
TESTS=$((TESTS + 1))
|
|
FAIL=$((FAIL + 1))
|
|
echo "not ok $TESTS - $1"
|
|
}
|
|
|
|
echo "TAP version 13"
|
|
echo "1..12"
|
|
|
|
# --- Test 1: Randomized MAC generates alert ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
mg_check_station "02:aa:bb:cc:dd:ee" "-65" "wlan0"
|
|
if grep -q "randomized_mac" "$MG_EVENTS_LOG"; then
|
|
ok "Randomized MAC (02:xx) triggers alert"
|
|
else
|
|
not_ok "Randomized MAC (02:xx) triggers alert"
|
|
fi
|
|
|
|
# --- Test 2: Non-randomized MAC does NOT generate randomized alert ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
mg_check_station "00:11:22:33:44:55" "-60" "wlan0"
|
|
if grep -q "randomized_mac" "$MG_EVENTS_LOG"; then
|
|
not_ok "Non-randomized MAC (00:xx) does not trigger randomized alert"
|
|
else
|
|
ok "Non-randomized MAC (00:xx) does not trigger randomized alert"
|
|
fi
|
|
|
|
# --- Test 3: New station event logged ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
mg_check_station "00:aa:bb:cc:dd:01" "-55" "wlan0"
|
|
if grep -q "new_station" "$MG_EVENTS_LOG"; then
|
|
ok "New station event logged for unknown MAC"
|
|
else
|
|
not_ok "New station event logged for unknown MAC"
|
|
fi
|
|
|
|
# --- Test 4: OUI anomaly detection ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
now=$(date +%s)
|
|
# Seed database with 4 MACs sharing same OUI on wlan0 (threshold is 3)
|
|
echo "aa:bb:cc:00:00:01|AA:BB:CC|$now|$now|wlan0|host1|unknown" >> "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:02|AA:BB:CC|$now|$now|wlan0|host2|unknown" >> "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:03|AA:BB:CC|$now|$now|wlan0|host3|unknown" >> "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:04|AA:BB:CC|$now|$now|wlan0|host4|unknown" >> "$MG_DBFILE"
|
|
mg_detect_oui_anomaly "wlan0"
|
|
if grep -q "oui_anomaly" "$MG_EVENTS_LOG"; then
|
|
ok "OUI anomaly triggered when count exceeds threshold"
|
|
else
|
|
not_ok "OUI anomaly triggered when count exceeds threshold"
|
|
fi
|
|
|
|
# --- Test 5: OUI anomaly NOT triggered below threshold ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:01|AA:BB:CC|$now|$now|wlan0|host1|unknown" >> "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:02|AA:BB:CC|$now|$now|wlan0|host2|unknown" >> "$MG_DBFILE"
|
|
mg_detect_oui_anomaly "wlan0"
|
|
if grep -q "oui_anomaly" "$MG_EVENTS_LOG"; then
|
|
not_ok "OUI anomaly not triggered below threshold"
|
|
else
|
|
ok "OUI anomaly not triggered below threshold"
|
|
fi
|
|
|
|
# --- Test 6: MAC flood detection ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
now=$(date +%s)
|
|
# Seed with many recent MACs (threshold is 3)
|
|
echo "00:11:22:33:44:01|00:11:22|$now|$now|wlan0||unknown" >> "$MG_DBFILE"
|
|
echo "00:11:22:33:44:02|00:11:22|$now|$now|wlan0||unknown" >> "$MG_DBFILE"
|
|
echo "00:11:22:33:44:03|00:11:22|$now|$now|wlan0||unknown" >> "$MG_DBFILE"
|
|
echo "00:11:22:33:44:04|00:11:22|$now|$now|wlan0||unknown" >> "$MG_DBFILE"
|
|
mg_detect_mac_flip "wlan0"
|
|
if grep -q "mac_flood" "$MG_EVENTS_LOG"; then
|
|
ok "MAC flood detected when new MACs exceed threshold in window"
|
|
else
|
|
not_ok "MAC flood detected when new MACs exceed threshold in window"
|
|
fi
|
|
|
|
# --- Test 7: Spoof detection (interface change) ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
now=$(date +%s)
|
|
# Seed a MAC on wlan0
|
|
echo "00:aa:bb:cc:dd:99|00:AA:BB|$((now - 60))|$((now - 10))|wlan0|testhost|unknown" >> "$MG_DBFILE"
|
|
# Check same MAC on wlan1 -> should trigger spoof
|
|
mg_check_station "00:aa:bb:cc:dd:99" "-70" "wlan1"
|
|
if grep -q "spoof_detected" "$MG_EVENTS_LOG"; then
|
|
ok "Spoof detected when MAC appears on different interface"
|
|
else
|
|
not_ok "Spoof detected when MAC appears on different interface"
|
|
fi
|
|
|
|
# --- Test 8: Stats generation ---
|
|
: > "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:01|AA:BB:CC|$now|$now|wlan0|host1|trusted" >> "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:02|AA:BB:CC|$now|$now|wlan0|host2|suspect" >> "$MG_DBFILE"
|
|
echo "aa:bb:cc:00:00:03|AA:BB:CC|$now|$now|wlan0|host3|unknown" >> "$MG_DBFILE"
|
|
mg_stats_generate
|
|
if [ -f "$MG_STATS_FILE" ] && grep -q '"trusted":1' "$MG_STATS_FILE"; then
|
|
ok "Stats generation produces valid JSON with correct counts"
|
|
else
|
|
not_ok "Stats generation produces valid JSON with correct counts"
|
|
fi
|
|
|
|
# --- DHCP Detection Tests ---
|
|
|
|
# Override DHCP leases path for testing
|
|
MG_DHCP_LEASES="$TEST_TMPDIR/dhcp.leases"
|
|
|
|
# --- Test 9: DHCP hostname conflict event logged ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DBFILE"
|
|
now=$(date +%s)
|
|
cat > "$MG_DHCP_LEASES" <<EOF
|
|
$((now - 50)) 00:aa:bb:cc:01:01 192.168.1.50 duphost *
|
|
$now 00:aa:bb:cc:01:02 192.168.1.51 duphost *
|
|
EOF
|
|
mg_dhcp_find_hostname_dupes
|
|
if grep -q "dhcp_hostname_conflict" "$MG_EVENTS_LOG"; then
|
|
ok "DHCP hostname conflict event logged"
|
|
else
|
|
not_ok "DHCP hostname conflict event logged"
|
|
fi
|
|
|
|
# --- Test 10: DHCP stale removal event logged ---
|
|
: > "$MG_EVENTS_LOG"
|
|
now=$(date +%s)
|
|
cat > "$MG_DHCP_LEASES" <<EOF
|
|
$((now - 7200)) 00:aa:bb:cc:02:01 192.168.1.60 oldhost *
|
|
EOF
|
|
mg_dhcp_cleanup_stale
|
|
if grep -q "dhcp_stale_removed" "$MG_EVENTS_LOG"; then
|
|
ok "DHCP stale removal event logged"
|
|
else
|
|
not_ok "DHCP stale removal event logged"
|
|
fi
|
|
|
|
# --- Test 11: DHCP lease flood event logged ---
|
|
: > "$MG_EVENTS_LOG"
|
|
now=$(date +%s)
|
|
cat > "$MG_DHCP_LEASES" <<EOF
|
|
$now 00:aa:bb:cc:03:01 192.168.1.70 f1 *
|
|
$now 00:aa:bb:cc:03:02 192.168.1.71 f2 *
|
|
$now 00:aa:bb:cc:03:03 192.168.1.72 f3 *
|
|
$now 00:aa:bb:cc:03:04 192.168.1.73 f4 *
|
|
EOF
|
|
mg_dhcp_detect_flood
|
|
if grep -q "dhcp_lease_flood" "$MG_EVENTS_LOG"; then
|
|
ok "DHCP lease flood event logged"
|
|
else
|
|
not_ok "DHCP lease flood event logged"
|
|
fi
|
|
|
|
# --- Test 12: Empty leases file is handled safely ---
|
|
: > "$MG_EVENTS_LOG"
|
|
: > "$MG_DHCP_LEASES"
|
|
mg_dhcp_maintenance
|
|
if [ -f "$MG_DHCP_LEASES" ]; then
|
|
ok "Empty leases file handled safely by maintenance"
|
|
else
|
|
not_ok "Empty leases file handled safely by maintenance"
|
|
fi
|
|
|
|
# --- Summary ---
|
|
echo ""
|
|
echo "# Tests: $TESTS, Passed: $PASS, Failed: $FAIL"
|
|
if [ "$FAIL" -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|