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>
478 lines
17 KiB
C++
478 lines
17 KiB
C++
/*
|
|
* VWE fork: `backend=steam` -- EthernetConnection over the vwesteam:: tunnel.
|
|
* See ethernet_steam.h for why this exists, steamlink.h for the wire format,
|
|
* and ../steam/SESSION-CONTRACT.md for the session file and env handoff.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#ifdef WIN32
|
|
|
|
#include "ethernet_steam.h"
|
|
#include "steamlink.h"
|
|
#include "dosbox.h"
|
|
#include "logging.h"
|
|
#include "support.h" /* strcasecmp */
|
|
#include "control.h" /* [ne2000] macaddr -- required here, not optional */
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
/* vwesteam:: logs through a plain function pointer so the gateway can send the
|
|
* same diagnostics to stderr; in the fork they belong wherever every other
|
|
* device logs. Set before Init() so Init()'s own failures are visible. */
|
|
static void SteamLog(const char* msg)
|
|
{
|
|
LOG_MSG("%s", msg);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* Session file (SESSION-CONTRACT section 2)
|
|
*
|
|
* A tolerant hand-rolled reader, deliberately NOT a JSON library: the fork
|
|
* has no JSON dependency and this file is machine-written by TeslaLobby in
|
|
* one fixed shape, so scanning for the six keys we need is cheaper than
|
|
* taking on one. What it will not do is half-read a roster -- a peer that
|
|
* silently went missing looks exactly like a peer that never connected, and
|
|
* that is the worst thing to be debugging at 9pm. Anything malformed is a
|
|
* hard failure naming the file and the key.
|
|
* ------------------------------------------------------------------ */
|
|
|
|
struct SessionPeer {
|
|
uint64_t steamId = 0;
|
|
uint32_t ip = 0; /* host order; 0 = absent */
|
|
uint8_t mac[6] = {};
|
|
bool macKnown = false;
|
|
int slot = -1;
|
|
bool gateway = false;
|
|
std::string name;
|
|
};
|
|
|
|
static const char* SkipWs(const char* p, const char* end)
|
|
{
|
|
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) p++;
|
|
return p;
|
|
}
|
|
|
|
/* Matching '}' / ']' for the bracket at p. Quote-aware, so a bracket inside a
|
|
* pilot name cannot unbalance the walk. */
|
|
static const char* MatchBracket(const char* p, const char* end)
|
|
{
|
|
const char open = *p, close = (open == '{') ? '}' : ']';
|
|
int depth = 0;
|
|
bool instr = false;
|
|
for (; p < end; p++) {
|
|
if (instr) {
|
|
if (*p == '\\') p++;
|
|
else if (*p == '"') instr = false;
|
|
continue;
|
|
}
|
|
if (*p == '"') instr = true;
|
|
else if (*p == open) depth++;
|
|
else if (*p == close && --depth == 0) return p;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Scalar value of "key" in [p,end), quotes and escapes stripped. Callers bound
|
|
* the span to one object, so a flat search cannot wander into a sibling. */
|
|
static bool JsonGet(const char* p, const char* end, const char* key, std::string& out)
|
|
{
|
|
char pat[32];
|
|
snprintf(pat, sizeof(pat), "\"%s\"", key);
|
|
const size_t klen = strlen(pat);
|
|
|
|
for (const char* q = p; q < end; q++) {
|
|
q = (const char*)memchr(q, '"', (size_t)(end - q));
|
|
if (!q) break;
|
|
if ((size_t)(end - q) < klen || memcmp(q, pat, klen) != 0) continue;
|
|
const char* v = SkipWs(q + klen, end);
|
|
if (v >= end || *v != ':') continue;
|
|
v = SkipWs(v + 1, end);
|
|
out.clear();
|
|
if (v < end && *v == '"') {
|
|
for (v++; v < end && *v != '"'; v++) {
|
|
if (*v == '\\' && v + 1 < end) v++;
|
|
out += *v;
|
|
}
|
|
} else {
|
|
while (v < end && *v != ',' && *v != '}' && *v != ']' &&
|
|
*v != ' ' && *v != '\t' && *v != '\r' && *v != '\n')
|
|
out += *v++;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Contents of the {object} or [array] value of "key", brackets excluded. */
|
|
static bool JsonSpan(const char* p, const char* end, const char* key,
|
|
const char*& bo, const char*& eo)
|
|
{
|
|
char pat[32];
|
|
snprintf(pat, sizeof(pat), "\"%s\"", key);
|
|
const size_t klen = strlen(pat);
|
|
|
|
for (const char* q = p; q < end; q++) {
|
|
q = (const char*)memchr(q, '"', (size_t)(end - q));
|
|
if (!q) break;
|
|
if ((size_t)(end - q) < klen || memcmp(q, pat, klen) != 0) continue;
|
|
const char* v = SkipWs(q + klen, end);
|
|
if (v >= end || *v != ':') continue;
|
|
v = SkipWs(v + 1, end);
|
|
if (v >= end || (*v != '{' && *v != '[')) continue;
|
|
const char* close = MatchBracket(v, end);
|
|
if (!close) return false;
|
|
bo = v + 1;
|
|
eo = close;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Next {object} inside an array span, advancing p past it. */
|
|
static bool JsonNextObject(const char*& p, const char* end,
|
|
const char*& bo, const char*& eo)
|
|
{
|
|
p = SkipWs(p, end);
|
|
while (p < end && *p == ',') p = SkipWs(p + 1, end);
|
|
if (p >= end || *p != '{') return false;
|
|
const char* close = MatchBracket(p, end);
|
|
if (!close) return false;
|
|
bo = p + 1;
|
|
eo = close;
|
|
p = close + 1;
|
|
return true;
|
|
}
|
|
|
|
/* The WIDTH-LIMITED %02x is not cosmetic: it is exactly what ne2000.cpp's own
|
|
* macaddr parser uses, and the emulated NIC's answer has to be the same six
|
|
* bytes this backend filters on and announces in HELLO. Plain %x diverges
|
|
* silently on a malformed conf -- measured, not theorised:
|
|
*
|
|
* macaddr ne2000.cpp (%02x) plain %x
|
|
* 02:00:C8:00:00:7100 02:00:c8:00:00:71 02:00:c8:00:00:00 (truncated)
|
|
* 02:00:3E7:00:00:70 ac:de:48:88:bb:aa 02:00:e7:00:00:70 (its fallback)
|
|
*
|
|
* Either row is a pod whose NIC answers to one address while the tunnel routes
|
|
* to another: no unicast frame ever arrives and nothing says why. %n + the tail
|
|
* check turns the leftover-digits row into a loud refusal instead of a third
|
|
* wrong answer. ("02:00:3E7:..." is not hypothetical -- see configure.ps1's own
|
|
* warning comment about emitting exactly that.) */
|
|
static bool ParseMac(const char* s, uint8_t mac[6])
|
|
{
|
|
unsigned int m[6];
|
|
int n = -1;
|
|
if (!s || sscanf(s, "%02x:%02x:%02x:%02x:%02x:%02x%n",
|
|
&m[0],&m[1],&m[2],&m[3],&m[4],&m[5], &n) != 6 || n < 0)
|
|
return false;
|
|
while (s[n] == ' ' || s[n] == '\t' || s[n] == '\r' || s[n] == '\n') n++;
|
|
if (s[n] != '\0') return false; /* trailing junk = a MAC nobody meant */
|
|
for (int i = 0; i < 6; i++) mac[i] = (uint8_t)m[i];
|
|
return true;
|
|
}
|
|
|
|
static bool ParseIp(const char* s, uint32_t& ip)
|
|
{
|
|
unsigned int a,b,c,d;
|
|
if (!s || sscanf(s, "%u.%u.%u.%u", &a,&b,&c,&d) != 4 || (a|b|c|d) > 255)
|
|
return false;
|
|
ip = (a << 24) | (b << 16) | (c << 8) | d;
|
|
return true;
|
|
}
|
|
|
|
static bool ParseId(const std::string& s, uint64_t& id)
|
|
{
|
|
char* e = NULL;
|
|
id = strtoull(s.c_str(), &e, 10);
|
|
return id != 0 && e && *e == '\0';
|
|
}
|
|
|
|
/* One peer/self object. `where` names it in errors ("peers[1]", "self"). */
|
|
static bool ParsePeerObject(const char* bo, const char* eo, SessionPeer& out,
|
|
const std::string& path, const char* where,
|
|
std::string& err)
|
|
{
|
|
std::string v;
|
|
if (!JsonGet(bo, eo, "steamId", v) || !ParseId(v, out.steamId)) {
|
|
err = path + ": " + where + " has no usable steamId";
|
|
return false;
|
|
}
|
|
if (JsonGet(bo, eo, "ip", v) && !ParseIp(v.c_str(), out.ip)) {
|
|
err = path + ": " + where + " has a bad ip \"" + v + "\"";
|
|
return false;
|
|
}
|
|
if (JsonGet(bo, eo, "mac", v)) {
|
|
if (!ParseMac(v.c_str(), out.mac)) {
|
|
err = path + ": " + where + " has a bad mac \"" + v + "\"";
|
|
return false;
|
|
}
|
|
out.macKnown = true;
|
|
}
|
|
if (JsonGet(bo, eo, "slot", v)) out.slot = atoi(v.c_str());
|
|
if (JsonGet(bo, eo, "gateway", v)) out.gateway = (strcasecmp(v.c_str(), "true") == 0);
|
|
JsonGet(bo, eo, "name", out.name);
|
|
return true;
|
|
}
|
|
|
|
static bool ParseSessionFile(const std::string& path, SessionPeer& self,
|
|
std::vector<SessionPeer>& peers, std::string& err)
|
|
{
|
|
FILE* f = fopen(path.c_str(), "rb");
|
|
if (!f) {
|
|
err = path + ": cannot open the session file";
|
|
return false;
|
|
}
|
|
std::string doc;
|
|
char buf[4096];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) doc.append(buf, n);
|
|
fclose(f);
|
|
if (doc.size() >= 3 && (unsigned char)doc[0] == 0xef) doc.erase(0, 3); /* stray BOM */
|
|
|
|
const char* p = doc.c_str();
|
|
const char* end = p + doc.size();
|
|
const char *bo, *eo;
|
|
|
|
/* self is optional: the MAC that matters comes from [ne2000] anyway, and
|
|
* the IP is only diagnostics. peers is not -- without it there is no
|
|
* session and no allowlist. */
|
|
if (JsonSpan(p, end, "self", bo, eo) &&
|
|
!ParsePeerObject(bo, eo, self, path, "self", err))
|
|
return false;
|
|
|
|
if (!JsonSpan(p, end, "peers", bo, eo)) {
|
|
err = path + ": no \"peers\" array";
|
|
return false;
|
|
}
|
|
for (const char* q = bo; ; ) {
|
|
const char *pb, *pe;
|
|
if (!JsonNextObject(q, eo, pb, pe)) break;
|
|
SessionPeer peer;
|
|
char where[24];
|
|
snprintf(where, sizeof(where), "peers[%u]", (unsigned)peers.size());
|
|
if (!ParsePeerObject(pb, pe, peer, path, where, err)) return false;
|
|
peers.push_back(peer);
|
|
}
|
|
if (peers.empty()) {
|
|
err = path + ": \"peers\" is empty -- nobody to fly with";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* A conf key left at the literal "env" (the default) defers to `var`. The
|
|
* confs are install-static -- configure.ps1 renders them once -- while the
|
|
* roster changes every session, so everything per-session arrives by env or
|
|
* session file. SESSION-CONTRACT section 2. */
|
|
static std::string ConfOrEnv(Section_prop* section, const char* key, const char* var)
|
|
{
|
|
const char* v = section->Get_string(key);
|
|
if (v && *v && strcasecmp(v, "env") != 0) return std::string(v);
|
|
const char* e = getenv(var);
|
|
return e ? std::string(e) : std::string();
|
|
}
|
|
|
|
static void ParseIdCsv(const std::string& csv, std::vector<uint64_t>& out)
|
|
{
|
|
size_t i = 0;
|
|
while (i < csv.size()) {
|
|
size_t j = csv.find_first_of(",; \t", i);
|
|
if (j == std::string::npos) j = csv.size();
|
|
const std::string tok = csv.substr(i, j - i);
|
|
i = j + 1;
|
|
if (tok.empty()) continue;
|
|
uint64_t id;
|
|
if (!ParseId(tok, id)) {
|
|
LOG_MSG("STEAM: ignoring unparseable peer id \"%s\"", tok.c_str());
|
|
continue;
|
|
}
|
|
if (std::find(out.begin(), out.end(), id) == out.end()) out.push_back(id);
|
|
}
|
|
}
|
|
|
|
SteamEthernetConnection::SteamEthernetConnection()
|
|
: EthernetConnection()
|
|
{
|
|
}
|
|
|
|
SteamEthernetConnection::~SteamEthernetConnection()
|
|
{
|
|
/* Only if Init() took: Shutdown() is refcounted and ethernet.cpp deletes
|
|
* us when Initialize() fails. */
|
|
if (up) vwesteam::Shutdown();
|
|
}
|
|
|
|
bool SteamEthernetConnection::Initialize(Section* config)
|
|
{
|
|
Section_prop *section = static_cast<Section_prop*>(config);
|
|
|
|
const std::string sessionpath = ConfOrEnv(section, "session", "VWE_STEAM_SESSION");
|
|
const std::string peerscsv = ConfOrEnv(section, "peers", "VWE_STEAM_PEERS");
|
|
const std::string lobbystr = ConfOrEnv(section, "lobby", "VWE_STEAM_LOBBY");
|
|
|
|
// Where pcap only loses its RX filter without a macaddr, the mesh ROUTES
|
|
// to this one: peers bind it to our SteamID from the HELLO record and
|
|
// address unicast frames by it. "random" would hand every peer a new
|
|
// address on every restart -- so refuse to come up rather than half-work.
|
|
const char* macstr = NULL;
|
|
Section_prop *ne2ksec = control ? static_cast<Section_prop*>(control->GetSection("ne2000")) : NULL;
|
|
if (ne2ksec) macstr = ne2ksec->Get_string("macaddr");
|
|
if (!ParseMac(macstr, mac)) {
|
|
LOG_MSG("STEAM: [ne2000] macaddr is \"%s\" -- backend=steam needs a fixed MAC, "
|
|
"exactly six two-digit hex octets (02:00: + the four IP octets, "
|
|
"see SESSION-CONTRACT section 1)",
|
|
(macstr && *macstr) ? macstr : "unset");
|
|
return false;
|
|
}
|
|
|
|
vwesteam::Config cfg;
|
|
uint32_t selfip = 0;
|
|
uint64_t sessionid = 0;
|
|
std::string name;
|
|
|
|
if (!sessionpath.empty()) {
|
|
SessionPeer self;
|
|
std::vector<SessionPeer> peers;
|
|
std::string err;
|
|
if (!ParseSessionFile(sessionpath, self, peers, err)) {
|
|
LOG_MSG("STEAM: %s", err.c_str());
|
|
return false;
|
|
}
|
|
selfip = self.ip;
|
|
sessionid = self.steamId;
|
|
name = self.name;
|
|
if (self.macKnown && memcmp(self.mac, mac, 6) != 0)
|
|
LOG_MSG("STEAM: session file says our MAC is %02x:%02x:%02x:%02x:%02x:%02x but "
|
|
"[ne2000] macaddr is %02x:%02x:%02x:%02x:%02x:%02x -- the conf wins, "
|
|
"peers will route to the conf MAC",
|
|
self.mac[0],self.mac[1],self.mac[2],self.mac[3],self.mac[4],self.mac[5],
|
|
mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
|
|
// Peer MACs/IPs are logged but not fed to the transport -- those are
|
|
// learned from HELLO, which stays correct when a player relaunches into
|
|
// a different slot. The "gateway": true flag IS passed through: it is
|
|
// the default route for any unicast we cannot place, which in practice
|
|
// means everything bound for TeslaConsole at .10 (a host-LAN NIC MAC
|
|
// that can never appear in the peer table). Without it those frames
|
|
// would flood, putting the whole egg transfer on every player's uplink.
|
|
for (size_t i = 0; i < peers.size(); i++) {
|
|
cfg.peers.push_back(peers[i].steamId);
|
|
if (peers[i].gateway) {
|
|
if (cfg.gateway && cfg.gateway != peers[i].steamId)
|
|
LOG_MSG("STEAM: session file marks more than one peer as the "
|
|
"gateway; keeping %s", std::to_string(cfg.gateway).c_str());
|
|
else
|
|
cfg.gateway = peers[i].steamId;
|
|
}
|
|
LOG_MSG("STEAM: peer slot %d \"%s\" %s ip %u.%u.%u.%u mac %02x:%02x:%02x:%02x:%02x:%02x%s",
|
|
peers[i].slot, peers[i].name.c_str(),
|
|
std::to_string(peers[i].steamId).c_str(),
|
|
(peers[i].ip >> 24) & 0xff, (peers[i].ip >> 16) & 0xff,
|
|
(peers[i].ip >> 8) & 0xff, peers[i].ip & 0xff,
|
|
peers[i].mac[0],peers[i].mac[1],peers[i].mac[2],
|
|
peers[i].mac[3],peers[i].mac[4],peers[i].mac[5],
|
|
peers[i].gateway ? " (gateway)" : "");
|
|
}
|
|
}
|
|
|
|
// A csv is a union with the session file's roster rather than an override:
|
|
// peers is an allowlist, and an operator who names an ID means to admit it.
|
|
ParseIdCsv(peerscsv, cfg.peers);
|
|
|
|
if (!lobbystr.empty() && !ParseId(lobbystr, cfg.lobby))
|
|
LOG_MSG("STEAM: ignoring unparseable lobby id \"%s\"", lobbystr.c_str());
|
|
|
|
cfg.channel = section->Get_int("channel");
|
|
|
|
const char* envname = getenv("VWE_STEAM_NAME");
|
|
cfg.name = (envname && *envname) ? std::string(envname) : name;
|
|
|
|
if (cfg.peers.empty() && cfg.lobby == 0) {
|
|
LOG_MSG("STEAM: no peers -- set [ethernet, steam] session/peers or the "
|
|
"VWE_STEAM_SESSION / VWE_STEAM_PEERS environment variables");
|
|
return false;
|
|
}
|
|
|
|
// No session file: recover our IP from the MAC. Every VWE MAC is 02:00: +
|
|
// the four IP octets (deploy/configure.ps1's formula), so this is exact
|
|
// for a configured pod and simply stays 0 ("unknown") for anything else.
|
|
if (!selfip && mac[0] == 0x02 && mac[1] == 0x00)
|
|
selfip = ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) |
|
|
((uint32_t)mac[4] << 8) | (uint32_t)mac[5];
|
|
|
|
vwesteam::SetLog(SteamLog);
|
|
std::string err;
|
|
if (!vwesteam::Init(cfg, mac, selfip, err)) {
|
|
LOG_MSG("STEAM: link did not come up: %s", err.c_str());
|
|
return false;
|
|
}
|
|
up = true;
|
|
|
|
// Stale session file, or the machine is signed into a different Steam
|
|
// account than the lobby handed the roster to: every peer's allowlist will
|
|
// refuse us. Say it here, not at mission start when nobody appears.
|
|
if (sessionid && vwesteam::SelfSteamID() && sessionid != vwesteam::SelfSteamID())
|
|
LOG_MSG("STEAM: session file was written for SteamID %s but this machine is signed in as %s "
|
|
"-- peers will refuse us",
|
|
std::to_string(sessionid).c_str(), std::to_string(vwesteam::SelfSteamID()).c_str());
|
|
|
|
LOG_MSG("STEAM: link up -- self %s mac %02x:%02x:%02x:%02x:%02x:%02x ip %u.%u.%u.%u, "
|
|
"%u peer(s), channel %d",
|
|
std::to_string(vwesteam::SelfSteamID()).c_str(),
|
|
mac[0],mac[1],mac[2],mac[3],mac[4],mac[5],
|
|
(selfip >> 24) & 0xff, (selfip >> 16) & 0xff, (selfip >> 8) & 0xff, selfip & 0xff,
|
|
(unsigned)cfg.peers.size(), cfg.channel);
|
|
return true;
|
|
}
|
|
|
|
void SteamEthernetConnection::SendPacket(const uint8_t* packet, int len)
|
|
{
|
|
static unsigned long txn = 0;
|
|
++txn; /* split off: pcap's one-liner form of this is a sequence-point warning */
|
|
if ((txn & (txn - 1)) == 0 && len >= 14) /* powers of 2, avoid spam */
|
|
LOG_MSG("STEAM TX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x",
|
|
txn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8]);
|
|
vwesteam::SendFrame(packet, len);
|
|
}
|
|
|
|
void SteamEthernetConnection::GetPackets(std::function<void(const uint8_t*, int)> callback)
|
|
{
|
|
// The one and only pump site. NE2000_Poller is a TIMER tick handler, so
|
|
// this runs once per emulated millisecond -- which is why the Steam link
|
|
// needs no thread and no timer of its own anywhere.
|
|
vwesteam::Pump();
|
|
|
|
static unsigned long rxn = 0;
|
|
vwesteam::PollFrames([&](const uint8_t* packet, int len) {
|
|
// Acceptance mirrors the pcap backend's BPF clause for clause so the
|
|
// guest cannot tell the two backends apart.
|
|
if (len < 14 || len > vwesteam::VWE_MAX_FRAME) return;
|
|
const bool bcast = (packet[0] & packet[1] & packet[2] &
|
|
packet[3] & packet[4] & packet[5]) == 0xff;
|
|
if (!bcast) {
|
|
if (packet[0] & 1) return; /* multicast: the BPF never matched it */
|
|
if (memcmp(packet, mac, 6) != 0) return; /* ether dst <self> */
|
|
}
|
|
// "and not ether src <self>", the BPF's third clause. steamlink.h
|
|
// calls this structurally impossible because a peer never echoes;
|
|
// the GATEWAY can, though. Its capture filter is bare
|
|
// "ether broadcast" until the first HELLO lands (steam-gw
|
|
// BuildFilter()), with no "not ether src" clause to build yet -- so
|
|
// in that window it captures its own npcap injection of OUR
|
|
// broadcast and tunnels it straight back to us. A real NIC never
|
|
// hears its own transmit, and a boot-ARP that returns to its sender
|
|
// is duplicate-address bait, so drop it here for one memcmp.
|
|
if (memcmp(packet + 6, mac, 6) == 0) return;
|
|
++rxn;
|
|
if ((rxn & (rxn - 1)) == 0)
|
|
LOG_MSG("STEAM RX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x",
|
|
rxn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8]);
|
|
callback(packet, len);
|
|
});
|
|
}
|
|
|
|
#endif
|