Files
HY2XS_flamy/apps/model/constant/config.go
T
founder cf094f6e6f fix(v1): сделать отзыв доступа, бэкап и диагностику соответствующими своим именам
Проход по операциям, которые делают не то, что обещает их имя.

P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать
пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS
продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор
удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же
секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config:
«создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной
транзакцией.

P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и
отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь
недоступный секрет любого пира отклоняет весь запрос с указанием имени.

P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет,
если оно не начиналось с v1: — остаток поколения с открытыми секретами.

P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на
проблему обрывала все живые соединения.

P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в
/etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine
auth переставал совпадать.

P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести
артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и
документации, отсутствие проверяется приёмкой.

P2. UPDATE по отсутствующей строке config считался успехом, и cron
перепланировался при несохранённом значении. Решение по RowsAffected.

P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения
ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage.

P2. Удалены алиасы /:id/client-url и /:id/qr.

Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go —
языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют
среду с versions.env.
2026-08-30 06:48:50 +05:00

127 lines
7.9 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"
// BootstrapPeerSeeded — отметка о том, что пир установщика УЖЕ был создан
// однажды. Не «существует сейчас», а именно «был создан».
//
// Различие и есть суть ключа. Раньше единственным признаком служило наличие
// строки в таблице пиров, поэтому удаление bootstrap-admin-peer выглядело
// отзывом доступа, но им не было: HY2XS_ADMIN_CON_PASS продолжает жить в
// /etc/hy2xs/hy2xs.env, и ближайший `systemctl restart hy2xs-admin` или
// перезагрузка сервера возвращали тот же самый секрет в базу. Молча.
//
// Отзыв учётных данных, который не переживает перезапуск, — не отзыв.
// Маркер делает удаление окончательным: seeder читает его, а не таблицу
// пиров, поэтому после первого успешного bootstrap он не создаёт запись
// больше никогда.
BootstrapPeerSeeded = "BOOTSTRAP_PEER_SEEDED"
)
// Что операторский 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
}