Продакшн рефактор без легаси

This commit is contained in:
2026-04-30 20:54:38 +05:00
parent c10ab1fafd
commit 4a0d9569d1
41 changed files with 193 additions and 888 deletions
+11 -7
View File
@@ -1,4 +1,3 @@
import { randomBytes } from "node:crypto";
import type { InstallContext, InstallOptions } from "../types/context";
import { exists, readText, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
@@ -16,10 +15,6 @@ import { applyFirewall } from "../steps/firewall";
import { writePostInstallEnv } from "../steps/env";
import { smoke } from "../steps/smoke";
function secret(): string {
return randomBytes(24).toString("base64url");
}
export async function install(options: InstallOptions): Promise<void> {
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
if (options.sourceConfigPath && !hasSourceConfig) {
@@ -30,15 +25,22 @@ export async function install(options: InstallOptions): Promise<void> {
const config = parseRuntimeEnv(sourceConfigRaw);
const context: InstallContext = {
mode: "install",
options,
config,
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
installDate: new Date().toISOString(),
hysteriaAuthPassword: secret(),
hysteriaVersion: "unknown"
hysteriaVersion: "unknown",
hysteriaTargetVersion: await readPackageValue(options.packageDir, "hysteria.version", ""),
hysteriaArtifactUrl: await readPackageValue(options.packageDir, "hysteria.url", ""),
hysteriaArtifactSha256: await readPackageValue(options.packageDir, "hysteria.sha256", "")
};
if (!context.hysteriaTargetVersion || !context.hysteriaArtifactUrl || !context.hysteriaArtifactSha256) {
throw new Error("missing Hysteria lock metadata in package: hysteria.version/hysteria.url/hysteria.sha256");
}
step("preflight");
await preflight(context);
step("system dependencies");
@@ -48,6 +50,8 @@ export async function install(options: InstallOptions): Promise<void> {
step("write runtime env");
await runVisible`mkdir -p /etc/hy2xs`;
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
await runVisible`chown root:root ${options.runtimeConfigPath}`;
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
step("bundled UI");
await deployUi(context);
step("Hysteria2 upstream install");
+4 -2
View File
@@ -46,13 +46,13 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
const configRaw = await readText(options.sourceConfigPath);
const config = parseRuntimeEnv(configRaw);
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
const context: ReconfigureContext = {
mode: "reconfigure",
options,
config,
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
installDate: new Date().toISOString(),
hysteriaAuthPassword: "managed-by-ui-auth",
hysteriaVersion: await readInstalledHysteriaVersion()
};
@@ -81,6 +81,8 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
await applyFirewall(context);
step("write env artifacts");
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
await runVisible`chown root:root ${options.runtimeConfigPath}`;
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
await writePostInstallEnv(context);
step("smoke checks");
await smoke(context);
-4
View File
@@ -148,8 +148,6 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
false
),
hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml",
allowLatestHysteria: parseBool("HY2XS_ALLOW_LATEST_HYSTERIA", env.HY2XS_ALLOW_LATEST_HYSTERIA, false),
hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "v2.6.0",
installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin",
dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin",
logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs",
@@ -240,8 +238,6 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
`HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`,
`HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`,
`HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`,
`HY2XS_ALLOW_LATEST_HYSTERIA=${config.allowLatestHysteria}`,
`HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`,
`HY2XS_INSTALL_DIR=${config.installDir}`,
`HY2XS_DATA_DIR=${config.dataDir}`,
`HY2XS_LOG_DIR=${config.logDir}`
+2 -3
View File
@@ -1,8 +1,8 @@
import type { InstallContext } from "../types/context";
import type { RuntimeContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
export async function generateConfig(context: InstallContext): Promise<void> {
export async function generateConfig(context: RuntimeContext): Promise<void> {
const tlsAcmeBlock = context.config.tlsMode === "acme"
? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0\n type: ${context.config.acmeType}`
: "";
@@ -14,7 +14,6 @@ export async function generateConfig(context: InstallContext): Promise<void> {
const rendered = renderTemplate(template, {
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
HYSTERIA_PORT: context.config.hysteriaPort,
HYSTERIA_AUTH_PASSWORD: context.hysteriaAuthPassword,
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
+7 -2
View File
@@ -1,7 +1,8 @@
import type { InstallContext } from "../types/context";
import type { RuntimeContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
export async function writePostInstallEnv(context: InstallContext): Promise<void> {
export async function writePostInstallEnv(context: RuntimeContext): Promise<void> {
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
PACKAGE_VERSION: context.packageVersion,
PACKAGE_BUILD_ID: context.packageBuildId,
@@ -30,6 +31,8 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
});
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
await runVisible`chown root:root /etc/hysteria/post-install.env`;
await runVisible`chmod 0600 /etc/hysteria/post-install.env`;
await writeText(
context.config.bootstrapAdminSecretPath,
[
@@ -39,4 +42,6 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
].join("\n") + "\n",
0o600
);
await runVisible`chown root:root ${context.config.bootstrapAdminSecretPath}`;
await runVisible`chmod 0600 ${context.config.bootstrapAdminSecretPath}`;
}
+8 -5
View File
@@ -4,10 +4,13 @@ import { runVisible } from "../lib/process";
export async function prepareFilesystem(context: InstallContext): Promise<void> {
await runVisible`id -u hysteria >/dev/null 2>&1 || useradd --system --home /var/lib/hysteria --shell /usr/sbin/nologin hysteria`;
await runVisible`id -u hy2xs-admin >/dev/null 2>&1 || useradd --system --home ${context.config.dataDir} --shell /usr/sbin/nologin hy2xs-admin`;
await runVisible`mkdir -p /etc/hy2xs /etc/hysteria /var/lib/hysteria ${context.config.installDir} ${context.config.dataDir} ${context.config.logDir} /usr/local/lib/hy2xs /etc/nftables.d`;
await runVisible`chown -R hysteria:hysteria /etc/hysteria /var/lib/hysteria`;
await runVisible`chown -R root:root ${context.config.installDir}`;
await runVisible`install -d -m 0700 -o root -g root /etc/hy2xs`;
await runVisible`install -d -m 0755 -o root -g root /etc/hysteria`;
await runVisible`install -d -m 0750 -o hysteria -g hysteria /var/lib/hysteria`;
await runVisible`install -d -m 0750 -o hy2xs-admin -g hy2xs-admin ${context.config.dataDir}`;
await runVisible`install -d -m 0750 -o hy2xs-admin -g hy2xs-admin ${context.config.logDir}`;
await runVisible`install -d -m 0755 -o root -g root ${context.config.installDir}`;
await runVisible`install -d -m 0755 -o root -g root /usr/local/lib/hy2xs`;
await runVisible`install -d -m 0755 -o root -g root /etc/nftables.d`;
await runVisible`chmod -R go-w ${context.config.installDir}`;
await runVisible`chown -R hy2xs-admin:hy2xs-admin ${context.config.dataDir} ${context.config.logDir}`;
await runVisible`chmod 700 /etc/hy2xs`;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import type { InstallContext } from "../types/context";
import type { RuntimeContext } from "../types/context";
import { exists, readText, renderTemplate, writeText } from "../lib/fs";
import { fail, info } from "../lib/log";
import { runVisible } from "../lib/process";
@@ -27,7 +27,7 @@ function isSafeNftablesEntrypoint(content: string): boolean {
return effective === "flush ruleset";
}
export async function applyFirewall(context: InstallContext): Promise<void> {
export async function applyFirewall(context: RuntimeContext): Promise<void> {
if (context.options.skipFirewall || !context.config.firewallEnabled) {
info("firewall skipped by flag");
return;
+11 -20
View File
@@ -9,41 +9,32 @@ function normalizeInstalledVersion(raw: string): string {
return raw.trim();
}
function validateVersionPolicy(value: string): void {
function validatePinnedVersion(value: string): void {
if (/^v\d+\.\d+\.\d+$/.test(value)) {
return;
}
throw new Error(`invalid HY2XS_HYSTERIA_VERSION policy: ${value}`);
throw new Error(`invalid pinned Hysteria version in package metadata: ${value}`);
}
export async function installHysteria(context: InstallContext): Promise<void> {
const policy = context.config.hysteriaVersionPolicy;
if (policy === "latest" && !context.config.allowLatestHysteria) {
throw new Error("HY2XS_HYSTERIA_VERSION=latest is not allowed in production; pin vX.Y.Z or set HY2XS_ALLOW_LATEST_HYSTERIA=true");
}
validateVersionPolicy(policy);
validatePinnedVersion(context.hysteriaTargetVersion);
const scriptPath = "/tmp/hy2xs-install-hysteria.sh";
await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location https://get.hy2.sh/ -o ${scriptPath}`;
await runVisible`test -s ${scriptPath}`;
await runVisible`chmod 700 ${scriptPath}`;
const tmp = "/tmp/hy2xs-hysteria-linux-amd64";
if (policy === "latest") {
await runVisible`bash ${scriptPath}`;
} else {
await runVisible`bash ${scriptPath} --version ${policy}`;
}
await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location ${context.hysteriaArtifactUrl} -o ${tmp}`;
await runVisible`test -s ${tmp}`;
await runVisible`printf '%s %s\n' ${context.hysteriaArtifactSha256} ${tmp} | sha256sum -c -`;
await runVisible`install -m 0755 ${tmp} /usr/local/bin/hysteria`;
await runVisible`rm -f ${tmp}`;
await runVisible`test -x /usr/local/bin/hysteria`;
const versionOutput = await run`/usr/local/bin/hysteria version`;
const installedVersion = normalizeInstalledVersion(versionOutput);
context.hysteriaVersion = installedVersion;
if (policy !== "latest" && installedVersion !== policy) {
if (installedVersion !== context.hysteriaTargetVersion) {
throw new Error(
`installed Hysteria version mismatch: expected ${policy}, got ${installedVersion}. Review upstream installer env contract.`
`installed Hysteria version mismatch: expected ${context.hysteriaTargetVersion}, got ${installedVersion}`
);
}
await runVisible`rm -f ${scriptPath}`;
}
+13 -4
View File
@@ -1,4 +1,4 @@
import type { InstallContext } from "../types/context";
import type { RuntimeContext } from "../types/context";
import { exists, readText } from "../lib/fs";
import { fail, info } from "../lib/log";
import { run } from "../lib/process";
@@ -21,8 +21,8 @@ async function isUnitActive(unit: string): Promise<boolean> {
}
}
export async function preflight(context: InstallContext): Promise<void> {
const isReconfigure = context.packageVersion === "reconfigure";
export async function preflight(context: RuntimeContext): Promise<void> {
const isReconfigure = context.mode === "reconfigure";
if (process.getuid?.() !== 0) {
fail("installer must run as root");
@@ -33,7 +33,16 @@ export async function preflight(context: InstallContext): Promise<void> {
fail("HY2XS baseline supports only clean Debian 12");
}
if (!(await exists(`${context.options.packageDir}/ui/hy2xs-admin`))) {
if (!(await exists(`${context.options.packageDir}/systemd/hy2xs-admin.service`))) {
fail("missing hy2xs-admin systemd unit in package");
}
if (!(await exists(`${context.options.packageDir}/systemd/hysteria-server.service`))) {
fail("missing hysteria-server systemd unit in package");
}
if (!(await exists(`${context.options.packageDir}/templates/hysteria/config.yaml.tpl`))) {
fail("missing Hysteria config template in package");
}
if (context.mode === "install" && !(await exists(`${context.options.packageDir}/ui/hy2xs-admin/hy2xs-admin`))) {
fail("bundled HY2XS admin is missing from install package");
}
+12 -2
View File
@@ -1,8 +1,8 @@
import type { InstallContext } from "../types/context";
import type { RuntimeContext } from "../types/context";
import { info } from "../lib/log";
import { runHidden, runSecret, runVisible } from "../lib/process";
export async function smoke(context: InstallContext): Promise<void> {
export async function smoke(context: RuntimeContext): Promise<void> {
if (context.options.skipStart) {
info("service start and smoke checks skipped by flag");
return;
@@ -30,6 +30,9 @@ export async function smoke(context: InstallContext): Promise<void> {
await runVisible`sudo -u hysteria test -r /etc/hysteria/config.yaml`;
await runVisible`sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml`;
await runVisible`sudo -u hy2xs-admin test ! -w /etc/hysteria/config.yaml`;
await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env`;
await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/bootstrap-admin.secret`;
await runVisible`sudo -u hysteria test ! -r /etc/hy2xs/bootstrap-admin.secret`;
await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`;
if (context.config.uiBindHost === "127.0.0.1") {
await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`;
@@ -42,6 +45,13 @@ export async function smoke(context: InstallContext): Promise<void> {
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`);
}
for (let i = 0; i < 10; i += 1) {
const response = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
if (!/"ok"\s*:\s*false/.test(response)) {
throw new Error(`unexpected auth response during rate-limit smoke: ${response}`);
}
}
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
if (!adminConPass) {
throw new Error("admin connection password is empty in bootstrap secret file");
+2 -2
View File
@@ -1,8 +1,8 @@
import type { InstallContext } from "../types/context";
import type { RuntimeContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
export async function deploySystemd(context: InstallContext): Promise<void> {
export async function deploySystemd(context: RuntimeContext): Promise<void> {
const values = {
UI_BIND_HOST: context.config.uiBindHost,
UI_PORT: context.config.uiPort,
+20 -16
View File
@@ -1,4 +1,4 @@
export type InstallOptions = {
export type CommonOptions = {
packageDir: string;
sourceConfigPath: string;
runtimeConfigPath: string;
@@ -7,17 +7,15 @@ export type InstallOptions = {
skipStart: boolean;
};
export type ReconfigureOptions = {
packageDir: string;
sourceConfigPath: string;
runtimeConfigPath: string;
nonInteractive: boolean;
export type InstallOptions = CommonOptions;
export type ReconfigureOptions = CommonOptions & {
dryRun: boolean;
apply: boolean;
skipFirewall: boolean;
skipStart: boolean;
};
export type RunMode = "install" | "reconfigure";
export type TlsMode = "acme" | "file" | "self_signed_dev";
export type RuntimeConfig = {
@@ -54,25 +52,31 @@ export type RuntimeConfig = {
hysteriaBandwidthDown: string;
hysteriaIgnoreClientBandwidth: boolean;
hysteriaConfigPath: string;
allowLatestHysteria: boolean;
hysteriaVersionPolicy: string;
installDir: string;
dataDir: string;
logDir: string;
bootstrapAdminSecretPath: string;
};
export type InstallContext = {
options: InstallOptions;
export type RuntimeContext = {
mode: RunMode;
options: CommonOptions;
config: RuntimeConfig;
packageVersion: string;
packageBuildId: string;
installDate: string;
hysteriaAuthPassword: string;
hysteriaVersion: string;
};
export type ReconfigureContext = {
options: ReconfigureOptions;
config: RuntimeConfig;
export type InstallContext = RuntimeContext & {
mode: "install";
options: InstallOptions;
hysteriaTargetVersion: string;
hysteriaArtifactUrl: string;
hysteriaArtifactSha256: string;
};
export type ReconfigureContext = RuntimeContext & {
mode: "reconfigure";
options: ReconfigureOptions;
};