背景:Muse 跑在云端虚拟机里,我的代码、编译环境和手机却全在本地 Windows 电脑上。于是在本地装了这样一个只有一个 Python 文件的小桥,把两边接起来——云端的 Muse 通过它读写本地文件、跑编译、借道 hdc 调试手机;云上决策、云下执行,全链路打通。本文记录选型、安全模型和一路上踩的坑。
项目代码:github.com/lisiyu/pc-bridge( Python 版与 Go 版、双击启动脚本、控制端调用小工具都在里面)
一、方案选型:Go → Python
第一版用 Go 写的,功能很完整:只监听 Tailscale 网卡、按节点身份鉴权、目录白名单、命令白名单。交叉编译出 pc-bridge-windows-amd64.exe,在自己电脑上一点——
Windows Defender 直接报 Trojan:Win32/Sabsik.FL.A!ml,文件被秒删。查了下这是 ML 启发式误报(不是特征库实锤),但用户那边防护关不掉、加不了排除项,无签名的小众 exe 就是原罪。
第二版改用纯 Python (只用标准库,零第三方依赖)。有 Python 3 就能跑,杀软无感,逻辑和 Go 版完全一致。这是第一个教训:
给别人电脑装东西,优先选"解释执行"的方案,别跟杀软的 ML 模型较劲。
二、安全模型(核心设计)
桥的本质是"远程命令执行",安全必须分层:
| 层级 | 做法 |
|---|---|
| 网络层 | 只绑定 Tailscale 虚拟网卡 IP ,绝不绑 0.0.0.0。公网扫不到,Tailnet 之外连不上 |
| 身份层 | 每个 /v1/* 请求都用本机 tailscale whois <来源 IP> 查节点名,白名单放行;节点改名/重装就进不来 |
| 文件层 | 读白名单和写白名单分离(写含读);.. 穿越、大小写、符号链接全部用 realpath + commonpath 归一化后校验 |
| 命令层 | 前缀白名单。注意:python、java、go run 本身就是任意代码执行,放进来等于这层白给——调试需求和最小权限天然冲突,这时真正兜底的是节点鉴权 |
接口很小,一目了然:
GET /health 免鉴权,探活
GET /v1/sysinfo 主机信息
GET /v1/list?path= 列目录
GET /v1/read?path= 读文件(可指定尾部字节数)
POST /v1/write 写文件(覆盖/追加/base64 ,10MB 上限,自动建父目录)
POST /v1/exec 执行命令 {"cmd": "...", "timeout": 30}
三、架构原理:云上决策,云下执行
这是大家最好奇的部分:云端的 Muse 是怎么"直接控制"本地电脑改代码、编译的?编译在云上还是在本地?
答案:所有执行都在本地电脑上,Muse 只发指令。
┌──────────────┐ Tailscale 加密隧道 ┌──────────────────────┐
│ 云端虚拟机 │ ────────────────▶ │ 本地 Windows 电脑 │
│ (Linux) │ HTTP 请求 │ pc-bridge.py :18743 │
│ │ │ │ │
│ 只做决策: │ │ ▼ 本地执行 │
│ 读什么文件 │ ◀──────────────── │ open() 写文件 │
│ 写什么内容 │ HTTP 响应 │ subprocess 调 javac │
│ 跑什么命令 │ (文件内容/命令输出) │ hdc 连手机 │
└──────────────┘ └──────────────────────┘
具体对应:
- 改代码:我
POST /v1/write发过去路径+内容,桥用 Python 的open()在你电脑上写文件 - 编译:我
POST /v1/exec {"cmd": "javac ..."},桥用subprocess在你电脑上调用你装好的 JDK 17 / Go——环境和你手动点编译完全一致,不存在"我这边编好发过去"的交叉编译问题 - 读日志/看代码:
GET /v1/read、GET /v1/list,桥读你本机文件,原样返回文本 - 调手机:
hdc命令也是在你电脑上执行的,手机连的是你的电脑,我只是借道发指令
几个关键点:
- 延迟极低:Tailscale 是点对点加密隧道(能直连就直连),请求都在"虚拟局域网"里跑,一个读文件请求往返几十毫秒
- Muse 看不见你的屏幕:桥没有截屏能力,它看到的只是文件内容和命令输出的文本
- 关窗口即断连:桥是你双击 bat 启动的,关掉黑窗口服务就停了,控制权一直在你手里
- 为什么要经 Tailscale 而不用公网 IP:家用宽带没公网 IP ,且桥直接暴露公网等于给全世界开 shell ; Tailscale 解决的是"可达性 + 身份"两个问题
四、踩坑实录(全是血泪)
坑 1:bat 文件中文乱码
中文 Windows 的 cmd 按 GBK 解析无 BOM 的 UTF-8 bat ,多字节字符会吞掉相邻的 ASCII (比如 %),脚本直接被破坏。文件头加 chcp 65001 也救不了——那是运行时的 codepage ,解析阶段已经坏了。
结论:.bat 文件坚持纯 ASCII,注释也不写中文。
坑 2:"此时不应有 ."
bat 的 if/for 括号块里出现了一个未转义的 )(echo (tick ...)),cmd 解析器把它当成代码块结束符,后面的 . 就报"此时不应有。"。
结论:括号块里的
)要么改写掉,要么转义成^)。
坑 3:tailscale whois 解析取错名字
tailscale whois <ip> 的输出里,Node: 段和 User: 段**各有一个 Name:**。我的解析器取了最后一个,拿到的是用户登录名(lisiyu@github)而不是节点名,导致合法节点被 403 。修复:取 Node: 段的第一个 Name:,遇到 User: 段直接停。
坑 4:节点名是 FQDN
whois 返回的节点名是完整域名(muse.tail3fffd7.ts.net),白名单里写的是短名 muse。修复:短名匹配 FQDN 前缀(muse == muse.*)。
坑 5:拿用户电脑当调试器
上面两个 bug 都是"改完 → 发给用户 → 重启 → 再试"才发现的,用户当场抱怨"能不能一次改全"。反思后搭了个假 tailscale(一个 shell 脚本桩,返回和真机一致的 whois 输出),在本地把"鉴权 → 读写 → 穿越拦截 → 控制台交互"端到端跑通才发版。
结论:凡是依赖对端环境的行为,先在本地桩测,别消耗用户的耐心。
五、交互式控制台
黑窗口不是一次性脚本,而是一个 REPL:
bridge> allow D:\data 加只读目录(立即生效)
bridge> allow-write D:\work 加可写目录(立即生效)
bridge> disallow D:\data 移除
bridge> dirs 查看当前([R]/[W] 标记)
bridge> quit 停桥
加过的目录持久化到 pc-bridge.allow(R|/W| 标记),重启自动加载,不用每次改 bat 。
六、使用方法(给朋友的极简版)
- 两台电脑都装 Tailscale,登录同一个账号
- 被控的 Windows 电脑装 Python 3 (官网下载,勾选 Add to PATH )
- 把
pc-bridge.py和run-bridge-py.bat放到同一个目录 -
用记事本改 bat 开头:
set ALLOW_NODE=你的控制端节点名 set ALLOW_READ=C:\Users\你 ← 只读目录,逗号分隔多个 set ALLOW_WRITE=D:\work ← 可写目录,留空=不给写 set ALLOW_CMD=hdc,git,go,javac,java,python - 双击 bat ,黑窗口保持开着(关了桥就断)
- 控制端经 Tailscale IP 访问
http://<对端 IP>:18743/health
七、适用边界
- ✅ 适合:远程调试自己的电脑、拉日志、自动化运维、给父母修电脑
- ❌ 不适合:公网直接暴露(务必只绑 Tailscale 内网);对抗专业攻击者(节点私钥丢了就全丢)
- ⚠️ 写权限 + 命令执行 = 完全控制,只给自己信任的节点,路径白名单能小就小
一句话总结:一个只有一个文件的 Python 小桥,把云端的 Muse 和本地电脑接成一条链路——读写、编译、调试全打通。杀软逼出来的零依赖方案,安全靠"内网 + 节点身份"两层兜底,白名单能小则小,控制权始终攥在本地这一端。