每一次你在 Kubernetes 里 kubectl expose 一个
Service,背后都有一堆 iptables 规则被悄悄塞进内核。每一次
Pod 访问外网,MASQUERADE 替你做了源地址伪装。每一次 NodePort
把流量导进集群,DNAT 在 PREROUTING 链里把目标地址改了。
但大多数人对 iptables 的理解停留在”四表五链”这个口诀上。问到遍历顺序,答案往往是错的。问到 conntrack 为什么吃内存,更是一脸茫然。
这篇文章不讲口诀。我们从 Netfilter 框架开始,逐层拆开内核的包处理路径,搞清楚每个 hook 点到底在干什么,conntrack 的代价有多大,以及为什么 iptables 终将被取代。
如果你还没看过前两篇,建议先读 Linux 网络栈 和 虚拟网络设备。本篇讲的所有内容,都发生在那个网络栈的中间层。
一、Netfilter:内核里的五道关卡
先纠正一个普遍的误解:iptables 不是防火墙,Netfilter 才是。
iptables 只是一个用户态工具,它通过 Netlink socket 把规则下发给内核里的 Netfilter 框架。Netfilter 才是真正在内核网络栈里做包过滤、地址转换、连接追踪的那个家伙。
Netfilter 是什么
Netfilter 是 Linux 内核中的一个包处理框架。它在内核网络栈的关键位置预留了 5 个 hook 点,任何内核模块都可以在这些 hook 点上注册回调函数。当网络包经过某个 hook 点时,所有注册在该点上的回调函数会按优先级依次执行。
这个设计非常精妙——Netfilter 自己不做任何具体的过滤或转换工作,它只提供框架。iptables、nftables、conntrack、甚至 eBPF 程序,都是挂在这些 hook 点上的”客户”。
五个 Hook 点
内核在 include/uapi/linux/netfilter_ipv4.h
里定义了这五个 hook:
enum nf_inet_hooks {
NF_INET_PRE_ROUTING, // 包刚从网卡进来,路由判定之前
NF_INET_LOCAL_IN, // 路由判定后,发现目标是本机
NF_INET_FORWARD, // 路由判定后,发现需要转发
NF_INET_LOCAL_OUT, // 本机进程产生的包,即将进入路由
NF_INET_POST_ROUTING, // 路由判定后,包即将离开本机
};每个 hook 点对应一种网络包的”人生阶段”:
NF_INET_PRE_ROUTING —— 包刚从网卡驱动上来,IP 层收到,但还没做路由查找。这是 DNAT 的最佳时机,因为改完目标地址后路由判定才会把包发对方向。
NF_INET_LOCAL_IN —— 路由表说”这个包是给我的”,包正在往本机的传输层(TCP/UDP)走。filter 表的 INPUT 链在这里做访问控制。
NF_INET_FORWARD —— 路由表说”这个包不是给我的,转发出去”。Linux 当路由器用时,所有转发流量都经过这里。
NF_INET_LOCAL_OUT —— 本机进程通过 socket 发出的包,刚进入 IP 层。这里可以对出站流量做 DNAT(是的,OUTPUT 链也能做 DNAT,很多人不知道)。
NF_INET_POST_ROUTING —— 包已经做完路由判定,即将交给网卡驱动发出去。SNAT 和 MASQUERADE 在这里执行,因为此时已经确定了出口网卡和源地址。
Hook 的注册机制
内核模块通过 nf_register_net_hook()
注册回调。关键结构体是 nf_hook_ops:
struct nf_hook_ops {
nf_hookfn *hook; // 回调函数
struct net_device *dev; // 绑定的网络设备(可选)
int pf; // 协议族(PF_INET / PF_INET6)
unsigned int hooknum; // 挂载的 hook 点
int priority; // 优先级,数字越小越先执行
};回调函数的返回值决定了包的命运:
// 回调函数返回值
NF_ACCEPT // 继续走下一个回调或正常流程
NF_DROP // 丢弃,不再执行后续回调
NF_STOLEN // 回调函数已经接管了这个包
NF_QUEUE // 排队到用户态(NFQUEUE)
NF_REPEAT // 再执行一次当前回调Hook 优先级:一切秩序的根源
这组优先级数字是理解”真实遍历顺序”的关键:
enum nf_ip_hook_priorities {
NF_IP_PRI_RAW = -300, // raw 表
NF_IP_PRI_MANGLE = -150, // mangle 表
NF_IP_PRI_NAT_DST = -100, // DNAT(nat 表的 PREROUTING)
NF_IP_PRI_FILTER = 0, // filter 表
NF_IP_PRI_NAT_SRC = 100, // SNAT(nat 表的 POSTROUTING)
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, // conntrack 确认
};注意:优先级数字越小,执行越早。raw 表的 -300 远比 filter
表的 0 更早执行。这不是随便定的——raw 表之所以叫
raw,就是因为它在 conntrack 之前执行,可以用
NOTRACK 标记来绕过连接追踪。
下面这张图展示了完整的 hook 点和表链遍历顺序:
二、四表五链:你以为的顺序是错的
网上大多数教程会告诉你”iptables 有四张表、五条链”,然后画一张简化的流程图。但当你真正需要调试一条诡异的 NAT 规则时,那些简化图一个都靠不住。
四张表
raw 表 —— 优先级最高(-300),在
conntrack 之前执行。唯一的用途就是标记
NOTRACK,让某些流量绕过连接追踪。在高吞吐场景下,这张表能救命。
mangle 表 ——
修改包头字段:TTL、TOS、MARK 等。MARK
尤其重要——它不修改包的内容,而是在内核的 skb
结构体上打一个标记,后续的路由策略、tc 流控都可以根据这个
MARK 做决策。
nat 表 —— 地址转换。DNAT
改目标地址/端口,SNAT
改源地址/端口。有一个极其重要的特性:nat
表的规则只对连接的第一个包生效。后续包由 conntrack
自动处理。这意味着你 iptables -t nat -L
看到的规则命中计数远低于实际被 NAT 的包数。
filter 表 ——
做过滤决策:ACCEPT、DROP、REJECT。这是大多数人唯一接触过的表,也是默认表(不加
-t 参数时操作的就是 filter)。
五条链
链(chain)就是 hook 点在 iptables 世界里的名字。映射关系很直接:
| Netfilter Hook | iptables Chain |
|---|---|
| NF_INET_PRE_ROUTING | PREROUTING |
| NF_INET_LOCAL_IN | INPUT |
| NF_INET_FORWARD | FORWARD |
| NF_INET_LOCAL_OUT | OUTPUT |
| NF_INET_POST_ROUTING | POSTROUTING |
但不是每张表在每条链上都有规则。这是真实的关系矩阵:
| PREROUTING | INPUT | FORWARD | OUTPUT | POSTROUTING | |
|---|---|---|---|---|---|
| raw | Y | Y | |||
| mangle | Y | Y | Y | Y | Y |
| nat | Y (DNAT) | Y | Y (DNAT) | Y (SNAT) | |
| filter | Y | Y | Y |
真实的遍历顺序
现在来看三种场景下包到底怎么走。记住,同一个 hook 点上,表的优先级决定了执行顺序。
场景一:外部进来 -> 本机进程(收包)
网卡 -> [PREROUTING: raw(-300) -> conntrack -> mangle(-150) -> nat/DNAT(-100)]
-> 路由判定(发现目标是本机)
-> [INPUT: mangle(-150) -> filter(0) -> nat(100)]
-> 本地进程
注意 INPUT 链上的 nat 表。很多教程不提这个,但它从 Linux 2.6.36 开始就存在了。用途是对进入本机的流量做 SNAT——听起来反直觉,但在某些透明代理场景下有用。
场景二:外部进来 -> 转发出去(路由器模式)
网卡 -> [PREROUTING: raw -> conntrack -> mangle -> nat/DNAT]
-> 路由判定(发现目标不是本机,需要转发)
-> [FORWARD: mangle(-150) -> filter(0)]
-> [POSTROUTING: mangle(-150) -> nat/SNAT(100)]
-> 网卡
转发路径不经过 INPUT 和 OUTPUT。ip_forward() 直接从路由判定跳到 FORWARD hook。
场景三:本机进程 -> 外部(发包)
本地进程
-> [OUTPUT: raw(-300) -> conntrack -> mangle(-150) -> nat/DNAT(-100) -> filter(0)]
-> 路由判定
-> [POSTROUTING: mangle(-150) -> nat/SNAT(100)]
-> 网卡
OUTPUT 链是最复杂的——它同时跑了 raw、conntrack、mangle、nat 和 filter 五个处理阶段。这也是为什么本机发出的包如果规则配错,调试起来格外痛苦。
为什么顺序很重要
举个真实的坑:你在 filter 表的 INPUT 链写了一条规则
DROP -d 10.0.0.1,想阻止访问 10.0.0.1。但如果
PREROUTING 的 nat 表已经把目标地址从 10.0.0.1 改成了
10.0.0.2(DNAT),那你的 filter 规则永远不会匹配——因为 nat
在 PREROUTING 阶段就改完了,到 INPUT 的 filter
阶段时,包的目标地址已经是 10.0.0.2 了。
规则不生效不是因为语法错了,而是因为你在错误的阶段匹配了错误的地址。理解遍历顺序,就是理解”这个包在这个时刻长什么样”。
三、conntrack:连接追踪的代价
conntrack(connection tracking)是 Netfilter 最核心也最昂贵的子系统。没有它,NAT 和有状态防火墙都不可能实现。但它的代价,很多人在生产环境翻车之后才知道。
为什么需要连接追踪
IP 层是无状态的,每个包独立路由、独立处理。但 NAT 需要状态——当你把一个包的目标地址从 A 改成 B 时,后续属于同一条”连接”的包也必须做同样的转换,回程的包还要做反向转换。没有连接追踪,NAT 就是一次性的,根本没法用。
有状态防火墙也是同理。你想表达”允许已建立连接的回程包”,就必须知道哪些包属于同一条连接。这就是 conntrack 的工作。
状态机
conntrack 为每条连接维护一个状态。iptables 里用
-m conntrack --ctstate 匹配这些状态:
NEW —— 这条连接的第一个包。对 TCP 来说是 SYN,对 UDP 来说是这个源/目标对的第一个包。
ESTABLISHED —— 连接已经看到双向的包。TCP 完成三次握手后进入,UDP 收到回程包后进入。
RELATED —— 这个包属于一条新连接,但这条新连接和某条已有连接”有关”。最典型的例子是 FTP 的数据连接——控制连接先建立,数据连接通过控制通道协商端口后建立,conntrack 的 FTP helper 模块能识别这种关系。ICMP 的 destination unreachable 也属于 RELATED。
INVALID —— 不属于任何已知连接,也不是合法的新连接。比如一个 TCP RST 但找不到对应的连接条目。生产环境里大量 INVALID 包通常意味着 conntrack 表满了。
UNTRACKED —— 被 raw 表的
NOTRACK 标记的包,跳过连接追踪。
用 iptables 表达有状态防火墙的经典写法:
# 允许已建立连接和关联连接的包
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 允许新的 SSH 连接
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# 丢弃其他
iptables -A INPUT -j DROPconntrack 表的内部结构
conntrack 表本质是一个哈希表。每条连接占一个
nf_conn 结构体,大小约 300-400
字节(取决于内核版本和启用的 feature)。
关键参数:
# 最大连接数(默认通常是 65536 或根据内存自动计算)
sysctl net.netfilter.nf_conntrack_max
# 哈希表桶数(默认 = max / 4)
sysctl net.netfilter.nf_conntrack_buckets
# 查看当前连接数
cat /proc/sys/net/netfilter/nf_conntrack_count当连接数达到 nf_conntrack_max
时,新连接会被丢弃,dmesg 里出现那条臭名昭著的日志:
nf_conntrack: table full, dropping packet
在 Kubernetes 环境里,每个 Service 的每次访问都会创建 conntrack 条目。一个中等规模的集群(几百个 Service,几千个 Pod)轻松能打到几十万条连接。默认的 65536 根本不够。
性能开销
conntrack 的开销主要在三个方面:
内存 —— 每条连接约 300-400 字节。100 万条连接 = ~400MB。对于现代服务器不算什么,但要注意这是不可 swap 的内核内存。
CPU —— 每个包都要查哈希表,做一次查找。哈希表设计得当的话是 O(1),但如果桶数太少、链过长,性能会退化。更关键的开销是 conntrack 的锁——早期内核用全局 spinlock,5.x 之后改成了 per-bucket lock,大幅改善了并发性能。
超时管理 —— conntrack 条目有超时时间。TCP ESTABLISHED 默认 5 天(432000 秒),TIME_WAIT 120 秒,UDP 30 秒。过期条目需要被清理。
# 查看超时设置
sysctl -a | grep conntrack | grep timeout
# 常见调优
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_buckets=262144用 conntrack 工具观察
conntrack 命令行工具是调试利器:
# 安装
apt install conntrack
# 查看所有连接
conntrack -L
# 实时监听新连接和销毁事件
conntrack -E
# 按源地址过滤
conntrack -L -s 10.244.1.5
# 统计
conntrack -S输出长这样:
tcp 6 431999 ESTABLISHED src=10.244.1.5 dst=10.96.0.1 sport=48210 dport=443
src=10.244.0.3 dst=10.244.1.5 sport=443 dport=48210 [ASSURED] mark=0 use=1
两行分别是正向和反向的五元组。[ASSURED]
表示这条连接双向都有流量,不会被提前驱逐。
四、NAT 三兄弟:SNAT、DNAT 与 MASQUERADE
NAT 是 Netfilter 最常用的功能之一,也是 Kubernetes 网络的基石。但 SNAT、DNAT、MASQUERADE 三者的区别,很多人搞不清楚。
DNAT:目标地址转换
DNAT 改的是包的目标地址和/或目标端口。在 PREROUTING 链执行(也可以在 OUTPUT 链执行,用于本机发出的包)。
典型场景:端口转发、负载均衡。
# 把访问本机 8080 端口的流量转发到 192.168.1.100:80
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.100:80
# Kubernetes kube-proxy 做的事情本质就是这个:
# 访问 ClusterIP:Port -> DNAT -> PodIP:TargetPort
iptables -t nat -A PREROUTING -d 10.96.0.10 -p tcp --dport 53 \
-j DNAT --to-destination 10.244.0.3:53DNAT 创建的 conntrack 条目会记住转换关系。回程的包会被自动做反向 NAT(称为 “un-DNAT”),把源地址从 PodIP 改回 ClusterIP。这一切都由 conntrack 自动完成,不需要额外规则。
SNAT:源地址转换
SNAT 改的是包的源地址。在 POSTROUTING 链执行。
典型场景:内网主机通过网关访问外网,需要把源地址改成网关的公网 IP。
# 把来自 10.0.0.0/24 的包,源地址改成 203.0.113.1
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j SNAT --to-source 203.0.113.1
# 指定源端口范围(避免端口耗尽)
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 \
-j SNAT --to-source 203.0.113.1:1024-65535SNAT 要求你指定一个固定的源地址。如果网关的公网 IP 是固定的,SNAT 是最佳选择——因为它不需要每次都查网卡地址,性能更好。
MASQUERADE:动态的 SNAT
MASQUERADE 和 SNAT 做的事情完全一样——改源地址。区别在于:MASQUERADE 不需要指定源地址,它会自动使用出口网卡的 IP。
# 不需要指定 IP,自动用 eth0 当前的地址
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE这在以下场景下更方便:
- 拨号上网(PPPoE),IP 地址每次拨号都变
- DHCP 获取地址,可能会变
- 多网卡环境,想用出口网卡的地址而不想硬编码
但 MASQUERADE 有代价:每个包都要查一次出口网卡的 IP 地址,而且当网卡地址变化时,所有对应的 conntrack 条目会被清除(连接中断)。SNAT 没有这个问题。
什么时候用哪个
| 场景 | 选择 | 原因 |
|---|---|---|
| 服务器固定公网 IP | SNAT | 性能好,不需要每包查地址 |
| 拨号/DHCP 动态 IP | MASQUERADE | IP 会变,没法硬编码 |
| Kubernetes Pod 出站 | MASQUERADE | Node IP 不固定,Pod 跨 Node 迁移 |
| 端口转发/负载均衡 | DNAT | 改目标地址 |
| 透明代理 | REDIRECT | DNAT 的特殊形式,改到本机 |
Kubernetes 里的 NAT
kube-proxy 在 iptables 模式下,为每个 Service 创建一系列规则:
- PREROUTING 链做 DNAT:ClusterIP:Port -> PodIP:TargetPort
- POSTROUTING 链做 MASQUERADE:Pod 访问外网时伪装源地址
- 对于 NodePort,还要在 PREROUTING 里匹配 NodeIP:NodePort 做 DNAT
当 Service 有多个 Endpoint 时,kube-proxy 用
statistic 模块实现概率负载均衡:
# 50% 概率跳到 Pod1,50% 跳到 Pod2
iptables -t nat -A KUBE-SVC-xxx -m statistic --mode random --probability 0.5 \
-j KUBE-SEP-pod1
iptables -t nat -A KUBE-SVC-xxx -j KUBE-SEP-pod2这个做法简单但粗暴。等你有 5000 个 Service,就知道它有多慢了。
五、iptables 的性能之殇
iptables 在设计之初根本没想过要处理上千条规则。它的数据结构和更新机制,在 Kubernetes 这种规模下暴露了严重的性能问题。
O(n) 规则匹配
iptables 的规则存储在链表里。匹配一个包时,从链的第一条规则开始,逐条比较,直到匹配成功或走完整条链。这是 O(n) 的线性扫描。
对于一个有 5000 个 Service、每个 Service 平均 3 个 Endpoint 的集群,kube-proxy 会生成大约 25000-40000 条 iptables 规则。一个包进来,最坏情况要扫描几万条规则才能找到匹配项。
实测数据(来自 Kubernetes 社区的 benchmark):
| Service 数量 | 规则数 | 每包延迟 (新连接) | 规则更新延迟 |
|---|---|---|---|
| 1,000 | ~8,000 | ~2ms | ~1s |
| 5,000 | ~40,000 | ~8ms | ~5s |
| 10,000 | ~80,000 | ~15ms | ~15s |
| 20,000 | ~160,000 | ~30ms | ~45s |
这还是新连接的延迟。已建立的连接走 conntrack 快速路径,不受规则数影响。但对于短连接密集的场景(比如微服务间的 HTTP 调用),这个延迟是不可接受的。
iptables-restore 的全量更新
iptables 的规则更新只有两种方式:
iptables -A/-D/-I:逐条添加/删除/插入。每次操作都要获取全局锁、序列化所有规则、修改、反序列化写回。iptables-restore:原子地替换整张表的所有规则。
kube-proxy 选择了方案 2,因为它需要保证规则集的一致性。但问题是:即使只改了一条规则,也要把全部几万条规则重新写入内核。
更新流程的时间复杂度是 O(n)——n 是规则总数。这意味着:
# 添加一个新 Service,kube-proxy 要做的事:
# 1. 生成完整的规则集(几万条)
# 2. 调用 iptables-restore 全量写入
# 3. 内核解析并重建所有规则的链表在 20000 个 Service 的集群里,增加一个 Service 可能需要 30-60 秒才能生效。这段时间里,新 Service 不可达。
xtables 锁竞争
iptables
在内核态有一把全局锁(xtables_lock)。任何读写
iptables
规则的操作都要先拿这把锁。在用户态,iptables
命令也有一把文件锁(/run/xtables.lock)。
当多个进程同时操作 iptables 时——比如 kube-proxy 在更新规则的同时,NetworkPolicy controller 也在改规则——它们会互相等锁。在规模较大的集群中,锁等待时间可以长达数十秒。
# iptables 1.8+ 支持 -w 参数等待锁(秒)
iptables -w 10 -A INPUT -p tcp --dport 80 -j ACCEPT为什么 IPVS 也不是银弹
kube-proxy 的 IPVS 模式用 hash table 做负载均衡,规则匹配是 O(1)。但它只替换了 Service -> Endpoint 这一层的匹配逻辑,其他功能(SNAT、MASQUERADE、NetworkPolicy)仍然依赖 iptables。IPVS 模式减轻了症状,但没有根治问题。
六、nftables:为什么要替换 iptables
nftables 是 Linux 内核从 3.13 开始引入的 iptables 替代方案。它不是对 iptables 的修修补补,而是在 Netfilter 框架之上重新设计的规则引擎。
设计上的改进
统一框架 —— iptables
实际上是四个工具:iptables(IPv4)、ip6tables(IPv6)、arptables(ARP)、ebtables(bridge)。它们各自独立,语法不同,规则不共享。nftables
用一个 nft 命令统一处理所有协议族。
用户态虚拟机 —— iptables 的每条规则在内核里是独立的数据结构。nftables 把规则编译成字节码,在内核的虚拟机里执行。这允许更复杂的匹配逻辑,也减少了内核/用户态的交互次数。
增量更新 —— nftables 支持原子地添加/删除单条规则,不需要像 iptables-restore 那样全量替换。这在大规模场景下是质的飞跃。
Sets、Maps 和 Concatenations
这是 nftables 的杀手锏。
Sets —— 把多个匹配条件合并为一个集合,用 hash/rbtree 做查找。O(1) 匹配,替代 iptables 的多条规则链式扫描。
# iptables:5 条规则
iptables -A INPUT -s 10.0.0.1 -j DROP
iptables -A INPUT -s 10.0.0.2 -j DROP
iptables -A INPUT -s 10.0.0.3 -j DROP
iptables -A INPUT -s 10.0.0.4 -j DROP
iptables -A INPUT -s 10.0.0.5 -j DROP
# nftables:1 条规则 + 1 个 set
nft add set ip filter blackhole { type ipv4_addr \; }
nft add element ip filter blackhole { 10.0.0.1, 10.0.0.2, 10.0.0.3, 10.0.0.4, 10.0.0.5 }
nft add rule ip filter input ip saddr @blackhole drop5 条 O(n) 规则变成 1 次 O(1) 查找。当集合有 10000 个元素时,性能差距是碾压级的。
Maps —— 不仅匹配,还能根据匹配结果做不同的动作。可以用来实现”根据目标端口跳转到不同的链”:
# 根据端口号跳转到不同的处理链
nft add map ip filter portmap { type inet_service : verdict \; }
nft add element ip filter portmap { 80 : jump http_chain, 443 : jump https_chain, 22 : jump ssh_chain }
nft add rule ip filter input tcp dport vmap @portmapConcatenations —— 允许用多个字段组合做匹配,比如”源 IP + 目标端口”组合:
# 匹配 IP + 端口组合
nft add set ip filter allowed { type ipv4_addr . inet_service \; }
nft add element ip filter allowed { 10.0.0.1 . 80, 10.0.0.2 . 443 }
nft add rule ip filter input ip saddr . tcp dport @allowed accept性能对比
| 维度 | iptables | nftables |
|---|---|---|
| 规则匹配 | O(n) 线性扫描 | O(1) set/map 查找 |
| 规则更新 | 全量替换 | 增量原子更新 |
| 协议支持 | 4 个独立工具 | 统一框架 |
| 规则数量扩展 | 万级别吃力 | 十万级别无压力 |
| 内核接口 | 多套 xt_* 模块 | 单一 nf_tables 模块 |
迁移现状
主流发行版的情况:
- Debian 10+ / Ubuntu 20.04+:默认用 nftables 后端(iptables 命令是 nft 的兼容层)
- RHEL 8+ / CentOS Stream 8+:默认 nftables
- Kubernetes 1.29+:kube-proxy 支持 nftables 模式(beta)
但生态迁移是缓慢的。大量的教程、工具、运维脚本仍然是 iptables 语法。如果你现在在管生产环境,两边都得会。
七、实验:用 iptables 搭建完整的 NAT 网关
纸上得来终觉浅。我们用 network namespace 搭一个最小的 NAT 网关实验环境,亲手看看 conntrack 是怎么工作的。
网络拓扑
[client-ns] [gateway-ns] [server-ns]
10.0.1.2/24 <-- veth --> 10.0.1.1 | 10.0.2.1 <-- veth --> 10.0.2.2/24
(NAT gateway)
三个 namespace:client 模拟内网主机,gateway 做 NAT 网关,server 模拟外网服务器。
环境搭建
# 创建 namespace
ip netns add client
ip netns add gateway
ip netns add server
# 创建 veth 对:client <-> gateway
ip link add veth-c type veth peer name veth-gc
ip link set veth-c netns client
ip link set veth-gc netns gateway
# 创建 veth 对:gateway <-> server
ip link add veth-gs type veth peer name veth-s
ip link set veth-gs netns gateway
ip link set veth-s netns server
# 配置 client
ip netns exec client ip addr add 10.0.1.2/24 dev veth-c
ip netns exec client ip link set veth-c up
ip netns exec client ip link set lo up
ip netns exec client ip route add default via 10.0.1.1
# 配置 gateway(两块网卡)
ip netns exec gateway ip addr add 10.0.1.1/24 dev veth-gc
ip netns exec gateway ip addr add 10.0.2.1/24 dev veth-gs
ip netns exec gateway ip link set veth-gc up
ip netns exec gateway ip link set veth-gs up
ip netns exec gateway ip link set lo up
# 开启转发
ip netns exec gateway sysctl -w net.ipv4.ip_forward=1
# 配置 server
ip netns exec server ip addr add 10.0.2.2/24 dev veth-s
ip netns exec server ip link set veth-s up
ip netns exec server ip link set lo up
ip netns exec server ip route add default via 10.0.2.1配置 NAT 规则
# 在 gateway 上配置 SNAT(MASQUERADE)
# 从 client 发出经 gateway 到 server 的包,源地址改为 gateway 的出口地址
ip netns exec gateway iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -o veth-gs -j MASQUERADE
# 允许转发
ip netns exec gateway iptables -A FORWARD -i veth-gc -o veth-gs -j ACCEPT
ip netns exec gateway iptables -A FORWARD -i veth-gs -o veth-gc -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 配置 DNAT:把访问 gateway 8080 的流量转到 server 的 80 端口
ip netns exec gateway iptables -t nat -A PREROUTING -i veth-gc -p tcp --dport 8080 \
-j DNAT --to-destination 10.0.2.2:80测试连通性
# 在 server 上启动一个简单的 HTTP 服务
ip netns exec server python3 -m http.server 80 &
# 从 client 通过 MASQUERADE 访问 server
ip netns exec client curl http://10.0.2.2
# 从 client 通过 DNAT 访问 gateway:8080 -> server:80
ip netns exec client curl http://10.0.1.1:8080用 conntrack 观察状态
# 在 gateway 上查看 conntrack 表
ip netns exec gateway conntrack -L
# 输出示例(MASQUERADE 场景):
# tcp 6 117 TIME_WAIT src=10.0.1.2 dst=10.0.2.2 sport=54312 dport=80
# src=10.0.2.2 dst=10.0.2.1 sport=80 dport=54312 [ASSURED] mark=0 use=1注意第二行的 dst=10.0.2.1——这说明 MASQUERADE
把回程包的目标地址从 10.0.2.1(gateway 的出口
IP)转换回了 10.0.1.2(client 的原始 IP),但
conntrack 记录的是 NAT 之前的回程信息。
# 实时监控连接事件
ip netns exec gateway conntrack -E
# 查看统计
ip netns exec gateway conntrack -S
# 输出:
# cpu=0 found=2 invalid=0 insert=0 insert_failed=0 drop=0 early_drop=0 ...清理
# 停掉 HTTP 服务
ip netns exec server pkill -f "python3 -m http.server"
# 删除 namespace(会自动清理 veth 和路由)
ip netns del client
ip netns del gateway
ip netns del server这个实验虽然简单,但它包含了 Kubernetes 网络里 NAT 的所有核心机制:DNAT(Service ClusterIP)、MASQUERADE(Pod 出站)、conntrack(状态追踪)、ip_forward(跨网段转发)。理解了这些,再去看 kube-proxy 生成的 iptables 规则就不会觉得天书了。
八、eBPF:防火墙的未来
iptables 的问题已经清楚了:O(n) 匹配、全量更新、锁竞争。nftables 解决了前两个,但它仍然是在 Netfilter 框架里打转。有没有一种方式,直接绕过 Netfilter,在更底层拦截和处理网络包?
有。那就是 eBPF。
eBPF 允许你在内核的关键路径上注入自定义程序——包括 XDP(网卡驱动层)、TC(traffic control 层)和 socket 层。这些挂载点比 Netfilter 的 hook 点更早(XDP 甚至在 skb 分配之前就能处理包),性能天然更好。
Cilium 就是基于 eBPF 实现的 Kubernetes CNI 插件。它完全绕过了 kube-proxy 和 iptables:
- Service 负载均衡:eBPF map 做 O(1) 查找,替代 iptables 的 DNAT 链
- NetworkPolicy:eBPF 程序在 TC 层直接判定,不走 Netfilter
- 连接追踪:Cilium 有自己的 CT map,不依赖 nf_conntrack
- SNAT/MASQUERADE:eBPF 程序在 TC egress hook 完成
eBPF 的详细原理和安全模型,我们在 eBPF 与安全 里专门展开。这里只需要记住一点:eBPF 不是对 iptables 的改良,而是对整个 Netfilter 框架的替代。
但 eBPF 需要较新的内核(4.19+,理想情况下 5.10+),而且调试工具链还不如 iptables 成熟。在可预见的未来,iptables/nftables 和 eBPF 会长期共存。理解 Netfilter 的工作原理仍然是必修课。
结语
回顾一下这篇文章的核心内容:
- Netfilter 是内核框架,iptables 只是它的用户态前端。五个 hook 点定义了包处理的完整生命周期。
- 四表五链的真实遍历顺序由 hook 优先级决定,不是教程里的简化版。DNAT 在 PREROUTING,SNAT 在 POSTROUTING,filter 在中间——这个顺序决定了你在哪个阶段能看到什么地址。
- conntrack 是 NAT 和有状态防火墙的基础,但它的内存和 CPU
开销在大规模环境下不可忽视。
nf_conntrack: table full是生产环境里最常见的网络故障之一。 - SNAT 用于固定 IP,MASQUERADE 用于动态 IP,DNAT 做端口转发。Kubernetes 三个都用。
- iptables 的 O(n) 匹配和全量更新在千级 Service 规模下成为瓶颈。nftables 的 sets/maps 和增量更新解决了这些问题,eBPF 则更进一步绕过了整个 Netfilter。
下一篇 路由与隧道,我们从单机的包处理走向跨主机通信——Linux 路由表、策略路由、VXLAN/GENEVE 隧道封装。这些是理解 Kubernetes 跨 Node Pod 通信的前置知识。