dns: switch to active upstream pool and wave fallback behavior

This commit is contained in:
beckline
2026-02-22 19:15:37 +03:00
parent a7ec4fe801
commit 0f88cfeeaa
9 changed files with 382 additions and 130 deletions

View File

@@ -31,11 +31,14 @@ const (
routesCacheIPs = stateDir + "/routes-clear-cache-ips.txt"
routesCacheDyn = stateDir + "/routes-clear-cache-ips-dyn.txt"
routesCacheMap = stateDir + "/routes-clear-cache-ips-map.txt"
routesCacheMapD = stateDir + "/routes-clear-cache-ips-map-direct.txt"
routesCacheMapW = stateDir + "/routes-clear-cache-ips-map-wildcard.txt"
routesCacheRT = stateDir + "/routes-clear-cache-routes.txt"
autoloopLogPath = stateDir + "/adguard-autoloop.log"
loginStatePath = stateDir + "/adguard-login.json"
dnsUpstreamsPath = stateDir + "/dns-upstreams.json"
dnsUpstreamPool = stateDir + "/dns-upstream-pool.json"
smartdnsWLPath = stateDir + "/smartdns-wildcards.json"
smartdnsRTPath = stateDir + "/smartdns-runtime.json"
desiredLocation = stateDir + "/adguard-location.txt"
@@ -64,6 +67,7 @@ const (
// RU: Дополнительные метки для per-app маршрутизации (systemd scope / cgroup).
MARK_DIRECT = "0x67" // force direct (bypass VPN table even in full tunnel)
MARK_APP = "0x68" // force VPN for app-scoped traffic (works even in traffic-mode=direct)
MARK_INGRESS = "0x69" // keep ingress reply-path direct in full tunnel (server-safe)
defaultDNS1 = "94.140.14.14"
defaultDNS2 = "94.140.15.15"
defaultMeta1 = "46.243.231.30"

View File

@@ -56,6 +56,30 @@ func handleDNSUpstreams(w http.ResponseWriter, r *http.Request) {
}
}
func handleDNSUpstreamPool(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
items := loadDNSUpstreamPoolState()
writeJSON(w, http.StatusOK, DNSUpstreamPoolState{Items: items})
case http.MethodPost:
var body DNSUpstreamPoolState
if r.Body != nil {
defer r.Body.Close()
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil && err != io.EOF {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
}
if err := saveDNSUpstreamPoolState(body.Items); err != nil {
http.Error(w, "write error", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, DNSUpstreamPoolState{Items: loadDNSUpstreamPoolState()})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
// ---------------------------------------------------------------------
// EN: `handleDNSStatus` is an HTTP handler for dns status.
// RU: `handleDNSStatus` - HTTP-обработчик для dns status.
@@ -95,13 +119,23 @@ func handleDNSBenchmark(w http.ResponseWriter, r *http.Request) {
upstreams := normalizeBenchmarkUpstreams(req.Upstreams)
if len(upstreams) == 0 {
cfg := loadDNSUpstreamsConf()
upstreams = normalizeBenchmarkUpstreamStrings([]string{
cfg.Default1,
cfg.Default2,
cfg.Meta1,
cfg.Meta2,
})
pool := loadDNSUpstreamPoolState()
if len(pool) > 0 {
tmp := make([]DNSBenchmarkUpstream, 0, len(pool))
for _, item := range pool {
tmp = append(tmp, DNSBenchmarkUpstream{Addr: item.Addr, Enabled: item.Enabled})
}
upstreams = normalizeBenchmarkUpstreams(tmp)
}
if len(upstreams) == 0 {
cfg := loadDNSUpstreamsConf()
upstreams = normalizeBenchmarkUpstreamStrings([]string{
cfg.Default1,
cfg.Default2,
cfg.Meta1,
cfg.Meta2,
})
}
}
if len(upstreams) == 0 {
http.Error(w, "no upstreams", http.StatusBadRequest)
@@ -197,9 +231,6 @@ func normalizeBenchmarkUpstreams(in []DNSBenchmarkUpstream) []string {
out := make([]string, 0, len(in))
seen := map[string]struct{}{}
for _, item := range in {
if !item.Enabled {
continue
}
n := normalizeDNSUpstream(item.Addr, "53")
if n == "" {
continue
@@ -842,11 +873,7 @@ func prewarmAggressiveFromEnv() bool {
}
}
// ---------------------------------------------------------------------
// EN: `loadDNSUpstreamsConf` loads dns upstreams conf from storage or config.
// RU: `loadDNSUpstreamsConf` - загружает dns upstreams conf из хранилища или конфига.
// ---------------------------------------------------------------------
func loadDNSUpstreamsConf() DNSUpstreams {
func loadDNSUpstreamsConfFile() DNSUpstreams {
cfg := DNSUpstreams{
Default1: defaultDNS1,
Default2: defaultDNS2,
@@ -903,11 +930,139 @@ func loadDNSUpstreamsConf() DNSUpstreams {
return cfg
}
// ---------------------------------------------------------------------
// EN: `saveDNSUpstreamsConf` saves dns upstreams conf to persistent storage.
// RU: `saveDNSUpstreamsConf` - сохраняет dns upstreams conf в постоянное хранилище.
// ---------------------------------------------------------------------
func saveDNSUpstreamsConf(cfg DNSUpstreams) error {
func normalizeDNSUpstreamPoolItems(items []DNSUpstreamPoolItem) []DNSUpstreamPoolItem {
out := make([]DNSUpstreamPoolItem, 0, len(items))
seen := map[string]struct{}{}
for _, item := range items {
addr := normalizeDNSUpstream(item.Addr, "53")
if addr == "" {
continue
}
if _, ok := seen[addr]; ok {
continue
}
seen[addr] = struct{}{}
out = append(out, DNSUpstreamPoolItem{
Addr: addr,
Enabled: item.Enabled,
})
}
return out
}
func dnsUpstreamPoolFromLegacy(cfg DNSUpstreams) []DNSUpstreamPoolItem {
raw := []string{cfg.Default1, cfg.Default2, cfg.Meta1, cfg.Meta2}
out := make([]DNSUpstreamPoolItem, 0, len(raw))
for _, item := range raw {
n := normalizeDNSUpstream(item, "53")
if n == "" {
continue
}
out = append(out, DNSUpstreamPoolItem{Addr: n, Enabled: true})
}
return normalizeDNSUpstreamPoolItems(out)
}
func dnsUpstreamPoolToLegacy(items []DNSUpstreamPoolItem) DNSUpstreams {
enabled := make([]string, 0, len(items))
all := make([]string, 0, len(items))
for _, item := range items {
n := normalizeDNSUpstream(item.Addr, "53")
if n == "" {
continue
}
all = append(all, n)
if item.Enabled {
enabled = append(enabled, n)
}
}
list := enabled
if len(list) == 0 {
list = all
}
if len(list) == 0 {
list = []string{defaultDNS1, defaultDNS2, defaultMeta1, defaultMeta2}
}
pick := func(idx int, fallback string) string {
if len(list) == 0 {
return fallback
}
if idx < len(list) {
return list[idx]
}
return list[idx%len(list)]
}
return DNSUpstreams{
Default1: pick(0, defaultDNS1),
Default2: pick(1, defaultDNS2),
Meta1: pick(2, defaultMeta1),
Meta2: pick(3, defaultMeta2),
}
}
func saveDNSUpstreamPoolFile(items []DNSUpstreamPoolItem) error {
state := DNSUpstreamPoolState{Items: normalizeDNSUpstreamPoolItems(items)}
if err := os.MkdirAll(filepath.Dir(dnsUpstreamPool), 0o755); err != nil {
return err
}
tmp := dnsUpstreamPool + ".tmp"
b, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(tmp, b, 0o644); err != nil {
return err
}
return os.Rename(tmp, dnsUpstreamPool)
}
func loadDNSUpstreamPoolState() []DNSUpstreamPoolItem {
data, err := os.ReadFile(dnsUpstreamPool)
if err == nil {
var st DNSUpstreamPoolState
if json.Unmarshal(data, &st) == nil {
items := normalizeDNSUpstreamPoolItems(st.Items)
if len(items) > 0 {
return items
}
}
}
legacy := loadDNSUpstreamsConfFile()
items := dnsUpstreamPoolFromLegacy(legacy)
if len(items) > 0 {
_ = saveDNSUpstreamPoolFile(items)
}
return items
}
func saveDNSUpstreamPoolState(items []DNSUpstreamPoolItem) error {
items = normalizeDNSUpstreamPoolItems(items)
if len(items) == 0 {
items = dnsUpstreamPoolFromLegacy(loadDNSUpstreamsConfFile())
}
if err := saveDNSUpstreamPoolFile(items); err != nil {
return err
}
return saveDNSUpstreamsConfFile(dnsUpstreamPoolToLegacy(items))
}
func loadEnabledDNSUpstreamPool() []string {
items := loadDNSUpstreamPoolState()
out := make([]string, 0, len(items))
for _, item := range items {
if !item.Enabled {
continue
}
n := normalizeDNSUpstream(item.Addr, "53")
if n == "" {
continue
}
out = append(out, n)
}
return uniqueStrings(out)
}
func saveDNSUpstreamsConfFile(cfg DNSUpstreams) error {
cfg.Default1 = normalizeDNSUpstream(cfg.Default1, "53")
cfg.Default2 = normalizeDNSUpstream(cfg.Default2, "53")
cfg.Meta1 = normalizeDNSUpstream(cfg.Meta1, "53")
@@ -947,10 +1102,32 @@ func saveDNSUpstreamsConf(cfg DNSUpstreams) error {
if b, err := json.MarshalIndent(cfg, "", " "); err == nil {
_ = os.WriteFile(dnsUpstreamsPath, b, 0o644)
}
return nil
}
// ---------------------------------------------------------------------
// EN: `loadDNSUpstreamsConf` loads dns upstreams conf from storage or config.
// RU: `loadDNSUpstreamsConf` - загружает dns upstreams conf из хранилища или конфига.
// ---------------------------------------------------------------------
func loadDNSUpstreamsConf() DNSUpstreams {
pool := loadDNSUpstreamPoolState()
if len(pool) > 0 {
return dnsUpstreamPoolToLegacy(pool)
}
return loadDNSUpstreamsConfFile()
}
// ---------------------------------------------------------------------
// EN: `saveDNSUpstreamsConf` saves dns upstreams conf to persistent storage.
// RU: `saveDNSUpstreamsConf` - сохраняет dns upstreams conf в постоянное хранилище.
// ---------------------------------------------------------------------
func saveDNSUpstreamsConf(cfg DNSUpstreams) error {
if err := saveDNSUpstreamsConfFile(cfg); err != nil {
return err
}
return saveDNSUpstreamPoolFile(dnsUpstreamPoolFromLegacy(cfg))
}
// ---------------------------------------------------------------------
// EN: `loadDNSMode` loads dns mode from storage or config.
// RU: `loadDNSMode` - загружает dns mode из хранилища или конфига.

View File

@@ -141,14 +141,9 @@ type wildcardMatcher struct {
suffix []string
}
var resolverFallbackDNS = []string{
"1.1.1.1#53",
"1.0.0.1#53",
"9.9.9.9#53",
"149.112.112.112#53",
"8.8.8.8#53",
"8.8.4.4#53",
}
// Empty by default: primary resolver pool comes from DNS upstream pool state.
// Optional fallback list can still be provided via RESOLVE_DNS_FALLBACKS env.
var resolverFallbackDNS []string
func normalizeWildcardDomain(raw string) string {
d := strings.TrimSpace(strings.SplitN(raw, "#", 2)[0])
@@ -562,14 +557,8 @@ func digA(host string, dnsList []string, timeout time.Duration, logf func(string
if logf != nil {
logf("dns warn %s via %s: kind=%s err=%v", host, addr, kind, err)
}
// NXDOMAIN usually means authoritative negative answer.
// Do not fan out further retries for this host.
if kind == dnsErrorNXDomain {
break
}
continue
}
stats.addSuccess(addr)
var ips []string
for _, ip := range records {
if isPrivateIPv4(ip) {
@@ -577,6 +566,14 @@ func digA(host string, dnsList []string, timeout time.Duration, logf func(string
}
ips = append(ips, ip)
}
if len(ips) == 0 {
stats.addError(addr, dnsErrorOther)
if logf != nil {
logf("dns warn %s via %s: kind=other err=no_public_ips", host, addr)
}
continue
}
stats.addSuccess(addr)
return uniqueStrings(ips), stats
}
return nil, stats
@@ -1036,6 +1033,11 @@ func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
SmartDNS: smartDNSAddr(),
Mode: DNSModeDirect,
}
activePool := loadEnabledDNSUpstreamPool()
if len(activePool) > 0 {
cfg.Default = activePool
cfg.Meta = activePool
}
// 1) Если форсируем SmartDNS — вообще игнорим файл и ходим только через локальный резолвер.
if smartDNSForced() {
@@ -1051,12 +1053,14 @@ func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
return cfg
}
// 2) Иначе пытаемся прочитать dns-upstreams.conf, как и раньше.
// 2) Читаем dns-upstreams.conf для legacy-совместимости и smartdns/mode значений.
data, err := os.ReadFile(path)
if err != nil {
if logf != nil {
logf("dns-config: use built-in defaults, can't read %s: %v", path, err)
logf("dns-config: can't read %s: %v", path, err)
}
cfg.Default = mergeDNSUpstreamPools(cfg.Default, resolverFallbackPool())
cfg.Meta = mergeDNSUpstreamPools(cfg.Meta, resolverFallbackPool())
return cfg
}
@@ -1098,11 +1102,13 @@ func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
}
}
}
if len(def) > 0 {
cfg.Default = def
}
if len(meta) > 0 {
cfg.Meta = meta
if len(activePool) == 0 {
if len(def) > 0 {
cfg.Default = def
}
if len(meta) > 0 {
cfg.Meta = meta
}
}
cfg.Default = mergeDNSUpstreamPools(cfg.Default, resolverFallbackPool())
cfg.Meta = mergeDNSUpstreamPools(cfg.Meta, resolverFallbackPool())

View File

@@ -169,6 +169,7 @@ func Run() {
// DNS upstreams
mux.HandleFunc("/api/v1/dns-upstreams", handleDNSUpstreams)
mux.HandleFunc("/api/v1/dns/upstream-pool", handleDNSUpstreamPool)
mux.HandleFunc("/api/v1/dns/status", handleDNSStatus)
mux.HandleFunc("/api/v1/dns/mode", handleDNSModeSet)
mux.HandleFunc("/api/v1/dns/benchmark", handleDNSBenchmark)

View File

@@ -46,6 +46,15 @@ type DNSUpstreams struct {
Meta2 string `json:"meta2"`
}
type DNSUpstreamPoolItem struct {
Addr string `json:"addr"`
Enabled bool `json:"enabled"`
}
type DNSUpstreamPoolState struct {
Items []DNSUpstreamPoolItem `json:"items"`
}
type DNSResolverMode string
const (