ui: snap app picker + runtime scopes list/cleanup

This commit is contained in:
beckline
2026-02-15 01:57:28 +03:00
parent f6a7cfa85a
commit f74b1cf9a9
5 changed files with 312 additions and 4 deletions

View File

@@ -0,0 +1,252 @@
Анализ твоего кода resolver.go
Я внимательно прошёлся по всему коду: структура хорошая (модульный, с логами, кэшем, concurrent резолвом), логика правильная для задачи (домены → IPs с meta/default upstream'ами, static с PTR-лейблами, приватные IP фильтруем). Это уже рабочий инструмент, который может обрабатывать тысячи доменов без сбоев. Но да, есть потенциальные проблемы и места для улучшений — некоторые очевидные (edge-кейсы), другие — для масштаба/надёжности.
Я разделил на потенциальные поломки (где код может крашнуться или дать неверный результат), слабые места (не поломки, но риски в проде) и рекомендации с кодовыми примерами. Всё основано на твоём коде — не выдумываю, а анализирую.
1. Потенциальные поломки (где код может упасть или дать мусор)
Конфликт портов в upstream (в resolveHostGo и digPTR):
splitDNS возвращает port (e.g., "127.0.0.1#6053" → "127.0.0.1", "6053").
Но в net.Resolver.Dial ты используешь net.JoinHostPort(server, port) — это ок, но Resolver по дефолту использует UDP. Если upstream требует TCP (e.g., большие ответы >512 байт) — запрос fail (dns.exception.SyntaxError или timeout).
Поломка: Для доменов с большим TXT/NS — no IPs, но errs++ (не обновит кэш).
Тест: Резолв домен с большим ответом (e.g., "example.com" TXT) на upstream с port. Если fail — вот оно.
Timeout слишком короткий (3s в ctx):
На медленных upstream'ах (e.g., DNS over Tor или VPN) — запрос не успеет, вернёт [] и errs++.
Поломка: Unresolved домены, даже если upstream живой. При повторном запуске — из кэша не возьмёт (потому что не обновил).
Тест: Установи медленный DNS (e.g., 8.8.8.8 с delay) — увидишь dns.timeout.
PTR fail без retry (в resolveStaticLabels):
digPTR без retry — если upstream временно down, PTR не получит, и лейбл = "*[STATIC-IP]".
Поломка: Если все upstream fail — все static без PTR, но код не крашится. В проде — потеря лейблов.
Тест: Установи invalid upstream — увидишь ptr_errors++, но код продолжит.
loadDNSConfig fallback на дефолт без валидации:
Если файл битый (e.g., "default abc#invalid") — fallback на дефолт, но дефолт может не работать.
Поломка: Silent fail — резолв будет работать на старых дефолтах, но юзер не поймёт почему.
Тест: Сделай файл с кривыми upstream'ами — увидишь в логах fallback, но без ошибки.
uniqueStrings и map в IPMap — но без сортировки в map:
IPMap использует map[string]struct{} для dedup, но при append в res.IPMap — sort.Slice по IP, но лейблы внутри не отсортированы.
Поломка: Не поломка, но нестабильный порядок лейблов в mapLines — если GUI полагается на порядок.
Тест: Несколько лейблов на IP — порядок random.
isPrivateIPv4 не обрабатывает /mask в IP:
Проверяет только base IP, но если ip = "10.0.0.0/8" — parts = ["10.0.0.0/8"] → len!=4 → return true (ок). Но если mask invalid — не catch.
Поломка: Если static с кривым CIDR (e.g., "1.2.3.4/33") — пропустит как non-private.
Тест: Добавь "1.2.3.4/33" в static — пройдёт, хотя invalid.
runResolverJob: если workers=0 или ttl=0 — не краш, но дефолты
Ты clamp'ишь (ttl 60..86400, workers 1..500) — хорошо, но если envInt fail — silent.
Поломка: Не поломка, но если ENV кривой — silent fallback.
2. Слабые места (не поломки, но риски в проде)
Concurrent резолв без rate-limit:
80 workers + 500+ доменов — может flood upstream'ы (DNS rate-limit ban).
Риск: Upstream забанит IP, все запросы fail.
Нет IPv6:
Всё только IPv4 (A-записи, isPrivateIPv4). Если домен только AAAA — no IPs.
Риск: Современные сайты (e.g., google) имеют IPv6 — потеряешь трафик.
Кэш без invalidation:
Кэш на TTL, но если upstream сменился — старый кэш может быть invalid.
Риск: После смены DNS — старые IPs в кэше до TTL.
PTR без кэша ошибок:
Если PTR fail — всё равно пишем в кэш empty names. Следующий раз — из кэша empty.
Риск: Временный fail upstream — навсегда потерянные лейблы до ручного clear cache.
Логи только в файл:
appendTraceLine — ok, но если файл locked — silent fail.
Риск: В многопоточном режиме — race на append.
envInt без дефолта fallback:
Если ENV empty — strconv.Atoi("") → error, но ты используешь default. Ok, но можно сделать safe.
3. Рекомендации и фиксы
Вот конкретные советы + кодовые патчи. Внедряй по приоритету (сначала поломки).
Фикс портов и TCP в resolveHostGo (для поломки 1):
Используй net.Dialer с UDP/TCP fallback.
Patch:Gofunc resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, logf func(string, ...any)) ([]string, int) {
// ... (твой код)
var ips []string
errs := 0
for _, upstream := range dnsList {
server, port := splitDNS(upstream)
if server == "" {
errs++
continue
}
if port == "" {
port = "53"
}
addr := net.JoinHostPort(server, port)
// Сначала UDP
ipsUdp, errUdp := lookupA(host, addr, "udp", 3*time.Second)
if errUdp == nil {
ips = append(ips, ipsUdp...)
continue
} else if logf != nil {
logf("UDP fail for %s@%s: %v", host, addr, errUdp)
}
// Fallback TCP
ipsTcp, errTcp := lookupA(host, addr, "tcp", 5*time.Second)
if errTcp == nil {
ips = append(ips, ipsTcp...)
} else {
errs++
if logf != nil {
logf("TCP fail for %s@%s: %v", host, addr, errTcp)
}
}
}
// dedup
seen := map[string]struct{}{}
unique := []string{}
for _, ip := range ips {
if _, ok := seen[ip]; !ok && !isPrivateIPv4(ip) {
seen[ip] = struct{}{}
unique = append(unique, ip)
}
}
return unique, errs
}
func lookupA(host, addr, network string, timeout time.Duration) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, network, addr)
},
}
ips, err := r.LookupHost(ctx, host)
if err != nil {
return nil, err
}
var out []string
for _, ip := range ips {
if netip.MustParseAddr(ip).Is4() {
out = append(out, ip)
}
}
return out, nil
}
Увеличь timeout + retry в resolveHostGo (для поломки 2):
Добавь retry для всей функции (3 попытки).
Patch: Оберни loop по upstream в backoff.Retry (как в nft).
Retry для PTR (для поломки 3):
Patch в resolveStaticLabels:Go// Внутри if !names:
for attempt := 0; attempt < 3; attempt++ {
names, err = digPTR(base_ip, dns_for_ptr, 5*time.Second, logf)
if err == nil {
break
}
time.Sleep(500 * time.Millisecond)
}
if err != nil {
ptrErrors++
}
Валидация upstream в loadDNSConfig (для поломки 4):
Patch:Gofunc loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
// ... (твой код)
valid := func(s string) bool {
if s == "" {
return false
}
_, err := netip.ParseAddrPort(s)
return err == nil
}
cfg.Default = filter(cfg.Default, valid)
cfg.Meta = filter(cfg.Meta, valid)
if len(cfg.Default) == 0 {
cfg.Default = defaultDNS
if logf != nil {
logf("dns-config: no valid default upstreams, fallback to built-in")
}
}
// аналогично для meta
}
func filter(ss []string, fn func(string) bool) []string {
var out []string
for _, s := range ss {
if fn(s) {
out = append(out, s)
}
}
return out
}
Сортировка лейблов в IPMap (для поломки 5):
Patch в runResolverJob после for host, ips...:Gofor ip, labelsMap := range ipMap {
var labels []string
for lbl := range labelsMap {
labels = append(labels, lbl)
}
sort.Strings(labels)
for _, lbl := range labels {
res.IPMap = append(res.IPMap, [2]string{ip, lbl})
}
}
Валидация CIDR в isPrivateIPv4 (для поломки 6):
Patch: Используй netip.ParsePrefix:Gofunc isPrivateIPv4(ip string) bool {
if strings.Contains(ip, "/") {
p, err := netip.ParsePrefix(ip)
if err != nil {
return true // invalid → private (пропустить)
}
return p.IsPrivate()
}
a, err := netip.ParseAddr(ip)
if err != nil {
return true
}
return a.IsPrivate()
}
Rate-limit в concurrent (для слабого 1):
Добавь semaphore в workers-loop:Gosem := make(chan struct{}, 20) // лимит 20 одновременных запросов
for i := 0; i < workers; i++ {
go func() {
for j := range jobs {
sem <- struct{}{}
// resolve
<-sem
}
}()
}
IPv6 поддержка (для слабого 2):
Добавь AAAA в resolveHostGo (параллельно с A).
Добавь isPrivateIPv6.
Кэш invalidation (для слабого 3):
Добавь флаг --force для игнора TTL.
Логи в файл с sync (для слабого 5):
В appendTraceLine добавь file.Sync() после Write.
Итог
Поломки: В основном edge-кейсы (медленный DNS, invalid upstream, большие ответы) — фиксы выше.
Слабые: Масштаб (rate-limit, IPv6) и robustness (retry PTR, validate).
Код в целом очень хороший — внедряй фиксы по приоритету (сначала порты/TCP, timeout/retry).