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:
co-authored by
Claude Opus 4.8
parent
3a32d65fb7
commit
29040e8a5a
@@ -35,6 +35,24 @@ pub fn render_grub_menu(isos: &[IsoMeta], base_url: &str) -> String {
|
||||
let dev = grub_http_device(base);
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "# OpenPXE — Secure Boot menu (signed shim+GRUB chain)");
|
||||
// v0.7.1: before showing the limited signed menu, try to hand the
|
||||
// boot back to full iPXE *in this same boot cycle*. With Secure Boot
|
||||
// OFF the chainload succeeds and the client gets the complete iPXE
|
||||
// feature set (sanboot, wimboot, the full menu) despite having been
|
||||
// escalated here. With Secure Boot ON, shim's verifier refuses the
|
||||
// unsigned image INLINE — no reboot, no failed cycle — and execution
|
||||
// falls through to the signed menu below. The all-drivers build is
|
||||
// used because a MAC only lands here after the firmware-net build
|
||||
// already failed once.
|
||||
let _ = writeln!(s, "if [ \"$grub_cpu\" = \"arm64\" ]; then");
|
||||
let _ = writeln!(s, " set openpxe_ipxe=ipxe-arm64.efi");
|
||||
let _ = writeln!(s, "else");
|
||||
let _ = writeln!(s, " set openpxe_ipxe=ipxe.efi");
|
||||
let _ = writeln!(s, "fi");
|
||||
let _ = writeln!(s, "if chainloader {dev}/ipxe/$openpxe_ipxe ; then");
|
||||
let _ = writeln!(s, " boot");
|
||||
let _ = writeln!(s, "fi");
|
||||
let _ = writeln!(s);
|
||||
let _ = writeln!(s, "set timeout=30");
|
||||
let _ = writeln!(s, "set default=0");
|
||||
let _ = writeln!(s);
|
||||
@@ -139,6 +157,24 @@ mod tests {
|
||||
assert!(cfg.contains("Boot from local disk"), "{cfg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_tries_ipxe_chainload_before_menu() {
|
||||
// v0.7.1: SB-off machines recover full iPXE in the same boot;
|
||||
// SB-on machines fail the chainload inline and reach the menu.
|
||||
let cfg = render_grub_menu(&[linux_iso()], "http://10.0.0.5:8080");
|
||||
let chain_pos = cfg
|
||||
.find("if chainloader (http,10.0.0.5:8080)/ipxe/$openpxe_ipxe ; then")
|
||||
.expect("chainload attempt missing");
|
||||
let menu_pos = cfg.find("menuentry").expect("menu missing");
|
||||
assert!(
|
||||
chain_pos < menu_pos,
|
||||
"chainload must precede the menu:\n{cfg}"
|
||||
);
|
||||
// Arch-conditional binary selection via GRUB's $grub_cpu.
|
||||
assert!(cfg.contains("set openpxe_ipxe=ipxe-arm64.efi"), "{cfg}");
|
||||
assert!(cfg.contains("set openpxe_ipxe=ipxe.efi"), "{cfg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanboot_and_wimboot_entries_are_omitted() {
|
||||
let mut iso = linux_iso();
|
||||
|
||||
@@ -2751,3 +2751,23 @@ async fn arch_selective_rule_ignores_other_arches() {
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
assert!(String::from_utf8_lossy(&b).contains("boot rule -> fake-alpine-linux"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn boot_rule_driver_mode_pin_round_trips_via_api() {
|
||||
// v0.7.1: a rule may pin only a boot binary (no target) — the API
|
||||
// must persist and return it for the DHCP proxy to consult.
|
||||
let (state, _dir) = build_state().await;
|
||||
let app = build_router(state.clone());
|
||||
let cfg = r#"{"rules":[{"mac_prefix":"aa:bb:cc","arch":"","target":"","driver_mode":"shim","enabled":true,"note":"SB rack"}],"webhook_url":""}"#;
|
||||
let (s, _) = put_json(&app, "/api/boot-rules", cfg).await;
|
||||
assert_eq!(s, StatusCode::NO_CONTENT);
|
||||
let (s, b) = get(&app, "/api/boot-rules").await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v["rules"][0]["driver_mode"], "shim");
|
||||
// And the store the DHCP proxy shares resolves the pin.
|
||||
assert_eq!(
|
||||
state.boot_rules.driver_mode_hint("aa:bb:cc:00:00:07", None),
|
||||
Some(openpxe_core::DriverMode::Shim)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user