托管 HTTP 路由
托管 HTTP 路由
托管路由由 Server 接收公网 HTTP/HTTPS/WebSocket 请求,再通过 Agent 连接到指定内网服务。公网只需要暴露 Server 的 80/443,不需要为每个 Agent 新开端口。
protocol=http-proxy 的路由不是反向代理:它由 TLS SNI 选路由、目标由每个代理请求决定,
target_host/target_port 存的是哨兵 */0,域名固定为 tp-<name>.<domain_suffix>。
使用说明见 HTTP 代理入口(tp-)](/TunnelMesh/user-guide/http-proxy-entry.html),部署见
[OpenResty tp- 代理入口。
明确子域名
在管理后台创建 Route,填写:
domain:例如git.example.compathPrefix:例如/agentId:目标 AgenttargetHost:Agent 网络内的服务地址targetPort:目标服务端口protocol:http或wstargetScheme:Agent 到目标服务的上游协议,http或https,默认httphostHeader:发送给目标服务的 Host,默认等于targetHosttlsServerName:上游 HTTPS 的 SNI 和证书校验名,默认等于targetHost
路由匹配按域名和路径执行;同一域名、同一路径不能重复创建。Server 会通过数据库唯一约束和 API 冲突检查共同保证这一点。
只支持域名访问的上游服务
targetHost 可以填写 IP 或域名。如果服务部署在固定 IP 上但只接受特定域名访问,可以分开配置拨号地址和 Host:
{
"agentId": "agent-devbox",
"protocol": "http",
"domain": "app.example.com",
"pathPrefix": "/",
"targetHost": "10.0.0.10",
"targetPort": 443,
"hostHeader": "service.internal.example.com",
"targetScheme": "https",
"tlsServerName": "service.internal.example.com"
}
实际链路为:
客户端 Host: app.example.com
Agent 拨号: 10.0.0.10:443
上游 TLS SNI: service.internal.example.com
上游 HTTP Host: service.internal.example.com
targetScheme=https 时,Agent 会使用系统信任根校验目标证书,并要求 TLS 1.2 及以上;不能关闭证书校验。tlsServerName 只在 targetScheme=https 时允许配置。公网路由的 protocol 仍表示对外协议,websocket 路由也可以配置 targetScheme=https 来访问 wss 上游服务。
client 本地端口映射没有独立的 hostHeader、targetScheme 或 tlsServerName 配置。普通 HTTP 虚拟主机可在本地请求中携带原始 Host;HTTPS-only 服务可使用 TCP 透明映射并让本地应用完成 TLS。具体命令参见 tunnelmesh-client 使用帮助。
修改托管路由
在管理后台 Routes 页面点击“编辑”,可以修改 Agent、域名、路径、协议、目标地址、目标端口和状态。普通用户只能编辑自己拥有 Agent 的路由;管理员可以编辑所有路由。修改目标 Agent 时,后端会再次校验新 Agent 的归属,避免把路由转移到其他用户资源。
路由更新会写入 route.updated 审计日志,详情包含 Agent、域名、路径、目标地址、端口和状态,不包含凭据。Server 内部路由缓存默认 5 秒刷新一次,因此修改成功后最长 5 秒内生效。
动态泛域名
在 Server 配置 server.dynamic_suffix,并启用 wildcard DNS 后,可使用以下形式表达 Agent、目标 IP 和端口:
<agent-id>-<ip-encoding>-<port>.apps.example.com
其中 IP 和端口使用明文编码,便于人工配置和排查;127.0.0.1 表示 Agent 所在主机上的本机服务。DNS Host 不区分大小写,因此动态域名中的 Agent ID 会与在线 Agent 做大小写不敏感匹配;如果存在多个仅大小写不同的 Agent ID,Server 会拒绝该动态请求,避免路由到错误 Agent。不要把 Token 或其他敏感信息放进域名。动态 wildcard 只适用于服务端支持的 HTTP/HTTPS/WebSocket 入口;VPN 网关的公网 UDP 端口是独立入口,不参与动态域名解析(ADR 0002,仅 -tags vpn 构建提供)。
动态泛域名当前只支持 IPv4 目标和明文 HTTP 上游,不支持 hostHeader、targetScheme=https 或 tlsServerName。如需这些能力,请创建显式托管路由。
显式泛域名
显式托管路由只支持单层 tm-* 前缀。API 中将域名模式写为
tm-*.example.com,实际请求例如 tm-git.example.com。通配符不能直接写成
*.example.com,也不能匹配多级子域名;不符合规则的域名会被 Server 拒绝。
上游路径与控制面保留端点
托管路由按 Host 归属,不按路径前缀归属。一个域名要么整站属于某条路由,要么是 Server 的控制面 origin(管理后台、Agent 拨号、健康检查)。因此上游服务可以自由使用 /api/...、/ws/... 这类路径,Server 不会把它们当成自己的管理接口截走。
tm-* 命名空间(tm-*.<domain_suffix> 显式泛域名与动态泛域名)的域名把全部路径交给上游,控制面不保留任何路径:
| 请求 | 处理方 |
|---|---|
https://tm-6000d.example.com/api/skill/claw/cate |
上游服务 |
https://tm-6000d.example.com/ws/chat |
上游服务 |
https://tm-6000d.example.com/health、/metrics |
上游服务 |
https://tm-6000d.example.com/skills |
上游服务 |
显式域名路由(例如 git.example.com)同样把业务路径交给上游,但保留四个 TunnelMesh 自有端点,因为运营者填写的域名有可能与控制面 origin 或 Agent 拨号 origin 撞名,而这两个端点被接走的后果是整个 Agent 无法重连、LB 把健康节点摘除:
| 保留路径 | 归属 | 原因 |
|---|---|---|
/ws/agent、/ws/client |
控制面 | Agent/Client 握手端点,被劫持会导致该 Agent 背后所有路由一起失效 |
/health/ 前缀、/metrics |
控制面 | LB 与监控探针目标,必须先于路由解析可用 |
上游服务如果确实需要在显式域名上暴露 /health、/metrics、/ws/agent、/ws/client,请改用其它路径,或改用 HTTP 代理入口(tp-*)(该入口按注入的身份头解析路由,不做路径前缀判定)。
不要把管理后台 origin 或 Agent 拨号 origin 配成显式域名路由,否则该 Host 的控制面(含后台静态资源)会被路由接管。tm-* 命名空间不存在这个风险,管理后台 origin 不会以 tm- 开头。
DNS 与 TLS
- 为
*.apps.example.com配置 wildcard DNS,指向 Server/LB;若使用显式泛域名,额外配置*.tunnel.example.com。 - 为相应 wildcard 域名配置证书,或在 LB 层终止 TLS。
- 将 HTTP/HTTPS 流量转发到 Server 的 80/443。
- 在后台创建明确路由,或将
server.dynamic_suffix配置为实际动态域名后缀。 - 用浏览器和
curl -v验证 Host、路径和 WebSocket Upgrade。
大响应体与流控
托管路由的响应体走 浏览器 → Server → Agent → 内网服务 隧道,与 WebSSH 共用同一套流控:Server 在 OPEN_STREAM 中通告 512 KiB 接收窗口,Agent 按窗口分帧发送(单帧上限 32 KiB),Server 在真正读走字节后才回补 WINDOW_UPDATE。前端打包产物(例如几 MB 的 assets/*.js)、文件下载和长轮询都能完整传输,无需任何配置项。流控全貌见 大文件传输与通道流控。
排障要点:
- 响应体在固定大小处被截断,浏览器报资源加载中断或
net::ERR_CONTENT_LENGTH_MISMATCH,而响应头里的Content-Length正常:说明 Agent 侧的流在队列满时被关闭,通常是 Agent 版本旧于 Server(每流发送队列小于 Server 通告的窗口)。把 Server 与 Agent 升级到同一版本即可。 - 经 Nginx/OpenResty 反代时,
proxy_buffering off与足够长的proxy_read_timeout是大响应体不被中间层掐断的前提,见 Nginx/WSS 推荐配置。
API 示例
curl -X POST https://tunnel.example.com/api/v1/routes \
-H "Authorization: Bearer $TUNNELMESH_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: route-git-001" \
-d '{
"agentId": "agent-devbox",
"protocol": "http",
"domain": "git.example.com",
"pathPrefix": "/",
"targetHost": "127.0.0.1",
"targetPort": 3000,
"targetScheme": "http"
}'
写操作应始终设置稳定的 Idempotency-Key,重试时复用同一个值。
更新已有路由使用 PATCH /api/v1/routes/{routeId},只提交需要修改的字段:
curl -X PATCH https://tunnel.example.com/api/v1/routes/route-id \
-H "Authorization: Bearer $TUNNELMESH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "agent-devbox",
"domain": "git.example.com",
"pathPrefix": "/",
"protocol": "websocket",
"targetHost": "127.0.0.1",
"targetPort": 3000,
"targetScheme": "https",
"tlsServerName": "service.internal.example.com",
"hostHeader": "service.internal.example.com",
"status": "active"
}'