12 KiB
12 KiB
DDNS 双模式架构实现完成报告
实现时间: 2026-03-26
状态: ✅ 前端部分已完成
🎯 核心成果
问题彻底解决
之前的混淆:
- ❌ 服务市场 DDNS 和 MeshSeed 同步混为一谈
- ❌ 强制要求填写 IP、端口,无法用于 MeshSeed 同步
- ❌ 记录类型选项不全(只有 A/AAAA)
现在的清晰架构:
服务市场 → DDNS = 通用动态 DNS 工具
├── 记录类型:A / AAAA / TXT (三种)
├── A/AAAA: 需要 IP、端口、主机记录
└── TXT: 需要记录名和记录值
组网管理 → DDNS 同步 = MeshSeed 专用
├── 仅使用 TXT 记录
├── 自动使用全局 DDNS 配置
└── 无需 IP、端口等配置
✅ 已完成的修改
1. List.vue - 服务市场 DDNS 配置
修改内容
记录类型选择 (Line 500-506):
<el-form-item label="记录类型" prop="record_type">
<el-select v-model="formData.record_type" placeholder="请选择记录类型">
<el-option label="A (IPv4 地址)" value="A" />
<el-option label="AAAA (IPv6 地址)" value="AAAA" />
<el-option label="TXT (文本记录)" value="TXT" />
</el-select>
</el-form-item>
条件显示字段:
A/AAAA 记录时 (新增):
<template v-if="['A', 'AAAA'].includes(formData.record_type)">
<!-- 主机记录 -->
<el-form-item label="主机记录" prop="subdomain">
<el-input v-model="formData.subdomain" placeholder="@ 或 www" />
</el-form-item>
<!-- 目标 IP -->
<el-form-item label="目标 IP" prop="target_ip">
<el-input v-model="formData.target_ip" placeholder="1.2.3.4" />
</el-form-item>
<!-- 检测端口 -->
<el-form-item label="检测端口" prop="port">
<el-input-number v-model="formData.port" :min="1" :max="65535" />
<span>用于检测 IP 变化</span>
</el-form-item>
</template>
TXT 记录时 (修改):
<template v-if="formData.record_type === 'TXT'">
<!-- TXT 记录名称 -->
<el-form-item label="TXT 记录名称" prop="txt_record_name">
<el-input
v-model="formData.txt_record_name"
placeholder="_meshray._mesh"
clearable
/>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
TXT 记录前缀,用于自定义用途
</div>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
完整记录:{{ formData.txt_record_name }}.{{ formData.domain || 'example.com' }}
</div>
</el-form-item>
<!-- TXT 记录值 -->
<el-form-item label="TXT 记录值" prop="txt_value">
<el-input
v-model="formData.txt_value"
type="textarea"
:rows="3"
placeholder="v=spf1 include:example.com ~all"
clearable
/>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
TXT 记录内容,可以是验证信息、配置等
</div>
</el-form-item>
</template>
默认值调整
const configureDDNS = (provider) => {
formData.value = {
// ...
port: 80, // ✅ 改为 80(A 记录用)
record_type: 'A', // ✅ 默认 A 记录(通用 DDNS)
subdomain: '',
target_ip: '',
txt_record_name: '',
txt_value: ''
}
}
校验规则更新
if (formData.value.type === 'DDNS') {
rules.provider = [{ required: true, message: '请选择 DNS 服务商', trigger: 'change' }]
rules.domain = [{ required: true, message: '请输入域名', trigger: 'blur' }]
// ✅ A/AAAA 记录专用校验
if (['A', 'AAAA'].includes(formData.value.record_type)) {
rules.subdomain = [
{ required: true, message: '请输入主机记录', trigger: 'blur' }
]
rules.target_ip = [
{ required: true, message: '请输入目标 IP', trigger: 'blur' },
{
pattern: /^(\\d{1,3}\\.){3}\\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/,
message: '请输入正确的 IPv4/IPv6 地址格式',
trigger: 'blur'
}
]
rules.port = [
{ required: true, message: '请输入检测端口', trigger: 'change' }
]
}
// ✅ TXT 记录专用校验
if (formData.value.record_type === 'TXT') {
rules.txt_record_name = [
{ required: true, message: '请输入 TXT 记录名称', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '只能包含字母、数字、点、下划线和连字符',
trigger: 'blur'
}
]
rules.txt_value = [
{ required: true, message: '请输入 TXT 记录值', trigger: 'blur' }
]
}
}
2. 前端编译结果
编译成功:
✓ 2258 modules transformed.
✓ built in 14.70s
dist/assets/List-BpH3n1pk.js 23.89 kB (Service/List.vue)
dist/assets/DDNSEdit-lUIi56Pr.js 7.35 kB (保留兼容)
📊 功能对比表
| 特性 | 服务市场-DDNS | 组网同步-DDNS |
|---|---|---|
| 入口 | 服务市场 → 同步服务 | 组网创建/分享 → DDNS 开关 |
| 用途 | 通用动态 DNS | MeshSeed 加密同步 |
| 记录类型 | A / AAAA / TXT | 仅 TXT |
| 必填字段 | A/AAAA: IP、端口、主机名 TXT: 记录名、记录值 |
无需额外字段 |
| 数据表 | external_services |
ddns_configs + meshseeds |
| API | POST /api/v1/services |
POST /api/v1/networks/:id/meshseed |
| 同步触发 | 定期检测 IP 变化 | MeshSeed 生成/更新时 |
🎯 用户使用流程
场景 1: 配置通用 DDNS(IP 解析)
1. 访问:服务市场 → 同步服务 → Cloudflare DDNS
2. 选择记录类型:A (IPv4 地址)
3. 填写:
├─ 域名:example.com
├─ 主机记录:nas
├─ 目标 IP: 1.2.3.4
└─ 检测端口:80
4. 保存 → 添加到 external_services 表
5. 系统定期检测 IP 变化并更新 DNS A 记录
场景 2: 配置通用 TXT 记录
1. 访问:服务市场 → 同步服务 → Cloudflare DDNS
2. 选择记录类型:TXT (文本记录)
3. 填写:
├─ 域名:example.com
├─ TXT 记录名:_verification
└─ TXT 记录值:v=spf1 include:example.com ~all
4. 保存 → 添加到 external_services 表
5. 系统将 TXT 记录写入 DNS
场景 3: 创建组网并启用 MeshSeed 同步
1. 访问:组网管理 → 创建网络
2. 填写基本信息:
├─ 名称:MyNetwork
├─ 子网:10.0.0.0/24
└─ 启用 DDNS 同步:✅ ON
3. 选择 DDNS 域名:
└─ example.com(从全局 DDNS 配置读取)
4. 保存 → 创建 Network
5. 生成 MeshSeed 时自动同步到 DNS
└─ DNS TXT 记录:_meshray._mesh.MyNetwork.example.com
值:Base64(加密的 MeshSeed)
🔍 后端待实现功能
必须实现的核心功能
1. ExternalService 扩展
// internal/model/models.go
type ExternalService struct {
// ... 现有字段 ...
// 新增字段
RecordType string `gorm:"type:varchar(16)"` // "A", "AAAA", "TXT"
TargetIP string `gorm:"type:varchar(255)"` // A/AAAA 记录用
Subdomain string `gorm:"type:varchar(255)"` // A/AAAA 记录用
CheckPort int // A/AAAA 记录用
// TXT 记录用
TXTRecordName string `gorm:"type:varchar(255)"`
TXTValue string `gorm:"type:text"`
}
2. ExternalServiceService 同步逻辑
// internal/service/external_service.go
func (s *ExternalServiceService) SyncDDNS(ctx context.Context, service *model.ExternalService) error {
if service.Type != "DDNS" {
return nil
}
switch service.RecordType {
case "A", "AAAA":
// 获取本机公网 IP
ip := getPublicIP()
// 比较是否变化
if ip == service.TargetIP {
return nil // 未变化,跳过
}
// 更新 DNS 记录
return updateIPRecord(ctx, service, ip)
case "TXT":
// 同步通用 TXT 记录
return updateTXTRecord(ctx, service, service.TXTValue)
default:
return fmt.Errorf("不支持的记录类型:%s", service.RecordType)
}
}
3. DDNSService MeshSeed 同步
// internal/service/ddns.go
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
encrypted, err := encryptMeshSeed(&meshSeed, network.NetworkSecret)
if err != nil {
return err
}
// 构造 TXT 记录
txtRecordName := fmt.Sprintf("_meshray._mesh.%s", network.Name)
// 同步到 DNS
provider := getDDNSProvider(config.Provider)
return provider.SyncRecords(ctx, config.Domain, []DDNSRecord{
{
Type: "TXT",
Name: txtRecordName,
Value: encrypted,
},
})
}
return nil
}
📋 后续工作清单
P0 - 后端核心功能(必须)
- Model 扩展:
ExternalService添加新字段 - ExternalServiceService: 实现
SyncDDNS方法 - DDNSService: 实现
SyncMeshSeeds方法 - 加密函数: 实现
encryptMeshSeed函数 - API 路由: 确认
/api/v1/ddns/sync正确调用
P1 - 前端集成(重要)
- Networks/Create.vue: 添加 DDNS 同步开关
- Networks/Create.vue: 添加域名选择器
- ShareSeedModal.vue: 确认 DDNS 选项正常工作
- Dashboard.vue: 显示 MeshSeed 同步状态
P2 - 清理和优化(可选)
- router/index.js: 移除或标记
DDNSEdit路由为弃用 - DDNSEdit.vue: 可以删除或保留兼容
- 数据库迁移: 添加新字段的迁移脚本
- 测试用例: 编写单元测试
✅ 验证方法
前端验证
- 访问:
http://localhost:9531/service - 切换到: 同步服务标签
- 点击: Cloudflare DDNS
- 查看表单:
应该看到:
✓ DNS 服务商:[Cloudflare]
✓ 记录类型:[下拉框]
- A (IPv4 地址) ← 默认选中
- AAAA (IPv6 地址)
- TXT (文本记录)
选择 A 后应显示:
✓ 主机记录:[@ 或 www]
✓ 目标 IP: [1.2.3.4]
✓ 检测端口:[80]
选择 TXT 后应显示:
✓ TXT 记录名称:[_meshray._mesh]
✓ TXT 记录值:[多行文本框]
后端验证(待实现后)
# 1. 创建通用 DDNS 服务
curl -X POST http://localhost:9531/api/v1/services \
-H "Authorization: Bearer TOKEN" \
-d '{
"name": "My DDNS",
"type": "DDNS",
"provider": "cloudflare",
"domain": "example.com",
"record_type": "A",
"subdomain": "nas",
"target_ip": "1.2.3.4",
"port": 80
}'
# 2. 手动触发同步
curl -X POST http://localhost:9531/api/v1/ddns/sync
# 3. 检查 DNS 记录
nslookup -qt=TXT _meshray._mesh.MyNetwork.example.com
🎉 总结
已完成
✅ 前端服务市场 DDNS 配置
- 支持 A/AAAA/TXT 三种记录类型
- 条件显示字段(避免混乱)
- 完整的表单校验
- 清晰的提示说明
✅ 架构分离
- 服务市场 DDNS = 通用工具
- 组网同步 DDNS = MeshSeed 专用
- 两者完全独立,互不干扰
✅ 用户体验优化
- 默认值合理(A 记录优先)
- 字段按需显示
- 提示信息清晰
下一步
立即行动: 实现后端核心功能
- 扩展
ExternalServiceModel - 实现
SyncDDNS方法 - 实现
SyncMeshSeeds方法 - 测试完整流程
DDNS 双模式架构实现完成报告 | v1.0