nftables/nft 使用
以前的 Linux 系统配置网络流量主要是用 iptables,但是从内核版 3.13 开始加入 nf_tables 模块,真正的 iptables 便慢慢被弃用,现在的系统底层都是用的 nftables,iptables 命令只是一个命令转换器,用来兼容以前的 iptables 命令语法,底层还是写入到了 nftables。ufw 和 firewalld 也只是前端工具,将 nftables 封装的更易于使用。
表(Table)、链(Chain)、规则(Rule)
表、链、规则是 nftables 里最核心的三个概念。
表 (Table) —— 你的“独立大部门”
每个表是一个独立的大容器,有点类似 namespace 的概念,比如每个应用可以建立一个自己命名的表,将自己需要的所有规则都配置到里面。
在旧的 iptables 时代,系统把表(如 filter, nat)死死地固定好了。而 nftables 非常自由:表是完全由你(或软件)自己创建和命名的容器。
- 类比:表就像是服务器里的“独立大部门”。
- 现象:看你之前的输出,你系统里有
table ip filter(Docker 占用的部门)、table ip netbird(NetBird 组网部门)。它们各过各的,互不干扰。
# table [地址族] [名字]
table ip filter {
... 这里面放具体的链、集合、规则 ...
}
地址族 (Family)
每个表在创建时,必须指定它属于哪个“家族”。这决定了它能处理什么类型的网络包:
ip:只管 IPv4。ip6:只管 IPv6。inet:超级全能型。同时管 IPv4 和 IPv6(现代配置最推荐用这个,省得两边写重复规则)。arp、bridge:管更底层的网络硬件和网桥。
表名是区分大小写的。
链 (Chain) —— 部门里的“流水线车间”
表里面装的是链。链才是真正和 Linux 内核网络通路的“关卡”绑定的地方。
链名也是区分大小写的。
在 nftables 中,链分为两种:
a. 基链 (Base Chain) —— 拦截物理流量的关卡
这种链必须明确告诉内核,你要把自己挂载到哪个网络“钩子 (Hook)”上。
一个典型的基链结构如下
chain door_guard {
type filter hook input priority 0; policy drop;
──┬─────── ──┬────── ────┬───── ───┬───────
│ │ │ │
│ │ │ └─► [默认策略]:没被规则匹配到的包,一律丢弃
│ │ │
│ │ └─► [优先级]:如果有别的链也挂在 input,数字小的先执行
│ │
│ └─► [网络钩子]:挂在“发给本机”的入站关卡上
│
└─► [链类型]:这是一个“数据包过滤拦截”类型的车间
tcp dport 22 accept
}
链类型
为了让内核高效工作,nftables 规定:你在把链挂到网卡钩子(Hook)上时,必须明确申报它的业务类型。内核会根据你申报的 type,为这条链分配专门的、最快的处理引擎。
filter(过滤型 —— 最常用的默认款)- 含义:它的主要职责就是做“网络包的质检和放行”。
- 支持的动作:
accept(放行)、drop(丢弃)、reject(拒绝)。 - 适用钩子:可以挂载到所有的 Hook 上(
input、output、forward、prerouting、postrouting)。 - 你的规则:
type filter就是明确告诉内核,我这条链纯粹是为了过滤包、决定生死用的。
nat(地址转换型)- 含义:专门用来做 NAT(Network Address Translation),也就是修改数据包的源 IP/端口(SNAT)或者目的 IP/端口(DNAT)。
- 特点:只有每条网络连接的第一个包(例如 TCP 的建连握手包)才会流经这种链。一旦第一个包转成功了,内核会自动记住,后续的包直接抄近路转发,不再过这个链。
route(路由导向型)- 含义:专门用来修改数据包的路由决策。
- 特点:如果在这个链里修改了数据包的某些特殊标记(比如 IP 头部的 TOS 字段),内核会自动对这个包重新跑一次路由查询,看看它该从哪个网卡飞出去。
- 限制:它只能挂载在
output钩子上。
网络钩子
Linux 内核在处理一个网络包时,会雷打不动地经过 5 个关键的钩子(关卡)。链挂在哪个钩子上,就能拦截到哪个阶段的流量:
graph TD
In([外部流量进来]) ---> PREROUTING
subgraph NetFilter 内核防火墙
PREROUTING{PREROUTING 钩子}
ROUTE_IN{路由决策: 发给谁}
INPUT{INPUT 钩子}
FORWARD{FORWARD 钩子}
OUTPUT{OUTPUT 钩子}
ROUTE_OUT{路由决策: 出站路径}
POSTROUTING{POSTROUTING 钩子}
PREROUTING --> ROUTE_IN
ROUTE_IN -- 本机包 --> INPUT
ROUTE_IN -- 转发包 --> FORWARD
OUTPUT --> ROUTE_OUT
ROUTE_OUT -- 出站 --> POSTROUTING
FORWARD --> POSTROUTING
end
TRAEFIK(宿主机应用程序: Traefik)
INPUT --> TRAEFIK
TRAEFIK --> OUTPUT
POSTROUTING ---> Out([网络包发出去])
%% 纯色填充,去掉复杂描边
style PREROUTING fill:#f9f
style INPUT fill:#f9f
style FORWARD fill:#f9f
style OUTPUT fill:#f9f
style POSTROUTING fill:#f9f
style TRAEFIK fill:#bbf
优先级和默认策略
priority 0:如果有多个链都挂在 INPUT,数字越小的链越先被执行。policy drop;:默认策略。如果里面的规则都没匹配上,最后直接丢弃。
b. 常规链 (Regular Chain) —— 负责分类的“小车间”
它不挂载任何 Hook,无法直接拦截网络流量。常规链直接就是具体的防火墙规则(Rule)。它没有 type、没有 hook、没有 priority,也没有 policy。它只能靠别的链通过 jump 或 goto 指令把包丢给它。
table ip filter {
# 这是一个常规链(安检车间)
chain DOCKER {
ip daddr 172.18.0.2 tcp dport 33080 accept
drop
}
}
常见规则里的 chain DOCKER、chain netbird-rt-fwd 就是常规链。主链觉得这个包是 Docker 的,就把它 jump DOCKER 丢过去精细审查,可以让规则结构更清晰。
规则 (Rule) —— 流水线上的“质检工人”
规则是真正干活的。每条规则由“匹配条件”和“动作(Action)”组成。 包会从链的第一条规则开始,从上到下一条条对号入座:
# 举例一条规则
tcp dport 10000 counter accept
└─── 匹配条件 ───┘ └─── 动作 ───┘
- 如果匹配成功:执行动作。如果是
accept(放行)或drop(丢弃),这个包的防火墙之旅就彻底结束,不再看后面的规则。 - 如果匹配失败:继续看下一条规则。
- 如果到最后都没匹配上:执行这条链的默认策略(
policy)。
Sets/Maps
在传统的 iptables 时代,如果你想针对 100 个不同的 IP 地址做封禁,你必须在防火墙里写 100 条一模一样的规则,只是换个 IP。当包进来时,内核得苦哈哈地把这 100 条规则从头到尾对齐一遍,效率极低。nftables 为了彻底解决这种“规则爆炸”和性能低下的问题,引入了两个非常现代的数据结构:集合(Sets) 和 映射(Maps)。
你可以把它们直接理解为编程语言里的 HashSet(集合) 和 HashMap(字典/键值对)。它们利用了内核底层的哈希表(Hash Table)或红黑树,无论里面装了多少数据,查找速度全都是 $O(1)$ 或 $O(\log n)$(近乎瞬间完成)。
集合 (Sets)
Sets 就是一个单纯的“值列表”(比如一堆 IP 地址、一堆端口号、或者一堆网卡名)。它只有“键(Key)”,没有“值(Value)”。
table ip netbird {
set nb0000003 {
type ipv4_addr
flags dynamic
elements = { 100.85.61.3, 100.85.94.235,
100.85.148.33, 100.85.180.221,
100.85.190.38 }
}
}
type ipv4_addr:告诉内核,这个 Set 里面装的全都是 IPv4 地址。- flags dynamic:标记这是一个可变集合(类似写代码里面的变量),后面可以在 rule 里根据调整往里面增删元素。
elements:里面就是具体合法的 NetBird 节点 IP。
怎么在规则里用它?
当 NetBird 想要放行这批 IP 的 ssh (22 端口) 流量时,它不需要写 5 条规则,只需要写一条规则,并用 @ 符号去引用这个 Set:
chain netbird-acl-input-rules {
ip saddr @nb0000003 tcp dport 22 accept
}
映射 (Maps)
Maps 比 Sets 更高级,它是“键值对(Key-Value)”结构。你可以根据一个输入条件(Key),直接映射出一个处理动作或者新数据(Value)。
这在做批量端口转发(DNAT)或者分类导流时是神器。
假设你有三个容器服务跑在不同的内网 IP 上,你想把外部不同的端口分别转发给它们:
- 外部端口
8081$\rightarrow$ 转发给172.18.0.2 - 外部端口
8082$\rightarrow$ 转发给172.18.0.3 - 外部端口
8083$\rightarrow$ 转发给172.18.0.4
传统写法(写 3 条 NAT 规则):
tcp dport 8081 dnat to 172.18.0.2
tcp dport 8082 dnat to 172.18.0.3
tcp dport 8083 dnat to 172.18.0.4
用 Map 的优雅写法:
我们先在表里定义一个名为 port_to_container 的 Map:
map port_to_container {
type inet_service : ipv4_addr
# [ 输入:端口 ] : [ 输出:IP ]
elements = { 8081 : 172.18.0.2,
8082 : 172.18.0.3,
8083 : 172.18.0.4 }
}
然后,在 PREROUTING 链里只需要唯一的一条规则就能搞定所有的转发逻辑:
chain PREROUTING {
type nat hook prerouting priority dstnat;
dnat to tcp dport vmap @port_to_container
}
nft 常用命令
查看
# 查看系统里所有的规则
sudo nft list ruleset
# 查看带有 Handle(句柄 ID)的规则(用于精准删除)
sudo nft list ruleset -a
# 或者
sudo nft list table inet filter -a
# 只查看特定的表
sudo nft list table ip netbird
# 查询某个钩子上的所有链
sudo nft list ruleset | grep -A 10 "hook input"
# 只查看特定的链
sudo nft list chain ip filter INPUT
│ │ │ │ │ │
│ │ │ │ │ └── 6. [链名]: INPUT
│ │ │ │ └───────── 5. [表名]: filter
│ │ │ └────────────── 4. [家族]: ip(管理 IPv4 的辖区)
│ │ └────────────────── 3. [对象]: chain(要看的是一个“链”)
│ └──────────────────────── 2. [动作]: list(查看/列出)
└──────────────────────────── 1. [主程序]: nft
# 监控防火墙动态(配合 `nftrace set 1` 使用)
sudo xtables-monitor --trace
增删规则
# 追加规则(Add - 放到链的最后面)
# 下面的 inet filter INPUT 用于指定一条链,其中 inet 是家族,filter 是表名,INPUT 是链名
sudo nft add rule inet filter INPUT tcp dport 80 accept
# 插入规则(Insert - 放到链的最前面,优先执行)
sudo nft insert rule inet filter INPUT ip saddr 1.2.3.4 drop
# 精准插队(在指定的 Handle 后面加规则)
# 假设插在 handle 10 后面
sudo nft add rule inet filter INPUT position 10 tcp dport 443 accept
# 精准删除规则(必须带上 Handle)
# 先用 nft list ruleset -a 查出 handle 数字,假设为 15
sudo nft delete rule inet filter INPUT handle 15
持久化
nft 的 add chain, add rule 命令都是直接修改的内存里的 nftables 配置,机器重启会丢失。如果需要持久化,需要用到下面的命令
sudo nft list ruleset | sudo tee /etc/nftables.conf
或者直接修改文件 /etc/nftables.conf,修改完后通过命令 sudo nft -f /etc/nftables.conf 让它生效。
流量染色
nftables 可以给某个流量打上标记,然后根据标记对这个流量执行规则,可以用来分流,或者排查问题。(后面用到了再记录)。