S SRE Notes

iptables、IPVS 与 nftables 详解:从安装使用到内核收发包全流程

本文系统梳理 Linux 内核网络三大包处理框架——iptablesIPVSnftables,覆盖安装、基本使用、底层原理、内核收发包路径,并分析它们在 Kubernetes(kube-proxy)中的实际使用与选型。


01三者关系总览

框架 全称 引入版本 内核模块 定位 用户态工具
iptables netfilter/iptables Linux 2.4(2001) ip_tables / ip6_tables / nf_conntrack 通用包过滤 / NAT 防火墙 iptablesiptables-saveiptables-restore
IPVS IP Virtual Server Linux 2.4(LVS 项目) ip_vs / ip_vs_rr 等调度器 专用四层负载均衡器 ipvsadm
nftables nftables Linux 3.13(2014)框架,长期演进 nf_tables iptables 的继任者,统一过滤/NAT/LB nft

三者都构建在 Netfilter 之上,并非互相替代的独立栈。nftables 是官方钦定的 iptables 继任者,IPVS 则是专门为负载均衡场景设计的独立子系统(同样挂在 Netfilter 钩子上)。

                 ┌─────────────────────────────────────┐
                 │        用户态工具 (userspace)         │
                 │  iptables   ipvsadm     nft          │
                 └────────┬────────┬────────┬──────────┘
                          │        │        │
                 ┌────────▼────────▼────────▼──────────┐
                 │   Netlink 接口 (配置下发/读取)        │
                 └────────┬────────┬────────┬──────────┘
                          │        │        │
   ┌──────────────────────▼────────▼────────▼──────────────────────┐
   │                      Linux Kernel                              │
   │  ┌────────────┐  ┌────────────┐  ┌──────────────┐             │
   │  │ ip_tables  │  │   ip_vs    │  │  nf_tables   │  ← 内核模块  │
   │  │ (iptables) │  │  (IPVS)    │  │ (nftables)   │             │
   │  └─────┬──────┘  └─────┬──────┘  └──────┬───────┘             │
   │        │               │                │                     │
   │   ┌────▼───────────────▼────────────────▼─────┐               │
   │   │        Netfilter 钩子 (5 个 hook 点)        │               │
   │   │  PREROUTING / INPUT / FORWARD /            │               │
   │   │  OUTPUT / POSTROUTING                      │               │
   │   └─────────────────────┬──────────────────────┘               │
   │                         │                                       │
   │   ┌─────────────────────▼──────────────────────┐               │
   │   │  conntrack (连接跟踪) + NAT + 路由决策        │               │
   │   └────────────────────────────────────────────┘               │
   └─────────────────────────────────────────────────────────────────┘

02一、安装

1.1 iptables

iptables 工具本身已预装在绝大多数 Linux 发行版。Linux 2.4.18 起内核默认编译进 Netfilter 框架。

# Debian / Ubuntu
sudo apt install -y iptables

# RHEL / CentOS / Rocky
sudo dnf install -y iptables

# 查看版本(区分 legacy 与 nf_tables 后端)
iptables --version
# iptables v1.8.9 (nf_tables)   ← 后端是 nf_tables(现代内核)
# iptables v1.8.9 (legacy)      ← 后端是旧的 xtables

现代发行版(RHEL 8+、Debian 10+、Ubuntu 22.04+)的 iptables 命令默认走 nf_tables 后端iptables-nft),命令兼容但底层已是 nftables 内核。update-alternatives --display iptables 可查看当前后端。

1.2 IPVS(ipvsadm)

IPVS 内核模块同样早就在主线内核中,但默认未必加载。用户态工具是 ipvsadm

# 安装用户态工具
sudo apt install -y ipvsadm        # Debian / Ubuntu
sudo dnf install -y ipvsadm        # RHEL 系

# 加载 IPVS 内核模块及调度算法
sudo modprobe ip_vs
sudo modprobe ip_vs_rr             # 轮询
sudo modprobe ip_vs_wrr            # 加权轮询
sudo modprobe ip_vs_lc             # 最少连接
sudo modprobe ip_vs_sh             # 源地址哈希
sudo modprobe nf_conntrack         # IPVS 依赖连接跟踪(4.19+ 统一用此模块)

# 持久化加载(重启生效)
cat <<'EOF' | sudo tee /etc/modules-load.d/ipvs.conf
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_lc
ip_vs_sh
nf_conntrack
EOF

# 验证
ipvsadm -Ln
lsmod | grep ip_vs

1.3 nftables

# Debian / Ubuntu
sudo apt install -y nftables

# RHEL / CentOS / Rocky
sudo dnf install -y nftables

# 启用并启动服务(加载 /etc/nftables.conf)
sudo systemctl enable --now nftables

# 版本与内核支持
nft --version
# nftables 1.0.6 (Lester Gooch)

# 内核模块(通常已内建,非独立模块)
grep NF_TABLES /boot/config-$(uname -r)
# CONFIG_NF_TABLES=y
# CONFIG_NF_TABLES_INET=y
# CONFIG_NFT_CT=y        # conntrack 支持
# CONFIG_NFT_CHAIN_NAT=y # NAT 支持

1.4 关键内核配置确认

# 查看当前内核的网络过滤相关配置
grep -E 'NETFILTER|NF_TABLES|IP_VS|NF_CONNTRACK|NETFILTER_XT_' /boot/config-$(uname -r)

关键配置项含义:

内核配置 作用
CONFIG_NETFILTER Netfilter 框架总开关
CONFIG_NF_CONNTRACK 连接跟踪(iptables/IPVS/nftables 状态防火墙都依赖)
CONFIG_NF_TABLES nftables 核心框架
CONFIG_NFT_CT nftables 使用 conntrack 状态匹配
CONFIG_NFT_CHAIN_NAT nftables NAT 链类型
CONFIG_IP_VS IPVS 核心
CONFIG_IP_VS_RR/LC/SH... IPVS 各调度算法
CONFIG_NETFILTER_XT_MATCH_* iptables 扩展匹配模块

03二、基本使用

2.1 iptables 基本用法

核心概念:表、链、规则

iptables 有 5 个表(按优先级):rawmanglenatfiltersecurity,每个表包含预定义的链。

作用 常用链
raw 在 conntrack 之前匹配,可对包免跟踪(NOTRACK) PREROUTING、OUTPUT
mangle 修改包的 TTL、TOS、Mark 等 全部 5 链
nat DNAT / SNAT / MASQUERADE PREROUTING、OUTPUT、POSTROUTING、INPUT
filter 过滤(ACCEPT/DROP/REJECT) INPUT、FORWARD、OUTPUT
security SELinux 安全标记 INPUT、FORWARD、OUTPUT
# 查看规则(-n 不解析域名,-v 显示计数,--line-numbers 显示编号)
sudo iptables -L -n -v --line-numbers
sudo iptables -t nat -L -n -v --line-numbers

# 允许已建立连接的回流(几乎每个防火墙的第一条规则)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# 允许 SSH(22)和 HTTP(80)/ HTTPS(443)
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

# 允许回环
sudo iptables -A INPUT -i lo -j ACCEPT

# 默认策略设为 DROP(谨慎,先留 SSH 后路)
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP

# NAT 示例:把入站 80 端口转发到内网 10.0.0.5:8080(DNAT)
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.0.0.5:8080
# 出站做 SNAT(MASQUERADE 会自动用出口网卡 IP)
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE

# 删除规则:先带 --line-numbers 查编号,再按编号删
sudo iptables -D INPUT 3

# 持久化(Debian/Ubuntu)
sudo apt install -y iptables-persistent
sudo netfilter-persistent save
# 规则存到 /etc/iptables/rules.v4

iptables -A(append,追加到末尾)和 iptables -I(insert,插入到顶部或指定位置)是日常最常用的两条命令。规则的顺序至关重要——内核按顺序匹配,命中即跳转。

iptables-save / iptables-restore

# 导出当前规则
sudo iptables-save > /tmp/iptables.rules

# 从文件恢复(原子操作,比逐条 -A 高效得多,kube-proxy 内部就是用这个)
sudo iptables-restore < /tmp/iptables.rules

2.2 IPVS(ipvsadm)基本用法

IPVS 的模型是「虚拟服务(Virtual Service)+ 真实服务器(Real Server)」。一个 VIP:Port 是一个虚拟服务,下面挂多个后端。

# 查看虚拟服务表(-L 列出,-n 不解析,--stats 显示流量统计)
sudo ipvsadm -Ln
sudo ipvsadm -Ln --stats
sudo ipvsadm -Ln --rate        # 实时速率

# 添加一个 TCP 虚拟服务:VIP=10.96.0.10:80,调度算法 rr(轮询)
sudo ipvsadm -A -t 10.96.0.10:80 -s rr

# 添加两个真实后端(-r),转发模式 -g(DR)/ -m(masquerade/NAT)/ -i(ipip 隧道)
sudo ipvsadm -a -t 10.96.0.10:80 -r 10.0.0.5:8080 -m        # NAT 模式
sudo ipvsadm -a -t 10.96.0.10:80 -r 10.0.0.6:8080 -m -w 2   # 权重 2

# 删除一个后端
sudo ipvsadm -d -t 10.96.0.10:80 -r 10.0.0.5:8080

# 删除整个虚拟服务
sudo ipvsadm -D -t 10.96.0.10:80

# 清空所有
sudo ipvsadm -C

# 持久化
sudo ipvsadm-save > /tmp/ipvs.rules
sudo ipvsadm-restore < /tmp/ipvs.rules

IPVS 调度算法一览(内核模块):

算法 模块 说明
rr ip_vs_rr 轮询(kube-proxy 默认)
wrr ip_vs_wrr 加权轮询
lc ip_vs_lc 最少连接数
wlc ip_vs_wlc 加权最少连接
sh ip_vs_sh 源地址哈希(会话保持)
dh ip_vs_dh 目的地址哈希
lblc / lblcr ip_vs_lblc 局部性最少连接(缓存场景)
sed ip_vs_sed 最短期望延迟
nq ip_vs_nq 永不排队

IPVS 三种转发模式:

模式 参数 工作方式 是否改 IP
NAT(masquerade) -m 修改目标 IP(DNAT)转发给后端,回包再 SNAT 回 VIP 是(DNAT + SNAT)
DR(Direct Routing) -g 改 MAC 不改 IP,后端直接回客户端(需在同一 L2) 否(仅 L2 重写)
Tunnel(IP-IP) -i 外层封装 IP 隧道,后端解封装后直接回客户端 否(IP-in-IP)

Kubernetes kube-proxy 用的是 NAT 模式-m),因为 Pod 可能跨节点,DR/Tunnel 要求后端能直接对外回包,与 k8s 网络模型不匹配。

2.3 nftables 基本用法

nftables 与 iptables 最大的语法差异:没有预定义表和链,全部要显式创建。

核心概念:family、table、chain、rule

概念 对应 iptables 说明
family(地址族) IPv4/IPv6 分开 ip/ip6/inet(v4+v6)/arp/bridge/netdev
table 表(filter/nat/...) 命名空间,名字自定义,不再固定
chain(base chain) 预定义链(INPUT/OUTPUT...) 必须手动绑定到 hook + priority
chain(regular chain) 用户自定义链 不绑 hook,仅供 jump
rule 规则 match + action(verdict)

交互式命令

# 创建一个 inet 表(同时处理 v4/v6)
sudo nft add table inet my_table

# 创建 base chain:input,绑定到 input hook,priority 0,默认 drop
sudo nft 'add chain inet my_table input { type filter hook input priority 0; policy drop; }'
sudo nft 'add chain inet my_table output { type filter hook output priority 0; policy accept; }'

# 添加规则
sudo nft add rule inet my_table input iifname "lo" accept
sudo nft add rule inet my_table input ct state established,related accept
sudo nft add rule inet my_table input tcp dport { 22, 80, 443 } accept
sudo nft add rule inet my_table input icmp type echo-request accept

# 查看规则集
sudo nft list ruleset
sudo nft -a list table inet my_table    # -a 显示 handle(删除时要用)

# 按 handle 删除规则
sudo nft delete rule inet my_table input handle 4

# 删除整张表
sudo nft delete table inet my_table

脚本化(推荐方式)

nftables 原生支持脚本,/etc/nftables.conf 就是脚本。这才是 nftables 的「正确」用法——整表原子替换,不存在「逐条加规则时的中间状态」问题。

# /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet firewall {
    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        ct state invalid drop
        iifname "lo" accept

        icmp type echo-request accept
        icmpv6 type { echo-request, nd-neighbor-solicit } accept

        tcp dport { 22, 80, 443 } accept comment "ssh http https"
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

# NAT 表
table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat;
        tcp dport 80 dnat to 10.0.0.5:8080
    }
    chain postrouting {
        type nat hook postrouting priority srcnat;
        oifname "eth0" ip saddr 10.0.0.0/24 masquerade
    }
}
# 加载脚本(原子操作)
sudo nft -f /etc/nftables.conf
# 或通过 systemd
sudo systemctl reload nftables

nftables 的杀手锏:sets / maps / objects

这是 iptables 做不到的——nftables 可以在内核里维护集合、字典、计数器等「有状态对象」,单条规则就能匹配上千个 IP。

# 定义一个集合(黑名单 IP)
sudo nft add set inet firewall blacklist_v4 '{ type ipv4_addr; flags interval; }'
sudo nft add element inet firewall blacklist_v4 '{ 1.2.3.4, 5.6.7.0/24, 10.0.0.0/8 }'

# 一条规则匹配整个集合
sudo nft add rule inet firewall input ip saddr @blacklist_v4 drop

# 增删集合元素(不影响规则本身)
sudo nft add element inet firewall blacklist_v4 '{ 9.9.9.9 }'
sudo nft delete element inet firewall blacklist_v4 '{ 1.2.3.4 }'

对比 iptables:要屏蔽 1000 个 IP,iptables 要写 1000 条规则(线性匹配 O(n));nftables 用 set,内核用哈希/基数树,匹配 O(1)。


04三、底层原理:Netfilter 与内核钩子

iptables、IPVS、nftables 三者的底层都挂在 Netfilter 上。Netfilter 是 Linux 内核网络协议栈里的一组「钩子点(hook points)」,允许内核模块在包流经协议栈的特定位置注册回调函数。

3.1 五个核心钩子

Netfilter 在 IP 层定义了 5 个钩子(IPv4 内核符号):

钩子 内核常量 触发时机
PREROUTING NF_INET_PRE_ROUTING 包刚进入协议栈、路由决策之前(最早可见 L3 包)
INPUT NF_INET_LOCAL_IN 路由决策后,包目的地是本机,送达本地进程之前
FORWARD NF_INET_FORWARD 路由决策后,包要转发到其他接口(本机做路由器)
OUTPUT NF_INET_LOCAL_OUT 本机进程产生的包,刚进入协议栈、路由决策之前
POSTROUTING NF_INET_POST_ROUTING 包即将离开协议栈送上网卡之前(最后机会)

后来还增加了:

  • ingress(Linux 4.2,netdev 族):在 PREROUTING 之前,绑定到具体网卡,比 tc 更早
  • egress(Linux 5.16,netdev 族):在 POSTROUTING 之后,包即将出网卡

tc 是什么:tc(Traffic Control,流量控制)是 Linux 内核的 QoS 子系统,工作在网卡队列层(比 IP 协议栈更靠近网卡),由 qdisc(队列规则)+ class(类别)+ filter(过滤器) 三层组件构成,用于限速、流量整形、带宽分配。tc 也有一个 ingress qdisc 挂在网卡入口处做入向整形。nftables netdev 族的 ingress hook 比 tc ingress qdisc 触发更早——换言之,包到 nftables ingress 时 tc 还没看到它。整体顺序:网卡 RX → nftables ingress(netdev) → tc ingress → PREROUTING。

3.2 钩子优先级

同一个钩子点上可以挂多个链/模块,按 priority 值从小到大依次执行。Netfilter 预定义的优先级常量:

关键字 数值 常量 含义
- -450 NF_IP_PRI_RAW_BEFORE_DEFRAG 分片重组之前(最早)
- -400 NF_IP_PRI_CONNTRACK_DEFRAG IP 分片重组
raw -300 NF_IP_PRI_RAW raw 表(conntrack 之前,可 NOTRACK)
- -225 NF_IP_PRI_SELINUX_FIRST SELinux 早期
- -200 NF_IP_PRI_CONNTRACK 连接跟踪(关键:在此之后才能用 ct state)
mangle -150 NF_IP_PRI_MANGLE mangle 表
dstnat -100 NF_IP_PRI_NAT_DST DNAT(目标地址转换)
filter 0 NF_IP_PRI_FILTER filter 表(过滤)
security 50 NF_IP_PRI_SECURITY security 表(SELinux 标记)
srcnat 100 NF_IP_PRI_NAT_SRC SNAT(源地址转换)
- 225 NF_IP_PRI_SELINUX_LAST SELinux 收尾
- 300 NF_IP_PRI_CONNTRACK_HELPER conntrack helper(期望连接)
- INT_MAX NF_IP_PRI_CONNTRACK_CONFIRM conntrack 确认(提交新连接)

理解这张表是理解「为什么 DNAT 在 PREROUTING、SNAT 在 POSTROUTING」「为什么 filter 在 NAT 之后」的关键。priority 决定了执行顺序,hook 决定了触发时机

3.3 iptables 如何挂在 Netfilter 上

iptables 把自己的预定义链注册到对应 hook 上。以入站到本机的包为例,依次经过:

入站包 → PREROUTING hook
         │
         ├─ raw.PREROUTING      (priority -300)
         ├─ (conntrack defrag)  (-400,分片重组,更早)
         ├─ (conntrack)         (-200,连接跟踪)
         ├─ mangle.PREROUTING   (-150)
         ├─ nat.PREROUTING      (-100,DNAT 在这里)
         ├─ (routing decision)  ← 路由:本机 or 转发
         │
         └→ INPUT hook
            ├─ mangle.INPUT     (-150)
            ├─ filter.INPUT     (0,过滤在这里)
            ├─ security.INPUT   (50)
            ├─ nat.INPUT        (100,少数内核支持)
            └─ (conntrack confirm) (INT_MAX)
            → 送达本地 socket

iptables 各表在各 hook 的分布(关键摘要):

表↓ \ 链→ PREROUTING INPUT FORWARD OUTPUT POSTROUTING
raw
mangle
nat(DNAT) ✓*
filter
security
nat(SNAT)

*nat.INPUT 仅在较新内核(CONFIG_NETFILTER_XT_TABLE_nat 启用 NAT LOCAL)才有。

3.4 nftables 如何挂在 Netfilter 上

nftables 不再有「预定义表/链」的概念。你创建的每张表只是一个命名空间,里面的 base chain 通过 type ... hook ... priority ... 显式绑定到 Netfilter 钩子。换言之,nftables 给了你「在任意 hook、任意 priority 上插链」的能力,比 iptables 灵活得多。

# 等价于 iptables 的 filter.INPUT
nft 'add chain inet fw input { type filter hook input priority filter; policy drop; }'

# 也可以在 PREROUTING 上插一条优先级 -300 的链(等价于 raw 表)
nft 'add chain inet fw raw_pre { type filter hook prerouting priority raw; }'

# NAT 链
nft 'add chain inet fw dnat { type nat hook prerouting priority dstnat; }'
nft 'add chain inet fw snat { type nat hook postrouting priority srcnat; }'

底层:nftables 通过 nf_tables 内核模块把 base chain 注册为 Netfilter hook 的回调。规则用「表达式(expression)」组合而成(如 tcp dport 22 = 取 L4 头部目的端口 + 比较 22),内核用一个通用的字节码 VM 执行,而不是像 iptables 那样每个匹配都写一个 xt_match 模块。

3.5 IPVS 如何挂在 Netfilter 上

IPVS 不走 filter/nat 表那一套,它直接在 LOCAL_INLOCAL_OUT 两个 hook 上注册自己的处理函数(优先级 NF_IP_PRI_LOCAL_IN 附近)。

IPVS 的处理逻辑:当包的目的 IP 是某个已注册的虚拟服务(VIP:Port)时,IPVS 拦截该包,按调度算法选一个后端 Real Server,做 DNAT(NAT 模式),然后重新走路由发出去。

入站包 → PREROUTING
         ├─ (iptables 的 nat.PREROUTING,priority -100)
         │   ↑ IPVS 通常不在这里,IPVS 在 LOCAL_IN
         ├─ (routing decision)
         │   ↑ 若 dst=VIP 且本机有 IPVS 虚拟服务 → 判定为本机
         └→ LOCAL_IN hook
            ├─ IPVS 处理函数(priority ≈ 0)
            │   ↑ 匹配 VIP:VPort → 选 RealServer → DNAT → 重新路由
            │   ↑ 包被「重新注入」协议栈,下次路由会走向 RealServer IP
            ├─ filter.INPUT(iptables)
            └→ conntrack confirm

关键点:IPVS 和 iptables 共存时,IPVS 优先于 filter.INPUT 拦截 VIP 流量;IPVS 模式下 kube-proxy 仍会用 iptables 做少量辅助工作(masquerade、NodePort 标记等),并用 ipset 保证 iptables 规则数量恒定(不随 Service 数增长)。

3.6 conntrack:三者共用的连接跟踪

连接跟踪(connection tracking,nf_conntrack 模块)是状态防火墙和 NAT 的基础。它为每条流维护一个条目,记录五元组、状态、NAT 映射、超时等。

连接状态

状态 含义
NEW 新连接的第一个包(如 TCP SYN)
ESTABLISHED 已建立(TCP 握手完成;UDP 双向都有包)
RELATED 与已有连接相关(如 FTP 数据连接、ICMP 错误回包)
INVALID 无法识别/不属于任何连接(如错误的 TCP 标志位)
SNAT/DNAT 伪状态,表示该包做过源/目标 NAT

conntrack 表关键参数/proc/sys/net/netfilter/nf_conntrack_*,源自内核文档):

参数 默认值 说明
nf_conntrack_max = nf_conntrack_buckets 允许的最大连接条目数。表满后新连接会被丢弃(生产事故常见诱因)
nf_conntrack_buckets 内存/16384,[1024, 262144] 哈希表桶数。每个连接正反方向各占一条,所以满表平均链长为 2
nf_conntrack_count 只读 当前条目数
nf_conntrack_tcp_timeout_established 432000s(5 天) TCP 已建立连接的超时。生产环境常调小到 1-2 小时
nf_conntrack_tcp_timeout_time_wait 120s TIME_WAIT 超时
nf_conntrack_udp_timeout 30s UDP 单向流超时
nf_conntrack_udp_timeout_stream 120s UDP 双向流超时
nf_conntrack_icmp_timeout 30s ICMP 超时
# 查看当前连接数 / 上限
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

# 查看连接表(人类可读)
sudo cat /proc/net/nf_conntrack | head

# 调大上限(高并发/k8s 大集群必备)
echo 1048576 | sudo tee /proc/sys/net/netfilter/nf_conntrack_max
# buckets 只能在模块加载时或 initial netns 设置,需重载模块或启动参数 nf_conntrack.hashsize=

# 内核日志满了会打:nf_conntrack: table full, dropping packet
dmesg | grep conntrack

k8s 集群 conntrack 调优是 kube-proxy 不论哪种模式都必须做的:nf_conntrack_max 按节点内存设(如 1M),tcp_timeout_established 从 5 天降到 1 天,避免长连接占满表。

3.7 Netfilter hook 调用机制(内核视角)

前面讲了「5 个 hook 点」,但内核里到底怎么调用?关键入口是 NF_HOOK 宏(定义在 include/linux/netfilter.h)。

NF_HOOK 宏的展开

// 简化形式
int NF_HOOK(u8 pf, unsigned int hook, struct sk_buff *skb,
            struct net_device *in, struct net_device *out,
            int (*okfn)(struct sk_buff *))
{
    int ret = nf_hook(pf, hook, skb, in, out, okfn);
    if (ret == 1)          // NF_ACCEPT
        return okfn(skb);  // 继续协议栈正常路径
    return ret;            // DROP/STOLEN/QUEUE 等,不再走 okfn
}
  • pf:协议族(PF_INET / PF_INET6 / PF_BRIDGE
  • hook:5 个 hook 编号之一(NF_INET_PRE_ROUTING 等)
  • okfn:hook 链全部 ACCEPT 后要执行的「下一站」函数(如 ip_rcv_finiship_local_deliver_finish
  • 内部调用 nf_hook()nf_hook_slow(),后者遍历该 hook 上注册的所有回调

nf_hook_slow 的遍历逻辑

NF_HOOK(pf, PRE_ROUTING, skb, in, out, ip_rcv_finish)
    │
    └→ nf_hook_slow(skb, hook)
         │
         │  struct nf_hook_entries *e = nf_hook_array[hook];
         │  for (i = 0; i < e->num_hook_entries; i++) {
         │      verdict = e->hooks[i].hook(e->hooks[i].priv, skb,
         │                                 state);
         │      switch (verdict) {
         │          case NF_ACCEPT:  continue;      // 走下一条
         │          case NF_DROP:    goto drop;     // 丢包
         │          case NF_STOLEN:  return stolen; // 被钩子接管,不再回协议栈
         │          case NF_QUEUE:   goto queue;    // 送用户态 nfqueue
         │          case NF_REPEAT:  i--;           // 重复本条
         │      }
         │  }
         └→ 全部 ACCEPT → 返回 1 → NF_HOOK 调用 okfn(skb)

5 种 verdict(判决)

verdict 常量值 含义
NF_DROP -1 丢包,kfree_skb,包从此消失
NF_ACCEPT 1 接受,继续下一条 hook;全部 ACCEPT 后走 okfn
NF_STOLEN 2 钩子「偷走」包,后续处理由钩子自己负责(如 IPVS 重新注入、conntrack 延迟确认)
NF_QUEUE 3 送入 nf_queue,用户态 nfqueuelibnetfilter_queue)处理
NF_REPEAT 4 重复执行当前钩子(极少用)

包从网卡到 PREROUTING hook 的精确调用栈

网卡 RX (硬中断)
  └→ napi_poll → netif_receive_skb
       └→ __netif_receive_skb_core
            └→ ip_rcv (IPv4 协议栈入口)
                 │  // 这里还没过 Netfilter
                 └→ NF_HOOK(PF_INET, NF_INET_PRE_ROUTING,
                            skb, indev, NULL, ip_rcv_finish)
                      │
                      ├→ [raw 表 / nftables raw 链回调]
                      ├→ [conntrack 回调 (CT=NEW 时建条目)]
                      ├→ [mangle 表回调]
                      ├→ [nat 表 DNAT 回调]
                      └→ 全部 ACCEPT → ip_rcv_finish(skb)
                           │  // 路由查表
                           └→ ip_route_input → 决定本机 or 转发

关键理解okfn(如 ip_rcv_finish)只有在所有 hook 都返回 ACCEPT 时才会被调用。任何一条返回 DROP/STOLEN,包就不再走协议栈正常路径。iptables/nftables/IPVS 的核心都是往这个 hook 数组里注册自己的回调函数。

3.8 iptables 内核数据结构与匹配流程

iptables 在内核里不是「一条一条规则散落存放」,而是把整张表组织成一个连续内存的线性数组,匹配时顺序遍历。

核心数据结构

struct xt_table {              // 一张表(filter/nat/mangle/raw)
    const char            *name;
    unsigned int          valid_hooks;   // 哪些 hook 上有效
    void                  *private;      // → struct xt_table_info
    struct module         *me;
};

struct xt_table_info {         // 表的实际内容
    unsigned int       size;          // 规则区总字节数
    unsigned int       number;        // 规则条数
    unsigned int       initial_entries;
    void              *entries[NR_CPUS]; // 每颗 CPU 一份(减少锁竞争)
};

struct ipt_entry {             // 单条规则头(固定部分)
    struct ipt_ip    ip;       // 源/目 IP、掩码、入/出接口名
    unsigned int     nfcache;
    u_int16_t        target_offset;  // 从本头到 target 的偏移
    u_int16_t        next_offset;    // 到下一条规则的偏移
    unsigned int     comefrom;
    struct xt_counters counters;     // 包/字节统计
    // 后面紧跟: matches[] + target (变长)
};

规则内存布局(一张表就是一个连续数组)

xt_table_info.entries[] 内存布局:
┌──────────────────────────────────────────────┐
│ ipt_entry #0                                 │
│  ├─ ipt_ip (src/dst/mask/in/out)             │
│  ├─ target_offset ──┐                        │
│  └─ counters        │                        │
│   ┌─────────────────┘                        │
│   │ matches[0]: -m tcp --dport 80 (变长)     │
│   │ matches[1]: -m conntrack --ctstate ...   │
│   ├─ target: -j ACCEPT / DROP / DNAT / JUMP  │
│   └─ padding                                │
├──────────────────────────────────────────────┤
│ ipt_entry #1  ...                            │
├──────────────────────────────────────────────┤
│ ...                                          │
├──────────────────────────────────────────────┤
│ ipt_error (ERROR 目标, 表结尾标记)             │
└──────────────────────────────────────────────┘

匹配主循环 ipt_do_table

ipt_do_table(skb, hook, state, table)
  │
  │  e = table->entries;         // 第一条规则
  │  while (e != error_entry) {
  │      // 1. 匹配 IP 头: src/dst/in/out/掩码
  │      if (!ip_packet_match(e->ip, skb)) goto next;
  │      // 2. 遍历 matches[] 逐个调用 xt_match->match()
  │      for (m = e->matches; m; m = m->next) {
  │          if (!m->match(skb, m)) goto next;
  │      }
  │      // 3. 全部匹配 → 执行 target
  │      verdict = t->target(skb, t);
  │      switch (verdict) {
  │          case NF_ACCEPT: return 1;        // 离开本表
  │          case NF_DROP:   return -1;
  │          case IPT_RETURN: e = back_stack;  // 从自定义链返回
  │          default: e = table + verdict;     // JUMP 到自定义链
  │      }
  │  next:
  │      e = (void*)e + e->next_offset;       // 下一条
  │  }

性能关键:这是纯线性扫描,O(n)。规则数到 10 万级时,每个包都要遍历前 N 条才能命中。iptables 模式的 kube-proxy 之所以慢,根本原因就在这里。

iptables-restore 原子替换机制

用户态:  iptables-restore < rules.v4
    │
    │  1. 解析文本 → 构造新的 xt_table_info
    │  2. setsockopt(SO_SET_REPLACE, new_info)
    │
    └→ 内核 translate_table() 校验新表
         │
         │  3. xt_replace_table(table, new_info)
         │      old = table->private;
         │      table->private = new_info;   // 原子指针替换
         │      synchronize_rcu();            // 等所有 reader 退出
         │      free(old);                   // 释放旧表
         │
         └→ 全程无锁读, 写一次完成

这就是为什么 iptables-restore 是原子的——整张表指针被一次性替换,正在遍历旧表的 CPU 通过 RCU 机制安全退出后再释放。但代价是:任何一条规则变更都要重写整张表,这在 kube-proxy 大集群下是 CPU 杀手。

3.9 conntrack 详细实现

连接跟踪是状态防火墙和 NAT 的基石。它在内核里用一张哈希表维护「每条流」的状态。

核心:nf_conntrack_tuple(五元组)

struct nf_conntrack_tuple {
    struct {
        union {
            __be32  ip;        // v4 地址
            struct in6_addr ip6; // v6 地址
        };
        __be32  mask;
    } src;                     // 源 IP
    struct {
        union {
            __be32  ip;
            struct in6_addr ip6;
        };
        __be32  mask;
    } dst;                     // 目的 IP
    union {
        struct { __be16 port; } u3;   // L4 端口
        struct { u8 type, code; } icmp;
    } src.u, dst.u;
    u_int8_t protonum;         // IPPROTO_TCP/UDP/ICMP
};

// 每条连接有两个 tuple: 正向(orig) + 反向(reply)
struct nf_conntrack_tuple_hash {
    struct hlist_node  node;
    struct nf_conntrack_tuple tuple;
};

struct nf_conn {
    struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; // [0]=orig, [1]=reply
    unsigned long       status;       // 状态位图(IPS_NEW/ESTABLISHED/...)
    unsigned long       timeout;
    struct nf_ct_ext   *ext;          // NAT/HELPER/timeout 等扩展
    struct nf_conn_help *helper;
    ...
};

哈希表组织与查找

nf_conntrack_hash (全局哈希表, nf_conntrack_buckets 个桶)
  │
  │  key = hash(tuple) % nf_conntrack_buckets
  │
  ├─ bucket[0] → conn_A.tuplehash[orig] ↔ conn_A.tuplehash[reply]
  ├─ bucket[k] → conn_B.tuplehash[orig]
  │              conn_C.tuplehash[reply]   (同桶链表)
  ...
  └─ bucket[N]

查找一个包属于哪条连接:
  1. 从包头部构造 tuple (srcIP,dstIP,proto,srcPort,dstPort)
  2. hash(tuple) → bucket
  3. 遍历 bucket 链表, 比较 tuple 相等
  4. 命中 → 返回 nf_conn (含状态/NAT映射)
  5. 未命中 → NEW 连接, 新建条目

反向回包查找:
  回包的 tuple 恰好等于正向的 reply tuple
  → 同一张表同一个 bucket 命中, 拿到同一条 conn

TCP 连接状态机(内核视角)

              SYN
   NEW ────────────────► IPS_EXPECTED (期望连接)
   │  SYN+ACK               │
   │                        ▼
   │  第一包          IPS_ASSURED (确认双向都见过包)
   │  ───────────► ESTABLISHED ◄───────────────┐
   │                  │  │                      │
   │                  │  │ FIN/FIN-ACK          │
   │                  │  ▼                      │
   │                  │ CLOSE_WAIT ──► LAST_ACK │
   │                  │                          │
   │                  │ FIN+ACK                  │
   │                  ▼                          │
   │              FIN_WAIT ──► TIME_WAIT ────────┘
   │                  │        (timeout 120s)
   │  超时             │
   └──────────────► 空闲超时 → conntrack GC 清理

status 位图关键位:IPS_SRC_NAT / IPS_DST_NAT(做过 SNAT/DNAT)、IPS_ASSURED(双向都见过包,超时更长)、IPS_SEEN_REPLY(见过回包)。

GC(garbage collector)机制

nf_conntrack GC worker (内核定时器, 每 GC_INTERVAL 秒跑一次)
  │
  │  for each bucket:
  │      for each conn in bucket:
  │          if (conn->timeout <= now):
  │              if (status & ASSURED && !over) 跳过(长连接保活)
  │              else __nf_conntrack_l4proto->expire(conn)
  │
  │  表满时(nf_conntrack_count >= nf_conntrack_max):
  │      early_drop() → 从最老桶里随机 evict 一条
  │      若仍满 → drop packet (dmesg: table full, dropping packet)

expectation(期望连接)机制

某些协议在一条控制连接上协商出新的数据连接(FTP 主动模式、SIP/RTP 等)。conntrack 的 helper 模块解析控制连接载荷,预先登记「期望连接」:

FTP 控制连接 (client:1234 → server:21)
  │  helper (nf_conntrack_ftp) 解析 PORT 命令
  │  "PORT 10,0,0,5,4,1" → 数据连接将是 10.0.0.5:1025 → server:20
  │
  └→ 登记 expectation:
        tuple = (server:20 → 10.0.0.5:1025, TCP)
        marked IPS_EXPECTED, 关联到控制连接的 conn

后续当数据连接第一个包到达:
  → 查 conntrack 表未命中
  → 查 expectation 表命中
  → 自动建 conn, status |= IPS_EXPECTED, 不走 NEW 的 NAT 规则
  → 按 expectation 里预设的 NAT 映射改写

这就是为什么 FTP 主动模式在 NAT 后面也能工作——nf_conntrack_ftp helper 模块解析了控制连接载荷并预登记了期望连接。K8s 环境一般禁用 helper(nf_conntrack_helper=0)避免安全风险。

3.10 NAT 实现细节

NAT 不是一个独立的 hook,而是挂在 conntrack 上的「变换动作」。它的核心是:只在第一个包(NEW)走规则匹配,后续包直接用 conntrack 里记录的映射改写

数据结构

struct nf_nat_range {
    unsigned int       flags;      // 范围标志(IP范围/端口范围/持久化)
    union nf_inet_addr min_addr, max_addr;  // 地址范围
    union nf_conntrack_man_proto min, max;  // 端口范围
};

struct nf_conn_nat {
    struct nf_nat_range range[IP_CT_DIR_MAX];  // orig/reply 两个方向
    struct nf_conn *ct;
};

DNAT/SNAT 在 conntrack 中的记录

第一个包 NEW, 做 DNAT 10.96.0.10:80 → 10.244.2.8:80:

conntrack 表记录:
  conn.tuplehash[orig]  = (src=PodA, dst=10.96.0.10:80)    ← 原始方向
  conn.tuplehash[reply] = (src=10.244.2.8:80, dst=PodA)    ← 反向(改写后)

  conn.nat = { orig: DNAT to 10.244.2.8:80 }

后续回包(10.244.2.8:80 → PodA):
  构造 tuple 查表 → 命中 conn.tuplehash[reply]
  → 取出对应 orig tuple → 反向改写:
      src 10.244.2.8:80 → 10.96.0.10:80
  → 直接送出, 不再过 nat.PREROUTING 规则

端口分配算法

nf_nat_setup_info(ct, range, maniptype)
  │
  │  // 在 range 指定的端口范围内找未占用的
  │  for (port = range->min; port <= range->max; port++) {
  │      new_tuple = *ct->tuplehash[maniptype].tuple;
  │      new_tuple.dst.u.tcp.port = port;
  │      if (!nf_nat_used_tuple(new_tuple, ct))   // 检查未占用
  │          goto found;
  │  }
  │  // 范围全占满 → 从整个端口空间随机找
  │  for (attempts = 0; attempts < 128; attempts++) {
  │      port = random();
  │      if (!nf_nat_used_tuple(...)) goto found;
  │  }
  │  return -1;  // 端口耗尽, 丢包

为什么 NAT 只对 NEW 包走规则

第一个包 (NEW):
  PREROUTING hook
    ├─ conntrack lookup → 未命中 → 建 conn, 标 NEW
    ├─ nat.PREROUTING 规则匹配 → 调 nf_nat_inet_fn()
    │      → nf_nat_setup_info() 记录映射到 conn.nat
    └→ 包带着改写后的地址继续协议栈

后续包 (ESTABLISHED):
  PREROUTING hook
    ├─ conntrack lookup → 命中 conn
    │   status |= SEEN_REPLY, 不再是 NEW
    ├─ nat.PREROUTING 规则【直接跳过】(仅对 NEW 有效)
    ├─ nf_nat_inet_fn() 检查 conn.nat 已有映射
    │   → 按 conn.nat 改写地址 (固定操作, 无需规则匹配)
    └→ 包继续走

这就是 NAT 的「fast path」——conntrack 命中后,NAT 改写是固定查表操作,不再扫描 nat 表规则。生产环境大量长连接下 NAT 开销可控,根本原因在此。

3.11 IPVS 数据结构与调度实现

IPVS 不走 iptables 那套线性表,它用哈希表组织「虚拟服务」和「连接」,查找 O(1)。

核心数据结构

struct ip_vs_service {        // 一个虚拟服务 (VIP:VPort:Proto)
    struct hlist_node  s_list;    // 按 {af,protocol,vip,vport} 哈希
    struct hlist_node  f_list;    // 按 {af,fwmark} 哈希(标记模式)
    atomic_t           refcnt;
    u16                protocol;
    union nf_inet_addr addr;      // VIP
    __be16             port;      // VPort
    struct ip_vs_scheduler *scheduler;  // → rr/wrr/lc/sh 等算法 ops
    struct list_head   destinations;    // Real Server 链表
    unsigned int       num_dests;
    ...
};

struct ip_vs_dest {           // 一个 Real Server
    struct list_head   n_list;    // 挂在 service->destinations
    union nf_inet_addr addr;      // RIP
    __be16             port;      // RPort
    atomic_t           weight;    // 权重
    atomic_t           activeconns, inactconns;  // 连接计数
    atomic_t           conn_flags; // 转发模式 NAT/DR/TUNNEL
    ...
};

struct ip_vs_conn {           // 一条 IPVS 连接(五元组哈希)
    struct hlist_node  c_list;    // 按 {proto, caddr,cport, vaddr,vport} 哈希
    struct ip_vs_dest *dest;       // 选中的 Real Server
    unsigned long      timeout;
    struct nf_conn    *ct;         // 关联的 conntrack 条目
    ...
};

包处理主流程 ip_vs_in

入站包到达 (PREROUTING → routing → LOCAL_IN)
  │
  └→ ip_vs_in(skb)
       │
       │  1. 构造 tuple {proto, saddr,sport, daddr,dport}
       │     (daddr 是 VIP)
       │
       │  2. 查 ip_vs_conn 表(哈希)
       │     ├─ 命中 → 已有连接, 取 dest
       │     └─ 未命中 → 新连接:
       │          a. 查 ip_vs_service 表(按 VIP:VPort:Proto 哈希) O(1)
       │          b. service->scheduler->schedule(service, skb)
       │             → 按 rr/wrr/lc/sh 选一个 dest
       │          c. 建 ip_vs_conn, 关联 dest
       │
       │  3. 根据 dest->conn_flags 选 xmit 函数:
       │     ├─ IP_VS_CONN_F_MASQ  → ip_vs_nat_xmit  (NAT 模式)
       │     │     做 DNAT: dst = dest->addr:port
       │     ├─ IP_VS_CONN_F_FWD   → ip_vs_dr_xmit   (DR 模式)
       │     │     改 L2 MAC, 不改 L3
       │     └─ IP_VS_CONN_F_TUNNEL→ ip_vs_tunnel_xmit (IPIP 隧道)
       │           外层封装 IP 头 dst=dest->addr
       │
       │  4. xmit → 重新注入协议栈(走 POSTROUTING)
       │
       └→ 包被「偷走」(NF_STOLEN), 不再走 filter.INPUT

调度算法实现要点

算法 实现
rr service->destinations 链表轮转,last_visited = last_visited->next
wrr 每个 dest 维护 weight,按权重比例轮转(经典 SM 算法)
lc 遍历 destinations,选 activeconns + inactconns 最小的
wlc 在 lc 基础上除以 weight,选 (active+inactive)/weight 最小的
sh hash(srcIP) % num_dests,源地址相同的包固定去同一个 dest(会话保持)
sed 在 wlc 基础上 +1,选 (active+inactive+1)/weight 最小的,避免权重高的 dest 一直被选
nq 在 sed 基础上,若有 inactconns=0 的 dest 优先选(永不排队)

IPVS 查找复杂度:service 查找哈希 O(1),conn 查找哈希 O(1),调度算法多数 O(n_dests)(n_dests 是单 service 的后端数,通常很小)。与 iptables 的 O(n_rules) 形成鲜明对比——iptables 的 n_rules 是全节点所有 Service 的规则总和。

3.12 nftables 字节码 VM 与事务机制

nftables 与 iptables 最大的架构差异:iptables 每个「匹配」都是一个独立内核模块(xt_match),nftables 用一个通用表达式 VM 统一处理所有匹配。

数据结构关系

struct nft_table {              // 一张表(命名空间)
    struct list_head  chains;    // 该表下所有 chain
    struct list_head  sets;      // 该表下所有 set/map
    u64               generation; // 所属 generation ID
    ...
};

struct nft_chain {              // 一条链
    struct nft_rule   *rules_gen0;  // 当前生效规则(链表)
    struct nft_rule   *rules_gen1;  // 新规则(事务提交中)
    u8                flags;
    struct nft_base_chain *base; // base chain 才有, 含 hook/priority
};

struct nft_rule {               // 一条规则
    struct list_head  list;
    unsigned int      dlen;      // 表达式区长度
    unsigned char     data[]     // 柔性数组: 连续存放 nft_expr
        __attribute__((aligned));
};

struct nft_expr {               // 单个表达式(变长)
    const struct nft_expr_ops *ops;  // 函数表: eval/init/destroy
    unsigned int      len;
    unsigned char     data[];    // 表达式私有数据
};

struct nft_expr_ops {
    const char       *name;      // 如 "payload"/"cmp"/"lookup"/"immediate"
    void            (*eval)(const struct nft_expr *expr,
                            struct nft_regs *regs,
                            const struct nft_pktinfo *pkt);
    ...
};

执行引擎 nft_do_chain

nft_do_chain(skb, state)   // 挂在 Netfilter hook 上的回调
  │
  │  struct nft_chain *chain = ...;
  │  struct nft_rule *rule;
  │
  │  list_for_each_entry(rule, &chain->rules, list) {
  │      struct nft_expr *expr;
  │      nft_rule_for_each_expr(expr, rule) {
  │          expr->ops->eval(expr, &regs, &pkt);
  │          // eval 把结果写入 regs.verdict
  │          // payload: 从 skb 取字段写入寄存器
  │          // cmp:     比较寄存器与立即数
  │          // lookup:  查 set
  │          // immediate: 设置 verdict(NF_DROP/NF_ACCEPT/...)
  │          if (regs.verdict.code != NFT_CONTINUE)
  │              break;   // 跳出本规则的表达式循环
  │      }
  │      switch (regs.verdict.code) {
  │          case NFT_BREAK:    continue;     // 本规则不匹配, 下一条
  │          case NF_ACCEPT:    return 1;     // 离开 chain
  │          case NF_DROP:      return -1;
  │          case NFT_JUMP:     push(chain); chain = target; continue;
  │          case NFT_RETURN:   chain = pop(); continue;
  │      }
  │  }
  │  return chain->policy;   // 默认策略

对比 iptables:

维度 iptables nftables
匹配实现 每个 -m xxx 一个 xt_match 内核模块 通用表达式 VM,payload/cmp/lookup 组合
新匹配扩展 要写内核模块、modprobe 加载 纯 VM 指令组合,无需新模块
规则存储 线性数组 ipt_entry[] 链表 + 每条规则内嵌连续表达式
执行 ipt_do_table 线性扫 + 逐 match 调函数 nft_do_chain 线性扫 + 逐 expr 调 eval

sets/maps 底层

nft set 的两种底层结构:

1. 哈希表 (rhashtable, 默认):
   用于精确匹配 { 1.2.3.4, 5.6.7.8, ... }
   查找 O(1)

2. 红黑树 (rbtree):
   用于区间匹配 { 10.0.0.0/8, 192.168.0.0/16 }
   查找 O(log n)

map = set + 关联数据:
   key: ipv4_addr . inet_service   (复合键)
   value: ipv4_addr . inet_service (后端地址)
   查找 key → 拿到 value → 用于 DNAT

事务机制(generation ID)

nftables 的规则更新是事务式、原子的,靠 generation ID 实现:

当前生效:  generation N
  │
用户态 nft 发命令 (NFT_MSG_NEWRULE / NEWCHAIN / ...):
  │
  │  1. 内核在 generation N+1 的副本上操作
  │     (chain->rules_gen1 指针, 与 gen0 并存)
  │
  │  2. 所有命令处理完, 用户态发 NFT_MSG_NEWGEN:
  │     内核做 commit:
  │       a. generation = N+1
  │       b. chain->rules_gen0 = chain->rules_gen1  (指针切换)
  │       c. 通知所有 reader 切到新 generation
  │
  │  3. synchronize_rcu(): 等所有正在用 gen N 的 reader 退出
  │
  └→ 4. free(gen N 的旧规则)   // RCU 延迟释放

这就是 nftables 增量更新的核心:规则可以在不停机、不锁读路径的前提下原子生效。对比 iptables 的 iptables-restore(整表替换),nftables 可以只改一条规则,开销恒定。


05四、内核收发包全流程

把上面所有概念串起来,看一个包从网卡进/出到底走了哪些路径。

4.1 完整收发包路径

                          [网卡 RX]
                              │
                              ▼ (DMA + 硬中断)
                       ┌──────────────┐
                       │  NAPI poll   │  ← 软中断 NET_RX,驱动 poll 拿包
                       └──────┬───────┘
                              ▼
                       ┌──────────────┐
                       │  netif_receive_skb  │
                       └──────┬───────┘
                              │ (netdev 族)
                              ▼
                        ┌──────────┐
                        │ ingress  │  ← nftables netdev ingress hook(最早,4.2+)
                        └────┬─────┘
                             ▼
                       ┌──────────────┐
                       │  XDP (可选)  │  ← eBPF,比 ingress 还早
                       └──────┬───────┘
                              ▼
                       ┌──────────────┐
                       │  PREROUTING  │  ← Netfilter hook #1
                       │  ┌─────────┐ │
                       │  │raw (-300)│ │  iptables raw / nftables raw 链
                       │  │mangle   │ │  (-150)
                       │  │nat DNAT │ │  (-100)  ← DNAT/端口转发在这里
                       │  └─────────┘ │
                       └──────┬───────┘
                              ▼
                       ┌──────────────┐
                       │ routing decision │  ← 路由查表:本机 or 转发?
                       └───┬──────┬─────┘
                  本机 ↓      │      ↓ 转发
              ┌──────────┐    │   ┌──────────┐
              │  INPUT   │    │   │ FORWARD  │  ← Netfilter hook #3
              │ ┌────────┐│   │   │ ┌────────┐│
              │ │mangle  ││   │   │ │mangle  ││
              │ │filter  ││   │   │ │filter  ││  ← iptables FORWARD 过滤
              │ │security││   │   │ └────────┘│
              │ └────────┘│   │   └────┬─────┘
              │ (IPVS    ││   │        │
              │  LOCAL_IN││   │        ▼
              │  hook)   ││   │   ┌──────────┐
              └─────┬────┘   │   │POSTROUTING│ ← Netfilter hook #4
                    │        │   │ ┌────────┐│
                    ▼        │   │ │srcnat  ││ ← SNAT/MASQUERADE 在这里
              ┌──────────┐  │   │ │(100)   ││
              │ 本地 socket│ │   │ └────────┘│
              │ (进程收包) │  │   └────┬─────┘
              └──────────┘  │        ▼
                            │   [网卡 TX]
                            │
              本机发包 ↓
              ┌──────────────┐
              │  本地进程产生包 │
              └──────┬───────┘
                     ▼
              ┌──────────────┐
              │  routing decision │  ← 先路由(选源 IP、出口)
              └──────┬───────┘
                     ▼
              ┌──────────────┐
              │   OUTPUT     │  ← Netfilter hook #5
              │  ┌─────────┐ │
              │  │raw      │ │
              │  │mangle   │ │
              │  │nat DNAT │ │  (本机发出也可 DNAT)
              │  │filter   │ │
              │  │(IPVS    │ │   LOCAL_OUT 也有 IPVS 钩子)
              │  └─────────┘ │
              └──────┬───────┘
                     ▼
              ┌──────────────┐
              │  POSTROUTING │  ← Netfilter hook #4(与转发合流)
              │  │ srcnat   │ │  ← SNAT/MASQUERADE
              └──────┬───────┘
                     ▼
                [网卡 TX]

4.2 三条典型路径

场景 路径
入站到本机进程 PREROUTING → routing(本机) → INPUT → 本地 socket
本机做路由器转发 PREROUTING → routing(转发) → FORWARD → POSTROUTING → 网卡
本机进程发包 本地进程 → routing → OUTPUT → POSTROUTING → 网卡

4.3 收包的软中断路径(与 CPU 软中断排查相关)

网卡收包到协议栈的细节(对应《CPU 软中断高排查记录》):

1. 网卡收到帧 → DMA 写入环形缓冲区 → 触发硬中断
2. 硬中断处理:NAPI 禁用该队列硬中断,触发 NET_RX 软中断
3. 软中断 net_rx_action → 驱动的 napi_poll 回调
4. napi_poll 批量取 skb → netif_receive_skb
5. __netif_receive_skb_core:处理 bridge/_vlan,调用协议栈 ptype_all 钩子
6. ip_rcv → NF_INET_PRE_ROUTING(Netfilter PREROUTING hook)→ 路由 → ...

RPS/RSS 影响的就是第 2-3 步:RSS 在硬中断层分流,RPS 在软中断层把包派发到其他 CPU 的 backlog 队列。这就是为什么 RSS 开了但 RPS 关闭会导致软中断集中在部分 CPU。

4.4 NAT 在内核里的位置

NAT(DNAT/SNAT)只在 第一个包(NEW 状态)走完整规则匹配,后续包由 conntrack 直接按已建立的映射改写,不再过 nat 表规则(nat 表规则只对 NEW 包有意义)。

第一个包(NEW):  PREROUTING → nat.PREROUTING(DNAT) → routing → ... → nat.POSTROUTING(SNAT)
                                                          conntrack 记录映射
后续包(ESTABLISHED): conntrack 直接按映射改写,跳过 nat 规则

这就是为什么生产环境大量长连接下,iptables NAT 的性能开销没有想象中大——大部分包走的是 conntrack 的快速路径,不是规则匹配。

4.5 flowtable:旁路加速(Linux 5.x)

对于已建立的流,Netfilter 提供 flowtable 旁路:匹配到 flowtable 的包不再走 PREROUTING/POSTROUTING 那一长串 hook,直接在 ingress 层做完 NAT 后送出网卡。等价于「硬件卸载的 fastpath」。

ingress → 查 flowtable → 命中 → 改 NAT、减 TTL、neigh_xmit → 出网卡
                          │
                          └─未命中→ 走经典 PREROUTING→...→POSTROUTING 路径
                                    并由 "flow offload" 规则把新流加入 flowtable

nf_flowtable_tcp_timeout 默认 30s,超时后流回到 conntrack 经典路径。这对高 PPS 长连接场景是显著加速。

4.6 三条路径的精确函数调用栈

4.1 给的是「hook 级」概览,这里给出「函数级」调用链,标注每个 NF_HOOK 触发点。所有函数名来自 net/ipv4/ip_input.cip_output.cip_forward.cnet/netfilter/

路径 1:入站到本机进程

ip_rcv(skb)                      // 协议栈入口, 简单校验
  └→ NF_HOOK(PRE_ROUTING, ip_rcv_finish)   ★ hook #1
       │
       │  // raw(-300) / conntrack(-200) /
       │  // mangle(-150) / nat DNAT(-100)
       │
       └→ ip_rcv_finish(skb)
            ├→ ip_route_input()   // 路由决策: 本机
            └→ dst_input = ip_local_deliver
                 │
                 └→ ip_local_deliver(skb)
                      └→ NF_HOOK(LOCAL_IN, ip_local_deliver_finish)  ★ hook #2
                           │
                           │  // mangle(-150) / filter(0) / security(50)
                           │  // (IPVS 在此 hook 拦截 VIP 流量)
                           │
                           └→ ip_local_deliver_finish(skb)
                                ├→ ipprot = skb->protocol (TCP=6)
                                └→ tcp_v4_rcv(skb)      // 交 TCP 协议栈
                                     ├→ __inet_lookup_skb()  // 找 socket
                                     └→ tcp_v4_do_rcv()
                                          └→ tcp_rcv_established()
                                               → 数据进 socket 接收队列
                                               → 唤醒进程 recv/read

路径 2:本机做路由器转发

ip_rcv(skb)
  └→ NF_HOOK(PRE_ROUTING, ip_rcv_finish)   ★ hook #1
       └→ ip_rcv_finish(skb)
            ├→ ip_route_input()   // 路由决策: 转发
            └→ dst_input = ip_forward
                 │
                 └→ ip_forward(skb)
                      ├→ ip_decrease_ttl()   // TTL-1, 为 0 则丢
                      └→ NF_HOOK(FORWARD, ip_forward_finish)  ★ hook #3
                           │
                           │  // mangle(-150) / filter(0) / security(50)
                           │  // iptables -A FORWARD 过滤在此
                           │
                           └→ ip_forward_finish(skb)
                                └→ dst_output = ip_output
                                     │
                                     └→ ip_output(skb)
                                          └→ NF_HOOK(POST_ROUTING, ip_finish_output)  ★ hook #4
                                               │
                                               │  // mangle(-150) / srcnat(100)
                                               │  // SNAT/MASQUERADE 在此
                                               │
                                               └→ ip_finish_output(skb)
                                                    ├→ ip_finish_output2()
                                                    └→ neigh_output() → dev_queue_xmit()
                                                         → 网卡驱动 TX

路径 3:本机进程发包

进程 send/write
  └→ tcp_sendmsg()
       └→ tcp_transmit_skb()
            └→ ip_queue_xmit(skb)
                 ├→ ip_route_output_flow()  // 路由: 选源 IP/出口
                 └→ NF_HOOK(LOCAL_OUT, ip_output)   ★ hook #5
                      │
                      │  // raw(-300) / mangle(-150) /
                      │  // nat DNAT(-100) / filter(0)
                      │  // (IPVS LOCAL_OUT hook 也在此)
                      │
                      └→ ip_output(skb)     // 与转发合流
                           └→ NF_HOOK(POST_ROUTING, ip_finish_output)  ★ hook #4
                                │
                                │  // mangle(-150) / srcnat(100)
                                │  // SNAT/MASQUERADE 在此
                                │
                                └→ ip_finish_output()
                                     → neigh_output() → dev_queue_xmit()
                                          → 网卡 TX

关键观察:5 个 hook 点对应 5 个 NF_HOOK 调用,分别嵌在 ip_rcv/ip_local_deliver/ip_forward/ip_output/ip_queue_xmit 里。路径 2 和路径 3 在 ip_output 处合流,共享 POSTROUTING hook。理解这个调用链是排查「包到哪一步消失了」的基础——结合 tcpdump(看网卡层)和 iptables -L -v -n(看 hook 计数器)能精确定位丢包点。


06五、iptables vs IPVS vs nftables 横向对比

维度 iptables IPVS nftables
设计目标 通用包过滤/NAT 防火墙 专用四层负载均衡 iptables 继任者,统一框架
数据结构 线性链表(逐条匹配) 哈希表(O(1) 查找) 表达式字节码 + sets(哈希/基数树)
查找复杂度 O(n),规则越多越慢 O(1),与 Service 数无关 O(1)~O(log n)(用 set 时)
规则更新 全量 iptables-restore 重写 增量 ipvsadm -a/-d 增量或整表原子替换
大规模更新开销 250K 规则时单次 restore 5-15s CPU 单条增删,常数时间 增量小,整表替换也快
NAT 能力 完整 DNAT/SNAT/MASQUERADE NAT/DR/Tunnel 三种模式 完整 NAT(继承自 Netfilter)
调度算法 随机(默认) rr/wrr/lc/wlc/sh/sed/nq... 等十余种 可用 map+numgen 实现,原生不强
连接跟踪 依赖 conntrack 依赖 conntrack(NAT 模式) 依赖 conntrack
L7 过滤 不支持(需 layer7 补丁,已废弃) 不支持 不支持(L7 应交给应用层代理)
语法 分散命令式,-A/-I/-D 命令式 -A/-a 脚本化,类编程语言
v4/v6 统一 否(iptables/ip6tables 分开) 是(inet 族)
内核演进 维护模式,不再加新特性 维护模式 活跃开发,新特性首选
官方定位 逐步淘汰 维护 推荐

三者的关系本质

iptables ──── 维护模式(不再新增特性)
   │
   │  (命令兼容层:iptables-nft)
   ▼
nftables ──── 官方继任者,活跃开发
   │
   └── 同样构建在 Netfilter 之上

IPVS ─────── 独立子系统,专为 LB,不参与 iptables→nftables 迁移
              在 k8s 中是 iptables/ipvs/nftables 三选一的「数据平面后端」

iptables-nft(现代发行版默认)让你继续用 iptables 语法,但底层规则存在 nf_tables 内核里。这是平滑迁移的过渡方案。


07六、在 Kubernetes 中的使用

6.1 kube-proxy 的本质

kube-proxy 是每个节点上运行的 daemon。它不转发包(名字是历史遗留,早期版本确实是 userspace 代理)。现代 kube-proxy 是「规则生成器」:watch API server 的 Service/EndpointSlice,把对应的内核规则(iptables/IPVS/nftables)写好,然后退出数据路径。真正的包转发由内核做。

┌─────────────┐    watch     ┌──────────────┐
│ API Server  │ ───────────→ │  kube-proxy  │  ← 用户态,只生成规则
└─────────────┘              └──────┬───────┘
                                    │ 写规则
                    ┌───────────────┼───────────────┐
                    ▼               ▼               ▼
              ┌──────────┐   ┌──────────┐   ┌──────────┐
              │ iptables │   │   IPVS   │   │ nftables │   ← 内核数据平面
              │  规则    │   │  虚拟服务│   │   set    │
              └──────────┘   └──────────┘   └──────────┘
                    │               │               │
                    └───────────────┴───────────────┘
                                    │
                          数据包由内核直接转发
                          (kube-proxy 不在路径上)

配置方式(kube-proxy ConfigMap):

apiVersion: kubeproxy.config.k8s.io/v1alpha1
# 或 kube-proxy 的 command flag: --proxy-mode
mode: "iptables"   # 默认。可选: iptables | ipvs | nftables | kernelspace

6.2 iptables 模式(默认)

工作原理

kube-proxy 在 nat 表里注入一组自定义链:

作用
KUBE-SERVICES 入口链,挂在 PREROUTING 和 OUTPUT 上。按 (ClusterIP, port, proto) 匹配,跳转到对应 Service 链
KUBE-SVC-<HASH> 每个 Service 一条,含若干「按概率 DNAT 到后端」的规则(统计模式实现加权随机)
KUBE-SEP-<HASH> 每个 Service EndPoint 一条,做 DNAT 到 Pod IP:Port
KUBE-MARK-MASQ 给包打 0x4000 mark,用于后续 SNAT
KUBE-POSTROUTING 挂在 POSTROUTING,对带 mark 的包做 MASQUERADE
KUBE-FIREWALL 丢弃 INVALID 状态包

一个 ClusterIP 请求的完整路径

以 Pod A(10.244.1.5)访问 Service 10.96.0.10:80(后端 Pod B/C/D)为例:

Pod A 发出: src=10.244.1.5 dst=10.96.0.10 (ClusterIP)
    │
    ▼
PREROUTING(nat) → KUBE-SERVICES
    │  线性扫描规则,匹配 dst=10.96.0.10 dport=80
    │  → jump KUBE-SVC-XXXXXXXX
    ▼
KUBE-SVC-XXXXXXXX
    │  rule1: statistic mode random probability 0.333 → jump KUBE-SEP-AAAA (Pod B)
    │  rule2: statistic mode random probability 0.500 → jump KUBE-SEP-BBBB (Pod C)
    │  rule3: → jump KUBE-SEP-CCCC (Pod D)  ← 最后一条必然命中
    │  假设随机落到 rule2 → jump KUBE-SEP-BBBB
    ▼
KUBE-SEP-BBBB
    │  DNAT to 10.244.2.8:80  ← Pod C 的 IP
    │  (若 src 是本节点 Pod,还会先 KUBE-MARK-MASQ 打标)
    ▼
routing → 经 CNI veth/overlay 送到 Pod C
    │
    │  conntrack 记录: (10.244.1.5, 54321, 10.96.0.10, 80) → (10.244.1.5, 54321, 10.244.2.8, 80)
    │                  原始 ClusterIP 已被记录,用于回包还原
    ▼
Pod C 收到: src=10.244.1.5 dst=10.244.2.8

回包: src=10.244.2.8 dst=10.244.1.5
    │
    ▼
conntrack 命中已记录的连接 → 反向 NAT
    │  src 还原为 10.96.0.10 (ClusterIP)
    ▼
Pod A 收到: src=10.96.0.10 dst=10.244.1.5  ← 看起来就是和 ClusterIP 通信

规则数量与性能

每个 Service 的规则数 ≈ 1(KUBE-SERVICES) + 1(KUBE-SVC) + N(KUBE-SEP, N=后端数)。1 万个 Service、平均 3 后端,就有约 4 万条规则。所有包都要线性遍历 KUBE-SERVICES 链找匹配——这就是 iptables 模式的性能瓶颈。

观测命令

# 查看所有 kube 相关链
sudo iptables-save | grep -E '^-A KUBE'

# 查看某 Service 的规则(KUBE-SVC-HASH 可从 KUBE-SERVICES 里找到)
sudo iptables -t nat -L KUBE-SERVICES -n | grep 10.96.0.10
sudo iptables -t nat -L KUBE-SVC-XXXXXXXX -n -v

# 查看某后端
sudo iptables -t nat -L KUBE-SEP-XXXXXXXX -n -v

# 查看连接跟踪
sudo cat /proc/net/nf_conntrack | grep 10.96.0.10
sudo conntrack -L | grep 10.96.0.10

6.3 IPVS 模式

工作原理

kube-proxy 为每个 Service 创建一个 IPVS 虚拟服务(VIP=ClusterIP),每个 Pod 后端作为一个 Real Server。包到达时,IPVS 用哈希表 O(1) 查到虚拟服务,按调度算法选后端,DNAT 转发。

# 启用 IPVS 模式(kube-proxy ConfigMap)
mode: "ipvs"
ipvs:
  scheduler: "rr"          # 调度算法,默认 rr

# 节点上验证
sudo ipvsadm -Ln
# Prot LocalAddress:Port Scheduler Flags
#   -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
# TCP  10.96.0.10:80 rr
#   -> 10.244.1.8:80                Masq    1      0          0
#   -> 10.244.2.5:80                Masq    1      0          0
#   -> 10.244.3.9:80                Masq    1      0          0

IPVS 模式仍需要 iptables

IPVS 不覆盖所有场景,kube-proxy 仍用 iptables(配合 ipset)做辅助:

用途 iptables 规则 ipset
出节点流量 SNAT(masquerade) KUBE-POSTROUTING KUBE-LOOP-BACK
NodePort 流量标记/拦截 KUBE-NODE-PORT KUBE-NODE-PORT-TCP/UDP
ClusterIP 非节点流量过滤 KUBE-FIREWALL KUBE-CLUSTER-IP
externalIPs 处理 - KUBE-EXTERNAL-IP

关键点:用 ipset 后,iptables 规则数量与 Service 数无关(恒定几十条),只有 ipset 集合元素随 Service 增长。这是 IPVS 模式在大规模集群性能优势的核心。

IPVS 模式的优势与坑

优势

  1. 查找 O(1),与 Service 数无关,10 万 Service 也不退化
  2. 增量更新,单个 Service 变化只增删一条 IPVS 条目,不重写全表
  3. 支持多种调度算法(rr/lc/sh...),可做会话保持
  4. kube-proxy CPU 恒定,不随 Service 数飙升

  1. IPVS 的 NAT 模式包路径与纯 iptables 不同,与其他用 iptables 的组件(如 Calico 策略)可能有兼容性问题(需验证)
  2. IPVS 无法完整实现 k8s Service 的所有语义(官方文档明确:「IPVS API 是 k8s Service API 的糟糕匹配,ipvs 模式从未能正确实现所有用例」)
  3. 仍依赖 conntrack,conntrack 满了一样丢包
  4. 需要 IPVS 内核模块全部加载,否则 kube-proxy 启动失败
# IPVS 模式前置检查(kube-proxy 启动失败时排查)
sudo lsmod | grep -E 'ip_vs|nf_conntrack'
# 应包含: ip_vs, ip_vs_rr, nf_conntrack 等

# conntrack 调优(IPVS 模式同样需要)
echo 1048576 | sudo tee /proc/sys/net/netfilter/nf_conntrack_max

6.4 nftables 模式(KEP-3866,推荐)

背景

nftables 模式是 Kubernetes 官方钦定的 iptables 和 ipvs 的替代品(官方原话:「nftables proxy mode is essentially a replacement for both the iptables and ipvs modes」)。KEP-3866 提出,Kubernetes 1.29 引入 alpha,1.31 beta。

工作原理

与 iptables 模式类似(都是写规则到 nat 表),但用 nftables 的 sets/maps 结构,把 ClusterIP→后端列表 的映射存进内核集合。查找走哈希,更新走集合元素增删,兼具 iptables 的语义完整性和 IPVS 的 O(1) 查找。

nftables set:
  name: svc-10.96.0.10:80
  type: inet_service . inet_service   # (clusterIP, port)
  elements: { 10.244.1.8, 10.244.2.5, 10.244.3.9 }  # 后端 Pod IP

规则: dst nat to @svc-10.96.0.10:80   # 命中即 DNAT 到集合里的某个后端

三种模式对比(k8s 场景)

维度 iptables 模式 IPVS 模式 nftables 模式
查找复杂度 O(n),线性遍历 KUBE-SERVICES O(1),IPVS 哈希 O(1),nft set 哈希
规则更新 全量 restore,大集群 5-15s 增量 增量
kube-proxy CPU 随 Service 数线性增长 恒定 恒定
Service 语义完整 完整 部分缺失 完整
与 CNI 兼容 好(都用 iptables) 需验证 取决于 CNI 是否支持 nft
内核版本要求 低(2.4+) 中(2.4+ ip_vs 模块) 高(5.13+,建议 5.18+)
官方推荐度 默认(历史) 不再推荐 推荐(替代 ipvs)
生产成熟度 极成熟 成熟 较新(1.31 beta)

官方建议(引自 kubernetes.io)

如果你部署在能跑 nftables 模式的较新 Linux 上,用 nftables 模式;如果系统太老跑不了 nftables 模式,优先考虑 iptables 模式而不是 ipvs 模式(因为 iptables 模式这些年性能已经大幅改进)。

启用 nftables 模式

# kube-proxy ConfigMap
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "nftables"
# nftables:
#   masqueradeAll: true
# 节点内核版本检查(建议 5.18+)
uname -r

# nftables 工具版本(建议 1.0.6+)
nft --version

# 查看 kube-proxy 写入的 nftables 规则
sudo nft list ruleset | grep -i kube

6.5 选型决策树

是否新集群 + 内核 ≥ 5.18?
├─ 是 → nftables 模式(官方推荐,未来方向)
└─ 否 →
   ├─ Service 数 < 1000 + 通用性要求高 → iptables 模式(默认,最稳)
   ├─ Service 数 > 5000 + 不能升内核 → IPVS 模式(性能兜底)
   └─ 已用 Cilium/eBPF → 直接用 Cilium 替代 kube-proxy(不在此文范围)

6.6 排查命令速查

# 1. 看 kube-proxy 模式
kubectl -n kube-system get cm kube-proxy -o yaml | grep mode

# 2. iptables 模式排查
sudo iptables-save | grep KUBE | wc -l           # 规则总数
sudo iptables -t nat -L KUBE-SERVICES -n | wc -l  # Service 入口链长度
sudo iptables -t nat -L KUBE-SVC-<HASH> -n -v     # 某 Service 后端

# 3. IPVS 模式排查
sudo ipvsadm -Ln                                   # 虚拟服务表
sudo ipvsadm -Ln --stats                           # 流量统计
sudo ipvsadm -Ln --rate                            # 实时速率

# 4. nftables 模式排查
sudo nft list ruleset                              # 完整规则集
sudo nft list sets                                 # 所有集合(ClusterIP 映射)

# 5. conntrack(三种模式通用)
cat /proc/sys/net/netfilter/nf_conntrack_count     # 当前连接数
cat /proc/sys/net/netfilter/nf_conntrack_max       # 上限
sudo conntrack -L | grep <PodIP>                   # 看某 Pod 的连接
sudo conntrack -L -p tcp --state ESTABLISHED | wc -l

# 6. 抓包确认 DNAT 是否生效
sudo tcpdump -i any -nn 'host <ClusterIP>'         # 看不到 ClusterIP 说明已 DNAT
sudo tcpdump -i any -nn 'host <PodIP>'             # 应能看到 DNAT 后的真实后端

08七、典型问题与排查

7.1 conntrack 表满

现象dmesg 出现 nf_conntrack: table full, dropping packet,业务间歇性超时。

根因nf_conntrack_count 达到 nf_conntrack_max,新连接被丢。常见于大集群、短连接多、或 tcp_timeout_established(默认 5 天)导致老条目不释放。

处理

# 临时调大
echo 1048576 | sudo tee /proc/sys/net/netfilter/nf_conntrack_max

# 永久生效
cat <<'EOF' | sudo tee -a /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_udp_timeout = 15
EOF
sudo sysctl --system

# k8s 节点可在 kube-proxy 启动参数或 init container 里设置
# --conntrack-max-per-core=0 (0 表示用内核默认), --conntrack-tcp-established-timeout=3600s

7.2 iptables 规则更新导致 kube-proxy CPU 飙高

现象:Service 频繁变更时,kube-proxy CPU 100%,iptables-restore 单次耗时数秒。

根因:iptables 模式下,任何 Service 变更都要全量重写整张 nat 表。规则数到 10 万级时,单次 restore 耗 CPU 显著。

处理

  • 短期:减少 Service 抖动(EndpointSlice 平滑、就绪探针合理)
  • 中期:切换到 IPVS 模式(增量更新)
  • 长期:升级内核,切到 nftables 模式

7.3 IPVS 模式下 kube-proxy 启动失败

现象:kube-proxy 日志报 ipvs module not loadedIPVS proxier failed to initialize

排查

# 检查内核模块
sudo lsmod | grep ip_vs
# 若为空,手动加载
sudo modprobe ip_vs ip_vs_rr nf_conntrack

# 检查内核是否编译了 IPVS
grep IP_VS /boot/config-$(uname -r)

# 检查 kube-proxy 日志
kubectl -n kube-system logs -l k8s-app=kube-proxy | grep -i ipvs

7.4 nftables 与 iptables 规则冲突

现象:升级到 nftables 模式后,部分流量不符合预期。

根因:同一 hook 上,iptables(-nft) 写的规则和 nftables 原生规则按 priority 共存。若两边都写了 NAT,可能互相覆盖。

排查

# 同时查看两套规则(iptables-nft 后端下,iptables 命令查的就是 nft 规则)
sudo iptables-save
sudo nft list ruleset

# 确认 iptables 后端
sudo update-alternatives --display iptables

09八、参考文档

本页目录