net-tools: tap2_mirror -- both console generations on the pod at once
With SendToRxAdapters serving the host .NET console, the SheepShaver Mac console was deaf (pod frames go up instead of out to TAP members). The mirror daemon turns TAP2's free application side into a userspace bridge port: pod frames destined for the Mac are written into TAP2 (bridge forwards them to TAP1/SheepShaver), and a reader thread drains the unicasts the bridge's poisoned MAC table steers into TAP2 and re-injects them where the pod's pcap hears them. Loop guards: no pod-sourced re-injection + short frame-hash dedup. Verified live: Console 4.10 readied the pod through the mirror while the .NET console path stayed intact -- operator can now alternate between the 1996 Mac console and the modern one without touching the driver config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+23
-5
@@ -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-
|
**the SheepShaver Mac console is DEAF while this is set** (and wire-
|
||||||
injection diagnostics can't reach the pod either; pod RX is unaffected).
|
injection diagnostics can't reach the pod either; pod RX is unaffected).
|
||||||
Revert: `Remove-ItemProperty ... -Name SendToRxAdapters` + service
|
Revert: `Remove-ItemProperty ... -Name SendToRxAdapters` + service
|
||||||
bounce + pod relaunch. PROPER BOTH-AT-ONCE FIX (future): upgrade Npcap
|
bounce + pod relaunch. Host connect/disconnect churns netnub (exit +
|
||||||
user-mode DLLs to >=1.83 and patch the fork's pcap backend to mirror
|
~60s relaunch) -- expected.
|
||||||
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
|
**BOTH CONSOLES AT ONCE — WORKING (2026-07-10, `net-tools/tap2_mirror.py`):**
|
||||||
connect/disconnect churns netnub (exit + ~60s relaunch) -- expected.
|
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
|
## MODERN CONSOLE MILESTONE (2026-07-09 late): TeslaSuite .NET console runs the pod, fully hands-free
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user