133 lines
3.7 KiB
Go
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)
|
|
}
|