我一直想在通勤路上给家里那台电脑上的 opencode 派活儿 —— 不是远程桌面那种”看着屏幕点点”,是真的把指令发过去,让它自己跑。
试过几条路:
| 方案 | 结果 |
|---|---|
| frp 自建服务器 | 要有台有公网 IP 的 VPS,还得自己运维 |
| Cloudflare Tunnel | 要绑自己的域名,配置一堆 |
| Tailscale + 客户端 App | 得装 App,网页端不一定好用 |
| SSH 端口转发 + 客户端 | 手机上没法开 SSH 隧道 |
| OpenTunnel | 一条命令,一个 https 网址,手机浏览器直接用 |
最后这个是 OpenCode 联合创始人 Dax Raad 做的开源项目:anomalyco/opentunnel。
一、它是什么:一个”盲”TLS 隧道
一句话概括它的设计哲学:
服务端只读 TLS 握手的第一个包(ClientHello),拿到 SNI 就知道该转给谁,剩下的一概不看。
这是个很反常规的设计。传统内网穿透(ngrok、frp)的模型是:客户端和服务端之间建立一条隧道,所有流量都经过服务端。所以服务端能看到你的明文 —— 包括密码、cookie、API Key。
OpenTunnel 换了个思路:
┌─────────────────────────────┐ 浏览器 ──HTTPS──▶ │ OpenTunnel Server (443) │ (以为在直连) │ 只读 ClientHello 里的 SNI │ └──────────────┬──────────────┘ │ 按 SNI 找到对应隧道 │ (剩余内容原样转发,加密状态不变) ▼ ┌─────────────────────────────┐ │ 本地 opentunnel client │ │ ★ 持有证书私钥,在这里终结 TLS │ │ ★ 服务端从来看不到明文 │ └──────────────┬──────────────┘ │ 明文 HTTP ▼ ┌─────────────────────────────┐ │ opencode server 127.0.0.1:4096 │ └─────────────────────────────┘关键点:证书私钥在你自己电脑上,服务端从头到尾没有碰过。 它就像一个只认门牌号、然后原样把包裹送过去的快递员 —— 它知道包裹要送给谁,但包裹里是什么它拆不开。
它管这叫 blind TLS tunnel(盲 TLS 隧道)。
二、完整时序图
NOTE下图里的
ROUTE是默认随机生成的 16 位路由名,ID是服务端分配给本机的隧道子域名,两者拼起来就是一个完整地址。Mermaid 的标签里不能用尖括号,所以这里用大写占位。
阶段 A:建隧道(一次性)
(Fly.io :443) participant ACME as ZeroSSL (ACME) participant CF as Cloudflare DNS CLI->>CLI2: opentunnel route add 4096 CLI2->>SRV: POST /api/routes (申请隧道) SRV-->>CLI2: 返回 id,例如 3f2a... Note over CLI2,SRV: 分配唯一子域名 ID.opentunnel.xyz CLI2->>ACME: 申请证书 (ACME DNS-01) ACME->>CF: 验证 _acme-challenge TXT 记录 CF-->>ACME: 验证通过 ACME-->>CLI2: 下发证书 + 私钥(只给客户端) Note over CLI2: ★ 私钥永不离开本机 CLI2->>SRV: 建立 bridge WebSocket(携带密文通道) SRV-->>CLI2: 就绪,开始接收转发 CLI2-->>CLI: 打印 https://ROUTE(随机16位).ID.opentunnel.xyz
注意 DNS-01 这一步:它不验证端口 80,不要求你有公网 IP,只验证 DNS 权限。 这是没有固定 IP 的家庭宽带能拿到正规证书的唯一途径。
路由名默认是 16 位随机字符,而且不会出现在公开的证书透明度(CT)日志里 —— 意味着没人能通过查 CT 日志反查出你的隧道地址。
opentunnel route add 4096# Added route 21992cc9713e5fc5 → 127.0.0.1:4096# https://21992cc9713e5fc5.3f2a91c4.opentunnel.xyz安装
curl -fsSL https://opentunnel.xyz/install | shbrew install anomalyco/tap/opentunnelyay -S opentunnel-bin # Archnpm install -g opentunnelcargo install opentunnel-cli阶段 B:手机发一条消息(每次)
127.0.0.1:4096 M->>S: TCP 连接 443,TLS ClientHello
SNI = ROUTE.ID.opentunnel.xyz Note over S: 只解析 SNI,其余字节看不懂也不看 S->>C: 经 bridge WebSocket 转发原始 ClientHello 密文 C->>C: ★ 用本地私钥终结 TLS,拿到明文 HTTP Note over C: 校验证书域名,握手完成 C->>O: HTTP 请求
GET / (Authorization: Basic ...) Note over C,O: 应用看到的是 loopback 对端 O-->>C: 200 + OpenCode Web 界面 C->>S: 用会话密钥加密后回传 S-->>M: 原样转发(浏览器全程以为在直连服务端)
NOTE最后一步是最舒服的地方:对浏览器来说,它就是访问一个普通的 https 站点。没有证书警告,没有客户端 App,没有 VPN 状态栏。它甚至不知道中间有个隧道。
三、最要命的一步:那个随机 24 位密码
隧道跑通之后我差点裸奔到公网上。然后我意识到一个很要命的事实:
README 里写得很清楚: “Route names never appear in public certificate logs, so the URL can’t be guessed, but anyone you share it with can reach the service: it is not authentication.”
地址猜不出来 ≠ 有鉴权。地址泄露了 = 完全门户大开。
而 opencode 官方文档也直说了:
If
OPENCODE_SERVER_PASSWORDis not set, the server will be unsecured. This is fine for local use but should be set for network access.
所以第一步就是打开 HTTP Basic 认证。用户名默认是 opencode,可以用 OPENCODE_SERVER_USERNAME 改。
密码怎么生成
密码强度必须对得起”这个地址现在挂在公网上”这件事。 别用 123456、别用手机号生日、别用你其他服务复用过的密码。
# Linux / macOS / WSL —— openssl 生成,18 字节 = base64 后正好 24 个字符export OPENCODE_SERVER_PASSWORD=$(openssl rand -base64 18)
# 顺手看一眼长度,确认是 24echo -n "$OPENCODE_SERVER_PASSWORD" | wc -c18 字节 = 144 bit 熵,base64 编码后是 24 个字符,且没有 = 补位。这个长度刚好能在密码管理器里一眼输完,又足够长到爆破无望。
WARNINGWindows 的 PowerShell 默认没有
openssl.exe(Windows 自带的是 OpenSSH,不是 OpenSSL)。用这个等效实现,长度同样是 24:Terminal window $b = New-Object byte[] 18[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b)$env:OPENCODE_SERVER_PASSWORD = [Convert]::ToBase64String($b).TrimEnd('=').Replace('+','-').Replace('/','_')
-和_的替换是为了避开+/,这样密码在 URL、配置文件、shell 历史里都不需要转义。
启动
# 只监听 localhost —— 有隧道就够了,不要在局域网里裸奔OPENCODE_SERVER_PASSWORD="$OPENCODE_SERVER_PASSWORD" opencode serve --port 4096 --hostname 127.0.0.1
# 手机打开那个 https 网址,弹 Basic Auth# 用户名:opencode# 密码:你刚生成的那 24 位--hostname 保持 127.0.0.1 是个原则性问题:暴露面只留隧道这一个口。你用隧道纯粹是为了穿越 NAT,不是为了让同 Wi-Fi 下的邻居也能连你。
还有一层:--proxy-protocol
默认情况下你的应用看到的是 loopback 对端,看不到真实来访 IP。OpenTunnel 官方专门写了份 trust-boundary 文档提醒这件事 —— 意思是:
如果你的应用里有任何基于
127.0.0.1判断”这是本地可信请求”的逻辑,它在隧道后面是失效的。
需要传递真实来访者地址的话,客户端加:
opentunnel route add 4096 --proxy-protocol v2只在你的应用确实支持 PROXY protocol 时才开。 乱开会往应用里塞它不认识的请求头。
四、串起来的完整命令
# 1. 生成 24 位密码并记住它export OPENCODE_SERVER_PASSWORD=$(openssl rand -base64 18)
# 2. 起 opencode,只听 localhost,带 Basic AuthOPENCODE_SERVER_PASSWORD="$OPENCODE_SERVER_PASSWORD" \ opencode serve --port 4096 --hostname 127.0.0.1 &
# 3. 开隧道opentunnel route add 4096# https://21992cc9713e5fc5.3f2a91c4.opentunnel.xyz
# 4. 手机浏览器打开它,用 opencode / 你的密码登录想两边同时用(电脑上看 TUI,手机上发消息),加一个终端 attach:
opencode attach http://localhost:4096两个客户端共享同一份 session 状态,不是两个独立实例。
五、几个值得记的点
为什么是 blind TLS 而不是普通反代
| 普通反代(ngrok 类) | Blind TLS(OpenTunnel) | |
|---|---|---|
| 服务端能看到明文 | ✅ 能 | ❌ 不能 |
| 服务端持有私钥 | ✅ 持有 | ❌ 没有 |
| DNS-01 签发证书 | 依赖服务端 | 依赖客户端自己 |
| 被入侵后的损失 | 全部流量泄露 | 只有转发通道,没有内容 |
| 延迟 | 多一跳解密再加密 | 只读 SNI,开销极小 |
这本质上是把信任边界画在了正确的地方 —— 中转节点只负责”路由”,不负责”内容”。
它不是什么
- 不是 VPN。没有全局流量接管,你手机上只有这一个网址是”远程”的。
- 不是端口转发。只支持 HTTP 应用(“任何支持 TLS 的现代浏览器/设备”),不是任意 TCP。
- 不是零配置的安全方案。地址保密性来自不可猜测,不来自鉴权。密码必须设。
TIP既然是”限时/白嫖性质”的第三方服务,我的取舍是:可以用它做个人开发工具,但不要把生产系统的面板挂在上面。 它本质上是一个公共的流量中转,服务端不做任何端到端的身份认证 —— 信任链完全建立在你那 24 位密码上。
小结
把 localhost 变成一个公网 https 地址,难点从来不是”穿透”,而是在不牺牲加密强度的前提下穿透。OpenTunnel 的答案是:让服务端看不懂数据,只让它认门牌号。
- Blind TLS:服务端只读 SNI,证书私钥和明文永远留在本地
- DNS-01 签发:不需要公网 IP、不需要域名、80 端口也不用开
- 16 位随机路由名 + 不进 CT 日志:地址不可枚举
- 但仍需鉴权:
OPENCODE_SERVER_PASSWORD+ 24 位随机密码,是这套方案成立的前提 --hostname 127.0.0.1:暴露面只留隧道一个入口
现在我出门在地铁上,动动手指就能给家里的 opencode 派活儿了。写完这段话,我马上试一次。