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

@@ -66,13 +66,25 @@ def cdp(src, device="WJ-CORE1.wjs.geaerospace.net", port="GigabitEthernet1/0/3",
+ llc
def eapol(src):
# EAPOL v2, type 0 (EAP packet), EAP Request/Identity
eap = bytes([1, 1]) + struct.pack("!H", 5) + bytes([1])
def eapol(src, code=1):
"""EAPOL v2, type 0 (EAP packet). code 1 Request/Identity (what a
switch port sends), 2 Response/Identity (a supplicant), 3 Success,
4 Failure."""
if code in (1, 2):
eap = bytes([code, 1]) + struct.pack("!H", 5) + bytes([1])
else:
eap = bytes([code, 1]) + struct.pack("!H", 4)
body = bytes([2, 0]) + struct.pack("!H", len(eap)) + eap
return eth("01:80:c2:00:00:03", src, 0x888E, body)
def eapol_start(src, eapol_type=1):
"""Supplicant EAPOL-Start (type 1) or Logoff (type 2), empty body:
iPXE sends three Starts each time it opens a NIC."""
body = bytes([2, eapol_type]) + struct.pack("!H", 0)
return eth("01:80:c2:00:00:03", src, 0x888E, body)
def stp(src):
bpdu = b"\x00\x00\x00\x00\x00" + b"\x80\x00" + mac(src) + b"\x00" * 23
llc = b"\x42\x42\x03" + bpdu