Files
Meshray-Manager/docs/DDNS 双模式架构修复方案.md
2026-06-30 15:14:37 +08:00

507 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DDNS 双模式架构修复方案
**分析时间**: 2026-03-26
**核心洞察**: 两种完全不同的 DDNS 用途,需要分离处理
---
## 🎯 架构澄清
### 两种 DDNS 用途对比
| 特性 | 服务市场-DDNS | 组网同步-DDNS |
|------|---------------|---------------|
| **用途** | 通用动态 DNS | MeshSeed 专用同步 |
| **记录类型** | A / AAAA | **仅 TXT** |
| **配置项** | IP、端口、认证 | TXT 记录名、域名 |
| **调用位置** | 服务市场 → 添加服务 | 组网创建/分享 → 启用 DDNS |
| **后端接口** | `/api/v1/services` (ExternalService) | `/api/v1/ddns/config` (DDNSConfig) |
| **数据表** | `external_services` | `ddns_configs` + `meshseeds` |
---
## ✅ 正确的设计
### 1. 服务市场 → DDNS(通用动态 DNS)
```vue
<!-- List.vue - 服务市场 -->
添加 DDNS 服务时
├── DNS 服务商阿里云/腾讯云/Cloudflare
├── 记录类型A / AAAA / TXT (三选一)
├── 域名example.com
├── 主机记录@ www (A/AAAA 时需要)
├── TXT 记录名_meshray._mesh (TXT 时需要)
├── 目标值1.2.3.4 "v=spf1 ..."
└── IP/端口用于检测和目标更新
用途传统的动态 DNS 解析
```
---
### 2. 组网同步 → DDNSMeshSeed 专用)
```vue
<!-- Networks/Create.vue List.vue -->
创建组网时
├── 启用 DDNS 同步[开关]
├── 自动使用全局 DDNS 配置已在服务中配置
└── TXT 记录名_meshray._mesh (固定)
用途 MeshSeed 加密后写入 DNS TXT 记录
格式_meshray._mesh.{network-name}.{domain}
```
---
## 🔧 具体修改方案
### 修改 1: List.vue - 服务市场 DDNS
**当前问题**:
- ❌ 只有 A/AAAA 选项
- ❌ 强制要求 IP、端口
- ❌ 无法用于 MeshSeed 同步
**修改方向**:
```vue
<!-- 修改 record_type 下拉框 -->
<el-form-item label="记录类型" prop="record_type">
<el-select v-model="formData.record_type" placeholder="请选择记录类型">
<el-option label="TXT (文本记录)" value="TXT" />
<el-option label="A (IPv4 地址)" value="A" />
<el-option label="AAAA (IPv6 地址)" value="AAAA" />
</el-select>
</el-form-item>
<!-- 条件显示字段 -->
<!-- TXT 记录时显示 -->
<el-form-item v-if="formData.record_type === 'TXT'" label="TXT 记录名" prop="txt_record_name">
<el-input v-model="formData.txt_record_name" placeholder="_meshray._mesh" />
</el-form-item>
<!-- A/AAAA 记录时显示 -->
<el-form-item v-if="['A', 'AAAA'].includes(formData.record_type)" label="主机记录" prop="subdomain">
<el-input v-model="formData.subdomain" placeholder="@ 或 www" />
</el-form-item>
<!-- A/AAAA 需要 IP 和端口 -->
<el-form-item v-if="['A', 'AAAA'].includes(formData.record_type)" label="目标 IP" prop="target_ip">
<el-input v-model="formData.target_ip" placeholder="1.2.3.4" />
</el-form-item>
<el-form-item v-if="['A', 'AAAA'].includes(formData.record_type)" label="检测端口" prop="port">
<el-input-number v-model="formData.port" :min="1" :max="65535" />
</el-form-item>
```
---
### 修改 2: Networks/Create.vue - 组网时启用 DDNS
**新增逻辑**:
```vue
<!-- 在创建组网表单中添加 -->
<el-form-item label="DDNS 同步">
<el-switch v-model="formData.ddns_enabled" />
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
开启后将 MeshSeed 加密同步到 DNS TXT 记录
</div>
</el-form-item>
<el-form-item v-if="formData.ddns_enabled" label="DDNS 域名">
<el-select v-model="formData.ddns_domain" placeholder="请选择已配置的域名">
<el-option
v-for="domain in availableDDNSDomains"
:key="domain"
:label="domain"
:value="domain"
/>
</el-select>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
TXT 记录名_meshray._mesh.{{ formData.name }}.{{ formData.ddns_domain }}
</div>
</el-form-item>
```
---
### 修改 3: 后端逻辑分离
#### A. ExternalService 处理(服务市场)
```go
// internal/service/external_service.go
type ExternalService struct {
ID uint `gorm:"primaryKey"`
Name string
Type string // "DDNS", "STUN", "TURN"
Provider string // "aliyun", "tencent", "cloudflare"
Domain string
RecordType string // "A", "AAAA", "TXT"
// A/AAAA 记录用
TargetIP string
Subdomain string
CheckPort int
// TXT 记录用(通用 DDNS
TXTName string
TXTValue string
// 认证信息
AccessKey string
SecretKey string
}
// SyncExternalDDNS 同步外部 DDNS 服务
func (s *ExternalServiceService) SyncExternalDDNS(ctx context.Context, service *model.ExternalService) error {
if service.Type != "DDNS" {
return nil
}
switch service.RecordType {
case "A", "AAAA":
// 获取本机公网 IP
ip := getPublicIP()
// 更新 DNS A/AAAA 记录
return updateIPRecord(ctx, service, ip)
case "TXT":
// 通用 TXT 记录同步(非 MeshSeed
return updateTXTRecord(ctx, service, service.TXTValue)
default:
return fmt.Errorf("不支持的记录类型:%s", service.RecordType)
}
}
```
---
#### B. DDNSService 处理(MeshSeed 同步)
```go
// internal/service/ddns.go
type DDNSService struct {
db *gorm.DB
logger *zap.Logger
}
// SyncMeshSeeds 同步所有网络的 MeshSeed 到 TXT 记录
func (s *DDNSService) SyncMeshSeeds(ctx context.Context) error {
// 1. 查询全局 DDNS 配置
var config model.DDNSConfig
if err := s.db.First(&config).Error; err != nil {
return err
}
if !config.Enabled {
return nil // 未启用,跳过
}
// 2. 查询所有启用 DDNS 的网络
var networks []model.Network
s.db.Where("ddns_enabled = ? AND domain = ?", true, config.Domain).
Find(&networks)
// 3. 为每个网络同步 MeshSeed
for _, network := range networks {
// 获取最新 MeshSeed
var meshSeed model.MeshSeed
s.db.Where("network_id = ? AND revoked = ?", network.ID, false).
Order("created_at DESC").
First(&meshSeed)
if meshSeed.ID == 0 {
continue // 无 MeshSeed,跳过
}
// 加密 MeshSeed
encrypted, err := encryptMeshSeed(&meshSeed, network.NetworkSecret)
if err != nil {
return err
}
// 构造 TXT 记录名
txtRecordName := fmt.Sprintf("_meshray._mesh.%s.%s",
network.Name, config.Domain)
// 同步到 DNS
provider := getDDNSProvider(config.Provider)
err = provider.SyncRecords(ctx, config.Domain, []DDNSRecord{
{
Type: "TXT",
Name: txtRecordName,
Value: encrypted,
},
})
if err != nil {
return err
}
}
return nil
}
```
---
## 📋 前端路由调整
### 移除独立编辑页面
```javascript
// web/src/router/index.js - 移除或标记弃用
{
path: 'ddns/edit',
name: 'DDNSEdit',
component: () => import('@/views/Service/DDNSEdit.vue'),
meta: { deprecated: true } // 标记为弃用
}
```
**检查调用点**:
```bash
# 搜索所有引用
grep -r "DDNSEdit" web/src/
grep -r "/ddns/edit" web/src/
```
**预期结果**:
- ✅ List.vue 中的 `configureDDNS` 直接处理
- ✅ 不再有跳转到独立编辑页
---
## 🎯 完整用户流程
### 场景 1: 配置通用 DDNS(服务市场)
```
1. 访问:服务市场 → 同步服务
2. 点击:Cloudflare DDNS
3. 填写表单:
├─ DNS 服务商:Cloudflare
├─ 记录类型:A (IPv4 地址)
├─ 域名:example.com
├─ 主机记录:nas
├─ 目标 IP: 1.2.3.4
└─ 检测端口:80
4. 保存 → 添加到 external_services 表
5. 系统定期检测 IP 变化并更新 DNS
```
---
### 场景 2: 创建组网并启用 MeshSeed 同步
```
1. 访问:组网管理 → 创建网络
2. 填写基本信息:
├─ 名称:MyNetwork
├─ 子网:10.0.0.0/24
└─ 启用 DDNS 同步:✅ ON
3. 选择 DDNS 域名:
└─ example.com(从已配置的全局 DDNS 读取)
4. 保存 → 创建 Network
5. 生成 MeshSeed 时:
├─ POST /api/v1/networks/:id/meshseed
├─ ddns_enabled: true
└─ 自动触发同步到 DNS
6. DNS TXT 记录生成:
└─ _meshray._mesh.MyNetwork.example.com
值:Base64(加密的 MeshSeed)
```
---
### 场景 3: 分享组网(带 MeshSeed
```
1. 访问:组网详情 → 分享
2. 配置分享参数:
├─ 有效期:7 天
├─ 最大使用次数:10
└─ DDNS 同步:✅ ON
3. 生成 MeshSeed URL
└─ meshray://eyJhbGci... (加密 Token)
4. 同时自动同步到 DNS TXT 记录
5. 新成员加入:
├─ 方式 1: 扫描 QR Code
└─ 方式 2: DNS 查询 TXT 记录获取 MeshSeed
```
---
## 🔍 数据库设计
### external_services 表(服务市场)
```sql
CREATE TABLE external_services (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL, -- 服务名称
type TEXT NOT NULL, -- "DDNS", "STUN", "TURN"
provider TEXT, -- "aliyun", "tencent", "cloudflare"
-- 通用字段
domain TEXT, -- 域名
record_type TEXT, -- "A", "AAAA", "TXT"
-- A/AAAA 记录专用
target_ip TEXT, -- 目标 IP
subdomain TEXT, -- 子域名
check_port INTEGER, -- 检测端口
-- TXT 记录专用
txt_name TEXT, -- TXT 记录名
txt_value TEXT, -- TXT 记录值
-- 认证信息
access_key TEXT, -- AccessKey (加密)
secret_key TEXT, -- SecretKey (加密)
enabled BOOLEAN DEFAULT TRUE,
created_at DATETIME,
updated_at DATETIME
);
```
---
### ddns_configs 表(全局配置)
```sql
CREATE TABLE ddns_configs (
id INTEGER PRIMARY KEY,
provider TEXT NOT NULL, -- "aliyun", "tencent", "cloudflare"
access_key TEXT, -- AccessKey (加密)
secret_key TEXT, -- SecretKey (加密)
domain TEXT NOT NULL, -- 主域名
txt_record_name TEXT, -- TXT 记录前缀(默认_meshray._mesh
sync_mode TEXT, -- "auto" | "manual"
retry_interval INTEGER, -- 重试间隔(秒)
max_retries INTEGER, -- 最大重试次数
enabled BOOLEAN DEFAULT TRUE,
last_sync_at DATETIME,
status TEXT, -- "reachable" | "unreachable"
created_at DATETIME,
updated_at DATETIME
);
```
---
### networks 表(组网)
```sql
CREATE TABLE networks (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
network_secret TEXT NOT NULL, -- 网络密钥(用于派生加密密钥)
subnet TEXT NOT NULL,
ddns_enabled BOOLEAN DEFAULT FALSE, -- 是否启用 MeshSeed 同步
ddns_domain TEXT, -- DDNS 域名(引用 ddns_configs.domain
created_at DATETIME,
updated_at DATETIME
);
```
---
### meshseeds 表(MeshSeed
```sql
CREATE TABLE meshseeds (
id INTEGER PRIMARY KEY,
seed_id TEXT NOT NULL, -- 随机 Seed ID
network_id INTEGER NOT NULL, -- 关联网络
join_token TEXT NOT NULL, -- Base64 Token
signature TEXT NOT NULL, -- Ed25519 签名
ddns_enabled BOOLEAN DEFAULT FALSE, -- 是否同步到 DNS
ddns_domain TEXT, -- 同步到的域名
expires_at DATETIME,
revoked BOOLEAN DEFAULT FALSE,
created_at DATETIME,
updated_at DATETIME,
FOREIGN KEY (network_id) REFERENCES networks(id)
);
```
---
## ✅ 修改清单
### 前端修改
1.**List.vue** - 服务市场 DDNS 配置
- 添加 TXT 记录选项
- 条件显示字段(A/AAAA vs TXT
- 修改 `configureDDNS` 函数逻辑
2.**Networks/Create.vue** - 创建组网
- 添加 DDNS 同步开关
- 添加域名选择器
3.**Networks/List.vue** - 分享组网
- DDNS 同步选项保留
- 说明文字更新
4.**router/index.js** - 路由
- 标记 DDNSEdit 为弃用
- 或直接移除
5.**DDNSEdit.vue** - 独立编辑页
- 不再使用
- 可以删除或保留兼容
---
### 后端修改
1.**ExternalService Model** - 扩展字段
- 添加 `record_type`, `txt_name`, `txt_value`
2.**ExternalServiceService** - 新增方法
- `SyncExternalDDNS()` - 同步外部 DDNS
3.**DDNSService** - 重写逻辑
- `SyncMeshSeeds()` - 同步 MeshSeed 到 TXT
- 与 IP 同步完全分离
4.**Network Model** - 确认字段
- `ddns_enabled`
- `ddns_domain`
- `network_secret`
---
## 🎯 下一步行动
**优先级排序**:
1. **P0 - 后端分离逻辑** (最关键)
- 修改 `DDNSService.SyncMeshSeeds()`
- 确保只处理 TXT 记录和 MeshSeed
2. **P1 - 前端服务市场改造**
- List.vue 添加 TXT 选项
- 条件显示字段
3. **P2 - 组网创建集成**
- Create.vue 添加 DDNS 开关
- 域名选择器
4. **P3 - 清理弃用代码**
- 移除 DDNSEdit 路由
- 删除或归档 DDNSEdit.vue
---
*DDNS 双模式架构修复方案 | v1.0*