Files
Meshray-Manager/docs/DDNS 双模式架构实现完成报告.md
2026-06-30 15:14:37 +08:00

12 KiB
Raw Permalink Blame History

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,           // ✅ 改为 80A 记录用)
    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: 可以删除或保留兼容
  • 数据库迁移: 添加新字段的迁移脚本
  • 测试用例: 编写单元测试

验证方法

前端验证

  1. 访问: http://localhost:9531/service
  2. 切换到: 同步服务标签
  3. 点击: Cloudflare DDNS
  4. 查看表单:

应该看到:

✓ 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 记录优先)
  • 字段按需显示
  • 提示信息清晰

下一步

立即行动: 实现后端核心功能

  1. 扩展 ExternalService Model
  2. 实现 SyncDDNS 方法
  3. 实现 SyncMeshSeeds 方法
  4. 测试完整流程

DDNS 双模式架构实现完成报告 | v1.0