# BOOTP responder for the VT1200 (`harness/serve.py --bootp`) Date: 2026-10-16. Scope: `harness/serve.py` only (a new optional responder). `harness/mame/vt1200.cpp` was read, not touched; the ROM is byte-identical (`md5 b46a3e442ccb86134d2356bd70d22fd2`). ## What was added `serve.py` can now answer the BOOTP request the terminal broadcasts at power-up, so its IP address no longer has to be typed in at the Terminal Manager. sudo python3 -u harness/serve.py 192.168.99.1 --bootp `--bootp` is **off by default**. A bare `serve.py 192.168.99.1` behaves exactly as before: TFTP font serving on port 69 and nothing else (no port 67 socket, no AF_PACKET socket, no banner). Extra options: `--bootp-client` (default `192.168.99.2`), `--bootp-server` (default the TFTP host), `--bootp-iface` (default: auto-detect `tap-mess-*`). ## The request, as the terminal actually sends it Captured on the tap (`tcpdump -i tap-mess-1000-0 'udp port 67'`) while MAME ran `run.sh net` headless. It is a plain RFC 951 request with **no** DHCP extensions: ``` op=1 htype=1 hlen=6 hops=0 xid=0xf81fccff secs=0,1024,2048,... flags=0x0000 ciaddr=0 yiaddr=0 siaddr=0 giaddr=0 chaddr = 08:00:2b:16:4b:5a:c1:86 00 00 00 00 00 00 00 00 (8 bytes!) sname = (empty) file = (empty) vend = 64 zero bytes (no 0x63825363 magic cookie) ``` Two things worth noting. The request is sent from source `0.0.0.0`, UDP port 68 to `255.255.255.255:67`, and `chaddr` carries **eight** bytes: the six-byte MAC plus the two checksum bytes the firmware read from its Ethernet address ROM. The `htype`/`hlen` (1/6) say six bytes are meaningful. **It asked for no boot file.** `sname` and `file` are empty, so the reply leaves them empty too. No filename was invented. ## The reply `op=2`, the `htype`/`hlen`/`xid`/`secs`/`flags`/`chaddr` echoed, `yiaddr` = the client address, `siaddr` = the server address, `ciaddr` and `giaddr` zero, `sname`/`file` empty, 300 bytes total (236-byte header + zero vend). ## Why it is an L2 responder (AF_PACKET), not a plain UDP socket The host **IP stack never receives the request**. The terminal sends it from `0.0.0.0` into the MAME tap, which is enslaved to the `vt1200` bridge. With a UDP socket bound to `0.0.0.0:67` on the host, no datagram arrives (verified: a listener saw zero, and `net.ipv4.conf.vt1200.rp_filter=0` made no difference). The frame is, however, visible at the tap's L2 — which is where `tcpdump` sees it — and an `AF_PACKET` socket bound to `tap-mess-1000-0` does receive it. The responder therefore reads the tap at L2 and writes the reply frame back. This is a *delivery* choice; the packet is still an RFC 951 reply. (`ETH_P_ALL` is used because `ETH_P_IP` misses the bridged-in frames.) The reply is a broadcast: `ff:ff:ff:ff:ff:ff`, source MAC = the bridge's MAC (`c6:fc:cc:16:f5:66`), IP `192.168.99.1` → `255.255.255.255`, UDP 67 → 68. ## Evidence the terminal adopted `192.168.99.2` Server log (`--bootp`): ``` bootp: L2 responder on tap-mess-1000-0: handing out 192.168.99.2, siaddr 192.168.99.1 (src mac c6:fc:cc:16:f5:66) bootp: request from 08:00:2b:16:4b:5a xid=f81fccff -> 192.168.99.2 ``` `tcpdump` on the tap (abridged): ``` 08:00:2b:16:4b:5a > ff:ff:ff:ff:ff:ff 255.255.255.255.68 > 255.255.255.255.67 BOOTP Request c6:fc:cc:16:f5:66 > ff:ff:ff:ff:ff:ff 192.168.99.1.67 > 255.255.255.255.68 BOOTP Reply Your-IP 192.168.99.2 08:00:2b:16:4b:5a > ff:ff:ff:ff:ff:ff 192.168.99.2.1024 > 192.168.99.255.771 UDP, payload "RCP\0DEC VT1000\0..." ``` The **only one** request (previously five retries over ~6 s) is itself the reply being accepted; and the next frame is sourced from `192.168.99.2`. The terminal's own screen (frame dump, `VT1200_DUMP_FRAMES`), Terminal Manager: ``` User settings invalid. Using local fonts. System defaults loaded. IP address set to 192.168.99.2 IP subnet mask set to 255.255.255.0. TCP/IP is now up. Ethernet Address 08-00-2B-16-4B-5A IP Address 192.168.99.2 ``` ## The two live symptoms 1. **"User settings invalid." / NVRAM does not persist — STILL PRESENT, not made moot by BOOTP.** BOOTP fixes only the address: the terminal now prints `IP address set to 192.168.99.2 ... TCP/IP is now up` instead of `Could not determine IP address after 5 retries / TCP/IP is now down`. The `User settings invalid. / System defaults loaded.` banner remains, and the emulated NVRAM file (`~/.mame/nvram/vt1200/nvram`) is written back all zeroes — a storage problem, and the driver holding it is out of scope here. 2. **Font fetch aborting with TFTP error 3 ("Disk full or allocation exceeded") — NOT REPRODUCED on this boot, and unrelated to BOOTP.** With settings invalid the terminal prints `Using local fonts.` and never configures a TFTP font path, so it issues **no** RRQ at all in a fresh run; the server log shows no TFTP request. The abort is a **client-side** condition (the terminal itself replies `ERROR ENOSPACE`), which a server-side change cannot cause or cure — as the task preamble anticipated. Reproducing it needs a configured TFTP font path, which needs the settings NVRAM to persist first. ## Default behaviour A bare `serve.py 192.168.99.1` was run and checked: it prints only the TFTP banner, listens on `192.168.99.1:69`, and opens no port-67 / AF_PACKET socket. Unchanged.