Полный продовый фикс HY2XS/Hysteria2: auth DTO, валидация bind, smoke, URI, preflight и env-контракт
This commit is contained in:
@@ -26,12 +26,17 @@ func validateStr(f validator.FieldLevel) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateField[T interface{}](c *gin.Context, field T) (T, error) {
|
func validateField[T interface{}](c *gin.Context, field T) (T, error) {
|
||||||
|
var bindErr error
|
||||||
if c.Request.Method == http.MethodGet {
|
if c.Request.Method == http.MethodGet {
|
||||||
_ = c.ShouldBindQuery(&field)
|
bindErr = c.ShouldBindQuery(&field)
|
||||||
} else if c.Request.Method == http.MethodPost ||
|
} else if c.Request.Method == http.MethodPost ||
|
||||||
c.Request.Method == http.MethodPut ||
|
c.Request.Method == http.MethodPut ||
|
||||||
c.Request.Method == http.MethodDelete {
|
c.Request.Method == http.MethodDelete {
|
||||||
_ = c.ShouldBindJSON(&field)
|
bindErr = c.ShouldBindJSON(&field)
|
||||||
|
}
|
||||||
|
if bindErr != nil {
|
||||||
|
vo.Fail(constant.InvalidError, c)
|
||||||
|
return field, fmt.Errorf(constant.InvalidError)
|
||||||
}
|
}
|
||||||
if err := validate.Struct(&field); err != nil {
|
if err := validate.Struct(&field); err != nil {
|
||||||
vo.Fail(constant.InvalidError, c)
|
vo.Fail(constant.InvalidError, c)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package dto
|
|||||||
type Hysteria2AuthDto struct {
|
type Hysteria2AuthDto struct {
|
||||||
Addr *string `json:"addr" form:"addr" validate:"required"`
|
Addr *string `json:"addr" form:"addr" validate:"required"`
|
||||||
Auth *string `json:"auth" form:"auth" validate:"required"`
|
Auth *string `json:"auth" form:"auth" validate:"required"`
|
||||||
Tx *string `json:"tx" form:"tx" validate:"required"`
|
Tx *int64 `json:"tx" form:"tx" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hysteria2KickDto struct {
|
type Hysteria2KickDto struct {
|
||||||
|
|||||||
@@ -270,19 +270,10 @@ func Hysteria2Url(accountId int64) (string, error) {
|
|||||||
hysteria2Config.ACME.Domains != nil &&
|
hysteria2Config.ACME.Domains != nil &&
|
||||||
len(hysteria2Config.ACME.Domains) > 0 {
|
len(hysteria2Config.ACME.Domains) > 0 {
|
||||||
urlConfig += fmt.Sprintf("&sni=%s", hysteria2Config.ACME.Domains[0])
|
urlConfig += fmt.Sprintf("&sni=%s", hysteria2Config.ACME.Domains[0])
|
||||||
// shadowrocket
|
|
||||||
urlConfig += fmt.Sprintf("&peer=%s", hysteria2Config.ACME.Domains[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
urlConfig += "&insecure=0"
|
urlConfig += "&insecure=0"
|
||||||
|
|
||||||
if hysteria2Config.Bandwidth != nil &&
|
|
||||||
hysteria2Config.Bandwidth.Down != nil &&
|
|
||||||
*hysteria2Config.Bandwidth.Down != "" {
|
|
||||||
// shadowrocket
|
|
||||||
urlConfig += fmt.Sprintf("&downmbps=%s", url.PathEscape(*hysteria2Config.Bandwidth.Down))
|
|
||||||
}
|
|
||||||
|
|
||||||
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ Hysteria2 — основной транспортный компонент се
|
|||||||
- `acme` -> только `acme` block в конфиге;
|
- `acme` -> только `acme` block в конфиге;
|
||||||
- `acme` block обязан содержать `type: http|tls` из runtime env (`HY2XS_ACME_TYPE`);
|
- `acme` block обязан содержать `type: http|tls` из runtime env (`HY2XS_ACME_TYPE`);
|
||||||
- `HY2XS_ACME_TYPE=dns` в production-профиле запрещён до отдельной реализации;
|
- `HY2XS_ACME_TYPE=dns` в production-профиле запрещён до отдельной реализации;
|
||||||
|
- `HY2XS_HYSTERIA_AUTH_MODE` зафиксирован в `http` и валидируется fail-fast;
|
||||||
|
- `HY2XS_HYSTERIA_OBFS_TYPE` зафиксирован в `salamander` и валидируется fail-fast;
|
||||||
|
- блок `masquerade` в baseline не задаётся (допустимо, но приводит к `404 Not Found` на обычный HTTP трафик);
|
||||||
- `file` -> только `tls.cert`/`tls.key` block;
|
- `file` -> только `tls.cert`/`tls.key` block;
|
||||||
- `self_signed_dev` -> только dev сценарии.
|
- `self_signed_dev` -> только dev сценарии.
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,22 @@ function normalizeAcmeType(value: string): "http" | "tls" | "dns" {
|
|||||||
throw new Error(`invalid HY2XS_ACME_TYPE: ${value}`);
|
throw new Error(`invalid HY2XS_ACME_TYPE: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFixedHysteriaAuthMode(value: string | undefined): "http" {
|
||||||
|
const mode = value || "http";
|
||||||
|
if (mode !== "http") {
|
||||||
|
throw new Error("HY2XS_HYSTERIA_AUTH_MODE is fixed in HY2XS production profile: http");
|
||||||
|
}
|
||||||
|
return "http";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFixedHysteriaObfsType(value: string | undefined): "salamander" {
|
||||||
|
const obfsType = value || "salamander";
|
||||||
|
if (obfsType !== "salamander") {
|
||||||
|
throw new Error("HY2XS_HYSTERIA_OBFS_TYPE is fixed in HY2XS production profile: salamander");
|
||||||
|
}
|
||||||
|
return "salamander";
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSafeAbsolutePath(name: string, value: string, options?: { disallowTmp?: boolean }): string {
|
function normalizeSafeAbsolutePath(name: string, value: string, options?: { disallowTmp?: boolean }): string {
|
||||||
const v = value.trim();
|
const v = value.trim();
|
||||||
if (!v.startsWith("/")) {
|
if (!v.startsWith("/")) {
|
||||||
@@ -157,6 +173,8 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
const tlsMode = normalizeTlsMode(env.HY2XS_TLS_MODE || "acme");
|
const tlsMode = normalizeTlsMode(env.HY2XS_TLS_MODE || "acme");
|
||||||
const acmeType = normalizeAcmeType(env.HY2XS_ACME_TYPE || "http");
|
const acmeType = normalizeAcmeType(env.HY2XS_ACME_TYPE || "http");
|
||||||
const firewallMode = normalizeFirewallMode(env);
|
const firewallMode = normalizeFirewallMode(env);
|
||||||
|
const hysteriaAuthMode = normalizeFixedHysteriaAuthMode(env.HY2XS_HYSTERIA_AUTH_MODE);
|
||||||
|
const hysteriaObfsType = normalizeFixedHysteriaObfsType(env.HY2XS_HYSTERIA_OBFS_TYPE);
|
||||||
|
|
||||||
const config: RuntimeConfig = {
|
const config: RuntimeConfig = {
|
||||||
domain: env.HY2XS_DOMAIN || "",
|
domain: env.HY2XS_DOMAIN || "",
|
||||||
@@ -181,14 +199,14 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
tlsKeyPath: normalizeSafeAbsolutePath("HY2XS_TLS_KEY_PATH", env.HY2XS_TLS_KEY_PATH || "/etc/hysteria/server.key"),
|
tlsKeyPath: normalizeSafeAbsolutePath("HY2XS_TLS_KEY_PATH", env.HY2XS_TLS_KEY_PATH || "/etc/hysteria/server.key"),
|
||||||
hysteriaBindHost: normalizeIpv4Host("HY2XS_HYSTERIA_BIND_HOST", env.HY2XS_HYSTERIA_BIND_HOST || "0.0.0.0"),
|
hysteriaBindHost: normalizeIpv4Host("HY2XS_HYSTERIA_BIND_HOST", env.HY2XS_HYSTERIA_BIND_HOST || "0.0.0.0"),
|
||||||
hysteriaPort,
|
hysteriaPort,
|
||||||
hysteriaAuthMode: "http",
|
hysteriaAuthMode,
|
||||||
hysteriaTrafficStatsHost: normalizeIpv4Host(
|
hysteriaTrafficStatsHost: normalizeIpv4Host(
|
||||||
"HY2XS_HYSTERIA_TRAFFIC_STATS_HOST",
|
"HY2XS_HYSTERIA_TRAFFIC_STATS_HOST",
|
||||||
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
||||||
),
|
),
|
||||||
hysteriaTrafficStatsPort: trafficStatsPort,
|
hysteriaTrafficStatsPort: trafficStatsPort,
|
||||||
hysteriaTrafficStatsSecret: valueOrGenerate(env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET),
|
hysteriaTrafficStatsSecret: valueOrGenerate(env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET),
|
||||||
hysteriaObfsType: "salamander",
|
hysteriaObfsType,
|
||||||
hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", valueOrGenerate(env.HY2XS_HYSTERIA_OBFS_PASSWORD)),
|
hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", valueOrGenerate(env.HY2XS_HYSTERIA_OBFS_PASSWORD)),
|
||||||
hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps",
|
hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps",
|
||||||
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export async function writePostInstallEnv(context: RuntimeContext): Promise<void
|
|||||||
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
|
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
|
||||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||||
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
|
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
|
||||||
|
BANDWIDTH_UP: context.config.hysteriaBandwidthUp,
|
||||||
|
BANDWIDTH_DOWN: context.config.hysteriaBandwidthDown,
|
||||||
|
IGNORE_CLIENT_BANDWIDTH: context.config.hysteriaIgnoreClientBandwidth ? "true" : "false",
|
||||||
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
|
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
|
||||||
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
|
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
|
||||||
UI_BIND_HOST: context.config.uiBindHost,
|
UI_BIND_HOST: context.config.uiBindHost,
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
|||||||
const aaaa = await run`getent ahostsv6 ${context.config.domain}`;
|
const aaaa = await run`getent ahostsv6 ${context.config.domain}`;
|
||||||
if (aaaa.trim()) {
|
if (aaaa.trim()) {
|
||||||
fail(
|
fail(
|
||||||
`domain ${context.config.domain} has AAAA record while HY2XS is IPv4-only; remove AAAA or set HY2XS_ALLOW_AAAA_WITH_IPV4_ONLY=true`
|
`domain ${context.config.domain} has AAAA record while HY2XS profile is IPv4-only; remove AAAA record before install`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -148,18 +148,23 @@ export async function smoke(context: RuntimeContext): Promise<void> {
|
|||||||
"auth invalid credentials",
|
"auth invalid credentials",
|
||||||
5,
|
5,
|
||||||
1000,
|
1000,
|
||||||
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
|
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":0}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
|
||||||
(response) => /"ok"\s*:\s*false/.test(response),
|
(response) => /"ok"\s*:\s*false/.test(response),
|
||||||
(response, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`),
|
(response, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < 10; i += 1) {
|
for (let i = 0; i < 10; i += 1) {
|
||||||
const response = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
|
const response = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":0}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
|
||||||
if (!/"ok"\s*:\s*false/.test(response)) {
|
if (!/"ok"\s*:\s*false/.test(response)) {
|
||||||
throw new Error(`unexpected auth response during rate-limit smoke: ${response}`);
|
throw new Error(`unexpected auth response during rate-limit smoke: ${response}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const invalidTypeAuthCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
|
||||||
|
if (invalidTypeAuthCode.trim() !== "400") {
|
||||||
|
throw new Error(`unexpected auth status for tx as string: ${invalidTypeAuthCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mode === "install") {
|
if (context.mode === "install") {
|
||||||
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
|
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
|
||||||
if (!adminConPass) {
|
if (!adminConPass) {
|
||||||
@@ -170,7 +175,7 @@ export async function smoke(context: RuntimeContext): Promise<void> {
|
|||||||
"auth valid credentials",
|
"auth valid credentials",
|
||||||
10,
|
10,
|
||||||
1000,
|
1000,
|
||||||
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
|
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":0}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
|
||||||
(response) => /"ok"\s*:\s*true/.test(response),
|
(response) => /"ok"\s*:\s*true/.test(response),
|
||||||
(response, error) => new Error(`unexpected auth response for valid credentials: ${response ?? String(error)}`),
|
(response, error) => new Error(`unexpected auth response for valid credentials: ${response ?? String(error)}`),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ HY2XS_DOMAIN=uk.api.withen.pro
|
|||||||
HY2XS_PUBLIC_HOST=uk.api.withen.pro
|
HY2XS_PUBLIC_HOST=uk.api.withen.pro
|
||||||
HY2XS_PUBLIC_PORT=443
|
HY2XS_PUBLIC_PORT=443
|
||||||
HY2XS_SSH_PORT=22
|
HY2XS_SSH_PORT=22
|
||||||
HY2XS_FIREWALL_ENABLED=true
|
HY2XS_FIREWALL_MODE=managed
|
||||||
HY2XS_FIREWALL_STAGED_APPLY=true
|
HY2XS_FIREWALL_STAGED_APPLY=true
|
||||||
HY2XS_FIREWALL_ALLOW_TAKEOVER=false
|
|
||||||
HY2XS_UI_BIND_HOST=127.0.0.1
|
HY2XS_UI_BIND_HOST=127.0.0.1
|
||||||
HY2XS_UI_PUBLIC_ACCESS=false
|
HY2XS_UI_PUBLIC_ACCESS=false
|
||||||
HY2XS_UI_PORT=8080
|
HY2XS_UI_PORT=8080
|
||||||
@@ -22,10 +21,12 @@ HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt
|
|||||||
HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key
|
HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key
|
||||||
HY2XS_HYSTERIA_BIND_HOST=0.0.0.0
|
HY2XS_HYSTERIA_BIND_HOST=0.0.0.0
|
||||||
HY2XS_HYSTERIA_PORT=443
|
HY2XS_HYSTERIA_PORT=443
|
||||||
|
# fixed in production profile: must remain http
|
||||||
HY2XS_HYSTERIA_AUTH_MODE=http
|
HY2XS_HYSTERIA_AUTH_MODE=http
|
||||||
HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1
|
HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1
|
||||||
HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712
|
HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712
|
||||||
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=__GENERATE__
|
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=__GENERATE__
|
||||||
|
# fixed in production profile: must remain salamander
|
||||||
HY2XS_HYSTERIA_OBFS_TYPE=salamander
|
HY2XS_HYSTERIA_OBFS_TYPE=salamander
|
||||||
HY2XS_HYSTERIA_OBFS_PASSWORD=__GENERATE__
|
HY2XS_HYSTERIA_OBFS_PASSWORD=__GENERATE__
|
||||||
HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
|
HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
|
||||||
|
|||||||
+3
-3
@@ -32,9 +32,9 @@ HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth
|
|||||||
HY2_TRAFFIC_STATS_LISTEN={{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}}
|
HY2_TRAFFIC_STATS_LISTEN={{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}}
|
||||||
HY2_OBFS_TYPE=salamander
|
HY2_OBFS_TYPE=salamander
|
||||||
HY2_OBFS_PASSWORD={{HYSTERIA_OBFS_PASSWORD}}
|
HY2_OBFS_PASSWORD={{HYSTERIA_OBFS_PASSWORD}}
|
||||||
HY2_BANDWIDTH_UP_Mbps=50
|
HY2_BANDWIDTH_UP={{BANDWIDTH_UP}}
|
||||||
HY2_BANDWIDTH_DOWN_Mbps=50
|
HY2_BANDWIDTH_DOWN={{BANDWIDTH_DOWN}}
|
||||||
HY2_IGNORE_CLIENT_BANDWIDTH=false
|
HY2_IGNORE_CLIENT_BANDWIDTH={{IGNORE_CLIENT_BANDWIDTH}}
|
||||||
HY2_CONFIG_PATH=/etc/hysteria/config.yaml
|
HY2_CONFIG_PATH=/etc/hysteria/config.yaml
|
||||||
|
|
||||||
HUI_ENABLED=true
|
HUI_ENABLED=true
|
||||||
|
|||||||
Reference in New Issue
Block a user