fix8: усилить rollback reconfigure, идемпотентность firewall и prod-hardening baseline

This commit is contained in:
2026-04-29 02:25:07 +05:00
parent 5455e41fc1
commit 0545990968
7 changed files with 65 additions and 121 deletions
+14 -113
View File
@@ -43,6 +43,13 @@
</el-form>
</div>
<el-alert
title="Managed by hy2xs-orchestrator reconfigure"
type="info"
:closable="false"
style="margin-bottom: 12px"
/>
<el-card shadow="never">
<el-form
ref="dataFormRef"
@@ -54,6 +61,7 @@
<el-input
v-model="dataForm.huiWebPort"
:placeholder="$t('config.huiWebPort')"
disabled
clearable
/>
</el-form-item>
@@ -61,6 +69,7 @@
<el-input
v-model="dataForm.huiWebContext"
:placeholder="$t('config.huiWebContext')"
disabled
clearable
/>
</el-form-item>
@@ -75,7 +84,7 @@
/>
</el-form-item>
<el-form-item :label="$t('config.huiHttps')" prop="huiHttps">
<el-select v-model="huiHttps" style="width: 50%" ref="huiHttpsRef">
<el-select v-model="huiHttps" style="width: 50%" ref="huiHttpsRef" disabled>
<el-option
v-for="item in huiHttpsList"
:key="item.key"
@@ -83,9 +92,6 @@
:value="item.value"
/>
</el-select>
<el-button v-if="huiHttps" @click="setCertPath"
>{{ t("config.useHysteria2Cert") }}
</el-button>
</el-form-item>
<el-form-item
v-if="huiHttps"
@@ -96,27 +102,9 @@
v-model="dataForm.huiCrtPath"
:placeholder="$t('config.huiCrtPath')"
style="width: 50%"
disabled
clearable
/>
<el-upload
style="height: 32px"
ref="uploadCrtFile"
action=""
:file-list="crtFileList"
:http-request="uploadCertFile"
accept=".crt"
:before-upload="
() => {
crtFileList = [];
}
"
:show-file-list="false"
:limit="1"
>
<template #trigger>
<el-button>{{ t("config.uploadCrtFile") }}</el-button>
</template>
</el-upload>
</el-form-item>
<el-form-item
v-if="huiHttps"
@@ -127,27 +115,9 @@
v-model="dataForm.huiKeyPath"
:placeholder="$t('config.huiKeyPath')"
style="width: 50%"
disabled
clearable
/>
<el-upload
style="height: 32px"
ref="uploadKeyFile"
action=""
:file-list="keyFileList"
:http-request="uploadCertFile"
accept=".key"
:before-upload="
() => {
keyFileList = [];
}
"
:show-file-list="false"
:limit="1"
>
<template #trigger>
<el-button>{{ t("config.uploadKeyFile") }}</el-button>
</template>
</el-upload>
</el-form-item>
<el-tooltip
:content="$t('config.resetTrafficCronTip')"
@@ -189,12 +159,10 @@ export default {
import { Select } from "@element-plus/icons-vue";
import {
exportConfigApi,
hysteria2AcmePathApi,
importConfigApi,
listConfigApi,
restartServerApi,
updateConfigsApi,
uploadCertFileApi,
} from "@/api/config";
import { ConfigsUpdateDto } from "@/api/config/types";
import {
@@ -204,7 +172,6 @@ import {
} from "element-plus/lib/components";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { UploadUserFile } from "element-plus";
const { t } = useI18n();
const route = useRoute();
@@ -278,50 +245,19 @@ const state = reactive({
resetTrafficCron: "",
},
huiHttps: 0,
fileList: [] as UploadUserFile[],
crtFileList: [] as UploadUserFile[],
keyFileList: [] as UploadUserFile[],
fileList: [] as UploadFile[],
});
const { dataForm, huiHttps, fileList, crtFileList, keyFileList } =
toRefs(state);
const { dataForm, huiHttps, fileList } = toRefs(state);
const submitForm = () => {
dataFormRef.value.validate((valid: boolean) => {
if (valid) {
if (state.huiHttps) {
if (!state.dataForm.huiCrtPath || !state.dataForm.huiKeyPath) {
ElMessage.error("crt and key required");
return;
}
}
if (!state.huiHttps) {
state.dataForm.huiCrtPath = "";
state.dataForm.huiKeyPath = "";
}
let configs: ConfigsUpdateDto[] = [
{
key: huiWebPortKey,
value: state.dataForm.huiWebPort,
},
{
key: huiWebContext,
value: state.dataForm.huiWebContext,
},
{
key: hysteria2TrafficTimeKey,
value: state.dataForm.hysteria2TrafficTime,
},
{
key: huiCrtPathKey,
value: state.dataForm.huiCrtPath,
},
{
key: huiKeyPathKey,
value: state.dataForm.huiKeyPath,
},
{
key: resetTrafficCronKey,
value: state.dataForm.resetTrafficCron,
@@ -415,41 +351,6 @@ const handleExport = async () => {
}
};
const setCertPath = async () => {
try {
const { data } = await hysteria2AcmePathApi();
const { crtPath, keyPath } = data;
state.dataForm.huiCrtPath = crtPath;
state.dataForm.huiKeyPath = keyPath;
} catch (e) {
/* empty */
}
};
const uploadCertFile = async (params: UploadRequestOptions) => {
try {
if (
!params.file.name.endsWith(".crt") &&
!params.file.name.endsWith(".key")
) {
ElMessage.error("file format not supported");
}
if (params.file.size > 1024 * 1024) {
ElMessage.error("the file is too big");
}
let formData = new FormData();
formData.append("file", params.file);
const { data } = await uploadCertFileApi(formData);
if (params.file.name.endsWith(".crt")) {
state.dataForm.huiCrtPath = data;
} else if (params.file.name.endsWith(".key")) {
state.dataForm.huiKeyPath = data;
}
} catch (e) {
/* empty */
}
};
const handleRestartServer = async () => {
try {
ElMessageBox.confirm("Are you sure to restart panel?", "Warning", {
@@ -71,8 +71,8 @@ project/
│ ├── branding/
│ ├── patches/
│ └── BUILD_NOTES.md
├── examples/
│ └── post-install.env.example
├── config/
│ └── hy2xs.env
└── dist/
└── hy2xs-install-<version>.tar.gz
@@ -30,9 +30,9 @@ HY2XS: runtime package layout
- package.build_id
- checksums.txt
7. docs/examples
7. docs/config
- короткий README по установке
- post-install.env.example
- canonical runtime source config (hy2xs.env)
Что не должно быть в runtime package
- builder/
+15 -1
View File
@@ -15,14 +15,28 @@ async function backupCurrentState(): Promise<void> {
await runVisible`cp -a /etc/hysteria/config.yaml /etc/hy2xs/backups/config.yaml.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/systemd/system/hy2xs-admin.service /etc/hy2xs/backups/hy2xs-admin.service.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/systemd/system/hysteria-server.service /etc/hy2xs/backups/hysteria-server.service.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/hy2xs.env /etc/hy2xs/backups/hy2xs.env.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/hysteria/post-install.env /etc/hy2xs/backups/post-install.env.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.conf /etc/hy2xs/backups/nftables.conf.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/hy2xs/backups/hy2xs.nft.bak 2>/dev/null || true`;
await runVisible`test -f /etc/hy2xs/hy2xs.env && echo 1 > /etc/hy2xs/backups/hy2xs.env.existed || rm -f /etc/hy2xs/backups/hy2xs.env.existed`;
await runVisible`test -f /etc/hysteria/post-install.env && echo 1 > /etc/hy2xs/backups/post-install.env.existed || rm -f /etc/hy2xs/backups/post-install.env.existed`;
await runVisible`test -f /etc/nftables.conf && echo 1 > /etc/hy2xs/backups/nftables.conf.existed || rm -f /etc/hy2xs/backups/nftables.conf.existed`;
await runVisible`test -f /etc/nftables.d/hy2xs.nft && echo 1 > /etc/hy2xs/backups/hy2xs.nft.existed || rm -f /etc/hy2xs/backups/hy2xs.nft.existed`;
}
async function rollbackCurrentState(): Promise<void> {
await runVisible`cp -a /etc/hy2xs/backups/config.yaml.bak /etc/hysteria/config.yaml 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/backups/hy2xs-admin.service.bak /etc/systemd/system/hy2xs-admin.service 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/backups/hysteria-server.service.bak /etc/systemd/system/hysteria-server.service 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/backups/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true`;
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/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`;
await runVisible`systemctl daemon-reload`;
await runVisible`systemctl restart hysteria-server hy2xs-admin || true`;
}
+9 -2
View File
@@ -29,12 +29,19 @@ export async function applyFirewall(context: InstallContext): Promise<void> {
await writeText("/etc/nftables.d/hy2xs.nft.candidate", rendered, 0o600);
await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`;
const nftablesConf = `#!/usr/sbin/nft -f
flush ruleset
include "/etc/nftables.d/hy2xs.nft"
`;
await writeText("/etc/nftables.conf", nftablesConf, 0o644);
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 'cp -a /etc/nftables.conf.hy2xs.bak /etc/nftables.conf 2>/dev/null || true; 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; nft -f /etc/nftables.conf >/dev/null 2>&1 || true'`;
await runVisible`systemd-run --unit hy2xs-fw-rollback --on-active=45s /bin/sh -c 'if [ -f /etc/nftables.conf.hy2xs.bak ]; then cp -a /etc/nftables.conf.hy2xs.bak /etc/nftables.conf 2>/dev/null || true; 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; nft -f /etc/nftables.conf >/dev/null 2>&1 || true'`;
}
await runVisible`mv /etc/nftables.d/hy2xs.nft.candidate /etc/nftables.d/hy2xs.nft`;
await runVisible`grep -q 'include "/etc/nftables.d/hy2xs.nft"' /etc/nftables.conf || printf '\ninclude "/etc/nftables.d/hy2xs.nft"\n' >> /etc/nftables.conf`;
await runVisible`nft -f /etc/nftables.conf`;
await runVisible`systemctl enable --now nftables`;
+1 -1
View File
@@ -30,7 +30,7 @@ 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_HYSTERIA_VERSION=v2.6.0
HY2XS_INSTALL_DIR=/opt/hy2xs-admin
HY2XS_DATA_DIR=/var/lib/hy2xs-admin
HY2XS_LOG_DIR=/var/log/hy2xs
+22
View File
@@ -6,6 +6,25 @@ BUN_REQUIRED="${BUN_REQUIRED:-1.1.45}"
NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}"
PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}"
TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}"
VERIFY_TOOLCHAIN_CHECKSUMS="${VERIFY_TOOLCHAIN_CHECKSUMS:-false}"
GO_ARCHIVE_SHA256="${GO_ARCHIVE_SHA256:-}"
NODE_ARCHIVE_SHA256="${NODE_ARCHIVE_SHA256:-}"
BUN_ARCHIVE_SHA256="${BUN_ARCHIVE_SHA256:-}"
verify_archive_sha256() {
local archive="$1"
local expected="$2"
local label="$3"
if [ "$VERIFY_TOOLCHAIN_CHECKSUMS" != "true" ]; then
return 0
fi
[ -n "$expected" ] || fail "missing expected SHA256 for $label (set ${label}_SHA256 env)"
local actual
actual="$(sha256sum "$archive" | awk '{print $1}')"
[ "$actual" = "$expected" ] || fail "$label sha256 mismatch: expected $expected, got $actual"
}
require_linux_debian12_amd64() {
[ "$(uname -s)" = "Linux" ] || fail "production builder supports only Linux Debian 12 amd64"
@@ -79,6 +98,7 @@ ensure_go() {
mkdir -p "$TOOLCHAIN_DIR/downloads"
local archive="$TOOLCHAIN_DIR/downloads/go${GO_REQUIRED}.linux-amd64.tar.gz"
download_file "https://go.dev/dl/go${GO_REQUIRED}.linux-amd64.tar.gz" "$archive"
verify_archive_sha256 "$archive" "$GO_ARCHIVE_SHA256" "GO_ARCHIVE"
rm -rf "$TOOLCHAIN_DIR/go"
tar -C "$TOOLCHAIN_DIR" -xzf "$archive"
GO_BIN="$managed"
@@ -102,6 +122,7 @@ ensure_bun() {
mkdir -p "$TOOLCHAIN_DIR/downloads" "$TOOLCHAIN_DIR/bun"
local archive="$TOOLCHAIN_DIR/downloads/bun-linux-x64-${BUN_REQUIRED}.zip"
download_file "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_REQUIRED}/bun-linux-x64.zip" "$archive"
verify_archive_sha256 "$archive" "$BUN_ARCHIVE_SHA256" "BUN_ARCHIVE"
rm -rf "$TOOLCHAIN_DIR/bun-tmp" "$TOOLCHAIN_DIR/bun"
mkdir -p "$TOOLCHAIN_DIR/bun-tmp"
unzip -q "$archive" -d "$TOOLCHAIN_DIR/bun-tmp"
@@ -131,6 +152,7 @@ ensure_node() {
mkdir -p "$TOOLCHAIN_DIR/downloads"
local archive="$TOOLCHAIN_DIR/downloads/node-v${NODE_REQUIRED}-linux-x64.tar.xz"
download_file "https://nodejs.org/dist/v${NODE_REQUIRED}/node-v${NODE_REQUIRED}-linux-x64.tar.xz" "$archive"
verify_archive_sha256 "$archive" "$NODE_ARCHIVE_SHA256" "NODE_ARCHIVE"
rm -rf "$TOOLCHAIN_DIR/node" "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64"
tar -C "$TOOLCHAIN_DIR" -xJf "$archive"
mv "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64" "$TOOLCHAIN_DIR/node"