1

I am trying to calculate the checksum of an ICMPv6 message (to be precise, a Neighbor Advertisement).

RFC 4443 describes it as the "16-bit one's complement of the one's complement sum of the entire ICMPv6 message"

There is also some example code on how to do that (although I think it's from IPv4, but the only difference is what is included in the sum, not how to calculate it): RFC 1071

I have taken a packet from wireshark and entered the shorts in host byte order. Then I print the correct checksum, zero it out and calculate mine. But they do not match. According to RFC 1071, endianess shouldn't be the problem (the result isn't just byte swapped, but completely off).

According to RFC 2460 #8.1 I need to include the "pseudo-header" into the calculation, containing only Src+Dst address, length as 32 Bit wide field and next header type.

The Calling code:

uint32_t payloadlen = htonl(32);
struct ip6_hdr *ip6;
struct nd_neighbor_advert *na;
size_t len, offset, tmplen;
uint8_t *tmppacket, icmp = 58;

uint8_t packet[] = {
            0x60, 0x00, 0x00, 0x00, 0x00, 0x20, 0x3A, 0xFF, 0x20, 0x01, 0x0D, 0xB8,
            0xDD, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
            0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x54, 0xFF,
            0xFE, 0x00, 0x22, 0x00, 0x88, 0x00, 0x54, 0xB9, 0x60, 0x00, 0x00, 0x00,
            0x20, 0x01, 0x0D, 0xB8, 0xDD, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x01, 0x00, 0x02, 0x01, 0x00, 0x03, 0x54, 0x00, 0x00, 0x13
        };

na->nd_na_hdr.icmp6_cksum = 0;

tmplen = 40+sizeof(struct nd_neighbor_advert)+ICMP_OPT_LEN;
tmppacket = malloc(tmplen);
memset(tmppacket, 0, 40);
offset = sizeof(struct in6_addr);
memcpy(tmppacket, &ip6->ip6_src, offset);
memcpy(tmppacket+offset, &ip6->ip6_dst, offset);
memcpy(tmppacket+offset*2, &payloadlen, 4);
memcpy(tmppacket+39, &icmp, 1);
memcpy(tmppacket+40, packet+sizeof(struct ip6_hdr),
        sizeof(struct nd_neighbor_advert)+ICMP_OPT_LEN);

na = (struct nd_neighbor_advert *) (tmppacket+40);
na->nd_na_hdr.icmp6_cksum = checksum((uint16_t *) tmppacket, tmplen);
printf("Checksum calc: %hX\n", na->nd_na_hdr.icmp6_cksum);

dump((unsigned char *) tmppacket, tmplen);

The checksum function:

uint16_t checksum (uint16_t *addr, size_t bytes) {
    unsigned int i;
    uint16_t *p = addr;
    uint32_t sum = 0;

    /* Sum */
    for (i=bytes; i > 1; i -= 2)
        sum += *p++;

    /* If uneven length */
    if (i > 0)
        sum += (uint16_t) *((unsigned char *) (p));

    /* Carry */
    while ((sum & 0xFFFF0000) != 0)
        sum = (sum >> 16) + (sum & 0xFFFF);

    return ~((uint16_t) sum);
}

This is just quick and dirty to get it working at first. The code for "main" omits some stuff. Endianess in the checksum function shouldn't be a problem, also this packet has an even length.

The result should be B954, but I get DB32. The output of dump is:

Packet size: 72
2001 0DB8 DDDD 0000 0000 0000 0000 0100 FE80 0000 0000 0000 0203 54FF FE00 2200 0000 0020 0000 003A 8800 32DB 6000 0000 2001 0DB8 DDDD 0000 0000 0000 0000 0100 0201 0003 5400 0013

Thanks for the tips so far. If you have an idea what could be still wrong, I'd appreciate your input.

Community
  • 1
  • 1
Benjamin Maurer
  • 3,602
  • 5
  • 28
  • 49
  • There's no need to represent the packet itself as `uint16_t[]`, I think that just causes confusion. If you need to add the bytes, then do that, i.e. represent the packet as an array of `uint8_t`, preserving the exact byte ordering of course. – unwind Feb 18 '13 at 12:42
  • @unwind True, I didn't need to do that. But in the final program, I won't be entering packets manually anyway. In the checksum, I don't need to add bytes, but 16 Bit values. It doesn't change the result whether I pass a uint8_t[] or uint16_t[] to the function. – Benjamin Maurer Feb 18 '13 at 12:57
  • Did you try `setsockopt(2)` with opname = IPV6_CHECKSUM? – Aif Sep 14 '17 at 17:10

2 Answers2

3

I think there are three problems with your code:

  1. You checksum the IPv6 header, which you shouldn't. The checksum should cover both IPv6 addresses and the length, but not other header fields.
  2. Endianity matters if the length is uneven. Before adding the extra character, you need to ntohs it (or actually, on a little-endian platform, shift it right 8 bits). EDIT: Ignoring it is OK on little-endian platforms. The shift is needed on big-endian.
  3. When reducing to 16 bits, the sum may overflow a short. In this case, you need to feed this carry back to the calculation (i.e. add 1).
ugoren
  • 16,023
  • 3
  • 35
  • 65
  • To be more precise, the ICMPv6 checksum takes into account a pseudoheader of 40 bytes, which is a derivative of the real IPv6 header, and which is composed as follows (in order): - 16 bytes for the source address - 16 bytes for the destination address - 4 bytes high endian payload length (the same value as in the IPv6 header) - 3 bytes zero - 1 byte nextheader (so, 58 decimal) – KJH Jan 08 '16 at 11:18
3

Try this version of the checksum calculation function (it worked for me)

uint16_t
checksum (void * buffer, int bytes) {
   uint32_t   total;
   uint16_t * ptr;
   int        words;

   total = 0;
   ptr   = (uint16_t *) buffer;
   words = (bytes + 1) / 2; // +1 & truncation on / handles any odd byte at end

   /*
    *   As we're using a 32 bit int to calculate 16 bit checksum
    *   we can accumulate carries in top half of DWORD and fold them in later
    */
   while (words--) total += *ptr++;

   /*
    *   Fold in any carries
    *   - the addition may cause another carry so we loop
    */
   while (total & 0xffff0000) total = (total >> 16) + (total & 0xffff);

   return (uint16_t) total;
}

then assign it to the checksum field like this

yourpkt->checksum = ~(checksum (buff, length));
Davide Berra
  • 6,387
  • 2
  • 29
  • 50
  • The odd byte handling seems to work only if the byte following the buffer is zero. You can't assume it (it might even be invalid memory). – ugoren Feb 18 '13 at 19:33
  • You're right... but before the last edit, the array was made of uint16_t elements... no invalid memory access was possible – Davide Berra Feb 19 '13 at 08:09
  • 1
    The original prototype was actually incorrect. It's convenient for the checksum function to view the data as 16-bit words, but it doesn't mean that it's actually so. – ugoren Feb 19 '13 at 12:51
  • How do you take into account source and destination IP address ? – Leanid Vouk Dec 29 '17 at 02:07