diff --git a/emulator/DEPLOYMENT-PLAN.md b/emulator/DEPLOYMENT-PLAN.md index 44fffb1..2bf95f3 100644 --- a/emulator/DEPLOYMENT-PLAN.md +++ b/emulator/DEPLOYMENT-PLAN.md @@ -88,6 +88,38 @@ segment the console is a separate machine and hears every pod over the wire. Also gone: the machine-specific `realnic=DB5521D` GUID (postinstall binds the one NIC) and the `200.0.0.x` hardcoding (postinstall stamps the assigned IPs). +**Co-located smoke-test caveats (all hit 2026-07-10, first dist smoke test; +end-to-end egg->mission achieved same night):** running TeslaConsole on the +pod PC itself brings dev-era requirements back. On the real segment, with the +console on its own machine, none of these apply: + +1. Npcap `SendToRxAdapters` must list the pod NIC's `\Device\{GUID}` (restart + the npcap service after setting it -- the stop fails silently while DOSBox + holds a capture). +2. The host must NOT hold the game IP. A leftover static at bayIP+100 (the + old bridge address) makes the console dial itself -- source = destination, + the SYN never reaches the wire, `SynSent` forever. +3. **Disable NIC checksum offload (+ LSO) on the pod NIC** + (`Disable-NetAdapterChecksumOffload`/`-NetAdapterLso`). The host's OWN + outbound frames are captured BEFORE the hardware fills in IP/TCP checksums; + WATTCP silently discards every bad-checksum IP packet while still answering + checksum-less ARP -- so ARP resolves, the NIC accepts the frames, and the + pod stays dead silent at the TCP layer (netnub prints "discarding..."). + This was the final console-connect blocker. Frames arriving over the real + wire always carry completed checksums, so separate-machine deployments are + immune. + +Also learned same night, not co-location-specific: **bt/rp REQUIRE sound** +(`--no-sound` strips the AWE32; the FAST SOS clock never ticks and BTL4OPT +freezes in the RIO-reset busy-wait), and **the pod requires a packaged +renderer** (pod-launch refuses to start without one unless `--no-bridge`; +the smoke test ran with the dev tree's render bridge via `--root`) -- closes +the "freeze renderer vs bundle Python" OPEN item in favor of: MUST ship one. +Related guard: `configure.ps1` refuses to guess when several candidate bay +IPs survive its physical-adapter filter (`-BayIp x.x.x.x` disambiguates, and +also allows deliberately binding a virtual adapter on a dev rig); +`-ConsoleIp` sets the WATTCP gateway/nameserver (default: the bay IP). + ## The archive (self-contained -- air-gap forbids any download) Single root folder + `postinstall.bat`. Everything the install needs is inside: diff --git a/emulator/deploy/configure.ps1 b/emulator/deploy/configure.ps1 index 40a296d..a5ce6bc 100644 --- a/emulator/deploy/configure.ps1 +++ b/emulator/deploy/configure.ps1 @@ -3,9 +3,13 @@ # DOSBox game IP = bayIP + 100, binds DOSBox's pcap to the NIC, renders the # net_*.conf templates, and stamps the WATTCP.CFG my_ip. Air-gapped, static, # <=32 pods. See ../DEPLOYMENT-PLAN.md. -param([Parameter(Mandatory = $true)][string]$Root) +param([Parameter(Mandatory = $true)][string]$Root, + [string]$BayIp, # explicit bay IP; bypasses the physical-adapter filter + [string]$ConsoleIp) # TeslaConsole's IP (default: bay IP, i.e. co-located) $ErrorActionPreference = 'Stop' function Log($m) { Write-Host "[configure] $m" } +if (-not (Test-Path -LiteralPath $Root -PathType Container)) { throw "-Root '$Root' is not a directory (typo?)" } +$Root = (Resolve-Path -LiteralPath $Root).Path # --- prefix length -> dotted netmask ------------------------------------- function PrefixToMask([int]$p) { @@ -15,14 +19,31 @@ function PrefixToMask([int]$p) { } # --- 1. the pod's NIC + bay IP (the one real static IPv4) ---------------- -$ip = Get-NetIPAddress -AddressFamily IPv4 | +# A bay has exactly ONE physical NIC up with ONE static IPv4. Virtual adapters +# (Network Bridge multiplexor, TAPs -- dev leftovers) are excluded; if several +# candidates still survive, fail loudly instead of coin-tossing (a multi-homed +# host once got lucky here). -BayIp forces the choice -- and skips the +# physical filter, so a dev rig can deliberately bind a bridge. +$cands = @(Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' } | - Sort-Object -Property SkipAsSource, InterfaceMetric | - Select-Object -First 1 -if (-not $ip) { throw "no usable static IPv4 found on any adapter" } + ForEach-Object { + $ad = Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue + if ($ad) { [pscustomobject]@{ Ip = $_; Adapter = $ad } } + }) +if ($BayIp) { + $cands = @($cands | Where-Object { $_.Ip.IPAddress -eq $BayIp }) + if ($cands.Count -eq 0) { throw "-BayIp $BayIp is not configured on any adapter" } +} else { + $cands = @($cands | Where-Object { -not $_.Adapter.Virtual -and $_.Adapter.Status -eq 'Up' }) + if ($cands.Count -eq 0) { throw "no IPv4 on an Up physical adapter; rerun with -BayIp x.x.x.x" } +} +if ($cands.Count -gt 1) { + $list = ($cands | ForEach-Object { "$($_.Ip.IPAddress) on '$($_.Adapter.Name)'" }) -join ', ' + throw "ambiguous bay IP -- $($cands.Count) candidates ($list); rerun with -BayIp x.x.x.x" +} +$ip = $cands[0].Ip $bayIp = $ip.IPAddress -$ifIndex = $ip.InterfaceIndex -$adapter = Get-NetAdapter -InterfaceIndex $ifIndex +$adapter = $cands[0].Adapter $guid = $adapter.InterfaceGuid.Trim('{', '}') $mask = PrefixToMask $ip.PrefixLength Log "bay IP $bayIp/$($ip.PrefixLength) on '$($adapter.Name)' guid $guid" @@ -50,13 +71,15 @@ Log "game IP = $gameIp (bay + 100)" $mac = "02:00:{0:X2}:{1:X2}:{2:X2}:{3:X2}" -f [int]$o[0], [int]$o[1], [int]$o[2], $last Log "macaddr = $mac" -# --- 5. gateway/nameserver: the host's real gateway if set, else the host -# itself (always up + on-subnet -> WATTCP's boot ARP resolves; never -# actually routed, the mesh is same-subnet L2) ----------------------- -$gw = $null -try { $gw = (Get-NetIPConfiguration -InterfaceIndex $ifIndex).IPv4DefaultGateway.NextHop } catch { } +# --- 5. gateway/nameserver: needs only to be a LIVE on-subnet host (WATTCP +# boot-ARPs it; nothing is ever routed -- the mesh is same-subnet L2, +# and the CONSOLE dials the pod, not vice versa). The console PC's IP +# is the natural pick: always up whenever a mission can start. Default +# = this machine's bay IP (co-located console / smoke test); real +# deployments pass -ConsoleIp . --------- +$gw = $ConsoleIp if (-not $gw) { $gw = $bayIp } -Log "gateway/nameserver = $gw" +Log "gateway/nameserver (console PC) = $gw" # --- 6. render the conf templates (@@ROOT@@/@@REALNIC@@/@@MACADDR@@) ------ $tokens = @{ '@@ROOT@@' = $Root; '@@REALNIC@@' = $realnic; '@@MACADDR@@' = $mac }