import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { renderHysteriaConfig } from "../src/steps/config"; import { assertHysteriaConfigMatchesProfile } from "../src/steps/configAssertions"; import { baselineConfig, testContext } from "./fixtures"; const TEMPLATE = readFileSync( join(import.meta.dir, "..", "..", "package", "templates", "hysteria", "config.yaml.tpl"), "utf8" ); function renderFor(overrides: Record = {}) { const config = baselineConfig(overrides); return { config, yaml: renderHysteriaConfig(testContext(config), TEMPLATE) }; } describe("сгенерированный конфиг проходит собственную семантическую проверку", () => { for (const obfsType of ["gecko", "salamander"]) { for (const tlsMode of ["acme", "file"]) { test(`obfs=${obfsType}, tls=${tlsMode}`, () => { const { config, yaml } = renderFor({ HY2XS_HYSTERIA_OBFS_TYPE: obfsType, HY2XS_TLS_MODE: tlsMode }); expect(() => assertHysteriaConfigMatchesProfile(yaml, config)).not.toThrow(); }); } } }); describe("подмены в конфиге обнаруживаются", () => { test("тип obfs не совпадает с профилем", () => { const { config, yaml } = renderFor({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" }); const tampered = yaml.replace("type: gecko", "type: salamander"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/obfs.*type/); }); test("в obfs остался второй подтип", () => { const { config, yaml } = renderFor({ HY2XS_HYSTERIA_OBFS_TYPE: "gecko" }); const tampered = yaml.replace( "obfs:\n type: gecko", "obfs:\n salamander:\n password: \"leftover\"\n type: gecko" ); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/exactly the gecko subsection/); }); test("изменённый gecko packet size", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("maxPacketSize: 1200", "maxPacketSize: 1400"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/maxPacketSize/); }); test("stateless reset выключен", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("disableStatelessReset: false", "disableStatelessReset: true"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/disableStatelessReset/); }); test("loss compensation выключена", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("disableLossCompensation: false", "disableLossCompensation: true"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/disableLossCompensation/); }); test("подменён congestion controller", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("type: bbr", "type: reno"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/congestion\.type/); }); test("подменён bbr profile", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("bbrProfile: standard", "bbrProfile: aggressive"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/bbrProfile/); }); test("исчезла секция congestion", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/congestion:\n {2}type: bbr\n {2}bbrProfile: standard\n/, ""); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/missing required section congestion/); }); test("auth url потерял machine token", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/\?access_token=[^\s]*/, ""); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/machine access token/); }); test("пустой obfs-пароль", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/password: "[^"]*"/, 'password: ""'); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/non-empty string/); }); test("acme-профиль с посторонней tls-секцией", () => { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: "acme" }); const tampered = `${yaml}\ntls:\n cert: /tmp/x.crt\n key: /tmp/x.key\n`; expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/must not emit a tls section/); }); test("file-профиль с посторонней acme-секцией", () => { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: "file" }); const tampered = `${yaml}\nacme:\n domains:\n - x.example.com\n`; expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/must not emit an acme section/); }); test("подменён listen", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("listen: 0.0.0.0:443", "listen: 0.0.0.0:8443"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/listen must be/); }); test("подменён trafficStats listen", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("127.0.0.1:36712", "0.0.0.0:36712"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/trafficStats\.listen/); }); test("невалидный YAML отвергается", () => { const { config } = renderFor(); expect(() => assertHysteriaConfigMatchesProfile("just a string", config)).toThrow(); }); // Инвариант присутствовал в профиле, но не проверялся: конфиг с уехавшим // idle timeout проходил семантическую проверку. test("подменён quic.maxIdleTimeout", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("maxIdleTimeout: 30s", "maxIdleTimeout: 5s"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/maxIdleTimeout/); }); test("исчез quic.maxIdleTimeout", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/ {2}maxIdleTimeout: 30s\n/, ""); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/maxIdleTimeout/); }); test("auth url указывает на другой порт", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("http://127.0.0.1:8080/hui", "http://127.0.0.1:9090/hui"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/auth\.http\.url must be/); }); test("auth url указывает на другой путь", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("/hui/hysteria2/auth", "/hui/hysteria2/authorize"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/auth\.http\.url must be/); }); test("auth url несёт чужой токен", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace(/access_token=[^\s]*/, "access_token=someone-elses-token"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/auth\.http\.url must be/); }); test("сообщение об ошибке auth url не печатает сам токен", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("http://127.0.0.1:8080/hui", "http://127.0.0.1:9090/hui"); try { assertHysteriaConfigMatchesProfile(tampered, config); throw new Error("expected assertion to fail"); } catch (error) { expect(String(error)).not.toContain(config.hysteriaTrafficStatsSecret); } }); test("auth.http.insecure включён вне self_signed_dev", () => { const { config, yaml } = renderFor(); const tampered = yaml.replace("insecure: false", "insecure: true"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/auth\.http\.insecure/); }); test("подменён acme.email", () => { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: "acme" }); const tampered = yaml.replace("email: admin@example.com", "email: attacker@example.com"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/acme\.email/); }); test("подменён acme.ca", () => { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: "acme" }); const tampered = yaml.replace("ca: letsencrypt", "ca: zerossl"); expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow(/acme\.ca/); }); test("посторонняя секция верхнего уровня отвергается", () => { const { config, yaml } = renderFor(); const tampered = `${yaml}\nresolver:\n type: system\n`; expect(() => assertHysteriaConfigMatchesProfile(tampered, config)).toThrow( /unexpected top-level sections/ ); }); test("сгенерированный конфиг не содержит посторонних секций", () => { for (const tlsMode of ["acme", "file"]) { const { config, yaml } = renderFor({ HY2XS_TLS_MODE: tlsMode }); expect(() => assertHysteriaConfigMatchesProfile(yaml, config)).not.toThrow(); } }); });