From 6b3b0ed5032fdd95dc86a4e92e0ce527794fcbfd Mon Sep 17 00:00:00 2001 From: cschantz Date: Thu, 25 Dec 2025 16:16:22 -0500 Subject: [PATCH] Optimize IPset integration for maximum performance in live attack monitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: Live attack monitor was calling CSF unnecessarily for every block, causing performance overhead during DDoS attacks. The code was creating a new temporary IPset (live_monitor_$$) instead of using CSF's existing chain_DENY IPset, resulting in: - IPset add failures (IP already in CSF's set) - Unnecessary CSF fallback calls - Slower blocking due to CSF overhead - Duplicate blocking attempts ROOT CAUSE: Lines 68-86: Created unique per-process IPset instead of detecting/using CSF's existing chain_DENY IPset THE FIX: 1. Smart IPset Detection (lines 67-103): ✓ Detects CSF's chain_DENY IPset FIRST (preferred) ✓ Uses chain_DENY directly if found ✓ Falls back to temporary live_monitor_$$ if no CSF ✓ Auto-detects timeout support capability ✓ Never destroys CSF's permanent IPset on cleanup (line 141) 2. Aggressive IPset Prioritization (lines 855-911): block_ip_temporary(): ✓ ALWAYS tries IPset first if available ✓ Uses -exist flag to handle duplicates gracefully ✓ For CSF chain_DENY without timeout: Adds to IPset immediately, then calls CSF in background for timeout management ✓ CSF only used as fallback if IPset unavailable block_ip_permanent(): ✓ Adds to IPset immediately for instant blocking ✓ CSF called after for persistent management ✓ Handles both timeout/no-timeout IPsets 3. Subnet Blocking Optimization (lines 2307-2320): ✓ Uses $IPSET_NAME variable instead of hardcoded "blocklist" ✓ IPset subnet block happens FIRST (instant) ✓ CSF called in background after IPset PERFORMANCE BENEFITS: ✓ Kernel-level blocking (IPset) instead of userspace (CSF) ✓ Instant blocking during DDoS attacks ✓ No CSF overhead for every block ✓ Integrates with CSF's existing infrastructure ✓ Backward compatible (works without CSF) TESTED: ✓ Bash syntax validation passed ✓ Files synced (main + v2) ✓ All blocking paths prioritize IPset --- modules/security/live-attack-monitor-v2.sh | 106 +++++++++++++-------- modules/security/live-attack-monitor.sh | 106 +++++++++++++-------- 2 files changed, 136 insertions(+), 76 deletions(-) diff --git a/modules/security/live-attack-monitor-v2.sh b/modules/security/live-attack-monitor-v2.sh index 9a5b88d..cce2b53 100755 --- a/modules/security/live-attack-monitor-v2.sh +++ b/modules/security/live-attack-monitor-v2.sh @@ -65,21 +65,38 @@ echo "0" > "$TEMP_DIR/event_counter" echo "0" > "$TEMP_DIR/total_blocks" # IPset configuration -IPSET_NAME="live_monitor_$$" +IPSET_NAME="" IPSET_AVAILABLE=0 +IPSET_SUPPORTS_TIMEOUT=0 # Initialize IPset for fast blocking (if available) if command -v ipset &>/dev/null; then - # Create temporary IPset with 1-hour default timeout - if ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>/dev/null; then + # Check if CSF's chain_DENY IPset exists (preferred - already integrated with CSF) + if ipset list chain_DENY &>/dev/null 2>&1; then + IPSET_NAME="chain_DENY" IPSET_AVAILABLE=1 - # Add iptables rule to block IPs in the set - iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null - - echo "✓ IPset initialized: $IPSET_NAME (fast blocking enabled)" >> "$TEMP_DIR/debug.log" + # Check if chain_DENY supports timeouts + if ipset list chain_DENY | grep -q "^Type:.*timeout"; then + IPSET_SUPPORTS_TIMEOUT=1 + echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" + else + echo "✓ Using CSF IPset: chain_DENY (no timeout support, will use CSF for temp blocks)" >> "$TEMP_DIR/debug.log" + fi else - echo "✗ IPset creation failed - falling back to CSF" >> "$TEMP_DIR/debug.log" + # No CSF IPset found, create our own temporary one + IPSET_NAME="live_monitor_$$" + if ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>/dev/null; then + IPSET_AVAILABLE=1 + IPSET_SUPPORTS_TIMEOUT=1 + + # Add iptables rule to block IPs in the set + iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null + + echo "✓ IPset initialized: $IPSET_NAME (fast blocking enabled)" >> "$TEMP_DIR/debug.log" + else + echo "✗ IPset creation failed - falling back to CSF" >> "$TEMP_DIR/debug.log" + fi fi else echo "✗ IPset not available - using CSF for blocking" >> "$TEMP_DIR/debug.log" @@ -120,8 +137,8 @@ cleanup() { # Wait a moment for background jobs sleep 1 - # Clean up IPset and iptables rule if we created them - if [ "$IPSET_AVAILABLE" -eq 1 ]; then + # Clean up IPset and iptables rule ONLY if we created them (not CSF's chain_DENY) + if [ "$IPSET_AVAILABLE" -eq 1 ] && [ "$IPSET_NAME" != "chain_DENY" ]; then echo "Removing IPset firewall rules..." iptables -D INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null ipset destroy "$IPSET_NAME" 2>/dev/null @@ -847,33 +864,41 @@ block_ip_temporary() { return 1 fi - # Use IPset for instant blocking if available + # PRIORITY 1: Use IPset for instant kernel-level blocking (performance critical) if [ "$IPSET_AVAILABLE" -eq 1 ]; then - echo "Blocking $ip for ${hours}h: $reason" - if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" 2>/dev/null; then - echo "✓ $ip blocked via IPset (auto-expires in ${hours}h)" - echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" - - # Update counter atomically - increment_block_counter 1 - - return 0 + # Try IPset with timeout if supported + if [ "$IPSET_SUPPORTS_TIMEOUT" -eq 1 ]; then + if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" -exist 2>/dev/null; then + echo "✓ $ip blocked via IPset $IPSET_NAME (expires in ${hours}h)" + echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + increment_block_counter 1 + return 0 + fi else - echo "✗ Warning: IPset add failed (IP may already be blocked)" - return 1 + # IPset without timeout (CSF's chain_DENY) - add to IPset for instant block, + # then let CSF manage the timeout removal + if ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null; then + echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + increment_block_counter 1 + + # Let CSF manage the timeout in background (IPset already blocking) + if command -v csf &>/dev/null; then + csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1 & + echo "✓ $ip blocked via IPset $IPSET_NAME (CSF managing timeout: ${hours}h)" + else + echo "✓ $ip blocked via IPset $IPSET_NAME (permanent - no CSF timeout)" + fi + return 0 + fi fi fi - # Fallback to CSF if IPset not available + # FALLBACK: CSF-only blocking (slower, but still works) if command -v csf &>/dev/null; then - echo "Blocking $ip for ${hours}h: $reason" if csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1; then - echo "✓ $ip blocked via CSF" + echo "✓ $ip blocked via CSF (expires in ${hours}h)" echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" - - # Update counter atomically increment_block_counter 1 - return 0 else echo "✗ Warning: CSF block failed for $ip" @@ -881,7 +906,7 @@ block_ip_temporary() { fi fi - echo "✗ Error: CSF not available" + echo "✗ Error: No blocking method available" return 1 } @@ -911,13 +936,18 @@ block_ip_permanent() { return 1 fi - # Permanent blocks always use CSF (IPset is temp-only for this script) - # But we can add to IPset with max timeout as well for immediate effect + # PRIORITY: Add to IPset immediately for instant kernel-level blocking if [ "$IPSET_AVAILABLE" -eq 1 ]; then - # Add to IPset with 24-hour timeout for immediate blocking - ipset add "$IPSET_NAME" "$ip" timeout 86400 2>/dev/null + if [ "$IPSET_SUPPORTS_TIMEOUT" -eq 1 ]; then + # IPset with timeout - use max timeout (24 hours) + ipset add "$IPSET_NAME" "$ip" timeout 86400 -exist 2>/dev/null + else + # IPset without timeout (CSF's chain_DENY) - permanent add + ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null + fi fi + # CSF for persistent management (runs after IPset for immediate effect) if command -v csf &>/dev/null; then echo "Permanently blocking $ip: $reason" if csf -d "$ip" "$reason" >/dev/null 2>&1; then @@ -2279,16 +2309,16 @@ monitor_network_attacks() { for subnet in "${!hostile_subnets[@]}"; do local subnet_ip_count=${hostile_subnets[$subnet]} if [ "$subnet_ip_count" -ge 10 ]; then - # Block entire /24 subnet via IPset + # Block entire /24 subnet via IPset (PRIORITY) then CSF local subnet_cidr="${subnet}.0/24" if ! grep -q "^${subnet_cidr}\$" "$TEMP_DIR/blocked_subnets" 2>/dev/null; then echo "$subnet_cidr" >> "$TEMP_DIR/blocked_subnets" ( - # Add to IPset if available - if command -v ipset &>/dev/null && ipset list blocklist &>/dev/null 2>&1; then - ipset add blocklist "$subnet_cidr" -exist 2>/dev/null + # PRIORITY: Add to IPset for instant kernel-level blocking + if [ "$IPSET_AVAILABLE" -eq 1 ]; then + ipset add "$IPSET_NAME" "$subnet_cidr" -exist 2>/dev/null fi - # Also add to CSF + # CSF for persistent management (runs in background after IPset) if command -v csf &>/dev/null; then csf -d "$subnet_cidr" "SUBNET_DDOS:${subnet_ip_count}IPs" 2>/dev/null fi diff --git a/modules/security/live-attack-monitor.sh b/modules/security/live-attack-monitor.sh index 9a5b88d..cce2b53 100755 --- a/modules/security/live-attack-monitor.sh +++ b/modules/security/live-attack-monitor.sh @@ -65,21 +65,38 @@ echo "0" > "$TEMP_DIR/event_counter" echo "0" > "$TEMP_DIR/total_blocks" # IPset configuration -IPSET_NAME="live_monitor_$$" +IPSET_NAME="" IPSET_AVAILABLE=0 +IPSET_SUPPORTS_TIMEOUT=0 # Initialize IPset for fast blocking (if available) if command -v ipset &>/dev/null; then - # Create temporary IPset with 1-hour default timeout - if ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>/dev/null; then + # Check if CSF's chain_DENY IPset exists (preferred - already integrated with CSF) + if ipset list chain_DENY &>/dev/null 2>&1; then + IPSET_NAME="chain_DENY" IPSET_AVAILABLE=1 - # Add iptables rule to block IPs in the set - iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null - - echo "✓ IPset initialized: $IPSET_NAME (fast blocking enabled)" >> "$TEMP_DIR/debug.log" + # Check if chain_DENY supports timeouts + if ipset list chain_DENY | grep -q "^Type:.*timeout"; then + IPSET_SUPPORTS_TIMEOUT=1 + echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" + else + echo "✓ Using CSF IPset: chain_DENY (no timeout support, will use CSF for temp blocks)" >> "$TEMP_DIR/debug.log" + fi else - echo "✗ IPset creation failed - falling back to CSF" >> "$TEMP_DIR/debug.log" + # No CSF IPset found, create our own temporary one + IPSET_NAME="live_monitor_$$" + if ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>/dev/null; then + IPSET_AVAILABLE=1 + IPSET_SUPPORTS_TIMEOUT=1 + + # Add iptables rule to block IPs in the set + iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null + + echo "✓ IPset initialized: $IPSET_NAME (fast blocking enabled)" >> "$TEMP_DIR/debug.log" + else + echo "✗ IPset creation failed - falling back to CSF" >> "$TEMP_DIR/debug.log" + fi fi else echo "✗ IPset not available - using CSF for blocking" >> "$TEMP_DIR/debug.log" @@ -120,8 +137,8 @@ cleanup() { # Wait a moment for background jobs sleep 1 - # Clean up IPset and iptables rule if we created them - if [ "$IPSET_AVAILABLE" -eq 1 ]; then + # Clean up IPset and iptables rule ONLY if we created them (not CSF's chain_DENY) + if [ "$IPSET_AVAILABLE" -eq 1 ] && [ "$IPSET_NAME" != "chain_DENY" ]; then echo "Removing IPset firewall rules..." iptables -D INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null ipset destroy "$IPSET_NAME" 2>/dev/null @@ -847,33 +864,41 @@ block_ip_temporary() { return 1 fi - # Use IPset for instant blocking if available + # PRIORITY 1: Use IPset for instant kernel-level blocking (performance critical) if [ "$IPSET_AVAILABLE" -eq 1 ]; then - echo "Blocking $ip for ${hours}h: $reason" - if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" 2>/dev/null; then - echo "✓ $ip blocked via IPset (auto-expires in ${hours}h)" - echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" - - # Update counter atomically - increment_block_counter 1 - - return 0 + # Try IPset with timeout if supported + if [ "$IPSET_SUPPORTS_TIMEOUT" -eq 1 ]; then + if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" -exist 2>/dev/null; then + echo "✓ $ip blocked via IPset $IPSET_NAME (expires in ${hours}h)" + echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + increment_block_counter 1 + return 0 + fi else - echo "✗ Warning: IPset add failed (IP may already be blocked)" - return 1 + # IPset without timeout (CSF's chain_DENY) - add to IPset for instant block, + # then let CSF manage the timeout removal + if ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null; then + echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + increment_block_counter 1 + + # Let CSF manage the timeout in background (IPset already blocking) + if command -v csf &>/dev/null; then + csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1 & + echo "✓ $ip blocked via IPset $IPSET_NAME (CSF managing timeout: ${hours}h)" + else + echo "✓ $ip blocked via IPset $IPSET_NAME (permanent - no CSF timeout)" + fi + return 0 + fi fi fi - # Fallback to CSF if IPset not available + # FALLBACK: CSF-only blocking (slower, but still works) if command -v csf &>/dev/null; then - echo "Blocking $ip for ${hours}h: $reason" if csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1; then - echo "✓ $ip blocked via CSF" + echo "✓ $ip blocked via CSF (expires in ${hours}h)" echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" - - # Update counter atomically increment_block_counter 1 - return 0 else echo "✗ Warning: CSF block failed for $ip" @@ -881,7 +906,7 @@ block_ip_temporary() { fi fi - echo "✗ Error: CSF not available" + echo "✗ Error: No blocking method available" return 1 } @@ -911,13 +936,18 @@ block_ip_permanent() { return 1 fi - # Permanent blocks always use CSF (IPset is temp-only for this script) - # But we can add to IPset with max timeout as well for immediate effect + # PRIORITY: Add to IPset immediately for instant kernel-level blocking if [ "$IPSET_AVAILABLE" -eq 1 ]; then - # Add to IPset with 24-hour timeout for immediate blocking - ipset add "$IPSET_NAME" "$ip" timeout 86400 2>/dev/null + if [ "$IPSET_SUPPORTS_TIMEOUT" -eq 1 ]; then + # IPset with timeout - use max timeout (24 hours) + ipset add "$IPSET_NAME" "$ip" timeout 86400 -exist 2>/dev/null + else + # IPset without timeout (CSF's chain_DENY) - permanent add + ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null + fi fi + # CSF for persistent management (runs after IPset for immediate effect) if command -v csf &>/dev/null; then echo "Permanently blocking $ip: $reason" if csf -d "$ip" "$reason" >/dev/null 2>&1; then @@ -2279,16 +2309,16 @@ monitor_network_attacks() { for subnet in "${!hostile_subnets[@]}"; do local subnet_ip_count=${hostile_subnets[$subnet]} if [ "$subnet_ip_count" -ge 10 ]; then - # Block entire /24 subnet via IPset + # Block entire /24 subnet via IPset (PRIORITY) then CSF local subnet_cidr="${subnet}.0/24" if ! grep -q "^${subnet_cidr}\$" "$TEMP_DIR/blocked_subnets" 2>/dev/null; then echo "$subnet_cidr" >> "$TEMP_DIR/blocked_subnets" ( - # Add to IPset if available - if command -v ipset &>/dev/null && ipset list blocklist &>/dev/null 2>&1; then - ipset add blocklist "$subnet_cidr" -exist 2>/dev/null + # PRIORITY: Add to IPset for instant kernel-level blocking + if [ "$IPSET_AVAILABLE" -eq 1 ]; then + ipset add "$IPSET_NAME" "$subnet_cidr" -exist 2>/dev/null fi - # Also add to CSF + # CSF for persistent management (runs in background after IPset) if command -v csf &>/dev/null; then csf -d "$subnet_cidr" "SUBNET_DDOS:${subnet_ip_count}IPs" 2>/dev/null fi