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:
2026-07-15 16:08:13 +08:00
parent 15dab96872
commit 59e3059246
39 changed files with 2350 additions and 4361 deletions
+375 -127
View File
@@ -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 抽象层,支持多云服务商
- Cloudflare Provider(真实 API 集成)
- 腾讯云 DNSPod Provider(真实 API 集成)
- 阿里云 Provider(占位实现)
- ✅ IP 自动检测服务(公网/本地 IPv4/IPv6
- ✅ 后台任务调度器(每 5 分钟自动检测 IP 变化)
- ✅ Dashboard DDNS 监控卡片
- ✅ 前端 IP 自动检测按钮
- ✅ 防抖动设计 + 事务处理
故按用户决策:
- 项目命名统一为 **meshray**wg-go 集成属于 manager / UI / GUI 范畴,不污染 core。
- 把原 `core/` 目录拆为**两个隔离仓库**
- `Meshray`(模块 `git.zkcoi.com/zkcoi/meshray/core`):零 wireguard、零 contract 依赖的纯转发底座。
- `Meshray-Manager`(模块 `git.zkcoi.com/zkcoi/meshray`):wg-go / contract / UI / 管控面。
#### P1 管理功能
- ✅ 修改密码功能(bcrypt 加密)
- ✅ 重启核心服务功能
### 改动(核心仓库 `Meshray`
- 模块路径由 `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
- ✅ 列出备份 API
- ✅ 恢复备份 API
- ✅ 删除备份 API
- ✅ 下载备份 API
### 验证
- `Meshray``go build ./...` → 0`go test ./transport/... ./connect/...` → ok(零 wg / contract 依赖)。
- `Meshray-Manager``go build ./...` → 0`go build ./cmd/mr-wg/... ./internal/ctr/...` → 0。
- 已知:manager `go mod tidy` 在 Go 1.26 下因「兄弟模块 replace + 本地 replace」触发内部 panic
已用 `go mod download` + 直接路径 `go build ./path/...` 绕过,未执行 tidy。
**WebSocket 实时通知推送系统**:
- ✅ Notification 数据模型(SQLite 持久化)
- ✅ 6 个完整的 RESTful API
- ✅ 单播/广播双模式
- ✅ 前端通知中心组件(铃铛图标 + 红色角标)
- ✅ 下拉通知列表(滚动条 + 空状态)
- ✅ 一键全部已读
- ✅ 删除单条通知
- ✅ 自动刷新未读数(每 30 秒)
- ✅ 布局集成到顶部栏
## 2026-07-15 重构:Direct-UDP 改为共享监听 socket + WG 学址,原生支持 NAT 穿透(核心)
#### 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 只接收所连对端的包:
- NASNAT 后)拨 VPS:51820 成功,但其源地址经 NAT 改写为 `WAN:随机端口`
- VPS 的 connected socket 连的是 `NAS内网:51820`,收到的 `WAN:随机端口` 源地址不匹配 → 丢弃。
#### 后端架构
- ✅ 完善 Service 层数据库访问封装(GetDB 方法)
- ✅ 统一 Handler 层构造函数设计
- ✅ 优化中间件注册流程
- ✅ 改进错误处理和日志记录
即公网侧「收得到、回不去」。这不是 NAT 的固有限制,而是 connected socket 语义所致。
#### 前端架构
- ✅ 创建独立的 notifications API 模块
- ✅ 开发可复用的 NotificationCenter 组件
- ✅ 集成到 MainLayout 布局
- ✅ 实现响应式通知列表 UI
### 修复(回归标准 WireGuard Bind 语义)
Direct-UDP 改由 `EnhancedBind` 的**共享未连接 UDP socket**`directSock`,绑定 listen_port)承载:
- `core/bind.go`
- 新增 `directSock *net.UDPConn``listenPort``inboundPacket` 增加 `ep conn.Endpoint`
- `Open``net.ListenUDP` 在 listen_port 建共享 socketlisten_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
- ✅ 创建 Linux/Mac启动脚本(start.sh
- ✅ 完善 .gitignore 配置
- ✅ 优化前端编译配置
### 效果
- 双公网对称直连:`A:port↔B:port` 原生可通。
- NAT 后主动侧 ↔ 公网侧:公网侧收包学到对端真实地址,回包自然返回,**无需对端预先公网可达、
无需路由器端口映射**。core 成为地址权威,集成侧可从 core 取对端地址喂给 wg-go。
- TURN/WebRTC/WS 等中继/信令层不受影响,仍经 connMgr 承载(Direct 不可用时补充)。
- TCP 直连族(FakeTCP/RealTCP)为 TCP,与 UDP `directSock` 不冲突。
### 📚 文档更新
### 验证
- `go build ./... && go vet ./... && go test ./...` → 全部通过(3 个测试包 ok)。
#### 新增文档
- ✅ README.md - 项目主文档
- ✅ QUICKSTART.md - 快速入门指南
- ✅ README_开发完成总览.md - 开发完成总览
- ✅ 功能验证与测试报告.md - 测试验证文档
- ✅ 交付清单.md - 最终交付清单
## 2026-07-15 修复:直连传输 socket 未绑定本地端口,两台机器互传失败(核心 P0)
#### 实现报告
- ✅ 完整功能开发总结报告.md
- ✅ WebSocket 实时通知推送功能实现报告.md
- ✅ P3_系统更新检查功能实现报告.md
- ✅ 完整功能开发 - 最终完成报告.md
### 背景(用户排查「两台机器传不了数据」)
此前已修复「初始直连缺入站读取」,但端到端仍不通。进一步排查数据路径(Bind→Relay→
DirectFactory)发现更底层缺陷:增强模式下 WG 经 `EnhancedBind` 接管,**并不在 listen_port
上监听真实 UDP**`Open` 仅从 `inboundCh` 取包,无 `ListenUDP`)。
### 📊 统计数据
### 根因
`DirectFactory.Dial``dialer.DialContext(ctx,"udp",candidate)`**本地端口为随机临时端口**
- A 拨 B:51820 → A 的 socket 本地 `A:临时`、远端 `B:51820`
- B 拨 A:51820 → B 的 socket 本地 `B:临时`、远端 `A:51820`
- **新增文件**: 22 个
- **代码行数**: ~6,100 行
- **API 接口**: 20 个(100% 实现)
- **文档**: 8 份
A 发往 `B:51820` 的包到达 B 的 OS 时,B 在 51820 无任何监听 socketB 的传输 socket 本地是
临时端口),**被 OS 丢弃**;B→A 同理。即两端都出得去、进不来——这正是「frp 能传、meshray-core
不行」的本质:frp 是中心中继(两端都出向连 server),而本仓增强模式要求对称直连却没把
本地端口绑到 listen_port。
### 🔒 安全性
### 修复
让 Direct-UDP 传输 socket **把本地端口绑到 listen_portWG 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 身份验证
- ✅ CORS 跨域控制
- ✅ SQL 参数化查询(防注入)
- ✅ 权限隔离
- ✅ 操作日志记录(AuditLog)
### 验证
- 临时单测模拟两机(回环两端口):A:15180↔B:15181 对称直连,双向 Write/Read 均成功
(修复前临时端口下对端收不到)。已删除该临时测试。
- `go build ./... && go vet ./...` → 通过。
### ⚠️ 已知问题
###
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 中间件**
- 原因:已有轮询机制(每 30 秒),非必需
- 计划:可选优化,实现实时推送
### 注意(TOML 语义坑)
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 组网核心功能
- ✅ 用户管理系统
- ✅ 设备管理
- ✅ 策略管理
- ✅ MeshSeed 凭证生成
- ✅ 待审核加入机制
- ✅ Dashboard 基础监控
- ✅ 实时监控面板
- ✅ 日志查看
- ✅ 系统设置
### 需求
除默认的 9 层自动降级外,需支持手动指定传输层级,便于在「已知网络环境」下锁定链路
(如强制走 TURN-TCP 中继、或强制 Direct-UDP 直连),也便于排障时固定单链路复现。
---
### 配置入口(独立 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(计划中
- [ ] 阿里云 DNS Provider 实现
- [ ] 真实的备份/恢复逻辑
- [ ] WebSocket 实时推送中间件
- [ ] 告警规则管理
- [ ] 资源监控图表优化
### ICE 配置补齐(手动 TURN/WebRTC 必需
- 独立二进制此前从未调用 `Engine.SetICEConfig`,导致 TURN/WebRTC 工厂拿不到 STUN/TURN 地址。
- `main.go` 现已把配置里的 `ice.stun` / `ice.turn` 下达到引擎,手动 TURN-TCP/TURN-TLS/
WebRTC 层级方可真正建连。
### v2.2.0(规划中
- [ ] 多语言国际化
- [ ] 主题切换功能
- [ ] 移动端适配优化
- [ ] 性能监控和告警
- [ ] CI/CD 集成
### 管控面(ctr
- `internal/ctr` 已通过 `update.EnabledLayers``SetLayerOrder` 支持自定义顺序,
等价「手动多层级」,本次无需改动;后续若需「auto/manual」语义可在此扩展。
---
### 构建验证
- `core` 独立:`go build ./... && go vet ./...` → 通过。
## 📝 说明
## 2026-07-14 核心路基缺陷修复 (P0 + P1)
- 版本号格式:主版本号。次版本号。修订号
- 优先级说明:
- P0: 核心功能
- P1: 重要功能
- P2: 次要功能
- P3: 增强功能
### 背景
此前验证集中在 HTTP/前端接口与页面,属表层验证。核心组网路基(WireGuard 增强模式
P2P 劫持转发链路)实际断裂。本次以单元测试钉死缺陷后,按 P0→P1 全修。
---
### 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
**维护人员**: MeshRay Team
### P0-2 建连永不触发
- 文件: `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** 匹配 peerEndpoint 仅用于
握手更新对端出向地址与 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` 后分配隧道 IPLinux `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
View File
@@ -14,7 +14,7 @@ func main() {
log.Fatal(err)
}
fmt.Println("=== MeshRay 数据库表结构检查 ===\n")
fmt.Println("=== MeshRay 数据库表结构检查 ===")
// 检查所有表是否存在
tables := []string{
+106
View File
@@ -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
}
+265
View File
@@ -0,0 +1,265 @@
// 命令 meshray-coremeshray-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. 创建 Bindenhanced=自定义接管;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 并 upbest-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
}
+17
View File
@@ -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": [] }
}
+54
View File
@@ -0,0 +1,54 @@
# meshray-core 独立运行配置(类 frp 风格,TOML)
# 用法: meshray-core -c meshray-core.toml
# 也兼容旧 JSON 配置(扩展名 .json 自动按 JSON 解析)。
# 网络标识,同网段节点需一致
network_id = "net1"
# 本地监听端口(WireGuard / 增强模式收发端口)
listen_port = 51820
# 隧道 MTU0 则默认 1420
mtu = 1420
# 私钥:留空则自动生成并写入 private_key_file
private_key = ""
private_key_file = "key.txt"
# 本机隧道 IPCIDR
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
View File
@@ -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
→ 创建 WGPluginplugins/wg/wgparse.go
→ 创建 Relay,传入 plugintransport/relay.go
→ 创建 Strategyconnect/strategy.go
→ 创建 ConnManagertransport/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
-93
View File
@@ -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)
}
-219
View File
@@ -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
}
-585
View File
@@ -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
}
-181
View File
@@ -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
}
-754
View File
@@ -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-FakeTCPUDP 封装 TCP 头部,欺骗防火墙)
LayerFakeTCP
// LayerRealTCP Direct-RealTCPP2P 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 DataChannelDTLS 加密)
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
}
-119
View File
@@ -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 服务器均查询失败")
}
-358
View File
@@ -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)
}
-253
View File
@@ -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 支持时再完善
}
-216
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
-83
View File
@@ -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)
}
-47
View File
@@ -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
}
-99
View File
@@ -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()
}
-17
View File
@@ -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)
}
-197
View File
@@ -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))
}
}
+10 -2
View File
@@ -2,10 +2,18 @@ module git.zkcoi.com/zkcoi/meshray
go 1.26.0
// core 使
replace git.zkcoi.com/zkcoi/meshray/core => ./core
// meshray-core module
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 (
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/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
+2
View File
@@ -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/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
+13
View File
@@ -61,6 +61,17 @@ func NewServer(cfg *config.Config, logger *zap.Logger, dbStore *sqlite.Store) (*
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()
@@ -142,7 +153,9 @@ func (s *Server) registerRoutes() error {
}
// 尝试直接提供文件
// staticFS 已通过 fs.Sub 剥离 "static" 前缀,需先去掉路径中的 "static/"
filePath := strings.TrimPrefix(path, "/")
filePath = strings.TrimPrefix(filePath, "static/")
if filePath == "" {
filePath = "index.html"
}
+8
View File
@@ -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"
+67
View File
@@ -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,
}
}
+309
View File
@@ -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 交给 WGWG 用密文内的 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) -> peerKeySend 反查中继连接)
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()
}
+80
View File
@@ -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
View File
@@ -5,8 +5,7 @@ import (
"strconv"
"sync"
"git.zkcoi.com/zkcoi/meshray/core"
"git.zkcoi.com/zkcoi/meshray/core/connect"
contract "git.zkcoi.com/zkcoi/meshray-contract"
"go.uber.org/zap"
)
@@ -20,8 +19,8 @@ type Ctr struct {
// WireGuard 管理器
wgManager *WGManager
// Core 实例(直接集成,无需 gRPC)
coreInst *core.Core
// 增强引擎实例(由 meshray-core 提供,按 networkID 索引);原生模式为空
engines map[string]contract.EngineController
mu sync.RWMutex
}
@@ -38,14 +37,12 @@ func NewCtr(name string, networkID uint64, config *CtrConfig, logger *zap.Logger
networkID: networkID,
config: config,
logger: logger,
engines: make(map[string]contract.EngineController),
}
// 初始化 WireGuard 管理器
// 初始化 WireGuard 管理器(两种模式都需要)
ctr.wgManager = NewWGManager(logger)
// 初始化 Core 实例(直接集成,无需 gRPC)
ctr.coreInst = core.NewCore(logger)
return ctr, nil
}
@@ -58,9 +55,6 @@ func (c *Ctr) Start() error {
zap.String("name", c.name),
zap.Uint64("network_id", c.networkID))
// ✅ Watchdog 监控暂不实现(P3 阶段)
// go c.watchdog.Start()
return nil
}
@@ -72,10 +66,13 @@ func (c *Ctr) Stop() error {
c.logger.Info("停止 meshray-ctr",
zap.Uint64("network_id", c.networkID))
// 停止 Core 实例(直接调用方法)
if c.coreInst != nil {
// ✅ Close 方法已实现(会停止所有 Engine)
c.coreInst.Close()
// 停止所有增强引擎
for networkID, engineCtl := range c.engines {
if err := engineCtl.Stop(); err != nil {
c.logger.Warn("停止增强引擎失败",
zap.String("network_id", networkID), zap.Error(err))
}
delete(c.engines, networkID)
}
// 停止 WireGuard 管理
@@ -87,8 +84,8 @@ func (c *Ctr) Stop() error {
}
// CreateNetwork 创建网络(根据组网模式决定行为)
// - 原生模式:仅创建 WG 设备
// - 增强模式:创建 WG 设备 + 启动 Core 实例
// - 原生模式:仅创建 WG 设备wireguard-go 默认 Bind 直连)
// - 增强模式:WG 设备使用 meshray-core 的自定义 conn.Bind 接管收发;并启动增强引擎控制面
func (c *Ctr) CreateNetwork(networkID uint64, subnet string, listenPort int, meshMode string) error {
c.mu.Lock()
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.String("mesh_mode", meshMode))
// 1. 创建 WireGuard 设备(两种模式都需要)
// 1. 创建 WireGuard 设备(两种模式都需要;增强模式使用 core 注册的自定义 Bind
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)
}
c.logger.Debug("WireGuard 设备创建成功",
zap.Uint64("network_id", networkID))
// 2. 仅增强模式需要创建并启动 Core 实例
// 2. 仅增强模式需要启动增强引擎(控制面)
if meshMode == "enhanced" {
// 直接调用 Core 实例的方法(无需 gRPC)
metrics := core.NewMetrics()
engine, err := c.coreInst.CreateEngine(networkIDStr, metrics)
engineCtl, err := contract.NewEngineController(networkIDStr, c.logger)
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.logger.Error("启动 Engine 失败",
zap.Uint64("network_id", networkID),
zap.Error(err))
return fmt.Errorf("启动 Engine 失败:%w", err)
return fmt.Errorf("增强模式需 meshray-core%w", err)
}
c.logger.Info("增强模式:Engine 实例已启动",
if err := engineCtl.Start(); err != nil {
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))
} else {
c.logger.Info("原生模式:仅创建 WG 设备,不启动 Core",
c.logger.Info("原生模式:仅创建 WG 设备,标准直连",
zap.Uint64("network_id", networkID))
}
@@ -152,11 +138,17 @@ func (c *Ctr) DeleteNetwork(networkID uint64) error {
c.logger.Info("开始删除网络",
zap.Uint64("network_id", networkID))
// 1. Core 实例不需要显式删除(它是内存中的对象)
// 如果需要清理,可以在未来实现 Engine.Stop() 方法
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 {
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)
}
// 2. 通知 Core
// 如果已经在 Enhanced 模式,需要接管刚刚加入的这个 Peer
engine, err := c.coreInst.GetEngine(networkIDStr)
if err == nil && engine != nil {
// 引擎存在,说明是 enhanced 模式
// 由 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))
}
}
// 2. 增强模式:meshray-core 的 conn.Bind 已接管该组网收发,peer Endpoint 保持真实地址,
// 无需改写回环端口;候选地址后续由信令层经 NotifyPeerCandidates 注入触发建连。
if _, ok := c.engines[networkIDStr]; ok {
c.logger.Info("增强模式:动态 Peer 已交由 meshray-core 接管",
zap.String("public_key", publicKey[:8]+"..."))
} else {
c.logger.Debug("Core 未启动,普通模式无需代理 Peer", zap.Uint64("network_id", networkID))
c.logger.Debug("原生模式无需代理 Peer", zap.Uint64("network_id", networkID))
}
return nil
@@ -225,36 +201,38 @@ func (c *Ctr) RemovePeer(networkID uint64, publicKey string) error {
return fmt.Errorf("移除 Peer 失败:%w", err)
}
// 通知 Core 卸载这台 Peer 的监听
engine, err := c.coreInst.GetEngine(networkIDStr)
if err == nil && engine != nil {
engine.Unbind(publicKey)
c.logger.Info("增强模式:Core 已解除对该 Peer 的挂载", zap.String("public_key", publicKey[:8]+"..."))
// 增强模式:meshray-core 的 conn.Bind 按 WG peer 状态自动管理收发,
// 移除 WG peer 后不再有该 peer 的包,无需显式解绑
if _, ok := c.engines[networkIDStr]; ok {
c.logger.Info("增强模式:Core 已随 WG Peer 移除停止对该 Peer 的转发",
zap.String("public_key", publicKey[:8]+"..."))
}
return nil
}
// SetSTUNTURNConfig 为指定网络设置 STUN/TURN 配置
func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServers []connect.TURNServerConfig) error {
// SetSTUNTURNConfig 为指定网络设置 STUN/TURN 配置(仅增强模式生效)
func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServers []string) error {
c.mu.RLock()
defer c.mu.RUnlock()
networkIDStr := strconv.FormatUint(networkID, 10)
// 获取 Engine 实例
engine, err := c.coreInst.GetEngine(networkIDStr)
if err != nil {
// 获取增强引擎
engineCtl, ok := c.engines[networkIDStr]
if !ok {
c.logger.Debug("网络未启动增强模式,跳过 STUN/TURN 配置",
zap.Uint64("network_id", networkID))
return nil // 无需错误,因为原生模式不需要
}
// 更新 WebRTC 工厂的 ICE 配置
engine.SetICEConfig(connect.ICEConfig{
if err := engineCtl.SetICEConfig(contract.ICEConfig{
STUNServers: stunServers,
TURNServers: turnServers,
})
}); err != nil {
return fmt.Errorf("设置 ICE 配置失败:%w", err)
}
c.logger.Info("STUN/TURN 配置已设置",
zap.Uint64("network_id", networkID),
@@ -263,6 +241,7 @@ func (c *Ctr) SetSTUNTURNConfig(networkID uint64, stunServers []string, turnServ
return nil
}
func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
c.mu.RLock()
defer c.mu.RUnlock()
@@ -279,11 +258,9 @@ func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
}
status.WGStatus = wgStatus
// 获取 Core 状态(直接调用 Engine 的方法)
engine, err := c.coreInst.GetEngine(networkIDStr)
if err == nil && engine != nil {
coreStatus, err := engine.GetStatus()
if err == nil {
// 获取增强引擎状态
if engineCtl, ok := c.engines[networkIDStr]; ok {
if coreStatus, err := engineCtl.GetStatus(); err == nil {
status.CoreStatus = coreStatus
}
}
@@ -292,7 +269,8 @@ func (c *Ctr) GetStatus(networkID uint64) (*NetworkStatus, error) {
}
// SwitchMode 切换传输模式(原生→增强)
// 仅支持 "native" → "enhanced" 单向切换
// 仅支持 "native" → "enhanced" 单向切换。增强模式由 meshray-core 的 conn.Bind 接管收发,
// 不再改写 peer Endpoint 为回环端口(根治入站接不通的缺陷)。
func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
c.mu.Lock()
defer c.mu.Unlock()
@@ -317,18 +295,17 @@ func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
c.logger.Info("获取到 WG 状态",
zap.Int("peer_count", wgStatus.PeerCount))
// 3. 创建并启动 Core Engine(增强模式核心
metrics := core.NewMetrics()
engine, err := c.coreInst.CreateEngine(networkIDStr, metrics)
// 3. 创建并启动增强引擎(控制面
engineCtl, err := contract.NewEngineController(networkIDStr, c.logger)
if err != nil {
return fmt.Errorf("创建 Core Engine 失败%w", err)
return fmt.Errorf("创建增强引擎失败(需 meshray-core%w", err)
}
if err := engine.Start(); err != nil {
return fmt.Errorf("启动 Core Engine 失败:%w", err)
if err := engineCtl.Start(); err != nil {
return fmt.Errorf("启动增强引擎失败:%w", err)
}
c.engines[networkIDStr] = engineCtl
c.logger.Info("Core Engine 已启动",
c.logger.Info("增强引擎已启动(收发由 meshray-core conn.Bind 接管)",
zap.Uint64("network_id", networkID))
// 4. 获取 Peer 列表
@@ -337,60 +314,87 @@ func (c *Ctr) SwitchMode(networkID uint64, mode string) error {
return fmt.Errorf("获取 Peer 列表失败:%w", err)
}
// 5. 修改 Peer Endpoint → 127.0.0.1:{port},并且让 Core 绑定
// 增强模式下,所有 Peer Endpoint 都指向本地回环地址
// 5. 把已知真实 Endpoint 作为候选注入,触发 P2P 建连
// (不再改写 peer Endpoint 为回环端口;peer Endpoint 保持真实地址,WG 入站匹配成功)
for _, peer := range peers {
// 1. Core 引擎动态接管,传 0 意为让操作系统挑个未占用的随意回环端口
localPort, err := engine.Bind(peer.PublicKey, 0)
if err != nil {
c.logger.Error("Engine Bind 失败",
zap.String("public_key", peer.PublicKey[:8]+"..."),
zap.Error(err))
continue
if peer.Endpoint != "" {
candidates := []contract.Candidate{
{Addr: peer.Endpoint, Type: "host", Priority: 1, Protocol: "udp"},
}
if err := engineCtl.NotifyPeerCandidates(peer.PublicKey, candidates); err != nil {
c.logger.Warn("注入对端候选地址失败",
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.String("mode", mode))
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 配置(策略修改后同步)
// ✅ P3-1 阶段实现
// 真实生效:更新 ICE/STUN/TURN 配置到 WebRTC 工厂,并按需调整传输层优先级顺序
func (c *Ctr) UpdateCoreConfig(networkID uint64, config interface{}) error {
c.mu.RLock()
defer c.mu.RUnlock()
c.logger.Info("更新 Core 配置",
zap.Uint64("network_id", networkID))
update, ok := config.(*CoreConfigUpdate)
if !ok {
return fmt.Errorf("不支持的配置类型:%T", config)
}
// ✅ P3-1 阶段 - 暂不实现
// 未来实现:
// 1. 根据策略类型解析配置
// 2. 调用 Core 的 UpdateConfig 方法
// 3. 持久化到数据库
networkIDStr := strconv.FormatUint(networkID, 10)
engineCtl, ok := c.engines[networkIDStr]
if !ok {
return fmt.Errorf("网络未启用增强模式")
}
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 模式
@@ -400,7 +404,7 @@ func (c *Ctr) GetWGMode() string {
// NetworkStatus 网络状态
type NetworkStatus struct {
NetworkID string `json:"network_id"`
WGStatus *WGStatus `json:"wg_status"`
CoreStatus *core.EngineStatus `json:"core_status,omitempty"`
NetworkID string `json:"network_id"`
WGStatus *WGStatus `json:"wg_status"`
CoreStatus *contract.EngineStatus `json:"core_status,omitempty"`
}
+52 -12
View File
@@ -8,8 +8,8 @@ import (
"strings"
"sync"
contract "git.zkcoi.com/zkcoi/meshray-contract"
"go.uber.org/zap"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/wgctrl"
@@ -48,7 +48,8 @@ func NewWGManager(logger *zap.Logger) *WGManager {
}
// 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()
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("device", deviceName),
zap.String("subnet", subnet),
zap.Int("listen_port", listenPort))
zap.Int("listen_port", listenPort),
zap.String("mesh_mode", meshMode))
// 生成密钥对
privateKey, err := wgtypes.GeneratePrivateKey()
@@ -76,7 +78,7 @@ func (m *WGManager) CreateDevice(networkID string, subnet string, listenPort int
zap.String("public_key", publicKey.String()))
// 使用用户态模式启动 wireguard-go 进程,并获取资源引用
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort)
tunDev, wgDev, err := m.startUserModeWGProcessWithRefs(deviceName, privateKey, listenPort, networkID, meshMode)
if err != nil {
return err
}
@@ -356,19 +358,43 @@ type WGStatus struct {
}
// ListPeers 获取设备下所有 Peer 信息
// 除内存记录外,还会从 wgctrl 读取对端真实 Endpoint,作为增强模式 P2P 建连的候选地址来源
func (m *WGManager) ListPeers(networkID string) ([]PeerInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
device, ok := m.devices[networkID]
if !ok {
m.mu.RUnlock()
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
// 注意:增强模式(meshray-core 接管收发)下不应再改写 peer Endpoint 为回环端口,
// 否则 WG 入站源地址匹配失败;此方法仅用于设置对端真实地址或原生模式。
func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEndpoint string) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -434,7 +460,7 @@ func (m *WGManager) UpdatePeerEndpoint(networkID string, publicKey string, newEn
// startUserModeWGProcess 用户态模式启动 wireguard-go(保存资源引用)
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 {
return err
}
@@ -445,10 +471,12 @@ func (m *WGManager) startUserModeWGProcess(deviceName string, privateKey wgtypes
}
// startUserModeWGProcessWithRefs 用户态模式启动 wireguard-go,返回资源引用
func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey wgtypes.Key, listenPort int) (tun.Device, *device.Device, error) {
// meshMode 决定 Bind 来源:native 用 wireguard-go 默认 Bindenhanced 用 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",
zap.String("device", deviceName),
zap.Int("port", listenPort))
zap.Int("port", listenPort),
zap.String("mesh_mode", meshMode))
// 1. 创建 TUN 设备并保存引用
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)
}
// 2. 创建 UDP bind
bind := conn.NewDefaultBind()
// 2. 创建 UDP bind:根据组网模式选择
// - 原生模式: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 并保存引用
logger := &device.Logger{
+2 -2
View File
@@ -32,7 +32,7 @@ func NewDeviceService(store *sqlite.Store, ctrClient ctr.Client) *DeviceService
// GetDevice 获取设备详情
func (s *DeviceService) GetDevice(id uint64) (*model.Device, error) {
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 errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("设备不存在")
@@ -45,7 +45,7 @@ func (s *DeviceService) GetDevice(id uint64) (*model.Device, error) {
// ListAllDevices 获取所有设备列表
func (s *DeviceService) ListAllDevices() ([]model.Device, error) {
var devices []model.Device
err := s.store.DB().Preload("Network").Find(&devices).Error
err := s.store.DB().Find(&devices).Error
return devices, err
}
+186
View File
@@ -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
}
+14
View File
@@ -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
)
+20
View File
@@ -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=
+1 -1
View File
@@ -8,7 +8,7 @@ echo.
REM 检查后端是否已编译
if not exist "meshray.exe" (
echo [1/3] 正在编译后端...
go build -o meshray.exe
go build -o meshray.exe ./cmd/meshray
if errorlevel 1 (
echo ❌ 后端编译失败!
pause
+183 -8
View File
@@ -261,16 +261,191 @@
</div>
</div>
<!-- 其他页面占位 -->
<div v-if="['devices', 'services', 'settings'].includes(currentPage)">
<h2 class="text-2xl font-bold mb-6">
{{ currentPage === 'devices' ? '设备管理' :
currentPage === 'services' ? '服务管理' : '系统设置' }}
</h2>
<div class="card p-6 text-center text-gray-500">
<p>页面开发中...</p>
<!-- 设备管理页面 -->
<div v-if="currentPage === 'devices'">
<h2 class="text-2xl font-bold mb-6">设备管理</h2>
<div class="card p-6">
<el-button type="primary" @click="openCreateDevice">
<el-icon><Plus /></el-icon>
创建设备
</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 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-container>
</el-container>
+434 -117
View File
@@ -29,10 +29,41 @@ const app = createApp({
// 检查是否已登录
if (authToken) {
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({
networkCount: 0,
deviceCount: 0,
@@ -40,13 +71,8 @@ const app = createApp({
pendingApprovals: 0
});
// 组网列表
const networks = ref([]);
// 对话框控制
const showCreateNetworkDialog = ref(false);
// 新组网表单
const newNetwork = reactive({
name: '',
subnet_ipv4: '10.0.0.0/24',
@@ -54,67 +80,51 @@ const app = createApp({
mesh_mode: 'enhanced'
});
// API 基础 URL
const API_BASE = '/api/v1';
// 加载统计数据
const loadStats = async () => {
try {
const response = await fetch(`${API_BASE}/dashboard/stats`);
if (response.ok) {
const result = await response.json();
if (result.data) {
stats.networkCount = result.data.network_count || 0;
stats.deviceCount = result.data.device_count || 0;
stats.onlineDevices = result.data.online_devices || 0;
stats.pendingApprovals = result.data.pending_approvals || 0;
}
const r = await apiRequest('/dashboard/stats');
if (r.ok && r.data) {
stats.networkCount = r.data.total_networks || 0;
stats.deviceCount = r.data.total_devices || 0;
stats.onlineDevices = r.data.online_devices || 0;
stats.pendingApprovals = r.data.pending_approvals || 0;
} else if (!r.ok && r.status !== 401) {
console.error('加载统计数据失败:', r.error);
}
} catch (error) {
console.error('加载统计数据失败:', error);
}
};
// 加载组网列表
const loadNetworks = async () => {
try {
const response = await fetch(`${API_BASE}/networks`);
if (response.ok) {
const result = await response.json();
networks.value = result.data || [];
const r = await apiRequest('/networks');
if (r.ok) {
networks.value = r.data || [];
} else if (r.status !== 401) {
console.error('加载组网列表失败:', r.error);
}
} catch (error) {
console.error('加载组网列表失败:', error);
}
};
// 创建组网
const createNetwork = async () => {
try {
const response = await fetch(`${API_BASE}/networks`, {
const r = await apiRequest('/networks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newNetwork)
});
if (response.ok) {
const result = await response.json();
if (r.ok) {
ElementPlus.ElMessage.success('组网创建成功');
showCreateNetworkDialog.value = false;
// 重置表单
newNetwork.name = '';
newNetwork.subnet_ipv4 = '10.0.0.0/24';
newNetwork.listen_port = 51820;
// 刷新列表
await loadNetworks();
await loadStats();
} else {
const result = await response.json();
ElementPlus.ElMessage.error(result.error || '创建失败');
ElementPlus.ElMessage.error(r.error || '创建失败');
}
} catch (error) {
console.error('创建组网失败:', error);
@@ -122,20 +132,16 @@ const app = createApp({
}
};
// 查看组网详情
const viewNetwork = (network) => {
ElementPlus.ElMessageBox.alert(
`组网名称:${network.name}\n` +
`子网:${network.subnet_ipv4}\n` +
`模式:${network.mesh_mode === 'enhanced' ? '增强' : '原生'}`,
'组网详情',
{
confirmButtonText: '确定'
}
{ confirmButtonText: '确定' }
);
};
// 生成 MeshSeed
const generateMeshSeed = (network) => {
ElementPlus.ElMessageBox.prompt(
'请输入 MeshSeed 有效期(小时)',
@@ -150,23 +156,12 @@ const app = createApp({
try {
const hours = parseInt(value);
const expiresAt = new Date(Date.now() + hours * 3600 * 1000).toISOString();
const response = await fetch(`${API_BASE}/networks/${network.id}/mesh-seed`, {
const r = await apiRequest(`/networks/${network.id}/mesh-seed`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
max_uses: 10,
expires_at: expiresAt,
ddns_enabled: false
})
body: JSON.stringify({ max_uses: 10, expires_at: expiresAt, ddns_enabled: false })
});
if (response.ok) {
const result = await response.json();
const seedUrl = result.data?.seed_url || result.data?.seed;
if (r.ok) {
const seedUrl = (r.data && (r.data.seed_url || r.data.seed)) || '';
if (seedUrl) {
ElementPlus.ElMessageBox.alert(
`MeshSeed 生成成功:<br><br>` +
@@ -174,15 +169,13 @@ const app = createApp({
`style="width: 100%; padding: 8px; margin-top: 10px; border: 1px solid #ddd;" ` +
`onclick="this.select()">`,
'MeshSeed',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '复制并关闭'
}
{ dangerouslyUseHTMLString: true, confirmButtonText: '复制并关闭' }
);
} else {
ElementPlus.ElMessage.success('MeshSeed 生成成功');
}
} else {
const result = await response.json();
ElementPlus.ElMessage.error(result.error || '生成失败');
ElementPlus.ElMessage.error(r.error || '生成失败');
}
} catch (error) {
console.error('生成 MeshSeed 失败:', error);
@@ -191,30 +184,20 @@ const app = createApp({
}).catch(() => {});
};
// 删除组网
const deleteNetwork = async (network) => {
try {
await ElementPlus.ElMessageBox.confirm(
`确定要删除组网 "${network.name}" 吗?此操作不可恢复。`,
'警告',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'
}
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
);
const response = await fetch(`${API_BASE}/networks/${network.id}`, {
method: 'DELETE'
});
if (response.ok) {
const r = await apiRequest(`/networks/${network.id}`, { method: 'DELETE' });
if (r.ok) {
ElementPlus.ElMessage.success('删除成功');
await loadNetworks();
await loadStats();
} else {
const result = await response.json();
ElementPlus.ElMessage.error(result.error || '删除失败');
ElementPlus.ElMessage.error(r.error || '删除失败');
}
} catch (error) {
if (error !== 'cancel') {
@@ -224,49 +207,365 @@ const app = createApp({
}
};
// 组件挂载时加载数据
onMounted(() => {
if (isLoggedIn.value) {
loadStats();
loadNetworks();
// ===================== 设备管理 =====================
const devices = ref([]);
const showCreateDeviceDialog = ref(false);
const deviceForm = reactive({ network_id: null, name: '', virtual_ip: '' });
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 () => {
if (!loginForm.username || !loginForm.password) {
ElementPlus.ElMessage.warning('请输入用户名和密码');
return;
}
loggingIn.value = true;
try {
const response = await fetch(`${API_BASE}/auth/login`, {
const r = await apiRequest('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: loginForm.username,
password: loginForm.password
})
});
const result = await response.json();
if (response.ok && result.data) {
// 保存 token
authToken = result.data.token || result.data.access_token;
if (r.ok && r.data) {
authToken = r.data.token || r.data.access_token;
localStorage.setItem('meshray_token', authToken);
isLoggedIn.value = true;
ElementPlus.ElMessage.success('登录成功');
// 加载数据
await loadStats();
await loadNetworks();
await loadAll();
} else {
ElementPlus.ElMessage.error(result.error || '登录失败');
ElementPlus.ElMessage.error(r.error || '登录失败');
}
} catch (error) {
console.error('登录错误:', error);
@@ -276,26 +575,15 @@ const app = createApp({
}
};
// 处理退出
const handleLogout = () => {
ElementPlus.ElMessageBox.confirm(
'确定要退出登录吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 清除 token
ElementPlus.ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(() => {
localStorage.removeItem('meshray_token');
authToken = '';
isLoggedIn.value = false;
// 重置表单
loginForm.username = '';
loginForm.password = '';
ElementPlus.ElMessage.success('已退出登录');
}).catch(() => {});
};
@@ -314,7 +602,36 @@ const app = createApp({
createNetwork,
viewNetwork,
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
};
}
});