ᕕ( ᐛ )ᕗ Jimyag's Blog

深入理解 Netfilter, iptables 与 nftables:从内核 Hook 到状态机

· 2882 words · ~ 14 min read

Last modified:

在 Linux 网络世界中,Netfilteriptablesnftables 的关系常被简化为"新旧工具"。但要真正掌握 Linux 网络,需要深入到内核的 钩子机制 (Hooks)状态跟踪 (Conntrack) 的哈希结构以及 nftables 的字节码虚拟机 (VM)

为了方便理解,我们可以把 Linux 内核想象成一家大型公司的 收发室,所有的网络数据包(Packet)就是进出的 快递件


1. Netfilter:内核协议栈的"钩子"矩阵

Netfilter 是一套内核中的规章制度,它在协议栈中设立了 5 个必经的 检查站(Hooks)。当数据包穿过协议栈(如 ip_input.cip_output.c)时,内核会调用对应的回调函数。

五大 Hook 点与流量流向

  flowchart TD
    NIC_IN["网卡收包
eth0 ingress"]
    PRE["PREROUTING
大门口"]
    ROUTE1{"路由判断"}
    LOCAL_IN["LOCAL_IN
办公室门"]
    FORWARD["FORWARD
转运台"]
    POST["POSTROUTING
装车点"]
    APP["本机进程
nginx / sshd …"]
    LOCAL_OUT["LOCAL_OUT
寄件处"]
    NIC_OUT["网卡发包
eth1 egress"]
    ROUTE2{"路由判断"}

    NIC_IN --> PRE
    PRE --> ROUTE1
    ROUTE1 -- "目的 = 本机" --> LOCAL_IN
    ROUTE1 -- "目的 = 其他主机" --> FORWARD
    LOCAL_IN --> APP
    APP --> LOCAL_OUT
    LOCAL_OUT --> ROUTE2
    ROUTE2 --> POST
    FORWARD --> POST
    POST --> NIC_OUT

    style PRE fill:#4a90d9,color:#fff
    style LOCAL_IN fill:#4a90d9,color:#fff
    style FORWARD fill:#4a90d9,color:#fff
    style LOCAL_OUT fill:#4a90d9,color:#fff
    style POST fill:#4a90d9,color:#fff
    style ROUTE1 fill:#f5a623,color:#fff
    style ROUTE2 fill:#f5a623,color:#fff

每个 Hook 对应的典型用途:

  1. PREROUTING:数据包刚进入网络栈,路由决策之前。这是做 DNAT(修改目的地址)的唯一正确位置,因为路由是根据目的地址决定转发方向的,必须先改地址再路由。
  2. LOCAL_IN:路由决定包发给本机进程之后。用于本机防火墙过滤入站流量。
  3. FORWARD:包只是路过,路由决定转发到其他接口。用于路由器或网关场景。
  4. LOCAL_OUT:本机进程产生的外发包,路由决策之前。
  5. POSTROUTING:包即将从网卡发出,路由决策之后。这是做 SNAT/Masquerade(修改源地址)的位置。

三种典型数据包路径:

1
2
3
4
5
6
7
8
# 路径一:外部流量到本机进程(如访问本机 80 端口)
eth0 → PREROUTING → [路由:目的=本机] → LOCAL_IN → nginx

# 路径二:外部流量穿越主机(如路由器转发)
eth0 → PREROUTING → [路由:目的=其他] → FORWARD → POSTROUTING → eth1

# 路径三:本机进程发出流量(如 curl 请求)
curl → LOCAL_OUT → [路由] → POSTROUTING → eth0

优先级 (Priority)

同一个 Hook 点可以注册多个功能模块,内核按优先级(数值从小到大)依次调用。以下是 Linux 内核头文件 linux/netfilter_ipv4.h 定义的标准优先级常量:

优先级数值 名称 说明
-400 CONNTRACK_DEFRAG IP 分片重组(必须最先)
-300 RAW iptables raw 表,跳过 conntrack
-225 SELINUX_FIRST SELinux 第一阶段
-200 CONNTRACK 连接追踪建立,识别包的身份
-150 MANGLE 修改包头(TTL、TOS、Mark)
-100 NAT_DST (DNAT) 修改目的地址
0 FILTER 放行或丢弃决策
50 SECURITY 安全策略(SELinux/AppArmor)
100 NAT_SRC (SNAT) 修改源地址(完成 NAT)
300 CONNTRACK_HELPER conntrack helper(FTP、H.323 等)
MAXINT CONNTRACK_CONFIRM 将新连接正式写入哈希表

为什么 SNAT 优先级(100)必须在 FILTER(0)之后?因为我们要先决定这个包是否放行,才有必要修改它的源地址。如果 FILTER 先丢弃了包,SNAT 就完全没有执行的意义。

为什么 CONNTRACK(-200)必须在 FILTER(0)之前?因为 FILTER 中的 -m conntrack --ctstate ESTABLISHED 规则依赖 conntrack 已经识别了包的状态,conntrack 必须先运行。

IPv6 的 Hook 体系

IPv6 拥有完全独立但结构对称的 Hook 体系(NF_INET_* 系列同时覆盖 IPv4/IPv6)。nftables 通过 inet 地址族同时挂载两套协议栈,避免重复写规则。


2. Conntrack:状态防火墙的"记忆体"

Conntrack(连接追踪) 是检查站旁边的 “出入登记簿”,它记录了谁正在和谁通话。这使得防火墙可以区分"主动建立的连接"和"可疑的无状态包"。

核心结构:元组 (Tuple)

内核用哈希表维护所有活跃连接。每个连接记录(nf_conn)由两个方向的 Tuple(五元组) 组成:

1
2
ORIGINAL Tuple(原始方向):  saddr=10.0.0.1  daddr=8.8.8.8  sport=54321  dport=53  proto=UDP
REPLY Tuple  (回程方向):   saddr=8.8.8.8   daddr=10.0.0.1  sport=53     dport=54321 proto=UDP

这两个 Tuple 都作为哈希键存入哈希表,所以无论是去程包还是回程包,内核都能以 O(1) 时间找到对应的连接记录。

读取连接追踪表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 方法一:直接读取 proc 文件(原始格式)
cat /proc/net/nf_conntrack

# 方法二:使用 conntrack 工具(需安装 conntrack-tools)
conntrack -L

# 过滤查看特定协议
conntrack -L -p tcp
conntrack -L -p udp

# 查看连接总数
conntrack -C

/proc/net/nf_conntrack 的一行典型输出(TCP ESTABLISHED):

1
2
3
4
ipv4  2  tcp  6  86399  ESTABLISHED
  src=10.0.0.1  dst=93.184.216.34  sport=54321  dport=443
  src=93.184.216.34  dst=10.0.0.1  sport=443  dport=54321
  [ASSURED]  mark=0  zone=0  use=2

字段解读:

字段 说明
ipv4 L3 协议族
2 L3 协议号(2 = IPv4)
tcp L4 协议名
6 L4 协议号(6 = TCP)
86399 该条目距超时的剩余秒数
ESTABLISHED TCP 内部状态(仅 TCP 有)
src=10.0.0.1 原始方向:客户端 IP
dst=93.184.216.34 原始方向:服务器 IP
sport=54321 原始方向:客户端端口
dport=443 原始方向:服务器端口
src=93.184.216.34 回程方向:服务器 IP
dst=10.0.0.1 回程方向:客户端 IP
sport=443 回程方向:服务器端口
dport=54321 回程方向:客户端端口
[ASSURED] 双向都有流量,连接稳定(不会因内存压力被提前淘汰)
[UNREPLIED] 只见到单向流量(连接未被服务器回应)
mark=0 数据包 nfmark(可被 mangle 设置,用于策略路由)
zone=0 conntrack zone(用于 VPN 等多路复用场景)
use=2 内核内部引用计数

UDP 连接没有状态字段,仅凭超时判断是否活跃:

1
2
3
4
ipv4  2  udp  17  28
  src=10.0.0.1  dst=8.8.8.8  sport=53891  dport=53
  src=8.8.8.8  dst=10.0.0.1  sport=53  dport=53891
  mark=0  zone=0  use=2

四大 Conntrack 状态

这四个状态是 Netfilter 对 L4 协议做的 高层抽象,不是 TCP 状态机里的 SYN_SENT 等:

NEW:收到连接的第一个包,但尚未看到回程包。对 TCP 而言,就是 SYN 包。此时连接记录刚创建,标记为 [UNREPLIED]

1
2
# 只允许由内网主动发起的 TCP 新连接
iptables -A FORWARD -m conntrack --ctstate NEW -i eth1 -o eth0 -j ACCEPT

ESTABLISHED:收到了回程包,双向通信已确认。TCP 是看到 SYN+ACK 后转入此状态。后续 99% 的数据包只需查哈希表就能放行,绕过所有复杂过滤规则,极大节省 CPU。

1
2
# 放行所有已建立的连接(几乎所有防火墙规则集都有这条)
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

RELATED:与已有连接关联的新连接。最典型的例子是 FTP 数据通道和 ICMP 错误报文。

FTP 协议本身的工作方式导致了 RELATED 的存在:

1
2
3
4
5
6
7
主动模式(PORT):
  客户端  ─── 控制连接 TCP:21 ──►  服务器  (ESTABLISHED)
  客户端  ◄── 数据连接 TCP:20 ───  服务器  (RELATED,服务器主动发起)

被动模式(PASV):
  客户端  ─── 控制连接 TCP:21 ──►  服务器  (ESTABLISHED)
  客户端  ─── 数据连接 TCP:高端口 ► 服务器  (RELATED)

内核模块 nf_conntrack_ftp 会解析 FTP 控制连接中的 PORT/PASV 命令,提前在 期望表(expectation table) 中登记将要出现的数据连接。当数据连接到达时,内核在期望表里找到匹配,将其标记为 RELATED 而不是 NEW,从而可以放行。

1
2
3
4
# 加载 FTP conntrack helper
modprobe nf_conntrack_ftp
# 之后即可用一条规则放行 FTP 数据通道
iptables -A INPUT -m conntrack --ctstate RELATED -j ACCEPT

INVALID:包的状态异常,如 TCP 标志位组合非法、不属于任何已知连接的 ACK 包、序列号错误等。应当直接丢弃:

1
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

TCP 内部状态机

conntrack 在 ESTABLISHED 状态下,内部还维护着更细粒度的 TCP 状态,可用于精确过滤:

TCP 状态 说明
SYN_SENT 客户端发出 SYN,等待 SYN+ACK
SYN_RECV 服务器发出 SYN+ACK,等待 ACK
ESTABLISHED 三次握手完成
FIN_WAIT 一方发出 FIN,开始关闭
CLOSE_WAIT 收到 FIN,等待本端关闭
LAST_ACK 被动关闭方发出 FIN,等待最后的 ACK
TIME_WAIT 等待网络中残留包消散(默认 120 秒)
CLOSE 连接完全关闭

用 conntrack 工具可以直接看到这些状态:

1
conntrack -L -p tcp --tcp-flags SYN_SENT

性能调优

高并发场景下如果登记簿写满(nf_conntrack_count >= nf_conntrack_max),内核会丢弃新连接,dmesg 中出现 nf_conntrack: table full, dropping packet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 查看当前连接数与上限
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

# 查看哈希桶大小
cat /sys/module/nf_conntrack/parameters/hashsize

# 调整上限(建议值:服务器内存 / 16KB,例如 16GB 内存 → 约 1M 条目)
sysctl -w net.netfilter.nf_conntrack_max=1048576
# 对应哈希桶设为 max/4(减少碰撞同时节省内存)
echo 262144 > /sys/module/nf_conntrack/parameters/hashsize

# 持久化到 /etc/sysctl.d/99-conntrack.conf
cat >> /etc/sysctl.d/99-conntrack.conf << 'EOF'
net.netfilter.nf_conntrack_max = 1048576
EOF

# 调整超时(默认 TCP ESTABLISHED 超时高达 432000 秒 = 5 天!)
# 对于高并发 HTTP/HTTPS 服务,通常 1 小时足够
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600

# TIME_WAIT 状态超时(默认 120 秒,一般不需要修改)
sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait

# UDP 超时(DNS 查询用 30 秒足够,但 UDP 流媒体可能需要更长)
sysctl -w net.netfilter.nf_conntrack_udp_timeout=30
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=120

哈希表内存占用估算:

1
2
3
4
5
内存 = hashsize × 8 字节(桶指针)+ max × ~360 字节(每条连接记录)
示例:hashsize=262144, max=1048576
     = 262144 × 8 + 1048576 × 360
     ≈ 2 MB + 360 MB
     ≈ 362 MB

3. iptables:硬编码的线性匹配

iptables 就像一个查阅 纸质活页夹 的老保安,办事严谨但手段原始。它将规则组织在固定的 表(Table)链(Chain) 中。

表的层次与链的可用性

iptables 有 5 张表,每张表只在特定 Hook 上生效:

表名 可用链 典型用途
raw PREROUTING, OUTPUT 跳过 conntrack(NOTRACK)
mangle 所有五条链 修改 TTL、TOS、MARK
nat PREROUTING, INPUT, OUTPUT, POSTROUTING 地址转换(DNAT/SNAT)
filter INPUT, FORWARD, OUTPUT 放行/丢弃(主防火墙)
security INPUT, FORWARD, OUTPUT SELinux 安全标记

同一 Hook 上多张表的执行顺序(以 PREROUTING 为例):

1
2
3
4
5
数据包到达 PREROUTING Hook
    ├─► raw 表 (-300)        ← 最先执行,可设 NOTRACK 跳过 conntrack
    ├─► mangle 表 (-150)     ← 修改包头
    └─► nat 表 (-100)        ← DNAT 改目的地址

规则结构

每条 iptables 规则 = 匹配条件 + 目标动作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
iptables -t <表> -A <链> <匹配条件> -j <目标>

# 目标类型:
# ACCEPT  放行,继续后续处理
# DROP    静默丢弃,发送方不知道被丢了
# REJECT  拒绝,向发送方回一个 ICMP 错误或 TCP RST
# LOG     记录到 syslog,然后继续匹配下一条规则
# RETURN  退出当前链,返回调用链
# DNAT    修改目的地址(仅 nat 表)
# SNAT    修改源地址(仅 nat 表)
# MASQUERADE  动态 SNAT(出口 IP 动态获取,适合 DHCP 场景)

示例一:基础有状态防火墙

这是 Linux 服务器最常见的防火墙模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 清空现有规则
iptables -F
iptables -X

# 默认策略:INPUT 和 FORWARD 拒绝,OUTPUT 放行
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# 放行本地回环
iptables -A INPUT -i lo -j ACCEPT

# 丢弃状态异常的包(防止各种欺骗攻击)
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# 放行已建立/关联的连接(这条必须在 DROP 规则之前)
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# 放行 ICMP ping(可选,有助于诊断问题)
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# 放行 SSH(仅来自管理网段)
iptables -A INPUT -s 192.168.10.0/24 -p tcp --dport 22 \
    -m conntrack --ctstate NEW -j ACCEPT

# 放行 HTTP/HTTPS
iptables -A INPUT -p tcp -m multiport --dports 80,443 \
    -m conntrack --ctstate NEW -j ACCEPT

# 记录并丢弃其余所有包(日志在默认策略 DROP 之前触发)
iptables -A INPUT -j LOG --log-prefix "[FW-DROP] " --log-level 4

查看规则(带序号,方便插入和删除):

1
iptables -L INPUT -n -v --line-numbers

输出示例:

1
2
3
4
5
6
7
Chain INPUT (policy DROP 0 packets, 0 bytes)
num  pkts bytes target     prot opt in     out     source               destination
1       5   300 ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0
2       0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0  ctstate INVALID
3    1523  2.1M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0  ctstate RELATED,ESTABLISHED
4       2   120 ACCEPT     tcp  --  *      *       192.168.10.0/24      0.0.0.0/0  tcp dpt:22 ctstate NEW
5      87  4.2K ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0  multiport dports 80,443 ctstate NEW

示例二:NAT 路由器(共享上网)

场景:内网 192.168.1.0/24,通过 eth0(公网 IP)出口上网:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 开启内核 IP 转发(必须!)
echo 1 > /proc/sys/net/ipv4/ip_forward
# 持久化
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-forwarding.conf

# MASQUERADE:自动使用 eth0 当前 IP 作为源地址(适合 DHCP 动态 IP)
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE

# 如果公网 IP 是静态的,用 SNAT 更高效(避免每包查询出口 IP)
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j SNAT --to-source 1.2.3.4

# 放行内网到公网的转发
iptables -A FORWARD -i eth1 -o eth0 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

示例三:端口转发(DNAT)

场景:将公网 8080 端口的流量转发到内网服务器 192.168.1.10:80

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 在 PREROUTING 改目的地址(路由决策之前,确保包被路由到正确接口)
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 \
    -j DNAT --to-destination 192.168.1.10:80

# 放行转发的流量
iptables -A FORWARD -d 192.168.1.10 -p tcp --dport 80 \
    -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# 如果本机也要访问转发的服务(同一台机器发出 → 需要 OUTPUT 中的 DNAT)
iptables -t nat -A OUTPUT -p tcp --dport 8080 \
    -j DNAT --to-destination 192.168.1.10:80

验证 NAT 规则:

1
2
3
4
5
# 查看 nat 表所有规则
iptables -t nat -L -n -v

# 实时查看匹配计数
watch -n1 'iptables -t nat -L PREROUTING -n -v'

示例四:防暴力破解(连接速率限制)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 创建自定义链处理 SSH 速率限制
iptables -N SSH_CHECK

# 使用 recent 模块:60 秒内超过 5 次 NEW 连接则封锁
iptables -A SSH_CHECK -m recent --name SSH_BLOCK --update \
    --seconds 60 --hitcount 5 -j DROP
iptables -A SSH_CHECK -m recent --name SSH_BLOCK --set -j ACCEPT

# 将 SSH 新连接导入自定义链
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j SSH_CHECK

规则持久化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Debian/Ubuntu
apt install iptables-persistent
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
# 重启后自动加载

# RHEL/CentOS
service iptables save
# 或
iptables-save > /etc/sysconfig/iptables

线性匹配的性能问题

iptables 采用 线性顺序匹配,每个数据包依次比对每条规则,直到命中为止。当规则数量大时,后面的规则命中延迟高。例如:

规则数量 每包比对次数(最坏情况)
100 条 100 次比对
1000 条 1000 次比对
10000 条 10000 次比对(Kubernetes 集群的典型规模)

此外,添加或删除规则时,iptables 需要将整个规则集 dump 出来、修改、再整体写回内核,这在规则频繁变化的场景(如 Kubernetes Service)下会造成明显的锁竞争和延迟。


4. nftables:基于寄存器的虚拟机 (VM)

nftables 是那个用 电脑数据库 办公的新生代保安。它在内核中实现了一个轻量级的 字节码虚拟机,用来解释规则引擎生成的指令。从 Linux 3.13(2014 年)开始合入主线,Debian 10+、RHEL 8+、Ubuntu 20.04+ 均已默认使用。

虚拟机架构

nftables 内核模块维护 16 个 128 位的 寄存器。匹配一个规则的流程是:

1
2
3
1. 提取(load):从数据包头部读取字段(如 ip daddr)→ 存入寄存器 reg[1]
2. 比较(cmp):将 reg[1] 与目标值比较
3. 判决(verdict):写入判决寄存器  ACCEPT / DROP / CONTINUE

这种设计的核心优势:规则被编译为紧凑的字节码,内核逐条执行字节码指令,而不是像 iptables 那样调用一长串函数指针。对于简单规则,字节码执行非常快。

查看某个规则的字节码(需要调试内核或使用 nft --debug 选项):

1
nft --debug=netlink add rule inet filter input ip saddr 1.2.3.4 drop

层次结构:Table → Chain → Rule

nftables 没有 iptables 那样预定义的表和链,一切都是用户自己创建的:

1
2
3
4
Table(地址族 + 名字)
  └── Chain(类型 + Hook + 优先级 + 默认策略)
        └── Rule(匹配条件 + 动作)
              └── Set / Map(可被多条规则引用的数据结构)

地址族(address family):

地址族 说明
ip 仅 IPv4
ip6 仅 IPv6
inet 同时覆盖 IPv4 和 IPv6(最常用)
arp ARP 协议
bridge 网桥层
netdev 网卡入口(ingress),最早介入,可做 XDP 前的过滤

完整规则集示例

以下是一个生产可用的完整 /etc/nftables.conf,包含防火墙 + NAT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/sbin/nft -f

# 清空所有现有规则
flush ruleset

# ── 防火墙 ──────────────────────────────────────────────────
table inet filter {

    # 被封锁的 IP 集合(动态更新,带超时自动过期)
    set blocked_ips {
        type ipv4_addr
        flags dynamic, timeout
        timeout 1h
        comment "fail2ban 或手动封锁的 IP,1 小时后自动解封"
    }

    # 允许入站的 TCP 端口集合
    set allowed_tcp_ports {
        type inet_service
        flags interval
        elements = { 22, 80, 443, 8080-8090 }
    }

    chain input {
        type filter hook input priority filter
        policy drop                        # 默认丢弃

        iif "lo" accept                    # 回环必须放行

        ct state invalid drop              # 丢弃异常状态包
        ct state { established, related } accept  # 已建立连接放行

        # ICMP(允许 ping 和 MTU 探测)
        ip  protocol icmp   icmp  type { echo-request, destination-unreachable, time-exceeded } accept
        ip6 nexthdr  icmpv6 icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert } accept

        # 检查封锁列表(在允许规则之前)
        ip saddr @blocked_ips drop

        # 允许的 TCP 端口(包含端口范围)
        tcp dport @allowed_tcp_ports ct state new accept

        # SSH 速率限制(防暴力破解)
        tcp dport 22 ct state new \
            limit rate over 5/minute burst 10 packets \
            log prefix "[SSH-RATELIMIT] " drop

        # 记录其他被丢弃的包(供审计)
        log prefix "[FW-DROP] " flags all
    }

    chain forward {
        type filter hook forward priority filter
        policy drop

        ct state { established, related } accept
        ct state invalid drop

        # 允许内网流量出站
        iif "eth1" oif "eth0" ct state new accept
    }

    chain output {
        type filter hook output priority filter
        policy accept
        # 出站默认放行;如有需要可在此限制本机对外连接
    }
}

# ── NAT ─────────────────────────────────────────────────────
table ip nat {

    chain prerouting {
        type nat hook prerouting priority dstnat

        # 端口转发:将 8080 → 内网 192.168.1.10:80
        iif "eth0" tcp dport 8080 dnat to 192.168.1.10:80
    }

    chain postrouting {
        type nat hook postrouting priority srcnat

        # 内网共享出口(动态 MASQUERADE)
        oif "eth0" ip saddr 192.168.1.0/24 masquerade
    }
}

应用规则集:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 语法检查(不实际加载)
nft -c -f /etc/nftables.conf

# 加载
nft -f /etc/nftables.conf

# 查看当前规则集(带计数器)
nft list ruleset

# 开机自动加载(systemd)
systemctl enable nftables

集合 (Sets):O(1) 匹配的核心

nftables 原生支持哈希集合,这是相对 iptables 最显著的性能提升:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 创建一个 IP 集合(立即生效,不需重载规则集)
nft add set inet filter blocked_ips { type ipv4_addr \; }

# 批量添加元素
nft add element inet filter blocked_ips { 1.2.3.4, 5.6.7.8, 9.10.11.12 }

# 在规则中引用(@ 前缀)
nft add rule inet filter input ip saddr @blocked_ips drop

# 带超时的自动过期(例如 fail2ban 临时封锁)
nft add element inet filter blocked_ips { 1.2.3.4 timeout 30m }

# 删除单个元素
nft delete element inet filter blocked_ips { 1.2.3.4 }

# 查看集合内容
nft list set inet filter blocked_ips

集合支持多种类型和复合键:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 端口范围集合(带 interval flag)
nft add set inet filter admin_ports {
    type inet_service
    flags interval
    elements = { 22, 8000-8999 }
}

# 带地址族的复合集合(IP + 端口对)
nft add set inet filter rate_limit_keys {
    type ipv4_addr . inet_service
    flags dynamic, timeout
    timeout 1m
    size 65536
}

# 在规则中用复合键
nft add rule inet filter input \
    ip saddr . tcp dport @rate_limit_keys \
    ct state new drop

性能对比:无论集合中有 10 个还是 100,000 个 IP,nftables 都通过哈希查找在 O(1) 时间完成匹配。iptables 使用 ipset 扩展也能做到 O(1),但需要额外工具,且与 iptables 规则语法集成度较低。

字典映射 (Maps) 与判决映射 (Verdict Maps)

Maps 是 nftables 最强大的功能之一,允许用数据包字段直接查表得到动作或值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 判决映射:根据源 IP 决定动作
nft add map inet filter ip_actions {
    type ipv4_addr : verdict
    elements = {
        192.168.1.1 : accept,
        10.0.0.1    : drop,
        172.16.0.1  : continue,
    }
}
nft add rule inet filter input ip saddr vmap @ip_actions

# 端口转发映射:将外部端口映射到内网 IP:端口
nft add map ip nat port_forward {
    type inet_service : ipv4_addr . inet_service
    elements = {
        8080 : 192.168.1.10 . 80,
        8443 : 192.168.1.10 . 443,
        3306 : 192.168.1.20 . 3306,
    }
}
nft add rule ip nat prerouting \
    iif "eth0" \
    dnat ip addr . port to tcp dport map @port_forward

这种写法将多个端口转发规则压缩为一条规则 + 一个 Map,Map 查询是 O(1),不管有多少个映射条目,性能恒定。

Flowtable:绕过钩子的软件快速路径

对于长连接(如大文件传输),每个数据包仍然要走完整的 Netfilter Hook 链,即使规则只有"放行已建立连接"。Flowtable 通过在内核网络设备层直接转发已建立的 TCP/UDP 流,完全绕过 iptables/nftables 的规则处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
table inet filter {
    flowtable ft {
        hook ingress priority 0
        devices = { eth0, eth1 }  # 两个接口之间的转发流量走快速路径
    }

    chain forward {
        type filter hook forward priority filter
        policy drop

        ct state { established, related } accept

        # 将已建立的 TCP/UDP 流卸载到 flowtable
        ip  protocol { tcp, udp } ct state established flow add @ft
        ip6 nexthdr  { tcp, udp } ct state established flow add @ft
    }
}

Flowtable 的效果:连接建立后,后续数据包直接在网卡驱动层转发,延迟更低,CPU 占用显著减少,适合大带宽路由场景。

从 iptables 迁移到 nftables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 1. 导出现有 iptables 规则
iptables-save  > /tmp/iptables.rules
ip6tables-save > /tmp/ip6tables.rules

# 2. 自动翻译为 nftables 语法(iptables-translate 或 iptables-restore-translate)
iptables-restore-translate  -f /tmp/iptables.rules  > /tmp/nftables-from-v4.conf
ip6tables-restore-translate -f /tmp/ip6tables.rules >> /tmp/nftables-from-v4.conf

# 3. 检查生成的规则(翻译不一定 100% 准确,需人工审核)
cat /tmp/nftables-from-v4.conf

# 4. 语法检查
nft -c -f /tmp/nftables-from-v4.conf

# 5. 停用 iptables,启用 nftables
systemctl stop  iptables
systemctl disable iptables
systemctl enable --now nftables

单条规则转换(可用于理解语法差异):

1
2
3
4
5
iptables-translate -A INPUT -p tcp --dport 443 -j ACCEPT
# 输出:nft add rule ip filter INPUT tcp dport 443 counter accept

iptables-translate -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# 输出:nft add rule ip nat POSTROUTING oifname "eth0" counter masquerade

5. 常见调试命令速查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# ── Conntrack ───────────────────────────────────────────────
conntrack -L                          # 列出所有连接
conntrack -L -p tcp --dport 80        # 过滤 HTTP 连接
conntrack -D -s 1.2.3.4              # 删除指定源 IP 的连接(踢掉某个客户端)
conntrack -E                          # 实时监听连接事件(类似 tcpdump)
conntrack -C                          # 显示当前连接总数

# ── iptables ────────────────────────────────────────────────
iptables -L -n -v --line-numbers      # 查看 filter 表规则(带统计)
iptables -t nat -L -n -v             # 查看 nat 表
iptables -Z                           # 清零所有计数器(用于流量统计)
iptables -D INPUT 3                  # 按行号删除规则

# ── nftables ────────────────────────────────────────────────
nft list ruleset                      # 查看所有规则
nft list table inet filter            # 查看指定表
nft list chain inet filter input      # 查看指定链
nft list set inet filter blocked_ips  # 查看集合内容
nft monitor                           # 实时监听规则变更事件
nft -j list ruleset | jq .           # JSON 格式输出(便于脚本处理)

# ── 排查数据包路径 ──────────────────────────────────────────
# 在 raw 表跟踪特定数据包(nftables 方式)
nft add table inet debug_trace
nft add chain inet debug_trace prerouting { type filter hook prerouting priority -350\; }
nft add rule  inet debug_trace prerouting ip saddr 1.2.3.4 meta nftrace set 1
nft monitor trace   # 实时显示匹配过程

# 完成后清理
nft delete table inet debug_trace

6. 总结

特性 Netfilter iptables nftables
角色 基础设施层(Hook/Conntrack) 经典执行引擎 现代 VM 引擎
匹配效率 - 线性匹配 O(n) 哈希/集合 O(1)
规则语言 C API 命令行参数拼接 结构化 DSL
表/链 预定义固定结构 预定义固定结构 用户自定义
IP 版本 IPv4 + IPv6 分离 分两套命令 inet 族统一
动态规则更新 - 全量替换(有锁竞争) 原子增删元素
集合/映射 需外挂 ipset 需外挂 ipset 原生支持
状态追踪 核心支持(Conntrack) 依赖 conntrack 模块 依赖 conntrack 模块
Flowtable - 不支持 原生支持
内核版本要求 2.4+ 2.4+ 3.13+
推荐场景 内核开发/深层调优 维护旧系统 现代服务器/高性能防火墙

理解这套架构的本质:Netfilter 是不可绕过的基础设施,Conntrack 是有状态防火墙的记忆,iptables 和 nftables 只是在这个基础上的两种不同的 规则执行引擎。选择 nftables 的核心理由不是"新",而是原生集合(O(1) 匹配)、原子更新(无锁竞争)和统一的 IPv4/IPv6 规则。

#Linux #Iptables #Nftables #Netfilter #Conntrack #Kernel