import type { HysteriaObfsType, RuntimeConfig } from "../types/context"; /** * HY2XS production profile: единственное место, где определены значения * серверной политики. Всё остальное (шаблоны, post-install.env, smoke, * build-time compatibility gate) только передаёт эти значения дальше. */ export const HY2XS_CONFIG_SCHEMA_VERSION = 2; /** * Линия релиза продукта. Меняется только при смене поколения, внутри которого * установка остаётся совместимой сама с собой. Используется install-state, * чтобы reconfigure/repair не работали поверх чужого поколения. * * Значение синхронизировано с HY2XS_RELEASE_LINE в корневом versions.env * (проверяется шагом verify_versions_contract на сборке). */ export const HY2XS_RELEASE_LINE = 1; /** * Целевая платформа production-профиля. Значения синхронизированы с * HY2XS_TARGET_OS_VERSION / HY2XS_TARGET_ARCH в корневом versions.env. */ export const HY2XS_TARGET_DEBIAN_VERSION = 13; export const HY2XS_TARGET_ARCH = "amd64"; /** * Пространства имён HTTP API админки. * * `HYSTERIA_MACHINE_AUTH_PATH` — не внутреннее имя переменной, а runtime-контракт * продукта: оркестратор записывает этот путь в /etc/hysteria/config.yaml и в * post-install.env, и по нему Hysteria спрашивает у админки разрешение на * подключение пира. Раньше эта строка (в терминах H UI) была размазана по * шаблонам, smoke, assertions, тестам, acceptance и e2e. * * Значения обязаны совпадать с константами админки * (apps/model/constant/api.go). Сверка выполняется на сборке шагом * verify_versions_contract через tools/print-contract.ts. */ export const ADMIN_API_BASE = "/api"; export const HYSTERIA_MACHINE_AUTH_PATH = "/internal/hysteria/auth"; /** Полный machine-auth URL, который видит Hysteria. */ export function hysteriaMachineAuthUrl(uiPort: number, machineToken: string): string { return `http://127.0.0.1:${uiPort}${HYSTERIA_MACHINE_AUTH_PATH}?access_token=${machineToken}`; } export const HYSTERIA_OBFS_TYPES: readonly HysteriaObfsType[] = ["gecko", "salamander"]; /** Тип обфускации для новой установки. Salamander остаётся compatibility fallback. */ export const DEFAULT_HYSTERIA_OBFS_TYPE: HysteriaObfsType = "gecko"; /** Upstream defaults Gecko. HY2XS фиксирует их явно как tested production profile. */ export const GECKO_DEFAULT_MIN_PACKET_SIZE = 512; export const GECKO_DEFAULT_MAX_PACKET_SIZE = 1200; /** Upstream ограничение: maxPacketSize >= minPacketSize и <= 2048. */ export const GECKO_MAX_PACKET_SIZE_LIMIT = 2048; /** * Fallback congestion controller. Используется, когда Brutal bandwidth * не согласован сторонами; сам Brutal включается через bandwidth up/down. */ export const CONGESTION_TYPE = "bbr"; export const BBR_PROFILE = "standard"; /** * Loss compensation оставлен включённым (upstream default), поэтому * в конфиге явно фиксируется disableLossCompensation: false. */ export const DISABLE_LOSS_COMPENSATION = false; /** * QUIC stateless reset нужен HY2XS: клиент со stale-соединением после * перезапуска сервера или сна устройства переподключается сразу. */ export const DISABLE_STATELESS_RESET = false; export const QUIC_BASELINE = { initStreamReceiveWindow: 8388608, maxStreamReceiveWindow: 8388608, initConnReceiveWindow: 20971520, maxConnReceiveWindow: 20971520, maxIdleTimeout: "30s", maxIncomingStreams: 1024, disablePathMTUDiscovery: false } as const; export function isHysteriaObfsType(value: string): value is HysteriaObfsType { return (HYSTERIA_OBFS_TYPES as readonly string[]).includes(value); } export function normalizeHysteriaObfsType(value: string | undefined): HysteriaObfsType { const obfsType = (value ?? "").trim() || DEFAULT_HYSTERIA_OBFS_TYPE; if (!isHysteriaObfsType(obfsType)) { throw new Error( `invalid HY2XS_HYSTERIA_OBFS_TYPE: ${value} (supported: ${HYSTERIA_OBFS_TYPES.join(", ")})` ); } return obfsType; } export function validateGeckoPacketSizes(minPacketSize: number, maxPacketSize: number): void { if (!Number.isInteger(minPacketSize) || minPacketSize <= 0) { throw new Error(`invalid Gecko minPacketSize: ${minPacketSize} (must be a positive integer)`); } if (!Number.isInteger(maxPacketSize) || maxPacketSize <= 0) { throw new Error(`invalid Gecko maxPacketSize: ${maxPacketSize} (must be a positive integer)`); } if (maxPacketSize < minPacketSize) { throw new Error( `invalid Gecko packet sizes: maxPacketSize ${maxPacketSize} must be >= minPacketSize ${minPacketSize}` ); } if (maxPacketSize > GECKO_MAX_PACKET_SIZE_LIMIT) { throw new Error( `invalid Gecko maxPacketSize: ${maxPacketSize} (upstream limit is ${GECKO_MAX_PACKET_SIZE_LIMIT})` ); } } function assertYamlSafeQuoted(name: string, value: string): string { if (!value) { throw new Error(`missing required ${name}`); } if (/["\n\r]/.test(value)) { throw new Error(`${name} contains forbidden characters for HY2XS YAML profile`); } return value; } /** * Рендерит целиком проверенный obfs-блок. Type selector никогда не собирается * внутри статического YAML, поэтому комбинация вида `type: gecko` + `salamander:` * структурно невозможна. */ export function renderObfsBlock(config: RuntimeConfig): string { const password = assertYamlSafeQuoted("HY2XS_HYSTERIA_OBFS_PASSWORD", config.hysteriaObfsPassword); if (config.hysteriaObfsType === "gecko") { validateGeckoPacketSizes(config.hysteriaGeckoMinPacketSize, config.hysteriaGeckoMaxPacketSize); return [ "obfs:", " type: gecko", " gecko:", ` password: "${password}"`, ` minPacketSize: ${config.hysteriaGeckoMinPacketSize}`, ` maxPacketSize: ${config.hysteriaGeckoMaxPacketSize}` ].join("\n"); } if (config.hysteriaObfsType === "salamander") { return ["obfs:", " type: salamander", " salamander:", ` password: "${password}"`].join("\n"); } throw new Error(`unsupported obfs type: ${config.hysteriaObfsType satisfies never}`); } export function renderCongestionBlock(): string { return ["congestion:", ` type: ${CONGESTION_TYPE}`, ` bbrProfile: ${BBR_PROFILE}`].join("\n"); } export function renderQuicBlock(): string { return [ "quic:", ` initStreamReceiveWindow: ${QUIC_BASELINE.initStreamReceiveWindow}`, ` maxStreamReceiveWindow: ${QUIC_BASELINE.maxStreamReceiveWindow}`, ` initConnReceiveWindow: ${QUIC_BASELINE.initConnReceiveWindow}`, ` maxConnReceiveWindow: ${QUIC_BASELINE.maxConnReceiveWindow}`, ` maxIdleTimeout: ${QUIC_BASELINE.maxIdleTimeout}`, ` maxIncomingStreams: ${QUIC_BASELINE.maxIncomingStreams}`, ` disablePathMTUDiscovery: ${QUIC_BASELINE.disablePathMTUDiscovery}`, ` disableStatelessReset: ${DISABLE_STATELESS_RESET}` ].join("\n"); }