linux内核ipv6 nat时ipsec接收流程
分析下ipv6在nat和非nat环境下,内核收到ipsec流量时是如何处理的(主要是ip6_input_finish后,xfrm6_udp_encap_rcv前这一部分)。
本人一届内核菜鸟,工作原因看了几天nat环境下的ipv6的ipsec建立过程,略有所得,斗胆撰写此文,然学艺不精,有所疏忽,在所难免,还请各位斧正。
用户侧ipsec版本:Strongswan 5.3.5
内核版本:4.19.68
注:linux内核实际上是在5.8版本才正式支持nat环境下的ipv6 ipsec,但是我们用的内核版本是4.19,所以移植了部分5.8的patch到本内核上,对实际流程没什么影响。
移植内容详见:
xfrm: add support for UDPv6 encapsulation of ESP
udp: implement complete book-keeping for encap_needed
xfrm和udp隧道
了解内核中ipsec的建立和收发流程就要先了解xfrm,xfrm是内核为处理ipsec之类的协议引入的一个框架,他会将收到的ipsec报文转换(解密)为原始报文,然后再交给原始报文对应的协议处理。
内核接收到数据时会通过ipprot->handler(skb)回调函数,调用对应协议的处理函数,ipsec的esp报文走到这里的回调函数就是xfrm4_rcv(ipv4)和xfrm6_rcv(ipv6)。
无论是xfrm对ipsec报文的解密,还是对解密后生成的原始报文的处理,都是在内核内部完成的,也就是说在ipsec隧道内的流量,strongswan是很少甚至不会参与其中的,解密和后续处理都是由内核来完成的。
nat情况下有些特殊,ipsec会在esp上面再封装一层udp,所以ipprot->handler(skb);回调函数会先调用udp_rcv()(ipv4)和udpv6_rcv()(ipv6),然后通过udp隧道(udp_queue)将数据包发送到xfrm_udp_encap_rcv(ipv4)和xfrm6_udp_encap_rcv(ipv6),之后的流程就和非nat下大同小异了。
ipv6下ipsec接收流程图
先上流程图,接下来细说各个函数。

源码分析
ip6_input_finish()
对应ipv4流程中的ip_local_deliver_finish(),重点关注ret = ipprot->handler(skb); 这一行,负责调用上层传输协议回调函数。
如果收到的是 ipsec 的 esp 报文,ipprot->handler(skb)对应的回调函数是xfrm6_rcv(),以此进入xfrm框架处理。值得一提的是,这个函数每拆一层封装都会调用一次来处理解封后的内容(假设内部依旧是ipv6报文,如果是ipv4的话会走ipv4专用函数ip_local_deliver_finish()),也就是说如果我在隧道内ping包的话,处理流程为 ip6_input_finish() --> xfrm6_rcv() --> ip6_input_finish() --> icmp_rcv()。
如果ipsec工作在nat环境下,会对esp报文外面再封一层4500端口的UDP封装,所以ipprot->handler(skb)对应的回调函数是udpv6_rcv(),之后再走UDP隧道(udp_queue)进入xfrm框架。 udpv6_rcv()会直接通过自己函数内部的udp隧道进入xfrm,所以nat下隧道内ping包的流程为 ip6_input_finish() --> udpv6_rcv() --> xfrm6_rcv_encap() --> ip6_input_finish() --> icmp_rcv()。
想了解更多,推荐阅读
static int ip6_input_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
const struct inet6_protocol *ipprot;
struct inet6_dev *idev;
unsigned int nhoff;
int nexthdr;
bool raw;
bool have_final = false;
/*
* Parse extension headers
*/
rcu_read_lock();
resubmit:
idev = ip6_dst_idev(skb_dst(skb));
if (!pskb_pull(skb, skb_transport_offset(skb)))
goto discard;
/* 扩展头协议在ipv6头结构中的偏移 */
nhoff = IP6CB(skb)->nhoff;
/* 从IP头部取出protocol协议号字段 */
nexthdr = skb_network_header(skb)[nhoff];
resubmit_final:
/* 从数组中取出相应的注册协议结构体 */
raw = raw6_local_deliver(skb, nexthdr);
ipprot = rcu_dereference(inet6_protos[nexthdr]);
if (ipprot) {
int ret;
if (have_final) {
if (!(ipprot->flags & INET6_PROTO_FINAL)) {
/* Once we've seen a final protocol don't
* allow encapsulation on any non-final
* ones. This allows foo in UDP encapsulation
* to work.
*/
goto discard;
}
} else if (ipprot->flags & INET6_PROTO_FINAL) {
const struct ipv6hdr *hdr;
/* Only do this once for first final protocol */
have_final = true;
/* Free reference early: we don't need it any more,
and it may hold ip_conntrack module loaded
indefinitely. */
nf_reset(skb);
skb_postpull_rcsum(skb, skb_network_header(skb),
skb_network_header_len(skb));
hdr = ipv6_hdr(skb);
if (ipv6_addr_is_multicast(&hdr->daddr) &&
!ipv6_chk_mcast_addr(skb->dev, &hdr->daddr,
&hdr->saddr) &&
!ipv6_is_mld(skb, nexthdr, skb_network_header_len(skb)))
goto discard;
}
if (!(ipprot->flags & INET6_PROTO_NOPOLICY) &&
!xfrm6_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard;
//执行对应协议的处理函数
ret = ipprot->handler(skb);
if (ret > 0) {
if (ipprot->flags & INET6_PROTO_FINAL) {
/* Not an extension header, most likely UDP
* encapsulation. Use return value as nexthdr
* protocol not nhoff (which presumably is
* not set by handler).
*/
nexthdr = ret;
goto resubmit_final;
} else {
goto resubmit;
}
} else if (ret == 0) {
__IP6_INC_STATS(net, idev, IPSTATS_MIB_INDELIVERS);
}
} else { //没有找到上层处理函数
if (!raw) {
if (xfrm6_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP6_INC_STATS(net, idev,
IPSTATS_MIB_INUNKNOWNPROTOS);
icmpv6_send(skb, ICMPV6_PARAMPROB,
ICMPV6_UNK_NEXTHDR, nhoff);
}
kfree_skb(skb);
} else {
__IP6_INC_STATS(net, idev, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
rcu_read_unlock();
return 0;
discard:
__IP6_INC_STATS(net, idev, IPSTATS_MIB_INDISCARDS);
rcu_read_unlock();
kfree_skb(skb);
return 0;
}
udpv6_rcv()
就是个内联函数,没什么好说的。
static __inline__ int udpv6_rcv(struct sk_buff *skb)
{
return __udp6_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}
__udp6_lib_rcv()
这个函数比较长,又没什么好说的(主要是因为没细看),就不放源码了。
主要作用为初始化校验和,然后根据是多播还是单播进入不同的分支,多播的分支有兴趣自己看下,我没看。单播的话会走到udp6_unicast_rcv_skb()函数。
值得注意的是这里只是初始化了校验和模块,并没有真正进行校验。
udp6_unicast_rcv_skb()
看注释的意思,作用是封装udpv6_queue_rcv_skb()的,用来处理checksum的转换和返回值的转换。
如果udpv6_queue_rcv_skb()的返回值大于0,则在第一个函数中会通过goto跳转到resubmit_final或resubmit来重新解析。
/* wrapper for udp_queue_rcv_skb tacking care of csum conversion and
* return code conversion for ip layer consumption
*/
static int udp6_unicast_rcv_skb(struct sock *sk, struct sk_buff *skb,
struct udphdr *uh)
{
int ret;
if (inet_get_convert_csum(sk) && uh->check && !IS_UDPLITE(sk))
skb_checksum_try_convert(skb, IPPROTO_UDP, uh->check,
ip6_compute_pseudo);
ret = udpv6_queue_rcv_skb(sk, skb);
/* a return value > 0 means to resubmit the input */
if (ret > 0)
return ret;
return 0;
}
udpv6_queue_rcv_skb()
这个函数就是进入xfrm前的最后一步了。函数指针encap_rcv对应的回调就是xfrm6_udp_encap_rcv()了,下面的udp_lib_checksum_complete(skb)就是真正在做sumcheck校验了,其内部会先通过skb_csum_unnecessary()判断该数据是否需要校验,若需要则会通过__udp_lib_checksum_complete()进行sumcheck。
static int udpv6_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
struct udp_sock *up = udp_sk(sk);
int is_udplite = IS_UDPLITE(sk);
/* 根据IPsec策略检查数据包。如果允许处理数据包,则此函数的返回值为1;如果不允许,则返回0 */
if (!xfrm6_policy_check(sk, XFRM_POLICY_IN, skb))
goto drop;
/* 判断当前套接口的udp_encap_needed是否使能,并且encap_type不为0 */
if (static_branch_unlikely(&udpv6_encap_needed_key) && up->encap_type) {
int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);
/*
* This is an encapsulation socket so pass the skb to
* the socket's udp_encap_rcv() hook. Otherwise, just
* fall through and pass this up the UDP socket.
* up->encap_rcv() returns the following value:
* =0 if skb was successfully passed to the encap
* handler or was discarded by it.
* >0 if skb should be passed on to UDP.
* <0 if skb should be resubmitted as proto -N
*/
/* if we're overly short, let UDP handle it */
encap_rcv = READ_ONCE(up->encap_rcv);
if (encap_rcv) {
int ret;
/* Verify checksum before giving to encap */
if (udp_lib_checksum_complete(skb))
goto csum_error;
ret = encap_rcv(sk, skb);
if (ret <= 0) {
__UDP_INC_STATS(sock_net(sk),
UDP_MIB_INDATAGRAMS,
is_udplite);
return -ret;
}
}
/* FALLTHROUGH -- it's a UDP Packet */
}
/*
* UDP-Lite specific tests, ignored on UDP sockets (see net/ipv4/udp.c).
*/
if ((is_udplite & UDPLITE_RECV_CC) && UDP_SKB_CB(skb)->partial_cov) {
if (up->pcrlen == 0) { /* full coverage was set */
net_dbg_ratelimited("UDPLITE6: partial coverage %d while full coverage %d requested\n",
UDP_SKB_CB(skb)->cscov, skb->len);
goto drop;
}
if (UDP_SKB_CB(skb)->cscov < up->pcrlen) {
net_dbg_ratelimited("UDPLITE6: coverage %d too small, need min %d\n",
UDP_SKB_CB(skb)->cscov, up->pcrlen);
goto drop;
}
}
prefetch(&sk->sk_rmem_alloc);
if (rcu_access_pointer(sk->sk_filter) &&
udp_lib_checksum_complete(skb))
goto csum_error;
if (sk_filter_trim_cap(sk, skb, sizeof(struct udphdr)))
goto drop;
udp_csum_pull_header(skb);
skb_dst_drop(skb);
return __udpv6_queue_rcv_skb(sk, skb);
csum_error:
__UDP6_INC_STATS(sock_net(sk), UDP_MIB_CSUMERRORS, is_udplite);
drop:
__UDP6_INC_STATS(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
atomic_inc(&sk->sk_drops);
kfree_skb(skb);
return -1;
}
参考:

浙公网安备 33010602011771号