Search code examples
c#network-programmingtcpchecksum

Computing TCP's checksum from byte array


I have been working at this for a while now without being able to get the correct CheckSum value. Here my method is taking in 3-byte arrays. The first containing the IPv4Header's bytes, the second containing TCPHeader bytes, and lastly the payload. I already verified that these bytes are correct, and I am trying to cross reference as I go along but after so many attempts, I keep coming up short. I have also not found any guide with any actual code that does this from a byte array.

        public override ushort ComputeChecksum(byte[] ipv4Header, byte[] transportHeader, byte[] payload)
        {
            int pseudoHeaderLength = 12; // Pseudo-header is 12 bytes
            int tcpLength = transportHeader.Length;
            int payloadLength = payload.Length;
            int totalLength = pseudoHeaderLength + tcpLength + payloadLength;
            bool isOddLength = (totalLength % 2 != 0);

            byte[] pseudoHeaderAndTcpSegment = new byte[totalLength + (isOddLength ? 1 : 0)];
            Console.WriteLine($"pseudoHeaderLength:{pseudoHeaderLength} transportHeader.Length:{tcpLength} payloadLength:{payloadLength} totalLength:{totalLength} newTotalLength:{pseudoHeaderAndTcpSegment.Length} OddLength:{isOddLength}");

            // Copy Source IP address (4 bytes)
            Buffer.BlockCopy(ipv4Header, 12, pseudoHeaderAndTcpSegment, 0, 4);
            Console.WriteLine((BitConverter.ToString(pseudoHeaderAndTcpSegment).Replace("-", " ") + ", "));
            Console.WriteLine("");

            // Copy Destination IP address (4 bytes)
            Buffer.BlockCopy(ipv4Header, 16, pseudoHeaderAndTcpSegment, 4, 4);
            Console.WriteLine((BitConverter.ToString(pseudoHeaderAndTcpSegment).Replace("-", " ") + ", "));
            Console.WriteLine("");

            // Zero byte (1 byte)
            pseudoHeaderAndTcpSegment[8] = 0;

            // Protocol (1 byte)
            pseudoHeaderAndTcpSegment[9] = ipv4Header[9];
            Console.WriteLine((BitConverter.ToString(pseudoHeaderAndTcpSegment).Replace("-", " ") + ", ") + " Protocol:" + ipv4Header[9]); // ipv4Header[9] prints 6 like it should
            Console.WriteLine("");

            // TCP length (2 bytes)
            pseudoHeaderAndTcpSegment[10] = (byte)((tcpLength + payloadLength) >> 8); // try <<
            pseudoHeaderAndTcpSegment[11] = (byte)(tcpLength & 0xFF);
            Console.WriteLine((BitConverter.ToString(pseudoHeaderAndTcpSegment).Replace("-", " ") + ", "));
            Console.WriteLine("");

            // Make a copy of the transport header to zero out the checksum
            byte[] transportHeaderCopy = new byte[tcpLength];
            Buffer.BlockCopy(transportHeader, 0, transportHeaderCopy, 0, tcpLength);
            transportHeaderCopy[16] = 0; // Zero out the checksum field (16th byte)
            transportHeaderCopy[17] = 0; // Zero out the checksum field (17th byte)
            Console.WriteLine((BitConverter.ToString(transportHeaderCopy).Replace("-", " ") + ", "));
            Console.WriteLine("");

            // Copy the modified TCP header with zeroed checksum field
            Buffer.BlockCopy(transportHeaderCopy, 0, pseudoHeaderAndTcpSegment, 12, tcpLength);
            Console.WriteLine("TCPHeader:" + (BitConverter.ToString(pseudoHeaderAndTcpSegment).Replace("-", " ") + ", "));
            Console.WriteLine("");

            // Copy the payload
            Buffer.BlockCopy(payload, 0, pseudoHeaderAndTcpSegment, 12 + tcpLength, payload.Length);
            if (isOddLength)
            {
                Console.WriteLine("Odd Length adding a zero");
                pseudoHeaderAndTcpSegment[totalLength] = 0;
            }
            Console.WriteLine("Payload:" + (BitConverter.ToString(pseudoHeaderAndTcpSegment).Replace("-", " ") + ", "));
            Console.WriteLine("");

            return ComputeChecksum(pseudoHeaderAndTcpSegment);
        }

        protected static ushort ComputeChecksum(byte[] data)
        {
            int length = data.Length;
            int i = 0;
            long sum = 0;

            while (length > 1)
            {
                sum += BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(i, 2));

                i += 2;
                length -= 2;
            }

            if (length > 0)
            {
                sum += data[i] << 8;
            }

            while ((sum >> 16) != 0)
            {
                sum = (sum & 0xFFFF) + (sum >> 16);
            }

            return (ushort)(~sum);
        }

The bytes that get printed seem to be correct and I read this article calculation-of-tcp-checksum which told me the that I should go Source IP, Destination IP, Segment Length, Protocol, fixed 8 bits followed up by the TCP header and the payload. So, I think my order is correct although I am not sure if I did the fixed 8 bits and the Length part correctly. I am also not sure if I am calculating the actual 2 byte answer correctly using ones complement. Any corrections would be greatly appreciated thank you!

Edit: I thought I might also add in the debug messages in case anyone finds it useful

A2 9F 82 EA 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00,

A2 9F 82 EA C0 A8 32 86 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00,

A2 9F 82 EA C0 A8 32 86 00 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00,

A2 9F 82 EA C0 A8 32 86 00 42 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00,  Protocol:6

A2 9F 82 EA C0 A8 32 86 00 42 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00,

A2 9F 82 EA C0 A8 32 86 00 42 06 00 01 BB F0 49 FD 02 5E BB 35 90 17 70 50 18 00 04 83 57 00 00 17 03 03 00 29 E0 63 19 68 7C 3F 19 3F 33 A6 9E 0B CD 54 47 8A 97 1B CC 97 1A 45 CD 5C EF 75 3E 75 A0 32 A3 46 2D 3F FD 37 DC F3 DB 34 AF,

CorrectCheckSumCheck:22403 0x8357  OurCheck:64005 0x05FA 

The endianness is kina of a pain since the original ushort of our checksum is 22403 and since C# on Windows uses little endian, I needed to convert it to big endian before then displaying it as hex which gives 0x8357 aka the correct value (I know its correct since it matches what I see in the packets full byte array). Then for ours I printed both and its so far off that it's obvious it's not an endianness issue. For context here is the byte array of the TCP header unmodified 01 BB F0 49 FD 02 5E BB 35 90 17 70 50 18 00 04 83 57 00 00


Solution

  • Here is a working version in LINQPad including two sample headers.

    Two very important things to note: words must be read in big endian (network order), and the checksum in the header must be zeroed out if you're verifying captured headers.

    void Main()
    {
        var pseudoHeader =
            CreatePseudoHeader
            (
                new byte[] { 0xac, 0x18, 0x0b, 0x9f },
                new byte[] { 0xac, 0x18, 0x7e, 0x34 },
                6,
                FromHex("0d3df160ea5e5c54e0e45e585018fa0000000000170303002e0000000000002015cd7f0a9400f87f031162f4de4118d5670f885cea116c630a416883d8569bedc94f2df41d5e20")
            );
    
    
        ComputeChecksum(pseudoHeader).ToString("X4").Dump();
    
        pseudoHeader =
                CreatePseudoHeader
                (
                    new byte[] { 0xac, 0x18, 0x7e, 0x34 },
                    new byte[] { 0xac, 0x18, 0x0b, 0x9f },
                    6,
                    FromHex("f1600d3de0e45e58ea5e5c875010101100000000")
                );
    
    
        ComputeChecksum(pseudoHeader).ToString("X4").Dump();
    }
    
    private static byte[] FromHex(string v)
    {
        var buffer = new byte[v.Length / 2];
    
        for (var i = 0; i < buffer.Length; i++)
        {
            buffer[i] = Convert.ToByte(v.Substring(i * 2, 2), 16);
        }
    
        return buffer;
    }
    
    public static ushort ComputeChecksum(byte[] data)
    {
        int length = data.Length;
        int i = 0;
        long sum = 0;
    
        while (length > 1)
        {
            sum += BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(i, 2));
    
            i += 2;
            length -= 2;
        }
    
        if (length > 0)
        {
            sum += data[i] << 8;
        }
    
        while ((sum >> 16) != 0)
        {
            sum = (sum & 0xFFFF) + (sum >> 16);
        }
    
        return (ushort)(~sum);
    }
    
    public byte[] CreatePseudoHeader(byte[] sourceIp, byte[] destIp, byte protocol, byte[] tcpHeader)
    {
        var tcpLength = tcpHeader.Length;
        var pseudoHeader = new byte[12 + tcpLength];
        Buffer.BlockCopy(sourceIp, 0, pseudoHeader, 0, 4);
        Buffer.BlockCopy(destIp, 0, pseudoHeader, 4, 4);
        pseudoHeader[8] = 0;
        pseudoHeader[9] = protocol;
        pseudoHeader[10] = (byte)(tcpLength >> 8);
        pseudoHeader[11] = (byte)(tcpLength & 0xff);
        Buffer.BlockCopy(tcpHeader, 0, pseudoHeader, 12, tcpLength);
    
        return pseudoHeader;
    }
    

    Of course, this is a very straightforward implementation; good enough for learning purposes, but certainly not for any real use :)