本文讲解如何计算报文头部的 checksum。
UDP checksum 计算
计算方法
关于传输层 UDP 头部的介绍可以查看 这里 。
如何计算 UDP 的 checksum 值?
创建伪头部(对于 IPv4 报文,占 12 字节) :把伪头部添加到 UDP 头部前面。
准备 UDP 头部(8 字节) :计算前将头部的 checksum 字段 清零 。
将 UDP 头部和数据扩展到偶数字节的总长度 :一般 UDP 头部字节数为偶数,如果 UDP 数据部分的长度为奇数,则需要在末尾“添加”一个字节的填充值(通常为 0),以确保总长度为偶数。注意,不需要修改 UDP 头部中的长度字段的值。
计算 checksum:
把所有字段(伪头部、UDP 头部和数据)看作是(划分为)按顺序排列的 16 位字(2 字节),如果数据长度为奇数,补充的一个 0 字节(在报文地址的最高位)也需要包含在内。
将所有 16 位字相加,如果有进位(超过 16 位),则将进位加回到最低有效位 。
对结果按位取反,得到最终的 checksum 值 。
如果按位取反后的结果为 0,则需要将 checksum 设置为 0xFFFF,这是一种特殊情况,用于表示 checksum 为 0。因为,若发送方不愿意计算 checksum,它可以将 checksum 字段设置为 0x0000。接收方在接收这样的包时,会按照协议规定,忽略该包的 checksum 的验证。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include <arpa/inet.h> #include <netinet/in.h> #include <stdint.h> #include <stdio.h> #include <string.h> #define HANDLE_CARRY_OVERFLOW #define HANDLE_CARRY_HIGHEST_BIT struct pseudo_header { struct in_addr src_addr ; struct in_addr dst_addr ; uint8_t reserved; uint8_t protocol; uint16_t length; }; struct udp_header { uint16_t src_port; uint16_t dst_port; uint16_t length; uint16_t checksum; }; uint16_t calc_checksum (void * data, int length) { uint32_t sum = 0 ; uint16_t * ptr = (uint16_t *)data; while (length > 1 ) { sum += *ptr; ptr++; #ifdef HANDLE_CARRY_OVERFLOW if (sum > 0xffff ) { sum = (sum & 0xffff ) + (sum >> 16 ); } #else if (sum & 0x80000000 ) { sum = (sum & 0xffff ) + (sum >> 16 ); } #endif length -= 2 ; } if (length) { sum += *((uint8_t *)ptr); } while (sum >> 16 ) { sum = (sum & 0xffff ) + (sum >> 16 ); } return (uint16_t )(~sum); } int main () { struct in_addr src_addr , dst_addr ; (void )inet_aton("10.20.111.10" , &src_addr); (void )inet_aton("10.20.11.11" , &dst_addr); uint8_t data[] = {0x12 , 0x34 , 0x56 , 0x78 , 0x9a , 0xbc , 0xde , 0xf0 , 0xf1 , 0xf2 , 0xf3 }; struct udp_header udp ; udp.src_port = htons(0xaaaa ); udp.dst_port = htons(0xbbbb ); udp.length = htons(sizeof (struct udp_header) + sizeof (data)); udp.checksum = 0 ; struct pseudo_header pseudo ; pseudo.src_addr = src_addr; pseudo.dst_addr = dst_addr; pseudo.reserved = 0 ; pseudo.protocol = IPPROTO_UDP; pseudo.length = udp.length; uint8_t buffer[sizeof (struct pseudo_header) + sizeof (struct udp_header) + sizeof (data)]; memcpy (buffer, &pseudo, sizeof (struct pseudo_header)); memcpy (buffer + sizeof (struct pseudo_header), &udp, sizeof (struct udp_header)); memcpy (buffer + sizeof (struct pseudo_header) + sizeof (struct udp_header), data, sizeof (data)); udp.checksum = calc_checksum(buffer, sizeof (buffer)); printf ("UDP Checksum: 0x%4X\n" , ntohs(udp.checksum)); return 0 ; }
上面的代码中,有 HANDLE_CARRY_OVERFLOW 和 HANDLE_CARRY_HIGHEST_BIT 两种处理进位的逻辑,这是怎么回事?
在网络协议实现中,尤其是校验和计算时,有些实现会在特定条件下进行进位处理(如最高位为 1 时),而有些实现则会在累计和超过 16 位时进行处理(如 sum > 0xFFFF 时)。
最高位为 1(0x80000000)的处理:
这种处理方式是在累加过程中,立即检测是否出现了可能的溢出情况(累加值达到或超过 2^31)。这是为了在处理大数据包时,避免过早处理进位 。虽然这种方式在实现上较为简单直接,但在一般的 UDP/TCP 校验和计算中,不如传统的进位处理方式常见。
累计和超过 16 位(sum > 0xFFFF)的处理:
这种处理方式更符合大多数校验和算法(如互联网校验和)的习惯:每次累加之后,立即检查总和是否超过 16 位。如果超过 16 位(即高于 0xFFFF),则将高位进位添加到低位部分,符合循环进位的概念。该方法确保每次累加后的数值都保持在 16 位以内,从而 避免逐字节累加大数值时可能出现的溢出情况 。
增量计算方法
在网络设备中,通常会修改某些字段,而这些字段一般只有几个字节(如源 IP、源 PORT),如果再次通过遍历完整的头部、数据部分重新计算 checksum,效率不高。通过比较修改的几个字节,进行增量计算 checksum,更高效。下面的代码是 RFC1624 中给出了计算方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 uint16_t update_checksum (uint16_t cur_cksum, uint16_t old_val, uint16_t new_val) {#ifndef RFC1624_EFFICIENT_CHECKSUM uint32_t new_cksum = 0 ; if (old_val == new_val) { return cur_cksum; } cur_cksum = ntohs(cur_cksum); old_val = ntohs(old_val); new_val = ntohs(new_val); new_cksum = ((~cur_cksum) & 0xffff ) + ((~old_val) & 0xffff ) + new_val; new_cksum = (new_cksum & 0xffff ) + (new_cksum >> 16 ); new_cksum += (new_cksum >> 16 ); return (uint16_t )(~new_cksum); #else int32_t new_cksum = 0 ; if (old_val == new_val) { return cur_cksum; } cur_cksum = ntohs(cur_cksum); old_val = ntohs(old_val); new_val = ntohs(new_val); new_cksum = (cur_cksum & 0xffff ) - ((~old_val) & 0xffff ) - new_val; new_cksum = (new_cksum & 0xffff ) + (new_cksum >> 16 ); new_cksum += (new_cksum >> 16 ); return (uint16_t )(new_cksum); #endif } int main () { uint16_t cksum = update_checksum(0x43D7 , 0xf1f2 , 0xe0e3 ); printf ("UDP Checksum: 0x%4X\n" , ntohs(cksum)); return 0 ; }
对于右移操作,通常需要区分的是算术右移(也称为符号右移)和逻辑右移:
算术右移:保留符号位,右移时高位用符号位填充 。例如,负数右移后仍然是负数。
逻辑右移:不保留符号位,右移时高位用 0 填充 。
在 C 语言中,对于有符号类型(比如 int32_t),右移操作通常被实现为算术右移(具体取决于编译器)。对于无符号类型(比如 uint32_t),右移操作是逻辑右移。
TCP checksum 计算
TCP 和 UDP 的 checksum 计算方式基本相同。TCP checksum 计算 涉及到 TCP 伪头部、TCP 头部和 TCP 数据三部分 。TCP 伪头部的构成与 UDP 伪头部的构成完全一致,只是填充的成员值不同,如 TCP 的 protocol=6、UDP 的 protocol=17。
IPv4 checksum 计算
IPv4 的 checksum 计算方法与 TCP 和 UDP 的 checksum 计算方式相似,不同之处在于:IPv4 仅使用 IPv4 头部计算 checksum、不使用数据部分。
不管是 TCP、UDP 还是 IPv4,在计算其 checksum 前,都需要将头部中的原 checksum 字段清零。