Files
TeslaRel410/emulator/vpx-device/steamlink.cpp
T
CydandClaude Fable 5 f2687a3146 BT410 5.3.72: the fault caught in the act; the inside view gets its cockpit
VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched
linear address, dump guest registers, the last 64 serial RX deliveries with
guest cs:eip at each, the code bytes at the faulting EIP and the stack top.
serialnamedpipe's doReceive feeds the ring.  Armed in podrun.sh and
launch_pod.ps1.

The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4
in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment
F000 offset CA60, a BIOS default interrupt handler -- and the faulting access
is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2
7000FA64.  The host probes a word 0x3004 bytes past a real-mode vector value
treated as a flat pointer: harmless for its own low-memory handlers, a fault
for BIOS F000:xxxx defaults.  The serial ring shows a steady 1-byte/1-3ms
vRIO stream with nothing special at the fault -- the stream determines which
vectors get walked, not the crash itself.  The 0x3004 appears nowhere in
DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code
bytes at EIP; faulthunt.sh loops runs until the next catch.

Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet
discriminating.

The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant
naming convention (MAD->MAX etc., all 64 skeletons present) selects the same
25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell
with the PUNCH-texel windows from the capture forensics.  The donor names the
same mechanism from the decomp side (inside = SkeletonType_A with '_cop'
selection).  Fallback to the body skeleton when no X file exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:09:22 -05:00

1057 lines
40 KiB
C++

/*
* steamlink.cpp -- VWE Steam transport core. Implements steamlink.h; that
* header carries the API contract, the wire format and the
* why-reliable-not-unreliable rationale. Addressing, the session file and the
* gateway's forwarding rules live in ../steam/SESSION-CONTRACT.md.
*
* This file is compiled VERBATIM into two binaries -- the fork's
* `backend=steam` NE2000 backend and the host's steam<->pcap gateway -- so it
* pulls in nothing from DOSBox-X (no dosbox.h, no logging.h, no config.h) and
* never printf()s. Every diagnostic goes out through the LogFn the caller
* installs; that is the only reason the two consumers can route logs
* differently without a fork of this file.
*
* Steamworks is bound at RUNTIME, the same way ethernet_pcap.cpp binds
* WPCAP.DLL: a build made on a machine with no Steam SDK still compiles and
* runs, and `backend=steam` just fails Init() with a readable message the way
* a missing Npcap fails the pcap backend.
*/
#include "steamlink.h"
#include <cstdarg>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <map>
#include <set>
namespace vwesteam {
static LogFn g_log = 0;
void SetLog(LogFn fn) { g_log = fn; }
static void Log(const char* fmt, ...)
{
if (!g_log) return;
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
buf[sizeof(buf) - 1] = 0;
g_log(buf);
}
/* Powers of 2 only, so a whole mission's frames cost ~20 log lines. Same
* throttle the pcap backend's "PCAP TX/RX #n" lines use. */
static inline bool LogNth(unsigned long long n) { return (n & (n - 1)) == 0; }
} // namespace vwesteam
#if defined(WIN32) || defined(_WIN32)
#include <windows.h>
#ifdef VWE_STEAM_USE_SDK_HEADERS
/* Validation build: -DVWE_STEAM_USE_SDK_HEADERS -I<sdk>/public. The struct
* declarations below are then the SDK's own and the static_asserts check
* Valve's layout instead of our transcription of it. */
#include <steam/steam_api.h>
#include <steam/steam_api_flat.h>
#include <steam/isteammatchmaking.h>
#include <steam/isteamnetworkingmessages.h>
#include <steam/steamnetworkingtypes.h>
#endif
namespace vwesteam {
/* ------------------------------------------------------------------ *
* Protocol constants (SDK name in the comment; we spell them locally so
* the SDK-headers build cannot collide with Valve's enums).
* ------------------------------------------------------------------ */
enum {
IDENTITY_TYPE_STEAMID = 16, /* k_ESteamNetworkingIdentityType_SteamID */
SEND_NONAGLE = 1, /* k_nSteamNetworkingSend_NoNagle */
SEND_RELIABLE = 8, /* k_nSteamNetworkingSend_Reliable */
SEND_FLAGS = SEND_RELIABLE | SEND_NONAGLE,
RESULT_OK = 1, /* k_EResultOK */
CB_SESSION_REQUEST = 1251, /* SteamNetworkingMessagesSessionRequest_t */
CB_SESSION_FAILED = 1252, /* SteamNetworkingMessagesSessionFailed_t */
CB_LOBBY_CHAT_UPDATE = 506, /* LobbyChatUpdate_t */
CHAT_MEMBER_ENTERED = 0x0001/* k_EChatMemberStateChangeEntered */
};
namespace sdkabi {
#ifdef VWE_STEAM_USE_SDK_HEADERS
typedef ::SteamNetworkingIdentity SteamNetworkingIdentity;
typedef ::SteamNetworkingMessage_t SteamNetworkingMessage_t;
typedef ::CallbackMsg_t CallbackMsg_t;
#else
/* Hand-declared, ABI-compatible with Steamworks SDK 1.62 on x86-64. Only the
* three structs that cross the DLL boundary are here; everything else the
* flat API takes is a scalar. */
struct SteamNetworkingIdentity {
int32_t m_eType; /* ESteamNetworkingIdentityType */
int32_t m_cbSize; /* 8 when carrying a SteamID64 */
union {
uint64_t m_steamID64;
uint8_t m_genericBytes[32];
char m_szUnknownRawString[128];
uint32_t m_reserved[32]; /* this is what pins the struct at 136 bytes */
};
};
struct SteamNetworkingMessage_t {
void* m_pData;
int32_t m_cbSize;
uint32_t m_conn; /* HSteamNetConnection */
SteamNetworkingIdentity m_identityPeer;
int64_t m_nConnUserData;
int64_t m_usecTimeReceived;
int64_t m_nMessageNumber;
void (*m_pfnFreeData)(SteamNetworkingMessage_t*);
void (*m_pfnRelease)(SteamNetworkingMessage_t*);
int32_t m_nChannel;
int32_t m_nFlags;
int64_t m_nUserData;
uint16_t m_idxLane;
uint16_t _pad1__;
};
struct CallbackMsg_t {
int32_t m_hSteamUser; /* HSteamUser */
int32_t m_iCallback;
uint8_t* m_pubParam;
int32_t m_cubParam;
};
#endif /* VWE_STEAM_USE_SDK_HEADERS */
/* m_eType is an int here and a Valve enum in the SDK build; the casts keep one
* body compiling both ways. */
static inline void IdentitySetSteamID(SteamNetworkingIdentity& id, uint64_t steamId)
{
memset(&id, 0, sizeof(id));
id.m_eType = static_cast<decltype(id.m_eType)>(IDENTITY_TYPE_STEAMID);
id.m_cbSize = static_cast<decltype(id.m_cbSize)>(sizeof(uint64_t));
id.m_steamID64 = steamId;
}
static inline uint64_t IdentityGetSteamID(const SteamNetworkingIdentity& id)
{
if ((int)id.m_eType != IDENTITY_TYPE_STEAMID) return 0;
return id.m_steamID64;
}
} // namespace sdkabi
/* ================================================================== *
* LAYOUT IS LOAD-BEARING -- READ BEFORE TOUCHING THE STRUCTS ABOVE.
*
* steam_api64.dll allocates SteamNetworkingMessage_t and the callback
* payloads; we only index into them. A wrong offset is not a compile
* error and not a crash, it is silent memory corruption inside a
* process that runs all evening.
*
* Layout transcribed from Steamworks SDK 1.62 (steamnetworkingtypes.h,
* steamclientpublic.h, isteamclient.h), x86-64. To CHECK it against the
* real headers rather than trust the transcription, build this file with
* -DVWE_STEAM_USE_SDK_HEADERS -I<sdk>/public -- the asserts below then
* run against Valve's own declarations and any drift fails the build.
* ================================================================== */
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Winvalid-offsetof" /* SDK structs carry methods */
#endif
static_assert(sizeof(void*) == 8,
"steam_api64.dll is x86-64 -- build steamlink.cpp 64-bit");
static_assert(sizeof(sdkabi::SteamNetworkingIdentity) == 136,
"SteamNetworkingIdentity must be 136 bytes");
static_assert(offsetof(sdkabi::SteamNetworkingMessage_t, m_identityPeer) == 16,
"SteamNetworkingMessage_t::m_identityPeer must sit at offset 16");
static_assert(offsetof(sdkabi::SteamNetworkingMessage_t, m_pfnRelease) == 184,
"SteamNetworkingMessage_t tail drifted -- re-check against the SDK");
static_assert(sizeof(sdkabi::SteamNetworkingMessage_t) == 216,
"SteamNetworkingMessage_t must be 216 bytes");
static_assert(sizeof(sdkabi::CallbackMsg_t) == 24,
"CallbackMsg_t must be 24 bytes");
#ifdef VWE_STEAM_USE_SDK_HEADERS
static_assert((int)CB_SESSION_REQUEST == (int)SteamNetworkingMessagesSessionRequest_t::k_iCallback, "");
static_assert((int)CB_SESSION_FAILED == (int)SteamNetworkingMessagesSessionFailed_t::k_iCallback, "");
static_assert((int)CB_LOBBY_CHAT_UPDATE == (int)LobbyChatUpdate_t::k_iCallback, "");
static_assert((int)IDENTITY_TYPE_STEAMID == (int)k_ESteamNetworkingIdentityType_SteamID, "");
static_assert((int)SEND_FLAGS == (int)(k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_NoNagle), "");
#endif
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
using sdkabi::SteamNetworkingIdentity;
using sdkabi::SteamNetworkingMessage_t;
using sdkabi::CallbackMsg_t;
/* ------------------------------------------------------------------ *
* Runtime-bound entry points (flat C exports of steam_api64.dll -- the
* same surface every non-C++ language binding consumes, and the only
* one that is stable enough to GetProcAddress).
* ------------------------------------------------------------------ */
static int (__cdecl *SteamInitFlat)(char* errmsg) = 0; /* 0 == OK */
static bool (__cdecl *SteamInitOld)(void) = 0; /* pre-1.59 fallback */
static void (__cdecl *SteamShutdown)(void) = 0;
static int32_t (__cdecl *SteamGetPipe)(void) = 0;
static void (__cdecl *DispatchInit)(void) = 0;
static void (__cdecl *DispatchRunFrame)(int32_t pipe) = 0;
static bool (__cdecl *DispatchGetNext)(int32_t pipe, CallbackMsg_t* out) = 0;
static void (__cdecl *DispatchFreeLast)(int32_t pipe) = 0;
static void* (__cdecl *GetMessagesIface)(void) = 0;
static int (__cdecl *MsgSendToUser)(void* self, const SteamNetworkingIdentity* to,
const void* data, uint32_t len,
int flags, int channel) = 0;
static int (__cdecl *MsgReceiveOnChannel)(void* self, int channel,
SteamNetworkingMessage_t** out, int max) = 0;
static bool (__cdecl *MsgAcceptSession)(void* self, const SteamNetworkingIdentity* who) = 0;
static bool (__cdecl *MsgCloseSession)(void* self, const SteamNetworkingIdentity* who) = 0;
static void (__cdecl *MsgRelease)(SteamNetworkingMessage_t* msg) = 0;
static void* (__cdecl *GetUserIface)(void) = 0;
static uint64_t (__cdecl *UserGetSteamID)(void* self) = 0;
/* optional -- absence degrades to "no lobby roster", never a failed Init */
static void* (__cdecl *GetMatchmakingIface)(void) = 0;
static uint64_t (__cdecl *LobbyJoin)(void* self, uint64_t lobby) = 0;
static int (__cdecl *LobbyNumMembers)(void* self, uint64_t lobby) = 0;
static uint64_t (__cdecl *LobbyMemberByIndex)(void* self, uint64_t lobby, int i) = 0;
/* optional -- route diagnostics only (SetRouteDiagnostics) */
typedef void (__cdecl *SteamDebugOutputFn)(int level, const char* msg);
static void* (__cdecl *GetUtilsIface)(void) = 0;
static void (__cdecl *UtilsSetDebugOutput)(void* self, int level, SteamDebugOutputFn fn) = 0;
/* Version-suffixed accessors change between SDK releases, so try an ordered
* list and log which one bound -- that log line is the first thing to look at
* when a new Steam client ships and Init suddenly fails. */
static const char* const kUserNames[] = {
"SteamAPI_SteamUser_v023", "SteamAPI_SteamUser_v022", "SteamAPI_SteamUser_v021"
};
static const char* const kMessagesNames[] = {
"SteamAPI_SteamNetworkingMessages_SteamAPI_v002"
};
static const char* const kMatchmakingNames[] = {
"SteamAPI_SteamMatchmaking_v009"
};
/* Only used by SetRouteDiagnostics(). Optional everywhere -- absence costs a
* diagnostic, not the link. */
static const char* const kUtilsNames[] = {
"SteamAPI_SteamNetworkingUtils_SteamAPI_v004",
"SteamAPI_SteamNetworkingUtils_SteamAPI_v003"
};
static FARPROC BindFirst(HMODULE dll, const char* const* names, size_t count,
const char** bound)
{
for (size_t i = 0; i < count; i++) {
FARPROC p = GetProcAddress(dll, names[i]);
if (p) { *bound = names[i]; return p; }
}
*bound = 0;
return 0;
}
static bool g_bound = false;
static std::string g_bindErr;
/* Same idiom as LoadPcapLibrary(); one macro deep because there are twenty of
* these and a hand-spelled cast per entry point is where a signature typo
* hides. decltype keeps the cast welded to the declaration above. */
#define BIND_ONE(ptr, name) \
do { ptr = (decltype(ptr))GetProcAddress(dll, (name)); } while (0)
#define BIND_ANY(ptr, names, what) \
do { const char* _w = 0; \
ptr = (decltype(ptr))BindFirst(dll, names, sizeof(names)/sizeof(names[0]), &_w); \
if (_w) Log("STEAM bound %s", _w); \
else Log("STEAM no %s accessor in steam_api64.dll", what); } while (0)
static bool BindSteamApi(std::string& err)
{
/* Cache the handle forever, like LoadPcapLibrary: -1 = never tried,
* NULL = tried and failed (don't thrash LoadLibrary on every retry). */
static HMODULE dll = (HMODULE)-1;
if (dll != (HMODULE)-1) { err = g_bindErr; return g_bound; }
dll = LoadLibraryA("steam_api64.dll");
if (!dll) {
g_bindErr = "steam_api64.dll not found -- copy the Steamworks redistributable "
"(redistributable_bin/win64) next to the executable";
err = g_bindErr;
return false;
}
#ifdef __MINGW32__
// C++ defines function and data pointers as separate types to reflect
// Harvard architecture machines (like the Arduino). As such, casting
// between them isn't portable and GCC will helpfully warn us about it.
// We're only running this code on Windows which explicitly allows this
// behaviour, so silence the warning to avoid confusion.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
BIND_ONE(SteamInitFlat, "SteamAPI_InitFlat");
BIND_ONE(SteamInitOld, "SteamAPI_Init");
BIND_ONE(SteamShutdown, "SteamAPI_Shutdown");
BIND_ONE(SteamGetPipe, "SteamAPI_GetHSteamPipe");
BIND_ONE(DispatchInit, "SteamAPI_ManualDispatch_Init");
BIND_ONE(DispatchRunFrame, "SteamAPI_ManualDispatch_RunFrame");
BIND_ONE(DispatchGetNext, "SteamAPI_ManualDispatch_GetNextCallback");
BIND_ONE(DispatchFreeLast, "SteamAPI_ManualDispatch_FreeLastCallback");
BIND_ONE(MsgSendToUser, "SteamAPI_ISteamNetworkingMessages_SendMessageToUser");
BIND_ONE(MsgReceiveOnChannel, "SteamAPI_ISteamNetworkingMessages_ReceiveMessagesOnChannel");
BIND_ONE(MsgAcceptSession, "SteamAPI_ISteamNetworkingMessages_AcceptSessionWithUser");
BIND_ONE(MsgCloseSession, "SteamAPI_ISteamNetworkingMessages_CloseSessionWithUser");
BIND_ONE(MsgRelease, "SteamAPI_SteamNetworkingMessage_t_Release");
BIND_ONE(UserGetSteamID, "SteamAPI_ISteamUser_GetSteamID");
BIND_ONE(LobbyJoin, "SteamAPI_ISteamMatchmaking_JoinLobby");
BIND_ONE(LobbyNumMembers, "SteamAPI_ISteamMatchmaking_GetNumLobbyMembers");
BIND_ONE(LobbyMemberByIndex, "SteamAPI_ISteamMatchmaking_GetLobbyMemberByIndex");
BIND_ANY(GetMessagesIface, kMessagesNames, "ISteamNetworkingMessages");
BIND_ANY(GetUserIface, kUserNames, "ISteamUser");
BIND_ANY(GetMatchmakingIface, kMatchmakingNames, "ISteamMatchmaking");
BIND_ANY(GetUtilsIface, kUtilsNames, "ISteamNetworkingUtils");
BIND_ONE(UtilsSetDebugOutput, "SteamAPI_ISteamNetworkingUtils_SetDebugOutputFunction");
#ifdef __MINGW32__
#pragma GCC diagnostic pop
#endif
/* Matchmaking is deliberately NOT in this list: no lobby is a degraded
* mode (peers= still works), a missing transport export is not. */
struct { const void* p; const char* what; } required[] = {
{ (const void*)SteamShutdown, "SteamAPI_Shutdown" },
{ (const void*)SteamGetPipe, "SteamAPI_GetHSteamPipe" },
{ (const void*)DispatchInit, "SteamAPI_ManualDispatch_Init" },
{ (const void*)DispatchRunFrame, "SteamAPI_ManualDispatch_RunFrame" },
{ (const void*)DispatchGetNext, "SteamAPI_ManualDispatch_GetNextCallback" },
{ (const void*)DispatchFreeLast, "SteamAPI_ManualDispatch_FreeLastCallback" },
{ (const void*)GetMessagesIface, "SteamAPI_SteamNetworkingMessages_SteamAPI_v002" },
{ (const void*)MsgSendToUser, "SendMessageToUser" },
{ (const void*)MsgReceiveOnChannel, "ReceiveMessagesOnChannel" },
{ (const void*)MsgAcceptSession, "AcceptSessionWithUser" },
{ (const void*)MsgCloseSession, "CloseSessionWithUser" },
{ (const void*)MsgRelease, "SteamNetworkingMessage_t_Release" },
{ (const void*)GetUserIface, "ISteamUser accessor" },
{ (const void*)UserGetSteamID, "ISteamUser_GetSteamID" }
};
for (size_t i = 0; i < sizeof(required)/sizeof(required[0]); i++) {
if (!required[i].p) {
g_bindErr = std::string("steam_api64.dll is missing ") + required[i].what +
" -- SDK too old for the flat NetworkingMessages API (need 1.46+)";
err = g_bindErr;
return false;
}
}
if (!SteamInitFlat && !SteamInitOld) {
g_bindErr = "steam_api64.dll exports neither SteamAPI_InitFlat nor SteamAPI_Init";
err = g_bindErr;
return false;
}
g_bound = true;
return true;
}
/* ------------------------------------------------------------------ *
* Link state. Single-threaded by contract (see steamlink.h), so no locks
* and no atomics anywhere below.
* ------------------------------------------------------------------ */
struct Peer {
uint64_t steamId;
uint8_t mac[6];
bool macKnown;
uint32_t ip; /* host order */
char name[16];
bool sessionUp;
bool isGateway; /* the session host: default route for unknown unicast */
uint64_t framesTx, framesRx;
uint32_t helloSentMs; /* 0 = never sent */
uint32_t helloHeardMs; /* 0 = never answered -- keeps it on the fast ladder */
bool fromLobby; /* lobby-learned: a lobby leave may retire it */
};
enum {
HELLO_LEN = 28, /* 'H' ver mac[6] ip[4] name[16] */
HELLO_FAST_MS = 2000, /* until the peer has answered */
HELLO_SLOW_MS = 30000, /* after */
HELLO_REPLY_MIN_MS = 1000, /* floor on answering a peer's HELLO */
LOBBY_POLL_MS = 2000,
RX_BATCH = 16
};
static bool g_up = false;
static int g_refs = 0;
static int32_t g_pipe = 0;
static void* g_iMessages = 0;
static void* g_iUser = 0;
static void* g_iMatchmaking = 0;
static uint64_t g_selfId = 0;
static uint8_t g_selfMac[6] = { 0, 0, 0, 0, 0, 0 };
static uint32_t g_selfIp = 0;
static char g_selfName[16] = { 0 };
static int g_channel = 0;
static uint64_t g_lobby = 0;
static uint32_t g_lobbyPolledMs = 0;
static uint32_t g_epoch = 0;
static uint64_t g_tx = 0, g_rx = 0, g_txDropped = 0;
/* std::map, not unordered_map: the peer set is <=7 entries and the gateway
* builds a BPF string out of Peers(), which is easier to eyeball and diff when
* the order is stable. */
static std::map<uint64_t, Peer> g_peers;
/* Refused identities, so a stranger hammering us costs one log line total. */
static std::set<uint64_t> g_refused;
/* 32-bit like the rest of the fork (vpxlog.cpp): every elapsed test below is
* an unsigned 32-bit subtraction, which is exact across the 49.7-day wrap.
* GetTickCount64 is deliberately NOT used -- it disappears when a build pins
* _WIN32_WINNT below 0x0600, which parts of this tree do. */
static inline uint32_t NowMs() { return (uint32_t)GetTickCount(); }
static Peer* Find(uint64_t steamId)
{
std::map<uint64_t, Peer>::iterator it = g_peers.find(steamId);
return (it == g_peers.end()) ? 0 : &it->second;
}
static Peer* FindByMac(const uint8_t mac[6])
{
for (std::map<uint64_t, Peer>::iterator it = g_peers.begin(); it != g_peers.end(); ++it)
if (it->second.macKnown && memcmp(it->second.mac, mac, 6) == 0) return &it->second;
return 0;
}
/* The session host, i.e. the default route for unicast we can't place. Null in
* a gateway-less session (two players and no console), which is why every
* caller falls back to flooding rather than dropping. */
static Peer* FindGateway(void)
{
for (std::map<uint64_t, Peer>::iterator it = g_peers.begin(); it != g_peers.end(); ++it)
if (it->second.isGateway) return &it->second;
return 0;
}
static const char* PeerName(const Peer& p)
{
return p.name[0] ? p.name : "?";
}
/* ------------------------------------------------------------------ *
* Wire
* ------------------------------------------------------------------ */
static bool SendRecord(Peer& p, const uint8_t* rec, int len)
{
SteamNetworkingIdentity id;
sdkabi::IdentitySetSteamID(id, p.steamId);
/* A send to a peer we have no session with is what OPENS the session --
* the peer gets a 1251 and accepts if we are in its allowlist. */
const int r = MsgSendToUser(g_iMessages, &id, rec, (uint32_t)len, SEND_FLAGS, g_channel);
if (r != RESULT_OK) {
static unsigned long long errn = 0;
if (LogNth(++errn))
Log("STEAM send error #%llu EResult=%d to %s (%llu)",
(unsigned long long)errn, r, PeerName(p), (unsigned long long)p.steamId);
return false;
}
return true;
}
static void SendHello(Peer& p, uint32_t now)
{
uint8_t rec[HELLO_LEN];
memset(rec, 0, sizeof(rec));
rec[0] = REC_HELLO;
rec[1] = 1; /* version */
memcpy(rec + 2, g_selfMac, 6);
rec[8] = (uint8_t)(g_selfIp >> 24); /* BIG-endian on the wire */
rec[9] = (uint8_t)(g_selfIp >> 16);
rec[10] = (uint8_t)(g_selfIp >> 8);
rec[11] = (uint8_t)(g_selfIp);
memcpy(rec + 12, g_selfName, 15); /* rec is zeroed => NUL-terminated */
p.helloSentMs = now;
SendRecord(p, rec, (int)sizeof(rec));
}
/* ------------------------------------------------------------------ *
* Peer table
* ------------------------------------------------------------------ */
static void AddPeerInternal(uint64_t steamId, bool fromLobby)
{
if (!g_up || !steamId || steamId == g_selfId) return;
if (g_peers.find(steamId) != g_peers.end()) return;
Peer p;
memset(&p, 0, sizeof(p));
p.steamId = steamId;
p.fromLobby = fromLobby;
g_peers[steamId] = p;
g_refused.erase(steamId); /* allowlisted now; let it be logged again if dropped */
Log("STEAM peer %llu added%s", (unsigned long long)steamId, fromLobby ? " (lobby)" : "");
/* HELLO immediately: it opens the session AND carries our MAC binding. */
SendHello(g_peers[steamId], NowMs());
}
void AddPeer(uint64_t steamId) { AddPeerInternal(steamId, false); }
void RemovePeer(uint64_t steamId)
{
std::map<uint64_t, Peer>::iterator it = g_peers.find(steamId);
if (it == g_peers.end()) return;
const bool hadMac = it->second.macKnown;
if (g_iMessages && MsgCloseSession) {
SteamNetworkingIdentity id;
sdkabi::IdentitySetSteamID(id, steamId);
MsgCloseSession(g_iMessages, &id);
}
Log("STEAM peer %s (%llu) removed", PeerName(it->second), (unsigned long long)steamId);
g_peers.erase(it);
if (hadMac) g_epoch++;
}
/* ------------------------------------------------------------------ *
* Callbacks (manual dispatch)
* ------------------------------------------------------------------ */
/* Lift the leading SteamNetworkingIdentity out of a callback payload.
* memcpy, not a cast: the payload is a uint8_t* into steam_api64.dll's own
* callback buffer, so it carries no alignment promise (same reasoning as
* OnLobbyChatUpdate below). It also must not be handed straight back to the
* SDK -- the flat API takes the identity BY VALUE, which on the Win64 ABI is
* a pointer to a caller-owned temporary the callee is free to write to, and
* that buffer is Steam's, not ours. Copy first, pass the copy. */
static bool IdentityFromCallback(const CallbackMsg_t& cb, SteamNetworkingIdentity& out)
{
if (!cb.m_pubParam || cb.m_cubParam < (int)sizeof(SteamNetworkingIdentity)) return false;
memcpy(&out, cb.m_pubParam, sizeof(out));
return true;
}
static void OnSessionRequest(const CallbackMsg_t& cb)
{
SteamNetworkingIdentity ident;
if (!IdentityFromCallback(cb, ident)) return;
const SteamNetworkingIdentity* id = &ident;
const uint64_t who = sdkabi::IdentityGetSteamID(*id);
Peer* p = Find(who);
if (!p) {
/* App 480 is public, so anyone who learns our SteamID can ring the
* bell; refusing everything outside the session's peer list IS the
* security story for a private group. Nothing else gates inbound. */
if (g_refused.insert(who).second)
Log("STEAM session request from %llu REFUSED (not in peer list)",
(unsigned long long)who);
return;
}
if (!MsgAcceptSession(g_iMessages, id)) {
Log("STEAM AcceptSessionWithUser failed for %llu", (unsigned long long)who);
return;
}
p->sessionUp = true;
Log("STEAM session accepted from %s (%llu)", PeerName(*p), (unsigned long long)who);
/* A fresh inbound session means that peer just (re)started: HELLO now so it
* binds our MAC without waiting out its own ladder. */
SendHello(*p, NowMs());
}
static void OnSessionFailed(const CallbackMsg_t& cb)
{
/* Payload is a SteamNetConnectionInfo_t whose FIRST member is the peer
* identity. We read exactly that and nothing past it -- the tail of that
* struct has grown between SDK releases. */
SteamNetworkingIdentity ident;
if (!IdentityFromCallback(cb, ident)) return;
const SteamNetworkingIdentity* id = &ident;
const uint64_t who = sdkabi::IdentityGetSteamID(*id);
Peer* p = Find(who);
if (!p) return;
/* Close it explicitly: a failed session stays failed, and every later
* SendMessageToUser fails instantly until the session is torn down. This
* call is what makes the re-announce below an actual retry. */
if (MsgCloseSession) MsgCloseSession(g_iMessages, id);
p->sessionUp = false;
p->helloHeardMs = 0; /* back onto the 2s ladder... */
p->helloSentMs = 0; /* ...starting with the next Pump */
Log("STEAM session FAILED with %s (%llu) -- re-announcing",
PeerName(*p), (unsigned long long)who);
}
static void OnLobbyChatUpdate(const CallbackMsg_t& cb)
{
/* LobbyChatUpdate_t: u64 lobby, u64 userChanged, u64 userMakingChange,
* u32 stateChange. memcpy because the callback buffer is only byte-aligned. */
if (!g_lobby || !cb.m_pubParam || cb.m_cubParam < 28) return;
uint64_t lobby = 0, user = 0;
uint32_t state = 0;
memcpy(&lobby, cb.m_pubParam + 0, 8);
memcpy(&user, cb.m_pubParam + 8, 8);
memcpy(&state, cb.m_pubParam + 24, 4);
if (lobby != g_lobby || !user || user == g_selfId) return;
if (state & CHAT_MEMBER_ENTERED) {
AddPeerInternal(user, true);
} else {
/* Only lobby-learned peers leave with the lobby: the session file's
* peer list is the allowlist and stays put for the whole session. */
Peer* p = Find(user);
if (p && p->fromLobby) RemovePeer(user);
}
}
static void ServiceHellos(uint32_t now)
{
for (std::map<uint64_t, Peer>::iterator it = g_peers.begin(); it != g_peers.end(); ++it) {
Peer& p = it->second;
const uint32_t due = p.helloHeardMs ? (uint32_t)HELLO_SLOW_MS : (uint32_t)HELLO_FAST_MS;
if (!p.helloSentMs || (uint32_t)(now - p.helloSentMs) >= due) SendHello(p, now);
}
}
static void ServiceLobby(uint32_t now)
{
if (!g_lobby || !g_iMatchmaking || !LobbyNumMembers || !LobbyMemberByIndex) return;
if (g_lobbyPolledMs && (uint32_t)(now - g_lobbyPolledMs) < (uint32_t)LOBBY_POLL_MS) return;
g_lobbyPolledMs = now;
/* JoinLobby is asynchronous and we intentionally await no call result (see
* Pump), so the initial roster arrives by polling; 506 handles the deltas
* after that. Polling a 2-8 entry list every 2s costs nothing. */
const int n = LobbyNumMembers(g_iMatchmaking, g_lobby);
for (int i = 0; i < n; i++) {
const uint64_t id = LobbyMemberByIndex(g_iMatchmaking, g_lobby, i);
if (id && id != g_selfId && !Find(id)) AddPeerInternal(id, true);
}
}
/* ------------------------------------------------------------------ *
* API
* ------------------------------------------------------------------ */
bool Init(const Config& cfg, const uint8_t self_mac[6], uint32_t self_ip, std::string& err)
{
err.clear();
if (g_up) {
/* Refcounted per the header. A second Init only widens the peer set --
* the MAC/IP/name of the first caller win, because they are the guest's
* and there is only one guest per process. */
g_refs++;
for (size_t i = 0; i < cfg.peers.size(); i++) AddPeerInternal(cfg.peers[i], false);
return true;
}
if (!self_mac) { err = "steam: self MAC is required"; return false; }
if (!BindSteamApi(err)) return false;
if (SteamInitFlat) {
char msg[1024];
msg[0] = 0;
const int r = SteamInitFlat(msg);
if (r != 0) {
err = std::string("SteamAPI_Init failed (") + std::to_string(r) + "): " +
(msg[0] ? msg : "is Steam running and logged in? SteamAppId must be set (480)");
return false;
}
} else if (!SteamInitOld()) {
err = "SteamAPI_Init failed -- is Steam running and logged in? "
"SteamAppId must be set (480)";
return false;
}
DispatchInit(); /* we own the pump; see Pump() */
g_pipe = SteamGetPipe();
if (!g_pipe) { err = "SteamAPI_GetHSteamPipe returned 0"; SteamShutdown(); return false; }
g_iMessages = GetMessagesIface();
if (!g_iMessages) { err = "ISteamNetworkingMessages unavailable"; SteamShutdown(); return false; }
g_iUser = GetUserIface();
g_selfId = g_iUser ? UserGetSteamID(g_iUser) : 0;
if (!g_selfId) {
err = "Steam user is not logged in (no SteamID)";
SteamShutdown();
return false;
}
g_iMatchmaking = GetMatchmakingIface ? GetMatchmakingIface() : 0;
memcpy(g_selfMac, self_mac, 6);
g_selfIp = self_ip;
g_channel = cfg.channel;
g_lobby = cfg.lobby;
memset(g_selfName, 0, sizeof(g_selfName));
if (!cfg.name.empty()) memcpy(g_selfName, cfg.name.c_str(),
cfg.name.size() < 15 ? cfg.name.size() : 15);
g_lobbyPolledMs = 0;
g_tx = g_rx = g_txDropped = 0;
g_peers.clear();
g_refused.clear();
g_up = true;
g_refs = 1;
Log("STEAM link up: self %llu %02x:%02x:%02x:%02x:%02x:%02x %u.%u.%u.%u \"%s\" channel %d",
(unsigned long long)g_selfId,
g_selfMac[0], g_selfMac[1], g_selfMac[2], g_selfMac[3], g_selfMac[4], g_selfMac[5],
(g_selfIp >> 24) & 0xff, (g_selfIp >> 16) & 0xff, (g_selfIp >> 8) & 0xff, g_selfIp & 0xff,
g_selfName, g_channel);
/* The pod MAC is derived from the slot IP by configure.ps1's formula; a
* mismatch means a hand-edited conf and shows up later as a peer nobody can
* route to, so say it once at Init instead. */
if (g_selfIp &&
(g_selfMac[0] != 0x02 || g_selfMac[1] != 0x00 ||
g_selfMac[2] != (uint8_t)(g_selfIp >> 24) || g_selfMac[3] != (uint8_t)(g_selfIp >> 16) ||
g_selfMac[4] != (uint8_t)(g_selfIp >> 8) || g_selfMac[5] != (uint8_t)(g_selfIp)))
Log("STEAM WARNING: macaddr is not 02:00:+IP octets -- check [ne2000] macaddr against the slot IP");
for (size_t i = 0; i < cfg.peers.size(); i++) AddPeerInternal(cfg.peers[i], false);
/* The gateway is a peer like any other -- it just also serves as the
* default route. Seed it even if it was left out of cfg.peers, otherwise a
* session file listing it only as the gateway would have nowhere to send
* console traffic. */
if (cfg.gateway) {
AddPeerInternal(cfg.gateway, false);
Peer* gw = Find(cfg.gateway);
if (gw) {
gw->isGateway = true;
Log("STEAM gateway (session host) = %llu -- default route for non-peer MACs",
(unsigned long long)cfg.gateway);
}
} else {
Log("STEAM no gateway configured -- unicast to an unknown MAC will flood every peer");
}
if (g_lobby) {
if (g_iMatchmaking && LobbyJoin) {
LobbyJoin(g_iMatchmaking, g_lobby);
Log("STEAM joining lobby %llu for the roster", (unsigned long long)g_lobby);
} else {
Log("STEAM lobby %llu ignored -- no ISteamMatchmaking in this steam_api64.dll",
(unsigned long long)g_lobby);
}
}
if (g_peers.empty() && !g_lobby)
Log("STEAM WARNING: no peers configured -- nothing will be sent");
return true;
}
void Shutdown()
{
if (!g_up) return;
if (--g_refs > 0) return;
bool hadMacs = false;
for (std::map<uint64_t, Peer>::iterator it = g_peers.begin(); it != g_peers.end(); ++it) {
hadMacs = hadMacs || it->second.macKnown;
if (MsgCloseSession) {
SteamNetworkingIdentity id;
sdkabi::IdentitySetSteamID(id, it->first);
MsgCloseSession(g_iMessages, &id);
}
}
g_peers.clear();
g_refused.clear();
if (hadMacs) g_epoch++;
Log("STEAM link down (tx %llu rx %llu dropped %llu)",
(unsigned long long)g_tx, (unsigned long long)g_rx, (unsigned long long)g_txDropped);
g_up = false;
g_iMessages = g_iUser = g_iMatchmaking = 0;
g_selfId = 0;
SteamShutdown();
/* The DLL handle stays loaded on purpose: Steam does not survive a
* LoadLibrary/FreeLibrary cycle, and the pcap backend caches its handle the
* same way. */
}
void Pump()
{
if (!g_up) return;
/* The documented manual-dispatch loop. Manual dispatch (rather than
* SteamAPI_RunCallbacks) is what lets the fork service Steam from the
* emulator thread inside NE2000_Poller -- no callback registration, no
* second thread, no timer. */
DispatchRunFrame(g_pipe);
CallbackMsg_t cb;
while (DispatchGetNext(g_pipe, &cb)) {
switch (cb.m_iCallback) {
case CB_SESSION_REQUEST: OnSessionRequest(cb); break;
case CB_SESSION_FAILED: OnSessionFailed(cb); break;
case CB_LOBBY_CHAT_UPDATE: OnLobbyChatUpdate(cb); break;
default: break; /* incl. SteamAPICallCompleted_t: nothing here awaits
* a call result, so there is no GetAPICallResult arm */
}
DispatchFreeLast(g_pipe); /* every GetNextCallback that returned true */
}
const uint32_t now = NowMs();
ServiceHellos(now);
ServiceLobby(now);
}
void SendFrame(const uint8_t* frame, int len)
{
if (!g_up) return;
if (!frame || len < 14 || len > (int)VWE_MAX_FRAME) {
g_txDropped++;
if (LogNth(g_txDropped))
Log("STEAM TX DROP #%llu len=%d (not 14..%d)",
(unsigned long long)g_txDropped, len, (int)VWE_MAX_FRAME);
return;
}
/* One record = ['F'][frame]. Single-threaded by contract, so one static
* staging buffer beats a per-frame allocation on a 1 kHz path. */
static uint8_t rec[1 + VWE_MAX_FRAME];
rec[0] = REC_FRAME;
memcpy(rec + 1, frame, (size_t)len);
const int reclen = len + 1;
int tried = 0, sent = 0;
Peer* dest = 0;
if ((frame[0] & 1) == 0) { /* unicast */
dest = FindByMac(frame);
/* Not a peer MAC: this is almost certainly console-bound (200.0.0.10
* lives on the host LAN behind the gateway, so its NIC MAC can never
* appear in the peer table). Route it to the gateway rather than
* flooding -- flooding would put the whole console<->pod stream, the
* egg transfer included, on every other player's uplink. */
if (!dest) dest = FindGateway();
}
if (dest) {
tried = 1;
if (SendRecord(*dest, rec, reclen)) { sent = 1; dest->framesTx++; }
} else {
/* Broadcast/multicast (ARP must reach everyone), or unicast with no
* gateway configured -- a gateway-less session, or the narrow race
* before a peer's HELLO has bound its MAC. */
for (std::map<uint64_t, Peer>::iterator it = g_peers.begin(); it != g_peers.end(); ++it) {
tried++;
if (SendRecord(it->second, rec, reclen)) { sent++; it->second.framesTx++; }
}
}
if (sent) {
g_tx++;
if (LogNth(g_tx))
Log("STEAM TX #%llu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x peers=%d",
(unsigned long long)g_tx, len, frame[0], frame[1], frame[2],
frame[6], frame[7], frame[8], sent);
} else {
g_txDropped++;
if (LogNth(g_txDropped))
Log("STEAM TX DROP #%llu len=%d dst=%02x:%02x:%02x (%s)",
(unsigned long long)g_txDropped, len, frame[0], frame[1], frame[2],
tried ? "no peer took it" : "no peers");
}
}
/* Returns 1 if a frame reached the callback. ALWAYS releases the message --
* every early-out below leaks a Steam allocation otherwise, and this runs a
* thousand times a second for a whole evening. */
static int HandleMessage(SteamNetworkingMessage_t* m,
const std::function<void(const uint8_t*, int)>& cb)
{
if (!m) return 0;
int delivered = 0;
const uint8_t* d = (const uint8_t*)m->m_pData;
const int len = m->m_cbSize;
const uint64_t who = sdkabi::IdentityGetSteamID(m->m_identityPeer);
Peer* p = Find(who);
if (!p) {
/* Only allowlisted identities get a session, so this is a peer we
* dropped mid-flight -- or an SDK identity type we do not speak. */
static unsigned long long strayn = 0;
if (LogNth(++strayn))
Log("STEAM RX from unknown identity %llu discarded (#%llu)",
(unsigned long long)who, (unsigned long long)strayn);
} else if (!d || len < 1) {
/* Throttled like every other junk path: these are driven by the PEER's
* send rate, and a version-skewed pod re-HELLOs every 2s for the whole
* mission. Powers of 2 still log the first one loudly. */
static unsigned long long emptyn = 0;
if (LogNth(++emptyn))
Log("STEAM RX empty record from %s (#%llu)",
PeerName(*p), (unsigned long long)emptyn);
} else if (d[0] == REC_HELLO) {
if (len < HELLO_LEN || d[1] != 1) {
static unsigned long long badhn = 0;
if (LogNth(++badhn))
Log("STEAM RX bad HELLO from %llu (len=%d ver=%d) (#%llu)",
(unsigned long long)who, len, len >= 2 ? d[1] : -1,
(unsigned long long)badhn);
} else {
const uint32_t now = NowMs();
p->sessionUp = true;
p->helloHeardMs = now;
p->ip = ((uint32_t)d[8] << 24) | ((uint32_t)d[9] << 16) |
((uint32_t)d[10] << 8) | (uint32_t)d[11];
memcpy(p->name, d + 12, 16);
p->name[15] = 0;
if (!p->macKnown || memcmp(p->mac, d + 2, 6) != 0) {
memcpy(p->mac, d + 2, 6);
p->macKnown = true;
g_epoch++; /* the gateway recompiles its BPF off this */
Log("STEAM peer %s (%llu) is %02x:%02x:%02x:%02x:%02x:%02x %u.%u.%u.%u",
PeerName(*p), (unsigned long long)who,
p->mac[0], p->mac[1], p->mac[2], p->mac[3], p->mac[4], p->mac[5],
(p->ip >> 24) & 0xff, (p->ip >> 16) & 0xff, (p->ip >> 8) & 0xff, p->ip & 0xff);
}
/* Answer a HELLO we were not going to send for another 30s: a peer
* that just restarted has an empty table and needs our binding now.
* The 1s floor is what keeps two peers from ping-ponging forever. */
if ((uint32_t)(now - p->helloSentMs) >= (uint32_t)HELLO_REPLY_MIN_MS)
SendHello(*p, now);
}
} else if (d[0] == REC_FRAME) {
const int flen = len - 1;
if (flen < 14) {
static unsigned long long runtn = 0;
if (LogNth(++runtn))
Log("STEAM RX runt frame from %s len=%d (#%llu)",
PeerName(*p), flen, (unsigned long long)runtn);
} else {
p->sessionUp = true;
if (!p->macKnown) {
/* Fallback binding for a HELLO that raced past us. HELLO stays
* authoritative -- it can correct this, this never overwrites it
* (a relayed frame's src MAC is not necessarily the peer's). */
memcpy(p->mac, d + 1 + 6, 6);
p->macKnown = true;
g_epoch++;
Log("STEAM peer %llu src-MAC learned %02x:%02x:%02x:%02x:%02x:%02x (no HELLO yet)",
(unsigned long long)who,
p->mac[0], p->mac[1], p->mac[2], p->mac[3], p->mac[4], p->mac[5]);
}
p->framesRx++;
g_rx++;
if (LogNth(g_rx))
Log("STEAM RX #%llu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x from=%s",
(unsigned long long)g_rx, flen, d[1], d[2], d[3], d[7], d[8], d[9],
PeerName(*p));
cb(d + 1, flen);
delivered = 1;
}
} else {
static unsigned long long unkn = 0;
if (LogNth(++unkn))
Log("STEAM RX unknown record type 0x%02x from %s (#%llu)",
d[0], PeerName(*p), (unsigned long long)unkn);
}
MsgRelease(m);
return delivered;
}
int PollFrames(const std::function<void(const uint8_t*, int)>& cb)
{
if (!g_up) return 0;
int delivered = 0;
SteamNetworkingMessage_t* msgs[RX_BATCH];
for (;;) {
const int n = MsgReceiveOnChannel(g_iMessages, g_channel, msgs, RX_BATCH);
if (n <= 0) break;
for (int i = 0; i < n; i++) delivered += HandleMessage(msgs[i], cb);
if (n < RX_BATCH) break; /* short batch == queue drained */
}
return delivered;
}
uint64_t SelfSteamID() { return g_selfId; }
/* Steam calls this from ITS OWN service thread, not ours. Log() must therefore
* stay a plain sink write -- do not touch the peer table or any other state
* from in here. */
static void __cdecl SteamDebugSink(int level, const char* msg)
{
Log("STEAM/net[%d] %s", level, msg ? msg : "");
}
void SetRouteDiagnostics(int level)
{
if (!g_up) { Log("STEAM route diagnostics ignored -- link is not up"); return; }
void* utils = GetUtilsIface ? GetUtilsIface() : 0;
if (!utils || !UtilsSetDebugOutput) {
Log("STEAM route diagnostics unavailable -- this steam_api64.dll has no "
"ISteamNetworkingUtils::SetDebugOutputFunction");
return;
}
UtilsSetDebugOutput(utils, level, level > 0 ? SteamDebugSink : 0);
Log("STEAM route diagnostics ON at level %d -- watch for the relay/direct path", level);
}
uint32_t PeerEpoch() { return g_epoch; }
size_t Peers(std::vector<PeerInfo>& out)
{
out.clear();
out.reserve(g_peers.size());
for (std::map<uint64_t, Peer>::const_iterator it = g_peers.begin(); it != g_peers.end(); ++it) {
const Peer& p = it->second;
PeerInfo pi;
memset(&pi, 0, sizeof(pi));
pi.steamId = p.steamId;
memcpy(pi.mac, p.mac, 6);
pi.macKnown = p.macKnown;
pi.ip = p.ip;
memcpy(pi.name, p.name, sizeof(pi.name));
pi.name[sizeof(pi.name) - 1] = 0;
pi.sessionUp = p.sessionUp;
pi.isGateway = p.isGateway;
pi.framesTx = p.framesTx;
pi.framesRx = p.framesRx;
out.push_back(pi);
}
return out.size();
}
void Counters(uint64_t& tx, uint64_t& rx, uint64_t& txDropped)
{
tx = g_tx;
rx = g_rx;
txDropped = g_txDropped;
}
} // namespace vwesteam
#else /* !WIN32 -- the whole VWE deployment is Windows; this keeps the file
* harmless in a cross-platform build instead of breaking it. */
namespace vwesteam {
bool Init(const Config&, const uint8_t*, uint32_t, std::string& err)
{
err = "steam transport is Windows-only in this build";
Log("%s", err.c_str());
return false;
}
void Shutdown() {}
void Pump() {}
void SendFrame(const uint8_t*, int) {}
int PollFrames(const std::function<void(const uint8_t*, int)>&) { return 0; }
uint64_t SelfSteamID() { return 0; }
void SetRouteDiagnostics(int) {}
uint32_t PeerEpoch() { return 0; }
size_t Peers(std::vector<PeerInfo>& out) { out.clear(); return 0; }
void AddPeer(uint64_t) {}
void RemovePeer(uint64_t) {}
void Counters(uint64_t& tx, uint64_t& rx, uint64_t& txDropped) { tx = rx = txDropped = 0; }
} // namespace vwesteam
#endif /* WIN32 */