# dtns-http-proxy-site **Repository Path**: dtnsman/dtns-http-proxy-site ## Basic Information - **Project Name**: dtns-http-proxy-site - **Description**: DTNS HTTP Proxy 项目介绍站 — 静态站点与本地预览服务器(GPLv3) - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-27 - **Last Updated**: 2026-10-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # DTNS HTTP Proxy > 一个域名,直达内网服务。把「本地能跑」变成「公网可用」——不需要公网 IP,不需要装客户端,甚至不需要一台常驻服务器。 许可证:**GPL-3.0** · 运行环境:**Node.js ≥ 18** · 线上示例: --- ## 目录 - [解决什么问题](#解决什么问题) - [与同类方案对比](#与同类方案对比) - [典型使用场景](#典型使用场景) - [快速开始](#快速开始) - [架构](#架构) - [浏览器端 Agent API](#浏览器端-agent-api) - [代理 SDK](#代理-sdk) - [本地运行](#本地运行) - [配置参考](#配置参考) - [安全边界](#安全边界) - [开源协议](#开源协议) --- ## 解决什么问题 内网穿透这个品类已经很成熟,但真实工作里的卡点往往不在「有没有工具」,而在**这个工具在你的环境里跑不跑得起来**。 ### 困境一:没有公网 IP,端口映射也做不了 家宽、校园网、公司办公网几乎都在 NAT 后面。路由器不在你手上,即使拿到了动态公网 IP,运营商也可能封了 80/443。想给同事看一眼刚做好的页面,或者让外部回调打到本机,第一步就卡住。 ### 困境二:装不了客户端 这是最常见、也最少被讨论的痛点。公司电脑没有管理员权限,`ngrok`、`cloudflared`、`frpc` 这类二进制装不上;即使能装,安全策略也常常禁止未经审批的常驻进程。**但浏览器一定是有的。** ### 困境三:想让 AI Agent 操作"我正开着的那个页面" 给 Agent 开放浏览器这事,卡在两难: - 直接给 Agent 一个真实浏览器 → 它拿着你的登录态,风险不可控; - 用无头浏览器(Playwright / Browserbase)→ 得复刻登录态、过验证码、同步 Cookie,而且它操作的**永远是一份副本**,不是你屏幕上那个有状态的页面。 你真正想要的,往往是"让 Agent 读一下这个页面、点一下这个按钮",而不是"再起一个浏览器"。 ### 困境四:实时能力被阉割 很多穿透方案把 HTTP 转发做得很顺,一到 WebSocket 就开始断流、要额外配置、或者干脆不支持。SSE 流式响应同理。做实时看板、协同编辑、AI 流式输出时,这个短板很致命。 ### 困境五:不想把整个内网暴露出去 只想让外部访问**一个页面**或**一个 API**,而不是把整台机器、整个网段挂到公网上。最小暴露面本身就是安全需求。 **DTNS HTTP Proxy 的答案**:把隧道客户端做成**一个浏览器标签页**,让页面本身成为可被 HTTP 调用、可被 AI Agent 驱动的公网端点;同时用一条自定义二进制协议,把 WebSocket 双工、SSE 流式响应和背压一起透传过去。 --- ## 与同类方案对比 ### 横向对比 | 方案 | 运行形态 | 需要公网 IP / 自备服务器 | 需要安装客户端 | 浏览器标签页即可起服务 | WebSocket / SSE | 主要门槛 | | --- | --- | --- | --- | --- | --- | --- | | **DTNS HTTP Proxy**(本项目) | 隧道 + 浏览器/Node 双端客户端 | 否(隧道服务端已提供) | 浏览器端:**否**
Node 端:是 | ✅ | ✅ 双工 + SSE + 背压 | 需要一条已注册的隧道域名 | | [ngrok](https://ngrok.com/) | 商业 SaaS 隧道 | 否 | 是(ngrok agent) | ❌ | ✅ | 免费版为随机临时域名、HTML 响应带浏览器提示页,固定域名属付费能力 | | [Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/) | 商业 SaaS 隧道 | 否 | 是(cloudflared) | ❌ | ✅ | 域名 DNS 必须托管在 Cloudflare,连接器与路由需自行配置 | | [frp](https://github.com/fatedier/frp) | 开源自建 | **是**(自己的 VPS) | 是(frpc) | ❌ | ✅(需额外配置) | 需自购 VPS、维护 frps、处理域名与证书 | | [localtunnel](https://github.com/localtunnel/localtunnel) | 免费公共中继 | 否 | 是(`npx`,近乎零安装) | ❌ | ⚠️ 支持有限 | 访客需在同意页输入隧道密码;公共中继稳定性与限速不可控 | | [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) | 组网 + 公网出口 | 否 | 是(Tailscale 客户端) | ❌ | ✅ | 仅开放 443 / 8443 / 10000 端口,需登录 Tailscale 账号,域名固定为 `*.ts.net` | | [Playwright](https://playwright.dev/) / [Browserbase](https://www.browserbase.com/) 等浏览器自动化 | 无头浏览器(本地或云) | 通常需要 | 是(SDK + 浏览器运行时) | ❌ | ⚠️ 走 CDP 调试协议 | 需复刻登录态、应对验证码,操作的是浏览器副本而非用户的真实会话 | > 表中竞品特性基于其公开文档与常见部署形态整理,具体额度与限制以各项目官方当前政策为准。 ### 我们的四个竞争点 #### 1. 零安装:浏览器就是隧道客户端 这是最硬的一条差异。上面几乎所有方案都要求你在目标机器上装一个二进制或常驻进程;本项目把隧道客户端做成了 `assets/agent.js`——**打开一个页面,隧道就通了**。 对于受限终端(无管理员权限、禁止安装软件、只能用浏览器)的场景,这不是"更方便",而是"唯一可行"。 #### 2. 页面即 API:操作真实会话,而不是复刻一份 `test-agent.html` / `agent.html` 把当前页面的能力包装成标准 HTTP API,并自动生成 OpenAPI 3 描述: ```bash curl -s -H "Authorization: Bearer $TOKEN" \ "http://agent-xxxx.html.dtns.top/agent/page" ``` Agent 拿到的是**你屏幕上这个页面的真实 DOM 与真实登录态**,不需要 Cookie 导出、不需要重放登录流程、不需要对抗风控。读取标题与章节、滚动、高亮、点击按钮——都在原页面里发生。 对比无头浏览器方案,这省掉的是整条"复刻环境"的成本;对比直接开放浏览器,它把能力收敛到一组**白名单端点**(只允许 button / summary / tab / `[data-agent-click]`,链接一律拒绝),暴露面小得多。 #### 3. 协议级完整透传:不只是 HTTP DTNS 是一套二进制帧协议,请求与响应**原样搬运**,因此同一条 WSS 长连接上可以同时承载: - **WebSocket 全双工**:升级请求、双向帧、关闭码都走隧道,SDK 提供 `app.ws()` 路由; - **SSE 与流式响应**:分块响应不被缓冲截断; - **双向背压**:下游慢的时候上游会被压住,而不是无限堆内存; - **Host 即路由**:一个服务端按 `Host` 分发到不同隧道,无需为每个服务开端口。 #### 4. 可自托管,不锁定 协议实现、浏览器客户端、代理 SDK、站点源码都在这个仓库里,GPL-3.0 授权。你不需要把流量交给一个只能订阅的 SaaS:可以自己部署隧道服务端,把域名、证书、访问策略握在自己手里。 SDK 是**零依赖**的——`assets/sdk.js` 内置了协议编解码,浏览器用全局 `DTNSAgent`,Node.js 直接 `require`,不拉任何 npm 包。 ### 不适合的场景 诚实地讲,以下场景请选别的方案: | 你的需求 | 更合适的选择 | 原因 | | --- | --- | --- | | 生产级 SLA、固定域名、团队协作与审计 | Cloudflare Tunnel / 商业版 ngrok | 本项目定位是轻量、可控、可自托管,不承诺商业 SLA | | 转发 SSH、数据库、任意 TCP/UDP | frp / Tailscale | 本项目专注 HTTP 系协议(含 WebSocket / SSE),不是通用四层隧道 | | 大规模网页抓取、无人值守的浏览器集群 | Playwright + 代理池 / Browserbase | 页面隧道依赖标签页存活,不适合做批量采集基础设施 | | 7×24 常驻服务 | 用 Node 客户端而非浏览器标签页 | 浏览器标签页关闭即离线,这是设计上的取舍 | --- ## 典型使用场景 | 场景 | 怎么做 | | --- | --- | | **给同事/客户演示本地页面** | 起本地服务 → 配一条隧道 → 发域名,对方直接打开,无需你装任何东西 | | **接收第三方 Webhook 回调** | 把回调地址指向隧道域名,请求直接落到 `127.0.0.1` 的服务 | | **给 AI Agent 一个可调用的界面** | 打开 `agent.html`,把 API Base + OpenAPI 地址交给 Agent,它用普通 HTTP 就能读页面、点按钮 | | **远程联调 WebSocket 服务** | 隧道透传升级与双工帧,公网 `ws://` 直连本地实现 | | **演示环境临时暴露单个 API** | 只暴露目标端口,内网其余部分不可见 | | **在受限终端上做临时出口** | 无管理员权限时,浏览器标签页就是客户端 | --- ## 快速开始 ### 方式一:浏览器标签页(零安装) 1. 打开 ; 2. 页面自动注册一条随机隧道(形如 `agent-xxxxxxxx.html.dtns.top`),域名即凭证; 3. 把页面显示的 API Base 与 `/agent/openapi.json` 地址交给调用方; ```bash curl -s "http://agent-xxxxxxxx.html.dtns.top/agent/page" ``` > URL-only 模式下,拿到链接的人即可调用。只发给可信对象,不要公开。需要 Token 校验请用 `test-agent.html`。 ### 方式二:Node 客户端(常驻) ```bash git clone https://github.com/dtnsman/dtns-http-proxy-site.git cd dtns-http-proxy-site node server.js # 本地预览站点,默认 127.0.0.1:3016 ``` 在 `client/config.js` 中登记隧道: ```js { id: 'site', domain: 'dtns-proxy.html.dtns.top', local: { host: '127.0.0.1', port: 3016, protocol: 'http' }, enabled: true, description: '项目介绍网站' } ``` 重启隧道客户端使配置生效,浏览器访问域名,看到 200 即打通。 ### 方式三:用 SDK 在自己的页面里起服务 ```html ``` --- ## 架构 ``` 浏览器 ──► 接入层(nginx, 终止 TLS) ──► 代理(:18386) ──► 隧道服务端 │ WSS 长连接 ▼ 隧道客户端(浏览器标签页 / Node) │ ▼ 本地服务 :3016 ``` 一次请求的完整链路: 1. **接入**:接入层终止 TLS,把 HTTP 请求交给 `:18386` 代理; 2. **查表**:代理读取 `Host`,在已注册隧道中定位 `clientId` 与目标隧道; 3. **编码**:请求被编码为 DTNS 二进制帧,经 WSS 长连接送达内网; 4. **转发**:客户端按 `config.js` 转发到本地端口,响应沿原路返回; 5. **透传**:WebSocket 升级、双向帧与 SSE 事件流都走同一条通道。 ### 核心能力 | 能力 | 说明 | | --- | --- | | Host 即路由 | 一个服务端按域名分发到多条隧道,互不干扰 | | WebSocket 全双工 | 升级与双向帧原生支持,关闭码在协议内保留 | | SSE 与流式响应 | 分块响应不缓冲、不截断 | | DTNS 二进制协议 | 紧凑帧格式,请求响应原样搬运 | | 双向背压 | 下游拥塞时反向压制上游,避免内存堆积 | | 自愈连接 | 心跳 + 线性退避重连(上限 60 秒),健康检查发现隧道丢失会主动重注册 | ### 本仓库的范围 本仓库包含**项目介绍站**、**浏览器端隧道客户端**(`assets/agent.js`)、**代理 SDK**(`assets/sdk.js`)与**协议浏览器移植**(`assets/dtns-protocol.js`)。隧道服务端(`res2.dtns.top`)为独立部署,不在本仓库内。 ``` dtns-http-proxy-site/ ├── index.html # 首页(概览 / 架构 / 能力 / 协议 / 上手 / FAQ) ├── 404.html # 404 页面 ├── server.js # 零依赖静态服务 + 只读 API ├── start.sh # 启动脚本 ├── package.json ├── test-agent.html # Agent API 测试页(index.html 的副本 + 控制台,Bearer Token) ├── agent.html # URL-only 版本(不校验 Token,随机 URL 即凭证) ├── sdk.html # SDK 指南 + Live Demo └── assets/ ├── style.css ├── agent.css ├── sdk.css ├── sdk.js # 零依赖 Express 风格隧道代理 SDK(内置 DTNS 协议) ├── sdk-guide.js # sdk.html 的 Live Demo ├── app.js ├── agent.js # 浏览器隧道 + 页面 HTTP API ├── dtns-protocol.js # client/protocol.js 的浏览器移植 ├── early.js └── favicon.svg ``` 站点的只读 API:`/healthz`(探活)、`/api/status`(运行状态)、`/api/tunnels`(隧道数量,不含域名与端口)。 --- ## 浏览器端 Agent API `test-agent.html` 是 `index.html` 的副本,额外挂了一个 Agent 控制台。页面会: 1. 在浏览器里运行 `assets/dtns-protocol.js`(`client/protocol.js` 的浏览器移植); 2. 连接 `wss://res2.dtns.top/tunnel`,以浏览器身份注册一条随机 `agent-xxxx.html.dtns.top` 隧道; 3. 接收隧道转来的 `http_request`,在本页执行只读/交互操作,再以 `http_response` 返回。 只要标签页保持打开,AI Agent 就能用普通 HTTP 调用页面: | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/agent/status` | 连接状态、协议版本、端点列表(公开) | | GET | `/agent/openapi.json` | OpenAPI 3 描述(公开) | | GET | `/agent/help` | 端点与 curl 示例(公开) | | GET | `/agent/page` | 标题、章节、滚动位置、站点统计 | | GET | `/agent/sections` | 章节大纲 | | GET | `/agent/text?selector=main` | 读取元素文本(Token 自动脱敏) | | POST | `/agent/scroll` | 滚动到章节或坐标 | | POST | `/agent/highlight` | 高亮元素 | | POST | `/agent/click` | 点击 button / summary / tab | | GET | `/agent/ping` | 回显请求(敏感头已过滤) | | GET | `/agent/site` | 站点 `/api/status` | | GET | `/agent/log` | 最近请求日志 | | POST | `/agent/log/clear` | 清空日志 | | POST | `/agent/reconnect` | 重连隧道 | 鉴权使用 Bearer Token: ```bash curl -s -H "Authorization: Bearer $TOKEN" \ "http://agent-xxxx.html.dtns.top/agent/page" ``` - Token 每个标签页随机生成,只显示在页面上;`status` / `openapi.json` / `help` 为公开端点; - 默认域名随机,避免和其他标签页或服务冲突;页面里可以改成固定域名(仅限 `*.html.dtns.top`); - 关闭标签页后隧道立即离线,API 不可用;重新打开会得到新的随机域名(同一标签页刷新会保留 sessionStorage 中的域名); - 页面不执行任意 `eval`,点击仅允许 button / summary / tab / `[data-agent-click]`,链接会被拒绝; - `text` / `ping` / `page` / `log` 输出会脱敏 Token;API 不返回 Token。 ### URL-only 版本:agent.html `http://dtns-proxy.html.dtns.top/agent.html` 与 `test-agent.html` 共用 `assets/agent.js`,但页面 `body` 带 `data-agent-auth="none"`,运行时会: - 不生成、不显示、不校验 Token; - API 的 `auth.required` 为 `false`,`/agent/openapi.json` 不含 `security` / `components`; - 默认域名使用 96 bit 随机值(`agent-` + 24 位 hex),URL 本身就是凭证; - 页面不显示 Token 行,curl 示例与「复制给 Agent」只包含 API Base 和 OpenAPI 地址。 **安全边界**:拿到 URL 的人就能调用 API。只把链接发给可信的 Agent,不要发到公开渠道;自定义固定域名会降低不可猜测性,建议保留随机域名。关闭标签页后 API 立即离线。 --- ## 代理 SDK `http://dtns-proxy.html.dtns.top/sdk.html` `assets/sdk.js` 是零依赖代理 SDK,内置 DTNS 二进制编解码。浏览器用全局 `DTNSAgent`,Node.js 用 `require`。它负责: - 连接 `wss://res2.dtns.top/tunnel`、注册域名、心跳、断线重连; - 解码 `http_request`、匹配 Express 风格路由、编码 `http_response`; - 解析 query / JSON / form / text body,处理 HEAD / OPTIONS / 404 / 405 / 401; - 处理 WebSocket 升级、`websocket_data` / `websocket_close` 帧,提供 `app.ws` 双工路由; - URL-only 或 Bearer Token。 浏览器: ```html ``` Node.js: ```js const DTNSAgent = require('./sdk.js'); const WebSocket = require('ws'); (async () => { const app = DTNSAgent.app({ domain: 'agent-xxxx.html.dtns.top', WebSocket }); app.get('/hello', (req, res) => res.json({ ok: true })); await app.listen(); })(); ``` WebSocket(同一个 app): ```js app.ws('/live/:room', (ws, req) => { ws.send('welcome ' + req.params.room); ws.on('message', (data, isBinary) => { // data: string(文本)/ Uint8Array(二进制) ws.send(data, isBinary); }); ws.on('close', (code, reason) => { console.log('closed', code, reason); }); }); ``` 浏览器端: ```js const socket = new WebSocket('ws://agent-xxxx.html.dtns.top/live/room1'); socket.onopen = () => socket.send('hello'); socket.onmessage = (event) => console.log(event.data); ``` 当前入口只配置了 HTTP,所以公网 WebSocket 使用 `ws://`;启用 TLS 后改为 `wss://`。 Token 模式只需多传 `token`: ```js const app = DTNSAgent.app({ domain: 'agent-xxxx.html.dtns.top', token: '', WebSocket }); ``` SDK 会校验 `Authorization: Bearer ...`;不传 token 即 URL-only。OPTIONS 预检不需要 token。浏览器 WebSocket 不能自定义 Authorization 头,Token 模式请用 `ws://.../live?token=`。 主要 API: | 方法 | 说明 | | --- | --- | | `DTNSAgent.app(options)` | 创建隧道应用 | | `app.get/post/put/patch/delete/head/options/all(path, handler)` | 绑定 HTTP 路由 | | `app.ws(path, handler)` | 绑定 WebSocket 路由 | | `app.use([path], middleware)` | `(req, res, next)` 中间件 | | `app.listen() / app.close()` | 连接注册 / 断开 | | `app.status() / app.on(event, fn)` | 状态 / 事件 | | `app.routes / app.apiBase` | 路由表 / 公网 URL | | `req.method/path/query/params/headers/body` | 请求元信息 | | `req.get(name)` | 读取请求头 | | `res.status(code) / res.set(name, value)` | 状态码 / 响应头 | | `res.json(obj) / res.send(value)` | JSON / 自动类型响应 | | `res.text(str) / res.end(body)` | 文本 / 结束响应 | | `res.redirect(url, status)` | 重定向 | | `ws.send(data[, binary])` / `sendText` / `sendBinary` | 发送文本 / 二进制 | | `ws.on('message', (data, isBinary) => ...)` | 接收双工消息 | | `ws.on('close', (code, reason) => ...)` | 连接关闭 | | `ws.close(code, reason)` | 主动关闭 | Handler 支持 async/await;返回对象会自动 JSON;抛错自动 500。中间件使用 `(req, res, next)`,路由支持 `:param` 路径参数。 WebSocket 关闭码在隧道协议中占 1 字节;SDK 会将正常关闭 `1000` 映射为 `0` 后由服务端还原为 `1000`,建议使用 `ws.close(1000, reason)`,非 `1000` 的自定义关闭码可能被归一化。 `sdk.html` 的 Demo 会注册一条独立的 `sdk-demo-*.html.dtns.top` 隧道,按钮直接请求 `/hello`、`/users/:id`、`/echo`、`/auto`、`/page`、`/highlight`、`/status`,并显示 Demo 请求日志;下方 WebSocket 面板连接 `app.ws('/live')`,发送消息即可看到双工回显。`/page` 和 `/highlight` 展示了 SDK 在浏览器里直接操作页面 DOM 的能力。 --- ## 本地运行 ```bash node server.js # http://127.0.0.1:3016/ ``` 可用环境变量: | 变量 | 默认值 | 说明 | | --- | --- | --- | | `PORT` | `3016` | 监听端口 | | `HOST` | `127.0.0.1` | 监听地址,默认只绑定回环 | | `DTNS_CONFIG` | `../client/config.js` | 隧道配置路径 | 零依赖:只用 `http` / `fs` / `path` / `zlib`,无需 `npm install`。 --- ## 配置参考 ### 客户端 `client/config.js` ```js { id: 'site', domain: 'dtns-proxy.html.dtns.top', local: { host: '127.0.0.1', port: 3016, protocol: 'http' }, enabled: true, description: '项目介绍网站' } ``` 修改前先备份: ```bash cp client/config.js client/config.js.bak-$(date +%Y%m%d-%H%M%S) ``` 然后重启隧道客户端,让新的隧道完成注册。 ### 常见问题 | 现象 | 原因与处理 | | --- | --- | | 域名返回 `No tunnel registered for domain` | 请求已到服务端,但 Host 未匹配到已注册隧道。检查域名拼写、客户端是否在线、客户端日志是否显示「隧道注册成功」 | | 4xx / 5xx 状态码显示不正常 | v1 协议单字节状态码限制(`401 & 0xff = 145`)。使用 v2 协议即可无损;客户端连接旧服务端时会自动把超范围响应切到 JSON 包 | | WebSocket 连上就断 | 先确认本地服务本身能升级,再检查路径、子协议与 `Origin`。若本地框架校验 Host / Origin,可在隧道上开启 `rewriteHost` / `rewriteOrigin` | | 上传大文件被 413 | 服务端默认请求体上限 32 MB。可在 `limits.maxBodySize` 调整全局值,或用 `maxBodySizeByDomain` 为特定域名放宽 | | 断线要手动重连吗 | 不需要。客户端内置心跳与线性退避重连(上限 60 秒);健康检查发现隧道未注册时会主动重连并全量重新注册 | | 能给隧道加访问控制吗 | 建议在入口层(nginx basic auth、IP 白名单、OIDC 等)做。隧道配置里的 auth 是访问上游时附加的 Basic Auth,不是公网鉴权 | --- ## 安全边界 - 服务只读,仅绑定 `127.0.0.1`,不直接暴露到公网; - `/api/tunnels` 只返回数量,不公开域名与端口; - Agent API 的 Token 不写入日志、不返回给调用方;`text` 输出会脱敏; - `agent.html` 没有 Token,URL 即凭证;请勿公开链接; - `sdk.js` 不存储凭证;Token 只在调用方传入时用于 `Authorization` 头; - 页面不执行任意代码,点击目标有白名单; - 响应带 CSP、`X-Content-Type-Options`、`Frame-Options`、`Referrer-Policy` 等头; - 站点目录内不要放置密钥、备份或未脱敏配置。 > 当前实例的公网入口只配置了 HTTP;生产环境应在 nginx 侧为隧道域名配置证书并强制 HTTPS。 --- ## 开源协议 本项目基于 [GNU General Public License v3.0](LICENSE) 发布。 你可以自由使用、修改、分发,包括商业用途,但衍生作品须以同等许可开源,并保留版权声明。完整条款见 [LICENSE](LICENSE)。