ui: snap app picker + runtime scopes list/cleanup
This commit is contained in:
@@ -1,460 +0,0 @@
|
||||
Окей, давай сначала аккуратно разрулим логику, а уже потом можно будет добивать конкретный код (Go + Qt).
|
||||
|
||||
Ты сейчас хочешь:
|
||||
|
||||
Нормальную модель «DNS/SmartDNS» (кто за что отвечает).
|
||||
|
||||
Новую вкладку DNS в дашборде, без текстовой панели, но с понятной индикацией:
|
||||
|
||||
запущен ли smartdns-daemon (systemd unit),
|
||||
|
||||
использует ли именно приложение SmartDNS или ходит напрямую на default/meta.
|
||||
|
||||
Логи пока оставим на второй шаг — главное, чтобы сама схема была ровная.
|
||||
|
||||
1. Предлагаемая модель DNS / SmartDNS
|
||||
1.1. Состояния
|
||||
|
||||
У нас по сути три независимых штуки:
|
||||
|
||||
Upstreams для резолвера selective-vpn
|
||||
|
||||
default1, default2, meta1, meta2
|
||||
|
||||
лежат в dnsUpstreamsConf (как и сейчас).
|
||||
|
||||
SmartDNS systemd unit
|
||||
|
||||
юнит типа smartdns-local.service
|
||||
|
||||
может быть active/inactive/failed.
|
||||
|
||||
Режим работы резолвера selective-vpn
|
||||
|
||||
via_smartdns = false → резолвер ходит напрямую на default/meta.
|
||||
|
||||
via_smartdns = true → резолвер ходит на 127.0.0.1#6053 (SmartDNS), а тот уже сам цепочкой: локальные → bind9 → adguard-dns.
|
||||
|
||||
Важно: юнит SmartDNS и режим резолвера — разные вещи.
|
||||
Может быть включён юнит, но приложение всё равно ходит напрямую на adguard-dns.
|
||||
|
||||
1.2. Где хранить режим
|
||||
|
||||
Вариант попроще (без изменения 100500 мест):
|
||||
|
||||
Заводим отдельный json в stateDir, например
|
||||
dns-mode.json:
|
||||
|
||||
{
|
||||
"via_smartdns": true
|
||||
}
|
||||
|
||||
|
||||
Go-структура:
|
||||
|
||||
// types.go
|
||||
type DNSMode struct {
|
||||
ViaSmartDNS bool `json:"via_smartdns"`
|
||||
}
|
||||
|
||||
|
||||
Помощники:
|
||||
|
||||
// config.go или resolver.go
|
||||
const dnsModePath = stateDir + "/dns-mode.json"
|
||||
|
||||
func loadDNSMode() DNSMode {
|
||||
data, err := os.ReadFile(dnsModePath)
|
||||
if err != nil {
|
||||
return DNSMode{ViaSmartDNS: false}
|
||||
}
|
||||
var m DNSMode
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return DNSMode{ViaSmartDNS: false}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func saveDNSMode(m DNSMode) {
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tmp := dnsModePath + ".tmp"
|
||||
_ = os.WriteFile(tmp, b, 0o644)
|
||||
_ = os.Rename(tmp, dnsModePath)
|
||||
}
|
||||
|
||||
1.3. Как это зашить в резолвер
|
||||
|
||||
В ResolverOpts (в resolver.go) уже есть поле DNSConfigPath.
|
||||
Добавляем туда флаг:
|
||||
|
||||
type ResolverOpts struct {
|
||||
DomainsPath string
|
||||
MetaPath string
|
||||
StaticPath string
|
||||
CachePath string
|
||||
PtrCachePath string
|
||||
TraceLog string
|
||||
TTL int
|
||||
Workers int
|
||||
DNSConfigPath string
|
||||
|
||||
ViaSmartDNS bool
|
||||
}
|
||||
|
||||
|
||||
Там, где сейчас создаёшь ResolverOpts (в routes_update.go / autoloop.go), просто подставляешь:
|
||||
|
||||
mode := loadDNSMode()
|
||||
opts := ResolverOpts{
|
||||
// ...
|
||||
DNSConfigPath: dnsUpstreamsConf,
|
||||
ViaSmartDNS: mode.ViaSmartDNS,
|
||||
}
|
||||
|
||||
|
||||
И дальше в runResolverJob:
|
||||
|
||||
func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResult, error) {
|
||||
// ...
|
||||
|
||||
cfg := loadDNSConfig(opts.DNSConfigPath, logf)
|
||||
|
||||
// если включён режим SmartDNS – игнорируем default/meta из файла
|
||||
if opts.ViaSmartDNS {
|
||||
cfg.Default = []string{"127.0.0.1#6053"}
|
||||
cfg.Meta = []string{"127.0.0.1#6053"}
|
||||
if logf != nil {
|
||||
logf("dns-mode: via smartdns 127.0.0.1#6053")
|
||||
}
|
||||
}
|
||||
|
||||
// дальше как было
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
Так мы гарантируем, что при включённом via_smartdns твой Go-резолвер больше не будет ходить на 94.140.* и т.п. — всё через локальный SmartDNS.
|
||||
|
||||
1.4. API для режима и юнита
|
||||
|
||||
Чтобы вкладка DNS могла всё это показать/пощёлкать, делаем два эндпойнта:
|
||||
|
||||
Статус
|
||||
|
||||
// types.go
|
||||
type DNSStatusResponse struct {
|
||||
ViaSmartDNS bool `json:"via_smartdns"`
|
||||
UnitState string `json:"unit_state"` // "active", "inactive", "failed", "unknown"
|
||||
}
|
||||
|
||||
// vpn_handlers.go или dns_handlers.go
|
||||
func handleDNSStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
mode := loadDNSMode()
|
||||
|
||||
unitState := "unknown"
|
||||
if out, err := ShellRunCapture("systemctl", "is-active", "smartdns-local.service"); err == nil {
|
||||
unitState = strings.TrimSpace(out)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, DNSStatusResponse{
|
||||
ViaSmartDNS: mode.ViaSmartDNS,
|
||||
UnitState: unitState,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Смена режима (использовать SmartDNS или нет)
|
||||
|
||||
type DNSModeRequest struct {
|
||||
ViaSmartDNS bool `json:"via_smartdns"`
|
||||
}
|
||||
|
||||
func handleDNSModeSet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req DNSModeRequest
|
||||
if r.Body != nil {
|
||||
defer r.Body.Close()
|
||||
_ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req)
|
||||
}
|
||||
|
||||
mode := loadDNSMode()
|
||||
mode.ViaSmartDNS = req.ViaSmartDNS
|
||||
saveDNSMode(mode)
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"via_smartdns": mode.ViaSmartDNS,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Управление юнитом SmartDNS (у тебя уже что-то такое есть — можно просто нормализовать):
|
||||
|
||||
type SmartdnsServiceRequest struct {
|
||||
Action string `json:"action"` // "start" / "stop" / "restart"
|
||||
}
|
||||
|
||||
type SmartdnsServiceResponse struct {
|
||||
UnitState string `json:"unit_state"`
|
||||
}
|
||||
|
||||
func handleSmartdnsService(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req SmartdnsServiceRequest
|
||||
if r.Body != nil {
|
||||
defer r.Body.Close()
|
||||
_ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req)
|
||||
}
|
||||
|
||||
act := strings.ToLower(strings.TrimSpace(req.Action))
|
||||
if act == "" {
|
||||
act = "restart"
|
||||
}
|
||||
|
||||
if act == "start" || act == "restart" || act == "stop" {
|
||||
_ = ShellRun("systemctl", act, "smartdns-local.service")
|
||||
}
|
||||
|
||||
state := "unknown"
|
||||
if out, err := ShellRunCapture("systemctl", "is-active", "smartdns-local.service"); err == nil {
|
||||
state = strings.TrimSpace(out)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, SmartdnsServiceResponse{
|
||||
UnitState: state,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Роуты:
|
||||
|
||||
mux.HandleFunc("/api/v1/dns/status", handleDNSStatus)
|
||||
mux.HandleFunc("/api/v1/dns/mode", handleDNSModeSet)
|
||||
mux.HandleFunc("/api/v1/dns/smartdns-service", handleSmartdnsService)
|
||||
|
||||
2. Новая вкладка DNS в Qt
|
||||
|
||||
Теперь UI можно сделать максимально простым:
|
||||
|
||||
2.1. Как будет выглядеть
|
||||
|
||||
Группа 1 — Upstreams
|
||||
|
||||
4 поля: default1, default2, meta1, meta2
|
||||
|
||||
Кнопка «Save upstreams»
|
||||
|
||||
Группа 2 — SmartDNS
|
||||
|
||||
Чекбокс: Use SmartDNS (127.0.0.1:6053) → это via_smartdns
|
||||
|
||||
Лейбл: SmartDNS unit: active/inactive/failed
|
||||
|
||||
Лейбл: Resolver mode: via SmartDNS / direct upstreams
|
||||
|
||||
(опционально) две кнопки — Start / Stop юнит
|
||||
|
||||
Нижняя текстовая панель полностью убирается — всё, что касается логов, уже есть на вкладке Trace (режим mode=smartdns и mode=full).
|
||||
|
||||
2.2. Код виджета (переписанный _build_tab_dns)
|
||||
|
||||
В vpn_dashboard_qt.py вместо старой _build_tab_dns можно вот так (без учёта импортов — у тебя уже есть):
|
||||
|
||||
def _build_tab_dns(self):
|
||||
tab = QWidget()
|
||||
main_layout = QVBoxLayout(tab)
|
||||
|
||||
# ---------------- Upstreams ----------------
|
||||
grp_up = QGroupBox("Upstreams")
|
||||
up_layout = QFormLayout(grp_up)
|
||||
|
||||
self.dns_default1 = QLineEdit()
|
||||
self.dns_default2 = QLineEdit()
|
||||
self.dns_meta1 = QLineEdit()
|
||||
self.dns_meta2 = QLineEdit()
|
||||
|
||||
up_layout.addRow("default1", self.dns_default1)
|
||||
up_layout.addRow("default2", self.dns_default2)
|
||||
up_layout.addRow("meta1", self.dns_meta1)
|
||||
up_layout.addRow("meta2", self.dns_meta2)
|
||||
|
||||
btn_save = QPushButton("Save upstreams")
|
||||
btn_save.clicked.connect(self.on_save_upstreams_clicked)
|
||||
up_layout.addRow(btn_save)
|
||||
|
||||
# ---------------- SmartDNS ----------------
|
||||
grp_smartdns = QGroupBox("SmartDNS")
|
||||
sd_layout = QVBoxLayout(grp_smartdns)
|
||||
|
||||
self.chk_dns_via_smartdns = QCheckBox("Use SmartDNS (127.0.0.1:6053)")
|
||||
self.chk_dns_via_smartdns.stateChanged.connect(self.on_dns_mode_changed)
|
||||
|
||||
self.lbl_smartdns_unit = QLabel("SmartDNS unit: unknown")
|
||||
self.lbl_dns_mode = QLabel("Resolver mode: unknown")
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
self.btn_smartdns_start = QPushButton("Start unit")
|
||||
self.btn_smartdns_stop = QPushButton("Stop unit")
|
||||
self.btn_smartdns_start.clicked.connect(
|
||||
lambda: self.on_smartdns_unit_action("start")
|
||||
)
|
||||
self.btn_smartdns_stop.clicked.connect(
|
||||
lambda: self.on_smartdns_unit_action("stop")
|
||||
)
|
||||
btn_row.addWidget(self.btn_smartdns_start)
|
||||
btn_row.addWidget(self.btn_smartdns_stop)
|
||||
btn_row.addStretch(1)
|
||||
|
||||
sd_layout.addWidget(self.chk_dns_via_smartdns)
|
||||
sd_layout.addWidget(self.lbl_smartdns_unit)
|
||||
sd_layout.addWidget(self.lbl_dns_mode)
|
||||
sd_layout.addLayout(btn_row)
|
||||
|
||||
# ---------------- Compose ----------------
|
||||
main_layout.addWidget(grp_up)
|
||||
main_layout.addWidget(grp_smartdns)
|
||||
main_layout.addStretch(1)
|
||||
|
||||
self.tab_dns = tab
|
||||
self.tabs.addTab(tab, "DNS")
|
||||
|
||||
2.3. Обновление вкладки (refresh_dns_tab)
|
||||
def refresh_dns_tab(self):
|
||||
# 1) upstreams
|
||||
ups = self.c.dns_upstreams_view() # как и было
|
||||
self.dns_default1.setText(ups.default1 or "")
|
||||
self.dns_default2.setText(ups.default2 or "")
|
||||
self.dns_meta1.setText(ups.meta1 or "")
|
||||
self.dns_meta2.setText(ups.meta2 or "")
|
||||
|
||||
# 2) статус DNS / SmartDNS
|
||||
st = self.c.dns_status_view() # новый метод в контроллере
|
||||
|
||||
# режим
|
||||
self.chk_dns_via_smartdns.blockSignals(True)
|
||||
self.chk_dns_via_smartdns.setChecked(bool(st.via_smartdns))
|
||||
self.chk_dns_via_smartdns.blockSignals(False)
|
||||
|
||||
mode_txt = "via SmartDNS" if st.via_smartdns else "direct upstreams"
|
||||
self.lbl_dns_mode.setText(f"Resolver mode: {mode_txt}")
|
||||
|
||||
# юнит
|
||||
self.lbl_smartdns_unit.setText(f"SmartDNS unit: {st.unit_state or 'unknown'}")
|
||||
|
||||
# немного UX: если юнит inactive, кнопка Start активна, Stop — серый
|
||||
is_active = (st.unit_state == "active")
|
||||
self.btn_smartdns_start.setEnabled(not is_active)
|
||||
self.btn_smartdns_stop.setEnabled(is_active)
|
||||
|
||||
2.4. Обработчики
|
||||
def on_save_upstreams_clicked(self):
|
||||
ups = self.c.dns_upstreams_view()
|
||||
ups.default1 = self.dns_default1.text().strip()
|
||||
ups.default2 = self.dns_default2.text().strip()
|
||||
ups.meta1 = self.dns_meta1.text().strip()
|
||||
ups.meta2 = self.dns_meta2.text().strip()
|
||||
|
||||
ok, err = self.c.dns_upstreams_save(ups)
|
||||
if not ok:
|
||||
QMessageBox.critical(self, "Error", f"Failed to save upstreams:\n{err}")
|
||||
else:
|
||||
self.show_status("DNS upstreams saved")
|
||||
|
||||
|
||||
def on_dns_mode_changed(self, state: int):
|
||||
via = (state == Qt.Checked)
|
||||
ok, st, err = self.c.dns_mode_set(via)
|
||||
if not ok:
|
||||
QMessageBox.critical(self, "Error", f"Failed to change DNS mode:\n{err}")
|
||||
# откатываем чекбокс
|
||||
self.refresh_dns_tab()
|
||||
return
|
||||
# обновляем лейблы
|
||||
self.refresh_dns_tab()
|
||||
|
||||
|
||||
def on_smartdns_unit_action(self, action: str):
|
||||
ok, st, err = self.c.smartdns_service_action(action)
|
||||
if not ok:
|
||||
QMessageBox.critical(self, "Error", f"SmartDNS {action} failed:\n{err}")
|
||||
return
|
||||
self.refresh_dns_tab()
|
||||
self.refresh_status_tab() # если хочешь синхронизировать индикатор на Status
|
||||
|
||||
2.5. Методы в dashboard_controller.py
|
||||
|
||||
Нужно добавить:
|
||||
|
||||
@dataclass
|
||||
class DNSStatus:
|
||||
via_smartdns: bool
|
||||
unit_state: str
|
||||
|
||||
class DashboardController:
|
||||
# ...
|
||||
|
||||
def dns_status_view(self) -> DNSStatus:
|
||||
data = self.client.get_json("/api/v1/dns/status")
|
||||
return DNSStatus(
|
||||
via_smartdns=bool(data.get("via_smartdns")),
|
||||
unit_state=data.get("unit_state") or "unknown",
|
||||
)
|
||||
|
||||
def dns_mode_set(self, via: bool) -> tuple[bool, DNSStatus | None, str | None]:
|
||||
try:
|
||||
data = self.client.post_json(
|
||||
"/api/v1/dns/mode",
|
||||
{"via_smartdns": bool(via)},
|
||||
)
|
||||
st = self.dns_status_view()
|
||||
return True, st, None
|
||||
except Exception as e:
|
||||
return False, None, str(e)
|
||||
|
||||
def smartdns_service_action(self, action: str) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
data = self.client.post_json(
|
||||
"/api/v1/dns/smartdns-service",
|
||||
{"action": action},
|
||||
)
|
||||
return True, data.get("unit_state"), None
|
||||
except Exception as e:
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
Upstreams-методы у тебя уже есть, их трогать не нужно.
|
||||
|
||||
3. Что дальше с логами
|
||||
|
||||
Когда эта схема заведётся (юнит живёт своей жизнью, резолвер знает via_smartdns и ходит только на 127.0.0.1), дальше можно:
|
||||
|
||||
В digA логировать отдельно:
|
||||
|
||||
dns smartdns когда ViaSmartDNS == true,
|
||||
|
||||
dns direct для прямых upstream.
|
||||
|
||||
В handleTraceJSON(mode="gui") продолжать выкидывать «шум» вроде больших дампов резолвера.
|
||||
|
||||
Для mode=smartdns показывать только строки, где есть smartdns в префиксе — и там уже будет видно цепочку.
|
||||
|
||||
Если хочешь, в следующем шаге можем прям точечно добить:
|
||||
|
||||
исправление поля ViaSmartDNS bool в types.go (я видел, что у тебя там раздралось на ViaSmartDN S),
|
||||
|
||||
аккуратно переписать digA-лог, чтобы не было этих сообщений вида dns error ... via 94.140.14.14: lookup ... on 192.168.50.10:53, когда на самом деле всё ок.
|
||||
@@ -1,565 +0,0 @@
|
||||
Ок, давай разберём по двум пунктам:
|
||||
|
||||
Финальные имена и что именно править в коде
|
||||
|
||||
Почему стало ~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.16–31.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) – скинь их ещё раз, я аккуратно вставлю всё в нужные места без переименования функций и с теми же структурами, что уже есть.
|
||||
@@ -1,201 +0,0 @@
|
||||
Инструкция 3 (safe-версия)
|
||||
|
||||
Цель:
|
||||
Сделать улучшения резолвера без risky-переписывания, сохранить обратную совместимость с текущим GUI/API,
|
||||
и убрать главные источники шума: массовые NXDOMAIN, таймауты и нечитабельные метрики.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
0) Что подтверждено по текущему состоянию
|
||||
--------------------------------------------------------------------
|
||||
|
||||
1. Основной рабочий код сейчас в:
|
||||
- app/resolver.go
|
||||
- app/routes_update.go
|
||||
- app/dns_settings.go
|
||||
- app/domains_handlers.go
|
||||
|
||||
2. Логи показывают:
|
||||
- много NXDOMAIN (ожидаемо при широком base x subs)
|
||||
- заметную долю timeout
|
||||
- один агрегированный счетчик dns_errors, из-за чего трудно понять причину деградации.
|
||||
|
||||
3. Формат dns upstream у нас: host#port (например 94.140.14.15#53 или 127.0.0.1#6053).
|
||||
Это важно: нельзя использовать валидацию, которая принимает только host:port.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
1) Архитектурное решение (рекомендуемое)
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Не оставлять только переключатель direct <-> smartdns.
|
||||
Сделать 3 режима резолвера:
|
||||
|
||||
- direct:
|
||||
обычные домены через default/meta upstream.
|
||||
|
||||
- smartdns:
|
||||
все домены через SmartDNS address.
|
||||
|
||||
- hybrid_wildcard (recommended):
|
||||
только wildcard-домены через SmartDNS, остальные напрямую через default/meta.
|
||||
|
||||
Почему так лучше:
|
||||
- сохраняем скорость и отказоустойчивость direct для обычных доменов;
|
||||
- wildcard-логику держим строго в SmartDNS, как ты и хотел;
|
||||
- не ломаем текущий UX: можно оставить старый bool и маппить его на mode.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
2) Что НЕ внедряем из старой инструкция3
|
||||
--------------------------------------------------------------------
|
||||
|
||||
1. Не используем netip.ParseAddrPort для upstream-валидации (ломает host#port).
|
||||
2. Не используем netip.MustParseAddr в hot path (может паниковать).
|
||||
3. Не добавляем лишний semaphore поверх worker pool (сложность без явной выгоды).
|
||||
4. Не делаем агрессивный рефактор API-контрактов без backward-compat.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
3) Пакет безопасных правок (приоритет P1)
|
||||
--------------------------------------------------------------------
|
||||
|
||||
P1.1 - Режимы DNS (backward compatible)
|
||||
Файлы:
|
||||
- app/types.go
|
||||
- app/dns_settings.go
|
||||
- app/routes_update.go
|
||||
- app/resolver.go
|
||||
|
||||
Изменения:
|
||||
1) Ввести enum режима:
|
||||
type DNSResolverMode string
|
||||
const (
|
||||
DNSModeDirect DNSResolverMode = "direct"
|
||||
DNSModeSmartDNS DNSResolverMode = "smartdns"
|
||||
DNSModeHybrid DNSResolverMode = "hybrid_wildcard"
|
||||
)
|
||||
|
||||
2) Расширить DNSMode/DNSStatusResponse/DNSModeRequest полем Mode,
|
||||
но оставить ViaSmartDNS для старого GUI:
|
||||
- если Mode пустой, использовать ViaSmartDNS:
|
||||
- true -> smartdns
|
||||
- false -> direct
|
||||
|
||||
3) В ResolverOpts передавать Mode и список wildcard-доменов (один раз на job).
|
||||
|
||||
4) В resolveHostGo выбирать dnsList так:
|
||||
- mode == smartdns: []{smartdnsAddr}
|
||||
- mode == hybrid_wildcard и host совпал с wildcard: []{smartdnsAddr}
|
||||
- иначе: meta или default по текущей логике.
|
||||
|
||||
Примечание:
|
||||
Wildcard-список уже хранится в smartdns-wildcards.json через /api/v1/smartdns/wildcards.
|
||||
Нужно только использовать его в резолвере.
|
||||
|
||||
P1.2 - Upstream fallback с классификацией ошибок
|
||||
Файл:
|
||||
- app/resolver.go
|
||||
|
||||
Изменения:
|
||||
1) В digA:
|
||||
- идти по upstream последовательно;
|
||||
- timeout/temporary -> fallback на следующий upstream;
|
||||
- nxdomain -> остановить попытки для домена (дальше пробовать бессмысленно).
|
||||
|
||||
2) Классифицировать ошибки через net.DNSError + fallback по тексту:
|
||||
- nxdomain
|
||||
- timeout
|
||||
- temporary
|
||||
- other
|
||||
|
||||
3) Вместо одного dns_errors вести структуру счетчиков:
|
||||
dns_attempts, dns_ok, dns_nxdomain, dns_timeout, dns_temporary, dns_other.
|
||||
|
||||
P1.3 - Разделенные метрики в summary
|
||||
Файл:
|
||||
- app/resolver.go
|
||||
|
||||
Изменения:
|
||||
1) Обновить финальный лог "resolve summary" с раздельными счетчиками DNS-ошибок.
|
||||
2) Добавить per-upstream агрегаты (минимум attempts/ok/timeout/nxdomain/other).
|
||||
Формат может быть одной строкой JSON, чтобы GUI/анализатору было проще парсить.
|
||||
|
||||
P1.4 - Ограничение domain expansion
|
||||
Файл:
|
||||
- app/routes_update.go
|
||||
|
||||
Изменения:
|
||||
1) Добавить конфиг-лимиты через env:
|
||||
- RESOLVE_SUBS_PER_BASE_LIMIT (например default 25)
|
||||
- RESOLVE_DOMAINS_HARD_CAP (например default 12000)
|
||||
|
||||
2) После построения domainSet:
|
||||
- сортировать домены;
|
||||
- при превышении hard cap обрезать хвост детерминированно;
|
||||
- писать явный warning в trace.
|
||||
|
||||
3) Логировать breakdown:
|
||||
bases_count, subs_count, expanded_count, total_domains.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
4) Пакет улучшений P2 (после P1)
|
||||
--------------------------------------------------------------------
|
||||
|
||||
P2.1 - Negative cache
|
||||
Файл:
|
||||
- app/resolver.go
|
||||
|
||||
Идея:
|
||||
- кэшировать nxdomain/servfail на короткий TTL (например 10-20 минут),
|
||||
чтобы не долбить одинаковые несуществующие имена каждую прогонку.
|
||||
|
||||
P2.2 - PTR retry (ограниченный)
|
||||
Файл:
|
||||
- app/resolver.go
|
||||
|
||||
Идея:
|
||||
- для digPTR сделать 1-2 retry только на timeout/temporary;
|
||||
- не ретраить nxdomain.
|
||||
|
||||
P2.3 - GUI/API отображение режима
|
||||
Файлы GUI:
|
||||
- selective-vpn-gui/api_client.py
|
||||
- selective-vpn-gui/dashboard_controller.py
|
||||
- selective-vpn-gui/vpn_dashboard_qt.py
|
||||
|
||||
Идея:
|
||||
- показать mode = direct/smartdns/hybrid_wildcard;
|
||||
- оставить старый toggle рабочим (маппинг direct/smartdns),
|
||||
а hybrid можно добавить как отдельный выбор (позже).
|
||||
|
||||
--------------------------------------------------------------------
|
||||
5) Порядок внедрения (рекомендуемый)
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Шаг 1:
|
||||
Реализовать режимы + fallback + split-метрики (P1.1, P1.2, P1.3).
|
||||
|
||||
Шаг 2:
|
||||
Добавить лимиты expansion (P1.4), проверить изменение unresolved/timeouts.
|
||||
|
||||
Шаг 3:
|
||||
Добавить negative cache и PTR retry (P2.1, P2.2).
|
||||
|
||||
--------------------------------------------------------------------
|
||||
6) Критерии готовности
|
||||
--------------------------------------------------------------------
|
||||
|
||||
1. В trace видно не один dns_errors, а раздельные причины.
|
||||
2. В режиме direct при падении одного upstream запросы частично продолжают проходить через fallback.
|
||||
3. В режиме hybrid_wildcard wildcard-домены уходят через SmartDNS, остальные через direct.
|
||||
4. Количество timeout и общее время прогона снижаются относительно текущего baseline.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
7) Короткий диагноз по сути
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Проблема не в одном SmartDNS bottleneck.
|
||||
Основной вклад сейчас дает комбинация:
|
||||
- большой domain expansion,
|
||||
- много несуществующих имен (NXDOMAIN),
|
||||
- и отсутствие четкой стратегии fallback/метрик.
|
||||
|
||||
Safe-путь:
|
||||
ввести гибридный routing DNS + раздельную диагностику + контролируемый expansion.
|
||||
@@ -1,252 +0,0 @@
|
||||
Анализ твоего кода 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).
|
||||
Reference in New Issue
Block a user