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 一般就不会再因为空闲被回收。

主要细节我会改这几处:

  1. <SSH_PORT> 这种占位符在 PowerShell 脚本里不能直接运行,要明确写“发布前替换成数字,例如 60001”。
  2. 需要补一句:本方案依赖 WSL2 的 localhostForwarding 正常工作,必须先验证 ssh -p <SSH_PORT> <LINUX_USER>@127.0.0.1
  3. 0.0.0.0 -> 127.0.0.1 不建议写,这个判断要保留。
  4. “systemd keepalive 不够”可以保留,但不要展开太多,重点说“最终要 Windows 侧 wsl.exe 常驻”。
  5. 计划任务里 /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 仍然更简单、更可靠。

posted @ 2026-06-11 16:08  wickyan  阅读(27)  评论(0)    收藏  举报