ui: make ControlGroup query robust; drop legacy instruction files
This commit is contained in:
@@ -1,460 +0,0 @@
|
|||||||
Окей, давай сначала аккуратно разрулим логику, а уже потом можно будет добивать конкретный код (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, когда на самом деле всё ок.
|
|
||||||
@@ -1,565 +0,0 @@
|
|||||||
Ок, давай разберём по двум пунктам:
|
|
||||||
|
|
||||||
Финальные имена и что именно править в коде
|
|
||||||
|
|
||||||
Почему стало ~590 IP вместо привычных ~1700
|
|
||||||
|
|
||||||
1. Имена / где что менять
|
|
||||||
|
|
||||||
Чтобы не было зоопарка, фиксируем одну схему:
|
|
||||||
|
|
||||||
внутренний конфиг резолвера:
|
|
||||||
|
|
||||||
// types.go
|
|
||||||
type DNSMode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
DNSModeDirect DNSMode = "direct" // ходим напрямую на default/meta
|
|
||||||
DNSModeSmartOnly DNSMode = "smartdns" // всё через SmartDNS
|
|
||||||
)
|
|
||||||
|
|
||||||
type dnsConfig struct {
|
|
||||||
Default []string `json:"default"`
|
|
||||||
Meta []string `json:"meta"`
|
|
||||||
SmartDNS string `json:"smartdns"`
|
|
||||||
Mode DNSMode `json:"mode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Важно: одно имя — dnsConfig, одно поле режима — Mode.
|
|
||||||
Никаких ViaSmartDNS, ViaSmartDnS и т.п. – только Mode.
|
|
||||||
|
|
||||||
1.1. loadDNSConfig (используется резолвером)
|
|
||||||
|
|
||||||
Внутри dns_settings.go (или там, где у тебя сейчас чтение /etc/selective-vpn/dns-upstreams.conf) логика должна отдавать именно такой dnsConfig:
|
|
||||||
|
|
||||||
func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
|
|
||||||
cfg := dnsConfig{
|
|
||||||
Default: []string{defaultDNS1, defaultDNS2},
|
|
||||||
Meta: []string{defaultMeta1, defaultMeta2},
|
|
||||||
SmartDNS: "",
|
|
||||||
Mode: DNSModeDirect,
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
// файла нет – просто дефолты
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
var def, meta []string
|
|
||||||
mode := DNSModeDirect
|
|
||||||
smart := ""
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, ln := range lines {
|
|
||||||
s := strings.TrimSpace(ln)
|
|
||||||
if s == "" || strings.HasPrefix(s, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.Fields(s)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := strings.ToLower(parts[0])
|
|
||||||
vals := parts[1:]
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
case "default":
|
|
||||||
def = append(def, vals...)
|
|
||||||
case "meta":
|
|
||||||
meta = append(meta, vals...)
|
|
||||||
case "smartdns":
|
|
||||||
if len(vals) > 0 {
|
|
||||||
smart = vals[0]
|
|
||||||
}
|
|
||||||
case "mode":
|
|
||||||
switch DNSMode(vals[0]) {
|
|
||||||
case DNSModeSmartOnly:
|
|
||||||
mode = DNSModeSmartOnly
|
|
||||||
default:
|
|
||||||
mode = DNSModeDirect
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(def) > 0 {
|
|
||||||
cfg.Default = def
|
|
||||||
}
|
|
||||||
if len(meta) > 0 {
|
|
||||||
cfg.Meta = meta
|
|
||||||
}
|
|
||||||
if smart != "" {
|
|
||||||
cfg.SmartDNS = smart
|
|
||||||
}
|
|
||||||
cfg.Mode = mode
|
|
||||||
|
|
||||||
if logf != nil {
|
|
||||||
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
|
|
||||||
logf("dns-config: mode=smartdns smartdns=%s", cfg.SmartDNS)
|
|
||||||
} else {
|
|
||||||
logf("dns-config: mode=direct default=%v meta=%v", cfg.Default, cfg.Meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
1.2. runResolverJob — как он выбирает режим
|
|
||||||
|
|
||||||
Твой кусок:
|
|
||||||
|
|
||||||
cfg := loadDNSConfig(opts.DNSConfigPath, logf)
|
|
||||||
if logf != nil {
|
|
||||||
if cfg.ViaSmartDNS {
|
|
||||||
logf("resolver dns mode: SmartDNS-only (%v)", cfg.Default)
|
|
||||||
} else {
|
|
||||||
logf("resolver dns mode: direct default=%v meta=%v", cfg.Default, cfg.Meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Нужно заменить на:
|
|
||||||
|
|
||||||
cfg := loadDNSConfig(opts.DNSConfigPath, logf)
|
|
||||||
|
|
||||||
if logf != nil {
|
|
||||||
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
|
|
||||||
logf("resolver dns mode: SmartDNS-only (%s)", cfg.SmartDNS)
|
|
||||||
} else {
|
|
||||||
logf("resolver dns mode: direct default=%v meta=%v", cfg.Default, cfg.Meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
1.3. Как резолвер реально ходит в DNS
|
|
||||||
|
|
||||||
В resolveHostGo сейчас у тебя примерно так (я по смыслу):
|
|
||||||
|
|
||||||
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, logf func(string, ...any)) ([]string, int) {
|
|
||||||
useMeta := false
|
|
||||||
for _, m := range metaSpecial {
|
|
||||||
if host == m {
|
|
||||||
useMeta = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsList := cfg.Default
|
|
||||||
if useMeta {
|
|
||||||
dnsList = cfg.Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, errs := digA(host, dnsList, 3*time.Second, logf)
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Делаем так, чтобы при включённом SmartDNS всегда шли только на него:
|
|
||||||
|
|
||||||
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, logf func(string, ...any)) ([]string, int) {
|
|
||||||
useMeta := false
|
|
||||||
for _, m := range metaSpecial {
|
|
||||||
if host == m {
|
|
||||||
useMeta = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dnsList []string
|
|
||||||
|
|
||||||
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
|
|
||||||
// ВСЁ через SmartDNS
|
|
||||||
dnsList = []string{cfg.SmartDNS}
|
|
||||||
} else if useMeta {
|
|
||||||
dnsList = cfg.Meta
|
|
||||||
} else {
|
|
||||||
dnsList = cfg.Default
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, errs := digA(host, dnsList, 3*time.Second, logf)
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
И для PTR-запросов (resolveStaticLabels → digPTR) — аналогично:
|
|
||||||
|
|
||||||
func resolveStaticLabels(entries [][3]string, cfg dnsConfig, ptrCache map[string]any, ttl int, logf func(string, ...any)) (map[string][]string, int, int) {
|
|
||||||
...
|
|
||||||
dnsForPtr := ""
|
|
||||||
|
|
||||||
if cfg.Mode == DNSModeSmartOnly && cfg.SmartDNS != "" {
|
|
||||||
dnsForPtr = cfg.SmartDNS
|
|
||||||
} else if len(cfg.Default) > 0 {
|
|
||||||
dnsForPtr = cfg.Default[0]
|
|
||||||
} else {
|
|
||||||
dnsForPtr = defaultDNS1
|
|
||||||
}
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
1.4. routes_update.go — что именно должно быть
|
|
||||||
|
|
||||||
Там, где запускается резолвер (что-то вроде runResolverJob), должно быть только это (без своих конфигов):
|
|
||||||
|
|
||||||
opts := ResolverOpts{
|
|
||||||
DomainsPath: domainDir + "/bases.txt",
|
|
||||||
MetaPath: domainDir + "/meta-special.txt",
|
|
||||||
StaticPath: staticIPsFile,
|
|
||||||
CachePath: stateDir + "/resolver-cache.json",
|
|
||||||
PtrCachePath: stateDir + "/resolver-ptr-cache.json",
|
|
||||||
TraceLog: traceLogPath,
|
|
||||||
TTL: 24 * 3600, // или твой config.ResolverTTLSeconds
|
|
||||||
Workers: 200, // или config.ResolverWorkers
|
|
||||||
DNSConfigPath: dnsUpstreamsConf, // ВАЖНО: один путь, один формат
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := runResolverJob(opts, logf)
|
|
||||||
|
|
||||||
|
|
||||||
Никаких других структур / полей для DNS в этом файле не нужно.
|
|
||||||
Вся магия DNS живёт в dns_settings.go + resolver.go.
|
|
||||||
|
|
||||||
2. Почему теперь 590 IP, а не 1700
|
|
||||||
|
|
||||||
Смотрим на лог:
|
|
||||||
|
|
||||||
summary: domains=14864, cache_hits=2 resolved_now=1281 unresolved=13581 static_entries=294 ...
|
|
||||||
unique_ips=590 ...
|
|
||||||
|
|
||||||
|
|
||||||
Это говорит о трёх вещах:
|
|
||||||
|
|
||||||
Резолвер реально прошёлся по всем ~15k доменам.
|
|
||||||
|
|
||||||
Ответы он получил только по ~1281 домену.
|
|
||||||
|
|
||||||
Из них после фильтра isPrivateIPv4 осталось 590 уникальных публичных IP.
|
|
||||||
|
|
||||||
Что поменялось по сравнению с прошлой версией:
|
|
||||||
|
|
||||||
2.1. Всё теперь идёт ТОЛЬКО через SmartDNS
|
|
||||||
|
|
||||||
Раньше схема была примерно:
|
|
||||||
|
|
||||||
резолвер → напрямую на 94.140.* + 46.243.*
|
|
||||||
|
|
||||||
SmartDNS жил своей жизнью как локальный помощник
|
|
||||||
|
|
||||||
Сейчас при Use SmartDNS for resolver:
|
|
||||||
|
|
||||||
резолвер → только на 127.0.0.1#6053
|
|
||||||
|
|
||||||
SmartDNS → дальше сам ходит на 192.168.50.10:53 (bind) → AdGuard DNS и т.д.
|
|
||||||
|
|
||||||
Поэтому:
|
|
||||||
|
|
||||||
все блокировки / NXDOMAIN / подмена на 0.0.0.0 / 127.0.0.1 происходят до нашего Go-кода;
|
|
||||||
|
|
||||||
Go-резолвер видит либо нормальный внешний IP, либо 0.0.0.0/локалку/ошибку.
|
|
||||||
|
|
||||||
2.2. Мы фильтруем все приватные и мусорные IP
|
|
||||||
|
|
||||||
В runResolverJob:
|
|
||||||
|
|
||||||
if len(rawIps) > 0 && ts > 0 && now-int(ts) <= ttl {
|
|
||||||
for _, v := range rawIps {
|
|
||||||
if s, ok := v.(string); ok && s != "" && !isPrivateIPv4(s) {
|
|
||||||
ips = append(ips, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
...
|
|
||||||
if isPrivateIPv4(ip) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
А isPrivateIPv4 считает приватными:
|
|
||||||
|
|
||||||
0.x.x.x
|
|
||||||
|
|
||||||
10.x.x.x
|
|
||||||
|
|
||||||
127.x.x.x
|
|
||||||
|
|
||||||
192.168.x.x
|
|
||||||
|
|
||||||
172.16–31.x.x
|
|
||||||
|
|
||||||
и вообще всё, что невалидное
|
|
||||||
|
|
||||||
То есть:
|
|
||||||
|
|
||||||
если AdGuard/SmartDNS возвращает 0.0.0.0 или 127.0.0.1 для трекеров / рекламы,
|
|
||||||
мы их больше не учитываем вообще;
|
|
||||||
|
|
||||||
раньше, когда ходили мимо SmartDNS, таких «мусорных» ответов было гораздо меньше.
|
|
||||||
|
|
||||||
Отсюда очень понятная картина:
|
|
||||||
|
|
||||||
1700 IP ранее = реальные IP + всякий мусор (0.0.0.0, локалки, возможно ещё что-то);
|
|
||||||
|
|
||||||
590 IP сейчас = только реальные публичные адреса, которые реально нужно засунуть в ipset и гонять через VPN.
|
|
||||||
|
|
||||||
2.3. Что с ошибками в логах вида ... on 192.168.50.10:53
|
|
||||||
|
|
||||||
Это просто текст ошибки из SmartDNS/bind, который пробрасывается наружу:
|
|
||||||
|
|
||||||
ptr error 104.26.10.38 via 127.0.0.1:6053: lookup 38.10.26.104.in-addr.arpa. on 192.168.50.10:53: no such host
|
|
||||||
|
|
||||||
|
|
||||||
мы обращаемся к 127.0.0.1:6053;
|
|
||||||
|
|
||||||
SmartDNS дальше ходит на 192.168.50.10:53 и говорит «no such host»;
|
|
||||||
|
|
||||||
net.Resolver формирует такое красивое сообщение.
|
|
||||||
|
|
||||||
То есть Go-код SmartDNS не обходит, всё как раз правильно идёт через него.
|
|
||||||
|
|
||||||
Если хочешь «как раньше» по цифрам
|
|
||||||
|
|
||||||
Если прям хочется снова видеть ~1700 IP (включая мусор):
|
|
||||||
|
|
||||||
Можно временно ослабить фильтр:
|
|
||||||
|
|
||||||
func isPrivateIPv4(ip string) bool {
|
|
||||||
// В тесте можно не резать 0.0.0.0 / 127.x.x.x и т.п.,
|
|
||||||
// просто вернуть false и посмотреть цифры
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Или сделать две метрики:
|
|
||||||
|
|
||||||
unique_ips_total — с учётом всего, что вернул DNS;
|
|
||||||
|
|
||||||
unique_ips_public — после фильтра приватных;
|
|
||||||
|
|
||||||
и логировать обе.
|
|
||||||
|
|
||||||
Но с точки зрения селективных маршрутов тебе как раз нужны только публичные IP, так что текущие 590 очень вероятно ближе к «правильной» картине.
|
|
||||||
|
|
||||||
Если хочешь, в следующем шаге могу:
|
|
||||||
|
|
||||||
дать патч для логов, чтобы в summary было:
|
|
||||||
unique_ips_public, unique_ips_private, blocked_0_ips и т.д.,
|
|
||||||
|
|
||||||
плюс подсветить в Trace вкладке отдельной строкой, сколько доменов SmartDNS убил в ноль.
|
|
||||||
|
|
||||||
в том то и дело, что теперь половина доменов которые железно работали, не работают теперь, что то изменилось, не понимаю что, у нас кеш пишется? что же не так ... и кстати логи по прежнему не верно отображается
|
|
||||||
Перестала думать
|
|
||||||
в том то и дело, что теперь половина доменов которые железно работали, не работают теперь, что то изменилось, не понимаю что, у нас кеш пишется? что же не так ... и кстати логи по прежнему не верно отображается
|
|
||||||
Думал на протяжении 2m 20s
|
|
||||||
|
|
||||||
Окей, давай по порядку, что видно из скрина логов, и что мы реально поменяли в коде.
|
|
||||||
|
|
||||||
⚠️ Сначала техническая ремарка
|
|
||||||
Часть файлов, которые ты раньше заливал (resolver.go, config.go и т.д.), у меня уже недоступны – система их подчищает со временем. Я помню общую структуру и последние куски, которые ты присылал в текст, но если захочешь, чтобы я дал прям точные патчи построчно по актуальному resolver.go / dns_settings.go, лучше закинуть их ещё раз.
|
|
||||||
|
|
||||||
1. Что сейчас не так по логам
|
|
||||||
|
|
||||||
На твоём скрине снизу была строчка типа:
|
|
||||||
|
|
||||||
dns_errors=13295 unresolved=13581 unique_ips=590 ...
|
|
||||||
|
|
||||||
|
|
||||||
И вверху куча:
|
|
||||||
|
|
||||||
dns warn www.twitterinc.com via 127.0.0.1:6053:
|
|
||||||
lookup www.twitterinc.com on 192.168.50.53:53: i/o timeout
|
|
||||||
...
|
|
||||||
dns warn www.twitterstat.us via 127.0.0.1:6053:
|
|
||||||
dial udp 127.0.0.1:6053: i/o timeout
|
|
||||||
|
|
||||||
|
|
||||||
Это говорит о простом:
|
|
||||||
|
|
||||||
Домены в большинстве случаев не резолвятся вообще.
|
|
||||||
Не из-за кэша, а из-за того, что SmartDNS/BIND/AdGuard под нагрузкой просто не отвечает (таймауты).
|
|
||||||
|
|
||||||
Из-за этого у нас:
|
|
||||||
|
|
||||||
resolved_now маленький
|
|
||||||
|
|
||||||
dns_errors огромный
|
|
||||||
|
|
||||||
unique_ips ≈ 500+, хотя раньше было ≈ 1700.
|
|
||||||
|
|
||||||
То есть лог честный: с точки зрения нашего Go-резолвера для этих доменов реально "no IPs".
|
|
||||||
|
|
||||||
2. Что именно изменилось в логике DNS
|
|
||||||
|
|
||||||
До рефакторинга:
|
|
||||||
|
|
||||||
наш resolver.go бился напрямую в default1/default2/meta1/meta2 (типа 94.140.14.15 и т.д.);
|
|
||||||
|
|
||||||
SmartDNS жил сам по себе и занимался вайлдкарт-ipset’ами.
|
|
||||||
|
|
||||||
После рефакторинга (который мы как раз и делали по твоей просьбе):
|
|
||||||
|
|
||||||
если отмечено «Use SmartDNS for resolver» — мы:
|
|
||||||
|
|
||||||
подменяем cfg.Default = []string{ smartdnsAddr } (127.0.0.1#6053);
|
|
||||||
|
|
||||||
и всё резолвим через SmartDNS;
|
|
||||||
|
|
||||||
параллельно мы оставили очень агрессивный пул: workers = 200, domains ≈ 15k.
|
|
||||||
|
|
||||||
И получается ситуация:
|
|
||||||
|
|
||||||
15 000 доменов × 200 воркеров × таймаут 3 секунды → местный SmartDNS + BIND просто не успевает, начинает дропать/таймаутить, мы видим кучу i/o timeout, и в итоге получаем только ~590 IP.
|
|
||||||
|
|
||||||
То есть сломали не кэш, а заддосили свой же локальный resolver 😅
|
|
||||||
|
|
||||||
3. Кэш – жив он или нет?
|
|
||||||
|
|
||||||
Да, по коду (который ты кидал):
|
|
||||||
|
|
||||||
domainCache := loadJSONMap(opts.CachePath)
|
|
||||||
ptrCache := loadJSONMap(opts.PtrCachePath)
|
|
||||||
...
|
|
||||||
domainCache[host] = map[string]any{
|
|
||||||
"ips": r.ips,
|
|
||||||
"last_resolved": now,
|
|
||||||
}
|
|
||||||
...
|
|
||||||
res.DomainCache = domainCache
|
|
||||||
res.PtrCache = ptrCache
|
|
||||||
...
|
|
||||||
saveJSON(res.DomainCache, opts.CachePath)
|
|
||||||
saveJSON(res.PtrCache, opts.PtrCachePath)
|
|
||||||
|
|
||||||
|
|
||||||
Кэш:
|
|
||||||
|
|
||||||
читается в начале (loadJSONMap);
|
|
||||||
|
|
||||||
обновляется только для успешных доменов;
|
|
||||||
|
|
||||||
сохраняется в конце.
|
|
||||||
|
|
||||||
Проблема в другом: когда у тебя за один прогон 13k DNS-ошибок, в кэш почти нечего писать — большая часть доменов вообще не получила IP, и в следующий прогон они снова идут "с нуля".
|
|
||||||
|
|
||||||
4. Что я бы сделал сейчас в коде (минимально и по делу)
|
|
||||||
4.1. Ограничить параллелизм при работе через SmartDNS
|
|
||||||
|
|
||||||
В runResolverJob (там, где ты сейчас считаешь workers), сделай так:
|
|
||||||
|
|
||||||
workers := opts.Workers
|
|
||||||
if workers <= 0 {
|
|
||||||
workers = 200
|
|
||||||
}
|
|
||||||
|
|
||||||
// если работаем через SmartDNS – не лупим по нему 200 горутин
|
|
||||||
if cfg.ViaSmartDNS && workers > 50 {
|
|
||||||
workers = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
// safety clamps
|
|
||||||
if workers < 1 {
|
|
||||||
workers = 1
|
|
||||||
}
|
|
||||||
if workers > 500 {
|
|
||||||
workers = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
if logf != nil {
|
|
||||||
logf("resolver start: domains=%d ttl=%ds workers=%d viaSmartDNS=%v",
|
|
||||||
len(domains), ttl, workers, cfg.ViaSmartDNS)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Суть:
|
|
||||||
|
|
||||||
при прямых DNS — всё как раньше (200 воркеров нормально жрут 94.140.*);
|
|
||||||
|
|
||||||
при SmartDNS — ставим жёсткий потолок, например 50 (можно 30-80 поэкспериментировать).
|
|
||||||
|
|
||||||
Это радикально уменьшит количество одновременных запросов к SmartDNS/BIND, и таймаутов станет сильно меньше → dns_errors упадут, unique_ips вырастут.
|
|
||||||
|
|
||||||
4.2. (Опционально) Увеличить таймаут для SmartDNS
|
|
||||||
|
|
||||||
Сейчас digA вызывается так (по прошлому коду):
|
|
||||||
|
|
||||||
ips, errs := digA(host, dnsList, 3*time.Second, logf)
|
|
||||||
|
|
||||||
|
|
||||||
Если хочешь мягко подстраховаться:
|
|
||||||
|
|
||||||
timeout := 3 * time.Second
|
|
||||||
if cfg.ViaSmartDNS {
|
|
||||||
timeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
ips, errs := digA(host, dnsList, timeout, logf)
|
|
||||||
|
|
||||||
|
|
||||||
Для этого нужно просто протащить cfg или флажок viaSmartDNS до resolveHostGo:
|
|
||||||
|
|
||||||
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, viaSmartDNS bool, logf func(string, ...any)) ([]string, int) {
|
|
||||||
...
|
|
||||||
timeout := 3 * time.Second
|
|
||||||
if viaSmartDNS {
|
|
||||||
timeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
ips, errs := digA(host, dnsList, timeout, logf)
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
И в runResolverJob вызывать:
|
|
||||||
|
|
||||||
ips, errs := resolveHostGo(j.host, cfg, metaSpecial, cfg.ViaSmartDNS, logf)
|
|
||||||
|
|
||||||
|
|
||||||
Это не обязательно, но в связке с уменьшением workers даст более стабильную картину.
|
|
||||||
|
|
||||||
5. Почему лог «неверно отображается»
|
|
||||||
|
|
||||||
Тут важно разделить:
|
|
||||||
|
|
||||||
То, что реально делает наш резолвер.
|
|
||||||
Если он пишет:
|
|
||||||
|
|
||||||
dns warn www.twitterinc.com via 127.0.0.1:6053: dial udp 127.0.0.1:6053: i/o timeout
|
|
||||||
|
|
||||||
|
|
||||||
– это значит, что конкретно этот запрос реально упал по таймауту.
|
|
||||||
Даже если "в браузере всё открывается", это уже другие запросы, в другое время и с другим паттерном (не 15k доменов за раз).
|
|
||||||
|
|
||||||
Фильтрация на вкладке Events/SmartDNS/Full.
|
|
||||||
Мы уже делали фильтр для mode=gui, чтобы убрать:
|
|
||||||
|
|
||||||
[routes], resolver, dnstt, smartdns, nft add element, cache hit и т.п. шум.
|
|
||||||
|
|
||||||
Если сейчас в режиме Events ты всё равно видишь этот трэш с [routes] dns warn ... — значит, в handleTraceJSON в ветке case "gui": надо ещё сильнее ужать фильтр, например:
|
|
||||||
|
|
||||||
if strings.HasPrefix(ll, "[routes]") ||
|
|
||||||
strings.Contains(ll, " dns warn ") ||
|
|
||||||
strings.Contains(ll, " dns error ") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Тогда:
|
|
||||||
|
|
||||||
весь подробный DNS-дебаг остаётся в Full и SmartDNS;
|
|
||||||
|
|
||||||
Events показывает только человеческие штуки [gui], [login], [vpn], [error] и т.д.
|
|
||||||
|
|
||||||
Если хочешь, чтобы я точно подогнал фильтр под твой текущий trace_handlers.go, просто перекинь файл ещё раз — старая версия у меня уже протухла.
|
|
||||||
|
|
||||||
6. Что бы я сделал дальше по шагам
|
|
||||||
|
|
||||||
Внести правку с ограничением workers при ViaSmartDNS.
|
|
||||||
|
|
||||||
(Опционально) Протянуть флаг viaSmartDNS в resolveHostGo и увеличить таймаут до 5s.
|
|
||||||
|
|
||||||
Запустить update-selective-routes и посмотреть:
|
|
||||||
|
|
||||||
строку summary (особенно dns_errors и unique_ips);
|
|
||||||
|
|
||||||
стало ли больше IP (должно быть ближе к твоим старым ~1700, хотя не обязательно 1:1).
|
|
||||||
|
|
||||||
Если с IP всё норм, но Events всё ещё замусорен – подправить фильтр в handleTraceJSON (mode = "gui").
|
|
||||||
|
|
||||||
Если хочешь, чтобы я прям по твоим актуальным файлам выписал патчи diffом (resolver.go, dns_settings.go, trace_handlers.go) – скинь их ещё раз, я аккуратно вставлю всё в нужные места без переименования функций и с теми же структурами, что уже есть.
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
Инструкция 3 (safe-версия)
|
|
||||||
|
|
||||||
Цель:
|
|
||||||
Сделать улучшения резолвера без risky-переписывания, сохранить обратную совместимость с текущим GUI/API,
|
|
||||||
и убрать главные источники шума: массовые NXDOMAIN, таймауты и нечитабельные метрики.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
0) Что подтверждено по текущему состоянию
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
1. Основной рабочий код сейчас в:
|
|
||||||
- app/resolver.go
|
|
||||||
- app/routes_update.go
|
|
||||||
- app/dns_settings.go
|
|
||||||
- app/domains_handlers.go
|
|
||||||
|
|
||||||
2. Логи показывают:
|
|
||||||
- много NXDOMAIN (ожидаемо при широком base x subs)
|
|
||||||
- заметную долю timeout
|
|
||||||
- один агрегированный счетчик dns_errors, из-за чего трудно понять причину деградации.
|
|
||||||
|
|
||||||
3. Формат dns upstream у нас: host#port (например 94.140.14.15#53 или 127.0.0.1#6053).
|
|
||||||
Это важно: нельзя использовать валидацию, которая принимает только host:port.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
1) Архитектурное решение (рекомендуемое)
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
Не оставлять только переключатель direct <-> smartdns.
|
|
||||||
Сделать 3 режима резолвера:
|
|
||||||
|
|
||||||
- direct:
|
|
||||||
обычные домены через default/meta upstream.
|
|
||||||
|
|
||||||
- smartdns:
|
|
||||||
все домены через SmartDNS address.
|
|
||||||
|
|
||||||
- hybrid_wildcard (recommended):
|
|
||||||
только wildcard-домены через SmartDNS, остальные напрямую через default/meta.
|
|
||||||
|
|
||||||
Почему так лучше:
|
|
||||||
- сохраняем скорость и отказоустойчивость direct для обычных доменов;
|
|
||||||
- wildcard-логику держим строго в SmartDNS, как ты и хотел;
|
|
||||||
- не ломаем текущий UX: можно оставить старый bool и маппить его на mode.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
2) Что НЕ внедряем из старой инструкция3
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
1. Не используем netip.ParseAddrPort для upstream-валидации (ломает host#port).
|
|
||||||
2. Не используем netip.MustParseAddr в hot path (может паниковать).
|
|
||||||
3. Не добавляем лишний semaphore поверх worker pool (сложность без явной выгоды).
|
|
||||||
4. Не делаем агрессивный рефактор API-контрактов без backward-compat.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
3) Пакет безопасных правок (приоритет P1)
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
P1.1 - Режимы DNS (backward compatible)
|
|
||||||
Файлы:
|
|
||||||
- app/types.go
|
|
||||||
- app/dns_settings.go
|
|
||||||
- app/routes_update.go
|
|
||||||
- app/resolver.go
|
|
||||||
|
|
||||||
Изменения:
|
|
||||||
1) Ввести enum режима:
|
|
||||||
type DNSResolverMode string
|
|
||||||
const (
|
|
||||||
DNSModeDirect DNSResolverMode = "direct"
|
|
||||||
DNSModeSmartDNS DNSResolverMode = "smartdns"
|
|
||||||
DNSModeHybrid DNSResolverMode = "hybrid_wildcard"
|
|
||||||
)
|
|
||||||
|
|
||||||
2) Расширить DNSMode/DNSStatusResponse/DNSModeRequest полем Mode,
|
|
||||||
но оставить ViaSmartDNS для старого GUI:
|
|
||||||
- если Mode пустой, использовать ViaSmartDNS:
|
|
||||||
- true -> smartdns
|
|
||||||
- false -> direct
|
|
||||||
|
|
||||||
3) В ResolverOpts передавать Mode и список wildcard-доменов (один раз на job).
|
|
||||||
|
|
||||||
4) В resolveHostGo выбирать dnsList так:
|
|
||||||
- mode == smartdns: []{smartdnsAddr}
|
|
||||||
- mode == hybrid_wildcard и host совпал с wildcard: []{smartdnsAddr}
|
|
||||||
- иначе: meta или default по текущей логике.
|
|
||||||
|
|
||||||
Примечание:
|
|
||||||
Wildcard-список уже хранится в smartdns-wildcards.json через /api/v1/smartdns/wildcards.
|
|
||||||
Нужно только использовать его в резолвере.
|
|
||||||
|
|
||||||
P1.2 - Upstream fallback с классификацией ошибок
|
|
||||||
Файл:
|
|
||||||
- app/resolver.go
|
|
||||||
|
|
||||||
Изменения:
|
|
||||||
1) В digA:
|
|
||||||
- идти по upstream последовательно;
|
|
||||||
- timeout/temporary -> fallback на следующий upstream;
|
|
||||||
- nxdomain -> остановить попытки для домена (дальше пробовать бессмысленно).
|
|
||||||
|
|
||||||
2) Классифицировать ошибки через net.DNSError + fallback по тексту:
|
|
||||||
- nxdomain
|
|
||||||
- timeout
|
|
||||||
- temporary
|
|
||||||
- other
|
|
||||||
|
|
||||||
3) Вместо одного dns_errors вести структуру счетчиков:
|
|
||||||
dns_attempts, dns_ok, dns_nxdomain, dns_timeout, dns_temporary, dns_other.
|
|
||||||
|
|
||||||
P1.3 - Разделенные метрики в summary
|
|
||||||
Файл:
|
|
||||||
- app/resolver.go
|
|
||||||
|
|
||||||
Изменения:
|
|
||||||
1) Обновить финальный лог "resolve summary" с раздельными счетчиками DNS-ошибок.
|
|
||||||
2) Добавить per-upstream агрегаты (минимум attempts/ok/timeout/nxdomain/other).
|
|
||||||
Формат может быть одной строкой JSON, чтобы GUI/анализатору было проще парсить.
|
|
||||||
|
|
||||||
P1.4 - Ограничение domain expansion
|
|
||||||
Файл:
|
|
||||||
- app/routes_update.go
|
|
||||||
|
|
||||||
Изменения:
|
|
||||||
1) Добавить конфиг-лимиты через env:
|
|
||||||
- RESOLVE_SUBS_PER_BASE_LIMIT (например default 25)
|
|
||||||
- RESOLVE_DOMAINS_HARD_CAP (например default 12000)
|
|
||||||
|
|
||||||
2) После построения domainSet:
|
|
||||||
- сортировать домены;
|
|
||||||
- при превышении hard cap обрезать хвост детерминированно;
|
|
||||||
- писать явный warning в trace.
|
|
||||||
|
|
||||||
3) Логировать breakdown:
|
|
||||||
bases_count, subs_count, expanded_count, total_domains.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
4) Пакет улучшений P2 (после P1)
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
P2.1 - Negative cache
|
|
||||||
Файл:
|
|
||||||
- app/resolver.go
|
|
||||||
|
|
||||||
Идея:
|
|
||||||
- кэшировать nxdomain/servfail на короткий TTL (например 10-20 минут),
|
|
||||||
чтобы не долбить одинаковые несуществующие имена каждую прогонку.
|
|
||||||
|
|
||||||
P2.2 - PTR retry (ограниченный)
|
|
||||||
Файл:
|
|
||||||
- app/resolver.go
|
|
||||||
|
|
||||||
Идея:
|
|
||||||
- для digPTR сделать 1-2 retry только на timeout/temporary;
|
|
||||||
- не ретраить nxdomain.
|
|
||||||
|
|
||||||
P2.3 - GUI/API отображение режима
|
|
||||||
Файлы GUI:
|
|
||||||
- selective-vpn-gui/api_client.py
|
|
||||||
- selective-vpn-gui/dashboard_controller.py
|
|
||||||
- selective-vpn-gui/vpn_dashboard_qt.py
|
|
||||||
|
|
||||||
Идея:
|
|
||||||
- показать mode = direct/smartdns/hybrid_wildcard;
|
|
||||||
- оставить старый toggle рабочим (маппинг direct/smartdns),
|
|
||||||
а hybrid можно добавить как отдельный выбор (позже).
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
5) Порядок внедрения (рекомендуемый)
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
Шаг 1:
|
|
||||||
Реализовать режимы + fallback + split-метрики (P1.1, P1.2, P1.3).
|
|
||||||
|
|
||||||
Шаг 2:
|
|
||||||
Добавить лимиты expansion (P1.4), проверить изменение unresolved/timeouts.
|
|
||||||
|
|
||||||
Шаг 3:
|
|
||||||
Добавить negative cache и PTR retry (P2.1, P2.2).
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
6) Критерии готовности
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
1. В trace видно не один dns_errors, а раздельные причины.
|
|
||||||
2. В режиме direct при падении одного upstream запросы частично продолжают проходить через fallback.
|
|
||||||
3. В режиме hybrid_wildcard wildcard-домены уходят через SmartDNS, остальные через direct.
|
|
||||||
4. Количество timeout и общее время прогона снижаются относительно текущего baseline.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
7) Короткий диагноз по сути
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
Проблема не в одном SmartDNS bottleneck.
|
|
||||||
Основной вклад сейчас дает комбинация:
|
|
||||||
- большой domain expansion,
|
|
||||||
- много несуществующих имен (NXDOMAIN),
|
|
||||||
- и отсутствие четкой стратегии fallback/метрик.
|
|
||||||
|
|
||||||
Safe-путь:
|
|
||||||
ввести гибридный routing DNS + раздельную диагностику + контролируемый expansion.
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
Анализ твоего кода resolver.go
|
|
||||||
Я внимательно прошёлся по всему коду: структура хорошая (модульный, с логами, кэшем, concurrent резолвом), логика правильная для задачи (домены → IPs с meta/default upstream'ами, static с PTR-лейблами, приватные IP фильтруем). Это уже рабочий инструмент, который может обрабатывать тысячи доменов без сбоев. Но да, есть потенциальные проблемы и места для улучшений — некоторые очевидные (edge-кейсы), другие — для масштаба/надёжности.
|
|
||||||
Я разделил на потенциальные поломки (где код может крашнуться или дать неверный результат), слабые места (не поломки, но риски в проде) и рекомендации с кодовыми примерами. Всё основано на твоём коде — не выдумываю, а анализирую.
|
|
||||||
1. Потенциальные поломки (где код может упасть или дать мусор)
|
|
||||||
|
|
||||||
Конфликт портов в upstream (в resolveHostGo и digPTR):
|
|
||||||
splitDNS возвращает port (e.g., "127.0.0.1#6053" → "127.0.0.1", "6053").
|
|
||||||
Но в net.Resolver.Dial ты используешь net.JoinHostPort(server, port) — это ок, но Resolver по дефолту использует UDP. Если upstream требует TCP (e.g., большие ответы >512 байт) — запрос fail (dns.exception.SyntaxError или timeout).
|
|
||||||
Поломка: Для доменов с большим TXT/NS — no IPs, но errs++ (не обновит кэш).
|
|
||||||
Тест: Резолв домен с большим ответом (e.g., "example.com" TXT) на upstream с port. Если fail — вот оно.
|
|
||||||
|
|
||||||
Timeout слишком короткий (3s в ctx):
|
|
||||||
На медленных upstream'ах (e.g., DNS over Tor или VPN) — запрос не успеет, вернёт [] и errs++.
|
|
||||||
Поломка: Unresolved домены, даже если upstream живой. При повторном запуске — из кэша не возьмёт (потому что не обновил).
|
|
||||||
Тест: Установи медленный DNS (e.g., 8.8.8.8 с delay) — увидишь dns.timeout.
|
|
||||||
|
|
||||||
PTR fail без retry (в resolveStaticLabels):
|
|
||||||
digPTR без retry — если upstream временно down, PTR не получит, и лейбл = "*[STATIC-IP]".
|
|
||||||
Поломка: Если все upstream fail — все static без PTR, но код не крашится. В проде — потеря лейблов.
|
|
||||||
Тест: Установи invalid upstream — увидишь ptr_errors++, но код продолжит.
|
|
||||||
|
|
||||||
loadDNSConfig fallback на дефолт без валидации:
|
|
||||||
Если файл битый (e.g., "default abc#invalid") — fallback на дефолт, но дефолт может не работать.
|
|
||||||
Поломка: Silent fail — резолв будет работать на старых дефолтах, но юзер не поймёт почему.
|
|
||||||
Тест: Сделай файл с кривыми upstream'ами — увидишь в логах fallback, но без ошибки.
|
|
||||||
|
|
||||||
uniqueStrings и map в IPMap — но без сортировки в map:
|
|
||||||
IPMap использует map[string]struct{} для dedup, но при append в res.IPMap — sort.Slice по IP, но лейблы внутри не отсортированы.
|
|
||||||
Поломка: Не поломка, но нестабильный порядок лейблов в mapLines — если GUI полагается на порядок.
|
|
||||||
Тест: Несколько лейблов на IP — порядок random.
|
|
||||||
|
|
||||||
isPrivateIPv4 не обрабатывает /mask в IP:
|
|
||||||
Проверяет только base IP, но если ip = "10.0.0.0/8" — parts = ["10.0.0.0/8"] → len!=4 → return true (ок). Но если mask invalid — не catch.
|
|
||||||
Поломка: Если static с кривым CIDR (e.g., "1.2.3.4/33") — пропустит как non-private.
|
|
||||||
Тест: Добавь "1.2.3.4/33" в static — пройдёт, хотя invalid.
|
|
||||||
|
|
||||||
runResolverJob: если workers=0 или ttl=0 — не краш, но дефолты
|
|
||||||
Ты clamp'ишь (ttl 60..86400, workers 1..500) — хорошо, но если envInt fail — silent.
|
|
||||||
Поломка: Не поломка, но если ENV кривой — silent fallback.
|
|
||||||
|
|
||||||
|
|
||||||
2. Слабые места (не поломки, но риски в проде)
|
|
||||||
|
|
||||||
Concurrent резолв без rate-limit:
|
|
||||||
80 workers + 500+ доменов — может flood upstream'ы (DNS rate-limit ban).
|
|
||||||
Риск: Upstream забанит IP, все запросы fail.
|
|
||||||
|
|
||||||
Нет IPv6:
|
|
||||||
Всё только IPv4 (A-записи, isPrivateIPv4). Если домен только AAAA — no IPs.
|
|
||||||
Риск: Современные сайты (e.g., google) имеют IPv6 — потеряешь трафик.
|
|
||||||
|
|
||||||
Кэш без invalidation:
|
|
||||||
Кэш на TTL, но если upstream сменился — старый кэш может быть invalid.
|
|
||||||
Риск: После смены DNS — старые IPs в кэше до TTL.
|
|
||||||
|
|
||||||
PTR без кэша ошибок:
|
|
||||||
Если PTR fail — всё равно пишем в кэш empty names. Следующий раз — из кэша empty.
|
|
||||||
Риск: Временный fail upstream — навсегда потерянные лейблы до ручного clear cache.
|
|
||||||
|
|
||||||
Логи только в файл:
|
|
||||||
appendTraceLine — ok, но если файл locked — silent fail.
|
|
||||||
Риск: В многопоточном режиме — race на append.
|
|
||||||
|
|
||||||
envInt без дефолта fallback:
|
|
||||||
Если ENV empty — strconv.Atoi("") → error, но ты используешь default. Ok, но можно сделать safe.
|
|
||||||
|
|
||||||
|
|
||||||
3. Рекомендации и фиксы
|
|
||||||
Вот конкретные советы + кодовые патчи. Внедряй по приоритету (сначала поломки).
|
|
||||||
|
|
||||||
Фикс портов и TCP в resolveHostGo (для поломки 1):
|
|
||||||
Используй net.Dialer с UDP/TCP fallback.
|
|
||||||
Patch:Gofunc resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, logf func(string, ...any)) ([]string, int) {
|
|
||||||
// ... (твой код)
|
|
||||||
var ips []string
|
|
||||||
errs := 0
|
|
||||||
|
|
||||||
for _, upstream := range dnsList {
|
|
||||||
server, port := splitDNS(upstream)
|
|
||||||
if server == "" {
|
|
||||||
errs++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if port == "" {
|
|
||||||
port = "53"
|
|
||||||
}
|
|
||||||
addr := net.JoinHostPort(server, port)
|
|
||||||
|
|
||||||
// Сначала UDP
|
|
||||||
ipsUdp, errUdp := lookupA(host, addr, "udp", 3*time.Second)
|
|
||||||
if errUdp == nil {
|
|
||||||
ips = append(ips, ipsUdp...)
|
|
||||||
continue
|
|
||||||
} else if logf != nil {
|
|
||||||
logf("UDP fail for %s@%s: %v", host, addr, errUdp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback TCP
|
|
||||||
ipsTcp, errTcp := lookupA(host, addr, "tcp", 5*time.Second)
|
|
||||||
if errTcp == nil {
|
|
||||||
ips = append(ips, ipsTcp...)
|
|
||||||
} else {
|
|
||||||
errs++
|
|
||||||
if logf != nil {
|
|
||||||
logf("TCP fail for %s@%s: %v", host, addr, errTcp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dedup
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
unique := []string{}
|
|
||||||
for _, ip := range ips {
|
|
||||||
if _, ok := seen[ip]; !ok && !isPrivateIPv4(ip) {
|
|
||||||
seen[ip] = struct{}{}
|
|
||||||
unique = append(unique, ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return unique, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupA(host, addr, network string, timeout time.Duration) ([]string, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
r := &net.Resolver{
|
|
||||||
PreferGo: true,
|
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
d := net.Dialer{}
|
|
||||||
return d.DialContext(ctx, network, addr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ips, err := r.LookupHost(ctx, host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []string
|
|
||||||
for _, ip := range ips {
|
|
||||||
if netip.MustParseAddr(ip).Is4() {
|
|
||||||
out = append(out, ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
Увеличь timeout + retry в resolveHostGo (для поломки 2):
|
|
||||||
Добавь retry для всей функции (3 попытки).
|
|
||||||
Patch: Оберни loop по upstream в backoff.Retry (как в nft).
|
|
||||||
|
|
||||||
Retry для PTR (для поломки 3):
|
|
||||||
Patch в resolveStaticLabels:Go// Внутри if !names:
|
|
||||||
for attempt := 0; attempt < 3; attempt++ {
|
|
||||||
names, err = digPTR(base_ip, dns_for_ptr, 5*time.Second, logf)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
ptrErrors++
|
|
||||||
}
|
|
||||||
|
|
||||||
Валидация upstream в loadDNSConfig (для поломки 4):
|
|
||||||
Patch:Gofunc loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
|
|
||||||
// ... (твой код)
|
|
||||||
valid := func(s string) bool {
|
|
||||||
if s == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := netip.ParseAddrPort(s)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
cfg.Default = filter(cfg.Default, valid)
|
|
||||||
cfg.Meta = filter(cfg.Meta, valid)
|
|
||||||
if len(cfg.Default) == 0 {
|
|
||||||
cfg.Default = defaultDNS
|
|
||||||
if logf != nil {
|
|
||||||
logf("dns-config: no valid default upstreams, fallback to built-in")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// аналогично для meta
|
|
||||||
}
|
|
||||||
|
|
||||||
func filter(ss []string, fn func(string) bool) []string {
|
|
||||||
var out []string
|
|
||||||
for _, s := range ss {
|
|
||||||
if fn(s) {
|
|
||||||
out = append(out, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
Сортировка лейблов в IPMap (для поломки 5):
|
|
||||||
Patch в runResolverJob после for host, ips...:Gofor ip, labelsMap := range ipMap {
|
|
||||||
var labels []string
|
|
||||||
for lbl := range labelsMap {
|
|
||||||
labels = append(labels, lbl)
|
|
||||||
}
|
|
||||||
sort.Strings(labels)
|
|
||||||
for _, lbl := range labels {
|
|
||||||
res.IPMap = append(res.IPMap, [2]string{ip, lbl})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Валидация CIDR в isPrivateIPv4 (для поломки 6):
|
|
||||||
Patch: Используй netip.ParsePrefix:Gofunc isPrivateIPv4(ip string) bool {
|
|
||||||
if strings.Contains(ip, "/") {
|
|
||||||
p, err := netip.ParsePrefix(ip)
|
|
||||||
if err != nil {
|
|
||||||
return true // invalid → private (пропустить)
|
|
||||||
}
|
|
||||||
return p.IsPrivate()
|
|
||||||
}
|
|
||||||
a, err := netip.ParseAddr(ip)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return a.IsPrivate()
|
|
||||||
}
|
|
||||||
|
|
||||||
Rate-limit в concurrent (для слабого 1):
|
|
||||||
Добавь semaphore в workers-loop:Gosem := make(chan struct{}, 20) // лимит 20 одновременных запросов
|
|
||||||
for i := 0; i < workers; i++ {
|
|
||||||
go func() {
|
|
||||||
for j := range jobs {
|
|
||||||
sem <- struct{}{}
|
|
||||||
// resolve
|
|
||||||
<-sem
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
IPv6 поддержка (для слабого 2):
|
|
||||||
Добавь AAAA в resolveHostGo (параллельно с A).
|
|
||||||
Добавь isPrivateIPv6.
|
|
||||||
|
|
||||||
Кэш invalidation (для слабого 3):
|
|
||||||
Добавь флаг --force для игнора TTL.
|
|
||||||
|
|
||||||
Логи в файл с sync (для слабого 5):
|
|
||||||
В appendTraceLine добавь file.Sync() после Write.
|
|
||||||
|
|
||||||
|
|
||||||
Итог
|
|
||||||
|
|
||||||
Поломки: В основном edge-кейсы (медленный DNS, invalid upstream, большие ответы) — фиксы выше.
|
|
||||||
Слабые: Масштаб (rate-limit, IPv6) и robustness (retry PTR, validate).
|
|
||||||
Код в целом очень хороший — внедряй фиксы по приоритету (сначала порты/TCP, timeout/retry).
|
|
||||||
@@ -928,19 +928,54 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise RuntimeError(f"systemd-run failed: {p.returncode}\n{out}".strip())
|
raise RuntimeError(f"systemd-run failed: {p.returncode}\n{out}".strip())
|
||||||
|
|
||||||
p2 = subprocess.run(
|
# EN: Some apps (e.g. Chrome wrappers) can return quickly; the transient scope
|
||||||
["systemctl", "--user", "show", "-p", "ControlGroup", "--value", unit],
|
# EN: may appear/disappear fast. Retry briefly to avoid race.
|
||||||
capture_output=True,
|
# RU: Некоторые приложения (например, chrome-wrapper) быстро завершаются; scope
|
||||||
text=True,
|
# RU: может появиться/исчезнуть очень быстро. Делаем небольшой retry.
|
||||||
check=False,
|
cg = self._control_group_for_unit_retry(unit, timeout_sec=2.0)
|
||||||
)
|
|
||||||
out2 = ((p2.stdout or "") + (p2.stderr or "")).strip()
|
|
||||||
cg = (p2.stdout or "").strip()
|
|
||||||
if p2.returncode != 0 or not cg:
|
|
||||||
raise RuntimeError(f"failed to query ControlGroup\n{out2}".strip())
|
|
||||||
|
|
||||||
return cg, out
|
return cg, out
|
||||||
|
|
||||||
|
def _control_group_for_unit_retry(self, unit: str, *, timeout_sec: float = 2.0) -> str:
|
||||||
|
u = (unit or "").strip()
|
||||||
|
if not u:
|
||||||
|
raise ValueError("empty unit")
|
||||||
|
|
||||||
|
deadline = time.time() + max(0.2, float(timeout_sec or 0))
|
||||||
|
last_out = ""
|
||||||
|
while time.time() < deadline:
|
||||||
|
code, out = self._systemctl_user(["show", "-p", "ControlGroup", "--value", u])
|
||||||
|
last_out = out or ""
|
||||||
|
if code == 0:
|
||||||
|
cg = (out or "").strip()
|
||||||
|
if cg:
|
||||||
|
return cg
|
||||||
|
low = (out or "").lower()
|
||||||
|
if "could not be found" in low or "not found" in low or "loaded units listed" in low:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Provide a more actionable error for users.
|
||||||
|
code_s, out_s = self._systemctl_user(["status", u, "--no-pager", "--plain"])
|
||||||
|
status_txt = (out_s or "").strip()
|
||||||
|
hint = (
|
||||||
|
"EN: Scope unit may have exited immediately. If the app is already running, "
|
||||||
|
"this launch may not create a new process tree inside the scope.\n"
|
||||||
|
"EN: Try closing the app полностью and запуск again from here.\n"
|
||||||
|
"RU: Scope мог завершиться сразу. Если приложение уже запущено, повторный запуск "
|
||||||
|
"может не создать новый процесс внутри scope.\n"
|
||||||
|
"RU: Попробуй полностью закрыть приложение и запустить снова отсюда."
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
(
|
||||||
|
"failed to query ControlGroup\n"
|
||||||
|
+ (last_out.strip() or "(no output)")
|
||||||
|
+ ("\n\nstatus:\n" + status_txt if status_txt else "")
|
||||||
|
+ "\n\n"
|
||||||
|
+ hint
|
||||||
|
).strip()
|
||||||
|
)
|
||||||
|
|
||||||
def on_app_run(self) -> None:
|
def on_app_run(self) -> None:
|
||||||
def work() -> None:
|
def work() -> None:
|
||||||
cmdline = (self.ed_app_cmd.text() or "").strip()
|
cmdline = (self.ed_app_cmd.text() or "").strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user