一. 简介

本文详细分析一个ESP Packet在协议栈的input方向和output方向是如何处理的。先讲述相对简单的output方向,再讲述复杂一点的input方向。

本文仅仅涉及重要的函数名,用以显示调用关系与注册时机,不会陷入到具体的实现细节之中。

1.1 网络拓扑

本节中将使用传统的站点到站点的业务拓扑:

moon站点保护子网10.1.0.0/16,其中Alice是一个主机,IP地址为10.1.0.10/16;

sun站点保护子网10.2.0.0/16,其中Bob是一个主机,IP地址为10.2.0.10/16;

1
2
3
4
5
10.1.0.0/16 -- | 192.168.0.1 | === | 192.168.0.2 | -- 10.2.0.0/16
moon-net moon sun sun-net
| |
10.1.0.10/16 10.2.0.10/16
Alice Bob

使用Alice ping Bob,我们在moon站点的192.168.0.1接口抓包。

1
2
3
4
moon:~# tcpdump -ni eth0 esp or icmp
09:30:24.456208 IP 192.168.0.1 > 192.168.0.2: ESP(spi=0xc7aa5990,seq=0x3), length 120
09:30:24.460150 IP 192.168.0.2 > 192.168.0.1: ESP(spi=0xcbda26bb,seq=0x3), length 120
09:30:24.462169 IP 10.2.0.10 > 10.1.0.10: ICMP echo reply, id 58139, seq 1, length 64

我们也将按照这三个分组的时间顺序,来分析协议栈是如何处理数据包的。

1.2 先觉条件

了解IP、ESP的报文格式,可以去参考链接中熟悉一下。

1.3 参考链接

Strongswan 中的 IPsec 协议简介

二. OUTPUT方向

OUTPUT方向是针对moon站点来说的,moon站点将向sun站点发送ESP分组。

我们是使用Alice ping Bob来产生ESP分组的。意味着moon站点的10.1.0.1接口将首先受到一个来自Alice设备的ICMP请求。源地址为10.1.0.10,目的地址为10.2.0.10。

1
2
moon:~# tcpdump -ni eth1 icmp
09:46:28.524684 IP 10.1.0.10 > 10.2.0.10: ICMP echo request, id 43193, seq 1, length 64

2.1 查路由

moon站点在执行ip_rcv_finish_core(),在查路由的时候将ip_forward()注册到了input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ip_rcv_finish() {
ip_rcv_finish_core() {
ip_route_input_noref() {
ip_route_input_rcu() {
ip_route_input_slow() {
ip_mkroute_input() {
__mkroute_input() {
rth->dst.input = ip_forward;
}
}
}
}
}
}
}

2.2 ip_forward

OUTPUT方向上ESP的所有处理都将从ip_forward()开始。

1
2
3
ip_forward() {
xfrm4_route_forward()
}

1. xfrm4_route_forward

在转发时,会在xfrm policy中查询分组是否需要进入XFRM流程。

此时,我们需要处理的分组是这样子的:

1
09:46:28.524684 IP 10.1.0.10 > 10.2.0.10: ICMP echo request, id 43193, seq 1, length 64

我们可以使用ip xfrm policy get src 10.1.0.0/16 dst 10.2.0.0/16 dir out来查看当前XFRM策略

1
2
3
4
5
moon:~# ip xfrm policy get src 10.1.0.0/16 dst 10.2.0.0/16 dir out
src 10.1.0.0/16 dst 10.2.0.0/16
dir out priority 383615 ptype main
tmpl src 192.168.0.1 dst 192.168.0.2
proto esp spi 0xc6121b90 reqid 1 mode tunnel

可以发现,在OUT方向上,这个分组与xfrm policy是匹配的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ip_forward() {
xfrm4_route_forward() {
xfrm_route_forward() {
__xfrm_route_forward() {
xfrm_lookup() {
xfrm_lookup_with_ifid() {
xfrm_bundle_lookup() {
xfrm_resolve_and_create_bundle() {
xfrm_bundle_create()
}
}
}
}
}
}
}
}

2. xfrm4_output

匹配到XFRM策略后,将进入XFRM框架。

我们可以使用ip xfrm policy get src 10.1.0.0/16 dst 10.2.0.0/16 dir out来查看当前XFRM策略的templates:

1
2
3
4
5
moon:~# ip xfrm policy get src 10.1.0.0/16 dst 10.2.0.0/16 dir out
src 10.1.0.0/16 dst 10.2.0.0/16
dir out priority 383615 ptype main
tmpl src 192.168.0.1 dst 192.168.0.2
proto esp spi 0xc6121b90 reqid 1 mode tunnel

可以看到,这个策略的templates是tmpl src 192.168.0.1 dst 192.168.0.2

XFRM框架会将我们的Packet信息与tmpl进行匹配,最终找到或创建一个XFRM State实例。

通过这个XFRM State就可以找到afinfo注册的回调函数afinfo->output

这个output函数是在xfrm4_state_init()的时候,通过xfrm_state_register_afinfo()xfrm4_state_afinfo注册到了xfrm_state_afinfo中。

而afinfo->output就是xfrm4_output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xfrm_resolve_and_create_bundle() {
xfrm_tmpl_resolve() {
xfrm_tmpl_resolve_one() {
xfrm_state_find() {
}
}
}

xfrm_bundle_create() {
inner_mode = &xfrm[i]->inner_mode;
afinfo = xfrm_state_afinfo_get_rcu(inner_mode->family);
/* xfrm4_output() */
dst1->output = afinfo->output;
}
}

3. xfrm_output

接下来进入xfrm4_output()流程。

1
2
3
4
5
xfrm4_output() {
__xfrm4_output() {
xfrm_output()
}
}

4. xfrm_output_one

在xfrm_output_one中,会根据xfrm state来对ICMP Packet作封装。

我们可以查看与这个Packet匹配的XFRM State,可以发现,mode为tunnel,意味着XFRM框架会对这个PACKET作一层外侧封装。

1
2
    外层IP                      内层IP
|192.168.0.1 192.168.0.2 | 10.1.0.10 10.2.0.10 |
1
2
3
4
5
6
7
moon:~# ip xfrm state list src 192.168.0.1 dst 192.168.0.2 proto esp
src 192.168.0.1 dst 192.168.0.2
proto esp spi 0xcb29792e reqid 1 mode tunnel
replay-window 0 flag af-unspec
aead rfc4106(gcm(aes)) 0x2847cc697e65aae5dbe32300f31810896c22f23a 128
lastused 2024-05-22 01:35:45
anti-replay context: seq 0x0, oseq 0x1, bitmap 0x00000000
1
2
3
4
5
6
7
8
9
xfrm_output() {
xfrm_output_resume() {
xfrm_output_one() {
/* 此处完成tunnel封装 */
xfrm_outer_mode_output()
}
skb_dst(skb)->ops->local_out(net, sk, skb);
}
}

5. xfrm_register_type

此时,我们已经为PACKET作了tunnel分装,接下来,将会进入ESP模块,对Packet进行加密处理。

1
2
3
4
5
6
7
8
9
xfrm_output_one() {
/* 此处完成tunnel封装 */
xfrm_outer_mode_output()

xfrm_type_output() {
/* esp_output() */
x->type->output(x, skb)
}
}

这个x->type->output是在ESP模块初始化时,通过xfrm_register_type()注册到xfrm_state_get_afinfo(family)中去的。

在创建XFRM State实例的时候,注册到了x->type字段。

1
2
3
__xfrm_init_state() {
x->type = xfrm_get_type(x->id.proto, family);
}

那么,此时x->type->output(x, skb)其实就是在调用esp_output()

6. esp_output

在此处,会在内层IP和外层IP之间插入ESP Header,并添加ESP tail,并对ESP有效载荷作加密处理。

IPsec Tunnel Mode

7. __ip_local_out

至此,网络层的所有处理已经完成,将会调用协议栈的__ip_local_out()将这个SKB发往下一层,这里就不是我们关注的重点了。

ESP在OUTPUT方向上经过的所有关键函数已经全部罗列了出来。

2.3 总结

moon站点在eth0接口收到一个ICMP报文,经过了XFRM协议栈,向sun站点发送一个ESP报文。至此,第一个ESP报文的生命流程已经解释完毕。

1
2
3
4
moon:~# tcpdump -ni eth0 esp or icmp
09:30:24.456208 IP 192.168.0.1 > 192.168.0.2: ESP(spi=0xc7aa5990,seq=0x3), length 120
09:30:24.460150 IP 192.168.0.2 > 192.168.0.1: ESP(spi=0xcbda26bb,seq=0x3), length 120
09:30:24.462169 IP 10.2.0.10 > 10.1.0.10: ICMP echo reply, id 58139, seq 1, length 64

IPsec ESP Tunnel Mode

三. INPUT方向

moon站点向sun站点发送了ESP Packet,紧接着,收到了来自sun站点的回复。

1
09:30:24.460150 IP 192.168.0.2 > 192.168.0.1: ESP(spi=0xcbda26bb,seq=0x3), length 120

接下来,我们将分析这个Packet是如何进入moon站点的XFRM协议栈的。

3.1 ip_local_deliver_finish

同样,当Packet进入网络层,查询路由,发现目的地址为本地地址,于是进入ip_local_deliver函数继续将数据包发送至上一层。

1
2
3
4
5
6
7
netif_receive_skb_list_internal() {
__netif_receive_skb_list_core() {
ip_list_rcv() {
ip_local_deliver_finish()
}
}
}

3.2 xfrm4_esp_rcv

ip_protocol_deliver_rcu根据ip_hdr(skb)->protocol来选择应该调用哪个处理函数来继续解析Packet。

由于IP层内是ESP Header,那么ip_hdr(skb)->protocol就是IPPROTO_ESP(50)。所以就会调用IPPROTO_ESP的hander处理函数xfrm4_esp_rcv。

xfrm4_esp_rcv通过xfrm4_protocol_register注册到inet_protos[IPPROTO_ESP]。

1
2
3
4
5
6
7
ip_local_deliver_finish() {
ip_protocol_deliver_rcu() {
ipprot = rcu_dereference(inet_protos[protocol]);
/* xfrm4_esp_rcv */
ipprot->handler();
}
}

3.3 xfrm4_rcv

xfrm4_rcv通过xfrm4_protocol_register()esp4_protocol注册到struct xfrm4_protocolhandler链表中。xfrm4_esp_rcv将遍历hander链表,执行处理函数。

1
2
3
4
5
6
7
xfrm4_esp_rcv() {
for_each_protocol_rcu(esp4_handlers, handler)
{
/* xfrm4_rcv */
handler->handler(skb);
}
}

3.4 xfrm_input

xfrm_input函数是INPUT方向最核心的处理函数。

1
2
3
4
5
xfrm4_rcv() {
xfrm4_rcv_spi() {
xfrm_input()
}
}

1. xfrm_state_lookup

此时,协议栈正在处理报文格式是这样的:

1
09:30:24.460150 IP 192.168.0.2 > 192.168.0.1: ESP(spi=0xcbda26bb,seq=0x3), length 120

XFRM框架将会把这个Packet和XFRM State进行对比,查找到匹配的XFRM State。

1
2
3
4
5
6
moon:~# ip x s list src 192.168.0.2 dst 192.168.0.1 proto esp
src 192.168.0.2 dst 192.168.0.1
proto esp spi 0xc0a1c17e reqid 1 mode tunnel
replay-window 32 flag af-unspec
aead rfc4106(gcm(aes)) 0xf507bc1f40d1ff250eeb93b566940d89f0a2abbd 128
anti-replay context: seq 0x0, oseq 0x0, bitmap 0x00000000

可以发现这个Packet匹配到了存储解密ESP方法的XFRM State。

至此,我们就可以通过这个XFRM State进入ESP模块,解密ESP消息。

1
2
3
xfrm_input() {
xfrm_state_lookup()
}

2. esp_input

所有解密ESP Packet的线索都存在了XFRM State中,包括了解密方法。

解密方法存储来type->input中。而type是在创建XFRM state时初始化的。

1
2
3
__xfrm_init_state() {
x->type = xfrm_get_type(x->id.proto, family);
}
1
2
3
4
5
6
7
8
xfrm_input() {
xfrm_state_lookup()
xfrm_type_input() {
x = xfrm_input_state();
/* esp_input */
x->type->input();
}
}

在这个函数中,将解密ESP消息,并且剥离ESP headerESP tail

3. xfrm_inner_mode_input

至此,Packet的状态是这样的:

1
2
    外层IP                      内层IP
|192.168.0.2 192.168.0.1 | 10.2.0.10 10.1.0.10 |

xfrm_inner_mode_input()将会脱去外层IP,我们就得到了一个从10.2.0.10发往10.1.0.10的ICMP Packet。这也就是第三个数据包:

1
09:30:24.462169 IP 10.2.0.10 > 10.1.0.10: ICMP echo reply, id 58139, seq 1, length 64

4. xfrm_rcv_cb

剥离ESP封装后,将进入xfrm_rcv_cb流程。

xfrm_rcv_cb通过xfrm_input_register_afinfo(&xfrm4_input_afinfo)注册到afinfo中。

1
2
3
4
5
6
7
8
9
10
xfrm_input() {
xfrm_state_lookup()
xfrm_type_input()
xfrm_inner_mode_input()
xfrm_rcv_cb() {
afinfo = xfrm_input_get_afinfo(family, is_ipip);
/* xfrm4_rcv_cb */
ret = afinfo->callback(skb, protocol, err);
}
}

接下来,分析xfrm4_rcv_cb作了什么:

1
2
3
4
5
6
7
8
xfrm4_rcv_cb() {
struct xfrm4_protocol __rcu **head = proto_handlers(protocol);
for_each_protocol_rcu(*head, handler)
{
/* esp4_rcv_cb */
handler->cb_handler(skb, err)
}
}

其实esp4_rcv_cb中什么都没有作,仅仅直接返回了0。

5. gro_cells_receive

至此,我们已经完整地从ESP Packet中,解析到了ICMP Packet。接下来,需要让这个ICMP Packet重走协议栈即可。

1
2
3
4
5
6
7
8
9
10
11
xfrm_input() {
xfrm_state_lookup()
xfrm_type_input()
xfrm_inner_mode_input()
xfrm_rcv_cb()
gro_cells_receive() {
cell = this_cpu_ptr(gcells->cells);
__skb_queue_tail(&cell->napi_skbs, skb);
napi_schedule(&cell->napi);
}
}

在这里,把新的ICMP Packet加入到cell->napi_skbs的队尾。并唤起NET_RX_SOFTIRQ中断。

3.5 总结

moon站点在eth0接口收到一个ESP报文,首先通过ip_local_deliver发现是ESP Packet,接着进入了XFRM协议栈,进行ESP解密、ESP解封装、tunnel解封装,把内层的IP Packet追加的napi_skbs的队尾,唤起软中断,让ICMP Packet重走协议栈。

1
2
3
4
moon:~# tcpdump -ni eth0 esp or icmp
09:30:24.456208 IP 192.168.0.1 > 192.168.0.2: ESP(spi=0xc7aa5990,seq=0x3), length 120
09:30:24.460150 IP 192.168.0.2 > 192.168.0.1: ESP(spi=0xcbda26bb,seq=0x3), length 120
09:30:24.462169 IP 10.2.0.10 > 10.1.0.10: ICMP echo reply, id 58139, seq 1, length 64

IPsec ESP Tunnel Mode