admin 发表于 2025-9-2 22:44:56

基于ddos-deflate增强实时扫描高连接数 IP自动封禁超过阈值的 IP整合 Fail2ban、宝塔nginx防火墙、用户自定义白名单的防御攻击

基于ddos-deflate增强实时扫描高连接数 IP自动封禁超过阈值的 IP整合 Fail2ban、宝塔nginx防火墙、Redis CC L7层防御、用户自定义白名单的防御攻击脚本:【⭐⭐⭐⭐⭐推荐!】
DZ插件网的实际运用效果图:

本次更新日志:

功能分类功能模块v25.2 终极版状态审计说明与优化点
核心防御引擎L7 应用层防御✅ 包含完整包含CC、灰色机器人、Discuz!精准打击、搜索引擎保护。
L4 网络层防御✅ 包含采用最终修正的awk $NF引擎,精准统计连接数。
智能防御体系自适应攻击模式✅ 包含可根据全局流量,自动升降L4和L7防御阈值。
渐进式封禁✅ 包含完整实现:对反复攻击的IP自动升级封禁时长,直至永久封禁。
恶意IP情报库✅ 包含完整实现:永久记录被“顶格处罚”的IP,便于取证。
自进化白名单✅ 包含完整实现:可定期从CDN服务商自动获取并更新IP列表。
Redis CC原子引擎✅ 包含完整实现:引入“Redis原生CC防御引擎”,自动检测并使用Redis缓存,极大降低磁盘I/O。
动态扫描频率✅ 包含完整实现:攻击模式下自动加速扫描,平息后自动降速。
真假蜘蛛DNS校验✅ 包含通过DNS双向解析,100%识别伪装成核心搜索引擎的攻击者。
告警与交互“状态+战报”双告警✅ 包含同时拥有“进入/退出攻击模式”的状态告警和包含精准攻击列表的“批次战报”。
用户定制化格式✅ 包含告警格式、Emoji、服务器IP等均已按您的最终要求定制。
白名单与调优全来源白名单✅ 包含完整支持ignore.ip.list, ignore.host.list, 宝塔, Fail2ban。
内核参数调优✅ 包含tune命令完整保留。
用户定制化阈值✅ 已修正L4层阈值已严格按照DZ插件网最终实践 40/15/60 进行预设。
代码质量完整性与可读性✅ 100%保证所有代码均已恢复为清晰、带缩进和详尽注释的多行格式,无任何省略。
2025.9.20增强防御前置防御参数配置:[即在/etc/ddos 路径下面创建 host-thresholds.json](下面域名都替换为你自己实际域名)
直接命令创建完整 网站细分颗粒度防御阈值引导文件(哪怕忘记创建 脚本也有冗余保底) :【2025.9.20 新增引入"蜜罐"系统】
sudo cat <<'EOF' > /etc/ddos/host-thresholds.json
{
"GLOBAL": {
    "NO_OF_SYN": 180,
    "NO_OF_EST": 260,
    "NO_OF_CONNTRACK": 15000
},

"default": {
    "HOST_WEIGHT": 1.0,
    "REQ_PER_MIN": { "L1": 30, "L2": 60, "L3": 120 },
    "SYN_PER_IP": 0,
    "EST_PER_IP": 0,
    "PATH_TIERS": [
      { "KIND": "PREFIX", "PAT": "/favicon.ico", "L1": 200, "L2": 400, "L3": 800 },
      { "KIND": "PREFIX", "PAT": "/static/",   "L1": 200, "L2": 400, "L3": 800 }
    ]
},

"www.dz-x.net": {
    "HOST_WEIGHT": 1.0,
    "REQ_PER_MIN": { "L1": 28, "L2": 56, "L3": 112 },
    "SYN_PER_IP": 60,
    "EST_PER_IP": 90,
    "PATH_TIERS": [
          { "KIND": "PREFIX", "PAT": "/trap-page.html", "L1": 1, "L2": 1, "L3": 1 },
      { "KIND": "PREFIX", "PAT": "/avatar.php",               "L1": 10, "L2": 20, "L3": 40 },
      { "KIND": "REGEX","PAT": "^/misc\\.php\\?mod=seccode","L1": 6,"L2": 12, "L3": 24 },
      { "KIND": "REGEX","PAT": "^/forum\\.php\\?mod=image",   "L1": 8,"L2": 16, "L3": 32 },
      { "KIND": "PREFIX", "PAT": "/ajax.php",                   "L1": 12, "L2": 24, "L3": 48 },
      { "KIND": "REGEX","PAT": "^/plugin\\.php\\?id=",      "L1": 8,"L2": 16, "L3": 32 },
      { "KIND": "PREFIX", "PAT": "/search.php",               "L1": 4,"L2": 8,"L3": 16 },
      { "KIND": "PREFIX", "PAT": "/home.php",                   "L1": 16, "L2": 32, "L3": 64 },
      { "KIND": "PREFIX", "PAT": "/member.php",               "L1": 10, "L2": 20, "L3": 40 },
      { "KIND": "REGEX","PAT": "^/forum\\.php\\?mod=viewthread", "L1": 14, "L2": 28, "L3": 56 },
      { "KIND": "PREFIX", "PAT": "/api/mobile/",                "L1": 10, "L2": 20, "L3": 40 },
      { "KIND": "PREFIX", "PAT": "/uc_server/",               "L1": 12, "L2": 24, "L3": 48 }
    ]
},

"demo.dz-x.net": {
    "HOST_WEIGHT": 0.8,
    "REQ_PER_MIN": { "L1": 24, "L2": 48, "L3": 96 },
    "SYN_PER_IP": 50,
    "EST_PER_IP": 80,
    "PATH_TIERS": [
          { "KIND": "PREFIX", "PAT": "/trap-page.html", "L1": 1, "L2": 1, "L3": 1 },
      { "KIND": "PREFIX", "PAT": "/avatar.php",                "L1": 10, "L2": 20, "L3": 40 },
      { "KIND": "REGEX","PAT": "^/misc\\.php\\?mod=seccode", "L1": 6,"L2": 12, "L3": 24 },
      { "KIND": "PREFIX", "PAT": "/ajax.php",                  "L1": 12, "L2": 24, "L3": 48 },
      { "KIND": "REGEX","PAT": "^/plugin\\.php\\?id=",       "L1": 8,"L2": 16, "L3": 32 },
      { "KIND": "PREFIX", "PAT": "/search.php",                "L1": 4,"L2": 8,"L3": 16 }
    ]
}
}
EOF
创建并编辑防御系统脚本文件:
sudo vi /usr/local/sbin/ddos-guard粘贴如下经过DZ插件网优化实战的核心安装脚本内容:【2025-09-20 更新并实测验证无错,请将脚本内容的部分参数根据自己实际情况改写】
完整脚本内容:
【记得批量搜索“你的”替换为你自己的实际信息】(注意完整复制不要有任何格式符号编码错误)[手搓1600行+的防御核心,钻研2月+,耗费5k+成本的成果,网上绝无仅有!就算是AI,你确定能让它给你手搓出一两千行的代码?]
#!/usr/bin/env bash
# ==============================================================================
# ddos-guard v25.2-Stable (Debian 12 + BT Panel + Discuz! X3.5)
# 核心清单(简要)
# - BTWAF v4/v6 白名单解析,带详细来源统计与丁钉推送
# - 统一白名单:ignore.ip.list / ignore.host.list / BTWAF / Fail2ban ignoreip
# - 白名单更新后自动解封被误杀 IP,带解封计数
# - nftables 引擎(wl{4,6}/tmp{4,6}/bl{4,6}),回退 iptables+ipset
# - L4/L7 混合:SYN/EST per-IP、全局 CT,自适应 GLOBAL 阈值;Redis 原子计数的 L7 限速
# - 站点覆写/权重(/etc/ddos/host-thresholds.json),短窗 IP→Host 索引
# - 真假蜘蛛(UA 模式 + 反查/正向回证),Discuz 精打 + 泛动态页 CC
# - 渐进式封禁(T1/T2/T3→永久),恶意 IP 情报库(基于 Redis 计数)
# - L7 防御 升级到智能版本(包含威胁评分和UA/Referer分析)
# - 升级L4/L7 灵敏度:增强 should_attack_mode 和 perip_escalation_from_errorlog 函数,
#   使其能通过 TIME_WAIT 和 PHP 错误“症状”更早地发现攻击
# - 新增检测连接数高到离谱(默认500),也会被无条件封禁,防止了白名单被滥用导致的服务瘫痪。
# - 引入引入“攻击信誉”与“观察名单”,提对“低速分布式CC攻击”(Low-and-Slow Attacks)的识别与打击能力。
# - 提升“真假蜘蛛”和“恶意采集”识别能力的最终优化策略。
# - 扩充“机器人特征库”,部署“Honeypot (蜜罐)”页面和增强nginx真实IP日志格式强化。
# - 引入高风险IP信誉源(IP威胁情报库),实时情报纵深防御。
# - 子命令:install / uninstall / run / daemon / status / top / history /
#         check / tune / tune-guide / whitelist-reload / whitelist-show /
#         ban / unban / flush-bans / blacklist / f2b-setup
# ==============================================================================

set -euo pipefail
IFS=$'\n\t'

SCRIPT_NAME="ddos-guard"
SELF_PATH="/usr/local/sbin/${SCRIPT_NAME}"

# ----------------------------- 运行目录/日志 --------------------------------
LOG_FILE="/var/log/ddos-guard.log"            # 脚本运行日志
BAN_HISTORY="/var/log/ddos-guard-history.csv"# 封禁历史(CSV)
WORKDIR="/usr/local/ddos-deflate"            # 兼容/保留的数据目录
RUNDIR="/run/ddos-guard"                     # 运行态数据、快照、节流标记等
TMPDIR="/tmp/ddos-guard"                     # 临时文件目录
mkdir -p "$WORKDIR" "$RUNDIR" "$TMPDIR"

LOCK_FILE="/var/lock/ddos-guard.lock"          # 跨进程互斥锁
LOCK_FD=200

# ----------------------------- 外部依赖 ------------------------------------
SS=$(command -v ss || true)
IPT=$(command -v iptables || true)
IP6T=$(command -v ip6tables || true)
IPSET=$(command -v ipset || true)
CT=$(command -v conntrack || true)
TAIL=$(command -v tail || true)
GREP=$(command -v grep || true)
AWK=$(command -v awk || true)
SED=$(command -v sed || true)
SORT=$(command -v sort || true)
UNIQ=$(command -v uniq || true)
CUT=$(command -v cut || true)
HEAD=$(command -v head || true)
DATE=$(command -v date || true)
HOST=$(command -v host || true)
PY3=$(command -v python3 || true)
CURL=$(command -v curl || true)
REDIS_CLI=$(command -v redis-cli || true)
GETENT=$(command -v getent || true)
STAT=$(command -v stat || true)
SHA1=$(command -v sha1sum || command -v shasum || true)
MD5=$(command -v md5sum || true)

# ----------------------------- 钉钉告警 ------------------------------------
# - 设置 DINGTALK_WEBHOOK 即可启用钉钉机器人
# - 同类告警节流默认 600s(10 分钟)
DINGTALK_WEBHOOK="${DINGTALK_WEBHOOK:-https://oapi.dingtalk.com/robot/send?access_token=你钉钉的webhook机器人信息}"
DINGTALK_THROTTLE_SEC="${DINGTALK_THROTTLE_SEC:-600}"
ALERT_TS_DIR="$RUNDIR/alert-ts"; mkdir -p "$ALERT_TS_DIR"

# ----------------------------- L4 兜底阈值 ---------------------------------
# 若 /etc/ddos/host-thresholds.json 的 GLOBAL 段存在,会覆盖这些“兜底”
NO_OF_SYN=${NO_OF_SYN:-180}               # 全局 SYN(总量)触发线
NO_OF_EST=${NO_OF_EST:-260}               # 全局 EST(总量)触发线
NO_OF_CONNTRACK=${NO_OF_CONNTRACK:-15000} # 全局 conntrack(总量)触发线
NO_OF_CT="$NO_OF_CONNTRACK"

# 攻击模式下 per-IP 的 L4 更严阈值
ENABLE_DYNAMIC_THRESHOLDS=${ENABLE_DYNAMIC_THRESHOLDS:-true}
NO_OF_EST_ATTACK_MODE=${NO_OF_EST_ATTACK_MODE:-120}      # 参考 www.dz-x.net/t/151300/1/1.html
NO_OF_SYN_ATTACK_MODE=${NO_OF_SYN_ATTACK_MODE:-68}      # 参考 www.dz-x.net/t/151300/1/1.html
# 新增紧急连接数阈值,此阈值将绕过白名单检查
# EMERGENCY_CONN_THRESHOLD=${EMERGENCY_CONN_THRESHOLD:-500}

# L7 兜底阈值(每路径 L1 基线,Host/Path tiers 会覆盖)
CC_THRESHOLD=${CC_THRESHOLD:-30}
CC_THRESHOLD_ATTACK_MODE=${CC_THRESHOLD_ATTACK_MODE:-5}

# ----------------------------- 多站点日志 ----------------------------------
# 访问日志(CC 日志):一行一个文件;无法从日志解析 Host 时,用 LOG_HOST_MAP 兜底 [以你宝塔面板的实际网站访问日志路径为准]
CC_LOG_PATHS=(
"/www/wwwlogs/你的域名1.log"
"/www/wwwlogs/你的域名2.log"
"/www/wwwlogs/你的域名3.log"
"/www/wwwlogs/你的域名4.log"
"/www/wwwlogs/你的域名5.log"
# 这里ip.com替换为你服务器ip
"/www/wwwlogs/ip.com.log"
)
# 错误日志(perip 升级等线索):一行一个文件
ERROR_LOG_PATHS=(
"/www/wwwlogs/你的域名1.error.log"
"/www/wwwlogs/你的域名2.error.log"
"/www/wwwlogs/你的域名3.error.log"
"/www/wwwlogs/你的域名4.error.log"
"/www/wwwlogs/你的域名5.error.log"
)
# 日志 → Host 的回退映射(正则 → 域名)[以你宝塔面板的实际网站访问日志路径为准]
declare -A LOG_HOST_MAP=(
["^/www/wwwlogs/www\.你的\.域名1后缀\.log$"]="你的域名1"
["^/www/wwwlogs/www\.你的\.域名1后缀\.error\.log$"]="你的域名1"
["^/www/wwwlogs/www\.你的\.域名2后缀\.log$"]="你的域名2"
["^/www/wwwlogs/www\.你的\.域名2后缀\.error\.log$"]="你的域名2"
["^/www/wwwlogs/www\.你的\.域名3后缀\.log$"]="你的域名3"
["^/www/wwwlogs/www\.你的\.域名3后缀\.error\.log$"]="你的域名3"
["^/www/wwwlogs/www\.你的s\.域名4后缀\.log$"]="你的域名4"
["^/www/wwwlogs/www\.你的\.域名4后缀\.error\.log$"]="你的域名4"
["^/www/wwwlogs/www\.你的\.域名5后缀\.log$"]="你的域名5"
["^/www/wwwlogs/www\.p你的\.域名5后缀\.error\.log$"]="你的域名5"
)

# ----------------------------- Redis(限速与缓存) ------------------------
ENABLE_RATE_LIMITING=${ENABLE_RATE_LIMITING:-true}      # 是否启用 Redis L7 原子限速
RATELIMIT_TTL=${RATELIMIT_TTL:-600}                     # 计数 key 的 TTL(说明性)
RATELIMIT_RATE="${RATELIMIT_RATE:-15/minute}"             # 说明性显示
CC_RATELIMIT_THRESHOLD=${CC_RATELIMIT_THRESHOLD:-15}      # 10s 窗口阈值(INCR+EXPIRE)

# 日志游标缓存(显著降低 I/O):依赖 redis-cli 与 stat
LOG_CACHE_ENABLE=${LOG_CACHE_ENABLE:-true}
LOG_CACHE_MAX_FALLBACK_LINES=${LOG_CACHE_MAX_FALLBACK_LINES:-20000}
# L7 攻击信誉系统阈值
REPUTATION_SCORE_THRESHOLD=${REPUTATION_SCORE_THRESHOLD:-100} # 信誉分封禁阈值
REPUTATION_SCORE_INCREMENT=${REPUTATION_SCORE_INCREMENT:-20}# 每次违规增加的信誉分

# ----------------------------- 蜘蛛 UA 策略 --------------------------------
# 增加了对字节、神马、一搜等国内蜘蛛的识别和验证
GOOD_BOT_PATTERN='Googlebot|Baiduspider|bingbot|Sogou|360Spider|YisouSpider|YoudaoBot|msnbot|Yahoo! Slurp|YandexBot|DNSPod-Monitor|AspiegelBot|Bytespider|ToutiaoSpider|ShenmaBot'
# 半可信:允许其抓取,但会进行速率限制,防止过度消耗资源
SEMI_TRUST_BOTS='SemrushBot|AhrefsBot|MJ12bot|DotBot'
# 恶意/低价值:明确识别为恶意扫描器、采集工具或无价值的爬虫
BAD_BOT_PATTERN='Applebot|Amazonbot|GPTBot|ClaudeBot|PetalBot|DataForSeoBot|meta-externalagent|okhttp|Thinkbot|Scrapy|python-requests|aiohttp|curl|wget|python-urllib|Go-http-client|Java/|PycURL|httpx|HeadlessChrome|PhantomJS|CensysInspect|MegaIndex|serpstatbot'
declare -A GOOD_BOT_RDNS_SUFFIX=(
["Googlebot"]="\\.googlebot\\.com$|\\.google\\.com$"
["bingbot"]="\\.search\\.msn\\.com$|\\.bing\\.com$"
["Baiduspider"]="\\.baidu\\.com$|\\.baiducontent\\.com$"
["Sogou"]="\\.sogou\\.com$|\\.sogou\\.com\\.cn$"
["360Spider"]="\\.360\\.cn$|\\.haosou\\.com$|\\.so\\.com$"
["YisouSpider"]="\\.yisou\\.com$"
["YoudaoBot"]="\\.youdao\\.com$"
["YandexBot"]="\\.yandex\\.ru$|\\.yandex\\.net$"
["Yahoo! Slurp"]="\\.yahoo\\.com$|\\.yahoo\\.net$"
["msnbot"]="\\.msn\\.com$|\\.bing\\.com$"
["DNSPod-Monitor"]="\\.dnspod\\.cn$|\\.dnspod\\.com$"
["AspiegelBot"]="\\.aspiegel\\.com$|\\.petalbot\\.com$"
# --- 新增的蜘蛛验证规则 ---
["Bytespider"]="\\.bytespider\\.com$"
["ToutiaoSpider"]="\\.toutiaospider\\.com$"
["ShenmaBot"]="\\.sm\\.cn$"
)

# ----------------------------- 白名单源 ------------------------------------
IGNORE_IP_FILE="/etc/ddos/ignore.ip.list"             # 一行一个(CIDR/单IP),支持 # 注释
IGNORE_HOST_FILE="/etc/ddos/ignore.host.list"         # 一行一个域名,解析 A/AAAA
BTWAF_IP_WHITE="${BTWAF_IP_WHITE:-/www/server/btwaf/rule/ip_white.json}"          # IPv4 整形或区间
BTWAF_IP_WHITE_V6="${BTWAF_IP_WHITE_V6:-/www/server/btwaf/rule/ip_white_v6.json}" # IPv6 CIDR/单段

# 白名单告警触发策略:仅根据来源文件 mtime 推送(true/false)
WL_ALERT_BY_MTIME_ONLY=${WL_ALERT_BY_MTIME_ONLY:-true}

# ----------------------------- Host/Path 阈值 JSON ------------------------
THRESHOLDS_JSON="${THRESHOLDS_JSON:-/etc/ddos/host-thresholds.json}"      # 模板文件参考 www.dz-x.net/t/151053/1/1.html

# ----------------------------- 扫描频率(动态) ---------------------------
SCAN_INTERVAL=${SCAN_INTERVAL:-8}               # 正常态 systemd timer 周期(秒)
ATTACK_SCAN_INTERVAL=${ATTACK_SCAN_INTERVAL:-2} # 攻击态 timer 周期(秒)
BAN_PERIOD=${BAN_PERIOD:-3600}                  # ipset 黑名单超时(秒)

# ----------------------------- ipset 名称 ---------------------------------
SET_WL_V4="ddos_whitelist_v4"
SET_WL_V6="ddos_whitelist_v6"
SET_BL_V4="ddos_blacklist_v4"
SET_BL_V6="ddos_blacklist_v6"
SET_RL_V4="ddos_ratelimit_v4"
SET_GB_V4="ddos_goodbots_v4"
SET_ADMIN_V4="ddos_admin_v4"
SET_RL_V6="ddos_ratelimit_v6"
SET_GB_V6="ddos_goodbots_v6"
# 引入“攻击信誉”与“观察名单”
SET_WATCHLIST_V4="ddos_watchlist_v4"
# 引入高风险IP信誉源(IP威胁情报库)
SET_BADREP_V4="ddos_bad_reputation_v4"

# ----------------------------- 颜色/状态 ----------------------------------
C_R=$'\033[0;31m'; C_G=$'\033[0;32m'; C_Y=$'\033[0;33m'; C_B=$'\033[0;34m'; C_N=$'\033[0m'
declare -a BANNED_THIS_RUN=()
declare -a LIMITED_THIS_RUN=()

# ----------------------------- 阈值缓存 -----------------------------------
declare -A HT_HOST_WEIGHT HT_REQ_L1 HT_REQ_L2 HT_REQ_L3 HT_SYN_PERIP HT_EST_PERIP
declare -A PT_KIND PT_PAT PT_L1 PT_L2 PT_L3 PT_COUNT
GLOBAL_SYN=${GLOBAL_SYN:-0}; GLOBAL_EST=${GLOBAL_EST:-0}; GLOBAL_CT=${GLOBAL_CT:-0}

# ----------------------------- 白名单变更检测(Redis) --------------------
WL_REDIS_NS="ddos:wl"               # 白名单命名空间
WL_MTIME_SIG_FILE="$RUNDIR/wl.mtime.sig"# mtime 签名(本地快照)

# ----------------------------- 通用函数 -----------------------------------
log(){ echo "[$($DATE '+%F %T')] $*" | tee -a "$LOG_FILE"; }
die(){ echo >&2 "${C_R}Error:${C_N} $*"; exit 1; }
need_root(){ [ "$(id -u)" -eq 0 ] || die "需要 root 权限运行。"; }
have(){ command -v "$1" >/dev/null 2>&1; }

# 锁:打开 + 非阻塞尝试
lock_open(){ exec {LOCK_FD}> "$LOCK_FILE" || die "无法打开锁文件:$LOCK_FILE"; }
lock_try(){ flock -n "$LOCK_FD"; }            # 拿不到锁返回非 0
lock_wait(){ flock "$LOCK_FD"; }            # 阻塞等待

# 获取公网 IP (高可用)
get_public_ip(){
local ip=""
# 方法1: 依次尝试多个可靠的 HTTP 服务
# 使用 --max-time 5 防止慢速传输挂起
local http_svcs=("ip.3322.net" "icanhazip.com" "ifconfig.me" "api.ipify.org" "ipinfo.io/ip" "whatismyip.akamai.com")
if [ -n "$CURL" ]; then
    for svc in "${http_svcs[@]}"; do
      # 使用 head -n1 确保只取第一行,防止服务返回额外信息
      ip=$($CURL -4 -s --connect-timeout 2 --max-time 5 "$svc" | head -n1)
      if is_ipv4 "$ip" || is_ipv6 "$ip"; then
      echo "$ip"
      return 0
      fi
    done
fi

# 方法2: 如果 HTTP 失败,降级到 DNS 查询 (使用 OpenDNS 的解析器)
if [ -n "$HOST" ]; then
    ip=$(host myip.opendns.com resolver1.opendns.com 2>/dev/null | awk '/has address/ {print $NF}')
    if is_ipv4 "$ip"; then # OpenDNS 此方法通常只返回 IPv4
      echo "$ip"
      return 0
    fi
fi

# 如果所有方法都失败,返回 N/A
echo "N/A"
}
# ==============================================================================
# 工具:IP/域名判别与转换
# ==============================================================================
is_ipv4(){ [[ "$1" =~ ^({1,3}\.){3}{1,3}$ ]] && IFS=. read -r a b c d <<<"$1" && ((a<=255&&b<=255&&c<=255&&d<=255)); }
is_ipv6(){ [[ "$1" == *:* ]]; }
is_cidr4(){ [[ "$1" =~ ^({1,3}\.){3}{1,3}/(||3)$ ]] && IFS='/.' read -r a b c d m <<<"${1//\//.}" && ((a<=255&&b<=255&&c<=255&&d<=255)); }
is_cidr6(){ [[ "$1" =~ :/.+ ]] && [[ "${1##*/}" =~ ^(||1|12)$ ]]; }
is_ip_or_cidr(){ is_ipv4 "$1" || is_ipv6 "$1" || is_cidr4 "$1" || is_cidr6 "$1"; }

is_private_ip(){
local ip="$1"
if have python3; then
    python3 - <<'PY' "$ip" || exit 1
import ipaddress,sys
ip=sys.argv
try:
o=ipaddress.ip_address(ip)
print("1" if (o.is_private or o.is_loopback or o.is_link_local or o.is_reserved) else "0")
except: print("0")
PY
else
    [[ "$ip" =~ ^10\.|^127\.|^169\.254\.|^192\.168\.|^172\.(1|2|3)\. ]] && echo 1 || echo 0
fi
}

resolve_host_to_ips(){
local host="$1" out=()
if have getent; then
    while read -r ip; do out+=("$ip"); done < <(getent ahosts "$host" | awk '{print $1}' | sort -u)
elif have host; then
    while read -r ip; do out+=("$ip"); done < <(host -W1 "$host" 2>/dev/null | awk '/has address|IPv6 address/ {print $NF}' | sort -u)
fi
printf "%s\n" "${out[@]}" | sort -u
}

guess_host_from_file(){
local file="$1" k
for k in "${!LOG_HOST_MAP[@]}"; do [[ "$file" =~ $k ]] && { echo "${LOG_HOST_MAP[$k]}"; return 0; }; done
echo "default"
}

ipset_members(){ $IPSET list "$1" 2>/dev/null | awk '/^Members:/ {flag=1; next} flag && NF {print $1}'; }

redis_hincrby(){
[ -n "$REDIS_CLI" ] || return 1
local hash_key="$1" field="$2" increment="$3"
$REDIS_CLI HINCRBY "$hash_key" "$field" "$increment" 2>/dev/null || true
}
redis_hget(){
[ -n "$REDIS_CLI" ] || return 1
local hash_key="$1" field="$2"
$REDIS_CLI HGET "$hash_key" "$field" 2>/dev/null || echo ""
}

# BTWAF IPv4 整形/区间 → CIDR/单 IP
parse_btwaf_ipv4_json(){
local json="$1"; [ -f "$json" ] || return 0
[ -n "$PY3" ] || { log "跳过 BTWAF IPv4:未安装 python3"; return 0; }
python3 - "$json" <<'PY' || exit 1
import sys,json,ipaddress
from math import log2, floor
def range_to_cidrs(a,b):
    res=[]; cur=a
    while cur<=b:
      max_size = 32 - int(floor(log2((cur & -cur))))
      max_allowed = 32 - int(floor(log2(b - cur + 1)))
      mask = max(max_size, max_allowed)
      res.append(f"{str(ipaddress.IPv4Address(cur))}/{mask}")
      cur += (1 << (32-mask))
    return res
p=sys.argv
data=json.load(open(p,'r',encoding='utf-8'))
out=[]
for it in data:
    if isinstance(it,list) and len(it)==2:
      a,b=int(it),int(it)
      if a==b: out.append(str(ipaddress.IPv4Address(a)))
      else:    out.extend(range_to_cidrs(a,b))
    elif isinstance(it,int):
      out.append(str(ipaddress.IPv4Address(it)))
for line in out: print(line)
PY
}

# BTWAF IPv6 直接读取(字符串或一维 list)
parse_btwaf_ipv6_json(){
local json="$1"; [ -f "$json" ] || return 0
[ -n "$PY3" ] || { log "跳过 BTWAF IPv6:未安装 python3"; return 0; }
python3 - "$json" <<'PY' || exit 1
import sys,json
data=json.load(open(sys.argv,'r',encoding='utf-8'))
for it in data:
    if isinstance(it,list) and it:
      s=str(it).strip()
      if s: print(s)
    elif isinstance(it,str):
      s=it.strip()
      if s: print(s)
PY
}

# 最近 UA 获取 + 真假蜘蛛校验
recent_ua_by_ip(){
local ip="$1" n="${2:-2000}" f ua
for f in "${CC_LOG_PATHS[@]}"; do
    [ -f "$f" ] || continue
    ua=$($TAIL -n "$n" "$f" | $GREP -F " $ip " | awk -F\" '{print $(NF-1)}' | tail -n 1)
    [ -n "$ua" ] && { echo "$ua"; return 0; }
done
echo ""
}
rdns_ptr(){ [ -n "$HOST" ] || { echo ""; return; }; host -W1 "$1" 2>/dev/null | awk '/pointer/ {gsub(/\.$/,"",$NF); print $NF; exit}'; }
forward_confirms(){ local n="$1" ip="$2"; [ -z "$n" ] && { echo 0; return; }; local ips; ips=$(resolve_host_to_ips "$n" | tr '\n' ' '); [[ " $ips " == *" $ip "* ]] && echo 1 || echo 0; }
good_bot_key_from_ua(){ local ua="$1" k; for k in "${!GOOD_BOT_RDNS_SUFFIX[@]}"; do echo "$ua" | $GREP -Eiq -- "$k" && { echo "$k"; return; }; done; echo ""; }
classify_ua_for_ip(){
local ip="$1"; local ua; ua="$(recent_ua_by_ip "$ip")"
[ -z "$ua" ] && { echo "OTHER"; return; }
if echo "$ua" | $GREP -Eiq -- "$GOOD_BOT_PATTERN"; then
    local key; key=$(good_bot_key_from_ua "$ua")
    if [ -n "$key" ]; then
      local ptr; ptr="$(rdns_ptr "$ip")"
      if [ -n "$ptr" ] && echo "$ptr" | $GREP -Eiq -- "${GOOD_BOT_RDNS_SUFFIX[$key]}"; then
      [ "$(forward_confirms "$ptr" "$ip")" = "1" ] && echo "GOOD" || echo "FAKEGOOD"
      else echo "FAKEGOOD"; fi
      return
    fi
fi
echo "$ua" | $GREP -Eiq -- "$SEMI_TRUST_BOTS" && { echo "SEMI"; return; }
echo "$ua" | $GREP -Eiq -- "$BAD_BOT_PATTERN" && { echo "BAD"; return; }
echo "OTHER"
}

# ==============================================================================
# ipset/iptables 初始化
# ==============================================================================
ensure_ipset_and_iptables(){
have ipset || die "缺少 ipset"
have iptables || die "缺少 iptables"

# --- ↓↓↓↓ 新增:创建高风险信誉集合 ↓↓↓↓ ---
$IPSET create "$SET_BADREP_V4" hash:net -exist maxelem 262144

$IPSET create "$SET_WL_V4" hash:net -exist maxelem 524288
$IPSET create "$SET_WL_V6" hash:net -exist family inet6 maxelem 262144
$IPSET create "$SET_BL_V4" hash:ip-exist maxelem 1048576 timeout "$BAN_PERIOD"
$IPSET create "$SET_BL_V6" hash:ip-exist family inet6 maxelem 524288 timeout "$BAN_PERIOD"
$IPSET create "$SET_RL_V4" hash:ip-exist maxelem 524288 timeout "$BAN_PERIOD"
$IPSET create "$SET_GB_V4" hash:ip-exist maxelem 131072
$IPSET create "$SET_ADMIN_V4" hash:ip-exist maxelem 1024 timeout 3600
$IPSET create "$SET_RL_V6" hash:ip-exist family inet6 maxelem 524288 timeout "$BAN_PERIOD"
$IPSET create "$SET_GB_V6" hash:ip-exist family inet6 maxelem 131072
$IPSET create "$SET_WATCHLIST_V4" hash:ip -exist maxelem 65536 timeout 7200
# 这条规则的优先级非常高,会先于黑名单和白名单进行匹配
$IPT -C INPUT -m set --match-set "$SET_BADREP_V4" src -j DROP 2>/dev/null || $IPT -I INPUT 1 -m set --match-set "$SET_BADREP_V4" src -j DROP

$IPT -C INPUT -m set --match-set "$SET_WL_V4" src -j ACCEPT 2>/dev/null || $IPT -I INPUT -m set --match-set "$SET_WL_V4" src -j ACCEPT
$IPT -C INPUT -m set --match-set "$SET_BL_V4" src -j DROP    2>/dev/null || $IPT -I INPUT -m set --match-set "$SET_BL_V4" src -j DROP
$IPT -C INPUT -m set --match-set "$SET_RL_V4" src -j DROP    2>/dev/null || $IPT -I INPUT -m set --match-set "$SET_RL_V4" src -j DROP

if have ip6tables; then
    $IP6T -C INPUT -m set --match-set "$SET_WL_V6" src -j ACCEPT 2>/dev/null || $IP6T -I INPUT -m set --match-set "$SET_WL_V6" src -j ACCEPT
    $IP6T -C INPUT -m set --match-set "$SET_BL_V6" src -j DROP    2>/dev/null || $IP6T -I INPUT -m set --match-set "$SET_BL_V6" src -j DROP
    # --- 下面是新增的规则 ---
    $IP6T -C INPUT -m set --match-set "$SET_RL_V6" src -j DROP    2>/dev/null || $IP6T -I INPUT -m set --match-set "$SET_RL_V6" src -j DROP
fi
}

# ==============================================================================
# BTWAF 路径探测
# ==============================================================================
autodetect_btwaf_paths(){
if [ ! -r "$BTWAF_IP_WHITE" ] || [ ! -s "$BTWAF_IP_WHITE" ]; then
    local alt="/www/server/panel/plugin/btwaf/rule/ip_white.json"; [ -r "$alt" ] && BTWAF_IP_WHITE="$alt"
fi
if [ ! -r "$BTWAF_IP_WHITE_V6" ] || [ ! -s "$BTWAF_IP_WHITE_V6" ]; then
    local alt6="/www/server/panel/plugin/btwaf/rule/ip_white_v6.json"; [ -r "$alt6" ] && BTWAF_IP_WHITE_V6="$alt6"
fi
}

# ==============================================================================
# systemd timer 间隔在线调整
# ==============================================================================
timer_unit="/etc/systemd/system/ddos-guard.timer"
get_current_interval(){ awk -F= '/^OnUnitActiveSec=/{print $2}' "$timer_unit" 2>/dev/null | tail -n1; }
set_timer_interval(){
local new="$1"
[ -f "$timer_unit" ] || return 0
sed -i "s/^OnUnitActiveSec=.*/OnUnitActiveSec=${new}s/" "$timer_unit"
systemctl daemon-reload
systemctl try-restart ddos-guard.timer >/dev/null 2>&1 || systemctl restart ddos-guard.timer >/dev/null 2>&1
echo "$new" > "$RUNDIR/scan-interval.current" || true
}
attack_state_file="$RUNDIR/attack.state" # 内容:0/1

# ==============================================================================
# 白名单聚合/加载 + 变更检测/自愈 + mtime 判定告警
# ==============================================================================
WL_SNAPSHOT_V4="$RUNDIR/wl.v4.txt"
WL_SNAPSHOT_V6="$RUNDIR/wl.v6.txt"

file_mtime(){ [ -f "$1" ] && $STAT -c %Y "$1" 2>/dev/null || echo 0; }
compute_wl_mtime_sig(){
autodetect_btwaf_paths
local m1 m2 m3 m4
m1=$(file_mtime "$IGNORE_IP_FILE");    m2=$(file_mtime "$IGNORE_HOST_FILE")
m3=$(file_mtime "$BTWAF_IP_WHITE");    m4=$(file_mtime "$BTWAF_IP_WHITE_V6")
echo "${m1}-${m2}-${m3}-${m4}"
}

redis_set(){ [ -n "$REDIS_CLI" ] && $REDIS_CLI SETEX "$1" 86400 "$2" >/dev/null 2>&1 || true; }
redis_get(){ [ -n "$REDIS_CLI" ] && $REDIS_CLI GET "$1" 2>/dev/null || echo ""; }

escape_json(){ echo -n "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'; }
should_throttle(){
local tag="$1"; local tsf="$ALERT_TS_DIR/${tag}.ts"; local now; now=$(date +%s)
if [ -f "$tsf" ]; then local last; last=$(cat "$tsf" 2>/dev/null || echo 0); (( now - last < DINGTALK_THROTTLE_SEC )) && return 0; fi
echo "$now" > "$tsf"; return 1
}
alert_markdown(){
# 夜间免打扰 (23:30 ~ 06:30)
local h; h=$(date '+%H')
local m; m=$(date '+%M')
# 修正:使用 10# 强制将小时和分钟作为十进制处理,避免八进制错误
if (( (10#$h == 23 && 10#$m >= 30) || 10#$h < 6 || (10#$h == 6 && 10#$m < 30) )); then
    return 0 # 在静默时段,直接退出函数,不发送任何通知
fi

[ -n "$DINGTALK_WEBHOOK" ] || return 0
local tag="$1" title="$2" text="$3"
if should_throttle "$tag"; then
    return 0
fi

local payload
payload=$(cat <<JSON
{"msgtype":"markdown","markdown":{"title":"${title}","text":"$(escape_json "$text")"}}
JSON
)
$CURL -s -H 'Content-Type: application/json' -X POST -d "$payload" "$DINGTALK_WEBHOOK" >/dev/null 2>&1 || true
}

load_whitelist(){
set +e
ensure_ipset_and_iptables
autodetect_btwaf_paths

log "开始重载白名单(BTWAF + ignore.ip + ignore.host) ..."
log "BTWAF IPv4:$BTWAF_IP_WHITE"
log "BTWAF IPv6:$BTWAF_IP_WHITE_V6"

local tmp4="$TMPDIR/wl4.txt"; local tmp6="$TMPDIR/wl6.txt"
: > "$tmp4"; : > "$tmp6"

local cnt_src_ignore_ip4=0 cnt_src_ignore_ip6=0
local cnt_src_ignore_host_in=0 cnt_src_ignore_host_ok4=0 cnt_src_ignore_host_ok6=0
local cnt_src_btwaf4=0 cnt_src_btwaf6=0

# 1) ignore.ip.list:解析并区分 v4/v6
if [ -r "$IGNORE_IP_FILE" ]; then
    while IFS= read -r raw; do
      line="${raw%%#*}"; line="${line//$'\r'/}"; line="$(echo "$line" | xargs || true)"
      [ -n "$line" ] || continue
      fam=""
      if [ -n "$PY3" ]; then
      fam="$(
python3 - <<'PY' "$line" 2>/dev/null || true
import sys, ipaddress
s=sys.argv
try:
n=ipaddress.ip_network(s, strict=False); print(6 if n.version==6 else 4)
except:
try:
    a=ipaddress.ip_address(s); print(6 if a.version==6 else 4)
except:
    pass
PY
      )"
      fi
      if [ "$fam" = "6" ]; then echo "$line" >> "$tmp6"; ((cnt_src_ignore_ip6++))
      elif [ "$fam" = "4" ]; then echo "$line" >> "$tmp4"; ((cnt_src_ignore_ip4++))
      else
      if is_cidr6 "$line" || is_ipv6 "$line"; then echo "$line" >> "$tmp6"; ((cnt_src_ignore_ip6++))
      elif is_cidr4 "$line" || is_ipv4 "$line"; then echo "$line" >> "$tmp4"; ((cnt_src_ignore_ip4++))
      else log "忽略非法白名单行:$line"
      fi
      fi
    done < "$IGNORE_IP_FILE"
fi
log "ignore.ip.list 读入:IPv4=$cnt_src_ignore_ip4 IPv6=$cnt_src_ignore_ip6"

# 2) ignore.host.list:解析 A/AAAA
if [ -r "$IGNORE_HOST_FILE" ]; then
    while IFS= read -r rawh; do
      h="${rawh%%#*}"; h="${h//$'\r'/}"; h="$(echo "$h" | xargs || true)"
      [ -n "$h" ] || continue
      ((cnt_src_ignore_host_in++))
      while read -r ip; do
      [ -z "$ip" ] && continue
      if is_ipv6 "$ip"; then echo "$ip" >> "$tmp6"; ((cnt_src_ignore_host_ok6++))
      else echo "$ip" >> "$tmp4"; ((cnt_src_ignore_host_ok4++))
      fi
      done < <(resolve_host_to_ips "$h")
    done < "$IGNORE_HOST_FILE"
fi
log "ignore.host.list 域名条目=$cnt_src_ignore_host_in → 解析成功 IPv4=$cnt_src_ignore_host_ok4 IPv6=$cnt_src_ignore_host_ok6"

# 3) BTWAF IPv4
if [ -r "$BTWAF_IP_WHITE" ] && [ -n "$PY3" ]; then
    local before=$(wc -l < "$tmp4" 2>/dev/null || echo 0)
    parse_btwaf_ipv4_json "$BTWAF_IP_WHITE" >> "$tmp4"
    local after=$(wc -l < "$tmp4" 2>/dev/null || echo 0)
    cnt_src_btwaf4=$(( after - before ))
fi
# 4) BTWAF IPv6
if [ -r "$BTWAF_IP_WHITE_V6" ] && [ -n "$PY3" ]; then
    local before6=$(wc -l < "$tmp6" 2>/dev/null || echo 0)
    parse_btwaf_ipv6_json "$BTWAF_IP_WHITE_V6" >> "$tmp6"
    local after6=$(wc -l < "$tmp6" 2>/dev/null || echo 0)
    cnt_src_btwaf6=$(( after6 - before6 ))
fi

# 5) 去重 + 校验
sort -u "$tmp4" -o "$tmp4"; sort -u "$tmp6" -o "$tmp6"
local tmp4v="$TMPDIR/wl4.valid"; local tmp6v="$TMPDIR/wl6.valid"
: > "$tmp4v"; : > "$tmp6v"
local v4=0 v6=0
while read -r x; do is_ip_or_cidr "$x" && { echo "$x"; ((v4++)); }; done < "$tmp4" > "$tmp4v"
while read -r x; do is_ip_or_cidr "$x" && { echo "$x"; ((v6++)); }; done < "$tmp6" > "$tmp6v"

log "聚合计数:ignore.ip → v4=$cnt_src_ignore_ip4 v6=$cnt_src_ignore_ip6;ignore.host 成功 → v4=$cnt_src_ignore_host_ok4 v6=$cnt_src_ignore_host_ok6;BTWAF → v4=$cnt_src_btwaf4 v6=$cnt_src_btwaf6;校验通过 → v4=$v4 v6=$v6"

# 6) 批量注入 ipset
{
    echo "flush $SET_WL_V4"
    while read -r net; do [ -n "$net" ] && echo "add $SET_WL_V4 $net"; done < "$tmp4v"
    echo "flush $SET_WL_V6"
    while read -r net; do [ -n "$net" ] && echo "add $SET_WL_V6 $net"; done < "$tmp6v"
} | $IPSET restore -exist >/dev/null 2>&1 || log "WARN: ipset restore 返回非零(已忽略)"

# 7) 保存快照
ipset_members "$SET_WL_V4" > "$WL_SNAPSHOT_V4" || true
ipset_members "$SET_WL_V6" > "$WL_SNAPSHOT_V6" || true
local final4=$(wc -l < "$WL_SNAPSHOT_V4" 2>/dev/null || echo 0)
local final6=$(wc -l < "$WL_SNAPSHOT_V6" 2>/dev/null || echo 0)

# 7.1 记录有效聚合哈希(仅用于自愈判定,不触发告警)
local h4="" h6=""
if [ -n "$SHA1" ]; then
    h4=$(cat "$tmp4v" 2>/dev/null | $SHA1 | awk '{print $1}')
    h6=$(cat "$tmp6v" 2>/dev/null | $SHA1 | awk '{print $1}')
else
    h4="$(wc -l < "$tmp4v" 2>/dev/null)-$(head -n1 "$tmp4v" 2>/dev/null)"
    h6="$(wc -l < "$tmp6v" 2>/dev/null)-$(head -n1 "$tmp6v" 2>/dev/null)"
fi
local p4="$(redis_get "$WL_REDIS_NS:hash:v4")"
local p6="$(redis_get "$WL_REDIS_NS:hash:v6")"
local src_stat="ignore.v4=$cnt_src_ignore_ip4 ignore.v6=$cnt_src_ignore_ip6 host.v4=$cnt_src_ignore_host_ok4 host.v6=$cnt_src_ignore_host_ok6 btwaf.v4=$cnt_src_btwaf4 btwaf.v6=$cnt_src_btwaf6 valid.v4=$v4 valid.v6=$v6"
redis_set "$WL_REDIS_NS:hash:v4" "$h4"
redis_set "$WL_REDIS_NS:hash:v6" "$h6"
redis_set "$WL_REDIS_NS:srcstat" "$src_stat"
echo "$src_stat" > "$RUNDIR/wl.srcstat" || true

# 7.2 变更 → 自愈(无推送)
if [ "$h4" != "$p4" ] || [ "$h6" != "$p6" ]; then
    reconcile_unban_whitelisted
fi

# 7.3 来源文件 mtime 变化才推送
local cur_msig; cur_msig="$(compute_wl_mtime_sig)"
local prev_msig=""
[ -f "$WL_MTIME_SIG_FILE" ] && prev_msig="$(cat "$WL_MTIME_SIG_FILE" 2>/dev/null || echo "")"
echo "$cur_msig" > "$WL_MTIME_SIG_FILE"

if [ "${WL_ALERT_BY_MTIME_ONLY}" = true ]; then
    if [ "$cur_msig" != "$prev_msig" ]; then
      local unb="$(cat "$RUNDIR/last_unban_wl.count" 2>/dev/null || echo 0)"
      local wl_text="### 🧾 白名单更新(来源文件变更)
- **时间**:$($DATE '+%F %T')
- **来源统计**:$src_stat
- **当前快照**:IPv4=${final4} 条,IPv6=${final6} 条
- **本轮自愈解封**:${unb} 个"
      alert_markdown "wl-change" "白名单更新" "$wl_text"
    fi
else
    # 不仅按 mtime,哈希变化也推送(可选)
    if [ "$h4" != "$p4" ] || [ "$h6" != "$p6" ]; then
      local unb="$(cat "$RUNDIR/last_unban_wl.count" 2>/dev/null || echo 0)"
      local wl_text="### 🧾 白名单更新(聚合内容变化)
- **时间**:$($DATE '+%F %T')
- **来源统计**:$src_stat
- **当前快照**:IPv4=${final4} 条,IPv6=${final6} 条
- **本轮自愈解封**:${unb} 个"
      alert_markdown "wl-change" "白名单更新" "$wl_text"
    fi
fi

log "白名单重载完成:IPv4 $final4 条,IPv6 $final6 条。"
set -e
}

show_whitelist(){
ensure_ipset_and_iptables
echo "=== IPv4 whitelist ($SET_WL_V4) ==="; ipset_members "$SET_WL_V4" | head -n 200
echo "=== IPv6 whitelist ($SET_WL_V6) ==="; ipset_members "$SET_WL_V6" | head -n 200
echo "(仅展示前 200 条;完整请使用:ipset list $SET_WL_V4 / $SET_WL_V6)"
}

is_in_whitelist_snapshot(){
local ip="$1"
if is_ipv6 "$ip"; then
    grep -Fxq -- "$ip" "$WL_SNAPSHOT_V6" 2>/dev/null && return 0
else
    $IPSET test "$SET_WL_V4" "$ip" >/dev/null 2>&1 && return 0
    grep -Fxq -- "$ip" "$WL_SNAPSHOT_V4" 2>/dev/null && return 0
fi
return 1
}

reconcile_unban_whitelisted(){
local tmp="$TMPDIR/reconcile.$$" cnt=0
ipset_members "$SET_BL_V4" > "$tmp" || true
while read -r ip; do
    [ -n "$ip" ] || continue
    if is_in_whitelist_snapshot "$ip"; then
      unban_ip "$ip" "auto-unban-wl" && ((cnt++))
    fi
done < "$tmp"
if have ip6tables; then
    ipset_members "$SET_BL_V6" > "$tmp" || true
    while read -r ip; do
      [ -n "$ip" ] || continue
      if is_in_whitelist_snapshot "$ip"; then
      unban_ip "$ip" "auto-unban-wl" && ((cnt++))
      fi
    done < "$tmp"
fi
echo "$cnt" > "$RUNDIR/last_unban_wl.count"
}

# ==============================================================================
# L4 实时排行(修复版)
# ==============================================================================
extract_peer_ip_awk='
function peerip(s,    x){
# :443 / 1.2.3.4:54321 / 2001:db8::1
if (s ~ /^\[/) { gsub(/^\[/,"",s); sub(/\].*$/,"",s); return s; }
sub(/:+$/,"",s);
return s;
}
{ ip=peerip($NF); if (ip!="" && ip!="-") print ip; }
'
top_talkers_l4(){
have ss || die "缺少 ss"
local est syn
est=$($SS -Hnta state established 2>/dev/null | awk "$extract_peer_ip_awk" | sort | uniq -c | sort -nr | head -n 20)
syn=$($SS -Hnta state syn-recv    2>/dev/null | awk "$extract_peer_ip_awk" | sort | uniq -c | sort -nr | head -n 20)
echo "---- Top EST ----"; [ -n "$est" ] && echo "$est" || echo "(无活动连接)"
echo "---- Top SYN_RECV ----"; [ -n "$syn" ] && echo "$syn" || echo "(无活动连接)"
}

should_attack_mode(){
local ct_count=0; have conntrack && ct_count=$(conntrack -C 2>/dev/null || echo 0)
local syn_total est_total time_wait_total
syn_total=$($SS -Hnta state syn-recv    | awk '{c++} END{print c+0}')
est_total=$($SS -Hnta state established | awk '{c++} END{print c+0}')
time_wait_total=$($SS -Hnta state time-wait | awk '{c++} END{print c+0}') # 新增

local result=0
# 新增 TIME_WAIT > 5000 (可调) 作为触发条件
if (( ct_count > NO_OF_CT || syn_total > NO_OF_SYN || est_total > NO_OF_EST || time_wait_total > 5000 )); then
    result=1
fi

# 第一行输出结果 1 或 0
echo "$result"
# 后续行输出详细的指标,用于告警
echo "Conntrack: ${ct_count} / ${NO_OF_CT}"
echo "SYN Total: ${syn_total} / ${NO_OF_SYN}"
echo "EST Total: ${est_total} / ${NO_OF_EST}"
echo "TIME_WAIT: ${time_wait_total} / 5000" # 新增
}

# ==============================================================================
# Host/Path 阈值(外部 JSON 优先;否则内置 Discuz PATH_TIERS)
# ==============================================================================
DEFAULT_THRESHOLDS_JSON='{
"GLOBAL": { "SYN_GLOBAL_THRESHOLD": 180, "EST_GLOBAL_THRESHOLD": 260, "CONNTRACK_GLOBAL_THRESHOLD": 15000 },
"*": {
    "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600,
    "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120,
    "PATH_TIERS": [
      { "kind": "prefix", "pattern": "/forum.php?mod=image",            "L1": 10, "L2": 20,"L3": 40},
      { "kind": "regex","pattern": "^/plugin\\.php\\?id=aljol(&|$)","L1":6, "L2": 12,"L3": 24},
      { "kind": "regex","pattern": "^/ajax\\.php(\\?|$)",             "L1":8, "L2": 16,"L3": 32},
      { "kind": "regex","pattern": "^/avatar\\.php(\\?|$)",             "L1":8, "L2": 16,"L3": 32},
      { "kind": "regex","pattern": "^/misc\\.php\\?mod=seccode(&|$)","L1":4, "L2":8,"L3": 16},
      { "kind": "regex","pattern": "^/search\\.php(\\?|$)",         "L1":8, "L2": 16,"L3": 32},
      { "kind": "prefix", "pattern": "/data/attachment/",                "L1": 30, "L2": 60,"L3": 120 },
      { "kind": "prefix", "pattern": "/static/",                         "L1": 40, "L2": 80,"L3": 160 }
    ]
},
"你的域名1": { "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600, "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120 },
"你的域名2": { "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600, "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120 },
"你的域名3": { "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600, "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120 },
"你的域名4": { "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600, "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120 },
"你的域名5": { "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600, "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120 }
}'

load_host_thresholds(){
local src="$THRESHOLDS_JSON" json
if [ -s "$src" ]; then json="$(cat "$src")"; log "已加载外部 host-thresholds.json:$src"
else json="$DEFAULT_THRESHOLDS_JSON"; log "使用脚本内置默认阈值(固化配置 + Discuz PATH_TIERS)"; fi
[ -n "$PY3" ] || { log "未检测到 python3,无法解析 host/path 阈值 JSON"; return 0; }

local out
out="$(python3 - <<'PY' "$json" || exit 1
import sys,json
cfg=json.loads(sys.argv)

# 增强的 g() 函数,可以检查备用键名
def g(k, d=0, alt_k=None):
    glob = cfg.get("GLOBAL", {})
    v = glob.get(k)
    if v is None and alt_k:
      v = glob.get(alt_k)
    return v if isinstance(v, (int, float)) else d

# 兼容 GLOBAL 区域的新旧两种键名
print("GLOBAL_SYN=%s" % g("SYN_GLOBAL_THRESHOLD", 0, alt_k="NO_OF_SYN"))
print("GLOBAL_EST=%s" % g("EST_GLOBAL_THRESHOLD", 0, alt_k="NO_OF_EST"))
print("GLOBAL_CT=%s" % g("CONNTRACK_GLOBAL_THRESHOLD", 0, alt_k="NO_OF_CONNTRACK"))

for host,val in cfg.items():
if host=="GLOBAL" or not isinstance(val,dict): continue

# 兼容嵌套的 REQ_PER_MIN 对象和独立的 REQ_PER_MIN_L1/L2/L3 键
req_min_obj = val.get("REQ_PER_MIN", {})
hw = val.get("HOST_WEIGHT",1.0)
l1 = val.get("REQ_PER_MIN_L1") or req_min_obj.get("L1") or 0
l2 = val.get("REQ_PER_MIN_L2") or req_min_obj.get("L2") or 0
l3 = val.get("REQ_PER_MIN_L3") or req_min_obj.get("L3") or 0

# 兼容 SYN_PER_IP 和 EST_PER_IP 键名
syn = val.get("CONN_PER_IP_SYN_THRESHOLD") or val.get("SYN_PER_IP") or 0
est = val.get("CONN_PER_IP_EST_THRESHOLD") or val.get("EST_PER_IP") or 0

print("H|%s|%s|%s|%s|%s|%s"%(host,hw,l1,l2,l3,syn or 0)); print("E|%s|%s"%(host,est or 0))

pts=val.get("PATH_TIERS",[])
if isinstance(pts,list):
    for i,r in enumerate(pts):
      # 兼容 KIND/PAT (大写) 和 kind/pattern (小写)
      kind_val = r.get("kind") or r.get("KIND") or "regex"
      kind = str(kind_val).lower()
      pat = str(r.get("pattern") or r.get("PAT") or "").strip()
      
      L1=int(r.get("L1",0) or 0); L2=int(r.get("L2",0) or 0); L3=int(r.get("L3",0) or 0)
      if not pat: continue
      if kind not in ("regex","prefix"): kind="regex"
      print("P|%s|%d|%s|%s|%d|%d|%d"%(host,i,kind,pat.replace("|","\\|"),L1,L2,L3))
print("PCNT|%s|%d"%(host,len(pts)))
PY
)"
HT_HOST_WEIGHT=(); HT_REQ_L1=(); HT_REQ_L2=(); HT_REQ_L3=(); HT_SYN_PERIP=(); HT_EST_PERIP=()
PT_KIND=(); PT_PAT=(); PT_L1=(); PT_L2=(); PT_L3=(); PT_COUNT=()

while IFS= read -r line; do
    case "$line" in
      GLOBAL_SYN=*) GLOBAL_SYN="${line#GLOBAL_SYN=}" ;;
      GLOBAL_EST=*) GLOBAL_EST="${line#GLOBAL_EST=}" ;;
      GLOBAL_CT=*)GLOBAL_CT="${line#GLOBAL_CT=}" ;;
      H|* ) IFS='|' read -r _ h w l1 l2 l3 syn <<<"$line"; HT_HOST_WEIGHT["$h"]="$w"; HT_REQ_L1["$h"]="$l1"; HT_REQ_L2["$h"]="$l2"; HT_REQ_L3["$h"]="$l3"; HT_SYN_PERIP["$h"]="$syn" ;;
      E|* ) IFS='|' read -r _ h est <<<"$line"; HT_EST_PERIP["$h"]="$est" ;;
      P|* ) IFS='|' read -r _ h idx kind pat L1 L2 L3 <<<"$line"; key="${h}|${idx}"; PT_KIND["$key"]="$kind"; PT_PAT["$key"]="$pat"; PT_L1["$key"]="$L1"; PT_L2["$key"]="$L2"; PT_L3["$key"]="$L3" ;;
      PCNT|* ) IFS='|' read -r _ h pc <<<"$line"; PT_COUNT["$h"]="$pc" ;;
    esac
done <<< "$out"

# GLOBAL 覆盖兜底
[ "$GLOBAL_SYN" -gt 0 ] && NO_OF_SYN="$GLOBAL_SYN"
[ "$GLOBAL_EST" -gt 0 ] && NO_OF_EST="$GLOBAL_EST"
[ "$GLOBAL_CT"-gt 0 ] && NO_OF_CT="$GLOBAL_CT"
log "阈值装载:GLOBAL SYN=$NO_OF_SYN EST=$NO_OF_EST CT=$NO_OF_CT;站点数=$(printf %s "${!HT_HOST_WEIGHT[@]}" | wc -w)"
}

# ==============================================================================
# Redis 日志游标缓存(降 I/O)
# ==============================================================================
stream_new_lines(){
local f="$1"; [ -f "$f" ] || return 0
if [ "$LOG_CACHE_ENABLE" = true ] && [ -n "$REDIS_CLI" ] && [ -n "$STAT" ]; then
    local key="ddos:logpos:$(echo -n "$f" | ${MD5:-md5sum} | awk '{print $1}')"
    local size; size=$($STAT -c %s "$f" 2>/dev/null || echo 0)
    local pos; pos=$($REDIS_CLI GET "$key" 2>/dev/null || echo "")
    if [[ -z "$pos" || "$pos" -gt "$size" ]]; then
      $TAIL -n "$LOG_CACHE_MAX_FALLBACK_LINES" "$f"
      $REDIS_CLI SETEX "$key" 86400 "$size" >/dev/null 2>&1 || true
    else
      local start=$(( pos + 1 ))
      tail -c +$start "$f" 2>/dev/null || $TAIL -n "$LOG_CACHE_MAX_FALLBACK_LINES" "$f"
      $REDIS_CLI SETEX "$key" 86400 "$size" >/dev/null 2>&1 || true
    fi
else
    $TAIL -n "$LOG_CACHE_MAX_FALLBACK_LINES" "$f"
fi
}

# ==============================================================================
# L7 统计/处置 + Redis 限速决策
# ==============================================================================
count_hits_in_logs(){
local tmp="$TMPDIR/l7_hits.$$"; : > "$tmp"
for f in "${CC_LOG_PATHS[@]}"; do
    [ -f "$f" ] || continue
    local fb; fb="$(guess_host_from_file "$f")"
    stream_new_lines "$f" | $AWK -v fb="$fb" '
      function g(line,   h) {
      if (match(line, /"https?:\/\/([^\/"]+)/, a)) return a;
      if (match(line, /"[^"]+ (https?:\/\/([^\/ ]+))?\/[^"]*"/, b)) { if (b!="") return b; }
      return fb;
      }
      {
      ip=$1; line=$0;
      path="/"; req=""; status_code=0; threat_weight=1; ua="-"; ref="-";

      if (match(line, /"([^"]+)"/, a)) {
            req=a
            if (match(line, /" "({3})/, s)) status_code=s

            # 1. 威胁评分逻辑
            if (status_code == 404 || status_code == 403) threat_weight=3
            if (status_code == 499) threat_weight=5 # 客户端主动断开
            if (status_code >= 500) threat_weight=4 # 服务端错误

            if (match(req, /[^ ]+ ([^ ?"]+)/, p)) path=p

            # 2. UA/Referer 提取
            if (match(line, /"([^"]*)" "([^"]*)"$/, fields)) {
                ref=fields; ua=fields;
            }
            
            # 3. 管理员后台页面操作豁免
            if (path ~ /^/(dismall|admin|plugin)\.php$/ && status_code == 200) {
                # 管理员权重为0,字段需要对齐
                print "ADMIN_LOGIN", ip, g(line), path, 0, ua, ref
            } else {
                print "NORMAL_HIT", ip, g(line), path, threat_weight, ua, ref
            }
      }
      }' >> "$tmp"
done

# 4. 升级聚合逻辑以处理新字段
$AWK '{
    tag=$1; ip=$2; host=$3; path=$4; weight=$5; ua=$6; ref=$7;
    # 对于管理员登录,直接输出
    if (tag == "ADMIN_LOGIN") {
      print 0, tag, ip, host, path, ua, ref;
      next;
    }
    # 对于普通访问,按 key 累加权重
    key=ip"|"host"|"path;
    weights += weight;
    # 只需记录第一次出现的 ua 和 ref
    if (!(key in uas)) uas=ua;
    if (!(key in refs)) refs=ref;
} END {
    for (k in weights) {
      split(k,a,"|");
      print weights, "NORMAL_HIT", a, a, a, uas, refs
    }
}' "$tmp" | sort -nr
}

perip_escalation_from_errorlog(){
local tmp="$TMPDIR/perip.$$"; : > "$tmp"
for f in "${ERROR_LOG_PATHS[@]}"; do
    [ -f "$f" ] || continue
    local fb; fb="$(guess_host_from_file "$f")"
    stream_new_lines "$f" | $AWK -v fb="$fb" '
      # --- 新增规则:匹配 PHP 致命错误 ---
      /PHP Fatal error:.*(Maximum execution time|Too many connections|Allowed memory size)/ {
      ip=""; if (match($0, /client: ([^, ]+)/, a)) ip=a;
      if (ip!="") {print ip, fb, "php-fatal"}
      }
      # --- 原有规则 ---
      /limiting connections by zone "perip"/ {
      ip="";host="";
      if (match($0, /client: (+)/, a)) ip=a;
      if (match($0, /server: ([^, ]+)/, b)) host=b;
      if (ip!="") {print ip, (host==""?fb:host), "nginx-limit"}
      }
    ' >> "$tmp"
done

if [ -s "$tmp" ]; then
    # --- 升级聚合逻辑 ---
    $AWK '{k=$1"|" $2"|" $3; c++} END{for (k in c){split(k,a,"|"); print c, a, a, a}}' "$tmp" \
    | sort -nr \
    | while read -r cnt ip host reason; do
      # 只要出现 2 次以上 PHP 致命错误,就提高封禁优先级
      local esc_thr=2
      if [ "$reason" == "nginx-limit" ]; then
            local base="${HT_REQ_L3[$host]:-${HT_REQ_L3["*"]:-$((CC_THRESHOLD*4))}}"
            esc_thr=$(( base/2 )); [ "$esc_thr" -lt 5 ] && esc_thr=5
      fi

      if (( cnt >= esc_thr )); then
          if $IPSET test "$SET_RL_V4" "$ip" >/dev/null 2>&1; then
            ban_ip "$ip" "L7-perip-escalation(cnt=$cnt,thr=$esc_thr)"
          else
            $IPSET add "$SET_RL_V4" "$ip" -exist
            LIMITED_THIS_RUN+=("$ip ($host perip-escalation cnt=$cnt thr=$esc_thr)")
          fi
      fi
      done
fi
}

redis_should_limit(){
[ -n "$REDIS_CLI" ] || { echo 0; return; }
local ip="$1" k="ddos:l7:$ip" r
r=$($REDIS_CLI -x <<EOF
MULTI
INCR $k
EXPIRE $k 10
EXEC
EOF
)
local cnt; cnt=$(echo "$r" | tail -n1 | tr -dc 0-9)
if [ -z "$cnt" ]; then echo 0; else [ "$cnt" -ge "$CC_RATELIMIT_THRESHOLD" ] && echo 1 || echo 0; fi
}

# ==============================================================================
# Ban/Unban/Flush
# ==============================================================================
ban_ip(){
local ip="$1" reason="${2:-unknown}"
if is_in_whitelist_snapshot "$ip"; then log "跳过封禁(白名单):$ip"; return 0; fi
if is_ipv6 "$ip"; then have ip6tables || { log "IPv6 不可用,跳过 $ip"; return 0; }; $IPSET add "$SET_BL_V6" "$ip" -exist
else $IPSET add "$SET_BL_V4" "$ip" -exist; fi
BANNED_THIS_RUN+=("$ip ($reason)") # 将原因加入批次战报
echo "$($DATE '+%F %T'),BAN,$ip,$reason" >> "$BAN_HISTORY"
}
unban_ip(){
local ip="$1" reason="${2:-unknown}"
is_ipv6 "$ip" && $IPSET del "$SET_BL_V6" "$ip" 2>/dev/null || $IPSET del "$SET_BL_V4" "$ip" 2>/dev/null || true
echo "$($DATE '+%F %T'),UNBAN,$ip,$reason" >> "$BAN_HISTORY"
}
flush_bans(){ $IPSET flush "$SET_BL_V4" 2>/dev/null || true; $IPSET flush "$SET_BL_V6" 2>/dev/null || true; log "已清空黑名单"; have fail2ban-client && fail2ban-client unban --all 2>/dev/null || true; }

# ==============================================================================
# 主巡检:动态调速 + L4/L7 判决 + 告警
# ==============================================================================
# 全新的“封禁摘要”函数,替代旧的 alert_markdown_batch
alert_ban_summary(){
[ -n "$DINGTALK_WEBHOOK" ] || return 0
[ "${#BANNED_THIS_RUN[@]}" -eq 0 ] && [ "${#LIMITED_THIS_RUN[@]}" -eq 0 ] && return 0

local pub; pub="$(get_public_ip)"
local ban_count="${#BANNED_THIS_RUN[@]}"
local limit_count="${#LIMITED_THIS_RUN[@]}"

local top_ips="" reason_dist="" limited_list=""

# 统计封禁的 Top IP
if [ "$ban_count" -gt 0 ]; then
    top_ips=$(printf "%s\n" "${BANNED_THIS_RUN[@]}" | awk '{print $1}' | sort | uniq -c | sort -nr | head -n 5 | awk '{printf "| %s | %s |\\n", $2, $1}')
fi

# 统计封禁的原因分布
if [ "$ban_count" -gt 0 ]; then
    reason_dist=$(printf "%s\n" "${BANNED_THIS_RUN[@]}" | sed -E 's/^+ \(([^ (]+).*/\1/' | sort | uniq -c | sort -nr | awk '{printf "| %s | %s |\\n", $2, $1}')
fi

# 格式化限速列表
if [ "$limit_count" -gt 0 ]; then
    limited_list=$(printf -- "- %s\n" "${LIMITED_THIS_RUN[@]}" | head -n 10)
    [ "$limit_count" -gt 10 ] && limited_list+=$'\n- ...等'"$limit_count"'个条目'
fi

local text="### 🚧 DDoS-Guard 封禁摘要
- **时间**: $($DATE '+%F %T')
- **主机**: $pub
- **封禁IP数**: ${ban_count}
- **限速IP数**: ${limit_count}
"
if [ -n "$top_ips" ]; then
    text+=$'\n#### Top 5 被封禁 IP\n| IP地址 | 次数 |\n|:---|:---|\n'"$top_ips"
fi
if [ -n "$reason_dist" ]; then
    text+=$'\n#### 封禁原因分布\n| 原因 | 次数 |\n|:---|:---|\n'"$reason_dist"
fi
if [ -n "$limited_list" ]; then
    text+=$'\n#### 限速IP列表 (最多10条)\n'"$limited_list"
fi

alert_markdown "ban-summary" "DDoS-Guard 封禁摘要" "$text"
}

# --- alert_attack_state 函数开始 ---
alert_attack_state(){
local state="$1"      # 参数1: enter / exit
local metrics="$2"    # 参数2: 触发指标
local banned_ips="$3" # 参数3: 当轮封禁的IP列表
local pub="N/A" lan="N/A"
pub="$(get_public_ip)"
lan="$(hostname -I 2>/dev/null | awk '{print $1}')"

# 获取当前的扫描间隔
local current_interval
current_interval=$(get_current_interval || echo '?')

# 使用 $'' 语法来确保 \n 被正确解析为换行符
local text="### ⚠️ 攻击模式$( [ "$state" = "enter" ] && echo 进入 || echo 退出 )"
text+=$'\n- **时间**:'"$($DATE '+%F %T')"
text+=$'\n- **主机**:'"$pub / $lan"
text+=$'\n- **扫描间隔**:'"${current_interval%s}s"

if [ "$state" = "enter" ]; then
    [ -n "$metrics" ] && text+=$'\n- **触发原因**:\n'"${metrics}"
    [ -n "$banned_ips" ] && text+=$'\n- **当场封禁 (部分)**:\n'"${banned_ips}"
fi

# 调用通用的告警函数,将上面拼接好的完整消息 ($text) 发送出去
alert_markdown "state-$state" "DDoS-Guard 攻击模式:${state}" "$text"
}
# --- alert_attack_state 函数结束 ---

run_core(){
ensure_ipset_and_iptables
load_host_thresholds
load_whitelist
reconcile_unban_whitelisted   # 双保险自愈

BANNED_THIS_RUN=(); LIMITED_THIS_RUN=()

# 升级后的代码
local attack_mode_details; attack_mode_details=$(should_attack_mode)
local attack_mode; attack_mode=$(echo "$attack_mode_details" | head -n 1)
local metrics_summary; metrics_summary=$(echo "$attack_mode_details" | tail -n +2 | sed 's/^/- /')

local est_thr=$NO_OF_EST syn_thr=$NO_OF_SYN cc_thr=$CC_THRESHOLD
local prev_state=0; [ -f "$attack_state_file" ] && prev_state=$(cat "$attack_state_file" 2>/dev/null || echo 0)

local is_entering_attack_mode=0 # 新增一个标志位

if [ "$ENABLE_DYNAMIC_THRESHOLDS" = true ] && [ "$attack_mode" -eq 1 ]; then
    est_thr=$NO_OF_EST_ATTACK_MODE; syn_thr=$NO_OF_SYN_ATTACK_MODE; cc_thr=$CC_THRESHOLD_ATTACK_MODE
    if [ "$prev_state" -ne 1 ]; then
      set_timer_interval "$ATTACK_SCAN_INTERVAL"
      echo 1 > "$attack_state_file"
      is_entering_attack_mode=1 # 设置标志位,但不立即告警
      log "进入攻击模式:EST<$est_thr SYN<$syn_thr L7<$cc_thr;扫描间隔=${ATTACK_SCAN_INTERVAL}s"
    fi
else
    if [ "$prev_state" -ne 0 ]; then
      set_timer_interval "$SCAN_INTERVAL"
      echo 0 > "$attack_state_file"
      alert_attack_state "exit" "" "" # 退出告警不变
      log "退出攻击模式:扫描间隔恢复=${SCAN_INTERVAL}s"
    fi
fi

# --- ▽▽▽▽▽ 从这里开始,是新的完整代码 ▽▽▽▽▽ ---
# L4: EST per-IP (增强版:带紧急刹车)
$SS -Hnta state established \
| awk "$extract_peer_ip_awk" \
| sort | uniq -c | sort -nr \
| while read -r cnt ip; do
      [ -z "$ip" ] && continue

      # 1. 紧急刹车:无论IP是否在白名单,连接数过高则立即封禁
      if (( cnt >= EMERGENCY_CONN_THRESHOLD )); then
      ban_ip "$ip" "L4-EMERGENCY(cnt=$cnt,thr=$EMERGENCY_CONN_THRESHOLD)"
      continue # 封禁后继续处理下一个IP
      fi

      # 2. 常规检查:白名单、管理员、蜘蛛等
      is_in_whitelist_snapshot "$ip" && continue
      # 增加动态管理员豁免检查
      $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2>&1 && continue
      [ "$(is_private_ip "$ip")" = "1" ] && continue
      $IPSET test "$SET_GB_V4" "$ip" >/dev/null 2>&1 && continue
      
      # 3. 常规阈值判断
      (( cnt >= est_thr )) && ban_ip "$ip" "L4-EST(cnt=$cnt,thr=$est_thr)"
    done

# L4: SYN_RECV per-IP (保持不变)
$SS -Hnta state syn-recv \
| awk "$extract_peer_ip_awk" \
| sort | uniq -c | sort -nr \
| while read -r cnt ip; do
      [ -z "$ip" ] && continue
      is_in_whitelist_snapshot "$ip" && continue
      # 增加动态管理员豁免检查
      $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2>&1 && continue
      [ "$(is_private_ip "$ip")" = "1" ] && continue
      $IPSET test "$SET_GB_V4" "$ip" >/dev/null 2>&1 && continue
      (( cnt >= syn_thr )) && ban_ip "$ip" "L4-SYN(cnt=$cnt,thr=$syn_thr)"
    done

# 新增:L4 CLOSE_WAIT per-IP 监控
# 监控 CLOSE_WAIT 状态,高 CLOSE_WAIT 数通常是应用层处理不过来的症状
local close_wait_thr=$(( est_thr / 2 )); [ "$close_wait_thr" -lt 20 ] && close_wait_thr=20
$SS -Hnta state close-wait \
| awk "$extract_peer_ip_awk" \
| sort | uniq -c | sort -nr \
| while read -r cnt ip; do
      [ -z "$ip" ] && continue

      # 紧急刹车同样适用于 CLOSE_WAIT
      if (( cnt >= EMERGENCY_CONN_THRESHOLD )); then
      ban_ip "$ip" "L4-EMERGENCY-CW(cnt=$cnt,thr=$EMERGENCY_CONN_THRESHOLD)"
      continue
      fi
      
      is_in_whitelist_snapshot "$ip" && continue
      $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2>&1 && continue
      $IPSET test "$SET_GB_V4" "$ip" >/dev/null 2>&1 && continue

      (( cnt >= close_wait_thr )) && ban_ip "$ip" "L4-CLOSE-WAIT(cnt=$cnt,thr=$close_wait_thr)"
    done
# --- △△△△△ 到这里结束,是新的完整代码 △△△△△ ---

# L7:增量日志计数 → Host/Path Tiers → UA 分类 → Redis 限速/封禁
local hits; hits=$(count_hits_in_logs || true)
if [ -n "$hits" ]; then
# --- 这是修正后的 while read 循环 ---
# --- ▽▽▽▽▽ 从这里开始,是新的完整 L7 循环代码 ▽▽▽▽▽ ---
while read -r cnt tag ip host path ua ref; do
# 如果标签是 ADMIN_LOGIN, 说明是管理员IP
if [ "$tag" = "ADMIN_LOGIN" ]; then
      $IPSET add "$SET_ADMIN_V4" "$ip" -exist
      continue
fi

[ -z "$ip" ] && continue
    is_in_whitelist_snapshot "$ip" && continue
    $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2>&1 && continue
    local uac; uac="$(classify_ua_for_ip "$ip")"
    # --- ↓↓↓↓ 新增:对高风险信誉IP进行降权或直接封禁 ↓↓↓↓ ---
    if $IPSET test "$SET_BADREP_V4" "$ip" >/dev/null 2>&1; then
      # 如果IP在高风险名单中,信誉分惩罚加倍,且无视其UA
      redis_hincrby "ddos:reputation" "$ip" "$(($REPUTATION_SCORE_INCREMENT * 2))"
      uac="BAD" # 直接将其标记为恶意UA
    fi
    # --- ↑↑↑↑ 新增结束 ↑↑↑↑ ---
    if [ "$uac" = "GOOD" ]; then $IPSET add "$SET_GB_V4" "$ip" -exist; continue; fi

    # --- 信誉系统逻辑 Part 1: 检查IP是否已在观察名单中 ---
    local on_watchlist=0
    if $IPSET test "$SET_WATCHLIST_V4" "$ip" >/dev/null 2>&1; then
      on_watchlist=1
    fi

    local base_host="$host"; [ -n "${HT_REQ_L1[$base_host]:-}" ] || base_host="*"
    local l1="${HT_REQ_L1[$base_host]:-$cc_thr}"
    local l2="${HT_REQ_L2[$base_host]:-$((cc_thr*2))}"
    local l3="${HT_REQ_L3[$base_host]:-$((cc_thr*4))}"
    local w="${HT_HOST_WEIGHT[$base_host]:-1.0}"

    local pc="${PT_COUNT[$base_host]:-0}"
    if [ "$pc" -gt 0 ]; then
      local idx=0
      while [ "$idx" -lt "$pc" ]; do
      local key="${base_host}|${idx}"
      local kind="${PT_KIND[$key]:-regex}"
      local pat="${PT_PAT[$key]:-}"
      local L1="${PT_L1[$key]:-0}"; local L2="${PT_L2[$key]:-0}"; local L3="${PT_L3[$key]:-0}"
      if [ -n "$pat" ]; then
          if [ "$kind" = "prefix" ]; then
            case "$path" in
            "$pat"*) [ "$L1" -gt 0 ] && l1="$L1"; [ "$L2" -gt 0 ] && l2="$L2"; [ "$L3" -gt 0 ] && l3="$L3"; idx=$pc; continue
            esac
          else
            echo "$path" | $GREP -Eiq -- "$pat" && { [ "$L1" -gt 0 ] && l1="$L1"; [ "$L2" -gt 0 ] && l2="$L2"; [ "$L3" -gt 0 ] && l3="$L3"; idx=$pc; continue; }
          fi
      fi
      idx=$((idx+1))
      done
    fi

    local thr=$l1
    if [ "$ENABLE_DYNAMIC_THRESHOLDS" = true ] && [ "$attack_mode" -eq 1 ]; then
      thr=$(python3 - <<PY "$l1" "$w"
import sys
l1=float(sys.argv); w=float(sys.argv)
print(int(max(3, l1*w*0.4)))
PY
)
    fi

    local current_interval; current_interval=$(cat "$RUNDIR/scan-interval.current" 2>/dev/null || echo "$SCAN_INTERVAL")
    thr=$(python3 -c "print(int(max(2, ($thr / 60.0) * $current_interval)))")

    # --- 信誉系统逻辑 Part 2: 如果在观察名单中,使用更严厉的阈值 ---
    if [ "$on_watchlist" -eq 1 ]; then
      thr=$(( thr / 4 )); # 对观察名单中的IP,阈值严厉4倍
      [ "$thr" -lt 2 ] && thr=2
    fi

    if [[ "$ua" == "-" || "$ua" == "" ]] && [[ "$ref" == "-" || "$ref" == "" ]]; then
       thr=$(( thr / 2 ));
       [ "$thr" -lt 2 ] && thr=2
    fi

    case "$uac" in
      SEMI) $IPSET add "$SET_RL_V4" "$ip" -exist; LIMITED_THIS_RUN+=("$ip ($host $path semi-trust UA)"); continue;;
      BAD|FAKEGOOD) thr=$(( thr/3 )); [ "$thr" -lt 3 ] && thr=3 ;;
    esac

    if (( cnt >= thr )); then
      local decide_limit=0
      if [ "$ENABLE_RATE_LIMITING" = true ]; then decide_limit=$(redis_should_limit "$ip"); fi
      if [ "$decide_limit" -eq 1 ] && [ "$uac" != "BAD" ] && [ "$uac" != "FAKEGOOD" ]; then
      # --- 信誉系统逻辑 Part 3: 将被限速的IP加入观察名单并增加信誉分 ---
      $IPSET add "$SET_RL_V4" "$ip" -exist
      $IPSET add "$SET_WATCHLIST_V4" "$ip" -exist # 加入观察名单
      redis_hincrby "ddos:reputation" "$ip" "$REPUTATION_SCORE_INCREMENT" # 增加信誉分
      LIMITED_THIS_RUN+=("$ip ($host $path L7-limit/watchlist cnt=$cnt thr=$thr)")
      else
      ban_ip "$ip" "L7-CC($host $path,cnt=$cnt,thr=$thr,ua=$uac)"
      # 被封禁后,清空其不良信誉记录
      $REDIS_CLI HDEL "ddos:reputation" "$ip" >/dev/null 2>&1 || true
      fi
    fi

    # --- 信誉系统逻辑 Part 4: 信誉分升级封禁:检查总分是否超限 ---
    # 仅当IP在观察名单中,且本次没被封禁时,才检查总分
    if [ "$on_watchlist" -eq 1 ] && ! [[ " ${BANNED_THIS_RUN[*]} " =~ " $ip " ]]; then
      local current_score
      current_score=$(redis_hget "ddos:reputation" "$ip")
      if [[ -n "$current_score" && "$current_score" -ge "$REPUTATION_SCORE_THRESHOLD" ]]; then
            ban_ip "$ip" "L7-Reputation(score=$current_score)"
            # 被封禁后,清空其不良信誉记录
            $REDIS_CLI HDEL "ddos:reputation" "$ip" >/dev/null 2>&1 || true
      fi
    fi
done <<< "$hits"
# --- △△△△△ 到这里结束,是新的完整 L7 循环代码 △△△△△ ---
fi

# 错误日志 perip 升级:多次限流 → 限速/封禁
perip_escalation_from_errorlog
# 如果是刚进入攻击模式,发送增强的“进入”告警
if [ "$is_entering_attack_mode" -eq 1 ]; then
    local banned_now_list=""
    if [ "${#BANNED_THIS_RUN[@]}" -gt 0 ]; then
      banned_now_list=$(printf -- '- %s\n' "${BANNED_THIS_RUN[@]}" | head -n 10)
      [ "${#BANNED_THIS_RUN[@]}" -gt 10 ] && banned_now_list+=$'\n- ...等'"${#BANNED_THIS_RUN[@]}"'个IP'
    fi
    alert_attack_state "enter" "$metrics_summary" "$banned_now_list"
fi

# “封禁摘要”新版告警
alert_ban_summary
}

# 运行一次(锁控制:--nowait 非阻塞;默认阻塞等待)
run_once(){
local mode="${1:-wait}"# wait / nowait
lock_open
if [ "$mode" = "nowait" ]; then
    if ! lock_try; then
      log "检测到已有实例在运行(nowait 模式,跳过本轮)。"
      return 0
    fi
else
    lock_wait   # CLI 调用:阻塞等待,不再报“已有实例在运行”
fi
run_core
}

# ==============================================================================
# 子命令
# ==============================================================================
cmd_install(){
need_root
# 1) systemd 单元
cat >/etc/systemd/system/ddos-guard.service <<SERVICE

Description=DDoS Guard one-shot scan
After=network-online.target
Wants=network-online.target


Type=oneshot
ExecStart=$SELF_PATH run --nowait
User=root
Group=root
Nice=-5
IOSchedulingClass=realtime


WantedBy=multi-user.target
SERVICE

cat >"$timer_unit" <<TIMER

Description=Run ddos-guard every ${SCAN_INTERVAL}s


OnBootSec=30s
OnUnitActiveSec=${SCAN_INTERVAL}s
AccuracySec=1s
Unit=ddos-guard.service
Persistent=true


WantedBy=timers.target
TIMER

systemctl daemon-reload
systemctl enable --now ddos-guard.timer

# 2) 目录/白名单文件
mkdir -p /etc/ddos
touch "$IGNORE_IP_FILE" "$IGNORE_HOST_FILE"

# 3) 首次白名单加载(失败不阻断)
load_whitelist || true
reconcile_unban_whitelisted || true

log "安装完成:systemd timer 每 ${SCAN_INTERVAL}s 巡检一次。"
alert_markdown "install" "DDoS-Guard 安装完成" "✅ 安装完成 @ $($DATE '+%F %T'),定时巡检 ${SCAN_INTERVAL}s;攻击模式自动提升至 ${ATTACK_SCAN_INTERVAL}s。"
echo "可用子命令:status/top/history/check/tune/tune-guide/whitelist-reload/whitelist-show/whitelist-debug/redis-status/ban/unban/flush-bans/blacklist/f2b-setup/btwaf-sync-whitelist"
}

cmd_uninstall(){ need_root; systemctl disable --now ddos-guard.timer 2>/dev/null || true; systemctl disable --now ddos-guard.service 2>/dev/null || true; rm -f /etc/systemd/system/ddos-guard.{service,timer}; systemctl daemon-reload; log "已卸载 systemd 配置。"; }
cmd_daemon(){ need_root; log "进入前台守护(${SCAN_INTERVAL}s)..."; while :; do run_once nowait || true; sleep "$SCAN_INTERVAL"; done; }

cmd_status(){
echo "== ipset 概览 =="
ipset_members "$SET_WL_V4" | awk 'END{print "WLv4:", NR+0}'
ipset_members "$SET_WL_V6" | awk 'END{print "WLv6:", NR+0}'
ipset_members "$SET_BL_V4" | awk 'END{print "BLv4:", NR+0}'
ipset_members "$SET_BL_V6" | awk 'END{print "BLv6:", NR+0}'
ipset_members "$SET_RL_V4" | awk 'END{print "RLv4:", NR+0}'
ipset_members "$SET_GB_V4" | awk 'END{print "GBv4:", NR+0}'
echo "== systemd =="; systemctl status --no-pager ddos-guard.timer 2>/dev/null | sed -n '1,12p'
echo "== whitelist srcstat =="; [ -f "$RUNDIR/wl.srcstat" ] && cat "$RUNDIR/wl.srcstat" || echo "(暂无)"
echo "== scan interval =="; [ -f "$RUNDIR/scan-interval.current" ] && cat "$RUNDIR/scan-interval.current" || echo "${SCAN_INTERVAL}"
echo "== attack state =="; [ -f "$attack_state_file" ] && cat "$attack_state_file" || echo 0
}

cmd_top(){ top_talkers_l4; }
cmd_history(){ [ -f "$BAN_HISTORY" ] && tail -n 50 "$BAN_HISTORY" || echo "暂无历史。"; }

cmd_check(){
echo "== 依赖检查 =="; for c in ss iptables ipset python3 host conntrack redis-cli; do printf "%-12s : " "$c"; have "$c" && echo OK || echo MISSING; done
autodetect_btwaf_paths
echo "== BTWAF 文件 =="; ls -l "$BTWAF_IP_WHITE" 2>/dev/null || echo "无 IPv4 整形白名单"; ls -l "$BTWAF_IP_WHITE_V6" 2>/dev/null || echo "无 IPv6 白名单"
echo "== 白名单源 =="; echo "$IGNORE_IP_FILE"; [ -s "$IGNORE_IP_FILE" ] && echo "(has content)"; echo "$IGNORE_HOST_FILE"; [ -s "$IGNORE_HOST_FILE" ] && echo "(has content)"
echo "== Host/Path JSON =="; [ -s "$THRESHOLDS_JSON" ] && echo "$THRESHOLDS_JSON (present)" || echo "使用脚本内置默认"
}

cmd_tune(){ echo "保底阈值:SYN=${NO_OF_SYN} EST=${NO_OF_EST} CT=${NO_OF_CT};攻击态 L7 起点=${CC_THRESHOLD_ATTACK_MODE}"; }
cmd_tune_guide(){
cat <<'EOF'
[调优指南]
- 多站点:CC_LOG_PATHS/ERROR_LOG_PATHS 一行一个;LOG_HOST_MAP 可指定“路径正则 → 域名”。
- 阈值 JSON:GLOBAL 覆盖兜底;每 host 配 HOST_WEIGHT 和 L1/L2/L3;PATH_TIERS 支持 regex/prefix。
- 蜘蛛:GOOD(UA+PTR+回证) → ddos_goodbots_v4 免扰;SEMI 先限速;BAD/FAKE 收紧阈值优先封禁。
- 白名单:聚合 BTWAF v4/v6 + ignore.ip/host;Redis 哈希用于静默自愈;**告警仅按 mtime**。
- Redis:用于 L7 原子计数(10s窗口) 与“日志游标缓存”;无 Redis 自动退化。
- 动态调速:攻击模式→timer 降至 ATTACK_SCAN_INTERVAL;退出恢复 SCAN_INTERVAL。
- 钉钉:状态告警(进/退)+ 批次战报;同类 10 分钟节流(DINGTALK_THROTTLE_SEC)。
EOF
}

cmd_whitelist_reload(){ load_whitelist; reconcile_unban_whitelisted; }
cmd_whitelist_show(){ show_whitelist; }

cmd_whitelist_debug(){
autodetect_btwaf_paths
echo "== 路径与环境 =="
echo "IGNORE_IP_FILE    : $IGNORE_IP_FILE"
echo "IGNORE_HOST_FILE: $IGNORE_HOST_FILE"
echo "BTWAF_IP_WHITE    : $BTWAF_IP_WHITE"
echo "BTWAF_IP_WHITE_V6 : $BTWAF_IP_WHITE_V6"
echo "python3         : $PY3"
echo "host            : $HOST"
echo "getent            : $GETENT"
echo
echo "== ignore.ip.list 预览 =="; [ -r "$IGNORE_IP_FILE" ] && nl -ba "$IGNORE_IP_FILE" | sed -n '1,30p' || echo "(不存在或不可读)"
echo
echo "== ignore.host.list 解析测试(前 10 个域名)=="
if [ -r "$IGNORE_HOST_FILE" ]; then
    head -n 50 "$IGNORE_HOST_FILE" | sed 's/#.*$//' | sed '/^[[:space:]]*$/d' | head -n 10 | while read -r h; do
      echo "-- $h"; resolve_host_to_ips "$h" | head -n 5 | sed 's/^/   /'
    done
else
    echo "(不存在或不可读)"
fi
echo
echo "== BTWAF IPv4 JSON 转换(前 20 条)=="
if [ -r "$BTWAF_IP_WHITE" ] && [ -n "$PY3" ]; then parse_btwaf_ipv4_json "$BTWAF_IP_WHITE" 2>/dev/null | head -n 20; else echo "(文件不存在/不可读或 python3 不可用)"; fi
echo
echo "== BTWAF IPv6 JSON 转换(前 20 条)=="
if [ -r "$BTWAF_IP_WHITE_V6" ] && [ -n "$PY3" ]; then parse_btwaf_ipv6_json "$BTWAF_IP_WHITE_V6" 2>/dev/null | head -n 20; else echo "(文件不存在/不可读或 python3 不可用)"; fi
}

cmd_redis_status(){
if [ -z "$REDIS_CLI" ]; then echo "redis-cli 未安装"; return 1; fi
echo "== Redis 连接测试 =="; $REDIS_CLI PING 2>/dev/null || { echo "PING 失败"; return 1; }; echo "PONG"
echo "== 原子计数 10s 窗口测试 =="
local key="ddos:test:$$"; local r
r=$($REDIS_CLI -x <<EOF
MULTI
DEL $key
INCR $key
EXPIRE $key 10
EXEC
EOF
)
echo "$r" | sed 's/^//'
local cnt; cnt=$(echo "$r" | tail -n1 | tr -dc 0-9)
[ -n "$cnt" ] && echo "计数=$cnt(>=1 正常)" || echo "未拿到计数值"
}

cmd_ban(){   [ $# -ge 2 ] || die "用法:$0 ban <ip>";   ban_ip "$2" "manual"; }
cmd_unban(){ [ $# -ge 2 ] || die "用法:$0 unban <ip>"; unban_ip "$2" "manual"; }
cmd_flush_bans(){ flush_bans; }
cmd_blacklist(){
[ $# -ge 2 ] || die "用法:$0 blacklist <ip|file>"
local arg="$2"
if [ -f "$arg" ]; then
    while read -r x; do
      x="${x%%#*}"; x="${x//$'\r'/}"; x="$(echo "$x" | xargs || true)"; [ -n "$x" ] || continue
      is_ipv6 "$x" && $IPSET add "$SET_BL_V6" "$x" -exist || $IPSET add "$SET_BL_V4" "$x" -exist
    done < "$arg"
else
    is_ipv6 "$arg" && $IPSET add "$SET_BL_V6" "$arg" -exist || $IPSET add "$SET_BL_V4" "$arg" -exist
fi
}

cmd_f2b_setup(){
need_root
local jail="${F2B_JAIL_LOCAL:-/etc/fail2ban/jail.local}"
touch "$jail"
local tmp="$TMPDIR/f2b.ignore.v4"; ipset_members "$SET_WL_V4" > "$tmp" || true
if ! grep -q '^\' "$jail"; then echo -e "\nignoreip = 127.0.0.1/8" >> "$jail"; fi
local old; old=$(awk -F'= *' '/^\/{f=1} f&&/^ignoreip *=/{print $2; exit}' "$jail" | tr -d ' \t')
local addrs; addrs=$(cat "$tmp" | paste -sd, -)
[ -n "$addrs" ] || { log "Fail2Ban:无可合并白名单"; return 0; }
local merged
merged=$(python3 - <<PY "$old" "$addrs"
import sys
o=(sys.argv or "").split(",")
a=(sys.argv or "").split(",")
s=set()|set()
print(",".join(sorted(s)))
PY
)
python3 - <<'PY' "$jail" "$merged"
import sys,re
p,merged=sys.argv,sys.argv
s=open(p,'r',encoding='utf-8').read()
def repl(m): return m.group(1)+'= '+merged
s=re.sub(r'(^\[\s\S]*?^ignoreip\s*)=.*$', lambda m: repl(m), s, flags=re.M)
open(p,'w',encoding='utf-8').write(s)
PY
systemctl restart fail2ban 2>/dev/null || true
log "Fail2Ban ignoreip 已增量合并白名单,并尝试重启服务。"
}
cmd_daily_report(){
log "开始生成每日战报..."
local start_ts; start_ts=$(date -d "yesterday 19:30:00" +%s)
local end_ts; end_ts=$(date -d "today 19:30:00" +%s)

local report_data; report_data=$(
    awk -F, -v start="$start_ts" -v end="$end_ts" '
      BEGIN { OFS="," }
      {
      cmd="date -d \""$1"\" +%s"
      cmd | getline ts
      close(cmd)
      if (ts >= start && ts < end) {
          print $0
      }
      }
    ' "$BAN_HISTORY" 2>/dev/null || true
)

local ban_count=0 unban_count=0
local ban_details=""
ban_count=$(echo "$report_data" | grep -c ',BAN,' || true)
unban_count=$(echo "$report_data" | grep -c ',UNBAN,' || true)

if [ "$ban_count" -gt 0 ]; then
    ban_details=$(echo "$report_data" | grep ',BAN,' | head -n 20 | awk -F, '{
      gsub(/"/, "\\\"", $4); # Escape quotes in reason
      print "- **" $3 "** (" $4 ")"
    }' || true)
fi

local pub; pub="$(get_public_ip)"
local text="### 📈 DDoS-Guard 每日战报
- **报告时间**: $($DATE '+%F %T')
- **统计周期**: 过去 24 小时
- **主机公网 IP**: $pub
- **总计封禁IP数**: ${ban_count}
- **总计解封IP数**: ${unban_count}
#### 封禁详情 (最多显示20条)
$([ -n "$ban_details" ] && echo -e "$ban_details" || echo "- 今日无新增封禁IP")"

alert_markdown "daily-report" "DDoS-Guard 每日战报" "$text"
log "每日战报已生成并推送。"
}
# ==============================================================================
# 主入口
# ==============================================================================
case "${1:-}" in
install)             cmd_install;;
uninstall)         cmd_uninstall;;
run)               shift || true; mode="${1:-wait}"; [ "$mode" = "--nowait" ] && mode="nowait" || mode="wait"; run_once "$mode";;
daemon)            cmd_daemon;;
status)            cmd_status;;
top)               cmd_top;;
history)             cmd_history;;
check)               cmd_check;;
tune)                cmd_tune;;
tune-guide)          cmd_tune_guide;;
whitelist-reload)    cmd_whitelist_reload;;
whitelist-show)      cmd_whitelist_show;;
whitelist-debug)   cmd_whitelist_debug;;
redis-status)      cmd_redis_status;;
ban)               cmd_ban "$@";;
unban)               cmd_unban "$@";;
flush-bans)          cmd_flush_bans;;
blacklist)         cmd_blacklist "$@";;
f2b-setup)         cmd_f2b_setup;;
daily-report)      cmd_daily_report;;
btwaf-sync-whitelist) cmd_whitelist_reload;;
*)
cat <<'USAGE'
用法:ddos-guard <subcommand>

子命令:
install | uninstall | run [--nowait] | daemon
status| top | history | check | tune | tune-guide
whitelist-reload | whitelist-show | whitelist-debug | redis-status
ban <ip> | unban <ip> | flush-bans | blacklist <ip|file> | f2b-setup
btwaf-sync-whitelist   # 兼容别名(同 whitelist-reload)

说明:
- 多站点:在 CC_LOG_PATHS 与 ERROR_LOG_PATHS 里一行一个日志文件即可;
          若日志里无法解析 Host,可在 LOG_HOST_MAP 中添加“文件路径正则 → 域名”的映射。
- 阈值:若存在 /etc/ddos/host-thresholds.json,则以该文件为准;否则使用脚本内置默认(已固化你的配置 + Discuz PATH_TIERS)。
- 蜘蛛策略:真·好蜘蛛(UA+PTR+正向回证)→ ddos_goodbots_v4 免扰;半可信先限速;伪造/恶意优先封禁。
- 白名单:每轮 run 都会聚合 BTWAF v4/v6 + ignore.ip/host;ban 前二次校验;
          whitelist-reload 后自动解封已进入白名单的误封 IP。
- Redis:用于 L7 原子计数和“日志游标缓存”(降低磁盘 I/O),无 Redis 自动降级。
- 动态调速:进入攻击模式→将巡检间隔改为 ATTACK_SCAN_INTERVAL(默认 2s),退出后恢复为 SCAN_INTERVAL。
- 钉钉:拥有“进入/退出攻击模式”的状态告警与“批次战报”,同类 10 分钟节流。
USAGE
    ;;
esac

为防止出错,你可以直接下载此脚本,然后进行修改后上传至:/usr/local/sbin/ 下面。【建议还是脚本命令步骤手工复制,以免文件存在编码差异问题】
其中白名单文件可以参考格式(加入自己的白名单):(上传至:/etc/ddos/)

然后,赋予脚本安装执行权限:
sudo chmod +x /usr/local/sbin/ddos-guard粘贴修改内容后的语法校验:(无任何错误输出表示通过)sudo bash -n /usr/local/sbin/ddos-guard启动安装并部署防御系统:
sudo /usr/local/sbin/ddos-guard install创建每日防御情况汇报计划任务:(每日防御日报)crontab -e粘贴到计划任务最后一行:(这里预设的是每日19:30进行防御汇报,改为你自己想要的时间。)30 19 * * * /usr/local/sbin/ddos-guard daily-report >/dev/null 2>&12025.9.20,新增防御细节手工操作:(智能陷阱):部署“Honeypot (蜜罐)”页面:
在宝塔所有站点:修改 robots.txt 在网站根目录的 robots.txt 文件中,增加一行,禁止所有爬虫访问防御系统的陷阱页面:新增:Disallow: /trap-page.html在网站模板的公共页脚部分,加入一个隐藏的链接。
在 Discuz! 默认模板 template/default/common/footer.htm 文件的 </body> 标签前,加入:
<a href="/trap-page.html" style="display:none; color:#fff;">do not crawl</a>这个链接对访客是完全不可见的,不影响程序正常运行。
最后,在宝塔面板的每个网站根目录创建个空文件为:trap-page.html---------------------------------------------------------------------------------------------------日志增强:(2025.9.20新功能)创建一个专门存放增强日志格式的文件。
sudo tee /www/server/nginx/conf/log_formats.conf <<'EOF'
# --- Custom Log Formats for ddos-guard ---

# 1. Map to get the first IP from X-Forwarded-For, fallback to remote_addr
map $http_x_forwarded_for $real_ip {
    "" $remote_addr;
    ~^(?P<first_ip>+),?.*$ $first_ip;
}

# 2. Define the new log format with the real IP as the first field
log_format cdn_real_ip '$real_ip - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent"';
EOF作用:这个命令会在 Nginx 的配置目录下创建一个名为 log_formats.conf 的新文件,并将我们需要的 map 和 log_format 定义写入其中。map 块的作用是智能地从 X-Forwarded-For 请求头中提取第一个 IP 作为真实 IP。
在宝塔面板中引用新文件并应用格式:
第二部分:在宝塔面板中引用新文件并应用格式
现在,我们需要告诉 Nginx 加载这个新文件,并让您的网站使用我们刚定义的 cdn_real_ip 格式。
[*]修改全局配置:

[*]登录宝塔面板,点击左侧 【软件商店】 -> 找到 Nginx -> 点击右侧的 【设置】。
[*]在弹出的窗口中,点击 【配置修改】。
[*]找到 http { 这一行,在它的正下方,加入 include /www/server/nginx/conf/log_formats.conf; 这一行。
如图所示新增:


include /www/server/nginx/conf/log_formats.conf;修改站点配置:
[*]回到宝塔面板主界面,点击左侧 【网站】 -> 找到 www.dz-x.net 站点 -> 点击 【设置】。【以你自己站点为准】
[*]点击 【配置文件】 选项卡。
[*]现在,我们只需要修改 access_log 这一行即可。找到它:

修改后,如图所示:




cdn_real_ip;----------------------------------------------------------------------------------------------------------------------------------
2025.9.20 下午追加功能:引入高风险IP信誉源(IP威胁情报库),实时情报纵深防御。创建一个全新的、独立的脚本,专门负责每天自动下载和更新高风险IP名单。
sudo vi /usr/local/sbin/ddos-guard-fetch-bad-ips.sh将以下完整内容粘贴到文件中:
#!/usr/bin/env bash
# ==============================================================================
# ddos-guard-fetch-bad-ips.sh: 威胁情报收集器
# 作用:每天从公开的信誉源拉取高风险IP/CIDR黑名单,并加载到ipset中
# ==============================================================================
set -euo pipefail

# --- 配置 ---
# 定义高风险IP信誉源 (您可以按需增删)
# FireHOL Level 1: 包含确认的攻击IP,误报率极低
SOURCES=(
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/dshield.netset"
    "https://sslbl.abuse.ch/blacklist/sslipblacklist.txt"
)

# ipset集合名称 (必须与主脚本 ddos-guard 中的一致)
SET_BADREP_V4="ddos_bad_reputation_v4"
IPSET=$(command -v ipset || { echo "ipset not found"; exit 1; })

# 临时工作目录
TMPDIR="/tmp/ddos-guard-badips"
mkdir -p "$TMPDIR"

# 最终合并的IP名单
FINAL_LIST_FILE="/etc/ddos/reputation-blacklist.txt"

# --- 脚本主逻辑 ---
echo "开始更新高风险IP名单..."

# 下载并处理所有源
RAW_LIST="$TMPDIR/raw_list.txt"
: > "$RAW_LIST"
for url in "${SOURCES[@]}"; do
    echo "正在下载: $url"
    curl -s --connect-timeout 15 "$url" | grep -v -E "^#|^;" | grep -v -E "^$" >> "$RAW_LIST" || echo "下载失败: $url"
done

# 清理、去重并格式化
echo "正在清理和去重..."
# 筛选出合法的IPv4地址和CIDR
grep -E "^({1,3}\.){3}{1,3}(/{1,2})?$" "$RAW_LIST" | sort -u > "$FINAL_LIST_FILE"

# 获取最终条目数
final_count=$(wc -l < "$FINAL_LIST_FILE")
echo "处理完成,共获得 $final_count 条独立的高风险IPv4条目。"

# 将新名单原子化地加载到 ipset 中
if [ -s "$FINAL_LIST_FILE" ]; then
    echo "正在将名单加载到 ipset: $SET_BADREP_V4 ..."
    # 使用临时ipset集合实现平滑、无缝的更新
    TEMP_SET="${SET_BADREP_V4}_temp"
    $IPSET create "$TEMP_SET" hash:net -exist

    # 批量导入
    {
      while read -r ip; do
            [ -n "$ip" ] && echo "add $TEMP_SET $ip"
      done < "$FINAL_LIST_FILE"
    } | $IPSET restore -exist

    # 原子化切换
    $IPSET swap "$TEMP_SET" "$SET_BADREP_V4"

    #销毁临时集合
    $IPSET destroy "$TEMP_SET"

    echo "ipset $SET_BADREP_V4 更新成功!"
else
    echo "未能生成有效的IP名单,跳过 ipset 更新。"
fi

# 清理临时文件
rm -rf "$TMPDIR"
echo "更新完成。"保存文件并赋予执行权限:
sudo chmod +x /usr/local/sbin/ddos-guard-fetch-bad-ips.sh设置定时任务 (Cron Job),让它每天凌晨自动执行一次:
# 打开 crontab 编辑器sudo crontab -e
# 在文件末尾加入下面这行,然后保存退出30 4 * * * /usr/local/sbin/ddos-guard-fetch-bad-ips.sh > /var/log/ddos-guard-fetch.log 2>&1注意:该IP威胁情报库功能,需要你结合上面最新升级的主脚本 ddos-guard 以利用情报。----------------------------------------------------------------------------------------------------------------------------------
常用命令:(如果白名单解析不完整,记得:sudo ddos-guard whitelist-reload重新装载一次所有来源白名单)# 重载服务重启时间计数器
sudo systemctl daemon-reload && sudo systemctl restart ddos-guard.timer

# 同步 宝塔nginx防火墙 白名单 + 强制巡检一轮
sudo ddos-guard btwaf-sync-whitelist
sudo ddos-guard run
sudo ddos-guard status

# 查看所有ipv4白名单
sudo ddos-guard whitelist-show

# 查看所有白名单统计
sudo ddos-guard whitelist-show --count

# Fail2ban 部署
sudo ddos-guard f2b-setup
sudo ddos-guard f2b-status

# 一键清空黑名单并尽量联动Fail2ban解封
sudo ddos-guard clear-bans [--all-jails]

# 删除单个被永久封禁的 IP
sudo nft delete element inet ddg bl4 { 220.181.108.93 }

# 清空所有永久封禁(慎用)
sudo nft flush set inet ddg bl4

# 清空所有临时封禁(如果也想顺手解开 tmp)
sudo ddos-guard flush-temp

# 修改后重新加载执行
sudo systemctl daemon-reload
sudo systemctl start ddos-guard.timer

# 查看防护定时器状态
sudo systemctl status ddos-guard.timer
--------------------------------------------------------------------------
# 查看状态
sudo ddos-guard status

# 实时监控
sudo ddos-guard top

# 钉钉推送消息自检(打印两次返回体)
sudo ddos-guard ding-check

# 查看钉钉配置(这里会显示完整 host)
sudo ddos-guard ding-show-config

# 并手工跑一轮,观测是否有 MASS_BAN 推送
sudo rm -f /var/lib/ddos-guard/notify/last_*.ts
sudo env DINGTALK_DEBUG=2 ddos-guard run

# 查看防护定时器状态
sudo systemctl status ddos-guard.timer

# 查看封禁历史
sudo ddos-guard history

# 手动运行一次扫描
sudo ddos-guard run

# 查看防护服务运行状态
sudo systemctl status ddos-guard.service

# 查看当前黑名单
sudo ddos-guard blacklist

# 查看IP威胁情报库
sudo ipset list ddos_tempban

# 查看永久封禁黑名单(IP威胁情报库)
sudo ddos-guard perma-list

# 网络调优内核防御参数临时应用
sudo ddos-guard tune

# ddos-guard 环境自检报告
sudo ddos-guard check

# 查看服务器Redis原生CC防御引擎状态
sudo ddos-guard redis-status

# 一键清空封禁
sudo ddos-guard flush-bans

# 或同时清情报库并尝试从 fail2ban 解封
sudo ddos-guard flush-bans --purge-intel

# 集成 Fail2ban 联动防御初始化
sudo ddos-guard f2b-setup

# 刷新域名白名单缓存
sudo ddos-guard refresh-dns

# 刷新宝塔白名单缓存
ddos-guard refresh-btwaf

# 完全卸载~
sudo ddos-guard uninstall || true

# 手动销毁旧集合(此时 iptables 规则已被卸载,不会被引用)
for s in ddos_white ddos_tempban ddos_permban ddos_white_v6 ddos_tempban_v6 ddos_permban_v6; do
sudo ipset destroy "$s" 2>/dev/null || true
done

# 重新安装
sudo ddos-guard install

# 如果之前 family 弄错、timer 没装好:
sudo ddos-guard repair-ipsets
sudo ddos-guard fix-systemd


# 重新同步白名单并跑一轮检测
sudo ddos-guard btwaf-sync-whitelist
sudo ddos-guard run

# 关闭宝塔系统加固
btpython /www/server/panel/plugin/syssafe/stop.py 0**** Hidden Message *****



Discuz智能体 发表于 2025-9-2 22:49:45

{
    "title": "DDoS-Guard 安装与使用指南",
    "content": "感谢您选择 DZ插件网 优化的 DDoS-Guard 防御脚本。以下是完整的安装配置说明:\n\n## 功能特性\n- 实时监控服务器连接数\n- 自动封禁超阈值IP地址\n- 智能整合 Fail2ban、宝塔WAF、用户白名单\n- 详细的日志记录与分析功能\n- 实时状态监控与交互界面\n- 系统服务化安装,稳定可靠\n\n## 安装步骤\n\n### 1. 创建安装脚本\n\nsudo vi /usr/local/sbin/ddos-guard\n\n\n### 2. 粘贴脚本内容\n将提供的脚本内容完整复制到文件中\n\n### 3. 赋予执行权限\n\nsudo chmod +x /usr/local/sbin/ddos-guard\n\n\n### 4. 创建配置文件目录\n\nsudo mkdir -p /etc/ddos\n\n\n### 5. 创建白名单文件(可选)\n\n# IP白名单\nsudo touch /etc/ddos/ignore.ip.list\n\n# 域名白名单\nsudo touch /etc/ddos/ignore.host.list\n\n\n### 6. 安装系统服务\n\nsudo /usr/local/sbin/ddos-guard install\n\n\n## 配置说明\n\n### 主要参数调整\n编辑脚本中的以下参数以适应您的服务器环境:\n\n\nNO_OF_CONNECTIONS=68      # 连接数阈值\nBAN_PERIOD=3600             # 封禁时长(秒)\nSCAN_INTERVAL=10            # 扫描间隔(秒)\n\n\n### 日志文件位置\n\nLOG_FILE=\"/var/log/ddos-guard.log\"         # 运行日志\nBAN_HISTORY=\"/var/log/ddos-guard-history.log\" # 封禁历史\n\n\n## 使用方法\n\n### 启动守护进程\n\nsudo systemctl start ddos-guard\n\n\n### 查看实时状态\n\nsudo /usr/local/sbin/ddos-guard status\n\n\n### 交互式监控\n\nsudo /usr/local/sbin/ddos-guard monitor\n\n\n### 查看封禁历史\n\nsudo /usr/local/sbin/ddos-guard history\n\n\n## 注意事项\n\n1. **系统要求**:Debian 12 + 宝塔面板 11\n2. **依赖检查**:确保已安装 fail2ban 和宝塔防火墙\n3. **白名单配置**:将信任的IP添加到 `/etc/ddos/ignore.ip.list`\n4. **阈值调整**:根据服务器性能调整连接数阈值\n5. **日志监控**:定期检查日志文件了解防御情况\n\n## 故障排除\n\n如果遇到问题,请检查:\n- 脚本执行权限是否正确\n- 依赖工具(iptables、netstat)是否安装\n- 日志文件是否有错误信息\n- 防火墙规则是否正常生效\n\n## 技术支持\n\n如需进一步的技术支持,请访问 DZ插件网 获取更多帮助文档和社区支持。\n\n---\n*本脚本基于 ddos-deflate 增强开发,专为 Discuz! 网站优化防御能力*"
}
-- 本回答由 人工智能 AI智能体 生成,内容仅供参考,请仔细甄别。

神风 发表于 2025-9-10 22:07:51

看看隐藏内容

apwl 发表于 2025-9-11 09:05:45

学习一下

GM-爱好者 发表于 2025-9-11 13:45:42

基于ddos-deflate增强实时扫描高连接数 IP自动封禁超过阈值的 IP整合 Fail2ban、宝塔nginx防火墙、用户自定义白名单的防御攻击 [修改]

shijingyuan 发表于 2025-9-12 11:21:04

看看qqqqqqqq

code_boy 发表于 2025-9-17 00:55:40

学习收藏下哦

peminga 发表于 2025-9-19 01:05:38

看看好用不

iceblood16 发表于 2025-9-20 11:01:11

感谢站长无私分享

热火朝天 发表于 2025-9-28 12:26:31

感谢站长无私分享
页: [1]
查看完整版本: 基于ddos-deflate增强实时扫描高连接数 IP自动封禁超过阈值的 IP整合 Fail2ban、宝塔nginx防火墙、用户自定义白名单的防御攻击