/* * Copyright (C) 2021 The DOSBox Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config.h" #if C_PCAP #include "ethernet_pcap.h" #include "dosbox.h" #include "logging.h" #include "support.h" /* strcasecmp */ #include "control.h" /* [ne2000] macaddr for the RX filter */ #include #include /* getenv/strtoul -- the latency shim's knobs */ #include #include extern std::string niclist; #ifdef WIN32 #include // DLL loading #define pcap_sendpacket(A,B,C) PacketSendPacket(A,B,C) #define pcap_close(A) PacketClose(A) #define pcap_freealldevs(A) PacketFreealldevs(A) #define pcap_open(A,B,C,D,E,F) PacketOpen(A,B,C,D,E,F) #define pcap_next_ex(A,B,C) PacketNextEx(A,B,C) #define pcap_findalldevs_ex(A,B,C,D) PacketFindALlDevsEx(A,B,C,D) #define pcap_geterr(A) PacketGetError(A) #define pcap_compile(A,B,C,D,E) PacketCompile(A,B,C,D,E) #define pcap_setfilter(A,B) PacketSetFilter(A,B) #define pcap_freecode(A) PacketFreecode(A) int (*PacketSendPacket)(pcap_t *, const u_char *, int) = 0; void (*PacketClose)(pcap_t *) = 0; void (*PacketFreealldevs)(pcap_if_t *) = 0; pcap_t* (*PacketOpen)(char const *,int,int,int,struct pcap_rmtauth *,char *) = 0; int (*PacketNextEx)(pcap_t *, struct pcap_pkthdr **, const u_char **) = 0; int (*PacketFindALlDevsEx)(char *, struct pcap_rmtauth *, pcap_if_t **, char *) = 0; char* (*PacketGetError)(pcap_t *) = nullptr; int (*PacketCompile)(pcap_t *, struct bpf_program *, const char *, int, bpf_u_int32) = 0; int (*PacketSetFilter)(pcap_t *, struct bpf_program *) = 0; void (*PacketFreecode)(struct bpf_program *) = 0; char pcap_src_if_string[] = PCAP_SRC_IF_STRING; bool LoadPcapLibrary() { // remember if we've already initialized the library static HINSTANCE pcapinst = (HINSTANCE)-1; if(pcapinst!=(HINSTANCE)-1) { return (pcapinst!=NULL); } // init the library pcapinst = LoadLibrary("WPCAP.DLL"); if(pcapinst==NULL) { niclist = "WinPcap has to be installed for the NE2000 to work."; LOG_MSG(niclist.c_str()); return false; } FARPROC psp; #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 psp = GetProcAddress(pcapinst,"pcap_sendpacket"); if(!PacketSendPacket) PacketSendPacket = (int (__cdecl *)(pcap_t *,const u_char *,int))psp; psp = GetProcAddress(pcapinst,"pcap_close"); if(!PacketClose) PacketClose = (void (__cdecl *)(pcap_t *)) psp; psp = GetProcAddress(pcapinst,"pcap_freealldevs"); if(!PacketFreealldevs) PacketFreealldevs = (void (__cdecl *)(pcap_if_t *)) psp; psp = GetProcAddress(pcapinst,"pcap_open"); if(!PacketOpen) PacketOpen = (pcap_t* (__cdecl *)(char const *,int,int,int,struct pcap_rmtauth *,char *)) psp; psp = GetProcAddress(pcapinst,"pcap_next_ex"); if(!PacketNextEx) PacketNextEx = (int (__cdecl *)(pcap_t *, struct pcap_pkthdr **, const u_char **)) psp; psp = GetProcAddress(pcapinst,"pcap_findalldevs_ex"); if(!PacketFindALlDevsEx) PacketFindALlDevsEx = (int (__cdecl *)(char *, struct pcap_rmtauth *, pcap_if_t **, char *)) psp; psp = GetProcAddress(pcapinst,"pcap_geterr"); if(!PacketGetError) PacketGetError = (char* (__cdecl *)(pcap_t *)) psp; /* RX-filter trio: optional -- if absent we just run unfiltered */ psp = GetProcAddress(pcapinst,"pcap_compile"); if(!PacketCompile) PacketCompile = (int (__cdecl *)(pcap_t *, struct bpf_program *, const char *, int, bpf_u_int32)) psp; psp = GetProcAddress(pcapinst,"pcap_setfilter"); if(!PacketSetFilter) PacketSetFilter = (int (__cdecl *)(pcap_t *, struct bpf_program *)) psp; psp = GetProcAddress(pcapinst,"pcap_freecode"); if(!PacketFreecode) PacketFreecode = (void (__cdecl *)(struct bpf_program *)) psp; #ifdef __MINGW32__ #pragma GCC diagnostic pop #endif if(PacketFindALlDevsEx==0 || PacketNextEx==0 || PacketOpen==0 || PacketFreealldevs==0 || PacketClose==0 || PacketSendPacket==0 || PacketGetError==0) { niclist = "Incorrect or non-functional WinPcap version."; LOG_MSG(niclist.c_str()); pcapinst = NULL; return false; } return true; } #endif /* ---- Phase 0: the latency-tolerance gate -------------------------------- * * The riskiest assumption in the whole internet-play plan is that the 1995 * stack -- WATTCP + netnub + L4NetworkManager, every one of them tuned for a * same-switch LAN with sub-millisecond RTT -- still functions at 30-150ms RTT * with jitter. They are unpatchable 30-year-old binaries, so the answer has to * be MEASURED on the existing LAN rigs, with zero Steam code, before any * transport work is committed to. External shapers (clumsy, WinDivert) hook * the host IP stack and never see Npcap-injected frames at all, so the delay * has to live inside the backend. Three env knobs, read once at Initialize(): * * VWE_NET_DELAY_MS fixed one-way delay applied to BOTH TX and RX, so one * shimmed pod adds 2x delay to every RTT through it. * VWE_NET_JITTER_MS uniform 0..N ms extra per frame, with release times * clamped monotonic per queue -- frames NEVER reorder. * The real transport is Steam's reliable ORDERED channel; * a reordering shim would be measuring a transport we are * not going to build. * VWE_NET_LOSS_PCT drop percentage, default 0. Reliable delivery surfaces * loss as a delay spike rather than a drop, so this is * for characterization only and is not part of the gate. * * All unset = the shim does not exist: SendPacket/GetPackets run byte-for-byte * the code they ran before it was added, and not a single extra log line is * emitted (same convention as VPXLOG). If any of the three IS set, exactly one * NETSHIM line is always logged -- ACTIVE or INERT -- because a silent run is * ambiguous with a binary that was built without this file at all. * * Timebase is WALL clock, not emulated time -- network latency is wall clock * and the guest's own timers must stay free to disagree with it. GetPackets() * is called from NE2000_Poller, a TIMER_AddTickHandler tick, i.e. once per * emulated millisecond, and that is the drain quantum: no thread and no timer * is added anywhere. steady_clock rather than GetTickCount64 because the * latter advances in ~15.6ms steps, which would quantize the 15ms knobs into * all-or-nothing jumps. * * State is file-static rather than class members because ethernet_pcap.h is * stock DOSBox-X we would rather not fork, and there is only ever one NE2000. * Test matrix, two-rig topology and the pass/fail gate: emulator/NET-NOTES.md. */ enum { SHIM_FRAMEBUF = 1600 }; /* larger than any frame the NE2000 can make */ struct DelayedFrame { uint64_t release_ms; int len; uint8_t data[SHIM_FRAMEBUF]; }; static bool shim_on = false; static unsigned shim_delay_ms = 0, shim_jitter_ms = 0, shim_loss_pct = 0; static std::deque shim_txq, shim_rxq; static uint64_t shim_tx_release = 0, shim_rx_release = 0; /* monotonic clamp */ static unsigned long shim_lost = 0; static uint32_t shim_rng = 0; static bool shim_oversize_warned = false; static uint64_t shim_now_ms() { using namespace std::chrono; return (uint64_t)duration_cast( steady_clock::now().time_since_epoch()).count(); } /* Own xorshift instead of rand(): the shim must not perturb the global rand() * sequence the rest of the emulator draws from. */ static uint32_t shim_rand() { shim_rng ^= shim_rng << 13; shim_rng ^= shim_rng >> 17; shim_rng ^= shim_rng << 5; return shim_rng; } /* Park a frame on `q`. True = the caller must NOT act on the frame now (it was * queued, or the loss knob ate it). A frame too big for the buffer is passed * through undelayed instead of truncated -- a truncated frame would be a * corruption bug wearing a latency finding's clothes. */ static bool shim_hold(std::deque& q, uint64_t& last_release, const uint8_t* frame, int len) { if (len < 0 || len > (int)SHIM_FRAMEBUF) { if (!shim_oversize_warned) { shim_oversize_warned = true; LOG_MSG("NETSHIM: len=%d exceeds the %d-byte queue buffer -- oversize frames pass through undelayed", len, (int)SHIM_FRAMEBUF); } return false; } if (shim_loss_pct && (shim_rand() % 100u) < shim_loss_pct) { shim_lost++; return true; } uint64_t rel = shim_now_ms() + shim_delay_ms; if (shim_jitter_ms) rel += shim_rand() % (shim_jitter_ms + 1u); if (rel < last_release) rel = last_release; /* ordered, never reordered */ last_release = rel; q.push_back(DelayedFrame()); DelayedFrame& f = q.back(); f.release_ms = rel; f.len = len; memcpy(f.data, frame, (size_t)len); return true; } /* Read one knob. `present` is set if the variable EXISTS, whatever its value, * so that (a) an all-zero control run still announces itself and (b) a typo'd * or non-numeric value is reported instead of quietly measuring nothing -- * strtoul("abc") is 0, which is indistinguishable from "shim not built in". * Trailing whitespace is tolerated: `set VWE_NET_DELAY_MS=50 ` in a .bat keeps * the space, and rejecting that would be a trap rather than a check. */ static unsigned shim_knob(const char* name, bool& present) { const char* e = getenv(name); if (!e) return 0; present = true; const char* p = e; while (*p == ' ' || *p == '\t') p++; char* end = NULL; unsigned long v = strtoul(p, &end, 10); if (end) while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') end++; if (end == p || (end && *end) || *p == '-') { LOG_MSG("NETSHIM: %s=\"%s\" is not a plain non-negative number -- IGNORED (treated as 0)", name, e); return 0; } if (v > 100000ul) { /* a 100s one-way delay is a typo, not a test point */ LOG_MSG("NETSHIM: %s=%lu is absurd -- clamped to 100000", name, v); v = 100000ul; } return (unsigned)v; } /* Queue-depth tail for the throttled TX/RX log lines; empty when off, so an * unshimmed run's logs are unchanged. */ static const char* shim_qtail() { static char t[64]; if (!shim_on) return ""; if (shim_loss_pct) snprintf(t, sizeof(t), " q=%u/%u lost=%lu", (unsigned)shim_txq.size(), (unsigned)shim_rxq.size(), shim_lost); else snprintf(t, sizeof(t), " q=%u/%u", (unsigned)shim_txq.size(), (unsigned)shim_rxq.size()); return t; } PcapEthernetConnection::PcapEthernetConnection() : EthernetConnection() { } PcapEthernetConnection::~PcapEthernetConnection() { if(adhandle) pcap_close(adhandle); } bool PcapEthernetConnection::Initialize(Section* config) { Section_prop *section = static_cast(config); const char* realnicstring = section->Get_string("realnic"); #ifdef WIN32 if(!LoadPcapLibrary()) { return false; } #endif // find out which pcap device to use pcap_if_t *alldevs; pcap_if_t *currentdev = NULL; char errbuf[PCAP_ERRBUF_SIZE]; unsigned int userdev; #ifdef WIN32 if (pcap_findalldevs_ex(pcap_src_if_string, NULL, &alldevs, errbuf) == -1) #else if (pcap_findalldevs(&alldevs, errbuf) == -1) #endif { niclist = "Cannot enumerate network interfaces: "+std::string(errbuf); LOG_MSG("%s", niclist.c_str()); return false; } { Bitu i = 0; niclist = "Network Interface List\n-------------------------------------------------------------\n"; for(currentdev=alldevs; currentdev!=NULL; currentdev=currentdev->next) { const char* desc = "no description"; if(currentdev->description) desc=currentdev->description; i++; niclist+=(i<10?" ":"")+std::to_string(i)+" "+currentdev->name+"\n ("+desc+")\n"; } } if (!strcasecmp(realnicstring,"list")) { // print list and quit std::istringstream in(("\n"+niclist+"\n").c_str()); if (in) for (std::string line; std::getline(in, line); ) LOG_MSG("%s", line.c_str()); LOG_MSG("NOTE: No interface was chosen because the realnic setting is set to 'list', set realnic to the number of the interface you want to use."); pcap_freealldevs(alldevs); return false; } else if(1==sscanf(realnicstring,"%u",&userdev)) { // user passed us a number Bitu i = 0; currentdev=alldevs; while(currentdev!=NULL) { i++; if(i==userdev) break; else currentdev=currentdev->next; } } else { // user might have passed a piece of name for(currentdev=alldevs; currentdev!=NULL; currentdev=currentdev->next) { if(strstr(currentdev->name,realnicstring)) { break; }else if(currentdev->description!=NULL && strstr(currentdev->description,realnicstring)) { break; } } } if(currentdev==NULL) { LOG_MSG("Unable to find network interface - check realnic parameter\n"); pcap_freealldevs(alldevs); return false; } // print out which interface we are going to use const char* desc = "no description"; if(currentdev->description) desc=currentdev->description; LOG_MSG("Using Network interface:\n%s\n(%s)\n",currentdev->name,desc); const char *timeoutstr = section->Get_string("timeout"); char *end; int timeout = -1; if (!strlen(timeoutstr)||(timeoutstr[0]!='-'&&!isdigit(timeoutstr[0]))) { // Default timeout values #ifdef MACOSX timeout = 3000; // For macOS, use 3000ms as it does not appear to support -1 #else timeout = -1; // For other platforms, use -1 which should mean "non-blocking mode" #endif } else timeout = strtoul(timeoutstr,&end,10); // attempt to open it #ifdef WIN32 if ( (adhandle= pcap_open( currentdev->name, // name of the device 65536, // portion of the packet to capture // 65536 = whole packet PCAP_OPENFLAG_PROMISCUOUS, // promiscuous mode timeout, // read timeout NULL, // authentication on the remote machine errbuf // error buffer ) ) == NULL) #else /*pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf)*/ if ( (adhandle= pcap_open_live( currentdev->name, // name of the device 65536, // portion of the packet to capture // 65536 = whole packet true, // promiscuous mode timeout, // read timeout errbuf // error buffer ) ) == NULL) #endif { LOG_MSG("\nUnable to open the interface: %s.", errbuf); pcap_freealldevs(alldevs); return false; } pcap_freealldevs(alldevs); #ifndef WIN32 pcap_setnonblock(adhandle,1,errbuf); #endif // A real NE2000 in non-promiscuous mode hears only its own MAC plus // broadcast; this promiscuous tap otherwise hands the guest EVERY frame // on the host NIC. On a live LAN the host's own bulk traffic wedges the // DOS-era stack before it boots (netnub sits at "discarding..."), so // filter kernel-side and the flood never reaches the emulation. The // guest stack is unicast+ARP only: multicast is deliberately excluded, // and "not ether src" drops npcap's loopback of our own injected frames // (a real NIC never hears its own TX). Any failure = run unfiltered, // i.e. the old behavior. #ifdef WIN32 const bool have_filter_api = (PacketCompile != 0 && PacketSetFilter != 0); #else const bool have_filter_api = true; #endif const char *macstr = NULL; Section_prop *ne2ksec = control ? static_cast(control->GetSection("ne2000")) : NULL; if (ne2ksec) macstr = ne2ksec->Get_string("macaddr"); unsigned int m[6]; if (have_filter_api && macstr && sscanf(macstr, "%x:%x:%x:%x:%x:%x", &m[0],&m[1],&m[2],&m[3],&m[4],&m[5]) == 6) { char fexp[160]; snprintf(fexp, sizeof(fexp), "(ether dst %02x:%02x:%02x:%02x:%02x:%02x or ether broadcast)" " and not ether src %02x:%02x:%02x:%02x:%02x:%02x", m[0],m[1],m[2],m[3],m[4],m[5], m[0],m[1],m[2],m[3],m[4],m[5]); struct bpf_program fprog; if (pcap_compile(adhandle, &fprog, fexp, 1, (bpf_u_int32)0xffffffffu) == 0) { if (pcap_setfilter(adhandle, &fprog) == 0) LOG_MSG("PCAP RX filter active: %s", fexp); else LOG_MSG("PCAP setfilter failed (%s) -- running unfiltered", pcap_geterr(adhandle)); #ifdef WIN32 if (PacketFreecode) #endif pcap_freecode(&fprog); } else LOG_MSG("PCAP filter compile failed (%s) -- running unfiltered", pcap_geterr(adhandle)); } else LOG_MSG("PCAP RX filter NOT installed (%s) -- promiscuous, expect LAN noise", have_filter_api ? "no ne2000 macaddr" : "wpcap lacks pcap_compile"); /* Latency-tolerance gate: read the knobs once, and say out loud what was * applied so a test run's logs are self-describing months later. */ shim_txq.clear(); shim_rxq.clear(); shim_tx_release = shim_rx_release = 0; shim_lost = 0; shim_oversize_warned = false; shim_delay_ms = shim_jitter_ms = shim_loss_pct = 0; bool shim_env_seen = false; shim_delay_ms = shim_knob("VWE_NET_DELAY_MS", shim_env_seen); shim_jitter_ms = shim_knob("VWE_NET_JITTER_MS", shim_env_seen); shim_loss_pct = shim_knob("VWE_NET_LOSS_PCT", shim_env_seen); if (shim_loss_pct > 100) shim_loss_pct = 100; shim_on = (shim_delay_ms || shim_jitter_ms || shim_loss_pct); if (shim_on) { shim_rng = (uint32_t)shim_now_ms() | 1u; /* xorshift is stuck at 0 */ LOG_MSG("NETSHIM ACTIVE: delay=%ums each way (+%ums on every RTT through this pod), " "jitter=0..%ums monotonic, loss=%u%% -- Phase 0 latency gate, see NET-NOTES.md", shim_delay_ms, shim_delay_ms * 2, shim_jitter_ms, shim_loss_pct); } else if (shim_env_seen) { /* All knobs zero is a legitimate control point in the matrix -- but it * is ALSO exactly what a dosbox-x.exe built WITHOUT this file looks * like, and vpx-device/ is only the version-controlled copy (it has to * be copied into src/src/misc/ before `make`). Announcing the inert * case is what tells the operator the binary really does contain the * shim: VWE_NET_* set and NO "NETSHIM" line at all means it does not, * and the whole matrix would otherwise read as "0ms, no effect". */ LOG_MSG("NETSHIM INERT: VWE_NET_* present but every knob is 0 " "-- baseline/control run, no delay applied (shim IS compiled in)"); } return true; } void PcapEthernetConnection::SendPacket(const uint8_t* packet, int len) { static unsigned long txn = 0; if (((++txn) & (txn - 1)) == 0) /* powers of 2, avoid spam */ LOG_MSG("PCAP TX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x%s", txn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8], shim_qtail()); if (shim_on && shim_hold(shim_txq, shim_tx_release, packet, len)) return; int ret = pcap_sendpacket(adhandle, packet, len); if(ret) LOG_MSG("PCAP error: %s", pcap_geterr(adhandle)); } void PcapEthernetConnection::GetPackets(std::function callback) { int res; struct pcap_pkthdr *header; u_char *pkt_data; static unsigned long rxn = 0; /* Shim TX first: SendPacket parked these, and this handler is the only * thing that runs often enough to release them on time. */ if (shim_on) { uint64_t now = shim_now_ms(); while (!shim_txq.empty() && shim_txq.front().release_ms <= now) { const DelayedFrame& f = shim_txq.front(); if (pcap_sendpacket(adhandle, f.data, f.len)) LOG_MSG("PCAP error: %s", pcap_geterr(adhandle)); shim_txq.pop_front(); } } //#if 0 while((res = pcap_next_ex( adhandle, &header, (const u_char **)&pkt_data)) > 0) { if (((++rxn) & (rxn - 1)) == 0) LOG_MSG("PCAP RX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x%s", rxn, header->len, pkt_data[0],pkt_data[1],pkt_data[2], pkt_data[6],pkt_data[7],pkt_data[8], shim_qtail()); if (shim_on && shim_hold(shim_rxq, shim_rx_release, pkt_data, (int)header->len)) continue; callback(pkt_data, header->len); } //#endif if (shim_on) { uint64_t now = shim_now_ms(); while (!shim_rxq.empty() && shim_rxq.front().release_ms <= now) { const DelayedFrame& f = shim_rxq.front(); callback(f.data, f.len); shim_rxq.pop_front(); } } } #endif