Initial commit
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
# MeshRay 全面问题排查与修复清单
|
||||
|
||||
## 🔍 排查范围
|
||||
|
||||
**排查时间**: 2026-03-20
|
||||
**排查重点**: 逻辑不完整、前后端不一致、功能缺失
|
||||
**排查方法**: Handler → Service → Model 全链路审查
|
||||
|
||||
---
|
||||
|
||||
## 📋 问题清单总览
|
||||
|
||||
| # | 问题类别 | 严重程度 | 状态 |
|
||||
|---|---------|---------|------|
|
||||
| 1 | PendingJoin 审核通过无后续动作 | 🔴 P0 | 待修复 |
|
||||
| 2 | DeviceService 生成配置缺少私钥 | 🔴 P0 | 待修复 |
|
||||
| 3 | Network 创建后未返回完整信息 | 🟡 P1 | 待确认 |
|
||||
| 4 | DDNS 同步缺少失败重试 | 🟡 P1 | 待修复 |
|
||||
| 5 | STUN/TURN 配置来源不明确 | 🟡 P1 | 待明确 |
|
||||
| 6 | WebSocket 断线重连机制 | 🟢 P2 | 优化 |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 P0 - 严重问题
|
||||
|
||||
### 问题 1: PendingJoin 审核通过逻辑不完整
|
||||
|
||||
**位置**: `internal/service/pending_join.go` - `ApproveJoin()`
|
||||
|
||||
**现状**:
|
||||
```go
|
||||
func (s *PendingJoinService) ApproveJoin(id uint) error {
|
||||
// 1. 查询记录
|
||||
var record model.PendingJoin
|
||||
s.store.DB().First(&record, id)
|
||||
|
||||
// 2. 更新状态
|
||||
record.Status = "approved"
|
||||
record.ApprovedAt = &now
|
||||
|
||||
// 3. 保存
|
||||
s.store.DB().Save(&record)
|
||||
|
||||
// ❌ 缺失:
|
||||
// - 没有创建设备
|
||||
// - 没有生成密钥对
|
||||
// - 没有分配 IP
|
||||
// - 没有返回配置
|
||||
}
|
||||
```
|
||||
|
||||
**影响**:
|
||||
- 管理员审核通过后,申请人无法获得连接配置
|
||||
- 组网断联情况下,新用户无法加入
|
||||
- 审核流程形同虚设
|
||||
|
||||
**修复方案**:
|
||||
```go
|
||||
type ApprovalResult struct {
|
||||
Device *model.Device
|
||||
PrivateKey string
|
||||
Network *model.Network
|
||||
ConfigText string // WireGuard 配置文本
|
||||
}
|
||||
|
||||
func (s *PendingJoinService) ApproveJoin(id uint) (*ApprovalResult, error) {
|
||||
// 1. 查询申请记录
|
||||
var record model.PendingJoin
|
||||
if err := s.store.DB().First(&record, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 查询 MeshSeed 获取网络信息
|
||||
var meshSeed model.MeshSeed
|
||||
s.store.DB().Where("seed_id = ?", record.SeedID).First(&meshSeed)
|
||||
|
||||
// 3. 查询网络详情
|
||||
var network model.Network
|
||||
s.store.DB().First(&network, meshSeed.NetworkID)
|
||||
|
||||
// 4. 生成设备密钥对
|
||||
privateKey, publicKey := generateKeyPair()
|
||||
|
||||
// 5. 分配 IP 地址
|
||||
ipAddress := s.allocateIP(network.SubnetIPv4)
|
||||
|
||||
// 6. 创建设备记录
|
||||
device := &model.Device{
|
||||
Name: record.DeviceName,
|
||||
NetworkID: network.ID,
|
||||
PublicKey: publicKey,
|
||||
IPAddress: ipAddress,
|
||||
Status: "active",
|
||||
}
|
||||
s.store.DB().Create(device)
|
||||
|
||||
// 7. 更新审核状态
|
||||
record.Status = "approved"
|
||||
record.ApprovedAt = &now
|
||||
s.store.DB().Save(&record)
|
||||
|
||||
// 8. 生成配置文本
|
||||
configText := generateWireGuardConfig(device, network, privateKey)
|
||||
|
||||
return &ApprovalResult{
|
||||
Device: device,
|
||||
PrivateKey: privateKey,
|
||||
Network: &network,
|
||||
ConfigText: configText,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**修复优先级**: **最高(阻塞性功能)**
|
||||
|
||||
---
|
||||
|
||||
### 问题 2: DeviceService 生成配置缺少私钥
|
||||
|
||||
**位置**: `internal/service/device.go` - `GenerateDeviceConfig()`
|
||||
|
||||
**现状**:
|
||||
```go
|
||||
func (s *DeviceService) GenerateDeviceConfig(deviceID uint64) (string, error) {
|
||||
// 1. 获取设备信息
|
||||
device := s.GetDevice(deviceID)
|
||||
|
||||
// 2. 获取网络信息
|
||||
network := device.Network
|
||||
|
||||
// 3. 生成配置
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[Interface]\n")
|
||||
sb.WriteString("PrivateKey = <❌ 从哪里获取?>\n") // ❌ 问题
|
||||
sb.WriteString(fmt.Sprintf("Address = %s\n", device.IPAddress))
|
||||
|
||||
// ❌ 问题:设备表只存储了 PublicKey,没有 PrivateKey
|
||||
}
|
||||
```
|
||||
|
||||
**影响**:
|
||||
- 管理员创建设备后,无法下载配置文件
|
||||
- 只能手动导入公钥,无法生成完整 WG 配置
|
||||
- 设备管理功能残废
|
||||
|
||||
**根本原因**:
|
||||
```go
|
||||
// model.Device 定义
|
||||
type Device struct {
|
||||
ID uint64
|
||||
PublicKey string // ✅ 存储公钥
|
||||
PrivateKey string // ❌ 没有此字段!
|
||||
IPAddress string
|
||||
}
|
||||
```
|
||||
|
||||
**解决方案 A: 添加 PrivateKey 字段**(不推荐)
|
||||
```go
|
||||
type Device struct {
|
||||
PublicKey string
|
||||
PrivateKey string // ⚠️ 安全风险:服务端存储私钥
|
||||
}
|
||||
```
|
||||
|
||||
**解决方案 B: 创建时返回,之后不存储**(推荐)
|
||||
```go
|
||||
// CreateDevice 返回完整信息
|
||||
type CreateDeviceResult struct {
|
||||
Device *model.Device
|
||||
PrivateKey string // 仅首次返回
|
||||
ConfigText string
|
||||
}
|
||||
|
||||
func (s *DeviceService) CreateDevice(req *CreateDeviceRequest) (*CreateDeviceResult, error) {
|
||||
// 生成密钥对
|
||||
privateKey, publicKey := generateKeyPair()
|
||||
|
||||
// 创建设备(只存公钥)
|
||||
device := &model.Device{
|
||||
PublicKey: publicKey,
|
||||
// ...
|
||||
}
|
||||
s.store.DB().Create(device)
|
||||
|
||||
// 生成配置
|
||||
configText := generateConfig(device, privateKey)
|
||||
|
||||
return &CreateDeviceResult{
|
||||
Device: device,
|
||||
PrivateKey: privateKey, // 仅此次返回
|
||||
ConfigText: configText,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**修复优先级**: **高(核心功能)**
|
||||
|
||||
---
|
||||
|
||||
## 🟡 P1 - 重要问题
|
||||
|
||||
### 问题 3: Network 创建后未返回完整信息
|
||||
|
||||
**位置**: `internal/api/handler/network.go` - `CreateNetwork()`
|
||||
|
||||
**现状**:
|
||||
```go
|
||||
func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
|
||||
network := h.networkService.CreateNetwork(req)
|
||||
|
||||
// ❌ 只返回基础信息
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": network,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**缺失信息**:
|
||||
- ❌ STUN/TURN 服务器列表
|
||||
- ❌ DDNS Provider 配置
|
||||
- ❌ Server 公网 IP 和端口
|
||||
- ❌ WireGuard 密钥对(如果是管理员设备)
|
||||
|
||||
**影响**:
|
||||
- 创建网络后,无法立即使用
|
||||
- 需要额外调用多个 API 获取配置
|
||||
- 用户体验差
|
||||
|
||||
**修复方案**:
|
||||
```go
|
||||
type CreateNetworkResponse struct {
|
||||
Network *model.Network
|
||||
STUNServers []model.Service
|
||||
TURNServers []model.Service
|
||||
DDNSConfig *DDNSConfig
|
||||
ServerInfo *ServerInfo
|
||||
}
|
||||
|
||||
func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
|
||||
network := h.networkService.CreateNetwork(req)
|
||||
|
||||
// 查询关联配置
|
||||
stunServers := s.getSTUNServers()
|
||||
turnServers := s.getTURNServers()
|
||||
ddnsConfig := s.getDDNSConfig(network.DDNSServiceID)
|
||||
|
||||
resp := &CreateNetworkResponse{
|
||||
Network: network,
|
||||
STUNServers: stunServers,
|
||||
TURNServers: turnServers,
|
||||
DDNSConfig: ddnsConfig,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": resp})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题 4: DDNS 同步缺少失败重试
|
||||
|
||||
**位置**: `internal/service/ddns_operation.go`
|
||||
|
||||
**现状**:
|
||||
```go
|
||||
func (s *DDNSOperationService) SyncMeshSeedToDNS(networkID uint64, seedString string) error {
|
||||
// 1. 查询 DDNS 配置
|
||||
config := s.getDDNSConfig(networkID)
|
||||
|
||||
// 2. 加密 MeshSeed
|
||||
encrypted := encrypt(seedString)
|
||||
|
||||
// 3. 创建 TXT 记录
|
||||
err := s.provider.CreateTXTRecord(config.Domain, encrypted)
|
||||
|
||||
// ❌ 没有重试机制
|
||||
// ❌ 没有错误处理
|
||||
// ❌ 没有状态记录
|
||||
}
|
||||
```
|
||||
|
||||
**影响**:
|
||||
- DNS API 临时故障导致同步失败
|
||||
- 用户不知道同步结果
|
||||
- 数据不一致
|
||||
|
||||
**修复方案**:
|
||||
```go
|
||||
func (s *DDNSOperationService) SyncMeshSeedToDNS(networkID uint64, seedString string) error {
|
||||
// 最多重试 3 次
|
||||
for i := 0; i < 3; i++ {
|
||||
err := s.doSync(networkID, seedString)
|
||||
if err == nil {
|
||||
// 成功
|
||||
s.updateStatus(networkID, "success", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 记录失败
|
||||
if i < 2 {
|
||||
time.Sleep(time.Duration(i+1) * time.Second) // 指数退避
|
||||
}
|
||||
}
|
||||
|
||||
// 全部失败
|
||||
s.updateStatus(networkID, "failed", "重试 3 次失败")
|
||||
return fmt.Errorf("同步失败")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题 5: STUN/TURN 配置来源不明确
|
||||
|
||||
**位置**: 多处使用
|
||||
|
||||
**现状**:
|
||||
```go
|
||||
// Core 层接收 STUN 服务器列表
|
||||
stunServers := config.STUNServers // ❌ 从哪里来?
|
||||
```
|
||||
|
||||
**可能来源**:
|
||||
1. Service 表查询 (`type='STUN'`)
|
||||
2. ExternalService 表查询
|
||||
3. 硬编码默认值
|
||||
4. 配置文件
|
||||
|
||||
**排查结果**:
|
||||
- ✅ Service 层有查询逻辑
|
||||
- ⚠️ 未明确调用链
|
||||
- ⚠️ 未传递给 Core 层
|
||||
|
||||
**修复建议**:
|
||||
```go
|
||||
// 在 Ctr 层明确传递
|
||||
func (c *Ctr) CreateNetwork(...) {
|
||||
// 查询 STUN/TURN 配置
|
||||
stunServers := s.getExternalServicesByType("STUN")
|
||||
turnServers := s.getExternalServicesByType("TURN")
|
||||
|
||||
// 传递给 Core
|
||||
engine := c.coreInst.CreateEngine(..., stunServers, turnServers)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟢 P2 - 优化建议
|
||||
|
||||
### 问题 6: WebSocket 断线重连机制
|
||||
|
||||
**位置**: `web/src/utils/websocket.js`
|
||||
|
||||
**现状**:
|
||||
```javascript
|
||||
// 前端 WebSocket 连接
|
||||
const ws = new WebSocket(url)
|
||||
|
||||
ws.onclose = () => {
|
||||
// ❌ 没有自动重连
|
||||
console.log('WebSocket 已关闭')
|
||||
}
|
||||
```
|
||||
|
||||
**修复方案**:
|
||||
```javascript
|
||||
class WebSocketClient {
|
||||
constructor(url) {
|
||||
this.url = url
|
||||
this.reconnectAttempts = 0
|
||||
this.maxReconnectAttempts = 5
|
||||
this.connect()
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url)
|
||||
|
||||
this.ws.onclose = () => {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++
|
||||
setTimeout(() => this.connect(), 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 问题统计
|
||||
|
||||
### 按严重程度
|
||||
|
||||
| 级别 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| **P0** | 2 | 阻塞性功能缺失 |
|
||||
| **P1** | 3 | 重要功能不完整 |
|
||||
| **P2** | 1 | 体验优化 |
|
||||
|
||||
### 按类别
|
||||
|
||||
| 类别 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| **逻辑不完整** | 3 | 审核、配置生成、创建返回 |
|
||||
| **错误处理** | 2 | DDNS 重试、WebSocket 重连 |
|
||||
| **配置管理** | 1 | STUN/TURN 来源 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 修复计划
|
||||
|
||||
### Phase 1: P0 问题修复(立即)
|
||||
|
||||
1. **修复 PendingJoin 审核逻辑**
|
||||
- 添加设备创建
|
||||
- 添加密钥生成
|
||||
- 添加配置返回
|
||||
- 预计:2 小时
|
||||
|
||||
2. **修复 DeviceService 配置生成**
|
||||
- 修改 CreateDevice 返回值
|
||||
- 添加配置下载接口
|
||||
- 预计:1 小时
|
||||
|
||||
### Phase 2: P1 问题修复(今天)
|
||||
|
||||
3. **完善 Network 创建返回**
|
||||
- 添加关联配置查询
|
||||
- 返回完整信息包
|
||||
- 预计:1 小时
|
||||
|
||||
4. **添加 DDNS 重试机制**
|
||||
- 实现指数退避
|
||||
- 添加状态记录
|
||||
- 预计:1 小时
|
||||
|
||||
5. **明确 STUN/TURN 传递链**
|
||||
- 添加代码注释
|
||||
- 确保配置传递
|
||||
- 预计:0.5 小时
|
||||
|
||||
### Phase 3: P2 优化(明天)
|
||||
|
||||
6. **WebSocket 重连机制**
|
||||
- 前端实现重连
|
||||
- 添加心跳检测
|
||||
- 预计:1 小时
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验收标准
|
||||
|
||||
### P0 问题验收
|
||||
|
||||
**PendingJoin 审核**:
|
||||
- ✅ 管理员审核通过后,能看到完整配置
|
||||
- ✅ 可以复制配置发送给申请人
|
||||
- ✅ 申请人导入配置即可连接
|
||||
|
||||
**DeviceService 配置**:
|
||||
- ✅ 创建设备时能下载配置文件
|
||||
- ✅ 配置文件格式正确
|
||||
- ✅ 可直接导入 WireGuard 客户端
|
||||
|
||||
### P1 问题验收
|
||||
|
||||
**Network 创建**:
|
||||
- ✅ 创建后返回完整信息包
|
||||
- ✅ 包含 STUN/TURN 配置
|
||||
- ✅ 包含 DDNS 配置(如果启用)
|
||||
|
||||
**DDNS 重试**:
|
||||
- ✅ 临时故障自动恢复
|
||||
- ✅ 失败有明确提示
|
||||
- ✅ 状态可查询
|
||||
|
||||
---
|
||||
|
||||
## 📝 总结
|
||||
|
||||
**核心问题**:
|
||||
1. ❌ 审核通过 ≠ 获得配置(逻辑断裂)
|
||||
2. ❌ 创建设备 ≠ 能下载配置(功能残废)
|
||||
3. ❌ 创建网络 ≠ 能用(信息不完整)
|
||||
|
||||
**根本原因**:
|
||||
- 前后端沟通不足
|
||||
- 服务边界不清晰
|
||||
- 缺少端到端验证
|
||||
|
||||
**改进方向**:
|
||||
1. 加强全链路测试
|
||||
2. 建立验收标准
|
||||
3. 完善错误处理
|
||||
4. 增加日志记录
|
||||
|
||||
---
|
||||
|
||||
**排查人员**: AI Assistant
|
||||
**排查时间**: 2026-03-20
|
||||
**下一步**: 立即开始 P0 问题修复
|
||||
Reference in New Issue
Block a user