ddf0ddf71e
Сквозная миграция 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 файлов), документация на русском.
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { renderHysteriaConfig } from "../src/steps/config";
|
|
import { renderObfsBlock } from "../src/config/profile";
|
|
import { baselineConfig, testContext } from "./fixtures";
|
|
|
|
const TEMPLATE = readFileSync(
|
|
join(import.meta.dir, "..", "..", "package", "templates", "hysteria", "config.yaml.tpl"),
|
|
"utf8"
|
|
);
|
|
|
|
function render(overrides: Record<string, string | null> = {}): string {
|
|
return renderHysteriaConfig(testContext(baselineConfig(overrides)), TEMPLATE);
|
|
}
|
|
|
|
describe("obfs block", () => {
|
|
test("gecko рендерит только gecko-подблок", () => {
|
|
const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" });
|
|
expect(yaml).toContain("obfs:\n type: gecko\n gecko:");
|
|
expect(yaml).toContain("minPacketSize: 512");
|
|
expect(yaml).toContain("maxPacketSize: 1200");
|
|
expect(yaml).not.toContain("salamander");
|
|
});
|
|
|
|
test("salamander рендерит только salamander-подблок", () => {
|
|
const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: "salamander" });
|
|
expect(yaml).toContain("obfs:\n type: salamander\n salamander:");
|
|
expect(yaml).not.toContain("gecko");
|
|
expect(yaml).not.toContain("minPacketSize");
|
|
});
|
|
|
|
test("в конфиге никогда нет двух подтипов obfs одновременно", () => {
|
|
for (const obfsType of ["gecko", "salamander"]) {
|
|
const yaml = render({ HY2XS_HYSTERIA_OBFS_TYPE: obfsType });
|
|
const subtypes = [" gecko:", " salamander:"].filter((marker) => yaml.includes(marker));
|
|
expect(subtypes).toHaveLength(1);
|
|
}
|
|
});
|
|
|
|
test("obfs-пароль экранируется кавычками и попадает в оба профиля", () => {
|
|
for (const obfsType of ["gecko", "salamander"]) {
|
|
const yaml = render({
|
|
HY2XS_HYSTERIA_OBFS_TYPE: obfsType,
|
|
HY2XS_HYSTERIA_OBFS_PASSWORD: "p@ss:w#rd with spaces"
|
|
});
|
|
expect(yaml).toContain('password: "p@ss:w#rd with spaces"');
|
|
}
|
|
});
|
|
|
|
test("рендер блока защищается от YAML-инъекции даже в обход env-валидации", () => {
|
|
const config = baselineConfig();
|
|
const injected = { ...config, hysteriaObfsPassword: 'x"\nlisten: 0.0.0.0:1' };
|
|
expect(() => renderObfsBlock(injected)).toThrow(/forbidden characters/);
|
|
});
|
|
|
|
test("рендер отклоняет невалидные gecko-размеры, даже если они пришли из обхода env", () => {
|
|
const config = baselineConfig();
|
|
expect(() => renderObfsBlock({ ...config, hysteriaGeckoMaxPacketSize: 4096 })).toThrow(
|
|
/upstream limit is 2048/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("современный серверный baseline", () => {
|
|
test("bandwidth содержит явный disableLossCompensation: false", () => {
|
|
expect(render()).toContain("disableLossCompensation: false");
|
|
});
|
|
|
|
test("congestion фиксирует bbr/standard", () => {
|
|
const yaml = render();
|
|
expect(yaml).toContain("congestion:\n type: bbr\n bbrProfile: standard");
|
|
});
|
|
|
|
test("quic фиксирует disableStatelessReset: false", () => {
|
|
expect(render()).toContain("disableStatelessReset: false");
|
|
});
|
|
|
|
test("quic содержит полный набор baseline-полей", () => {
|
|
const yaml = render();
|
|
for (const field of [
|
|
"initStreamReceiveWindow: 8388608",
|
|
"maxStreamReceiveWindow: 8388608",
|
|
"initConnReceiveWindow: 20971520",
|
|
"maxConnReceiveWindow: 20971520",
|
|
"maxIdleTimeout: 30s",
|
|
"maxIncomingStreams: 1024",
|
|
"disablePathMTUDiscovery: false"
|
|
]) {
|
|
expect(yaml).toContain(field);
|
|
}
|
|
});
|
|
|
|
test("bandwidth-политика 50/50 сохраняется", () => {
|
|
const yaml = render();
|
|
expect(yaml).toContain('up: "50 mbps"');
|
|
expect(yaml).toContain('down: "50 mbps"');
|
|
expect(yaml).toContain("ignoreClientBandwidth: false");
|
|
});
|
|
});
|
|
|
|
describe("TLS-режимы", () => {
|
|
test("acme рендерит только acme-блок", () => {
|
|
const yaml = render({ HY2XS_TLS_MODE: "acme" });
|
|
expect(yaml).toMatch(/^acme:/m);
|
|
expect(yaml).not.toMatch(/^tls:/m);
|
|
expect(yaml).toContain("insecure: false");
|
|
});
|
|
|
|
test("file рендерит только tls-блок", () => {
|
|
const yaml = render({ HY2XS_TLS_MODE: "file" });
|
|
expect(yaml).toMatch(/^tls:/m);
|
|
expect(yaml).not.toMatch(/^acme:/m);
|
|
});
|
|
|
|
test("self_signed_dev включает insecure для локального auth", () => {
|
|
const yaml = render({
|
|
HY2XS_TLS_MODE: "self_signed_dev",
|
|
HY2XS_ALLOW_SELF_SIGNED_DEV: "true"
|
|
});
|
|
expect(yaml).toContain("insecure: true");
|
|
});
|
|
});
|
|
|
|
describe("шаблон", () => {
|
|
test("не осталось незаполненных плейсхолдеров", () => {
|
|
expect(render()).not.toMatch(/\{\{\s*[A-Z0-9_]+\s*\}\}/);
|
|
});
|
|
|
|
test("auth URL содержит machine access token", () => {
|
|
expect(render()).toContain(
|
|
"/hui/hysteria2/auth?access_token=traffic-stats-secret"
|
|
);
|
|
});
|
|
|
|
test("шаблон не содержит захардкоженного типа обфускации", () => {
|
|
expect(TEMPLATE).not.toContain("salamander");
|
|
expect(TEMPLATE).not.toContain("gecko");
|
|
expect(TEMPLATE).toContain("{{OBFS_BLOCK}}");
|
|
});
|
|
});
|