v1.19.21
17
.env
@ -20,6 +20,15 @@ export CLASH_URL=''
|
||||
# false = 禁用自动更新,直接使用本地已有 config.yaml
|
||||
export CLASH_AUTO_UPDATE="true"
|
||||
|
||||
# 自动下载 Mihomo 内核
|
||||
CLASH_AUTO_DOWNLOAD=auto
|
||||
|
||||
# 内核固定版本
|
||||
MIHOMO_VERSION=v1.19.21
|
||||
|
||||
# 内核自定义下载地址
|
||||
CLASH_DOWNLOAD_URL_TEMPLATE='https://github.com/MetaCubeX/mihomo/releases/download/v1.19.21/mihomo-linux-amd64-v1.19.21.gz'
|
||||
|
||||
# 订阅请求头(可选)
|
||||
# 常见机场需要 User-Agent;如不需要可留空
|
||||
export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39'
|
||||
@ -42,6 +51,8 @@ CLASH_SHOW_SECRET=true
|
||||
# 是否显示脱敏 Secret(推荐)
|
||||
CLASH_SHOW_SECRET_MASKED=true
|
||||
|
||||
|
||||
|
||||
# External Controller(Clash RESTful API)
|
||||
# ⚠️ 安全建议:
|
||||
# - 默认仅监听本机:127.0.0.1:9090 (推荐)
|
||||
@ -84,10 +95,10 @@ export CLASH_CONFIG_TEST_TIMEOUT="30"
|
||||
# -------------------------
|
||||
|
||||
# 可叠加多个 YAML 文件,后者覆盖前者(逗号分隔)
|
||||
# export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml'
|
||||
# export CLASH_MIXIN_PATHS='config/mixin.d/base.yaml,config/mixin.d/rules.yaml'
|
||||
|
||||
# 指定 Mixin 目录(默认:conf/mixin.d)
|
||||
# export CLASH_MIXIN_DIR='conf/mixin.d'
|
||||
# 指定 Mixin 目录(默认:config/mixin.d)
|
||||
# export CLASH_MIXIN_DIR='config/mixin.d'
|
||||
|
||||
# -------------------------
|
||||
# 6) Tun 模式(高级可选,需要 Clash Meta / Premium)
|
||||
|
||||
42
README.md
@ -48,7 +48,7 @@
|
||||
```
|
||||
git clone --branch master --depth 1 https://github.com/wnlen/clash-for-linux.git
|
||||
cd clash-for-linux
|
||||
sudo bash install.sh
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
安装脚本将自动完成:
|
||||
@ -68,13 +68,13 @@ sudo bash install.sh
|
||||
编辑 `.env` 文件,设置订阅地址:
|
||||
|
||||
```
|
||||
sudo bash -c 'echo "CLASH_URL=<订阅地址>" > /root/clash-for-linux/.env'
|
||||
bash -c 'echo "CLASH_URL=<订阅地址>" > /root/clash-for-linux/.env'
|
||||
```
|
||||
|
||||
配置完成后,**重启服务使配置生效**:
|
||||
|
||||
```
|
||||
sudo systemctl restart clash-for-linux.service
|
||||
systemctl restart clash-for-linux.service
|
||||
```
|
||||
|
||||
说明:
|
||||
@ -91,7 +91,7 @@ sudo systemctl restart clash-for-linux.service
|
||||
true = 启动时检查订阅并重新下载/转换配置
|
||||
false = 禁用自动更新,直接使用本地已有 config.yaml
|
||||
```
|
||||
sudo bash -c 'echo "CLASH_AUTO_UPDATE=false" > /opt/clash-for-linux/.env'
|
||||
bash -c 'echo "CLASH_AUTO_UPDATE=false" > /opt/clash-for-linux/.env'
|
||||
```
|
||||
------
|
||||
|
||||
@ -122,19 +122,19 @@ http://127.0.0.1:9090/ui
|
||||
编辑 `.env` 文件,设置公网访问(对外端口不用改,改了机器人也能扫到,密钥设置长点就行):
|
||||
|
||||
```
|
||||
sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9090" > /opt/clash-for-linux/.env'
|
||||
bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9090" > /opt/clash-for-linux/.env'
|
||||
```
|
||||
|
||||
配置完成后,**重启服务使配置生效**:
|
||||
|
||||
```
|
||||
sudo systemctl restart clash-for-linux.service
|
||||
systemctl restart clash-for-linux.service
|
||||
```
|
||||
|
||||
密钥留空时:脚本可自动生成随机值
|
||||
获取密钥命令:
|
||||
```
|
||||
sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "/opt/clash-for-linux/conf/config.yaml" | head -n 1
|
||||
sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "/opt/clash-for-linux/runtime/config.yaml" | head -n 1
|
||||
```
|
||||
|
||||
|
||||
@ -190,7 +190,7 @@ clashctl sub log
|
||||
### 修改 Clash 配置并重启
|
||||
|
||||
```
|
||||
vim conf/config.yaml
|
||||
vim runtime/config.yaml
|
||||
clashctl restart
|
||||
```
|
||||
|
||||
@ -214,12 +214,12 @@ clashctl sub update personal
|
||||
|
||||
用于追加或覆盖 Clash 配置。
|
||||
|
||||
- 默认读取:`conf/mixin.d/*.yaml`(按文件名排序)
|
||||
- 默认读取:`config/mixin.d/*.yaml`(按文件名排序)
|
||||
- 也可在 `.env` 中指定:
|
||||
|
||||
```
|
||||
export CLASH_MIXIN_DIR='conf/mixin.d'
|
||||
export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml'
|
||||
export CLASH_MIXIN_DIR='config/mixin.d'
|
||||
export CLASH_MIXIN_PATHS='config/mixin.d/base.yaml,config/mixin.d/rules.yaml'
|
||||
```
|
||||
|
||||
------
|
||||
@ -267,7 +267,7 @@ env | grep -E 'http_proxy|https_proxy'
|
||||
## 🧹 卸载
|
||||
|
||||
```
|
||||
sudo bash uninstall.sh
|
||||
bash uninstall.sh
|
||||
```
|
||||
|
||||
------
|
||||
@ -314,25 +314,25 @@ export SUBCONVERTER_DOWNLOAD_URL_TEMPLATE='https://example.com/subconverter_{arc
|
||||
1. 开启 IP 转发
|
||||
|
||||
```bash
|
||||
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
echo "net.ipv4.ip_forward = 1" | tee -a /etc/sysctl.conf
|
||||
sysctl -p
|
||||
```
|
||||
|
||||
2.配置iptables
|
||||
```bash
|
||||
# 先清空旧规则
|
||||
sudo iptables -t nat -F
|
||||
iptables -t nat -F
|
||||
|
||||
# 允许本机访问代理端口
|
||||
sudo iptables -t nat -A OUTPUT -p tcp --dport 7890 -j RETURN
|
||||
sudo iptables -t nat -A OUTPUT -p tcp --dport 7891 -j RETURN
|
||||
sudo iptables -t nat -A OUTPUT -p tcp --dport 7892 -j RETURN
|
||||
iptables -t nat -A OUTPUT -p tcp --dport 7890 -j RETURN
|
||||
iptables -t nat -A OUTPUT -p tcp --dport 7891 -j RETURN
|
||||
iptables -t nat -A OUTPUT -p tcp --dport 7892 -j RETURN
|
||||
|
||||
# 让所有 TCP 流量通过 7892 代理
|
||||
sudo iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 7892
|
||||
iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 7892
|
||||
|
||||
# 保存规则
|
||||
sudo iptables-save | sudo tee /etc/iptables.rules
|
||||
iptables-save | tee /etc/iptables.rules
|
||||
```
|
||||
|
||||
3. 让 iptables 规则开机生效
|
||||
@ -345,7 +345,7 @@ exit 0
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo chmod +x /etc/rc.local
|
||||
chmod +x /etc/rc.local
|
||||
```
|
||||
|
||||
|
||||
|
||||
458
clashctl
@ -65,44 +65,27 @@ usage() {
|
||||
Usage:
|
||||
clashctl COMMAND [OPTIONS]
|
||||
|
||||
Core Commands:
|
||||
Commands:
|
||||
on 开启当前终端代理
|
||||
off 关闭当前终端代理
|
||||
start 启动 Clash
|
||||
stop 停止 Clash
|
||||
restart 重新生成配置并重启
|
||||
status 查看当前状态
|
||||
update git pull + 生成配置 + 重启
|
||||
generate 仅生成配置,不启动
|
||||
mode 查看当前运行模式(systemd/script/none)
|
||||
|
||||
Utility Commands:
|
||||
ui 输出 Dashboard 地址
|
||||
secret 输出当前 secret
|
||||
sub show 查看订阅地址
|
||||
sub update 重新生成配置并重启
|
||||
tun status|on|off 查看/启用/关闭 Tun
|
||||
mixin status|on|off 查看/启用/关闭 Mixin
|
||||
doctor 健康检查
|
||||
logs [-f] [-n 100] 查看日志
|
||||
update git pull + 重新生成配置并重启
|
||||
sub show|update 查看订阅地址 / 重新生成配置并重启
|
||||
tun status|on|off 查看/启用/关闭 Tun
|
||||
mixin status|on|off 查看/启用/关闭 Mixin
|
||||
|
||||
Options:
|
||||
--from-systemd 内部使用,避免 stop 递归调用 systemctl
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
Examples:
|
||||
clashctl on
|
||||
clashctl off
|
||||
clashctl start
|
||||
clashctl stop
|
||||
clashctl restart
|
||||
clashctl status
|
||||
clashctl update
|
||||
clashctl generate
|
||||
clashctl ui
|
||||
clashctl secret
|
||||
clashctl sub show
|
||||
clashctl tun on
|
||||
EOF
|
||||
}
|
||||
|
||||
@ -145,6 +128,66 @@ read_state_value() {
|
||||
sed -nE "s/^${key}=(.*)$/\1/p" "$STATE_FILE" | head -n 1
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
port_from_controller() {
|
||||
local controller
|
||||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||
if [ -n "${controller:-}" ]; then
|
||||
printf '%s\n' "${controller##*:}"
|
||||
else
|
||||
printf '9090\n'
|
||||
fi
|
||||
}
|
||||
|
||||
http_port_from_config() {
|
||||
local v
|
||||
|
||||
v="$(read_runtime_config_value "mixed-port" || true)"
|
||||
if [ -n "${v:-}" ]; then
|
||||
printf '%s\n' "$v"
|
||||
return 0
|
||||
fi
|
||||
|
||||
v="$(read_runtime_config_value "port" || true)"
|
||||
if [ -n "${v:-}" ]; then
|
||||
printf '%s\n' "$v"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '7890\n'
|
||||
}
|
||||
|
||||
check_port_listening() {
|
||||
local port="$1"
|
||||
|
||||
if command_exists ss; then
|
||||
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
|
||||
return $?
|
||||
elif command_exists netstat; then
|
||||
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
check_dashboard_http() {
|
||||
local url="$1"
|
||||
|
||||
if command_exists curl; then
|
||||
curl -fsS --max-time 3 "$url" >/dev/null 2>&1
|
||||
return $?
|
||||
elif command_exists wget; then
|
||||
wget -q -T 3 -O /dev/null "$url" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
cmd_on() {
|
||||
require_profiled
|
||||
# shellcheck disable=SC1090
|
||||
@ -163,6 +206,11 @@ cmd_mode() {
|
||||
detect_mode
|
||||
}
|
||||
|
||||
cmd_generate() {
|
||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
||||
ok "Config generated"
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
local mode
|
||||
mode="$(detect_mode)"
|
||||
@ -174,6 +222,7 @@ cmd_start() {
|
||||
;;
|
||||
script|none)
|
||||
start_via_script
|
||||
ok "Clash started via script mode"
|
||||
;;
|
||||
*)
|
||||
err "未知模式: $mode"
|
||||
@ -190,13 +239,15 @@ cmd_stop() {
|
||||
case "$mode" in
|
||||
systemd)
|
||||
if [ "$from_systemd" = "true" ]; then
|
||||
# 被 systemd ExecStop 调用时,不能再反向 systemctl stop 自己
|
||||
ok "Stop requested from systemd, skip recursive systemctl stop"
|
||||
exit 0
|
||||
return 0
|
||||
fi
|
||||
stop_via_systemd
|
||||
ok "Clash stopped via systemd"
|
||||
;;
|
||||
systemd-installed)
|
||||
info "systemd service installed but not running"
|
||||
;;
|
||||
script)
|
||||
stop_via_script
|
||||
ok "Clash stopped via script mode"
|
||||
@ -211,51 +262,142 @@ cmd_stop() {
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_generate() {
|
||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
||||
ok "Config generated"
|
||||
cmd_restart() {
|
||||
cmd_generate
|
||||
cmd_stop "${1:-false}" || true
|
||||
cmd_start
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
||||
cmd_update() {
|
||||
git -C "$PROJECT_DIR" pull
|
||||
cmd_restart
|
||||
ok "Project updated"
|
||||
}
|
||||
|
||||
local mode
|
||||
mode="$(detect_mode)"
|
||||
cmd_ui() {
|
||||
local raw="${1:-}"
|
||||
local controller host port secret base_url
|
||||
|
||||
case "$mode" in
|
||||
systemd|systemd-installed)
|
||||
restart_via_systemd
|
||||
ok "Clash restarted via systemd"
|
||||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||
[ -n "${controller:-}" ] || controller="127.0.0.1:9090"
|
||||
|
||||
host="${controller%:*}"
|
||||
port="${controller##*:}"
|
||||
|
||||
case "$host" in
|
||||
0.0.0.0|::|localhost)
|
||||
host="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
[ -n "${host:-}" ] || host="127.0.0.1"
|
||||
;;
|
||||
script|none)
|
||||
restart_via_script
|
||||
ok "Clash restarted via script mode"
|
||||
esac
|
||||
|
||||
secret="$(read_runtime_config_value "secret" || true)"
|
||||
base_url="http://${host}:${port}/ui"
|
||||
|
||||
if [ -n "${secret:-}" ]; then
|
||||
base_url="${base_url}/#/setup?hostname=${host}&port=${port}&secret=${secret}"
|
||||
fi
|
||||
|
||||
if [ "$raw" = "--raw" ]; then
|
||||
printf '%s\n' "$base_url"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$base_url"
|
||||
}
|
||||
|
||||
cmd_secret() {
|
||||
local secret
|
||||
|
||||
if [ ! -s "$RUNTIME_CONFIG" ]; then
|
||||
err "runtime config not found: $RUNTIME_CONFIG"
|
||||
echo "Please run: clashctl generate" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secret="$(read_runtime_config_value "secret" || true)"
|
||||
if [ -z "${secret:-}" ]; then
|
||||
err "secret not found in $RUNTIME_CONFIG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$secret"
|
||||
}
|
||||
|
||||
cmd_sub() {
|
||||
local subcmd="${1:-show}"
|
||||
|
||||
case "$subcmd" in
|
||||
show)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || echo "CLASH_URL=未配置"
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
cmd_restart
|
||||
;;
|
||||
*)
|
||||
err "未知模式: $mode"
|
||||
err "未知 sub 子命令: $subcmd"
|
||||
echo "用法: clashctl sub [show|update]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
git -C "$PROJECT_DIR" pull
|
||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
||||
cmd_tun() {
|
||||
local subcmd="${1:-status}"
|
||||
|
||||
local mode
|
||||
mode="$(detect_mode)"
|
||||
|
||||
case "$mode" in
|
||||
systemd)
|
||||
restart_via_systemd
|
||||
ok "Project updated and restarted via systemd"
|
||||
case "$subcmd" in
|
||||
status)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置'
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
script|none)
|
||||
restart_via_script
|
||||
ok "Project updated and restarted via script mode"
|
||||
on)
|
||||
write_env_bool "CLASH_TUN" "true"
|
||||
ok "已写入 CLASH_TUN=true"
|
||||
;;
|
||||
off)
|
||||
write_env_bool "CLASH_TUN" "false"
|
||||
ok "已写入 CLASH_TUN=false"
|
||||
;;
|
||||
*)
|
||||
err "未知模式: $mode"
|
||||
err "未知 tun 子命令: $subcmd"
|
||||
echo "用法: clashctl tun [status|on|off]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_mixin() {
|
||||
local subcmd="${1:-status}"
|
||||
|
||||
case "$subcmd" in
|
||||
status)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_MIXIN=' "$ENV_FILE" || echo 'CLASH_MIXIN=未配置'
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
on)
|
||||
write_env_bool "CLASH_MIXIN" "true"
|
||||
ok "已写入 CLASH_MIXIN=true"
|
||||
;;
|
||||
off)
|
||||
write_env_bool "CLASH_MIXIN" "false"
|
||||
ok "已写入 CLASH_MIXIN=false"
|
||||
;;
|
||||
*)
|
||||
err "未知 mixin 子命令: $subcmd"
|
||||
echo "用法: clashctl mixin [status|on|off]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -387,23 +529,6 @@ cmd_status() {
|
||||
fi
|
||||
|
||||
echo "Secret : $secret_exists"
|
||||
|
||||
echo
|
||||
case "$running" in
|
||||
yes)
|
||||
ok "status summary: running"
|
||||
;;
|
||||
no)
|
||||
if [ "$mode" = "systemd-installed" ]; then
|
||||
warn "status summary: installed but not running"
|
||||
else
|
||||
warn "status summary: not running"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
warn "status summary: unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
doctor_ok() {
|
||||
@ -418,182 +543,6 @@ doctor_err() {
|
||||
printf "\033[31m[ERROR]\033[0m %s\n" "$*"
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
port_from_controller() {
|
||||
local controller
|
||||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||
if [ -n "${controller:-}" ]; then
|
||||
printf '%s\n' "${controller##*:}"
|
||||
else
|
||||
printf '9090\n'
|
||||
fi
|
||||
}
|
||||
|
||||
http_port_from_config() {
|
||||
local v
|
||||
|
||||
v="$(read_runtime_config_value "mixed-port" || true)"
|
||||
if [ -n "${v:-}" ]; then
|
||||
printf '%s\n' "$v"
|
||||
return 0
|
||||
fi
|
||||
|
||||
v="$(read_runtime_config_value "port" || true)"
|
||||
if [ -n "${v:-}" ]; then
|
||||
printf '%s\n' "$v"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '7890\n'
|
||||
}
|
||||
|
||||
check_port_listening() {
|
||||
local port="$1"
|
||||
|
||||
if command_exists ss; then
|
||||
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
|
||||
return $?
|
||||
elif command_exists netstat; then
|
||||
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
check_dashboard_http() {
|
||||
local url="$1"
|
||||
|
||||
if command_exists curl; then
|
||||
curl -fsS --max-time 3 "$url" >/dev/null 2>&1
|
||||
return $?
|
||||
elif command_exists wget; then
|
||||
wget -q -T 3 -O /dev/null "$url" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
cmd_ui() {
|
||||
local raw="${1:-}"
|
||||
local controller host port
|
||||
|
||||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||
[ -n "${controller:-}" ] || controller="127.0.0.1:9090"
|
||||
|
||||
host="${controller%:*}"
|
||||
port="${controller##*:}"
|
||||
|
||||
case "$host" in
|
||||
0.0.0.0|::|localhost)
|
||||
host="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
[ -n "${host:-}" ] || host="127.0.0.1"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$raw" = "--raw" ]; then
|
||||
printf 'http://%s:%s/ui\n' "$host" "$port"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Dashboard URL:"
|
||||
printf 'http://%s:%s/ui\n' "$host" "$port"
|
||||
}
|
||||
|
||||
cmd_secret() {
|
||||
local secret
|
||||
secret="$(read_runtime_config_value "secret" || true)"
|
||||
if [ -n "${secret:-}" ]; then
|
||||
echo "$secret"
|
||||
else
|
||||
err "未读取到 secret"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_sub() {
|
||||
local subcmd="${1:-show}"
|
||||
|
||||
case "$subcmd" in
|
||||
show)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || echo "CLASH_URL=未配置"
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
update)
|
||||
cmd_restart
|
||||
;;
|
||||
*)
|
||||
err "未知 sub 子命令: $subcmd"
|
||||
echo "用法: clashctl sub [show|update]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_tun() {
|
||||
local subcmd="${1:-status}"
|
||||
|
||||
case "$subcmd" in
|
||||
status)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置'
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
on)
|
||||
write_env_bool "CLASH_TUN" "true"
|
||||
ok "已写入 CLASH_TUN=true"
|
||||
;;
|
||||
off)
|
||||
write_env_bool "CLASH_TUN" "false"
|
||||
ok "已写入 CLASH_TUN=false"
|
||||
;;
|
||||
*)
|
||||
err "未知 tun 子命令: $subcmd"
|
||||
echo "用法: clashctl tun [status|on|off]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_mixin() {
|
||||
local subcmd="${1:-status}"
|
||||
|
||||
case "$subcmd" in
|
||||
status)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_MIXIN=' "$ENV_FILE" || echo 'CLASH_MIXIN=未配置'
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
on)
|
||||
write_env_bool "CLASH_MIXIN" "true"
|
||||
ok "已写入 CLASH_MIXIN=true"
|
||||
;;
|
||||
off)
|
||||
write_env_bool "CLASH_MIXIN" "false"
|
||||
ok "已写入 CLASH_MIXIN=false"
|
||||
;;
|
||||
*)
|
||||
err "未知 mixin 子命令: $subcmd"
|
||||
echo "用法: clashctl mixin [status|on|off]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_doctor() {
|
||||
local mode running="no"
|
||||
local failed=0
|
||||
@ -612,7 +561,6 @@ cmd_doctor() {
|
||||
echo "Config : $RUNTIME_CONFIG"
|
||||
echo
|
||||
|
||||
# 1. 检查运行配置是否存在
|
||||
if [ -s "$RUNTIME_CONFIG" ]; then
|
||||
doctor_ok "runtime config exists: $RUNTIME_CONFIG"
|
||||
else
|
||||
@ -620,7 +568,6 @@ cmd_doctor() {
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 2. 检查配置里是否还有未渲染占位符
|
||||
if [ -f "$RUNTIME_CONFIG" ]; then
|
||||
if grep -q '\${' "$RUNTIME_CONFIG"; then
|
||||
doctor_err "runtime config contains unresolved placeholders"
|
||||
@ -630,7 +577,6 @@ cmd_doctor() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. 检查 state.env
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
doctor_ok "state file exists: $STATE_FILE"
|
||||
|
||||
@ -658,7 +604,6 @@ cmd_doctor() {
|
||||
warned=1
|
||||
fi
|
||||
|
||||
# 4. 检查运行模式 / 进程状态
|
||||
case "$mode" in
|
||||
systemd)
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
@ -692,7 +637,6 @@ cmd_doctor() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# 5. 检查 dashboard 地址
|
||||
dashboard_url="$(cmd_ui --raw 2>/dev/null || true)"
|
||||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||
dashboard_port="$(port_from_controller)"
|
||||
@ -711,7 +655,6 @@ cmd_doctor() {
|
||||
warned=1
|
||||
fi
|
||||
|
||||
# 6. 检查 HTTP 代理端口
|
||||
http_port="$(http_port_from_config)"
|
||||
if check_port_listening "$http_port"; then
|
||||
doctor_ok "proxy port is listening: $http_port"
|
||||
@ -731,7 +674,6 @@ cmd_doctor() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7. 检查 dashboard 端口
|
||||
if check_port_listening "$dashboard_port"; then
|
||||
doctor_ok "dashboard port is listening: $dashboard_port"
|
||||
else
|
||||
@ -750,7 +692,6 @@ cmd_doctor() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# 8. 检查 dashboard HTTP 可访问性
|
||||
if [ -n "${dashboard_url:-}" ]; then
|
||||
if check_dashboard_http "$dashboard_url"; then
|
||||
doctor_ok "dashboard http reachable"
|
||||
@ -766,7 +707,6 @@ cmd_doctor() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# 9. 检查 secret
|
||||
secret="$(read_runtime_config_value "secret" || true)"
|
||||
if [ -n "${secret:-}" ]; then
|
||||
doctor_ok "secret exists in runtime config"
|
||||
@ -775,7 +715,6 @@ cmd_doctor() {
|
||||
warned=1
|
||||
fi
|
||||
|
||||
# 10. 检查 clashctl 安装位置(可选)
|
||||
if command_exists clashctl; then
|
||||
doctor_ok "clashctl command available: $(command -v clashctl)"
|
||||
else
|
||||
@ -783,7 +722,6 @@ cmd_doctor() {
|
||||
warned=1
|
||||
fi
|
||||
|
||||
# 11. 检查代理快捷函数文件
|
||||
if [ -f "$PROFILED_FILE" ]; then
|
||||
doctor_ok "shell proxy helper exists: $PROFILED_FILE"
|
||||
else
|
||||
@ -807,13 +745,13 @@ cmd_doctor() {
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
shift || true
|
||||
|
||||
local follow="false"
|
||||
local lines="50"
|
||||
local mode
|
||||
local arg
|
||||
|
||||
shift || true
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
arg="$1"
|
||||
case "$arg" in
|
||||
@ -893,7 +831,7 @@ main() {
|
||||
cmd_stop "$from_systemd"
|
||||
;;
|
||||
restart)
|
||||
cmd_restart
|
||||
cmd_restart "$from_systemd"
|
||||
;;
|
||||
status)
|
||||
cmd_status
|
||||
|
||||
@ -5,5 +5,5 @@
|
||||
如需手动指定顺序或使用自定义路径,请在 `.env` 中设置:
|
||||
|
||||
```bash
|
||||
export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml'
|
||||
export CLASH_MIXIN_PATHS='config/mixin.d/base.yaml,config/mixin.d/rules.yaml'
|
||||
```
|
||||
|
||||
@ -25,14 +25,14 @@ Mixin 用于在 **不直接修改主配置文件** 的情况下,
|
||||
|
||||
### Default Behavior
|
||||
|
||||
- 默认读取目录:`conf/mixin.d/`
|
||||
- 默认读取目录:`config/mixin.d/`
|
||||
- 按文件名排序后依次合并
|
||||
- 后加载的文件会覆盖前面的配置
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
# conf/mixin.d/10-rules.yaml
|
||||
# config/mixin.d/10-rules.yaml
|
||||
rules:
|
||||
- DOMAIN-SUFFIX,example.com,DIRECT
|
||||
```
|
||||
@ -170,7 +170,7 @@ clash-for-linux 以 **安全默认配置** 为原则:
|
||||
|
||||
### Service Keeps Restarting
|
||||
|
||||
- 检查 `conf/config.yaml` 是否存在语法错误
|
||||
- 检查 `runtime/config.yaml` 是否存在语法错误
|
||||
- 查看 systemd 日志:
|
||||
|
||||
```
|
||||
|
||||
@ -87,15 +87,14 @@ CLASH_START_SERVICE=true
|
||||
|
||||
### `CLASH_AUTO_DOWNLOAD`
|
||||
|
||||
```
|
||||
```env
|
||||
CLASH_AUTO_DOWNLOAD=auto
|
||||
```
|
||||
|
||||
- 是否自动下载 Clash 内核
|
||||
- 是否在本地未检测到可用内核时自动下载 Mihomo 内核
|
||||
- 可选值:
|
||||
- `auto`(默认):检测不到内核时自动下载
|
||||
- `true`:强制重新下载
|
||||
- `false`:关闭自动下载
|
||||
- `auto`(默认):当未检测到可用内核时自动下载(已有内核则不覆盖)
|
||||
- `false`:不进行任何自动下载,仅使用本地已有内核(找不到则报错)
|
||||
- `true`:强制重新下载内核(即使本地已有也会覆盖)
|
||||
|
||||
适用于:
|
||||
|
||||
@ -107,12 +106,13 @@ CLASH_AUTO_DOWNLOAD=auto
|
||||
|
||||
### `CLASH_DOWNLOAD_URL_TEMPLATE`
|
||||
|
||||
```
|
||||
CLASH_DOWNLOAD_URL_TEMPLATE=https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz
|
||||
```
|
||||
```env
|
||||
CLASH_DOWNLOAD_URL_TEMPLATE=https://your-mirror.example.com/{version}/mihomo-{arch}-{version}.gz
|
||||
|
||||
- Mihomo 内核下载地址模板(可选,高级配置)
|
||||
- 仅在 CLASH_AUTO_DOWNLOAD=true 或 auto 且本地无内核时生效
|
||||
- 默认情况下无需配置,脚本会自动使用官方 GitHub Release 地址
|
||||
|
||||
- Clash 内核下载地址模板
|
||||
- `{arch}` 会自动替换为当前系统架构(如 `amd64`、`arm64`)
|
||||
|
||||
适用于:
|
||||
|
||||
@ -173,13 +173,13 @@ Mixin 用于在不修改主配置的情况下,追加或覆盖 Clash 配置项
|
||||
|
||||
### 默认行为
|
||||
|
||||
- 默认读取目录:`conf/mixin.d/`
|
||||
- 默认读取目录:`config/mixin.d/`
|
||||
- 按文件名排序后依次合并
|
||||
|
||||
### 示例
|
||||
|
||||
```
|
||||
# conf/mixin.d/rules.yaml
|
||||
# config/mixin.d/rules.yaml
|
||||
rules:
|
||||
- DOMAIN-SUFFIX,example.com,DIRECT
|
||||
```
|
||||
|
||||
635
install.sh
@ -1,113 +1,33 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =========================
|
||||
# 基础参数
|
||||
# =========================
|
||||
Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}"
|
||||
Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}"
|
||||
|
||||
Service_Name="clash-for-linux"
|
||||
Service_User="root"
|
||||
Service_Group="root"
|
||||
|
||||
# =========================
|
||||
# 彩色输出(统一 printf + 自动降级 + 手动关色)
|
||||
# =========================
|
||||
|
||||
# ---- 关色开关(优先级最高)----
|
||||
NO_COLOR_FLAG=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-color|--nocolor)
|
||||
NO_COLOR_FLAG=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "${NO_COLOR:-}" ]] || [[ -n "${CLASH_NO_COLOR:-}" ]]; then
|
||||
NO_COLOR_FLAG=1
|
||||
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
|
||||
|
||||
# ---- 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}"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 前置校验
|
||||
# 基础校验
|
||||
# =========================
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
err "需要 root 权限执行安装脚本(请使用 bash install.sh)"
|
||||
echo "[ERROR] root required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${Server_Dir}/.env" ]; then
|
||||
err "未找到 .env 文件,请确认脚本所在目录:${Server_Dir}"
|
||||
echo "[ERROR] .env not found in ${Server_Dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 同步到安装目录(保持你原逻辑)
|
||||
# 同步文件
|
||||
# =========================
|
||||
mkdir -p "$Install_Dir"
|
||||
|
||||
if [ "$Server_Dir" != "$Install_Dir" ]; then
|
||||
info "同步项目文件到安装目录:${Install_Dir}"
|
||||
echo "[INFO] sync project to ${Install_Dir}"
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/"
|
||||
else
|
||||
@ -115,253 +35,80 @@ if [ "$Server_Dir" != "$Install_Dir" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x "$Install_Dir"/*.sh 2>/dev/null || true
|
||||
chmod +x "$Install_Dir"/clashctl 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
|
||||
|
||||
# =========================
|
||||
# 加载环境与依赖脚本
|
||||
# 目录初始化(新结构)
|
||||
# =========================
|
||||
mkdir -p \
|
||||
"$Install_Dir/runtime" \
|
||||
"$Install_Dir/logs" \
|
||||
"$Install_Dir/config/mixin.d"
|
||||
|
||||
# =========================
|
||||
# 加载 env
|
||||
# =========================
|
||||
# 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 架构"
|
||||
# =========================
|
||||
# 内核检查
|
||||
# =========================
|
||||
if ! resolve_clash_bin "$Install_Dir" "${CpuArch:-}" >/dev/null 2>&1; then
|
||||
echo "[ERROR] clash core not ready" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# .env 写入工具:write_env_kv(必须在 prompt 之前定义)
|
||||
# - 自动创建文件
|
||||
# - 存在则替换,不存在则追加
|
||||
# - 统一写成:export KEY="VALUE"
|
||||
# - 自动转义双引号/反斜杠
|
||||
# 安装 clashctl
|
||||
# =========================
|
||||
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
|
||||
}
|
||||
install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl
|
||||
|
||||
# =========================
|
||||
# 交互式填写订阅地址(仅在 CLASH_URL 为空时触发)
|
||||
# - 若非 TTY(CI/管道)则跳过交互
|
||||
# - 若用户回车跳过,则保持原行为:装完提示手动配置
|
||||
# 安装 proxy helper
|
||||
# =========================
|
||||
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}"
|
||||
cat >/etc/profile.d/clash-for-linux.sh <<EOF
|
||||
proxy_on() {
|
||||
local port="\${1:-7890}"
|
||||
export http_proxy="http://127.0.0.1:\${port}"
|
||||
export https_proxy="\$http_proxy"
|
||||
export HTTP_PROXY="\$http_proxy"
|
||||
export HTTPS_PROXY="\$http_proxy"
|
||||
export no_proxy="127.0.0.1,localhost"
|
||||
export NO_PROXY="\$no_proxy"
|
||||
echo "[OK] Proxy enabled: \$http_proxy"
|
||||
}
|
||||
|
||||
prompt_clash_url_if_empty
|
||||
proxy_off() {
|
||||
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY no_proxy NO_PROXY
|
||||
echo "[OK] Proxy disabled"
|
||||
}
|
||||
EOF
|
||||
|
||||
chmod 644 /etc/profile.d/clash-for-linux.sh
|
||||
|
||||
# =========================
|
||||
# 端口冲突检测(保持你原逻辑)
|
||||
# =========================
|
||||
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
|
||||
}
|
||||
|
||||
# 计算字符串可视宽度:中文大概率按 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
|
||||
}
|
||||
|
||||
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" ""
|
||||
}
|
||||
|
||||
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))"
|
||||
}
|
||||
|
||||
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" ""
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# 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'
|
||||
)"
|
||||
|
||||
# 去掉纯空格
|
||||
s="$(printf '%s' "$s" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
|
||||
[ -n "$s" ] || return 1
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# systemd 安装与启动
|
||||
# 安装 systemd
|
||||
# =========================
|
||||
Service_Enabled="unknown"
|
||||
Service_Started="unknown"
|
||||
|
||||
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" "$Install_Dir"
|
||||
|
||||
if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then
|
||||
systemctl start "${Service_Name}.service" || true
|
||||
systemctl enable "${Service_Name}.service" || true
|
||||
fi
|
||||
|
||||
if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then
|
||||
systemctl start "${Service_Name}.service" || true
|
||||
fi
|
||||
@ -378,270 +125,26 @@ if command -v systemctl >/dev/null 2>&1; then
|
||||
Service_Started="inactive"
|
||||
fi
|
||||
else
|
||||
warn "未检测到 systemd,已跳过服务单元生成"
|
||||
echo "[WARN] systemd not found, will use script mode"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Shell 代理快捷命令
|
||||
# 生成:/etc/profile.d/clash-for-linux.sh
|
||||
# 输出(全部收敛到 clashctl)
|
||||
# =========================
|
||||
PROFILED_FILE="/etc/profile.d/clash-for-linux.sh"
|
||||
echo
|
||||
echo "=== Install Complete ==="
|
||||
echo "Install Dir : $Install_Dir"
|
||||
echo "clashctl : /usr/local/bin/clashctl"
|
||||
|
||||
install_profiled() {
|
||||
local install_dir="$Install_Dir"
|
||||
echo
|
||||
echo "Next:"
|
||||
echo " clashctl generate"
|
||||
echo " clashctl start"
|
||||
echo " clashctl doctor"
|
||||
|
||||
cat >"$PROFILED_FILE" <<EOF
|
||||
# Clash for Linux proxy helpers
|
||||
# Auto-generated by clash-for-linux installer.
|
||||
|
||||
# ===== 自动加载 .env =====
|
||||
CLASH_INSTALL_DIR="${install_dir}"
|
||||
ENV_FILE="\${CLASH_INSTALL_DIR}/.env"
|
||||
|
||||
if [ -f "\$ENV_FILE" ]; then
|
||||
set +u
|
||||
. "\$ENV_FILE" >/dev/null 2>&1 || true
|
||||
set -u
|
||||
fi
|
||||
|
||||
# ===== 默认值(兜底)=====
|
||||
CLASH_LISTEN_IP="\${CLASH_LISTEN_IP:-127.0.0.1}"
|
||||
CLASH_HTTP_PORT="\${CLASH_HTTP_PORT:-7890}"
|
||||
CLASH_SOCKS_PORT="\${CLASH_SOCKS_PORT:-7891}"
|
||||
|
||||
# ===== 开启代理 =====
|
||||
proxy_on() {
|
||||
export http_proxy="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}"
|
||||
export https_proxy="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}"
|
||||
export HTTP_PROXY="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}"
|
||||
export HTTPS_PROXY="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}"
|
||||
export all_proxy="socks5://\${CLASH_LISTEN_IP}:\${CLASH_SOCKS_PORT}"
|
||||
export ALL_PROXY="socks5://\${CLASH_LISTEN_IP}:\${CLASH_SOCKS_PORT}"
|
||||
export no_proxy="127.0.0.1,localhost,::1"
|
||||
export NO_PROXY="127.0.0.1,localhost,::1"
|
||||
echo "[OK] Proxy enabled: http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}"
|
||||
}
|
||||
|
||||
# ===== 关闭代理 =====
|
||||
proxy_off() {
|
||||
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
|
||||
unset all_proxy ALL_PROXY no_proxy NO_PROXY
|
||||
echo "[OK] Proxy disabled"
|
||||
}
|
||||
|
||||
# ===== 状态 =====
|
||||
proxy_status() {
|
||||
echo "http_proxy=\${http_proxy:-<empty>}"
|
||||
echo "https_proxy=\${https_proxy:-<empty>}"
|
||||
echo "all_proxy=\${all_proxy:-<empty>}"
|
||||
echo "CLASH_HTTP_PORT=\${CLASH_HTTP_PORT}"
|
||||
echo "CLASH_SOCKS_PORT=\${CLASH_SOCKS_PORT}"
|
||||
}
|
||||
EOF
|
||||
|
||||
chmod 644 "$PROFILED_FILE"
|
||||
|
||||
# ===== 自动写入 bashrc(关键!)=====
|
||||
local BASHRC_FILE="/root/.bashrc"
|
||||
local SOURCE_LINE='[ -f /etc/profile.d/clash-for-linux.sh ] && source /etc/profile.d/clash-for-linux.sh'
|
||||
|
||||
if [ -f "$BASHRC_FILE" ]; then
|
||||
if ! grep -Fq "$SOURCE_LINE" "$BASHRC_FILE"; then
|
||||
echo "$SOURCE_LINE" >> "$BASHRC_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
install_profiled || true
|
||||
|
||||
# =========================
|
||||
# 安装 clashctl 命令
|
||||
# =========================
|
||||
if [ -f "$Install_Dir/clashctl" ]; then
|
||||
install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl
|
||||
fi
|
||||
|
||||
if [ -f "$Install_Dir/clashon" ]; then
|
||||
install -m 0755 "$Install_Dir/clashon" /usr/local/bin/clashon
|
||||
fi
|
||||
|
||||
if [ -f "$Install_Dir/clashoff" ]; then
|
||||
install -m 0755 "$Install_Dir/clashoff" /usr/local/bin/clashoff
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 友好收尾输出
|
||||
# - 不再强调瞬时 active / inactive
|
||||
# - 统一引导到 clashctl
|
||||
# - 兼容 systemd / 非 systemd
|
||||
# =========================
|
||||
|
||||
section "安装完成"
|
||||
ok "Clash for Linux 已安装至: $(path "${Install_Dir}")"
|
||||
|
||||
log "📦 安装目录:$(path "${Install_Dir}")"
|
||||
log "👤 运行用户:${Service_User}:${Service_Group}"
|
||||
log "🧩 管理入口:$(cmd "clashctl")"
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
log "🔧 服务名称:${Service_Name}.service"
|
||||
else
|
||||
log "🔧 运行模式:非 systemd 环境(将使用脚本模式兜底)"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 安装结果
|
||||
# 不在这里渲染瞬时运行态,避免误导
|
||||
# =========================
|
||||
section "安装结果"
|
||||
|
||||
ok "核心文件已就位"
|
||||
ok "运行入口已收敛为 clashctl"
|
||||
|
||||
if [[ -x "/usr/local/bin/clashctl" ]]; then
|
||||
ok "命令已可用:$(cmd "clashctl")"
|
||||
else
|
||||
warn "未检测到 /usr/local/bin/clashctl,请确认安装脚本是否已完成链接"
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "${C_BOLD}推荐下一步:${C_NC}"
|
||||
log " 1. $(cmd "clashctl generate") # 生成运行配置"
|
||||
log " 2. $(cmd "clashctl start") # 启动 Clash"
|
||||
log " 3. $(cmd "clashctl doctor") # 健康检查"
|
||||
|
||||
# =========================
|
||||
# 控制面板 / Secret
|
||||
# =========================
|
||||
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"
|
||||
RUNTIME_DIR="$Install_Dir/runtime"
|
||||
|
||||
SECRET_VAL=""
|
||||
SECRET_FILE=""
|
||||
|
||||
read_secret_safe() {
|
||||
local f="$1"
|
||||
[[ -f "$f" ]] || return 1
|
||||
read_secret_from_config "$f" 2>/dev/null || true
|
||||
}
|
||||
|
||||
for _ in {1..15}; do
|
||||
for f in \
|
||||
"$RUNTIME_DIR/config.yaml" \
|
||||
"$TEMP_DIR/config.yaml" \
|
||||
"$CONF_DIR/config.yaml"
|
||||
do
|
||||
SECRET_VAL="$(read_secret_safe "$f")"
|
||||
if [[ -n "$SECRET_VAL" ]]; then
|
||||
SECRET_FILE="$f"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
dash="http://${api_host}:${api_port}/ui"
|
||||
log "🌐 Dashboard:$(url "$dash")"
|
||||
|
||||
SHOW_FILE="${SECRET_FILE:-$RUNTIME_DIR/config.yaml}"
|
||||
|
||||
if [[ -n "$SECRET_VAL" ]]; then
|
||||
log "🔐 Secret:${C_YELLOW}${SECRET_VAL}${C_NC}"
|
||||
else
|
||||
log "🔐 Secret:${C_YELLOW}暂未读取到${C_NC}"
|
||||
log " 启动后可查看:$(cmd "sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$RUNTIME_DIR/config.yaml\" | head -n 1")"
|
||||
log " 或检查旧路径:$(cmd "sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$TEMP_DIR/config.yaml\" | 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 "bash -c 'printf \"%s\n\" \"CLASH_URL=<订阅地址>\" > \"${ENV_FILE}\"'")"
|
||||
log ""
|
||||
log "配置完成后执行:"
|
||||
log " 1. $(cmd "clashctl generate")"
|
||||
log " 2. $(cmd "clashctl start")"
|
||||
log " 3. $(cmd "clashctl doctor")"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 常用命令(统一只教 clashctl)
|
||||
# =========================
|
||||
section "常用命令"
|
||||
|
||||
log " $(cmd "clashctl status") # 查看运行状态"
|
||||
log " $(cmd "clashctl logs") # 查看最近日志"
|
||||
log " $(cmd "clashctl logs -f") # 持续追踪日志"
|
||||
log " $(cmd "clashctl stop") # 停止 Clash"
|
||||
log " $(cmd "clashctl restart") # 重启 Clash"
|
||||
log " $(cmd "clashctl doctor") # 健康检查"
|
||||
|
||||
# =========================
|
||||
# 终端代理(可选)
|
||||
# =========================
|
||||
section "终端代理(可选)"
|
||||
|
||||
log " $(cmd "clashctl on") # 开启当前终端代理"
|
||||
log " $(cmd "clashctl off") # 关闭当前终端代理"
|
||||
|
||||
# =========================
|
||||
# 旧入口收敛提示
|
||||
# =========================
|
||||
section "说明"
|
||||
|
||||
log "旧脚本已收敛,请优先使用:$(cmd "clashctl")"
|
||||
log "不建议继续直接操作 restart.sh / update.sh / 手写 systemctl 命令"
|
||||
|
||||
# =========================
|
||||
# 启动后快速诊断
|
||||
# =========================
|
||||
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 / 被墙)"
|
||||
log "建议执行:$(cmd "clashctl doctor")"
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
echo "Commands:"
|
||||
echo " clashctl status"
|
||||
echo " clashctl logs"
|
||||
echo " clashctl restart"
|
||||
echo " clashctl stop"
|
||||
@ -0,0 +1,6 @@
|
||||
LAST_GENERATE_STATUS=
|
||||
LAST_GENERATE_AT=
|
||||
LAST_RUN_STATUS=
|
||||
LAST_RUN_MODE=
|
||||
LAST_RUN_AT=
|
||||
LAST_RUN_PID=
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 900 B |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 622 B After Width: | Height: | Size: 622 B |
@ -1,16 +1,36 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
trim_value() {
|
||||
local value="$1"
|
||||
echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 安全写入:避免重复块
|
||||
# =========================
|
||||
remove_block_if_exists() {
|
||||
local file="$1"
|
||||
local marker="$2"
|
||||
|
||||
[ -f "$file" ] || return 0
|
||||
|
||||
# 删除已有 block(从 marker 到文件结束)
|
||||
if grep -q "$marker" "$file"; then
|
||||
sed -i "/$marker/,\$d" "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# =========================
|
||||
# TUN 配置
|
||||
# =========================
|
||||
apply_tun_config() {
|
||||
local config_path="$1"
|
||||
|
||||
local enable="${CLASH_TUN_ENABLE:-false}"
|
||||
if [ "$enable" != "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
[ "$enable" = "true" ] || return 0
|
||||
|
||||
remove_block_if_exists "$config_path" "# ==== TUN CONFIG START ===="
|
||||
|
||||
local stack="${CLASH_TUN_STACK:-system}"
|
||||
local auto_route="${CLASH_TUN_AUTO_ROUTE:-true}"
|
||||
@ -22,65 +42,91 @@ apply_tun_config() {
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "# ==== TUN CONFIG START ===="
|
||||
echo "tun:"
|
||||
echo " enable: true"
|
||||
echo " stack: ${stack}"
|
||||
echo " auto-route: ${auto_route}"
|
||||
echo " auto-redirect: ${auto_redirect}"
|
||||
echo " strict-route: ${strict_route}"
|
||||
if [ -n "$device" ]; then
|
||||
echo " device: ${device}"
|
||||
fi
|
||||
if [ -n "$mtu" ]; then
|
||||
echo " mtu: ${mtu}"
|
||||
fi
|
||||
|
||||
[ -n "$device" ] && echo " device: ${device}"
|
||||
[ -n "$mtu" ] && echo " mtu: ${mtu}"
|
||||
|
||||
if [ -n "$dns_hijack" ]; then
|
||||
echo " dns-hijack:"
|
||||
IFS=',' read -r -a hijacks <<< "$dns_hijack"
|
||||
for item in "${hijacks[@]}"; do
|
||||
local trimmed
|
||||
trimmed=$(trim_value "$item")
|
||||
if [ -n "$trimmed" ]; then
|
||||
echo " - ${trimmed}"
|
||||
fi
|
||||
item="$(trim_value "$item")"
|
||||
[ -n "$item" ] && echo " - ${item}"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "# ==== TUN CONFIG END ===="
|
||||
} >> "$config_path"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# MIXIN 配置
|
||||
# =========================
|
||||
apply_mixin_config() {
|
||||
local config_path="$1"
|
||||
local base_dir="${2:-$Server_Dir}"
|
||||
local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/conf/mixin.d}"
|
||||
local base_dir="$2"
|
||||
|
||||
local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/config/mixin.d}"
|
||||
local mixin_paths=()
|
||||
|
||||
remove_block_if_exists "$config_path" "# ==== MIXIN CONFIG START ===="
|
||||
|
||||
# 用户手动指定优先
|
||||
if [ -n "${CLASH_MIXIN_PATHS:-}" ]; then
|
||||
IFS=',' read -r -a mixin_paths <<< "$CLASH_MIXIN_PATHS"
|
||||
fi
|
||||
|
||||
# 自动扫描目录(补充)
|
||||
if [ -d "$mixin_dir" ]; then
|
||||
while IFS= read -r -d '' file; do
|
||||
mixin_paths+=("$file")
|
||||
done < <(find "$mixin_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -z)
|
||||
done < <(
|
||||
find "$mixin_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) \
|
||||
-print0 | sort -z
|
||||
)
|
||||
fi
|
||||
|
||||
# 去重
|
||||
local uniq_paths=()
|
||||
local seen=""
|
||||
|
||||
for path in "${mixin_paths[@]}"; do
|
||||
local trimmed
|
||||
trimmed=$(trim_value "$path")
|
||||
if [ -z "$trimmed" ]; then
|
||||
continue
|
||||
path="$(trim_value "$path")"
|
||||
[ -z "$path" ] && continue
|
||||
|
||||
# 相对路径转绝对
|
||||
if [ "${path:0:1}" != "/" ]; then
|
||||
path="$base_dir/$path"
|
||||
fi
|
||||
if [ "${trimmed:0:1}" != "/" ]; then
|
||||
trimmed="$base_dir/$trimmed"
|
||||
fi
|
||||
if [ -f "$trimmed" ]; then
|
||||
{
|
||||
echo ""
|
||||
echo "# ---- mixin: ${trimmed} ----"
|
||||
cat "$trimmed"
|
||||
} >> "$config_path"
|
||||
else
|
||||
echo "[WARN] Mixin file not found: $trimmed" >&2
|
||||
|
||||
if [[ "$seen" != *"|$path|"* ]]; then
|
||||
uniq_paths+=("$path")
|
||||
seen="${seen}|$path|"
|
||||
fi
|
||||
done
|
||||
|
||||
# 写入
|
||||
{
|
||||
echo ""
|
||||
echo "# ==== MIXIN CONFIG START ===="
|
||||
|
||||
for path in "${uniq_paths[@]}"; do
|
||||
if [ -f "$path" ]; then
|
||||
echo ""
|
||||
echo "# ---- mixin: ${path} ----"
|
||||
cat "$path"
|
||||
else
|
||||
echo "[WARN] Mixin not found: $path" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "# ==== MIXIN CONFIG END ===="
|
||||
} >> "$config_path"
|
||||
}
|
||||
@ -1,21 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
RUNTIME_DIR="$PROJECT_DIR/runtime"
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
echo "=== Clash Doctor ==="
|
||||
|
||||
if [ -f "$RUNTIME_DIR/config.yaml" ]; then
|
||||
echo "[OK] config exists"
|
||||
else
|
||||
echo "[ERROR] config missing"
|
||||
fi
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl is-active --quiet clash-for-linux.service; then
|
||||
echo "[OK] service running"
|
||||
else
|
||||
echo "[WARN] service not running"
|
||||
fi
|
||||
fi
|
||||
exec "$PROJECT_DIR/clashctl" doctor "$@"
|
||||
@ -12,6 +12,7 @@ STATE_FILE="$RUNTIME_DIR/state.env"
|
||||
TMP_DOWNLOAD="$RUNTIME_DIR/subscription.raw.yaml"
|
||||
TMP_NORMALIZED="$RUNTIME_DIR/subscription.normalized.yaml"
|
||||
TMP_PROXY_FRAGMENT="$RUNTIME_DIR/proxy.fragment.yaml"
|
||||
TMP_CONFIG="$RUNTIME_DIR/config.yaml.tmp"
|
||||
|
||||
mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR"
|
||||
|
||||
@ -64,6 +65,15 @@ generate_secret() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -s "$RUNTIME_CONFIG" ]; then
|
||||
local old_secret
|
||||
old_secret="$(sed -nE 's/^[[:space:]]*secret:[[:space:]]*"?([^"#]+)"?.*$/\1/p' "$RUNTIME_CONFIG" | head -n 1)"
|
||||
if [ -n "${old_secret:-}" ]; then
|
||||
echo "$old_secret"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex 16
|
||||
else
|
||||
@ -103,10 +113,12 @@ apply_controller_to_config() {
|
||||
|
||||
rm -rf "$ui_dir"
|
||||
mkdir -p "$ui_dir"
|
||||
cp -a "$PROJECT_DIR/dashboard/public/." "$ui_dir/"
|
||||
|
||||
if [ -d "$PROJECT_DIR/dashboard/public" ]; then
|
||||
cp -a "$PROJECT_DIR/dashboard/public/." "$ui_dir/"
|
||||
upsert_yaml_kv_local "$file" "external-ui" "$ui_dir"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
download_subscription() {
|
||||
@ -121,11 +133,32 @@ download_subscription() {
|
||||
|
||||
is_complete_clash_config() {
|
||||
local file="$1"
|
||||
grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$file"
|
||||
grep -qE '^[[:space:]]*(proxies:|proxy-providers:|mixed-port:|port:)' "$file"
|
||||
}
|
||||
|
||||
cleanup_tmp_files() {
|
||||
rm -f "$TMP_NORMALIZED" "$TMP_PROXY_FRAGMENT"
|
||||
rm -f "$TMP_PROXY_FRAGMENT" "$TMP_CONFIG"
|
||||
}
|
||||
|
||||
build_fragment_config() {
|
||||
local template_file="$1"
|
||||
local target_file="$2"
|
||||
|
||||
sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT"
|
||||
|
||||
cat "$template_file" > "$target_file"
|
||||
cat "$TMP_PROXY_FRAGMENT" >> "$target_file"
|
||||
|
||||
sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$target_file"
|
||||
sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$target_file"
|
||||
sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$target_file"
|
||||
sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$target_file"
|
||||
sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$target_file"
|
||||
}
|
||||
|
||||
finalize_config() {
|
||||
local file="$1"
|
||||
mv -f "$file" "$RUNTIME_CONFIG"
|
||||
}
|
||||
|
||||
main() {
|
||||
@ -156,9 +189,10 @@ main() {
|
||||
cp -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED"
|
||||
|
||||
if is_complete_clash_config "$TMP_NORMALIZED"; then
|
||||
cp -f "$TMP_NORMALIZED" "$RUNTIME_CONFIG"
|
||||
apply_controller_to_config "$RUNTIME_CONFIG"
|
||||
apply_secret_to_config "$RUNTIME_CONFIG"
|
||||
cp -f "$TMP_NORMALIZED" "$TMP_CONFIG"
|
||||
apply_controller_to_config "$TMP_CONFIG"
|
||||
apply_secret_to_config "$TMP_CONFIG"
|
||||
finalize_config "$TMP_CONFIG"
|
||||
write_state "success" "subscription_full" "subscription_full"
|
||||
cleanup_tmp_files
|
||||
exit 0
|
||||
@ -171,22 +205,14 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT"
|
||||
|
||||
cat "$template_file" > "$RUNTIME_CONFIG"
|
||||
cat "$TMP_PROXY_FRAGMENT" >> "$RUNTIME_CONFIG"
|
||||
|
||||
sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$RUNTIME_CONFIG"
|
||||
sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$RUNTIME_CONFIG"
|
||||
sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$RUNTIME_CONFIG"
|
||||
sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$RUNTIME_CONFIG"
|
||||
sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$RUNTIME_CONFIG"
|
||||
|
||||
apply_controller_to_config "$RUNTIME_CONFIG"
|
||||
apply_secret_to_config "$RUNTIME_CONFIG"
|
||||
build_fragment_config "$template_file" "$TMP_CONFIG"
|
||||
apply_controller_to_config "$TMP_CONFIG"
|
||||
apply_secret_to_config "$TMP_CONFIG"
|
||||
|
||||
finalize_config "$TMP_CONFIG"
|
||||
write_state "success" "subscription_fragment_merged" "subscription_fragment"
|
||||
cleanup_tmp_files
|
||||
}
|
||||
|
||||
trap cleanup_tmp_files EXIT
|
||||
main "$@"
|
||||
@ -10,14 +10,14 @@ SERVICE_GROUP="${CLASH_SERVICE_GROUP:-root}"
|
||||
|
||||
RUNTIME_DIR="$PROJECT_DIR/runtime"
|
||||
LOG_DIR="$PROJECT_DIR/logs"
|
||||
CONF_DIR="$PROJECT_DIR/conf"
|
||||
CONFIG_DIR="$PROJECT_DIR/config"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "[ERROR] root required to install systemd unit" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONF_DIR"
|
||||
install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONFIG_DIR" "$CONFIG_DIR/mixin.d"
|
||||
|
||||
cat >"$UNIT_PATH" <<EOF
|
||||
[Unit]
|
||||
@ -35,8 +35,11 @@ Group=${SERVICE_GROUP}
|
||||
WorkingDirectory=${PROJECT_DIR}
|
||||
Environment=HOME=/root
|
||||
|
||||
ExecStart=/bin/bash ${PROJECT_DIR}/scripts/run_clash.sh --foreground
|
||||
ExecStop=/bin/bash ${PROJECT_DIR}/clashctl --from-systemd stop
|
||||
ExecStart=${PROJECT_DIR}/clashctl start
|
||||
ExecStop=${PROJECT_DIR}/clashctl --from-systemd stop
|
||||
ExecReload=${PROJECT_DIR}/clashctl restart
|
||||
|
||||
PIDFile=${PROJECT_DIR}/runtime/clash.pid
|
||||
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
@ -62,4 +65,5 @@ echo "[OK] systemd unit installed: ${UNIT_PATH}"
|
||||
echo "start : systemctl start ${SERVICE_NAME}.service"
|
||||
echo "stop : systemctl stop ${SERVICE_NAME}.service"
|
||||
echo "restart : systemctl restart ${SERVICE_NAME}.service"
|
||||
echo "reload : systemctl reload ${SERVICE_NAME}.service"
|
||||
echo "status : systemctl status ${SERVICE_NAME}.service -l --no-pager"
|
||||
@ -1,20 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
LOG_FILE="$PROJECT_DIR/logs/clash.log"
|
||||
SERVICE_NAME="clash-for-linux.service"
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
if [ "${1:-}" = "-f" ]; then
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
journalctl -u "$SERVICE_NAME" -f
|
||||
else
|
||||
tail -f "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
journalctl -u "$SERVICE_NAME" -n 50 --no-pager
|
||||
else
|
||||
tail -n 50 "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
exec "$PROJECT_DIR/clashctl" logs "$@"
|
||||
@ -1,36 +1,49 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT_CHECK_WARNED=${PORT_CHECK_WARNED:-0}
|
||||
|
||||
# =========================
|
||||
# 判断端口是否被占用(更稳)
|
||||
# =========================
|
||||
is_port_in_use() {
|
||||
local port="$1"
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
||||
ss -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
||||
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -iTCP -sTCP:LISTEN -P -n | awk '{print $9}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
|
||||
lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | awk '{print $9}' | grep -E "[:.]${port}$" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if [ "$PORT_CHECK_WARNED" -eq 0 ]; then
|
||||
echo -e "\033[33m[WARN] 未找到端口检测工具,端口冲突检测可能不准确\033[0m" >&2
|
||||
echo "[WARN] no port check tool found (ss/netstat/lsof)" >&2
|
||||
PORT_CHECK_WARNED=1
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 找可用端口(优化版)
|
||||
# =========================
|
||||
find_available_port() {
|
||||
local start_port=${1:-20000}
|
||||
local end_port=${2:-65000}
|
||||
local start="${1:-20000}"
|
||||
local end="${2:-65000}"
|
||||
local port
|
||||
|
||||
# 优先随机尝试
|
||||
if command -v shuf >/dev/null 2>&1; then
|
||||
for _ in {1..50}; do
|
||||
port=$(shuf -i "${start_port}-${end_port}" -n 1)
|
||||
for _ in {1..30}; do
|
||||
port=$(shuf -i "${start}-${end}" -n 1)
|
||||
if ! is_port_in_use "$port"; then
|
||||
echo "$port"
|
||||
return 0
|
||||
@ -38,7 +51,8 @@ find_available_port() {
|
||||
done
|
||||
fi
|
||||
|
||||
for port in $(seq "$start_port" "$end_port"); do
|
||||
# fallback 顺序扫描(限制范围避免慢)
|
||||
for port in $(seq "$start" "$((start + 2000))"); do
|
||||
if ! is_port_in_use "$port"; then
|
||||
echo "$port"
|
||||
return 0
|
||||
@ -48,43 +62,56 @@ find_available_port() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 解析端口值(核心函数)
|
||||
# =========================
|
||||
resolve_port_value() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
local resolved
|
||||
|
||||
# auto / 空
|
||||
if [ -z "$value" ] || [ "$value" = "auto" ]; then
|
||||
resolved=$(find_available_port)
|
||||
if [ -z "$resolved" ]; then
|
||||
resolved=$(find_available_port) || {
|
||||
echo "[ERROR] ${name} failed to allocate port" >&2
|
||||
return 1
|
||||
fi
|
||||
echo -e "\033[33m[WARN] ${name} 端口已自动分配为 ${resolved}\033[0m" >&2
|
||||
}
|
||||
echo "[WARN] ${name} auto assigned: ${resolved}" >&2
|
||||
echo "$resolved"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
# 非数字
|
||||
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
echo "[ERROR] invalid port: $value" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 被占用 → 自动替换
|
||||
if is_port_in_use "$value"; then
|
||||
resolved=$(find_available_port)
|
||||
if [ -n "$resolved" ]; then
|
||||
echo -e "\033[33m[WARN] ${name} 端口 ${value} 已被占用,已自动切换为 ${resolved}\033[0m" >&2
|
||||
echo "[WARN] ${name} port ${value} in use, switched to ${resolved}" >&2
|
||||
echo "$resolved"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 解析 host:port
|
||||
# =========================
|
||||
resolve_host_port() {
|
||||
local name="$1"
|
||||
local raw="$2"
|
||||
local default_host="$3"
|
||||
|
||||
local host
|
||||
local port
|
||||
|
||||
if [ "$raw" = "auto" ] || [ -z "$raw" ]; then
|
||||
if [ -z "$raw" ] || [ "$raw" = "auto" ]; then
|
||||
host="$default_host"
|
||||
port="auto"
|
||||
else
|
||||
@ -97,6 +124,10 @@ resolve_host_port() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# host 兜底
|
||||
[ -z "$host" ] && host="$default_host"
|
||||
|
||||
port=$(resolve_port_value "$name" "$port") || return 1
|
||||
|
||||
echo "${host}:${port}"
|
||||
}
|
||||
@ -1,104 +1,180 @@
|
||||
#!/bin/bash
|
||||
|
||||
resolve_clash_arch() {
|
||||
local raw_arch="$1"
|
||||
case "$raw_arch" in
|
||||
x86_64|amd64)
|
||||
echo "linux-amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "linux-arm64"
|
||||
;;
|
||||
armv7*|armv7l)
|
||||
echo "linux-armv7"
|
||||
;;
|
||||
*)
|
||||
echo "linux-${raw_arch}"
|
||||
;;
|
||||
x86_64|amd64) echo "linux-amd64" ;;
|
||||
aarch64|arm64) echo "linux-arm64" ;;
|
||||
armv7*|armv7l) echo "linux-armv7" ;;
|
||||
*) echo "linux-${raw_arch}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_latest_mihomo_version() {
|
||||
local url="https://api.github.com/repos/MetaCubeX/mihomo/releases/latest"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$url" \
|
||||
| grep '"tag_name"' \
|
||||
| sed -E 's/.*"([^"]+)".*/\1/' \
|
||||
| head -n 1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- "$url" \
|
||||
| grep '"tag_name"' \
|
||||
| sed -E 's/.*"([^"]+)".*/\1/' \
|
||||
| head -n 1
|
||||
fi
|
||||
}
|
||||
|
||||
download_clash_bin() {
|
||||
local server_dir="$1"
|
||||
local detected_arch="$2"
|
||||
|
||||
local resolved_arch
|
||||
local version
|
||||
local download_url
|
||||
|
||||
local download_target
|
||||
local archive_file
|
||||
local tmp_bin
|
||||
|
||||
resolved_arch="$(resolve_clash_arch "$detected_arch")"
|
||||
|
||||
resolved_arch=$(resolve_clash_arch "$detected_arch")
|
||||
if [ -z "$resolved_arch" ]; then
|
||||
echo -e "\033[33m[WARN] 无法识别 CPU 架构,跳过 Clash 内核自动下载\033[0m"
|
||||
echo "[WARN] 无法识别 CPU 架构" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${CLASH_AUTO_DOWNLOAD:-auto}" = "false" ]; then
|
||||
version="${MIHOMO_VERSION:-}"
|
||||
if [ -z "$version" ]; then
|
||||
version="$(get_latest_mihomo_version || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "[ERROR] 无法获取 Mihomo 版本" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
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"
|
||||
if [ -z "${CLASH_DOWNLOAD_URL_TEMPLATE:-}" ]; then
|
||||
echo "[ERROR] CLASH_DOWNLOAD_URL_TEMPLATE 未设置" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
download_url="${download_url//\{arch\}/${resolved_arch}}"
|
||||
download_url="${CLASH_DOWNLOAD_URL_TEMPLATE//\{arch\}/${resolved_arch}}"
|
||||
download_url="${download_url//\{version\}/${version}}"
|
||||
|
||||
download_target="${server_dir}/bin/clash-${resolved_arch}"
|
||||
archive_file="${server_dir}/temp/clash-${resolved_arch}.download"
|
||||
archive_file="${server_dir}/runtime/.clash_download.tmp"
|
||||
tmp_bin="${server_dir}/runtime/.clash_bin.tmp"
|
||||
|
||||
mkdir -p "${server_dir}/bin" "${server_dir}/temp"
|
||||
mkdir -p "${server_dir}/bin" "${server_dir}/runtime"
|
||||
|
||||
rm -f "$archive_file" "$tmp_bin"
|
||||
|
||||
echo "[INFO] downloading: $download_url"
|
||||
|
||||
# =========================
|
||||
# 下载
|
||||
# =========================
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -L -sS -o "${archive_file}" "${download_url}"
|
||||
if ! curl -fL -sS -o "$archive_file" "$download_url"; then
|
||||
echo "[ERROR] 下载失败: $download_url" >&2
|
||||
return 1
|
||||
fi
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q -O "${archive_file}" "${download_url}"
|
||||
if ! wget -q -O "$archive_file" "$download_url"; then
|
||||
echo "[ERROR] 下载失败: $download_url" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo -e "\033[33m[WARN] 未找到 curl 或 wget,无法自动下载 Clash 内核\033[0m"
|
||||
echo "[ERROR] 未找到 curl 或 wget" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -f "${archive_file}" ]; then
|
||||
if gzip -t "${archive_file}" >/dev/null 2>&1; then
|
||||
gzip -dc "${archive_file}" >"${download_target}"
|
||||
else
|
||||
mv "${archive_file}" "${download_target}"
|
||||
fi
|
||||
chmod +x "${download_target}"
|
||||
echo "${download_target}"
|
||||
return 0
|
||||
# =========================
|
||||
# 基础校验(防 404 / HTML)
|
||||
# =========================
|
||||
if [ ! -s "$archive_file" ]; then
|
||||
echo "[ERROR] 下载文件为空" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "\033[33m[WARN] Clash 内核自动下载失败\033[0m"
|
||||
if head -c 200 "$archive_file" | grep -qiE "not found|html"; then
|
||||
echo "[ERROR] 下载内容疑似错误页面(404/HTML)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 解压 / 直写
|
||||
# =========================
|
||||
if gzip -t "$archive_file" >/dev/null 2>&1; then
|
||||
if ! gzip -dc "$archive_file" > "$tmp_bin"; then
|
||||
echo "[ERROR] gzip 解压失败" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
cp "$archive_file" "$tmp_bin"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# ELF 校验(关键)
|
||||
# =========================
|
||||
if ! file "$tmp_bin" | grep -q "ELF"; then
|
||||
echo "[ERROR] 非有效 ELF 二进制" >&2
|
||||
echo "[DEBUG] file result: $(file "$tmp_bin")" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod +x "$tmp_bin"
|
||||
mv "$tmp_bin" "$download_target"
|
||||
|
||||
rm -f "$archive_file"
|
||||
|
||||
echo "[OK] downloaded: $download_target"
|
||||
echo "$download_target"
|
||||
}
|
||||
|
||||
resolve_clash_bin() {
|
||||
local server_dir="$1"
|
||||
local detected_arch="$2"
|
||||
|
||||
local resolved_arch
|
||||
local candidates=()
|
||||
local candidate
|
||||
local downloaded_bin
|
||||
local mode
|
||||
|
||||
if [ -n "${CLASH_BIN:-}" ]; then
|
||||
if [ -x "$CLASH_BIN" ]; then
|
||||
echo "$CLASH_BIN"
|
||||
return 0
|
||||
fi
|
||||
echo -e "\033[31m[ERROR] CLASH_BIN 指定的文件不可执行: $CLASH_BIN\033[0m"
|
||||
echo "[ERROR] CLASH_BIN 不可执行: $CLASH_BIN" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
resolved_arch=$(resolve_clash_arch "$detected_arch")
|
||||
resolved_arch="$(resolve_clash_arch "$detected_arch")"
|
||||
|
||||
if [ -n "$resolved_arch" ]; then
|
||||
candidates+=("${server_dir}/bin/clash-${resolved_arch}")
|
||||
fi
|
||||
|
||||
candidates+=(
|
||||
"${server_dir}/bin/clash-${detected_arch}"
|
||||
"${server_dir}/bin/clash"
|
||||
)
|
||||
|
||||
mode="${CLASH_AUTO_DOWNLOAD:-auto}"
|
||||
|
||||
case "$mode" in
|
||||
false)
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [ -x "$candidate" ]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
;;
|
||||
|
||||
auto)
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [ -x "$candidate" ]; then
|
||||
echo "$candidate"
|
||||
@ -106,14 +182,33 @@ resolve_clash_bin() {
|
||||
fi
|
||||
done
|
||||
|
||||
if downloaded_bin=$(download_clash_bin "$server_dir" "$detected_arch"); then
|
||||
if downloaded_bin="$(download_clash_bin "$server_dir" "$detected_arch")"; then
|
||||
echo "$downloaded_bin"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
|
||||
true)
|
||||
if downloaded_bin="$(download_clash_bin "$server_dir" "$detected_arch")"; then
|
||||
echo "$downloaded_bin"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "\033[31m\n[ERROR] 未找到可用的 Clash 二进制。\033[0m"
|
||||
echo -e "请将对应架构的二进制放入: $server_dir/bin/"
|
||||
echo -e "可用命名示例: clash-${resolved_arch} 或 clash-${detected_arch}"
|
||||
echo -e "或通过 CLASH_BIN 指定自定义路径。"
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [ -x "$candidate" ]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "[ERROR] CLASH_AUTO_DOWNLOAD 非法值: $mode" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[ERROR] 未找到可用 Mihomo 内核" >&2
|
||||
echo "请放入: ${server_dir}/bin/" >&2
|
||||
return 1
|
||||
}
|
||||
@ -12,7 +12,9 @@ mkdir -p "$RUNTIME_DIR" "$LOG_DIR"
|
||||
FOREGROUND=false
|
||||
DAEMON=false
|
||||
|
||||
# 解析参数
|
||||
# =========================
|
||||
# 参数解析
|
||||
# =========================
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--foreground) FOREGROUND=true ;;
|
||||
@ -29,6 +31,14 @@ if [ "$FOREGROUND" = true ] && [ "$DAEMON" = true ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ "$FOREGROUND" = false ] && [ "$DAEMON" = false ]; then
|
||||
echo "[ERROR] Must specify --foreground or --daemon" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 基础校验
|
||||
# =========================
|
||||
if [ ! -s "$CONFIG_FILE" ]; then
|
||||
echo "[ERROR] runtime config not found: $CONFIG_FILE" >&2
|
||||
exit 2
|
||||
@ -39,6 +49,9 @@ if grep -q '\${' "$CONFIG_FILE"; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 加载依赖
|
||||
# =========================
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/scripts/get_cpu_arch.sh"
|
||||
# shellcheck disable=SC1091
|
||||
@ -46,40 +59,49 @@ source "$PROJECT_DIR/scripts/resolve_clash.sh"
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/scripts/service_lib.sh"
|
||||
|
||||
# =========================
|
||||
# 获取二进制
|
||||
# =========================
|
||||
CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")"
|
||||
|
||||
if [ ! -x "$CLASH_BIN" ]; then
|
||||
echo "[ERROR] clash binary not found or not executable: $CLASH_BIN" >&2
|
||||
echo "[ERROR] clash binary not executable: $CLASH_BIN" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
test_config() {
|
||||
local bin="$1"
|
||||
local config="$2"
|
||||
local runtime_dir="$3"
|
||||
"$bin" -d "$runtime_dir" -t -f "$config" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if ! test_config "$CLASH_BIN" "$CONFIG_FILE" "$RUNTIME_DIR"; then
|
||||
echo "[ERROR] config test failed: $CONFIG_FILE" >&2
|
||||
# =========================
|
||||
# config 测试(唯一一次)
|
||||
# =========================
|
||||
if ! "$CLASH_BIN" -t -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >/dev/null 2>&1; then
|
||||
echo "[ERROR] clash config test failed: $CONFIG_FILE" >&2
|
||||
write_run_state "failed" "config-test"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# systemd 模式
|
||||
# =========================
|
||||
# 前台模式(systemd)
|
||||
# =========================
|
||||
if [ "$FOREGROUND" = true ]; then
|
||||
write_run_state "running" "systemd"
|
||||
exec "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR"
|
||||
fi
|
||||
|
||||
# script / daemon 模式
|
||||
if [ "$DAEMON" = true ]; then
|
||||
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
|
||||
pid=$!
|
||||
echo "$pid" > "$PID_FILE"
|
||||
write_run_state "running" "script" "$pid"
|
||||
echo "[OK] Clash started in script mode, pid=$pid"
|
||||
# =========================
|
||||
# 后台模式(script)
|
||||
# =========================
|
||||
cleanup_dead_pid
|
||||
|
||||
if is_script_running; then
|
||||
pid="$(read_pid 2>/dev/null || true)"
|
||||
echo "[INFO] clash already running, pid=${pid:-unknown}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[ERROR] Must specify --foreground or --daemon" >&2
|
||||
exit 2
|
||||
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
|
||||
|
||||
pid=$!
|
||||
echo "$pid" > "$PID_FILE"
|
||||
|
||||
write_run_state "running" "script" "$pid"
|
||||
|
||||
echo "[OK] Clash started in script mode, pid=$pid"
|
||||
@ -9,6 +9,9 @@ SERVICE_NAME="clash-for-linux.service"
|
||||
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
|
||||
# =========================
|
||||
# 基础能力
|
||||
# =========================
|
||||
has_systemd() {
|
||||
command -v systemctl >/dev/null 2>&1
|
||||
}
|
||||
@ -19,17 +22,39 @@ service_unit_exists() {
|
||||
}
|
||||
|
||||
read_pid() {
|
||||
[ -f "$PID_FILE" ] || return 1
|
||||
cat "$PID_FILE"
|
||||
[ -s "$PID_FILE" ] || return 1
|
||||
tr -d '[:space:]' < "$PID_FILE"
|
||||
}
|
||||
|
||||
is_pid_running() {
|
||||
local pid="$1"
|
||||
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
is_script_running() {
|
||||
local pid
|
||||
pid="$(read_pid 2>/dev/null || true)"
|
||||
[ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null
|
||||
is_pid_running "$pid"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 清理僵尸 PID(关键)
|
||||
# =========================
|
||||
cleanup_dead_pid() {
|
||||
local pid
|
||||
pid="$(read_pid 2>/dev/null || true)"
|
||||
|
||||
if [ -n "${pid:-}" ] && ! is_pid_running "$pid"; then
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 模式检测(统一)
|
||||
# =========================
|
||||
detect_mode() {
|
||||
cleanup_dead_pid
|
||||
|
||||
if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
echo "systemd"
|
||||
elif is_script_running; then
|
||||
@ -41,59 +66,65 @@ detect_mode() {
|
||||
fi
|
||||
}
|
||||
|
||||
write_run_state() {
|
||||
local status="$1"
|
||||
local mode="${2:-unknown}"
|
||||
local pid="${3:-}"
|
||||
# =========================
|
||||
# state 写入(唯一实现)
|
||||
# =========================
|
||||
write_state_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
touch "$STATE_FILE"
|
||||
|
||||
if grep -q '^LAST_RUN_STATUS=' "$STATE_FILE" 2>/dev/null; then
|
||||
sed -i -E "s/^LAST_RUN_STATUS=.*/LAST_RUN_STATUS=${status}/" "$STATE_FILE"
|
||||
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_FILE"
|
||||
else
|
||||
echo "LAST_RUN_STATUS=${status}" >> "$STATE_FILE"
|
||||
fi
|
||||
|
||||
if grep -q '^LAST_RUN_MODE=' "$STATE_FILE" 2>/dev/null; then
|
||||
sed -i -E "s/^LAST_RUN_MODE=.*/LAST_RUN_MODE=${mode}/" "$STATE_FILE"
|
||||
else
|
||||
echo "LAST_RUN_MODE=${mode}" >> "$STATE_FILE"
|
||||
fi
|
||||
|
||||
if grep -q '^LAST_RUN_AT=' "$STATE_FILE" 2>/dev/null; then
|
||||
sed -i -E "s/^LAST_RUN_AT=.*/LAST_RUN_AT=$(date -Iseconds)/" "$STATE_FILE"
|
||||
else
|
||||
echo "LAST_RUN_AT=$(date -Iseconds)" >> "$STATE_FILE"
|
||||
fi
|
||||
|
||||
if [ -n "$pid" ]; then
|
||||
if grep -q '^LAST_RUN_PID=' "$STATE_FILE" 2>/dev/null; then
|
||||
sed -i -E "s/^LAST_RUN_PID=.*/LAST_RUN_PID=${pid}/" "$STATE_FILE"
|
||||
else
|
||||
echo "LAST_RUN_PID=${pid}" >> "$STATE_FILE"
|
||||
fi
|
||||
echo "${key}=${value}" >> "$STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
write_run_state() {
|
||||
local status="${1:-unknown}"
|
||||
local mode="${2:-unknown}"
|
||||
local pid="${3:-}"
|
||||
|
||||
write_state_kv "LAST_RUN_STATUS" "$status"
|
||||
write_state_kv "LAST_RUN_MODE" "$mode"
|
||||
write_state_kv "LAST_RUN_AT" "$(date -Iseconds)"
|
||||
|
||||
if [ -n "$pid" ]; then
|
||||
write_state_kv "LAST_RUN_PID" "$pid"
|
||||
fi
|
||||
}
|
||||
|
||||
# =========================
|
||||
# systemd 模式
|
||||
# =========================
|
||||
start_via_systemd() {
|
||||
systemctl start "$SERVICE_NAME"
|
||||
}
|
||||
|
||||
stop_via_systemd() {
|
||||
systemctl stop "$SERVICE_NAME"
|
||||
systemctl stop "$SERVICE_NAME" || true
|
||||
cleanup_dead_pid
|
||||
write_run_state "stopped" "systemd"
|
||||
rm -f "$PID_FILE"
|
||||
}
|
||||
|
||||
restart_via_systemd() {
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
}
|
||||
|
||||
# =========================
|
||||
# script 模式
|
||||
# =========================
|
||||
start_via_script() {
|
||||
cleanup_dead_pid
|
||||
|
||||
if is_script_running; then
|
||||
echo "[INFO] clash already running (script mode)"
|
||||
echo "[INFO] clash already running (script)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
"$PROJECT_DIR/scripts/run_clash.sh" --daemon
|
||||
}
|
||||
|
||||
@ -101,11 +132,19 @@ stop_via_script() {
|
||||
local pid
|
||||
pid="$(read_pid 2>/dev/null || true)"
|
||||
|
||||
if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
|
||||
echo "[INFO] stopping clash pid=$pid"
|
||||
kill "$pid"
|
||||
|
||||
kill "$pid" 2>/dev/null || true
|
||||
|
||||
for _ in 1 2 3 4 5; do
|
||||
if ! is_pid_running "$pid"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
done
|
||||
|
||||
if is_pid_running "$pid"; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -1,28 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/scripts/service_lib.sh"
|
||||
|
||||
mode="$(detect_mode)"
|
||||
|
||||
echo "=== Clash Status ==="
|
||||
echo "Project : $PROJECT_DIR"
|
||||
echo "Mode : $mode"
|
||||
|
||||
case "$mode" in
|
||||
systemd)
|
||||
echo "Running : yes (systemd)"
|
||||
;;
|
||||
script)
|
||||
echo "Running : yes (script)"
|
||||
;;
|
||||
systemd-installed)
|
||||
echo "Running : no (installed but not started)"
|
||||
;;
|
||||
*)
|
||||
echo "Running : no"
|
||||
;;
|
||||
esac
|
||||
exec "$PROJECT_DIR/clashctl" status "$@"
|
||||
@ -1,23 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/scripts/service_lib.sh"
|
||||
|
||||
mode="$(detect_mode)"
|
||||
|
||||
case "$mode" in
|
||||
systemd)
|
||||
stop_via_systemd
|
||||
echo "[OK] stopped via systemd"
|
||||
;;
|
||||
script)
|
||||
stop_via_script
|
||||
echo "[OK] stopped via script"
|
||||
;;
|
||||
*)
|
||||
echo "[WARN] nothing is running"
|
||||
;;
|
||||
esac
|
||||
exec "$PROJECT_DIR/clashctl" stop "$@"
|
||||
@ -1,19 +1,33 @@
|
||||
[Unit]
|
||||
Description=Clash for Linux
|
||||
After=network.target
|
||||
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
|
||||
EnvironmentFile=-/etc/default/clash-for-linux
|
||||
Environment=CLASH_HOME=/opt/clash-for-linux
|
||||
ExecStart=/bin/bash -c 'exec /bin/bash "${CLASH_HOME}/start.sh"'
|
||||
ExecStop=/bin/bash -c 'exec /bin/bash "${CLASH_HOME}/shutdown.sh"'
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=clash
|
||||
Group=clash
|
||||
PIDFile=%E{CLASH_HOME}/temp/clash.pid
|
||||
Environment=CLASH_ENV_FILE=%E{CLASH_HOME}/temp/clash-for-linux.sh
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/clash-for-linux
|
||||
Environment=HOME=/root
|
||||
|
||||
ExecStart=/bin/bash /opt/clash-for-linux/clashctl start
|
||||
ExecStop=/bin/bash /opt/clash-for-linux/clashctl --from-systemd stop
|
||||
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
KillMode=mixed
|
||||
TimeoutStartSec=120
|
||||
TimeoutStopSec=30
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
UMask=0022
|
||||
NoNewPrivileges=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -82,7 +82,7 @@ fi
|
||||
# 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"
|
||||
PID_FILE="$d/runtime/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
|
||||
|
||||