diff --git a/emulator/NET-NOTES.md b/emulator/NET-NOTES.md index b7db118..dbcd793 100644 --- a/emulator/NET-NOTES.md +++ b/emulator/NET-NOTES.md @@ -530,11 +530,29 @@ pod frames reach the host stack but no longer reach the TAP members, so **the SheepShaver Mac console is DEAF while this is set** (and wire- injection diagnostics can't reach the pod either; pod RX is unaffected). Revert: `Remove-ItemProperty ... -Name SendToRxAdapters` + service -bounce + pod relaunch. PROPER BOTH-AT-ONCE FIX (future): upgrade Npcap -user-mode DLLs to >=1.83 and patch the fork's pcap backend to mirror -every send onto a second handle set to per-handle MODE_SENDTORX -- -frames then go out to the TAPs AND up to the host. Host -connect/disconnect churns netnub (exit + ~60s relaunch) -- expected. +bounce + pod relaunch. Host connect/disconnect churns netnub (exit + +~60s relaunch) -- expected. + +**BOTH CONSOLES AT ONCE — WORKING (2026-07-10, `net-tools/tap2_mirror.py`):** +keep SendToRxAdapters set (host console works) and run the mirror +daemon, which turns TAP2's free application side into a userspace +bridge port for the Mac: (a) pod-sourced frames destined for the Mac +MAC or broadcast are written into TAP2 -> Windows bridge forwards +member->member to TAP1 -> SheepShaver hears; (b) REQUIRED second +direction: writing pod-src frames into TAP2 teaches the bridge's MAC +table that the pod lives on TAP2, so the Mac's unicasts to the pod get +steered INTO TAP2 -- a reader thread drains them and re-injects on the +bridge (RX-indicated up under SendToRx = exactly where the pod's pcap +listens). Loop guards: pod-src frames never re-injected; 0.2s +frame-hash dedup on the mirror side. Verified live: Console 4.10 +readied the pod through it (counters: mirrored=24 pod->Mac, +reinjected=105 Mac->pod) with the .NET console path intact. Run +detached: `pythonw net-tools/tap2_mirror.py` (log/pid in the vwe-pod +work dir; pod MAC + TAP2 GUID are constants at the top). Operate +ALTERNATING (one console readied at a time -- two masters vs one netnub +is untested). Cleaner future fix stands: Npcap >=1.83 user-mode DLLs + +dual-handle MODE_SENDTORX in the fork's pcap backend, which retires +both the registry setting and the mirror. ## MODERN CONSOLE MILESTONE (2026-07-09 late): TeslaSuite .NET console runs the pod, fully hands-free diff --git a/emulator/net-tools/tap2_mirror.py b/emulator/net-tools/tap2_mirror.py new file mode 100644 index 0000000..8c8eb3e --- /dev/null +++ b/emulator/net-tools/tap2_mirror.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Pod->Mac mirror for the SendToRx era: with Npcap's SendToRxAdapters set +on the Network Bridge (so the host .NET console hears the pod), the pod's +injected frames no longer reach the TAP members -- SheepShaver goes deaf. +Every other path still works. This daemon closes the one missing hop: +capture pod-sourced frames destined for the Mac (or broadcast) on the +bridge, and write them into TAP2's free application side; the Windows +bridge then forwards them member->member to TAP1 where SheepShaver reads +them. Result: BOTH consoles (Mac 4.10 + host .NET) work at once. + +A short-lived frame-hash dedup breaks the reinjection loop (the TAP2 copy +is re-captured on the bridge; identical bytes within the window = skip). + +SECOND DIRECTION (required!): writing pod-sourced frames into TAP2 teaches +the Windows bridge that the pod's MAC lives on TAP2 -- from then on the +Mac's UNICAST frames to the pod are steered to TAP2's app side instead of +up the miniport where the pod's pcap listens. So a reader thread drains +TAP2 and re-injects those frames on the bridge (under SendToRxAdapters the +injection is RX-indicated up, exactly where the pod hears it). Frames the +pod itself sourced are never re-injected (loop guard). + +Run with any CPython (ctypes only). Stop with Ctrl+C / kill. +""" +import ctypes, os, struct, sys, threading, time, zlib +from ctypes import wintypes as wt + +BRIDGE = rb"\Device\NPF_{5DB5521D-2D56-40E8-9E3D-3B36C9EE7C8F}" +TAP2_PATH = r"\\.\Global\{C78EA2D1-6120-4944-A949-786B1B963CB5}.tap" +POD_MAC = bytes.fromhex("ce3d72673869") # DOSBox NE2000 (stable across runs) +MAC_MAC = bytes.fromhex("525400e73ef0") # SheepShaver (etherpermanentaddress) +BCAST = b"\xff" * 6 + +os.add_dll_directory(r"C:\Windows\System32\Npcap") +pc = ctypes.WinDLL(r"C:\Windows\System32\Npcap\wpcap.dll") +pc.pcap_open_live.restype = ctypes.c_void_p +pc.pcap_open_live.argtypes = [ctypes.c_char_p] + [ctypes.c_int]*3 + [ctypes.c_char_p] +pc.pcap_next_ex.restype = ctypes.c_int +pc.pcap_next_ex.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(ctypes.c_void_p)] +pc.pcap_setnonblock.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p] + +class PktHdr(ctypes.Structure): + _fields_ = [("s", ctypes.c_long), ("u", ctypes.c_long), + ("cl", ctypes.c_uint), ("l", ctypes.c_uint)] + +k32 = ctypes.WinDLL("kernel32", use_last_error=True) +GENERIC_RW = 0xC0000000 +OPEN_EXISTING = 3 +FILE_ATTRIBUTE_SYSTEM = 0x4 +tap = k32.CreateFileW(TAP2_PATH, GENERIC_RW, 0, None, OPEN_EXISTING, + FILE_ATTRIBUTE_SYSTEM, None) +if tap == wt.HANDLE(-1).value: + sys.exit(f"cannot open TAP2 app side ({ctypes.get_last_error()}) -- is " + "another process attached?") +# TAP_IOCTL_SET_MEDIA_STATUS(1): belt & braces alongside the registry force +TAP_IOCTL_SET_MEDIA_STATUS = 0x22C018 +status = ctypes.c_ulong(1) +ret = ctypes.c_ulong(0) +k32.DeviceIoControl(tap, TAP_IOCTL_SET_MEDIA_STATUS, ctypes.byref(status), 4, + ctypes.byref(status), 4, ctypes.byref(ret), None) + +err = ctypes.create_string_buffer(256) +cap = pc.pcap_open_live(BRIDGE, 65535, 1, 20, err) +if not cap: + sys.exit(f"pcap open failed: {err.value.decode()}") +pc.pcap_setnonblock(cap, 1, err) +pc.pcap_sendpacket.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int] + +# a second pcap handle for the reader thread's re-injections (thread safety) +inj = pc.pcap_open_live(BRIDGE, 65535, 0, 20, err) +if not inj: + sys.exit(f"pcap inject-handle open failed: {err.value.decode()}") + +reinjected = [0] +def tap_reader(): + """Drain frames the bridge steers INTO TAP2 (unicasts to MACs it has + learned on this port -- i.e. the Mac's frames to the pod once the + mirror has been writing) and re-inject them on the bridge so the + pod's capture receives them. Never re-inject pod-sourced frames.""" + buf = ctypes.create_string_buffer(2048) + n = ctypes.c_ulong(0) + while True: + ok = k32.ReadFile(tap, buf, 2048, ctypes.byref(n), None) + if not ok or n.value < 14: + time.sleep(0.002) + continue + frame = buf.raw[:n.value] + if frame[6:12] == POD_MAC: + continue + pc.pcap_sendpacket(inj, frame, len(frame)) + reinjected[0] += 1 + +threading.Thread(target=tap_reader, daemon=True).start() + +print(f"pod->Mac mirror up: bridge capture -> TAP2 app side " + f"(pod {POD_MAC.hex(':')}, mac {MAC_MAC.hex(':')}); " + f"TAP2 reader re-injects bridge-steered frames", flush=True) + +recent = {} # frame crc32 -> expiry time (loop breaker) +hdr = ctypes.c_void_p(); dat = ctypes.c_void_p() +mirrored = dropped_dup = 0 +last_report = time.time() +while True: + r = pc.pcap_next_ex(cap, ctypes.byref(hdr), ctypes.byref(dat)) + now = time.time() + if r == 1: + ph = ctypes.cast(hdr, ctypes.POINTER(PktHdr)).contents + if ph.cl >= 14: + frame = ctypes.string_at(dat, ph.cl) + if frame[6:12] == POD_MAC and (frame[0:6] == MAC_MAC + or frame[0:6] == BCAST): + h = zlib.crc32(frame) + exp = recent.get(h) + if exp is not None and exp > now: + dropped_dup += 1 + else: + recent[h] = now + 0.2 + written = ctypes.c_ulong(0) + k32.WriteFile(tap, frame, len(frame), + ctypes.byref(written), None) + mirrored += 1 + else: + time.sleep(0.005) + if now - last_report > 30: + last_report = now + for h in [k for k, v in recent.items() if v <= now]: + del recent[h] + print(f"mirrored={mirrored} dup_dropped={dropped_dup} " + f"reinjected={reinjected[0]}", flush=True)