logo
Public
0
0
WeChat Login
feat: 添加登录鉴权系统

MTT — Multi-Tenant Tunnel

Go Reference

mtt 是一个面向 多租户、多设备、多映射 场景的单 WebSocket 反向穿透系统。通过单一二进制程序,以子命令切换 server / agent / enroll 等角色,实现内网服务的安全公网暴露。

目录


特性

  • 单 WebSocket 多路复用:一条 WebSocket 连接承载控制面信令与数据面流量转发,基于 yamux 实现多流复用
  • 多租户隔离:租户级设备管理与映射隔离,支持独立的域名/端口暴露策略
  • HMAC-SHA256 握手认证:每次隧道建连均携带签名认证,配合 Nonce 防重放与时间戳校验
  • HTTP/TCP 双协议穿透:支持基于 Host 的 HTTP 反向代理和基于端口级的 TCP 端口转发
  • Snappy 压缩传输:可选的 Snappy 压缩,支持 off / auto / force 三种模式
  • 断线自动重连:Agent 端指数退避重连策略(1s ~ 15s)
  • 内嵌 Web UI:前端资源通过 embed.FS 编译进单一二进制,运行时注入角色配置切换管理端/Agent端界面
  • 零外部依赖运行:使用纯 Go SQLite 驱动 (modernc.org/sqlite),无 CGO 依赖
  • 乐观锁并发控制:映射更新/删除基于版本号的乐观锁机制
  • 多种状态存储:Agent 本地状态支持 YAML(默认)/ SQLite / 内存三种存储后端

架构概览

┌─────────────┐ │ 公网客户端 │ └──────┬──────┘ │ HTTP/TCP ┌────────────▼────────────┐ │ MTT Server │ │ ┌──────────────────┐ │ │ │ Route Cache │ │ host/port 双索引路由表 │ ├──────────────────┤ │ │ │ Session Registry│ │ 在线设备会话管理 │ ├──────────────────┤ │ │ │ Tunnel Auth │ │ HMAC 握手鉴权 │ └────────┬─────────┘ │ │ │ WebSocket │ │ yamux multiplexing │ └───────────┼──────────────┘ │ ┌───────────────┼───────────────┐ │ │ │ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ Agent A │ │ Agent B │ │ Agent C │ │ ┌──────────┐│ │ ┌──────────┐│ │ ┌──────────┐│ │ │Local API ││ │ │Local API ││ │ │Local API ││ │ └──────────┘│ │ └──────────┘│ │ └──────────┘│ │ ┌──────────┐│ │ ┌──────────┐│ │ ┌──────────┐│ │ │YAML Store││ │ │YAML Store││ │ │YAML Store││ │ └──────────┘│ │ └──────────┘│ │ └──────────┘│ │ 内网服务:8080│ │ 内网服务:3000│ │ 内网服务:9090│ └─────────────┘ └─────────────┘ └─────────────┘

核心数据流

  1. 控制流(Control Stream):心跳保活、配置快照下发(ConfigSnapshot)、变更请求(MutationRequest)、状态上报
  2. HTTP 数据流:公网 HTTP 请求 → 路由匹配 → yamux Stream → Agent → 本地服务 → 响应原路返回
  3. TCP 数据流:公网 TCP 连接 → 端口匹配 → yamux Stream → Agent → 本地服务 → 双向透传

快速开始

前提条件

  • Go >= 1.26
  • Node.js >= 18(构建 Web UI 时需要)

安装

# 克隆仓库 git clone <repository-url> cd mtt # 构建 Web UI cd web && npm ci && npm run build cd .. # 构建单二进制 go build -o bin/mtt ./cmd/mtt # 或安装到 $GOBIN / $GOPATH/bin go install ./cmd/mtt

构建

当前仓库未提供 Makefile,建议使用以下命令完成构建:

# 构建前端资源 cd web && npm ci && npm run build # 构建后端二进制 cd .. go build -o bin/mtt ./cmd/mtt # 运行真实进程级 smoke python3 scripts/real_e2e_smoke.py

提交前若需执行最小 CI Gate,可直接运行:

./scripts/ci_gate.sh

构建注入的版本信息(通过 -ldflags):

变量说明
version版本号(默认 0.1.0-dev
commitGit commit SHA
buildTimeISO 8601 构建时间戳

示例:

go build -ldflags "-X cnb.cool/svn/mtt/internal/buildinfo.version=v0.1.0" ./cmd/mtt

三分钟上手

以下命令快速演示完整的穿透流程:

1. 启动服务端

# 使用默认配置启动(监听 :18080) ./bin/mtt server run # 指定自定义参数 ./bin/mtt server run --listen :8080 --base-domain tunnel.example.com

2. 注册设备

# 在 Agent 机器上执行注册(需提供 enroll token) ./bin/mtt enroll \ --server http://localhost:18080 \ --token your-enroll-token \ --device-name my-laptop

成功后,enroll 命令会将设备凭证写入状态文件,并输出 device_idtenant_idcredential_versionstate_filedevice_secret 仅会在服务端的 enroll API 响应中出现一次,不会由 CLI 明文回显。

3. 启动 Agent

# 启动 Agent 连接到服务端 ./bin/mtt agent run \ --server ws://localhost:18080/tunnel # Agent 同时在 127.0.0.1:18990 提供本地管理 API 和 Web UI

4. 创建映射

# 通过 Admin API 创建 HTTP 映射 curl -X POST http://localhost:18080/api/v1/devices/dev-xxxxxxxxxxxxxxxx/mappings \ -H "Content-Type: application/json" \ -d '{ "name": "app-http", "protocol": "http", "local_scheme": "http", "local_host": "127.0.0.1", "local_port": 8080, "public_mode": "custom_domain", "compression_mode": "off", "compression_algo": "snappy", "traffic_hint": "interactive", "custom_domain": "app.tunnel.example.com", "enabled": true }' # 通过 Local API 创建 TCP 映射 curl -X POST http://localhost:18990/local/api/v1/mutations \ -H "Content-Type: application/json" \ -d '{ "request_id": "req-local-1", "idempotency_key": "idem-local-1", "op": "create_mapping", "payload": { "name": "ssh-tcp", "protocol": "tcp", "local_scheme": "tcp", "local_host": "127.0.0.1", "local_port": 22, "public_mode": "custom_port", "compression_mode": "auto", "compression_algo": "snappy", "traffic_hint": "binary", "custom_port": 2222, "enabled": true } }'

5. 验证穿透

# HTTP 穿透:通过公网域名访问内网服务 curl -H "Host: app.tunnel.example.com" http://localhost:18080/ # TCP 穿透:通过公网端口连接内网 SSH ssh -p 2222 user@localhost

使用指南

命令行参考

MTT 采用 cobra 构建 CLI,支持以下子命令:

mtt [command] Commands: server run 启动公网服务端 agent run 启动内网 Agent enroll 执行设备首次绑定或重新绑定 doctor 输出诊断信息(支持 --json 格式化输出) version 输出详细版本信息 Flags: -h, --help 帮助信息

mtt server run

启动 MTT 服务端,负责公网流量接入、隧道管理和 API 服务。

mtt server run [flags]
Flag环境变量默认值说明
--listenMTT_SERVER_LISTEN:18080HTTP 服务监听地址
--base-domainMTT_SERVER_BASE_DOMAINtunnel.local默认公网域名后缀
--public-tcp-addrMTT_SERVER_PUBLIC_TCP_ADDR:19000TCP 映射监听地址段
--state-dirMTT_SERVER_STATE_DIRdata/server运行时状态目录
--dbMTT_SERVER_DATABASE_PATHdata/server/server.dbSQLite 数据库路径
--tenant-idMTT_SERVER_TENANT_IDtenant-dev默认租户 ID
--enroll-tokenMTT_SERVER_ENROLL_TOKEN设备注册令牌;不配置则 enroll 不可用
--log-levelMTT_SERVER_LOG_LEVELinfo日志级别

mtt agent run

启动内网 Agent,建立隧道并接受服务端指令。

mtt agent run [flags]
Flag环境变量默认值说明
--serverMTT_AGENT_SERVER_URLws://127.0.0.1:18080/tunnel服务端 WebSocket 地址
--local-apiMTT_AGENT_LOCAL_API127.0.0.1:18990本地 API 监听地址
--state-dirMTT_AGENT_STATE_DIRdata/agent本地状态目录
--state-fileMTT_AGENT_STATE_PATHdata/agent/agent.yaml本地状态文件路径
--device-nameMTT_AGENT_DEVICE_NAMEmtt-agent设备名称
--log-levelMTT_AGENT_LOG_LEVELinfo日志级别

mtt enroll

执行设备绑定,获取访问凭证。

mtt enroll [flags]
Flag环境变量默认值说明
--serverMTT_ENROLL_SERVER_URLhttp://127.0.0.1:18080服务端地址,推荐使用 http://;也兼容 ws:// / wss:// 根地址
--tokenMTT_ENROLL_TOKEN注册令牌
--device-nameMTT_ENROLL_DEVICE_NAMEmtt-agent设备名称
--state-fileMTT_ENROLL_STATE_PATHdata/agent/agent.yaml凭证保存路径

mtt doctor

输出当前环境诊断信息。

mtt doctor [--json]

mtt version

输出版本、commit、构建时间等详细信息。

mtt version # 示例输出: # {Version:v0.1.0 Commit:a1b2c3d BuildTime:2026-01-15T10:30:00Z}

服务端配置

服务端配置加载遵循以下优先级:

CLI Flags > 环境变量 > 内置默认值(通过内嵌 YAML 内容装载)

环境变量统一使用 MTT_SERVER_ 前缀,配置结构定义于 internal/config/config.go

关键默认值

参数默认值说明
Listen:18080HTTP/API/WebSocket 统一入口
Base Domaintunnel.local默认公网域名后缀
Public TCP Addr:19000TCP 映射动态端口范围
Databasedata/server/server.dbSQLite 持久化存储
Tenant IDtenant-dev默认 enroll 租户
Enroll Token需显式配置后 enroll 才可用

Agent 配置

Agent 配置同样遵循 CLI Flags > 环境变量 > 内置默认值(通过内嵌 YAML 内容装载) 优先级,环境变量前缀为 MTT_AGENT_

状态存储策略

  • 默认:YAML 文件 (agent.yaml),通过 darkit-sysconf 加载
  • SQLite 兼容:当 --state-file / MTT_AGENT_STATE_PATH 指向 .db / .sqlite / .sqlite3 文件时,自动切换至 SQLite 存储
  • 内存模式:用于测试场景

设备注册(Enroll)

设备首次使用必须完成注册流程:

  1. 派生设备标识:基于 {deviceName}|{hostname}|{GOOS}|{GOARCH} 计算 SHA256,取前 16 位 hex 作为 deviceID(格式 dev-xxxxxxxxxxxxxxxx
  2. 提交注册请求:携带 enroll token、device ID、平台指纹等信息 POST 到 /api/v1/device-enroll
  3. 签发凭证:服务端验证 token 后生成 32 字节随机 secret,SHA256 摘要入库,明文 secret 仅在响应中返回一次
  4. 本地持久化:Agent 将 DeviceMeta(含明文 secret)保存至本地 YAML/SQLite

安全提示:device_secret 仅在 enroll 成功时返回一次,请妥善保管。如丢失需重新执行 enroll 流程。


API 文档

管理 API

服务端在 /api/v1 路径下提供 RESTful 管理 API,实现定义于 internal/adminapi/handler.go

所有响应均采用标准 JSON envelope 格式:

{ "code": "OK", "message": "success", "request_id": "req-1775484378463912526", "data": { ... }, "meta": { ... } }

设备管理

获取设备详情
GET /api/v1/devices/{deviceID}

响应示例

{ "code": "OK", "message": "success", "request_id": "req-1775484378463912526", "data": { "device_id": "dev-a1b2c3d4e5f6g7h8", "tenant_id": "tenant-dev", "name": "my-laptop", "bind_mode": "host", "agent_version": "0.1.0-dev", "desired_revision": 3, "applied_revision": 3, "status": "online", "last_seen_at": "2026-04-06T14:05:57Z" } }
列出设备映射
GET /api/v1/devices/{deviceID}/mappings
创建映射
POST /api/v1/devices/{deviceID}/mappings Content-Type: application/json

请求体

{ "name": "app-http", "protocol": "http", "local_scheme": "http", "local_host": "127.0.0.1", "local_port": 8080, "compression_mode": "off", "compression_algo": "snappy", "traffic_hint": "interactive", "public_mode": "custom_domain", "custom_domain": "app.example.com", "enabled": true }
字段类型必填说明
namestring映射展示名称
protocolstring协议类型:httptcp
local_schemestring本地协议:常见为 http / https / tcp
local_hoststring内网目标主机
local_portinteger内网目标端口
public_modestring公网暴露模式:如 custom_domain / custom_port
compression_modestring压缩模式:off / auto / force(默认 HTTP 为 off,TCP 为 auto
compression_algostring当前唯一有效值为 snappy,省略时会自动归一为 snappy
traffic_hintstring流量类型提示:如 interactive / binary
custom_domainstring条件*自定义公网域名(通常配合 public_mode=custom_domain
custom_portinteger条件*自定义公网端口(通常配合 public_mode=custom_port
enabledboolean是否启用(默认 true

*注:custom_domaincustom_port 的必填条件由 public_mode 决定。

映射管理

更新映射
PATCH /api/v1/mappings/{mappingID} Content-Type: application/json

请求体(支持部分更新):

{ "local_port": 9090, "compression_mode": "auto", "version": 3 }

乐观锁:更新操作必须携带当前 version 号,服务端校验版本一致性后才执行修改。

删除映射
DELETE /api/v1/mappings/{mappingID} Content-Type: application/json

请求体

{ "version": 5 }

同样需要在请求体中携带 version 进行乐观锁校验。

其他端点

方法路径说明
GET/api/v1/healthz健康检查
POST/api/v1/device-enroll设备注册
GET/tunnelWebSocket 隧道升级端点
GET/*Web UI 静态资源(带运行时配置注入)

本地 Agent API

Agent 在本地 127.0.0.1:18990 提供管理接口,实现定义于 internal/localapi/handler.go

读取本地状态

GET /local/api/v1/state

返回 Agent 本地完整状态,包括设备元信息、映射镜像、待同步变更队列等。

提交变更请求

POST /local/api/v1/mutations Content-Type: application/json

请求体

{ "request_id": "req-local-1", "idempotency_key": "unique-key-123", "op": "create_mapping", "payload": { "name": "ssh-tcp", "protocol": "tcp", "local_scheme": "tcp", "local_host": "127.0.0.1", "local_port": 22, "public_mode": "custom_port", "compression_mode": "auto", "compression_algo": "snappy", "traffic_hint": "binary", "custom_port": 2222, "enabled": true } }

说明:

  • request_ididempotency_keyop 为必填字段
  • 兼容别名 operation,但不支持 action
  • 变更请求入队后,Agent 会通过 Control Stream 定期(每 500ms)flush 至服务端

协议说明

MTT 自定义了一套轻量级二进制协议,分为控制帧协议流前导协议两层。协议实现位于 internal/protocol/ 包中。

控制帧协议(Control Frame)

控制帧用于隧道控制面通信(握手、心跳、配置同步等),定义于 internal/protocol/control.go

帧头格式(24 字节固定长度)

Offset Size Field Description ─────── ──── ───────── ───────────────────────── 0 2 Type 消息类型(uint16 BE) 2 2 Flags 标志位(uint16 BE) 4 8 Seq 序列号(uint64 BE) 12 8 Ack 确认号(uint64 BE) 20 4 PayloadLen 负载长度(uint32 BE)

消息类型

类型值名称说明
0x0001ClientHelloAgent 握手请求(含压缩能力宣告)
0x0002ServerHelloAck服务端握手确认(含压缩协商结果)
0x0010ConfigSnapshot全量配置快照下发
0x0011ConfigDeltaApply增量配置更新
0x0012ConfigApplyAck配置应用确认
0x0020DeviceStatusReport设备状态上报
0x0021RouteBindResult路由绑定结果通知
0x0030Heartbeat心跳/PING-PONG
0x0040MutationRequest变更请求(服务端→Agent 或 Agent→服务端)
0x0041MutationResult变更处理结果
0x00FFError错误响应

流前导协议(Stream Preamble)

每个 yamux data stream 开头附带 16 字节前导头,用于标识流的用途和编解码方式。定义于 internal/protocol/stream.go

前导头格式(16 字节)

Offset Size Field Description ─────── ──── ─────────── ───────────────────────── 0 4 Magic 魔数 "MTT1"(ASCII) 4 1 Version 协议版本(当前 = 1) 5 1 Kind 流类型 6 2 Flags 标志位(uint16 BE) 8 1 Codec 编码方式(保留) 9 1 Compression 压缩算法 10 2 Reserved 保留字段 12 4 MetaLen 元数据长度(uint32 BE)

流类型(Kind)

名称说明
0x01Control控制流(承载上述 Control Frame)
0x02HTTPHTTP 反向代理数据流
0x03TCPTCP 端口转发数据流

压缩协商

压缩协商算法实现于 internal/protocol/compression.gointernal/tunnel/compression.go

支持的压缩算法

算法标识说明
none0x00无压缩
snappy0x01Snappy 压缩

压缩模式

模式行为
off不启用压缩
auto根据双方能力集自动协商,优先使用 snappy
force强制使用 snappy,对方不支持则失败

协商流程

  1. ClientHello 中声明本地支持的压缩算法列表
  2. ServerHelloAck 中根据交集选择最终算法
  3. 选择优先级:force > auto > off,双方 mode 取较强制者

项目结构

mtt/ ├── cmd/ │ └── mtt/ │ └── main.go # 程序入口 ├── internal/ │ ├── adminapi/ │ │ └── handler.go # 管理 REST API handler │ ├── agent/ │ │ ├── types.go # Agent 本地状态类型定义 │ │ ├── store.go # StateStore 接口与内存实现 │ │ ├── yaml_store.go # YAML 持久化(默认) │ │ ├── sqlite_store.go # SQLite 持久化(legacy) │ │ ├── state_store_open.go # 存储后端工厂函数 │ │ ├── enroll.go # 注册请求构建 │ │ ├── snapshot.go # ConfigSnapshot/MutationResult 应用逻辑 │ │ ├── enroll_client.go # Enroll HTTP 客户端 │ │ ├── sqlc/ # Agent SQL 源文件(sqlc 输入) │ │ └── sqlcgen/ # sqlc 生成代码 │ ├── buildinfo/ │ │ └── buildinfo.go # ldflags 版本信息注入 │ ├── cli/ │ │ ├── app.go # CLI 应用骨架(cobra) │ │ ├── server_run.go # server run 命令 │ │ ├── server_tunnel_runtime.go # 服务端隧道运行时 │ │ ├── agent_run.go # agent run 命令 │ │ ├── agent_tunnel_runtime.go # Agent 隧道运行时 │ │ ├── enroll_run.go # enroll 命令 │ │ └── ui_runtime.go # Web UI 运行时配置注入 │ ├── config/ │ │ └── config.go # 配置结构与加载 │ ├── device/ │ │ ├── types.go # 注册相关类型定义 │ │ └── service.go # 注册业务逻辑 │ ├── localapi/ │ │ └── handler.go # Agent 本地 API handler │ ├── protocol/ │ │ ├── control.go # 控制帧协议定义 │ │ ├── stream.go # 流前导协议定义 │ │ └── compression.go # 压缩协商算法 │ ├── routing/ │ │ └── cache.go # 双索引路由缓存 │ ├── server/ │ │ ├── runtime.go # 服务端运行时容器 │ │ ├── session_registry.go # 在线会话注册表 │ │ └── tunnel_auth.go # 隧道鉴权器 │ ├── sqlutil/ │ │ └── split.go # SQL 语句分割工具 │ ├── store/sqlite/ │ │ ├── store.go # SQLite 持久化层 │ │ ├── device_repository.go # 设备仓储实现 │ │ ├── mapping_repository.go # 映射仓储实现 │ │ ├── route_repository.go # 路由仓储实现 │ │ ├── schema_embed.go # schema 嵌入 │ │ ├── sqlc/ # 服务端 SQL 源文件 │ │ └── sqlcgen/ # sqlc 生成代码 │ └── tunnel/ │ ├── transport.go # WebSocket→net.Conn 适配器 │ ├── control.go # 控制通道收发 │ ├── handshake.go # HMAC-SHA256 握手认证 │ ├── http_stream.go # HTTP 流前导封装 │ ├── tcp_stream.go # TCP 流前导封装 │ ├── heartbeat.go # yamux 心跳保活 │ ├── reconnect.go # 断线重连策略 │ └── compression.go # Snappy 压缩包装器 ├── web/ │ ├── embed.go # go:embed 前端资源 │ ├── package.json │ ├── package-lock.json │ ├── tsconfig.json │ ├── vite.config.ts │ └── src/ │ ├── App.tsx # 应用入口(按 runtime 切换页面) │ ├── runtime.ts # 运行时配置读取 │ └── pages/ │ ├── AdminPage.tsx # 管理端界面 │ └── AgentPage.tsx # Agent 端界面 ├── docs/plans/ # 技术方案文档 │ ├── multi-tenant-websocket-tunnel_blueprint.md │ ├── multi-tenant-websocket-tunnel_api-contract-lite.md │ ├── multi-tenant-websocket-tunnel_openapi-lite.yaml │ └── multi-tenant-websocket-tunnel_tasks/ ├── scripts/ │ ├── real_e2e_smoke.sh # E2E 冒烟测试入口 │ └── real_e2e_smoke.py # E2E 冒烟测试脚本 ├── sqlc.yaml # sqlc 代码生成配置 ├── go.mod └── go.sum

开发指南

目录约定

路径用途
.cnb.ymlCNB push / pull_request CI Gate 配置
cmd/主程序入口
internal/私有包(不被外部导入)
internal/*/sqlc/SQL 源文件(sqlc 输入)
internal/*/sqlcgen/sqlc 生成的 Go 代码(不要手动编辑
scripts/ci_gate.sh本地与 CI 共用的最小质量门禁脚本
web/src/React 前端源码
web/dist/app/前端构建产物(被 Go embed 嵌入)
data/server/服务端运行时数据(数据库等)
data/agent/Agent 运行时数据(状态文件等)
docs/plans/技术方案与 API 契约文档

代码生成

项目使用 sqlc 从 SQL schema 生成类型安全的 Go 数据访问代码:

# 同时生成服务端与 Agent 侧代码 sqlc generate -f sqlc.yaml # 静态校验 query 与 schema 契约 sqlc vet -f sqlc.yaml

sqlc 配置规则sqlc.yaml):

规则名SQL 源目录代码输出目录
sqliteinternal/store/sqlite/sqlc/internal/store/sqlite/sqlcgen/
agent_sqliteinternal/agent/sqlc/internal/agent/sqlcgen/

E2E 测试

项目提供了进程级冒烟测试脚本,验证完整的穿透链路:

# 运行完整 E2E 测试(构建 → 启动 server → enroll → 启动 agent → 验证) python3 scripts/real_e2e_smoke.py

测试覆盖:

  • Admin API:创建 / 更新 / 删除 HTTP 映射
  • Local API:提交 mutation、创建 TCP 映射
  • Admin / Agent 首页运行时注入验证
  • HTTP Host 穿透验证
  • TCP 端口穿透验证

CI Gate

仓库根目录提供了 CNB Pipeline 配置 .cnb.yml,会在 push / pull_request 时自动执行以下门禁:

./scripts/ci_gate.sh

该门禁会依次执行:

  1. sqlc generate -f sqlc.yaml
  2. 校验 internal/*/sqlcgen/ 未因 generate 产生漂移
  3. sqlc vet -f sqlc.yaml
  4. go test ./... -count=1

若魔尊在本地预检,建议在提交前先手动跑一遍相同脚本,以便更早发现 sqlcgen 漂移或回归测试失败。


关键依赖

依赖版本用途
gorilla/websocketv1.5.3WebSocket 客户端与服务端
hashicorp/yamuxv0.1.2基于 stream 的多路复用协议
golang/snappyv1.0.0Snappy 压缩算法
darkit/sysconfv1.0.7统一配置加载(YAML + env)
darkit/zcliv0.2.1CLI 应用框架
spf13/cobrav1.10.2CLI 命令框架
google/uuidv1.6.0UUID 生成
modernc.org/sqlitev1.48.1纯 Go SQLite 驱动(零 CGO)

许可证

当前仓库中尚未附带 LICENSE 文件;若需对外分发或开源发布,请先补充明确的许可声明。