This commit is contained in:
wnlen
2026-03-17 14:18:29 +08:00
10 changed files with 336 additions and 195 deletions

7
.env
View File

@ -15,6 +15,11 @@
# 示例export CLASH_URL='https://example.com/sub?token=xxx' # 示例export CLASH_URL='https://example.com/sub?token=xxx'
export CLASH_URL='' export CLASH_URL=''
# 是否自动更新 Clash 订阅配置:
# true = 启动时检查订阅并重新下载/转换配置
# false = 禁用自动更新,直接使用本地已有 config.yaml
export CLASH_AUTO_UPDATE="true"
# 订阅请求头(可选) # 订阅请求头(可选)
# 常见机场需要 User-Agent如不需要可留空 # 常见机场需要 User-Agent如不需要可留空
export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39' export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39'
@ -42,7 +47,7 @@ CLASH_SHOW_SECRET_MASKED=true
# - 默认仅监听本机127.0.0.1:9090 (推荐) # - 默认仅监听本机127.0.0.1:9090 (推荐)
# - 如需局域网访问再改成0.0.0.0:9090并确保 CLASH_SECRET 足够复杂 # - 如需局域网访问再改成0.0.0.0:9090并确保 CLASH_SECRET 足够复杂
export EXTERNAL_CONTROLLER_ENABLED=true export EXTERNAL_CONTROLLER_ENABLED=true
export EXTERNAL_CONTROLLER='127.0.0.1:9090' export EXTERNAL_CONTROLLER='0.0.0.0:9090'
# ------------------------- # -------------------------
# 3) 代理端口与监听(常用) # 3) 代理端口与监听(常用)

View File

@ -1,5 +1,3 @@
[TOC]
# 关于本项目 # 关于本项目
**clash-for-linux** 是一个面向 Linux 服务器/桌面环境的 **Clash 自动化运行与管理脚本集** **clash-for-linux** 是一个面向 Linux 服务器/桌面环境的 **Clash 自动化运行与管理脚本集**
@ -11,11 +9,10 @@
</p> </p>
本项目主要解决以下问题: 本项目主要解决以下问题:
- ✕ 官方 Clash 二进制下载、架构区分、配置部署繁琐
- ❌ 官方 Clash 二进制下载、架构区分、配置部署繁琐 - ✕ 手动管理 Clash 进程、端口、环境变量不稳定
- ❌ 手动管理 Clash 进程、端口、环境变量不稳定 - ✕ systemd 服务、权限、安全配置缺乏统一方案
- ❌ systemd 服务、权限、安全配置缺乏统一方案 - ✕ 多订阅 / 配置混乱,升级和回滚成本高
- ❌ 多订阅 / 配置混乱,升级和回滚成本高
### 核心特性 ### 核心特性
@ -28,7 +25,7 @@
- 自动生成或自定义 Secret - 自动生成或自定义 Secret
- 默认开启 TLS 校验 - 默认开启 TLS 校验
- 🧪 **端口自动检测与分配**,避免冲突 - 🧪 **端口自动检测与分配**,避免冲突
- 🔄 **多订阅管理clashctl**,支持订阅切换、更新、日志查看 - 🔄 **多订阅管理clashctl**,支持自动订阅切换Vmess / V2Ray、Shadowsocks (SS)、ShadowsocksR (SSR)、Trojan、VLESS、Hysteria / Hysteria2、TUIC、HTTP / SOCKS5
- 🧠 **Mixin 机制**,可按需追加/覆盖 Clash 配置 - 🧠 **Mixin 机制**,可按需追加/覆盖 Clash 配置
- 🌐 **Tun 模式支持**(需 Clash Meta / Premium - 🌐 **Tun 模式支持**(需 Clash Meta / Premium
@ -46,13 +43,6 @@
- ❌ 不适合只想“点点 UI 就用”的纯桌面用户 - ❌ 不适合只想“点点 UI 就用”的纯桌面用户
- ❌ 不包含任何节点、机场或订阅推荐 - ❌ 不包含任何节点、机场或订阅推荐
### 更新状态
📅 **持续维护中**
最近更新:**2026-01-15**
# 安装 # 安装
> **推荐路径优先,一键安装即可满足 90% 使用场景。** > **推荐路径优先,一键安装即可满足 90% 使用场景。**
@ -127,6 +117,26 @@ http://127.0.0.1:9090/ui
> 不建议直接将管理端口暴露到公网。 > 不建议直接将管理端口暴露到公网。
如果想要**公网访问**
编辑 `.env` 文件,设置公网访问(对外端口不用改,改了机器人也能扫到,密钥设置的长点就行):
```
sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9090" > /opt/clash-for-linux/.env'
```
配置完成后,**重启服务使配置生效**
```
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
```
------ ------
## ▶️ 开启 / 关闭系统代理 ## ▶️ 开启 / 关闭系统代理

View File

@ -35,7 +35,9 @@ PID_FILE="$CLASH_HOME/temp/clash.pid"
SUBSCRIPTION_FILE="$CLASH_HOME/conf/subscriptions.list" SUBSCRIPTION_FILE="$CLASH_HOME/conf/subscriptions.list"
use_systemd() { 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() { action_with_systemd() {
@ -78,10 +80,11 @@ set_env_var() {
echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2 echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2
exit 1 exit 1
fi fi
local escaped local escaped escaped_sed
escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g") escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g")
escaped_sed=$(printf "%s" "$escaped" | sed 's/[\\&@]/\\&/g')
if grep -q "^export ${key}=" "$ENV_FILE"; then 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 else
echo "export ${key}='${escaped}'" >> "$ENV_FILE" echo "export ${key}='${escaped}'" >> "$ENV_FILE"
fi fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -5,7 +5,7 @@ set -euo pipefail
# 基础参数 # 基础参数
# ========================= # =========================
Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 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_Name="clash-for-linux"
Service_User="root" Service_User="root"
Service_Group="root" Service_Group="root"
@ -136,7 +136,6 @@ if [[ -z "${CpuArch:-}" ]]; then
err "无法识别 CPU 架构" err "无法识别 CPU 架构"
exit 1 exit 1
fi fi
info "CPU architecture: ${CpuArch}"
# ========================= # =========================
# .env 写入工具write_env_kv必须在 prompt 之前定义) # .env 写入工具write_env_kv必须在 prompt 之前定义)
@ -361,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" CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh"
if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then
systemctl enable "${Service_Name}.service" >/dev/null 2>&1 || true systemctl start "${Service_Name}.service" || true
fi fi
if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then
systemctl start "${Service_Name}.service" >/dev/null 2>&1 || true systemctl start "${Service_Name}.service" || true
fi fi
if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then
@ -474,28 +473,65 @@ section "控制面板"
api_port="$(parse_port "${EXTERNAL_CONTROLLER}")" api_port="$(parse_port "${EXTERNAL_CONTROLLER}")"
api_host="${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 if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then
api_host="127.0.0.1" api_host="127.0.0.1"
fi 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" CONF_DIR="$Install_Dir/conf"
CONF_FILE="$CONF_DIR/config.yaml" TEMP_DIR="$Install_Dir/temp"
SECRET_VAL="" SECRET_VAL=""
if wait_secret_ready "$CONF_FILE" 6; then SECRET_FILE=""
SECRET_VAL="$(read_secret_from_config "$CONF_FILE" || true)"
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" dash="http://${api_host}:${api_port}/ui"
log "🌐 Dashboard$(url "$dash")" log "🌐 Dashboard$(url "$dash")"
SHOW_FILE="${SECRET_FILE:-$CONF_DIR/config.yaml}"
if [[ -n "$SECRET_VAL" ]]; then 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${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 else
log "🔐 Secret${C_YELLOW}启动中暂未读到(稍后再试)${C_NC}" 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 fi
# ========================= # =========================

View File

@ -47,4 +47,5 @@ else
exitWithError "Unsupported Linux distribution" exitWithError "Unsupported Linux distribution"
fi fi
echo "CPU architecture: $CpuArch" log_info() { echo "[INFO] $*"; }
log_info "CPU architecture: $CpuArch"

View File

@ -10,47 +10,64 @@ Service_User="root"
Service_Group="root" Service_Group="root"
Unit_Path="/etc/systemd/system/${Service_Name}.service" 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 if [ "$(id -u)" -ne 0 ]; then
echo -e "\033[31m[ERROR] 需要 root 权限来安装 systemd 单元\033[0m" echo -e "[ERROR] 需要 root 权限来安装 systemd 单元"
exit 1 exit 1
fi fi
#################### 目录初始化 #################### #################### 目录初始化 ####################
install -d -m 0755 \ install -d -m 0755 "$Server_Dir/conf" "$Server_Dir/logs" "$Server_Dir/temp"
"$Server_Dir/conf" \
"$Server_Dir/logs" \ # 预创建 env 文件,避免 systemd 因路径不存在报错
"$Server_Dir/temp" : > "$Env_File"
chmod 0644 "$Env_File"
#################### 生成 systemd Unit #################### #################### 生成 systemd Unit ####################
cat >"$Unit_Path"<<EOF cat >"$Unit_Path" <<EOF
[Unit] [Unit]
Description=Clash for Linux Description=Clash for Linux (Mihomo)
After=network-online.target Documentation=https://github.com/wnlen/clash-for-linux
After=network-online.target nss-lookup.target
Wants=network-online.target Wants=network-online.target
StartLimitIntervalSec=0
StartLimitBurst=10
[Service] [Service]
Type=simple Type=simple
User=$Service_User
Group=$Service_Group
WorkingDirectory=$Server_Dir WorkingDirectory=$Server_Dir
# 启动 / 停止 # 启动环境
Environment=SYSTEMD_MODE=true
Environment=CLASH_ENV_FILE=$Env_File
Environment=HOME=/root
# 主进程必须由 start.sh 最后一跳 exec 成 mihomo/clash
ExecStart=/bin/bash $Server_Dir/start.sh ExecStart=/bin/bash $Server_Dir/start.sh
ExecStop=/bin/bash $Server_Dir/shutdown.sh ExecStop=/bin/bash $Server_Dir/shutdown.sh
ExecReload=/bin/kill -HUP \$MAINPID
# 失败策略 # 常驻策略:即使上层脚本正常退出,也要由 systemd 拉回
Restart=on-failure Restart=always
RestartSec=5 RestartSec=5s
# 停止与日志
KillMode=mixed
TimeoutStartSec=120 TimeoutStartSec=120
TimeoutStopSec=30 TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
# 环境变量 # 安全与文件权限
Environment=SYSTEMD_MODE=true UMask=0022
Environment=CLASH_ENV_FILE=$Server_Dir/temp/clash-for-linux.sh NoNewPrivileges=false
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@ -59,7 +76,10 @@ EOF
#################### 刷新 systemd #################### #################### 刷新 systemd ####################
systemctl daemon-reload systemctl daemon-reload
systemctl enable "$Service_Name".service >/dev/null 2>&1 || true
echo -e "\033[32m[OK] 已生成 systemd 单元: ${Unit_Path}\033[0m" echo -e "[OK] 已生成 systemd 单元: ${Unit_Path}"
echo -e "可执行以下命令启动服务:" echo -e "已启用开机自启,可执行以下命令启动服务:"
echo -e " sudo systemctl enable --now ${Service_Name}.service" echo -e " systemctl restart ${Service_Name}.service"
echo -e "查看状态:"
echo -e " systemctl status ${Service_Name}.service -l --no-pager"

View File

@ -36,7 +36,8 @@ download_clash_bin() {
return 1 return 1
fi 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 if [ -z "$download_url" ]; then
echo -e "\033[33m[WARN] 未设置 CLASH_DOWNLOAD_URL_TEMPLATE跳过 Clash 内核自动下载\033[0m" echo -e "\033[33m[WARN] 未设置 CLASH_DOWNLOAD_URL_TEMPLATE跳过 Clash 内核自动下载\033[0m"
return 1 return 1

143
start.sh
View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 严格模式 # 严格模式
set -eo pipefail set -euo pipefail
# --- DEBUG: 打印具体失败的行号和命令systemd 下非常关键) --- # --- DEBUG: 打印具体失败的行号和命令systemd 下非常关键) ---
trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR
@ -88,13 +88,41 @@ URL="${CLASH_URL:-}"
# 清理可能的 CRLFWindows 写 .env 很常见) # 清理可能的 CRLFWindows 写 .env 很常见)
URL="$(printf '%s' "$URL" | tr -d '\r')" 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 子进程能拿到 #让 bash 子进程能拿到
export CLASH_URL="$URL" export CLASH_URL="$URL"
# 只有在“需要在线更新订阅”的模式下才强制要求 URL if [ -n "$URL" ] && ! printf '%s' "$URL" | grep -Eq '^https?://'; then
if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2
echo "[ERR] CLASH_URL 为空(未配置订阅地址)"
exit 2 exit 2
fi fi
@ -114,10 +142,10 @@ fi
# 兜底生成随机 secret # 兜底生成随机 secret
if [ -z "$Secret" ]; then if [ -z "$Secret" ]; then
if command -v openssl >/dev/null 2>&1; then if command -v openssl >/dev/null 2>&1; then
Secret="$(openssl rand -hex 32)" Secret="$(openssl rand -hex 16)"
else else
# 32 bytes -> 64 hex chars # 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
fi fi
@ -174,7 +202,7 @@ ensure_ui_links() {
force_write_controller_and_ui() { force_write_controller_and_ui() {
local file="$1" 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 [ -n "$file" ] || return 1
@ -244,11 +272,11 @@ fix_external_ui_by_safe_paths() {
CLASH_HTTP_PORT="${CLASH_HTTP_PORT:-7890}" CLASH_HTTP_PORT="${CLASH_HTTP_PORT:-7890}"
CLASH_SOCKS_PORT="${CLASH_SOCKS_PORT:-7891}" CLASH_SOCKS_PORT="${CLASH_SOCKS_PORT:-7891}"
CLASH_REDIR_PORT="${CLASH_REDIR_PORT:-7892}" 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}" CLASH_ALLOW_LAN="${CLASH_ALLOW_LAN:-false}"
EXTERNAL_CONTROLLER_ENABLED="${EXTERNAL_CONTROLLER_ENABLED:-true}" 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}" ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}"
@ -347,6 +375,10 @@ ensure_subconverter() {
local bin="${Server_Dir}/tools/subconverter/subconverter" local bin="${Server_Dir}/tools/subconverter/subconverter"
local port="25500" local port="25500"
# 自动获取服务器IP
local host_ip
host_ip="$(hostname -I | awk '{print $1}')"
# 没有二进制直接跳过 # 没有二进制直接跳过
if [ ! -x "$bin" ]; then if [ ! -x "$bin" ]; then
echo "[WARN] subconverter bin not found: $bin" echo "[WARN] subconverter bin not found: $bin"
@ -356,20 +388,20 @@ ensure_subconverter() {
# 已在监听则认为就绪 # 已在监听则认为就绪
if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then 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" export SUBCONVERTER_READY="true"
return 0 return 0
fi fi
# 启动(后台 # 启动(监听所有IP
echo "[INFO] starting subconverter..." 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 for _ in 1 2 3 4 5; do
sleep 1 sleep 1
if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then 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" export SUBCONVERTER_READY="true"
echo "[OK] subconverter ready at ${SUBCONVERTER_URL}" echo "[OK] subconverter ready at ${SUBCONVERTER_URL}"
return 0 return 0
@ -431,15 +463,21 @@ ensure_fallback_config() {
} }
SKIP_CONFIG_REBUILD=false SKIP_CONFIG_REBUILD=false
# systemd 模式下 URL 为空:直接兜底启动 # systemd 模式下 URL 为空,或手动模式下用户回车跳过:直接兜底启动
if [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; then 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 为空,跳过订阅更新,使用本地兜底配置启动" 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 ensure_fallback_config || true
SKIP_CONFIG_REBUILD=true SKIP_CONFIG_REBUILD=true
fi fi
CLASH_AUTO_UPDATE="${CLASH_AUTO_UPDATE:-true}"
#################### Clash 订阅地址检测及配置文件下载 #################### #################### Clash 订阅地址检测及配置文件下载 ####################
if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" = "true" ]; then
echo -e '\n正在检测订阅地址...' echo -e '\n正在检测订阅地址...'
Text1="Clash订阅地址可访问" Text1="Clash订阅地址可访问"
Text2="Clash订阅地址不可访问" Text2="Clash订阅地址不可访问"
@ -484,7 +522,7 @@ if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
fi fi
#################### 下载订阅并生成 config.yaml非兜底路径 #################### #################### 下载订阅并生成 config.yaml非兜底路径 ####################
if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" = "true" ]; then
ensure_subconverter || true ensure_subconverter || true
echo -e '\n正在下载Clash配置文件...' echo -e '\n正在下载Clash配置文件...'
Text3="配置文件clash.yaml下载成功" Text3="配置文件clash.yaml下载成功"
@ -638,11 +676,30 @@ if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
fi fi
fi fi
if [ "$SKIP_CONFIG_REBUILD" != "true" ] && [ "$CLASH_AUTO_UPDATE" != "true" ]; then
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
# ========================================================= # =========================================================
# 判断订阅是否已是完整 Clash YAMLMeta / Mihomo / Premium # 判断订阅是否已是完整 Clash YAMLMeta / 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" echo "[INFO] subscription is a full Clash config, use it directly"
cp -f "$Temp_Dir/clash.yaml" "$Conf_Dir/config.yaml" cp -f "$Temp_Dir/clash.yaml" "$Conf_Dir/config.yaml"
@ -668,7 +725,7 @@ if grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$Temp_Dir/clash.ya
fi fi
SKIP_CONFIG_REBUILD=true SKIP_CONFIG_REBUILD=true
fi fi
#################### 订阅转换/拼接(非兜底路径) #################### #################### 订阅转换/拼接(非兜底路径) ####################
if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then
@ -822,12 +879,33 @@ Clash_Bin="$(resolve_clash_bin "$Server_Dir" "$CpuArch")"
ReturnStatus=$? ReturnStatus=$?
if [ "$ReturnStatus" -eq 0 ]; then if [ "$ReturnStatus" -eq 0 ]; then
echo ''
if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then
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}"
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 if [ "${SYSTEMD_MODE:-false}" = "true" ]; then
echo "[INFO] SYSTEMD_MODE=true前台启动交给 systemd 监管" echo "[INFO] SYSTEMD_MODE=true前台启动交给 systemd 监管"
echo "[INFO] Using config: $CONFIG_FILE" echo "[INFO] Using config: $CONFIG_FILE"
echo "[INFO] Using runtime dir: $RUNTIME_DIR" echo "[INFO] Using runtime dir: $RUNTIME_DIR"
# systemd 前台:只用 -f 指定配置文件,-d 作为工作目录
exec "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" exec "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR"
else else
echo "[INFO] 后台启动 (nohup)" echo "[INFO] 后台启动 (nohup)"
@ -850,29 +928,6 @@ else
if_success "$Text5" "$Text6" "$ReturnStatus" if_success "$Text5" "$Text6" "$ReturnStatus"
fi 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:-}" Env_File="${CLASH_ENV_FILE:-}"

View File

@ -1,142 +1,152 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ========================= # More accurate uninstall for clash-for-linux
# 参数(对标 install.sh + install_systemd.sh SERVICE_NAME="clash-for-linux"
# ========================= UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
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"
# =========================
# 彩色输出
# =========================
RED='\033[31m' RED='\033[31m'
GREEN='\033[32m' GREEN='\033[32m'
YELLOW='\033[33m' YELLOW='\033[33m'
NC='\033[0m' NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; } info() { echo -e "${GREEN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; }
# =========================
# 前置校验
# =========================
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
err "需要 root 权限执行卸载脚本(请使用 sudo bash uninstall.sh" err "需要 root 权限执行卸载脚本(请使用 sudo bash uninstall.sh"
exit 1 exit 1
fi fi
info "开始卸载 ${Service_Name} ..." # Candidate install dirs:
info "Install_Dir=${Install_Dir}" # 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
# ========================= if [ -f "$UNIT_PATH" ]; then
# 1) 优雅停止(优先 shutdown.sh再 systemd wd="$(sed -nE 's#^WorkingDirectory=(.*)#\1#p' "$UNIT_PATH" | head -n1 || true)"
# ========================= [ -n "$wd" ] && candidates+=("$wd")
if [ -f "${Install_Dir}/shutdown.sh" ]; then
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优雅停止..." info "执行 shutdown.sh优雅停止..."
bash "${Install_Dir}/shutdown.sh" >/dev/null 2>&1 || true bash "${INSTALL_DIR}/shutdown.sh" >/dev/null 2>&1 || true
fi fi
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
info "停止并禁用 systemd 服务..." info "停止并禁用 systemd 服务..."
systemctl stop "${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 systemctl disable "${SERVICE_NAME}.service" >/dev/null 2>&1 || true
fi fi
# ========================= # 2) stop process by pid file from all likely dirs
# 2) 兜底:按 PID 文件杀进程(对标 unit 的 PIDFile for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do
# ========================= [ -n "$d" ] || continue
PID_FILE="${Install_Dir}/temp/clash.pid" PID_FILE="$d/temp/clash.pid"
if [ -f "$PID_FILE" ]; then if [ -f "$PID_FILE" ]; then
PID="$(cat "$PID_FILE" 2>/dev/null || true)" PID="$(cat "$PID_FILE" 2>/dev/null || true)"
if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then
info "检测到 PID=${PID},尝试停止..." info "检测到 PID=${PID}(来自 $PID_FILE,尝试停止..."
kill "$PID" 2>/dev/null || true kill "$PID" 2>/dev/null || true
sleep 1 sleep 1
if kill -0 "$PID" 2>/dev/null; then if kill -0 "$PID" 2>/dev/null; then
warn "进程仍在运行,强制 kill -9 ${PID}" warn "进程仍在运行,强制 kill -9 ${PID}"
kill -9 "$PID" 2>/dev/null || true kill -9 "$PID" 2>/dev/null || true
fi fi
ok "已停止 clash 进程PIDFile"
fi fi
fi rm -f "$PID_FILE" || true
fi
done
# 兜底:按进程名(系统可能有多个 clash不建议无脑 pkill -9先提示再杀 # 兜底:按完整路径匹配,避免误杀其他 clash
if pgrep -x clash >/dev/null 2>&1; then pkill -f '/clash-for-linux/.*/clash' >/dev/null 2>&1 || true
warn "检测到仍有 clash 进程存在(可能非本项目),尝试温和结束..." pkill -f '/clash-for-linux/.*/mihomo' >/dev/null 2>&1 || true
pkill -x clash >/dev/null 2>&1 || true sleep 1
sleep 1 pkill -9 -f '/clash-for-linux/.*/clash' >/dev/null 2>&1 || true
fi pkill -9 -f '/clash-for-linux/.*/mihomo' >/dev/null 2>&1 || true
if pgrep -x clash >/dev/null 2>&1; then
warn "仍残留 clash 进程,执行 pkill -9可能影响其它 clash 实例)..."
pkill -9 -x clash >/dev/null 2>&1 || true
fi
# ========================= # 3) remove unit and related files
# 3) 删除 systemd unit对标 install_systemd.sh if [ -f "$UNIT_PATH" ]; then
# ========================= rm -f "$UNIT_PATH"
if [ -f "$Unit_Path" ]; then ok "已移除 systemd 单元: ${UNIT_PATH}"
rm -f "$Unit_Path"
ok "已移除 systemd 单元: ${Unit_Path}"
fi fi
if [ -d "/etc/systemd/system/${SERVICE_NAME}.service.d" ]; then
# drop-in万一用户自定义过 rm -rf "/etc/systemd/system/${SERVICE_NAME}.service.d"
if [ -d "/etc/systemd/system/${Service_Name}.service.d" ]; then ok "已移除 drop-in: /etc/systemd/system/${SERVICE_NAME}.service.d"
rm -rf "/etc/systemd/system/${Service_Name}.service.d"
ok "已移除 drop-in: /etc/systemd/system/${Service_Name}.service.d"
fi fi
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true systemctl daemon-reload >/dev/null 2>&1 || true
systemctl reset-failed >/dev/null 2>&1 || true systemctl reset-failed >/dev/null 2>&1 || true
fi fi
# ========================= # 4) cleanup env / command entry
# 4) 清理默认配置/环境脚本/命令入口 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
if [ -f "/etc/default/${Service_Name}" ]; then rm -f "/usr/local/bin/clashctl" >/dev/null 2>&1 || true
rm -f "/etc/default/${Service_Name}" for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do
ok "已移除: /etc/default/${Service_Name}" [ -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 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 echo
warn "如果你曾执行 proxy_on当前终端可能仍保留代理环境变量。可执行" warn "如果你曾执行 proxy_on当前终端可能仍保留代理环境变量。可执行"
echo " unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY" echo " unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY"
echo " # 或关闭终端重新打开" echo " # 或关闭终端重新打开"
echo echo
ok "卸载完成root-only 模式)✅" ok "卸载完成 ✅"