Продакшн-фиксы install/reconfigure: rollback, firewall lifecycle, preflight и runbook
This commit is contained in:
@@ -3,9 +3,18 @@ import { exists, readText } from "../lib/fs";
|
||||
import { fail, info } from "../lib/log";
|
||||
import { run } from "../lib/process";
|
||||
|
||||
async function isPortBusy(port: number): Promise<boolean> {
|
||||
async function isTcpPortListening(port: number): Promise<boolean> {
|
||||
try {
|
||||
const output = await run`ss -H -lntu`;
|
||||
const output = await run`ss -H -ltn`;
|
||||
return output.split("\n").some((line) => line.includes(`:${port} `) || line.endsWith(`:${port}`));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isUdpPortListening(port: number): Promise<boolean> {
|
||||
try {
|
||||
const output = await run`ss -H -lun`;
|
||||
return output.split("\n").some((line) => line.includes(`:${port} `) || line.endsWith(`:${port}`));
|
||||
} catch {
|
||||
return false;
|
||||
@@ -28,6 +37,12 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
fail("installer must run as root");
|
||||
}
|
||||
|
||||
try {
|
||||
await run`command -v sudo >/dev/null 2>&1`;
|
||||
} catch {
|
||||
fail("sudo is required for installer smoke checks. Install it with: apt-get update && apt-get install -y sudo");
|
||||
}
|
||||
|
||||
const osRelease = await readText("/etc/os-release");
|
||||
if (!/^ID=debian$/m.test(osRelease) || !/^VERSION_ID="?12"?$/m.test(osRelease)) {
|
||||
fail("HY2XS baseline supports only clean Debian 12");
|
||||
@@ -67,6 +82,10 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
fail("HY2XS UI bind host must be IPv4-only");
|
||||
}
|
||||
|
||||
if (context.config.ipv6Enabled) {
|
||||
fail("HY2XS is IPv4-only: disable IPv6 in config (HY2XS_IPV6_ENABLED=false)");
|
||||
}
|
||||
|
||||
if (context.config.hysteriaBindHost !== "0.0.0.0") {
|
||||
fail("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 for production profile");
|
||||
}
|
||||
@@ -77,7 +96,7 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
|
||||
if (!isReconfigure && context.config.tlsMode === "acme") {
|
||||
const acmeChallengePort = context.config.acmeType === "http" ? 80 : 443;
|
||||
if (await isPortBusy(acmeChallengePort)) {
|
||||
if (await isTcpPortListening(acmeChallengePort)) {
|
||||
fail(`ACME ${context.config.acmeType}-challenge port is already in use: ${acmeChallengePort}`);
|
||||
}
|
||||
}
|
||||
@@ -101,24 +120,24 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const hysteriaPortBusy = await isPortBusy(context.config.hysteriaPort);
|
||||
const uiPortBusy = await isPortBusy(context.config.uiPort);
|
||||
const hysteriaUdpBusy = await isUdpPortListening(context.config.hysteriaPort);
|
||||
const uiTcpBusy = await isTcpPortListening(context.config.uiPort);
|
||||
|
||||
if (!isReconfigure) {
|
||||
if (hysteriaPortBusy) {
|
||||
fail(`Hysteria UDP/TCP port already appears to be in use: ${context.config.hysteriaPort}`);
|
||||
if (hysteriaUdpBusy) {
|
||||
fail(`Hysteria UDP port already appears to be in use: ${context.config.hysteriaPort}`);
|
||||
}
|
||||
if (uiPortBusy) {
|
||||
if (uiTcpBusy) {
|
||||
fail(`HY2XS admin port already appears to be in use: ${context.config.uiPort}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hysteriaPortBusy && !(await isUnitActive("hysteria-server"))) {
|
||||
if (hysteriaUdpBusy && !(await isUnitActive("hysteria-server"))) {
|
||||
fail(`Hysteria port ${context.config.hysteriaPort} is occupied by a non-HY2XS process`);
|
||||
}
|
||||
|
||||
if (uiPortBusy && !(await isUnitActive("hy2xs-admin"))) {
|
||||
if (uiTcpBusy && !(await isUnitActive("hy2xs-admin"))) {
|
||||
fail(`HY2XS admin port ${context.config.uiPort} is occupied by a non-HY2XS process`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user