secubox-openwrt/package/secubox/secubox-app-mac-guardian/files/usr/sbin/mac-guardian
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

308 lines
7.3 KiB
Bash

#!/bin/sh
# mac-guardian -- WiFi MAC Security Monitor for SecuBox
# Usage: mac-guardian {start|scan|status|trust|block|list|version}
. /usr/lib/secubox/mac-guardian/functions.sh
cmd_start() {
mg_load_config
if [ "$MG_ENABLED" != "1" ]; then
echo "mac-guardian is disabled. Enable with: uci set mac-guardian.main.enabled=1"
exit 1
fi
mg_init
mg_log "info" "mac-guardian v${MG_VERSION} starting (interval=${MG_SCAN_INTERVAL}s policy=${MG_POLICY})"
trap 'mg_log "info" "Shutting down"; mg_unlock 2>/dev/null; exit 0' INT TERM
trap 'mg_load_config; mg_log "info" "Configuration reloaded"' HUP
local stats_counter=0
while true; do
mg_scan_all
stats_counter=$((stats_counter + MG_SCAN_INTERVAL))
if [ "$stats_counter" -ge "$MG_STATS_INTERVAL" ]; then
mg_stats_generate
stats_counter=0
fi
mg_log_rotate
sleep "$MG_SCAN_INTERVAL"
done
}
cmd_scan() {
mg_load_config
mg_init
mg_scan_all
echo "Scan complete. $(wc -l < "$MG_DBFILE" 2>/dev/null || echo 0) clients in database."
}
cmd_status() {
mg_load_config
echo "mac-guardian v${MG_VERSION}"
echo "========================="
if [ "$MG_ENABLED" != "1" ]; then
echo "Status: DISABLED"
echo ""
echo "Enable with: uci set mac-guardian.main.enabled=1 && uci commit mac-guardian"
return
fi
local pid
pid=$(cat /var/run/mac-guardian.pid 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "Status: RUNNING (PID $pid)"
else
echo "Status: STOPPED"
fi
echo "Policy: $MG_POLICY"
echo ""
# Interfaces
echo "WiFi Interfaces:"
local ifaces
ifaces=$(mg_get_wifi_ifaces)
if [ -z "$ifaces" ]; then
echo " (none detected)"
else
for iface in $ifaces; do
local essid
essid=$(iwinfo "$iface" info 2>/dev/null | grep "ESSID" | sed 's/.*ESSID: "\(.*\)"/\1/')
local sta_count
sta_count=$(iwinfo "$iface" assoclist 2>/dev/null | grep -cE '[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}')
echo " $iface ($essid) - $sta_count stations"
done
fi
echo ""
# Database stats
if [ -f "$MG_DBFILE" ] && [ -s "$MG_DBFILE" ]; then
local total trusted suspect blocked unknown
total=$(wc -l < "$MG_DBFILE")
trusted=$(grep -c '|trusted$' "$MG_DBFILE" 2>/dev/null || echo 0)
suspect=$(grep -c '|suspect$' "$MG_DBFILE" 2>/dev/null || echo 0)
blocked=$(grep -c '|blocked$' "$MG_DBFILE" 2>/dev/null || echo 0)
unknown=$(grep -c '|unknown$' "$MG_DBFILE" 2>/dev/null || echo 0)
echo "Known Clients: $total"
echo " Trusted: $trusted"
echo " Unknown: $unknown"
echo " Suspect: $suspect"
echo " Blocked: $blocked"
else
echo "Known Clients: 0"
fi
echo ""
# Last alerts
if [ -f "$MG_EVENTS_LOG" ] && [ -s "$MG_EVENTS_LOG" ]; then
echo "Last 5 Alerts:"
tail -5 "$MG_EVENTS_LOG" | while read -r line; do
echo " $line"
done
else
echo "No alerts recorded."
fi
}
cmd_trust() {
local mac="$1"
if [ -z "$mac" ]; then
echo "Usage: mac-guardian trust <MAC>"
exit 1
fi
if ! mg_validate_mac "$mac"; then
echo "Error: Invalid MAC address format: $mac"
exit 1
fi
mac=$(mg_normalize_mac "$mac")
mg_load_config
mg_init
# Add to UCI whitelist
uci add_list mac-guardian.whitelist.mac="$mac"
uci commit mac-guardian
# Update database
mg_db_set_status "$mac" "trusted"
# Remove any enforcement
mg_unenforce "$mac"
mg_log "notice" "MAC $mac marked as trusted"
echo "MAC $mac is now trusted."
}
cmd_block() {
local mac="$1"
if [ -z "$mac" ]; then
echo "Usage: mac-guardian block <MAC>"
exit 1
fi
if ! mg_validate_mac "$mac"; then
echo "Error: Invalid MAC address format: $mac"
exit 1
fi
mac=$(mg_normalize_mac "$mac")
mg_load_config
mg_init
# Get the interface this MAC is on
local iface=""
local existing
existing=$(mg_db_lookup "$mac")
if [ -n "$existing" ]; then
iface=$(echo "$existing" | cut -d'|' -f5)
fi
# Force deny policy for this action
local saved_policy="$MG_POLICY"
MG_POLICY="deny"
mg_enforce "$mac" "${iface:-unknown}" "manual_block"
MG_POLICY="$saved_policy"
mg_db_set_status "$mac" "blocked"
mg_log_event "manual_block" "$mac" "${iface:-unknown}" "blocked_by_admin"
echo "MAC $mac is now blocked."
}
cmd_list() {
local filter="${1:-all}"
mg_load_config
mg_init
if [ ! -f "$MG_DBFILE" ] || [ ! -s "$MG_DBFILE" ]; then
echo "No clients in database."
return
fi
printf "%-19s %-10s %-12s %-8s %-15s %s\n" "MAC" "OUI" "FIRST SEEN" "IFACE" "HOSTNAME" "STATUS"
printf "%-19s %-10s %-12s %-8s %-15s %s\n" "---" "---" "----------" "-----" "--------" "------"
while IFS='|' read -r mac oui first_seen last_seen iface hostname status; do
[ -z "$mac" ] && continue
# Apply filter
case "$filter" in
all) ;;
trusted|blocked|suspect|unknown)
[ "$status" != "$filter" ] && continue
;;
*)
echo "Unknown filter: $filter (use: trusted, blocked, suspect, unknown, all)"
return 1
;;
esac
# Format first_seen as date
local date_str
date_str=$(date -d "@${first_seen}" "+%Y-%m-%d" 2>/dev/null || echo "$first_seen")
printf "%-19s %-10s %-12s %-8s %-15s %s\n" \
"$mac" "$oui" "$date_str" "$iface" "${hostname:--}" "$status"
done < "$MG_DBFILE"
}
cmd_dhcp_status() {
mg_load_config
mg_init
echo "DHCP Lease Protection"
echo "====================="
if [ "$MG_DHCP_ENABLED" != "1" ]; then
echo "Status: DISABLED"
return
fi
echo "Status: ENABLED"
local lease_count=0 conflict_count=0 stale_count=0
if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then
lease_count=$(wc -l < /tmp/dhcp.leases)
# Count hostname conflicts (hostnames with >1 MAC)
conflict_count=$(awk '{print $4}' /tmp/dhcp.leases | grep -v '^\*$' | sort | uniq -d | wc -l)
# Count stale leases
local now
now=$(date +%s)
local cutoff=$((now - MG_DHCP_STALE_TIMEOUT))
stale_count=$(awk -v cutoff="$cutoff" '$1 < cutoff' /tmp/dhcp.leases | wc -l)
fi
echo "Leases: $lease_count"
echo "Conflicts: $conflict_count"
echo "Stale: $stale_count"
echo ""
echo "Settings:"
echo " Dedup hostnames: $MG_DHCP_DEDUP_HOSTNAMES"
echo " Cleanup stale: $MG_DHCP_CLEANUP_STALE"
echo " Stale timeout: ${MG_DHCP_STALE_TIMEOUT}s"
echo " Flood threshold: $MG_DHCP_FLOOD_THRESHOLD"
echo " Flood window: ${MG_DHCP_FLOOD_WINDOW}s"
}
cmd_dhcp_cleanup() {
mg_load_config
mg_init
if [ "$MG_DHCP_ENABLED" != "1" ]; then
echo "DHCP protection is disabled."
exit 1
fi
echo "Running DHCP lease maintenance..."
mg_dhcp_maintenance
echo "Done."
}
cmd_version() {
echo "mac-guardian v${MG_VERSION}"
}
# --- Main dispatcher ---
case "${1:-}" in
start) cmd_start ;;
scan) cmd_scan ;;
status) cmd_status ;;
trust) cmd_trust "$2" ;;
block) cmd_block "$2" ;;
list) cmd_list "$2" ;;
dhcp-status) cmd_dhcp_status ;;
dhcp-cleanup) cmd_dhcp_cleanup ;;
version) cmd_version ;;
*)
echo "Usage: mac-guardian {start|scan|status|trust|block|list|dhcp-status|dhcp-cleanup|version}"
echo ""
echo "Commands:"
echo " start Start the daemon"
echo " scan Run a single scan pass"
echo " status Show service status"
echo " trust <MAC> Add MAC to trusted whitelist"
echo " block <MAC> Block and deauthenticate MAC"
echo " list [filter] List known clients (trusted|blocked|suspect|unknown|all)"
echo " dhcp-status Show DHCP lease protection status"
echo " dhcp-cleanup Run one-shot DHCP lease maintenance"
echo " version Print version"
exit 1
;;
esac