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接收流程图

先上流程图,接下来细说各个函数。

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_finalresubmit来重新解析。

/* 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;
}

参考:

内核网络协议栈传输层协议框架
XFRM – IPsec协议的内核实现框架
内核UDP隧道框架
IPV6 实现

posted @ 2023-01-18 19:43  江子无怒  阅读(0)  评论(0)    收藏  举报  来源