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

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
+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,