# 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 添加 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 创建组网时: ├── 启用 DDNS 同步:[开关] ├── 自动使用全局 DDNS 配置(已在服务中配置) └── TXT 记录名:_meshray._mesh (固定) 用途:将 MeshSeed 加密后写入 DNS TXT 记录 格式:_meshray._mesh.{network-name}.{domain} ``` --- ## 🔧 具体修改方案 ### 修改 1: List.vue - 服务市场 DDNS **当前问题**: - ❌ 只有 A/AAAA 选项 - ❌ 强制要求 IP、端口 - ❌ 无法用于 MeshSeed 同步 **修改方向**: ```vue ``` --- ### 修改 2: Networks/Create.vue - 组网时启用 DDNS **新增逻辑**: ```vue
开启后将 MeshSeed 加密同步到 DNS TXT 记录
TXT 记录名:_meshray._mesh.{{ formData.name }}.{{ formData.ddns_domain }}
``` --- ### 修改 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*