feat(v1): Gecko-обфускация, latest-stable Hysteria на сборке и forward-compatible admin
Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1. Build: - версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и замораживается в metadata пакета (version + immutable url + sha256); - compatibility gate: реальный бинарник должен принять канонический конфиг HY2XS для gecko и salamander до создания пакета; - сборка прогоняет тесты оркестратора и админки. Конфигурационный контракт: - HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast; - obfs стал настоящим union gecko|salamander, gecko — default; - obfs-блок рендерится оркестратором целиком, два подтипа одновременно структурно невозможны; - современный baseline: congestion bbr/standard, disableLossCompensation=false, disableStatelessReset=false, полный quic-блок. Исправления: - share URI для gecko: генератор был завязан на Obfs.Salamander.Password и выдавал нерабочую ссылку при любой другой обфускации; - SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file; - экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль; - экспорт терял неизвестные upstream-поля при round-trip через типизированную модель; - renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом; - namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0). Тесты: - 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover; - тесты URI и экспорта в Go; - tools/test/e2e-hysteria.sh с реальным клиентом Hysteria. UX: - подсказки и примеры в форме создания пира. Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов), документация на русском.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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;
|
||||
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user