This commit is contained in:
Arvin
2026-03-20 21:05:37 +08:00
parent 81522abe7f
commit 0bd74ee2ed
17 changed files with 21 additions and 1432 deletions

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
exec clashctl off "$@"

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
exec clashctl on "$@"

Binary file not shown.

View File

@ -1,21 +0,0 @@
mixed-port: 7890
allow-lan: false
bind-address: '*'
mode: rule
log-level: info
ipv6: true
udp: true
external-controller: 127.0.0.1:9090
external-ui: /opt/clash-for-linux/.config/mihomo/ui
proxies: []
proxy-groups: []
rules:
- MATCH,DIRECT

View File

@ -1,16 +0,0 @@
mixed-port: 7890
allow-lan: false
bind-address: '*'
mode: rule
log-level: info
ipv6: true
udp: true
external-controller: 127.0.0.1:9090
external-ui: /opt/clash-for-linux/conf/ui
proxies: []
proxy-groups: []
rules:
- MATCH,DIRECT

View File

@ -1,9 +0,0 @@
# Mixin 配置目录
将额外的 Clash YAML 配置放在此目录下,脚本会按文件名排序后依次拼接到生成的 `config.yaml` 末尾。
如需手动指定顺序或使用自定义路径,请在 `.env` 中设置:
```bash
export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml'
```

View File

@ -1,31 +0,0 @@
# HTTP 代理端口
port: CLASH_HTTP_PORT_PLACEHOLDER
# SOCKS5 代理端口
socks-port: CLASH_SOCKS_PORT_PLACEHOLDER
# Linux 和 macOS 的 redir 代理端口
redir-port: CLASH_REDIR_PORT_PLACEHOLDER
# 监听IP地址
bind-address: CLASH_LISTEN_IP_PLACEHOLDER
# 允许局域网的连接
allow-lan: CLASH_ALLOW_LAN_PLACEHOLDER
# 规则模式Rule规则 / Global全局代理/ Direct全局直连
mode: rule
# 设置日志输出级别 (默认级别silent即不输出任何内容以避免因日志内容过大而导致程序内存溢出
# 5 个级别silent / info / warning / error / debug。级别越高日志输出量越大越倾向于调试若需要请自行开启。
log-level: silent
# Clash 的 RESTful API
external-controller: 'EXTERNAL_CONTROLLER_PLACEHOLDER'
# RESTful API 的口令
secret: 'b&ZlKTte5OnEt2Sn'
# 您可以将静态网页资源(如 clash-dashboard放置在一个目录中clash 将会服务于 `RESTful API/ui`
# 参数应填写配置目录的相对路径或绝对路径。
# external-ui: /code/clash-dashboard

View File

@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -e
SERVICE="clash-for-linux.service"
echo "[INFO] Updating config..."
# 只负责生成配置,不启动内核
bash start.sh --only-generate
echo "[INFO] Restarting systemd service..."
systemctl restart "$SERVICE"
echo "[OK] Clash restarted via systemd"

View File

@ -12,6 +12,7 @@ mkdir -p "$RUNTIME_DIR" "$LOG_DIR"
FOREGROUND=false FOREGROUND=false
DAEMON=false DAEMON=false
# 解析参数
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--foreground) FOREGROUND=true ;; --foreground) FOREGROUND=true ;;
@ -23,6 +24,11 @@ for arg in "$@"; do
esac esac
done done
if [ "$FOREGROUND" = true ] && [ "$DAEMON" = true ]; then
echo "[ERROR] Cannot use both --foreground and --daemon" >&2
exit 2
fi
if [ ! -s "$CONFIG_FILE" ]; then if [ ! -s "$CONFIG_FILE" ]; then
echo "[ERROR] runtime config not found: $CONFIG_FILE" >&2 echo "[ERROR] runtime config not found: $CONFIG_FILE" >&2
exit 2 exit 2
@ -33,22 +39,25 @@ if grep -q '\${' "$CONFIG_FILE"; then
exit 2 exit 2
fi fi
# 这里先沿用你原来的 resolve_clash.sh
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/get_cpu_arch.sh" source "$PROJECT_DIR/scripts/get_cpu_arch.sh"
# shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/resolve_clash.sh" source "$PROJECT_DIR/scripts/resolve_clash.sh"
CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")" CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")"
# systemd 模式
if [ "$FOREGROUND" = true ]; then if [ "$FOREGROUND" = true ]; then
write_run_state "running" "systemd"
exec "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" exec "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR"
fi fi
# script / daemon 模式
if [ "$DAEMON" = true ]; then if [ "$DAEMON" = true ]; then
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 & nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
echo $! > "$PID_FILE" pid=$!
echo "[OK] Clash started in script mode, pid=$(cat "$PID_FILE")" echo "$pid" > "$PID_FILE"
write_run_state "running" "script" "$pid"
echo "[OK] Clash started in script mode, pid=$pid"
exit 0 exit 0
fi fi

View File

@ -72,8 +72,16 @@ stop_via_script() {
fi fi
fi fi
rm -f "$PID_FILE" rm -f "$PID_FILE"
if [ -f "$PROJECT_DIR/runtime/state.env" ]; then
if grep -q '^LAST_RUN_STATUS=' "$PROJECT_DIR/runtime/state.env" 2>/dev/null; then
sed -i -E "s/^LAST_RUN_STATUS=.*/LAST_RUN_STATUS=stopped/" "$PROJECT_DIR/runtime/state.env"
else
echo "LAST_RUN_STATUS=stopped" >> "$PROJECT_DIR/runtime/state.env"
fi
fi
} }
restart_via_script() { restart_via_script() {
stop_via_script || true stop_via_script || true
start_via_script start_via_script

View File

@ -1,62 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# 关闭 clash 服务
Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Temp_Dir="$Server_Dir/temp"
Conf_Dir="$Server_Dir/conf"
PID_FILE="$Temp_Dir/clash.pid"
mkdir -p "$Temp_Dir"
# 1) 优先按 PID_FILE 停
if [ -f "$PID_FILE" ]; then
PID="$(cat "$PID_FILE" 2>/dev/null || true)"
if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then
kill "$PID" 2>/dev/null || true
for _ in {1..8}; do
sleep 1
if ! kill -0 "$PID" 2>/dev/null; then
break
fi
done
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID" 2>/dev/null || true
fi
fi
rm -f "$PID_FILE"
else
# 2) 兜底:按 “-d $Conf_Dir” 特征找(比 clash-linux- 更稳)
# 说明:你的 start.sh 启动命令形如:<clashbin> -d "$Conf_Dir"
PIDS="$(pgrep -f " -d ${Conf_Dir}(\s|$)" || true)"
if [ -n "${PIDS:-}" ]; then
kill $PIDS 2>/dev/null || true
for _ in {1..8}; do
sleep 1
if ! pgrep -f " -d ${Conf_Dir}(\s|$)" >/dev/null 2>&1; then
break
fi
done
if pgrep -f " -d ${Conf_Dir}(\s|$)" >/dev/null 2>&1; then
kill -9 $PIDS 2>/dev/null || true
fi
fi
fi
# 3) 清理环境变量文件(删除,而不是置空)
Env_File="${CLASH_ENV_FILE:-}"
if [ "$Env_File" != "off" ] && [ "$Env_File" != "disabled" ]; then
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
if [ -f "$Env_File" ]; then
rm -f "$Env_File" || true
fi
fi
echo -e "\n服务关闭成功。若当前终端已开启代理请执行proxy_off\n"

1023
start.sh

File diff suppressed because it is too large Load Diff

View File

View File

247
update.sh
View File

@ -1,247 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
#################### 脚本初始化任务 ####################
Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 加载.env变量文件优先使用安装目录 /opt也支持手动指定
ENV_FILE_CANDIDATES=()
# 1) 允许手动指定CLASH_ENV=/path/to/.env ./update.sh
if [ -n "${CLASH_ENV:-}" ]; then
ENV_FILE_CANDIDATES+=("$CLASH_ENV")
fi
# 2) 默认优先安装目录systemd 实际运行目录)
ENV_FILE_CANDIDATES+=("/opt/clash-for-linux/.env")
# 3) 回退到脚本目录(仓库本地调试)
ENV_FILE_CANDIDATES+=("$Server_Dir/.env")
ENV_FILE=""
for f in "${ENV_FILE_CANDIDATES[@]}"; do
if [ -f "$f" ]; then ENV_FILE="$f"; break; fi
done
if [ -z "$ENV_FILE" ]; then
echo -e "\033[31m[ERROR]\033[0m 未找到 .env已尝试${ENV_FILE_CANDIDATES[*]}"
exit 1
fi
echo -e "\033[36m[INFO]\033[0m Using .env: $ENV_FILE"
# shellcheck disable=SC1090
source "$ENV_FILE"
#################### 变量设置 ####################
Conf_Dir="$Server_Dir/conf"
Temp_Dir="$Server_Dir/temp"
Log_Dir="$Server_Dir/logs"
mkdir -p "$Conf_Dir" "$Temp_Dir" "$Log_Dir"
URL="${CLASH_URL:?Error: CLASH_URL variable is not set or empty}"
# 获取 CLASH_SECRET 值,若未设置则尝试读取旧配置,否则生成随机数
Secret="${CLASH_SECRET:-}"
if [ -z "$Secret" ] && [ -f "$Conf_Dir/config.yaml" ]; then
Secret="$(awk -F': ' '/^secret:/{print $2; exit}' "$Conf_Dir/config.yaml" || true)"
fi
if [ -z "$Secret" ]; then
Secret="$(openssl rand -hex 32)"
fi
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:-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}"
ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}"
CLASH_HEADERS="${CLASH_HEADERS:-}"
# 工具脚本
# 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" "0.0.0.0")"
# shellcheck disable=SC1090
source "$Server_Dir/scripts/config_utils.sh"
#################### action / if_success ####################
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 rc; STRING=$1; echo -n "$STRING "; shift; "$@" && success || failure; rc=$?; echo; return $rc; }
if_success() {
local ok_msg="$1" fail_msg="$2" st="$3"
if [ "$st" -eq 0 ]; then
action "$ok_msg" /bin/true
else
action "$fail_msg" /bin/false
exit 1
fi
}
#################### 任务执行 ####################
# 临时取消环境变量(避免被自身代理影响下载)
unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY || true
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] 已启用不安全的 TLS 下载(跳过证书校验)\033[0m"
fi
if [ -n "$CLASH_HEADERS" ]; then
CHECK_CMD+=(-H "$CLASH_HEADERS")
fi
CHECK_CMD+=("$URL")
status_code="$("${CHECK_CMD[@]}")"
echo "$status_code" | grep -E '^[23][0-9]{2}$' &>/dev/null
ReturnStatus=$?
if_success "$Text1" "$Text2" "$ReturnStatus"
echo -e '\n正在下载Clash配置文件...'
Text3="配置文件下载成功!"
Text4="配置文件下载失败,退出更新!"
CURL_CMD=(curl -L -sS --retry 5 -m 20 -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")
"${CURL_CMD[@]}" || true
ReturnStatus=$?
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
"${WGET_CMD[@]}" && ReturnStatus=0 && break || ReturnStatus=$?
done
fi
if_success "$Text3" "$Text4" "$ReturnStatus"
# 基础内容校验(避免 HTML/空文件)
if ! grep -Eq '^(proxies:|proxy-groups:|rules:|mixed-port:|port:)' "$Temp_Dir/clash.yaml"; then
echo -e "\033[31m[ERROR]\033[0m 下载内容不像 Clash 配置(缺少关键字段),请检查订阅是否返回了网页/登录页/错误信息。"
echo -e "可执行head -n 20 $Temp_Dir/clash.yaml 查看内容"
exit 1
fi
\cp -a "$Temp_Dir/clash.yaml" "$Temp_Dir/clash_config.yaml"
# 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] 未检测到可用的 subconverter跳过订阅转换\033[0m"
fi
# ========= 生成最终 config.yaml =========
# 兼容两类订阅:
# A) 全量 config包含 port/mixed-port 等),直接用订阅为主
# B) 仅节点列表(含 proxies:),用 templete + proxies 合并
FULL_CONFIG=false
if grep -Eq '^(port:|mixed-port:|socks-port:|redir-port:)' "$Temp_Dir/clash_config.yaml"; then
FULL_CONFIG=true
fi
if [ "$FULL_CONFIG" = "true" ]; then
echo -e "\n检测到订阅为【全量配置】模式直接使用订阅生成 config.yaml"
\cp -a "$Temp_Dir/clash_config.yaml" "$Temp_Dir/config.yaml"
else
echo -e "\n检测到订阅为【节点/片段】模式,使用 templete 合并 proxies"
if [ ! -f "$Temp_Dir/templete_config.yaml" ]; then
echo -e "\033[31m[ERROR]\033[0m 未找到 templete_config.yaml$Temp_Dir/templete_config.yaml"
exit 1
fi
sed -n '/^proxies:/,$p' "$Temp_Dir/clash_config.yaml" > "$Temp_Dir/proxy.txt"
cat "$Temp_Dir/templete_config.yaml" > "$Temp_Dir/config.yaml"
cat "$Temp_Dir/proxy.txt" >> "$Temp_Dir/config.yaml"
fi
# 替换占位符(仅在 templete 模式才会命中;全量模式下无害)
sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$Temp_Dir/config.yaml"
sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$Temp_Dir/config.yaml"
sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$Temp_Dir/config.yaml"
sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$Temp_Dir/config.yaml"
sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$Temp_Dir/config.yaml"
# external-controller全量 config 也允许覆盖/写入)
if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then
# 如果已经有 external-controller 则替换;没有则追加
if grep -qE '^external-controller:' "$Temp_Dir/config.yaml"; then
sed -i "s@^external-controller:.*@external-controller: ${EXTERNAL_CONTROLLER}@g" "$Temp_Dir/config.yaml"
else
echo "external-controller: ${EXTERNAL_CONTROLLER}" >> "$Temp_Dir/config.yaml"
fi
else
# 禁用:若存在则注释
sed -i "s@^external-controller:.*@# external-controller: disabled@g" "$Temp_Dir/config.yaml" || true
fi
apply_tun_config "$Temp_Dir/config.yaml"
apply_mixin_config "$Temp_Dir/config.yaml" "$Server_Dir"
# ---- guard: never apply empty config ----
if [ ! -s "$Temp_Dir/config.yaml" ]; then
echo -e "\033[31m[ERROR]\033[0m 生成的配置为空:$Temp_Dir/config.yaml"
echo -e "\033[31m[ERROR]\033[0m 已中止写入 $Conf_Dir/config.yaml保护最后一次可用配置"
exit 1
fi
\cp "$Temp_Dir/config.yaml" "$Conf_Dir/config.yaml"
# Dashboard
Work_Dir="$(cd "$(dirname "$0")" && pwd)"
Dashboard_Dir="${Work_Dir}/dashboard/public"
if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then
# 若有 external-ui 注释行则替换;否则追加
if grep -qE '^(#\s*)?external-ui:' "$Conf_Dir/config.yaml"; then
sed -ri "s@^(#\s*)?external-ui:.*@external-ui: ${Dashboard_Dir}@g" "$Conf_Dir/config.yaml"
else
echo "external-ui: ${Dashboard_Dir}" >> "$Conf_Dir/config.yaml"
fi
fi
# 写入 secret用 awk 重写,避免 sed 转义问题)
tmpfile="$(mktemp)"
awk -v sec="$Secret" '
BEGIN{done=0}
/^secret:/ {print "secret: " sec; done=1; next}
{print}
END{ if(done==0) print "secret: " sec }
' "$Conf_Dir/config.yaml" > "$tmpfile"
mv "$tmpfile" "$Conf_Dir/config.yaml"
echo -e "\n订阅更新完成如需生效请执行: bash restart.sh\n"