Files
HY2XS_flamy/apps/model/constant/config.go
T
founder b99be7d514 fix(v1): разблокировать сборку, починить жизненный цикл cron и закрыть каналы утечки
Сборка не собиралась: два контракта приёмки роняли её на корректном коде.

verify_api_namespace_contract искал возвращение legacy-пространства имён
через grep по '/hui' и находил router_test.go, который ПЕРЕЧИСЛЯЕТ этот
префикс, чтобы доказать отсутствие маршрута, и сам versions.sh, где строка
стоит в тексте проверки. Падение приходило шестым шагом из четырнадцати, до
резолва Hysteria. За ним прятался второй такой же: проверка транзакционности
импорта пиров брала файл от начала applyPeerImportEntry и до конца, захватывая
объявленные ниже ExistPeerName и UpdatePeerLastConnectionAt.

Обе проверки теперь смотрят на код, а не на упоминания: добавлены помощники
code_without_comments и code_mentions_in, а отсутствие legacy-маршрута
доказывает тест на таблице маршрутов собранного роутера.

Планировщик стал собственностью процесса. InitCron вызывался из runServer и
на каждом вызове создавал новый cron.New(), не сохраняя ссылку; cron.Stop()
не вызывался нигде. Смена RESET_TRAFFIC_CRON выполняла StopServer(), точка
входа крутила for { runServer() } — и каждая правка добавляла целый
дублирующий набор джоб, а старое расписание сброса продолжало работать.
Фиксированные джобы регистрируются один раз, расписание переносится на месте
по EntryID, HTTP-сервер не трогается. Добавлено штатное завершение по SIGTERM.

Выражение проверяется до записи в базу тем же парсером (cron.ParseStandard),
которым его разбирает планировщик: раньше невалидная строка сохранялась, API
отвечал успехом, а сброс трафика молча исчезал.

updateConfigs стал атомарным: полная проверка партии, одна транзакция,
применение к рантайму. Прежний тест ставил запрещённый ключ первым и не
смотрел в базу — поймать частичное применение он был неспособен.

Удалены четыре ключа таблицы config без единого потребителя: HYSTERIA2_ENABLE,
HYSTERIA2_CONFIG (второй источник истины, читался первым), HYSTERIA2_TRAFFIC_TIME
и HYSTERIA2_CONFIG_REMARK. Имя профиля в share URI выводится из имени пира.

Безопасность:
- bootstrap-пароль администратора больше не генерируется и не пишется в журнал,
  который отдаётся кнопкой выгрузки; отсутствие env — отказ старта;
- собственный журнал админки санитизируется наравне с чужим;
- golang-jwt/jwt v3 -> v5: GO-2025-3553 не имеет исправленной версии в v3 и
  достижима с неаутентифицированного запроса; набор алгоритмов подписи
  зафиксирован через WithValidMethods;
- удалён вход по несолёному SHA-224 из предыдущего поколения;
- убран modulo bias в util.RandomString — единственном генераторе секретов;
- пир установщика защищён во всех путях записи, а не только в импорте;
- удалена латентная паника в service.GetToken и недостижимая ветка GetAdminInfo,
  проверявшая меньше, чем middleware.

Toolchain: Go 1.21.13 -> 1.26.7, Node 20.19.0 (EOL) -> 24.20.0. На прежнем
графе govulncheck находил 21 вызываемую уязвимость, 17 из них в stdlib,
попадающей в production-бинарь. Сейчас — ноль. Добавлен обязательный шаг
проверки зависимостей (govulncheck + pnpm audit) с записью результата в
metadata пакета.
2026-08-29 21:37:50 +05:00

112 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package constant
// Ключи таблицы `config` — то, чем действительно владеет админка.
//
// Сетевых настроек самой панели (порт, web-контекст, пути к сертификатам)
// здесь больше нет. Они достались от H UI, где панель конфигурировала себя
// сама, и в архитектуре HY2XS были вторым источником истины: порт приходит из
// HY2XS_UI_PORT через systemd, bind — из HY2XS_UI_BIND_HOST, TLS
// терминируется на внешнем слое. Панель записывала в SQLite тот же порт,
// который ей передали аргументом, и тут же читала его обратно.
// Ключей HYSTERIA2_ENABLE, HYSTERIA2_CONFIG, HYSTERIA2_TRAFFIC_TIME и
// HYSTERIA2_CONFIG_REMARK здесь больше нет. Все четыре достались от предыдущего
// поколения панели и к v1 перестали чем-либо управлять:
//
// HYSTERIA2_ENABLE — жизненным циклом Hysteria владеет systemd, а
// единственным потребителем ключа была строка лога;
// HYSTERIA2_CONFIG — второй источник истины рядом с
// /etc/hysteria/config.yaml, причём читался ПЕРВЫМ;
// HYSTERIA2_TRAFFIC_TIME — настройка в панели без единого потребителя в
// рантайме: интервал сбора метрик задан в коде;
// HYSTERIA2_CONFIG_REMARK — пустая read-only строка, которую никто не писал.
//
// Пустой ключ, который нельзя изменить и который ни на что не влияет, — это не
// «задел на будущее», а состояние, которое приходится объяснять в документации
// и защищать в allowlist. Настоящую замену получил только remark: имя профиля
// в share URI теперь выводится из имени пира (см. service/hysteria2_api.go).
const (
JwtSecret = "JWT_SECRET"
PeerSecretKey = "PEER_SECRET_KEY"
PeerSecretEncryptionKey = "PEER_SECRET_ENCRYPTION_KEY"
Hysteria2TrafficStatsSecret = "HYSTERIA2_TRAFFIC_STATS_SECRET"
ResetTrafficCron = "RESET_TRAFFIC_CRON"
)
// Что операторский API имеет право прочитать и записать в таблице `config`.
//
// Список — allowlist, и это принципиально. Раньше здесь работал denylist из
// трёх orchestrator-ключей, а `getConfig`/`listConfig` принимали произвольную
// строку. В той же таблице лежат JWT_SECRET, PEER_SECRET_KEY,
// PEER_SECRET_ENCRYPTION_KEY и HYSTERIA2_TRAFFIC_STATS_SECRET — то есть
// авторизованный запрос `?key=PEER_SECRET_ENCRYPTION_KEY` отдавал master-key
// шифрования секретов пиров, а updateConfigs позволял его подменить. После
// подмены перестают расшифровываться секреты уже существующих пиров.
//
// Именно ради этого удаляли generic export/import таблицы `config`, но точечный
// API остался прежним, и дыра просто сменила размер отверстия.
//
// Denylist здесь неверен структурно: он требует, чтобы автор КАЖДОГО нового
// ключа вспомнил про этот файл. Забытый ключ при denylist сразу публичен, при
// allowlist — сразу закрыт. Отказ по умолчанию — единственное поведение,
// которое не зависит от внимательности.
var (
// publicReadableConfigKeys — настройки, которые панель показывает оператору.
publicReadableConfigKeys = map[string]struct{}{
ResetTrafficCron: {},
}
// publicWritableConfigKeys — подмножество читаемых, которые оператор
// действительно редактирует в интерфейсе.
//
// После очистки мёртвых ключей оба списка совпали: единственная настройка,
// которой владеет панель, — расписание сброса счётчиков трафика. Списки
// намеренно не схлопнуты в один: читаемое и записываемое — разные права, и
// первый же ключ «показываем, но менять нельзя» вернёт разницу.
publicWritableConfigKeys = map[string]struct{}{
ResetTrafficCron: {},
}
)
// IsPublicReadableConfigKey сообщает, можно ли отдать значение ключа наружу.
func IsPublicReadableConfigKey(key string) bool {
_, ok := publicReadableConfigKeys[key]
return ok
}
// IsPublicWritableConfigKey сообщает, можно ли принять значение ключа снаружи.
func IsPublicWritableConfigKey(key string) bool {
_, ok := publicWritableConfigKeys[key]
return ok
}
// PublicReadableConfigKeys возвращает копию списка читаемых ключей.
// Используется тестами и текстами ошибок.
func PublicReadableConfigKeys() []string {
return keysOf(publicReadableConfigKeys)
}
// PublicWritableConfigKeys возвращает копию списка записываемых ключей.
func PublicWritableConfigKeys() []string {
return keysOf(publicWritableConfigKeys)
}
// InternalConfigKeys — ключи, которые не покидают процесс ни при каком
// сценарии. Объявлены явно, чтобы тест мог проверить их поимённо, а не
// полагаться на то, что их «случайно нет» в allowlist.
func InternalConfigKeys() []string {
return []string{
JwtSecret,
PeerSecretKey,
PeerSecretEncryptionKey,
Hysteria2TrafficStatsSecret,
}
}
func keysOf(set map[string]struct{}) []string {
out := make([]string, 0, len(set))
for key := range set {
out = append(out, key)
}
return out
}