Test wikitext: Difference between revisions

From 清冽之泉
Jump to navigation Jump to search
Created page with "== 总框架 == 排查网站被骚扰、连接占满或访问困难时,应当先区分五个层级: {| class="wikitable" ! 层级 ! 观察对象 ! 主要工具 | ! 解决的问题 | | ------------------------------------ | | 上游网络 | | 总带宽、分布式 DDoS | | 云厂商、CDN、WAF | | 流量是否已经在到达服务器之前堵塞线路..."
 
Blanked the page
Tag: Blanking
Line 1: Line 1:
== 总框架 ==


排查网站被骚扰、连接占满或访问困难时,应当先区分五个层级:
{| class="wikitable"
! 层级
! 观察对象
! 主要工具
| ! 解决的问题                              |
| ------------------------------------ |
| 上游网络                                |
| 总带宽、分布式 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">
ss -s
</syntaxhighlight>
`ss -s` 中:
estab 662
表示这台服务器当前共有大约 662 个处于 `ESTABLISHED` 状态的 TCP Socket,可能包括:
* Apache 的 80、443;
* SSH 的 22;
* MariaDB、代理、邮件等其他连接;
* 本机主动连接到外界的连接。
它只是全局概览,不是 Apache 专用统计。`ss -s` 的 `-s` 是 `--summary`,意思是输出 Socket 汇总统计。
精确查看服务器本地 443 端口的已建立连接,应使用:
<syntaxhighlight lang="bash">
ss -Hnt state established '( sport = :443 )'
</syntaxhighlight>
只统计数量:
<syntaxhighlight lang="bash">
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">
ss -Hnt state established '( sport = :443 )' | wc -l
</syntaxhighlight>
这条命令解决:
“现在究竟有多少条连接已经真正进入 HTTPS 服务?”
监控变化:
<syntaxhighlight lang="bash">
watch -n 1 \
"ss -Hnt state established '( sport = :443 )' | wc -l"
</syntaxhighlight>
`watch -n 1`:
* `watch`:重复执行命令;
* `-n`:`interval`,刷新间隔;
* `1`:每一秒执行一次。
=== 查看半连接数量 ===
<syntaxhighlight lang="bash">
ss -Hnt state syn-recv '( sport = :443 )' | wc -l
</syntaxhighlight>
这条命令解决:
“现在是否有大量 TCP 握手没有完成?”
如果 `SYN-RECV` 持续很多,而 `ESTABLISHED` 不一定很多,更接近:
* SYN flood;
* 对方大量发 SYN 后不完成握手;
* 线路丢包;
* 服务器握手队列或网络栈压力。
=== 找出连接最多的来源 IP ===
推荐写法:
<syntaxhighlight lang="bash">
ss -Hnt state established '( sport = :443 )' |
awk '{print $5}' |
sed -E 's/^\[?([^]]+)\]?:[0-9]+$/\1/' |
sort |
uniq -c |
sort -nr |
head
</syntaxhighlight>
作用:
1. `ss` 找出本地 443 的已建立连接;
2. `awk '{print $5}'` 取出对方地址和端口;
3. `sed` 去掉对方端口;
4. `sort` 排序;
5. `uniq -c` 合并相同 IP,并统计出现次数;
6. `sort -nr` 按数字倒序;
7. `head` 显示前十名。
各命令的英文概念:
{| class="wikitable"
! 命令
! 英文概念
| ! 作用      |
| ---------- |
| `awk`      |
| 文本字段处理工具  |
| 取出指定列      |
| -          |
| `sort`    |
| sort      |
| 排序        |
| -          |
| `uniq`    |
| unique    |
| 合并相邻的重复行  |
| -          |
| `uniq -c`  |
| count      |
| 统计每项出现次数  |
| -          |
| `sort -n`  |
| numeric    |
| 按数字排序      |
| -          |
| `sort -r`  |
| reverse    |
| 倒序        |
| -          |
| `head`    |
| head      |
| 只显示最前面的若干行 |
| }          |
原来的命令:
<syntaxhighlight lang="bash">
ss -ant | awk '{print $5}' | cut -d: -f1 |
sort | uniq -c | sort -nr | head
</syntaxhighlight>
主要有三个问题:
* 没有限定本地 443;
* 没有限定 `ESTABLISHED`;
* `cut -d: -f1` 遇到 IPv6 地址会被冒号切坏。
=== 确认 Apache 是否还在监听 443 ===
<syntaxhighlight lang="bash">
ss -lntp '( sport = :443 )'
</syntaxhighlight>
参数:
* `-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 解决的痛点是:
“如果有几百、几千、几万个恶意 IP,不能为每个 IP 写一条 iptables 规则。”
iptables 只需要写一条规则,去查询 ipset 名单。
=== 创建可保存 IP 和网段的黑名单 ===
<syntaxhighlight lang="bash">
ipset create banhash hash:net \
  family inet \
  hashsize 4096 \
  maxelem 65536
</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
它适合保存大小不一的网段。
如果脚本可能被重复运行,可以使用:
<syntaxhighlight lang="bash">
ipset create banhash hash:net \
  family inet \
  hashsize 4096 \
  maxelem 65536 \
  -exist
</syntaxhighlight>
`-exist` 的作用是:集合已经存在时,不把它当作错误。
=== 添加单个 IP ===
<syntaxhighlight lang="bash">
ipset add banhash 74.7.227.12
</syntaxhighlight>
=== 添加整个 /24 网段 ===
<syntaxhighlight lang="bash">
ipset add banhash 74.7.227.0/24
</syntaxhighlight>
`/24` 表示前 24 位是网络部分,通常覆盖:
74.7.227.0 ~ 74.7.227.255
这不是“封一个 IP”,而是封整个网段。
必须确认该网段确实大量恶意,避免误伤同一网段内的正常用户、搜索引擎、代理或云服务。
=== 让 iptables 使用该黑名单 ===
<syntaxhighlight lang="bash">
iptables -I INPUT \
  -m set \
  --match-set banhash src \
  -j DROP
</syntaxhighlight>
该规则解决的需求是:
“只要来源地址存在于 banhash,就在进入 Apache 以前直接丢弃。”
参数含义:
{| class="wikitable"
! 参数
! 英文
| ! 含义                  |
| --------------------- |
| `-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>
=== 正确统计 ipset 中的成员数量 ===
原命令:
<syntaxhighlight lang="bash">
ipset list banhash | wc -l
</syntaxhighlight>
统计的是输出总行数,其中还包含:
* Name;
* Type;
* Revision;
* Header;
* Members;
* 其他说明行。
它不是准确的成员数。
更合适的写法:
<syntaxhighlight lang="bash">
ipset save banhash | grep -c '^add banhash '
</syntaxhighlight>
动态监控:
<syntaxhighlight lang="bash">
watch -n 1 \
"ipset save banhash | grep -c '^add banhash '"
</syntaxhighlight>
== connlimit:限制单个 IP 的并发连接数 ==
推荐按“建立连接时”检查:
<syntaxhighlight lang="bash">
iptables -A INPUT \
  -p tcp \
  --syn \
  --dport 443 \
  -m connlimit \
  --connlimit-above 30 \
  --connlimit-mask 32 \
  -j DROP
</syntaxhighlight>
该规则解决的痛点是:
“同一个来源 IP 同时维持大量 HTTPS 连接,占满 Apache 或服务器连接资源。”
各部分含义:
{| class="wikitable"
! 参数
! 英文
| ! 含义                  |
| ---------------------- |
| `-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">
iptables -I INPUT \
  -p tcp \
  --dport 443 \
  -m conntrack \
  --ctstate NEW \
  -m hashlimit \
  --hashlimit-name HTTPS \
  --hashlimit-above 30/minute \
  --hashlimit-burst 20 \
  --hashlimit-mode srcip \
  --hashlimit-htable-expire 600000 \
  -j DROP
</syntaxhighlight>
它解决的痛点是:
“某个来源 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 可能过于严格 ===
`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">
iptables -I INPUT \
  -p tcp \
  --dport 443 \
  -m conntrack \
  --ctstate NEW \
  -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 \
  --syn \
  --dport 443 \
  -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>
核心工具:
ipset
=== 第二层:单 IP 并发限制 ===
解决:
“一个 IP 同时占着太多连接不放。”
<syntaxhighlight lang="bash">
-p tcp --syn --dport 443 \
-m connlimit \
--connlimit-above N \
--connlimit-mask 32 \
-j DROP
</syntaxhighlight>
核心工具:
connlimit
`N` 必须根据正常流量确定。
=== 第三层:单 IP 新连接速率限制 ===
解决:
“一个 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">
iptables -L INPUT -n -v --line-numbers
</syntaxhighlight>
以及:
* 网站是否仍可正常打开;
* 正常请求延迟是否上升;
* Apache 是否仍有可用 worker;
* CPU、内存是否耗尽;
* iptables 对应规则的包计数是否增长;
* 是否出现大量正常用户误伤。
真正的目标是:
“在恶意或异常负载出现时,服务器仍能为正常用户保留服务能力。”
不是单纯追求:
“压力测试出现越多失败越好。”
== 网站异常时的排查顺序 ==
=== 第一步:确认服务是否还在监听 ===
<syntaxhighlight lang="bash">
ss -lntp '( sport = :443 )'
</syntaxhighlight>
没有监听,先排查 Apache。
=== 第二步:查看全局 Socket 概况 ===
<syntaxhighlight lang="bash">
ss -s
</syntaxhighlight>
确认是否有异常数量的 Socket。
=== 第三步:按状态拆分 443 连接 ===
<syntaxhighlight lang="bash">
ss -Hnt '( sport = :443 )' |
awk '{print $1}' |
sort |
uniq -c |
sort -nr
</syntaxhighlight>
重点区分:
* `ESTABLISHED` 很多;
* `SYN-RECV` 很多;
* `TIME-WAIT` 很多;
* `CLOSE-WAIT` 很多。
=== 第四步:找连接最多的来源 IP ===
<syntaxhighlight lang="bash">
ss -Hnt state established '( sport = :443 )' |
awk '{print $5}' |
sed -E 's/^\[?([^]]+)\]?:[0-9]+$/\1/' |
sort |
uniq -c |
sort -nr |
head
</syntaxhighlight>
=== 第五步:查看防火墙规则是否正在命中 ===
<syntaxhighlight lang="bash">
iptables -L INPUT -n -v --line-numbers
</syntaxhighlight>
重点看:
* 每条规则前的 packets;
* bytes;
* DROP 规则是否快速增长;
* 规则顺序是否符合设计。
=== 第六步:查看 ipset 名单 ===
<syntaxhighlight lang="bash">
ipset list banhash
ipset save banhash | grep -c '^add banhash '
</syntaxhighlight>
=== 第七步:根据现象选择防线 ===
{| class="wikitable"
! 现象
| ! 优先措施                    |
| -------------------------- |
| 明确的一批恶意 IP                |
| 加入 ipset                  |
| -                          |
| 单个 IP 同时几十、几百条连接          |
| connlimit                  |
| -                          |
| 单个 IP 疯狂建立短连接              |
| hashlimit                  |
| -                          |
| 大量 `SYN-RECV`              |
| 检查 SYN flood、云厂商防护和上游能力    |
| -                          |
| TCP 连接不多,但 HTTP 请求量极高      |
| CDN、WAF、HTTP 层限速          |
| -                          |
| 慢慢发送请求头或请求体                |
| RequestReadTimeout        |
| -                          |
| 空闲 KeepAlive 长时间占资源        |
| KeepAliveTimeout、event MPM |
| -                          |
| 不同 IP 分布式涌入                |
| 云厂商、CDN、WAF、上游清洗          |
| }                          |
== 最终记忆模型 ==
ipset
= 名单管理员
= 保存哪些 IP 或网段属于白名单、黑名单
iptables
= 门卫
= 根据名单、端口、连接状态决定放行或丢弃
connlimit
= 数一个 IP 现在同时占着多少条连接
hashlimit
= 数一个 IP 最近建立连接有多快
limit
= 数所有人加起来有多快
recent
= 记录最近出现过的 IP 和命中次数,但容易与 hashlimit 重复
ss
= 查看服务器当前实际存在什么 Socket、处于什么 TCP 状态
Apache
= TCP 连接进门以后,负责读取和处理 HTTP 请求
CDN/WAF
= 在服务器上游识别和阻挡分布式攻击及 HTTP 层攻击
最精简、最容易维护的防护框架是:
1. ipset 黑名单:处理已经确认的恶意来源
2. connlimit:处理单 IP 并发连接占满
3. hashlimit:处理单 IP 新连接洪水
4. Apache 超时与 event MPM:处理慢请求和空闲持久连接
5. CDN/WAF/云厂商:处理 HTTP flood 和分布式 DDoS
不要为了“防得更多”而把所有模块都叠上去。
防火墙规则越多,越需要明确回答两个问题:
这条规则到底统计什么?
超过阈值后究竟会误伤谁?

Revision as of 23:15, 13 July 2026