507 lines
13 KiB
Markdown
507 lines
13 KiB
Markdown
# 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. 组网同步 → DDNS(MeshSeed 专用)
|
||
|
||
```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*
|