feat(v1): Gecko-обфускация, latest-stable Hysteria на сборке и forward-compatible admin
Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1. Build: - версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и замораживается в metadata пакета (version + immutable url + sha256); - compatibility gate: реальный бинарник должен принять канонический конфиг HY2XS для gecko и salamander до создания пакета; - сборка прогоняет тесты оркестратора и админки. Конфигурационный контракт: - HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast; - obfs стал настоящим union gecko|salamander, gecko — default; - obfs-блок рендерится оркестратором целиком, два подтипа одновременно структурно невозможны; - современный baseline: congestion bbr/standard, disableLossCompensation=false, disableStatelessReset=false, полный quic-блок. Исправления: - share URI для gecko: генератор был завязан на Obfs.Salamander.Password и выдавал нерабочую ссылку при любой другой обфускации; - SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file; - экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль; - экспорт терял неизвестные upstream-поля при round-trip через типизированную модель; - renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом; - namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0). Тесты: - 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover; - тесты URI и экспорта в Go; - tools/test/e2e-hysteria.sh с реальным клиентом Hysteria. UX: - подсказки и примеры в форме создания пира. Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов), документация на русском.
This commit is contained in:
@@ -361,21 +361,69 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="dataForm.obfs.type === 'salamander'"
|
||||
:content="$t('hysteria.config.obfs.salamander.password')"
|
||||
placement="bottom"
|
||||
<template
|
||||
v-if="
|
||||
dataForm.obfs.type === 'salamander' && dataForm.obfs.salamander
|
||||
"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.salamander.password"
|
||||
prop="obfs.salamander.password"
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.salamander.password')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.obfs.salamander.password"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-form-item
|
||||
label="obfs.salamander.password"
|
||||
prop="obfs.salamander.password"
|
||||
>
|
||||
<el-input
|
||||
v-model="dataForm.obfs.salamander.password"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template
|
||||
v-if="dataForm.obfs.type === 'gecko' && dataForm.obfs.gecko"
|
||||
>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.gecko.password')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.gecko.password"
|
||||
prop="obfs.gecko.password"
|
||||
>
|
||||
<el-input v-model="dataForm.obfs.gecko.password" clearable />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.gecko.minPacketSize')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.gecko.minPacketSize"
|
||||
prop="obfs.gecko.minPacketSize"
|
||||
>
|
||||
<el-input
|
||||
v-model.number="dataForm.obfs.gecko.minPacketSize"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.obfs.gecko.maxPacketSize')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="obfs.gecko.maxPacketSize"
|
||||
prop="obfs.gecko.maxPacketSize"
|
||||
>
|
||||
<el-input
|
||||
v-model.number="dataForm.obfs.gecko.maxPacketSize"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('hysteria.quic')" name="quic" v-if="quic">
|
||||
<el-tooltip
|
||||
@@ -470,6 +518,17 @@
|
||||
<el-switch v-model="dataForm.quic.disablePathMTUDiscovery" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.quic.disableStatelessReset')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="quic.disableStatelessReset"
|
||||
prop="quic.disableStatelessReset"
|
||||
>
|
||||
<el-switch v-model="dataForm.quic.disableStatelessReset" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
:label="$t('hysteria.bandwidth')"
|
||||
@@ -494,6 +553,19 @@
|
||||
<el-input v-model="dataForm.bandwidth.down" clearable />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.bandwidth.disableLossCompensation')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="bandwidth.disableLossCompensation"
|
||||
prop="bandwidth.disableLossCompensation"
|
||||
>
|
||||
<el-switch
|
||||
v-model="dataForm.bandwidth.disableLossCompensation"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.ignoreClientBandwidth')"
|
||||
placement="bottom"
|
||||
@@ -506,6 +578,53 @@
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
:label="$t('hysteria.congestion')"
|
||||
name="congestion"
|
||||
v-if="congestion && dataForm.congestion"
|
||||
>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.congestion.type')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item label="congestion.type" prop="congestion.type">
|
||||
<el-select
|
||||
v-model="dataForm.congestion.type"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in congestionTypes"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.congestion.bbrProfile')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item
|
||||
label="congestion.bbrProfile"
|
||||
prop="congestion.bbrProfile"
|
||||
>
|
||||
<el-select
|
||||
v-model="dataForm.congestion.bbrProfile"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in bbrProfiles"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
:label="$t('hysteria.speedTest')"
|
||||
name="speedTest"
|
||||
@@ -1020,15 +1139,22 @@ const tlsSniGuards = ref<string[]>(["dns-san", "disable", "strict"]);
|
||||
const aclTypes = ref<string[]>(["file", "inline"]);
|
||||
const acmeCas = ref<string[]>(["zerossl", "letsencrypt"]);
|
||||
const acmeTypes = ref<string[]>(["http", "tls", "dns"]);
|
||||
// Актуальный upstream-реестр ACME DNS-провайдеров.
|
||||
// `namedotcom` удалён в Hysteria 2.11.0 вместе с переписанным ACME-стеком:
|
||||
// конфигурация с ним больше не запускается.
|
||||
const dnsNames = ref<string[]>([
|
||||
"cloudflare",
|
||||
"duckdns",
|
||||
"gandi",
|
||||
"godaddy",
|
||||
"namedotcom",
|
||||
"namecheap",
|
||||
"njalla",
|
||||
"porkbun",
|
||||
"vultr",
|
||||
]);
|
||||
const obfsTypes = ref<string[]>(["salamander"]);
|
||||
const obfsTypes = ref<string[]>(["gecko", "salamander"]);
|
||||
const congestionTypes = ref<string[]>(["bbr", "reno"]);
|
||||
const bbrProfiles = ref<string[]>(["standard", "conservative", "aggressive"]);
|
||||
const resolverTypes = ref<string[]>(["tcp", "udp", "tls", "https"]);
|
||||
const masqueradeTypes = ref<string[]>(["file", "proxy", "string"]);
|
||||
|
||||
@@ -1043,6 +1169,7 @@ const state = reactive({
|
||||
obfs: false,
|
||||
quic: false,
|
||||
bandwidth: false,
|
||||
congestion: false,
|
||||
speedTest: false,
|
||||
udp: false,
|
||||
resolver: false,
|
||||
@@ -1067,6 +1194,7 @@ const {
|
||||
obfs,
|
||||
quic,
|
||||
bandwidth,
|
||||
congestion,
|
||||
speedTest,
|
||||
udp,
|
||||
resolver,
|
||||
@@ -1120,6 +1248,7 @@ const setConfig = () => {
|
||||
state.obfs = !!data?.obfs;
|
||||
state.quic = !!data?.quic;
|
||||
state.bandwidth = !!data?.bandwidth;
|
||||
state.congestion = !!data?.congestion;
|
||||
state.speedTest = !!data?.speedTest;
|
||||
state.udp = !!data?.disableUDP || !!data?.udpIdleTimeout;
|
||||
state.resolver = !!data?.resolver;
|
||||
@@ -1130,10 +1259,28 @@ const setConfig = () => {
|
||||
|
||||
state.dataForm = deepCopy(defaultHysteria2ServerConfig);
|
||||
assignWith(state.dataForm, data);
|
||||
dropInactiveObfsSubtype(state.dataForm, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Форма строится как merge дефолта и ответа API, поэтому неактивная ветка obfs
|
||||
// осталась бы от дефолта и UI показал бы блок, которого нет в конфиге сервера.
|
||||
const dropInactiveObfsSubtype = (
|
||||
form: Hysteria2ServerConfig,
|
||||
data: Hysteria2ServerConfig
|
||||
) => {
|
||||
if (!form.obfs) {
|
||||
return;
|
||||
}
|
||||
if (form.obfs.type !== "gecko" || !data?.obfs?.gecko) {
|
||||
delete form.obfs.gecko;
|
||||
}
|
||||
if (form.obfs.type !== "salamander" || !data?.obfs?.salamander) {
|
||||
delete form.obfs.salamander;
|
||||
}
|
||||
};
|
||||
|
||||
const setHysteria2Monitor = async () => {
|
||||
const { data } = await dashboardSummaryApi();
|
||||
state.hysteria2Monitor.version = data.hysteria.version;
|
||||
|
||||
@@ -65,9 +65,7 @@
|
||||
<el-table-column :label="$t('peer.traffic')" min-width="260">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{
|
||||
formatBytes(scope.row.downloadBytes + scope.row.uploadBytes)
|
||||
}}
|
||||
{{ formatBytes(scope.row.downloadBytes + scope.row.uploadBytes) }}
|
||||
/ {{ quotaText(scope.row.quotaBytes) }}
|
||||
</div>
|
||||
<el-progress
|
||||
@@ -167,18 +165,32 @@
|
||||
:rules="rules"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item :label="$t('peer.name')" prop="name"
|
||||
><el-input v-model="dataForm.name"
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.remark')"
|
||||
><el-input v-model="dataForm.remark"
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.secret')" prop="secret"
|
||||
><el-input v-model="dataForm.secret" show-password
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.quota')"
|
||||
><el-input-number v-model="dataForm.quotaBytes" :min="-1"
|
||||
/></el-form-item>
|
||||
<el-form-item :label="$t('peer.name')" prop="name">
|
||||
<el-input
|
||||
v-model="dataForm.name"
|
||||
:placeholder="$t('peer.form.namePlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.nameHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.remark')">
|
||||
<el-input
|
||||
v-model="dataForm.remark"
|
||||
:placeholder="$t('peer.form.remarkPlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.remarkHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.secret')" prop="secret">
|
||||
<el-input
|
||||
v-model="dataForm.secret"
|
||||
show-password
|
||||
:placeholder="$t('peer.form.secretPlaceholder')"
|
||||
/>
|
||||
<div class="form-hint">{{ $t("peer.form.secretHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.quota')">
|
||||
<el-input-number v-model="dataForm.quotaBytes" :min="-1" />
|
||||
<div class="form-hint">{{ $t("peer.form.quotaHint") }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('peer.expireTime')"
|
||||
><el-date-picker
|
||||
v-model="dataForm.expiresAt"
|
||||
@@ -573,23 +585,35 @@ onMounted(handleQuery);
|
||||
.peer-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.peer-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.peer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.peer-more-btn {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.qr-dialog-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 0 20px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user