253 lines
12 KiB
Plaintext
253 lines
12 KiB
Plaintext
Анализ твоего кода 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).
|