-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
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