mtt 是一个面向 多租户、多设备、多映射 场景的单 WebSocket 反向穿透系统。通过单一二进制程序,以子命令切换 server / agent / enroll 等角色,实现内网服务的安全公网暴露。
off / auto / force 三种模式embed.FS 编译进单一二进制,运行时注入角色配置切换管理端/Agent端界面┌─────────────┐ │ 公网客户端 │ └──────┬──────┘ │ 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│ └─────────────┘ └─────────────┘ └─────────────┘
核心数据流:
# 克隆仓库
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) |
commit | Git commit SHA |
buildTime | ISO 8601 构建时间戳 |
示例:
go build -ldflags "-X cnb.cool/svn/mtt/internal/buildinfo.version=v0.1.0" ./cmd/mtt
以下命令快速演示完整的穿透流程:
# 使用默认配置启动(监听 :18080)
./bin/mtt server run
# 指定自定义参数
./bin/mtt server run --listen :8080 --base-domain tunnel.example.com
# 在 Agent 机器上执行注册(需提供 enroll token)
./bin/mtt enroll \
--server http://localhost:18080 \
--token your-enroll-token \
--device-name my-laptop
成功后,enroll 命令会将设备凭证写入状态文件,并输出 device_id、tenant_id、credential_version 与 state_file。device_secret 仅会在服务端的 enroll API 响应中出现一次,不会由 CLI 明文回显。
# 启动 Agent 连接到服务端
./bin/mtt agent run \
--server ws://localhost:18080/tunnel
# Agent 同时在 127.0.0.1:18990 提供本地管理 API 和 Web UI
# 通过 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
}
}'
# 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 服务端,负责公网流量接入、隧道管理和 API 服务。
mtt server run [flags]
| Flag | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
--listen | MTT_SERVER_LISTEN | :18080 | HTTP 服务监听地址 |
--base-domain | MTT_SERVER_BASE_DOMAIN | tunnel.local | 默认公网域名后缀 |
--public-tcp-addr | MTT_SERVER_PUBLIC_TCP_ADDR | :19000 | TCP 映射监听地址段 |
--state-dir | MTT_SERVER_STATE_DIR | data/server | 运行时状态目录 |
--db | MTT_SERVER_DATABASE_PATH | data/server/server.db | SQLite 数据库路径 |
--tenant-id | MTT_SERVER_TENANT_ID | tenant-dev | 默认租户 ID |
--enroll-token | MTT_SERVER_ENROLL_TOKEN | 空 | 设备注册令牌;不配置则 enroll 不可用 |
--log-level | MTT_SERVER_LOG_LEVEL | info | 日志级别 |
启动内网 Agent,建立隧道并接受服务端指令。
mtt agent run [flags]
| Flag | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
--server | MTT_AGENT_SERVER_URL | ws://127.0.0.1:18080/tunnel | 服务端 WebSocket 地址 |
--local-api | MTT_AGENT_LOCAL_API | 127.0.0.1:18990 | 本地 API 监听地址 |
--state-dir | MTT_AGENT_STATE_DIR | data/agent | 本地状态目录 |
--state-file | MTT_AGENT_STATE_PATH | data/agent/agent.yaml | 本地状态文件路径 |
--device-name | MTT_AGENT_DEVICE_NAME | mtt-agent | 设备名称 |
--log-level | MTT_AGENT_LOG_LEVEL | info | 日志级别 |
执行设备绑定,获取访问凭证。
mtt enroll [flags]
| Flag | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
--server | MTT_ENROLL_SERVER_URL | http://127.0.0.1:18080 | 服务端地址,推荐使用 http://;也兼容 ws:// / wss:// 根地址 |
--token | MTT_ENROLL_TOKEN | 空 | 注册令牌 |
--device-name | MTT_ENROLL_DEVICE_NAME | mtt-agent | 设备名称 |
--state-file | MTT_ENROLL_STATE_PATH | data/agent/agent.yaml | 凭证保存路径 |
输出当前环境诊断信息。
mtt doctor [--json]
输出版本、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 | :18080 | HTTP/API/WebSocket 统一入口 |
| Base Domain | tunnel.local | 默认公网域名后缀 |
| Public TCP Addr | :19000 | TCP 映射动态端口范围 |
| Database | data/server/server.db | SQLite 持久化存储 |
| Tenant ID | tenant-dev | 默认 enroll 租户 |
| Enroll Token | 空 | 需显式配置后 enroll 才可用 |
Agent 配置同样遵循 CLI Flags > 环境变量 > 内置默认值(通过内嵌 YAML 内容装载) 优先级,环境变量前缀为 MTT_AGENT_。
状态存储策略:
agent.yaml),通过 darkit-sysconf 加载--state-file / MTT_AGENT_STATE_PATH 指向 .db / .sqlite / .sqlite3 文件时,自动切换至 SQLite 存储设备首次使用必须完成注册流程:
{deviceName}|{hostname}|{GOOS}|{GOARCH} 计算 SHA256,取前 16 位 hex 作为 deviceID(格式 dev-xxxxxxxxxxxxxxxx)/api/v1/device-enroll安全提示:device_secret 仅在 enroll 成功时返回一次,请妥善保管。如丢失需重新执行 enroll 流程。
服务端在 /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
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 映射展示名称 |
protocol | string | 是 | 协议类型:http 或 tcp |
local_scheme | string | 是 | 本地协议:常见为 http / https / tcp |
local_host | string | 是 | 内网目标主机 |
local_port | integer | 是 | 内网目标端口 |
public_mode | string | 是 | 公网暴露模式:如 custom_domain / custom_port |
compression_mode | string | 否 | 压缩模式:off / auto / force(默认 HTTP 为 off,TCP 为 auto) |
compression_algo | string | 否 | 当前唯一有效值为 snappy,省略时会自动归一为 snappy |
traffic_hint | string | 是 | 流量类型提示:如 interactive / binary |
custom_domain | string | 条件* | 自定义公网域名(通常配合 public_mode=custom_domain) |
custom_port | integer | 条件* | 自定义公网端口(通常配合 public_mode=custom_port) |
enabled | boolean | 否 | 是否启用(默认 true) |
*注:
custom_domain与custom_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 | /tunnel | WebSocket 隧道升级端点 |
| GET | /* | Web UI 静态资源(带运行时配置注入) |
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_id、idempotency_key、op 为必填字段operation,但不支持 actionMTT 自定义了一套轻量级二进制协议,分为控制帧协议和流前导协议两层。协议实现位于 internal/protocol/ 包中。
控制帧用于隧道控制面通信(握手、心跳、配置同步等),定义于 internal/protocol/control.go。
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)
| 类型值 | 名称 | 说明 |
|---|---|---|
0x0001 | ClientHello | Agent 握手请求(含压缩能力宣告) |
0x0002 | ServerHelloAck | 服务端握手确认(含压缩协商结果) |
0x0010 | ConfigSnapshot | 全量配置快照下发 |
0x0011 | ConfigDeltaApply | 增量配置更新 |
0x0012 | ConfigApplyAck | 配置应用确认 |
0x0020 | DeviceStatusReport | 设备状态上报 |
0x0021 | RouteBindResult | 路由绑定结果通知 |
0x0030 | Heartbeat | 心跳/PING-PONG |
0x0040 | MutationRequest | 变更请求(服务端→Agent 或 Agent→服务端) |
0x0041 | MutationResult | 变更处理结果 |
0x00FF | Error | 错误响应 |
每个 yamux data stream 开头附带 16 字节前导头,用于标识流的用途和编解码方式。定义于 internal/protocol/stream.go。
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)
| 值 | 名称 | 说明 |
|---|---|---|
0x01 | Control | 控制流(承载上述 Control Frame) |
0x02 | HTTP | HTTP 反向代理数据流 |
0x03 | TCP | TCP 端口转发数据流 |
压缩协商算法实现于 internal/protocol/compression.go 和 internal/tunnel/compression.go。
支持的压缩算法:
| 算法 | 标识 | 说明 |
|---|---|---|
| none | 0x00 | 无压缩 |
| snappy | 0x01 | Snappy 压缩 |
压缩模式:
| 模式 | 行为 |
|---|---|
off | 不启用压缩 |
auto | 根据双方能力集自动协商,优先使用 snappy |
force | 强制使用 snappy,对方不支持则失败 |
协商流程:
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.yml | CNB 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 源目录 | 代码输出目录 |
|---|---|---|
| sqlite | internal/store/sqlite/sqlc/ | internal/store/sqlite/sqlcgen/ |
| agent_sqlite | internal/agent/sqlc/ | internal/agent/sqlcgen/ |
项目提供了进程级冒烟测试脚本,验证完整的穿透链路:
# 运行完整 E2E 测试(构建 → 启动 server → enroll → 启动 agent → 验证)
python3 scripts/real_e2e_smoke.py
测试覆盖:
仓库根目录提供了 CNB Pipeline 配置 .cnb.yml,会在 push / pull_request 时自动执行以下门禁:
./scripts/ci_gate.sh
该门禁会依次执行:
sqlc generate -f sqlc.yamlinternal/*/sqlcgen/ 未因 generate 产生漂移sqlc vet -f sqlc.yamlgo test ./... -count=1若魔尊在本地预检,建议在提交前先手动跑一遍相同脚本,以便更早发现 sqlcgen 漂移或回归测试失败。
| 依赖 | 版本 | 用途 |
|---|---|---|
| gorilla/websocket | v1.5.3 | WebSocket 客户端与服务端 |
| hashicorp/yamux | v0.1.2 | 基于 stream 的多路复用协议 |
| golang/snappy | v1.0.0 | Snappy 压缩算法 |
| darkit/sysconf | v1.0.7 | 统一配置加载(YAML + env) |
| darkit/zcli | v0.2.1 | CLI 应用框架 |
| spf13/cobra | v1.10.2 | CLI 命令框架 |
| google/uuid | v1.6.0 | UUID 生成 |
| modernc.org/sqlite | v1.48.1 | 纯 Go SQLite 驱动(零 CGO) |
当前仓库中尚未附带 LICENSE 文件;若需对外分发或开源发布,请先补充明确的许可声明。