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> </el-form>
</div> </div>
<el-alert
title="Managed by hy2xs-orchestrator reconfigure"
type="info"
:closable="false"
style="margin-bottom: 12px"
/>
<el-card shadow="never"> <el-card shadow="never">
<el-form <el-form
ref="dataFormRef" ref="dataFormRef"
@@ -54,6 +61,7 @@
<el-input <el-input
v-model="dataForm.huiWebPort" v-model="dataForm.huiWebPort"
:placeholder="$t('config.huiWebPort')" :placeholder="$t('config.huiWebPort')"
disabled
clearable clearable
/> />
</el-form-item> </el-form-item>
@@ -61,6 +69,7 @@
<el-input <el-input
v-model="dataForm.huiWebContext" v-model="dataForm.huiWebContext"
:placeholder="$t('config.huiWebContext')" :placeholder="$t('config.huiWebContext')"
disabled
clearable clearable
/> />
</el-form-item> </el-form-item>
@@ -75,7 +84,7 @@
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('config.huiHttps')" prop="huiHttps"> <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 <el-option
v-for="item in huiHttpsList" v-for="item in huiHttpsList"
:key="item.key" :key="item.key"
@@ -83,9 +92,6 @@
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<el-button v-if="huiHttps" @click="setCertPath"
>{{ t("config.useHysteria2Cert") }}
</el-button>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="huiHttps" v-if="huiHttps"
@@ -96,27 +102,9 @@
v-model="dataForm.huiCrtPath" v-model="dataForm.huiCrtPath"
:placeholder="$t('config.huiCrtPath')" :placeholder="$t('config.huiCrtPath')"
style="width: 50%" style="width: 50%"
disabled
clearable 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>
<el-form-item <el-form-item
v-if="huiHttps" v-if="huiHttps"
@@ -127,27 +115,9 @@
v-model="dataForm.huiKeyPath" v-model="dataForm.huiKeyPath"
:placeholder="$t('config.huiKeyPath')" :placeholder="$t('config.huiKeyPath')"
style="width: 50%" style="width: 50%"
disabled
clearable 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-form-item>
<el-tooltip <el-tooltip
:content="$t('config.resetTrafficCronTip')" :content="$t('config.resetTrafficCronTip')"
@@ -189,12 +159,10 @@ export default {
import { Select } from "@element-plus/icons-vue"; import { Select } from "@element-plus/icons-vue";
import { import {
exportConfigApi, exportConfigApi,
hysteria2AcmePathApi,
importConfigApi, importConfigApi,
listConfigApi, listConfigApi,
restartServerApi, restartServerApi,
updateConfigsApi, updateConfigsApi,
uploadCertFileApi,
} from "@/api/config"; } from "@/api/config";
import { ConfigsUpdateDto } from "@/api/config/types"; import { ConfigsUpdateDto } from "@/api/config/types";
import { import {
@@ -204,7 +172,6 @@ import {
} from "element-plus/lib/components"; } from "element-plus/lib/components";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { UploadUserFile } from "element-plus";
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@@ -278,50 +245,19 @@ const state = reactive({
resetTrafficCron: "", resetTrafficCron: "",
}, },
huiHttps: 0, huiHttps: 0,
fileList: [] as UploadUserFile[], fileList: [] as UploadFile[],
crtFileList: [] as UploadUserFile[],
keyFileList: [] as UploadUserFile[],
}); });
const { dataForm, huiHttps, fileList, crtFileList, keyFileList } = const { dataForm, huiHttps, fileList } = toRefs(state);
toRefs(state);
const submitForm = () => { const submitForm = () => {
dataFormRef.value.validate((valid: boolean) => { dataFormRef.value.validate((valid: boolean) => {
if (valid) { 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[] = [ let configs: ConfigsUpdateDto[] = [
{
key: huiWebPortKey,
value: state.dataForm.huiWebPort,
},
{
key: huiWebContext,
value: state.dataForm.huiWebContext,
},
{ {
key: hysteria2TrafficTimeKey, key: hysteria2TrafficTimeKey,
value: state.dataForm.hysteria2TrafficTime, value: state.dataForm.hysteria2TrafficTime,
}, },
{
key: huiCrtPathKey,
value: state.dataForm.huiCrtPath,
},
{
key: huiKeyPathKey,
value: state.dataForm.huiKeyPath,
},
{ {
key: resetTrafficCronKey, key: resetTrafficCronKey,
value: state.dataForm.resetTrafficCron, 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 () => { const handleRestartServer = async () => {
try { try {
ElMessageBox.confirm("Are you sure to restart panel?", "Warning", { ElMessageBox.confirm("Are you sure to restart panel?", "Warning", {
@@ -71,8 +71,8 @@ project/
│ ├── branding/ │ ├── branding/
│ ├── patches/ │ ├── patches/
│ └── BUILD_NOTES.md │ └── BUILD_NOTES.md
├── examples/ ├── config/
│ └── post-install.env.example │ └── hy2xs.env
└── dist/ └── dist/
└── hy2xs-install-<version>.tar.gz └── hy2xs-install-<version>.tar.gz
@@ -30,9 +30,9 @@ HY2XS: runtime package layout
- package.build_id - package.build_id
- checksums.txt - checksums.txt
7. docs/examples 7. docs/config
- короткий README по установке - короткий README по установке
- post-install.env.example - canonical runtime source config (hy2xs.env)
Что не должно быть в runtime package Что не должно быть в runtime package
- builder/ - 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/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/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/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`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> { 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/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/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/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 daemon-reload`;
await runVisible`systemctl restart hysteria-server hy2xs-admin || true`; 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 writeText("/etc/nftables.d/hy2xs.nft.candidate", rendered, 0o600);
await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`; 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) { 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`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`nft -f /etc/nftables.conf`;
await runVisible`systemctl enable --now nftables`; 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_BANDWIDTH_DOWN=50 mbps
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml 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_INSTALL_DIR=/opt/hy2xs-admin
HY2XS_DATA_DIR=/var/lib/hy2xs-admin HY2XS_DATA_DIR=/var/lib/hy2xs-admin
HY2XS_LOG_DIR=/var/log/hy2xs 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}" NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}"
PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}" PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}"
TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}" 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() { require_linux_debian12_amd64() {
[ "$(uname -s)" = "Linux" ] || fail "production builder supports only Linux Debian 12 amd64" [ "$(uname -s)" = "Linux" ] || fail "production builder supports only Linux Debian 12 amd64"
@@ -79,6 +98,7 @@ ensure_go() {
mkdir -p "$TOOLCHAIN_DIR/downloads" mkdir -p "$TOOLCHAIN_DIR/downloads"
local archive="$TOOLCHAIN_DIR/downloads/go${GO_REQUIRED}.linux-amd64.tar.gz" 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" 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" rm -rf "$TOOLCHAIN_DIR/go"
tar -C "$TOOLCHAIN_DIR" -xzf "$archive" tar -C "$TOOLCHAIN_DIR" -xzf "$archive"
GO_BIN="$managed" GO_BIN="$managed"
@@ -102,6 +122,7 @@ ensure_bun() {
mkdir -p "$TOOLCHAIN_DIR/downloads" "$TOOLCHAIN_DIR/bun" mkdir -p "$TOOLCHAIN_DIR/downloads" "$TOOLCHAIN_DIR/bun"
local archive="$TOOLCHAIN_DIR/downloads/bun-linux-x64-${BUN_REQUIRED}.zip" 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" 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" rm -rf "$TOOLCHAIN_DIR/bun-tmp" "$TOOLCHAIN_DIR/bun"
mkdir -p "$TOOLCHAIN_DIR/bun-tmp" mkdir -p "$TOOLCHAIN_DIR/bun-tmp"
unzip -q "$archive" -d "$TOOLCHAIN_DIR/bun-tmp" unzip -q "$archive" -d "$TOOLCHAIN_DIR/bun-tmp"
@@ -131,6 +152,7 @@ ensure_node() {
mkdir -p "$TOOLCHAIN_DIR/downloads" mkdir -p "$TOOLCHAIN_DIR/downloads"
local archive="$TOOLCHAIN_DIR/downloads/node-v${NODE_REQUIRED}-linux-x64.tar.xz" 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" 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" rm -rf "$TOOLCHAIN_DIR/node" "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64"
tar -C "$TOOLCHAIN_DIR" -xJf "$archive" tar -C "$TOOLCHAIN_DIR" -xJf "$archive"
mv "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64" "$TOOLCHAIN_DIR/node" mv "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64" "$TOOLCHAIN_DIR/node"