secubox-openwrt/package/secubox/secubox-app-mac-guardian/tests/test_functions.sh
CyberMind-FR 2d810a2e95 feat(mac-guardian): Add DHCP lease protection for odhcpd
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>
2026-02-03 16:22:37 +01:00

268 lines
7.8 KiB
Bash

#!/bin/sh
# TAP test suite for mac-guardian functions
# Run: sh tests/test_functions.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 for test environment
logger() { :; }
# Stub UCI functions for testing
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 defaults for tests
MG_ENABLED=1
MG_DEBUG=0
MG_DETECT_RANDOM=1
MG_DETECT_OUI_DUP=1
MG_OUI_DUP_THRESHOLD=5
MG_DETECT_FLIP=1
MG_FLIP_WINDOW=300
MG_FLIP_THRESHOLD=10
MG_DETECT_SPOOF=1
MG_POLICY="alert"
MG_NOTIFY_CROWDSEC=0
MG_WL_MACS=""
MG_WL_OUIS=""
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"
}
assert_true() {
if eval "$1"; then
ok "$2"
else
not_ok "$2"
fi
}
assert_false() {
if eval "$1"; then
not_ok "$2"
else
ok "$2"
fi
}
assert_eq() {
if [ "$1" = "$2" ]; then
ok "$3"
else
not_ok "$3 (got '$1', expected '$2')"
fi
}
echo "TAP version 13"
echo "1..27"
# --- Test mg_is_randomized ---
assert_true 'mg_is_randomized "02:11:22:33:44:55"' "mg_is_randomized: 02:xx is randomized"
assert_false 'mg_is_randomized "00:11:22:33:44:55"' "mg_is_randomized: 00:xx is not randomized"
assert_true 'mg_is_randomized "da:11:22:33:44:55"' "mg_is_randomized: da:xx is randomized (bit1 set)"
assert_false 'mg_is_randomized "dc:11:22:33:44:55"' "mg_is_randomized: dc:xx is not randomized (bit1 clear)"
assert_true 'mg_is_randomized "fe:11:22:33:44:55"' "mg_is_randomized: fe:xx is randomized"
assert_false 'mg_is_randomized "fc:11:22:33:44:55"' "mg_is_randomized: fc:xx is not randomized"
# --- Test mg_validate_mac ---
assert_true 'mg_validate_mac "aa:bb:cc:dd:ee:ff"' "mg_validate_mac: valid lowercase MAC"
assert_true 'mg_validate_mac "AA:BB:CC:DD:EE:FF"' "mg_validate_mac: valid uppercase MAC"
assert_false 'mg_validate_mac "aa:bb:cc:dd:ee"' "mg_validate_mac: short MAC rejected"
assert_false 'mg_validate_mac "aabbccddeeff"' "mg_validate_mac: no colons rejected"
assert_false 'mg_validate_mac "gg:hh:ii:jj:kk:ll"' "mg_validate_mac: hex overflow rejected"
# --- Test mg_get_oui ---
result=$(mg_get_oui "aa:bb:cc:dd:ee:ff")
assert_eq "$result" "AA:BB:CC" "mg_get_oui: extracts first 3 octets uppercase"
# --- Test mg_db_upsert + mg_db_lookup ---
# Insert new entry
mg_db_upsert "aa:bb:cc:11:22:33" "wlan0" "test-host"
entry=$(mg_db_lookup "aa:bb:cc:11:22:33")
assert_true '[ -n "$entry" ]' "mg_db_upsert: inserts new entry"
# Update existing (no duplicate)
mg_db_upsert "aa:bb:cc:11:22:33" "wlan0" "test-host-updated"
count=$(grep -c "aa:bb:cc:11:22:33" "$MG_DBFILE")
assert_eq "$count" "1" "mg_db_upsert: update does not create duplicate"
# Lookup missing
missing=$(mg_db_lookup "ff:ff:ff:ff:ff:ff")
assert_true '[ -z "$missing" ]' "mg_db_lookup: missing MAC returns empty"
# --- Test mg_is_whitelisted ---
MG_WL_MACS="aa:bb:cc:11:22:33"
assert_true 'mg_is_whitelisted "aa:bb:cc:11:22:33"' "mg_is_whitelisted: whitelisted MAC matches"
assert_false 'mg_is_whitelisted "ff:ee:dd:cc:bb:aa"' "mg_is_whitelisted: non-whitelisted MAC rejected"
MG_WL_MACS=""
# --- Test lock acquire/release ---
mg_lock
assert_true '[ -d "$MG_LOCKDIR" ]' "mg_lock: lock directory created"
mg_unlock
assert_false '[ -d "$MG_LOCKDIR" ]' "mg_unlock: lock directory removed"
# --- DHCP Protection Tests ---
# Override DHCP leases path for testing
MG_DHCP_LEASES="$TEST_TMPDIR/dhcp.leases"
# Test 20: mg_dhcp_read_leases returns valid temp file
echo "1000 aa:bb:cc:dd:ee:01 192.168.1.10 host1 *" > "$MG_DHCP_LEASES"
lease_tmp=$(mg_dhcp_read_leases)
if [ -f "$lease_tmp" ] && grep -q "aa:bb:cc:dd:ee:01" "$lease_tmp"; then
ok "mg_dhcp_read_leases: returns readable temp copy"
else
not_ok "mg_dhcp_read_leases: returns readable temp copy"
fi
rm -f "$lease_tmp"
# Test 21: mg_dhcp_find_hostname_dupes detects duplicate hostnames
: > "$MG_EVENTS_LOG"
now=$(date +%s)
cat > "$MG_DHCP_LEASES" <<EOF
$((now - 100)) aa:bb:cc:dd:ee:01 192.168.1.10 myhost *
$now aa:bb:cc:dd:ee:02 192.168.1.11 myhost *
EOF
mg_dhcp_find_hostname_dupes
if grep -q "dhcp_hostname_conflict" "$MG_EVENTS_LOG"; then
ok "mg_dhcp_find_hostname_dupes: detects hostname conflict"
else
not_ok "mg_dhcp_find_hostname_dupes: detects hostname conflict"
fi
# Test 22: mg_dhcp_find_hostname_dupes removes older duplicate
if ! grep -q "aa:bb:cc:dd:ee:01" "$MG_DHCP_LEASES" && grep -q "aa:bb:cc:dd:ee:02" "$MG_DHCP_LEASES"; then
ok "mg_dhcp_find_hostname_dupes: removes older duplicate lease"
else
not_ok "mg_dhcp_find_hostname_dupes: removes older duplicate lease"
fi
# Test 23: mg_dhcp_cleanup_stale removes expired leases
: > "$MG_EVENTS_LOG"
now=$(date +%s)
cat > "$MG_DHCP_LEASES" <<EOF
$((now - 7200)) aa:bb:cc:dd:ee:03 192.168.1.12 stale-host *
$now aa:bb:cc:dd:ee:04 192.168.1.13 fresh-host *
EOF
mg_dhcp_cleanup_stale
if grep -q "dhcp_stale_removed" "$MG_EVENTS_LOG" && grep -q "aa:bb:cc:dd:ee:04" "$MG_DHCP_LEASES"; then
ok "mg_dhcp_cleanup_stale: removes expired, keeps fresh"
else
not_ok "mg_dhcp_cleanup_stale: removes expired, keeps fresh"
fi
# Test 24: mg_dhcp_remove_lease removes specific MAC
now=$(date +%s)
cat > "$MG_DHCP_LEASES" <<EOF
$now aa:bb:cc:dd:ee:05 192.168.1.14 host5 *
$now aa:bb:cc:dd:ee:06 192.168.1.15 host6 *
EOF
mg_dhcp_remove_lease "aa:bb:cc:dd:ee:05"
if ! grep -q "aa:bb:cc:dd:ee:05" "$MG_DHCP_LEASES" && grep -q "aa:bb:cc:dd:ee:06" "$MG_DHCP_LEASES"; then
ok "mg_dhcp_remove_lease: removes target MAC, keeps others"
else
not_ok "mg_dhcp_remove_lease: removes target MAC, keeps others"
fi
# Test 25: mg_dhcp_detect_flood detects lease flooding
: > "$MG_EVENTS_LOG"
now=$(date +%s)
cat > "$MG_DHCP_LEASES" <<EOF
$now aa:bb:cc:dd:ee:10 192.168.1.20 h1 *
$now aa:bb:cc:dd:ee:11 192.168.1.21 h2 *
$now aa:bb:cc:dd:ee:12 192.168.1.22 h3 *
$now aa:bb:cc:dd:ee:13 192.168.1.23 h4 *
EOF
mg_dhcp_detect_flood
if grep -q "dhcp_lease_flood" "$MG_EVENTS_LOG"; then
ok "mg_dhcp_detect_flood: detects flood above threshold"
else
not_ok "mg_dhcp_detect_flood: detects flood above threshold"
fi
# Test 26: mg_dhcp_detect_flood does not trigger below threshold
: > "$MG_EVENTS_LOG"
now=$(date +%s)
cat > "$MG_DHCP_LEASES" <<EOF
$now aa:bb:cc:dd:ee:10 192.168.1.20 h1 *
$now aa:bb:cc:dd:ee:11 192.168.1.21 h2 *
EOF
mg_dhcp_detect_flood
if grep -q "dhcp_lease_flood" "$MG_EVENTS_LOG"; then
not_ok "mg_dhcp_detect_flood: no flood below threshold"
else
ok "mg_dhcp_detect_flood: no flood below threshold"
fi
# Test 27: mg_dhcp_remove_lease handles empty leases file
: > "$MG_DHCP_LEASES"
mg_dhcp_remove_lease "ff:ff:ff:ff:ff:ff"
if [ -f "$MG_DHCP_LEASES" ]; then
ok "mg_dhcp_remove_lease: handles empty leases safely"
else
not_ok "mg_dhcp_remove_lease: handles empty leases safely"
fi
# --- Summary ---
echo ""
echo "# Tests: $TESTS, Passed: $PASS, Failed: $FAIL"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0