10.5 接收一个 IPsec 数据包(传输模式)
上一节我们在初始化的最后停在了 xfrm4_rcv() 上——这是 ESP 协议注册给内核的接收入口。管道铺好了,现在真的有数据包流进来了,它会经历一场什么样的旅程?
让我们假设这是一个 IPv4 的 ESP 传输模式报文,且目的地址正是本机。
10.5. 接收一个 IPsec 数据包(传输模式)
上一节我们提到,xfrm4_rcv() 被注册成了 ESP、AH 甚至 IPCOMP 协议的共同入口。这很聪明,因为既然大家进门后的第一件事都要查表、验证重放攻击,不如把这些脏活累活集中在一个地方干。
所以,当这个 ESP 包到达 ip_local_deliver_finish() 时,内核发现 IP 头部里的协议字段是 50(ESP),于是直接调用 xfrm4_rcv()。而这个函数没干别的,转手就把包扔给了通用的 XFRM 处理中心——xfrm_input()。
这才是真正要把这层「加密壳」敲开的地方。
第一步:去 SAD 里找钥匙
xfrm_input() 拿到包后,第一件事并不是急着解密,而是先确认一件事:我们和对面有没有商量好怎么解这个包?
这需要去安全关联数据库(SAD)里查一下。查表的关键字是 SPI(安全参数索引)、目的地址和协议号。如果查不到,说明这个包要么是发错了,要么是有人胡乱发包,直接扔掉:
/* 在 state_byspi 哈希表中执行查找 */
x = xfrm_state_lookup(net, skb->mark, daddr, spi, nexthdr, family);
/* 如果查找失败,静默丢弃数据包 */
if (x == NULL) {
XFRM_INC_STATS(net, LINUX_MIB_XFRMINNOSTATES);
xfrm_audit_state_notfound(skb, family, spi, seq);
goto drop;
}
这一步如果没找到对应的 SA(x == NULL),内核就会把这个包默默丢掉,并增加一个计数器 XFRMINNOSTATES。这种「静默失败」在网络层很常见,因为你没法给一个未知的加密包发 ICMP 错误消息——你根本不知道它是谁发的,也没法解密它的内容。
第二步:协议专用的解密回调
如果运气好,SA 找到了,x 指针就指向了一个有效的 xfrm_state 结构。接下来,内核会根据这个 SA 关联的协议类型(比如 ESP),调用该协议注册的 .input 回调函数。
对于我们正在讨论的 IPv4 ESP 场景,这个 x->type 就是上一节注册的 esp_type,它的 .input 回调指向 esp_input():
/* 调用对应协议的 input 方法(这里是 esp_input),
返回值 nexthdr 是加密前原始数据的协议号 */
nexthdr = x->type->input(x, skb);
esp_input() 会去调用 Crypto API 进行解密和认证(AEAD 操作)。如果密码校验失败,包会被丢弃;如果成功,ESP 头部和 ESP 尾部会被剥离,留下的就是原始的 IP 载荷。
这个函数返回了一个重要的东西:nexthdr。这是剥离 ESP 封装后,里面那个原始包的协议号(比如 TCP 是 6,UDP 是 17)。
内核小心翼翼地把这个协议号存了起来,塞到了 skb 的控制缓冲区(cb)里。因为 skb 的数据区域现在已经解密了,原来的 IP 头部还没来得及更新,我们需要先记下来,等下修 IP 头的时候要用:
/* 将原始协议号保存在 SKB 的控制缓冲区中,
* 稍后修改 IP 头时会用到 */
XFRM_MODE_SKB_CB(skb)->protocol = nexthdr;
第三步:善后与伪装(Transport Finish)
解密完了,现在我们手里拿着一个已经被「还原」的数据包:它的数据部分是明文了,但它的 IP 头部还写着「协议=ESP」(50)。这显然不行,传输层(L4)拿到这个包会一脸懵逼。
所以,xfrm_input() 最后会调用 xfrm4_transport_finish() 来做最后的整容手术。
来看看这个函数干了什么:
int xfrm4_transport_finish(struct sk_buff *skb, int async)
{
struct iphdr *iph = ip_hdr(skb);
/* 此时 iph->protocol 还是 50 (ESP),
* 我们要把它改成解密后原始包的协议号(如 TCP/UDP),
* 这样 L4 协议栈才能认领这个包 */
iph->protocol = XFRM_MODE_SKB_CB(skb)->protocol;
/* 调整 skb 的指针,因为解密后包的长度可能变了,
* 需要让 IP 头部的指针回到正确的位置 */
__skb_push(skb, skb->data - skb_network_header(skb));
/* 更新 IP 头部的总长度字段 */
iph->tot_len = htons(skb->len);
/* 重新计算 IP 头部校验和,因为我们改了 protocol 和 tot_len */
ip_send_check(iph);
这一段非常关键。你可以把它想象成拆快递:esp_input 负责把箱子里的东西(数据)拿出来,而 xfrm4_transport_finish 负责把快递单上的信息(IP 头)改成商品原本的样子。
修改 IP 头部是必须的,因为接下来这个包要重新进入网络栈的后续流程。如果不改,内核会以为这还是个 ESP 包,继续往加密引擎里送,那就死循环了。
第四步:重新注入协议栈
伪装完成之后,这个包在逻辑上已经变成了一个普通的、未加密的 IP 包。为了让它走完剩下的路程(到达 TCP 或 UDP socket),内核会再次把它扔回到协议栈的接收路径中。
这一步通过 Netfilter hook 实现:
/* 调用 netfilter 的 PRE_ROUTING hook,
* 然后通过 xfrm4_rcv_encap_finish 继续流转 */
NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,
xfrm4_rcv_encap_finish);
return 0;
}
xfrm4_rcv_encap_finish() 最终会调用 ip_local_deliver()。此时,IP 头部里的协议字段已经是 TCP 或 UDP 了,内核会像处理一个刚从网卡收到的普通包一样,把它交给上层协议处理。
到这里,一个 IPsec 传输模式包的接收之旅就真正结束了。
从外层看,这是一个 ESP 包;进到 xfrm_input() 里,它被查表、解密;出来后,它改头换面,变成了一个普通的 IP 包,神不知鬼不觉地混入了正常的数据流中。这就是传输模式的精髓——不改变原有的路由路径,只是悄悄地加固了传输过程。