10.3 The XFRM Framework
上一节我们聊了 IPsec 用到的那些加密算法,看到了内核是如何通过 Crypto API 和 pcrypt 来榨干 CPU 性能的。算法固然是肌肉,但光有肌肉没法干活——你还需要骨架和神经系统来把这些算法组织起来,告诉内核什么时候该加密、该用哪个密钥、以及把数据包发到哪里去。
这个骨架,就是 XFRM 框架。
说实话,"XFRM" 这个名字起得挺技术的,读作 "transform"(变换)。如果你看到目录里写着 "The XFRM Framework",别被这个缩写吓到了。它只是内核里的一套机制,专门负责把进进出出的数据包按照你定好的规则进行「变身」——加密、解密、封装、解封装。它最初源于 USAGI 项目,那是个早在 20 多年前就致力于把 IPv6 和 IPsec 搞进 Linux 内核的先锋项目。
协议无关的“变形金刚”
XFRM 框架有一个设计哲学:跟协议族解耦。
这意味着什么?意味着不管是 IPv4 还是 IPv6,不管是加密还是认证,大部分通用逻辑(比如策略管理、状态维护、垃圾回收)都是复用的。这部分通用代码躺在 net/xfrm 目录下。而具体的协议细节,比如 IPv4 的 ESP 实现放在 net/ipv4/esp4.c,IPv6 的放在 net/ipv6/esp6.c,再配上各自的策略模块(如 xfrm4_policy.c)。
这种分层设计很聪明,但也意味着你在源码里跳转时会比较累——通用逻辑和具体协议的实现是分开的。
命名空间里的一个小世界
现在稍微停一下,想一想网络命名空间。
如果你在容器里跑 IPsec,你肯定不希望容器 A 的 VPN 策略跑到容器 B 的碗里去。XFRM 框架从设计之初就支持网络命名空间。每个网络命名空间(struct net)里都有一个独立的 xfrm 成员,类型是 netns_xfrm。
你可以把这个结构体看作是 XFRM 框架在这个命名空间里的“控制室”。打开 include/net/netns/xfrm.h,你会看到:
struct netns_xfrm {
struct hlist_head *state_bydst;
struct hlist_head *state_bysrc;
struct hlist_head *state_byspi;
. . .
unsigned int state_num;
. . .
struct work_struct state_gc_work;
. . .
u32 sysctl_aevent_etime;
u32 sysctl_aevent_rseqth;
int sysctl_larval_drop;
u32 sysctl_acq_expires;
};
看到这堆哈希表头指针了吗?
state_bydst、state_bysrc、state_byspi——这是后面我们要找的“安全关联”(SA)的索引库。还有那个 state_gc_work,这是负责清理过期状态的垃圾回收工。至于 sysctl_larval_drop,这个参数非常重要,我们后面聊“丢包还是等待”的时候会碰到它。
初始化与 Netlink 通道
XFRM 框架是怎么启动的?
在 IPv4 初始化路由子系统(ip_rt_init)的时候,顺带就把 XFRM 初始化了(调用 xfrm_init() 和 xfrm4_init())。IPv6 同理。这没什么花头,属于基础设施的“通水通电”。
但 XFRM 框架自己不会凭空产生密钥和策略——它得听用户的。用户空间的守护进程(比如 StrongSwan 或 iproute2 工具)如何跟内核对话?通过 Netlink。
具体的协议类型是 NETLINK_XFRM。内核在启动时会创建一个对应的 Netlink 内核 socket:
static int __net_init xfrm_user_net_init(struct net *net)
{
struct sock *nlsk;
struct netlink_kernel_cfg cfg = {
.groups = XFRMNLGRP_MAX,
.input = xfrm_netlink_rcv,
};
nlsk = netlink_kernel_create(net, NETLINK_XFRM, &cfg);
. . .
return 0;
}
看那个 cfg.input = xfrm_netlink_rcv。当你在命令行敲下 ip xfrm state add ... 时,这条命令会变成一个 Netlink 消息(比如 XFRM_MSG_NEWSA),飞进内核,被 xfrm_netlink_rcv() 接住,然后分发给 xfrm_user_rcv_msg() 去处理。
这就是用户空间控制内核的把手。
好了,现在“舞台”搭好了,灯光(Netlink)也亮了。接下来我们要请出这出戏的两个主角:策略 和 状态。
XFRM Policies(策略)
先问一个问题:当内核收到一个数据包,或者要发送一个数据包时,它怎么知道要不要对这个包进行 IPsec 处理?
它不会瞎猜。它查表。这个表就是 SPD(Security Policy Database,安全策略数据库)。
在内核里,一条策略用 struct xfrm_policy 表示。你可以把它想象成一个“过滤器”,或者一个“交警”——它拦住车流(数据包),检查它的证件,决定是放行、拦截、还是把它送去加密车间。
这个交警怎么识别目标车辆?通过 选择器。
选择器:精准匹配
每个策略都有一个 xfrm_selector 结构体,这是策略的眼睛:
struct xfrm_selector {
xfrm_address_t daddr;
xfrm_address_t saddr;
__be16 dport;
__be16 dport_mask;
__be16 sport;
__be16 sport_mask;
__u16 family;
__u8 prefixlen_d;
__u8 prefixlen_s;
__u8 proto;
int ifindex;
__kernel_uid32_t user;
};
这里什么都有:源地址、目的地址、源端口、目的端口、上层协议(TCP/UDP/ICMP)。甚至还有网络接口索引和用户 ID。
内核通过 xfrm_selector_match() 方法来把一个数据包的流信息跟这个选择器比对。只有匹配上了,这条策略才会生效。
策略结构体解析
现在我们来看看 struct xfrm_policy 本身。这是一个大结构体,我们只挑那些关系到“能不能通”和“会不会炸”的字段来看:
struct xfrm_policy {
. . .
struct hlist_node bydst;
struct hlist_node byidx;
/* This lock only affects elements except for entry. */
rwlock_t lock;
atomic_t refcnt;
struct timer_list timer;
struct flow_cache_object flo;
atomic_t genid;
u32 priority;
u32 index;
struct xfrm_mark mark;
struct xfrm_selector selector;
struct xfrm_lifetime_cfg lft;
struct xfrm_lifetime_cur curlft;
struct xfrm_policy_walk_entry walk;
struct xfrm_policy_queue polq;
u8 type;
u8 action;
u8 flags;
u8 xfrm_nr;
u16 family;
struct xfrm_sec_ctx *security;
struct xfrm_tmpl xfrm_vec[XFRM_MAX_DEPTH];
};
让我们深入几个关键字段:
1. refcnt(引用计数)
任何内核对象要是没了引用计数,那就是个定时炸弹。refcnt 初始化为 1,用的时候 xfrm_pol_hold() 加一,不用了 xfrm_pol_put() 减一。减到零的时候,这个策略就被销毁了。
2. timer(定时器)
策略不是永久有效的。xfrm_policy_timer() 负责监控策略的寿命。一旦时间到(lft 规定的硬限制),它会执行两件事:
- 删掉策略(调用
xfrm_policy_delete())。 - 给所有注册的密钥管理程序发个讣告(
XFRM_MSG_POLEXPIRE),告诉它“老弟,这策略过期了”。
3. lft 和 curlft(生命周期)
lft(Lifetime Config):这是配置项。你可以在命令行通过limit参数设置,比如:这里就把软字节数限制设为了 6000。ip xfrm policy add src 172.16.2.0/24 dst 172.16.1.0/24 limit byte-soft 6000 ...curlft(Current Lifetime):这是仪表盘,记录当前走了多少流量。它有四个u64字段:- bytes: 处理的总字节数(发送路径在
xfrm_output_one()加,收包路径在xfrm_input()加)。 - packets: 处理的总包数。
- add_time: 策略出生的时间戳。
- use_time: 上次被用到的时间戳。
- bytes: 处理的总字节数(发送路径在
如果你用 ip -stat xfrm policy show,就能看到这些统计数据。
4. polq(策略队列) 这是一个非常有意思,也容易让人掉坑的地方。 场景是这样的:数据包来了,匹配到了一条策略,说要加密。但是!对应的 SA(安全关联)还没建立好(密钥还没协商完)。这时候怎么办?
- 默认行为:直接扔掉。调用
make_blackhole()包就没了。 - 另一种选择:如果你把
/proc/sys/net/core/xfrm_larval_drop设为 0,内核就不会把包扔掉,而是把它们塞进这个polq.hold_queue里排队。- 队列最长 100 个包(
XFRM_MAX_QUEUE_LEN)。 - 这会创建一个“虚拟 Bundle”(
xfrm_create_dummy_bundle())来占位。 - 等密钥协商好了,这些包就会被处理。
- 队列最长 100 个包(
⚠️ 注意
sysctl_larval_drop 默认是 1(直接丢)。如果你在生产环境调试 IPsec,发现密钥交换还没完全搞定时包全断了,别慌,先看看这个参数是不是 1。
5. action(动作) 策略决定了命运。这个字段只有两个值:
- XFRM_POLICY_ALLOW (0): 放行。这通常意味着“如果你能找到 SA 来加密,那就走;如果找不到,看你的配置,可能丢弃,可能走明文”。
- XFRM_POLICY_BLOCK (1): 拦截。这就好比你在配置文件里写了
type=reject,内核直接把这个包掐死。
6. xfrm_vec(模板数组)
策略不直接指定“用密钥 A 加密”,它指定的是“模板”。xfrm_vec 是一个数组,最多能放 6 个模板(XFRM_MAX_DEPTH)。
这允许你做一些复杂的事情,比如“先做 ESP 加密,再做 IP 封装”。这些模板(xfrm_tmpl)是连接策略和具体状态的桥梁。
XFRM States(状态 / 安全关联)
如果说 策略 是“法律”,规定了什么该做,那么 状态 就是“武器”,是真正干活的家伙。
在内核里,一个 Security Association (SA) 用 struct xfrm_state 表示。
注意,SA 是单向的。如果你要双向通信,你需要两个 SA(一个进,一个出)。
你可以通过 ip xfrm state add 命令,发送 XFRM_MSG_NEWSA 消息给内核,触发 xfrm_state_add() 来创建它;删掉它则发 XFRM_MSG_DELSA。
状态结构体解析
struct xfrm_state 是个大块头,里面全是敏感信息:
struct xfrm_state {
. . .
union {
struct hlist_node gclist;
struct hlist_node bydst;
};
struct hlist_node bysrc;
struct hlist_node byspi;
atomic_t refcnt;
spinlock_t lock;
struct xfrm_id id; // <-- 身份证
struct xfrm_selector sel;
struct xfrm_mark mark;
u32 tfcpad;
u32 genid;
/* Key manager bits */
struct xfrm_state_walk km;
/* Parameters of this state. */
struct {
u32 reqid;
u8 mode;
u8 replay_window;
u8 aalgo, ealgo, calgo;
u8 flags;
u16 family;
xfrm_address_t saddr;
int header_len;
int trailer_len;
} props;
struct xfrm_lifetime_cfg lft;
/* Data for transformer */
struct xfrm_algo_auth *aalg;
struct xfrm_algo *ealg;
struct xfrm_algo *calg;
struct xfrm_algo_aead *aead;
/* Data for encapsulator */
struct xfrm_encap_tmpl *encap;
/* Data for care-of address */
xfrm_address_t *coaddr;
/* IPComp needs an IPIP tunnel for handling uncompressed packets */
struct xfrm_state *tunnel;
/* If a tunnel, number of users + 1 */
atomic_t tunnel_users;
/* State for replay detection */
struct xfrm_replay_state replay;
struct xfrm_replay_state_esn *replay_esn;
/* Replay detection state at the time we sent the last notification */
struct xfrm_replay_state preplay;
struct xfrm_replay_state_esn *preplay_esn;
/* The functions for replay detection. */
struct xfrm_replay *reply;
. . .
/* Statistics */
struct xfrm_stats stats;
struct xfrm_lifetime_cur curlft;
. . .
/* Reference to data common to all the instances of this
* transformer. */
const struct xfrm_type *type;
struct xfrm_mode *inner_mode;
struct xfrm_mode *inner_mode_iaf;
struct xfrm_mode *outer_mode;
/* Security context */
struct xfrm_sec_ctx *security;
/* Private data of this transformer, format is opaque,
* interpreted by xfrm_type methods. */
void *data;
};
这里面有几个关键点必须拎出来:
1. id(身份证)
xfrm_id 结构体包含三个字段:目的地址、SPI、协议号(AH/ESP/IPCOMP)。
这三元组在逻辑上唯一标识了一个 SA。注意是“逻辑上”,因为内核为了查找方便,建了好几个哈希表。
2. props(属性) 这里面都是实打实的配置:
- mode: 是 Transport(传输模式)还是 Tunnel(隧道模式)?这是 IPsec 的核心概念。
- replay_window: 抗重放攻击的窗口大小。
- aalgo, ealgo: 认证算法和加密算法的 ID。
- flags: 比如
XFRM_STATE_ICMP,用来控制一些特殊行为。 - saddr: 这个 SA 的源地址(因为是单向的,这个很重要)。
3. aalg / ealg / aead(算法指针)
这些指针指向具体的算法实例。这就是上一节我们讲的 Crypto API 真正被调用的地方。如果配置了 AES-GCM,aead 指针就会指向对应的 AEAD 算法实例。
4. replay(重放攻击防御)
IPsec 必须防重放。内核通过 replay 结构体记录序列号,如果收到的包序列号在窗口内且重复,直接丢掉。
5. type / mode(多态支持)
xfrm_type 指向了具体的协议操作方法(比如 ESP 怎么输出、ESP 怎么输入)。xfrm_mode 则决定了是传输模式还是隧道模式。这是 C 语言实现面向对象设计的经典案例。
SAD:安全关联数据库
内核把所有的 xfrm_state 存在哪里?
回看刚才的 netns_xfrm 结构体,那三个哈希表:
state_bydst: 按目的地址哈希。state_bysrc: 按源地址哈希。state_byspi: 按 SPI 哈希。
这有点像用三把不同的钥匙开同一扇门,根据你手头有的线索不同(比如有时候你只知道 SPI,有时候你有地址),可以走不同的入口查找。
当内核把一个 SA 插入数据库时(__xfrm_state_insert),它会同时计算这三个哈希值并挂进去。
特殊的“获取状态”
这里有一个很有意思的细节:SPI 为 0 的状态。
正常情况下,SPI 不会是 0。那什么时候是 0? 当你配置了策略,但密钥还没协商好时(比如 IKEv2 还在握手),内核为了防止疯狂发请求去骚扰用户空间的守护进程,会创建一个临时的、SPI 为 0 的“获取状态”。
- 这个状态不会挂到
state_byspi哈希表里(毕竟全是 0,没法查)。 - 只要这个状态存在,内核就知道“哦,我在协商中”,就不再发新的 Acquire 消息。
- 一旦协商完成,这个临时状态会被真正的 SA 替换掉。
查找 SA
内核提供了几个查找函数,对应不同的场景:
xfrm_state_lookup(): 最常用,查state_byspi。xfrm_state_lookup_byaddr(): 查state_bysrc。xfrm_state_find(): 查state_bydst,通常用于发送路径。
到这里,我们已经把 XFRM 的骨架——策略数据库(SPD)和状态数据库(SAD)——摸清楚了。你知道了内核怎么决定该处理谁,以及用哪把“钥匙”去处理。
但这还只是静态的配置。下一节,我们将看数据包是如何在这些哈希表和结构体之间流动,最终变成一串乱码飞向互联网的。