Ping Part III: Adventures in Socket programming using System.Net

 

In Part II of this article, we saw how we are going to use the ICMP protocol to implement a simple Ping client. We also saw a skeleton of this program which showed how to translate the ICMP packet specification into a C# structure.

 

In this part, we will write a routine to calculate the checksum of the packet, and a routine to serialize the packet into a byte array. Recall from PartII that the request and reply packets have a particular encoding on the wire. We will have to write a routine that will encode the packet into a byte array, so that the array can be sent on the wire.

 

Take the skeleton that we created in Part II and add the following (lines in blue):

 

using System;

using System.Text;

using System.Globalization;

namespace ping

{

      public class ICMP_PACKET

      {

            ...

            public ICMP_PACKET(Byte kind, Byte code, UInt16 id, UInt16 seq, byte[] data)

            {

                  this.i_type = kind;

                  this.i_code = code;

                  this.i_id = ToNetworkOrder(id);

                  this.i_seq = ToNetworkOrder(seq);

                  this.data = data;

                  this.i_cksum = Checksum();

            }

            public static ICMP_PACKET CreateRequestPacket(UInt16 id, UInt16 seq, byte[] data)

            {

                  return new ICMP_PACKET(8, 0, id, seq, data);

            }

            public static byte[] GetBytesInNetworkOrder(UInt16 number)

            {

                  byte[] bytes = BitConverter.GetBytes(number);

                  if (BitConverter.IsLittleEndian)

                        return new byte[] { bytes[1], bytes[0] };

                  else

                        return bytes;

            }

      public static UInt16 ToNetworkOrder(UInt16 number)

            {

                  byte[] networkBytes = GetBytesInNetworkOrder(number);

                  return BitConverter.ToUInt16(networkBytes, 0);

            }

            public UInt16 Checksum()

            {

                  int cksum_buffer_length = (int)(Length / 2);

                  byte [] packetBytes = Serialize(this);

                  //

                  // first, convert the serialized bytes into a UInt16 array

                  // we will use the UInt16 array to do the checksum

                  //

                  UInt16[] checksumBuffer = new UInt16[cksum_buffer_length];

                  int index = 0;

                  int i = 0;

                  while (index < Length)

                  {

                        checksumBuffer[i] = BitConverter.ToUInt16(packetBytes,index);

                        index += 2;

                        ++i;

                  }

                  int checksum = 0;

                  for (i = 0; i < checksumBuffer.Length; i++)

                  {

                        checksum += Convert.ToInt32(checksumBuffer[i]);

                  }

                  checksum = (checksum >> 16) + (checksum & 0xffff);

                  checksum += (checksum >> 16);

                  return (UInt16)(~checksum);

            }

            public byte[] Serialize()

            {

                  return Serialize(this);

            }

            public static byte[] Serialize(ICMP_PACKET packet)

            {

                  // first, find out how many bytes to allocate for the serialized packet

                  int packet_size = packet.Length;

                  bool isLittleEndian = BitConverter.IsLittleEndian;

                  UInt16 cksum = packet.i_cksum;

                  UInt16 id = packet.i_id;

                  UInt16 seq = packet.i_seq;

                  //allocate the byte array

                  byte[] packetArray = new byte[packet_size];

                  // now serialize the packet into the array.

                  int index = 0;

                  packetArray[index++] = packet.i_type;

                  packetArray[index++] = packet.i_code;

                  // the checksum is 16 bits.

                  byte[] temp = BitConverter.GetBytes(cksum);

                  // copy it into the packetArray at the required offset

                  Array.Copy(temp, 0, packetArray, index, temp.Length);

                  index += 2;

                  // similarly, copy the rest.

                  temp = BitConverter.GetBytes(id);

                  Array.Copy(temp, 0, packetArray, index, temp.Length);

                  index += 2;

                  // copy seq#

                  temp = BitConverter.GetBytes(seq);

                  Array.Copy(temp, 0, packetArray, index, temp.Length);

                  index += 2;

                  // copy payload

                  if (packet.PayloadLength > 0)

                  {

                        Array.Copy(packet.data, 0, packetArray, index, packet.PayloadLength);

                  }

                  return packetArray;

            }

      };

      class Program

      {

            static void Main(string[] args)

            {

                  //if (args.Length != 1)

                  //{

                  // Usage();

                  //}

                  //if (0 == String.Compare(args[0], "/?", true, CultureInfo.InvariantCulture)

                  //|| 0 == String.Compare(args[0], "-h", true, CultureInfo.InvariantCulture)

                  //|| 0 == String.Compare(args[0], "-?", true, CultureInfo.InvariantCulture))

                  //{

                  // Usage();

                  //}

                  byte [] data = new byte[32];

                  int j = 0;

                  for (int i = 0; i < 32; i++)

                  {

                        data[i] = (byte)((int)'a' + j);

                        j = (j + 1) % 23;

                  }

                  ICMP_PACKET packet = ICMP_PACKET.CreateRequestPacket(0x200, 0x9603, data);

                  byte[] serialized = packet.Serialize();

                  for (int i = 0; i < serialized.Length; i++)

                  {

                        Console.Write("{0:X} ", serialized[i]);

                  }

            }

            ...

      }

}

 

 

A couple of important points that I wanted to bring to your attention:

 

1) I have commented out the code which checks the validity of command-line parameters. While we are developing this application, we will sometimes be running this program with no command line arguments, so I didn’t want that code there. We will put it back in later.

2) As you might recall, the Platform/CPU stores data in a certain order. This is called Endianness. Intel (X86) platform, on which I am writing this program is Little-Endian. However it turns out that the byte-order on the network is always Big-Endian. Hence we need to convert multi-byte numbers from LittleEndian to Big-Endian before we send them on the network. Therefore, I have written the functions GetBytesInNetworkOrder() and ToNetworkOrder() to do the conversion from Little-Endian to Big-Endian.

3) The functions Serialize() and Checksum() are used to serialize the packet into a byte array, and calculate the checksum respectively. Note that as per the RFC, while calculating the checksum, we should assume that the i_cksum field of the packet structure has a value of zero. Once the checksum is calculated, we write the value into the structure, and then serialize the structure to get the final bytes on the wire.

4) In Part II of this series, I showed you the network trace of the Ping.exe routine that comes with the OS. In this part, I created a request packet with the same data, to make sure that our serialization routine and checksum routines are right. You can compile the program, and run it to verify.

 

 

D:\ping>bin\debug\ping.exe

8 0 B5 58 2 0 96 3 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 7

5 76 77 61 62 63 64 65 66 67 68 69

 

If you compare this output with the Ethereal trace of the actual Ping command that I ran in Part II, you will see that they match. This tells us that the Checksum and serialization routines that we wrote are correct.