Skip to content

Commit bd203a1

Browse files
test: add offline Windows ICMP loopback coverage
1 parent bf41d30 commit bd203a1

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,30 @@ if(BUILD_TESTING)
147147
target_link_options(WinMTRCoreTests PRIVATE /fsanitize=address)
148148
endif()
149149
add_test(NAME probe_scheduler COMMAND WinMTRCoreTests)
150+
151+
# The scheduled ASAN job intentionally builds only the pure core target.
152+
# Normal Debug/Release CI also exercises the real unprivileged Windows
153+
# ICMP API against loopback without depending on public network access.
154+
if(NOT WINMTR_ENABLE_ASAN)
155+
add_executable(WinMTRIcmpLoopbackTests
156+
"${CMAKE_CURRENT_SOURCE_DIR}/tests/WindowsIcmpLoopbackTests.cpp"
157+
)
158+
target_include_directories(WinMTRIcmpLoopbackTests PRIVATE
159+
"${CMAKE_CURRENT_SOURCE_DIR}"
160+
)
161+
target_compile_features(WinMTRIcmpLoopbackTests PRIVATE cxx_std_23)
162+
target_compile_options(WinMTRIcmpLoopbackTests PRIVATE
163+
/utf-8
164+
/W4
165+
/WX
166+
/sdl
167+
/permissive-
168+
/Zc:__cplusplus
169+
)
170+
set_property(TARGET WinMTRIcmpLoopbackTests PROPERTY
171+
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
172+
)
173+
target_link_libraries(WinMTRIcmpLoopbackTests PRIVATE iphlpapi ws2_32)
174+
add_test(NAME icmp_loopback COMMAND WinMTRIcmpLoopbackTests)
175+
endif()
150176
endif()

tests/WindowsIcmpLoopbackTests.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#define WIN32_LEAN_AND_MEAN
2+
#define NOMINMAX
3+
#include <WinSock2.h>
4+
#include <WS2tcpip.h>
5+
#include <Windows.h>
6+
7+
#include "WinMTRICMPPIOdef.h"
8+
9+
#include <algorithm>
10+
#include <array>
11+
#include <cstddef>
12+
#include <cstdint>
13+
#include <iostream>
14+
#include <stdexcept>
15+
#include <vector>
16+
17+
namespace {
18+
19+
void require(bool condition, const char* message)
20+
{
21+
if (!condition) throw std::runtime_error(message);
22+
}
23+
24+
class IcmpHandle final {
25+
public:
26+
explicit IcmpHandle(HANDLE value) noexcept : value_(value) {}
27+
~IcmpHandle() noexcept
28+
{
29+
if (value_ != nullptr && value_ != INVALID_HANDLE_VALUE) IcmpCloseHandle(value_);
30+
}
31+
IcmpHandle(const IcmpHandle&) = delete;
32+
IcmpHandle& operator=(const IcmpHandle&) = delete;
33+
[[nodiscard]] HANDLE get() const noexcept { return value_; }
34+
[[nodiscard]] explicit operator bool() const noexcept
35+
{
36+
return value_ != nullptr && value_ != INVALID_HANDLE_VALUE;
37+
}
38+
private:
39+
HANDLE value_;
40+
};
41+
42+
void test_ipv4_loopback()
43+
{
44+
IcmpHandle handle(IcmpCreateFile());
45+
require(static_cast<bool>(handle), "IcmpCreateFile failed for an unprivileged process");
46+
in_addr destination{};
47+
require(InetPtonW(AF_INET, L"127.0.0.1", &destination) == 1,
48+
"could not parse IPv4 loopback");
49+
std::array<std::uint8_t, 64> payload{};
50+
std::fill(payload.begin(), payload.end(), std::uint8_t{ 0x20 });
51+
IP_OPTION_INFORMATION options{};
52+
options.Ttl = 64;
53+
options.Tos = 0x2e;
54+
options.Flags = IP_FLAG_DF;
55+
std::vector<std::byte> reply(sizeof(ICMP_ECHO_REPLY) + payload.size() + 8u);
56+
SetLastError(ERROR_SUCCESS);
57+
const DWORD count = IcmpSendEcho2Ex(handle.get(), nullptr, nullptr, nullptr,
58+
ADDR_ANY, destination.S_un.S_addr, payload.data(),
59+
static_cast<WORD>(payload.size()), &options, reply.data(),
60+
static_cast<DWORD>(reply.size()), 1'000);
61+
require(count != 0, "IPv4 loopback probe returned no reply");
62+
// A nonzero synchronous return count means the API already populated parsed
63+
// ICMP_ECHO_REPLY records; IcmpParseReplies is only for async completion.
64+
const auto* parsed = reinterpret_cast<const ICMP_ECHO_REPLY*>(reply.data());
65+
require(parsed->Status == IP_SUCCESS, "IPv4 loopback reply was not successful");
66+
require(parsed->DataSize == payload.size(), "IPv4 loopback payload size changed");
67+
require(parsed->Data != nullptr
68+
&& std::equal(payload.begin(), payload.end(),
69+
static_cast<const std::uint8_t*>(parsed->Data)),
70+
"IPv4 loopback payload content changed");
71+
}
72+
73+
void test_ipv6_loopback()
74+
{
75+
IcmpHandle handle(Icmp6CreateFile());
76+
require(static_cast<bool>(handle), "Icmp6CreateFile failed for an unprivileged process");
77+
sockaddr_in6 source{};
78+
source.sin6_family = AF_INET6;
79+
sockaddr_in6 destination{};
80+
destination.sin6_family = AF_INET6;
81+
require(InetPtonW(AF_INET6, L"::1", &destination.sin6_addr) == 1,
82+
"could not parse IPv6 loopback");
83+
std::array<std::uint8_t, 64> payload{};
84+
std::fill(payload.begin(), payload.end(), std::uint8_t{ 0x20 });
85+
IP_OPTION_INFORMATION options{};
86+
options.Ttl = 64;
87+
options.Tos = 0x2e;
88+
options.Flags = 0; // DF is an IPv4-only option.
89+
std::vector<std::byte> reply(sizeof(ICMPV6_ECHO_REPLY) + sizeof(IO_STATUS_BLOCK)
90+
+ payload.size() + 8u);
91+
SetLastError(ERROR_SUCCESS);
92+
const DWORD count = Icmp6SendEcho2(handle.get(), nullptr, nullptr, nullptr,
93+
&source, &destination, payload.data(), static_cast<WORD>(payload.size()),
94+
&options, reply.data(), static_cast<DWORD>(reply.size()), 1'000);
95+
require(count != 0, "IPv6 loopback probe returned no reply");
96+
// As with IPv4, a nonzero synchronous return is already parsed.
97+
const auto* parsed = reinterpret_cast<const ICMPV6_ECHO_REPLY*>(reply.data());
98+
require(parsed->Status == IP_SUCCESS, "IPv6 loopback reply was not successful");
99+
}
100+
101+
} // namespace
102+
103+
int main()
104+
{
105+
try {
106+
test_ipv4_loopback();
107+
test_ipv6_loopback();
108+
std::cout << "Windows ICMP IPv4/IPv6 loopback tests passed.\n";
109+
return 0;
110+
}
111+
catch (const std::exception& error) {
112+
std::cerr << "Windows ICMP loopback test failure: " << error.what()
113+
<< " (Win32 " << GetLastError() << ")\n";
114+
return 1;
115+
}
116+
}

0 commit comments

Comments
 (0)