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>
231 lines
6.1 KiB
Bash
231 lines
6.1 KiB
Bash
#!/bin/sh
|
|
|
|
. /lib/functions.sh
|
|
. /usr/share/libubox/jshn.sh
|
|
|
|
MG_DBFILE="/var/run/mac-guardian/known.db"
|
|
MG_LOGFILE="/var/log/mac-guardian.log"
|
|
|
|
case "$1" in
|
|
list)
|
|
echo '{"status":{},"get_clients":{},"get_events":{"count":"int"},"scan":{},"start":{},"stop":{},"restart":{},"trust":{"mac":"str"},"block":{"mac":"str"},"dhcp_status":{},"dhcp_cleanup":{}}'
|
|
;;
|
|
call)
|
|
case "$2" in
|
|
status)
|
|
json_init
|
|
|
|
enabled=$(uci -q get mac-guardian.main.enabled)
|
|
policy=$(uci -q get mac-guardian.enforcement.policy)
|
|
scan_interval=$(uci -q get mac-guardian.main.scan_interval)
|
|
detect_random=$(uci -q get mac-guardian.detection.random_mac)
|
|
detect_spoof=$(uci -q get mac-guardian.detection.spoof_detection)
|
|
|
|
json_add_boolean "enabled" ${enabled:-0}
|
|
json_add_string "policy" "${policy:-alert}"
|
|
json_add_int "scan_interval" ${scan_interval:-30}
|
|
json_add_boolean "detect_random" ${detect_random:-1}
|
|
json_add_boolean "detect_spoof" ${detect_spoof:-1}
|
|
|
|
# Service running?
|
|
if pgrep mac-guardian >/dev/null 2>&1; then
|
|
json_add_string "service_status" "running"
|
|
else
|
|
json_add_string "service_status" "stopped"
|
|
fi
|
|
|
|
# WiFi interfaces
|
|
json_add_array "interfaces"
|
|
if command -v iwinfo >/dev/null 2>&1; then
|
|
iwinfo 2>/dev/null | grep "ESSID" | while read -r line; do
|
|
iface=$(echo "$line" | awk '{print $1}')
|
|
essid=$(echo "$line" | sed 's/.*ESSID: "\(.*\)"/\1/')
|
|
sta_count=$(iwinfo "$iface" assoclist 2>/dev/null | grep -cE '[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}')
|
|
json_add_object ""
|
|
json_add_string "name" "$iface"
|
|
json_add_string "essid" "$essid"
|
|
json_add_int "stations" ${sta_count:-0}
|
|
json_close_object
|
|
done
|
|
fi
|
|
json_close_array
|
|
|
|
# DB stats
|
|
total=0
|
|
trusted=0
|
|
suspect=0
|
|
blocked=0
|
|
unknown=0
|
|
if [ -f "$MG_DBFILE" ] && [ -s "$MG_DBFILE" ]; then
|
|
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)
|
|
fi
|
|
json_add_object "clients"
|
|
json_add_int "total" $total
|
|
json_add_int "trusted" $trusted
|
|
json_add_int "suspect" $suspect
|
|
json_add_int "blocked" $blocked
|
|
json_add_int "unknown" $unknown
|
|
json_close_object
|
|
|
|
json_dump
|
|
;;
|
|
|
|
get_clients)
|
|
json_init
|
|
json_add_array "clients"
|
|
|
|
if [ -f "$MG_DBFILE" ] && [ -s "$MG_DBFILE" ]; then
|
|
while IFS='|' read -r mac oui first_seen last_seen iface hostname status; do
|
|
[ -z "$mac" ] && continue
|
|
json_add_object ""
|
|
json_add_string "mac" "$mac"
|
|
json_add_string "oui" "$oui"
|
|
json_add_int "first_seen" ${first_seen:-0}
|
|
json_add_int "last_seen" ${last_seen:-0}
|
|
json_add_string "iface" "$iface"
|
|
json_add_string "hostname" "${hostname:--}"
|
|
json_add_string "status" "$status"
|
|
|
|
# OUI vendor lookup
|
|
vendor=""
|
|
if [ -f /usr/lib/secubox/mac-guardian/oui.tsv ]; then
|
|
oui_upper=$(echo "$oui" | tr 'a-f' 'A-F')
|
|
vendor=$(grep -i "^${oui_upper} " /usr/lib/secubox/mac-guardian/oui.tsv 2>/dev/null | cut -f2 | head -1)
|
|
fi
|
|
json_add_string "vendor" "${vendor:--}"
|
|
|
|
# Randomized check
|
|
first_octet=$(echo "$mac" | cut -d: -f1)
|
|
is_rand=0
|
|
[ $((0x$first_octet & 0x02)) -ne 0 ] && is_rand=1
|
|
json_add_boolean "randomized" $is_rand
|
|
|
|
json_close_object
|
|
done < "$MG_DBFILE"
|
|
fi
|
|
|
|
json_close_array
|
|
json_dump
|
|
;;
|
|
|
|
get_events)
|
|
read -r input
|
|
count=$(echo "$input" | jsonfilter -e '@.count' 2>/dev/null)
|
|
[ -z "$count" ] && count=20
|
|
|
|
json_init
|
|
json_add_array "events"
|
|
|
|
if [ -f "$MG_LOGFILE" ] && [ -s "$MG_LOGFILE" ]; then
|
|
tail -"$count" "$MG_LOGFILE" 2>/dev/null | while read -r line; do
|
|
json_add_string "" "$line"
|
|
done
|
|
fi
|
|
|
|
json_close_array
|
|
json_dump
|
|
;;
|
|
|
|
scan)
|
|
/usr/sbin/mac-guardian scan >/dev/null 2>&1
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
;;
|
|
|
|
start)
|
|
/etc/init.d/mac-guardian start >/dev/null 2>&1
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
;;
|
|
|
|
stop)
|
|
/etc/init.d/mac-guardian stop >/dev/null 2>&1
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
;;
|
|
|
|
restart)
|
|
/etc/init.d/mac-guardian restart >/dev/null 2>&1
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
;;
|
|
|
|
trust)
|
|
read -r input
|
|
mac=$(echo "$input" | jsonfilter -e '@.mac' 2>/dev/null)
|
|
if [ -n "$mac" ]; then
|
|
/usr/sbin/mac-guardian trust "$mac" >/dev/null 2>&1
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
else
|
|
echo '{"success":false,"error":"missing mac"}'
|
|
fi
|
|
;;
|
|
|
|
block)
|
|
read -r input
|
|
mac=$(echo "$input" | jsonfilter -e '@.mac' 2>/dev/null)
|
|
if [ -n "$mac" ]; then
|
|
/usr/sbin/mac-guardian block "$mac" >/dev/null 2>&1
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
else
|
|
echo '{"success":false,"error":"missing mac"}'
|
|
fi
|
|
;;
|
|
|
|
dhcp_status)
|
|
json_init
|
|
|
|
dhcp_enabled=$(uci -q get mac-guardian.dhcp.enabled)
|
|
json_add_boolean "enabled" ${dhcp_enabled:-1}
|
|
|
|
leases=0
|
|
conflicts=0
|
|
stale=0
|
|
|
|
if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then
|
|
leases=$(wc -l < /tmp/dhcp.leases)
|
|
|
|
# Count hostname conflicts
|
|
conflicts=$(awk '{print $4}' /tmp/dhcp.leases | grep -v '^\*$' | sort | uniq -d | wc -l)
|
|
|
|
# Count stale leases
|
|
now=$(date +%s)
|
|
stale_timeout=$(uci -q get mac-guardian.dhcp.stale_timeout)
|
|
stale_timeout=${stale_timeout:-3600}
|
|
cutoff=$((now - stale_timeout))
|
|
stale=$(awk -v cutoff="$cutoff" '$1 < cutoff' /tmp/dhcp.leases | wc -l)
|
|
fi
|
|
|
|
json_add_int "leases" $leases
|
|
json_add_int "conflicts" $conflicts
|
|
json_add_int "stale" $stale
|
|
json_dump
|
|
;;
|
|
|
|
dhcp_cleanup)
|
|
/usr/sbin/mac-guardian dhcp-cleanup >/dev/null 2>&1
|
|
removed=0
|
|
|
|
json_init
|
|
json_add_boolean "success" 1
|
|
json_add_int "removed" $removed
|
|
json_dump
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
|
|
exit 0
|