#include "icmphook_net.h" #include "util/detour.h" #include "util/logging.h" #include #include #include #include #include #include #include #include #include void icmphook_iphlpapi_install(); #ifndef SPICE64 using socket_uint = unsigned int; #else using socket_uint = unsigned long long; #endif namespace icmphook_internal { constexpr uint32_t k_loopback_host = 0x7f000001u; #pragma pack(push, 1) struct IcmpHeaderView { uint8_t type; uint8_t code; uint16_t checksum_be; }; #pragma pack(pop) bool icmp_packet_valid(const uint8_t *icmp, size_t len) { if (len < sizeof(IcmpHeaderView)) { return false; } unsigned sum = 0; for (size_t i = 0; i + 1 < len; i += 2) { sum += (static_cast(icmp[i]) << 8) | icmp[i + 1]; sum = (sum >> 16) + (sum & 0xFFFF); } if (len & 1) { sum += static_cast(icmp[len - 1]) << 8; sum = (sum >> 16) + (sum & 0xFFFF); } sum = (sum >> 16) + (sum & 0xFFFF); return sum == 0xFFFFu || sum == 0; } bool icmp_build_echo_reply(std::vector &out, const uint8_t *req, size_t req_len) { if (req_len < sizeof(IcmpHeaderView)) { return false; } out.resize(req_len); memcpy(out.data(), req, req_len); auto *hdr = reinterpret_cast(out.data()); hdr->type = 0; hdr->code = 0; hdr->checksum_be = 0; unsigned sum = 0; for (size_t i = 0; i + 1 < out.size(); i += 2) { sum += (static_cast(out[i]) << 8) | out[i + 1]; sum = (sum >> 16) + (sum & 0xFFFF); } if (out.size() & 1) { sum += static_cast(out[out.size() - 1]) << 8; sum = (sum >> 16) + (sum & 0xFFFF); } sum = (sum >> 16) + (sum & 0xFFFF); uint16_t csum = static_cast(~sum); hdr->checksum_be = htons(csum); return true; } bool icmp_fix_pointer( const uint8_t *base, size_t total_len, const uint8_t **icmp_start, size_t *icmp_len) { if (total_len >= 20 && (base[0] >> 4) == 4) { size_t ihl = (base[0] & 0x0Fu) * 4; if (ihl < 20 || ihl > total_len || base[9] != IPPROTO_ICMP) { return false; } *icmp_start = base + ihl; *icmp_len = total_len - ihl; return true; } *icmp_start = base; *icmp_len = total_len; return true; } bool ipv4_encode_datagram( std::vector &out, uint32_t src_host_order, uint32_t dst_host_order, const uint8_t *payload, size_t payload_len) { const size_t hdr_len = 20; if (payload_len > 0xFFFFu - hdr_len) { return false; } const auto total_len = static_cast(hdr_len + payload_len); out.resize(hdr_len + payload_len); uint8_t *p = out.data(); memset(p, 0, hdr_len); p[0] = 0x45; p[1] = 0; p[2] = static_cast(total_len >> 8); p[3] = static_cast(total_len & 0xFF); p[6] = 0x40; p[8] = 1; p[9] = IPPROTO_ICMP; uint32_t src_wire = htonl(src_host_order); uint32_t dst_wire = htonl(dst_host_order); memcpy(p + 12, &src_wire, 4); memcpy(p + 16, &dst_wire, 4); unsigned sum = 0; for (size_t i = 0; i < hdr_len; i += 2) { sum += (static_cast(p[i]) << 8) | p[i + 1]; while (sum >> 16) { sum = (sum & 0xFFFF) + (sum >> 16); } } uint16_t hdr_csum = static_cast(~sum); p[10] = static_cast(hdr_csum >> 8); p[11] = static_cast(hdr_csum & 0xFF); memcpy(p + hdr_len, payload, payload_len); return true; } // Cached adapter list. Populated once per process: adapter changes after the // first ICMP send are intentionally not picked up, in exchange for avoiding // repeated IP Helper calls on every emulated send. std::once_flag g_adapters_cache_init; std::vector g_adapters_cache; void fill_adapters_cache() { std::call_once(g_adapters_cache_init, []() { ULONG buf_len = 0; DWORD r = GetAdaptersInfo(nullptr, &buf_len); if (r != ERROR_BUFFER_OVERFLOW && r != ERROR_SUCCESS) { return; } if (buf_len == 0) { return; } g_adapters_cache.resize(buf_len); auto *info = reinterpret_cast(g_adapters_cache.data()); if (GetAdaptersInfo(info, &buf_len) != ERROR_SUCCESS) { g_adapters_cache.clear(); } }); } uint32_t local_ipv4_for_peer( uint32_t peer_host_order, uint32_t bind_pref_host_order, bool has_bind) { if (peer_host_order == k_loopback_host) { return k_loopback_host; } if (has_bind && bind_pref_host_order != 0) { return bind_pref_host_order; } fill_adapters_cache(); if (g_adapters_cache.empty()) { return peer_host_order; } auto *info = reinterpret_cast(g_adapters_cache.data()); for (PIP_ADAPTER_INFO a = info; a != nullptr; a = a->Next) { uint32_t ip = ntohl(inet_addr(a->IpAddressList.IpAddress.String)); uint32_t mask = ntohl(inet_addr(a->IpAddressList.IpMask.String)); if (mask == 0) { continue; } if ((peer_host_order & mask) == (ip & mask)) { return ip; } } return peer_host_order; } struct EmuSock { std::deque> queue; std::condition_variable_any cv; DWORD rx_timeout_ms = INFINITE; bool nonblocking = false; bool has_bind = false; uint32_t bind_ip_host = 0; }; std::recursive_mutex g_mu; std::unordered_map> g_socks; std::atomic g_seq{1}; // Fast-path flag so the bind() hook in networkhook.cpp can short-circuit // without taking g_mu when the ICMP feature was never enabled. std::atomic g_installed{false}; decltype(socket) *socket_orig = nullptr; decltype(WSASocketW) *WSASocketW_orig = nullptr; decltype(WSASocketA) *WSASocketA_orig = nullptr; decltype(closesocket) *closesocket_orig = nullptr; decltype(bind) *bind_trampoline_orig = nullptr; decltype(sendto) *sendto_orig = nullptr; decltype(recvfrom) *recvfrom_orig = nullptr; decltype(WSASendTo) *WSASendTo_orig = nullptr; decltype(WSARecvFrom) *WSARecvFrom_orig = nullptr; decltype(ioctlsocket) *ioctlsocket_orig = nullptr; decltype(setsockopt) *setsockopt_orig = nullptr; EmuSock *lookup(SOCKET s) { std::lock_guard lock(g_mu); auto it = g_socks.find(s); if (it == g_socks.end()) { return nullptr; } return it->second.get(); } SOCKET alloc_icmp_socket() { std::lock_guard lock(g_mu); for (;;) { uint32_t x = g_seq.fetch_add(1, std::memory_order_relaxed); SOCKET s = (SOCKET)(socket_uint)(0xE0000000u | (x & 0x0FFFFFFFu)); if (s == INVALID_SOCKET) { continue; } if (g_socks.find(s) == g_socks.end()) { g_socks[s] = std::make_unique(); return s; } } } void release_icmp_socket(SOCKET s) { std::lock_guard lock(g_mu); auto it = g_socks.find(s); if (it != g_socks.end()) { it->second->cv.notify_all(); g_socks.erase(it); } } bool process_icmp_send(SOCKET s, const uint8_t *buf, int len, const sockaddr *to, int tolen) { if (len <= 0 || tolen < (int) sizeof(sockaddr_in) || !buf || !to) { return true; } auto *sin = reinterpret_cast(to); if (sin->sin_family != AF_INET) { return true; } uint32_t peer_host = ntohl(sin->sin_addr.s_addr); const uint8_t *icmp_ptr = nullptr; size_t icmp_len = 0; const auto *raw = reinterpret_cast(buf); if (!icmp_fix_pointer(raw, static_cast(len), &icmp_ptr, &icmp_len)) { return true; } if (!icmp_packet_valid(icmp_ptr, icmp_len)) { return true; } if (icmp_len < sizeof(IcmpHeaderView)) { return true; } const auto *ih = reinterpret_cast(icmp_ptr); if (ih->type != 8 || ih->code != 0) { return true; } std::vector icmp_reply; if (!icmp_build_echo_reply(icmp_reply, icmp_ptr, icmp_len)) { return true; } std::vector packet; { std::lock_guard lock(g_mu); auto it = g_socks.find(s); if (it == g_socks.end()) { return true; } EmuSock *es = it->second.get(); uint32_t local_host = local_ipv4_for_peer(peer_host, es->bind_ip_host, es->has_bind); if (!ipv4_encode_datagram( packet, peer_host, local_host, icmp_reply.data(), icmp_reply.size())) { return true; } es->queue.push_back(std::move(packet)); es->cv.notify_all(); } return true; } SOCKET WINAPI socket_hook(int af, int type, int protocol) { if (af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP) { SOCKET s = alloc_icmp_socket(); log_info("network", "ICMP emulation: allocated raw socket {}", (socket_uint) s); return s; } return socket_orig(af, type, protocol); } SOCKET WINAPI WSASocketW_hook( int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags) { const bool is_raw_icmp = af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP; if (lpProtocolInfo == nullptr && is_raw_icmp) { SOCKET s = alloc_icmp_socket(); log_info("network", "ICMP emulation: allocated raw WSASocketW {}", (socket_uint) s); return s; } return WSASocketW_orig(af, type, protocol, lpProtocolInfo, g, dwFlags); } SOCKET WINAPI WSASocketA_hook( int af, int type, int protocol, LPWSAPROTOCOL_INFOA lpProtocolInfo, GROUP g, DWORD dwFlags) { const bool is_raw_icmp = af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP; if (lpProtocolInfo == nullptr && is_raw_icmp) { SOCKET s = alloc_icmp_socket(); log_info("network", "ICMP emulation: allocated raw WSASocketA {}", (socket_uint) s); return s; } return WSASocketA_orig(af, type, protocol, lpProtocolInfo, g, dwFlags); } int WINAPI closesocket_hook(SOCKET s) { if (lookup(s)) { release_icmp_socket(s); return 0; } return closesocket_orig(s); } int WINAPI bind_hook_ws2(SOCKET s, const sockaddr *name, int namelen) { { std::lock_guard lock(g_mu); auto it = g_socks.find(s); if (it != g_socks.end() && name && namelen >= (int) sizeof(sockaddr_in)) { auto *in = reinterpret_cast(name); if (in->sin_family != AF_INET) { WSASetLastError(WSAEAFNOSUPPORT); return SOCKET_ERROR; } it->second->has_bind = true; it->second->bind_ip_host = ntohl(in->sin_addr.s_addr); return 0; } } return bind_trampoline_orig(s, name, namelen); } int WINAPI sendto_hook( SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen) { EmuSock *es = lookup(s); if (!es) { return sendto_orig(s, buf, len, flags, to, tolen); } if (!buf || !to) { WSASetLastError(WSAEFAULT); return SOCKET_ERROR; } process_icmp_send(s, reinterpret_cast(buf), len, to, tolen); return len; } int WINAPI WSASendTo_hook( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo, int iTolen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) { EmuSock *es = lookup(s); if (!es) { return WSASendTo_orig( s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpTo, iTolen, lpOverlapped, lpCompletionRoutine); } if (lpOverlapped != nullptr) { WSASetLastError(WSAEOPNOTSUPP); return SOCKET_ERROR; } if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesSent) { WSASetLastError(WSAEINVAL); return SOCKET_ERROR; } int len = (int) lpBuffers[0].len; process_icmp_send( s, reinterpret_cast(lpBuffers[0].buf), len, lpTo, iTolen); *lpNumberOfBytesSent = static_cast(len); return 0; } // Dequeue one datagram for an emulated ICMP socket into the caller-provided // buffer. If out_full_dgram_bytes is non-null, it receives the full packet // size (before truncation to len) so callers can signal WSAEMSGSIZE. int emu_recv_dequeue( SOCKET s, char *buf, int len, sockaddr *from, int *fromlen, size_t *out_full_dgram_bytes) { if (!buf || len <= 0) { WSASetLastError(WSAEINVAL); return SOCKET_ERROR; } std::vector pkt; { std::unique_lock lock(g_mu); auto it = g_socks.find(s); if (it == g_socks.end()) { WSASetLastError(WSAENOTSOCK); return SOCKET_ERROR; } auto wait_pred = [&] { auto j = g_socks.find(s); return j == g_socks.end() || !j->second->queue.empty(); }; EmuSock *es = it->second.get(); if (es->nonblocking) { if (es->queue.empty()) { WSASetLastError(WSAEWOULDBLOCK); return SOCKET_ERROR; } } else if (es->rx_timeout_ms == INFINITE || es->rx_timeout_ms == 0) { it->second->cv.wait(lock, wait_pred); } else { const auto timeout = std::chrono::milliseconds(es->rx_timeout_ms); if (!it->second->cv.wait_for(lock, timeout, wait_pred)) { WSASetLastError(WSAETIMEDOUT); return SOCKET_ERROR; } } it = g_socks.find(s); if (it == g_socks.end()) { WSASetLastError(WSAENOTSOCK); return SOCKET_ERROR; } es = it->second.get(); if (es->queue.empty()) { WSASetLastError(WSAENOTSOCK); return SOCKET_ERROR; } pkt = std::move(es->queue.front()); es->queue.pop_front(); } if (out_full_dgram_bytes) { *out_full_dgram_bytes = pkt.size(); } const int take = (len < (int) pkt.size()) ? len : (int) pkt.size(); memcpy(buf, pkt.data(), static_cast(take)); if (from && fromlen && *fromlen >= (int) sizeof(sockaddr_in)) { auto *out = reinterpret_cast(from); memset(out, 0, sizeof(*out)); out->sin_family = AF_INET; if (pkt.size() >= 20) { memcpy(&out->sin_addr, pkt.data() + 12, 4); } else { out->sin_addr.s_addr = htonl(k_loopback_host); } *fromlen = sizeof(sockaddr_in); } return take; } int WINAPI recvfrom_hook( SOCKET s, char *buf, int len, int flags, sockaddr *from, int *fromlen) { if (!lookup(s)) { return recvfrom_orig(s, buf, len, flags, from, fromlen); } return emu_recv_dequeue(s, buf, len, from, fromlen, nullptr); } int WINAPI WSARecvFrom_hook( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) { if (!lookup(s)) { return WSARecvFrom_orig( s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpFrom, lpFromlen, lpOverlapped, lpCompletionRoutine); } if (lpOverlapped != nullptr) { WSASetLastError(WSAEOPNOTSUPP); return SOCKET_ERROR; } if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesRecvd) { WSASetLastError(WSAEINVAL); return SOCKET_ERROR; } sockaddr_in from_tmp{}; int fromlen_tmp = sizeof(from_tmp); size_t full_bytes = 0; int r = emu_recv_dequeue( s, lpBuffers[0].buf, (int) lpBuffers[0].len, reinterpret_cast(&from_tmp), &fromlen_tmp, &full_bytes); if (r == SOCKET_ERROR) { return SOCKET_ERROR; } if (full_bytes > lpBuffers[0].len) { WSASetLastError(WSAEMSGSIZE); return SOCKET_ERROR; } *lpNumberOfBytesRecvd = static_cast(r); if (lpFlags) { *lpFlags = 0; } if (lpFrom && lpFromlen && *lpFromlen >= (int) sizeof(sockaddr_in)) { *reinterpret_cast(lpFrom) = from_tmp; *lpFromlen = sizeof(sockaddr_in); } return 0; } int WINAPI ioctlsocket_hook(SOCKET s, long cmd, u_long *argp) { { std::lock_guard lock(g_mu); auto it = g_socks.find(s); if (it != g_socks.end()) { EmuSock *es = it->second.get(); if (argp && static_cast(cmd) == FIONBIO) { es->nonblocking = (*argp != 0); return 0; } WSASetLastError(WSAEOPNOTSUPP); return SOCKET_ERROR; } } return ioctlsocket_orig(s, cmd, argp); } int WINAPI setsockopt_hook( SOCKET s, int level, int optname, const char *optval, int optlen) { { std::lock_guard lock(g_mu); auto it = g_socks.find(s); if (it != g_socks.end()) { const bool deref_optval = level == SOL_SOCKET && optlen >= (int) sizeof(DWORD); if (deref_optval && optval) { EmuSock *es = it->second.get(); auto v = reinterpret_cast(optval); if (optname == SO_RCVTIMEO) { DWORD ms = *v; es->rx_timeout_ms = (ms == 0) ? INFINITE : ms; return 0; } if (optname == SO_SNDTIMEO) { return 0; } WSASetLastError(WSAEOPNOTSUPP); return SOCKET_ERROR; } if (!deref_optval) { WSASetLastError(WSAEOPNOTSUPP); return SOCKET_ERROR; } } } return setsockopt_orig(s, level, optname, optval, optlen); } void install_icmphook_hooks() { static bool done = false; if (done) { return; } done = true; bool ok = true; ok &= detour::trampoline_try( "ws2_32.dll", "socket", (void *) socket_hook, (void **) &socket_orig); ok &= detour::trampoline_try( "ws2_32.dll", "WSASocketW", (void *) WSASocketW_hook, (void **) &WSASocketW_orig); ok &= detour::trampoline_try( "ws2_32.dll", "WSASocketA", (void *) WSASocketA_hook, (void **) &WSASocketA_orig); ok &= detour::trampoline_try( "ws2_32.dll", "closesocket", (void *) closesocket_hook, (void **) &closesocket_orig); ok &= detour::trampoline_try( "ws2_32.dll", "bind", (void *) bind_hook_ws2, (void **) &bind_trampoline_orig); ok &= detour::trampoline_try( "ws2_32.dll", "sendto", (void *) sendto_hook, (void **) &sendto_orig); ok &= detour::trampoline_try( "ws2_32.dll", "recvfrom", (void *) recvfrom_hook, (void **) &recvfrom_orig); ok &= detour::trampoline_try( "ws2_32.dll", "WSASendTo", (void *) WSASendTo_hook, (void **) &WSASendTo_orig); ok &= detour::trampoline_try( "ws2_32.dll", "WSARecvFrom", (void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig); ok &= detour::trampoline_try( "ws2_32.dll", "ioctlsocket", (void *) ioctlsocket_hook, (void **) &ioctlsocket_orig); ok &= detour::trampoline_try( "ws2_32.dll", "setsockopt", (void *) setsockopt_hook, (void **) &setsockopt_orig); if (!ok) { log_warning( "network", "ICMP emulation: one or more ws2_32 hooks failed to install"); } else { log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)"); } g_installed.store(true, std::memory_order_release); } } // namespace icmphook_internal bool icmphook_is_emulated_socket(SOCKET s) { if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) { return false; } std::lock_guard lock(icmphook_internal::g_mu); return icmphook_internal::g_socks.find(s) != icmphook_internal::g_socks.end(); } bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result) { if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) { return false; } std::lock_guard lock(icmphook_internal::g_mu); if (icmphook_internal::g_socks.find(s) == icmphook_internal::g_socks.end()) { return false; } *out_result = icmphook_internal::bind_hook_ws2(s, name, namelen); return true; } void icmphook_net_init() { icmphook_internal::install_icmphook_hooks(); icmphook_iphlpapi_install(); }