Connect Windows / WSL — Mac 上从 SSH 连 Windows 和 WSL 的操作手册
术-操作 · 术层 skill 全文
本页是 <code>rules/skills/connect_windows.md</code> 的逐字投影(仅隐私清洗,零改写)。
时点提示:本页是仓内文件 rules/skills/connect_windows.md 的逐字投影(仅做隐私清洗:仓库根绝对路径→相对路径、家目录→~/;除此零改写)。若源文件后续有修订,以仓内真源为准。
Connect Windows / WSL — Mac 上从 SSH 连 Windows 和 WSL 的操作手册
元数据
- 类型: API Guide
- 适用场景: Mac 上需要
ssh win或ssh wsl连过去做事;连不上需要诊断;连接架构改变后需要更新配置 - 配置真相源:
config/opencode_remote/Windows_SSH_Tailscale_Config.md(authoritative)+config/opencode_remote/WSL_Mac_Tailscale_SSH_One_Command_Guide.md - 创建日期: 2026-05-21
目标
在 Tailscale 网络可达时,Mac 上一条命令即可连到 Windows(ssh win)或 WSL(ssh wsl)。不需要先去 Windows 上人工登录、不需要操心 WSL 内网 IP、不需要刷新 portproxy。
验收标准
ssh -o ConnectTimeout=10 win 'whoami; hostname'返回91201+LAPTOP-S44RCPHF,<3s。ssh -o ConnectTimeout=20 wsl 'whoami; hostname; uname -r'返回dth_zlx+LAPTOP-S44RCPHF+*-microsoft-standard-WSL2。第一次连可能因 WSL distro lazy 启动慢 3-10s,之后稳定。- 上述两条在 Windows 处于「锁屏 / 未登录桌面」状态下也应通(autologon 启用后已无差别)。
tailscale ping 100.79.139.116Mac 上返回 OK。
任一条不通走下面的「诊断决策树」。
不做什么
- 不修改 Windows 上
sshd_config、防火墙、startup folder 内容,除非另有明确变更需求。这些已经过多轮迭代,每次改都可能踩坑。 - 不尝试
Start-Service sshd。该 service 在本机历史上反复 1067 失败,已知 root cause 是ListenAddress 100.79.139.116bind 早于 Tailscale 拿到 IP;sshd.vbs 路径是已验证 work 的解。 - 不修改 ProxyCommand 回到
-W <WSL_IP>:22方案。WSL2 NAT 隔离 + IP 漂移会让该方案不稳定。 - 不在 Mac SSH config 里硬编码 WSL 内网 IP(
172.26..)。WSL 重启 IP 会变。 - 不为获得 sudo 权限去暴露 WSL 密码或在脚本里写密码。WSL sshd 启动由
wsl.conf的[boot] command = service ssh start处理,正常情况无需人工干预。
可用资源
Mac 端
~/.ssh/config的Host win和Host wsl两个入口(详见 config 文档的「Mac SSH 配置」段)~/.ssh/id_windows、~/.ssh/id_wsl两把专用私钥tailscaleCLI(status / ping / up)
Windows 端(通过 ssh win 后可用)
Get-NetTCPConnection、Get-Service、Get-ProcessPowerShell cmdlet — 不需要 admin 即可看 sshd / Tailscale 监听状态wsl -l -v、wsl --— 进入 WSL 命名空间执行命令Get-NetTCPConnection -LocalPort 22022 -State Listen看 sshd 实际监听 PID(不要相信Get-Service sshd的 Status,sshd.vbs 启的进程独立于 SCM)
WSL 端(通过 ssh wsl 或 ssh win 'wsl -- ...')
ss -tln看监听端口/etc/ssh/sshd_config、/etc/wsl.conf(后者位于\\wsl$\Ubuntu\etc\wsl.conf从 Windows 也可读)journalctl -b | grep ssh看本次启动 sshd 行为
跨 Windows shell 的引号陷阱
复杂 PowerShell 命令通过 ssh win 'powershell -Command "..."' 调用经常被 cmd 解析吞引号导致中文乱码错误。可靠方式:
PS='Get-Service sshd | Format-List'
ENC=$(printf '%s' "$PS" | iconv -t UTF-16LE | base64)
ssh win "powershell -EncodedCommand $ENC"WSL 内复杂 bash 命令同理用 base64:
CMD='set -x
ss -tln
ps -ef | grep sshd'
B64=$(printf '%s' "$CMD" | base64)
ssh win "wsl -- bash -c \"echo $B64 | base64 -d | bash\""诊断决策树
按链路上游到下游逐段诊断,先确认上游通了再看下游。每一步都有可观测的成功/失败信号,不要跳过验证直接猜。
第 0 段:Mac 自身能 ping 通 Tailscale 节点
tailscale status # 自己是否在线、win 节点是否 active 或 idle
tailscale ping 100.79.139.116 # 双向应答;如果只单向到达说明 NAT 穿透没完成不通:tailscale up 重登。tailscale up --reset 清掉本地状态再登。
第 1 段:Mac → Windows sshd(22022)
nc -zv 100.79.139.116 22022 # 应 succeeded
ssh -v -o ConnectTimeout=10 win 'echo ok' # 看 banner + 认证nc 通但 ssh 不通 → 钥匙问题(-v 看 debug1: Offering public key)。
nc 不通:去 win 上查 sshd 监听
Get-NetTCPConnection -LocalPort 22022 -State Listen- 有监听但外部
nc不通 → 防火墙规则被改动了,看Get-NetFirewallRule -DisplayName "SSH-Tailscale"(需 admin)。 - 没监听 → sshd 没起来。不要
Start-Service sshd。在 win 上直接跑C:\Windows\System32\OpenSSH\sshd.exe或重启 Windows 让 autologon + sshd.vbs 自动拉起。
如果连 ssh win 都进不去(key 失效、防火墙断了),只能本机或 RDP 处理。
第 2 段:Windows → WSL(wsl.exe lazy 调用)
ssh win 'wsl -- bash -c "ss -tln | grep :22"'期望看到 0.0.0.0:22 LISTEN。
不通:
ssh win 'wsl -l -v'看 distro 状态。若Stopped是 idle hibernation,下次 wsl.exe 调用会自动唤醒,等 5s 再试。ssh win 'wsl --shutdown'然后ssh win 'wsl -- echo ok'强制重启 distro。重启会触发wsl.conf的[boot] command = service ssh start。- 真不行去看
ssh win 'wsl -- bash -c "journalctl -b | grep -i ssh"'。
第 3 段:完整 ssh wsl
ssh -v -o ConnectTimeout=20 wsl 'whoami; hostname; uname -r'-v 关键 debug 信号:
Executing proxy command: exec ssh win "wsl -- nc -q0 127.0.0.1 22"— ProxyCommand 触发了kex_exchange_identification: Connection closed by remote host— WSL sshd 没起来或 nc 转发失败,回到第 2 段Permission denied (publickey)—id_wsl公钥没在 WSL~/.ssh/authorized_keys里
已知陷阱(按踩坑频率从高到低)
| # | 现象 | 真原因 | 处置 |
|---|---|---|---|
| 1 | ssh -W <WSL_IP>:22 win ProxyCommand 失败 | WSL2 默认 NAT 网络,Windows 主机到 WSL IP TCP 22 被 vmswitch 拒(ping 通但 TCP 拒);且 WSL 重启 IP 漂移 | 改用 ssh win "wsl -- nc -q0 127.0.0.1 22"。已固化在 config 中 |
| 2 | Get-Service sshd 显示 Stopped 但 ssh 能连上 | sshd 由 startup folder sshd.vbs 拉起,进程独立于 Service Control Manager。Get-Service 看的是 SCM 视角 | 看 Get-NetTCPConnection -LocalPort 22022 -State Listen 才是真相 |
| 3 | 试图 Start-Service sshd 报 1067 | sshd_config 中 ListenAddress 100.79.139.116,service 启动顺序早于 Tailscale 拿到 IP,bind 失败退出 | 维持 sshd.vbs 方案,autologon 解决"必须登录"依赖 |
| 4 | 重启后 ssh win 不通,本机却一切正常 | sshd.vbs 在用户启动文件夹,需要用户登录后才执行;如果机器在锁屏未登录,sshd 不会起 | 启用 Windows autologon(sysinternals AutoLogon,LSA Secret 加密存密码)。本机 2026-05-21 起已启用,可用上面"验证 autologon"命令确认 |
| 5 | WSL 内 systemctl status ssh.socket 显示 failed (Address already in use) | wsl.conf 的 [boot] command = service ssh start 先启动了 sshd 占 22;systemd 的 socket-activated ssh.socket 然后启动失败。实际 sshd 在跑,不影响功能 | 忽略 status 看 ss -tln 真相 |
| 6 | netsh interface portproxy show all 是空的但历史文档说应该有规则 | portproxy 方案已弃用(WSL IP 漂移导致规则失效,且 wsl nc 方案不依赖 portproxy) | 不要重建 portproxy 规则;按新方案 |
| 7 | ssh win 'powershell -Command "..."' 出现中文乱码 / cmd 错误 | cmd 解析 PowerShell 嵌套引号失败 | 用 powershell -EncodedCommand <base64>(UTF-16LE 编码) |
| 8 | wsl -l -v 显示 distro Stopped 担心是不是挂了 | distro 进入 idle 后 hibernation 是正常行为;下次 wsl.exe 调用会自动唤醒 | 别动;下次 ssh wsl 会自动 lazy 启动,第一次连慢 3-10s |
| 9 | Mac 上 ssh wsl Permission denied | id_wsl.pub 没在 WSL ~/.ssh/authorized_keys 里 | 把公钥 append 进去(可经 ssh win 'wsl -- bash -c "..."' 间接写) |
| 10 | 91201 不是 administrator,写错位置 | 91201 不在 Administrators 组,所以 SSH key 走 C:\Users\91201\.ssh\authorized_keys;若用户是 admin 则走 C:\ProgramData\ssh\administrators_authorized_keys。先用 ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 判断 | 写到对应位置 |
| 11 | ssh win 'echo a; whoami; hostname' 输出字面 "a; whoami; hostname" | ssh win 默认进入 cmd.exe,分号 ; 不是 cmd 的命令分隔符(cmd 用 & / &&)。被当成 echo 的字面参数打印 | 单条命令直接跑;多条用 & 分隔;复杂逻辑跑 PowerShell:ssh win 'powershell -NoProfile -Command "..."',复杂 PowerShell 用 -EncodedCommand(见上方"跨 Windows shell 的引号陷阱") |
常用任务速查
加一个公钥到 WSL authorized_keys(不需要直连 WSL 也能写)
KEY='ssh-ed25519 AAAA... comment'
CMD="mkdir -p ~/.ssh && echo '$KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
B64=$(printf '%s' "$CMD" | base64)
ssh win "wsl -- bash -c \"echo $B64 | base64 -d | bash\""加一个公钥到 Windows 91201 authorized_keys
PS='$key = "ssh-ed25519 AAAA... comment"; $path = "$env:USERPROFILE\.ssh\authorized_keys"; if (-not (Test-Path (Split-Path $path))) { New-Item -ItemType Directory -Force (Split-Path $path) | Out-Null }; Add-Content -Path $path -Value $key'
ENC=$(printf '%s' "$PS" | iconv -t UTF-16LE | base64)
ssh win "powershell -EncodedCommand $ENC"一次性健康体检(建议在被卡时跑)
注意 ssh win 默认进入 cmd.exe(不是 bash 也不是 PowerShell),命令分隔符是 & 而不是 ;;ssh wsl 走 bash,可用 ;。
echo "--- 0. tailscale ---"; tailscale status | grep -E "(self|laptop-s44)"
echo "--- 1. ssh win (cmd 用 &) ---"; ssh -o ConnectTimeout=5 win 'echo ok & whoami & hostname' 2>&1 | head -5
echo "--- 2. win sshd listener (PowerShell) ---"; ssh win 'powershell -NoProfile -Command "(Get-NetTCPConnection -LocalPort 22022 -State Listen) | Select-Object LocalAddress, OwningProcess"' 2>&1 | grep -v -E 'CLIXML|<Objs|<Obj|Props>|progress' | head -5
echo "--- 3. ssh wsl (bash 用 ;) ---"; ssh -o ConnectTimeout=15 wsl 'echo ok; whoami; hostname; uname -r' 2>&1 | head -5
echo "--- 4. wsl sshd ---"; ssh win 'wsl -- bash -c "ss -tln 2>/dev/null | grep :22"' 2>&1 | head -5期望(autologon 已启用、链路完整时):
- tailscale:
100.79.139.116 laptop-s44rcphf ... active; direct ... - ssh win: 三行
ok / laptop-s44rcphf\91201 / LAPTOP-S44RCPHF - sshd listener:
LocalAddress=100.79.139.116, OwningProcess=<sshd pid> - ssh wsl:
ok / dth_zlx / LAPTOP-S44RCPHF / 6.x.x-microsoft-standard-WSL2 - wsl sshd:
0.0.0.0:22 LISTEN+[::]:22 LISTEN
验证 autologon 是否启用(决定无人值守路径是否完整)
PS='$p = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Write-Host "AutoAdminLogon = $($p.AutoAdminLogon) / User = $($p.DefaultUserName) / Domain = $($p.DefaultDomainName)"
if ($p.PSObject.Properties.Name -contains "DefaultPassword") { Write-Host "WARN: 密码明文存在注册表" } else { Write-Host "OK: 密码不在明文注册表(LSA Secret 加密)" }'
ENC=$(printf '%s' "$PS" | iconv -t UTF-16LE | base64)
ssh win "powershell -EncodedCommand $ENC"期望输出:AutoAdminLogon = 1 / User = 91201 / Domain = LAPTOP-S44RCPHF + OK: 密码不在明文注册表(LSA Secret 加密)。
任一项不符(例如 AutoAdminLogon = 0 或 WARN: 密码明文)说明 autologon 没正确配置,开机后 startup folder 的 sshd.vbs / OpenCode.vbs 不会自动跑。重新跑 C:\Users\91201\Tools\AutoLogon\Autologon64.exe GUI 配置一次。
重新配置或撤销 autologon
工具位置:C:\Users\91201\Tools\AutoLogon\Autologon64.exe(64-bit Windows 用这个,32-bit 用 Autologon.exe,ARM64 用 Autologon64a.exe)。
# Windows 本机管理员 PowerShell
& "$env:USERPROFILE\Tools\AutoLogon\Autologon64.exe"GUI 三栏 Username / Domain / Password 输入。点 Enable 启用、Disable 撤销(Disable 会同时清掉 LSA Secret 中的密码,比注册表单独改 AutoAdminLogon=0 更干净)。
配置变更时的发布纪律
任何对以下任一项的实质修改,都要更新 Windows_SSH_Tailscale_Config.md 的"历史问题与修复"段(追加新日期段,不重写历史),并同步更新 WSL_Mac_Tailscale_SSH_One_Command_Guide.md 如涉及 WSL 通路:
- Mac
~/.ssh/config的Host win或Host wsl - Windows
sshd_config、防火墙规则、startup folder 内容、Tailscale 配置、autologon 状态 - WSL
wsl.conf、sshd_config、authorized_keys、网络/IP 方案
修改后跑一次"健康体检"确认链路完好。
参考
- 主配置文档:
config/opencode_remote/Windows_SSH_Tailscale_Config.md - WSL 通路文档:
config/opencode_remote/WSL_Mac_Tailscale_SSH_One_Command_Guide.md - Skill 写作规范:
bestpractice_skill_writing_guide