# 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*
