← 返回首页

把 Linux NFS 挂到 Windows:从「网络错误 53」一路踩到能写的笔记

2026-06-16 · NFSWindowsLinuxSambaufw踩坑

环境: Windows 11 专业工作站版 + 一台 Ubuntu 24.04 服务器(nfs-kernel-server)。目标:把服务器上 /srv/share 挂到 Windows 的 P:,作为读写盘使用。两台机在同一个内网子网。全程使用 Windows 内置的 Client for NFS,只支持 NFSv3。

需求很普通,Microsoft 自己也有文档,但跑下来很少有人能一遍过。下面按踩坑发生的顺序记录,每个坑给出最终能用的具体命令,而不只是"应该这样做"。

坑 1:Enable-WindowsOptionalFeature 装了但二进制文件没出现

第一步在管理员 PowerShell 里启 Client for NFS:

Enable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly -All

输出 RestartNeeded: False,看起来一切顺利。然后:

PS> C:\Windows\System32\mount.exe -o anon server:/srv/share P:
C:\Windows\System32\mount.exe : 无法将"C:\Windows\System32\mount.exe"项识别为...

mount.exe 根本不在 System32。全盘搜一下:

Get-ChildItem C:\Windows -Recurse -Include mount.exe,nfsclnt.exe -ErrorAction SilentlyContinue

文件只在 C:\Windows\WinSxS\amd64_microsoft-windows-nfs-clientcmdtools_*,没被链接到 System32。原因是 ServicesForNFS-ClientOnly 是一个伞特征,实际工具二进制在子特征 ClientForNFS-Infrastructure 里。单独 enable 父项不会自动拉子项。

修法(管理员):

Enable-WindowsOptionalFeature -Online -FeatureName ClientForNFS-Infrastructure -All
Enable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly -All
# 如果 RestartNeeded 是 True,重启一次

完成后 Test-Path C:\Windows\System32\mount.exe 应该返回 True,Get-Service NfsClnt, NfsRdr 也应该能看到两个服务。

坑 2:PowerShell 里 mountNew-PSDrive 的别名

装好后想试一下:

PS> mount -o anon server:/srv/share P:
New-PSDrive : 无法处理参数,因为参数名称"o"具有二义性。可能的匹配项包括:
              -OutVariable -OutBuffer。

mount 在 PowerShell 里是 New-PSDrive 的默认别名。-o 被当成 -OutVariable 的简写,触发了歧义。始终用 mount.exe 全路径:

C:\Windows\System32\mount.exe -o anon server:/srv/share P:

坑 3:PATH 里被另一个 mount.exe 抢走

如果你装了 Anaconda、Git for Windows、Cygwin、MSYS 之类,PATH 里很可能已经有一个完全不同mount.exe。即使你写 mount.exe(只省了路径),也会撞上:

PS> mount.exe -o anon server:/srv/share P:
mount: invalid option - 'anon'

这个报错的格式是 getopt 风格,一看就不是 Windows NFS 的那个 mount(它的错是 ERROR - Invalid command line argument)。验证哪个被解析:

Get-Command mount.exe -All | Select-Object Source
# 例如会看到 C:\Users\<you>\anaconda3\Library\usr\bin\mount.exe 排在前面

修法:就老老实实写 C:\Windows\System32\mount.exe,别想着省字符。

坑 4:服务器的 mountd 用动态端口,防火墙拦了

挂载报:

Network Error - 53

Type 'NET HELPMSG 53' for more information.

NET HELPMSG 53 = “找不到网络路径”。但 TCP 探测显示服务器的 111(portmap) 和 2049(nfs) 都通,容易误判成"网络已经通了,是路径写错了"。

真因要从 NFSv3 的工作流去想。客户端挂载时至少要走三个 RPC 程序:

mountd 默认是动态端口,所以你在防火墙白名单里永远写不上它。从客户端 showmount 的报错可以确认:

PS> C:\Windows\System32\showmount.exe -e server
RPC: Unable to send
RPC: Port mapper failure - RPC: Timed out.

(实际上服务器 rpcinfo -p 显示 mountd 是注册了的,问题是它的端口客户端连不上。)

修法:在服务器上把 mountd 钉到一个固定端口(经典选 32767),再放行防火墙。

# 在 /etc/nfs.conf 的 [mountd] 段加一行 port=32767
sudo sed -i '/^\[mountd\]/a port=32767' /etc/nfs.conf
sudo systemctl restart nfs-server

# 验证 mountd 现在所有条目都是 32767:
rpcinfo -p | grep mountd
#     100005    3   tcp  32767  mountd

# ufw 放行(注意多端口必须显式指定 proto):
sudo ufw allow from 10.0.0.0/24 to any port 111,2049,32767 proto tcp
sudo ufw allow from 10.0.0.0/24 to any port 111,2049,32767 proto udp

# (NFSv3 还会用到 lockd / statd,它们是动态端口。
#  如果你之后撞上"挂载随机 21 s 才返回",见坑 8。
#  最省心的写法是直接放整段内网:
#  sudo ufw allow from 10.0.0.0/24
#  覆盖所有端口,适合内网信任环境。)

ufw allow ... port 111,2049,32767 单写不带 proto 会报 ERROR: Must specify 'tcp' or 'udp' with multiple ports,所以一定要分开两条。

坑 5:挂上了但 Get-ChildItem Q:\ 报「Access is denied」

服务器导出:

/srv/share 10.0.0.42(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)

mount.exesuccessfully connected,但访问根目录拒绝。原因是 /srv/share 的目录权限是 drwxr-x---(750)——Windows 这边的请求虽然走 all_squash 被映射成了 anonuid=1000,但实际 readdir 路径上的某次 attribute 查询走的不是 squash 后的 uid,导致 server 端拒绝。具体什么场景下走漏 squash 取决于客户端实现,排查起来很烦。

最干净的修法是不去硬碰这个:把 export 改成更深一层的、本来就 755 的目录。比如不导出 /home/zj(750),而是导出 /home/zj/Projects(755)。配 export 时尽量挑路径上所有节点都是其他用户可遍历的目录。

修完 export 记得 sudo exportfs -ra

坑 6:多层路径必须用 UNC 风格

修完 export 重新挂:

PS> C:\Windows\System32\mount.exe -o anon server:/home/zj/Projects Q:
Network Error - 53

又是 53,但这次端口都通、showmount 也正常,真因是路径语法。Windows NFS 的 mount.exe 接受两种路径写法:

PS> C:\Windows\System32\mount.exe -o anon \\server\home\zj\Projects Q:
Q: is now successfully connected to \\server\home\zj\Projects

注意 UNC 风格用反斜杠,而且没有冒号;server:\dir1\dir2 这种半中半西的写法会直接报 Invalid command line argument

坑 7:正确的写权限

挂上后写一下:

'hello' | Out-File Q:\probe.tmp
Remove-Item Q:\probe.tmp

不报错 = all_squash → anonuid=1000 生效、本地用户拥有该目录的写权限。如果写失败,常见原因:

工作命令清单(可直接抄)

服务器端(Ubuntu,以 root):

# 1) 装包
sudo apt-get install -y nfs-kernel-server

# 2) 加 export(替换成你的实际路径 / 客户端 IP)
echo '/srv/share 10.0.0.42(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)' \
  | sudo tee -a /etc/exports

# 3) 钉 mountd 端口
sudo sed -i '/^\[mountd\]/a port=32767' /etc/nfs.conf

# 4) 启动 + 重载 export
sudo systemctl enable --now nfs-server
sudo exportfs -ra
rpcinfo -p | grep mountd       # 确认全部 32767

# 5) ufw 放行(tcp + udp 分别写)
sudo ufw allow from 10.0.0.0/24 to any port 111,2049,32767 proto tcp
sudo ufw allow from 10.0.0.0/24 to any port 111,2049,32767 proto udp

Windows 端(管理员 PowerShell):

# 1) 装 NFS 客户端(两个特征都要)
Enable-WindowsOptionalFeature -Online -FeatureName ClientForNFS-Infrastructure -All
Enable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly -All
# RestartNeeded 是 True 就重启

# 2) 验证工具已就位
Test-Path C:\Windows\System32\mount.exe   # True
Get-Service NfsClnt, NfsRdr               # Running 才正常

# 3) 挂载(单层冒号风格;多层用 UNC)
C:\Windows\System32\mount.exe -o anon server:/srv P:
C:\Windows\System32\mount.exe -o anon \\server\srv\share Q:

# 4) 列挂载状态
C:\Windows\System32\mount.exe

几个调试小工具

错误信息往往不直接,以下几条用来快速定位"卡在哪一步":

# 端口是否通(NFS 三件套)
foreach ($p in 111,2049,32767) {
    Test-NetConnection -ComputerName server -Port $p -InformationLevel Quiet
}

# 服务器到底导出了什么
C:\Windows\System32\showmount.exe -e server

# 真在 server 端注册的端口
C:\Windows\System32\rpcinfo.exe -p server

如果 Test-NetConnection 32767 = Trueshowmount 仍然 Port mapper failure - RPC: Timed out,大概率是 mountd 还没钉到 32767(回到坑 4)。

坑 8(后续踩到):cold mount 偶发卡 21 秒

文章发出去之后,我注意到一个新现象:有时挂载瞬间完成,有时同样的命令卡 21 秒,但都成功。看着像随机,直觉怀疑 server 累了或者网络抖动,实际都不是。

先量化"随机"。卸载后等不同时长再挂:

卸载后等待mount 耗时
0.5 s32 ms
3 s44 ms
10 s41 ms
30 s21 s
60 s21 s
120 s21 s

完全不随机:卸载后再挂的间隔 ≥ 30 s 就会卡 21 s,短间隔一律快

21 s 卡在哪

strace -f -e trace=connect,sendto,recvfrom,recvmsg,poll,select -p $(pgrep rpc.mountd) 抓 server 端 mountd,触发一次冷挂载——strace 文件最后只有 92 字节 1 行,server 端 mountd 在那 21 s 里根本没被调用。所以卡的不是 server 处理,是 Windows 客户端自己。

对照时间戳更直观:

13:24:00  mountd 收到 umount(上一次的卸载)
13:24:35  PowerShell 启动 mount.exe(35 s 间隔后)
13:24:45  mountd 终于收到 mount request   ← 客户端启动 10 s 后才发 RPC
13:24:56  mount.exe 返回                   ← server 回完后客户端又等 11 s

10 s + 11 s ≈ 21 s,这种"两段 10 s"的模式是 RPC 子程序连接 timeout 的典型签名——挂 NFSv3 时 Windows 客户端不只联系 mountd,还要联系 NSM(statd) 和 NLM(lockd)。在 server 端 rpcinfo -p 查这俩:

100024  status     udp 50410  tcp 56047
100021  nlockmgr   udp 59051  tcp 44423

它们也用动态端口,而且我们一开始的 ufw 规则只放行了 111, 2049, 32767(mountd 钉死的端口),50410/56047/44423/59051 全在防火墙黑名单里。Windows 端 Test-NetConnection 这几个端口都是 False,完美对上 timeout 等待。

nolock 帮不上

第一反应是给 Windows mount 加 -o nolock 让它别去碰 lockd。Windows NFS 客户端确实接受这选项,但加上后仍是 21 s——nolock 的语义是挂载完成后不要发起锁请求,不影响 mount 阶段的 NLM/NSM 探测。这个选项治不了根。

server 端也没 30 s 缓存的故事

为什么短间隔挂载就快?因为 server 端的 nfsd / mountd 维护着一个 RPC 客户端表项,短时间内同一 client IP 复用之前的认证 session,跳过 NSM/NLM 重新握手;这个表项的 TTL 大约是 30 秒。同一原因,Windows 客户端那边也维护着一个"刚卸下来,服务器是好的"的本地缓存。两边的窗口对齐,所以 30 s 是临界点。

怎么修

两条路:

A — 把 lockd / statd 也钉到固定端口,然后 ufw 放行那些端口。

# /etc/modprobe.d/lockd.conf
options lockd nlm_udpport=32768 nlm_tcpport=32768

# /etc/nfs.conf
[statd]
port=32769
outgoing-port=32770

sudo systemctl restart nfs-server

但 lockd 是内核模块——modprobe -r lockd 会失败(Module lockd is in use),所以新参数不会立即生效,需要重启服务器才能让 lockd 拿到新端口。statd 是用户态进程,跟着 nfs-server 一起重启就行。重启完再 ufw allow ... port 32768,32769,32770

B — 接受动态端口,从 ufw 那一头直接放整段内网。

sudo ufw allow from 10.0.0.0/24

或者只针对那台 Windows:

sudo ufw allow from 10.0.0.42

这条规则覆盖所有端口,所以无论 lockd / statd 用哪个动态端口都通。代价是规则更宽,但对一台只暴露在内网的 NFS server 来说完全可接受,而且不需要重启系统就立即生效。实测做完后, cold mount 从 21 s 掉到 30–60 ms。

我两台 server 用了不同方案——一台早就有完整 NFS 端口允许列表的就保留;另一台是新装,直接用 B,简洁。

一点旁证:Windows NFS 客户端的"持久挂载"行为

挂载触发后,server 端的 mountd 日志会出现一堆之前挂过、现在已经不导出的目标:

mountd: refused mount request from CLIENT for /old_share_a (/): not exported
mountd: authenticated mount request from CLIENT for /share_in_use (/share_in_use)
mountd: refused mount request from CLIENT for /old_share_b (/): not exported
mountd: refused mount request from CLIENT for /old_share_c (/): not exported

这是 Windows NFS 客户端在每次新挂载触发时,顺带尝试重连本机历史上所有挂载过的 NFS 目标,不管你这次命令里有没有它们。这些请求本身只是日志上的噪声,server 直接 refused,但每个 refused 的处理路径同样要走完 mountd 的 RPC 流程。在防火墙拦了 RPC 端口时,这些尝试也跟着卡。在彻底修好之前,这堆 refused 是个有用的旁证——说明你看到的"mount 慢"不是因为你这次命令本身复杂,而是客户端整个 NFS 子系统在挂的时候要把一堆历史状态走一遍。

把这点写在这儿是因为它最容易让人误判方向——你以为只是挂 /srv/share 一个挂载点,server 日志显示十几个目标都在动作,容易跑去查"为什么 Windows 想挂这些?“实际上是个无关紧要的 side effect,把 ufw 修对就一并消停了。

坑 9:NFS 路径里有 symlink,Windows 默认不跟随

挂上 NFS、ls Q:\ 能看到目录、单层文件能读,但访问一个 symlink 目标时报:

The symbolic link cannot be followed because its type is disabled.

Python 那边表现为 OSError: [Errno 22] Invalid argument(不是 ENOENT 也不是 EACCES,所以最容易让人误判文件不存在)。

原因是 Windows 默认禁止跟随某几类 symbolic link——出于反钓鱼:防止本地点开一个看起来无害的文件、实际被引到网络上的恶意目标。这个开关粒度按"链接的源端 + 目标端"四个方向独立控制:

方向含义默认
L2LLocal → Local允许
L2RLocal → Remote禁止
R2LRemote → Local禁止
R2RRemote → Remote禁止

Local = 本机硬盘,Remote = 任何"网络挂载”——NFS、SMB、WebDAV 一视同仁。

NFS 上跑 Linux 项目几乎一定踩 R2R:Linux 习惯用相对路径的 symlink 把目录交叉引用(Workspaces -> ../sibling/Workspaces 这种共享布局),解析后两端都在同一个 NFS 挂载里 → 走 R2R 这一桶。R2L 也常见:有些项目用 symlink 把 data 指到 /mnt/local-ssd/...

查当前状态

fsutil behavior query SymlinkEvaluation

输出形如:

Local-to-local symbolic link evaluation is: ENABLED
Local-to-remote symbolic link evaluation is: DISABLED
Remote-to-local symbolic link evaluation is: DISABLED
Remote-to-remote symbolic link evaluation is: DISABLED

管理员 PowerShell(或管理员 CMD,fsutil 不挑 shell):

fsutil behavior set SymlinkEvaluation R2R:1
# 如果还会有 NFS symlink 指向本机的需求,顺手:
fsutil behavior set SymlinkEvaluation R2L:1

立即生效,不需要重启系统、也不用重挂 NFS。这是个系统级开关、所有用户/进程共享。

如果错过这一步,排查方向会跑得极偏——文件明明用 dir 看得见,但 Python open()Invalid argument,容易跑去查路径编码、网络盘字符集、Python 字符串规范化之类。Windows 那条原文 cannot be followed because its type is disabled 倒是直接,但只有 .NET / Win32 API 调用会拿到原文,Python 那层翻译成 EINVAL 就把线索丢了。

安全权衡

默认禁止是 Microsoft 给"普通用户"的保守选择——担心把网络盘当本地、点开看似无害的链接被引到危险位置。NFS 服务器是自己内网的机器(不是公开的外部攻击面),symlink 解析出的"远端目标"也还在同一台 server 上,所以实际风险面是零;开发环境里打开 R2R / R2L 没什么需要犹豫的。

如果场景是 NFS 服务器面向多用户/部分不可信、或挂的是公司外的对象存储 gateway,那 R2R 开启确实会扩大攻击面(本地进程可能跟着 symlink 走到 server 上的其它路径),那种环境值得谨慎。

结语

整件事最大的体感是:**Windows NFS 客户端的错误信息几乎一律是「Network Error - 53」,但背后原因可以是装包不全、PATH 抢占、防火墙、路径语法、目录权限等完全不同的层。**调试方法只能一层层往下挖:Test-NetConnectionshowmountrpcinfo → server 端 journalctl -u nfs-server。一旦走通这个流程,后续再挂别的 NFS 共享就是几秒钟的事。

至于性能——这是另一个故事:Linux 的 NFS 实现下 metadata op 大约 0.01ms,Windows NFS 客户端实测大约 0.2-0.5ms,比 SMB 的 2-4ms 快一个量级,但比 Linux 客户端慢一个量级。如果你的负载是大量小文件 metadata op、又非要在 Windows 上跑,Windows NFS 比 SMB 显著好,但不要期待和 Linux 客户端一样的数字。