Mobile Development: PingNG-the next generation ping

As I needed a tool to test the maximum MTU size for a network, I needed a ping tool where I can define the packet size and the DF (Do Not Fragment-Flag). As I did not find such a tool, I wrote PingNG.

main main2 options_df

It is a simple tool but the code had to avoid some pitfalls.

First, if you enter a host name, it has to be translated to an IPAddress before you can use an IcmpEchoReply. But GetHostEntry API call blocks. It will especially block very, very long if you enter an unknown host. If you want to offer a responsive GUI, we need a way to limit the time allowed to resolve the host name:

        bool checkHost(ref HostParms hostParms)
        {
            bool bRet = false;
            IPHostEntry ipHost = new IPHostEntry();
            try
            {
                IPHostEntry entry = null;
                int maxSeconds = hostParms.retry/* 10 */, counter = 0;
                // see http://jianmingli.com/wp/?p=22
                // Start the asynchronous request for DNS information. 
                // This example does not use a delegate or user-supplied object
                // so the last two arguments are null.
                IAsyncResult result = Dns.BeginGetHostEntry(hostParms.host/* ipAddress */, null, null);
                // Poll for completion information.
                while (result.IsCompleted != true && counter < maxSeconds)
                {
                    Thread.Sleep(1000);
                    counter++;
                }
                if (result.IsCompleted) //when we got here, the result is ready
                    entry = Dns.EndGetHostEntry(result);    //blocks?
                else
                    hostParms.isValid = false; // Thread.CurrentThread.Abort();
                hostParms.ipAddress = entry.AddressList[0];
                hostParms.isValid = true;
            }
            catch (ThreadAbortException e)
            {
                Thread.CurrentThread.Abort();// ("DNS failed within timeout", e);
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine(string.Format( "Exception in checkHost({0}): {1}", hostParms.host, e.Message));
            }

            return hostParms.isValid;
        }

The above checkHost function will be called by a thread and will either deliver a valid response or returns after a specified timeout.

As the IcmpReply API call itself blocks too, the ping is wrapped into a thread. The status is delivered to the GUI using delegates and event subscribers.

        //thread to do ping
        public void startPing(string sHost, PingOptions pOptions)
        {
            System.Diagnostics.Debug.WriteLine("+++thread started");
            string ipAddress = sHost;//make a local copy
            Ping myPing = new Ping();
            PingOptions myPingOptions = pOptions;//make a local copy
            
            int iTimeout = pOptions.TimeOut;
            int _numberOfPings = pOptions.numOfPings;
            bool doNotFragment = pOptions.DontFragment;
            int bufferSize = pOptions.bufferSize;
            byte[] buf = new byte[bufferSize];

            //long sumRoundtripTime=0;
            onReply(new PingReplyEventArgs("ping started...",PingReplyTypes.info));
            replyStats _replyStats = new replyStats(ipAddress);
            PingReply reply = null;
            try
            {
                onReply(new PingReplyEventArgs(pOptions.ToString(), PingReplyTypes.info));
                //check DNS first as this may block for long time
                IPAddress address;
                HostParms hostParms = new HostParms(ipAddress, 4);
                try
                {
                    //is IP address
                    address = IPAddress.Parse(ipAddress);
                    hostParms.isValid = true;
                }
                catch
                {
                    if (checkHost(ref hostParms))
                        ipAddress = hostParms.ipAddress.ToString();
                }
                if (!hostParms.isValid)
                    throw new PingUnknownHostException("Unkown host: " + ipAddress);

                for (int ix = 0; ix < _numberOfPings; ix++)
                {
                    reply = myPing.Send(ipAddress, buf, iTimeout, myPingOptions);
                    string sReply = "";
                    if (reply.Status == IPStatus.Success)
                    {
                        //sumRoundtripTime += reply.RoundTripTime;
                        _replyStats.add(1, 1, reply.RoundTripTime);
                        sReply = myResources.getReply(reply, ipAddress);
                    }
                    else if (reply.Status == IPStatus.DestinationHostUnreachable)
                    {
                        _replyStats.add(1, 0, reply.RoundTripTime);
                        throw new PingUnknownHostException("Destination unreachable");
                    }
                    else
                    {                        
                        _replyStats.add(1, 0, reply.RoundTripTime);
                        sReply = myResources.getReply(reply, ipAddress);
                    }
                    System.Diagnostics.Debug.WriteLine(sReply);
                    onReply(new PingReplyEventArgs(sReply));
                }
                onReply(new PingReplyEventArgs(_replyStats.ToString(), PingReplyTypes.info));
            }
...

The small tool could be extended to do automatic sweeps over varying IP addresses or with varying packet sizes.

If you specify a packet size that is beyond the MTU and set the DF flag you will get the correct answer:

ping started...
ttl=128, DF=True, buf=1480, #p=4, timeout=1000
IPStatus.PacketTooBig
IPStatus.PacketTooBig
IPStatus.PacketTooBig
IPStatus.PacketTooBig
Ping statistics for 192.168.128.5:
    Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms

done

By reducing the packet size you will get a working ping:

ping started...
ttl=128, DF=True, buf=1472, #p=4, timeout=1000
Reply from 192.168.128.5: bytes=1472 time=8ms TTL=128
Reply from 192.168.128.5: bytes=1472 time=5ms TTL=128
Reply from 192.168.128.5: bytes=1472 time=4ms TTL=128
Reply from 192.168.128.5: bytes=1472 time=6ms TTL=128
Ping statistics for 192.168.128.5:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 4ms, Maximum = 8ms, Average = 5ms

done

The ICMP reply has a lot of possible result codes. PingNG only evaluates some of them in depth. To display the statistics, I created a new class

    public class replyStats
    {
        public int sent=0;
        public int rcvd=0;
        public int percentLost
        {
            get
            {
                if (sent != 0)
                    return ((lost * 100) / sent);
                else
                    return 100;
            }
        }
        public long minTrip=0;
        public long maxTrip=0;
        public long avgTrip{
            get
            {
                if (sent == 0)
                    return 0;
                if (tripValues.Count == 0)
                    return 0;
                int sum = 0;
                foreach (int v in tripValues)
                    sum += v;
                int avg = sum / tripValues.Count;
                return avg;
            }
        }
        List<long> tripValues=new List<long>();
        string ipAddress = "";
        public int lost
        {
            get
            {
                return sent - rcvd;
            }
        }
        public replyStats(string sIPaddress){
            ipAddress = sIPaddress;
            minTrip = long.MaxValue;
            maxTrip = 0;
        }
        public void add(int iSent, int iRcvd, long lTrip)
        {
            if (lTrip < minTrip)
                minTrip = lTrip;
            if (lTrip > maxTrip)
                maxTrip = lTrip;
            tripValues.Add(lTrip);
            sent += iSent;
            rcvd += iRcvd;
        }
        public override string ToString()
        {
            string s = string.Format(ReplyStats, ipAddress,
                sent, rcvd, lost, percentLost,
                minTrip, maxTrip, avgTrip);
            return s;
        }
        const string ReplyStats = "Ping statistics for {0}:\r\n" +
            "    Packets: Sent = {1}, Received = {2}, Lost = {3} ({4}% loss),\r\n" +
            "Approximate round trip times in milli-seconds:\r\n" +
            "    Minimum = {5}ms, Maximum = {6}ms, Average = {7}ms\r\n";
    }

This class can report the stats if you add the single ICMP results on every return. For example, for a successful ping:

_replyStats.add(1, 1, reply.RoundTripTime);

There are still many enhancements possible. The project is done in VS2008 using Compact Framework 2.0 and targets Windows CE and will run nice on Windows Mobile too.

The code is hosted at code.google.com (discontinued, now at github) as part of my win-mobile-code repo.