Files
elmprodvpn/selective-vpn-api/инструкция3.txt
beckline 10a10f44a8 baseline: api+gui traffic mode + candidates picker
Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
2026-02-14 15:52:20 +03:00

253 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Анализ твоего кода 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).