v0.7.1: walk the ladder once ever — persistent learned modes, rule pins, same-boot iPXE recovery

Answers the operational question 'can a machine try all three boot
binaries in one go?' The protocol can't carry three NBPs in one cycle
(one boot file per DHCP round, the Secure-Boot refusal happens after
handoff with no error report, and the broken-NIC case specifically needs
the firmware itself to load builtin-driver iPXE — GRUB's network rides
the same broken firmware stack). What we CAN do is make the walk a
once-per-machine-ever event and give operators a way to skip it:

- Learned driver modes persist (<work_dir>/driver_modes.json). A MAC
  that reaches the Shim rung, or confirms an iPXE handoff at Builtin,
  is pinned to disk: immune to the 30-min TTL, reloaded at startup.
  The file only carries exceptions — a healthy fleet never writes it.
  Corrupt file starts empty (standard crash-cache policy).
- Boot rules gain an optional driver_mode pin (auto/firmware/builtin/
  shim), consulted by the DHCP proxy BEFORE the escalation ladder:
  'this OUI is a Secure Boot rack -> serve shim immediately' = zero
  failed cycles. Mode-only rules coexist with target rules (a pin
  doesn't shadow a later target match). Editor column on Hosts tab.
- grub.cfg now tries to chainload all-drivers iPXE before showing the
  signed menu: with SB off the chainload succeeds and the client gets
  the full iPXE feature set back in the SAME boot (self-healing for
  mis-escalations, and the handoff then pins the working mode); with
  SB on, shim's verifier refuses it inline — no reboot — and the
  signed menu appears.

DhcpProxyServer now takes the escalation table + rules store from main
(persistence path comes from the configured work dir).

Validation: clippy clean, fmt clean, 299 workspace tests green (+9:
persistence round-trip across restart, Shim pin survives TTL, learned
Builtin survives TTL, corrupt-file recovery, default-mode-never-
persisted, rule-pin matching incl. unknown-mode tolerance and
pin/target coexistence, GRUB chainload-before-menu ordering, API
round-trip of the driver_mode field).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-06-09 21:16:22 -04:00
co-authored by Claude Opus 4.8
parent 3a32d65fb7
commit 29040e8a5a
10 changed files with 517 additions and 100 deletions
+17 -5
View File
@@ -209,6 +209,13 @@
['', 'any arch'], ['bios', 'BIOS'], ['uefi-x64', 'UEFI x64'],
['uefi-ia32', 'UEFI IA32'], ['uefi-arm64', 'UEFI ARM64'],
];
// v0.7.1: optional first-boot binary pin. "Auto" lets the
// escalation ladder learn per machine; pinning skips the learning
// walk entirely (e.g. a rack known to run Secure Boot → shim).
const modeChoices = [
['', 'auto (learn)'], ['firmware', 'Firmware NIC'],
['builtin', 'iPXE drivers'], ['shim', 'Secure Boot (shim)'],
];
const rules = (cfg.rules || []).map(r => Object.assign({}, r));
const tbody = el('tbody', {});
const msg = el('div', {class:'msg'});
@@ -224,8 +231,8 @@
const redraw = () => {
tbody.innerHTML = '';
if (!rules.length) {
tbody.appendChild(el('tr', {}, el('td', {colspan:'6', class:'empty', style:'padding:14px'},
'No rules. Add one to route whole groups of machines (an OUI, an architecture) to a target.')));
tbody.appendChild(el('tr', {}, el('td', {colspan:'7', class:'empty', style:'padding:14px'},
'No rules. Add one to route whole groups of machines (an OUI, an architecture) to a target — or to pin a boot binary (e.g. Secure Boot racks → shim, zero failed cycles).')));
}
rules.forEach((r, i) => {
const macIn = el('input', {type:'text', spellcheck:'false', placeholder:'aa:bb:cc (prefix)',
@@ -235,6 +242,9 @@
el('option', Object.assign({value: v}, v === (r.arch || '') ? {selected:''} : {}), label)));
const tgtSel = targetSelect(r.target || '');
tgtSel.onchange = e => { r.target = e.target.value; };
const modeSel = el('select', {onchange: e => { r.driver_mode = e.target.value; }},
modeChoices.map(([v, label]) =>
el('option', Object.assign({value: v}, v === (r.driver_mode || '') ? {selected:''} : {}), label)));
const noteIn = el('input', {type:'text', placeholder:'note',
value: r.note || '', oninput: e => { r.note = e.target.value; }});
const enabled = el('input', {type:'checkbox', onchange: e => { r.enabled = e.target.checked; }});
@@ -243,6 +253,7 @@
el('td', {}, macIn),
el('td', {}, archSel),
el('td', {}, tgtSel),
el('td', {}, modeSel),
el('td', {}, noteIn),
el('td', {style:'text-align:center'}, enabled),
el('td', {style:'text-align:right'},
@@ -257,8 +268,9 @@
redraw();
}}, '+ Add rule');
const saveBtn = el('button', {onclick: async () => {
const bad = rules.find(r => r.enabled !== false && !r.target);
if (bad) { msg.textContent = 'Every enabled rule needs a target.'; msg.className = 'msg err'; return; }
// A rule needs at least one effect: a target or a boot-binary pin.
const bad = rules.find(r => r.enabled !== false && !r.target && !r.driver_mode);
if (bad) { msg.textContent = 'Every enabled rule needs a target or a boot-binary pin.'; msg.className = 'msg err'; return; }
const r = await putJSON('/api/boot-rules', {rules, webhook_url: webhookInput.value.trim()});
if (r.ok) { msg.textContent = 'Saved.'; msg.className = 'msg ok'; }
else { msg.textContent = 'Save failed: ' + await r.text(); msg.className = 'msg err'; }
@@ -273,7 +285,7 @@
el('table', {}, [
el('thead', {}, el('tr', {}, [
el('th',{},'MAC prefix'), el('th',{},'Arch'), el('th',{},'Target'),
el('th',{},'Note'), el('th',{},'On'), el('th',{},''),
el('th',{},'Boot binary'), el('th',{},'Note'), el('th',{},'On'), el('th',{},''),
])),
tbody,
]),