
香港机房的NOC凌晨3:17给我打来电话:“大厅又被打了,PPS 飙到 2.8M,登录排队卡死。”我远程进了带外 KVM,看见 top 里 ksoftirqd 飙到 180%(多核叠加),网卡中断队列一片红,conntrackd 统计里无意义的 UDP 报文像潮水一样涌进来。
我做过太多次这种救火:与其“事后分析”,不如“事前建模”。这篇就是我当夜在 Debian 12 (bookworm) 的一台香港单机上落地的 iptables 限速/抗压 实操全记录——把我踩过的坑、能复用的脚本、阈值表、核对清单,统统放出来。你照着做,至少能顶住中等规模的流量冲击,给上游清洗或接入层调度赢得时间。
1. 环境画像与目标
硬件/系统(单机):
| 项目 | 规格 |
|---|---|
| 机房 | HK(电信/联通/移动多线,接入 10G 上联) |
| CPU | Intel Xeon E-2288G(8C/16T) |
| 内存 | 64 GB DDR4 |
| 系统盘 | NVMe 480 GB |
| 网卡 | Intel X520-DA2(万兆,开启多队列 RSS) |
| OS | Debian 12 (bookworm),内核 6.1 |
| conntrack | 已启用(nf_conntrack) |
| 游戏大厅端口 | TCP 443(登录/网关),UDP 30000–30100(匹配大厅分区/房间) |
设计目标:
- 限制每源 PPS/连接速率,把无效突发“削平”,优先给真实玩家让路。
- SYN/UDP 泛洪抵御:SYNPROXY 抗 SYN flood;UDP 以 hashlimit 做 per-src 限速。
- 黑白名单与动态封堵:ipset 管理;触发阈值自动拉黑短时段。
- 顺序与性能:规则短、命中早、尽量走快速路径;Ingress 侧做早丢弃。
- 可观测:每条关键链路带计数器/限频日志,配表格化阈值,便于复盘。
2. 开始前的核对清单(极重要)
# 2.1 检查 iptables 后端(Debian 12 默认 iptables-nft)
iptables -V # 看到 nf_tables 则是 iptables-nft;看到 legacy 则是 iptables-legacy
# 如需切换(一般保持 nft 即可):
# update-alternatives --config iptables
# 2.2 安装工具与持久化
apt update
apt install -y iptables ipset conntrack iptables-persistent netfilter-persistent
# 2.3 加载内核模块(SYNPROXY/hashlimit/recent/ipset)
modprobe nf_synproxy_core
modprobe xt_SYNPROXY
modprobe xt_hashlimit
modprobe xt_recent
modprobe ip_set
modprobe ip_set_hash_ip
# 开机自载:
cat >/etc/modules-load.d/netfilter.conf <<'EOF'
nf_synproxy_core
xt_SYNPROXY
xt_hashlimit
xt_recent
ip_set
ip_set_hash_ip
EOF
系统参数(/etc/sysctl.d/99-game-lobby.conf):
cat >/etc/sysctl.d/99-game-lobby.conf <<'EOF'
# 连接跟踪容量(按内存估算:每 1GB ≈ 65k)
net.netfilter.nf_conntrack_max = 4194304 # 约 64GB 内存 -> 4,194,304
net.netfilter.nf_conntrack_tcp_loose = 1
net.netfilter.nf_conntrack_tcp_timeout_established = 7440
# SYN 与队列
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 16384
net.core.somaxconn = 16384
net.core.netdev_max_backlog = 16384
# 接收/发送缓冲
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.udp_mem = 4096 87380 268435456
# 路由安全
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
# 降低无意义 ICMP 干扰
net.ipv4.icmp_echo_ignore_broadcasts = 1
EOF
sysctl --system
conntrack 容量估算参考(可按需调整):
| 内存 | 建议 nf_conntrack_max |
|---|---|
| 16 GB | 1,048,576 |
| 32 GB | 2,097,152 |
| 64 GB | 4,194,304 |
备注:真实占用取决于协议分布/内核版本;上线后用 slabtop -o 和 conntrack -S 校验。
3. 白/黑名单与日志限频:ipset 与基础链
# 3.1 集合
ipset create whitelist hash:ip timeout 0 -exist
ipset create blacklist hash:ip timeout 3600 -exist # 触发后封 1h
# 3.2 开机恢复(/etc/rc.local 或 systemd Oneshoot 亦可)
ipset save > /etc/ipset.conf
# 在 /etc/rc.local 里加:ipset restore < /etc/ipset.conf
4. 一次性清空并建防火墙骨架
规则设计原则:先放白名单、再丢黑名单、再做 CT 快速通过、最后做分协议限速。尽量把高命中规则放前面,少跳表。
# 清空
iptables -F
iptables -t nat -F
iptables -t mangle -F
iptables -t raw -F
iptables -X
# 缺省策略
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# 4.1 白/黑名单优先
iptables -N WHITELIST
iptables -N BLACKLIST
iptables -A INPUT -m set --match-set whitelist src -j ACCEPT
iptables -A INPUT -m set --match-set blacklist src -j DROP
# 4.2 loopback 与已建立连接
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# 4.3 控制台与管理面(SSH 限速防爆破)
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m hashlimit --hashlimit-name ssh_new --hashlimit-above 10/min --hashlimit-burst 20 --hashlimit-mode srcip \
-j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
5. SYN 泛洪:SYNPROXY + per-IP SYN 限速
思路:用 raw 表把新到达的 SYN 标记为不跟踪(减轻 conntrack),在 filter 表用 SYNPROXY 接管三次握手,后端只看到已验证连接。再辅以 hashlimit 做每源 SYN 速率限制。
# 5.1 raw 表 CT notrack(仅对 TCP SYN)
iptables -t raw -A PREROUTING -p tcp -m tcp --syn -j CT --notrack
# 5.2 针对对外服务端口(例如大厅走 443)
# 提前做 per-src SYN 限速:单源最多 60/s,突发 100
iptables -A INPUT -p tcp --dport 443 -m tcp --syn \
-m hashlimit --hashlimit-name syn_rate_443 --hashlimit-above 60/second --hashlimit-burst 100 \
--hashlimit-mode srcip -j DROP
# 5.3 SYNPROXY 抗 SYN flood(带 SACK/Timestamp/MSS/WScale 参数)
iptables -A INPUT -p tcp --dport 443 -m tcp --syn \
-j SYNPROXY --sack-perm --timestamp --wscale 7 --mss 1460
# SYN 之外的后续包仍需接受(已建立连接之前面规则已放行)
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
坑点:SYNPROXY 需要 nf_synproxy_core、xt_SYNPROXY,且 在 raw 表先 notrack,否则效果打折;参数建议与上游/客户端 MTU 匹配(常见 1500 MSS≈1460)。
6. UDP 泛洪:全局与 per-IP 双阈值限速
游戏大厅/房间常走 UDP。对 30000–30100 端口我们这样处理:
全局阈值(避免单机被灌爆):总 PPS 上限。
per-src 阈值(卡住单源喷洪):每源速率上限。
触发拉黑:超出严重阈值,暂时加入 blacklist。
# 6.1 全局速率(端口范围):超过 150k/s 的新到达 UDP 直接丢
iptables -N UDP_GUARD
iptables -A INPUT -p udp --dport 30000:30100 -j UDP_GUARD
iptables -A UDP_GUARD -m hashlimit \
--hashlimit-name udp_glob --hashlimit-above 150000/second --hashlimit-burst 200000 \
--hashlimit-mode dstport -j DROP
# 6.2 per-src 限速:单源 1500/s,突发 3000
iptables -A UDP_GUARD -m hashlimit \
--hashlimit-name udp_src --hashlimit-above 1500/second --hashlimit-burst 3000 \
--hashlimit-mode srcip --hashlimit-htable-expire 600000 -j DROP
# 6.3 正常放行
iptables -A UDP_GUARD -j ACCEPT
阈值建议:看你的大厅心跳/打点频率和正常玩家并发;我在一次 12 万并发在线下,per-src=1500/s 足够宽松;真实玩家心跳通常 < 50/s。
7. 连接并发限制(防爬虫/批量探测)
# 单源到 443 并发连接数上限 200
iptables -A INPUT -p tcp --dport 443 -m connlimit --connlimit-above 200 --connlimit-mask 32 -j REJECT
# 单源到 UDP 端口的新"流"(借 conntrack)/每分钟上限(更温和)
iptables -A INPUT -p udp --dport 30000:30100 -m conntrack --ctstate NEW \
-m hashlimit --hashlimit-name udp_new --hashlimit-above 1200/minute --hashlimit-burst 2000 \
--hashlimit-mode srcip -j DROP
8. 限频日志与动态拉黑(自动化“挖沟”)
我们用限频日志 + 简单脚本,把“严重超限”的源 IP 短时拉黑(1 小时),同时避免日志风暴。
# 8.1 限频日志链
iptables -N LOG_LIMITED
iptables -A LOG_LIMITED -m limit --limit 10/second --limit-burst 20 \
-j LOG --log-prefix "[FW-DROP] " --log-level 6
# 在关键 DROP 前附带一次 LOG(例如 UDP per-src 超限)
iptables -R UDP_GUARD 2 \
-m hashlimit --hashlimit-name udp_src --hashlimit-above 1500/second --hashlimit-burst 3000 \
--hashlimit-mode srcip --hashlimit-htable-expire 600000 -j LOG_LIMITED
iptables -I UDP_GUARD 3 \
-m hashlimit --hashlimit-name udp_src --hashlimit-above 1500/second --hashlimit-burst 3000 \
--hashlimit-mode srcip --hashlimit-htable-expire 600000 -j DROP
拉黑脚本 /usr/local/sbin/fw-autoban.sh:
#!/usr/bin/env bash
# 从 syslog 中抓取超限源,拉入 ipset: blacklist(超限一次拉黑 1h)
# 使用前:apt install -y gawk
LOGFILE="/var/log/syslog"
STATE="/run/fw-autoban.cursor"
[ -f "$STATE" ] || echo 0 > "$STATE"
CUR=$(cat "$STATE")
LINES=$(wc -l < "$LOGFILE")
awk -v start="$CUR" -v end="$LINES" 'NR>start && NR<=end' "$LOGFILE" \
| awk '/\[FW-DROP\].*SRC=([0-9]{1,3}\.){3}[0-9]{1,3}/ {
match($0, /SRC=([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1] != "") print m[1];
}' \
| sort -u \
| while read ip; do
ipset add blacklist "$ip" timeout 3600 -exist
echo "$(date) autoban $ip"
done
echo "$LINES" > "$STATE"
定时任务(每分钟跑一次):
cat >/etc/cron.d/fw-autoban <<'EOF'
* * * * * root /usr/local/sbin/fw-autoban.sh >/dev/null 2>&1
EOF
9. ICMP 与乱七八糟的东西
# 适度放行必要 ICMP(PMTUD),限速 echo
iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 10/second --limit-burst 20 -j ACCEPT
10. 持久化与开机应用
# 保存
iptables-save > /etc/iptables/rules.v4
ipset save > /etc/ipset.conf
# 持久服务
systemctl enable netfilter-persistent
systemctl enable iptables
11. 观测与验证(上线前必做)
# 当前规则命中/字节
watch -n1 'iptables -S; echo; iptables -L -v -n | sed -n "1,80p"'
# 连接跟踪统计
watch -n1 conntrack -S
# 网卡统计(看丢包/队列)
watch -n1 'ip -s link show dev eth0'
ethtool -S eth0 | egrep "rx|tx|dropped|fifo|miss"
# 流量视图
apt install -y iftop nload
nload -u M -t 200 # 每 200ms 更新,单位 MB
压测建议(请在内网或授权靶机)
- TCP:hping3 -S -p 443 --flood <server>(仅在实验环境)
- UDP:nping --udp -p 30000 --rate 1000 --count 100000 <server>
生产禁止对外乱测;我用的是隔离靶机 + 端口镜像。
12. 典型阈值对照表(可按需微调)
| 场景 | 建议阈值 | 备注 |
|---|---|---|
| TCP 443 每源 SYN | 60/s(突发 100) | 真实玩家握手极少,足够宽松 |
| UDP 30000–30100 全局 | 150k/s(突发 200k) | 先保主机 CPU 不被灌爆 |
| UDP 每源 PPS | 1500/s(突发 3000) | 兼顾大厅心跳与技能瞬发 |
| SSH 新连接 | 10/min(突发 20) | 防脚本爆破 |
| TCP 并发/源 | 200 | 防批量探测 |
13. 进一步优化(当夜我还做了这些)
网卡/RSS:确认 ethtool -l eth0 开了多队列,/proc/interrupts 里把中断均衡到不同 CPU(irqbalance 开着)。
RPS/RFS:/sys/class/net/eth0/queues/rx-*/rps_cpus 适度开启,分散 softirq。
Ingress 早丢(可选,进阶):tc ingress/ifb 做 police,对攻击明显端口做硬门限,减少上层压力:
# 简例:超过 2Mpps 的 UDP 直丢(单位到 pps 需要 tc-police pps 能力或基于速率近似)
tc qdisc add dev eth0 handle ffff: ingress
tc filter add dev eth0 parent ffff: protocol ip u32 \
match ip protocol 17 0xff \
police rate 800mbit burst 25mb drop flowid :1
是的,Ingress 丢弃对“对端带宽占用”无济于事,但对本机 CPU/队列保活很有用。
应用层协同:大厅心跳/握手添加 来源校验 / token,无效报文尽量早丢,减轻 conntrack。
上游清洗/接入层:真遇大流量(Gbps 级),还是得接 清洗/CDN(如 Spectrum/Anycast),单机策略只是 缓冲垫。
14. 常见坑与当场解法
- 规则顺序:WHITELIST 必须在任何丢弃前;ESTABLISHED,RELATED 要早放行。
- SYNPROXY 无效:忘了 raw 表 --notrack;或模块没加载。检查 lsmod | grep synproxy。
- hashlimit“串号”:不同链/不同端口用 不同的 --hashlimit-name,否则共享桶导致误判。
- iptables-nft 与 legacy 混乱:确认只用一种后端;切换后重启 netfilter-persistent。
- 阈值过严:先从宽松起步,观察真实命中再收紧;日志限频避免被“反杀”。
- conntrack 爆表:提高 nf_conntrack_max;用 raw notrack 绕开无状态报文;压测看 slab 使用。
15. 一键重放/回滚脚本(上线友好)
应用: /usr/local/sbin/fw-apply.sh
#!/usr/bin/env bash
set -euo pipefail
backup="/root/iptables.backup.$(date +%s)"
iptables-save > "$backup" || true
echo "[+] backup saved: $backup"
# 把本文关键命令整理为一个脚本片段,source 即可(略)
/usr/local/sbin/fw-snippet.sh
iptables-save > /etc/iptables/rules.v4
ipset save > /etc/ipset.conf
systemctl restart netfilter-persistent
echo "[+] done. rollback: iptables-restore < $backup"
回滚:
iptables-restore < /root/iptables.backup.<timestamp>
systemctl restart netfilter-persistent
16. 完整示例(简化版 iptables-save 片段,便于对账)
*raw
:PREROUTING ACCEPT [0:0]
-A PREROUTING -p tcp -m tcp --syn -j CT --notrack
COMMIT
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:WHITELIST - [0:0]
:BLACKLIST - [0:0]
:UDP_GUARD - [0:0]
:LOG_LIMITED - [0:0]
-A INPUT -m set --match-set whitelist src -j ACCEPT
-A INPUT -m set --match-set blacklist src -j DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
# SSH
-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name ssh_new \
--hashlimit-above 10/min --hashlimit-burst 20 --hashlimit-mode srcip -j DROP
-A INPUT -p tcp --dport 22 -j ACCEPT
# TCP 443
-A INPUT -p tcp --dport 443 -m tcp --syn -m hashlimit --hashlimit-name syn_rate_443 \
--hashlimit-above 60/second --hashlimit-burst 100 --hashlimit-mode srcip -j DROP
-A INPUT -p tcp --dport 443 -m tcp --syn -j SYNPROXY --sack-perm --timestamp --wscale 7 --mss 1460
-A INPUT -p tcp --dport 443 -j ACCEPT
# UDP 大厅
-A INPUT -p udp --dport 30000:30100 -j UDP_GUARD
-A UDP_GUARD -m hashlimit --hashlimit-name udp_glob --hashlimit-above 150000/second \
--hashlimit-burst 200000 --hashlimit-mode dstport -j DROP
-A UDP_GUARD -m hashlimit --hashlimit-name udp_src --hashlimit-above 1500/second \
--hashlimit-burst 3000 --hashlimit-mode srcip --hashlimit-htable-expire 600000 -j LOG_LIMITED
-A UDP_GUARD -m hashlimit --hashlimit-name udp_src --hashlimit-above 1500/second \
--hashlimit-burst 3000 --hashlimit-mode srcip --hashlimit-htable-expire 600000 -j DROP
-A UDP_GUARD -j ACCEPT
# ICMP
-A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
-A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
-A INPUT -p icmp --icmp-type echo-request -m limit --limit 10/second --limit-burst 20 -j ACCEPT
# LOG 限频
-A LOG_LIMITED -m limit --limit 10/second --limit-burst 20 -j LOG --log-prefix "[FW-DROP] " --log-level 6
COMMIT
凌晨 3:34,SYN 曲线被切掉一截,UDP 的“锯齿”被削成温和的波形,登录排队从 3 分多钟回落到 20 秒以内。
我靠在机房的钣金柜前喘了口气,把 iptables-save 和 ipset.conf 备份发到 Git。
后来我把这套模板推广到了同集群的其他节点,阈值因地制宜。再大的流量,还是要靠上游清洗与调度,但 单机这层守得住,我们就能从“手忙脚乱的灭火”,变成“按预案行事的消防”。
如果你也在香港的深夜里守着一台 Debian,大厅里是成千上万的玩家——祝你今晚顺利。把这篇架起来,先保住 CPU 的喘息,再谈更优雅的全局方案