c++winapivisual-c++windows-10-desktopwin64

WinAPI IcmpSendEcho on 64-bit platform


I am trying to send a ping request with the IcmpSendEcho function provided by Windows, as documented here.
For the most part, it seems straightforward, however, I am having trouble understanding how the caller-provided echo request buffer (LPVOID ReplyBuffer and DWORD ReplySize parameters) should be set up and used. The documentation states the following:

ReplyBuffer
[...]
A buffer to hold any replies to the echo request. Upon return, the buffer contains an array of ICMP_ECHO_REPLY structures followed by the options and data for the replies. The buffer should be large enough to hold at least one ICMP_ECHO_REPLY structure plus RequestSize bytes of data.
On a 64-bit platform, upon return the buffer contains an array of ICMP_ECHO_REPLY32 structures followed by the options and data for the replies.

and

ReplySize
[...]
The allocated size, in bytes, of the reply buffer. The buffer should be large enough to hold at least one ICMP_ECHO_REPLY structure plus RequestSize bytes of data. On a 64-bit platform, The buffer should be large enough to hold at least one ICMP_ECHO_REPLY32 structure plus RequestSize bytes of data.
This buffer should also be large enough to also hold 8 more bytes of data (the size of an ICMP error message).

I am developing on and targeting a 64-bit system and version of Windows, so my understanding from the documentation is that I need to use ICMP_ECHO_REPLY32 to calculate the buffer size, ie.:

ReplySize >= sizeof(ICMP_ECHO_REPLY32) + RequestSize + 8

However when testing this, the IcmpSendEcho always fails instantly with return value 0.
For RequestSize < 4, GetLastError() returns ERROR_INVALID_PARAMETER, which the documentation says indicates that "the ReplySize parameter specifies a value less than the size of an ICMP_ECHO_REPLY or ICMP_ECHO_REPLY32 structure".
For RequestSize >= 4, GetLastError returns an undocumented value that when parsed with GetIpErrorString() indicates "General failure".

I also see that the example code in the documentation excludes the 8 bytes in the reply buffer that is specified as required for an "ICMP error message" (and have seen speculation that 8 is incorrect for 64-bit platforms) - I am unsure if this affects the issue.

If I instead use ICMP_ECHO_REPLY for the ReplySize calculation, IcmpSendEcho no longer fails for any payload size, however it still seems unclear which the function actually uses internally. Ie., is ReplyBuffer written as an array of ICMP_ECHO_REPLY or ICMP_ECHO_REPLY32? Since once must access the buffer via casting the pointer type, using the wrong type could silently misalign and mangle all operations on it.

So how should the reply buffer be set up correctly? Should ICMP_ECHO_REPLY or ICMP_ECHO_REPLY32 be used?

This is the test code I have been working with:

#include <WS2tcpip.h>
#include <Windows.h>
#include <iphlpapi.h>
#include <IcmpAPI.h>

#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib, "Ws2_32.lib")

int main()
{
    // Create the ICMP context.
    HANDLE icmp_handle = IcmpCreateFile();
    if (icmp_handle == INVALID_HANDLE_VALUE) {
        throw;
    }

    // Parse the destination IP address.
    IN_ADDR dest_ip{};
    if (1 != InetPtonA(AF_INET, "<IP here>", &dest_ip)) {
        throw;
    }

    // Payload to send.
    constexpr WORD payload_size = 1;
    unsigned char payload[payload_size]{};

    // Reply buffer for exactly 1 echo reply, payload data, and 8 bytes for ICMP error message.
    constexpr DWORD reply_buf_size = sizeof(ICMP_ECHO_REPLY32) + payload_size + 8;
    unsigned char reply_buf[reply_buf_size]{};

    // Make the echo request.
    DWORD reply_count = IcmpSendEcho(icmp_handle, dest_ip.S_un.S_addr,
        payload, payload_size, NULL, reply_buf, reply_buf_size, 10000);

    // Return value of 0 indicates failure, try to get error info.
    if (reply_count == 0) {
        auto e = GetLastError();

        // Some documented error codes from IcmpSendEcho docs.
        switch (e) {
        case ERROR_INSUFFICIENT_BUFFER:
            throw;
        case ERROR_INVALID_PARAMETER:
            throw;
        case ERROR_NOT_ENOUGH_MEMORY:
            throw;
        case ERROR_NOT_SUPPORTED:
            throw;
        case IP_BUF_TOO_SMALL:
            throw;
        }

        // Try to get an error message for all other error codes.
        DWORD buf_size = 1000;
        WCHAR buf[1000];
        GetIpErrorString(e, buf, &buf_size);
        throw;
    }

    // Close ICMP context.
    IcmpCloseHandle(icmp_handle);
}

Solution

  • I believe the documentation to be wrong.

    If you look at what's actually in an ICMP_ECHO_REPLY32, you will find some pointers declared with the __ptr32 attribute, and there's some information about these guys here.

    However, when you actually dereference one of these, it turns out to be a bona-fide 64 bit pointer after all, which is not what it says in Hans' post. I guess things have changed since he wrote that (tested by me on Visual Studio 2017 just now).

    Anyway, go figure, but what is breaking your code is that sizeof for such a pointer returns 4, even in a 64 bit build, and this means that the total reported size of an ICMP_ECHO_REPLY32 structure is not large enough (with such a small payload) to hold even a single ICMP reply and so IcmpSendEcho fails with ERROR_INVALID_PARAMETER, as you have observed.

    SO, all you have to do to get your code to work is to use sizeof (ICMP_ECHO_REPLY) when setting up your request and then cast reply_buf to const ICMP_ECHO_REPLY * in order to interpret the results.

    Here is the entire code of my (working) test program, with some logging added (I stripped out some of your error handling for brevity):

    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    #include <WS2tcpip.h>
    #include <Windows.h>
    #include <iphlpapi.h>
    #include <IcmpAPI.h>
    
    #include <iostream>
    
    #pragma comment(lib, "Iphlpapi.lib")
    #pragma comment(lib, "Ws2_32.lib")
    
    int main()
    {
        // Create the ICMP context.
        HANDLE icmp_handle = IcmpCreateFile();
        if (icmp_handle == INVALID_HANDLE_VALUE) {
            throw;
        }
    
        // Parse the destination IP address.
        IN_ADDR dest_ip{};
        if (1 != InetPtonA(AF_INET, "89.238.162.170", &dest_ip)) {
            throw;
        }
    
        // Payload to send.
        constexpr WORD payload_size = 1;
        unsigned char payload[payload_size] { 42 };
    
        // Reply buffer for exactly 1 echo reply, payload data, and 8 bytes for ICMP error message.
        constexpr DWORD reply_buf_size = sizeof(ICMP_ECHO_REPLY) + payload_size + 8;
        unsigned char reply_buf[reply_buf_size]{};
    
        // Make the echo request.
        DWORD reply_count = IcmpSendEcho(icmp_handle, dest_ip.S_un.S_addr,
            payload, payload_size, NULL, reply_buf, reply_buf_size, 10000);
    
        // Return value of 0 indicates failure, try to get error info.
        if (reply_count == 0) {
            auto e = GetLastError();
            DWORD buf_size = 1000;
            WCHAR buf[1000];
            GetIpErrorString(e, buf, &buf_size);
            std::cout << "IcmpSendEcho returned error " << e << " (" << buf << ")" << std::endl;
            return 255;
        }
    
        const ICMP_ECHO_REPLY *r = (const ICMP_ECHO_REPLY *) reply_buf;
        struct in_addr addr;
        addr.s_addr = r->Address;
        char *s_ip = inet_ntoa (addr);
        std::cout << "Reply from: " << s_ip << ": bytes=" << r->DataSize << " time=" << r->RoundTripTime << "ms TTL=" << (int) r->Options.Ttl << std::endl;
    
        // Close ICMP context.
        IcmpCloseHandle(icmp_handle);
        return 0;
    }
    

    Run it at rextester. (That's the IP address of my website, did you see what I did there? :)

    Note for future visitors: This code does not check r->Status when IcmpSendEcho returns. But it should do. 0 = success, see the documentation.