feat: enhance DoH profile management and resolver policy in modules
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 1m2s
CI / go (push) Successful in 27s
CI / docker-web (push) Successful in 1m27s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Successful in 8m5s

- Added `DohResolverPolicy` schema to OpenAPI documentation, defining policies for domain resolution.
- Updated module handling to support multiple DoH profiles via `doh_profile_ids` and introduced `doh_resolver_policy` in the API.
- Refactored related functions to accommodate the new DoH profile structure, ensuring backward compatibility with existing `doh_profile_id`.
- Enhanced UI components to allow selection and management of DoH profiles and policies in the web interface.
- Updated database interactions to handle new fields and ensure proper data normalization.
This commit is contained in:
Denozordec
2026-05-19 14:57:50 +07:00
parent a536a2d5eb
commit 34ecc5c235
21 changed files with 810 additions and 114 deletions
+2
View File
@@ -114,6 +114,8 @@ type ModulePatch struct {
CronExpr *string `json:"cron_expr,omitempty"`
DefaultCommunityID *string `json:"default_community_id,omitempty"`
DohProfileID *string `json:"doh_profile_id,omitempty"`
DohProfileIDs *[]string `json:"doh_profile_ids,omitempty"`
DohResolverPolicy *string `json:"doh_resolver_policy,omitempty"`
LastRefreshedAt *time.Time `json:"-"`
}
+110
View File
@@ -0,0 +1,110 @@
package store
import "strings"
const (
DohPolicyPrimaryOnly = "primary_only"
DohPolicyFailover = "failover"
DohPolicyUnion = "union"
)
// NormalizeDohResolverPolicy returns a supported resolver policy name.
func NormalizeDohResolverPolicy(policy string) string {
switch strings.TrimSpace(policy) {
case DohPolicyFailover, DohPolicyUnion:
return strings.TrimSpace(policy)
default:
return DohPolicyPrimaryOnly
}
}
// NormalizeDohProfileIDList deduplicates profile ids preserving order.
func NormalizeDohProfileIDList(ids []string) []string {
if len(ids) == 0 {
return nil
}
seen := make(map[string]struct{}, len(ids))
out := make([]string, 0, len(ids))
for _, id := range ids {
id = strings.TrimSpace(id)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
if len(out) == 0 {
return nil
}
return out
}
// EffectiveDohProfileIDs returns ordered DoH profile ids for a module.
func (m *Module) EffectiveDohProfileIDs() []string {
if m == nil {
return nil
}
if ids := NormalizeDohProfileIDList(m.DohProfileIDs); len(ids) > 0 {
return ids
}
if m.DohProfileID != nil {
if id := strings.TrimSpace(*m.DohProfileID); id != "" {
return []string{id}
}
}
return nil
}
// SyncLegacyDohProfileID keeps deprecated doh_profile_id aligned with the first profile.
func (m *Module) SyncLegacyDohProfileID() {
if m == nil {
return
}
ids := NormalizeDohProfileIDList(m.DohProfileIDs)
if len(ids) == 0 {
m.DohProfileID = nil
return
}
first := ids[0]
m.DohProfileID = &first
}
// NormalizeModuleDoh fills doh_profile_ids, policy and legacy id from module input.
func NormalizeModuleDoh(m *Module) {
if m == nil {
return
}
ids := NormalizeDohProfileIDList(m.DohProfileIDs)
if len(ids) == 0 && m.DohProfileID != nil {
if id := strings.TrimSpace(*m.DohProfileID); id != "" {
ids = []string{id}
}
}
m.DohProfileIDs = ids
m.DohResolverPolicy = NormalizeDohResolverPolicy(m.DohResolverPolicy)
m.SyncLegacyDohProfileID()
}
// ApplyModuleDohPatch merges DoH-related fields from patch into mod.
func ApplyModuleDohPatch(mod *Module, patch *ModulePatch) {
if mod == nil || patch == nil {
return
}
if patch.DohProfileIDs != nil {
mod.DohProfileIDs = NormalizeDohProfileIDList(*patch.DohProfileIDs)
} else if patch.DohProfileID != nil {
v := strings.TrimSpace(*patch.DohProfileID)
if v == "" {
mod.DohProfileIDs = nil
} else {
mod.DohProfileIDs = []string{v}
}
}
if patch.DohResolverPolicy != nil {
mod.DohResolverPolicy = NormalizeDohResolverPolicy(*patch.DohResolverPolicy)
}
mod.SyncLegacyDohProfileID()
}
+32
View File
@@ -0,0 +1,32 @@
package store
import "testing"
func TestNormalizeDohProfileIDList(t *testing.T) {
got := NormalizeDohProfileIDList([]string{" a ", "b", "a", "", "b"})
if len(got) != 2 || got[0] != "a" || got[1] != "b" {
t.Fatalf("unexpected: %v", got)
}
}
func TestApplyModuleDohPatch(t *testing.T) {
mod := &Module{DohProfileIDs: []string{"one"}, DohResolverPolicy: DohPolicyPrimaryOnly}
ids := []string{"ru", "eu"}
policy := DohPolicyUnion
ApplyModuleDohPatch(mod, &ModulePatch{DohProfileIDs: &ids, DohResolverPolicy: &policy})
if len(mod.DohProfileIDs) != 2 || mod.DohResolverPolicy != DohPolicyUnion {
t.Fatalf("unexpected module doh fields: %+v", mod)
}
if mod.DohProfileID == nil || *mod.DohProfileID != "ru" {
t.Fatalf("legacy id not synced: %+v", mod.DohProfileID)
}
}
func TestEffectiveDohProfileIDs_LegacyField(t *testing.T) {
id := "legacy-id"
mod := &Module{DohProfileID: &id}
got := mod.EffectiveDohProfileIDs()
if len(got) != 1 || got[0] != "legacy-id" {
t.Fatalf("unexpected: %v", got)
}
}
+3 -1
View File
@@ -72,7 +72,9 @@ type Module struct {
CronExpr string // optional cron for scheduler (display / future use)
Priority int
DefaultCommunityID *string
DohProfileID *string
DohProfileID *string // deprecated: first id in DohProfileIDs
DohProfileIDs []string
DohResolverPolicy string
LastRefreshedAt *time.Time
DeletedAt *time.Time
}
+11 -11
View File
@@ -31,9 +31,11 @@ func (m *Memory) CreateModule(tenantID string, in *Module) (*Module, error) {
RefreshIntervalSec: in.RefreshIntervalSec,
CronExpr: in.CronExpr,
DefaultCommunityID: in.DefaultCommunityID,
DohProfileID: in.DohProfileID,
DohProfileIDs: append([]string(nil), in.DohProfileIDs...),
DohResolverPolicy: in.DohResolverPolicy,
LastRefreshedAt: in.LastRefreshedAt,
}
NormalizeModuleDoh(mod)
m.modules[id] = mod
return mod, nil
}
@@ -71,14 +73,7 @@ func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*M
mod.DefaultCommunityID = &v
}
}
if patch.DohProfileID != nil {
v := strings.TrimSpace(*patch.DohProfileID)
if v == "" {
mod.DohProfileID = nil
} else {
mod.DohProfileID = &v
}
}
ApplyModuleDohPatch(mod, patch)
if patch.LastRefreshedAt != nil {
t := patch.LastRefreshedAt.UTC()
mod.LastRefreshedAt = &t
@@ -555,8 +550,13 @@ func (m *Memory) DeleteDohProfile(tenantID, id string) error {
return ErrNotFound
}
for _, mod := range m.modules {
if mod.DohProfileID != nil && *mod.DohProfileID == id {
return ErrInvalidInput
if mod == nil || mod.DeletedAt != nil {
continue
}
for _, pid := range mod.EffectiveDohProfileIDs() {
if pid == id {
return ErrInvalidInput
}
}
}
delete(m.dohProfiles, id)