Some checks failed
Windows project source guards / test (push) Failing after 1m12s
Пользователь 0.8.36: VK и Яндекс перестали открываться, пока не добавил
прямые правила. В диагностике через туннель шли 29–39 % соединений к
российским IP (geoip-ru), но по ней нельзя было понять, каким правилом:
сайдкар видит только IP, а конфиг — уже исправленный. Шаблон и данные
маршрутизации в 0.8.30…0.8.36 не менялись (srs побайтно равны); причина —
правило пользователя, вставшее выше стокового «РФ напрямую».
- «Простое правило»: программа «Через VPN» по умолчанию «Кроме российских
сайтов» — native logical and с {rule_set geosite-category-ru/geoip-ru (если
есть в документе), domain_suffix ru/xn--p1ai/su, invert}; российское
назначение идёт дальше по списку. Без галочки — предупреждение, что VK,
Яндекс, банки уйдут в туннель. Семантика проверена ядром той же версии:
`domain_suffix && !(domain_suffix=[ru xn--p1ai su]) => route(...)`.
- diagnostics/route_decisions: сводка решений маршрута по последним четырём
сессиям (процесс, сайт из двух меток или IP, правило ядра «… => …»/final,
маршрут, число соединений; отдельно российские сайты в туннеле) из того же
опроса Clash API /connections, что метрики; журнал живёт у контроллера и
переживает переподключение.
- runtime_redacted: route_history — правила route последних восьми запусков
sing-box.
- Тесты: форма правила, explain_route (российские сайты мимо правила к
стоковому direct), страница (галочка, предупреждение, «Напрямую»), сводка и
история; docs/sing-box/routing-gui.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
13 KiB
Markdown
133 lines
13 KiB
Markdown
# Маршрутизация: структурный редактор sing-box
|
||
|
||
Раздел «Маршрутизация» в боковом меню редактирует **активный native JSON**
|
||
sing-box (`data/configs/sing-box/*.json`). Своей модели маршрутизации у
|
||
приложения нет: формы меняют разобранный JSON на месте и записывают его обратно,
|
||
страница «JSON» показывает тот же документ.
|
||
|
||
## Страницы
|
||
|
||
| Подпункт | Что редактирует |
|
||
|---|---|
|
||
| Обзор | выбор конфига и шаблона, сравнение со стоком, сброс всего конфига или одного раздела, статус |
|
||
| Правила | `route.rules` (сверху вниз, первое совпадение) и `route.final` |
|
||
| Наборы правил | `route.rule_set`: local / remote / inline |
|
||
| DNS | параметры `dns`, `dns.servers`, `dns.rules` |
|
||
| Исходящие | `outbounds` и `endpoints`; `proxy` — слот выбранного сервера, только просмотр |
|
||
| Система | `inbounds` (TUN), остальные `route.*`, `log`, `experimental`, прочие секции |
|
||
| JSON | весь документ текстом |
|
||
|
||
Правки живые: «Готово» закрывает подстраницу, «Отменить правки» возвращает объект
|
||
к состоянию на момент открытия. На диск пишут только «Сохранить»/«Применить»
|
||
в верхней панели; «Проверить» запускает `sing-box check` бандл-ядром по
|
||
редактируемому JSON в фоне (служебные подстановки запуска в эту проверку не входят).
|
||
|
||
## Простое правило и «Проверить сайт»
|
||
|
||
- «Добавить правило» → «Сайты, IP, программы (просто)» (`ui/singbox/simple_rule_page.py`,
|
||
разбор — `singbox_config/simple_rule.py`): куда (через VPN / напрямую / блок) и
|
||
списки по одному в строке в синтаксисе v2rayN. `2ip.ru` — `domain_suffix` (сайт и
|
||
поддомены), `full:`/`keyword:`/`regexp:`, `geosite:`/`geoip:` — существующие наборы,
|
||
`geoip:private` — `ip_is_private`, программа с путём — `process_path`. Сайты/IP и
|
||
программы собираются в `logical` `or`: в ядре разные виды условий идут через И.
|
||
Документ не меняется до «Добавить»; правило встаёт сразу после `sniff`/`hijack-dns`.
|
||
«Добавить и применить» идёт тем же путём, что «Применить».
|
||
- Программа «Через VPN» по умолчанию идёт «Кроме российских сайтов»: правило встаёт
|
||
выше стокового «РФ напрямую», и без исключения VK, Яндекс, банки этой программы
|
||
уходили в туннель, где с зарубежного адреса часто не открываются (16.10.2026).
|
||
Исключение — обычное native-правило ядра: `logical` `and` из условий пользователя
|
||
и `{"rule_set": [geosite-category-ru, geoip-ru], "domain_suffix": [ru, xn--p1ai, su],
|
||
"invert": true}`; наборы берутся, только если есть в документе. Российское
|
||
назначение не совпадает с правилом и идёт дальше по списку. Без галочки страница
|
||
предупреждает, что российские сайты программы тоже пойдут через VPN.
|
||
- Диагностика маршрута: в `runtime_redacted.json` — `route_decisions` (по последним
|
||
четырём сессиям: процесс, сайт из двух меток имени или IP, правило ядра
|
||
«… => route(…)» или `final`, маршрут и число соединений; отдельно — российские сайты,
|
||
ушедшие в туннель) из того же опроса Clash API `/connections`, что метрики, и
|
||
`components["sing-box"].route_history` — правила `route` последних восьми запусков.
|
||
- «Проверить сайт» над списком (`singbox_config/route_explain.py`,
|
||
`application/route_check.py`): порядок ядра — сверху вниз до первого совпадения,
|
||
`sniff`/`resolve`/`route-options` не останавливают, иначе `route.final`. Запрос —
|
||
браузер: TCP/443/TLS, программа неизвестна; условия вроде процесса дают «может
|
||
перехватить», перебор идёт дальше. IP-условия — только в TUN или после `resolve`.
|
||
Наборы проверяет само ядро: `sing-box rule-set match -f <format>` (без `-f` ядро
|
||
не читает `.srs`), наборы по URL — «не узнать». Проверяется редактируемый документ,
|
||
включая несохранённые правки.
|
||
|
||
## Откуда берутся поля
|
||
|
||
`assets/sing-box-schema/schema.json` — JSON Schema, которую генерирует само
|
||
закреплённое ядро (`sing-box schema`) из своих Go-структур:
|
||
`python3 scripts/generate_singbox_schema.py` после смены пина sing-box в
|
||
`scripts/core-lock.windows-x64.json`. Форк не умеет описать несколько своих
|
||
типов (amnezia-диапазоны endpoints, byte-размеры, providers); генератор
|
||
ослабляет только их и пишет пути в `loose-paths.txt`. Тест гарантирует, что их
|
||
нет в `route`, `dns`, `inbounds`, `outbounds`, `log` и `experimental`; такие
|
||
поля endpoints редактируются как JSON.
|
||
|
||
`sing-box check` по исходному JSON не видит служебных подстановок запуска.
|
||
Поэтому outbound `direct` и DNS-сервер `bootstrap-dns`, на которые запуск
|
||
ссылается сам, помечены как нужные приложению, а их удаление требует
|
||
подтверждения.
|
||
|
||
- `xray_fluent/singbox_config/schema.py` — чтение схемы: поля объекта с учётом
|
||
дискриминатора (`type`, `action`), `Listable`-поля, ссылки на теги
|
||
(`x-tag-reference`).
|
||
- `xray_fluent/singbox_config/catalog.py` — только подписи, закреплённые поля и
|
||
сводки строк. Поле без подписи всё равно доступно под своим native-ключом.
|
||
- `xray_fluent/ui/singbox/form.py` — форма показывает заданные, обязательные и
|
||
закреплённые поля; всё остальное, что знает ядро, — в «Добавить параметр».
|
||
Смена `type`/`action` убирает поля, которые новый вариант не принимает.
|
||
|
||
## Инварианты
|
||
|
||
- Открытие страниц и объектов не меняет JSON (пустые секции создаются только
|
||
при первой настоящей правке).
|
||
- Неизвестные ядру ключи сохраняются и показываются как «неизвестное поле».
|
||
- Одиночное значение `Listable`-поля остаётся скаляром, пока в нём один элемент.
|
||
- При ошибке JSON разделы только читают и предлагают исправить текст на странице «JSON».
|
||
- Обновление приложения сливает стоковый шаблон посекционно
|
||
(`template_sync.merge_stock_sections`): нетронутые разделы следуют за стоком,
|
||
изменённые остаются вашими. Принудительной синхронизации DNS больше нет.
|
||
- Виджеты редактора удаляются только через `qt_lifecycle.dispose_later`:
|
||
скрытие сразу, разрушение на следующем такте при выключенном циклическом GC
|
||
(у `ScrollArea`/`PlainTextEdit`/`RoundMenu` qfluentwidgets есть
|
||
`SmoothScrollDelegate`, гонка GC с деструкторами роняет процесс, на Windows —
|
||
особенно). Меню «Добавить…» строятся по клику и удаляются после закрытия.
|
||
- qfluentwidgets 1.11 не освобождает Python-обёртки удалённых стилизованных
|
||
виджетов (лямбды на `destroyed` и `qconfig.themeChanged`). Глобально это не
|
||
патчится: освобождённые обёртки попадают под ту же гонку GC во всём
|
||
приложении. Поэтому редактор бережёт виджеты: строки списков обновляются на
|
||
месте, невидимые разделы пересобираются только при показе, повторная загрузка
|
||
того же текста ничего не пересобирает, схема ядра грузится при первой сборке
|
||
раздела.
|
||
- Смена `type`/`action` пересобирает форму на следующем такте: выбор приходит из
|
||
всплывающего меню самого комбобокса.
|
||
- Незафиксированный ввод (списки и JSON применяются с задержкой) сбрасывается в
|
||
документ по потере фокуса, «Готово», переключению раздела и перед
|
||
«Сохранить/Применить/Проверить».
|
||
|
||
## Графика и анимация
|
||
|
||
- Строки списков несут значок-плитку (`ui/singbox/visuals.py`): тон по смыслу —
|
||
прокси = акцент, напрямую = зелёный (`success`), блок = `error`, DNS = `info`,
|
||
служебные действия = `warning`. Цвета берутся из `ui/theme.py` в момент
|
||
отрисовки, так что следуют теме и акценту; hex-литералов вне `theme.py` нет.
|
||
- На странице раздела только заголовок и данные. Пояснения для новичков и живая
|
||
иллюстрация открываются ссылкой «Как это б#&^ь работает?» в стоковом окне
|
||
`MessageBoxBase` поверх приложения (`ui/singbox/guide.py`: тексты `GUIDES`,
|
||
окно удаляется через `dispose_later`). Иллюстрация (`ui/singbox/art.py`,
|
||
`QPainter`) рисует конфиг пользователя: «Обзор» — карта, где пакеты уходят в прокси,
|
||
напрямую или в блок пропорционально числу правил (`visuals.rule_outcomes`);
|
||
«Правила» — зонд идёт сверху вниз и останавливается на первом совпадении;
|
||
«DNS» — вращающийся глобус и запрос/ответ; «Исходящие» — хаб с импульсами;
|
||
«Система» — туннель TUN; «JSON» — набор текста.
|
||
- Анимации работают на `motion.FrameGate`: кадры (25 fps, у JSON 12 fps) идут
|
||
только пока виджет виден и в Windows не выключена анимация интерфейса; иначе
|
||
рисуется неподвижный кадр. Фаза берётся из реального времени.
|
||
- Иконки Fluent рисуются из кэша растров по (иконка, цвет, размер, DPR):
|
||
`FluentIcon.render(fill=…)` разбирает SVG на каждом вызове.
|
||
- Микроанимации: вспышка строки при добавлении/дублировании/перемещении,
|
||
пульсирующая точка несохранённых правок, подстраницы въезжают справа, возврат —
|
||
слева, разделы появляются снизу (`PopUpAniStackedWidget` qfluentwidgets; текущая
|
||
страница переключается сразу, анимируется только положение).
|