fix(runtime): закрыть fix2 P0 runtime pass без legacy

This commit is contained in:
2026-04-28 03:28:19 +05:00
parent e2901db6cd
commit 96d9bbcece
31 changed files with 264 additions and 631 deletions
+4 -2
View File
@@ -3,7 +3,7 @@ import type { InstallContext, InstallOptions } from "../types/context";
import { exists, readText, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
import { step } from "../lib/log";
import { defaultRuntimeConfig, parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
import { preflight } from "../steps/preflight";
import { installDeps } from "../steps/deps";
import { prepareFilesystem } from "../steps/filesystem";
@@ -29,7 +29,9 @@ function secret(): string {
export async function install(options: InstallOptions): Promise<void> {
const hasConfig = await exists(options.configPath);
const config = hasConfig ? parseRuntimeEnv(await readText(options.configPath)) : defaultRuntimeConfig();
const sourceConfigPath = hasConfig ? options.configPath : `${options.packageDir}/config/hy2xs.env`;
const sourceConfigRaw = await readText(sourceConfigPath);
const config = parseRuntimeEnv(sourceConfigRaw);
const context: InstallContext = {
options,
+10 -40
View File
@@ -83,6 +83,13 @@ function normalizeTlsMode(value: string): TlsMode {
throw new Error(`invalid HY2XS_TLS_MODE: ${value}`);
}
function normalizeAcmeType(value: string): "http" | "tls" | "dns" {
if (value === "http" || value === "tls" || value === "dns") {
return value;
}
throw new Error(`invalid HY2XS_ACME_TYPE: ${value}`);
}
export function parseRuntimeEnv(content: string): RuntimeConfig {
const env = parseEnv(content);
@@ -91,6 +98,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
const hysteriaPort = parsePort("HY2XS_HYSTERIA_PORT", env.HY2XS_HYSTERIA_PORT, 443);
const trafficStatsPort = parsePort("HY2XS_HYSTERIA_TRAFFIC_STATS_PORT", env.HY2XS_HYSTERIA_TRAFFIC_STATS_PORT, 36712);
const tlsMode = normalizeTlsMode(env.HY2XS_TLS_MODE || "acme");
const acmeType = normalizeAcmeType(env.HY2XS_ACME_TYPE || "http");
const config: RuntimeConfig = {
domain: env.HY2XS_DOMAIN || "",
@@ -106,6 +114,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
adminInitialPassword: env.HY2XS_ADMIN_INITIAL_PASSWORD || randomSecret(),
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
tlsMode,
acmeType,
acmeEmail: env.HY2XS_ACME_EMAIL || "",
tlsCertPath: env.HY2XS_TLS_CERT_PATH || "/etc/hysteria/server.crt",
tlsKeyPath: env.HY2XS_TLS_KEY_PATH || "/etc/hysteria/server.key",
@@ -140,46 +149,6 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
return config;
}
export function defaultRuntimeConfig(): RuntimeConfig {
return parseRuntimeEnv(
[
"HY2XS_IPV6_ENABLED=false",
"HY2XS_DOMAIN=",
"HY2XS_PUBLIC_HOST=127.0.0.1",
"HY2XS_PUBLIC_PORT=443",
"HY2XS_SSH_PORT=22",
"HY2XS_FIREWALL_ENABLED=true",
"HY2XS_FIREWALL_STAGED_APPLY=true",
"HY2XS_UI_BIND_HOST=127.0.0.1",
"HY2XS_UI_PORT=8080",
"HY2XS_ADMIN_USER=admin",
`HY2XS_ADMIN_INITIAL_PASSWORD=${randomSecret()}`,
"HY2XS_FORCE_PASSWORD_CHANGE=true",
"HY2XS_TLS_MODE=self_signed_dev",
"HY2XS_ACME_EMAIL=",
"HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt",
"HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key",
"HY2XS_HYSTERIA_BIND_HOST=0.0.0.0",
"HY2XS_HYSTERIA_PORT=443",
"HY2XS_HYSTERIA_AUTH_MODE=http",
"HY2XS_HYSTERIA_AUTH_URL=http://127.0.0.1:8080/hui/hysteria2/auth",
"HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1",
"HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712",
`HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=${randomSecret()}`,
"HY2XS_HYSTERIA_OBFS_TYPE=salamander",
`HY2XS_HYSTERIA_OBFS_PASSWORD=${randomSecret()}`,
"HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps",
"HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps",
"HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false",
"HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml",
"HY2XS_HYSTERIA_VERSION=latest",
"HY2XS_INSTALL_DIR=/opt/hy2xs-admin",
"HY2XS_DATA_DIR=/var/lib/hy2xs-admin",
"HY2XS_LOG_DIR=/var/log/hy2xs"
].join("\n")
);
}
export function validateRuntimeConfig(config: RuntimeConfig): void {
if (config.ipv6Enabled) {
throw new Error("HY2XS is IPv4-only: HY2XS_IPV6_ENABLED must be false");
@@ -228,6 +197,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string {
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
`HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`,
`HY2XS_TLS_MODE=${config.tlsMode}`,
`HY2XS_ACME_TYPE=${config.acmeType}`,
`HY2XS_ACME_EMAIL=${config.acmeEmail}`,
`HY2XS_TLS_CERT_PATH=${config.tlsCertPath}`,
`HY2XS_TLS_KEY_PATH=${config.tlsKeyPath}`,
+29
View File
@@ -48,3 +48,32 @@ export async function runVisible(command: TemplateStringsArray, ...args: unknown
throw new Error(`command failed (${exitCode}): ${rendered}`);
}
}
export async function runHidden(command: TemplateStringsArray, ...args: unknown[]): Promise<void> {
const rendered = renderCommand(command, args);
const process = Bun.spawn(["sh", "-c", rendered], {
stdout: "inherit",
stderr: "inherit"
});
const exitCode = await process.exited;
if (exitCode !== 0) {
throw new Error(`command failed (${exitCode})`);
}
}
export async function runSecret(command: TemplateStringsArray, ...args: unknown[]): Promise<string> {
const rendered = renderCommand(command, args);
const process = Bun.spawn(["sh", "-c", rendered], {
stdout: "pipe",
stderr: "pipe"
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(process.stdout).text(),
new Response(process.stderr).text(),
process.exited
]);
if (exitCode !== 0) {
throw new Error(`command failed (${exitCode}): ${stderr.trim()}`);
}
return stdout.trim();
}
+7 -2
View File
@@ -9,11 +9,17 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
return;
}
const acmeChallengePort = context.config.acmeType === "tls" ? 443 : 80;
const acmeRule = context.config.tlsMode === "acme"
? `tcp dport ${acmeChallengePort} accept`
: "# acme challenge port disabled";
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
SSH_PORT: context.config.sshPort,
HYSTERIA_PORT: context.config.hysteriaPort,
UI_PORT: context.config.uiPort,
UI_BIND_HOST: context.config.uiBindHost
UI_BIND_HOST: context.config.uiBindHost,
ACME_RULE: acmeRule
});
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/nftables.d/hy2xs.nft.bak 2>/dev/null || true`;
@@ -30,7 +36,6 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
await runVisible`systemctl enable --now nftables`;
await runVisible`ss -H -ltn | grep -q ':${context.config.sshPort} ' || (echo 'ssh port check failed' >&2; exit 1)`;
await runVisible`ss -H -lun | grep -q ':${context.config.hysteriaPort} ' || (echo 'hysteria udp port check failed' >&2; exit 1)`;
if (context.config.firewallStagedApply) {
await runVisible`systemctl stop hy2xs-fw-rollback || true`;
+1 -1
View File
@@ -31,7 +31,7 @@ export async function installHysteria(context: InstallContext): Promise<void> {
if (policy === "latest") {
await runVisible`bash ${scriptPath}`;
} else {
await runVisible`HYSTERIA_VERSION=${policy} bash ${scriptPath}`;
await runVisible`bash ${scriptPath} --version ${policy}`;
}
await runVisible`test -x /usr/local/bin/hysteria`;
+7 -4
View File
@@ -1,6 +1,6 @@
import type { InstallContext } from "../types/context";
import { info } from "../lib/log";
import { runVisible } from "../lib/process";
import { runHidden, runSecret, runVisible } from "../lib/process";
export async function smoke(context: InstallContext): Promise<void> {
if (context.options.skipStart) {
@@ -26,10 +26,13 @@ export async function smoke(context: InstallContext): Promise<void> {
}
await runVisible`ss -H -lun | grep -q '0.0.0.0:${context.config.hysteriaPort} '`;
await runVisible`! ss -H -ltnu | grep -q '\[::\]'`;
await runVisible`curl -fsS --max-time 5 http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth >/dev/null`;
await runHidden`curl -fsS --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 >/dev/null`;
await runVisible`curl -fsS --max-time 5 -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online >/dev/null`;
await runVisible`curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online | grep -Eq '401|403'`;
await runHidden`curl -fsS --max-time 5 -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online >/dev/null`;
const deniedCode = await runSecret`curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`;
if (!/(401|403)/.test(deniedCode)) {
throw new Error(`unexpected trafficStats status for invalid secret: ${deniedCode}`);
}
await runVisible`nft -c -f /etc/nftables.conf`;
+1
View File
@@ -32,6 +32,7 @@ export type RuntimeConfig = {
adminInitialPassword: string;
forcePasswordChange: boolean;
tlsMode: TlsMode;
acmeType: "http" | "tls" | "dns";
acmeEmail: string;
tlsCertPath: string;
tlsKeyPath: string;