杂 小老第的杂货铺

用一台海外小 VPS,把手机连回家里的电脑

SSH 隧道nginxHTTPS折腾记录

我平时在电脑上用一个叫 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高度

拿到一组对比数据,问题立刻现形:

场景ihvhot#root 高输入框位置
键盘收起59959985684471→599 ✅
键盘弹起314314370684186→314 ❌

#root 恒为 684px,可视区只剩 314px → 文档能滚 370px → iOS 为了露出输入框,直接滚到最大值 → 输入框被推到屏幕上方。

所以真正的修法是:键盘弹起时把 #root 高度设成视觉视口高度,让文档无处可滚。还有一个关键细节——必须忽略键盘动画中途的异常值(实测会瞬间蹦出 ih=483 / vh=314 这种不一致的值,直接用会把界面压扁)。这就是第一版失败的原因。

教训:连续两次猜错就该停手,去拿数据。 探针花了十分钟,比继续猜快得多。

最后的效果

  • 手机开 https://example.com:9443,输密码进
  • 电脑浏览器开 127.0.0.1:3080,免密、11ms
  • 两边是同一个会话,可以同时用
  • 手机竖屏打字正常

成本:一台 464MB 内存的海外小 VPS(本来就有),没装任何额外软件。

回头看,其实就三句话

  1. 不要试图"从外面连进来",让内网主动拨出去
  2. 每一层都要有门(HTTPS、密码、只转发一个端口、密钥限权)
  3. 说不清原因的时候就上探针,别猜

第三条是我这次最大的收获。前两次改动都是"看起来有道理"的猜测,结果都不对;一张数据截图胜过十次推理。