Repo docs

Per-repo documentation — each repo's docs/ and README, ingested and associated with the repo. Rendered through the <<<RepoDocs>>> tag.

← netboot docs · README.md

netboot — Raspberry Pi 5 netboot demo

A stand-alone demo that turns the Mac into a Raspberry Pi 5 (naszilla) network-boot server and proves, from a packet capture, whether the Pi truly netbooted.

It runs six stages end-to-end and ends with a one-word verdict:

pin  ->  serve  ->  capture  ->  boot  ->  decode  ->  verdict

| Stage | What it does | |---------|--------------| | pin | Give the Mac's USB-ethernet port en8 the fixed address 10.10.0.10/24 — its end of the direct cable to the Pi. | | serve | Start dnsmasq on en8 (DHCP :67 + TFTP :69) and prove both are bound (not merely "running"). | | capture | Run tcpdump on en8 into a timestamped pcap — the evidence. | | boot | Reboot the Pi over SSH and watch her down→up cycle. (Skipped in dry-run.) | | decode | Parse the pcap once, via scapy, into {discovers, offers, acks, tftp_rrqs}. | | verdict | From that one decode: NETBOOT / LEASE_ONLY / NO_OFFER. |

Topology (the proven path)

  Mac ──en8 (USB-ethernet, Realtek RTL8153)── direct cable ── Pi 5 (naszilla) eth0
        10.10.0.10/24                                          DHCPs from .50-.150
        dnsmasq: DHCP + TFTP, tcpdump capturing

An isolated point-to-point segment. The Pi's bootloader DHCPs (before Linux), gets an OFFER, then pulls her boot files over TFTP from /Users/steve/pi-netboot/tftp (serial dir 18cc271b/, already staged with all 56 boot files). The capture is the proof.

Get started

cd ~/code/netboot

./netboot.sh build       # check prerequisites + assets (no network changes)
./netboot.sh dry-run     # full cycle on the EMPTY wire, no Pi reboot -> expect NO_OFFER
./netboot.sh live-test   # full cycle WITH a Pi reboot -> expect NETBOOT
./netboot.sh stop        # kill dnsmasq + tcpdump, release the en8 pin

The pin / serve / capture stages need sudo (set a manual IP, bind ports, sniff the wire). Run from a terminal that can authenticate; macOS will prompt.

One-liner to begin

./netboot.sh build && ./netboot.sh dry-run

What to expect

- ./netboot.sh build — all-green prerequisite report (python>=3.11, dnsmasq, tcpdump, uv for scapy, en8 present, TFTP serial dir staged), ending in BUILD OK. Changes nothing on the wire.

- ./netboot.sh dry-run (Pi OFF or uncabled) — pins en8, starts the server, captures ~60s of an empty wire, skips the reboot, decodes, and prints:

  ==> VERDICT: NO_OFFER — no DHCP traffic on the wire (empty wire / Pi off / no boot attempt)

This validates the whole plumbing without the Pi. NO_OFFER is the correct dry-run result.

- ./netboot.sh live-test (Pi ON + en8 cabled) — same, but reboots the Pi over SSH and watches her cycle. Expected:

  ==> VERDICT: NETBOOT — leased an address AND pulled boot files over TFTP

- LEASE_ONLY = she got an address but no TFTP → fix the /<serial>/ path. - NO_OFFER with DISCOVERs > 0 = she asked but the server didn't answer →

`en8` subnet/binding (the failure mode of the last live run: 64 DISCOVERs / 0 OFFERs).

Re-check an old capture

./netboot.sh verdict /Users/steve/pi-netboot/netboot-YYYYmmdd-HHMMSS.pcap

Decode + verdict are pure functions of the pcap, so any capture can be re-judged offline. (Verified against the prior netboot-20260628-225119.pcap: 315 packets, 64 DISCOVERs, 0 OFFERs → NO_OFFER.)

Output

- Trace log /Users/steve/pi-netboot/netboot.trace — one tab-separated line per stage, greppable, no emoji:

  2026-06-29T14:23:45Z  pin       done    en8=10.10.0.10/255.255.255.0 link=up
  2026-06-29T14:23:46Z  serve     done    dnsmasq SERVING DHCP:67=1 TFTP:69=1 ...
  2026-06-29T14:23:47Z  capture   done    pcap=netboot-20260629-142347.pcap
  2026-06-29T14:23:50Z  boot      skipped dry-run: idle capture 60s (Pi not rebooted)
  2026-06-29T14:24:50Z  decode    done    pkts=0 discovers=0 offers=0 acks=0 tftp_rrqs=0
  2026-06-29T14:24:50Z  verdict   done    NO_OFFER (...)

- pcap in /Users/steve/pi-netboot/ (latest path also in .last-pcap). - Telemetry into ~/code/forge/telemetry.db (component netboot) — fire-and-forget, off if jazz_telemetry is unavailable.

Manual server mode

To power/cable the Pi by hand between stages:

./netboot.sh serve     # pin + serve + capture, then leaves them running
#  ... power on + cable the Pi, let her try to boot ...
./netboot.sh stop      # tear down
./netboot.sh verdict /Users/steve/pi-netboot/netboot-<stamp>.pcap

Files

netboot/
├── netboot.py        driver — six stages, centralized decode, structured trace
├── __main__.py       CLI (run/serve/verdict/decode/stop/status; --dry/--step/--watch)
├── __init__.py
├── netboot.toml      all addresses, paths, Pi SSH contact, telemetry
├── netboot.sh        walkthrough CLI: build / dry-run / live-test / serve / stop / verdict
├── bin/netboot       thin wrapper (run `netboot` from anywhere)
└── README.md         this file

vm/netboot/           EXPERIMENTAL alternate topology (server inside a VM) — see below
├── netboot.xml       libvirt domain: SLIRP eth0 (ssh :2223) + USB-NIC passthrough eth1
├── user-data         cloud-init (uid 501 for 9p RW, scapy/dnsmasq/tcpdump, eth1=10.10.0.10)
├── meta-data
├── netboot_ed25519   SSH keypair for the VM
├── netboot_ed25519.pub
└── build-vm.sh       builds seed.iso + disk, then virsh define/start

Direct CLI (no wrapper)

python3 -m netboot run --dry --watch     # dry-run with a live trace tail
python3 -m netboot run --step            # pause after each stage
python3 -m netboot serve                 # server up, left running
python3 -m netboot verdict <pcap>        # re-judge a capture
python3 -m netboot status                # interface + process + trace state
python3 -m netboot stop                  # tear down

Prerequisites

- macOS (Apple Silicon), dnsmasq (Homebrew), tcpdump, python3 >= 3.11. - uv (the decode runs uv run --with scapy, so no global scapy install is needed). Without uv, install scapy into the calling python and clear decode.scapy_runner in netboot.toml. - The Realtek RTL8153 USB-ethernet adapter on en8, cabled to the Pi. - TFTP assets already staged at /Users/steve/pi-netboot/tftp/18cc271b/. - SSH to the Pi via the naszilla alias (~/.ssh/config10.0.0.156, key pi-fleet). Only used by live-test to trigger the reboot; skipped gracefully if unreachable.

The experimental VM path

The design also called for an isolated topology where the server runs inside a libvirt VM with the USB-NIC passed through to the guest (the Mac goes blind on en8; the guest owns the wire). It's fully manifested in vm/netboot/:

vm/netboot/build-vm.sh                                   # build + start the VM
ssh -i vm/netboot/netboot_ed25519 -p 2223 [email protected]
#  in the guest:
ip -br link                                              # expect eth1 (the RTL8153)
cd /Users/steve/code/netboot && python3 -m netboot run --dry

Caveat: USB passthrough on macOS HVF is fragile. If eth1 doesn't appear in the guest, passthrough failed — fall back to the host path (./netboot.sh), which is the proven one. The VM path is provided for completeness, not relied upon.

Safety / cleanup

pin sets a manual IP on en8; stop (and ./netboot.sh stop) releases it back to DHCP and kills both dnsmasq and tcpdump. While the demo runs, the Mac uses en8 as the Pi's isolated boot segment — it is not your normal LAN uplink.

Do not revert the Pi's EEPROMnaszilla stays at BOOT_ORDER=0xf612 (net → SD → NVMe). This demo serves that net-boot attempt; it does not change Pi firmware.