Files
spice2x.github.io/src/spice2x/hooks/networkhook.cpp
T
drmext 5bec3d5db7 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).
2026-05-14 00:44:14 -07:00

277 lines
8.3 KiB
C++

#include <winsock2.h>
#include <windows.h>
#include <iphlpapi.h>
#include <stdlib.h>
#include <string>
#include <mutex>
#include "avs/core.h"
#include "avs/ea3.h"
#include "avs/game.h"
#include "util/logging.h"
#include "util/detour.h"
#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;
static decltype(bind) *bind_orig = nullptr;
// settings
std::string NETWORK_ADDRESS = "10.9.0.0";
std::string NETWORK_SUBNET = "255.255.0.0";
static bool GetAdaptersInfo_log = true;
// network structs
static struct in_addr network;
static struct in_addr prefix;
static struct in_addr subnet;
static void defer_network_adapter_error() {
static std::once_flag printed;
std::call_once(printed, []() {
deferredlogs::defer_error_messages({
"network adapter issue detected!",
" ensure you have at least one network adapter with a valid IPv4 address",
" the IPv4 address can be external or internal, it just needs to be valid",
" the network adapter can be a wired or wireless connection",
" you still need to do this even if you are connecting to a local server!",
});
});
}
static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen) {
// call orig
ULONG ret = GetAdaptersInfo_orig(pAdapterInfo, pOutBufLen);
if (ret != ERROR_SUCCESS) {
// workaround for QMA not having enough buffer space
if (pAdapterInfo != nullptr && avs::game::is_model({ "LMA", "MMA" })) {
// allocate the output buffer size
auto pAdapterInfo2 = (PIP_ADAPTER_INFO) malloc(*pOutBufLen);
// call ourself with an appropriate buffer size
ret = GetAdaptersInfo_hook(pAdapterInfo2, pOutBufLen);
if (ret != ERROR_SUCCESS) {
return ret;
}
// copy best interface
memcpy(pAdapterInfo, pAdapterInfo2, sizeof(*pAdapterInfo));
pAdapterInfo->Next = nullptr;
// free our allocated memory
free(pAdapterInfo2);
}
if (ret != ERROR_SUCCESS && ret != ERROR_BUFFER_OVERFLOW) {
defer_network_adapter_error();
log_warning(
"network",
"GetAdaptersInfo failed with {}; "
"check if you have at least one network adapter with a valid IPv4 address!",
ret);
}
return ret;
}
// set the best network adapter
PIP_ADAPTER_INFO info = pAdapterInfo;
while (info != nullptr) {
// set subnet
struct in_addr info_subnet;
info_subnet.s_addr = inet_addr(info->IpAddressList.IpMask.String);
// set prefix
struct in_addr info_prefix;
info_prefix.s_addr = inet_addr(info->IpAddressList.IpAddress.String) & info_subnet.s_addr;
// check base IP and subnet
bool isCorrectBaseIp = prefix.s_addr == info_prefix.s_addr;
bool isCorrectSubnetMask = subnet.s_addr == info_subnet.s_addr;
// check if requirements are met
if (isCorrectBaseIp && isCorrectSubnetMask) {
// log adapter
if (GetAdaptersInfo_log)
log_info("network", "Using preferred network adapter: {}, {}",
info->AdapterName,
info->Description);
// set adapter information
memcpy(pAdapterInfo, info, sizeof(*info));
pAdapterInfo->Next = nullptr;
// we're done
GetAdaptersInfo_log = false;
return ret;
}
// iterate
info = info->Next;
}
// get IP forward table
PMIB_IPFORWARDTABLE pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(sizeof(MIB_IPFORWARDTABLE));
DWORD dwSize = 0;
if (GetIpForwardTable(pIpForwardTable, &dwSize, 1) == ERROR_INSUFFICIENT_BUFFER) {
free(pIpForwardTable);
pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(dwSize);
}
if (GetIpForwardTable(pIpForwardTable, &dwSize, 1) != NO_ERROR || pIpForwardTable->dwNumEntries == 0) {
defer_network_adapter_error();
if (GetAdaptersInfo_log) {
log_warning("network", "GetIpForwardTable failed");
}
return ret;
}
// determine best interface
DWORD best = pIpForwardTable->table[0].dwForwardIfIndex;
free(pIpForwardTable);
// find fallback adapter
info = pAdapterInfo;
while (info != nullptr) {
// check if this the adapter we search for
if (info->Index == best) {
// log information
if (GetAdaptersInfo_log)
log_info("network", "Using fallback network adapter: {}, {}",
info->AdapterName,
info->Description);
// set adapter information
memcpy(pAdapterInfo, info, sizeof(*info));
pAdapterInfo->Next = nullptr;
// exit the loop
break;
}
// iterate
info = info->Next;
}
if (pAdapterInfo->IpAddressList.IpAddress.String[0] == 0 ||
pAdapterInfo->IpAddressList.IpAddress.String[0] == '0') {
defer_network_adapter_error();
if (GetAdaptersInfo_log) {
log_warning(
"network",
"invalid IPv4 address for adapter {}, {} = {}; "
"ensure you have at least one network adapter with valid IPv4 address!",
pAdapterInfo->AdapterName,
pAdapterInfo->Description,
pAdapterInfo->IpAddressList.IpAddress.String);
}
}
// return original value
GetAdaptersInfo_log = false;
return ret;
}
static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen) {
#ifdef __clang__
#pragma clang diagnostic push
#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;
#ifdef __clang__
#pragma clang diagnostic pop
#endif
// override bind to allow all hosts
in_name->sin_addr.s_addr = inet_addr("0.0.0.0");
// call original
int ret = bind_orig(s, name, namelen);
if (ret != 0) {
log_warning("network", "bind failed: {}", WSAGetLastError());
}
// return result
return ret;
}
void networkhook_init() {
// announce init
log_info("network", "SpiceTools Network");
// set some same defaults
network.s_addr = inet_addr(NETWORK_ADDRESS.c_str());
subnet.s_addr = inet_addr(NETWORK_SUBNET.c_str());
prefix.s_addr = network.s_addr & subnet.s_addr;
// inet_ntoa(...) reuses the same char array so the results must be copied
char s_network[17]{}, s_subnet[17]{}, s_prefix[17]{};
strncpy(s_network, inet_ntoa(network), 16);
strncpy(s_subnet, inet_ntoa(subnet), 16);
strncpy(s_prefix, inet_ntoa(prefix), 16);
// log preferences
// log_info("network", "Network preferences: {}, {}, {}", s_network, s_subnet, s_prefix);
// GetAdaptersInfo hook
auto orig_addr = detour::iat_try(
"GetAdaptersInfo", GetAdaptersInfo_hook, nullptr);
if (!orig_addr) {
log_warning("network", "Could not hook GetAdaptersInfo");
} else if (GetAdaptersInfo_orig == nullptr) {
GetAdaptersInfo_orig = orig_addr;
}
/*
* Bind Hook
*/
bool bind_hook_enabled = true;
// disable hook for DDR A since the bind hook crashes there for some reason
if (fileutils::file_exists(MODULE_PATH / "gamemdx.dll")) {
bind_hook_enabled = false;
}
// hook bind
if (bind_hook_enabled) {
// hook by name
auto new_bind_orig = detour::iat_try("bind", bind_hook, nullptr);
if (bind_orig == nullptr) {
bind_orig = new_bind_orig;
}
// hook ESS by ordinal
HMODULE ess = libutils::try_module("ess.dll");
if (ess) {
auto new_bind_orig2 = detour::iat_try_ordinal("WS2_32.dll", 2, bind_hook, ess);
// try to get some valid pointer
if (bind_orig == nullptr && new_bind_orig2 != nullptr) {
bind_orig = new_bind_orig2;
}
}
}
}