Initial commit
This commit is contained in:
@@ -0,0 +1,601 @@
|
||||
# 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 (设备注册)
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 标准化,避免混乱
|
||||
- ✅ 安全性高(防止恶意前缀)
|
||||
- ✅ 实现简单
|
||||
|
||||
**缺点**:
|
||||
- ❌ 灵活性较低
|
||||
- ❌ 无法适配特殊场景
|
||||
|
||||
**数据库设计**:
|
||||
```go
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**前端实现**:
|
||||
```vue
|
||||
<!-- 创建 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
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 灵活性极高
|
||||
- ✅ 适配各种场景
|
||||
|
||||
**缺点**:
|
||||
- ❌ 容易冲突(需要验证唯一性)
|
||||
- ❌ 安全性风险(可能输入恶意前缀)
|
||||
- ❌ 用户学习成本高
|
||||
|
||||
**数据库设计**:
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
**前端实现**:
|
||||
```vue
|
||||
<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>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案三:混合模式(最佳实践)🏆
|
||||
|
||||
**设计思路**:
|
||||
```
|
||||
系统预设 + 管理员自定义组合
|
||||
├─ 预设类型:快速选择,安全可靠
|
||||
└─ 自定义:高级选项,满足特殊需求
|
||||
```
|
||||
|
||||
**数据库设计**:
|
||||
```go
|
||||
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" // 默认回退
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**前端实现**:
|
||||
```vue
|
||||
<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. **用户体验好** ✅
|
||||
- 普通用户选预设即可
|
||||
- 专家用户可以深度定制
|
||||
|
||||
---
|
||||
|
||||
## 🔧 完整实现(混合模式)
|
||||
|
||||
### 后端验证逻辑
|
||||
|
||||
```go
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 前端完整表单
|
||||
|
||||
```vue
|
||||
<!-- 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` - 日志投递
|
||||
|
||||
这样既保证了大多数用户的易用性,又满足了高级用户的灵活性需求!🎯
|
||||
|
||||
需要我立即开始实现吗?
|
||||
Reference in New Issue
Block a user