Compare commits

..

2 Commits

Author SHA1 Message Date
b06337813d Fix hysteria config file permissions 2026-03-28 17:21:31 +08:00
ea5655cd96 Update hy2 server config template generation 2026-03-28 17:21:31 +08:00

213
hy2.sh
View File

@@ -4,6 +4,9 @@ trap 'echo "[错误] 第 $LINENO 行执行失败:$BASH_COMMAND" >&2' ERR
CONFIG_FILE="/etc/hysteria/config.yaml" CONFIG_FILE="/etc/hysteria/config.yaml"
SERVICE_NAME="hysteria-server.service" SERVICE_NAME="hysteria-server.service"
DEVICE_USERS=(macminim4 iphone12p ipadmini5 pve_debian other)
USERPASS_ENTRIES=()
TRAFFIC_STATS_SECRET=""
red() { printf '\033[31m%s\033[0m\n' "$*"; } red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; }
@@ -89,8 +92,16 @@ wait_for_apt_lock() {
} }
generate_password() { generate_password() {
openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24 local length="${1:-36}"
echo local password=""
while ((${#password} < length)); do
password+="$(
openssl rand -base64 48 | tr -dc 'A-Za-z0-9'
)"
done
printf '%s\n' "${password:0:length}"
} }
mask_secret() { mask_secret() {
@@ -103,6 +114,13 @@ mask_secret() {
fi fi
} }
yaml_quote() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
printf '"%s"' "$value"
}
validate_domain() { validate_domain() {
local domain="$1" local domain="$1"
[[ -n "$domain" ]] || return 1 [[ -n "$domain" ]] || return 1
@@ -369,12 +387,96 @@ backup_existing_config() {
fi fi
} }
write_config() { set_config_permissions() {
local service_user service_group
service_user="$(systemctl show -p User --value "${SERVICE_NAME}" 2>/dev/null || true)"
service_user="${service_user//$'\n'/}"
if [[ -n "${service_user}" && "${service_user}" != "root" ]]; then
service_group="$(id -gn "${service_user}" 2>/dev/null || true)"
if [[ -n "${service_group}" ]]; then
chown root:"${service_group}" "${CONFIG_FILE}"
chmod 640 "${CONFIG_FILE}"
green "已按服务账户 ${service_user}:${service_group} 设置配置文件权限为 640"
return 0
fi
fi
chown root:root "${CONFIG_FILE}"
chmod 644 "${CONFIG_FILE}"
yellow "未识别到可用的服务账户组,已回退为 root:root 且权限 644"
}
render_userpass_block() {
local indent="${1:-4}"
local prefix="" entry username password
prefix="$(printf '%*s' "${indent}" '')"
for entry in "${USERPASS_ENTRIES[@]}"; do
username="${entry%%:*}"
password="${entry#*:}"
printf '%s%s: %s\n' "${prefix}" "${username}" "$(yaml_quote "$password")"
done
}
render_config() {
local domain="$1" local domain="$1"
local email="$2" local email="$2"
local cf_token="$3" local cf_token="$3"
local password="$4" local proxy_url="$4"
local proxy_url="$5"
cat <<EOF
listen: :443
acme:
domains:
- $(yaml_quote "$domain")
email: $(yaml_quote "$email")
type: dns
dns:
name: cloudflare
config:
cloudflare_api_token: $(yaml_quote "$cf_token")
auth:
type: userpass
userpass:
EOF
render_userpass_block 4
cat <<EOF
trafficStats:
listen: 127.0.0.1:9999
secret: $(yaml_quote "$TRAFFIC_STATS_SECRET")
disableUDP: false
udpIdleTimeout: 90s
resolver:
type: https
https:
addr: 1.1.1.1:443
timeout: 10s
sni: cloudflare-dns.com
insecure: false
speedTest: true
masquerade:
type: proxy
proxy:
url: $(yaml_quote "$proxy_url")
rewriteHost: true
insecure: false
EOF
}
render_preview_config() {
local domain="$1"
local email="$2"
local cf_token="$3"
local proxy_url="$4"
local masked_token local masked_token
if ! validate_domain "$domain"; then if ! validate_domain "$domain"; then
@@ -383,32 +485,23 @@ write_config() {
fi fi
masked_token="$(mask_secret "$cf_token")" masked_token="$(mask_secret "$cf_token")"
render_config "$domain" "$email" "$masked_token" "$proxy_url"
}
write_config() {
local domain="$1"
local email="$2"
local cf_token="$3"
local proxy_url="$4"
if ! validate_domain "$domain"; then
red "域名无效,拒绝写入配置:${domain@Q}"
exit 1
fi
echo echo
blue "==> 即将写入如下配置到 ${CONFIG_FILE}" blue "==> 即将写入如下配置到 ${CONFIG_FILE}"
cat <<EOF render_preview_config "$domain" "$email" "$cf_token" "$proxy_url"
listen: :443
acme:
domains:
- ${domain}
email: ${email}
type: dns
dns:
name: cloudflare
config:
cloudflare_api_token: ${masked_token}
auth:
type: password
password: ${password}
masquerade:
type: proxy
proxy:
url: ${proxy_url}
rewriteHost: true
EOF
echo echo
if ! confirm_yn "是否确认写入配置文件?"; then if ! confirm_yn "是否确认写入配置文件?"; then
@@ -419,32 +512,9 @@ EOF
mkdir -p /etc/hysteria mkdir -p /etc/hysteria
backup_existing_config backup_existing_config
cat > "${CONFIG_FILE}" <<EOF render_config "$domain" "$email" "$cf_token" "$proxy_url" > "${CONFIG_FILE}"
listen: :443
acme: set_config_permissions
domains:
- ${domain}
email: ${email}
type: dns
dns:
name: cloudflare
config:
cloudflare_api_token: ${cf_token}
auth:
type: password
password: ${password}
masquerade:
type: proxy
proxy:
url: ${proxy_url}
rewriteHost: true
EOF
chown root:root "${CONFIG_FILE}"
chmod 644 "${CONFIG_FILE}"
green "配置已写入 ${CONFIG_FILE}" green "配置已写入 ${CONFIG_FILE}"
} }
@@ -463,25 +533,34 @@ start_service() {
show_result() { show_result() {
local domain="$1" local domain="$1"
local password="$2" local proxy_url="$2"
local proxy_url="$3" local ip_info="$3"
local ip_info="$4"
local ipv4="${ip_info%%|*}" local ipv4="${ip_info%%|*}"
local ipv6="${ip_info##*|}" local ipv6="${ip_info##*|}"
local entry username password
local share_link="hysteria2://${password}@${domain}:443/?sni=${domain}&insecure=0"
echo echo
green "================= HY2 节点信息 =================" green "================= HY2 节点信息 ================="
echo "域名: ${domain}" echo "域名: ${domain}"
echo "端口: 443" echo "端口: 443"
echo "密码: ${password}" echo "认证方式: userpass"
echo "伪装站点: ${proxy_url}" echo "伪装站点: ${proxy_url}"
echo "流量统计监听: 127.0.0.1:9999"
echo "流量统计密钥: ${TRAFFIC_STATS_SECRET}"
[[ -n "${ipv4}" ]] && echo "IPv4: ${ipv4}" [[ -n "${ipv4}" ]] && echo "IPv4: ${ipv4}"
[[ -n "${ipv6}" ]] && echo "IPv6: ${ipv6}" [[ -n "${ipv6}" ]] && echo "IPv6: ${ipv6}"
echo echo
echo "代理链接:" echo "客户端连接参数:"
echo "${share_link}" echo " server: ${domain}:443"
echo " sni: ${domain}"
echo " auth: userpass"
echo
echo "设备账号:"
for entry in "${USERPASS_ENTRIES[@]}"; do
username="${entry%%:*}"
password="${entry#*:}"
echo " ${username}: ${password}"
done
echo echo
echo "hysteria 状态:" echo "hysteria 状态:"
systemctl --no-pager --full status "${SERVICE_NAME}" || true systemctl --no-pager --full status "${SERVICE_NAME}" || true
@@ -509,7 +588,7 @@ main() {
require_cmd python3 require_cmd python3
require_python3_json || { red "python3 缺少 json 模块,无法解析 Cloudflare API 返回值。"; exit 1; } require_python3_json || { red "python3 缺少 json 模块,无法解析 Cloudflare API 返回值。"; exit 1; }
local email zone subdomain proxy_url password ip_info ipv4 ipv6 domain local email zone subdomain default_subdomain proxy_url ip_info ipv4 ipv6 domain username
email="$(prompt_nonempty '请输入 ACME 邮箱: ')" email="$(prompt_nonempty '请输入 ACME 邮箱: ')"
zone="$(prompt_nonempty '请输入 Cloudflare Zone例如 example.com: ')" zone="$(prompt_nonempty '请输入 Cloudflare Zone例如 example.com: ')"
@@ -522,7 +601,11 @@ main() {
CF_API_TOKEN="$(prompt_nonempty '请输入 Cloudflare API Token: ')" CF_API_TOKEN="$(prompt_nonempty '请输入 Cloudflare API Token: ')"
export CF_API_TOKEN export CF_API_TOKEN
password="$(generate_password)" USERPASS_ENTRIES=()
for username in "${DEVICE_USERS[@]}"; do
USERPASS_ENTRIES+=("${username}:$(generate_password 36)")
done
TRAFFIC_STATS_SECRET="$(generate_password 36)"
ip_info="$(get_server_ip)" ip_info="$(get_server_ip)"
ipv4="${ip_info%%|*}" ipv4="${ip_info%%|*}"
@@ -549,9 +632,9 @@ main() {
domain="$(prompt_nonempty '请输入已存在并已解析到本机的完整域名: ')" domain="$(prompt_nonempty '请输入已存在并已解析到本机的完整域名: ')"
fi fi
write_config "${domain}" "${email}" "${CF_API_TOKEN}" "${password}" "${proxy_url}" write_config "${domain}" "${email}" "${CF_API_TOKEN}" "${proxy_url}"
start_service start_service
show_result "${domain}" "${password}" "${proxy_url}" "${ip_info}" show_result "${domain}" "${proxy_url}" "${ip_info}"
} }
main "$@" main "$@"