503 lines
12 KiB
Markdown
503 lines
12 KiB
Markdown
# 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 问题修复
|