v0.4.67: NFSv3 alongside SMB (in-process via nfs3_client crate)

NFS is back — done right this time. v0.4.67 ships a pure-Rust NFSv3
client (`nfs3_client` 0.9 from the xetdata/Vaiz crate family) running
in-process inside the openpxe binary. No `mount.nfs`, no kernel
modules, no `CAP_SYS_ADMIN`, no subprocess. Works in every container
that the v0.4.65 SMB path works in (Unraid included).

The v0.4.65 SMB path stays as-is. Operators get both protocols
side-by-side and pick whichever their NAS prefers — or use both
together. NFSv3 has one architectural advantage over the SMB
userspace path: HTTP Range requests work for NFS-sourced ISOs
because NFSv3 READ3 takes an explicit offset. SMB-sourced ISOs still
return 416 for ranges (smbclient CLI can't seek mid-stream).

## What's new

- `crates/iso-store/src/nfs_share.rs` — `NfsShareManager` mirroring
  `SmbShareManager` structurally. Lists ISOs via READDIR3+LOOKUP3+
  GETATTR3, streams files via READ3 in 64 KiB chunks piped to axum
  body streams. Uses `connect_from_privileged_port(false)` because
  the openpxe binary runs as uid 10001 — most modern NFS servers
  allow that; a server that demands privileged ports needs
  `insecure` in /etc/exports, and the hint translation calls that
  out specifically.
- `IsoSource::Nfs { share_id, relative_path }` variant alongside the
  existing `Smb`. `IsoStore::iso_path_for` returns None for both;
  the HTTP handler dispatches to the right share manager.
- `/api/nfs-shares` CRUD + scan endpoints, parallel to
  `/api/smb-shares`. `POST` body: `{ server, export, port? }`.
- `nfs` terminal command back (this time as in-process, not kernel
  mount): `list | add <srv>:<export> [port] | remove | scan`. The
  v0.4.64 `nfs` command name pointing at kernel mount is moot
  history — same name, completely different mechanism.
- Storage tab: a new NFS shares card sits directly below the SMB
  shares card. The form is simpler (no auth fields) since NFSv3
  uses AUTH_SYS and access is gated server-side by client IP.
- Dashboard "Images available" tile sums SMB + NFS reachable shares
  into a generic "N remote shares" line.

## What's the same

- The structured `{error, stderr, hint}` JSON shape on failures
  matches the SMB API exactly, so the UI's error banner renders
  identically.
- Hint translation: NFS3ERR_ACCES → "exports list", NFS3ERR_NOENT →
  "export path doesn't exist", `mount denied` → "/etc/exports may
  need `insecure`", timeouts → "check IP/port/firewall".
- Persistence: `<work_dir>/nfs_shares.json`. No conflict with the
  long-dead v0.4.64 `nfs.json`.

## Why nfs3_client

User picked it: pure-Rust matches the architecture, NFSv3 covers the
real-world cases, AUTH_SYS keeps the UI simple. The crate is at
0.9.0, MIT/Unlicense, rust-version 1.88 (we're on 1.95). Tokio
feature flag enabled. Image size unchanged at compile time — single
musl static binary, no extra OS packages.

## Tests

160 passing (was 150 in v0.4.66, +10):
- nfs_share parser: stable share ids, server normalization (smb://,
  cifs://, \\, // all stripped).
- hint_for(): NFS3ERR_ACCES, NFS3ERR_NOENT, mount denied, unknown.
- status_label() covers the common nfsstat3 codes.
- HTTP integration: nfs-shares list starts empty, missing server
  rejected, export without leading slash rejected.

`cargo clippy --workspace --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-05-28 12:56:46 -04:00
co-authored by Claude Opus 4.7
parent 9f66c269c4
commit 3f9d8568f0
12 changed files with 1466 additions and 69 deletions
Generated
+42 -8
View File
@@ -1102,6 +1102,37 @@ dependencies = [
"version_check",
]
[[package]]
name = "nfs3_client"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ac227029a6127f3474b4fb61cbf2d3dddbe9424ad50c24fada68b633687a532"
dependencies = [
"fastrand",
"nfs3_types",
"tokio",
]
[[package]]
name = "nfs3_macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06d8fb377e6efeb91911a8ed962a69615535e167e675025a2be8fa109751426a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "nfs3_types"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5d5ab1a9fdfeab2f03b36b6300dea4ea3806e6a057cb479628b41d65e356b94"
dependencies = [
"nfs3_macros",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1140,7 +1171,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openpxe"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"anyhow",
"axum",
@@ -1162,7 +1193,7 @@ dependencies = [
[[package]]
name = "openpxe-core"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"anyhow",
"bcrypt",
@@ -1181,7 +1212,7 @@ dependencies = [
[[package]]
name = "openpxe-dhcp-proxy"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"anyhow",
"bytes",
@@ -1195,7 +1226,7 @@ dependencies = [
[[package]]
name = "openpxe-http-api"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"anyhow",
"axum",
@@ -1226,7 +1257,7 @@ dependencies = [
[[package]]
name = "openpxe-ipxe-assets"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"openpxe-core",
"rust-embed",
@@ -1236,14 +1267,17 @@ dependencies = [
[[package]]
name = "openpxe-iso-store"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"anyhow",
"bcrypt",
"bytes",
"futures",
"hex",
"image",
"libc",
"nfs3_client",
"nfs3_types",
"openpxe-core",
"parking_lot",
"serde",
@@ -1260,7 +1294,7 @@ dependencies = [
[[package]]
name = "openpxe-tftp"
version = "0.4.66"
version = "0.4.67"
dependencies = [
"anyhow",
"bytes",
@@ -1274,7 +1308,7 @@ dependencies = [
[[package]]
name = "openpxe-webui"
version = "0.4.66"
version = "0.4.67"
[[package]]
name = "parking_lot"