baseline: api+gui traffic mode + candidates picker

Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
This commit is contained in:
beckline
2026-02-14 15:32:25 +03:00
parent 50e2999cad
commit 10a10f44a8
55 changed files with 16488 additions and 0 deletions

View File

@@ -0,0 +1,565 @@
Ок, давай разберём по двум пунктам:
Финальные имена и что именно править в коде
Почему стало ~590 IP вместо привычных ~1700
1. Имена / где что менять
Чтобы не было зоопарка, фиксируем одну схему:
внутренний конфиг резолвера:
// types.go
type DNSMode string
const (
DNSModeDirect DNSMode = "direct" // ходим напрямую на default/meta
DNSModeSmartOnly DNSMode = "smartdns" // всё через SmartDNS
)
type dnsConfig struct {
Default []string `json:"default"`
Meta []string `json:"meta"`
SmartDNS string `json:"smartdns"`
Mode DNSMode `json:"mode"`
}
Важно: одно имя — dnsConfig, одно поле режима — Mode.
Никаких ViaSmartDNS, ViaSmartDnS и т.п. только Mode.
1.1. loadDNSConfig (используется резолвером)
Внутри dns_settings.go (или там, где у тебя сейчас чтение /etc/selective-vpn/dns-upstreams.conf) логика должна отдавать именно такой dnsConfig:
func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
cfg := dnsConfig{
Default: []string{defaultDNS1, defaultDNS2},
Meta: []string{defaultMeta1, defaultMeta2},
SmartDNS: "",
Mode: DNSModeDirect,
}
data, err := os.ReadFile(path)
if err != nil {
// файла нет просто дефолты
return cfg
}
var def, meta []string
mode := DNSModeDirect
smart := ""
lines := strings.Split(string(data), "\n")
for _, ln := range lines {
s := strings.TrimSpace(ln)
if s == "" || strings.HasPrefix(s, "#") {
continue
}
parts := strings.Fields(s)
if len(parts) < 2 {
continue
}
key := strings.ToLower(parts[0])
vals := parts[1:]
switch key {
case "default":
def = append(def, vals...)
case "meta":
meta = append(meta, vals...)
case "smartdns":
if len(vals) > 0 {
smart = vals[0]
}
case "mode":
switch DNSMode(vals[0]) {
case DNSModeSmartOnly:
mode = DNSModeSmartOnly
default:
mode = DNSModeDirect
}
}
}
if len(def) > 0 {
cfg.Default = def
}
if len(meta) > 0 {
cfg.Meta = meta
}
if smart != "" {
cfg.SmartDNS = smart
}
cfg.Mode = mode
if logf != nil {
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
logf("dns-config: mode=smartdns smartdns=%s", cfg.SmartDNS)
} else {
logf("dns-config: mode=direct default=%v meta=%v", cfg.Default, cfg.Meta)
}
}
return cfg
}
1.2. runResolverJob — как он выбирает режим
Твой кусок:
cfg := loadDNSConfig(opts.DNSConfigPath, logf)
if logf != nil {
if cfg.ViaSmartDNS {
logf("resolver dns mode: SmartDNS-only (%v)", cfg.Default)
} else {
logf("resolver dns mode: direct default=%v meta=%v", cfg.Default, cfg.Meta)
}
}
Нужно заменить на:
cfg := loadDNSConfig(opts.DNSConfigPath, logf)
if logf != nil {
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
logf("resolver dns mode: SmartDNS-only (%s)", cfg.SmartDNS)
} else {
logf("resolver dns mode: direct default=%v meta=%v", cfg.Default, cfg.Meta)
}
}
1.3. Как резолвер реально ходит в DNS
В resolveHostGo сейчас у тебя примерно так (я по смыслу):
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, logf func(string, ...any)) ([]string, int) {
useMeta := false
for _, m := range metaSpecial {
if host == m {
useMeta = true
break
}
}
dnsList := cfg.Default
if useMeta {
dnsList = cfg.Meta
}
ips, errs := digA(host, dnsList, 3*time.Second, logf)
...
}
Делаем так, чтобы при включённом SmartDNS всегда шли только на него:
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, logf func(string, ...any)) ([]string, int) {
useMeta := false
for _, m := range metaSpecial {
if host == m {
useMeta = true
break
}
}
var dnsList []string
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
// ВСЁ через SmartDNS
dnsList = []string{cfg.SmartDNS}
} else if useMeta {
dnsList = cfg.Meta
} else {
dnsList = cfg.Default
}
ips, errs := digA(host, dnsList, 3*time.Second, logf)
...
}
И для PTR-запросов (resolveStaticLabels → digPTR) — аналогично:
func resolveStaticLabels(entries [][3]string, cfg dnsConfig, ptrCache map[string]any, ttl int, logf func(string, ...any)) (map[string][]string, int, int) {
...
dnsForPtr := ""
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
dnsForPtr = cfg.SmartDNS
} else if len(cfg.Default) > 0 {
dnsForPtr = cfg.Default[0]
} else {
dnsForPtr = defaultDNS1
}
...
}
1.4. routes_update.go — что именно должно быть
Там, где запускается резолвер (что-то вроде runResolverJob), должно быть только это (без своих конфигов):
opts := ResolverOpts{
DomainsPath: domainDir + "/bases.txt",
MetaPath: domainDir + "/meta-special.txt",
StaticPath: staticIPsFile,
CachePath: stateDir + "/resolver-cache.json",
PtrCachePath: stateDir + "/resolver-ptr-cache.json",
TraceLog: traceLogPath,
TTL: 24 * 3600, // или твой config.ResolverTTLSeconds
Workers: 200, // или config.ResolverWorkers
DNSConfigPath: dnsUpstreamsConf, // ВАЖНО: один путь, один формат
}
res, err := runResolverJob(opts, logf)
Никаких других структур / полей для DNS в этом файле не нужно.
Вся магия DNS живёт в dns_settings.go + resolver.go.
2. Почему теперь 590 IP, а не 1700
Смотрим на лог:
summary: domains=14864, cache_hits=2 resolved_now=1281 unresolved=13581 static_entries=294 ...
unique_ips=590 ...
Это говорит о трёх вещах:
Резолвер реально прошёлся по всем ~15k доменам.
Ответы он получил только по ~1281 домену.
Из них после фильтра isPrivateIPv4 осталось 590 уникальных публичных IP.
Что поменялось по сравнению с прошлой версией:
2.1. Всё теперь идёт ТОЛЬКО через SmartDNS
Раньше схема была примерно:
резолвер → напрямую на 94.140.* + 46.243.*
SmartDNS жил своей жизнью как локальный помощник
Сейчас при Use SmartDNS for resolver:
резолвер → только на 127.0.0.1#6053
SmartDNS → дальше сам ходит на 192.168.50.10:53 (bind) → AdGuard DNS и т.д.
Поэтому:
все блокировки / NXDOMAIN / подмена на 0.0.0.0 / 127.0.0.1 происходят до нашего Go-кода;
Go-резолвер видит либо нормальный внешний IP, либо 0.0.0.0/локалку/ошибку.
2.2. Мы фильтруем все приватные и мусорные IP
В runResolverJob:
if len(rawIps) > 0 && ts > 0 && now-int(ts) <= ttl {
for _, v := range rawIps {
if s, ok := v.(string); ok && s != "" && !isPrivateIPv4(s) {
ips = append(ips, s)
}
}
}
...
if isPrivateIPv4(ip) {
continue
}
А isPrivateIPv4 считает приватными:
0.x.x.x
10.x.x.x
127.x.x.x
192.168.x.x
172.1631.x.x
и вообще всё, что невалидное
То есть:
если AdGuard/SmartDNS возвращает 0.0.0.0 или 127.0.0.1 для трекеров / рекламы,
мы их больше не учитываем вообще;
раньше, когда ходили мимо SmartDNS, таких «мусорных» ответов было гораздо меньше.
Отсюда очень понятная картина:
1700 IP ранее = реальные IP + всякий мусор (0.0.0.0, локалки, возможно ещё что-то);
590 IP сейчас = только реальные публичные адреса, которые реально нужно засунуть в ipset и гонять через VPN.
2.3. Что с ошибками в логах вида ... on 192.168.50.10:53
Это просто текст ошибки из SmartDNS/bind, который пробрасывается наружу:
ptr error 104.26.10.38 via 127.0.0.1:6053: lookup 38.10.26.104.in-addr.arpa. on 192.168.50.10:53: no such host
мы обращаемся к 127.0.0.1:6053;
SmartDNS дальше ходит на 192.168.50.10:53 и говорит «no such host»;
net.Resolver формирует такое красивое сообщение.
То есть Go-код SmartDNS не обходит, всё как раз правильно идёт через него.
Если хочешь «как раньше» по цифрам
Если прям хочется снова видеть ~1700 IP (включая мусор):
Можно временно ослабить фильтр:
func isPrivateIPv4(ip string) bool {
// В тесте можно не резать 0.0.0.0 / 127.x.x.x и т.п.,
// просто вернуть false и посмотреть цифры
return false
}
Или сделать две метрики:
unique_ips_total — с учётом всего, что вернул DNS;
unique_ips_public — после фильтра приватных;
и логировать обе.
Но с точки зрения селективных маршрутов тебе как раз нужны только публичные IP, так что текущие 590 очень вероятно ближе к «правильной» картине.
Если хочешь, в следующем шаге могу:
дать патч для логов, чтобы в summary было:
unique_ips_public, unique_ips_private, blocked_0_ips и т.д.,
плюс подсветить в Trace вкладке отдельной строкой, сколько доменов SmartDNS убил в ноль.
в том то и дело, что теперь половина доменов которые железно работали, не работают теперь, что то изменилось, не понимаю что, у нас кеш пишется? что же не так ... и кстати логи по прежнему не верно отображается
Перестала думать
в том то и дело, что теперь половина доменов которые железно работали, не работают теперь, что то изменилось, не понимаю что, у нас кеш пишется? что же не так ... и кстати логи по прежнему не верно отображается
Думал на протяжении 2m 20s
Окей, давай по порядку, что видно из скрина логов, и что мы реально поменяли в коде.
⚠️ Сначала техническая ремарка
Часть файлов, которые ты раньше заливал (resolver.go, config.go и т.д.), у меня уже недоступны система их подчищает со временем. Я помню общую структуру и последние куски, которые ты присылал в текст, но если захочешь, чтобы я дал прям точные патчи построчно по актуальному resolver.go / dns_settings.go, лучше закинуть их ещё раз.
1. Что сейчас не так по логам
На твоём скрине снизу была строчка типа:
dns_errors=13295 unresolved=13581 unique_ips=590 ...
И вверху куча:
dns warn www.twitterinc.com via 127.0.0.1:6053:
lookup www.twitterinc.com on 192.168.50.53:53: i/o timeout
...
dns warn www.twitterstat.us via 127.0.0.1:6053:
dial udp 127.0.0.1:6053: i/o timeout
Это говорит о простом:
Домены в большинстве случаев не резолвятся вообще.
Не из-за кэша, а из-за того, что SmartDNS/BIND/AdGuard под нагрузкой просто не отвечает (таймауты).
Из-за этого у нас:
resolved_now маленький
dns_errors огромный
unique_ips ≈ 500+, хотя раньше было ≈ 1700.
То есть лог честный: с точки зрения нашего Go-резолвера для этих доменов реально "no IPs".
2. Что именно изменилось в логике DNS
До рефакторинга:
наш resolver.go бился напрямую в default1/default2/meta1/meta2 (типа 94.140.14.15 и т.д.);
SmartDNS жил сам по себе и занимался вайлдкарт-ipsetами.
После рефакторинга (который мы как раз и делали по твоей просьбе):
если отмечено «Use SmartDNS for resolver» — мы:
подменяем cfg.Default = []string{ smartdnsAddr } (127.0.0.1#6053);
и всё резолвим через SmartDNS;
параллельно мы оставили очень агрессивный пул: workers = 200, domains ≈ 15k.
И получается ситуация:
15 000 доменов × 200 воркеров × таймаут 3 секунды → местный SmartDNS + BIND просто не успевает, начинает дропать/таймаутить, мы видим кучу i/o timeout, и в итоге получаем только ~590 IP.
То есть сломали не кэш, а заддосили свой же локальный resolver 😅
3. Кэш жив он или нет?
Да, по коду (который ты кидал):
domainCache := loadJSONMap(opts.CachePath)
ptrCache := loadJSONMap(opts.PtrCachePath)
...
domainCache[host] = map[string]any{
"ips": r.ips,
"last_resolved": now,
}
...
res.DomainCache = domainCache
res.PtrCache = ptrCache
...
saveJSON(res.DomainCache, opts.CachePath)
saveJSON(res.PtrCache, opts.PtrCachePath)
Кэш:
читается в начале (loadJSONMap);
обновляется только для успешных доменов;
сохраняется в конце.
Проблема в другом: когда у тебя за один прогон 13k DNS-ошибок, в кэш почти нечего писать — большая часть доменов вообще не получила IP, и в следующий прогон они снова идут "с нуля".
4. Что я бы сделал сейчас в коде (минимально и по делу)
4.1. Ограничить параллелизм при работе через SmartDNS
В runResolverJob (там, где ты сейчас считаешь workers), сделай так:
workers := opts.Workers
if workers <= 0 {
workers = 200
}
// если работаем через SmartDNS не лупим по нему 200 горутин
if cfg.ViaSmartDNS && workers > 50 {
workers = 50
}
// safety clamps
if workers < 1 {
workers = 1
}
if workers > 500 {
workers = 500
}
if logf != nil {
logf("resolver start: domains=%d ttl=%ds workers=%d viaSmartDNS=%v",
len(domains), ttl, workers, cfg.ViaSmartDNS)
}
Суть:
при прямых DNS — всё как раньше (200 воркеров нормально жрут 94.140.*);
при SmartDNS — ставим жёсткий потолок, например 50 (можно 30-80 поэкспериментировать).
Это радикально уменьшит количество одновременных запросов к SmartDNS/BIND, и таймаутов станет сильно меньше → dns_errors упадут, unique_ips вырастут.
4.2. (Опционально) Увеличить таймаут для SmartDNS
Сейчас digA вызывается так (по прошлому коду):
ips, errs := digA(host, dnsList, 3*time.Second, logf)
Если хочешь мягко подстраховаться:
timeout := 3 * time.Second
if cfg.ViaSmartDNS {
timeout = 5 * time.Second
}
ips, errs := digA(host, dnsList, timeout, logf)
Для этого нужно просто протащить cfg или флажок viaSmartDNS до resolveHostGo:
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, viaSmartDNS bool, logf func(string, ...any)) ([]string, int) {
...
timeout := 3 * time.Second
if viaSmartDNS {
timeout = 5 * time.Second
}
ips, errs := digA(host, dnsList, timeout, logf)
...
}
И в runResolverJob вызывать:
ips, errs := resolveHostGo(j.host, cfg, metaSpecial, cfg.ViaSmartDNS, logf)
Это не обязательно, но в связке с уменьшением workers даст более стабильную картину.
5. Почему лог «неверно отображается»
Тут важно разделить:
То, что реально делает наш резолвер.
Если он пишет:
dns warn www.twitterinc.com via 127.0.0.1:6053: dial udp 127.0.0.1:6053: i/o timeout
это значит, что конкретно этот запрос реально упал по таймауту.
Даже если "в браузере всё открывается", это уже другие запросы, в другое время и с другим паттерном (не 15k доменов за раз).
Фильтрация на вкладке Events/SmartDNS/Full.
Мы уже делали фильтр для mode=gui, чтобы убрать:
[routes], resolver, dnstt, smartdns, nft add element, cache hit и т.п. шум.
Если сейчас в режиме Events ты всё равно видишь этот трэш с [routes] dns warn ... — значит, в handleTraceJSON в ветке case "gui": надо ещё сильнее ужать фильтр, например:
if strings.HasPrefix(ll, "[routes]") ||
strings.Contains(ll, " dns warn ") ||
strings.Contains(ll, " dns error ") {
continue
}
Тогда:
весь подробный DNS-дебаг остаётся в Full и SmartDNS;
Events показывает только человеческие штуки [gui], [login], [vpn], [error] и т.д.
Если хочешь, чтобы я точно подогнал фильтр под твой текущий trace_handlers.go, просто перекинь файл ещё раз — старая версия у меня уже протухла.
6. Что бы я сделал дальше по шагам
Внести правку с ограничением workers при ViaSmartDNS.
(Опционально) Протянуть флаг viaSmartDNS в resolveHostGo и увеличить таймаут до 5s.
Запустить update-selective-routes и посмотреть:
строку summary (особенно dns_errors и unique_ips);
стало ли больше IP (должно быть ближе к твоим старым ~1700, хотя не обязательно 1:1).
Если с IP всё норм, но Events всё ещё замусорен подправить фильтр в handleTraceJSON (mode = "gui").
Если хочешь, чтобы я прям по твоим актуальным файлам выписал патчи diffом (resolver.go, dns_settings.go, trace_handlers.go) скинь их ещё раз, я аккуратно вставлю всё в нужные места без переименования функций и с теми же структурами, что уже есть.