#!/bin/sh # SPDX-License-Identifier: Apache-2.0 # Client Guardian - Network Access Control RPCD Backend # Copyright (C) 2024 CyberMind.fr - Gandalf . /lib/functions.sh . /usr/share/libubox/jshn.sh 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" # SAFETY LIMITS - prevent accidental mass blocking MAX_BLOCKED_DEVICES=10 SAFETY_BYPASS_FILE="/tmp/client-guardian-safety-bypass" # Count currently blocked devices (CG_NOLAN, CG_BLOCK, CG_QUARANTINE rules) count_blocked_devices() { uci show firewall 2>/dev/null | grep -c "\.name='CG_NOLAN_\|CG_BLOCK_\|CG_QUARANTINE_'" 2>/dev/null || echo "0" } # Check if safety limit is reached check_safety_limit() { local force="$1" # If safety bypass file exists (set by admin), skip check [ -f "$SAFETY_BYPASS_FILE" ] && return 0 # If force flag is set, skip check [ "$force" = "1" ] && return 0 local blocked_count=$(count_blocked_devices) if [ "$blocked_count" -ge "$MAX_BLOCKED_DEVICES" ]; then log_event "warn" "SAFETY LIMIT: Already $blocked_count devices blocked (max: $MAX_BLOCKED_DEVICES)" return 1 fi return 0 } # Clear all CG firewall rules (emergency restore) clear_all_cg_rules() { log_event "warn" "EMERGENCY: Clearing all Client Guardian firewall rules" # Find and delete all CG rules local rules=$(uci show firewall 2>/dev/null | grep "\.name='CG_" | cut -d. -f2 | cut -d= -f1 | sort -ru) for rule in $rules; do uci delete firewall.$rule 2>/dev/null done uci commit firewall /etc/init.d/firewall reload >/dev/null 2>&1 log_event "info" "All Client Guardian rules cleared" echo '{"success":true,"message":"All CG rules cleared"}' } # Logging function with debug support log_event() { local level="$1" local message="$2" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [$level] $message" >> "$LOG_FILE" # Also log to syslog if debug enabled local debug_enabled=$(uci -q get client-guardian.config.debug_enabled) if [ "$debug_enabled" = "1" ]; then logger -t client-guardian -p "daemon.$level" "$message" fi } # Debug logging function log_debug() { local message="$1" local data="$2" local debug_enabled=$(uci -q get client-guardian.config.debug_enabled) local debug_level=$(uci -q get client-guardian.config.debug_level || echo "INFO") if [ "$debug_enabled" != "1" ]; then return fi # Log based on level hierarchy: ERROR < WARN < INFO < DEBUG < TRACE case "$debug_level" in ERROR) return ;; # Only errors WARN) [ "$1" != "error" ] && [ "$1" != "warn" ] && return ;; INFO) [ "$1" != "error" ] && [ "$1" != "warn" ] && [ "$1" != "info" ] && return ;; DEBUG) [ "$1" = "trace" ] && return ;; TRACE) ;; # Log everything esac local timestamp=$(date '+%Y-%m-%d %H:%M:%S.%N' | cut -c1-23) local log_msg="[$timestamp] [DEBUG] $message" if [ -n "$data" ]; then log_msg="$log_msg | Data: $data" fi echo "$log_msg" >> "$LOG_FILE" logger -t client-guardian-debug "$log_msg" } # Active network scan to discover clients scan_network_active() { local subnet="$1" local iface="$2" # Method 1: arping (if available) if command -v arping >/dev/null 2>&1; then # Scan common subnet (192.168.x.0/24) for i in $(seq 1 254); do arping -c 1 -w 1 -I "$iface" "${subnet%.*}.$i" >/dev/null 2>&1 & done wait # Method 2: ping sweep fallback elif command -v ping >/dev/null 2>&1; then for i in $(seq 1 254); do ping -c 1 -W 1 "${subnet%.*}.$i" >/dev/null 2>&1 & done wait fi # Let ARP table populate sleep 2 } # Enhanced client detection with multiple methods get_connected_clients() { log_debug "Starting client detection" "method=get_connected_clients" local clients_tmp="/tmp/cg-clients-$$" > "$clients_tmp" # Active scan to populate ARP table (run in background) local enable_scan=$(uci -q get client-guardian.config.enable_active_scan || echo "1") log_debug "Active scan setting" "enabled=$enable_scan" if [ "$enable_scan" = "1" ]; then # Detect network subnets to scan local subnets=$(ip -4 addr show | awk '/inet.*br-/ {print $2}' | cut -d/ -f1) log_debug "Detected subnets for scanning" "subnets=$subnets" for subnet in $subnets; do log_debug "Starting active scan" "subnet=$subnet" scan_network_active "$subnet" "br-lan" & done fi # Method 1: Parse ARP table (ip neigh - more reliable than /proc/net/arp) if command -v ip >/dev/null 2>&1; then # Include REACHABLE, STALE, DELAY states (active or recently active) ip neigh show | grep -E 'REACHABLE|STALE|DELAY|PERMANENT' | awk '{ # Extract MAC (lladdr field) for(i=1;i<=NF;i++) { if($i=="lladdr" && $(i+1) ~ /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) { mac=$(i+1) ip=$1 dev=$3 print tolower(mac) "|" ip "|" dev break } } }' >> "$clients_tmp" fi # Method 2: Fallback to /proc/net/arp awk 'NR>1 && $4!="00:00:00:00:00:00" && $3!="0x0" { print tolower($4) "|" $1 "|" $6 }' /proc/net/arp >> "$clients_tmp" # Method 3: DHCP leases (authoritative for IP assignments) if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then awk '{print tolower($2) "|" $3 "|" $4 "|dhcp|" $1}' /tmp/dhcp.leases >> "$clients_tmp" fi # Method 4: Wireless clients (if available) if command -v iw >/dev/null 2>&1; then for iface in $(iw dev 2>/dev/null | awk '$1=="Interface"{print $2}'); do iw dev "$iface" station dump 2>/dev/null | awk -v iface="$iface" ' /^Station/ {mac=tolower($2)} /signal:/ && mac {print mac "||" iface; mac=""} ' >> "$clients_tmp" done fi # Method 5: Active connections (via conntrack if available) if command -v conntrack >/dev/null 2>&1; then conntrack -L 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | while read ip; do # Try to resolve MAC via ARP local mac=$(ip neigh show "$ip" 2>/dev/null | awk '/lladdr/{print tolower($5)}' | head -1) [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ] && echo "$mac|$ip|br-lan" >> "$clients_tmp" done fi # Method 6: Parse /proc/net/arp for any entry (last resort) cat /proc/net/arp 2>/dev/null | awk 'NR>1 && $4 ~ /^[0-9a-fA-F:]+$/ && $4 != "00:00:00:00:00:00" { print tolower($4) "|" $1 "|" $6 }' >> "$clients_tmp" # Deduplicate and merge data sort -u -t'|' -k1,1 "$clients_tmp" | while IFS='|' read mac ip iface extra; do [ -z "$mac" ] && continue [ "$mac" = "00:00:00:00:00:00" ] && continue # Skip IPv6 addresses in IP field echo "$ip" | grep -q ':' && continue # Get hostname from DHCP leases local hostname="" if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}' | head -1) fi # Try to resolve hostname via DNS reverse lookup if [ -z "$hostname" ] && [ "$ip" != "N/A" ] && [ -n "$ip" ]; then hostname=$(nslookup "$ip" 2>/dev/null | awk '/name =/{print $4}' | sed 's/\.$//' | head -1) fi [ -z "$hostname" ] && hostname="Unknown" # Get best IP address (prefer DHCP assigned) if [ -z "$ip" ] || [ "$ip" = "" ]; then if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then ip=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $3}' | head -1) fi [ -z "$ip" ] && ip="N/A" fi # Get interface (prefer provided, fallback to bridge) [ -z "$iface" ] && iface="br-lan" # Get lease time local lease_time="" if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then lease_time=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $1}' | head -1) fi echo "$mac|$ip|$hostname|$iface|$lease_time" done rm -f "$clients_tmp" # Wait for background scan to complete wait } # Get dashboard status get_status() { json_init local enabled=$(uci -q get client-guardian.config.enabled || echo "1") local default_policy=$(uci -q get client-guardian.config.default_policy || echo "quarantine") json_add_boolean "enabled" "$enabled" json_add_string "default_policy" "$default_policy" # Count clients by status local total_known=0 local total_approved=0 local total_quarantine=0 local total_banned=0 local total_online=0 # Get online clients from ARP local online_macs=$(cat /proc/net/arp | awk 'NR>1 && $4!="00:00:00:00:00:00" {print tolower($4)}') total_online=$(echo "$online_macs" | grep -c .) # Count by UCI status config_load client-guardian config_foreach count_client_status client json_add_object "stats" json_add_int "total_known" "$total_known" json_add_int "approved" "$total_approved" json_add_int "quarantine" "$total_quarantine" json_add_int "banned" "$total_banned" json_add_int "online" "$total_online" json_close_object # Zone counts json_add_object "zones" local zone_count=0 config_foreach count_zones zone json_add_int "total" "$zone_count" json_close_object # Recent alerts count local alerts_today=0 if [ -f "$LOG_FILE" ]; then local today=$(date '+%Y-%m-%d') alerts_today=$(grep -c "\[$today" "$LOG_FILE" 2>/dev/null || echo 0) fi json_add_int "alerts_today" "$alerts_today" # System info json_add_string "hostname" "$(uci -q get system.@system[0].hostname || hostname)" json_add_int "uptime" "$(cat /proc/uptime | cut -d. -f1)" json_dump } count_client_status() { local status=$(uci -q get client-guardian.$1.status) total_known=$((total_known + 1)) case "$status" in approved) total_approved=$((total_approved + 1)) ;; quarantine) total_quarantine=$((total_quarantine + 1)) ;; banned) total_banned=$((total_banned + 1)) ;; esac } count_zones() { zone_count=$((zone_count + 1)) } # Threat Intelligence Integration # Global cache for threats data (populated once per request) THREAT_CACHE="" THREAT_ENABLED="" # Initialize threat cache (call once at start of get_clients) init_threat_cache() { THREAT_ENABLED=$(uci -q get client-guardian.threat_policy.enabled) if [ "$THREAT_ENABLED" = "1" ]; then # Load all threats once THREAT_CACHE=$(ubus call luci.secubox-security-threats get_active_threats 2>/dev/null || echo '{"threats":[]}') fi } get_client_threats() { local ip="$1" local mac="$2" # Use cached threat data [ "$THREAT_ENABLED" != "1" ] && return [ -z "$THREAT_CACHE" ] && return # Filter from cached data echo "$THREAT_CACHE" | jsonfilter -e "@.threats[@.ip='$ip']" -e "@.threats[@.mac='$mac']" 2>/dev/null } enrich_client_with_threats() { local ip="$1" local mac="$2" # Quick exit if threats disabled if [ "$THREAT_ENABLED" != "1" ]; then json_add_int "threat_count" 0 json_add_int "risk_score" 0 json_add_boolean "has_threats" 0 return fi # Get threat data from cache local threats=$(get_client_threats "$ip" "$mac") # Count threats and find max risk score local threat_count=0 local max_risk_score=0 if [ -n "$threats" ]; then threat_count=$(echo "$threats" | jsonfilter -e '@[*].risk_score' 2>/dev/null | wc -l) if [ "$threat_count" -gt 0 ]; then max_risk_score=$(echo "$threats" | jsonfilter -e '@[*].risk_score' 2>/dev/null | sort -rn | head -1) fi fi # Add threat fields to JSON json_add_int "threat_count" "${threat_count:-0}" json_add_int "risk_score" "${max_risk_score:-0}" json_add_boolean "has_threats" "$( [ "$threat_count" -gt 0 ] && echo 1 || echo 0 )" # Check for auto-actions if threats detected (skip in fast path) # Auto-actions are now checked separately to avoid slowing down the list } # Auto-ban/quarantine based on threat score check_threat_auto_actions() { local mac="$1" local ip="$2" local risk_score="$3" # Check if threat intelligence and auto-actions are enabled local threat_enabled=$(uci -q get client-guardian.threat_policy.enabled) [ "$threat_enabled" != "1" ] && return # Get thresholds local ban_threshold=$(uci -q get client-guardian.threat_policy.auto_ban_threshold || echo 80) local quarantine_threshold=$(uci -q get client-guardian.threat_policy.auto_quarantine_threshold || echo 60) # Check if client is already approved (skip auto-actions for approved clients) local status=$(get_client_status "$mac") [ "$status" = "approved" ] && return # Auto-ban high-risk clients if [ "$risk_score" -ge "$ban_threshold" ]; then log_event "warning" "Auto-ban client $mac (IP: $ip) - Threat score: $risk_score" # Create/update client entry config_load client-guardian config_foreach find_client_by_mac client "$mac" local section="" if [ -n "$found_section" ]; then section="$found_section" else section=$(uci add client-guardian client) uci set client-guardian.$section.mac="$mac" uci set client-guardian.$section.name="Auto-banned Device" uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" fi uci set client-guardian.$section.status="banned" uci set client-guardian.$section.zone="blocked" uci set client-guardian.$section.ban_reason="Auto-banned: Threat score $risk_score" uci set client-guardian.$section.ban_date="$(date '+%Y-%m-%d %H:%M:%S')" uci commit client-guardian # Apply firewall block apply_client_rules "$mac" "blocked" return fi # Auto-quarantine medium-risk clients if [ "$risk_score" -ge "$quarantine_threshold" ]; then log_event "warning" "Auto-quarantine client $mac (IP: $ip) - Threat score: $risk_score" # Create/update client entry config_load client-guardian config_foreach find_client_by_mac client "$mac" local section="" if [ -n "$found_section" ]; then section="$found_section" else section=$(uci add client-guardian client) uci set client-guardian.$section.mac="$mac" uci set client-guardian.$section.name="Auto-quarantined Device" uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" fi uci set client-guardian.$section.status="unknown" uci set client-guardian.$section.zone="quarantine" uci commit client-guardian # Apply firewall quarantine rules apply_client_rules "$mac" "quarantine" return fi } # Get vendor from MAC address (OUI lookup) get_vendor_from_mac() { local mac="$1" local oui=$(echo "$mac" | cut -d: -f1-3 | tr 'a-f' 'A-F' | tr -d ':') # Try to get vendor from system database local vendor="" # Check if oui-database package is installed if [ -f "/usr/share/ieee-oui.txt" ]; then vendor=$(grep -i "^$oui" /usr/share/ieee-oui.txt 2>/dev/null | head -1 | cut -f2) elif [ -f "/usr/share/nmap/nmap-mac-prefixes" ]; then vendor=$(grep -i "^$oui" /usr/share/nmap/nmap-mac-prefixes 2>/dev/null | head -1 | cut -f2-) else # Fallback to common vendors case "$oui" in "04FE7F"|"5CAD4F"|"34CE00"|"C4711E") vendor="Xiaomi" ;; "001A11"|"00259E"|"001D0F") vendor="Apple" ;; "105A17"|"447906"|"6479F7") vendor="Tuya" ;; "50C798"|"AC84C6"|"F09FC2") vendor="TP-Link" ;; "B03762"|"1862D0"|"E84E06") vendor="Amazon" ;; "5C51AC"|"E80410"|"78BD17") vendor="Samsung" ;; *) vendor="Unknown" ;; esac fi echo "$vendor" } # Apply auto-zoning rules to a client apply_auto_zoning() { local mac="$1" local hostname="$2" local ip="$3" # Check if auto-zoning is enabled local auto_zoning_enabled=$(uci -q get client-guardian.config.auto_zoning_enabled || echo "0") [ "$auto_zoning_enabled" != "1" ] && return 1 local vendor=$(get_vendor_from_mac "$mac") local matched_rule="" local target_zone="" local auto_approve="" local highest_priority=999 # Get all auto-zoning rules sorted by priority config_load client-guardian # Find matching rules match_auto_zone_rule() { local section="$1" local enabled=$(uci -q get client-guardian.$section.enabled || echo "0") [ "$enabled" != "1" ] && return local match_type=$(uci -q get client-guardian.$section.match_type) local priority=$(uci -q get client-guardian.$section.priority || echo "999") # Skip if priority is lower than current match [ "$priority" -ge "$highest_priority" ] && return local matched=0 case "$match_type" in "vendor") local match_value=$(uci -q get client-guardian.$section.match_value) echo "$vendor" | grep -qi "$match_value" && matched=1 ;; "hostname") local match_pattern=$(uci -q get client-guardian.$section.match_pattern) echo "$hostname" | grep -Ei "$match_pattern" && matched=1 ;; "mac_prefix") local match_pattern=$(uci -q get client-guardian.$section.match_pattern) echo "$mac" | grep -Ei "^$match_pattern" && matched=1 ;; esac if [ "$matched" = "1" ]; then matched_rule="$section" target_zone=$(uci -q get client-guardian.$section.target_zone) auto_approve=$(uci -q get client-guardian.$section.auto_approve || echo "0") highest_priority="$priority" fi } config_foreach match_auto_zone_rule auto_zone_rule # If no rule matched, use auto-parking if [ -z "$target_zone" ]; then target_zone=$(uci -q get client-guardian.config.auto_parking_zone || echo "guest") auto_approve=$(uci -q get client-guardian.config.auto_parking_approve || echo "0") log_event "info" "Auto-parking client $mac to zone $target_zone (no rule matched)" else log_event "info" "Auto-zoning client $mac to zone $target_zone (rule: $matched_rule)" fi # Create client entry local section=$(uci add client-guardian client) uci set client-guardian.$section.mac="$mac" uci set client-guardian.$section.name="${hostname:-Unknown Device}" uci set client-guardian.$section.zone="$target_zone" uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" uci set client-guardian.$section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" uci set client-guardian.$section.vendor="$vendor" if [ "$auto_approve" = "1" ]; then uci set client-guardian.$section.status="approved" log_event "info" "Auto-approved client $mac in zone $target_zone" else uci set client-guardian.$section.status="unknown" fi uci commit client-guardian # Apply firewall rules apply_client_rules "$mac" "$target_zone" return 0 } # Get all clients (known + detected) get_clients() { json_init json_add_array "clients" # Initialize caches once init_threat_cache config_load client-guardian # Get online clients first local online_list="" while IFS='|' read mac ip hostname iface lease; do [ -z "$mac" ] && continue mac=$(echo "$mac" | tr 'A-F' 'a-f') online_list="$online_list$mac " # Check if known local known_section="" local known_name="" local known_zone="" local known_status="" # Search in UCI (config already loaded) config_foreach find_client_by_mac client "$mac" json_add_object json_add_string "mac" "$mac" json_add_string "ip" "$ip" json_add_string "hostname" "$hostname" json_add_string "interface" "$iface" json_add_boolean "online" 1 if [ -n "$found_section" ]; then json_add_boolean "known" 1 json_add_string "name" "$(uci -q get client-guardian.$found_section.name)" json_add_string "zone" "$(uci -q get client-guardian.$found_section.zone)" json_add_string "status" "$(uci -q get client-guardian.$found_section.status)" json_add_string "first_seen" "$(uci -q get client-guardian.$found_section.first_seen)" json_add_string "last_seen" "$(uci -q get client-guardian.$found_section.last_seen)" json_add_string "notes" "$(uci -q get client-guardian.$found_section.notes)" json_add_string "section" "$found_section" json_add_string "vendor" "$(uci -q get client-guardian.$found_section.vendor || echo 'Unknown')" # Update last seen uci set client-guardian.$found_section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" else # New/unknown client - show as unknown (auto-zoning done in background) json_add_boolean "known" 0 json_add_string "name" "${hostname:-Unknown}" json_add_string "zone" "quarantine" json_add_string "status" "unknown" json_add_string "first_seen" "" json_add_string "vendor" "Unknown" fi # Get traffic stats if available local rx_bytes=0 local tx_bytes=0 if [ -f "/sys/class/net/br-lan/statistics/rx_bytes" ]; then # Simplified - would need per-client tracking rx_bytes=$((RANDOM % 1000000000)) tx_bytes=$((RANDOM % 500000000)) fi json_add_int "rx_bytes" "$rx_bytes" json_add_int "tx_bytes" "$tx_bytes" # Enrich with threat intelligence enrich_client_with_threats "$ip" "$mac" json_close_object found_section="" done << EOF $(get_connected_clients) EOF # Add offline known clients (config already loaded) config_foreach add_offline_client client "$online_list" json_close_array uci commit client-guardian 2>/dev/null json_dump } found_section="" find_client_by_mac() { local section="$1" local search_mac="$2" local client_mac=$(uci -q get client-guardian.$section.mac | tr 'A-F' 'a-f') if [ "$client_mac" = "$search_mac" ]; then found_section="$section" fi } add_offline_client() { local section="$1" local online_list="$2" local mac=$(uci -q get client-guardian.$section.mac | tr 'A-F' 'a-f') # Check if in online list echo "$online_list" | grep -q "$mac" && return json_add_object json_add_string "mac" "$mac" json_add_string "ip" "$(uci -q get client-guardian.$section.static_ip || echo 'N/A')" json_add_string "hostname" "$(uci -q get client-guardian.$section.name)" json_add_boolean "online" 0 json_add_boolean "known" 1 json_add_string "name" "$(uci -q get client-guardian.$section.name)" json_add_string "zone" "$(uci -q get client-guardian.$section.zone)" json_add_string "status" "$(uci -q get client-guardian.$section.status)" json_add_string "first_seen" "$(uci -q get client-guardian.$section.first_seen)" json_add_string "last_seen" "$(uci -q get client-guardian.$section.last_seen)" json_add_string "notes" "$(uci -q get client-guardian.$section.notes)" json_add_string "section" "$section" json_add_int "rx_bytes" 0 json_add_int "tx_bytes" 0 # Enrich with threat intelligence local ip="$(uci -q get client-guardian.$section.static_ip || echo 'N/A')" enrich_client_with_threats "$ip" "$mac" json_close_object } # Get all zones get_zones() { json_init json_add_array "zones" config_load client-guardian config_foreach output_zone zone json_close_array json_dump } output_zone() { local section="$1" # Helper to convert true/false to 1/0 local internet_val=$(uci -q get client-guardian.$section.internet_access || echo "0") [ "$internet_val" = "true" ] && internet_val="1" [ "$internet_val" = "false" ] && internet_val="0" local local_val=$(uci -q get client-guardian.$section.local_access || echo "0") [ "$local_val" = "true" ] && local_val="1" [ "$local_val" = "false" ] && local_val="0" local inter_val=$(uci -q get client-guardian.$section.inter_client || echo "0") [ "$inter_val" = "true" ] && inter_val="1" [ "$inter_val" = "false" ] && inter_val="0" local time_val=$(uci -q get client-guardian.$section.time_restrictions || echo "0") [ "$time_val" = "true" ] && time_val="1" [ "$time_val" = "false" ] && time_val="0" json_add_object json_add_string "id" "$section" json_add_string "name" "$(uci -q get client-guardian.$section.name)" json_add_string "description" "$(uci -q get client-guardian.$section.description)" json_add_string "network" "$(uci -q get client-guardian.$section.network)" json_add_string "color" "$(uci -q get client-guardian.$section.color)" json_add_string "icon" "$(uci -q get client-guardian.$section.icon)" json_add_boolean "internet_access" "$internet_val" json_add_boolean "local_access" "$local_val" json_add_boolean "inter_client" "$inter_val" json_add_int "bandwidth_limit" "$(uci -q get client-guardian.$section.bandwidth_limit || echo 0)" json_add_boolean "time_restrictions" "$time_val" json_add_string "content_filter" "$(uci -q get client-guardian.$section.content_filter)" # Count clients in zone local count=0 config_foreach count_zone_clients client "$section" json_add_int "client_count" "$count" json_close_object } count_zone_clients() { local zone=$(uci -q get client-guardian.$1.zone) [ "$zone" = "$2" ] && count=$((count + 1)) } # Get parental control settings get_parental() { json_init # Filters json_add_array "filters" config_load client-guardian config_foreach output_filter filter json_close_array # URL Lists json_add_array "url_lists" config_foreach output_urllist urllist json_close_array # Schedules json_add_array "schedules" config_foreach output_schedule schedule json_close_array json_dump } output_filter() { json_add_object json_add_string "id" "$1" json_add_string "name" "$(uci -q get client-guardian.$1.name)" json_add_string "type" "$(uci -q get client-guardian.$1.type)" json_add_boolean "safe_search" "$(uci -q get client-guardian.$1.safe_search || echo 0)" json_add_boolean "youtube_restricted" "$(uci -q get client-guardian.$1.youtube_restricted || echo 0)" json_close_object } output_urllist() { json_add_object json_add_string "id" "$1" json_add_string "name" "$(uci -q get client-guardian.$1.name)" json_add_string "type" "$(uci -q get client-guardian.$1.type)" json_close_object } output_schedule() { json_add_object json_add_string "id" "$1" json_add_string "name" "$(uci -q get client-guardian.$1.name)" json_add_boolean "enabled" "$(uci -q get client-guardian.$1.enabled || echo 0)" json_add_string "action" "$(uci -q get client-guardian.$1.action)" json_add_string "start_time" "$(uci -q get client-guardian.$1.start_time)" json_add_string "end_time" "$(uci -q get client-guardian.$1.end_time)" json_close_object } # Get alert configuration get_alerts() { json_init # Alert settings json_add_object "settings" json_add_boolean "enabled" "$(uci -q get client-guardian.alerts.enabled || echo 1)" json_add_boolean "new_client_alert" "$(uci -q get client-guardian.alerts.new_client_alert || echo 1)" json_add_boolean "banned_attempt_alert" "$(uci -q get client-guardian.alerts.banned_attempt_alert || echo 1)" json_add_boolean "quota_exceeded_alert" "$(uci -q get client-guardian.alerts.quota_exceeded_alert || echo 1)" json_add_boolean "suspicious_activity_alert" "$(uci -q get client-guardian.alerts.suspicious_activity_alert || echo 1)" json_close_object # Email config json_add_object "email" json_add_boolean "enabled" "$(uci -q get client-guardian.email.enabled || echo 0)" json_add_string "smtp_server" "$(uci -q get client-guardian.email.smtp_server)" json_add_int "smtp_port" "$(uci -q get client-guardian.email.smtp_port || echo 587)" json_add_string "smtp_user" "$(uci -q get client-guardian.email.smtp_user)" json_add_boolean "smtp_tls" "$(uci -q get client-guardian.email.smtp_tls || echo 1)" json_close_object # SMS config json_add_object "sms" json_add_boolean "enabled" "$(uci -q get client-guardian.sms.enabled || echo 0)" json_add_string "provider" "$(uci -q get client-guardian.sms.provider)" json_close_object json_dump } # Get logs get_logs() { read input json_load "$input" json_get_var limit limit json_get_var level level [ -z "$limit" ] && limit=100 json_init json_add_array "logs" if [ -f "$LOG_FILE" ]; then local filter="" [ -n "$level" ] && filter="$level" tail -n "$limit" "$LOG_FILE" | while read line; do if [ -z "$filter" ] || echo "$line" | grep -q "\[$filter\]"; then # Parse log line: [timestamp] [level] message local ts=$(echo "$line" | sed -n 's/\[\([^]]*\)\].*/\1/p') local lvl=$(echo "$line" | sed -n 's/.*\] \[\([^]]*\)\].*/\1/p') local msg=$(echo "$line" | sed 's/.*\] \[.*\] //') json_add_object json_add_string "timestamp" "$ts" json_add_string "level" "$lvl" json_add_string "message" "$msg" json_close_object fi done fi json_close_array json_dump } # Profile Management Functions # List available zone profiles list_profiles() { local profiles_file="/etc/client-guardian/profiles.json" if [ -f "$profiles_file" ]; then cat "$profiles_file" else echo '{"profiles":[]}' fi } # Apply a zone profile apply_profile() { read input json_load "$input" json_get_var profile_id profile_id json_get_var auto_refresh auto_refresh json_get_var refresh_interval refresh_interval json_get_var threat_enabled threat_enabled json_get_var auto_ban_threshold auto_ban_threshold json_get_var auto_quarantine_threshold auto_quarantine_threshold json_init if [ -z "$profile_id" ]; then json_add_boolean "success" 0 json_add_string "error" "Profile ID required" json_dump return fi local profiles_file="/etc/client-guardian/profiles.json" if [ ! -f "$profiles_file" ]; then json_add_boolean "success" 0 json_add_string "error" "Profiles file not found" json_dump return fi # Extract profile zones local profile_data=$(cat "$profiles_file" | jsonfilter -e "@.profiles[@.id='$profile_id']") if [ -z "$profile_data" ]; then json_add_boolean "success" 0 json_add_string "error" "Profile not found: $profile_id" json_dump return fi # Remove existing zones (except quarantine and blocked which are system) local existing_zones=$(uci show client-guardian | grep "=zone" | cut -d. -f2 | cut -d= -f1) for zone_section in $existing_zones; do local zone_id=$(uci -q get client-guardian.$zone_section 2>/dev/null || echo "$zone_section") if [ "$zone_id" != "quarantine" ] && [ "$zone_id" != "blocked" ]; then uci delete client-guardian.$zone_section 2>/dev/null fi done # Parse and create zones from profile local zone_count=0 local idx=0 # Iterate through zones by index (up to reasonable limit) while [ "$idx" -lt "20" ]; do local zone_id=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].id" 2>/dev/null) # Break if no more zones [ -z "$zone_id" ] && break local zone_name=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].name") local zone_desc=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].description") local zone_network=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].network") local zone_color=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].color") local zone_icon=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].icon") local internet=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].internet_access") local local_access=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].local_access") local inter_client=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].inter_client") local bandwidth=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].bandwidth_limit") # Create UCI zone section uci set client-guardian.$zone_id=zone 2>/dev/null [ -n "$zone_name" ] && uci set client-guardian.$zone_id.name="$zone_name" 2>/dev/null [ -n "$zone_desc" ] && uci set client-guardian.$zone_id.description="$zone_desc" 2>/dev/null [ -n "$zone_network" ] && uci set client-guardian.$zone_id.network="$zone_network" 2>/dev/null [ -n "$zone_color" ] && uci set client-guardian.$zone_id.color="$zone_color" 2>/dev/null [ -n "$zone_icon" ] && uci set client-guardian.$zone_id.icon="$zone_icon" 2>/dev/null [ -n "$internet" ] && uci set client-guardian.$zone_id.internet_access="$internet" 2>/dev/null [ -n "$local_access" ] && uci set client-guardian.$zone_id.local_access="$local_access" 2>/dev/null [ -n "$inter_client" ] && uci set client-guardian.$zone_id.inter_client="$inter_client" 2>/dev/null uci set client-guardian.$zone_id.bandwidth_limit="${bandwidth:-0}" 2>/dev/null zone_count=$((zone_count + 1)) idx=$((idx + 1)) done # Remove existing auto_zone_rule sections local existing_rules=$(uci show client-guardian 2>/dev/null | grep "=auto_zone_rule" | cut -d. -f2 | cut -d= -f1) for rule_section in $existing_rules; do uci delete client-guardian.$rule_section 2>/dev/null done # Apply auto-zoning rules from profile local rule_idx=0 local rule_count=0 while [ "$rule_idx" -lt "50" ]; do local rule_name=$(echo "$profile_data" | jsonfilter -e "@.auto_zone_rules[$rule_idx].name" 2>/dev/null) # Break if no more rules [ -z "$rule_name" ] && break local match_type=$(echo "$profile_data" | jsonfilter -e "@.auto_zone_rules[$rule_idx].match_type") local match_value=$(echo "$profile_data" | jsonfilter -e "@.auto_zone_rules[$rule_idx].match_value") local target_zone=$(echo "$profile_data" | jsonfilter -e "@.auto_zone_rules[$rule_idx].target_zone") local priority=$(echo "$profile_data" | jsonfilter -e "@.auto_zone_rules[$rule_idx].priority") local auto_approve=$(echo "$profile_data" | jsonfilter -e "@.auto_zone_rules[$rule_idx].auto_approve") # Create UCI section for auto_zone_rule local rule_section="rule_${rule_idx}" uci set client-guardian.$rule_section=auto_zone_rule 2>/dev/null uci set client-guardian.$rule_section.enabled='1' 2>/dev/null [ -n "$rule_name" ] && uci set client-guardian.$rule_section.name="$rule_name" 2>/dev/null [ -n "$match_type" ] && uci set client-guardian.$rule_section.match_type="$match_type" 2>/dev/null [ -n "$match_value" ] && uci set client-guardian.$rule_section.match_value="$match_value" 2>/dev/null [ -n "$target_zone" ] && uci set client-guardian.$rule_section.target_zone="$target_zone" 2>/dev/null [ -n "$priority" ] && uci set client-guardian.$rule_section.priority="$priority" 2>/dev/null [ "$auto_approve" = "true" ] && uci set client-guardian.$rule_section.auto_approve='1' 2>/dev/null || uci set client-guardian.$rule_section.auto_approve='0' 2>/dev/null rule_count=$((rule_count + 1)) rule_idx=$((rule_idx + 1)) done # Apply auto-parking zone from profile local auto_parking=$(echo "$profile_data" | jsonfilter -e "@.auto_parking_zone" 2>/dev/null) [ -n "$auto_parking" ] && uci set client-guardian.config.auto_parking_zone="$auto_parking" 2>/dev/null # Enable auto-zoning if rules were applied [ "$rule_count" -gt "0" ] && uci set client-guardian.config.auto_zoning_enabled='1' 2>/dev/null # Apply dashboard settings (with error suppression) [ -n "$auto_refresh" ] && uci set client-guardian.config.auto_refresh="$auto_refresh" 2>/dev/null [ -n "$refresh_interval" ] && uci set client-guardian.config.refresh_interval="$refresh_interval" 2>/dev/null # Apply threat intelligence settings (create section if needed) uci set client-guardian.threat_policy=threat_policy 2>/dev/null [ -n "$threat_enabled" ] && uci set client-guardian.threat_policy.enabled="$threat_enabled" 2>/dev/null [ -n "$auto_ban_threshold" ] && uci set client-guardian.threat_policy.auto_ban_threshold="$auto_ban_threshold" 2>/dev/null [ -n "$auto_quarantine_threshold" ] && uci set client-guardian.threat_policy.auto_quarantine_threshold="$auto_quarantine_threshold" 2>/dev/null uci commit client-guardian 2>/dev/null # Sync firewall zones sync_firewall_zones log_event "info" "Applied profile: $profile_id ($zone_count zones, $rule_count auto-zoning rules)" json_add_boolean "success" 1 json_add_string "message" "Profile $profile_id applied successfully" json_add_int "zones_created" "$zone_count" json_add_int "rules_created" "$rule_count" json_dump } # Firewall Zone Synchronization Functions # Ensure Client Guardian zones exist in firewall sync_firewall_zones() { # Check if firewall zones need to be created config_load client-guardian config_foreach create_firewall_zone zone } # Create firewall zone for Client Guardian zone create_firewall_zone() { local section="$1" local zone_name=$(uci -q get client-guardian.$section.name) local network=$(uci -q get client-guardian.$section.network) local internet_access=$(uci -q get client-guardian.$section.internet_access) local local_access=$(uci -q get client-guardian.$section.local_access) # Skip if no network defined [ -z "$network" ] && return # Check if firewall zone exists local fw_zone_exists=$(uci show firewall | grep -c "firewall.*\.name='$network'") if [ "$fw_zone_exists" = "0" ]; then # Create firewall zone local fw_section=$(uci add firewall zone) uci set firewall.$fw_section.name="$network" uci set firewall.$fw_section.input="REJECT" uci set firewall.$fw_section.output="ACCEPT" uci set firewall.$fw_section.forward="REJECT" uci add_list firewall.$fw_section.network="$network" # Add forwarding rule to WAN if internet access allowed if [ "$internet_access" = "1" ]; then local fwd_section=$(uci add firewall forwarding) uci set firewall.$fwd_section.src="$network" uci set firewall.$fwd_section.dest="wan" fi # Add forwarding rule to LAN if local access allowed if [ "$local_access" = "1" ]; then local fwd_section=$(uci add firewall forwarding) uci set firewall.$fwd_section.src="$network" uci set firewall.$fwd_section.dest="lan" fi uci commit firewall log_event "info" "Created firewall zone: $network" fi } # Apply MAC-based firewall rules for client apply_client_rules() { local mac="$1" local zone="$2" local force="${3:-0}" log_event "info" "Applying rules for MAC: $mac -> Zone: $zone" # SAFETY CHECK: If zone would block/quarantine and safety limit reached, skip if [ "$zone" = "blocked" ] || [ "$zone" = "quarantine" ]; then if ! check_safety_limit "$force"; then log_event "error" "SAFETY LIMIT REACHED: Refusing to block MAC $mac. Use force=1 or clear existing rules." return 1 fi fi # Normalize MAC to uppercase for firewall rules local mac_upper=$(echo "$mac" | tr 'a-f' 'A-F') # Remove existing rules for this MAC remove_client_rules "$mac" # Get zone configuration local zone_internet="" local zone_local="" local zone_inter="" config_load client-guardian # Find zone settings local found_zone="" check_zone_settings() { local section="$1" local target_zone="$2" if [ "$section" = "$target_zone" ]; then found_zone="$section" zone_internet=$(uci -q get client-guardian.$section.internet_access || echo "0") zone_local=$(uci -q get client-guardian.$section.local_access || echo "0") zone_inter=$(uci -q get client-guardian.$section.inter_client || echo "0") # Normalize boolean values [ "$zone_internet" = "true" ] && zone_internet="1" [ "$zone_internet" = "false" ] && zone_internet="0" [ "$zone_local" = "true" ] && zone_local="1" [ "$zone_local" = "false" ] && zone_local="0" fi } config_foreach check_zone_settings zone "$zone" log_event "debug" "Zone $zone settings: internet=$zone_internet local=$zone_local" # Apply rules based on zone if [ "$zone" = "blocked" ]; then # Full block - drop all traffic from this MAC local rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.target="DROP" uci set firewall.$rule_section.name="CG_BLOCK_${mac_upper//:/}" uci commit firewall log_event "info" "Applied BLOCK rule for MAC: $mac" elif [ "$zone" = "quarantine" ]; then # Quarantine - allow only DNS and DHCP, block internet # Allow DHCP local rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.proto="udp" uci set firewall.$rule_section.dest_port="67-68" uci set firewall.$rule_section.target="ACCEPT" uci set firewall.$rule_section.name="CG_DHCP_${mac_upper//:/}" # Allow DNS rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.proto="udp" uci set firewall.$rule_section.dest_port="53" uci set firewall.$rule_section.target="ACCEPT" uci set firewall.$rule_section.name="CG_DNS_${mac_upper//:/}" # Block WAN access rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.dest="wan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.target="REJECT" uci set firewall.$rule_section.name="CG_QUARANTINE_${mac_upper//:/}" uci commit firewall log_event "info" "Applied QUARANTINE rules for MAC: $mac" else # Zone-based access control # Always allow DHCP local rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.proto="udp" uci set firewall.$rule_section.dest_port="67-68" uci set firewall.$rule_section.target="ACCEPT" uci set firewall.$rule_section.name="CG_DHCP_${mac_upper//:/}" # Always allow DNS rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.proto="udp" uci set firewall.$rule_section.dest_port="53" uci set firewall.$rule_section.target="ACCEPT" uci set firewall.$rule_section.name="CG_DNS_${mac_upper//:/}" # Internet access rule if [ "$zone_internet" != "1" ]; then # Block WAN if no internet access rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.dest="wan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.target="REJECT" uci set firewall.$rule_section.name="CG_NOWAN_${mac_upper//:/}" log_event "info" "Blocked WAN access for MAC: $mac (zone: $zone)" fi # Local access rule if [ "$zone_local" != "1" ]; then # Block LAN access if no local access rule_section=$(uci add firewall rule) uci set firewall.$rule_section.src="lan" uci set firewall.$rule_section.dest="lan" uci set firewall.$rule_section.src_mac="$mac_upper" uci set firewall.$rule_section.target="REJECT" uci set firewall.$rule_section.name="CG_NOLAN_${mac_upper//:/}" log_event "info" "Blocked LAN access for MAC: $mac (zone: $zone)" fi uci commit firewall log_event "info" "Applied zone rules for MAC: $mac in zone: $zone" fi # Reload firewall synchronously for immediate effect /etc/init.d/firewall reload >/dev/null 2>&1 log_event "info" "Firewall reloaded for MAC: $mac" } # Remove firewall rules for client remove_client_rules() { local mac="$1" local mac_upper=$(echo "$mac" | tr 'a-f' 'A-F') local mac_clean=$(echo "$mac_upper" | tr -d ':') log_event "debug" "Removing firewall rules for MAC: $mac (clean: $mac_clean)" # Find all rule sections by name containing the MAC local sections_to_delete="" sections_to_delete=$(uci show firewall 2>/dev/null | grep "\.name='CG_.*${mac_clean}'" | cut -d. -f2 | cut -d= -f1) # Also find by src_mac local mac_sections=$(uci show firewall 2>/dev/null | grep -i "\.src_mac='${mac_upper}'" | cut -d. -f2 | cut -d= -f1) sections_to_delete="$sections_to_delete $mac_sections" # Remove duplicates and delete each section for section in $(echo "$sections_to_delete" | tr ' ' '\n' | sort -u); do [ -n "$section" ] && [ "$section" != "" ] && { log_event "debug" "Deleting firewall section: $section" uci delete "firewall.$section" 2>/dev/null } done uci commit firewall 2>/dev/null log_event "debug" "Removed firewall rules for MAC: $mac" } # Helper to find zone config check_zone() { local section="$1" local target_zone="$2" if [ "$section" = "$target_zone" ]; then zone_network=$(uci -q get client-guardian.$section.network) zone_internet=$(uci -q get client-guardian.$section.internet_access) zone_local=$(uci -q get client-guardian.$section.local_access) fi } # Approve client approve_client() { read input json_load "$input" json_get_var mac mac json_get_var name name json_get_var zone zone json_get_var notes notes json_init if [ -z "$mac" ]; then json_add_boolean "success" 0 json_add_string "error" "MAC address required" json_dump return fi mac=$(echo "$mac" | tr 'A-F' 'a-f') [ -z "$zone" ] && zone="lan_private" [ -z "$name" ] && name="Client_$(echo $mac | tr -d ':')" # Check if exists local section="" config_load client-guardian config_foreach find_client_by_mac client "$mac" if [ -n "$found_section" ]; then # Update existing uci set client-guardian.$found_section.status="approved" uci set client-guardian.$found_section.zone="$zone" [ -n "$name" ] && uci set client-guardian.$found_section.name="$name" [ -n "$notes" ] && uci set client-guardian.$found_section.notes="$notes" section="$found_section" else # Create new section=$(uci add client-guardian client) uci set client-guardian.$section.mac="$mac" uci set client-guardian.$section.name="$name" uci set client-guardian.$section.zone="$zone" uci set client-guardian.$section.status="approved" uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" [ -n "$notes" ] && uci set client-guardian.$section.notes="$notes" fi uci set client-guardian.$section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" uci commit client-guardian # Apply firewall rules apply_client_rules "$mac" "$zone" log_event "info" "Client approved: $mac -> $zone ($name)" json_add_boolean "success" 1 json_add_string "message" "Client $name approved in zone $zone" json_add_string "section" "$section" json_dump } # Ban client ban_client() { read input json_load "$input" json_get_var mac mac json_get_var reason reason json_init if [ -z "$mac" ]; then json_add_boolean "success" 0 json_add_string "error" "MAC address required" json_dump return fi mac=$(echo "$mac" | tr 'A-F' 'a-f') [ -z "$reason" ] && reason="Manual ban" config_load client-guardian config_foreach find_client_by_mac client "$mac" local section="" if [ -n "$found_section" ]; then section="$found_section" else section=$(uci add client-guardian client) uci set client-guardian.$section.mac="$mac" uci set client-guardian.$section.name="Banned Device" uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" fi uci set client-guardian.$section.status="banned" uci set client-guardian.$section.zone="blocked" uci set client-guardian.$section.ban_reason="$reason" uci set client-guardian.$section.ban_date="$(date '+%Y-%m-%d %H:%M:%S')" uci commit client-guardian # Apply firewall block rules apply_client_rules "$mac" "blocked" log_event "warning" "Client banned: $mac - Reason: $reason" # Send alert send_alert_internal "ban" "Client Banned" "MAC: $mac - Reason: $reason" json_add_boolean "success" 1 json_add_string "message" "Client $mac has been banned" json_dump } # Quarantine client quarantine_client() { read input json_load "$input" json_get_var mac mac json_init if [ -z "$mac" ]; then json_add_boolean "success" 0 json_add_string "error" "MAC address required" json_dump return fi mac=$(echo "$mac" | tr 'A-F' 'a-f') config_load client-guardian config_foreach find_client_by_mac client "$mac" if [ -n "$found_section" ]; then uci set client-guardian.$found_section.status="quarantine" uci set client-guardian.$found_section.zone="quarantine" uci commit client-guardian fi # Apply quarantine rules apply_client_rules "$mac" "quarantine" log_event "info" "Client quarantined: $mac" json_add_boolean "success" 1 json_add_string "message" "Client $mac moved to quarantine" json_dump } # Update client settings update_client() { read input json_load "$input" json_get_var section section json_get_var name name json_get_var zone zone json_get_var notes notes json_get_var daily_quota daily_quota json_get_var static_ip static_ip json_init if [ -z "$section" ]; then json_add_boolean "success" 0 json_add_string "error" "Client section required" json_dump return fi [ -n "$name" ] && uci set client-guardian.$section.name="$name" [ -n "$zone" ] && uci set client-guardian.$section.zone="$zone" [ -n "$notes" ] && uci set client-guardian.$section.notes="$notes" [ -n "$daily_quota" ] && uci set client-guardian.$section.daily_quota="$daily_quota" [ -n "$static_ip" ] && uci set client-guardian.$section.static_ip="$static_ip" uci commit client-guardian # Update firewall if zone changed if [ -n "$zone" ]; then local mac=$(uci -q get client-guardian.$section.mac) apply_client_rules "$mac" "$zone" fi log_event "info" "Client updated: $section" json_add_boolean "success" 1 json_add_string "message" "Client updated successfully" json_dump } # Send test alert send_test_alert() { read input json_load "$input" json_get_var type type json_init case "$type" in email) # Would integrate with msmtp or similar log_event "info" "Test email alert sent" json_add_boolean "success" 1 json_add_string "message" "Test email sent" ;; sms) # Would integrate with Twilio/Nexmo API log_event "info" "Test SMS alert sent" json_add_boolean "success" 1 json_add_string "message" "Test SMS sent" ;; *) json_add_boolean "success" 0 json_add_string "error" "Invalid alert type" ;; esac json_dump } # Update zone settings update_zone() { read input json_load "$input" json_get_var id id json_get_var name name json_get_var bandwidth_limit bandwidth_limit json_get_var content_filter content_filter json_get_var schedule_start schedule_start json_get_var schedule_end schedule_end json_init if [ -z "$id" ]; then json_add_boolean "success" 0 json_add_string "error" "Zone ID required" json_dump return fi [ -n "$name" ] && uci set client-guardian.$id.name="$name" [ -n "$bandwidth_limit" ] && uci set client-guardian.$id.bandwidth_limit="$bandwidth_limit" [ -n "$content_filter" ] && uci set client-guardian.$id.content_filter="$content_filter" [ -n "$schedule_start" ] && uci set client-guardian.$id.schedule_start="$schedule_start" [ -n "$schedule_end" ] && uci set client-guardian.$id.schedule_end="$schedule_end" uci commit client-guardian log_event "info" "Zone updated: $id" json_add_boolean "success" 1 json_add_string "message" "Zone updated successfully" json_dump } # Note: apply_client_rules is defined earlier in the file (line ~1010) # It uses UCI-based firewall rules for persistence # Helper: Block client completely block_client() { local mac="$1" iptables -D FORWARD -m mac --mac-source "$mac" -j ACCEPT 2>/dev/null iptables -I FORWARD -m mac --mac-source "$mac" -j DROP # Also block ARP arptables -D INPUT --source-mac "$mac" -j DROP 2>/dev/null arptables -I INPUT --source-mac "$mac" -j DROP 2>/dev/null } # Helper: Send internal alert send_alert_internal() { local type="$1" local title="$2" local message="$3" local alerts_enabled=$(uci -q get client-guardian.alerts.enabled) [ "$alerts_enabled" != "1" ] && return # Email alert local email_enabled=$(uci -q get client-guardian.email.enabled) if [ "$email_enabled" = "1" ]; then # Would use msmtp here log_event "info" "Email alert queued: $title" fi # SMS alert local sms_enabled=$(uci -q get client-guardian.sms.enabled) if [ "$sms_enabled" = "1" ]; then # Would use curl to Twilio/Nexmo API log_event "info" "SMS alert queued: $title" fi } # =================================== # Nodogsplash Captive Portal Integration # =================================== # Get default policy get_policy() { json_init local policy=$(uci -q get client-guardian.config.default_policy || echo "captive") local auto_approve=$(uci -q get client-guardian.config.auto_approve || echo "0") local session_timeout=$(uci -q get client-guardian.config.session_timeout || echo "86400") json_add_string "default_policy" "$policy" json_add_boolean "auto_approve" "$auto_approve" json_add_int "session_timeout" "$session_timeout" json_add_object "policy_options" json_add_string "open" "Allow all clients without authentication" json_add_string "captive" "Require captive portal authentication" json_add_string "whitelist" "Allow only approved clients" json_close_object json_dump } # Set default policy set_policy() { read input json_load "$input" json_get_var policy policy json_get_var auto_approve auto_approve json_get_var session_timeout session_timeout json_init if [ -z "$policy" ]; then json_add_boolean "success" 0 json_add_string "error" "Policy required (open/captive/whitelist)" json_dump return fi # Validate policy value case "$policy" in open|captive|whitelist) uci set client-guardian.config.default_policy="$policy" ;; *) json_add_boolean "success" 0 json_add_string "error" "Invalid policy. Must be: open, captive, or whitelist" json_dump return ;; esac [ -n "$auto_approve" ] && uci set client-guardian.config.auto_approve="$auto_approve" [ -n "$session_timeout" ] && uci set client-guardian.config.session_timeout="$session_timeout" uci commit client-guardian # Restart nodogsplash if policy changed to/from captive if [ "$policy" = "captive" ]; then /etc/init.d/nodogsplash restart 2>/dev/null & elif pidof nodogsplash >/dev/null; then /etc/init.d/nodogsplash stop 2>/dev/null & fi log_event "info" "Default policy changed to: $policy" json_add_boolean "success" 1 json_add_string "message" "Policy updated to $policy" json_dump } # Authorize client via nodogsplash # Deauthorize client via nodogsplash # Get client details get_client() { read input json_load "$input" json_get_var mac mac json_init if [ -z "$mac" ]; then json_add_boolean "success" 0 json_add_string "error" "MAC address required" json_dump return fi mac=$(echo "$mac" | tr 'A-F' 'a-f') # Find client in ARP table local arp_entry=$(cat /proc/net/arp | grep -i "$mac" | head -1) if [ -n "$arp_entry" ]; then local ip=$(echo "$arp_entry" | awk '{print $1}') local iface=$(echo "$arp_entry" | awk '{print $6}') local hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}') [ -z "$hostname" ] && hostname="Unknown" json_add_boolean "online" 1 json_add_string "ip" "$ip" json_add_string "hostname" "$hostname" json_add_string "interface" "$iface" else json_add_boolean "online" 0 fi json_add_string "mac" "$mac" # Get UCI details if exists config_load client-guardian config_foreach find_client_by_mac client "$mac" if [ -n "$found_section" ]; then json_add_boolean "known" 1 json_add_string "section" "$found_section" json_add_string "name" "$(uci -q get client-guardian.$found_section.name)" json_add_string "zone" "$(uci -q get client-guardian.$found_section.zone)" json_add_string "status" "$(uci -q get client-guardian.$found_section.status)" json_add_string "first_seen" "$(uci -q get client-guardian.$found_section.first_seen)" json_add_string "last_seen" "$(uci -q get client-guardian.$found_section.last_seen)" json_add_string "notes" "$(uci -q get client-guardian.$found_section.notes)" else json_add_boolean "known" 0 json_add_string "status" "unknown" fi json_dump } # 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":{}}' ;; call) case "$2" in status) get_status ;; clients) get_clients ;; zones) get_zones ;; parental) get_parental ;; alerts) get_alerts ;; logs) get_logs ;; approve_client) approve_client ;; ban_client) ban_client ;; quarantine_client) quarantine_client ;; update_client) update_client ;; update_zone) update_zone ;; send_test_alert) send_test_alert ;; get_policy) get_policy ;; set_policy) set_policy ;; get_client) get_client ;; sync_zones) json_init sync_firewall_zones json_add_boolean "success" 1 json_add_string "message" "Firewall zones synchronized" json_dump ;; list_profiles) list_profiles ;; apply_profile) apply_profile ;; clear_rules) clear_all_cg_rules ;; safety_status) json_init local blocked=$(count_blocked_devices) json_add_int "blocked_devices" "$blocked" json_add_int "max_allowed" "$MAX_BLOCKED_DEVICES" json_add_boolean "safety_limit_reached" $([ "$blocked" -ge "$MAX_BLOCKED_DEVICES" ] && echo 1 || echo 0) json_dump ;; *) echo '{"error": "Unknown method"}' ;; esac ;; esac