diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index d194d827..b495b535 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -617,6 +617,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/icmphook_net.cpp hooks/icmphook_iphlpapi.cpp hooks/nicspoof.cpp + hooks/nicspoof_tunnel.cpp hooks/powrprof.cpp #hooks/rom.cpp hooks/setupapihook.cpp diff --git a/src/spice2x/hooks/icmphook_net.cpp b/src/spice2x/hooks/icmphook_net.cpp index b655822b..ddc9606d 100644 --- a/src/spice2x/hooks/icmphook_net.cpp +++ b/src/spice2x/hooks/icmphook_net.cpp @@ -1,5 +1,6 @@ #include "icmphook_net.h" +#include "hooks/nicspoof.h" #include "util/detour.h" #include "util/logging.h" @@ -353,7 +354,11 @@ 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)) { + if (it != g_socks.end()) { + if (!name || namelen < (int) sizeof(sockaddr_in)) { + WSASetLastError(WSAEFAULT); + return SOCKET_ERROR; + } auto *in = reinterpret_cast(name); if (in->sin_family != AF_INET) { WSASetLastError(WSAEAFNOSUPPORT); @@ -618,7 +623,10 @@ void install_icmphook_hooks() { } done = true; + const bool defer_divert = nicspoof_tunnel_enabled(); bool ok = true; + + // Always own socket creation so raw ICMP sockets become emulated. ok &= detour::trampoline_try( "ws2_32.dll", "socket", (void *) socket_hook, (void **) &socket_orig); @@ -628,37 +636,52 @@ void install_icmphook_hooks() { 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) { + if (defer_divert) { + // NIC tunnel already hooked bind/sendto/recvfrom/closesocket/ioctlsocket. + // Tunnel hooks call icmphook_try_* for emulated ICMP sockets. + log_info("network", + "ICMP emulation: socket hooks installed; divert deferred to " + "NIC tunnel (icmphook_try_*)"); + } else { + 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); + + 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)"); + } + } + + if (defer_divert && !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)"); + "ICMP emulation: one or more socket-creation hooks failed"); } g_installed.store(true, std::memory_order_release); @@ -686,6 +709,66 @@ bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int * return true; } +bool icmphook_try_sendto(SOCKET s, const char *buf, int len, int flags, + const sockaddr *to, int tolen, int *out_result) { + if (!icmphook_is_emulated_socket(s)) { + return false; + } + *out_result = icmphook_internal::sendto_hook(s, buf, len, flags, to, tolen); + return true; +} + +bool icmphook_try_recvfrom(SOCKET s, char *buf, int len, int flags, + sockaddr *from, int *fromlen, int *out_result) { + if (!icmphook_is_emulated_socket(s)) { + return false; + } + *out_result = icmphook_internal::recvfrom_hook(s, buf, len, flags, from, fromlen); + return true; +} + +bool icmphook_try_WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, + LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo, + int iTolen, LPWSAOVERLAPPED lpOverlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result) { + if (!icmphook_is_emulated_socket(s)) { + return false; + } + *out_result = icmphook_internal::WSASendTo_hook( + s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpTo, + iTolen, lpOverlapped, lpCompletionRoutine); + return true; +} + +bool icmphook_try_WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, + LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom, + LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result) { + if (!icmphook_is_emulated_socket(s)) { + return false; + } + *out_result = icmphook_internal::WSARecvFrom_hook( + s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpFrom, + lpFromlen, lpOverlapped, lpCompletionRoutine); + return true; +} + +bool icmphook_try_ioctlsocket(SOCKET s, long cmd, u_long *argp, int *out_result) { + if (!icmphook_is_emulated_socket(s)) { + return false; + } + *out_result = icmphook_internal::ioctlsocket_hook(s, cmd, argp); + return true; +} + +bool icmphook_try_closesocket(SOCKET s, int *out_result) { + if (!icmphook_is_emulated_socket(s)) { + return false; + } + *out_result = icmphook_internal::closesocket_hook(s); + 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 index 10813665..cd998aa3 100644 --- a/src/spice2x/hooks/icmphook_net.h +++ b/src/spice2x/hooks/icmphook_net.h @@ -6,8 +6,27 @@ 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). + * Returns true if this socket was handled (caller should return *out_result). */ bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result); +/*! + * Divert-path helpers for when NIC tunnel owns the MinHook slots on these APIs. + * Return true if the socket is an emulated ICMP socket and was fully handled. + */ +bool icmphook_try_sendto(SOCKET s, const char *buf, int len, int flags, + const sockaddr *to, int tolen, int *out_result); +bool icmphook_try_recvfrom(SOCKET s, char *buf, int len, int flags, + sockaddr *from, int *fromlen, int *out_result); +bool icmphook_try_WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, + LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo, + int iTolen, LPWSAOVERLAPPED lpOverlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result); +bool icmphook_try_WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, + LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom, + LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result); +bool icmphook_try_ioctlsocket(SOCKET s, long cmd, u_long *argp, int *out_result); +bool icmphook_try_closesocket(SOCKET s, int *out_result); + void icmphook_net_init(); diff --git a/src/spice2x/hooks/nicspoof.cpp b/src/spice2x/hooks/nicspoof.cpp index 3ba787cb..46132468 100644 --- a/src/spice2x/hooks/nicspoof.cpp +++ b/src/spice2x/hooks/nicspoof.cpp @@ -14,26 +14,32 @@ #include "util/detour.h" #include "util/logging.h" +#include "hooks/nicspoof_tunnel.h" -namespace { -constexpr uint32_t k_local_ip = 0x0A390302u; /* 10.57.3.2 */ -constexpr uint32_t k_gateway = 0x0A390301u; /* 10.57.3.1 */ -constexpr uint32_t k_dns = 0x0A390301u; /* 10.57.3.1 */ -constexpr uint32_t k_dhcp = 0x0A390301u; /* 10.57.3.1 */ -constexpr uint32_t k_mask = 0xFFFFFF00u; /* 255.255.255.0 */ -constexpr uint32_t k_subnet = 0x0A390300u; -constexpr int k_prefix_len = 24; +namespace nicspoof_detail { +NicSpoofConfig g_cfg; + +constexpr uint32_t k_default_local_ip = 0x0A64640Au; /* 10.100.100.10 */ +constexpr uint32_t k_gateway = 0x0A646401u; /* 10.100.100.1 */ +constexpr uint32_t k_dns1 = 0x0A02010Au; /* 10.2.1.10 */ +constexpr uint32_t k_dns2 = 0x0A02011Eu; /* 10.2.1.30 */ +constexpr uint32_t k_dhcp = 0xC0A80001u; /* 192.168.0.1 */ +constexpr uint32_t k_mask = 0xFF000000u; /* 255.0.0.0 */ +constexpr int k_prefix_len = 8; constexpr DWORD k_ifindex = 77; constexpr uint8_t k_fake_mac[6] = {0x12, 0x37, 0x13, 0x37, 0x13, 0x37}; -constexpr char k_hostname[] = "N1CSP00F"; -constexpr char k_domain[] = "local"; -constexpr char k_hostname_fqdn[] = "N1CSP00F.local"; -constexpr wchar_t k_domain_w[] = L"local"; +constexpr char k_hostname[] = "N1C5P00F"; +constexpr char k_domain[] = "sp2x"; +constexpr char k_hostname_fqdn[] = "N1C5P00F.sp2x"; +constexpr wchar_t k_domain_w[] = L"sp2x"; constexpr char k_adapter_name[] = "{NICSPOOF-0880-0001-4250-4E6963537066}"; constexpr char k_adapter_desc[] = "NicSpoof Virtual Ethernet"; constexpr wchar_t k_friendly_name[] = L"NicSpoof"; constexpr wchar_t k_adapter_desc_w[] = L"NicSpoof Virtual Ethernet"; +uint32_t g_local_ip = k_default_local_ip; +uint32_t g_subnet = k_default_local_ip & k_mask; + #define ALIGN_UP_PTR(p, a) \ ((BYTE *)(((ULONG_PTR)(p) + ((ULONG_PTR)(a) - 1)) & ~((ULONG_PTR)(a) - 1))) @@ -125,7 +131,7 @@ DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) { memcpy(p->Address, k_fake_mac, 6); p->Type = MIB_IF_TYPE_ETHERNET; p->DhcpEnabled = 1; - ip_to_str(k_local_ip, ip, sizeof(ip)); + ip_to_str(g_local_ip, ip, sizeof(ip)); ip_to_str(k_gateway, gw, sizeof(gw)); ip_to_str(k_dhcp, dhcp, sizeof(dhcp)); strcpy(p->IpAddressList.IpAddress.String, ip); @@ -151,8 +157,11 @@ DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) { } DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) { - ULONG need = static_cast(sizeof(FIXED_INFO)); - char dns[16]; + /* FIXED_INFO embeds the first IP_ADDR_STRING; second follows in the buffer. */ + ULONG need = static_cast(sizeof(FIXED_INFO) + sizeof(IP_ADDR_STRING)); + char dns1[16]; + char dns2[16]; + PIP_ADDR_STRING dns_second = nullptr; if (!s) { return ERROR_INVALID_PARAMETER; @@ -164,18 +173,27 @@ DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) { memset(p, 0, need); strncpy(p->HostName, k_hostname, sizeof(p->HostName) - 1); strncpy(p->DomainName, k_domain, sizeof(p->DomainName) - 1); - ip_to_str(k_dns, dns, sizeof(dns)); - strcpy(p->DnsServerList.IpAddress.String, dns); + ip_to_str(k_dns1, dns1, sizeof(dns1)); + ip_to_str(k_dns2, dns2, sizeof(dns2)); + strcpy(p->DnsServerList.IpAddress.String, dns1); p->DnsServerList.IpMask.String[0] = 0; p->DnsServerList.Context = 0; - p->DnsServerList.Next = nullptr; + dns_second = reinterpret_cast( + reinterpret_cast(p) + sizeof(FIXED_INFO)); + memset(dns_second, 0, sizeof(*dns_second)); + strcpy(dns_second->IpAddress.String, dns2); + dns_second->IpMask.String[0] = 0; + dns_second->Context = 0; + dns_second->Next = nullptr; + p->DnsServerList.Next = dns_second; p->CurrentDnsServer = &p->DnsServerList; p->NodeType = 1; p->EnableDns = 1; *s = need; if (g_log_params) { - log_misc("network", "NIC spoof GetNetworkParams host={} domain={} dns={}", - k_hostname, k_domain, dns); + log_misc("network", + "NIC spoof GetNetworkParams host={} domain={} dns={} dns2={}", + k_hostname, k_domain, dns1, dns2); g_log_params = false; } return ERROR_SUCCESS; @@ -197,19 +215,21 @@ ULONG WINAPI GetAdaptersAddresses_hook( sizeof(void *) * 8 + sizeof(IP_ADAPTER_UNICAST_ADDRESS) + sizeof(IP_ADAPTER_PREFIX) + - sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) + + sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) * 2 + sizeof(IP_ADAPTER_GATEWAY_ADDRESS) + - sizeof(sockaddr_in) * 4 + 64); + sizeof(sockaddr_in) * 5 + 64); BYTE *blob = nullptr; PIP_ADAPTER_ADDRESSES a = nullptr; PIP_ADAPTER_UNICAST_ADDRESS u = nullptr; PIP_ADAPTER_PREFIX pref = nullptr; PIP_ADAPTER_GATEWAY_ADDRESS gw = nullptr; PIP_ADAPTER_DNS_SERVER_ADDRESS dns = nullptr; + PIP_ADAPTER_DNS_SERVER_ADDRESS dns2 = nullptr; sockaddr_in *sa = nullptr; sockaddr_in *sm = nullptr; sockaddr_in *sg = nullptr; sockaddr_in *ds = nullptr; + sockaddr_in *ds2 = nullptr; (void)Reserved; (void)Flags; @@ -267,7 +287,7 @@ ULONG WINAPI GetAdaptersAddresses_hook( blob += sizeof(*sa); memset(sa, 0, sizeof(*sa)); sa->sin_family = AF_INET; - sa->sin_addr.s_addr = htonl(k_local_ip); + sa->sin_addr.s_addr = htonl(g_local_ip); u->Address.lpSockaddr = reinterpret_cast(sa); u->Address.iSockaddrLength = sizeof(*sa); a->FirstUnicastAddress = u; @@ -282,7 +302,7 @@ ULONG WINAPI GetAdaptersAddresses_hook( blob += sizeof(*sm); memset(sm, 0, sizeof(*sm)); sm->sin_family = AF_INET; - sm->sin_addr.s_addr = htonl(k_subnet); + sm->sin_addr.s_addr = htonl(g_subnet); pref->Address.lpSockaddr = reinterpret_cast(sm); pref->Address.iSockaddrLength = sizeof(*sm); a->FirstPrefix = pref; @@ -309,10 +329,23 @@ ULONG WINAPI GetAdaptersAddresses_hook( blob += sizeof(*ds); memset(ds, 0, sizeof(*ds)); ds->sin_family = AF_INET; - ds->sin_addr.s_addr = htonl(k_dns); + ds->sin_addr.s_addr = htonl(k_dns1); dns->Address.lpSockaddr = reinterpret_cast(ds); dns->Address.iSockaddrLength = sizeof(*ds); - dns->Next = nullptr; + + blob = ALIGN_UP_PTR(blob, sizeof(void *)); + dns2 = reinterpret_cast(blob); + blob += sizeof(*dns2); + memset(dns2, 0, sizeof(*dns2)); + ds2 = reinterpret_cast(blob); + blob += sizeof(*ds2); + memset(ds2, 0, sizeof(*ds2)); + ds2->sin_family = AF_INET; + ds2->sin_addr.s_addr = htonl(k_dns2); + dns2->Address.lpSockaddr = reinterpret_cast(ds2); + dns2->Address.iSockaddrLength = sizeof(*ds2); + dns2->Next = nullptr; + dns->Next = dns2; a->FirstDnsServerAddress = dns; a->Next = nullptr; @@ -404,7 +437,7 @@ INT WSAAPI getaddrinfo_hook( if (!pNodeName || !pNodeName[0] || name_is_local(pNodeName)) { if (pNodeName && (_stricmp(pNodeName, k_hostname) == 0 || _stricmp(pNodeName, k_hostname_fqdn) == 0)) { - ip = k_local_ip; + ip = g_local_ip; } else { ip = 0x7F000001u; } @@ -412,7 +445,7 @@ INT WSAAPI getaddrinfo_hook( ip = parse_ipv4(pNodeName); } else { /* offline=1: map external names to local_ip */ - ip = k_local_ip; + ip = g_local_ip; } if (pServiceName && pServiceName[0]) { @@ -471,7 +504,7 @@ void install_nicspoof_hooks() { char ipstr[16]; char maskstr[16]; char gwstr[16]; - ip_to_str(k_local_ip, ipstr, sizeof(ipstr)); + ip_to_str(g_local_ip, ipstr, sizeof(ipstr)); ip_to_str(k_mask, maskstr, sizeof(maskstr)); ip_to_str(k_gateway, gwstr, sizeof(gwstr)); log_info("network", @@ -508,7 +541,6 @@ void install_nicspoof_hooks() { (void *) freeaddrinfo_hook, (void **) &freeaddrinfo_orig); - /* FreeAddrInfoA is often the same address; only hook if distinct. */ { HMODULE ws = GetModuleHandleA("ws2_32.dll"); void *faa = nullptr; @@ -539,8 +571,75 @@ void install_nicspoof_hooks() { } } -} // namespace +bool ip_in_overlay_range(uint32_t ip) { + const uint32_t net = k_gateway & k_mask; + const uint32_t bcast = net | ~k_mask; + if ((ip & k_mask) != net) { + return false; + } + if (ip == net || ip == bcast || ip == k_gateway || + ip == k_dns1 || ip == k_dns2) { + return false; + } + return true; +} + +void init_impl() { + if (g_cfg.mode == NicSpoofMode::Off) { + return; + } + + g_local_ip = g_cfg.local_ip ? g_cfg.local_ip : k_default_local_ip; + if (!ip_in_overlay_range(g_local_ip)) { + char bad[16]; + ip_to_str(g_local_ip, bad, sizeof(bad)); + log_warning("network", + "NIC spoof: IP {} is outside 10.0.0.0/8 usable range; " + "using 10.100.100.10", + bad); + g_local_ip = k_default_local_ip; + } + g_subnet = g_local_ip & k_mask; + + const char *mode_str = "offline"; + if (g_cfg.mode == NicSpoofMode::TunnelHost) { + mode_str = "tunnelhost"; + } else if (g_cfg.mode == NicSpoofMode::TunnelClient) { + mode_str = "tunnelclient"; + } + log_info("network", "NIC spoof mode={}", mode_str); + + install_nicspoof_hooks(); + + if (g_cfg.mode == NicSpoofMode::TunnelHost || + g_cfg.mode == NicSpoofMode::TunnelClient) { + nicspoof_tunnel_init(g_cfg); + } +} + +} // namespace nicspoof_detail + +void nicspoof_configure(const NicSpoofConfig &cfg) { + nicspoof_detail::g_cfg = cfg; +} + +bool nicspoof_tunnel_enabled() { + return nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelHost || + nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelClient; +} + +uint32_t nicspoof_local_ip() { + return nicspoof_detail::g_local_ip; +} + +uint32_t nicspoof_mask() { + return nicspoof_detail::k_mask; +} + +uint32_t nicspoof_subnet() { + return nicspoof_detail::g_subnet; +} void nicspoof_init() { - install_nicspoof_hooks(); + nicspoof_detail::init_impl(); } diff --git a/src/spice2x/hooks/nicspoof.h b/src/spice2x/hooks/nicspoof.h index 6f00bad2..63a32ad0 100644 --- a/src/spice2x/hooks/nicspoof.h +++ b/src/spice2x/hooks/nicspoof.h @@ -1,3 +1,28 @@ #pragma once +#include +#include + +enum class NicSpoofMode { + Off, + Offline, + TunnelHost, + TunnelClient, +}; + +struct NicSpoofConfig { + NicSpoofMode mode = NicSpoofMode::Off; + uint32_t local_ip = 0x0A64640Au; /* 10.100.100.10 */ + uint16_t tunnel_port = 51820; + std::string hub_host; +}; + +void nicspoof_configure(const NicSpoofConfig &cfg); void nicspoof_init(); + +/*! True when mode is TunnelHost or TunnelClient (divert hooks own ws2 sendto/etc.). */ +bool nicspoof_tunnel_enabled(); + +uint32_t nicspoof_local_ip(); +uint32_t nicspoof_mask(); +uint32_t nicspoof_subnet(); diff --git a/src/spice2x/hooks/nicspoof_tunnel.cpp b/src/spice2x/hooks/nicspoof_tunnel.cpp new file mode 100644 index 00000000..c73fadc3 --- /dev/null +++ b/src/spice2x/hooks/nicspoof_tunnel.cpp @@ -0,0 +1,2055 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef _WINSOCK_DEPRECATED_NO_WARNINGS +#define _WINSOCK_DEPRECATED_NO_WARNINGS +#endif +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 +#endif + +#include "nicspoof_tunnel.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "hooks/icmphook_net.h" +#include "util/detour.h" +#include "util/logging.h" + +namespace nicspoof_tunnel_detail { + +constexpr uint8_t MAGIC0 = 'B'; +constexpr uint8_t MAGIC1 = 'P'; +constexpr uint8_t MAGIC2 = 'L'; +constexpr uint8_t MAGIC3 = '1'; +constexpr uint8_t MSG_REGISTER = 1; +constexpr uint8_t MSG_REGISTER_ACK = 2; +constexpr uint8_t MSG_KEEPALIVE = 3; +constexpr uint8_t MSG_DATA = 4; +constexpr uint8_t ACK_OK = 0; +constexpr uint8_t KIND_UDP = 1; +constexpr uint8_t KIND_TCP_SYN = 2; +constexpr uint8_t KIND_TCP_ACCEPTED = 3; +constexpr uint8_t KIND_TCP_DATA = 4; +constexpr uint8_t KIND_TCP_FIN = 5; + +constexpr int MAX_UDP_Q = 64; +constexpr int MAX_UDP_PAYLOAD = 8192; +constexpr int MAX_TCP = 32; +constexpr int MAX_LISTEN = 8; +constexpr int MAX_ACCEPT_Q = 8; +constexpr int MAX_UDP_SOCK = 32; +constexpr int KEEP_ALIVE_SEC = 5; + +bool g_is_hub = false; +uint16_t g_tunnel_port = 51820; +char g_hub[256] = {}; +uint32_t g_local_ip = 0; +uint32_t g_peer_ip = 0; +uint32_t g_subnet = 0; +uint32_t g_mask = 0; + +SOCKET g_tun_sock = INVALID_SOCKET; +sockaddr_in g_peer_addr = {}; +int g_peer_valid = 0; +volatile LONG g_registered = 0; +volatile LONG g_shutdown = 0; +HANDLE g_tunnel_th = nullptr; +HANDLE g_keepalive_th = nullptr; +CRITICAL_SECTION g_peer_cs; + +bool g_logged_overlay_listen = false; + +decltype(sendto) *sendto_orig = nullptr; +decltype(recvfrom) *recvfrom_orig = nullptr; +decltype(bind) *bind_orig = nullptr; +decltype(connect) *connect_orig = nullptr; +decltype(WSAConnect) *WSAConnect_orig = nullptr; +decltype(listen) *listen_orig = nullptr; +decltype(accept) *accept_orig = nullptr; +decltype(closesocket) *closesocket_orig = nullptr; +decltype(ioctlsocket) *ioctlsocket_orig = nullptr; +decltype(send) *send_orig = nullptr; +decltype(recv) *recv_orig = nullptr; +decltype(getsockname) *getsockname_orig = nullptr; +decltype(select) *select_orig = nullptr; +decltype(WSASendTo) *WSASendTo_orig = nullptr; +decltype(WSARecvFrom) *WSARecvFrom_orig = nullptr; + +struct UdpDgram { + uint32_t from_ip; + uint16_t from_port; + int32_t len; + uint8_t data[MAX_UDP_PAYLOAD]; +}; + +struct UdpSock { + SOCKET s; + int32_t in_use; + int32_t closing; + int32_t bound_port; + volatile LONG refs; + CRITICAL_SECTION cs; + HANDLE event; + int32_t q_head; + int32_t q_tail; + int32_t q_count; + UdpDgram q[MAX_UDP_Q]; +}; + +UdpSock g_udp[MAX_UDP_SOCK]; +CRITICAL_SECTION g_udp_cs; + +struct TcpConn { + int32_t in_use; + uint32_t conn_id; + SOCKET game_sock; + SOCKET relay_sock; + HANDLE thread; + volatile LONG alive; +}; + +struct AcceptItem { + SOCKET client_sock; + sockaddr_in peer; +}; + +struct ListenState { + SOCKET listen_sock; + int32_t in_use; + int32_t closing; + uint16_t port; + int32_t nonblock; + volatile LONG refs; + CRITICAL_SECTION cs; + HANDLE event; + int32_t q_count; + AcceptItem q[MAX_ACCEPT_Q]; +}; + +TcpConn g_tcp[MAX_TCP]; +ListenState g_listen[MAX_LISTEN]; +CRITICAL_SECTION g_tcp_cs; +CRITICAL_SECTION g_conn_wait_cs; + +struct ConnWait { + uint32_t id; + HANDLE ev; + int32_t used; +}; + +ConnWait g_conn_wait[MAX_TCP]; +volatile LONG g_next_conn_id = 1; + +constexpr int MAX_NB = 64; + +struct NbState { + SOCKET s; + int32_t in_use; + int32_t nonblock; +}; + +NbState g_nb[MAX_NB]; +CRITICAL_SECTION g_nb_cs; + +volatile LONG g_tunnel_up = 0; +bool g_wsa_started = false; + +void ip_to_str(uint32_t ip, char *out, size_t n) { + if (!out || n == 0) { + return; + } + snprintf(out, n, "%u.%u.%u.%u", + (ip >> 24) & 255, (ip >> 16) & 255, (ip >> 8) & 255, ip & 255); + out[n - 1] = 0; +} + +bool in_overlay(uint32_t ip) { + return (ip & g_mask) == (g_subnet & g_mask); +} + +bool is_bcast(uint32_t ip) { + return ip == 0xFFFFFFFFu || ip == (g_subnet | ~g_mask); +} + +uint16_t rd16(const uint8_t *p) { + return static_cast((p[0] << 8) | p[1]); +} + +uint32_t rd32(const uint8_t *p) { + return (static_cast(p[0]) << 24) | + (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | + static_cast(p[3]); +} + +void wr16(uint8_t *p, uint16_t v) { + p[0] = static_cast(v >> 8); + p[1] = static_cast(v); +} + +void wr32(uint8_t *p, uint32_t v) { + p[0] = static_cast(v >> 24); + p[1] = static_cast(v >> 16); + p[2] = static_cast(v >> 8); + p[3] = static_cast(v); +} + +void peer_set(const sockaddr_in *addr, int32_t valid, uint32_t peer_ip) { + EnterCriticalSection(&g_peer_cs); + if (addr) { + g_peer_addr = *addr; + } + g_peer_valid = valid; + if (peer_ip) { + g_peer_ip = peer_ip; + } + LeaveCriticalSection(&g_peer_cs); +} + +int32_t peer_get(sockaddr_in *addr, uint32_t *peer_ip) { + int32_t v; + EnterCriticalSection(&g_peer_cs); + v = g_peer_valid; + if (addr) { + *addr = g_peer_addr; + } + if (peer_ip) { + *peer_ip = g_peer_ip; + } + LeaveCriticalSection(&g_peer_cs); + return v; +} + +UdpSock *udp_find(SOCKET s) { + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + if (g_udp[i].in_use && !g_udp[i].closing && g_udp[i].s == s) { + return &g_udp[i]; + } + } + return nullptr; +} + +void udp_destroy_unlocked(UdpSock *st) { + if (st->event) { + CloseHandle(st->event); + st->event = nullptr; + } + DeleteCriticalSection(&st->cs); + memset(st, 0, sizeof(*st)); +} + +UdpSock *udp_acquire(SOCKET s) { + UdpSock *st; + + EnterCriticalSection(&g_udp_cs); + st = udp_find(s); + if (st) { + InterlockedIncrement(&st->refs); + } + LeaveCriticalSection(&g_udp_cs); + return st; +} + +void udp_release(UdpSock *st) { + if (!st) { + return; + } + EnterCriticalSection(&g_udp_cs); + if (InterlockedDecrement(&st->refs) == 0) { + udp_destroy_unlocked(st); + } + LeaveCriticalSection(&g_udp_cs); +} + +UdpSock *udp_add(SOCKET s) { + UdpSock *st; + HANDLE ev; + + EnterCriticalSection(&g_udp_cs); + st = udp_find(s); + if (!st) { + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + if (!g_udp[i].in_use) { + memset(&g_udp[i], 0, sizeof(g_udp[i])); + ev = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!ev) { + LeaveCriticalSection(&g_udp_cs); + return nullptr; + } + g_udp[i].s = s; + g_udp[i].in_use = 1; + g_udp[i].refs = 1; + InitializeCriticalSection(&g_udp[i].cs); + g_udp[i].event = ev; + st = &g_udp[i]; + break; + } + } + } + LeaveCriticalSection(&g_udp_cs); + return st; +} + +void udp_remove(SOCKET s) { + UdpSock *st = nullptr; + + EnterCriticalSection(&g_udp_cs); + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + if (g_udp[i].in_use && g_udp[i].s == s) { + st = &g_udp[i]; + break; + } + } + if (st && !st->closing) { + st->closing = 1; + if (st->event) { + SetEvent(st->event); + } + } else { + st = nullptr; + } + LeaveCriticalSection(&g_udp_cs); + if (st) { + udp_release(st); + } +} + +void sock_signal_read(HANDLE ev) { + if (ev) { + SetEvent(ev); + } +} + +void udp_enqueue_sock(UdpSock *st, uint32_t from_ip, uint16_t from_port, + const uint8_t *data, int32_t len) { + if (!st || st->closing || len < 0 || len > MAX_UDP_PAYLOAD) { + return; + } + EnterCriticalSection(&st->cs); + if (st->q_count < MAX_UDP_Q) { + UdpDgram *d = &st->q[st->q_tail]; + d->from_ip = from_ip; + d->from_port = from_port; + d->len = len; + memcpy(d->data, data, static_cast(len)); + st->q_tail = (st->q_tail + 1) % MAX_UDP_Q; + st->q_count++; + sock_signal_read(st->event); + } + LeaveCriticalSection(&st->cs); +} + +void udp_enqueue(uint16_t dport, uint32_t from_ip, uint16_t from_port, + const uint8_t *data, int32_t len) { + int32_t matched = 0; + + if (len < 0 || len > MAX_UDP_PAYLOAD) { + return; + } + EnterCriticalSection(&g_udp_cs); + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + UdpSock *st = &g_udp[i]; + if (!st->in_use || st->closing) { + continue; + } + if (st->bound_port == static_cast(dport)) { + udp_enqueue_sock(st, from_ip, from_port, data, len); + matched = 1; + } + } + if (!matched) { + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + UdpSock *st = &g_udp[i]; + if (!st->in_use || st->closing) { + continue; + } + if (!st->bound_port) { + udp_enqueue_sock(st, from_ip, from_port, data, len); + matched = 1; + } + } + } + LeaveCriticalSection(&g_udp_cs); + (void) matched; +} + +ListenState *listen_find(SOCKET s) { + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (g_listen[i].in_use && !g_listen[i].closing && + g_listen[i].listen_sock == s) { + return &g_listen[i]; + } + } + return nullptr; +} + +ListenState *listen_find_port(uint16_t port) { + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (g_listen[i].in_use && !g_listen[i].closing && + g_listen[i].port == port) { + return &g_listen[i]; + } + } + return nullptr; +} + +void listen_destroy_unlocked(ListenState *ls) { + if (ls->event) { + CloseHandle(ls->event); + ls->event = nullptr; + } + DeleteCriticalSection(&ls->cs); + memset(ls, 0, sizeof(*ls)); +} + +ListenState *listen_acquire(SOCKET s) { + ListenState *ls; + + EnterCriticalSection(&g_tcp_cs); + ls = listen_find(s); + if (ls) { + InterlockedIncrement(&ls->refs); + } + LeaveCriticalSection(&g_tcp_cs); + return ls; +} + +ListenState *listen_acquire_port(uint16_t port) { + ListenState *ls; + + EnterCriticalSection(&g_tcp_cs); + ls = listen_find_port(port); + if (ls) { + InterlockedIncrement(&ls->refs); + } + LeaveCriticalSection(&g_tcp_cs); + return ls; +} + +void listen_release(ListenState *ls) { + if (!ls) { + return; + } + EnterCriticalSection(&g_tcp_cs); + if (InterlockedDecrement(&ls->refs) == 0) { + listen_destroy_unlocked(ls); + } + LeaveCriticalSection(&g_tcp_cs); +} + +void nb_set(SOCKET s, int32_t nonblock) { + EnterCriticalSection(&g_nb_cs); + for (int32_t i = 0; i < MAX_NB; i++) { + if (g_nb[i].in_use && g_nb[i].s == s) { + g_nb[i].nonblock = nonblock; + LeaveCriticalSection(&g_nb_cs); + return; + } + } + for (int32_t i = 0; i < MAX_NB; i++) { + if (!g_nb[i].in_use) { + g_nb[i].in_use = 1; + g_nb[i].s = s; + g_nb[i].nonblock = nonblock; + break; + } + } + LeaveCriticalSection(&g_nb_cs); +} + +int32_t nb_get(SOCKET s) { + int32_t v = 0; + EnterCriticalSection(&g_nb_cs); + for (int32_t i = 0; i < MAX_NB; i++) { + if (g_nb[i].in_use && g_nb[i].s == s) { + v = g_nb[i].nonblock; + break; + } + } + LeaveCriticalSection(&g_nb_cs); + return v; +} + +void nb_clear(SOCKET s) { + EnterCriticalSection(&g_nb_cs); + for (int32_t i = 0; i < MAX_NB; i++) { + if (g_nb[i].in_use && g_nb[i].s == s) { + g_nb[i].in_use = 0; + } + } + LeaveCriticalSection(&g_nb_cs); +} + +int32_t sock_overlay_readable(SOCKET s) { + UdpSock *us; + ListenState *ls; + int32_t n = 0; + + EnterCriticalSection(&g_udp_cs); + us = udp_find(s); + if (us) { + EnterCriticalSection(&us->cs); + n = us->q_count; + LeaveCriticalSection(&us->cs); + LeaveCriticalSection(&g_udp_cs); + return n > 0; + } + LeaveCriticalSection(&g_udp_cs); + + EnterCriticalSection(&g_tcp_cs); + ls = listen_find(s); + if (ls) { + EnterCriticalSection(&ls->cs); + n = ls->q_count; + LeaveCriticalSection(&ls->cs); + LeaveCriticalSection(&g_tcp_cs); + return n > 0; + } + LeaveCriticalSection(&g_tcp_cs); + return 0; +} + +int32_t fd_set_has_overlay(const fd_set *set) { + if (!set) { + return 0; + } + for (u_int i = 0; i < set->fd_count; i++) { + if (sock_overlay_readable(set->fd_array[i])) { + return 1; + } + } + return 0; +} + +int32_t fd_set_contains_tracked(const fd_set *set) { + if (!set) { + return 0; + } + EnterCriticalSection(&g_udp_cs); + EnterCriticalSection(&g_tcp_cs); + for (u_int i = 0; i < set->fd_count; i++) { + SOCKET s = set->fd_array[i]; + if (udp_find(s) || listen_find(s)) { + LeaveCriticalSection(&g_tcp_cs); + LeaveCriticalSection(&g_udp_cs); + return 1; + } + } + LeaveCriticalSection(&g_tcp_cs); + LeaveCriticalSection(&g_udp_cs); + return 0; +} + +int32_t merge_overlay_into_fdset(fd_set *set, const fd_set *orig) { + int32_t added = 0; + + if (!set || !orig) { + return 0; + } + for (u_int i = 0; i < orig->fd_count; i++) { + SOCKET s = orig->fd_array[i]; + if (sock_overlay_readable(s) && !FD_ISSET(s, set)) { + if (set->fd_count >= FD_SETSIZE) { + break; + } + FD_SET(s, set); + added++; + } + } + return added; +} + +int32_t tun_sendto_raw(SOCKET s, const char *buf, int32_t n, + const sockaddr_in *to) { + if (sendto_orig) { + return sendto_orig(s, buf, n, 0, + reinterpret_cast(to), sizeof(*to)); + } + return sendto(s, buf, n, 0, + reinterpret_cast(to), sizeof(*to)); +} + +int32_t tun_recvfrom_raw(SOCKET s, char *buf, int32_t n, sockaddr_in *from, + int32_t *flen) { + if (recvfrom_orig) { + return recvfrom_orig(s, buf, n, 0, + reinterpret_cast(from), flen); + } + return recvfrom(s, buf, n, 0, reinterpret_cast(from), flen); +} + +int32_t tun_bind_raw(SOCKET s, const sockaddr *name, int32_t namelen) { + if (bind_orig) { + return bind_orig(s, name, namelen); + } + return bind(s, name, namelen); +} + +int32_t tun_send(uint8_t type, const uint8_t *payload, int32_t plen) { + uint8_t buf[65536]; + int32_t n; + sockaddr_in peer; + int32_t valid; + + if (g_tun_sock == INVALID_SOCKET) { + return -1; + } + valid = peer_get(&peer, nullptr); + if (!valid) { + return -1; + } + if (5 + plen > static_cast(sizeof(buf))) { + return -1; + } + buf[0] = MAGIC0; + buf[1] = MAGIC1; + buf[2] = MAGIC2; + buf[3] = MAGIC3; + buf[4] = type; + if (plen > 0) { + memcpy(buf + 5, payload, static_cast(plen)); + } + n = 5 + plen; + return tun_sendto_raw(g_tun_sock, reinterpret_cast(buf), n, &peer); +} + +int32_t tun_send_udp(uint16_t sport, uint16_t dport, const uint8_t *data, + int32_t len) { + uint8_t buf[MAX_UDP_PAYLOAD + 8]; + int32_t r; + + if (len < 0 || len > MAX_UDP_PAYLOAD) { + return -2; + } + buf[0] = KIND_UDP; + wr16(buf + 1, sport); + wr16(buf + 3, dport); + memcpy(buf + 5, data, static_cast(len)); + r = tun_send(MSG_DATA, buf, 5 + len); + if (r < 0) { + return -1; + } + return len; +} + +void tun_send_tcp_hdr(uint8_t kind, uint32_t conn_id, const uint8_t *extra, + int32_t elen) { + uint8_t buf[MAX_UDP_PAYLOAD + 16]; + int32_t n = 5; + + buf[0] = kind; + wr32(buf + 1, conn_id); + if (extra && elen > 0) { + if (elen > MAX_UDP_PAYLOAD) { + elen = MAX_UDP_PAYLOAD; + } + memcpy(buf + 5, extra, static_cast(elen)); + n = 5 + elen; + } + tun_send(MSG_DATA, buf, n); +} + +void tun_send_tcp_syn(uint32_t conn_id, uint16_t dport, uint16_t sport) { + uint8_t buf[9]; + + buf[0] = KIND_TCP_SYN; + wr32(buf + 1, conn_id); + wr16(buf + 5, dport); + wr16(buf + 7, sport); + tun_send(MSG_DATA, buf, 9); +} + +int32_t conn_wait_reg(uint32_t id, HANDLE ev) { + int32_t ok = 0; + + EnterCriticalSection(&g_conn_wait_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (!g_conn_wait[i].used) { + g_conn_wait[i].used = 1; + g_conn_wait[i].id = id; + g_conn_wait[i].ev = ev; + ok = 1; + break; + } + } + LeaveCriticalSection(&g_conn_wait_cs); + return ok; +} + +void conn_wait_clear(uint32_t id) { + EnterCriticalSection(&g_conn_wait_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_conn_wait[i].used && g_conn_wait[i].id == id) { + g_conn_wait[i].used = 0; + } + } + LeaveCriticalSection(&g_conn_wait_cs); +} + +void conn_wait_signal(uint32_t id) { + EnterCriticalSection(&g_conn_wait_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_conn_wait[i].used && g_conn_wait[i].id == id) { + SetEvent(g_conn_wait[i].ev); + } + } + LeaveCriticalSection(&g_conn_wait_cs); +} + +DWORD WINAPI tcp_relay_thread(LPVOID param) { + TcpConn *c = static_cast(param); + char buf[4096]; + int32_t n; + SOCKET rs = c->relay_sock; + + while (!g_shutdown && c->alive && rs != INVALID_SOCKET) { + fd_set rfds; + timeval tv; + FD_ZERO(&rfds); + FD_SET(rs, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 200000; + n = select_orig(0, &rfds, nullptr, nullptr, &tv); + if (n > 0 && FD_ISSET(rs, &rfds)) { + n = recv_orig(rs, buf, sizeof(buf), 0); + if (n <= 0) { + break; + } + tun_send_tcp_hdr(KIND_TCP_DATA, c->conn_id, + reinterpret_cast(buf), n); + } + } + tun_send_tcp_hdr(KIND_TCP_FIN, c->conn_id, nullptr, 0); + InterlockedExchange(&c->alive, 0); + return 0; +} + +int32_t make_loopback_pair(SOCKET *out_game, SOCKET *out_relay) { + SOCKET lst = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SOCKET a = INVALID_SOCKET; + SOCKET b = INVALID_SOCKET; + sockaddr_in addr; + int32_t alen = sizeof(addr); + u_long nb = 1; + + if (lst == INVALID_SOCKET) { + return -1; + } + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind_orig(lst, reinterpret_cast(&addr), sizeof(addr)) != 0) { + goto fail; + } + if (getsockname_orig(lst, reinterpret_cast(&addr), &alen) != 0) { + goto fail; + } + if (listen_orig(lst, 1) != 0) { + goto fail; + } + a = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (a == INVALID_SOCKET) { + goto fail; + } + if (connect_orig(a, reinterpret_cast(&addr), sizeof(addr)) != 0) { + goto fail; + } + b = accept_orig(lst, nullptr, nullptr); + if (b == INVALID_SOCKET) { + goto fail; + } + closesocket_orig(lst); + if (ioctlsocket_orig) { + ioctlsocket_orig(a, FIONBIO, &nb); + ioctlsocket_orig(b, FIONBIO, &nb); + } else { + ioctlsocket(a, FIONBIO, &nb); + ioctlsocket(b, FIONBIO, &nb); + } + *out_game = a; + *out_relay = b; + return 0; + +fail: + if (lst != INVALID_SOCKET) { + closesocket_orig(lst); + } + if (a != INVALID_SOCKET) { + closesocket_orig(a); + } + if (b != INVALID_SOCKET) { + closesocket_orig(b); + } + return -1; +} + +TcpConn *tcp_alloc(uint32_t conn_id, SOCKET game, SOCKET relay) { + HANDLE th; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (!g_tcp[i].in_use) { + memset(&g_tcp[i], 0, sizeof(g_tcp[i])); + g_tcp[i].in_use = 1; + g_tcp[i].conn_id = conn_id; + g_tcp[i].game_sock = game; + g_tcp[i].relay_sock = relay; + g_tcp[i].alive = 1; + th = CreateThread(nullptr, 0, tcp_relay_thread, &g_tcp[i], 0, nullptr); + if (!th) { + g_tcp[i].in_use = 0; + LeaveCriticalSection(&g_tcp_cs); + return nullptr; + } + g_tcp[i].thread = th; + LeaveCriticalSection(&g_tcp_cs); + return &g_tcp[i]; + } + } + LeaveCriticalSection(&g_tcp_cs); + return nullptr; +} + +void tcp_free_by_index(int32_t i) { + HANDLE th; + SOCKET rs; + + EnterCriticalSection(&g_tcp_cs); + if (!g_tcp[i].in_use) { + LeaveCriticalSection(&g_tcp_cs); + return; + } + InterlockedExchange(&g_tcp[i].alive, 0); + rs = g_tcp[i].relay_sock; + th = g_tcp[i].thread; + g_tcp[i].relay_sock = INVALID_SOCKET; + g_tcp[i].thread = nullptr; + LeaveCriticalSection(&g_tcp_cs); + if (rs != INVALID_SOCKET) { + closesocket_orig(rs); + } + if (th) { + WaitForSingleObject(th, INFINITE); + CloseHandle(th); + } + EnterCriticalSection(&g_tcp_cs); + g_tcp[i].in_use = 0; + LeaveCriticalSection(&g_tcp_cs); +} + +void tcp_free_sock(SOCKET s) { + int32_t hit = -1; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_tcp[i].in_use && + (g_tcp[i].game_sock == s || g_tcp[i].relay_sock == s)) { + hit = i; + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + if (hit >= 0) { + tcp_free_by_index(hit); + } +} + +void tcp_free_id(uint32_t id) { + int32_t hit = -1; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_tcp[i].in_use && g_tcp[i].conn_id == id) { + hit = i; + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + if (hit >= 0) { + tcp_free_by_index(hit); + } +} + +SOCKET tcp_relay_for_id(uint32_t id) { + SOCKET rs = INVALID_SOCKET; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_tcp[i].in_use && g_tcp[i].conn_id == id) { + rs = g_tcp[i].relay_sock; + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + return rs; +} + +void on_tcp_syn(uint32_t conn_id, uint16_t dport, uint16_t sport) { + ListenState *ls; + SOCKET game_side; + SOCKET relay_side; + AcceptItem item; + uint32_t peer_ip; + int32_t queued = 0; + + ls = listen_acquire_port(dport); + if (!ls) { + log_warning("network", "NIC tunnel: TCP_SYN unbound port {}", dport); + return; + } + if (make_loopback_pair(&game_side, &relay_side) != 0) { + log_warning("network", "NIC tunnel: loopback pair failed"); + listen_release(ls); + return; + } + if (!tcp_alloc(conn_id, game_side, relay_side)) { + closesocket_orig(game_side); + closesocket_orig(relay_side); + listen_release(ls); + return; + } + peer_get(nullptr, &peer_ip); + memset(&item, 0, sizeof(item)); + item.client_sock = game_side; + item.peer.sin_family = AF_INET; + item.peer.sin_addr.s_addr = htonl(peer_ip); + item.peer.sin_port = htons(sport); + EnterCriticalSection(&ls->cs); + if (!ls->closing && ls->q_count < MAX_ACCEPT_Q) { + ls->q[ls->q_count++] = item; + sock_signal_read(ls->event); + queued = 1; + } + LeaveCriticalSection(&ls->cs); + if (!queued) { + log_warning("network", + "NIC tunnel: TCP accept queue full dport={}", dport); + closesocket_orig(game_side); + tcp_free_id(conn_id); + tun_send_tcp_hdr(KIND_TCP_FIN, conn_id, nullptr, 0); + listen_release(ls); + return; + } + tun_send_tcp_hdr(KIND_TCP_ACCEPTED, conn_id, nullptr, 0); + log_info("network", "NIC tunnel: TCP accepted conn={} dport={}", + conn_id, dport); + listen_release(ls); +} + +void on_data_payload(const uint8_t *p, int32_t n) { + uint8_t kind; + uint32_t peer_ip; + + if (n < 1) { + return; + } + kind = p[0]; + peer_get(nullptr, &peer_ip); + if (!peer_ip) { + return; + } + if (kind == KIND_UDP && n >= 5) { + udp_enqueue(rd16(p + 3), peer_ip, rd16(p + 1), p + 5, n - 5); + return; + } + if (kind == KIND_TCP_SYN && n >= 9) { + on_tcp_syn(rd32(p + 1), rd16(p + 5), rd16(p + 7)); + return; + } + if (kind == KIND_TCP_ACCEPTED && n >= 5) { + conn_wait_signal(rd32(p + 1)); + return; + } + if (kind == KIND_TCP_DATA && n >= 5) { + SOCKET rs = tcp_relay_for_id(rd32(p + 1)); + if (rs != INVALID_SOCKET && send_orig) { + send_orig(rs, reinterpret_cast(p + 5), n - 5, 0); + } + return; + } + if (kind == KIND_TCP_FIN && n >= 5) { + tcp_free_id(rd32(p + 1)); + } +} + +DWORD WINAPI keepalive_thread(LPVOID) { + uint8_t ipb[4]; + + wr32(ipb, g_local_ip); + while (!g_shutdown) { + for (int32_t i = 0; i < KEEP_ALIVE_SEC * 10 && !g_shutdown; i++) { + Sleep(100); + } + if (g_shutdown || g_tun_sock == INVALID_SOCKET) { + continue; + } + if (!g_is_hub && peer_get(nullptr, nullptr) && + InterlockedCompareExchange(&g_registered, 0, 0) == 0) { + uint8_t reg[4]; + uint8_t pkt[9]; + sockaddr_in peer; + + wr32(reg, g_local_ip); + pkt[0] = MAGIC0; + pkt[1] = MAGIC1; + pkt[2] = MAGIC2; + pkt[3] = MAGIC3; + pkt[4] = MSG_REGISTER; + memcpy(pkt + 5, reg, 4); + if (peer_get(&peer, nullptr)) { + tun_sendto_raw(g_tun_sock, reinterpret_cast(pkt), 9, + &peer); + } + } + tun_send(MSG_KEEPALIVE, ipb, 4); + } + return 0; +} + +DWORD WINAPI tunnel_thread(LPVOID) { + uint8_t buf[65536]; + + while (!g_shutdown) { + sockaddr_in from; + int32_t flen = sizeof(from); + int32_t n; + uint8_t type; + uint8_t *payload; + int32_t plen; + + if (g_tun_sock == INVALID_SOCKET) { + Sleep(50); + continue; + } + n = tun_recvfrom_raw(g_tun_sock, reinterpret_cast(buf), + sizeof(buf), &from, &flen); + if (n < 5) { + if (g_shutdown) { + break; + } + continue; + } + if (buf[0] != MAGIC0 || buf[1] != MAGIC1 || buf[2] != MAGIC2 || + buf[3] != MAGIC3) { + continue; + } + type = buf[4]; + payload = buf + 5; + plen = n - 5; + if (type == MSG_REGISTER && g_is_hub && plen >= 4) { + peer_set(&from, 1, rd32(payload)); + { + uint8_t pkt[10]; + pkt[0] = MAGIC0; + pkt[1] = MAGIC1; + pkt[2] = MAGIC2; + pkt[3] = MAGIC3; + pkt[4] = MSG_REGISTER_ACK; + pkt[5] = ACK_OK; + wr32(pkt + 6, g_local_ip); + tun_sendto_raw(g_tun_sock, reinterpret_cast(pkt), 10, + &from); + } + InterlockedExchange(&g_registered, 1); + { + char peer_str[16]; + ip_to_str(rd32(payload), peer_str, sizeof(peer_str)); + log_info("network", + "NIC tunnel: hub peer registered peer_ip={}", peer_str); + } + continue; + } + if (type == MSG_REGISTER_ACK && plen >= 1) { + if (payload[0] == ACK_OK) { + InterlockedExchange(&g_registered, 1); + if (plen >= 5) { + peer_set(&from, 1, rd32(payload + 1)); + char hub_str[16]; + ip_to_str(rd32(payload + 1), hub_str, sizeof(hub_str)); + log_info("network", + "NIC tunnel: registered ok hub_peer_ip={}", hub_str); + } else { + log_info("network", "NIC tunnel: registered ok"); + } + } else { + log_warning("network", "NIC tunnel: register status={}", + payload[0]); + } + continue; + } + if (type == MSG_KEEPALIVE) { + /* Hub ignores keepalive until REGISTER establishes the peer. */ + if (g_is_hub && + InterlockedCompareExchange(&g_registered, 0, 0) == 0) { + continue; + } + if (plen >= 4) { + peer_set(&from, 1, rd32(payload)); + } else if (g_is_hub) { + peer_set(&from, 1, 0); + } + continue; + } + if (type == MSG_DATA && plen > 0) { + uint32_t peer_ip = 0; + + if (InterlockedCompareExchange(&g_registered, 0, 0) == 0) { + continue; + } + if (g_is_hub) { + peer_set(&from, 1, 0); + } + peer_get(nullptr, &peer_ip); + if (!peer_ip) { + continue; + } + on_data_payload(payload, plen); + } + } + return 0; +} + +int32_t start_tunnel() { + WSADATA wsa; + sockaddr_in bind_addr; + + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { + log_warning("network", "NIC tunnel: WSAStartup failed"); + return -1; + } + g_wsa_started = true; + g_tun_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (g_tun_sock == INVALID_SOCKET) { + log_warning("network", "NIC tunnel: socket create failed"); + goto fail; + } + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (g_is_hub) { + bind_addr.sin_port = htons(g_tunnel_port); + if (tun_bind_raw(g_tun_sock, reinterpret_cast(&bind_addr), + sizeof(bind_addr)) != 0) { + log_warning("network", "NIC tunnel: hub bind fail err={}", + WSAGetLastError()); + goto fail; + } + log_info("network", "NIC tunnel: hub listen UDP {}", g_tunnel_port); + } else { + sockaddr_in peer; + char host[128]; + char *colon; + int32_t port = g_tunnel_port; + size_t hl; + + bind_addr.sin_port = 0; + if (tun_bind_raw(g_tun_sock, reinterpret_cast(&bind_addr), + sizeof(bind_addr)) != 0) { + log_warning("network", "NIC tunnel: client bind fail err={}", + WSAGetLastError()); + goto fail; + } + memset(&peer, 0, sizeof(peer)); + peer.sin_family = AF_INET; + colon = strchr(g_hub, ':'); + if (colon) { + hl = static_cast(colon - g_hub); + if (hl >= sizeof(host)) { + hl = sizeof(host) - 1; + } + memcpy(host, g_hub, hl); + host[hl] = 0; + port = atoi(colon + 1); + } else { + strncpy(host, g_hub, sizeof(host) - 1); + host[sizeof(host) - 1] = 0; + } + peer.sin_port = htons(static_cast(port)); + peer.sin_addr.s_addr = inet_addr(host); + if (peer.sin_addr.s_addr == INADDR_NONE) { + hostent *he = gethostbyname(host); + if (!he) { + log_warning("network", + "NIC tunnel: failed to resolve hub host"); + goto fail; + } + memcpy(&peer.sin_addr, he->h_addr, 4); + } + peer_set(&peer, 1, 0); + { + uint8_t reg[4]; + uint8_t pkt[9]; + + wr32(reg, g_local_ip); + pkt[0] = MAGIC0; + pkt[1] = MAGIC1; + pkt[2] = MAGIC2; + pkt[3] = MAGIC3; + pkt[4] = MSG_REGISTER; + memcpy(pkt + 5, reg, 4); + tun_sendto_raw(g_tun_sock, reinterpret_cast(pkt), 9, &peer); + log_info("network", "NIC tunnel: client register -> {}", g_hub); + } + } + g_tunnel_th = CreateThread(nullptr, 0, tunnel_thread, nullptr, 0, nullptr); + g_keepalive_th = CreateThread(nullptr, 0, keepalive_thread, nullptr, 0, + nullptr); + if (!g_tunnel_th || !g_keepalive_th) { + log_warning("network", "NIC tunnel: thread create failed"); + goto fail; + } + return 0; + +fail: + if (g_tunnel_th) { + InterlockedExchange(&g_shutdown, 1); + WaitForSingleObject(g_tunnel_th, 2000); + CloseHandle(g_tunnel_th); + g_tunnel_th = nullptr; + } + if (g_keepalive_th) { + InterlockedExchange(&g_shutdown, 1); + WaitForSingleObject(g_keepalive_th, 2000); + CloseHandle(g_keepalive_th); + g_keepalive_th = nullptr; + } + InterlockedExchange(&g_shutdown, 0); + if (g_tun_sock != INVALID_SOCKET) { + closesocket(g_tun_sock); + g_tun_sock = INVALID_SOCKET; + } + if (g_wsa_started) { + WSACleanup(); + g_wsa_started = false; + } + return -1; +} + +uint16_t sock_local_port(SOCKET s) { + sockaddr_in a; + int32_t alen = sizeof(a); + + memset(&a, 0, sizeof(a)); + if (getsockname_orig && + getsockname_orig(s, reinterpret_cast(&a), &alen) == 0) { + return ntohs(a.sin_port); + } + return 0; +} + +int WSAAPI bind_hook(SOCKET s, const sockaddr *name, int namelen) { + int32_t r; + UdpSock *us; + int32_t type = 0; + int32_t tlen = sizeof(type); + int icmp_r = 0; + + if (icmphook_try_bind(s, name, namelen, &icmp_r)) { + return icmp_r; + } + + r = bind_orig(s, name, namelen); + if (r != 0 || !g_tunnel_up) { + return r; + } + getsockopt(s, SOL_SOCKET, SO_TYPE, reinterpret_cast(&type), &tlen); + if (type == SOCK_DGRAM) { + us = udp_add(s); + if (us) { + uint16_t p = sock_local_port(s); + us->bound_port = static_cast(p); + } + } + (void) namelen; + return r; +} + +int WSAAPI sendto_hook(SOCKET s, const char *buf, int len, int flags, + const sockaddr *to, int tolen) { + int icmp_r = 0; + + if (icmphook_try_sendto(s, buf, len, flags, to, tolen, &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock || !g_tunnel_up) { + return sendto_orig(s, buf, len, flags, to, tolen); + } + if (to && to->sa_family == AF_INET && len >= 0) { + const sockaddr_in *in = reinterpret_cast(to); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (is_bcast(dip) || in_overlay(dip)) { + uint16_t sport = sock_local_port(s); + uint16_t dport = ntohs(in->sin_port); + UdpSock *us = udp_add(s); + int32_t to_self = (dip == g_local_ip); + + if (us && !us->bound_port) { + us->bound_port = static_cast(sport); + } + if (to_self) { + udp_enqueue(dport, g_local_ip, sport, + reinterpret_cast(buf), len); + return len; + } + { + int32_t tr = tun_send_udp(sport, dport, + reinterpret_cast(buf), len); + if (tr == -2) { + WSASetLastError(WSAEMSGSIZE); + return SOCKET_ERROR; + } + if (tr < 0) { + WSASetLastError(WSAENETUNREACH); + return SOCKET_ERROR; + } + return len; + } + } + } + return sendto_orig(s, buf, len, flags, to, tolen); +} + +int WSAAPI WSASendTo_hook(SOCKET s, LPWSABUF b, DWORD n, LPDWORD sent, + DWORD flags, const sockaddr *to, int tolen, LPWSAOVERLAPPED ov, + LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) { + int icmp_r = 0; + + if (icmphook_try_WSASendTo(s, b, n, sent, flags, to, tolen, ov, cr, + &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock) { + return WSASendTo_orig(s, b, n, sent, flags, to, tolen, ov, cr); + } + if (!ov && !cr && n == 1 && b && to && to->sa_family == AF_INET) { + const sockaddr_in *in = reinterpret_cast(to); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (is_bcast(dip) || in_overlay(dip)) { + int32_t r = sendto_hook(s, reinterpret_cast(b[0].buf), + static_cast(b[0].len), 0, to, tolen); + if (sent) { + *sent = static_cast(r < 0 ? 0 : r); + } + return r < 0 ? SOCKET_ERROR : 0; + } + } + return WSASendTo_orig(s, b, n, sent, flags, to, tolen, ov, cr); +} + +int32_t pop_udp(UdpSock *us, char *buf, int32_t len, sockaddr *from, + int32_t *fromlen, int32_t peek) { + UdpDgram d; + + if (!us || len < 0) { + return -1; + } + EnterCriticalSection(&us->cs); + if (us->closing || us->q_count <= 0) { + LeaveCriticalSection(&us->cs); + return -1; + } + d = us->q[us->q_head]; + if (!peek) { + us->q_head = (us->q_head + 1) % MAX_UDP_Q; + us->q_count--; + } + LeaveCriticalSection(&us->cs); + if (len > d.len) { + len = d.len; + } + memcpy(buf, d.data, static_cast(len)); + if (from && fromlen && *fromlen >= static_cast(sizeof(sockaddr_in))) { + sockaddr_in *in = reinterpret_cast(from); + memset(in, 0, sizeof(*in)); + in->sin_family = AF_INET; + in->sin_addr.s_addr = htonl(d.from_ip); + in->sin_port = htons(d.from_port); + *fromlen = sizeof(*in); + } + return len; +} + +int WSAAPI recvfrom_hook(SOCKET s, char *buf, int len, int flags, + sockaddr *from, int *fromlen) { + UdpSock *us; + int32_t r; + int icmp_r = 0; + const int32_t peek = (flags & MSG_PEEK) ? 1 : 0; + + if (icmphook_try_recvfrom(s, buf, len, flags, from, fromlen, &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock) { + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + if (!g_tunnel_up) { + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + if (len < 0) { + WSASetLastError(WSAEINVAL); + return SOCKET_ERROR; + } + + us = udp_acquire(s); + if (!us) { + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + + for (;;) { + r = pop_udp(us, buf, len, from, fromlen, peek); + if (r >= 0) { + udp_release(us); + return r; + } + if (us->closing) { + udp_release(us); + WSASetLastError(WSAENOTSOCK); + return SOCKET_ERROR; + } + + { + fd_set rfds; + timeval tv; + FD_ZERO(&rfds); + FD_SET(s, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select_orig && select_orig(0, &rfds, nullptr, nullptr, &tv) > 0) { + udp_release(us); + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + } + + if (nb_get(s)) { + udp_release(us); + WSASetLastError(WSAEWOULDBLOCK); + return SOCKET_ERROR; + } + + if (!us->event) { + udp_release(us); + WSASetLastError(WSAENOTSOCK); + return SOCKET_ERROR; + } + if (WaitForSingleObject(us->event, 500) == WAIT_FAILED) { + udp_release(us); + WSASetLastError(WSAENOTSOCK); + return SOCKET_ERROR; + } + } +} + +int WSAAPI WSARecvFrom_hook(SOCKET s, LPWSABUF b, DWORD n, LPDWORD recvd, + LPDWORD flags, sockaddr *from, LPINT fromlen, LPWSAOVERLAPPED ov, + LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) { + int icmp_r = 0; + + if (icmphook_try_WSARecvFrom(s, b, n, recvd, flags, from, fromlen, ov, cr, + &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock) { + return WSARecvFrom_orig(s, b, n, recvd, flags, from, fromlen, ov, cr); + } + if (!ov && !cr && n == 1 && b) { + int32_t fl = fromlen ? *fromlen : 0; + int32_t r = recvfrom_hook(s, reinterpret_cast(b[0].buf), + static_cast(b[0].len), flags ? *flags : 0, from, + fromlen ? &fl : nullptr); + if (fromlen) { + *fromlen = fl; + } + if (r == SOCKET_ERROR) { + return SOCKET_ERROR; + } + if (recvd) { + *recvd = static_cast(r); + } + return 0; + } + return WSARecvFrom_orig(s, b, n, recvd, flags, from, fromlen, ov, cr); +} + +int WSAAPI select_hook(int nfds, fd_set *readfds, fd_set *writefds, + fd_set *exceptfds, const timeval *timeout) { + fd_set orig_r; + int32_t tracked = 0; + DWORD start; + DWORD budget_ms; + + if (readfds) { + orig_r = *readfds; + tracked = fd_set_contains_tracked(&orig_r); + } + if (!tracked) { + return select_orig(nfds, readfds, writefds, exceptfds, timeout); + } + + start = GetTickCount(); + if (!timeout) { + budget_ms = INFINITE; + } else { + budget_ms = static_cast( + timeout->tv_sec * 1000 + timeout->tv_usec / 1000); + if (timeout->tv_sec == 0 && timeout->tv_usec == 0) { + budget_ms = 0; + } + } + + for (;;) { + fd_set r; + fd_set w; + fd_set e; + fd_set *pr = nullptr; + fd_set *pw = nullptr; + fd_set *pe = nullptr; + timeval tv; + int32_t n; + int32_t added; + DWORD elapsed; + DWORD remain; + int32_t slice_ms; + + if (budget_ms == INFINITE) { + slice_ms = 50; + } else { + elapsed = GetTickCount() - start; + if (elapsed >= budget_ms) { + if (readfds) { + FD_ZERO(readfds); + } + if (writefds) { + FD_ZERO(writefds); + } + if (exceptfds) { + FD_ZERO(exceptfds); + } + if (fd_set_has_overlay(&orig_r)) { + int32_t ready = 0; + if (readfds) { + FD_ZERO(readfds); + merge_overlay_into_fdset(readfds, &orig_r); + ready = static_cast(readfds->fd_count); + } + return ready; + } + return 0; + } + remain = budget_ms - elapsed; + slice_ms = static_cast(remain > 50 ? 50 : remain); + } + + if (fd_set_has_overlay(&orig_r)) { + slice_ms = 0; + } + tv.tv_sec = slice_ms / 1000; + tv.tv_usec = (slice_ms % 1000) * 1000; + + if (readfds) { + r = orig_r; + pr = &r; + } + if (writefds) { + w = *writefds; + pw = &w; + } + if (exceptfds) { + e = *exceptfds; + pe = &e; + } + + n = select_orig(nfds, pr, pw, pe, &tv); + if (n == SOCKET_ERROR) { + return SOCKET_ERROR; + } + added = 0; + if (pr) { + added = merge_overlay_into_fdset(pr, &orig_r); + } + if (n + added > 0) { + if (readfds) { + *readfds = r; + } + if (writefds) { + *writefds = w; + } + if (exceptfds) { + *exceptfds = e; + } + return n + added; + } + if (budget_ms == 0) { + return 0; + } + } +} + +int WSAAPI listen_hook(SOCKET s, int backlog) { + int32_t r; + uint16_t port; + HANDLE ev; + + r = listen_orig(s, backlog); + port = sock_local_port(s); + if (r == 0 && port && g_tunnel_up) { + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (!g_listen[i].in_use) { + memset(&g_listen[i], 0, sizeof(g_listen[i])); + ev = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!ev) { + break; + } + g_listen[i].in_use = 1; + g_listen[i].listen_sock = s; + g_listen[i].port = port; + g_listen[i].nonblock = nb_get(s); + g_listen[i].refs = 1; + InitializeCriticalSection(&g_listen[i].cs); + g_listen[i].event = ev; + if (!g_logged_overlay_listen) { + log_misc("network", + "NIC tunnel: overlay listen port={} nb={}", + port, g_listen[i].nonblock); + g_logged_overlay_listen = true; + } + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + } + return r; +} + +int WSAAPI ioctlsocket_hook(SOCKET s, long cmd, u_long *argp) { + int icmp_r = 0; + int32_t r; + + if (icmphook_try_ioctlsocket(s, cmd, argp, &icmp_r)) { + return icmp_r; + } + r = ioctlsocket_orig(s, cmd, argp); + + if (r == 0 && static_cast(cmd) == FIONBIO && argp) { + ListenState *ls; + nb_set(s, (*argp) ? 1 : 0); + EnterCriticalSection(&g_tcp_cs); + ls = listen_find(s); + if (ls) { + ls->nonblock = (*argp) ? 1 : 0; + } + LeaveCriticalSection(&g_tcp_cs); + } + return r; +} + +SOCKET WSAAPI accept_hook(SOCKET s, sockaddr *addr, int *addrlen) { + ListenState *ls; + + ls = listen_acquire(s); + if (!ls) { + return accept_orig(s, addr, addrlen); + } + for (;;) { + EnterCriticalSection(&ls->cs); + if (ls->closing) { + LeaveCriticalSection(&ls->cs); + listen_release(ls); + WSASetLastError(WSAENOTSOCK); + return INVALID_SOCKET; + } + if (ls->q_count > 0) { + AcceptItem it = ls->q[0]; + for (int32_t i = 1; i < ls->q_count; i++) { + ls->q[i - 1] = ls->q[i]; + } + ls->q_count--; + LeaveCriticalSection(&ls->cs); + if (addr && addrlen && + *addrlen >= static_cast(sizeof(it.peer))) { + memcpy(addr, &it.peer, sizeof(it.peer)); + *addrlen = sizeof(it.peer); + } + listen_release(ls); + return it.client_sock; + } + LeaveCriticalSection(&ls->cs); + if (ls->nonblock || nb_get(s)) { + listen_release(ls); + WSASetLastError(WSAEWOULDBLOCK); + return INVALID_SOCKET; + } + if (!ls->event) { + listen_release(ls); + WSASetLastError(WSAENOTSOCK); + return INVALID_SOCKET; + } + WaitForSingleObject(ls->event, 500); + } +} + +int WSAAPI connect_hook(SOCKET s, const sockaddr *name, int namelen) { + if (!g_tunnel_up) { + return connect_orig(s, name, namelen); + } + if (name && name->sa_family == AF_INET) { + const sockaddr_in *in = reinterpret_cast(name); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (in_overlay(dip) && dip != g_local_ip) { + uint32_t cid = static_cast( + InterlockedIncrement(&g_next_conn_id)); + uint16_t dport = ntohs(in->sin_port); + uint16_t sport; + HANDLE ev; + sockaddr_in la; + int32_t was_nb = nb_get(s); + u_long zero = 0; + u_long one = 1; + char dip_str[16]; + + sport = sock_local_port(s); + if (!sport) { + memset(&la, 0, sizeof(la)); + la.sin_family = AF_INET; + la.sin_addr.s_addr = htonl(INADDR_ANY); + la.sin_port = 0; + bind_orig(s, reinterpret_cast(&la), sizeof(la)); + sport = sock_local_port(s); + } + ev = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (!ev) { + WSASetLastError(WSAENOBUFS); + return SOCKET_ERROR; + } + if (!conn_wait_reg(cid, ev)) { + CloseHandle(ev); + WSASetLastError(WSAENOBUFS); + return SOCKET_ERROR; + } + tun_send_tcp_syn(cid, dport, sport); + ip_to_str(dip, dip_str, sizeof(dip_str)); + log_info("network", + "NIC tunnel: TCP connect -> {}:{} conn={}", + dip_str, dport, cid); + if (WaitForSingleObject(ev, 10000) != WAIT_OBJECT_0) { + conn_wait_clear(cid); + CloseHandle(ev); + WSASetLastError(WSAETIMEDOUT); + return SOCKET_ERROR; + } + conn_wait_clear(cid); + CloseHandle(ev); + { + SOCKET lst = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SOCKET rel; + sockaddr_in addr; + int32_t alen = sizeof(addr); + int32_t cr; + int32_t err; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &zero); + } else { + ioctlsocket(s, FIONBIO, &zero); + } + bind_orig(lst, reinterpret_cast(&addr), sizeof(addr)); + getsockname_orig(lst, reinterpret_cast(&addr), &alen); + listen_orig(lst, 1); + cr = connect_orig(s, reinterpret_cast(&addr), + sizeof(addr)); + err = (cr != 0) ? WSAGetLastError() : 0; + if (cr != 0 && err == WSAEWOULDBLOCK) { + fd_set wfds; + timeval tv; + FD_ZERO(&wfds); + FD_SET(s, &wfds); + tv.tv_sec = 2; + tv.tv_usec = 0; + if (select_orig(0, nullptr, &wfds, nullptr, &tv) > 0) { + cr = 0; + err = 0; + } + } + if (cr != 0) { + log_warning("network", + "NIC tunnel: TCP loopback connect fail " + "conn={} err={}", + cid, err); + closesocket_orig(lst); + if (was_nb) { + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &one); + } else { + ioctlsocket(s, FIONBIO, &one); + } + } + WSASetLastError(err ? err : WSAECONNREFUSED); + return SOCKET_ERROR; + } + rel = accept_orig(lst, nullptr, nullptr); + closesocket_orig(lst); + if (rel == INVALID_SOCKET) { + log_warning("network", + "NIC tunnel: TCP loopback accept fail " + "conn={} err={}", + cid, WSAGetLastError()); + if (was_nb) { + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &one); + } else { + ioctlsocket(s, FIONBIO, &one); + } + } + return SOCKET_ERROR; + } + { + u_long nbr = 1; + if (ioctlsocket_orig) { + ioctlsocket_orig(rel, FIONBIO, &nbr); + } else { + ioctlsocket(rel, FIONBIO, &nbr); + } + } + if (was_nb) { + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &one); + } else { + ioctlsocket(s, FIONBIO, &one); + } + nb_set(s, 1); + } else { + nb_set(s, 0); + } + if (!tcp_alloc(cid, s, rel)) { + closesocket_orig(rel); + return SOCKET_ERROR; + } + log_info("network", + "NIC tunnel: TCP connected conn={}", cid); + return 0; + } + } + } + return connect_orig(s, name, namelen); +} + +int WSAAPI WSAConnect_hook(SOCKET s, const sockaddr *name, int namelen, + LPWSABUF caller, LPWSABUF callee, LPQOS sqos, LPQOS gqos) { + if (name && name->sa_family == AF_INET) { + const sockaddr_in *in = reinterpret_cast(name); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (in_overlay(dip) && dip != g_local_ip) { + return connect_hook(s, name, namelen); + } + } + if (WSAConnect_orig) { + return WSAConnect_orig(s, name, namelen, caller, callee, sqos, gqos); + } + return connect_orig(s, name, namelen); +} + +void listen_close(SOCKET s) { + ListenState *ls = nullptr; + AcceptItem drain[MAX_ACCEPT_Q]; + int32_t dn = 0; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (g_listen[i].in_use && g_listen[i].listen_sock == s) { + ls = &g_listen[i]; + break; + } + } + if (ls && !ls->closing) { + ls->closing = 1; + if (ls->event) { + SetEvent(ls->event); + } + EnterCriticalSection(&ls->cs); + dn = ls->q_count; + for (int32_t i = 0; i < dn; i++) { + drain[i] = ls->q[i]; + } + ls->q_count = 0; + LeaveCriticalSection(&ls->cs); + InterlockedIncrement(&ls->refs); + } else { + ls = nullptr; + } + LeaveCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < dn; i++) { + tcp_free_sock(drain[i].client_sock); + closesocket_orig(drain[i].client_sock); + } + if (ls) { + /* Drop close-path ref and the listen_hook base ref. */ + listen_release(ls); + listen_release(ls); + } +} + +int WSAAPI closesocket_hook(SOCKET s) { + int icmp_r = 0; + + if (icmphook_try_closesocket(s, &icmp_r)) { + return icmp_r; + } + udp_remove(s); + tcp_free_sock(s); + nb_clear(s); + listen_close(s); + return closesocket_orig(s); +} + +void install_hooks() { + bool ok = true; + + ok &= detour::trampoline_try("ws2_32.dll", "bind", + bind_hook, &bind_orig); + ok &= detour::trampoline_try("ws2_32.dll", "sendto", + sendto_hook, &sendto_orig); + ok &= detour::trampoline_try("ws2_32.dll", "WSASendTo", + WSASendTo_hook, &WSASendTo_orig); + ok &= detour::trampoline_try("ws2_32.dll", "recvfrom", + recvfrom_hook, &recvfrom_orig); + ok &= detour::trampoline_try("ws2_32.dll", "WSARecvFrom", + WSARecvFrom_hook, &WSARecvFrom_orig); + ok &= detour::trampoline_try("ws2_32.dll", "select", + select_hook, &select_orig); + ok &= detour::trampoline_try("ws2_32.dll", "listen", + listen_hook, &listen_orig); + ok &= detour::trampoline_try("ws2_32.dll", "accept", + accept_hook, &accept_orig); + ok &= detour::trampoline_try("ws2_32.dll", "connect", + connect_hook, &connect_orig); + ok &= detour::trampoline_try("ws2_32.dll", "WSAConnect", + WSAConnect_hook, &WSAConnect_orig); + ok &= detour::trampoline_try("ws2_32.dll", "ioctlsocket", + ioctlsocket_hook, &ioctlsocket_orig); + ok &= detour::trampoline_try("ws2_32.dll", "closesocket", + closesocket_hook, &closesocket_orig); + + { + HMODULE ws = GetModuleHandleA("ws2_32.dll"); + if (!ws) { + ws = LoadLibraryA("ws2_32.dll"); + } + if (ws) { + send_orig = reinterpret_cast( + GetProcAddress(ws, "send")); + recv_orig = reinterpret_cast( + GetProcAddress(ws, "recv")); + getsockname_orig = reinterpret_cast( + GetProcAddress(ws, "getsockname")); + } + } + + if (!send_orig || !recv_orig || !getsockname_orig || !select_orig || + !sendto_orig || !recvfrom_orig || !bind_orig || !closesocket_orig) { + ok = false; + } + + if (!ok) { + log_warning("network", + "NIC tunnel: one or more divert hooks failed to install"); + } else { + log_info("network", + "NIC tunnel: divert hooks installed " + "(bind/sendto/recvfrom/select/listen/accept/connect/...)"); + } +} + +bool install_hooks_ok() { + return send_orig && recv_orig && getsockname_orig && select_orig && + sendto_orig && recvfrom_orig && bind_orig && closesocket_orig && + listen_orig && accept_orig && connect_orig && ioctlsocket_orig; +} + +} // namespace nicspoof_tunnel_detail + +void nicspoof_tunnel_init(const NicSpoofConfig &cfg) { + using namespace nicspoof_tunnel_detail; + + static bool done = false; + char ipstr[16]; + + if (done) { + return; + } + + if (cfg.mode != NicSpoofMode::TunnelHost && + cfg.mode != NicSpoofMode::TunnelClient) { + done = true; + return; + } + + g_is_hub = (cfg.mode == NicSpoofMode::TunnelHost); + g_tunnel_port = cfg.tunnel_port ? cfg.tunnel_port : 51820; + + if (!g_is_hub && cfg.hub_host.empty()) { + log_warning("network", + "NIC tunnel: TunnelClient requires hub host; not starting"); + done = true; + return; + } + + g_local_ip = nicspoof_local_ip(); + g_mask = nicspoof_mask(); + g_subnet = nicspoof_subnet(); + g_peer_ip = 0; + + memset(g_hub, 0, sizeof(g_hub)); + if (!cfg.hub_host.empty()) { + strncpy(g_hub, cfg.hub_host.c_str(), sizeof(g_hub) - 1); + } + + InitializeCriticalSection(&g_peer_cs); + InitializeCriticalSection(&g_udp_cs); + InitializeCriticalSection(&g_tcp_cs); + InitializeCriticalSection(&g_conn_wait_cs); + InitializeCriticalSection(&g_nb_cs); + + ip_to_str(g_local_ip, ipstr, sizeof(ipstr)); + if (g_is_hub) { + log_info("network", + "NIC tunnel init role=host local={} port={}", + ipstr, g_tunnel_port); + } else { + log_info("network", + "NIC tunnel init role=client local={} port={} hub={}", + ipstr, g_tunnel_port, g_hub); + } + + /* Install hooks first with g_tunnel_up=0 so divert stays inert until ready. */ + install_hooks(); + if (!install_hooks_ok()) { + log_warning("network", + "NIC tunnel: aborting; required Winsock trampolines missing"); + done = true; + return; + } + if (start_tunnel() != 0) { + log_warning("network", "NIC tunnel: start failed"); + done = true; + return; + } + InterlockedExchange(&g_tunnel_up, 1); + done = true; +} diff --git a/src/spice2x/hooks/nicspoof_tunnel.h b/src/spice2x/hooks/nicspoof_tunnel.h new file mode 100644 index 00000000..32a062e2 --- /dev/null +++ b/src/spice2x/hooks/nicspoof_tunnel.h @@ -0,0 +1,5 @@ +#pragma once + +#include "nicspoof.h" + +void nicspoof_tunnel_init(const NicSpoofConfig &cfg); diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 09804cd9..2ec4007f 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -258,7 +259,7 @@ int main_implementation(int argc, char *argv[]) { bool load_stubs = false; bool netfix_disable = false; bool icmphook_enable = false; - bool nicspoof_enable = false; + NicSpoofConfig nicspoof_cfg; bool lang_disable = false; std::string process_priority_str = "high"; bool cardio_enabled = false; @@ -801,8 +802,43 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::EnableICMPHook].value_bool()) { icmphook_enable = true; } - if (options[launcher::Options::EnableNICSpoof].value_bool()) { - nicspoof_enable = true; + if (options[launcher::Options::EnableNICSpoof].is_active()) { + const auto &mode = options[launcher::Options::EnableNICSpoof].value_text(); + if (mode == "offline" || mode == "/ENABLED") { + nicspoof_cfg.mode = NicSpoofMode::Offline; + } else if (mode == "tunnelhost") { + nicspoof_cfg.mode = NicSpoofMode::TunnelHost; + } else if (mode == "tunnelclient") { + nicspoof_cfg.mode = NicSpoofMode::TunnelClient; + } else { + log_warning("launcher", "unknown -nicspoof value '{}', ignoring", mode); + } + } + if (nicspoof_cfg.mode != NicSpoofMode::Off) { + if (options[launcher::Options::NICSpoofIP].is_active()) { + unsigned a = 0, b = 0, c = 0, d = 0; + char trail = 0; + const auto &ip = options[launcher::Options::NICSpoofIP].value_text(); + if (sscanf(ip.c_str(), "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) == 4 && + a <= 255 && b <= 255 && c <= 255 && d <= 255) { + nicspoof_cfg.local_ip = (a << 24) | (b << 16) | (c << 8) | d; + } else { + log_warning("launcher", "invalid -nicspoofip '{}'", ip); + } + } + if (options[launcher::Options::NICSpoofHostRealIP].is_active()) { + nicspoof_cfg.hub_host = + options[launcher::Options::NICSpoofHostRealIP].value_text(); + } + if (options[launcher::Options::NICSpoofPort].is_active()) { + const uint32_t p = + options[launcher::Options::NICSpoofPort].value_uint32(); + if (p == 0 || p > 65535) { + log_warning("launcher", "invalid -nicspoofport {}", p); + } else { + nicspoof_cfg.tunnel_port = static_cast(p); + } + } } if (options[launcher::Options::DisableACPHook].value_bool()) { lang_disable = true; @@ -2558,14 +2594,17 @@ 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(); + // NIC spoof / matching tunnel first so divert owns overlapping ws2_32 + // MinHook slots (bind/sendto/recvfrom/...). ICMP then installs only the + // non-overlapping socket-creation hooks and is reached via icmphook_try_*. + if (nicspoof_cfg.mode != NicSpoofMode::Off) { + nicspoof_configure(nicspoof_cfg); + nicspoof_init(); } - // NIC spoof (opt-in; fake Ethernet with no real NIC) - if (nicspoof_enable) { - nicspoof_init(); + // ICMP emulation (opt-in; after tunnel so hooks do not collide) + if (icmphook_enable) { + icmphook_net_init(); } // net fix diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 632258b2..a4922275 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -3188,12 +3188,57 @@ static const std::vector OPTION_DEFINITIONS = std::invoke([]() .category = "Network Dev", }; c[launcher::Options::EnableNICSpoof] = { - .title = "Enable NIC Spoof", + .title = "NIC Spoof", .name = "nicspoof", - .desc = "Report a fake Ethernet IPv4 adapter (10.57.3.2) so the game can " - "boot when no real NIC is present. DNS names are mapped to that " - "address. Does not create a real adapter.", - .type = OptionType::Bool, + .desc = + "Use with -icmphook.\n\n" + "offline: report a fake Ethernet IPv4 adapter and map DNS names to " + "that address so the game can boot when no real NIC is present. " + "Does not create a real adapter.\n\n" + "tunnelhost: same fake NIC, plus listen for a matching tunnel " + "from a peer.\n\n" + "tunnelclient: same fake NIC, plus connect to a tunnel host. " + "Set -nicspoofhostrealip.", + .type = OptionType::Enum, + .category = "Network Dev", + .elements = { + {"offline", ""}, + {"tunnelhost", ""}, + {"tunnelclient", ""}, + }, + }; + c[launcher::Options::NICSpoofIP] = { + .title = "NIC Spoof IP", + .name = "nicspoofip", + .desc = + "Overlay IPv4 for the fake adapter. Used when NIC Spoof is not Default. " + "If unset, 10.100.100.10 is used.\n\n" + "Must be on the same subnet as gateway 10.100.100.1 with mask " + "255.0.0.0 (10.0.0.0/8). Examples: 10.100.100.10, 10.0.0.10. " + "Do not use 10.0.0.0, 10.255.255.255, the gateway 10.100.100.1, " + "or DNS 10.2.1.10 / 10.2.1.30. Host and client must use different IPs.", + .type = OptionType::Text, + .setting_name = "10.100.100.10", + .category = "Network Dev", + }; + c[launcher::Options::NICSpoofHostRealIP] = { + .title = "NIC Spoof Tunnel Host", + .name = "nicspoofhostrealip", + .desc = + "Real IP address or hostname of the tunnel host. Required for " + "tunnel client mode. Not used for offline or tunnel host.", + .type = OptionType::Text, + .setting_name = "192.168.1.10", + .category = "Network Dev", + }; + c[launcher::Options::NICSpoofPort] = { + .title = "NIC Spoof Tunnel Port", + .name = "nicspoofport", + .desc = + "UDP port for the matching tunnel. Host listens on this port; " + "client connects to it. If unset, 51820 is used.", + .type = OptionType::Integer, + .setting_name = "51820", .category = "Network Dev", }; c[launcher::Options::AutoElevate] = { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index adc42de1..c37dcccc 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -317,6 +317,9 @@ namespace launcher { DisableHighResTimer, EnableICMPHook, EnableNICSpoof, + NICSpoofIP, + NICSpoofHostRealIP, + NICSpoofPort, AutoElevate, CfgForceSoftwareRender, OBSWebSocketEnabled,