service-tunnels
大约 4 分钟
service-tunnels
服务端口发现与隧道(完整实现随扩展自包含,2026-08 从 pi-web core 剥离)。在哪都该能安全访问自己的本地服务——这正是它的来源。
能力
1. 服务端口发现(Ports)
- 自动发现宿主进程树(含 pi agent 及其 spawn 的进程)当前监听的 TCP 端口(
GET /api/extensions/service-tunnels/services?refresh=1) - 端口列表即 proxy 的 allow-list:只有 enrolled 的端口可经 vhost(
<port><suffix>)代理访问(默认零转发) - 附带隧道信息与平台信息
2. 暴露与隧道(Expose / Tunnels)
- expose:把本地端口暴露为 http / https / tcp 隧道(schema 驱动参数,如 provider / site / host 后缀)
- tunnel-start / tunnel-stop / tunnel-stop-all:隧道启停管理
- 暴露入口绑定(addBinding)由扩展 middleware 统一代理(vhost 直通)
3. 自暴露(Self)
- exposeSelf / stopSelf:把宿主自身暴露为可访问站点(
PI_WEB_EXPOSE启动配置) - allowlocal 两阶段门禁:自暴露直连需显式允许(本地访问控制)
- allowhosts:自暴露 host 白名单管理
- 自暴露访问地址查询(getSelfAccessUrls)
4. 认证(Users / TOTP)
- 用户管理:user-add / remove / list / set-password / managers / summary(Basic 认证)
- TOTP 两因素:enroll / activate / status / disable / default-label(生产站点强制 two_factor 策略)
- 未认证请求 401/302 到登录(Authelia 或内置用户)
5. 注入联动(Inject)
- middleware 在认证决策后的 HTML 响应上注入 aiti-devtools launcher(
globalThis.__aitiInject判定的注入端口白名单) - 被注入页面与隧道同源,注入/调试桥/隧道共用同一套 vhost 门禁
6. 路由器映射 / IPv6 直连 / DDNS(0.4.0)
- portmap 模式:UPnP/NAT-PMP/PCP/ssh-router 路由器映射;表单「探测路由器」判定(CGNAT→建议隧道、公网直连→免映射);详情含外部 ip:port 与租约剩余
- ipv6 模式:直连暴露(候选地址下拉 + 公网出口标注 + 手动输入)
- DDNS tab:provider 凭据(120 家下拉)/ profile / 注册条目三层——portmap/tunnel 暴露自动更新 A/AAAA/CNAME/TXT;与库 CLI/自带 UI 同一 registry 文件双开互查
7. 体验与治理(0.4.0)
- capabilities 能力协商:库版本能力探测式门控,tab/表单项缺能力自动隐藏(0.3.66 部署面优雅降级)
- 服务识别:端口卡主行 favicon/探测标题/用户自定义名(label > title 单源规则)+ ✎ 重命名
- 公开方案(加密存储 + 上次使用回填 + 一键动作)、注入源勾选、Base Path 端口属性
8. 安全与记录
- DNS-rebinding 门禁(宿主 proxy.ts):所有 API 请求先过门禁(fail-closed)
- allow-tool-download:工具下载许可开关
- 隧道日志:暴露/隧道启停与自暴露状态记录
架构(2026-08 从 pi-web core 剥离后)
pi-web core(只留通用门禁)
├─ proxy.ts:扩展 middleware 钩子(fail-closed)→ DNS-rebinding + PI_WEB_PASSWORD
├─ instrumentation.ts:ext registry 加载/启动(service-tunnels 由 startAll 启动)
└─ app/api/extensions/[...path]/route.ts:/api/extensions/<id>/<action> 分发(含门禁)
service-tunnels 扩展(~/.pi/web/extensions/service-tunnels/,完全自包含)
├─ server.startup → createServiceTunnels 单例(globalThis.__piServiceTunnels)+ PI_WEB_EXPOSE
├─ server.middleware → 服务 vhost 分支(st.proxy.parseHost/resolve)+ allowlocal 门禁
├─ server.api → /api/extensions/service-tunnels/<action>(发现/暴露/隧道/用户/TOTP/自暴露)
├─ ui.toolbar/dialogs/banners + client bundle → ServicesDialog(五 tab:端口/Pi-web/用户/DDNS/体检,capabilities 门控)+ 🔴 自暴露告警
└─ node_modules/service-tunnels/ —— 标准 npm 依赖(私有 npm 源,deploy 时
`npm install --omit=dev` 安装;加载器按扩展目录解析裸说明符)- URL 变更:旧
/api/services、/api/service-tunnels已随剥离移除;统一走/api/extensions/service-tunnels/services与/api/extensions/service-tunnels/status - middleware 兼容:宿主 proxy.ts 在核心门禁前调用扩展 middleware 钩子——vhost 决策、allowlocal 门禁、自暴露隧道域名映射全部在扩展内(与迁移前同进程、同 globalThis 单例)
- aiti-devtools 联动:DI 方案 P1 起新通道 =
ctx.services.get("service-tunnels.api");globalThis.__piServiceTunnels双轨保留(宿主 middleware 独立 bundle 仍走它)。读库实例做 注入端口 enroll;middleware 的 aiti 注入经globalThis.__aitiInject读取 - 无扩展时:无 vhost/自暴露功能,
/api/extensions/service-tunnels/*404, 核心门禁不受影响
开发与部署
node tools/build-client.mjs # 客户端 bundle(esbuild,react→host-shim 外部化)
npm install # 开发依赖(esbuild)
node tools/deploy.mjs # 构建客户端 + 拷贝源码 + 目标内 npm install --omit=dev
# 生效:POST /api/extensions/reload;测试:node --test server/*.test.mjs依赖说明:
service-tunnels0.3.39+ 已把 justget 打包进自身 dist(零运行时依赖), 扩展经标准 npm 依赖引入;pi-web 扩展加载器按扩展目录解析裸说明符(lib/ext/loader.tsjiti id = 扩展入口),扩展可带标准 node_modules。
双端单源与设计全文
展示语义(modeOf/identityView/ddnsBadge/netcheckEventView/portmapProbeView)与 doctor 事件契约键(ui.doctor.ev.*)由库 src/ui-shared.ts 单源承载——库自带 st ui 面板与 pi-web 弹窗行为必然一致(漂移由测试钉死)。
真机验收覆盖 UPnP portmap / ipv6 失败路径 / DDNS 双端互查闭环。