SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Ubuntu 上的 iptables 与 nftables:如何查看实际规则

Ubuntu 的 iptables 命令实际上在写入 nftables 规则。本文教你如何验证后端配置,查看被 ufw 和 Docker 隐藏的规则集,并解决 iptables 与 nftables 同时管理规则导致的冲突问题。

Ubuntu 上的 iptables 与 nftables:你的服务器正在运行哪一个?

在 Ubuntu 20.04 及更高版本中,iptables 命令是一个写入 nftables 规则的前端。内核中运行着一个包过滤器 nftables,并由两个用户空间命令对其进行编程。iptables -A INPUT 行依然可以像往常一样工作,它创建的规则是 nftables 规则,nft 可以将其打印出来。

在相信这一点之前,请先在自己的服务器上进行确认。

iptables -V
sudo update-alternatives --display iptables
sudo nft list ruleset

在 Ubuntu 24.04(截至 2026 年 8 月,iptables 版本为 1.8.10)上,iptables -V 会打印 iptables v1.8.10 (nf_tables)。方括号中的名称是后端。(nf_tables) 表示该命令与 nftables 通信。(legacy) 表示旧的 x_tables 后端,Ubuntu 仍将其作为 iptables-legacy 提供,且内核将其保留为完全独立的规则集。update-alternatives 会打印该选择背后的符号链接:link currently points to /usr/sbin/iptables-nft

在未配置防火墙的全新 VPS 上,sudo nft list ruleset 不会打印任何内容。该空输出即为你的基准。以旧方式添加一条规则,然后再次查看。

sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
  chain INPUT {
    type filter hook input priority filter; policy accept;
    tcp dport 22 counter packets 0 bytes 0 accept
  }
  chain FORWARD {
    type filter hook forward priority filter; policy accept;
  }
  chain OUTPUT {
    type filter hook output priority filter; policy accept;
  }
}

你的 iptables 规则实际上是一条 nftables 规则。iptables-nft 标记了它所创建的表,而 nft 在看到该标记时会打印警告,因为使用 nft 编辑此类表会导致两个工具同时管理相同的规则。观察一条命令产生的结果:一个你未命名的表,以及你未请求的链。这是旧模型,也是当你直接编写 nftables 时首先会改变的内容。

iptables -L 对您隐藏的内容

iptables -L 仅显示 filter 表。NAT(网络地址转换)规则需要 iptables -t nat -L,而 mangle 规则需要 -t mangle。IPv6 存在于独立的命令 ip6tables 中,且拥有自己的一套规则副本。因此,一台服务器可能在某次查看时看起来很干净,但实际上有其他表在丢弃或重写您的数据包,而您从未检查过那些表。

sudo nft list ruleset 可在一次输出中打印所有协议族、所有表、所有链及所有规则。在非您本人构建的服务器上,该命令是查看当前实际加载内容的最高效方式。添加 -a 可打印规则句柄,若要删除单条规则而非清空整个链,则必须使用此参数。

在此过程中,有两个习惯值得修正。iptables -L 会将地址和端口解析为名称,因此在解析器故障的服务器上,该命令看起来会像卡住了一样:请改用 iptables -nvL。此外,请使用 sudo iptables-legacy -nvL 确认旧版后端为空,因为如果两个后端中同时存在规则,内核会同时评估两者,而任何单一列表都无法向您展示全貌。

自行创建而非继承的表与链

nftables 启动时为空。除非您手动创建,否则不存在 filter 表,且 filter 这个词仅为您选定的名称。链只有在被赋予类型、钩子(hook)和优先级(priority)时才能接收数据包,此时它被称为基础链(base chain)。若链不具备这些属性,则只能通过显式的 jumpgoto 才能访问,因此在被跳转调用前,它不会产生任何开销。

另一个重大变化是 inet 族。一个 inet 表可以在同一规则集中同时处理 IPv4 和 IPv6 流量,这消除了因端口在 iptables 中已关闭但在 ip6tables 中却完全开放而导致的各类错误。这种不一致性非常普遍,甚至在 ufw 系统上有其特定的故障模式

以下是一个完整的服务器规则集。将其存入 /etc/nftables.conf

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  set admin_ips {
    type ipv4_addr
    flags interval
    elements = { 203.0.113.5, 198.51.100.0/24 }
  }

  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iif lo accept
    ip protocol icmp accept
    meta l4proto ipv6-icmp accept
    tcp dport 22 ip saddr @admin_ips counter accept
    tcp dport { 80, 443 } counter accept
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}

请仔细阅读第 2 行。flush ruleset 会删除服务器上的所有表,包括 ufw 和 Docker 自行创建的表。在生产服务器上运行此命令前,请务必继续阅读。

input 链中的第一条规则承担了大部分工作。ct state established,related accept 允许您发起的连接的回复包进入,因此链中后续规则只需处理新连接。ct state invalid drop 会丢弃所有不匹配已知连接且非有效起始的数据包。其后的所有规则均为显式放行,而 policy drop 则处理其余流量。

在加载文件前请先检查,并保持第二个 SSH 会话处于开启状态。policy drop 配合 SSH 规则中的一个拼写错误,会将您锁定在服务器之外。

sudo nft -c -f /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo nft list ruleset

nft -c -f 会解析文件并报告错误,而不会加载任何内容。解析成功时不会输出任何信息。

使用集合替代长规则列表

tcp dport { 80, 443 } 是一个匿名集合:只需一条规则即可完成查找,无需为每个端口单独设置规则。命名集合(如 admin_ips)功能更强,因为你可以在防火墙运行时动态修改它。

sudo nft add element inet filter admin_ips { 203.0.113.9 }
sudo nft list set inet filter admin_ips
sudo nft delete element inet filter admin_ips { 203.0.113.9 }

无需重载规则,无需重新编号,无论集合中包含 5 个还是 5 万个地址,匹配过程始终只需一次查找。flags interval 标志允许集合存储范围和 CIDR(无类别域间路由)前缀,例如 198.51.100.0/24。若不使用该标志,集合仅能存储单个地址,加载前缀将会失败。

集合还可以自动清除其中的元素。

set banned {
  type ipv4_addr
  flags timeout
  timeout 1h
}

使用规则 ip saddr @banned drop,每个元素在添加一小时后会自动移除。这就是 Ubuntu 24.04 上的 fail2ban 中 nftables 的工作方式:它向集合添加元素,而不是添加规则。如果端口的概念对你来说比较陌生,请先阅读 Linux 中端口的本质

迁移过程中有一个常见的差异:除非明确要求,否则 nftables 不会统计数据包。iptables -nvL 总是显示每条规则的计数器。而在 nftables 中,只有包含 counter 关键字的规则才会显示计数,因此请在你预期后续需要调试的规则中加入 counter

钩子与优先级如何决定执行顺序

基础链指定了一个钩子,即数据包路径中执行规则的位置。prerouting 在路由决策前运行。input 针对发往本机的流量运行。forward 针对经由本机路由的流量运行。output 针对本地进程发出的流量运行。postrouting 在数据包离开前最后运行。

优先级用于确定同一钩子内各链的执行顺序,数值越小越先执行。nftables 为经典数值提供了名称:raw 为 -300,mangle 为 -150,dstnat 为 -100,filter 为 0,srcnat 为 100。编写 priority filter; 与编写 priority 0; 的效果相同。

现在讨论决定不同工具混用是否可行的问题。注册在同一钩子上的每个基础链都会按优先级顺序运行。在你的链中接受数据包并不意味着处理结束:accept 仅终止当前链,数据包会继续流向同一钩子上的下一个基础链。drop 在任何位置都是最终操作,会立即丢弃数据包。因此,无论谁先运行,你在自己表中设置的宽松规则都无法撤销 ufw 表中的丢弃操作,且你的 accept 也无法为你提供针对后续链的保护。

若同一钩子上有两个优先级相同的基础链,它们将按注册顺序运行,而注册顺序取决于服务的启动先后。该顺序在重启后可能会发生变化。如果你必须在 ufw 之外运行自己的表,请为其指定一个不同的优先级,以便明确执行顺序,避免产生竞争条件。

为什么不需要编写反向 NAT 规则?

这是人们最常犯的错误,以下是直接的答案:连接跟踪(connection tracking)会自动为您写入反向转换规则。无需添加第二条规则。

一个同时处理 VPS 常见任务两个部分的 nat 表如下所示。

table inet nat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    iifname "enp1s0" tcp dport 8080 dnat ip to 10.0.0.5:80
  }

  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    ip saddr 10.0.0.0/24 oifname "enp1s0" masquerade
  }
}

只有连接的第一个数据包会根据 nat 链进行评估。当规则匹配时,内核会将该转换存储在连接跟踪表中,并与连接条目关联。后续的每个数据包(无论哪个方向)都会根据存储的条目进行重写,不再读取任何规则。安装 conntrack 工具并查看实时条目。

sudo apt install -y conntrack
sudo conntrack -L -p tcp
tcp 6 431999 ESTABLISHED src=198.51.100.20 dst=203.0.113.10 sport=54321 dport=8080 src=10.0.0.5 dst=198.51.100.20 sport=80 dport=54321 [ASSURED] mark=0 use=1

将其理解为两个元组。前四个字段是客户端发送的连接,目标地址为 203.0.113.10:8080(您的公网地址)。后四个字段是内核预期的回复,已完成反向转换,来自 10.0.0.5:80(真实的后端)。第二个元组就是反向规则。内核在第一个数据包匹配时就已将其写入。

因此,不要为返回方向编写规则。它无法匹配,因为返回数据包属于已建立的连接,永远不会到达 nat 链;如果它真的匹配了,您反而会转换一个内核已经修复过的数据包。

重写规则必须放置的位置遵循相同的机制。目标地址转换必须在 prerouting 中运行,即在路由决策之前,因为路由必须看到新的目标地址,否则数据包会发送到错误的地方。出于同样的原因,本机生成的流量由 output 钩子处理。源地址转换(包括源端口重写)必须在 postrouting 中运行,即在路由选择出站接口之后。masquerade 从该接口获取地址,而在路由运行之前,接口是未知的。

这就是为什么像这样的规则必须放在路径的末尾,而不能放在其他地方。

ip saddr 10.0.0.0/24 oifname "enp1s0" snat ip to 203.0.113.10:20000-30000

端口范围会连同源地址一起重写源端口,当多个内部客户端共享一个公网地址且源端口冲突时,这正是您所需要的。回复到达时,其目标地址为该范围内的端口,conntrack 会将其与条目匹配,并在数据包交付前恢复原始源端口。同样,不需要第二条规则。

一个实际后果是:更改 NAT 规则不会影响已存在的连接,因为它们的转换信息已经存储。它们会保持旧的行为,直到条目过期。sudo conntrack -D -p tcp --dport 8080 会删除匹配的条目,而 sudo conntrack -F 会删除所有条目。在 NAT 设备上使用后者时要小心,因为这些存储的转换信息是维持当前连接的关键,清除它们会立即中断通过该设备的所有连接。

ufw 和 Docker 都会各自写入防火墙规则

ufw 是 iptables 的前端,而在 Ubuntu 上,iptables 又是 nftables 的前端。因此,启用了 ufw 的服务器会拥有一个充满 ufw-before-inputufw-user-input 等链的 ip filter 表,以及该结构的 ip6 filter 副本。请使用 sudo nft list ruleset | grep ufw 查看这些内容。这些链由 /etc/ufw 中的文件生成,且 ufw reload 会从零开始重写这些文件,这就是为什么手动添加的 iptables 规则在下次重载时会消失的原因。VPS 的 ufw 基础知识 涵盖了该文件布局。

Docker 会自行配置防火墙,且不会参考 ufw。使用 -p 80:80 发布端口时,Docker 会将一条 DNAT 规则写入 nat 表,并在转发路径中写入一条 accept 规则,这两者都会在 ufw 的用户链之前运行。结果往往令人意外:即使 ufw deny 80 已加载,容器仍然可以从公网访问。解决方法在于 Docker 为用户规则预留的 DOCKER-USER 链,为什么 Docker 容器会忽略 ufw 对此进行了详细说明。请使用 sudo nft list ruleset | grep -i docker 查看服务器上的当前规则。

现在重新阅读上述配置中的 flush ruleset 行。它会删除所有表,包括这两个工具所管理的表。在 Docker 主机上,已发布的端口会停止工作,直到 sudo systemctl restart docker 重建这些链。在清理防火墙时,这一行代码是导致用户意外使自身服务下线的常见原因。

重启后生效的规则

这两个规则集本身都不会持久化。内核在关机时会清除所有规则,因此双方都通过独立的软件包来解决此问题。

对于 nftables,/etc/nftables.confnftables.service 读取。Ubuntu 默认禁用了该服务,因此在信任它之前请务必检查状态。

systemctl is-enabled nftables
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftables

对于 iptables,对应的软件包是 iptables-persistent,它会安装 netfilter-persistent 并将规则保存到 /etc/iptables/rules.v4/etc/iptables/rules.v6

sudo apt install -y iptables-persistent
sudo netfilter-persistent save

不要同时运行这两者。如果两个文件都声称拥有防火墙控制权,它们的内容会产生偏差;最后加载的那个文件会生效,而这种结果无法通过阅读任一文件来预测。

导出实时规则集时存在一个相关陷阱。sudo nft -s list ruleset > /etc/nftables.conf 会捕获当前加载的所有内容,包括 ufw 和 Docker 的表。如果在启动时恢复这些规则,系统会先加载这些工具本应自行构建的规则的静态副本,随后这些工具启动时又会再次添加一份。请仅使用 sudo nft -s list table inet filter 导出你自己的表。-s 标志会排除计数器,因为计数器不应出现在配置文件中。

是否应该启用 VPS 防火墙?

除非有 ufw 无法实现的需求,否则请保持 ufw 不变。ufw 能够胜任常规的 VPS 任务:默认拒绝所有连接,仅开放少量端口。为了折腾而手动编写规则集,不仅无法提升防火墙性能,反而增加了维护负担。

当你的需求超出 ufw 的模型时,再考虑使用原生工具:例如需要 NAT 和端口转发、需要在运行时更新的地址集、一条规则同时覆盖 IPv4 和 IPv6,或者需要自定义链的优先级。这些才是合理的理由,而 ufw 无法实现上述任何功能。

如果决定使用原生工具,请彻底弃用 ufw。运行 sudo ufw disablesudo systemctl disable --now ufw,通过 sudo nft list ruleset 确认其表项已清除,然后再加载你自己的规则文件。如果同时运行 ufw 和手动编写的表项,流量依然可以通行,但实际生效的策略是两个规则集的并集,且评估顺序取决于服务启动顺序。此时,任何阅读配置文件的人都无法判断服务器的真实防火墙状态。

迁移现有的 iptables 规则集

iptables-translate 用于转换单条规则并输出对应的 nftables 格式。它不会对服务器进行任何更改。

iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
nft add rule ip filter INPUT tcp dport 22 counter accept

iptables-restore-translate -f /etc/iptables/rules.v4 用于转换整个已保存的规则集。请将输出结果视为初稿。该转换过程是机械化的,且逐条进行,因此你会得到旧的表名和链名,以及 IPv4 和 IPv6 两套独立的规则集,且无法利用 nftables 的集合(sets)特性。建议手动将其重写为一个 inet 表,并在应用到生产服务器前使用 nft -c -f 进行检查。

这些示例中的地址取自文档范围 203.0.113.0/24198.51.100.0/24enp1s0 为接口名称。请使用 ip route show defaultip -br addr 中的实际值,不要直接复制我的示例,因为当前的 Ubuntu 镜像很少将接口命名为 eth0

FAQ

Ubuntu 上的 iptables 是否已被弃用?

该命令不会被移除,且在 Ubuntu 24.04 上仍可正常工作。变化在于底层实现:iptables 是一个前端,它通过 iptables-nft 后端写入 nftables 规则。请使用 iptables -V 检查你的系统,该命令在 24.04 上会输出 iptables v1.8.10 (nf_tables)。旧的 x_tables 后端仍以 iptables-legacy 的形式提供,它维护着一套完全独立的规则集,因此请仅选择一个后端配置规则,不要混用。

我是否需要添加第二条规则来撤销回程流量的 NAT?

不需要。连接跟踪(connection tracking)会在连接的首个数据包匹配 NAT 规则时存储转换信息,后续双向的所有数据包都会根据该存储条目进行重写。sudo conntrack -L 会显示每个连接的两个元组:原始方向,以及已反转的回复方向。为回程方向编写规则无效,因为回程数据包不会经过 NAT 链。

我可以同时运行 ufw 和自定义的 nftables 规则吗?

可以运行,但会带来隐患。挂载点上的每个基础链都会执行,因此最终生效的策略是两个规则集的叠加,其优先级由规则优先级决定;若优先级相同,则取决于哪个服务先启动。其中任何一个规则集中的 drop 都是最终判定,而你自定义的 accept 无法阻止另一个工具丢弃同一个数据包。请只选择一种工具。如果选择 nftables,请先禁用 ufw,并确认 sudo nft list ruleset 中已不再包含其表项。

如何让 nftables 规则在 Ubuntu 重启后依然生效?

将规则集放入 /etc/nftables.conf,使用 sudo nft -c -f /etc/nftables.conf 进行检查,然后运行 sudo systemctl enable --now nftables。该服务默认未启用,因此建议运行一次 systemctl is-enabled nftables。生成该文件时,请仅使用 sudo nft -s list table inet filter 导出你自己的表,因为完整的 list ruleset 导出还会包含 ufw 和 Docker 自行管理的表。