跳到主要内容

10.8 快速参考

这章的内容确实有点烧脑——XFRM 框架的状态机、策略与状态的纠缠、还有 NAT-T 那种「为了生存而不得不做的妥协」。

当你以后真正在内核代码里跟这些逻辑打交道时,你会发现手里如果能有一份 API 地图,心态会稳很多。这一节就是那张地图。

这里我不会把 API 照单全收地列一遍(你可以去内核头文件里找),而是把前面那些散落在代码路径里的关键函数、核心操作以及那些关键时刻会涨起来的计数器,整理成一张方便查阅的清单。如果你在调试 IPsec 连接时对着 /proc/net/snmp 发呆,或者在看 xfrm_input() 时想知道某个错误码是从哪飞出来的,这一节就是为你准备的。


核心方法速查

大部分时候,我们是在跟「匹配」、「查找」、「创建」和「销毁」这四类操作打交道。以下是那些你在阅读源码或调试时会反复遇到的核心函数。

1. 策略匹配与查找

一切的决策都始于流量匹配。内核需要知道当前这个包到底该不该被 IPsec 接管。

xfrm_selector_match()

bool xfrm_selector_match(const struct xfrm_selector *sel,
const struct flowi *fl,
unsigned short family);

这是最基础的匹配检查。它会问:这个特定的数据包流(flowi)是否命中了这条选择器(selector)? 根据协议族不同,它会调用底层的 __xfrm4_selector_match()(IPv4)或 __xfrm6_selector_match()(IPv6)。返回 true 就意味着「对上号了」。

xfrm_policy_match()

int xfrm_policy_match(const struct xfrm_policy *pol,
const struct flowi *fl,
u8 type,
u16 family,
int dir);

这一步比选择器匹配更进一步。它不仅看选没选中,还看这条策略(policy)能不能被应用在当前流上。 如果返回 0,说明策略允许应用;否则返回负的 errno。注意这里的 dir 参数,它指明了流量方向(入站/出站/转发),这很关键——同样的 IP 流,进和出的策略可能完全不同。


2. 策略的生命周期管理

策略一旦被配置下来,就要在内核里常驻,直到被手动删除。这里的引用计数是重中之重。

xfrm_policy_alloc()

struct xfrm_policy *xfrm_policy_alloc(struct net *net, gfp_t gfp);

分配并初始化一个 XFRM 策略。 除了分配内存,它还做了一系列初始化工作:

  • 把引用计数设为 1。
  • 初始化读写锁。
  • 关联到指定的网络命名空间(xp_net)。
  • 设置定时器回调为 xfrm_policy_timer()
  • 设置策略队列的定时器(policy->polq.hold_timer)回调为 xfrm_policy_process()

xfrm_pol_hold() / xfrm_pol_put()

void xfrm_pol_hold(struct xfrm_policy *policy);
static inline void xfrm_pol_put(struct xfrm_policy *policy);

标准的引用计数操作。

  • hold:引用计数加 1。表示「我正在用它,别删」。
  • put:引用计数减 1。当计数降为 0 时,它会调用 xfrm_policy_destroy() 彻底销毁对象。这是一种延迟销毁机制,确保正在使用该策略的代码路径不会突然访问到野指针。

xfrm_policy_destroy()

void xfrm_policy_destroy(struct xfrm_policy *policy);

这是真正的「终点」。它会移除策略关联的定时器,并释放策略占用的内存。通常只由 xfrm_pol_put() 在引用计数归零时调用。


3. 状态与数据库操作

有了策略只是有了「规矩」,还得有具体的「方案」(即 SA,State)。状态的管理直接关系到数据包能不能正确加密或解密。

xfrm_state_add() / xfrm_state_delete()

int xfrm_state_add(struct xfrm_state *x);
int xfrm_state_delete(struct xfrm_state *x);
  • add:将一个协商好的 SA(由 xfrm_state 描述)加入 SAD(安全关联数据库)。这通常发生在 IKE 协商完成后,由用户空间守护进程通过 Netlink 下发。
  • delete:从 SAD 中移除指定的 SA。

xfrm_state_alloc()

struct xfrm_state *xfrm_state_alloc(struct net *net);

分配一个新的 XFRM 状态对象。这是 SA 在内核诞生的第一步。

__xfrm_state_destroy()

void __xfrm_state_destroy(struct xfrm_state *x);

注意函数名前面的双下划线 __,这通常意味着它是内部实现。 它不直接释放内存,而是把状态对象加入到 XFRM 的垃圾回收列表(GC list),并激活垃圾回收器。这是一种常用的内核优化手段,避免在频繁分配释放的场景下产生性能抖动。

xfrm_state_walk()

int xfrm_state_walk(struct net *net,
struct xfrm_state_walk *walk,
int (*func)(struct xfrm_state *, int, void*),
void *data);

这是一个遍历器。它会遍历命名空间内所有的 XFRM 状态(net->xfrm.state_all),并对每一个状态调用你提供的 func 回调。如果你在写内核模块或者调试脚本需要 dump 所有 SA,你会用到它。


4. 数据包处理路径

这是数据流动的地方,也是前面所有的「配置」最终产生效果的地方。

xfrm_input()

int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type);

IPsec 接收路径的主入口。 当一个 ESP 或 AH 包到达时,协议处理器会调用它。它负责查 SAD、解密、验证完整性、检查重放攻击,最后把还原出来的干净 IP 包交回网络栈。如果这里报错,你的 SSH 连接就会断。

esp_input()

int esp_input(struct xfrm_state *x, struct sk_buff *skb);

IPv4 ESP 协议的具体处理器。xfrm_input() 在确定协议类型是 ESP 后,会把工作转交给它。

xfrm_lookup() / xfrm_bundle_create() 我们在发送路径一节花了很多篇幅讲这个。

  • xfrm_lookup():发送路径的核心。它根据策略查找对应的 SA,并决定这个包该走哪条路。
  • xfrm_bundle_create():当策略和状态都就绪后,这个函数负责创建 xfrm_dst(Bundle),把路由和 IPsec 处理逻辑绑定在一起,加速后续包的转发。

5. 异常处理与黑洞

并不是所有的查找都会成功,也不是所有的 SA 都能及时协商出来。

make_blackhole()

static struct dst_entry *make_blackhole(struct net *net,
u16 family,
struct dst_entry *dst_orig);

还记得 sysctl_larval_drop 吗? 当找不到状态且该参数开启时,内核不会让包傻等,而是创建一个「黑洞路由」。凡是发给黑洞路由的包,都会被静默丢弃。 这个函数就是造黑洞的。对于 IPv4,它调用 ipv4_blackhole_route()

xdst_queue_output()

int xdst_queue_output(struct sk_buff *skb);

如果内核选择「等待」而不是丢弃(即 sysctl_larval_drop=0),包就会被放入策略的等待队列(polq.hold_queue)。 这个函数负责把包塞进去。队列有长度限制(默认 100),满了就得丢。


错误计数器映射表:调试的罗盘

当你在排查为什么 VPN 跑不起来,或者为什么性能突然下降时,/proc/net/snmp 里的 XFRM 计数器是最好的线索。但那一串 XfrmInErrorXfrmInNoStates 到底对应内核里的哪段逻辑?

下表直接把内核符号(Linux Symbol)、SNMP 报错名以及触发它们的方法连了起来。

表 10-1:XFRM SNMP MIB 计数器映射

内核符号 (Linux Symbol)SNMP 名称可能触发的调用路径
LINUX_MIB_XFRMINERRORXfrmInErrorxfrm_input() — 接收路径的通用错误
LINUX_MIB_XFRMINBUFFERERRORXfrmInBufferErrorxfrm_input(), __xfrm_policy_check() — 数据包处理出错
LINUX_MIB_XFRMINHDRERRORXfrmInHdrErrorxfrm_input(), __xfrm_policy_check() — 头部解析失败
LINUX_MIB_XFRMINNOSTATESXfrmInNoStatesxfrm_input() — 收到包但找不到对应的 SA
LINUX_MIB_XFRMINSTATEPROTOERRORXfrmInStateProtoErrorxfrm_input() — 协议本身有误(如 ESP 格式错)
LINUX_MIB_XFRMINSTATEMODEERRORXfrmInStateModeErrorxfrm_input() — 模式不匹配(如隧道模式包进了传输模式 SA)
LINUX_MIB_XFRMINSTATESEQERRORXfrmInStateSeqErrorxfrm_input() — 序列号错乱(重放攻击检测失败)
LINUX_MIB_XFRMINSTATEEXPIREDXfrmInStateExpiredxfrm_input() — SA 已经过期了
LINUX_MIB_XFRMINSTATEMISMATCHXfrmInStateMismatchxfrm_input(), __xfrm_policy_check() — SA 和策略对不上
LINUX_MIB_XFRMINSTATEINVALIDXfrmInStateInvalidxfrm_input() — SA 无效
LINUX_MIB_XFRMINTMPLMISMATCHXfrmInTmplMismatch__xfrm_policy_check() — 策略模板不匹配
LINUX_MIB_XFRMINNOPOLSXfrmInNoPols__xfrm_policy_check() — 找不到匹配的策略
LINUX_MIB_XFRMINPOLBLOCKXfrmInPolBlock__xfrm_policy_check() — 策略明确阻止该流量
LINUX_MIB_XFRMINPOLERRORXfrmInPolError__xfrm_policy_check() — 策略处理出错
LINUX_MIB_XFRMOUTERRORXfrmOutErrorxfrm_output_one(), xfrm_output() — 发送路径通用错误
LINUX_MIB_XFRMOUTBUNDLEGENERRORXfrmOutBundleGenErrorxfrm_resolve_and_create_bundle() — 生成 Bundle 失败
LINUX_MIB_XFRMOUTBUNDLECHECKERRORXfrmOutBundleCheckErrorxfrm_resolve_and_create_bundle() — Bundle 校验失败
LINUX_MIB_XFRMOUTNOSTATESXfrmOutNoStatesxfrm_lookup() — 发送时找不到 SA
LINUX_MIB_XFRMOUTSTATEPROTOERRORXfrmOutStateProtoErrorxfrm_output_one() — 协议处理失败
LINUX_MIB_XFRMOUTSTATEMODEERRORXfrmOutStateModeErrorxfrm_output_one() — 模式错误
LINUX_MIB_XFRMOUTSTATESEQERRORXfrmOutStateSeqErrorxfrm_output_one() — 序列号问题
LINUX_MIB_XFRMOUTSTATEEXPIREDXfrmOutStateExpiredxfrm_output_one() — 发送时发现 SA 过期
LINUX_MIB_XFRMOUTPOLBLOCKXfrmOutPolBlockxfrm_lookup() — 策略禁止发送
LINUX_MIB_XFRMOUTPOLDEADXfrmOutPolDeadN/A — 策略已死锁
LINUX_MIB_XFRMOUTPOLERRORXfrmOutPolErrorxfrm_bundle_lookup(), xfrm_resolve_and_create_bundle() — 发送策略出错
LINUX_MIB_XFRMFWDHDRERRORXfrmFwdHdrError__xfrm_route_forward() — 转发头部错误
LINUX_MIB_XFRMOUTSTATEINVALIDXfrmOutStateInvalidxfrm_output_one() — SA 无效

补充资源:追踪源码

Linux 内核的网络子系统迭代很快,IPsec 作为核心安全组件更是如此。如果你想追踪最新的修复、补丁或者还没合并到主线的新特性,以下三个 Git 树是你需要关注的:

  1. IPsec git tree: git://git.kernel.org/pub/scm/linux/kernel/git/klassert/ipsec.git

    • 用途:针对 IPsec 网络子系统的修复补丁。
    • 基准:基于 David Miller 的 net 树开发。
  2. ipsec-next git tree: git://git.kernel.org/pub/scm/linux/kernel/git/klassert/ipsec-next.git

    • 用途:针对 IPsec 的新特性修改,目标是合并进 linux-next
    • 基准:基于 David Miller 的 net-next 树开发。
  3. 维护者

    • Steffen Klassert
    • Herbert Xu
    • David S. Miller

如果你想深入理解某个 XfrmIn...Error 为什么会涨,或者想看看 NAT-T 最近又修了什么奇怪的 Bug,去这几个 Git 树里翻 commit log 往往比看静态的源码要有用的多。


本章回响

IPsec 这章讲到这里,就画上句号了。

回想一下我们在本章开头那个关于「为什么不直接用 HTTPS」的疑问——答案现在应该很清晰了:IPsec 工作在网络层(L3),它保护的是 IP 包本身。这意味着应用层完全无感,你的 TCP、UDP 甚至 ICMP 协议都可以被透明地加密。这是 TLS/SSL 做不到的。

理解了 XFRM 框架,你其实就掌握了 Linux 网络栈中「策略与执行分离」这一设计的精髓。

  • 策略 是老板(xfrm_policy),它只定规矩,不干活。
  • 状态 是员工(xfrm_state),它拿着具体的密钥和算法去执行具体的加密解密任务。
  • Bundle 是老板和员工之间的契约,确保流量能顺畅地在路由和加密之间流转。

而在发送和接收路径上,我们看到的不仅仅是一个个函数调用,而是内核为了性能(查找缓存)和安全(重放窗口、序列号)所做的精妙平衡。甚至在面对 NAT 这种「破坏层」模型的历史遗留问题时,内核也展现了惊人的适应力——通过加一层 UDP 封装(NAT-T)让 IPsec 在充满路由器的现代互联网中活了下来。

下一章,我们将进入另一个同样重要且充满话题性的领域:网络过滤与防火墙(Netfilter)。如果说 IPsec 是给数据包穿上了防弹衣,那么 Netfilter 就是数据包进出这栋大楼的安检门。我们会看到内核是如何在每一个关键节点拦截流量,又是如何让 iptablesnftables 这些用户空间工具发号施令的。

带上你在本章学到的内核网络子系统基础知识,下一章会轻松很多。我们到时见。


练习题

练习 1:understanding

题目:在 Linux 内核的 IPsec 实现中,假设用户配置了一个安全策略(SPD条目),但由于密钥协商尚未完成,内核中还没有对应的 Security Association (SA)。此时内核收到匹配该策略的数据包,请问内核会怎么处理?这与 sysctl_larval_drop 参数有何关系?

答案与解析

答案:内核会创建一个临时的 Acquire State(SPI 为 0),并根据 sysctl_larval_drop 的值决定数据包的命运:若为 1(默认),数据包会被丢弃(黑洞);若为 0,数据包会被加入策略队列等待 SA 协商完成(最多 100 个包)。

解析:当流量匹配策略但 SA 尚未建立时,内核会通过 Acquire State 来记录这一状态并触发 IKE 守护进程进行协商。为了防止在协商期间数据包丢失或无限期等待,内核提供了 sysctl_larval_drop 开关。如果设为 1,表示丢弃数据包(Make Blackhole);如果设为 0,内核会利用 xfrm_policy 结构中的 polq 队列缓存这些数据包,直到 SA 建立完成(Larval state 变为 Mature state)。这涉及 XFRM 策略与状态的同步问题。

练习 2:application

题目:某公司的网络架构中,移动办公员工通过 IPsec VPN 连接到公司内网。员工的公网 IP 地址经常变化,且位于不同的 NAT 设备之后。为了保证连接稳定性,IKEv2 协商使用了 NAT-T(NAT Traversal)技术。请问在 Linux 内核的 IPsec 接收路径中,经过 UDP 封装的 ESP 数据包是如何被处理的?请描述从网卡驱动到 XFRM 框架的解封装流程。

答案与解析

答案:UDP 数据包到达后,内核协议栈首先将其传递给 UDP 层处理。由于使用了 UDP 封装(端口 4500),UDP socket 层会将其转交给 IPsec 的 UDP 封装接收函数 xfrm4_udp_encap_rcv()。该函数会剥离 UDP 头部,取出内部的 ESP 数据包,并将其作为标准 ESP 包重新注入 IP 协议栈,最终调用 xfrm4_rcv() 进入通用的 xfrm_input() 流程进行解密和验证。

解析:这是一个典型的 NAT-T 应用场景。NAT 设备只能正确转换 UDP/TCP 头部的 IP 地址和端口,无法处理纯粹的 ESP 协议(协议号 50)。因此,IPsec 将 ESP 包封装在 UDP(端口 4500)中。在内核接收路径中,xfrm4_udp_encap_rcv() 函数扮演了“去封装器”的角色,它识别出这是一个 NAT-T 包,去掉 UDP 头,还原出原始 ESP 包,再交给 XFRM 框架处理。这使得 IPsec 流量可以无缝穿越 NAT 设备。

练习 3:thinking

题目:在分析网络性能时,你发现一台配置了 AES-GCM 加密(支持 Intel AES-NI 硬件加速)的 Linux VPN 网关,其 IPsec 吞吐量并未达到硬件的线速理论值。通过 perf 工具分析,发现 CPU 时间主要消耗在 spinlock 的自旋等待上,尤其是在多核并发处理大量短连接场景下。请结合 XFRM 框架的数据结构,分析可能的瓶颈来源,并提出一种优化思路。

答案与解析

答案:瓶颈很可能出在 XFRM 策略数据库(SPD)和状态数据库(SAD)的查找锁竞争上。在多核并发处理大量流时,不同 CPU 核心试图同时查找或更新哈希表,导致 xfrm_policyxfrm_state 的自旋锁频繁争用。

优化思路:利用内核提供的异步加密接口(如 pcrypt 模板)并行化加密操作以减少处理时间,或者升级内核版本以支持 RCU(Read-Copy-Update)机制来优化 XFRM 策略的读取路径,从而减少锁的持有时间。

解析:XFRM 框架高度依赖哈希表来存储策略(xfrm_policy)和状态(xfrm_state)。虽然查找是通过哈希进行的,但在高并发短连接场景下,锁竞争会非常严重。此外,虽然 AES-NI 加速了计算,但数据包的路径 traversal(策略查找、路由查找、Bundle 构建)涉及大量共享数据的访问。

题目中的关键点在于“大量短连接”,这会导致频繁的 SA 查找和策略匹配。如果内核使用粗粒度锁或热点锁,性能就会下降。解决方案可以从两方面入手:一是利用并发编程技术(如 RCU)优化数据结构读取;二是利用软件技术(如 pcrypt)将加密请求分发到不同 CPU 核,或者利用硬件加速的异步特性,避免 CPU 在加密/解密等待上浪费算力。


要点提炼

IPsec 旨在不可信的网络上建立可信通道,其核心机制并非净化网络,而是通过 ESP(封装安全载荷)协议将数据加密封装。ESP 协议支持传输模式(仅加密载荷)和隧道模式(加密整个原 IP 包),后者常用于 VPN 以隐藏私有地址通信;配合 AES-GCM 等 AEAD 算法,它能利用 CPU 硬件指令集同时完成加密与认证,实现高性能的数据保护。

密钥协商与参数配置在用户空间由 IKE(Internet Key Exchange)守护进程(如 strongSwan)负责,它是连接用户配置与内核执行的桥梁。IKEv2 相比老旧的 v1 版本显著简化了握手流程(从 9 条消息减至 4 条),并原生支持 NAT 穿透和 EAP 认证,协商出的密钥最终通过 Netlink 接口写入内核。

内核中的 XFRM 框架是 IPsec 的实际执行者,它通过维护两个核心数据库来工作:SPD(安全策略数据库)根据流量特征(地址、端口等)决定哪些包需要处理;SAD(安全关联数据库)则存储具体的加密上下文(SA),由 SPI、目的地址和协议号唯一标识。策略与状态分离的设计允许灵活定义复杂的加密规则。

在发送路径上,xfrm_lookup() 函数利用流缓存机制(Bundle)优化性能,将策略查找、路由选择和 SA 关联打包复用,避免每个数据包都重复查表。接收路径则通过 xfrm_input() 驱动,系统依据 SPI 定位 SA,解密剥离 ESP 头尾后,修改 IP 协议头并重新将包注入协议栈,使其对上层应用透明。

IPsec 与 NAT 设备存在天然冲突,因为 NAT 无法修改被 ESP 加密的传输层校验和,这会导致连接中断。解决方法是 NAT-T(NAT 穿透),它将 ESP 包封装在 UDP 数据报中,使 NAT 设备能像处理普通 UDP 流量一样处理 IPsec 数据包,从而绕过限制,保证在 NAT 环境下的连通性。