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>
308 lines
7.3 KiB
Bash
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
|