0

Sooo... I found an instance where the specified timeout with Ping.Send() or Ping.SendAsync do not adhere to the specified timeouts or TTLs and I am trying to work around it. When an invalid host is attempted to be pinged the specified timeout is ignored and in my case a full 2 seconds passes before the ping fails. For the project I am working on I need that to be something like 100 ms or less (if the host was valid it would only be a hop or 2 away so 100 ms seems plenty).

I've tried Ping.Send() and Ping.SendAsync and cannot for the life me figure out how to do this. The closest I got was doing a .Close on the AutoResetEvent but I couldn't catch/suppress the error that was created doing that. (Code not shown here)

Here is the current iteration of the code, as you can see I use both .Send and .SendAsync; I would prefer to use SendAsync I think... If you run the code below everything is executed in under 100ms (usually 60ms) for a host on the same network, but if the host in invalid the code doesn't exit till after 2000ms.

    //.NETCore.App\3.1.4
    // Microsoft Visual Studio Community 2019 - Version 16.5.5
    // Microsoft Windows [Version 10.0.18362.836]

using System;
using System.Text;
using System.Net.NetworkInformation;
using System.Threading;
using System.Text.RegularExpressions;

class Program
    {
        public static void Main(string[] args)
        {
            var watch = new System.Diagnostics.Stopwatch();
            watch.Start();


            string who = String.Empty;


            //who = "firewall0"; //Hostname of my local firewall
            //who = "www.yahoo.com";
            who = "not-a-valid-host";
            //who = "10.1.1.151"; //My interface
            //who = "localhost"; //Should succeed but error has to be suppressed because of IPv6
            //who = "2a03:2880:f131:83:face:b00c:0:25de"; //Facebook IPv6, should fail quickly
            //who = "127.0.0.1";

            string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            byte[] buffer = Encoding.ASCII.GetBytes(data);
            int timeout = 1;
            int numMaxHops = 1; //Supposedly this is TTL, but after some experimenting it appears to be number of hops not TTL in ms
            Ping pingSender = new Ping();
            PingOptions options = new PingOptions(numMaxHops, true);

            if (true) //Use false to toggle off this section of code while testing
            {
                AutoResetEvent waiter = new AutoResetEvent(false);
                pingSender.PingCompleted += new PingCompletedEventHandler(PingCompletedCallback);         
                Console.WriteLine("Time to live set to: {0}", options.Ttl);
                Console.WriteLine("");
                pingSender.SendAsync(who, timeout, buffer, options, waiter);
                waiter.WaitOne();
                Console.WriteLine("Ping example completed.");
            }

            if (true) //Use false to toggle off this section of code while testing
            {
                try
                {
                    PingReply reply = pingSender.Send(who, timeout, buffer, options);

                    if (reply.Status == IPStatus.Success)
                    {
                        Console.WriteLine("Address: {0}", reply.Address.ToString());
                        Console.WriteLine("RoundTrip time: {0}", reply.RoundtripTime);
                        if (reply.Address.ToString() != "::1")
                        {
                            Console.WriteLine("Time to live: {0}", reply.Options.Ttl);
                            Console.WriteLine("Don't fragment: {0}", reply.Options.DontFragment);
                            Console.WriteLine("Buffer size: {0}", reply.Buffer.Length);
                        }
                    }
                    else
                    {
                        Console.WriteLine(reply.Status);
                    }
                }
                catch (Exception ex)
                {
                    Regex noHostMatch = new Regex("No such host is known");
                    if (noHostMatch.IsMatch(ex.InnerException.ToString()))
                    //if (false)
                    {
                        Console.WriteLine("No such host is known.");
                    }
                    else
                    {
                        throw;
                    }
                }
            }

            watch.Stop();
            Console.WriteLine("");
            Console.WriteLine($"Execution Time: {watch.ElapsedMilliseconds} ms");
            Console.WriteLine("Press the Enter key");
            Console.ReadLine();
        }//End Main()

        private static void PingCompletedCallback(object sender, PingCompletedEventArgs e)
        {
            // If the operation was canceled, display a message to the user.

            if (e.Cancelled)
            {
                Console.WriteLine("Ping canceled.");

                // Let the main thread resume.
                // UserToken is the AutoResetEvent object that the main thread
                // is waiting for.
                ((AutoResetEvent)e.UserState).Set();
            }

            // If an error occurred, display the exception to the user.
            if (e.Error != null)
            {
                Console.WriteLine("Ping failed:");
                //Console.WriteLine(e.Error.ToString());

                // Let the main thread resume.
                ((AutoResetEvent)e.UserState).Set();
            }
            else
            {
                PingReply reply = e.Reply;
                DisplayReply(reply);

                // Let the main thread resume.
                ((AutoResetEvent)e.UserState).Set();
            }

        }//End PingCompletedCallback()

        public static void DisplayReply(PingReply reply)
        {
            if (reply == null)
                return;

            Console.WriteLine("Ping Status: {0}", reply.Status);
            if (reply.Status == IPStatus.Success)
            {
                Console.WriteLine("Address: {0}", reply.Address.ToString());
                Console.WriteLine("RoundTrip time: {0}", reply.RoundtripTime);

                if (reply.Address.ToString() != "::1")
                {
                    Console.WriteLine("Time to live: {0}", reply.Options.Ttl);
                    Console.WriteLine("Don't fragment: {0}", reply.Options.DontFragment);
                    Console.WriteLine("Buffer size: {0}", reply.Buffer.Length);
                }
            }
        }//End DisplayReply()

Out out from running the above while pinging my local firewall.

Time to live set to: 1

Ping Status: Success
Address: 10.1.1.1
RoundTrip time: 0
Time to live: 64
Don't fragment: False
Buffer size: 32
Ping example completed.
Address: 10.1.1.1
RoundTrip time: 0
Time to live: 64
Don't fragment: False
Buffer size: 32

Execution Time: 61 ms
Press the Enter key

Output when pinging not-a-valid-host

Time to live set to: 1

Ping failed:
Ping example completed.
No such host is known.

Execution Time: 2139 ms
Press the Enter key

^ I want to get that execution time down to under 100ms with an invalid host.

  • From [`PingOptions.Ttl` Property](https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.pingoptions.ttl?view=netcore-3.1): "An `Int32` value that specifies the number of times the `Ping` data packets can be forwarded. The default is 128." No mention of units of time, only forwarding, i.e. hops. You can create a separate timer and abandon the Ping if your code gets bored. – HABO May 30 '20 at 03:23
  • @HABO `PingOptions` doesn't, but `Ping`'s `SendAsync` does have a timeout value as its second argument in almost all of its overloads: "An `Int32` value that specifies the maximum number of milliseconds (after sending the echo message) to wait for the ICMP echo reply message." – Powerlord May 30 '20 at 03:47
  • The problem is not the ping but the DNS lookup. Your OS will try to resolve the hostname to an IP address before it can be used in the actual ping. You can check https://stackoverflow.com/questions/13248971/resolve-hostname-to-ip to see if it takes 2 seconds as well (on linux it fails immediately with an exception saying that the hostname is invalid). – Progman May 30 '20 at 07:09
  • @ HABO and @ Powerlord Thank you for your responses, I appreciate it. Unfortunately it appears that the timeout nor hops arguments get set if the resolver cannot resolve the host. @ Progman I suspected as much as the error that is received appears to happen outside of the Ping.Send. I'm going to check out Zeb Rawnsley's code now. – Jeff Goines May 30 '20 at 18:12

1 Answers1

0

Put the ping call inside a Task and Wait() up to your chosen timeout, after that has completed or timed out, we check to see if reply was set.

I've implemented this into your code below

//.NETCore.App\3.1.4
// Microsoft Visual Studio Community 2019 - Version 16.5.5
// Microsoft Windows [Version 10.0.18362.836]

using System;
using System.Text;
using System.Net.NetworkInformation;
using System.Threading;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

class Program
{
    public static void Main(string[] args)
    {
        var watch = new System.Diagnostics.Stopwatch();
        watch.Start();


        string who = String.Empty;


        //who = "firewall0"; //Hostname of my local firewall
        //who = "www.yahoo.com";
        who = "not-a-valid-host";
        //who = "10.1.1.151"; //My interface
        //who = "localhost"; //Should succeed but error has to be suppressed because of IPv6
        //who = "2a03:2880:f131:83:face:b00c:0:25de"; //Facebook IPv6, should fail quickly
        //who = "127.0.0.1";

        string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
        byte[] buffer = Encoding.ASCII.GetBytes(data);
        int timeout = 1;
        int numMaxHops = 1; //Supposedly this is TTL, but after some experimenting it appears to be number of hops not TTL in ms
        Ping pingSender = new Ping();
        PingOptions options = new PingOptions(numMaxHops, true);

        if (false) //Use false to toggle off this section of code while testing
        {
            AutoResetEvent waiter = new AutoResetEvent(false);
            pingSender.PingCompleted += new PingCompletedEventHandler(PingCompletedCallback);
            Console.WriteLine("Time to live set to: {0}", options.Ttl);
            Console.WriteLine("");
            pingSender.SendAsync(who, timeout, buffer, options, waiter);
            waiter.WaitOne();
            Console.WriteLine("Ping example completed.");
        }

        if (true) //Use false to toggle off this section of code while testing
        {
            try
            {
                PingReply reply = null;

            var t = Task.Run(() =>
            {
                reply = pingSender.Send(who, timeout, buffer, options);
                Console.WriteLine("Ping example completed.");
            });

            t.Wait(TimeSpan.FromMilliseconds(100));

            if (reply == null)
            {
                Console.WriteLine("timed out");
            }
            else if (reply.Status == IPStatus.Success)
            {
                Console.WriteLine("Address: {0}", reply.Address.ToString());
                Console.WriteLine("RoundTrip time: {0}", reply.RoundtripTime);
                if (reply.Address.ToString() != "::1")
                    {
                        Console.WriteLine("Time to live: {0}", reply.Options.Ttl);
                        Console.WriteLine("Don't fragment: {0}", reply.Options.DontFragment);
                        Console.WriteLine("Buffer size: {0}", reply.Buffer.Length);
                    }
                }
                else
                {
                    Console.WriteLine(reply.Status);
                }
            }
            catch (Exception ex)
            {
                Regex noHostMatch = new Regex("No such host is known");
                if (noHostMatch.IsMatch(ex.InnerException.ToString()))
                //if (false)
                {
                    Console.WriteLine("No such host is known.");
                }
                else
                {
                    throw;
                }
            }
        }

        watch.Stop();
        Console.WriteLine("");
        Console.WriteLine($"Execution Time: {watch.ElapsedMilliseconds} ms");
        Console.WriteLine("Press the Enter key");
        Console.ReadLine();
    }//End Main()

    private static void PingCompletedCallback(object sender, PingCompletedEventArgs e)
    {
        // If the operation was canceled, display a message to the user.

        if (e.Cancelled)
        {
            Console.WriteLine("Ping canceled.");

            // Let the main thread resume.
            // UserToken is the AutoResetEvent object that the main thread
            // is waiting for.
            ((AutoResetEvent)e.UserState).Set();
        }

        // If an error occurred, display the exception to the user.
        if (e.Error != null)
        {
            Console.WriteLine("Ping failed:");
            //Console.WriteLine(e.Error.ToString());

            // Let the main thread resume.
            ((AutoResetEvent)e.UserState).Set();
        }
        else
        {
            PingReply reply = e.Reply;
            DisplayReply(reply);

            // Let the main thread resume.
            ((AutoResetEvent)e.UserState).Set();
        }

    }//End PingCompletedCallback()

    public static void DisplayReply(PingReply reply)
    {
        if (reply == null)
            return;

        Console.WriteLine("Ping Status: {0}", reply.Status);
        if (reply.Status == IPStatus.Success)
        {
            Console.WriteLine("Address: {0}", reply.Address.ToString());
            Console.WriteLine("RoundTrip time: {0}", reply.RoundtripTime);

            if (reply.Address.ToString() != "::1")
            {
                Console.WriteLine("Time to live: {0}", reply.Options.Ttl);
                Console.WriteLine("Don't fragment: {0}", reply.Options.DontFragment);
                Console.WriteLine("Buffer size: {0}", reply.Buffer.Length);
            }
        }
    }//End DisplayReply()
}
Zeb Rawnsley
  • 2,210
  • 1
  • 21
  • 33
  • My apologies for the dumb question but does the task "t" have to be disposed afterward? If not, why not? (I'm obviously learning as I go here) – Jeff Goines Jun 03 '20 at 01:36
  • Oh, I forgot to mention I opted to use SendAsync in the task and not use the standard Ping.Send. I felt like SendAsync would be better if the team uses the function with GUI, but again I am learning. – Jeff Goines Jun 03 '20 at 01:42
  • @JeffGoines task doesn't implement `IDisposable` so once it is out of scope the garbage collector will automatically handle it – Zeb Rawnsley Jun 03 '20 at 18:35