Skip to content

feat: add portmap_listener for Windows NFS clients#44

Draft
XciD wants to merge 3 commits into
mainfrom
feat/portmap-listener
Draft

feat: add portmap_listener for Windows NFS clients#44
XciD wants to merge 3 commits into
mainfrom
feat/portmap-listener

Conversation

@XciD
Copy link
Copy Markdown
Member

@XciD XciD commented Apr 27, 2026

Summary

NFSTcpListener already serves portmap RPC on the same TCP port as NFS and MOUNT (see portmap_handlers.rs). This works for Linux/macOS clients that pass mountport=N to 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).

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 (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.

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

  • Intentionally minimal: only PMAPPROC_NULL (proc 0) and PMAPPROC_GETPORT (proc 3); other procedures return PROC_UNAVAIL.
  • Reuses the existing portmap, rpc, rpcwire::write_fragment, and xdr types; no new dependencies.
  • Returns a tokio::task::JoinHandle that aborts the UDP and TCP loops on drop.

Usage

let listener = NFSTcpListener::bind("127.0.0.1:0", fs).await?;
let port = listener.get_listen_port();
let _pm = nfsserve::portmap_listener::spawn(
    "127.0.0.1:111".parse()?,
    port,
).await?;
listener.handle_forever().await?;

Why a separate listener (not extending the main TCP listener)

The Windows client connects to 127.0.0.1:111 specifically. Adding a second port to NFSTcpListener would 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).
  • Auto-binding 111 from NFSTcpListener::bind (kept opt-in to avoid surprising privilege requirements).

`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?;
@XciD XciD force-pushed the feat/portmap-listener branch from 8cb5cc6 to bbfcce6 Compare April 27, 2026 18:35
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.
XciD added 2 commits April 27, 2026 21:22
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant