Leadaxe 提出用 route.default_domain_resolver 对象形式关闭节点域名的乐观 DNS 缓存

在自有仓库 LxBox 的 issue #168(仍 open)中,Leadaxe 提出一条待办规范:节点地址会轮换的提供商(A 记录每分钟变化、旧 IP 很快失效)遇到核心的乐观 DNS 缓存时会把客户端留在失效地址上——TTL 过期后核心仍返回旧应答并在后台刷新;DNS 规则也帮不上忙,因为一旦设置了 domain_resolver,节点服务器域名就直接由该解析器解析、绕过规则(sing-box dns/router.go 的 Lookup,`if options.Transport != nil`),可用的开关是 domain_resolver 的对象形式。他要求新增模板全局变量 dns_resolver_no_stale(bool、默认 true,标题「Never serve stale answers for node hostnames」,提示为 TTL 过期后重新解析节点域名而非拨号到缓存地址),并把 route.default_domain_resolver 改为对象 {"server": "@dns_default_domain_resolver", "disable_optimistic_cache": "@dns_resolver_no_stale"}:默认构建得到 true、关闭勾选得到 false,核心两种形式都接受;不使用 disable_cache,因为那会对每个连接发一次查询。读取该字段的代码都要同时支持字符串与对象,替换解析器时只改 server;UI 在 DNS 选项卡加开关,LX Backup 中该变量可移植,另需 corpus 用例与一个集成测试。该行为只影响地址由域名给出、自己发起连接的组件(带 server 域名的 outbounds、域名地址的 DNS 服务器、endpoints、chain),不涉及客户端目标域名流量。

作者原文

Контракт 1.1.119, contract/TASKS_LXBOX.md §116 п. 10, норма contract/docs/TEMPLATE_LANG.md §6.9. Синк контракта с лаунчера: develop baf7e9a2. Остальное из §116 (for_each_node, #do/#final, unique/internal) — отдельная задача, здесь не трогать.

Зачем

Провайдеры с ротацией адресов узлов (A-запись меняется раз в минуту, старый IP вскоре глохнет) при optimistic DNS-кэше ядра оставляют клиент на мёртвом адресе: после истечения TTL ядро отдаёт старый ответ и обновляет его в фоне. DNS-правила не помогают: при заданном domain_resolver имя сервера узла резолвится напрямую этим сервером, минуя правила (sing-box dns/router.go, Lookup: if options.Transport != nil). Рычаг — объектная форма domain_resolver.

Что сделать

  1. Глобальная переменная шаблона dns_resolver_no_stale: bool, default_value: "true", wizard_ui: "fix", title «Never serve stale answers for node hostnames», tooltip «Resolve a node's hostname again once its TTL expires instead of dialing a cached address. Keeps nodes alive with providers that rotate addresses.». Объявление рядом с dns_default_domain_resolver.
  2. route.default_domain_resolver — объект:
    json {"server": "@dns_default_domain_resolver", "disable_optimistic_cache": "@dns_resolver_no_stale"}
    Сборка по умолчанию даёт {"server": "<тег>", "disable_optimistic_cache": true}, при выключенной галке — false (bool подставляется литералом, ключ не снимается). Ядро принимает обе формы. disable_cache не использовать: это запрос на каждое соединение.
  3. Чтение в обеих формах. Весь код, который читает route.default_domain_resolver (сборка, пост-шаги, показ в UI, подмена резолвера при отбраковке DNS-сервера, импорт sing-box конфига), понимает строку и объект; тот, кто подменяет резолвер, меняет только server и не теряет остальные ключи. Искать grep'ом по app/lib.
  4. UI: переключатель во вкладке DNS рядом с выбором «Default domain resolver», подписи из title/tooltip переменной, перевод в каталог локализации; изменение помечает конфиг изменённым и хранится в переменных состояния как dns_optimistic.
  5. LX Backup: переменная переносимая (contract/registry/vars.json уже объявляет её portable): экспорт, импорт, слияние по общим правилам vars.
  6. Корпус обязателен: contract/corpus/template/subst/default_resolver_object_defaults и default_resolver_object_off проходят вашим движком. Плюс один интеграционный тест сборки: с галкой и без — объект в route с true/false; после смены резолвера объект сохраняется.
  7. Релиз-ноты и дока по DNS-настройкам шаблона — по строке.

Где это работает

default_domain_resolver — дефолт для «адрес сервера задан доменом» у компонентов, которые сами открывают соединение: outbounds с server-доменом, DNS-серверы с доменным адресом без своего domain_resolver, endpoints (WireGuard по hostname, Tailscale), chain. Доменов назначения в трафике клиента он не касается: те идут через DNS-правила, dns.final и действие resolve, и обычный кэш для сайтов остаётся optimistic.

Ограничения

Спорное — текстом в DECISIONS лаунчера через сессию лаунчера. Релиз, теги, main, force-push — только по явной команде владельца. В отчёте: файлы, как разворачивается route при обоих значениях, какие тесты и кейсы прошли, что разошлось с нормой.