From 11a3eb1524cc9892931972ae7deff9ade8c41352 Mon Sep 17 00:00:00 2001 From: beckline Date: Sun, 15 Feb 2026 02:08:47 +0300 Subject: [PATCH] ui: make ControlGroup query robust; drop legacy instruction files --- _legacy/selective-vpn-api/инструкция.txt | 460 -------------- _legacy/selective-vpn-api/инструкция2.txt | 565 ------------------ .../selective-vpn-api/инструкция3-safe.txt | 201 ------- _legacy/selective-vpn-api/инструкция3.txt | 252 -------- selective-vpn-gui/traffic_mode_dialog.py | 55 +- 5 files changed, 45 insertions(+), 1488 deletions(-) delete mode 100644 _legacy/selective-vpn-api/инструкция.txt delete mode 100644 _legacy/selective-vpn-api/инструкция2.txt delete mode 100644 _legacy/selective-vpn-api/инструкция3-safe.txt delete mode 100644 _legacy/selective-vpn-api/инструкция3.txt diff --git a/_legacy/selective-vpn-api/инструкция.txt b/_legacy/selective-vpn-api/инструкция.txt deleted file mode 100644 index dea5985..0000000 --- a/_legacy/selective-vpn-api/инструкция.txt +++ /dev/null @@ -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, когда на самом деле всё ок. diff --git a/_legacy/selective-vpn-api/инструкция2.txt b/_legacy/selective-vpn-api/инструкция2.txt deleted file mode 100644 index 7f84c31..0000000 --- a/_legacy/selective-vpn-api/инструкция2.txt +++ /dev/null @@ -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) – скинь их ещё раз, я аккуратно вставлю всё в нужные места без переименования функций и с теми же структурами, что уже есть. diff --git a/_legacy/selective-vpn-api/инструкция3-safe.txt b/_legacy/selective-vpn-api/инструкция3-safe.txt deleted file mode 100644 index 6fc6aec..0000000 --- a/_legacy/selective-vpn-api/инструкция3-safe.txt +++ /dev/null @@ -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. diff --git a/_legacy/selective-vpn-api/инструкция3.txt b/_legacy/selective-vpn-api/инструкция3.txt deleted file mode 100644 index 83c84b8..0000000 --- a/_legacy/selective-vpn-api/инструкция3.txt +++ /dev/null @@ -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). diff --git a/selective-vpn-gui/traffic_mode_dialog.py b/selective-vpn-gui/traffic_mode_dialog.py index d9099f5..49f81ce 100644 --- a/selective-vpn-gui/traffic_mode_dialog.py +++ b/selective-vpn-gui/traffic_mode_dialog.py @@ -928,19 +928,54 @@ RU: Применяет policy-rules и проверяет health. При оши if p.returncode != 0: raise RuntimeError(f"systemd-run failed: {p.returncode}\n{out}".strip()) - p2 = subprocess.run( - ["systemctl", "--user", "show", "-p", "ControlGroup", "--value", unit], - capture_output=True, - text=True, - check=False, - ) - out2 = ((p2.stdout or "") + (p2.stderr or "")).strip() - cg = (p2.stdout or "").strip() - if p2.returncode != 0 or not cg: - raise RuntimeError(f"failed to query ControlGroup\n{out2}".strip()) + # EN: Some apps (e.g. Chrome wrappers) can return quickly; the transient scope + # EN: may appear/disappear fast. Retry briefly to avoid race. + # RU: Некоторые приложения (например, chrome-wrapper) быстро завершаются; scope + # RU: может появиться/исчезнуть очень быстро. Делаем небольшой retry. + cg = self._control_group_for_unit_retry(unit, timeout_sec=2.0) return cg, out + def _control_group_for_unit_retry(self, unit: str, *, timeout_sec: float = 2.0) -> str: + u = (unit or "").strip() + if not u: + raise ValueError("empty unit") + + deadline = time.time() + max(0.2, float(timeout_sec or 0)) + last_out = "" + while time.time() < deadline: + code, out = self._systemctl_user(["show", "-p", "ControlGroup", "--value", u]) + last_out = out or "" + if code == 0: + cg = (out or "").strip() + if cg: + return cg + low = (out or "").lower() + if "could not be found" in low or "not found" in low or "loaded units listed" in low: + break + time.sleep(0.1) + + # Provide a more actionable error for users. + code_s, out_s = self._systemctl_user(["status", u, "--no-pager", "--plain"]) + status_txt = (out_s or "").strip() + hint = ( + "EN: Scope unit may have exited immediately. If the app is already running, " + "this launch may not create a new process tree inside the scope.\n" + "EN: Try closing the app полностью and запуск again from here.\n" + "RU: Scope мог завершиться сразу. Если приложение уже запущено, повторный запуск " + "может не создать новый процесс внутри scope.\n" + "RU: Попробуй полностью закрыть приложение и запустить снова отсюда." + ) + raise RuntimeError( + ( + "failed to query ControlGroup\n" + + (last_out.strip() or "(no output)") + + ("\n\nstatus:\n" + status_txt if status_txt else "") + + "\n\n" + + hint + ).strip() + ) + def on_app_run(self) -> None: def work() -> None: cmdline = (self.ed_app_cmd.text() or "").strip()