☰
Linux防火墙从入门到进阶:netfilter、iptables与nftables全解析
2026/10/19 0:32:48 网站建设 项目流程

如果你和我一样,第一次是在一台刚开机的服务器上摸索Linux防火墙,大概率会经历这样的尴尬:以为敲几条命令、把自己需要的端口放行就万事大吉,结果一刷新网页,发现连管理端口都进不去了。我后来花了很长时间才彻底想明白一个问题——Linux防火墙不是某个单一命令,而是Linux内核里一套完整的数据包过滤体系。你用什么管理工具,只是选择坐在哪一层操作台面前而已。

这篇内容我会从内核的数据包路径讲起,一路聊到nftables、iptables这些常见工具,再给出可以直接抄的配置示例、生产环境里经常用到的限速和转发写法,最后重点讲讲“规则失效”和“性能损耗”这两个最容易被忽视的坑。不管你是刚接触Linux的新手,还是已经能看懂一大堆规则、但还说不清背后原理的运维,都能在里面找到对应自己水平的那一层。

1. 数据包在内核里的“检查站”:先弄懂包是怎么过五关的

1.1 五个钩子点:一台Linux设备对数据包的三条处置路径

Linux防火墙之所以让你改一条规则就断连,本质上是因为内核在网络路径上提前安排了若干个“检查站”,每个数据包经过时都要停下来被问一遍:你是谁、从哪来、要到哪去、我放不放你过。这些检查站在netfilter框架里被称为钩子点,iptables和nftables写的规则,最终都要挂到这些钩子上。

一个数据包进入Linux设备之后,通常有三种命运:要么是发给本机进程的,要么是需要转发给别的机器的,要么是本机进程自己发出去的。为了覆盖这三种情况,netfilter一共设置了五个核心钩子。每个钩子后面都挂着一张或多张表,表中的链决定这个包要不要被放行、要不要被改写地址、要不要记录状态。

为了让你快速建立画面感,我把五个钩子、对应链和职责整理成了一张表:

钩子/链出现时机主要职责
PREROUTING包刚进入网络层,还没做路由决策目标地址改写(DNAT)、报文标记、必要的地址转换前置处理
INPUT路由决策后确认包目的地是本机本机进程是否允许接收该包
FORWARD路由决策后确认包只是路过,需要转发本机是否允许继续转发这个包
OUTPUT本机进程生成的数据包刚进入网络栈本机进程是否允许发出这个包
POSTROUTING包即将从网卡发出去之前源地址改写(SNAT)、伪装、最后一次改写机会

这里最容易被忽略的是FORWARD链。很多人以为防火墙只负责“挡外部请求”,于是在INPUT链上写满了规则,结果要做端口转发时发现流量死活过不来。原因就是那个需要被转发的包走的根本不是INPUT,而是FORWARD。这也是我为什么强调,上手第一件事不是抄命令,而是先搞清楚你要处理的流量到底要过哪条链。

钩子点之上还有“表”这一层。最常用的filter表管放行和丢弃,nat表管地址改写,mangle表管报文标记,raw表管是否跳过连接跟踪。数据包在某个钩子处会按照raw、mangle、nat、filter的顺序依次检查,这也解释了为什么规则顺序一旦乱了,行为就会非常诡异。

1.2 iptables、nftables、firewalld、ufw分别站在哪一层

搞清楚数据包路径之后,再看那些花花绿绿的命令工具就顺眼多了。我经常遇到有人把iptables和防火墙上强放行规则混为一谈,其实它们只是面向不同操作层级的管理工具。

iptables是从Linux 2.4时代一直用到今天的老一代用户态配置工具,它把规则翻译给内核的netfilter框架执行。优点是资料多、排错经验丰富,缺点是规则语法比较繁琐,而且规则一多,匹配效率就成了需要操心的问题。

nftables是新一代的包过滤框架,它不只是换个命令,而是把内核里的规则处理引擎整个重写了。相比之下语法更紧凑,支持集合、查表和更灵活的分类,规则处理性能也有明显提升。现在很多新版的发行版里,你执行iptables命令,底层其实已经通过兼容层转成了nftables规则。对普通使用者来说,旧习惯还能继续用,但新环境我更建议直接学nftables。

firewalld和ufw则属于更上层的东西。firewalld是一个常驻的防火墙服务,它把规则组织成一个个“区域”,按来源网段、接口去套用不同的放行策略,适合需要经常动态调整的场景。ufw则更简单,就是给单机快速开端口用的前端封装。很多人觉得有firewalld就不需要懂底层了,但事实是firewalld最终要把规则翻译成内核识别的规则集,你手工加一条iptables规则,它一reload就可能被覆盖掉。所以选工具时第一条原则:一台机器认证一种管理方式,别混着来。

我自己的建议是:如果是自己的Linux桌面或家里的小服务器,ufw这种傻瓜式工具完全够用;如果是在正经机房环境里维护防火墙规则,无论用firewalld还是直接写nftables,都一定要搞清楚最终落到内核里的规则长什么样,排错时才能抓住根源。

2. 从零开始配一套能用又持久的规则集

2.1 动手之前先做三个决定

直接上手敲命令之前,我建议你先坐下来做三个决定,哪怕每个决定只花五分钟,也能避免后面整晚的折腾。

第一个决定是用哪种管理工具。这里没有万能答案,主要看你所在发行版的习惯和你手头要维护的机器规模。如果你之后要管理几十台机器,我倾向用firewalld或直接统一用nftables配置文件,方便用脚本批量生成;如果只是单台机器临时开几个端口,ufw就够了。

第二个决定是默认策略选DROP还是REJECT。这是防火墙配置里最经典的分歧。DROP相当于把请求丢进黑洞,客户端那边没有任何回应,只能一直重试到超时;REJECT则会主动回应一个“不可达”或“被拒”的差错,客户端马上就知道这个端口不服务。两种策略的适用场景差别很大:

策略表现适用场景
DROP无响应,客户端反复重试直到超时对外隐藏内部服务,减少端口扫描反馈
REJECT立即明确拒绝,客户端很快知道结果内网调试,能快速定位连接问题

从安全性看,DROP不会主动暴露“这里有台机器在挡我”,但代价是排查问题时很难分清对方是网络不通还是被防火墙丢了。我记得自己在内网里调试服务时,习惯用REJECT,反馈快;到了公网入口,才用DROP减少暴露面。

第三个决定是列出明确的放行清单。哪些端口是管理端口,哪些是业务端口,哪些来源网段允许访问,这些都应该先写在一张纸上。放行清单越清楚,规则集就越简单,也就越不容易出安全问题。很多人最后变成“防火墙规则写了几百条,自己都忘了哪些在用”,基本都是跳过了这一步。

2.2 一份可以直接抄的nftables配置

确定好三个决定之后,就可以写规则了。我平时最常用的是nftables,下面给出一份默认丢弃入站、放行已建立连接、放行本地回环、放行管理网段SSH和Web端口的完整配置:

flush ruleset table inet myfilter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iifname "lo" accept iifname "eth0" ip saddr 192.168.1.0/24 tcp dport 22 accept iifname "eth0" tcp dport 80 accept iifname "eth0" tcp dport 443 accept ct state invalid drop counter log prefix "input-drop: " drop } chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept } chain output { type filter hook output priority filter; policy accept; } }

这份配置里每一行都有它存在的理由。ct state established,related accept放行所有已经建立的会话以及关联的子连接,比如FTP数据连接或某些主动模式连接,没有这一行,你从外部能建立TCP握手,但后续数据传输可能全部被自己拦掉。iifname "lo" accept放行本地回环口,很多人漏掉这一行,结果本机上的多个服务之间互相访问却超时,定位起来特别痛苦。最后那个log prefix很有价值,凡是走到默认丢弃策略的包,都会打一条带前缀的日志,排查外部扫描和规则问题时,它就是你的账本。

这里要特别澄清一点:limit限制的不是每个来源IP,而是全局包的到达速率。也就是说这个例子里的SSH限流,所有来源IP加起来每秒最多进来3个新连接,看起来挺粗暴,但对个人小服务器来说已经能挡住绝大多数傻乎乎的暴力扫描了。真正精细到“每个IP每分钟只能连几次”,需要用到nftables里更高级的meter或hashlimit机制,我在后面动态封禁那一节会再展开。

2.3 持久化:你改的是内存,不是磁盘

新手最容易栽的跟头,就是配置完成之后没有做持久化,然后某次机房重启,所有规则全部消失,服务器裸奔在公网上。这个问题我见过太多次,所以每次都会把它单独拎出来强调。

用iptables命令或nft命令直接添加的规则,默认只存在于内存里的内核运行态。规则是立即生效,但重启后内核会按照默认配置重新初始化,你之前辛苦调好的那堆东西就没了。想要持久化,得把当前生效的规则导出成文件,再让系统开机时自动加载。

对于还在用iptables的机器,做法通常是:

iptables-save > /etc/iptables/rules.v4 ip6tables-save > /etc/iptables/rules.v6

然后安装一个开机加载这些文件的工具,或者自行配置一个系统服务,在启动早期把这些文件原样恢复回去。nftables就舒服一点,因为它的配置文件本身就支持整套规则集描述。你写好/etc/nftables.conf之后,用nft -f /etc/nftables.conf一次性加载,开机时服务会自动读取并应用。

写配置时还有一个小技巧:先把脚本或配置文件放在一个固定目录下,改动前备份一份当前生效的规则。哪怕只是加一条端口放行,也要保证随时能回滚到上一个可用状态。这个习惯在远程服务器上尤其重要,因为一旦你写错规则把自己SSH禁掉,如果手边没有能立即恢复的备份,就只能去物理终端或管理面板里重置了。

3. 拦截之外的进阶玩法:限速、转发与动态封禁

3.1 用limit给SSH加道限流

基础放行规则配好之后,很多人就认为防火墙“做完了”。其实只做到这一步,就像给大门装了锁但没装监控,遇到暴力攻击时你还是会头疼。生产环境里最常遇到的场景之一,就是SSH端口被无数来源IP轮番扫描试密码。

暴力扫描的特征是短时间内产生大量新建SSH连接。我们不需要挨个把扫描IP封掉,只要在规则层面给这个端口加上速率限制,扫描行为就会立刻暴露甚至直接被丢弃。nftables里可以用limit表达式做简单的速率控制,比如我前面提到的那个SSH限流规则:

tcp dport 22 limit rate 3/second accept tcp dport 22 log prefix "ssh-limited: " drop

这两行的意思是:如果到达SSH端口的包速率没有超过每秒3个,就放行;一旦超过,后续的包先记一条日志,然后直接丢弃。实际效果就是正常人的手工连接几乎不会受影响,但像爆破脚本那样的高频尝试就会一头撞在墙上。

我之所以说这是“简单限流”,是因为limit是按全局匹配的,所有来源IP共享同一个速率。如果有人故意用很多不同的IP同时做低频扫描,还是可能绕过去。真要更细致地按IP区分,可以用hashlimit或者nftables的集合加meter配合实现。不过在那之前,至少先把密钥登录用起来,很多暴力攻击的麻烦根本就不会出现。

3.2 DNAT端口转发与SNAT:让防火墙同时当路由器

有一类需求非常常见:内网里某台机器提供了Web服务,但我不想直接暴露它的内网地址,只想让外界通过防火墙的公网端口访问。这种场景就要用到DNAT,也就是在PREROUTING阶段改写数据包的目标地址。对应的nftables配置可以写成这样:

table ip nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; iifname "eth0" tcp dport 8080 dnat to 192.168.1.50:80 } chain postrouting { type nat hook postrouting priority srcnat; policy accept; ip saddr 192.168.1.0/24 oifname "eth0" masquerade } }

上面第一条规则,把从外网接口进入、目标端口是8080的TCP包,改写成目标地址192.168.1.50的80端口。第二条规则是SNAT的伪装形式,让内网机器访问外部网络时统一使用防火墙自己的地址返回数据,这样内网出网不会回不了包。

这里我重点提醒一下容易漏掉的步骤:DNAT只解决“目标地址改成谁”,但数据包真正要从防火墙转发到内网服务器,还得经过FORWARD链。很多人在nat表里配好了转发,却发现还是不生效,一查才想起FORWARD链默认drop。所以转发场景一定要在filter表的forward链里,把从外网接口到内网接口、目标端口为80的流量放行掉,同时放行已建立连接的回程流量。

iptables老用户可能会更喜欢等价的写法,我顺便放出来对比一下逻辑:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.50:80 iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE iptables -A FORWARD -i eth0 -o eth1 -p tcp --dport 80 -j ACCEPT iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

从这个对比就能看出来,iptables和nftables只是同一套逻辑的不同表达。理解数据包路径后,你只需要学一次网络层面的原理,所有工具对你来说都只是语法差异。

3.3 用IP集合做动态封禁,替代成百上千条静态规则

如果你的服务器经常被特定来源IP骚扰,最朴素的办法是用脚本逐条添加DROP规则。但IP数量一多,规则列表会变成上千行的巨兽,性能和维护性都会出问题。更优雅的方案是使用nftables的集合特性。

先定义一个IP集合,约定里面元素的生存时间,然后让规则直接以集合为判断条件:

table inet myfilter { set banned_ips { type ipv4_addr flags timeout timeout 1h } chain input { type filter hook input priority filter; policy drop; ip saddr @banned_ips drop } }

后面想封那个IP,就执行一条添加命令:

nft add element inet myfilter banned_ips { 203.0.113.7 }

这条命令把IP加入集合,之后该地址的包会立即被规则丢弃,完全不需要再新增一条规则。集合里的元素带了一个小时的超时时间,时间到了会自动消失,很适合对扫描IP做临时封禁。

这种做法的好处是显而易见的:不管集合里有多少个IP,规则集本身始终只有一条判断规则,性能和可维护性都远好于逐条加规则。我在网关设备上维护过大量来源IP封禁,几十条临时封禁IP用脚本维护都很吃力,改用集合之后,封禁与解封直接成了两个原子操作。

4. “防火墙失灵”的排查思路:先看计数器,再谈优化

4.1 计数器不会说谎:第一步永远是看命中

我在网上看到过无数次“我加了规则但就是不生效”的求助帖,自己也栽过,先说结论:绝大多数“防火墙不生效”不是命令写错了,而是没搞清楚包到底经过了哪条链,或者规则顺序不对。

遇到规则失效,别急着改规则瞎试,先回答一个问题:数据包到底有没有走到这条规则面前?回答这个问题最直接的办法是看计数器。nftables和iptables的每条规则都会自动统计匹配了多少包、多少字节。用下面命令能把计数看得很清楚:

nft list ruleset iptables -nvL --line-numbers

如果一条规则命中数始终是0,说明包根本就没走到它面前。这时候要检查几件事:接口名是不是写错了,源IP或目标端口是不是匹配错了,更重要的是确认这个包走的到底是INPUT链还是FORWARD链。如果命中数在增长,但行为还是不对,那就说明有更早的规则先把流量放行或者丢弃了,需要按序号逐条看决策路径。

我排查时还有一个常用手段:给可疑位置加一条带日志的规则,专门记录被丢弃的包。比如:

counter log prefix "debug-input: " drop

然后在另一个终端窗口看系统日志或journal日志,就能看到这个包是在哪一步被处理的。再配合tcpdump抓包,确认包确实到达了防火墙,排错链路基本上就能一步到位。我给普通用户整理了一套快速自查清单:

现象最可能的原因
规则命中数为0匹配条件不对,端口、协议、来源IP或接口写错
命中数在涨但行为不对前面有更高优先级的规则先做了决策
手动加的规则重启后消失没有持久化,或者被防火墙服务reload覆盖
设备上看着有规则,网络仍不通数据包根本没走到这条链,或走了另一块网卡

4.2 管理工具互踩:firewalld和手工规则的冲突

第二种“失灵”很有迷惑性:你用iptables -A加了一条规则,当时测试是通的,隔了一段时间或者做了一次reload,规则又没了。

真相是,firewalld或ufw这类上层管理工具会维护自己的一套规则配置。每次reload它都会重新生成整个规则集,而你手工加进去的那些运行时规则,不在它的管理范围里,自然会被清得干干净净。这一点在firewalld上体现得尤其明显。

所以我的建议是:既然装了firewalld,就用它自带的接口来管理规则,不要一边用firewall-cmd,一边又直接改iptables。比如想让某端口永久放行,正确的做法是:

firewall-cmd --permanent --add-service=http firewall-cmd --reload

这里的--permanent是关键,它表示写入持久化配置,而不是只作用于当前运行时。如果不加这个参数,直接reload之后规则也会丢。

如果实在需要自己完全掌控底层规则,那就要果断放弃firewalld这个中间层,直接关掉它并改用nftables配置文件管理。两条路都能走得通,最怕的是两个工具同时上手,最后谁都说不清规则到底是谁写的。

4.3 容器环境带来的“透明防火墙”错觉

这几年还有一个新的坑,就是容器运行时会在宿主机上自动创建一套虚拟网卡、桥接设备和自定义链。很多人在容器环境里排查防火墙问题时,沿用过去的做法——清空防火墙规则,从头开始配,结果发现容器之间的通信、容器访问外部网络的能力也跟着“被清空”了。

这不是防火墙本身的问题,而是你没有意识到,容器运行时早就往netfilter框架里注入了一整套自己的规则链,用于实现端口映射和网络隔离。如果你直接flush ruleset或清空iptables规则集,等于把这套自动维护的链也一起删了,容器网络当然会全面瘫痪。

遇到这种情况,最稳妥的做法是不要整链清空,而是只操作你自己创建的表或链,保留容器运行时管理的那些链。更省心的方案则是换一个思路:尽量避免在宿主机上对转发流量做过多干预,把需要对外暴露的服务放到宿主机的端口映射层去处理,让容器运行时自己去协调网络规则。简单说,就是别把容器宿主机的防火墙当成传统单机防火墙来一把梭,它背后的规则生态比你想象的要复杂。

5. 规则之外:连接跟踪、性能损耗与变更回滚

5.1 conntrack满表:流量莫名其妙变差

防火墙规则本身写得没问题,但服务器流量还是莫名变差,甚至新连接全部超时,已经建立的连接却一直正常,这种时候别急着怀疑网络线路,先看看连接跟踪表是不是满了。

Linux防火墙靠conntrack机制记录每个连接的状态,这也是前面ct state established,related识别已建立连接的基础。但连接跟踪是有上限的,它需要占用内存,一旦条目数达到最大值,内核就会丢弃新连接的数据包,导致一种非常诡异的现象:服务器负载正常,ping也通,但所有新连接都建立不起来。

检查方式非常直接:

sysctl net.netfilter.nf_conntrack_max sysctl net.netfilter.nf_conntrack_count

如果count已经非常接近max,说明连接跟踪表快要爆了。常见的解决办法是适当调大上限,比如:

sysctl -w net.netfilter.nf_conntrack_max=1048576

不过调大上限要结合机器内存评估,因为每条conntrack条目都会占固定开销。更根本的方式是缩短超时时间,把那些早已不活跃的会话更快清理掉,比如减少TCP连接在关闭后仍被跟踪的时间。尤其是在大量NAT转发的网关上,这个表简直就是最容易爆的瓶颈,我见过太多现场了,有条件的话最好提前监控这两个数值。

5.2 规则集的维护习惯:把几百条规则压到几十条

防火墙配好之后,剩下的工作就是长期维护。我以为维护最大的敌人不是策略复杂,而是规则集混乱,乱到后来没人敢动,只能靠继续往后面追加规则来“止血”。

我见过一台服务器上iptables有上千条规则,其中大部分是按块逐条添加的IP丢弃规则,匹配效率差不说,维护的人看一眼都要头疼。实际上,这类需求用集合合并成一条规则就行,前面已经演示过。规则集的健康程度不取决于条数多少,而取决于结构和注释。

我个人的维护习惯有这么几条:

  • 始终把ct state established,related accept放在链的最前面,先放行已建立连接,再处理新连接。
  • 常访问的流量放在前面,比如Web端口放行,要排在低频或很少出现的规则之前。
  • 给每条规则加注释,尤其是NAT规则,说明这条规则是哪个业务用的、当初为什么加。
  • 用集合或端口组减少重复条目,而不是逐条加IP和端口。
  • 规则文件按功能分区,入站、转发、出站、NAT分开写,方便快速定位。

这些习惯很朴素,但在真实维护过程中非常救命。规则集是一个长期演化的产物,今天加的每一条规则,几个月后可能连当时写的人都看不懂,更何况接手的人。

5.3 用整文件加载和版本管理兜底

最后聊聊变更回滚。远程改防火墙规则最大的心理压力,就是怕把自己关在门外。我经历过几次之后总结出几条非常管用的保底手段。

第一,尽量使用整文件加载,而不是逐条执行命令。nftables支持先做语法检查,再原子加载:

nft -c -f /etc/nftables.conf nft -f /etc/nftables.conf

-c参数只做检查,不真正加载,语法的错误在还没有影响运行的时候就暴露出来。nft -f加载整份配置时,规则集作为一个整体提交,如果中途有错会直接回退,不会留下一个加载了一半的破碎状态。iptables对应的做法是iptables-restore < /etc/iptables/rules.v4,同样具有原子性。

第二,每次变更前把当前生效规则导出一份作为回滚点。文档或脚本里存的是一份理想状态,机器上当前正在运行的又是另一份状态,两者可能早就不一致了。强制要求每次操作前备份运行时规则,关键时刻能直接一键还原。

第三,把规则文件纳入版本管理。这不是什么高大上的操作,就是把配置文件放进一个git仓库,每次变更都留一句备注。几个月后回看,哪条规则是为什么存在的,一眼就能看出来。我每次帮别人处理完防火墙问题,都会顺手问一句“你这些规则有没有记录原因”,绝大多数人这时候都会愣一下,然后就明白了我的意思。

写到这里,再分享一个我自己常用的保命技巧:在远程机器上调试防火墙时,先加一条临时的全放行规则,再按顺序做实验,实验结束后用定时任务把备份的旧规则恢复。也就是说,永远给自己留一条“只管敲命令,不怕断SSH”的后路。我后来把所有规则文件都放进版本管理,每次变更带一行备注,几个月后回看,哪些规则为什么存在,一眼就能看出来。Linux防火墙的学习曲线确实有点陡,但只要先把数据包路径、工具分层、持久化、排查这几条主干吃透,后面加多少条规则都不会再慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询