dns ui: compact tab + benchmark dialog and api endpoint

This commit is contained in:
beckline
2026-02-22 14:40:40 +03:00
parent 0b28586f31
commit a7ec4fe801
7 changed files with 1089 additions and 50 deletions

View File

@@ -36,6 +36,7 @@ from PySide6.QtWidgets import (
from api_client import ApiClient, DnsUpstreams
from dashboard_controller import DashboardController, TraceMode
from dns_benchmark_dialog import DNSBenchmarkDialog
from traffic_mode_dialog import TrafficModeDialog
_NEXT_CHECK_RE = re.compile(r"(?i)next check in \d+s")
@@ -420,31 +421,27 @@ RU: Агрессивный режим дополнительно дергает
tip.setStyleSheet("color: gray;")
main_layout.addWidget(tip)
ups_group = QGroupBox("Upstreams (auto-save)")
ups_group.setToolTip("""EN: DNS upstreams for direct resolver mode (and non-wildcard lists in hybrid mode).
RU: DNS апстримы для direct-резолвера (и для не-wildcard списков в hybrid режиме).""")
ups_form = QFormLayout(ups_group)
self.ent_def1 = QLineEdit()
self.ent_def1.setToolTip("""EN: Upstream default1. You can set an IP (port 53 is assumed).
RU: Апстрим default1. Можно указать IP (порт 53 по умолчанию).""")
self.ent_def2 = QLineEdit()
self.ent_def2.setToolTip("""EN: Upstream default2. You can set an IP (port 53 is assumed).
RU: Апстрим default2. Можно указать IP (порт 53 по умолчанию).""")
self.ent_meta1 = QLineEdit()
self.ent_meta1.setToolTip("""EN: Upstream meta1. You can set an IP (port 53 is assumed).
RU: Апстрим meta1. Можно указать IP (порт 53 по умолчанию).""")
self.ent_meta2 = QLineEdit()
self.ent_meta2.setToolTip("""EN: Upstream meta2. You can set an IP (port 53 is assumed).
RU: Апстрим meta2. Можно указать IP (порт 53 по умолчанию).""")
self.ent_def1.textEdited.connect(self._schedule_dns_autosave)
self.ent_def2.textEdited.connect(self._schedule_dns_autosave)
self.ent_meta1.textEdited.connect(self._schedule_dns_autosave)
self.ent_meta2.textEdited.connect(self._schedule_dns_autosave)
ups_form.addRow("default1", self.ent_def1)
ups_form.addRow("default2", self.ent_def2)
ups_form.addRow("meta1", self.ent_meta1)
ups_form.addRow("meta2", self.ent_meta2)
main_layout.addWidget(ups_group)
resolver_group = QGroupBox("Resolver DNS")
resolver_group.setToolTip("""EN: Compact resolver DNS status. Open benchmark to test/apply upstreams.
RU: Компактный статус DNS резолвера. Открой benchmark для проверки/применения апстримов.""")
resolver_layout = QVBoxLayout(resolver_group)
row = QHBoxLayout()
self.btn_dns_benchmark = QPushButton("Open DNS benchmark")
self.btn_dns_benchmark.clicked.connect(self.on_open_dns_benchmark)
row.addWidget(self.btn_dns_benchmark)
row.addStretch(1)
resolver_layout.addLayout(row)
self.lbl_dns_resolver_upstreams = QLabel("Resolver upstreams: default[—, —] meta[—, —]")
self.lbl_dns_resolver_upstreams.setStyleSheet("color: gray;")
resolver_layout.addWidget(self.lbl_dns_resolver_upstreams)
self.lbl_dns_resolver_health = QLabel("Resolver health: —")
self.lbl_dns_resolver_health.setStyleSheet("color: gray;")
resolver_layout.addWidget(self.lbl_dns_resolver_health)
main_layout.addWidget(resolver_group)
smart_group = QGroupBox("SmartDNS")
smart_group.setToolTip("""EN: SmartDNS is used for wildcard domains in hybrid mode.
@@ -732,17 +729,58 @@ 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}]"
)
self.lbl_dns_resolver_upstreams.setStyleSheet("color: gray;")
avg_ms = self._ui_settings.value("dns_benchmark/last_avg_ms", None)
ok = self._ui_settings.value("dns_benchmark/last_ok", None)
fail = self._ui_settings.value("dns_benchmark/last_fail", None)
timeout = self._ui_settings.value("dns_benchmark/last_timeout", None)
if avg_ms is None or ok is None or fail is None:
self.lbl_dns_resolver_health.setText("Resolver health: no benchmark yet")
self.lbl_dns_resolver_health.setStyleSheet("color: gray;")
return
try:
avg = int(avg_ms)
ok_i = int(ok)
fail_i = int(fail)
timeout_i = int(timeout or 0)
except Exception:
self.lbl_dns_resolver_health.setText("Resolver health: no benchmark yet")
self.lbl_dns_resolver_health.setStyleSheet("color: gray;")
return
color = "green" if avg < 200 else ("#b58900" if avg <= 400 else "red")
if timeout_i > 0 and color != "red":
color = "#b58900"
self.lbl_dns_resolver_health.setText(
f"Resolver health: avg={avg}ms ok={ok_i} fail={fail_i} timeout={timeout_i}"
)
self.lbl_dns_resolver_health.setStyleSheet(f"color: {color};")
def _set_traffic_mode_state(
self,
desired_mode: str,
applied_mode: str,
preferred_iface: str,
advanced_active: bool,
auto_local_bypass: bool,
auto_local_active: bool,
ingress_reply_bypass: bool,
ingress_reply_active: bool,
bypass_candidates: int,
overrides_applied: int,
cgroup_resolved_uids: int,
cgroup_warning: str,
healthy: bool,
ingress_rule_present: bool,
ingress_nft_active: bool,
probe_ok: bool,
probe_message: str,
active_iface: str,
@@ -763,9 +801,17 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
diag_parts = []
diag_parts.append(f"preferred={preferred_iface or 'auto'}")
diag_parts.append(
f"auto_local_bypass={'on' if auto_local_bypass else 'off'}"
f"advanced={'on' if advanced_active else 'off'}"
)
if bypass_candidates > 0:
diag_parts.append(
f"auto_local={'on' if auto_local_bypass else 'off'}"
f"({'active' if auto_local_active else 'saved'})"
)
diag_parts.append(
f"ingress_reply={'on' if ingress_reply_bypass else 'off'}"
f"({'active' if ingress_reply_active else 'saved'})"
)
if auto_local_active and bypass_candidates > 0:
diag_parts.append(f"bypass_routes={bypass_candidates}")
diag_parts.append(f"overrides={overrides_applied}")
if cgroup_resolved_uids > 0:
@@ -776,6 +822,10 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
diag_parts.append(f"iface={active_iface}")
if iface_reason:
diag_parts.append(f"source={iface_reason}")
diag_parts.append(
f"ingress_diag=rule:{'ok' if ingress_rule_present else 'off'}"
f"/nft:{'ok' if ingress_nft_active else 'off'}"
)
diag_parts.append(f"probe={'ok' if probe_ok else 'fail'}")
if probe_message:
diag_parts.append(probe_message)
@@ -998,12 +1048,18 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
t.desired_mode,
t.applied_mode,
t.preferred_iface,
bool(t.advanced_active),
bool(t.auto_local_bypass),
bool(t.auto_local_active),
bool(t.ingress_reply_bypass),
bool(t.ingress_reply_active),
int(t.bypass_candidates),
int(t.overrides_applied),
int(t.cgroup_resolved_uids),
t.cgroup_warning,
bool(t.healthy),
bool(t.ingress_rule_present),
bool(t.ingress_nft_active),
bool(t.probe_ok),
t.probe_message,
t.active_iface,
@@ -1017,10 +1073,7 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
self._dns_ui_refresh = True
try:
ups = self.ctrl.dns_upstreams_view()
self.ent_def1.setText(ups.default1 or "")
self.ent_def2.setText(ups.default2 or "")
self.ent_meta1.setText(ups.meta1 or "")
self.ent_meta2.setText(ups.meta2 or "")
self._set_dns_resolver_summary(ups)
st = self.ctrl.dns_status_view()
self.ent_smartdns_addr.setText(st.smartdns_addr or "")
@@ -1037,12 +1090,6 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
self.chk_dns_via_smartdns.setChecked(hybrid_enabled)
self.chk_dns_via_smartdns.blockSignals(False)
# In direct + hybrid modes upstreams stay editable.
self.ent_def1.setEnabled(True)
self.ent_def2.setEnabled(True)
self.ent_meta1.setEnabled(True)
self.ent_meta2.setEnabled(True)
unit_state = (st.unit_state or "unknown").strip().lower()
unit_active = unit_state == "active"
self.chk_dns_unit_relay.blockSignals(True)
@@ -1386,13 +1433,6 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
def work():
if self._dns_ui_refresh:
return
ups = DnsUpstreams(
default1=self.ent_def1.text().strip(),
default2=self.ent_def2.text().strip(),
meta1=self.ent_meta1.text().strip(),
meta2=self.ent_meta2.text().strip(),
)
self.ctrl.dns_upstreams_save(ups)
self.ctrl.dns_mode_set(
self.chk_dns_via_smartdns.isChecked(),
self.ent_smartdns_addr.text().strip(),
@@ -1400,6 +1440,18 @@ RU: Источник wildcard IP: резолвер, runtime nftset SmartDNS, и
self.ctrl.log_gui("DNS settings autosaved")
self._safe(work, title="DNS save error")
def on_open_dns_benchmark(self) -> None:
def work():
dlg = DNSBenchmarkDialog(
self.ctrl,
settings=self._ui_settings,
refresh_cb=self.refresh_dns_tab,
parent=self,
)
dlg.exec()
self.refresh_dns_tab()
self._safe(work, title="DNS benchmark error")
def on_dns_mode_toggle(self) -> None:
def work():
via = self.chk_dns_via_smartdns.isChecked()