v0.5.6: advertise the HTTP port in client-facing boot URLs

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) <[email protected]>
This commit is contained in:
Miles Ward
2026-06-03 18:49:50 -04:00
co-authored by Claude Opus 4.8
parent edf3a69daa
commit 2c8c17d444
3 changed files with 53 additions and 10 deletions
Generated
+8 -8
View File
@@ -2669,7 +2669,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "openpxe" name = "openpxe"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -2691,7 +2691,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-core" name = "openpxe-core"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@@ -2718,7 +2718,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-dhcp-proxy" name = "openpxe-dhcp-proxy"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2732,7 +2732,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-http-api" name = "openpxe-http-api"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -2768,7 +2768,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-ipxe-assets" name = "openpxe-ipxe-assets"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"openpxe-core", "openpxe-core",
"rust-embed", "rust-embed",
@@ -2778,7 +2778,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-iso-store" name = "openpxe-iso-store"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bcrypt", "bcrypt",
@@ -2807,7 +2807,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-tftp" name = "openpxe-tftp"
version = "0.5.5" version = "0.5.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2821,7 +2821,7 @@ dependencies = [
[[package]] [[package]]
name = "openpxe-webui" name = "openpxe-webui"
version = "0.5.5" version = "0.5.6"
[[package]] [[package]]
name = "p256" name = "p256"
+1 -1
View File
@@ -12,7 +12,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.5.5" version = "0.5.6"
edition = "2021" edition = "2021"
rust-version = "1.95" rust-version = "1.95"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+44 -1
View File
@@ -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()); let iso_store = IsoStore::new(config.paths.iso_dir.clone());
iso_store.load_from_disk().await?; 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 /// a loopback address (which would give every PXE client an unreachable
/// `http://127.0.0.1/...`). Users in multi-homed setups should set /// `http://127.0.0.1/...`). Users in multi-homed setups should set
/// `OPENPXE_PUBLIC_IP` explicitly. /// `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<Ipv4Addr> { fn detect_primary_ipv4() -> Option<Ipv4Addr> {
// First try: route to the public internet. `UdpSocket::connect` to a // First try: route to the public internet. `UdpSocket::connect` to a
// well-known external address causes the OS to populate `local_addr` // well-known external address causes the OS to populate `local_addr`
@@ -478,3 +498,26 @@ fn prefix_to_dotted(prefix: u8) -> String {
mask & 0xff 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");
}
}