rebuild: split core into separate repo; move wg-go integration to internal/ctr/corebind
- go.mod: require git.zkcoi.com/zkcoi/meshray/core, replace => ../Meshray - internal/ctr/corebind: EnhancedBind + registry + adapter (wg-go integration isolated) - cmd/mr-wg migrated from old core/cmd/meshray-core - core/ removed; CHANGELOG updated; fix checkdb vet warning
This commit is contained in:
+375
-127
@@ -1,163 +1,411 @@
|
|||||||
# 更新日志
|
# 变更日志 (CHANGELOG)
|
||||||
|
|
||||||
## [2.0.2] - 2026-03-20
|
## 2026-07-15 仓库拆分:core 独立为 `meshray/core`,wg-go 集成迁移至 `internal/ctr/corebind`
|
||||||
|
|
||||||
### ✨ 新增功能
|
### 背景(架构决策)
|
||||||
|
core(转发引擎)本质是**去中心化 frp**——9 层传输只转发字节 A↔B,不关心载荷是应用流量
|
||||||
|
(形态 A:独立 frp 隧道)还是 WG 密文(形态 B:增强传输)。此前 core 直接 `import wg-go`
|
||||||
|
并内置 `EnhancedBind`,导致「集成干扰核心」:核心层被 wg-go / contract 绑架,难以独立演进,
|
||||||
|
也无法作为纯粹的去中心化 frp 基地。
|
||||||
|
|
||||||
#### DDNS 完整功能(P0 优先级)
|
故按用户决策:
|
||||||
- ✅ DNS Provider 抽象层,支持多云服务商
|
- 项目命名统一为 **meshray**;wg-go 集成属于 manager / UI / GUI 范畴,不污染 core。
|
||||||
- Cloudflare Provider(真实 API 集成)
|
- 把原 `core/` 目录拆为**两个隔离仓库**:
|
||||||
- 腾讯云 DNSPod Provider(真实 API 集成)
|
- `Meshray`(模块 `git.zkcoi.com/zkcoi/meshray/core`):零 wireguard、零 contract 依赖的纯转发底座。
|
||||||
- 阿里云 Provider(占位实现)
|
- `Meshray-Manager`(模块 `git.zkcoi.com/zkcoi/meshray`):wg-go / contract / UI / 管控面。
|
||||||
- ✅ IP 自动检测服务(公网/本地 IPv4/IPv6)
|
|
||||||
- ✅ 后台任务调度器(每 5 分钟自动检测 IP 变化)
|
|
||||||
- ✅ Dashboard DDNS 监控卡片
|
|
||||||
- ✅ 前端 IP 自动检测按钮
|
|
||||||
- ✅ 防抖动设计 + 事务处理
|
|
||||||
|
|
||||||
#### P1 管理功能
|
### 改动(核心仓库 `Meshray`)
|
||||||
- ✅ 修改密码功能(bcrypt 加密)
|
- 模块路径由 `git.zkcoi.com/zkcoi/meshray`(与 manager 冲突)改为
|
||||||
- ✅ 重启核心服务功能
|
`git.zkcoi.com/zkcoi/meshray/core`;`core/` 目录重命名为 `engine/`,全仓
|
||||||
|
`git.zkcoi.com/zkcoi/meshray/` → `git.zkcoi.com/zkcoi/meshray/core/` 替换。
|
||||||
|
- `engine/engine.go`:
|
||||||
|
- `NewEngine(logger, metrics, plugin ...transport.ProtocolPlugin)`——插件改为**可选变参**,
|
||||||
|
形态 A 可直接空插件启动。
|
||||||
|
- 移除 `bind *EnhancedBind` 字段与 `wg.NewWGPlugin()` 导入。
|
||||||
|
- 新增**集成钩子**(core 不依赖 wg,由外部注入):
|
||||||
|
- `PeerRegistrar` 接口 + `SetPeerRegistrar`:外部 bind 注册对端地址供 NAT 学址。
|
||||||
|
- `SetRelayInboundHandler(func(peerKey string, packet []byte))`:中继入站回调交给外部。
|
||||||
|
- `SetDirectByBind(bool)` / `SetListenPort(int)`:声明由 bind 接管直连与端口。
|
||||||
|
- `NotifyPeerInfo` 改调 `e.peerRegistrar.RegisterPeer(addr, peerKey)` 而非 `e.bind.RegisterPeer`。
|
||||||
|
- `transport/relay_test.go`:移除对 `plugins/wg` 的依赖,改用本地 `mockPlugin`
|
||||||
|
(实现 `IsControlPacket`/`IsDataPacket` 返回 false)。
|
||||||
|
- `transport/`、`connect/` 仅注释提及 WireGuard,无任何 import 依赖。
|
||||||
|
- `go.mod`:仅依赖 gorilla/websocket、pion/stun、pion/turn/v2、pion/webrtc/v3、
|
||||||
|
quic-go、zap;**移除 wireguard-go / wgctrl / meshray-contract**。
|
||||||
|
|
||||||
#### P2 系统功能
|
### 改动(manager 仓库 `Meshray-Manager`)
|
||||||
|
- `internal/ctr/corebind/`(`package corebind`,**新增**):把 wg-go 集成从原 core 迁出:
|
||||||
|
- `bind.go`:移植 `EnhancedBind`,`NewEnhancedBind` 改为通过 core 钩子接线
|
||||||
|
(`SetPeerRegistrar` / `SetRelayInboundHandler` / `SetDirectByBind(true)` / `SetListenPort`),
|
||||||
|
实现 `conn.Bind`(`Open`/`Send`/`Close`/`ParseEndpoint`/`BatchSize`/`SetMark` 等)。
|
||||||
|
- `registry.go`:移植注册逻辑 + `getOrCreateEngine`(用 `core.NewEngine(logger, core.NewMetrics())`),
|
||||||
|
`init()` 注册 `contract.RegisterEnhancedBindFactory` / `contract.RegisterEngineProvider`。
|
||||||
|
- `adapter.go`:meshray-contract ↔ meshray/core 类型互转
|
||||||
|
(`toCoreCandidates` / `toCoreICEConfig` / `toCoreLayerOrder` / `toContractStatus`)。
|
||||||
|
- `internal/ctr/core_enable.go`:空白导入改为
|
||||||
|
`_ "git.zkcoi.com/zkcoi/meshray/internal/ctr/corebind"`。
|
||||||
|
- `cmd/mr-wg/`:**由原 `core/cmd/meshray-core/` 迁移**,导入修正为
|
||||||
|
`git.zkcoi.com/zkcoi/meshray/core` 与 `internal/ctr/corebind`,使用
|
||||||
|
`corebind.NewEnhancedBind`、候选类型 `meshraycore.Candidate`。
|
||||||
|
- `go.mod`:
|
||||||
|
- 删除旧 `replace git.zkcoi.com/zkcoi/meshray-core => ./core` 与 `require meshray-core`。
|
||||||
|
- 新增 `require git.zkcoi.com/zkcoi/meshray/core v0.0.0` +
|
||||||
|
`replace git.zkcoi.com/zkcoi/meshray/core => ../Meshray`(本地兄弟模块)。
|
||||||
|
- `internal/ctr/wg.go`、`ctr.go`、`interface.go`:仅注释/字符串提及 `meshray-core`,无 import 依赖,保持不变。
|
||||||
|
|
||||||
**备份恢复功能**:
|
### 验证
|
||||||
- ✅ 创建备份 API
|
- `Meshray`:`go build ./...` → 0;`go test ./transport/... ./connect/...` → ok(零 wg / contract 依赖)。
|
||||||
- ✅ 列出备份 API
|
- `Meshray-Manager`:`go build ./...` → 0;`go build ./cmd/mr-wg/... ./internal/ctr/...` → 0。
|
||||||
- ✅ 恢复备份 API
|
- 已知:manager `go mod tidy` 在 Go 1.26 下因「兄弟模块 replace + 本地 replace」触发内部 panic,
|
||||||
- ✅ 删除备份 API
|
已用 `go mod download` + 直接路径 `go build ./path/...` 绕过,未执行 tidy。
|
||||||
- ✅ 下载备份 API
|
|
||||||
|
|
||||||
**WebSocket 实时通知推送系统**:
|
## 2026-07-15 重构:Direct-UDP 改为共享监听 socket + WG 学址,原生支持 NAT 穿透(核心)
|
||||||
- ✅ Notification 数据模型(SQLite 持久化)
|
|
||||||
- ✅ 6 个完整的 RESTful API
|
|
||||||
- ✅ 单播/广播双模式
|
|
||||||
- ✅ 前端通知中心组件(铃铛图标 + 红色角标)
|
|
||||||
- ✅ 下拉通知列表(滚动条 + 空状态)
|
|
||||||
- ✅ 一键全部已读
|
|
||||||
- ✅ 删除单条通知
|
|
||||||
- ✅ 自动刷新未读数(每 30 秒)
|
|
||||||
- ✅ 布局集成到顶部栏
|
|
||||||
|
|
||||||
#### P3 增强功能
|
### 背景(用户诉求)
|
||||||
- ✅ 系统更新检查(GitHub Releases API + SemVer 比较)
|
用户指出:NAT 后的机器与公网 VPS 通信本不应有限制(标准 UDP/WireGuard 语义:主动侧发包后
|
||||||
- ✅ 版本对比对话框
|
NAT 记录会话,回包可返回)。且希望 core 成为「地址权威」——集成时对端地址从 core 获取再喂给
|
||||||
- ✅ 更新日志展示
|
wg-go,而非依赖 wg-go 预配置的 endpoint 可达性。
|
||||||
- ✅ 下载链接跳转
|
|
||||||
|
|
||||||
### 🔧 技术改进
|
### 根因(旧实现为何 NAT 场景不通)
|
||||||
|
增强模式此前用「每 peer 一个 connected UDP socket」(`DirectFactory.Dial` →
|
||||||
|
`net.DialContext`)。connected socket 只接收所连对端的包:
|
||||||
|
- NAS(NAT 后)拨 VPS:51820 成功,但其源地址经 NAT 改写为 `WAN:随机端口`;
|
||||||
|
- VPS 的 connected socket 连的是 `NAS内网:51820`,收到的 `WAN:随机端口` 源地址不匹配 → 丢弃。
|
||||||
|
|
||||||
#### 后端架构
|
即公网侧「收得到、回不去」。这不是 NAT 的固有限制,而是 connected socket 语义所致。
|
||||||
- ✅ 完善 Service 层数据库访问封装(GetDB 方法)
|
|
||||||
- ✅ 统一 Handler 层构造函数设计
|
|
||||||
- ✅ 优化中间件注册流程
|
|
||||||
- ✅ 改进错误处理和日志记录
|
|
||||||
|
|
||||||
#### 前端架构
|
### 修复(回归标准 WireGuard Bind 语义)
|
||||||
- ✅ 创建独立的 notifications API 模块
|
Direct-UDP 改由 `EnhancedBind` 的**共享未连接 UDP socket**(`directSock`,绑定 listen_port)承载:
|
||||||
- ✅ 开发可复用的 NotificationCenter 组件
|
- `core/bind.go`:
|
||||||
- ✅ 集成到 MainLayout 布局
|
- 新增 `directSock *net.UDPConn`、`listenPort`;`inboundPacket` 增加 `ep conn.Endpoint`。
|
||||||
- ✅ 实现响应式通知列表 UI
|
- `Open` 用 `net.ListenUDP` 在 listen_port 建共享 socket(listen_port=0 时由 OS 分配并回填
|
||||||
|
引擎实际端口),启动 `readDirectLoop`。
|
||||||
|
- `readDirectLoop` 从任意来源收包,把**真实源地址**作为 Endpoint 投入 `inboundCh`
|
||||||
|
(NAT 穿透学址核心:公网侧据此学到对端 NAT 映射后地址)。
|
||||||
|
- `receiveFunc`/`endpointForPacket`:Direct 入站用学得的真实源地址;中继入站按 peerKey 查缓存。
|
||||||
|
- `Send` → `directSendTo`:经共享 socket `WriteToUDP` 发往目标地址(源端口固定 listen_port,
|
||||||
|
满足对称直连)。移除旧的随机端口兜底 socket(`fallbackSock`/`fallbackSend`)。
|
||||||
|
- `core/engine.go`:新增 `directByBind` 标志(由 `NewEnhancedBind` 置位)。增强模式下
|
||||||
|
`NotifyPeerInfo` 不再对 Direct 层主动拨号(WG 启动后自动向 endpoint 发握手经 `Send` 承载),
|
||||||
|
避免 `DirectFactory.Dial` 再绑 listen_port 造成 UDP 端口冲突。
|
||||||
|
|
||||||
#### 编译与部署
|
### 效果
|
||||||
- ✅ 创建 Windows 一键启动脚本(start.bat)
|
- 双公网对称直连:`A:port↔B:port` 原生可通。
|
||||||
- ✅ 创建 Linux/Mac启动脚本(start.sh)
|
- NAT 后主动侧 ↔ 公网侧:公网侧收包学到对端真实地址,回包自然返回,**无需对端预先公网可达、
|
||||||
- ✅ 完善 .gitignore 配置
|
无需路由器端口映射**。core 成为地址权威,集成侧可从 core 取对端地址喂给 wg-go。
|
||||||
- ✅ 优化前端编译配置
|
- TURN/WebRTC/WS 等中继/信令层不受影响,仍经 connMgr 承载(Direct 不可用时补充)。
|
||||||
|
- TCP 直连族(FakeTCP/RealTCP)为 TCP,与 UDP `directSock` 不冲突。
|
||||||
|
|
||||||
### 📚 文档更新
|
### 验证
|
||||||
|
- `go build ./... && go vet ./... && go test ./...` → 全部通过(3 个测试包 ok)。
|
||||||
|
|
||||||
#### 新增文档
|
## 2026-07-15 修复:直连传输 socket 未绑定本地端口,两台机器互传失败(核心 P0)
|
||||||
- ✅ README.md - 项目主文档
|
|
||||||
- ✅ QUICKSTART.md - 快速入门指南
|
|
||||||
- ✅ README_开发完成总览.md - 开发完成总览
|
|
||||||
- ✅ 功能验证与测试报告.md - 测试验证文档
|
|
||||||
- ✅ 交付清单.md - 最终交付清单
|
|
||||||
|
|
||||||
#### 实现报告
|
### 背景(用户排查「两台机器传不了数据」)
|
||||||
- ✅ 完整功能开发总结报告.md
|
此前已修复「初始直连缺入站读取」,但端到端仍不通。进一步排查数据路径(Bind→Relay→
|
||||||
- ✅ WebSocket 实时通知推送功能实现报告.md
|
DirectFactory)发现更底层缺陷:增强模式下 WG 经 `EnhancedBind` 接管,**并不在 listen_port
|
||||||
- ✅ P3_系统更新检查功能实现报告.md
|
上监听真实 UDP**(`Open` 仅从 `inboundCh` 取包,无 `ListenUDP`)。
|
||||||
- ✅ 完整功能开发 - 最终完成报告.md
|
|
||||||
|
|
||||||
### 📊 统计数据
|
### 根因
|
||||||
|
`DirectFactory.Dial` 用 `dialer.DialContext(ctx,"udp",candidate)`,**本地端口为随机临时端口**:
|
||||||
|
- A 拨 B:51820 → A 的 socket 本地 `A:临时`、远端 `B:51820`;
|
||||||
|
- B 拨 A:51820 → B 的 socket 本地 `B:临时`、远端 `A:51820`。
|
||||||
|
|
||||||
- **新增文件**: 22 个
|
A 发往 `B:51820` 的包到达 B 的 OS 时,B 在 51820 无任何监听 socket(B 的传输 socket 本地是
|
||||||
- **代码行数**: ~6,100 行
|
临时端口),**被 OS 丢弃**;B→A 同理。即两端都出得去、进不来——这正是「frp 能传、meshray-core
|
||||||
- **API 接口**: 20 个(100% 实现)
|
不行」的本质:frp 是中心中继(两端都出向连 server),而本仓增强模式要求对称直连却没把
|
||||||
- **文档**: 8 份
|
本地端口绑到 listen_port。
|
||||||
|
|
||||||
### 🔒 安全性
|
### 修复
|
||||||
|
让 Direct-UDP 传输 socket **把本地端口绑到 listen_port(WG listen_port)**,形成
|
||||||
|
`A:port↔B:port` 对称直连:
|
||||||
|
- `connect/direct.go`:`DirectFactory` 增加 `localPort` 与 `SetLocalPort`;`Dial` 时
|
||||||
|
`dialer.LocalAddr = &net.UDPAddr{Port: bindPort}`(优先 `DialConfig.LocalPort`,其次工厂值)。
|
||||||
|
- `connect/strategy.go`:`DialConfig` 增加 `LocalPort int`。
|
||||||
|
- `core/engine.go`:`Engine` 持有 `listenPort` 与 `directFactory` 引用,新增 `SetListenPort`
|
||||||
|
回填工厂;`initiateConnection` 把 `listenPort` 注入 `DialConfig.LocalPort`。
|
||||||
|
- `core/bind.go`:`NewEnhancedBind` 内调用 `eng.SetListenPort(listenPort)`,
|
||||||
|
使 standalone 与受管模式(均经此入口)都能拿到本端端口。
|
||||||
|
|
||||||
- ✅ bcrypt 密码加密(DefaultCost 强度)
|
### 验证
|
||||||
- ✅ JWT 身份验证
|
- 临时单测模拟两机(回环两端口):A:15180↔B:15181 对称直连,双向 Write/Read 均成功
|
||||||
- ✅ CORS 跨域控制
|
(修复前临时端口下对端收不到)。已删除该临时测试。
|
||||||
- ✅ SQL 参数化查询(防注入)
|
- `go build ./... && go vet ./...` → 通过。
|
||||||
- ✅ 权限隔离
|
|
||||||
- ✅ 操作日志记录(AuditLog)
|
|
||||||
|
|
||||||
### ⚠️ 已知问题
|
### 注
|
||||||
|
NAT 后的机器仍依赖 TURN/WebRTC 中继/打洞层(需配置 `ice` 服务器),本修复解决的是
|
||||||
|
「直连(独立 IP / 公网 / 同局域网)对称互通」这一基础能力。
|
||||||
|
|
||||||
#### 待完善功能
|
## 2026-07-15 独立 meshray-core 改用 TOML 配置文件(类 frp)
|
||||||
|
|
||||||
1. **阿里云 DNS Provider**
|
### 需求
|
||||||
- 原因:网络问题导致无法下载 libdns/aliyun
|
独立运行(类 frp 风格)希望像 frp 一样使用 TOML 配置文件,而非 JSON。
|
||||||
- 计划:网络恢复后安装并完成实现
|
|
||||||
|
|
||||||
2. **真实备份逻辑**
|
### 改动
|
||||||
- 原因:优先级较低,先完成框架
|
- 新增依赖 `github.com/BurntSushi/toml`(与 frp 同款)。
|
||||||
- 计划:实现数据库导出、配置文件备份等逻辑
|
- `core/cmd/meshray-core/config.go`:
|
||||||
|
- `Config` 各字段增加 `toml` 标签(键名与 JSON 一致,snake_case)。
|
||||||
|
- `loadConfig` 按扩展名自动选解码器:`.toml`/`.conf` → TOML;其它(含 `.json`)→ JSON,
|
||||||
|
旧 JSON 配置继续可用。判断前先剥离 `.sample` 模板后缀,避免 `meshray-core.toml.sample`
|
||||||
|
被误判为 JSON。
|
||||||
|
- `main.go`:默认配置路径由 `meshray-core.json` 改为 `meshray-core.toml`;`-c` 仍可指定任意路径。
|
||||||
|
- 新增样例 `core/cmd/meshray-core/meshray-core.toml.sample`。
|
||||||
|
|
||||||
3. **WebSocket 中间件**
|
### 注意(TOML 语义坑)
|
||||||
- 原因:已有轮询机制(每 30 秒),非必需
|
TOML 中 `[[peers]]` 数组表之后、下一个表头之前的键会被归入该 peers 元素。
|
||||||
- 计划:可选优化,实现实时推送
|
故 `layer_strategy` / `layers` 等顶层键**必须置于 `[[peers]]` 之前**,否则会被误塞进 peers 表。
|
||||||
|
样例已据此排布,并加注释提醒。
|
||||||
|
|
||||||
---
|
### 验证
|
||||||
|
- 临时单测加载 `meshray-core.toml.sample`,确认 network_id / interface / mode / peers /
|
||||||
|
ice / layer_strategy / layers 全部正确映射(已删除该临时测试)。
|
||||||
|
- `go build ./... && go vet ./...` → 通过。
|
||||||
|
|
||||||
## [2.0.1] - 之前的版本
|
## 2026-07-15 新增:传输层级支持手动指定(auto / manual)
|
||||||
|
|
||||||
### 基础功能
|
### 需求
|
||||||
- ✅ WireGuard 组网核心功能
|
除默认的 9 层自动降级外,需支持手动指定传输层级,便于在「已知网络环境」下锁定链路
|
||||||
- ✅ 用户管理系统
|
(如强制走 TURN-TCP 中继、或强制 Direct-UDP 直连),也便于排障时固定单链路复现。
|
||||||
- ✅ 设备管理
|
|
||||||
- ✅ 策略管理
|
|
||||||
- ✅ MeshSeed 凭证生成
|
|
||||||
- ✅ 待审核加入机制
|
|
||||||
- ✅ Dashboard 基础监控
|
|
||||||
- ✅ 实时监控面板
|
|
||||||
- ✅ 日志查看
|
|
||||||
- ✅ 系统设置
|
|
||||||
|
|
||||||
---
|
### 配置入口(独立 meshray-core)
|
||||||
|
- `core/cmd/meshray-core/config.go`:`Config` 新增两字段
|
||||||
|
- `layer_strategy`:`"auto"`(默认,9 层自动降级)或 `"manual"`(仅用下方 layers)。
|
||||||
|
- `layers`:`[]string`,仅 manual 生效。单元素=强制固定层、禁用降级;
|
||||||
|
多元素=按列表顺序自定义降级链。
|
||||||
|
- 支持别名解析(`core/connect/strategy.go` 新增 `ParseLayer`):如 `udp`/`tcp`/`wss`/
|
||||||
|
`webrtc`/`turn` 等均可识别。
|
||||||
|
- 示例:`{ "layer_strategy":"manual", "layers":["TURN-TCP"] }` 强制 TURN-TCP 中继;
|
||||||
|
`{ "layer_strategy":"manual", "layers":["Direct-UDP","TURN-TCP"] }` 直连优先、不通再中继。
|
||||||
|
|
||||||
## 🎯 未来计划
|
### 联动修复(降级顺序感知)
|
||||||
|
- `FallbackController` 原本按 `Layer` 枚举下标(`currentIndex+1`)降级/恢复,手动自定义顺序下
|
||||||
|
会越界到错误层级。改为按 `StrategyScheduler.layerOrder` 顺序索引取上一/下一层
|
||||||
|
(新增 `nextLayerInOrder` / `prevLayerInOrder`)。
|
||||||
|
- 由此,手动单层级场景下「无下一层」自然退化为不降级,语义正确。
|
||||||
|
|
||||||
### v2.1.0(计划中)
|
### ICE 配置补齐(手动 TURN/WebRTC 必需)
|
||||||
- [ ] 阿里云 DNS Provider 实现
|
- 独立二进制此前从未调用 `Engine.SetICEConfig`,导致 TURN/WebRTC 工厂拿不到 STUN/TURN 地址。
|
||||||
- [ ] 真实的备份/恢复逻辑
|
- `main.go` 现已把配置里的 `ice.stun` / `ice.turn` 下达到引擎,手动 TURN-TCP/TURN-TLS/
|
||||||
- [ ] WebSocket 实时推送中间件
|
WebRTC 层级方可真正建连。
|
||||||
- [ ] 告警规则管理
|
|
||||||
- [ ] 资源监控图表优化
|
|
||||||
|
|
||||||
### v2.2.0(规划中)
|
### 管控面(ctr)
|
||||||
- [ ] 多语言国际化
|
- `internal/ctr` 已通过 `update.EnabledLayers` → `SetLayerOrder` 支持自定义顺序,
|
||||||
- [ ] 主题切换功能
|
等价「手动多层级」,本次无需改动;后续若需「auto/manual」语义可在此扩展。
|
||||||
- [ ] 移动端适配优化
|
|
||||||
- [ ] 性能监控和告警
|
|
||||||
- [ ] CI/CD 集成
|
|
||||||
|
|
||||||
---
|
### 构建验证
|
||||||
|
- `core` 独立:`go build ./... && go vet ./...` → 通过。
|
||||||
|
|
||||||
## 📝 说明
|
## 2026-07-14 核心路基缺陷修复 (P0 + P1)
|
||||||
|
|
||||||
- 版本号格式:主版本号。次版本号。修订号
|
### 背景
|
||||||
- 优先级说明:
|
此前验证集中在 HTTP/前端接口与页面,属表层验证。核心组网路基(WireGuard 增强模式
|
||||||
- P0: 核心功能
|
P2P 劫持转发链路)实际断裂。本次以单元测试钉死缺陷后,按 P0→P1 全修。
|
||||||
- P1: 重要功能
|
|
||||||
- P2: 次要功能
|
|
||||||
- P3: 增强功能
|
|
||||||
|
|
||||||
---
|
### P0-1 Bind 空指针崩溃
|
||||||
|
- 文件: `core/transport/conn_manager.go`
|
||||||
|
- 问题: `Add`/`Remove`/`CloseAll` 在 `conn==nil` 时调用 `conn.Close()`/`RemoteAddr()`
|
||||||
|
空指针 panic。增强模式每次 `AddPeer` 都会直接崩。
|
||||||
|
- 修复: 三处全部对 `nil` conn 做安全处理(仅登记 peerKey,不调用 conn 方法)。
|
||||||
|
|
||||||
**最后更新**: 2026-03-20
|
### P0-2 建连永不触发
|
||||||
**维护人员**: MeshRay Team
|
- 文件: `core/engine.go`、`core/connect/direct.go`、`internal/ctr/ctr.go`、`internal/ctr/wg.go`
|
||||||
|
- 问题: `ctr` 从不调用 `engine.NotifyPeerInfo` 注入对端候选,`candidateStore` 恒空,
|
||||||
|
`initiateConnection` 直接 return,9 层策略拨号永不触发;`DialConfig` 也无 `Candidates`,
|
||||||
|
`DirectFactory` 拿不到对端地址。
|
||||||
|
- 修复:
|
||||||
|
- `DialConfig` 增加 `Candidates` 字段。
|
||||||
|
- `DirectFactory.Dial` 优先使用信令下发的候选地址。
|
||||||
|
- `engine.initiateConnection` 将 `candidateStore` 转为 `DialConfig.Candidates`。
|
||||||
|
- `WGManager.ListPeers` 从 `wgctrl` 读取真实 Endpoint 作为候选来源。
|
||||||
|
- `ctr.SwitchMode` 切增强模式时注入对端真实 Endpoint 触发建连;
|
||||||
|
新增 `NotifyPeerCandidates` 供信令层注入。
|
||||||
|
|
||||||
|
### P1-1 routeID 体系断裂(双向转发只通一半)
|
||||||
|
- 文件: `core/transport/relay.go`、`core/engine.go`
|
||||||
|
- 问题: `engine.Bind` 用 `extractRouteID(publicKey)`(前4字符 ASCII 和)注册本地端口,
|
||||||
|
而 `relay.forwardIncoming` 用 WG 包 `receiver index` 查表,两套 routeID 无映射,
|
||||||
|
对端→本地转发全部 miss。
|
||||||
|
- 修复: 转发路由统一改为 `peerKey`。删除伪造的 `extractRouteID`;
|
||||||
|
`RegisterLocalPort`/`UnregisterLocalPort`/`StartReadFromLocalPort`/`forwardIncoming`
|
||||||
|
全部以 `peerKey` 为索引。
|
||||||
|
|
||||||
|
### P1-2 UpdateCoreConfig 桩
|
||||||
|
- 文件: `internal/ctr/ctr.go`、`core/engine.go`、`core/connect/strategy.go`、`core/connect/ice.go`
|
||||||
|
- 问题: `UpdateCoreConfig` 直接 `return "尚未实现"`;`SetICEConfig` 仅打日志;
|
||||||
|
WebRTC 工厂 ICE 无法更新。
|
||||||
|
- 修复:
|
||||||
|
- 新增 `CoreConfigUpdate` 结构(STUN/TURN/启用层)。
|
||||||
|
- `UpdateCoreConfig` 真实生效:下发 ICE 配置 + 设置传输层优先级顺序。
|
||||||
|
- `engine.SetICEConfig` 真正调用 `scheduler.UpdateWebRTCICE`。
|
||||||
|
- `scheduler` 新增 `UpdateWebRTCICE`;`WebRTCFactory` 新增 `UpdateICE`;
|
||||||
|
`ICEClient` 新增 `UpdateConfig`。
|
||||||
|
|
||||||
|
### P1-1 后续清理:routeIDStore 死状态
|
||||||
|
- 文件: `core/engine.go`、`internal/ctr/ctr.go`、`core/engine_test.go`
|
||||||
|
- 问题: 上轮删除了 `extractRouteID` 并把转发路由统一为 `peerKey`,但 `Engine` 仍保留
|
||||||
|
`routeIDStore map[string]uint32`,`NotifyPeerInfo` 仍接收并存储恒为 0 的 `routeID`,
|
||||||
|
`initiateConnection` 仍查 `routeIDStore[peerKey]` 作为冗余闸门。该 map 实际只是
|
||||||
|
“是否收到过 NotifyPeerInfo”的布尔标记,存的 0 无任何用途,且对其他触发路径是陷阱。
|
||||||
|
- 修复: 彻底删除 `routeIDStore` 字段、其初始化与清理、`NotifyPeerInfo` 的 `routeID` 参数
|
||||||
|
及存储逻辑;`initiateConnection` 删除 “获取 RouteID” 冗余检查。路由判定完全统一为
|
||||||
|
`candidateStore` 非空 + `peerKey` 索引。同步清理 `ctr`、`engine_test` 的调用点。
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
- `core/transport/relay_test.go`: peerKey 路由匹配转发 / 未知丢弃。
|
||||||
|
- `core/engine_test.go`: Bind 不再崩溃、无候选不盲目建连、注入候选后链路接通。
|
||||||
|
- `core/connect/scheduler_test.go`: 策略层选择、DirectFactory 缺候选必败、
|
||||||
|
候选地址真实建连(本地 listener 验证)。
|
||||||
|
- 验证说明: 沙箱 App Control 拦截 `core` 包测试二进制的执行;`core/connect`、
|
||||||
|
`core/transport` 测试已成功运行通过;全量 `go build ./...` 与 `go vet ./core/...` 通过。
|
||||||
|
|
||||||
|
## 2026-07-15 架构重构:多仓解耦 + 编译期注入 + 闭源 core + conn.Bind 接管
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
原架构把闭源增强逻辑(`core`)以本地子目录方式内嵌于开源管理器主仓,存在两点硬伤:
|
||||||
|
1. **开源条约风险**:开源主仓直接包含闭源 `core` 源码,无法干净剥离。
|
||||||
|
2. **Endpoint 劫持原理缺陷**:旧方案把 peer Endpoint 改写为回环端口,企图让 WG 入站
|
||||||
|
流量经本地端口旁路;但 WG 入站按真实源地址匹配 peer,改写后源地址与 peer 不一致,
|
||||||
|
**入站接不通**,9 层传输大部分是空壳,信令层缺失,零端到端验证。
|
||||||
|
|
||||||
|
经架构决策(用户确认):
|
||||||
|
- 底层用开源 `wireguard-go`,增强层 `meshray-core` 闭源,二者物理边界清晰、
|
||||||
|
可分开授权;管理器开源;**无 meshray-core 时系统仍以标准 WG 直连运行**。
|
||||||
|
- 代码物理边界 = **多仓解耦**(独立 module)。
|
||||||
|
- 增强层接入方式 = **编译期注入**(build tag 触发空白导入 `init()` 注册)。
|
||||||
|
|
||||||
|
### 关键契约:开源契约层(叶子 module)
|
||||||
|
- 新增 `pkg/contract`(`module git.zkcoi.com/zkcoi/meshray-contract`,开源叶子模块),
|
||||||
|
作为主仓与 core 之间**唯一的 module 边界契约**,解决 `meshray ↔ meshray-core`
|
||||||
|
双向依赖导致的 module 级循环依赖:
|
||||||
|
- 定义 `Candidate`、`Layer`(9 层常量 + `String` + `DefaultLayerOrder`)、`ICEConfig`、
|
||||||
|
`PeerStatus`、`EngineStatus`、`EngineController` 接口
|
||||||
|
(`Start`/`Stop`/`NotifyPeerCandidates`/`SetICEConfig`/`SetLayerOrder`/`GetStatus`)。
|
||||||
|
- 定义 `BindFactory = func(listenPort int, networkID string, logger *zap.Logger) (conn.Bind, error)`;
|
||||||
|
注册表 `RegisterEnhancedBindFactory`/`GetBindFactory(enhanced bool)`:
|
||||||
|
enhanced=false 返回 wireguard-go 默认 Bind(**保证无 core 仍可直连**);
|
||||||
|
enhanced=true 未注册时返回 `errBindNotEnhanced`。
|
||||||
|
|
||||||
|
### core 独立 module(闭源)
|
||||||
|
- 新增 `core/go.mod`(`module git.zkcoi.com/zkcoi/meshray-core`,go 1.26.0),
|
||||||
|
require 主仓全部依赖 + `meshray-contract`,末尾
|
||||||
|
`replace git.zkcoi.com/zkcoi/meshray-contract => ../pkg/contract`。
|
||||||
|
- 新增 `core/bind.go`:`EnhancedBind` 实现 `conn.Bind`,包裹 `conn.NewDefaultBind()`;
|
||||||
|
`Send` 当前透传并预留 9 层策略插入点;`NewEnhancedBind` 调用 `getOrCreateEngine`。
|
||||||
|
- 新增 `core/registry.go`:`engines` 单例 + `getOrCreateEngine`;`engineController`
|
||||||
|
适配 `contract.EngineController`;`init()` 注册 `RegisterEnhancedBindFactory` +
|
||||||
|
`RegisterEngineProvider`(**仅编译进 core 时执行**)。
|
||||||
|
- 新增 `core/adapter.go`:契约 ↔ core 类型转换(候选 / ICE / 层顺序 / 状态)。
|
||||||
|
- `core/engine.go`:import 路径改 `.../meshray/core/` → `.../meshray-core/`;
|
||||||
|
删除废弃的 `Bind(peerKey, localPort)` / `Unbind(peerKey)` 旁路方法(根治 Endpoint 旁路缺陷);
|
||||||
|
删除不再使用的 `"fmt"` import。
|
||||||
|
- `core/transport/plugin.go`:删除死代码 `ExtractRouteID` 接口方法。
|
||||||
|
- `core/plugins/wg/wgparse.go`:删除 `ExtractRouteID` 实现及无用 import,仅保留
|
||||||
|
`IsControlPacket`/`IsDataPacket`。
|
||||||
|
- `core/engine_test.go`:删除依赖已废弃 `engine.Bind`/`Unbind` 的用例。
|
||||||
|
|
||||||
|
### 管理器主仓(开源)
|
||||||
|
- `go.mod`:删除旧 `replace .../core => ./core`;新增
|
||||||
|
`replace .../meshray-contract => ./pkg/contract`、
|
||||||
|
`replace .../meshray-core => ./core`;require 两新 module(均 `v0.0.0`)。
|
||||||
|
- 新增 `internal/ctr/core_enable.go`:`//go:build meshray_core` + `_ "git.zkcoi.com/zkcoi/meshray-core"`
|
||||||
|
空白导入,触发 core `init()` 注册。
|
||||||
|
- `internal/ctr/wg.go`:`CreateDevice` 增 `meshMode` 参数;
|
||||||
|
`startUserModeWGProcessWithRefs` 用 `contract.GetBindFactory(meshMode == "enhanced")`
|
||||||
|
替代硬编码 `conn.NewDefaultBind()`(**交由 core 经 conn.Bind 接管 WG 收发**);
|
||||||
|
增强模式获取失败返回错误;`UpdatePeerEndpoint` 注释强调增强模式不应改写回环端口。
|
||||||
|
- `internal/ctr/ctr.go`:去除 `core` 具名 import;`coreInst *core.Core` →
|
||||||
|
`engines map[string]contract.EngineController`;`CreateNetwork` enhanced 模式走
|
||||||
|
`contract.NewEngineController`;`AddPeer`/`RemovePeer` 不再改写 Endpoint 为回环端口;
|
||||||
|
`SwitchMode` 经 `engineCtl.NotifyPeerCandidates` 注入候选;
|
||||||
|
`CoreConfigUpdate.TURNServers` 改 `[]string`、`EnabledLayers` 改 `[]contract.Layer`;
|
||||||
|
`NetworkStatus.CoreStatus` 改 `*contract.EngineStatus`。
|
||||||
|
|
||||||
|
### 构建验证
|
||||||
|
- `core` 独立:`go build ./... && go vet ./...` → 通过。
|
||||||
|
- 管理器默认(无 core):`go build ./...` → 通过(标准 WG 直连)。
|
||||||
|
- 管理器增强(编译期注入):`go build -tags meshray_core ./...` → 通过。
|
||||||
|
|
||||||
|
### 仍然待补(非本次范围)
|
||||||
|
- `EnhancedBind.Send` 实际插入 9 层策略(当前透传)。
|
||||||
|
- 信令层真正驱动 `NotifyPeerCandidates`;P3 传输层空壳补全。
|
||||||
|
- `docs/` 下仍描述 “relay 按 route_id 查表转发” 的旧文档校正(用户规则禁止新增说明文档,
|
||||||
|
已通过代码注释反映新路由逻辑)。
|
||||||
|
|
||||||
|
## 2026-07-15 增强 Bind 虚拟桥接 + meshray-core 独立二进制(类 frp)
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
用户希望像 frp 一样直接运行 core 在两台机器上验证收发,而非必须经由管理器。
|
||||||
|
核查发现旧 `EnhancedBind` 仅是**纯透传**(Send 直接转交默认 UDP bind),而 9 层
|
||||||
|
`relay` 的 `localPorts` 在增强模式下从未被注册,导致 9 层传输逻辑根本没接到 WG 收发路径上。
|
||||||
|
本次把 `EnhancedBind` 重写为真正桥接 WG ↔ 引擎 9 层传输,并新增独立可运行二进制。
|
||||||
|
|
||||||
|
### 关键发现(wireguard-go 匹配机制)
|
||||||
|
阅读 `device/receive.go` 确认:传输包由包内 **receiver index** 匹配 peer,Endpoint 仅用于
|
||||||
|
握手更新对端出向地址与 MAC2 cookie 计算。因此回灌入站时只要携带**对端真实 Endpoint**,
|
||||||
|
即可正确接通,无需让 WG 监听真实 UDP 端口——这正是增强 Bind 完全接管收发的依据。
|
||||||
|
|
||||||
|
### core/bind.go(重写)
|
||||||
|
- `EnhancedBind` 不再依赖默认 bind 的真实 UDP 监听:`Open` 返回从 `inboundCh` 读取引擎
|
||||||
|
入站的接收函数;`Send` 按 `ep` 反查 peerKey,经引擎 9 层传输连接发送,连接未就绪时
|
||||||
|
惰性触发拨号并走直连兜底。
|
||||||
|
- `deliverInbound`:引擎传输连接读到的入站密文投入 `inboundCh`,由接收函数拷贝进 WG
|
||||||
|
缓冲并填入对端真实 Endpoint 回灌。
|
||||||
|
- `RegisterPeer(addr, peerKey)`:维护 endpoint→peerKey 与 peerKey→真实 Endpoint 映射。
|
||||||
|
- 接收函数严格遵守 WG 缓冲所有权(拷贝而非替换指针),`BatchSize`/`ParseEndpoint` 委托默认 bind。
|
||||||
|
|
||||||
|
### core/transport/relay.go
|
||||||
|
- 新增 `OnInbound func(peerKey string, packet []byte)` 钩子;`StartReadFromRemoteConn` 设置后
|
||||||
|
入站改投该钩子(即 EnhancedBind),否则走原本地端口转发(向后兼容)。
|
||||||
|
|
||||||
|
### core/engine.go
|
||||||
|
- `Engine` 增加 `bind *EnhancedBind` 字段(由 `NewEnhancedBind` 回填)。
|
||||||
|
- `NotifyPeerInfo` 在收到候选时自动 `RegisterPeer`,并新增 `EnsureDial`(Send 惰性拨号用)。
|
||||||
|
|
||||||
|
### core/cmd/meshray-core(新增独立二进制)
|
||||||
|
- `main.go` + `config.go`:类 frp 风格,`-c config.json` 启动、`-genkey` 生成私钥。
|
||||||
|
- 直接以 `core.NewEnhancedBind` 接管 wireguard-go 用户态设备收发;按配置创建 TUN、
|
||||||
|
注入对端候选触发 9 层拨号、`Up` 后分配隧道 IP(Linux `ip` / Windows `netsh`)。
|
||||||
|
- `mode: enhanced` 走 9 层接管;`mode: native` 退化为裸 WG 直连,便于对照测试。
|
||||||
|
- 新增 `meshray-core.example.json` 示例配置(占位符,用户替换公私钥/endpoint 即可两机对测)。
|
||||||
|
- `go.mod`:`go mod tidy` 引入 `wireguard/wgctrl` 与 Windows `wintun` 依赖。
|
||||||
|
|
||||||
|
### 构建验证
|
||||||
|
- `core` 独立:`go build ./... && go vet ./...` → 通过;`go build -o meshray-core.exe ./cmd/meshray-core` 通过;
|
||||||
|
`./meshray-core.exe -genkey` 正常输出私钥。
|
||||||
|
- 管理器默认:`go build ./...` → 通过;增强:`go build -tags meshray_core ./...` → 通过。
|
||||||
|
|
||||||
|
### 两机对测步骤(摘要,详见二进制内注释与示例配置)
|
||||||
|
1. 两台机器各自 `meshray-core -genkey` 得私钥,用 `private_key` 对应公钥填对方 `public_key`。
|
||||||
|
2. A 的 `interface=10.0.0.1/24`、`peers[0].endpoint=B公网IP:51820`、`allowed_ips=10.0.0.2/32`;B 对称。
|
||||||
|
3. 两台均 `meshray-core -c meshray-core.json`,随后 `ping 10.0.0.x` 验证隧道收发。
|
||||||
|
4. Windows 需已安装 Wintun 驱动(随 WireGuard 客户端提供)。
|
||||||
|
|
||||||
|
## 2026-07-15 修复:初始直连缺少入站读取,公网/独立 IP 机器直连亦不通
|
||||||
|
|
||||||
|
### 背景(用户纠正)
|
||||||
|
用户指出 meshray-core 的核心机制必须**同时**满足「NAT 后的机器」与「有独立 IP 的机器」,
|
||||||
|
二者都要能直连(P2P)→ 失败再走中继/打洞兜底。这意味着即使两台公网机器直连,
|
||||||
|
当前实现也应能接通。实测与代码核查发现:**初始直连路径本身就没接通**,与是否 NAT 无关。
|
||||||
|
|
||||||
|
### 根因
|
||||||
|
- `StrategyScheduler.Dial`(`core/connect/strategy.go:284`)成功后**仅返回连接,从不调用 `OnConnectionUpdate`**。
|
||||||
|
- `OnConnectionUpdate` 回调(`core/engine.go:45`)才是唯一会调用
|
||||||
|
`relay.StartReadFromRemoteConn` 启动入站读取的地方;而它只在 `reconnectToLayer`
|
||||||
|
(降级重连)与 `probeHigherLayers`(恢复探测)内触发。
|
||||||
|
- `Engine.initiateConnection` 拿到 `Dial` 返回的直连后只做了 `connMgr.Add`,**未启动入站读取协程**。
|
||||||
|
|
||||||
|
### 断点后果
|
||||||
|
两侧都只发出握手、收不到对方回包 → 握手无法完成 → 隧道起不来。
|
||||||
|
该缺陷在「两台公网/独立 IP 机器直连」场景下同样存在,并非 NAT 专属问题。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `core/engine.go`:`initiateConnection` 在 `connMgr.Add` 之后显式调用
|
||||||
|
`e.relay.StartReadFromRemoteConn(context.Background(), peerKey, conn)`,
|
||||||
|
使初始直连的入站密文能经 `OnInbound` 回灌 WG(增强模式)或写入本地端口。
|
||||||
|
- 降级重连 / 恢复探测仍走 `OnConnectionUpdate` 原路径,无重复启动冲突
|
||||||
|
(旧连接被 `connMgr.Add` 关闭后其读取协程自然退出)。
|
||||||
|
|
||||||
|
### 构建验证
|
||||||
|
- `core` 独立:`go build ./... && go vet ./...` → 通过。
|
||||||
|
- 注:沙箱无法运行 TUN/Wintun,端到端需用户在两台真实机器(或同机双进程)验证
|
||||||
|
`ping 10.0.0.x` 收发。修复后公网 IP / 同局域网直连场景预期可正常握手建隧。
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("=== MeshRay 数据库表结构检查 ===\n")
|
fmt.Println("=== MeshRay 数据库表结构检查 ===")
|
||||||
|
|
||||||
// 检查所有表是否存在
|
// 检查所有表是否存在
|
||||||
tables := []string{
|
tables := []string{
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 是 meshray-core 独立运行(类 frp 风格)的配置文件结构。
|
||||||
|
// 支持 TOML(默认,类 frp)与 JSON 两种格式,按文件扩展名自动选择解码器。
|
||||||
|
//
|
||||||
|
// TOML 示例:
|
||||||
|
//
|
||||||
|
// network_id = "net1"
|
||||||
|
// listen_port = 51820
|
||||||
|
// mtu = 1420
|
||||||
|
// private_key = "" # 留空则自动生成并写入 private_key_file
|
||||||
|
// private_key_file = "key.txt"
|
||||||
|
// interface = "10.0.0.1/24" # 本机隧道 IP
|
||||||
|
// mode = "enhanced" # enhanced=经 9 层传输接管;native=裸 WG 直连
|
||||||
|
//
|
||||||
|
// [[peers]]
|
||||||
|
// public_key = "<对端公钥 base64>"
|
||||||
|
// endpoint = "203.0.113.2:51820" # 增强模式经 9 层拨号对端;留空则仅作为接收方
|
||||||
|
// allowed_ips = ["10.0.0.2/32"]
|
||||||
|
//
|
||||||
|
// [ice]
|
||||||
|
// stun = ["stun:stun.l.google.com:19302"]
|
||||||
|
// turn = ["turn:your.turn:3478"]
|
||||||
|
//
|
||||||
|
// # 传输层级策略:auto=9 层自动降级(默认);manual=仅用下方 layers 指定的层
|
||||||
|
// layer_strategy = "auto"
|
||||||
|
// layers = ["Direct-UDP"] # 仅 manual 生效;单元素=强制固定层;多元素=自定义降级顺序
|
||||||
|
type Config struct {
|
||||||
|
NetworkID string `json:"network_id" toml:"network_id"`
|
||||||
|
ListenPort int `json:"listen_port" toml:"listen_port"`
|
||||||
|
MTU int `json:"mtu" toml:"mtu"`
|
||||||
|
PrivateKey string `json:"private_key" toml:"private_key"`
|
||||||
|
PrivateKeyFile string `json:"private_key_file" toml:"private_key_file"`
|
||||||
|
InterfaceIP string `json:"interface" toml:"interface"`
|
||||||
|
Mode string `json:"mode" toml:"mode"`
|
||||||
|
Peers []PeerConf `json:"peers" toml:"peers"`
|
||||||
|
ICE ICEConf `json:"ice" toml:"ice"`
|
||||||
|
|
||||||
|
// LayerStrategy 传输层级策略:
|
||||||
|
// "auto"(默认)= 按 DefaultLayerOrder 自动 9 层降级;
|
||||||
|
// "manual" = 仅使用 Layers 指定的层(手动),不再自动探测其它层。
|
||||||
|
LayerStrategy string `json:"layer_strategy" toml:"layer_strategy"`
|
||||||
|
// Layers 手动层级列表(仅 layer_strategy=manual 生效)。
|
||||||
|
// 单元素(如 ["TURN-TCP"])= 强制固定该层、禁用降级;
|
||||||
|
// 多元素(如 ["Direct-UDP","TURN-TCP"])= 按列表顺序自定义降级链。
|
||||||
|
Layers []string `json:"layers" toml:"layers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerConf 对端配置
|
||||||
|
type PeerConf struct {
|
||||||
|
PublicKey string `json:"public_key" toml:"public_key"`
|
||||||
|
Endpoint string `json:"endpoint" toml:"endpoint"`
|
||||||
|
AllowedIPs []string `json:"allowed_ips" toml:"allowed_ips"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICEConf STUN/TURN 服务器(增强模式打洞/中继用)
|
||||||
|
type ICEConf struct {
|
||||||
|
STUN []string `json:"stun" toml:"stun"`
|
||||||
|
TURN []string `json:"turn" toml:"turn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig 读取并解析配置文件:按扩展名自动选择 TOML 或 JSON 解码器。
|
||||||
|
// 默认且推荐 TOML(类 frp);旧 .json 配置仍可继续使用。
|
||||||
|
func loadConfig(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
// 按扩展名选择解码器:先剥离可能的 .sample 模板后缀,再判扩展名,
|
||||||
|
// 避免 "meshray-core.toml.sample" 被误判为 JSON。
|
||||||
|
lower := strings.ToLower(path)
|
||||||
|
lower = strings.TrimSuffix(lower, ".sample")
|
||||||
|
ext := filepath.Ext(lower)
|
||||||
|
switch ext {
|
||||||
|
case ".toml", ".conf":
|
||||||
|
// TOML:与 frp 风格一致
|
||||||
|
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 TOML 配置 %s 失败: %w", path, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// JSON:兼容旧配置
|
||||||
|
if err := json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 JSON 配置 %s 失败: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.MTU == 0 {
|
||||||
|
cfg.MTU = 1420
|
||||||
|
}
|
||||||
|
if cfg.Mode == "" {
|
||||||
|
cfg.Mode = "enhanced"
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
// 命令 meshray-core:meshray-core 增强层的独立运行入口(类 frp 风格)。
|
||||||
|
//
|
||||||
|
// 直接以自定义 conn.Bind 接管 WireGuard 收发,使所有隧道密文流量经由本仓 9 层传输
|
||||||
|
// (Direct/TURN/WebRTC/...)而非裸 UDP。无需管理器即可在两台机器间建立加密隧道,
|
||||||
|
// 用于端到端验证 core 的收发能力。
|
||||||
|
//
|
||||||
|
// 用法:
|
||||||
|
//
|
||||||
|
// meshray-core -genkey # 生成私钥
|
||||||
|
// meshray-core -c meshray-core.toml # 按配置启动(默认 TOML,类 frp;兼容 .json)
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
meshraycore "git.zkcoi.com/zkcoi/meshray/core/engine"
|
||||||
|
"git.zkcoi.com/zkcoi/meshray/core/connect"
|
||||||
|
corebind "git.zkcoi.com/zkcoi/meshray/internal/ctr/corebind"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.zx2c4.com/wireguard/conn"
|
||||||
|
"golang.zx2c4.com/wireguard/device"
|
||||||
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfgPath := flag.String("c", "meshray-core.toml", "配置文件路径(默认 TOML,类 frp;也兼容 .json)")
|
||||||
|
genKey := flag.Bool("genkey", false, "仅生成私钥并打印后退出")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *genKey {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "生成私钥失败:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(key.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadConfig(*cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "加载配置失败:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := newLogger()
|
||||||
|
defer func() { _ = logger.Sync() }()
|
||||||
|
|
||||||
|
// 1. 准备私钥:配置中给出则解析,否则生成并落盘
|
||||||
|
priv, err := resolvePrivateKey(cfg, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("私钥处理失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建 TUN 设备(用户态)。Windows 需已安装 Wintun 驱动。
|
||||||
|
devName := "mrc0"
|
||||||
|
tunDev, err := tun.CreateTUN(devName, cfg.MTU)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("创建 TUN 设备失败(Windows 需 Wintun 驱动)", zap.Error(err))
|
||||||
|
}
|
||||||
|
defer tunDev.Close()
|
||||||
|
|
||||||
|
// 3. 创建 Bind:enhanced=自定义接管;native=裸 WG 默认 Bind
|
||||||
|
var wgBind conn.Bind
|
||||||
|
if cfg.Mode == "native" {
|
||||||
|
logger.Info("以原生直连模式运行(裸 WG UDP)")
|
||||||
|
wgBind = conn.NewDefaultBind()
|
||||||
|
} else {
|
||||||
|
logger.Info("以增强模式运行(9 层传输接管收发)")
|
||||||
|
eb, eerr := corebind.NewEnhancedBind(cfg.ListenPort, cfg.NetworkID, logger)
|
||||||
|
if eerr != nil {
|
||||||
|
logger.Fatal("创建增强 Bind 失败", zap.Error(eerr))
|
||||||
|
}
|
||||||
|
wgBind = eb
|
||||||
|
}
|
||||||
|
|
||||||
|
wgLogger := &device.Logger{
|
||||||
|
Verbosef: func(format string, args ...interface{}) { logger.Debug(fmt.Sprintf(format, args...)) },
|
||||||
|
Errorf: func(format string, args ...interface{}) { logger.Error(fmt.Sprintf(format, args...)) },
|
||||||
|
}
|
||||||
|
wgDev := device.NewDevice(tunDev, wgBind, wgLogger)
|
||||||
|
defer wgDev.Close()
|
||||||
|
|
||||||
|
// 4. 组装 WG 配置(私钥 / 监听端口 / 各 peer)
|
||||||
|
var sb strings.Builder
|
||||||
|
fmt.Fprintf(&sb, "private_key=%s\nlisten_port=%d\n", priv.String(), cfg.ListenPort)
|
||||||
|
for _, p := range cfg.Peers {
|
||||||
|
pub, perr := wgtypes.ParseKey(p.PublicKey)
|
||||||
|
if perr != nil {
|
||||||
|
logger.Fatal("对端公钥解析失败", zap.String("public_key", p.PublicKey), zap.Error(perr))
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "public_key=%s\n", pub.String())
|
||||||
|
if p.Endpoint != "" {
|
||||||
|
fmt.Fprintf(&sb, "endpoint=%s\n", p.Endpoint)
|
||||||
|
}
|
||||||
|
for _, a := range p.AllowedIPs {
|
||||||
|
fmt.Fprintf(&sb, "allowed_ip=%s\n", a)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "persistent_keepalive=25\n")
|
||||||
|
}
|
||||||
|
if err := wgDev.IpcSet(sb.String()); err != nil {
|
||||||
|
logger.Fatal("配置 WireGuard 设备失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 增强模式:把对端候选(endpoint)注入引擎触发 9 层拨号
|
||||||
|
if eb, ok := wgBind.(*corebind.EnhancedBind); ok {
|
||||||
|
eng := eb.Engine()
|
||||||
|
|
||||||
|
// 5.0 下达 ICE 配置(STUN/TURN),手动 TURN/WebRTC 层级必需
|
||||||
|
if len(cfg.ICE.STUN) > 0 || len(cfg.ICE.TURN) > 0 {
|
||||||
|
turnCfgs := make([]connect.TURNServerConfig, 0, len(cfg.ICE.TURN))
|
||||||
|
for _, t := range cfg.ICE.TURN {
|
||||||
|
turnCfgs = append(turnCfgs, connect.TURNServerConfig{URLs: []string{t}})
|
||||||
|
}
|
||||||
|
ice := connect.ICEConfig{
|
||||||
|
STUNServers: cfg.ICE.STUN,
|
||||||
|
TURNServers: turnCfgs,
|
||||||
|
}
|
||||||
|
if err := eng.SetICEConfig(ice); err != nil {
|
||||||
|
logger.Error("下达 ICE 配置失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.1 应用传输层级策略:auto=默认 9 层自动降级;manual=仅用配置的 layers
|
||||||
|
if cfg.LayerStrategy == "manual" {
|
||||||
|
if len(cfg.Layers) == 0 {
|
||||||
|
logger.Fatal("layer_strategy=manual 但 layers 为空")
|
||||||
|
}
|
||||||
|
layers, perr := parseLayers(cfg.Layers)
|
||||||
|
if perr != nil {
|
||||||
|
logger.Fatal("解析手动层级失败", zap.Error(perr))
|
||||||
|
}
|
||||||
|
eng.SetLayerOrder(layers)
|
||||||
|
if len(layers) == 1 {
|
||||||
|
logger.Info("手动层级模式:强制固定层(禁用降级)",
|
||||||
|
zap.String("layer", layers[0].String()))
|
||||||
|
} else {
|
||||||
|
logger.Info("手动层级模式:自定义降级顺序",
|
||||||
|
zap.Strings("layers", cfg.Layers))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info("自动层级模式:9 层自动降级",
|
||||||
|
zap.Int("default_layers", len(connect.DefaultLayerOrder)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.2 注入对端候选,触发拨号
|
||||||
|
for _, p := range cfg.Peers {
|
||||||
|
if p.Endpoint == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := eng.NotifyPeerInfo(p.PublicKey, []meshraycore.Candidate{{Addr: p.Endpoint}}); err != nil {
|
||||||
|
logger.Error("注入对端候选失败", zap.String("peer", p.PublicKey), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 启动设备并配置隧道 IP
|
||||||
|
if err := wgDev.Up(); err != nil {
|
||||||
|
logger.Fatal("启动 WireGuard 设备失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
devName, nameErr := tunDev.Name()
|
||||||
|
if nameErr != nil {
|
||||||
|
logger.Warn("获取 TUN 设备名失败", zap.Error(nameErr))
|
||||||
|
devName = "mrc0"
|
||||||
|
}
|
||||||
|
assignInterfaceIP(devName, cfg.InterfaceIP, logger)
|
||||||
|
|
||||||
|
logger.Info("meshray-core 已启动",
|
||||||
|
zap.String("network_id", cfg.NetworkID),
|
||||||
|
zap.String("mode", cfg.Mode),
|
||||||
|
zap.String("device", devName),
|
||||||
|
zap.String("interface", cfg.InterfaceIP))
|
||||||
|
|
||||||
|
// 7. 阻塞等待退出信号
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sig
|
||||||
|
logger.Info("正在退出...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePrivateKey 返回私钥:配置给出则解析,否则生成并写入 private_key_file
|
||||||
|
func resolvePrivateKey(cfg *Config, logger *zap.Logger) (wgtypes.Key, error) {
|
||||||
|
if cfg.PrivateKey != "" {
|
||||||
|
return wgtypes.ParseKey(cfg.PrivateKey)
|
||||||
|
}
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return wgtypes.Key{}, err
|
||||||
|
}
|
||||||
|
if cfg.PrivateKeyFile != "" {
|
||||||
|
if werr := os.WriteFile(cfg.PrivateKeyFile, []byte(key.String()), 0600); werr != nil {
|
||||||
|
logger.Warn("写入私钥文件失败", zap.Error(werr))
|
||||||
|
} else {
|
||||||
|
logger.Info("已生成并写入私钥", zap.String("file", cfg.PrivateKeyFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignInterfaceIP 给隧道接口分配 IP 并 up(best-effort,失败仅告警)
|
||||||
|
func assignInterfaceIP(name, cidr string, logger *zap.Logger) {
|
||||||
|
if cidr == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ip, ipNet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("解析隧道 IP 失败", zap.String("cidr", cidr), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
run(logger, "ip", "addr", "add", cidr, "dev", name)
|
||||||
|
run(logger, "ip", "link", "set", name, "up")
|
||||||
|
case "windows":
|
||||||
|
// ipNet.Mask 已是网络掩码字节,转点分十进制
|
||||||
|
m := ipNet.Mask
|
||||||
|
maskStr := fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3])
|
||||||
|
run(logger, "netsh", "interface", "ip", "set", "address",
|
||||||
|
"name="+name, "static", ip.String(), maskStr)
|
||||||
|
default:
|
||||||
|
logger.Warn("不支持的平台,请手动配置隧道 IP", zap.String("os", runtime.GOOS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run 执行外部命令(best-effort,输出仅调试)
|
||||||
|
func run(logger *zap.Logger, name string, args ...string) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("执行命令失败(可忽略,手动配置即可)",
|
||||||
|
zap.String("cmd", name), zap.String("args", strings.Join(args, " ")),
|
||||||
|
zap.Error(err), zap.String("output", string(out)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newLogger 创建生产级 logger
|
||||||
|
func newLogger() *zap.Logger {
|
||||||
|
l, err := zap.NewProduction()
|
||||||
|
if err != nil {
|
||||||
|
l = zap.NewNop()
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLayers 将配置中的层级名列表解析为 connect.Layer 列表,任一无法解析则返回错误
|
||||||
|
func parseLayers(names []string) ([]connect.Layer, error) {
|
||||||
|
layers := make([]connect.Layer, 0, len(names))
|
||||||
|
for _, n := range names {
|
||||||
|
l, ok := connect.ParseLayer(n)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("未知传输层: %q", n)
|
||||||
|
}
|
||||||
|
layers = append(layers, l)
|
||||||
|
}
|
||||||
|
return layers, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"network_id": "net1",
|
||||||
|
"listen_port": 51820,
|
||||||
|
"mtu": 1420,
|
||||||
|
"private_key": "",
|
||||||
|
"private_key_file": "key.txt",
|
||||||
|
"interface": "10.0.0.1/24",
|
||||||
|
"mode": "enhanced",
|
||||||
|
"peers": [
|
||||||
|
{
|
||||||
|
"public_key": "<对端机器 -genkey 生成的私钥对应的公钥>",
|
||||||
|
"endpoint": "对端公网IP:51820",
|
||||||
|
"allowed_ips": ["10.0.0.2/32"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ice": { "stun": [], "turn": [] }
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# meshray-core 独立运行配置(类 frp 风格,TOML)
|
||||||
|
# 用法: meshray-core -c meshray-core.toml
|
||||||
|
# 也兼容旧 JSON 配置(扩展名 .json 自动按 JSON 解析)。
|
||||||
|
|
||||||
|
# 网络标识,同网段节点需一致
|
||||||
|
network_id = "net1"
|
||||||
|
|
||||||
|
# 本地监听端口(WireGuard / 增强模式收发端口)
|
||||||
|
listen_port = 51820
|
||||||
|
|
||||||
|
# 隧道 MTU(0 则默认 1420)
|
||||||
|
mtu = 1420
|
||||||
|
|
||||||
|
# 私钥:留空则自动生成并写入 private_key_file
|
||||||
|
private_key = ""
|
||||||
|
private_key_file = "key.txt"
|
||||||
|
|
||||||
|
# 本机隧道 IP(CIDR)
|
||||||
|
interface = "10.0.0.1/24"
|
||||||
|
|
||||||
|
# 运行模式:
|
||||||
|
# enhanced = 经 9 层传输接管 WG 收发(支持自动降级 / 手动层级)
|
||||||
|
# native = 裸 WireGuard UDP 直连(不经本仓传输层)
|
||||||
|
mode = "enhanced"
|
||||||
|
|
||||||
|
# 传输层级策略:
|
||||||
|
# auto = 默认,9 层自动降级(直连 → 中继 → ... → WS/WSS)
|
||||||
|
# manual = 仅使用下方 layers 指定的层
|
||||||
|
layer_strategy = "auto"
|
||||||
|
|
||||||
|
# 手动层级列表(仅 layer_strategy = "manual" 生效)。
|
||||||
|
# 单元素 = 强制固定该层,禁用降级,例如强制走 TURN 中继:["TURN-TCP"]
|
||||||
|
# 多元素 = 按列表顺序自定义降级链,例如直连优先、不通再中继:["Direct-UDP","TURN-TCP"]
|
||||||
|
# 可选值:Direct-UDP / FakeTCP / RealTCP / TURN-UDP / TURN-QUIC / TURN-TCP /
|
||||||
|
# TURN-TLS / WebRTC / WS(或 WSS)
|
||||||
|
# 注意:该键须置于 [[peers]] 之前(TOML 中 [[peers]] 后键会被归入 peers 表)。
|
||||||
|
layers = ["Direct-UDP"]
|
||||||
|
|
||||||
|
# 对端列表
|
||||||
|
[[peers]]
|
||||||
|
public_key = "对端公钥base64" # 必填
|
||||||
|
endpoint = "203.0.113.2:51820" # 增强模式经 9 层拨号对端;留空则仅作为接收方
|
||||||
|
allowed_ips = ["10.0.0.2/32"]
|
||||||
|
|
||||||
|
# 可选第二个对端
|
||||||
|
# [[peers]]
|
||||||
|
# public_key = "另一个对端公钥base64"
|
||||||
|
# endpoint = "198.51.100.7:51820"
|
||||||
|
# allowed_ips = ["10.0.0.3/32"]
|
||||||
|
|
||||||
|
# STUN/TURN 服务器(增强模式打洞/中继用;手动 TURN/WebRTC 层级必需)
|
||||||
|
[ice]
|
||||||
|
stun = ["stun:stun.l.google.com:19302"]
|
||||||
|
turn = ["turn:your.turn.server:3478"]
|
||||||
-272
@@ -1,272 +0,0 @@
|
|||||||
# MeshRay-Core 架构规范
|
|
||||||
|
|
||||||
> Core 是通用的数据传输引擎,通过 ProtocolPlugin 接口适配不同协议。当前默认内置 WG 插件。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
core/
|
|
||||||
├── core.go # 进程入口
|
|
||||||
├── engine.go # 引擎实例
|
|
||||||
├── metrics.go # 监控指标
|
|
||||||
│
|
|
||||||
├── connect/ # 建连层
|
|
||||||
│ ├── strategy.go # 策略调度
|
|
||||||
│ ├── stun.go # STUN 协议
|
|
||||||
│ ├── direct.go # Layer 1
|
|
||||||
│ ├── fake_tcp.go # Layer 2
|
|
||||||
│ ├── real_tcp.go # Layer 3
|
|
||||||
│ ├── turn.go # Layer 4/6/7
|
|
||||||
│ ├── turn_quic.go # Layer 5
|
|
||||||
│ ├── ice.go # Layer 8
|
|
||||||
│ └── ws.go # Layer 9
|
|
||||||
│
|
|
||||||
├── transport/ # 传输层
|
|
||||||
│ ├── conn_manager.go # 连接索引
|
|
||||||
│ ├── relay.go # 转发循环
|
|
||||||
│ └── plugin.go # Plugin 接口
|
|
||||||
│
|
|
||||||
└── plugins/ # 协议插件
|
|
||||||
└── wg/
|
|
||||||
└── wgparse.go # WG 插件实现
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、各文件职责
|
|
||||||
|
|
||||||
### 2.1 根目录(4 个文件)
|
|
||||||
|
|
||||||
| 文件 | 职责 | 持有什么 | 不做什么 |
|
|
||||||
|------|------|---------|----------|
|
|
||||||
| `core.go` | 进程入口,管理多个 Engine | `map[engineID]*Engine` | 不做建连、不转发数据 |
|
|
||||||
| `engine.go` | 一个组网的引擎实例 | strategy、conn_manager、relay、plugin | 不直接调用 connect,由 relay 调用 |
|
|
||||||
| `metrics.go` | 监控指标采集 | 原子计数器(连接数、字节数、切换次数) | 不做业务逻辑 |
|
|
||||||
|
|
||||||
**关键关系**:
|
|
||||||
- `core.go` 持有多个 `engine.go`
|
|
||||||
- `internal/ctr/ctr.go` 直接调用 `core.go` 和 `engine.go` (进程内函数调用)
|
|
||||||
- `engine.go` 持有 connect/、transport/、plugins/ 的实例
|
|
||||||
|
|
||||||
### 2.2 connect/(9 个文件)— 建连层
|
|
||||||
|
|
||||||
**职责**:通过各种网络方式建立连接,最终返回 `net.Conn`。
|
|
||||||
|
|
||||||
**对外暴露的唯一入口**:`strategy.go` 的 `Connect()` 方法。其他 connect 文件只被 `strategy.go` 调用。
|
|
||||||
|
|
||||||
| 文件 | 对应层级 | 职责 | 返回什么 |
|
|
||||||
|------|---------|------|---------|
|
|
||||||
| `strategy.go` | 全部 | 按优先级尝试各层,不通自动切换,定期探测恢复 | `net.Conn` + 当前层级名 |
|
|
||||||
| `stun.go` | 被 direct/ice 调用 | STUN 协议:发送 Binding Request,获取本机公网地址 | `*net.UDPAddr`(地址,不是连接) |
|
|
||||||
| `direct.go` | Layer 1 | 调用 stun 获取候选地址,然后 UDP 打洞 | `net.Conn` |
|
|
||||||
| `fake_tcp.go` | Layer 2 | UDP 包外层封装 TCP 头部,欺骗防火墙 | `net.Conn` |
|
|
||||||
| `real_tcp.go` | Layer 3 | 真正的 TCP 直连打洞 | `net.Conn` |
|
|
||||||
| `turn.go` | Layer 4/6/7 | TURN 协议协商(Allocate/Permission/ChannelBind),参数区分 UDP/TCP/TLS | `net.Conn` |
|
|
||||||
| `turn_quic.go` | Layer 5 | TURN-QUIC 私有扩展(RFC 9000) | `net.Conn` |
|
|
||||||
| `ice.go` | Layer 8 | ICE 协商 + WebRTC DataChannel,内部调用 stun 收集候选 | `net.Conn` |
|
|
||||||
| `ws.go` | Layer 9 | WS/WSS 握手 + 帧收发 + 身份标识 | `net.Conn` |
|
|
||||||
|
|
||||||
**9 层完整编号**:
|
|
||||||
|
|
||||||
| 层级 | 链路名称 | 文件 | 传输方式 | 穿透力 |
|
|
||||||
|------|---------|------|---------|--------|
|
|
||||||
| 1 | Direct-UDP | direct.go | P2P 直连 | 弱(性能最好) |
|
|
||||||
| 2 | Direct-FakeTCP | fake_tcp.go | P2P 直连 | 弱 |
|
|
||||||
| 3 | Direct-RealTCP | real_tcp.go | P2P 直连 | 中 |
|
|
||||||
| 4 | TURN-UDP | turn.go | 中继 | 中 |
|
|
||||||
| 5 | TURN-QUIC | turn_quic.go | 中继 | 中 |
|
|
||||||
| 6 | TURN-TCP | turn.go | 中继 | 强 |
|
|
||||||
| 7 | TURN-TLS | turn.go | 中继 | 强 |
|
|
||||||
| 8 | WebRTC | ice.go | ICE/TURN | 强 |
|
|
||||||
| 9 | WS/WSS | ws.go | 隧道 | 最强(兜底) |
|
|
||||||
|
|
||||||
**strategy.go 的自动切换逻辑**:
|
|
||||||
- 单包超时 500ms → 切到下一层
|
|
||||||
- 10s 滑动窗口丢包率 > 10% → 切到下一层
|
|
||||||
- 当前在第 N 层时,每 30s 探测 Layer 1 → 连续 2 次成功直接切回 Layer 1(不逐层回退)
|
|
||||||
|
|
||||||
**stun.go 的特殊地位**:唯一被多处调用的 connect 文件(direct.go 和 ice.go 都需要它),所以独立存在。
|
|
||||||
|
|
||||||
### 2.3 transport/(3 个文件)— 传输层
|
|
||||||
|
|
||||||
**职责**:用 `net.Conn` 转发数据。不感知具体协议,通过 ProtocolPlugin 接口适配。
|
|
||||||
|
|
||||||
| 文件 | 职责 | 不做什么 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `conn_manager.go` | 连接索引:`peer_key → net.Conn` 的映射 | 不做建连、不转发数据 |
|
|
||||||
| `relay.go` | Read/Write 循环:从本地端口收包 → 查路由 → 通过 conn 发送;从 conn 收包 → 发到本地端口 | 不做建连 |
|
|
||||||
| `plugin.go` | 定义 ProtocolPlugin 接口 | 不实现任何协议 |
|
|
||||||
|
|
||||||
**relay.go 的工作流程**:
|
|
||||||
|
|
||||||
```
|
|
||||||
本地端口收到 WG 密文包
|
|
||||||
→ 调用 plugin.IsControlPacket() 判断包类型
|
|
||||||
→ true:控制包,按已建链路透传
|
|
||||||
→ 调用 plugin.IsDataPacket() 判断
|
|
||||||
→ true:调用 plugin.ExtractRouteID() 提取路由标识
|
|
||||||
→ 查路由标识映射表 → 发往对应本地端口
|
|
||||||
→ 都不是:丢弃
|
|
||||||
```
|
|
||||||
|
|
||||||
**关键**:relay.go 不知道 WireGuard,不知道 receiver index,只知道 route_id。
|
|
||||||
|
|
||||||
### 2.4 plugins/wg/(1 个文件)— 协议插件
|
|
||||||
|
|
||||||
**职责**:实现 ProtocolPlugin 接口,处理 WG 协议特有的包解析。
|
|
||||||
|
|
||||||
| 文件 | 职责 | 不做什么 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `wgparse.go` | 实现 `IsDataPacket`/`ExtractRouteID`/`IsControlPacket` | 不做建连、不转发数据 |
|
|
||||||
|
|
||||||
**WG 插件的具体实现**:
|
|
||||||
|
|
||||||
| 方法 | 逻辑 |
|
|
||||||
|------|------|
|
|
||||||
| `IsControlPacket(packet)` | `packet[0]` ∈ {1, 2, 3} → true |
|
|
||||||
| `IsDataPacket(packet)` | `packet[0]` == 4 → true |
|
|
||||||
| `ExtractRouteID(packet)` | 读取 `packet[4:8]`,网络字节序解析为 uint32(即 WG receiver index) |
|
|
||||||
|
|
||||||
### 2.5 plugins/wg/(1 个文件)
|
|
||||||
|
|
||||||
| 文件 | 职责 |
|
|
||||||
|------|------|
|
|
||||||
| `wgparse.go` | WireGuard 数据包解析和封装 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、分层架构图
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ internal/ctr/ctr.go │
|
|
||||||
│ 直接调用 Core (进程内函数调用) │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│ 函数调用
|
|
||||||
┌────────────────────────▼────────────────────────────────┐
|
|
||||||
│ core.go │
|
|
||||||
│ 管理多个 Engine 实例 │
|
|
||||||
└───┬─────────────────────────────────────────────────────┘
|
|
||||||
│ 每个组网一个 Engine
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ engine.go │
|
|
||||||
│ 持有:strategy + conn_manager + relay + plugin │
|
|
||||||
│ 协调 connect/ 和 transport/ 工作 │
|
|
||||||
└───┬─────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
├──────────────────────────────────────────┐
|
|
||||||
▼ ▼
|
|
||||||
┌───────────────────────┐ ┌───────────────────────┐
|
|
||||||
│ connect/ │ │ transport/ │
|
|
||||||
│ 建连层 │ │ 传输层 │
|
|
||||||
│ │ │ │
|
|
||||||
│ strategy.go │ 返回 │ relay.go │
|
|
||||||
│ ├─ direct.go (L1) │ net.Conn├─ plugin.go │
|
|
||||||
│ ├─ fake_tcp.go (L2) │────────►│ │ ProtocolPlugin │
|
|
||||||
│ ├─ real_tcp.go (L3) │ │ │ │
|
|
||||||
│ ├─ turn.go (L4/6/7) │ │ 插件调用 │
|
|
||||||
│ ├─ turn_quic.go(L5) │ │ ▼ │
|
|
||||||
│ ├─ ice.go (L8) │ │ plugins/wg/ │
|
|
||||||
│ └─ ws.go (L9) │ │ └─ wgparse.go │
|
|
||||||
│ │ │ │
|
|
||||||
│ stun.go (被 direct/ice 调用) │ conn_manager.go │
|
|
||||||
└───────────────────────┘ └───────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、调用关系
|
|
||||||
|
|
||||||
### 4.1 Engine 创建时
|
|
||||||
|
|
||||||
```
|
|
||||||
engine.go
|
|
||||||
→ 创建 WGPlugin(plugins/wg/wgparse.go)
|
|
||||||
→ 创建 Relay,传入 plugin(transport/relay.go)
|
|
||||||
→ 创建 Strategy(connect/strategy.go)
|
|
||||||
→ 创建 ConnManager(transport/conn_manager.go)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Bind 流程
|
|
||||||
|
|
||||||
```
|
|
||||||
ctr 直接调用:Bind()
|
|
||||||
→ engine.go 接收函数调用
|
|
||||||
→ engine.go 调用 strategy.Connect()
|
|
||||||
→ strategy 按优先级尝试各层
|
|
||||||
→ Layer 1: direct.go 调用 stun.go 获取候选,尝试 UDP 打洞
|
|
||||||
→ 不通?→ Layer 4: turn.go 调用 TURN 协商
|
|
||||||
→ 不通?→ Layer 9: ws.go 调用 WS 握手
|
|
||||||
→ 返回 net.Conn + 当前层级名
|
|
||||||
→ engine.go 把 net.Conn 注册到 conn_manager
|
|
||||||
→ engine.go 启动 relay 的 Read/Write 循环
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 数据转发流程
|
|
||||||
|
|
||||||
```
|
|
||||||
WG 发出密文包 → 本地端口
|
|
||||||
→ relay.go 收到
|
|
||||||
→ 调用 plugin.IsControlPacket()
|
|
||||||
→ true:按已建链路透传(conn_manager 查 conn)
|
|
||||||
→ 调用 plugin.IsDataPacket()
|
|
||||||
→ true:调用 plugin.ExtractRouteID() 获取 route_id
|
|
||||||
→ 查路由标识映射表 → 找到本地端口 → 发送
|
|
||||||
→ 都不是:丢弃
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、通用层与插件层的边界
|
|
||||||
|
|
||||||
| 层 | 知道什么 | 不知道什么 |
|
|
||||||
|---|---------|-----------|
|
|
||||||
| **connect/** | 网络协议(STUN/TURN/WS/WebRTC) | WireGuard、route_id |
|
|
||||||
| **transport/** | net.Conn、route_id、ProtocolPlugin 接口 | WireGuard、receiver index |
|
|
||||||
| **plugins/wg/** | WG 包格式、receiver index | 网络连接、net.Conn |
|
|
||||||
| **engine.go** | 协调 connect/ 和 transport/ | WG 包格式细节 |
|
|
||||||
|
|
||||||
**如果将来要支持其他协议**:
|
|
||||||
- 新建 `plugins/xxx/xxxparse.go`
|
|
||||||
- 实现 `ProtocolPlugin` 接口的三个方法
|
|
||||||
- `engine.go` 里换成 `xxx.NewPlugin()`
|
|
||||||
- connect/、transport/、core.go 的代码完全不用改
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、Core 接口(直接被 ctr 调用)
|
|
||||||
|
|
||||||
| 方法 | 调用方 | 说明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| `CreateEngine` | ctr | 创建一个 Engine 实例(直接函数调用) |
|
|
||||||
| `Bind` | ctr | 为每个 Peer 开启本地端口,开始建链 |
|
|
||||||
| `Unbind` | ctr | 停止指定 Peer 的端口监听 |
|
|
||||||
| `Start` | ctr | 启动转发主循环 |
|
|
||||||
| `Stop` | ctr | 停止 Engine |
|
|
||||||
| `GetStatus` | ctr | 查询 Engine 状态 |
|
|
||||||
| `NotifyPeerInfo` | ctr | 下发对端候选地址和 route_id |
|
|
||||||
|
|
||||||
**实现位置**:
|
|
||||||
- 所有方法都在 `engine.go` 中实现
|
|
||||||
- `core.go` 提供 Engine 实例管理
|
|
||||||
- ctr通过`coreInst.CreateEngine(...)`直接调用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、文件清单汇总
|
|
||||||
|
|
||||||
| 目录 | 文件数 | 文件 |
|
|
||||||
|------|--------|------|
|
|
||||||
| 根目录 | 3 | core.go, engine.go, metrics.go |
|
|
||||||
| connect/ | 9 | strategy.go, stun.go, direct.go, fake_tcp.go, real_tcp.go, turn.go, turn_quic.go, ice.go, ws.go |
|
|
||||||
| transport/ | 3 | conn_manager.go, relay.go, plugin.go |
|
|
||||||
| plugins/wg/ | 1 | wgparse.go |
|
|
||||||
| **总计** | **16** | |
|
|
||||||
|
|
||||||
**已删除的文件**:
|
|
||||||
- ~~grpc_service.go~~ - 不再需要(改为直接函数调用)
|
|
||||||
- ~~pool/connpool.go~~ - 不再需要(无连接池)
|
|
||||||
- ~~proto/core.proto~~ - 不再需要 gRPC
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DirectFactory Direct-UDP 直连工厂(Layer 1)
|
|
||||||
type DirectFactory struct {
|
|
||||||
stunServers []string
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDirectFactory 创建 Direct-UDP 工厂
|
|
||||||
func NewDirectFactory(stunServers []string, logger *zap.Logger) *DirectFactory {
|
|
||||||
return &DirectFactory{
|
|
||||||
stunServers: stunServers,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
func (f *DirectFactory) Layer() Layer {
|
|
||||||
return LayerDirectUDP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name 返回传输方式名称
|
|
||||||
func (f *DirectFactory) Name() string {
|
|
||||||
return "Direct-UDP"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 建立 Direct-UDP 直连
|
|
||||||
func (f *DirectFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
|
||||||
f.logger.Info("开始建立 Direct-UDP 直连",
|
|
||||||
zap.String("peer_id", config.PeerID))
|
|
||||||
|
|
||||||
servers := f.stunServers
|
|
||||||
if len(servers) == 0 {
|
|
||||||
servers = config.STUNServers
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(servers) == 0 {
|
|
||||||
f.logger.Warn("未配置 STUN 服务器列表,仅尝试内部 P2P 打洞")
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidates []string
|
|
||||||
if len(servers) > 0 {
|
|
||||||
// 1. 创建 STUN 客户端收集候选地址
|
|
||||||
stun := NewSTUNClient(servers, f.logger)
|
|
||||||
candidates = stun.CollectCandidates()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
f.logger.Warn("未能收集到任何 STUN 候选地址,回退至 PeerID")
|
|
||||||
// As a fallback, maybe PeerID contains IP:PORT
|
|
||||||
candidates = append(candidates, config.PeerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Info("STUN 候选地址收集完成",
|
|
||||||
zap.Strings("candidates", candidates))
|
|
||||||
|
|
||||||
// 2. 实际 P2P 连接尝试
|
|
||||||
f.logger.Warn("当前尝试所有候选地址...")
|
|
||||||
|
|
||||||
dialer := &net.Dialer{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立 UDP 连接并返回最近可用的
|
|
||||||
var lastErr error
|
|
||||||
for _, candidate := range candidates {
|
|
||||||
if candidate == "" { continue }
|
|
||||||
|
|
||||||
conn, err := dialer.DialContext(ctx, "udp", candidate)
|
|
||||||
if err == nil {
|
|
||||||
f.logger.Info("Direct-UDP 直连建立成功",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.String("remote_addr", conn.RemoteAddr().String()))
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Warn("候选地址连接失败",
|
|
||||||
zap.String("candidate", candidate),
|
|
||||||
zap.Error(err))
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("所有候选地址 UDP 连接均失败,最后错误:%w", lastErr)
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FakeTCPConn FakeTCP 连接(UDP 包封装为 TCP 流)
|
|
||||||
type FakeTCPConn struct {
|
|
||||||
conn net.Conn
|
|
||||||
mu sync.Mutex
|
|
||||||
closed bool
|
|
||||||
readBuffer []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFakeTCPConn 创建 FakeTCP 连接
|
|
||||||
func NewFakeTCPConn(conn net.Conn) *FakeTCPConn {
|
|
||||||
return &FakeTCPConn{
|
|
||||||
conn: conn,
|
|
||||||
readBuffer: make([]byte, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read 读取数据(带长度前缀解析)
|
|
||||||
func (c *FakeTCPConn) Read(b []byte) (int, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// 如果缓冲区有数据,直接返回
|
|
||||||
if len(c.readBuffer) > 0 {
|
|
||||||
n := copy(b, c.readBuffer)
|
|
||||||
c.readBuffer = c.readBuffer[n:]
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取长度前缀(4 字节)
|
|
||||||
var length uint32
|
|
||||||
if err := binary.Read(c.conn, binary.BigEndian, &length); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制最大长度(防止恶意攻击)
|
|
||||||
if length > 65535 {
|
|
||||||
return 0, fmt.Errorf("packet too large: %d bytes", length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取实际数据
|
|
||||||
data := make([]byte, length)
|
|
||||||
if _, err := io.ReadFull(c.conn, data); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回请求的数据
|
|
||||||
n := copy(b, data)
|
|
||||||
if n < len(data) {
|
|
||||||
// 剩余数据存入缓冲区
|
|
||||||
c.readBuffer = data[n:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 写入数据(添加 4 字节长度前缀)
|
|
||||||
func (c *FakeTCPConn) Write(b []byte) (int, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return 0, fmt.Errorf("connection closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入长度前缀
|
|
||||||
length := uint32(len(b))
|
|
||||||
if err := binary.Write(c.conn, binary.BigEndian, length); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入实际数据
|
|
||||||
n, err := c.conn.Write(b)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭连接
|
|
||||||
func (c *FakeTCPConn) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.closed = true
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr 本地地址
|
|
||||||
func (c *FakeTCPConn) LocalAddr() net.Addr {
|
|
||||||
return c.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr 远程地址
|
|
||||||
func (c *FakeTCPConn) RemoteAddr() net.Addr {
|
|
||||||
return c.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDeadline 设置截止时间
|
|
||||||
func (c *FakeTCPConn) SetDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline 设置读截止时间
|
|
||||||
func (c *FakeTCPConn) SetReadDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline 设置写截止时间
|
|
||||||
func (c *FakeTCPConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialFakeTCP 拨号 FakeTCP 连接
|
|
||||||
func DialFakeTCP(ctx context.Context, network, addr string, logger *zap.Logger) (net.Conn, error) {
|
|
||||||
logger.Debug("dialing FakeTCP", zap.String("addr", addr))
|
|
||||||
|
|
||||||
// 建立 TCP 连接
|
|
||||||
conn, err := (&net.Dialer{}).DialContext(ctx, network, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to dial TCP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 包装为 FakeTCP 连接
|
|
||||||
return NewFakeTCPConn(conn), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenFakeTCP 监听 FakeTCP 端口
|
|
||||||
func ListenFakeTCP(network, addr string, logger *zap.Logger) (net.Listener, error) {
|
|
||||||
logger.Info("listening FakeTCP", zap.String("addr", addr))
|
|
||||||
|
|
||||||
// 监听 TCP 端口
|
|
||||||
listener, err := net.Listen(network, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to listen TCP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &fakeTCPListener{
|
|
||||||
Listener: listener,
|
|
||||||
logger: logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fakeTCPListener FakeTCP 监听器
|
|
||||||
type fakeTCPListener struct {
|
|
||||||
net.Listener
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept 接受连接并包装为 FakeTCPConn
|
|
||||||
func (l *fakeTCPListener) Accept() (net.Conn, error) {
|
|
||||||
conn, err := l.Listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.logger.Debug("accepted FakeTCP connection", zap.String("addr", conn.RemoteAddr().String()))
|
|
||||||
return NewFakeTCPConn(conn), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FakeTCPFactory FakeTCP 传输工厂
|
|
||||||
type FakeTCPFactory struct {
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFakeTCPFactory 创建 FakeTCP 工厂
|
|
||||||
func NewFakeTCPFactory(logger *zap.Logger) *FakeTCPFactory {
|
|
||||||
return &FakeTCPFactory{
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
func (f *FakeTCPFactory) Layer() Layer {
|
|
||||||
return LayerFakeTCP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name 返回名称
|
|
||||||
func (f *FakeTCPFactory) Name() string {
|
|
||||||
return "FakeTCP"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 建立 FakeTCP 连接
|
|
||||||
func (f *FakeTCPFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
|
||||||
f.logger.Info("开始建立 FakeTCP 连接",
|
|
||||||
zap.String("peer_id", config.PeerID))
|
|
||||||
|
|
||||||
// 1. 解析对端地址(PeerID 格式应为 "ip:port")
|
|
||||||
if config.PeerID == "" {
|
|
||||||
return nil, fmt.Errorf("PeerID 为空")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 建立 TCP 连接
|
|
||||||
dialer := &net.Dialer{Timeout: config.Timeout}
|
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", config.PeerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("TCP 连接失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 包装为 FakeTCP 连接(UDP 包封装为 TCP 流)
|
|
||||||
fakeConn := NewFakeTCPConn(conn)
|
|
||||||
|
|
||||||
f.logger.Info("FakeTCP 连接建立成功",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.String("local_addr", conn.LocalAddr().String()),
|
|
||||||
zap.String("remote_addr", conn.RemoteAddr().String()))
|
|
||||||
|
|
||||||
return fakeConn, nil
|
|
||||||
}
|
|
||||||
@@ -1,585 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pion/webrtc/v3"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ICEConfig ICE 配置
|
|
||||||
type ICEConfig struct {
|
|
||||||
STUNServers []string
|
|
||||||
TURNServers []TURNServerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// TURNServerConfig TURN 服务器配置
|
|
||||||
type TURNServerConfig struct {
|
|
||||||
URLs []string
|
|
||||||
Username string
|
|
||||||
Credential string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ICEServer ICE 服务器配置(别名,保持兼容)
|
|
||||||
type ICEICEServer = TURNServerConfig
|
|
||||||
|
|
||||||
// ICEClient ICE 客户端(ICE协商 + WebRTC DataChannel)
|
|
||||||
type ICEClient struct {
|
|
||||||
config *ICEConfig
|
|
||||||
logger *zap.Logger
|
|
||||||
api *webrtc.API
|
|
||||||
peerConns map[string]*webrtc.PeerConnection // peerID -> PeerConnection
|
|
||||||
dataChannels map[string]*webrtc.DataChannel // peerID -> DataChannel
|
|
||||||
signalingCh map[string]chan SignalMessage // peerID -> 信令通道
|
|
||||||
mu sync.RWMutex
|
|
||||||
onSignal func(peerID string, signal SignalMessage) // 信令回调
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignalMessage 信令消息
|
|
||||||
type SignalMessage struct {
|
|
||||||
Type string `json:"type"` // "offer" | "answer" | "candidate"
|
|
||||||
SDP string `json:"sdp,omitempty"`
|
|
||||||
Candidate string `json:"candidate,omitempty"`
|
|
||||||
Target string `json:"target"` // 目标 PeerID
|
|
||||||
Source string `json:"source"` // 来源 PeerID
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewICEClient 创建 ICE 客户端
|
|
||||||
func NewICEClient(config *ICEConfig, logger *zap.Logger) *ICEClient {
|
|
||||||
// 创建 WebRTC API(使用默认配置)
|
|
||||||
api := webrtc.NewAPI()
|
|
||||||
|
|
||||||
return &ICEClient{
|
|
||||||
config: config,
|
|
||||||
logger: logger,
|
|
||||||
api: api,
|
|
||||||
peerConns: make(map[string]*webrtc.PeerConnection),
|
|
||||||
dataChannels: make(map[string]*webrtc.DataChannel),
|
|
||||||
signalingCh: make(map[string]chan SignalMessage),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createPeerConnection 创建 PeerConnection
|
|
||||||
func (c *ICEClient) createPeerConnection(peerID string) (*webrtc.PeerConnection, error) {
|
|
||||||
// 构建 ICE 服务器配置
|
|
||||||
var iceServers []webrtc.ICEServer
|
|
||||||
|
|
||||||
// 添加 STUN 服务器
|
|
||||||
for _, stun := range c.config.STUNServers {
|
|
||||||
iceServers = append(iceServers, webrtc.ICEServer{
|
|
||||||
URLs: []string{stun},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 TURN 服务器
|
|
||||||
for _, turn := range c.config.TURNServers {
|
|
||||||
iceServers = append(iceServers, webrtc.ICEServer{
|
|
||||||
URLs: turn.URLs,
|
|
||||||
Username: turn.Username,
|
|
||||||
Credential: turn.Credential,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 PeerConnection 配置
|
|
||||||
config := webrtc.Configuration{
|
|
||||||
ICEServers: iceServers,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 PeerConnection
|
|
||||||
pc, err := c.api.NewPeerConnection(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建 PeerConnection 失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 PeerConnection
|
|
||||||
c.mu.Lock()
|
|
||||||
c.peerConns[peerID] = pc
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
c.logger.Info("创建 PeerConnection",
|
|
||||||
zap.String("peer_id", peerID),
|
|
||||||
zap.Int("ice_servers", len(iceServers)))
|
|
||||||
|
|
||||||
return pc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateOffer 创建 Offer(主动发起方)
|
|
||||||
func (c *ICEClient) CreateOffer(ctx context.Context, peerID string) (*SignalMessage, error) {
|
|
||||||
pc, err := c.createPeerConnection(peerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 DataChannel
|
|
||||||
dc, err := pc.CreateDataChannel("meshray", nil)
|
|
||||||
if err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return nil, fmt.Errorf("创建 DataChannel 失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 DataChannel 处理器
|
|
||||||
dc.OnOpen(func() {
|
|
||||||
c.logger.Info("DataChannel 已打开", zap.String("peer_id", peerID))
|
|
||||||
})
|
|
||||||
|
|
||||||
dc.OnClose(func() {
|
|
||||||
c.logger.Info("DataChannel 已关闭", zap.String("peer_id", peerID))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 存储 DataChannel
|
|
||||||
c.mu.Lock()
|
|
||||||
c.dataChannels[peerID] = dc
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// 创建 Offer
|
|
||||||
offer, err := pc.CreateOffer(nil)
|
|
||||||
if err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return nil, fmt.Errorf("创建 Offer 失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置本地描述
|
|
||||||
if err := pc.SetLocalDescription(offer); err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return nil, fmt.Errorf("设置本地描述失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 ICE 候选回调
|
|
||||||
c.setupICECandidateHandler(pc, peerID)
|
|
||||||
|
|
||||||
return &SignalMessage{
|
|
||||||
Type: "offer",
|
|
||||||
SDP: offer.SDP,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleAnswer 处理 Answer(主动发起方收到应答)
|
|
||||||
func (c *ICEClient) HandleAnswer(peerID string, answer SignalMessage) error {
|
|
||||||
c.mu.RLock()
|
|
||||||
pc, exists := c.peerConns[peerID]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("未找到 PeerConnection:%s", peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置远程描述
|
|
||||||
if err := pc.SetRemoteDescription(webrtc.SessionDescription{
|
|
||||||
Type: webrtc.SDPTypeAnswer,
|
|
||||||
SDP: answer.SDP,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("设置远程描述失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Info("已设置 Answer", zap.String("peer_id", peerID))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleOffer 处理 Offer(被动接收方)
|
|
||||||
func (c *ICEClient) HandleOffer(ctx context.Context, peerID string, offer SignalMessage) (*SignalMessage, error) {
|
|
||||||
pc, err := c.createPeerConnection(peerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置远程描述
|
|
||||||
if err := pc.SetRemoteDescription(webrtc.SessionDescription{
|
|
||||||
Type: webrtc.SDPTypeOffer,
|
|
||||||
SDP: offer.SDP,
|
|
||||||
}); err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return nil, fmt.Errorf("设置远程描述失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听 DataChannel
|
|
||||||
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
|
|
||||||
c.logger.Info("收到 DataChannel", zap.String("peer_id", peerID), zap.String("label", dc.Label()))
|
|
||||||
|
|
||||||
// 存储 DataChannel
|
|
||||||
c.mu.Lock()
|
|
||||||
c.dataChannels[peerID] = dc
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
dc.OnOpen(func() {
|
|
||||||
c.logger.Info("DataChannel 已打开", zap.String("peer_id", peerID))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建 Answer
|
|
||||||
answer, err := pc.CreateAnswer(nil)
|
|
||||||
if err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return nil, fmt.Errorf("创建 Answer 失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置本地描述
|
|
||||||
if err := pc.SetLocalDescription(answer); err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return nil, fmt.Errorf("设置本地描述失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 ICE 候选回调
|
|
||||||
c.setupICECandidateHandler(pc, peerID)
|
|
||||||
|
|
||||||
return &SignalMessage{
|
|
||||||
Type: "answer",
|
|
||||||
SDP: answer.SDP,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleICECandidate 处理 ICE 候选
|
|
||||||
func (c *ICEClient) HandleICECandidate(peerID string, candidate SignalMessage) error {
|
|
||||||
c.mu.RLock()
|
|
||||||
pc, exists := c.peerConns[peerID]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("未找到 PeerConnection:%s", peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 ICE 候选
|
|
||||||
if err := pc.AddICECandidate(webrtc.ICECandidateInit{
|
|
||||||
Candidate: candidate.Candidate,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("添加 ICE 候选失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Debug("已添加 ICE 候选", zap.String("peer_id", peerID))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupICECandidateHandler 设置 ICE 候选处理器
|
|
||||||
func (c *ICEClient) setupICECandidateHandler(pc *webrtc.PeerConnection, peerID string) {
|
|
||||||
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
||||||
if candidate == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Debug("发现 ICE 候选",
|
|
||||||
zap.String("peer_id", peerID),
|
|
||||||
zap.String("candidate", candidate.String()))
|
|
||||||
|
|
||||||
// 触发信令回调
|
|
||||||
if c.onSignal != nil {
|
|
||||||
c.onSignal(peerID, SignalMessage{
|
|
||||||
Type: "candidate",
|
|
||||||
Candidate: candidate.ToJSON().Candidate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForConnection 等待连接建立
|
|
||||||
func (c *ICEClient) WaitForConnection(ctx context.Context, peerID string, timeout time.Duration) error {
|
|
||||||
c.mu.RLock()
|
|
||||||
pc, exists := c.peerConns[peerID]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("未找到 PeerConnection:%s", peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 创建连接状态通道
|
|
||||||
stateCh := make(chan webrtc.PeerConnectionState, 1)
|
|
||||||
|
|
||||||
// 监听连接状态
|
|
||||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
|
||||||
c.logger.Info("连接状态变化",
|
|
||||||
zap.String("peer_id", peerID),
|
|
||||||
zap.String("state", state.String()))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case stateCh <- state:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查当前状态
|
|
||||||
if pc.ConnectionState() == webrtc.PeerConnectionStateConnected {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待连接建立
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case state := <-stateCh:
|
|
||||||
switch state {
|
|
||||||
case webrtc.PeerConnectionStateConnected:
|
|
||||||
return nil
|
|
||||||
case webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateDisconnected:
|
|
||||||
return fmt.Errorf("连接失败:%s", state.String())
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return fmt.Errorf("等待连接超时")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDataChannel 获取 DataChannel
|
|
||||||
func (c *ICEClient) GetDataChannel(peerID string) (*webrtc.DataChannel, bool) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
dc, ok := c.dataChannels[peerID]
|
|
||||||
return dc, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClosePeer 关闭指定 Peer 的连接
|
|
||||||
func (c *ICEClient) ClosePeer(peerID string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if pc, ok := c.peerConns[peerID]; ok {
|
|
||||||
delete(c.peerConns, peerID)
|
|
||||||
if dc, ok := c.dataChannels[peerID]; ok {
|
|
||||||
dc.Close()
|
|
||||||
delete(c.dataChannels, peerID)
|
|
||||||
}
|
|
||||||
return pc.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOnSignal 设置信令回调
|
|
||||||
func (c *ICEClient) SetOnSignal(callback func(peerID string, signal SignalMessage)) {
|
|
||||||
c.onSignal = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭所有连接
|
|
||||||
func (c *ICEClient) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
for peerID, pc := range c.peerConns {
|
|
||||||
if dc, ok := c.dataChannels[peerID]; ok {
|
|
||||||
dc.Close()
|
|
||||||
}
|
|
||||||
if err := pc.Close(); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("关闭 %s 失败:%w", peerID, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.peerConns = make(map[string]*webrtc.PeerConnection)
|
|
||||||
c.dataChannels = make(map[string]*webrtc.DataChannel)
|
|
||||||
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return fmt.Errorf("关闭连接时发生错误:%v", errs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebRTCFactory WebRTC 工厂
|
|
||||||
type WebRTCFactory struct {
|
|
||||||
client *ICEClient
|
|
||||||
logger *zap.Logger
|
|
||||||
config *ICEConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWebRTCFactory 创建 WebRTC 工厂
|
|
||||||
func NewWebRTCFactory(config *ICEConfig, logger *zap.Logger) *WebRTCFactory {
|
|
||||||
return &WebRTCFactory{
|
|
||||||
client: NewICEClient(config, logger),
|
|
||||||
logger: logger,
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
func (f *WebRTCFactory) Layer() Layer {
|
|
||||||
return LayerWebRTC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name 返回名称
|
|
||||||
func (f *WebRTCFactory) Name() string {
|
|
||||||
return "WebRTC"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 建立 WebRTC 连接
|
|
||||||
// 注意:WebRTC 需要信令服务器交换 SDP,这里提供简化的直连模式
|
|
||||||
// 实际使用时需要通过信令服务器交换 Offer/Answer
|
|
||||||
func (f *WebRTCFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
|
||||||
f.logger.Info("开始建立 WebRTC 连接",
|
|
||||||
zap.String("peer_id", config.PeerID))
|
|
||||||
|
|
||||||
// WebRTC 需要信令服务器支持
|
|
||||||
// 这里返回错误,提示需要使用信令服务器
|
|
||||||
return nil, fmt.Errorf("WebRTC 需要信令服务器交换 SDP,请使用 ICEClient 配合信令服务")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClient 获取 ICE 客户端
|
|
||||||
func (f *WebRTCFactory) GetClient() *ICEClient {
|
|
||||||
return f.client
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataChannelConn DataChannel net.Conn 包装器
|
|
||||||
type DataChannelConn struct {
|
|
||||||
dc *webrtc.DataChannel
|
|
||||||
localAddr net.Addr
|
|
||||||
remoteAddr net.Addr
|
|
||||||
readCh chan []byte
|
|
||||||
readBuf []byte
|
|
||||||
mu sync.Mutex
|
|
||||||
closed bool
|
|
||||||
onClose func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDataChannelConn 创建 DataChannel 连接
|
|
||||||
func NewDataChannelConn(dc *webrtc.DataChannel, onClose func()) *DataChannelConn {
|
|
||||||
conn := &DataChannelConn{
|
|
||||||
dc: dc,
|
|
||||||
readCh: make(chan []byte, 100),
|
|
||||||
onClose: onClose,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置消息处理
|
|
||||||
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
|
||||||
conn.mu.Lock()
|
|
||||||
if conn.closed {
|
|
||||||
conn.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case conn.readCh <- msg.Data:
|
|
||||||
default:
|
|
||||||
// 缓冲区满,丢弃消息
|
|
||||||
}
|
|
||||||
conn.mu.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 设置关闭处理
|
|
||||||
dc.OnClose(func() {
|
|
||||||
conn.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read 从 DataChannel 读取数据
|
|
||||||
func (c *DataChannelConn) Read(b []byte) (n int, err error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
if c.closed {
|
|
||||||
c.mu.Unlock()
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有缓冲数据,先返回
|
|
||||||
if len(c.readBuf) > 0 {
|
|
||||||
n = copy(b, c.readBuf)
|
|
||||||
c.readBuf = c.readBuf[n:]
|
|
||||||
c.mu.Unlock()
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// 等待新数据
|
|
||||||
select {
|
|
||||||
case data := <-c.readCh:
|
|
||||||
c.mu.Lock()
|
|
||||||
if c.closed {
|
|
||||||
c.mu.Unlock()
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
n = copy(b, data)
|
|
||||||
if n < len(data) {
|
|
||||||
// 缓冲剩余数据
|
|
||||||
c.readBuf = data[n:]
|
|
||||||
}
|
|
||||||
c.mu.Unlock()
|
|
||||||
return n, nil
|
|
||||||
case <-time.After(30 * time.Second):
|
|
||||||
return 0, fmt.Errorf("读取超时")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 写入 DataChannel
|
|
||||||
func (c *DataChannelConn) Write(b []byte) (n int, err error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.dc.Send(b); err != nil {
|
|
||||||
return 0, fmt.Errorf("发送失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭连接
|
|
||||||
func (c *DataChannelConn) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.closed = true
|
|
||||||
|
|
||||||
if c.onClose != nil {
|
|
||||||
c.onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.dc.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr 返回本地地址
|
|
||||||
func (c *DataChannelConn) LocalAddr() net.Addr {
|
|
||||||
if c.localAddr == nil {
|
|
||||||
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
|
|
||||||
}
|
|
||||||
return c.localAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr 返回远程地址
|
|
||||||
func (c *DataChannelConn) RemoteAddr() net.Addr {
|
|
||||||
if c.remoteAddr == nil {
|
|
||||||
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
|
|
||||||
}
|
|
||||||
return c.remoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDeadline 设置截止时间
|
|
||||||
func (c *DataChannelConn) SetDeadline(t time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline 设置读取截止时间
|
|
||||||
func (c *DataChannelConn) SetReadDeadline(t time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline 设置写入截止时间
|
|
||||||
func (c *DataChannelConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON 序列化信令消息
|
|
||||||
func (m SignalMessage) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias SignalMessage
|
|
||||||
return json.Marshal((*Alias)(&m))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON 反序列化信令消息
|
|
||||||
func (m *SignalMessage) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias SignalMessage
|
|
||||||
var tmp Alias
|
|
||||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*m = SignalMessage(tmp)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RealTCPConn 真正的 TCP 连接(用于传输 WireGuard 密文)
|
|
||||||
// 与 FakeTCP 不同,RealTCP 不封装 UDP 包,直接传输原始数据
|
|
||||||
type RealTCPConn struct {
|
|
||||||
conn net.Conn
|
|
||||||
closed bool
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRealTCPConn 创建 RealTCP 连接
|
|
||||||
func NewRealTCPConn(conn net.Conn) *RealTCPConn {
|
|
||||||
return &RealTCPConn{
|
|
||||||
conn: conn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read 读取数据
|
|
||||||
func (c *RealTCPConn) Read(b []byte) (int, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return 0, fmt.Errorf("connection closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 写入数据
|
|
||||||
func (c *RealTCPConn) Write(b []byte) (int, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return 0, fmt.Errorf("connection closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭连接
|
|
||||||
func (c *RealTCPConn) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.closed = true
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RealTCPFactory RealTCP 传输工厂
|
|
||||||
type RealTCPFactory struct {
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRealTCPFactory 创建 RealTCP 工厂
|
|
||||||
func NewRealTCPFactory(logger *zap.Logger) *RealTCPFactory {
|
|
||||||
return &RealTCPFactory{
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
func (f *RealTCPFactory) Layer() Layer {
|
|
||||||
return LayerRealTCP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name 返回名称
|
|
||||||
func (f *RealTCPFactory) Name() string {
|
|
||||||
return "RealTCP"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 建立 RealTCP 连接
|
|
||||||
func (f *RealTCPFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
|
||||||
f.logger.Info("开始建立 RealTCP 连接",
|
|
||||||
zap.String("peer_id", config.PeerID))
|
|
||||||
|
|
||||||
// 1. 解析对端地址(PeerID 格式应为 "ip:port")
|
|
||||||
if config.PeerID == "" {
|
|
||||||
return nil, fmt.Errorf("PeerID 为空")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 建立 TCP 连接
|
|
||||||
dialer := &net.Dialer{Timeout: config.Timeout}
|
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", config.PeerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("TCP 连接失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 包装为 RealTCP 连接(直接传输原始数据)
|
|
||||||
realConn := NewRealTCPConn(conn)
|
|
||||||
|
|
||||||
f.logger.Info("RealTCP 连接建立成功",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.String("local_addr", conn.LocalAddr().String()),
|
|
||||||
zap.String("remote_addr", conn.RemoteAddr().String()))
|
|
||||||
|
|
||||||
return realConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr 本地地址
|
|
||||||
func (c *RealTCPConn) LocalAddr() net.Addr {
|
|
||||||
return c.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr 远程地址
|
|
||||||
func (c *RealTCPConn) RemoteAddr() net.Addr {
|
|
||||||
return c.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDeadline 设置截止时间
|
|
||||||
func (c *RealTCPConn) SetDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline 设置读截止时间
|
|
||||||
func (c *RealTCPConn) SetReadDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline 设置写截止时间
|
|
||||||
func (c *RealTCPConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialRealTCP 拨号 RealTCP 连接
|
|
||||||
func DialRealTCP(ctx context.Context, network, addr string, logger *zap.Logger) (net.Conn, error) {
|
|
||||||
logger.Debug("dialing RealTCP", zap.String("addr", addr))
|
|
||||||
|
|
||||||
// 建立 TCP 连接
|
|
||||||
conn, err := (&net.Dialer{}).DialContext(ctx, network, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to dial TCP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 包装为 RealTCP 连接
|
|
||||||
return NewRealTCPConn(conn), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenRealTCP 监听 RealTCP 端口
|
|
||||||
func ListenRealTCP(network, addr string, logger *zap.Logger) (net.Listener, error) {
|
|
||||||
logger.Info("listening RealTCP", zap.String("addr", addr))
|
|
||||||
|
|
||||||
// 监听 TCP 端口
|
|
||||||
listener, err := net.Listen(network, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to listen TCP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &realTCPListener{
|
|
||||||
Listener: listener,
|
|
||||||
logger: logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// realTCPListener RealTCP 监听器
|
|
||||||
type realTCPListener struct {
|
|
||||||
net.Listener
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept 接受连接并包装为 RealTCPConn
|
|
||||||
func (l *realTCPListener) Accept() (net.Conn, error) {
|
|
||||||
conn, err := l.Listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.logger.Debug("accepted RealTCP connection", zap.String("addr", conn.RemoteAddr().String()))
|
|
||||||
return NewRealTCPConn(conn), nil
|
|
||||||
}
|
|
||||||
@@ -1,754 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Layer 传输层类型(9 层策略)
|
|
||||||
type Layer int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// LayerDirectUDP Direct-UDP 直连(WireGuard over UDP)- 最高效
|
|
||||||
LayerDirectUDP Layer = iota
|
|
||||||
|
|
||||||
// LayerFakeTCP Direct-FakeTCP(UDP 封装 TCP 头部,欺骗防火墙)
|
|
||||||
LayerFakeTCP
|
|
||||||
|
|
||||||
// LayerRealTCP Direct-RealTCP(P2P TCP 直连)
|
|
||||||
LayerRealTCP
|
|
||||||
|
|
||||||
// LayerTURNUDP TURN-UDP 中继(标准 RFC 5766)
|
|
||||||
LayerTURNUDP
|
|
||||||
|
|
||||||
// LayerTURNQUIC TURN-QUIC 中继(私有扩展,RFC 9000)
|
|
||||||
LayerTURNQUIC
|
|
||||||
|
|
||||||
// LayerTURNTCP TURN-TCP 中继(TCP 中继)
|
|
||||||
LayerTURNTCP
|
|
||||||
|
|
||||||
// LayerTURNTLS TURN-TLS 中继(TLS 加密,RFC 8656)
|
|
||||||
LayerTURNTLS
|
|
||||||
|
|
||||||
// LayerWebRTC WebRTC DataChannel(DTLS 加密)
|
|
||||||
LayerWebRTC
|
|
||||||
|
|
||||||
// LayerWS WS/WSS 兜底(仅 80/443 端口,终极兜底)
|
|
||||||
LayerWS
|
|
||||||
|
|
||||||
// LayerCount 传输层总数
|
|
||||||
LayerCount
|
|
||||||
)
|
|
||||||
|
|
||||||
// String 实现 Stringer 接口
|
|
||||||
func (l Layer) String() string {
|
|
||||||
switch l {
|
|
||||||
case LayerDirectUDP:
|
|
||||||
return "Direct-UDP"
|
|
||||||
case LayerFakeTCP:
|
|
||||||
return "Direct-FakeTCP"
|
|
||||||
case LayerRealTCP:
|
|
||||||
return "Direct-RealTCP"
|
|
||||||
case LayerTURNUDP:
|
|
||||||
return "TURN-UDP"
|
|
||||||
case LayerTURNQUIC:
|
|
||||||
return "TURN-QUIC"
|
|
||||||
case LayerTURNTCP:
|
|
||||||
return "TURN-TCP"
|
|
||||||
case LayerTURNTLS:
|
|
||||||
return "TURN-TLS"
|
|
||||||
case LayerWebRTC:
|
|
||||||
return "WebRTC"
|
|
||||||
case LayerWS:
|
|
||||||
return "WS/WSS"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultLayerOrder 默认优先级顺序(从最优到兜底)
|
|
||||||
// 根据 MeshRay_项目文档 v2.0.1 第 132-153 行定义
|
|
||||||
var DefaultLayerOrder = []Layer{
|
|
||||||
LayerDirectUDP, // 1. Direct-UDP - 公网/锥型 NAT,首选链路
|
|
||||||
LayerFakeTCP, // 2. Direct-FakeTCP - 校园网、酒店 Wi-Fi、UDP 被 QoS 限速
|
|
||||||
LayerRealTCP, // 3. Direct-RealTCP - 完全禁用 UDP,仅允许 TCP 出站
|
|
||||||
LayerTURNUDP, // 4. TURN-UDP 中继 - 无 P2P 直连,但 UDP 可通
|
|
||||||
LayerTURNQUIC, // 5. TURN-QUIC 中继 - UDP 可通但弱网(4G/5G、高丢包)【私有扩展】
|
|
||||||
LayerTURNTCP, // 6. TURN-TCP 中继 - UDP 封禁,仅放行 TCP
|
|
||||||
LayerTURNTLS, // 7. TURN-TLS 中继 - 企业防火墙 DPI,仅放行 HTTPS
|
|
||||||
LayerWebRTC, // 8. WebRTC 终极兜底 - 最严格隔离内网、代理环境
|
|
||||||
LayerWS, // 9. WS/WSS 兜底 - 仅放行 80/443 端口,且封锁 TURN
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransportFactory 传输工厂接口 - 每种传输方式必须实现
|
|
||||||
type TransportFactory interface {
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
Layer() Layer
|
|
||||||
|
|
||||||
// Dial 建立连接到对端
|
|
||||||
// 返回标准的 net.Conn 接口
|
|
||||||
Dial(ctx context.Context, config *DialConfig) (net.Conn, error)
|
|
||||||
|
|
||||||
// Name 返回传输方式名称(用于日志)
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialConfig 拨号配置
|
|
||||||
type DialConfig struct {
|
|
||||||
// PeerID 对端标识
|
|
||||||
PeerID string
|
|
||||||
|
|
||||||
// PeerPublicKey 对端公钥
|
|
||||||
PeerPublicKey string
|
|
||||||
|
|
||||||
// STUNServers STUN 服务器列表(用于 P2P)
|
|
||||||
STUNServers []string
|
|
||||||
|
|
||||||
// TURNServers TURN 服务器列表
|
|
||||||
TURNServers []string
|
|
||||||
|
|
||||||
// WSServers WebSocket 服务器列表
|
|
||||||
WSServers []string
|
|
||||||
|
|
||||||
// SignalingServers WebRTC 第三方信令服务器列表
|
|
||||||
SignalingServers []string
|
|
||||||
|
|
||||||
// ICESServers ICE 服务器列表(STUN+TURN 的组合)
|
|
||||||
ICESServers []string
|
|
||||||
|
|
||||||
// Timeout 连接超时
|
|
||||||
Timeout time.Duration
|
|
||||||
|
|
||||||
// Logger 日志记录器
|
|
||||||
Logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrategyScheduler 9 层策略调度器(主动调度层)
|
|
||||||
// 职责:
|
|
||||||
// 1. 按优先级选择链路(P2P → Mesh中继 → TURN-UDP → ... → WS/WSS)
|
|
||||||
// 2. 根据网络环境自动切换(500ms 超时 / 10s 丢包率 > 10%)
|
|
||||||
// 3. 切换后探测恢复并自动切回高性能链路(30s)
|
|
||||||
type StrategyScheduler struct {
|
|
||||||
layerFactories map[Layer]TransportFactory // 各层的工厂
|
|
||||||
layerOrder []Layer // 优先级顺序
|
|
||||||
logger *zap.Logger
|
|
||||||
|
|
||||||
// 每个 Peer 的降级控制器
|
|
||||||
fallbackControllers map[string]*FallbackController // peerID -> controller
|
|
||||||
fallbackMu sync.RWMutex
|
|
||||||
|
|
||||||
// 当前活跃连接
|
|
||||||
activeConnections map[string]activeConn // peerID -> 连接信息
|
|
||||||
connMu sync.RWMutex
|
|
||||||
|
|
||||||
// 统计
|
|
||||||
stats *SchedulerStats
|
|
||||||
|
|
||||||
// 连接变更回调(通知上层 ConnManager)
|
|
||||||
OnConnectionUpdate func(peerID string, conn net.Conn, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// activeConn 活跃连接信息
|
|
||||||
type activeConn struct {
|
|
||||||
conn net.Conn
|
|
||||||
layer Layer
|
|
||||||
peerID string
|
|
||||||
established time.Time
|
|
||||||
config *DialConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// SchedulerStats 调度器统计
|
|
||||||
type SchedulerStats struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
totalDials int64
|
|
||||||
successDials int64
|
|
||||||
fallbackCount int64
|
|
||||||
recoveryCount int64
|
|
||||||
layerDialCount map[Layer]int64
|
|
||||||
layerFailCount map[Layer]int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStrategyScheduler 创建策略调度器
|
|
||||||
func NewStrategyScheduler(logger *zap.Logger) *StrategyScheduler {
|
|
||||||
return &StrategyScheduler{
|
|
||||||
layerFactories: make(map[Layer]TransportFactory),
|
|
||||||
layerOrder: DefaultLayerOrder,
|
|
||||||
logger: logger,
|
|
||||||
fallbackControllers: make(map[string]*FallbackController),
|
|
||||||
activeConnections: make(map[string]activeConn),
|
|
||||||
stats: &SchedulerStats{
|
|
||||||
layerDialCount: make(map[Layer]int64),
|
|
||||||
layerFailCount: make(map[Layer]int64),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterFactory 注册传输工厂
|
|
||||||
func (s *StrategyScheduler) RegisterFactory(factory TransportFactory) {
|
|
||||||
layer := factory.Layer()
|
|
||||||
s.layerFactories[layer] = factory
|
|
||||||
s.logger.Debug("注册传输工厂",
|
|
||||||
zap.String("layer", layer.String()),
|
|
||||||
zap.String("name", factory.Name()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLayerOrder 设置优先级顺序
|
|
||||||
func (s *StrategyScheduler) SetLayerOrder(order []Layer) {
|
|
||||||
if len(order) == 0 {
|
|
||||||
s.logger.Warn("空的层级顺序,使用默认顺序")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.layerOrder = order
|
|
||||||
s.logger.Info("更新传输层优先级顺序", zap.Any("order", order))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 按优先级顺序尝试建立连接
|
|
||||||
// 这是核心方法,实现了 9 层策略调度
|
|
||||||
func (s *StrategyScheduler) Dial(config *DialConfig) (net.Conn, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
if config.Timeout > 0 {
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
ctx, cancel = context.WithTimeout(ctx, config.Timeout)
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.logger.Info("开始 8 层策略调度连接",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.Int("total_layers", len(s.layerOrder)))
|
|
||||||
|
|
||||||
// 统计
|
|
||||||
s.stats.mu.Lock()
|
|
||||||
s.stats.totalDials++
|
|
||||||
s.stats.mu.Unlock()
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for i, layer := range s.layerOrder {
|
|
||||||
factory, ok := s.layerFactories[layer]
|
|
||||||
if !ok {
|
|
||||||
s.logger.Debug("该传输层未注册,跳过",
|
|
||||||
zap.String("layer", layer.String()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.logger.Debug("尝试第 N 层传输",
|
|
||||||
zap.Int("index", i),
|
|
||||||
zap.String("layer", layer.String()),
|
|
||||||
zap.String("name", factory.Name()))
|
|
||||||
|
|
||||||
// 统计该层拨号次数
|
|
||||||
s.stats.mu.Lock()
|
|
||||||
s.stats.layerDialCount[layer]++
|
|
||||||
s.stats.mu.Unlock()
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
conn, err := factory.Dial(ctx, config)
|
|
||||||
duration := time.Since(startTime)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
// 成功!
|
|
||||||
s.stats.mu.Lock()
|
|
||||||
s.stats.successDials++
|
|
||||||
s.stats.mu.Unlock()
|
|
||||||
|
|
||||||
// 记录活跃连接
|
|
||||||
s.connMu.Lock()
|
|
||||||
s.activeConnections[config.PeerID] = activeConn{
|
|
||||||
conn: conn,
|
|
||||||
layer: layer,
|
|
||||||
peerID: config.PeerID,
|
|
||||||
established: time.Now(),
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
s.connMu.Unlock()
|
|
||||||
|
|
||||||
// 创建或更新降级控制器
|
|
||||||
s.ensureFallbackController(config.PeerID, layer)
|
|
||||||
|
|
||||||
s.logger.Info("连接建立成功",
|
|
||||||
zap.String("layer", layer.String()),
|
|
||||||
zap.String("name", factory.Name()),
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.String("remote_addr", conn.RemoteAddr().String()),
|
|
||||||
zap.Duration("duration", duration))
|
|
||||||
|
|
||||||
// 包装连接,用于监控
|
|
||||||
return newMonitoredConn(conn, config.PeerID, layer, s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 失败,统计
|
|
||||||
s.stats.mu.Lock()
|
|
||||||
s.stats.layerFailCount[layer]++
|
|
||||||
s.stats.mu.Unlock()
|
|
||||||
|
|
||||||
// 记录失败并继续尝试下一层
|
|
||||||
lastErr = err
|
|
||||||
s.logger.Warn("该传输层连接失败,尝试下一层",
|
|
||||||
zap.String("layer", layer.String()),
|
|
||||||
zap.Duration("duration", duration),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有层都失败
|
|
||||||
return nil, fmt.Errorf("所有传输层均失败,最后错误:%w", lastErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconnectToLayer 触发重连到指定层级
|
|
||||||
func (s *StrategyScheduler) reconnectToLayer(peerID string, toLayer Layer) {
|
|
||||||
s.connMu.RLock()
|
|
||||||
ac, exists := s.activeConnections[peerID]
|
|
||||||
s.connMu.RUnlock()
|
|
||||||
|
|
||||||
if !exists || ac.config == nil {
|
|
||||||
s.logger.Warn("重连失败:找不到活跃连接配置", zap.String("peer_id", peerID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
factory, ok := s.layerFactories[toLayer]
|
|
||||||
if !ok {
|
|
||||||
s.logger.Error("重连失败:找不到目标层级工厂", zap.String("layer", toLayer.String()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := factory.Dial(ctx, ac.config)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("降级重连失败", zap.Error(err))
|
|
||||||
if s.OnConnectionUpdate != nil {
|
|
||||||
s.OnConnectionUpdate(peerID, nil, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wrappedConn := newMonitoredConn(conn, peerID, toLayer, s)
|
|
||||||
|
|
||||||
s.connMu.Lock()
|
|
||||||
if oldAc, exists := s.activeConnections[peerID]; exists {
|
|
||||||
oldAc.conn.Close()
|
|
||||||
}
|
|
||||||
s.activeConnections[peerID] = activeConn{
|
|
||||||
conn: wrappedConn,
|
|
||||||
layer: toLayer,
|
|
||||||
peerID: peerID,
|
|
||||||
established: time.Now(),
|
|
||||||
config: ac.config,
|
|
||||||
}
|
|
||||||
s.connMu.Unlock()
|
|
||||||
|
|
||||||
if s.OnConnectionUpdate != nil {
|
|
||||||
s.OnConnectionUpdate(peerID, wrappedConn, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureFallbackController 确保对端有降级控制器
|
|
||||||
func (s *StrategyScheduler) ensureFallbackController(peerID string, initialLayer Layer) {
|
|
||||||
s.fallbackMu.Lock()
|
|
||||||
defer s.fallbackMu.Unlock()
|
|
||||||
|
|
||||||
if _, exists := s.fallbackControllers[peerID]; !exists {
|
|
||||||
controller := NewFallbackController(
|
|
||||||
initialLayer,
|
|
||||||
func(from, to Layer) {
|
|
||||||
// 降级回调
|
|
||||||
s.stats.mu.Lock()
|
|
||||||
s.stats.fallbackCount++
|
|
||||||
s.stats.mu.Unlock()
|
|
||||||
|
|
||||||
s.logger.Warn("链路降级",
|
|
||||||
zap.String("peer_id", peerID),
|
|
||||||
zap.String("from_layer", from.String()),
|
|
||||||
zap.String("to_layer", to.String()))
|
|
||||||
|
|
||||||
// 触发重连到新层级
|
|
||||||
go s.reconnectToLayer(peerID, to)
|
|
||||||
},
|
|
||||||
func(to Layer) {
|
|
||||||
// 恢复回调
|
|
||||||
s.stats.mu.Lock()
|
|
||||||
s.stats.recoveryCount++
|
|
||||||
s.stats.mu.Unlock()
|
|
||||||
|
|
||||||
s.logger.Info("链路恢复",
|
|
||||||
zap.String("peer_id", peerID),
|
|
||||||
zap.String("to_layer", to.String()))
|
|
||||||
|
|
||||||
// 回调处理已经在 probeHighLayers 中完成并传递了新连接
|
|
||||||
},
|
|
||||||
s.logger,
|
|
||||||
s, // pass scheduler to access activeConnections
|
|
||||||
)
|
|
||||||
s.fallbackControllers[peerID] = controller
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordLatency 记录延迟(供 MonitoredConn 调用)
|
|
||||||
func (s *StrategyScheduler) RecordLatency(peerID string, success bool, duration time.Duration) {
|
|
||||||
s.fallbackMu.RLock()
|
|
||||||
controller, exists := s.fallbackControllers[peerID]
|
|
||||||
s.fallbackMu.RUnlock()
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
controller.CheckAndFallback(success, duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetActiveLayer 获取当前活跃的传输层(用于监控)
|
|
||||||
func (s *StrategyScheduler) GetActiveLayer() Layer {
|
|
||||||
// 返回第一个活跃连接的层级
|
|
||||||
s.connMu.RLock()
|
|
||||||
defer s.connMu.RUnlock()
|
|
||||||
|
|
||||||
for _, ac := range s.activeConnections {
|
|
||||||
return ac.layer
|
|
||||||
}
|
|
||||||
return LayerDirectUDP // 默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllActiveLayers 获取所有 Peer 的活跃层级(用于全局监控)
|
|
||||||
func (s *StrategyScheduler) GetAllActiveLayers() map[string]Layer {
|
|
||||||
s.connMu.RLock()
|
|
||||||
defer s.connMu.RUnlock()
|
|
||||||
|
|
||||||
result := make(map[string]Layer)
|
|
||||||
for peerID, ac := range s.activeConnections {
|
|
||||||
result[peerID] = ac.layer
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerLayer 获取指定 Peer 的当前层级
|
|
||||||
func (s *StrategyScheduler) GetPeerLayer(peerID string) Layer {
|
|
||||||
s.connMu.RLock()
|
|
||||||
defer s.connMu.RUnlock()
|
|
||||||
|
|
||||||
if ac, exists := s.activeConnections[peerID]; exists {
|
|
||||||
return ac.layer
|
|
||||||
}
|
|
||||||
return LayerDirectUDP // 默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStats 获取统计信息
|
|
||||||
func (s *StrategyScheduler) GetStats() map[string]interface{} {
|
|
||||||
s.stats.mu.RLock()
|
|
||||||
defer s.stats.mu.RUnlock()
|
|
||||||
|
|
||||||
layerStats := make(map[string]int64)
|
|
||||||
for layer, count := range s.stats.layerDialCount {
|
|
||||||
layerStats[layer.String()+"_dial"] = count
|
|
||||||
}
|
|
||||||
for layer, count := range s.stats.layerFailCount {
|
|
||||||
layerStats[layer.String()+"_fail"] = count
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]interface{}{
|
|
||||||
"total_dials": s.stats.totalDials,
|
|
||||||
"success_dials": s.stats.successDials,
|
|
||||||
"fallback_count": s.stats.fallbackCount,
|
|
||||||
"recovery_count": s.stats.recoveryCount,
|
|
||||||
"layer_stats": layerStats,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClosePeer 关闭指定 Peer 的连接和控制器
|
|
||||||
func (s *StrategyScheduler) ClosePeer(peerID string) {
|
|
||||||
// 关闭连接
|
|
||||||
s.connMu.Lock()
|
|
||||||
if ac, exists := s.activeConnections[peerID]; exists {
|
|
||||||
ac.conn.Close()
|
|
||||||
delete(s.activeConnections, peerID)
|
|
||||||
}
|
|
||||||
s.connMu.Unlock()
|
|
||||||
|
|
||||||
// 移除降级控制器
|
|
||||||
s.fallbackMu.Lock()
|
|
||||||
if controller, exists := s.fallbackControllers[peerID]; exists {
|
|
||||||
// 停止恢复探测器
|
|
||||||
if controller.recoveryTimer != nil {
|
|
||||||
controller.recoveryTimer.Stop()
|
|
||||||
}
|
|
||||||
delete(s.fallbackControllers, peerID)
|
|
||||||
}
|
|
||||||
s.fallbackMu.Unlock()
|
|
||||||
|
|
||||||
s.logger.Debug("已关闭 Peer 连接和控制器",
|
|
||||||
zap.String("peer_id", peerID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MonitoredConn 带监控的连接包装器
|
|
||||||
type MonitoredConn struct {
|
|
||||||
net.Conn
|
|
||||||
peerID string
|
|
||||||
layer Layer
|
|
||||||
scheduler *StrategyScheduler
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMonitoredConn 创建带监控的连接
|
|
||||||
func newMonitoredConn(conn net.Conn, peerID string, layer Layer, scheduler *StrategyScheduler) *MonitoredConn {
|
|
||||||
return &MonitoredConn{
|
|
||||||
Conn: conn,
|
|
||||||
peerID: peerID,
|
|
||||||
layer: layer,
|
|
||||||
scheduler: scheduler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read 重写 Read 方法,记录延迟
|
|
||||||
func (c *MonitoredConn) Read(b []byte) (n int, err error) {
|
|
||||||
start := time.Now()
|
|
||||||
n, err = c.Conn.Read(b)
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
// 记录成功/失败
|
|
||||||
c.scheduler.RecordLatency(c.peerID, err == nil, duration)
|
|
||||||
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 重写 Write 方法,记录延迟
|
|
||||||
func (c *MonitoredConn) Write(b []byte) (n int, err error) {
|
|
||||||
start := time.Now()
|
|
||||||
n, err = c.Conn.Write(b)
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
// 记录成功/失败
|
|
||||||
c.scheduler.RecordLatency(c.peerID, err == nil, duration)
|
|
||||||
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FallbackController 降级控制器
|
|
||||||
type FallbackController struct {
|
|
||||||
currentLayer Layer // 当前使用的层
|
|
||||||
windowStart time.Time // 滑动窗口起始时间
|
|
||||||
packetCount int // 总包数
|
|
||||||
lostPacketCount int // 丢包数
|
|
||||||
mu chan struct{} // 互斥锁(用 channel 实现)
|
|
||||||
triggerFallback func(Layer, Layer) // 降级触发回调
|
|
||||||
triggerRecovery func(Layer) // 恢复触发回调
|
|
||||||
logger *zap.Logger
|
|
||||||
recoveryTimer *time.Timer // 恢复探测定时器
|
|
||||||
scheduler *StrategyScheduler
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// TimeoutThreshold 单次超时阈值
|
|
||||||
TimeoutThreshold = 500 * time.Millisecond
|
|
||||||
|
|
||||||
// PacketLossThreshold 丢包率阈值
|
|
||||||
PacketLossThreshold = 0.10 // 10%
|
|
||||||
|
|
||||||
// RecoveryInterval 恢复探测间隔
|
|
||||||
RecoveryInterval = 30 * time.Second
|
|
||||||
|
|
||||||
// SlidingWindowDuration 滑动窗口时长
|
|
||||||
SlidingWindowDuration = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewFallbackController 创建降级控制器
|
|
||||||
func NewFallbackController(
|
|
||||||
initialLayer Layer,
|
|
||||||
onFallback func(Layer, Layer),
|
|
||||||
onRecovery func(Layer),
|
|
||||||
logger *zap.Logger,
|
|
||||||
scheduler *StrategyScheduler,
|
|
||||||
) *FallbackController {
|
|
||||||
fc := &FallbackController{
|
|
||||||
currentLayer: initialLayer,
|
|
||||||
mu: make(chan struct{}, 1),
|
|
||||||
triggerFallback: onFallback,
|
|
||||||
triggerRecovery: onRecovery,
|
|
||||||
logger: logger,
|
|
||||||
scheduler: scheduler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动恢复探测
|
|
||||||
fc.startRecoveryProbe()
|
|
||||||
|
|
||||||
return fc
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckAndFallback 检查是否需要降级
|
|
||||||
// 在每次连接操作后调用
|
|
||||||
func (fc *FallbackController) CheckAndFallback(success bool, duration time.Duration) {
|
|
||||||
select {
|
|
||||||
case fc.mu <- struct{}{}:
|
|
||||||
defer func() { <-fc.mu }()
|
|
||||||
default:
|
|
||||||
// 锁被占用,说明正在处理,直接返回
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置滑动窗口
|
|
||||||
if time.Since(fc.windowStart) > SlidingWindowDuration {
|
|
||||||
fc.windowStart = time.Now()
|
|
||||||
fc.packetCount = 0
|
|
||||||
fc.lostPacketCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计
|
|
||||||
fc.packetCount++
|
|
||||||
if !success || duration > TimeoutThreshold {
|
|
||||||
fc.lostPacketCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否达到阈值
|
|
||||||
if fc.packetCount >= 10 {
|
|
||||||
lossRate := float64(fc.lostPacketCount) / float64(fc.packetCount)
|
|
||||||
if lossRate > PacketLossThreshold {
|
|
||||||
fc.triggerFallbackLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// triggerFallbackLocked 执行降级(已持有锁)
|
|
||||||
func (fc *FallbackController) triggerFallbackLocked() {
|
|
||||||
currentIndex := int(fc.currentLayer)
|
|
||||||
if currentIndex >= int(LayerCount)-1 {
|
|
||||||
// 已经是最低优先级,无法降级
|
|
||||||
fc.logger.Warn("已是最底层级,无法降级",
|
|
||||||
zap.String("current_layer", fc.currentLayer.String()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextLayer := Layer(currentIndex + 1)
|
|
||||||
|
|
||||||
// 在更新 currentLayer 之前保存旧值用于回调
|
|
||||||
oldLayer := fc.currentLayer
|
|
||||||
|
|
||||||
fc.logger.Warn("触发降级",
|
|
||||||
zap.String("from_layer", oldLayer.String()),
|
|
||||||
zap.String("to_layer", nextLayer.String()))
|
|
||||||
|
|
||||||
fc.currentLayer = nextLayer
|
|
||||||
fc.resetWindow()
|
|
||||||
|
|
||||||
if fc.triggerFallback != nil {
|
|
||||||
fc.triggerFallback(oldLayer, nextLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置恢复定时器
|
|
||||||
fc.startRecoveryProbe()
|
|
||||||
}
|
|
||||||
|
|
||||||
// startRecoveryProbe 启动恢复探测
|
|
||||||
func (fc *FallbackController) startRecoveryProbe() {
|
|
||||||
if fc.recoveryTimer != nil {
|
|
||||||
fc.recoveryTimer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
fc.recoveryTimer = time.AfterFunc(RecoveryInterval, func() {
|
|
||||||
fc.probeHigherLayers()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeHigherLayers 探测更高层级
|
|
||||||
func (fc *FallbackController) probeHigherLayers() {
|
|
||||||
select {
|
|
||||||
case fc.mu <- struct{}{}:
|
|
||||||
defer func() { <-fc.mu }()
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex := int(fc.currentLayer)
|
|
||||||
if currentIndex == 0 {
|
|
||||||
// 已经是最高优先级,无需探测
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试上一层
|
|
||||||
higherLayer := Layer(currentIndex - 1)
|
|
||||||
fc.logger.Info("探测更高层级",
|
|
||||||
zap.String("current_layer", fc.currentLayer.String()),
|
|
||||||
zap.String("probe_layer", higherLayer.String()))
|
|
||||||
|
|
||||||
// 获取 PeerID 及 Config
|
|
||||||
fc.scheduler.connMu.RLock()
|
|
||||||
var peerID string
|
|
||||||
var config *DialConfig
|
|
||||||
for pid, ac := range fc.scheduler.activeConnections {
|
|
||||||
if ac.layer == fc.currentLayer {
|
|
||||||
peerID = pid
|
|
||||||
config = ac.config
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fc.scheduler.connMu.RUnlock()
|
|
||||||
|
|
||||||
if config == nil {
|
|
||||||
fc.logger.Warn("探测更高层级失败:找不到有效 DialConfig")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
factory, ok := fc.scheduler.layerFactories[higherLayer]
|
|
||||||
if !ok {
|
|
||||||
fc.logger.Debug("更高层级未注册工厂,跳过探测")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := factory.Dial(ctx, config)
|
|
||||||
if err == nil {
|
|
||||||
fc.logger.Info("更高层级探测成功,准备切换")
|
|
||||||
|
|
||||||
wrappedConn := newMonitoredConn(conn, peerID, higherLayer, fc.scheduler)
|
|
||||||
|
|
||||||
fc.scheduler.connMu.Lock()
|
|
||||||
if ac, exists := fc.scheduler.activeConnections[peerID]; exists {
|
|
||||||
ac.conn.Close() // Close old
|
|
||||||
ac.conn = wrappedConn
|
|
||||||
ac.layer = higherLayer
|
|
||||||
fc.scheduler.activeConnections[peerID] = ac
|
|
||||||
}
|
|
||||||
fc.scheduler.connMu.Unlock()
|
|
||||||
|
|
||||||
if fc.scheduler.OnConnectionUpdate != nil {
|
|
||||||
fc.scheduler.OnConnectionUpdate(peerID, wrappedConn, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发恢复回调
|
|
||||||
fc.triggerRecoveryLocked(higherLayer)
|
|
||||||
} else {
|
|
||||||
fc.logger.Debug("更高层级探测失败", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// triggerRecoveryLocked 执行恢复(已持有锁)
|
|
||||||
func (fc *FallbackController) triggerRecoveryLocked(higherLayer Layer) {
|
|
||||||
fc.logger.Info("触发恢复",
|
|
||||||
zap.String("from_layer", fc.currentLayer.String()),
|
|
||||||
zap.String("to_layer", higherLayer.String()))
|
|
||||||
|
|
||||||
fc.currentLayer = higherLayer
|
|
||||||
fc.resetWindow()
|
|
||||||
|
|
||||||
if fc.triggerRecovery != nil {
|
|
||||||
fc.triggerRecovery(higherLayer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetWindow 重置滑动窗口
|
|
||||||
func (fc *FallbackController) resetWindow() {
|
|
||||||
fc.windowStart = time.Now()
|
|
||||||
fc.packetCount = 0
|
|
||||||
fc.lostPacketCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentLayer 获取当前层级
|
|
||||||
func (fc *FallbackController) GetCurrentLayer() Layer {
|
|
||||||
select {
|
|
||||||
case fc.mu <- struct{}{}:
|
|
||||||
defer func() { <-fc.mu }()
|
|
||||||
default:
|
|
||||||
return fc.currentLayer
|
|
||||||
}
|
|
||||||
return fc.currentLayer
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pion/stun"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// STUNClient STUN 客户端 - 用于 NAT 探测和候选地址采集
|
|
||||||
type STUNClient struct {
|
|
||||||
servers []string
|
|
||||||
logger *zap.Logger
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSTUNClient 创建 STUN 客户端
|
|
||||||
func NewSTUNClient(servers []string, logger *zap.Logger) *STUNClient {
|
|
||||||
return &STUNClient{
|
|
||||||
servers: servers,
|
|
||||||
logger: logger,
|
|
||||||
timeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscoverAddress 发现外部地址(通过单个 STUN 服务器)
|
|
||||||
func (c *STUNClient) DiscoverAddress(server string) (*net.UDPAddr, error) {
|
|
||||||
host, port, err := net.SplitHostPort(server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("STUN 服务器地址格式错误:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("解析 UDP 地址失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("连接 STUN 服务器失败:%w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
conn.SetDeadline(time.Now().Add(c.timeout))
|
|
||||||
|
|
||||||
// 构建 STUN Binding Request
|
|
||||||
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("构建 STUN 请求失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
if _, err := conn.Write(msg.Raw); err != nil {
|
|
||||||
return nil, fmt.Errorf("发送 STUN 请求失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("读取 STUN 响应失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析响应
|
|
||||||
res := &stun.Message{Raw: buf[:n]}
|
|
||||||
if err := res.Decode(); err != nil {
|
|
||||||
return nil, fmt.Errorf("解码 STUN 响应失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取 XOR-MAPPED-ADDRESS
|
|
||||||
var xorAddr stun.XORMappedAddress
|
|
||||||
if err := xorAddr.GetFrom(res); err != nil {
|
|
||||||
return nil, fmt.Errorf("提取外部地址失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Debug("STUN 查询成功",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.String("external_addr", xorAddr.String()))
|
|
||||||
|
|
||||||
return &net.UDPAddr{
|
|
||||||
IP: xorAddr.IP,
|
|
||||||
Port: xorAddr.Port,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectCandidates 收集候选地址(通过多个 STUN 服务器)
|
|
||||||
func (c *STUNClient) CollectCandidates() []string {
|
|
||||||
var candidates []string
|
|
||||||
|
|
||||||
for _, server := range c.servers {
|
|
||||||
addr, err := c.DiscoverAddress(server)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Debug("STUN 服务器查询失败",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.Error(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates = append(candidates, addr.String())
|
|
||||||
c.logger.Debug("收集到候选地址",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.String("candidate", addr.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExternalIP 获取外部 IP(兼容旧 API)
|
|
||||||
func (c *STUNClient) GetExternalIP() (string, error) {
|
|
||||||
for _, server := range c.servers {
|
|
||||||
addr, err := c.DiscoverAddress(server)
|
|
||||||
if err == nil {
|
|
||||||
return addr.IP.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("所有 STUN 服务器均查询失败")
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pion/turn/v2"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TURNProtocol TURN 协议类型
|
|
||||||
type TURNProtocol string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TURNProtocolUDP TURNProtocol = "udp"
|
|
||||||
TURNProtocolTCP TURNProtocol = "tcp"
|
|
||||||
TURNProtocolTLS TURNProtocol = "tls"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TURNFactory TURN 工厂(Layer 4-6: TURN-UDP/TCP/TLS)
|
|
||||||
// 自包含实现:TURN 协议协商 + 建连
|
|
||||||
type TURNFactory struct {
|
|
||||||
protocol TURNProtocol
|
|
||||||
servers []string
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTURNFactory 创建 TURN 工厂
|
|
||||||
func NewTURNFactory(protocol TURNProtocol, servers []string, username, password string, logger *zap.Logger) *TURNFactory {
|
|
||||||
return &TURNFactory{
|
|
||||||
protocol: protocol,
|
|
||||||
servers: servers,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
func (f *TURNFactory) Layer() Layer {
|
|
||||||
switch f.protocol {
|
|
||||||
case TURNProtocolUDP:
|
|
||||||
return LayerTURNUDP
|
|
||||||
case TURNProtocolTCP:
|
|
||||||
return LayerTURNTCP
|
|
||||||
case TURNProtocolTLS:
|
|
||||||
return LayerTURNTLS
|
|
||||||
default:
|
|
||||||
return LayerTURNUDP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name 返回名称
|
|
||||||
func (f *TURNFactory) Name() string {
|
|
||||||
switch f.protocol {
|
|
||||||
case TURNProtocolUDP:
|
|
||||||
return "TURN-UDP"
|
|
||||||
case TURNProtocolTCP:
|
|
||||||
return "TURN-TCP"
|
|
||||||
case TURNProtocolTLS:
|
|
||||||
return "TURN-TLS"
|
|
||||||
default:
|
|
||||||
return "TURN-UDP"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 建立 TURN 中继连接
|
|
||||||
func (f *TURNFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
|
||||||
f.logger.Info("开始建立 TURN 中继连接",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.String("protocol", string(f.protocol)))
|
|
||||||
|
|
||||||
servers := f.servers
|
|
||||||
if len(servers) == 0 {
|
|
||||||
servers = config.TURNServers
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(servers) == 0 {
|
|
||||||
return nil, fmt.Errorf("未配置 TURN 服务器")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析第一个 TURN 服务器
|
|
||||||
server := servers[0]
|
|
||||||
host, port := parseServerAddr(server)
|
|
||||||
|
|
||||||
// 根据协议类型建立连接
|
|
||||||
var relayConn net.PacketConn
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch f.protocol {
|
|
||||||
case TURNProtocolUDP:
|
|
||||||
relayConn, err = f.allocateUDP(ctx, host, port, config)
|
|
||||||
case TURNProtocolTCP:
|
|
||||||
relayConn, err = f.allocateTCP(ctx, host, port, config)
|
|
||||||
case TURNProtocolTLS:
|
|
||||||
return nil, fmt.Errorf("TURN-TLS 尚未实现")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("不支持的 TURN 协议:%s", f.protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("TURN 分配失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Info("TURN 中继连接建立成功",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.String("relay_addr", relayConn.LocalAddr().String()))
|
|
||||||
|
|
||||||
// 包装成 net.Conn 返回
|
|
||||||
return newTURNConn(relayConn, f.logger), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocateUDP UDP TURN 分配
|
|
||||||
func (f *TURNFactory) allocateUDP(ctx context.Context, host, port string, config *DialConfig) (net.PacketConn, error) {
|
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("解析 UDP 地址失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.DialUDP("udp", nil, udpAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建 UDP 连接失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig := &turn.ClientConfig{
|
|
||||||
STUNServerAddr: net.JoinHostPort(host, port),
|
|
||||||
TURNServerAddr: net.JoinHostPort(host, port),
|
|
||||||
Username: f.username,
|
|
||||||
Password: f.password,
|
|
||||||
Conn: conn,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := turn.NewClient(clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("创建 TURN 客户端失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Listen(); err != nil {
|
|
||||||
client.Close()
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("TURN 客户端监听失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
relayConn, err := client.Allocate()
|
|
||||||
if err != nil {
|
|
||||||
client.Close()
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("分配 TURN 中继失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 Permission(允许特定对端地址使用中继)
|
|
||||||
// 这是 TURN 协议的关键步骤,否则无法收发数据
|
|
||||||
// 注意:PeerID 在这里应该是对端的公网地址(由信使服务器转发)
|
|
||||||
if config != nil && config.PeerID != "" {
|
|
||||||
peerAddr, err := net.ResolveUDPAddr("udp", config.PeerID)
|
|
||||||
if err == nil {
|
|
||||||
if permErr := client.CreatePermission(peerAddr); permErr != nil {
|
|
||||||
f.logger.Warn("CreatePermission 失败",
|
|
||||||
zap.String("peer_addr", peerAddr.String()),
|
|
||||||
zap.Error(permErr))
|
|
||||||
// 注意:CreatePermission 失败不影响连接建立,只是警告
|
|
||||||
} else {
|
|
||||||
f.logger.Debug("CreatePermission 成功",
|
|
||||||
zap.String("peer_addr", peerAddr.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Debug("TURN-UDP 分配成功",
|
|
||||||
zap.String("relay_addr", relayConn.LocalAddr().String()))
|
|
||||||
|
|
||||||
return relayConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocateTCP TCP TURN 分配
|
|
||||||
func (f *TURNFactory) allocateTCP(ctx context.Context, host, port string, config *DialConfig) (net.PacketConn, error) {
|
|
||||||
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("TCP 连接失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
packetConn := newTCPPacketConn(conn, f.logger)
|
|
||||||
|
|
||||||
clientConfig := &turn.ClientConfig{
|
|
||||||
STUNServerAddr: net.JoinHostPort(host, port),
|
|
||||||
TURNServerAddr: net.JoinHostPort(host, port),
|
|
||||||
Username: f.username,
|
|
||||||
Password: f.password,
|
|
||||||
Conn: packetConn,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := turn.NewClient(clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("创建 TURN 客户端失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Listen(); err != nil {
|
|
||||||
client.Close()
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("TURN 客户端监听失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
relayConn, err := client.Allocate()
|
|
||||||
if err != nil {
|
|
||||||
client.Close()
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("分配 TURN 中继失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 Permission(允许特定对端地址使用中继)
|
|
||||||
if config != nil && config.PeerID != "" {
|
|
||||||
peerAddr, err := net.ResolveTCPAddr("tcp", config.PeerID)
|
|
||||||
if err == nil {
|
|
||||||
if permErr := client.CreatePermission(peerAddr); permErr != nil {
|
|
||||||
f.logger.Warn("CreatePermission 失败",
|
|
||||||
zap.String("peer_addr", peerAddr.String()),
|
|
||||||
zap.Error(permErr))
|
|
||||||
} else {
|
|
||||||
f.logger.Debug("CreatePermission 成功",
|
|
||||||
zap.String("peer_addr", peerAddr.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.logger.Debug("TURN-TCP 分配成功",
|
|
||||||
zap.String("relay_addr", relayConn.LocalAddr().String()))
|
|
||||||
|
|
||||||
return relayConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseServerAddr 解析服务器地址
|
|
||||||
func parseServerAddr(server string) (host, port string) {
|
|
||||||
h, p, _ := net.SplitHostPort(server)
|
|
||||||
if h == "" {
|
|
||||||
h = server
|
|
||||||
p = "3478" // 默认 TURN 端口
|
|
||||||
}
|
|
||||||
return h, p
|
|
||||||
}
|
|
||||||
|
|
||||||
// turnConn TURN 连接包装器
|
|
||||||
type turnConn struct {
|
|
||||||
relay net.PacketConn
|
|
||||||
remoteAddr net.Addr // 对端地址
|
|
||||||
buffer []byte
|
|
||||||
logger *zap.Logger
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTURNConn 创建 TURN 连接
|
|
||||||
func newTURNConn(relay net.PacketConn, logger *zap.Logger) *turnConn {
|
|
||||||
return &turnConn{
|
|
||||||
relay: relay,
|
|
||||||
buffer: make([]byte, 65535),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRemoteAddr 设置对端地址(必须在 Write 之前调用)
|
|
||||||
func (c *turnConn) SetRemoteAddr(addr net.Addr) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.remoteAddr = addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) Read(b []byte) (n int, err error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
n, _, err = c.relay.ReadFrom(b)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) Write(b []byte) (n int, err error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.remoteAddr == nil {
|
|
||||||
return 0, fmt.Errorf("未设置对端地址,请先调用 SetRemoteAddr()")
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = c.relay.WriteTo(b, c.remoteAddr)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) Close() error {
|
|
||||||
return c.relay.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) LocalAddr() net.Addr {
|
|
||||||
return c.relay.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) RemoteAddr() net.Addr {
|
|
||||||
return nil // TURN 中继没有固定的 RemoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) SetDeadline(t time.Time) error {
|
|
||||||
return c.relay.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) SetReadDeadline(t time.Time) error {
|
|
||||||
return c.relay.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *turnConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return c.relay.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tcpPacketConn TCP PacketConn 包装器
|
|
||||||
type tcpPacketConn struct {
|
|
||||||
conn net.Conn
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTCPPacketConn(conn net.Conn, logger *zap.Logger) *tcpPacketConn {
|
|
||||||
return &tcpPacketConn{
|
|
||||||
conn: conn,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
|
||||||
n, err = p.conn.Read(b)
|
|
||||||
return n, p.conn.RemoteAddr(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|
||||||
return p.conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) Close() error {
|
|
||||||
return p.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) LocalAddr() net.Addr {
|
|
||||||
return p.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) SetDeadline(t time.Time) error {
|
|
||||||
return p.conn.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) SetReadDeadline(t time.Time) error {
|
|
||||||
return p.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *tcpPacketConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return p.conn.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/quic-go/quic-go"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QUICListener QUIC 监听器
|
|
||||||
type QUICListener struct {
|
|
||||||
listener *quic.Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewQUICListener 创建 QUIC 监听器
|
|
||||||
func NewQUICListener(addr string, logger *zap.Logger) (*QUICListener, error) {
|
|
||||||
// 生成自签名证书(用于测试)
|
|
||||||
cert, err := generateSelfSignedCert()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("生成证书失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf := &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
NextProtos: []string{"meshray-quic"},
|
|
||||||
}
|
|
||||||
|
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
listener, err := quic.Listen(udpConn, tlsConf, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建 QUIC 监听器失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("QUIC 监听器已启动", zap.String("addr", addr))
|
|
||||||
|
|
||||||
return &QUICListener{
|
|
||||||
listener: listener,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept 接受 QUIC 连接
|
|
||||||
func (l *QUICListener) Accept(ctx context.Context) (*quic.Conn, error) {
|
|
||||||
return l.listener.Accept(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭监听器
|
|
||||||
func (l *QUICListener) Close() error {
|
|
||||||
return l.listener.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// QUICClient QUIC 客户端
|
|
||||||
type QUICClient struct {
|
|
||||||
servers []string
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewQUICClient 创建 QUIC 客户端
|
|
||||||
func NewQUICClient(servers []string, logger *zap.Logger) *QUICClient {
|
|
||||||
return &QUICClient{
|
|
||||||
servers: servers,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect 建立 QUIC 连接
|
|
||||||
func (c *QUICClient) Connect(ctx context.Context) (net.Conn, error) {
|
|
||||||
if len(c.servers) == 0 {
|
|
||||||
return nil, fmt.Errorf("未配置 QUIC 服务器")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用不安全的 TLS 配置(跳过证书验证,用于测试)
|
|
||||||
tlsConf := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
NextProtos: []string{"meshray-quic"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试连接第一个服务器
|
|
||||||
for _, server := range c.servers {
|
|
||||||
_, err := net.ResolveUDPAddr("udp", server)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Warn("解析 QUIC 服务器地址失败",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.Error(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var conn *quic.Conn
|
|
||||||
conn, err = quic.DialAddr(ctx, server, tlsConf, nil)
|
|
||||||
if err == nil {
|
|
||||||
c.logger.Info("QUIC 连接已建立",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.String("local_addr", conn.LocalAddr().String()))
|
|
||||||
return newQUICConn(conn), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Warn("QUIC 连接失败",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("所有 QUIC 服务器连接失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// quicConn QUIC 连接包装器(实现 net.Conn)
|
|
||||||
type quicConn struct {
|
|
||||||
conn *quic.Conn // quic-go v0.59.0 使用 *quic.Conn
|
|
||||||
stream *quic.Stream // 使用 *quic.Stream
|
|
||||||
}
|
|
||||||
|
|
||||||
// newQUICConn 创建 QUIC 连接包装器
|
|
||||||
func newQUICConn(conn *quic.Conn) *quicConn {
|
|
||||||
return &quicConn{
|
|
||||||
conn: conn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenStream 打开流
|
|
||||||
func (c *quicConn) OpenStream() error {
|
|
||||||
stream, err := c.conn.OpenStreamSync(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.stream = stream
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read 实现 net.Conn
|
|
||||||
func (c *quicConn) Read(b []byte) (n int, err error) {
|
|
||||||
if c.stream == nil {
|
|
||||||
stream, err := c.conn.OpenStreamSync(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
c.stream = stream
|
|
||||||
}
|
|
||||||
return c.stream.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 实现 net.Conn
|
|
||||||
func (c *quicConn) Write(b []byte) (n int, err error) {
|
|
||||||
if c.stream == nil {
|
|
||||||
stream, err := c.conn.OpenStreamSync(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
c.stream = stream
|
|
||||||
}
|
|
||||||
return c.stream.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 实现 net.Conn
|
|
||||||
func (c *quicConn) Close() error {
|
|
||||||
if c.stream != nil {
|
|
||||||
c.stream.Close()
|
|
||||||
}
|
|
||||||
return c.conn.CloseWithError(0, "closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr 实现 net.Conn
|
|
||||||
func (c *quicConn) LocalAddr() net.Addr {
|
|
||||||
return c.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr 实现 net.Conn
|
|
||||||
func (c *quicConn) RemoteAddr() net.Addr {
|
|
||||||
return c.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDeadline 实现 net.Conn
|
|
||||||
func (c *quicConn) SetDeadline(t time.Time) error {
|
|
||||||
if c.stream != nil {
|
|
||||||
return (*c.stream).SetDeadline(t)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline 实现 net.Conn
|
|
||||||
func (c *quicConn) SetReadDeadline(t time.Time) error {
|
|
||||||
if c.stream != nil {
|
|
||||||
return (*c.stream).SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline 实现 net.Conn
|
|
||||||
func (c *quicConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
if c.stream != nil {
|
|
||||||
return (*c.stream).SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateSelfSignedCert 生成自签名证书(仅用于测试)
|
|
||||||
func generateSelfSignedCert() (tls.Certificate, error) {
|
|
||||||
// 生成私钥
|
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成证书模板
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
|
||||||
DNSNames: []string{"localhost"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自签名
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编码证书和私钥
|
|
||||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: certDER,
|
|
||||||
})
|
|
||||||
|
|
||||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载证书
|
|
||||||
return tls.X509KeyPair(certPEM, keyPEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTURNFactoryQUIC 创建 QUIC TURN 工厂(用于 9 层降级策略)
|
|
||||||
// 注意:当前版本暂不启用 QUIC 支持,返回 nil
|
|
||||||
func NewTURNFactoryQUIC(servers []string, username, password string, logger *zap.Logger) *TURNFactory {
|
|
||||||
logger.Warn("QUIC 传输模式暂不支持,已跳过")
|
|
||||||
return nil // 暂时返回 nil,未来实现 QUIC 支持时再完善
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
package connect
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WSClient WebSocket 客户端
|
|
||||||
type WSClient struct {
|
|
||||||
servers []string
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWSClient 创建 WebSocket 客户端
|
|
||||||
func NewWSClient(servers []string, logger *zap.Logger) *WSClient {
|
|
||||||
return &WSClient{
|
|
||||||
servers: servers,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect 连接到 WebSocket 服务器
|
|
||||||
func (c *WSClient) Connect(ctx context.Context) (net.Conn, error) {
|
|
||||||
for _, server := range c.servers {
|
|
||||||
conn, err := c.connectServer(ctx, server)
|
|
||||||
if err == nil {
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
c.logger.Warn("WebSocket 服务器连接失败",
|
|
||||||
zap.String("server", server),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("所有 WebSocket 服务器均连接失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectServer 连接单个服务器
|
|
||||||
func (c *WSClient) connectServer(ctx context.Context, server string) (net.Conn, error) {
|
|
||||||
dialer := websocket.Dialer{
|
|
||||||
HandshakeTimeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
wsConn, _, err := dialer.DialContext(ctx, server, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("WebSocket 握手失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Debug("WebSocket 连接已建立",
|
|
||||||
zap.String("local_addr", wsConn.LocalAddr().String()),
|
|
||||||
zap.String("remote_addr", wsConn.RemoteAddr().String()))
|
|
||||||
|
|
||||||
return NewWSConn(wsConn, c.logger), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WSFactory WebSocket 传输工厂
|
|
||||||
type WSFactory struct {
|
|
||||||
client *WSClient
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWSFactory 创建 WebSocket 工厂
|
|
||||||
func NewWSFactory(servers []string, logger *zap.Logger) *WSFactory {
|
|
||||||
return &WSFactory{
|
|
||||||
client: NewWSClient(servers, logger),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer 返回传输层类型
|
|
||||||
func (f *WSFactory) Layer() Layer {
|
|
||||||
return LayerWS
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name 返回名称
|
|
||||||
func (f *WSFactory) Name() string {
|
|
||||||
return "WS/WSS"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 建立 WebSocket 连接
|
|
||||||
func (f *WSFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
|
||||||
f.logger.Info("开始建立 WebSocket 连接",
|
|
||||||
zap.String("peer_id", config.PeerID),
|
|
||||||
zap.Strings("ws_servers", config.WSServers))
|
|
||||||
|
|
||||||
servers := config.WSServers
|
|
||||||
if len(servers) == 0 {
|
|
||||||
servers = f.client.servers
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(servers) == 0 {
|
|
||||||
return nil, fmt.Errorf("未配置 WebSocket 服务器")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.client.servers = servers
|
|
||||||
return f.client.Connect(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WSConn WebSocket 连接包装器(实现 net.Conn)
|
|
||||||
type WSConn struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
localAddr net.Addr
|
|
||||||
remoteAddr net.Addr
|
|
||||||
readBuf []byte
|
|
||||||
mu sync.Mutex
|
|
||||||
closed bool
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWSConn 创建 WebSocket net.Conn 包装器
|
|
||||||
func NewWSConn(wsConn *websocket.Conn, logger *zap.Logger) *WSConn {
|
|
||||||
return &WSConn{
|
|
||||||
conn: wsConn,
|
|
||||||
localAddr: wsConn.LocalAddr(),
|
|
||||||
remoteAddr: wsConn.RemoteAddr(),
|
|
||||||
readBuf: make([]byte, 0),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read 实现 net.Conn
|
|
||||||
func (c *WSConn) Read(b []byte) (n int, err error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.readBuf) > 0 {
|
|
||||||
n = copy(b, c.readBuf)
|
|
||||||
c.readBuf = c.readBuf[n:]
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, message, err := c.conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n = copy(b, message)
|
|
||||||
if n < len(message) {
|
|
||||||
c.readBuf = append(c.readBuf, message[n:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 实现 net.Conn
|
|
||||||
func (c *WSConn) Write(b []byte) (n int, err error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.conn.WriteMessage(websocket.BinaryMessage, b)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 实现 net.Conn
|
|
||||||
func (c *WSConn) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.closed = true
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr 实现 net.Conn
|
|
||||||
func (c *WSConn) LocalAddr() net.Addr {
|
|
||||||
return c.localAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr 实现 net.Conn
|
|
||||||
func (c *WSConn) RemoteAddr() net.Addr {
|
|
||||||
return c.remoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDeadline 实现 net.Conn
|
|
||||||
func (c *WSConn) SetDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline 实现 net.Conn
|
|
||||||
func (c *WSConn) SetReadDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline 实现 net.Conn
|
|
||||||
func (c *WSConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return c.conn.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsClosed 检查是否已关闭
|
|
||||||
func (c *WSConn) IsClosed() bool {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.closed
|
|
||||||
}
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Core 进程入口 - 管理多个 Engine 实例
|
|
||||||
type Core struct {
|
|
||||||
engines map[string]*Engine // engineID -> Engine
|
|
||||||
mu sync.RWMutex
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCore 创建 Core 实例(进程入口)
|
|
||||||
func NewCore(logger *zap.Logger) *Core {
|
|
||||||
return &Core{
|
|
||||||
engines: make(map[string]*Engine),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateEngine 创建 Engine 实例
|
|
||||||
func (c *Core) CreateEngine(engineID string, metrics *Metrics) (*Engine, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// 检查是否已存在
|
|
||||||
if _, ok := c.engines[engineID]; ok {
|
|
||||||
return nil, fmt.Errorf("engine %s already exists", engineID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新 Engine
|
|
||||||
engine := NewEngine(c.logger, metrics)
|
|
||||||
c.engines[engineID] = engine
|
|
||||||
|
|
||||||
c.logger.Info("创建 Engine 实例",
|
|
||||||
zap.String("engine_id", engineID))
|
|
||||||
|
|
||||||
return engine, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEngine 获取 Engine 实例
|
|
||||||
func (c *Core) GetEngine(engineID string) (*Engine, error) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
engine, ok := c.engines[engineID]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("engine %s not found", engineID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return engine, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveEngine 移除 Engine 实例
|
|
||||||
func (c *Core) RemoveEngine(engineID string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
engine, ok := c.engines[engineID]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("engine %s not found", engineID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止 Engine
|
|
||||||
if err := engine.Stop(); err != nil {
|
|
||||||
c.logger.Warn("停止 Engine 失败",
|
|
||||||
zap.String("engine_id", engineID),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(c.engines, engineID)
|
|
||||||
c.logger.Info("移除 Engine 实例",
|
|
||||||
zap.String("engine_id", engineID))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEngines 列出所有 Engine ID
|
|
||||||
func (c *Core) ListEngines() []string {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
ids := make([]string, 0, len(c.engines))
|
|
||||||
for id := range c.engines {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count 获取 Engine 数量
|
|
||||||
func (c *Core) Count() int {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
return len(c.engines)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭所有 Engine
|
|
||||||
func (c *Core) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for id, engine := range c.engines {
|
|
||||||
if err := engine.Stop(); err != nil {
|
|
||||||
c.logger.Warn("停止 Engine 失败",
|
|
||||||
zap.String("engine_id", id),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.engines = make(map[string]*Engine)
|
|
||||||
c.logger.Info("关闭所有 Engine")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
-333
@@ -1,333 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.zkcoi.com/zkcoi/meshray/core/connect"
|
|
||||||
"git.zkcoi.com/zkcoi/meshray/core/plugins/wg"
|
|
||||||
"git.zkcoi.com/zkcoi/meshray/core/transport"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Engine 引擎实例 - 一个组网的引擎实例
|
|
||||||
type Engine struct {
|
|
||||||
logger *zap.Logger
|
|
||||||
scheduler *connect.StrategyScheduler
|
|
||||||
connMgr *transport.ConnManager
|
|
||||||
relay *transport.Relay
|
|
||||||
plugin transport.ProtocolPlugin
|
|
||||||
metrics *Metrics
|
|
||||||
|
|
||||||
// 候选地址存储(用于 NotifyPeerInfo)
|
|
||||||
candidateStore map[string][]Candidate
|
|
||||||
routeIDStore map[string]uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEngine 创建引擎实例
|
|
||||||
func NewEngine(logger *zap.Logger, metrics *Metrics) *Engine {
|
|
||||||
// 创建 WG 插件
|
|
||||||
plugin := wg.NewWGPlugin()
|
|
||||||
|
|
||||||
// 创建连接管理器
|
|
||||||
connMgr := transport.NewConnManager(logger)
|
|
||||||
|
|
||||||
// 创建数据转发器
|
|
||||||
relay := transport.NewRelay(plugin, connMgr, logger)
|
|
||||||
|
|
||||||
// 创建策略调度器
|
|
||||||
scheduler := connect.NewStrategyScheduler(logger)
|
|
||||||
scheduler.OnConnectionUpdate = func(peerID string, conn net.Conn, err error) {
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("收到策略调度器连接错误更新", zap.String("peer_id", peerID), zap.Error(err))
|
|
||||||
}
|
|
||||||
if conn != nil {
|
|
||||||
logger.Info("策略调度器连接已建立,开始双向转发", zap.String("peer_id", peerID))
|
|
||||||
connMgr.Add(peerID, conn)
|
|
||||||
// 启动远端接收协程
|
|
||||||
relay.StartReadFromRemoteConn(context.Background(), peerID, conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduler.RegisterFactory(connect.NewDirectFactory(nil, logger)) // 1. Direct-UDP
|
|
||||||
scheduler.RegisterFactory(connect.NewFakeTCPFactory(logger)) // 2. FakeTCP
|
|
||||||
scheduler.RegisterFactory(connect.NewRealTCPFactory(logger)) // 3. RealTCP
|
|
||||||
scheduler.RegisterFactory(connect.NewTURNFactory(connect.TURNProtocolUDP, nil, "", "", logger)) // 4. TURN-UDP
|
|
||||||
// scheduler.RegisterFactory(connect.NewTURNFactoryQUIC(nil, "", "", logger)) // 5. TURN-QUIC (暂不启用)
|
|
||||||
scheduler.RegisterFactory(connect.NewTURNFactory(connect.TURNProtocolTCP, nil, "", "", logger)) // 6. TURN-TCP
|
|
||||||
scheduler.RegisterFactory(connect.NewTURNFactory(connect.TURNProtocolTLS, nil, "", "", logger)) // 7. TURN-TLS
|
|
||||||
scheduler.RegisterFactory(connect.NewWebRTCFactory(&connect.ICEConfig{}, logger)) // 8. WebRTC
|
|
||||||
scheduler.RegisterFactory(connect.NewWSFactory(nil, logger)) // 9. WS/WSS
|
|
||||||
|
|
||||||
engine := &Engine{
|
|
||||||
logger: logger,
|
|
||||||
scheduler: scheduler,
|
|
||||||
connMgr: connMgr,
|
|
||||||
relay: relay,
|
|
||||||
plugin: plugin,
|
|
||||||
metrics: metrics,
|
|
||||||
candidateStore: make(map[string][]Candidate),
|
|
||||||
routeIDStore: make(map[string]uint32),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置拨号触发器:当 WG 发包但没连接时自动 9 层拨号
|
|
||||||
relay.OnDialTrigger = func(peerKey string) {
|
|
||||||
go engine.initiateConnection(peerKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return engine
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start 启动引擎
|
|
||||||
func (e *Engine) Start() error {
|
|
||||||
e.logger.Info("Core 引擎启动")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop 停止引擎
|
|
||||||
func (e *Engine) Stop() error {
|
|
||||||
e.logger.Info("Core 引擎停止")
|
|
||||||
|
|
||||||
// 关闭所有连接
|
|
||||||
if e.connMgr != nil {
|
|
||||||
e.connMgr.CloseAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetICEConfig 设置 ICE 配置(用于 WebRTC)
|
|
||||||
func (e *Engine) SetICEConfig(config connect.ICEConfig) error {
|
|
||||||
e.logger.Info("更新 ICE 配置",
|
|
||||||
zap.Int("stun_servers", len(config.STUNServers)),
|
|
||||||
zap.Int("turn_servers", len(config.TURNServers)))
|
|
||||||
|
|
||||||
// TODO: 实现 ICE 配置更新逻辑
|
|
||||||
// 1. 找到 WebRTC 工厂
|
|
||||||
// 2. 更新其 ICE 配置
|
|
||||||
// 3. 重新注册工厂
|
|
||||||
|
|
||||||
// 目前先记录日志,P3 阶段实现
|
|
||||||
e.logger.Warn("SetICEConfig 暂未实现,将在 P3 阶段完成")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetScheduler 获取策略调度器
|
|
||||||
func (e *Engine) GetScheduler() *connect.StrategyScheduler {
|
|
||||||
return e.scheduler
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConnMgr 获取连接管理器
|
|
||||||
func (e *Engine) GetConnMgr() *transport.ConnManager {
|
|
||||||
return e.connMgr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRelay 获取数据转发器
|
|
||||||
func (e *Engine) GetRelay() *transport.Relay {
|
|
||||||
return e.relay
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetrics 获取监控指标
|
|
||||||
func (e *Engine) GetMetrics() *Metrics {
|
|
||||||
return e.metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind 为指定 Peer 开启本地端口,开始建连
|
|
||||||
// peerKey: 对端公钥哈希(8 字符)
|
|
||||||
// localPort: 本地监听端口(传 0 表示系统自动分配)
|
|
||||||
// 返回值:实际绑定的端口号
|
|
||||||
func (e *Engine) Bind(peerKey string, localPort int) (int, error) {
|
|
||||||
// 1. 在本地端口监听
|
|
||||||
addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: localPort}
|
|
||||||
conn, err := net.ListenUDP("udp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("监听本地端口失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
actualPort := conn.LocalAddr().(*net.UDPAddr).Port
|
|
||||||
|
|
||||||
// 2. 提取 route_id(从 peerKey 派生)
|
|
||||||
routeID := extractRouteID(peerKey)
|
|
||||||
|
|
||||||
// 3. 注册到 Relay
|
|
||||||
e.relay.RegisterLocalPort(routeID, conn)
|
|
||||||
|
|
||||||
// 4. 注册到 ConnManager
|
|
||||||
e.connMgr.Add(peerKey, nil) // conn 初始为 nil,建连后设置
|
|
||||||
|
|
||||||
// 5. 启动读取协程
|
|
||||||
ctx := context.Background()
|
|
||||||
e.relay.StartReadFromLocalPort(ctx, routeID, peerKey)
|
|
||||||
|
|
||||||
e.logger.Info("Bind 成功",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Int("local_port", actualPort),
|
|
||||||
zap.Uint32("route_id", routeID))
|
|
||||||
|
|
||||||
return actualPort, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unbind 停止指定 Peer 的端口监听
|
|
||||||
func (e *Engine) Unbind(peerKey string) error {
|
|
||||||
// 1. 提取 route_id
|
|
||||||
routeID := extractRouteID(peerKey)
|
|
||||||
|
|
||||||
// 2. 从 Relay 注销
|
|
||||||
e.relay.UnregisterLocalPort(routeID)
|
|
||||||
|
|
||||||
// 3. 从 ConnManager 移除
|
|
||||||
e.connMgr.Remove(peerKey)
|
|
||||||
|
|
||||||
// 4. 清理存储
|
|
||||||
delete(e.candidateStore, peerKey)
|
|
||||||
delete(e.routeIDStore, peerKey)
|
|
||||||
|
|
||||||
e.logger.Info("Unbind 成功",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Uint32("route_id", routeID))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EngineStatus Engine 状态
|
|
||||||
type EngineStatus struct {
|
|
||||||
PeerCount int `json:"peer_count"`
|
|
||||||
Peers map[string]*PeerStatus `json:"peers"`
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
ActiveConnections int64 `json:"active_connections"`
|
|
||||||
TotalConnections int64 `json:"total_connections"`
|
|
||||||
BytesSent uint64 `json:"bytes_sent"`
|
|
||||||
BytesReceived uint64 `json:"bytes_received"`
|
|
||||||
StrategyFallbacks int64 `json:"strategy_fallbacks"`
|
|
||||||
LastSwitchTime int64 `json:"last_switch_time"` // unix timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeerStatus Peer 状态
|
|
||||||
type PeerStatus struct {
|
|
||||||
PeerKey string `json:"peer_key"`
|
|
||||||
Connected bool `json:"connected"`
|
|
||||||
Layer string `json:"layer,omitempty"` // 当前传输层
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatus 查询 Engine 状态
|
|
||||||
func (e *Engine) GetStatus() (*EngineStatus, error) {
|
|
||||||
status := &EngineStatus{
|
|
||||||
PeerCount: e.connMgr.Count(),
|
|
||||||
Peers: make(map[string]*PeerStatus),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集 Metrics
|
|
||||||
if e.metrics != nil {
|
|
||||||
status.ActiveConnections = e.metrics.GetActiveConnections()
|
|
||||||
status.TotalConnections = e.metrics.GetTotalConnections()
|
|
||||||
status.BytesSent = e.metrics.GetBytesSent()
|
|
||||||
status.BytesReceived = e.metrics.GetBytesReceived()
|
|
||||||
status.StrategyFallbacks = e.metrics.GetStrategyFallbacks()
|
|
||||||
status.LastSwitchTime = e.metrics.GetLastSwitchTime().Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集所有 Peer 状态
|
|
||||||
for peerKey, conn := range e.connMgr.GetAll() {
|
|
||||||
peerStatus := &PeerStatus{
|
|
||||||
PeerKey: peerKey,
|
|
||||||
Connected: conn != nil,
|
|
||||||
}
|
|
||||||
if conn != nil {
|
|
||||||
peerStatus.Layer = e.scheduler.GetPeerLayer(peerKey).String()
|
|
||||||
}
|
|
||||||
status.Peers[peerKey] = peerStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
return status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Candidate 候选地址(与 connect.Candidate 对齐)
|
|
||||||
type Candidate struct {
|
|
||||||
Addr string `json:"addr"` // 候选地址(ip:port)
|
|
||||||
Type string `json:"type"` // 候选类型:host/srflx/relay
|
|
||||||
Priority int `json:"priority"` // 优先级
|
|
||||||
Protocol string `json:"protocol"` // 协议:udp/tcp
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifyPeerInfo 下发对端候选地址和 route_id
|
|
||||||
// peerKey: 对端公钥哈希
|
|
||||||
// candidates: 对端候选地址列表(由信使服务器转发)
|
|
||||||
// routeID: 路由 ID(用于数据转发)
|
|
||||||
func (e *Engine) NotifyPeerInfo(peerKey string, candidates []Candidate, routeID uint32) error {
|
|
||||||
// 1. 存储候选地址(用于后续建连)
|
|
||||||
e.candidateStore[peerKey] = candidates
|
|
||||||
|
|
||||||
// 2. 存储 route_id 映射
|
|
||||||
e.routeIDStore[peerKey] = routeID
|
|
||||||
|
|
||||||
// 3. 触发建连流程
|
|
||||||
go e.initiateConnection(peerKey)
|
|
||||||
|
|
||||||
e.logger.Info("NotifyPeerInfo 成功",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Int("candidate_count", len(candidates)),
|
|
||||||
zap.Uint32("route_id", routeID))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiateConnection 触发建连流程
|
|
||||||
func (e *Engine) initiateConnection(peerKey string) {
|
|
||||||
// 1. 获取候选地址
|
|
||||||
candidates := e.candidateStore[peerKey]
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查是否已经在拨号或已连接
|
|
||||||
if conn, ok := e.connMgr.Get(peerKey); ok && conn != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 初始化 DialConfig
|
|
||||||
config := &connect.DialConfig{
|
|
||||||
PeerID: peerKey,
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
Logger: e.logger,
|
|
||||||
}
|
|
||||||
|
|
||||||
e.logger.Info("开始建立连接到对端",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Int("candidate_count", len(candidates)))
|
|
||||||
|
|
||||||
// 3. 获取 RouteID
|
|
||||||
_, ok := e.routeIDStore[peerKey]
|
|
||||||
if !ok {
|
|
||||||
e.logger.Warn("未找到 route_id",
|
|
||||||
zap.String("peer_key", peerKey))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 使用策略调度器尝试建连
|
|
||||||
conn, err := e.scheduler.Dial(config)
|
|
||||||
if err != nil {
|
|
||||||
e.logger.Error("所有策略层尝试连接均失败",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 连接成功,更新到 ConnManager
|
|
||||||
e.connMgr.Add(peerKey, conn)
|
|
||||||
e.logger.Info("连接建立并更新成功", zap.String("peer_key", peerKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractRouteID 从 peerKey 提取 route_id(简化版本)
|
|
||||||
// 实际应该使用一致的哈希算法
|
|
||||||
func extractRouteID(peerKey string) uint32 {
|
|
||||||
// 简单哈希:取前 4 个字符的 ASCII 码和
|
|
||||||
var sum uint32 = 0
|
|
||||||
for i := 0; i < len(peerKey) && i < 4; i++ {
|
|
||||||
sum += uint32(peerKey[i])
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Metrics Core 监控指标
|
|
||||||
type Metrics struct {
|
|
||||||
// 连接统计
|
|
||||||
activeConnections atomic.Int64
|
|
||||||
totalConnections atomic.Int64
|
|
||||||
|
|
||||||
// 流量统计
|
|
||||||
bytesSent atomic.Uint64
|
|
||||||
bytesReceived atomic.Uint64
|
|
||||||
|
|
||||||
// 策略统计
|
|
||||||
strategyFallbacks atomic.Int64
|
|
||||||
lastSwitchTime atomic.Int64 // Unix timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMetrics 创建监控指标
|
|
||||||
func NewMetrics() *Metrics {
|
|
||||||
return &Metrics{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetActiveConnections 获取活跃连接数
|
|
||||||
func (m *Metrics) GetActiveConnections() int64 {
|
|
||||||
return m.activeConnections.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrActiveConnections 增加活跃连接数
|
|
||||||
func (m *Metrics) IncrActiveConnections() {
|
|
||||||
m.activeConnections.Add(1)
|
|
||||||
m.totalConnections.Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecrActiveConnections 减少活跃连接数
|
|
||||||
func (m *Metrics) DecrActiveConnections() {
|
|
||||||
m.activeConnections.Add(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddBytesSent 增加发送字节数
|
|
||||||
func (m *Metrics) AddBytesSent(n uint64) {
|
|
||||||
m.bytesSent.Add(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddBytesReceived 增加接收字节数
|
|
||||||
func (m *Metrics) AddBytesReceived(n uint64) {
|
|
||||||
m.bytesReceived.Add(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrStrategyFallbacks 增加策略降级次数
|
|
||||||
func (m *Metrics) IncrStrategyFallbacks() {
|
|
||||||
m.strategyFallbacks.Add(1)
|
|
||||||
m.lastSwitchTime.Store(time.Now().Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTotalConnections 获取总连接数
|
|
||||||
func (m *Metrics) GetTotalConnections() int64 {
|
|
||||||
return m.totalConnections.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBytesSent 获取发送字节数
|
|
||||||
func (m *Metrics) GetBytesSent() uint64 {
|
|
||||||
return m.bytesSent.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBytesReceived 获取接收字节数
|
|
||||||
func (m *Metrics) GetBytesReceived() uint64 {
|
|
||||||
return m.bytesReceived.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStrategyFallbacks 获取策略降级次数
|
|
||||||
func (m *Metrics) GetStrategyFallbacks() int64 {
|
|
||||||
return m.strategyFallbacks.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLastSwitchTime 获取最后切换时间
|
|
||||||
func (m *Metrics) GetLastSwitchTime() time.Time {
|
|
||||||
return time.Unix(m.lastSwitchTime.Load(), 0)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package wg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WGPlugin WireGuard 协议插件实现
|
|
||||||
type WGPlugin struct{}
|
|
||||||
|
|
||||||
// NewWGPlugin 创建 WireGuard 协议插件
|
|
||||||
func NewWGPlugin() *WGPlugin {
|
|
||||||
return &WGPlugin{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsControlPacket 判断是否为控制包
|
|
||||||
// WG 控制包类型:1 (Initiation), 2 (Response), 3 (CookieReply)
|
|
||||||
func (p *WGPlugin) IsControlPacket(packet []byte) bool {
|
|
||||||
if len(packet) < 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
packetType := packet[0]
|
|
||||||
return packetType == 1 || packetType == 2 || packetType == 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDataPacket 判断是否为数据包
|
|
||||||
// WG 数据包类型:4
|
|
||||||
func (p *WGPlugin) IsDataPacket(packet []byte) bool {
|
|
||||||
if len(packet) < 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return packet[0] == 4
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractRouteID 从数据包中提取路由标识(WG receiver index)
|
|
||||||
// WG 数据包格式:[类型 (1 字节)][保留 (3 字节)][receiver index (4 字节)]...
|
|
||||||
func (p *WGPlugin) ExtractRouteID(packet []byte) (uint32, error) {
|
|
||||||
if len(packet) < 8 {
|
|
||||||
return 0, fmt.Errorf("数据包过短:%d", len(packet))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取 packet[4:8],网络字节序解析为 uint32
|
|
||||||
routeID := binary.BigEndian.Uint32(packet[4:8])
|
|
||||||
return routeID, nil
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConnManager 连接管理器
|
|
||||||
// 维护 peer_key → net.Conn 的映射关系
|
|
||||||
type ConnManager struct {
|
|
||||||
conns map[string]net.Conn
|
|
||||||
mu sync.RWMutex
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConnManager 创建连接管理器
|
|
||||||
func NewConnManager(logger *zap.Logger) *ConnManager {
|
|
||||||
return &ConnManager{
|
|
||||||
conns: make(map[string]net.Conn),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 添加连接
|
|
||||||
func (m *ConnManager) Add(peerKey string, conn net.Conn) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
// 如果已存在,先关闭旧连接
|
|
||||||
if oldConn, ok := m.conns[peerKey]; ok {
|
|
||||||
oldConn.Close()
|
|
||||||
m.logger.Debug("关闭旧连接", zap.String("peer_key", peerKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
m.conns[peerKey] = conn
|
|
||||||
m.logger.Info("添加新连接",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.String("remote_addr", conn.RemoteAddr().String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取连接
|
|
||||||
func (m *ConnManager) Get(peerKey string) (net.Conn, bool) {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
conn, ok := m.conns[peerKey]
|
|
||||||
return conn, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 移除连接
|
|
||||||
func (m *ConnManager) Remove(peerKey string) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if conn, ok := m.conns[peerKey]; ok {
|
|
||||||
conn.Close()
|
|
||||||
delete(m.conns, peerKey)
|
|
||||||
m.logger.Info("移除连接", zap.String("peer_key", peerKey))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count 获取连接数量
|
|
||||||
func (m *ConnManager) Count() int {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
return len(m.conns)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseAll 关闭所有连接
|
|
||||||
func (m *ConnManager) CloseAll() {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
for peerKey, conn := range m.conns {
|
|
||||||
conn.Close()
|
|
||||||
m.logger.Debug("关闭连接", zap.String("peer_key", peerKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
m.conns = make(map[string]net.Conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 列出所有连接(返回副本)
|
|
||||||
func (m *ConnManager) List() map[string]net.Conn {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
result := make(map[string]net.Conn)
|
|
||||||
for k, v := range m.conns {
|
|
||||||
result[k] = v
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAll 获取所有连接(同 List,为了兼容)
|
|
||||||
func (m *ConnManager) GetAll() map[string]net.Conn {
|
|
||||||
return m.List()
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
// ProtocolPlugin 协议插件接口
|
|
||||||
// relay.go 通过这个接口适配不同协议,不感知具体协议细节
|
|
||||||
type ProtocolPlugin interface {
|
|
||||||
// IsControlPacket 判断是否为控制包
|
|
||||||
// 控制包用于建连协商,需要透传到对端
|
|
||||||
IsControlPacket(packet []byte) bool
|
|
||||||
|
|
||||||
// IsDataPacket 判断是否为数据包
|
|
||||||
// 数据包包含路由标识,需要查表转发
|
|
||||||
IsDataPacket(packet []byte) bool
|
|
||||||
|
|
||||||
// ExtractRouteID 从数据包中提取路由标识
|
|
||||||
// 返回的 route_id 用于查找对应的本地端口
|
|
||||||
ExtractRouteID(packet []byte) (uint32, error)
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnDialTrigger 当从本地端口收到包但没有远端连接时触发
|
|
||||||
type OnDialTrigger func(peerKey string)
|
|
||||||
|
|
||||||
// Relay 数据转发器
|
|
||||||
// 负责从本地端口收包 → 查路由 → 通过 conn 发送
|
|
||||||
// 从 conn 收包 → 发到本地端口
|
|
||||||
type Relay struct {
|
|
||||||
plugin ProtocolPlugin
|
|
||||||
connMgr *ConnManager
|
|
||||||
localPorts map[uint32]net.PacketConn // route_id → local_port
|
|
||||||
lastAddr map[uint32]net.Addr // route_id → last wg source addr
|
|
||||||
portMu sync.RWMutex
|
|
||||||
logger *zap.Logger
|
|
||||||
OnDialTrigger OnDialTrigger // 拨号触发回调
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRelay 创建数据转发器
|
|
||||||
func NewRelay(plugin ProtocolPlugin, connMgr *ConnManager, logger *zap.Logger) *Relay {
|
|
||||||
return &Relay{
|
|
||||||
plugin: plugin,
|
|
||||||
connMgr: connMgr,
|
|
||||||
localPorts: make(map[uint32]net.PacketConn),
|
|
||||||
lastAddr: make(map[uint32]net.Addr),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterLocalPort 注册本地端口(用于接收 WG 密文包)
|
|
||||||
func (r *Relay) RegisterLocalPort(routeID uint32, port net.PacketConn) {
|
|
||||||
r.portMu.Lock()
|
|
||||||
defer r.portMu.Unlock()
|
|
||||||
|
|
||||||
r.localPorts[routeID] = port
|
|
||||||
r.logger.Info("注册本地端口",
|
|
||||||
zap.Uint32("route_id", routeID),
|
|
||||||
zap.String("addr", port.LocalAddr().String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnregisterLocalPort 注销本地端口
|
|
||||||
func (r *Relay) UnregisterLocalPort(routeID uint32) {
|
|
||||||
r.portMu.Lock()
|
|
||||||
defer r.portMu.Unlock()
|
|
||||||
|
|
||||||
if port, ok := r.localPorts[routeID]; ok {
|
|
||||||
port.Close()
|
|
||||||
delete(r.localPorts, routeID)
|
|
||||||
delete(r.lastAddr, routeID)
|
|
||||||
r.logger.Info("注销本地端口", zap.Uint32("route_id", routeID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartReadFromLocalPort 从本地端口读取 WG 密文包并转发(发送到远端)
|
|
||||||
func (r *Relay) StartReadFromLocalPort(ctx context.Context, routeID uint32, peerKey string) {
|
|
||||||
r.portMu.RLock()
|
|
||||||
port, ok := r.localPorts[routeID]
|
|
||||||
r.portMu.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
r.logger.Warn("本地端口未注册", zap.Uint32("route_id", routeID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
buf := make([]byte, 65535)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
n, addr, err := port.ReadFrom(buf)
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否是由于关闭引起的错误
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
r.logger.Debug("读取本地端口失败",
|
|
||||||
zap.Uint32("route_id", routeID),
|
|
||||||
zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录 WG 的来源地址,以便后续把包发回去
|
|
||||||
r.portMu.Lock()
|
|
||||||
r.lastAddr[routeID] = addr
|
|
||||||
r.portMu.Unlock()
|
|
||||||
|
|
||||||
packet := buf[:n]
|
|
||||||
r.forwardOutgoing(ctx, packet, peerKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
r.logger.Info("启动本地端口读取协程",
|
|
||||||
zap.Uint32("route_id", routeID),
|
|
||||||
zap.String("peer_key", peerKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartReadFromRemoteConn 从远端连接读取数据并转发给本地监听端口(接收远端数据)
|
|
||||||
func (r *Relay) StartReadFromRemoteConn(ctx context.Context, peerKey string, conn net.Conn) {
|
|
||||||
if conn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
buf := make([]byte, 65535)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Debug("读取远端连接失败,停止读取协程",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
packet := buf[:n]
|
|
||||||
// 远端进来的包,需要根据 packet 里的索引转发给对应的 localPort
|
|
||||||
r.forwardIncoming(packet, peerKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
r.logger.Info("启动远端连接读取协程",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.String("addr", conn.RemoteAddr().String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// forwardOutgoing 处理发出去的包(Local -> Remote)
|
|
||||||
func (r *Relay) forwardOutgoing(ctx context.Context, packet []byte, peerKey string) {
|
|
||||||
// 获取或触发建连
|
|
||||||
conn, ok := r.connMgr.Get(peerKey)
|
|
||||||
if !ok || conn == nil {
|
|
||||||
// 没有连接,触发拨号
|
|
||||||
if r.OnDialTrigger != nil {
|
|
||||||
r.OnDialTrigger(peerKey)
|
|
||||||
}
|
|
||||||
r.logger.Debug("尚未建立连接,包已丢弃,触发静默拨号", zap.String("peer_key", peerKey))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转发给远端
|
|
||||||
_, err := conn.Write(packet)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Debug("转发包到远端失败",
|
|
||||||
zap.String("peer_key", peerKey),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// forwardIncoming 处理进来的包(Remote -> Local)
|
|
||||||
func (r *Relay) forwardIncoming(packet []byte, _ string) {
|
|
||||||
// 1. 判断是否为控制包/数据包并提取 routeID
|
|
||||||
// 无论哪种 WG 包,前几位都是 routeID (receiver index)
|
|
||||||
routeID, err := r.plugin.ExtractRouteID(packet)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Debug("提取包内索引失败", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 这里的 routeID 是我们 RegisterLocalPort 时用的 ID
|
|
||||||
r.portMu.RLock()
|
|
||||||
port, ok := r.localPorts[routeID]
|
|
||||||
addr, addrOk := r.lastAddr[routeID]
|
|
||||||
r.portMu.RUnlock()
|
|
||||||
|
|
||||||
if !ok || port == nil {
|
|
||||||
r.logger.Debug("未找到转发目标的本地端口", zap.Uint32("route_id", routeID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addrOk || addr == nil {
|
|
||||||
// 如果还没收到过 WG 的包,尝试发给 127.0.0.1:0 (通常不会成功,但作为 fallback)
|
|
||||||
// 实际上 WG 发送握手包后就会刷新 addr
|
|
||||||
addr = &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 转发给本地 WG
|
|
||||||
_, err = port.WriteTo(packet, addr)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Debug("转发给本地 WG 失败", zap.Uint32("route_id", routeID), zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,18 @@ module git.zkcoi.com/zkcoi/meshray
|
|||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
// core 模块使用本地目录
|
// 开源契约层(管理器与 meshray-core 共享,独立叶子模块,避免 module 级循环依赖)
|
||||||
replace git.zkcoi.com/zkcoi/meshray/core => ./core
|
replace git.zkcoi.com/zkcoi/meshray-contract => ./pkg/contract
|
||||||
|
|
||||||
|
// 核心转发引擎 meshray(去中心化 frp 本体 + 9 层传输):独立仓库,零 wireguard 依赖。
|
||||||
|
// 模块路径为 git.zkcoi.com/zkcoi/meshray/core(避免与本品 meshray 模块路径冲突)。
|
||||||
|
// 管理器经此 replace 引用本地核心,wg-go 集成(corebind)在本仓库内,二者隔离。
|
||||||
|
replace git.zkcoi.com/zkcoi/meshray/core => ../Meshray
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.zkcoi.com/zkcoi/meshray-contract v0.0.0
|
||||||
|
git.zkcoi.com/zkcoi/meshray/core v0.0.0
|
||||||
|
github.com/BurntSushi/toml v1.4.0
|
||||||
github.com/getlantern/systray v1.2.2
|
github.com/getlantern/systray v1.2.2
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||||
|
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ func NewServer(cfg *config.Config, logger *zap.Logger, dbStore *sqlite.Store) (*
|
|||||||
store: dbStore,
|
store: dbStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首次启动初始化管理员账户(已存在则跳过,仅新建时返回随机密码)
|
||||||
|
userService := service.NewUserService(dbStore)
|
||||||
|
if uname, pwd, ierr := userService.InitializeAdmin(); ierr == nil {
|
||||||
|
if pwd != "" {
|
||||||
|
server.logger.Warn("已创建默认管理员账户,请尽快登录并修改密码",
|
||||||
|
zap.String("username", uname), zap.String("password", pwd))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
server.logger.Error("初始化管理员账户失败", zap.Error(ierr))
|
||||||
|
}
|
||||||
|
|
||||||
// 注册中间件
|
// 注册中间件
|
||||||
server.registerMiddleware()
|
server.registerMiddleware()
|
||||||
|
|
||||||
@@ -142,7 +153,9 @@ func (s *Server) registerRoutes() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 尝试直接提供文件
|
// 尝试直接提供文件
|
||||||
|
// staticFS 已通过 fs.Sub 剥离 "static" 前缀,需先去掉路径中的 "static/"
|
||||||
filePath := strings.TrimPrefix(path, "/")
|
filePath := strings.TrimPrefix(path, "/")
|
||||||
|
filePath = strings.TrimPrefix(filePath, "static/")
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
filePath = "index.html"
|
filePath = "index.html"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build meshray_core
|
||||||
|
|
||||||
|
// Package ctr 管理器在增强构建下空白导入 corebind,
|
||||||
|
// 触发其 init() 向开源契约注册表注册自定义 conn.Bind 工厂与引擎提供方。
|
||||||
|
// 默认构建不编译本文件,因此管理器可独立编译、以标准 WireGuard 直连运行。
|
||||||
|
package ctr
|
||||||
|
|
||||||
|
import _ "git.zkcoi.com/zkcoi/meshray/internal/ctr/corebind"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Package corebind 的契约类型转换:在 meshray-contract 与 meshray/core 内部类型间双向转换。
|
||||||
|
package corebind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.zkcoi.com/zkcoi/meshray-contract"
|
||||||
|
"git.zkcoi.com/zkcoi/meshray/core/connect"
|
||||||
|
"git.zkcoi.com/zkcoi/meshray/core/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 以下函数完成开源契约类型(contract 包)与 meshray/core 内部类型之间的双向转换。
|
||||||
|
// 数值型 Layer 在 contract 与 connect 中定义一致,可直接转型。
|
||||||
|
|
||||||
|
func toCoreCandidates(in []contract.Candidate) []core.Candidate {
|
||||||
|
out := make([]core.Candidate, 0, len(in))
|
||||||
|
for _, c := range in {
|
||||||
|
out = append(out, core.Candidate{
|
||||||
|
Addr: c.Addr,
|
||||||
|
Type: c.Type,
|
||||||
|
Priority: c.Priority,
|
||||||
|
Protocol: c.Protocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCoreICEConfig(in contract.ICEConfig) connect.ICEConfig {
|
||||||
|
turn := make([]connect.TURNServerConfig, 0, len(in.TURNServers))
|
||||||
|
for _, s := range in.TURNServers {
|
||||||
|
turn = append(turn, connect.TURNServerConfig{URLs: []string{s}})
|
||||||
|
}
|
||||||
|
return connect.ICEConfig{
|
||||||
|
STUNServers: in.STUNServers,
|
||||||
|
TURNServers: turn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCoreLayerOrder(in []contract.Layer) []connect.Layer {
|
||||||
|
out := make([]connect.Layer, 0, len(in))
|
||||||
|
for _, l := range in {
|
||||||
|
out = append(out, connect.Layer(l))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toContractStatus(in *core.EngineStatus) *contract.EngineStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
peers := make(map[string]*contract.PeerStatus, len(in.Peers))
|
||||||
|
for k, v := range in.Peers {
|
||||||
|
peers[k] = &contract.PeerStatus{
|
||||||
|
PeerKey: v.PeerKey,
|
||||||
|
Connected: v.Connected,
|
||||||
|
Layer: v.Layer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &contract.EngineStatus{
|
||||||
|
PeerCount: in.PeerCount,
|
||||||
|
Peers: peers,
|
||||||
|
ActiveConnections: in.ActiveConnections,
|
||||||
|
TotalConnections: in.TotalConnections,
|
||||||
|
BytesSent: in.BytesSent,
|
||||||
|
BytesReceived: in.BytesReceived,
|
||||||
|
StrategyFallbacks: in.StrategyFallbacks,
|
||||||
|
LastSwitchTime: in.LastSwitchTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
// Package corebind 是 Meshray-Manager 侧的 wireguard-go 集成层(形态 B:增强传输)。
|
||||||
|
//
|
||||||
|
// 它实现 wireguard-go 的 conn.Bind 接口(EnhancedBind),完全接管 WG 的 UDP 收发,
|
||||||
|
// 把密文流量导向 meshray 核心(git.zkcoi.com/zkcoi/meshray)的 9 层传输。
|
||||||
|
// 该层属于「管理器 / UI」范畴,而非核心;核心本身零 wireguard 依赖。
|
||||||
|
package corebind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.zkcoi.com/zkcoi/meshray/core/engine"
|
||||||
|
"golang.zx2c4.com/wireguard/conn"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnhancedBind 是 corebind 提供的 conn.Bind 实现,完全接管 WireGuard 的 UDP 收发,
|
||||||
|
// 使所有 WG 密文流量都经由 meshray 核心的 9 层传输(Direct/TURN/WebRTC/...)而非裸 UDP。
|
||||||
|
//
|
||||||
|
// 设计要点:
|
||||||
|
// - Direct-UDP 由一个绑定在 listen_port 的*共享未连接 UDP socket*(directSock)承载,
|
||||||
|
// 从任意来源收包,把「真实源地址」作为 Endpoint 交给 WG,WG 用密文内的 receiver index
|
||||||
|
// 匹配 peer 并据此更新对端出向地址(学址)。由此对称直连与 NAT 穿透学址原生可通。
|
||||||
|
// - TURN/WebRTC/WS 等中继层经核心传输连接(connMgr)承载,Direct 不可用时补充。
|
||||||
|
// - 集成通过核心暴露的钩子接入:SetPeerRegistrar / SetRelayInboundHandler /
|
||||||
|
// SetDirectByBind,核心永不反向依赖本包。
|
||||||
|
type EnhancedBind struct {
|
||||||
|
inner conn.Bind
|
||||||
|
networkID string
|
||||||
|
engine *core.Engine
|
||||||
|
logger *zap.Logger
|
||||||
|
|
||||||
|
// listenPort 本端 WG 监听端口;Direct-UDP 共享监听 socket 绑定于此,形成对称直连。
|
||||||
|
listenPort int
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
peerByAddr map[string]string // endpoint addr(str) -> peerKey(Send 反查中继连接)
|
||||||
|
peerEndpoint map[string]conn.Endpoint // peerKey -> 对端真实 Endpoint(中继入站回灌用)
|
||||||
|
|
||||||
|
// directSock 承载 Direct-UDP 的共享未连接 UDP socket(绑定 listenPort)。
|
||||||
|
// 从任意来源收包(学习对端经 NAT 后的真实源地址),并按目标地址发包。
|
||||||
|
directSock *net.UDPConn
|
||||||
|
|
||||||
|
inboundCh chan inboundPacket
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// inboundPacket 入站密文;携带来源信息以便回灌 WG 时定位对端 Endpoint。
|
||||||
|
type inboundPacket struct {
|
||||||
|
peerKey string // 中继入站时按 peerKey 查 Endpoint
|
||||||
|
ep conn.Endpoint // Direct 共享 socket 学得的真实源地址;非空时优先使用
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEnhancedBind 创建增强 Bind,并关联指定组网的 Engine 单例;
|
||||||
|
// 同时经核心钩子把入站回灌接到本 Bind,使 9 层传输连接读到的包能回到 WG。
|
||||||
|
func NewEnhancedBind(listenPort int, networkID string, logger *zap.Logger) (*EnhancedBind, error) {
|
||||||
|
eng := getOrCreateEngine(networkID, logger)
|
||||||
|
inner := conn.NewDefaultBind()
|
||||||
|
b := &EnhancedBind{
|
||||||
|
inner: inner,
|
||||||
|
networkID: networkID,
|
||||||
|
engine: eng,
|
||||||
|
logger: logger,
|
||||||
|
listenPort: listenPort,
|
||||||
|
peerByAddr: make(map[string]string),
|
||||||
|
peerEndpoint: make(map[string]conn.Endpoint),
|
||||||
|
inboundCh: make(chan inboundPacket, 1024),
|
||||||
|
}
|
||||||
|
// 通过核心钩子接入,避免核心反向依赖本包:
|
||||||
|
eng.SetPeerRegistrar(b) // 收到对端候选时注册 endpoint→peerKey
|
||||||
|
eng.SetRelayInboundHandler(b.deliverInbound) // 传输连接入站回灌 WG
|
||||||
|
eng.SetDirectByBind(true) // Direct 由本 Bind 共享 socket 承载
|
||||||
|
eng.SetListenPort(listenPort)
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine 返回关联的引擎实例(standalone 二进制用于注入候选触发拨号)
|
||||||
|
func (b *EnhancedBind) Engine() *core.Engine { return b.engine }
|
||||||
|
|
||||||
|
// RegisterPeer 注册 endpoint 地址到 peerKey 的映射,并缓存对端真实 Endpoint 供回灌使用。
|
||||||
|
// 由 Engine.NotifyPeerInfo 在收到候选时自动调用;standalone 也可直接调用。
|
||||||
|
func (b *EnhancedBind) RegisterPeer(addr string, peerKey string) {
|
||||||
|
ep, err := b.inner.ParseEndpoint(addr)
|
||||||
|
if err != nil {
|
||||||
|
b.logger.Warn("解析对端 Endpoint 失败,回灌将使用 0.0.0.0:0",
|
||||||
|
zap.String("addr", addr), zap.Error(err))
|
||||||
|
ep = nil
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
b.peerByAddr[addr] = peerKey
|
||||||
|
if ep != nil {
|
||||||
|
b.peerEndpoint[peerKey] = ep
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open 创建 Direct-UDP 共享监听 socket(绑定 listen_port),并启动接收协程。
|
||||||
|
// 返回从 inboundCh 读取入站密文的接收函数:Direct 入站与中继入站统一经此回灌 WG。
|
||||||
|
func (b *EnhancedBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.closed = false
|
||||||
|
// 端口优先用 WG 传入的 port,其次用构造时的 listenPort;0 表示由 OS 分配。
|
||||||
|
bindPort := int(port)
|
||||||
|
if bindPort == 0 {
|
||||||
|
bindPort = b.listenPort
|
||||||
|
}
|
||||||
|
// 关闭可能残留的旧 socket(重开场景)
|
||||||
|
if b.directSock != nil {
|
||||||
|
b.directSock.Close()
|
||||||
|
b.directSock = nil
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
sock, err := net.ListenUDP("udp", &net.UDPAddr{Port: bindPort})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := sock.LocalAddr().(*net.UDPAddr).Port
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
b.directSock = sock
|
||||||
|
b.listenPort = actual
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
// 回填引擎实际监听端口(listen_port=0 自动分配时同步)
|
||||||
|
if b.engine != nil {
|
||||||
|
b.engine.SetListenPort(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
go b.readDirectLoop(sock)
|
||||||
|
|
||||||
|
b.logger.Info("Direct-UDP 共享监听 socket 已就绪",
|
||||||
|
zap.Int("listen_port", actual))
|
||||||
|
|
||||||
|
// v4/v6 各一个接收函数,均从同一个 inboundCh 读取
|
||||||
|
return []conn.ReceiveFunc{b.receiveFunc, b.receiveFunc}, uint16(actual), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDirectLoop 从共享监听 socket 收包(任意来源),把「真实源地址」作为 Endpoint 投入 inboundCh。
|
||||||
|
// 这是 NAT 穿透学址的核心:公网侧据此学到对端 NAT 映射后的真实地址,回包得以正确返回。
|
||||||
|
func (b *EnhancedBind) readDirectLoop(sock *net.UDPConn) {
|
||||||
|
buf := make([]byte, 65535)
|
||||||
|
for {
|
||||||
|
n, src, err := sock.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
b.mu.RLock()
|
||||||
|
closed := b.closed
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.logger.Debug("Direct 共享 socket 读取失败,停止接收", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := make([]byte, n)
|
||||||
|
copy(data, buf[:n])
|
||||||
|
|
||||||
|
ep, perr := b.inner.ParseEndpoint(src.String())
|
||||||
|
if perr != nil {
|
||||||
|
b.logger.Debug("解析入站源地址失败,丢弃", zap.String("src", src.String()), zap.Error(perr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case b.inboundCh <- inboundPacket{ep: ep, data: data}:
|
||||||
|
default:
|
||||||
|
b.logger.Warn("入站队列满,丢弃包", zap.String("src", src.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveFunc 从 inboundCh 取出入站密文,拷贝进 WG 提供的缓冲(保持 WG 缓冲所有权),
|
||||||
|
// 并填入来源 Endpoint 供 WG 握手更新出向地址:
|
||||||
|
// - Direct 入站:ep 为共享 socket 学得的真实源地址(优先);
|
||||||
|
// - 中继入站:ep 为空,按 peerKey 查缓存的对端 Endpoint。
|
||||||
|
func (b *EnhancedBind) receiveFunc(packets [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
|
||||||
|
first, ok := <-b.inboundCh
|
||||||
|
if !ok {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
copy(packets[n], first.data)
|
||||||
|
sizes[n] = len(first.data)
|
||||||
|
eps[n] = b.endpointForPacket(first)
|
||||||
|
n++
|
||||||
|
for n < len(packets) {
|
||||||
|
select {
|
||||||
|
case pkt := <-b.inboundCh:
|
||||||
|
copy(packets[n], pkt.data)
|
||||||
|
sizes[n] = len(pkt.data)
|
||||||
|
eps[n] = b.endpointForPacket(pkt)
|
||||||
|
n++
|
||||||
|
default:
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// endpointForPacket 返回入站包的来源 Endpoint:Direct 入站直接用其学得的 ep;
|
||||||
|
// 中继入站按 peerKey 查缓存,未知时退化为 0.0.0.0:0。
|
||||||
|
func (b *EnhancedBind) endpointForPacket(pkt inboundPacket) conn.Endpoint {
|
||||||
|
if pkt.ep != nil {
|
||||||
|
return pkt.ep
|
||||||
|
}
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
if ep, ok := b.peerEndpoint[pkt.peerKey]; ok {
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
ep, _ := b.inner.ParseEndpoint("0.0.0.0:0")
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 出站路径:
|
||||||
|
// - 若该对端已建立中继/信令层连接(TURN/WebRTC/WS,位于 connMgr),经其发送;
|
||||||
|
// - 否则(Direct-UDP)经共享监听 socket 直接发往目标地址(WG 维护的对端 Endpoint:
|
||||||
|
// 初始为配置 endpoint,学址后为对端真实地址)。
|
||||||
|
func (b *EnhancedBind) Send(bufs [][]byte, ep conn.Endpoint) error {
|
||||||
|
addr := ep.DstToString()
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
peerKey, mapped := b.peerByAddr[addr]
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
if mapped {
|
||||||
|
if c, ok := b.engine.GetConnMgr().Get(peerKey); ok && c != nil {
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := c.Write(buf); err != nil {
|
||||||
|
b.logger.Debug("经中继连接发送失败,转 Direct 共享 socket",
|
||||||
|
zap.String("peer", peerKey), zap.Error(err))
|
||||||
|
return b.directSendTo(addr, bufs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Direct-UDP:共享监听 socket 直发(NAT 穿透原生支持)
|
||||||
|
return b.directSendTo(addr, bufs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// directSendTo 经共享监听 socket 把数据发往目标地址。源端口固定为 listen_port,
|
||||||
|
// 满足对称直连;NAT 后主动侧发包后,NAT 记录会话,公网侧回包得以返回。
|
||||||
|
func (b *EnhancedBind) directSendTo(addr string, bufs [][]byte) error {
|
||||||
|
b.mu.RLock()
|
||||||
|
sock := b.directSock
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if sock == nil {
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := sock.WriteToUDP(buf, udpAddr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deliverInbound 由引擎中继传输连接读取到入站时调用,把密文投入 inboundCh 回灌 WG。
|
||||||
|
// 中继入站不携带来源地址,按 peerKey 在回灌时查对端 Endpoint。
|
||||||
|
func (b *EnhancedBind) deliverInbound(peerKey string, packet []byte) {
|
||||||
|
cp := make([]byte, len(packet))
|
||||||
|
copy(cp, packet)
|
||||||
|
select {
|
||||||
|
case b.inboundCh <- inboundPacket{peerKey: peerKey, data: cp}:
|
||||||
|
default:
|
||||||
|
b.logger.Warn("入站队列满,丢弃包", zap.String("peer", peerKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭共享监听 socket 与入站通道(使接收函数返回 net.ErrClosed)。
|
||||||
|
func (b *EnhancedBind) Close() error {
|
||||||
|
b.mu.Lock()
|
||||||
|
if !b.closed {
|
||||||
|
b.closed = true
|
||||||
|
close(b.inboundCh)
|
||||||
|
}
|
||||||
|
if b.directSock != nil {
|
||||||
|
b.directSock.Close()
|
||||||
|
b.directSock = nil
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
return b.inner.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMark 本实现不涉及内核 socket,置空标记为 no-op
|
||||||
|
func (b *EnhancedBind) SetMark(mark uint32) error { return nil }
|
||||||
|
|
||||||
|
// ParseEndpoint 委托默认 Bind 解析
|
||||||
|
func (b *EnhancedBind) ParseEndpoint(s string) (conn.Endpoint, error) {
|
||||||
|
return b.inner.ParseEndpoint(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchSize 返回默认 Bind 的批量大小
|
||||||
|
func (b *EnhancedBind) BatchSize() int {
|
||||||
|
return b.inner.BatchSize()
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Package corebind 的契约注册:向开源契约(meshray-contract)注册增强 Bind 工厂与引擎提供方。
|
||||||
|
package corebind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.zkcoi.com/zkcoi/meshray-contract"
|
||||||
|
"git.zkcoi.com/zkcoi/meshray/core/engine"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.zx2c4.com/wireguard/conn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// engines 维护 networkID -> Engine 单例映射。
|
||||||
|
// EnhancedBind(收发接管)与 engineController(控制面)均通过 networkID 关联同一个 Engine,
|
||||||
|
// 使信令下发的候选地址、ICE 配置、层顺序都能落到接管该组网收发的 Engine 上。
|
||||||
|
var (
|
||||||
|
enginesMu sync.Mutex
|
||||||
|
engines = make(map[string]*core.Engine)
|
||||||
|
)
|
||||||
|
|
||||||
|
// getOrCreateEngine 获取或创建指定组网的 Engine 单例(线程安全)
|
||||||
|
func getOrCreateEngine(networkID string, logger *zap.Logger) *core.Engine {
|
||||||
|
enginesMu.Lock()
|
||||||
|
defer enginesMu.Unlock()
|
||||||
|
if e, ok := engines[networkID]; ok {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e := core.NewEngine(logger, core.NewMetrics())
|
||||||
|
engines[networkID] = e
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// engineController 适配 contract.EngineController,以 networkID 关联 Engine 单例,
|
||||||
|
// 供管理器在 enhanced 模式调用,无需直接依赖本包具体类型。
|
||||||
|
type engineController struct {
|
||||||
|
networkID string
|
||||||
|
engine *core.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *engineController) Start() error { return c.engine.Start() }
|
||||||
|
|
||||||
|
func (c *engineController) Stop() error { return c.engine.Stop() }
|
||||||
|
|
||||||
|
// NotifyPeerCandidates 由信令层调用:注入对端候选地址并触发 P2P 建连
|
||||||
|
func (c *engineController) NotifyPeerCandidates(peerKey string, candidates []contract.Candidate) error {
|
||||||
|
return c.engine.NotifyPeerInfo(peerKey, toCoreCandidates(candidates))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetICEConfig 更新 ICE/STUN/TURN 配置
|
||||||
|
func (c *engineController) SetICEConfig(cfg contract.ICEConfig) error {
|
||||||
|
return c.engine.SetICEConfig(toCoreICEConfig(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLayerOrder 设置传输层优先级顺序(策略核心配置)
|
||||||
|
func (c *engineController) SetLayerOrder(order []contract.Layer) error {
|
||||||
|
c.engine.SetLayerOrder(toCoreLayerOrder(order))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus 查询引擎状态
|
||||||
|
func (c *engineController) GetStatus() (*contract.EngineStatus, error) {
|
||||||
|
st, err := c.engine.GetStatus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toContractStatus(st), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 向开源契约注册表注册增强 Bind 工厂与引擎提供方。
|
||||||
|
// 由管理器侧 core_enable.go 在 -tags meshray_core 构建下空白导入本包触发,
|
||||||
|
// 此时管理器侧 contract.GetBindFactory(true) / NewEngineController 才能取到实现;
|
||||||
|
// 默认构建不引入本包,原生直连(默认 Bind)始终可用。
|
||||||
|
func init() {
|
||||||
|
contract.RegisterEnhancedBindFactory(func(listenPort int, networkID string, logger *zap.Logger) (conn.Bind, error) {
|
||||||
|
return NewEnhancedBind(listenPort, networkID, logger)
|
||||||
|
})
|
||||||
|
contract.RegisterEngineProvider(func(networkID string, logger *zap.Logger) (contract.EngineController, error) {
|
||||||
|
return &engineController{networkID: networkID, engine: getOrCreateEngine(networkID, logger)}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
+135
-131
@@ -5,8 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.zkcoi.com/zkcoi/meshray/core"
|
contract "git.zkcoi.com/zkcoi/meshray-contract"
|
||||||
"git.zkcoi.com/zkcoi/meshray/core/connect"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,8 +19,8 @@ type Ctr struct {
|
|||||||
// WireGuard 管理器
|
// WireGuard 管理器
|
||||||
wgManager *WGManager
|
wgManager *WGManager
|
||||||
|
|
||||||
// Core 实例(直接集成,无需 gRPC)
|
// 增强引擎实例(由 meshray-core 提供,按 networkID 索引);原生模式为空
|
||||||
coreInst *core.Core
|
engines map[string]contract.EngineController
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
@@ -38,14 +37,12 @@ func NewCtr(name string, networkID uint64, config *CtrConfig, logger *zap.Logger
|
|||||||
networkID: networkID,
|
networkID: networkID,
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
engines: make(map[string]contract.EngineController),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 WireGuard 管理器
|
// 初始化 WireGuard 管理器(两种模式都需要)
|
||||||
ctr.wgManager = NewWGManager(logger)
|
ctr.wgManager = NewWGManager(logger)
|
||||||
|
|
||||||
// 初始化 Core 实例(直接集成,无需 gRPC)
|
|
||||||
ctr.coreInst = core.NewCore(logger)
|
|
||||||
|
|
||||||
return ctr, nil
|
return ctr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,9 +55,6 @@ func (c *Ctr) Start() error {
|
|||||||
zap.String("name", c.name),
|
zap.String("name", c.name),
|
||||||
zap.Uint64("network_id", c.networkID))
|
zap.Uint64("network_id", c.networkID))
|
||||||
|
|
||||||
// ✅ Watchdog 监控暂不实现(P3 阶段)
|
|
||||||
// go c.watchdog.Start()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +66,13 @@ func (c *Ctr) Stop() error {
|
|||||||
c.logger.Info("停止 meshray-ctr",
|
c.logger.Info("停止 meshray-ctr",
|
||||||
zap.Uint64("network_id", c.networkID))
|
zap.Uint64("network_id", c.networkID))
|
||||||
|
|
||||||
// 停止 Core 实例(直接调用方法)
|
// 停止所有增强引擎
|
||||||
if c.coreInst != nil {
|
for networkID, engineCtl := range c.engines {
|
||||||
// ✅ Close 方法已实现(会停止所有 Engine)
|
if err := engineCtl.Stop(); err != nil {
|
||||||
c.coreInst.Close()
|
c.logger.Warn("停止增强引擎失败",
|
||||||
|
zap.String("network_id", networkID), zap.Error(err))
|
||||||
|
}
|
||||||
|
delete(c.engines, networkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止 WireGuard 管理
|
// 停止 WireGuard 管理
|
||||||
@@ -87,8 +84,8 @@ func (c *Ctr) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateNetwork 创建网络(根据组网模式决定行为)
|
// CreateNetwork 创建网络(根据组网模式决定行为)
|
||||||
// - 原生模式:仅创建 WG 设备
|
// - 原生模式:仅创建 WG 设备(wireguard-go 默认 Bind 直连)
|
||||||
// - 增强模式:创建 WG 设备 + 启动 Core 实例
|
// - 增强模式:WG 设备使用 meshray-core 的自定义 conn.Bind 接管收发;并启动增强引擎控制面
|
||||||
func (c *Ctr) CreateNetwork(networkID uint64, subnet string, listenPort int, meshMode string) error {
|
func (c *Ctr) CreateNetwork(networkID uint64, subnet string, listenPort int, meshMode string) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@@ -99,41 +96,30 @@ func (c *Ctr) CreateNetwork(networkID uint64, subnet string, listenPort int, mes
|
|||||||
zap.Int("listen_port", listenPort),
|
zap.Int("listen_port", listenPort),
|
||||||
zap.String("mesh_mode", meshMode))
|
zap.String("mesh_mode", meshMode))
|
||||||
|
|
||||||
// 1. 创建 WireGuard 设备(两种模式都需要)
|
// 1. 创建 WireGuard 设备(两种模式都需要;增强模式使用 core 注册的自定义 Bind)
|
||||||
networkIDStr := strconv.FormatUint(networkID, 10)
|
networkIDStr := strconv.FormatUint(networkID, 10)
|
||||||
if err := c.wgManager.CreateDevice(networkIDStr, subnet, listenPort); err != nil {
|
if err := c.wgManager.CreateDevice(networkIDStr, subnet, listenPort, meshMode); err != nil {
|
||||||
return fmt.Errorf("创建 WireGuard 设备失败:%w", err)
|
return fmt.Errorf("创建 WireGuard 设备失败:%w", err)
|
||||||
}
|
}
|
||||||
c.logger.Debug("WireGuard 设备创建成功",
|
c.logger.Debug("WireGuard 设备创建成功",
|
||||||
zap.Uint64("network_id", networkID))
|
zap.Uint64("network_id", networkID))
|
||||||
|
|
||||||
// 2. 仅增强模式需要创建并启动 Core 实例
|
// 2. 仅增强模式需要启动增强引擎(控制面)
|
||||||
if meshMode == "enhanced" {
|
if meshMode == "enhanced" {
|
||||||
// 直接调用 Core 实例的方法(无需 gRPC)
|
engineCtl, err := contract.NewEngineController(networkIDStr, c.logger)
|
||||||
metrics := core.NewMetrics()
|
|
||||||
engine, err := c.coreInst.CreateEngine(networkIDStr, metrics)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.wgManager.DeleteDevice(networkIDStr) // 回滚 WG设备
|
|
||||||
c.logger.Error("创建 Engine 失败",
|
|
||||||
zap.Uint64("network_id", networkID),
|
|
||||||
zap.Error(err))
|
|
||||||
return fmt.Errorf("创建 Engine 失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := engine.Start(); err != nil {
|
|
||||||
// ✅ 调用 Stop 方法停止(会清理所有资源)
|
|
||||||
engine.Stop()
|
|
||||||
c.wgManager.DeleteDevice(networkIDStr) // 回滚 WG 设备
|
c.wgManager.DeleteDevice(networkIDStr) // 回滚 WG 设备
|
||||||
c.logger.Error("启动 Engine 失败",
|
return fmt.Errorf("增强模式需 meshray-core:%w", err)
|
||||||
zap.Uint64("network_id", networkID),
|
|
||||||
zap.Error(err))
|
|
||||||
return fmt.Errorf("启动 Engine 失败:%w", err)
|
|
||||||
}
|
}
|
||||||
|
if err := engineCtl.Start(); err != nil {
|
||||||
c.logger.Info("增强模式:Engine 实例已启动",
|
c.wgManager.DeleteDevice(networkIDStr) // 回滚 WG 设备
|
||||||
|
return fmt.Errorf("启动增强引擎失败:%w", err)
|
||||||
|
}
|
||||||
|
c.engines[networkIDStr] = engineCtl
|
||||||
|
c.logger.Info("增强模式:引擎已启动(收发由 meshray-core conn.Bind 接管)",
|
||||||
zap.Uint64("network_id", networkID))
|
zap.Uint64("network_id", networkID))
|
||||||
} else {
|
} else {
|
||||||
c.logger.Info("原生模式:仅创建 WG 设备,不启动 Core",
|
c.logger.Info("原生模式:仅创建 WG 设备,标准直连",
|
||||||
zap.Uint64("network_id", networkID))
|
zap.Uint64("network_id", networkID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,11 +138,17 @@ func (c *Ctr) DeleteNetwork(networkID uint64) error {
|
|||||||
c.logger.Info("开始删除网络",
|
c.logger.Info("开始删除网络",
|
||||||
zap.Uint64("network_id", networkID))
|
zap.Uint64("network_id", networkID))
|
||||||
|
|
||||||
// 1. Core 实例不需要显式删除(它是内存中的对象)
|
|
||||||
// 如果需要清理,可以在未来实现 Engine.Stop() 方法
|
|
||||||
networkIDStr := strconv.FormatUint(networkID, 10)
|
networkIDStr := strconv.FormatUint(networkID, 10)
|
||||||
|
|
||||||
// 2. 删除 WireGuard 设备
|
// 停止并清理增强引擎(若存在)
|
||||||
|
if engineCtl, ok := c.engines[networkIDStr]; ok {
|
||||||
|
if err := engineCtl.Stop(); err != nil {
|
||||||
|
c.logger.Warn("停止增强引擎失败", zap.String("network_id", networkIDStr), zap.Error(err))
|
||||||
|
}
|
||||||
|
delete(c.engines, networkIDStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 WireGuard 设备
|
||||||
if err := c.wgManager.DeleteDevice(networkIDStr); err != nil {
|
if err := c.wgManager.DeleteDevice(networkIDStr); err != nil {
|
||||||
return fmt.Errorf("删除 WireGuard 设备失败:%w", err)
|
return fmt.Errorf("删除 WireGuard 设备失败:%w", err)
|
||||||
}
|
}
|
||||||
@@ -182,29 +174,13 @@ func (c *Ctr) AddPeer(networkID uint64, publicKey, allowedIP string) error {
|
|||||||
return fmt.Errorf("添加 Peer 失败:%w", err)
|
return fmt.Errorf("添加 Peer 失败:%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 通知 Core
|
// 2. 增强模式:meshray-core 的 conn.Bind 已接管该组网收发,peer Endpoint 保持真实地址,
|
||||||
// 如果已经在 Enhanced 模式,需要接管刚刚加入的这个 Peer
|
// 无需改写回环端口;候选地址后续由信令层经 NotifyPeerCandidates 注入触发建连。
|
||||||
engine, err := c.coreInst.GetEngine(networkIDStr)
|
if _, ok := c.engines[networkIDStr]; ok {
|
||||||
if err == nil && engine != nil {
|
c.logger.Info("增强模式:动态 Peer 已交由 meshray-core 接管",
|
||||||
// 引擎存在,说明是 enhanced 模式
|
zap.String("public_key", publicKey[:8]+"..."))
|
||||||
// 由 Core 分配一个本地端口
|
|
||||||
localPort, err := engine.Bind(publicKey, 0)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Error("增强模式动态 AddPeer Bind 失败", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
// 通知 WG 更新为本地代理 Endpoint
|
|
||||||
newEndpoint := fmt.Sprintf("127.0.0.1:%d", localPort)
|
|
||||||
if err := c.wgManager.UpdatePeerEndpoint(networkIDStr, publicKey, newEndpoint); err != nil {
|
|
||||||
c.logger.Warn("增强模式自动重写 Endpoint 失败", zap.Error(err))
|
|
||||||
engine.Unbind(publicKey)
|
|
||||||
} else {
|
|
||||||
c.logger.Info("增强模式:动态 Peer 添加并成功代理给 Core",
|
|
||||||
zap.String("public_key", publicKey[:8]+"..."),
|
|
||||||
zap.String("new_endpoint", newEndpoint))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
c.logger.Debug("Core 未启动,普通模式无需代理 Peer", zap.Uint64("network_id", networkID))
|
c.logger.Debug("原生模式无需代理 Peer", zap.Uint64("network_id", networkID))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -225,36 +201,38 @@ func (c *Ctr) RemovePeer(networkID uint64, publicKey string) error {
|
|||||||
return fmt.Errorf("移除 Peer 失败:%w", err)
|
return fmt.Errorf("移除 Peer 失败:%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知 Core 卸载这台 Peer 的监听
|
// 增强模式:meshray-core 的 conn.Bind 按 WG peer 状态自动管理收发,
|
||||||
engine, err := c.coreInst.GetEngine(networkIDStr)
|
// 移除 WG peer 后不再有该 peer 的包,无需显式解绑
|
||||||
if err == nil && engine != nil {
|
if _, ok := c.engines[networkIDStr]; ok {
|
||||||
engine.Unbind(publicKey)
|
c.logger.Info("增强模式:Core 已随 WG Peer 移除停止对该 Peer 的转发",
|
||||||
c.logger.Info("增强模式:Core 已解除对该 Peer 的挂载", zap.String("public_key", publicKey[:8]+"..."))
|
zap.String("public_key", publicKey[:8]+"..."))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSTUNTURNConfig 为指定网络设置 STUN/TURN 配置
|
// SetSTUNTURNConfig 为指定网络设置 STUN/TURN 配置(仅增强模式生效)
|
||||||
func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServers []connect.TURNServerConfig) error {
|
func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServers []string) error {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
networkIDStr := strconv.FormatUint(networkID, 10)
|
networkIDStr := strconv.FormatUint(networkID, 10)
|
||||||
|
|
||||||
// 获取 Engine 实例
|
// 获取增强引擎
|
||||||
engine, err := c.coreInst.GetEngine(networkIDStr)
|
engineCtl, ok := c.engines[networkIDStr]
|
||||||
if err != nil {
|
if !ok {
|
||||||
c.logger.Debug("网络未启动增强模式,跳过 STUN/TURN 配置",
|
c.logger.Debug("网络未启动增强模式,跳过 STUN/TURN 配置",
|
||||||
zap.Uint64("network_id", networkID))
|
zap.Uint64("network_id", networkID))
|
||||||
return nil // 无需错误,因为原生模式不需要
|
return nil // 无需错误,因为原生模式不需要
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 WebRTC 工厂的 ICE 配置
|
// 更新 WebRTC 工厂的 ICE 配置
|
||||||
engine.SetICEConfig(connect.ICEConfig{
|
if err := engineCtl.SetICEConfig(contract.ICEConfig{
|
||||||
STUNServers: stunServers,
|
STUNServers: stunServers,
|
||||||
TURNServers: turnServers,
|
TURNServers: turnServers,
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("设置 ICE 配置失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
c.logger.Info("STUN/TURN 配置已设置",
|
c.logger.Info("STUN/TURN 配置已设置",
|
||||||
zap.Uint64("network_id", networkID),
|
zap.Uint64("network_id", networkID),
|
||||||
@@ -263,6 +241,7 @@ func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServ
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
|
func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -279,11 +258,9 @@ func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
|
|||||||
}
|
}
|
||||||
status.WGStatus = wgStatus
|
status.WGStatus = wgStatus
|
||||||
|
|
||||||
// 获取 Core 状态(直接调用 Engine 的方法)
|
// 获取增强引擎状态
|
||||||
engine, err := c.coreInst.GetEngine(networkIDStr)
|
if engineCtl, ok := c.engines[networkIDStr]; ok {
|
||||||
if err == nil && engine != nil {
|
if coreStatus, err := engineCtl.GetStatus(); err == nil {
|
||||||
coreStatus, err := engine.GetStatus()
|
|
||||||
if err == nil {
|
|
||||||
status.CoreStatus = coreStatus
|
status.CoreStatus = coreStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +269,8 @@ func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SwitchMode 切换传输模式(原生→增强)
|
// SwitchMode 切换传输模式(原生→增强)
|
||||||
// 仅支持 "native" → "enhanced" 单向切换
|
// 仅支持 "native" → "enhanced" 单向切换。增强模式由 meshray-core 的 conn.Bind 接管收发,
|
||||||
|
// 不再改写 peer Endpoint 为回环端口(根治入站接不通的缺陷)。
|
||||||
func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
|
func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@@ -317,18 +295,17 @@ func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
|
|||||||
c.logger.Info("获取到 WG 状态",
|
c.logger.Info("获取到 WG 状态",
|
||||||
zap.Int("peer_count", wgStatus.PeerCount))
|
zap.Int("peer_count", wgStatus.PeerCount))
|
||||||
|
|
||||||
// 3. 创建并启动 Core Engine(增强模式核心)
|
// 3. 创建并启动增强引擎(控制面)
|
||||||
metrics := core.NewMetrics()
|
engineCtl, err := contract.NewEngineController(networkIDStr, c.logger)
|
||||||
engine, err := c.coreInst.CreateEngine(networkIDStr, metrics)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("创建 Core Engine 失败:%w", err)
|
return fmt.Errorf("创建增强引擎失败(需 meshray-core):%w", err)
|
||||||
}
|
}
|
||||||
|
if err := engineCtl.Start(); err != nil {
|
||||||
if err := engine.Start(); err != nil {
|
return fmt.Errorf("启动增强引擎失败:%w", err)
|
||||||
return fmt.Errorf("启动 Core Engine 失败:%w", err)
|
|
||||||
}
|
}
|
||||||
|
c.engines[networkIDStr] = engineCtl
|
||||||
|
|
||||||
c.logger.Info("Core Engine 已启动",
|
c.logger.Info("增强引擎已启动(收发由 meshray-core conn.Bind 接管)",
|
||||||
zap.Uint64("network_id", networkID))
|
zap.Uint64("network_id", networkID))
|
||||||
|
|
||||||
// 4. 获取 Peer 列表
|
// 4. 获取 Peer 列表
|
||||||
@@ -337,60 +314,87 @@ func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
|
|||||||
return fmt.Errorf("获取 Peer 列表失败:%w", err)
|
return fmt.Errorf("获取 Peer 列表失败:%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 修改 Peer Endpoint → 127.0.0.1:{port},并且让 Core 绑定
|
// 5. 把已知真实 Endpoint 作为候选注入,触发 P2P 建连
|
||||||
// 增强模式下,所有 Peer 的 Endpoint 都指向本地回环地址
|
// (不再改写 peer Endpoint 为回环端口;peer Endpoint 保持真实地址,WG 入站匹配成功)
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
// 1. Core 引擎动态接管,传 0 意为让操作系统挑个未占用的随意回环端口
|
if peer.Endpoint != "" {
|
||||||
localPort, err := engine.Bind(peer.PublicKey, 0)
|
candidates := []contract.Candidate{
|
||||||
if err != nil {
|
{Addr: peer.Endpoint, Type: "host", Priority: 1, Protocol: "udp"},
|
||||||
c.logger.Error("Engine Bind 失败",
|
}
|
||||||
zap.String("public_key", peer.PublicKey[:8]+"..."),
|
if err := engineCtl.NotifyPeerCandidates(peer.PublicKey, candidates); err != nil {
|
||||||
zap.Error(err))
|
c.logger.Warn("注入对端候选地址失败",
|
||||||
continue
|
zap.String("public_key", peer.PublicKey[:8]+"..."),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newEndpoint := fmt.Sprintf("127.0.0.1:%d", localPort)
|
|
||||||
|
|
||||||
// 2. 将此端口告知 WG 更新 Endpoint
|
|
||||||
if err := c.wgManager.UpdatePeerEndpoint(networkIDStr, peer.PublicKey, newEndpoint); err != nil {
|
|
||||||
c.logger.Warn("更新 Peer Endpoint 失败",
|
|
||||||
zap.String("public_key", peer.PublicKey[:8]+"..."),
|
|
||||||
zap.Error(err))
|
|
||||||
|
|
||||||
// 如果更新失败,解绑 Core
|
|
||||||
engine.Unbind(peer.PublicKey)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
c.logger.Info("Peer Endpoint 已成功切换并接入 Core",
|
|
||||||
zap.String("public_key", peer.PublicKey[:8]+"..."),
|
|
||||||
zap.String("original_endpoint", peer.Endpoint),
|
|
||||||
zap.String("new_endpoint", newEndpoint))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Info("传输模式切换成功(基础功能已实现,P3 阶段待完善)",
|
c.logger.Info("传输模式切换成功(增强模式:meshray-core 接管收发)",
|
||||||
zap.Uint64("network_id", networkID),
|
zap.Uint64("network_id", networkID),
|
||||||
zap.String("mode", mode))
|
zap.String("mode", mode))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CoreConfigUpdate 核心配置更新请求(由 server 层在策略变更时构造并传入)
|
||||||
|
type CoreConfigUpdate struct {
|
||||||
|
STUNServers []string // STUN 服务器列表
|
||||||
|
TURNServers []string // TURN 服务器列表(URL)
|
||||||
|
EnabledLayers []contract.Layer // 启用的传输层顺序(为空保持默认 9 层)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyPeerCandidates 由信令层调用:注入对端候选地址并触发 P2P 建连
|
||||||
|
func (c *Ctr) NotifyPeerCandidates(networkID uint64, publicKey string, candidates []contract.Candidate) error {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
networkIDStr := strconv.FormatUint(networkID, 10)
|
||||||
|
engineCtl, ok := c.engines[networkIDStr]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("网络未启用增强模式")
|
||||||
|
}
|
||||||
|
return engineCtl.NotifyPeerCandidates(publicKey, candidates)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateCoreConfig 更新 Core 配置(策略修改后同步)
|
// UpdateCoreConfig 更新 Core 配置(策略修改后同步)
|
||||||
// ✅ P3-1 阶段实现
|
// 真实生效:更新 ICE/STUN/TURN 配置到 WebRTC 工厂,并按需调整传输层优先级顺序
|
||||||
func (c *Ctr) UpdateCoreConfig(networkID uint64, config interface{}) error {
|
func (c *Ctr) UpdateCoreConfig(networkID uint64, config interface{}) error {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
c.logger.Info("更新 Core 配置",
|
update, ok := config.(*CoreConfigUpdate)
|
||||||
zap.Uint64("network_id", networkID))
|
if !ok {
|
||||||
|
return fmt.Errorf("不支持的配置类型:%T", config)
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ P3-1 阶段 - 暂不实现
|
networkIDStr := strconv.FormatUint(networkID, 10)
|
||||||
// 未来实现:
|
engineCtl, ok := c.engines[networkIDStr]
|
||||||
// 1. 根据策略类型解析配置
|
if !ok {
|
||||||
// 2. 调用 Core 的 UpdateConfig 方法
|
return fmt.Errorf("网络未启用增强模式")
|
||||||
// 3. 持久化到数据库
|
}
|
||||||
|
|
||||||
return fmt.Errorf("UpdateCoreConfig 尚未实现(P3-1 阶段)")
|
// 1. 更新 ICE/STUN/TURN 配置(下发到 WebRTC 工厂)
|
||||||
|
if err := engineCtl.SetICEConfig(contract.ICEConfig{
|
||||||
|
STUNServers: update.STUNServers,
|
||||||
|
TURNServers: update.TURNServers,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("设置 ICE 配置失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新传输层优先级顺序(策略核心配置)
|
||||||
|
if len(update.EnabledLayers) > 0 {
|
||||||
|
if err := engineCtl.SetLayerOrder(update.EnabledLayers); err != nil {
|
||||||
|
return fmt.Errorf("设置传输层顺序失败:%w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Info("Core 配置已更新",
|
||||||
|
zap.Uint64("network_id", networkID),
|
||||||
|
zap.Int("stun_count", len(update.STUNServers)),
|
||||||
|
zap.Int("turn_count", len(update.TURNServers)),
|
||||||
|
zap.Int("enabled_layers", len(update.EnabledLayers)))
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWGMode 获取系统 WG 模式
|
// GetWGMode 获取系统 WG 模式
|
||||||
@@ -400,7 +404,7 @@ func (c *Ctr) GetWGMode() string {
|
|||||||
|
|
||||||
// NetworkStatus 网络状态
|
// NetworkStatus 网络状态
|
||||||
type NetworkStatus struct {
|
type NetworkStatus struct {
|
||||||
NetworkID string `json:"network_id"`
|
NetworkID string `json:"network_id"`
|
||||||
WGStatus *WGStatus `json:"wg_status"`
|
WGStatus *WGStatus `json:"wg_status"`
|
||||||
CoreStatus *core.EngineStatus `json:"core_status,omitempty"`
|
CoreStatus *contract.EngineStatus `json:"core_status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-12
@@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
contract "git.zkcoi.com/zkcoi/meshray-contract"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.zx2c4.com/wireguard/conn"
|
|
||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/tun"
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
@@ -48,7 +48,8 @@ func NewWGManager(logger *zap.Logger) *WGManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateDevice 创建 WireGuard 设备
|
// CreateDevice 创建 WireGuard 设备
|
||||||
func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int) error {
|
// meshMode: "native" 使用 wireguard-go 默认 Bind 直连;"enhanced" 使用 meshray-core 注册的自定义 Bind 接管收发
|
||||||
|
func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int, meshMode string) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
@@ -63,7 +64,8 @@ func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int
|
|||||||
zap.String("network_id", networkID),
|
zap.String("network_id", networkID),
|
||||||
zap.String("device", deviceName),
|
zap.String("device", deviceName),
|
||||||
zap.String("subnet", subnet),
|
zap.String("subnet", subnet),
|
||||||
zap.Int("listen_port", listenPort))
|
zap.Int("listen_port", listenPort),
|
||||||
|
zap.String("mesh_mode", meshMode))
|
||||||
|
|
||||||
// 生成密钥对
|
// 生成密钥对
|
||||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||||
@@ -76,7 +78,7 @@ func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int
|
|||||||
zap.String("public_key", publicKey.String()))
|
zap.String("public_key", publicKey.String()))
|
||||||
|
|
||||||
// 使用用户态模式启动 wireguard-go 进程,并获取资源引用
|
// 使用用户态模式启动 wireguard-go 进程,并获取资源引用
|
||||||
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort)
|
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort, networkID, meshMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -356,19 +358,43 @@ type WGStatus struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListPeers 获取设备下所有 Peer 信息
|
// ListPeers 获取设备下所有 Peer 信息
|
||||||
|
// 除内存记录外,还会从 wgctrl 读取对端真实 Endpoint,作为增强模式 P2P 建连的候选地址来源
|
||||||
func (m *WGManager) ListPeers(networkID string) ([]PeerInfo, error) {
|
func (m *WGManager) ListPeers(networkID string) ([]PeerInfo, error) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
device, ok := m.devices[networkID]
|
device, ok := m.devices[networkID]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
m.mu.RUnlock()
|
||||||
return nil, fmt.Errorf("网络 %s 的设备不存在", networkID)
|
return nil, fmt.Errorf("网络 %s 的设备不存在", networkID)
|
||||||
}
|
}
|
||||||
|
// 复制内存中的 peer 列表,避免返回内部共享引用
|
||||||
|
peers := make([]PeerInfo, len(device.Peers))
|
||||||
|
copy(peers, device.Peers)
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
return device.Peers, nil
|
// 从 wgctrl 读取真实 Endpoint(若已配置)
|
||||||
|
client, err := wgctrl.New()
|
||||||
|
if err == nil {
|
||||||
|
defer client.Close()
|
||||||
|
if dev, err := client.Device(device.Name); err == nil {
|
||||||
|
for i := range peers {
|
||||||
|
for _, wp := range dev.Peers {
|
||||||
|
if wp.PublicKey.String() == peers[i].PublicKey {
|
||||||
|
if wp.Endpoint != nil {
|
||||||
|
peers[i].Endpoint = wp.Endpoint.String()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePeerEndpoint 更新 Peer 的 Endpoint
|
// UpdatePeerEndpoint 更新 Peer 的 Endpoint
|
||||||
|
// 注意:增强模式(meshray-core 接管收发)下不应再改写 peer Endpoint 为回环端口,
|
||||||
|
// 否则 WG 入站源地址匹配失败;此方法仅用于设置对端真实地址或原生模式。
|
||||||
func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEndpoint string) error {
|
func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEndpoint string) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
@@ -434,7 +460,7 @@ func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEn
|
|||||||
|
|
||||||
// startUserModeWGProcess 用户态模式启动 wireguard-go(保存资源引用)
|
// startUserModeWGProcess 用户态模式启动 wireguard-go(保存资源引用)
|
||||||
func (m *WGManager) startUserModeWGProcess(deviceName string, privateKey wgtypes.Key, listenPort int) error {
|
func (m *WGManager) startUserModeWGProcess(deviceName string, privateKey wgtypes.Key, listenPort int) error {
|
||||||
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort)
|
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort, "", "native")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -445,10 +471,12 @@ func (m *WGManager) startUserModeWGProcess(deviceName string, privateKey wgtypes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startUserModeWGProcessWithRefs 用户态模式启动 wireguard-go,返回资源引用
|
// startUserModeWGProcessWithRefs 用户态模式启动 wireguard-go,返回资源引用
|
||||||
func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey wgtypes.Key, listenPort int) (tun.Device, *device.Device, error) {
|
// meshMode 决定 Bind 来源:native 用 wireguard-go 默认 Bind;enhanced 用 meshray-core 注册的自定义 Bind
|
||||||
|
func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey wgtypes.Key, listenPort int, networkID string, meshMode string) (tun.Device, *device.Device, error) {
|
||||||
m.logger.Info("使用用户态模式启动 WireGuard",
|
m.logger.Info("使用用户态模式启动 WireGuard",
|
||||||
zap.String("device", deviceName),
|
zap.String("device", deviceName),
|
||||||
zap.Int("port", listenPort))
|
zap.Int("port", listenPort),
|
||||||
|
zap.String("mesh_mode", meshMode))
|
||||||
|
|
||||||
// 1. 创建 TUN 设备并保存引用
|
// 1. 创建 TUN 设备并保存引用
|
||||||
tunDevice, err := tun.CreateTUN(deviceName, 1420)
|
tunDevice, err := tun.CreateTUN(deviceName, 1420)
|
||||||
@@ -456,8 +484,20 @@ func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey
|
|||||||
return nil, nil, fmt.Errorf("创建 TUN 设备失败:%w", err)
|
return nil, nil, fmt.Errorf("创建 TUN 设备失败:%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 创建 UDP bind
|
// 2. 创建 UDP bind:根据组网模式选择
|
||||||
bind := conn.NewDefaultBind()
|
// - 原生模式:wireguard-go 默认 Bind(标准直连,无需 meshray-core)
|
||||||
|
// - 增强模式:优先使用 meshray-core 注册的自定义 Bind(接管收发,根治 Endpoint 旁路缺陷);
|
||||||
|
// 若默认构建未引入 meshray-core,则返回错误,增强模式不可用。
|
||||||
|
bindFactory, err := contract.GetBindFactory(meshMode == "enhanced")
|
||||||
|
if err != nil {
|
||||||
|
tunDevice.Close()
|
||||||
|
return nil, nil, fmt.Errorf("获取 Bind 工厂失败(增强模式需 meshray-core):%w", err)
|
||||||
|
}
|
||||||
|
bind, err := bindFactory(listenPort, networkID, m.logger)
|
||||||
|
if err != nil {
|
||||||
|
tunDevice.Close()
|
||||||
|
return nil, nil, fmt.Errorf("创建 Bind 失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 创建 WireGuard device 并保存引用
|
// 3. 创建 WireGuard device 并保存引用
|
||||||
logger := &device.Logger{
|
logger := &device.Logger{
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewDeviceService(store *sqlite.Store, ctrClient ctr.Client) *DeviceService
|
|||||||
// GetDevice 获取设备详情
|
// GetDevice 获取设备详情
|
||||||
func (s *DeviceService) GetDevice(id uint64) (*model.Device, error) {
|
func (s *DeviceService) GetDevice(id uint64) (*model.Device, error) {
|
||||||
var device model.Device
|
var device model.Device
|
||||||
err := s.store.DB().Preload("Network").First(&device, id).Error
|
err := s.store.DB().First(&device, id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.New("设备不存在")
|
return nil, errors.New("设备不存在")
|
||||||
@@ -45,7 +45,7 @@ func (s *DeviceService) GetDevice(id uint64) (*model.Device, error) {
|
|||||||
// ListAllDevices 获取所有设备列表
|
// ListAllDevices 获取所有设备列表
|
||||||
func (s *DeviceService) ListAllDevices() ([]model.Device, error) {
|
func (s *DeviceService) ListAllDevices() ([]model.Device, error) {
|
||||||
var devices []model.Device
|
var devices []model.Device
|
||||||
err := s.store.DB().Preload("Network").Find(&devices).Error
|
err := s.store.DB().Find(&devices).Error
|
||||||
return devices, err
|
return devices, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// Package contract 定义 Meshray 开源管理器与闭源 meshray-core 增强层之间的契约。
|
||||||
|
//
|
||||||
|
// 本包为开源叶子模块:不依赖管理器(meshray)也不依赖增强层(meshray-core),
|
||||||
|
// 二者均 import 本包,从而避免 module 级循环依赖。
|
||||||
|
//
|
||||||
|
// 设计目标(来自架构决策:多仓解耦 + 编译期注入):
|
||||||
|
// - 管理器默认构建不 import meshray-core,因此可独立编译、以标准 WireGuard 直连运行;
|
||||||
|
// - 增强构建(go build -tags meshray_core)空白导入 meshray-core,触发其 init() 把
|
||||||
|
// 自定义 conn.Bind 工厂与 EngineProvider 注册到本包的全局注册表;
|
||||||
|
// - 无 meshray-core 时,GetBindFactory(false) 返回 wireguard-go 默认 Bind(原生直连),
|
||||||
|
// NewEngineController 返回 ErrCoreNotAvailable(增强模式优雅降级)。
|
||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.zx2c4.com/wireguard/conn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ===== 候选地址与传输层 =====
|
||||||
|
|
||||||
|
// Candidate 对端候选地址(由信令层经 meshray-core 转发下发)
|
||||||
|
type Candidate struct {
|
||||||
|
Addr string `json:"addr"` // 候选地址(ip:port)
|
||||||
|
Type string `json:"type"` // 候选类型:host/srflx/relay
|
||||||
|
Priority int `json:"priority"` // 优先级
|
||||||
|
Protocol string `json:"protocol"` // 协议:udp/tcp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer 传输层类型(9 层策略),数值与 meshray-core 内部 connect.Layer 对齐
|
||||||
|
type Layer int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LayerDirectUDP Layer = iota
|
||||||
|
LayerFakeTCP
|
||||||
|
LayerRealTCP
|
||||||
|
LayerTURNUDP
|
||||||
|
LayerTURNQUIC
|
||||||
|
LayerTURNTCP
|
||||||
|
LayerTURNTLS
|
||||||
|
LayerWebRTC
|
||||||
|
LayerWS
|
||||||
|
LayerCount
|
||||||
|
)
|
||||||
|
|
||||||
|
// String 实现 Stringer
|
||||||
|
func (l Layer) String() string {
|
||||||
|
switch l {
|
||||||
|
case LayerDirectUDP:
|
||||||
|
return "Direct-UDP"
|
||||||
|
case LayerFakeTCP:
|
||||||
|
return "Direct-FakeTCP"
|
||||||
|
case LayerRealTCP:
|
||||||
|
return "Direct-RealTCP"
|
||||||
|
case LayerTURNUDP:
|
||||||
|
return "TURN-UDP"
|
||||||
|
case LayerTURNQUIC:
|
||||||
|
return "TURN-QUIC"
|
||||||
|
case LayerTURNTCP:
|
||||||
|
return "TURN-TCP"
|
||||||
|
case LayerTURNTLS:
|
||||||
|
return "TURN-TLS"
|
||||||
|
case LayerWebRTC:
|
||||||
|
return "WebRTC"
|
||||||
|
case LayerWS:
|
||||||
|
return "WS/WSS"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultLayerOrder 默认优先级顺序(从最优到兜底)
|
||||||
|
var DefaultLayerOrder = []Layer{
|
||||||
|
LayerDirectUDP,
|
||||||
|
LayerFakeTCP,
|
||||||
|
LayerRealTCP,
|
||||||
|
LayerTURNUDP,
|
||||||
|
LayerTURNQUIC,
|
||||||
|
LayerTURNTCP,
|
||||||
|
LayerTURNTLS,
|
||||||
|
LayerWebRTC,
|
||||||
|
LayerWS,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICEConfig ICE 配置(STUN/TURN 服务器),用于 WebRTC 等传输层
|
||||||
|
type ICEConfig struct {
|
||||||
|
STUNServers []string
|
||||||
|
TURNServers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 状态 =====
|
||||||
|
|
||||||
|
// PeerStatus 单 Peer 状态
|
||||||
|
type PeerStatus struct {
|
||||||
|
PeerKey string `json:"peer_key"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
Layer string `json:"layer,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngineStatus 增强引擎状态
|
||||||
|
type EngineStatus struct {
|
||||||
|
PeerCount int `json:"peer_count"`
|
||||||
|
Peers map[string]*PeerStatus `json:"peers"`
|
||||||
|
ActiveConnections int64 `json:"active_connections"`
|
||||||
|
TotalConnections int64 `json:"total_connections"`
|
||||||
|
BytesSent uint64 `json:"bytes_sent"`
|
||||||
|
BytesReceived uint64 `json:"bytes_received"`
|
||||||
|
StrategyFallbacks int64 `json:"strategy_fallbacks"`
|
||||||
|
LastSwitchTime int64 `json:"last_switch_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 增强引擎控制面契约 =====
|
||||||
|
//
|
||||||
|
// EngineController 由 meshray-core 实现,供管理器(开源)在 enhanced 模式调用。
|
||||||
|
type EngineController interface {
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
// NotifyPeerCandidates 由信令层调用:注入对端候选地址并触发 P2P 建连
|
||||||
|
NotifyPeerCandidates(peerKey string, candidates []Candidate) error
|
||||||
|
// SetICEConfig 更新 ICE/STUN/TURN 配置
|
||||||
|
SetICEConfig(cfg ICEConfig) error
|
||||||
|
// SetLayerOrder 设置传输层优先级顺序(策略核心配置)
|
||||||
|
SetLayerOrder(order []Layer) error
|
||||||
|
// GetStatus 查询引擎状态
|
||||||
|
GetStatus() (*EngineStatus, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngineProvider 创建 EngineController 的工厂(由 meshray-core 注册)
|
||||||
|
type EngineProvider func(networkID string, logger *zap.Logger) (EngineController, error)
|
||||||
|
|
||||||
|
var (
|
||||||
|
engineProvider EngineProvider
|
||||||
|
errCoreNotAvail = errors.New("meshray-core 未启用:增强模式不可用(仅原生直连可用)")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterEngineProvider 由 meshray-core 在 init 中注册
|
||||||
|
func RegisterEngineProvider(p EngineProvider) {
|
||||||
|
engineProvider = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngineProviderRegistered 是否已注册增强引擎提供方
|
||||||
|
func EngineProviderRegistered() bool {
|
||||||
|
return engineProvider != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngineController 创建增强引擎;未注册 core 时返回 errCoreNotAvail
|
||||||
|
func NewEngineController(networkID string, logger *zap.Logger) (EngineController, error) {
|
||||||
|
if engineProvider == nil {
|
||||||
|
return nil, errCoreNotAvail
|
||||||
|
}
|
||||||
|
return engineProvider(networkID, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== WireGuard conn.Bind 工厂契约 =====
|
||||||
|
//
|
||||||
|
// BindFactory 创建接管 WG 收发的 conn.Bind。原生模式用 wireguard-go 默认 Bind;
|
||||||
|
// 增强模式由 meshray-core 注册自定义 Bind(在 Send/Recv 中插入 9 层传输与打洞)。
|
||||||
|
type BindFactory func(listenPort int, networkID string, logger *zap.Logger) (conn.Bind, error)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nativeBindFactory = func(listenPort int, networkID string, logger *zap.Logger) (conn.Bind, error) {
|
||||||
|
return conn.NewDefaultBind(), nil
|
||||||
|
}
|
||||||
|
enhancedBindFactory BindFactory
|
||||||
|
errBindNotEnhanced = errors.New("meshray-core 未启用:enhanced 模式无法创建自定义 Bind")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterEnhancedBindFactory 由 meshray-core 在 init 中注册
|
||||||
|
func RegisterEnhancedBindFactory(f BindFactory) {
|
||||||
|
enhancedBindFactory = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBindFactory 获取 Bind 工厂:
|
||||||
|
// - enhanced=true 且已注册 core → 返回 core 的自定义 Bind 工厂;
|
||||||
|
// - enhanced=true 但未注册 core → 返回 errBindNotEnhanced;
|
||||||
|
// - enhanced=false → 返回 wireguard-go 默认 Bind(原生直连)。
|
||||||
|
func GetBindFactory(enhanced bool) (BindFactory, error) {
|
||||||
|
if enhanced {
|
||||||
|
if enhancedBindFactory == nil {
|
||||||
|
return nil, errBindNotEnhanced
|
||||||
|
}
|
||||||
|
return enhancedBindFactory, nil
|
||||||
|
}
|
||||||
|
return nativeBindFactory, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
module git.zkcoi.com/zkcoi/meshray-contract
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/net v0.39.0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -8,7 +8,7 @@ echo.
|
|||||||
REM 检查后端是否已编译
|
REM 检查后端是否已编译
|
||||||
if not exist "meshray.exe" (
|
if not exist "meshray.exe" (
|
||||||
echo [1/3] 正在编译后端...
|
echo [1/3] 正在编译后端...
|
||||||
go build -o meshray.exe
|
go build -o meshray.exe ./cmd/meshray
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ❌ 后端编译失败!
|
echo ❌ 后端编译失败!
|
||||||
pause
|
pause
|
||||||
|
|||||||
+183
-8
@@ -261,16 +261,191 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 其他页面占位 -->
|
<!-- 设备管理页面 -->
|
||||||
<div v-if="['devices', 'services', 'settings'].includes(currentPage)">
|
<div v-if="currentPage === 'devices'">
|
||||||
<h2 class="text-2xl font-bold mb-6">
|
<h2 class="text-2xl font-bold mb-6">设备管理</h2>
|
||||||
{{ currentPage === 'devices' ? '设备管理' :
|
<div class="card p-6">
|
||||||
currentPage === 'services' ? '服务管理' : '系统设置' }}
|
<el-button type="primary" @click="openCreateDevice">
|
||||||
</h2>
|
<el-icon><Plus /></el-icon>
|
||||||
<div class="card p-6 text-center text-gray-500">
|
创建设备
|
||||||
<p>页面开发中...</p>
|
</el-button>
|
||||||
|
<el-table :data="devices" style="width: 100%; margin-top: 20px;">
|
||||||
|
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||||
|
<el-table-column prop="name" label="名称"></el-table-column>
|
||||||
|
<el-table-column prop="network_id" label="网络ID" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="virtual_ip" label="虚拟IP"></el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 'online' ? 'success' : 'info'">
|
||||||
|
{{ row.status === 'online' ? '在线' : '离线' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="downloadDeviceConfig(row)">配置</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteDevice(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div v-if="devices.length === 0" class="text-center text-gray-400 py-8">
|
||||||
|
暂无设备,点击「创建设备」开始
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务管理页面 -->
|
||||||
|
<div v-if="currentPage === 'services'">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">服务管理</h2>
|
||||||
|
<div class="card p-6">
|
||||||
|
<el-button type="primary" @click="openCreateService">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
创建服务
|
||||||
|
</el-button>
|
||||||
|
<el-table :data="services" style="width: 100%; margin-top: 20px;">
|
||||||
|
<el-table-column prop="id" label="ID" width="200"></el-table-column>
|
||||||
|
<el-table-column prop="name" label="名称"></el-table-column>
|
||||||
|
<el-table-column prop="type" label="类型" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="address" label="地址"></el-table-column>
|
||||||
|
<el-table-column prop="port" label="端口" width="80"></el-table-column>
|
||||||
|
<el-table-column label="操作" width="260">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="editService(row)">编辑</el-button>
|
||||||
|
<el-button size="small" @click="testService(row)">测试</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteService(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div v-if="services.length === 0" class="text-center text-gray-400 py-8">
|
||||||
|
暂无服务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统设置页面 -->
|
||||||
|
<div v-if="currentPage === 'settings'">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">系统设置</h2>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="card p-6">
|
||||||
|
<h3 class="text-lg font-bold mb-4">基础设置</h3>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="监听端口">
|
||||||
|
<el-input-number v-model="settings.server_port" :min="1" :max="65535"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日志级别">
|
||||||
|
<el-select v-model="settings.log_level" style="width: 100%">
|
||||||
|
<el-option label="Debug" value="debug"></el-option>
|
||||||
|
<el-option label="Info" value="info"></el-option>
|
||||||
|
<el-option label="Warn" value="warn"></el-option>
|
||||||
|
<el-option label="Error" value="error"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="主题">
|
||||||
|
<el-select v-model="settings.theme" style="width: 100%">
|
||||||
|
<el-option label="浅色" value="light"></el-option>
|
||||||
|
<el-option label="深色" value="dark"></el-option>
|
||||||
|
<el-option label="跟随系统" value="auto"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言">
|
||||||
|
<el-select v-model="settings.language" style="width: 100%">
|
||||||
|
<el-option label="简体中文" value="zh-CN"></el-option>
|
||||||
|
<el-option label="English" value="en-US"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="saveSettings">保存设置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="card p-6">
|
||||||
|
<h3 class="text-lg font-bold mb-4">安全</h3>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="旧密码">
|
||||||
|
<el-input v-model="passwordForm.old_password" type="password" show-password></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码">
|
||||||
|
<el-input v-model="passwordForm.new_password" type="password" show-password></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认新密码">
|
||||||
|
<el-input v-model="passwordForm.confirm" type="password" show-password></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="changePassword">修改密码</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div class="card p-6 mt-6">
|
||||||
|
<h3 class="text-lg font-bold mb-4">运行状态</h3>
|
||||||
|
<p class="mb-2">WireGuard 模式:<el-tag>{{ wgModeDisplay || wgMode || '未知' }}</el-tag></p>
|
||||||
|
<el-button type="warning" @click="restartCore">重启核心服务</el-button>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 创建设备对话框 -->
|
||||||
|
<el-dialog v-model="showCreateDeviceDialog" title="创建设备" width="500px">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="所属组网">
|
||||||
|
<el-select v-model="deviceForm.network_id" placeholder="请选择组网" style="width: 100%">
|
||||||
|
<el-option v-for="n in networks" :key="n.id" :label="n.name" :value="n.id"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备名称">
|
||||||
|
<el-input v-model="deviceForm.name" placeholder="请输入设备名称"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="虚拟IP">
|
||||||
|
<el-input v-model="deviceForm.virtual_ip" placeholder="留空则自动分配"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreateDeviceDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="createDevice">创建</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 设备配置对话框 -->
|
||||||
|
<el-dialog v-model="showDeviceConfigDialog" title="设备 WireGuard 配置" width="600px">
|
||||||
|
<el-input type="textarea" :rows="12" v-model="deviceConfigText" readonly></el-input>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="() => { navigator.clipboard && navigator.clipboard.writeText(deviceConfigText); ElementPlus.ElMessage.success('已复制到剪贴板') }">复制</el-button>
|
||||||
|
<el-button @click="showDeviceConfigDialog = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 服务对话框 -->
|
||||||
|
<el-dialog v-model="showServiceDialog" :title="serviceDialogTitle" width="500px">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="服务名称">
|
||||||
|
<el-input v-model="serviceForm.name" placeholder="请输入服务名称"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-select v-model="serviceForm.type" style="width: 100%">
|
||||||
|
<el-option v-for="t in serviceTypes" :key="t.value" :label="t.label" :value="t.value"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址">
|
||||||
|
<el-input v-model="serviceForm.address" placeholder="例如 stun.example.com"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="端口">
|
||||||
|
<el-input-number v-model="serviceForm.port" :min="0" :max="65535"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="serviceForm.auth_username" placeholder="可选"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="serviceForm.auth_password" type="password" show-password placeholder="可选,留空不修改"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showServiceDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveService">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
|||||||
+434
-117
@@ -29,10 +29,41 @@ const app = createApp({
|
|||||||
// 检查是否已登录
|
// 检查是否已登录
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
isLoggedIn.value = true;
|
isLoggedIn.value = true;
|
||||||
// 可以在这里验证 token 是否有效
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据
|
// API 基础 URL
|
||||||
|
const API_BASE = '/api/v1';
|
||||||
|
|
||||||
|
// 统一请求封装:自动注入 Bearer Token,并兼容 {data}|{error}|纯文本 三种响应
|
||||||
|
const apiRequest = async (path, options = {}) => {
|
||||||
|
const headers = {};
|
||||||
|
if (options.body) headers['Content-Type'] = 'application/json';
|
||||||
|
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
|
||||||
|
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await fetch(API_BASE + path, { ...options, headers });
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: '网络请求失败:' + e.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ct = resp.headers.get('content-type') || '';
|
||||||
|
let result = {};
|
||||||
|
try {
|
||||||
|
result = ct.includes('application/json') ? await resp.json() : { text: await resp.text() };
|
||||||
|
} catch (e) { /* 忽略解析错误 */ }
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: resp.ok,
|
||||||
|
status: resp.status,
|
||||||
|
data: result.data,
|
||||||
|
error: result.error || result.message,
|
||||||
|
text: result.text,
|
||||||
|
raw: result
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===================== 统计数据 / 组网 =====================
|
||||||
const stats = reactive({
|
const stats = reactive({
|
||||||
networkCount: 0,
|
networkCount: 0,
|
||||||
deviceCount: 0,
|
deviceCount: 0,
|
||||||
@@ -40,13 +71,8 @@ const app = createApp({
|
|||||||
pendingApprovals: 0
|
pendingApprovals: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组网列表
|
|
||||||
const networks = ref([]);
|
const networks = ref([]);
|
||||||
|
|
||||||
// 对话框控制
|
|
||||||
const showCreateNetworkDialog = ref(false);
|
const showCreateNetworkDialog = ref(false);
|
||||||
|
|
||||||
// 新组网表单
|
|
||||||
const newNetwork = reactive({
|
const newNetwork = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
subnet_ipv4: '10.0.0.0/24',
|
subnet_ipv4: '10.0.0.0/24',
|
||||||
@@ -54,67 +80,51 @@ const app = createApp({
|
|||||||
mesh_mode: 'enhanced'
|
mesh_mode: 'enhanced'
|
||||||
});
|
});
|
||||||
|
|
||||||
// API 基础 URL
|
|
||||||
const API_BASE = '/api/v1';
|
|
||||||
|
|
||||||
// 加载统计数据
|
|
||||||
const loadStats = async () => {
|
const loadStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/dashboard/stats`);
|
const r = await apiRequest('/dashboard/stats');
|
||||||
if (response.ok) {
|
if (r.ok && r.data) {
|
||||||
const result = await response.json();
|
stats.networkCount = r.data.total_networks || 0;
|
||||||
if (result.data) {
|
stats.deviceCount = r.data.total_devices || 0;
|
||||||
stats.networkCount = result.data.network_count || 0;
|
stats.onlineDevices = r.data.online_devices || 0;
|
||||||
stats.deviceCount = result.data.device_count || 0;
|
stats.pendingApprovals = r.data.pending_approvals || 0;
|
||||||
stats.onlineDevices = result.data.online_devices || 0;
|
} else if (!r.ok && r.status !== 401) {
|
||||||
stats.pendingApprovals = result.data.pending_approvals || 0;
|
console.error('加载统计数据失败:', r.error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载统计数据失败:', error);
|
console.error('加载统计数据失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载组网列表
|
|
||||||
const loadNetworks = async () => {
|
const loadNetworks = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/networks`);
|
const r = await apiRequest('/networks');
|
||||||
if (response.ok) {
|
if (r.ok) {
|
||||||
const result = await response.json();
|
networks.value = r.data || [];
|
||||||
networks.value = result.data || [];
|
} else if (r.status !== 401) {
|
||||||
|
console.error('加载组网列表失败:', r.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载组网列表失败:', error);
|
console.error('加载组网列表失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建组网
|
|
||||||
const createNetwork = async () => {
|
const createNetwork = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/networks`, {
|
const r = await apiRequest('/networks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(newNetwork)
|
body: JSON.stringify(newNetwork)
|
||||||
});
|
});
|
||||||
|
if (r.ok) {
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
ElementPlus.ElMessage.success('组网创建成功');
|
ElementPlus.ElMessage.success('组网创建成功');
|
||||||
showCreateNetworkDialog.value = false;
|
showCreateNetworkDialog.value = false;
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
newNetwork.name = '';
|
newNetwork.name = '';
|
||||||
newNetwork.subnet_ipv4 = '10.0.0.0/24';
|
newNetwork.subnet_ipv4 = '10.0.0.0/24';
|
||||||
newNetwork.listen_port = 51820;
|
newNetwork.listen_port = 51820;
|
||||||
|
|
||||||
// 刷新列表
|
|
||||||
await loadNetworks();
|
await loadNetworks();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
} else {
|
} else {
|
||||||
const result = await response.json();
|
ElementPlus.ElMessage.error(r.error || '创建失败');
|
||||||
ElementPlus.ElMessage.error(result.error || '创建失败');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建组网失败:', error);
|
console.error('创建组网失败:', error);
|
||||||
@@ -122,20 +132,16 @@ const app = createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 查看组网详情
|
|
||||||
const viewNetwork = (network) => {
|
const viewNetwork = (network) => {
|
||||||
ElementPlus.ElMessageBox.alert(
|
ElementPlus.ElMessageBox.alert(
|
||||||
`组网名称:${network.name}\n` +
|
`组网名称:${network.name}\n` +
|
||||||
`子网:${network.subnet_ipv4}\n` +
|
`子网:${network.subnet_ipv4}\n` +
|
||||||
`模式:${network.mesh_mode === 'enhanced' ? '增强' : '原生'}`,
|
`模式:${network.mesh_mode === 'enhanced' ? '增强' : '原生'}`,
|
||||||
'组网详情',
|
'组网详情',
|
||||||
{
|
{ confirmButtonText: '确定' }
|
||||||
confirmButtonText: '确定'
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成 MeshSeed
|
|
||||||
const generateMeshSeed = (network) => {
|
const generateMeshSeed = (network) => {
|
||||||
ElementPlus.ElMessageBox.prompt(
|
ElementPlus.ElMessageBox.prompt(
|
||||||
'请输入 MeshSeed 有效期(小时)',
|
'请输入 MeshSeed 有效期(小时)',
|
||||||
@@ -150,23 +156,12 @@ const app = createApp({
|
|||||||
try {
|
try {
|
||||||
const hours = parseInt(value);
|
const hours = parseInt(value);
|
||||||
const expiresAt = new Date(Date.now() + hours * 3600 * 1000).toISOString();
|
const expiresAt = new Date(Date.now() + hours * 3600 * 1000).toISOString();
|
||||||
|
const r = await apiRequest(`/networks/${network.id}/mesh-seed`, {
|
||||||
const response = await fetch(`${API_BASE}/networks/${network.id}/mesh-seed`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
body: JSON.stringify({ max_uses: 10, expires_at: expiresAt, ddns_enabled: false })
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
max_uses: 10,
|
|
||||||
expires_at: expiresAt,
|
|
||||||
ddns_enabled: false
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
if (r.ok) {
|
||||||
if (response.ok) {
|
const seedUrl = (r.data && (r.data.seed_url || r.data.seed)) || '';
|
||||||
const result = await response.json();
|
|
||||||
const seedUrl = result.data?.seed_url || result.data?.seed;
|
|
||||||
|
|
||||||
if (seedUrl) {
|
if (seedUrl) {
|
||||||
ElementPlus.ElMessageBox.alert(
|
ElementPlus.ElMessageBox.alert(
|
||||||
`MeshSeed 生成成功:<br><br>` +
|
`MeshSeed 生成成功:<br><br>` +
|
||||||
@@ -174,15 +169,13 @@ const app = createApp({
|
|||||||
`style="width: 100%; padding: 8px; margin-top: 10px; border: 1px solid #ddd;" ` +
|
`style="width: 100%; padding: 8px; margin-top: 10px; border: 1px solid #ddd;" ` +
|
||||||
`onclick="this.select()">`,
|
`onclick="this.select()">`,
|
||||||
'MeshSeed',
|
'MeshSeed',
|
||||||
{
|
{ dangerouslyUseHTMLString: true, confirmButtonText: '复制并关闭' }
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
confirmButtonText: '复制并关闭'
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.success('MeshSeed 生成成功');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await response.json();
|
ElementPlus.ElMessage.error(r.error || '生成失败');
|
||||||
ElementPlus.ElMessage.error(result.error || '生成失败');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('生成 MeshSeed 失败:', error);
|
console.error('生成 MeshSeed 失败:', error);
|
||||||
@@ -191,30 +184,20 @@ const app = createApp({
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除组网
|
|
||||||
const deleteNetwork = async (network) => {
|
const deleteNetwork = async (network) => {
|
||||||
try {
|
try {
|
||||||
await ElementPlus.ElMessageBox.confirm(
|
await ElementPlus.ElMessageBox.confirm(
|
||||||
`确定要删除组网 "${network.name}" 吗?此操作不可恢复。`,
|
`确定要删除组网 "${network.name}" 吗?此操作不可恢复。`,
|
||||||
'警告',
|
'警告',
|
||||||
{
|
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||||
confirmButtonText: '删除',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
const r = await apiRequest(`/networks/${network.id}`, { method: 'DELETE' });
|
||||||
const response = await fetch(`${API_BASE}/networks/${network.id}`, {
|
if (r.ok) {
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
ElementPlus.ElMessage.success('删除成功');
|
ElementPlus.ElMessage.success('删除成功');
|
||||||
await loadNetworks();
|
await loadNetworks();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
} else {
|
} else {
|
||||||
const result = await response.json();
|
ElementPlus.ElMessage.error(r.error || '删除失败');
|
||||||
ElementPlus.ElMessage.error(result.error || '删除失败');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
@@ -224,49 +207,365 @@ const app = createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
// ===================== 设备管理 =====================
|
||||||
onMounted(() => {
|
const devices = ref([]);
|
||||||
if (isLoggedIn.value) {
|
const showCreateDeviceDialog = ref(false);
|
||||||
loadStats();
|
const deviceForm = reactive({ network_id: null, name: '', virtual_ip: '' });
|
||||||
loadNetworks();
|
const showDeviceConfigDialog = ref(false);
|
||||||
|
const deviceConfigText = ref('');
|
||||||
|
|
||||||
|
const loadDevices = async () => {
|
||||||
|
try {
|
||||||
|
const r = await apiRequest('/devices');
|
||||||
|
if (r.ok) {
|
||||||
|
devices.value = r.data || [];
|
||||||
|
} else if (r.status !== 401) {
|
||||||
|
console.error('加载设备列表失败:', r.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载设备列表失败:', error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateDevice = () => {
|
||||||
|
deviceForm.network_id = networks.value.length ? networks.value[0].id : null;
|
||||||
|
deviceForm.name = '';
|
||||||
|
deviceForm.virtual_ip = '';
|
||||||
|
showCreateDeviceDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDevice = async () => {
|
||||||
|
if (!deviceForm.network_id) {
|
||||||
|
ElementPlus.ElMessage.warning('请先选择所属组网');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await apiRequest(`/devices?network_id=${deviceForm.network_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
network_id: deviceForm.network_id,
|
||||||
|
name: deviceForm.name,
|
||||||
|
virtual_ip: deviceForm.virtual_ip
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
const cfg = (r.data && r.data.wireguard_config) || (r.data && r.data.config_text) || '';
|
||||||
|
if (cfg) {
|
||||||
|
deviceConfigText.value = cfg;
|
||||||
|
showDeviceConfigDialog.value = true;
|
||||||
|
}
|
||||||
|
showCreateDeviceDialog.value = false;
|
||||||
|
ElementPlus.ElMessage.success('设备创建成功');
|
||||||
|
await loadDevices();
|
||||||
|
await loadStats();
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '创建失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建设备失败:', error);
|
||||||
|
ElementPlus.ElMessage.error('创建失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadDeviceConfig = async (device) => {
|
||||||
|
try {
|
||||||
|
const r = await apiRequest(`/devices/${device.id}/config`);
|
||||||
|
if (!r.ok) {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '获取配置失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = r.text || (r.raw && r.raw.text) || '';
|
||||||
|
const blob = new Blob([text], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${device.name || 'device'}-wg.conf`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载设备配置失败:', error);
|
||||||
|
ElementPlus.ElMessage.error('下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDevice = async (device) => {
|
||||||
|
try {
|
||||||
|
await ElementPlus.ElMessageBox.confirm(
|
||||||
|
`确定要删除设备 "${device.name}" 吗?`,
|
||||||
|
'警告',
|
||||||
|
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||||
|
);
|
||||||
|
const r = await apiRequest(`/devices/${device.id}`, { method: 'DELETE' });
|
||||||
|
if (r.ok) {
|
||||||
|
ElementPlus.ElMessage.success('删除成功');
|
||||||
|
await loadDevices();
|
||||||
|
await loadStats();
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除设备失败:', error);
|
||||||
|
ElementPlus.ElMessage.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===================== 服务管理 =====================
|
||||||
|
const services = ref([]);
|
||||||
|
const showServiceDialog = ref(false);
|
||||||
|
const serviceDialogTitle = ref('创建服务');
|
||||||
|
const serviceForm = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: 'turn',
|
||||||
|
address: '',
|
||||||
|
port: null,
|
||||||
|
auth_username: '',
|
||||||
|
auth_password: ''
|
||||||
|
});
|
||||||
|
const serviceTypes = [
|
||||||
|
{ value: 'tun', label: 'TUN 隧道' },
|
||||||
|
{ value: 'turn', label: 'TURN 服务' },
|
||||||
|
{ value: 'ddns', label: 'DDNS 服务' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const loadServices = async () => {
|
||||||
|
try {
|
||||||
|
const r = await apiRequest('/services');
|
||||||
|
if (r.ok) {
|
||||||
|
services.value = r.data || [];
|
||||||
|
} else if (r.status !== 401) {
|
||||||
|
console.error('加载服务列表失败:', r.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务列表失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateService = () => {
|
||||||
|
serviceDialogTitle.value = '创建服务';
|
||||||
|
serviceForm.id = '';
|
||||||
|
serviceForm.name = '';
|
||||||
|
serviceForm.type = 'turn';
|
||||||
|
serviceForm.address = '';
|
||||||
|
serviceForm.port = null;
|
||||||
|
serviceForm.auth_username = '';
|
||||||
|
serviceForm.auth_password = '';
|
||||||
|
showServiceDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editService = (svc) => {
|
||||||
|
serviceDialogTitle.value = '编辑服务';
|
||||||
|
serviceForm.id = svc.id;
|
||||||
|
serviceForm.name = svc.name;
|
||||||
|
serviceForm.type = svc.type;
|
||||||
|
serviceForm.address = svc.address;
|
||||||
|
serviceForm.port = svc.port;
|
||||||
|
serviceForm.auth_username = svc.username || '';
|
||||||
|
serviceForm.auth_password = '';
|
||||||
|
showServiceDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveService = async () => {
|
||||||
|
const payload = {
|
||||||
|
name: serviceForm.name,
|
||||||
|
type: serviceForm.type,
|
||||||
|
address: serviceForm.address,
|
||||||
|
port: serviceForm.port
|
||||||
|
};
|
||||||
|
if (serviceForm.auth_username) payload.username = serviceForm.auth_username;
|
||||||
|
if (serviceForm.auth_password) payload.password = serviceForm.auth_password;
|
||||||
|
|
||||||
|
const isEdit = !!serviceForm.id;
|
||||||
|
const r = isEdit
|
||||||
|
? await apiRequest(`/services/${serviceForm.id}`, { method: 'PUT', body: JSON.stringify(payload) })
|
||||||
|
: await apiRequest('/services', { method: 'POST', body: JSON.stringify(payload) });
|
||||||
|
|
||||||
|
if (r.ok) {
|
||||||
|
ElementPlus.ElMessage.success(isEdit ? '更新成功' : '创建成功');
|
||||||
|
showServiceDialog.value = false;
|
||||||
|
await loadServices();
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteService = async (svc) => {
|
||||||
|
try {
|
||||||
|
await ElementPlus.ElMessageBox.confirm(
|
||||||
|
`确定要删除服务 "${svc.name}" 吗?`,
|
||||||
|
'警告',
|
||||||
|
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||||
|
);
|
||||||
|
const r = await apiRequest(`/services/${svc.id}`, { method: 'DELETE' });
|
||||||
|
if (r.ok) {
|
||||||
|
ElementPlus.ElMessage.success('删除成功');
|
||||||
|
await loadServices();
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除服务失败:', error);
|
||||||
|
ElementPlus.ElMessage.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testService = async (svc) => {
|
||||||
|
const r = await apiRequest(`/services/${svc.id}/test`, { method: 'POST' });
|
||||||
|
if (r.ok) {
|
||||||
|
const status = (r.data && r.data.status) || 'unknown';
|
||||||
|
ElementPlus.ElMessage.info(`连通性状态:${status}`);
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '测试失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===================== 系统设置 =====================
|
||||||
|
const settings = reactive({
|
||||||
|
server_port: 51820,
|
||||||
|
log_level: 'info',
|
||||||
|
theme: 'light',
|
||||||
|
language: 'zh-CN',
|
||||||
|
max_backups: 7,
|
||||||
|
max_age: 30
|
||||||
|
});
|
||||||
|
const wgMode = ref('');
|
||||||
|
const wgModeDisplay = ref('');
|
||||||
|
const passwordForm = reactive({ old_password: '', new_password: '', confirm: '' });
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const r = await apiRequest('/settings');
|
||||||
|
if (r.ok && r.data) {
|
||||||
|
Object.assign(settings, r.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载系统设置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadWGMode = async () => {
|
||||||
|
try {
|
||||||
|
const r = await apiRequest('/system-config/wg-mode');
|
||||||
|
if (r.ok && r.data) {
|
||||||
|
wgMode.value = r.data.actual_mode || r.data.wg_mode || '';
|
||||||
|
wgModeDisplay.value = r.data.wg_mode_display || wgMode.value;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载 WG 模式失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
const r = await apiRequest('/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
server_port: settings.server_port,
|
||||||
|
log_level: settings.log_level,
|
||||||
|
theme: settings.theme,
|
||||||
|
language: settings.language,
|
||||||
|
max_backups: settings.max_backups,
|
||||||
|
max_age: settings.max_age
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
ElementPlus.ElMessage.success('设置已保存');
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePassword = async () => {
|
||||||
|
if (!passwordForm.old_password || !passwordForm.new_password) {
|
||||||
|
ElementPlus.ElMessage.warning('请输入旧密码和新密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passwordForm.new_password !== passwordForm.confirm) {
|
||||||
|
ElementPlus.ElMessage.warning('两次输入的新密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const r = await apiRequest('/system/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
old_password: passwordForm.old_password,
|
||||||
|
new_password: passwordForm.new_password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
ElementPlus.ElMessage.success('密码修改成功');
|
||||||
|
passwordForm.old_password = '';
|
||||||
|
passwordForm.new_password = '';
|
||||||
|
passwordForm.confirm = '';
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '修改失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restartCore = async () => {
|
||||||
|
try {
|
||||||
|
await ElementPlus.ElMessageBox.confirm(
|
||||||
|
'确定要重启核心服务吗?重启过程约需数秒。',
|
||||||
|
'重启核心',
|
||||||
|
{ confirmButtonText: '重启', cancelButtonText: '取消', type: 'warning' }
|
||||||
|
);
|
||||||
|
const r = await apiRequest('/system/restart-core', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ graceful: true })
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
ElementPlus.ElMessage.success('核心服务正在重启...');
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error(r.error || '重启失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('重启核心失败:', error);
|
||||||
|
ElementPlus.ElMessage.error('重启失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===================== 登录 / 登出 =====================
|
||||||
|
const loadAll = async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadStats(),
|
||||||
|
loadNetworks(),
|
||||||
|
loadDevices(),
|
||||||
|
loadServices(),
|
||||||
|
loadSettings(),
|
||||||
|
loadWGMode()
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isLoggedIn.value) loadAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理登录
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!loginForm.username || !loginForm.password) {
|
if (!loginForm.username || !loginForm.password) {
|
||||||
ElementPlus.ElMessage.warning('请输入用户名和密码');
|
ElementPlus.ElMessage.warning('请输入用户名和密码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loggingIn.value = true;
|
loggingIn.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
const r = await apiRequest('/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: loginForm.username,
|
username: loginForm.username,
|
||||||
password: loginForm.password
|
password: loginForm.password
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
if (r.ok && r.data) {
|
||||||
const result = await response.json();
|
authToken = r.data.token || r.data.access_token;
|
||||||
|
|
||||||
if (response.ok && result.data) {
|
|
||||||
// 保存 token
|
|
||||||
authToken = result.data.token || result.data.access_token;
|
|
||||||
localStorage.setItem('meshray_token', authToken);
|
localStorage.setItem('meshray_token', authToken);
|
||||||
isLoggedIn.value = true;
|
isLoggedIn.value = true;
|
||||||
|
|
||||||
ElementPlus.ElMessage.success('登录成功');
|
ElementPlus.ElMessage.success('登录成功');
|
||||||
|
await loadAll();
|
||||||
// 加载数据
|
|
||||||
await loadStats();
|
|
||||||
await loadNetworks();
|
|
||||||
} else {
|
} else {
|
||||||
ElementPlus.ElMessage.error(result.error || '登录失败');
|
ElementPlus.ElMessage.error(r.error || '登录失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录错误:', error);
|
console.error('登录错误:', error);
|
||||||
@@ -276,26 +575,15 @@ const app = createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理退出
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
ElementPlus.ElMessageBox.confirm(
|
ElementPlus.ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||||
'确定要退出登录吗?',
|
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||||
'提示',
|
}).then(() => {
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
// 清除 token
|
|
||||||
localStorage.removeItem('meshray_token');
|
localStorage.removeItem('meshray_token');
|
||||||
authToken = '';
|
authToken = '';
|
||||||
isLoggedIn.value = false;
|
isLoggedIn.value = false;
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
loginForm.username = '';
|
loginForm.username = '';
|
||||||
loginForm.password = '';
|
loginForm.password = '';
|
||||||
|
|
||||||
ElementPlus.ElMessage.success('已退出登录');
|
ElementPlus.ElMessage.success('已退出登录');
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -314,7 +602,36 @@ const app = createApp({
|
|||||||
createNetwork,
|
createNetwork,
|
||||||
viewNetwork,
|
viewNetwork,
|
||||||
generateMeshSeed,
|
generateMeshSeed,
|
||||||
deleteNetwork
|
deleteNetwork,
|
||||||
|
// devices
|
||||||
|
devices,
|
||||||
|
showCreateDeviceDialog,
|
||||||
|
deviceForm,
|
||||||
|
showDeviceConfigDialog,
|
||||||
|
deviceConfigText,
|
||||||
|
openCreateDevice,
|
||||||
|
createDevice,
|
||||||
|
downloadDeviceConfig,
|
||||||
|
deleteDevice,
|
||||||
|
// services
|
||||||
|
services,
|
||||||
|
showServiceDialog,
|
||||||
|
serviceDialogTitle,
|
||||||
|
serviceForm,
|
||||||
|
serviceTypes,
|
||||||
|
openCreateService,
|
||||||
|
editService,
|
||||||
|
saveService,
|
||||||
|
deleteService,
|
||||||
|
testService,
|
||||||
|
// settings
|
||||||
|
settings,
|
||||||
|
wgMode,
|
||||||
|
wgModeDisplay,
|
||||||
|
passwordForm,
|
||||||
|
saveSettings,
|
||||||
|
changePassword,
|
||||||
|
restartCore
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user