64 位平台上的 WinAPI IcmpSendEcho

WinAPI IcmpSendEcho on 64-bit platform

我正在尝试使用 Windows 提供的 IcmpSendEcho 功能发送 ping 请求,如 here.
所述 在大多数情况下,它看起来很简单,但是,我无法理解应如何设置和使用调用者提供的回显请求缓冲区(LPVOID ReplyBufferDWORD ReplySize 参数)。文档说明如下:

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.

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).

我正在开发并针对 64 位系统和 Windows 版本,因此我从文档中了解到我需要使用 ICMP_ECHO_REPLY32 来计算缓冲区大小,即。 :

ReplySize >= sizeof(ICMP_ECHO_REPLY32) + RequestSize + 8

然而,在测试这个时,IcmpSendEcho 总是立即失败,return 值为 0。
对于 RequestSize < 4GetLastError() returns ERROR_INVALID_PARAMETER,文档说这表明 "the ReplySize parameter specifies a value less than the size of an ICMP_ECHO_REPLY or ICMP_ECHO_REPLY32 structure".
对于 RequestSize >= 4GetLastError returns 一个未记录的值,当用 GetIpErrorString() 解析时表示 "General failure"。

我还看到文档中的示例代码不包括 "ICMP error message" 所需的回复缓冲区中的 8 个字节(并且已经看到 speculation that 8 is incorrect for 64-bit platforms)- 我不确定是否这影响了这个问题。

如果我改为使用 ICMP_ECHO_REPLY 进行 ReplySize 计算,IcmpSendEcho 将不再因任何有效负载大小而失败,但似乎仍不清楚该函数在内部实际使用了哪个。即,ReplyBuffer 是写成 ICMP_ECHO_REPLY 还是 ICMP_ECHO_REPLY32 的数组?由于 once 必须通过强制转换指针类型来访问缓冲区,使用错误的类型可能会默默地错位并破坏其上的所有操作。

那么应该如何正确设置回复缓冲区呢?应该使用 ICMP_ECHO_REPLY 还是 ICMP_ECHO_REPLY32

这是我一直在使用的测试代码:

#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);
}

我认为文档有误。

如果您查看 和 ICMP_ECHO_REPLY32 中的实际内容,您会发现一些用 __ptr32 属性声明的指针,并且有一些关于这些的信息伙计们 here.

但是,当你实际上取消引用其中一个时,它毕竟是一个真正的 64 位指针,这不是它在 Hans 中所说的 post。我想自从他写了那篇文章后情况发生了变化(我刚刚在 Visual Studio 2017 年测试过)。

不管怎样,想想看,但破坏你的代码的是 sizeof 对于这样的指针 returns 4,即使是在 64 位构建中,这意味着 ICMP_ECHO_REPLY32 结构的总报告大小不足以容纳单个 ICMP 回复,因此 IcmpSendEcho 失败 ERROR_INVALID_PARAMETER,如您所见。

所以,要使代码正常工作,您要做的就是在设置请求时使用 sizeof (ICMP_ECHO_REPLY),然后将 reply_buf 转换为 const ICMP_ECHO_REPLY * 以解释结果。

这是我的(工作)测试程序的完整代码,添加了一些日志记录(为简洁起见,我删除了一些错误处理):

#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;
}

运行 它位于 rextester。 (那是我网站的 IP 地址,你看到我在那里做了什么吗?:)

未来访客请注意: 此代码在 IcmpSendEcho returns 时不检查 r->Status。但它应该这样做。 0 = 成功,参见 documentation.