Files
Meshray-Manager/docs/DDNS_Usage 前缀定义策略.md
T
2026-06-30 15:14:37 +08:00

16 KiB
Raw Blame History

DDNS Usage 前缀定义策略

设计时间: 2026-03-26
核心问题: 如何定义和管理 TXT 记录前缀


🎯 问题背景

当前需求

同一个 DDNS 配置(如 example.com)需要支持多个组网同步:
├─ 组网 A → _meshray._mesh.network-a.example.com
├─ 组网 B → _meshray._mesh.network-b.example.com
└─ 组网 C → _custom.prefix.network-c.example.com

问题:前缀 (_meshray._mesh) 如何定义?谁来决定?

三种设计方案

方案一:系统预设固定前缀(推荐)

设计思路:

系统内置标准前缀,用户不可自定义
├─ MeshSeed 同步专用:_meshray._mesh
├─ 未来扩展 1: _meshray.config (配置同步)
└─ 未来扩展 2: _meshray.device (设备注册)

优点:

  • 标准化,避免混乱
  • 安全性高(防止恶意前缀)
  • 实现简单

缺点:

  • 灵活性较低
  • 无法适配特殊场景

数据库设计:

// DDNSUsage 模型 - 前缀字段枚举化
type DDNSUsage struct {
    ID           string     `gorm:"primaryKey;type:varchar(36)"`
    ServiceID    string     `gorm:"type:varchar(36);index"`
    
    // ✅ 方案 A:预设前缀类型(枚举)
    PrefixType   string     `gorm:"type:varchar(32);not null"` 
    /*
    可选值:
    - "MESHSEED_SYNC" → 对应 "_meshray._mesh"
    - "CONFIG_SYNC"   → 对应 "_meshray.config"
    - "DEVICE_REG"    → 对应 "_meshray.device"
    */
    
    RecordPrefix string     `gorm:"-"` // 计算字段,不存储
    FullDomain   string     // 自动生成
    
    NetworkID    *uint64    `gorm:"type:bigint;index"`
    Enabled      bool       `gorm:"default:true"`
}

// 方法:获取实际前缀
func (u *DDNSUsage) GetRecordPrefix() string {
    switch u.PrefixType {
    case "MESHSEED_SYNC":
        return "_meshray._mesh"
    case "CONFIG_SYNC":
        return "_meshray.config"
    case "DEVICE_REG":
        return "_meshray.device"
    default:
        panic("未知的前缀类型:" + u.PrefixType)
    }
}

前端实现:

<!-- 创建 Usage 时只能选择预设类型 -->
<el-form-item label="用途类型" prop="prefix_type">
  <el-select v-model="formData.prefix_type" placeholder="请选择">
    <el-option 
      label="MeshSeed 同步 (_meshray._mesh)" 
      value="MESHSEED_SYNC" 
    />
    <el-option 
      label="配置同步 (_meshray.config)" 
      value="CONFIG_SYNC"
      disabled 
    />
    <el-option 
      label="设备注册 (_meshray.device)" 
      value="DEVICE_REG"
      disabled 
    />
  </el-select>
  <div class="form-tip">
    <el-icon><InfoFilled /></el-icon>
    当前仅支持 MeshSeed 同步其他功能开发中
  </div>
</el-form-item>

方案二:管理员自定义前缀(灵活)🔧

设计思路:

管理员创建 Usage 时自由填写前缀
├─ 示例 1: _meshray._mesh
├─ 示例 2: _custom.prefix
└─ 示例 3: anything.you.want

优点:

  • 灵活性极高
  • 适配各种场景

缺点:

  • 容易冲突(需要验证唯一性)
  • 安全性风险(可能输入恶意前缀)
  • 用户学习成本高

数据库设计:

type DDNSUsage struct {
    ID           string     `gorm:"primaryKey;type:varchar(36)"`
    ServiceID    string     `gorm:"type:varchar(36);index"`
    
    // ✅ 方案 B:完全自定义前缀
    RecordPrefix string     `gorm:"type:varchar(255);not null"`
    /*
    示例:
    - "_meshray._mesh"
    - "_custom.test"
    - "anything"
    */
    
    // 格式验证
    validator func(string) error
    
    NetworkID    *uint64    `gorm:"type:bigint;index"`
    Enabled      bool       `gorm:"default:true"`
}

// 验证函数
func validateRecordPrefix(prefix string) error {
    if prefix == "" {
        return fmt.Errorf("前缀不能为空")
    }
    
    // DNS 标签规则
    if len(prefix) > 253 {
        return fmt.Errorf("前缀过长(最大 253 字符)")
    }
    
    // 只能包含字母、数字、连字符、点
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9._-]+$`, prefix)
    if !matched {
        return fmt.Errorf("前缀只能包含字母、数字、点、下划线和连字符")
    }
    
    // 不能以特殊字符开头
    if strings.HasPrefix(prefix, "_") && !strings.HasPrefix(prefix, "_meshray") {
        return fmt.Errorf("下划线前缀仅限系统使用")
    }
    
    return nil
}

前端实现:

<el-form-item label="TXT 记录前缀" prop="record_prefix">
  <el-input
    v-model="formData.record_prefix"
    placeholder="_meshray._mesh"
    maxlength="253"
  />
  <div class="form-tip">
    <el-icon><InfoFilled /></el-icon>
    完整记录名{{ formData.record_prefix }}.网络名称域名
  </div>
  <div class="form-tip">
    <el-icon><WarningFilled /></el-icon>
    只能包含字母数字下划线和连字符
  </div>
</el-form-item>

方案三:混合模式(最佳实践)🏆

设计思路:

系统预设 + 管理员自定义组合
├─ 预设类型:快速选择,安全可靠
└─ 自定义:高级选项,满足特殊需求

数据库设计:

type DDNSUsage struct {
    ID           string     `gorm:"primaryKey;type:varchar(36)"`
    ServiceID    string     `gorm:"type:varchar(36);index"`
    
    // ✅ 方案 C:混合模式
    PrefixMode   string     `gorm:"type:varchar(16);not null;default:'preset'"`
    /*
    - "preset"    → 使用预设类型
    - "custom"    → 使用自定义前缀
    */
    
    PrefixType   string     `gorm:"type:varchar(32)"`          // preset 模式下使用
    RecordPrefix string     `gorm:"type:varchar(255)"`         // custom 模式下使用
    
    NetworkID    *uint64    `gorm:"type:bigint;index"`
    Enabled      bool       `gorm:"default:true"`
}

// 方法:获取实际前缀
func (u *DDNSUsage) GetRecordPrefix() string {
    if u.PrefixMode == "preset" {
        return u.GetPresetPrefix()
    } else {
        return u.RecordPrefix
    }
}

func (u *DDNSUsage) GetPresetPrefix() string {
    switch u.PrefixType {
    case "MESHSEED_SYNC":
        return "_meshray._mesh"
    case "CONFIG_SYNC":
        return "_meshray.config"
    default:
        return "_meshray._mesh" // 默认回退
    }
}

前端实现:

<el-form-item label="前缀模式" prop="prefix_mode">
  <el-radio-group v-model="formData.prefix_mode">
    <el-radio label="preset">系统预设</el-radio>
    <el-radio label="custom">自定义</el-radio>
  </el-radio-group>
</el-form-item>

<!-- 预设模式 -->
<el-form-item v-if="formData.prefix_mode === 'preset'" label="预设类型">
  <el-select v-model="formData.prefix_type" placeholder="请选择">
    <el-option 
      label="✨ MeshSeed 同步 (_meshray._mesh)" 
      value="MESHSEED_SYNC" 
    />
    <el-option 
      label="🔧 配置同步 (_meshray.config)" 
      value="CONFIG_SYNC"
      disabled
    />
  </el-select>
</el-form-item>

<!-- 自定义模式 -->
<el-form-item v-else label="TXT 记录前缀">
  <el-input
    v-model="formData.record_prefix"
    placeholder="例如:my.custom.prefix"
    maxlength="253"
  />
  <div class="form-tip">
    <el-icon><InfoFilled /></el-icon>
    需符合 DNS 命名规范
  </div>
</el-form-item>

🎯 推荐方案:混合模式

理由

  1. 兼顾安全与灵活

    • 默认使用预设,避免错误
    • 允许高级用户自定义
  2. 渐进式扩展

    • 初期只有 MeshSeed 同步
    • 后续可增加其他用途
  3. 用户体验好

    • 普通用户选预设即可
    • 专家用户可以深度定制

🔧 完整实现(混合模式)

后端验证逻辑

// internal/service/ddns_usage.go

// CreateUsage 创建 DDNS 使用方式
func (s *DDNSService) CreateUsage(ctx context.Context, req CreateUsageRequest) (*model.DDNSUsage, error) {
    // 1. 验证 DDNS 服务存在
    var service model.ExternalService
    if err := s.db.First(&service, req.ServiceID).Error; err != nil {
        return nil, fmt.Errorf("DDNS 服务不存在")
    }
    
    // 2. 根据模式验证前缀
    var recordPrefix string
    if req.PrefixMode == "preset" {
        // 预设模式:验证类型合法性
        switch req.PrefixType {
        case "MESHSEED_SYNC":
            recordPrefix = "_meshray._mesh"
        default:
            return nil, fmt.Errorf("未知的预设类型")
        }
    } else if req.PrefixMode == "custom" {
        // 自定义模式:严格验证格式
        if err := validateCustomPrefix(req.RecordPrefix); err != nil {
            return nil, err
        }
        recordPrefix = req.RecordPrefix
    } else {
        return nil, fmt.Errorf("无效的前缀模式")
    }
    
    // 3. 解析域名
    var config map[string]interface{}
    json.Unmarshal([]byte(service.Config), &config)
    domain, _ := config["domain"].(string)
    if domain == "" {
        return nil, fmt.Errorf("DDNS 配置缺少域名")
    }
    
    // 4. 创建 Usage
    usage := &model.DDNSUsage{
        ID:           generateUUID(),
        ServiceID:    req.ServiceID,
        PrefixMode:   req.PrefixMode,
        PrefixType:   req.PrefixType,
        RecordPrefix: recordPrefix,
        FullDomain:   fmt.Sprintf("%s.%s", recordPrefix, domain),
        Enabled:      true,
    }
    
    // 5. 检查是否重复(同一域名 + 前缀组合)
    var count int64
    s.db.Model(&model.DDNSUsage{}).
        Where("service_id = ? AND record_prefix = ? AND network_id IS NOT NULL",
            req.ServiceID, recordPrefix).
        Count(&count)
    
    if count > 0 {
        return nil, fmt.Errorf("该前缀已被其他组网占用")
    }
    
    // 6. 保存
    if err := s.db.Create(usage).Error; err != nil {
        return nil, fmt.Errorf("创建失败:%w", err)
    }
    
    return usage, nil
}

// 自定义前缀验证
func validateCustomPrefix(prefix string) error {
    if prefix == "" {
        return fmt.Errorf("前缀不能为空")
    }
    
    if len(prefix) > 253 {
        return fmt.Errorf("前缀过长")
    }
    
    // DNS 标签规范
    if !regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).MatchString(prefix) {
        return fmt.Errorf("前缀只能包含字母、数字、点、下划线和连字符")
    }
    
    // 保留前缀检查
    if strings.HasPrefix(prefix, "_meshray.") && prefix != "_meshray._mesh" {
        return fmt.Errorf("_meshray.* 前缀为系统保留")
    }
    
    return nil
}

前端完整表单

<!-- CreateUsageDialog.vue -->
<template>
  <el-dialog title="创建 DDNS 使用方式" v-model="visible">
    <el-form :model="form" label-width="120px">
      
      <!-- 选择 DDNS 配置 -->
      <el-form-item label="DDNS 服务" required>
        <el-select v-model="form.service_id" filterable style="width: 100%">
          <el-option
            v-for="svc in ddnsServices"
            :key="svc.id"
            :label="`${svc.name} (${svc.config.domain})`"
            :value="svc.id"
          />
        </el-select>
      </el-form-item>
      
      <!-- 前缀模式 -->
      <el-form-item label="前缀模式" required>
        <el-radio-group v-model="form.prefix_mode">
          <el-radio label="preset">
             系统预设
            <span class="radio-desc">推荐使用安全可靠</span>
          </el-radio>
          <el-radio label="custom">
            🔧 自定义
            <span class="radio-desc">高级选项需谨慎填写</span>
          </el-radio>
        </el-radio-group>
      </el-form-item>
      
      <!-- 预设类型 -->
      <el-form-item v-if="form.prefix_mode === 'preset'" label="预设类型" required>
        <el-select v-model="form.prefix_type" style="width: 100%">
          <el-option 
            label="✨ MeshSeed 同步 (_meshray._mesh)" 
            value="MESHSEED_SYNC" 
          >
            <div style="display: flex; justify-content: space-between;">
              <span>MeshSeed 同步</span>
              <el-tag size="small" type="success">推荐</el-tag>
            </div>
            <div class="option-desc">用于组网配置加密同步到 DNS TXT 记录</div>
          </el-option>
          <el-option 
            label="🔧 配置同步 (_meshray.config)" 
            value="CONFIG_SYNC"
            disabled
          >
            <div class="option-desc">即将支持敬请期待</div>
          </el-option>
        </el-select>
      </el-form-item>
      
      <!-- 自定义前缀 -->
      <el-form-item v-else label="TXT 记录前缀" required>
        <el-input
          v-model="form.record_prefix"
          placeholder="例如:my.custom.prefix"
          maxlength="253"
          show-word-limit
        />
        <div class="form-tip">
          <el-icon><InfoFilled /></el-icon>
          完整记录名{{ form.record_prefix }}.网络名称域名
        </div>
        <div class="form-tip">
          <el-icon><WarningFilled /></el-icon>
          只能包含字母数字下划线和连字符
        </div>
        <div class="form-tip">
          <el-icon><WarningFilled /></el-icon>
          _meshray.* 前缀为系统保留不能使用
        </div>
      </el-form-item>
      
      <!-- 用途说明 -->
      <el-form-item label="用途说明">
        <el-input
          v-model="form.description"
          type="textarea"
          :rows="2"
          placeholder="描述这个用途,如:办公网络 MeshSeed 同步"
        />
      </el-form-item>
      
    </el-form>
    
    <template #footer>
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit">创建</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
const form = ref({
  service_id: '',
  prefix_mode: 'preset',
  prefix_type: 'MESHSEED_SYNC',
  record_prefix: '',
  description: ''
})

const handleSubmit = async () => {
  try {
    await createDDNSUsage(form.value)
    ElMessage.success('创建成功')
    emit('success')
    visible.value = false
  } catch (error: any) {
    ElMessage.error('创建失败:' + error.message)
  }
}
</script>

📊 实际应用示例

场景 1: 标准企业用户(使用预设)

公司 IT 管理员配置:
1. 添加 DDNS 服务
   └─ Cloudflare + mesh.company.com
   
2. 创建 Usage(预设模式)
   └─ 类型:MeshSeed 同步 (_meshray._mesh)
   
3. 创建组网 A
   └─ 选择 Usage → _meshray._mesh.office.mesh.company.com
   
4. 创建组网 B
   └─ 选择 Usage → _meshray._mesh.dev.mesh.company.com
   
结果:
✅ 自动分配不同子域名
✅ 不会冲突
✅ 管理简单

场景 2: 高级用户(自定义前缀)

技术专家配置:
1. 添加 DDNS 服务
   └─ Cloudflare + example.com
   
2. 创建 Usage(自定义模式)
   └─ 前缀:prod.meshray.sync
   
3. 创建生产环境组网
   └─ 选择 Usage → prod.meshray.sync.prod-net.example.com
   
4. 创建第二个 Usage
   └─ 前缀:test.meshray.sync
   
5. 创建测试环境组网
   └─ 选择 Usage → test.meshray.sync.test-net.example.com
   
结果:
✅ 环境隔离清晰
✅ 命名规范自主定义
✅ 灵活性极高

最终建议

采用混合模式:

  1. 默认引导用户使用预设 (90% 场景)

    • MeshSeed 同步专用前缀:_meshray._mesh
    • 安全、标准、无需思考
  2. 提供自定义入口 (10% 高级场景)

    • 严格验证格式
    • 保留系统前缀
    • 防止冲突
  3. 未来扩展预留

    • _meshray.config - 配置同步
    • _meshray.device - 设备注册
    • _meshray.log - 日志投递

这样既保证了大多数用户的易用性,又满足了高级用户的灵活性需求!🎯

需要我立即开始实现吗?