我正在开发一个接收IPv6路由器通告数据包的Linux用户空间程序。作为RFC4861的一部分,我需要验证ICMPv6校验和。基于我的研究,其中大部分指的是一般来说,如果你计算IPv6伪报头的补码校验和和数据包内容,结果应该是0xffff,那么就是指IP校验和。但我一直得到0x3fff的校验和。
我的校验和实现有问题吗? Linux内核在将数据包传递给用户空间之前是否验证了ICMPv6校验和?是否有一个很好的参考源,用于测试已知良好的ICMPv6数据包?
uint16_t
checksum(const struct in6_addr *src, const struct in6_addr *dst, const void *data, size_t len) {
uint32_t checksum = 0;
union {
uint32_t dword;
uint16_t word[2];
uint8_t byte[4];
} temp;
// IPv6 Pseudo header source address, destination address, length, zeros, next header
checksum += src->s6_addr16[0];
checksum += src->s6_addr16[1];
checksum += src->s6_addr16[2];
checksum += src->s6_addr16[3];
checksum += src->s6_addr16[4];
checksum += src->s6_addr16[5];
checksum += src->s6_addr16[6];
checksum += src->s6_addr16[7];
checksum += dst->s6_addr16[0];
checksum += dst->s6_addr16[1];
checksum += dst->s6_addr16[2];
checksum += dst->s6_addr16[3];
checksum += dst->s6_addr16[4];
checksum += dst->s6_addr16[5];
checksum += dst->s6_addr16[6];
checksum += dst->s6_addr16[7];
temp.dword = htonl(len);
checksum += temp.word[0];
checksum += temp.word[1];
temp.byte[0] = 0;
temp.byte[1] = 0;
temp.byte[2] = 0;
temp.byte[3] = 58; // ICMPv6
checksum += temp.word[0];
checksum += temp.word[1];
while (len > 1) {
checksum += *((const uint16_t *)data);
data = (const uint16_t *)data + 1;
len -= 2;
}
if (len > 0)
checksum += *((const uint8_t *)data);
printf("Checksum %x\n", checksum);
while (checksum >> 16 != 0)
checksum = (checksum & 0xffff) + (checksum >> 16);
checksum = ~checksum;
return (uint16_t)checksum;
}
答案 0 :(得分:1)
如果这是在小端机器上运行,那么我认为在累积校验和时需要(更多)字节交换。
例如,在小端机器上,从s6_addr16[0]
开始的典型IPv6地址的2001:
元素将包含0x0120
,而不是0x2001
。这会将你的进位放在错误的位置。
长度代码显示正常,因为您在那里使用htonl()
,但0x00 0x00 0x00 0x58
和后续的消息累积逻辑没有。我认为任何剩余的位也应该在高字节中结束,而不是代码中发生的低字节。
此外,使用0x0000
作为伪标头校验和字节是生成校验和时应该执行的操作。要验证,校验和使用IPv6 RA中收到的实际校验和字节,然后您应该将0xffff
作为最终值。
答案 1 :(得分:1)
while循环过度杀伤。身体只会发生一次。
while (checksum >> 16 != 0)
checksum = (checksum & 0xffff) + (checksum >> 16);
checksum = ~checksum;
return (uint16_t)checksum;
相反
checksum += checksum >> 16;
return (uint16_t)~checksum;
这是不必要的。 len始终是16位
temp.dword = htonl(len);
checksum += temp.word[0];
checksum += temp.word[1];
这是不必要的。常量总是00 00 00 58,所以只需添加58。
temp.byte[0] = 0;
temp.byte[1] = 0;
temp.byte[2] = 0;
temp.byte[3] = 58; // ICMPv6
checksum += temp.word[0];
checksum += temp.word[1];
除了处理整数的字节顺序和最后一个字节奇数字节的方式外,你的算法看起来一般都是正确的。从我如何读取协议开始,字节将以大端顺序求和,即字节0xAB 0xCD将被解释为16位0xABCD。您的代码取决于您的机器的订购。
构建整数的顺序将影响正确添加到校验和中的进位数。但是如果您的代码与目标机器匹配,则最后一个奇数编号的字节是错误的。 0xAB会导致0xAB00,而不会写入0x00AB。
答案 2 :(得分:1)
我发现了我的错误:我有一个256字节的输入缓冲区,并假设iov_len
上msg_iov
的{{1}}元素被修改为返回接收数据的长度。由于路由器广告的长度为常数64字节,因此这些长度之间的差异导致校验和中的常量错误。我不需要更改字节顺序来验证校验和(尽管我没有使用奇数长度的ICMPv6数据包来验证我在奇数长度情况下对最后一个字节的处理。
此外,校验和的最终NOT仅用于计算校验和,而不是验证校验和。如果校验和有效,则上面的代码recvmsg()
将返回0。