先说结论:装了防火墙却不排序,等于没装
前阵子帮一个朋友看他的小站,问他有没有配防火墙,他说配了,iptables 规则写了四十多条,看起来很专业。我上机跑了一遍 iptables -L -n --line-numbers,问题一眼就看出来了:他在 INPUT 链的第 3 条写了 ACCEPT all -- anywhere anywhere,后面三十多条针对具体端口的放行和拒绝规则全都成了摆设。iptables 是从上往下逐条匹配的,第一条匹配上的规则直接决定结局,后面的规则根本不会被读到。这就好比在门口第一道岗贴了张「所有人随便进」,第二道岗再贴「禁止张三入内」还有什么意义。
这件事让我意识到,很多人对防火墙的理解停留在「加了规则就安全了」这个层面,而真正决定防火墙行为的是三件更枯燥的事:规则的匹配顺序、链的默认策略、以及状态跟踪。这三件事任意一件搞错,规则写了跟没写差别不大,甚至更危险——因为你以为自己被保护了。
这篇文章就按「先看现状、再定顺序、然后写规则、最后验证」这个顺序,把这套东西讲清楚。不讲理论,全是能直接抄到自己服务器上跑的命令和判断依据。
第一步:先看清当前到底有哪些规则,别急着加
在动手之前,必须先完整读一遍现状。很多人接手服务器第一件事就是往上加规则,结果越加越乱,因为旧规则已经在那里悄悄决定了行为。
# 表 1:filter 表的默认策略和规则(最常用)
iptables -L -n -v --line-numbers
# 只看 INPUT 链,带行号和包计数
iptables -L INPUT -n -v --line-numbers
# 表 2:把规则输出成可直接回放的格式,比 -L 直观得多
iptables-save
# 表 3:如果用的是 nftables(Debian 10+ / CentOS 8+ 默认)
nft list ruleset
# 表 4:确认内核模块和当前是 iptables-legacy 还是 iptables-nft
iptables --version-v 这个参数极其重要,它输出每条规则匹配了多少个包、多少个字节。看这个计数就能立刻判断出一条规则到底有没有在工作。一条规则如果挂在链上但计数永远是 0,要么位置被前面的规则截胡了,要么条件写错了从来没匹配上。我看过太多「防火墙配了半天其实一条没生效」的服务器,全是靠这个 pkts 列发现的。
还有一个更隐蔽的坑:Debian 10 之后同时装了 iptables-legacy 和 iptables-nft 两套工具。如果你用 iptables-legacy 加的规则,用 nft list ruleset 是看不到的;反过来也一样。表现出来就是「我明明加了规则,iptables -L 有,nft 看不到」或者「重启后又没了」。判断当前用的是哪套:
# 看 alternatives 当前指向哪一套
update-alternatives --display iptables
# 或直接对比两者的输出,哪边有你的规则就是哪套在生效
iptables-legacy -L INPUT -n | head -5
iptables-nft -L INPUT -n | head -5这一步千万别省。当两套工具并存时,iptables 这个名字到底指向谁,取决于系统配置,而 iptables-save 和 nft list ruleset 保存的又是不同后端的内容。用错了工具,你的规则写在一个后端、重启加载的是另一个后端,最后表现成「重启后规则消失」——这个现象我下面还会专门讲。
第二步:把默认策略定下来,这是整套规则的骨架
默认策略(policy)指的是:一条包从头到尾没有被任何规则匹配上,最后怎么处理。只有三种:ACCEPT、DROP、REJECT。
| 策略 | 行为 | 适用场景 | 代价 |
|---|---|---|---|
| ACCEPT | 放行 | 初期调试、内网可信环境 | 漏一条拒绝规则就是一个洞,等于白名单没做 |
| DROP | 静默丢弃,不回包 | 生产环境 INPUT 链 | 客户端要等到超时才失败,排查体验差 |
| REJECT | 丢弃并回一个拒绝包 | 调试期、明确想快速失败的场景 | 会暴露「这里有防火墙」,且对某些扫描器反而有信息价值 |
生产环境 INPUT 链的正确做法是:默认 DROP,然后一条一条把需要的东西放进来。这叫白名单模型,安全性远高于「默认放行 + 加几条拒绝」。因为拒绝模型里,你只要漏想一个端口,那个端口就是对外敞开的;而白名单模型里,你漏写一条规则,后果是某个服务连不上——它吵闹、可见、能修,不会安静地把后端暴露出去。
# 正确顺序:先把要放行的写全,最后再改默认策略
# 反过来的话,你会在改完策略的那一瞬间把 SSH 也一起锁死
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# 确认以上规则都在,最后才动策略
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT这个顺序不是洁癖,是保命。我见过有人先 iptables -P INPUT DROP 再想加放行规则,结果新规则加进去时连接已经断了,SSH 会话直接卡死,云控制台的 VNC 又是另一回事,最后只能靠服务商的重置功能救回来。如果非要先改策略,务必先挂一个定时恢复的保险:
# 保险:5 分钟后自动清空所有规则,防止把自己锁在外面
# 在另一个 SSH 会话里执行,不要在当前会话里执行
(sleep 300; iptables -F; iptables -P INPUT ACCEPT) &
# 确认新规则可用之后,kill 掉这个后台任务(或者让它自己跑完,反正已经通了)这个「定时解锁」的小技巧值得养成肌肉记忆。任何可能影响 SSH 连通性的操作——改防火墙、改网卡、改路由、改 SSH 配置——都应该先在另一个会话里挂上它。三百秒足够你验证完并决定是保留还是回滚。
第三步:规则顺序的四条硬规则
现在回到文章开头那个事故。规则顺序不是凭感觉排的,有四条硬性要求,按这个顺序排基本不会出错。
第一条:状态跟踪规则永远排在最前面(lo 之后)。-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 这条是整套防火墙的地基。没有它,你去连别人(比如 apt update)时,回来的包会被 INPUT 链的 DROP 策略丢掉,所有出站请求都会失败。而且它必须排在任何可能命中这些回包的规则之前,否则会被提前截住。有些教程把它放在最后,理由是「前面先放行业务端口」,这是错的:业务端口的规则对回包通常是命不中的(源端口不匹配),但如果有人写了一条 -p tcp --sport 80 -j DROP 之类的规则,回包就全没了。
第二条:拒绝单个 IP 的黑名单规则,必须排在同一端口的放行规则之前。假设你要拉黑一个暴力破解 SSH 的 IP:
# 错误:先放行,再拉黑,拉黑永远不生效
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -s 1.2.3.4 -j DROP
# 正确:拉黑用 -I 插到链的最前面
iptables -I INPUT 1 -s 1.2.3.4 -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT-A 是追加到链尾,-I 是插入到指定位置(不写位置就是第 1 条)。这是新手最容易混的一对参数,也是「黑名单不生效」的第一原因。顺便说,如果你需要长期维护一个黑名单 IP 集合,不要一条一条写,用 ipset:一条规则配一个集合,增删 IP 只动集合不动规则,几百个 IP 也只有一次匹配成本。
ipset create blacklist hash:ip timeout 86400
iptables -I INPUT -m set --match-set blacklist src -j DROP
# 之后随时增删,无需触碰 iptables
ipset add blacklist 1.2.3.4
ipset del blacklist 1.2.3.4
ipset list blacklist | headtimeout 86400 让集合里的 IP 24 小时后自动过期,配合 fail2ban 的封禁动作可以做出很干净的自动解封。这也是 fail2ban 内部常用的做法。
第三条:端口范围和小范围规则排在宽泛规则之前。这个道理跟写 CSS 一样,越具体的越先写。要做全端口限速时,一定有例外:
# 先给 22 和 443 单独开口(不受限速影响)
iptables -A INPUT -p tcp -m multiport --dports 22,443 -j ACCEPT
# 再给其余端口做统一处理
iptables -A INPUT -p tcp -m tcp --tcp-flags ALL NONE -j DROP
iptables -A INPUT -p tcp -m limit --limit 20/min -j ACCEPT第四条:默认策略在链尾,但链尾之前一定要有一条显式的兜底 LOG(生产环境可选)。DROP 是安静的,出了问题你不知道是谁被丢了。加一条限速的日志规则,能让你在排查时知道是谁在敲门,同时不会把磁盘写满:
iptables -A INPUT -m limit --limit 3/min --limit-burst 10 \
-j LOG --log-prefix "ipt-dropped: " --log-level 4
# 这条要排在 DROP 策略之前,因为策略是「没匹配上」的兜底,无法被 LOG 影响
# 日志落地位置:/var/log/kern.log 或 journalctl -k | grep ipt-dropped第四步:让规则活过重启——三种持久化方式,别混用
规则写在内存里,重启就没了。这是「重启后防火墙规则丢失」的根本原因,也是新手最常问的问题。持久化方式有三种,选一种就好,混用会互相打架。
# 方式一:iptables-persistent(Debian/Ubuntu 老牌做法)
apt install iptables-persistent
# 安装时会问要不要保存当前规则,之后手动保存:
netfilter-persistent save # 写到 /etc/iptables/rules.v4
netfilter-persistent reload
# 方式二:systemd 服务自己写(不依赖额外包,最可控)
iptables-save > /etc/iptables/rules.v4
iptables-restore < /etc/iptables/rules.v4 # 测试一下能回放
# 方式三:nftables 原生(新系统推荐)
nft list ruleset > /etc/nftables.conf
systemctl enable --now nftables混用的典型事故是这样:系统里同时启用了 netfilter-persistent 和 nftables 服务,前者从 /etc/iptables/rules.v4 恢复 iptables-legacy 的表,后者从 /etc/nftables.conf 恢复 nft 的表,两者都往同一个 netfilter 里灌规则。结果要么互相覆盖,要么叠加成了两套,iptables -L 只能看到一半。检查有没有这种冲突:
systemctl list-unit-files | grep -Ei 'netfilter|iptables|nftables|firewalld'
# 只应该有一个处于 enabled 状态顺便一提,firewalld 和裸 iptables 也是互斥的:firewalld 会自己管理链,你在它运行期间用 iptables 手加的规则会在 firewalld reload 时被清掉。用 CentOS/Rocky 的话,要么全用 firewalld(firewall-cmd --permanent --add-port=80/tcp),要么先 systemctl disable --now firewalld 再用裸 iptables,别两套同时来。
第五步:写完之后必须验证,这五条检查缺一不可
规则配好之后,最危险的状态是「看起来配好了」。必须逐条验证,而且要从外部验证,不只是本地 iptables -L。
# 1. 规则顺序与计数(看 pkts 列,确认关键规则真的命中过)
iptables -L INPUT -n -v --line-numbers
# 2. 默认策略确认
iptables -S | grep -- '-P '
# 3. 从外部验证端口是否真的关着(在别的机器上跑)
# nc -zv 你的IP 端口,开放会显示 succeeded,关闭会 timeout
nc -zv -w 3 1.2.3.4 3306
# 4. 用扫描器看全貌,只扫常用端口,别做全端口扫描
nmap -Pn --top-ports 100 1.2.3.4
# 5. 重启后复查(这一步最容易被跳过)
reboot
# 起来之后重新跑第 1、2 条,确认规则还在第 3 条和第 4 条的价值在于:本机看 iptables -L 只能看到「我以为的规则」,外部扫描才能看到「实际暴露的端口」。这两者不一致的原因有很多——云服务商的安全组在前面又拦了一层、服务监听在 127.0.0.1、Docker 会用自己的一套 iptables 链绕过你的规则。最后这一点值得单独说。
容易忽略的一节:Docker 和 ufw 会偷偷改写你的规则
如果你的服务器上有 Docker,那你写的 iptables 规则可能根本没管到容器端口。Docker 启动时会往 nat 表的 DOCKER 链和 filter 表的 DOCKER-USER 链里插规则,容器映射出来的端口(-p 8080:80)走的是 FORWARD 链的 DOCKER 分支,直接跳过了你在 INPUT 链里写的所有限制。表现出来就是:明明 INPUT 只开了 22/80/443,容器映射的 8080 从外面照样能访问。
# 确认容器是否真的在绕过 INPUT
iptables -L DOCKER-USER -n -v
iptables -t nat -L DOCKER -n | head
# 正确的做法:把限制写进 DOCKER-USER 链(它由 Docker 保留,不会被清空)
iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 -j DROP
# 注意 DOCKER-USER 是 Docker 自动创建的,重启 docker 后规则会丢,
# 需要放进 /etc/ufw/after.rules 或你自己的 restore 脚本里同理,ufw 是 iptables 的前端,它的状态在 /etc/ufw/*.rules 里,用 ufw status 看。如果 ufw 是启用的,你手写的 iptables 规则会在 ufw reload 时被冲掉。判断当前是不是 ufw 在管:
ufw status verbose
# 输出 active 就说明 ufw 在管,此时应该用 ufw 命令而不是裸 iptables
ufw allow 80/tcp
ufw delete allow 3306/tcp一张排错对照表
| 现象 | 最可能的原因 | 怎么确认 |
|---|---|---|
| 规则加了但完全不生效 | 位置在某条 ACCEPT 之后 | 看 -v 的 pkts 计数是否为 0 |
| 黑名单 IP 还是能连 | 用了 -A 没用 -I | 规则排在放行规则之后 |
| 重启后规则全没了 | 没有持久化,或持久化到了另一个后端 | 对比 iptables-save 与 nft list ruleset |
| 改了规则 SSH 断了 | 策略先于放行规则生效 | 云控制台 VNC 或服务商重置 |
| 容器端口拦不住 | Docker 走 FORWARD 链 | iptables -L DOCKER-USER -n |
| apt update 卡住 | 缺 ESTABLISHED 放行 | 临时 iptables -P INPUT ACCEPT 试一下 |
最后:小站要不要上 nftables
如果你的系统是 Debian 10+ 或 CentOS 8+,内核和工具链其实已经是 nftables 后端了,iptables 命令只是一个兼容层,把命令翻译成 nft 规则。这种情况下用 nftables 原生语法反而更清晰:一套 table 里可以同时放 IPv4 和 IPv6,规则用集合(set)和映射(map)表达,不需要 ipset 这个外挂。
# nftables 的一段等价配置,比 iptables 简洁很多
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
nft add rule inet filter input iif lo accept
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input tcp dport '{ 22, 80, 443 }' accept
# 一次性给一组 IP 拉黑,原生 set
nft add set inet filter blacklist '{ type ipv4_addr; flags timeout; }'
nft add rule inet filter input ip saddr @blacklist drop但如果你只是维护一台小站,iptables 的规则已经能跑、能持久化、能看懂,那就别为了换而换。更换防火墙体系的代价不在写规则,而在于所有依赖它的东西——fail2ban 的 banaction、Docker 的自动规则、云服务商的安全组脚本——都要重新对一遍。我在一台跑了四年的机器上试过迁移,最后花了一整个下午处理 fail2ban 的动作配置,收益其实只是配置文件短了几行。真要迁移,挑一个业务低峰、并且把旧配置原样备份出来,出问题时能一条命令切回去,这比什么都重要。
总结一下这篇的要点,也是我自己每次配防火墙都会默念的四句话:先读现状再动手;默认策略 DROP,但改动放在放行规则写全之后;黑名单用 -I,状态跟踪放最前;写完必须从外部扫描验证一遍,还得重启一次确认它活下来了。做到这四点,iptables 这个工具就不会再给你惊喜了。