Files
pxe-server/tests/pxe-nic-check
cproudlock 21ff95af78 NIC guard: ignore client EAPOL, re-check on carrier change, SMB-only lease cleanup
- pxe-nic-check: count 802.1X only from an authenticator (EAPOL type 0
  with EAP Request/Success/Failure). iPXE sends EAPOL-Start on every NIC
  open and Windows sends Start/Response, so a PXE-booting bay made the
  verdict 'ge' and the watch stopped dnsmasq for the floor.
- pxe-nic-check: join the STP/PAE/LLDP/CDP/PVST+ multicast groups on
  every capture socket, so CDP-only ports are heard on a plain NIC.
- pxe-nic-check --watch: if dnsmasq was (re)started inside the window,
  re-check (~6 s) before stopping it; skip when already stopped.
  New --state-cmd and --print-imaging.
- pxe-nic-carrier (+ pxe-nic-carrier.service): networkd-dispatcher hook
  for the imaging NIC only. Carrier loss stops dnsmasq; carrier gain
  restarts it through its guard if it was running or the hook stopped it.
- pxe-dhcp-hook.sh: clear only SMB (tcp 445/139) conntrack entries and
  sockets, not all traffic for the client, which raced iPXE's first
  HTTP fetch.
- GetPxeScript.aspx: every boot line has a failure branch (iPXE 2.0
  accepts an unsigned EFI kernel and fails at boot under Secure Boot,
  which aborted the script).
- Tests: client EAPOL, packet_mreq packing, watch race, shell helpers on
  stub PATH, lab scenarios eapol-client, br-eapol-client, race, carrier.
2026-10-15 14:26:39 -04:00
..

pxe-nic-check tests

Tests for playbook/pxe-server-helpers/pxe-nic-check. Neither needs root or touches a real network.

Unit tests

python3 -m unittest discover -s tests/pxe-nic-check -v

Synthesized LLDP, CDP, EAPOL, STP and ARP frames and DHCPOFFER/ACK packets (frames.py) fed to the parsers; the verdict table; own-server and cloned-PXE-server handling; networkd lease parsing; config fallbacks; the dnsmasq_bound logic; the status JSON shape and atomic 0644 write.

Shell helpers

test_helpers.py runs pxe-dhcp-hook.sh (add/del/old with PXEClient, MSFT 5.0 and no vendor class: only SMB 445/139 conntrack entries and sockets are cleared) and pxe-nic-carrier (loss stops dnsmasq, gain hands off once, GE NIC events touch nothing, bridge ports count) with stub ss, conntrack, logger, systemctl and pxe-nic-check on a fake PATH and a fake /sys/class/net. Part of the unit run above.

Namespace lab

python3 tests/pxe-nic-check/lab.py              # all scenarios, ~5 min
python3 tests/pxe-nic-check/lab.py rogue br-lldp   # some

Re-runs itself under unshare --user --map-root-user --net --mount: a private network namespace with veth pairs, so raw sockets work without sudo and every frame stays inside it. Each scenario runs the real script in check (--json --probe), --guard and --watch modes (watch with --stop-cmd "touch <marker>" instead of systemctl stop dnsmasq) and asserts the imaging verdict, the guard exit code, whether watch "stopped dnsmasq", the evidence text and the status JSON shape.

Peers: lab_peer.py (a raw-socket DHCP responder and LLDP/EAPOL sender; refuses to run unless PXE_NIC_LAB=1, which only the lab sets) and real dnsmasq if installed (started with --user= --group=, since a user namespace cannot drop privileges; scenarios are skipped without it).

Scenario Peer Expect Guard
isolated none isolated 0
rogue DHCP, no domain foreign-dhcp 1
fake-ge DHCP domain test.geaerospace.net + LLDP ge 1
not-corp-domain DHCP domain notgeaerospace.net foreign-dhcp 1
lldp-only LLDP ge 1
eapol-only 802.1X EAPOL ge 1
eapol-client EAPOL-Start + EAP-Response, like a PXE-booting bay isolated 0
own-server DHCP with server id 172.16.9.1 from a local MAC isolated 0
cloned-pxe DHCP with server id 172.16.9.1, foreign MAC foreign-dhcp 1
no-link peer end down no-link 0
dnsmasq-corp real dnsmasq, domain lab.geaerospace.net ge 1
dnsmasq-rogue real dnsmasq, no domain foreign-dhcp 1
own-dnsmasq real dnsmasq serving the imaging NIC itself isolated, dnsmasq_bound true 0
br-* same, imaging NIC is a bridge (like br-pxe), named in nic.conf by a port's MAC

rogue, lldp-only and br-lldp also run --watch as if dnsmasq had been restarted inside the window: it must log the re-check and still stop. Every watch gets --state-cmd (a printf standing in for systemctl show).

The GE NIC (g0) is checked from a fake networkd lease with domain test.geaerospace.net and must read ge in every scenario.