最近有朋友问了我一个特别典型的问题:他在 Windows 上用 WSL2 起了个 Web 服务,自己电脑上打开http://localhost:8080一切正常,但换到手机或者办公室另一台电脑,输入他那台 Windows 主机的局域网 IP,就是死活连不上。
这个问题我太熟了。WSL2 跑 Web 服务之后,本机能访问、局域网设备访问不了,根本原因就是 WSL2 的网络架构默认是一层 NAT,它和 Windows 主机之间有一条“特殊通道”,但和局域网之间隔着一堵墙。今天我就把完整的“两步法”拆开讲清楚——第一步让服务监听对地址,第二步把 Windows 主机的流量转进 WSL2,照着做,局域网设备就能直接访问你在 WSL2 里跑起来的 Web 服务了。
这篇文章也顺手把相关的高频问题一起整理了:WSL2 安装、Ubuntu 22.04 环境配置、systemd 启动、防火墙放行、netsh 端口转发、动态 IP 导致的规则失效,基本覆盖从装好 WSL2 到稳定对外提供服务的完整链路。适合正在折腾 WSL2 开发环境、想用手机或局域网设备测试 Web 项目的朋友。
1. 先搞懂 WSL2 的网络结构,不然你连问题都描述不清楚
1.1 WSL2 本质上是台虚拟机,NAT 模式下默认“藏”在 Windows 后面
WSL2 和第一代 WSL 最大的区别,就是它真的跑在了一个轻量级虚拟机里(基于 Hyper-V 架构)。既然是虚拟机,网络就是虚拟出来的:WSL2 自己有一块虚拟网卡,Windows 主机也有一块对应的虚拟网卡,两者之间组成一个私有的 NAT 网络。WSL2 里的服务绑定端口后,实际上是在 NAT 内网里监听,Windows 这边能访问,是因为 WSL2 做了一层 localhost 转发;但局域网里的其他设备,和 WSL2 根本不在同一个网段,它们只能看到 Windows 主机的物理网卡 IP,自然就摸不到 WSL2 里的服务。
用一个生活化的类比:WSL2 就像你家院子里的一间独立小屋,Windows 是主屋。你在小屋里做饭(跑 Web 服务),主屋里的人闻着香味就能过来吃(本机 localhost 访问)。但小区外面的人(局域网设备)只知道你主屋的门牌号,院子里的独立小屋他们没有钥匙也看不见路,想吃到小屋里的饭,就得靠主屋的人帮忙端出来(端口转发)。
这里有个非常关键的技术点:WSL2 每次启动时,虚拟网卡的 IP 是动态分配的,不是固定的。你可以打开终端跑一下:
wsl hostname -I每次启动 WSL2,这个 IP 都可能变。这一点直接决定了后面端口转发规则不能写死,否则 WSL2 重启一次,规则就失效了。这是整篇文章里最容易踩的坑,先记在心里。
1.2 本机能访问,是因为 localhost forwarding 在“帮你作弊”
很多人会有个疑惑:既然 WSL2 是 NAT 网络,为什么我在 Windows 浏览器里输localhost:8080就能访问 WSL2 里的服务?
这是 WSL2 内置的 localhost forwarding 功能在起作用。Windows 侧的 WSL 服务会自动监听 localhost,并把发往 localhost 的请求转发给 WSL2 虚拟网卡对应的端口。也就是说,本机访问走的是 Windows 和 WSL2 之间的“专用内部通道”,这个通道不做任何防火墙隔离,所以本机能直接通。
理解这一点非常重要,因为它解释了一个很多人困惑的现象:
- 在 Windows 里访问
http://localhost:8080,能通,是因为 localhost 转发。 - 在 Windows 里访问
http://127.0.0.1:8080,通常也能通,因为 127.0.0.1 和 localhost 绑定的是同一个回环地址。 - 在 Windows 里访问
http://<WSL2 的 IP>:8080,也能通,因为 Windows 和 WSL2 在同一 NAT 网段。 - 在局域网设备上访问
http://<Windows 主机 IP>:8080,不通,因为流量到了 Windows 物理网卡,Windows 默认不会把它转给 WSL2。
所以“本机能访问”这件事,恰恰成了误导你排查方向的障眼法。很多人一直以为是服务端问题,反复在 WSL2 里改配置,改了半天依然没用,因为问题根本不出在 WSL2 服务本身,而是出在 Windows 主机这一层,缺少了一条把局域网流量导入 WSL2 的“路”。
2. 第一步:让 WSL2 里的服务真正监听在 0.0.0.0 而不是 127.0.0.1
2.1 监听地址决定了服务“只对谁可见”
大多数 Web 框架和开发服务器,默认监听地址都是127.0.0.1(也就是 localhost)。这意味着服务只能在 WSL2 自己内部被访问,WSL2 之外一概不认。即便你做好了 Windows 转发的所有配置,服务只听 127.0.0.1,流量进来了也没进程接收,照样失败。
要让外部流量能进 WSL2,服务必须监听0.0.0.0。0.0.0.0的含义是“本机所有网络接口”,包括回环地址、WSL2 的虚拟网卡地址。监听在这个地址上,WSL2 内部、Windows 主机、经转发后的局域网设备,才都能访问到服务。
拿最常见的几个服务举例:
- Python 自带的 HTTP 服务器:
python3 -m http.server 8080 --bind 0.0.0.0- Flask 开发服务器:
flask run --host=0.0.0.0 --port=8080或者:
app.run(host='0.0.0.0', port=8080)- Node.js:
app.listen(8080, '0.0.0.0', () => { console.log('Server running on port 8080'); });- 如果是 Vite 之类的前端开发服务器,一般在配置文件里加
server.host: true或者--host参数,它就会监听0.0.0.0。
2.2 验证服务到底在哪个地址上监听
很多人在这一步会犯迷糊:明明写了--host=0.0.0.0,怎么还是不行?这时候不要靠猜,直接在 WSL2 里用命令看监听状态:
sudo ss -tlnp | grep 8080如果输出里监听地址是0.0.0.0:8080,说明服务本身没问题。如果显示的是127.0.0.1:8080,说明配置没生效,或者框架有自己独立的配置项覆盖了命令行参数。
这里有个经验分享:我在实际开发中见过太多人把 Flask 的app.run(host='0.0.0.0')写在if __name__ == '__main__'里,但实际跑的时候用了flask run命令,根本没走app.run(),导致监听地址始终是 127.0.0.1。遇到这种情况,别纠结,直接显式传--host=0.0.0.0参数,效果立竿见影。
如果服务在 Docker 容器里,情况又有变化。容器跑在 Docker Desktop for Windows 的 WSL2 后端时,你需要在docker run时做端口映射:
docker run -p 0.0.0.0:8080:8080 your-image-p参数前面是 Windows/WSL2 对外暴露的端口,后面是容器内部的端口。Docker 会自动把流量转到 WSL2 里对应容器的端口,这一步比裸跑进程省心不少,因为 Docker Desktop 已经帮你处理了一部分网络桥接。但同样的道理,如果容器里的服务监听的是 127.0.0.1,映射照样白搭。
2.3 一个容易忽略的点:WSL2 里 systemd 和自启动服务的监听
如果你在 WSL2 里用了 systemd(新版 WSL2 默认开启,或者通过/etc/wsl.conf配置systemd=true开启),并且配置了类似 nginx、apache 这类开机自启服务,注意它们的默认配置往往也是监听127.0.0.1或者只监听 IPv4 回环地址。以 nginx 为例,需要修改/etc/nginx/sites-available/default里的:
server { listen 0.0.0.0:8080; ... }改完记得sudo nginx -s reload。这一步很多人改完忘了重载配置,服务还是旧的监听状态,白白浪费时间。核心思路就一句话:凡是 Web 服务进程,你都得确认它监听在0.0.0.0上,这一步不搞定,后面全白做。
3. 第二步:在 Windows 主机上做端口转发 + 防火墙放行
3.1 用 netsh 建立 Windows 到 WSL2 的端口代理规则
当 WSL2 里的服务已经监听0.0.0.0之后,接下来要解决的核心问题就是:Windows 主机收到局域网设备发来的访问请求后,怎么把这个请求“搬运”到 WSL2 的虚拟网卡上。
这一步靠 Windows 自带的netsh interface portproxy命令就能实现。它的本质是一个端口代理:Windows 在指定的 IP:端口 上监听,收到流量后转发给目标 IP:端口。这里有三个关键参数:
listenaddress:Windows 主机监听哪个地址。要让局域网设备能访问,必须监听0.0.0.0,意思是所有网卡接口都监听。listenport:对外提供服务的端口,也就是局域网设备访问 Windows 主机时用的端口。connectaddress:转发目标是哪个 IP,这里填 WSL2 当前的虚拟网卡 IP。connectport就是 WSL2 里服务监听的端口。
先获取 WSL2 的 IP:
wsl hostname -I假设返回172.20.10.5,服务端口是8080,那就在 Windows PowerShell(管理员权限)里执行:
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8080 connectaddress=172.20.10.5 connectport=8080执行成功后,可以用下面的命令确认规则:
netsh interface portproxy show all看到0.0.0.0 8080到172.20.10.5 8080的转发规则,就说明端口代理已经建立。
3.2 防火墙放行是局域网访问成功的关键
端口转发规则建好了,还有一个最容易卡住人的环节:Windows 防火墙。默认情况下,Windows 防火墙会拦截来自局域网设备对主机端口的入站请求。你需要在高级安全 Windows 防御防火墙里,或者在命令行里,放行对应端口。
命令行操作更直接,管理员权限的 PowerShell 执行:
netsh advfirewall firewall add rule name="WSL2 Web 8080" dir=in action=allow protocol=TCP localport=8080这条命令会在入站规则里增加一条放行 TCP 8080 端口的规则。如果你用的是 Docker Desktop + WSL2 后端,Docker 有时候会自动放行映射的端口,但裸跑 WSL2 里的 Web 服务时,防火墙规则通常得手动加。
这里有一个非常实际的经验:添加完防火墙规则后,最好重启一下 Windows 的防火墙服务,或者至少用netsh advfirewall reset重载一下所有规则,否则偶尔会遇到规则不生效的情况。多数时候我测试下来是立即生效的,但如果你发现局域网设备还是连不上,优先级最高的排查动作,就是先临时关闭防火墙测一次。
注意:关闭防火墙测试只适合在可控环境里临时验证。真实场景下开了端口到局域网,意味着整个局域网里任何一台设备都能访问这个端口,如果服务没有鉴权,会暴露内部信息。我在自己机器上测试时,用完会立刻删除规则:
netsh advfirewall firewall delete rule name="WSL2 Web 8080" netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=80803.3 本机访问的完整链路,和局域网访问的链路对比
到这里,两条访问路径就都通了:
- 本机访问:Windows
localhost:8080→ localhost forwarding → WSL2 的172.20.10.5:8080。 - 局域网访问:局域网设备
Windows 主机 IP:8080→ Windows 防火墙放行 → netsh portproxy 转发 → WSL2 的172.20.10.5:8080。
画成文字流程,区别就非常清楚。前者走的是一条 Windows 专门为 WSL2 开辟的“内部快速路”,后者走的是“外部大门进、内部转送”的流程。后者缺了任何一环,局域网设备就访问不到。
还有一种情况要提一下:如果你的 WSL2 服务还需要其他端口(比如前端 3000、后端 8080),那每个端口都要单独加一条 portproxy 规则和防火墙规则。嫌麻烦的话可以直接写个脚本一次配完,脚本我放在下一节。
4. 一套脚本搞定动态 IP、多条规则、频繁重启的问题
4.1 为什么手动配置总是不够用
手动执行 netsh 命令适合一次性临时使用,但有两个明显的痛点:
- WSL2 的 IP 是动态的,每次重启 WSL2 都可能变。一旦 IP 变了,portproxy 规则里写死的
connectaddress就失效了。 - 端口多的时候,每次都要重复敲命令,容易漏配、配错。
解决思路不复杂:写一个 PowerShell 脚本,每次 WSL2 启动后自动执行一次,脚本内部先动态获取 WSL2 当前 IP,再根据这个 IP 重建所有 portproxy 规则和防火墙规则。这样 Windows 重启、WSL2 重启后,只需跑一次脚本,全部规则自动恢复。
4.2 从获取 IP 到写入规则,完整脚本拆解
下面是我自己一直在用的脚本,注释写得比较详细,你可以直接保存为setup-wsl2-portproxy.ps1:
# 需要管理员权限运行 # 作用:将 WSL2 内的服务端口转发到 Windows 主机,并放行防火墙 # 1. 获取 WSL2 当前 IP $wslIp = (wsl hostname -I).Trim() Write-Host "WSL2 IP: $wslIp" -ForegroundColor Green # 2. 清空旧规则(防止 IP 变化后残留失效规则) netsh interface portproxy reset # 3. 定义要转发的端口列表:前端、后端等 $ports = @( @{ Listen = 8080; Connect = 8080 }, @{ Listen = 3000; Connect = 3000 } ) foreach ($p in $ports) { # 4. 添加端口代理规则 netsh interface portproxy add v4tov4 ` listenaddress=0.0.0.0 ` listenport=$($p.Listen) ` connectaddress=$wslIp ` connectport=$($p.Connect) # 5. 添加防火墙入站放行规则(如果已存在会报错,但不影响) netsh advfirewall firewall delete rule name="WSL2-Port-$($p.Listen)" 2>$null netsh advfirewall firewall add rule ` name="WSL2-Port-$($p.Listen)" ` dir=in ` action=allow ` protocol=TCP ` localport=$($p.Listen) } # 6. 显示最终规则,方便确认 netsh interface portproxy show all Write-Host "Port forwarding setup complete." -ForegroundColor Green这段脚本有几个地方值得说明:
netsh interface portproxy reset会清空所有 portproxy 规则。如果你机器上还有其他用途的转发规则,先确认一下有没有影响。我在自己环境里是专门拿出一台开发机跑这套脚本,所以直接 reset 没有问题。如果你想保守一点,可以改成手动遍历删除指定 listenport 的规则。- 防火墙规则删除时用了
2>$null,避免第一次执行时因为没有旧规则而报错刷屏。 - 脚本里写了
$ports数组,你可以按需增删端口,前端、后端、API 一起配好,一劳永逸。
4.3 每次 WSL2 启动后自动执行脚本
脚本写好了,得让它自动跑,否则换 IP 之后还得手动打开 PowerShell 执行,违背“彻底搞懂”的初衷。
Windows 自带“任务计划程序”可以做到这一点。创建一个新任务,触发器设为“登录时”或者“特定事件”,操作里选择启动 PowerShell,参数填:
-ExecutionPolicy Bypass -File "C:\scripts\setup-wsl2-portproxy.ps1"勾选“使用最高权限运行”,因为 netsh 命令需要管理员权限。任务计划程序默认用最高权限执行时,不需要每次弹 UAC 确认,比较省心。
如果你是在每次手动开 WSL2 的时候才需要局域网访问,也可以在 WSL2 的~/.bashrc里写一行,WSL 每次启动时自动触发 Windows 侧的脚本:
/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy Bypass -File 'C:\scripts\setup-wsl2-portproxy.ps1'这个方法的好处是更“按需执行”,缺点是每次进 WSL2 终端都会触一次。我个人的习惯是用任务计划程序绑定 Windows 登录事件,一次配置,长期有效。
还有一个升级玩法:如果你担心 WSL2 的 IP 变化导致规则连接失效,可以再加一条逻辑,用 Windows 自带的“任务计划程序”配合事件查看器,监听 WSL2 启动的事件 ID 然后触发脚本。不过这个属于进阶优化,大多数开发场景下登录时执行一次就够用了。
5. 常见问题排查与排错记录
5.1 WSL2 本身起不来:虚拟化未启用怎么处理
折腾 WSL2 访问之前,先确认 WSL2 能正常跑。很多人遇到的是启动 WSL 时报错“Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS”。
这个问题的根源是虚拟化没开。排查顺序如下:
- 打开 PowerShell,执行
systeminfo,看 Hyper-V 要求那一栏是不是全为“是”。如果显示“已检测到固件中未启用虚拟化”,需要进 BIOS 开启 Intel VT-x(Intel)或 AMD-V(AMD)。 - 确认 Windows 功能里“适用于 Linux 的 Windows 子系统”和“虚拟机平台”这两个选项都是勾选状态。
- 在管理员 PowerShell 里执行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart- 重启电脑再试。
这条排错路径我每次重装开发机都要走一遍,十次里有八次是 BIOS 虚拟化没开,剩下两次是功能没勾全。所以先看 BIOS,再看 Windows 功能,基本能覆盖绝大多数情况。
5.2 局域网设备访问超时,先从这四个点查
如果你已经按上面的两步法配完了,手机还是打不开,别急着怀疑路由器。按顺序排查:
第一,确认服务进程确实监听在 0.0.0.0。回到 WSL2 里执行sudo ss -tlnp | grep 端口,看监听地址。如果还是 127.0.0.1,回到本文第二节重新配置服务。
第二,确认 Windows 主机的 IP 和设备在同一个局域网。手机或另一台电脑访问的地址是 Windows 主机的物理网卡 IP,不是 WSL2 的 IP。你可以在 Windows 里执行ipconfig,找到“无线局域网适配器”或“以太网适配器”下的 IPv4 地址,把这个地址给局域网设备访问。
第三,确认防火墙规则添加成功,并且没有其他安全软件拦截。有些第三方安全软件会接管 Windows 防火墙,导致 netsh 加的规则被忽略。我自己遇到过一次,关了第三方防护软件,局域网访问立刻恢复。
第四,确认路由器没有开启“AP 隔离”之类的功能。AP 隔离会让同一个 Wi-Fi 下的设备互相不可见,这在公共 Wi-Fi 或访客网络里很常见。如果是公司或学校网络,还可能存在 vlan 隔离,这种情况属于网络策略限制,代码层面解决不了,只能找网管协调。
5.3 排查信息速查表,五分钟定位问题
我把平常排查的要点整理成了一张表,遇到问题照着看就行:
| 现象 | 可能原因 | 排查命令/动作 |
|---|---|---|
| WSL2 启动报虚拟化错误 | BIOS 未开虚拟化 / Windows 功能没开 | systeminfo查看 Hyper-V 要求,检查固件设置 |
| 服务在 WSL2 内部能访问,Windows 本机访问不了 | 服务监听了 127.0.0.1 | sudo ss -tlnp查看监听地址,改为 0.0.0.0 |
| Windows 本机 localhost 能访问,局域网设备访问不了 | 防火墙拦截 / 没做 portproxy / WSL2 IP 变了 | netsh interface portproxy show all检查规则,win+r输入wf.msc查看入站规则 |
| 局域网设备能 telnet 通端口,但页面打不开 | 服务本身异常 / 监听地址不对 / 端口不一致 | 检查服务日志,回 WSL2 里用 curl 验证 |
| 之前能访问,WSL2 重启后失效 | WSL2 IP 变了,portproxy 里 connectaddress 失效 | 重新执行脚本,或者用任务计划程序自动化 |
| netsh 命令提示请求的操作需要提升 | 权限不足 | 以管理员身份运行 PowerShell |
5.4 一些额外的小经验:从 WSL2 里反查 Windows IP,以及用 curl 验证
排查时经常需要快速确认 WSL2 到 Windows、Windows 到 WSL2 的连通性。我常用的方式是:
在 WSL2 里访问 Windows 主机,可以用默认网关地址:
ip route show default输出里的default via 172.20.10.1就是 Windows 在 WSL2 NAT 网络里的 IP。浏览器或者 curl 访问http://172.20.10.1:8080能通,说明 Windows 到 WSL2 的转发链路没问题。
在 Windows 里验证 WSL2 的端口连通性:
curl http://<wslIp>:8080如果 Windows 这边 curl 通,但局域网设备不通,问题基本锁定在防火墙或者路由层面。如果 Windows 这边 curl 都不通,先检查服务监听和 portproxy 规则。
还有一个容易被忽略的点:WSL2 的网络配置有时候会因为 Windows 更新或者 WSL 版本升级而重置。遇到“昨天还好好的,今天突然不行了”,先重新跑一遍脚本,大概率能解决。别花太多时间深挖底层原因,WSL2 的动态 IP 机制决定了这种“配置漂移”很难完全避免。
6. 写在最后的一点实操感受
这套两步法我在自己机器上反复验证过很多次,从最早手动配 netsh 到后来写脚本自动执行,核心思路一直没变过:服务监听 0.0.0.0,Windows 做端口转发和防火墙放行。只要把这两步理解透了,不管是 Flask、Node、nginx 还是 Docker 容器里的服务,都能通过同一种方式暴露给局域网。
最后再分享一个我踩过很多次坑之后养成的小习惯:每次配好转发规则,我会先在 WSL2 里curl确认服务正常,再在 Windows 里curl确认转发正常,最后才用手机访问测试。这样一层层验证,出了问题能立刻缩小到具体哪一环,比直接拿手机反复试高效得多。希望这篇文章能帮你少走一些弯路。