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 {