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

@@ -10,6 +10,8 @@ plays a switch and/or a DHCP server with raw AF_PACKET sockets:
--domain NAME option 15 in the offer
--lldp SYSNAME send an LLDP frame every second
--eapol send an EAPOL Request/Identity every second
--eapol-start send EAPOL-Start + EAP-Response like a PXE-booting
client (iPXE, Windows dot3svc) every second
--mac MAC source MAC for the frames (default: the iface's MAC)
Never run this outside the lab netns: it is a rogue DHCP server.
@@ -34,6 +36,7 @@ def main():
ap.add_argument("--domain")
ap.add_argument("--lldp")
ap.add_argument("--eapol", action="store_true")
ap.add_argument("--eapol-start", action="store_true")
ap.add_argument("--mac")
ap.add_argument("--duration", type=float, default=120)
args = ap.parse_args()
@@ -58,6 +61,9 @@ def main():
vlan=309))
if args.eapol:
s.send(F.eapol(mac))
if args.eapol_start:
s.send(F.eapol_start(mac))
s.send(F.eapol(mac, code=2))
next_beacon = now + 1.0
r, _, _ = select.select([s], [], [], 0.2)
if not r: