这次要解决的问题很具体:人在外面,用 Android 手机通过 Tailscale SSH 到家里的 Mac,进入 zsh、tmux,继续操作本地项目、Docker、本地服务,以及 Claude Code / Codex 这类终端工具。
最终跑通的链路是:
ssh -p 22 xpx@100.122.73.61
对应环境如下:
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 本身特别复杂,而是因为它依赖很多层同时正确。
这条链路大概是:
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
任何一层错了,表面上都可能只剩一句报错:
Connection timed out
Connection refused
Permission denied
Host key verification failed
所以这次排查没有从 SSH key 开始,而是先确认最小链路:Mac 本机能不能连自己,手机到 Mac 的 Tailscale TCP 是否通,SSH 请求有没有真正到达 Mac。
一开始走错了方向:以为是 SSH 配置问题
最开始的现象很容易让人误判:
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 服务:
cd ~
python3 -m http.server 8080
然后 Android 浏览器访问:
http://100.122.73.61:8080
浏览器能打开,说明手机上的普通 App 到 Mac 的 Tailscale TCP 链路是通的。
接着在 Mac 上抓 SSH 包:
sudo tcpdump -i any 'host 100.72.29.25 and tcp port 22'
再从手机发起 SSH:
ssh -p 22 xpx@100.122.73.61
正常情况下,至少应该看到三次握手:
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。
这解释了前面的矛盾现象:
浏览器走 Tailscale,所以访问 8080 成功。
Termius / Termux 没走 Tailscale,所以 SSH 请求根本没到 Mac。
Mac 上 tcpdump 抓不到 22 包。
Tailscale App 自己的分流设置正常,不代表 Android 厂商系统没有再套一层分流。尤其是带“智能联网”“智能 VPN”“应用联网控制”这类功能的系统,排查时要把系统层面的 VPN 权限也纳入检查。
这次在魅族系统上重点检查了这些入口:
设置 → 网络
设置 → VPN
设置 → 智能联网
设置 → 智能 VPN
设置 → 应用联网控制
排查阶段建议先不要开启 Tailscale 的 Included apps / Excluded apps,让所有 App 都走 Tailscale。确认链路稳定以后,再按需做精细分流。
Mac 端只保留必要条件
Mac 端需要确认三件事:Tailscale 入站允许、Remote Login 开启、sshd 监听 22 端口。
图形界面开启远程登录:
系统设置 → 通用 → 共享 → 远程登录
命令行开启:
sudo systemsetup -setremotelogin on
sudo systemsetup -getremotelogin
期望结果:
Remote Login: On
确认 22 端口监听:
sudo lsof -nP -iTCP:22 -sTCP:LISTEN
正常结果里应该能看到类似:
TCP *:22 (LISTEN)
这表示 sshd 正在监听所有网卡,包括 Tailscale 网卡。
然后在 Mac 本机先测两次:
ssh xpx@localhost
ssh -p 22 xpx@100.122.73.61
如果 Mac 自己都连不上自己,不要查手机,先修 macOS Remote Login / sshd。
Android 端先用密码跑通,再处理 key
Termius 新建 Host 时,先保持配置简单:
Address / Hostname: 100.122.73.61
Port: 22
Username: xpx
Authentication: Password
Password: Mac 登录密码
先不要配置:
Proxy
Host Chaining
Agent Forwarding
Mosh
Key / Certificate / FIDO2
Startup snippet
这个阶段也不要填:
ssh.github.com
443
192.168.1.x
xpxs-MacBook-Pro.local
本场景只验证一件事:
Android → Tailscale → 100.122.73.61:22
Termux 可以作为交叉验证工具:
pkg update
pkg install openssh
ssh -vvv -p 22 xpx@100.122.73.61
如果 Termux 和 Termius 都不通,但浏览器 8080 通,优先查 Android 系统 VPN / 智能联网 / 应用分流,不要先折腾 SSH key。
SSH key 放到第二阶段
密码登录确认跑通以后,再切到 key 登录。
私钥和公钥不要混:
id_ed25519 私钥
id_ed25519.pub 公钥
id_ed25519_personal 私钥
id_ed25519_personal.pub 公钥
Termius 里作为 Identity Key 的应该是没有 .pub 后缀的私钥。
Mac 上的权限保持为:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519
后续更推荐给手机单独生成一把 key,例如:
android-to-mac
不要复用 GitHub key。这样以后丢手机、换设备或撤销访问时,影响范围更小。
第一次连接时还要确认 host key 指纹。本次 Termius 看到的 ECDSA 指纹是:
SHA256:PckOp5on8nccIBQy7Ha1/I4a1BfpxWPxP7JqKFQJ0QU
在 Mac 上验证:
ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key.pub
输出一致,才接受客户端提示。不同客户端可能使用 ED25519、ECDSA、RSA 等不同 host key 类型,指纹不同不一定异常,要先看 key 类型。
tmux 是手机远程操作的保险绳
手机 SSH 最大的问题不是登录,而是网络会断、App 会被杀、屏幕会锁。
Mac 上安装 tmux:
brew install tmux
手机 SSH 进 Mac 后,第一条命令建议运行:
tmux new -A -s ops
含义是:如果 ops 会话存在,就进入;不存在,就创建。
后续即使手机断网、Termius 退出、Termux 被杀,Mac 上的任务仍在 tmux 里。重新连上后继续:
tmux new -A -s ops
再进入项目目录运行需要的工具:
cd ~/projects/your-project
claude
或者:
cd ~/projects/your-project
codex
根因二:Tailscale DNS 和 Clash fake-ip/TUN 抢 DNS
SSH 跑通以后,本地 AI 工具又出现了另一个问题。
直接访问 OpenAI API:
curl -I https://api.openai.com
报错:
LibreSSL SSL_connect: SSL_ERROR_SYSCALL
但显式走 Clash mixed port:
curl -v -x http://127.0.0.1:7897 https://api.openai.com/v1/models
返回:
HTTP/2 401
Missing bearer authentication in header
这个 401 不是网络错误,只是没有带 API key。它反而证明了三件事:
OpenAI API 本身能访问。
Clash 节点能访问 OpenAI。
问题在系统默认 DNS / TUN / fake-ip / Tailscale DNS 的组合链路。
最后采用的稳定原则是:
Tailscale 只管虚拟内网。
Clash 只管公网代理和公网 DNS。
SSH 直接用 Tailscale IP。
终端 AI 显式走 Clash mixed port。
终端里显式设置代理:
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:
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'
生效:
source ~/.zshrc
使用时:
proxy_on
codex
或者:
proxy_on
claude
注意,通过手机 SSH 进 Mac 后运行 Codex / Claude Code,也同样需要在 SSH 会话里执行 proxy_on。SSH 会话里的环境变量不一定和本地终端一致。
Tailscale DNS 不是这条 SSH 链路的必需项
Tailscale DNS 主要解决两个问题。
第一是 MagicDNS:不用记 IP,可以用设备名连接:
ssh xpx@xpxs-macbook-pro
或者完整域名:
ssh xpx@xpxs-macbook-pro.tailxxxx.ts.net
第二是 Split DNS:把特定内网域名交给指定 DNS 解析。例如公司内部的:
gitlab.company.internal
jira.company.internal
db.prod.internal
k8s-api.company.internal
但 DNS 只负责名字解析,不负责把流量送进目标网络。公司场景里通常还需要 Subnet Router:
Tailscale 虚拟网络:让手机和公司节点互通
Tailscale DNS / Split DNS:解析公司内部域名
Subnet Router:把流量转进公司内网网段
ACL / Grants:限制谁能访问哪些资源
个人场景只是:
Android 手机 → SSH → Mac
而且已经知道 Mac 的 Tailscale IP:
100.122.73.61
所以可以直接:
ssh xpx@100.122.73.61
不需要 MagicDNS。
当本机同时启用了:
Tailscale DNS
Clash Verge TUN
Clash fake-ip
DNS hijack
本地 AI CLI
DNS 和流量路径就容易互相干扰。对这次场景来说,关掉 Tailscale DNS,SSH 直接使用 Tailscale IP,是更少变量的方案。
Clash TUN 需要排除 Tailscale 网段
如果 Clash Verge 开 TUN,保留这些排除项:
100.64.0.0/10
fd7a:115c:a1e0::/48
100.100.100.100/32
含义分别是:
100.64.0.0/10 Tailscale IPv4 网段
fd7a:115c:a1e0::/48 Tailscale IPv6 网段
100.100.100.100 Tailscale DNS / MagicDNS
目标是把职责拆开:
访问 Tailscale 设备 → 走 Tailscale
访问公网 / AI API → 走 Clash
不要让 Clash 抢 Tailscale 的 100.x.x.x 流量。
最后保留下来的稳定配置
Mac 端:
Tailscale: 开
Allow incoming connections: 开
Use Tailscale DNS settings: 关
Remote Login / SSH: 开
SSH port: 22
Clash Verge TUN: 可开
Clash mixed port: 7897
Clash 排除 Tailscale 网段
Android 端:
Tailscale: 开
Termius: 不被系统 VPN / 智能联网排除
Termux: 不被系统 VPN / 智能联网排除
浏览器: 用于 HTTP 对照测试
SSH:
ssh -p 22 xpx@100.122.73.61
tmux:
tmux new -A -s ops
本地 AI:
proxy_on
codex
几类 SSH 报错的判断顺序
Connection timed out 通常优先看网络层:
请求没到
端口被丢弃
VPN/分流没走对
防火墙丢包
Tailscale ACL / 系统 VPN 策略拦了
这次就是 Android 系统把 Termius / Termux 排除出 VPN,导致 timeout。
Connection refused 说明机器到了,但目标端口没人听:
sshd 没启动
端口不是 22
服务只监听 localhost
防火墙主动拒绝
Permission denied 说明网络和 sshd 都通了,问题在认证层:
用户名错
密码错
key 错
authorized_keys 没放公钥
权限太宽
sshd_config 禁用了某种登录方式
Host key verification failed 通常是远程机器重装过系统,host key 变了,本地 known_hosts 里还是旧记录:
ssh-keygen -R 100.122.73.61
然后重新连接并确认指纹。
公司场景不能直接套个人配置
如果手机和公司电脑都加入同一个 tailnet:
手机:外面 5G
公司电脑:公司内网
它们不一定在同一个物理局域网,但会处在同一个 Tailscale 虚拟内网里。
如果只是 SSH 到公司电脑本身,通常只需要:
Tailscale 网络
MagicDNS 或 Tailscale IP
公司电脑允许入站
不一定需要 Subnet Router。
如果要让手机访问公司内网里的 GitLab、Jira、数据库,就需要再引入:
Subnet Router
Split DNS
ACL / Grants
公司网络不要私自绕过安全策略。更合适的做法应该包括:
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 可以少掉一层变量。
这次最终原则可以收成一句话:
Tailscale 管虚拟内网,Clash 管公网代理,SSH 用 Tailscale IP,tmux 保持远程会话,终端 AI 显式走 mixed port,Android 系统不要排除终端 App 的 VPN。