Безопасность
Как сообщить об уязвимости
Заголовок раздела «Как сообщить об уязвимости»Не открывайте публичный issue. В репозитории включены приватные сообщения об уязвимостях GitHub: откройте новое сообщение на вкладке Security. Его видят только мейнтейнеры, а обсуждение и исправление идут в закрытом advisory.
Чтобы мы быстрее разобрались, укажите:
- версию или коммит Citrus и платформу;
- затронутый компонент (протокол, листенер, контроллер, обновление и так далее);
- минимальный конфиг и шаги воспроизведения;
- что может сделать атакующий и какой доступ ему нужен.
Мы подтверждаем получение, исправляем в закрытой ветке, публикуем исправление и advisory и указываем автора, если он не против. Правила те же, что в SECURITY.md.
Уязвимости в форках getmikan/utls, getmikan/tls и getmikan/http присылайте сюда же. Если проблема есть и в mihomo, отметьте это в сообщении.
Что поддерживается
Заголовок раздела «Что поддерживается»Исправления выходят в ветке dev, затем в main и в релизах. Отдельных веток поддержки для старых версий нет: обновляйтесь до последней сборки.
Что уже сделано
Заголовок раздела «Что уже сделано»Исправления по ревью ядра, форков и статическим аудитам перечислены в журнале изменений, вместе с PR и проверками. Каждый новый отчёт сверяется с текущим кодом: наличие старого исправления в журнале само по себе не подтверждает закрытие нового пункта. Ограничения совместимости описаны ниже.
Контроллер
Заголовок раздела «Контроллер»- Без
secretконтроллер не слушает сетевой адрес; без secret на loopback он отклоняет запросы браузера с чужих сайтов и DNS rebinding. - Unix-сокет и именованный канал доступны только владельцу;
secretпроверяется на всех листенерах, включая/debug. - Смена
secretдействует сразу и закрывает открытые потоки; соединения ограничены по времени заголовков и простоя. - API не отдаёт пароли и ключи входящих, а
external-uiне может указывать на каталог с конфигом и ключами.
Авторизация на входящих
Заголовок раздела «Авторизация на входящих»- HTTP-прокси авторизует каждый запрос, а не только первый на соединении.
- UDP через SOCKS5 работает только при открытой ассоциации и привязан к пользователю и порту.
- Смена пароля или удаление пользователя закрывает его сессии; пустые имена запрещены.
- UDP-сессии разделены по входящему и пользователю, так что правила и лимиты одного пользователя не достаются другому.
- Адрес клиента из заголовков прокси принимается только от перечисленных
trusted-proxies. - Пользователи серверных входящих не могут через
DIRECTпопасть на loopback, в частные сети, к metadata облака и на этот хост, какие бы ни были правила: защита исходящего трафика.
Ресурсы до авторизации
Заголовок раздела «Ресурсы до авторизации»- Общая TCP-обёртка REALITY, ShadowTLS, JLS и ResTLS допускает до 256 незавершённых рукопожатий на listener и закрывает их через 15 секунд. После решения «клиент или fallback» этот срок снимается; ограничения fallback настраиваются отдельно от рукопожатия.
- Состояние, которое клиент может создать до проверки учётных данных (сессии XHTTP, тикеты 0-RTT, потоки QUIC, мультиплексированные потоки, сборка фрагментов), ограничено по числу, объёму и времени.
- Длины, которые объявляет собеседник, проверяются до выделения памяти.
Загрузки и файлы
Заголовок раздела «Загрузки и файлы»- Ответы провайдеров, geodata, архивы UI и пакеты обновлений ограничены по размеру, архивы — ещё и по числу записей.
- Пути проверяются с учётом символических ссылок, файлы провайдеров записываются атомарно.
- Публичный URL не может перенаправить загрузку во внутреннюю сеть.
- Новые файлы конфигурации, провайдеров и кэша создаются с правами только для владельца.
TLS и обновления
Заголовок раздела «TLS и обновления»- Минимум TLS 1.2 на всех TLS-входящих и на контроллере.
- В форки getmikan/tls и getmikan/utls перенесены исправления безопасности crypto/tls из Go 1.26 (проверка ссылок на внешние расширения ECH, CVE-2026-97031, и ограничение KeyUpdate).
- Самообновление требует подписанный Ed25519 manifest со сроком действия и привязкой к каналу, sequence сборки, платформе, имени, размеру и SHA-256. Два встроенных release-ключа позволяют overlap-ротацию; отозванные ID отклоняются, sequence по каналу блокирует rollback.
- Удалённым GEO-базам можно задать SHA-256 для каждого типа (
geox-sha256): база с другим хешем отклоняется. Без пина база принимается как есть: издатели баз их не подписывают. - Набор корневых сертификатов Mozilla хранится в репозитории, а не копируется с раннера CI, и государственные CA для перехвата TLS по умолчанию не считаются доверенными; см. Корневые сертификаты.
Сборка и CI
Заголовок раздела «Сборка и CI»- Все GitHub Actions закреплены по SHA коммита, базовые образы Docker и образы тестов — по digest; обновления приходят через Dependabot.
- Токены CI получают минимальные права: сборке — только чтение, публикации релиза и образа — только нужные scope.
- На каждый PR работают CodeQL, govulncheck, dependency review,
go vetи проверкаgo mod tidy. - Бинарники Xray и mihomo для интеграционных тестов сверяются с закреплённым SHA-256.
Корневые сертификаты
Заголовок раздела «Корневые сертификаты»Речь о TLS-клиентах самого ядра: транспорты TLS, WebSocket и gRPC у VLESS и Trojan, HTTP и SOCKS поверх TLS, Hysteria2, TUIC, DoH, DoT и DoQ, загрузка провайдеров и geodata.
- Встроенный набор. В бинарник встроено хранилище корневых сертификатов Mozilla в выгрузке curl (
component/ca/ca-certificates.crt, источник — curl.se/ca/cacert.pem); дата данных Mozilla записана в заголовке файла.make ca-bundleобновляет его и сверяет SHA-256, который публикует curl. - Источники.
tls.ca-rootsзадаёт, откуда берутся доверенные корни:both(по умолчанию) — системное хранилище плюс встроенный набор,bundle— только встроенный набор, установленные на устройство CA не учитываются,system— только системное хранилище. Переменные окруженияDISABLE_SYSTEM_CAиDISABLE_EMBED_CAпо-прежнему сужаютboth. - Чёрный список. Некоторые государственные CA существуют для перехвата TLS: если такой CA установлен на устройство, его владелец может читать проксируемый трафик. Ядро никогда не доверяет им, откуда бы они ни пришли, из системы или из набора: Russian Trusted Root CA и Russian Trusted Sub CA (ключи 2022 и 2024 годов), казахстанские Qaznet Trust Network и Information Security Certification Authority (2019–2021). Записи сверяются по SHA-256 открытого ключа, так что перевыпущенный сертификат с тем же ключом тоже отклоняется. Цепочка через такой CA завершается ошибкой
certificate chain goes through the distrusted CA. Список и источники — вcomponent/ca/denylist.go. - Отключение.
tls.ca-denylist: falseснова разрешает эти CA. Чтобы доверять только одному из них, добавьте его вtls.custom-certifactes: цепочки, которые заканчиваются на пользовательском сертификате, чёрный список не проверяет. - Пиннинг.
fingerprintиskip-cert-verify— явный выбор для отдельного прокси, на них чёрный список не действует. - Исключение. Управляющий клиент Tailscale сам добавляет эти корни к системному пулу, поэтому
bundleего не сужает и чёрный список там не действует.
tls: ca-roots: bundle # both | bundle | system ca-denylist: true # false снова разрешает государственные CA из спискаWebSocket контроллера
Заголовок раздела «WebSocket контроллера»Браузерная панель сначала отправляет POST /auth/ws-ticket с Authorization: Bearer <secret>, Content-Type: application/json и телом {"path":"/events?types=auth"}. Успешный ответ — HTTP 201 и {"ticket":"...","expires_in":30} с Cache-Control: no-store. Затем панель подключается к /events?types=auth&ticket=... по WebSocket. Для каждого переподключения нужен новый билет.
Билет действует 30 секунд, погашается атомарно при одной попытке авторизации и разрешает только GET-upgrade на указанный поток с теми же query-параметрами. Если при выдаче был заголовок Origin, он должен совпасть при подключении. Поддерживаются /events, /logs, /traffic, /traffic/users, /memory и /connections. Билетом нельзя вызвать REST API или выпустить другой билет. На один обработчик контроллера допускается до 1024 непогашенных билетов; сверх лимита выдача отвечает HTTP 429, истёкшие билеты освобождают ёмкость. Пересоздание контроллера, включая смену секрета, аннулирует ранее выданные билеты.
Нативные клиенты могут продолжать передавать Bearer-заголовок при WebSocket-upgrade. Для старых браузерных панелей есть выключенная по умолчанию опция external-controller-allow-legacy-websocket-token: true. Она разрешает прежний ?token=<secret> на потоках WebSocket и выводит предупреждение: URL снова содержит основной многоразовый секрет, поэтому это режим совместимости, а не устранение риска. В обычном режиме в URL находится только короткоживущий одноразовый билет.
Ограничения совместимости
Заголовок раздела «Ограничения совместимости»- Sudoku: текущий handshake принимает знание настроенного
keyкак секрет доступа и не доказывает владение отдельным приватным ключом. Храните это значение в секрете, даже если инструмент называет его публичным ключом. Добавление такого доказательства требует изменения протокола; текущая совместимость сохранена, этот пункт не считается исправленным. - KCPTun: предупреждение о стандартном ключе и лимиты ресурсов не заменяют уникальный секрет. Старый общий ключ остаётся доступен для совместимости; без явного
keyэтот транспорт нельзя считать защищённым отдельным секретом сервера.
Подпись релизов и проверка подлинности загружаемых дашбордов и geodata — в дорожной карте.
