From 5bec3d5db76d96e296377f475ade75d28fdc0684 Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Thu, 14 May 2026 07:44:14 +0000 Subject: [PATCH] network: icmp emulation (#694) ## Link to GitHub Issue or related Pull Request, if one exists #0 ## Description of change Add `-icmphook` option under spicecfg Development/Network for emulating keepalive ping replies in user mode, so games do not need to open privileged raw ICMP sockets. This makes connecting to networks possible on macOS under Whisky, and on Windows with ICMP firewalled or spice running without Administrator privileges. ## Testing Tested DDR and SDVX on Windows 10 and macOS Tahoe. Network check status in test menu shows `CONNECTED` when -icmphook is enabled, and `NOT CONNECTED` when it is disabled. Actual network functionality works as expected (under previously impossible conditions). --- src/spice2x/CMakeLists.txt | 2 + src/spice2x/hooks/icmphook_iphlpapi.cpp | 94 ++++ src/spice2x/hooks/icmphook_net.cpp | 692 ++++++++++++++++++++++++ src/spice2x/hooks/icmphook_net.h | 13 + src/spice2x/hooks/networkhook.cpp | 6 + src/spice2x/launcher/launcher.cpp | 10 + src/spice2x/launcher/options.cpp | 9 + src/spice2x/launcher/options.h | 1 + 8 files changed, 827 insertions(+) create mode 100644 src/spice2x/hooks/icmphook_iphlpapi.cpp create mode 100644 src/spice2x/hooks/icmphook_net.cpp create mode 100644 src/spice2x/hooks/icmphook_net.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index ccca346..343998a 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -523,6 +523,8 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/lang.cpp hooks/libraryhook.cpp hooks/networkhook.cpp + hooks/icmphook_net.cpp + hooks/icmphook_iphlpapi.cpp hooks/powrprof.cpp #hooks/rom.cpp hooks/setupapihook.cpp diff --git a/src/spice2x/hooks/icmphook_iphlpapi.cpp b/src/spice2x/hooks/icmphook_iphlpapi.cpp new file mode 100644 index 0000000..bee3a59 --- /dev/null +++ b/src/spice2x/hooks/icmphook_iphlpapi.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "util/detour.h" +#include "util/logging.h" + +namespace { + +#pragma pack(push, 1) +struct IcmpHdr { + uint8_t type; + uint8_t code; + uint16_t checksum_be; +}; +#pragma pack(pop) + +decltype(IcmpSendEcho) *IcmpSendEcho_orig = nullptr; + +DWORD WINAPI IcmpSendEcho_hook( + HANDLE IcmpHandle, + IPAddr DestinationAddress, + LPVOID RequestData, + WORD RequestSize, + PIP_OPTION_INFORMATION RequestOptions, + LPVOID ReplyBuffer, + DWORD ReplySize, + DWORD Timeout) { + if (!IcmpSendEcho_orig) { + SetLastError(ERROR_INVALID_FUNCTION); + return 0; + } + DWORD n = IcmpSendEcho_orig( + IcmpHandle, + DestinationAddress, + RequestData, + RequestSize, + RequestOptions, + ReplyBuffer, + ReplySize, + Timeout); + if (n != 0) { + return n; + } + if (ReplyBuffer == nullptr || ReplySize < sizeof(ICMP_ECHO_REPLY) || RequestData == nullptr) { + return 0; + } + if (RequestSize < sizeof(IcmpHdr)) { + return 0; + } + const auto *req = reinterpret_cast(RequestData); + const auto *ih = reinterpret_cast(req); + if (ih->type != 8 || ih->code != 0) { + return 0; + } + + memset(ReplyBuffer, 0, ReplySize); + auto *reply = reinterpret_cast(ReplyBuffer); + const size_t data_off = offsetof(ICMP_ECHO_REPLY, Data); + const size_t avail = ReplySize > data_off ? ReplySize - data_off : 0; + const size_t copy_len = (std::min)(static_cast(RequestSize), avail); + reply->Address = DestinationAddress; + reply->Status = IP_SUCCESS; + reply->RoundTripTime = 1; + reply->DataSize = static_cast(copy_len); + if (copy_len > 0) { + memcpy(reinterpret_cast(ReplyBuffer) + data_off, RequestData, copy_len); + } + SetLastError(ERROR_SUCCESS); + return 1; +} + +} // namespace + +void icmphook_iphlpapi_install() { + static bool done = false; + if (done) { + return; + } + done = true; + if (!detour::trampoline_try( + "iphlpapi.dll", + "IcmpSendEcho", + (void *) IcmpSendEcho_hook, + (void **) &IcmpSendEcho_orig)) { + log_warning("network", "ICMP emulation: IcmpSendEcho hook was not installed"); + } +} diff --git a/src/spice2x/hooks/icmphook_net.cpp b/src/spice2x/hooks/icmphook_net.cpp new file mode 100644 index 0000000..b655822 --- /dev/null +++ b/src/spice2x/hooks/icmphook_net.cpp @@ -0,0 +1,692 @@ +#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(); +} diff --git a/src/spice2x/hooks/icmphook_net.h b/src/spice2x/hooks/icmphook_net.h new file mode 100644 index 0000000..1081366 --- /dev/null +++ b/src/spice2x/hooks/icmphook_net.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +bool icmphook_is_emulated_socket(SOCKET s); + +/*! + * Handle bind() for emulated ICMP sockets: records interface address and succeeds without kernel bind. + * Returns true if this socket was handled (caller should return 0). + */ +bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result); + +void icmphook_net_init(); diff --git a/src/spice2x/hooks/networkhook.cpp b/src/spice2x/hooks/networkhook.cpp index 65405a1..7b5cb16 100644 --- a/src/spice2x/hooks/networkhook.cpp +++ b/src/spice2x/hooks/networkhook.cpp @@ -14,6 +14,7 @@ #include "util/fileutils.h" #include "util/libutils.h" #include "util/deferlog.h" +#include "hooks/icmphook_net.h" // hooking related stuff static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr; @@ -189,6 +190,11 @@ static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen) #pragma ide diagnostic ignored "OCDFAInspection" #endif + int icmp_bind_result = 0; + if (icmphook_try_bind(s, name, namelen, &icmp_bind_result)) { + return icmp_bind_result; + } + // cast to sockaddr_in struct sockaddr_in *in_name = (struct sockaddr_in *) name; diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index a4879d8..a61876a 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -76,6 +76,7 @@ #include "hooks/graphics/graphics.h" #include "hooks/lang.h" #include "hooks/networkhook.h" +#include "hooks/icmphook_net.h" #include "hooks/unisintrhook.h" #include "launcher/launcher.h" #include "launcher/logger.h" @@ -244,6 +245,7 @@ int main_implementation(int argc, char *argv[]) { bool easrv_smart = false; bool load_stubs = false; bool netfix_disable = false; + bool icmphook_enable = false; bool lang_disable = false; std::string process_priority_str = "high"; bool cardio_enabled = false; @@ -732,6 +734,9 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::DisableNetworkFixes].value_bool()) { netfix_disable = true; } + if (options[launcher::Options::EnableICMPHook].value_bool()) { + icmphook_enable = true; + } if (options[launcher::Options::DisableACPHook].value_bool()) { lang_disable = true; } @@ -2259,6 +2264,11 @@ int main_implementation(int argc, char *argv[]) { avs::core::load_dll(); avs::ea3::load_dll(); + // ICMP emulation (opt-in; before games open raw ICMP sockets) + if (icmphook_enable) { + icmphook_net_init(); + } + // net fix if (!netfix_disable) { networkhook_init(); diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index d0611c5..e5014ec 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -2803,6 +2803,15 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "Development" }, + { + // EnableICMPHook + .title = "Enable ICMP Emulation", + .name = "icmphook", + .desc = "Emulate keepalive ping replies in user mode, so the game does not need to " + "open privileged raw ICMP sockets.", + .type = OptionType::Bool, + .category = "Network (Development)", + }, }; const std::vector &launcher::get_categories(Options::OptionsCategory category) { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 21ed49c..c0fec7d 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -282,6 +282,7 @@ namespace launcher { OptionConflictResolution, OtocaCamHook, DisableHighResTimer, + EnableICMPHook, }; enum class OptionsCategory {