多台 Windows 用 Tailscale 组网 + SSH 互控与文件互投
> 版本 1.2.0 | 多台 Windows 长期互连实测产出:组网、判链路、通 SSH、传文件、定时任务,
> 五层各有一批反直觉结论。每一条「反直觉」都是踩出来的,别凭常识改。
> 本技能属于「多台 Windows 远程运维」系列,总纲与决策路由见 remote-win-fleet;系列通用的硬规则(判链路、远程执行、脚本交付、传输通道、钥匙策略)在总纲里统一定义。
这个技能解决什么问题
手上有三四台 Windows:一台放办公室长期开机、一台随身、一台在另一个办公点。
想让它们变成一个机群——彼此能 SSH 上去执行命令,能互丢文件,不需要任何人肉 U 盘。
这件事难的地方不在「软件怎么装」,而在每一步都有会骗你的现象:
- 装满屏的
relay "sfo",你以为要排查服务,其实等几秒它就自己变direct; ping100% 超时,你以为路由没生效,其实链路完全是好的——Windows 防火墙把 ICMP 丢了;Permission denied反复出现,你以为公钥贴错了,其实是用户名不对——这两种情况在 Windows 上返回一模一样;scp报一个看不懂的pipe: Unknown error,你换个姿势再试,其实它正静静地什么都没传过去。
每一条的代价都是几小时。本技能按「组网 → 判链路 → 通 SSH → 传文件」四层把它们一次说清。
更新记录
v1.2.0
- 新增 1.7 找 exe 别只查默认安装位置:只查 C:\Program Files\Git\ 就断言「没装 Git」,
实际装在 C:\Users\<用户>\AppData\Local\Programs\GitFull\。2026-10-04 因此误判了一整轮。
- 新增 3.8 Git for Windows 自带的 ssh/scp 优先于 Windows 自带的:
同一台机器实测,GitFull\usr\bin\ssh.exe 连得上,C:\Windows\System32\OpenSSH\ssh.exe 连不上。
原因是 Win32 那份用命名管道(见 5.1),在 Tailscale 环境下握手成功率明显更低。
- 新增 3.9 计划任务里跑 SSH 拿不到 Tailscale 网卡:SYSTEM 身份会看到一串 @@@@。
分「有用户登录」和「无人值守」两种情况;无人值守那节进一步拆成两个正交的开关 ——
Tailscale 层(--unattended,管无人时在不在线)与任务层(LogonType,管无人时触不触发),
附「5 分钟一轮同步小文件」的模板与实测到的 Interactive 判读。
- 新增 3.10 私钥权限不一致会被 OpenSSH 拒绝:一把 (I)(F) 能用、另一把 (R) 被拒,
报 Permission denied (publickey)。用 icacls 统一。
- 新增 5.6 node 里 spawn ssh/scp 会报 EBUSY:文件锁,不是调用姿势问题。
同机 bash 调同样命令完全正常 —— 工具能跑就别硬套 node。
- 修 1.7 的失效交叉引用、1.5 的 metric 表述(0 vs 256 改成「隧道路由 metric 更低」并给出实测样例
5 / 291)、3.4 的 Server accepts key: 补冒号、5.2 补「等大小稳定」的具体阈值。
- allowed-tools 补上 PowerShell —— 正文大量用 icacls / where.exe / Test-NetConnection。
- 新增目录:按层分组列出全部小节号与「治什么」。
目录只写小节号、不做跳转链接 —— 各家渲染器的锚点算法不一样(. : → —— 怎么处理全无共识),
写死链接注定在某个平台上失效;小节号才是稳定的。读者用 Ctrl+F 搜 3.9 即可。
- 1.4 的 KeyExpiry 对照表去掉「值为 null」一行(Go 的 time.Time 不会序列化成 JSON null,
那行是推测而非实测),并注明这一点。
<details>
<summary><b>更早的版本(v1.1.0 / v1.0.0)</b></summary>
v1.1.0
- 新增 1.6 手机/平板入网:Google 账号 tailnet 在大陆必须走 auth key。
这是 2026-09-30 拿一台安卓平板实测出来的:login/controlplane 都通,只有 accounts.google.com 挂,
于是「Sign in with Google」和「扫二维码」两条路一起死,且症状完全一样(卡在登录页)。
根因不在客户端版本,在身份提供方。
v1.0.0
- 首版:四层作业线 + SendFile.ps1 工具 + 排错速查表。
</details>
目录
只想解决一个具体问题?用 Ctrl+F 搜小节号(如 3.9)直接跳,不必通读。
> 这里刻意不写跳转链接 —— 各家渲染器生成锚点的规则不一样(有的保留 1.1 里的点,
> 有的去掉,还有的会把 → —— 变成连字符),写死链接注定在某个平台上失效。
> 小节号是稳定的,锚点不是。
| 层 | 小节号与标题 | 治什么 |
|---|---|---|
| 1 组网 | 1.1 离线包必须下完整 MSI | 离线机装不上 |
| 1.2 安装返回 1603 的根因 | iphlpsvc 被禁 |
|
| 1.3 免交互入网用 auth key | 免交互入网 | |
| 1.4 key expiry 永久了没 | 是不是永久 | |
| 1.5 子网路由:三条禁令 | 全网段黑洞 | |
| 1.6 手机/平板入网 | 扫码登录卡死 | |
1.7 找 exe 之前先 where |
误判「没装」 | |
| 2 判链路 | 2.1 tailscale ping 首次报中继 |
误判链路坏 |
| 2.2 判断通不通,看端口不看 ICMP | ping 全超时 | |
| 2.3 本机代理会把你骗得更惨 | 代理捣乱 | |
| 2.4 顺手记一条 | NoState 别急着重登 |
|
| 3 SSH | 3.1 Windows 自带的 OpenSSH 只有客户端 | 背景节,见配套技能 |
| 3.2 Permission denied 是没法自己告诉你答案的 | 用户名还是公钥 | |
| 3.3 生成密钥的引号坑 | 私钥被加密 | |
| 3.4 「Server accepts key」却仍被拒 | 病根在客户端 | |
| 3.5 公钥文件不能带 BOM | 贴了不生效 | |
| 3.6 多把钥匙是常态 | 试错白等超时 | |
3.7 ssh.exe 的位置各台不一样 |
写死路径 | |
| 3.8 Git 自带的 ssh/scp 优先 | ssh 时通时不通 | |
| 3.9 计划任务里跑 SSH | 手动能连、计划任务不能(含无人值守方案) | |
| 3.10 私钥权限不一致 | Permission denied | |
| 4 远程执行 | 4.1 中文路径的正解:-EncodedCommand |
中文路径传不过去 |
| 4.2 路径带空格会被吃掉引号 | 引号被吃 | |
4.3 含中文的 .ps1 要带 BOM |
假解析错误 | |
4.4 远端的 $env:TEMP 可能是空的 |
参数绑定异常 | |
| 5 传文件 | 5.1 千万别信 Win32-OpenSSH 的 scp | pipe Unknown error |
| 5.2 Taildrop | 官方通道 | |
| 5.3 主机名在局域网里会骗你 | 投递一律用 IP | |
5.4 not in your Tailnet |
发起方掉线 | |
| 5.5 怎么选 | 三条路怎么挑 | |
| 5.6 node 里 spawn ssh/scp 会报 EBUSY | 定时任务改用 bash | |
| 工具与速查 | SendFile.ps1 | 选机器选文件直接发 |
| 端到端验收清单 | 新机器进群先打勾 | |
| 排错速查 | 34 条症状对照表 |
第 1 层 · 组网:把机器拉进同一个 tailnet
1.1 离线包必须下完整 MSI
https://pkgs.tailscale.com/stable/?mode=json
取 tailscale-setup-<ver>-amd64.msi(约 36 MB),安装过程不需要联网。
⚠️ 那个 1.4 MB 的 tailscale-setup.exe 是联网引导器,运行时才现拉 MSI。
精简系统、离线机、网络受限的内网机器上用它,结果就是静默失败。
1.2 安装返回 1603 的根因
msiexec 返回 1603,十有八九不是包坏了,而是 iphlpsvc(IP 帮助程序)被禁用
——优化软件常干这事,精简系统出厂就可能这样。
Set-Service iphlpsvc -StartupType Automatic
Start-Service iphlpsvc
救完再静默装,一次就过。这条要排在所有安装动作之前。
1.3 免交互入网用 auth key
后台生成 tskey-...(勾 Reusable,有效期 1–7 天),然后:
tailscale up --authkey=<key> --accept-routes=false
两点必须说清:
--accept-routes=false是默认姿势。 不要一上来就接收别人广播的子网路由,原因见 1.5。- 入网成功之后,去后台 revoke 这个 auth key。它作为明文留在了那台机器的落地包里。
读 auth key 文件时注意:匹配要锚定行首(^\s*tskey-),
并且模板里的说明文字不要写出这个前缀——踩过一次:说明里写了「只认含 tskey- 的行」,
脚本取「第一个含 tskey- 的行」,于是读到了说明行自己。
1.4 key expiry 永久了没?别再去看年份
判据很反直觉:
tailscale status --json
看 Self 对象——没有 KeyExpiry 这个字段 = 永久有效。
字段消失,不是被设成 0001-01-01。拿着旧读数算天数会得出「快过期了」的错误结论。
长期开机的机群,这一步不做,半年后某天会全体掉线。
⚠️ 别把「字段不存在」和「字段存在但为空」搞混,这两个状态在 JSON 里长得很像:
Self 里的样子 |
含义 |
|---|---|
压根没有 KeyExpiry 这个键 |
永久有效 ✅ |
有这个键,值是 0001-01-01T00:00:00Z 这种零值 |
不是永久,去管理台核对 |
| 有这个键,值是一个未来的时间戳 | 真会过期,看还剩几天 |
判据要落在「键是否存在」上,而不是去比对值。
(Tailscale 的这个字段是 Go 的 time.Time,序列化时要么整个键消失、要么给一个具体时间戳,
不会输出 JSON null —— 所以只要你看到 null,那多半不是这个字段,别顺着它往下查。)
1.5 子网路由:三条禁令
想让 A 机访问 B 机所在的内网网段(比如在另一台机器上架了个服务),才需要这一节。
能用 Tailscale 地址解决的事,就不要碰子网路由。
禁令一:广播的网段必须是网关机自己所在的网段。
别照抄脚本里写死的网段。先在网关机上 ipconfig 确认真实内网地址,再
tailscale up --advertise-routes=<真网段>,后台 approve。
禁令二:广播的网段绝不能与接收方自身所在网段重叠。
这条是拿一次全网段黑洞换来的:接收方内网是 192.168.2.0/24,网关机广播了同一个段,
于是该段全部流量被塞进隧道交给网关机,而网关机根本不在这个段里 → 黑洞。
ping 网关从 3–12ms 变成全丢。
机制说明:Tailscale 写入的子网路由,会被系统赋予比本地 WLAN 更低的 metric,
于是优先生效。metric 的具体数值依版本与网卡而异 —— 实测见过
「Tailscale 隧道 5 / 本地 Wi-Fi 291」这种组合,别把某个数字当成固定值,
要判断谁优先就看 route print -4 里这两条路由的 metric 谁小。
route print -4
恢复了之后记住:任何需要路由的日子,--accept-routes=false 是保命值;
救回来靠的也是它:tailscale up --accept-routes=false 之后路由表只剩本地条目。
禁令三:广播之后别用 ping 验收。 见下一层。
1.6 手机/平板入网:Google 账号的 tailnet 在大陆只能走 auth key ❗
如果 tailnet 是拿 Google 账号建的(tailscale status 里看到 xxx@gmail.com),
那么在大陆网络下,安卓/iOS 客户端两条常规登录路都会死,而且死法一模一样:
| 登录方式 | 实际走的链路 | 大陆结果 |
|---|---|---|
| Sign in with Google | → accounts.google.com |
❌ 打不开 |
| Sign in with other(IdP 是 Google 时) | → 同上 | ❌ |
| 扫二维码(用另一台设备扫) | 那台设备的浏览器 → login.tailscale.com → 仍跳 Google |
❌ |
⚠️ 顺手排掉一个高频误判:Tailscale 有两种二维码,用户很容易混。
- 下载码 —— 官网
tailscale.com/download/android页面上那个,
以及管理台Machines → Add device → Android → Download Tailscale重定向过去的同一个码。
官方文案就是「Scan the QR code or use the link below to download」,
扫开指向 Google Play / 下载页完全正常,和登录无关。 - 登录码 —— 只有打开 App 本身、在登录页点
QR code才出。
用户说「二维码指向谷歌商店」时,八成撞的是①。
但它对解决问题没有帮助——② 那条路在大陆本来也是死的(见上表)。
→ 只要看到用户在二维码上打转,就别陪着分析码指向哪,直接转到 auth key 这条路。
懒得读上面两段的话,看这张表就够:
| 码的类型 | 在哪扫到 | 扫开跳向 | 能解决登录问题吗 |
|---|---|---|---|
| 下载码 | 官网下载页 / 管理台 Add device | Google Play、下载页 | ❌ 与登录无关 |
| 登录码 | App 打开后的登录页点 QR code |
跳 Google 登录 | ❌ 大陆仍走不通 |
两张码都救不了大陆的 Google 登录,只有 auth key 能。
2026-09-30 实测(本机直连、不走代理):
login.tailscale.com -> 302 ✅
controlplane.tailscale.com -> 302 ✅
accounts.google.com -> 000 超时 10s ❌ ← 只有它挂
关键结论:问题不在客户端版本、不在 APK 来源、不在 Android 版本。
换 APK、清缓存都没用——挂的是身份提供方那一步。
为什么电脑上还能用? 因为它早就登录过了:节点密钥默认 180 天有效期,
只要不掉线就一直续着,不需要再走一次 Google。
所以「电脑好好的、手机登不上」是最典型的症状,别据此怀疑客户端。
正解:auth key。这条链路不经过任何身份提供方。
安卓客户端(1.82 以后版本菜单一致):
打开 Tailscale
→ 若自动弹出浏览器要你登 Google:直接关掉(X / 返回),不要登
→ 右上角 ⚙ 设置 → Accounts
→ 右上角 ⋮(三个点)→ Use an auth key
→ 粘贴 tskey-auth-... → Add account
→ 回主页点 Connect
⚠️ 菜单里 Use an auth key 就挨着 Use an alternate server,
两者都在 Accounts 页的同一个 ⋮ 菜单下,别点错(后者是给自建 headscale 用的,填错会把你导到别的控制面)。
key 在管理台 https://login.tailscale.com/admin/settings/keys 生成:
勾 Reusable、有效期选 90 天,比一次性 key 省事(一次性用完就得再签,且失败一次就废了)。
排「到底卡在哪一步」的判据(拿移动端浏览器直接试,不用猜):
| 测试 | 结果 | 结论 |
|---|---|---|
打开 controlplane.tailscale.com |
打不开 | 设备本身出不了网 → 换 DNS / 换 Wi-Fi / 关厂商"纯净模式"拦截 |
打开 controlplane.tailscale.com |
打得开仍登不上 | 就是身份提供方(Google)的问题 → 上面 auth key |
打开 accounts.google.com |
打不开 | 实锤:Google 被墙,auth key 是唯一解 |
彻底绕开账号的备选:EasyTier 安卓版(只填「网络名 + 密码 + 公共节点」,无账号无登录),
代价是链路两端都要换成 EasyTier,见配套技能 easytier-win-mesh。
1.7 找 exe 之前先 where,别只查默认安装位置 ❗
这一条是 2026-10-04 拿一整轮无效排查换来的。
当时要判断「目标机器上有没有 bash」,只查了 C:\Program Files\Git\bin\bash.exe
和 C:\Program Files (x86)\Git\...,都不存在,就直接下了结论「没装 Git,bash 跑不了」。
实际上那台机器装了 Git for Windows,装在:
C:\Users\<用户>\AppData\Local\Programs\GitFull\bin\bash.exe
C:\Users\<用户>\AppData\Local\Programs\GitFull\usr\bin\ssh.exe
C:\Users\<用户>\AppData\Local\Programs\GitFull\usr\bin\scp.exe
再往前一步就已经跑通了。Git for Windows 装到用户目录是常见情况(免管理员权限的安装方式),
不是异常情况。
正确做法:先把「这个 exe 到底在哪」问出来,再谈有没有。
where.exe bash
where.exe git
where.exe ssh
where.exe scp
where 会把 PATH 里所有命中都列出来,一眼就能看出装在标准位置还是用户目录。
三条延伸:
where结果里出现多行是常态(Program Files 一个、System32 一个、用户目录一个),
这时不能只取第一行 —— 要挨个实测哪个能用(见 3.8)。where找不到的东西,可能在%LOCALAPPDATA%\Programs下(Git、Node、Python 都爱这么装),
值得单独扫一眼。- 想找某个 exe 属于哪个包,直接看同级目录:
bin\放入口、usr\bin\放真正的可执行文件、mingw64\bin\放运行时。
Git for Windows 的bash.exe在bin\,ssh.exe/scp.exe在usr\bin\,两个别搞混。
教训:在 Windows 上断言「某工具不存在」之前,至少要跑过一次 where,
并且按第 1.7 节「三条延伸」的第 2 条确认过用户目录。凡是结论只有一条路径支撑的,都可能是误判。
第 2 层 · 判链路:别拿 ping 下结论
这是整个技能里最高频、最害人的一层。三条:
2.1 tailscale ping 首次报中继,别急着下结论
tailscale ping <对方 100.x>
第一次的输出经常是 via DERP(...)。这往往是打洞协商过程中的临时状态,
几秒后会自动升级成 direct。实测 6 对链路里有 3 对首次报中继,复查全部变直连。
→ 判定一对链路快不快,必须复查。 第一次的结果什么都不能证明。
2.2 判断通不通,看端口不看 ICMP
Windows 公用网络配置文件、AP 隔离策略、远端主机防火墙——任何一层都会静默丢 ICMP。
ping 100% 超时而链路完全正常的情形,是常态不是意外。
正确姿势:
Test-NetConnection <目标> -Port <业务端口>
再加一层 HTTP 交叉验证:
$env:http_proxy = $null # 先清代理,见 2.3
Invoke-WebRequest http://<目标>:<端口>/ -UseBasicParsing
判据优先级:TCP 端口 OPEN > HTTP 200 > ping。 ping 只能排最后。
顺带一条:某台机器上跑的服务在 Tailscale 地址上访问不到、在内网地址上能访问,
多半不是「socket 绑死了具体 IP」,而是安装包配的防火墙入站规则带了 scope(只放行局域网段)。
补一条 -Profile Any、不带 -LocalAddress 的放行规则即可。别急着加 portproxy 转接,
那是多余的层。
2.3 本机代理会把你骗得更惨
本机若设了 http_proxy,.NET/curl 会自动走代理,结果是
「不含点的主机名」自动 bypass 代理能通,而 IP 形式的地址全部超时
——看起来完全像「路由没生效、DNS 没配好」。
→ 凡是要用 HTTP 判定链路,先 $env:http_proxy=$null,并用 TCP 层交叉验证。
2.4 顺手记一条
Tailscale 掉到 NoState 时别急着重登。托盘里的 tailscale-ipn 多半本来就在跑
(再启动会因 UI mutex 已存在而退出),是服务侧在重连,等一会自己会好;
而且 CLI 的 tailscale up 这时也拉不起来。先 tailscale status 确认,再决定要不要动。
第 3 层 · 通 SSH:装机与钥匙分发
3.1 Windows 自带的 OpenSSH 只有客户端
C:\Windows\System32\OpenSSH\ 里通常没有 sshd.exe,不能就地「反正有 OpenSSH 那就改改配置」。
必须单独补服务端。精简 / Ghost 版系统尤其麻烦,Add-WindowsCapability 装出来的往往是残缺品
(能力显示 Installed,运行时报入口点错误)。这一步的完整做法见配套技能 remote-win-ssh-offline。
3.2 Permission denied 是没法自己告诉你答案的 ❗
Windows OpenSSH 里,下面两种情况返回完全相同的信息:
Permission denied (publickey,password,keyboard-interactive).
- 用户名根本不存在
- 用户名对,但公钥不匹配
无法据此判断用户名对错。
→ 必须先去目标机拿真实账户列表:Get-LocalUser,或 net user,或者直接看 C:\Users 下的目录名。
拿到之后再逐个试。这一步省掉,后面所有排查都是瞎猜。
另外留意:同一台机器上可能有多个账户,跑 Tailscale 托盘的是 A 账户,而 SSH 登录的是 B 账户。
这会直接影响第 5 层的文件落点。
3.3 生成密钥的引号坑(真踩过)
想在 bash 里表达「空密码」:
ssh-keygen -N '' # ✅ 正确
ssh-keygen -N '""' # ❌ 两个引号字符被当成字面密码
第二种写法会生成一个被 aes256-ctr + bcrypt 加密的私钥,后面各种奇怪的认证失败由此而来。
生成后务必自查:
ssh-keygen -y -P '' -f <key> # 能输出公钥才算真的没密码
3.4 「Server accepts key」却仍被拒 → 病根在客户端
ssh -vvv 里只要看到 Server accepts key:(带冒号,服务端回了 PK_OK),
就说明服务端的 authorized_keys 已经配对成功了。此时还报 Permission denied,
问题是客户端这把私钥读不出来(被密码加密了、权限不对、格式不对)。
→ 别再去折腾服务端的公钥文件。
3.5 公钥文件不能带 BOM
.pub 文件一旦带 UTF-8 BOM,authorized_keys 里那一行的首字符会变成不可见字符,整条公钥直接失效,
而且不会有任何明确报错。
3.6 多把钥匙是常态,别指望一把通吃
机器多了以后,各台授权的公钥往往不统一。别试图「统一一下」,直接接受现实:
- 把所有候选私钥都装到每台机器上
- 连接时依次试,哪把成功就用哪把
- 成功后写进缓存(如
%TEMP%\..._keycache.txt),下次先试对的
为什么值得做缓存:试错一把不对的钥匙,代价是白白等一个连接超时,实测能拖到 15 秒以上。
跨地点 SSH 握手偶发很慢(实测过同刻网络正常、单次超过 8 秒而重试只要 0.7 秒),
所以 ConnectTimeout 别设太小(建议 20–25 秒),并且整轮失败后再试一次,
免得把在线的机器误判成离线。
3.7 ssh.exe 的位置各台不一样
离线包装进 C:\Program Files\OpenSSH,Windows 可选功能才是 C:\Windows\System32\OpenSSH。
挨个探测,写死必挂。 先跑 where ssh(见 1.7)拿到全量,再按 3.8 决定用哪一份。
3.8 Git 自带的 ssh/scp 优先于 Windows 自带的那份 ❗
2026-10-04 同一台机器上的实测对比(连同一台对端、同一把私钥,只换 ssh.exe):
| ssh.exe 来源 | 结果 |
|---|---|
C:\Users\<用户>\AppData\Local\Programs\GitFull\usr\bin\ssh.exe |
✅ 成功,返回对端主机名 |
C:\Windows\System32\OpenSSH\ssh.exe |
❌ status=null(握手失败/超时) |
两个原因叠加:
- Win32 自带那份用命名管道实现
pipe(),会ERROR_PIPE_BUSY(见 5.1),
在 Tailscale 环境下握手成功率明显更低; C:\Program Files\OpenSSH\(离线包装的那份)也可能不可用 —— 它和 System32 那份是同一套实现。
所以候选顺序应该是:
1) Git for Windows 的 usr\bin\ssh.exe ← 最稳,优先
2) Git for Windows 的 usr\bin\scp.exe
3) C:\Program Files\OpenSSH\ssh.exe
4) C:\Windows\System32\OpenSSH\ssh.exe
5) PATH 里兜底
探测时不要只看「文件存在」,要挨个真连一次:
foreach ($p in $cands) {
if (Test-Path $p) {
$r = & $p -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 `
-i $key "$user@$ip" hostname 2>$null
if ($LASTEXITCODE -eq 0 -and $r) { "可用: $p -> $r"; break }
else { "不可用: $p" }
}
}
Git for Windows 的目录结构别搞混(见 1.7.3):
GitFull\bin\bash.exe ← 入口
GitFull\usr\bin\ssh.exe ← 真正的 ssh
GitFull\usr\bin\scp.exe ← 真正的 scp
3.9 计划任务里跑 SSH:拿不到 Tailscale 网卡 ❗
想让某台机器定时连其他机器(同步文件、跑巡检),把它做成计划任务时会撞上这堵墙。
先分清两种情况——「有人在用这台机器」和「没人用」,解法完全不同。
情况 A:有用户保持登录(最省事)
| 运行身份 | 症状 |
|---|---|
SYSTEM(/ru SYSTEM) |
ssh 回显一长串 @@@@,随后超时;schtasks 报「找不到系统文件」 |
当前用户(/ru 用户) |
✅ 正常 |
不写 /ru(默认当前用户上下文) |
✅ 正常 |
根因:Tailscale 默认跑在交互式登录会话里。非交互式上下文(SYSTEM、计划任务、
某些 SSH 会话)拿不到那个虚拟网卡,于是包发不出去。
写法:干脆不写 /ru。
schtasks /create /tn SyncTask ^
/tr "\"C:\Users\<用户>\AppData\Local\Programs\GitFull\bin\bash.exe\" \"D:\job\sync.sh\"" ^
/sc minute /mo 5 /f
- 不带
/ru→ 任务在当前用户上下文里跑,能用上 Tailscale /sc minute /mo 5→ 每 5 分钟一轮,跑完即退,
空闲时 CPU 0%、内存 0 MB(这是「定时同步」最省资源的做法,
比常驻守护进程省一个数量级)- 不要用
/rl HIGHEST,普通权限即可,反而更不容易出问题 - 脚本路径含中文时,
schtasks的引号处理容易出错,
最稳的做法是让 action 指向一个纯 ASCII 的.bat入口,由它去调 bash
⚠️ 这一招有前提:那个用户处于登录状态。
机器重启后没人登录、或会话被注销,「当前用户上下文」就不存在了,任务要么不跑,
要么跑在一个同样拿不到网卡的会话里。要覆盖这种场景,看下面的情况 B。
情况 B:需要无人值守(没人登录也要跑)
⚠️ 这里有两个互相独立的开关,别混成一个方案。
| 层 | 管什么 | 由谁决定 |
|---|---|---|
| Tailscale 层 | 无人登录时 Tailscale 在不在线 | tailscale up --unattended=true |
| 任务层 | 无人登录时计划任务触不触发 | 任务的 LogonType(/ru 决定) |
--unattended 只管上面那层。任务不写 /ru 的话,LogonType 默认是 Interactive,
用户一注销,任务压根不会被触发 —— 不是「触发了但拿不到网卡」,而是「根本不跑」。
实测(本机 Get-ScheduledTask,未写 /ru 建的任务):
Name : WorkbenchAddressSync
LogonType : Interactive ← 仅在用户已登录时运行
UserId : YOGA3PRO
所以无人值守要两层都配齐:
| Tailscale 层 | 任务层 | 无人时 Tailscale 在线? | 无人时任务触发? | 备注 |
|---|---|---|---|---|
| 未开 unattended | /ru SYSTEM |
❌ 网卡不存在 | ✅ | 情况 A 那个 @@@@ 就是它,实打实踩过 |
| 未开 unattended | /ru 用户名 /rp 密码 |
?推理 | ✅ | 密码进凭据管理器 |
| 开 unattended | /ru SYSTEM |
?推理 | ✅ | 推荐方向 |
| 开 unattended | 任务不写 /ru(Interactive) |
✅ | ❌ 不触发 | 实测 LogonType 为 Interactive |
| 开 unattended | /ru 用户名 /rp 密码 |
?推理 | ✅ | 也能用,但密码仍要交出去 |
⚠️ 只有第 1 行的 ❌ 和第 4 行的「不触发」是实测/已确认的,
带「?推理」的三行都没在本机复现。
三行的不确定性不是同一个量级,别并列看。 决定「无人时在线」的有两个变量:
- 变量 ①:Tailscale 服务会不会随 GUI 用户登出下线 → unattended 管这个
- 变量 ②:网卡在「非交互式会话」这个身份上下文里出不出现 → unattended 管不到
对着这两个变量看:
| 行 | 变量 ① 服务在线 | 变量 ② 网卡可见 | 整体把握 |
|---|---|---|---|
第 1 行(未开 unattended + /ru SYSTEM) |
❌ 确定不通(实测 @@@@) |
❌ 不必验 | 不可行 |
第 2 行(未开 unattended + /ru 用户名) |
⚠️ 大概率不通(见下) | ?不确定 | 基本没戏 |
第 3 行(开 unattended + /ru SYSTEM) |
✅ 有保证 | ?不确定 | 只剩一个未知量 |
第 5 行(开 unattended + /ru 用户名) |
✅ 有保证 | ?不确定 | 只剩一个未知量 |
第 2 行为什么是「大概率不通」而不是「确定不通」:
--unattended 的官方说明是 "keeps running even after the current GUI user logs out"
—— 反过来读,默认状态就是随 GUI 登出下线。所以没人登录时 Tailscale 服务本身可能已经不在了。
唯一可能翻盘的情形:/ru 用户名 /rp 密码 跑起来后,Windows 建了一个「准交互式会话」,
而 Tailscale 内部把它算作「GUI 用户在线」。没实测过,不好一口咬死。
要排除这个可能性,去下面「要自己验,用行为验证」那四步实测一次。
所以第 2 行 ≈ 第 1 行,别选。 区别只在于第 2 行留了个理论上的翻盘口子。
第 5 行明显优于第 2 行 —— unattended 的作用正是让服务不随 GUI 登出下线,
开了它就把变量 ① 消掉了(从「大概率不通」变成「有保证」)。
第 3 行和第 5 行把握相当,区别只在要不要把密码交出去(见下面的取舍)。
要自己验,用行为验证(这是唯一可靠的判据):
- 退出 Tailscale 托盘:托盘图标右键 → Exit
(只退 GUI 进程tailscale-ipn.exe,不要停服务 ——
服务还活着正是要测的东西) - 让任务以目标身份跑一次(
/ru SYSTEM就触发 SYSTEM 任务,/ru 用户名就触发那个任务) - 从另一台机器
tailscale ping <该机 IP> - 还 pong = 这一行的「在线」成立;不通 = 不成立
第 1 行的 ❌ 不用验,情况 A 那个 @@@@ 就是它。
推荐做法:
tailscale up --unattended=true
schtasks /create /tn SyncTask /ru SYSTEM ^
/tr "\"C:\Program Files\Git\bin\bash.exe\" \"D:\job\sync.sh\"" ^
/sc minute /mo 5 /f
⚠️ bash 路径要用 Git 的,不是 C:\Windows\System32\OpenSSH\bash.exe ——
后者根本不存在:那个目录是 Win32-OpenSSH 的实现目录,只有
ssh.exe / scp.exe / sftp.exe / sshd.exe。bash 在 Git 里(见 3.8)。
⚠️ 情况 A 和情况 B 的路径前提不同,别照抄。
情况 A(不写 /ru) |
情况 B(/ru SYSTEM) |
|
|---|---|---|
| 跑在谁上下文 | 当前用户 | SYSTEM |
| 读用户目录里的 AppData | ✅ 读自己的,没问题 | ⚠️ 取决于那个目录的 ACL |
实测(2026-10-04,一台 Windows 10 22H2,Git 装在用户目录 AppData\Local\Programs\GitFull):
> whoami
nt authority\system
> & "C:\Users\<用户名>\AppData\Local\Programs\GitFull\bin\bash.exe" -c "echo SYSTEM_BASH_OK"
SYSTEM_BASH_OK
同一次测试里查到的 ACL:
> icacls "C:\Users\<用户名>\AppData\Local\Programs\GitFull"
NT AUTHORITY\SYSTEM FullControl Allow
BUILTIN\Administrators FullControl Allow
但这不能当通例 —— ACL 收紧过的机器上可能读不到。两个做法:
:: 做法一(推荐):把 Git 装成系统级,路径固定,谁都能跑
:: https://git-scm.com/download/win 选 "System Level Git"
:: 做法二:先用 icacls 确认 SYSTEM 有读权限
icacls "C:\Users\<用户>\AppData\Local\Programs\GitFull" | findstr SYSTEM
看不到 SYSTEM 且带 (F) 就别用这条路,换成 /ru 用户名 /rp 密码。
--unattended的官方说明是:"run in Unattended Mode where Tailscale keeps running
even after the current GUI user logs out (Windows-only)",默认false,
不开这一步 Tailscale 就随 GUI 登出一起下线 —— 那正是情况 A 里 SYSTEM 看不到网卡的原因。- 任务侧用
/ru SYSTEM:LogonType 变成ServiceAccount,不依赖任何用户登录,
也不用把密码交给计划任务。 - 前提:tailnet 已经在这台机器上登录过一次(有有效节点密钥)。
--unattended只改运行身份,不能替代首次登录。 - 也可以在 GUI 里开:托盘图标右键 → Preferences → 勾选 Run unattended。
怎么确认 unattended 真的开着(tailscale status 不显示这个状态,别对着输出发愣):
tailscale debug prefs
(SYSTEM 上下文或 PATH 没它时用全路径 & "C:\Program Files\Tailscale\tailscale.exe" debug prefs。)
看输出里有没有 Unattended 字段。⚠️ 部分版本(如 1.102.4)这个字段压根不输出 ——
那就没法从命令行查,唯一的硬证据是行为验证:
- 托盘图标右键 → Exit(只退 GUI 进程
tailscale-ipn.exe;
不要停服务 —— 服务tailscaled还活着正是要测的东西) - 在另一台机器上
tailscale ping <该机 IP> - 还 pong = unattended 生效;不通 = 没生效
⚠️ Windows 11 24H2 上有额外一层(社区实测,非官方文档):
整个「完整网络初始化」被绑定到了交互式登录,即使 tailscaled 服务以 SYSTEM 启动、
虚拟网卡存在,未登录阶段 Tailscale 仍可能上不了线。
变通办法是开自动登录(netplwiz),开机自动登录一次把网络栈拉起来,之后再锁屏。
取舍:
- 自动登录省事但安全性降一档(开机后直接进桌面)。
- /ru 用户名 /rp 密码 避开自动登录,但有两个坑:
① 该用户改密码后任务即失败(报「登录失败:未知用户名或密码错误」);
② 企业域环境常开 网络访问: 不允许存储网络身份验证的密码和凭据 这条安全策略,
开了就此路不通,只能回到 /ru SYSTEM + unattended。
- 如果这台机器本来就有人守着,用情况 A 就够,不必上这套。
配套的三条省心技巧(做定时同步时一并用上)
- 先比对哈希,相同就跳过 —— 稳定态零传输,占用降到几乎为零
- 离线机标记 PENDING 后立刻跳过,不要重试等待;上线后下一轮自动补
- 状态存文件(如
sync-state.txt),只记「上次推送成功时的源文件哈希」,
下一轮拿它和当前源文件比
3.10 私钥权限不一致,会被 OpenSSH 直接拒掉 ❗
症状:同一台机器上,一把私钥能连上、另一把报
Permission denied (publickey,password,keyboard-interactive)。
根因:OpenSSH 客户端会检查私钥文件的 ACL,权限过松或带继承标记就拒绝使用。
最容易翻车的是「两把钥匙的权限不一样」—— 一把是继承来的、另一条是手工设过的。
先看清现状:
icacls C:\Users\<用户名>\.ssh\<私钥文件名>
输出里看三样东西:
- 有没有
(I)—— 继承标记,有就会被拒 - 本人权限是
(F)还是(R)—— 不统一就出事 - 有没有多余的主体(
Users、Everyone等)—— 有也要去掉
统一成「只有三个主体、全部完全控制」:
icacls <私钥路径> /inheritance:r
icacls <私钥路径> /grant:r "SYSTEM:(F)" "Administrators:(F)" "<用户名>:(F)"
icacls C:\Users\<用户>\.ssh /inheritance:r
icacls C:\Users\<用户>\.ssh /grant:r "SYSTEM:(OI)(CI)(F)" "Administrators:(OI)(CI)(F)" "<用户名>:(OI)(CI)(F)"
⚠️ 目录也要改 —— 只改文件不改目录,OpenSSH 仍会挑出继承来的那一层。
⚠️ 这条操作需要管理员权限,非管理员会静默失败
(EPERM: operation not permitted / Permission denied),改完记得回读 icacls 确认真的生效了。
第 4 层 · 远程执行:中文、空格与环境变量
4.1 中文路径的正解:-EncodedCommand
要把「含中文路径」的命令送到对面执行,唯一稳的写法:
powershell -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand <base64>
其中 payload 是 [Text.Encoding]::Unicode.GetBytes($code) 再 base64
(注意是 UTF-16LE,不是 UTF-8)。
好处是整条命令行全是 ASCII,彻底绕开 cmd 的编码问题,
也绕开了管道符被误切分(此前 |Out-Null 被 cmd 当成外部命令,报「'Out-Null' 不是内部或外部命令」)。
回传也一样:远端把文件名 base64 后打印,本机解码,避免本机按 GBK 解码成乱码。
4.2 路径带空格会被吃掉引号
命令行传 C:\Program Files\... 到对面,会出现
'C:\Program' is not recognized 这类错误——引号在转发过程中丢了。
→ 用 8.3 短路径:C:\Progra~1\。
4.3 含中文的 .ps1 必须 UTF-8 带 BOM
PowerShell 5.1 下,无 BOM 的 UTF-8 脚本里的中文字面量会变成乱码。
最典型的症状:某个中文文件名的判断永远为 False,还以为是逻辑错。
4.4 远端的 $env:TEMP 可能是空的
通过 SSH 拉起的会话里,$env:TEMP 有时为空,
于是 Test-Path -LiteralPath $null 直接抛参数绑定异常。
→ 远端脚本一律写死绝对路径,不要用 $env:TEMP 拼。
第 5 层 · 传文件:三条路怎么选
5.1 千万别信 Win32-OpenSSH 的 scp / sftp
症状:
pipe: Unknown error (exit 255)
看着像在传,其实静默失败——文件没过去,命令还可能返回 0。
scp -vvv 能给出决定性日志:
debug3: pipe - ERROR CreateFile() :231 → 231 = ERROR_PIPE_BUSY
Win32-OpenSSH 在 Windows 上用命名管道实现 pipe(),这是系统级失败,不是调用姿势问题。
同一个版本的 ssh 往往是好的(它对这个错误容错继续),所以「ssh 能连」不能证明 scp 能用。
自检方法:让 scp 连一个不存在的地址,比如 192.0.2.1。
正常应该走到 Connection timed out(说明 pipe 创建成功);坏的则根本到不了这一步。
→ 换 MSYS2 版的 scp / sftp(Git for Windows 自带,在 Git\usr\bin\ 下),立刻好。
(Windows 自带 sc.exe 那类同名命令会干扰,shim 时注意 PATH 顺序:生效 PATH 是系统 PATH 在前、用户 PATH 在后。)
5.2 Taildrop:慢活多路里最稳的一条
tailscale file cp 走的是官方通道,不依赖 ssh/scp,不需要开防火墙、不需要管理员权限。
代价是:
- 异步——投递命令返回 ≠ 对方已收到。要轮询等文件出现、且等大小稳定再动它,否则搬半截。
「稳定」给个可抄的阈值:文件大小 > 0,且连续 3 次轮询(间隔 1 秒)大小都没变。
大文件(10 MB 以上)把间隔放宽到 3 秒,但判据是「连续 3 次不变」,不是「等够几秒」。 - 文件落在对方 Downloads,且 Tailscale 跑在哪个账户下就落哪个账户的 Downloads。
- 同名文件会被加后缀。
5.3 主机名在局域网里会骗你 ❗
两台机器同处一个局域网时,主机名会被解析成 IPv6 链路本地地址 fe80::…%WLAN
而不是 Tailscale 的 100.x 地址,于是 tailscale file cp <主机名>: 报
is not a Tailscale IP address。
→ 投递一律写 100.x IP,不用主机名。
5.4 not in your Tailnet 多半是你自己的问题
tailscale file cp 报 unknown target; not in your Tailnet,
第一反应通常是「对方掉线了」,实际上多半是发起方自己处于 NoState(见 2.4)。
→ 先查自己的 tailscale status,别去折腾目标机。
5.5 怎么选
| 场景 | 用什么 |
|---|---|
| 人在本机,要丢个文件给对方、不指定目录 | Taildrop(最快最省心) |
| 要落到对方指定目录(含中文) | Taildrop 投递 + 一条 -EncodedCommand 让对方自己搬 |
| 大批量 / 要续传 / 要回读校验 | MSYS2 的 sftp,见配套技能 remote-win-backup-mesh |
| 同在一个局域网 | 直接用局域网 IP 走 SSH,能绕开「仅放行 100.64.0.0/10」的防火墙规则 |
| 做定时任务(同步、巡检) | bash 脚本 + 计划任务,见 3.9 与 5.6 |
5.6 node 里 spawn ssh/scp 会报 EBUSY —— 别硬套 node ❗
2026-10-04 实测:写了个 Node 脚本调 ssh/scp/taskkill,
全部返回 spawnSync <路径> EBUSY(有时是 exit=null,症状一样),
而同一台机器上用 bash 调完全相同的命令,一切正常。
这不是调用姿势问题,是文件锁。 排查时试过的、都无效的「解法」:
- 换绝对路径(System32 → Program Files → Git)—— 照样 EBUSY
- 清空环境变量、
spawnSync改成execSync、加windowsHide—— 照旧 - 在 bash 里用
node -e包一层再 spawn —— 也 EBUSY - 改成
powershell -File调脚本 —— 仍 EBUSY
最终有效的做法:直接用 bash 写。 Git for Windows 自带 bash(见 1.7),
它的 ssh/scp 走的是 MSYS2 那一套,没有这个锁问题。
#!/bin/bash
# 别在 node 里 spawn ssh,用 bash 直接跑
GF="C:/Users/$USER/AppData/Local/Programs/GitFull"
SSH_BIN="$GF/usr/bin/ssh.exe"
SCP_BIN="$GF/usr/bin/scp.exe"
SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL -o ConnectTimeout=25 -o LogLevel=ERROR"
# UserKnownHostsFile=NUL:Windows 设备名,意思是「不记录 known_hosts」。
# 在 Git Bash(MSYS2)下实测过 NUL 和 /dev/null 都能连上,但 **NUL 更干净** ——
# /dev/null 那次多打一行 "Warning: Permanently added ... to the list of known hosts"。
# 纯 Windows 环境(cmd/PowerShell 里的 ssh.exe)就只能用 NUL。
"$SSH_BIN" $SSH_OPTS -i "$KEY" "$USER@$IP" hostname >/dev/null 2>&1 \
&& echo "在线" || echo "离线"
"$SCP_BIN" $SSH_OPTS -i "$KEY" "$SRC" "$USER@$IP:$DEST"
判断该用哪个工具链:
| 你要做的事 | 用什么 |
|---|---|
| 单机运维、几条命令、想有结构化输出 | node(这台机上 spawn 别的 exe 没 EBUSY 的话) |
| 批量调用 ssh/scp、写计划任务 | bash(Git 自带) |
| 需要复杂判断 + 调 ssh | PowerShell + -EncodedCommand(见 4.1) |
原则:工具能跑就别硬套另一个。 在 Windows 上,
「bash 调 ssh」和「node 调 ssh」不是一回事,
后者多一层进程、多一把锁、多一个失败点,而收益是零。
工具:SendFile.ps1
scripts/SendFile.ps1(配 scripts/SendFile.bat 双击用)把上面第 5 层全部结论固化成了一个命令。
.\SendFile.ps1 # 交互:选机器 → 选文件(可多选)→ 发送
.\SendFile.ps1 -List # 只看机器清单
.\SendFile.ps1 -To <别名> -Path '.\报告.xlsx' # 发到对方 Downloads
.\SendFile.ps1 -To <别名> -Path '.\地图.xmind' -Dest 'C:\Users\<账号>\Desktop\某中文目录'
.\SendFile.ps1 -To all -Path '.\a.zip','.\b.pdf' -Dest 'D:\项目资料'
.\SendFile.ps1 -To <别名> -Path '.\大文件.zip' -Dest 'D:\资料' -WaitSec 90
它替你处理掉的事:
- 只用 IP,不用主机名(躲开 5.3)
- 发送前自检本机 Tailscale 是否 NoState(躲开 5.4)
- 中文目录名 / 文件名一律 base64 后下发,远端
-EncodedCommand执行(躲开 4.1) - 远端轮询等文件出现且大小稳定再搬(躲开 5.2 的半截问题)
- 回执里的文件名同样 base64 回传(躲开 GBK 乱码)
- 多把私钥依次试 + 成功即缓存(躲开 3.6 的白等超时)
ssh.exe位置挨个探测(躲开 3.7)- 远端扫描
C:\Users\*\Downloads而不是只认$env:USERPROFILE
——因为「跑托盘的账户」和「SSH 登录的账户」不一定是同一个(躲开 3.2 末尾那条) - 有重名文件时自动改成逐个发送(Downloads 是平铺目录,同名会互相覆盖)
- 判断投递失败不用
Out-String匹配error——PowerShell 会把 stderr 渲染成NativeCommandError,
一条无害警告就能把你误判成失败。它逐条取ErrorRecord.Exception.Message,并剔除is not replying那句提示。
改机器表:脚本顶部 $Machines 数组,四列分别是别名 / SSH 账户 / 显示名 / IP。改成你自己的即可。
端到端验收清单
新拉一台机器进群,按顺序打完勾:
- [ ] Tailscale 装上了,
tailscale status能拿到 100.x - [ ]
tailscale status --json里Self没有KeyExpiry字段(永久) - [ ]
tailscale up --accept-routes=false(没有子网路由需求就一直保持) - [ ] 入网用的 auth key 已在后台 revoke
- [ ]
where ssh至少两条候选,且已知该用哪一份(Git 自带的优先,见 3.8) - [ ]
icacls <私钥>看过:没有(I)继承标记,主体只有三个(见 3.10) - [ ]
ssh能连上——用户名是去目标机Get-LocalUser查出来的,不是猜的 - [ ]
ssh-keygen -y -P '' -f <key>能输出公钥(私钥没被密码加密) - [ ]
tailscale ping <对方>复查一次,是direct不是derp - [ ]
Test-NetConnection <对方> -Port 22是 OPEN(不是用 ping 判的) - [ ]
.\SendFile.ps1 -To <对方> -Path <一个小文件>成功收到 - [ ] 若要做定时任务:手动触发一次验证过(见 3.9)
- [ ] 若要做无人值守 · Tailscale 层:已开
--unattended
(tailscale debug prefs看Unattended;tailscale status不显示这个状态。
字段压根不输出的版本,用「退出托盘后从另一台 ping 还通不通」做行为验证)(3.9) - [ ] 若要做无人值守 · 任务层:用
/ru SYSTEM或/ru 用户名 /rp 密码,
不是不写/ru(不写就是Interactive,注销后不触发)(3.9)
排错速查
| 现象 | 真正的原因 | 怎么办 |
|---|---|---|
| 装 Tailscale 返回 1603 | iphlpsvc 被禁 |
起服务后再装(1.2) |
tailscale-setup.exe 装不动 |
它是联网引导器 | 下完整 MSI(1.1) |
tailscale ping 报 via DERP |
打洞协商临时态 | 复查(2.1) |
ping 全丢但服务能开 |
ICMP 被 DHCP/防火墙/AP 隔离丢 | 用 -Port 判(2.2) |
| 主机名通、IP 不通 | 本机 HTTP 代理 | 清 http_proxy(2.3) |
| 「这机器没装 Git / 没 bash」 | 只查了 C:\Program Files\Git\,实际装在用户目录 |
先跑 where(1.7) |
where 出来好几行,不确定用哪个 |
多份实现(Git 的 / OpenSSH 的) | 挨个真连一次,能连上的那个(3.8) |
| 同一台机器上 ssh 时通时不通 | 换成了 Windows 自带的 ssh.exe | 用 Git 自带那份(3.8) |
计划任务里 SSH 回显一串 @@@@ |
SYSTEM 身份拿不到 Tailscale 网卡 | 不写 /ru(3.9 情况 A) |
| 计划任务报「找不到系统文件」 | 同上,PATH 里也没有 ssh | 不写 /ru + 脚本里用绝对路径(3.9) |
| 手动能连、计划任务不行 | 同上 | 同上(3.9 情况 A) |
| 机器重启后没人登录,计划任务就不跑了 | 任务 LogonType 是 Interactive(不写 /ru 的默认值),注销后压根不触发 |
任务改 /ru SYSTEM 或 /ru 用户名 /rp 密码;无人时还要 Tailscale 在线得另开 --unattended(3.9 情况 B,两个开关要配齐) |
想查 unattended 开了没,tailscale status 里找不到 |
status 本来就不显示这个字段 |
tailscale debug prefs 看 Unattended;该版本不输出的话,用「退出托盘后从别的机器 ping」做行为验证(3.9) |
Permission denied (publickey...) |
用户名不存在 或 公钥不匹配,返回一样 | Get-LocalUser 查真实账户(3.2) |
一把私钥能连、另一把 Permission denied |
私钥 ACL 不一致(有 (I) 继承标记或权限不一致) |
icacls 统一(3.10) |
Server accepts key 仍被拒 |
客户端私钥被加密/读不了 | 查 -N ''(3.3 / 3.4) |
| 公钥贴了没生效 | .pub 带 BOM |
去掉 BOM(3.5) |
scp pipe: Unknown error |
Win32-OpenSSH 的 named pipe 缺陷 | 换 MSYS2/Git 版(5.1 / 3.8) |
node 里 spawn ssh/scp 报 EBUSY |
文件锁,node 这条路在这台机上走不通 | 改用 bash(5.6) |
spawnSync ... exit=null |
同上(EBUSY 的另一种表现) | 同上(5.6) |
is not a Tailscale IP address |
主机名被解析成 fe80:: | 用 IP(5.3) |
用短主机名(ssh config 里的别名)报 ENOTFOUND |
那是你本机 ssh config 里的别名,MagicDNS 不认 | 用全名或 100.x IP(5.3) |
| 把「连不上」当成「机器离线」 | 短名解析失败 ≠ 机器不在线 | 先 tailscale ping <IP> 确认(2.2 / 5.3) |
not in your Tailnet |
发起方自己 NoState | 查自己 status(5.4) |
远端 'C:\Program' not recognized |
空格路径引号被吃 | 8.3 短路径(4.2) |
远端 'Out-Null' 不是内部或外部命令 |
管道符被 cmd 切了 | -EncodedCommand(4.1) |
| 远端含中文脚本报假的解析错误 | 无 BOM 的 UTF-8 | 加 BOM(4.3) |
Test-Path 参数绑定异常 |
远端 $env:TEMP 为空 |
写死绝对路径(4.4) |
| 电脑能上、手机/平板卡在登录页;换 APK、扫码都不行 | tailnet 绑的是 Google 账号,accounts.google.com 在大陆打不开 |
客户端改用 auth key(1.6) |
平板浏览器打不开 controlplane.tailscale.com |
设备本身出不了网 | 换 DNS / Wi-Fi / 关厂商拦截(1.6) |
| 传完是 0 字节 / 半截 | Taildrop 异步,没等稳 | 轮询等到大小稳定(5.2) |
| 文件传到对方却找不到 | 托盘账户 ≠ SSH 登录账户 | 扫 C:\Users\*\Downloads |
| 本地网络全断 | 子网路由与自己网段重叠,隧道路由 metric 更低造成黑洞 | --accept-routes=false(1.5) |
| ssh 能连但 scp 传不过去 | 又是 Win32 那份管道缺陷 | 换 Git 自带的(3.8 / 5.1) |
边界声明
不做:跨平台的 Linux/macOS 节点;Tailscale 之外其它组网方案的配置(WireGuard 原生、ZeroTier 等);
ACL / 多用户权限治理;替代 remote-win-ssh-offline(补 SSH 服务端)与
remote-win-backup-mesh(大批量带校验搬运)——那两个是配套的上下游。
也不替用户决定「哪台机器该广播哪个网段」,这件事必须由人先确认真实内网地址再动。
资源
scripts/SendFile.ps1—— 四机互传工具,改顶部机器表即用;支持指定中文目标目录scripts/SendFile.bat—— 双击入口(本地自用;对外发布时服务端不收.bat,走上面那个 ps1 即可)