在 Linux 网络世界中,Netfilter、iptables 和 nftables 的关系常被简化为"新旧工具"。但要真正掌握 Linux 网络,需要深入到内核的 钩子机制 (Hooks)、状态跟踪 (Conntrack) 的哈希结构以及 nftables 的字节码虚拟机 (VM)。
为了方便理解,我们可以把 Linux 内核想象成一家大型公司的 收发室,所有的网络数据包(Packet)就是进出的 快递件。
1. Netfilter:内核协议栈的"钩子"矩阵
Netfilter 是一套内核中的规章制度,它在协议栈中设立了 5 个必经的 检查站(Hooks)。当数据包穿过协议栈(如 ip_input.c 或 ip_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 对应的典型用途:
- PREROUTING:数据包刚进入网络栈,路由决策之前。这是做 DNAT(修改目的地址)的唯一正确位置,因为路由是根据目的地址决定转发方向的,必须先改地址再路由。
- LOCAL_IN:路由决定包发给本机进程之后。用于本机防火墙过滤入站流量。
- FORWARD:包只是路过,路由决定转发到其他接口。用于路由器或网关场景。
- LOCAL_OUT:本机进程产生的外发包,路由决策之前。
- 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 规则。