1. nftables是什么:为什么该从iptables走出来了
先说结论:如果你现在还在用iptables,工作里又经常需要加规则、排查防火墙问题,那nftables值得你花一个下午把它用起来。它不是什么实验性玩具,而是Linux内核从4.x时代开始主推的下一代防火墙框架,现在主流发行版默认装了它,很多云厂商的VPS控制台后端也在用它的语法下发规则。
我第一次认真接触nftables,不是因为追求新潮,而是因为某台机器上的iptables规则实在太多,看起来累,改起来更累。那台机器跑了几个Web服务,还有数据库和SSH,规则前后堆了上百条,每次加一个IP都要小心不要挡住别的服务。后来换成nftables,同样的策略用集合和映射压到不到二十行,一眼能看明白,线上改动也敢动手了。
1.1 一次内核防火墙的换代:底层逻辑变了
很多人以为nftables只是iptables的“新版命令”,这个理解不太准确。iptables背后是内核里五个独立的hook点加一堆专用模块,每加一条规则就是往链表里塞一个结构体,规则一多就慢,语法也被历史包袱拖得比较重。
nftables做了一个关键改变:它把“规则引擎”从内核模块里抽出来,做成一个通用的虚拟机。你写的每条规则,本质是让内核按一组表达式(expression)去执行匹配和动作。表、链、规则、集合这些核心概念仍然保留,但实现方式完全变了:
- iptables的协议栈逻辑写死在多个内核模块里,nftables的匹配逻辑由用户态编译成字节码,内核只负责执行。
- iptables对IPv4和IPv6是两套规则表,nftables用
inet类型一个表同时管IPv4和IPv6,少维护一半规则。 - iptables规则是线性遍历,nftables对集合、映射可以走哈希和位图路径,匹配效率更稳。
- iptables的扩展靠
-m和-j拼模块,nftables直接用表达式组合,语法统一,不再有“这个模块要单独装”的麻烦。
对使用者来说,最直观的感受是:规则写起来更像一种小型描述语言,而不是一串参数。你之前用iptables要-A INPUT -p tcp --dport 80 -j ACCEPT,在nftables里是tcp dport 80 accept,确实短了,而且更接近自然语言。
1.2 灵魂三件套:表、链、规则一次讲清
nftables的基本架构可以压缩成三个词:表、链、规则。
表(table)是最高层容器,必须指定地址簇。常用的有ip(只处理IPv4)、ip6(只处理IPv6)、inet(同时处理两种)。日常服务器我基本只用inet,一次配置省心很多。
链(chain)在表下面,又分成两类:一类是带hook的常规链,比如input、forward、output,它们挂在协议栈的固定位置;另一类是不带hook的普通链,可以用来做规则分组或跳转,比如你在input链里jump auth_scan,跳到一个专门处理认证流量的自定义链。
规则(rule)挂在链上,由两部分组成:匹配条件和处理动作。举个例子:
tcp dport 22 accept这行里tcp dport 22是匹配条件,accept是处理动作,nftables里把这种动作叫verdict(判定)。规则从上到下执行,命中了就执行对应动作并结束本次匹配。
如果你以前写过iptables,可以把表理解成filter/nat那种命名空间,链理解成INPUT/FORWARD/OUTPUT,规则理解成-A后面的条件加动作。大致对应得上,但nftables更灵活,而且不再有filter、nat、mangle那种“内置表”的概念——一切都要你自己定义。
2. 基础语法实战:先跑通第一条规则
直接上手练。下面所有命令我都建议在测试机上跑一遍,比只看文档印象深得多。别怕敲错,nftables的规则提交要么全成功要么全失败,不会出现iptables那种一半生效一半报错的情况。
2.1 命令行结构:一行命令读懂的规律
nftables命令行可以分成两个部分看:
nft [操作对象] [操作内容]常见的操作词有:list(查看)、add(新增)、create(创建,已存在会报错)、delete(删除)、flush(清空)、insert(插入到链头)。
先执行下面两条:
nft list tables nft list ruleset第一条会列出所有表,是纯查看;第二条直接导出当前全部规则集,格式和配置文件一模一样,相当于iptables的iptables-save。我在机器上排查问题的时候,第一步永远是nft list ruleset,先把全量规则看一遍,再决定从哪下手。
新增一张表比较简单:
nft add table inet demo这条命令创建了一张inet簇的表,名字叫demo。接着创建一个带inputhook的链:
nft add chain inet demo input { type filter hook input priority 0 ; policy accept ; }你可能会好奇那对花括号里面的语法是干吗的。这是nftables的块语法,用来给链配置属性:type filter表示这是过滤类型链,hook input表示挂在入口处,priority 0是优先级,多张表都可以挂同一个hook,靠优先级决定先后顺序,policy accept是链的默认策略,也就是没有规则命中时怎么办。注意块内部的语句用分号分隔,行尾的花括号前面也要有分号,这是个新手常见的语法坑。
然后加一条规则:
nft add rule inet demo input ip saddr 192.0.2.10 drop这条规则就一个意思:来自192.0.2.10的包直接丢掉。你把规则里的IP换成自己的测试源地址(比如云主机内网IP),配合抓包工具看到丢包效果,第一条nftables规则就算跑通了。
2.2 第一个完整的nftables脚本
命令行适合临时调试,正式环境我更推荐把规则写成脚本文件,用nft -f加载。好处是规则可以版本化管理,也能在改坏时快速回滚。
创建一个/etc/nftables.conf,内容如下:
#!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; ct state established,related accept iif "lo" accept ip protocol icmp accept tcp dport { 22, 80, 443 } accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } }这个脚本做了一件关键的事:第一行flush ruleset先清空全部规则,确保重复执行不会叠加旧规则。这是和iptables风格差异很大的地方,iptables的-F只清某个表,flush ruleset则是把整个规则集全部重置,所以用的时候要确定这台机器只有nftables在管防火墙,否则会连带清掉别的工具的规则。
ct state established,related accept是状态匹配,允许已经建立连接的回包通过,没有这行的话,光放行TCP 443端口是打不开网页的,因为回包会被policy drop挡掉。iif "lo" accept放行本地回环,一些本机进程之间的通信依赖这个。tcp dport { 22, 80, 443 }是nftables的集合语法,花括号里写多个端口,等价于iptables里连写三条规则,简洁得多。
保存后用nft -f /etc/nftables.conf加载,执行完没有任何输出就是成功。然后马上开一个新SSH会话验证,别把现有连接关掉,这是防火墙操作的铁律。
2.3 规则匹配的核心:表达式与verdict
脚本看懂了,还得懂原理,不然遇到“规则没生效”会一头雾水。
nftables的匹配条件由表达式组成,每个表达式完成一类检查。常见的表达式几类:
- 链路层:
ether saddr、ether daddr,匹配MAC地址。 - 网络层:
ip saddr、ip daddr、ip protocol,另外IPv6对应的是ip6 saddr等。 - 传输层:
tcp dport、tcp sport、udp dport、icmp type,也就是四层端口和类型。 - 状态层:
ct state,匹配连接跟踪状态(new、established、related、invalid)。 - 元数据层:
iif、oif、meta mark,匹配网卡、mark等。 - 负载层:
payload,直接提取包内容做高级匹配。
动作部分就是verdict,有五种常用值:
accept:放行,停止当前链的继续匹配。drop:丢弃,效果等同iptables的DROP,包直接消失。reject:拒绝,给源端回一个错误应答(比如TCP RST或ICMP端口不可达)。queue:把包交给用户态进程处理。jump/goto:跳转到指定链继续处理。区别是return后会不会回到原链的下一条规则。
我把一条规则内部的执行逻辑用一句话概括:按顺序执行每个表达式,全部匹配就执行verdict,然后结束本轮规则匹配。要特别注意,如果一条规则只有匹配条件没有写verdict,那它就只是“看一眼”然后放过去,继续匹配下一条规则。很多新手在这上面踩坑,以为写了条件就代表拦截了,实际上还得补上drop或accept。
3. 服务器场景常用配置示例
光会跑通语法还不够,真正有价值的是把常用场景的配置直接拿出来用。接下来按服务器日常运维的高频需求来拆,每段配置都可以直接抄,根据实际网段和端口改一下就行。
3.1 单机防护标配:从放行到拦截
单台Linux服务器最常见的角色是Web服务器或者业务后端,典型需求是:SSH只允许特定IP或跳板机访问,Web端口(80/443)开放给公网,其他端口一律拦住,同时防一下常见的扫描。
下面的配置我用policy drop做默认拒绝,只放行明确允许的流量。这种方式比“默认放行、单独拒绝”更安全,规则也少:
table inet filter { chain input { type filter hook input priority 0; policy drop; ct state invalid drop ct state established,related accept iif "lo" accept tcp dport 22 ip saddr { 203.0.113.0/24 } accept tcp dport { 80, 443 } accept ip protocol icmp accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } }ct state invalid drop是很多人忽略的一条。连接跟踪状态为invalid的包通常是异常分段、伪造请求或者状态失配,直接丢能挡掉一部分扫描和攻击流量。放行链里放established,related后,回包和外连的应答流量都能自动通过,不需要逐个放行高位端口。
关于ICMP我要说一句:ip protocol icmp accept表示放行所有ICMP类型,这对ping和traceroute的排查有帮助。如果安全要求高,可以只放行icmp type echo-request,其余类型丢弃或限速。不过在内网环境里,完全禁掉ICMP反而会让自己排查问题的时候抓瞎。
3.2 集合与映射:告别一条条手写规则
规则多起来以后,最怕的就是“这个IP之前加了没有?”iptables时代我经常要iptables -L -n | grep IP。nftables的set(集合)和map(映射)把这个问题解决了。
集合就是一组相同类型的数据,可以动态增删。声明一个集合:
table inet filter { set bad_ips { type ipv4_addr flags interval elements = { 198.51.100.1, 198.51.100.2 } } chain input { type filter hook input priority 0; policy accept; ip saddr @bad_ips drop } }注意集合元素用@bad_ips引用。加了flags interval后,元素里可以写网段,比如198.51.100.0/24,内核会按区间匹配,效率和单条规则差不多,不会因为段多而退化成遍历。我在生产环境用一个集合维护所有被封的扫描源IP,高峰期维护了几千个条目,匹配没感觉到延迟。
维护集合用命令行:
nft add element inet filter bad_ips { 203.0.113.50 } nft delete element inet filter bad_ips { 203.0.113.50 }映射比集合更进一步,它是“键-值”结构。比如你想对不同端口跳转到不同处理链:
table inet filter { map jump_map { type inet_service : verdict elements = { 22 : jump ssh_chain, 80 : jump web_chain, 443 : jump web_chain } } chain input { type filter hook input priority 0; policy drop; tcp dport vmap @jump_map } chain ssh_chain { ip saddr 192.0.2.0/24 accept log prefix "SSH_JUMP: " drop } chain web_chain { accept } }tcp dport vmap @jump_map是个很妙的写法:用目标端口做键,查到对应的verdict就直接执行,跳转到对应链。规则数从“端口数”降到一条,后面加端口只要改映射里的元素就行,不用改链逻辑。
3.3 限速、端口转发与动态黑名单
再进阶一点,看看限速和端口转发,这两个场景在服务器上经常遇到。
限速防暴力破解,nftables用limit表达式就能搞定。下面这条规则限制每台来源IP每分钟最多发起5个新SSH连接,超过的直接丢:
table inet filter { chain input { type filter hook input priority 0; policy accept; tcp dport 22 ct state new limit rate 5/minute accept tcp dport 22 ct state new log prefix "SSH_DROP: " tcp dport 22 ct state new drop } }它怎么生效的?逻辑是这样:第一条规则命中tcp dport 22且是new状态时,如果没超过限速就直接accept;一旦速度超标,第一条规则的limit匹配失败,落到第二条记录日志,再落到第三条drop。用ct state new限定只统计新建连接,是因为正常业务的已建立连接不希望被限速误伤。
端口转发用dnat,把公网进来的某个端口流量转发到内网某台机器:
table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; tcp dport 8080 dnat to 192.168.1.100:80 } chain postrouting { type nat hook postrouting priority 100; policy accept; oifname "eth0" masquerade } }prerouting链负责改目标地址,dnat to 192.168.1.100:80把流量指到内网。postrouting里的masquerade做源地址伪装,相当于iptables的MASQUERADE,常用于让内网机器共享这台服务器的公网出口。
如果你要更精细地控制内网出网流量,可以给内网机器单独建一个forward链,对来源地址和目的地址分别做匹配。
动态黑名单这个需求有点意思。以前我用iptables处理突发扫描,要先写脚本捕获日志再调iptables -A INPUT -s IP -j DROP,脚本复杂,还容易挨个刷屏。换成nftables的集合以后,直接写一个简单的循环脚本就行:
TABLE=inet SET=blocklist while read ip; do nft add element inet blocklist "$ip" 2>/dev/null done < attacker_ips.txt刚才定义的集合配合policy accept,只要把来源IP加进去,流量就会被默认丢弃规则挡掉。因为集合是引用数据结构,增删元素不影响链里的规则,所以动态更新非常轻量。实际使用中还可以把ipset迁移过来,nftables的集合语法比ipset更统一,不再需要额外安装工具包。
3.4 从iptables平滑迁移
很多人手里有大量iptables规则,担心迁移成本。这个话题绕不开,我直接给结论:如果规则不超过几十条,手工迁移做一次就行;如果上百条,用工具转。
nftables自带一个转换工具,先把iptables规则转出来,再喂给nft命令。
先保存现有iptables规则:
iptables-save > /tmp/rules.ipt然后转换:
iptables-restore-translate -f /tmp/rules.ipt它会在屏幕上输出对应的nftables语法。这个输出不保证100%完美,像-m string这类复杂扩展可能无法完全映射,但因为转换结果直接就是nft -f能读懂的格式,你可以先保存下来调整,再加载。
我自己的经验是:转换工具只是起点,不是终点。转完之后,把重复的规则合并成集合,把逻辑相近的规则分组到子链,迁移的过程反而是清理规则的好机会。很多iptables规则在历史迭代中已经失效了,只是没人删,换到nftables正好做一次瘦身。
4. 调试与排错:别慌,先看这几张表
配置防火墙总会遇到“规则没生效”或者“服务怎么访问不了了”的情况。这里有一套我实测顺手的调试流程,从查看规则到追踪包,一步到位。
4.1 常用调试命令与trace
nft list ruleset是第一步,看当前规则集的全貌。但输出可读性对复杂配置不太友好,我更常用nft list table inet filter只看某个表。
如果确认规则在,但行为不对,下一步用nft -t开trace。注意要先在链里加一条trace规则,因为-t只是让trace输出可见,触发点还是要规则。举个例子:
nft add rule inet filter input tcp dport 80 trace accept加了之后,命中80端口流量的包会打出trace日志,再用dmesg或journalctl -f看。日志里能看到包经过了哪些规则、每条规则的匹配结果、最后执行了什么动作。这一步能把“规则为什么没生效”从玄学变成可分析的问题。
还有一个经常用到的命令是nft monitor trace,它会实时输出内核上报的trace事件,调试时不看日志文件直接看终端,效率更高。我用这个方式排查过一次诡异问题:规则明明是按顺序写的,但总有包没按预期落到drop。监控trace的时候发现,问题出在较早的一条accept规则上,它把目标流量提前放开,后面所有规则根本没机会执行。
查看计数器也很有用。给规则加counter后:
nft add rule inet filter input tcp dport 22 counter accept每次命中这个规则,计数器会递增。用nft list ruleset查看时就能看到命中次数,这是判断规则是否真正被流量命中最直接的手段。我在排查生产问题时,习惯先把关键链上的规则都补上counter,观察一段时间再根据计数决定规则去留。
4.2 常见错误与排查速查表
直接上我整理过的一份速查表,基本覆盖了日常踩坑:
| 现象 | 原因 | 解决办法 |
|---|---|---|
Operation not permitted | 权限不足,或规则与内核配置冲突 | 检查是否root;确认模块已加载 |
File exists | 同名表/链/集合已存在 | 用add改成create前先确认,或直接flush ruleset |
| 语法报错:semicolon | 块内语句之间少分号 | 块内每条语句以分号结尾 |
| 规则没生效 | 规则没有写verdict,只是匹配后继续 | 补上accept/drop |
| 规则顺序问题 | 前面的放行规则先命中了 | 调整规则顺序,或用insert插到链头 |
| 端口不匹配 | 没指定协议就写端口 | 先写tcp dport或udp dport |
| IPv6连不通 | 只在ip表里写了规则 | 用inet簇统管v4和v6,或单独建ip6表 |
| 系统重启后规则没了 | 没有持久化 | 用nft -f加载配置文件或接入systemd服务 |
加载时报Conflicting priorities | 多个链挂在同一hook,优先级冲突 | 检查各表的priority值是否重复占用 |
这些坑我自己基本都踩过一遍。最典型的是规则顺序问题:在我的一个后端机器上,想封某个IP段,把封禁规则放在accept规则后面,结果永远不生效。原因很简单,包在前面已经accept了,根本轮不到后面的规则。所以新增拦截规则时,我会用nft insert rule而不是add rule,插到链首去。
4.3 性能与维护习惯
nftables的性能在同等逻辑下通常不会比iptables差,尤其在规则多时优势更明显。内核里集合用哈希表维护,匹配路径变成了常量时间而不是线性扫描。我实测过一张含几千个IP的集合,对比iptables的相同规则,CPU占用有可感知的下降,不过在规则量小的时候两者差异基本可以忽略。
但性能不只是内核的事,配置习惯影响更大。我给自己定的几条规则,用了很久很顺手:
第一,每张表都要有明确用途。表名不要用test、new这种意义不明的名字,建议按业务命名,比如inet proxy、inet db_firewall。第二,所有环境都用inet簇加flush ruleset的开头,不要混用多簇表,不然v4规则生效、v6规则没生效的问题迟早找上门。第三,规则里的IP段和端口优先写成具名集合,业务IP变更时只改集合元素,不用动主规则。第四,关键规则带counter和comment,comment相当于一条注释,几个月后回看配置时,能记住当时为什么加这条规则。
关于规则持久化,不同发行版方式不一样,但思路一致:把规则存成/etc/nftables.conf,开机自动加载。我之前在一台机器上手动敲规则,运行了几个月,一次意外重启后规则全丢,防火墙大开,从那以后不管多急的改动,都会同步更新配置文件。你可以在/etc/nftables.conf里写规则集,然后测试nft -f /etc/nftables.conf能正常加载,再启用对应systemd服务。
最后分享一个维护小技巧:给规则加comment的习惯。nftables的comment语法很简单,就是在规则末尾加一句描述,例如:
tcp dport 22 accept comment "Allow SSH from ops network"几个月后再看规则,这行注释能立刻告诉你当年的意图。我见过太多防火墙规则集,满屏的tcp dport 80 accept,根本没人记得是哪次变更加进来的。规则本身不说话,但注释会。想要运维的时候少掉几根头发,这个习惯越早养成越好。