WSL2 无人值守远程访问踩坑:SSH 自启、localhost 转发与 keepalive 保活
WSL2 无人值守远程访问踩坑:SSH 自启、localhost 转发与 keepalive 保活
0. 太长不读:最终可用方案
本文最终方案不是把 Windows 端口转发到 WSL2 的 172.x.x.x,而是转发到 Windows 本机的 127.0.0.1:<SSH_PORT>:
外部机器
-> Windows 对外 IP:<SSH_PORT>
-> Windows portproxy
-> 127.0.0.1:<SSH_PORT>
-> WSL2 localhost forwarding
-> WSL2 Ubuntu sshd
同时,需要在 Windows 侧保持一个长期运行的 wsl.exe,否则 WSL2 空闲后可能被 Windows 回收,导致 127.0.0.1:<SSH_PORT> 消失。
0.1 需要替换的占位符
下面命令中的占位符需要先替换成自己的实际值:
<DISTRO> WSL 发行版名,例如 Ubuntu-22.04
<LINUX_USER> WSL 内 Linux 用户名
<WIN_HOST> Windows 主机名
<WIN_USER> Windows 用户名
<SSH_PORT> SSH 端口,例如 60001
<LAN_IP> Windows 局域网 IP
<VPN_IP> Windows VPN / ZeroTier / Tailscale IP
注意:<SSH_PORT> 在 PowerShell 脚本里要替换成纯数字,不要保留尖括号。
0.2 WSL2 内部开启 SSH
进入 WSL:
wsl -d <DISTRO>
在 Ubuntu 内执行:
sudo apt update
sudo apt install -y openssh-server
sudo vim /etc/ssh/sshd_config
配置 SSH:
Port <SSH_PORT>
ListenAddress 0.0.0.0
ListenAddress ::
PasswordAuthentication yes
PermitRootLogin no
检查并启动:
sudo mkdir -p /run/sshd
sudo sshd -t
sudo systemctl restart ssh 2>/dev/null || sudo service ssh restart
ss -lntp | grep <SSH_PORT>
应看到:
LISTEN 0 128 0.0.0.0:<SSH_PORT>
LISTEN 0 128 [::]:<SSH_PORT>
0.3 先验证 Windows 能否连接 WSL2 localhost
回到 Windows PowerShell,先测试:
ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
如果这一步能进入 WSL2,说明 WSL2 的 localhost forwarding 是通的。
如果这一步不通,后面的 portproxy 没必要继续做,应该先排查 WSL 内部 sshd 或 localhost forwarding。
0.4 Windows portproxy 转发到 127.0.0.1
管理员 PowerShell 执行:
netsh interface portproxy delete v4tov4 listenaddress=<LAN_IP> listenport=<SSH_PORT>
netsh interface portproxy delete v4tov4 listenaddress=<VPN_IP> listenport=<SSH_PORT>
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=<SSH_PORT>
添加转发:
netsh interface portproxy add v4tov4 listenaddress=<LAN_IP> listenport=<SSH_PORT> connectaddress=127.0.0.1 connectport=<SSH_PORT>
netsh interface portproxy add v4tov4 listenaddress=<VPN_IP> listenport=<SSH_PORT> connectaddress=127.0.0.1 connectport=<SSH_PORT>
检查:
netsh interface portproxy show all
正确结果类似:
侦听 ipv4: 连接到 ipv4:
地址 端口 地址 端口
--------------- ---------- --------------- ----------
<LAN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
<VPN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
防火墙放行:
New-NetFirewallRule -DisplayName "WSL2 SSH LocalhostForward" -Direction Inbound -Action Allow -Protocol TCP -LocalPort <SSH_PORT> -Profile Any
不建议写成:
0.0.0.0:<SSH_PORT> -> 127.0.0.1:<SSH_PORT>
因为 0.0.0.0 可能覆盖 localhost,导致转发冲突或循环。建议明确监听具体外部入口 IP。
0.5 创建 Windows 侧 WSL2 保活任务
这是核心。保活任务的作用是长期保持一个不退出的 wsl.exe,防止 WSL2 被 Windows 回收。
管理员 PowerShell 执行:
Unregister-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive" -Confirm:$false -ErrorAction SilentlyContinue
$Action = New-ScheduledTaskAction `
-Execute "$env:WINDIR\System32\wsl.exe" `
-Argument '-d <DISTRO> -u root -- /bin/bash -lc "mkdir -p /run/sshd; systemctl restart ssh 2>/dev/null || service ssh restart; exec /bin/sleep infinity"'
$Trigger1 = New-ScheduledTaskTrigger -AtStartup
$Trigger2 = New-ScheduledTaskTrigger -AtLogOn -User "<WIN_HOST>\<WIN_USER>"
$Settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
-RestartCount 999 `
-RestartInterval (New-TimeSpan -Minutes 1)
$pwd = Read-Host "输入 Windows 用户密码" -AsSecureString
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd)
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
Register-ScheduledTask `
-TaskName "Keep WSL2 Ubuntu Alive" `
-Action $Action `
-Trigger @($Trigger1, $Trigger2) `
-Settings $Settings `
-User "<WIN_HOST>\<WIN_USER>" `
-Password $plain `
-RunLevel Highest `
-Force
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
启动任务:
Start-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive"
检查:
Get-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive" | Select-Object TaskName, State
正确状态必须是:
Keep WSL2 Ubuntu Alive Running
如果是 Ready,说明保活进程已经退出,WSL2 仍可能被回收。
0.6 创建 SSH 与 portproxy 刷新脚本
保活任务只负责让 WSL2 不休眠。还需要一个刷新脚本负责启动 sshd、重建 portproxy、防火墙规则。
创建脚本:
mkdir C:\Users\<WIN_USER>\scripts -Force
notepad C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1
脚本内容如下:
$ErrorActionPreference = "Continue"
$Distro = "<DISTRO>"
$ListenPort = <SSH_PORT>
$ListenAddresses = @(
"<LAN_IP>",
"<VPN_IP>"
)
$WslExe = "$env:WINDIR\System32\wsl.exe"
$RuleName = "WSL2 SSH LocalhostForward"
$LogFile = "C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.log"
function Write-Log {
param([string]$Msg)
$time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$time $Msg" | Out-File -FilePath $LogFile -Append -Encoding utf8
}
Write-Log "===== Start WSL2 SSH localhost-forward setup ====="
Start-Service iphlpsvc -ErrorAction SilentlyContinue
Write-Log "iphlpsvc checked."
& $WslExe -d $Distro -u root -- /bin/bash -lc "mkdir -p /run/sshd; systemctl restart ssh 2>/dev/null || service ssh restart"
Start-Sleep -Seconds 5
Write-Log "WSL and ssh restarted."
$ok = $false
for ($i = 0; $i -lt 30; $i++) {
$test = Test-NetConnection 127.0.0.1 -Port $ListenPort -InformationLevel Quiet
if ($test) {
$ok = $true
Write-Log "127.0.0.1:$ListenPort is reachable."
break
}
Write-Log "Waiting for 127.0.0.1:$ListenPort ..."
Start-Sleep -Seconds 2
}
if (-not $ok) {
Write-Log "WARNING: 127.0.0.1:$ListenPort not reachable yet."
}
foreach ($addr in $ListenAddresses) {
netsh interface portproxy delete v4tov4 listenaddress=$addr listenport=$ListenPort | Out-Null
}
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=$ListenPort | Out-Null
foreach ($addr in $ListenAddresses) {
netsh interface portproxy add v4tov4 listenaddress=$addr listenport=$ListenPort connectaddress=127.0.0.1 connectport=$ListenPort
Write-Log "Portproxy added: ${addr}:$ListenPort -> 127.0.0.1:$ListenPort"
}
if (-not (Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort $ListenPort -Profile Any | Out-Null
Write-Log "Firewall rule created."
} else {
Set-NetFirewallRule -DisplayName $RuleName -Enabled True -Action Allow -Profile Any
Write-Log "Firewall rule enabled."
}
$proxyInfo = netsh interface portproxy show all
Write-Log "Current portproxy:"
$proxyInfo | Out-File -FilePath $LogFile -Append -Encoding utf8
Write-Log "===== Done ====="
手动运行一次:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1
0.7 创建开机与登录刷新任务
登录后刷新:
schtasks /Create /TN "Start WSL2 SSH Localhost" /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1" /SC ONLOGON /RL HIGHEST /F
开机后刷新:
schtasks /Create /TN "Start WSL2 SSH Localhost OnBoot" /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1" /SC ONSTART /DELAY 0003:00 /RL HIGHEST /RU "<WIN_HOST>\<WIN_USER>" /RP * /F
这里输入的是 Windows 用户密码,不是 WSL 密码。如果平时只用 PIN 登录,需要确保 Windows 账户本身有密码。
0.8 最终检查
查看计划任务:
Get-ScheduledTask | Where-Object {
$_.TaskName -like "*WSL*" -or $_.TaskName -like "*Ubuntu*"
} | Select-Object TaskName, State
正确状态类似:
TaskName State
-------- -----
Keep WSL2 Ubuntu Alive Running
Start WSL2 SSH Localhost Ready
Start WSL2 SSH Localhost OnBoot Ready
查看 portproxy:
netsh interface portproxy show all
应看到:
<LAN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
<VPN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
测试:
ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
ssh -p <SSH_PORT> <LINUX_USER>@<LAN_IP>
ssh -p <SSH_PORT> <LINUX_USER>@<VPN_IP>
重启 Windows 后,再确认:
Get-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive" | Select-Object TaskName, State
只要它长期保持:
Running
WSL2 一般就不会再因为空闲被回收。
主要细节我会改这几处:
<SSH_PORT>这种占位符在 PowerShell 脚本里不能直接运行,要明确写“发布前替换成数字,例如 60001”。- 需要补一句:本方案依赖 WSL2 的
localhostForwarding正常工作,必须先验证ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1。 0.0.0.0 -> 127.0.0.1不建议写,这个判断要保留。- “systemd keepalive 不够”可以保留,但不要展开太多,重点说“最终要 Windows 侧 wsl.exe 常驻”。
- 计划任务里
/RP *或脚本注册方式要求 Windows 账户密码,PIN 不行,这个要说明。
1. WSL2 内部开启 SSH
进入 WSL2:
wsl -d <DISTRO>
安装 OpenSSH Server:
sudo apt update
sudo apt install -y openssh-server
编辑 SSH 配置:
sudo vim /etc/ssh/sshd_config
建议明确写入:
Port <SSH_PORT>
ListenAddress 0.0.0.0
ListenAddress ::
PasswordAuthentication yes
PermitRootLogin no
检查并启动 SSH:
sudo mkdir -p /run/sshd
sudo sshd -t
sudo systemctl restart ssh 2>/dev/null || sudo service ssh restart
ss -lntp | grep <SSH_PORT>
应看到类似:
LISTEN 0 128 0.0.0.0:<SSH_PORT>
LISTEN 0 128 [::]:<SSH_PORT>
然后回到 Windows PowerShell,先验证最关键的一步:
ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
如果这一步能进入 WSL2,说明 Windows 到 WSL2 的 localhost forwarding 是通的。后面的方案才成立。
2. Windows portproxy 转发到 localhost
在管理员 PowerShell 中清理旧规则:
netsh interface portproxy delete v4tov4 listenaddress=<LAN_IP> listenport=<SSH_PORT>
netsh interface portproxy delete v4tov4 listenaddress=<VPN_IP> listenport=<SSH_PORT>
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=<SSH_PORT>
添加新规则:
netsh interface portproxy add v4tov4 listenaddress=<LAN_IP> listenport=<SSH_PORT> connectaddress=127.0.0.1 connectport=<SSH_PORT>
netsh interface portproxy add v4tov4 listenaddress=<VPN_IP> listenport=<SSH_PORT> connectaddress=127.0.0.1 connectport=<SSH_PORT>
检查:
netsh interface portproxy show all
正确结果应类似:
侦听 ipv4: 连接到 ipv4:
地址 端口 地址 端口
--------------- ---------- --------------- ----------
<LAN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
<VPN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
放行防火墙:
New-NetFirewallRule -DisplayName "WSL2 SSH LocalhostForward" -Direction Inbound -Action Allow -Protocol TCP -LocalPort <SSH_PORT> -Profile Any
测试:
ssh -p <SSH_PORT> <LINUX_USER>@<LAN_IP>
ssh -p <SSH_PORT> <LINUX_USER>@<VPN_IP>
注意:不建议写成:
0.0.0.0:<SSH_PORT> -> 127.0.0.1:<SSH_PORT>
因为 0.0.0.0 可能覆盖 localhost,导致转发冲突或循环。更稳妥的做法是明确监听具体外部入口 IP。
3. 为什么不转发到 WSL2 的 172.x.x.x
很多教程会建议这样做:
Windows IP:<SSH_PORT> -> WSL2_IP:<SSH_PORT>
也就是:
connectaddress=<WSL_IP>
但在我的环境里,WSL 内部明明能看到 sshd 监听:
ss -lntp | grep <SSH_PORT>
WSL 内部自测也正常:
ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
但是 Windows 侧直接连 WSL2 的 172.x.x.x:<SSH_PORT> 会失败:
ssh -p <SSH_PORT> <LINUX_USER>@<WSL_IP>
报错类似:
ssh: connect to host <WSL_IP> port <SSH_PORT>: Connection refused
所以我最后没有继续使用 WSL2 的 172.x.x.x,而是改用 Windows 能稳定访问的:
127.0.0.1:<SSH_PORT>
4. 重启后的真正问题:WSL2 会被回收
完成 portproxy 后,外部 SSH 可以成功进入 WSL2。但重启 Windows 后,可能会出现:
Connection reset
此时检查 portproxy:
netsh interface portproxy show all
规则还在。
检查 Windows 监听:
netstat -ano | findstr <SSH_PORT>
也能看到 Windows 正在监听 <LAN_IP>:<SSH_PORT> 或 <VPN_IP>:<SSH_PORT>。
但本机直接连 localhost 会失败:
ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
报错:
ssh: connect to host 127.0.0.1 port <SSH_PORT>: Connection refused
这说明 portproxy 没坏,真正的问题是后端:
127.0.0.1:<SSH_PORT> 没人监听了。
原因是 WSL2 被 Windows 回收,localhost forwarding 消失。
5. 保活关键:Windows 侧常驻 wsl.exe
在我的环境里,单纯在 WSL 内部用 systemd 跑 sleep infinity 不够。只要关闭 Windows 侧的 WSL 窗口,过一段时间外部还是连不上。
最终有效方案是:在 Windows 侧长期运行一个不退出的 wsl.exe。
手动命令如下:
wsl.exe -d <DISTRO> -u root -- /bin/bash -lc "mkdir -p /run/sshd; systemctl restart ssh 2>/dev/null || service ssh restart; exec /bin/sleep infinity"
重点是最后:
exec /bin/sleep infinity
它会让当前 wsl.exe 一直不退出,从 Windows 侧保持 WSL2 活着。
手动执行时,PowerShell 窗口会一直卡住,这是正常现象。无人值守时,需要把它做成计划任务。
6. 创建 WSL2 保活计划任务
管理员 PowerShell 执行:
Unregister-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive" -Confirm:$false -ErrorAction SilentlyContinue
$Action = New-ScheduledTaskAction `
-Execute "$env:WINDIR\System32\wsl.exe" `
-Argument '-d <DISTRO> -u root -- /bin/bash -lc "mkdir -p /run/sshd; systemctl restart ssh 2>/dev/null || service ssh restart; exec /bin/sleep infinity"'
$Trigger1 = New-ScheduledTaskTrigger -AtStartup
$Trigger2 = New-ScheduledTaskTrigger -AtLogOn -User "<WIN_HOST>\<WIN_USER>"
$Settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
-RestartCount 999 `
-RestartInterval (New-TimeSpan -Minutes 1)
$pwd = Read-Host "输入 Windows 用户密码" -AsSecureString
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd)
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
Register-ScheduledTask `
-TaskName "Keep WSL2 Ubuntu Alive" `
-Action $Action `
-Trigger @($Trigger1, $Trigger2) `
-Settings $Settings `
-User "<WIN_HOST>\<WIN_USER>" `
-Password $plain `
-RunLevel Highest `
-Force
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
注意:这里输入的是 Windows 用户密码,不是 WSL 密码。如果平时只用 PIN 登录,需要确保 Windows 账户本身有密码。
启动任务:
Start-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive"
检查状态:
Get-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive" | Select-Object TaskName, State
正确状态应为:
Keep WSL2 Ubuntu Alive Running
这个任务必须长期是 Running。如果是 Ready,说明保活进程已经退出。
7. 创建 SSH 和 portproxy 刷新脚本
保活任务只负责让 WSL2 不休眠。还需要一个刷新脚本负责:
启动 sshd
确认 127.0.0.1:<SSH_PORT> 可达
重建 portproxy
放行防火墙
创建脚本:
mkdir C:\Users\<WIN_USER>\scripts -Force
notepad C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1
脚本内容如下。注意:实际使用时,把 <DISTRO>、<WIN_USER>、<SSH_PORT>、<LAN_IP>、<VPN_IP> 替换为真实值;<SSH_PORT> 在脚本里应写成数字,不要保留尖括号。
$ErrorActionPreference = "Continue"
$Distro = "<DISTRO>"
$ListenPort = <SSH_PORT>
$ListenAddresses = @(
"<LAN_IP>",
"<VPN_IP>"
)
$WslExe = "$env:WINDIR\System32\wsl.exe"
$RuleName = "WSL2 SSH LocalhostForward"
$LogFile = "C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.log"
function Write-Log {
param([string]$Msg)
$time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$time $Msg" | Out-File -FilePath $LogFile -Append -Encoding utf8
}
Write-Log "===== Start WSL2 SSH localhost-forward setup ====="
Start-Service iphlpsvc -ErrorAction SilentlyContinue
Write-Log "iphlpsvc checked."
& $WslExe -d $Distro -u root -- /bin/bash -lc "mkdir -p /run/sshd; systemctl restart ssh 2>/dev/null || service ssh restart"
Start-Sleep -Seconds 5
Write-Log "WSL and ssh restarted."
$ok = $false
for ($i = 0; $i -lt 30; $i++) {
$test = Test-NetConnection 127.0.0.1 -Port $ListenPort -InformationLevel Quiet
if ($test) {
$ok = $true
Write-Log "127.0.0.1:$ListenPort is reachable."
break
}
Write-Log "Waiting for 127.0.0.1:$ListenPort ..."
Start-Sleep -Seconds 2
}
if (-not $ok) {
Write-Log "WARNING: 127.0.0.1:$ListenPort not reachable yet."
}
foreach ($addr in $ListenAddresses) {
netsh interface portproxy delete v4tov4 listenaddress=$addr listenport=$ListenPort | Out-Null
}
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=$ListenPort | Out-Null
foreach ($addr in $ListenAddresses) {
netsh interface portproxy add v4tov4 listenaddress=$addr listenport=$ListenPort connectaddress=127.0.0.1 connectport=$ListenPort
Write-Log "Portproxy added: ${addr}:$ListenPort -> 127.0.0.1:$ListenPort"
}
if (-not (Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort $ListenPort -Profile Any | Out-Null
Write-Log "Firewall rule created."
} else {
Set-NetFirewallRule -DisplayName $RuleName -Enabled True -Action Allow -Profile Any
Write-Log "Firewall rule enabled."
}
$proxyInfo = netsh interface portproxy show all
Write-Log "Current portproxy:"
$proxyInfo | Out-File -FilePath $LogFile -Append -Encoding utf8
Write-Log "===== Done ====="
手动运行一次:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1
8. 创建开机和登录刷新任务
登录后刷新:
schtasks /Create /TN "Start WSL2 SSH Localhost" /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1" /SC ONLOGON /RL HIGHEST /F
开机后刷新:
schtasks /Create /TN "Start WSL2 SSH Localhost OnBoot" /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Users\<WIN_USER>\scripts\start_wsl2_ssh_localhost.ps1" /SC ONSTART /DELAY 0003:00 /RL HIGHEST /RU "<WIN_HOST>\<WIN_USER>" /RP * /F
最终应有三个任务:
Get-ScheduledTask | Where-Object {
$_.TaskName -like "*WSL*" -or $_.TaskName -like "*Ubuntu*"
} | Select-Object TaskName, State
正确结果类似:
TaskName State
-------- -----
Keep WSL2 Ubuntu Alive Running
Start WSL2 SSH Localhost Ready
Start WSL2 SSH Localhost OnBoot Ready
9. 最终检查
检查 portproxy:
netsh interface portproxy show all
应看到:
<LAN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
<VPN_IP> <SSH_PORT> 127.0.0.1 <SSH_PORT>
检查 Windows 监听:
netstat -ano | findstr <SSH_PORT>
测试本机:
ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
测试外部入口:
ssh -p <SSH_PORT> <LINUX_USER>@<LAN_IP>
ssh -p <SSH_PORT> <LINUX_USER>@<VPN_IP>
重启后,再检查:
Get-ScheduledTask -TaskName "Keep WSL2 Ubuntu Alive" | Select-Object TaskName, State
只要它是:
Running
一般就能稳定保持外部 SSH 访问。
10. 总结
WSL2 可以被配置成类似无人值守 Linux 服务器的使用方式,但它不是原生 Linux 服务器。
这次踩坑的核心结论是:
1. WSL2 的 172.x.x.x 地址不一定适合直接作为 Windows portproxy 的后端。
2. 如果 Windows 能访问 127.0.0.1:<SSH_PORT>,就优先转发到 localhost。
3. portproxy 建议监听具体外部 IP,不建议监听 0.0.0.0。
4. WSL2 会被 Windows 回收,必须做保活。
5. 保活关键是 Windows 侧长期运行一个不退出的 wsl.exe。
最终稳定结构是:
WSL2 内部:OpenSSH Server 监听 <SSH_PORT>
Windows portproxy:<LAN_IP>/<VPN_IP>:<SSH_PORT> -> 127.0.0.1:<SSH_PORT>
Windows 计划任务:Keep WSL2 Ubuntu Alive 长期 Running
这套方案适合个人开发机、轻量训练机和临时远程实验环境。
如果是严肃生产环境,原生 Linux 仍然更简单、更可靠。

浙公网安备 33010602011771号