一台长期稳定运行的 Windows 服务器,某天开机后 Tailscale 突然连不上了。排查下来发现:它不是"今天坏的",而是已经带病运行了六周。本文记录完整排查链路、几个看似正确实则无效的修复尝试,以及最后的定论。
文中主机名、IP、账号、节点 ID 等均已脱敏。
一、现象
开机后,托盘图标报错,远程组网完全中断:
- 客户端显示异常状态,拿不到任何 Tailscale IP
tailscale status输出:
# Health check:
# - Tailscale is starting. Please wait.
unexpected state: NoState- 服务本身是
Running、启动类型Automatic,看起来"没问题"
典型的"服务活着,但后端死了"。
二、环境
| 项目 | 值 |
|---|---|
| 系统 | Windows Server 2016(10.0.14393) |
| Tailscale | 1.98.4(MSI 安装,以服务方式运行) |
| 服务账户 | LocalSystem |
| 网络 | USB 无线网卡 + VMware 虚拟网卡 |
| 安全软件 | 第三方杀毒/卫士常驻 |
服务依赖为 Dnscache、iphlpsvc、netprofm、WinHttpAutoProxySvc,失败恢复策略为"重启",带递增延迟(1s→64s)。
三、排查过程
3.1 先看日志,不要猜
Tailscale 在 Windows 上的日志位于:
C:\ProgramData\Tailscale\Logs\tailscale-service-<时间戳>-<ID>.txt最新一份日志立刻暴露了三个关键错误:
TPM: error opening: TBS Error 0x8028400F: A compatible Trusted Platform Module
(TPM) Security Device cannot be found on this computer.
Failed to setup adapter (problem code: 0x0, ntstatus: 0x0): 设备未就绪 (Code 0x000010DF)
profile data directory: profile not found三条错误的性质完全不同,需要分开归因。
3.2 第一层:开机时序竞争(可解释"为什么偏偏开机出错")
08:57:17 Creating adapter
08:57:32 Timed out waiting for device query: 等待的操作过时 (Code 0x00000102)
08:57:32 Failed to setup adapter: 设备未就绪 (Code 0x000010DF)
08:57:32 tstunNew: backoff: 5 msec
08:57:41 waitInterfaceUp: TUN interface already up时间线很清楚:开机第 17 秒开始创建虚拟网卡,5 秒后超时失败,退避重试,第 41 秒才建起。
原因是 Tailscale 服务是 AUTO_START,但开机那一刻网络栈还没就绪——尤其是 USB 无线网卡枚举慢、NetSetupSvc 还在初始化。日志里甚至打印了 NetSetupSvc 的完整状态快照(RUNNING / DEMAND_START),说明程序自己也意识到这是个服务依赖时序问题。
这一层会造成"开机瞬间报错",但它有退避重试,最终能自愈——不是致命的。
3.3 第二层:后端陷入死循环(真正的致命伤)
继续往下看日志,出现了极有规律的循环,大约每秒一轮:
client connected (...\Administrator): switching to profile (xxxx)
auditlog: stopped for profileID: xxxx
profile data directory: profile not found
Switching ipn state NoState -> NeedsLogin (WantRunning=false)
client disconnected (...\Administrator): disconnecting Tailscale同时伴随:
magicsock: ReSTUN("link-change-minor") ignored; stopped, no private key
auditlog: [unexpected] failed to restore logs: empty keyno private key 是决定性的一条——后端认为自己没有节点私钥,所以永远进不了 Running,只能停在 NeedsLogin;GUI 一连接就要求切 profile,切不动就断开,断开又重连,形成死锁。
3.4 第三层:state 文件里的命名不一致
嫌疑自然落到 state 文件 C:\ProgramData\Tailscale\server-state.conf。它是一个 JSON,值是若干 base64 编码的 blob:
{
"_current/S-1-5-21-...-500": "<base64>",
"_machinekey": "<base64>",
"_profiles": "<base64>",
"profile-xxxx": "<base64>"
}逐个解码后,问题浮出水面——同一个 profile,在四个地方用了两种不同的标识:
| 位置 | 值 |
|---|---|
_profiles 的 map 键 | xxxx |
_profiles 里的 Profile.Key | profile-xxxx |
_current/<SID> 的值 | profile-xxxx |
| 存 prefs 的顶层键 | profile-xxxx |
| 磁盘上的 profile 目录 | xxxx |
而程序内部是混用的:_current 和 ProfileDir 走 Key,map 查找走 ID。两套标识混搭,必然有一边查不到。
_machinekey 里的机器私钥本身是完好的(明文,因为这台机器没有 TPM,StateEncrypted: false),_profiles 里的 PrivateNodeKey 也在——身份没丢,只是目录对不上号。
3.5 时间线回溯:这不是今天才坏的
写个脚本扫一遍 Logs 目录里所有历史日志,统计 profile not found / no private key 的出现:
- 命中 51 个日志文件
- 最早一条:2026-10-02
也就是说,这个问题至少存在了六周。期间它每次都能"挣扎着连上",直到今天开机后彻底放弃。
再看文件时间戳,答案更清楚:
server-state.conf 2026-06-29 18:40 ← 4 个半月没更新过
profile-data/<id>/ 2026-06-29 18:40 ← 同上,且目录里只剩 netmap-cache6 月 29 日正是升级到 1.98.4 的那天。 升级时 state 格式做了迁移,但 profile 目录结构没迁移干净——从那以后就一直带病运行。
四、几个"看起来对、实际无效"的尝试
这部分可能比结论更有价值,都是真实踩过的坑。
尝试 1:给 profile 目录建软链接
猜测是程序找 profile-data/profile-xxxx,而磁盘上是 profile-data/xxxx,于是建了个 Junction 指向过去。
结果:报错一模一样。 说明瓶颈不在目录名。
尝试 2:统一 _profiles 的 map 键
把 map 键改成 profile-xxxx,与 Key 对齐。
结果:错误变了。
an error occurred; staying on empty profile:
[unexpected] profile not found: <账号> (xxxx)从"找不到目录"变成"找不到 profile",说明程序查 map 时用的是 ID,我还原方向了。立即回滚。
尝试 3:反向统一成纯 ID
既然 map 用 ID、目录用 ID,那就把 Key、_current、顶层 prefs 键全部改成 xxxx,四者对齐。
结果:有进展,但没解决。 prefs 这次成功加载了——WantRunning 变回 true、LoggedOut 变回 false、NodeID 也回来了。但 profile data directory: profile not found 依然把后端打回 NeedsLogin。
尝试 4:删掉 profile 目录让程序重建
停服务,把 profile-data/<id> 整个删掉,期望程序自动重建并完成迁移。
结果:目录确实被重建了(说明程序知道路径),但里面只有 netmap-cache,报错照旧。
尝试 5:排查权限
怀疑 LocalSystem 对 C:\ProgramData\Tailscale 没权限。
结果:ACL 完全正常,SYSTEM 与 Administrators 都是 FullControl,且正常继承。排除。
尝试 6:反复 tailscale up
返回码 0,但状态纹丝不动。CLI 读的是后端状态,后端卡在 NoState,up 无从下手。
六次尝试的共同点:都在修"身份数据",而真正的断点是"程序无法把身份数据挂载到 profile 目录上"。 这个挂载逻辑在二进制里,没有配置项,也没有文档——逆向成本远高于重装的成本。
五、最终方案:清 state,重新认证
判断依据很朴素:这是格式迁移的残骸,不是可配置项的问题。继续打补丁是在跟未知的内部实现博弈。
操作步骤:
# 1. 停服务与 GUI
Stop-Service -Name Tailscale -Force
Stop-Process -Name tailscale-ipn -Force
# 2. 完整备份(关键,别省)
# server-state.conf + profile-data/ 全部拷走
# 3. 清理损坏的 state
Remove-Item "C:\ProgramData\Tailscale\server-state.conf" -Force
Remove-Item "C:\ProgramData\Tailscale\profile-data" -Recurse -Force
# 4. 启动服务
Start-Service -Name Tailscale
# 5. 触发认证,拿到链接
tailscale login --timeout=15s
# → To authenticate, visit: https://login.tailscale.com/a/xxxxxxxx在浏览器完成授权后即可恢复。
代价必须提前想清楚:
- 节点会重新注册,Tailscale IP 大概率变化
- 任何依赖旧 IP 的 SSH、RDP、反向代理、监控配置都需要同步修改
- 控制台里可能残留一个离线旧节点,需要手动清理
- 授权时必须使用原来的账号,换账号会进入另一个 tailnet,等于重建网络
如果有强依赖固定 IP 的场景,可以在授权前先去控制台的 DNS 页面把旧 IP 占位,或改用其他方式保持可达性。
六、事后加固
1. 服务改为延迟自动启动
针对第一层的开机时序竞争:
sc.exe config Tailscale start= delayed-auto让服务在系统启动约 2 分钟后再拉起,避开网络栈初始化高峰,尤其是 USB 网卡、虚拟网卡较多、装了安全软件的机器。
2. 定期备份 state
C:\ProgramData\Tailscale\server-state.conf 加上 profile-data\ 就是这台机器的全部身份。做个计划任务定期冷备,出事时至少能回滚到一个可用版本。
注意:state 与机器绑定,不能拷到别的机器上复用,只能用于本机回滚。
3. 升级后主动验证
升级完别只看"服务在跑",要实际验证:
tailscale status # 必须能列出节点,而不是 NoState
tailscale ip -4 # 必须有 IP4. 监控日志里的关键词
把这两个模式加进巡检脚本,早发现早处理:
no private keyprofile ... not found
它们出现时服务依然是 Running,任何只看服务状态的监控都发现不了。
七、几条经验
- 服务
Running不等于功能可用。 Tailscale 是"服务容器 + 后端状态机"的结构,NoState/NeedsLogin这类状态必须单独检查。 - 先看日志时间线,再动手。 本文最关键的发现不是某个报错,而是"这个问题从 10 月 2 日就开始了"——它直接改变了修复策略,从"修一个开机 bug"变成"处理一次四个月前的迁移事故"。
- 小版本升级也可能是结构变更。
1.96 → 1.98看着像补丁版本,实际动了 state 的存储结构。升级前备份 state 是便宜到可以无脑做的保险。 - 修不动就重装,别硬刚。 逆向二进制内部逻辑的时间成本,几乎总是高于重新认证。前提是:先判断清楚"代价是什么"(本例是 IP 变化),再决定值不值。
本文中的主机名、Tailscale IP、公网地址、账号标识、节点 ID 及认证链接均已脱敏处理。