关于IP报文首部校验和(checksum)算法,简单的说就是16位累加的反码运算,但具体是如何实现的,许多资料不得其详。TCP和UDP数据报首部也使用相同的校验算法,但参与运算的数据与IP报文首部不一样。此外,IPv6对校验和的运算与IPv4又有些许不同。因此有必要对IP分组的校验和算法作全面的解析。
Nothing in life is to be feared, it is only to be understood. — Marie Curie (居里夫人,波兰裔法国籍物理学家、化学家,两届诺贝尔奖得主)
IPv4首部校验和
IPv4报文首部的结构如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
其中的Header Checksum
域即为首部校验和部分。当要计算IPv4报文首部校验和时,发送方先将其置为全0,然后按16位逐一累加至IPv4报文首部结束,累加和保存于一个32位的数值中。如果总的字节数为奇数,则最后一个字节单独相加。累加完毕将结果中高16位再加到低16位上,重复这一过程直到高16位为全0。最后将结果取反存入首部校验和域。
下面用实际截获的IPv4分组来演示整个计算过程:
1 2 3 4 0x0000: 00 60 47 41 11 c9 00 09 6b 7a 5b 3b 08 00 45 00 0x0010: 00 1c 74 68 00 00 80 11 59 8f c0 a8 64 01 ab 46 0x0020: 9c e9 0f 3a 04 05 00 08 7f c5 00 00 00 00 00 00 0x0030: 00 00 00 00 00 00 00 00 00 00 00 00
在上面的16进制转储中,起始为以太网 (Ethernet) 帧的开头。IPv4报文首部从地址偏移量0x000e开始,第一个字节为0x45,最后一个字节为0xe9。根据以上的算法描述,我们可以作如下计算:
1 2 3 4 (1) 0x4500 + 0x001c + 0x7468 + 0x0000 + 0x8011 + 0x0000 + 0xc0a8 + 0x6401 + 0xab46 + 0x9ce9 = 0x3a66d (2) 0xa66d + 0x3 = 0xa670 (3) 0xffff - 0xa670 = 0x598f
注意在第一步我们用0x0000 设置首部校验和部分。可以看出这一报文首部的校验和与收到的值完全一致。以上的过程仅用于发送方计算初始的校验和,实际中对于中间转发的路由器和最终接收方,可将收到的IPv4报文首部校验和部分直接按同样算法相加,如果结果为0xffff ,则校验正确。
C语言实现
如何编程计算 IPv4 首部校验和?RFC 1071 (Computing the Internet Checksum) 给出了一个C语言的参考实现,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { register long sum = 0 ; while ( count > 1 ) { sum += * (unsigned short *) addr++; count -= 2 ; } if ( count > 0 ) sum += * (unsigned char *) addr; while (sum>>16 ) sum = (sum & 0xffff ) + (sum >> 16 ); checksum = ~sum; }
在实际的网络连接中,源点设备可以调用以上代码产生初始IPv4报文首部校验和。而后在每一步的路由跳转中,因为路由器必须递减首部存活时间 (Time To Live,简写TTL) 字段,所以要更新首部校验和。RFC 1141 (Incremental Updating of the Internet Checksum) 给出了快速更新校验和的参考实现:
1 2 3 4 unsigned long sum;ipptr->ttl--; sum = ipptr->Checksum + 0x100 ; ipptr->Checksum = (sum + (sum>>16 ));
TCP/UDP首部校验和
对于TCP和UDP的数据报,其首部也包含16位的校验和,由目的地接收端验证。校验算法与IPv4报文首部完全一致,但参与校验的数据不同。这时校验和不仅包含整个TCP/UDP数据报,还覆盖了一个伪首部。IPv4伪首部的定义如下:
1 2 3 4 5 6 7 8 0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | source address | +--------+--------+--------+--------+ | destination address | +--------+--------+--------+--------+ | zero |protocol| TCP/UDP length | +--------+--------+--------+--------+
其中有IP源地址,IP目的地址,协议号(TCP:6/UDP:17)及TCP或UDP数据报的总长度(首部+数据)。将伪首部加入校验的目的,是为了再次核对数据报是否到达正确的目的地,并防止IP欺骗攻击 (spoofing)。另外对于IPv4,UDP首部校验和是可选的,不用时该字段应被填充为全0。
IPv6的不同
IPv6是网际协议第6版,其设计的主要目的是为了解决IPv4地址枯竭问题,当然它在其他方面也有许多改进。虽然IPv6的使用量增长缓慢,但是其趋势不可阻挡。IPv6的最新互联网标准由RFC 8200 (Internet Protocol, Version 6 (IPv6) Specification)规范。IPv6报文首部的结构如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| Traffic Class | Flow Label | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Payload Length | Next Header | Hop Limit | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Source Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Destination Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
注意到IPv6首部并没有包含校验和字段,这也是与IPv4的一个显著不同点。IPv6协议的设计延展了互联网设计端到端原则,取消首部校验和字段简化了路由器的处理过程,加快了IPv6报文网络传输。对报文数据完整度的保护可由链路层或端点间高层协议(TCP/UDP)的差错检测功能完成。这也是为什么IPv6强制要求UDP层设定首部校验和字段的原因。
对于IPv6数据报TCP/UDP首部校验和的计算,其IPv6伪首部的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Source Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Destination Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Upper-Layer Packet Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | zero | Next Header | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
UDP-Lite应用
在实际的IPv6网络应用中,为了兼顾差错检测和传输效率,可以采用UDP-Lite(Lightweight UDP,轻量用户数据报协议)。UDP-Lite有自己的IP协议号136,其规范定义于 RFC 3828 (The Lightweight User Datagram Protocol (UDP-Lite))。参考以下的UDP-Lite首部格式,它使用与UDP相同的端口分配 ,但是它将原来UDP首部的“长度”字段重定义为“校验和覆盖”(Checksum Coverage)域,这样就可以允许应用层自行控制需要计算校验和的数据长度,从而容许没被覆盖的数据部分可能有损地传输。
1 2 3 4 5 6 7 8 9 10 11 12 0 15 16 31 +--------+--------+--------+--------+ | Source | Destination | | Port | Port | +--------+--------+--------+--------+ | Checksum | | | Coverage | Checksum | +--------+--------+--------+--------+ | | : Payload : | | +-----------------------------------+
UDP-Lite协议规定了“校验和覆盖”域的取值(以8位组为单位):
0
整个UDP-Lites数据报
计算要包括IP伪首部
1-7
(无效值)
接收方必须抛弃数据报
8
UDP-Lites首部
计算要包括IP伪首部
> 8
UDP-Lites 首部 + 部分负载数据 (payload)
计算要包括IP伪首部
> IP 数据报长度
(无效值)
接收方必须抛弃数据报
对于多媒体应用,采用VoIP或流视频数据传输协议,接收有一定程度损坏的数据比没接收到任何数据要好。另一个实例,是思科(Cisco)的无线局域网控制器和无线接入点之间的连接所基于的 CAPWAP 协议规范,它就规定了当连接建立于IPv6网络之上时,其数据通道缺省使用校验和覆盖值为8的UDP-Lite协议。
最后,分享一小段C程序,示例如何初始化Berkeley套接字 (socket) 以建立 IPv6 UDP-Lite 连接:
1 2 3 4 5 6 7 8 #include <sys/socket.h> #include <netinet/in.h> #include <net/udplite.h> int udplite_conn = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDPLITE);int val = 8 ; (void )setsockopt(udplite_conn, IPPROTO_UDPLITE, UDPLITE_SEND_CSCOV, &val, sizeof val); (void )setsockopt(udplite_conn, IPPROTO_UDPLITE, UDPLITE_RECV_CSCOV, &val, sizeof val);
这里 IPPROTO_UDPLITE
为协议号136,用它和AF_INET6
地址集参数一起调用socket()
函数来创建 IPv6 套接字。UDPLITE_SEND_CSCOV
(10) 和 UDPLITE_RECV_CSCOV
(11) 为套接字选项设置函数setsockopt()
的控制参数,分别用来指定发送和接受时的校验和覆盖值。注意收发双方必须设置同样的数值,否则接受方无法正确验证校验和。