dns: switch to active upstream pool and wave fallback behavior

This commit is contained in:
beckline
2026-02-22 19:15:37 +03:00
parent a7ec4fe801
commit 0f88cfeeaa
9 changed files with 382 additions and 130 deletions

View File

@@ -259,6 +259,11 @@ class DNSBenchmarkResponse:
recommended_meta: List[str]
@dataclass(frozen=True)
class DNSUpstreamPoolState:
items: List[DNSBenchmarkUpstream]
@dataclass(frozen=True)
class SmartdnsServiceState:
state: str
@@ -1185,6 +1190,48 @@ class ApiClient:
},
)
def dns_upstream_pool_get(self) -> DNSUpstreamPoolState:
data = cast(Dict[str, Any], self._json(self._request("GET", "/api/v1/dns/upstream-pool")) or {})
raw = data.get("items") or []
if not isinstance(raw, list):
raw = []
items: List[DNSBenchmarkUpstream] = []
for row in raw:
if not isinstance(row, dict):
continue
addr = str(row.get("addr") or "").strip()
if not addr:
continue
items.append(DNSBenchmarkUpstream(addr=addr, enabled=bool(row.get("enabled", True))))
return DNSUpstreamPoolState(items=items)
def dns_upstream_pool_set(self, items: List[DNSBenchmarkUpstream]) -> DNSUpstreamPoolState:
data = cast(
Dict[str, Any],
self._json(
self._request(
"POST",
"/api/v1/dns/upstream-pool",
json_body={
"items": [{"addr": u.addr, "enabled": bool(u.enabled)} for u in (items or [])],
},
)
)
or {},
)
raw = data.get("items") or []
if not isinstance(raw, list):
raw = []
out: List[DNSBenchmarkUpstream] = []
for row in raw:
if not isinstance(row, dict):
continue
addr = str(row.get("addr") or "").strip()
if not addr:
continue
out.append(DNSBenchmarkUpstream(addr=addr, enabled=bool(row.get("enabled", True))))
return DNSUpstreamPoolState(items=out)
def dns_benchmark(
self,
upstreams: List[DNSBenchmarkUpstream],

View File

@@ -26,6 +26,7 @@ from api_client import (
CmdResult,
DNSBenchmarkResponse,
DNSBenchmarkUpstream,
DNSUpstreamPoolState,
DNSStatus,
DnsUpstreams,
DomainsFile,
@@ -870,6 +871,12 @@ class DashboardController:
def dns_upstreams_save(self, cfg: DnsUpstreams) -> None:
self.client.dns_upstreams_set(cfg)
def dns_upstream_pool_view(self) -> DNSUpstreamPoolState:
return self.client.dns_upstream_pool_get()
def dns_upstream_pool_save(self, items: List[DNSBenchmarkUpstream]) -> DNSUpstreamPoolState:
return self.client.dns_upstream_pool_set(items)
def dns_benchmark(
self,
upstreams: List[DNSBenchmarkUpstream],

View File

@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
QWidget,
)
from api_client import DNSBenchmarkUpstream, DnsUpstreams
from api_client import DNSBenchmarkUpstream
from dashboard_controller import DashboardController
@@ -61,24 +61,22 @@ class DNSBenchmarkDialog(QDialog):
self.ctrl = ctrl
self.settings = settings
self.refresh_cb = refresh_cb
self._last_recommended_default: List[str] = []
self._last_recommended_meta: List[str] = []
self.setWindowTitle("DNS benchmark")
self.resize(980, 650)
self.resize(980, 660)
root = QVBoxLayout(self)
hint = QLabel(
"List format: one DNS per row. Toggle checkbox to include in test. "
"Then run benchmark and apply best DNS to resolver."
"One DNS per row. Checkbox means ACTIVE for resolver wave mode. "
"Benchmark checks all rows and shows health."
)
hint.setWordWrap(True)
hint.setStyleSheet("color: gray;")
root.addWidget(hint)
self.tbl_sources = QTableWidget(0, 2)
self.tbl_sources.setHorizontalHeaderLabels(["Use", "DNS upstream"])
self.tbl_sources.setHorizontalHeaderLabels(["Active", "DNS upstream"])
self.tbl_sources.horizontalHeader().setStretchLastSection(True)
self.tbl_sources.setSelectionBehavior(QTableWidget.SelectRows)
self.tbl_sources.setSelectionMode(QTableWidget.SingleSelection)
@@ -95,6 +93,12 @@ class DNSBenchmarkDialog(QDialog):
self.btn_reset = QPushButton("Reset defaults")
self.btn_reset.clicked.connect(self.on_reset_defaults)
row_btns.addWidget(self.btn_reset)
self.btn_reload = QPushButton("Reload active set")
self.btn_reload.clicked.connect(self.on_reload_pool)
row_btns.addWidget(self.btn_reload)
self.btn_save_pool = QPushButton("Save active set")
self.btn_save_pool.clicked.connect(self.on_save_pool)
row_btns.addWidget(self.btn_save_pool)
row_btns.addStretch(1)
root.addLayout(row_btns)
@@ -150,18 +154,12 @@ class DNSBenchmarkDialog(QDialog):
self.tbl_results.setSelectionMode(QTableWidget.SingleSelection)
root.addWidget(self.tbl_results, stretch=3)
apply_row = QHBoxLayout()
self.btn_apply_default = QPushButton("Apply top-2 to Default")
self.btn_apply_default.clicked.connect(self.on_apply_default)
apply_row.addWidget(self.btn_apply_default)
self.btn_apply_meta = QPushButton("Apply top-2 to Meta")
self.btn_apply_meta.clicked.connect(self.on_apply_meta)
apply_row.addWidget(self.btn_apply_meta)
apply_row.addStretch(1)
close_row = QHBoxLayout()
close_row.addStretch(1)
self.btn_close = QPushButton("Close")
self.btn_close.clicked.connect(self.accept)
apply_row.addWidget(self.btn_close)
root.addLayout(apply_row)
close_row.addWidget(self.btn_close)
root.addLayout(close_row)
self._load_sources()
self._load_domains()
@@ -173,33 +171,51 @@ class DNSBenchmarkDialog(QDialog):
QMessageBox.critical(self, title, str(e))
def _load_sources(self) -> None:
raw = str(self.settings.value("dns_benchmark/upstreams", "") or "").strip()
rows: List[tuple[bool, str]] = []
if raw:
try:
data = json.loads(raw)
if isinstance(data, list):
for item in data:
if not isinstance(item, dict):
continue
addr = str(item.get("addr") or "").strip()
if not addr:
continue
rows.append((bool(item.get("enabled", True)), addr))
except Exception:
rows = []
try:
st = self.ctrl.dns_upstream_pool_view()
for item in st.items:
addr = str(item.addr or "").strip()
if not addr:
continue
rows.append((bool(item.enabled), addr))
except Exception:
rows = []
if not rows:
raw = str(self.settings.value("dns_benchmark/upstreams", "") or "").strip()
if raw:
try:
data = json.loads(raw)
if isinstance(data, list):
for item in data:
if not isinstance(item, dict):
continue
addr = str(item.get("addr") or "").strip()
if not addr:
continue
rows.append((bool(item.get("enabled", True)), addr))
except Exception:
rows = []
if not rows:
rows = [(True, item) for item in DEFAULT_UPSTREAMS]
self.tbl_sources.blockSignals(True)
self.tbl_sources.setRowCount(0)
for enabled, addr in rows:
self._append_source_row(enabled, addr)
self.tbl_sources.blockSignals(False)
self._save_settings()
def _load_domains(self) -> None:
raw = str(self.settings.value("dns_benchmark/domains", "") or "").strip()
if not raw:
raw = "\n".join(DEFAULT_DOMAINS)
self.txt_domains.blockSignals(True)
self.txt_domains.setPlainText(raw)
self.txt_domains.blockSignals(False)
def _save_settings(self) -> None:
items = []
@@ -264,14 +280,33 @@ class DNSBenchmarkDialog(QDialog):
row = self.tbl_sources.currentRow()
if row >= 0:
self.tbl_sources.removeRow(row)
self._save_settings()
self._save_settings()
def on_reset_defaults(self) -> None:
self.tbl_sources.blockSignals(True)
self.tbl_sources.setRowCount(0)
for item in DEFAULT_UPSTREAMS:
self._append_source_row(True, item)
self.tbl_sources.blockSignals(False)
self._save_settings()
def on_reload_pool(self) -> None:
self._safe(self._load_sources, "Reload DNS active set error")
def on_save_pool(self) -> None:
def work() -> None:
payload = self._source_payload()
st = self.ctrl.dns_upstream_pool_save(payload)
active = sum(1 for x in st.items if x.enabled)
total = len(st.items)
self._save_settings()
self.lbl_summary.setText(f"Saved active DNS set: active={active}/{total}")
self.lbl_summary.setStyleSheet("color: green;")
if self.refresh_cb:
self.refresh_cb()
self._safe(work, "Save DNS active set error")
def on_run_benchmark(self) -> None:
def work() -> None:
self._save_settings()
@@ -284,8 +319,6 @@ class DNSBenchmarkDialog(QDialog):
attempts=int(self.spin_attempts.value()),
concurrency=int(self.spin_concurrency.value()),
)
self._last_recommended_default = list(resp.recommended_default or [])
self._last_recommended_meta = list(resp.recommended_meta or [])
self._render_results(resp)
if self.refresh_cb:
self.refresh_cb()
@@ -320,11 +353,9 @@ class DNSBenchmarkDialog(QDialog):
st_item.setForeground(QColor("red"))
self.tbl_results.setItem(row, 6, st_item)
dflt = ", ".join(resp.recommended_default or []) or ""
meta = ", ".join(resp.recommended_meta or []) or ""
self.lbl_summary.setText(
f"Checked: {len(resp.results)} DNS | domains={len(resp.domains_used)} "
f"| timeout={resp.timeout_ms}ms | rec default: {dflt} | rec meta: {meta}"
f"| timeout={resp.timeout_ms}ms"
)
self.lbl_summary.setStyleSheet("color: gray;")
@@ -334,43 +365,3 @@ class DNSBenchmarkDialog(QDialog):
self.settings.setValue("dns_benchmark/last_ok", ok_total)
self.settings.setValue("dns_benchmark/last_fail", fail_total)
self.settings.setValue("dns_benchmark/last_timeout", timeout_total)
def on_apply_default(self) -> None:
def work() -> None:
picks = list(self._last_recommended_default or [])
if len(picks) < 2:
raise ValueError("run benchmark first (need at least 2 recommended DNS)")
cur = self.ctrl.dns_upstreams_view()
cfg = DnsUpstreams(
default1=picks[0],
default2=picks[1],
meta1=cur.meta1,
meta2=cur.meta2,
)
self.ctrl.dns_upstreams_save(cfg)
if self.refresh_cb:
self.refresh_cb()
self.lbl_summary.setText(f"Applied default DNS: {picks[0]}, {picks[1]}")
self.lbl_summary.setStyleSheet("color: green;")
self._safe(work, "Apply default DNS error")
def on_apply_meta(self) -> None:
def work() -> None:
picks = list(self._last_recommended_meta or [])
if len(picks) < 2:
raise ValueError("run benchmark first (need at least 2 recommended DNS)")
cur = self.ctrl.dns_upstreams_view()
cfg = DnsUpstreams(
default1=cur.default1,
default2=cur.default2,
meta1=picks[0],
meta2=picks[1],
)
self.ctrl.dns_upstreams_save(cfg)
if self.refresh_cb:
self.refresh_cb()
self.lbl_summary.setText(f"Applied meta DNS: {picks[0]}, {picks[1]}")
self.lbl_summary.setStyleSheet("color: green;")
self._safe(work, "Apply meta DNS error")

View File

@@ -34,7 +34,7 @@ from PySide6.QtWidgets import (
QProgressBar,
)
from api_client import ApiClient, DnsUpstreams
from api_client import ApiClient
from dashboard_controller import DashboardController, TraceMode
from dns_benchmark_dialog import DNSBenchmarkDialog
from traffic_mode_dialog import TrafficModeDialog
@@ -729,14 +729,24 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
self.lbl_dns_mode_state.setText(txt)
self.lbl_dns_mode_state.setStyleSheet(f"color: {color};")
def _set_dns_resolver_summary(self, ups: DnsUpstreams) -> None:
d1 = (ups.default1 or "").strip() or ""
d2 = (ups.default2 or "").strip() or ""
m1 = (ups.meta1 or "").strip() or ""
m2 = (ups.meta2 or "").strip() or ""
self.lbl_dns_resolver_upstreams.setText(
f"Resolver upstreams: default[{d1}, {d2}] meta[{m1}, {m2}]"
)
def _set_dns_resolver_summary(self, pool_items) -> None:
active = []
total = 0
for item in pool_items or []:
addr = str(getattr(item, "addr", "") or "").strip()
if not addr:
continue
total += 1
if bool(getattr(item, "enabled", False)):
active.append(addr)
if not active:
text = f"Resolver upstreams: active=0/{total} (empty set)"
else:
preview = ", ".join(active[:4])
if len(active) > 4:
preview += f", +{len(active)-4} more"
text = f"Resolver upstreams: active={len(active)}/{total} [{preview}]"
self.lbl_dns_resolver_upstreams.setText(text)
self.lbl_dns_resolver_upstreams.setStyleSheet("color: gray;")
avg_ms = self._ui_settings.value("dns_benchmark/last_avg_ms", None)
@@ -1072,8 +1082,8 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
def work():
self._dns_ui_refresh = True
try:
ups = self.ctrl.dns_upstreams_view()
self._set_dns_resolver_summary(ups)
pool = self.ctrl.dns_upstream_pool_view()
self._set_dns_resolver_summary(getattr(pool, "items", []))
st = self.ctrl.dns_status_view()
self.ent_smartdns_addr.setText(st.smartdns_addr or "")