linux checksum
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