From 0a3b1dfc6e76da64954d1566d662cc327bac3635 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 6 Feb 2026 16:34:35 +0100 Subject: [PATCH] feat(secubox-core): Add double-buffer status cache and fix LED blocking - Remove mmc0 LED from heartbeat loop (was causing LED freeze) - Implement background status_collector_loop() with staggered intervals - Add 10 cache files at /tmp/secubox/*.json for instant status reads - Add status_cached RPCD methods to 6 packages: - luci.crowdsec-dashboard - luci.mitmproxy - luci.secubox-netifyd - luci.client-guardian - luci.mac-guardian - luci.network-anomaly Dashboards and APIs now read pre-computed JSON cache instead of spawning subprocesses, eliminating blocking during concurrent requests. Co-Authored-By: Claude Opus 4.5 --- .../usr/libexec/rpcd/luci.client-guardian | 13 +- .../usr/libexec/rpcd/luci.crowdsec-dashboard | 15 +- .../root/usr/libexec/rpcd/luci.mac-guardian | 15 +- .../root/usr/libexec/rpcd/luci.mitmproxy | 13 +- .../usr/libexec/rpcd/luci.network-anomaly | 14 + .../usr/libexec/rpcd/luci.secubox-netifyd | 12 + .../secubox-core/root/usr/sbin/secubox-core | 525 ++++++++++++++---- 7 files changed, 484 insertions(+), 123 deletions(-) diff --git a/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian b/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian index 3c13f8f7..79591750 100755 --- a/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian +++ b/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian @@ -10,6 +10,16 @@ CONFIG_FILE="/etc/config/client-guardian" LOG_FILE="/var/log/client-guardian.log" CLIENTS_DB="/tmp/client-guardian-clients.json" ALERTS_QUEUE="/tmp/client-guardian-alerts.json" +CLIENT_GUARDIAN_CACHE="/tmp/secubox/client-guardian.json" + +# Read cached status for fast API responses +get_cached_status() { + if [ -f "$CLIENT_GUARDIAN_CACHE" ]; then + cat "$CLIENT_GUARDIAN_CACHE" + else + echo '{"online":0,"approved":0,"quarantine":0,"banned":0,"threats":0,"total":0,"timestamp":0}' + fi +} # SAFETY LIMITS - prevent accidental mass blocking MAX_BLOCKED_DEVICES=10 @@ -1701,11 +1711,12 @@ get_client() { # Main dispatcher case "$1" in list) - echo '{"status":{},"clients":{},"zones":{},"parental":{},"alerts":{},"logs":{"limit":"int","level":"str"},"approve_client":{"mac":"str","name":"str","zone":"str","notes":"str"},"ban_client":{"mac":"str","reason":"str"},"quarantine_client":{"mac":"str"},"update_client":{"section":"str","name":"str","zone":"str","notes":"str","daily_quota":"int","static_ip":"str"},"update_zone":{"id":"str","name":"str","bandwidth_limit":"int","content_filter":"str"},"send_test_alert":{"type":"str"},"get_policy":{},"set_policy":{"policy":"str","auto_approve":"bool","session_timeout":"int"},"get_client":{"mac":"str"},"sync_zones":{},"list_profiles":{},"apply_profile":{"profile_id":"str","auto_refresh":"str","refresh_interval":"str","threat_enabled":"str","auto_ban_threshold":"str","auto_quarantine_threshold":"str"},"clear_rules":{},"safety_status":{}}' + echo '{"status":{},"status_cached":{},"clients":{},"zones":{},"parental":{},"alerts":{},"logs":{"limit":"int","level":"str"},"approve_client":{"mac":"str","name":"str","zone":"str","notes":"str"},"ban_client":{"mac":"str","reason":"str"},"quarantine_client":{"mac":"str"},"update_client":{"section":"str","name":"str","zone":"str","notes":"str","daily_quota":"int","static_ip":"str"},"update_zone":{"id":"str","name":"str","bandwidth_limit":"int","content_filter":"str"},"send_test_alert":{"type":"str"},"get_policy":{},"set_policy":{"policy":"str","auto_approve":"bool","session_timeout":"int"},"get_client":{"mac":"str"},"sync_zones":{},"list_profiles":{},"apply_profile":{"profile_id":"str","auto_refresh":"str","refresh_interval":"str","threat_enabled":"str","auto_ban_threshold":"str","auto_quarantine_threshold":"str"},"clear_rules":{},"safety_status":{}}' ;; call) case "$2" in status) get_status ;; + status_cached) get_cached_status ;; clients) get_clients ;; zones) get_zones ;; parental) get_parental ;; diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard index 57107dcd..b33bedf4 100755 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard @@ -7,12 +7,22 @@ . /usr/share/libubox/jshn.sh SECCUBOX_LOG="/usr/sbin/secubox-log" +CROWDSEC_CACHE="/tmp/secubox/crowdsec.json" secubox_log() { [ -x "$SECCUBOX_LOG" ] || return "$SECCUBOX_LOG" --tag "crowdsec" --message "$1" >/dev/null 2>&1 } +# Read cached status for fast API responses +get_cached_status() { + if [ -f "$CROWDSEC_CACHE" ]; then + cat "$CROWDSEC_CACHE" + else + echo '{"running":0,"version":"unknown","alerts":0,"bans":0,"bouncers":0,"machines":0,"timestamp":0}' + fi +} + CSCLI="/usr/bin/cscli" CSCLI_TIMEOUT=10 @@ -2416,7 +2426,7 @@ get_overview() { # Main dispatcher case "$1" in list) - echo '{"get_overview":{},"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"secubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"repair_capi":{},"reset_wizard":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"},"configure_acquisition":{"syslog_enabled":"string","firewall_enabled":"string","ssh_enabled":"string","http_enabled":"string","syslog_path":"string"},"acquisition_config":{},"acquisition_metrics":{},"health_check":{},"capi_metrics":{},"hub_available":{},"install_hub_item":{"item_type":"string","item_name":"string"},"remove_hub_item":{"item_type":"string","item_name":"string"},"get_settings":{},"save_settings":{"enrollment_key":"string","machine_name":"string","auto_enroll":"string"}}' + echo '{"get_overview":{},"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"status_cached":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"secubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"repair_capi":{},"reset_wizard":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"},"configure_acquisition":{"syslog_enabled":"string","firewall_enabled":"string","ssh_enabled":"string","http_enabled":"string","syslog_path":"string"},"acquisition_config":{},"acquisition_metrics":{},"health_check":{},"capi_metrics":{},"hub_available":{},"install_hub_item":{"item_type":"string","item_name":"string"},"remove_hub_item":{"item_type":"string","item_name":"string"},"get_settings":{},"save_settings":{"enrollment_key":"string","machine_name":"string","auto_enroll":"string"}}' ;; call) case "$2" in @@ -2443,6 +2453,9 @@ case "$1" in status) get_status ;; + status_cached) + get_cached_status + ;; ban) read -r input ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null) 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 bf1b1546..f6d7e420 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 @@ -5,10 +5,19 @@ MG_DBFILE="/var/run/mac-guardian/known.db" MG_LOGFILE="/var/log/mac-guardian.log" +MAC_GUARDIAN_CACHE="/tmp/secubox/mac-guardian.json" + +get_cached_status() { + if [ -f "$MAC_GUARDIAN_CACHE" ]; then + cat "$MAC_GUARDIAN_CACHE" + else + echo '{"running":0,"total":0,"trusted":0,"suspect":0,"blocked":0,"unknown":0,"wifi_stations":0,"dhcp_leases":0,"timestamp":0}' + fi +} 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":{}}' + echo '{"status":{},"status_cached":{},"get_clients":{},"get_events":{"count":"int"},"scan":{},"start":{},"stop":{},"restart":{},"trust":{"mac":"str"},"block":{"mac":"str"},"dhcp_status":{},"dhcp_cleanup":{}}' ;; call) case "$2" in @@ -74,6 +83,10 @@ case "$1" in json_dump ;; + status_cached) + get_cached_status + ;; + get_clients) json_init json_add_array "clients" diff --git a/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy b/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy index fb2f799c..d03c3672 100755 --- a/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy +++ b/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy @@ -7,6 +7,16 @@ CONFIG="mitmproxy" LXC_NAME="mitmproxy" LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" +MITMPROXY_CACHE="/tmp/secubox/mitmproxy.json" + +# Read cached status for fast API responses +get_cached_status() { + if [ -f "$MITMPROXY_CACHE" ]; then + cat "$MITMPROXY_CACHE" + else + echo '{"running":0,"threats_today":0,"autobans":0,"pending":0,"wan_enabled":0,"lan_enabled":0,"timestamp":0}' + fi +} uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2"; } @@ -657,7 +667,7 @@ wan_clear() { } list_methods() { cat <<'EOFM' -{"status":{},"settings":{},"save_settings":{"mode":"str","enabled":"bool","proxy_port":"int","web_port":"int","apply_now":"bool","wan_protection_enabled":"bool","wan_interface":"str"},"set_mode":{"mode":"str","apply_now":"bool"},"setup_firewall":{},"clear_firewall":{},"wan_setup":{},"wan_clear":{},"install":{},"start":{},"stop":{},"restart":{},"alerts":{},"threat_stats":{},"clear_alerts":{},"haproxy_enable":{},"haproxy_disable":{},"sync_routes":{}} +{"status":{},"status_cached":{},"settings":{},"save_settings":{"mode":"str","enabled":"bool","proxy_port":"int","web_port":"int","apply_now":"bool","wan_protection_enabled":"bool","wan_interface":"str"},"set_mode":{"mode":"str","apply_now":"bool"},"setup_firewall":{},"clear_firewall":{},"wan_setup":{},"wan_clear":{},"install":{},"start":{},"stop":{},"restart":{},"alerts":{},"threat_stats":{},"clear_alerts":{},"haproxy_enable":{},"haproxy_disable":{},"sync_routes":{}} EOFM } @@ -666,6 +676,7 @@ case "$1" in call) case "$2" in status) get_status ;; + status_cached) get_cached_status ;; settings) get_settings ;; save_settings) save_settings ;; set_mode) set_mode ;; diff --git a/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly b/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly index e777c3e2..cecfd65d 100644 --- a/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly +++ b/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly @@ -7,6 +7,15 @@ CONFIG="network-anomaly" STATE_DIR="/var/lib/network-anomaly" ALERTS_FILE="$STATE_DIR/alerts.json" BASELINE_FILE="$STATE_DIR/baseline.json" +NETDIAG_CACHE="/tmp/secubox/netdiag.json" + +get_cached_status() { + if [ -f "$NETDIAG_CACHE" ]; then + cat "$NETDIAG_CACHE" + else + echo '{"enabled":0,"daemon_running":0,"alert_count":0,"unacked_count":0,"localai_status":"offline","timestamp":0}' + fi +} log_info() { logger -t network-anomaly-rpcd "$*"; } @@ -17,6 +26,7 @@ case "$1" in cat <<'EOF' { "status": {}, + "status_cached": {}, "get_alerts": {"limit": 50}, "get_stats": {}, "run": {}, @@ -67,6 +77,10 @@ EOF EOF ;; + status_cached) + get_cached_status + ;; + get_alerts) read -r input limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) diff --git a/package/secubox/luci-app-secubox-netifyd/root/usr/libexec/rpcd/luci.secubox-netifyd b/package/secubox/luci-app-secubox-netifyd/root/usr/libexec/rpcd/luci.secubox-netifyd index c464850b..6b0ad861 100755 --- a/package/secubox/luci-app-secubox-netifyd/root/usr/libexec/rpcd/luci.secubox-netifyd +++ b/package/secubox/luci-app-secubox-netifyd/root/usr/libexec/rpcd/luci.secubox-netifyd @@ -15,6 +15,16 @@ SOCKET_DUMP="/run/netifyd/sink-request.json" LOG_FILE="/var/log/secubox-netifyd.log" FLOW_CACHE="/tmp/netifyd-flows.json" STATS_CACHE="/tmp/netifyd-stats.json" +NETIFYD_CACHE="/tmp/secubox/netifyd.json" + +# Read cached status for fast API responses +get_cached_status() { + if [ -f "$NETIFYD_CACHE" ]; then + cat "$NETIFYD_CACHE" + else + echo '{"running":0,"version":"","flows":0,"devices":0,"dns_cache":0,"rx_bytes":0,"tx_bytes":0,"rx_packets":0,"tx_packets":0,"timestamp":0}' + fi +} NETIFYD_SINK_CONF="/etc/netifyd.d/secubox-sink.conf" NETIFYD_PLUGIN_LIBDIR="/usr/lib/netifyd" NETIFYD_PLUGIN_CONF_DIR="/etc/netifyd/plugins.d" @@ -1033,6 +1043,7 @@ case "$1" in cat <<'EOF' { "get_service_status": {}, + "status_cached": {}, "get_netifyd_status": {}, "get_realtime_flows": {}, "get_flow_statistics": {}, @@ -1065,6 +1076,7 @@ EOF call) case "$2" in get_service_status) get_service_status ;; + status_cached) get_cached_status ;; get_netifyd_status) get_netifyd_status ;; get_realtime_flows) get_realtime_flows ;; get_flow_statistics) get_flow_statistics ;; diff --git a/package/secubox/secubox-core/root/usr/sbin/secubox-core b/package/secubox/secubox-core/root/usr/sbin/secubox-core index 6e315f0a..b842cc55 100755 --- a/package/secubox/secubox-core/root/usr/sbin/secubox-core +++ b/package/secubox/secubox-core/root/usr/sbin/secubox-core @@ -16,11 +16,10 @@ PID_FILE="/var/run/secubox/core.pid" STATE_DIR="/var/run/secubox" WATCHDOG_STATE="/var/run/secubox/watchdog.json" -# LED paths for MochaBin (RGB LEDs: led1, led2, led3 + mmc0) +# LED paths for MochaBin (RGB LEDs: led1, led2, led3) # led1: Global health status (green=ok, yellow=degraded, red=critical) # led2: Security threat level (green=safe, red=attack) # led3: Global capacity meter (CPU + Network) -# mmc0: Classic heartbeat when states are stable LED_GREEN1="/sys/class/leds/green:led1" LED_GREEN2="/sys/class/leds/green:led2" LED_GREEN3="/sys/class/leds/green:led3" @@ -30,7 +29,6 @@ LED_RED3="/sys/class/leds/red:led3" LED_BLUE1="/sys/class/leds/blue:led1" LED_BLUE2="/sys/class/leds/blue:led2" LED_BLUE3="/sys/class/leds/blue:led3" -LED_MMC0="/sys/class/leds/mmc0::" # Legacy aliases for compatibility LED_GREEN="$LED_GREEN1" LED_RED="$LED_RED1" @@ -55,35 +53,12 @@ led_init() { echo none > "$led/trigger" 2>/dev/null echo 0 > "$led/brightness" 2>/dev/null done - # mmc0 LED: start with heartbeat trigger (stable state) - if [ -d "$LED_MMC0" ]; then - echo heartbeat > "$LED_MMC0/trigger" 2>/dev/null - fi - log debug "LED enabled: led1=health, led2=security, led3=capacity, mmc0=heartbeat" + log debug "LED enabled: led1=health, led2=security, led3=capacity" else log debug "LED heartbeat disabled (no compatible LEDs)" fi } -# mmc0 classic heartbeat control -# When states are stable: heartbeat trigger (classic pulse) -# When states change: rapid blink to indicate activity -led_mmc0_heartbeat() { - [ -d "$LED_MMC0" ] || return 0 - local stable="$1" # 1=stable, 0=changing - - if [ "$stable" = "1" ]; then - # Classic heartbeat - stable state - local current=$(cat "$LED_MMC0/trigger" 2>/dev/null) - [ "$current" != "heartbeat" ] && echo heartbeat > "$LED_MMC0/trigger" 2>/dev/null - else - # Rapid blink - state change detected - echo timer > "$LED_MMC0/trigger" 2>/dev/null - echo 50 > "$LED_MMC0/delay_on" 2>/dev/null - echo 50 > "$LED_MMC0/delay_off" 2>/dev/null - fi -} - # Set all colors for a specific LED (1, 2, or 3) led_set_rgb() { local led_num="$1" # 1, 2, or 3 @@ -199,91 +174,423 @@ led_pulse() { ( sleep 1 && led_set "$led" 0 ) & } -# Security threat level for LED2 -# Reads from CrowdSec alerts and mitmproxy threats -THREAT_CACHE_FILE="/tmp/secubox/threat_level" +# ============================================================================ +# Double-Buffer Status Cache System +# Background collector writes to cache files; consumers read instantly +# ============================================================================ -get_threat_level() { - # Check cache age (refresh every 10s) - local cache_age=999 - if [ -f "$THREAT_CACHE_FILE" ]; then - local now=$(date +%s) - local mtime=$(stat -c %Y "$THREAT_CACHE_FILE" 2>/dev/null || echo 0) - cache_age=$((now - mtime)) - fi +# Cache file paths +CACHE_DIR="/tmp/secubox" +HEALTH_CACHE="$CACHE_DIR/health.json" +THREAT_CACHE="$CACHE_DIR/threat.json" +CAPACITY_CACHE="$CACHE_DIR/capacity.json" - if [ "$cache_age" -lt 10 ]; then - cat "$THREAT_CACHE_FILE" 2>/dev/null || echo 0 - return - fi +# Extended cache files for multi-package support +CROWDSEC_CACHE="$CACHE_DIR/crowdsec.json" +MITMPROXY_CACHE="$CACHE_DIR/mitmproxy.json" +NETIFYD_CACHE="$CACHE_DIR/netifyd.json" +CLIENT_GUARDIAN_CACHE="$CACHE_DIR/client-guardian.json" +MAC_GUARDIAN_CACHE="$CACHE_DIR/mac-guardian.json" +NETDIAG_CACHE="$CACHE_DIR/netdiag.json" - # Count recent threats (last 5 minutes) - local threats=0 - - # CrowdSec alerts - if [ -f "/var/log/crowdsec.log" ]; then - local cs_alerts=$(tail -100 /var/log/crowdsec.log 2>/dev/null | grep -c "alert" || echo 0) - threats=$((threats + cs_alerts)) - fi - - # CrowdSec decisions (active bans) - if command -v cscli >/dev/null 2>&1; then - local bans=$(cscli decisions list -o raw 2>/dev/null | wc -l || echo 0) - threats=$((threats + bans / 2)) - fi - - # mitmproxy threats today - local mitm_threats=$(uci -q get mitmproxy.stats.threats_today 2>/dev/null || echo 0) - threats=$((threats + mitm_threats / 10)) - - # Normalize to 0-100 - [ "$threats" -gt 100 ] && threats=100 - echo "$threats" > "$THREAT_CACHE_FILE" - echo "$threats" +# Fast cache readers (no subprocess, instant return) +get_health_score() { + jsonfilter -i "$HEALTH_CACHE" -e '@.score' 2>/dev/null || echo 100 } -# Get global health score from services -HEALTH_CACHE_FILE="/tmp/secubox/health_score" +get_threat_level() { + jsonfilter -i "$THREAT_CACHE" -e '@.level' 2>/dev/null || echo 0 +} -get_health_score() { - # Check cache age (refresh every 15s) - local cache_age=999 - if [ -f "$HEALTH_CACHE_FILE" ]; then - local now=$(date +%s) - local mtime=$(stat -c %Y "$HEALTH_CACHE_FILE" 2>/dev/null || echo 0) - cache_age=$((now - mtime)) - fi +get_capacity() { + jsonfilter -i "$CAPACITY_CACHE" -e '@.combined' 2>/dev/null || echo 0 +} - if [ "$cache_age" -lt 15 ]; then - cat "$HEALTH_CACHE_FILE" 2>/dev/null || echo 100 - return - fi +# ============================================================================ +# Status Collectors (called by background loop) +# ============================================================================ +# Collect health data (every 15s) +_collect_health() { local score=100 local penalty=0 + local services_ok=0 + local services_total=0 # Check critical services - pgrep haproxy >/dev/null 2>&1 || penalty=$((penalty + 20)) - pgrep crowdsec >/dev/null 2>&1 || penalty=$((penalty + 15)) - lxc-info -n haproxy 2>/dev/null | grep -q RUNNING || penalty=$((penalty + 20)) + services_total=$((services_total + 1)) + if pgrep haproxy >/dev/null 2>&1; then + services_ok=$((services_ok + 1)) + else + penalty=$((penalty + 20)) + fi + + services_total=$((services_total + 1)) + if pgrep crowdsec >/dev/null 2>&1; then + services_ok=$((services_ok + 1)) + else + penalty=$((penalty + 15)) + fi + + services_total=$((services_total + 1)) + if lxc-info -n haproxy 2>/dev/null | grep -q RUNNING; then + services_ok=$((services_ok + 1)) + else + penalty=$((penalty + 20)) + fi # Check system resources local mem_free=$(grep MemAvailable /proc/meminfo 2>/dev/null | awk '{print $2}') local mem_total=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}') + local mem_pct=0 if [ -n "$mem_free" ] && [ -n "$mem_total" ] && [ "$mem_total" -gt 0 ]; then - local mem_pct=$((mem_free * 100 / mem_total)) + mem_pct=$((mem_free * 100 / mem_total)) [ "$mem_pct" -lt 10 ] && penalty=$((penalty + 25)) [ "$mem_pct" -lt 20 ] && penalty=$((penalty + 10)) fi # Check disk space local disk_pct=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%') - [ -n "$disk_pct" ] && [ "$disk_pct" -gt 90 ] && penalty=$((penalty + 20)) + [ -z "$disk_pct" ] && disk_pct=0 + [ "$disk_pct" -gt 90 ] && penalty=$((penalty + 20)) score=$((100 - penalty)) [ "$score" -lt 0 ] && score=0 - echo "$score" > "$HEALTH_CACHE_FILE" - echo "$score" + + printf '{"score":%d,"services_ok":%d,"services_total":%d,"mem_free_pct":%d,"disk_used_pct":%d,"timestamp":%d}\n' \ + "$score" "$services_ok" "$services_total" "$mem_pct" "$disk_pct" "$(date +%s)" +} + +# Collect threat data (every 10s) +_collect_threat() { + local threats=0 + local cs_alerts=0 + local bans=0 + local mitm_threats=0 + + # CrowdSec alerts + if [ -f "/var/log/crowdsec.log" ]; then + cs_alerts=$(tail -100 /var/log/crowdsec.log 2>/dev/null | grep -c "alert" || echo 0) + threats=$((threats + cs_alerts)) + fi + + # CrowdSec decisions (active bans) + if command -v cscli >/dev/null 2>&1; then + bans=$(cscli decisions list -o raw 2>/dev/null | wc -l || echo 0) + threats=$((threats + bans / 2)) + fi + + # mitmproxy threats today + mitm_threats=$(uci -q get mitmproxy.stats.threats_today 2>/dev/null || echo 0) + threats=$((threats + mitm_threats / 10)) + + # Normalize to 0-100 + [ "$threats" -gt 100 ] && threats=100 + + printf '{"level":%d,"crowdsec_alerts":%d,"bans":%d,"mitm_threats":%d,"timestamp":%d}\n' \ + "$threats" "$cs_alerts" "$bans" "$mitm_threats" "$(date +%s)" +} + +# Collect capacity data (every 3s) +_collect_capacity() { + # CPU Load (0-100%) + local load=$(cat /proc/loadavg 2>/dev/null | cut -d' ' -f1) + local ncpu=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || echo 4) + local load_int=$(echo "$load" | cut -d. -f1) + local load_dec=$(echo "$load" | cut -d. -f2 | cut -c1-2) + [ -z "$load_dec" ] && load_dec=0 + local cpu_pct=$(( (load_int * 100 + load_dec) * 100 / ncpu / 100 )) + [ "$cpu_pct" -gt 100 ] && cpu_pct=100 + + # Network Activity (0-100%) + local net_now=0 + for iface in /sys/class/net/*/statistics/rx_bytes; do + [ -f "$iface" ] || continue + local rx=$(cat "$iface" 2>/dev/null || echo 0) + local tx=$(cat "${iface%rx_bytes}tx_bytes" 2>/dev/null || echo 0) + net_now=$((net_now + rx + tx)) + done + + local net_prev=$(cat "$NET_PREV_FILE" 2>/dev/null || echo "$net_now") + echo "$net_now" > "$NET_PREV_FILE" + local net_delta=$((net_now - net_prev)) + [ "$net_delta" -lt 0 ] && net_delta=0 + + # Convert to percentage (scale: 10MB/s = 100%) + local net_pct=$((net_delta * 100 / 15728640)) + [ "$net_pct" -gt 100 ] && net_pct=100 + + # Combined: weighted average (60% CPU, 40% network) + local combined=$(( (cpu_pct * 60 + net_pct * 40) / 100 )) + [ "$combined" -gt 100 ] && combined=100 + + printf '{"cpu_pct":%d,"net_pct":%d,"combined":%d,"timestamp":%d}\n' \ + "$cpu_pct" "$net_pct" "$combined" "$(date +%s)" +} + +# ============================================================================ +# Extended Package Collectors (for multi-package cache) +# ============================================================================ + +# Collect CrowdSec data (every 30s) +_collect_crowdsec() { + local running=0 + local version="" alerts=0 bans=0 bouncers=0 machines=0 + + if pgrep crowdsec >/dev/null 2>&1; then + running=1 + version=$(cscli version 2>/dev/null | head -1 | awk '{print $NF}' || echo "unknown") + + # Alerts count + local alerts_json=$(cscli alerts list -o json 2>/dev/null | head -c 50000) + if [ -n "$alerts_json" ] && [ "$alerts_json" != "null" ]; then + alerts=$(echo "$alerts_json" | grep -c '"id":' || echo 0) + fi + + # Decisions/bans count + local decisions_json=$(cscli decisions list -o json 2>/dev/null | head -c 10000) + if [ -n "$decisions_json" ] && [ "$decisions_json" != "null" ]; then + bans=$(echo "$decisions_json" | grep -c '"id":' || echo 0) + fi + + # Bouncers count + bouncers=$(cscli bouncers list -o json 2>/dev/null | grep -c '"name":' || echo 0) + + # Machines count + machines=$(cscli machines list -o json 2>/dev/null | grep -c '"machineId":' || echo 0) + fi + + printf '{"running":%d,"version":"%s","alerts":%d,"bans":%d,"bouncers":%d,"machines":%d,"timestamp":%d}\n' \ + "$running" "$version" "$alerts" "$bans" "$bouncers" "$machines" "$(date +%s)" +} + +# Collect mitmproxy data (every 30s) +_collect_mitmproxy() { + local running=0 threats_today=0 autobans=0 pending=0 + local wan_enabled=0 lan_enabled=0 + + # Check LXC container status + if lxc-info -n mitmproxy -s 2>/dev/null | grep -q RUNNING; then + running=1 + fi + + # Read threat stats + if [ -f "/srv/mitmproxy/threats.log" ]; then + threats_today=$(wc -l < /srv/mitmproxy/threats.log 2>/dev/null || echo 0) + fi + + # Autoban stats + if [ -f "/srv/mitmproxy/autoban-processed.log" ]; then + autobans=$(wc -l < /srv/mitmproxy/autoban-processed.log 2>/dev/null || echo 0) + fi + if [ -f "/srv/mitmproxy/autoban-requests.log" ]; then + pending=$(wc -l < /srv/mitmproxy/autoban-requests.log 2>/dev/null || echo 0) + fi + + # Check firewall status + wan_enabled=$(uci -q get mitmproxy.main.wan_enabled 2>/dev/null || echo 0) + lan_enabled=$(uci -q get mitmproxy.main.enabled 2>/dev/null || echo 0) + + printf '{"running":%d,"threats_today":%d,"autobans":%d,"pending":%d,"wan_enabled":%d,"lan_enabled":%d,"timestamp":%d}\n' \ + "$running" "$threats_today" "$autobans" "$pending" "$wan_enabled" "$lan_enabled" "$(date +%s)" +} + +# Collect netifyd data (every 15s) +_collect_netifyd() { + local running=0 version="" flows=0 devices=0 dns_cache=0 + local rx_bytes=0 tx_bytes=0 rx_packets=0 tx_packets=0 + + if pidof netifyd >/dev/null 2>&1; then + running=1 + version=$(netifyd -V 2>/dev/null | head -1 || echo "unknown") + + # Parse status.json if available + local status_file="/var/run/netifyd/status.json" + if [ -f "$status_file" ]; then + flows=$(jsonfilter -i "$status_file" -e '@.flows' 2>/dev/null || echo 0) + devices=$(jsonfilter -i "$status_file" -e '@.devices' 2>/dev/null || echo 0) + dns_cache=$(jsonfilter -i "$status_file" -e '@.dns_hint_cache' 2>/dev/null || echo 0) + fi + fi + + # Interface stats from sysfs + for iface in eth0 eth1 lan wan; do + if [ -f "/sys/class/net/$iface/statistics/rx_bytes" ]; then + rx_bytes=$((rx_bytes + $(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0))) + tx_bytes=$((tx_bytes + $(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0))) + rx_packets=$((rx_packets + $(cat /sys/class/net/$iface/statistics/rx_packets 2>/dev/null || echo 0))) + tx_packets=$((tx_packets + $(cat /sys/class/net/$iface/statistics/tx_packets 2>/dev/null || echo 0))) + fi + done + + printf '{"running":%d,"version":"%s","flows":%d,"devices":%d,"dns_cache":%d,"rx_bytes":%d,"tx_bytes":%d,"rx_packets":%d,"tx_packets":%d,"timestamp":%d}\n' \ + "$running" "$version" "$flows" "$devices" "$dns_cache" "$rx_bytes" "$tx_bytes" "$rx_packets" "$tx_packets" "$(date +%s)" +} + +# Collect client-guardian data (every 30s) +_collect_client_guardian() { + local online=0 approved=0 quarantine=0 banned=0 threats=0 + + # Count ARP entries (online clients) + online=$(ip neigh show 2>/dev/null | grep -cE 'REACHABLE|STALE|DELAY' || echo 0) + + # Count by status from UCI + local uci_clients=$(uci show client-guardian 2>/dev/null | grep -c "\.mac=" || echo 0) + approved=$(uci show client-guardian 2>/dev/null | grep "status='approved'" | wc -l || echo 0) + quarantine=$(uci show client-guardian 2>/dev/null | grep "status='quarantine'" | wc -l || echo 0) + banned=$(uci show client-guardian 2>/dev/null | grep "status='banned'" | wc -l || echo 0) + + # Count threats from log + if [ -f "/var/log/client-guardian.log" ]; then + threats=$(grep -c "THREAT\|ALERT" /var/log/client-guardian.log 2>/dev/null || echo 0) + fi + + printf '{"online":%d,"approved":%d,"quarantine":%d,"banned":%d,"threats":%d,"total":%d,"timestamp":%d}\n' \ + "$online" "$approved" "$quarantine" "$banned" "$threats" "$uci_clients" "$(date +%s)" +} + +# Collect mac-guardian data (every 30s) +_collect_mac_guardian() { + local running=0 total=0 trusted=0 suspect=0 blocked=0 unknown=0 + local wifi_stations=0 dhcp_leases=0 + + # Service status + pgrep mac-guardian >/dev/null 2>&1 && running=1 + + # Parse known.db + local db_file="/var/run/mac-guardian/known.db" + if [ -f "$db_file" ] && [ -s "$db_file" ]; then + total=$(wc -l < "$db_file" 2>/dev/null) + [ -z "$total" ] && total=0 + trusted=$(grep -c '|trusted|' "$db_file" 2>/dev/null) || trusted=0 + suspect=$(grep -c '|suspect|' "$db_file" 2>/dev/null) || suspect=0 + blocked=$(grep -c '|blocked|' "$db_file" 2>/dev/null) || blocked=0 + unknown=$(grep -c '|unknown|' "$db_file" 2>/dev/null) || unknown=0 + fi + + # WiFi stations + for iface in wlan0 wlan1 phy0-ap0 phy1-ap0; do + local count + count=$(iwinfo "$iface" assoclist 2>/dev/null | grep -c "dBm") || count=0 + [ -z "$count" ] && count=0 + wifi_stations=$((wifi_stations + count)) + done + + # DHCP leases + if [ -f "/tmp/dhcp.leases" ]; then + dhcp_leases=$(wc -l < /tmp/dhcp.leases 2>/dev/null) + [ -z "$dhcp_leases" ] && dhcp_leases=0 + fi + + printf '{"running":%d,"total":%d,"trusted":%d,"suspect":%d,"blocked":%d,"unknown":%d,"wifi_stations":%d,"dhcp_leases":%d,"timestamp":%d}\n' \ + "$running" "$total" "$trusted" "$suspect" "$blocked" "$unknown" "$wifi_stations" "$dhcp_leases" "$(date +%s)" +} + +# Collect netdiag data (every 60s) +_collect_netdiag() { + local total_ifaces=0 up_ifaces=0 errors=0 warnings=0 + local cpu_temp=0 soc_temp=0 + local total_rx=0 total_tx=0 + + # Aggregate interface stats (simpler than building JSON array) + for iface_path in /sys/class/net/*/; do + local iface=$(basename "$iface_path") + [ "$iface" = "lo" ] && continue + [ ! -f "$iface_path/statistics/rx_bytes" ] && continue + + total_ifaces=$((total_ifaces + 1)) + + local operstate=$(cat "$iface_path/operstate" 2>/dev/null) + [ "$operstate" = "up" ] && up_ifaces=$((up_ifaces + 1)) + + local rx=$(cat "$iface_path/statistics/rx_bytes" 2>/dev/null) + local tx=$(cat "$iface_path/statistics/tx_bytes" 2>/dev/null) + local rx_err=$(cat "$iface_path/statistics/rx_errors" 2>/dev/null) + local tx_err=$(cat "$iface_path/statistics/tx_errors" 2>/dev/null) + + [ -n "$rx" ] && total_rx=$((total_rx + rx)) + [ -n "$tx" ] && total_tx=$((total_tx + tx)) + [ -n "$rx_err" ] && errors=$((errors + rx_err)) + [ -n "$tx_err" ] && errors=$((errors + tx_err)) + done + + # Temperature + local temp_raw + if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then + temp_raw=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null) + [ -n "$temp_raw" ] && cpu_temp=$((temp_raw / 1000)) + fi + if [ -f "/sys/class/thermal/thermal_zone1/temp" ]; then + temp_raw=$(cat /sys/class/thermal/thermal_zone1/temp 2>/dev/null) + [ -n "$temp_raw" ] && soc_temp=$((temp_raw / 1000)) + fi + + # Count dmesg errors + warnings=$(dmesg 2>/dev/null | grep -ciE 'error|fail|timeout') || warnings=0 + [ -z "$warnings" ] && warnings=0 + + printf '{"total_ifaces":%d,"up_ifaces":%d,"errors":%d,"warnings":%d,"total_rx":%d,"total_tx":%d,"cpu_temp":%d,"soc_temp":%d,"timestamp":%d}\n' \ + "$total_ifaces" "$up_ifaces" "$errors" "$warnings" "$total_rx" "$total_tx" "$cpu_temp" "$soc_temp" "$(date +%s)" +} + +# Background status collector loop +# Writes to cache files using atomic mv for consistency +status_collector_loop() { + local counter=0 + + # Ensure cache directory exists + mkdir -p "$CACHE_DIR" + + log debug "Status collector starting" + + while true; do + # Capacity (fastest - every 3s) + _collect_capacity > "$CAPACITY_CACHE.tmp" 2>/dev/null + mv "$CAPACITY_CACHE.tmp" "$CAPACITY_CACHE" 2>/dev/null + + # Threat (every 9s = counter % 3 == 0) + if [ $((counter % 3)) -eq 0 ]; then + _collect_threat > "$THREAT_CACHE.tmp" 2>/dev/null + mv "$THREAT_CACHE.tmp" "$THREAT_CACHE" 2>/dev/null + fi + + # Health (every 15s = counter % 5 == 0) + if [ $((counter % 5)) -eq 0 ]; then + _collect_health > "$HEALTH_CACHE.tmp" 2>/dev/null + mv "$HEALTH_CACHE.tmp" "$HEALTH_CACHE" 2>/dev/null + + # Netifyd (every 15s - same as health) + _collect_netifyd > "$NETIFYD_CACHE.tmp" 2>/dev/null + mv "$NETIFYD_CACHE.tmp" "$NETIFYD_CACHE" 2>/dev/null + fi + + # CrowdSec, mitmproxy, client-guardian, mac-guardian (every 30s = counter % 10 == 0) + if [ $((counter % 10)) -eq 0 ]; then + _collect_crowdsec > "$CROWDSEC_CACHE.tmp" 2>/dev/null + mv "$CROWDSEC_CACHE.tmp" "$CROWDSEC_CACHE" 2>/dev/null + + _collect_mitmproxy > "$MITMPROXY_CACHE.tmp" 2>/dev/null + mv "$MITMPROXY_CACHE.tmp" "$MITMPROXY_CACHE" 2>/dev/null + + _collect_client_guardian > "$CLIENT_GUARDIAN_CACHE.tmp" 2>/dev/null + mv "$CLIENT_GUARDIAN_CACHE.tmp" "$CLIENT_GUARDIAN_CACHE" 2>/dev/null + + _collect_mac_guardian > "$MAC_GUARDIAN_CACHE.tmp" 2>/dev/null + mv "$MAC_GUARDIAN_CACHE.tmp" "$MAC_GUARDIAN_CACHE" 2>/dev/null + fi + + # Netdiag (every 60s = counter % 20 == 0) + if [ $((counter % 20)) -eq 0 ]; then + _collect_netdiag > "$NETDIAG_CACHE.tmp" 2>/dev/null + mv "$NETDIAG_CACHE.tmp" "$NETDIAG_CACHE" 2>/dev/null + fi + + counter=$((counter + 1)) + # Reset counter to prevent overflow + [ "$counter" -ge 1000 ] && counter=0 + + sleep 3 + done } # Heartbeat function - 3 dedicated LEDs @@ -792,46 +1099,15 @@ led_event_pulse() { # led1: Global health status # led2: Security threat level # led3: Global capacity meter (CPU + Network) with event pulse overlay -# mmc0: Classic heartbeat when states stable, rapid blink on changes +# Reads from pre-computed cache files (no subprocess calls) led_heartbeat_loop() { local status_file="/tmp/secubox/led-status" echo "healthy" > "$status_file" - local prev_health=0 - local prev_threat=0 - local stable_counter=0 - while true; do local status=$(cat "$status_file" 2>/dev/null || echo "healthy") - # Get current states - local cur_health=$(get_health_score) - local cur_threat=$(get_threat_level) - - # Detect state changes - local health_delta=$((cur_health - prev_health)) - [ "$health_delta" -lt 0 ] && health_delta=$((-health_delta)) - local threat_delta=$((cur_threat - prev_threat)) - [ "$threat_delta" -lt 0 ] && threat_delta=$((-threat_delta)) - - # Check if states are stable (delta < 5) - if [ "$health_delta" -lt 5 ] && [ "$threat_delta" -lt 5 ]; then - stable_counter=$((stable_counter + 1)) - else - stable_counter=0 - fi - - # mmc0: classic heartbeat when stable for 3+ cycles - if [ "$stable_counter" -ge 3 ]; then - led_mmc0_heartbeat 1 # Stable - else - led_mmc0_heartbeat 0 # Changing - fi - - prev_health=$cur_health - prev_threat=$cur_threat - - # Update RGB LEDs + # Update RGB LEDs (reads from cache - instant) led_heartbeat "$status" # Check for event pulse first (overrides capacity momentarily) @@ -852,6 +1128,17 @@ daemon_mode() { # Write PID echo $$ > "$PID_FILE" + # Ensure cache directory exists + mkdir -p "$CACHE_DIR" + + # Start background status collector (populates cache files) + status_collector_loop & + STATUS_COLLECTOR_PID=$! + log debug "Status collector started (PID: $STATUS_COLLECTOR_PID)" + + # Wait for initial cache population + sleep 1 + # Initialize LED heartbeat led_init led_heartbeat boot