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 файлов), документация на русском.
172 lines
5.0 KiB
TypeScript
172 lines
5.0 KiB
TypeScript
/**
|
|
* Build-time CLI: рендерит канонический серверный конфиг HY2XS тем же кодом,
|
|
* который работает на target-сервере.
|
|
*
|
|
* Используется compatibility gate: получившийся YAML скармливается реальному
|
|
* upstream-бинарнику Hysteria до того, как будет создан release package.
|
|
*
|
|
* bun run tools/render-canonical-config.ts \
|
|
* --package-dir . --obfs gecko --tls-mode file \
|
|
* --cert /tmp/x.crt --key /tmp/x.key --out /tmp/config.yaml
|
|
*/
|
|
|
|
import { parseRuntimeEnv } from "../src/config/env";
|
|
import { HYSTERIA_OBFS_TYPES } from "../src/config/profile";
|
|
import { hysteriaConfigTemplatePath, renderHysteriaConfig } from "../src/steps/config";
|
|
import type { RuntimeContext } from "../src/types/context";
|
|
|
|
type Options = {
|
|
packageDir: string;
|
|
configPath: string;
|
|
templatePath: string;
|
|
obfs: string;
|
|
tlsMode: string;
|
|
certPath: string;
|
|
keyPath: string;
|
|
hysteriaPort: string;
|
|
trafficStatsPort: string;
|
|
outPath: string;
|
|
};
|
|
|
|
function fail(message: string): never {
|
|
console.error(`[hy2xs-build] ERROR: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function parseArgs(argv: string[]): Options {
|
|
const options: Options = {
|
|
packageDir: "",
|
|
configPath: "",
|
|
templatePath: "",
|
|
obfs: "gecko",
|
|
tlsMode: "file",
|
|
certPath: "",
|
|
keyPath: "",
|
|
hysteriaPort: "",
|
|
trafficStatsPort: "",
|
|
outPath: ""
|
|
};
|
|
|
|
const flags: Record<string, keyof Options> = {
|
|
"--package-dir": "packageDir",
|
|
"--config": "configPath",
|
|
"--template": "templatePath",
|
|
"--obfs": "obfs",
|
|
"--tls-mode": "tlsMode",
|
|
"--cert": "certPath",
|
|
"--key": "keyPath",
|
|
"--port": "hysteriaPort",
|
|
"--traffic-stats-port": "trafficStatsPort",
|
|
"--out": "outPath"
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const key = flags[argv[i]];
|
|
if (!key) {
|
|
fail(`unknown argument: ${argv[i]}`);
|
|
}
|
|
const value = argv[i + 1];
|
|
if (value === undefined || value.startsWith("--")) {
|
|
fail(`missing value for ${argv[i]}`);
|
|
}
|
|
options[key] = value;
|
|
i += 1;
|
|
}
|
|
|
|
if (!options.packageDir) {
|
|
fail("missing --package-dir");
|
|
}
|
|
if (!options.outPath) {
|
|
fail("missing --out");
|
|
}
|
|
if (!(HYSTERIA_OBFS_TYPES as readonly string[]).includes(options.obfs)) {
|
|
fail(`unsupported --obfs value: ${options.obfs}`);
|
|
}
|
|
|
|
options.configPath ||= `${options.packageDir}/config/hy2xs.env`;
|
|
options.templatePath ||= hysteriaConfigTemplatePath(options.packageDir);
|
|
|
|
return options;
|
|
}
|
|
|
|
function overrideEnv(source: string, overrides: Record<string, string>): string {
|
|
const pending = new Map(Object.entries(overrides));
|
|
const lines = source.split(/\r?\n/).map((line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) {
|
|
return line;
|
|
}
|
|
const key = trimmed.slice(0, trimmed.indexOf("="));
|
|
if (pending.has(key)) {
|
|
const value = pending.get(key) as string;
|
|
pending.delete(key);
|
|
return `${key}=${value}`;
|
|
}
|
|
return line;
|
|
});
|
|
|
|
for (const [key, value] of pending) {
|
|
lines.push(`${key}=${value}`);
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const options = parseArgs(Bun.argv.slice(2));
|
|
|
|
const overrides: Record<string, string> = {
|
|
HY2XS_HYSTERIA_OBFS_TYPE: options.obfs,
|
|
HY2XS_TLS_MODE: options.tlsMode,
|
|
// Compatibility gate работает офлайн и не должен зависеть от секретов пакета.
|
|
HY2XS_HYSTERIA_OBFS_PASSWORD: "hy2xs-compat-gate-obfs-password",
|
|
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET: "hy2xs-compat-gate-traffic-secret",
|
|
HY2XS_ADMIN_INITIAL_PASSWORD: "hy2xs-compat-gate-admin-password",
|
|
HY2XS_ADMIN_CON_PASS: "hy2xs-compat-gate-con-password"
|
|
};
|
|
|
|
if (options.certPath) {
|
|
overrides.HY2XS_TLS_CERT_PATH = options.certPath;
|
|
}
|
|
if (options.keyPath) {
|
|
overrides.HY2XS_TLS_KEY_PATH = options.keyPath;
|
|
}
|
|
if (options.hysteriaPort) {
|
|
overrides.HY2XS_HYSTERIA_PORT = options.hysteriaPort;
|
|
}
|
|
if (options.trafficStatsPort) {
|
|
overrides.HY2XS_HYSTERIA_TRAFFIC_STATS_PORT = options.trafficStatsPort;
|
|
}
|
|
|
|
const sourceEnv = await Bun.file(options.configPath).text();
|
|
const config = parseRuntimeEnv(overrideEnv(sourceEnv, overrides));
|
|
|
|
const context = {
|
|
mode: "install",
|
|
options: {
|
|
packageDir: options.packageDir,
|
|
sourceConfigPath: options.configPath,
|
|
runtimeConfigPath: "/etc/hy2xs/hy2xs.env",
|
|
nonInteractive: true,
|
|
skipFirewall: true,
|
|
skipServiceStart: true,
|
|
skipSmoke: true
|
|
},
|
|
config,
|
|
packageVersion: "compat-gate",
|
|
packageBuildId: "compat-gate",
|
|
installDate: new Date().toISOString(),
|
|
hysteriaVersion: "compat-gate",
|
|
hysteriaResolution: "compat-gate"
|
|
} satisfies RuntimeContext;
|
|
|
|
const template = await Bun.file(options.templatePath).text();
|
|
await Bun.write(options.outPath, renderHysteriaConfig(context, template));
|
|
}
|
|
|
|
try {
|
|
await main();
|
|
} catch (error) {
|
|
fail(error instanceof Error ? error.message : String(error));
|
|
}
|