Довёл fix20: firewall-mode, staged state, diagnostics, readiness и build-gate

This commit is contained in:
2026-05-07 23:38:55 +05:00
parent 0a8f4e0c3d
commit 4b382d6ef9
25 changed files with 840 additions and 133 deletions
+105 -44
View File
@@ -1,23 +1,44 @@
import type { RuntimeContext } from "../types/context";
import { exists, readText, renderTemplate, writeText } from "../lib/fs";
import { fileExists, readText, renderTemplate, writeText } from "../lib/fs";
import { fail, info } from "../lib/log";
import { runVisible } from "../lib/process";
const FW_BACKUP_FILES = [
"/etc/nftables.conf.hy2xs.bak",
"/etc/nftables.conf.candidate",
"/etc/nftables.d/hy2xs.nft.bak",
"/etc/nftables.d/hy2xs.nft.candidate",
"/etc/nftables.d/hy2xs.nft.existed",
"/etc/nftables.d/hy2xs.nft.include.existed",
"/etc/nftables.d/nftables.conf.existed",
"/etc/nftables.d/hy2xs.rollback.prepared"
];
type NftEntrypointKind =
| "missing"
| "hy2xs-managed"
| "empty"
| "debian-empty-template"
| "include-compatible"
| "foreign";
async function cleanupFirewallBackupFiles(): Promise<void> {
for (const file of FW_BACKUP_FILES) {
await runVisible`rm -f ${file}`;
}
export type FirewallEntrypointKind = NftEntrypointKind;
function rollbackRoot(opId: string): string {
return `/run/hy2xs/rollback/${opId}`;
}
function rollbackUnit(opId: string): string {
return `hy2xs-fw-rollback-${opId}`;
}
function operationKey(context: RuntimeContext): string {
return context.installDate.replace(/[^a-zA-Z0-9_.-]/g, "-");
}
function rollbackMarker(opId: string): string {
return `${rollbackRoot(opId)}/prepared`;
}
function rollbackBackup(path: string, opId: string): string {
return `${rollbackRoot(opId)}/${path}`;
}
async function ensureRollbackRoot(opId: string): Promise<void> {
await runVisible`mkdir -p ${rollbackRoot(opId)}`;
}
async function cleanupFirewallBackupFiles(opId: string): Promise<void> {
await runVisible`rm -rf ${rollbackRoot(opId)}`;
}
function stripNftComments(content: string): string {
@@ -28,28 +49,55 @@ function stripNftComments(content: string): string {
.join("\n");
}
function isSafeNftablesEntrypoint(content: string): boolean {
if (content.includes("HY2XS-MANAGED")) {
return true;
function classifyNftEntrypoint(content: string): NftEntrypointKind {
if (!content.trim()) {
return "missing";
}
const effective = stripNftComments(content)
if (content.includes("HY2XS-MANAGED")) {
return "hy2xs-managed";
}
const withoutComments = stripNftComments(content);
const effective = withoutComments
.replace(/^#!\/usr\/sbin\/nft\s+-f\s*/m, "")
.trim();
if (!effective) {
return true;
return "empty";
}
return effective === "flush ruleset";
const normalized = effective.replace(/\s+/g, " ").trim();
if (normalized === "flush ruleset") {
return "debian-empty-template";
}
if (/include\s+"\/etc\/nftables\.d\/hy2xs\.nft"/.test(effective)) {
return "include-compatible";
}
return "foreign";
}
export async function detectFirewallEntrypointKind(): Promise<FirewallEntrypointKind> {
if (!(await fileExists("/etc/nftables.conf"))) {
return "missing";
}
return classifyNftEntrypoint(await readText("/etc/nftables.conf"));
}
export async function applyFirewall(context: RuntimeContext): Promise<void> {
if (context.options.skipFirewall || !context.config.firewallEnabled) {
const opId = operationKey(context);
if (context.options.skipFirewall || context.config.firewallMode === "off") {
info("firewall skipped by flag");
return;
}
if (context.config.firewallMode === "external") {
info("firewall mode is external: nftables is not modified");
return;
}
const acmeChallengePort = context.config.acmeType === "tls" ? 443 : 80;
const acmeRule = context.config.tlsMode === "acme"
? `tcp dport ${acmeChallengePort} accept`
@@ -61,20 +109,28 @@ export async function applyFirewall(context: RuntimeContext): Promise<void> {
ACME_RULE: acmeRule
});
const existing = await exists("/etc/nftables.conf")
const existing = await fileExists("/etc/nftables.conf")
? await readText("/etc/nftables.conf")
: "";
if (existing && !isSafeNftablesEntrypoint(existing) && !context.config.firewallAllowTakeover) {
fail("existing non-HY2XS nftables.conf found; set HY2XS_FIREWALL_ALLOW_TAKEOVER=true or HY2XS_FIREWALL_ENABLED=false");
const entrypointKind = classifyNftEntrypoint(existing);
const managedAllowed = new Set<NftEntrypointKind>([
"missing",
"hy2xs-managed",
"empty",
"debian-empty-template",
"include-compatible"
]);
if (context.config.firewallMode === "managed" && !managedAllowed.has(entrypointKind)) {
fail("foreign nftables.conf detected; use HY2XS_FIREWALL_MODE=takeover|external|off");
}
await runVisible`touch /etc/nftables.d/hy2xs.rollback.prepared`;
await runVisible`cp -a /etc/nftables.conf /etc/nftables.conf.hy2xs.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/nftables.d/hy2xs.nft.bak 2>/dev/null || true`;
await runVisible`test -f /etc/nftables.conf && echo 1 > /etc/nftables.d/nftables.conf.existed || rm -f /etc/nftables.d/nftables.conf.existed`;
await runVisible`test -f /etc/nftables.d/hy2xs.nft && echo 1 > /etc/nftables.d/hy2xs.nft.existed || rm -f /etc/nftables.d/hy2xs.nft.existed`;
await runVisible`grep -q 'include "/etc/nftables.d/hy2xs.nft"' /etc/nftables.conf && echo 1 > /etc/nftables.d/hy2xs.nft.include.existed || rm -f /etc/nftables.d/hy2xs.nft.include.existed`;
await ensureRollbackRoot(opId);
await runVisible`touch ${rollbackMarker(opId)}`;
await runVisible`cp -a /etc/nftables.conf ${rollbackBackup("nftables.conf.bak", opId)} 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.d/hy2xs.nft ${rollbackBackup("hy2xs.nft.bak", opId)} 2>/dev/null || true`;
await runVisible`test -f /etc/nftables.conf && echo 1 > ${rollbackBackup("nftables.conf.existed", opId)} || rm -f ${rollbackBackup("nftables.conf.existed", opId)}`;
await runVisible`test -f /etc/nftables.d/hy2xs.nft && echo 1 > ${rollbackBackup("hy2xs.nft.existed", opId)} || rm -f ${rollbackBackup("hy2xs.nft.existed", opId)}`;
await writeText("/etc/nftables.d/hy2xs.nft.candidate", rendered, 0o600);
await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`;
@@ -103,7 +159,8 @@ include "/etc/nftables.d/hy2xs.nft"
await runVisible`nft -c -f /etc/nftables.conf`;
if (context.config.firewallStagedApply) {
await runVisible`systemd-run --unit hy2xs-fw-rollback --on-active=45s /bin/sh -c 'if [ -f /etc/nftables.d/hy2xs.rollback.prepared ]; then if [ -f /etc/nftables.d/nftables.conf.existed ]; then cp -a /etc/nftables.conf.hy2xs.bak /etc/nftables.conf 2>/dev/null || true; else rm -f /etc/nftables.conf; fi; if [ -f /etc/nftables.d/hy2xs.nft.existed ]; then cp -a /etc/nftables.d/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi; if [ -f /etc/nftables.d/nftables.conf.existed ]; then nft -f /etc/nftables.conf >/dev/null 2>&1 || true; else nft flush ruleset >/dev/null 2>&1 || true; fi; fi'`;
const unit = rollbackUnit(opId);
await runVisible`systemd-run --unit ${unit} --on-active=45s /bin/sh -c 'if [ -f ${rollbackMarker(opId)} ]; then if [ -f ${rollbackBackup("nftables.conf.existed", opId)} ]; then cp -a ${rollbackBackup("nftables.conf.bak", opId)} /etc/nftables.conf 2>/dev/null || true; else rm -f /etc/nftables.conf; fi; if [ -f ${rollbackBackup("hy2xs.nft.existed", opId)} ]; then cp -a ${rollbackBackup("hy2xs.nft.bak", opId)} /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi; if [ -f ${rollbackBackup("nftables.conf.existed", opId)} ]; then nft -f /etc/nftables.conf >/dev/null 2>&1 || true; else nft flush ruleset >/dev/null 2>&1 || true; fi; fi'`;
}
await runVisible`nft -f /etc/nftables.conf`;
@@ -115,35 +172,39 @@ include "/etc/nftables.d/hy2xs.nft"
}
export async function cancelFirewallRollback(context: RuntimeContext): Promise<void> {
if (!context.config.firewallEnabled || context.options.skipFirewall) {
const opId = operationKey(context);
if (context.options.skipFirewall || context.config.firewallMode === "off" || context.config.firewallMode === "external") {
return;
}
if (context.config.firewallStagedApply) {
await runVisible`systemctl stop hy2xs-fw-rollback || true`;
await runVisible`systemctl reset-failed hy2xs-fw-rollback || true`;
const unit = rollbackUnit(opId);
await runVisible`systemctl stop ${unit}.timer ${unit}.service || true`;
await runVisible`systemctl reset-failed ${unit}.timer ${unit}.service || true`;
}
await cleanupFirewallBackupFiles();
await cleanupFirewallBackupFiles(opId);
}
export async function rollbackFirewallNow(context: RuntimeContext): Promise<void> {
if (!context.config.firewallEnabled || context.options.skipFirewall) {
const opId = operationKey(context);
if (context.options.skipFirewall || context.config.firewallMode === "off" || context.config.firewallMode === "external") {
return;
}
if (!(await exists("/etc/nftables.d/hy2xs.rollback.prepared"))) {
if (!(await fileExists(rollbackMarker(opId)))) {
info("firewall rollback skipped: no HY2XS rollback markers found");
return;
}
if (context.config.firewallStagedApply) {
await runVisible`systemctl stop hy2xs-fw-rollback || true`;
await runVisible`systemctl reset-failed hy2xs-fw-rollback || true`;
const unit = rollbackUnit(opId);
await runVisible`systemctl stop ${unit}.timer ${unit}.service || true`;
await runVisible`systemctl reset-failed ${unit}.timer ${unit}.service || true`;
}
await runVisible`if [ -f /etc/nftables.d/nftables.conf.existed ]; then cp -a /etc/nftables.conf.hy2xs.bak /etc/nftables.conf 2>/dev/null || true; else rm -f /etc/nftables.conf; fi`;
await runVisible`if [ -f /etc/nftables.d/hy2xs.nft.existed ]; then cp -a /etc/nftables.d/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi`;
await runVisible`if [ -f /etc/nftables.d/nftables.conf.existed ]; then nft -f /etc/nftables.conf >/dev/null 2>&1 || true; else nft flush ruleset >/dev/null 2>&1 || true; fi`;
await cleanupFirewallBackupFiles();
await runVisible`if [ -f ${rollbackBackup("nftables.conf.existed", opId)} ]; then cp -a ${rollbackBackup("nftables.conf.bak", opId)} /etc/nftables.conf 2>/dev/null || true; else rm -f /etc/nftables.conf; fi`;
await runVisible`if [ -f ${rollbackBackup("hy2xs.nft.existed", opId)} ]; then cp -a ${rollbackBackup("hy2xs.nft.bak", opId)} /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi`;
await runVisible`if [ -f ${rollbackBackup("nftables.conf.existed", opId)} ]; then nft -f /etc/nftables.conf >/dev/null 2>&1 || true; else nft flush ruleset >/dev/null 2>&1 || true; fi`;
await cleanupFirewallBackupFiles(opId);
}