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,160 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { parseRuntimeEnv, renderRuntimeEnv } from "../src/config/env";
|
||||
import {
|
||||
GECKO_DEFAULT_MAX_PACKET_SIZE,
|
||||
GECKO_DEFAULT_MIN_PACKET_SIZE,
|
||||
HY2XS_CONFIG_SCHEMA_VERSION,
|
||||
normalizeHysteriaObfsType,
|
||||
validateGeckoPacketSizes
|
||||
} from "../src/config/profile";
|
||||
import { baselineConfig, envText } from "./fixtures";
|
||||
|
||||
describe("obfs type resolution", () => {
|
||||
test("новая установка без явного значения выбирает gecko", () => {
|
||||
const config = parseRuntimeEnv(envText({ HY2XS_HYSTERIA_OBFS_TYPE: null }));
|
||||
expect(config.hysteriaObfsType).toBe("gecko");
|
||||
});
|
||||
|
||||
test("явный gecko принимается", () => {
|
||||
expect(baselineConfig({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" }).hysteriaObfsType).toBe("gecko");
|
||||
});
|
||||
|
||||
test("явный salamander принимается как compatibility fallback", () => {
|
||||
expect(baselineConfig({ HY2XS_HYSTERIA_OBFS_TYPE: "salamander" }).hysteriaObfsType).toBe("salamander");
|
||||
});
|
||||
|
||||
test("неизвестный тип отклоняется", () => {
|
||||
expect(() => baselineConfig({ HY2XS_HYSTERIA_OBFS_TYPE: "none" })).toThrow(
|
||||
/invalid HY2XS_HYSTERIA_OBFS_TYPE/
|
||||
);
|
||||
});
|
||||
|
||||
test("тип нечувствителен к окружающим пробелам, но не к регистру", () => {
|
||||
expect(normalizeHysteriaObfsType(" gecko ")).toBe("gecko");
|
||||
expect(() => normalizeHysteriaObfsType("Gecko")).toThrow(/invalid HY2XS_HYSTERIA_OBFS_TYPE/);
|
||||
});
|
||||
|
||||
test("пустой obfs-пароль означает автогенерацию, а не пустое значение в конфиге", () => {
|
||||
for (const raw of ["", "__GENERATE__"]) {
|
||||
const config = baselineConfig({ HY2XS_HYSTERIA_OBFS_PASSWORD: raw });
|
||||
expect(config.hysteriaObfsPassword).not.toBe("");
|
||||
expect(config.hysteriaObfsPassword).not.toBe("__GENERATE__");
|
||||
expect(config.hysteriaObfsPassword.length).toBeGreaterThanOrEqual(24);
|
||||
}
|
||||
});
|
||||
|
||||
test("два разбора генерируют разные пароли", () => {
|
||||
const first = baselineConfig({ HY2XS_HYSTERIA_OBFS_PASSWORD: "__GENERATE__" });
|
||||
const second = baselineConfig({ HY2XS_HYSTERIA_OBFS_PASSWORD: "__GENERATE__" });
|
||||
expect(first.hysteriaObfsPassword).not.toBe(second.hysteriaObfsPassword);
|
||||
});
|
||||
|
||||
test("obfs-пароль с кавычкой или переводом строки отклоняется", () => {
|
||||
expect(() => baselineConfig({ HY2XS_HYSTERIA_OBFS_PASSWORD: 'pa"ss' })).toThrow(
|
||||
/forbidden characters/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gecko packet sizes", () => {
|
||||
test("baseline фиксирует upstream defaults 512/1200", () => {
|
||||
const config = baselineConfig();
|
||||
expect(config.hysteriaGeckoMinPacketSize).toBe(512);
|
||||
expect(config.hysteriaGeckoMaxPacketSize).toBe(1200);
|
||||
expect(config.hysteriaGeckoMinPacketSize).toBe(GECKO_DEFAULT_MIN_PACKET_SIZE);
|
||||
expect(config.hysteriaGeckoMaxPacketSize).toBe(GECKO_DEFAULT_MAX_PACKET_SIZE);
|
||||
});
|
||||
|
||||
test("валидная пара принимается", () => {
|
||||
expect(() => validateGeckoPacketSizes(512, 1200)).not.toThrow();
|
||||
expect(() => validateGeckoPacketSizes(700, 700)).not.toThrow();
|
||||
});
|
||||
|
||||
test("max < min отклоняется", () => {
|
||||
expect(() => validateGeckoPacketSizes(1200, 512)).toThrow(/must be >= minPacketSize/);
|
||||
});
|
||||
|
||||
test("max > 2048 отклоняется", () => {
|
||||
expect(() => validateGeckoPacketSizes(512, 2049)).toThrow(/upstream limit is 2048/);
|
||||
});
|
||||
|
||||
test("max == 2048 принимается как граничное значение", () => {
|
||||
expect(() => validateGeckoPacketSizes(512, 2048)).not.toThrow();
|
||||
});
|
||||
|
||||
test("неположительный min отклоняется", () => {
|
||||
expect(() => validateGeckoPacketSizes(0, 1200)).toThrow(/positive integer/);
|
||||
expect(() => validateGeckoPacketSizes(-1, 1200)).toThrow(/positive integer/);
|
||||
});
|
||||
|
||||
test("нецелые значения отклоняются", () => {
|
||||
expect(() => validateGeckoPacketSizes(512.5, 1200)).toThrow(/positive integer/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("config schema version", () => {
|
||||
test("отсутствие значения даёт текущую схему", () => {
|
||||
const config = parseRuntimeEnv(envText({ HY2XS_CONFIG_SCHEMA_VERSION: null }));
|
||||
expect(config.configSchemaVersion).toBe(HY2XS_CONFIG_SCHEMA_VERSION);
|
||||
});
|
||||
|
||||
test("схема v0/v1 отклоняется с указанием на чистую установку", () => {
|
||||
expect(() => baselineConfig({ HY2XS_CONFIG_SCHEMA_VERSION: "1" })).toThrow(
|
||||
/requires a clean installation/
|
||||
);
|
||||
});
|
||||
|
||||
test("нечисловая схема отклоняется", () => {
|
||||
expect(() => baselineConfig({ HY2XS_CONFIG_SCHEMA_VERSION: "two" })).toThrow(
|
||||
/invalid HY2XS_CONFIG_SCHEMA_VERSION/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderRuntimeEnv round-trip", () => {
|
||||
// Регрессия: renderRuntimeEnv раньше печатал литерал `salamander`, из-за чего
|
||||
// записанный на диск runtime-конфиг расходился с разобранным.
|
||||
test("gecko переживает render → parse без потерь", () => {
|
||||
const config = baselineConfig({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" });
|
||||
const reparsed = parseRuntimeEnv(renderRuntimeEnv(config));
|
||||
expect(reparsed).toEqual(config);
|
||||
});
|
||||
|
||||
test("salamander переживает render → parse без подмены на дефолт", () => {
|
||||
const config = baselineConfig({ HY2XS_HYSTERIA_OBFS_TYPE: "salamander" });
|
||||
const rendered = renderRuntimeEnv(config);
|
||||
expect(rendered).toContain("HY2XS_HYSTERIA_OBFS_TYPE=salamander");
|
||||
expect(rendered).not.toContain("HY2XS_HYSTERIA_OBFS_TYPE=gecko");
|
||||
expect(parseRuntimeEnv(rendered)).toEqual(config);
|
||||
});
|
||||
|
||||
test("render не теряет ни одно поле runtime-конфига", () => {
|
||||
const config = baselineConfig({ HY2XS_HYSTERIA_OBFS_TYPE: "salamander" });
|
||||
const reparsed = parseRuntimeEnv(renderRuntimeEnv(config));
|
||||
for (const key of Object.keys(config) as (keyof typeof config)[]) {
|
||||
expect(reparsed[key]).toEqual(config[key]);
|
||||
}
|
||||
});
|
||||
|
||||
test("схема конфигурации попадает в runtime-файл", () => {
|
||||
expect(renderRuntimeEnv(baselineConfig())).toContain(
|
||||
`HY2XS_CONFIG_SCHEMA_VERSION=${HY2XS_CONFIG_SCHEMA_VERSION}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("остальные production-инварианты", () => {
|
||||
test("IPv6 остаётся запрещённым", () => {
|
||||
expect(() => baselineConfig({ HY2XS_IPV6_ENABLED: "true" })).toThrow(/IPv4-only/);
|
||||
});
|
||||
|
||||
test("auth mode зафиксирован в http", () => {
|
||||
expect(() => baselineConfig({ HY2XS_HYSTERIA_AUTH_MODE: "password" })).toThrow(
|
||||
/HY2XS_HYSTERIA_AUTH_MODE is fixed/
|
||||
);
|
||||
});
|
||||
|
||||
test("ACME DNS challenge пока запрещён в production-профиле", () => {
|
||||
expect(() => baselineConfig({ HY2XS_ACME_TYPE: "dns" })).toThrow(/not supported in production profile/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user