Окей, давай сначала аккуратно разрулим логику, а уже потом можно будет добивать конкретный код (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, когда на самом деле всё ок.