本文涉及的 IP、域名均为示例,请按自己实际情况替换:
- JP 服务器 IP:
203.0.113.42(RFC 5737 文档保留段,不是真实地址)- 自己的域名:
ssh.example.com- launchd 标签:
com.example.reverse-tunnel-jp
这篇文章会反复出现一些黑话,先用一段把它们讲明白,不然后面看得云里雾里。
VPS = Virtual Private Server,虚拟专用服务器。人话就是"在云服务商(腾讯云、AWS、甲骨文、搬瓦工这种)家租一台 Linux 机器,24 小时开着,给你一个公网 IP"。
"小鸡"是 VPS 圈的黑话,一台便宜 VPS 就叫"一只小鸡"。我这台租在日本机房的就被我叫做 JP 小鸡,一个月也就几十块。
Cloudflare(简称 CF):全球最大的 CDN 网络服务商之一,免费服务多得离谱,个人玩家人手必备。
CF Edge(边缘节点):Cloudflare 在全球几百个城市都有机房(专业术语叫 POP),你访问 CF 服务的时候会被自动分到"离你最近"的那个机房,这就叫 Edge。你本地 ping cloudflare.com 只有十几毫秒就是这个原因。
CF Tunnel(内网穿透):Cloudflare 送的免费内网穿透服务。以前你想从外网连家里的服务,得搞公网 IP、端口映射、防火墙、动态 DNS,一堆折腾。CF Tunnel 直接让家里跑一个 cloudflared 进程主动连出去到 Cloudflare,然后你从外面访问 ssh.example.com 的时候,Cloudflare 就通过那条隧道把请求扔回家,中间你家路由器不用开一个端口,安全又省事。
~/.ssh/config)文章里我经常写 ssh jp 就能登录日本服务器,不是魔法,是在 ~/.ssh/config 里配了别名:
Host jp HostName 203.0.113.42 User root IdentityFile ~/.ssh/id_ed25519
以后不用敲 ssh -i ~/.ssh/id_ed25519 [email protected] 这种又长又容易错的命令,直接 ssh jp。后面的 mini、mini-cf、mini-jp 都是别名。
每次连服务器都输密码太烦,正规玩法是"公钥/私钥对":
ssh-keygen -t ed25519~/.ssh/id_ed25519.pub 内容复制到远程机器的 ~/.ssh/authorized_keys 里ssh 服务器 自动登录,不用密码这篇文章里所有 SSH 操作默认都是免密的,不然 autossh 守护进程根本跑不起来。
ProxyJump jp 的意思是:连某台机器之前,先经过 JP 中转一下。一行配置搞定多级跳板。常用场景:目标机器没公网 IP,但能从 JP 那头连到它。
ssh -R)划重点,这是整篇文章的主角。
普通 SSH 是"你主动连别人"(正向)。反向隧道反过来:内网机器主动连出去到公网机器,然后把自己的端口映射到公网机器上。公网机器上就多了一个"反过来的端口",谁连这个端口等于连到内网机器。
打个比方:mini 在家里连不到,但它能主动"打电话"给日本小鸡,打通之后这通电话就不挂了。你想联系 mini,就先拨日本小鸡,接通以后请对方把你的话"递"给那头还没挂断的 mini。整条链路就是这么个原理。
名词扫盲结束,下面开始正事。
家里那台 Mac mini 搬到家之后一直用 Cloudflare Tunnel 远程连,免费、稳定、还不用折腾端口映射,看着挺香。直到有一天我出差了,想远程 scp 一个几十 MB 的文件回来,眼睁睁看着进度条像蜗牛爬。
顺手 time 了一下:
scp mini-cf:/tmp/testfile_50m . → 50 MB 花了 1 分 46 秒 → 换算一下,0.48 MB/s
这速度,传个项目代码都能泡杯咖啡回来还没传完。
我不死心,换了几个网络环境试,都是这个速度。那就不是我家网络的锅,CF Tunnel 本身有问题。
正好我在日本租了一台小鸡(常年跑加速用),本地连 JP 快得飞起,那能不能让 JP 当跳板救一下?说干就干。
凭感觉拍脑袋不行,得测。我把整条链路拆成几段,一段一段 curl + scp 跑数据。
测试方法:
curl 测到 Cloudflare edge 的下载速度(https://speed.cloudflare.com/__down?bytes=52428800)scp 传一个 50MB 的随机文件测点对点吞吐跑下来结果是这样的:
| 链路 | 速度 | 备注 |
|---|---|---|
| 本地 → CF edge | 10.9 MB/s | 客户端到 CDN 一点不慢 |
| mini → CF edge | 5.0 MB/s | 家宽出口也还行 |
| 本地 → JP(直连) | 3.2 MB/s | JP 小鸡带宽 |
| JP → CF edge | 136 MB/s | 数据中心内网级别 |
| 本地 → CF Tunnel → mini | 0.48 MB/s | ❌ 瓶颈元凶 |
看到这里我人都傻了。两端到 CF edge 明明都有几 MB/s 起步,tunnel 一接起来怎么掉到 0.48 MB/s?
翻了一下资料才反应过来:CF Tunnel 是 mini 的 cloudflared 反向出站连一个 POP,客户端连另一个 POP,两个 POP 可能绕了大半个地球。你以为走的是"最快路径",实际上 Cloudflare 给你安排的是"他家机房空闲路径"。
顺着这个思路,我想既然本地连 CF edge 可能被扔到了奇怪的 POP,那让 JP 当跳板、JP 上装 cloudflared 是不是就能强制走东京 POP?
干就完事了:
bashssh jp 'curl -sL -o /usr/local/bin/cloudflared \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
&& chmod +x /usr/local/bin/cloudflared'
然后从 JP 走 cloudflared + SSH 到 mini,scp 同一个 50MB 文件:
50 MB,95 秒,0.54 MB/s
对不起,贵不是苹果的错,是我的错,但这次是 Cloudflare 的错。
换 POP 屁用没有。瓶颈在 CF Tunnel 协议本身(QUIC over 反向连接的奇怪路由),换入口完全无效。
方案 A 宣告死亡。
方案 A 测试的时候我顺手在 mini 上 ping 了一下 JP:
bashssh mini-cf 'ping -c 5 203.0.113.42'
→ round-trip min/avg/max = 0.337/0.416/0.477 ms
0.4ms!!! 国内到日本的 IP 不可能是这个延迟,除非是本地环回。
想了两秒反应过来:mini 上我本来就挂了一条到 JP 的科学上网链路,所以 mini 访问 JP IP 是走这条加速链路过去的,几乎等于和 JP 同机房。
这就好办了。如果 mini 直连 JP 这么快,我就让 mini 主动连出去到 JP,建一个反向 SSH 隧道,客户端跳板 JP 就能连回 mini。完全绕开 CF Tunnel。
实测 mini → JP 的吞吐:
scp /tmp/testfile_50m mini → jp → 50 MB,10.8 秒,4.6 MB/s
妥了,这就是可用速度。
架构长这样:
sequenceDiagram
autonumber
participant C as 客户端
participant J as JP 跳板
participant M as Mac mini
Note over M,J: 阶段 1:autossh 常驻
M->>J: ssh -R 2222:localhost:22 tunnel@jp
J-->>M: 认证通过
Note over J: 127.0.0.1:2222 监听
Note over C,M: 阶段 2:客户端按需连
C->>J: ssh jp (ProxyJump)
C->>J: 请求转发到 localhost:2222
J->>M: 经反向隧道推回 mini
M-->>C: 端到端 SSH 握手
几个关键点:
GatewayPorts no 保证反向端口外网访问不到不能给 mini 的反向隧道用 root,权限太大。单独开一个 tunnel 用户,用 authorized_keys 限制只能做端口转发:
bashssh jp '
useradd -m -s /bin/bash tunnel
mkdir -p /home/tunnel/.ssh
chown tunnel:tunnel /home/tunnel/.ssh
chmod 700 /home/tunnel/.ssh
'
bashssh mini 'ssh-keygen -t ed25519 -N "" \
-C "mini-reverse-tunnel-to-jp" \
-f ~/.ssh/id_tunnel_jp'
这里是整个方案最关键的一行,少一个参数就裸奔:
bashMINI_PUBKEY="$(ssh mini 'cat ~/.ssh/id_tunnel_jp.pub')"
ssh jp "echo 'restrict,port-forwarding,command=\"echo tunnel-only; sleep infinity\" ${MINI_PUBKEY}' \
> /home/tunnel/.ssh/authorized_keys \
&& chmod 600 /home/tunnel/.ssh/authorized_keys \
&& chown tunnel:tunnel /home/tunnel/.ssh/authorized_keys"
参数解读:
restrict:一键关掉所有能力(pty、X11、agent forwarding、命令执行)port-forwarding:把端口转发单独开回来(因为 restrict 默认连这个也禁了)command="... sleep infinity":即使 key 泄露也拿不到 shell用 ssh -R 直接跑不行,断线就没了。要上 autossh(一个会自动重连 SSH 的工具):
bashssh mini '/opt/homebrew/bin/brew install autossh'
ssh mini 'ssh-keyscan -t ed25519 203.0.113.42 > ~/.ssh/known_hosts_tunnel'
然后写 launchd plist(macOS 的开机自启服务配置),开机自启、挂了自动拉起来:
~/Library/LaunchAgents/com.example.reverse-tunnel-jp.plist
xml<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.reverse-tunnel-jp</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/autossh</string>
<string>-M</string><string>0</string>
<string>-N</string><string>-T</string>
<string>-o</string><string>ServerAliveInterval=30</string>
<string>-o</string><string>ServerAliveCountMax=3</string>
<string>-o</string><string>ExitOnForwardFailure=yes</string>
<string>-o</string><string>StrictHostKeyChecking=yes</string>
<string>-o</string><string>UserKnownHostsFile=/Users/你的用户名/.ssh/known_hosts_tunnel</string>
<string>-i</string><string>/Users/你的用户名/.ssh/id_tunnel_jp</string>
<string>-R</string><string>2222:localhost:22</string>
<string>[email protected]</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
</dict>
</plist>
加载:
bashlaunchctl load ~/Library/LaunchAgents/com.example.reverse-tunnel-jp.plist
验证自愈:pkill -9 autossh,等 10 秒再看,launchd 会自动把它拉起来。
Host mini-jp HostName localhost Port 2222 User 你的mini用户名 ProxyJump jp IdentityFile ~/.ssh/id_ed25519 StrictHostKeyChecking no UserKnownHostsFile /dev/null LocalForward 55900 localhost:5900
之后就能:
bashssh mini-jp
搞定。
一开始我只写了 restrict,结果反向隧道死活建不起来,报 administratively prohibited。翻 man 手册才知道 restrict 的"所有权限"连端口转发也一起关了,要显式加 port-forwarding 开回来。
我复制粘贴的时候不小心 Host mini-jp 前面带了 2 个空格,SSH 倒是容忍解析了,但有些选项(像 LocalForward)会不生效,debug 半天才发现。
不良示范:
# 反向 SSH 隧道 Host mini-jp HostName localhost
好的示范:
# 反向 SSH 隧道 Host mini-jp HostName localhost
Host 顶格写,选项 4 空格缩进,别搞花的。
配好 LocalForward 55900 localhost:5900 之后,满怀期待打开"屏幕共享"连 vnc://localhost:55900,告诉我连接失败。
我当时脑子一热跑了一句:
bashssh mini-jp 'lsof -iTCP:5900 -sTCP:LISTEN'
确认 mini 上 5900 是开的。但 VNC 就是连不上。
问题出在:ssh mini-jp '命令' 是一次性会话,命令跑完 session 就断,LocalForward 的端口也跟着关了。
VNC 需要 SSH 隧道保持。正确姿势:
bashssh -fN mini-jp
-f:后台-N:不执行远程命令然后 VNC 连 vnc://localhost:55900,丝滑。
想杀掉后台隧道:
bashpkill -f 'ssh.*mini-jp'
或者精准一点:
bashlsof -iTCP:55900 -sTCP:LISTEN
# 找 PID
kill <PID>
注意:-f 后台模式关闭终端窗口不会自动停隧道,要手动杀。想"关窗口即断"用 ssh -N mini-jp(前台阻塞,Ctrl+C 结束)。
端到端测试:
ssh mini-jp 'echo ok' → 3.4s 建连 scp mini-jp:/tmp/testfile_50m . → 50 MB,20.7 秒,2.4 MB/s
对比 CF Tunnel 的 0.48 MB/s,快了整整 5 倍。 日常 VNC 屏幕共享也完全能用,画质自适应之后跟本地没啥差别。
自愈也验证过了,断网、重启、kill 进程,autossh + launchd 都能在 10 秒内恢复。
家里三种连接方式按场景切换:
bashssh mini # 局域网直连,最快
ssh mini-jp # 主力,远程走反向 SSH,~2.4 MB/s
ssh mini-cf # 兜底,CF Tunnel(JP 挂了用它)
VNC 屏幕共享:
bashssh -fN mini-jp # 后台建隧道
open vnc://localhost:55900 # 连接
# 用完:pkill -f 'ssh.*mini-jp'
| 类型 | 名字 | 用途 | 花费 |
|---|---|---|---|
| VPS | 任意海外小鸡(日本优先) | 跳板 + 反向隧道落地 | 约 ¥20~50/月 |
| 加速 | mini 上到 JP 的加速链路 | 让 mini 到 JP 速度快 | 已有 |
| 账号 | Cloudflare | 兜底 CF Tunnel 方案 | 免费 |
| 软件 | cloudflared | CF Tunnel 客户端 | 免费 |
| 软件 | autossh | 守护反向 SSH | brew 免费 |
| 软件 | OpenSSH | SSH 本身,现代 Mac/Linux 自带 | 免费 |
| 内置 | macOS launchd | 开机自启 + 自愈 | 免费 |
整条方案里最关键的不是技术,是先把瓶颈量化出来。我一开始也以为"加个 JP 跳板 cloudflared 就能解决",结果方案 A 白做一轮。是分段 scp 测速逼出了"CF Tunnel 协议本身就慢"这个结论,才有后面方案 C 的顺利落地。
优点:
缺点:
能跑通了再说,先干出来比啥都强。如果你也在家 selfhost 一台 Mac mini / NAS,下次 CF Tunnel 卡成 PPT 的时候,记得还有这条野路子。
加入AI技术交流群,可以先关注本公众号,然后在后台回复「交流」,获取入群方式。
感谢你看到这里,如果觉得有帮助,转发,点个赞或在看,就是对我最大的鼓励。也欢迎留言交流你的想法~
本文作者:花菜
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!