
WSL2 装好之后,大部分人停在能在 Windows 里敲 Linux 命令这一步就满足了。但只要你有第二台机器——哪怕只是同一张桌子上的另一台笔记本、一台平板,或者公司里那台常年开着的编译服务器——你很快就会碰到一个很具体的需求:我要从别的设备直接连进这台 WSL2,而不是每次都先远程桌面到 Windows 再开个终端窗口。这时候 NAT 模式就开始碍事了。这篇内容讲的就是我在这条路上来回折腾了几轮之后沉淀下来的做法:把 WSL2 的网络入口打通,用 SSH 作为统一入口,再让 VSCode 的 Remote-SSH 直接落在这个入口上。适合已经用过 WSL2、能看懂基本 Linux 命令、但被 IP 漂移和端口转发折磨过的开发者;如果你刚装完 WSL2,建议先跑通基础环境再回来看,不然容易被细节淹没。1. 先想清楚:我为什么要逃离 NAT 模式1.1 那个每次重启都会变的 172 开头的地址WSL2 默认跑在一个轻量虚拟机里,Windows 主机构建了一个虚拟网卡给它做 NAT 转发。你在 WSL2 里执行ip addr,大概率会看到eth0上挂着一个172.x.x.x的地址。这个地址不是你路由器分配的,是 WSL 内部的虚拟 DHCP 给的,而且每次wsl --shutdown之后再启动,它大概率会变。这就是一切麻烦的源头。我最早的做法很朴素:需要从别的机器连进来时,先跳回 Windows 敲一遍wsl hostname -I把地址抄下来,再去另一台机器上ssh user172.x.x.x。第一次还能忍,第三次就开始烦了,因为你永远在重复同一件事,而且这个地址不能写进任何配置文件,更不能写进脚本——写了第二天就失效。真正让我下决心改造的,是我把一份构建脚本扔到另一台机器上跑,脚本里硬编码了 WSL 的地址,隔天早上定时任务全线失败。所以这里要先明确一个判断标准:如果你的使用场景里,连接发起方永远是 Windows 主机自己,那么 NAT 模式完全够用,你甚至不需要任何额外配置,localhost转发已经帮你处理好了。只有当连接发起方是别的设备或者别的容器时,你才需要往下走。1.2 局域网里那台设备连不进来的真实原因很多人第一反应是防火墙,关掉防火墙发现还是连不上,就懵了。其实原因很简单:NAT 模式下,WSL2 的那台虚拟机和你的物理局域网之间隔了一层地址转换。局域网里的设备发包给 Windows 主机的 IP,Windows 收到了,但它不知道该转发给内部的哪台虚拟机,于是直接丢弃。这不是防火墙拦的,是压根没有路由规则。这就解释了为什么在 Windows 主机上ssh localhost能通,换成ssh 192.168.1.x就不通——前者走的是 WSL 自己注册的本地回环转发,后者走的是物理网卡,两条路径完全不同。理解这一点非常关键,后面所有方案的取舍都建立在它之上:我们要做的不是打开某个开关,而是在 NAT 边界上凿一个洞,或者干脆换掉 NAT 这层结构。顺便说一个常见误解:有人会去 WSL2 里执行ip route看到默认网关是172.x.x.1,就以为把这个网关改成路由器地址就能桥接。改完之后大概率是彻底断网,因为 WSL 的虚拟交换机不认外部的 MAC 和路由,恢复起来还得wsl --shutdown重来。这条路我试过,浪费时间,不建议。2. 三条技术路线的真实取舍,别在错误的方向上耗时间2.1 端口转发:最土但最可控的方案netsh interface portproxy是 Windows 自带的端口转发工具,原理是在 Windows 主机上监听一个端口,把流量转发到 WSL2 的内部地址。命令长这样,需要管理员权限的 PowerShell:# 假设 WSL2 当前地址是 172.28.100.5,想把宿主机的 2222 端口转到 WSL 的 22 netsh interface portproxy add v4tov4 listenport2222 listenaddress0.0.0.0 connectport22 connectaddress172.28.100.5配套还要放行防火墙:New-NetFirewallRule -DisplayName WSL SSH 2222 -Direction Inbound -Protocol TCP -LocalPort 2222 -Action Allow这个方案最大的优点是不改变任何网络结构,失败了删掉规则就恢复原样,风险极低。缺点是它把IP 会变这个问题原封不动地留给了你:WSL 重启、地址变了,转发规则还指向旧地址,连接就会超时,而且超时的表现和服务没起来几乎一模一样,排查起来很迷惑。我的处理办法是写一个开机脚本,每次 WSL 启动后自动刷新转发规则。核心逻辑就是取当前地址、删旧规则、加新规则。这个脚本我放在任务计划里,登录时触发。它不优雅,但运行了一年多没出过问题,原因很简单:它把不确定性收敛到了一个地方,你只需要盯住这一个脚本有没有跑成功。2.2 镜像网络模式:最接近桥接体验的官方路子如果你用的是较新的 Windows 11 和较新的 WSL 版本,有一个更干净的选择:镜像网络模式(mirrored networking)。它让 WSL2 直接复用 Windows 主机的网络接口和地址,不再单独做一层 NAT。开启方式是在用户目录下建一个.wslconfig文件,路径是C:\Users\你的用户名\.wslconfig,内容:[wsl2] networkingModemirrored保存后执行wsl --shutdown,等几秒再启动,进去ip addr,你会发现网络接口变了,地址和 Windows 主机在同一网段。此时在 WSL2 里跑的服务,Windows 上用localhost:端口能直接访问,局域网里的其他设备用Windows 主机的 IP 端口也能直接访问,不需要任何portproxy规则。这个模式之所以被很多人叫作桥接,是因为从使用体验上看它确实达到了桥接的效果:同一网段、可直接被外部访问、不用转发。但从实现上说它不是二层桥接,只是把主机的网络栈共享给了 WSL。这个区别在实际使用中通常无关紧要,但在排查问题时很重要——比如你会发现 WSL 里看到的网卡列表和 Windows 上高度一致,包括那些虚拟网卡,这就是共享网络栈的直接体现。镜像模式有一个必须注意的前提:它要求较新的 WSL 版本。可以先跑wsl --version看一眼,如果命令本身报错说不认识,说明版本偏旧,先升级。另外在某些企业环境里,主机的网络策略或安全软件会对这种共享栈的行为有额外限制,表现是开起来之后主机自己断网了,遇到这种情况直接改回默认模式,别硬刚。2.3 想用 Hyper-V 外部虚拟交换机做真桥接?我劝你放弃网上不少教程会告诉你:打开 Hyper-V 管理器,创建一个外部虚拟交换机绑定物理网卡,然后把 WSL 的虚拟网卡挂上去,这样就实现了真正的桥接。这个说法在早期的 Hyper-V 虚拟机上成立,但在 WSL2 上基本行不通。原因是 WSL2 的虚拟网卡由 WSL 自己管理,并不会以可以随意挂载到外部交换机的形式暴露出来,你在 Hyper-V 管理器里往往找不到对应的可操作对象。更糟的是,一旦你在 Hyper-V 里创建了绑定物理网卡的外部交换机,Windows 主机的网络配置可能被改写,轻则 IP 变了,重则直接断网,而恢复过程需要删掉交换机、重置网卡,有时候还得重启。我在这上面栽过一次,当时以为是自己操作不对,反复试了三遍,每次都把主机网络搞挂,最后确认这条路在 WSL2 上就是不成立的。把三条路线放在一起对比,结论就很清楚了:方案外部设备可访问IP 是否稳定对主机网络的侵入性恢复难度适用场景netsh 端口转发是,但要放行防火墙否,需脚本刷新低,仅增加转发规则删规则即可老版本系统、企业受限环境镜像网络模式是,直接可达是,跟随主机地址中,改变网络栈结构改回配置重启新版本系统,首选Hyper-V 外部交换机理论可行,实际难不确定高,可能打断主机网络高,常需重启修复不建议我的实际选择是:主力机用镜像网络模式,备用机上留一套端口转发脚本兜底。两套方案并存听起来冗余,但正是这套冗余让我在换机器、重装系统、版本回退的几种情况下都没有卡住过。3. 让 sshd 在 WSL2 里真正跑起来并活过重启3.1 安装与 sshd_config 里真正需要改的那几行网络通了只是修好了路,还得有个服务在门口收件。WSL2 的发行版里默认通常没装 SSH 服务端,先装:sudo apt update sudo apt install -y openssh-server装完之后先别急着改配置,直接sudo service ssh start,然后在 Windows 上试着连一次。先跑通默认配置,再逐项加固,这个顺序很重要。我见过太多人一上来就把PermitRootLogin、PasswordAuthentication、AllowUsers全改了,结果连不上,还得回头一项项排查到底是哪一行出的问题。默认配置跑通之后,打开/etc/ssh/sshd_config,真正需要动的其实就三处:# 明确监听所有地址,排除只绑回环的可能 ListenAddress 0.0.0.0 # 端口按需改,避开 Windows 主机上可能已被占用的 22 Port 2222 # 密钥登录为主,密码登录视安全要求关闭 PubkeyAuthentication yes PasswordAuthentication no这里解释一下为什么把端口改掉。Windows 10/11 自带一个可选的 OpenSSH Server 功能,如果你之前手滑装过并启用了它,Windows 侧的 22 端口就被占了。此时你在 WSL 里也开 22,再用端口转发或者镜像模式,就会出现连接上了但认证的是 Windows 那个服务的诡异现象——表现是用户名密码怎么输都不对,而且报错信息完全看不出是连错了地方。换一个非标准端口(2222、2202之类)能一劳永逸地躲开这个坑。改完配置先用sudo sshd -t做语法检查,这个命令只校验不启动,能提前发现拼写错误。确认无误再重启服务。3.2 密钥登录:那两个权限数字比密钥本身更容易出错生成密钥对在 Windows 侧做就行,ssh-keygen -t ed25519 -f $HOME\.ssh\id_ed25519_wsl,然后把公钥内容追加到 WSL2 里的~/.ssh/authorized_keys。这一步看起来简单,但权限问题几乎人人都踩过。OpenSSH 对权限的检查非常严格,而且它的检查逻辑在服务端:如果~/.ssh或authorized_keys的权限过于宽松(比如组用户或其他用户可写),它会静默拒绝使用这个密钥,日志里可能只有一句很含糊的 Authentication refused: bad ownership or modes。修复方式:chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys还有两个容易忽略的点。第一,~/.ssh的所有上级目录也不能对组用户或其他用户可写,如果你把家目录权限改成了777,密钥同样会失效。第二,如果 WSL2 里的用户是通过 Windows 用户映射进来的,注意authorized_keys要放在实际登录用户的家目录下,而不是 root 的目录下。我见过有人把公钥追加到了sudo之后的 root 家目录,然后一直困惑为什么用普通用户登录不进去。密钥验证是否真的生效,有个快速判断方法:ssh -v看输出里有没有Offering public key以及紧随其后的Server accepts key。如果只有前者没有后者,基本就是服务端权限检查没过;如果连前者都没有,说明客户端的IdentityFile路径写错了。这个二分法能省掉大量瞎猜的时间。3.3 开机自启:别指望手动service ssh startWSL2 的实例在你不使用时会自动休眠,下次访问时重新拉起。这意味着你手动start的 sshd,在下一次实例重启后就不在了。表现是今天配好了,明天早上连不上,进去一看进程没了。这个问题如果不解决,整套配置就是一次性的。两种接管方式。第一种是启用 systemd,在/etc/wsl.conf里写:[boot] systemdtrue保存后wsl --shutdown,重进,systemctl status能正常输出就说明 systemd 起来了,然后sudo systemctl enable --now ssh即可实现开机自启。这是目前更推荐的方式,因为很多其他服务也依赖 systemd。第二种是不启用 systemd,直接用wsl.conf的 boot 命令:[boot] command service ssh start这个方式更轻,不改变 init 系统,适合那些不想动 systemd 的环境。缺点是它只能执行单条命令,想同时启动多个服务就得包一层脚本。我的建议是:如果你的发行版和 WSL 版本支持 systemd,就用 systemd。理由不是 systemd 更好,而是生态更一致——你在别的 Linux 机器上学到的systemctl enable经验可以直接复用,不用为 WSL 单独记一套。4. 把 VSCode 的 Remote-SSH 接到这个入口上4.1 config 文件怎么写才不用每次输一堆参数VSCode 的 Remote-SSH 扩展读取的是标准 SSH 配置文件,Windows 侧路径是C:\Users\用户名\.ssh\config。写好之后 VSCode 左侧远程资源管理器里会直接列出这个主机名,点一下就能连:Host wsl-dev HostName 127.0.0.1 Port 2222 User devuser IdentityFile ~/.ssh/id_ed25519_wsl ServerAliveInterval 30 ServerAliveCountMax 3这里有几个参数值得说明。HostName填127.0.0.1是配合端口转发或镜像模式的写法,因为最终暴露端口的是 Windows 主机;如果你走的是纯局域网直连,就填主机的局域网 IP。ServerAliveInterval 30是每 30 秒发一次心跳,配合ServerAliveCountMax 3,能在网络短暂抖动时避免连接被判定为断开——WSL2 实例休眠再唤醒的场景下,这个设置能显著减少连接已关闭的提示频率。IdentityFile里的~在 Windows 的 SSH 客户端下会解析到用户目录,一般没问题,但如果你在 VSCode 里遇到找不到密钥的报错,直接改成绝对路径C:/Users/xxx/.ssh/id_ed25519_wsl,注意用正斜杠。还有一个细节:改完 config 之后,如果连接行为没变化,可能是 VSCode 缓存了旧配置。这时候执行一次命令面板里的Remote-SSH: Kill VS Code Server on Host,清掉远端的服务端进程,再重连,通常就恢复正常了。4.2 首次连接会遇到的三道坎第一道是 host key 确认。首次连接会弹出一个指纹确认,选继续即可。但如果你之前用同一个 IP 和端口连过别的机器,会遇到REMOTE HOST IDENTIFICATION HAS CHANGED的硬性拒绝。这是因为旧指纹还留在known_hosts里。处理方式是ssh-keygen -R [127.0.0.1]:2222,注意带端口时要加方括号和引号,不带端口的话格式不同,写错了删不掉。这个报错看起来吓人,实际上是保护机制在正常工作,不用紧张。第二道是远端服务端安装。Remote-SSH 连接成功后,会在远端自动下载并安装一个轻量的服务端组件。这个过程需要远端能访问下载源。如果 WSL2 里的网络出口有问题,就会卡在 Setting up SSH Host 这一步很久最后失败。遇到这种情况,可以看 VSCode 的输出面板里 Remote-SSH 通道的具体报错,通常会给出下载地址和失败原因。实在装不上,也可以手动把安装包放进远端指定目录,不过这条路比较绕,优先解决网络出口问题。第三道是扩展的安装位置。这是新手最容易困惑的地方:Remote-SSH 连上之后,你的工作区在远端,大部分扩展需要安装到远端。在扩展面板里,已安装的扩展会显示一个在 SSH: wsl-dev 上安装的按钮,点一下才会在远端生效。像语言服务、调试器、格式化工具这些必须装远端;主题、快捷键这类界面相关的装本地就行。搞混了的表现是扩展明明装了但就是不工作,排查半天发现装错了位置。4.3 WSL 扩展和 Remote-SSH,到底该用哪个这里必须说清楚一个容易混淆的点:VSCode 有一个官方的 WSL 扩展,它直接对接 WSL2 实例,不需要 SSH,也不需要网络配置,打开文件夹时选 Connect to WSL 就行。那既然有这个,为什么还要折腾 Remote-SSH?答案是使用场景不同。WSL 扩展要求 VSCode 跑在 Windows 主机上,它通过本地管道和 WSL 通信。如果你的开发机就是这台 Windows,那 WSL 扩展是更省事的选择,网络部分完全不用管。但如果你的场景是:代码跑在 A 机器的 WSL2 里,而你在 B 机器上写代码,那 WSL 扩展就无能为力了,因为它只能连本机的 WSL。这时候 Remote-SSH 才是正解,因为它本质上是通过 SSH 连一台 Linux 机器,至于那台机器是物理机、虚拟机还是 WSL2,它并不关心。我自己的用法是两者并存:坐在主力机前的时候,用 WSL 扩展,启动快、零配置;出门用笔记本的时候,用 Remote-SSH 连回主力机的 WSL2。两个入口指向同一份代码,互不干扰。需要注意的是,不要在两边同时打开同一个项目并同时修改,虽然不会立刻冲突,但文件监听和自动格式化可能互相打架,造成难以复现的奇怪问题。5. 那些让我卡了半天的坑,以及我的排查顺序5.1 服务在跑但连不上:从监听地址开始查最典型的现象:WSL2 里ps aux | grep sshd明明有进程,service ssh status也说 running,但外面就是连不上。这时候别急着怀疑网络,先确认它到底监听在哪个地址上:ss -tlnp | grep sshd如果输出是127.0.0.1:2222而不是0.0.0.0:2222或[::]:2222,那问题就找到了——它只监听回环,外部流量进不来。原因通常是sshd_config里有ListenAddress 127.0.0.1的残留,或者被某个覆盖配置改掉了。改成0.0.0.0重启即可。我的排查顺序固定为四步,基本能覆盖九成情况:第一步看监听地址(ss -tlnp),第二步从本机连一次(ssh -p 2222 user127.0.0.1,排除认证问题),第三步从 Windows 连一次(区分是 WSL 内部问题还是转发问题),第四步从局域网第三台设备连一次(区分是主机防火墙问题还是路由问题)。每一步只增加一个变量,这样一旦某一步失败,问题范围立刻缩小一半。5.2 开了镜像模式,宿主机反而连不上了这个坑很反直觉:改配置之前localhost能连,改成镜像模式之后反而连不上了。原因通常是端口冲突——镜像模式下,WSL2 和 Windows 主机共享网络端口空间,如果 Windows 上已经有进程占了那个端口(比如前面提过的 Windows 自带 SSH 服务占了 22),WSL 里的服务就绑不上,或者绑上了但流量被 Windows 那个进程接走了。验证方法很直接:在 Windows 上netstat -ano | findstr :2222,看有没有别的 PID 占着。有的话要么换端口,要么把占用进程停掉。我现在的习惯是给 WSL 的 SSH 固定用一个不常见的端口,彻底避开这类冲突,省得每次都要查。还有一种情况是防火墙的配置文件区分。Windows 防火墙对专用网络和公用网络的规则是分开的,你放行了专用网络,但当前网卡被识别成公用网络,规则就不生效。表现是主机自己能连、局域网连不上。检查方式是看网络属性里的配置文件类型,如果显示公用,要么切换成专用,要么在防火墙规则里把公用网络也勾上。5.3 IP 漂移导致的间歇性失败,以及我怎么把它固定下来端口转发方案下,最烦的不是配置麻烦,而是它会在你完全没预期的时候失效。某天早上连不上,进去一看,WSL 地址从172.28.100.5变成了172.30.200.8,转发规则还指着旧地址。这种失败没有任何报错提示,只有超时,很容易误判成服务挂了。我的解决办法是把刷新逻辑写成一个 PowerShell 脚本,登录时自动执行:$wslIp (wsl hostname -I).Trim().Split( )[0] if ($wslIp) { netsh interface portproxy delete v4tov4 listenport2222 listenaddress0.0.0.0 2$null netsh interface portproxy add v4tov4 listenport2222 listenaddress0.0.0.0 connectport2222 connectaddress$wslIp Write-Output 已刷新转发规则 - $wslIp }这个脚本的关键在于先删后加,因为add一个已存在的规则会报错,而delete一个不存在的规则也会报错,所以用2$null把删除时的噪音吞掉。脚本执行完可以用netsh interface portproxy show all确认当前规则指向的地址是不是最新的。说到底,这个脚本解决的不是技术问题,而是把需要人工记住的事变成机器自动做的事。这类自动化在小规模环境里往往被忽略,觉得手动改一下也就半分钟,但半分钟乘以一年两百次,加上每次都要重新回忆命令语法的时间成本,账就不一样了。6. 跑稳之后我把配置固化下来的几个习惯配置这东西,只要不落到文件里,过三个月自己都想不起来当初改了什么。我现在给自己定了两条规矩:第一,所有非默认的改动都写进版本管理的 dotfiles 仓库,包括.wslconfig、sshd_config的差异部分、Windows 侧的 SSH config 和那个刷新脚本;第二,在仓库根目录放一个README,只写为什么这么配而不写怎么配,因为怎么配网上到处都有,为什么这么配才是三个月后真正需要的信息。具体到版本管理,sshd_config我不直接覆盖系统文件,而是维护一份片段,用的时候手动合并或用配置片段目录加载。这样做的好处是系统升级时不会因为文件被改过而弹出一堆交互询问。.wslconfig直接在仓库里放一份完整版本,换机器时拷过去就行,内容少,维护成本低。还有一个容易被忽略的固化点:把验证步骤写成脚本。我写了一个很短的健康检查脚本,依次检查 sshd 进程、监听地址、Windows 侧转发规则、防火墙规则是否存在,全部通过就打印一行 OK。每次换机器或者系统大版本更新后跑一遍,比手动一项项确认快得多,也避免了以为配好了其实漏了一步的尴尬。最后分享一个我在反复重装中总结出来的小经验:先跑通最小路径,再逐层加固。最小路径指的是:默认 sshd 配置 密码登录 端口转发,能在另一台机器上ssh进去看到提示符,这一步通了,后面所有的密钥、端口改号、systemd 接管、镜像模式,都只是在这条已验证的路径上做替换,任何一步出问题都可以立刻回退到上一步可用的状态。反过来,如果一上来就把所有优化项全堆上去,一旦不通,你面对的是一堆未知变量的组合,排查成本会翻好几倍。这套思路我在别的环境配置上也一直在用,屡试不爽。