如何验证ICMPv6校验和? (为什么我一直得到0x3fff的校验和?)

时间:2011-09-17 21:17:42

标签: c linux sockets ipv6 icmp

我正在开发一个接收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;
}

3 个答案:

答案 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_lenmsg_iov的{​​{1}}元素被修改为返回接收数据的长度。由于路由器广告的长度为常数64字节,因此这些长度之间的差异导致校验和中的常量错误。我不需要更改字节顺序来验证校验和(尽管我没有使用奇数长度的ICMPv6数据包来验证我在奇数长度情况下对最后一个字节的处理。

此外,校验和的最终NOT仅用于计算校验和,而不是验证校验和。如果校验和有效,则上面的代码recvmsg()将返回0。