Files
TeslaRel410/emulator/NET-NOTES.md
T
CydandClaude Fable 5 bda2f7b8b3 Networking WORKS: real console <-> real pod over the emulated wire
The emulated SheepShaver console and DOSBox pod now exchange the live
console protocol on TCP 1501. Required chain: rebuild the DOSBox pcap
backend + launch with C:\Windows\System32\Npcap on PATH (npcap DLL
location); a two-TAP Windows bridge (single shared TAP fails); TAP2
MediaStatus=Always-Connected; and binding the pod's pcap to the BRIDGE
MINIPORT (not a member TAP -- member-injected frames aren't re-forwarded),
via realnic=DB5521D (letter-leading GUID fragment; leading-digit is parsed
as an interface index). Captured + decoded the real protocol values
(StateResponse clientID=5/flags=1/host=0/state=1/app=1=BattleTech), which
corrects the earlier synthetic guesses. Full writeup in NET-NOTES.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 17:14:48 -05:00

354 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Networking subsystem — recon & bring-up plan
Goal: bring the pod network up under the emulator so a pod boots the
production way (`netnub -f BTL4OPT`, mission egg delivered **over the
wire**) instead of the `-egg test.egg` dev bypass. This is also the
substrate the ops-console port will plug into.
## What the pod network actually is (recon 2026-07-04)
The stack, bottom to top:
1. **Ethernet NIC + Novell ODI** (AUTOEXEC.BAT): `lsl` (Link Support
Layer) → `lnepci` (Lance/PCnet PCI ODI driver) → **`odipkt`** (Dan
Lanciani's ODI→Packet-Driver shim). The ODI/Netware login side
(IPXODI/VLM/NET LOGIN in STARTNET.BAT) is for file-server access; the
game itself only needs the **packet-driver interface** odipkt exposes.
`NET.CFG`: LNEPCI, FRAME Ethernet_II + 802.2.
2. **WATTCP** — the TCP/IP stack. Confirmed by `WATTCP.CFG` in
`REL410/BT`, `REL410/RP`, and per-pod `VGL_LABS/THISPOD`:
```
my_ip = 200.0.0.113 netmask = 255.255.255.0
gateway = 200.0.0.1 nameserver = 200.0.0.1
```
So each pod is a static host on an isolated **200.0.0.0/24** LAN; the
ops console is almost certainly **200.0.0.1**.
3. **NetNub** (`netnub.exe`, real-mode) — launches the game as a child
(`netnub -f BTL4OPT`) and is the network server for the
protected-mode game. Interface (`NetNub/NETNUB.HPP`): a shared `Netcom`
struct (version 11, 64KB buffer) + a software interrupt. The game sets
a Function code (TCP_OPEN=3, TCP_LISTEN=4, TCP_CLOSE=5,
RESOLVE_ADDRESS=6, CHECK_SOCKET=7, UDP_*, plus remote file
OPEN/READ/WRITE/SEEK/CLOSE 12-19), copies the marked fields to real
mode, INTs, copies back. `tcp_Socket` is ~4300 bytes = classic WATTCP.
4. **L4NetworkManager** (`L4NET.HPP/.TCP`) — the game's net brick. The
**console is master and connects to the pods**; the pod receives
`ReceiveEggFileMessage` (the mission egg), replies
`AcknowledgeEggFileMessage` ("connected, ready, send the next host"),
and tracks `HostConnected/HostDisconnected`. If the console drops, the
pod is built to auto-start anyway.
Topology to replicate:
```
[ops console 200.0.0.1] --TCP--> [pod 200.0.0.113] (+ more pods .114..)
(master, egg source) (listens, ACKs, runs mission)
```
## Emulator enablers (already in the fork)
- **NE2000** ISA NIC emulated (`hardware/ne2000.cpp`, Bochs-derived);
config `[ne2000] ne2000=true, nicbase=, nicirq=, macaddr=, backend=`.
- **Two Ethernet backends** built: `misc/ethernet_pcap.cpp` (bridge to a
host NIC via npcap) and `misc/ethernet_slirp.cpp` (user-mode virtual
net / NAT). Plus `ethernet_nothing`.
Key simplification: the emulated card is an **NE2000, not a PCI Lance**, so
`lnepci` won't bind. We don't need the Novell ODI chain at all — WATTCP
finds a packet driver by scanning INT 0x60-0x80 for the `PKT DRVR`
signature, so we load a generic **NE2000 packet driver** (Crynwr
`NE2000.COM`) directly against the emulated card's base/IRQ. That drops
lsl/lnepci/odipkt/VLM entirely and hands NetNub/WATTCP the packet
interface they expect.
## Bring-up plan
**Backend choice.** Two viable paths:
- *pcap + host-only adapter (recommended, matches real topology):* bridge
the NE2000 to a host virtual switch; run the pod at 200.0.0.113 and the
stand-in console at 200.0.0.1 on that segment. WATTCP's static IP + LAN
assumptions hold exactly; the console connects inbound to the pod with
no NAT. Cost: npcap + a host-only/loopback adapter + admin.
- *slirp (fallback, self-contained):* no host NIC/admin, but it's NAT and
defaults to 10.0.2.0/24 — the pod LISTENS, so inbound needs slirp
host-forwarding and a guest-network/IP reconciliation with WATTCP's
hard-coded 200.0.0.113. Investigate whether DOSBox-X slirp allows the
custom net + static guest IP + inbound forward cleanly.
**Milestones**
1. **NIC up**: `[ne2000]` on, NE2000.COM packet driver loaded, WATTCP/
NetNub start clean; pod boots via `netnub -f BTL4OPT` (no `-egg`) and
sits waiting for the console. Verify NetNub reports net address
200.0.0.113 and a TCP_LISTEN is queued. (New scratch conf, mirror the
RIO/sound conf pattern.)
2. **L3/L4 reachability**: from the host segment, confirm the pod answers
ARP/ping at 200.0.0.113 and a raw TCP connect to its listen port
completes (proves NE2000↔backend↔host path end-to-end).
3. **Decode the console→pod egg protocol**: `NetworkPacketHeader` +
message framing from `network.hpp`/`hostmgr.hpp` + the
ReceiveEggFileMessage layout, cross-checked with a live capture of the
pod's listen/ACK. (Pin the listen port here — not yet found in source;
grep NETNUB.EXE strings / capture.)
4. **Eggs over the wire**: a minimal host-side **stand-in console**
(Python) connects to the pod, pushes a mission egg, handles the ACK →
pod runs the mission with no `-egg` bypass. **This is the headline
goal.**
5. *(later, joins the console-port workstream):* replace the stand-in
with the ported/emulated Mac ops console; multi-pod coordination
(HostConnected/Disconnected, mission review, camera ship).
## Milestone 1 — DONE (2026-07-04): pod boots on the network path
Verified end to end under DOSBox-X (slirp backend), no `-egg` bypass:
- `[ne2000] ne2000=true, nicbase=300, nicirq=3, backend=slirp` → NE2000
emulated at Base=0x300 irq=3; slirp 4.9.1 initialized.
- **The Novell ODI chain works against the emulated NE2000**, no external
packet driver needed: `lsl` → `ne2000` (Novell/Eagle NE2000 MLID v1.53,
from NWCLIENT) → `odipkt` (FTP Software ODI packet driver). ODIPKT
installed at **SINT 0x60**, MLID NE2000, MAC CE:3D:72:67:38:69, frames
Ethernet_II (board 1) + 802.2 (board 2).
- GOTCHA: the ODI tools read `NET.CFG` from the directory the `.COM` loads
*from*, and the stock `NWCLIENT\NET.CFG` says `Link Driver LNEPCI` — with
no NE2000 section the MLID defaults to 802.2-only and odipkt fails
("An MLID could not be found"). Fix without touching ALPHA_1: keep an
emulator `NET.CFG` (`Link Driver NE2000` + `FRAME Ethernet_II`) beside
copies of lsl/ne2000/odipkt on a scratch drive and load from there.
- `netnub -f btl4opt` (no egg) launches the game as `btl4opt -net 250224`,
sets up the game↔netnub channel at **INT 0x61** (separate from odipkt's
0x60), initializes the network manager ("Changing blocking from 0 to 1"),
and the game boots through the VPX handshake to an open (blank) render
window — **waiting for a console to deliver a mission egg.**
Working scratch files: `scratchpad/net_stageB.conf`,
`scratchpad/net/{NET.CFG,LSL.COM,NE2000.COM,ODIPKT.COM}`. Launch env:
VPXLOG + VPX_RESPOND=1 + VPX_RENDER=1 (VPX board must answer or the game
exits before networking).
## Console side: a Mac emulator stands in for the ops console (user, 2026-07-04)
The user is building a **Mac emulator running the real 410console** as the
console peer (instead of a from-scratch Python stand-in). This merges the
networking and console-port workstreams: the real console software will
connect to the pod and push eggs. Implication for topology — two separate
emulators (DOSBox pod + Mac console) must share an L2 segment, which slirp
(NAT, per-process isolation) cannot bridge. **Plan: move the pod's NE2000
to `backend=pcap` on a host-only/loopback adapter; bridge the Mac emulator
to the same adapter; pod=200.0.0.113, console=200.0.0.1 on 200.0.0.0/24.**
Then milestone 3 (protocol) can be captured live from the real console
traffic rather than reverse-engineered blind.
## Console emulator = SheepShaver (2026-07-04)
The real ops console is a **Power Macintosh 6100/66** (PowerPC 601) →
emulate with **SheepShaver** (PPC Mac, Mac OS 7.5.29.0.4). Basilisk II
(68k) is out. GOTCHA: the 6100's OWN ROM does NOT work in SheepShaver
("Unsupported ROM type" — SheepShaver emulates a PCI 9500; the 6100 is
NuBus). Use a compatible old-world PPC ROM instead (7100/66, 7500, 7600,
or 8500) — PPC apps are Toolbox/OS-based, not ROM-specific, so the
410console app runs regardless. Target Mac OS 7.5.57.6.1 (console era).
Networking: SheepShaver TAP ↔ DOSBox-X NE2000 pcap, both bridged to a
host adapter on 200.0.0.0/24 (console .1, pod .113).
**Console 4.10 OS compatibility (from the binary, 2026-07-04):** classic
PowerPC PowerPlant/CodeWarrior app; PEF imports are only `InterfaceLib`,
`MathLib`, `ObjectSupportLib`. NO `AppearanceLib` (⇒ does NOT need Mac OS
8) and NO Open Transport libs (⇒ classic **MacTCP API**). Resource fork
has the startup check "TCP/IP not installed. Install either MacTCP or
OpenTransport…" via Gestalt/SysEnvirons — so it accepts either stack.
→ **Runs System 7.1.2Mac OS 9.0.4; practical floor 7.5** (ObjectSupportLib
ships with 7.5). **Recommended SheepShaver target: System 7.5.5 / 7.6.1**
(period-correct for a 6100, includes Open Transport 1.1.x so the TCP/IP
Gestalt check passes, most stable under SheepShaver w/ a 7100/7500/8500
old-world ROM). Set TCP/IP control panel to 200.0.0.1.
**Leverage the real 6100 (user has it):** (1) image its hard drive → get
the exact console software + OS + MacTCP/OpenTransport config → drop into
SheepShaver for a faithful reproducible console; (2) fastest path to a
first egg + LIVE protocol capture = put the real 6100 on a physical
Ethernet with the pcap-bridged pod (needs an AAUI→RJ45 transceiver) and
capture the console→pod egg exchange off the wire (hands us milestone 3).
Sequence: real 6100 first (seeds image + capture) → SheepShaver as the
archival console built from that image.
## Console software EXTRACTED + protocol port FOUND (2026-07-04)
The user unstuffed `410consoleArchive.sit` (via infinitemac.org) to
`4_10extractedConsole/`. Contents: **`Console 4.10`** (the app), per-site
config, fonts, logs. This means the dead 6100 is NOT a blocker — we have
the console software directly; run it in SheepShaver (no disk image
needed; fresh Mac OS 7.6.1/8.1 + MacTCP set to 200.0.0.1).
- **App is PowerPC** — data fork magic `Joy!peffpwpc` (PEF/PowerPC),
Metrowerks CodeWarrior 1993-95; 3.4MB resource fork. Confirms
SheepShaver (needs standard shared libs: InterfaceLib/MathLib, present
in any 7.5+ install).
- **`Console.ini`** is the master config: `[NetworkEndpoint::Cockpit::*]`
sections define every pod. **THE TCP PORT IS 1501** (`defaultPort` /
`localHostPort`) — this answers the milestone-3 "listen port unknown"
question. Console connects to each pod IP:1501; pod LISTENS on 1501.
Cockpit endpoint roster (base Console.ini) — our emulated pod = **"Puck"
200.0.0.113**:
| cockpit | IP | hostType |
|---|---|---|
| Frequent Flyer | 200.0.0.11 (sic) | 0 |
| Privateer | 200.0.0.112 | 0 |
| **Puck** | **200.0.0.113** | 0 |
| Carpe Diem | 200.0.0.114 | 0 |
| Man O' War | 200.0.0.115 | 0 |
| Divine Wind | 200.0.0.116 | 0 |
| Icarus | 200.0.0.117 | 0 |
| Gypsy | 200.0.0.118 | 0 |
| Alpha Mission Review | 200.0.0.119 | 2 |
| Alpha Camera | 200.0.0.120 | 2 |
hostType 0 = playable cockpit, 2 = special (mission-review / camera ship).
`ini Folder/` holds real per-venue configs (DBAtlanta/Chicago/Houston/
Toronto/LaZerPark/... — the actual VWE centers), same .11x/1501 scheme.
Revised topology: SheepShaver console @200.0.0.1 → TCP 200.0.0.113:1501 →
pod "Puck". Remaining protocol unknown is just the on-stream message
framing (NetworkPacketHeader + ReceiveEggFileMessage) — capture it once
the console connects, or read it from network.hpp.
## Egg-delivery protocol — decoded from source (2026-07-04)
Decoded from `CODE/RP/MUNGA/{NETWORK,RECEIVER,HOSTID}.HPP` +
`MUNGA_L4/L4NET.HPP`. The real send/receive *implementation* (framing on
the byte stream) did NOT survive in the archive — only headers + a test
harness (`L4NET.TCP`'s `TestClass`, `#if 0`). So the logical message
layout below is solid; two low-level details (stream framing + endianness)
need a live capture or a binary disasm to pin — see caveats.
**Transport:** console → TCP connect to pod IP : **1501** → the pod
(NetNub `TCP_LISTEN`) accepts. All base types are 32-bit
(`Enumeration=int`, `size_t`, `LWord`, `Time::ticks=long` → 4 bytes each).
**On-wire unit = NetworkPacket = NetworkPacketHeader + a Receiver::Message.**
NetworkPacketHeader (16 bytes):
| off | field | type |
|--|--|--|
| 0 | clientID | ClientID enum (0=NetworkMgr,2=HostMgr,5=Console...) |
| 4 | gameID | Enumeration |
| 8 | fromHost | HostID (Enumeration) |
| 12 | timeStamp | Time (long ticks) |
Receiver::Message header (12 bytes) that every message starts with:
| off | field | type |
|--|--|--|
| 0 | messageLength | size_t (= sizeof the whole message) |
| 4 | messageID | Enumeration (ReceiveEggFileMessageID etc.) |
| 8 | messageFlags | LWord (bit0 ReliableFlag=1) |
**ReceiveEggFileMessage** (the egg carrier; messageLength = 1024):
| off | field | type |
|--|--|--|
| 0 | (Receiver::Message header) | 12 B |
| 12 | sequenceNumber | int (chunk index) |
| 16 | notationFileLength | int (total egg size) |
| 20 | thisMessageLength | int (bytes valid in this chunk, ≤1000) |
| 24 | notationData[1000] | char (the egg chunk) |
So a full egg packet on the wire = **16 (header) + 1024 (message) = 1040
bytes** — matches the `nb≤1040` payload cap seen on the VPX/iserver link.
**Egg-delivery algorithm:** the console splits the mission egg (a text
"notation file", same INI/notation format as Console.ini) into
ceil(len/1000) chunks; sends each as a ReceiveEggFileMessage with
sequenceNumber 0..N, notationFileLength=total, thisMessageLength≤1000. The
pod's `ReceiveEggFileMessageHandler` reassembles into `eggTempBuffer` by
sequence, and when `notationFileLength` bytes have arrived, parses it as
the mission notation file. Pod replies `AcknowledgeEggFileMessage`
("connected, ready, next host"). `messageID` values start at
`NetworkClient::NextMessageID`; ReceiveEggFileMessageID is the first
NetworkManager message ID.
**Caveats to confirm with the first live capture (or a binary disasm of
Console 4.10 send / BTL4OPT receive):**
1. *Stream framing:* whether each 1040-B NetworkPacket is one discrete
NetNub/WATTCP record, or the receiver frames within the TCP stream via
the leading `messageLength`. (NetNub `RECEIVE_PACKET` returns up to
1600 B; MAX aligns with one packet.)
2. *Endianness:* console is **big-endian PPC**, pod is **little-endian
x86** — the multi-byte header/length ints must be byte-swapped by one
side (or sent in network order). `notationData` (egg text) is
endian-agnostic. The capture will show which order the length fields
use; a stand-in sender must match it.
Everything needed to PARSE a capture and BUILD a stand-in egg-sender is
here except those two bytes-on-the-wire details, which the console-connect
milestone resolves immediately.
## Console VM standup (user, 2026-07-04): SheepShaver + OS 9.0.4 + old-world ROM
Valid combo (old-world ROMs support the full 7.5.29.0.4 range); OS 9's
Open Transport provides the MacTCP-API compat the app's Gestalt check
wants, so Console 4.10 runs. Networking initially set to "Basilisk II
Router" — NOTE that's **NAT** (assigns a 10.x IP, routes to host), fine
for building/updating the VM but it CANNOT reach the pod at 200.0.0.113.
For the console↔pod link both must be **bridged (TAP)** on one L2 segment:
console SheepShaver TCP/IP = manual 200.0.0.1 / 255.255.255.0 on a TAP;
pod DOSBox-X moves off `backend=slirp` to **`backend=pcap`** bound to the
same TAP/bridge (200.0.0.113 already via WATTCP). Then the console's
outbound TCP to 200.0.0.113:1501 reaches the pod and we capture the egg
exchange (resolves the framing + endianness caveats above).
## MILESTONE: real console <-> real pod talking over the wire (2026-07-05)
The emulated SheepShaver console and the emulated DOSBox pod now exchange
the live console protocol on TCP 1501. Hard-won setup (all required):
1. **DOSBox-X pcap backend had to be rebuilt + npcap DLL path.** config.h
had `C_PCAP 1` but the stale `ethernet.o` predated it; force-recompile
(`rm src/misc/ethernet.o ethernet_pcap.o; make`). Runtime: npcap installs
its DLLs in `C:\Windows\System32\Npcap\` (npcap-only mode), NOT System32,
so DOSBox couldn't load wpcap.dll -> **launch dosbox-x.exe with
`C:\Windows\System32\Npcap` prepended to PATH.**
2. **Two-TAP Windows bridge** (single shared TAP fails: SheepShaver holds the
user-mode handle, DOSBox rides NDIS, frames don't cross). Console
SheepShaver -> TAP1 (`ether tap` + etherguid); pod -> TAP2; both bridged.
3. **TAP2 media status = Always-Connected** (registry MediaStatus=1 on the
TAP2 class key, needs elevation; the GUI toggle didn't persist). Without
it TAP2 reports "unplugged" and the bridge won't forward to it.
4. **Pod pcap binds to the BRIDGE MINIPORT, not a member TAP.** Injecting on
a bridge *member* (TAP2) isn't re-forwarded by the bridge (the pod TX'd
but the console never saw it). Bind DOSBox `realnic` to the bridge
miniport = "Microsoft Network Adapter Multiplexor Driver". GOTCHA: DOSBox
`realnic` matches a substring of the pcap device NAME (`\Device\NPF_{GUID}`)
and parses a *leading-digit* value as an interface index -- the bridge
GUID `5DB5521D...` starts with 5 -> picked iface #5 (Bluetooth!). Use a
letter-leading fragment: `realnic=DB5521D`.
5. Diagnostics: added `PCAP TX/RX` counters to `ethernet_pcap.cpp`
(SendPacket/GetPackets) -- confirmed the pod WAS transmitting all along;
the bridge forwarding was the only gap. Capture the wire with
`dumpcap -i <bridge#> -f "arp or tcp port 1501" -w cap.pcapng`.
**Verified protocol on the wire (little-endian, matches the source decode):**
- Console->pod **StateQuery** (32B): hdr clientID=4(App) gameID=0 fromHost=1
ts=.. ; msg len=16 id=3 flags=0 ; body requestingHost=1.
- Pod->console **StateResponse** (40B): hdr **clientID=5 (ConsoleClientID)**
gameID=0 fromHost=0 ts=.. ; msg len=24 id=1 **flags=1 (Reliable)** ; body
respondingHostID=0, **applicationState=1**, application=1 (BTL4/BattleTech).
(My earlier synthetic guess had clientID=4/flags=0/host=1/state=0 -- all
wrong, which is why the real console rejected the stand-in. These are the
correct values for pod_responder.py.)
- Console polls StateQuery ~every 2s; pod ACKs + StateResponse each time.
Clean 3-way handshake; zero-length "dup ACKs" are benign keepalives.
OPEN: with this working, the console still hadn't enabled mission-send in
the stripped-down (keyboard-only, no-RIO, no-sound) pod boot -- retrying
with a full RIO+sound boot (`net_full.conf`) so the pod presents its normal
state (applicationState may change when fully booted). Need the console UI
readout to know what state it wants before it will send the mission egg.
## Open questions / notes
- Exact TCP listen port(s) — not in the source grep; get from NETNUB.EXE
or a capture at milestone 3.
- Does WATTCP need a real ARP peer for the gateway at boot, or does it
proceed with a static IP and only ARP on connect? Affects whether the
stand-in console must answer ARP for 200.0.0.1.
- `NETCLIENT=PNW` (PARAMETR.bat) selects Personal NetWare — file-server
side, not the game's TCP path; likely irrelevant to egg delivery and
can stay unloaded under emulation.
- RP uses the identical MUNGA net brick + its own WATTCP.CFG — everything
here carries over to Red Planet.