This is the heavier virtual lab: two separate Debian VMs (their own kernels)
joined by one bare QEMU socket cable, driven from two terminals side by
side so you can watch a frame leave one machine and arrive on the other. It
sits alongside the lighter ../virtual/ namespace lab.
Use the namespace lab for an instant, zero-boot look at ARP. Use this one when you want two genuinely separate machines and — the payoff the namespace lab can't give you — a Layer 1 carrier you can seat and unseat by hand.
| Physical bench | This lab |
|---|---|
| a Raspberry Pi | a QEMU process (its own Linux kernel) |
the eth0 Ethernet port |
a virtio NIC renamed eth0 |
| the Ethernet cable | a QEMU socket netdev — pure L2, nothing in between |
wlan0 Wi-Fi you SSH over |
a per-VM user-mode NAT (ssh-a / ssh-b) |
| the image's Wi-Fi isolation | automatic — the two NATs can't reach each other |
| seating / pulling the cable | ./link.sh a on / off (real carrier change) |
The two nodes can reach each other only over the cable, exactly as the
hardware image enforces — so anything you see on eth0 really crossed the wire.
Runs on macOS and Linux, and on Windows via WSL2 (which is Linux). The lab auto-detects your CPU and picks the matching QEMU binary and Debian image (Apple Silicon / arm64 or Intel / amd64).
- QEMU, plus a tool to build the seed ISO:
- macOS:
brew install qemu(uses HVF, no sudo;hdiutilis built in). - Debian/Ubuntu:
sudo apt-get install qemu-system xorriso(uses KVM; you need access to/dev/kvm, e.g. add yourself to thekvmgroup). - Windows: do everything inside WSL2 (Ubuntu), then follow the Linux steps.
- macOS:
python3andcurl(built into macOS; preinstalled on most Linux).- About 2GB RAM free and 2GB disk. First boot needs internet (downloads the image
and installs
tcpdumpin the guests).
./lab-up.sh # downloads Debian once, boots pi-a + pi-b, waits for SSHIt prints the two SSH commands when the nodes are ready. Everything it creates
lives in ~/.little-internet/lab00-vm/, not in the repo.
Open two panes. Left is pi-a, right is pi-b.
# LEFT pane # RIGHT pane
./ssh-a ./ssh-bBeat 1 — is there a wire? On pi-a: ip link show eth0 (expect LOWER_UP —
the cable is seated). From a third pane on the Mac, ./link.sh a off, then look
again: NO-CARRIER. Bring it back with ./link.sh a on.
Beat 3 — the wire has no identity. On pi-a:
ip -4 addr show eth0 # no inet — blank
ping -c1 -W1 10.10.0.2 # leaks out the mgmt NAT, 0 received (like wlan0 on a Pi)Beat 4 — ARP makes the introduction. Leave a capture running on the right, then address both ends and ping from the left:
# RIGHT pane (pi-b): leave this running
sudo tcpdump -i eth0 -n -e
# LEFT pane (pi-a):
sudo ip addr add 10.10.0.1/24 dev eth0
# and on pi-b: sudo ip addr add 10.10.0.2/24 dev eth0
ping -c2 10.10.0.2On the right you'll watch the exchange arrive, frame by frame:
ARP, Request who-has 10.10.0.2 tell 10.10.0.1
ARP, Reply 10.10.0.2 is-at b8:27:eb:00:00:02
IP 10.10.0.1 > 10.10.0.2: ICMP echo request
IP 10.10.0.2 > 10.10.0.1: ICMP echo reply
Note seq 2 is faster than seq 1 — that's the ARP cache. Check it with
ip neigh show dev eth0 on pi-a (expect 10.10.0.2 ... REACHABLE), and note the
b8:27:eb Pi vendor prefix, reproduced here on purpose.
One command stands up the lab (if it isn't already) and opens a live black-and-white view of both nodes in your browser:
./dashboard.sh # serves http://127.0.0.1:8099 and opens it- pi-a and pi-b side by side: link state (
LINK UP/NO CARRIER), theeth0address, and the ARP cache, with each neighbor's state color-coded (REACHABLEgreen,STALE/DELAYamber,FAILEDred). - Each node's serial console, following the newest lines.
- The wire: every frame crossing
eth0, streamed viatsharkand color-coded (ARP amber, ICMP request green, ICMP reply cyan).
Panels refresh once a second with sticky auto-scroll: pinned to the newest
line, but you can scroll up to read without it snapping back. It pairs well
with the runbook above: run the beats in the two SSH panes and watch the
dashboard react, or ./link.sh a off and watch pi-a flip to NO CARRIER.
Ctrl-C stops the dashboard; the lab keeps running. dashboard.py is Python
stdlib only (nothing to install), and the wire view uses the tshark that
lab-up.sh puts on the nodes. Set DASH_PORT to serve on a different port.
sudo ip addr flush dev eth0
sudo ip neigh flush dev eth0./lab-down.sh # stop both VMs (keeps disks, so next boot is fast)
./lab-down.sh --wipe # also delete the node disks for a factory-fresh bootTwo kernels and a real carrier close most of the gap, but a virtio NIC has no PHY, so link speed / duplex / autonegotiation aren't real (only the carrier up/down event is). And short frames likely aren't padded to Ethernet's 60-byte minimum, so the diary's 42-vs-60-byte "did I send or receive this?" tell may not appear. Layer 1's physical texture is still the part you only fully feel on metal.