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.
This commit is contained in:
cproudlock
2026-10-15 14:26:39 -04:00
parent 53d39bfc50
commit 21ff95af78
13 changed files with 1022 additions and 36 deletions

View File

@@ -12,6 +12,15 @@ Synthesized LLDP, CDP, EAPOL, STP and ARP frames and DHCPOFFER/ACK packets
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
@@ -38,6 +47,7 @@ namespace cannot drop privileges; scenarios are skipped without it).
| 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 |
@@ -46,5 +56,9 @@ namespace cannot drop privileges; scenarios are skipped without it).
| 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.