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

@@ -37,13 +37,23 @@ initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BC
initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi
initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi
initrd http://${server}/win11/sources/boot.wim sources/boot.wim
boot
# A command that fails without || aborts the whole iPXE script (the PC
# drops to firmware with no explanation), so every boot line has one.
boot || goto winpe_failed
:winpe_failed
echo
echo Windows PE boot FAILED (see the error above).
echo Check that the PXE server is up and the win11 boot files are staged.
prompt Press any key to return to menu... && goto menu || goto menu
:clonezilla
set base http://${server}/clonezilla
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs || goto secureboot_warn
initrd ${base}/initrd.img
boot
initrd ${base}/initrd.img || goto boot_failed
# iPXE 2.0 accepts an unsigned EFI kernel (PE fallback) and fails only
# here under Secure Boot.
boot || goto secureboot_warn
:blancco
iseq ${platform} pcbios && goto blancco_bios || goto blancco_efi
@@ -55,7 +65,7 @@ initrd --name intel-ucode.img http://${server}/blancco/intel-ucode.img || goto b
initrd --name amd-ucode.img http://${server}/blancco/amd-ucode.img || goto blancco_bios_failed
initrd --name config.img http://${server}/blancco/config.img || goto blancco_bios_failed
initrd --name initramfs-bde-linux.img http://${server}/blancco/initramfs-bde-linux.img || goto blancco_bios_failed
boot
boot || goto blancco_bios_failed
:blancco_bios_failed
echo
echo Blancco BIOS boot FAILED.
@@ -91,7 +101,14 @@ chain http://${server}/blancco/grubx64.efi || goto secureboot_warn
:memtest
kernel http://${server}/memtest/memtest.efi || goto secureboot_warn
boot
# iPXE 2.0 accepts an unsigned EFI image (PE fallback) and fails only
# here under Secure Boot.
boot || goto secureboot_warn
:boot_failed
echo
echo Download FAILED (see the error above). Check the PXE server and retry.
prompt Press any key to return to menu... && goto menu || goto menu
:secureboot_warn
echo

View File

@@ -3,10 +3,15 @@
# pxe-dhcp-hook.sh - dnsmasq dhcp-script hook.
#
# Runs every time a PXE client gets/changes/releases a DHCP lease on
# 172.16.9.0/24. Flushes conntrack entries and drops any lingering
# TCP sockets for that client IP. Prevents stale server-side state from
# causing "System error 53 - network path not found" when a WinPE client
# re-images the same machine without a clean SMB session teardown.
# 172.16.9.0/24. Flushes that client's SMB (TCP 445 and 139) conntrack
# entries and drops its lingering SMB sockets. Prevents stale server-side
# state from causing "System error 53 - network path not found" when a
# WinPE client re-images the same machine without a clean SMB session
# teardown.
#
# SMB only, on purpose: the lease that fires this hook is iPXE's, and its
# first HTTP fetch of the boot menu is racing this script. Killing every
# socket or conntrack entry for the client IP broke that fetch.
#
# dnsmasq calls this with argv:
# $1 = action (add | old | del)
@@ -26,6 +31,9 @@
set +e
# dnsmasq's environment may lack sbin; keep any PATH it gave us first.
PATH="${PATH:+$PATH:}/usr/sbin:/usr/bin:/sbin:/bin"
action="$1"
mac="$2"
ip="$3"
@@ -34,11 +42,15 @@ host="${4:-}"
[ -z "$ip" ] && exit 0
clear_state() {
/usr/sbin/conntrack -D -s "$ip" >/dev/null 2>&1
/usr/sbin/conntrack -D -d "$ip" >/dev/null 2>&1
/usr/bin/ss -K "dst = $ip" >/dev/null 2>&1
/usr/bin/ss -K "src = $ip" >/dev/null 2>&1
logger -t pxe-dhcp-hook "cleared stale state for $1 $ip ($mac${host:+ $host})"
local port
for port in 445 139; do
# client -> our share, and anything the other way round
conntrack -D -p tcp -s "$ip" --dport "$port" >/dev/null 2>&1
conntrack -D -p tcp -d "$ip" --sport "$port" >/dev/null 2>&1
done
# our SMB sockets (local port 445/139) whose peer is this client
ss -K -t "( sport = :445 or sport = :139 ) and dst = $ip" >/dev/null 2>&1
logger -t pxe-dhcp-hook "cleared stale SMB state for $1 $ip ($mac${host:+ $host})"
}
case "$action" in

View File

@@ -0,0 +1,145 @@
#!/bin/bash
#
# pxe-nic-carrier - re-check the imaging NIC whenever its cable moves.
#
# The dnsmasq guard (pxe-nic-check --guard) runs only when dnsmasq starts,
# and the watch timer only every 5 min. If the imaging cable is moved into
# a GE port while dnsmasq runs, dnsmasq would answer DHCP on the GE LAN
# until the next watch. This hook closes that gap:
#
# carrier lost dnsmasq is stopped at once (if it was running) and a
# marker remembers that this hook stopped it.
# carrier back pxe-nic-carrier.service runs "pxe-nic-carrier --gain":
# after a 2 s settle, if dnsmasq is running or the marker
# is set, "systemctl restart dnsmasq" - its ExecStartPre
# guard checks the NIC while dnsmasq is down and refuses
# to start it on the GE network. Otherwise it only runs
# the guard to refresh /run/pxe-nic-status.json. dnsmasq
# that an operator or the watch stopped stays stopped.
#
# Only the imaging NIC (and, for a bridge, its ports) counts, as resolved
# by "pxe-nic-check --print-imaging" from /etc/pxe-server/nic.conf or the
# dnsmasq interface= pin. Events on any other NIC (the GE NIC) exit at
# once and touch nothing. Link state is read from sysfs, not from the
# event name, and remembered per interface, so the same transition seen
# twice (carrier, then routable) acts once.
#
# Called by networkd-dispatcher with IFACE and STATE in the environment
# (installed as /etc/networkd-dispatcher/<state>.d/50-pxe-nic-carrier for
# carrier, degraded, degraded-carrier, enslaved, routable, no-carrier and
# off), or by hand: pxe-nic-carrier IFACE. Logs: journalctl -t pxe-nic-carrier
#
# The dispatcher waits for this script, so the event path never runs a
# check itself: loss is two systemctl calls, gain hands off to the service.
set -u
CHECK="${PXE_NIC_CHECK:-/usr/local/sbin/pxe-nic-check}"
SYS_NET="${PXE_SYS_NET:-/sys/class/net}"
RUN="${PXE_NIC_CARRIER_RUN:-/run/pxe-nic-carrier}"
SYSTEMCTL="${PXE_SYSTEMCTL:-systemctl}"
SETTLE="${PXE_NIC_CARRIER_SETTLE:-2}"
GAIN_UNIT="pxe-nic-carrier.service"
TAG="pxe-nic-carrier"
PATH="${PATH:+$PATH:}/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"
log() { # log PRIORITY MESSAGE
logger -t "$TAG" -p "daemon.$1" -- "$2" 2>/dev/null || \
echo "$TAG: $2" >&2
}
imaging_ifaces() {
"$CHECK" --print-imaging 2>/dev/null
}
carrier_of() { # 1 if the interface has carrier, else 0 (down reads EINVAL)
local c
c=$(cat "$SYS_NET/$1/carrier" 2>/dev/null) || c=0
[ "$c" = 1 ] && echo 1 || echo 0
}
any_carrier() { # any imaging interface or port with carrier
local i
for i in "$@"; do
[ "$(carrier_of "$i")" = 1 ] && return 0
done
return 1
}
dnsmasq_running() { # active, starting or reloading
case "$("$SYSTEMCTL" is-active dnsmasq 2>/dev/null)" in
active|activating|reloading) return 0 ;;
esac
return 1
}
on_event() {
local iface="$1" list=() match="" i now prev
mapfile -t list < <(imaging_ifaces)
for i in "${list[@]}"; do
[ -n "$i" ] && [ "$i" = "$iface" ] && match=1
done
[ -n "$match" ] || exit 0 # not the imaging NIC: hands off
mkdir -p "$RUN" || exit 0
exec 9>"$RUN/event.lock"
flock -w 10 9 || log warning "lock busy, handling $iface anyway"
now=$(carrier_of "$iface")
prev=$(cat "$RUN/link.$iface" 2>/dev/null) || prev=unknown
echo "$now" > "$RUN/link.$iface"
[ "$now" = "$prev" ] && exit 0 # same state again: already handled
if [ "$now" = 0 ]; then
# an in-flight gain check must not restart dnsmasq after this
"$SYSTEMCTL" stop "$GAIN_UNIT" >/dev/null 2>&1
if dnsmasq_running; then
: > "$RUN/stopped-dnsmasq"
"$SYSTEMCTL" stop --no-block dnsmasq
log warning "carrier lost on imaging NIC $iface (${STATE:-event}): stopped dnsmasq; it restarts after the guard passes when the link returns"
else
log info "carrier lost on imaging NIC $iface (${STATE:-event}); dnsmasq not running"
fi
else
log notice "carrier up on imaging NIC $iface (${STATE:-event}, was $prev): re-checking before dnsmasq serves"
"$SYSTEMCTL" restart --no-block "$GAIN_UNIT"
fi
exit 0
}
on_gain() {
local list=() rc
sleep "$SETTLE"
mapfile -t list < <(imaging_ifaces)
if [ "${#list[@]}" -eq 0 ] || [ -z "${list[0]}" ]; then
log err "imaging NIC unknown, nothing to re-check"
exit 0
fi
if ! any_carrier "${list[@]}"; then
log info "imaging NIC ${list[0]} lost carrier again, nothing to do"
exit 0
fi
if dnsmasq_running || [ -e "$RUN/stopped-dnsmasq" ]; then
# restart = stop, then the ExecStartPre guard with dnsmasq down
"$SYSTEMCTL" restart dnsmasq
rc=$?
rm -f "$RUN/stopped-dnsmasq"
if [ "$rc" -eq 0 ]; then
log notice "imaging NIC ${list[0]}: guard passed, dnsmasq (re)started"
else
log crit "imaging NIC ${list[0]}: dnsmasq NOT started (guard refused or start failed, rc $rc); see journalctl -u dnsmasq. Fix the cabling, then systemctl start dnsmasq"
fi
else
"$CHECK" --guard >/dev/null 2>&1
rc=$?
log info "imaging NIC ${list[0]}: dnsmasq was not running, left stopped (guard rc $rc, status refreshed)"
fi
exit 0
}
case "${1:-}" in
--gain) on_gain ;;
-h|--help) sed -n '2,36p' "$0"; exit 0 ;;
*) on_event "${1:-${IFACE:-}}" ;;
esac

View File

@@ -0,0 +1,16 @@
# Started (never enabled) by pxe-nic-carrier, the networkd-dispatcher
# hook, when the imaging NIC gets carrier back. Re-checks the imaging NIC
# before dnsmasq serves again: if dnsmasq was running (or the hook stopped
# it on carrier loss), "systemctl restart dnsmasq" so its ExecStartPre
# guard decides; otherwise it only refreshes /run/pxe-nic-status.json.
# The hook restarts this unit on every carrier gain and stops it on a
# carrier loss, so the newest event wins.
[Unit]
Description=PXE NIC carrier change: re-check the imaging NIC before dnsmasq serves
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/pxe-nic-carrier --gain
TimeoutStartSec=120

View File

@@ -32,7 +32,9 @@ Verdicts per interface:
no-link no carrier
isolated no foreign DHCP server, no LLDP/CDP/EAPOL seen
ge foreign DHCP with a corp domain (opt 15 / 119), or any
LLDP, CDP or EAPOL seen (unmanaged switches send none)
LLDP, CDP or authenticator EAPOL seen (unmanaged switches
send none). EAPOL from clients (iPXE and Windows send
EAPOL-Start and EAP-Response) is ignored.
foreign-dhcp a non-own DHCP server without a corp domain
error the check itself failed
@@ -46,9 +48,14 @@ Modes:
exit 0 = safe to serve, exit 1 = DO NOT serve (ge or
foreign-dhcp). Internal errors and no link exit 0 with a loud
log line: fail-open on bugs, fail-closed only on detection.
--print-imaging
the imaging NIC (and its bridge ports), for pxe-nic-carrier.
--watch systemd timer. 60 s window + probe, writes the status file,
and on a bad imaging verdict runs "systemctl stop dnsmasq"
and logs CRIT. It never starts dnsmasq.
and logs CRIT. It never starts dnsmasq. If dnsmasq was
(re)started inside the window (its guard passed after part
of the evidence was heard), it re-checks for ~6 s first and
stops dnsmasq only if that still says blocked.
Config: /etc/pxe-server/nic.conf (INI, [nic] section optional)
@@ -96,6 +103,8 @@ STATUS_PATH = "/run/pxe-nic-status.json"
DEFAULT_SUFFIXES = "geaerospace.net, ge.com"
DEFAULT_OWN_IP = "172.16.9.1"
STOP_CMD = "systemctl stop dnsmasq"
STATE_CMD = ("systemctl show -p ActiveState "
"-p ActiveEnterTimestampMonotonic dnsmasq")
ETH_P_ALL = 0x0003
PACKET_OUTGOING = 4
@@ -329,6 +338,23 @@ def parse_bootp(udp_payload):
}
EAPOL_TYPE_EAP = 0
EAP_AUTHENTICATOR_CODES = (1, 3, 4) # Request, Success, Failure
def eapol_from_authenticator(payload):
"""True only for EAPOL that an 802.1X authenticator (a switch port)
sends: EAPOL type 0 (EAP-Packet) carrying EAP code 1 Request, 3 Success
or 4 Failure. Supplicants send EAPOL-Start (type 1), Logoff (type 2)
and EAP Responses (code 2): iPXE sends three EAPOL-Starts every time
it opens a NIC, and Windows dot3svc sends Starts and Responses, so a
bay PXE-booting next to us must not read as a GE switch port."""
if len(payload) < 5:
return False
return payload[1] == EAPOL_TYPE_EAP \
and payload[4] in EAP_AUTHENTICATOR_CODES
def parse_dhcp_frame(etype, payload):
"""IPv4/UDP 67->68 server message -> bootp dict (+ src ip) or None."""
if etype != ETH_IP or len(payload) < 20:
@@ -352,7 +378,8 @@ class Observations:
def __init__(self):
self.lldp = {} # (chassis, port) -> dict
self.cdp = {} # (device, port) -> dict
self.eapol = False
self.eapol = False # EAPOL from an authenticator (a switch)
self.eapol_client = 0 # supplicant EAPOL frames, ignored
self.stp = False
self.arp_macs = set()
self.dhcp = {} # (server, src_mac) -> dict
@@ -371,7 +398,10 @@ class Observations:
self.lldp[(info["chassis"], info["port"])] = info
return
if etype == ETH_EAPOL:
self.eapol = True
if eapol_from_authenticator(payload):
self.eapol = True
else:
self.eapol_client += 1
return
if etype <= 1500: # 802.3 length field: LLC frames
if dst == MAC_CDP:
@@ -763,6 +793,45 @@ def attach_bpf(sock):
# ---------------------------------------------------------------- capture
SOL_PACKET = 263
PACKET_ADD_MEMBERSHIP = 1
PACKET_MR_MULTICAST = 0
# Link-local groups the NIC's hardware filter drops unless someone joins
# them: STP, EAPOL (PAE), LLDP, CDP/VTP/DTP, PVST+. Without them a plain
# (non-promiscuous) NIC on a CDP-only Cisco port hears nothing.
CAPTURE_GROUPS = ["01:80:c2:00:00:00", "01:80:c2:00:00:03",
"01:80:c2:00:00:0e", "01:00:0c:cc:cc:cc",
"01:00:0c:cc:cc:cd"]
def packet_mreq(ifindex, mac):
"""struct packet_mreq {int mr_ifindex; unsigned short mr_type,
mr_alen; unsigned char mr_address[8];} for PACKET_MR_MULTICAST."""
addr = bytes.fromhex(norm_mac(mac).replace(":", ""))
if len(addr) != 6:
raise ValueError("bad MAC %r" % mac)
return struct.pack("iHH8s", ifindex, PACKET_MR_MULTICAST, len(addr),
addr)
def join_groups(sock, ifname, groups=CAPTURE_GROUPS):
"""Join each group on this socket's interface. The kernel drops the
memberships when the socket closes. Returns the groups that failed."""
failed = []
try:
ifindex = socket.if_nametoindex(ifname)
except OSError:
return list(groups)
for g in groups:
try:
sock.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP,
packet_mreq(ifindex, g))
except (OSError, ValueError):
failed.append(g)
return failed
def capture(targets, window, probe_ifaces, own_macs, probe_window=6.0):
"""targets: {report_iface: [capture ifaces]}. Listens on all of them
for `window` s (at least probe_window after the probe). Returns
@@ -783,6 +852,10 @@ def capture(targets, window, probe_ifaces, own_macs, probe_window=6.0):
log("no BPF filter on %s (%s), parsing every frame"
% (cap, e), "warning")
s.bind((cap, ETH_P_ALL))
bad = join_groups(s, cap)
if bad:
log("could not join %s on %s; a quiet switch may be "
"missed" % (", ".join(bad), cap), "warning")
s.setblocking(False)
socks[s] = name
except OSError as e:
@@ -883,6 +956,10 @@ def classify(link_up, error, obs, dhcp_servers, suffixes, window,
evidence.append(txt)
if obs and obs.eapol:
evidence.append("802.1X EAPOL seen (port authentication)")
if obs and obs.eapol_client:
evidence.append("%d client EAPOL frames ignored (supplicants such "
"as iPXE EAPOL-Start, informational)"
% obs.eapol_client)
if obs and obs.stp:
evidence.append("STP BPDUs seen (informational)")
@@ -1135,6 +1212,81 @@ def summary(status):
return "\n".join(lines)
def parse_unit_state(text):
"""`systemctl show -p ActiveState -p ActiveEnterTimestampMonotonic`
output -> (active_state or None, active_enter_us or None)."""
props = {}
for line in (text or "").splitlines():
if "=" in line:
k, v = line.split("=", 1)
props[k.strip()] = v.strip()
state = props.get("ActiveState") or None
try:
since = int(props.get("ActiveEnterTimestampMonotonic", ""))
except ValueError:
since = None
return state, since
def dnsmasq_state(cmd):
try:
res = subprocess.run(cmd, shell=True, capture_output=True,
text=True, timeout=10)
except (OSError, subprocess.SubprocessError):
return None, None
if res.returncode != 0:
return None, None
return parse_unit_state(res.stdout)
def watch_decision(state, since_us, window_start_us):
"""What --watch does with a 'blocked' verdict, given dnsmasq's
ActiveState / ActiveEnterTimestampMonotonic and the CLOCK_MONOTONIC
microsecond the watch's capture began. -> "stop", "skip" (already
stopped) or "recheck" (dnsmasq was (re)started inside the window, so
its own guard passed after part of our evidence was heard; confirm
with a fresh short check before stopping it)."""
if state is None:
return "stop" # cannot tell: stopping is the safe side
if state in ("inactive", "failed", "deactivating"):
return "skip"
if state in ("activating", "reloading"):
return "recheck" # its guard is running right now
if since_us and since_us > window_start_us:
return "recheck"
return "stop"
def stop_dnsmasq(cmd):
try:
rc = subprocess.run(cmd, shell=True, timeout=60).returncode
if rc != 0:
log("'%s' exited %d" % (cmd, rc), "crit")
except (OSError, subprocess.SubprocessError) as e:
log("could not stop dnsmasq: %s" % e, "crit")
def recheck(args, cfg):
"""A guard-style check (imaging NIC, short window + probe), written to
the status file. -> (state, reason). An internal error counts as
blocked: the 60 s watch already saw the bad evidence."""
sub = argparse.Namespace(**vars(args))
sub.mode = "guard"
sub.window = 5.0
sub.probe = True
try:
status, _ = run_check(sub, cfg)
except Exception as e:
return "blocked", "re-check failed (%s); keeping the watch verdict" \
% e
if args.write:
try:
write_status(args.write, status)
except OSError as e:
log("could not write %s: %s" % (args.write, e), "err")
return status["dhcp_guard"]["state"], status["dhcp_guard"]["reason"]
def main(argv=None):
ap = argparse.ArgumentParser(
prog=PROG,
@@ -1149,6 +1301,10 @@ def main(argv=None):
mode.add_argument("--watch", dest="mode", action="store_const",
const="watch", help="timer mode: stop dnsmasq on a "
"bad imaging verdict")
mode.add_argument("--print-imaging", dest="mode", action="store_const",
const="print-imaging", help="print the imaging NIC "
"and, for a bridge, its ports (one per line; nothing "
"if unknown), then exit. Used by pxe-nic-carrier.")
ap.add_argument("--json", action="store_true",
help="print the status JSON")
ap.add_argument("--write", metavar="PATH", nargs="?", const=STATUS_PATH,
@@ -1171,6 +1327,9 @@ def main(argv=None):
ap.add_argument("--lease-dir", default=LEASE_DIR)
ap.add_argument("--stop-cmd", default=STOP_CMD,
help="command --watch runs to stop dnsmasq")
ap.add_argument("--state-cmd", default=STATE_CMD,
help="command --watch runs to read dnsmasq's "
"ActiveState and ActiveEnterTimestampMonotonic")
args = ap.parse_args(argv)
args.mode = args.mode or "check"
if args.window is None:
@@ -1180,6 +1339,22 @@ def main(argv=None):
if args.mode in ("guard", "watch") and not args.write:
args.write = STATUS_PATH
if args.mode == "print-imaging":
try:
cfg = load_config(args.config, args.dnsmasq_nic_conf)
ifaces = all_ifaces()
imaging, _ge, _notes = resolve_roles(cfg, ifaces, iface_addrs(),
args.lease_dir)
except Exception as e:
log("could not resolve the imaging NIC: %s" % e, "err")
return 2
if imaging:
print("\n".join([imaging] + (bridge_ports(imaging)
if is_bridge(imaging) else [])))
return 0
# CLOCK_MONOTONIC, the clock of ActiveEnterTimestampMonotonic
window_start_us = int(time.clock_gettime(time.CLOCK_MONOTONIC) * 1e6)
try:
cfg = load_config(args.config, args.dnsmasq_nic_conf)
status, imaging = run_check(args, cfg)
@@ -1217,15 +1392,25 @@ def main(argv=None):
return 0
if args.mode == "watch":
if state == "blocked":
log("imaging NIC is NOT isolated, stopping dnsmasq: " + reason,
"crit")
try:
rc = subprocess.run(args.stop_cmd, shell=True,
timeout=60).returncode
if rc != 0:
log("'%s' exited %d" % (args.stop_cmd, rc), "crit")
except (OSError, subprocess.SubprocessError) as e:
log("could not stop dnsmasq: %s" % e, "crit")
dstate, since = dnsmasq_state(args.state_cmd)
what = watch_decision(dstate, since, window_start_us)
if what == "recheck":
log("dnsmasq was (re)started during this watch window "
"(its guard passed); re-checking before stopping it: "
+ reason, "warning")
state, reason = recheck(args, cfg)
if state == "blocked":
what = "stop"
else:
log("re-check says %s, leaving dnsmasq running: %s"
% (state, reason), "notice")
if what == "skip":
log("imaging NIC is NOT isolated, dnsmasq already %s: %s"
% (dstate, reason), "crit")
elif what == "stop":
log("imaging NIC is NOT isolated, stopping dnsmasq: "
+ reason, "crit")
stop_dnsmasq(args.stop_cmd)
elif state == "unknown":
log("watch could not verify the imaging NIC: " + reason, "err")
else:

View File

@@ -3,7 +3,8 @@
# /run/pxe-nic-status.json for the webapp, and STOPS dnsmasq if the imaging
# NIC turns out to be on the GE network or next to another DHCP server.
# It never starts dnsmasq: after fixing the cabling, start it by hand
# (systemctl start dnsmasq), which re-runs the guard.
# (systemctl start dnsmasq), which re-runs the guard. If dnsmasq was
# restarted inside the window it re-checks (~6 s) before stopping it.
[Unit]
Description=PXE NIC watch: keep dnsmasq off the GE corporate network