feat: add portmap_listener for Windows NFS clients#44
Draft
XciD wants to merge 3 commits into
Draft
Conversation
`NFSTcpListener` already serves portmap on the same TCP port as NFS and MOUNT,
which works for Linux/macOS clients that pass `mountport=N` and skip the
portmapper round-trip. The Windows "Client for NFS" has no equivalent option:
`mount.exe` always queries portmapper at port 111 to discover the MOUNT and
NFS service ports, and without a listener at 111 the mount fails with
"network path not found" (NET HELPMSG 53).
`portmap_listener::spawn(bind_addr, target_port)` binds a TCP+UDP listener on
the caller-chosen address (typically 127.0.0.1:111, requires Administrator
on Windows / root on Linux) and answers PMAPPROC_GETPORT queries with the
supplied `target_port` for the NFS (100003) and MOUNT (100005) program
numbers. Intentionally minimal: no SET/UNSET/DUMP/CALLIT.
Reuses the existing portmap/rpc/xdr types in the crate; adds no new deps.
The returned JoinHandle aborts the listener loops on drop.
Typical usage:
let listener = NFSTcpListener::bind("127.0.0.1:0", fs).await?;
let port = listener.get_listen_port();
let _pm = portmap_listener::spawn("127.0.0.1:111".parse()?, port).await?;
listener.handle_forever().await?;
8cb5cc6 to
bbfcce6
Compare
XciD
added a commit
to huggingface/hf-mount
that referenced
this pull request
Apr 27, 2026
Drop the inline RFC 1833 portmapper module and depend on the matching upstream feature in nfsserve (huggingface/nfsserve#44, branch feat/portmap-listener via [patch.crates-io]). Net: -147 lines locally; the portmapper now lives where it logically belongs (next to nfsserve's existing portmap_handlers and rpc/xdr code, where future maintainers will look first). Drop the patch.crates-io entry once nfsserve cuts a release containing the new module.
Codex review caught real bugs in the initial cut:
- TCP fragment framing ignored the "last fragment" bit in RFC 1057 RM
records. Now accumulates fragments until last == true and caps total
record size at 4 KiB so a peer can't pin ~2 GiB of memory by advertising
a max-length first fragment.
- GETPORT lookup ignored mapping.vers and mapping.prot. RFC 1833 specifies
lookup by (prog, vers, prot); only the queried tuple matching nfsserve's
actual service shape (NFS_v3 / MOUNT_v3, TCP) returns target_port.
Anything else now returns 0 ("no such mapping"), including UDP queries
for a UDP NFS service that doesn't exist.
- Missing rpcvers check: non-RPC-v2 calls now get rpc_vers_mismatch
instead of being treated as portmap calls.
- Malformed GETPORT mapping args were silently dropped, which loops UDP
retries; now returns garbage_args_reply_message.
- Listener lifecycle: dropping the returned JoinHandle does NOT abort
Tokio tasks, so the previous design leaked port 111 binding indefinitely
on shutdown. Restructure so the outer task owns child tasks in a
JoinSet (and TCP per-conn tasks in a nested JoinSet); aborting the
outer handle now propagates Drop -> JoinSet -> child cancellation.
Docstring updated to clarify .abort() is required.
Cover all reply branches: - NULL returns success - GETPORT(NFS_v3, TCP) and (MOUNT_v3, TCP) return target_port - GETPORT over UDP, unknown program, wrong version return 0 - Truncated GETPORT args return garbage_args - Wrong rpcvers returns rpc_vers_mismatch - Unknown program / proc return prog_unavail / proc_unavail - Truncated RPC header returns None (drop) 11 tests, no new deps. Catches the regressions codex flagged.
XciD
added a commit
to huggingface/hf-mount
that referenced
this pull request
May 4, 2026
Drop the inline RFC 1833 portmapper module and depend on the matching upstream feature in nfsserve (huggingface/nfsserve#44, branch feat/portmap-listener via [patch.crates-io]). Net: -147 lines locally; the portmapper now lives where it logically belongs (next to nfsserve's existing portmap_handlers and rpc/xdr code, where future maintainers will look first). Drop the patch.crates-io entry once nfsserve cuts a release containing the new module.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NFSTcpListeneralready serves portmap RPC on the same TCP port as NFS and MOUNT (seeportmap_handlers.rs). This works for Linux/macOS clients that passmountport=Nto skip the portmapper round-trip.The Windows "Client for NFS" has no equivalent option.
mount.exealways queries portmapper at port 111 to discover the MOUNT and NFS service ports, and without a listener at 111 the mount fails with "network path not found" (NET HELPMSG 53).This PR adds
portmap_listener::spawn(bind_addr, target_port), a minimal standalone portmapper that binds a TCP + UDP listener at a caller-chosen address (typically127.0.0.1:111, requires Administrator on Windows / root on Linux) and answersPMAPPROC_GETPORTqueries with the suppliedtarget_portfor the NFS (100003) and MOUNT (100005) program numbers.Verified end-to-end on Windows Server 2022 with the "Client for NFS" feature:
mount.exe -o anon \\127.0.0.1\! Z:succeeds,dir Z:lists the export, file reads work correctly.Design
PMAPPROC_NULL(proc 0) andPMAPPROC_GETPORT(proc 3); other procedures returnPROC_UNAVAIL.portmap,rpc,rpcwire::write_fragment, andxdrtypes; no new dependencies.tokio::task::JoinHandlethat aborts the UDP and TCP loops on drop.Usage
Why a separate listener (not extending the main TCP listener)
The Windows client connects to
127.0.0.1:111specifically. Adding a second port toNFSTcpListenerwould require either binding to two ports (and exposing both in the public API) or running portmap on both the existing port and 111. A standalone module keeps the change narrowly scoped and lets callers opt in only when they target Windows.Out of scope
PMAPPROC_SET/PMAPPROC_UNSET/PMAPPROC_DUMP/PMAPPROC_CALLIT(not used by Windows mount.exe).NFSTcpListener::bind(kept opt-in to avoid surprising privilege requirements).