From c0d17fa9cab4d04d9855ad90993dbfd70f8a9e3c Mon Sep 17 00:00:00 2001 From: Miles Ward Date: Wed, 3 Jun 2026 18:49:50 -0400 Subject: [PATCH] v0.5.6: advertise the HTTP port in client-facing boot URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base URL handed to PXE clients was built as `http://{ip}` with no port, ignoring OPENPXE_HTTP_PORT. Every client-facing URL derives from it — the DHCP-proxy iPXE filename, UEFI HTTP boot, and the boot menu's kernel/initrd/ISO links — so any non-80 deployment told clients to fetch :80 (the wrong service). On Unraid that's the webGUI, which 301s to https; iPXE (no TLS) then fails the chain with "Operation not supported". This broke the exact configuration the Unraid template recommends (HTTP port 4200, to avoid the webGUI on :80). Fix: build_public_base_url(ip, port) includes the port unless it's 80, so http://10.0.0.5 stays clean while http://10.0.0.5:4200 is reachable. One source of truth, so the whole URL surface is corrected at once. Regression-tested (port included for 4200/8080, omitted for 80). Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 16 +++++++------- Cargo.toml | 2 +- crates/openpxe/src/main.rs | 45 +++++++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f1b17c..d45fbce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2669,7 +2669,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openpxe" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "axum", @@ -2691,7 +2691,7 @@ dependencies = [ [[package]] name = "openpxe-core" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "base64", @@ -2718,7 +2718,7 @@ dependencies = [ [[package]] name = "openpxe-dhcp-proxy" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "bytes", @@ -2732,7 +2732,7 @@ dependencies = [ [[package]] name = "openpxe-http-api" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "axum", @@ -2768,7 +2768,7 @@ dependencies = [ [[package]] name = "openpxe-ipxe-assets" -version = "0.5.5" +version = "0.5.6" dependencies = [ "openpxe-core", "rust-embed", @@ -2778,7 +2778,7 @@ dependencies = [ [[package]] name = "openpxe-iso-store" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "bcrypt", @@ -2807,7 +2807,7 @@ dependencies = [ [[package]] name = "openpxe-tftp" -version = "0.5.5" +version = "0.5.6" dependencies = [ "anyhow", "bytes", @@ -2821,7 +2821,7 @@ dependencies = [ [[package]] name = "openpxe-webui" -version = "0.5.5" +version = "0.5.6" [[package]] name = "p256" diff --git a/Cargo.toml b/Cargo.toml index 70aa85c..c9a34e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.5.5" +version = "0.5.6" edition = "2021" rust-version = "1.95" license = "MIT OR Apache-2.0" diff --git a/crates/openpxe/src/main.rs b/crates/openpxe/src/main.rs index 794335c..d835d2c 100644 --- a/crates/openpxe/src/main.rs +++ b/crates/openpxe/src/main.rs @@ -94,7 +94,13 @@ async fn main() -> anyhow::Result<()> { } }, }; - let public_base_url = format!("http://{our_ip}"); + // v0.5.6: the advertised base URL must carry the HTTP port. Every + // client-facing URL (the DHCP-proxy iPXE filename, UEFI HTTP boot, + // and the menu's kernel/initrd/ISO links) is derived from this one + // string, so omitting the port silently pointed PXE clients at :80 — + // breaking every non-80 deployment (e.g. the Unraid template's 4200, + // chosen to dodge the webGUI). See `build_public_base_url`. + let public_base_url = build_public_base_url(our_ip, config.server.http_port); let iso_store = IsoStore::new(config.paths.iso_dir.clone()); iso_store.load_from_disk().await?; @@ -345,6 +351,20 @@ async fn seed_from_dir( /// a loopback address (which would give every PXE client an unreachable /// `http://127.0.0.1/...`). Users in multi-homed setups should set /// `OPENPXE_PUBLIC_IP` explicitly. +/// Build the base URL advertised to PXE clients. The port is included +/// unless it's the HTTP default (80), keeping the common case clean +/// (`http://10.0.0.5`) while a remapped port (`http://10.0.0.5:4200`) +/// stays reachable. This is the single source of truth for every +/// client-facing URL — the DHCP-proxy iPXE filename, UEFI HTTP boot, and +/// the boot menu's kernel/initrd/ISO links all derive from it. +fn build_public_base_url(ip: Ipv4Addr, http_port: u16) -> String { + if http_port == 80 { + format!("http://{ip}") + } else { + format!("http://{ip}:{http_port}") + } +} + fn detect_primary_ipv4() -> Option { // First try: route to the public internet. `UdpSocket::connect` to a // well-known external address causes the OS to populate `local_addr` @@ -478,3 +498,26 @@ fn prefix_to_dotted(prefix: u8) -> String { mask & 0xff ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn public_base_url_includes_non_default_port() { + // The v0.5.6 regression guard: a remapped HTTP port (e.g. the + // Unraid template's 4200) MUST appear in the advertised URL, or + // PXE clients fetch :80 — the wrong service — and boot fails. + let ip: Ipv4Addr = "192.168.1.49".parse().unwrap(); + assert_eq!(build_public_base_url(ip, 4200), "http://192.168.1.49:4200"); + assert_eq!(build_public_base_url(ip, 8080), "http://192.168.1.49:8080"); + } + + #[test] + fn public_base_url_omits_default_port() { + // Port 80 stays clean (no `:80`) so the common case reads nicely + // and matches what every browser/iPXE assumes by default. + let ip: Ipv4Addr = "10.0.0.5".parse().unwrap(); + assert_eq!(build_public_base_url(ip, 80), "http://10.0.0.5"); + } +}