文章目录
校验和算法
发送方先把校验和字段置为0,然后对参与校验的数据每16bit进行二进制反码求和,然后将结果保存在校验和字段中。由于校验和计算方法是累加求和的方式,所以将校验和置为0可以将校验和字段本身排除在校验结果之外。
接收方收到报文后,带着校验和字段一起对对需要校验的数据每16bit进行二进制反码求和,如果最后的计算结果为0,那么校验通过,否则数据有损坏。
IP首部校验和计算API
由于IP首部的校验和计算范围只覆盖IP首部,所以其校验和的计算总是由软件来完成。考虑到转发过程中,往往会改变IP首部的某些字段(如TTL),所以为了更高效更新校验和,特意为IP首部的校验和计算提供了一些专用API。
ip_compute_csum()
/*
* this routine is used for miscellaneous IP-like checksums, mainly
* in icmp.c
*/
__sum16 ip_compute_csum(const void *buff, int len);
通用的IP首部校验和计算API。
ip_fast_csum()
/*
* This is a version of ip_compute_csum() optimized for IP headers,
* which always checksum on 4 octet boundaries.
*/
static inline __sum16 ip_fast_csum(const void *iph, unsigned int ihl);
该函数针对IP首部校验和的计算进行了优化,它的实现是体系结构相关的。
ip_send_check()
/* Generate a checksum for an outgoing IP datagram. */
__inline__ void ip_send_check(struct iphdr *iph)
{
iph->check = 0;
iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
}
就是ip_fast_csum()的包裹函数,在计算之前会把IP首部校验和清0再计算,用于发送报文的IP首部校验和计算。
ip_decrease_ttl()
static inline int ip_decrease_ttl(struct iphdr *iph)
{
u32 check = (__force u32)iph->check;
check += (__force u32)htons(0x0100);
iph->check = (__force __sum16)(check + (check>=0xFFFF));
return --iph->ttl;
}
该函数用于再报文转发过程中递减TTL并且更新校验和。
通用校验和计算API
通用校验和计算API主要由L4协议使用,这些协议的校验和通常会覆盖整个报文。
skb_checksum()
__wsum skb_checksum(const struct sk_buff *skb, int offset, int len, __wsum csum);
将skb封装的数据逻辑上是一块连续内存(内部可能在frags数组和frag_list中也有数据)。该函数以入参csum为校验和计算初始值,计算从内存开头(skb->data)偏移offset字节开始的len字节数据的校验和。
增加csum入参,是为了让该调用者能够先计算一块内存校验和,然后将其计算结果累加到skb_checksum()的结果上,后续很多接口是类似的功能。
csum_fold()
/*
* Fold a partial checksum
*/
static inline __sum16 csum_fold(__wsum sum);
为了计算高效,校验和很多时候是以32bit为单位计算的,该函数将32位的校验和计算结果转换为16位的结果(方法就是把高16位折到低16位上)。这通常是校验和计算的最后一步。
csum_partial系列
/*
* computes the checksum of a memory block at buff, length len,
* and adds in "sum" (32-bit)
*
* returns a 32-bit number suitable for feeding into itself
* or csum_tcpudp_magic
*
* this function must be called with even lengths, except
* for the last fragment, which may be odd
*
* it's best to have buff aligned on a 32-bit boundary
*/
__wsum csum_partial(const void *buff, int len, __wsum sum);
/*
* the same as csum_partial, but copies from src while it
* checksums, and handles user-space pointer exceptions correctly, when needed.
*
* here even more important to align src and dst on a 32-bit (or even
* better 64-bit) boundary
*/
__wsum csum_partial_copy_nocheck(const void *src, void *dst, int len, __wsum sum);
__wsum csum_partial_copy_from_user(const void __user *src, void *dst, int len,
__wsum sum, int *err_ptr);
/*
* And now for the all-in-one: copy and checksum from a user iovec
* directly to a datagram
* Calls to csum_partial but the last must be in 32 bit chunks
*
* ip_build_xmit must ensure that when fragmenting only the last
* call to this function will be unaligned also.
*/
int csum_partial_copy_fromiovecend(unsigned char *kdata, struct iovec *iov,
int offset, unsigned int len, __wsum *csump);
这组函数是最通用的校验和计算API了。它们的功能注释已经很清晰了。
csum_block_add/sub()
static inline __wsum csum_block_add(__wsum csum, __wsum csum2, int offset)
{
u32 sum = (__force u32)csum2;
if (offset&1)
sum = ((sum&0xFF00FF)<<8)+((sum>>8)&0xFF00FF);
return csum_add(csum, (__force __wsum)sum);
}
static inline __wsum csum_block_sub(__wsum csum, __wsum csum2, int offset)
{
u32 sum = (__force u32)csum2;
if (offset&1)
sum = ((sum&0xFF00FF)<<8)+((sum>>8)&0xFF00FF);
return csum_sub(csum, (__force __wsum)sum);
}
A和B两块数据的校验和分别位csum和csum2,如果现在这两块数据需要合并,那么它们的校验和也需要合并,此时可以使用csum_block_add(),它返回合并后的数据块的校验和。
类似的,如果B时A的一部分数据,现在需要把B从A中删除,它们也需要将B的校验和从A中删除,此时可以使用csum_block_sub(),它返回拆分后数据块A的校验和。
这两个函数都有个offset参数,是因为如果两块数据拆分或者合并时的偏移量不是偶数,因为校验和的计算必须是16位对齐的,所以需要特殊处理下。整体上来说,offset就是相对于数据块A开始的偏移量。
csum_add/sub()
static inline __wsum csum_add(__wsum csum, __wsum addend)
{
u32 res = (__force u32)csum;
res += (__force u32)addend;
return (__force __wsum)(res + (res < (__force u32)addend));
}
static inline __wsum csum_sub(__wsum csum, __wsum addend)
{
return csum_add(csum, ~addend);
}
这两个函数是csum_block_add/sub()的简化版,它们去掉了offset参数,所以调用者需要保证必须是16位对齐的。
skb_checksum_help()
这是设备接口层实现的一个函数,它整体的策略是让硬件校验和失效。网络协议栈底层在需要时会调用该函数。
/*
* Invalidate hardware checksum when packet is to be mangled, and
* complete checksum manually on outgoing path.
*/
int skb_checksum_help(struct sk_buff *skb)
{
__wsum csum;
int ret = 0, offset;
// 只有接收方向才会设置CHECKSUM_COMPLETE,所以直接将skb->ip_summed
// 设置为CHECKSUM_NONE,这样L4层软件就会自己进行校验
if (skb->ip_summed == CHECKSUM_COMPLETE)
goto out_set_summed;
// 下面是针对出口数据报的处理
// GSO会处理校验和,这里不处理
if (unlikely(skb_shinfo(skb)->gso_size)) {
/* Let GSO fix up the checksum. */
goto out_set_summed;
}
// 软件重新进行校验和计算
offset = skb->csum_start - skb_headroom(skb);
BUG_ON(offset >= skb_headlen(skb));
csum = skb_checksum(skb, offset, skb->len - offset, 0);
offset += skb->csum_offset;
BUG_ON(offset + sizeof(__sum16) > skb_headlen(skb));
if (skb_cloned(skb) &&
!skb_clone_writable(skb, offset + sizeof(__sum16))) {
ret = pskb_expand_head(skb, 0, 0, GFP_ATOMIC);
if (ret)
goto out;
}
// 将计算结果保存到报文指定位置
*(__sum16 *)(skb->data + offset) = csum_fold(csum);
out_set_summed:
skb->ip_summed = CHECKSUM_NONE;
out:
return ret;
}
对于出口数据包,该函数重新进行校验和计算。对于入口数据包,它会使硬件校验和无效,进而触发L4层软件重新校验。
csum_tcpudp_magic()
static inline __wsum
csum_tcpudp_nofold(__be32 saddr, __be32 daddr, unsigned short len,
unsigned short proto, __wsum sum);
/*
* computes the checksum of the TCP/UDP pseudo-header
* returns a 16-bit checksum, already complemented
*/
static inline __sum16
csum_tcpudp_magic(__be32 saddr, __be32 daddr, unsigned short len,
unsigned short proto, __wsum sum);
这是个体系结构相关的接口,它用于帮助TCP和UDP协议在全部报文校验和(入参sum)的基础上添加伪首部的校验和。csum_tcpudp_magic()返回的是16位的校验和结果;csum_tcpudp_nofold()返回的是32位的校验和结果。