|
|
| (3 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| == 总框架 ==
| | 网站突然变慢、无法访问,或者重启 Apache 后立即恢复,可能不是带宽已经耗尽,而是大量 TCP 连接占用了 Apache 的进程、线程或连接队列。排查这类问题时,只需先建立一个简单框架:ss 负责观察连接,ipset 负责保存 IP 名单,iptables 负责决定放行或丢弃,connlimit 限制一个 IP 同时占用的连接数,hashlimit 限制一个 IP 建立新连接的速度。 |
|
| |
|
| 排查网站被骚扰、连接占满或访问困难时,应当先区分五个层级:
| | == 观察连接现场:ss 与 watch == |
|
| |
|
| {| class="wikitable"
| | `ss` 是 Linux 中查看 Socket 状态的工具,可以把它理解成服务器网络层的现场监控器。它能够显示服务器是否仍在监听端口、目前有多少连接,以及这些连接处在 TCP 通信的哪个阶段。 |
| ! 层级
| |
| ! 观察对象
| |
| ! 主要工具
| |
|
| |
|
| | ! 解决的问题 |
| | 下面的命令显示整个系统的 Socket 概况: |
| | ------------------------------------ |
| |
| | 上游网络 |
| |
| | 总带宽、分布式 DDoS |
| |
| | 云厂商、CDN、WAF |
| |
| | 流量是否已经在到达服务器之前堵塞线路 |
| |
| | - |
| |
| | Linux 防火墙 |
| |
| | IP、TCP 包、新连接 |
| |
| | iptables、ipset |
| |
| | 哪些 IP 可以进入;单个 IP 能建立多少连接 |
| |
| | - |
| |
| | TCP Socket |
| |
| | ESTABLISHED、SYN-RECV、TIME-WAIT 等连接状态 |
| |
| | ss |
| |
| | 连接究竟堵在哪个阶段 |
| |
| | - |
| |
| | Apache |
| |
| | worker、thread、KeepAlive、请求读取时间 |
| |
| | Apache MPM、超时配置 |
| |
| | TCP 已经接入后,Apache 是否被慢连接或请求占满 |
| |
| | - |
| |
| | MediaWiki、PHP、MariaDB |
| |
| | 页面请求、PHP 进程、数据库查询 |
| |
| | 日志、进程与数据库工具 |
| |
| | 请求是否已经进入应用层并造成计算或数据库压力 |
| |
| | } |
| |
| | |
| 整体路径可以记成:
| |
| | |
| Internet
| |
| ↓
| |
| 云厂商/CDN/WAF
| |
| ↓
| |
| iptables+ipset
| |
| ↓
| |
| Linux TCP Socket
| |
| ↓
| |
| Apache worker/thread
| |
| ↓
| |
| MediaWiki+PHP+MariaDB
| |
| | |
| ipset 和 iptables 只能看见 IP、端口、数据包和连接状态;它们看不懂“这个人一秒钟请求了多少个 MediaWiki 页面”。
| |
| | |
| == 原笔记中最需要纠正的地方 ==
| |
| | |
| === 1. estab 662 不等于 443 端口有 662 条连接 ===
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| Line 60: |
Line 11: |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| `ss -s` 中:
| | 它适合快速判断服务器的连接总量是否突然异常,但不能直接说明 443 端口有多少连接。 |
|
| |
|
| estab 662
| | 下面的命令检查是否仍有程序监听 HTTPS 端口: |
| | |
| 表示这台服务器当前共有大约 662 个处于 `ESTABLISHED` 状态的 TCP Socket,可能包括:
| |
| | |
| * Apache 的 80、443;
| |
| * SSH 的 22;
| |
| * MariaDB、代理、邮件等其他连接;
| |
| * 本机主动连接到外界的连接。
| |
| | |
| 它只是全局概览,不是 Apache 专用统计。`ss -s` 的 `-s` 是 `--summary`,意思是输出 Socket 汇总统计。
| |
| | |
| 精确查看服务器本地 443 端口的已建立连接,应使用:
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -Hnt state established '( sport = :443 )' | | ss -lntp '( sport = :443 )' |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 只统计数量:
| | 如果没有任何输出,说明当前没有程序监听 443 端口。问题通常应从 Apache 是否运行、HTTPS 配置是否成功加载、端口是否被其他程序占用等方向排查。 |
|
| |
|
| <syntaxhighlight lang="bash">
| | 下面的命令统计已经完成 TCP 握手的 HTTPS 连接: |
| ss -Hnt state established '( sport = :443 )' | wc -l
| |
| </syntaxhighlight>
| |
| | |
| 这里的参数含义:
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | ------------------------ |
| |
| | `-H` |
| |
| | `--no-header` |
| |
| | 不显示标题行,避免标题也被 `wc -l` 统计 |
| |
| | - |
| |
| | `-n` |
| |
| | `--numeric` |
| |
| | 直接显示数字 IP 和端口,不做域名或服务名解析 |
| |
| | - |
| |
| | `-t` |
| |
| | `--tcp` |
| |
| | 只查看 TCP Socket |
| |
| | - |
| |
| | `state established` |
| |
| | established state |
| |
| | 只查看已经完成 TCP 握手的连接 |
| |
| | - |
| |
| | `sport` |
| |
| | source port |
| |
| | 对当前服务器 Socket 而言,本地服务端口 |
| |
| | - |
| |
| | `dport` |
| |
| | destination port |
| |
| | 当前 Socket 的另一端端口 |
| |
| | } |
| |
| | |
| `ss` 支持直接按 TCP 状态及 `sport`、`dport` 过滤,所以比用 `grep :443` 更精确。
| |
| | |
| === 2. netstat 加 grep 只能粗略计数 ===
| |
| | |
| 原命令:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| netstat -ant | grep :443 | wc -l
| |
| </syntaxhighlight>
| |
| | |
| 参数含义:
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | ------------------------------- |
| |
| | `-a` |
| |
| | `--all` |
| |
| | 显示监听和非监听 Socket |
| |
| | - |
| |
| | `-n` |
| |
| | `--numeric` |
| |
| | 使用数字 IP 和端口 |
| |
| | - |
| |
| | `-t` |
| |
| | `--tcp` |
| |
| | 只显示 TCP |
| |
| | - |
| |
| | `grep :443` |
| |
| | global regular expression print |
| |
| | 找出含有 `:443` 的行 |
| |
| | - |
| |
| | `wc -l` |
| |
| | word count, lines |
| |
| | 统计行数 |
| |
| | } |
| |
| | |
| 痛点在于,含有 `:443` 的行可能包括:
| |
| | |
| * 本地端口是 443;
| |
| * 对方端口碰巧是 443;
| |
| * `ESTABLISHED`;
| |
| * `TIME_WAIT`;
| |
| * `SYN_RECV`;
| |
| * `FIN_WAIT`;
| |
| * 监听 Socket。
| |
| | |
| 所以它只能回答:
| |
| | |
| “当前 Socket 列表中,有多少行出现了 :443?”
| |
| | |
| 不能直接回答:
| |
| | |
| “当前有多少个已经建立的 HTTPS 入站连接?”
| |
| | |
| 而且 `netstat` 已被其手册列为基本过时,官方建议使用 `ss` 替代。
| |
| | |
| 因此这条命令可以从核心笔记中删除,统一使用 `ss`。
| |
| | |
| == ss:排查 TCP Socket 的主力工具 ==
| |
| | |
| `ss` 可以理解为 `Socket Statistics`。它解决的核心痛点是:
| |
| | |
| “网站访问异常时,连接究竟处于什么状态?”
| |
| | |
| === 查看全局概况 ===
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ss -s
| |
| </syntaxhighlight>
| |
| | |
| 用途:
| |
| | |
| * 快速确认系统 Socket 总数是否突然异常;
| |
| * 判断 `ESTABLISHED`、`TIME-WAIT` 等是否非常多;
| |
| * 适合第一眼查看,不适合精确归因。
| |
| | |
| === 查看 443 的各种状态分别有多少 ===
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ss -Hnt '( sport = :443 )' |
| |
| awk '{print $1}' |
| |
| sort |
| |
| uniq -c |
| |
| sort -nr
| |
| </syntaxhighlight>
| |
| | |
| 可能输出:
| |
| | |
| ESTAB 500
| |
| TIME-WAIT 300
| |
| SYN-RECV 80
| |
| CLOSE-WAIT 20
| |
| | |
| 各状态的大致含义:
| |
| | |
| {| class="wikitable"
| |
| ! 状态
| |
| ! 含义
| |
| | |
| | ! 可能反映的问题 |
| |
| | ------------------ |
| |
| | `ESTAB` |
| |
| | `ESTABLISHED` |
| |
| | 已完成握手,连接正在使用或保持 |
| |
| | - |
| |
| | `SYN-RECV` |
| |
| | `SYN received` |
| |
| | 服务器收到 SYN,等待握手完成 |
| |
| | - |
| |
| | `TIME-WAIT` |
| |
| | 等待旧连接彻底消失 |
| |
| | 短连接很多、连接频繁建立和关闭 |
| |
| | - |
| |
| | `CLOSE-WAIT` |
| |
| | 对方已经关闭,服务器程序尚未完成关闭 |
| |
| | 应用程序没有及时回收连接 |
| |
| | - |
| |
| | `FIN-WAIT` |
| |
| | 正在等待连接关闭流程完成 |
| |
| | 网络延迟、对方迟迟不回应关闭 |
| |
| | } |
| |
| | |
| `ss` 支持 `established`、`syn-recv`、`time-wait`、`close-wait` 等标准 TCP 状态。
| |
| | |
| === 只看已经建立的 HTTPS 连接 ===
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| Line 251: |
Line 27: |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 这条命令解决:
| | `ESTABLISHED` 表示 TCP 三次握手已经完成。这些连接可能来自正常用户、搜索引擎、爬虫、KeepAlive 或慢连接,因此连接数量很高并不自动等于正在遭受攻击。 |
| | |
| “现在究竟有多少条连接已经真正进入 HTTPS 服务?”
| |
| | |
| 监控变化:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| watch -n 1 \
| |
| "ss -Hnt state established '( sport = :443 )' | wc -l"
| |
| </syntaxhighlight>
| |
| | |
| `watch -n 1`: | |
| | |
| * `watch`:重复执行命令;
| |
| * `-n`:`interval`,刷新间隔;
| |
| * `1`:每一秒执行一次。
| |
|
| |
|
| === 查看半连接数量 ===
| | 下面的命令统计尚未完成握手的 HTTPS 连接: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| Line 274: |
Line 35: |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 这条命令解决:
| | 如果 `SYN-RECV` 长时间异常增多,说明服务器收到了大量连接请求,但其中许多握手没有完成。这可能与 SYN flood、网络丢包或者 TCP 握手队列压力有关。 |
| | |
| “现在是否有大量 TCP 握手没有完成?”
| |
| | |
| 如果 `SYN-RECV` 持续很多,而 `ESTABLISHED` 不一定很多,更接近: | |
| | |
| * SYN flood;
| |
| * 对方大量发 SYN 后不完成握手;
| |
| * 线路丢包;
| |
| * 服务器握手队列或网络栈压力。
| |
| | |
| === 找出连接最多的来源 IP ===
| |
|
| |
|
| 推荐写法:
| | `watch` 本身不懂网络,它只是反复执行另一条命令。例如: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -Hnt state established '( sport = :443 )' | | | watch -n 1 "ss -Hnt state established '( sport = :443 )' | wc -l" |
| awk '{print $5}' |
| |
| sed -E 's/^\[?([^]]+)\]?:[0-9]+$/\1/' |
| |
| sort |
| |
| uniq -c |
| |
| sort -nr |
| |
| head
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 作用:
| | 这表示每秒重新统计一次已经建立的 HTTPS 连接。 |
|
| |
|
| 1. `ss` 找出本地 443 的已建立连接;
| | '''ss 负责观察,watch 负责持续观察。''' |
| 2. `awk '{print $5}'` 取出对方地址和端口;
| |
| 3. `sed` 去掉对方端口;
| |
| 4. `sort` 排序;
| |
| 5. `uniq -c` 合并相同 IP,并统计出现次数;
| |
| 6. `sort -nr` 按数字倒序;
| |
| 7. `head` 显示前十名。
| |
|
| |
|
| 各命令的英文概念:
| | == 管理名单与检查流量:ipset 和 iptables == |
|
| |
|
| {| class="wikitable"
| | `ipset` 是一个 IP 名单管理工具。它可以保存单个 IP,也可以保存整个网段。它本身不决定放行还是封禁,只负责维护名单。 |
| ! 命令
| |
| ! 英文概念
| |
|
| |
|
| | ! 作用 |
| | 例如,`mw_white` 可以保存白名单,`mw_ban` 可以保存黑名单。iptables 只需使用一条规则,便能查询整个名单,不必为每个 IP 分别建立一条防火墙规则。 |
| | ---------- |
| |
| | `awk` |
| |
| | 文本字段处理工具 |
| |
| | 取出指定列 |
| |
| | - |
| |
| | `sort` |
| |
| | sort |
| |
| | 排序 |
| |
| | - |
| |
| | `uniq` |
| |
| | unique |
| |
| | 合并相邻的重复行 |
| |
| | - |
| |
| | `uniq -c` |
| |
| | count |
| |
| | 统计每项出现次数 |
| |
| | - |
| |
| | `sort -n` |
| |
| | numeric |
| |
| | 按数字排序 |
| |
| | - |
| |
| | `sort -r` |
| |
| | reverse |
| |
| | 倒序 |
| |
| | - |
| |
| | `head` |
| |
| | head |
| |
| | 只显示最前面的若干行 |
| |
| | } |
| |
|
| |
|
| 原来的命令:
| | 向黑名单加入一个 IP: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -ant | awk '{print $5}' | cut -d: -f1 |
| | ipset add mw_ban 74.7.227.12 |
| sort | uniq -c | sort -nr | head
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 主要有三个问题:
| | 向黑名单加入整个 `/24` 网段: |
| | |
| * 没有限定本地 443;
| |
| * 没有限定 `ESTABLISHED`;
| |
| * `cut -d: -f1` 遇到 IPv6 地址会被冒号切坏。
| |
| | |
| === 确认 Apache 是否还在监听 443 ===
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -lntp '( sport = :443 )'
| | ipset add mw_ban 74.7.227.0/24 |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 参数:
| | 一个 `/24` 网段通常包含最多 256 个 IPv4 地址。封禁整个网段能够一次处理大量同源地址,但也比封禁单个 IP 更容易影响同一网段中的正常来源。 |
| | |
| * `-l`:`--listening`,只看监听 Socket;
| |
| * `-p`:`--processes`,显示占用 Socket 的进程。
| |
| | |
| 这条命令解决:
| |
| | |
| “Apache 究竟还在不在 443 端口上听连接?”
| |
| | |
| 如果完全没有输出,可能是:
| |
| | |
| * Apache 已经停止;
| |
| * TLS 虚拟主机没有成功加载;
| |
| * 443 被其他程序占用;
| |
| * Apache 启动失败。
| |
| | |
| == 攻击和资源耗尽的范畴 ==
| |
| | |
| 不要把所有“网站打不开”都叫作同一种攻击。
| |
| | |
| {| class="wikitable"
| |
| ! 范畴
| |
| ! 攻击或异常方式
| |
| ! 主要观察
| |
| | |
| | ! 主要防线 |
| |
| | ----------------------------- |
| |
| | 已知恶意来源 |
| |
| | 某些明确的 IP 或网段持续骚扰 |
| |
| | 来源 IP 固定 |
| |
| | ipset 黑名单 |
| |
| | - |
| |
| | 单 IP 并发耗尽 |
| |
| | 一个 IP 同时维持大量 TCP 连接 |
| |
| | `ESTABLISHED` 很多,某 IP 排名突出 |
| |
| | connlimit |
| |
| | - |
| |
| | 新连接洪水 |
| |
| | 一个 IP 不断建立短连接 |
| |
| | `NEW`、SYN、TIME-WAIT 增长 |
| |
| | hashlimit |
| |
| | - |
| |
| | SYN flood |
| |
| | 只发起握手,不完成握手 |
| |
| | `SYN-RECV` 激增 |
| |
| | 内核、云厂商、上游清洗 |
| |
| | - |
| |
| | 慢连接 |
| |
| | 很慢地发送 HTTP 头或请求体 |
| |
| | 连接存在很久,Apache worker 被占用 |
| |
| | RequestReadTimeout、Apache MPM |
| |
| | - |
| |
| | KeepAlive 滥用 |
| |
| | 建立连接后长时间闲置 |
| |
| | 大量空闲持久连接 |
| |
| | KeepAliveTimeout、event MPM |
| |
| | - |
| |
| | HTTP 请求洪水 |
| |
| | 在一个或少量连接中不停请求页面 |
| |
| | 请求数高,但 TCP 新连接未必多 |
| |
| | CDN、WAF、反向代理、HTTP 层限速 |
| |
| | - |
| |
| | 分布式 DDoS |
| |
| | 大量不同 IP 同时攻击 |
| |
| | 单 IP 都不突出,但总量巨大 |
| |
| | 云厂商、CDN、WAF、上游清洗 |
| |
| | } |
| |
| | |
| 尤其要记住:
| |
| | |
| `connlimit` 管“同时有多少条连接”。
| |
| | |
| `hashlimit` 管“单位时间来了多少个匹配的数据包或新连接”。
| |
| | |
| iptables 不负责统计“HTTP 页面请求次数”。
| |
| | |
| 在 HTTP/1.1 KeepAlive、HTTP/2 等情况下,同一条 TCP 连接可以承载多次请求,所以限制 TCP `NEW` 只能缓解连接洪水,不能完整阻止 HTTP flood。
| |
| | |
| == ipset:维护大批量 IP 名单 ==
| |
|
| |
|
| ipset 解决的痛点是: | | iptables 是真正执行网络判断的工具。进入服务器的数据包会按照规则的先后顺序接受检查。规则可以检查来源地址、目标端口、连接状态和 ipset 名单,然后决定继续检查、接受、返回上一层规则或者直接丢弃。 |
|
| |
|
| “如果有几百、几千、几万个恶意 IP,不能为每个 IP 写一条 iptables 规则。”
| | `DROP` 表示静默丢弃数据包。`RETURN` 表示结束当前自定义链,返回调用它的上一层链继续检查。 |
|
| |
|
| iptables 只需要写一条规则,去查询 ipset 名单。 | | `-I` 是插入规则,`-A` 是把规则追加到链尾。由于 iptables 从上到下检查规则,规则顺序会直接影响最终结果。 |
|
| |
|
| === 创建可保存 IP 和网段的黑名单 ===
| | 下面的命令可以查看 INPUT 链、规则编号和每条规则的命中次数: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ipset create banhash hash:net \
| | iptables -L INPUT -n -v --line-numbers |
| family inet \
| |
| hashsize 4096 \
| |
| maxelem 65536
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 各部分含义:
| | 规则左侧的数据包数量会随着命中而增加,因此可以据此判断具体是哪条规则正在发挥作用。 |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | -------------------- |
| |
| | `create` |
| |
| | create |
| |
| | 创建一个集合 |
| |
| | - |
| |
| | `banhash` |
| |
| | 自定义名称 |
| |
| | 该黑名单的名字 |
| |
| | - |
| |
| | `hash:net` |
| |
| | hashed networks |
| |
| | 用哈希结构保存 IP 或 CIDR 网段 |
| |
| | - |
| |
| | `family inet` |
| |
| | address family IPv4 |
| |
| | 只保存 IPv4 地址 |
| |
| | - |
| |
| | `hashsize 4096` |
| |
| | hash table size |
| |
| | 初始哈希桶数量,属于性能参数 |
| |
| | - |
| |
| | `maxelem 65536` |
| |
| | maximum elements |
| |
| | 最多允许保存 65536 个条目 |
| |
| | } |
| |
| | |
| `hash:net` 可以同时保存:
| |
| | |
| 74.7.227.12
| |
| 74.7.227.0/24
| |
| 10.0.0.0/8
| |
|
| |
|
| 它适合保存大小不一的网段。
| | '''ipset 是名单册,iptables 是按照名单和条件执行判断的门卫。''' |
|
| |
|
| 如果脚本可能被重复运行,可以使用:
| | == 两种核心限制:connlimit 和 hashlimit == |
|
| |
|
| <syntaxhighlight lang="bash">
| | `connlimit` 解决的是并发连接问题。它关心的不是某个 IP 一天访问了多少次,而是这个 IP 此刻同时保持着多少条 TCP 连接。 |
| ipset create banhash hash:net \
| |
| family inet \
| |
| hashsize 4096 \
| |
| maxelem 65536 \
| |
| -exist
| |
| </syntaxhighlight>
| |
|
| |
|
| `-exist` 的作用是:集合已经存在时,不把它当作错误。
| | 如果一个来源 IP 同时保持几十甚至几百条连接,它可能持续占用服务器的 Socket、Apache 进程或工作线程。connlimit 可以在连接数量超过阈值以后,拒绝这个来源继续建立新连接。 |
|
| |
|
| === 添加单个 IP ===
| | '''connlimit 限制的是一个 IP 同时占用多少个座位。''' |
|
| |
|
| <syntaxhighlight lang="bash">
| | `hashlimit` 解决的是新连接速度问题。有些异常流量不会长期保持大量连接,而是不断重复建立连接、关闭连接、再次建立连接。即使同时存在的连接数量不高,这种行为仍然会反复消耗 TCP 握手、TLS 握手和 Apache 资源。 |
| ipset add banhash 74.7.227.12
| |
| </syntaxhighlight>
| |
|
| |
|
| === 添加整个 /24 网段 ===
| | 使用 `--hashlimit-mode srcip` 时,每个来源 IP 都会单独计算速度。某个 IP 超过速率,只会影响这个 IP,不会让所有访问者共同争抢一个总额度。 |
|
| |
|
| <syntaxhighlight lang="bash">
| | '''hashlimit 限制的是一个 IP 冲进大门的速度。''' |
| ipset add banhash 74.7.227.0/24
| |
| </syntaxhighlight>
| |
|
| |
|
| `/24` 表示前 24 位是网络部分,通常覆盖:
| | hashlimit 看到的是 TCP 新连接,而不是 MediaWiki 页面请求。一个已经建立的 KeepAlive 连接可以连续发送多个 HTTP 请求,不会因为每次请求页面而重新进入 TCP 新连接状态。 |
|
| |
|
| 74.7.227.0 ~ 74.7.227.255
| | 因此,connlimit 和 hashlimit 主要处理连接层面的异常。真正的 HTTP 请求洪水还需要 CDN、WAF、反向代理或者 Apache 应用层规则处理。 |
|
| |
|
| 这不是“封一个 IP”,而是封整个网段。
| | == 最小防护规则 == |
|
| |
|
| 必须确认该网段确实大量恶意,避免误伤同一网段内的正常用户、搜索引擎、代理或云服务。
| | 为了避免把新增规则散落在原有 INPUT 链中,可以建立一条独立的自定义链。所有网站连接限制都集中放在这条链里,撤销时也只需要删除这一条入口和自定义链。 |
|
| |
|
| === 让 iptables 使用该黑名单 ===
| | 应用规则前,可以把当前状态保存为文本备份: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| iptables -I INPUT \ | | # 保存当前 iptables 规则 |
| -m set \
| | iptables-save > /root/iptables-before-web-guard.v4 |
| --match-set banhash src \
| |
| -j DROP
| |
| </syntaxhighlight>
| |
| | |
| 该规则解决的需求是:
| |
| | |
| “只要来源地址存在于 banhash,就在进入 Apache 以前直接丢弃。”
| |
| | |
| 参数含义:
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
|
| |
|
| | ! 含义 |
| | # 保存当前 ipset 名单 |
| | --------------------- |
| | ipset save > /root/ipset-before-web-guard.set |
| | `-I` |
| |
| | `--insert` |
| |
| | 把规则插入链中;未写序号时默认插到第一条 |
| |
| | - |
| |
| | `INPUT` |
| |
| | input chain |
| |
| | 处理进入本机服务的包 |
| |
| | - |
| |
| | `-m set` |
| |
| | match set |
| |
| | 加载 ipset 匹配模块 |
| |
| | - |
| |
| | `--match-set banhash` |
| |
| | match named set |
| |
| | 查询名为 banhash 的集合 |
| |
| | - |
| |
| | `src` |
| |
| | source |
| |
| | 使用数据包的来源地址进行匹配 |
| |
| | - |
| |
| | `-j` |
| |
| | `--jump` |
| |
| | 匹配成功后跳转到指定动作 |
| |
| | - |
| |
| | `DROP` |
| |
| | drop |
| |
| | 静默丢弃数据包 |
| |
| | } |
| |
| | |
| iptables 的规则按顺序检查;`-I` 默认插到链首,`-A` 则追加到链尾。匹配到 `ACCEPT` 或 `DROP` 后,就不会再继续检查后面的普通规则。
| |
| | |
| 因此不能机械地认为“插到第一条永远最好”。
| |
| | |
| 例如已有:
| |
| | |
| * 管理员白名单;
| |
| * 腾讯云安全链;
| |
| * SSH 防锁死规则;
| |
| * 其他必要的 ACCEPT 规则;
| |
| | |
| 就要先设计清楚顺序。
| |
| | |
| 查看现有顺序:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| iptables -L INPUT -n -v --line-numbers
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| === 正确统计 ipset 中的成员数量 ===
| | 建立独立的网站防护链: |
| | |
| 原命令:
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ipset list banhash | wc -l
| | # 如果防护链不存在,就创建它 |
| </syntaxhighlight>
| | iptables -nL MW-WEB-GUARD >/dev/null 2>&1 || \ |
| | | iptables -N MW-WEB-GUARD |
| 统计的是输出总行数,其中还包含:
| |
| | |
| * Name;
| |
| * Type;
| |
| * Revision;
| |
| * Header;
| |
| * Members;
| |
| * 其他说明行。
| |
| | |
| 它不是准确的成员数。
| |
|
| |
|
| 更合适的写法:
| | # 清空防护链中的旧测试规则 |
| | | iptables -F MW-WEB-GUARD |
| <syntaxhighlight lang="bash">
| |
| ipset save banhash | grep -c '^add banhash '
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 动态监控:
| | 白名单来源跳过本链中的连接限制,但仍会返回 INPUT 链,继续接受服务器原有规则的检查: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| watch -n 1 \
| | # 白名单跳过本链中的并发和速率限制 |
| "ipset save banhash | grep -c '^add banhash '"
| | iptables -A MW-WEB-GUARD \ |
| | -m set --match-set mw_white src \ |
| | -j RETURN |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| == connlimit:限制单个 IP 的并发连接数 ==
| | 限制单个 IPv4 地址同时占用过多网站连接: |
| | |
| 推荐按“建立连接时”检查:
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| iptables -A INPUT \ | | # 单个 IPv4 地址同时超过 40 条网站连接时,拒绝继续建立新连接 |
| -p tcp \ | | iptables -A MW-WEB-GUARD \ |
| --syn \
| | -p tcp --syn \ |
| --dport 443 \
| |
| -m connlimit \ | | -m connlimit \ |
| --connlimit-above 30 \ | | --connlimit-saddr \ |
| | --connlimit-above 40 \ |
| --connlimit-mask 32 \ | | --connlimit-mask 32 \ |
| -j DROP | | -j DROP |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 该规则解决的痛点是:
| | 这条规则只在对方建立新连接时检查。`--connlimit-saddr` 表示按照来源地址统计,`--connlimit-mask 32` 表示每个 IPv4 地址单独计算。 |
| | |
| “同一个来源 IP 同时维持大量 HTTPS 连接,占满 Apache 或服务器连接资源。”
| |
| | |
| 各部分含义:
| |
|
| |
|
| {| class="wikitable"
| | 限制单个 IPv4 地址持续高速建立新连接: |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | ---------------------- |
| |
| | `-p tcp` |
| |
| | protocol TCP |
| |
| | 只匹配 TCP |
| |
| | - |
| |
| | `--syn` |
| |
| | synchronize flag |
| |
| | 只匹配建立 TCP 连接时的 SYN 包 |
| |
| | - |
| |
| | `--dport 443` |
| |
| | destination port |
| |
| | 服务器目标端口为 443 |
| |
| | - |
| |
| | `-m connlimit` |
| |
| | connection limit |
| |
| | 加载并发连接限制模块 |
| |
| | - |
| |
| | `--connlimit-above 30` |
| |
| | connection limit above |
| |
| | 已有连接数量超过 30 时匹配 |
| |
| | - |
| |
| | `--connlimit-mask 32` |
| |
| | source grouping mask |
| |
| | IPv4 按单个 IP 分组 |
| |
| | - |
| |
| | `-j DROP` |
| |
| | drop |
| |
| | 丢弃超限的新连接 |
| |
| | } |
| |
| | |
| connlimit 官方定义就是按客户端 IP 或客户端地址块限制并行连接数量;IPv4 未指定 mask 时默认也是最完整的主机前缀,即按单 IP,但显式写 `32` 更容易理解。
| |
| | |
| 需要注意:
| |
| | |
| * 它限制的是 TCP 并发连接,不是 HTTP 请求;
| |
| * 公司、学校、移动网络可能有许多正常用户共享同一个公网 IP;
| |
| * 若服务器位于 CDN 或反向代理之后,服务器可能只看见 CDN 节点 IP;
| |
| * 阈值 20、30 并不是通用真理,应根据正常访问峰值确定。
| |
| | |
| 所以:
| |
| | |
| `--connlimit-above 30`
| |
| | |
| 比较准确的解释是:
| |
| | |
| “如果这个来源 IP 当前已经拥有超过 30 条被 conntrack 统计的并行连接,则匹配并丢弃新的 SYN。”
| |
| | |
| 不能简单理解为“任何时候都绝对只允许 30 个用户”。
| |
| | |
| == hashlimit:按 IP 限制新连接速率 ==
| |
| | |
| 原笔记中的这类规则,方向是正确的:
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| iptables -I INPUT \ | | # 单个 IPv4 地址持续超过每秒 10 个新连接时,丢弃超出的连接 |
| -p tcp \ | | iptables -A MW-WEB-GUARD \ |
| --dport 443 \
| | -p tcp --syn \ |
| -m conntrack \
| |
| --ctstate NEW \
| |
| -m hashlimit \ | | -m hashlimit \ |
| --hashlimit-name HTTPS \ | | --hashlimit-name mw_web_new \ |
| --hashlimit-above 30/minute \
| |
| --hashlimit-burst 20 \
| |
| --hashlimit-mode srcip \ | | --hashlimit-mode srcip \ |
| | --hashlimit-above 10/second \ |
| | --hashlimit-burst 30 \ |
| --hashlimit-htable-expire 600000 \ | | --hashlimit-htable-expire 600000 \ |
| -j DROP | | -j DROP |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 它解决的痛点是:
| | `10/second` 是持续速率,`burst 30` 用来容忍短时间的正常突发。该规则只丢弃超速期间的新连接,不会把来源 IP 永久加入黑名单。长期不活跃的速率记录会在十分钟后清理。 |
| | |
| “某个来源 IP 不维持大量长连接,而是不断快速建立新连接。”
| |
| | |
| === 参数解释 ===
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | ---------------------------------- |
| |
| | `-m conntrack` |
| |
| | connection tracking |
| |
| | 使用内核连接跟踪状态 |
| |
| | - |
| |
| | `--ctstate NEW` |
| |
| | connection state NEW |
| |
| | 匹配尚未完成双向通信的新连接状态 |
| |
| | - |
| |
| | `-m hashlimit` |
| |
| | hash-based rate limit |
| |
| | 使用哈希表按某个维度分别计速 |
| |
| | - |
| |
| | `--hashlimit-name HTTPS` |
| |
| | hashlimit table name |
| |
| | 给该统计表起名 |
| |
| | - |
| |
| | `--hashlimit-above` |
| |
| | rate above |
| |
| | 速率超过阈值时才匹配 |
| |
| | - |
| |
| | `--hashlimit-burst 20` |
| |
| | burst capacity |
| |
| | 允许一定程度的瞬时突发 |
| |
| | - |
| |
| | `--hashlimit-mode srcip` |
| |
| | source IP mode |
| |
| | 每个来源 IP 单独统计 |
| |
| | - |
| |
| | `--hashlimit-htable-expire 600000` |
| |
| | hash table entry expiry |
| |
| | 600000 毫秒,即十分钟后清理长期不活跃的统计条目 |
| |
| | - |
| |
| | `-j DROP` |
| |
| | drop |
| |
| | 只丢弃超出限制的匹配包 |
| |
| | } |
| |
| | |
| hashlimit 可以按来源 IP、来源端口、目标 IP、目标端口分别建立速率桶;`srcip` 才是“每个来源 IP 各算各的”。
| |
| | |
| === 令牌桶的直观理解 ===
| |
| | |
| 可以把每个 IP 想象成有一个水桶:
| |
| | |
| * `--hashlimit-burst 20`:桶最多暂存 20 个令牌;
| |
| * 正常速率会不断补充令牌;
| |
| * 新连接消耗令牌;
| |
| * 短时间突然来几次,可以使用桶里积攒的令牌;
| |
| * 长期超过平均速率,桶会耗尽,后续包便匹配 `--hashlimit-above`。
| |
| | |
| 因此 burst 不是:
| |
| | |
| “超过 20 就永久封禁。”
| |
| | |
| 而是:
| |
| | |
| “允许短时间突发,但不允许长期持续超速。”
| |
|
| |
|
| === 30/minute 可能过于严格 ===
| | 让进入 80 和 443 端口的 TCP 流量经过自定义防护链: |
| | |
| `30/minute` 等于平均每两秒一个新连接。
| |
| | |
| 现代网页可能同时加载:
| |
| | |
| * HTML;
| |
| * CSS;
| |
| * JavaScript;
| |
| * 图片;
| |
| * API;
| |
| * 字体;
| |
| * 多个域名资源。
| |
| | |
| 虽然 KeepAlive 会减少新连接,但移动网络重连、共享 NAT、爬虫和浏览器并发仍可能触发较低阈值。
| |
| | |
| 所以 `30/minute` 更适合作为一个需要验证的实验参数,而不是默认安全值。
| |
| | |
| 应先观察正常峰值,再确定:
| |
| | |
| * 每秒还是每分钟;
| |
| * 平均速率;
| |
| * burst;
| |
| * 是否按单 IP、网段或全局统计。
| |
| | |
| === NEW 不等于 HTTP 请求 ===
| |
| | |
| conntrack 中:
| |
| | |
| * `NEW`:连接尚未看到双向数据,或者刚开始建立;
| |
| * `ESTABLISHED`:连接已经看到双向数据。
| |
| | |
| 所以:
| |
| | |
| `--ctstate NEW`
| |
| | |
| 限制的是新 TCP 连接相关的数据包,不是:
| |
| | |
| GET /wiki/Page
| |
| POST /api.php
| |
| GET /load.php
| |
| | |
| 一个 `ESTABLISHED` TCP KeepAlive 连接,可以继续发送很多 HTTP 请求,而不会再次进入 `NEW`。
| |
| | |
| 因此原笔记里的:
| |
| | |
| NEW limit → 防 HTTP flood
| |
| | |
| 应改为:
| |
| | |
| NEW/SYN rate limit → 防新连接洪水、短连接洪水
| |
| | |
| 真正按 HTTP URL 或请求次数限速,应在:
| |
| | |
| * CDN;
| |
| * WAF;
| |
| * 反向代理;
| |
| * Apache 模块;
| |
| * 应用层;
| |
| | |
| 完成。
| |
| | |
| == limit:全局限速,不是单 IP 限速 ==
| |
| | |
| 原规则:
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| iptables -I INPUT \ | | # 如果入口规则不存在,就把 80、443 流量送入网站防护链 |
| | iptables -C INPUT \ |
| -p tcp \ | | -p tcp \ |
| --dport 443 \
| | -m multiport --dports 80,443 \ |
| -m conntrack \ | | -j MW-WEB-GUARD 2>/dev/null || \ |
| --ctstate NEW \
| | iptables -I INPUT 1 \ |
| -m limit \ | |
| --limit 30/second \
| |
| --limit-burst 60 \
| |
| -j ACCEPT
| |
| | |
| iptables -A INPUT
| |
| -p tcp
| |
| --dport 443
| |
| -m conntrack
| |
| --ctstate NEW
| |
| -j DROP </syntaxhighlight>
| |
| | |
| 它表达的不是:
| |
| | |
| “每个 IP 每秒最多 30 个新连接。”
| |
| | |
| 而是:
| |
| | |
| “整个服务器的这条规则,所有来源 IP 加起来,平均每秒允许 30 个匹配包,突发最多 60;其余新连接全部丢弃。”
| |
| | |
| `limit` 使用的是单个共享令牌桶。官方将它定义为按有限平均速率进行匹配;`hashlimit` 才能按来源 IP 等维度分组。
| |
| | |
| 这类规则的痛点是:
| |
| | |
| “服务器过载时,无论是谁都不再接受超过全局上限的新连接。”
| |
| | |
| 它属于全局熔断器,而不是精细反滥用规则。
| |
| | |
| 风险包括:
| |
| | |
| * 所有正常用户共用同一个额度;
| |
| * 攻击者可以消耗掉正常用户的额度;
| |
| * `ACCEPT` 会立即终止当前链的普通检查,可能绕过后续规则;
| |
| * `-I` 和 `-A` 混用后,实际顺序容易与想象不同;
| |
| * 如果现有 INPUT 链还有其他规则,盲目追加最终 DROP 很危险。
| |
| | |
| 所以这组规则不应作为日常核心规则,建议从主笔记中删除,只保留其概念:
| |
| | |
| limit = 全局速率桶
| |
| | |
| hashlimit = 可按来源 IP 分组的速率桶
| |
| | |
| == SYN limit:限制整个服务器的握手速率 ==
| |
| | |
| 原规则:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| iptables -I INPUT \ | |
| -p tcp \ | | -p tcp \ |
| --syn \
| | -m multiport --dports 80,443 \ |
| --dport 443 \
| | -j MW-WEB-GUARD |
| -m limit \ | |
| --limit 50/second \
| |
| --limit-burst 100 \
| |
| -j ACCEPT
| |
| | |
| iptables -A INPUT
| |
| -p tcp
| |
| --syn
| |
| --dport 443
| |
| -j DROP </syntaxhighlight>
| |
| | |
| 它解决的需求是:
| |
| | |
| “整个服务器每秒最多接收一定数量的 TCP 建连 SYN,超出的全部丢弃。”
| |
| | |
| 注意:
| |
| | |
| * 它是全局限制;
| |
| * 不区分正常 IP 和攻击 IP;
| |
| * 它与全局 `NEW limit` 高度重复;
| |
| * SYN flood 若已经占满服务器外部带宽,本机 iptables 再丢包也无法疏通上游线路;
| |
| * 分布式攻击需要云厂商、CDN、WAF 或上游清洗。
| |
| | |
| 因此不建议同时堆叠:
| |
| | |
| * 全局 SYN limit;
| |
| * 全局 NEW limit;
| |
| * per-IP hashlimit;
| |
| * recent hitcount;
| |
| | |
| 否则自己很难知道究竟是哪条规则造成误伤。
| |
| | |
| 日常基础防护中,更容易理解的组合是:
| |
| | |
| # 已知坏 IP:ipset
| |
| | |
| # 单 IP 并发:connlimit
| |
| | |
| # 单 IP 新连接速率:hashlimit
| |
| | |
| 全局 SYN 限速只作为经过正常流量基线验证后的额外熔断方案。
| |
| | |
| == recent:滚动记录最近命中的 IP ==
| |
| | |
| 原规则:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| iptables -I INPUT \
| |
| -p tcp \
| |
| --dport 443 \
| |
| -m recent \ | |
| --set
| |
| | |
| iptables -I INPUT
| |
| -p tcp
| |
| --dport 443
| |
| -m recent
| |
| --update
| |
| --seconds 1
| |
| --hitcount 20
| |
| -j DROP </syntaxhighlight> | |
| | |
| `recent` 模块解决的痛点是:
| |
| | |
| “动态记住最近出现过的来源 IP,并按最近若干秒内的命中次数进行判断。”
| |
| | |
| 参数:
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | -------------------------- |
| |
| | `--set` |
| |
| | set |
| |
| | 把来源 IP 加入 recent 名单,已存在则更新 |
| |
| | - |
| |
| | `--update` |
| |
| | update |
| |
| | 检查名单,并更新最后出现时间 |
| |
| | - |
| |
| | `--seconds 1` |
| |
| | seconds |
| |
| | 只考虑最近一秒 |
| |
| | - |
| |
| | `--hitcount 20` |
| |
| | hit count |
| |
| | 命中次数达到 20 次时匹配 |
| |
| | } |
| |
| | |
| recent 可以维护动态 IP 列表,并结合 `seconds`、`hitcount` 判断最近时间窗口内的命中次数。
| |
| | |
| 但原规则有两个严重的理解问题。
| |
| | |
| 第一,它没有写:
| |
| | |
| --syn
| |
| | |
| 也没有写:
| |
| | |
| --ctstate NEW
| |
| | |
| 所以它可能记录的是到达 443 的 TCP 数据包,而不是“HTTP 访问次数”或“新连接次数”。
| |
| | |
| 正常传输一个网页,本身就会产生许多 TCP 包,很容易超过 20。
| |
| | |
| 第二,连续使用 `-I` 时,后执行的规则会插到前面。命令书写顺序和最终规则顺序正好相反,维护起来很容易迷糊。
| |
| | |
| 这类需求已经可以由:
| |
| | |
| hashlimit --hashlimit-mode srcip
| |
| | |
| 更直观地解决。
| |
| | |
| 因此建议把 recent 从核心防护笔记中删除。等将来明确需要“动态记名单”而不仅是“丢弃超速包”时,再单独研究。
| |
| | |
| == 去除重复后,iptables 防护只保留三类概念 ==
| |
| | |
| === 第一层:已知黑名单 ===
| |
| | |
| 解决:
| |
| | |
| “这个 IP 或网段已经确认恶意,不必再给它机会。”
| |
| | |
| <syntaxhighlight lang="bash">
| |
| -m set --match-set banhash src -j DROP
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 核心工具:
| | `iptables -C` 用来检查入口规则是否已经存在。只有规则不存在时,后面的插入命令才会执行,因此重复运行不会不断产生相同入口。 |
|
| |
|
| ipset
| | 流量进入 `MW-WEB-GUARD` 后,白名单会先返回原 INPUT 链。其他来源依次检查并发连接数和建立新连接的速度。没有超限的流量走到自定义链末尾时,也会自动返回 INPUT 链,继续经过原有的腾讯云规则、黑名单规则和其他防火墙规则。 |
|
| |
|
| === 第二层:单 IP 并发限制 ===
| | 这套命令使用的是 IPv4 的 `iptables`。如果服务器同时通过 IPv6 对外提供网站服务,IPv6 流量需要由 `ip6tables`、nftables 或相应的 IPv6 防火墙规则处理。 |
|
| |
|
| 解决:
| | == 查看命中与撤销规则 == |
|
| |
|
| “一个 IP 同时占着太多连接不放。”
| | 下面的命令显示自定义防护链及其命中次数: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| -p tcp --syn --dport 443 \ | | iptables -L MW-WEB-GUARD -n -v --line-numbers |
| -m connlimit \ | |
| --connlimit-above N \ | |
| --connlimit-mask 32 \
| |
| -j DROP
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 核心工具:
| | 如果 connlimit 或 hashlimit 规则前面的数据包计数持续增加,说明相应规则正在丢弃超限的新连接。 |
| | |
| connlimit | |
| | |
| `N` 必须根据正常流量确定。
| |
| | |
| === 第三层:单 IP 新连接速率限制 ===
| |
|
| |
|
| 解决:
| | 下面的命令显示 INPUT 链中的入口: |
| | |
| “一个 IP 不保持连接,而是不断创建短连接。”
| |
| | |
| <syntaxhighlight lang="bash">
| |
| -p tcp --syn --dport 443 \
| |
| -m hashlimit \
| |
| --hashlimit-name HTTPS_NEW \
| |
| --hashlimit-mode srcip \
| |
| --hashlimit-above R/second \
| |
| --hashlimit-burst B \
| |
| --hashlimit-htable-expire 600000 \
| |
| -j DROP
| |
| </syntaxhighlight>
| |
| | |
| 核心工具:
| |
| | |
| hashlimit
| |
| | |
| `R` 是平均速率,`B` 是允许的瞬时突发。
| |
| | |
| === 暂时从核心规则中删除 ===
| |
| | |
| * 全局 `limit ACCEPT` 加最终 `DROP`;
| |
| * 全局 SYN limit;
| |
| * 全局 NEW limit;
| |
| * recent hitcount。
| |
| | |
| 不是因为这些模块完全无用,而是因为它们:
| |
| | |
| * 与现有规则重复;
| |
| * 更容易误伤;
| |
| * 依赖准确的流量基线;
| |
| * 增加规则顺序的理解难度。
| |
| | |
| == Apache 层:iptables 无法解决的资源耗尽 ==
| |
| | |
| === 重启 Apache 后立刻恢复说明什么 ===
| |
| | |
| 这种现象比较符合:
| |
| | |
| * Apache worker 或 thread 被占满;
| |
| * 大量连接被 Apache 进程持有;
| |
| * 某些进程卡住;
| |
| * 请求队列积压;
| |
| * 重启后旧连接和进程状态被清空。
| |
| | |
| 但它不是绝对证明。
| |
| | |
| 要结合以下信息判断:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ss -Hnt state established '( sport = :443 )' | wc -l
| |
| ss -Hnt state syn-recv '( sport = :443 )' | wc -l
| |
| apache2ctl -M | grep mpm
| |
| systemctl status apache2
| |
| </syntaxhighlight>
| |
| | |
| === KeepAlive 不是应该关闭的坏东西 ===
| |
| | |
| KeepAlive 的作用是:
| |
| | |
| “让一个 TCP 连接承载多个 HTTP 请求,避免每张图片、每个脚本都重新握手和重新进行 TLS。”
| |
| | |
| Apache 默认:
| |
| | |
| KeepAlive On
| |
| KeepAliveTimeout 5
| |
| MaxKeepAliveRequests 100
| |
| | |
| 较高的 `KeepAliveTimeout` 会让更多服务器进程或线程等待空闲客户端;Apache 官方也建议遭遇 DoS 风险时适当降低该值。
| |
| | |
| 较保守的起点可以是:
| |
| | |
| <syntaxhighlight lang="apache">
| |
| KeepAlive On
| |
| KeepAliveTimeout 2
| |
| MaxKeepAliveRequests 100
| |
| </syntaxhighlight>
| |
| | |
| 其中:
| |
| | |
| * `KeepAlive On`:保留正常性能优势;
| |
| * `KeepAliveTimeout 2`:空闲两秒后关闭持久连接;
| |
| * `MaxKeepAliveRequests 100`:每条持久连接最多处理 100 个请求。
| |
| | |
| 原笔记建议:
| |
| | |
| MaxKeepAliveRequests 50
| |
| | |
| 不一定比 100 更安全。
| |
| | |
| Apache 官方反而建议该值保持较高,以取得较好的性能;设置过低会增加 TCP 和 TLS 重新建连次数。
| |
| | |
| 所以核心防护重点应放在:
| |
| | |
| KeepAliveTimeout
| |
| | |
| 而不是盲目压低:
| |
| | |
| MaxKeepAliveRequests
| |
| | |
| === 慢速发送请求应由 RequestReadTimeout 处理 ===
| |
| | |
| 慢连接攻击不一定是“连接后保持空闲”,也可能是:
| |
| | |
| * 每隔几秒发送一个 HTTP 头字符;
| |
| * 很慢地上传请求体;
| |
| * 让 Apache 一直等待一个永远发不完的请求。
| |
| | |
| 这类问题应由 Apache 的:
| |
| | |
| RequestReadTimeout
| |
| | |
| 限制客户端发送请求头和请求体的时间及最低速率。Apache 官方将其列为缓解 DoS 的重要指令。
| |
| | |
| === Apache MPM 比单纯 KeepAlive 更重要 ===
| |
| | |
| 查看当前 MPM:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| apache2ctl -M | grep mpm
| |
| </syntaxhighlight>
| |
| | |
| 可能看到:
| |
| | |
| mpm_prefork_module
| |
| mpm_worker_module
| |
| mpm_event_module
| |
| | |
| 概念区别:
| |
| | |
| {| class="wikitable"
| |
| ! MPM
| |
| ! 模型
| |
| | |
| | ! 面对大量连接时的特点 |
| |
| | ------------------------------ |
| |
| | prefork |
| |
| | 每个进程一次处理一个连接 |
| |
| | 内存消耗较大 |
| |
| | - |
| |
| | worker |
| |
| | 每个进程包含多个线程 |
| |
| | 比 prefork 更节省内存 |
| |
| | - |
| |
| | event |
| |
| | 在 worker 基础上,更专门处理空闲 KeepAlive |
| |
| | 不必让主工作线程一直等待空闲连接 |
| |
| | } |
| |
| | |
| Apache 官方说明,event MPM 使用异步方式处理部分连接工作,避免每条空闲连接长期占用一个工作线程。
| |
| | |
| === MaxRequestWorkers 是 Apache 的总容量 ===
| |
| | |
| `MaxRequestWorkers` 决定 Apache 同时允许多少请求进入处理状态。
| |
| | |
| 它过低:
| |
| | |
| * 正常峰值也容易排队;
| |
| * 攻击者更容易占满。
| |
| | |
| 它过高:
| |
| | |
| * 可能耗尽内存;
| |
| * PHP、数据库也可能被拖垮。
| |
| | |
| Apache 官方建议根据服务器资源调整它,使服务器能够处理足够并发,又不至于耗尽资源。
| |
| | |
| == 压力测试工具 ==
| |
| | |
| 压力测试解决的痛点是:
| |
| | |
| “规则和 Apache 配置修改后,服务器在可控负载下究竟会发生什么?”
| |
| | |
| 只能对自己拥有或明确获准测试的服务器进行。
| |
| | |
| === ab:固定请求总数和并发数 ===
| |
| | |
| 安装:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| apt install apache2-utils
| |
| </syntaxhighlight>
| |
| | |
| 测试:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ab -n 5000 -c 200 https://qingliezhiquan.com/
| |
| </syntaxhighlight>
| |
| | |
| 参数:
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | ------------------ |
| |
| | `-n 5000` |
| |
| | number of requests |
| |
| | 总共发送 5000 个请求 |
| |
| | - |
| |
| | `-c 200` |
| |
| | concurrency |
| |
| | 同时最多进行 200 个请求 |
| |
| | } |
| |
| | |
| Apache 官方将 ab 定义为 HTTP 服务器基准测试工具,用于观察服务器每秒能够处理多少请求。
| |
| | |
| 特别注意:
| |
| | |
| ab 默认不开启 HTTP KeepAlive。
| |
| | |
| 所以:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ab -n 5000 -c 200 https://qingliezhiquan.com/
| |
| </syntaxhighlight>
| |
| | |
| 更偏向测试不断发请求和建立连接的情况。
| |
| | |
| 测试 KeepAlive:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ab -k -n 5000 -c 200 https://qingliezhiquan.com/
| |
| </syntaxhighlight>
| |
| | |
| `-k` 在 ab 中表示:
| |
| | |
| keep alive
| |
| | |
| 即复用 HTTP 连接。
| |
| | |
| === wrk:持续一段时间维持大量连接 ===
| |
| | |
| 安装:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| apt install wrk
| |
| </syntaxhighlight>
| |
| | |
| 测试:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| wrk -t8 -c200 -d30s https://qingliezhiquan.com/
| |
| </syntaxhighlight>
| |
| | |
| 参数:
| |
| | |
| {| class="wikitable"
| |
| ! 参数
| |
| ! 英文
| |
| | |
| | ! 含义 |
| |
| | ------------------ |
| |
| | `-t8` |
| |
| | threads |
| |
| | 客户端使用 8 个测试线程 |
| |
| | - |
| |
| | `-c200` |
| |
| | connections |
| |
| | 总共保持 200 条 HTTP 连接 |
| |
| | - |
| |
| | `-d30s` |
| |
| | duration |
| |
| | 持续测试 30 秒 |
| |
| | } |
| |
| | |
| wrk 的官方说明中,`connections` 是测试期间保持打开的总 HTTP 连接数,连接会分配给各测试线程。
| |
| | |
| 它比 ab 更适合观察:
| |
| | |
| * 持续吞吐量;
| |
| * 延迟;
| |
| * 大量连接长期存在时的表现;
| |
| * Apache KeepAlive 和 worker 压力。
| |
| | |
| === curl 循环:粗略制造一批独立请求 ===
| |
| | |
| <syntaxhighlight lang="bash">
| |
| for i in {1..100}; do
| |
| curl -k https://qingliezhiquan.com/ &
| |
| done
| |
| wait
| |
| </syntaxhighlight>
| |
| | |
| 作用:
| |
| | |
| * 创建 100 个后台 curl 进程;
| |
| * 每个进程请求一次;
| |
| * `&` 放入后台;
| |
| * `wait` 等待所有后台进程结束。
| |
| | |
| 需要纠正:
| |
| | |
| curl 的 `-k` 不是 KeepAlive。
| |
| | |
| 它表示:
| |
| | |
| --insecure
| |
| | |
| 即不验证 HTTPS 证书。
| |
| | |
| 所以这只是一个粗糙的并发冒烟测试,不是精确压力测试,也不能准确模拟慢连接攻击。
| |
| | |
| === 如何判断防护是否有效 ===
| |
| | |
| 不能只看:
| |
| | |
| failed requests
| |
| connection reset
| |
| | |
| 因为这些现象也可能意味着规则过于严格,误伤了正常请求。
| |
| | |
| 应同时观察:
| |
| | |
| <syntaxhighlight lang="bash">
| |
| watch -n 1 \
| |
| "ss -Hnt state established '( sport = :443 )' | wc -l"
| |
| </syntaxhighlight>
| |
| | |
| <syntaxhighlight lang="bash">
| |
| watch -n 1 \
| |
| "ss -Hnt state syn-recv '( sport = :443 )' | wc -l"
| |
| </syntaxhighlight>
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| Line 1,406: |
Line 203: |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 以及:
| | INPUT 链中应当存在一条把 80、443 端口流量送入 `MW-WEB-GUARD` 的规则。 |
| | |
| * 网站是否仍可正常打开;
| |
| * 正常请求延迟是否上升;
| |
| * Apache 是否仍有可用 worker;
| |
| * CPU、内存是否耗尽;
| |
| * iptables 对应规则的包计数是否增长;
| |
| * 是否出现大量正常用户误伤。
| |
| | |
| 真正的目标是:
| |
| | |
| “在恶意或异常负载出现时,服务器仍能为正常用户保留服务能力。”
| |
| | |
| 不是单纯追求:
| |
| | |
| “压力测试出现越多失败越好。”
| |
| | |
| == 网站异常时的排查顺序 ==
| |
|
| |
|
| === 第一步:确认服务是否还在监听 ===
| | 压力测试可以使用 Apache 自带的 `ab` 工具。例如: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -lntp '( sport = :443 )'
| | ab -n 500 -c 50 https://qingliezhiquan.com/ |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 没有监听,先排查 Apache。
| | 这表示总共发送 500 个请求,同时保持最多 50 个并发请求。由于 ab 默认会频繁建立新连接,它可能同时触发 connlimit 和 hashlimit。 |
| | |
| === 第二步:查看全局 Socket 概况 ===
| |
| | |
| <syntaxhighlight lang="bash">
| |
| ss -s
| |
| </syntaxhighlight>
| |
|
| |
|
| 确认是否有异常数量的 Socket。
| | 如果压力测试的来源 IP 存在于 `mw_white` 中,该来源会跳过本链限制,因此不会验证这两条规则。 |
|
| |
|
| === 第三步:按状态拆分 443 连接 ===
| | 撤销规则时,先删除 INPUT 链中的入口: |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -Hnt '( sport = :443 )' |
| | # 删除 INPUT 通往网站防护链的入口 |
| awk '{print $1}' |
| | iptables -D INPUT \ |
| sort |
| | -p tcp \ |
| uniq -c |
| | -m multiport --dports 80,443 \ |
| sort -nr
| | -j MW-WEB-GUARD |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 重点区分:
| | 然后清空并删除自定义链: |
| | |
| * `ESTABLISHED` 很多;
| |
| * `SYN-RECV` 很多;
| |
| * `TIME-WAIT` 很多;
| |
| * `CLOSE-WAIT` 很多。
| |
| | |
| === 第四步:找连接最多的来源 IP ===
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ss -Hnt state established '( sport = :443 )' |
| | # 清空网站防护链 |
| awk '{print $5}' |
| | iptables -F MW-WEB-GUARD |
| sed -E 's/^\[?([^]]+)\]?:[0-9]+$/\1/' |
| |
| sort |
| |
| uniq -c |
| |
| sort -nr |
| |
| head
| |
| </syntaxhighlight>
| |
|
| |
|
| === 第五步:查看防火墙规则是否正在命中 ===
| | # 删除网站防护链 |
| | | iptables -X MW-WEB-GUARD |
| <syntaxhighlight lang="bash">
| |
| iptables -L INPUT -n -v --line-numbers | |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| 重点看:
| | 由于新增限制都集中在自定义链中,这些操作不会删除 `mw_white`、`mw_ban`、腾讯云链或其他原有规则。 |
|
| |
|
| * 每条规则前的 packets;
| | 也可以使用备份完整恢复此前的 iptables 状态: |
| * bytes;
| |
| * DROP 规则是否快速增长;
| |
| * 规则顺序是否符合设计。
| |
| | |
| === 第六步:查看 ipset 名单 ===
| |
|
| |
|
| <syntaxhighlight lang="bash"> | | <syntaxhighlight lang="bash"> |
| ipset list banhash
| | iptables-restore < /root/iptables-before-web-guard.v4 |
| ipset save banhash | grep -c '^add banhash '
| |
| </syntaxhighlight> | | </syntaxhighlight> |
|
| |
|
| === 第七步:根据现象选择防线 === | | == Apache 与防火墙的边界 == |
| | |
| {| class="wikitable"
| |
| ! 现象
| |
|
| |
|
| | ! 优先措施 |
| | iptables 工作在 Apache 外部。它可以根据 IP、端口、TCP 标志、连接数量和连接速度丢弃流量,但它看不懂具体访问的是哪个 MediaWiki 页面,也不知道一个已经建立的连接中发送了多少次 HTTP 请求。 |
| | -------------------------- |
| |
| | 明确的一批恶意 IP |
| |
| | 加入 ipset |
| |
| | - |
| |
| | 单个 IP 同时几十、几百条连接 |
| |
| | connlimit |
| |
| | - |
| |
| | 单个 IP 疯狂建立短连接 |
| |
| | hashlimit |
| |
| | - |
| |
| | 大量 `SYN-RECV` |
| |
| | 检查 SYN flood、云厂商防护和上游能力 |
| |
| | - |
| |
| | TCP 连接不多,但 HTTP 请求量极高 |
| |
| | CDN、WAF、HTTP 层限速 |
| |
| | - |
| |
| | 慢慢发送请求头或请求体 |
| |
| | RequestReadTimeout |
| |
| | - |
| |
| | 空闲 KeepAlive 长时间占资源 |
| |
| | KeepAliveTimeout、event MPM |
| |
| | - |
| |
| | 不同 IP 分布式涌入 |
| |
| | 云厂商、CDN、WAF、上游清洗 |
| |
| | } |
| |
|
| |
|
| == 最终记忆模型 ==
| | Apache 负责处理已经进入服务器的 HTTP 请求。慢速发送请求头、慢速上传请求体、KeepAlive 长时间空闲以及 Apache 工作线程被占满,都属于 Apache 层的问题。 |
|
| |
|
| ipset
| | `KeepAliveTimeout` 控制一次请求完成后,持久连接还可以空闲等待多久。等待时间较短,可以减少空闲连接长期占用服务器资源。 |
| = 名单管理员
| |
| = 保存哪些 IP 或网段属于白名单、黑名单
| |
|
| |
|
| iptables
| | `RequestReadTimeout` 控制客户端必须在多长时间内、以怎样的最低速度发送完请求头或请求体。它主要用来处理故意极慢发送数据的 HTTP 客户端。 |
| = 门卫
| |
| = 根据名单、端口、连接状态决定放行或丢弃
| |
|
| |
|
| connlimit
| | Apache 的 `event MPM` 比 `prefork MPM` 更擅长处理大量空闲 KeepAlive 连接,因为它不必让主要工作线程一直等待空闲连接。但 MPM 的选择还与 PHP 使用的是 mod_php 还是 PHP-FPM 有关。 |
| = 数一个 IP 现在同时占着多少条连接
| |
|
| |
|
| hashlimit
| | '''iptables 负责控制谁能建立连接,Apache 负责控制连接进入以后怎样处理请求。''' |
| = 数一个 IP 最近建立连接有多快
| |
|
| |
|
| limit
| | == 最终概念 == |
| = 数所有人加起来有多快 | |
|
| |
|
| recent
| | `ss` 用来观察服务器当前存在什么连接,以及这些连接处于什么 TCP 状态。 |
| = 记录最近出现过的 IP 和命中次数,但容易与 hashlimit 重复
| |
|
| |
|
| ss
| | `ipset` 用来保存 IP 和网段名单。 |
| = 查看服务器当前实际存在什么 Socket、处于什么 TCP 状态
| |
|
| |
|
| Apache
| | `iptables` 用来按照规则顺序决定流量的去向。 |
| = TCP 连接进门以后,负责读取和处理 HTTP 请求
| |
|
| |
|
| CDN/WAF
| | `connlimit` 用来限制单个 IP 同时占用的连接数量。 |
| = 在服务器上游识别和阻挡分布式攻击及 HTTP 层攻击
| |
|
| |
|
| 最精简、最容易维护的防护框架是:
| | `hashlimit` 用来限制单个 IP 建立新连接的速度。 |
|
| |
|
| 1. ipset 黑名单:处理已经确认的恶意来源
| | Apache 负责处理已经进入服务器的 HTTP 请求。 |
| 2. connlimit:处理单 IP 并发连接占满
| |
| 3. hashlimit:处理单 IP 新连接洪水
| |
| 4. Apache 超时与 event MPM:处理慢请求和空闲持久连接
| |
| 5. CDN/WAF/云厂商:处理 HTTP flood 和分布式 DDoS
| |
|
| |
|
| 不要为了“防得更多”而把所有模块都叠上去。
| | CDN、WAF 和云厂商负责在服务器上游处理分布式攻击、带宽型攻击和应用层请求洪水。 |
|
| |
|
| 防火墙规则越多,越需要明确回答两个问题:
| | 最小防护结构只需要保留三个核心方向:已经确认的恶意来源进入 `mw_ban`,单 IP 并发连接过高由 connlimit 处理,单 IP 建立新连接过快由 hashlimit 处理。 |
|
| |
|
| 这条规则到底统计什么?
| | '''防火墙规则的价值不在于数量,而在于每条规则都明确知道自己限制的对象。''' |
| 超过阈值后究竟会误伤谁?
| |
网站突然变慢、无法访问,或者重启 Apache 后立即恢复,可能不是带宽已经耗尽,而是大量 TCP 连接占用了 Apache 的进程、线程或连接队列。排查这类问题时,只需先建立一个简单框架:ss 负责观察连接,ipset 负责保存 IP 名单,iptables 负责决定放行或丢弃,connlimit 限制一个 IP 同时占用的连接数,hashlimit 限制一个 IP 建立新连接的速度。
观察连接现场:ss 与 watch
`ss` 是 Linux 中查看 Socket 状态的工具,可以把它理解成服务器网络层的现场监控器。它能够显示服务器是否仍在监听端口、目前有多少连接,以及这些连接处在 TCP 通信的哪个阶段。
下面的命令显示整个系统的 Socket 概况:
它适合快速判断服务器的连接总量是否突然异常,但不能直接说明 443 端口有多少连接。
下面的命令检查是否仍有程序监听 HTTPS 端口:
ss -lntp '( sport = :443 )'
如果没有任何输出,说明当前没有程序监听 443 端口。问题通常应从 Apache 是否运行、HTTPS 配置是否成功加载、端口是否被其他程序占用等方向排查。
下面的命令统计已经完成 TCP 握手的 HTTPS 连接:
ss -Hnt state established '( sport = :443 )' | wc -l
`ESTABLISHED` 表示 TCP 三次握手已经完成。这些连接可能来自正常用户、搜索引擎、爬虫、KeepAlive 或慢连接,因此连接数量很高并不自动等于正在遭受攻击。
下面的命令统计尚未完成握手的 HTTPS 连接:
ss -Hnt state syn-recv '( sport = :443 )' | wc -l
如果 `SYN-RECV` 长时间异常增多,说明服务器收到了大量连接请求,但其中许多握手没有完成。这可能与 SYN flood、网络丢包或者 TCP 握手队列压力有关。
`watch` 本身不懂网络,它只是反复执行另一条命令。例如:
watch -n 1 "ss -Hnt state established '( sport = :443 )' | wc -l"
这表示每秒重新统计一次已经建立的 HTTPS 连接。
ss 负责观察,watch 负责持续观察。
管理名单与检查流量:ipset 和 iptables
`ipset` 是一个 IP 名单管理工具。它可以保存单个 IP,也可以保存整个网段。它本身不决定放行还是封禁,只负责维护名单。
例如,`mw_white` 可以保存白名单,`mw_ban` 可以保存黑名单。iptables 只需使用一条规则,便能查询整个名单,不必为每个 IP 分别建立一条防火墙规则。
向黑名单加入一个 IP:
ipset add mw_ban 74.7.227.12
向黑名单加入整个 `/24` 网段:
ipset add mw_ban 74.7.227.0/24
一个 `/24` 网段通常包含最多 256 个 IPv4 地址。封禁整个网段能够一次处理大量同源地址,但也比封禁单个 IP 更容易影响同一网段中的正常来源。
iptables 是真正执行网络判断的工具。进入服务器的数据包会按照规则的先后顺序接受检查。规则可以检查来源地址、目标端口、连接状态和 ipset 名单,然后决定继续检查、接受、返回上一层规则或者直接丢弃。
`DROP` 表示静默丢弃数据包。`RETURN` 表示结束当前自定义链,返回调用它的上一层链继续检查。
`-I` 是插入规则,`-A` 是把规则追加到链尾。由于 iptables 从上到下检查规则,规则顺序会直接影响最终结果。
下面的命令可以查看 INPUT 链、规则编号和每条规则的命中次数:
iptables -L INPUT -n -v --line-numbers
规则左侧的数据包数量会随着命中而增加,因此可以据此判断具体是哪条规则正在发挥作用。
ipset 是名单册,iptables 是按照名单和条件执行判断的门卫。
两种核心限制:connlimit 和 hashlimit
`connlimit` 解决的是并发连接问题。它关心的不是某个 IP 一天访问了多少次,而是这个 IP 此刻同时保持着多少条 TCP 连接。
如果一个来源 IP 同时保持几十甚至几百条连接,它可能持续占用服务器的 Socket、Apache 进程或工作线程。connlimit 可以在连接数量超过阈值以后,拒绝这个来源继续建立新连接。
connlimit 限制的是一个 IP 同时占用多少个座位。
`hashlimit` 解决的是新连接速度问题。有些异常流量不会长期保持大量连接,而是不断重复建立连接、关闭连接、再次建立连接。即使同时存在的连接数量不高,这种行为仍然会反复消耗 TCP 握手、TLS 握手和 Apache 资源。
使用 `--hashlimit-mode srcip` 时,每个来源 IP 都会单独计算速度。某个 IP 超过速率,只会影响这个 IP,不会让所有访问者共同争抢一个总额度。
hashlimit 限制的是一个 IP 冲进大门的速度。
hashlimit 看到的是 TCP 新连接,而不是 MediaWiki 页面请求。一个已经建立的 KeepAlive 连接可以连续发送多个 HTTP 请求,不会因为每次请求页面而重新进入 TCP 新连接状态。
因此,connlimit 和 hashlimit 主要处理连接层面的异常。真正的 HTTP 请求洪水还需要 CDN、WAF、反向代理或者 Apache 应用层规则处理。
最小防护规则
为了避免把新增规则散落在原有 INPUT 链中,可以建立一条独立的自定义链。所有网站连接限制都集中放在这条链里,撤销时也只需要删除这一条入口和自定义链。
应用规则前,可以把当前状态保存为文本备份:
# 保存当前 iptables 规则
iptables-save > /root/iptables-before-web-guard.v4
# 保存当前 ipset 名单
ipset save > /root/ipset-before-web-guard.set
建立独立的网站防护链:
# 如果防护链不存在,就创建它
iptables -nL MW-WEB-GUARD >/dev/null 2>&1 || \
iptables -N MW-WEB-GUARD
# 清空防护链中的旧测试规则
iptables -F MW-WEB-GUARD
白名单来源跳过本链中的连接限制,但仍会返回 INPUT 链,继续接受服务器原有规则的检查:
# 白名单跳过本链中的并发和速率限制
iptables -A MW-WEB-GUARD \
-m set --match-set mw_white src \
-j RETURN
限制单个 IPv4 地址同时占用过多网站连接:
# 单个 IPv4 地址同时超过 40 条网站连接时,拒绝继续建立新连接
iptables -A MW-WEB-GUARD \
-p tcp --syn \
-m connlimit \
--connlimit-saddr \
--connlimit-above 40 \
--connlimit-mask 32 \
-j DROP
这条规则只在对方建立新连接时检查。`--connlimit-saddr` 表示按照来源地址统计,`--connlimit-mask 32` 表示每个 IPv4 地址单独计算。
限制单个 IPv4 地址持续高速建立新连接:
# 单个 IPv4 地址持续超过每秒 10 个新连接时,丢弃超出的连接
iptables -A MW-WEB-GUARD \
-p tcp --syn \
-m hashlimit \
--hashlimit-name mw_web_new \
--hashlimit-mode srcip \
--hashlimit-above 10/second \
--hashlimit-burst 30 \
--hashlimit-htable-expire 600000 \
-j DROP
`10/second` 是持续速率,`burst 30` 用来容忍短时间的正常突发。该规则只丢弃超速期间的新连接,不会把来源 IP 永久加入黑名单。长期不活跃的速率记录会在十分钟后清理。
让进入 80 和 443 端口的 TCP 流量经过自定义防护链:
# 如果入口规则不存在,就把 80、443 流量送入网站防护链
iptables -C INPUT \
-p tcp \
-m multiport --dports 80,443 \
-j MW-WEB-GUARD 2>/dev/null || \
iptables -I INPUT 1 \
-p tcp \
-m multiport --dports 80,443 \
-j MW-WEB-GUARD
`iptables -C` 用来检查入口规则是否已经存在。只有规则不存在时,后面的插入命令才会执行,因此重复运行不会不断产生相同入口。
流量进入 `MW-WEB-GUARD` 后,白名单会先返回原 INPUT 链。其他来源依次检查并发连接数和建立新连接的速度。没有超限的流量走到自定义链末尾时,也会自动返回 INPUT 链,继续经过原有的腾讯云规则、黑名单规则和其他防火墙规则。
这套命令使用的是 IPv4 的 `iptables`。如果服务器同时通过 IPv6 对外提供网站服务,IPv6 流量需要由 `ip6tables`、nftables 或相应的 IPv6 防火墙规则处理。
查看命中与撤销规则
下面的命令显示自定义防护链及其命中次数:
iptables -L MW-WEB-GUARD -n -v --line-numbers
如果 connlimit 或 hashlimit 规则前面的数据包计数持续增加,说明相应规则正在丢弃超限的新连接。
下面的命令显示 INPUT 链中的入口:
iptables -L INPUT -n -v --line-numbers
INPUT 链中应当存在一条把 80、443 端口流量送入 `MW-WEB-GUARD` 的规则。
压力测试可以使用 Apache 自带的 `ab` 工具。例如:
ab -n 500 -c 50 https://qingliezhiquan.com/
这表示总共发送 500 个请求,同时保持最多 50 个并发请求。由于 ab 默认会频繁建立新连接,它可能同时触发 connlimit 和 hashlimit。
如果压力测试的来源 IP 存在于 `mw_white` 中,该来源会跳过本链限制,因此不会验证这两条规则。
撤销规则时,先删除 INPUT 链中的入口:
# 删除 INPUT 通往网站防护链的入口
iptables -D INPUT \
-p tcp \
-m multiport --dports 80,443 \
-j MW-WEB-GUARD
然后清空并删除自定义链:
# 清空网站防护链
iptables -F MW-WEB-GUARD
# 删除网站防护链
iptables -X MW-WEB-GUARD
由于新增限制都集中在自定义链中,这些操作不会删除 `mw_white`、`mw_ban`、腾讯云链或其他原有规则。
也可以使用备份完整恢复此前的 iptables 状态:
iptables-restore < /root/iptables-before-web-guard.v4
Apache 与防火墙的边界
iptables 工作在 Apache 外部。它可以根据 IP、端口、TCP 标志、连接数量和连接速度丢弃流量,但它看不懂具体访问的是哪个 MediaWiki 页面,也不知道一个已经建立的连接中发送了多少次 HTTP 请求。
Apache 负责处理已经进入服务器的 HTTP 请求。慢速发送请求头、慢速上传请求体、KeepAlive 长时间空闲以及 Apache 工作线程被占满,都属于 Apache 层的问题。
`KeepAliveTimeout` 控制一次请求完成后,持久连接还可以空闲等待多久。等待时间较短,可以减少空闲连接长期占用服务器资源。
`RequestReadTimeout` 控制客户端必须在多长时间内、以怎样的最低速度发送完请求头或请求体。它主要用来处理故意极慢发送数据的 HTTP 客户端。
Apache 的 `event MPM` 比 `prefork MPM` 更擅长处理大量空闲 KeepAlive 连接,因为它不必让主要工作线程一直等待空闲连接。但 MPM 的选择还与 PHP 使用的是 mod_php 还是 PHP-FPM 有关。
iptables 负责控制谁能建立连接,Apache 负责控制连接进入以后怎样处理请求。
最终概念
`ss` 用来观察服务器当前存在什么连接,以及这些连接处于什么 TCP 状态。
`ipset` 用来保存 IP 和网段名单。
`iptables` 用来按照规则顺序决定流量的去向。
`connlimit` 用来限制单个 IP 同时占用的连接数量。
`hashlimit` 用来限制单个 IP 建立新连接的速度。
Apache 负责处理已经进入服务器的 HTTP 请求。
CDN、WAF 和云厂商负责在服务器上游处理分布式攻击、带宽型攻击和应用层请求洪水。
最小防护结构只需要保留三个核心方向:已经确认的恶意来源进入 `mw_ban`,单 IP 并发连接过高由 connlimit 处理,单 IP 建立新连接过快由 hashlimit 处理。
防火墙规则的价值不在于数量,而在于每条规则都明确知道自己限制的对象。