e7be235d6b
MAJOR UX IMPROVEMENT: Consolidated security hardening into single 'c' key menu
REMOVED:
- 'f' key (Auto-Fix menu) - merged into 'c' key
- Scattered security recommendations across multiple menus
- Confusing workflow with multiple entry points
NEW UNIFIED MENU (Press 'c'):
┌─ Security Hardening & Firewall Optimization ─┐
│ Current Security Status: │
│ ✓ SYNFLOOD Protection: Enabled │
│ ✗ SSH Security: Default (LF_SSHD=5) │
│ ✓ Connection Tracking: Configured (200) │
│ │
│ Available Hardening Options: │
│ 1 - Enable SYNFLOOD Protection │
│ 2 - Harden SSH Security (Lower LF_SSHD) │
│ 3 - Optimize CT_LIMIT (Auto-analyze) │
│ 4 - Configure Port Knocking (Coming soon) │
│ a - Apply All Needed Fixes │
│ q - Return to Monitor │
└───────────────────────────────────────────────┘
FEATURES:
1. Status Display:
- Shows current state of all security settings
- ✓ green checkmark = already configured
- ✗ red X = needs attention
- Clear indication of what's already done
2. CT_LIMIT Auto Mode (--auto flag):
- Runs analysis silently when called from menu
- Automatically applies BALANCED recommendation
- No user prompts - just analyzes and applies
- Creates backup before making changes
3. Intelligent Recommendations:
- Quick Actions panel checks current settings
- Only recommends DDoS protection if SYNFLOOD disabled OR CT_LIMIT not set
- Only recommends SSH hardening if LF_SSHD > 3
- Recommendations disappear after being applied
- Clear actionable guidance
4. Apply All:
- Option 'a' applies all needed fixes automatically
- Skips already-configured settings
- Shows count of fixes applied
- One-click hardening for new servers
WORKFLOW IMPROVEMENTS:
Before:
1. See recommendation in Quick Actions
2. Press 'f' to open auto-fix menu
3. Select option from dynamic list
4. Different menu for CT_LIMIT ('c' key)
After:
1. See recommendation: "Press 'c' for Security Hardening menu"
2. Press 'c' - see status of ALL security settings
3. Select what to fix or press 'a' for all
4. Everything in ONE place
CT_LIMIT SIMPLIFICATION:
- Added --auto flag to optimize-ct-limit.sh
- When called with --auto: runs analysis + auto-applies BALANCED
- No user prompts in auto mode
- Perfect for automated workflows and menu integration
SMART RECOMMENDATIONS:
- DDoS recommendation only shows if:
- SYNFLOOD = 0 OR CT_LIMIT not set/zero
- SSH recommendation only shows if:
- LF_SSHD > 3
- After applying fixes, recommendations disappear
- No more "already configured" noise
USER EXPERIENCE:
- Single entry point for all security hardening
- Clear visual status indicators
- Actionable next steps
- No redundant options
- Professional menu layout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
885 lines
32 KiB
Bash
Executable File
885 lines
32 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
################################################################################
|
|
# CT_LIMIT Optimizer - Intelligent Connection Limit Calculator
|
|
################################################################################
|
|
# Purpose: Analyze real traffic patterns to recommend optimal CT_LIMIT
|
|
# Method:
|
|
# 1. Analyze Apache logs for legitimate concurrent connection patterns
|
|
# 2. Check current active connections per IP
|
|
# 3. Identify CDNs, bots, and legitimate high-traffic sources
|
|
# 4. Calculate safe CT_LIMIT that won't block real users
|
|
# 5. Provide CSF configuration recommendations
|
|
################################################################################
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
|
source "$SCRIPT_DIR/lib/bot-signatures.sh"
|
|
source "$SCRIPT_DIR/lib/reference-db.sh"
|
|
|
|
# Require root
|
|
if [ "$EUID" -ne 0 ]; then
|
|
print_error "This script must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
# Analysis configuration
|
|
ANALYSIS_HOURS=${1:-24} # Default: analyze last 24 hours
|
|
TEMP_ANALYSIS="/tmp/ct-limit-analysis-$$"
|
|
mkdir -p "$TEMP_ANALYSIS"
|
|
|
|
# Color definitions
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m'
|
|
|
|
################################################################################
|
|
# Functions
|
|
################################################################################
|
|
|
|
cleanup() {
|
|
rm -rf "$TEMP_ANALYSIS" 2>/dev/null
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
get_current_ct_limit() {
|
|
if [ -f "/etc/csf/csf.conf" ]; then
|
|
grep "^CT_LIMIT" /etc/csf/csf.conf | cut -d'=' -f2 | tr -d '"' | tr -d ' '
|
|
else
|
|
echo "Not configured"
|
|
fi
|
|
}
|
|
|
|
detect_cdn_usage() {
|
|
local domain="$1"
|
|
|
|
# Check DNS for CDN providers
|
|
local dns_result=$(dig +short "$domain" 2>/dev/null | head -5)
|
|
|
|
# Check for common CDN patterns
|
|
if echo "$dns_result" | grep -qiE "(cloudflare|cdn77|akamai|fastly|cloudfront|sucuri)"; then
|
|
echo "yes"
|
|
return
|
|
fi
|
|
|
|
# Check nameservers
|
|
local ns=$(dig +short NS "$domain" 2>/dev/null)
|
|
if echo "$ns" | grep -qiE "(cloudflare|cdn|akamai)"; then
|
|
echo "yes"
|
|
return
|
|
fi
|
|
|
|
echo "no"
|
|
}
|
|
|
|
detect_caching() {
|
|
local doc_root="$1"
|
|
local caching_score=0
|
|
|
|
# Check for Redis
|
|
if systemctl is-active redis &>/dev/null || pgrep redis-server &>/dev/null; then
|
|
((caching_score+=3))
|
|
fi
|
|
|
|
# Check for Memcached
|
|
if systemctl is-active memcached &>/dev/null || pgrep memcached &>/dev/null; then
|
|
((caching_score+=3))
|
|
fi
|
|
|
|
# Check for WordPress caching plugins
|
|
if [ -d "$doc_root/wp-content/plugins" ]; then
|
|
local cache_plugins=0
|
|
[ -d "$doc_root/wp-content/plugins/wp-rocket" ] && ((cache_plugins++))
|
|
[ -d "$doc_root/wp-content/plugins/w3-total-cache" ] && ((cache_plugins++))
|
|
[ -d "$doc_root/wp-content/plugins/wp-super-cache" ] && ((cache_plugins++))
|
|
[ -d "$doc_root/wp-content/plugins/litespeed-cache" ] && ((cache_plugins++))
|
|
[ -d "$doc_root/wp-content/plugins/wp-fastest-cache" ] && ((cache_plugins++))
|
|
|
|
((caching_score+=cache_plugins))
|
|
fi
|
|
|
|
# Check for .htaccess caching rules
|
|
if [ -f "$doc_root/.htaccess" ]; then
|
|
if grep -q "mod_expires\|mod_cache\|Cache-Control" "$doc_root/.htaccess"; then
|
|
((caching_score++))
|
|
fi
|
|
fi
|
|
|
|
echo "$caching_score"
|
|
}
|
|
|
|
detect_site_type() {
|
|
local domain="$1"
|
|
local doc_root="$2"
|
|
|
|
# Check if WordPress
|
|
if grep -q "^WP|$domain|" "$SYSREF_DB" 2>/dev/null; then
|
|
echo "wordpress"
|
|
return
|
|
fi
|
|
|
|
# Check for ecommerce indicators
|
|
if [ -d "$doc_root" ]; then
|
|
if [ -f "$doc_root/wp-content/plugins/woocommerce/woocommerce.php" ] || \
|
|
[ -d "$doc_root/skin/frontend" ] || \
|
|
[ -f "$doc_root/app/Mage.php" ] || \
|
|
[ -d "$doc_root/catalog" ]; then
|
|
echo "ecommerce"
|
|
return
|
|
fi
|
|
|
|
# Check for frameworks
|
|
if [ -f "$doc_root/composer.json" ] || [ -d "$doc_root/vendor" ]; then
|
|
echo "framework"
|
|
return
|
|
fi
|
|
|
|
# Count PHP files to determine complexity
|
|
local php_count=$(find "$doc_root" -maxdepth 3 -name "*.php" 2>/dev/null | wc -l)
|
|
if [ "$php_count" -gt 50 ]; then
|
|
echo "dynamic"
|
|
elif [ "$php_count" -gt 5 ]; then
|
|
echo "moderate"
|
|
else
|
|
echo "static"
|
|
fi
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
calculate_site_complexity() {
|
|
local domain="$1"
|
|
local doc_root="$2"
|
|
local site_type="$3"
|
|
|
|
# Base complexity score (1-10)
|
|
local complexity=1
|
|
|
|
# WordPress adds complexity
|
|
if [ "$site_type" = "wordpress" ]; then
|
|
# Check plugin count
|
|
local wp_data=$(grep "^WP|$domain|" "$SYSREF_DB" 2>/dev/null)
|
|
if [ -n "$wp_data" ]; then
|
|
local plugin_count=$(echo "$wp_data" | cut -d'|' -f6)
|
|
# More plugins = more concurrent connections needed
|
|
complexity=$((complexity + (plugin_count / 5)))
|
|
fi
|
|
complexity=$((complexity + 3)) # WordPress admin/ajax
|
|
fi
|
|
|
|
# Ecommerce needs higher limits
|
|
if [ "$site_type" = "ecommerce" ]; then
|
|
complexity=$((complexity + 5)) # Shopping cart, checkout, etc.
|
|
fi
|
|
|
|
# Framework/dynamic sites
|
|
if [ "$site_type" = "framework" ] || [ "$site_type" = "dynamic" ]; then
|
|
complexity=$((complexity + 2))
|
|
fi
|
|
|
|
# Check for Ajax-heavy sites (lots of .js files)
|
|
if [ -d "$doc_root" ]; then
|
|
local js_count=$(find "$doc_root" -maxdepth 2 -name "*.js" 2>/dev/null | wc -l)
|
|
if [ "$js_count" -gt 20 ]; then
|
|
complexity=$((complexity + 2)) # Ajax-heavy = more concurrent
|
|
fi
|
|
fi
|
|
|
|
# REDUCE complexity if good caching in place
|
|
local caching_score=$(detect_caching "$doc_root")
|
|
if [ "$caching_score" -gt 0 ]; then
|
|
# Good caching reduces connection needs
|
|
local cache_reduction=$((caching_score / 2))
|
|
complexity=$((complexity - cache_reduction))
|
|
[ "$complexity" -lt 1 ] && complexity=1
|
|
fi
|
|
|
|
# REDUCE complexity if CDN is in use
|
|
local has_cdn=$(detect_cdn_usage "$domain")
|
|
if [ "$has_cdn" = "yes" ]; then
|
|
# CDN handles static assets, reduces direct connections
|
|
complexity=$((complexity - 2))
|
|
[ "$complexity" -lt 1 ] && complexity=1
|
|
fi
|
|
|
|
# Cap at 10
|
|
[ "$complexity" -gt 10 ] && complexity=10
|
|
|
|
echo "$complexity"
|
|
}
|
|
|
|
analyze_per_site_traffic() {
|
|
print_status "Analyzing per-site traffic patterns..."
|
|
|
|
# Create per-site analysis file
|
|
echo "DOMAIN|SITE_TYPE|COMPLEXITY|MAX_CONN|AVG_CONN|UNIQUE_IPS|TOTAL_REQUESTS" > "$TEMP_ANALYSIS/per_site_analysis.txt"
|
|
|
|
# Get all active domains from sysref
|
|
grep "^DOMAIN|" "$SYSREF_DB" 2>/dev/null | while IFS='|' read -r _ domain owner doc_root log_path php_ver is_primary relation target status_code status_text health; do
|
|
|
|
# Skip aliases and unknowns
|
|
[ "$owner" = "unknown" ] && continue
|
|
[ "$is_primary" = "no" ] && continue
|
|
[ -z "$log_path" ] && continue
|
|
[ ! -f "$log_path" ] && continue
|
|
|
|
# Detect site type
|
|
local site_type=$(detect_site_type "$domain" "$doc_root")
|
|
local complexity=$(calculate_site_complexity "$domain" "$doc_root" "$site_type")
|
|
|
|
# Analyze traffic for this specific domain
|
|
local max_conn=0
|
|
local total_ips=0
|
|
local total_requests=0
|
|
|
|
if [ -f "$TEMP_ANALYSIS/connections_by_ip.txt" ]; then
|
|
# Get stats for this domain
|
|
local domain_data=$(grep "|$domain|" "$TEMP_ANALYSIS/connections_by_ip.txt")
|
|
|
|
if [ -n "$domain_data" ]; then
|
|
max_conn=$(echo "$domain_data" | cut -d'|' -f3 | sort -rn | head -1)
|
|
total_ips=$(echo "$domain_data" | cut -d'|' -f1 | sort -u | wc -l)
|
|
total_requests=$(echo "$domain_data" | cut -d'|' -f4 | awk '{s+=$1} END {print s}')
|
|
fi
|
|
fi
|
|
|
|
# Calculate average connections
|
|
local avg_conn=0
|
|
if [ "$total_ips" -gt 0 ]; then
|
|
avg_conn=$((total_requests / total_ips))
|
|
fi
|
|
|
|
echo "$domain|$site_type|$complexity|${max_conn:-0}|${avg_conn:-0}|${total_ips:-0}|${total_requests:-0}" >> "$TEMP_ANALYSIS/per_site_analysis.txt"
|
|
done
|
|
|
|
print_success "Per-site analysis complete"
|
|
}
|
|
|
|
check_server_resources() {
|
|
print_status "Checking server resources..."
|
|
|
|
# Get total RAM
|
|
local total_ram_mb=$(free -m | awk '/^Mem:/{print $2}')
|
|
|
|
# Get CPU cores
|
|
local cpu_cores=$(nproc 2>/dev/null || echo "2")
|
|
|
|
# Calculate max safe connections based on resources
|
|
# Rule of thumb: ~1MB RAM per connection, reserve 50% for other processes
|
|
local max_conn_by_ram=$((total_ram_mb / 2))
|
|
|
|
# Also factor in CPU (roughly 50 connections per core max)
|
|
local max_conn_by_cpu=$((cpu_cores * 50))
|
|
|
|
# Take the lower of the two
|
|
local max_safe_conn=$max_conn_by_ram
|
|
[ "$max_conn_by_cpu" -lt "$max_safe_conn" ] && max_safe_conn=$max_conn_by_cpu
|
|
|
|
echo "$total_ram_mb|$cpu_cores|$max_safe_conn" > "$TEMP_ANALYSIS/server_resources.txt"
|
|
|
|
print_success "Server: ${total_ram_mb}MB RAM, ${cpu_cores} cores, max safe connections: ${max_safe_conn}"
|
|
}
|
|
|
|
analyze_apache_logs() {
|
|
local hours="$1"
|
|
local cutoff_time=$(date -d "$hours hours ago" "+%d/%b/%Y:%H:%M:%S" 2>/dev/null)
|
|
|
|
print_status "Analyzing Apache access logs (last $hours hours)..."
|
|
|
|
# Use system-detected log directory (no fallback - rely on system-detect.sh)
|
|
local log_dir="${SYS_LOG_DIR}"
|
|
local total_logs=0
|
|
|
|
if [ -z "$log_dir" ] || [ ! -d "$log_dir" ]; then
|
|
print_warning "Apache log directory not found or not detected: $log_dir"
|
|
print_warning "System detection may have failed. Check SYS_LOG_DIR."
|
|
return 1
|
|
fi
|
|
|
|
# Analyze each domain's access patterns
|
|
echo "IP|DOMAIN|MAX_CONCURRENT|TOTAL_REQUESTS|USER_AGENT" > "$TEMP_ANALYSIS/connections_by_ip.txt"
|
|
|
|
# Track hourly patterns
|
|
> "$TEMP_ANALYSIS/hourly_traffic.txt"
|
|
|
|
find "$log_dir" -type f \( -name "*.com" -o -name "*.net" -o -name "*.org" -o -name "*.dev" \) 2>/dev/null | while read -r logfile; do
|
|
local domain=$(basename "$logfile")
|
|
((total_logs++))
|
|
|
|
# Extract IP, timestamp, user agent
|
|
awk -v domain="$domain" '{
|
|
# Parse: IP - - [timestamp] "METHOD URL" status bytes "ref" "UA"
|
|
match($0, /^([0-9.]+).*\[([^\]]+)\].*"([^"]*)".*"([^"]*)"$/, arr)
|
|
if (arr[1] != "") {
|
|
ip = arr[1]
|
|
timestamp = arr[2]
|
|
ua = arr[4]
|
|
|
|
# Extract hour for time-of-day analysis
|
|
match(timestamp, /([0-9]{2}):([0-9]{2}):/, time_arr)
|
|
hour = time_arr[1]
|
|
if (hour != "") {
|
|
hourly_count[hour]++
|
|
}
|
|
|
|
# Track requests per second per IP
|
|
gsub(/:.*/, "", timestamp) # Remove time, keep date
|
|
key = ip "|" domain "|" timestamp
|
|
count[key]++
|
|
user_agent[ip] = ua
|
|
}
|
|
}
|
|
END {
|
|
# Output hourly traffic patterns
|
|
for (h in hourly_count) {
|
|
print h "|" hourly_count[h] >> "/tmp/ct-limit-analysis-$$/hourly_traffic.txt"
|
|
}
|
|
|
|
for (key in count) {
|
|
split(key, parts, "|")
|
|
ip = parts[1]
|
|
dom = parts[2]
|
|
|
|
# Track max concurrent requests
|
|
if (count[key] > max_concurrent[ip "|" dom]) {
|
|
max_concurrent[ip "|" dom] = count[key]
|
|
}
|
|
total_requests[ip "|" dom] += count[key]
|
|
}
|
|
|
|
for (key in max_concurrent) {
|
|
split(key, parts, "|")
|
|
ip = parts[1]
|
|
dom = parts[2]
|
|
print ip "|" dom "|" max_concurrent[key] "|" total_requests[key] "|" user_agent[ip]
|
|
}
|
|
}' "$logfile" >> "$TEMP_ANALYSIS/connections_by_ip.txt" 2>/dev/null
|
|
done
|
|
|
|
print_success "Analyzed logs from $log_dir"
|
|
}
|
|
|
|
analyze_current_connections() {
|
|
print_status "Analyzing current active connections..."
|
|
|
|
if command -v ss &>/dev/null; then
|
|
# Count current connections per IP
|
|
ss -tn state established 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort | uniq -c | sort -rn > "$TEMP_ANALYSIS/current_connections.txt"
|
|
elif command -v netstat &>/dev/null; then
|
|
netstat -tn | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn > "$TEMP_ANALYSIS/current_connections.txt"
|
|
else
|
|
print_warning "Neither ss nor netstat available"
|
|
return 1
|
|
fi
|
|
|
|
print_success "Current connection analysis complete"
|
|
}
|
|
|
|
classify_ip_behavior() {
|
|
local ip="$1"
|
|
local user_agent="$2"
|
|
local max_concurrent="$3"
|
|
local total_requests="$4"
|
|
|
|
# Classify using bot signatures
|
|
local bot_type=$(classify_bot_type "$user_agent")
|
|
|
|
# Additional classification
|
|
local classification="unknown"
|
|
|
|
# Known good sources
|
|
if [ "$bot_type" = "legit" ]; then
|
|
classification="legitimate_bot"
|
|
elif [ "$bot_type" = "ai" ]; then
|
|
classification="ai_crawler"
|
|
elif [ "$bot_type" = "monitor" ]; then
|
|
classification="monitoring_service"
|
|
# CDN detection
|
|
elif [[ "$user_agent" =~ (Cloudflare|CloudFront|Akamai|Fastly|Sucuri) ]]; then
|
|
classification="cdn"
|
|
# High request rate but legitimate
|
|
elif [ "$max_concurrent" -gt 50 ] && [ "$total_requests" -gt 1000 ]; then
|
|
if [[ "$user_agent" =~ (Chrome|Firefox|Safari|Edge) ]]; then
|
|
classification="high_traffic_user"
|
|
else
|
|
classification="potential_scraper"
|
|
fi
|
|
# Normal user
|
|
elif [ "$max_concurrent" -lt 20 ]; then
|
|
classification="normal_user"
|
|
else
|
|
classification="moderate_user"
|
|
fi
|
|
|
|
echo "$classification"
|
|
}
|
|
|
|
calculate_percentile() {
|
|
local percentile="$1"
|
|
local data_file="$2"
|
|
|
|
# Sort data and get Nth percentile
|
|
local count=$(wc -l < "$data_file")
|
|
local position=$(awk -v p="$percentile" -v c="$count" 'BEGIN {printf "%.0f", (p/100) * c}')
|
|
|
|
[ "$position" -lt 1 ] && position=1
|
|
|
|
sort -n "$data_file" | sed -n "${position}p"
|
|
}
|
|
|
|
generate_recommendation() {
|
|
print_banner "CT_LIMIT Optimizer - Analysis Results"
|
|
echo ""
|
|
|
|
# Parse analysis data
|
|
local max_legitimate=0
|
|
local max_bot=0
|
|
local max_cdn=0
|
|
local total_ips=0
|
|
|
|
echo "Analyzing connection patterns..."
|
|
echo ""
|
|
|
|
# Create summary files
|
|
> "$TEMP_ANALYSIS/legitimate_connections.txt"
|
|
> "$TEMP_ANALYSIS/bot_connections.txt"
|
|
> "$TEMP_ANALYSIS/all_connections.txt"
|
|
|
|
# Skip header
|
|
tail -n +2 "$TEMP_ANALYSIS/connections_by_ip.txt" | while IFS='|' read -r ip domain max_concurrent total_requests user_agent; do
|
|
[ -z "$ip" ] && continue
|
|
|
|
local classification=$(classify_ip_behavior "$ip" "$user_agent" "$max_concurrent" "$total_requests")
|
|
|
|
((total_ips++))
|
|
|
|
# Track max connections by type
|
|
case "$classification" in
|
|
legitimate_bot|ai_crawler|monitoring_service|cdn)
|
|
echo "$max_concurrent" >> "$TEMP_ANALYSIS/bot_connections.txt"
|
|
[ "$max_concurrent" -gt "$max_bot" ] && max_bot=$max_concurrent
|
|
;;
|
|
normal_user|moderate_user|high_traffic_user)
|
|
echo "$max_concurrent" >> "$TEMP_ANALYSIS/legitimate_connections.txt"
|
|
[ "$max_concurrent" -gt "$max_legitimate" ] && max_legitimate=$max_concurrent
|
|
;;
|
|
esac
|
|
|
|
echo "$max_concurrent" >> "$TEMP_ANALYSIS/all_connections.txt"
|
|
done
|
|
|
|
# Calculate statistics
|
|
local legit_count=$(wc -l < "$TEMP_ANALYSIS/legitimate_connections.txt" 2>/dev/null || echo "0")
|
|
local bot_count=$(wc -l < "$TEMP_ANALYSIS/bot_connections.txt" 2>/dev/null || echo "0")
|
|
|
|
echo -e "${BOLD}Connection Analysis Summary:${NC}"
|
|
echo "──────────────────────────────────────────────────────────────"
|
|
echo " Total unique IPs analyzed: $total_ips"
|
|
echo " Legitimate users: $legit_count"
|
|
echo " Bots/CDNs/Crawlers: $bot_count"
|
|
echo ""
|
|
|
|
# Show per-site analysis
|
|
if [ -f "$TEMP_ANALYSIS/per_site_analysis.txt" ]; then
|
|
local site_count=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" | wc -l)
|
|
if [ "$site_count" -gt 0 ]; then
|
|
echo -e "${BOLD}Per-Site Analysis (All $site_count Sites Checked):${NC}"
|
|
echo "──────────────────────────────────────────────────────────────"
|
|
printf " %-30s %-12s %5s %8s %8s\n" "DOMAIN" "TYPE" "CMPLX" "MAX_CONN" "UNIQ_IPs"
|
|
echo " $(printf '─%.0s' {1..70})"
|
|
|
|
tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" | sort -t'|' -k4 -rn | head -15 | while IFS='|' read -r domain site_type complexity max_conn avg_conn unique_ips total_requests; do
|
|
# Truncate long domain names
|
|
local short_domain=$(echo "$domain" | cut -c1-28)
|
|
[ ${#domain} -gt 28 ] && short_domain="${short_domain}.."
|
|
|
|
# Color code by complexity
|
|
local color="${NC}"
|
|
if [ "$complexity" -ge 7 ]; then
|
|
color="${HIGH_COLOR}"
|
|
elif [ "$complexity" -ge 4 ]; then
|
|
color="${MEDIUM_COLOR}"
|
|
fi
|
|
|
|
printf " ${color}%-30s %-12s %5s %8s %8s${NC}\n" \
|
|
"$short_domain" "$site_type" "$complexity" "$max_conn" "$unique_ips"
|
|
done
|
|
|
|
local remaining=$((site_count - 15))
|
|
if [ "$remaining" -gt 0 ]; then
|
|
echo " ... and $remaining more sites analyzed"
|
|
fi
|
|
echo ""
|
|
|
|
# Identify high-complexity sites that need extra headroom
|
|
local high_complexity_sites=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" | awk -F'|' '$3 >= 7 {print $1}' | wc -l)
|
|
if [ "$high_complexity_sites" -gt 0 ]; then
|
|
echo -e "${MEDIUM_COLOR} ⚠️ $high_complexity_sites high-complexity sites detected${NC}"
|
|
echo " (WordPress/Ecommerce/Framework - need higher CT_LIMIT)"
|
|
echo ""
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Calculate percentiles for legitimate traffic
|
|
if [ -s "$TEMP_ANALYSIS/legitimate_connections.txt" ]; then
|
|
local p95=$(calculate_percentile 95 "$TEMP_ANALYSIS/legitimate_connections.txt")
|
|
local p99=$(calculate_percentile 99 "$TEMP_ANALYSIS/legitimate_connections.txt")
|
|
|
|
echo -e "${BOLD}Legitimate User Connection Patterns:${NC}"
|
|
echo " Max concurrent from single IP: $max_legitimate"
|
|
echo " 95th percentile: $p95 concurrent connections"
|
|
echo " 99th percentile: $p99 concurrent connections"
|
|
echo ""
|
|
fi
|
|
|
|
# Check current connections
|
|
if [ -s "$TEMP_ANALYSIS/current_connections.txt" ]; then
|
|
local current_max=$(head -1 "$TEMP_ANALYSIS/current_connections.txt" | awk '{print $1}')
|
|
local current_max_ip=$(head -1 "$TEMP_ANALYSIS/current_connections.txt" | awk '{print $2}')
|
|
|
|
echo -e "${BOLD}Current Active Connections:${NC}"
|
|
echo " Highest right now: $current_max connections from $current_max_ip"
|
|
echo ""
|
|
fi
|
|
|
|
# Server resource limits
|
|
if [ -f "$TEMP_ANALYSIS/server_resources.txt" ]; then
|
|
IFS='|' read -r total_ram cpu_cores max_safe_conn < "$TEMP_ANALYSIS/server_resources.txt"
|
|
|
|
echo -e "${BOLD}Server Resource Limits:${NC}"
|
|
echo " RAM: ${total_ram}MB | CPU: ${cpu_cores} cores"
|
|
echo " Max safe connections (hardware): $max_safe_conn"
|
|
echo ""
|
|
fi
|
|
|
|
# Time-of-day patterns
|
|
if [ -f "$TEMP_ANALYSIS/hourly_traffic.txt" ]; then
|
|
local peak_hour=$(awk -F'|' '{if($2>max){max=$2; hour=$1}} END{print hour}' "$TEMP_ANALYSIS/hourly_traffic.txt")
|
|
local peak_requests=$(awk -F'|' '{if($2>max){max=$2}} END{print max}' "$TEMP_ANALYSIS/hourly_traffic.txt")
|
|
local avg_requests=$(awk -F'|' '{sum+=$2; count++} END{if(count>0) print int(sum/count)}' "$TEMP_ANALYSIS/hourly_traffic.txt")
|
|
|
|
if [ -n "$peak_hour" ] && [ "$peak_requests" -gt "$((avg_requests * 2))" ]; then
|
|
echo -e "${BOLD}Traffic Patterns:${NC}"
|
|
echo " Peak hour: ${peak_hour}:00 (${peak_requests} requests)"
|
|
echo " Average: ${avg_requests} requests/hour"
|
|
echo " Peak is ${MEDIUM_COLOR}$((peak_requests * 100 / avg_requests))% above average${NC}"
|
|
echo " ${DIM}→ CT_LIMIT should handle peak, not average${NC}"
|
|
echo ""
|
|
fi
|
|
fi
|
|
|
|
# Optimization opportunities
|
|
local opt_count=0
|
|
echo -e "${BOLD}Optimization Opportunities:${NC}"
|
|
|
|
# Check for sites without CDN
|
|
local no_cdn=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" 2>/dev/null | while IFS='|' read -r domain site_type complexity max_conn avg_conn unique_ips total_requests; do
|
|
if [ "$complexity" -ge 6 ]; then
|
|
local has_cdn=$(detect_cdn_usage "$domain" 2>/dev/null)
|
|
if [ "$has_cdn" = "no" ]; then
|
|
echo "$domain"
|
|
((opt_count++))
|
|
fi
|
|
fi
|
|
done | head -3)
|
|
|
|
if [ -n "$no_cdn" ]; then
|
|
echo " 📦 CDN Recommended for:"
|
|
echo "$no_cdn" | while read -r domain; do
|
|
echo " • $domain (would reduce CT_LIMIT need)"
|
|
done
|
|
fi
|
|
|
|
# Check for WordPress without caching
|
|
local no_cache=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" 2>/dev/null | grep "wordpress" | while IFS='|' read -r domain site_type complexity max_conn avg_conn unique_ips total_requests; do
|
|
# Get doc root from sysref
|
|
local doc_root=$(grep "^DOMAIN|$domain|" "$SYSREF_DB" 2>/dev/null | cut -d'|' -f4)
|
|
if [ -n "$doc_root" ]; then
|
|
local cache_score=$(detect_caching "$doc_root" 2>/dev/null)
|
|
if [ "$cache_score" -eq 0 ]; then
|
|
echo "$domain"
|
|
fi
|
|
fi
|
|
done | head -3)
|
|
|
|
if [ -n "$no_cache" ]; then
|
|
echo " ⚡ Caching Recommended for:"
|
|
echo "$no_cache" | while read -r domain; do
|
|
echo " • $domain (WP Rocket, Redis, or W3 Total Cache)"
|
|
done
|
|
fi
|
|
|
|
if [ -z "$no_cdn" ] && [ -z "$no_cache" ]; then
|
|
echo " ✅ Sites are well-optimized (CDN + caching in place)"
|
|
fi
|
|
echo ""
|
|
|
|
# Current CSF setting
|
|
local current_ct=$(get_current_ct_limit)
|
|
echo -e "${BOLD}Current CSF Configuration:${NC}"
|
|
echo " CT_LIMIT = $current_ct"
|
|
echo ""
|
|
|
|
# Generate recommendation
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo -e "${BOLD}📊 RECOMMENDED CT_LIMIT VALUES${NC}"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
|
|
# Calculate safe recommendations
|
|
local conservative=$((max_legitimate + 20))
|
|
local balanced=$((max_legitimate + 10))
|
|
local aggressive=$((max_legitimate + 5))
|
|
|
|
# Factor in site complexity - high-complexity sites need more headroom
|
|
if [ -f "$TEMP_ANALYSIS/per_site_analysis.txt" ]; then
|
|
local avg_complexity=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" | awk -F'|' '{sum+=$3; count++} END {if(count>0) print int(sum/count); else print 0}')
|
|
local max_complexity=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" | awk -F'|' '{if($3>max) max=$3} END {print max+0}')
|
|
|
|
# Add complexity buffer (0-20 based on average complexity)
|
|
local complexity_buffer=$((avg_complexity * 2))
|
|
conservative=$((conservative + complexity_buffer))
|
|
balanced=$((balanced + (complexity_buffer / 2)))
|
|
|
|
# If we have ecommerce sites, be extra conservative
|
|
local has_ecommerce=$(tail -n +2 "$TEMP_ANALYSIS/per_site_analysis.txt" | grep -c "ecommerce")
|
|
if [ "$has_ecommerce" -gt 0 ]; then
|
|
conservative=$((conservative + 15))
|
|
balanced=$((balanced + 10))
|
|
fi
|
|
fi
|
|
|
|
# Minimum safety thresholds
|
|
[ "$conservative" -lt 100 ] && conservative=100
|
|
[ "$balanced" -lt 80 ] && balanced=80
|
|
[ "$aggressive" -lt 50 ] && aggressive=50
|
|
|
|
# Cap at server's max safe capacity
|
|
if [ -f "$TEMP_ANALYSIS/server_resources.txt" ]; then
|
|
IFS='|' read -r total_ram cpu_cores max_safe_conn < "$TEMP_ANALYSIS/server_resources.txt"
|
|
|
|
if [ "$conservative" -gt "$max_safe_conn" ]; then
|
|
conservative=$max_safe_conn
|
|
echo " ${MEDIUM_COLOR}Note: Conservative capped at server max ($max_safe_conn)${NC}" >&2
|
|
fi
|
|
if [ "$balanced" -gt "$max_safe_conn" ]; then
|
|
balanced=$max_safe_conn
|
|
fi
|
|
fi
|
|
|
|
echo -e "${BOLD}1. CONSERVATIVE${NC} (Recommended for high-traffic sites)"
|
|
echo " CT_LIMIT = $conservative"
|
|
echo " • Allows headroom for traffic spikes"
|
|
echo " • Won't block legitimate users"
|
|
echo " • Good protection against moderate attacks"
|
|
echo ""
|
|
|
|
echo -e "${BOLD}2. BALANCED${NC} (Recommended for most servers) ⭐"
|
|
echo " CT_LIMIT = $balanced"
|
|
echo " • Balances security and usability"
|
|
echo " • Based on 99th percentile + buffer"
|
|
echo " • Blocks most attack traffic"
|
|
echo ""
|
|
|
|
echo -e "${BOLD}3. AGGRESSIVE${NC} (Only if under active attack)"
|
|
echo " CT_LIMIT = $aggressive"
|
|
echo " • Tight connection limits"
|
|
echo " • May affect some legitimate users"
|
|
echo " • Maximum DDoS protection"
|
|
echo ""
|
|
|
|
# Whitelist recommendations
|
|
if [ "$bot_count" -gt 0 ]; then
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo -e "${BOLD}⚠️ WHITELIST RECOMMENDATIONS${NC}"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo "Found bots/crawlers with high connection counts."
|
|
echo "Consider whitelisting these IPs to prevent blocking:"
|
|
echo ""
|
|
|
|
# Show top legitimate bots
|
|
tail -n +2 "$TEMP_ANALYSIS/connections_by_ip.txt" | while IFS='|' read -r ip domain max_concurrent total_requests user_agent; do
|
|
[ -z "$ip" ] && continue
|
|
local classification=$(classify_ip_behavior "$ip" "$user_agent" "$max_concurrent" "$total_requests")
|
|
|
|
if [[ "$classification" =~ (legitimate_bot|ai_crawler|monitoring_service) ]]; then
|
|
local bot_name=$(echo "$user_agent" | grep -oE '(Googlebot|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|SemrushBot|AhrefsBot|facebookexternalhit|Twitterbot|LinkedInBot|UptimeRobot|Pingdom)' | head -1)
|
|
[ -z "$bot_name" ] && bot_name="Bot"
|
|
|
|
if [ "$max_concurrent" -gt "$balanced" ]; then
|
|
printf " • %-15s (%-20s) %3d connections\n" "$ip" "$bot_name" "$max_concurrent"
|
|
fi
|
|
fi
|
|
done | sort -t'(' -k2 -u | head -10
|
|
|
|
echo ""
|
|
echo "To whitelist: Add to /etc/csf/csf.ignore or use: csf -a <IP>"
|
|
fi
|
|
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo -e "${BOLD}🔧 HOW TO APPLY${NC}"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo "1. Edit CSF configuration:"
|
|
echo " ${BOLD}nano /etc/csf/csf.conf${NC}"
|
|
echo ""
|
|
echo "2. Find CT_LIMIT line and change to recommended value:"
|
|
echo " ${BOLD}CT_LIMIT = \"$balanced\"${NC}"
|
|
echo ""
|
|
echo "3. Enable SYNFLOOD protection (if not already):"
|
|
echo " ${BOLD}SYNFLOOD = \"1\"${NC}"
|
|
echo ""
|
|
echo "4. Apply changes:"
|
|
echo " ${BOLD}csf -r${NC}"
|
|
echo ""
|
|
echo "5. Monitor effectiveness:"
|
|
echo " ${BOLD}watch -n 2 'csf -g | tail -20'${NC}"
|
|
echo ""
|
|
|
|
# Save recommendation to file
|
|
cat > "$TEMP_ANALYSIS/recommendation.txt" <<EOF
|
|
CT_LIMIT Optimization Report
|
|
Generated: $(date)
|
|
Analysis Period: Last $ANALYSIS_HOURS hours
|
|
|
|
RECOMMENDED VALUES:
|
|
- Conservative: CT_LIMIT = "$conservative"
|
|
- Balanced: CT_LIMIT = "$balanced" (RECOMMENDED)
|
|
- Aggressive: CT_LIMIT = "$aggressive"
|
|
|
|
ANALYSIS DATA:
|
|
- Total IPs analyzed: $total_ips
|
|
- Legitimate users: $legit_count
|
|
- Bots/Crawlers: $bot_count
|
|
- Max legitimate connections: $max_legitimate
|
|
- Current CT_LIMIT: $current_ct
|
|
|
|
To apply balanced recommendation:
|
|
1. Edit /etc/csf/csf.conf
|
|
2. Set: CT_LIMIT = "$balanced"
|
|
3. Run: csf -r
|
|
EOF
|
|
|
|
echo "Full report saved to: $TEMP_ANALYSIS/recommendation.txt"
|
|
echo ""
|
|
}
|
|
|
|
apply_recommendation() {
|
|
local new_limit="$1"
|
|
|
|
if [ ! -f "/etc/csf/csf.conf" ]; then
|
|
print_error "CSF not installed or config not found"
|
|
return 1
|
|
fi
|
|
|
|
print_status "Backing up CSF configuration..."
|
|
cp /etc/csf/csf.conf "/etc/csf/csf.conf.backup.$(date +%Y%m%d_%H%M%S)"
|
|
|
|
print_status "Setting CT_LIMIT = $new_limit..."
|
|
sed -i "s/^CT_LIMIT = .*/CT_LIMIT = \"$new_limit\"/" /etc/csf/csf.conf
|
|
|
|
# Also ensure SYNFLOOD is enabled
|
|
if grep -q "^SYNFLOOD = \"0\"" /etc/csf/csf.conf; then
|
|
print_status "Enabling SYNFLOOD protection..."
|
|
sed -i 's/^SYNFLOOD = "0"/SYNFLOOD = "1"/' /etc/csf/csf.conf
|
|
fi
|
|
|
|
print_status "Restarting CSF..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
print_success "CT_LIMIT updated to $new_limit and CSF restarted!"
|
|
echo ""
|
|
echo "Monitor effectiveness with: csf -g | tail -20"
|
|
}
|
|
|
|
################################################################################
|
|
# Main
|
|
################################################################################
|
|
|
|
main() {
|
|
# Check for auto mode
|
|
local AUTO_MODE=0
|
|
if [ "$1" = "--auto" ] || [ "$1" = "-a" ]; then
|
|
AUTO_MODE=1
|
|
fi
|
|
|
|
if [ $AUTO_MODE -eq 0 ]; then
|
|
clear
|
|
print_banner "CT_LIMIT Optimizer - Intelligent Connection Limit Calculator"
|
|
echo ""
|
|
echo "This tool analyzes your actual traffic patterns to recommend"
|
|
echo "an optimal CT_LIMIT that protects against DDoS without blocking"
|
|
echo "legitimate users, bots, and CDNs."
|
|
echo ""
|
|
echo "Analysis period: Last $ANALYSIS_HOURS hours"
|
|
echo ""
|
|
|
|
read -p "Press Enter to start analysis or Ctrl+C to cancel..."
|
|
echo ""
|
|
else
|
|
echo "Running CT_LIMIT analysis in auto mode..."
|
|
echo ""
|
|
fi
|
|
|
|
# Check if sysref database exists, build if needed
|
|
if [ ! -f "$SYSREF_DB" ] || [ ! -s "$SYSREF_DB" ]; then
|
|
print_status "Building system reference database (first run)..."
|
|
build_reference_database >/dev/null 2>&1
|
|
fi
|
|
|
|
# Run analysis
|
|
check_server_resources
|
|
analyze_apache_logs "$ANALYSIS_HOURS"
|
|
analyze_per_site_traffic
|
|
analyze_current_connections
|
|
|
|
# Generate and show recommendations
|
|
generate_recommendation
|
|
|
|
# Apply automatically in auto mode, otherwise ask
|
|
if [ $AUTO_MODE -eq 1 ]; then
|
|
# Extract balanced value from recommendation
|
|
local balanced=$(grep "2. BALANCED" -A1 "$TEMP_ANALYSIS/recommendation.txt" | grep "CT_LIMIT" | grep -oE '[0-9]+')
|
|
|
|
if [ -n "$balanced" ]; then
|
|
echo ""
|
|
echo "Auto-applying BALANCED recommendation..."
|
|
apply_recommendation "$balanced"
|
|
else
|
|
print_error "Could not determine balanced recommendation value"
|
|
return 1
|
|
fi
|
|
else
|
|
# Offer to apply
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
read -p "Would you like to apply the BALANCED recommendation automatically? (y/n): " apply
|
|
|
|
if [[ "$apply" =~ ^[Yy] ]]; then
|
|
# Extract balanced value from recommendation
|
|
local balanced=$(grep "2. BALANCED" -A1 "$TEMP_ANALYSIS/recommendation.txt" | grep "CT_LIMIT" | grep -oE '[0-9]+')
|
|
|
|
if [ -n "$balanced" ]; then
|
|
apply_recommendation "$balanced"
|
|
else
|
|
print_error "Could not determine balanced recommendation value"
|
|
fi
|
|
else
|
|
echo ""
|
|
echo "No changes made. You can apply manually using the commands above."
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if [ $AUTO_MODE -eq 0 ]; then
|
|
print_success "Analysis complete!"
|
|
fi
|
|
}
|
|
|
|
main "$@"
|