Skip to content

Ping RoundtripTime is Wrong #118150

@shapea

Description

@shapea

Description

With most ICMP ping implementations, both Success and TTL Expired typically return RTT in the reply.

However, the .NET Ping wrapping implementation treats ONLY Success as a valid case to return RTT, which doesn't even match the methods it pinvokes into under the hood.

For example, the Windows implementation of .NET Ping wraps IcmpSendEcho2 using ICMP_ECHO_REPLY replies. If you use this API directly in C/C++, the ICMP_ECHO_REPLY will include an RTT any time status = 11013 (i.e. IP_TTL_EXPIRED_TRANSIT). However, the .NET Ping wrapper completely ignores this and simply returns 0 because the status isn't Success.

This is clearly an implementation bug and greatly weakens the .NET implementation while using PingOptions.Ttl for trace scenarios, making Ping overall much less useful. For example, many internet routers will return with a TTL Expired but will NOT respond with an ICMP Reply if pinged directly, running into a situation where you can determine the route but not actually determine the RTT to each hop along the route.

Consider the following C++ (which is only provided as example, not meant to be super pretty):

#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")

#include <WinSock2.h>
#include <WS2tcpip.h>
#include <Windows.h>
#include <iphlpapi.h>
#include <IcmpAPI.h>
#include <memory>
#include <array>

int main(int, char**)
{
    // 1.1.1.1
    auto destination = sockaddr_in { AF_INET, 0, { 1, 1, 1, 1 } };

    auto handle = IcmpCreateFile();
    if (handle != INVALID_HANDLE_VALUE)
    {
        char request[] = "cn78bva87hv987aWEvha3w898a293tg";
        auto buffer = std::array<std::byte, sizeof(ICMP_ECHO_REPLY) + sizeof(request) + 8> {};

        auto options = IP_OPTION_INFORMATION { 0, 0, IP_FLAG_DF };
        for (UCHAR i = 1; i != 32; ++i)
        {
            options.Ttl = i;

            auto result = IcmpSendEcho2(handle, nullptr, nullptr, nullptr, destination.sin_addr.S_un.S_addr, request, sizeof(request), &options, buffer.data(), (DWORD)buffer.size(), 1000);

            if (result != 0)
            {
                auto* response = reinterpret_cast<ICMP_ECHO_REPLY*>(buffer.data());

                auto source = sockaddr_in { };
                source.sin_addr.S_un.S_addr = response->Address;

                printf_s("%d: status=%u address=%d.%d.%d.%d rtt=%u\r\n",
                  i,
                  response->Status,
                  source.sin_addr.S_un.S_un_b.s_b1,
                  source.sin_addr.S_un.S_un_b.s_b2,
                  source.sin_addr.S_un.S_un_b.s_b3,
                  source.sin_addr.S_un.S_un_b.s_b4,
                  response->RoundTripTime);

                if (response->Status == 0)
                {
                    break;
                }
            }
        }

        IcmpCloseHandle(handle);
    }

    return 0;
}

For me, this results in something like:

1: status=11013 address=10.30.124.2 rtt=25
2: status=11013 address=10.37.81.138 rtt=29
3: status=11013 address=10.37.35.125 rtt=29
4: status=11013 address=10.200.174.66 rtt=38
5: status=11013 address=10.200.174.162 rtt=34
6: status=11013 address=131.107.6.130 rtt=33
7: status=11013 address=131.107.5.114 rtt=32
8: status=11013 address=131.107.200.82 rtt=51
9: status=11013 address=207.46.36.105 rtt=45
10: status=11013 address=104.44.36.76 rtt=72
11: status=11013 address=108.162.243.51 rtt=50
12: status=0 address=1.1.1.1 rtt=60

Now if we do the same thing using Ping in .NET (which wraps the SAME API under Windows):

namespace PingTest
{
    using System;
    using System.Net;
    using System.Net.NetworkInformation;
    using System.Text;
    using System.Threading.Tasks;

    public static class Program
    {
        static async Task Main(string[] args)
        {
            var ping = new Ping();
            var options = new PingOptions(1, true);
            var destination = IPAddress.Parse("1.1.1.1");
            var buffer = Encoding.ASCII.GetBytes("cn78bva87hv987aWEvha3w898a293tg");

            for (int i = 1; i != 32; ++i)
            {
                options.Ttl = i;
                var response = await ping.SendPingAsync(destination, 1000, buffer, options);

                Console.WriteLine("{0}: status={1} address={2} rtt={3}", i, response.Status, response.Address, response.RoundtripTime);

                if (response.Status == IPStatus.Success)
                {
                    break;
                }
            }
       }
     }
}

This results in an output such as:

1: status=TtlExpired address=10.30.124.2 rtt=0
2: status=TtlExpired address=10.37.81.138 rtt=0
3: status=TtlExpired address=10.37.35.125 rtt=0
4: status=TtlExpired address=10.200.174.66 rtt=0
5: status=TtlExpired address=10.200.174.162 rtt=0
6: status=TtlExpired address=131.107.6.130 rtt=0
7: status=TtlExpired address=131.107.5.114 rtt=0
8: status=TtlExpired address=131.107.200.82 rtt=0
9: status=TtlExpired address=207.46.36.105 rtt=0
10: status=TtlExpired address=104.44.36.76 rtt=0
11: status=TtlExpired address=108.162.243.51 rtt=0
12: status=Success address=1.1.1.1 rtt=42

Even though the .NET Ping is calling the same API.

Reproduction Steps

See the code in the issue description. This can be faithfully reproduced with any TtlExpired reply in the .NET Ping implementation.

Expected behavior

The expectation is for the RTT to be set properly the same as the underlying APIs being used for TTL Expired ICMP replies, rather than dropping the value and returning 0.

Actual behavior

For any non-Success reply, Ping always ignores API values and returns 0 RTT, even for TTL Expired.

Regression?

No response

Known Workarounds

The only workaround until this is fixed is manually pInvoking into the same APIs.

Configuration

This was only tested under Windows, but the same expectation is held for other OS's.

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions