Files
TeslaRel410/emulator/net-tools/tap2_mirror.py
T
CydandClaude Fable 5 9e922ecd89 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>
2026-07-09 12:16:50 -05:00

130 lines
5.7 KiB
Python

#!/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)