v0.6.2: Mythos Validation — full-codebase polish, hot-path optimizations, dhcproto 0.15
Codebase-wide review pass: finish or remove every loose end, take the safe performance wins on the serving hot paths, and refresh the dependency tree for reliability. No behavior changes for working clients; legacy clients get clearer protocol errors. Finalize / cleanup: - Remove mac_allowlist/subnet_allowlist config fields — parsed but never enforced since introduction; the operator wants line-of-sight serving, so the honest fix is deletion, not wiring. - Remove dead ClientRegistry API (get, set_selected_target, always-None selected_target field, never-emitted DhcpRequest/ HttpIsoAsset events). - TFTP: reject WRQ with ERR_ILLEGAL_OP and non-octet modes with a clear error instead of silent timeouts (legacy-client friendliness); fold plan_window into cfg(test); drop the unused-constant keep-alive hack. - rustfmt sweep over the six files with accumulated drift. Hot-path optimizations (all behavior-preserving): - Serve embedded iPXE binaries zero-copy (Cow over rodata) on both TFTP and HTTP — was a ~1 MiB heap copy per boot file request. - Cache the composited PXE boot-menu background PNG keyed on the branding logo revision — was ~50-200 ms of image work per booting client; now one compose per logo change. - Run bcrypt verify/hash on the blocking pool (boot password gate, login, setup, credential rotation) so CPU-heavy auth can't stall the workers streaming ISO ranges to imaging machines. - iso_raw: reuse the already-cloned IsoMeta for path resolution instead of a second registry lock + deep clone per range request. - DriverEscalation: amortize the TTL sweep (1-min interval + inline staleness check) instead of an O(map) retain per DHCP packet. - format_mac: one allocation instead of four per datagram. - Introspection haystack sized to min(scan cap, file size) — was guaranteed a 32 MiB realloc on every large-ISO probe. Robustness: - parse_range: malformed Range headers are now ignored per RFC 7233 (200 + full body) instead of answered with a bogus 206. Dependencies: - dhcproto 0.12 -> 0.15: drops the deprecated/unmaintained trust-dns-proto from the tree (hickory-proto), three releases of DHCP option coverage. Compiles + passes the full suite unchanged. - socket2 0.6 (dedupes tree), bcrypt 0.19, tower-http 0.6.11 (sheds iri-string), tokio 1.52.3 / hyper 1.10 lockfile refresh; dead nom workspace entry removed; requested versions synced to shipped reality. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4f193cac05
commit
5da05a519d
+30
-16
@@ -7,12 +7,12 @@
|
||||
//! `tftpd`/`in.tftpd` works and is why TFTP is awkward behind stateful NAT:
|
||||
//! the ephemeral ports must be reachable from the client.
|
||||
//!
|
||||
//! We only serve files from `openpxe_ipxe_assets::asset_bytes` — that is,
|
||||
//! We only serve files from `openpxe_ipxe_assets::asset_slice` — that is,
|
||||
//! the bundled iPXE binaries and wimboot. No filesystem is ever opened, so
|
||||
//! `../` path traversal attempts simply return ENOENT.
|
||||
|
||||
use openpxe_core::{ClientEvent, ClientRegistry};
|
||||
use openpxe_ipxe_assets::asset_bytes;
|
||||
use openpxe_ipxe_assets::asset_slice;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
@@ -21,6 +21,7 @@ use tokio::net::UdpSocket;
|
||||
|
||||
// TFTP opcodes.
|
||||
const OP_RRQ: u16 = 1;
|
||||
const OP_WRQ: u16 = 2;
|
||||
const OP_DATA: u16 = 3;
|
||||
const OP_ACK: u16 = 4;
|
||||
const OP_ERROR: u16 = 5;
|
||||
@@ -89,16 +90,34 @@ async fn handle_rrq(
|
||||
metrics: openpxe_core::Metrics,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(req) = parse_rrq(&packet) else {
|
||||
// Not a well-formed RRQ. A WRQ deserves an explicit refusal —
|
||||
// legacy clients retry a silently-dropped write until they time
|
||||
// out; an ERROR packet fails them fast with a readable reason.
|
||||
if packet.len() >= 2 && u16::from_be_bytes([packet[0], packet[1]]) == OP_WRQ {
|
||||
let sock = bind_udp(bind_ip, 0)?;
|
||||
let _ = send_error(&sock, peer, ERR_ILLEGAL_OP, "writes not supported").await;
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
let Request {
|
||||
filename, options, ..
|
||||
filename,
|
||||
mode,
|
||||
options,
|
||||
} = req;
|
||||
|
||||
// Per-transfer ephemeral socket.
|
||||
let sock = bind_udp(bind_ip, 0)?;
|
||||
|
||||
let Some(file_bytes) = asset_bytes(&filename) else {
|
||||
// We serve binary boot artifacts; netascii line-ending translation
|
||||
// would corrupt them. Refuse loudly instead of timing out silently —
|
||||
// matters for legacy clients that default to netascii.
|
||||
if !mode.eq_ignore_ascii_case("octet") {
|
||||
let _ = send_error(&sock, peer, ERR_NOT_DEFINED, "only octet mode is supported").await;
|
||||
tracing::info!(target: "openpxe::tftp", peer=%peer, %mode, "rejected non-octet transfer");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(file_bytes) = asset_slice(&filename) else {
|
||||
let _ = send_error(&sock, peer, ERR_FILE_NOT_FOUND, "no such file").await;
|
||||
tracing::info!(target: "openpxe::tftp", peer=%peer, file=%filename, "404");
|
||||
clients.record(
|
||||
@@ -262,7 +281,6 @@ async fn handle_rrq(
|
||||
#[derive(Debug)]
|
||||
struct Request {
|
||||
filename: String,
|
||||
#[allow(dead_code)]
|
||||
mode: String,
|
||||
options: Vec<(String, String)>,
|
||||
}
|
||||
@@ -393,17 +411,13 @@ fn bind_udp(bind: IpAddr, port: u16) -> anyhow::Result<UdpSocket> {
|
||||
Ok(UdpSocket::from_std(std_sock)?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const _UNUSED: (u16, u16) = (ERR_NOT_DEFINED, ERR_ILLEGAL_OP);
|
||||
|
||||
/// Pure-logic helper used by the unit tests below and (in a refactor) by
|
||||
/// `handle_rrq`. Given a position in the file and the window, return the
|
||||
/// (block_no, chunk_len) list this window will emit. Useful as a sanity
|
||||
/// check that our windowing math matches the wire behavior the spec
|
||||
/// requires — tested against edge cases (exact-blksize tail, short tail,
|
||||
/// single-block window).
|
||||
#[must_use]
|
||||
pub fn plan_window(
|
||||
/// Pure-logic mirror of `handle_rrq`'s windowing math, exercised by the
|
||||
/// unit tests below. Given a position in the file and the window, return
|
||||
/// the (block_no, chunk_len) list this window will emit — tested against
|
||||
/// edge cases (exact-blksize tail, short tail, single-block window,
|
||||
/// block-number wraparound).
|
||||
#[cfg(test)]
|
||||
fn plan_window(
|
||||
total: usize,
|
||||
offset: usize,
|
||||
blksize: usize,
|
||||
|
||||
Reference in New Issue
Block a user