import type { RuntimeContext } from "../types/context"; import { info } from "../lib/log"; import { runHidden, runSecret, runVisible } from "../lib/process"; async function retry( label: string, attempts: number, delayMs: number, action: () => Promise, validate: (value: T) => boolean, errorFactory: (value: T | undefined, error: unknown) => Error, ): Promise { let lastValue: T | undefined; let lastError: unknown; for (let i = 0; i < attempts; i += 1) { try { const value = await action(); lastValue = value; if (validate(value)) { return value; } } catch (error) { lastError = error; } if (i < attempts - 1) { info(`${label}: retry ${i + 1}/${attempts}`); await runHidden`sleep ${Math.max(1, Math.ceil(delayMs / 1000))}`; } } throw errorFactory(lastValue, lastError); } export async function smoke(context: RuntimeContext): Promise { if (context.options.skipStart) { info("service start and smoke checks skipped by flag"); return; } await runVisible`systemctl restart hysteria-server hy2xs-admin`; await runVisible`systemctl is-active --quiet hysteria-server`; await runVisible`systemctl is-active --quiet hy2xs-admin`; await runVisible`/usr/local/bin/hysteria version`; await runVisible`test -s /etc/hysteria/config.yaml`; await runVisible`test -s /etc/hy2xs/hy2xs.env`; await runVisible`test -s /etc/hysteria/post-install.env`; await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`; await runVisible`grep -q '^ADMIN_USER=' ${context.config.bootstrapAdminSecretPath}`; await runVisible`grep -q '^ADMIN_INITIAL_PASSWORD=' ${context.config.bootstrapAdminSecretPath}`; await runVisible`grep -q '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath}`; await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '640'`; await runVisible`test "$(stat -c '%U:%G' /etc/hysteria/config.yaml)" = 'hysteria:hy2xs-admin'`; await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`; await runVisible`test "$(stat -c '%U:%G' /etc/hy2xs/hy2xs.env)" = 'root:root'`; await runVisible`test "$(stat -c '%a' /etc/hysteria/post-install.env)" = '600'`; await runVisible`test "$(stat -c '%U:%G' /etc/hysteria/post-install.env)" = 'root:root'`; await runVisible`test "$(stat -c '%a' ${context.config.bootstrapAdminSecretPath})" = '600'`; await runVisible`test "$(stat -c '%U:%G' ${context.config.bootstrapAdminSecretPath})" = 'root:root'`; await runVisible`sudo -u hysteria test -r /etc/hysteria/config.yaml`; await runVisible`sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml`; await runVisible`sudo -u hy2xs-admin test ! -w /etc/hysteria/config.yaml`; await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env`; await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/bootstrap-admin.secret`; await runVisible`sudo -u hysteria test ! -r /etc/hy2xs/bootstrap-admin.secret`; await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`; if (context.config.uiBindHost === "127.0.0.1") { await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`; } await runVisible`ss -H -lun | grep -q '0.0.0.0:${context.config.hysteriaPort} '`; await runVisible`! ss -H -ltn | grep -q '\[::\]:${context.config.uiPort} '`; await runVisible`! ss -H -lun | grep -q '\[::\]:${context.config.hysteriaPort} '`; const invalidAuthResponse = await retry( "auth invalid credentials", 5, 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`, (response) => /"ok"\s*:\s*false/.test(response), (response, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`), ); 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`; if (!/"ok"\s*:\s*false/.test(response)) { throw new Error(`unexpected auth response during rate-limit smoke: ${response}`); } } if (context.mode === "install") { const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim(); if (!adminConPass) { throw new Error("admin connection password is empty in bootstrap secret file"); } await retry( "auth valid credentials", 10, 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`, (response) => /"ok"\s*:\s*true/.test(response), (response, error) => new Error(`unexpected auth response for valid credentials: ${response ?? String(error)}`), ); } await retry( "trafficStats valid secret", 10, 1000, async () => runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`, (code) => /^2\d\d$/.test(code.trim()), (code, error) => new Error(`unexpected trafficStats status for valid secret: ${code ?? String(error)}`), ); const deniedCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`; if (!/(401|403)/.test(deniedCode)) { throw new Error(`unexpected trafficStats status for invalid secret: ${deniedCode}`); } await runVisible`nft -c -f /etc/nftables.conf`; if (context.config.tlsMode === "acme") { await runVisible`grep -q '^acme:' /etc/hysteria/config.yaml`; await runVisible`! grep -q '^tls:' /etc/hysteria/config.yaml`; } if (context.config.tlsMode === "file") { await runVisible`grep -q '^tls:' /etc/hysteria/config.yaml`; await runVisible`! grep -q '^acme:' /etc/hysteria/config.yaml`; await runVisible`grep -q 'insecure: false' /etc/hysteria/config.yaml`; } if (context.config.tlsMode === "self_signed_dev") { await runVisible`grep -q '^tls:' /etc/hysteria/config.yaml`; await runVisible`! grep -q '^acme:' /etc/hysteria/config.yaml`; await runVisible`grep -q 'insecure: true' /etc/hysteria/config.yaml`; } }