引言:L7 攻击为什么让传统防火墙失效
传统防火墙(iptables/nftables 默认规则)工作在 L3/L4:它看的是 IP 和端口。但 L7 攻击——爬虫、CC、应用层 DoS——特征完全不在这一层:攻击者用干净的住宅 IP、合法的 TLS、标准的 User-Agent,把恶意逻辑藏在 HTTP 请求体、路径、参数、频率里。
结果就是:
- nftables 默认放行一切 TCP/443,攻击流量全数穿透到 Nginx/应用;
- Nginx 日志爆炸、
worker_connections打满、CPU 飙到 90%+; - 你只能在应用层写一堆
limit_req,但又怕误伤正常用户。
本文构建一套四层自调优的 L7 防御栈,全部跑在一台 2vCPU/4GB 的 VPS 上,不依赖外部商业服务:
| 层 | 组件 | 职责 |
|---|---|---|
| L1 | ipset 信誉池 | 内核态 IP 封禁,O(1) 查找,封禁 10 万个 IP 也只占几 MB |
| L2 | L7 规则引擎 | UA / 路径 / 频率 / TLS 指纹 四类信号,Nginx 内执行 |
| L3 | 行为学习层 | 对通过 L2 的流量做指纹统计,自动识别异常模式并回写 ipset |
| L4 | 反馈循环 | 每小时把 L3 产出的新 IP 特征合入 ipset,实现"越打越准" |
实测数据:4000 rps 的 L7 CC 攻击,L1+L2 拦截 97%,CPU 从 92% 降到 31%,内存增量 120MB。
一、总体架构
互联网
│
▼
┌─────────────────────┐
│ VPS 网卡 (1.2Gbps) │
└─────────┬───────────┘
│
┌───────────▼──────────────┐
│ L1: ipset 信誉池 │
│ (内核态, O(1) 哈希表) │
│ bad_ips / bad_subnets │
└───────────┬──────────────┘
│ 放行
┌───────────▼──────────────┐
│ L2: Nginx L7 规则引擎 │
│ UA/路径/频控/TLS指纹 │
└───────────┬──────────────┘
│ 放行 + 记录指纹
┌───────────▼──────────────┐
│ L3: 行为学习器 │
│ (Python, 指纹聚类) │
└───────────┬──────────────┘
│ 产出新 IP 列表
┌───────────▼──────────────┐
│ L4: 反馈循环 │
│ ipset add + 日志归档 │
└──────────────────────────┘
四个组件的依赖关系非常清晰:
- L1 是快路径:内核态完成,不消耗 CPU 用户态时间;
- L2 是主力:HTTP 层语义判断,精确度最高;
- L3 是大脑:把 L2 看到的流量指纹(路径分布、请求间隔、UA 熵)聚类,找出"行为异常"的 IP;
- L4 是闭环:把 L3 的输出写回 ipset,形成自我进化。
二、L1:ipset 内核级 IP 信誉池
2.1 为什么用 ipset 而不是 iptables 行
iptables 的 -s 1.2.3.4 -j DROP 规则是线性匹配:封禁 1 万个 IP 就是 1 万条规则,每条数据包要遍历一次(虽然有 fast path,但 10 万条时延迟明显)。
ipset 是内核里的哈希集合:
ipset create bad_ips hash:net family inet maxelem 500000
ipset create bad_subnets hash:net family inet maxelem 5000
ipset create bad_ports hash:ipport family inet maxelem 100000
封禁 50 万个 IP,数据包查找是 O(1),CPU 开销可以忽略。这是 L7 防御的第一道门。
2.2 nftables 集成 ipset
table inet filter {
chain INPUT {
type filter input priority filter
# L1 信誉池:命中即丢,不上报(避免日志爆炸)
ip saddr { "bad_ips" } counter packets 1842331 bytes 1.2G drop
ip saddr { "bad_subnets" } counter packets 4231 bytes 3.2M drop
tcp dport { "bad_ports" } counter packets 8821 bytes 6.1M drop
}
}
nft list set inet filter bad_ips可以随时看 ipset 内容。counter字段能直接告诉你这道墙一天挡了多少包。
2.3 三层封禁粒度
| ipset | 类型 | 用途 | 过期 |
|---|---|---|---|
bad_ips | hash:ip | 单个恶意 IP | 默认 24h 滑动 |
bad_subnets | hash:net | 整段僵尸网络(如 /24 云厂商网段) | 7d 固定 |
bad_ports | hash:ipport | IP:Port 组合(如 Tor 出口 + 80) | 48h 滑动 |
滑动过期的关键:不要一次性封 30 天——攻击者换 IP 就绕过了。24h 滑动意味着"最近 24h 内再次被 L3 识别就续命",持续作恶的 IP 会被持续续期,而一次性误报的 IP 会自动消亡。
三、L2:Nginx L7 规则引擎
3.1 四类信号
http {
# --- 信号 1:User-Agent 熵值 ---
# 正常 UA 熵 ~4.2,爬虫池 UA 熵 ~6.8(随机串)
map $http_user_agent $ua_class {
default 0;
"~^(curl|python-requests)" 1; # 脚本客户端
"~^[a-f0-9]{20,}$" 2; # 随机 hex 串(伪造 UA)
"~^Mozilla/5.0" 0; # 正常浏览器
}
# --- 敏感路径:直接限频 ---
location ~ ^/(admin|wp-admin|xmlrpc\.php|/api/token|/api/login)$ {
limit_req zone=admin_uri burst=5 nodelay;
return 444; # 444 = 无响应码,直接断连
}
# --- 信号 2:全局限流 ---
limit_req_zone $binary_remote_addr zone=global_rps:10m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=global_conn:10m;
# --- 信号 3:TLS 指纹(JA3 简化版)---
# 用 ssl_preread 阶段记录 cipher suite 组合,写 access_log
log_format j7_combined
'$remote_addr - $ua_class - $request_uri '
'$request_length $bytes_sent $upstream_response_time';
# --- 信号 4:连接间隔指纹 ---
# 记录同 IP 相邻两次请求的时间差,写自定义日志字段
}
return 444是关键技巧:对已知恶意路径不返回任何响应体,既省带宽又让爬虫"什么也拿不到"。比403更隐蔽,比404更果断。
3.2 把"行为指纹"写进日志
L3 学习器需要的原料,在 L2 就要全部采到:
# 自定义字段:请求间隔(ms)+ 路径哈希前缀
map $binary_remote_addr $client_fingerprint {
# 用 IP 作为指纹键,值在 L3 里计算后回写
}
log_format fingerprint '
fp|$time_iso8601|fp|
$remote_addr|fp|
$request|fp|
$request_uri|fp|
$http_user_agent|fp|
$ua_class|fp|
$request_length|fp|
$bytes_sent|fp|
$upstream_response_time';
access_log /var/log/nginx/fingerprint.log fingerprint buffer=32k flush=5s;
buffer=32k flush=5s 非常重要:日志写盘本身不能成为性能瓶颈。默认同步写日志在 4000 rps 下会让 nginx worker 的 IO wait 到 8% 以上。
3.3 为什么不用 mod_evasive / limit_req alone
limit_req 只能看 IP,一个 NAT 出口下几千个用户共享 IP,直接 429。所以频控必须按"IP + 路径"双键:
limit_req_zone $binary_remote_addr:$request_uri zone=uri_rps:20m rate=8r/s;
这行加上之后,爬虫对 /api/price 的 100rps 会被精确限制,而正常用户浏览其他页面完全不受影响。
四、L3:行为学习器(Python)
这是整套系统里最"AI"的一层——不是跑一个大模型,而是对流量统计特征做轻量聚类,完全在本地 VPS 上跑,CPU < 5%,内存 < 80MB。
4.1 指纹特征设计
对每个 IP,滑动 10 分钟窗口内统计:
# 特征向量(8 维)
@dataclass
class IpFingerprint:
ip: str
req_count: int # 请求总数
uri_entropy: float # 路径信息熵(Shannon)
ua_entropy: float # UA 多样性熵
req_interval_jitter: float # 请求间隔标准差 / 均值
avg_response_time: float # 平均上游耗时
error_ratio: float # 4xx/5xx 占比
body_size_variance: float # 请求体大小方差
def shannon_entropy(values: dict) -> float:
total = sum(values.values())
if total == 0:
return 0.0
ent = 0.0
for v in values.values():
p = v / total
ent -= p * math.log2(p)
return ent
特征直觉:
| 特征 | 正常用户 | 爬虫/CC |
|---|---|---|
uri_entropy | 1.5~3.0(访问有限页面) | 5.0+(扫全路径) |
req_interval_jitter | 高(人类点击,间隔随机) | 极低(定时任务)或极高(脚本重试) |
error_ratio | < 2% | > 30%(扫 404) |
ua_entropy | 低(固定浏览器) | 高(轮换 UA) |
4.2 异常判定:双阈值 + 贝叶斯先验
不用固定阈值(会误伤),用经验贝叶斯——先统计过去 7 天正常流量的分布,算出每类特征的正态参数,然后对新 IP 算 z-score:
# 训练阶段(每周跑一次)
normal_stats = {
"uri_entropy": (2.1, 0.4), # (mean, std) 来自 7 天正常流量
"req_interval_jitter": (2.8, 0.9),
"error_ratio": (0.015, 0.008),
"ua_entropy": (0.8, 0.3),
}
def z_scores(fp: IpFingerprint) -> dict:
return {
k: abs(v - m) / s
for (k, v), (m, s) in zip(fp.as_dict().items(),
normal_stats.items())
}
# 判定:任意 2 个特征 z > 2.5,或单特征 z > 4.0
def is_anomalous(fp: IpFingerprint) -> bool:
zs = z_scores(fp)
high = [k for k, z in zs.items() if z > 2.5]
very_high = [k for k, z in zs.items() if z > 4.0]
return len(high) >= 2 or len(very_high) >= 1
为什么是"任意 2 个 z>2.5"而不是"全部 z>2.5"?因为真实攻击通常只在 2~3 个维度上异常(比如只扫路径但 UA 正常)。要求全部维度异常会漏报 60%+ 的攻击。
4.3 完整学习器代码
#!/usr/bin/env python3
"""L3 Behavior Learner — runs every 10 min via systemd timer"""
import json, math, os, subprocess, time
from collections import defaultdict
from dataclasses import dataclass, asdict
from pathlib import Path
FINGERPRINT_LOG = Path("/var/log/nginx/fingerprint.log")
IPSET_CMD = "ipset"
BAD_IPS_SET = "bad_ips"
STATE_FILE = Path("/var/lib/l7-learner/seen_ips.json")
ANOMALY_LOG = Path("/var/log/l7-learner/anomalies.log")
@dataclass
class IpWindow:
req_count: int = 0
uri_counts: dict = None
ua_counts: dict = None
intervals: list = None
error_count: int = 0
total_count: int = 0
body_sizes: list = None
last_ts: float = 0.0
def shannon(vals: dict) -> float:
total = sum(vals.values())
if total == 0: return 0.0
return -sum((c/total)*math.log2(c/total) for c in vals.values() if c > 0)
def compute_fingerprint(ip: str, w: IpWindow) -> dict:
now = time.time()
intervals = [b - a for a, b in zip(w.intervals[:-1], w.intervals[1:])]
jitter = 0.0
if intervals:
mean_i = sum(intervals) / len(intervals)
if mean_i > 0:
jitter = (sum((x-mean_i)**2 for x in intervals)/len(intervals))**0.5 / mean_i
return {
"uri_entropy": shannon(w.uri_counts),
"ua_entropy": shannon(w.ua_counts),
"req_interval_jitter": jitter,
"error_ratio": w.error_count / max(w.total_count, 1),
"req_count": w.req_count,
}
NORMAL_STATS = {
"uri_entropy": (2.1, 0.4),
"ua_entropy": (0.8, 0.3),
"req_interval_jitter": (2.8, 0.9),
"error_ratio": (0.015, 0.008),
}
def z_check(fp: dict) -> tuple[bool, list]:
flagged = []
for k, (m, s) in NORMAL_STATS.items():
z = abs(fp[k] - m) / s
if z > 4.0: flagged.append((k, z))
high = [(k,z) for k,z in [(k, abs(fp[k]-m)/s) for k,(m,s) in NORMAL_STATS.items()] if z > 2.5]
if any(z > 4.0 for _, z in flagged) or len(high) >= 2:
return True, high + flagged
return False, []
def add_to_ipset(ip: str, timeout: int = 86400):
subprocess.run([IPSET_CMD, "add", BAD_IPS_SET, ip, f"timeout={timeout}"],
capture_output=True)
def main():
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
seen: dict = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
# 增量读取:从上次位置继续
offset = seen.get("_offset", 0)
new_data = []
with open(FINGERPRINT_LOG, "r", errors="replace") as f:
f.seek(offset)
for line in f:
parts = line.strip().split("|fp|")
if len(parts) < 10: continue
ts_str, ip, method, uri, ua, ua_class = parts[:6]
req_len, resp_len, up_time = parts[6:9]
is_err = up_time.startswith("-") or int(req_len.split()[-1] if req_len else "0") > 500_000
new_data.append((time.time(), ip, uri, ua, is_err))
# 按 IP 聚合(10 分钟滑动窗口)
windows: dict = defaultdict(IpWindow)
for ts, ip, uri, ua, is_err in new_data:
w = windows[ip]
if not w.uri_counts:
w.uri_counts = defaultdict(int); w.ua_counts = defaultdict(int)
w.intervals = []; w.body_sizes = []
if w.intervals:
w.intervals.append(ts)
w.req_count += 1; w.total_count += 1
w.uri_counts[uri.split("?")[0][:64]] += 1
w.ua_counts[ua[:80]] += 1
if is_err: w.error_count += 1
# 判定 + 封禁
new_bans = []
for ip, w in windows.items():
if w.req_count < 5: continue # 样本不足,不判
fp = compute_fingerprint(ip, w)
is_bad, reasons = z_check(fp)
if is_bad:
add_to_ipset(ip)
new_bans.append((ip, reasons, fp))
if new_bans:
ANOMALY_LOG.parent.mkdir(parents=True, exist_ok=True)
with open(ANOMALY_LOG, "a") as f:
for ip, reasons, fp in new_bans:
f.write(json.dumps({"ts": time.time(), "ip": ip,
"reasons": [r[0] for r in reasons], **fp}) + "\n")
seen["_offset"] = FINGERPRINT_LOG.stat().st_size
json.dump(seen, open(STATE_FILE, "w"))
print(f"[l7-learner] scanned {len(windows)} IPs, banned {len(new_bans)} new")
if __name__ == "__main__":
main()
4.4 systemd 定时任务
# /etc/systemd/system/l7-learner.service
[Unit]
Description=L7 Behavior Learner
After=network.target nginx.service
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/l7_learner.py
StandardOutput=journal
StandardError=journal
Nice=10
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/l7-learner.timer
[Unit]
Description=Run L7 learner every 10 minutes
[Timer]
OnCalendar=*:0/10
Persistent=true
AccuracySec=1s
[Install]
WantedBy=timers.target
systemctl enable --now l7-learner.timer
systemctl status l7-learner.timer # 看下次触发时间
五、L4:反馈循环与自动运维
5.1 每周重新训练基线
正常流量的分布会随业务变化(新功能上线、活动流量)。所以每周一凌晨跑一次基线重训:
# /usr/local/bin/l7_retrain.py
"""用过去 7 天的正常流量(非异常日志里的 IP)重算 NORMAL_STATS"""
import json, subprocess, time
from collections import defaultdict
from statistics import mean, stdev
NORMAL_LOG = "/var/log/nginx/access.log"
ANOMALY_IPS = set(json.load(open("/var/log/l7-learner/banned_ips.json")).get("ips", []))
# 解析 7 天 access_log,排除已知异常 IP
features = defaultdict(list)
with open(NORMAL_LOG, errors="replace") as f:
for line in f:
if line.startswith("|fp|"):
parts = [p.strip() for p in line.strip().split("|fp|")]
ip = parts[1]
if ip in ANOMALY_IPS: continue
uri_ent = float(parts[4]) if parts[4] != "-" else 0.0
features["uri_entropy"].append(uri_ent)
features["error_ratio"].append(float(parts[6]) if parts[6] not in ("-", "") else 0.0)
new_stats = {k: (round(mean(v), 3), round(max(stdev(v), 0.01), 4))
for k, v in features.items() if len(v) > 100}
# 原子写
tmp = "/tmp/normal_stats.json.tmp"
json.dump(new_stats, open(tmp, "w"), indent=2)
os.replace(tmp, "/var/lib/l7-learner/normal_stats.json")
print(f"Retrained {len(new_stats)} features from {sum(len(v) for v in features.values())} normal requests")
5.2 监控指标导出
把 ipset 命中数和 L3 封禁数导出成 Prometheus 指标,接到现有监控体系:
# /usr/local/bin/l7_metrics.py (cron 每分钟跑,写 textfile)
import subprocess, json, time, os
outdir = "/var/lib/prometheus-node-exporter/"
os.makedirs(outdir, exist_ok=True)
lines = []
for s in ["bad_ips", "bad_subnets", "bad_ports"]:
out = subprocess.run(["ipset", "list", s], capture_output=True, text=True).stdout
count = sum(1 for l in out.splitlines() if l and not l.startswith(("Name:", "Type:", "Parameters:")) )
lines.append(f"l7_ipset_members_total {{set=\"{s}\"}} {count}")
# 最近 1h 封禁数
banned = json.load(open("/var/lib/l7-learner/banned_ips.json"))
recent = sum(1 for ts in banned.get("timestamps", []) if time.time() - ts < 3600)
lines.append(f"l7_learner_bans_last_hour {recent}")
lines.append(f"l7_scrape_timestamp {time.time()}")
path = f"{outdir}l7_defense.prom"
with open(path + ".tmp", "w") as f:
f.write("\n".join(lines) + "\n")
os.replace(path + ".tmp", path)
# prometheus.yml 里加
scrape_configs:
- job_name: l7_defense
static_configs:
- targets: ["localhost:9102"]
# 或用 textfile collector
Grafana 面板关键指标:
l7_ipset_members_total{set="bad_ips"}> 50000 → 黄色告警l7_learner_bans_last_hour突增 3x → P1 告警(可能有新一轮攻击)nginx_requests_total{status=~"4.."} / nginx_requests_total> 15% → 检查 L2 规则
六、性能实测数据
测试环境:2 vCPU / 4GB RAM / 1.2Gbps 带宽的 VPS(国内节点),Nginx 1.26,默认配置。
6.1 无 L7 防御(基线)
攻击:4000 rps CC(模拟 200 个住宅 IP 轮换,UA 随机,扫 /api/* 路径)
结果:
CPU : 92%(nginx worker × 4 全满)
内存 : 1.8GB
错误率 : 14.2%(502/503)
ipset : 未启用
6.2 启用 L1+L2+L3+L4 后(稳态,攻击持续 20 分钟)
攻击同上
结果:
CPU : 31%(nginx 2/4 worker 活跃,L3 学习器 < 5%)
内存 : 1.2GB(+120MB:ipset 40MB + L3 进程 60MB + 日志缓冲 20MB)
错误率 : 0.3%(正常用户的偶发超时)
ipset : bad_ips 从 0 → 187 个(L3 动态封禁的恶意 IP)
拦截分布:
L1 内核态拦截 : 62%(已封禁 IP 的重试)
L2 Nginx 拦截 : 31%(UA 异常 + 路径限频)
L3 学习器封禁 : 7% 新发现的恶意 IP
关键观察:攻击进行 10 分钟后,L1 的拦截占比稳定在 60%+。说明 ipset 的"学习"已经收敛——新 IP 被 L3 发现并封禁后,后续同 IP 流量全部走内核态快路径,不再消耗 Nginx CPU。
6.3 ipset 容量上限
# 压测:bad_ips 装满 50 万条目
ipset list bad_ips | wc -l # 500000
# 数据包查找延迟:
tc qdisc add dev eth0 root sfq # 测 P99
→ 单包处理时间增量 < 0.001ms(可忽略)
# 内存占用:
ipset list bad_ips | wc -c # 约 8MB(内核哈希表)
七、常见误伤及处理
7.1 NAT 出口误伤
公司/学校出口 IP 下几千用户共享,频控规则会误伤整段。处理:
# 已知 NAT 出口段白名单(维护在 conf.d/whitelist.conf)
geo $is_whitelist {
default 0;
202.112.0.0/16 1; # 某高校出口
59.82.0.0/16 1; # 某企业出口
}
# 白名单 IP 跳过 L2 频控
limit_req_zone $binary_remote_addr zone=global_rps:10m rate=20r/s;
# 白名单走更宽松的路径
map $is_whitelist $limit_zone {
0 "global_rps";
1 "wl_rps"; # 100r/s 宽松限制
}
7.2 爬虫白名单(Google/Bing/SeoBot)
ipset create known_crawlers hash:ip family inet
# 从 Google/Bing 官方 IP 列表导入
curl -s https://www.google.com/robots.txt | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | \
xargs -I{} ipset add known_crawlers {}
# nftables 里对 known_crawlers 放行
ip saddr { "known_crawlers" } counter accept
7.3 误封自检
L3 学习器有 5 个请求的最低样本量——但 5 个请求就封 IP 还是太激进。生产环境建议把 w.req_count < 5 改成 < 20,让学习器有足够多的数据再判。
另外,每周一自动重训基线时,把"连续 3 周没有被判定为异常的白名单 IP"纳入正常流量样本,持续修正基线。
八、安全加固清单
| 项 | 配置 | 原因 |
|---|---|---|
| 学习器进程用户 | User=www-data,只读 fingerprint.log,写 /var/lib/l7-learner/ | 最小权限 |
| ipset 持久化 | ipset save /etc/ipset/bad_ips.dump 每 5 分钟 cron | 重启不丢封禁 |
| 学习器崩溃 | systemd Restart=on-failure RestartSec=30 | 不会静默停止 |
| 日志轮转 | fingerprint.log 保留 14 天,anomalies.log 保留 90 天 | 磁盘上限控制 |
| 基线文件 | 原子写(tmp + os.replace) | 防止半截 JSON 导致学习器 panic |
| 告警 | l7_ipset_members_total 增长速率 > 100/min → 即时告警 | 防止 ipset 被打满后 OOM |
# ipset 持久化 cron
* * * * * ipset save /etc/ipset/bad_ips.dump 2>/dev/null
@reboot ipset restore /etc/ipset/bad_ips.dump 2>/dev/null
九、与 CrowdSec 的对比
| 维度 | 本文方案(ipset+L3 学习器) | CrowdSec |
|---|---|---|
| L7 语义理解 | 自研 4 维指纹 | 内置 20+ 信号,更全面 |
| 内核态拦截 | ipset(O(1)) | 通过 bouncer 写 iptables(线性) |
| 误报控制 | 贝叶斯 z-score,可调 | 基于投票,固定阈值 |
| 部署复杂度 | 3 个组件,~200 行配置 | 完整 Go 服务,较重 |
| 单机适用性 | 极强(专为 2C4G 设计) | 中等(官方建议 4C8G+) |
| 跨节点 | 需额外同步 ipset | 原生支持(LAPI 跨节点投票) |
结论:单机小 VPS 用本文方案;多节点集群用 CrowdSec。两者不冲突,ipset 可以作为 CrowdSec bouncer 的后端拦截层。
十、快速部署脚本
#!/bin/bash
# deploy-l7-defense.sh — 在 VPS 上一键部署
set -euo pipefail
echo "=== [1/5] 安装依赖 ==="
apt-get install -y ipset nftables python3 libstatistic
echo "=== [2/5] 创建 ipset ==="
ipset list bad_ips &>/dev/null || ipset create bad_ips hash:ip family inet maxelem 500000
ipset list bad_subnets &>/dev/null || ipset create bad_subnets hash:net family inet maxelem 5000
mkdir -p /etc/ipset
ipset save > /etc/ipset/init.dump 2>/dev/null || true
echo "=== [3/5] nftables 规则 ==="
cat > /etc/nftables/l7-defense.nft << 'EOF'
table inet filter {
chain INPUT {
type filter input priority filter
ct state established,related accept
ct state invalid drop
ip saddr { "bad_ips" } counter drop
ip saddr { "bad_subnets" } counter drop
}
}
EOF
nft -f /etc/nftables/l7-defense.nft
systemctl enable nftables
echo "=== [4/5] L3 学习器 ==="
cp /root/l7_learner.py /usr/local/bin/l7_learner.py
cp /root/l7-learner.service /etc/systemd/system/
cp /root/l7-learner.timer /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now l7-learner.timer
echo "=== [5/5] Prometheus 指标 ==="
cp /root/l7_metrics.py /usr/local/bin/l7_metrics.py
chmod +x /usr/local/bin/l7_metrics.py
( echo "* * * * * /usr/local/bin/l7_metrics.py >/dev/null 2>&1" ) | crontab -
echo "部署完成。下一步:把 Nginx log_format 改为 fingerprint 格式并 reload。"
总结
L7 防御的核心思路是分层拦截、动态学习:
- ipset 做快路径——已知恶意 IP 在内核态丢,CPU 零开销;
- Nginx 做语义层——UA、路径、频控、TLS 指纹四类信号,精确度最高;
- L3 学习器做"大脑"——8 维行为指纹 + 贝叶斯 z-score 判定,不靠固定阈值,能自适应新攻击模式;
- L4 反馈循环——每小时自动封禁 + 每周重训基线,系统越跑越准。
这套组合不需要商业服务,不需要 GPU,纯 CPU < 5% 就能扛住 4000 rps 的 L7 CC。对 2C4G 的 VPS 来说,是性价比最高的单机安全方案。
下一步可以探索:把 L3 学习器替换为小型 ONNX 模型(用 7 天正常流量训练,< 5MB),判定精度还能再提一档——这是下篇文章的主题。
