From 0ccf7193cd91bc5bed7d90217381beaa81424e38 Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Mon, 19 Jan 2026 20:44:46 +0800
Subject: [PATCH 01/33] Update README.md
---
README.md | 18 +++++-------------
1 file changed, 5 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 69b698b..b2669a9 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,10 @@
本项目主要解决以下问题:
-
-- ❌ 官方 Clash 二进制下载、架构区分、配置部署繁琐
-- ❌ 手动管理 Clash 进程、端口、环境变量不稳定
-- ❌ systemd 服务、权限、安全配置缺乏统一方案
-- ❌ 多订阅 / 配置混乱,升级和回滚成本高
+- ✕ 官方 Clash 二进制下载、架构区分、配置部署繁琐
+- ✕ 手动管理 Clash 进程、端口、环境变量不稳定
+- ✕ systemd 服务、权限、安全配置缺乏统一方案
+- ✕ 多订阅 / 配置混乱,升级和回滚成本高
### 核心特性
@@ -28,7 +27,7 @@
- 自动生成或自定义 Secret
- 默认开启 TLS 校验
- 🧪 **端口自动检测与分配**,避免冲突
-- 🔄 **多订阅管理(clashctl)**,支持订阅切换、更新、日志查看
+- 🔄 **多订阅管理(clashctl)**,支持自动订阅切换(Vmess / V2Ray、Shadowsocks (SS)、ShadowsocksR (SSR)、Trojan、VLESS、Hysteria / Hysteria2、TUIC、HTTP / SOCKS5)
- 🧠 **Mixin 机制**,可按需追加/覆盖 Clash 配置
- 🌐 **Tun 模式支持**(需 Clash Meta / Premium)
@@ -46,13 +45,6 @@
- ❌ 不适合只想“点点 UI 就用”的纯桌面用户
- ❌ 不包含任何节点、机场或订阅推荐
-### 更新状态
-
-📅 **持续维护中**
- 最近更新:**2026-01-15**
-
-
-
# 安装
> **推荐路径优先,一键安装即可满足 90% 使用场景。**
From 48d2d90ee55dafca0bff8003bb38b53f70c13740 Mon Sep 17 00:00:00 2001
From: wnlen <62139570+wnlen@users.noreply.github.com>
Date: Tue, 20 Jan 2026 08:03:47 +0800
Subject: [PATCH 02/33] Update README.md
---
README.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/README.md b/README.md
index b2669a9..17b5af3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-[TOC]
-
# 关于本项目
**clash-for-linux** 是一个面向 Linux 服务器/桌面环境的 **Clash 自动化运行与管理脚本集**。
@@ -341,4 +339,4 @@ sudo chmod +x /etc/rc.local
3. 程序日志中出现`error: unsupported rule type RULE-SET`报错,解决方法查看官方[WIKI](https://github.com/Dreamacro/clash/wiki/FAQ#error-unsupported-rule-type-rule-set)
## ⭐ Star History
-[](https://star-history.com/#wnlen/clash-for-linux&Date)
\ No newline at end of file
+[](https://star-history.com/#wnlen/clash-for-linux&Date)
From 87397bb62ee52630b335863004e66c3f81f59499 Mon Sep 17 00:00:00 2001
From: Taoyu Yang
Date: Sat, 14 Feb 2026 08:35:35 +0000
Subject: [PATCH 03/33] Support docker containers
---
clashctl | 9 ++++---
install.sh | 75 ++++++++++++++++++++++++++++++++++++++----------------
start.sh | 5 ++++
3 files changed, 64 insertions(+), 25 deletions(-)
diff --git a/clashctl b/clashctl
index 4d26e2e..e7f02ea 100755
--- a/clashctl
+++ b/clashctl
@@ -35,7 +35,9 @@ PID_FILE="$CLASH_HOME/temp/clash.pid"
SUBSCRIPTION_FILE="$CLASH_HOME/conf/subscriptions.list"
use_systemd() {
- command -v systemctl >/dev/null 2>&1
+ command -v systemctl >/dev/null 2>&1 || return 1
+ systemctl show --property=Version --value >/dev/null 2>&1 || return 1
+ return 0
}
action_with_systemd() {
@@ -78,10 +80,11 @@ set_env_var() {
echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2
exit 1
fi
- local escaped
+ local escaped escaped_sed
escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g")
+ escaped_sed=$(printf "%s" "$escaped" | sed 's/[\\&@]/\\&/g')
if grep -q "^export ${key}=" "$ENV_FILE"; then
- sed -i "s@^export ${key}=.*@export ${key}='${escaped}'@" "$ENV_FILE"
+ sed -i "s@^export ${key}=.*@export ${key}='${escaped_sed}'@" "$ENV_FILE"
else
echo "export ${key}='${escaped}'" >> "$ENV_FILE"
fi
diff --git a/install.sh b/install.sh
index f397e45..3b7aef9 100755
--- a/install.sh
+++ b/install.sh
@@ -351,35 +351,57 @@ read_secret_from_config() {
printf '%s' "$s"
}
+# 判断 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
+ return 0
+}
+
# =========================
# systemd 安装与启动
# =========================
Service_Enabled="unknown"
Service_Started="unknown"
+Systemd_Usable="false"
-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 systemd_ready; then
+ Systemd_Usable="true"
+fi
- 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
+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"
- if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then
- Service_Enabled="enabled"
+ 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
+
+ if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then
+ Service_Enabled="enabled"
+ else
+ Service_Enabled="disabled"
+ fi
+
+ if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then
+ Service_Started="active"
+ else
+ Service_Started="inactive"
+ fi
else
+ info "已按配置跳过 systemd 服务安装与启动(CLASH_ENABLE_SERVICE=false 且 CLASH_START_SERVICE=false)"
Service_Enabled="disabled"
- fi
-
- if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then
- Service_Started="active"
- else
Service_Started="inactive"
fi
else
- warn "未检测到 systemd,已跳过服务单元生成"
+ if command -v systemctl >/dev/null 2>&1; then
+ warn "检测到 systemctl 命令,但当前环境不可用 systemd(常见于 Docker 容器),已跳过服务单元生成"
+ else
+ warn "未检测到 systemd,已跳过服务单元生成"
+ fi
fi
# =========================
@@ -394,7 +416,7 @@ install_profiled() {
[ "$http_port" = "auto" ] && http_port="7890"
# 只写 IPv4 loopback,避免某些环境 ::1 解析问题
- sudo tee "$PROFILED_FILE" >/dev/null </dev/null </dev/null 2>&1; then
+if [ "$Systemd_Usable" = "true" ]; then
section "服务状态"
se="${Service_Enabled:-unknown}"
@@ -464,6 +486,11 @@ if command -v systemctl >/dev/null 2>&1; then
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
# =========================
@@ -514,7 +541,11 @@ else
log " $(cmd "sudo bash -c 'echo \"CLASH_URL=<订阅地址>\" > ${ENV_FILE}'")"
log ""
log "配置完成后重启服务:"
- log " $(cmd "sudo systemctl restart ${Service_Name}.service")"
+ if [ "$Systemd_Usable" = "true" ]; then
+ log " $(cmd "sudo systemctl restart ${Service_Name}.service")"
+ else
+ log " $(cmd "sudo clashctl restart")"
+ fi
fi
# =========================
@@ -535,9 +566,9 @@ fi
# 启动后快速诊断
# =========================
sleep 1
-if command -v journalctl >/dev/null 2>&1; then
+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
-fi
\ No newline at end of file
+fi
diff --git a/start.sh b/start.sh
index 80b45f2..9bd352d 100644
--- a/start.sh
+++ b/start.sh
@@ -88,6 +88,7 @@ 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"
@@ -97,6 +98,10 @@ 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:-}"
From 3740c60d8975ef04eae2393d7ab78d88eee77bf9 Mon Sep 17 00:00:00 2001
From: Jalen Yan
Date: Tue, 3 Mar 2026 11:08:08 +0800
Subject: [PATCH 04/33] Fix brace parsing bug in CLASH_DOWNLOAD_URL_TEMPLATE
default value
The `}` in `{arch}` prematurely closes the `${...:-...}` parameter
expansion, causing the default URL to be truncated to
`clash-{arch.gz}` instead of `clash-{arch}.gz`. The subsequent
`{arch}` replacement then fails silently.
Extract the default URL into a separate variable to avoid the
nested brace conflict.
---
scripts/resolve_clash.sh | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scripts/resolve_clash.sh b/scripts/resolve_clash.sh
index 06a7357..d1edb6f 100755
--- a/scripts/resolve_clash.sh
+++ b/scripts/resolve_clash.sh
@@ -36,7 +36,8 @@ download_clash_bin() {
return 1
fi
- download_url="${CLASH_DOWNLOAD_URL_TEMPLATE:-https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz}"
+ local _default_url="https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz"
+ download_url="${CLASH_DOWNLOAD_URL_TEMPLATE:-$_default_url}"
if [ -z "$download_url" ]; then
echo -e "\033[33m[WARN] 未设置 CLASH_DOWNLOAD_URL_TEMPLATE,跳过 Clash 内核自动下载\033[0m"
return 1
From 057733ca08a370b98469eac2ff3d201e1dead7a6 Mon Sep 17 00:00:00 2001
From: wnlen <62139570+wnlen@users.noreply.github.com>
Date: Tue, 3 Mar 2026 21:37:08 +0800
Subject: [PATCH 05/33] Update README with public access setup instructions
Add instructions for public access configuration and service restart.
---
README.md | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/README.md b/README.md
index 17b5af3..f3706cd 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,19 @@ http://127.0.0.1:9090/ui
> 不建议直接将管理端口暴露到公网。
+如果执意想要**公网访问**
+编辑 `.env` 文件,设置公网访问并修改对外端口(建议范围20000–60000):
+
+```
+sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9099" > /opt/clash-for-linux/.env'
+```
+
+配置完成后,**重启服务使配置生效**:
+
+```
+sudo systemctl restart clash-for-linux.service
+```
+
------
## ▶️ 开启 / 关闭系统代理
From 6725ff5555949f702e6f8b061e7a9294ed628b00 Mon Sep 17 00:00:00 2001
From: Arvin <62139570+wnlen@users.noreply.github.com>
Date: Tue, 3 Mar 2026 22:06:20 +0800
Subject: [PATCH 06/33] Clarify public access instructions in README
Rephrase instructions for public access in README.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f3706cd..393a3fc 100644
--- a/README.md
+++ b/README.md
@@ -117,7 +117,7 @@ http://127.0.0.1:9090/ui
> 不建议直接将管理端口暴露到公网。
-如果执意想要**公网访问**
+如果想要**公网访问**
编辑 `.env` 文件,设置公网访问并修改对外端口(建议范围20000–60000):
```
From a8014e6759e1ddacc804fbfd4dde8d1cb1f0ab99 Mon Sep 17 00:00:00 2001
From: Arvin <62139570+wnlen@users.noreply.github.com>
Date: Tue, 3 Mar 2026 22:10:23 +0800
Subject: [PATCH 07/33] Update README with clearer public access instructions
Clarify instructions for public access and key retrieval
---
README.md | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 393a3fc..0fa5908 100644
--- a/README.md
+++ b/README.md
@@ -118,7 +118,7 @@ http://127.0.0.1:9090/ui
> 不建议直接将管理端口暴露到公网。
如果想要**公网访问**
-编辑 `.env` 文件,设置公网访问并修改对外端口(建议范围20000–60000):
+编辑 `.env` 文件,设置公网访问并修改对外端口:
```
sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9099" > /opt/clash-for-linux/.env'
@@ -130,6 +130,13 @@ sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9099" > /opt/clash-for-linux/.en
sudo systemctl restart clash-for-linux.service
```
+密钥留空时:脚本可自动生成随机值
+获取密钥命令:
+```
+sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "/opt/clash-for-linux/conf/config.yaml" | head -n 1
+```
+
+
------
## ▶️ 开启 / 关闭系统代理
From 9858d27216ae8dfd91abcdc6934c0b24cb2dbceb Mon Sep 17 00:00:00 2001
From: Arvin <62139570+wnlen@users.noreply.github.com>
Date: Tue, 3 Mar 2026 22:12:22 +0800
Subject: [PATCH 08/33] Revise public access setup instructions in README
Updated instructions for public access configuration in README.
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 0fa5908..56961cf 100644
--- a/README.md
+++ b/README.md
@@ -118,10 +118,10 @@ http://127.0.0.1:9090/ui
> 不建议直接将管理端口暴露到公网。
如果想要**公网访问**
-编辑 `.env` 文件,设置公网访问并修改对外端口:
+编辑 `.env` 文件,设置公网访问(对外端口不用改,改了机器人也能扫到,密钥设置的长点就行):
```
-sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9099" > /opt/clash-for-linux/.env'
+sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9090" > /opt/clash-for-linux/.env'
```
配置完成后,**重启服务使配置生效**:
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 09/33] 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 10/33] 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 "[31m[ERROR] 需要 root 权限来安装 systemd 单元[0m"
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 "[32m[OK] 已生成 systemd 单元: ${Unit_Path}[0m"
+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 11/33] 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 12/33] 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 13/33] 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 14/33] 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 15/33] 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 16/33] 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 17/33] 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 18/33] 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 19/33] 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 20/33] 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 21/33] 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 22/33] 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 23/33] 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 24/33] 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 25/33] 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 26/33] 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 27/33] 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
From 70f88187afdb2bf8815382f2c4bac7444d83107d Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Tue, 17 Mar 2026 01:57:35 +0800
Subject: [PATCH 28/33] Update .env
---
.env | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.env b/.env
index ba52ac7..d94c22e 100644
--- a/.env
+++ b/.env
@@ -15,6 +15,11 @@
# 示例:export CLASH_URL='https://example.com/sub?token=xxx'
export CLASH_URL=''
+# 是否自动更新 Clash 订阅配置:
+# true = 启动时检查订阅并重新下载/转换配置
+# false = 禁用自动更新,直接使用本地已有 config.yaml
+export CLASH_AUTO_UPDATE="false"
+
# 订阅请求头(可选)
# 常见机场需要 User-Agent;如不需要可留空
export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39'
From 79f9e5167cacdca6341b20566ae0e9aafc75cacf Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Tue, 17 Mar 2026 02:01:20 +0800
Subject: [PATCH 29/33] Update start.sh
---
start.sh | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/start.sh b/start.sh
index f2be2b1..ec2c65b 100644
--- a/start.sh
+++ b/start.sh
@@ -448,8 +448,10 @@ if { [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; } || [ "${MANUAL_EMPTY
SKIP_CONFIG_REBUILD=true
fi
+CLASH_AUTO_UPDATE="${CLASH_AUTO_UPDATE:-true}"
+
#################### Clash 订阅地址检测及配置文件下载 ####################
-if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
+if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" = "true" ]; then
echo -e '\n正在检测订阅地址...'
Text1="Clash订阅地址可访问!"
Text2="Clash订阅地址不可访问!"
@@ -494,7 +496,7 @@ if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
fi
#################### 下载订阅并生成 config.yaml(非兜底路径) ####################
-if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
+if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" = "true" ]; then
ensure_subconverter || true
echo -e '\n正在下载Clash配置文件...'
Text3="配置文件clash.yaml下载成功!"
@@ -648,6 +650,12 @@ if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
fi
fi
+if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" != "true" ]; then
+ echo -e "\033[33m[WARN]\033[0m 已关闭自动更新订阅,使用本地已有配置启动"
+ ensure_fallback_config || true
+ SKIP_CONFIG_REBUILD=true
+fi
+
# =========================================================
# 判断订阅是否已是完整 Clash YAML(Meta / Mihomo / Premium)
# 若是完整配置,则直接使用,跳过后续代理拆解与拼接
From 9b97197acbe50a7fafcde0d6b1f77e5dfc48287a Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Tue, 17 Mar 2026 02:06:49 +0800
Subject: [PATCH 30/33] Update start.sh
---
start.sh | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/start.sh b/start.sh
index ec2c65b..244189a 100644
--- a/start.sh
+++ b/start.sh
@@ -660,7 +660,7 @@ fi
# 判断订阅是否已是完整 Clash YAML(Meta / Mihomo / Premium)
# 若是完整配置,则直接使用,跳过后续代理拆解与拼接
# =========================================================
-if grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$Temp_Dir/clash.yaml"; then
+if [ -s "$Temp_Dir/clash.yaml" ] && 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"
@@ -681,8 +681,8 @@ if grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$Temp_Dir/clash.ya
ln -sfn "$Dashboard_Src" "$Conf_Dir/ui" 2>/dev/null || true
fi
- SKIP_CONFIG_REBUILD=true
- fi
+ SKIP_CONFIG_REBUILD=true
+fi
#################### 订阅转换/拼接(非兜底路径) ####################
if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
From b84b17f7cb36eeebcff02379cbe2890b8a9b8e88 Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Tue, 17 Mar 2026 02:11:50 +0800
Subject: [PATCH 31/33] Update start.sh
---
start.sh | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/start.sh b/start.sh
index 244189a..b423362 100644
--- a/start.sh
+++ b/start.sh
@@ -651,8 +651,21 @@ if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" = "true" ]; th
fi
if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" != "true" ]; then
- echo -e "\033[33m[WARN]\033[0m 已关闭自动更新订阅,使用本地已有配置启动"
- ensure_fallback_config || true
+ echo -e "\033[33m[WARN]\033[0m 已关闭自动更新订阅,优先使用本地已有配置启动"
+
+ # 1) 优先使用已有 conf/config.yaml;没有才 fallback
+ if [ ! -s "$Conf_Dir/config.yaml" ]; then
+ ensure_fallback_config || true
+ fi
+
+ # 2) 补齐运行必须字段
+ force_write_controller_and_ui "$Conf_Dir/config.yaml" || true
+ force_write_secret "$Conf_Dir/config.yaml" || true
+
+ # 3) 明确指定运行配置
+ CONFIG_FILE="$Conf_Dir/config.yaml"
+
+ # 4) 跳过后续“下载 / 转换 / 拼接”流程
SKIP_CONFIG_REBUILD=true
fi
@@ -838,7 +851,10 @@ ReturnStatus=$?
if [ "$ReturnStatus" -eq 0 ]; then
echo ''
if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then
- echo -e "Clash Dashboard 访问地址: http://${EXTERNAL_CONTROLLER}/ui"
+ SERVER_IP="$(hostname -I | awk '{print $1}')"
+ API_PORT="${EXTERNAL_CONTROLLER##*:}"
+
+ echo -e "Clash Dashboard 访问地址: http://${SERVER_IP}:${API_PORT}/ui"
SHOW_SECRET="${CLASH_SHOW_SECRET:-false}"
SHOW_SECRET_MASKED="${CLASH_SHOW_SECRET_MASKED:-true}"
From f41d75088cc32651ab49a9be6e1e4ea4503e4ed8 Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Tue, 17 Mar 2026 02:14:02 +0800
Subject: [PATCH 32/33] Update .env
---
.env | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.env b/.env
index d94c22e..effea66 100644
--- a/.env
+++ b/.env
@@ -18,7 +18,7 @@ export CLASH_URL=''
# 是否自动更新 Clash 订阅配置:
# true = 启动时检查订阅并重新下载/转换配置
# false = 禁用自动更新,直接使用本地已有 config.yaml
-export CLASH_AUTO_UPDATE="false"
+export CLASH_AUTO_UPDATE="true"
# 订阅请求头(可选)
# 常见机场需要 User-Agent;如不需要可留空
From c74601c89240265fa2321c5498527767aa05738c Mon Sep 17 00:00:00 2001
From: wnlen <544241974@qq.com>
Date: Tue, 17 Mar 2026 02:41:09 +0800
Subject: [PATCH 33/33] Update 1.png
---
docs/assets/1.png | Bin 91585 -> 149811 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/docs/assets/1.png b/docs/assets/1.png
index 90b4521b2575c2d2e0d9b38c8999188d1fdf53ae..be96fc1d5761c4b67f2fe7d5b8d2f941528a053c 100644
GIT binary patch
literal 149811
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
z
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