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