Cilium 深度参考:Linux XFRM 子系统详解——策略、状态、报文流转、ip xfrm 输出与性能结构
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
本篇技术指南以 Cilium 仓库中的 XFRM Reference Guide 为核心,系统讲解 Linux XFRM(IP packet transformation)子系统的两大配置对象——XFRM 策略(Policy)与状态(State)、出站/入站报文在 Netfilter 栈中的 XFRM 加解密流转路径、ip xfrm输出的逐字段解读、全部 XFRM 错误计数器的语义,以及内核中策略与状态的底层查找数据结构。读完后你将能够:独立读懂ip xfrm policy/ip xfrm state的任意输出、诊断 IPsec 解密失败的各类错误计数,并理解 Cilium 如何利用 packet mark 在 tc-bpf 钩子中标记并重路由 IPsec 流量(对应0xd00/0xf00、0xcb93eXX等文档示例中出现的 mark)。
XFRM 是什么:Linux IPsec 加解密的底层框架
Linux 内核中的 IPsec 加密依赖 XFRM 子系统。XFRM(IP Transforms)是一个用于报文变换的 IP 框架,覆盖从加密到压缩的一系列操作,它通过一组policy(策略)和state(状态)对象进行配置——在 IPsec 语境下,它们分别对应安全策略(Security Policy)与安全关联(Security Association, SA)。
需要强调的是,该参考指南面向希望深入理解 Linux XFRM 子系统的开发者和用户。阅读它有助于拓宽对 Cilium 的理解,但并非使用 Cilium 的前提条件。在 Cilium 的 Linux 数据面实现中,XFRM 正是节点间 IPsec 加密的承载机制,其抽象封装在pkg/datapath/linux/ipsec包中(包注释 明确写道:该包“提供 Linux 数据面特定的抽象和有用助手,以通过 Linux xfrm 管理 IPSec”)。
XFRM 策略与状态(Policies and States)
从宏观上看,策略定义接受哪些流量、拒绝哪些流量,而状态定义如何执行加解密。策略可以按方向(out、in或fwd)、带 CIDR 的源/目的 IP 地址以及 packet mark 进行匹配。例如,下面这条策略匹配出站报文中源地址任意、目的地址为 10.56.1.0/24、packet mark 为0xcb93eXX的流量,并且默认动作是放行:
src 0.0.0.0/0 dst 10.56.1.0/24 dir out priority 0 mark 0xcb93e00/0xffffff00 [...]注意这里mark 0xcb93e00/0xffffff00正是“值/掩码”的匹配形式:掩码0xffffff00意味着只看 mark 的高 24 位。这类 mark 值与 Cilium 源码中的常量一一对应——pkg/datapath/linux/linux_defaults/linux_defaults.go定义了RouteMarkMask = 0xF00(路由 mark 的位掩码)、OutputMarkMask = 0xFFFFFF00(XFRM 状态 output-mark 使用的掩码),以及IPsecMarkMaskNodeID = 0xFFFF0000(node ID 占高 16 位)。Cilium 通过 ipsec_linux.go 中的generateEncryptMark()/generateDecryptMark()生成这些 mark:加密方向 mark 由RouteMarkEncrypt置位并编码 SPI 与远端 node ID,解密方向则由RouteMarkDecrypt置位。
状态(State)与策略较为相似,但与流量方向无关,且只能按精确 IP 地址(或 0.0.0.0 匹配所有)匹配。下面这条状态作用于 10.56.0.17 → 10.56.1.238 的报文,packet mark 与上文相同:
src 10.56.0.17 dst 10.56.1.238 proto esp spi 0x00000003 reqid 1 mode tunnel replay-window 0 mark 0xcb93e00/0xffffff00 output-mark 0xe00/0xffffff00 aead rfc4106(gcm(aes)) 0x6254fced5f7a5ea9401b9015ecf10d65eac51a69 128 anti-replay context: seq 0x0, oseq 0x36, bitmap 0x00000000 sel src 0.0.0.0/0 dst 0.0.0.0/0在隧道模式(tunnel mode)IPsec 中,这些 IP 地址对应外层IP 地址;对于入站加密报文,SPI 也会参与匹配(见下文)。
你可能会注意到,这条状态并没有指明它应执行加密还是解密——因为它两者都能做。状态与流量方向无关,所以同一状态在理论上可同时用于加密和解密;具体做什么取决于该状态在协议栈的哪个位置被匹配到(例如在入站路径匹配到时就是解密)。在 Cilium 的实现中,getDirFromXfrmMark() 正是通过检查 mark 中的RouteMarkDecrypt/RouteMarkEncrypt位来判定一条 XFRM 状态属于入站还是出站方向——因为状态本身没有方向字段。
策略模板(Policy Templates)
XFRM 策略通常还定义一个模板(template):
src 0.0.0.0/0 dst 10.56.1.0/24 dir out priority 0 mark 0xcb93e00/0xffffff00 tmpl src 10.56.0.17 dst 10.56.1.238 proto esp spi 0x00000003 reqid 1 mode tunnel模板的用法取决于方向:
- 出站流量:模板定义了要执行的编码方式。例如上述模板会把报文封装 IP 头 + ESP 头;IP 头的源/目的地址分别为 10.56.0.17 与 10.56.1.238,ESP 头的 SPI 为 3。
- 入站与转发流量:模板充当额外的过滤器。例如下面这条策略只有在报文是外层 IP 地址为 10.56.1.238 ↔ 10.56.0.17 的 ESP 报文、且 packet mark 匹配
0xd00/0xf00时才放行:
src 0.0.0.0/0 dst 10.56.0.0/24 dir in priority 0 mark 0xd00/0xf00 tmpl src 10.56.1.238 dst 10.56.0.17 proto esp reqid 1 mode tunnel注意0xd00/0xf00与 Cilium 源码中RouteMarkDecrypt(配合RouteMarkMask = 0xF00)的取值直接对应——这正是 Cilium 为解密流量打的特殊标记。
在隧道模式下,应当总是能看到与 OUT 策略模板相匹配的 XFRM 状态。因为在出站方向上,状态是在模板应用之后才被匹配的:IP 地址、SPI、协议、模式和 reqid 在 XFRM 状态与其对应模板之间必须全部一致。Cilium 在构造 OUT 状态时同样保证了这一点,ipSecReplaceStateOut() 将state.Src/Dst设为隧道 IP、state.Reqid = params.ReqID,并把 mark 设置为generateEncryptMark(key.Spi, params.RemoteNodeID)——即把 SPI(右移 12 位,见IPsecXFRMMarkSPIShift = 12)与远端 node ID(左移 16 位)编码进 mark。
XFRM 报文流转路径(Packet Flows)
下图展示了带 XFRM 元素(紫色)的标准 Netfilter 报文流转:
出站报文流(Egress)
出站时,报文首先命中 “XFRM OUT policy” 块,此时对 XFRM OUT 策略做查找。若匹配成功,报文进入 “XFRM encode” 块:应用模板(例如封装),随后再与 XFRM 状态匹配;若找到状态,就用其信息加密报文。加密后的报文会再次经过 OUTPUT 与 POSTROUTING 链。
入站报文流(Ingress)
入站时,加密报文(例如 ESP 报文)在穿过 INPUT 链之后到达 “XFRM decode”。
隧道模式的加密报文通常以本机 IP 之一为外层目的地址,因此会被自动路由到 INPUT 链;否则可能需要添加 IP 路由将报文引向 INPUT 链。Cilium 的做法是:在 tc-bpf ingress 钩子中识别 IPsec 流量并打上特殊 mark,再用该 mark 将报文重路由到 INPUT 链——这与入站策略示例中mark 0xd00/0xf00的匹配值相吻合。
在 “XFRM decode” 处,若报文匹配某个 XFRM 状态,则用状态信息对其进行解码(即解封装 + 解密)。匹配依据为源/目的地址、mark、SPI 与协议。若解码出错(例如密钥不对),报文被丢弃且对应错误计数器递增。
如图所示,解码本身并不要求报文匹配某条 XFRM 策略(它直接进入 “XFRM decode”),但报文要进入本地进程或穿过 FORWARD 链则必须有策略放行。一条带有可选模板(即level use)的 XFRM 策略会放行所有解码成功的报文;从未加密、因而不来自 “XFRM decode” 的流量则默认放行。
报文解码后会在协议栈中重新环回(recirculate),表现得像是从最初接收它的接口再次进来。更具体地说,报文在 tc 层之前被重新环回,因此在 tc-bpf 钩子上会第二次可见(解密前一次、解密后一次)。重新环回时 packet mark 被保留,因此可以识别并追踪已经过解密和环回的报文。
逐字段解读ip xfrm输出
以下输出基于 iproute2-6.1.0,更新版本可能出现更多字段。例如新内核(v6.10+)中的 XFRM 状态带有dir字段,未来很可能出现在ip xfrm state输出中。
在ip xfrm输出中,策略按创建日期排序,新策略位于顶部。这一点很重要:当两条策略都能匹配同一报文且优先级相同时,较新的那条会被使用。
ip xfrm policy输出解读
$ ip xfrm policy # - `src 0.0.0.0/0` 为匹配源 IP 地址的 CIDR # - `dst 0.0.0.0/0` 为匹配目的 IP 地址的 CIDR src 0.0.0.0/0 dst 0.0.0.0/0 uid 0 # - `dir fwd` 指明方向,定义策略在 Linux 栈中的生效位置:入站、出站或转发 # - `action allow` 为匹配报文执行的动作,报文只能被放行(默认)或丢弃 # - `index 18` 用于区分可能具有相同或重叠选择器的不同策略。若未给出或已存在, # 则自动(重新)生成(参见内核 `xfrm_gen_index`)。三个 LSB 编码方向 # (例如 1 表示 `XFRM_POLICY_OUT`),MSB 部分每次递增 1(即 index 加 8)直到找到空 index # - `priority 2975` 表示多条策略可能匹配该报文时的优先级,0 为最高 # - `share any` 始终为 `any`,目前未使用 # - `flag (0x00000000)` 为 XFRM 策略标志集合。目前仅支持 # `XFRM_POLICY_ICMP` (0x2);`XFRM_POLICY_LOCALOK` (0x1) 未实现。 # 给出 `XFRM_POLICY_ICMP` 时,策略也适用于 payload 内报文匹配选择器的 ICMP 报文 dir fwd action allow index 18 priority 2975 share any flag (0x00000000) lifetime config: # 基于接收字节数、报文数、策略添加后的时间或最后一次被匹配后的时间的各种限制与过期时间。 # 达到软限制或软过期时间时,通过 netlink(`struct xfrm_user_expire`)通知用户态; # 达到硬限制或硬过期时间时,策略被删除 limit: soft (INF)(bytes), hard (INF)(bytes) limit: soft (INF)(packets), hard (INF)(packets) expire add: soft 0(sec), hard 0(sec) expire use: soft 0(sec), hard 0(sec) lifetime current: # 被该策略匹配的字节数与报文数计数器(若设置了限制则用于计量) 0(bytes), 0(packets) # 策略添加时间与最后一次被匹配的时间戳(若设置了过期时间则用于计时) add 2024-06-17 11:24:49 use 2024-06-17 11:25:01 # - `src 0.0.0.0` / `dst 10.92.0.164` 用法见“策略模板”一节 tmpl src 0.0.0.0 dst 10.92.0.164 # - `proto esp`、`spi 0x00000000(0)`、`reqid 1(0x00000001)`、`mode tunnel` # 用法均见“策略模板”一节 proto esp spi 0x00000000(0) reqid 1(0x00000001) mode tunnel # - `level use` 是表示该模板为“可选”的晦涩写法,另一种取值是 `level required`。 # 若找不到匹配模板的 XFRM 状态,可选模板会被跳过;否则报文以 # `XfrmInTmplMismatch` 丢弃 # - `share any` 未实现,始终为 `any` level use share any # - `enc-mask ffffffff` 位掩码,定义允许的加密算法列表 # - `auth-mask ffffffff` 位掩码,定义允许的认证算法列表 # - `comp-mask ffffffff` 未实现的位掩码(可能为压缩算法定义) enc-mask ffffffff auth-mask ffffffff comp-mask ffffffffip xfrm state输出解读
$ ip xfrm state # - `src 10.92.1.189` 为匹配报文源 IP 的地址;`dst 10.92.0.164` 为目的 IP src 10.92.1.189 dst 10.92.0.164 # - `proto esp` 指明使用的 IPsec 协议 # - `spi 0x00000000(0)` 为安全参数索引(SPI),用于区分可能使用不同 # 算法/密钥的多条 IPsec 流,密钥轮换时尤其有用 # - `reqid 1(0x00000001)` 仅用于确保 XFRM 策略模板与状态相互匹配的内核 ID # - `mode tunnel` 指明报文被封装(tunnel)还是仅添加 ESP 头(transport) proto esp spi 0x00000003(3) reqid 1(0x00000001) mode tunnel # - `replay-window 0` 为防重放检查(容忍度)的窗口大小 # - `seq 0x00000000`;`flag (0x00000000)` 含多种标志,包括 ESN 模式的 # `XFRM_STATE_ESN` (0x80) replay-window 0 seq 0x00000000 flag (0x00000000) # - `mark 0x4db50d00/0xffff0f00` 用于匹配报文 mark 的值与掩码 # - `output-mark 0xd00/0xffffff00` 为加解密之后施加到报文 mark 上的值与掩码 mark 0x4db50d00/0xffff0f00 output-mark 0xd00/0xffffff00 # - `aead rfc4106(gcm(aes))` 为所用算法的类型与名称 # - `0x856f... (160 bits)` 为密钥及其长度——这是敏感信息,需妥善对待 # - `128` 为 ICV 长度,支持的长度取决于所用算法 aead rfc4106(gcm(aes)) 0x856f15d0ccabe682286b4286bccf5d595b88b168 (160 bits) 128 # - `seq 0x0` 为接收侧当前序列号,用于防重放 # - `oseq 0x0` 为最近发出的序列号;若该 32 位数溢出,报文丢弃且 # `XfrmOutStateSeqError` 计数递增;ESN 模式下序列号编码为 64 位 # - `bitmap 0x00000000` 跟踪重放窗口中已经出现过的序列号 anti-replay context: seq 0x0, oseq 0x0, bitmap 0x00000000 # - `sel src 0.0.0.0/0 dst 0.0.0.0/0` 是施加于解密后报文的附加过滤器, # 确保内层报文来自/去往预期位置 # - `uid 0` 此字段似乎未被使用(`struct xfrm_selector` 中的 `user`) sel src 0.0.0.0/0 dst 0.0.0.0/0 uid 0 lifetime config: # 与策略相同:软/硬限制与过期时间;软限制触发 netlink 通知, # 硬限制触发状态删除 limit: soft (INF)(bytes), hard (INF)(bytes) limit: soft (INF)(packets), hard (INF)(packets) expire add: soft 0(sec), hard 0(sec) expire use: soft 0(sec), hard 0(sec) lifetime current: 20124(bytes), 83(packets) add 2024-06-17 11:15:48 use 2024-06-17 11:16:02 stats: # - `replay-window 0` 在收到序列号超出窗口范围的报文时递增 # - `replay 0` 在收到序列号在窗口内但已出现过的报文时递增 # - `failed 0`(内核侧全名 `integrity_failed`)在认证/加密头校验和错误时 # 递增;该计数器递增时 `XfrmInStateProtoError` 也必然递增 replay-window 0 replay 0 failed 0值得对照的是,output-mark 0xd00/0xffffff00中的值与掩码正与 Cilium 的RouteMarkDecrypt常量配合OutputMarkMask = 0xFFFFFF00的用法一致:报文完成解密后,XFRM 内核会把该 mark 写回报文,供 tc-bpf 钩子在第二次可见时识别“这是已解密的报文”。
XFRM 错误计数器全景
所有 XFRM 错误都对应一次报文丢弃,部分错误还伴随特定状态的计数器递增。要在/proc/net/xfrm_stat中看到这些错误计数器,需要开启CONFIG_XFRM_STATISTICS内核配置。
| 错误计数器 | 触发条件 |
|---|---|
XfrmInError | 加密期间内核内存分配失败 |
XfrmInBufferError | 报文穿过过多 XFRM 状态(上限XFRM_MAX_DEPTH= 6);或过多 XFRM 策略模板适用于一个报文(上限同为 6) |
XfrmInHdrError | 报文中 SPI 部分畸形;或外层 IP 头畸形 |
XfrmInNoStates | 到达 INPUT 链的 AH/ESP 报文未找到任何匹配的 XFRM IN 状态 |
XfrmInStateProtoError | AH/ESP 校验和错误;报文 IPsec 协议与状态指定协议不一致;以及所有协议特定错误(例如来自esp_input):加解密失败(如 IN 状态密钥与加密密钥不匹配)、协议头/尾畸形、内存不足 |
XfrmInStateModeError | 报文为隧道模式,但匹配到的 XFRM 状态是传输模式 |
XfrmInStateSeqError | 防重放检查拒绝报文。序列号超出窗口时递增该状态的replay-window计数;序列号已出现过时递增replay计数 |
XfrmInStateExpired | 状态过期(硬限制)到实际删除之间存在延迟,该窗口内入站匹配报文被丢弃 |
XfrmInStateMismatch | 状态封装协议(如ip xfrm state中encap字段的espinudp)与报文封装协议不匹配;或解密后报文不匹配所用状态的sel选择器 |
XfrmInStateInvalid | 报文匹配到正在删除或已过期(expired)的 XFRM 状态 |
XfrmInTmplMismatch | 报文匹配了带“非可选”模板的 XFRM 策略,但模板与用于解密的状态都不匹配(一个报文可以被多次解码);或报文上使用了mode tunnel状态,但它不匹配任何策略模板 |
XfrmInNoPols | 入站报文不匹配任何 XFRM 策略且默认动作为block(用ip xfrm policy {get,set}default查看/设置默认动作) |
XfrmInPolBlock | 报文匹配到action block的 XFRM IN 策略 |
XfrmOutError | 加密期间内存分配失败;某些情况下待加密报文畸形 |
XfrmOutBundleCheckError | 未使用 |
XfrmOutNoStates | 报文匹配了 XFRM OUT 策略,但找不到匹配该策略模板的状态 |
XfrmOutStateProtoError | 发生协议特定(如 ESP)加密错误 |
XfrmOutStateModeError | 封装后报文超过 MTU 且不允许分片 |
XfrmOutStateSeqError | 状态的输出序列号(oseq)达到最大值——非 ESN 模式下为UINT32_MAX |
XfrmOutStateExpired | 与XfrmInStateExpired对称,出站方向的过期未删除状态 |
XfrmOutPolBlock | 报文匹配到action block的 XFRM OUT 策略 |
XfrmOutPolDead | 未使用;对删除中的状态改为报告XfrmOutStateInvalid |
XfrmOutPolError | 过多策略模板适用于一个报文(上限XFRM_MAX_DEPTH= 6);或匹配策略的非可选模板找不到任何状态 |
XfrmFwdHdrError | 报文在 FWD 策略检查时畸形 |
XfrmOutStateInvalid | 出站报文匹配到正在删除或已过期的状态 |
XfrmOutStateDirError | 查找到的状态方向已定义且不是XFRM_SA_DIR_OUT(仅 v6.10+ 内核) |
XfrmInStateDirError | 查找到的状态方向已定义且不是XFRM_SA_DIR_IN(仅 v6.10+ 内核) |
排查 Cilium IPsec 问题时,可结合上述表格定位:XfrmInNoStates通常意味着节点间状态尚未同步完成;failed计数上升则指向密钥不一致(例如密钥轮换后一侧未更新)。Cilium 侧的对应机制可在 xfrm_state_cache.go(XFRM 状态缓存,用于跟踪远端节点重启等场景下的状态一致性)与 xfrm_collector.go(XFRM 数据采集,为cilium-dbg bpf等调试命令提供状态视图)中找到线索。
性能考量:策略与状态的查找数据结构
当你拥有成千上万条策略与状态时,查找成本即使相比加解密本身也不可忽略。了解 XFRM 存放策略与状态的数据结构,有助于理解何时以及如何改进索引、加速查找。
XFRM 策略的数据结构
XFRM 策略存储在一个由多棵红黑树与多个哈希表组成的复杂结构中。根层是一个可伸缩哈希表(resizable hashtable),索引键为网络命名空间、IP 族、方向以及接口(若使用 XFRM 接口)。每个哈希表项内部包含若干红黑树,策略就存放在这些树中;这些项由结构体xfrm_pol_inexact_bin表示。
拿到xfrm_pol_inexact_bin(依据当前 IP 族、命名空间与方向)后,其每棵红黑树都用源/目的 IP 进行查找:root_s树按源 IP 排序;root_d树按目的 IP 排序。此外root_d的叶节点还挂有一棵按源 IP 排序的子树。这使得对root_s与root_d的查找能从叶节点返回三组候选(src_ip; dst_ip)策略列表:
- 来自
root_s的(src_ip; any)候选列表; - 来自
root_d的(any; dst_ip)候选列表; - 来自
root_d叶节点子树的(src_ip; dst_ip)候选列表。
再补上直接存放在xfrm_pol_inexact_bin项中的(any; any)候选列表,共四组候选。注意:一条 XFRM 策略只根据其源/目的 CIDR 存在于其中一组列表里。
随后内核线性遍历这四组列表,寻找优先级最高(priority数值最小)且匹配报文的候选。两条策略同时匹配且优先级相同时,较新者优先。mark 的比对也只发生在这段线性候选求值过程中。
XFRM 状态的数据结构
XFRM 状态组织在四个哈希表中,索引字段与用途各不相同:
net->xfrm.state_bydst:按源/目的 IP 地址及 reqid 索引;net->xfrm.state_bysrc:仅按源/目的 IP 地址索引;net->xfrm.state_byspi:按目的 IP、SPI 与协议索引;net->xfrm.state_byseq:仅按序列号索引。
入站报文查找状态时使用state_byspi——这是合理的,因为 RFC4301 第 4.1 节建议每个 XFRM 状态拥有独立的 SPI,且加密报文本身就携带 SPI。
出站加密前查找策略模板对应的状态时,使用state_bydst——因为策略模板提供的恰好就是这套索引信息。遍历/清空全部状态(如 flush)时通常也走这张表,但理论上用任意一张表都能完成。
state_bysrc与state_byseq服务于其他管理任务,例如查找待更新的状态、响应用户 netlink 查询、或在添加新状态前检查是否已存在。
小结
本指南围绕 Cilium 仓库的 XFRM Reference Guide,完整覆盖了:XFRM 策略/状态/模板三者的语义分工(策略管放行、状态管加解密、模板出站编码入站过滤)、加解密报文在 Netfilter 栈中的具体路径与 tc-bpf 二次可见的环回机制、ip xfrm policy/ip xfrm state输出的逐字段语义、全部 25 个 XFRM 错误计数器的诊断含义,以及内核侧策略(红黑树 + 可伸缩哈希表)与状态(四张哈希表)的查找结构。在 Cilium 的实际部署中,这些机制与pkg/datapath/linux/ipsec(状态构建、mark 编码、远端重启恢复)及pkg/datapath/linux中的路由 mark 规则(RouteMarkDecrypt重路由)共同构成了节点间 IPsec 加密的数据面基础。
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考