Sparkle CodesSparkle
项目 / 运维

用 Tailscale 从 Android SSH 到 macOS:一次被系统 VPN 分流卡住的排查

x
xpx
May 08, 2025
Editorial Insight
#Clash#DNS#SSH#Tailscale

这次要解决的问题很具体:人在外面,用 Android 手机通过 Tailscale SSH 到家里的 Mac,进入 zsh、tmux,继续操作本地项目、Docker、本地服务,以及 Claude Code / Codex 这类终端工具。

最终跑通的链路是:

BASH
ssh -p 22 xpx@100.122.73.61

对应环境如下:

TEXT
Mac Tailscale IP:     100.122.73.61
Android Tailscale IP: 100.72.29.25
Mac 用户名:           xpx
SSH 端口:             22
Clash mixed port:     7897

这个问题最后不是 SSH key 的问题,也不是 macOS sshd 配置的问题。真正卡住我们的有两处:Android 厂商系统把终端 App 排除出了 VPN,以及 Tailscale DNS 和 Clash fake-ip/TUN 抢 DNS 后影响了公网 API 访问。

先把 SSH 当成一条分层链路看

远程 SSH 难排,不是因为 SSH 本身特别复杂,而是因为它依赖很多层同时正确。

这条链路大概是:

TEXT
Android 系统联网权限
  ↓
Android VPN / 智能 VPN / 应用分流
  ↓
Tailscale 虚拟网络
  ↓
Tailscale ACL / DNS / MagicDNS
  ↓
Mac 网络、防火墙、Tailscale 入站权限
  ↓
macOS Remote Login / sshd
  ↓
SSH 用户名、密码、key、known_hosts
  ↓
shell / zsh / tmux / Claude Code / Codex

任何一层错了,表面上都可能只剩一句报错:

TEXT
Connection timed out
Connection refused
Permission denied
Host key verification failed

所以这次排查没有从 SSH key 开始,而是先确认最小链路:Mac 本机能不能连自己,手机到 Mac 的 Tailscale TCP 是否通,SSH 请求有没有真正到达 Mac。

一开始走错了方向:以为是 SSH 配置问题

最开始的现象很容易让人误判:

TEXT
Tailscale 显示 direct
手机能看到 Mac
Taildrop 能传文件
手机浏览器能访问 Mac:8080
但是 Termius / Termux SSH 一直 timeout
Mac tcpdump 抓不到 22 端口包

当时第一反应是检查 SSH:端口、Remote Login、用户名、密码、key、known_hosts。后来发现方向不对。

Connection timed out 通常说明 TCP 连接没有建立。既然 Mac 上抓不到 22 端口的包,SSH 请求根本没有到 Mac。这个阶段继续改 sshd_config 或折腾 key 没有意义,应该回头查手机端的 VPN、分流和系统联网权限。

用 8080 和 tcpdump 把问题切开

我们先在 Mac 上开一个临时 HTTP 服务:

BASH
cd ~
python3 -m http.server 8080

然后 Android 浏览器访问:

TEXT
http://100.122.73.61:8080

浏览器能打开,说明手机上的普通 App 到 Mac 的 Tailscale TCP 链路是通的。

接着在 Mac 上抓 SSH 包:

BASH
sudo tcpdump -i any 'host 100.72.29.25 and tcp port 22'

再从手机发起 SSH:

BASH
ssh -p 22 xpx@100.122.73.61

正常情况下,至少应该看到三次握手:

TEXT
100.72.29.25.xxxxx > 100.122.73.61.22: Flags [S]
100.122.73.61.22 > 100.72.29.25.xxxxx: Flags [S.]
100.72.29.25.xxxxx > 100.122.73.61.22: Flags [.]

如果后面出现大量 Flags [P.],说明 SSH 加密通道已经在传数据。

但这次 SSH 抓不到任何 22 包,而浏览器访问 8080 能抓到。这个对照基本把问题范围缩小到了 Android 端:不是 Tailscale 整体不通,而是 Termius / Termux 这类终端 App 的流量没有走 Tailscale。

根因一:魅族系统把终端 App 排除出了 VPN

最后确认的问题是:魅族系统的“智能联网 / 智能 VPN”把 Termius / Termux 排除出了 VPN。

这解释了前面的矛盾现象:

TEXT
浏览器走 Tailscale,所以访问 8080 成功。
Termius / Termux 没走 Tailscale,所以 SSH 请求根本没到 Mac。
Mac 上 tcpdump 抓不到 22 包。

Tailscale App 自己的分流设置正常,不代表 Android 厂商系统没有再套一层分流。尤其是带“智能联网”“智能 VPN”“应用联网控制”这类功能的系统,排查时要把系统层面的 VPN 权限也纳入检查。

这次在魅族系统上重点检查了这些入口:

TEXT
设置 → 网络
设置 → VPN
设置 → 智能联网
设置 → 智能 VPN
设置 → 应用联网控制

排查阶段建议先不要开启 Tailscale 的 Included apps / Excluded apps,让所有 App 都走 Tailscale。确认链路稳定以后,再按需做精细分流。

Mac 端只保留必要条件

Mac 端需要确认三件事:Tailscale 入站允许、Remote Login 开启、sshd 监听 22 端口。

图形界面开启远程登录:

TEXT
系统设置 → 通用 → 共享 → 远程登录

命令行开启:

BASH
sudo systemsetup -setremotelogin on
sudo systemsetup -getremotelogin

期望结果:

TEXT
Remote Login: On

确认 22 端口监听:

BASH
sudo lsof -nP -iTCP:22 -sTCP:LISTEN

正常结果里应该能看到类似:

TEXT
TCP *:22 (LISTEN)

这表示 sshd 正在监听所有网卡,包括 Tailscale 网卡。

然后在 Mac 本机先测两次:

BASH
ssh xpx@localhost
ssh -p 22 xpx@100.122.73.61

如果 Mac 自己都连不上自己,不要查手机,先修 macOS Remote Login / sshd。

Android 端先用密码跑通,再处理 key

Termius 新建 Host 时,先保持配置简单:

TEXT
Address / Hostname: 100.122.73.61
Port: 22
Username: xpx
Authentication: Password
Password: Mac 登录密码

先不要配置:

TEXT
Proxy
Host Chaining
Agent Forwarding
Mosh
Key / Certificate / FIDO2
Startup snippet

这个阶段也不要填:

TEXT
ssh.github.com
443
192.168.1.x
xpxs-MacBook-Pro.local

本场景只验证一件事:

TEXT
Android → Tailscale → 100.122.73.61:22

Termux 可以作为交叉验证工具:

BASH
pkg update
pkg install openssh
ssh -vvv -p 22 xpx@100.122.73.61

如果 Termux 和 Termius 都不通,但浏览器 8080 通,优先查 Android 系统 VPN / 智能联网 / 应用分流,不要先折腾 SSH key。

SSH key 放到第二阶段

密码登录确认跑通以后,再切到 key 登录。

私钥和公钥不要混:

TEXT
id_ed25519              私钥
id_ed25519.pub          公钥
id_ed25519_personal     私钥
id_ed25519_personal.pub 公钥

Termius 里作为 Identity Key 的应该是没有 .pub 后缀的私钥。

Mac 上的权限保持为:

BASH
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519

后续更推荐给手机单独生成一把 key,例如:

TEXT
android-to-mac

不要复用 GitHub key。这样以后丢手机、换设备或撤销访问时,影响范围更小。

第一次连接时还要确认 host key 指纹。本次 Termius 看到的 ECDSA 指纹是:

TEXT
SHA256:PckOp5on8nccIBQy7Ha1/I4a1BfpxWPxP7JqKFQJ0QU

在 Mac 上验证:

BASH
ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key.pub

输出一致,才接受客户端提示。不同客户端可能使用 ED25519、ECDSA、RSA 等不同 host key 类型,指纹不同不一定异常,要先看 key 类型。

tmux 是手机远程操作的保险绳

手机 SSH 最大的问题不是登录,而是网络会断、App 会被杀、屏幕会锁。

Mac 上安装 tmux:

BASH
brew install tmux

手机 SSH 进 Mac 后,第一条命令建议运行:

BASH
tmux new -A -s ops

含义是:如果 ops 会话存在,就进入;不存在,就创建。

后续即使手机断网、Termius 退出、Termux 被杀,Mac 上的任务仍在 tmux 里。重新连上后继续:

BASH
tmux new -A -s ops

再进入项目目录运行需要的工具:

BASH
cd ~/projects/your-project
claude

或者:

BASH
cd ~/projects/your-project
codex

根因二:Tailscale DNS 和 Clash fake-ip/TUN 抢 DNS

SSH 跑通以后,本地 AI 工具又出现了另一个问题。

直接访问 OpenAI API:

BASH
curl -I https://api.openai.com

报错:

TEXT
LibreSSL SSL_connect: SSL_ERROR_SYSCALL

但显式走 Clash mixed port:

BASH
curl -v -x http://127.0.0.1:7897 https://api.openai.com/v1/models

返回:

TEXT
HTTP/2 401
Missing bearer authentication in header

这个 401 不是网络错误,只是没有带 API key。它反而证明了三件事:

TEXT
OpenAI API 本身能访问。
Clash 节点能访问 OpenAI。
问题在系统默认 DNS / TUN / fake-ip / Tailscale DNS 的组合链路。

最后采用的稳定原则是:

TEXT
Tailscale 只管虚拟内网。
Clash 只管公网代理和公网 DNS。
SSH 直接用 Tailscale IP。
终端 AI 显式走 Clash mixed port。

终端里显式设置代理:

BASH
export HTTP_PROXY=http://127.0.0.1:7897
export HTTPS_PROXY=http://127.0.0.1:7897
export ALL_PROXY=socks5://127.0.0.1:7897

为了方便,写进 ~/.zshrc:

BASH
alias proxy_on='export HTTP_PROXY=http://127.0.0.1:7897 HTTPS_PROXY=http://127.0.0.1:7897 ALL_PROXY=socks5://127.0.0.1:7897'
alias proxy_off='unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy'

生效:

BASH
source ~/.zshrc

使用时:

BASH
proxy_on
codex

或者:

BASH
proxy_on
claude

注意,通过手机 SSH 进 Mac 后运行 Codex / Claude Code,也同样需要在 SSH 会话里执行 proxy_on。SSH 会话里的环境变量不一定和本地终端一致。

Tailscale DNS 不是这条 SSH 链路的必需项

Tailscale DNS 主要解决两个问题。

第一是 MagicDNS:不用记 IP,可以用设备名连接:

BASH
ssh xpx@xpxs-macbook-pro

或者完整域名:

BASH
ssh xpx@xpxs-macbook-pro.tailxxxx.ts.net

第二是 Split DNS:把特定内网域名交给指定 DNS 解析。例如公司内部的:

TEXT
gitlab.company.internal
jira.company.internal
db.prod.internal
k8s-api.company.internal

但 DNS 只负责名字解析,不负责把流量送进目标网络。公司场景里通常还需要 Subnet Router:

TEXT
Tailscale 虚拟网络:让手机和公司节点互通
Tailscale DNS / Split DNS:解析公司内部域名
Subnet Router:把流量转进公司内网网段
ACL / Grants:限制谁能访问哪些资源

个人场景只是:

TEXT
Android 手机 → SSH → Mac

而且已经知道 Mac 的 Tailscale IP:

TEXT
100.122.73.61

所以可以直接:

BASH
ssh xpx@100.122.73.61

不需要 MagicDNS。

当本机同时启用了:

TEXT
Tailscale DNS
Clash Verge TUN
Clash fake-ip
DNS hijack
本地 AI CLI

DNS 和流量路径就容易互相干扰。对这次场景来说,关掉 Tailscale DNS,SSH 直接使用 Tailscale IP,是更少变量的方案。

Clash TUN 需要排除 Tailscale 网段

如果 Clash Verge 开 TUN,保留这些排除项:

TEXT
100.64.0.0/10
fd7a:115c:a1e0::/48
100.100.100.100/32

含义分别是:

TEXT
100.64.0.0/10        Tailscale IPv4 网段
fd7a:115c:a1e0::/48  Tailscale IPv6 网段
100.100.100.100      Tailscale DNS / MagicDNS

目标是把职责拆开:

TEXT
访问 Tailscale 设备 → 走 Tailscale
访问公网 / AI API → 走 Clash

不要让 Clash 抢 Tailscale 的 100.x.x.x 流量。

最后保留下来的稳定配置

Mac 端:

TEXT
Tailscale: 开
Allow incoming connections: 开
Use Tailscale DNS settings: 关
Remote Login / SSH: 开
SSH port: 22
Clash Verge TUN: 可开
Clash mixed port: 7897
Clash 排除 Tailscale 网段

Android 端:

TEXT
Tailscale: 开
Termius: 不被系统 VPN / 智能联网排除
Termux: 不被系统 VPN / 智能联网排除
浏览器: 用于 HTTP 对照测试

SSH:

BASH
ssh -p 22 xpx@100.122.73.61

tmux:

BASH
tmux new -A -s ops

本地 AI:

BASH
proxy_on
codex

几类 SSH 报错的判断顺序

Connection timed out 通常优先看网络层:

TEXT
请求没到
端口被丢弃
VPN/分流没走对
防火墙丢包
Tailscale ACL / 系统 VPN 策略拦了

这次就是 Android 系统把 Termius / Termux 排除出 VPN,导致 timeout。

Connection refused 说明机器到了,但目标端口没人听:

TEXT
sshd 没启动
端口不是 22
服务只监听 localhost
防火墙主动拒绝

Permission denied 说明网络和 sshd 都通了,问题在认证层:

TEXT
用户名错
密码错
key 错
authorized_keys 没放公钥
权限太宽
sshd_config 禁用了某种登录方式

Host key verification failed 通常是远程机器重装过系统,host key 变了,本地 known_hosts 里还是旧记录:

BASH
ssh-keygen -R 100.122.73.61

然后重新连接并确认指纹。

公司场景不能直接套个人配置

如果手机和公司电脑都加入同一个 tailnet:

TEXT
手机:外面 5G
公司电脑:公司内网

它们不一定在同一个物理局域网,但会处在同一个 Tailscale 虚拟内网里。

如果只是 SSH 到公司电脑本身,通常只需要:

TEXT
Tailscale 网络
MagicDNS 或 Tailscale IP
公司电脑允许入站

不一定需要 Subnet Router。

如果要让手机访问公司内网里的 GitLab、Jira、数据库,就需要再引入:

TEXT
Subnet Router
Split DNS
ACL / Grants

公司网络不要私自绕过安全策略。更合适的做法应该包括:

TEXT
IT/安全团队批准
SSO
MFA
设备准入
ACL 最小权限
日志审计
密钥管理
数据合规

Tailscale 在公司场景里不应该被当成“绕过内网限制”的工具,而应该是“受控远程访问”的一部分。

这次留下的判断句

这次最有用的不是某个配置,而是几个判断顺序。

SSH timeout 时,不要先查 key。Connection timed out 通常说明 TCP 连接没建立,优先查网络、VPN、分流、防火墙和端口。

浏览器能访问 Tailscale IP,但 Termius / Termux SSH 不通时,优先查 Android 系统 VPN / 应用分流。浏览器通不代表所有 App 都走了 Tailscale。

Mac tcpdump 抓不到 22 包时,请求根本没到 Mac。这个时候继续改 Mac 的 sshd_config 很可能是在错误方向上用力。

curl -x 127.0.0.1:7897 能返回 401,说明 OpenAI 的网络访问链路是通的。后续重点是终端代理环境变量,而不是继续怀疑 API 服务不可达。

Tailscale DNS 是给 tailnet 设备名和内网域名解析用的,不是个人远程 SSH 的必需项。已知 100.x.x.x IP 时,直接连 IP 可以少掉一层变量。

这次最终原则可以收成一句话:

TEXT
Tailscale 管虚拟内网,Clash 管公网代理,SSH 用 Tailscale IP,tmux 保持远程会话,终端 AI 显式走 mixed port,Android 系统不要排除终端 App 的 VPN。
BACK TO BLOG
The End of Interaction