pxe-nic-check classifies each NIC from what it hears: LLDP/CDP/802.1X from managed switches, foreign DHCP offers and their domain (one DHCPDISCOVER on the imaging NIC; the GE NIC's networkd lease is read instead), STP and ARP hosts for information. A kernel BPF filter keeps bulk SMB traffic out of the capture loop; bridge ports are listened on too, since LLDP and EAPOL stop at the port. - dnsmasq drop-in runs --guard as ExecStartPre: exit 1 on a ge or foreign-dhcp imaging verdict, fail open (loudly) on bugs or no link - pxe-nic-check.timer runs --watch every 5 min: stops dnsmasq on a bad imaging verdict, never starts it - /run/pxe-nic-status.json (0644, atomic) for the webapp - playbook installs it all and writes /etc/pxe-server/nic.conf from pxe_imaging_mac, pxe_ge_mac, pxe_corp_domain_suffixes, pxe_nic_allowed_switches - unit tests plus a no-root network-namespace lab (veth pairs, a raw socket DHCP/LLDP peer and real dnsmasq); docs/pxe-nic-guard.md has the operator notes and the two floor tests
82 lines
2.7 KiB
Python
Executable File
82 lines
2.7 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
|
|
--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("--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))
|
|
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()
|