XKeen SmartRoute v1.2.0
Итог планового аудита технического долга (2026-08-22): 4 критичных, 8
высоких, ~34 средних и 14 низких находки закрыты, каждая проверена вживую на
реальном роутере. Подробный отчёт по каждой находке — в истории коммитов
develop.
Безопасность
- Жёсткий kill-switch не работал вообще на стоковом OpenWrt — правило
firewall ссылалось на ipset, который никто не создавал (секцияconfig ipsetотсутствовала), так что при падении Xray трафик профиля с
включённым kill-switch не блокировался. Добавлена сама firewall-секция +
современнаяdhcp-секция вместо устаревшего inline-синтаксиса;
install.shтеперь сам ставитdnsmasq-full; проверка возможностей
dnsmasq перед включением с понятной ошибкой, если их нет. Заодно —
устаревшие IP отключённого профиля больше не утекают в правило другого
профиля при повторном включении (firewall reloadне подчищает
осиротевший nftables-набор сам). - Панелью мог управлять любой сайт при пустом пароле — wildcard CORS,
открытыйOPTIONS *, WebSocket без проверки Origin.corsWrapи
upgrader.CheckOriginтеперь сверяют Origin с реальнымHostзапроса;
найден и закрыт попутный баг того же класса — два хендлера ставили
собственныйAccess-Control-Allow-Origin: *поверх уже правильного
значения. - Хранимая XSS через адрес сервера подписки в обоих UI (LuCI и панель) —
враждебная подписка могла выполнить произвольный JS в авторизованной
сессии администратора. Свыше 12 мест рендеринга переведены на безопасный
вывод текста вместоinnerHTML. - Путь traversal через
/в имени профиля —save_profile/
delete_profile/killswitch_setмогли писать/удалять файл вне каталога
профилей. Имя с/теперь отклоняется. - URL подписки (с секретным токеном) мог попасть в лог при ошибке
импорта, файл подписок хранился с правами 0644. Токен обрезается перед
логированием, файл — 0600.
Исправлено
- Истечение сессии панели показывало пустой экран вместо редиректа на
логин —401теперь централизованно ловится и вызывает экран входа. uninstall.shмог зависать (xkeen -restartбез таймаута) и оставлял
живыми часть состояния (редирект трафика, gateway, kill-switch) после
удаления.- 12 обработчиков
rpcd-скрипта возвращали «успех» независимо от
реального кода выхода — в частности, включение kill-switch могло
показывать «включено» без единой реальной защиты. Оба UI теперь
откатывают контрол и показывают настоящую ошибку. - Потеря выбора серверов в свёрнутых группах при сохранении профиля и
Double VPN-пула (в LuCI это приводило к потере вообще всех выбранных
серверов при любой перерисовке). - Отключение клиента (закрытие вкладки/потеря сети) во время
refresh_subscription/import_subscriptionмогло оборвать запись
файлов состояния на середине — RPC-вызовы панели больше не привязаны к
времени жизни HTTP-соединения. - Импорт подписки был O(n²) (пересериализация всего списка серверов на
каждый новый сервер) — теперь линейный. - Xray перезапускался даже когда обновление всех подписок целиком
провалилось; IPv6-литералы в квадратных скобках калечились при парсинге
хоста. health.jsonрос монотонно и никогда не подчищался; добавлены
таймауты/keep-alive на HTTP-сервере панели (защита от slowloris и
зависших WebSocket-пиров);tailFileне переоткрывал файл логов при
ротации.- ~4300 лишних строк лога в сутки на установке без единого
balancer-профиля — теперь логируется только переход состояния. redirect.shудалял файл состояния при неудачной проверке firewall,
хотя лог утверждал обратное — теперь делает бэкап и восстанавливает при
отказе.- Gateway-профиль (Clash-совместимый
PUT /proxies/{name}) терял поля
devices/ip_ranges/removed_serversпри сохранении. - Битый файл одного профиля обрушивал список профилей целиком; расхождение
словарей RU/EN на 19+ ключей; несколько более мелких находок (нет
confirm()перед удалением профиля, параллельные poll-циклы панели,
расхождение окна опроса LuCI/панель, ACL с 5 неиспользуемыми
деструктивными методами логов) — полный список см. в истории коммитов.
Исправлено (найдено во время полного цикла переустановки на тестовой ветке)
install.sh: переустановка не была по-настоящему идемпотентной.
Обнаружено живым прогономuninstall.sh(без--purge) →install.sh
на тестовой ветке — три независимых бага:- Cron мог остаться полностью пустым после переустановки: под
set -eu
crontab -l | grep -v ...завершается с ненулевым кодом, если у
root'а ещё нет crontab (ровно так послеuninstall.shи на первой
установке) — обрывало подстановку до того, как записались реальные
задачи,crontab -получал пустой ввод. Без cron переставали работать
автообновление подписок/списков, ночной рестарт Xray и регенерация
routing/outbounds. 00_api.smartroute.json(gRPC API Xray, без которого панель не видит
трафик/Observatory) и очистка заглушки04_outbounds.jsonбыли
вложены в блок «xkeen ещё не установлен» — при переустановке этот
блок пропускался целиком, и файл не пересоздавался.- Сохранённое состояние (
/etc/xkeen-smartroute/state) переживает
переустановку без--purge, но реальное применение — сгенерированный
04_outbounds.smartroute.json, nftables-цепочка редиректа,
dnsmasq/firewall-секции kill-switch — нет:install.shтеперь
восстанавливает их из сохранённого состояния в конце шага установки
LuCI-модуля.
- Cron мог остаться полностью пустым после переустановки: под
Исправлено (найдено на живом роутере после того, как частые рестарты Xray стали нормой)
Как только рестарты Xray стали происходить регулярно (ночной cron заработал
впервые после фикса выше, плюс каждое сохранение профиля), на реальном
роутере проявились три независимых, ранее незаметных бага самого
Xray-core 26.2.6 — не связанных с сегодняшними правками install.sh, сам
пакет xray-core на этом роутере не менялся уже неделю:
xray run -confdirможет молча потерять один файл конфига при слиянии
— без единой ошибки в логе. Иногда это был файл с правилами
маршрутизации профилей — трафик в этом случае просто уходил через
«первый попавшийся» outbound вместо назначенного профилем сервера.
Подтверждено вживую: до 5 попыток подряд из 10 терялся файл.sr_restart_xray
теперь проверяет, что каждый*.jsonиз confdir реально попал в лог
запуска, и перезапускает Xray заново (до 10 раз), пока это не так.- Собственное правило маршрутизации gRPC API панели никогда не переживало
слияние конфига — не гонка, а стабильно воспроизводимый архитектурный
баг: как только confdir читает ещё один файл с ключомrouting, этот
файл побеждает целиком, без слияния массивов правил. Из-за этого
панель не могла получить от Xray вообще ничего (трафик, Observatory,
«сейчас в сети») — что и стояло за «протухшим на 46 часов» статусом
здоровья серверов. Правило теперь эмититgenroute.sh, в тот же файл,
что уже надёжно побеждает при слиянии (тот же приём, что раньше
применили к резервному правилуredirect/tproxyи к политике сбора
статистики — см. их собственные комментарии в коде). - Запросы трафика по outbound'ам падали с
QueryStats only works its own stats.Manager— известный пробел апстрима (XTLS/Xray-core#2296,
независимо подтверждён в XTLS/Xray-core#4509): нужен отдельный
верхнеуровневый блок"stats": {}, не только флаги в"policy". Добавлен.
Не обошлось без дублей и на нашей стороне — ретраи из первого пункта сами
поначалу дважды спотыкались о set -e в busybox ash (переменная
var="$(cmd)" наследует код возврата команды, даже внутри тела if), из-за
чего save_profile/delete_profile иногда падали без единого сообщения в
логе. Исправлено; 10/10 чистых прогонов после фикса, включая прогон с
реальным рестартом Xray.
Исправлено (найдено при живой проверке уже готовых правок)
- Обновление подписки на 10+ серверах перемешивало их порядок — фикс
O(n²)-импорта (см. выше) писал по одному файлу на сервер с именем вида
sv_$i.jsonбез ведущих нулей; слияние глобом сортирует имена по
алфавиту, а не по числу, так чтоsv_10.jsonвставал раньшеsv_2.json.
Порядок был важен: серверы с автовыбором должны идти первыми в списке.
Имена файлов теперь с ведущими нулями. - Удаление профиля не показывало никакого сообщения об успехе (в отличие
от сохранения, которое явно пишет «Сохранено, xray перезапущен») — добавлено
такое же сообщение об удалении в обоих интерфейсах.
Изменено
- Картинки статики панели сжаты (favicon 924 КБ → часть ~230 КБ суммарно
после сжатия) без потери качества на реальном размере отображения. - 16 копий 4 повторяющихся JS-функций вынесены в общий модуль для каждого
UI. go.mod/CI версия Go больше не может разойтись — CI читает версию из
go.modнапрямую.- Вся документация под
docs/functionality_doc/иdocs/UI_functionality/
и оба README сверены и обновлены по итогам этой сессии правок.