Files
elmprodvpn/_legacy/selective-vpn-api/инструкция.txt

461 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Окей, давай сначала аккуратно разрулим логику, а уже потом можно будет добивать конкретный код (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, когда на самом деле всё ок.