以前的 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(现代配置最推荐用这个,省得两边写重复规则)。
  • arpbridge:管更底层的网络硬件和网桥。
    表名是区分大小写的。

链 (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 上(inputoutputforwardpreroutingpostrouting)。
    • 你的规则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。它只能靠别的链通过 jumpgoto 指令把包丢给它。

table ip filter {
        # 这是一个常规链(安检车间)
        chain DOCKER {
                ip daddr 172.18.0.2 tcp dport 33080 accept
                drop
        }
}

常见规则里的 chain DOCKERchain 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 可以给某个流量打上标记,然后根据标记对这个流量执行规则,可以用来分流,或者排查问题。(后面用到了再记录)。