baseline: api+gui traffic mode + candidates picker
Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
This commit is contained in:
132
selective-vpn-api/app/smartdns_wildcards_store.go
Normal file
132
selective-vpn-api/app/smartdns_wildcards_store.go
Normal file
@@ -0,0 +1,132 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user