feat(secubox-core): Add 4-LED dashboard with dedicated functions
LED assignment for MochaBin: - led1: Global health status (green/yellow/red with pulse variations) - led2: Security threat meter (CrowdSec + mitmproxy activity) - led3: Global capacity (CPU + Network combined, color gradient) - mmc0: Classic heartbeat when stable, rapid blink on state changes Features: - Fast 1.5s heartbeat loop for reactive visual feedback - Health score from services (HAProxy, CrowdSec) + memory/disk - Threat level from CrowdSec alerts and mitmproxy stats - Combined CPU load + network throughput capacity meter - Event pulse system for config/task/alert notifications - State change detection for mmc0 stability indicator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
52d982218a
commit
29ba711acc
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-haproxy
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=23
|
||||
PKG_RELEASE:=24
|
||||
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
@ -858,6 +858,221 @@ cmd_cert_list() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if we have local BIND for a zone
|
||||
has_local_bind_zone() {
|
||||
local domain="$1"
|
||||
local zone_file="/etc/bind/zones/${domain}.zone"
|
||||
[ -f "$zone_file" ] && command -v rndc >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Add TXT record to local BIND zone
|
||||
bind_add_txt() {
|
||||
local subdomain="$1"
|
||||
local zone="$2"
|
||||
local value="$3"
|
||||
local zone_file="/etc/bind/zones/${zone}.zone"
|
||||
|
||||
[ ! -f "$zone_file" ] && return 1
|
||||
|
||||
# Update serial (format: YYYYMMDDNN)
|
||||
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
||||
local today=$(date +%Y%m%d)
|
||||
local new_serial
|
||||
|
||||
if [ -n "$current_serial" ]; then
|
||||
local serial_date="${current_serial:0:8}"
|
||||
local serial_num="${current_serial:8:2}"
|
||||
if [ "$serial_date" = "$today" ]; then
|
||||
serial_num=$((10#$serial_num + 1))
|
||||
new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
||||
else
|
||||
new_serial="${today}01"
|
||||
fi
|
||||
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
||||
fi
|
||||
|
||||
# Add TXT record (before the ; IPv6 comment or at end)
|
||||
local record="$subdomain IN TXT \"$value\""
|
||||
if grep -q "; IPv6" "$zone_file"; then
|
||||
sed -i "/; IPv6/i $record" "$zone_file"
|
||||
else
|
||||
echo "$record" >> "$zone_file"
|
||||
fi
|
||||
|
||||
# Reload BIND
|
||||
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null
|
||||
log_info "Added TXT record: $subdomain.$zone -> $value"
|
||||
}
|
||||
|
||||
# Remove TXT record from local BIND zone
|
||||
bind_remove_txt() {
|
||||
local subdomain="$1"
|
||||
local zone="$2"
|
||||
local value="$3"
|
||||
local zone_file="/etc/bind/zones/${zone}.zone"
|
||||
|
||||
[ ! -f "$zone_file" ] && return 1
|
||||
|
||||
# Remove the specific TXT record
|
||||
sed -i "/$subdomain IN TXT \"$value\"/d" "$zone_file"
|
||||
|
||||
# Update serial
|
||||
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
||||
if [ -n "$current_serial" ]; then
|
||||
local today=$(date +%Y%m%d)
|
||||
local serial_date="${current_serial:0:8}"
|
||||
local serial_num="${current_serial:8:2}"
|
||||
if [ "$serial_date" = "$today" ]; then
|
||||
serial_num=$((10#$serial_num + 1))
|
||||
else
|
||||
serial_num=1
|
||||
fi
|
||||
local new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
||||
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
||||
fi
|
||||
|
||||
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null
|
||||
log_info "Removed TXT record: $subdomain.$zone"
|
||||
}
|
||||
|
||||
# Get DNS provider configuration for DNS-01 challenge
|
||||
get_dns_provider_env() {
|
||||
local provider=$(uci -q get dns-provider.main.provider)
|
||||
[ -z "$provider" ] && return 1
|
||||
|
||||
case "$provider" in
|
||||
local|bind)
|
||||
# Using local BIND - acme.sh will use our custom hook
|
||||
echo "dns_local_bind"
|
||||
;;
|
||||
gandi)
|
||||
local api_key=$(uci -q get dns-provider.gandi.api_key)
|
||||
[ -z "$api_key" ] && return 1
|
||||
export GANDI_LIVEDNS_KEY="$api_key"
|
||||
echo "dns_gandi_livedns"
|
||||
;;
|
||||
ovh)
|
||||
local app_key=$(uci -q get dns-provider.ovh.app_key)
|
||||
local app_secret=$(uci -q get dns-provider.ovh.app_secret)
|
||||
local consumer_key=$(uci -q get dns-provider.ovh.consumer_key)
|
||||
local endpoint=$(uci -q get dns-provider.ovh.endpoint)
|
||||
[ -z "$app_key" ] || [ -z "$app_secret" ] || [ -z "$consumer_key" ] && return 1
|
||||
export OVH_AK="$app_key"
|
||||
export OVH_AS="$app_secret"
|
||||
export OVH_CK="$consumer_key"
|
||||
export OVH_END="${endpoint:-ovh-eu}"
|
||||
echo "dns_ovh"
|
||||
;;
|
||||
cloudflare)
|
||||
local api_token=$(uci -q get dns-provider.cloudflare.api_token)
|
||||
[ -z "$api_token" ] && return 1
|
||||
export CF_Token="$api_token"
|
||||
echo "dns_cloudflare"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# Create acme.sh DNS hook for local BIND
|
||||
create_local_dns_hook() {
|
||||
local hook_dir="/etc/acme/dnsapi"
|
||||
ensure_dir "$hook_dir"
|
||||
|
||||
cat > "$hook_dir/dns_local_bind.sh" << 'HOOK'
|
||||
#!/bin/sh
|
||||
# acme.sh DNS hook for local BIND zones
|
||||
|
||||
dns_local_bind_add() {
|
||||
local fulldomain="$1"
|
||||
local txtvalue="$2"
|
||||
|
||||
# Extract zone from domain (e.g., _acme-challenge.gk2.secubox.in -> secubox.in)
|
||||
local zone=$(echo "$fulldomain" | awk -F. '{print $(NF-1)"."$NF}')
|
||||
local subdomain="${fulldomain%.$zone}"
|
||||
|
||||
local zone_file="/etc/bind/zones/${zone}.zone"
|
||||
[ ! -f "$zone_file" ] && { echo "Zone file not found: $zone_file"; return 1; }
|
||||
|
||||
# Update serial
|
||||
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
||||
if [ -n "$current_serial" ]; then
|
||||
local today=$(date +%Y%m%d)
|
||||
local serial_date="${current_serial:0:8}"
|
||||
local serial_num="${current_serial:8:2}"
|
||||
if [ "$serial_date" = "$today" ]; then
|
||||
serial_num=$((10#$serial_num + 1))
|
||||
else
|
||||
serial_num=1
|
||||
fi
|
||||
local new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
||||
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
||||
fi
|
||||
|
||||
# Add TXT record
|
||||
local record="$subdomain IN TXT \"$txtvalue\""
|
||||
if grep -q "; IPv6" "$zone_file"; then
|
||||
sed -i "/; IPv6/i $record" "$zone_file"
|
||||
else
|
||||
echo "$record" >> "$zone_file"
|
||||
fi
|
||||
|
||||
# Reload BIND (use kill -HUP since rndc may not be configured)
|
||||
local bind_pid=$(pidof named 2>/dev/null)
|
||||
if [ -n "$bind_pid" ]; then
|
||||
kill -HUP "$bind_pid" 2>/dev/null
|
||||
fi
|
||||
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null || true
|
||||
echo "Added: $fulldomain TXT $txtvalue"
|
||||
|
||||
# Wait for DNS propagation
|
||||
sleep 5
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_local_bind_rm() {
|
||||
local fulldomain="$1"
|
||||
local txtvalue="$2"
|
||||
|
||||
local zone=$(echo "$fulldomain" | awk -F. '{print $(NF-1)"."$NF}')
|
||||
local subdomain="${fulldomain%.$zone}"
|
||||
local zone_file="/etc/bind/zones/${zone}.zone"
|
||||
|
||||
[ ! -f "$zone_file" ] && return 0
|
||||
|
||||
# Remove the TXT record
|
||||
sed -i "/$subdomain IN TXT \"$txtvalue\"/d" "$zone_file"
|
||||
|
||||
# Update serial
|
||||
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
||||
if [ -n "$current_serial" ]; then
|
||||
local today=$(date +%Y%m%d)
|
||||
local serial_date="${current_serial:0:8}"
|
||||
local serial_num="${current_serial:8:2}"
|
||||
if [ "$serial_date" = "$today" ]; then
|
||||
serial_num=$((10#$serial_num + 1))
|
||||
else
|
||||
serial_num=1
|
||||
fi
|
||||
local new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
||||
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
||||
fi
|
||||
|
||||
# Reload BIND
|
||||
local bind_pid=$(pidof named 2>/dev/null)
|
||||
if [ -n "$bind_pid" ]; then
|
||||
kill -HUP "$bind_pid" 2>/dev/null
|
||||
fi
|
||||
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null || true
|
||||
echo "Removed: $fulldomain TXT $txtvalue"
|
||||
return 0
|
||||
}
|
||||
HOOK
|
||||
chmod +x "$hook_dir/dns_local_bind.sh"
|
||||
}
|
||||
|
||||
cmd_cert_add() {
|
||||
require_root
|
||||
load_config
|
||||
@ -865,6 +1080,17 @@ cmd_cert_add() {
|
||||
local domain="$1"
|
||||
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
|
||||
|
||||
# Detect if this is a wildcard certificate request
|
||||
local is_wildcard=0
|
||||
local base_domain="$domain"
|
||||
case "$domain" in
|
||||
\*.*)
|
||||
is_wildcard=1
|
||||
base_domain="${domain#\*.}"
|
||||
log_info "Wildcard certificate requested for $domain"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Ensure HAProxy config has ACME backend (for webroot mode)
|
||||
log_info "Ensuring HAProxy config is up to date..."
|
||||
generate_config
|
||||
@ -886,6 +1112,37 @@ cmd_cert_add() {
|
||||
|
||||
[ -z "$email" ] && { log_error "ACME email not configured. Set in LuCI > Services > HAProxy > Settings"; return 1; }
|
||||
|
||||
# For wildcard certs, DNS-01 challenge is REQUIRED
|
||||
local dns_plugin=""
|
||||
local use_local_bind=0
|
||||
if [ "$is_wildcard" = "1" ]; then
|
||||
# First check if we have local BIND for this zone
|
||||
# Extract parent zone from domain (e.g., *.gk2.secubox.in -> secubox.in)
|
||||
# Use awk instead of rev which isn't available on OpenWrt
|
||||
local parent_zone=$(echo "$base_domain" | awk -F. '{print $(NF-1)"."$NF}')
|
||||
if has_local_bind_zone "$parent_zone"; then
|
||||
log_info "Local BIND zone found for $parent_zone"
|
||||
use_local_bind=1
|
||||
dns_plugin="dns_local_bind"
|
||||
create_local_dns_hook
|
||||
else
|
||||
# Try external DNS provider
|
||||
dns_plugin=$(get_dns_provider_env)
|
||||
fi
|
||||
|
||||
if [ -z "$dns_plugin" ]; then
|
||||
log_error "DNS provider not configured. Wildcard certificates require DNS-01 challenge."
|
||||
log_error "Options:"
|
||||
log_error " 1. Use local BIND: ensure /etc/bind/zones/<zone>.zone exists"
|
||||
log_error " 2. Configure external DNS: LuCI > Services > DNS Provider"
|
||||
log_error " uci set dns-provider.main.provider='gandi'"
|
||||
log_error " uci set dns-provider.gandi.api_key='YOUR_API_KEY'"
|
||||
log_error " uci commit dns-provider"
|
||||
return 1
|
||||
fi
|
||||
log_info "Using DNS-01 challenge with $dns_plugin"
|
||||
fi
|
||||
|
||||
# Warn about staging mode
|
||||
if [ "$staging" = "1" ]; then
|
||||
log_warn "=========================================="
|
||||
@ -923,56 +1180,77 @@ cmd_cert_add() {
|
||||
"$ACME_SH" --register-account -m "$email" --server letsencrypt $staging_flag --home "$LE_WORKING_DIR" || true
|
||||
fi
|
||||
|
||||
# Setup webroot for ACME challenges (HAProxy routes /.well-known/acme-challenge/ here)
|
||||
local ACME_WEBROOT="/var/www/acme-challenge"
|
||||
ensure_dir "$ACME_WEBROOT/.well-known/acme-challenge"
|
||||
chmod 755 "$ACME_WEBROOT" "$ACME_WEBROOT/.well-known" "$ACME_WEBROOT/.well-known/acme-challenge"
|
||||
|
||||
# Start simple webserver for ACME challenges (if not already running)
|
||||
local ACME_PORT=8402
|
||||
if ! netstat -tln 2>/dev/null | grep -q ":$ACME_PORT "; then
|
||||
log_info "Starting ACME challenge webserver on port $ACME_PORT..."
|
||||
# Use busybox httpd (available on OpenWrt)
|
||||
start-stop-daemon -S -b -x /usr/sbin/httpd -- -p $ACME_PORT -h "$ACME_WEBROOT" -f 2>/dev/null || \
|
||||
busybox httpd -p $ACME_PORT -h "$ACME_WEBROOT" &
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Ensure HAProxy is running with ACME backend
|
||||
if ! lxc_running; then
|
||||
log_info "Starting HAProxy..."
|
||||
/etc/init.d/haproxy start 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Issue certificate using webroot mode (NO HAProxy restart needed!)
|
||||
log_info "Issuing certificate (webroot mode - HAProxy stays running)..."
|
||||
local acme_result=0
|
||||
"$ACME_SH" --issue -d "$domain" \
|
||||
--server letsencrypt \
|
||||
--webroot "$ACME_WEBROOT" \
|
||||
--keylength "$key_type" \
|
||||
$staging_flag \
|
||||
--home "$LE_WORKING_DIR" || acme_result=$?
|
||||
|
||||
if [ "$is_wildcard" = "1" ]; then
|
||||
# DNS-01 challenge for wildcard certificates
|
||||
log_info "Issuing wildcard certificate via DNS-01 challenge..."
|
||||
log_info "This will create a TXT record at _acme-challenge.$base_domain"
|
||||
|
||||
"$ACME_SH" --issue -d "$domain" -d "$base_domain" \
|
||||
--server letsencrypt \
|
||||
--dns "$dns_plugin" \
|
||||
--keylength "$key_type" \
|
||||
$staging_flag \
|
||||
--home "$LE_WORKING_DIR" || acme_result=$?
|
||||
else
|
||||
# HTTP-01 challenge for regular domains (webroot mode)
|
||||
# Setup webroot for ACME challenges (HAProxy routes /.well-known/acme-challenge/ here)
|
||||
local ACME_WEBROOT="/var/www/acme-challenge"
|
||||
ensure_dir "$ACME_WEBROOT/.well-known/acme-challenge"
|
||||
chmod 755 "$ACME_WEBROOT" "$ACME_WEBROOT/.well-known" "$ACME_WEBROOT/.well-known/acme-challenge"
|
||||
|
||||
# Start simple webserver for ACME challenges (if not already running)
|
||||
local ACME_PORT=8402
|
||||
if ! netstat -tln 2>/dev/null | grep -q ":$ACME_PORT "; then
|
||||
log_info "Starting ACME challenge webserver on port $ACME_PORT..."
|
||||
# Use busybox httpd (available on OpenWrt)
|
||||
start-stop-daemon -S -b -x /usr/sbin/httpd -- -p $ACME_PORT -h "$ACME_WEBROOT" -f 2>/dev/null || \
|
||||
busybox httpd -p $ACME_PORT -h "$ACME_WEBROOT" &
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Ensure HAProxy is running with ACME backend
|
||||
if ! lxc_running; then
|
||||
log_info "Starting HAProxy..."
|
||||
/etc/init.d/haproxy start 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Issue certificate using webroot mode (NO HAProxy restart needed!)
|
||||
log_info "Issuing certificate (webroot mode - HAProxy stays running)..."
|
||||
"$ACME_SH" --issue -d "$domain" \
|
||||
--server letsencrypt \
|
||||
--webroot "$ACME_WEBROOT" \
|
||||
--keylength "$key_type" \
|
||||
$staging_flag \
|
||||
--home "$LE_WORKING_DIR" || acme_result=$?
|
||||
fi
|
||||
|
||||
# acme.sh returns 0 on success, 2 on "skip/already valid" - both are OK
|
||||
# Install the certificate to our certs path
|
||||
# For wildcard, use sanitized filename (replace * with _wildcard_)
|
||||
local cert_filename="$domain"
|
||||
case "$domain" in
|
||||
\*.*) cert_filename="_wildcard_.${domain#\*.}" ;;
|
||||
esac
|
||||
|
||||
if [ "$acme_result" -eq 0 ] || [ "$acme_result" -eq 2 ]; then
|
||||
log_info "Installing certificate..."
|
||||
"$ACME_SH" --install-cert -d "$domain" \
|
||||
--home "$LE_WORKING_DIR" \
|
||||
--cert-file "$CERTS_PATH/$domain.crt" \
|
||||
--key-file "$CERTS_PATH/$domain.key" \
|
||||
--fullchain-file "$CERTS_PATH/$domain.fullchain.pem" \
|
||||
--cert-file "$CERTS_PATH/$cert_filename.crt" \
|
||||
--key-file "$CERTS_PATH/$cert_filename.key" \
|
||||
--fullchain-file "$CERTS_PATH/$cert_filename.fullchain.pem" \
|
||||
--reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true
|
||||
|
||||
# HAProxy needs combined file: fullchain + private key
|
||||
log_info "Creating combined PEM for HAProxy..."
|
||||
cat "$CERTS_PATH/$domain.fullchain.pem" "$CERTS_PATH/$domain.key" > "$CERTS_PATH/$domain.pem"
|
||||
chmod 600 "$CERTS_PATH/$domain.pem"
|
||||
cat "$CERTS_PATH/$cert_filename.fullchain.pem" "$CERTS_PATH/$cert_filename.key" > "$CERTS_PATH/$cert_filename.pem"
|
||||
chmod 600 "$CERTS_PATH/$cert_filename.pem"
|
||||
|
||||
# Clean up intermediate files - HAProxy only needs the .pem file
|
||||
rm -f "$CERTS_PATH/$domain.crt" "$CERTS_PATH/$domain.key" "$CERTS_PATH/$domain.fullchain.pem" "$CERTS_PATH/$domain.crt.key" 2>/dev/null
|
||||
rm -f "$CERTS_PATH/$cert_filename.crt" "$CERTS_PATH/$cert_filename.key" "$CERTS_PATH/$cert_filename.fullchain.pem" "$CERTS_PATH/$cert_filename.crt.key" 2>/dev/null
|
||||
|
||||
# Reload HAProxy to pick up new cert
|
||||
log_info "Reloading HAProxy to use new certificate..."
|
||||
@ -980,14 +1258,24 @@ cmd_cert_add() {
|
||||
fi
|
||||
|
||||
# Check if certificate was created
|
||||
if [ ! -f "$CERTS_PATH/$domain.pem" ]; then
|
||||
log_error "Certificate issuance failed. Check:"
|
||||
log_error " 1. Domain $domain points to this server's public IP"
|
||||
log_error " 2. Port 80 is accessible from internet"
|
||||
log_error " 3. HAProxy is running with ACME backend (haproxyctl generate)"
|
||||
if [ ! -f "$CERTS_PATH/$cert_filename.pem" ]; then
|
||||
if [ "$is_wildcard" = "1" ]; then
|
||||
log_error "Wildcard certificate issuance failed. Check:"
|
||||
log_error " 1. DNS provider API credentials are correct"
|
||||
log_error " 2. You have permission to modify DNS for $base_domain"
|
||||
log_error " 3. DNS propagation may take time (try again in a few minutes)"
|
||||
else
|
||||
log_error "Certificate issuance failed. Check:"
|
||||
log_error " 1. Domain $domain points to this server's public IP"
|
||||
log_error " 2. Port 80 is accessible from internet"
|
||||
log_error " 3. HAProxy is running with ACME backend (haproxyctl generate)"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
log_info "Certificate ready: $CERTS_PATH/$domain.pem"
|
||||
log_info "Certificate ready: $CERTS_PATH/$cert_filename.pem"
|
||||
|
||||
# Update domain variable for UCI storage
|
||||
domain="$cert_filename"
|
||||
elif command -v certbot >/dev/null 2>&1; then
|
||||
certbot certonly --standalone -d "$domain" \
|
||||
--email "$email" --agree-tos -n \
|
||||
|
||||
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-core
|
||||
PKG_VERSION:=0.10.0
|
||||
PKG_RELEASE:=12
|
||||
PKG_RELEASE:=13
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
PKG_MAINTAINER:=SecuBox Team
|
||||
|
||||
@ -16,11 +16,28 @@ 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)
|
||||
LED_GREEN="/sys/class/leds/green:led1"
|
||||
LED_RED="/sys/class/leds/red:led1"
|
||||
LED_BLUE="/sys/class/leds/blue:led1"
|
||||
# LED paths for MochaBin (RGB LEDs: led1, led2, led3 + mmc0)
|
||||
# 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"
|
||||
LED_RED1="/sys/class/leds/red:led1"
|
||||
LED_RED2="/sys/class/leds/red:led2"
|
||||
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"
|
||||
LED_BLUE="$LED_BLUE1"
|
||||
LED_ENABLED=0
|
||||
LED_COLOR_CYCLE=0 # Cycles 0-5 for rainbow colors
|
||||
LED_PREV_STATE="" # Track previous state for change detection
|
||||
|
||||
# Services to monitor (init.d name:check_method:restart_delay)
|
||||
# check_method: pid, docker, lxc, port:PORT
|
||||
@ -29,18 +46,144 @@ MONITORED_SERVICES=""
|
||||
# LED helper functions
|
||||
led_init() {
|
||||
# Check if LEDs are available (MochaBin or compatible)
|
||||
if [ -d "$LED_GREEN" ] && [ -d "$LED_RED" ]; then
|
||||
if [ -d "$LED_GREEN1" ] && [ -d "$LED_RED1" ]; then
|
||||
LED_ENABLED=1
|
||||
# Set trigger to none for manual control
|
||||
echo none > "$LED_GREEN/trigger" 2>/dev/null
|
||||
echo none > "$LED_RED/trigger" 2>/dev/null
|
||||
echo none > "$LED_BLUE/trigger" 2>/dev/null
|
||||
log debug "LED heartbeat enabled (MochaBin detected)"
|
||||
# Set trigger to none for manual control on RGB LEDs
|
||||
for led in "$LED_RED1" "$LED_GREEN1" "$LED_BLUE1" \
|
||||
"$LED_RED2" "$LED_GREEN2" "$LED_BLUE2" \
|
||||
"$LED_RED3" "$LED_GREEN3" "$LED_BLUE3"; do
|
||||
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"
|
||||
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
|
||||
local r="$2" g="$3" b="$4"
|
||||
case "$led_num" in
|
||||
1)
|
||||
echo "$r" > "$LED_RED1/brightness" 2>/dev/null
|
||||
echo "$g" > "$LED_GREEN1/brightness" 2>/dev/null
|
||||
echo "$b" > "$LED_BLUE1/brightness" 2>/dev/null
|
||||
;;
|
||||
2)
|
||||
echo "$r" > "$LED_RED2/brightness" 2>/dev/null
|
||||
echo "$g" > "$LED_GREEN2/brightness" 2>/dev/null
|
||||
echo "$b" > "$LED_BLUE2/brightness" 2>/dev/null
|
||||
;;
|
||||
3)
|
||||
echo "$r" > "$LED_RED3/brightness" 2>/dev/null
|
||||
echo "$g" > "$LED_GREEN3/brightness" 2>/dev/null
|
||||
echo "$b" > "$LED_BLUE3/brightness" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Turn off all LEDs on one LED unit
|
||||
led_off() {
|
||||
local led_num="$1"
|
||||
led_set_rgb "$led_num" 0 0 0
|
||||
}
|
||||
|
||||
# Global capacity LED on led3 - combines CPU + Network activity
|
||||
# Color gradient: Green (idle) -> Cyan (light) -> Yellow (moderate) -> Orange (busy) -> Red (max)
|
||||
# Network state file for delta calculation
|
||||
NET_PREV_FILE="/tmp/secubox/net_prev"
|
||||
|
||||
led_global_capacity() {
|
||||
[ "$LED_ENABLED" = "1" ] || return 0
|
||||
|
||||
# === 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%) ===
|
||||
# Sum all interface bytes
|
||||
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
|
||||
|
||||
# Calculate delta from previous reading
|
||||
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%)
|
||||
# 10MB = 10485760 bytes, measured over ~1.5s interval
|
||||
local net_pct=$((net_delta * 100 / 15728640))
|
||||
[ "$net_pct" -gt 100 ] && net_pct=100
|
||||
|
||||
# === Combine: weighted average (60% CPU, 40% network) ===
|
||||
local pct=$(( (cpu_pct * 60 + net_pct * 40) / 100 ))
|
||||
[ "$pct" -gt 100 ] && pct=100
|
||||
|
||||
# === Color gradient with more variety ===
|
||||
local r g b
|
||||
if [ "$pct" -le 20 ]; then
|
||||
# Green (idle) - pure green
|
||||
r=0; g=255; b=0
|
||||
elif [ "$pct" -le 40 ]; then
|
||||
# Green to Cyan (light activity)
|
||||
r=0; g=255; b=$(( (pct - 20) * 12 ))
|
||||
elif [ "$pct" -le 60 ]; then
|
||||
# Cyan to Yellow (moderate)
|
||||
r=$(( (pct - 40) * 12 ))
|
||||
g=255
|
||||
b=$((255 - (pct - 40) * 12))
|
||||
elif [ "$pct" -le 80 ]; then
|
||||
# Yellow to Orange (busy)
|
||||
r=255
|
||||
g=$((255 - (pct - 60) * 8))
|
||||
b=0
|
||||
else
|
||||
# Orange to Red (max capacity)
|
||||
r=255
|
||||
g=$((100 - (pct - 80) * 5))
|
||||
[ "$g" -lt 0 ] && g=0
|
||||
b=0
|
||||
fi
|
||||
|
||||
led_set_rgb 3 "$r" "$g" "$b"
|
||||
}
|
||||
|
||||
led_set() {
|
||||
local led="$1"
|
||||
local state="$2" # 0 = off, 1 = on
|
||||
@ -56,34 +199,154 @@ led_pulse() {
|
||||
( sleep 1 && led_set "$led" 0 ) &
|
||||
}
|
||||
|
||||
# Heartbeat function - shows system status via LED
|
||||
# Green pulse = healthy, Red pulse = warning/error
|
||||
# Security threat level for LED2
|
||||
# Reads from CrowdSec alerts and mitmproxy threats
|
||||
THREAT_CACHE_FILE="/tmp/secubox/threat_level"
|
||||
|
||||
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
|
||||
|
||||
if [ "$cache_age" -lt 10 ]; then
|
||||
cat "$THREAT_CACHE_FILE" 2>/dev/null || echo 0
|
||||
return
|
||||
fi
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
# Get global health score from services
|
||||
HEALTH_CACHE_FILE="/tmp/secubox/health_score"
|
||||
|
||||
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
|
||||
|
||||
if [ "$cache_age" -lt 15 ]; then
|
||||
cat "$HEALTH_CACHE_FILE" 2>/dev/null || echo 100
|
||||
return
|
||||
fi
|
||||
|
||||
local score=100
|
||||
local penalty=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))
|
||||
|
||||
# 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}')
|
||||
if [ -n "$mem_free" ] && [ -n "$mem_total" ] && [ "$mem_total" -gt 0 ]; then
|
||||
local 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))
|
||||
|
||||
score=$((100 - penalty))
|
||||
[ "$score" -lt 0 ] && score=0
|
||||
echo "$score" > "$HEALTH_CACHE_FILE"
|
||||
echo "$score"
|
||||
}
|
||||
|
||||
# Heartbeat function - 3 dedicated LEDs
|
||||
# LED1: Global health (green=healthy, yellow=degraded, red=critical) with pulse
|
||||
# LED2: Security threat meter (green=safe, yellow=activity, orange=elevated, red=attack)
|
||||
# LED3: Global capacity (CPU+Network) - handled by led_global_capacity()
|
||||
led_heartbeat() {
|
||||
[ "$LED_ENABLED" = "1" ] || return 0
|
||||
local status="$1" # healthy, warning, error
|
||||
|
||||
case "$status" in
|
||||
healthy)
|
||||
# Single green flash
|
||||
led_set "$LED_GREEN" 255
|
||||
( sleep 1 && echo 0 > "${LED_GREEN}/brightness" 2>/dev/null ) &
|
||||
;;
|
||||
warning)
|
||||
# Double red flash
|
||||
led_set "$LED_RED" 255
|
||||
( sleep 1 && echo 0 > "${LED_RED}/brightness" && sleep 1 && echo 255 > "${LED_RED}/brightness" && sleep 1 && echo 0 > "${LED_RED}/brightness" ) &
|
||||
;;
|
||||
error)
|
||||
# Long red flash
|
||||
led_set "$LED_RED" 255
|
||||
( sleep 2 && echo 0 > "${LED_RED}/brightness" 2>/dev/null ) &
|
||||
;;
|
||||
boot)
|
||||
# Blue pulse during startup
|
||||
led_set "$LED_BLUE" 255
|
||||
( sleep 2 && echo 0 > "${LED_BLUE}/brightness" 2>/dev/null ) &
|
||||
;;
|
||||
esac
|
||||
# LED1: Global health status with rainbow pulse effect
|
||||
local health=$(get_health_score)
|
||||
local r1 g1 b1
|
||||
|
||||
if [ "$health" -ge 80 ]; then
|
||||
# Healthy - Green with subtle color cycling
|
||||
case "$((LED_COLOR_CYCLE % 3))" in
|
||||
0) r1=0; g1=255; b1=0 ;; # Pure green
|
||||
1) r1=0; g1=255; b1=50 ;; # Green-cyan
|
||||
2) r1=50; g1=255; b1=0 ;; # Green-yellow
|
||||
esac
|
||||
elif [ "$health" -ge 50 ]; then
|
||||
# Degraded - Yellow/Orange cycling
|
||||
case "$((LED_COLOR_CYCLE % 3))" in
|
||||
0) r1=255; g1=255; b1=0 ;; # Yellow
|
||||
1) r1=255; g1=200; b1=0 ;; # Gold
|
||||
2) r1=255; g1=150; b1=0 ;; # Orange
|
||||
esac
|
||||
else
|
||||
# Critical - Red pulsing
|
||||
case "$((LED_COLOR_CYCLE % 3))" in
|
||||
0) r1=255; g1=0; b1=0 ;; # Red
|
||||
1) r1=255; g1=50; b1=0 ;; # Red-orange
|
||||
2) r1=200; g1=0; b1=50 ;; # Red-magenta
|
||||
esac
|
||||
fi
|
||||
led_set_rgb 1 "$r1" "$g1" "$b1"
|
||||
LED_COLOR_CYCLE=$(( (LED_COLOR_CYCLE + 1) % 6 ))
|
||||
|
||||
# LED2: Security threat level
|
||||
local threat=$(get_threat_level)
|
||||
local r2 g2 b2
|
||||
|
||||
if [ "$threat" -le 5 ]; then
|
||||
# Green - all quiet
|
||||
r2=0; g2=255; b2=0
|
||||
elif [ "$threat" -le 20 ]; then
|
||||
# Green-Yellow - minor activity
|
||||
r2=$((threat * 10)); g2=255; b2=0
|
||||
elif [ "$threat" -le 50 ]; then
|
||||
# Yellow - elevated activity
|
||||
r2=255; g2=255; b2=0
|
||||
elif [ "$threat" -le 75 ]; then
|
||||
# Orange - significant threats
|
||||
r2=255; g2=$((180 - (threat - 50) * 4)); b2=0
|
||||
else
|
||||
# Red - under attack / high threat
|
||||
r2=255; g2=$((50 - (threat - 75) * 2))
|
||||
[ "$g2" -lt 0 ] && g2=0
|
||||
b2=0
|
||||
fi
|
||||
|
||||
led_set_rgb 2 "$r2" "$g2" "$b2"
|
||||
}
|
||||
|
||||
# Auto-discover SecuBox services from ctl scripts
|
||||
@ -488,6 +751,100 @@ get_watchdog_services() {
|
||||
echo "$core_services"
|
||||
}
|
||||
|
||||
# Event pulse - flash led3 with specific color when events happen
|
||||
# Usage: echo "config|task|alert" > /tmp/secubox/led-event
|
||||
led_event_pulse() {
|
||||
local event_file="/tmp/secubox/led-event"
|
||||
[ -f "$event_file" ] || return 1
|
||||
|
||||
local event=$(cat "$event_file" 2>/dev/null)
|
||||
rm -f "$event_file"
|
||||
[ -z "$event" ] && return 1
|
||||
|
||||
# Flash pattern based on event type
|
||||
case "$event" in
|
||||
config)
|
||||
# White flash for config changes
|
||||
led_set_rgb 3 255 255 255
|
||||
;;
|
||||
task)
|
||||
# Cyan flash for scheduled tasks
|
||||
led_set_rgb 3 0 255 255
|
||||
;;
|
||||
alert)
|
||||
# Magenta flash for alerts
|
||||
led_set_rgb 3 255 0 255
|
||||
;;
|
||||
network)
|
||||
# Blue flash for network events
|
||||
led_set_rgb 3 0 100 255
|
||||
;;
|
||||
*)
|
||||
# Purple for unknown events
|
||||
led_set_rgb 3 180 0 255
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# Fast LED heartbeat background loop
|
||||
# Runs independently at 1.5s intervals for reactive visual feedback
|
||||
# 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
|
||||
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
|
||||
led_heartbeat "$status"
|
||||
|
||||
# Check for event pulse first (overrides capacity momentarily)
|
||||
if led_event_pulse; then
|
||||
sleep 0.3 # Brief flash
|
||||
fi
|
||||
|
||||
# Global capacity on led3
|
||||
led_global_capacity
|
||||
sleep 1.2
|
||||
done
|
||||
}
|
||||
|
||||
# Daemon mode
|
||||
daemon_mode() {
|
||||
log info "SecuBox Core daemon starting (version $SECUBOX_VERSION)"
|
||||
@ -508,6 +865,13 @@ daemon_mode() {
|
||||
# LED heartbeat setting (enabled by default)
|
||||
local led_heartbeat_enabled=$(uci -q get secubox.main.led_heartbeat || echo "1")
|
||||
|
||||
# Start fast LED heartbeat in background
|
||||
if [ "$led_heartbeat_enabled" = "1" ]; then
|
||||
led_heartbeat_loop &
|
||||
LED_HEARTBEAT_PID=$!
|
||||
log debug "LED heartbeat loop started (PID: $LED_HEARTBEAT_PID)"
|
||||
fi
|
||||
|
||||
# Main daemon loop
|
||||
local health_counter=0
|
||||
local health_cycles=$((health_interval / watchdog_interval))
|
||||
@ -523,16 +887,12 @@ daemon_mode() {
|
||||
if [ "$health_counter" -ge "$health_cycles" ]; then
|
||||
local health_output=$(run_health_check)
|
||||
echo "$health_output" > /tmp/secubox/health-status.json
|
||||
# Extract status for LED
|
||||
# Extract status for LED and update status file
|
||||
last_health_status=$(echo "$health_output" | jsonfilter -e '@.status' 2>/dev/null || echo "healthy")
|
||||
echo "$last_health_status" > /tmp/secubox/led-status
|
||||
health_counter=0
|
||||
fi
|
||||
|
||||
# LED heartbeat pulse (once per watchdog cycle)
|
||||
if [ "$led_heartbeat_enabled" = "1" ]; then
|
||||
led_heartbeat "$last_health_status"
|
||||
fi
|
||||
|
||||
# Sleep until next check
|
||||
sleep "$watchdog_interval"
|
||||
done
|
||||
|
||||
Loading…
Reference in New Issue
Block a user