Files
pxe-server/tests/pxe-nic-check/lab_peer.py
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

88 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
lab_peer.py - the "other end of the cable" for the namespace lab.
Runs on one end of a veth pair inside a private network namespace and
plays a switch and/or a DHCP server with raw AF_PACKET sockets:
--dhcp IP answer every DHCPDISCOVER with a DHCPOFFER from IP
--server-id IP option 54 to put in the offer (default = --dhcp IP)
--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.
"""
import argparse
import os
import select
import socket
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import frames as F # noqa: E402
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--iface", required=True)
ap.add_argument("--dhcp")
ap.add_argument("--server-id")
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()
if os.environ.get("PXE_NIC_LAB") != "1":
sys.exit("lab_peer.py: refusing to run outside the lab (PXE_NIC_LAB)")
with open("/sys/class/net/%s/address" % args.iface) as f:
mac = args.mac or f.read().strip()
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(3))
s.bind((args.iface, 0))
end = time.monotonic() + args.duration
next_beacon = 0.0
answered = set()
sys.stdout.write("ready\n")
sys.stdout.flush()
while time.monotonic() < end:
now = time.monotonic()
if now >= next_beacon:
if args.lldp:
s.send(F.lldp(mac, system=args.lldp, port="Gi1/0/14",
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:
continue
frame, addr = s.recvfrom(65535)
if addr[2] == 4 or not args.dhcp: # our own outgoing frames
continue
d = F.parse_discover(frame)
if not d or d[0] in answered:
continue
answered.add(d[0])
xid, chaddr = d
s.send(F.dhcp_reply(mac, args.dhcp, xid, chaddr,
yiaddr="10.99.0.50", domain=args.domain,
server_id=args.server_id))
sys.stdout.write("offered xid %08x to %s\n" % (xid, chaddr))
sys.stdout.flush()
if __name__ == "__main__":
main()