diff --git a/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js b/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js
index bd0f28f6..3eca262d 100644
--- a/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js
+++ b/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js
@@ -56,6 +56,18 @@ var callBlock = rpc.declare({
params: ['mac']
});
+var callDhcpStatus = rpc.declare({
+ object: 'luci.mac-guardian',
+ method: 'dhcp_status',
+ expect: { '': {} }
+});
+
+var callDhcpCleanup = rpc.declare({
+ object: 'luci.mac-guardian',
+ method: 'dhcp_cleanup',
+ expect: { '': {} }
+});
+
function formatDate(ts) {
if (!ts || ts === 0) return '-';
var d = new Date(ts * 1000);
@@ -83,7 +95,8 @@ return view.extend({
uci.load('mac-guardian'),
callStatus(),
callGetClients(),
- callGetEvents(10)
+ callGetEvents(10),
+ callDhcpStatus()
]);
},
@@ -91,6 +104,7 @@ return view.extend({
var status = data[1];
var clientData = data[2];
var eventData = data[3];
+ var dhcpStatus = data[4] || {};
var clients = (clientData && clientData.clients) ? clientData.clients : [];
var events = (eventData && eventData.events) ? eventData.events : [];
var m, s, o;
@@ -143,6 +157,17 @@ return view.extend({
}
html += '';
+ // DHCP Protection card
+ var dhcpColor = dhcpStatus.enabled ? '#080' : '#888';
+ var dhcpLabel = dhcpStatus.enabled ? 'Enabled' : 'Disabled';
+ html += '
';
+ html += '
DHCP Protection
';
+ html += '
Status: ' + dhcpLabel + '
';
+ html += '
Leases: ' + (dhcpStatus.leases || 0) + '
';
+ html += '
Conflicts: ' + (dhcpStatus.conflicts || 0) + '
';
+ html += '
Stale: ' + (dhcpStatus.stale || 0) + '
';
+ html += '
';
+
html += '';
return html;
};
@@ -175,6 +200,19 @@ return view.extend({
});
};
+ o = s.option(form.Button, '_dhcp_cleanup', _('DHCP Cleanup'));
+ o.inputtitle = _('Clean Up');
+ o.inputstyle = 'reload';
+ o.onclick = function() {
+ ui.showModal(_('Cleaning'), [
+ E('p', { 'class': 'spinning' }, _('Running DHCP lease maintenance...'))
+ ]);
+ return callDhcpCleanup().then(function() {
+ ui.hideModal();
+ window.location.reload();
+ });
+ };
+
// ==========================================
// Clients Table
// ==========================================
diff --git a/package/secubox/luci-app-mac-guardian/root/usr/libexec/rpcd/luci.mac-guardian b/package/secubox/luci-app-mac-guardian/root/usr/libexec/rpcd/luci.mac-guardian
index 775ba895..bf1b1546 100644
--- a/package/secubox/luci-app-mac-guardian/root/usr/libexec/rpcd/luci.mac-guardian
+++ b/package/secubox/luci-app-mac-guardian/root/usr/libexec/rpcd/luci.mac-guardian
@@ -8,7 +8,7 @@ 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"}}'
+ 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
@@ -183,6 +183,46 @@ case "$1" in
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
diff --git a/package/secubox/secubox-app-mac-guardian/files/etc/config/mac-guardian b/package/secubox/secubox-app-mac-guardian/files/etc/config/mac-guardian
index ef613b94..012818cc 100644
--- a/package/secubox/secubox-app-mac-guardian/files/etc/config/mac-guardian
+++ b/package/secubox/secubox-app-mac-guardian/files/etc/config/mac-guardian
@@ -21,6 +21,14 @@ config whitelist 'whitelist'
# list mac 'aa:bb:cc:dd:ee:ff'
# list oui '00:50:E4'
+config dhcp_protection 'dhcp'
+ option enabled '1'
+ option cleanup_stale '1'
+ option dedup_hostnames '1'
+ option flood_threshold '10'
+ option flood_window '60'
+ option stale_timeout '3600'
+
config reporting 'reporting'
option stats_file '/var/run/mac-guardian/stats.json'
option stats_interval '60'
diff --git a/package/secubox/secubox-app-mac-guardian/files/etc/hotplug.d/hostapd/20-mac-guardian b/package/secubox/secubox-app-mac-guardian/files/etc/hotplug.d/hostapd/20-mac-guardian
index 1c7a0e52..46473869 100644
--- a/package/secubox/secubox-app-mac-guardian/files/etc/hotplug.d/hostapd/20-mac-guardian
+++ b/package/secubox/secubox-app-mac-guardian/files/etc/hotplug.d/hostapd/20-mac-guardian
@@ -46,7 +46,7 @@ config_get enabled main enabled 0
fi
;;
AP-STA-DISCONNECTED)
- # Lightweight: just update last_seen
+ # Update last_seen and clean up stale DHCP lease for this MAC
if mg_validate_mac "$mac"; then
mg_lock && {
local existing
@@ -56,6 +56,7 @@ config_get enabled main enabled 0
hostname=$(mg_resolve_hostname "$mac")
mg_db_upsert "$mac" "$iface" "$hostname"
fi
+ mg_dhcp_cleanup_stale_mac "$mac"
mg_unlock
}
fi
diff --git a/package/secubox/secubox-app-mac-guardian/files/usr/lib/secubox/mac-guardian/functions.sh b/package/secubox/secubox-app-mac-guardian/files/usr/lib/secubox/mac-guardian/functions.sh
index ce670ac1..90d9959a 100644
--- a/package/secubox/secubox-app-mac-guardian/files/usr/lib/secubox/mac-guardian/functions.sh
+++ b/package/secubox/secubox-app-mac-guardian/files/usr/lib/secubox/mac-guardian/functions.sh
@@ -28,6 +28,14 @@ MG_STATS_FILE="/var/run/mac-guardian/stats.json"
MG_STATS_INTERVAL=60
MG_MAX_LOG_SIZE=524288
+# DHCP protection config
+MG_DHCP_ENABLED=1
+MG_DHCP_CLEANUP_STALE=1
+MG_DHCP_DEDUP_HOSTNAMES=1
+MG_DHCP_FLOOD_THRESHOLD=10
+MG_DHCP_FLOOD_WINDOW=60
+MG_DHCP_STALE_TIMEOUT=3600
+
# Whitelist arrays stored as newline-separated strings
MG_WL_MACS=""
MG_WL_OUIS=""
@@ -79,6 +87,14 @@ mg_load_config() {
config_get MG_STATS_INTERVAL reporting stats_interval 60
config_get MG_MAX_LOG_SIZE reporting max_log_size 524288
+ # dhcp protection section
+ config_get MG_DHCP_ENABLED dhcp enabled 1
+ config_get MG_DHCP_CLEANUP_STALE dhcp cleanup_stale 1
+ config_get MG_DHCP_DEDUP_HOSTNAMES dhcp dedup_hostnames 1
+ config_get MG_DHCP_FLOOD_THRESHOLD dhcp flood_threshold 10
+ config_get MG_DHCP_FLOOD_WINDOW dhcp flood_window 60
+ config_get MG_DHCP_STALE_TIMEOUT dhcp stale_timeout 3600
+
# whitelist lists
MG_WL_MACS=""
MG_WL_OUIS=""
@@ -340,6 +356,146 @@ mg_resolve_hostname() {
echo "$hostname"
}
+# ============================================================
+# DHCP Lease Protection
+# ============================================================
+
+MG_DHCP_LEASES="/tmp/dhcp.leases"
+
+mg_dhcp_read_leases() {
+ local tmpfile="${MG_RUNDIR}/dhcp_leases.$$"
+ if [ -f "$MG_DHCP_LEASES" ] && [ -s "$MG_DHCP_LEASES" ]; then
+ cp "$MG_DHCP_LEASES" "$tmpfile"
+ else
+ : > "$tmpfile"
+ fi
+ echo "$tmpfile"
+}
+
+mg_dhcp_find_hostname_dupes() {
+ [ "$MG_DHCP_DEDUP_HOSTNAMES" != "1" ] && return 0
+ [ ! -f "$MG_DHCP_LEASES" ] || [ ! -s "$MG_DHCP_LEASES" ] && return 0
+
+ local leases_copy
+ leases_copy=$(mg_dhcp_read_leases)
+
+ # Format: timestamp mac ip hostname clientid
+ # Find hostnames that appear with more than one MAC
+ local dupes_file="${MG_RUNDIR}/hostname_dupes.$$"
+ awk '{print $4, $2, $1}' "$leases_copy" | \
+ sort -k1,1 -k3,3n | \
+ awk '
+ {
+ hostname=$1; mac=$2; ts=$3
+ if (hostname != "*" && hostname != "" && hostname == prev_host && mac != prev_mac) {
+ # Duplicate hostname with different MAC - print the older one
+ if (ts < prev_ts) {
+ print mac
+ } else {
+ print prev_mac
+ }
+ }
+ prev_host=hostname; prev_mac=mac; prev_ts=ts
+ }' | sort -u > "$dupes_file"
+
+ if [ -s "$dupes_file" ]; then
+ while read -r dup_mac; do
+ [ -z "$dup_mac" ] && continue
+ local hostname
+ hostname=$(awk -v m="$dup_mac" 'tolower($2)==tolower(m) {print $4; exit}' "$leases_copy")
+ mg_log_event "dhcp_hostname_conflict" "$dup_mac" "" "hostname=${hostname}"
+ mg_dhcp_remove_lease "$dup_mac"
+ done < "$dupes_file"
+ fi
+
+ rm -f "$dupes_file" "$leases_copy"
+}
+
+mg_dhcp_cleanup_stale() {
+ [ "$MG_DHCP_CLEANUP_STALE" != "1" ] && return 0
+ [ ! -f "$MG_DHCP_LEASES" ] || [ ! -s "$MG_DHCP_LEASES" ] && return 0
+
+ local now
+ now=$(date +%s)
+ local cutoff=$((now - MG_DHCP_STALE_TIMEOUT))
+ local stale_file="${MG_RUNDIR}/stale_leases.$$"
+
+ # Find leases with timestamp older than cutoff
+ awk -v cutoff="$cutoff" '$1 < cutoff {print $2}' "$MG_DHCP_LEASES" > "$stale_file"
+
+ if [ -s "$stale_file" ]; then
+ while read -r stale_mac; do
+ [ -z "$stale_mac" ] && continue
+ mg_log_event "dhcp_stale_removed" "$stale_mac" "" "timeout=${MG_DHCP_STALE_TIMEOUT}s"
+ mg_dhcp_remove_lease "$stale_mac"
+ done < "$stale_file"
+ fi
+
+ rm -f "$stale_file"
+}
+
+mg_dhcp_cleanup_stale_mac() {
+ local target_mac="$1"
+ [ "$MG_DHCP_CLEANUP_STALE" != "1" ] && return 0
+ [ ! -f "$MG_DHCP_LEASES" ] || [ ! -s "$MG_DHCP_LEASES" ] && return 0
+
+ local now
+ now=$(date +%s)
+ local cutoff=$((now - MG_DHCP_STALE_TIMEOUT))
+
+ # Check if this specific MAC's lease is stale
+ local lease_ts
+ lease_ts=$(awk -v m="$target_mac" 'tolower($2)==tolower(m) {print $1; exit}' "$MG_DHCP_LEASES")
+ [ -z "$lease_ts" ] && return 0
+
+ if [ "$lease_ts" -lt "$cutoff" ] 2>/dev/null; then
+ mg_log_event "dhcp_stale_removed" "$target_mac" "" "timeout=${MG_DHCP_STALE_TIMEOUT}s"
+ mg_dhcp_remove_lease "$target_mac"
+ fi
+}
+
+mg_dhcp_detect_flood() {
+ [ "$MG_DHCP_ENABLED" != "1" ] && return 0
+ [ ! -f "$MG_DHCP_LEASES" ] || [ ! -s "$MG_DHCP_LEASES" ] && return 0
+
+ local now
+ now=$(date +%s)
+ local window_start=$((now - MG_DHCP_FLOOD_WINDOW))
+
+ local recent_count
+ recent_count=$(awk -v ws="$window_start" '$1 >= ws' "$MG_DHCP_LEASES" | wc -l)
+ recent_count=$((recent_count + 0))
+
+ if [ "$recent_count" -gt "$MG_DHCP_FLOOD_THRESHOLD" ]; then
+ mg_log_event "dhcp_lease_flood" "" "" "leases=${recent_count} window=${MG_DHCP_FLOOD_WINDOW}s threshold=${MG_DHCP_FLOOD_THRESHOLD}"
+ fi
+}
+
+mg_dhcp_remove_lease() {
+ local mac="$1"
+ [ -z "$mac" ] && return 1
+ [ ! -f "$MG_DHCP_LEASES" ] && return 0
+
+ local tmpfile="${MG_DHCP_LEASES}.tmp.$$"
+ grep -iv " ${mac} " "$MG_DHCP_LEASES" > "$tmpfile" 2>/dev/null || : > "$tmpfile"
+ mv "$tmpfile" "$MG_DHCP_LEASES"
+
+ # Signal odhcpd to reload leases
+ local odhcpd_pid
+ odhcpd_pid=$(pgrep odhcpd)
+ if [ -n "$odhcpd_pid" ]; then
+ kill -HUP "$odhcpd_pid" 2>/dev/null
+ fi
+}
+
+mg_dhcp_maintenance() {
+ [ "$MG_DHCP_ENABLED" != "1" ] && return 0
+
+ mg_dhcp_find_hostname_dupes
+ mg_dhcp_cleanup_stale
+ mg_dhcp_detect_flood
+}
+
# ============================================================
# Logging
# ============================================================
@@ -658,6 +814,8 @@ mg_scan_all() {
mg_scan_iface "$iface"
done
+ mg_dhcp_maintenance
+
MG_TOTAL_SCANS=$((MG_TOTAL_SCANS + 1))
mg_unlock
diff --git a/package/secubox/secubox-app-mac-guardian/files/usr/sbin/mac-guardian b/package/secubox/secubox-app-mac-guardian/files/usr/sbin/mac-guardian
index ba8a66cd..027fd639 100644
--- a/package/secubox/secubox-app-mac-guardian/files/usr/sbin/mac-guardian
+++ b/package/secubox/secubox-app-mac-guardian/files/usr/sbin/mac-guardian
@@ -219,21 +219,78 @@ cmd_list() {
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" ;;
- version) cmd_version ;;
+ 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|version}"
+ echo "Usage: mac-guardian {start|scan|status|trust|block|list|dhcp-status|dhcp-cleanup|version}"
echo ""
echo "Commands:"
echo " start Start the daemon"
@@ -242,6 +299,8 @@ case "${1:-}" in
echo " trust Add MAC to trusted whitelist"
echo " block 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
;;
diff --git a/package/secubox/secubox-app-mac-guardian/tests/test_detection.sh b/package/secubox/secubox-app-mac-guardian/tests/test_detection.sh
index c96b33c8..9cf5d84c 100644
--- a/package/secubox/secubox-app-mac-guardian/tests/test_detection.sh
+++ b/package/secubox/secubox-app-mac-guardian/tests/test_detection.sh
@@ -59,6 +59,12 @@ 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
@@ -78,7 +84,7 @@ not_ok() {
}
echo "TAP version 13"
-echo "1..8"
+echo "1..12"
# --- Test 1: Randomized MAC generates alert ---
: > "$MG_EVENTS_LOG"
@@ -180,6 +186,65 @@ 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" < "$MG_EVENTS_LOG"
+now=$(date +%s)
+cat > "$MG_DHCP_LEASES" < "$MG_EVENTS_LOG"
+now=$(date +%s)
+cat > "$MG_DHCP_LEASES" < "$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"
diff --git a/package/secubox/secubox-app-mac-guardian/tests/test_functions.sh b/package/secubox/secubox-app-mac-guardian/tests/test_functions.sh
index 3a9394ed..e0cec936 100644
--- a/package/secubox/secubox-app-mac-guardian/tests/test_functions.sh
+++ b/package/secubox/secubox-app-mac-guardian/tests/test_functions.sh
@@ -58,6 +58,12 @@ 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
@@ -101,7 +107,7 @@ assert_eq() {
}
echo "TAP version 13"
-echo "1..19"
+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"
@@ -150,6 +156,108 @@ 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" < "$MG_EVENTS_LOG"
+now=$(date +%s)
+cat > "$MG_DHCP_LEASES" < "$MG_DHCP_LEASES" < "$MG_EVENTS_LOG"
+now=$(date +%s)
+cat > "$MG_DHCP_LEASES" < "$MG_EVENTS_LOG"
+now=$(date +%s)
+cat > "$MG_DHCP_LEASES" < "$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"