马上注册,免费下载更多dz插件网资源。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
基于ddos-deflate增强实时扫描高连接数 IP自动封禁超过阈值的 IP整合 Fail2ban、宝塔nginx防火墙、Redis CC L7层防御、用户自定义白名单的防御攻击脚本:【⭐⭐⭐⭐⭐推荐!】
DZ插件网的实际运用效果图:
本次更新日志: [2025.9.13 终极更新]
功能分类 | 功能模块 | v24.27 终极版状态 | 审计说明与优化点 | 核心防御引擎 | 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.13增强防御前置防御参数配置:[即在/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": "/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": "/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 }
- ]
- }
- }
复制代码
创建安装脚本:
- sudo vi /usr/local/sbin/ddos-guard
复制代码 粘贴如下经过DZ插件网优化实战的核心安装脚本内容:【2025-09-13 更新并实测验证无错,请将脚本内容的部分参数根据自己实际情况改写】
完整脚本内容:
【记得批量搜索“你的”替换为你自己的实际信息】(注意完整复制不要有任何格式符号编码错误)
- #!/usr/bin/env bash
- # ==============================================================================
- # ddos-guard v24.27-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 计数)
- # - 子命令: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
- # 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"
- )
- # 错误日志(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}
- # ----------------------------- 蜘蛛 UA 策略 --------------------------------
- GOOD_BOT_PATTERN='Googlebot|Baiduspider|bingbot|Sogou|360Spider|YisouSpider|YoudaoBot|msnbot|Yahoo! Slurp|YandexBot|DNSPod-Monitor|AspiegelBot'
- SEMI_TRUST_BOTS='Bytespider|ToutiaoSpider'
- BAD_BOT_PATTERN='Applebot|Amazonbot|GPTBot|ClaudeBot|PetalBot|DataForSeoBot|meta-externalagent|okhttp|Thinkbot|MJ12bot|SemrushBot|AhrefsBot|DotBot|Scrapy|python-requests|aiohttp|curl|wget|python-urllib|Go-http-client|Java/|PycURL|httpx'
- 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$"
- )
- # ----------------------------- 白名单源 ------------------------------------
- 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"
- # ----------------------------- 颜色/状态 ----------------------------------
- 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')] [ddos-guard] $*" | 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" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && IFS=. read -r a b c d <<<"$1" && ((a<=255&&b<=255&&c<=255&&d<=255)); }
- is_ipv6(){ [[ "$1" == *:* ]]; }
- is_cidr4(){ [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] && IFS='/.' read -r a b c d m <<<"${1//\//.}" && ((a<=255&&b<=255&&c<=255&&d<=255)); }
- is_cidr6(){ [[ "$1" =~ :/.+ ]] && [[ "${1##*/}" =~ ^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$ ]]; }
- 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[1]
- 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[6-9]|2[0-9]|3[0-1])\. ]] && 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}'; }
- # 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[1]
- 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[0]),int(it[1])
- 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[1],'r',encoding='utf-8'))
- for it in data:
- if isinstance(it,list) and it:
- s=str(it[0]).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_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
- $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
- 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())[1:-1])'; }
- 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(){
- [ -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[1]
- 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){
- # [2001:db8::1]:443 / 1.2.3.4:54321 / 2001:db8::1
- if (s ~ /^\[/) { gsub(/^\[/,"",s); sub(/\].*$/,"",s); return s; }
- sub(/:[0-9]+$/,"",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
- 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}')
- if (( ct_count>NO_OF_CT || syn_total>NO_OF_SYN || est_total>NO_OF_EST )); then echo 1; else echo 0; fi
- }
- # ==============================================================================
- # 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[1])
- # 增强的 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[1];
- if (match(line, /"[^"]+ (https?:\/\/([^\/ ]+))?\/[^"]*"/, b)) { if (b[2]!="") return b[2]; }
- return fb;
- }
- {
- ip=$1; line=$0; path="/"
- if (match($0, /"[^"]+"/, a)) {
- req=a[0]
- status_code=0
- # 尝试从日志行中提取 HTTP 状态码 (例如 " 200 ")
- if (match($0, /"[^"]+" ([0-9]{3})/, s)) status_code=s[1]
- if (match(req, /"[^ ]+ ([^ ?"]+)/, p)) path=p[1]
- # Discuz! 后台路径动态识别逻辑
- # 如果访问的是后台页面,并且状态码是200 (表示成功)
- if (path ~ /^/(dismall|admin|plugin)\.php$/ && status_code == 200) {
- # 输出一个特殊标记 ADMIN_LOGIN 和对应的 IP
- print "ADMIN_LOGIN", ip, g(line)
- } else {
- # 对于其他所有访问,输出常规标记 NORMAL_HIT
- print "NORMAL_HIT", ip, g(line), path
- }
- }
- }' >> "$tmp"
- done
- $AWK '{k=$1"|" $2"|" $3; c[k]++} END{for (k in c){split(k,a,"|"); print c[k], a[1], a[2], a[3]}}' "$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" '
- /limiting connections by zone "perip"/ {
- ip="";host="";
- if (match($0, /client: ([0-9.]+)/, a)) ip=a[1];
- if (match($0, /server: ([^, ]+)/, b)) host=b[1];
- if (ip!="") {print ip, (host==""?fb:host)}
- }' >> "$tmp"
- done
- if [ -s "$tmp" ]; then
- $AWK '{k=$1"|" $2; c[k]++} END{for (k in c){split(k,a,"|"); print c[k], a[1], a[2]}}' "$tmp" \
- | sort -nr \
- | while read -r cnt ip host; do
- local base="${HT_REQ_L3[$host]:-${HT_REQ_L3["*"]:-$((CC_THRESHOLD*4))}}"
- local esc_thr=$(( base/2 )); [ "$esc_thr" -lt 5 ] && esc_thr=5
- 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(){
- [ -n "$DINGTALK_WEBHOOK" ] || return 0
- [ "${#BANNED_THIS_RUN[@]}" -eq 0 ] && [ "${#LIMITED_THIS_RUN[@]}" -eq 0 ] && return 0
- local pub="N/A" lan="N/A"
- pub="$(get_public_ip)"
- lan="$(hostname -I 2>/dev/null | awk '{print $1}')"
- local ban_lines="" lim_lines=""
- for x in "${BANNED_THIS_RUN[@]}"; do ban_lines+="- $x\n"; done
- for x in "${LIMITED_THIS_RUN[@]}"; do lim_lines+="- $x\n"; done
- local text="### 🚧 DDoS-Guard 批次战报
- - **时间**:$($DATE '+%F %T')
- - **主机**:$pub(外网) / $lan(内网)
- - **封禁条目**:
- $([ -n "$ban_lines" ] && echo -e "$ban_lines" || echo "- 无")
- - **限速条目**:
- $([ -n "$lim_lines" ] && echo -e "$lim_lines" || echo "- 无")"
- alert_markdown "batch" "DDoS-Guard 批次战报" "$text"
- }
- alert_attack_state(){
- local state="$1" # enter / exit
- 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 '?')
- local text="### ⚠️ 攻击模式$( [ "$state" = "enter" ] && echo 进入 || echo 退出 )
- - **时间**:$($DATE '+%F %T')
- - **主机**:$pub / $lan
- - **扫描间隔**:${current_interval%s}s"
- alert_markdown "state-$state" "DDoS-Guard 攻击模式:${state}" "$text"
- }
- run_core(){
- ensure_ipset_and_iptables
- load_host_thresholds
- load_whitelist
- reconcile_unban_whitelisted # 双保险自愈
- BANNED_THIS_RUN=(); LIMITED_THIS_RUN=()
- local attack_mode=$(should_attack_mode)
- 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)
- 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"
- alert_attack_state "enter"
- 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
- is_in_whitelist_snapshot "$ip" && continue
- # 增加动态管理员豁免检查
- $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2_>/dev/null && continue
- [ "$(is_private_ip "$ip")" = "1" ] && continue
- $IPSET test "$SET_GB_V4" "$ip" >/dev/null 2>&1 && continue
- (( 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_>/dev/null && 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
- # L7:增量日志计数 → Host/Path Tiers → UA 分类 → Redis 限速/封禁
- local hits; hits=$(count_hits_in_logs || true)
- if [ -n "$hits" ]; then
- while read -r tag cnt ip host path; do
- # 如果标签是 ADMIN_LOGIN, 说明是管理员IP
- if [ "$tag" = "ADMIN_LOGIN" ]; then
- # 将该IP (此时存储在cnt变量中) 加入管理员信任集合并跳过后续检查
- $IPSET add "$SET_ADMIN_V4" "$cnt" -exist
- continue
- fi
- [ -z "$ip" ] && continue
- is_in_whitelist_snapshot "$ip" && continue
- # 增加动态管理员豁免检查
- $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2_>/dev/null && continue
- local uac; uac="$(classify_ua_for_ip "$ip")"
- if [ "$uac" = "GOOD" ]; then $IPSET add "$SET_GB_V4" "$ip" -exist; continue; 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[1]); w=float(sys.argv[2])
- print(int(max(3, l1*w*0.4)))
- PY
- )
- 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
- $IPSET add "$SET_RL_V4" "$ip" -exist
- LIMITED_THIS_RUN+=("$ip ($host $path L7-limit cnt=$cnt thr=$thr)")
- else
- ban_ip "$ip" "L7-CC($host $path,cnt=$cnt,thr=$thr,ua=$uac)"
- fi
- fi
- done <<< "$hits"
- fi
- # 错误日志 perip 升级:多次限流 → 限速/封禁
- perip_escalation_from_errorlog
- # 批次战报(节流 10 分钟)
- alert_markdown_batch
- }
- # 运行一次(锁控制:--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
- [Unit]
- Description=DDoS Guard one-shot scan
- After=network-online.target
- Wants=network-online.target
- [Service]
- Type=oneshot
- ExecStart=$SELF_PATH run --nowait
- User=root
- Group=root
- Nice=-5
- IOSchedulingClass=realtime
- [Install]
- WantedBy=multi-user.target
- SERVICE
- cat >"$timer_unit" <<TIMER
- [Unit]
- Description=Run ddos-guard every ${SCAN_INTERVAL}s
- [Timer]
- OnBootSec=30s
- OnUnitActiveSec=${SCAN_INTERVAL}s
- AccuracySec=1s
- Unit=ddos-guard.service
- Persistent=true
- [Install]
- 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 '^\[DEFAULT\]' "$jail"; then echo -e "[DEFAULT]\nignoreip = 127.0.0.1/8" >> "$jail"; fi
- local old; old=$(awk -F'= *' '/^\[DEFAULT\]/{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[1] or "").split(",")
- a=(sys.argv[2] or "").split(",")
- s=set([x.strip() for x in o if x.strip()])|set([x.strip() for x in a if x.strip()])
- print(",".join(sorted(s)))
- PY
- )
- python3 - <<'PY' "$jail" "$merged"
- import sys,re
- p,merged=sys.argv[1],sys.argv[2]
- s=open(p,'r',encoding='utf-8').read()
- def repl(m): return m.group(1)+'= '+merged
- s=re.sub(r'(^\[DEFAULT\][\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
复制代码 其中来源白名单文件参考或者直接借用:
ignore.ip.list
(10.56 KB, 下载次数: 1)
ignore.host.list
(506 Bytes, 下载次数: 1)
白名单自定义文件上传存放到:/etc/ddos/ 为防止出错,你可以直接下载此脚本,然后进行修改后上传至:/usr/local/sbin/ 下面。【建议还是脚本命令步骤手工复制,以免文件存在编码差异问题】 赋予脚本安装执行权限:
- sudo chmod +x /usr/local/sbin/ddos-guard
复制代码粘贴修改内容后的语法校验:(无任何错误输出表示通过) - sudo bash -n /usr/local/sbin/ddos-guard
复制代码 启动安装并部署防御系统:
- sudo /usr/local/sbin/ddos-guard install
复制代码创建每日防御情况汇报计划任务:(每日防御日报) 粘贴到计划任务最后一行:(这里预设的是每日19:30进行防御汇报,改为你自己想要的时间。) - 30 19 * * * /usr/local/sbin/ddos-guard daily-report >/dev/null 2>&1
复制代码
常用命令:(如果白名单解析不完整,记得:sudo ddos-guard whitelist-reload 重新装载一次所有来源白名单)
©DZ插件网所发布的一切资源仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。 本站内容为站长个人技术研究记录或网络,不提供用户交互功能,所有内容版权归原作者所有。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,得到更好的正版服务。 您在本站任何的赞助购买、下载、查阅、回复等行为等均表示接受并同意签订《DZ插件网免责声明协议》。 如有侵权请邮件与我们联系处理: discuzaddons@vip.qq.com 并出示相关证明以便删除。敬请谅解!
|
ip, echo, FILE, TMP, BAN, FILE, TMP, BAN, FILE, TMP, BAN, FILE, TMP, BAN, FILE, TMP, BAN, FILE, TMP, BAN, FILE, TMP, BAN, FILE, TMP,
|