Реализован production-hardening по fix1: env/reconfigure, IPv4-only, TLS, secrets, firewall, docs
This commit is contained in:
+67
-34
@@ -1,8 +1,11 @@
|
||||
import { install } from "./commands/install";
|
||||
import type { InstallOptions } from "./types/context";
|
||||
import { reconfigure } from "./commands/reconfigure";
|
||||
import type { InstallOptions, ReconfigureOptions } from "./types/context";
|
||||
|
||||
function usage(): never {
|
||||
console.error("Usage: hy2xs-orchestrator install --package-dir <path> [--domain <name>] [--port <udp>] [--ssh-port <tcp>] [--skip-firewall] [--skip-start] [--ui-port <tcp>] [--ui-bind-host <host>] [--non-interactive]");
|
||||
console.error("Usage:");
|
||||
console.error(" hy2xs-orchestrator install --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start] [--non-interactive]");
|
||||
console.error(" hy2xs-orchestrator reconfigure --package-dir <path> [--config <path>] [--dry-run|--apply] [--skip-firewall] [--skip-start]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
@@ -18,14 +21,10 @@ function takeValue(args: string[], index: number, flag: string): string {
|
||||
function parseInstallOptions(args: string[]): InstallOptions {
|
||||
const options: InstallOptions = {
|
||||
packageDir: "",
|
||||
configPath: "/etc/hy2xs/hy2xs.env",
|
||||
nonInteractive: false,
|
||||
domain: "",
|
||||
port: 443,
|
||||
sshPort: 22,
|
||||
skipFirewall: false,
|
||||
skipStart: false,
|
||||
uiPort: 8080,
|
||||
uiBindHost: "127.0.0.1"
|
||||
skipStart: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
@@ -38,30 +37,14 @@ function parseInstallOptions(args: string[]): InstallOptions {
|
||||
case "--non-interactive":
|
||||
options.nonInteractive = true;
|
||||
break;
|
||||
case "--domain":
|
||||
options.domain = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--port":
|
||||
options.port = Number(takeValue(args, i, arg));
|
||||
i += 1;
|
||||
break;
|
||||
case "--ssh-port":
|
||||
options.sshPort = Number(takeValue(args, i, arg));
|
||||
i += 1;
|
||||
break;
|
||||
case "--skip-firewall":
|
||||
options.skipFirewall = true;
|
||||
break;
|
||||
case "--skip-start":
|
||||
options.skipStart = true;
|
||||
break;
|
||||
case "--ui-port":
|
||||
options.uiPort = Number(takeValue(args, i, arg));
|
||||
i += 1;
|
||||
break;
|
||||
case "--ui-bind-host":
|
||||
options.uiBindHost = takeValue(args, i, arg);
|
||||
case "--config":
|
||||
options.configPath = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
default:
|
||||
@@ -75,23 +58,73 @@ function parseInstallOptions(args: string[]): InstallOptions {
|
||||
usage();
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries({ port: options.port, sshPort: options.sshPort, uiPort: options.uiPort })) {
|
||||
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
||||
console.error(`Invalid ${name}: ${value}`);
|
||||
usage();
|
||||
return options;
|
||||
}
|
||||
|
||||
function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
||||
const options: ReconfigureOptions = {
|
||||
packageDir: "",
|
||||
configPath: "/etc/hy2xs/hy2xs.env",
|
||||
nonInteractive: false,
|
||||
dryRun: false,
|
||||
apply: false,
|
||||
skipFirewall: false,
|
||||
skipStart: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
switch (arg) {
|
||||
case "--package-dir":
|
||||
options.packageDir = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--config":
|
||||
options.configPath = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--dry-run":
|
||||
options.dryRun = true;
|
||||
break;
|
||||
case "--apply":
|
||||
options.apply = true;
|
||||
break;
|
||||
case "--skip-firewall":
|
||||
options.skipFirewall = true;
|
||||
break;
|
||||
case "--skip-start":
|
||||
options.skipStart = true;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
usage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.packageDir) {
|
||||
console.error("Missing --package-dir");
|
||||
usage();
|
||||
}
|
||||
|
||||
if (options.dryRun === options.apply) {
|
||||
console.error("Specify exactly one of --dry-run or --apply");
|
||||
usage();
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const [command, ...args] = Bun.argv.slice(2);
|
||||
if (command !== "install") {
|
||||
usage();
|
||||
if (command === "install") {
|
||||
await install(parseInstallOptions(args));
|
||||
return;
|
||||
}
|
||||
|
||||
await install(parseInstallOptions(args));
|
||||
if (command === "reconfigure") {
|
||||
await reconfigure(parseReconfigureOptions(args));
|
||||
return;
|
||||
}
|
||||
usage();
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { InstallContext, InstallOptions } from "../types/context";
|
||||
import { readText } from "../lib/fs";
|
||||
import { exists, readText, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
import { step } from "../lib/log";
|
||||
import { defaultRuntimeConfig, parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
||||
import { preflight } from "../steps/preflight";
|
||||
import { installDeps } from "../steps/deps";
|
||||
import { prepareFilesystem } from "../steps/filesystem";
|
||||
@@ -26,15 +28,16 @@ function secret(): string {
|
||||
}
|
||||
|
||||
export async function install(options: InstallOptions): Promise<void> {
|
||||
const hasConfig = await exists(options.configPath);
|
||||
const config = hasConfig ? parseRuntimeEnv(await readText(options.configPath)) : defaultRuntimeConfig();
|
||||
|
||||
const context: InstallContext = {
|
||||
options,
|
||||
config,
|
||||
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
|
||||
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
|
||||
installDate: new Date().toISOString(),
|
||||
hysteriaAuthPassword: secret(),
|
||||
hysteriaObfsPassword: secret(),
|
||||
hysteriaApiSecret: secret(),
|
||||
hysteriaApiPort: 36712,
|
||||
hysteriaVersion: "unknown"
|
||||
};
|
||||
|
||||
@@ -44,6 +47,11 @@ export async function install(options: InstallOptions): Promise<void> {
|
||||
await installDeps(context);
|
||||
step("filesystem");
|
||||
await prepareFilesystem(context);
|
||||
if (!hasConfig) {
|
||||
step("write runtime env");
|
||||
await runVisible`mkdir -p /etc/hy2xs`;
|
||||
await writeText(options.configPath, renderRuntimeEnv(config), 0o600);
|
||||
}
|
||||
step("bundled UI");
|
||||
await deployUi(context);
|
||||
step("Hysteria2 upstream install");
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ReconfigureContext, ReconfigureOptions } from "../types/context";
|
||||
import { readText, writeText } from "../lib/fs";
|
||||
import { info, step } from "../lib/log";
|
||||
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
||||
import { preflight } from "../steps/preflight";
|
||||
import { generateConfig } from "../steps/config";
|
||||
import { deploySystemd } from "../steps/systemd";
|
||||
import { applyFirewall } from "../steps/firewall";
|
||||
import { writePostInstallEnv } from "../steps/env";
|
||||
import { smoke } from "../steps/smoke";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
async function backupCurrentState(): Promise<void> {
|
||||
await runVisible`mkdir -p /etc/hy2xs/backups`;
|
||||
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/nftables.d/hy2xs.nft /etc/hy2xs/backups/hy2xs.nft.bak 2>/dev/null || true`;
|
||||
}
|
||||
|
||||
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`systemctl daemon-reload`;
|
||||
await runVisible`systemctl restart hysteria-server hy2xs-admin || true`;
|
||||
}
|
||||
|
||||
export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
||||
const configRaw = await readText(options.configPath);
|
||||
const config = parseRuntimeEnv(configRaw);
|
||||
|
||||
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
|
||||
options,
|
||||
config,
|
||||
packageVersion: "reconfigure",
|
||||
packageBuildId: "reconfigure",
|
||||
installDate: new Date().toISOString(),
|
||||
hysteriaAuthPassword: "managed-by-ui-auth",
|
||||
hysteriaVersion: "unknown"
|
||||
};
|
||||
|
||||
step("preflight");
|
||||
await preflight(context);
|
||||
|
||||
if (options.dryRun) {
|
||||
info("reconfigure dry-run: validated config and execution graph");
|
||||
info(`config file: ${options.configPath}`);
|
||||
info(`ui bind: ${config.uiBindHost}:${config.uiPort}`);
|
||||
info(`hysteria bind: ${config.hysteriaBindHost}:${config.hysteriaPort}`);
|
||||
info(`public endpoint: ${config.publicHost}:${config.publicPort}`);
|
||||
return;
|
||||
}
|
||||
|
||||
step("backup");
|
||||
await backupCurrentState();
|
||||
|
||||
try {
|
||||
step("config generation");
|
||||
await generateConfig(context);
|
||||
step("systemd units");
|
||||
await deploySystemd(context);
|
||||
step("firewall");
|
||||
await applyFirewall(context);
|
||||
step("write env artifacts");
|
||||
await writeText(options.configPath, renderRuntimeEnv(config), 0o600);
|
||||
await writePostInstallEnv(context);
|
||||
step("smoke checks");
|
||||
await smoke(context);
|
||||
} catch (error) {
|
||||
info("reconfigure failed, rollback in progress");
|
||||
await rollbackCurrentState();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { RuntimeConfig, TlsMode } from "../types/context";
|
||||
|
||||
type EnvMap = Record<string, string>;
|
||||
|
||||
function randomSecret(): string {
|
||||
return randomBytes(24).toString("base64url");
|
||||
}
|
||||
|
||||
function parseEnv(content: string): EnvMap {
|
||||
const result: EnvMap = {};
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
const separator = line.indexOf("=");
|
||||
if (separator < 1) {
|
||||
throw new Error(`invalid env line: ${rawLine}`);
|
||||
}
|
||||
const key = line.slice(0, separator).trim();
|
||||
const value = line.slice(separator + 1).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parsePort(name: string, raw: string, fallback: number): number {
|
||||
const value = raw ? Number(raw) : fallback;
|
||||
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
||||
throw new Error(`invalid ${name}: ${raw}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseBool(name: string, raw: string, fallback: boolean): boolean {
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
if (raw === "true") {
|
||||
return true;
|
||||
}
|
||||
if (raw === "false") {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`invalid ${name}: ${raw}`);
|
||||
}
|
||||
|
||||
function requireValue(name: string, value: string): string {
|
||||
if (!value || !value.trim()) {
|
||||
throw new Error(`missing required ${name}`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function normalizeIpv4Host(name: string, value: string): string {
|
||||
if (value === "0.0.0.0" || value === "127.0.0.1") {
|
||||
return value;
|
||||
}
|
||||
if (/^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value.includes(":")) {
|
||||
throw new Error(`${name} must be IPv4-only`);
|
||||
}
|
||||
throw new Error(`invalid ${name}: ${value}`);
|
||||
}
|
||||
|
||||
function normalizePublicHost(value: string): string {
|
||||
if (!value) {
|
||||
throw new Error("missing required HY2XS_PUBLIC_HOST");
|
||||
}
|
||||
if (value.includes(":")) {
|
||||
throw new Error("HY2XS_PUBLIC_HOST must not contain IPv6");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeTlsMode(value: string): TlsMode {
|
||||
if (value === "acme" || value === "file" || value === "self_signed_dev") {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`invalid HY2XS_TLS_MODE: ${value}`);
|
||||
}
|
||||
|
||||
export function parseRuntimeEnv(content: string): RuntimeConfig {
|
||||
const env = parseEnv(content);
|
||||
|
||||
const uiPort = parsePort("HY2XS_UI_PORT", env.HY2XS_UI_PORT, 8080);
|
||||
const uiBindHost = normalizeIpv4Host("HY2XS_UI_BIND_HOST", env.HY2XS_UI_BIND_HOST || "127.0.0.1");
|
||||
const hysteriaPort = parsePort("HY2XS_HYSTERIA_PORT", env.HY2XS_HYSTERIA_PORT, 443);
|
||||
const trafficStatsPort = parsePort("HY2XS_HYSTERIA_TRAFFIC_STATS_PORT", env.HY2XS_HYSTERIA_TRAFFIC_STATS_PORT, 36712);
|
||||
const tlsMode = normalizeTlsMode(env.HY2XS_TLS_MODE || "acme");
|
||||
|
||||
const config: RuntimeConfig = {
|
||||
domain: env.HY2XS_DOMAIN || "",
|
||||
publicHost: normalizePublicHost(env.HY2XS_PUBLIC_HOST || env.HY2XS_DOMAIN || ""),
|
||||
publicPort: parsePort("HY2XS_PUBLIC_PORT", env.HY2XS_PUBLIC_PORT, hysteriaPort),
|
||||
ipv6Enabled: false,
|
||||
sshPort: parsePort("HY2XS_SSH_PORT", env.HY2XS_SSH_PORT, 22),
|
||||
firewallEnabled: parseBool("HY2XS_FIREWALL_ENABLED", env.HY2XS_FIREWALL_ENABLED, true),
|
||||
firewallStagedApply: parseBool("HY2XS_FIREWALL_STAGED_APPLY", env.HY2XS_FIREWALL_STAGED_APPLY, true),
|
||||
uiBindHost,
|
||||
uiPort,
|
||||
adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"),
|
||||
adminInitialPassword: env.HY2XS_ADMIN_INITIAL_PASSWORD || randomSecret(),
|
||||
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
|
||||
tlsMode,
|
||||
acmeEmail: env.HY2XS_ACME_EMAIL || "",
|
||||
tlsCertPath: env.HY2XS_TLS_CERT_PATH || "/etc/hysteria/server.crt",
|
||||
tlsKeyPath: env.HY2XS_TLS_KEY_PATH || "/etc/hysteria/server.key",
|
||||
hysteriaBindHost: normalizeIpv4Host("HY2XS_HYSTERIA_BIND_HOST", env.HY2XS_HYSTERIA_BIND_HOST || "0.0.0.0"),
|
||||
hysteriaPort,
|
||||
hysteriaAuthMode: "http",
|
||||
hysteriaAuthUrl: env.HY2XS_HYSTERIA_AUTH_URL || `http://127.0.0.1:${uiPort}/hui/hysteria2/auth`,
|
||||
hysteriaTrafficStatsHost: normalizeIpv4Host(
|
||||
"HY2XS_HYSTERIA_TRAFFIC_STATS_HOST",
|
||||
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
||||
),
|
||||
hysteriaTrafficStatsPort: trafficStatsPort,
|
||||
hysteriaTrafficStatsSecret: env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET || randomSecret(),
|
||||
hysteriaObfsType: "salamander",
|
||||
hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", env.HY2XS_HYSTERIA_OBFS_PASSWORD || randomSecret()),
|
||||
hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps",
|
||||
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
||||
hysteriaIgnoreClientBandwidth: parseBool(
|
||||
"HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH",
|
||||
env.HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH,
|
||||
false
|
||||
),
|
||||
hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml",
|
||||
hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "latest",
|
||||
installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin",
|
||||
dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin",
|
||||
logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs",
|
||||
bootstrapAdminSecretPath: "/etc/hy2xs/bootstrap-admin.secret"
|
||||
};
|
||||
|
||||
validateRuntimeConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defaultRuntimeConfig(): RuntimeConfig {
|
||||
return parseRuntimeEnv(
|
||||
[
|
||||
"HY2XS_IPV6_ENABLED=false",
|
||||
"HY2XS_DOMAIN=",
|
||||
"HY2XS_PUBLIC_HOST=127.0.0.1",
|
||||
"HY2XS_PUBLIC_PORT=443",
|
||||
"HY2XS_SSH_PORT=22",
|
||||
"HY2XS_FIREWALL_ENABLED=true",
|
||||
"HY2XS_FIREWALL_STAGED_APPLY=true",
|
||||
"HY2XS_UI_BIND_HOST=127.0.0.1",
|
||||
"HY2XS_UI_PORT=8080",
|
||||
"HY2XS_ADMIN_USER=admin",
|
||||
`HY2XS_ADMIN_INITIAL_PASSWORD=${randomSecret()}`,
|
||||
"HY2XS_FORCE_PASSWORD_CHANGE=true",
|
||||
"HY2XS_TLS_MODE=self_signed_dev",
|
||||
"HY2XS_ACME_EMAIL=",
|
||||
"HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt",
|
||||
"HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key",
|
||||
"HY2XS_HYSTERIA_BIND_HOST=0.0.0.0",
|
||||
"HY2XS_HYSTERIA_PORT=443",
|
||||
"HY2XS_HYSTERIA_AUTH_MODE=http",
|
||||
"HY2XS_HYSTERIA_AUTH_URL=http://127.0.0.1:8080/hui/hysteria2/auth",
|
||||
"HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1",
|
||||
"HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712",
|
||||
`HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=${randomSecret()}`,
|
||||
"HY2XS_HYSTERIA_OBFS_TYPE=salamander",
|
||||
`HY2XS_HYSTERIA_OBFS_PASSWORD=${randomSecret()}`,
|
||||
"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_INSTALL_DIR=/opt/hy2xs-admin",
|
||||
"HY2XS_DATA_DIR=/var/lib/hy2xs-admin",
|
||||
"HY2XS_LOG_DIR=/var/log/hy2xs"
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
export function validateRuntimeConfig(config: RuntimeConfig): void {
|
||||
if (config.ipv6Enabled) {
|
||||
throw new Error("HY2XS is IPv4-only: HY2XS_IPV6_ENABLED must be false");
|
||||
}
|
||||
if (config.uiBindHost.includes(":")) {
|
||||
throw new Error("HY2XS_UI_BIND_HOST must be IPv4-only");
|
||||
}
|
||||
if (config.hysteriaBindHost.includes(":")) {
|
||||
throw new Error("HY2XS_HYSTERIA_BIND_HOST must be IPv4-only");
|
||||
}
|
||||
if (config.hysteriaBindHost !== "0.0.0.0") {
|
||||
throw new Error("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 in production profile");
|
||||
}
|
||||
|
||||
if (config.tlsMode === "acme") {
|
||||
if (!config.domain) {
|
||||
throw new Error("HY2XS_DOMAIN is required for TLS mode acme");
|
||||
}
|
||||
if (!config.acmeEmail) {
|
||||
throw new Error("HY2XS_ACME_EMAIL is required for TLS mode acme");
|
||||
}
|
||||
}
|
||||
if (config.tlsMode === "file") {
|
||||
requireValue("HY2XS_TLS_CERT_PATH", config.tlsCertPath);
|
||||
requireValue("HY2XS_TLS_KEY_PATH", config.tlsKeyPath);
|
||||
}
|
||||
|
||||
if (config.publicHost === "0.0.0.0") {
|
||||
throw new Error("HY2XS_PUBLIC_HOST must be a routable domain or IPv4, not 0.0.0.0");
|
||||
}
|
||||
}
|
||||
|
||||
export function renderRuntimeEnv(config: RuntimeConfig): string {
|
||||
const lines = [
|
||||
"# HY2XS runtime config (editable)",
|
||||
"HY2XS_IPV6_ENABLED=false",
|
||||
`HY2XS_DOMAIN=${config.domain}`,
|
||||
`HY2XS_PUBLIC_HOST=${config.publicHost}`,
|
||||
`HY2XS_PUBLIC_PORT=${config.publicPort}`,
|
||||
`HY2XS_SSH_PORT=${config.sshPort}`,
|
||||
`HY2XS_FIREWALL_ENABLED=${config.firewallEnabled}`,
|
||||
`HY2XS_FIREWALL_STAGED_APPLY=${config.firewallStagedApply}`,
|
||||
`HY2XS_UI_BIND_HOST=${config.uiBindHost}`,
|
||||
`HY2XS_UI_PORT=${config.uiPort}`,
|
||||
`HY2XS_ADMIN_USER=${config.adminUser}`,
|
||||
`HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`,
|
||||
`HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`,
|
||||
`HY2XS_TLS_MODE=${config.tlsMode}`,
|
||||
`HY2XS_ACME_EMAIL=${config.acmeEmail}`,
|
||||
`HY2XS_TLS_CERT_PATH=${config.tlsCertPath}`,
|
||||
`HY2XS_TLS_KEY_PATH=${config.tlsKeyPath}`,
|
||||
`HY2XS_HYSTERIA_BIND_HOST=${config.hysteriaBindHost}`,
|
||||
`HY2XS_HYSTERIA_PORT=${config.hysteriaPort}`,
|
||||
"HY2XS_HYSTERIA_AUTH_MODE=http",
|
||||
`HY2XS_HYSTERIA_AUTH_URL=${config.hysteriaAuthUrl}`,
|
||||
`HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=${config.hysteriaTrafficStatsHost}`,
|
||||
`HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=${config.hysteriaTrafficStatsPort}`,
|
||||
`HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=${config.hysteriaTrafficStatsSecret}`,
|
||||
"HY2XS_HYSTERIA_OBFS_TYPE=salamander",
|
||||
`HY2XS_HYSTERIA_OBFS_PASSWORD=${config.hysteriaObfsPassword}`,
|
||||
`HY2XS_HYSTERIA_BANDWIDTH_UP=${config.hysteriaBandwidthUp}`,
|
||||
`HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`,
|
||||
`HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`,
|
||||
`HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`,
|
||||
`HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`,
|
||||
`HY2XS_INSTALL_DIR=${config.installDir}`,
|
||||
`HY2XS_DATA_DIR=${config.dataDir}`,
|
||||
`HY2XS_LOG_DIR=${config.logDir}`
|
||||
];
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
@@ -3,19 +3,40 @@ import { readText, renderTemplate, writeText } from "../lib/fs";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function generateConfig(context: InstallContext): Promise<void> {
|
||||
const tlsAcmeBlock = context.config.tlsMode === "acme"
|
||||
? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0`
|
||||
: "";
|
||||
const tlsFileBlock = context.config.tlsMode === "file" || context.config.tlsMode === "self_signed_dev"
|
||||
? `tls:\n cert: ${context.config.tlsCertPath}\n key: ${context.config.tlsKeyPath}`
|
||||
: "";
|
||||
|
||||
const template = await readText(`${context.options.packageDir}/templates/hysteria/config.yaml.tpl`);
|
||||
const rendered = renderTemplate(template, {
|
||||
HYSTERIA_PORT: context.options.port,
|
||||
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
|
||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||
HYSTERIA_AUTH_PASSWORD: context.hysteriaAuthPassword,
|
||||
HYSTERIA_OBFS_PASSWORD: context.hysteriaObfsPassword,
|
||||
HYSTERIA_API_PORT: context.hysteriaApiPort,
|
||||
HYSTERIA_API_SECRET: context.hysteriaApiSecret,
|
||||
UI_PORT: context.options.uiPort,
|
||||
BANDWIDTH_UP: "50 mbps",
|
||||
BANDWIDTH_DOWN: "50 mbps"
|
||||
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
|
||||
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
|
||||
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
|
||||
HYSTERIA_API_SECRET: context.config.hysteriaTrafficStatsSecret,
|
||||
UI_PORT: context.config.uiPort,
|
||||
BANDWIDTH_UP: context.config.hysteriaBandwidthUp,
|
||||
BANDWIDTH_DOWN: context.config.hysteriaBandwidthDown,
|
||||
TLS_ACME_BLOCK: tlsAcmeBlock,
|
||||
TLS_FILE_BLOCK: tlsFileBlock,
|
||||
AUTH_INSECURE: context.config.tlsMode === "self_signed_dev" ? "true" : "false"
|
||||
});
|
||||
|
||||
await writeText("/etc/hysteria/config.yaml", rendered, 0o600);
|
||||
await runVisible`openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=${context.options.domain || "hy2xs.local"} -keyout /etc/hysteria/server.key -out /etc/hysteria/server.crt`;
|
||||
await runVisible`chown hysteria:hysteria /etc/hysteria/config.yaml /etc/hysteria/server.key /etc/hysteria/server.crt`;
|
||||
await writeText("/etc/hysteria/config.yaml.tmp", rendered, 0o600);
|
||||
await runVisible`mv /etc/hysteria/config.yaml.tmp /etc/hysteria/config.yaml`;
|
||||
|
||||
if (context.config.tlsMode === "self_signed_dev") {
|
||||
await runVisible`openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=${context.config.domain || "hy2xs.local"} -keyout ${context.config.tlsKeyPath} -out ${context.config.tlsCertPath}`;
|
||||
await runVisible`chmod 600 ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`;
|
||||
}
|
||||
|
||||
await runVisible`chown hysteria:hysteria /etc/hysteria/config.yaml`;
|
||||
if (context.config.tlsMode !== "acme") {
|
||||
await runVisible`chown hysteria:hysteria ${context.config.tlsKeyPath} ${context.config.tlsCertPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,29 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
|
||||
PACKAGE_VERSION: context.packageVersion,
|
||||
PACKAGE_BUILD_ID: context.packageBuildId,
|
||||
INSTALL_DATE: context.installDate,
|
||||
DOMAIN: context.options.domain,
|
||||
SSH_PORT: context.options.sshPort,
|
||||
DOMAIN: context.config.domain,
|
||||
PUBLIC_HOST: context.config.publicHost,
|
||||
PUBLIC_PORT: context.config.publicPort,
|
||||
SSH_PORT: context.config.sshPort,
|
||||
FIREWALL_ENABLED: context.config.firewallEnabled ? "true" : "false",
|
||||
FIREWALL_STAGED_APPLY: context.config.firewallStagedApply ? "true" : "false",
|
||||
HYSTERIA_VERSION: context.hysteriaVersion,
|
||||
HYSTERIA_PORT: context.options.port,
|
||||
HYSTERIA_OBFS_PASSWORD: context.hysteriaObfsPassword,
|
||||
HYSTERIA_API_PORT: context.hysteriaApiPort,
|
||||
UI_BIND_HOST: context.options.uiBindHost,
|
||||
UI_PORT: context.options.uiPort
|
||||
TLS_MODE: context.config.tlsMode,
|
||||
ACME_EMAIL: context.config.acmeEmail,
|
||||
TLS_CERT_PATH: context.config.tlsCertPath,
|
||||
TLS_KEY_PATH: context.config.tlsKeyPath,
|
||||
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
|
||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
|
||||
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
|
||||
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
|
||||
UI_BIND_HOST: context.config.uiBindHost,
|
||||
UI_PORT: context.config.uiPort,
|
||||
INSTALL_DIR: context.config.installDir,
|
||||
DATA_DIR: context.config.dataDir,
|
||||
LOG_DIR: context.config.logDir
|
||||
});
|
||||
|
||||
await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
|
||||
await writeText(context.config.bootstrapAdminSecretPath, `${context.config.adminUser}:${context.config.adminInitialPassword}\n`, 0o600);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function prepareFilesystem(_context: InstallContext): Promise<void> {
|
||||
export async function prepareFilesystem(context: InstallContext): Promise<void> {
|
||||
await runVisible`id -u hysteria >/dev/null 2>&1 || useradd --system --home /var/lib/hysteria --shell /usr/sbin/nologin hysteria`;
|
||||
await runVisible`mkdir -p /etc/hysteria /var/lib/hysteria /opt/hy2xs-admin /var/lib/hy2xs-admin /var/log/hy2xs /usr/local/lib/hy2xs`;
|
||||
await runVisible`id -u hy2xs-admin >/dev/null 2>&1 || useradd --system --home ${context.config.dataDir} --shell /usr/sbin/nologin hy2xs-admin`;
|
||||
await runVisible`mkdir -p /etc/hy2xs /etc/hysteria /var/lib/hysteria ${context.config.installDir} ${context.config.dataDir} ${context.config.logDir} /usr/local/lib/hy2xs /etc/nftables.d`;
|
||||
await runVisible`chown -R hysteria:hysteria /etc/hysteria /var/lib/hysteria`;
|
||||
await runVisible`chown -R root:root /var/lib/hy2xs-admin`;
|
||||
await runVisible`chown -R root:root ${context.config.installDir}`;
|
||||
await runVisible`chmod -R go-w ${context.config.installDir}`;
|
||||
await runVisible`chown -R hy2xs-admin:hy2xs-admin ${context.config.dataDir} ${context.config.logDir}`;
|
||||
await runVisible`chmod 700 /etc/hy2xs`;
|
||||
}
|
||||
|
||||
@@ -4,19 +4,36 @@ import { info } from "../lib/log";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function applyFirewall(context: InstallContext): Promise<void> {
|
||||
if (context.options.skipFirewall) {
|
||||
if (context.options.skipFirewall || !context.config.firewallEnabled) {
|
||||
info("firewall skipped by flag");
|
||||
return;
|
||||
}
|
||||
|
||||
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/nftables/hy2xs.nft.tpl`), {
|
||||
SSH_PORT: context.options.sshPort,
|
||||
HYSTERIA_PORT: context.options.port,
|
||||
UI_PORT: context.options.uiPort
|
||||
SSH_PORT: context.config.sshPort,
|
||||
HYSTERIA_PORT: context.config.hysteriaPort,
|
||||
UI_PORT: context.config.uiPort,
|
||||
UI_BIND_HOST: context.config.uiBindHost
|
||||
});
|
||||
|
||||
await runVisible`cp -a /etc/nftables.conf /etc/nftables.conf.hy2xs.bak 2>/dev/null || true`;
|
||||
await writeText("/etc/nftables.conf", rendered, 0o644);
|
||||
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/nftables.d/hy2xs.nft.bak 2>/dev/null || true`;
|
||||
await writeText("/etc/nftables.d/hy2xs.nft.candidate", rendered, 0o600);
|
||||
await runVisible`nft -c -f /etc/nftables.d/hy2xs.nft.candidate`;
|
||||
|
||||
if (context.config.firewallStagedApply) {
|
||||
await runVisible`systemd-run --unit hy2xs-fw-rollback --on-active=45s /bin/sh -c 'cp -a /etc/nftables.d/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; 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`;
|
||||
|
||||
await runVisible`ss -H -ltn | grep -q ':${context.config.sshPort} ' || (echo 'ssh port check failed' >&2; exit 1)`;
|
||||
await runVisible`ss -H -lun | grep -q ':${context.config.hysteriaPort} ' || (echo 'hysteria udp port check failed' >&2; exit 1)`;
|
||||
|
||||
if (context.config.firewallStagedApply) {
|
||||
await runVisible`systemctl stop hy2xs-fw-rollback || true`;
|
||||
await runVisible`systemctl reset-failed hy2xs-fw-rollback || true`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InstallContext } from "../types/context";
|
||||
import { exists, readText } from "../lib/fs";
|
||||
import { fail } from "../lib/log";
|
||||
import { fail, info } from "../lib/log";
|
||||
import { run } from "../lib/process";
|
||||
|
||||
async function isPortBusy(port: number): Promise<boolean> {
|
||||
@@ -13,6 +13,8 @@ async function isPortBusy(port: number): Promise<boolean> {
|
||||
}
|
||||
|
||||
export async function preflight(context: InstallContext): Promise<void> {
|
||||
const isReconfigure = context.packageVersion === "reconfigure";
|
||||
|
||||
if (process.getuid?.() !== 0) {
|
||||
fail("installer must run as root");
|
||||
}
|
||||
@@ -26,28 +28,59 @@ export async function preflight(context: InstallContext): Promise<void> {
|
||||
fail("bundled HY2XS admin is missing from install package");
|
||||
}
|
||||
|
||||
if (await exists("/etc/hysteria/post-install.env")) {
|
||||
if (!isReconfigure && (await exists("/etc/hysteria/post-install.env"))) {
|
||||
fail("existing HY2XS post-install.env found; update/repair is out of scope");
|
||||
}
|
||||
|
||||
if (await exists("/opt/hy2xs-admin")) {
|
||||
if (!isReconfigure && (await exists(context.config.installDir))) {
|
||||
fail("existing /opt/hy2xs-admin found; conflicting old state");
|
||||
}
|
||||
|
||||
const ports = new Set([context.options.port, context.options.uiPort]);
|
||||
const ports = new Set([context.config.hysteriaPort, context.config.uiPort]);
|
||||
if (ports.size !== 2) {
|
||||
fail("Hysteria port and UI port must be different");
|
||||
}
|
||||
|
||||
if (context.options.domain && !/^[a-zA-Z0-9.-]+$/.test(context.options.domain)) {
|
||||
if (context.config.domain && !/^[a-zA-Z0-9.-]+$/.test(context.config.domain)) {
|
||||
fail("domain contains unsupported characters");
|
||||
}
|
||||
|
||||
if (await isPortBusy(context.options.port)) {
|
||||
fail(`Hysteria UDP/TCP port already appears to be in use: ${context.options.port}`);
|
||||
if (context.config.uiBindHost.includes(":")) {
|
||||
fail("HY2XS UI bind host must be IPv4-only");
|
||||
}
|
||||
|
||||
if (await isPortBusy(context.options.uiPort)) {
|
||||
fail(`HY2XS admin port already appears to be in use: ${context.options.uiPort}`);
|
||||
if (context.config.hysteriaBindHost !== "0.0.0.0") {
|
||||
fail("HY2XS_HYSTERIA_BIND_HOST must be 0.0.0.0 for production profile");
|
||||
}
|
||||
|
||||
if (context.config.tlsMode === "acme" && (!context.config.domain || !context.config.acmeEmail)) {
|
||||
fail("acme mode requires HY2XS_DOMAIN and HY2XS_ACME_EMAIL");
|
||||
}
|
||||
|
||||
if (context.config.domain) {
|
||||
try {
|
||||
const a = await run`getent ahostsv4 ${context.config.domain}`;
|
||||
if (!a.trim()) {
|
||||
fail(`domain has no A-record: ${context.config.domain}`);
|
||||
}
|
||||
} catch {
|
||||
fail(`domain has no A-record: ${context.config.domain}`);
|
||||
}
|
||||
try {
|
||||
const aaaa = await run`getent ahostsv6 ${context.config.domain}`;
|
||||
if (aaaa.trim()) {
|
||||
info(`warning: domain ${context.config.domain} has AAAA record; HY2XS remains IPv4-only`);
|
||||
}
|
||||
} catch {
|
||||
// no AAAA is acceptable
|
||||
}
|
||||
}
|
||||
|
||||
if (await isPortBusy(context.config.hysteriaPort)) {
|
||||
fail(`Hysteria UDP/TCP port already appears to be in use: ${context.config.hysteriaPort}`);
|
||||
}
|
||||
|
||||
if (await isPortBusy(context.config.uiPort)) {
|
||||
fail(`HY2XS admin port already appears to be in use: ${context.config.uiPort}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,17 @@ export async function smoke(context: InstallContext): Promise<void> {
|
||||
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`ss -H -lntu | grep -q ':${context.options.uiPort} '`;
|
||||
await runVisible`curl -fsS --max-time 5 http://127.0.0.1:${context.options.uiPort}/ >/dev/null`;
|
||||
await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`;
|
||||
await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`;
|
||||
await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`;
|
||||
await runVisible`test "$(stat -c '%a' ${context.config.bootstrapAdminSecretPath})" = '600'`;
|
||||
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 -ltnu | grep -q '\[::\]'`;
|
||||
await runVisible`curl -fsS --max-time 5 http://127.0.0.1:${context.config.uiPort}/ >/dev/null`;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import { runVisible } from "../lib/process";
|
||||
|
||||
export async function deploySystemd(context: InstallContext): Promise<void> {
|
||||
const values = {
|
||||
UI_BIND_HOST: context.options.uiBindHost,
|
||||
UI_PORT: context.options.uiPort
|
||||
UI_BIND_HOST: context.config.uiBindHost,
|
||||
UI_PORT: context.config.uiPort,
|
||||
INSTALL_DIR: context.config.installDir,
|
||||
DATA_DIR: context.config.dataDir,
|
||||
LOG_DIR: context.config.logDir
|
||||
};
|
||||
|
||||
const hysteriaUnit = await readText(`${context.options.packageDir}/systemd/hysteria-server.service`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { InstallContext } from "../types/context";
|
||||
import { runVisible } from "../lib/process";
|
||||
|
||||
export async function deployUi(context: InstallContext): Promise<void> {
|
||||
await runVisible`cp -a ${context.options.packageDir}/ui/hy2xs-admin/. /opt/hy2xs-admin/`;
|
||||
await runVisible`chown -R root:root /opt/hy2xs-admin`;
|
||||
await runVisible`chmod -R go-w /opt/hy2xs-admin`;
|
||||
await runVisible`cp -a ${context.options.packageDir}/ui/hy2xs-admin/. ${context.config.installDir}/`;
|
||||
await runVisible`chown -R root:root ${context.config.installDir}`;
|
||||
await runVisible`chmod -R go-w ${context.config.installDir}`;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,71 @@
|
||||
export type InstallOptions = {
|
||||
packageDir: string;
|
||||
configPath: string;
|
||||
nonInteractive: boolean;
|
||||
domain: string;
|
||||
port: number;
|
||||
sshPort: number;
|
||||
skipFirewall: boolean;
|
||||
skipStart: boolean;
|
||||
uiPort: number;
|
||||
};
|
||||
|
||||
export type ReconfigureOptions = {
|
||||
packageDir: string;
|
||||
configPath: string;
|
||||
nonInteractive: boolean;
|
||||
dryRun: boolean;
|
||||
apply: boolean;
|
||||
skipFirewall: boolean;
|
||||
skipStart: boolean;
|
||||
};
|
||||
|
||||
export type TlsMode = "acme" | "file" | "self_signed_dev";
|
||||
|
||||
export type RuntimeConfig = {
|
||||
domain: string;
|
||||
publicHost: string;
|
||||
publicPort: number;
|
||||
ipv6Enabled: false;
|
||||
sshPort: number;
|
||||
firewallEnabled: boolean;
|
||||
firewallStagedApply: boolean;
|
||||
uiBindHost: string;
|
||||
uiPort: number;
|
||||
adminUser: string;
|
||||
adminInitialPassword: string;
|
||||
forcePasswordChange: boolean;
|
||||
tlsMode: TlsMode;
|
||||
acmeEmail: string;
|
||||
tlsCertPath: string;
|
||||
tlsKeyPath: string;
|
||||
hysteriaBindHost: string;
|
||||
hysteriaPort: number;
|
||||
hysteriaAuthMode: "http";
|
||||
hysteriaAuthUrl: string;
|
||||
hysteriaTrafficStatsHost: string;
|
||||
hysteriaTrafficStatsPort: number;
|
||||
hysteriaTrafficStatsSecret: string;
|
||||
hysteriaObfsType: "salamander";
|
||||
hysteriaObfsPassword: string;
|
||||
hysteriaBandwidthUp: string;
|
||||
hysteriaBandwidthDown: string;
|
||||
hysteriaIgnoreClientBandwidth: boolean;
|
||||
hysteriaConfigPath: string;
|
||||
hysteriaVersionPolicy: string;
|
||||
installDir: string;
|
||||
dataDir: string;
|
||||
logDir: string;
|
||||
bootstrapAdminSecretPath: string;
|
||||
};
|
||||
|
||||
export type InstallContext = {
|
||||
options: InstallOptions;
|
||||
config: RuntimeConfig;
|
||||
packageVersion: string;
|
||||
packageBuildId: string;
|
||||
installDate: string;
|
||||
hysteriaAuthPassword: string;
|
||||
hysteriaObfsPassword: string;
|
||||
hysteriaApiSecret: string;
|
||||
hysteriaApiPort: number;
|
||||
hysteriaVersion: string;
|
||||
};
|
||||
|
||||
export type ReconfigureContext = {
|
||||
options: ReconfigureOptions;
|
||||
config: RuntimeConfig;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user