Files
elmprodvpn/selective-vpn-api/app/smartdns_wildcards_store.go
beckline 10a10f44a8 baseline: api+gui traffic mode + candidates picker
Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
2026-02-14 15:52:20 +03:00

133 lines
3.7 KiB
Go

package app
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"time"
)
// ---------------------------------------------------------------------
// smartdns wildcard canonical store
// ---------------------------------------------------------------------
// EN: Canonical SmartDNS wildcard storage is JSON in stateDir.
// EN: `/etc/selective-vpn/smartdns.conf` is generated as a runtime artifact.
// RU: Каноничное хранилище wildcard-доменов SmartDNS — JSON в stateDir.
// RU: `/etc/selective-vpn/smartdns.conf` генерируется как runtime-артефакт.
type smartDNSWildcardState struct {
Version int `json:"version"`
UpdatedAt string `json:"updated_at"`
Domains []string `json:"domains"`
}
func normalizeWildcardDomains(raw []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(raw))
for _, ln := range raw {
d := normalizeWildcardDomain(ln)
if d == "" {
continue
}
if _, ok := seen[d]; ok {
continue
}
seen[d] = struct{}{}
out = append(out, d)
}
return out
}
func parseSmartDNSDomainsContent(content string) []string {
return normalizeWildcardDomains(strings.Split(content, "\n"))
}
func renderSmartDNSDomainsContent(domains []string) string {
header := strings.TrimSpace(`
# Auto-generated by selective-vpn API.
# SmartDNS wildcard rules for selective VPN / AGVPN.
`) + "\n"
if len(domains) == 0 {
return header
}
return header + "\n" + strings.Join(domains, "\n") + "\n"
}
func loadSmartDNSWildcardDomainsState(logf func(string, ...any)) ([]string, string) {
if data, err := os.ReadFile(smartdnsWLPath); err == nil {
// preferred shape: object with metadata
var st smartDNSWildcardState
if json.Unmarshal(data, &st) == nil {
domains := normalizeWildcardDomains(st.Domains)
_ = writeSmartDNSDomainsArtifact(domains)
return domains, "state"
}
// backward-compat shape: plain []string
var arr []string
if json.Unmarshal(data, &arr) == nil {
domains := normalizeWildcardDomains(arr)
_ = saveSmartDNSWildcardDomainsState(domains)
return domains, "state-legacy"
}
if logf != nil {
logf("smartdns wildcards: invalid state json at %s, fallback to conf", smartdnsWLPath)
}
}
// migration path: parse legacy .conf file if state json is missing/broken.
confData, err := os.ReadFile(smartdnsDomainsFile)
if err == nil {
domains := parseSmartDNSDomainsContent(string(confData))
if saveErr := saveSmartDNSWildcardDomainsState(domains); saveErr != nil && logf != nil {
logf("smartdns wildcards: migration from conf failed: %v", saveErr)
}
return domains, "migrated-conf"
}
// bootstrap empty canonical state + artifact.
_ = saveSmartDNSWildcardDomainsState(nil)
return nil, "default"
}
func saveSmartDNSWildcardDomainsState(domains []string) error {
normalized := normalizeWildcardDomains(domains)
state := smartDNSWildcardState{
Version: 1,
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
Domains: normalized,
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(smartdnsWLPath), 0o755); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(smartdnsDomainsFile), 0o755); err != nil {
return err
}
stateTmp := smartdnsWLPath + ".tmp"
if err := os.WriteFile(stateTmp, data, 0o644); err != nil {
return err
}
if err := os.Rename(stateTmp, smartdnsWLPath); err != nil {
return err
}
return writeSmartDNSDomainsArtifact(normalized)
}
func writeSmartDNSDomainsArtifact(domains []string) error {
content := renderSmartDNSDomainsContent(domains)
tmp := smartdnsDomainsFile + ".tmp"
if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
return err
}
return os.Rename(tmp, smartdnsDomainsFile)
}