539 lines
24 KiB
C++
539 lines
24 KiB
C++
// DirectPlay relay: lets players join hosts they cannot reach directly (NAT without port forwarding).
|
|
//
|
|
// DS2 has no working NAT traversal of its own (docs/multiplayer/natneg.md): a joiner enumerates the host's
|
|
// DirectPlay port at the host's public IP and gives up after a timeout. With the relay:
|
|
// host - when IDirectPlay8Server::Host succeeds we register (public IP, DirectPlay port) with the
|
|
// server's relay (UDP 27910, server/ds2master/relay.py) and forward every relayed connection to
|
|
// the local DirectPlay port through its own socket, so DirectPlay sees one peer per joiner.
|
|
// joiner - when IDirectPlay8Client::EnumHosts targets a host outside the LAN we start a local tunnel
|
|
// socket and point DirectPlay at it instead; the enumeration response comes back from the
|
|
// tunnel, so the following Connect goes through it as well.
|
|
// [Network] Relay = auto (relay hosts with public addresses), always, or off.
|
|
//
|
|
// Trust: datagrams on the relay sockets are accepted only from the relay server's address, replies only
|
|
// with our own nonce / connection id, and tunnel traffic only from this PC's LAN address. A host owns its
|
|
// registration through a random per-process token, so another PC behind the same NAT cannot take it over.
|
|
#include "multiplayer.h"
|
|
#include <ws2tcpip.h>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
extern "C" BOOLEAN NTAPI SystemFunction036(PVOID buffer, ULONG length); // RtlGenRandom (advapi32)
|
|
|
|
namespace {
|
|
|
|
const GUID kClsidClient = {0x743F1DC6, 0x5ABA, 0x429F, {0x8B, 0xDF, 0xC5, 0x4D, 0x03, 0x25, 0x3D, 0xC2}};
|
|
const GUID kClsidServer = {0xDA825E1B, 0x6830, 0x43D7, {0x83, 0x5D, 0x0B, 0x5A, 0xD8, 0x29, 0x56, 0xA2}};
|
|
|
|
// Vtable indices from dplay8.h / dpaddr.h (DirectX SDK February 2006).
|
|
constexpr int kClient_EnumHosts = 5;
|
|
constexpr int kServer_Host = 13;
|
|
constexpr int kServer_Close = 23;
|
|
constexpr int kAddr_Release = 2;
|
|
constexpr int kAddr_Duplicate = 5;
|
|
constexpr int kAddr_GetComponentByName = 16;
|
|
constexpr int kAddr_AddComponent = 18;
|
|
constexpr DWORD kDpnaString = 1, kDpnaDword = 2;
|
|
constexpr u_short kRelayPort = 27910;
|
|
constexpr DWORD kDefaultDpPort = 2302;
|
|
constexpr size_t kMaxRelayedPeers = 32; // DS2 allows 8 players; keeps the select() set far below FD_SETSIZE
|
|
constexpr DWORD kTunnelIdleMs = 300000;
|
|
|
|
// Wire format: server/ds2master/relay.py.
|
|
const char kMagic[4] = {'D', 'S', 'R', '2'};
|
|
enum : BYTE {
|
|
kHostReg = 0x01, kHostOk = 0x02, kHostTaken = 0x03, kHostUnreg = 0x04,
|
|
kJoin = 0x10, kJoinOk = 0x11, kJoinFail = 0x12, kConnGone = 0x13,
|
|
kData = 0x20, kPing = 0x30,
|
|
};
|
|
|
|
enum class Mode { Off, Auto, Always };
|
|
Mode g_mode = Mode::Auto;
|
|
std::mutex g_relayLock;
|
|
sockaddr_in g_relay{};
|
|
bool g_relayResolved = false;
|
|
BYTE g_hostToken[8];
|
|
|
|
void Random(void* out, ULONG len) {
|
|
if (!SystemFunction036(out, len)) { // practically never fails; fall back to something non-constant
|
|
DWORD seed = GetTickCount() ^ GetCurrentProcessId() ^ static_cast<DWORD>(reinterpret_cast<UINT_PTR>(out));
|
|
for (ULONG i = 0; i < len; ++i) {
|
|
seed = seed * 1103515245u + 12345u;
|
|
static_cast<BYTE*>(out)[i] = static_cast<BYTE>(seed >> 16);
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename R, typename... A>
|
|
R VCall(void* obj, int index, A... a) {
|
|
auto fn = reinterpret_cast<R(__stdcall*)(void*, A...)>((*static_cast<void***>(obj))[index]);
|
|
return fn(obj, a...);
|
|
}
|
|
|
|
// Point vtable slot `index` of `obj` at `hook`, saving the current function in `*orig` first. DirectPlay
|
|
// objects of one class share a vtable, so this normally runs once per class; the lock keeps two threads
|
|
// creating objects at the same moment from saving the hook as its own original. An unhooked slot with an
|
|
// original already saved means dpnet.dll was unloaded and loaded again: hook the new vtable.
|
|
std::mutex g_patchLock;
|
|
template <typename F>
|
|
void HookSlot(void* obj, int index, void* hook, F* orig, const char* what) {
|
|
std::lock_guard<std::mutex> lock(g_patchLock);
|
|
void** vtbl = *static_cast<void***>(obj);
|
|
if (vtbl[index] == hook) return;
|
|
DWORD old;
|
|
if (!VirtualProtect(&vtbl[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &old)) {
|
|
ds2::log().Write("relay: %s: VirtualProtect failed (%lu)", what, GetLastError());
|
|
return;
|
|
}
|
|
*orig = reinterpret_cast<F>(vtbl[index]); // published before the hook can run
|
|
InterlockedExchangePointer(&vtbl[index], hook);
|
|
VirtualProtect(&vtbl[index], sizeof(void*), old, &old);
|
|
ds2::log().Detail("relay: hooked %s (vtable %p)", what, vtbl);
|
|
}
|
|
|
|
bool ResolveRelay(sockaddr_in* out) {
|
|
std::lock_guard<std::mutex> lock(g_relayLock);
|
|
if (!g_relayResolved) {
|
|
const char* name = mp::ServerName();
|
|
if (!name || !*name) return false;
|
|
addrinfo hints{}, *res = nullptr;
|
|
hints.ai_family = AF_INET;
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
if (getaddrinfo(name, "27910", &hints, &res) != 0 || !res) return false;
|
|
g_relay = *reinterpret_cast<sockaddr_in*>(res->ai_addr);
|
|
freeaddrinfo(res);
|
|
g_relayResolved = true;
|
|
}
|
|
if (out) *out = g_relay;
|
|
return true;
|
|
}
|
|
|
|
void SendRelay(SOCKET s, const sockaddr_in& relay, BYTE type, const void* body, int len) {
|
|
char buf[1600];
|
|
if (len < 0 || len > static_cast<int>(sizeof(buf)) - 5) return;
|
|
std::memcpy(buf, kMagic, 4);
|
|
buf[4] = static_cast<char>(type);
|
|
if (len) std::memcpy(buf + 5, body, len);
|
|
sendto(s, buf, len + 5, 0, reinterpret_cast<const sockaddr*>(&relay), sizeof(relay));
|
|
}
|
|
|
|
// recvfrom on a relay socket: returns the length of a well-formed datagram from the relay server, else -1.
|
|
int RecvRelay(SOCKET s, const sockaddr_in& relay, char* buf, int cap) {
|
|
sockaddr_in from{};
|
|
int fl = sizeof(from);
|
|
int n = recvfrom(s, buf, cap, 0, reinterpret_cast<sockaddr*>(&from), &fl);
|
|
if (n < 5 || from.sin_addr.s_addr != relay.sin_addr.s_addr || from.sin_port != relay.sin_port ||
|
|
std::memcmp(buf, kMagic, 4) != 0)
|
|
return -1;
|
|
return n;
|
|
}
|
|
|
|
SOCKET UdpSocket(in_addr bindAddr, u_short* boundPort) {
|
|
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
if (s == INVALID_SOCKET) return s;
|
|
sockaddr_in a{};
|
|
a.sin_family = AF_INET;
|
|
a.sin_addr = bindAddr;
|
|
int len = sizeof(a);
|
|
if (bind(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)) != 0 ||
|
|
(boundPort && getsockname(s, reinterpret_cast<sockaddr*>(&a), &len) != 0)) {
|
|
closesocket(s);
|
|
return INVALID_SOCKET;
|
|
}
|
|
if (boundPort) *boundPort = ntohs(a.sin_port);
|
|
return s;
|
|
}
|
|
|
|
struct IpText {
|
|
char s[16];
|
|
};
|
|
IpText IpStr(in_addr a) {
|
|
IpText t{};
|
|
InetNtopA(AF_INET, &a, t.s, sizeof(t.s));
|
|
return t;
|
|
}
|
|
|
|
bool IsPrivate(in_addr a) {
|
|
uint32_t h = ntohl(a.s_addr);
|
|
return (h >> 24) == 10 || (h >> 24) == 127 || (h >> 20) == 0xAC1 || (h >> 16) == 0xC0A8 || (h >> 16) == 0xA9FE;
|
|
}
|
|
|
|
// ---- host side ----------------------------------------------------------------------------------
|
|
// One host agent per Host() call, each with its own stop flag. Host() stops the previous agent and waits for
|
|
// it; Close() of the hosting server object only signals it (it exits within 0.5 s and unregisters). If an
|
|
// old agent ever outlives the wait, it still stops on its own flag, and the server ignores its late
|
|
// unregister because it comes from a socket that is no longer the registered one.
|
|
struct HostAgent {
|
|
std::atomic<bool> stop{false};
|
|
DWORD dpPort = 0;
|
|
};
|
|
std::mutex g_hostLock;
|
|
HANDLE g_hostThread = nullptr;
|
|
void* g_hostServer = nullptr;
|
|
std::shared_ptr<HostAgent> g_hostAgent;
|
|
|
|
DWORD WINAPI HostThread(LPVOID param) {
|
|
std::shared_ptr<HostAgent> agent = *static_cast<std::shared_ptr<HostAgent>*>(param);
|
|
delete static_cast<std::shared_ptr<HostAgent>*>(param);
|
|
const DWORD dpPort = agent->dpPort;
|
|
sockaddr_in relay;
|
|
in_addr lan;
|
|
if (!ResolveRelay(&relay) || !mp::PrimaryIPv4(&lan)) {
|
|
ds2::log().Write("relay: host agent not started (no relay address or LAN address)");
|
|
return 0;
|
|
}
|
|
SOCKET up = UdpSocket(in_addr{}, nullptr);
|
|
if (up == INVALID_SOCKET) {
|
|
ds2::log().Write("relay: host agent not started (socket error %d)", WSAGetLastError());
|
|
return 0;
|
|
}
|
|
struct Conn {
|
|
SOCKET s;
|
|
DWORD last;
|
|
};
|
|
std::map<uint32_t, Conn> conns;
|
|
sockaddr_in dp{};
|
|
dp.sin_family = AF_INET;
|
|
dp.sin_addr = lan;
|
|
dp.sin_port = htons(static_cast<u_short>(dpPort));
|
|
BYTE reg[10];
|
|
const u_short portBe = htons(static_cast<u_short>(dpPort));
|
|
std::memcpy(reg, &portBe, 2);
|
|
std::memcpy(reg + 2, g_hostToken, 8);
|
|
DWORD lastReg = GetTickCount() - 20000;
|
|
bool announced = false, taken = false;
|
|
char buf[1600];
|
|
while (!agent->stop) {
|
|
DWORD now = GetTickCount();
|
|
if (now - lastReg >= 10000) {
|
|
SendRelay(up, relay, kHostReg, reg, sizeof(reg));
|
|
lastReg = now;
|
|
}
|
|
fd_set rd;
|
|
FD_ZERO(&rd);
|
|
FD_SET(up, &rd);
|
|
for (auto& kv : conns) FD_SET(kv.second.s, &rd);
|
|
timeval tv{0, 500000};
|
|
if (select(0, &rd, nullptr, nullptr, &tv) > 0) {
|
|
if (FD_ISSET(up, &rd)) {
|
|
int n = RecvRelay(up, relay, buf, sizeof(buf));
|
|
if (n > 0 && buf[4] == kHostOk && !announced) {
|
|
ds2::log().Write("relay: host registered (DirectPlay port %lu)", dpPort);
|
|
announced = true;
|
|
taken = false;
|
|
} else if (n > 0 && buf[4] == kHostTaken && !taken) {
|
|
ds2::log().Write("relay: DirectPlay port %lu at this public IP is already registered by "
|
|
"another PC; relayed joins will go there", dpPort);
|
|
taken = true;
|
|
announced = false;
|
|
} else if (n >= 9 && buf[4] == kData) {
|
|
uint32_t conn;
|
|
std::memcpy(&conn, buf + 5, 4);
|
|
auto it = conns.find(conn);
|
|
if (it == conns.end() && conns.size() < kMaxRelayedPeers) {
|
|
SOCKET s = UdpSocket(lan, nullptr);
|
|
if (s != INVALID_SOCKET) {
|
|
it = conns.emplace(conn, Conn{s, now}).first;
|
|
ds2::log().Detail("relay: new relayed peer %08X", ntohl(conn));
|
|
}
|
|
}
|
|
if (it != conns.end()) {
|
|
it->second.last = now;
|
|
sendto(it->second.s, buf + 9, n - 9, 0, reinterpret_cast<sockaddr*>(&dp), sizeof(dp));
|
|
}
|
|
}
|
|
}
|
|
for (auto& kv : conns) {
|
|
if (!FD_ISSET(kv.second.s, &rd)) continue;
|
|
char out[1600];
|
|
sockaddr_in from{};
|
|
int fl = sizeof(from);
|
|
int n = recvfrom(kv.second.s, out + 4, sizeof(out) - 4 - 5, 0, reinterpret_cast<sockaddr*>(&from), &fl);
|
|
if (n <= 0 || from.sin_addr.s_addr != dp.sin_addr.s_addr || from.sin_port != dp.sin_port) continue;
|
|
std::memcpy(out, &kv.first, 4);
|
|
SendRelay(up, relay, kData, out, n + 4);
|
|
kv.second.last = now;
|
|
}
|
|
}
|
|
for (auto it = conns.begin(); it != conns.end();) {
|
|
if (now - it->second.last > 120000) {
|
|
closesocket(it->second.s);
|
|
it = conns.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
SendRelay(up, relay, kHostUnreg, reg, sizeof(reg));
|
|
for (auto& kv : conns) closesocket(kv.second.s);
|
|
closesocket(up);
|
|
ds2::log().Write("relay: host agent stopped");
|
|
return 0;
|
|
}
|
|
|
|
using ServerHost_t = HRESULT(__stdcall*)(void*, const void*, void** devices, DWORD count, const void*, const void*,
|
|
void*, DWORD);
|
|
using ServerClose_t = HRESULT(__stdcall*)(void*, DWORD);
|
|
ServerHost_t g_ServerHost;
|
|
ServerClose_t g_ServerClose;
|
|
|
|
HRESULT __stdcall HookServerHost(void* self, const void* desc, void** devices, DWORD count, const void* sec,
|
|
const void* cred, void* ctx, DWORD flags) {
|
|
HRESULT hr = g_ServerHost(self, desc, devices, count, sec, cred, ctx, flags);
|
|
if (FAILED(hr) || g_mode == Mode::Off || !ResolveRelay(nullptr)) return hr;
|
|
DWORD port = 0, size = sizeof(port), type = 0;
|
|
if (!devices || !count || !devices[0] ||
|
|
VCall<HRESULT>(devices[0], kAddr_GetComponentByName, L"port", static_cast<void*>(&port), &size, &type) < 0 ||
|
|
!port || port > 0xffff)
|
|
port = kDefaultDpPort;
|
|
std::lock_guard<std::mutex> lock(g_hostLock);
|
|
if (g_hostAgent) g_hostAgent->stop = true;
|
|
if (g_hostThread) { // previous session's agent: wait for it so the two normally never overlap
|
|
if (WaitForSingleObject(g_hostThread, 3000) != WAIT_OBJECT_0)
|
|
ds2::log().Write("relay: previous host agent is still stopping");
|
|
CloseHandle(g_hostThread);
|
|
g_hostThread = nullptr;
|
|
}
|
|
auto agent = std::make_shared<HostAgent>();
|
|
agent->dpPort = port;
|
|
auto* param = new std::shared_ptr<HostAgent>(agent);
|
|
g_hostThread = CreateThread(nullptr, 0, HostThread, param, 0, nullptr);
|
|
if (g_hostThread) {
|
|
g_hostAgent = agent;
|
|
g_hostServer = self;
|
|
} else {
|
|
ds2::log().Write("relay: could not start the host agent (%lu)", GetLastError());
|
|
delete param;
|
|
g_hostAgent.reset();
|
|
g_hostServer = nullptr;
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT __stdcall HookServerClose(void* self, DWORD flags) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_hostLock);
|
|
if (self == g_hostServer) {
|
|
if (g_hostAgent) g_hostAgent->stop = true;
|
|
g_hostServer = nullptr;
|
|
}
|
|
}
|
|
return g_ServerClose(self, flags);
|
|
}
|
|
|
|
// ---- joiner side --------------------------------------------------------------------------------
|
|
// A tunnel is owned by its thread. The map only hands out the local port (under the lock) and records
|
|
// the last use, so an enumeration can never see a tunnel that is being torn down.
|
|
struct Tunnel {
|
|
in_addr hostIp;
|
|
u_short hostPort;
|
|
SOCKET local;
|
|
u_short localPort;
|
|
DWORD lastUse; // guarded by g_tunnelsLock
|
|
};
|
|
std::mutex g_tunnelsLock;
|
|
std::map<uint64_t, Tunnel*> g_tunnels;
|
|
|
|
uint64_t TunnelKey(in_addr ip, u_short port) { return (static_cast<uint64_t>(ip.s_addr) << 16) | port; }
|
|
|
|
// Removes the tunnel from the map if it has been idle (or `force`); returns true if it was removed.
|
|
bool RetireTunnel(Tunnel* t, DWORD lastActivity, bool force) {
|
|
std::lock_guard<std::mutex> lock(g_tunnelsLock);
|
|
DWORD now = GetTickCount();
|
|
if (!force && (now - lastActivity <= kTunnelIdleMs || now - t->lastUse <= kTunnelIdleMs)) return false;
|
|
g_tunnels.erase(TunnelKey(t->hostIp, t->hostPort));
|
|
return true;
|
|
}
|
|
|
|
DWORD WINAPI TunnelThread(LPVOID param) {
|
|
auto* t = static_cast<Tunnel*>(param);
|
|
sockaddr_in relay;
|
|
in_addr lan{};
|
|
SOCKET up = INVALID_SOCKET;
|
|
if (ResolveRelay(&relay) && mp::PrimaryIPv4(&lan)) up = UdpSocket(in_addr{}, nullptr);
|
|
if (up == INVALID_SOCKET) {
|
|
ds2::log().Write("relay: tunnel to %s:%u could not start", IpStr(t->hostIp).s, t->hostPort);
|
|
RetireTunnel(t, 0, true);
|
|
} else {
|
|
uint32_t conn = 0, nonce = 0;
|
|
Random(&nonce, sizeof(nonce));
|
|
sockaddr_in dpPeer{};
|
|
bool havePeer = false;
|
|
DWORD now = GetTickCount(), nextJoin = now, lastPing = now, lastActivity = now;
|
|
char buf[1600];
|
|
char join[10];
|
|
std::memcpy(join, &t->hostIp, 4);
|
|
u_short hp = htons(t->hostPort);
|
|
std::memcpy(join + 4, &hp, 2);
|
|
std::memcpy(join + 6, &nonce, 4);
|
|
for (;;) {
|
|
now = GetTickCount();
|
|
if (!conn && static_cast<LONG>(now - nextJoin) >= 0) {
|
|
SendRelay(up, relay, kJoin, join, sizeof(join));
|
|
nextJoin = now + 1000;
|
|
}
|
|
if (conn && now - lastPing > 10000) {
|
|
SendRelay(up, relay, kPing, &conn, 4);
|
|
lastPing = now;
|
|
}
|
|
if (now - lastActivity > kTunnelIdleMs && RetireTunnel(t, lastActivity, false)) {
|
|
ds2::log().Write("relay: tunnel to %s:%u closed (idle)", IpStr(t->hostIp).s, t->hostPort);
|
|
break;
|
|
}
|
|
fd_set rd;
|
|
FD_ZERO(&rd);
|
|
FD_SET(up, &rd);
|
|
FD_SET(t->local, &rd);
|
|
timeval tv{0, 500000};
|
|
if (select(0, &rd, nullptr, nullptr, &tv) <= 0) continue;
|
|
if (FD_ISSET(t->local, &rd)) {
|
|
sockaddr_in from{};
|
|
int fl = sizeof(from);
|
|
int n = recvfrom(t->local, buf + 4, sizeof(buf) - 4 - 5, 0, reinterpret_cast<sockaddr*>(&from), &fl);
|
|
if (n > 0 && from.sin_addr.s_addr == lan.s_addr) { // only DirectPlay on this PC
|
|
dpPeer = from;
|
|
havePeer = true;
|
|
lastActivity = now;
|
|
if (conn) {
|
|
std::memcpy(buf, &conn, 4);
|
|
SendRelay(up, relay, kData, buf, n + 4);
|
|
}
|
|
}
|
|
}
|
|
if (FD_ISSET(up, &rd)) {
|
|
int n = RecvRelay(up, relay, buf, sizeof(buf));
|
|
if (n < 9 || std::memcmp(buf + 5, buf[4] == kData || buf[4] == kConnGone ? &conn : &nonce, 4) != 0) continue;
|
|
if (buf[4] == kJoinOk && n >= 13 && !conn) {
|
|
std::memcpy(&conn, buf + 9, 4);
|
|
ds2::log().Write("relay: tunnel to %s:%u established (conn %08X)", IpStr(t->hostIp).s,
|
|
t->hostPort, ntohl(conn));
|
|
} else if (buf[4] == kConnGone && conn) { // server forgot us (host re-hosted, expiry, restart)
|
|
ds2::log().Write("relay: tunnel to %s:%u lost its connection, joining again", IpStr(t->hostIp).s,
|
|
t->hostPort);
|
|
conn = 0;
|
|
Random(&nonce, sizeof(nonce));
|
|
std::memcpy(join + 6, &nonce, 4);
|
|
nextJoin = now;
|
|
} else if (buf[4] == kJoinFail && !conn) {
|
|
ds2::log().Write("relay: host %s:%u is not registered with the relay", IpStr(t->hostIp).s,
|
|
t->hostPort);
|
|
nextJoin = now + 3000; // back off
|
|
} else if (buf[4] == kData && conn && havePeer) {
|
|
sendto(t->local, buf + 9, n - 9, 0, reinterpret_cast<sockaddr*>(&dpPeer), sizeof(dpPeer));
|
|
lastActivity = now;
|
|
}
|
|
}
|
|
}
|
|
closesocket(up);
|
|
}
|
|
closesocket(t->local);
|
|
delete t;
|
|
return 0;
|
|
}
|
|
|
|
// Local tunnel port for (ip, port), starting the tunnel if needed; 0 on failure.
|
|
u_short EnsureTunnel(in_addr ip, u_short port) {
|
|
std::lock_guard<std::mutex> lock(g_tunnelsLock);
|
|
auto it = g_tunnels.find(TunnelKey(ip, port));
|
|
if (it != g_tunnels.end()) {
|
|
it->second->lastUse = GetTickCount();
|
|
return it->second->localPort;
|
|
}
|
|
in_addr lan;
|
|
if (!mp::PrimaryIPv4(&lan)) return 0;
|
|
auto* t = new Tunnel{ip, port, INVALID_SOCKET, 0, GetTickCount()};
|
|
t->local = UdpSocket(lan, &t->localPort);
|
|
if (t->local == INVALID_SOCKET) {
|
|
delete t;
|
|
return 0;
|
|
}
|
|
g_tunnels[TunnelKey(ip, port)] = t;
|
|
HANDLE th = CreateThread(nullptr, 0, TunnelThread, t, 0, nullptr);
|
|
if (!th) {
|
|
ds2::log().Write("relay: could not start a tunnel thread (%lu)", GetLastError());
|
|
g_tunnels.erase(TunnelKey(ip, port));
|
|
closesocket(t->local);
|
|
delete t;
|
|
return 0;
|
|
}
|
|
CloseHandle(th);
|
|
u_short localPort = t->localPort; // t belongs to the thread from here on (we still hold the lock)
|
|
ds2::log().Write("relay: joining %s:%u through the relay (local tunnel port %u)", IpStr(ip).s, port, localPort);
|
|
return localPort;
|
|
}
|
|
|
|
using EnumHosts_t = HRESULT(__stdcall*)(void*, void*, void* addrHost, void*, void*, DWORD, DWORD, DWORD, DWORD, void*,
|
|
void*, DWORD);
|
|
EnumHosts_t g_EnumHosts;
|
|
|
|
HRESULT __stdcall HookEnumHosts(void* self, void* desc, void* addrHost, void* device, void* user, DWORD userSize,
|
|
DWORD count, DWORD retry, DWORD timeout, void* ctx, void* handle, DWORD flags) {
|
|
if (g_mode != Mode::Off && addrHost && ResolveRelay(nullptr)) {
|
|
wchar_t host[128] = {};
|
|
DWORD size = sizeof(host) - sizeof(wchar_t), type = 0, port = 0;
|
|
in_addr ip{};
|
|
if (VCall<HRESULT>(addrHost, kAddr_GetComponentByName, L"hostname", static_cast<void*>(host), &size, &type) >=
|
|
0 &&
|
|
InetPtonW(AF_INET, host, &ip) == 1 && (g_mode == Mode::Always || !IsPrivate(ip))) {
|
|
size = sizeof(port);
|
|
if (VCall<HRESULT>(addrHost, kAddr_GetComponentByName, L"port", static_cast<void*>(&port), &size, &type) < 0 ||
|
|
!port || port > 0xffff)
|
|
port = kDefaultDpPort;
|
|
in_addr lan;
|
|
DWORD lp = EnsureTunnel(ip, static_cast<u_short>(port));
|
|
void* dup = nullptr;
|
|
if (lp && mp::PrimaryIPv4(&lan) && VCall<HRESULT>(addrHost, kAddr_Duplicate, &dup) >= 0 && dup) {
|
|
wchar_t lanStr[32];
|
|
InetNtopW(AF_INET, &lan, lanStr, 32);
|
|
VCall<HRESULT>(dup, kAddr_AddComponent, L"hostname", static_cast<const void*>(lanStr),
|
|
static_cast<DWORD>((wcslen(lanStr) + 1) * sizeof(wchar_t)), kDpnaString);
|
|
VCall<HRESULT>(dup, kAddr_AddComponent, L"port", static_cast<const void*>(&lp), static_cast<DWORD>(4),
|
|
kDpnaDword);
|
|
HRESULT hr = g_EnumHosts(self, desc, dup, device, user, userSize, count, retry, timeout, ctx, handle, flags);
|
|
VCall<ULONG>(dup, kAddr_Release);
|
|
return hr;
|
|
}
|
|
}
|
|
}
|
|
return g_EnumHosts(self, desc, addrHost, device, user, userSize, count, retry, timeout, ctx, handle, flags);
|
|
}
|
|
|
|
// ---- COM interception -----------------------------------------------------------------------------
|
|
using CoCreateInstance_t = HRESULT(__stdcall*)(const GUID&, void*, DWORD, const GUID&, void**);
|
|
CoCreateInstance_t g_CoCreateInstance;
|
|
|
|
HRESULT __stdcall HookCoCreateInstance(const GUID& clsid, void* outer, DWORD context, const GUID& iid, void** out) {
|
|
HRESULT hr = g_CoCreateInstance(clsid, outer, context, iid, out);
|
|
if (FAILED(hr) || !out || !*out) return hr;
|
|
if (std::memcmp(&clsid, &kClsidClient, sizeof(GUID)) == 0) {
|
|
HookSlot(*out, kClient_EnumHosts, reinterpret_cast<void*>(HookEnumHosts), &g_EnumHosts,
|
|
"IDirectPlay8Client::EnumHosts");
|
|
} else if (std::memcmp(&clsid, &kClsidServer, sizeof(GUID)) == 0) {
|
|
HookSlot(*out, kServer_Host, reinterpret_cast<void*>(HookServerHost), &g_ServerHost, "IDirectPlay8Server::Host");
|
|
HookSlot(*out, kServer_Close, reinterpret_cast<void*>(HookServerClose), &g_ServerClose,
|
|
"IDirectPlay8Server::Close");
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace mp {
|
|
|
|
void InstallRelay(const ds2::Ini& ini) {
|
|
if (TestLoopback()) { g_mode = Mode::Off; return; }
|
|
char mode[16];
|
|
ini.String("Network", "Relay", "auto", mode, sizeof(mode));
|
|
g_mode = _stricmp(mode, "off") == 0 ? Mode::Off : _stricmp(mode, "always") == 0 ? Mode::Always : Mode::Auto;
|
|
if (g_mode == Mode::Off || !ServerName()[0]) return;
|
|
Random(g_hostToken, sizeof(g_hostToken));
|
|
g_CoCreateInstance = static_cast<CoCreateInstance_t>(
|
|
ds2::HookImport("ole32.dll", "CoCreateInstance", reinterpret_cast<void*>(HookCoCreateInstance), "CoCreateInstance (relay)"));
|
|
ds2::log().Write("relay: mode %s via %s:%u", mode, ServerName(), kRelayPort);
|
|
}
|
|
|
|
} // namespace mp
|