16 KiB
16 KiB
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>
🎯 推荐方案:混合模式
理由
-
兼顾安全与灵活 ✅
- 默认使用预设,避免错误
- 允许高级用户自定义
-
渐进式扩展 ✅
- 初期只有 MeshSeed 同步
- 后续可增加其他用途
-
用户体验好 ✅
- 普通用户选预设即可
- 专家用户可以深度定制
🔧 完整实现(混合模式)
后端验证逻辑
// 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
结果:
✅ 环境隔离清晰
✅ 命名规范自主定义
✅ 灵活性极高
✅ 最终建议
采用混合模式:
-
默认引导用户使用预设 (90% 场景)
- MeshSeed 同步专用前缀:
_meshray._mesh - 安全、标准、无需思考
- MeshSeed 同步专用前缀:
-
提供自定义入口 (10% 高级场景)
- 严格验证格式
- 保留系统前缀
- 防止冲突
-
未来扩展预留
_meshray.config- 配置同步_meshray.device- 设备注册_meshray.log- 日志投递
这样既保证了大多数用户的易用性,又满足了高级用户的灵活性需求!🎯
需要我立即开始实现吗?