From 59e30592467e3dea4e7adb67d24484e5fdccac05 Mon Sep 17 00:00:00 2001 From: zkcoi Date: Wed, 15 Jul 2026 16:08:13 +0800 Subject: [PATCH] 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 --- CHANGELOG.md | 502 +++++++++++++----- cmd/checkdb/main.go | 2 +- cmd/mr-wg/config.go | 106 ++++ cmd/mr-wg/main.go | 265 ++++++++++ cmd/mr-wg/meshray-core.example.json | 17 + cmd/mr-wg/meshray-core.toml.sample | 54 ++ core/README.md | 272 ---------- core/connect/direct.go | 93 ---- core/connect/fake_tcp.go | 219 -------- core/connect/ice.go | 585 --------------------- core/connect/real_tcp.go | 181 ------- core/connect/strategy.go | 754 ---------------------------- core/connect/stun.go | 119 ----- core/connect/turn.go | 358 ------------- core/connect/turn_quic.go | 253 ---------- core/connect/ws.go | 216 -------- core/core.go | 118 ----- core/engine.go | 333 ------------ core/metrics.go | 83 --- core/plugins/wg/wgparse.go | 47 -- core/transport/conn_manager.go | 99 ---- core/transport/plugin.go | 17 - core/transport/relay.go | 197 -------- go.mod | 12 +- go.sum | 2 + internal/api/server.go | 13 + internal/ctr/core_enable.go | 8 + internal/ctr/corebind/adapter.go | 67 +++ internal/ctr/corebind/bind.go | 309 ++++++++++++ internal/ctr/corebind/registry.go | 80 +++ internal/ctr/ctr.go | 266 +++++----- internal/ctr/wg.go | 64 ++- internal/service/device.go | 4 +- pkg/contract/contract.go | 186 +++++++ pkg/contract/go.mod | 14 + pkg/contract/go.sum | 20 + start.bat | 2 +- web/static/index.html | 191 ++++++- web/static/js/app.js | 583 ++++++++++++++++----- 39 files changed, 2350 insertions(+), 4361 deletions(-) create mode 100644 cmd/mr-wg/config.go create mode 100644 cmd/mr-wg/main.go create mode 100644 cmd/mr-wg/meshray-core.example.json create mode 100644 cmd/mr-wg/meshray-core.toml.sample delete mode 100644 core/README.md delete mode 100644 core/connect/direct.go delete mode 100644 core/connect/fake_tcp.go delete mode 100644 core/connect/ice.go delete mode 100644 core/connect/real_tcp.go delete mode 100644 core/connect/strategy.go delete mode 100644 core/connect/stun.go delete mode 100644 core/connect/turn.go delete mode 100644 core/connect/turn_quic.go delete mode 100644 core/connect/ws.go delete mode 100644 core/core.go delete mode 100644 core/engine.go delete mode 100644 core/metrics.go delete mode 100644 core/plugins/wg/wgparse.go delete mode 100644 core/transport/conn_manager.go delete mode 100644 core/transport/plugin.go delete mode 100644 core/transport/relay.go create mode 100644 internal/ctr/core_enable.go create mode 100644 internal/ctr/corebind/adapter.go create mode 100644 internal/ctr/corebind/bind.go create mode 100644 internal/ctr/corebind/registry.go create mode 100644 pkg/contract/contract.go create mode 100644 pkg/contract/go.mod create mode 100644 pkg/contract/go.sum diff --git a/CHANGELOG.md b/CHANGELOG.md index dca7256..8b9473e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 只接收所连对端的包: +- NAS(NAT 后)拨 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 建共享 socket(listen_port=0 时由 OS 分配并回填 + 引擎实际端口),启动 `readDirectLoop`。 + - `readDirectLoop` 从任意来源收包,把**真实源地址**作为 Endpoint 投入 `inboundCh` + (NAT 穿透学址核心:公网侧据此学到对端 NAT 映射后地址)。 + - `receiveFunc`/`endpointForPacket`:Direct 入站用学得的真实源地址;中继入站按 peerKey 查缓存。 + - `Send` → `directSendTo`:经共享 socket `WriteToUDP` 发往目标地址(源端口固定 listen_port, + 满足对称直连)。移除旧的随机端口兜底 socket(`fallbackSock`/`fallbackSend`)。 +- `core/engine.go`:新增 `directByBind` 标志(由 `NewEnhancedBind` 置位)。增强模式下 + `NotifyPeerInfo` 不再对 Direct 层主动拨号(WG 启动后自动向 endpoint 发握手经 `Send` 承载), + 避免 `DirectFactory.Dial` 再绑 listen_port 造成 UDP 端口冲突。 -#### 编译与部署 -- ✅ 创建 Windows 一键启动脚本(start.bat) -- ✅ 创建 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 无任何监听 socket(B 的传输 socket 本地是 +临时端口),**被 OS 丢弃**;B→A 同理。即两端都出得去、进不来——这正是「frp 能传、meshray-core +不行」的本质:frp 是中心中继(两端都出向连 server),而本仓增强模式要求对称直连却没把 +本地端口绑到 listen_port。 -### 🔒 安全性 +### 修复 +让 Direct-UDP 传输 socket **把本地端口绑到 listen_port(WG listen_port)**,形成 +`A:port↔B:port` 对称直连: +- `connect/direct.go`:`DirectFactory` 增加 `localPort` 与 `SetLocalPort`;`Dial` 时 + `dialer.LocalAddr = &net.UDPAddr{Port: bindPort}`(优先 `DialConfig.LocalPort`,其次工厂值)。 +- `connect/strategy.go`:`DialConfig` 增加 `LocalPort int`。 +- `core/engine.go`:`Engine` 持有 `listenPort` 与 `directFactory` 引用,新增 `SetListenPort` + 回填工厂;`initiateConnection` 把 `listenPort` 注入 `DialConfig.LocalPort`。 +- `core/bind.go`:`NewEnhancedBind` 内调用 `eng.SetListenPort(listenPort)`, + 使 standalone 与受管模式(均经此入口)都能拿到本端端口。 -- ✅ bcrypt 密码加密(DefaultCost 强度) -- ✅ JWT 身份验证 -- ✅ 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** 匹配 peer,Endpoint 仅用于 +握手更新对端出向地址与 MAC2 cookie 计算。因此回灌入站时只要携带**对端真实 Endpoint**, +即可正确接通,无需让 WG 监听真实 UDP 端口——这正是增强 Bind 完全接管收发的依据。 + +### core/bind.go(重写) +- `EnhancedBind` 不再依赖默认 bind 的真实 UDP 监听:`Open` 返回从 `inboundCh` 读取引擎 + 入站的接收函数;`Send` 按 `ep` 反查 peerKey,经引擎 9 层传输连接发送,连接未就绪时 + 惰性触发拨号并走直连兜底。 +- `deliverInbound`:引擎传输连接读到的入站密文投入 `inboundCh`,由接收函数拷贝进 WG + 缓冲并填入对端真实 Endpoint 回灌。 +- `RegisterPeer(addr, peerKey)`:维护 endpoint→peerKey 与 peerKey→真实 Endpoint 映射。 +- 接收函数严格遵守 WG 缓冲所有权(拷贝而非替换指针),`BatchSize`/`ParseEndpoint` 委托默认 bind。 + +### core/transport/relay.go +- 新增 `OnInbound func(peerKey string, packet []byte)` 钩子;`StartReadFromRemoteConn` 设置后 + 入站改投该钩子(即 EnhancedBind),否则走原本地端口转发(向后兼容)。 + +### core/engine.go +- `Engine` 增加 `bind *EnhancedBind` 字段(由 `NewEnhancedBind` 回填)。 +- `NotifyPeerInfo` 在收到候选时自动 `RegisterPeer`,并新增 `EnsureDial`(Send 惰性拨号用)。 + +### core/cmd/meshray-core(新增独立二进制) +- `main.go` + `config.go`:类 frp 风格,`-c config.json` 启动、`-genkey` 生成私钥。 +- 直接以 `core.NewEnhancedBind` 接管 wireguard-go 用户态设备收发;按配置创建 TUN、 + 注入对端候选触发 9 层拨号、`Up` 后分配隧道 IP(Linux `ip` / Windows `netsh`)。 +- `mode: enhanced` 走 9 层接管;`mode: native` 退化为裸 WG 直连,便于对照测试。 +- 新增 `meshray-core.example.json` 示例配置(占位符,用户替换公私钥/endpoint 即可两机对测)。 +- `go.mod`:`go mod tidy` 引入 `wireguard/wgctrl` 与 Windows `wintun` 依赖。 + +### 构建验证 +- `core` 独立:`go build ./... && go vet ./...` → 通过;`go build -o meshray-core.exe ./cmd/meshray-core` 通过; + `./meshray-core.exe -genkey` 正常输出私钥。 +- 管理器默认:`go build ./...` → 通过;增强:`go build -tags meshray_core ./...` → 通过。 + +### 两机对测步骤(摘要,详见二进制内注释与示例配置) +1. 两台机器各自 `meshray-core -genkey` 得私钥,用 `private_key` 对应公钥填对方 `public_key`。 +2. A 的 `interface=10.0.0.1/24`、`peers[0].endpoint=B公网IP:51820`、`allowed_ips=10.0.0.2/32`;B 对称。 +3. 两台均 `meshray-core -c meshray-core.json`,随后 `ping 10.0.0.x` 验证隧道收发。 +4. Windows 需已安装 Wintun 驱动(随 WireGuard 客户端提供)。 + +## 2026-07-15 修复:初始直连缺少入站读取,公网/独立 IP 机器直连亦不通 + +### 背景(用户纠正) +用户指出 meshray-core 的核心机制必须**同时**满足「NAT 后的机器」与「有独立 IP 的机器」, +二者都要能直连(P2P)→ 失败再走中继/打洞兜底。这意味着即使两台公网机器直连, +当前实现也应能接通。实测与代码核查发现:**初始直连路径本身就没接通**,与是否 NAT 无关。 + +### 根因 +- `StrategyScheduler.Dial`(`core/connect/strategy.go:284`)成功后**仅返回连接,从不调用 `OnConnectionUpdate`**。 +- `OnConnectionUpdate` 回调(`core/engine.go:45`)才是唯一会调用 + `relay.StartReadFromRemoteConn` 启动入站读取的地方;而它只在 `reconnectToLayer` + (降级重连)与 `probeHigherLayers`(恢复探测)内触发。 +- `Engine.initiateConnection` 拿到 `Dial` 返回的直连后只做了 `connMgr.Add`,**未启动入站读取协程**。 + +### 断点后果 +两侧都只发出握手、收不到对方回包 → 握手无法完成 → 隧道起不来。 +该缺陷在「两台公网/独立 IP 机器直连」场景下同样存在,并非 NAT 专属问题。 + +### 修复 +- `core/engine.go`:`initiateConnection` 在 `connMgr.Add` 之后显式调用 + `e.relay.StartReadFromRemoteConn(context.Background(), peerKey, conn)`, + 使初始直连的入站密文能经 `OnInbound` 回灌 WG(增强模式)或写入本地端口。 +- 降级重连 / 恢复探测仍走 `OnConnectionUpdate` 原路径,无重复启动冲突 + (旧连接被 `connMgr.Add` 关闭后其读取协程自然退出)。 + +### 构建验证 +- `core` 独立:`go build ./... && go vet ./...` → 通过。 +- 注:沙箱无法运行 TUN/Wintun,端到端需用户在两台真实机器(或同机双进程)验证 + `ping 10.0.0.x` 收发。修复后公网 IP / 同局域网直连场景预期可正常握手建隧。 diff --git a/cmd/checkdb/main.go b/cmd/checkdb/main.go index 0b17a76..4638adb 100644 --- a/cmd/checkdb/main.go +++ b/cmd/checkdb/main.go @@ -14,7 +14,7 @@ func main() { log.Fatal(err) } - fmt.Println("=== MeshRay 数据库表结构检查 ===\n") + fmt.Println("=== MeshRay 数据库表结构检查 ===") // 检查所有表是否存在 tables := []string{ diff --git a/cmd/mr-wg/config.go b/cmd/mr-wg/config.go new file mode 100644 index 0000000..71e5de7 --- /dev/null +++ b/cmd/mr-wg/config.go @@ -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 +} diff --git a/cmd/mr-wg/main.go b/cmd/mr-wg/main.go new file mode 100644 index 0000000..a9354f7 --- /dev/null +++ b/cmd/mr-wg/main.go @@ -0,0 +1,265 @@ +// 命令 meshray-core:meshray-core 增强层的独立运行入口(类 frp 风格)。 +// +// 直接以自定义 conn.Bind 接管 WireGuard 收发,使所有隧道密文流量经由本仓 9 层传输 +// (Direct/TURN/WebRTC/...)而非裸 UDP。无需管理器即可在两台机器间建立加密隧道, +// 用于端到端验证 core 的收发能力。 +// +// 用法: +// +// meshray-core -genkey # 生成私钥 +// meshray-core -c meshray-core.toml # 按配置启动(默认 TOML,类 frp;兼容 .json) +package main + +import ( + "flag" + "fmt" + "net" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "syscall" + + meshraycore "git.zkcoi.com/zkcoi/meshray/core/engine" + "git.zkcoi.com/zkcoi/meshray/core/connect" + corebind "git.zkcoi.com/zkcoi/meshray/internal/ctr/corebind" + "go.uber.org/zap" + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func main() { + cfgPath := flag.String("c", "meshray-core.toml", "配置文件路径(默认 TOML,类 frp;也兼容 .json)") + genKey := flag.Bool("genkey", false, "仅生成私钥并打印后退出") + flag.Parse() + + if *genKey { + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + fmt.Fprintln(os.Stderr, "生成私钥失败:", err) + os.Exit(1) + } + fmt.Println(key.String()) + return + } + + cfg, err := loadConfig(*cfgPath) + if err != nil { + fmt.Fprintln(os.Stderr, "加载配置失败:", err) + os.Exit(1) + } + + logger := newLogger() + defer func() { _ = logger.Sync() }() + + // 1. 准备私钥:配置中给出则解析,否则生成并落盘 + priv, err := resolvePrivateKey(cfg, logger) + if err != nil { + logger.Fatal("私钥处理失败", zap.Error(err)) + } + + // 2. 创建 TUN 设备(用户态)。Windows 需已安装 Wintun 驱动。 + devName := "mrc0" + tunDev, err := tun.CreateTUN(devName, cfg.MTU) + if err != nil { + logger.Fatal("创建 TUN 设备失败(Windows 需 Wintun 驱动)", zap.Error(err)) + } + defer tunDev.Close() + + // 3. 创建 Bind:enhanced=自定义接管;native=裸 WG 默认 Bind + var wgBind conn.Bind + if cfg.Mode == "native" { + logger.Info("以原生直连模式运行(裸 WG UDP)") + wgBind = conn.NewDefaultBind() + } else { + logger.Info("以增强模式运行(9 层传输接管收发)") + eb, eerr := corebind.NewEnhancedBind(cfg.ListenPort, cfg.NetworkID, logger) + if eerr != nil { + logger.Fatal("创建增强 Bind 失败", zap.Error(eerr)) + } + wgBind = eb + } + + wgLogger := &device.Logger{ + Verbosef: func(format string, args ...interface{}) { logger.Debug(fmt.Sprintf(format, args...)) }, + Errorf: func(format string, args ...interface{}) { logger.Error(fmt.Sprintf(format, args...)) }, + } + wgDev := device.NewDevice(tunDev, wgBind, wgLogger) + defer wgDev.Close() + + // 4. 组装 WG 配置(私钥 / 监听端口 / 各 peer) + var sb strings.Builder + fmt.Fprintf(&sb, "private_key=%s\nlisten_port=%d\n", priv.String(), cfg.ListenPort) + for _, p := range cfg.Peers { + pub, perr := wgtypes.ParseKey(p.PublicKey) + if perr != nil { + logger.Fatal("对端公钥解析失败", zap.String("public_key", p.PublicKey), zap.Error(perr)) + } + fmt.Fprintf(&sb, "public_key=%s\n", pub.String()) + if p.Endpoint != "" { + fmt.Fprintf(&sb, "endpoint=%s\n", p.Endpoint) + } + for _, a := range p.AllowedIPs { + fmt.Fprintf(&sb, "allowed_ip=%s\n", a) + } + fmt.Fprintf(&sb, "persistent_keepalive=25\n") + } + if err := wgDev.IpcSet(sb.String()); err != nil { + logger.Fatal("配置 WireGuard 设备失败", zap.Error(err)) + } + + // 5. 增强模式:把对端候选(endpoint)注入引擎触发 9 层拨号 + if eb, ok := wgBind.(*corebind.EnhancedBind); ok { + eng := eb.Engine() + + // 5.0 下达 ICE 配置(STUN/TURN),手动 TURN/WebRTC 层级必需 + if len(cfg.ICE.STUN) > 0 || len(cfg.ICE.TURN) > 0 { + turnCfgs := make([]connect.TURNServerConfig, 0, len(cfg.ICE.TURN)) + for _, t := range cfg.ICE.TURN { + turnCfgs = append(turnCfgs, connect.TURNServerConfig{URLs: []string{t}}) + } + ice := connect.ICEConfig{ + STUNServers: cfg.ICE.STUN, + TURNServers: turnCfgs, + } + if err := eng.SetICEConfig(ice); err != nil { + logger.Error("下达 ICE 配置失败", zap.Error(err)) + } + } + + // 5.1 应用传输层级策略:auto=默认 9 层自动降级;manual=仅用配置的 layers + if cfg.LayerStrategy == "manual" { + if len(cfg.Layers) == 0 { + logger.Fatal("layer_strategy=manual 但 layers 为空") + } + layers, perr := parseLayers(cfg.Layers) + if perr != nil { + logger.Fatal("解析手动层级失败", zap.Error(perr)) + } + eng.SetLayerOrder(layers) + if len(layers) == 1 { + logger.Info("手动层级模式:强制固定层(禁用降级)", + zap.String("layer", layers[0].String())) + } else { + logger.Info("手动层级模式:自定义降级顺序", + zap.Strings("layers", cfg.Layers)) + } + } else { + logger.Info("自动层级模式:9 层自动降级", + zap.Int("default_layers", len(connect.DefaultLayerOrder))) + } + + // 5.2 注入对端候选,触发拨号 + for _, p := range cfg.Peers { + if p.Endpoint == "" { + continue + } + if err := eng.NotifyPeerInfo(p.PublicKey, []meshraycore.Candidate{{Addr: p.Endpoint}}); err != nil { + logger.Error("注入对端候选失败", zap.String("peer", p.PublicKey), zap.Error(err)) + } + } + } + + // 6. 启动设备并配置隧道 IP + if err := wgDev.Up(); err != nil { + logger.Fatal("启动 WireGuard 设备失败", zap.Error(err)) + } + devName, nameErr := tunDev.Name() + if nameErr != nil { + logger.Warn("获取 TUN 设备名失败", zap.Error(nameErr)) + devName = "mrc0" + } + assignInterfaceIP(devName, cfg.InterfaceIP, logger) + + logger.Info("meshray-core 已启动", + zap.String("network_id", cfg.NetworkID), + zap.String("mode", cfg.Mode), + zap.String("device", devName), + zap.String("interface", cfg.InterfaceIP)) + + // 7. 阻塞等待退出信号 + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + logger.Info("正在退出...") +} + +// resolvePrivateKey 返回私钥:配置给出则解析,否则生成并写入 private_key_file +func resolvePrivateKey(cfg *Config, logger *zap.Logger) (wgtypes.Key, error) { + if cfg.PrivateKey != "" { + return wgtypes.ParseKey(cfg.PrivateKey) + } + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + return wgtypes.Key{}, err + } + if cfg.PrivateKeyFile != "" { + if werr := os.WriteFile(cfg.PrivateKeyFile, []byte(key.String()), 0600); werr != nil { + logger.Warn("写入私钥文件失败", zap.Error(werr)) + } else { + logger.Info("已生成并写入私钥", zap.String("file", cfg.PrivateKeyFile)) + } + } + return key, nil +} + +// assignInterfaceIP 给隧道接口分配 IP 并 up(best-effort,失败仅告警) +func assignInterfaceIP(name, cidr string, logger *zap.Logger) { + if cidr == "" { + return + } + ip, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + logger.Error("解析隧道 IP 失败", zap.String("cidr", cidr), zap.Error(err)) + return + } + switch runtime.GOOS { + case "linux": + run(logger, "ip", "addr", "add", cidr, "dev", name) + run(logger, "ip", "link", "set", name, "up") + case "windows": + // ipNet.Mask 已是网络掩码字节,转点分十进制 + m := ipNet.Mask + maskStr := fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3]) + run(logger, "netsh", "interface", "ip", "set", "address", + "name="+name, "static", ip.String(), maskStr) + default: + logger.Warn("不支持的平台,请手动配置隧道 IP", zap.String("os", runtime.GOOS)) + } +} + +// run 执行外部命令(best-effort,输出仅调试) +func run(logger *zap.Logger, name string, args ...string) { + cmd := exec.Command(name, args...) + out, err := cmd.CombinedOutput() + if err != nil { + logger.Warn("执行命令失败(可忽略,手动配置即可)", + zap.String("cmd", name), zap.String("args", strings.Join(args, " ")), + zap.Error(err), zap.String("output", string(out))) + } +} + +// newLogger 创建生产级 logger +func newLogger() *zap.Logger { + l, err := zap.NewProduction() + if err != nil { + l = zap.NewNop() + } + return l +} + +// parseLayers 将配置中的层级名列表解析为 connect.Layer 列表,任一无法解析则返回错误 +func parseLayers(names []string) ([]connect.Layer, error) { + layers := make([]connect.Layer, 0, len(names)) + for _, n := range names { + l, ok := connect.ParseLayer(n) + if !ok { + return nil, fmt.Errorf("未知传输层: %q", n) + } + layers = append(layers, l) + } + return layers, nil +} diff --git a/cmd/mr-wg/meshray-core.example.json b/cmd/mr-wg/meshray-core.example.json new file mode 100644 index 0000000..aa73635 --- /dev/null +++ b/cmd/mr-wg/meshray-core.example.json @@ -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": [] } +} diff --git a/cmd/mr-wg/meshray-core.toml.sample b/cmd/mr-wg/meshray-core.toml.sample new file mode 100644 index 0000000..2ee15ce --- /dev/null +++ b/cmd/mr-wg/meshray-core.toml.sample @@ -0,0 +1,54 @@ +# meshray-core 独立运行配置(类 frp 风格,TOML) +# 用法: meshray-core -c meshray-core.toml +# 也兼容旧 JSON 配置(扩展名 .json 自动按 JSON 解析)。 + +# 网络标识,同网段节点需一致 +network_id = "net1" + +# 本地监听端口(WireGuard / 增强模式收发端口) +listen_port = 51820 + +# 隧道 MTU(0 则默认 1420) +mtu = 1420 + +# 私钥:留空则自动生成并写入 private_key_file +private_key = "" +private_key_file = "key.txt" + +# 本机隧道 IP(CIDR) +interface = "10.0.0.1/24" + +# 运行模式: +# enhanced = 经 9 层传输接管 WG 收发(支持自动降级 / 手动层级) +# native = 裸 WireGuard UDP 直连(不经本仓传输层) +mode = "enhanced" + +# 传输层级策略: +# auto = 默认,9 层自动降级(直连 → 中继 → ... → WS/WSS) +# manual = 仅使用下方 layers 指定的层 +layer_strategy = "auto" + +# 手动层级列表(仅 layer_strategy = "manual" 生效)。 +# 单元素 = 强制固定该层,禁用降级,例如强制走 TURN 中继:["TURN-TCP"] +# 多元素 = 按列表顺序自定义降级链,例如直连优先、不通再中继:["Direct-UDP","TURN-TCP"] +# 可选值:Direct-UDP / FakeTCP / RealTCP / TURN-UDP / TURN-QUIC / TURN-TCP / +# TURN-TLS / WebRTC / WS(或 WSS) +# 注意:该键须置于 [[peers]] 之前(TOML 中 [[peers]] 后键会被归入 peers 表)。 +layers = ["Direct-UDP"] + +# 对端列表 +[[peers]] +public_key = "对端公钥base64" # 必填 +endpoint = "203.0.113.2:51820" # 增强模式经 9 层拨号对端;留空则仅作为接收方 +allowed_ips = ["10.0.0.2/32"] + +# 可选第二个对端 +# [[peers]] +# public_key = "另一个对端公钥base64" +# endpoint = "198.51.100.7:51820" +# allowed_ips = ["10.0.0.3/32"] + +# STUN/TURN 服务器(增强模式打洞/中继用;手动 TURN/WebRTC 层级必需) +[ice] +stun = ["stun:stun.l.google.com:19302"] +turn = ["turn:your.turn.server:3478"] diff --git a/core/README.md b/core/README.md deleted file mode 100644 index 7ddd2e7..0000000 --- a/core/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# MeshRay-Core 架构规范 - -> Core 是通用的数据传输引擎,通过 ProtocolPlugin 接口适配不同协议。当前默认内置 WG 插件。 - ---- - -## 一、目录结构 - -``` -core/ -├── core.go # 进程入口 -├── engine.go # 引擎实例 -├── metrics.go # 监控指标 -│ -├── connect/ # 建连层 -│ ├── strategy.go # 策略调度 -│ ├── stun.go # STUN 协议 -│ ├── direct.go # Layer 1 -│ ├── fake_tcp.go # Layer 2 -│ ├── real_tcp.go # Layer 3 -│ ├── turn.go # Layer 4/6/7 -│ ├── turn_quic.go # Layer 5 -│ ├── ice.go # Layer 8 -│ └── ws.go # Layer 9 -│ -├── transport/ # 传输层 -│ ├── conn_manager.go # 连接索引 -│ ├── relay.go # 转发循环 -│ └── plugin.go # Plugin 接口 -│ -└── plugins/ # 协议插件 - └── wg/ - └── wgparse.go # WG 插件实现 -``` - ---- - -## 二、各文件职责 - -### 2.1 根目录(4 个文件) - -| 文件 | 职责 | 持有什么 | 不做什么 | -|------|------|---------|----------| -| `core.go` | 进程入口,管理多个 Engine | `map[engineID]*Engine` | 不做建连、不转发数据 | -| `engine.go` | 一个组网的引擎实例 | strategy、conn_manager、relay、plugin | 不直接调用 connect,由 relay 调用 | -| `metrics.go` | 监控指标采集 | 原子计数器(连接数、字节数、切换次数) | 不做业务逻辑 | - -**关键关系**: -- `core.go` 持有多个 `engine.go` -- `internal/ctr/ctr.go` 直接调用 `core.go` 和 `engine.go` (进程内函数调用) -- `engine.go` 持有 connect/、transport/、plugins/ 的实例 - -### 2.2 connect/(9 个文件)— 建连层 - -**职责**:通过各种网络方式建立连接,最终返回 `net.Conn`。 - -**对外暴露的唯一入口**:`strategy.go` 的 `Connect()` 方法。其他 connect 文件只被 `strategy.go` 调用。 - -| 文件 | 对应层级 | 职责 | 返回什么 | -|------|---------|------|---------| -| `strategy.go` | 全部 | 按优先级尝试各层,不通自动切换,定期探测恢复 | `net.Conn` + 当前层级名 | -| `stun.go` | 被 direct/ice 调用 | STUN 协议:发送 Binding Request,获取本机公网地址 | `*net.UDPAddr`(地址,不是连接) | -| `direct.go` | Layer 1 | 调用 stun 获取候选地址,然后 UDP 打洞 | `net.Conn` | -| `fake_tcp.go` | Layer 2 | UDP 包外层封装 TCP 头部,欺骗防火墙 | `net.Conn` | -| `real_tcp.go` | Layer 3 | 真正的 TCP 直连打洞 | `net.Conn` | -| `turn.go` | Layer 4/6/7 | TURN 协议协商(Allocate/Permission/ChannelBind),参数区分 UDP/TCP/TLS | `net.Conn` | -| `turn_quic.go` | Layer 5 | TURN-QUIC 私有扩展(RFC 9000) | `net.Conn` | -| `ice.go` | Layer 8 | ICE 协商 + WebRTC DataChannel,内部调用 stun 收集候选 | `net.Conn` | -| `ws.go` | Layer 9 | WS/WSS 握手 + 帧收发 + 身份标识 | `net.Conn` | - -**9 层完整编号**: - -| 层级 | 链路名称 | 文件 | 传输方式 | 穿透力 | -|------|---------|------|---------|--------| -| 1 | Direct-UDP | direct.go | P2P 直连 | 弱(性能最好) | -| 2 | Direct-FakeTCP | fake_tcp.go | P2P 直连 | 弱 | -| 3 | Direct-RealTCP | real_tcp.go | P2P 直连 | 中 | -| 4 | TURN-UDP | turn.go | 中继 | 中 | -| 5 | TURN-QUIC | turn_quic.go | 中继 | 中 | -| 6 | TURN-TCP | turn.go | 中继 | 强 | -| 7 | TURN-TLS | turn.go | 中继 | 强 | -| 8 | WebRTC | ice.go | ICE/TURN | 强 | -| 9 | WS/WSS | ws.go | 隧道 | 最强(兜底) | - -**strategy.go 的自动切换逻辑**: -- 单包超时 500ms → 切到下一层 -- 10s 滑动窗口丢包率 > 10% → 切到下一层 -- 当前在第 N 层时,每 30s 探测 Layer 1 → 连续 2 次成功直接切回 Layer 1(不逐层回退) - -**stun.go 的特殊地位**:唯一被多处调用的 connect 文件(direct.go 和 ice.go 都需要它),所以独立存在。 - -### 2.3 transport/(3 个文件)— 传输层 - -**职责**:用 `net.Conn` 转发数据。不感知具体协议,通过 ProtocolPlugin 接口适配。 - -| 文件 | 职责 | 不做什么 | -|------|------|---------| -| `conn_manager.go` | 连接索引:`peer_key → net.Conn` 的映射 | 不做建连、不转发数据 | -| `relay.go` | Read/Write 循环:从本地端口收包 → 查路由 → 通过 conn 发送;从 conn 收包 → 发到本地端口 | 不做建连 | -| `plugin.go` | 定义 ProtocolPlugin 接口 | 不实现任何协议 | - -**relay.go 的工作流程**: - -``` -本地端口收到 WG 密文包 - → 调用 plugin.IsControlPacket() 判断包类型 - → true:控制包,按已建链路透传 - → 调用 plugin.IsDataPacket() 判断 - → true:调用 plugin.ExtractRouteID() 提取路由标识 - → 查路由标识映射表 → 发往对应本地端口 - → 都不是:丢弃 -``` - -**关键**:relay.go 不知道 WireGuard,不知道 receiver index,只知道 route_id。 - -### 2.4 plugins/wg/(1 个文件)— 协议插件 - -**职责**:实现 ProtocolPlugin 接口,处理 WG 协议特有的包解析。 - -| 文件 | 职责 | 不做什么 | -|------|------|---------| -| `wgparse.go` | 实现 `IsDataPacket`/`ExtractRouteID`/`IsControlPacket` | 不做建连、不转发数据 | - -**WG 插件的具体实现**: - -| 方法 | 逻辑 | -|------|------| -| `IsControlPacket(packet)` | `packet[0]` ∈ {1, 2, 3} → true | -| `IsDataPacket(packet)` | `packet[0]` == 4 → true | -| `ExtractRouteID(packet)` | 读取 `packet[4:8]`,网络字节序解析为 uint32(即 WG receiver index) | - -### 2.5 plugins/wg/(1 个文件) - -| 文件 | 职责 | -|------|------| -| `wgparse.go` | WireGuard 数据包解析和封装 | - ---- - -## 三、分层架构图 - -``` -┌─────────────────────────────────────────────────────────┐ -│ internal/ctr/ctr.go │ -│ 直接调用 Core (进程内函数调用) │ -└────────────────────────┬────────────────────────────────┘ - │ 函数调用 -┌────────────────────────▼────────────────────────────────┐ -│ core.go │ -│ 管理多个 Engine 实例 │ -└───┬─────────────────────────────────────────────────────┘ - │ 每个组网一个 Engine - ▼ -┌─────────────────────────────────────────────────────────┐ -│ engine.go │ -│ 持有:strategy + conn_manager + relay + plugin │ -│ 协调 connect/ 和 transport/ 工作 │ -└───┬─────────────────────────────────────────────────────┘ - │ - ├──────────────────────────────────────────┐ - ▼ ▼ -┌───────────────────────┐ ┌───────────────────────┐ -│ connect/ │ │ transport/ │ -│ 建连层 │ │ 传输层 │ -│ │ │ │ -│ strategy.go │ 返回 │ relay.go │ -│ ├─ direct.go (L1) │ net.Conn├─ plugin.go │ -│ ├─ fake_tcp.go (L2) │────────►│ │ ProtocolPlugin │ -│ ├─ real_tcp.go (L3) │ │ │ │ -│ ├─ turn.go (L4/6/7) │ │ 插件调用 │ -│ ├─ turn_quic.go(L5) │ │ ▼ │ -│ ├─ ice.go (L8) │ │ plugins/wg/ │ -│ └─ ws.go (L9) │ │ └─ wgparse.go │ -│ │ │ │ -│ stun.go (被 direct/ice 调用) │ conn_manager.go │ -└───────────────────────┘ └───────────────────────┘ -``` - ---- - -## 四、调用关系 - -### 4.1 Engine 创建时 - -``` -engine.go - → 创建 WGPlugin(plugins/wg/wgparse.go) - → 创建 Relay,传入 plugin(transport/relay.go) - → 创建 Strategy(connect/strategy.go) - → 创建 ConnManager(transport/conn_manager.go) -``` - -### 4.2 Bind 流程 - -``` -ctr 直接调用:Bind() - → engine.go 接收函数调用 - → engine.go 调用 strategy.Connect() - → strategy 按优先级尝试各层 - → Layer 1: direct.go 调用 stun.go 获取候选,尝试 UDP 打洞 - → 不通?→ Layer 4: turn.go 调用 TURN 协商 - → 不通?→ Layer 9: ws.go 调用 WS 握手 - → 返回 net.Conn + 当前层级名 - → engine.go 把 net.Conn 注册到 conn_manager - → engine.go 启动 relay 的 Read/Write 循环 -``` - -### 4.3 数据转发流程 - -``` -WG 发出密文包 → 本地端口 - → relay.go 收到 - → 调用 plugin.IsControlPacket() - → true:按已建链路透传(conn_manager 查 conn) - → 调用 plugin.IsDataPacket() - → true:调用 plugin.ExtractRouteID() 获取 route_id - → 查路由标识映射表 → 找到本地端口 → 发送 - → 都不是:丢弃 -``` - ---- - -## 五、通用层与插件层的边界 - -| 层 | 知道什么 | 不知道什么 | -|---|---------|-----------| -| **connect/** | 网络协议(STUN/TURN/WS/WebRTC) | WireGuard、route_id | -| **transport/** | net.Conn、route_id、ProtocolPlugin 接口 | WireGuard、receiver index | -| **plugins/wg/** | WG 包格式、receiver index | 网络连接、net.Conn | -| **engine.go** | 协调 connect/ 和 transport/ | WG 包格式细节 | - -**如果将来要支持其他协议**: -- 新建 `plugins/xxx/xxxparse.go` -- 实现 `ProtocolPlugin` 接口的三个方法 -- `engine.go` 里换成 `xxx.NewPlugin()` -- connect/、transport/、core.go 的代码完全不用改 - ---- - -## 六、Core 接口(直接被 ctr 调用) - -| 方法 | 调用方 | 说明 | -|------|--------|------| -| `CreateEngine` | ctr | 创建一个 Engine 实例(直接函数调用) | -| `Bind` | ctr | 为每个 Peer 开启本地端口,开始建链 | -| `Unbind` | ctr | 停止指定 Peer 的端口监听 | -| `Start` | ctr | 启动转发主循环 | -| `Stop` | ctr | 停止 Engine | -| `GetStatus` | ctr | 查询 Engine 状态 | -| `NotifyPeerInfo` | ctr | 下发对端候选地址和 route_id | - -**实现位置**: -- 所有方法都在 `engine.go` 中实现 -- `core.go` 提供 Engine 实例管理 -- ctr通过`coreInst.CreateEngine(...)`直接调用 - ---- - -## 七、文件清单汇总 - -| 目录 | 文件数 | 文件 | -|------|--------|------| -| 根目录 | 3 | core.go, engine.go, metrics.go | -| connect/ | 9 | strategy.go, stun.go, direct.go, fake_tcp.go, real_tcp.go, turn.go, turn_quic.go, ice.go, ws.go | -| transport/ | 3 | conn_manager.go, relay.go, plugin.go | -| plugins/wg/ | 1 | wgparse.go | -| **总计** | **16** | | - -**已删除的文件**: -- ~~grpc_service.go~~ - 不再需要(改为直接函数调用) -- ~~pool/connpool.go~~ - 不再需要(无连接池) -- ~~proto/core.proto~~ - 不再需要 gRPC \ No newline at end of file diff --git a/core/connect/direct.go b/core/connect/direct.go deleted file mode 100644 index b340f57..0000000 --- a/core/connect/direct.go +++ /dev/null @@ -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) -} diff --git a/core/connect/fake_tcp.go b/core/connect/fake_tcp.go deleted file mode 100644 index b78a018..0000000 --- a/core/connect/fake_tcp.go +++ /dev/null @@ -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 -} diff --git a/core/connect/ice.go b/core/connect/ice.go deleted file mode 100644 index b484b1a..0000000 --- a/core/connect/ice.go +++ /dev/null @@ -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 -} diff --git a/core/connect/real_tcp.go b/core/connect/real_tcp.go deleted file mode 100644 index 6c2299b..0000000 --- a/core/connect/real_tcp.go +++ /dev/null @@ -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 -} diff --git a/core/connect/strategy.go b/core/connect/strategy.go deleted file mode 100644 index 3693be8..0000000 --- a/core/connect/strategy.go +++ /dev/null @@ -1,754 +0,0 @@ -package connect - -import ( - "context" - "fmt" - "net" - "sync" - "time" - - "go.uber.org/zap" -) - -// Layer 传输层类型(9 层策略) -type Layer int - -const ( - // LayerDirectUDP Direct-UDP 直连(WireGuard over UDP)- 最高效 - LayerDirectUDP Layer = iota - - // LayerFakeTCP Direct-FakeTCP(UDP 封装 TCP 头部,欺骗防火墙) - LayerFakeTCP - - // LayerRealTCP Direct-RealTCP(P2P TCP 直连) - LayerRealTCP - - // LayerTURNUDP TURN-UDP 中继(标准 RFC 5766) - LayerTURNUDP - - // LayerTURNQUIC TURN-QUIC 中继(私有扩展,RFC 9000) - LayerTURNQUIC - - // LayerTURNTCP TURN-TCP 中继(TCP 中继) - LayerTURNTCP - - // LayerTURNTLS TURN-TLS 中继(TLS 加密,RFC 8656) - LayerTURNTLS - - // LayerWebRTC WebRTC DataChannel(DTLS 加密) - LayerWebRTC - - // LayerWS WS/WSS 兜底(仅 80/443 端口,终极兜底) - LayerWS - - // LayerCount 传输层总数 - LayerCount -) - -// String 实现 Stringer 接口 -func (l Layer) String() string { - switch l { - case LayerDirectUDP: - return "Direct-UDP" - case LayerFakeTCP: - return "Direct-FakeTCP" - case LayerRealTCP: - return "Direct-RealTCP" - case LayerTURNUDP: - return "TURN-UDP" - case LayerTURNQUIC: - return "TURN-QUIC" - case LayerTURNTCP: - return "TURN-TCP" - case LayerTURNTLS: - return "TURN-TLS" - case LayerWebRTC: - return "WebRTC" - case LayerWS: - return "WS/WSS" - default: - return "Unknown" - } -} - -// DefaultLayerOrder 默认优先级顺序(从最优到兜底) -// 根据 MeshRay_项目文档 v2.0.1 第 132-153 行定义 -var DefaultLayerOrder = []Layer{ - LayerDirectUDP, // 1. Direct-UDP - 公网/锥型 NAT,首选链路 - LayerFakeTCP, // 2. Direct-FakeTCP - 校园网、酒店 Wi-Fi、UDP 被 QoS 限速 - LayerRealTCP, // 3. Direct-RealTCP - 完全禁用 UDP,仅允许 TCP 出站 - LayerTURNUDP, // 4. TURN-UDP 中继 - 无 P2P 直连,但 UDP 可通 - LayerTURNQUIC, // 5. TURN-QUIC 中继 - UDP 可通但弱网(4G/5G、高丢包)【私有扩展】 - LayerTURNTCP, // 6. TURN-TCP 中继 - UDP 封禁,仅放行 TCP - LayerTURNTLS, // 7. TURN-TLS 中继 - 企业防火墙 DPI,仅放行 HTTPS - LayerWebRTC, // 8. WebRTC 终极兜底 - 最严格隔离内网、代理环境 - LayerWS, // 9. WS/WSS 兜底 - 仅放行 80/443 端口,且封锁 TURN -} - -// TransportFactory 传输工厂接口 - 每种传输方式必须实现 -type TransportFactory interface { - // Layer 返回传输层类型 - Layer() Layer - - // Dial 建立连接到对端 - // 返回标准的 net.Conn 接口 - Dial(ctx context.Context, config *DialConfig) (net.Conn, error) - - // Name 返回传输方式名称(用于日志) - Name() string -} - -// DialConfig 拨号配置 -type DialConfig struct { - // PeerID 对端标识 - PeerID string - - // PeerPublicKey 对端公钥 - PeerPublicKey string - - // STUNServers STUN 服务器列表(用于 P2P) - STUNServers []string - - // TURNServers TURN 服务器列表 - TURNServers []string - - // WSServers WebSocket 服务器列表 - WSServers []string - - // SignalingServers WebRTC 第三方信令服务器列表 - SignalingServers []string - - // ICESServers ICE 服务器列表(STUN+TURN 的组合) - ICESServers []string - - // Timeout 连接超时 - Timeout time.Duration - - // Logger 日志记录器 - Logger *zap.Logger -} - -// StrategyScheduler 9 层策略调度器(主动调度层) -// 职责: -// 1. 按优先级选择链路(P2P → Mesh中继 → TURN-UDP → ... → WS/WSS) -// 2. 根据网络环境自动切换(500ms 超时 / 10s 丢包率 > 10%) -// 3. 切换后探测恢复并自动切回高性能链路(30s) -type StrategyScheduler struct { - layerFactories map[Layer]TransportFactory // 各层的工厂 - layerOrder []Layer // 优先级顺序 - logger *zap.Logger - - // 每个 Peer 的降级控制器 - fallbackControllers map[string]*FallbackController // peerID -> controller - fallbackMu sync.RWMutex - - // 当前活跃连接 - activeConnections map[string]activeConn // peerID -> 连接信息 - connMu sync.RWMutex - - // 统计 - stats *SchedulerStats - - // 连接变更回调(通知上层 ConnManager) - OnConnectionUpdate func(peerID string, conn net.Conn, err error) -} - -// activeConn 活跃连接信息 -type activeConn struct { - conn net.Conn - layer Layer - peerID string - established time.Time - config *DialConfig -} - -// SchedulerStats 调度器统计 -type SchedulerStats struct { - mu sync.RWMutex - totalDials int64 - successDials int64 - fallbackCount int64 - recoveryCount int64 - layerDialCount map[Layer]int64 - layerFailCount map[Layer]int64 -} - -// NewStrategyScheduler 创建策略调度器 -func NewStrategyScheduler(logger *zap.Logger) *StrategyScheduler { - return &StrategyScheduler{ - layerFactories: make(map[Layer]TransportFactory), - layerOrder: DefaultLayerOrder, - logger: logger, - fallbackControllers: make(map[string]*FallbackController), - activeConnections: make(map[string]activeConn), - stats: &SchedulerStats{ - layerDialCount: make(map[Layer]int64), - layerFailCount: make(map[Layer]int64), - }, - } -} - -// RegisterFactory 注册传输工厂 -func (s *StrategyScheduler) RegisterFactory(factory TransportFactory) { - layer := factory.Layer() - s.layerFactories[layer] = factory - s.logger.Debug("注册传输工厂", - zap.String("layer", layer.String()), - zap.String("name", factory.Name())) -} - -// SetLayerOrder 设置优先级顺序 -func (s *StrategyScheduler) SetLayerOrder(order []Layer) { - if len(order) == 0 { - s.logger.Warn("空的层级顺序,使用默认顺序") - return - } - s.layerOrder = order - s.logger.Info("更新传输层优先级顺序", zap.Any("order", order)) -} - -// Dial 按优先级顺序尝试建立连接 -// 这是核心方法,实现了 9 层策略调度 -func (s *StrategyScheduler) Dial(config *DialConfig) (net.Conn, error) { - ctx := context.Background() - if config.Timeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, config.Timeout) - defer cancel() - } - - s.logger.Info("开始 8 层策略调度连接", - zap.String("peer_id", config.PeerID), - zap.Int("total_layers", len(s.layerOrder))) - - // 统计 - s.stats.mu.Lock() - s.stats.totalDials++ - s.stats.mu.Unlock() - - var lastErr error - for i, layer := range s.layerOrder { - factory, ok := s.layerFactories[layer] - if !ok { - s.logger.Debug("该传输层未注册,跳过", - zap.String("layer", layer.String())) - continue - } - - s.logger.Debug("尝试第 N 层传输", - zap.Int("index", i), - zap.String("layer", layer.String()), - zap.String("name", factory.Name())) - - // 统计该层拨号次数 - s.stats.mu.Lock() - s.stats.layerDialCount[layer]++ - s.stats.mu.Unlock() - - startTime := time.Now() - conn, err := factory.Dial(ctx, config) - duration := time.Since(startTime) - - if err == nil { - // 成功! - s.stats.mu.Lock() - s.stats.successDials++ - s.stats.mu.Unlock() - - // 记录活跃连接 - s.connMu.Lock() - s.activeConnections[config.PeerID] = activeConn{ - conn: conn, - layer: layer, - peerID: config.PeerID, - established: time.Now(), - config: config, - } - s.connMu.Unlock() - - // 创建或更新降级控制器 - s.ensureFallbackController(config.PeerID, layer) - - s.logger.Info("连接建立成功", - zap.String("layer", layer.String()), - zap.String("name", factory.Name()), - zap.String("peer_id", config.PeerID), - zap.String("remote_addr", conn.RemoteAddr().String()), - zap.Duration("duration", duration)) - - // 包装连接,用于监控 - return newMonitoredConn(conn, config.PeerID, layer, s), nil - } - - // 失败,统计 - s.stats.mu.Lock() - s.stats.layerFailCount[layer]++ - s.stats.mu.Unlock() - - // 记录失败并继续尝试下一层 - lastErr = err - s.logger.Warn("该传输层连接失败,尝试下一层", - zap.String("layer", layer.String()), - zap.Duration("duration", duration), - zap.Error(err)) - } - - // 所有层都失败 - return nil, fmt.Errorf("所有传输层均失败,最后错误:%w", lastErr) -} - -// reconnectToLayer 触发重连到指定层级 -func (s *StrategyScheduler) reconnectToLayer(peerID string, toLayer Layer) { - s.connMu.RLock() - ac, exists := s.activeConnections[peerID] - s.connMu.RUnlock() - - if !exists || ac.config == nil { - s.logger.Warn("重连失败:找不到活跃连接配置", zap.String("peer_id", peerID)) - return - } - - factory, ok := s.layerFactories[toLayer] - if !ok { - s.logger.Error("重连失败:找不到目标层级工厂", zap.String("layer", toLayer.String())) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - conn, err := factory.Dial(ctx, ac.config) - if err != nil { - s.logger.Error("降级重连失败", zap.Error(err)) - if s.OnConnectionUpdate != nil { - s.OnConnectionUpdate(peerID, nil, err) - } - return - } - - wrappedConn := newMonitoredConn(conn, peerID, toLayer, s) - - s.connMu.Lock() - if oldAc, exists := s.activeConnections[peerID]; exists { - oldAc.conn.Close() - } - s.activeConnections[peerID] = activeConn{ - conn: wrappedConn, - layer: toLayer, - peerID: peerID, - established: time.Now(), - config: ac.config, - } - s.connMu.Unlock() - - if s.OnConnectionUpdate != nil { - s.OnConnectionUpdate(peerID, wrappedConn, nil) - } -} - -// ensureFallbackController 确保对端有降级控制器 -func (s *StrategyScheduler) ensureFallbackController(peerID string, initialLayer Layer) { - s.fallbackMu.Lock() - defer s.fallbackMu.Unlock() - - if _, exists := s.fallbackControllers[peerID]; !exists { - controller := NewFallbackController( - initialLayer, - func(from, to Layer) { - // 降级回调 - s.stats.mu.Lock() - s.stats.fallbackCount++ - s.stats.mu.Unlock() - - s.logger.Warn("链路降级", - zap.String("peer_id", peerID), - zap.String("from_layer", from.String()), - zap.String("to_layer", to.String())) - - // 触发重连到新层级 - go s.reconnectToLayer(peerID, to) - }, - func(to Layer) { - // 恢复回调 - s.stats.mu.Lock() - s.stats.recoveryCount++ - s.stats.mu.Unlock() - - s.logger.Info("链路恢复", - zap.String("peer_id", peerID), - zap.String("to_layer", to.String())) - - // 回调处理已经在 probeHighLayers 中完成并传递了新连接 - }, - s.logger, - s, // pass scheduler to access activeConnections - ) - s.fallbackControllers[peerID] = controller - } -} - -// RecordLatency 记录延迟(供 MonitoredConn 调用) -func (s *StrategyScheduler) RecordLatency(peerID string, success bool, duration time.Duration) { - s.fallbackMu.RLock() - controller, exists := s.fallbackControllers[peerID] - s.fallbackMu.RUnlock() - - if exists { - controller.CheckAndFallback(success, duration) - } -} - -// GetActiveLayer 获取当前活跃的传输层(用于监控) -func (s *StrategyScheduler) GetActiveLayer() Layer { - // 返回第一个活跃连接的层级 - s.connMu.RLock() - defer s.connMu.RUnlock() - - for _, ac := range s.activeConnections { - return ac.layer - } - return LayerDirectUDP // 默认值 -} - -// GetAllActiveLayers 获取所有 Peer 的活跃层级(用于全局监控) -func (s *StrategyScheduler) GetAllActiveLayers() map[string]Layer { - s.connMu.RLock() - defer s.connMu.RUnlock() - - result := make(map[string]Layer) - for peerID, ac := range s.activeConnections { - result[peerID] = ac.layer - } - return result -} - -// GetPeerLayer 获取指定 Peer 的当前层级 -func (s *StrategyScheduler) GetPeerLayer(peerID string) Layer { - s.connMu.RLock() - defer s.connMu.RUnlock() - - if ac, exists := s.activeConnections[peerID]; exists { - return ac.layer - } - return LayerDirectUDP // 默认值 -} - -// GetStats 获取统计信息 -func (s *StrategyScheduler) GetStats() map[string]interface{} { - s.stats.mu.RLock() - defer s.stats.mu.RUnlock() - - layerStats := make(map[string]int64) - for layer, count := range s.stats.layerDialCount { - layerStats[layer.String()+"_dial"] = count - } - for layer, count := range s.stats.layerFailCount { - layerStats[layer.String()+"_fail"] = count - } - - return map[string]interface{}{ - "total_dials": s.stats.totalDials, - "success_dials": s.stats.successDials, - "fallback_count": s.stats.fallbackCount, - "recovery_count": s.stats.recoveryCount, - "layer_stats": layerStats, - } -} - -// ClosePeer 关闭指定 Peer 的连接和控制器 -func (s *StrategyScheduler) ClosePeer(peerID string) { - // 关闭连接 - s.connMu.Lock() - if ac, exists := s.activeConnections[peerID]; exists { - ac.conn.Close() - delete(s.activeConnections, peerID) - } - s.connMu.Unlock() - - // 移除降级控制器 - s.fallbackMu.Lock() - if controller, exists := s.fallbackControllers[peerID]; exists { - // 停止恢复探测器 - if controller.recoveryTimer != nil { - controller.recoveryTimer.Stop() - } - delete(s.fallbackControllers, peerID) - } - s.fallbackMu.Unlock() - - s.logger.Debug("已关闭 Peer 连接和控制器", - zap.String("peer_id", peerID)) -} - -// MonitoredConn 带监控的连接包装器 -type MonitoredConn struct { - net.Conn - peerID string - layer Layer - scheduler *StrategyScheduler -} - -// newMonitoredConn 创建带监控的连接 -func newMonitoredConn(conn net.Conn, peerID string, layer Layer, scheduler *StrategyScheduler) *MonitoredConn { - return &MonitoredConn{ - Conn: conn, - peerID: peerID, - layer: layer, - scheduler: scheduler, - } -} - -// Read 重写 Read 方法,记录延迟 -func (c *MonitoredConn) Read(b []byte) (n int, err error) { - start := time.Now() - n, err = c.Conn.Read(b) - duration := time.Since(start) - - // 记录成功/失败 - c.scheduler.RecordLatency(c.peerID, err == nil, duration) - - return n, err -} - -// Write 重写 Write 方法,记录延迟 -func (c *MonitoredConn) Write(b []byte) (n int, err error) { - start := time.Now() - n, err = c.Conn.Write(b) - duration := time.Since(start) - - // 记录成功/失败 - c.scheduler.RecordLatency(c.peerID, err == nil, duration) - - return n, err -} - -// FallbackController 降级控制器 -type FallbackController struct { - currentLayer Layer // 当前使用的层 - windowStart time.Time // 滑动窗口起始时间 - packetCount int // 总包数 - lostPacketCount int // 丢包数 - mu chan struct{} // 互斥锁(用 channel 实现) - triggerFallback func(Layer, Layer) // 降级触发回调 - triggerRecovery func(Layer) // 恢复触发回调 - logger *zap.Logger - recoveryTimer *time.Timer // 恢复探测定时器 - scheduler *StrategyScheduler -} - -const ( - // TimeoutThreshold 单次超时阈值 - TimeoutThreshold = 500 * time.Millisecond - - // PacketLossThreshold 丢包率阈值 - PacketLossThreshold = 0.10 // 10% - - // RecoveryInterval 恢复探测间隔 - RecoveryInterval = 30 * time.Second - - // SlidingWindowDuration 滑动窗口时长 - SlidingWindowDuration = 10 * time.Second -) - -// NewFallbackController 创建降级控制器 -func NewFallbackController( - initialLayer Layer, - onFallback func(Layer, Layer), - onRecovery func(Layer), - logger *zap.Logger, - scheduler *StrategyScheduler, -) *FallbackController { - fc := &FallbackController{ - currentLayer: initialLayer, - mu: make(chan struct{}, 1), - triggerFallback: onFallback, - triggerRecovery: onRecovery, - logger: logger, - scheduler: scheduler, - } - - // 启动恢复探测 - fc.startRecoveryProbe() - - return fc -} - -// CheckAndFallback 检查是否需要降级 -// 在每次连接操作后调用 -func (fc *FallbackController) CheckAndFallback(success bool, duration time.Duration) { - select { - case fc.mu <- struct{}{}: - defer func() { <-fc.mu }() - default: - // 锁被占用,说明正在处理,直接返回 - return - } - - // 重置滑动窗口 - if time.Since(fc.windowStart) > SlidingWindowDuration { - fc.windowStart = time.Now() - fc.packetCount = 0 - fc.lostPacketCount = 0 - } - - // 统计 - fc.packetCount++ - if !success || duration > TimeoutThreshold { - fc.lostPacketCount++ - } - - // 检查是否达到阈值 - if fc.packetCount >= 10 { - lossRate := float64(fc.lostPacketCount) / float64(fc.packetCount) - if lossRate > PacketLossThreshold { - fc.triggerFallbackLocked() - } - } -} - -// triggerFallbackLocked 执行降级(已持有锁) -func (fc *FallbackController) triggerFallbackLocked() { - currentIndex := int(fc.currentLayer) - if currentIndex >= int(LayerCount)-1 { - // 已经是最低优先级,无法降级 - fc.logger.Warn("已是最底层级,无法降级", - zap.String("current_layer", fc.currentLayer.String())) - return - } - - nextLayer := Layer(currentIndex + 1) - - // 在更新 currentLayer 之前保存旧值用于回调 - oldLayer := fc.currentLayer - - fc.logger.Warn("触发降级", - zap.String("from_layer", oldLayer.String()), - zap.String("to_layer", nextLayer.String())) - - fc.currentLayer = nextLayer - fc.resetWindow() - - if fc.triggerFallback != nil { - fc.triggerFallback(oldLayer, nextLayer) - } - - // 重置恢复定时器 - fc.startRecoveryProbe() -} - -// startRecoveryProbe 启动恢复探测 -func (fc *FallbackController) startRecoveryProbe() { - if fc.recoveryTimer != nil { - fc.recoveryTimer.Stop() - } - - fc.recoveryTimer = time.AfterFunc(RecoveryInterval, func() { - fc.probeHigherLayers() - }) -} - -// probeHigherLayers 探测更高层级 -func (fc *FallbackController) probeHigherLayers() { - select { - case fc.mu <- struct{}{}: - defer func() { <-fc.mu }() - default: - return - } - - currentIndex := int(fc.currentLayer) - if currentIndex == 0 { - // 已经是最高优先级,无需探测 - return - } - - // 尝试上一层 - higherLayer := Layer(currentIndex - 1) - fc.logger.Info("探测更高层级", - zap.String("current_layer", fc.currentLayer.String()), - zap.String("probe_layer", higherLayer.String())) - - // 获取 PeerID 及 Config - fc.scheduler.connMu.RLock() - var peerID string - var config *DialConfig - for pid, ac := range fc.scheduler.activeConnections { - if ac.layer == fc.currentLayer { - peerID = pid - config = ac.config - break - } - } - fc.scheduler.connMu.RUnlock() - - if config == nil { - fc.logger.Warn("探测更高层级失败:找不到有效 DialConfig") - return - } - - factory, ok := fc.scheduler.layerFactories[higherLayer] - if !ok { - fc.logger.Debug("更高层级未注册工厂,跳过探测") - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - conn, err := factory.Dial(ctx, config) - if err == nil { - fc.logger.Info("更高层级探测成功,准备切换") - - wrappedConn := newMonitoredConn(conn, peerID, higherLayer, fc.scheduler) - - fc.scheduler.connMu.Lock() - if ac, exists := fc.scheduler.activeConnections[peerID]; exists { - ac.conn.Close() // Close old - ac.conn = wrappedConn - ac.layer = higherLayer - fc.scheduler.activeConnections[peerID] = ac - } - fc.scheduler.connMu.Unlock() - - if fc.scheduler.OnConnectionUpdate != nil { - fc.scheduler.OnConnectionUpdate(peerID, wrappedConn, nil) - } - - // 触发恢复回调 - fc.triggerRecoveryLocked(higherLayer) - } else { - fc.logger.Debug("更高层级探测失败", zap.Error(err)) - } -} - -// triggerRecoveryLocked 执行恢复(已持有锁) -func (fc *FallbackController) triggerRecoveryLocked(higherLayer Layer) { - fc.logger.Info("触发恢复", - zap.String("from_layer", fc.currentLayer.String()), - zap.String("to_layer", higherLayer.String())) - - fc.currentLayer = higherLayer - fc.resetWindow() - - if fc.triggerRecovery != nil { - fc.triggerRecovery(higherLayer) - } -} - -// resetWindow 重置滑动窗口 -func (fc *FallbackController) resetWindow() { - fc.windowStart = time.Now() - fc.packetCount = 0 - fc.lostPacketCount = 0 -} - -// GetCurrentLayer 获取当前层级 -func (fc *FallbackController) GetCurrentLayer() Layer { - select { - case fc.mu <- struct{}{}: - defer func() { <-fc.mu }() - default: - return fc.currentLayer - } - return fc.currentLayer -} diff --git a/core/connect/stun.go b/core/connect/stun.go deleted file mode 100644 index 63f7519..0000000 --- a/core/connect/stun.go +++ /dev/null @@ -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 服务器均查询失败") -} diff --git a/core/connect/turn.go b/core/connect/turn.go deleted file mode 100644 index 41881bf..0000000 --- a/core/connect/turn.go +++ /dev/null @@ -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) -} diff --git a/core/connect/turn_quic.go b/core/connect/turn_quic.go deleted file mode 100644 index c0af09b..0000000 --- a/core/connect/turn_quic.go +++ /dev/null @@ -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 支持时再完善 -} diff --git a/core/connect/ws.go b/core/connect/ws.go deleted file mode 100644 index ee0128a..0000000 --- a/core/connect/ws.go +++ /dev/null @@ -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 -} diff --git a/core/core.go b/core/core.go deleted file mode 100644 index f99a2a7..0000000 --- a/core/core.go +++ /dev/null @@ -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 -} diff --git a/core/engine.go b/core/engine.go deleted file mode 100644 index b8a1e74..0000000 --- a/core/engine.go +++ /dev/null @@ -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 -} diff --git a/core/metrics.go b/core/metrics.go deleted file mode 100644 index e67fb62..0000000 --- a/core/metrics.go +++ /dev/null @@ -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) -} diff --git a/core/plugins/wg/wgparse.go b/core/plugins/wg/wgparse.go deleted file mode 100644 index 265fc94..0000000 --- a/core/plugins/wg/wgparse.go +++ /dev/null @@ -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 -} diff --git a/core/transport/conn_manager.go b/core/transport/conn_manager.go deleted file mode 100644 index a9f8e2e..0000000 --- a/core/transport/conn_manager.go +++ /dev/null @@ -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() -} diff --git a/core/transport/plugin.go b/core/transport/plugin.go deleted file mode 100644 index 407c2a3..0000000 --- a/core/transport/plugin.go +++ /dev/null @@ -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) -} diff --git a/core/transport/relay.go b/core/transport/relay.go deleted file mode 100644 index b75caa4..0000000 --- a/core/transport/relay.go +++ /dev/null @@ -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)) - } -} diff --git a/go.mod b/go.mod index 0bad2af..85423a0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ceb8cf4..b1c038b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/api/server.go b/internal/api/server.go index 99fac35..e2906c8 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -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" } diff --git a/internal/ctr/core_enable.go b/internal/ctr/core_enable.go new file mode 100644 index 0000000..ccb8c6f --- /dev/null +++ b/internal/ctr/core_enable.go @@ -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" diff --git a/internal/ctr/corebind/adapter.go b/internal/ctr/corebind/adapter.go new file mode 100644 index 0000000..39a16d8 --- /dev/null +++ b/internal/ctr/corebind/adapter.go @@ -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, + } +} diff --git a/internal/ctr/corebind/bind.go b/internal/ctr/corebind/bind.go new file mode 100644 index 0000000..b4dd286 --- /dev/null +++ b/internal/ctr/corebind/bind.go @@ -0,0 +1,309 @@ +// Package corebind 是 Meshray-Manager 侧的 wireguard-go 集成层(形态 B:增强传输)。 +// +// 它实现 wireguard-go 的 conn.Bind 接口(EnhancedBind),完全接管 WG 的 UDP 收发, +// 把密文流量导向 meshray 核心(git.zkcoi.com/zkcoi/meshray)的 9 层传输。 +// 该层属于「管理器 / UI」范畴,而非核心;核心本身零 wireguard 依赖。 +package corebind + +import ( + "net" + "sync" + + "git.zkcoi.com/zkcoi/meshray/core/engine" + "golang.zx2c4.com/wireguard/conn" + "go.uber.org/zap" +) + +// EnhancedBind 是 corebind 提供的 conn.Bind 实现,完全接管 WireGuard 的 UDP 收发, +// 使所有 WG 密文流量都经由 meshray 核心的 9 层传输(Direct/TURN/WebRTC/...)而非裸 UDP。 +// +// 设计要点: +// - Direct-UDP 由一个绑定在 listen_port 的*共享未连接 UDP socket*(directSock)承载, +// 从任意来源收包,把「真实源地址」作为 Endpoint 交给 WG,WG 用密文内的 receiver index +// 匹配 peer 并据此更新对端出向地址(学址)。由此对称直连与 NAT 穿透学址原生可通。 +// - TURN/WebRTC/WS 等中继层经核心传输连接(connMgr)承载,Direct 不可用时补充。 +// - 集成通过核心暴露的钩子接入:SetPeerRegistrar / SetRelayInboundHandler / +// SetDirectByBind,核心永不反向依赖本包。 +type EnhancedBind struct { + inner conn.Bind + networkID string + engine *core.Engine + logger *zap.Logger + + // listenPort 本端 WG 监听端口;Direct-UDP 共享监听 socket 绑定于此,形成对称直连。 + listenPort int + + mu sync.RWMutex + peerByAddr map[string]string // endpoint addr(str) -> peerKey(Send 反查中继连接) + peerEndpoint map[string]conn.Endpoint // peerKey -> 对端真实 Endpoint(中继入站回灌用) + + // directSock 承载 Direct-UDP 的共享未连接 UDP socket(绑定 listenPort)。 + // 从任意来源收包(学习对端经 NAT 后的真实源地址),并按目标地址发包。 + directSock *net.UDPConn + + inboundCh chan inboundPacket + closed bool +} + +// inboundPacket 入站密文;携带来源信息以便回灌 WG 时定位对端 Endpoint。 +type inboundPacket struct { + peerKey string // 中继入站时按 peerKey 查 Endpoint + ep conn.Endpoint // Direct 共享 socket 学得的真实源地址;非空时优先使用 + data []byte +} + +// NewEnhancedBind 创建增强 Bind,并关联指定组网的 Engine 单例; +// 同时经核心钩子把入站回灌接到本 Bind,使 9 层传输连接读到的包能回到 WG。 +func NewEnhancedBind(listenPort int, networkID string, logger *zap.Logger) (*EnhancedBind, error) { + eng := getOrCreateEngine(networkID, logger) + inner := conn.NewDefaultBind() + b := &EnhancedBind{ + inner: inner, + networkID: networkID, + engine: eng, + logger: logger, + listenPort: listenPort, + peerByAddr: make(map[string]string), + peerEndpoint: make(map[string]conn.Endpoint), + inboundCh: make(chan inboundPacket, 1024), + } + // 通过核心钩子接入,避免核心反向依赖本包: + eng.SetPeerRegistrar(b) // 收到对端候选时注册 endpoint→peerKey + eng.SetRelayInboundHandler(b.deliverInbound) // 传输连接入站回灌 WG + eng.SetDirectByBind(true) // Direct 由本 Bind 共享 socket 承载 + eng.SetListenPort(listenPort) + return b, nil +} + +// Engine 返回关联的引擎实例(standalone 二进制用于注入候选触发拨号) +func (b *EnhancedBind) Engine() *core.Engine { return b.engine } + +// RegisterPeer 注册 endpoint 地址到 peerKey 的映射,并缓存对端真实 Endpoint 供回灌使用。 +// 由 Engine.NotifyPeerInfo 在收到候选时自动调用;standalone 也可直接调用。 +func (b *EnhancedBind) RegisterPeer(addr string, peerKey string) { + ep, err := b.inner.ParseEndpoint(addr) + if err != nil { + b.logger.Warn("解析对端 Endpoint 失败,回灌将使用 0.0.0.0:0", + zap.String("addr", addr), zap.Error(err)) + ep = nil + } + b.mu.Lock() + b.peerByAddr[addr] = peerKey + if ep != nil { + b.peerEndpoint[peerKey] = ep + } + b.mu.Unlock() +} + +// Open 创建 Direct-UDP 共享监听 socket(绑定 listen_port),并启动接收协程。 +// 返回从 inboundCh 读取入站密文的接收函数:Direct 入站与中继入站统一经此回灌 WG。 +func (b *EnhancedBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) { + b.mu.Lock() + b.closed = false + // 端口优先用 WG 传入的 port,其次用构造时的 listenPort;0 表示由 OS 分配。 + bindPort := int(port) + if bindPort == 0 { + bindPort = b.listenPort + } + // 关闭可能残留的旧 socket(重开场景) + if b.directSock != nil { + b.directSock.Close() + b.directSock = nil + } + b.mu.Unlock() + + sock, err := net.ListenUDP("udp", &net.UDPAddr{Port: bindPort}) + if err != nil { + return nil, 0, err + } + + actual := sock.LocalAddr().(*net.UDPAddr).Port + + b.mu.Lock() + b.directSock = sock + b.listenPort = actual + b.mu.Unlock() + + // 回填引擎实际监听端口(listen_port=0 自动分配时同步) + if b.engine != nil { + b.engine.SetListenPort(actual) + } + + go b.readDirectLoop(sock) + + b.logger.Info("Direct-UDP 共享监听 socket 已就绪", + zap.Int("listen_port", actual)) + + // v4/v6 各一个接收函数,均从同一个 inboundCh 读取 + return []conn.ReceiveFunc{b.receiveFunc, b.receiveFunc}, uint16(actual), nil +} + +// readDirectLoop 从共享监听 socket 收包(任意来源),把「真实源地址」作为 Endpoint 投入 inboundCh。 +// 这是 NAT 穿透学址的核心:公网侧据此学到对端 NAT 映射后的真实地址,回包得以正确返回。 +func (b *EnhancedBind) readDirectLoop(sock *net.UDPConn) { + buf := make([]byte, 65535) + for { + n, src, err := sock.ReadFromUDP(buf) + if err != nil { + b.mu.RLock() + closed := b.closed + b.mu.RUnlock() + if closed { + return + } + b.logger.Debug("Direct 共享 socket 读取失败,停止接收", zap.Error(err)) + return + } + data := make([]byte, n) + copy(data, buf[:n]) + + ep, perr := b.inner.ParseEndpoint(src.String()) + if perr != nil { + b.logger.Debug("解析入站源地址失败,丢弃", zap.String("src", src.String()), zap.Error(perr)) + continue + } + select { + case b.inboundCh <- inboundPacket{ep: ep, data: data}: + default: + b.logger.Warn("入站队列满,丢弃包", zap.String("src", src.String())) + } + } +} + +// receiveFunc 从 inboundCh 取出入站密文,拷贝进 WG 提供的缓冲(保持 WG 缓冲所有权), +// 并填入来源 Endpoint 供 WG 握手更新出向地址: +// - Direct 入站:ep 为共享 socket 学得的真实源地址(优先); +// - 中继入站:ep 为空,按 peerKey 查缓存的对端 Endpoint。 +func (b *EnhancedBind) receiveFunc(packets [][]byte, sizes []int, eps []conn.Endpoint) (int, error) { + first, ok := <-b.inboundCh + if !ok { + return 0, net.ErrClosed + } + n := 0 + copy(packets[n], first.data) + sizes[n] = len(first.data) + eps[n] = b.endpointForPacket(first) + n++ + for n < len(packets) { + select { + case pkt := <-b.inboundCh: + copy(packets[n], pkt.data) + sizes[n] = len(pkt.data) + eps[n] = b.endpointForPacket(pkt) + n++ + default: + return n, nil + } + } + return n, nil +} + +// endpointForPacket 返回入站包的来源 Endpoint:Direct 入站直接用其学得的 ep; +// 中继入站按 peerKey 查缓存,未知时退化为 0.0.0.0:0。 +func (b *EnhancedBind) endpointForPacket(pkt inboundPacket) conn.Endpoint { + if pkt.ep != nil { + return pkt.ep + } + b.mu.RLock() + defer b.mu.RUnlock() + if ep, ok := b.peerEndpoint[pkt.peerKey]; ok { + return ep + } + ep, _ := b.inner.ParseEndpoint("0.0.0.0:0") + return ep +} + +// Send 出站路径: +// - 若该对端已建立中继/信令层连接(TURN/WebRTC/WS,位于 connMgr),经其发送; +// - 否则(Direct-UDP)经共享监听 socket 直接发往目标地址(WG 维护的对端 Endpoint: +// 初始为配置 endpoint,学址后为对端真实地址)。 +func (b *EnhancedBind) Send(bufs [][]byte, ep conn.Endpoint) error { + addr := ep.DstToString() + + b.mu.RLock() + peerKey, mapped := b.peerByAddr[addr] + b.mu.RUnlock() + + if mapped { + if c, ok := b.engine.GetConnMgr().Get(peerKey); ok && c != nil { + for _, buf := range bufs { + if len(buf) == 0 { + continue + } + if _, err := c.Write(buf); err != nil { + b.logger.Debug("经中继连接发送失败,转 Direct 共享 socket", + zap.String("peer", peerKey), zap.Error(err)) + return b.directSendTo(addr, bufs) + } + } + return nil + } + } + // Direct-UDP:共享监听 socket 直发(NAT 穿透原生支持) + return b.directSendTo(addr, bufs) +} + +// directSendTo 经共享监听 socket 把数据发往目标地址。源端口固定为 listen_port, +// 满足对称直连;NAT 后主动侧发包后,NAT 记录会话,公网侧回包得以返回。 +func (b *EnhancedBind) directSendTo(addr string, bufs [][]byte) error { + b.mu.RLock() + sock := b.directSock + b.mu.RUnlock() + if sock == nil { + return net.ErrClosed + } + + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return err + } + for _, buf := range bufs { + if len(buf) == 0 { + continue + } + if _, err := sock.WriteToUDP(buf, udpAddr); err != nil { + return err + } + } + return nil +} + +// deliverInbound 由引擎中继传输连接读取到入站时调用,把密文投入 inboundCh 回灌 WG。 +// 中继入站不携带来源地址,按 peerKey 在回灌时查对端 Endpoint。 +func (b *EnhancedBind) deliverInbound(peerKey string, packet []byte) { + cp := make([]byte, len(packet)) + copy(cp, packet) + select { + case b.inboundCh <- inboundPacket{peerKey: peerKey, data: cp}: + default: + b.logger.Warn("入站队列满,丢弃包", zap.String("peer", peerKey)) + } +} + +// Close 关闭共享监听 socket 与入站通道(使接收函数返回 net.ErrClosed)。 +func (b *EnhancedBind) Close() error { + b.mu.Lock() + if !b.closed { + b.closed = true + close(b.inboundCh) + } + if b.directSock != nil { + b.directSock.Close() + b.directSock = nil + } + b.mu.Unlock() + return b.inner.Close() +} + +// SetMark 本实现不涉及内核 socket,置空标记为 no-op +func (b *EnhancedBind) SetMark(mark uint32) error { return nil } + +// ParseEndpoint 委托默认 Bind 解析 +func (b *EnhancedBind) ParseEndpoint(s string) (conn.Endpoint, error) { + return b.inner.ParseEndpoint(s) +} + +// BatchSize 返回默认 Bind 的批量大小 +func (b *EnhancedBind) BatchSize() int { + return b.inner.BatchSize() +} diff --git a/internal/ctr/corebind/registry.go b/internal/ctr/corebind/registry.go new file mode 100644 index 0000000..bf42ce6 --- /dev/null +++ b/internal/ctr/corebind/registry.go @@ -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 + }) +} diff --git a/internal/ctr/ctr.go b/internal/ctr/ctr.go index ff2dc2b..821197a 100644 --- a/internal/ctr/ctr.go +++ b/internal/ctr/ctr.go @@ -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"` } diff --git a/internal/ctr/wg.go b/internal/ctr/wg.go index ce4b947..b224eff 100644 --- a/internal/ctr/wg.go +++ b/internal/ctr/wg.go @@ -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 默认 Bind;enhanced 用 meshray-core 注册的自定义 Bind +func (m *WGManager) startUserModeWGProcessWithRefs(deviceName string, privateKey wgtypes.Key, listenPort int, networkID string, meshMode string) (tun.Device, *device.Device, error) { m.logger.Info("使用用户态模式启动 WireGuard", 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{ diff --git a/internal/service/device.go b/internal/service/device.go index fdd5ae0..5873db9 100644 --- a/internal/service/device.go +++ b/internal/service/device.go @@ -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 } diff --git a/pkg/contract/contract.go b/pkg/contract/contract.go new file mode 100644 index 0000000..9547c57 --- /dev/null +++ b/pkg/contract/contract.go @@ -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 +} diff --git a/pkg/contract/go.mod b/pkg/contract/go.mod new file mode 100644 index 0000000..4b291c7 --- /dev/null +++ b/pkg/contract/go.mod @@ -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 +) diff --git a/pkg/contract/go.sum b/pkg/contract/go.sum new file mode 100644 index 0000000..ab60876 --- /dev/null +++ b/pkg/contract/go.sum @@ -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= diff --git a/start.bat b/start.bat index b41d08d..8f46a3b 100644 --- a/start.bat +++ b/start.bat @@ -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 diff --git a/web/static/index.html b/web/static/index.html index 394922e..20d16dd 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -261,16 +261,191 @@ - -
-

- {{ currentPage === 'devices' ? '设备管理' : - currentPage === 'services' ? '服务管理' : '系统设置' }} -

-
-

页面开发中...

+ +
+

设备管理

+
+ + + 创建设备 + + + + + + + + + + + + + +
+ 暂无设备,点击「创建设备」开始 +
+ + +
+

服务管理

+
+ + + 创建服务 + + + + + + + + + + + +
+ 暂无服务 +
+
+
+ + +
+

系统设置

+ + +
+

基础设置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + 保存设置 + + +
+
+ +
+

安全

+ + + + + + + + + + + + 修改密码 + + +
+
+

运行状态

+

WireGuard 模式:{{ wgModeDisplay || wgMode || '未知' }}

+ 重启核心服务 +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/static/js/app.js b/web/static/js/app.js index 4652cc5..3a8f756 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -12,130 +12,136 @@ const app = createApp({ setup() { // 当前页面 const currentPage = ref('dashboard'); - + // 登录状态 const isLoggedIn = ref(false); const loggingIn = ref(false); - + // 登录表单 const loginForm = reactive({ username: '', password: '' }); - + // Token 存储 let authToken = localStorage.getItem('meshray_token') || ''; - + // 检查是否已登录 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, onlineDevices: 0, pendingApprovals: 0 }); - - // 组网列表 + const networks = ref([]); - - // 对话框控制 const showCreateNetworkDialog = ref(false); - - // 新组网表单 const newNetwork = reactive({ name: '', subnet_ipv4: '10.0.0.0/24', listen_port: 51820, 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); ElementPlus.ElMessage.error('创建失败'); } }; - - // 查看组网详情 + 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 生成成功:

` + @@ -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); @@ -190,31 +183,21 @@ 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') { @@ -223,50 +206,366 @@ 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); @@ -275,31 +574,20 @@ const app = createApp({ loggingIn.value = false; } }; - - // 处理退出 + 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(() => {}); }; - + return { currentPage, isLoggedIn, @@ -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 }; } });