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.
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, counter = 0;
IAsyncResult result = Dns.BeginGetHostEntry(hostParms.host, null, null);
while (result.IsCompleted != true && counter < maxSeconds)
{
Thread.Sleep(1000);
counter++;
}
if (result.IsCompleted)
entry = Dns.EndGetHostEntry(result);
else
hostParms.isValid = false;
hostParms.ipAddress = entry.AddressList[0];
hostParms.isValid = true;
}
catch (ThreadAbortException e)
{
Thread.CurrentThread.Abort();
}
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.
public void startPing(string sHost, PingOptions pOptions)
{
System.Diagnostics.Debug.WriteLine("+++thread started");
string ipAddress = sHost;
Ping myPing = new Ping();
PingOptions myPingOptions = pOptions;
int iTimeout = pOptions.TimeOut;
int _numberOfPings = pOptions.numOfPings;
bool doNotFragment = pOptions.DontFragment;
int bufferSize = pOptions.bufferSize;
byte[] buf = new byte[bufferSize];
onReply(new PingReplyEventArgs("ping started...",PingReplyTypes.info));
replyStats _replyStats = new replyStats(ipAddress);
PingReply reply = null;
try
{
onReply(new PingReplyEventArgs(pOptions.ToString(), PingReplyTypes.info));
IPAddress address;
HostParms hostParms = new HostParms(ipAddress, 4);
try
{
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)
{
_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 as part of my win-mobile-code repo.