去评论
dz插件网

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

admin
2025/09/02 22:44:56
鍩轰簬ddos-deflate澧炲己瀹炴椂鎵弿楂樿繛鎺ユ暟 IP鑷姩灏佺瓒呰繃闃堝€肩殑 IP鏁村悎 Fail2ban銆佸疂濉攏ginx闃茬伀澧欍€丷edis CC L7灞傞槻寰°€佺敤鎴疯嚜瀹氫箟鐧藉悕鍗曠殑闃插尽鏀诲嚮鑴氭湰锛氥€愨瓙猸愨瓙猸愨瓙鎺ㄨ崘锛
DZ鎻掍欢缃戠殑瀹為檯杩愮敤鏁堟灉鍥撅細

鏈鏇存柊鏃ュ織锛 [2025.9.13 缁堟瀬鏇存柊]
鍔熻兘鍒嗙被鍔熻兘妯″潡v24.27 缁堟瀬鐗堢姸鎬瀹¤璇存槑涓庝紭鍖栫偣
鏍稿績闃插尽寮曟搸L7 搴旂敤灞傞槻寰鍖呭惈瀹屾暣鍖呭惈CC銆佺伆鑹叉満鍣ㄤ汉銆丏iscuz!绮惧噯鎵撳嚮銆佹悳绱㈠紩鎿庝繚鎶ゃ€
L4 缃戠粶灞傞槻寰鍖呭惈閲囩敤鏈€缁堜慨姝g殑awk $NF寮曟搸锛岀簿鍑嗙粺璁¤繛鎺ユ暟銆
鏅鸿兘闃插尽浣撶郴鑷€傚簲鏀诲嚮妯″紡鍖呭惈鍙牴鎹叏灞€娴侀噺锛岃嚜鍔ㄥ崌闄峀4鍜孡7闃插尽闃堝€笺€
娓愯繘寮忓皝绂鍖呭惈瀹屾暣瀹炵幇锛氬鍙嶅鏀诲嚮鐨処P鑷姩鍗囩骇灏佺鏃堕暱锛岀洿鑷虫案涔呭皝绂併€
鎭舵剰IP鎯呮姤搴鍖呭惈瀹屾暣瀹炵幇锛氭案涔呰褰曡鈥滈《鏍煎缃氣€濈殑IP锛屼究浜庡彇璇併€
鑷繘鍖栫櫧鍚嶅崟鍖呭惈瀹屾暣瀹炵幇锛氬彲瀹氭湡浠嶤DN鏈嶅姟鍟嗚嚜鍔ㄨ幏鍙栧苟鏇存柊IP鍒楄〃銆
Redis CC鍘熷瓙寮曟搸鍖呭惈瀹屾暣瀹炵幇锛氬紩鍏モ€淩edis鍘熺敓CC闃插尽寮曟搸鈥濓紝鑷姩妫€娴嬪苟浣跨敤Redis缂撳瓨锛屾瀬澶ч檷浣庣鐩業/O銆
鍔ㄦ€佹壂鎻忛鐜鍖呭惈瀹屾暣瀹炵幇锛氭敾鍑绘ā寮忎笅鑷姩鍔犻€熸壂鎻忥紝骞虫伅鍚庤嚜鍔ㄩ檷閫熴€
鐪熷亣铚樿洓DNS鏍¢獙鍖呭惈閫氳繃DNS鍙屽悜瑙f瀽锛100%璇嗗埆浼鎴愭牳蹇冩悳绱㈠紩鎿庣殑鏀诲嚮鑰呫€
鍛婅涓庝氦浜鈥滅姸鎬+鎴樻姤鈥濆弻鍛婅鍖呭惈鍚屾椂鎷ユ湁鈥滆繘鍏/閫€鍑烘敾鍑绘ā寮忊€濈殑鐘舵€佸憡璀﹀拰鍖呭惈绮惧噯鏀诲嚮鍒楄〃鐨勨€滄壒娆℃垬鎶モ€濄€
鐢ㄦ埛瀹氬埗鍖栨牸寮鍖呭惈鍛婅鏍煎紡銆丒moji銆佹湇鍔″櫒IP绛夊潎宸叉寜鎮ㄧ殑鏈€缁堣姹傚畾鍒躲€
鐧藉悕鍗曚笌璋冧紭鍏ㄦ潵婧愮櫧鍚嶅崟鍖呭惈瀹屾暣鏀寔ignore.ip.list, ignore.host.list, 瀹濆, Fail2ban銆
鍐呮牳鍙傛暟璋冧紭鍖呭惈tune鍛戒护瀹屾暣淇濈暀銆
鐢ㄦ埛瀹氬埗鍖栭槇鍊宸蹭慨姝L4灞傞槇鍊煎凡涓ユ牸鎸夌収DZ鎻掍欢缃戞渶缁堝疄璺 40/15/60 杩涜棰勮
浠g爜璐ㄩ噺瀹屾暣鎬т笌鍙鎬100%淇濊瘉鎵€鏈変唬鐮佸潎宸叉仮澶嶄负娓呮櫚銆佸甫缂╄繘鍜岃灏芥敞閲婄殑澶氳鏍煎紡锛屾棤浠讳綍鐪佺暐銆
2025.9.13澧炲己闃插尽鍓嶇疆闃插尽鍙傛暟閰嶇疆锛歔鍗冲湪/etc/ddos 璺緞涓嬮潰鍒涘缓 host-thresholds.json]锛堜笅闈㈠煙鍚嶉兘鏇挎崲涓轰綘鑷繁瀹為檯鍩熷悕锛
鐩存帴鍛戒护鍒涘缓瀹屾暣 缃戠珯缁嗗垎棰楃矑搴﹂槻寰¢槇鍊煎紩瀵兼枃浠讹紙鍝€曞繕璁板垱寤 鑴氭湰涔熸湁鍐椾綑淇濆簳锛 锛

  1. sudo cat <<'EOF' > /etc/ddos/host-thresholds.json
  2. {
  3.   "GLOBAL": {
  4.     "NO_OF_SYN": 180,
  5.     "NO_OF_EST": 260,
  6.     "NO_OF_CONNTRACK": 15000
  7.   },

  8.   "default": {
  9.     "HOST_WEIGHT": 1.0,
  10.     "REQ_PER_MIN": { "L1": 30, "L2": 60, "L3": 120 },
  11.     "SYN_PER_IP": 0,
  12.     "EST_PER_IP": 0,
  13.     "PATH_TIERS": [
  14.       { "KIND": "PREFIX", "PAT": "/favicon.ico", "L1": 200, "L2": 400, "L3": 800 },
  15.       { "KIND": "PREFIX", "PAT": "/static/",     "L1": 200, "L2": 400, "L3": 800 }
  16.     ]
  17.   },

  18.   "www.dz-x.net": {
  19.     "HOST_WEIGHT": 1.0,
  20.     "REQ_PER_MIN": { "L1": 28, "L2": 56, "L3": 112 },
  21.     "SYN_PER_IP": 60,
  22.     "EST_PER_IP": 90,
  23.     "PATH_TIERS": [
  24.       { "KIND": "PREFIX", "PAT": "/avatar.php",                 "L1": 10, "L2": 20, "L3": 40 },
  25.       { "KIND": "REGEX",  "PAT": "^/misc\\.php\\?mod=seccode",  "L1": 6,  "L2": 12, "L3": 24 },
  26.       { "KIND": "REGEX",  "PAT": "^/forum\\.php\\?mod=image",   "L1": 8,  "L2": 16, "L3": 32 },
  27.       { "KIND": "PREFIX", "PAT": "/ajax.php",                   "L1": 12, "L2": 24, "L3": 48 },
  28.       { "KIND": "REGEX",  "PAT": "^/plugin\\.php\\?id=",        "L1": 8,  "L2": 16, "L3": 32 },
  29.       { "KIND": "PREFIX", "PAT": "/search.php",                 "L1": 4,  "L2": 8,  "L3": 16 },
  30.       { "KIND": "PREFIX", "PAT": "/home.php",                   "L1": 16, "L2": 32, "L3": 64 },
  31.       { "KIND": "PREFIX", "PAT": "/member.php",                 "L1": 10, "L2": 20, "L3": 40 },
  32.       { "KIND": "REGEX",  "PAT": "^/forum\\.php\\?mod=viewthread", "L1": 14, "L2": 28, "L3": 56 },
  33.       { "KIND": "PREFIX", "PAT": "/api/mobile/",                "L1": 10, "L2": 20, "L3": 40 },
  34.       { "KIND": "PREFIX", "PAT": "/uc_server/",                 "L1": 12, "L2": 24, "L3": 48 }
  35.     ]
  36.   },

  37.   "demo.dz-x.net": {
  38.     "HOST_WEIGHT": 0.8,
  39.     "REQ_PER_MIN": { "L1": 24, "L2": 48, "L3": 96 },
  40.     "SYN_PER_IP": 50,
  41.     "EST_PER_IP": 80,
  42.     "PATH_TIERS": [
  43.       { "KIND": "PREFIX", "PAT": "/avatar.php",                "L1": 10, "L2": 20, "L3": 40 },
  44.       { "KIND": "REGEX",  "PAT": "^/misc\\.php\\?mod=seccode", "L1": 6,  "L2": 12, "L3": 24 },
  45.       { "KIND": "PREFIX", "PAT": "/ajax.php",                  "L1": 12, "L2": 24, "L3": 48 },
  46.       { "KIND": "REGEX",  "PAT": "^/plugin\\.php\\?id=",       "L1": 8,  "L2": 16, "L3": 32 },
  47.       { "KIND": "PREFIX", "PAT": "/search.php",                "L1": 4,  "L2": 8,  "L3": 16 }
  48.     ]
  49.   }
  50. }
  51. EOF


鍒涘缓瀹夎鑴氭湰锛
  1. sudo vi /usr/local/sbin/ddos-guard
绮樿创濡備笅缁忚繃DZ鎻掍欢缃戜紭鍖栧疄鎴樼殑鏍稿績瀹夎鑴氭湰鍐呭锛氥€2025-09-13 鏇存柊骞跺疄娴嬮獙璇佹棤閿欙紝璇峰皢鑴氭湰鍐呭鐨勯儴鍒嗗弬鏁版牴鎹嚜宸卞疄闄呮儏鍐垫敼鍐欍€
瀹屾暣鑴氭湰鍐呭锛
銆愯寰楁壒閲忔悳绱⑩€滀綘鐨勨€濇浛鎹负浣犺嚜宸辩殑瀹為檯淇℃伅銆锛堟敞鎰忓畬鏁村鍒朵笉瑕佹湁浠讳綍鏍煎紡绗﹀彿缂栫爜閿欒锛
  1. #!/usr/bin/env bash
  2. # ==============================================================================
  3. # ddos-guard v24.27-Stable (Debian 12 + BT Panel + Discuz! X3.5)
  4. # 鏍稿績娓呭崟锛堢畝瑕侊級
  5. # - BTWAF v4/v6 鐧藉悕鍗曡В鏋愶紝甯﹁缁嗘潵婧愮粺璁′笌涓侀拤鎺ㄩ€
  6. # - 缁熶竴鐧藉悕鍗曪細ignore.ip.list / ignore.host.list / BTWAF / Fail2ban ignoreip
  7. # - 鐧藉悕鍗曟洿鏂板悗鑷姩瑙e皝琚鏉€ IP锛屽甫瑙e皝璁℃暟
  8. # - nftables 寮曟搸锛坵l{4,6}/tmp{4,6}/bl{4,6}锛夛紝鍥為€€ iptables+ipset
  9. # - L4/L7 娣峰悎锛歋YN/EST per-IP銆佸叏灞€ CT锛岃嚜閫傚簲 GLOBAL 闃堝€硷紱Redis 鍘熷瓙璁℃暟鐨 L7 闄愰€
  10. # - 绔欑偣瑕嗗啓/鏉冮噸锛/etc/ddos/host-thresholds.json锛夛紝鐭獥 IP鈫扝ost 绱㈠紩
  11. # - 鐪熷亣铚樿洓锛圲A 妯″紡 + 鍙嶆煡/姝e悜鍥炶瘉锛夛紝Discuz 绮炬墦 + 娉涘姩鎬侀〉 CC
  12. # - 娓愯繘寮忓皝绂侊紙T1/T2/T3鈫掓案涔咃級锛屾伓鎰 IP 鎯呮姤搴擄紙鍩轰簬 Redis 璁℃暟锛
  13. # - 瀛愬懡浠わ細install / uninstall / run / daemon / status / top / history /
  14. #           check / tune / tune-guide / whitelist-reload / whitelist-show /
  15. #           ban / unban / flush-bans / blacklist / f2b-setup
  16. # ==============================================================================

  17. set -euo pipefail
  18. IFS=$'\n\t'

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

  21. # ----------------------------- 杩愯鐩綍/鏃ュ織 --------------------------------
  22. LOG_FILE="/var/log/ddos-guard.log"            # 鑴氭湰杩愯鏃ュ織
  23. BAN_HISTORY="/var/log/ddos-guard-history.csv"  # 灏佺鍘嗗彶锛圕SV锛
  24. WORKDIR="/usr/local/ddos-deflate"              # 鍏煎/淇濈暀鐨勬暟鎹洰褰
  25. RUNDIR="/run/ddos-guard"                       # 杩愯鎬佹暟鎹€佸揩鐓с€佽妭娴佹爣璁扮瓑
  26. TMPDIR="/tmp/ddos-guard"                       # 涓存椂鏂囦欢鐩綍
  27. mkdir -p "$WORKDIR" "$RUNDIR" "$TMPDIR"

  28. LOCK_FILE="/var/lock/ddos-guard.lock"          # 璺ㄨ繘绋嬩簰鏂ラ攣
  29. LOCK_FD=200

  30. # ----------------------------- 澶栭儴渚濊禆 ------------------------------------
  31. SS=$(command -v ss || true)
  32. IPT=$(command -v iptables || true)
  33. IP6T=$(command -v ip6tables || true)
  34. IPSET=$(command -v ipset || true)
  35. CT=$(command -v conntrack || true)
  36. TAIL=$(command -v tail || true)
  37. GREP=$(command -v grep || true)
  38. AWK=$(command -v awk || true)
  39. SED=$(command -v sed || true)
  40. SORT=$(command -v sort || true)
  41. UNIQ=$(command -v uniq || true)
  42. CUT=$(command -v cut || true)
  43. HEAD=$(command -v head || true)
  44. DATE=$(command -v date || true)
  45. HOST=$(command -v host || true)
  46. PY3=$(command -v python3 || true)
  47. CURL=$(command -v curl || true)
  48. REDIS_CLI=$(command -v redis-cli || true)
  49. GETENT=$(command -v getent || true)
  50. STAT=$(command -v stat || true)
  51. SHA1=$(command -v sha1sum || command -v shasum || true)
  52. MD5=$(command -v md5sum || true)

  53. # ----------------------------- 閽夐拤鍛婅 ------------------------------------
  54. # - 璁剧疆 DINGTALK_WEBHOOK 鍗冲彲鍚敤閽夐拤鏈哄櫒浜
  55. # - 鍚岀被鍛婅鑺傛祦榛樿 600s锛10 鍒嗛挓锛
  56. DINGTALK_WEBHOOK="${DINGTALK_WEBHOOK:-https://oapi.dingtalk.com/robot/send?access_token=浣犻拤閽夌殑webhook鏈哄櫒浜轰俊鎭瘆"
  57. DINGTALK_THROTTLE_SEC="${DINGTALK_THROTTLE_SEC:-600}"
  58. ALERT_TS_DIR="$RUNDIR/alert-ts"; mkdir -p "$ALERT_TS_DIR"

  59. # ----------------------------- L4 鍏滃簳闃堝€ ---------------------------------
  60. # 鑻 /etc/ddos/host-thresholds.json 鐨 GLOBAL 娈靛瓨鍦紝浼氳鐩栬繖浜涒€滃厹搴曗€
  61. NO_OF_SYN=${NO_OF_SYN:-180}               # 鍏ㄥ眬 SYN锛堟€婚噺锛夎Е鍙戠嚎
  62. NO_OF_EST=${NO_OF_EST:-260}               # 鍏ㄥ眬 EST锛堟€婚噺锛夎Е鍙戠嚎
  63. NO_OF_CONNTRACK=${NO_OF_CONNTRACK:-15000} # 鍏ㄥ眬 conntrack锛堟€婚噺锛夎Е鍙戠嚎
  64. NO_OF_CT="$NO_OF_CONNTRACK"

  65. # 鏀诲嚮妯″紡涓 per-IP 鐨 L4 鏇翠弗闃堝€
  66. ENABLE_DYNAMIC_THRESHOLDS=${ENABLE_DYNAMIC_THRESHOLDS:-true}
  67. NO_OF_EST_ATTACK_MODE=${NO_OF_EST_ATTACK_MODE:-120}        # 鍙傝€ www.dz-x.net/t/151300/1/1.html
  68. NO_OF_SYN_ATTACK_MODE=${NO_OF_SYN_ATTACK_MODE:-68}        # 鍙傝€ www.dz-x.net/t/151300/1/1.html

  69. # L7 鍏滃簳闃堝€硷紙姣忚矾寰 L1 鍩虹嚎锛孒ost/Path tiers 浼氳鐩栵級
  70. CC_THRESHOLD=${CC_THRESHOLD:-30}
  71. CC_THRESHOLD_ATTACK_MODE=${CC_THRESHOLD_ATTACK_MODE:-5}

  72. # ----------------------------- 澶氱珯鐐规棩蹇 ----------------------------------
  73. # 璁块棶鏃ュ織锛圕C 鏃ュ織锛夛細涓€琛屼竴涓枃浠讹紱鏃犳硶浠庢棩蹇楄В鏋 Host 鏃讹紝鐢 LOG_HOST_MAP 鍏滃簳 [浠ヤ綘瀹濆闈㈡澘鐨勫疄闄呯綉绔欒闂棩蹇楄矾寰勪负鍑哴
  74. CC_LOG_PATHS=(
  75.   "/www/wwwlogs/浣犵殑鍩熷悕1.log"
  76.   "/www/wwwlogs/浣犵殑鍩熷悕2.log"
  77.   "/www/wwwlogs/浣犵殑鍩熷悕3.log"
  78.   "/www/wwwlogs/浣犵殑鍩熷悕4.log"
  79.   "/www/wwwlogs/浣犵殑鍩熷悕5.log"
  80. )
  81. # 閿欒鏃ュ織锛坧erip 鍗囩骇绛夌嚎绱級锛氫竴琛屼竴涓枃浠
  82. ERROR_LOG_PATHS=(
  83.   "/www/wwwlogs/浣犵殑鍩熷悕1.error.log"
  84.   "/www/wwwlogs/浣犵殑鍩熷悕2.error.log"
  85.   "/www/wwwlogs/浣犵殑鍩熷悕3.error.log"
  86.   "/www/wwwlogs/浣犵殑鍩熷悕4.error.log"
  87.   "/www/wwwlogs/浣犵殑鍩熷悕5.error.log"
  88. )
  89. # 鏃ュ織 鈫 Host 鐨勫洖閫€鏄犲皠锛堟鍒 鈫 鍩熷悕锛塠浠ヤ綘瀹濆闈㈡澘鐨勫疄闄呯綉绔欒闂棩蹇楄矾寰勪负鍑哴
  90. declare -A LOG_HOST_MAP=(
  91.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕1鍚庣紑\.log$"]="浣犵殑鍩熷悕1"
  92.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕1鍚庣紑\.error\.log$"]="浣犵殑鍩熷悕1"
  93.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕2鍚庣紑\.log$"]="浣犵殑鍩熷悕2"
  94.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕2鍚庣紑\.error\.log$"]="浣犵殑鍩熷悕2"
  95.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕3鍚庣紑\.log$"]="浣犵殑鍩熷悕3"
  96.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕3鍚庣紑\.error\.log$"]="浣犵殑鍩熷悕3"
  97.   ["^/www/wwwlogs/www\.浣犵殑s\.鍩熷悕4鍚庣紑\.log$"]="浣犵殑鍩熷悕4"
  98.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕4鍚庣紑\.error\.log$"]="浣犵殑鍩熷悕4"
  99.   ["^/www/wwwlogs/www\.浣犵殑\.鍩熷悕5鍚庣紑\.log$"]="浣犵殑鍩熷悕5"
  100.   ["^/www/wwwlogs/www\.p浣犵殑\.鍩熷悕5鍚庣紑\.error\.log$"]="浣犵殑鍩熷悕5"
  101. )

  102. # ----------------------------- Redis锛堥檺閫熶笌缂撳瓨锛 ------------------------
  103. ENABLE_RATE_LIMITING=${ENABLE_RATE_LIMITING:-true}        # 鏄惁鍚敤 Redis L7 鍘熷瓙闄愰€
  104. RATELIMIT_TTL=${RATELIMIT_TTL:-600}                       # 璁℃暟 key 鐨 TTL锛堣鏄庢€э級
  105. RATELIMIT_RATE="${RATELIMIT_RATE:-15/minute}"             # 璇存槑鎬ф樉绀
  106. CC_RATELIMIT_THRESHOLD=${CC_RATELIMIT_THRESHOLD:-15}      # 10s 绐楀彛闃堝€硷紙INCR+EXPIRE锛

  107. # 鏃ュ織娓告爣缂撳瓨锛堟樉钁楅檷浣 I/O锛夛細渚濊禆 redis-cli 涓 stat
  108. LOG_CACHE_ENABLE=${LOG_CACHE_ENABLE:-true}
  109. LOG_CACHE_MAX_FALLBACK_LINES=${LOG_CACHE_MAX_FALLBACK_LINES:-20000}

  110. # ----------------------------- 铚樿洓 UA 绛栫暐 --------------------------------
  111. GOOD_BOT_PATTERN='Googlebot|Baiduspider|bingbot|Sogou|360Spider|YisouSpider|YoudaoBot|msnbot|Yahoo! Slurp|YandexBot|DNSPod-Monitor|AspiegelBot'
  112. SEMI_TRUST_BOTS='Bytespider|ToutiaoSpider'
  113. 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'
  114. declare -A GOOD_BOT_RDNS_SUFFIX=(
  115.   ["Googlebot"]="\\.googlebot\\.com$|\\.google\\.com$"
  116.   ["bingbot"]="\\.search\\.msn\\.com$|\\.bing\\.com$"
  117.   ["Baiduspider"]="\\.baidu\\.com$|\\.baiducontent\\.com$"
  118.   ["Sogou"]="\\.sogou\\.com$|\\.sogou\\.com\\.cn$"
  119.   ["360Spider"]="\\.360\\.cn$|\\.haosou\\.com$|\\.so\\.com$"
  120.   ["YisouSpider"]="\\.yisou\\.com$"
  121.   ["YoudaoBot"]="\\.youdao\\.com$"
  122.   ["YandexBot"]="\\.yandex\\.ru$|\\.yandex\\.net$"
  123.   ["Yahoo! Slurp"]="\\.yahoo\\.com$|\\.yahoo\\.net$"
  124.   ["msnbot"]="\\.msn\\.com$|\\.bing\\.com$"
  125.   ["DNSPod-Monitor"]="\\.dnspod\\.cn$|\\.dnspod\\.com$"
  126.   ["AspiegelBot"]="\\.aspiegel\\.com$|\\.petalbot\\.com$"
  127. )

  128. # ----------------------------- 鐧藉悕鍗曟簮 ------------------------------------
  129. IGNORE_IP_FILE="/etc/ddos/ignore.ip.list"             # 涓€琛屼竴涓紙CIDR/鍗旾P锛夛紝鏀寔 # 娉ㄩ噴
  130. IGNORE_HOST_FILE="/etc/ddos/ignore.host.list"         # 涓€琛屼竴涓煙鍚嶏紝瑙f瀽 A/AAAA
  131. BTWAF_IP_WHITE="${BTWAF_IP_WHITE:-/www/server/btwaf/rule/ip_white.json}"          # IPv4 鏁村舰鎴栧尯闂
  132. BTWAF_IP_WHITE_V6="${BTWAF_IP_WHITE_V6:-/www/server/btwaf/rule/ip_white_v6.json}" # IPv6 CIDR/鍗曟

  133. # 鐧藉悕鍗曞憡璀﹁Е鍙戠瓥鐣ワ細浠呮牴鎹潵婧愭枃浠 mtime 鎺ㄩ€侊紙true/false锛
  134. WL_ALERT_BY_MTIME_ONLY=${WL_ALERT_BY_MTIME_ONLY:-true}

  135. # ----------------------------- Host/Path 闃堝€ JSON ------------------------
  136. THRESHOLDS_JSON="${THRESHOLDS_JSON:-/etc/ddos/host-thresholds.json}"        # 妯℃澘鏂囦欢鍙傝€ www.dz-x.net/t/151053/1/1.html

  137. # ----------------------------- 鎵弿棰戠巼锛堝姩鎬侊級 ---------------------------
  138. SCAN_INTERVAL=${SCAN_INTERVAL:-8}               # 姝e父鎬 systemd timer 鍛ㄦ湡锛堢锛
  139. ATTACK_SCAN_INTERVAL=${ATTACK_SCAN_INTERVAL:-2} # 鏀诲嚮鎬 timer 鍛ㄦ湡锛堢锛
  140. BAN_PERIOD=${BAN_PERIOD:-3600}                  # ipset 榛戝悕鍗曡秴鏃讹紙绉掞級

  141. # ----------------------------- ipset 鍚嶇О ---------------------------------
  142. SET_WL_V4="ddos_whitelist_v4"
  143. SET_WL_V6="ddos_whitelist_v6"
  144. SET_BL_V4="ddos_blacklist_v4"
  145. SET_BL_V6="ddos_blacklist_v6"
  146. SET_RL_V4="ddos_ratelimit_v4"
  147. SET_GB_V4="ddos_goodbots_v4"
  148. SET_ADMIN_V4="ddos_admin_v4"
  149. SET_RL_V6="ddos_ratelimit_v6"
  150. SET_GB_V6="ddos_goodbots_v6"

  151. # ----------------------------- 棰滆壊/鐘舵€ ----------------------------------
  152. C_R=$'\033[0;31m'; C_G=$'\033[0;32m'; C_Y=$'\033[0;33m'; C_B=$'\033[0;34m'; C_N=$'\033[0m'
  153. declare -a BANNED_THIS_RUN=()
  154. declare -a LIMITED_THIS_RUN=()

  155. # ----------------------------- 闃堝€肩紦瀛 -----------------------------------
  156. declare -A HT_HOST_WEIGHT HT_REQ_L1 HT_REQ_L2 HT_REQ_L3 HT_SYN_PERIP HT_EST_PERIP
  157. declare -A PT_KIND PT_PAT PT_L1 PT_L2 PT_L3 PT_COUNT
  158. GLOBAL_SYN=${GLOBAL_SYN:-0}; GLOBAL_EST=${GLOBAL_EST:-0}; GLOBAL_CT=${GLOBAL_CT:-0}

  159. # ----------------------------- 鐧藉悕鍗曞彉鏇存娴嬶紙Redis锛 --------------------
  160. WL_REDIS_NS="ddos:wl"               # 鐧藉悕鍗曞懡鍚嶇┖闂
  161. WL_MTIME_SIG_FILE="$RUNDIR/wl.mtime.sig"  # mtime 绛惧悕锛堟湰鍦板揩鐓э級

  162. # ----------------------------- 閫氱敤鍑芥暟 -----------------------------------
  163. log(){ echo "[$($DATE '+%F %T')] [ddos-guard] $*" | tee -a "$LOG_FILE"; }
  164. die(){ echo >&2 "${C_R}Error:${C_N} $*"; exit 1; }
  165. need_root(){ [ "$(id -u)" -eq 0 ] || die "闇€瑕 root 鏉冮檺杩愯銆"; }
  166. have(){ command -v "$1" >/dev/null 2>&1; }

  167. # 閿侊細鎵撳紑 + 闈為樆濉炲皾璇
  168. lock_open(){ exec {LOCK_FD}> "$LOCK_FILE" || die "鏃犳硶鎵撳紑閿佹枃浠讹細$LOCK_FILE"; }
  169. lock_try(){ flock -n "$LOCK_FD"; }            # 鎷夸笉鍒伴攣杩斿洖闈 0
  170. lock_wait(){ flock "$LOCK_FD"; }              # 闃诲绛夊緟

  171. # 鑾峰彇鍏綉 IP (楂樺彲鐢)
  172. get_public_ip(){
  173.   local ip=""
  174.   # 鏂规硶1: 渚濇灏濊瘯澶氫釜鍙潬鐨 HTTP 鏈嶅姟
  175.   # 浣跨敤 --max-time 5 闃叉鎱㈤€熶紶杈撴寕璧
  176.   local http_svcs=("ip.3322.net" "icanhazip.com" "ifconfig.me" "api.ipify.org" "ipinfo.io/ip" "whatismyip.akamai.com")
  177.   if [ -n "$CURL" ]; then
  178.     for svc in "${http_svcs[@]}"; do
  179.       # 浣跨敤 head -n1 纭繚鍙彇绗竴琛岋紝闃叉鏈嶅姟杩斿洖棰濆淇℃伅
  180.       ip=$($CURL -4 -s --connect-timeout 2 --max-time 5 "$svc" | head -n1)
  181.       if is_ipv4 "$ip" || is_ipv6 "$ip"; then
  182.         echo "$ip"
  183.         return 0
  184.       fi
  185.     done
  186.   fi

  187.   # 鏂规硶2: 濡傛灉 HTTP 澶辫触锛岄檷绾у埌 DNS 鏌ヨ (浣跨敤 OpenDNS 鐨勮В鏋愬櫒)
  188.   if [ -n "$HOST" ]; then
  189.     ip=$(host myip.opendns.com resolver1.opendns.com 2>/dev/null | awk '/has address/ {print $NF}')
  190.     if is_ipv4 "$ip"; then # OpenDNS 姝ゆ柟娉曢€氬父鍙繑鍥 IPv4
  191.       echo "$ip"
  192.       return 0
  193.     fi
  194.   fi

  195.   # 濡傛灉鎵€鏈夋柟娉曢兘澶辫触锛岃繑鍥 N/A
  196.   echo "N/A"
  197. }
  198. # ==============================================================================
  199. # 宸ュ叿锛欼P/鍩熷悕鍒ゅ埆涓庤浆鎹
  200. # ==============================================================================
  201. 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)); }
  202. is_ipv6(){ [[ "$1" == *:* ]]; }
  203. 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)); }
  204. is_cidr6(){ [[ "$1" =~ :/.+ ]] && [[ "${1##*/}" =~ ^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$ ]]; }
  205. is_ip_or_cidr(){ is_ipv4 "$1" || is_ipv6 "$1" || is_cidr4 "$1" || is_cidr6 "$1"; }

  206. is_private_ip(){
  207.   local ip="$1"
  208.   if have python3; then
  209.     python3 - <<'PY' "$ip" || exit 1
  210. import ipaddress,sys
  211. ip=sys.argv[1]
  212. try:
  213.   o=ipaddress.ip_address(ip)
  214.   print("1" if (o.is_private or o.is_loopback or o.is_link_local or o.is_reserved) else "0")
  215. except: print("0")
  216. PY
  217.   else
  218.     [[ "$ip" =~ ^10\.|^127\.|^169\.254\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\. ]] && echo 1 || echo 0
  219.   fi
  220. }

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

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

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

  236. # BTWAF IPv4 鏁村舰/鍖洪棿 鈫 CIDR/鍗 IP
  237. parse_btwaf_ipv4_json(){
  238.   local json="$1"; [ -f "$json" ] || return 0
  239.   [ -n "$PY3" ] || { log "璺宠繃 BTWAF IPv4锛氭湭瀹夎 python3"; return 0; }
  240.   python3 - "$json" <<'PY' || exit 1
  241. import sys,json,ipaddress
  242. from math import log2, floor
  243. def range_to_cidrs(a,b):
  244.     res=[]; cur=a
  245.     while cur<=b:
  246.         max_size = 32 - int(floor(log2((cur & -cur))))
  247.         max_allowed = 32 - int(floor(log2(b - cur + 1)))
  248.         mask = max(max_size, max_allowed)
  249.         res.append(f"{str(ipaddress.IPv4Address(cur))}/{mask}")
  250.         cur += (1 << (32-mask))
  251.     return res
  252. p=sys.argv[1]
  253. data=json.load(open(p,'r',encoding='utf-8'))
  254. out=[]
  255. for it in data:
  256.     if isinstance(it,list) and len(it)==2:
  257.         a,b=int(it[0]),int(it[1])
  258.         if a==b: out.append(str(ipaddress.IPv4Address(a)))
  259.         else:    out.extend(range_to_cidrs(a,b))
  260.     elif isinstance(it,int):
  261.         out.append(str(ipaddress.IPv4Address(it)))
  262. for line in out: print(line)
  263. PY
  264. }

  265. # BTWAF IPv6 鐩存帴璇诲彇锛堝瓧绗︿覆鎴栦竴缁 list锛
  266. parse_btwaf_ipv6_json(){
  267.   local json="$1"; [ -f "$json" ] || return 0
  268.   [ -n "$PY3" ] || { log "璺宠繃 BTWAF IPv6锛氭湭瀹夎 python3"; return 0; }
  269.   python3 - "$json" <<'PY' || exit 1
  270. import sys,json
  271. data=json.load(open(sys.argv[1],'r',encoding='utf-8'))
  272. for it in data:
  273.     if isinstance(it,list) and it:
  274.         s=str(it[0]).strip()
  275.         if s: print(s)
  276.     elif isinstance(it,str):
  277.         s=it.strip()
  278.         if s: print(s)
  279. PY
  280. }

  281. # 鏈€杩 UA 鑾峰彇 + 鐪熷亣铚樿洓鏍¢獙
  282. recent_ua_by_ip(){
  283.   local ip="$1" n="${2:-2000}" f ua
  284.   for f in "${CC_LOG_PATHS[@]}"; do
  285.     [ -f "$f" ] || continue
  286.     ua=$($TAIL -n "$n" "$f" | $GREP -F " $ip " | awk -F" '{print $(NF-1)}' | tail -n 1)
  287.     [ -n "$ua" ] && { echo "$ua"; return 0; }
  288.   done
  289.   echo ""
  290. }
  291. rdns_ptr(){ [ -n "$HOST" ] || { echo ""; return; }; host -W1 "$1" 2>/dev/null | awk '/pointer/ {gsub(/\.$/,"",$NF); print $NF; exit}'; }
  292. 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; }
  293. 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 ""; }
  294. classify_ua_for_ip(){
  295.   local ip="$1"; local ua; ua="$(recent_ua_by_ip "$ip")"
  296.   [ -z "$ua" ] && { echo "OTHER"; return; }
  297.   if echo "$ua" | $GREP -Eiq -- "$GOOD_BOT_PATTERN"; then
  298.     local key; key=$(good_bot_key_from_ua "$ua")
  299.     if [ -n "$key" ]; then
  300.       local ptr; ptr="$(rdns_ptr "$ip")"
  301.       if [ -n "$ptr" ] && echo "$ptr" | $GREP -Eiq -- "${GOOD_BOT_RDNS_SUFFIX[$key]}"; then
  302.         [ "$(forward_confirms "$ptr" "$ip")" = "1" ] && echo "GOOD" || echo "FAKEGOOD"
  303.       else echo "FAKEGOOD"; fi
  304.       return
  305.     fi
  306.   fi
  307.   echo "$ua" | $GREP -Eiq -- "$SEMI_TRUST_BOTS" && { echo "SEMI"; return; }
  308.   echo "$ua" | $GREP -Eiq -- "$BAD_BOT_PATTERN" && { echo "BAD"; return; }
  309.   echo "OTHER"
  310. }

  311. # ==============================================================================
  312. # ipset/iptables 鍒濆鍖
  313. # ==============================================================================
  314. ensure_ipset_and_iptables(){
  315.   have ipset || die "缂哄皯 ipset"
  316.   have iptables || die "缂哄皯 iptables"

  317.   $IPSET create "$SET_WL_V4" hash:net -exist maxelem 524288
  318.   $IPSET create "$SET_WL_V6" hash:net -exist family inet6 maxelem 262144
  319.   $IPSET create "$SET_BL_V4" hash:ip  -exist maxelem 1048576 timeout "$BAN_PERIOD"
  320.   $IPSET create "$SET_BL_V6" hash:ip  -exist family inet6 maxelem 524288 timeout "$BAN_PERIOD"
  321.   $IPSET create "$SET_RL_V4" hash:ip  -exist maxelem 524288 timeout "$BAN_PERIOD"
  322.   $IPSET create "$SET_GB_V4" hash:ip  -exist maxelem 131072
  323.   $IPSET create "$SET_ADMIN_V4" hash:ip  -exist maxelem 1024 timeout 3600
  324.   $IPSET create "$SET_RL_V6" hash:ip  -exist family inet6 maxelem 524288 timeout "$BAN_PERIOD"
  325.   $IPSET create "$SET_GB_V6" hash:ip  -exist family inet6 maxelem 131072

  326.   $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
  327.   $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
  328.   $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

  329.   if have ip6tables; then
  330.     $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
  331.     $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
  332.   fi
  333. }

  334. # ==============================================================================
  335. # BTWAF 璺緞鎺㈡祴
  336. # ==============================================================================
  337. autodetect_btwaf_paths(){
  338.   if [ ! -r "$BTWAF_IP_WHITE" ] || [ ! -s "$BTWAF_IP_WHITE" ]; then
  339.     local alt="/www/server/panel/plugin/btwaf/rule/ip_white.json"; [ -r "$alt" ] && BTWAF_IP_WHITE="$alt"
  340.   fi
  341.   if [ ! -r "$BTWAF_IP_WHITE_V6" ] || [ ! -s "$BTWAF_IP_WHITE_V6" ]; then
  342.     local alt6="/www/server/panel/plugin/btwaf/rule/ip_white_v6.json"; [ -r "$alt6" ] && BTWAF_IP_WHITE_V6="$alt6"
  343.   fi
  344. }

  345. # ==============================================================================
  346. # systemd timer 闂撮殧鍦ㄧ嚎璋冩暣
  347. # ==============================================================================
  348. timer_unit="/etc/systemd/system/ddos-guard.timer"
  349. get_current_interval(){ awk -F= '/^OnUnitActiveSec=/{print $2}' "$timer_unit" 2>/dev/null | tail -n1; }
  350. set_timer_interval(){
  351.   local new="$1"
  352.   [ -f "$timer_unit" ] || return 0
  353.   sed -i "s/^OnUnitActiveSec=.*/OnUnitActiveSec=${new}s/" "$timer_unit"
  354.   systemctl daemon-reload
  355.   systemctl try-restart ddos-guard.timer >/dev/null 2>&1 || systemctl restart ddos-guard.timer >/dev/null 2>&1
  356.   echo "$new" > "$RUNDIR/scan-interval.current" || true
  357. }
  358. attack_state_file="$RUNDIR/attack.state" # 鍐呭锛0/1

  359. # ==============================================================================
  360. # 鐧藉悕鍗曡仛鍚/鍔犺浇 + 鍙樻洿妫€娴/鑷剤 + mtime 鍒ゅ畾鍛婅
  361. # ==============================================================================
  362. WL_SNAPSHOT_V4="$RUNDIR/wl.v4.txt"
  363. WL_SNAPSHOT_V6="$RUNDIR/wl.v6.txt"

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

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

  374. escape_json(){ echo -n "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1])'; }
  375. should_throttle(){
  376.   local tag="$1"; local tsf="$ALERT_TS_DIR/${tag}.ts"; local now; now=$(date +%s)
  377.   if [ -f "$tsf" ]; then local last; last=$(cat "$tsf" 2>/dev/null || echo 0); (( now - last < DINGTALK_THROTTLE_SEC )) && return 0; fi
  378.   echo "$now" > "$tsf"; return 1
  379. }
  380. alert_markdown(){
  381.   [ -n "$DINGTALK_WEBHOOK" ] || return 0
  382.   local tag="$1" title="$2" text="$3"
  383.   if should_throttle "$tag"; then return 0; fi
  384.   local payload
  385.   payload=$(cat <<JSON
  386. {"msgtype":"markdown","markdown":{"title":"${title}","text":"$(escape_json "$text")"}}
  387. JSON
  388. )
  389.   $CURL -s -H 'Content-Type: application/json' -X POST -d "$payload" "$DINGTALK_WEBHOOK" >/dev/null 2>&1 || true
  390. }

  391. load_whitelist(){
  392.   set +e
  393.   ensure_ipset_and_iptables
  394.   autodetect_btwaf_paths

  395.   log "寮€濮嬮噸杞界櫧鍚嶅崟锛圔TWAF + ignore.ip + ignore.host锛 ..."
  396.   log "BTWAF IPv4锛$BTWAF_IP_WHITE"
  397.   log "BTWAF IPv6锛$BTWAF_IP_WHITE_V6"

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

  400.   local cnt_src_ignore_ip4=0 cnt_src_ignore_ip6=0
  401.   local cnt_src_ignore_host_in=0 cnt_src_ignore_host_ok4=0 cnt_src_ignore_host_ok6=0
  402.   local cnt_src_btwaf4=0 cnt_src_btwaf6=0

  403.   # 1) ignore.ip.list锛氳В鏋愬苟鍖哄垎 v4/v6
  404.   if [ -r "$IGNORE_IP_FILE" ]; then
  405.     while IFS= read -r raw; do
  406.       line="${raw%%#*}"; line="${line//$'\r'/}"; line="$(echo "$line" | xargs || true)"
  407.       [ -n "$line" ] || continue
  408.       fam=""
  409.       if [ -n "$PY3" ]; then
  410.         fam="$(
  411. python3 - <<'PY' "$line" 2>/dev/null || true
  412. import sys, ipaddress
  413. s=sys.argv[1]
  414. try:
  415.   n=ipaddress.ip_network(s, strict=False); print(6 if n.version==6 else 4)
  416. except:
  417.   try:
  418.     a=ipaddress.ip_address(s); print(6 if a.version==6 else 4)
  419.   except:
  420.     pass
  421. PY
  422.         )"
  423.       fi
  424.       if [ "$fam" = "6" ]; then echo "$line" >> "$tmp6"; ((cnt_src_ignore_ip6++))
  425.       elif [ "$fam" = "4" ]; then echo "$line" >> "$tmp4"; ((cnt_src_ignore_ip4++))
  426.       else
  427.         if is_cidr6 "$line" || is_ipv6 "$line"; then echo "$line" >> "$tmp6"; ((cnt_src_ignore_ip6++))
  428.         elif is_cidr4 "$line" || is_ipv4 "$line"; then echo "$line" >> "$tmp4"; ((cnt_src_ignore_ip4++))
  429.         else log "蹇界暐闈炴硶鐧藉悕鍗曡锛$line"
  430.         fi
  431.       fi
  432.     done < "$IGNORE_IP_FILE"
  433.   fi
  434.   log "ignore.ip.list 璇诲叆锛欼Pv4=$cnt_src_ignore_ip4 IPv6=$cnt_src_ignore_ip6"

  435.   # 2) ignore.host.list锛氳В鏋 A/AAAA
  436.   if [ -r "$IGNORE_HOST_FILE" ]; then
  437.     while IFS= read -r rawh; do
  438.       h="${rawh%%#*}"; h="${h//$'\r'/}"; h="$(echo "$h" | xargs || true)"
  439.       [ -n "$h" ] || continue
  440.       ((cnt_src_ignore_host_in++))
  441.       while read -r ip; do
  442.         [ -z "$ip" ] && continue
  443.         if is_ipv6 "$ip"; then echo "$ip" >> "$tmp6"; ((cnt_src_ignore_host_ok6++))
  444.         else echo "$ip" >> "$tmp4"; ((cnt_src_ignore_host_ok4++))
  445.         fi
  446.       done < <(resolve_host_to_ips "$h")
  447.     done < "$IGNORE_HOST_FILE"
  448.   fi
  449.   log "ignore.host.list 鍩熷悕鏉$洰=$cnt_src_ignore_host_in 鈫 瑙f瀽鎴愬姛 IPv4=$cnt_src_ignore_host_ok4 IPv6=$cnt_src_ignore_host_ok6"

  450.   # 3) BTWAF IPv4
  451.   if [ -r "$BTWAF_IP_WHITE" ] && [ -n "$PY3" ]; then
  452.     local before=$(wc -l < "$tmp4" 2>/dev/null || echo 0)
  453.     parse_btwaf_ipv4_json "$BTWAF_IP_WHITE" >> "$tmp4"
  454.     local after=$(wc -l < "$tmp4" 2>/dev/null || echo 0)
  455.     cnt_src_btwaf4=$(( after - before ))
  456.   fi
  457.   # 4) BTWAF IPv6
  458.   if [ -r "$BTWAF_IP_WHITE_V6" ] && [ -n "$PY3" ]; then
  459.     local before6=$(wc -l < "$tmp6" 2>/dev/null || echo 0)
  460.     parse_btwaf_ipv6_json "$BTWAF_IP_WHITE_V6" >> "$tmp6"
  461.     local after6=$(wc -l < "$tmp6" 2>/dev/null || echo 0)
  462.     cnt_src_btwaf6=$(( after6 - before6 ))
  463.   fi

  464.   # 5) 鍘婚噸 + 鏍¢獙
  465.   sort -u "$tmp4" -o "$tmp4"; sort -u "$tmp6" -o "$tmp6"
  466.   local tmp4v="$TMPDIR/wl4.valid"; local tmp6v="$TMPDIR/wl6.valid"
  467.   : > "$tmp4v"; : > "$tmp6v"
  468.   local v4=0 v6=0
  469.   while read -r x; do is_ip_or_cidr "$x" && { echo "$x"; ((v4++)); }; done < "$tmp4" > "$tmp4v"
  470.   while read -r x; do is_ip_or_cidr "$x" && { echo "$x"; ((v6++)); }; done < "$tmp6" > "$tmp6v"

  471.   log "鑱氬悎璁℃暟锛歩gnore.ip 鈫 v4=$cnt_src_ignore_ip4 v6=$cnt_src_ignore_ip6锛沬gnore.host 鎴愬姛 鈫 v4=$cnt_src_ignore_host_ok4 v6=$cnt_src_ignore_host_ok6锛汢TWAF 鈫 v4=$cnt_src_btwaf4 v6=$cnt_src_btwaf6锛涙牎楠岄€氳繃 鈫 v4=$v4 v6=$v6"

  472.   # 6) 鎵归噺娉ㄥ叆 ipset
  473.   {
  474.     echo "flush $SET_WL_V4"
  475.     while read -r net; do [ -n "$net" ] && echo "add $SET_WL_V4 $net"; done < "$tmp4v"
  476.     echo "flush $SET_WL_V6"
  477.     while read -r net; do [ -n "$net" ] && echo "add $SET_WL_V6 $net"; done < "$tmp6v"
  478.   } | $IPSET restore -exist >/dev/null 2>&1 || log "WARN: ipset restore 杩斿洖闈為浂锛堝凡蹇界暐锛"

  479.   # 7) 淇濆瓨蹇収
  480.   ipset_members "$SET_WL_V4" > "$WL_SNAPSHOT_V4" || true
  481.   ipset_members "$SET_WL_V6" > "$WL_SNAPSHOT_V6" || true
  482.   local final4=$(wc -l < "$WL_SNAPSHOT_V4" 2>/dev/null || echo 0)
  483.   local final6=$(wc -l < "$WL_SNAPSHOT_V6" 2>/dev/null || echo 0)

  484.   # 7.1 璁板綍鏈夋晥鑱氬悎鍝堝笇锛堜粎鐢ㄤ簬鑷剤鍒ゅ畾锛屼笉瑙﹀彂鍛婅锛
  485.   local h4="" h6=""
  486.   if [ -n "$SHA1" ]; then
  487.     h4=$(cat "$tmp4v" 2>/dev/null | $SHA1 | awk '{print $1}')
  488.     h6=$(cat "$tmp6v" 2>/dev/null | $SHA1 | awk '{print $1}')
  489.   else
  490.     h4="$(wc -l < "$tmp4v" 2>/dev/null)-$(head -n1 "$tmp4v" 2>/dev/null)"
  491.     h6="$(wc -l < "$tmp6v" 2>/dev/null)-$(head -n1 "$tmp6v" 2>/dev/null)"
  492.   fi
  493.   local p4="$(redis_get "$WL_REDIS_NS:hash:v4")"
  494.   local p6="$(redis_get "$WL_REDIS_NS:hash:v6")"
  495.   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"
  496.   redis_set "$WL_REDIS_NS:hash:v4" "$h4"
  497.   redis_set "$WL_REDIS_NS:hash:v6" "$h6"
  498.   redis_set "$WL_REDIS_NS:srcstat" "$src_stat"
  499.   echo "$src_stat" > "$RUNDIR/wl.srcstat" || true

  500.   # 7.2 鍙樻洿 鈫 鑷剤锛堟棤鎺ㄩ€侊級
  501.   if [ "$h4" != "$p4" ] || [ "$h6" != "$p6" ]; then
  502.     reconcile_unban_whitelisted
  503.   fi

  504.   # 7.3 鏉ユ簮鏂囦欢 mtime 鍙樺寲鎵嶆帹閫
  505.   local cur_msig; cur_msig="$(compute_wl_mtime_sig)"
  506.   local prev_msig=""
  507.   [ -f "$WL_MTIME_SIG_FILE" ] && prev_msig="$(cat "$WL_MTIME_SIG_FILE" 2>/dev/null || echo "")"
  508.   echo "$cur_msig" > "$WL_MTIME_SIG_FILE"

  509.   if [ "${WL_ALERT_BY_MTIME_ONLY}" = true ]; then
  510.     if [ "$cur_msig" != "$prev_msig" ]; then
  511.       local unb="$(cat "$RUNDIR/last_unban_wl.count" 2>/dev/null || echo 0)"
  512.       local wl_text="### 馃Ь 鐧藉悕鍗曟洿鏂帮紙鏉ユ簮鏂囦欢鍙樻洿锛
  513. - **鏃堕棿**锛$($DATE '+%F %T')
  514. - **鏉ユ簮缁熻**锛$src_stat
  515. - **褰撳墠蹇収**锛欼Pv4=${final4} 鏉★紝IPv6=${final6} 鏉
  516. - **鏈疆鑷剤瑙e皝**锛${unb} 涓"
  517.       alert_markdown "wl-change" "鐧藉悕鍗曟洿鏂" "$wl_text"
  518.     fi
  519.   else
  520.     # 涓嶄粎鎸 mtime锛屽搱甯屽彉鍖栦篃鎺ㄩ€侊紙鍙€夛級
  521.     if [ "$h4" != "$p4" ] || [ "$h6" != "$p6" ]; then
  522.       local unb="$(cat "$RUNDIR/last_unban_wl.count" 2>/dev/null || echo 0)"
  523.       local wl_text="### 馃Ь 鐧藉悕鍗曟洿鏂帮紙鑱氬悎鍐呭鍙樺寲锛
  524. - **鏃堕棿**锛$($DATE '+%F %T')
  525. - **鏉ユ簮缁熻**锛$src_stat
  526. - **褰撳墠蹇収**锛欼Pv4=${final4} 鏉★紝IPv6=${final6} 鏉
  527. - **鏈疆鑷剤瑙e皝**锛${unb} 涓"
  528.       alert_markdown "wl-change" "鐧藉悕鍗曟洿鏂" "$wl_text"
  529.     fi
  530.   fi

  531.   log "鐧藉悕鍗曢噸杞藉畬鎴愶細IPv4 $final4 鏉★紝IPv6 $final6 鏉°€"
  532.   set -e
  533. }

  534. show_whitelist(){
  535.   ensure_ipset_and_iptables
  536.   echo "=== IPv4 whitelist ($SET_WL_V4) ==="; ipset_members "$SET_WL_V4" | head -n 200
  537.   echo "=== IPv6 whitelist ($SET_WL_V6) ==="; ipset_members "$SET_WL_V6" | head -n 200
  538.   echo "(浠呭睍绀哄墠 200 鏉★紱瀹屾暣璇蜂娇鐢細ipset list $SET_WL_V4 / $SET_WL_V6)"
  539. }

  540. is_in_whitelist_snapshot(){
  541.   local ip="$1"
  542.   if is_ipv6 "$ip"; then
  543.     grep -Fxq -- "$ip" "$WL_SNAPSHOT_V6" 2>/dev/null && return 0
  544.   else
  545.     $IPSET test "$SET_WL_V4" "$ip" >/dev/null 2>&1 && return 0
  546.     grep -Fxq -- "$ip" "$WL_SNAPSHOT_V4" 2>/dev/null && return 0
  547.   fi
  548.   return 1
  549. }

  550. reconcile_unban_whitelisted(){
  551.   local tmp="$TMPDIR/reconcile.$$" cnt=0
  552.   ipset_members "$SET_BL_V4" > "$tmp" || true
  553.   while read -r ip; do
  554.     [ -n "$ip" ] || continue
  555.     if is_in_whitelist_snapshot "$ip"; then
  556.       unban_ip "$ip" "auto-unban-wl" && ((cnt++))
  557.     fi
  558.   done < "$tmp"
  559.   if have ip6tables; then
  560.     ipset_members "$SET_BL_V6" > "$tmp" || true
  561.     while read -r ip; do
  562.       [ -n "$ip" ] || continue
  563.       if is_in_whitelist_snapshot "$ip"; then
  564.         unban_ip "$ip" "auto-unban-wl" && ((cnt++))
  565.       fi
  566.     done < "$tmp"
  567.   fi
  568.   echo "$cnt" > "$RUNDIR/last_unban_wl.count"
  569. }

  570. # ==============================================================================
  571. # L4 瀹炴椂鎺掕锛堜慨澶嶇増锛
  572. # ==============================================================================
  573. extract_peer_ip_awk='
  574. function peerip(s,    x){
  575.   # [2001:db8::1]:443 / 1.2.3.4:54321 / 2001:db8::1
  576.   if (s ~ /^\[/) { gsub(/^\[/,"",s); sub(/\].*$/,"",s); return s; }
  577.   sub(/:[0-9]+$/,"",s);
  578.   return s;
  579. }
  580. { ip=peerip($NF); if (ip!="" && ip!="-") print ip; }
  581. '
  582. top_talkers_l4(){
  583.   have ss || die "缂哄皯 ss"
  584.   local est syn
  585.   est=$($SS -Hnta state established 2>/dev/null | awk "$extract_peer_ip_awk" | sort | uniq -c | sort -nr | head -n 20)
  586.   syn=$($SS -Hnta state syn-recv    2>/dev/null | awk "$extract_peer_ip_awk" | sort | uniq -c | sort -nr | head -n 20)
  587.   echo "---- Top EST ----"; [ -n "$est" ] && echo "$est" || echo "(鏃犳椿鍔ㄨ繛鎺)"
  588.   echo "---- Top SYN_RECV ----"; [ -n "$syn" ] && echo "$syn" || echo "(鏃犳椿鍔ㄨ繛鎺)"
  589. }

  590. should_attack_mode(){
  591.   local ct_count=0; have conntrack && ct_count=$(conntrack -C 2>/dev/null || echo 0)
  592.   local syn_total est_total
  593.   syn_total=$($SS -Hnta state syn-recv    | awk '{c++} END{print c+0}')
  594.   est_total=$($SS -Hnta state established | awk '{c++} END{print c+0}')

  595.   local result=0
  596.   if (( ct_count > NO_OF_CT || syn_total > NO_OF_SYN || est_total > NO_OF_EST )); then
  597.     result=1
  598.   fi

  599.   # 绗竴琛岃緭鍑虹粨鏋 1 鎴 0
  600.   echo "$result"
  601.   # 鍚庣画琛岃緭鍑鸿缁嗙殑鎸囨爣锛岀敤浜庡憡璀
  602.   echo "Conntrack: ${ct_count} / ${NO_OF_CT}"
  603.   echo "SYN Total: ${syn_total} / ${NO_OF_SYN}"
  604.   echo "EST Total: ${est_total} / ${NO_OF_EST}"
  605. }

  606. # ==============================================================================
  607. # Host/Path 闃堝€硷紙澶栭儴 JSON 浼樺厛锛涘惁鍒欏唴缃 Discuz PATH_TIERS锛
  608. # ==============================================================================
  609. DEFAULT_THRESHOLDS_JSON='{
  610.   "GLOBAL": { "SYN_GLOBAL_THRESHOLD": 180, "EST_GLOBAL_THRESHOLD": 260, "CONNTRACK_GLOBAL_THRESHOLD": 15000 },
  611.   "*": {
  612.     "HOST_WEIGHT": 2.0, "REQ_PER_MIN_L1": 160, "REQ_PER_MIN_L2": 300, "REQ_PER_MIN_L3": 600,
  613.     "CONN_PER_IP_SYN_THRESHOLD": 68, "CONN_PER_IP_EST_THRESHOLD": 120,
  614.     "PATH_TIERS": [
  615.       { "kind": "prefix", "pattern": "/forum.php?mod=image",            "L1": 10, "L2": 20,  "L3": 40  },
  616.       { "kind": "regex",  "pattern": "^/plugin\\.php\\?id=aljol(&|$)",  "L1":  6, "L2": 12,  "L3": 24  },
  617.       { "kind": "regex",  "pattern": "^/ajax\\.php(\\?|$)",             "L1":  8, "L2": 16,  "L3": 32  },
  618.       { "kind": "regex",  "pattern": "^/avatar\\.php(\\?|$)",             "L1":  8, "L2": 16,  "L3": 32  },
  619.       { "kind": "regex",  "pattern": "^/misc\\.php\\?mod=seccode(&|$)","L1":  4, "L2":  8,  "L3": 16  },
  620.       { "kind": "regex",  "pattern": "^/search\\.php(\\?|$)",           "L1":  8, "L2": 16,  "L3": 32  },
  621.       { "kind": "prefix", "pattern": "/data/attachment/",                "L1": 30, "L2": 60,  "L3": 120 },
  622.       { "kind": "prefix", "pattern": "/static/",                         "L1": 40, "L2": 80,  "L3": 160 }
  623.     ]
  624.   },
  625.   "浣犵殑鍩熷悕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 },
  626.   "浣犵殑鍩熷悕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 },
  627.   "浣犵殑鍩熷悕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 },
  628.   "浣犵殑鍩熷悕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 },
  629.   "浣犵殑鍩熷悕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 }
  630. }'

  631. load_host_thresholds(){
  632.   local src="$THRESHOLDS_JSON" json
  633.   if [ -s "$src" ]; then json="$(cat "$src")"; log "宸插姞杞藉閮 host-thresholds.json锛$src"
  634.   else json="$DEFAULT_THRESHOLDS_JSON"; log "浣跨敤鑴氭湰鍐呯疆榛樿闃堝€硷紙鍥哄寲閰嶇疆 + Discuz PATH_TIERS锛"; fi
  635.   [ -n "$PY3" ] || { log "鏈娴嬪埌 python3锛屾棤娉曡В鏋 host/path 闃堝€ JSON"; return 0; }

  636.   local out
  637.   out="$(python3 - <<'PY' "$json" || exit 1
  638. import sys,json
  639. cfg=json.loads(sys.argv[1])

  640. # 澧炲己鐨 g() 鍑芥暟锛屽彲浠ユ鏌ュ鐢ㄩ敭鍚
  641. def g(k, d=0, alt_k=None):
  642.     glob = cfg.get("GLOBAL", {})
  643.     v = glob.get(k)
  644.     if v is None and alt_k:
  645.         v = glob.get(alt_k)
  646.     return v if isinstance(v, (int, float)) else d

  647. # 鍏煎 GLOBAL 鍖哄煙鐨勬柊鏃т袱绉嶉敭鍚
  648. print("GLOBAL_SYN=%s" % g("SYN_GLOBAL_THRESHOLD", 0, alt_k="NO_OF_SYN"))
  649. print("GLOBAL_EST=%s" % g("EST_GLOBAL_THRESHOLD", 0, alt_k="NO_OF_EST"))
  650. print("GLOBAL_CT=%s" % g("CONNTRACK_GLOBAL_THRESHOLD", 0, alt_k="NO_OF_CONNTRACK"))

  651. for host,val in cfg.items():
  652.   if host=="GLOBAL" or not isinstance(val,dict): continue
  653.   
  654.   # 鍏煎宓屽鐨 REQ_PER_MIN 瀵硅薄鍜岀嫭绔嬬殑 REQ_PER_MIN_L1/L2/L3 閿
  655.   req_min_obj = val.get("REQ_PER_MIN", {})
  656.   hw = val.get("HOST_WEIGHT",1.0)
  657.   l1 = val.get("REQ_PER_MIN_L1") or req_min_obj.get("L1") or 0
  658.   l2 = val.get("REQ_PER_MIN_L2") or req_min_obj.get("L2") or 0
  659.   l3 = val.get("REQ_PER_MIN_L3") or req_min_obj.get("L3") or 0
  660.   
  661.   # 鍏煎 SYN_PER_IP 鍜 EST_PER_IP 閿悕
  662.   syn = val.get("CONN_PER_IP_SYN_THRESHOLD") or val.get("SYN_PER_IP") or 0
  663.   est = val.get("CONN_PER_IP_EST_THRESHOLD") or val.get("EST_PER_IP") or 0
  664.   
  665.   print("H|%s|%s|%s|%s|%s|%s"%(host,hw,l1,l2,l3,syn or 0)); print("E|%s|%s"%(host,est or 0))
  666.   
  667.   pts=val.get("PATH_TIERS",[])
  668.   if isinstance(pts,list):
  669.     for i,r in enumerate(pts):
  670.       # 鍏煎 KIND/PAT (澶у啓) 鍜 kind/pattern (灏忓啓)
  671.       kind_val = r.get("kind") or r.get("KIND") or "regex"
  672.       kind = str(kind_val).lower()
  673.       pat = str(r.get("pattern") or r.get("PAT") or "").strip()
  674.       
  675.       L1=int(r.get("L1",0) or 0); L2=int(r.get("L2",0) or 0); L3=int(r.get("L3",0) or 0)
  676.       if not pat: continue
  677.       if kind not in ("regex","prefix"): kind="regex"
  678.       print("P|%s|%d|%s|%s|%d|%d|%d"%(host,i,kind,pat.replace("|","\\|"),L1,L2,L3))
  679.   print("PCNT|%s|%d"%(host,len(pts)))
  680. PY
  681. )"
  682.   HT_HOST_WEIGHT=(); HT_REQ_L1=(); HT_REQ_L2=(); HT_REQ_L3=(); HT_SYN_PERIP=(); HT_EST_PERIP=()
  683.   PT_KIND=(); PT_PAT=(); PT_L1=(); PT_L2=(); PT_L3=(); PT_COUNT=()

  684.   while IFS= read -r line; do
  685.     case "$line" in
  686.       GLOBAL_SYN=*) GLOBAL_SYN="${line#GLOBAL_SYN=}" ;;
  687.       GLOBAL_EST=*) GLOBAL_EST="${line#GLOBAL_EST=}" ;;
  688.       GLOBAL_CT=*)  GLOBAL_CT="${line#GLOBAL_CT=}" ;;
  689.       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" ;;
  690.       E|* ) IFS='|' read -r _ h est <<<"$line"; HT_EST_PERIP["$h"]="$est" ;;
  691.       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" ;;
  692.       PCNT|* ) IFS='|' read -r _ h pc <<<"$line"; PT_COUNT["$h"]="$pc" ;;
  693.     esac
  694.   done <<< "$out"

  695.   # GLOBAL 瑕嗙洊鍏滃簳
  696.   [ "$GLOBAL_SYN" -gt 0 ] && NO_OF_SYN="$GLOBAL_SYN"
  697.   [ "$GLOBAL_EST" -gt 0 ] && NO_OF_EST="$GLOBAL_EST"
  698.   [ "$GLOBAL_CT"  -gt 0 ] && NO_OF_CT="$GLOBAL_CT"
  699.   log "闃堝€艰杞斤細GLOBAL SYN=$NO_OF_SYN EST=$NO_OF_EST CT=$NO_OF_CT锛涚珯鐐规暟=$(printf %s "${!HT_HOST_WEIGHT[@]}" | wc -w)"
  700. }

  701. # ==============================================================================
  702. # Redis 鏃ュ織娓告爣缂撳瓨锛堥檷 I/O锛
  703. # ==============================================================================
  704. stream_new_lines(){
  705.   local f="$1"; [ -f "$f" ] || return 0
  706.   if [ "$LOG_CACHE_ENABLE" = true ] && [ -n "$REDIS_CLI" ] && [ -n "$STAT" ]; then
  707.     local key="ddos:logpos:$(echo -n "$f" | ${MD5:-md5sum} | awk '{print $1}')"
  708.     local size; size=$($STAT -c %s "$f" 2>/dev/null || echo 0)
  709.     local pos; pos=$($REDIS_CLI GET "$key" 2>/dev/null || echo "")
  710.     if [[ -z "$pos" || "$pos" -gt "$size" ]]; then
  711.       $TAIL -n "$LOG_CACHE_MAX_FALLBACK_LINES" "$f"
  712.       $REDIS_CLI SETEX "$key" 86400 "$size" >/dev/null 2>&1 || true
  713.     else
  714.       local start=$(( pos + 1 ))
  715.       tail -c +$start "$f" 2>/dev/null || $TAIL -n "$LOG_CACHE_MAX_FALLBACK_LINES" "$f"
  716.       $REDIS_CLI SETEX "$key" 86400 "$size" >/dev/null 2>&1 || true
  717.     fi
  718.   else
  719.     $TAIL -n "$LOG_CACHE_MAX_FALLBACK_LINES" "$f"
  720.   fi
  721. }

  722. # ==============================================================================
  723. # L7 缁熻/澶勭疆 + Redis 闄愰€熷喅绛
  724. # ==============================================================================
  725. count_hits_in_logs(){
  726.   local tmp="$TMPDIR/l7_hits.$$"; : > "$tmp"
  727.   for f in "${CC_LOG_PATHS[@]}"; do
  728.     [ -f "$f" ] || continue
  729.     local fb; fb="$(guess_host_from_file "$f")"
  730.     stream_new_lines "$f" | $AWK -v fb="$fb" '
  731.       function g(line,   h) {
  732.         if (match(line, /"https?:\/\/([^\/"]+)/, a)) return a[1];
  733.         if (match(line, /"[^"]+ (https?:\/\/([^\/ ]+))?\/[^"]*"/, b)) { if (b[2]!="") return b[2]; }
  734.         return fb;
  735.       }
  736.       {
  737.         ip=$1; line=$0; path="/"
  738.         if (match($0, /"[^"]+"/, a)) {
  739.         req=a[0]
  740.         status_code=0
  741.         # 灏濊瘯浠庢棩蹇楄涓彁鍙 HTTP 鐘舵€佺爜 (渚嬪 " 200 ")
  742.         if (match($0, /"[^"]+" ([0-9]{3})/, s)) status_code=s[1]

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

  744.         # Discuz! 鍚庡彴璺緞鍔ㄦ€佽瘑鍒€昏緫
  745.         # 濡傛灉璁块棶鐨勬槸鍚庡彴绠$悊鍜屼笅杞絛ismall鎻掍欢鎿嶄綔鍔犵櫧鍚嶅崟闃叉璇潃鑷繁IP锛屽苟涓旂姸鎬佺爜鏄200 (琛ㄧず鎴愬姛)
  746.         if (path ~ /^/(dismall|admin|plugin)\.php$/ && status_code == 200) {
  747.             # 杈撳嚭涓€涓壒娈婃爣璁 ADMIN_LOGIN 鍜屽搴旂殑 IP
  748.             print "ADMIN_LOGIN", ip, g(line)
  749.         } else {
  750.             # 瀵逛簬鍏朵粬鎵€鏈夎闂紝杈撳嚭甯歌鏍囪 NORMAL_HIT
  751.             print "NORMAL_HIT", ip, g(line), path
  752.         }
  753.       }
  754.       }' >> "$tmp"
  755.   done
  756.   $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
  757. }

  758. perip_escalation_from_errorlog(){
  759.   local tmp="$TMPDIR/perip.$$"; : > "$tmp"
  760.   for f in "${ERROR_LOG_PATHS[@]}"; do
  761.     [ -f "$f" ] || continue
  762.     local fb; fb="$(guess_host_from_file "$f")"
  763.     stream_new_lines "$f" | $AWK -v fb="$fb" '
  764.       /limiting connections by zone "perip"/ {
  765.         ip="";host="";
  766.         if (match($0, /client: ([0-9.]+)/, a)) ip=a[1];
  767.         if (match($0, /server: ([^, ]+)/, b)) host=b[1];
  768.         if (ip!="") {print ip, (host==""?fb:host)}
  769.       }' >> "$tmp"
  770.   done
  771.   if [ -s "$tmp" ]; then
  772.     $AWK '{k=$1"|" $2; c[k]++} END{for (k in c){split(k,a,"|"); print c[k], a[1], a[2]}}' "$tmp" \
  773.     | sort -nr \
  774.     | while read -r cnt ip host; do
  775.         local base="${HT_REQ_L3[$host]:-${HT_REQ_L3["*"]:-$((CC_THRESHOLD*4))}}"
  776.         local esc_thr=$(( base/2 )); [ "$esc_thr" -lt 5 ] && esc_thr=5
  777.         if (( cnt >= esc_thr )); then
  778.           if $IPSET test "$SET_RL_V4" "$ip" >/dev/null 2>&1; then
  779.             ban_ip "$ip" "L7-perip-escalation(cnt=$cnt,thr=$esc_thr)"
  780.           else
  781.             $IPSET add "$SET_RL_V4" "$ip" -exist
  782.             LIMITED_THIS_RUN+=("$ip ($host perip-escalation cnt=$cnt thr=$esc_thr)")
  783.           fi
  784.         fi
  785.       done
  786.   fi
  787. }

  788. redis_should_limit(){
  789.   [ -n "$REDIS_CLI" ] || { echo 0; return; }
  790.   local ip="$1" k="ddos:l7:$ip" r
  791.   r=$($REDIS_CLI -x <<EOF
  792. MULTI
  793. INCR $k
  794. EXPIRE $k 10
  795. EXEC
  796. EOF
  797. )
  798.   local cnt; cnt=$(echo "$r" | tail -n1 | tr -dc 0-9)
  799.   if [ -z "$cnt" ]; then echo 0; else [ "$cnt" -ge "$CC_RATELIMIT_THRESHOLD" ] && echo 1 || echo 0; fi
  800. }

  801. # ==============================================================================
  802. # Ban/Unban/Flush
  803. # ==============================================================================
  804. ban_ip(){
  805.   local ip="$1" reason="${2:-unknown}"
  806.   if is_in_whitelist_snapshot "$ip"; then log "璺宠繃灏佺锛堢櫧鍚嶅崟锛夛細$ip"; return 0; fi
  807.   if is_ipv6 "$ip"; then have ip6tables || { log "IPv6 涓嶅彲鐢紝璺宠繃 $ip"; return 0; }; $IPSET add "$SET_BL_V6" "$ip" -exist
  808.   else $IPSET add "$SET_BL_V4" "$ip" -exist; fi
  809.   BANNED_THIS_RUN+=("$ip ($reason)") # 灏嗗師鍥犲姞鍏ユ壒娆℃垬鎶
  810.   echo "$($DATE '+%F %T'),BAN,$ip,$reason" >> "$BAN_HISTORY"
  811. }
  812. unban_ip(){
  813.   local ip="$1" reason="${2:-unknown}"
  814.   is_ipv6 "$ip" && $IPSET del "$SET_BL_V6" "$ip" 2>/dev/null || $IPSET del "$SET_BL_V4" "$ip" 2>/dev/null || true
  815.   echo "$($DATE '+%F %T'),UNBAN,$ip,$reason" >> "$BAN_HISTORY"
  816. }
  817. 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; }

  818. # ==============================================================================
  819. # 涓诲贰妫€锛氬姩鎬佽皟閫 + L4/L7 鍒ゅ喅 + 鍛婅
  820. # ==============================================================================
  821. alert_markdown_batch(){
  822.   [ -n "$DINGTALK_WEBHOOK" ] || return 0
  823.   [ "${#BANNED_THIS_RUN[@]}" -eq 0 ] && [ "${#LIMITED_THIS_RUN[@]}" -eq 0 ] && return 0
  824.   local pub="N/A" lan="N/A"
  825.   pub="$(get_public_ip)"
  826.   lan="$(hostname -I 2>/dev/null | awk '{print $1}')"
  827.   local ban_lines="" lim_lines=""
  828.   for x in "${BANNED_THIS_RUN[@]}";  do ban_lines+="- $x\n"; done
  829.   for x in "${LIMITED_THIS_RUN[@]}"; do lim_lines+="- $x\n"; done
  830.   local text="### 馃毀 DDoS-Guard 鎵规鎴樻姤
  831. - **鏃堕棿**锛$($DATE '+%F %T')
  832. - **涓绘満**锛$pub锛堝缃戯級 / $lan锛堝唴缃戯級
  833. - **灏佺鏉$洰**锛
  834. $([ -n "$ban_lines" ] && echo -e "$ban_lines" || echo "- 鏃")
  835. - **闄愰€熸潯鐩**锛
  836. $([ -n "$lim_lines" ] && echo -e "$lim_lines" || echo "- 鏃")"
  837.   alert_markdown "batch" "DDoS-Guard 鎵规鎴樻姤" "$text"
  838. }

  839. # --- alert_attack_state 鍑芥暟寮€濮 ---
  840. alert_attack_state(){
  841.   local state="$1"  # 鎺ユ敹绗竴涓弬鏁 (enter / exit)
  842.   local metrics="$2" # 鎺ユ敹鏂板鐨勭浜屼釜鍙傛暟锛岀敤浜庢帴鏀舵寚鏍囪鎯
  843.   
  844.   # 鑾峰彇涓绘満鍏綉鍜屽唴缃 IP
  845.   local pub="N/A" lan="N/A"
  846.   pub="$(get_public_ip)"
  847.   lan="$(hostname -I 2>/dev/null | awk '{print $1}')"
  848.   
  849.   # 鑾峰彇褰撳墠鐨勬壂鎻忛棿闅
  850.   local current_interval
  851.   current_interval=$(get_current_interval || echo '?')
  852.   
  853.   # 瀹氫箟閽夐拤娑堟伅鐨勪富瑕佸唴瀹
  854.   local text="### 鈿狅笍 鏀诲嚮妯″紡$( [ "$state" = "enter" ] && echo 杩涘叆 || echo 閫€鍑 )
  855. - **鏃堕棿**锛$($DATE '+%F %T')
  856. - **涓绘満**锛$pub / $lan
  857. - **鎵弿闂撮殧**锛${current_interval%s}s"
  858.   
  859.   # 杩欐槸涓€涓叧閿殑鏉′欢鍒ゆ柇锛
  860.   # 鍙湁鍦ㄦ槸鈥滆繘鍏モ€濇敾鍑绘ā寮忥紝骞朵笖鏈夊叿浣撶殑鎸囨爣璇︽儏鏃讹紝
  861.   # 鎵嶄細鍦ㄦ秷鎭湯灏捐拷鍔犫€滆Е鍙戝師鍥犫€濊繖閮ㄥ垎鍐呭銆
  862.   if [ "$state" = "enter" ] && [ -n "$metrics" ]; then
  863.     text+="\n- **瑙﹀彂鍘熷洜**:\n${metrics}"
  864.   fi
  865.   
  866.   # 璋冪敤閫氱敤鐨勫憡璀﹀嚱鏁帮紝灏嗕笂闈㈡嫾鎺ュソ鐨勫畬鏁存秷鎭 ($text) 鍙戦€佸嚭鍘
  867.   alert_markdown "state-$state" "DDoS-Guard 鏀诲嚮妯″紡锛${state}" "$text"
  868. }
  869. # --- alert_attack_state 鍑芥暟缁撴潫 ---

  870. run_core(){
  871.   ensure_ipset_and_iptables
  872.   load_host_thresholds
  873.   load_whitelist
  874.   reconcile_unban_whitelisted   # 鍙屼繚闄╄嚜鎰

  875.   BANNED_THIS_RUN=(); LIMITED_THIS_RUN=()

  876.   # 鍗囩骇鍚庣殑浠g爜
  877.   local attack_mode_details; attack_mode_details=$(should_attack_mode)
  878.   local attack_mode; attack_mode=$(echo "$attack_mode_details" | head -n 1)
  879.   local metrics_summary; metrics_summary=$(echo "$attack_mode_details" | tail -n +2 | sed 's/^/  - /')

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

  882.   if [ "$ENABLE_DYNAMIC_THRESHOLDS" = true ] && [ "$attack_mode" -eq 1 ]; then
  883.     est_thr=$NO_OF_EST_ATTACK_MODE; syn_thr=$NO_OF_SYN_ATTACK_MODE; cc_thr=$CC_THRESHOLD_ATTACK_MODE
  884.     if [ "$prev_state" -ne 1 ]; then
  885.       set_timer_interval "$ATTACK_SCAN_INTERVAL"
  886.       echo 1 > "$attack_state_file"
  887.       alert_attack_state "enter" "$metrics_summary"
  888.       log "杩涘叆鏀诲嚮妯″紡锛欵ST<$est_thr SYN<$syn_thr L7<$cc_thr锛涙壂鎻忛棿闅=${ATTACK_SCAN_INTERVAL}s"
  889.     fi
  890.   else
  891.     if [ "$prev_state" -ne 0 ]; then
  892.       set_timer_interval "$SCAN_INTERVAL"
  893.       echo 0 > "$attack_state_file"
  894.       alert_attack_state "exit"
  895.       log "閫€鍑烘敾鍑绘ā寮忥細鎵弿闂撮殧鎭㈠=${SCAN_INTERVAL}s"
  896.     fi
  897.   fi

  898.   # L4: EST per-IP
  899.   $SS -Hnta state established \
  900.   | awk "$extract_peer_ip_awk" \
  901.   | sort | uniq -c | sort -nr \
  902.   | while read -r cnt ip; do
  903.       [ -z "$ip" ] && continue
  904.       is_in_whitelist_snapshot "$ip" && continue
  905.       # 澧炲姞鍔ㄦ€佺鐞嗗憳璞佸厤妫€鏌
  906.       $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2_>/dev/null && continue
  907.       [ "$(is_private_ip "$ip")" = "1" ] && continue
  908.       $IPSET test "$SET_GB_V4" "$ip" >/dev/null 2>&1 && continue
  909.       (( cnt >= est_thr )) && ban_ip "$ip" "L4-EST(cnt=$cnt,thr=$est_thr)"
  910.     done

  911.   # L4: SYN_RECV per-IP
  912.   $SS -Hnta state syn-recv \
  913.   | awk "$extract_peer_ip_awk" \
  914.   | sort | uniq -c | sort -nr \
  915.   | while read -r cnt ip; do
  916.       [ -z "$ip" ] && continue
  917.       is_in_whitelist_snapshot "$ip" && continue
  918.       # 澧炲姞鍔ㄦ€佺鐞嗗憳璞佸厤妫€鏌
  919.       $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2_>/dev/null && continue
  920.       [ "$(is_private_ip "$ip")" = "1" ] && continue
  921.       $IPSET test "$SET_GB_V4" "$ip" >/dev/null 2>&1 && continue
  922.       (( cnt >= syn_thr )) && ban_ip "$ip" "L4-SYN(cnt=$cnt,thr=$syn_thr)"
  923.     done

  924.   # L7锛氬閲忔棩蹇楄鏁 鈫 Host/Path Tiers 鈫 UA 鍒嗙被 鈫 Redis 闄愰€/灏佺
  925.   local hits; hits=$(count_hits_in_logs || true)
  926.   if [ -n "$hits" ]; then
  927.     while read -r tag cnt ip host path; do
  928.     # 濡傛灉鏍囩鏄 ADMIN_LOGIN, 璇存槑鏄鐞嗗憳IP
  929.     if [ "$tag" = "ADMIN_LOGIN" ]; then
  930.         # 灏嗚IP (姝ゆ椂瀛樺偍鍦╟nt鍙橀噺涓) 鍔犲叆绠$悊鍛樹俊浠婚泦鍚堝苟璺宠繃鍚庣画妫€鏌
  931.         $IPSET add "$SET_ADMIN_V4" "$cnt" -exist
  932.         continue
  933.     fi

  934.     [ -z "$ip" ] && continue
  935.       is_in_whitelist_snapshot "$ip" && continue
  936.       # 澧炲姞鍔ㄦ€佺鐞嗗憳璞佸厤妫€鏌
  937.       $IPSET test "$SET_ADMIN_V4" "$ip" >/dev/null 2_>/dev/null && continue
  938.       local uac; uac="$(classify_ua_for_ip "$ip")"
  939.       if [ "$uac" = "GOOD" ]; then $IPSET add "$SET_GB_V4" "$ip" -exist; continue; fi

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

  945.       local pc="${PT_COUNT[$base_host]:-0}"
  946.       if [ "$pc" -gt 0 ]; then
  947.         local idx=0
  948.         while [ "$idx" -lt "$pc" ]; do
  949.           local key="${base_host}|${idx}"
  950.           local kind="${PT_KIND[$key]:-regex}"
  951.           local pat="${PT_PAT[$key]:-}"
  952.           local L1="${PT_L1[$key]:-0}"
  953.           local L2="${PT_L2[$key]:-0}"
  954.           local L3="${PT_L3[$key]:-0}"
  955.           if [ -n "$pat" ]; then
  956.             if [ "$kind" = "prefix" ]; then
  957.               case "$path" in
  958.                 "$pat"*) [ "$L1" -gt 0 ] && l1="$L1"; [ "$L2" -gt 0 ] && l2="$L2"; [ "$L3" -gt 0 ] && l3="$L3"; idx=$pc; continue
  959.               esac
  960.             else
  961.               echo "$path" | $GREP -Eiq -- "$pat" && { [ "$L1" -gt 0 ] && l1="$L1"; [ "$L2" -gt 0 ] && l2="$L2"; [ "$L3" -gt 0 ] && l3="$L3"; idx=$pc; continue; }
  962.             fi
  963.           fi
  964.           idx=$((idx+1))
  965.         done
  966.       fi

  967.       local thr=$l1
  968.       if [ "$ENABLE_DYNAMIC_THRESHOLDS" = true ] && [ "$attack_mode" -eq 1 ]; then
  969.         thr=$(python3 - <<PY "$l1" "$w"
  970. import sys
  971. l1=float(sys.argv[1]); w=float(sys.argv[2])
  972. print(int(max(3, l1*w*0.4)))
  973. PY
  974. )
  975.       fi

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

  980.       if (( cnt >= thr )); then
  981.         local decide_limit=0
  982.         if [ "$ENABLE_RATE_LIMITING" = true ]; then decide_limit=$(redis_should_limit "$ip"); fi
  983.         if [ "$decide_limit" -eq 1 ] && [ "$uac" != "BAD" ] && [ "$uac" != "FAKEGOOD" ]; then
  984.           $IPSET add "$SET_RL_V4" "$ip" -exist
  985.           LIMITED_THIS_RUN+=("$ip ($host $path L7-limit cnt=$cnt thr=$thr)")
  986.         else
  987.           ban_ip "$ip" "L7-CC($host $path,cnt=$cnt,thr=$thr,ua=$uac)"
  988.         fi
  989.       fi
  990.     done <<< "$hits"
  991.   fi

  992.   # 閿欒鏃ュ織 perip 鍗囩骇锛氬娆¢檺娴 鈫 闄愰€/灏佺
  993.   perip_escalation_from_errorlog

  994.   # 鎵规鎴樻姤锛堣妭娴 10 鍒嗛挓锛
  995.   alert_markdown_batch
  996. }

  997. # 杩愯涓€娆★紙閿佹帶鍒讹細--nowait 闈為樆濉烇紱榛樿闃诲绛夊緟锛
  998. run_once(){
  999.   local mode="${1:-wait}"  # wait / nowait
  1000.   lock_open
  1001.   if [ "$mode" = "nowait" ]; then
  1002.     if ! lock_try; then
  1003.       log "妫€娴嬪埌宸叉湁瀹炰緥鍦ㄨ繍琛岋紙nowait 妯″紡锛岃烦杩囨湰杞級銆"
  1004.       return 0
  1005.     fi
  1006.   else
  1007.     lock_wait   # CLI 璋冪敤锛氶樆濉炵瓑寰咃紝涓嶅啀鎶モ€滃凡鏈夊疄渚嬪湪杩愯鈥
  1008.   fi
  1009.   run_core
  1010. }

  1011. # ==============================================================================
  1012. # 瀛愬懡浠
  1013. # ==============================================================================
  1014. cmd_install(){
  1015.   need_root
  1016.   # 1) systemd 鍗曞厓
  1017.   cat >/etc/systemd/system/ddos-guard.service <<SERVICE
  1018. [Unit]
  1019. Description=DDoS Guard one-shot scan
  1020. After=network-online.target
  1021. Wants=network-online.target

  1022. [Service]
  1023. Type=oneshot
  1024. ExecStart=$SELF_PATH run --nowait
  1025. User=root
  1026. Group=root
  1027. Nice=-5
  1028. IOSchedulingClass=realtime

  1029. [Install]
  1030. WantedBy=multi-user.target
  1031. SERVICE

  1032.   cat >"$timer_unit" <<TIMER
  1033. [Unit]
  1034. Description=Run ddos-guard every ${SCAN_INTERVAL}s

  1035. [Timer]
  1036. OnBootSec=30s
  1037. OnUnitActiveSec=${SCAN_INTERVAL}s
  1038. AccuracySec=1s
  1039. Unit=ddos-guard.service
  1040. Persistent=true

  1041. [Install]
  1042. WantedBy=timers.target
  1043. TIMER

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

  1046.   # 2) 鐩綍/鐧藉悕鍗曟枃浠
  1047.   mkdir -p /etc/ddos
  1048.   touch "$IGNORE_IP_FILE" "$IGNORE_HOST_FILE"

  1049.   # 3) 棣栨鐧藉悕鍗曞姞杞斤紙澶辫触涓嶉樆鏂級
  1050.   load_whitelist || true
  1051.   reconcile_unban_whitelisted || true

  1052.   log "瀹夎瀹屾垚锛歴ystemd timer 姣 ${SCAN_INTERVAL}s 宸℃涓€娆°€"
  1053.   alert_markdown "install" "DDoS-Guard 瀹夎瀹屾垚" "鉁 瀹夎瀹屾垚 @ $($DATE '+%F %T')锛屽畾鏃跺贰妫€ ${SCAN_INTERVAL}s锛涙敾鍑绘ā寮忚嚜鍔ㄦ彁鍗囪嚦 ${ATTACK_SCAN_INTERVAL}s銆"
  1054.   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"
  1055. }

  1056. 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 閰嶇疆銆"; }
  1057. cmd_daemon(){ need_root; log "杩涘叆鍓嶅彴瀹堟姢锛${SCAN_INTERVAL}s锛..."; while :; do run_once nowait || true; sleep "$SCAN_INTERVAL"; done; }

  1058. cmd_status(){
  1059.   echo "== ipset 姒傝 =="
  1060.   ipset_members "$SET_WL_V4" | awk 'END{print "WLv4:", NR+0}'
  1061.   ipset_members "$SET_WL_V6" | awk 'END{print "WLv6:", NR+0}'
  1062.   ipset_members "$SET_BL_V4" | awk 'END{print "BLv4:", NR+0}'
  1063.   ipset_members "$SET_BL_V6" | awk 'END{print "BLv6:", NR+0}'
  1064.   ipset_members "$SET_RL_V4" | awk 'END{print "RLv4:", NR+0}'
  1065.   ipset_members "$SET_GB_V4" | awk 'END{print "GBv4:", NR+0}'
  1066.   echo "== systemd =="; systemctl status --no-pager ddos-guard.timer 2>/dev/null | sed -n '1,12p'
  1067.   echo "== whitelist srcstat =="; [ -f "$RUNDIR/wl.srcstat" ] && cat "$RUNDIR/wl.srcstat" || echo "(鏆傛棤)"
  1068.   echo "== scan interval =="; [ -f "$RUNDIR/scan-interval.current" ] && cat "$RUNDIR/scan-interval.current" || echo "${SCAN_INTERVAL}"
  1069.   echo "== attack state =="; [ -f "$attack_state_file" ] && cat "$attack_state_file" || echo 0
  1070. }

  1071. cmd_top(){ top_talkers_l4; }
  1072. cmd_history(){ [ -f "$BAN_HISTORY" ] && tail -n 50 "$BAN_HISTORY" || echo "鏆傛棤鍘嗗彶銆"; }

  1073. cmd_check(){
  1074.   echo "== 渚濊禆妫€鏌 =="; for c in ss iptables ipset python3 host conntrack redis-cli; do printf "%-12s : " "$c"; have "$c" && echo OK || echo MISSING; done
  1075.   autodetect_btwaf_paths
  1076.   echo "== BTWAF 鏂囦欢 =="; ls -l "$BTWAF_IP_WHITE" 2>/dev/null || echo "鏃 IPv4 鏁村舰鐧藉悕鍗"; ls -l "$BTWAF_IP_WHITE_V6" 2>/dev/null || echo "鏃 IPv6 鐧藉悕鍗"
  1077.   echo "== 鐧藉悕鍗曟簮 =="; echo "$IGNORE_IP_FILE"; [ -s "$IGNORE_IP_FILE" ] && echo "(has content)"; echo "$IGNORE_HOST_FILE"; [ -s "$IGNORE_HOST_FILE" ] && echo "(has content)"
  1078.   echo "== Host/Path JSON =="; [ -s "$THRESHOLDS_JSON" ] && echo "$THRESHOLDS_JSON (present)" || echo "浣跨敤鑴氭湰鍐呯疆榛樿"
  1079. }

  1080. cmd_tune(){ echo "淇濆簳闃堝€硷細SYN=${NO_OF_SYN} EST=${NO_OF_EST} CT=${NO_OF_CT}锛涙敾鍑绘€ L7 璧风偣=${CC_THRESHOLD_ATTACK_MODE}"; }
  1081. cmd_tune_guide(){
  1082. cat <<'EOF'
  1083. [璋冧紭鎸囧崡]
  1084. - 澶氱珯鐐癸細CC_LOG_PATHS/ERROR_LOG_PATHS 涓€琛屼竴涓紱LOG_HOST_MAP 鍙寚瀹氣€滆矾寰勬鍒 鈫 鍩熷悕鈥濄€
  1085. - 闃堝€ JSON锛欸LOBAL 瑕嗙洊鍏滃簳锛涙瘡 host 閰 HOST_WEIGHT 鍜 L1/L2/L3锛汸ATH_TIERS 鏀寔 regex/prefix銆
  1086. - 铚樿洓锛欸OOD(UA+PTR+鍥炶瘉) 鈫 ddos_goodbots_v4 鍏嶆壈锛汼EMI 鍏堥檺閫燂紱BAD/FAKE 鏀剁揣闃堝€间紭鍏堝皝绂併€
  1087. - 鐧藉悕鍗曪細鑱氬悎 BTWAF v4/v6 + ignore.ip/host锛汻edis 鍝堝笇鐢ㄤ簬闈欓粯鑷剤锛**鍛婅浠呮寜 mtime**銆
  1088. - Redis锛氱敤浜 L7 鍘熷瓙璁℃暟(10s绐楀彛) 涓庘€滄棩蹇楁父鏍囩紦瀛樷€濓紱鏃 Redis 鑷姩閫€鍖栥€
  1089. - 鍔ㄦ€佽皟閫燂細鏀诲嚮妯″紡鈫抰imer 闄嶈嚦 ATTACK_SCAN_INTERVAL锛涢€€鍑烘仮澶 SCAN_INTERVAL銆
  1090. - 閽夐拤锛氱姸鎬佸憡璀︼紙杩/閫€锛+ 鎵规鎴樻姤锛涘悓绫 10 鍒嗛挓鑺傛祦锛圖INGTALK_THROTTLE_SEC锛夈€
  1091. EOF
  1092. }

  1093. cmd_whitelist_reload(){ load_whitelist; reconcile_unban_whitelisted; }
  1094. cmd_whitelist_show(){ show_whitelist; }

  1095. cmd_whitelist_debug(){
  1096.   autodetect_btwaf_paths
  1097.   echo "== 璺緞涓庣幆澧 =="
  1098.   echo "IGNORE_IP_FILE    : $IGNORE_IP_FILE"
  1099.   echo "IGNORE_HOST_FILE  : $IGNORE_HOST_FILE"
  1100.   echo "BTWAF_IP_WHITE    : $BTWAF_IP_WHITE"
  1101.   echo "BTWAF_IP_WHITE_V6 : $BTWAF_IP_WHITE_V6"
  1102.   echo "python3           : $PY3"
  1103.   echo "host              : $HOST"
  1104.   echo "getent            : $GETENT"
  1105.   echo
  1106.   echo "== ignore.ip.list 棰勮 =="; [ -r "$IGNORE_IP_FILE" ] && nl -ba "$IGNORE_IP_FILE" | sed -n '1,30p' || echo "(涓嶅瓨鍦ㄦ垨涓嶅彲璇)"
  1107.   echo
  1108.   echo "== ignore.host.list 瑙f瀽娴嬭瘯锛堝墠 10 涓煙鍚嶏級=="
  1109.   if [ -r "$IGNORE_HOST_FILE" ]; then
  1110.     head -n 50 "$IGNORE_HOST_FILE" | sed 's/#.*$//' | sed '/^[[:space:]]*$/d' | head -n 10 | while read -r h; do
  1111.       echo "-- $h"; resolve_host_to_ips "$h" | head -n 5 | sed 's/^/   /'
  1112.     done
  1113.   else
  1114.     echo "(涓嶅瓨鍦ㄦ垨涓嶅彲璇)"
  1115.   fi
  1116.   echo
  1117.   echo "== BTWAF IPv4 JSON 杞崲锛堝墠 20 鏉★級=="
  1118.   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
  1119.   echo
  1120.   echo "== BTWAF IPv6 JSON 杞崲锛堝墠 20 鏉★級=="
  1121.   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
  1122. }

  1123. cmd_redis_status(){
  1124.   if [ -z "$REDIS_CLI" ]; then echo "redis-cli 鏈畨瑁"; return 1; fi
  1125.   echo "== Redis 杩炴帴娴嬭瘯 =="; $REDIS_CLI PING 2>/dev/null || { echo "PING 澶辫触"; return 1; }; echo "PONG"
  1126.   echo "== 鍘熷瓙璁℃暟 10s 绐楀彛娴嬭瘯 =="
  1127.   local key="ddos:test:$$"; local r
  1128.   r=$($REDIS_CLI -x <<EOF
  1129. MULTI
  1130. DEL $key
  1131. INCR $key
  1132. EXPIRE $key 10
  1133. EXEC
  1134. EOF
  1135. )
  1136.   echo "$r" | sed 's/^/  /'
  1137.   local cnt; cnt=$(echo "$r" | tail -n1 | tr -dc 0-9)
  1138.   [ -n "$cnt" ] && echo "璁℃暟=$cnt锛>=1 姝e父锛" || echo "鏈嬁鍒拌鏁板€"
  1139. }

  1140. cmd_ban(){   [ $# -ge 2 ] || die "鐢ㄦ硶锛$0 ban <ip>";   ban_ip "$2" "manual"; }
  1141. cmd_unban(){ [ $# -ge 2 ] || die "鐢ㄦ硶锛$0 unban <ip>"; unban_ip "$2" "manual"; }
  1142. cmd_flush_bans(){ flush_bans; }
  1143. cmd_blacklist(){
  1144.   [ $# -ge 2 ] || die "鐢ㄦ硶锛$0 blacklist <ip|file>"
  1145.   local arg="$2"
  1146.   if [ -f "$arg" ]; then
  1147.     while read -r x; do
  1148.       x="${x%%#*}"; x="${x//$'\r'/}"; x="$(echo "$x" | xargs || true)"; [ -n "$x" ] || continue
  1149.       is_ipv6 "$x" && $IPSET add "$SET_BL_V6" "$x" -exist || $IPSET add "$SET_BL_V4" "$x" -exist
  1150.     done < "$arg"
  1151.   else
  1152.     is_ipv6 "$arg" && $IPSET add "$SET_BL_V6" "$arg" -exist || $IPSET add "$SET_BL_V4" "$arg" -exist
  1153.   fi
  1154. }

  1155. cmd_f2b_setup(){
  1156.   need_root
  1157.   local jail="${F2B_JAIL_LOCAL:-/etc/fail2ban/jail.local}"
  1158.   touch "$jail"
  1159.   local tmp="$TMPDIR/f2b.ignore.v4"; ipset_members "$SET_WL_V4" > "$tmp" || true
  1160.   if ! grep -q '^\[DEFAULT\]' "$jail"; then echo -e "[DEFAULT]\nignoreip = 127.0.0.1/8" >> "$jail"; fi
  1161.   local old; old=$(awk -F'= *' '/^\[DEFAULT\]/{f=1} f&&/^ignoreip *=/{print $2; exit}' "$jail" | tr -d ' \t')
  1162.   local addrs; addrs=$(cat "$tmp" | paste -sd, -)
  1163.   [ -n "$addrs" ] || { log "Fail2Ban锛氭棤鍙悎骞剁櫧鍚嶅崟"; return 0; }
  1164.   local merged
  1165.   merged=$(python3 - <<PY "$old" "$addrs"
  1166. import sys
  1167. o=(sys.argv[1] or "").split(",")
  1168. a=(sys.argv[2] or "").split(",")
  1169. s=set([x.strip() for x in o if x.strip()])|set([x.strip() for x in a if x.strip()])
  1170. print(",".join(sorted(s)))
  1171. PY
  1172. )
  1173.   python3 - <<'PY' "$jail" "$merged"
  1174. import sys,re
  1175. p,merged=sys.argv[1],sys.argv[2]
  1176. s=open(p,'r',encoding='utf-8').read()
  1177. def repl(m): return m.group(1)+'= '+merged
  1178. s=re.sub(r'(^\[DEFAULT\][\s\S]*?^ignoreip\s*)=.*$', lambda m: repl(m), s, flags=re.M)
  1179. open(p,'w',encoding='utf-8').write(s)
  1180. PY
  1181.   systemctl restart fail2ban 2>/dev/null || true
  1182.   log "Fail2Ban ignoreip 宸插閲忓悎骞剁櫧鍚嶅崟锛屽苟灏濊瘯閲嶅惎鏈嶅姟銆"
  1183. }
  1184. cmd_daily_report(){
  1185.   log "寮€濮嬬敓鎴愭瘡鏃ユ垬鎶..."
  1186.   local start_ts; start_ts=$(date -d "yesterday 19:30:00" +%s)
  1187.   local end_ts; end_ts=$(date -d "today 19:30:00" +%s)

  1188.   local report_data; report_data=$(
  1189.     awk -F, -v start="$start_ts" -v end="$end_ts" '
  1190.       BEGIN { OFS="," }
  1191.       {
  1192.         cmd="date -d ""$1"" +%s"
  1193.         cmd | getline ts
  1194.         close(cmd)
  1195.         if (ts >= start && ts < end) {
  1196.           print $0
  1197.         }
  1198.       }
  1199.     ' "$BAN_HISTORY" 2>/dev/null || true
  1200.   )

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

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

  1211.   local pub; pub="$(get_public_ip)"
  1212.   local text="### 馃搱 DDoS-Guard 姣忔棩鎴樻姤
  1213. - **鎶ュ憡鏃堕棿**: $($DATE '+%F %T')
  1214. - **缁熻鍛ㄦ湡**: 杩囧幓 24 灏忔椂
  1215. - **涓绘満鍏綉 IP**: $pub
  1216. - **鎬昏灏佺IP鏁**: ${ban_count}
  1217. - **鎬昏瑙e皝IP鏁**: ${unban_count}
  1218. #### 灏佺璇︽儏 (鏈€澶氭樉绀20鏉)
  1219. $([ -n "$ban_details" ] && echo -e "$ban_details" || echo "- 浠婃棩鏃犳柊澧炲皝绂両P")"

  1220.   alert_markdown "daily-report" "DDoS-Guard 姣忔棩鎴樻姤" "$text"
  1221.   log "姣忔棩鎴樻姤宸茬敓鎴愬苟鎺ㄩ€併€"
  1222. }
  1223. # ==============================================================================
  1224. # 涓诲叆鍙
  1225. # ==============================================================================
  1226. case "${1:-}" in
  1227.   install)             cmd_install;;
  1228.   uninstall)           cmd_uninstall;;
  1229.   run)                 shift || true; mode="${1:-wait}"; [ "$mode" = "--nowait" ] && mode="nowait" || mode="wait"; run_once "$mode";;
  1230.   daemon)              cmd_daemon;;
  1231.   status)              cmd_status;;
  1232.   top)                 cmd_top;;
  1233.   history)             cmd_history;;
  1234.   check)               cmd_check;;
  1235.   tune)                cmd_tune;;
  1236.   tune-guide)          cmd_tune_guide;;
  1237.   whitelist-reload)    cmd_whitelist_reload;;
  1238.   whitelist-show)      cmd_whitelist_show;;
  1239.   whitelist-debug)     cmd_whitelist_debug;;
  1240.   redis-status)        cmd_redis_status;;
  1241.   ban)                 cmd_ban "$@";;
  1242.   unban)               cmd_unban "$@";;
  1243.   flush-bans)          cmd_flush_bans;;
  1244.   blacklist)           cmd_blacklist "$@";;
  1245.   f2b-setup)           cmd_f2b_setup;;
  1246.   daily-report)        cmd_daily_report;;
  1247.   btwaf-sync-whitelist) cmd_whitelist_reload;;
  1248.   *)
  1249. cat <<'USAGE'
  1250. 鐢ㄦ硶锛歞dos-guard <subcommand>

  1251. 瀛愬懡浠わ細
  1252.   install | uninstall | run [--nowait] | daemon
  1253.   status  | top | history | check | tune | tune-guide
  1254.   whitelist-reload | whitelist-show | whitelist-debug | redis-status
  1255.   ban <ip> | unban <ip> | flush-bans | blacklist <ip|file> | f2b-setup
  1256.   btwaf-sync-whitelist   # 鍏煎鍒悕锛堝悓 whitelist-reload锛

  1257. 璇存槑锛
  1258. - 澶氱珯鐐癸細鍦 CC_LOG_PATHS 涓 ERROR_LOG_PATHS 閲屼竴琛屼竴涓棩蹇楁枃浠跺嵆鍙紱
  1259.           鑻ユ棩蹇楅噷鏃犳硶瑙f瀽 Host锛屽彲鍦 LOG_HOST_MAP 涓坊鍔犫€滄枃浠惰矾寰勬鍒 鈫 鍩熷悕鈥濈殑鏄犲皠銆
  1260. - 闃堝€硷細鑻ュ瓨鍦 /etc/ddos/host-thresholds.json锛屽垯浠ヨ鏂囦欢涓哄噯锛涘惁鍒欎娇鐢ㄨ剼鏈唴缃粯璁わ紙宸插浐鍖栦綘鐨勯厤缃 + Discuz PATH_TIERS锛夈€
  1261. - 铚樿洓绛栫暐锛氱湡路濂借湗铔涳紙UA+PTR+姝e悜鍥炶瘉锛夆啋 ddos_goodbots_v4 鍏嶆壈锛涘崐鍙俊鍏堥檺閫燂紱浼€/鎭舵剰浼樺厛灏佺銆
  1262. - 鐧藉悕鍗曪細姣忚疆 run 閮戒細鑱氬悎 BTWAF v4/v6 + ignore.ip/host锛沚an 鍓嶄簩娆℃牎楠岋紱
  1263.           whitelist-reload 鍚庤嚜鍔ㄨВ灏佸凡杩涘叆鐧藉悕鍗曠殑璇皝 IP銆
  1264. - Redis锛氱敤浜 L7 鍘熷瓙璁℃暟鍜屸€滄棩蹇楁父鏍囩紦瀛樷€濓紙闄嶄綆纾佺洏 I/O锛夛紝鏃 Redis 鑷姩闄嶇骇銆
  1265. - 鍔ㄦ€佽皟閫燂細杩涘叆鏀诲嚮妯″紡鈫掑皢宸℃闂撮殧鏀逛负 ATTACK_SCAN_INTERVAL锛堥粯璁 2s锛夛紝閫€鍑哄悗鎭㈠涓 SCAN_INTERVAL銆
  1266. - 閽夐拤锛氭嫢鏈夆€滆繘鍏/閫€鍑烘敾鍑绘ā寮忊€濈殑鐘舵€佸憡璀︿笌鈥滄壒娆℃垬鎶モ€濓紝鍚岀被 10 鍒嗛挓鑺傛祦銆
  1267. USAGE
  1268.     ;;
  1269. esac

鍏朵腑鏉ユ簮鐧藉悕鍗曟枃浠跺弬鑰冩垨鑰呯洿鎺ュ€熺敤锛


鐧藉悕鍗曡嚜瀹氫箟鏂囦欢涓婁紶瀛樻斁鍒帮細/etc/ddos/
涓洪槻姝㈠嚭閿欙紝浣犲彲浠ョ洿鎺ヤ笅杞芥鑴氭湰锛岀劧鍚庤繘琛屼慨鏀瑰悗涓婁紶鑷筹細/usr/local/sbin/ 涓嬮潰銆銆愬缓璁繕鏄剼鏈懡浠ゆ楠ゆ墜宸ュ鍒讹紝浠ュ厤鏂囦欢瀛樺湪缂栫爜宸紓闂銆

璧嬩簣鑴氭湰瀹夎鎵ц鏉冮檺
  1. sudo chmod +x /usr/local/sbin/ddos-guard
绮樿创淇敼鍐呭鍚庣殑璇硶鏍¢獙锛锛堟棤浠讳綍閿欒杈撳嚭琛ㄧず閫氳繃锛
  1. sudo bash -n /usr/local/sbin/ddos-guard
鍚姩瀹夎骞堕儴缃查槻寰$郴缁

  1. sudo /usr/local/sbin/ddos-guard install
鍒涘缓姣忔棩闃插尽鎯呭喌姹囨姤璁″垝浠诲姟锛氾紙姣忔棩闃插尽鏃ユ姤锛
  1. crontab -e
绮樿创鍒拌鍒掍换鍔℃渶鍚庝竴琛岋細锛堣繖閲岄璁剧殑鏄瘡鏃19锛30杩涜闃插尽姹囨姤锛屾敼涓轰綘鑷繁鎯宠鐨勬椂闂淬€傦級
  1. 30 19 * * * /usr/local/sbin/ddos-guard daily-report >/dev/null 2>&1

甯哥敤鍛戒护
锛氾紙濡傛灉鐧藉悕鍗曡В鏋愪笉瀹屾暣锛岃寰楋細sudo ddos-guard whitelist-reload  閲嶆柊瑁呰浇涓€娆℃墍鏈夋潵婧愮櫧鍚嶅崟
  1. # 閲嶈浇鏈嶅姟閲嶅惎鏃堕棿璁℃暟鍣
  2. sudo systemctl daemon-reload && sudo systemctl restart ddos-guard.timer

  3. # 鍚屾 瀹濆nginx闃茬伀澧 鐧藉悕鍗 + 寮哄埗宸℃涓€杞
  4. sudo ddos-guard btwaf-sync-whitelist
  5. sudo ddos-guard run
  6. sudo ddos-guard status

  7. # 鏌ョ湅鎵€鏈塱pv4鐧藉悕鍗
  8. sudo ddos-guard whitelist-show

  9. # 鏌ョ湅鎵€鏈夌櫧鍚嶅崟缁熻
  10. sudo ddos-guard whitelist-show --count

  11. # Fail2ban 閮ㄧ讲
  12. sudo ddos-guard f2b-setup
  13. sudo ddos-guard f2b-status

  14. # 涓€閿竻绌洪粦鍚嶅崟骞跺敖閲忚仈鍔‵ail2ban瑙e皝
  15. sudo ddos-guard clear-bans [--all-jails]

  16. # 鍒犻櫎鍗曚釜琚案涔呭皝绂佺殑 IP
  17. sudo nft delete element inet ddg bl4 { 220.181.108.93 }

  18. # 娓呯┖鎵€鏈夋案涔呭皝绂侊紙鎱庣敤锛
  19. sudo nft flush set inet ddg bl4

  20. # 娓呯┖鎵€鏈変复鏃跺皝绂侊紙濡傛灉涔熸兂椤烘墜瑙e紑 tmp锛
  21. sudo ddos-guard flush-temp

  22. # 淇敼鍚庨噸鏂板姞杞芥墽琛
  23. sudo systemctl daemon-reload
  24. sudo systemctl start ddos-guard.timer

  25. # 鏌ョ湅闃叉姢瀹氭椂鍣ㄧ姸鎬
  26. sudo systemctl status ddos-guard.timer
  27. --------------------------------------------------------------------------
  28. # 鏌ョ湅鐘舵€
  29. sudo ddos-guard status

  30. # 瀹炴椂鐩戞帶
  31. sudo ddos-guard top

  32. # 閽夐拤鎺ㄩ€佹秷鎭嚜妫€锛堟墦鍗颁袱娆¤繑鍥炰綋锛
  33. sudo ddos-guard ding-check

  34. # 鏌ョ湅閽夐拤閰嶇疆锛堣繖閲屼細鏄剧ず瀹屾暣 host锛
  35. sudo ddos-guard ding-show-config

  36. # 骞舵墜宸ヨ窇涓€杞紝瑙傛祴鏄惁鏈 MASS_BAN 鎺ㄩ€
  37. sudo rm -f /var/lib/ddos-guard/notify/last_*.ts
  38. sudo env DINGTALK_DEBUG=2 ddos-guard run

  39. # 鏌ョ湅闃叉姢瀹氭椂鍣ㄧ姸鎬
  40. sudo systemctl status ddos-guard.timer

  41. # 鏌ョ湅灏佺鍘嗗彶
  42. sudo ddos-guard history

  43. # 鎵嬪姩杩愯涓€娆℃壂鎻
  44. sudo ddos-guard run

  45. # 鏌ョ湅闃叉姢鏈嶅姟杩愯鐘舵€
  46. sudo systemctl status ddos-guard.service

  47. # 鏌ョ湅褰撳墠榛戝悕鍗
  48. sudo ddos-guard blacklist

  49. # 鏌ョ湅IP濞佽儊鎯呮姤搴
  50. sudo ipset list ddos_tempban

  51. # 鏌ョ湅姘镐箙灏佺榛戝悕鍗曪紙IP濞佽儊鎯呮姤搴擄級
  52. sudo ddos-guard perma-list

  53. # 缃戠粶璋冧紭鍐呮牳闃插尽鍙傛暟涓存椂搴旂敤
  54. sudo ddos-guard tune

  55. # ddos-guard 鐜鑷鎶ュ憡
  56. sudo ddos-guard check

  57. # 鏌ョ湅鏈嶅姟鍣≧edis鍘熺敓CC闃插尽寮曟搸鐘舵€
  58. sudo ddos-guard redis-status

  59. # 涓€閿竻绌哄皝绂
  60. sudo ddos-guard flush-bans

  61. # 鎴栧悓鏃舵竻鎯呮姤搴撳苟灏濊瘯浠 fail2ban 瑙e皝
  62. sudo ddos-guard flush-bans --purge-intel

  63. # 闆嗘垚 Fail2ban 鑱斿姩闃插尽鍒濆鍖
  64. sudo ddos-guard f2b-setup

  65. # 鍒锋柊鍩熷悕鐧藉悕鍗曠紦瀛
  66. sudo ddos-guard refresh-dns

  67. # 鍒锋柊瀹濆鐧藉悕鍗曠紦瀛
  68. ddos-guard refresh-btwaf

  69. # 瀹屽叏鍗歌浇~
  70. sudo ddos-guard uninstall || true

  71. # 鎵嬪姩閿€姣佹棫闆嗗悎锛堟鏃 iptables 瑙勫垯宸茶鍗歌浇锛屼笉浼氳寮曠敤锛
  72. for s in ddos_white ddos_tempban ddos_permban ddos_white_v6 ddos_tempban_v6 ddos_permban_v6; do
  73.   sudo ipset destroy "$s" 2>/dev/null || true
  74. done

  75. # 閲嶆柊瀹夎
  76. sudo ddos-guard install

  77. # 濡傛灉涔嬪墠 family 寮勯敊銆乼imer 娌¤濂斤細
  78. sudo ddos-guard repair-ipsets
  79. sudo ddos-guard fix-systemd


  80. # 閲嶆柊鍚屾鐧藉悕鍗曞苟璺戜竴杞娴
  81. sudo ddos-guard btwaf-sync-whitelist
  82. sudo ddos-guard run

  83. # 鍏抽棴瀹濆绯荤粺鍔犲浐
  84. btpython /www/server/panel/plugin/syssafe/stop.py 0
[姝ゅ鍖呭惈闅愯棌鍐呭锛屽鏋滈渶瑕佹煡鐪嬭鍥炲]