From 4ea89a8e8f7607ea770a946fff653bcfbcf8d3ae Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Sun, 15 Mar 2026 15:21:05 +0800 Subject: [PATCH 01/19] Update install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3b7aef9..8c6fe9d 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ set -euo pipefail # 基础参数 # ========================= Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}" +Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" Service_Name="clash-for-linux" Service_User="root" Service_Group="root" From 63755f5e05b7a638859315b22d65889a3a3a7a2c Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Mon, 16 Mar 2026 21:56:32 +0800 Subject: [PATCH 02/19] Update install_systemd.sh --- scripts/install_systemd.sh | 57 +++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/scripts/install_systemd.sh b/scripts/install_systemd.sh index 04b99e9..b581131 100755 --- a/scripts/install_systemd.sh +++ b/scripts/install_systemd.sh @@ -10,47 +10,63 @@ Service_User="root" Service_Group="root" Unit_Path="/etc/systemd/system/${Service_Name}.service" -PID_FILE="$Server_Dir/temp/clash.pid" +Env_File="$Server_Dir/temp/clash-for-linux.sh" #################### 权限检查 #################### if [ "$(id -u)" -ne 0 ]; then - echo -e "\033[31m[ERROR] 需要 root 权限来安装 systemd 单元\033[0m" + echo -e "[ERROR] 需要 root 权限来安装 systemd 单元" exit 1 fi #################### 目录初始化 #################### -install -d -m 0755 \ - "$Server_Dir/conf" \ - "$Server_Dir/logs" \ - "$Server_Dir/temp" +install -d -m 0755 "$Server_Dir/conf" "$Server_Dir/logs" "$Server_Dir/temp" + +# 预创建 env 文件,避免 systemd 因路径不存在报错 +: > "$Env_File" +chmod 0644 "$Env_File" #################### 生成 systemd Unit #################### -cat >"$Unit_Path"<"$Unit_Path" </dev/null 2>&1 || true -echo -e "\033[32m[OK] 已生成 systemd 单元: ${Unit_Path}\033[0m" -echo -e "可执行以下命令启动服务:" -echo -e " sudo systemctl enable --now ${Service_Name}.service" +echo -e "[OK] 已生成 systemd 单元: ${Unit_Path}" +echo -e "已启用开机自启,可执行以下命令启动服务:" +echo -e " systemctl restart ${Service_Name}.service" +echo -e "查看状态:" +echo -e " systemctl status ${Service_Name}.service -l --no-pager" From 8c5a016dab9e51fbb18f43a76c0f29acf3eeb904 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Mon, 16 Mar 2026 21:56:34 +0800 Subject: [PATCH 03/19] Update install.sh --- install.sh | 1342 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 836 insertions(+), 506 deletions(-) diff --git a/install.sh b/install.sh index 8c6fe9d..4dad35e 100755 --- a/install.sh +++ b/install.sh @@ -1,574 +1,904 @@ -#!/bin/bash +#!/usr/bin/env bash +# 严格模式 set -euo pipefail -# ========================= -# 基础参数 -# ========================= -Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" -Service_Name="clash-for-linux" -Service_User="root" -Service_Group="root" +# --- DEBUG: 打印具体失败的行号和命令(systemd 下非常关键) --- +trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR +# 如需更详细:取消下一行注释 +# set -x +# --- DEBUG end --- -# ========================= -# 彩色输出(统一 printf + 自动降级 + 手动关色) -# ========================= +############################################ +# Clash for Linux - start.sh (Full Version) +# - systemd 模式下订阅失败/下载失败:不退出,使用 conf/config.yaml(必要时从 conf/fallback_config.yaml 拷贝)兜底启动 +# - 非 systemd 模式:订阅失败/下载失败直接退出(保持手动执行的强约束) +############################################ -# ---- 关色开关(优先级最高)---- -NO_COLOR_FLAG=0 -for arg in "$@"; do - case "$arg" in - --no-color|--nocolor) - NO_COLOR_FLAG=1 - ;; - esac -done +# 加载系统函数库(Only for RHEL Linux) +[ -f /etc/init.d/functions ] && source /etc/init.d/functions -if [[ -n "${NO_COLOR:-}" ]] || [[ -n "${CLASH_NO_COLOR:-}" ]]; then - NO_COLOR_FLAG=1 +#################### 脚本初始化任务 #################### + +# 获取脚本工作目录绝对路径 +export Server_Dir +Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 加载.env变量文件 +# shellcheck disable=SC1090 +# --- source .env(不可信输入,必须放宽) --- +if [ -f "$Server_Dir/.env" ]; then + set +u + source "$Server_Dir/.env" || echo "[WARN] failed to source .env" >&2 + set -u fi -# ---- 初始化颜色 ---- -if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then - if tput setaf 1 >/dev/null 2>&1; then - C_RED="$(tput setaf 1)" - C_GREEN="$(tput setaf 2)" - C_YELLOW="$(tput setaf 3)" - C_BLUE="$(tput setaf 4)" - C_CYAN="$(tput setaf 6)" - C_GRAY="$(tput setaf 8 2>/dev/null || true)" - C_BOLD="$(tput bold)" - C_UL="$(tput smul)" - C_NC="$(tput sgr0)" - fi -fi +# systemd 模式开关(必须在 set -u 下安全) +SYSTEMD_MODE="${SYSTEMD_MODE:-false}" -# ---- ANSI fallback ---- -if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && [[ -z "${C_NC:-}" ]]; then - C_RED=$'\033[31m' - C_GREEN=$'\033[32m' - C_YELLOW=$'\033[33m' - C_BLUE=$'\033[34m' - C_CYAN=$'\033[36m' - C_GRAY=$'\033[90m' - C_BOLD=$'\033[1m' - C_UL=$'\033[4m' - C_NC=$'\033[0m' -fi - -# ---- 强制无色 ---- -if [[ "$NO_COLOR_FLAG" -eq 1 ]] || [[ ! -t 1 ]]; then - C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN='' C_GRAY='' C_BOLD='' C_UL='' C_NC='' -fi - -# ========================= -# 基础输出函数 -# ========================= -log() { printf "%b\n" "$*"; } -info() { log "${C_CYAN}[INFO]${C_NC} $*"; } -ok() { log "${C_GREEN}[OK]${C_NC} $*"; } -warn() { log "${C_YELLOW}[WARN]${C_NC} $*"; } -err() { log "${C_RED}[ERROR]${C_NC} $*"; } - -# ========================= -# 样式助手 -# ========================= -path() { printf "%b" "${C_BOLD}$*${C_NC}"; } -cmd() { printf "%b" "${C_GRAY}$*${C_NC}"; } -url() { printf "%b" "${C_UL}$*${C_NC}"; } -good() { printf "%b" "${C_GREEN}$*${C_NC}"; } -bad() { printf "%b" "${C_RED}$*${C_NC}"; } - -# ========================= -# 分段标题(CLI 风格 section) -# ========================= -section() { - local title="$*" - log "" - log "${C_BOLD}▶ ${title}${C_NC}" - log "${C_GRAY}────────────────────────────────────────${C_NC}" -} - -# ========================= -# 前置校验 -# ========================= +# root-only 强约束:不是 root 直接退出 if [ "$(id -u)" -ne 0 ]; then - err "需要 root 权限执行安装脚本(请使用 sudo bash install.sh)" - exit 1 + echo "[ERR] root-only mode: please run as root" >&2 + exit 2 fi -if [ ! -f "${Server_Dir}/.env" ]; then - err "未找到 .env 文件,请确认脚本所在目录:${Server_Dir}" - exit 1 +# 给二进制启动程序、脚本等添加可执行权限 +chmod +x "$Server_Dir/bin/"* 2>/dev/null || true +chmod +x "$Server_Dir/scripts/"* 2>/dev/null || true +if [ -f "$Server_Dir/tools/subconverter/subconverter" ]; then + chmod +x "$Server_Dir/tools/subconverter/subconverter" 2>/dev/null || true fi -# ========================= -# 同步到安装目录(保持你原逻辑) -# ========================= -mkdir -p "$Install_Dir" -if [ "$Server_Dir" != "$Install_Dir" ]; then - info "同步项目文件到安装目录:${Install_Dir}" - if command -v rsync >/dev/null 2>&1; then - rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/" - else - cp -a "$Server_Dir/." "$Install_Dir/" - fi -fi +#################### 变量设置 #################### -chmod +x "$Install_Dir"/*.sh 2>/dev/null || true -chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true -chmod +x "$Install_Dir"/bin/* 2>/dev/null || true -chmod +x "$Install_Dir"/clashctl 2>/dev/null || true +Conf_Dir="$Server_Dir/conf" -# ========================= -# 加载环境与依赖脚本 -# ========================= -# shellcheck disable=SC1090 -source "$Install_Dir/.env" -# shellcheck disable=SC1090 -source "$Install_Dir/scripts/get_cpu_arch.sh" -# shellcheck disable=SC1090 -source "$Install_Dir/scripts/resolve_clash.sh" -# shellcheck disable=SC1090 -source "$Install_Dir/scripts/port_utils.sh" +# root-only:统一使用安装目录下的 temp/logs +Temp_Dir="$Server_Dir/temp" +Log_Dir="$Server_Dir/logs" -if [[ -z "${CpuArch:-}" ]]; then - err "无法识别 CPU 架构" - exit 1 -fi -info "CPU architecture: ${CpuArch}" - -# ========================= -# .env 写入工具:write_env_kv(必须在 prompt 之前定义) -# - 自动创建文件 -# - 存在则替换,不存在则追加 -# - 统一写成:export KEY="VALUE" -# - 自动转义双引号/反斜杠 -# ========================= -escape_env_value() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +mkdir -p "$Conf_Dir" "$Temp_Dir" "$Log_Dir" || { + echo "[ERR] cannot create dirs: Conf_Dir=$Conf_Dir Temp_Dir=$Temp_Dir Log_Dir=$Log_Dir" + exit 2 } -write_env_kv() { - local file="$1" - local key="$2" - local val="$3" +# 再做一次可写性检查,避免后面玄学 exit +touch "$Temp_Dir/.write_test" 2>/dev/null || { echo "[ERR] Temp_Dir not writable: $Temp_Dir"; exit 2; } +rm -f "$Temp_Dir/.write_test" 2>/dev/null || true - mkdir -p "$(dirname "$file")" 2>/dev/null || true - [ -f "$file" ] || touch "$file" +PID_FILE="${CLASH_PID_FILE:-$Temp_Dir/clash.pid}" - val="$(printf '%s' "$val" | tr -d '\r')" - local esc - esc="$(escape_env_value "$val")" - - if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file"; then - sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*|export ${key}=\"${esc}\"|g" "$file" - else - printf 'export %s="%s"\n' "$key" "$esc" >> "$file" - fi -} - -# ========================= -# 交互式填写订阅地址(仅在 CLASH_URL 为空时触发) -# - 若非 TTY(CI/管道)则跳过交互 -# - 若用户回车跳过,则保持原行为:装完提示手动配置 -# ========================= -prompt_clash_url_if_empty() { - # 兼容 .env 里可能是 CLASH_URL= / export CLASH_URL= / 带引号 - local cur="${CLASH_URL:-}" - cur="${cur%\"}"; cur="${cur#\"}" - - if [ -n "$cur" ]; then - return 0 - fi - - # 非交互环境:不阻塞 - if [ ! -t 0 ]; then - warn "CLASH_URL 为空且当前为非交互环境(stdin 非 TTY),将跳过输入引导。" - return 0 - fi - - echo - warn "未检测到订阅地址(CLASH_URL 为空)" - echo "请粘贴你的 Clash 订阅地址(直接回车跳过,稍后手动编辑 .env):" - read -r -p "Clash URL: " input_url - - input_url="$(printf '%s' "$input_url" | tr -d '\r')" - - # 回车跳过:保持原行为(不写入) - if [ -z "$input_url" ]; then - warn "已跳过填写订阅地址,安装完成后请手动编辑:${Install_Dir}/.env" - return 0 - fi - - # 先校验再写入,避免污染 .env - if ! echo "$input_url" | grep -Eq '^https?://'; then - err "订阅地址格式不正确(必须以 http:// 或 https:// 开头)" - exit 1 - fi - - ENV_FILE="${Install_Dir}/.env" - mkdir -p "$Install_Dir" - [ -f "$ENV_FILE" ] || touch "$ENV_FILE" - - # ✅ 只用这一套写入逻辑(统一 export KEY="...",兼容旧格式) - write_env_kv "$ENV_FILE" "CLASH_URL" "$input_url" - - export CLASH_URL="$input_url" - ok "已写入订阅地址到:${ENV_FILE}" -} - -prompt_clash_url_if_empty - -# ========================= -# 端口冲突检测(保持你原逻辑) -# ========================= -CLASH_HTTP_PORT=${CLASH_HTTP_PORT:-7890} -CLASH_SOCKS_PORT=${CLASH_SOCKS_PORT:-7891} -CLASH_REDIR_PORT=${CLASH_REDIR_PORT:-7892} -EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} - -parse_port() { - local raw="$1" - raw="${raw##*:}" - echo "$raw" -} - -Port_Conflicts=() -for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_port "$EXTERNAL_CONTROLLER")"; do - if [ "$port" = "auto" ] || [ -z "$port" ]; then - continue - fi - if [[ "$port" =~ ^[0-9]+$ ]]; then - if is_port_in_use "$port"; then - Port_Conflicts+=("$port") - fi - fi -done - -if [ "${#Port_Conflicts[@]}" -ne 0 ]; then - warn "检测到端口冲突: ${Port_Conflicts[*]},运行时将自动分配可用端口" -fi - -install -d -m 0755 "$Install_Dir/conf" "$Install_Dir/logs" "$Install_Dir/temp" - -# ========================= -# Clash 内核就绪检查/下载 -# ========================= -if ! resolve_clash_bin "$Install_Dir" "$CpuArch" >/dev/null 2>&1; then - err "Clash 内核未就绪,请检查下载配置或手动放置二进制" - exit 1 -fi - -# ========================= -# fonction 工具函数区 -# ========================= -# 等待 config.yaml 出现并写入 secret(默认最多等 6 秒) -wait_secret_ready() { - local conf_file="$1" - local timeout_sec="${2:-6}" - - local end=$((SECONDS + timeout_sec)) - while [ "$SECONDS" -lt "$end" ]; do - if [ -s "$conf_file" ] && grep -qE '^[[:space:]]*secret:' "$conf_file"; then +is_running() { + if [ -f "$PID_FILE" ]; then + local pid + pid="$(cat "$PID_FILE" 2>/dev/null || true)" + if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then return 0 fi - sleep 0.2 - done + fi return 1 } -# 计算字符串可视宽度:中文大概率按 2 宽处理(简单够用版) -# 注:终端宽度/字体不统一时,中文宽度估算永远只能“近似” -vis_width() { - python3 - <<'PY' "$1" -import sys -s=sys.argv[1] -w=0 -for ch in s: - # East Asian Wide/FullWidth 近似当 2 - w += 2 if ord(ch) >= 0x2E80 else 1 -print(w) -PY +if [ "${SYSTEMD_MODE:-false}" != "true" ] && is_running; then + echo -e "\n[OK] Clash 已在运行 (pid=$(cat "$PID_FILE")),跳过重复启动\n" + exit 0 +fi + +# systemd 模式下避免读取遗留 pid 干扰判断 +if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + rm -f "$PID_FILE" 2>/dev/null || true +fi + +# 统一订阅变量 +URL="${CLASH_URL:-}" + +# 清理可能的 CRLF(Windows 写 .env 很常见) +URL="$(printf '%s' "$URL" | tr -d '\r')" +URL="$(printf '%s' "$URL" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" + +#让 bash 子进程能拿到 +export CLASH_URL="$URL" + +# 只有在“需要在线更新订阅”的模式下才强制要求 URL +if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then + echo "[ERR] CLASH_URL 为空(未配置订阅地址)" + exit 2 +fi +if [ -n "$URL" ] && ! printf '%s' "$URL" | grep -Eq '^https?://'; then + echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2 + exit 2 +fi + +# 获取 CLASH_SECRET 值:优先 .env;其次读取旧 config;占位符视为无效;最后生成随机值 +Secret="${CLASH_SECRET:-}" + +# 尝试从旧 config.yaml 读取(仅当 .env 未提供) +if [ -z "$Secret" ] && [ -f "$Conf_Dir/config.yaml" ]; then + Secret="$(awk -F': *' '/^[[:space:]]*secret[[:space:]]*:/{print $2; exit}' "$Conf_Dir/config.yaml" 2>/dev/null | tr -d '"' || true)" +fi + +# 若读取到的是占位符(如 ${CLASH_SECRET}),视为无效 +if [[ "$Secret" =~ ^\$\{.*\}$ ]]; then + Secret="" +fi + +# 兜底生成随机 secret +if [ -z "$Secret" ]; then + if command -v openssl >/dev/null 2>&1; then + Secret="$(openssl rand -hex 32)" + else + # 32 bytes -> 64 hex chars + Secret="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" + fi +fi + +# 强制写入 secret 到指定配置文件(存在则替换,不存在则追加) +force_write_secret() { + local file="$1" + [ -f "$file" ] || return 0 + + if grep -qE '^[[:space:]]*secret:' "$file"; then + # 替换整行 secret(无论原来是啥,包括 SECRET_PLACEHOLDER / "${CLASH_SECRET}") + sed -i -E "s|^[[:space:]]*secret:.*$|secret: ${Secret}|g" "$file" + else + # 没有 secret 行就追加到文件末尾 + printf "\nsecret: %s\n" "$Secret" >> "$file" + fi } -pad_right() { # pad_right "text" width - local s="$1" w="$2" - local cur - cur="$(vis_width "$s")" - local pad=$(( w - cur )) - (( pad < 0 )) && pad=0 - printf "%s%*s" "$s" "$pad" "" +ensure_ui_link() { + mkdir -p "$Conf_Dir" + ln -sfn "$Server_Dir/dashboard/public" "$Conf_Dir/ui" } -box_title() { # box_title "标题" width - local title="$1" width="$2" - local inner=$((width-2)) - printf "┌%s┐\n" "$(printf '─%.0s' $(seq 1 $inner))" - # 标题居中(近似) - local t=" $title " - local tw; tw="$(vis_width "$t")" - local left=$(( (inner - tw)/2 )); ((left<0)) && left=0 - local right=$(( inner - tw - left )); ((right<0)) && right=0 - printf "│%*s%s%*s│\n" "$left" "" "$t" "$right" "" - printf "├%s┤\n" "$(printf '─%.0s' $(seq 1 $inner))" +# --- helpers: upsert yaml key (top-level), ensure UI links --- +upsert_yaml_kv() { + # Usage: upsert_yaml_kv + # Writes: key: value (top-level) + local file="$1" key="$2" value="$3" + [ -n "$file" ] && [ -n "$key" ] || return 1 + + # 如果文件不存在,先创建 + [ -f "$file" ] || : >"$file" || return 1 + + if grep -qE "^[[:space:]]*${key}:[[:space:]]*" "$file" 2>/dev/null; then + # 替换整行(避免残留引号) + sed -i -E "s|^[[:space:]]*${key}:[[:space:]]*.*$|${key}: ${value}|g" "$file" + else + # 追加前保证有换行 + tail -c 1 "$file" 2>/dev/null | read -r _last || true + # shellcheck disable=SC2034 + if [ "$(tail -c 1 "$file" 2>/dev/null || true)" != "" ]; then + printf "\n" >>"$file" + fi + printf "%s: %s\n" "$key" "$value" >>"$file" + fi } -box_row() { # box_row "key" "value" width keyw - local k="$1" v="$2" width="$3" keyw="$4" - local inner=$((width-2)) - # 形如:│ key: value │ - local left="$(pad_right "$k" "$keyw")" - local line=" ${left} ${v}" - local lw; lw="$(vis_width "$line")" - local pad=$(( inner - lw )); ((pad<0)) && pad=0 - printf "│%s%*s│\n" "$line" "$pad" "" +ensure_ui_links() { + local ui_src="${UI_SRC_DIR:-$Server_Dir/dashboard/public}" + mkdir -p "$Conf_Dir" 2>/dev/null || true + if [ -d "$ui_src" ]; then + ln -sfn "$ui_src" "$Conf_Dir/ui" 2>/dev/null || true + fi } -box_end() { # box_end width - local width="$1" inner=$((width-2)) - printf "└%s┘\n" "$(printf '─%.0s' $(seq 1 $inner))" +force_write_controller_and_ui() { + local file="$1" + local controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" + + [ -n "$file" ] || return 1 + + # external-controller + upsert_yaml_kv "$file" "external-controller" "$controller" || true + + # external-ui: fixed to Conf_Dir/ui + ensure_ui_links + if [ -e "$Conf_Dir/ui" ]; then + upsert_yaml_kv "$file" "external-ui" "$Conf_Dir/ui" || true + fi } -# 从 config.yaml 提取 secret(强韧:支持缩进/引号/CRLF/尾空格) -read_secret_from_config() { - local conf_file="$1" - [ -f "$conf_file" ] || return 1 - # 1) 找到 secret 行 -> 2) 去掉 key 和空格 -> 3) 去掉首尾引号 -> 4) 去掉 CR - local s - s="$( - sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "$conf_file" \ - | head -n 1 \ - | sed -E 's/^[[:space:]]*"(.*)"[[:space:]]*$/\1/; s/^[[:space:]]*'\''(.*)'\''[[:space:]]*$/\1/' \ - | tr -d '\r' - )" +fix_external_ui_by_safe_paths() { + local bin="$1" + local cfg="$2" + local test_out="$3" + local ui_src="${UI_SRC_DIR:-$Server_Dir/dashboard/public}" - # 去掉纯空格 - s="$(printf '%s' "$s" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" + [ -x "$bin" ] || return 0 + [ -s "$cfg" ] || return 0 - [ -n "$s" ] || return 1 - printf '%s' "$s" + # 先跑一次 test,把原因写入 test_out + "$bin" -t -f "$cfg" >"$test_out" 2>&1 + local rc=$? + [ $rc -eq 0 ] && return 0 + + # 只处理 external-ui 的 SAFE_PATH 报错 + if ! grep -q "SAFE_PATHS" "$test_out"; then + return $rc + fi + if ! grep -q "external-ui" "$cfg" && ! grep -q "external-ui" "$test_out"; then + return $rc + fi + + # 从 test_out 抽取 allowed paths 的第一个 base + # 例:allowed paths: [/opt/clash-for-linux/.config/mihomo] + local base + base="$(sed -n 's/.*allowed paths: \[\([^]]*\)\].*/\1/p' "$test_out" | head -n 1)" + + [ -n "$base" ] || return $rc + + # external-ui 必须在 allowed base 的子目录里 + local ui_dst="$base/ui" + mkdir -p "$ui_dst" 2>/dev/null || true + + # 把 UI 文件同步过去(真实目录,不用软链,避免跳出 base) + if [ -d "$ui_src" ]; then + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete "$ui_src"/ "$ui_dst"/ 2>/dev/null || true + else + rm -rf "$ui_dst"/* 2>/dev/null || true + cp -a "$ui_src"/. "$ui_dst"/ 2>/dev/null || true + fi + fi + + # 重写 external-ui 到新目录 + upsert_yaml_kv "$cfg" "external-ui" "$ui_dst" || true + + # 再 test 一次 + "$bin" -t -f "$cfg" >"$test_out" 2>&1 + return $? } -# 判断 systemd 是否可用(仅有 systemctl 命令但 PID 1 不是 systemd 时视为不可用) -systemd_ready() { - command -v systemctl >/dev/null 2>&1 || return 1 - systemctl show --property=Version --value >/dev/null 2>&1 || return 1 +# 设置默认值 +CLASH_HTTP_PORT="${CLASH_HTTP_PORT:-7890}" +CLASH_SOCKS_PORT="${CLASH_SOCKS_PORT:-7891}" +CLASH_REDIR_PORT="${CLASH_REDIR_PORT:-7892}" +CLASH_LISTEN_IP="${CLASH_LISTEN_IP:-127.0.0.1}" +CLASH_ALLOW_LAN="${CLASH_ALLOW_LAN:-false}" + +EXTERNAL_CONTROLLER_ENABLED="${EXTERNAL_CONTROLLER_ENABLED:-true}" +EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" + +ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}" + +# 端口与配置工具 +# shellcheck disable=SC1090 +source "$Server_Dir/scripts/port_utils.sh" +CLASH_HTTP_PORT="$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT")" +CLASH_SOCKS_PORT="$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT")" +CLASH_REDIR_PORT="$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT")" +EXTERNAL_CONTROLLER="$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "127.0.0.1")" + +# shellcheck disable=SC1090 +source "$Server_Dir/scripts/config_utils.sh" + +#################### 函数定义 #################### + +# 自定义action函数,实现通用action功能(兼容 journald;关键错误会额外 echo 到 stderr) +success() { + echo -en "\033[60G[\033[1;32m OK \033[0;39m]\r" return 0 } -# ========================= -# systemd 安装与启动 -# ========================= -Service_Enabled="unknown" -Service_Started="unknown" -Systemd_Usable="false" +failure() { + local rc=$? + echo -en "\033[60G[\033[1;31mFAILED\033[0;39m]\r" + [ -x /bin/plymouth ] && /bin/plymouth --details + return "$rc" +} -if systemd_ready; then - Systemd_Usable="true" +action() { + local STRING + STRING=$1 + shift + + # 执行命令本身的成功/失败,不应让 UI 输出影响返回码 + if "$@"; then + success $"$STRING" || true + return 0 + else + failure $"$STRING" || true + return 1 + fi +} + +# 判断命令是否正常执行 +# - 手动模式:失败直接 exit +# - systemd 模式:只打印状态,不影响退出码 +if_success() { + local ok_msg=$1 + local fail_msg=$2 + local rc=$3 + + if [ "$rc" -eq 0 ]; then + action "$ok_msg" /bin/true || true + return 0 + fi + + # rc != 0 + action "$fail_msg" /bin/false || true + + if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + # systemd 下不允许在 UI 函数中 exit + return "$rc" + else + exit "$rc" + fi +} + +ensure_subconverter() { + local bin="${Server_Dir}/tools/subconverter/subconverter" + local port="25500" + + # 没有二进制直接跳过 + if [ ! -x "$bin" ]; then + echo "[WARN] subconverter bin not found: $bin" + export SUBCONVERTER_READY="false" + return 0 + fi + + # 已在监听则认为就绪 + if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then + export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" + export SUBCONVERTER_READY="true" + return 0 + fi + + # 启动(后台) + echo "[INFO] starting subconverter..." + (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" >/dev/null 2>&1 &) + + # 等待端口起来 + for _ in 1 2 3 4 5; do + sleep 1 + if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then + export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" + export SUBCONVERTER_READY="true" + echo "[OK] subconverter ready at ${SUBCONVERTER_URL}" + return 0 + fi + done + + echo "[WARN] subconverter start failed or port not ready" + export SUBCONVERTER_READY="false" + return 0 +} + +#################### 任务执行 #################### + +## 获取CPU架构信息 +# shellcheck disable=SC1090 +source "$Server_Dir/scripts/get_cpu_arch.sh" + +if [[ -z "${CpuArch:-}" ]]; then + echo "[ERROR] Failed to obtain CPU architecture" >&2 + exit 2 fi -if [ "$Systemd_Usable" = "true" ]; then - if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ] || [ "${CLASH_START_SERVICE:-true}" = "true" ]; then - CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh" +# shellcheck disable=SC1090 +source "$Server_Dir/scripts/resolve_clash.sh" - if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then - systemctl enable "${Service_Name}.service" >/dev/null 2>&1 || true - fi - if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then - systemctl start "${Service_Name}.service" >/dev/null 2>&1 || true - fi +## 临时取消环境变量 +unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY || true - if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then - Service_Enabled="enabled" +######################################################## +# systemd 兜底:如果没有可用订阅 URL,则确保有 config.yaml +######################################################## +ensure_fallback_config() { + # conf/config.yaml 为空或不存在,则从 fallback 拷贝 + if [ ! -s "$Conf_Dir/config.yaml" ]; then + if [ -s "$Server_Dir/conf/fallback_config.yaml" ]; then + cp -f "$Server_Dir/conf/fallback_config.yaml" "$Conf_Dir/config.yaml" + echo -e "\033[33m[WARN]\033[0m 已复制 fallback_config.yaml -> conf/config.yaml(兜底)" else - Service_Enabled="disabled" + echo -e "\033[31m[ERROR]\033[0m 未找到可用的 conf/fallback_config.yaml,无法兜底启动" >&2 + if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + return 1 + else + exit 1 + fi fi + fi - if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then - Service_Started="active" + # 强制写入真实 secret(失败时也遵循同样规则) + if ! force_write_secret "$Conf_Dir/config.yaml"; then + echo -e "\033[31m[ERROR]\033[0m 写入 secret 失败:$Conf_Dir/config.yaml" >&2 + if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + return 1 else - Service_Started="inactive" + exit 1 fi + fi + + return 0 +} +SKIP_CONFIG_REBUILD=false + +# systemd 模式下若 URL 为空:直接兜底启动 +if [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; then + echo -e "\033[33m[WARN]\033[0m SYSTEMD_MODE=true 且 CLASH_URL 为空,跳过订阅更新,使用本地兜底配置启动" + ensure_fallback_config || true + SKIP_CONFIG_REBUILD=true +fi + +#################### Clash 订阅地址检测及配置文件下载 #################### +if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then + echo -e '\n正在检测订阅地址...' + Text1="Clash订阅地址可访问!" + Text2="Clash订阅地址不可访问!" + + CHECK_CMD=(curl -o /dev/null -L -sS --retry 5 -m 10 --connect-timeout 10 -w "%{http_code}") + if [ "$ALLOW_INSECURE_TLS" = "true" ]; then + CHECK_CMD+=(-k) + echo -e "\033[33m[WARN]\033[0m 已启用不安全的 TLS 下载(跳过证书校验)" + fi + if [ -n "${CLASH_HEADERS:-}" ]; then + CHECK_CMD+=(-H "$CLASH_HEADERS") + fi + CHECK_CMD+=("$URL") + + # 不让 set -e 干扰获取状态码 + set +e + status_code="$("${CHECK_CMD[@]}")" + curl_rc=$? + set -e + + # curl 本身失败,视为不可用 + if [ "$curl_rc" -ne 0 ]; then + status_code="" + ReturnStatus=1 else - info "已按配置跳过 systemd 服务安装与启动(CLASH_ENABLE_SERVICE=false 且 CLASH_START_SERVICE=false)" - Service_Enabled="disabled" - Service_Started="inactive" + echo "$status_code" | grep -E '^[23][0-9]{2}$' &>/dev/null + ReturnStatus=$? + fi + + if [ "$ReturnStatus" -eq 0 ]; then + action "$Text1" /bin/true || true + else + if [ "$SYSTEMD_MODE" = "true" ]; then + action "$Text2(systemd 模式不退出,尝试使用旧配置/兜底配置)" /bin/false || true + echo -e "\033[33m[WARN]\033[0m Subscribe check failed: http_code=${status_code:-unknown}, url=${URL}" >&2 + ensure_fallback_config || true + SKIP_CONFIG_REBUILD=true + else + if_success "$Text1" "$Text2" "$ReturnStatus" + fi + fi +fi + +#################### 下载订阅并生成 config.yaml(非兜底路径) #################### +if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then + ensure_subconverter || true + echo -e '\n正在下载Clash配置文件...' + Text3="配置文件clash.yaml下载成功!" + Text4="配置文件clash.yaml下载失败!" + + # --- DBG: 显式打印并验证临时目录可写(systemd 下常见权限问题) --- + echo "[DBG] uid=$(id -u) user=$(id -un) SYSTEMD_MODE=${SYSTEMD_MODE:-}" + echo "[DBG] Server_Dir=$Server_Dir Conf_Dir=$Conf_Dir Temp_Dir=$Temp_Dir Log_Dir=$Log_Dir" + echo "[DBG] URL=$(printf '%q' "$URL")" + + mkdir -p "$Temp_Dir" 2>/dev/null || true + touch "$Temp_Dir/.write_test" 2>/dev/null || { echo "[ERR] Temp_Dir not writable: $Temp_Dir" >&2; exit 2; } + rm -f "$Temp_Dir/.write_test" 2>/dev/null || true + # --- DBG end --- + + CURL_CMD=(curl -fL -S --retry 2 --connect-timeout 10 -m 30 -o "$Temp_Dir/clash.yaml") + if [ "$ALLOW_INSECURE_TLS" = "true" ]; then + CURL_CMD+=(-k) + fi + if [ -n "${CLASH_HEADERS:-}" ]; then + CURL_CMD+=(-H "$CLASH_HEADERS") + fi + CURL_CMD+=("$URL") + + set +e + CURL_ERR="$Temp_Dir/curl.err" + : > "$CURL_ERR" + "${CURL_CMD[@]}" 2>>"$CURL_ERR" + ReturnStatus=$? + set -e + + echo "[DBG] curl rc=$ReturnStatus" + if [ -s "$CURL_ERR" ]; then + echo "[DBG] curl stderr (last 50 lines):" + tail -n 50 "$CURL_ERR" + fi + + if [ "$ReturnStatus" -ne 0 ]; then + WGET_CMD=(wget -q -O "$Temp_Dir/clash.yaml") + if [ "$ALLOW_INSECURE_TLS" = "true" ]; then + WGET_CMD+=(--no-check-certificate) + fi + if [ -n "${CLASH_HEADERS:-}" ]; then + WGET_CMD+=(--header="$CLASH_HEADERS") + fi + WGET_CMD+=("$URL") + + for _ in {1..10}; do + set +e + "${WGET_CMD[@]}" + ReturnStatus=$? + set -e + if [ "$ReturnStatus" -eq 0 ]; then + break + fi + done + fi + + CONFIG_FILE="${CONFIG_FILE:-$Temp_Dir/config.yaml}" + mkdir -p "$Temp_Dir" || true + + if [ "$ReturnStatus" -eq 0 ] && [ -s "$Temp_Dir/clash.yaml" ]; then + SRC_YAML="$Temp_Dir/clash.yaml" + + # 1) 判断是否是完整 Clash 配置(关键字段之一存在即可) + if grep -qE '^(proxies:|proxy-providers:|rules:|port:|mixed-port:|dns:)' "$SRC_YAML"; then + cp -f "$SRC_YAML" "$CONFIG_FILE" + echo "[INFO] subscription already is a full clash config" + else + # 2) 非完整配置:尝试用 subconverter 转换 + echo "[INFO] subscription is not a full config, try conversion via subconverter..." + + export IN_FILE="$SRC_YAML" + export OUT_FILE="$Temp_Dir/clash_converted.yaml" + + set +e + bash "$Server_Dir/scripts/clash_profile_conversion.sh" + conv_rc=$? + set -e + + if [ "$conv_rc" -eq 0 ] && [ -s "$OUT_FILE" ]; then + cp -f "$OUT_FILE" "$CONFIG_FILE" + echo "[INFO] conversion ok -> runtime config ready" + else + echo "[WARN] conversion skipped/failed, will keep original and rely on fallback" + cp -f "$SRC_YAML" "$CONFIG_FILE" + fi + fi + + # 3) 强制注入 external-controller / external-ui(运行态兜底) + force_write_controller_and_ui "$CONFIG_FILE" || true + + # 4) 强制注入 secret + force_write_secret "$CONFIG_FILE" || true + + # Optional: Fix test URLs to HTTPS for reliability (safe, narrow scope) + if [ "${FIX_TEST_URL_HTTPS:-true}" = "true" ] && [ -s "$CONFIG_FILE" ]; then + # 1) proxy-groups: url-test / fallback url + sed -i -E "s#(url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true + + # 2) cfw-latency-url (some dashboards) + sed -i -E "s#(cfw-latency-url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true + + # 3) proxy-providers health-check url (mihomo warns about this) + sed -i -E "s#(health-check:[[:space:]]*\n[[:space:]]*url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true + fi + + # 5) 自检:失败则回退到旧配置(注意:脚本 set -e + trap ERR,必须 set +e 包裹) + BIN="${Server_Dir}/bin/clash-linux-amd64" + NEW_CFG="$CONFIG_FILE" + OLD_CFG="${Conf_Dir}/config.yaml" + TEST_OUT="$Temp_Dir/config.test.out" + + if [ -x "$BIN" ] && [ -f "$NEW_CFG" ]; then + # 先尝试自动修复 external-ui 的 SAFE_PATH 问题(内部会跑 -t) + set +e + fix_external_ui_by_safe_paths "$BIN" "$NEW_CFG" "$TEST_OUT" + test_rc=$? + set -e + + if [ "$test_rc" -ne 0 ]; then + echo "[ERROR] Generated config invalid, rc=$test_rc, reason(file=$TEST_OUT, size=$(wc -c <"$TEST_OUT" 2>/dev/null || echo 0))" >&2 + tail -n 120 "$TEST_OUT" >&2 || true + + echo "[ERROR] fallback to last good config: $OLD_CFG" >&2 + if [ -f "$OLD_CFG" ]; then + cp -f "$OLD_CFG" "$NEW_CFG" + else + echo "[FATAL] No valid config available, aborting startup" >&2 + exit 1 + fi + fi + fi + + echo "[INFO] Runtime config generated: $CONFIG_FILE (size=$(wc -c <"$CONFIG_FILE" 2>/dev/null || echo 0))" + else + echo "[WARN] Download did not produce clash.yaml (rc=$ReturnStatus), skip runtime config generation" >&2 + fi + + if [ "$ReturnStatus" -eq 0 ]; then + action "$Text3" /bin/true || true + else + if [ "$SYSTEMD_MODE" = "true" ]; then + action "$Text4(systemd 模式:下载失败,使用旧配置/兜底配置继续启动)" /bin/false || true + echo -e "\033[33m[WARN]\033[0m Download failed, will fallback. url=${URL}" >&2 + ensure_fallback_config || true + SKIP_CONFIG_REBUILD=true + else + if_success "$Text3" "$Text4(退出启动)" "$ReturnStatus" + fi + fi +fi + +# ========================================================= +# 判断订阅是否已是完整 Clash YAML(Meta / Mihomo / Premium) +# 若是完整配置,则直接使用,跳过后续代理拆解与拼接 +# ========================================================= +if grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$Temp_Dir/clash.yaml"; then + echo "[INFO] subscription is a full Clash config, use it directly" + cp -f "$Temp_Dir/clash.yaml" "$Conf_Dir/config.yaml" + + # 生成运行态(systemd non-root 实际启动用 Temp_Dir/config.yaml) + cp -f "$Temp_Dir/clash.yaml" "$Temp_Dir/config.yaml" + + # 写 controller/ui + secret(写到运行态) + force_write_controller_and_ui "$Temp_Dir/config.yaml" || true + force_write_secret "$Temp_Dir/config.yaml" || true + + # 同时把 conf/config.yaml 也补齐(方便你 grep/排查) + force_write_controller_and_ui "$Conf_Dir/config.yaml" || true + force_write_secret "$Conf_Dir/config.yaml" || true + + # 创建 UI 软链(systemd non-root 用 /tmp) + Dashboard_Src="$Server_Dir/dashboard/public" + if [ -d "$Dashboard_Src" ]; then + ln -sfn "$Dashboard_Src" "$Conf_Dir/ui" 2>/dev/null || true + fi + + SKIP_CONFIG_REBUILD=true + fi + +#################### 订阅转换/拼接(非兜底路径) #################### +if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then + # 运行期配置文件:默认用 Temp_Dir(systemd + clash 用户可写) + CONFIG_FILE="$Temp_Dir/config.yaml" + + # 1) 重命名订阅文件 + \cp -a "$Temp_Dir/clash.yaml" "$Temp_Dir/clash_config.yaml" + + # 2) 判断订阅内容是否符合 clash 配置文件标准,尝试转换(需 subconverter) + # shellcheck disable=SC1090 + source "$Server_Dir/scripts/resolve_subconverter.sh" + + if [ "${Subconverter_Ready:-false}" = "true" ]; then + echo -e '\n判断订阅内容是否符合clash配置文件标准:' + export SUBCONVERTER_BIN="$Subconverter_Bin" + bash "$Server_Dir/scripts/clash_profile_conversion.sh" + sleep 1 + else + echo -e "\033[33m[WARN]\033[0m 未检测到可用的 subconverter,跳过订阅转换" + fi + + # 3) 订阅形态判断: + # - 如果已经是完整 Clash 配置(Meta/Mihomo 常见 mixed-port / proxy-providers 等),直接用它作为运行配置 + # - 否则才走 “proxies: 抽取 + template 拼接” + if grep -qE '^(mixed-port:|port:|proxy-providers:|proxies:)' "$Temp_Dir/clash_config.yaml"; then + # 情况 A:完整配置(优先) + if grep -q '^proxies:' "$Temp_Dir/clash_config.yaml" || grep -q '^proxy-providers:' "$Temp_Dir/clash_config.yaml" || grep -q '^mixed-port:' "$Temp_Dir/clash_config.yaml" || grep -q '^port:' "$Temp_Dir/clash_config.yaml"; then + echo "[INFO] subscription looks like a full Clash config, use it directly" + cp -f "$Temp_Dir/clash_config.yaml" "$CONFIG_FILE" + # 写入 secret(运行态) + force_write_secret "$CONFIG_FILE" + # 直接跳过后续拼接流程 + SKIP_CONFIG_REBUILD=true + fi + fi + + # 情况 B:不是完整配置,才尝试抽取 proxies 并拼接 + if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then + if grep -q '^proxies:' "$Temp_Dir/clash_config.yaml"; then + sed -n '/^proxies:/,$p' "$Temp_Dir/clash_config.yaml" > "$Temp_Dir/proxy.txt" + else + echo "[ERROR] subscription is not a full config and also has no 'proxies:'; cannot build config." >&2 + # systemd 模式:兜底继续;非 systemd:退出 + if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + ensure_fallback_config || true + SKIP_CONFIG_REBUILD=true + else + exit 2 + fi + fi + fi + + # 4) 合并形成新的 config,并替换配置占位符 + if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then + cat "$Temp_Dir/templete_config.yaml" > "$CONFIG_FILE" + cat "$Temp_Dir/proxy.txt" >> "$CONFIG_FILE" + + sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$CONFIG_FILE" + sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$CONFIG_FILE" + sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$CONFIG_FILE" + sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$CONFIG_FILE" + sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$CONFIG_FILE" + fi + + # 5) 配置 external-controller + if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then + sed -i "s/EXTERNAL_CONTROLLER_PLACEHOLDER/${EXTERNAL_CONTROLLER}/g" "$CONFIG_FILE" + else + sed -i "s/external-controller: 'EXTERNAL_CONTROLLER_PLACEHOLDER'/# external-controller: disabled/g" "$CONFIG_FILE" + fi + + apply_tun_config "$CONFIG_FILE" + apply_mixin_config "$CONFIG_FILE" "$Server_Dir" + + # 6) 是否同步到 conf(root/非 systemd 时才做;systemd+非root跳过) + \cp "$CONFIG_FILE" "$Conf_Dir/" + + # 7) Dashboard external-ui(systemd+非root:把 ui 放 Temp_Dir 下,避免写 conf) + Work_Dir="$(cd "$(dirname "$0")" && pwd)" + Dashboard_Src="${Work_Dir}/dashboard/public" + + if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then + if [ "${SYSTEMD_MODE:-false}" = "true" ] && [ "$(id -u)" -ne 0 ]; then + # runtime ui path (writable) + Dashboard_Link="$Temp_Dir/ui" + if [ -d "$Dashboard_Src" ]; then + ln -sfn "$Dashboard_Src" "$Dashboard_Link" 2>/dev/null || true + fi + else + # conf ui path (root can manage) + Dashboard_Link="${Conf_Dir}/ui" + if [ -d "$Dashboard_Src" ]; then + ln -sfn "$Dashboard_Src" "$Dashboard_Link" || true + else + echo -e "\033[33m[WARN]\033[0m Dashboard source not found: $Dashboard_Src (external-ui may not work)" + fi + fi + + # ensure external-ui points to Dashboard_Link + if grep -qE '^[[:space:]]*external-ui:' "$CONFIG_FILE"; then + sed -i -E "s|^[[:space:]]*external-ui:.*$|external-ui: ${Dashboard_Link}|g" "$CONFIG_FILE" + else + printf "\nexternal-ui: %s\n" "$Dashboard_Link" >> "$CONFIG_FILE" + fi + fi + + # 8) 写入 secret(写到 runtime config) + force_write_secret "$CONFIG_FILE" + +else + # 兜底路径:尽量也写入 secret(conf/config.yaml 可写时) + if grep -qE '^secret:\s*' "$Conf_Dir/config.yaml" 2>/dev/null; then + force_write_secret "$Conf_Dir/config.yaml" + else + echo "secret: ${Secret}" >> "$Conf_Dir/config.yaml" || true + fi +fi + +#################### 启动Clash服务 #################### + +# 选择运行期配置文件与工作目录 +CONFIG_FILE="${CONFIG_FILE:-$Conf_Dir/config.yaml}" +RUNTIME_DIR="${Conf_Dir}" + +# 启动前确保配置文件存在且非空 +if [ ! -s "$CONFIG_FILE" ]; then + echo -e "\033[31m[ERROR]\033[0m config 不存在或为空:$CONFIG_FILE,无法启动 Clash" >&2 + exit 2 +fi + +# 最终护栏:禁止未渲染的占位符进入运行态 +if grep -q '\${' "$CONFIG_FILE"; then + echo "[ERROR] config contains unresolved placeholders (\${...}): $CONFIG_FILE" >&2 + exit 2 +fi + +# 确保运行目录存在且可写(clash/mihomo 可能会写 cache/geo 数据) +mkdir -p "$RUNTIME_DIR" 2>/dev/null || true +touch "$RUNTIME_DIR/.write_test" 2>/dev/null || { + echo "[ERROR] runtime dir not writable: $RUNTIME_DIR (uid=$(id -u))" >&2 + exit 2 +} +rm -f "$RUNTIME_DIR/.write_test" 2>/dev/null || true + +echo -e '\n正在启动Clash服务...' +Text5="服务启动成功!" +Text6="服务启动失败!" + +Clash_Bin="$(resolve_clash_bin "$Server_Dir" "$CpuArch")" +ReturnStatus=$? + +if [ "$ReturnStatus" -eq 0 ]; then + if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + echo "[INFO] SYSTEMD_MODE=true,前台启动交给 systemd 监管" + echo "[INFO] Using config: $CONFIG_FILE" + echo "[INFO] Using runtime dir: $RUNTIME_DIR" + + # systemd 前台:只用 -f 指定配置文件,-d 作为工作目录 + exec "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" + else + echo "[INFO] 后台启动 (nohup)" + echo "[INFO] Using config: $CONFIG_FILE" + echo "[INFO] Using runtime dir: $RUNTIME_DIR" + + nohup "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$Log_Dir/clash.log" 2>&1 & + PID=$! + ReturnStatus=$? + + if [ "$ReturnStatus" -eq 0 ]; then + echo "$PID" > "$PID_FILE" + fi + fi +fi + +if [ "${SYSTEMD_MODE:-false}" = "true" ]; then + if_success "$Text5" "$Text6" "$ReturnStatus" || true +else + if_success "$Text5" "$Text6" "$ReturnStatus" +fi + +#################### 输出信息 #################### + +echo '' +if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then + echo -e "Clash Dashboard 访问地址: http://${EXTERNAL_CONTROLLER}/ui" + + SHOW_SECRET="${CLASH_SHOW_SECRET:-false}" + SHOW_SECRET_MASKED="${CLASH_SHOW_SECRET_MASKED:-true}" + + if [ "$SHOW_SECRET" = "true" ]; then + echo -e "Secret: ${Secret}" + elif [ "$SHOW_SECRET_MASKED" = "true" ]; then + # 脱敏:前4后4 + masked="${Secret:0:4}****${Secret: -4}" + echo -e "Secret: ${masked} (set CLASH_SHOW_SECRET=true to show full)" + else + echo -e "Secret: 已生成(未显示)。查看:/opt/clash-for-linux/conf/config.yaml 或 .env" fi else - if command -v systemctl >/dev/null 2>&1; then - warn "检测到 systemctl 命令,但当前环境不可用 systemd(常见于 Docker 容器),已跳过服务单元生成" - else - warn "未检测到 systemd,已跳过服务单元生成" - fi + echo -e "External Controller (Dashboard) 已禁用" fi +echo '' -# ========================= -# Shell 代理快捷命令 -# 生成:/etc/profile.d/clash-for-linux.sh -# ========================= -PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" +#################### 写入代理环境变量文件 #################### -install_profiled() { - local http_port="${MIXED_PORT:-7890}" - # 兼容你后面可能支持 auto:auto 就先用 7890 - [ "$http_port" = "auto" ] && http_port="7890" +Env_File="${CLASH_ENV_FILE:-}" - # 只写 IPv4 loopback,避免某些环境 ::1 解析问题 - tee "$PROFILED_FILE" >/dev/null <"$Env_File"<}" - echo "https_proxy=\${https_proxy:-}" - echo "all_proxy=\${all_proxy:-}" +# 关闭系统代理 +function proxy_off() { + unset http_proxy + unset https_proxy + unset no_proxy + unset HTTP_PROXY + unset HTTPS_PROXY + unset NO_PROXY + echo -e "\033[31m[×] 已关闭代理\033[0m" } EOF - chmod 644 "$PROFILED_FILE" -} - -install_profiled || true - -# ========================= -# 安装 clashctl 命令 -# ========================= -if [ -f "$Install_Dir/clashctl" ]; then - install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl -fi - -# ========================= -# 友好收尾输出(闭环) -# ========================= - -section "安装完成" -ok "Clash for Linux 已安装至: $(path "${Install_Dir}")" - -log "📦 安装目录:$(path "${Install_Dir}")" -log "👤 运行用户:${Service_User}:${Service_Group}" -log "🔧 服务名称:${Service_Name}.service" - -if [ "$Systemd_Usable" = "true" ]; then - section "服务状态" - - se="${Service_Enabled:-unknown}" - ss="${Service_Started:-unknown}" - - [[ "$se" == "enabled" ]] && se_colored="$(good "$se")" || se_colored="$(bad "$se")" - [[ "$ss" == "active" ]] && ss_colored="$(good "$ss")" || ss_colored="$(bad "$ss")" - - log "🧷 开机自启:${se_colored}" - log "🟢 服务状态:${ss_colored}" - - log "" - log "${C_BOLD}常用命令:${C_NC}" - log " $(cmd "sudo systemctl status ${Service_Name}.service")" - log " $(cmd "sudo systemctl restart ${Service_Name}.service")" -else - section "服务状态" - warn "当前环境未启用 systemd(如 Docker 容器),请使用 clashctl 管理进程" - log " $(cmd "sudo clashctl start")" - log " $(cmd "sudo clashctl restart")" -fi - -# ========================= -# Dashboard / Secret -# ========================= -section "控制面板" - -api_port="$(parse_port "${EXTERNAL_CONTROLLER}")" -api_host="${EXTERNAL_CONTROLLER%:*}" - -if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then - api_host="127.0.0.1" -fi - -CONF_DIR="$Install_Dir/conf" -CONF_FILE="$CONF_DIR/config.yaml" - -SECRET_VAL="" -if wait_secret_ready "$CONF_FILE" 6; then - SECRET_VAL="$(read_secret_from_config "$CONF_FILE" || true)" -fi - -dash="http://${api_host}:${api_port}/ui" -log "🌐 Dashboard:$(url "$dash")" - -if [[ -n "$SECRET_VAL" ]]; then - MASKED="${SECRET_VAL:0:4}****${SECRET_VAL: -4}" - log "🔐 Secret:${C_YELLOW}${MASKED}${C_NC}" - log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" -else - log "🔐 Secret:${C_YELLOW}启动中暂未读到(稍后再试)${C_NC}" - log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" -fi - -# ========================= -# 订阅配置(必须) -# ========================= -section "订阅状态" - -ENV_FILE="${Install_Dir}/.env" - -if [[ -n "${CLASH_URL:-}" ]]; then - ok "订阅地址已配置(CLASH_URL 已写入 .env)" -else - warn "订阅地址未配置(必须)" - log "" - log "配置订阅地址:" - log " $(cmd "sudo bash -c 'echo \"CLASH_URL=<订阅地址>\" > ${ENV_FILE}'")" - log "" - log "配置完成后重启服务:" - if [ "$Systemd_Usable" = "true" ]; then - log " $(cmd "sudo systemctl restart ${Service_Name}.service")" - else - log " $(cmd "sudo clashctl restart")" - fi -fi - -# ========================= -# 下一步 -# ========================= -section "下一步开启代理(可选)" - -PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" - -if [ -f "$PROFILED_FILE" ]; then - log " $(cmd "source $PROFILED_FILE")" - log " $(cmd "proxy_on")" -else - log " (未安装 Shell 代理快捷命令,跳过)" -fi - -# ========================= -# 启动后快速诊断 -# ========================= -sleep 1 -if [ "$Systemd_Usable" = "true" ] && command -v journalctl >/dev/null 2>&1; then - if journalctl -u "${Service_Name}.service" -n 50 --no-pager 2>/dev/null \ - | grep -q "Clash订阅地址不可访问"; then - warn "服务启动异常:订阅不可用,请检查 CLASH_URL(可能过期 / 404 / 被墙)。" - fi + echo -e "请执行以下命令加载环境变量: source ${Env_File}\n" + echo -e "请执行以下命令开启系统代理: proxy_on\n" + echo -e "若要临时关闭系统代理,请执行: proxy_off\n" fi From 7d950698f153406f6b993d467216c6dae93f9080 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Mon, 16 Mar 2026 23:58:51 +0800 Subject: [PATCH 04/19] Update install.sh --- install.sh | 1328 +++++++++++++++++++--------------------------------- 1 file changed, 483 insertions(+), 845 deletions(-) diff --git a/install.sh b/install.sh index 4dad35e..cec8d78 100755 --- a/install.sh +++ b/install.sh @@ -1,904 +1,542 @@ -#!/usr/bin/env bash -# 严格模式 +#!/bin/bash set -euo pipefail -# --- DEBUG: 打印具体失败的行号和命令(systemd 下非常关键) --- -trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR -# 如需更详细:取消下一行注释 -# set -x -# --- DEBUG end --- +# ========================= +# 基础参数 +# ========================= +Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" +Service_Name="clash-for-linux" +Service_User="root" +Service_Group="root" -############################################ -# Clash for Linux - start.sh (Full Version) -# - systemd 模式下订阅失败/下载失败:不退出,使用 conf/config.yaml(必要时从 conf/fallback_config.yaml 拷贝)兜底启动 -# - 非 systemd 模式:订阅失败/下载失败直接退出(保持手动执行的强约束) -############################################ +# ========================= +# 彩色输出(统一 printf + 自动降级 + 手动关色) +# ========================= -# 加载系统函数库(Only for RHEL Linux) -[ -f /etc/init.d/functions ] && source /etc/init.d/functions +# ---- 关色开关(优先级最高)---- +NO_COLOR_FLAG=0 +for arg in "$@"; do + case "$arg" in + --no-color|--nocolor) + NO_COLOR_FLAG=1 + ;; + esac +done -#################### 脚本初始化任务 #################### - -# 获取脚本工作目录绝对路径 -export Server_Dir -Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# 加载.env变量文件 -# shellcheck disable=SC1090 -# --- source .env(不可信输入,必须放宽) --- -if [ -f "$Server_Dir/.env" ]; then - set +u - source "$Server_Dir/.env" || echo "[WARN] failed to source .env" >&2 - set -u +if [[ -n "${NO_COLOR:-}" ]] || [[ -n "${CLASH_NO_COLOR:-}" ]]; then + NO_COLOR_FLAG=1 fi -# systemd 模式开关(必须在 set -u 下安全) -SYSTEMD_MODE="${SYSTEMD_MODE:-false}" - -# root-only 强约束:不是 root 直接退出 -if [ "$(id -u)" -ne 0 ]; then - echo "[ERR] root-only mode: please run as root" >&2 - exit 2 +# ---- 初始化颜色 ---- +if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then + if tput setaf 1 >/dev/null 2>&1; then + C_RED="$(tput setaf 1)" + C_GREEN="$(tput setaf 2)" + C_YELLOW="$(tput setaf 3)" + C_BLUE="$(tput setaf 4)" + C_CYAN="$(tput setaf 6)" + C_GRAY="$(tput setaf 8 2>/dev/null || true)" + C_BOLD="$(tput bold)" + C_UL="$(tput smul)" + C_NC="$(tput sgr0)" + fi fi -# 给二进制启动程序、脚本等添加可执行权限 -chmod +x "$Server_Dir/bin/"* 2>/dev/null || true -chmod +x "$Server_Dir/scripts/"* 2>/dev/null || true -if [ -f "$Server_Dir/tools/subconverter/subconverter" ]; then - chmod +x "$Server_Dir/tools/subconverter/subconverter" 2>/dev/null || true +# ---- ANSI fallback ---- +if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && [[ -z "${C_NC:-}" ]]; then + C_RED=$'\033[31m' + C_GREEN=$'\033[32m' + C_YELLOW=$'\033[33m' + C_BLUE=$'\033[34m' + C_CYAN=$'\033[36m' + C_GRAY=$'\033[90m' + C_BOLD=$'\033[1m' + C_UL=$'\033[4m' + C_NC=$'\033[0m' fi -#################### 变量设置 #################### +# ---- 强制无色 ---- +if [[ "$NO_COLOR_FLAG" -eq 1 ]] || [[ ! -t 1 ]]; then + C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN='' C_GRAY='' C_BOLD='' C_UL='' C_NC='' +fi -Conf_Dir="$Server_Dir/conf" +# ========================= +# 基础输出函数 +# ========================= +log() { printf "%b\n" "$*"; } +info() { log "${C_CYAN}[INFO]${C_NC} $*"; } +ok() { log "${C_GREEN}[OK]${C_NC} $*"; } +warn() { log "${C_YELLOW}[WARN]${C_NC} $*"; } +err() { log "${C_RED}[ERROR]${C_NC} $*"; } -# root-only:统一使用安装目录下的 temp/logs -Temp_Dir="$Server_Dir/temp" -Log_Dir="$Server_Dir/logs" +# ========================= +# 样式助手 +# ========================= +path() { printf "%b" "${C_BOLD}$*${C_NC}"; } +cmd() { printf "%b" "${C_GRAY}$*${C_NC}"; } +url() { printf "%b" "${C_UL}$*${C_NC}"; } +good() { printf "%b" "${C_GREEN}$*${C_NC}"; } +bad() { printf "%b" "${C_RED}$*${C_NC}"; } -mkdir -p "$Conf_Dir" "$Temp_Dir" "$Log_Dir" || { - echo "[ERR] cannot create dirs: Conf_Dir=$Conf_Dir Temp_Dir=$Temp_Dir Log_Dir=$Log_Dir" - exit 2 +# ========================= +# 分段标题(CLI 风格 section) +# ========================= +section() { + local title="$*" + log "" + log "${C_BOLD}▶ ${title}${C_NC}" + log "${C_GRAY}────────────────────────────────────────${C_NC}" } -# 再做一次可写性检查,避免后面玄学 exit -touch "$Temp_Dir/.write_test" 2>/dev/null || { echo "[ERR] Temp_Dir not writable: $Temp_Dir"; exit 2; } -rm -f "$Temp_Dir/.write_test" 2>/dev/null || true +# ========================= +# 前置校验 +# ========================= +if [ "$(id -u)" -ne 0 ]; then + err "需要 root 权限执行安装脚本(请使用 sudo bash install.sh)" + exit 1 +fi -PID_FILE="${CLASH_PID_FILE:-$Temp_Dir/clash.pid}" +if [ ! -f "${Server_Dir}/.env" ]; then + err "未找到 .env 文件,请确认脚本所在目录:${Server_Dir}" + exit 1 +fi -is_running() { - if [ -f "$PID_FILE" ]; then - local pid - pid="$(cat "$PID_FILE" 2>/dev/null || true)" - if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then - return 0 +# ========================= +# 同步到安装目录(保持你原逻辑) +# ========================= +mkdir -p "$Install_Dir" +if [ "$Server_Dir" != "$Install_Dir" ]; then + info "同步项目文件到安装目录:${Install_Dir}" + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/" + else + cp -a "$Server_Dir/." "$Install_Dir/" + fi +fi + +chmod +x "$Install_Dir"/*.sh 2>/dev/null || true +chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true +chmod +x "$Install_Dir"/bin/* 2>/dev/null || true +chmod +x "$Install_Dir"/clashctl 2>/dev/null || true + +# ========================= +# 加载环境与依赖脚本 +# ========================= +# shellcheck disable=SC1090 +source "$Install_Dir/.env" +# shellcheck disable=SC1090 +source "$Install_Dir/scripts/get_cpu_arch.sh" +# shellcheck disable=SC1090 +source "$Install_Dir/scripts/resolve_clash.sh" +# shellcheck disable=SC1090 +source "$Install_Dir/scripts/port_utils.sh" + +if [[ -z "${CpuArch:-}" ]]; then + err "无法识别 CPU 架构" + exit 1 +fi + +# ========================= +# .env 写入工具:write_env_kv(必须在 prompt 之前定义) +# - 自动创建文件 +# - 存在则替换,不存在则追加 +# - 统一写成:export KEY="VALUE" +# - 自动转义双引号/反斜杠 +# ========================= +escape_env_value() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +write_env_kv() { + local file="$1" + local key="$2" + local val="$3" + + mkdir -p "$(dirname "$file")" 2>/dev/null || true + [ -f "$file" ] || touch "$file" + + val="$(printf '%s' "$val" | tr -d '\r')" + local esc + esc="$(escape_env_value "$val")" + + if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file"; then + sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*|export ${key}=\"${esc}\"|g" "$file" + else + printf 'export %s="%s"\n' "$key" "$esc" >> "$file" + fi +} + +# ========================= +# 交互式填写订阅地址(仅在 CLASH_URL 为空时触发) +# - 若非 TTY(CI/管道)则跳过交互 +# - 若用户回车跳过,则保持原行为:装完提示手动配置 +# ========================= +prompt_clash_url_if_empty() { + # 兼容 .env 里可能是 CLASH_URL= / export CLASH_URL= / 带引号 + local cur="${CLASH_URL:-}" + cur="${cur%\"}"; cur="${cur#\"}" + + if [ -n "$cur" ]; then + return 0 + fi + + # 非交互环境:不阻塞 + if [ ! -t 0 ]; then + warn "CLASH_URL 为空且当前为非交互环境(stdin 非 TTY),将跳过输入引导。" + return 0 + fi + + echo + warn "未检测到订阅地址(CLASH_URL 为空)" + echo "请粘贴你的 Clash 订阅地址(直接回车跳过,稍后手动编辑 .env):" + read -r -p "Clash URL: " input_url + + input_url="$(printf '%s' "$input_url" | tr -d '\r')" + + # 回车跳过:保持原行为(不写入) + if [ -z "$input_url" ]; then + warn "已跳过填写订阅地址,安装完成后请手动编辑:${Install_Dir}/.env" + return 0 + fi + + # 先校验再写入,避免污染 .env + if ! echo "$input_url" | grep -Eq '^https?://'; then + err "订阅地址格式不正确(必须以 http:// 或 https:// 开头)" + exit 1 + fi + + ENV_FILE="${Install_Dir}/.env" + mkdir -p "$Install_Dir" + [ -f "$ENV_FILE" ] || touch "$ENV_FILE" + + # ✅ 只用这一套写入逻辑(统一 export KEY="...",兼容旧格式) + write_env_kv "$ENV_FILE" "CLASH_URL" "$input_url" + + export CLASH_URL="$input_url" + ok "已写入订阅地址到:${ENV_FILE}" +} + +prompt_clash_url_if_empty + +# ========================= +# 端口冲突检测(保持你原逻辑) +# ========================= +CLASH_HTTP_PORT=${CLASH_HTTP_PORT:-7890} +CLASH_SOCKS_PORT=${CLASH_SOCKS_PORT:-7891} +CLASH_REDIR_PORT=${CLASH_REDIR_PORT:-7892} +EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} + +parse_port() { + local raw="$1" + raw="${raw##*:}" + echo "$raw" +} + +Port_Conflicts=() +for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_port "$EXTERNAL_CONTROLLER")"; do + if [ "$port" = "auto" ] || [ -z "$port" ]; then + continue + fi + if [[ "$port" =~ ^[0-9]+$ ]]; then + if is_port_in_use "$port"; then + Port_Conflicts+=("$port") fi fi +done + +if [ "${#Port_Conflicts[@]}" -ne 0 ]; then + warn "检测到端口冲突: ${Port_Conflicts[*]},运行时将自动分配可用端口" +fi + +install -d -m 0755 "$Install_Dir/conf" "$Install_Dir/logs" "$Install_Dir/temp" + +# ========================= +# Clash 内核就绪检查/下载 +# ========================= +if ! resolve_clash_bin "$Install_Dir" "$CpuArch" >/dev/null 2>&1; then + err "Clash 内核未就绪,请检查下载配置或手动放置二进制" + exit 1 +fi + +# ========================= +# fonction 工具函数区 +# ========================= +# 等待 config.yaml 出现并写入 secret(默认最多等 6 秒) +wait_secret_ready() { + local conf_file="$1" + local timeout_sec="${2:-6}" + + local end=$((SECONDS + timeout_sec)) + while [ "$SECONDS" -lt "$end" ]; do + if [ -s "$conf_file" ] && grep -qE '^[[:space:]]*secret:' "$conf_file"; then + return 0 + fi + sleep 0.2 + done return 1 } -if [ "${SYSTEMD_MODE:-false}" != "true" ] && is_running; then - echo -e "\n[OK] Clash 已在运行 (pid=$(cat "$PID_FILE")),跳过重复启动\n" - exit 0 -fi - -# systemd 模式下避免读取遗留 pid 干扰判断 -if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - rm -f "$PID_FILE" 2>/dev/null || true -fi - -# 统一订阅变量 -URL="${CLASH_URL:-}" - -# 清理可能的 CRLF(Windows 写 .env 很常见) -URL="$(printf '%s' "$URL" | tr -d '\r')" -URL="$(printf '%s' "$URL" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" - -#让 bash 子进程能拿到 -export CLASH_URL="$URL" - -# 只有在“需要在线更新订阅”的模式下才强制要求 URL -if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then - echo "[ERR] CLASH_URL 为空(未配置订阅地址)" - exit 2 -fi -if [ -n "$URL" ] && ! printf '%s' "$URL" | grep -Eq '^https?://'; then - echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2 - exit 2 -fi - -# 获取 CLASH_SECRET 值:优先 .env;其次读取旧 config;占位符视为无效;最后生成随机值 -Secret="${CLASH_SECRET:-}" - -# 尝试从旧 config.yaml 读取(仅当 .env 未提供) -if [ -z "$Secret" ] && [ -f "$Conf_Dir/config.yaml" ]; then - Secret="$(awk -F': *' '/^[[:space:]]*secret[[:space:]]*:/{print $2; exit}' "$Conf_Dir/config.yaml" 2>/dev/null | tr -d '"' || true)" -fi - -# 若读取到的是占位符(如 ${CLASH_SECRET}),视为无效 -if [[ "$Secret" =~ ^\$\{.*\}$ ]]; then - Secret="" -fi - -# 兜底生成随机 secret -if [ -z "$Secret" ]; then - if command -v openssl >/dev/null 2>&1; then - Secret="$(openssl rand -hex 32)" - else - # 32 bytes -> 64 hex chars - Secret="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" - fi -fi - -# 强制写入 secret 到指定配置文件(存在则替换,不存在则追加) -force_write_secret() { - local file="$1" - [ -f "$file" ] || return 0 - - if grep -qE '^[[:space:]]*secret:' "$file"; then - # 替换整行 secret(无论原来是啥,包括 SECRET_PLACEHOLDER / "${CLASH_SECRET}") - sed -i -E "s|^[[:space:]]*secret:.*$|secret: ${Secret}|g" "$file" - else - # 没有 secret 行就追加到文件末尾 - printf "\nsecret: %s\n" "$Secret" >> "$file" - fi +# 计算字符串可视宽度:中文大概率按 2 宽处理(简单够用版) +# 注:终端宽度/字体不统一时,中文宽度估算永远只能“近似” +vis_width() { + python3 - <<'PY' "$1" +import sys +s=sys.argv[1] +w=0 +for ch in s: + # East Asian Wide/FullWidth 近似当 2 + w += 2 if ord(ch) >= 0x2E80 else 1 +print(w) +PY } -ensure_ui_link() { - mkdir -p "$Conf_Dir" - ln -sfn "$Server_Dir/dashboard/public" "$Conf_Dir/ui" +pad_right() { # pad_right "text" width + local s="$1" w="$2" + local cur + cur="$(vis_width "$s")" + local pad=$(( w - cur )) + (( pad < 0 )) && pad=0 + printf "%s%*s" "$s" "$pad" "" } -# --- helpers: upsert yaml key (top-level), ensure UI links --- -upsert_yaml_kv() { - # Usage: upsert_yaml_kv - # Writes: key: value (top-level) - local file="$1" key="$2" value="$3" - [ -n "$file" ] && [ -n "$key" ] || return 1 - - # 如果文件不存在,先创建 - [ -f "$file" ] || : >"$file" || return 1 - - if grep -qE "^[[:space:]]*${key}:[[:space:]]*" "$file" 2>/dev/null; then - # 替换整行(避免残留引号) - sed -i -E "s|^[[:space:]]*${key}:[[:space:]]*.*$|${key}: ${value}|g" "$file" - else - # 追加前保证有换行 - tail -c 1 "$file" 2>/dev/null | read -r _last || true - # shellcheck disable=SC2034 - if [ "$(tail -c 1 "$file" 2>/dev/null || true)" != "" ]; then - printf "\n" >>"$file" - fi - printf "%s: %s\n" "$key" "$value" >>"$file" - fi +box_title() { # box_title "标题" width + local title="$1" width="$2" + local inner=$((width-2)) + printf "┌%s┐\n" "$(printf '─%.0s' $(seq 1 $inner))" + # 标题居中(近似) + local t=" $title " + local tw; tw="$(vis_width "$t")" + local left=$(( (inner - tw)/2 )); ((left<0)) && left=0 + local right=$(( inner - tw - left )); ((right<0)) && right=0 + printf "│%*s%s%*s│\n" "$left" "" "$t" "$right" "" + printf "├%s┤\n" "$(printf '─%.0s' $(seq 1 $inner))" } -ensure_ui_links() { - local ui_src="${UI_SRC_DIR:-$Server_Dir/dashboard/public}" - mkdir -p "$Conf_Dir" 2>/dev/null || true - if [ -d "$ui_src" ]; then - ln -sfn "$ui_src" "$Conf_Dir/ui" 2>/dev/null || true - fi +box_row() { # box_row "key" "value" width keyw + local k="$1" v="$2" width="$3" keyw="$4" + local inner=$((width-2)) + # 形如:│ key: value │ + local left="$(pad_right "$k" "$keyw")" + local line=" ${left} ${v}" + local lw; lw="$(vis_width "$line")" + local pad=$(( inner - lw )); ((pad<0)) && pad=0 + printf "│%s%*s│\n" "$line" "$pad" "" } -force_write_controller_and_ui() { - local file="$1" - local controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" - - [ -n "$file" ] || return 1 - - # external-controller - upsert_yaml_kv "$file" "external-controller" "$controller" || true - - # external-ui: fixed to Conf_Dir/ui - ensure_ui_links - if [ -e "$Conf_Dir/ui" ]; then - upsert_yaml_kv "$file" "external-ui" "$Conf_Dir/ui" || true - fi +box_end() { # box_end width + local width="$1" inner=$((width-2)) + printf "└%s┘\n" "$(printf '─%.0s' $(seq 1 $inner))" } +# 从 config.yaml 提取 secret(强韧:支持缩进/引号/CRLF/尾空格) +read_secret_from_config() { + local conf_file="$1" + [ -f "$conf_file" ] || return 1 -fix_external_ui_by_safe_paths() { - local bin="$1" - local cfg="$2" - local test_out="$3" - local ui_src="${UI_SRC_DIR:-$Server_Dir/dashboard/public}" + # 1) 找到 secret 行 -> 2) 去掉 key 和空格 -> 3) 去掉首尾引号 -> 4) 去掉 CR + local s + s="$( + sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "$conf_file" \ + | head -n 1 \ + | sed -E 's/^[[:space:]]*"(.*)"[[:space:]]*$/\1/; s/^[[:space:]]*'\''(.*)'\''[[:space:]]*$/\1/' \ + | tr -d '\r' + )" - [ -x "$bin" ] || return 0 - [ -s "$cfg" ] || return 0 + # 去掉纯空格 + s="$(printf '%s' "$s" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" - # 先跑一次 test,把原因写入 test_out - "$bin" -t -f "$cfg" >"$test_out" 2>&1 - local rc=$? - [ $rc -eq 0 ] && return 0 - - # 只处理 external-ui 的 SAFE_PATH 报错 - if ! grep -q "SAFE_PATHS" "$test_out"; then - return $rc - fi - if ! grep -q "external-ui" "$cfg" && ! grep -q "external-ui" "$test_out"; then - return $rc - fi - - # 从 test_out 抽取 allowed paths 的第一个 base - # 例:allowed paths: [/opt/clash-for-linux/.config/mihomo] - local base - base="$(sed -n 's/.*allowed paths: \[\([^]]*\)\].*/\1/p' "$test_out" | head -n 1)" - - [ -n "$base" ] || return $rc - - # external-ui 必须在 allowed base 的子目录里 - local ui_dst="$base/ui" - mkdir -p "$ui_dst" 2>/dev/null || true - - # 把 UI 文件同步过去(真实目录,不用软链,避免跳出 base) - if [ -d "$ui_src" ]; then - if command -v rsync >/dev/null 2>&1; then - rsync -a --delete "$ui_src"/ "$ui_dst"/ 2>/dev/null || true - else - rm -rf "$ui_dst"/* 2>/dev/null || true - cp -a "$ui_src"/. "$ui_dst"/ 2>/dev/null || true - fi - fi - - # 重写 external-ui 到新目录 - upsert_yaml_kv "$cfg" "external-ui" "$ui_dst" || true - - # 再 test 一次 - "$bin" -t -f "$cfg" >"$test_out" 2>&1 - return $? + [ -n "$s" ] || return 1 + printf '%s' "$s" } -# 设置默认值 -CLASH_HTTP_PORT="${CLASH_HTTP_PORT:-7890}" -CLASH_SOCKS_PORT="${CLASH_SOCKS_PORT:-7891}" -CLASH_REDIR_PORT="${CLASH_REDIR_PORT:-7892}" -CLASH_LISTEN_IP="${CLASH_LISTEN_IP:-127.0.0.1}" -CLASH_ALLOW_LAN="${CLASH_ALLOW_LAN:-false}" +# ========================= +# systemd 安装与启动 +# ========================= +Service_Enabled="unknown" +Service_Started="unknown" -EXTERNAL_CONTROLLER_ENABLED="${EXTERNAL_CONTROLLER_ENABLED:-true}" -EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" +if command -v systemctl >/dev/null 2>&1; then + CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh" -ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}" + if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then + systemctl enable "${Service_Name}.service" >/dev/null 2>&1 || true + fi + if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then + systemctl start "${Service_Name}.service" >/dev/null 2>&1 || true + fi -# 端口与配置工具 -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/port_utils.sh" -CLASH_HTTP_PORT="$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT")" -CLASH_SOCKS_PORT="$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT")" -CLASH_REDIR_PORT="$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT")" -EXTERNAL_CONTROLLER="$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "127.0.0.1")" - -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/config_utils.sh" - -#################### 函数定义 #################### - -# 自定义action函数,实现通用action功能(兼容 journald;关键错误会额外 echo 到 stderr) -success() { - echo -en "\033[60G[\033[1;32m OK \033[0;39m]\r" - return 0 -} - -failure() { - local rc=$? - echo -en "\033[60G[\033[1;31mFAILED\033[0;39m]\r" - [ -x /bin/plymouth ] && /bin/plymouth --details - return "$rc" -} - -action() { - local STRING - STRING=$1 - shift - - # 执行命令本身的成功/失败,不应让 UI 输出影响返回码 - if "$@"; then - success $"$STRING" || true - return 0 + if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then + Service_Enabled="enabled" else - failure $"$STRING" || true - return 1 - fi -} - -# 判断命令是否正常执行 -# - 手动模式:失败直接 exit -# - systemd 模式:只打印状态,不影响退出码 -if_success() { - local ok_msg=$1 - local fail_msg=$2 - local rc=$3 - - if [ "$rc" -eq 0 ]; then - action "$ok_msg" /bin/true || true - return 0 + Service_Enabled="disabled" fi - # rc != 0 - action "$fail_msg" /bin/false || true - - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - # systemd 下不允许在 UI 函数中 exit - return "$rc" + if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then + Service_Started="active" else - exit "$rc" - fi -} - -ensure_subconverter() { - local bin="${Server_Dir}/tools/subconverter/subconverter" - local port="25500" - - # 没有二进制直接跳过 - if [ ! -x "$bin" ]; then - echo "[WARN] subconverter bin not found: $bin" - export SUBCONVERTER_READY="false" - return 0 - fi - - # 已在监听则认为就绪 - if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" - export SUBCONVERTER_READY="true" - return 0 - fi - - # 启动(后台) - echo "[INFO] starting subconverter..." - (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" >/dev/null 2>&1 &) - - # 等待端口起来 - for _ in 1 2 3 4 5; do - sleep 1 - if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" - export SUBCONVERTER_READY="true" - echo "[OK] subconverter ready at ${SUBCONVERTER_URL}" - return 0 - fi - done - - echo "[WARN] subconverter start failed or port not ready" - export SUBCONVERTER_READY="false" - return 0 -} - -#################### 任务执行 #################### - -## 获取CPU架构信息 -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/get_cpu_arch.sh" - -if [[ -z "${CpuArch:-}" ]]; then - echo "[ERROR] Failed to obtain CPU architecture" >&2 - exit 2 -fi - -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/resolve_clash.sh" - -## 临时取消环境变量 -unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY || true - -######################################################## -# systemd 兜底:如果没有可用订阅 URL,则确保有 config.yaml -######################################################## -ensure_fallback_config() { - # conf/config.yaml 为空或不存在,则从 fallback 拷贝 - if [ ! -s "$Conf_Dir/config.yaml" ]; then - if [ -s "$Server_Dir/conf/fallback_config.yaml" ]; then - cp -f "$Server_Dir/conf/fallback_config.yaml" "$Conf_Dir/config.yaml" - echo -e "\033[33m[WARN]\033[0m 已复制 fallback_config.yaml -> conf/config.yaml(兜底)" - else - echo -e "\033[31m[ERROR]\033[0m 未找到可用的 conf/fallback_config.yaml,无法兜底启动" >&2 - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - return 1 - else - exit 1 - fi - fi - fi - - # 强制写入真实 secret(失败时也遵循同样规则) - if ! force_write_secret "$Conf_Dir/config.yaml"; then - echo -e "\033[31m[ERROR]\033[0m 写入 secret 失败:$Conf_Dir/config.yaml" >&2 - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - return 1 - else - exit 1 - fi - fi - - return 0 -} -SKIP_CONFIG_REBUILD=false - -# systemd 模式下若 URL 为空:直接兜底启动 -if [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; then - echo -e "\033[33m[WARN]\033[0m SYSTEMD_MODE=true 且 CLASH_URL 为空,跳过订阅更新,使用本地兜底配置启动" - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true -fi - -#################### Clash 订阅地址检测及配置文件下载 #################### -if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - echo -e '\n正在检测订阅地址...' - Text1="Clash订阅地址可访问!" - Text2="Clash订阅地址不可访问!" - - CHECK_CMD=(curl -o /dev/null -L -sS --retry 5 -m 10 --connect-timeout 10 -w "%{http_code}") - if [ "$ALLOW_INSECURE_TLS" = "true" ]; then - CHECK_CMD+=(-k) - echo -e "\033[33m[WARN]\033[0m 已启用不安全的 TLS 下载(跳过证书校验)" - fi - if [ -n "${CLASH_HEADERS:-}" ]; then - CHECK_CMD+=(-H "$CLASH_HEADERS") - fi - CHECK_CMD+=("$URL") - - # 不让 set -e 干扰获取状态码 - set +e - status_code="$("${CHECK_CMD[@]}")" - curl_rc=$? - set -e - - # curl 本身失败,视为不可用 - if [ "$curl_rc" -ne 0 ]; then - status_code="" - ReturnStatus=1 - else - echo "$status_code" | grep -E '^[23][0-9]{2}$' &>/dev/null - ReturnStatus=$? - fi - - if [ "$ReturnStatus" -eq 0 ]; then - action "$Text1" /bin/true || true - else - if [ "$SYSTEMD_MODE" = "true" ]; then - action "$Text2(systemd 模式不退出,尝试使用旧配置/兜底配置)" /bin/false || true - echo -e "\033[33m[WARN]\033[0m Subscribe check failed: http_code=${status_code:-unknown}, url=${URL}" >&2 - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true - else - if_success "$Text1" "$Text2" "$ReturnStatus" - fi - fi -fi - -#################### 下载订阅并生成 config.yaml(非兜底路径) #################### -if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - ensure_subconverter || true - echo -e '\n正在下载Clash配置文件...' - Text3="配置文件clash.yaml下载成功!" - Text4="配置文件clash.yaml下载失败!" - - # --- DBG: 显式打印并验证临时目录可写(systemd 下常见权限问题) --- - echo "[DBG] uid=$(id -u) user=$(id -un) SYSTEMD_MODE=${SYSTEMD_MODE:-}" - echo "[DBG] Server_Dir=$Server_Dir Conf_Dir=$Conf_Dir Temp_Dir=$Temp_Dir Log_Dir=$Log_Dir" - echo "[DBG] URL=$(printf '%q' "$URL")" - - mkdir -p "$Temp_Dir" 2>/dev/null || true - touch "$Temp_Dir/.write_test" 2>/dev/null || { echo "[ERR] Temp_Dir not writable: $Temp_Dir" >&2; exit 2; } - rm -f "$Temp_Dir/.write_test" 2>/dev/null || true - # --- DBG end --- - - CURL_CMD=(curl -fL -S --retry 2 --connect-timeout 10 -m 30 -o "$Temp_Dir/clash.yaml") - if [ "$ALLOW_INSECURE_TLS" = "true" ]; then - CURL_CMD+=(-k) - fi - if [ -n "${CLASH_HEADERS:-}" ]; then - CURL_CMD+=(-H "$CLASH_HEADERS") - fi - CURL_CMD+=("$URL") - - set +e - CURL_ERR="$Temp_Dir/curl.err" - : > "$CURL_ERR" - "${CURL_CMD[@]}" 2>>"$CURL_ERR" - ReturnStatus=$? - set -e - - echo "[DBG] curl rc=$ReturnStatus" - if [ -s "$CURL_ERR" ]; then - echo "[DBG] curl stderr (last 50 lines):" - tail -n 50 "$CURL_ERR" - fi - - if [ "$ReturnStatus" -ne 0 ]; then - WGET_CMD=(wget -q -O "$Temp_Dir/clash.yaml") - if [ "$ALLOW_INSECURE_TLS" = "true" ]; then - WGET_CMD+=(--no-check-certificate) - fi - if [ -n "${CLASH_HEADERS:-}" ]; then - WGET_CMD+=(--header="$CLASH_HEADERS") - fi - WGET_CMD+=("$URL") - - for _ in {1..10}; do - set +e - "${WGET_CMD[@]}" - ReturnStatus=$? - set -e - if [ "$ReturnStatus" -eq 0 ]; then - break - fi - done - fi - - CONFIG_FILE="${CONFIG_FILE:-$Temp_Dir/config.yaml}" - mkdir -p "$Temp_Dir" || true - - if [ "$ReturnStatus" -eq 0 ] && [ -s "$Temp_Dir/clash.yaml" ]; then - SRC_YAML="$Temp_Dir/clash.yaml" - - # 1) 判断是否是完整 Clash 配置(关键字段之一存在即可) - if grep -qE '^(proxies:|proxy-providers:|rules:|port:|mixed-port:|dns:)' "$SRC_YAML"; then - cp -f "$SRC_YAML" "$CONFIG_FILE" - echo "[INFO] subscription already is a full clash config" - else - # 2) 非完整配置:尝试用 subconverter 转换 - echo "[INFO] subscription is not a full config, try conversion via subconverter..." - - export IN_FILE="$SRC_YAML" - export OUT_FILE="$Temp_Dir/clash_converted.yaml" - - set +e - bash "$Server_Dir/scripts/clash_profile_conversion.sh" - conv_rc=$? - set -e - - if [ "$conv_rc" -eq 0 ] && [ -s "$OUT_FILE" ]; then - cp -f "$OUT_FILE" "$CONFIG_FILE" - echo "[INFO] conversion ok -> runtime config ready" - else - echo "[WARN] conversion skipped/failed, will keep original and rely on fallback" - cp -f "$SRC_YAML" "$CONFIG_FILE" - fi - fi - - # 3) 强制注入 external-controller / external-ui(运行态兜底) - force_write_controller_and_ui "$CONFIG_FILE" || true - - # 4) 强制注入 secret - force_write_secret "$CONFIG_FILE" || true - - # Optional: Fix test URLs to HTTPS for reliability (safe, narrow scope) - if [ "${FIX_TEST_URL_HTTPS:-true}" = "true" ] && [ -s "$CONFIG_FILE" ]; then - # 1) proxy-groups: url-test / fallback url - sed -i -E "s#(url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true - - # 2) cfw-latency-url (some dashboards) - sed -i -E "s#(cfw-latency-url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true - - # 3) proxy-providers health-check url (mihomo warns about this) - sed -i -E "s#(health-check:[[:space:]]*\n[[:space:]]*url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true - fi - - # 5) 自检:失败则回退到旧配置(注意:脚本 set -e + trap ERR,必须 set +e 包裹) - BIN="${Server_Dir}/bin/clash-linux-amd64" - NEW_CFG="$CONFIG_FILE" - OLD_CFG="${Conf_Dir}/config.yaml" - TEST_OUT="$Temp_Dir/config.test.out" - - if [ -x "$BIN" ] && [ -f "$NEW_CFG" ]; then - # 先尝试自动修复 external-ui 的 SAFE_PATH 问题(内部会跑 -t) - set +e - fix_external_ui_by_safe_paths "$BIN" "$NEW_CFG" "$TEST_OUT" - test_rc=$? - set -e - - if [ "$test_rc" -ne 0 ]; then - echo "[ERROR] Generated config invalid, rc=$test_rc, reason(file=$TEST_OUT, size=$(wc -c <"$TEST_OUT" 2>/dev/null || echo 0))" >&2 - tail -n 120 "$TEST_OUT" >&2 || true - - echo "[ERROR] fallback to last good config: $OLD_CFG" >&2 - if [ -f "$OLD_CFG" ]; then - cp -f "$OLD_CFG" "$NEW_CFG" - else - echo "[FATAL] No valid config available, aborting startup" >&2 - exit 1 - fi - fi - fi - - echo "[INFO] Runtime config generated: $CONFIG_FILE (size=$(wc -c <"$CONFIG_FILE" 2>/dev/null || echo 0))" - else - echo "[WARN] Download did not produce clash.yaml (rc=$ReturnStatus), skip runtime config generation" >&2 - fi - - if [ "$ReturnStatus" -eq 0 ]; then - action "$Text3" /bin/true || true - else - if [ "$SYSTEMD_MODE" = "true" ]; then - action "$Text4(systemd 模式:下载失败,使用旧配置/兜底配置继续启动)" /bin/false || true - echo -e "\033[33m[WARN]\033[0m Download failed, will fallback. url=${URL}" >&2 - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true - else - if_success "$Text3" "$Text4(退出启动)" "$ReturnStatus" - fi - fi -fi - -# ========================================================= -# 判断订阅是否已是完整 Clash YAML(Meta / Mihomo / Premium) -# 若是完整配置,则直接使用,跳过后续代理拆解与拼接 -# ========================================================= -if grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$Temp_Dir/clash.yaml"; then - echo "[INFO] subscription is a full Clash config, use it directly" - cp -f "$Temp_Dir/clash.yaml" "$Conf_Dir/config.yaml" - - # 生成运行态(systemd non-root 实际启动用 Temp_Dir/config.yaml) - cp -f "$Temp_Dir/clash.yaml" "$Temp_Dir/config.yaml" - - # 写 controller/ui + secret(写到运行态) - force_write_controller_and_ui "$Temp_Dir/config.yaml" || true - force_write_secret "$Temp_Dir/config.yaml" || true - - # 同时把 conf/config.yaml 也补齐(方便你 grep/排查) - force_write_controller_and_ui "$Conf_Dir/config.yaml" || true - force_write_secret "$Conf_Dir/config.yaml" || true - - # 创建 UI 软链(systemd non-root 用 /tmp) - Dashboard_Src="$Server_Dir/dashboard/public" - if [ -d "$Dashboard_Src" ]; then - ln -sfn "$Dashboard_Src" "$Conf_Dir/ui" 2>/dev/null || true - fi - - SKIP_CONFIG_REBUILD=true - fi - -#################### 订阅转换/拼接(非兜底路径) #################### -if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - # 运行期配置文件:默认用 Temp_Dir(systemd + clash 用户可写) - CONFIG_FILE="$Temp_Dir/config.yaml" - - # 1) 重命名订阅文件 - \cp -a "$Temp_Dir/clash.yaml" "$Temp_Dir/clash_config.yaml" - - # 2) 判断订阅内容是否符合 clash 配置文件标准,尝试转换(需 subconverter) - # shellcheck disable=SC1090 - source "$Server_Dir/scripts/resolve_subconverter.sh" - - if [ "${Subconverter_Ready:-false}" = "true" ]; then - echo -e '\n判断订阅内容是否符合clash配置文件标准:' - export SUBCONVERTER_BIN="$Subconverter_Bin" - bash "$Server_Dir/scripts/clash_profile_conversion.sh" - sleep 1 - else - echo -e "\033[33m[WARN]\033[0m 未检测到可用的 subconverter,跳过订阅转换" - fi - - # 3) 订阅形态判断: - # - 如果已经是完整 Clash 配置(Meta/Mihomo 常见 mixed-port / proxy-providers 等),直接用它作为运行配置 - # - 否则才走 “proxies: 抽取 + template 拼接” - if grep -qE '^(mixed-port:|port:|proxy-providers:|proxies:)' "$Temp_Dir/clash_config.yaml"; then - # 情况 A:完整配置(优先) - if grep -q '^proxies:' "$Temp_Dir/clash_config.yaml" || grep -q '^proxy-providers:' "$Temp_Dir/clash_config.yaml" || grep -q '^mixed-port:' "$Temp_Dir/clash_config.yaml" || grep -q '^port:' "$Temp_Dir/clash_config.yaml"; then - echo "[INFO] subscription looks like a full Clash config, use it directly" - cp -f "$Temp_Dir/clash_config.yaml" "$CONFIG_FILE" - # 写入 secret(运行态) - force_write_secret "$CONFIG_FILE" - # 直接跳过后续拼接流程 - SKIP_CONFIG_REBUILD=true - fi - fi - - # 情况 B:不是完整配置,才尝试抽取 proxies 并拼接 - if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - if grep -q '^proxies:' "$Temp_Dir/clash_config.yaml"; then - sed -n '/^proxies:/,$p' "$Temp_Dir/clash_config.yaml" > "$Temp_Dir/proxy.txt" - else - echo "[ERROR] subscription is not a full config and also has no 'proxies:'; cannot build config." >&2 - # systemd 模式:兜底继续;非 systemd:退出 - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true - else - exit 2 - fi - fi - fi - - # 4) 合并形成新的 config,并替换配置占位符 - if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - cat "$Temp_Dir/templete_config.yaml" > "$CONFIG_FILE" - cat "$Temp_Dir/proxy.txt" >> "$CONFIG_FILE" - - sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$CONFIG_FILE" - sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$CONFIG_FILE" - sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$CONFIG_FILE" - sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$CONFIG_FILE" - sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$CONFIG_FILE" - fi - - # 5) 配置 external-controller - if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - sed -i "s/EXTERNAL_CONTROLLER_PLACEHOLDER/${EXTERNAL_CONTROLLER}/g" "$CONFIG_FILE" - else - sed -i "s/external-controller: 'EXTERNAL_CONTROLLER_PLACEHOLDER'/# external-controller: disabled/g" "$CONFIG_FILE" - fi - - apply_tun_config "$CONFIG_FILE" - apply_mixin_config "$CONFIG_FILE" "$Server_Dir" - - # 6) 是否同步到 conf(root/非 systemd 时才做;systemd+非root跳过) - \cp "$CONFIG_FILE" "$Conf_Dir/" - - # 7) Dashboard external-ui(systemd+非root:把 ui 放 Temp_Dir 下,避免写 conf) - Work_Dir="$(cd "$(dirname "$0")" && pwd)" - Dashboard_Src="${Work_Dir}/dashboard/public" - - if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - if [ "${SYSTEMD_MODE:-false}" = "true" ] && [ "$(id -u)" -ne 0 ]; then - # runtime ui path (writable) - Dashboard_Link="$Temp_Dir/ui" - if [ -d "$Dashboard_Src" ]; then - ln -sfn "$Dashboard_Src" "$Dashboard_Link" 2>/dev/null || true - fi - else - # conf ui path (root can manage) - Dashboard_Link="${Conf_Dir}/ui" - if [ -d "$Dashboard_Src" ]; then - ln -sfn "$Dashboard_Src" "$Dashboard_Link" || true - else - echo -e "\033[33m[WARN]\033[0m Dashboard source not found: $Dashboard_Src (external-ui may not work)" - fi - fi - - # ensure external-ui points to Dashboard_Link - if grep -qE '^[[:space:]]*external-ui:' "$CONFIG_FILE"; then - sed -i -E "s|^[[:space:]]*external-ui:.*$|external-ui: ${Dashboard_Link}|g" "$CONFIG_FILE" - else - printf "\nexternal-ui: %s\n" "$Dashboard_Link" >> "$CONFIG_FILE" - fi - fi - - # 8) 写入 secret(写到 runtime config) - force_write_secret "$CONFIG_FILE" - -else - # 兜底路径:尽量也写入 secret(conf/config.yaml 可写时) - if grep -qE '^secret:\s*' "$Conf_Dir/config.yaml" 2>/dev/null; then - force_write_secret "$Conf_Dir/config.yaml" - else - echo "secret: ${Secret}" >> "$Conf_Dir/config.yaml" || true - fi -fi - -#################### 启动Clash服务 #################### - -# 选择运行期配置文件与工作目录 -CONFIG_FILE="${CONFIG_FILE:-$Conf_Dir/config.yaml}" -RUNTIME_DIR="${Conf_Dir}" - -# 启动前确保配置文件存在且非空 -if [ ! -s "$CONFIG_FILE" ]; then - echo -e "\033[31m[ERROR]\033[0m config 不存在或为空:$CONFIG_FILE,无法启动 Clash" >&2 - exit 2 -fi - -# 最终护栏:禁止未渲染的占位符进入运行态 -if grep -q '\${' "$CONFIG_FILE"; then - echo "[ERROR] config contains unresolved placeholders (\${...}): $CONFIG_FILE" >&2 - exit 2 -fi - -# 确保运行目录存在且可写(clash/mihomo 可能会写 cache/geo 数据) -mkdir -p "$RUNTIME_DIR" 2>/dev/null || true -touch "$RUNTIME_DIR/.write_test" 2>/dev/null || { - echo "[ERROR] runtime dir not writable: $RUNTIME_DIR (uid=$(id -u))" >&2 - exit 2 -} -rm -f "$RUNTIME_DIR/.write_test" 2>/dev/null || true - -echo -e '\n正在启动Clash服务...' -Text5="服务启动成功!" -Text6="服务启动失败!" - -Clash_Bin="$(resolve_clash_bin "$Server_Dir" "$CpuArch")" -ReturnStatus=$? - -if [ "$ReturnStatus" -eq 0 ]; then - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - echo "[INFO] SYSTEMD_MODE=true,前台启动交给 systemd 监管" - echo "[INFO] Using config: $CONFIG_FILE" - echo "[INFO] Using runtime dir: $RUNTIME_DIR" - - # systemd 前台:只用 -f 指定配置文件,-d 作为工作目录 - exec "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" - else - echo "[INFO] 后台启动 (nohup)" - echo "[INFO] Using config: $CONFIG_FILE" - echo "[INFO] Using runtime dir: $RUNTIME_DIR" - - nohup "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$Log_Dir/clash.log" 2>&1 & - PID=$! - ReturnStatus=$? - - if [ "$ReturnStatus" -eq 0 ]; then - echo "$PID" > "$PID_FILE" - fi - fi -fi - -if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - if_success "$Text5" "$Text6" "$ReturnStatus" || true -else - if_success "$Text5" "$Text6" "$ReturnStatus" -fi - -#################### 输出信息 #################### - -echo '' -if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - echo -e "Clash Dashboard 访问地址: http://${EXTERNAL_CONTROLLER}/ui" - - SHOW_SECRET="${CLASH_SHOW_SECRET:-false}" - SHOW_SECRET_MASKED="${CLASH_SHOW_SECRET_MASKED:-true}" - - if [ "$SHOW_SECRET" = "true" ]; then - echo -e "Secret: ${Secret}" - elif [ "$SHOW_SECRET_MASKED" = "true" ]; then - # 脱敏:前4后4 - masked="${Secret:0:4}****${Secret: -4}" - echo -e "Secret: ${masked} (set CLASH_SHOW_SECRET=true to show full)" - else - echo -e "Secret: 已生成(未显示)。查看:/opt/clash-for-linux/conf/config.yaml 或 .env" + Service_Started="inactive" fi else - echo -e "External Controller (Dashboard) 已禁用" + warn "未检测到 systemd,已跳过服务单元生成" fi -echo '' -#################### 写入代理环境变量文件 #################### +# ========================= +# Shell 代理快捷命令 +# 生成:/etc/profile.d/clash-for-linux.sh +# ========================= +PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" -Env_File="${CLASH_ENV_FILE:-}" +install_profiled() { + local http_port="${MIXED_PORT:-7890}" + # 兼容你后面可能支持 auto:auto 就先用 7890 + [ "$http_port" = "auto" ] && http_port="7890" -if [ "$Env_File" = "off" ] || [ "$Env_File" = "disabled" ]; then - echo -e "\033[33m[WARN]\033[0m 已关闭环境变量文件生成" -else - if [ -z "$Env_File" ]; then - if [ -w /etc/profile.d ]; then - Env_File="/etc/profile.d/clash-for-linux.sh" - else - Env_File="$Temp_Dir/clash-for-linux.sh" - fi - fi + # 只写 IPv4 loopback,避免某些环境 ::1 解析问题 + sudo tee "$PROFILED_FILE" >/dev/null <"$Env_File"<}" + echo "https_proxy=\${https_proxy:-}" + echo "all_proxy=\${all_proxy:-}" } EOF - echo -e "请执行以下命令加载环境变量: source ${Env_File}\n" - echo -e "请执行以下命令开启系统代理: proxy_on\n" - echo -e "若要临时关闭系统代理,请执行: proxy_off\n" + sudo chmod 644 "$PROFILED_FILE" +} + +install_profiled || true + +# ========================= +# 安装 clashctl 命令 +# ========================= +if [ -f "$Install_Dir/clashctl" ]; then + install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl fi + +# ========================= +# 友好收尾输出(闭环) +# ========================= + +section "安装完成" +ok "Clash for Linux 已安装至: $(path "${Install_Dir}")" + +log "📦 安装目录:$(path "${Install_Dir}")" +log "👤 运行用户:${Service_User}:${Service_Group}" +log "🔧 服务名称:${Service_Name}.service" + +if command -v systemctl >/dev/null 2>&1; then + section "服务状态" + + se="${Service_Enabled:-unknown}" + ss="${Service_Started:-unknown}" + + [[ "$se" == "enabled" ]] && se_colored="$(good "$se")" || se_colored="$(bad "$se")" + [[ "$ss" == "active" ]] && ss_colored="$(good "$ss")" || ss_colored="$(bad "$ss")" + + log "🧷 开机自启:${se_colored}" + log "🟢 服务状态:${ss_colored}" + + log "" + log "${C_BOLD}常用命令:${C_NC}" + log " $(cmd "sudo systemctl status ${Service_Name}.service")" + log " $(cmd "sudo systemctl restart ${Service_Name}.service")" +fi + +# ========================= +# Dashboard / Secret +# ========================= +section "控制面板" + +api_port="$(parse_port "${EXTERNAL_CONTROLLER}")" +api_host="${EXTERNAL_CONTROLLER%:*}" + +if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then + api_host="127.0.0.1" +fi + +CONF_DIR="$Install_Dir/conf" +CONF_FILE="$CONF_DIR/config.yaml" + +SECRET_VAL="" +if wait_secret_ready "$CONF_FILE" 6; then + SECRET_VAL="$(read_secret_from_config "$CONF_FILE" || true)" +fi + +dash="http://${api_host}:${api_port}/ui" +log "🌐 Dashboard:$(url "$dash")" + +if [[ -n "$SECRET_VAL" ]]; then + MASKED="${SECRET_VAL}" + log "🔐 Secret:${C_YELLOW}${MASKED}${C_NC}" + log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" +else + log "🔐 Secret:${C_YELLOW}启动中暂未读到(稍后再试)${C_NC}" + log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" +fi + +# ========================= +# 订阅配置(必须) +# ========================= +section "订阅状态" + +ENV_FILE="${Install_Dir}/.env" + +if [[ -n "${CLASH_URL:-}" ]]; then + ok "订阅地址已配置(CLASH_URL 已写入 .env)" +else + warn "订阅地址未配置(必须)" + log "" + log "配置订阅地址:" + log " $(cmd "sudo bash -c 'echo \"CLASH_URL=<订阅地址>\" > ${ENV_FILE}'")" + log "" + log "配置完成后重启服务:" + log " $(cmd "sudo systemctl restart ${Service_Name}.service")" +fi + +# ========================= +# 下一步 +# ========================= +section "下一步开启代理(可选)" + +PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" + +if [ -f "$PROFILED_FILE" ]; then + log " $(cmd "source $PROFILED_FILE")" + log " $(cmd "proxy_on")" +else + log " (未安装 Shell 代理快捷命令,跳过)" +fi + +# ========================= +# 启动后快速诊断 +# ========================= +sleep 1 +if command -v journalctl >/dev/null 2>&1; then + if journalctl -u "${Service_Name}.service" -n 50 --no-pager 2>/dev/null \ + | grep -q "Clash订阅地址不可访问"; then + warn "服务启动异常:订阅不可用,请检查 CLASH_URL(可能过期 / 404 / 被墙)。" + fi +fi \ No newline at end of file From fa36c30b9f9775199bb190951e4aabb036d23217 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Mon, 16 Mar 2026 23:58:54 +0800 Subject: [PATCH 05/19] Update get_cpu_arch.sh --- scripts/get_cpu_arch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_cpu_arch.sh b/scripts/get_cpu_arch.sh index 65d677f..737309b 100644 --- a/scripts/get_cpu_arch.sh +++ b/scripts/get_cpu_arch.sh @@ -47,4 +47,4 @@ else exitWithError "Unsupported Linux distribution" fi -echo "CPU architecture: $CpuArch" +info "CPU architecture: $CpuArch" From 573b11459f4f79b81335490a9dbfed0d698342dc Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Mon, 16 Mar 2026 23:58:56 +0800 Subject: [PATCH 06/19] Update start.sh --- start.sh | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/start.sh b/start.sh index 9bd352d..59c0eb8 100644 --- a/start.sh +++ b/start.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # 严格模式 -set -eo pipefail +set -euo pipefail # --- DEBUG: 打印具体失败的行号和命令(systemd 下非常关键) --- trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR @@ -90,14 +90,37 @@ URL="${CLASH_URL:-}" URL="$(printf '%s' "$URL" | tr -d '\r')" URL="$(printf '%s' "$URL" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" +# 允许手动启动时交互填写;直接回车则切到本地兜底配置 +MANUAL_EMPTY_URL_FALLBACK=false +if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then + if [ -t 0 ]; then + echo + echo "[WARN] 未检测到订阅地址(CLASH_URL 为空)" + echo "请粘贴你的 Clash 订阅地址(直接回车将使用本地兜底配置启动):" + read -r -p "Clash URL: " input_url + input_url="$(printf '%s' "$input_url" | tr -d '\r')" + input_url="$(printf '%s' "$input_url" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" + + if [ -n "$input_url" ]; then + if ! printf '%s' "$input_url" | grep -Eq '^https?://'; then + echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2 + exit 2 + fi + URL="$input_url" + export CLASH_URL="$URL" + else + echo "[WARN] 未填写订阅地址,切换为本地兜底配置启动" + MANUAL_EMPTY_URL_FALLBACK=true + fi + else + echo "[ERR] CLASH_URL 为空(未配置订阅地址)" >&2 + exit 2 + fi +fi + #让 bash 子进程能拿到 export CLASH_URL="$URL" -# 只有在“需要在线更新订阅”的模式下才强制要求 URL -if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then - echo "[ERR] CLASH_URL 为空(未配置订阅地址)" - exit 2 -fi if [ -n "$URL" ] && ! printf '%s' "$URL" | grep -Eq '^https?://'; then echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2 exit 2 @@ -179,7 +202,7 @@ ensure_ui_links() { force_write_controller_and_ui() { local file="$1" - local controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" + local controller="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}" [ -n "$file" ] || return 1 @@ -249,11 +272,11 @@ fix_external_ui_by_safe_paths() { CLASH_HTTP_PORT="${CLASH_HTTP_PORT:-7890}" CLASH_SOCKS_PORT="${CLASH_SOCKS_PORT:-7891}" CLASH_REDIR_PORT="${CLASH_REDIR_PORT:-7892}" -CLASH_LISTEN_IP="${CLASH_LISTEN_IP:-127.0.0.1}" +CLASH_LISTEN_IP="${CLASH_LISTEN_IP:-0.0.0.0}" CLASH_ALLOW_LAN="${CLASH_ALLOW_LAN:-false}" EXTERNAL_CONTROLLER_ENABLED="${EXTERNAL_CONTROLLER_ENABLED:-true}" -EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" +EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}" ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}" @@ -410,9 +433,13 @@ ensure_fallback_config() { } SKIP_CONFIG_REBUILD=false -# systemd 模式下若 URL 为空:直接兜底启动 -if [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; then - echo -e "\033[33m[WARN]\033[0m SYSTEMD_MODE=true 且 CLASH_URL 为空,跳过订阅更新,使用本地兜底配置启动" +# systemd 模式下 URL 为空,或手动模式下用户回车跳过:直接兜底启动 +if { [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; } || [ "${MANUAL_EMPTY_URL_FALLBACK:-false}" = "true" ]; then + if [ "${SYSTEMD_MODE}" = "true" ]; then + echo -e "\033[33m[WARN]\033[0m SYSTEMD_MODE=true 且 CLASH_URL 为空,跳过订阅更新,使用本地兜底配置启动" + else + echo -e "\033[33m[WARN]\033[0m 手动模式未填写订阅地址,跳过订阅更新,使用本地兜底配置启动" + fi ensure_fallback_config || true SKIP_CONFIG_REBUILD=true fi From 5851833f5a5c5c686f3a0d56bcc3a03efa033857 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 00:04:32 +0800 Subject: [PATCH 07/19] Update uninstall.sh --- uninstall.sh | 204 +++++++++++++++++++++++++++------------------------ 1 file changed, 107 insertions(+), 97 deletions(-) diff --git a/uninstall.sh b/uninstall.sh index 117944f..2ae7030 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,142 +1,152 @@ #!/usr/bin/env bash set -euo pipefail -# ========================= -# 参数(对标 install.sh + install_systemd.sh) -# ========================= -Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}" -Service_Name="clash-for-linux" -Service_User="root" -Service_Group="root" -Unit_Path="/etc/systemd/system/${Service_Name}.service" +# More accurate uninstall for clash-for-linux +SERVICE_NAME="clash-for-linux" +UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service" -# ========================= -# 彩色输出 -# ========================= RED='\033[31m' GREEN='\033[32m' YELLOW='\033[33m' NC='\033[0m' - info() { echo -e "${GREEN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; } -# ========================= -# 前置校验 -# ========================= if [ "$(id -u)" -ne 0 ]; then err "需要 root 权限执行卸载脚本(请使用 sudo bash uninstall.sh)" exit 1 fi -info "开始卸载 ${Service_Name} ..." -info "Install_Dir=${Install_Dir}" +# Candidate install dirs: +# 1) explicit CLASH_INSTALL_DIR +# 2) working directory if it looks like clash-for-linux +# 3) service WorkingDirectory / ExecStart path inferred from unit +# 4) common defaults +candidates=() +[ -n "${CLASH_INSTALL_DIR:-}" ] && candidates+=("${CLASH_INSTALL_DIR}") +PWD_BASENAME="$(basename "${PWD}")" +if [ "$PWD_BASENAME" = "clash-for-linux" ] && [ -f "${PWD}/start.sh" ]; then + candidates+=("${PWD}") +fi -# ========================= -# 1) 优雅停止(优先 shutdown.sh,再 systemd) -# ========================= -if [ -f "${Install_Dir}/shutdown.sh" ]; then +if [ -f "$UNIT_PATH" ]; then + wd="$(sed -nE 's#^WorkingDirectory=(.*)#\1#p' "$UNIT_PATH" | head -n1 || true)" + [ -n "$wd" ] && candidates+=("$wd") + + exec_path="$(sed -nE 's#^ExecStart=/bin/bash[[:space:]]+([^[:space:]]+/start\.sh).*#\1#p' "$UNIT_PATH" | head -n1 || true)" + if [ -n "$exec_path" ]; then + candidates+=("$(dirname "$exec_path")") + fi +fi + +candidates+=("/root/clash-for-linux" "/opt/clash-for-linux") + +# normalize + uniq + choose first existing dir containing start.sh or shutdown.sh +INSTALL_DIR="" +declare -A seen +for d in "${candidates[@]}"; do + [ -n "$d" ] || continue + d="${d%/}" + [ -n "$d" ] || continue + if [ -z "${seen[$d]:-}" ]; then + seen[$d]=1 + if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -f "$d/shutdown.sh" ] || [ -d "$d/conf" ]; }; then + INSTALL_DIR="$d" + break + fi + fi +done + +if [ -z "$INSTALL_DIR" ]; then + warn "未能自动识别安装目录,将按候选路径继续清理 systemd / 环境文件。" +else + info "识别到安装目录: $INSTALL_DIR" +fi + +info "开始卸载 ${SERVICE_NAME} ..." + +# 1) graceful stop +if [ -n "$INSTALL_DIR" ] && [ -f "${INSTALL_DIR}/shutdown.sh" ]; then info "执行 shutdown.sh(优雅停止)..." - bash "${Install_Dir}/shutdown.sh" >/dev/null 2>&1 || true + bash "${INSTALL_DIR}/shutdown.sh" >/dev/null 2>&1 || true fi if command -v systemctl >/dev/null 2>&1; then info "停止并禁用 systemd 服务..." - systemctl stop "${Service_Name}.service" >/dev/null 2>&1 || true - systemctl disable "${Service_Name}.service" >/dev/null 2>&1 || true + systemctl stop "${SERVICE_NAME}.service" >/dev/null 2>&1 || true + systemctl disable "${SERVICE_NAME}.service" >/dev/null 2>&1 || true fi -# ========================= -# 2) 兜底:按 PID 文件杀进程(对标 unit 的 PIDFile) -# ========================= -PID_FILE="${Install_Dir}/temp/clash.pid" -if [ -f "$PID_FILE" ]; then - PID="$(cat "$PID_FILE" 2>/dev/null || true)" - if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then - info "检测到 PID=${PID},尝试停止..." - kill "$PID" 2>/dev/null || true - sleep 1 - if kill -0 "$PID" 2>/dev/null; then - warn "进程仍在运行,强制 kill -9 ${PID}" - kill -9 "$PID" 2>/dev/null || true +# 2) stop process by pid file from all likely dirs +for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do + [ -n "$d" ] || continue + PID_FILE="$d/temp/clash.pid" + if [ -f "$PID_FILE" ]; then + PID="$(cat "$PID_FILE" 2>/dev/null || true)" + if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then + info "检测到 PID=${PID}(来自 $PID_FILE),尝试停止..." + kill "$PID" 2>/dev/null || true + sleep 1 + if kill -0 "$PID" 2>/dev/null; then + warn "进程仍在运行,强制 kill -9 ${PID}" + kill -9 "$PID" 2>/dev/null || true + fi fi - ok "已停止 clash 进程(PIDFile)" + rm -f "$PID_FILE" || true fi -fi +done -# 再兜底:按进程名(系统可能有多个 clash,不建议无脑 pkill -9;先提示再杀) -if pgrep -x clash >/dev/null 2>&1; then - warn "检测到仍有 clash 进程存在(可能非本项目),尝试温和结束..." - pkill -x clash >/dev/null 2>&1 || true - sleep 1 -fi -if pgrep -x clash >/dev/null 2>&1; then - warn "仍残留 clash 进程,执行 pkill -9(可能影响其它 clash 实例)..." - pkill -9 -x clash >/dev/null 2>&1 || true -fi +# 兜底:按完整路径匹配,避免误杀其他 clash +pkill -f '/clash-for-linux/.*/clash' >/dev/null 2>&1 || true +pkill -f '/clash-for-linux/.*/mihomo' >/dev/null 2>&1 || true +sleep 1 +pkill -9 -f '/clash-for-linux/.*/clash' >/dev/null 2>&1 || true +pkill -9 -f '/clash-for-linux/.*/mihomo' >/dev/null 2>&1 || true -# ========================= -# 3) 删除 systemd unit(对标 install_systemd.sh) -# ========================= -if [ -f "$Unit_Path" ]; then - rm -f "$Unit_Path" - ok "已移除 systemd 单元: ${Unit_Path}" +# 3) remove unit and related files +if [ -f "$UNIT_PATH" ]; then + rm -f "$UNIT_PATH" + ok "已移除 systemd 单元: ${UNIT_PATH}" fi - -# drop-in(万一用户自定义过) -if [ -d "/etc/systemd/system/${Service_Name}.service.d" ]; then - rm -rf "/etc/systemd/system/${Service_Name}.service.d" - ok "已移除 drop-in: /etc/systemd/system/${Service_Name}.service.d" +if [ -d "/etc/systemd/system/${SERVICE_NAME}.service.d" ]; then + rm -rf "/etc/systemd/system/${SERVICE_NAME}.service.d" + ok "已移除 drop-in: /etc/systemd/system/${SERVICE_NAME}.service.d" fi - if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload >/dev/null 2>&1 || true systemctl reset-failed >/dev/null 2>&1 || true fi -# ========================= -# 4) 清理默认配置/环境脚本/命令入口 -# ========================= -if [ -f "/etc/default/${Service_Name}" ]; then - rm -f "/etc/default/${Service_Name}" - ok "已移除: /etc/default/${Service_Name}" +# 4) cleanup env / command entry +rm -f "/etc/default/${SERVICE_NAME}" >/dev/null 2>&1 || true +rm -f "/etc/profile.d/clash-for-linux.sh" >/dev/null 2>&1 || true +rm -f "/usr/local/bin/clashctl" >/dev/null 2>&1 || true +for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do + [ -n "$d" ] || continue + rm -f "$d/temp/clash-for-linux.sh" >/dev/null 2>&1 || true +done + +# 5) remove install dirs +removed_any=false +for d in "${INSTALL_DIR:-}" "/root/clash-for-linux" "/opt/clash-for-linux"; do + [ -n "$d" ] || continue + if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -d "$d/conf" ] || [ "$d" = "$INSTALL_DIR" ]; }; then + rm -rf "$d" + ok "已移除安装目录: $d" + removed_any=true + fi +done + +if [ "$removed_any" = false ]; then + warn "未发现可删除的安装目录" fi -# 运行时 Env_File 可能写到 /etc/profile.d 或 temp,这里都清 -if [ -f "/etc/profile.d/clash-for-linux.sh" ]; then - rm -f "/etc/profile.d/clash-for-linux.sh" - ok "已移除: /etc/profile.d/clash-for-linux.sh" -fi - -if [ -f "${Install_Dir}/temp/clash-for-linux.sh" ]; then - rm -f "${Install_Dir}/temp/clash-for-linux.sh" || true - ok "已移除: ${Install_Dir}/temp/clash-for-linux.sh" -fi - -if [ -f "/usr/local/bin/clashctl" ]; then - rm -f "/usr/local/bin/clashctl" - ok "已移除: /usr/local/bin/clashctl" -fi - -# ========================= -# 5) 删除安装目录 -# ========================= -if [ -d "$Install_Dir" ]; then - rm -rf "$Install_Dir" - ok "已移除安装目录: ${Install_Dir}" -else - warn "未找到安装目录: ${Install_Dir}" -fi - -# ========================= -# 7) 提示:当前终端代理变量需要手动清 -# ========================= echo warn "如果你曾执行 proxy_on,当前终端可能仍保留代理环境变量。可执行:" echo " unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY" echo " # 或关闭终端重新打开" echo -ok "卸载完成(root-only 模式)✅" +ok "卸载完成 ✅" From f03e75166e880a6e424e2774a53db44d7c4fe1b1 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 00:20:13 +0800 Subject: [PATCH 08/19] Update install.sh --- install.sh | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index cec8d78..d29cce2 100755 --- a/install.sh +++ b/install.sh @@ -478,23 +478,37 @@ if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then fi CONF_DIR="$Install_Dir/conf" -CONF_FILE="$CONF_DIR/config.yaml" +TEMP_DIR="$Install_Dir/temp" SECRET_VAL="" -if wait_secret_ready "$CONF_FILE" 6; then - SECRET_VAL="$(read_secret_from_config "$CONF_FILE" || true)" -fi +SECRET_FILE="" + +for f in \ + "$TEMP_DIR/config.yaml" \ + "$CONF_DIR/config.yaml" +do + if wait_secret_ready "$f" 12; then + SECRET_VAL="$(read_secret_from_config "$f" || true)" + if [[ -n "$SECRET_VAL" ]]; then + SECRET_FILE="$f" + break + fi + fi +done dash="http://${api_host}:${api_port}/ui" log "🌐 Dashboard:$(url "$dash")" +SHOW_FILE="${SECRET_FILE:-$CONF_DIR/config.yaml}" + if [[ -n "$SECRET_VAL" ]]; then - MASKED="${SECRET_VAL}" + MASKED="${SECRET_VAL:0:4}****${SECRET_VAL: -4}" log "🔐 Secret:${C_YELLOW}${MASKED}${C_NC}" - log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" + log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$SHOW_FILE\" | head -n 1")" else log "🔐 Secret:${C_YELLOW}启动中暂未读到(稍后再试)${C_NC}" - log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" + log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_DIR/config.yaml\" | head -n 1")" + log " 也可检查运行态:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$TEMP_DIR/config.yaml\" | head -n 1")" fi # ========================= From f168016972adb0b0e2cb1566fe1cf34c166fb695 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 00:27:45 +0800 Subject: [PATCH 09/19] Update start.sh --- start.sh | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/start.sh b/start.sh index 59c0eb8..f34511c 100644 --- a/start.sh +++ b/start.sh @@ -824,12 +824,30 @@ Clash_Bin="$(resolve_clash_bin "$Server_Dir" "$CpuArch")" ReturnStatus=$? if [ "$ReturnStatus" -eq 0 ]; then + echo '' + if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then + echo -e "Clash Dashboard 访问地址: http://${EXTERNAL_CONTROLLER}/ui" + + SHOW_SECRET="${CLASH_SHOW_SECRET:-false}" + SHOW_SECRET_MASKED="${CLASH_SHOW_SECRET_MASKED:-true}" + + if [ "$SHOW_SECRET" = "true" ]; then + echo -e "Secret: ${Secret}" + elif [ "$SHOW_SECRET_MASKED" = "true" ]; then + masked="${Secret:0:4}****${Secret: -4}" + echo -e "Secret: ${masked} (set CLASH_SHOW_SECRET=true to show full)" + else + echo -e "Secret: 已生成(未显示)。查看:${CONFIG_FILE} 或 .env" + fi + else + echo -e "External Controller (Dashboard) 已禁用" + fi + echo '' + if [ "${SYSTEMD_MODE:-false}" = "true" ]; then echo "[INFO] SYSTEMD_MODE=true,前台启动交给 systemd 监管" echo "[INFO] Using config: $CONFIG_FILE" echo "[INFO] Using runtime dir: $RUNTIME_DIR" - - # systemd 前台:只用 -f 指定配置文件,-d 作为工作目录 exec "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" else echo "[INFO] 后台启动 (nohup)" @@ -852,29 +870,6 @@ else if_success "$Text5" "$Text6" "$ReturnStatus" fi -#################### 输出信息 #################### - -echo '' -if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - echo -e "Clash Dashboard 访问地址: http://${EXTERNAL_CONTROLLER}/ui" - - SHOW_SECRET="${CLASH_SHOW_SECRET:-false}" - SHOW_SECRET_MASKED="${CLASH_SHOW_SECRET_MASKED:-true}" - - if [ "$SHOW_SECRET" = "true" ]; then - echo -e "Secret: ${Secret}" - elif [ "$SHOW_SECRET_MASKED" = "true" ]; then - # 脱敏:前4后4 - masked="${Secret:0:4}****${Secret: -4}" - echo -e "Secret: ${masked} (set CLASH_SHOW_SECRET=true to show full)" - else - echo -e "Secret: 已生成(未显示)。查看:/opt/clash-for-linux/conf/config.yaml 或 .env" - fi -else - echo -e "External Controller (Dashboard) 已禁用" -fi -echo '' - #################### 写入代理环境变量文件 #################### Env_File="${CLASH_ENV_FILE:-}" From a1c2b9affe828d9990e346eb2f237410863282d4 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 00:27:47 +0800 Subject: [PATCH 10/19] Update uninstall.sh --- uninstall.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/uninstall.sh b/uninstall.sh index 2ae7030..414812d 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -129,19 +129,19 @@ for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do done # 5) remove install dirs -removed_any=false -for d in "${INSTALL_DIR:-}" "/root/clash-for-linux" "/opt/clash-for-linux"; do - [ -n "$d" ] || continue - if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -d "$d/conf" ] || [ "$d" = "$INSTALL_DIR" ]; }; then - rm -rf "$d" - ok "已移除安装目录: $d" - removed_any=true - fi -done +# removed_any=false +# for d in "${INSTALL_DIR:-}" "/root/clash-for-linux" "/opt/clash-for-linux"; do +# [ -n "$d" ] || continue +# if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -d "$d/conf" ] || [ "$d" = "$INSTALL_DIR" ]; }; then +# rm -rf "$d" +# ok "已移除安装目录: $d" +# removed_any=true +# fi +# done -if [ "$removed_any" = false ]; then - warn "未发现可删除的安装目录" -fi +# if [ "$removed_any" = false ]; then +# warn "未发现可删除的安装目录" +# fi echo warn "如果你曾执行 proxy_on,当前终端可能仍保留代理环境变量。可执行:" From 2255f065be1692f071d4bfd060e99aa278dc248f Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:06:13 +0800 Subject: [PATCH 11/19] Update install.sh --- install.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index d29cce2..1bc0788 100755 --- a/install.sh +++ b/install.sh @@ -225,7 +225,7 @@ prompt_clash_url_if_empty CLASH_HTTP_PORT=${CLASH_HTTP_PORT:-7890} CLASH_SOCKS_PORT=${CLASH_SOCKS_PORT:-7891} CLASH_REDIR_PORT=${CLASH_REDIR_PORT:-7892} -EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} +EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-0.0.0.0:9090} parse_port() { local raw="$1" @@ -360,10 +360,10 @@ if command -v systemctl >/dev/null 2>&1; then CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh" if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then - systemctl enable "${Service_Name}.service" >/dev/null 2>&1 || true + systemctl start "${Service_Name}.service" || true fi if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then - systemctl start "${Service_Name}.service" >/dev/null 2>&1 || true + systemctl start "${Service_Name}.service" || true fi if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then @@ -487,12 +487,10 @@ for f in \ "$TEMP_DIR/config.yaml" \ "$CONF_DIR/config.yaml" do - if wait_secret_ready "$f" 12; then - SECRET_VAL="$(read_secret_from_config "$f" || true)" - if [[ -n "$SECRET_VAL" ]]; then - SECRET_FILE="$f" - break - fi + SECRET_VAL="$(read_secret_from_config "$f" 2>/dev/null || true)" + if [[ -n "$SECRET_VAL" ]]; then + SECRET_FILE="$f" + break fi done From e21f7428b5c1225f8413f6e560b55f23285af578 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:06:15 +0800 Subject: [PATCH 12/19] Update get_cpu_arch.sh --- scripts/get_cpu_arch.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/get_cpu_arch.sh b/scripts/get_cpu_arch.sh index 737309b..a9a98e2 100644 --- a/scripts/get_cpu_arch.sh +++ b/scripts/get_cpu_arch.sh @@ -47,4 +47,5 @@ else exitWithError "Unsupported Linux distribution" fi -info "CPU architecture: $CpuArch" +log_info() { echo "[INFO] $*"; } +log_info "CPU architecture: $CpuArch" \ No newline at end of file From f8a35b7bc433d03df8e4f413b84e38e3de05d8c6 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:06:17 +0800 Subject: [PATCH 13/19] Update install_systemd.sh --- scripts/install_systemd.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/install_systemd.sh b/scripts/install_systemd.sh index b581131..132e486 100755 --- a/scripts/install_systemd.sh +++ b/scripts/install_systemd.sh @@ -35,6 +35,8 @@ Description=Clash for Linux (Mihomo) Documentation=https://github.com/wnlen/clash-for-linux After=network-online.target nss-lookup.target Wants=network-online.target +StartLimitIntervalSec=0 +StartLimitBurst=10 [Service] Type=simple @@ -55,7 +57,6 @@ ExecReload=/bin/kill -HUP \$MAINPID # 常驻策略:即使上层脚本正常退出,也要由 systemd 拉回 Restart=always RestartSec=5s -StartLimitIntervalSec=0 # 停止与日志 KillMode=mixed From 50f0eec55cf475c17b4681e389197458f5e3d841 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:20:31 +0800 Subject: [PATCH 14/19] Update install.sh --- install.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/install.sh b/install.sh index 1bc0788..83ffa39 100755 --- a/install.sh +++ b/install.sh @@ -483,15 +483,18 @@ TEMP_DIR="$Install_Dir/temp" SECRET_VAL="" SECRET_FILE="" -for f in \ - "$TEMP_DIR/config.yaml" \ - "$CONF_DIR/config.yaml" -do - SECRET_VAL="$(read_secret_from_config "$f" 2>/dev/null || true)" - if [[ -n "$SECRET_VAL" ]]; then - SECRET_FILE="$f" - break - fi +for _ in {1..15}; do + for f in \ + "$TEMP_DIR/config.yaml" \ + "$CONF_DIR/config.yaml" + do + SECRET_VAL="$(read_secret_from_config "$f" 2>/dev/null || true)" + if [[ -n "$SECRET_VAL" ]]; then + SECRET_FILE="$f" + break 2 + fi + done + sleep 0.2 done dash="http://${api_host}:${api_port}/ui" @@ -500,9 +503,9 @@ log "🌐 Dashboard:$(url "$dash")" SHOW_FILE="${SECRET_FILE:-$CONF_DIR/config.yaml}" if [[ -n "$SECRET_VAL" ]]; then - MASKED="${SECRET_VAL:0:4}****${SECRET_VAL: -4}" + MASKED="${SECRET_VAL}" log "🔐 Secret:${C_YELLOW}${MASKED}${C_NC}" - log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$SHOW_FILE\" | head -n 1")" + # log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$SHOW_FILE\" | head -n 1")" else log "🔐 Secret:${C_YELLOW}启动中暂未读到(稍后再试)${C_NC}" log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_DIR/config.yaml\" | head -n 1")" From ba8f43feb38cdab84012987e9b0d057ac90d8220 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:40:33 +0800 Subject: [PATCH 15/19] Update .env --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 90a360d..ba52ac7 100644 --- a/.env +++ b/.env @@ -42,7 +42,7 @@ CLASH_SHOW_SECRET_MASKED=true # - 默认仅监听本机:127.0.0.1:9090 (推荐) # - 如需局域网访问再改成:0.0.0.0:9090,并确保 CLASH_SECRET 足够复杂 export EXTERNAL_CONTROLLER_ENABLED=true -export EXTERNAL_CONTROLLER='127.0.0.1:9090' +export EXTERNAL_CONTROLLER='0.0.0.0:9090' # ------------------------- # 3) 代理端口与监听(常用) From f41cd5844839545a9be5205d2c1407d0637bcb60 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:40:36 +0800 Subject: [PATCH 16/19] Update install.sh --- install.sh | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 83ffa39..42de59b 100755 --- a/install.sh +++ b/install.sh @@ -225,7 +225,7 @@ prompt_clash_url_if_empty CLASH_HTTP_PORT=${CLASH_HTTP_PORT:-7890} CLASH_SOCKS_PORT=${CLASH_SOCKS_PORT:-7891} CLASH_REDIR_PORT=${CLASH_REDIR_PORT:-7892} -EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-0.0.0.0:9090} +EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} parse_port() { local raw="$1" @@ -473,10 +473,32 @@ section "控制面板" api_port="$(parse_port "${EXTERNAL_CONTROLLER}")" api_host="${EXTERNAL_CONTROLLER%:*}" +get_public_ip() { + if command -v curl >/dev/null 2>&1; then + curl -4 -fsS --max-time 3 https://api.ipify.org 2>/dev/null \ + || curl -4 -fsS --max-time 3 https://ifconfig.me 2>/dev/null \ + || curl -4 -fsS --max-time 3 https://ipv4.icanhazip.com 2>/dev/null \ + || true + elif command -v wget >/dev/null 2>&1; then + wget -qO- --timeout=3 https://api.ipify.org 2>/dev/null \ + || wget -qO- --timeout=3 https://ifconfig.me 2>/dev/null \ + || wget -qO- --timeout=3 https://ipv4.icanhazip.com 2>/dev/null \ + || true + else + true + fi +} + if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then api_host="127.0.0.1" fi +if [[ "$api_host" == "0.0.0.0" ]] || [[ "$api_host" == "::" ]] || [[ "$api_host" == "localhost" ]]; then + api_host="$(get_public_ip | tr -d '\r\n')" + [[ -z "$api_host" ]] && api_host="$(hostname -I 2>/dev/null | awk '{print $1}')" + [[ -z "$api_host" ]] && api_host="127.0.0.1" +fi + CONF_DIR="$Install_Dir/conf" TEMP_DIR="$Install_Dir/temp" From b7d42c8b0e9b3e9527b22c11c1ae03526fbac935 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:40:39 +0800 Subject: [PATCH 17/19] Update start.sh --- start.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/start.sh b/start.sh index f34511c..7b702cb 100644 --- a/start.sh +++ b/start.sh @@ -349,6 +349,10 @@ ensure_subconverter() { local bin="${Server_Dir}/tools/subconverter/subconverter" local port="25500" + # 自动获取服务器IP + local host_ip + host_ip="$(hostname -I | awk '{print $1}')" + # 没有二进制直接跳过 if [ ! -x "$bin" ]; then echo "[WARN] subconverter bin not found: $bin" @@ -358,20 +362,20 @@ ensure_subconverter() { # 已在监听则认为就绪 if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" + export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://${host_ip}:${port}}" export SUBCONVERTER_READY="true" return 0 fi - # 启动(后台) + # 启动(监听所有IP) echo "[INFO] starting subconverter..." - (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" >/dev/null 2>&1 &) + (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" -listen 0.0.0.0:${port} >/dev/null 2>&1 &) # 等待端口起来 for _ in 1 2 3 4 5; do sleep 1 if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" + export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://${host_ip}:${port}}" export SUBCONVERTER_READY="true" echo "[OK] subconverter ready at ${SUBCONVERTER_URL}" return 0 From a61a10688202b5e6ef9ba9d1c07cfb6dcc8ca63a Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:41:46 +0800 Subject: [PATCH 18/19] Update uninstall.sh --- uninstall.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/uninstall.sh b/uninstall.sh index 414812d..2ae7030 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -129,19 +129,19 @@ for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do done # 5) remove install dirs -# removed_any=false -# for d in "${INSTALL_DIR:-}" "/root/clash-for-linux" "/opt/clash-for-linux"; do -# [ -n "$d" ] || continue -# if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -d "$d/conf" ] || [ "$d" = "$INSTALL_DIR" ]; }; then -# rm -rf "$d" -# ok "已移除安装目录: $d" -# removed_any=true -# fi -# done +removed_any=false +for d in "${INSTALL_DIR:-}" "/root/clash-for-linux" "/opt/clash-for-linux"; do + [ -n "$d" ] || continue + if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -d "$d/conf" ] || [ "$d" = "$INSTALL_DIR" ]; }; then + rm -rf "$d" + ok "已移除安装目录: $d" + removed_any=true + fi +done -# if [ "$removed_any" = false ]; then -# warn "未发现可删除的安装目录" -# fi +if [ "$removed_any" = false ]; then + warn "未发现可删除的安装目录" +fi echo warn "如果你曾执行 proxy_on,当前终端可能仍保留代理环境变量。可执行:" From 894e3801054363583a9d20a628664077c258bed8 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Tue, 17 Mar 2026 01:47:22 +0800 Subject: [PATCH 19/19] Update start.sh --- start.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/start.sh b/start.sh index 7b702cb..f2be2b1 100644 --- a/start.sh +++ b/start.sh @@ -142,10 +142,10 @@ fi # 兜底生成随机 secret if [ -z "$Secret" ]; then if command -v openssl >/dev/null 2>&1; then - Secret="$(openssl rand -hex 32)" + Secret="$(openssl rand -hex 16)" else # 32 bytes -> 64 hex chars - Secret="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" + Secret="$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')" fi fi