Featured image of post VPS 七层 L7 智能防线:ipset + 规则引擎 + 行为学习

VPS 七层 L7 智能防线:ipset + 规则引擎 + 行为学习

单机 VPS 怎么扛住 L7 恶意流量?本文构建一套自调优的七层防御:ipset 内核级 IP 信誉池、L7 规则引擎(UA/路径/频控)、基于流量行为指纹的学习层,实测 4k rps 下把恶意请求拦在应用层之前,CPU 从 90% 压回 30%。

引言: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 上,不依赖外部商业服务:

层组件职责
L1ipset 信誉池内核态 IP 封禁,O(1) 查找,封禁 10 万个 IP 也只占几 MB
L2L7 规则引擎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_ipshash:ip单个恶意 IP默认 24h 滑动
bad_subnetshash:net整段僵尸网络(如 /24 云厂商网段)7d 固定
bad_portshash:ipportIP: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_entropy1.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 防御的核心思路是分层拦截、动态学习:

  1. ipset 做快路径——已知恶意 IP 在内核态丢,CPU 零开销;
  2. Nginx 做语义层——UA、路径、频控、TLS 指纹四类信号,精确度最高;
  3. L3 学习器做"大脑"——8 维行为指纹 + 贝叶斯 z-score 判定,不靠固定阈值,能自适应新攻击模式;
  4. L4 反馈循环——每小时自动封禁 + 每周重训基线,系统越跑越准。

这套组合不需要商业服务,不需要 GPU,纯 CPU < 5% 就能扛住 4000 rps 的 L7 CC。对 2C4G 的 VPS 来说,是性价比最高的单机安全方案。

下一步可以探索:把 L3 学习器替换为小型 ONNX 模型(用 7 天正常流量训练,< 5MB),判定精度还能再提一档——这是下篇文章的主题。


参考

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials