1. 关键结构

由于目前很多网卡设备是支持对L4层数据包进行校验和的计算和验证的,所以在L4协议软件的实现中,

会根据网卡的支持情况作不同的处理,为此内核在struct sk_buff结构和struct net_device中增加了校验和相关的参数,如下:

struct sk_buff

上面的结构中,和校验和有关的几个字段如下:

#define CHECKSUM_NONE 0
#define CHECKSUM_UNNECESSARY 1
#define CHECKSUM_COMPLETE 2
#define CHECKSUM_PARTIAL 3

struct sk_buff
{
	union {
		__wsum		csum;
		struct {
			__u16	csum_start;
			__u16	csum_offset;
		};
	};
    __u8 ip_summed:2,
}

联合体中哪个成员有效取决于ip_summed的值,ip_summed共两个bit,可取四个标志,而且在发送和接收时的含义还有所不同

在接收过程中,ip_summed字段包含了设备驱动告诉L4软件当前校验和的状态,各取值含义如下:

  • CHECKSUM_NONE:硬件没有提供校验和,可能是硬件不支持,也可能是硬件校验出错但是并未丢弃数据包,而是让L4软件重新校验
  • CHECKSUM_UNNECESSARY:硬件已经进行了完整的校验,无需软件再进行检查,L4收到数据包后如果检查ip_summed是这种情况,就可以跳过校验过程
  • CHECKSUM_COMPLETE:硬件已经校验了L4报头和其payload部分,并且校验和保存在了csum中,L4软件只需要再计算伪报头然后检查校验结果即可

在发送过程中,ip_summed字段包含了L4软件告诉设备驱动程序当前校验和的状态,各取值含义如下:

  • CHECKSUM_NONE:L4软件已经进行了校验,硬件无需做任何事情
  • CHECKSUM_PARTIAL:L4软件计算了伪报头,并且将值保存在了首部的check字段中,硬件需要计算其余部分的校验和

struct net_device

net_device结构中的feature字段中定义了如下和校验和相关的字段,这些字段表明了硬件计算校验和的能力

NETIF_F_NO_CSUM:该设备非常可靠,无需L4执行任何校验,环回设备一般设置该标记 NETIF_F_IP_CSUM:设备可以对基于IPv4的TCP和UDP数据包进行校验 NETIF_F_IPV6_CSUM:设备可以对基于IPv6的TCP和UDP数据包进行校验 NETIF_F_HW_CSUM: 设备可以对任何L4协议的数据包进行校验

注:这些概念和字段的含义同样适用于TCP校验和处理过程

2. 输入数据报的校验和计算

udp4_csum_init()

@skb: 待校验的数据报
@uh:该数据报的UDP首部
@proto:L4协议号,为IPPROTO_UDP或者IPPROTO_UDPLITE
static inline int udp4_csum_init(struct sk_buff *skb, struct udphdr *uh,
				 int proto)
{
	const struct iphdr *iph;
	int err;

	//这两个字段用于指示对报文的哪些部分进行校验,cov指coverage,
	//只有UDPLite使用,对于UDP,会对整个报文进行校验
	UDP_SKB_CB(skb)->partial_cov = 0;
	UDP_SKB_CB(skb)->cscov = skb->len;

	//UDPLITE,忽略
	if (proto == IPPROTO_UDPLITE) {
		err = udplite_checksum_init(skb, uh);
		if (err)
			return err;
	}

	iph = ip_hdr(skb);
	//UDP首部校验和字段为0,这种情况说明已经处理过了,设置为CHECKSUM_UNNECESSARY,后续无需再进行处理
	if (uh->check == 0) {
		skb->ip_summed = CHECKSUM_UNNECESSARY;
	} else if (skb->ip_summed == CHECKSUM_COMPLETE) {
		//还有伪首部需要校验,所以添加伪首部校验,如果校验成功,设置为CHECKSUM_UNNECESSARY
		//csum_tcpudp_magic()计算伪首部校验和后进行验证,如果验证ok,返回0,该函数体系结构相关,
		//为了高效,用汇编语言实现
		if (!csum_tcpudp_magic(iph->saddr, iph->daddr, skb->len, proto, skb->csum))
			skb->ip_summed = CHECKSUM_UNNECESSARY;
	}
	//如果经过上面处理后仍然需要校验,则计算校验和,并将结果放入到skb->csum中
	if (!skb_csum_unnecessary(skb))
		skb->csum = csum_tcpudp_nofold(iph->saddr, iph->daddr,
					       skb->len, proto, 0);

	return 0;
}

//在接收方向上,CHECKSUM_UNNECESSARY表示校验ok,无需再进行校验和计算
static inline int skb_csum_unnecessary(const struct sk_buff *skb)
{
	return skb->ip_summed & CHECKSUM_UNNECESSARY;
}

udp_lib_checksum_complete()

//返回0表示校验成功
static inline int udp_lib_checksum_complete(struct sk_buff *skb)
{
	//如果需要校验则调用__udp_lib_checksum_complete()进行校验
	return !skb_csum_unnecessary(skb) &&
		__udp_lib_checksum_complete(skb);
}

/*
 *	Generic checksumming routines for UDP(-Lite) v4 and v6
 */
static inline __sum16 __udp_lib_checksum_complete(struct sk_buff *skb)
{
	//增加一个需要校验的长度字段,对于UDP,该字段就是整个报文长度
	return __skb_checksum_complete_head(skb, UDP_SKB_CB(skb)->cscov);
}

__sum16 __skb_checksum_complete_head(struct sk_buff *skb, int len)
{
	__sum16 sum;

	//计算校验和,如果成功,那么最终结果应该是0
	sum = csum_fold(skb_checksum(skb, 0, len, skb->csum));
	if (likely(!sum)) {
		//为什么CHECKSUM_COMPLETE时是校验失败???
		if (unlikely(skb->ip_summed == CHECKSUM_COMPLETE))
			netdev_rx_csum_fault(skb->dev);
		//设置校验和状态为CHECKSUM_UNNECESSARY
		skb->ip_summed = CHECKSUM_UNNECESSARY;
	}
	return sum;
}

3. 输出数据包的校验和计算

4. 伪包头

伪首部,又称为伪包头(Pseudo Header):是指在TCP的分段或UDP的数据报格式中,

在数据报首部前面增加源IP地址、目的IP地址、IP分组的协议字段、TCP或UDP数据报的总长度等共12字节,

所构成的扩展首部结构。此伪首部是一个临时的结构,它既不向上也不向下传递,仅仅只是为了保证可以校验套接字的正确性

5. 参考

(1)UDP之数据报校验和:https://blog.csdn.net/xiaoyu_750516366/article/details/83422212

(2)Checksum in Linux Kernel:http://hustcat.github.io/checksum-in-kernel/

(3)linux内核分析,检验和:https://blog.csdn.net/qy532846454/article/details/7010852