用一台海外小 VPS,把手机连回家里的电脑
我平时在电脑上用一个叫 DSH 的 AI 工具,它本质是个本地服务,只监听 127.0.0.1:3080。
问题来了:我在外面的时候,想用手机接着聊。
先说我一开始想错的地方
第一反应是"把端口开到公网"。但这台电脑在家里路由器后面,没有公网 IP,做不了端口映射;就算做了,把一个能执行命令的界面直接暴露在公网,也是等着被人扫到。
第二个念头是用现成的内网穿透服务。但它们要么限速、要么要实名、要么免费版会随机断线——而这个会话一旦断了,正在跑的活儿就丢了。
最后走的路:SSH 反向隧道
关键的一念之差:不要"从外面连进来",改成"从家里拨出去"。
手机 ──HTTPS──> 海外 VPS ──隧道──> 家里的电脑
↑
这条连接是【电脑主动拨出去】的
因为连接方向是"由内向外",所以:
- 电脑不需要公网 IP
- 路由器不用改任何设置
- 不需要额外装任何软件(Windows 自带 OpenSSH)
具体一条命令:
ssh -N -R 127.0.0.1:2222:127.0.0.1:3080 user@vps
意思是:把 VPS 上的 2222 端口,转发到本机的 3080。注意绑的是 127.0.0.1,所以 VPS 上这个端口只有本机能连,公网连不到——这是刻意的。
加一层门:nginx + HTTPS + 密码
光有隧道还不够,手机要的是 https://。所以在 VPS 上放了个 nginx:
server {
listen 9443 ssl;
server_name 你的域名.com;
auth_basic "restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://127.0.0.1:2222;
}
}
于是完整链路是:
手机 → https://域名:9443 (HTTPS + 密码)
→ VPS nginx
→ 127.0.0.1:2222 (隧道入口)
→ SSH 隧道
→ 家里电脑 127.0.0.1:3080
中间每一跳都有人看着门。
踩的五个坑
1. 页面能加载,但一直在跳登录页
nginx 认证过了,页面也出来了,但所有接口都 403,前端以为没登录,又弹认证框。
原因是这个工具有个防跨站的校验:浏览器发来的 Origin 必须和 Host 完全一致。而 nginx 默认会把端口从 Host 里去掉:
浏览器 Origin : https://example.com:9443 ← 带端口
nginx 传的 Host: example.com ← 端口没了
→ 不相等 → 403
修法是一行:
proxy_set_header Host example.com:9443;
2. 从外面连 9443 全部超时
机器上防火墙放行了、ss -tln 也显示在监听,但外面连不上。
真正的原因在云平台控制台上——它有一层独立于机器的防火墙,白名单里没有 9443。包在到达机器之前就被丢了。
这里有个特别坑的"假阳性":在 VPS 上执行 curl 公网IP:9443 是通的(走回环,不经云平台过滤),差点让我以为是客户端的问题。
教训:新增对外端口要查三层——云平台防火墙、主机防火墙、服务监听。
3. 手机发带图的消息报 413
手机上发一条带截图的消息,界面报 HTTP 413。
查日志:
client intended to send too large body: 11452159 bytes
11.45MB 的截图,而 nginx 默认请求体上限是 1MB。加一行就好:
client_max_body_size 50m;
4. 手机能登录,但发不出消息
这个最有意思。能进去、能看历史,一发消息就提示"会话已被占用"。
原因是这个工具用内核文件锁保证一个会话同时只有一个进程能写。而我在电脑上还开着桌面版,它占着锁。
所以正确的用法是:
桌面版(独立进程)→ 自己持有一把锁 ← 会和别人抢
网页服务(一个进程)→ 持有一把锁
├─ 手机浏览器 ┐
└─ 电脑浏览器 ┘ 都只是客户端,共用同一把锁 → 不冲突
锁是"进程级"的,不是"客户端级"的。 想清楚这一点,一切就通了: 关掉桌面版,手机和电脑浏览器就能同时用。
5. 手机竖屏打字,输入框被键盘挡住
这个折腾最久,因为前两次我都在猜。
症状:点输入框,键盘弹起,输入框跑到屏幕上方,下面一片黑。
第一次改:把根元素高度设成视觉视口高度。→ 键盘能用了,但打第一个字会跳。
第二次改:连 html 和 body 一起锁。→ 更糟,每个字都跳。
第三次我停止猜测,往页面里塞了个只显示数字的探针,把几个值打印在屏幕顶部:
ih=innerHeight vh=visualViewport.height ot=vv.offsetTop rh=#root高度
拿到一组对比数据,问题立刻现形:
| 场景 | ih | vh | ot | #root 高 | 输入框位置 |
|---|---|---|---|---|---|
| 键盘收起 | 599 | 599 | 85 | 684 | 471→599 ✅ |
| 键盘弹起 | 314 | 314 | 370 | 684 | 186→314 ❌ |
#root 恒为 684px,可视区只剩 314px → 文档能滚 370px → iOS 为了露出输入框,直接滚到最大值 → 输入框被推到屏幕上方。
所以真正的修法是:键盘弹起时把 #root 高度设成视觉视口高度,让文档无处可滚。还有一个关键细节——必须忽略键盘动画中途的异常值(实测会瞬间蹦出 ih=483 / vh=314 这种不一致的值,直接用会把界面压扁)。这就是第一版失败的原因。
教训:连续两次猜错就该停手,去拿数据。 探针花了十分钟,比继续猜快得多。
最后的效果
- 手机开
https://example.com:9443,输密码进 - 电脑浏览器开
127.0.0.1:3080,免密、11ms - 两边是同一个会话,可以同时用
- 手机竖屏打字正常
成本:一台 464MB 内存的海外小 VPS(本来就有),没装任何额外软件。
回头看,其实就三句话
- 不要试图"从外面连进来",让内网主动拨出去
- 每一层都要有门(HTTPS、密码、只转发一个端口、密钥限权)
- 说不清原因的时候就上探针,别猜
第三条是我这次最大的收获。前两次改动都是"看起来有道理"的猜测,结果都不对;一张数据截图胜过十次推理。