用 Tailscale 远程登录家里的 Mac mini
今天要去深圳出差,下周日才回新加坡。出门前我顺手在手机上问了 Claude 一句:我去深圳这几天,你不会封我号吧?结果它搜了一圈,很认真地告诉我:新政策两天前刚生效,中国大陆、香港、澳门都不在支持地区名单上,出差、旅游都不能用,连机场转机都算;判断标准是人实际在哪里,用新加坡号码漫游或者境外 eSIM 也不行,“最稳妥的做法是这几天别用”。

我意识到,不妙,这一周的活儿可怎么干。。。
好在家里还有一台常年开机的 Mac mini,上面本来就连着 Tailscale。那就远程登回去,在那台机器上干活嘛!(先说清楚:政策是按人在哪算的,这么干并不等于合规,风险自负,这篇只聊技术。)本以为十分钟的事,结果从“为什么显示离线”一路折腾到“为什么这么卡”,踩了一串坑,值得写一篇,分享给有需要的朋友。
明明连着,为什么“离线”?
第一步就卡住了。Mac mini 明明开着、Tailscale 也连着,MacBook 这边 ping 不通、ssh 超时,tailscale status 里它那一行是这样的:
|
1 2 3 |
100.117.x.y 。。。。 100.117.x.y macmini macOS - 100.101.x.y titan-2-elite android offline, last seen 5d ago |
我一看最后一列是个 -,就以为是离线。其实不是:- 只表示当前没有活跃连接,真离线的会像下面那行一样明明白白写 offline, last seen ...。
那为什么连不上?看一眼路由就明白了:
|
1 2 3 4 |
$ route -n get 100.117.x.y destination: default gateway: 192.168.0.1 interface: en0 |
去 100.x 的流量走的是 en0 的默认网关,直接发到公网去了,当然不通。再看进程:
|
1 |
/opt/homebrew/bin/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 --outbound-http-proxy-listen=localhost:1056 |
破案了。我的 MacBook 上跑的是 Homebrew 装的 tailscaled,用的是 userspace networking 模式(当初这么装,就是因为不想让它动我的路由表,公司 VPN 已经够乱了)。这个模式的特点是:
1. 不创建虚拟网卡,系统里没有 100.x 的地址,路由表里一条相关路由都没有。
2. 不改 DNS。
3. 只在本机开两个代理端口:SOCKS5 的 1055 和 HTTP 的 1056,想访问 tailnet 里的机器,必须自己把流量送进代理。
所以 tailscale ping 能通(它走的是 tailscaled 进程自己),系统的 ping 和 ssh 却全部超时。这个现象特别有迷惑性。至于为什么用这个模式,其实是因为tailscale和我公司的部分内网服务冲突(都是100.x.x.x段的),我故意设置的。
ssh:让它走 SOCKS5
知道原因就好办了,在 ~/.ssh/config 里加一段:
|
1 2 3 4 5 |
Host macmini HostName 100.117.x.y User zs ProxyCommand nc -X 5 -x localhost:1055 %h %p LocalForward 5901 localhost:5900 |
ProxyCommand 那行让 ssh 通过 nc 走 SOCKS5 代理;LocalForward 那行是给后面的图形界面准备的。配完 ssh macmini 直接就进去了。
图形界面:屏幕共享 + SSH 隧道
我要用Claude,虽然也可以用CLI,但有图形界面其实更好。对面是台 Mac,图形界面用系统自带的“屏幕共享”就行,不用装任何东西。在 Mac mini 上打开:系统设置 → 通用 → 共享 → 屏幕共享。
问题是 MacBook 上的“屏幕共享”App 不会走 SOCKS5 代理,没法直接填 100.x 的地址。这就是上面那行 LocalForward 的用处:ssh 连上之后,本机的 5901 端口就被转发到了 Mac mini 的 5900(VNC 端口)。于是:
|
1 2 |
ssh -fN macmini # 后台建隧道 open vnc://zs@localhost:5901 # 或者 Finder 里 ⌘K |
登录窗口就出来了,还真挺顺。
能不能不占用真机的桌面?
家里的 Mac mini 不止我一个人用。我远程登上去的时候,如果正好有人坐在它前面,两个人抢一个桌面就很尴尬了。
macOS 的屏幕共享其实自带“后台会话”:连接的时候会弹一个选择,“请求共享显示器”还是“以本人身份登录”,选后者就进入自己账户的虚拟显示器,真机屏幕上的人完全不受影响,只会在菜单栏看到一个小图标。
但是这个选择不是每次都弹,规则是这样的:
1. 真机前台是别的账户在用:弹选择,可以进后台会话。这是理想情况。
2. 真机停在登录窗口,前台没人:不弹,直接把物理屏幕给你。右上角会有一行 “Your screen is being observed”,这时候输密码登录,真机显示器上会同步显示你的桌面。
3. 真机前台就是你自己的账户:看到的就是真机上那个桌面,同一个会话。
(第 2 条是我实际连上去看到物理屏幕的登录界面才搞明白的,一开始还以为登录窗口状态下进去也是后台会话。)
想知道真机前台现在是谁,ssh 上去一条命令:
|
1 |
stat -f %Su /dev/console # 其实就是看这个设备文件的属主 |
输出是用户名就是那个人在前台,输出 root 就是停在登录窗口。
另外同一个账户同一时间只有一个桌面会话。所以实际用法是:没人用真机时我直接登;有人要用,就让他用自己的账户登录,我的会话会退到后台不会丢,我这边断开重连一次,选“以本人身份登录”就回来了。
为什么这么卡?中继和直连
连是连上了,但画面有点卡。tailscale ping 一看:
|
1 2 |
pong from macmini (100.117.x.y) via DERP(sin) in 19ms direct connection not established |
流量走的是 DERP 中继(新加坡节点),没有建立直连。DERP 是 Tailscale 在打洞失败时的兜底,能通,但是限速,延迟也不稳。
而 tailscale status 的最下面其实一直挂着一条告警,我之前压根没在意:
|
1 2 |
# Health check: # - The MagicSock function ReceiveIPv4 is not running. You might experience connectivity issues. |
意思是 tailscaled 里负责收 IPv4 UDP 包的那部分停了。直连靠的就是 UDP,收不到包只能退回中继。这个进程已经连续跑了好几天,而且本机到所有节点都是中继,说明问题在我这边,不在 Mac mini 那边。
解决办法朴实无华,重启:
|
1 |
sudo launchctl kickstart -k system/com.tailscale.tailscaled |
它按原来的 plist 把进程再拉起来一次,参数不变,所以依然不碰路由和 DNS,登录状态也不用重新搞。副作用是代理端口会断几秒,已有的 ssh 隧道和屏幕共享会掉线,重连即可。
重启前后各测了一组:
|
1 2 3 4 5 |
重启前(中继) 重启后(直连) 路径 DERP(sin) 192.168.x.y:41641 延迟 平均 18ms,波动大 平均 11ms(8~17ms) 上传 18.1 Mbit/s 117.1 Mbit/s 下载 21.2 Mbit/s 78.2 Mbit/s |
吞吐是原来的四到六倍,屏幕共享一下子就顺滑了。
这里有个很尴尬的细节:直连地址是 192.168.x.y,也就是说,我人还在家里,两台机器就在同一个局域网,之前的流量居然一直在绕新加坡的中继。。。到了深圳肯定跑不出这个数字,能不能直连还要看两边的 NAT,到时候再测。
一条命令搞定
出差在外,不想每次都手敲这一串,于是让 Claude Code 帮我写了个脚本 macmini,放在 ~/.local/bin 里:
|
1 2 3 4 5 6 7 |
macmini 检查环境,然后打开屏幕共享 macmini ssh 检查环境,然后进 SSH macmini up 路径… 上传文件或目录 macmini down 路径… 下载文件或目录 macmini sync 路径… 双向同步,谁的修改时间新就听谁的 macmini bench 测路径(直连/中继)、延迟、吞吐 macmini stop 关掉后台隧道 |
“检查环境”是依次做这几件事,哪步不过就停下来说人话:
1. tailscaled 进程在不在,不在就用 launchctl 拉起来。
2. Tailscale 是不是已连接状态,不是就 tailscale up。
3. SOCKS5 代理端口有没有在监听。
4. Mac mini 在线不在线、ping 不 ping 得通。
5. 隧道已经有了就复用,没有就新建;确认对面 5900 开着,最后 open vnc://...。
bench 的核心就三条命令,嫌麻烦的话不写脚本直接手敲也行:
|
1 2 3 4 5 6 7 8 |
# 路径和延迟 tailscale ping --c 10 --until-direct=false macmini # 上传 dd if=/dev/zero bs=1m count=10 | ssh macmini 'cat >/dev/null' # 下载 ssh macmini 'dd if=/dev/zero bs=1m count=10' >/dev/null |
用 time 掐一下,10MB 除以秒数就是吞吐(记得扣掉 ssh 握手的一两秒)。屏幕共享 App 自己没有帧率、延迟之类的统计面板,但卡不卡基本就看这三个数。
顺手把传文件也解决了
我实测过,远程桌面状态下,系统剪贴板是能双向互通的,这其实很好了,但文件的话还是要两头倒腾,屏幕共享的拖拽走中继的时候慢得感人。ssh 既然通了,就给脚本加了 up / down / sync 三个子命令,底下就是 rsync,文件和目录都行。
唯一值得说的是路径。两台机器上我的用户名不一样,比如一边是 /Users/zhangsan,另一边是 /Users/zs,每次手敲两遍路径实在是烦。所以脚本会自动换算:两边保持同一个位置,只把家目录前缀互换,路径按哪边的写法给都行,相对路径也行。
|
1 2 3 |
$ cd ~/dev $ macmini up foo ==> up 本机 /Users/zhangsan/dev/foo <-> macmini /Users/zs/dev/foo |
sync 是用 rsync 的 -u(接收方的文件更新就跳过)来回各跑一遍实现的:
|
1 2 |
rsync -azu ~/dev/foo/ macmini:/Users/zs/dev/foo/ rsync -azu macmini:/Users/zs/dev/foo/ ~/dev/foo/ |
两遍跑完,每个文件都是两边里较新的那一份。简单是简单,但它不会同步删除(一边删掉的文件,下次会从另一边拷回来),两边都改了同一个文件也不会合并,新的那份直接赢。真要两边同时改,还是老老实实用 git 吧。
几个坑
1. tailscale status 最后一列的 - 不是离线,离线会写 offline。
2. userspace networking 模式下,系统的 ping、ssh、浏览器访问 100.x 全部超时,只有 tailscale ping 能通。别怀疑对面,先 route get 看一眼流量去了哪。
3. tailscale ping 有两个反直觉的行为:只拿到中继回应时,退出码是非 0(哪怕 pong 都收到了),写脚本不能靠退出码判断通不通;一旦拿到直连回应,它就提前结束,想测满 10 次要加 --until-direct=false。
4. 屏幕共享也可以 ssh 上去用 launchctl 命令行开启,但新版 macOS 这么开可能只能看、不能控制,完整控制还是得在系统设置里点一下。出门前务必先点好!
5. 真机停在登录窗口时连上去,拿到的是物理屏幕,不是后台会话。家里有人的话,你的桌面会直接出现在显示器上。
6. status 末尾的 Health check 告警别无视。直连全挂、只剩中继,很可能就是它。
7. 新版 macOS 自带的 rsync 其实是 openrsync(rsync --version 第一行就写着),跟平时在 Linux 上用的不完全一样:没有 --protect-args,远端路径里有空格得自己转义;-v 打印中文文件名时会冒出一堆 \#202 这样的转义。别慌,只是显示问题,文件本身是好的。
不说了,我得马上把本机的claude都退出,然后收拾行李赶飞机去。到了深圳第一件事就是跑一遍 macmini bench,看看隔着这么远还能不能打洞成功。要是只剩中继那十几兆,这一周怕是要对着幻灯片一样的远程桌面干活了。。。😭
发表回复