Полный production-фикс fix30/fix30.1: auth, smoke, bootstrap, preflight, rollback и валидация PATCH
This commit is contained in:
@@ -177,13 +177,16 @@ export async function install(options: InstallOptions): Promise<void> {
|
||||
await advanceInstallState(context, "installing");
|
||||
state.lastPhase = "installing";
|
||||
step("preflight");
|
||||
await preflight(context);
|
||||
await preflight(context, { requireCapabilities: false });
|
||||
stepDone("preflight");
|
||||
await advanceInstallState(context, "preflight_ok");
|
||||
state.lastPhase = "preflight_ok";
|
||||
step("system dependencies");
|
||||
await installDeps(context);
|
||||
stepDone("system dependencies");
|
||||
step("preflight capabilities");
|
||||
await preflight(context, { requireCapabilities: true });
|
||||
stepDone("preflight capabilities");
|
||||
await advanceInstallState(context, "deps_ok");
|
||||
state.lastPhase = "deps_ok";
|
||||
step("filesystem");
|
||||
|
||||
@@ -100,7 +100,7 @@ async function rollbackCurrentState(): Promise<void> {
|
||||
|
||||
await runVisible`if [ -f /etc/hy2xs/backups/hy2xs.env.existed ]; then cp -a /etc/hy2xs/backups/hy2xs.env.bak /etc/hy2xs/hy2xs.env 2>/dev/null || true; else rm -f /etc/hy2xs/hy2xs.env; fi`;
|
||||
await runVisible`if [ -f /etc/hy2xs/backups/post-install.env.existed ]; then cp -a /etc/hy2xs/backups/post-install.env.bak /etc/hysteria/post-install.env 2>/dev/null || true; else rm -f /etc/hysteria/post-install.env; fi`;
|
||||
await runVisible`if [ -f /etc/hy2xs/backups/nftables.conf.existed ]; then cp -a /etc/hy2xs/backups/nftables.conf.bak /etc/nftables.conf 2>/dev/null || true; fi`;
|
||||
await runVisible`if [ -f /etc/hy2xs/backups/nftables.conf.existed ]; then cp -a /etc/hy2xs/backups/nftables.conf.bak /etc/nftables.conf 2>/dev/null || true; else rm -f /etc/nftables.conf; fi`;
|
||||
await runVisible`if [ -f /etc/hy2xs/backups/hy2xs.nft.existed ]; then cp -a /etc/hy2xs/backups/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi`;
|
||||
|
||||
await runVisible`nft -f /etc/nftables.conf >/dev/null 2>&1 || true`;
|
||||
|
||||
@@ -5,6 +5,9 @@ type AssertPlatformOptions = {
|
||||
distro: "debian";
|
||||
supportedVersions: number[];
|
||||
architectures: Array<"amd64">;
|
||||
requireSystemdRun?: boolean;
|
||||
requireNftables?: boolean;
|
||||
requireOpenSsl3?: boolean;
|
||||
};
|
||||
|
||||
export async function assertPlatform(options: AssertPlatformOptions): Promise<void> {
|
||||
@@ -29,16 +32,16 @@ export async function assertPlatform(options: AssertPlatformOptions): Promise<vo
|
||||
`required capability missing: systemd (${profile.capabilityDetails.systemdReason}; pid1=${profile.capabilityDetails.pid1}; state=${profile.capabilityDetails.systemdState})`
|
||||
);
|
||||
}
|
||||
if (!profile.capabilities.systemdRun) {
|
||||
if (options.requireSystemdRun !== false && !profile.capabilities.systemdRun) {
|
||||
fail("required capability missing: systemd-run");
|
||||
}
|
||||
if (!profile.capabilities.nftables) {
|
||||
if (options.requireNftables !== false && !profile.capabilities.nftables) {
|
||||
fail("required capability missing: nft");
|
||||
}
|
||||
if (!profile.capabilities.nftAtomicReplace) {
|
||||
if (options.requireNftables !== false && !profile.capabilities.nftAtomicReplace) {
|
||||
fail("required capability missing: nft atomic replace");
|
||||
}
|
||||
if (!profile.capabilities.openssl3) {
|
||||
if (options.requireOpenSsl3 !== false && !profile.capabilities.openssl3) {
|
||||
fail("required capability missing: OpenSSL 3.x runtime");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { fail, info } from "../lib/log";
|
||||
import { run } from "../lib/process";
|
||||
import { assertPlatform } from "../platform/assert";
|
||||
|
||||
type PreflightOptions = {
|
||||
requireCapabilities?: boolean;
|
||||
};
|
||||
|
||||
function isNoDnsRecords(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
@@ -41,8 +45,13 @@ async function isUnitActive(unit: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
export async function preflight(context: RuntimeContext, options?: PreflightOptions): Promise<void> {
|
||||
const isReconfigure = context.mode === "reconfigure";
|
||||
const requireCapabilities = options?.requireCapabilities ?? true;
|
||||
|
||||
const needsFirewallCapabilities = !context.options.skipFirewall &&
|
||||
context.config.firewallMode !== "off" &&
|
||||
context.config.firewallMode !== "external";
|
||||
|
||||
if (process.getuid?.() !== 0) {
|
||||
fail("installer must run as root");
|
||||
@@ -51,7 +60,10 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
await assertPlatform({
|
||||
distro: "debian",
|
||||
supportedVersions: [13],
|
||||
architectures: ["amd64"]
|
||||
architectures: ["amd64"],
|
||||
requireSystemdRun: requireCapabilities,
|
||||
requireNftables: requireCapabilities && needsFirewallCapabilities,
|
||||
requireOpenSsl3: requireCapabilities
|
||||
});
|
||||
|
||||
if (!(await fileExists(`${context.options.packageDir}/systemd/hy2xs-admin.service`))) {
|
||||
|
||||
@@ -144,23 +144,27 @@ export async function smoke(context: RuntimeContext): Promise<void> {
|
||||
}
|
||||
await runVisible`! ss -H -ltn | grep -q '\[::\]:${context.config.uiPort} '`;
|
||||
await runVisible`! ss -H -lun | grep -q '\[::\]:${context.config.hysteriaPort} '`;
|
||||
const missingTokenAuthCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -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 (missingTokenAuthCode.trim() !== "403") {
|
||||
throw new Error(`unexpected auth status without machine token: ${missingTokenAuthCode}`);
|
||||
}
|
||||
const invalidAuthResponse = await retry(
|
||||
"auth invalid credentials",
|
||||
5,
|
||||
1000,
|
||||
async () => 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`,
|
||||
async () => 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?access_token=${context.config.hysteriaTrafficStatsSecret}'`,
|
||||
(response) => /"ok"\s*:\s*false/.test(response),
|
||||
(response, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`),
|
||||
);
|
||||
|
||||
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`;
|
||||
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?access_token=${context.config.hysteriaTrafficStatsSecret}'`;
|
||||
if (!/"ok"\s*:\s*false/.test(response)) {
|
||||
throw new Error(`unexpected auth response during rate-limit smoke: ${response}`);
|
||||
}
|
||||
}
|
||||
|
||||
const invalidTypeAuthCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -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`;
|
||||
const invalidTypeAuthCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -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?access_token=${context.config.hysteriaTrafficStatsSecret}'`;
|
||||
if (invalidTypeAuthCode.trim() !== "400") {
|
||||
throw new Error(`unexpected auth status for tx as string: ${invalidTypeAuthCode}`);
|
||||
}
|
||||
@@ -175,7 +179,7 @@ export async function smoke(context: RuntimeContext): Promise<void> {
|
||||
"auth valid credentials",
|
||||
10,
|
||||
1000,
|
||||
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":0}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
|
||||
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":0}' 'http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth?access_token=${context.config.hysteriaTrafficStatsSecret}'`,
|
||||
(response) => /"ok"\s*:\s*true/.test(response),
|
||||
(response, error) => new Error(`unexpected auth response for valid credentials: ${response ?? String(error)}`),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user