eating my own dog food
2026 年 06 月 03 日
前因
之前写了个 plankroad 给自己用,说实话感觉还不错,真的是可以应急。但写的实在太简陋了,除了特别轻(镜像约 9 M),似乎也没啥优点。
某天跟 chatgpt 聊天时发现,在这个工具思路的基础上,似乎可以扩展出不少用例。
比如说,跑一个客户端,长连到一个 PaaS 上运行的自己的服务端,就可以把内网服务暴露出去。也就是内网穿透。这个功能既像 cloudflare tunnel,但不会被限制在 cloudflare 的域名和服务上;也类似 frp,但服务端部署起来会更简单。
内网穿透是打通从外到内的线路,从内到外也是一样可行。这个就不细说了。
究其本质,其实是说,运行在 PaaS 上的一个普通的 http 服务,实际上可以是一个流量的中转站。虽然入口限制只能是 http,但其实任何流量都可以封装在这个协议里。
用 chatgpt 的说法,这可以叫做 Application Connectivity Gateway(应用连接网关)。这类产品有很多,但能在 PaaS 上跑的,似乎就真没有了,可能是因为大厂不想做,个人懒得做。
所以我做了一个给自己用。名字也是 chatgpt 帮忙起的,叫 monogate。
原理
monogate 实现内网穿透的原理,跟 frp 这类内网穿透工具是一样的。一个长连连接,外部请求通过这个长连连接转发,内网服务器响应。
flowchart LR
A(浏览器) -->|http请求| B[服务器] <-->|基于http/ws/grpc/...的自定义协议| C[客户端] -->|http请求| D(内网服务器)
D --> |http响应|C
B --> |http响应|A
不经过客户端的场景必然也是也是存在的,例如用 xterm.js 构建一个 web 终端来操作服务器的 shell:
flowchart LR
A(浏览器/其他应用) <-->|websocket| B[服务器] --> |shell指令| C(shell)
C --> |shell响应|B
还有另外一种场景,辅助 P2P 连接,不过现在还没实现这部分,没有很实际的需求:
flowchart LR
A[客户端A] <-->|基于http/ws/grpc/...的自定义协议| B[服务器] <--> |基于http/ws/grpc/...的自定义协议| C(客户端B)
C <-.-> |P2P 直连|A
可以看出,所有的用例都可以归结于以下三种方式:
- 类似内网穿透的用例:
- 服务器收到原始的 http 请求,将其封装到自定义协议中,再发送给客户端;
- 客户端解开封装获得原始请求,转发给真实目标;响应原路返回。
- 类似 xterm.js 的用例:
- 使用浏览器或其他应用,将要发到真实目标的流量封装在 http 请求/响应中;
- 服务器解封装得到目标流量,转发到真实目标;响应原路返回。
- P2P 连接方式,多个客户端通过服务器中转来辅助建立 P2P 连接。
由于是运行于 PaaS 平台,所以通常来说,约束只有一个,就是 http 协议。这也是 PaaS 平台通常能提供的外网连接的主要方式。
注:某些 PaaS 平台能提供 tcp 端口转发,这样可以带来更多可能性,比如 tcp 内网穿透。
服务端会把不同的功能,以资源的方式,在不同的 path 上提供。例如,客户端默认的长连 path,就是 /tunnel/ws。
用法
本来还想写写协议,毕竟每一个资源都对应一堆协议。不过想想意义也不大。一方面,大部分协议都是照文档做的,没什么写的必要。另一方面,自定义协议很简单,根据不同底层传输协议还有所区别,所以这部分先空着吧。
用法简单来说,按照以下几步走即可。
在 PaaS 平台上部署 Docker 服务
各大 PaaS 平台界面不同,但本质相同,都是拉取下面的镜像。
docker pull ghcr.io/cliffhan/monogate-server:master
然后是环境变量配置(注:并非完整列表):
| 变量名 | 必须 | 建议 | 含义 | 默认值 |
|---|---|---|---|---|
| MONOGATE_API_KEY | ✅ | ✅ | 认证密钥 | - |
| MONOGATE_SERVER_ORIGIN | ❌ | ✅ | 服务器地址,例如:https://my-monogate-server.com,如果知道的话建议配上 | - |
| MONOGATE_IP | ❌ | ❌ | 监听绑定 IP | 0.0.0.0 |
| MONOGATE_PORT | ❌ | ❌ | 监听绑定端口 | 8080 |
| MONOGATE_WS_TUNNEL_ENDPOINT | ❌ | ❌ | 客户端连接默认端点 | /tunnel/ws |
| MONOGATE_EMBEDDED_CONSOLE_ENABLED | ❌ | ❌ | 内置控制台,提供客户端列表查看和 xterm web shell | false |
| MONOGATE_EMBEDDED_CONSOLE_ENDPOINT | ❌ | ❌ | 内置控制台端点 | /monogate/console |
| MONOGATE_EMBEDDED_PORTAL_ENABLED | ❌ | ✅ | 内置门户,提供客户端下载,启用可以方便下载客户端 | false |
| MONOGATE_EMBEDDED_PORTAL_ENDPOINT | ❌ | ❌ | 内置门户端点 | /monogate/portal |
| MONOGATE_CLOUDFLARED_QUICK_TUNNEL_ENABLED | ❌ | ❌ | 启用 Cloudflare quick tunnel | false |
| MONOGATE_CLOUDFLARED_NAMED_TUNNEL_TOKEN | ❌ | ❌ | 启用 Cloudflare named tunnel,需同时设置 domain | - |
| MONOGATE_CLOUDFLARED_NAMED_TUNNEL_DOMAIN | ❌ | ❌ | 启用 Cloudflare named tunnel,需同时设置 token | - |
必须要设置的其实只有一个 API KEY,这个值同时用于客户端连接鉴权,和 embedded console 鉴权。
建议配置的,Server Origin 让服务端知道如何到达(实际上客户端连接时也会提交这个值);Embedded Portal 提供一个可以下载客户端的地址。
其他大部分使用默认值即可。
注:启用 Cloudflare tunnel 后可以提供更多连接方式。
运行客户端进行内网穿透
如果开启了 portal,下载客户端就比较省事,也不用担心版本适配问题。直接去 https://{yourserver}/monogate/portal 去下载对应版本客户端即可。注意要把 {yourserver} 替换成真正的服务器地址。
如果觉得开启 portal 有风险,那么暂时先开启一次,下载完客户端之后删掉这个环境变量,再重启服务器即可。
服务端命令在 portal 上有提示,但基本风格是这样的:
monogate-client -t {server} -k {api_key} --root {local_server}
把上面几个参数替换成正确的就可以了。
例如,monogate-client -t https://my-monogate-server.com -k 123456 --root http://192.168.0.1,这样就可以把http://192.168.0.1 的网页映射到 https://my-monogate-server.com。
总结
现在已经用 monogate 把 plankroad 替换掉了,后续的功能都会在这个工程上扩展。因为有明确需求,所以自己的狗粮真正吃起来,嗯,味道还不错。