Skip to content

XKeen SmartRoute v1.2.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 09:28
· 50 commits to master since this release

Итог планового аудита технического долга (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-модуля.

Исправлено (найдено на живом роутере после того, как частые рестарты 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 сверены и обновлены по итогам этой сессии правок.