v1.19.21
17
.env
@ -20,6 +20,15 @@ export CLASH_URL=''
|
|||||||
# false = 禁用自动更新,直接使用本地已有 config.yaml
|
# false = 禁用自动更新,直接使用本地已有 config.yaml
|
||||||
export CLASH_AUTO_UPDATE="true"
|
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;如不需要可留空
|
# 常见机场需要 User-Agent;如不需要可留空
|
||||||
export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39'
|
export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39'
|
||||||
@ -42,6 +51,8 @@ CLASH_SHOW_SECRET=true
|
|||||||
# 是否显示脱敏 Secret(推荐)
|
# 是否显示脱敏 Secret(推荐)
|
||||||
CLASH_SHOW_SECRET_MASKED=true
|
CLASH_SHOW_SECRET_MASKED=true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# External Controller(Clash RESTful API)
|
# External Controller(Clash RESTful API)
|
||||||
# ⚠️ 安全建议:
|
# ⚠️ 安全建议:
|
||||||
# - 默认仅监听本机:127.0.0.1:9090 (推荐)
|
# - 默认仅监听本机:127.0.0.1:9090 (推荐)
|
||||||
@ -84,10 +95,10 @@ export CLASH_CONFIG_TEST_TIMEOUT="30"
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
# 可叠加多个 YAML 文件,后者覆盖前者(逗号分隔)
|
# 可叠加多个 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)
|
# 指定 Mixin 目录(默认:config/mixin.d)
|
||||||
# export CLASH_MIXIN_DIR='conf/mixin.d'
|
# export CLASH_MIXIN_DIR='config/mixin.d'
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# 6) Tun 模式(高级可选,需要 Clash Meta / Premium)
|
# 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
|
git clone --branch master --depth 1 https://github.com/wnlen/clash-for-linux.git
|
||||||
cd clash-for-linux
|
cd clash-for-linux
|
||||||
sudo bash install.sh
|
bash install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
安装脚本将自动完成:
|
安装脚本将自动完成:
|
||||||
@ -68,13 +68,13 @@ sudo bash install.sh
|
|||||||
编辑 `.env` 文件,设置订阅地址:
|
编辑 `.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 = 启动时检查订阅并重新下载/转换配置
|
true = 启动时检查订阅并重新下载/转换配置
|
||||||
false = 禁用自动更新,直接使用本地已有 config.yaml
|
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` 文件,设置公网访问(对外端口不用改,改了机器人也能扫到,密钥设置长点就行):
|
编辑 `.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 配置并重启
|
### 修改 Clash 配置并重启
|
||||||
|
|
||||||
```
|
```
|
||||||
vim conf/config.yaml
|
vim runtime/config.yaml
|
||||||
clashctl restart
|
clashctl restart
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -214,12 +214,12 @@ clashctl sub update personal
|
|||||||
|
|
||||||
用于追加或覆盖 Clash 配置。
|
用于追加或覆盖 Clash 配置。
|
||||||
|
|
||||||
- 默认读取:`conf/mixin.d/*.yaml`(按文件名排序)
|
- 默认读取:`config/mixin.d/*.yaml`(按文件名排序)
|
||||||
- 也可在 `.env` 中指定:
|
- 也可在 `.env` 中指定:
|
||||||
|
|
||||||
```
|
```
|
||||||
export CLASH_MIXIN_DIR='conf/mixin.d'
|
export CLASH_MIXIN_DIR='config/mixin.d'
|
||||||
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'
|
||||||
```
|
```
|
||||||
|
|
||||||
------
|
------
|
||||||
@ -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 转发
|
1. 开启 IP 转发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
|
echo "net.ipv4.ip_forward = 1" | tee -a /etc/sysctl.conf
|
||||||
sudo sysctl -p
|
sysctl -p
|
||||||
```
|
```
|
||||||
|
|
||||||
2.配置iptables
|
2.配置iptables
|
||||||
```bash
|
```bash
|
||||||
# 先清空旧规则
|
# 先清空旧规则
|
||||||
sudo iptables -t nat -F
|
iptables -t nat -F
|
||||||
|
|
||||||
# 允许本机访问代理端口
|
# 允许本机访问代理端口
|
||||||
sudo iptables -t nat -A OUTPUT -p tcp --dport 7890 -j RETURN
|
iptables -t nat -A OUTPUT -p tcp --dport 7890 -j RETURN
|
||||||
sudo iptables -t nat -A OUTPUT -p tcp --dport 7891 -j RETURN
|
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 7892 -j RETURN
|
||||||
|
|
||||||
# 让所有 TCP 流量通过 7892 代理
|
# 让所有 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 规则开机生效
|
3. 让 iptables 规则开机生效
|
||||||
@ -345,7 +345,7 @@ exit 0
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo chmod +x /etc/rc.local
|
chmod +x /etc/rc.local
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
506
clashctl
@ -65,44 +65,27 @@ usage() {
|
|||||||
Usage:
|
Usage:
|
||||||
clashctl COMMAND [OPTIONS]
|
clashctl COMMAND [OPTIONS]
|
||||||
|
|
||||||
Core Commands:
|
Commands:
|
||||||
on 开启当前终端代理
|
on 开启当前终端代理
|
||||||
off 关闭当前终端代理
|
off 关闭当前终端代理
|
||||||
start 启动 Clash
|
start 启动 Clash
|
||||||
stop 停止 Clash
|
stop 停止 Clash
|
||||||
restart 重新生成配置并重启
|
restart 重新生成配置并重启
|
||||||
status 查看当前状态
|
status 查看当前状态
|
||||||
update git pull + 生成配置 + 重启
|
|
||||||
generate 仅生成配置,不启动
|
generate 仅生成配置,不启动
|
||||||
mode 查看当前运行模式(systemd/script/none)
|
mode 查看当前运行模式(systemd/script/none)
|
||||||
|
|
||||||
Utility Commands:
|
|
||||||
ui 输出 Dashboard 地址
|
ui 输出 Dashboard 地址
|
||||||
secret 输出当前 secret
|
secret 输出当前 secret
|
||||||
sub show 查看订阅地址
|
|
||||||
sub update 重新生成配置并重启
|
|
||||||
tun status|on|off 查看/启用/关闭 Tun
|
|
||||||
mixin status|on|off 查看/启用/关闭 Mixin
|
|
||||||
doctor 健康检查
|
doctor 健康检查
|
||||||
logs [-f] [-n 100] 查看日志
|
logs [-f] [-n 100] 查看日志
|
||||||
|
update git pull + 重新生成配置并重启
|
||||||
|
sub show|update 查看订阅地址 / 重新生成配置并重启
|
||||||
|
tun status|on|off 查看/启用/关闭 Tun
|
||||||
|
mixin status|on|off 查看/启用/关闭 Mixin
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--from-systemd 内部使用,避免 stop 递归调用 systemctl
|
--from-systemd 内部使用,避免 stop 递归调用 systemctl
|
||||||
-h, --help 显示帮助信息
|
-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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +128,66 @@ read_state_value() {
|
|||||||
sed -nE "s/^${key}=(.*)$/\1/p" "$STATE_FILE" | head -n 1
|
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() {
|
cmd_on() {
|
||||||
require_profiled
|
require_profiled
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
@ -163,23 +206,29 @@ cmd_mode() {
|
|||||||
detect_mode
|
detect_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_start() {
|
cmd_generate() {
|
||||||
local mode
|
"$PROJECT_DIR/scripts/generate_config.sh"
|
||||||
mode="$(detect_mode)"
|
ok "Config generated"
|
||||||
|
}
|
||||||
|
|
||||||
case "$mode" in
|
cmd_start() {
|
||||||
systemd|systemd-installed)
|
local mode
|
||||||
start_via_systemd
|
mode="$(detect_mode)"
|
||||||
ok "Clash started via systemd"
|
|
||||||
;;
|
case "$mode" in
|
||||||
script|none)
|
systemd|systemd-installed)
|
||||||
start_via_script
|
start_via_systemd
|
||||||
;;
|
ok "Clash started via systemd"
|
||||||
*)
|
;;
|
||||||
err "未知模式: $mode"
|
script|none)
|
||||||
exit 1
|
start_via_script
|
||||||
;;
|
ok "Clash started via script mode"
|
||||||
esac
|
;;
|
||||||
|
*)
|
||||||
|
err "未知模式: $mode"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_stop() {
|
cmd_stop() {
|
||||||
@ -190,13 +239,15 @@ cmd_stop() {
|
|||||||
case "$mode" in
|
case "$mode" in
|
||||||
systemd)
|
systemd)
|
||||||
if [ "$from_systemd" = "true" ]; then
|
if [ "$from_systemd" = "true" ]; then
|
||||||
# 被 systemd ExecStop 调用时,不能再反向 systemctl stop 自己
|
|
||||||
ok "Stop requested from systemd, skip recursive systemctl stop"
|
ok "Stop requested from systemd, skip recursive systemctl stop"
|
||||||
exit 0
|
return 0
|
||||||
fi
|
fi
|
||||||
stop_via_systemd
|
stop_via_systemd
|
||||||
ok "Clash stopped via systemd"
|
ok "Clash stopped via systemd"
|
||||||
;;
|
;;
|
||||||
|
systemd-installed)
|
||||||
|
info "systemd service installed but not running"
|
||||||
|
;;
|
||||||
script)
|
script)
|
||||||
stop_via_script
|
stop_via_script
|
||||||
ok "Clash stopped via script mode"
|
ok "Clash stopped via script mode"
|
||||||
@ -211,51 +262,142 @@ cmd_stop() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_generate() {
|
|
||||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
|
||||||
ok "Config generated"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_restart() {
|
cmd_restart() {
|
||||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
cmd_generate
|
||||||
|
cmd_stop "${1:-false}" || true
|
||||||
local mode
|
cmd_start
|
||||||
mode="$(detect_mode)"
|
|
||||||
|
|
||||||
case "$mode" in
|
|
||||||
systemd|systemd-installed)
|
|
||||||
restart_via_systemd
|
|
||||||
ok "Clash restarted via systemd"
|
|
||||||
;;
|
|
||||||
script|none)
|
|
||||||
restart_via_script
|
|
||||||
ok "Clash restarted via script mode"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
err "未知模式: $mode"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_update() {
|
cmd_update() {
|
||||||
git -C "$PROJECT_DIR" pull
|
git -C "$PROJECT_DIR" pull
|
||||||
"$PROJECT_DIR/scripts/generate_config.sh"
|
cmd_restart
|
||||||
|
ok "Project updated"
|
||||||
|
}
|
||||||
|
|
||||||
local mode
|
cmd_ui() {
|
||||||
mode="$(detect_mode)"
|
local raw="${1:-}"
|
||||||
|
local controller host port secret base_url
|
||||||
|
|
||||||
case "$mode" in
|
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||||
systemd)
|
[ -n "${controller:-}" ] || controller="127.0.0.1:9090"
|
||||||
restart_via_systemd
|
|
||||||
ok "Project updated and restarted via systemd"
|
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)
|
esac
|
||||||
restart_via_script
|
|
||||||
ok "Project updated and restarted via script mode"
|
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_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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@ -387,23 +529,6 @@ cmd_status() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Secret : $secret_exists"
|
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() {
|
doctor_ok() {
|
||||||
@ -418,182 +543,6 @@ doctor_err() {
|
|||||||
printf "\033[31m[ERROR]\033[0m %s\n" "$*"
|
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() {
|
cmd_doctor() {
|
||||||
local mode running="no"
|
local mode running="no"
|
||||||
local failed=0
|
local failed=0
|
||||||
@ -612,7 +561,6 @@ cmd_doctor() {
|
|||||||
echo "Config : $RUNTIME_CONFIG"
|
echo "Config : $RUNTIME_CONFIG"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# 1. 检查运行配置是否存在
|
|
||||||
if [ -s "$RUNTIME_CONFIG" ]; then
|
if [ -s "$RUNTIME_CONFIG" ]; then
|
||||||
doctor_ok "runtime config exists: $RUNTIME_CONFIG"
|
doctor_ok "runtime config exists: $RUNTIME_CONFIG"
|
||||||
else
|
else
|
||||||
@ -620,7 +568,6 @@ cmd_doctor() {
|
|||||||
failed=1
|
failed=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. 检查配置里是否还有未渲染占位符
|
|
||||||
if [ -f "$RUNTIME_CONFIG" ]; then
|
if [ -f "$RUNTIME_CONFIG" ]; then
|
||||||
if grep -q '\${' "$RUNTIME_CONFIG"; then
|
if grep -q '\${' "$RUNTIME_CONFIG"; then
|
||||||
doctor_err "runtime config contains unresolved placeholders"
|
doctor_err "runtime config contains unresolved placeholders"
|
||||||
@ -630,7 +577,6 @@ cmd_doctor() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. 检查 state.env
|
|
||||||
if [ -f "$STATE_FILE" ]; then
|
if [ -f "$STATE_FILE" ]; then
|
||||||
doctor_ok "state file exists: $STATE_FILE"
|
doctor_ok "state file exists: $STATE_FILE"
|
||||||
|
|
||||||
@ -658,7 +604,6 @@ cmd_doctor() {
|
|||||||
warned=1
|
warned=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. 检查运行模式 / 进程状态
|
|
||||||
case "$mode" in
|
case "$mode" in
|
||||||
systemd)
|
systemd)
|
||||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
@ -692,7 +637,6 @@ cmd_doctor() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 5. 检查 dashboard 地址
|
|
||||||
dashboard_url="$(cmd_ui --raw 2>/dev/null || true)"
|
dashboard_url="$(cmd_ui --raw 2>/dev/null || true)"
|
||||||
controller="$(read_runtime_config_value "external-controller" || true)"
|
controller="$(read_runtime_config_value "external-controller" || true)"
|
||||||
dashboard_port="$(port_from_controller)"
|
dashboard_port="$(port_from_controller)"
|
||||||
@ -711,7 +655,6 @@ cmd_doctor() {
|
|||||||
warned=1
|
warned=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6. 检查 HTTP 代理端口
|
|
||||||
http_port="$(http_port_from_config)"
|
http_port="$(http_port_from_config)"
|
||||||
if check_port_listening "$http_port"; then
|
if check_port_listening "$http_port"; then
|
||||||
doctor_ok "proxy port is listening: $http_port"
|
doctor_ok "proxy port is listening: $http_port"
|
||||||
@ -731,7 +674,6 @@ cmd_doctor() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 7. 检查 dashboard 端口
|
|
||||||
if check_port_listening "$dashboard_port"; then
|
if check_port_listening "$dashboard_port"; then
|
||||||
doctor_ok "dashboard port is listening: $dashboard_port"
|
doctor_ok "dashboard port is listening: $dashboard_port"
|
||||||
else
|
else
|
||||||
@ -750,7 +692,6 @@ cmd_doctor() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 8. 检查 dashboard HTTP 可访问性
|
|
||||||
if [ -n "${dashboard_url:-}" ]; then
|
if [ -n "${dashboard_url:-}" ]; then
|
||||||
if check_dashboard_http "$dashboard_url"; then
|
if check_dashboard_http "$dashboard_url"; then
|
||||||
doctor_ok "dashboard http reachable"
|
doctor_ok "dashboard http reachable"
|
||||||
@ -766,7 +707,6 @@ cmd_doctor() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 9. 检查 secret
|
|
||||||
secret="$(read_runtime_config_value "secret" || true)"
|
secret="$(read_runtime_config_value "secret" || true)"
|
||||||
if [ -n "${secret:-}" ]; then
|
if [ -n "${secret:-}" ]; then
|
||||||
doctor_ok "secret exists in runtime config"
|
doctor_ok "secret exists in runtime config"
|
||||||
@ -775,7 +715,6 @@ cmd_doctor() {
|
|||||||
warned=1
|
warned=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 10. 检查 clashctl 安装位置(可选)
|
|
||||||
if command_exists clashctl; then
|
if command_exists clashctl; then
|
||||||
doctor_ok "clashctl command available: $(command -v clashctl)"
|
doctor_ok "clashctl command available: $(command -v clashctl)"
|
||||||
else
|
else
|
||||||
@ -783,7 +722,6 @@ cmd_doctor() {
|
|||||||
warned=1
|
warned=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 11. 检查代理快捷函数文件
|
|
||||||
if [ -f "$PROFILED_FILE" ]; then
|
if [ -f "$PROFILED_FILE" ]; then
|
||||||
doctor_ok "shell proxy helper exists: $PROFILED_FILE"
|
doctor_ok "shell proxy helper exists: $PROFILED_FILE"
|
||||||
else
|
else
|
||||||
@ -807,13 +745,13 @@ cmd_doctor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_logs() {
|
cmd_logs() {
|
||||||
|
shift || true
|
||||||
|
|
||||||
local follow="false"
|
local follow="false"
|
||||||
local lines="50"
|
local lines="50"
|
||||||
local mode
|
local mode
|
||||||
local arg
|
local arg
|
||||||
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
arg="$1"
|
arg="$1"
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
@ -893,7 +831,7 @@ main() {
|
|||||||
cmd_stop "$from_systemd"
|
cmd_stop "$from_systemd"
|
||||||
;;
|
;;
|
||||||
restart)
|
restart)
|
||||||
cmd_restart
|
cmd_restart "$from_systemd"
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
cmd_status
|
cmd_status
|
||||||
@ -926,10 +864,10 @@ main() {
|
|||||||
shift || true
|
shift || true
|
||||||
cmd_mixin "${1:-status}"
|
cmd_mixin "${1:-status}"
|
||||||
;;
|
;;
|
||||||
doctor)
|
doctor)
|
||||||
cmd_doctor
|
cmd_doctor
|
||||||
;;
|
;;
|
||||||
logs)
|
logs)
|
||||||
cmd_logs "$@"
|
cmd_logs "$@"
|
||||||
;;
|
;;
|
||||||
""|-h|--help)
|
""|-h|--help)
|
||||||
|
|||||||
@ -5,5 +5,5 @@
|
|||||||
如需手动指定顺序或使用自定义路径,请在 `.env` 中设置:
|
如需手动指定顺序或使用自定义路径,请在 `.env` 中设置:
|
||||||
|
|
||||||
```bash
|
```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
|
### Default Behavior
|
||||||
|
|
||||||
- 默认读取目录:`conf/mixin.d/`
|
- 默认读取目录:`config/mixin.d/`
|
||||||
- 按文件名排序后依次合并
|
- 按文件名排序后依次合并
|
||||||
- 后加载的文件会覆盖前面的配置
|
- 后加载的文件会覆盖前面的配置
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# conf/mixin.d/10-rules.yaml
|
# config/mixin.d/10-rules.yaml
|
||||||
rules:
|
rules:
|
||||||
- DOMAIN-SUFFIX,example.com,DIRECT
|
- DOMAIN-SUFFIX,example.com,DIRECT
|
||||||
```
|
```
|
||||||
@ -170,7 +170,7 @@ clash-for-linux 以 **安全默认配置** 为原则:
|
|||||||
|
|
||||||
### Service Keeps Restarting
|
### Service Keeps Restarting
|
||||||
|
|
||||||
- 检查 `conf/config.yaml` 是否存在语法错误
|
- 检查 `runtime/config.yaml` 是否存在语法错误
|
||||||
- 查看 systemd 日志:
|
- 查看 systemd 日志:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@ -87,15 +87,14 @@ CLASH_START_SERVICE=true
|
|||||||
|
|
||||||
### `CLASH_AUTO_DOWNLOAD`
|
### `CLASH_AUTO_DOWNLOAD`
|
||||||
|
|
||||||
```
|
```env
|
||||||
CLASH_AUTO_DOWNLOAD=auto
|
CLASH_AUTO_DOWNLOAD=auto
|
||||||
```
|
|
||||||
|
|
||||||
- 是否自动下载 Clash 内核
|
- 是否在本地未检测到可用内核时自动下载 Mihomo 内核
|
||||||
- 可选值:
|
- 可选值:
|
||||||
- `auto`(默认):检测不到内核时自动下载
|
- `auto`(默认):当未检测到可用内核时自动下载(已有内核则不覆盖)
|
||||||
- `true`:强制重新下载
|
- `false`:不进行任何自动下载,仅使用本地已有内核(找不到则报错)
|
||||||
- `false`:关闭自动下载
|
- `true`:强制重新下载内核(即使本地已有也会覆盖)
|
||||||
|
|
||||||
适用于:
|
适用于:
|
||||||
|
|
||||||
@ -107,12 +106,13 @@ CLASH_AUTO_DOWNLOAD=auto
|
|||||||
|
|
||||||
### `CLASH_DOWNLOAD_URL_TEMPLATE`
|
### `CLASH_DOWNLOAD_URL_TEMPLATE`
|
||||||
|
|
||||||
```
|
```env
|
||||||
CLASH_DOWNLOAD_URL_TEMPLATE=https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz
|
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:
|
rules:
|
||||||
- DOMAIN-SUFFIX,example.com,DIRECT
|
- DOMAIN-SUFFIX,example.com,DIRECT
|
||||||
```
|
```
|
||||||
|
|||||||
635
install.sh
@ -1,113 +1,33 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# =========================
|
Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
# 基础参数
|
Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}"
|
||||||
# =========================
|
|
||||||
Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}"
|
|
||||||
Service_Name="clash-for-linux"
|
Service_Name="clash-for-linux"
|
||||||
Service_User="root"
|
Service_User="root"
|
||||||
Service_Group="root"
|
Service_Group="root"
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# 彩色输出(统一 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
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
err "需要 root 权限执行安装脚本(请使用 bash install.sh)"
|
echo "[ERROR] root required" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "${Server_Dir}/.env" ]; then
|
if [ ! -f "${Server_Dir}/.env" ]; then
|
||||||
err "未找到 .env 文件,请确认脚本所在目录:${Server_Dir}"
|
echo "[ERROR] .env not found in ${Server_Dir}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# 同步到安装目录(保持你原逻辑)
|
# 同步文件
|
||||||
# =========================
|
# =========================
|
||||||
mkdir -p "$Install_Dir"
|
mkdir -p "$Install_Dir"
|
||||||
|
|
||||||
if [ "$Server_Dir" != "$Install_Dir" ]; then
|
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
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/"
|
rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/"
|
||||||
else
|
else
|
||||||
@ -115,253 +35,80 @@ if [ "$Server_Dir" != "$Install_Dir" ]; then
|
|||||||
fi
|
fi
|
||||||
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"/scripts/* 2>/dev/null || true
|
||||||
chmod +x "$Install_Dir"/bin/* 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
|
# shellcheck disable=SC1090
|
||||||
source "$Install_Dir/.env"
|
source "$Install_Dir/.env"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$Install_Dir/scripts/get_cpu_arch.sh"
|
source "$Install_Dir/scripts/get_cpu_arch.sh"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$Install_Dir/scripts/resolve_clash.sh"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# .env 写入工具:write_env_kv(必须在 prompt 之前定义)
|
# 安装 clashctl
|
||||||
# - 自动创建文件
|
|
||||||
# - 存在则替换,不存在则追加
|
|
||||||
# - 统一写成:export KEY="VALUE"
|
|
||||||
# - 自动转义双引号/反斜杠
|
|
||||||
# =========================
|
# =========================
|
||||||
escape_env_value() {
|
install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl
|
||||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
write_env_kv() {
|
|
||||||
local file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local val="$3"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$file")" 2>/dev/null || true
|
|
||||||
[ -f "$file" ] || touch "$file"
|
|
||||||
|
|
||||||
val="$(printf '%s' "$val" | tr -d '\r')"
|
|
||||||
local esc
|
|
||||||
esc="$(escape_env_value "$val")"
|
|
||||||
|
|
||||||
if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file"; then
|
|
||||||
sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*|export ${key}=\"${esc}\"|g" "$file"
|
|
||||||
else
|
|
||||||
printf 'export %s="%s"\n' "$key" "$esc" >> "$file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# 交互式填写订阅地址(仅在 CLASH_URL 为空时触发)
|
# 安装 proxy helper
|
||||||
# - 若非 TTY(CI/管道)则跳过交互
|
|
||||||
# - 若用户回车跳过,则保持原行为:装完提示手动配置
|
|
||||||
# =========================
|
# =========================
|
||||||
prompt_clash_url_if_empty() {
|
cat >/etc/profile.d/clash-for-linux.sh <<EOF
|
||||||
# 兼容 .env 里可能是 CLASH_URL= / export CLASH_URL= / 带引号
|
proxy_on() {
|
||||||
local cur="${CLASH_URL:-}"
|
local port="\${1:-7890}"
|
||||||
cur="${cur%\"}"; cur="${cur#\"}"
|
export http_proxy="http://127.0.0.1:\${port}"
|
||||||
|
export https_proxy="\$http_proxy"
|
||||||
if [ -n "$cur" ]; then
|
export HTTP_PROXY="\$http_proxy"
|
||||||
return 0
|
export HTTPS_PROXY="\$http_proxy"
|
||||||
fi
|
export no_proxy="127.0.0.1,localhost"
|
||||||
|
export NO_PROXY="\$no_proxy"
|
||||||
# 非交互环境:不阻塞
|
echo "[OK] Proxy enabled: \$http_proxy"
|
||||||
if [ ! -t 0 ]; then
|
|
||||||
warn "CLASH_URL 为空且当前为非交互环境(stdin 非 TTY),将跳过输入引导。"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
warn "未检测到订阅地址(CLASH_URL 为空)"
|
|
||||||
echo "请粘贴你的 Clash 订阅地址(直接回车跳过,稍后手动编辑 .env):"
|
|
||||||
read -r -p "Clash URL: " input_url
|
|
||||||
|
|
||||||
input_url="$(printf '%s' "$input_url" | tr -d '\r')"
|
|
||||||
|
|
||||||
# 回车跳过:保持原行为(不写入)
|
|
||||||
if [ -z "$input_url" ]; then
|
|
||||||
warn "已跳过填写订阅地址,安装完成后请手动编辑:${Install_Dir}/.env"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 先校验再写入,避免污染 .env
|
|
||||||
if ! echo "$input_url" | grep -Eq '^https?://'; then
|
|
||||||
err "订阅地址格式不正确(必须以 http:// 或 https:// 开头)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ENV_FILE="${Install_Dir}/.env"
|
|
||||||
mkdir -p "$Install_Dir"
|
|
||||||
[ -f "$ENV_FILE" ] || touch "$ENV_FILE"
|
|
||||||
|
|
||||||
# ✅ 只用这一套写入逻辑(统一 export KEY="...",兼容旧格式)
|
|
||||||
write_env_kv "$ENV_FILE" "CLASH_URL" "$input_url"
|
|
||||||
|
|
||||||
export CLASH_URL="$input_url"
|
|
||||||
ok "已写入订阅地址到:${ENV_FILE}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_clash_url_if_empty
|
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
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# 端口冲突检测(保持你原逻辑)
|
# 安装 systemd
|
||||||
# =========================
|
|
||||||
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 安装与启动
|
|
||||||
# =========================
|
# =========================
|
||||||
Service_Enabled="unknown"
|
Service_Enabled="unknown"
|
||||||
Service_Started="unknown"
|
Service_Started="unknown"
|
||||||
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
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
|
if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then
|
||||||
systemctl start "${Service_Name}.service" || true
|
systemctl enable "${Service_Name}.service" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then
|
if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then
|
||||||
systemctl start "${Service_Name}.service" || true
|
systemctl start "${Service_Name}.service" || true
|
||||||
fi
|
fi
|
||||||
@ -378,270 +125,26 @@ if command -v systemctl >/dev/null 2>&1; then
|
|||||||
Service_Started="inactive"
|
Service_Started="inactive"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "未检测到 systemd,已跳过服务单元生成"
|
echo "[WARN] systemd not found, will use script mode"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Shell 代理快捷命令
|
# 输出(全部收敛到 clashctl)
|
||||||
# 生成:/etc/profile.d/clash-for-linux.sh
|
|
||||||
# =========================
|
# =========================
|
||||||
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() {
|
echo
|
||||||
local install_dir="$Install_Dir"
|
echo "Next:"
|
||||||
|
echo " clashctl generate"
|
||||||
|
echo " clashctl start"
|
||||||
|
echo " clashctl doctor"
|
||||||
|
|
||||||
cat >"$PROFILED_FILE" <<EOF
|
echo
|
||||||
# Clash for Linux proxy helpers
|
echo "Commands:"
|
||||||
# Auto-generated by clash-for-linux installer.
|
echo " clashctl status"
|
||||||
|
echo " clashctl logs"
|
||||||
# ===== 自动加载 .env =====
|
echo " clashctl restart"
|
||||||
CLASH_INSTALL_DIR="${install_dir}"
|
echo " clashctl stop"
|
||||||
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
|
|
||||||
@ -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() {
|
trim_value() {
|
||||||
local value="$1"
|
local value="$1"
|
||||||
echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
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() {
|
apply_tun_config() {
|
||||||
local config_path="$1"
|
local config_path="$1"
|
||||||
|
|
||||||
local enable="${CLASH_TUN_ENABLE:-false}"
|
local enable="${CLASH_TUN_ENABLE:-false}"
|
||||||
if [ "$enable" != "true" ]; then
|
[ "$enable" = "true" ] || return 0
|
||||||
return 0
|
|
||||||
fi
|
remove_block_if_exists "$config_path" "# ==== TUN CONFIG START ===="
|
||||||
|
|
||||||
local stack="${CLASH_TUN_STACK:-system}"
|
local stack="${CLASH_TUN_STACK:-system}"
|
||||||
local auto_route="${CLASH_TUN_AUTO_ROUTE:-true}"
|
local auto_route="${CLASH_TUN_AUTO_ROUTE:-true}"
|
||||||
@ -22,65 +42,91 @@ apply_tun_config() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "# ==== TUN CONFIG START ===="
|
||||||
echo "tun:"
|
echo "tun:"
|
||||||
echo " enable: true"
|
echo " enable: true"
|
||||||
echo " stack: ${stack}"
|
echo " stack: ${stack}"
|
||||||
echo " auto-route: ${auto_route}"
|
echo " auto-route: ${auto_route}"
|
||||||
echo " auto-redirect: ${auto_redirect}"
|
echo " auto-redirect: ${auto_redirect}"
|
||||||
echo " strict-route: ${strict_route}"
|
echo " strict-route: ${strict_route}"
|
||||||
if [ -n "$device" ]; then
|
|
||||||
echo " device: ${device}"
|
[ -n "$device" ] && echo " device: ${device}"
|
||||||
fi
|
[ -n "$mtu" ] && echo " mtu: ${mtu}"
|
||||||
if [ -n "$mtu" ]; then
|
|
||||||
echo " mtu: ${mtu}"
|
|
||||||
fi
|
|
||||||
if [ -n "$dns_hijack" ]; then
|
if [ -n "$dns_hijack" ]; then
|
||||||
echo " dns-hijack:"
|
echo " dns-hijack:"
|
||||||
IFS=',' read -r -a hijacks <<< "$dns_hijack"
|
IFS=',' read -r -a hijacks <<< "$dns_hijack"
|
||||||
for item in "${hijacks[@]}"; do
|
for item in "${hijacks[@]}"; do
|
||||||
local trimmed
|
item="$(trim_value "$item")"
|
||||||
trimmed=$(trim_value "$item")
|
[ -n "$item" ] && echo " - ${item}"
|
||||||
if [ -n "$trimmed" ]; then
|
|
||||||
echo " - ${trimmed}"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "# ==== TUN CONFIG END ===="
|
||||||
} >> "$config_path"
|
} >> "$config_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# MIXIN 配置
|
||||||
|
# =========================
|
||||||
apply_mixin_config() {
|
apply_mixin_config() {
|
||||||
local config_path="$1"
|
local config_path="$1"
|
||||||
local base_dir="${2:-$Server_Dir}"
|
local base_dir="$2"
|
||||||
local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/conf/mixin.d}"
|
|
||||||
|
local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/config/mixin.d}"
|
||||||
local mixin_paths=()
|
local mixin_paths=()
|
||||||
|
|
||||||
|
remove_block_if_exists "$config_path" "# ==== MIXIN CONFIG START ===="
|
||||||
|
|
||||||
|
# 用户手动指定优先
|
||||||
if [ -n "${CLASH_MIXIN_PATHS:-}" ]; then
|
if [ -n "${CLASH_MIXIN_PATHS:-}" ]; then
|
||||||
IFS=',' read -r -a mixin_paths <<< "$CLASH_MIXIN_PATHS"
|
IFS=',' read -r -a mixin_paths <<< "$CLASH_MIXIN_PATHS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 自动扫描目录(补充)
|
||||||
if [ -d "$mixin_dir" ]; then
|
if [ -d "$mixin_dir" ]; then
|
||||||
while IFS= read -r -d '' file; do
|
while IFS= read -r -d '' file; do
|
||||||
mixin_paths+=("$file")
|
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
|
fi
|
||||||
|
|
||||||
|
# 去重
|
||||||
|
local uniq_paths=()
|
||||||
|
local seen=""
|
||||||
|
|
||||||
for path in "${mixin_paths[@]}"; do
|
for path in "${mixin_paths[@]}"; do
|
||||||
local trimmed
|
path="$(trim_value "$path")"
|
||||||
trimmed=$(trim_value "$path")
|
[ -z "$path" ] && continue
|
||||||
if [ -z "$trimmed" ]; then
|
|
||||||
continue
|
# 相对路径转绝对
|
||||||
|
if [ "${path:0:1}" != "/" ]; then
|
||||||
|
path="$base_dir/$path"
|
||||||
fi
|
fi
|
||||||
if [ "${trimmed:0:1}" != "/" ]; then
|
|
||||||
trimmed="$base_dir/$trimmed"
|
if [[ "$seen" != *"|$path|"* ]]; then
|
||||||
fi
|
uniq_paths+=("$path")
|
||||||
if [ -f "$trimmed" ]; then
|
seen="${seen}|$path|"
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "# ---- mixin: ${trimmed} ----"
|
|
||||||
cat "$trimmed"
|
|
||||||
} >> "$config_path"
|
|
||||||
else
|
|
||||||
echo "[WARN] Mixin file not found: $trimmed" >&2
|
|
||||||
fi
|
fi
|
||||||
done
|
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
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
RUNTIME_DIR="$PROJECT_DIR/runtime"
|
|
||||||
|
|
||||||
echo "=== Clash Doctor ==="
|
exec "$PROJECT_DIR/clashctl" 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
|
|
||||||
@ -12,6 +12,7 @@ STATE_FILE="$RUNTIME_DIR/state.env"
|
|||||||
TMP_DOWNLOAD="$RUNTIME_DIR/subscription.raw.yaml"
|
TMP_DOWNLOAD="$RUNTIME_DIR/subscription.raw.yaml"
|
||||||
TMP_NORMALIZED="$RUNTIME_DIR/subscription.normalized.yaml"
|
TMP_NORMALIZED="$RUNTIME_DIR/subscription.normalized.yaml"
|
||||||
TMP_PROXY_FRAGMENT="$RUNTIME_DIR/proxy.fragment.yaml"
|
TMP_PROXY_FRAGMENT="$RUNTIME_DIR/proxy.fragment.yaml"
|
||||||
|
TMP_CONFIG="$RUNTIME_DIR/config.yaml.tmp"
|
||||||
|
|
||||||
mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR"
|
mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR"
|
||||||
|
|
||||||
@ -64,6 +65,15 @@ generate_secret() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
openssl rand -hex 16
|
openssl rand -hex 16
|
||||||
else
|
else
|
||||||
@ -103,9 +113,11 @@ apply_controller_to_config() {
|
|||||||
|
|
||||||
rm -rf "$ui_dir"
|
rm -rf "$ui_dir"
|
||||||
mkdir -p "$ui_dir"
|
mkdir -p "$ui_dir"
|
||||||
cp -a "$PROJECT_DIR/dashboard/public/." "$ui_dir/"
|
|
||||||
|
|
||||||
upsert_yaml_kv_local "$file" "external-ui" "$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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,11 +133,32 @@ download_subscription() {
|
|||||||
|
|
||||||
is_complete_clash_config() {
|
is_complete_clash_config() {
|
||||||
local file="$1"
|
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() {
|
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() {
|
main() {
|
||||||
@ -156,9 +189,10 @@ main() {
|
|||||||
cp -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED"
|
cp -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED"
|
||||||
|
|
||||||
if is_complete_clash_config "$TMP_NORMALIZED"; then
|
if is_complete_clash_config "$TMP_NORMALIZED"; then
|
||||||
cp -f "$TMP_NORMALIZED" "$RUNTIME_CONFIG"
|
cp -f "$TMP_NORMALIZED" "$TMP_CONFIG"
|
||||||
apply_controller_to_config "$RUNTIME_CONFIG"
|
apply_controller_to_config "$TMP_CONFIG"
|
||||||
apply_secret_to_config "$RUNTIME_CONFIG"
|
apply_secret_to_config "$TMP_CONFIG"
|
||||||
|
finalize_config "$TMP_CONFIG"
|
||||||
write_state "success" "subscription_full" "subscription_full"
|
write_state "success" "subscription_full" "subscription_full"
|
||||||
cleanup_tmp_files
|
cleanup_tmp_files
|
||||||
exit 0
|
exit 0
|
||||||
@ -171,22 +205,14 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT"
|
build_fragment_config "$template_file" "$TMP_CONFIG"
|
||||||
|
apply_controller_to_config "$TMP_CONFIG"
|
||||||
cat "$template_file" > "$RUNTIME_CONFIG"
|
apply_secret_to_config "$TMP_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"
|
|
||||||
|
|
||||||
|
finalize_config "$TMP_CONFIG"
|
||||||
write_state "success" "subscription_fragment_merged" "subscription_fragment"
|
write_state "success" "subscription_fragment_merged" "subscription_fragment"
|
||||||
cleanup_tmp_files
|
cleanup_tmp_files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trap cleanup_tmp_files EXIT
|
||||||
main "$@"
|
main "$@"
|
||||||
@ -10,14 +10,14 @@ SERVICE_GROUP="${CLASH_SERVICE_GROUP:-root}"
|
|||||||
|
|
||||||
RUNTIME_DIR="$PROJECT_DIR/runtime"
|
RUNTIME_DIR="$PROJECT_DIR/runtime"
|
||||||
LOG_DIR="$PROJECT_DIR/logs"
|
LOG_DIR="$PROJECT_DIR/logs"
|
||||||
CONF_DIR="$PROJECT_DIR/conf"
|
CONFIG_DIR="$PROJECT_DIR/config"
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
echo "[ERROR] root required to install systemd unit" >&2
|
echo "[ERROR] root required to install systemd unit" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
cat >"$UNIT_PATH" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
@ -35,8 +35,11 @@ Group=${SERVICE_GROUP}
|
|||||||
WorkingDirectory=${PROJECT_DIR}
|
WorkingDirectory=${PROJECT_DIR}
|
||||||
Environment=HOME=/root
|
Environment=HOME=/root
|
||||||
|
|
||||||
ExecStart=/bin/bash ${PROJECT_DIR}/scripts/run_clash.sh --foreground
|
ExecStart=${PROJECT_DIR}/clashctl start
|
||||||
ExecStop=/bin/bash ${PROJECT_DIR}/clashctl --from-systemd stop
|
ExecStop=${PROJECT_DIR}/clashctl --from-systemd stop
|
||||||
|
ExecReload=${PROJECT_DIR}/clashctl restart
|
||||||
|
|
||||||
|
PIDFile=${PROJECT_DIR}/runtime/clash.pid
|
||||||
|
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
@ -62,4 +65,5 @@ echo "[OK] systemd unit installed: ${UNIT_PATH}"
|
|||||||
echo "start : systemctl start ${SERVICE_NAME}.service"
|
echo "start : systemctl start ${SERVICE_NAME}.service"
|
||||||
echo "stop : systemctl stop ${SERVICE_NAME}.service"
|
echo "stop : systemctl stop ${SERVICE_NAME}.service"
|
||||||
echo "restart : systemctl restart ${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"
|
echo "status : systemctl status ${SERVICE_NAME}.service -l --no-pager"
|
||||||
@ -1,20 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
LOG_FILE="$PROJECT_DIR/logs/clash.log"
|
|
||||||
SERVICE_NAME="clash-for-linux.service"
|
|
||||||
|
|
||||||
if [ "${1:-}" = "-f" ]; then
|
exec "$PROJECT_DIR/clashctl" logs "$@"
|
||||||
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
|
|
||||||
@ -1,36 +1,49 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
PORT_CHECK_WARNED=${PORT_CHECK_WARNED:-0}
|
PORT_CHECK_WARNED=${PORT_CHECK_WARNED:-0}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 判断端口是否被占用(更稳)
|
||||||
|
# =========================
|
||||||
is_port_in_use() {
|
is_port_in_use() {
|
||||||
local port="$1"
|
local port="$1"
|
||||||
|
|
||||||
if command -v ss >/dev/null 2>&1; then
|
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 $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v netstat >/dev/null 2>&1; then
|
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 $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v lsof >/dev/null 2>&1; then
|
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 $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$PORT_CHECK_WARNED" -eq 0 ]; then
|
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
|
PORT_CHECK_WARNED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 找可用端口(优化版)
|
||||||
|
# =========================
|
||||||
find_available_port() {
|
find_available_port() {
|
||||||
local start_port=${1:-20000}
|
local start="${1:-20000}"
|
||||||
local end_port=${2:-65000}
|
local end="${2:-65000}"
|
||||||
local port
|
local port
|
||||||
|
|
||||||
|
# 优先随机尝试
|
||||||
if command -v shuf >/dev/null 2>&1; then
|
if command -v shuf >/dev/null 2>&1; then
|
||||||
for _ in {1..50}; do
|
for _ in {1..30}; do
|
||||||
port=$(shuf -i "${start_port}-${end_port}" -n 1)
|
port=$(shuf -i "${start}-${end}" -n 1)
|
||||||
if ! is_port_in_use "$port"; then
|
if ! is_port_in_use "$port"; then
|
||||||
echo "$port"
|
echo "$port"
|
||||||
return 0
|
return 0
|
||||||
@ -38,7 +51,8 @@ find_available_port() {
|
|||||||
done
|
done
|
||||||
fi
|
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
|
if ! is_port_in_use "$port"; then
|
||||||
echo "$port"
|
echo "$port"
|
||||||
return 0
|
return 0
|
||||||
@ -48,43 +62,56 @@ find_available_port() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 解析端口值(核心函数)
|
||||||
|
# =========================
|
||||||
resolve_port_value() {
|
resolve_port_value() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local value="$2"
|
local value="$2"
|
||||||
local resolved
|
local resolved
|
||||||
|
|
||||||
|
# auto / 空
|
||||||
if [ -z "$value" ] || [ "$value" = "auto" ]; then
|
if [ -z "$value" ] || [ "$value" = "auto" ]; then
|
||||||
resolved=$(find_available_port)
|
resolved=$(find_available_port) || {
|
||||||
if [ -z "$resolved" ]; then
|
echo "[ERROR] ${name} failed to allocate port" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
}
|
||||||
echo -e "\033[33m[WARN] ${name} 端口已自动分配为 ${resolved}\033[0m" >&2
|
echo "[WARN] ${name} auto assigned: ${resolved}" >&2
|
||||||
echo "$resolved"
|
echo "$resolved"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
# 非数字
|
||||||
if is_port_in_use "$value"; then
|
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||||
resolved=$(find_available_port)
|
echo "[ERROR] invalid port: $value" >&2
|
||||||
if [ -n "$resolved" ]; then
|
return 1
|
||||||
echo -e "\033[33m[WARN] ${name} 端口 ${value} 已被占用,已自动切换为 ${resolved}\033[0m" >&2
|
fi
|
||||||
echo "$resolved"
|
|
||||||
return 0
|
# 被占用 → 自动替换
|
||||||
fi
|
if is_port_in_use "$value"; then
|
||||||
|
resolved=$(find_available_port)
|
||||||
|
if [ -n "$resolved" ]; then
|
||||||
|
echo "[WARN] ${name} port ${value} in use, switched to ${resolved}" >&2
|
||||||
|
echo "$resolved"
|
||||||
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$value"
|
echo "$value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 解析 host:port
|
||||||
|
# =========================
|
||||||
resolve_host_port() {
|
resolve_host_port() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local raw="$2"
|
local raw="$2"
|
||||||
local default_host="$3"
|
local default_host="$3"
|
||||||
|
|
||||||
local host
|
local host
|
||||||
local port
|
local port
|
||||||
|
|
||||||
if [ "$raw" = "auto" ] || [ -z "$raw" ]; then
|
if [ -z "$raw" ] || [ "$raw" = "auto" ]; then
|
||||||
host="$default_host"
|
host="$default_host"
|
||||||
port="auto"
|
port="auto"
|
||||||
else
|
else
|
||||||
@ -97,6 +124,10 @@ resolve_host_port() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# host 兜底
|
||||||
|
[ -z "$host" ] && host="$default_host"
|
||||||
|
|
||||||
port=$(resolve_port_value "$name" "$port") || return 1
|
port=$(resolve_port_value "$name" "$port") || return 1
|
||||||
|
|
||||||
echo "${host}:${port}"
|
echo "${host}:${port}"
|
||||||
}
|
}
|
||||||
@ -1,119 +1,214 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
resolve_clash_arch() {
|
resolve_clash_arch() {
|
||||||
local raw_arch="$1"
|
local raw_arch="$1"
|
||||||
case "$raw_arch" in
|
case "$raw_arch" in
|
||||||
x86_64|amd64)
|
x86_64|amd64) echo "linux-amd64" ;;
|
||||||
echo "linux-amd64"
|
aarch64|arm64) echo "linux-arm64" ;;
|
||||||
;;
|
armv7*|armv7l) echo "linux-armv7" ;;
|
||||||
aarch64|arm64)
|
*) echo "linux-${raw_arch}" ;;
|
||||||
echo "linux-arm64"
|
|
||||||
;;
|
|
||||||
armv7*|armv7l)
|
|
||||||
echo "linux-armv7"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "linux-${raw_arch}"
|
|
||||||
;;
|
|
||||||
esac
|
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() {
|
download_clash_bin() {
|
||||||
local server_dir="$1"
|
local server_dir="$1"
|
||||||
local detected_arch="$2"
|
local detected_arch="$2"
|
||||||
|
|
||||||
local resolved_arch
|
local resolved_arch
|
||||||
|
local version
|
||||||
local download_url
|
local download_url
|
||||||
|
|
||||||
local download_target
|
local download_target
|
||||||
local archive_file
|
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
|
if [ -z "$resolved_arch" ]; then
|
||||||
echo -e "\033[33m[WARN] 无法识别 CPU 架构,跳过 Clash 内核自动下载\033[0m"
|
echo "[WARN] 无法识别 CPU 架构" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local _default_url="https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz"
|
if [ -z "${CLASH_DOWNLOAD_URL_TEMPLATE:-}" ]; then
|
||||||
download_url="${CLASH_DOWNLOAD_URL_TEMPLATE:-$_default_url}"
|
echo "[ERROR] CLASH_DOWNLOAD_URL_TEMPLATE 未设置" >&2
|
||||||
if [ -z "$download_url" ]; then
|
|
||||||
echo -e "\033[33m[WARN] 未设置 CLASH_DOWNLOAD_URL_TEMPLATE,跳过 Clash 内核自动下载\033[0m"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
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}"
|
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
|
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
|
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
|
else
|
||||||
echo -e "\033[33m[WARN] 未找到 curl 或 wget,无法自动下载 Clash 内核\033[0m"
|
echo "[ERROR] 未找到 curl 或 wget" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${archive_file}" ]; then
|
# =========================
|
||||||
if gzip -t "${archive_file}" >/dev/null 2>&1; then
|
# 基础校验(防 404 / HTML)
|
||||||
gzip -dc "${archive_file}" >"${download_target}"
|
# =========================
|
||||||
else
|
if [ ! -s "$archive_file" ]; then
|
||||||
mv "${archive_file}" "${download_target}"
|
echo "[ERROR] 下载文件为空" >&2
|
||||||
fi
|
return 1
|
||||||
chmod +x "${download_target}"
|
|
||||||
echo "${download_target}"
|
|
||||||
return 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\033[33m[WARN] Clash 内核自动下载失败\033[0m"
|
if head -c 200 "$archive_file" | grep -qiE "not found|html"; then
|
||||||
return 1
|
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() {
|
resolve_clash_bin() {
|
||||||
local server_dir="$1"
|
local server_dir="$1"
|
||||||
local detected_arch="$2"
|
local detected_arch="$2"
|
||||||
|
|
||||||
local resolved_arch
|
local resolved_arch
|
||||||
local candidates=()
|
local candidates=()
|
||||||
local candidate
|
local candidate
|
||||||
local downloaded_bin
|
local downloaded_bin
|
||||||
|
local mode
|
||||||
|
|
||||||
if [ -n "${CLASH_BIN:-}" ]; then
|
if [ -n "${CLASH_BIN:-}" ]; then
|
||||||
if [ -x "$CLASH_BIN" ]; then
|
if [ -x "$CLASH_BIN" ]; then
|
||||||
echo "$CLASH_BIN"
|
echo "$CLASH_BIN"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo -e "\033[31m[ERROR] CLASH_BIN 指定的文件不可执行: $CLASH_BIN\033[0m"
|
echo "[ERROR] CLASH_BIN 不可执行: $CLASH_BIN" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
resolved_arch=$(resolve_clash_arch "$detected_arch")
|
resolved_arch="$(resolve_clash_arch "$detected_arch")"
|
||||||
|
|
||||||
if [ -n "$resolved_arch" ]; then
|
if [ -n "$resolved_arch" ]; then
|
||||||
candidates+=("${server_dir}/bin/clash-${resolved_arch}")
|
candidates+=("${server_dir}/bin/clash-${resolved_arch}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
candidates+=(
|
candidates+=(
|
||||||
"${server_dir}/bin/clash-${detected_arch}"
|
"${server_dir}/bin/clash-${detected_arch}"
|
||||||
"${server_dir}/bin/clash"
|
"${server_dir}/bin/clash"
|
||||||
)
|
)
|
||||||
|
|
||||||
for candidate in "${candidates[@]}"; do
|
mode="${CLASH_AUTO_DOWNLOAD:-auto}"
|
||||||
if [ -x "$candidate" ]; then
|
|
||||||
echo "$candidate"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if downloaded_bin=$(download_clash_bin "$server_dir" "$detected_arch"); then
|
case "$mode" in
|
||||||
echo "$downloaded_bin"
|
false)
|
||||||
return 0
|
for candidate in "${candidates[@]}"; do
|
||||||
fi
|
if [ -x "$candidate" ]; then
|
||||||
|
echo "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
echo -e "\033[31m\n[ERROR] 未找到可用的 Clash 二进制。\033[0m"
|
auto)
|
||||||
echo -e "请将对应架构的二进制放入: $server_dir/bin/"
|
for candidate in "${candidates[@]}"; do
|
||||||
echo -e "可用命名示例: clash-${resolved_arch} 或 clash-${detected_arch}"
|
if [ -x "$candidate" ]; then
|
||||||
echo -e "或通过 CLASH_BIN 指定自定义路径。"
|
echo "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
return 1
|
||||||
}
|
}
|
||||||
@ -12,7 +12,9 @@ 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 ;;
|
||||||
@ -29,6 +31,14 @@ if [ "$FOREGROUND" = true ] && [ "$DAEMON" = true ]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$FOREGROUND" = false ] && [ "$DAEMON" = false ]; then
|
||||||
|
echo "[ERROR] Must specify --foreground or --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
|
||||||
@ -39,6 +49,9 @@ if grep -q '\${' "$CONFIG_FILE"; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 加载依赖
|
||||||
|
# =========================
|
||||||
# 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
|
# shellcheck disable=SC1091
|
||||||
@ -46,40 +59,49 @@ source "$PROJECT_DIR/scripts/resolve_clash.sh"
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "$PROJECT_DIR/scripts/service_lib.sh"
|
source "$PROJECT_DIR/scripts/service_lib.sh"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 获取二进制
|
||||||
|
# =========================
|
||||||
CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")"
|
CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")"
|
||||||
|
|
||||||
if [ ! -x "$CLASH_BIN" ]; then
|
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
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test_config() {
|
# =========================
|
||||||
local bin="$1"
|
# config 测试(唯一一次)
|
||||||
local config="$2"
|
# =========================
|
||||||
local runtime_dir="$3"
|
if ! "$CLASH_BIN" -t -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >/dev/null 2>&1; then
|
||||||
"$bin" -d "$runtime_dir" -t -f "$config" >/dev/null 2>&1
|
echo "[ERROR] clash config test failed: $CONFIG_FILE" >&2
|
||||||
}
|
write_run_state "failed" "config-test"
|
||||||
|
|
||||||
if ! test_config "$CLASH_BIN" "$CONFIG_FILE" "$RUNTIME_DIR"; then
|
|
||||||
echo "[ERROR] config test failed: $CONFIG_FILE" >&2
|
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# systemd 模式
|
# =========================
|
||||||
|
# 前台模式(systemd)
|
||||||
|
# =========================
|
||||||
if [ "$FOREGROUND" = true ]; then
|
if [ "$FOREGROUND" = true ]; then
|
||||||
write_run_state "running" "systemd"
|
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
|
# 后台模式(script)
|
||||||
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
|
# =========================
|
||||||
pid=$!
|
cleanup_dead_pid
|
||||||
echo "$pid" > "$PID_FILE"
|
|
||||||
write_run_state "running" "script" "$pid"
|
if is_script_running; then
|
||||||
echo "[OK] Clash started in script mode, pid=$pid"
|
pid="$(read_pid 2>/dev/null || true)"
|
||||||
|
echo "[INFO] clash already running, pid=${pid:-unknown}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[ERROR] Must specify --foreground or --daemon" >&2
|
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
|
||||||
exit 2
|
|
||||||
|
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"
|
mkdir -p "$RUNTIME_DIR"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 基础能力
|
||||||
|
# =========================
|
||||||
has_systemd() {
|
has_systemd() {
|
||||||
command -v systemctl >/dev/null 2>&1
|
command -v systemctl >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
@ -19,17 +22,39 @@ service_unit_exists() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
read_pid() {
|
read_pid() {
|
||||||
[ -f "$PID_FILE" ] || return 1
|
[ -s "$PID_FILE" ] || return 1
|
||||||
cat "$PID_FILE"
|
tr -d '[:space:]' < "$PID_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_pid_running() {
|
||||||
|
local pid="$1"
|
||||||
|
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
is_script_running() {
|
is_script_running() {
|
||||||
local pid
|
local pid
|
||||||
pid="$(read_pid 2>/dev/null || true)"
|
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() {
|
detect_mode() {
|
||||||
|
cleanup_dead_pid
|
||||||
|
|
||||||
if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then
|
if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
echo "systemd"
|
echo "systemd"
|
||||||
elif is_script_running; then
|
elif is_script_running; then
|
||||||
@ -41,59 +66,65 @@ detect_mode() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
write_run_state() {
|
# =========================
|
||||||
local status="$1"
|
# state 写入(唯一实现)
|
||||||
local mode="${2:-unknown}"
|
# =========================
|
||||||
local pid="${3:-}"
|
write_state_kv() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
mkdir -p "$RUNTIME_DIR"
|
||||||
touch "$STATE_FILE"
|
touch "$STATE_FILE"
|
||||||
|
|
||||||
if grep -q '^LAST_RUN_STATUS=' "$STATE_FILE" 2>/dev/null; then
|
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
|
||||||
sed -i -E "s/^LAST_RUN_STATUS=.*/LAST_RUN_STATUS=${status}/" "$STATE_FILE"
|
sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_FILE"
|
||||||
else
|
else
|
||||||
echo "LAST_RUN_STATUS=${status}" >> "$STATE_FILE"
|
echo "${key}=${value}" >> "$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
|
|
||||||
fi
|
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() {
|
start_via_systemd() {
|
||||||
systemctl start "$SERVICE_NAME"
|
systemctl start "$SERVICE_NAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_via_systemd() {
|
stop_via_systemd() {
|
||||||
systemctl stop "$SERVICE_NAME"
|
systemctl stop "$SERVICE_NAME" || true
|
||||||
|
cleanup_dead_pid
|
||||||
write_run_state "stopped" "systemd"
|
write_run_state "stopped" "systemd"
|
||||||
rm -f "$PID_FILE"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restart_via_systemd() {
|
restart_via_systemd() {
|
||||||
systemctl restart "$SERVICE_NAME"
|
systemctl restart "$SERVICE_NAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# script 模式
|
||||||
|
# =========================
|
||||||
start_via_script() {
|
start_via_script() {
|
||||||
|
cleanup_dead_pid
|
||||||
|
|
||||||
if is_script_running; then
|
if is_script_running; then
|
||||||
echo "[INFO] clash already running (script mode)"
|
echo "[INFO] clash already running (script)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$PROJECT_DIR/scripts/run_clash.sh" --daemon
|
"$PROJECT_DIR/scripts/run_clash.sh" --daemon
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,11 +132,19 @@ stop_via_script() {
|
|||||||
local pid
|
local pid
|
||||||
pid="$(read_pid 2>/dev/null || true)"
|
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"
|
echo "[INFO] stopping clash pid=$pid"
|
||||||
kill "$pid"
|
|
||||||
sleep 1
|
kill "$pid" 2>/dev/null || true
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
|
for _ in 1 2 3 4 5; do
|
||||||
|
if ! is_pid_running "$pid"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if is_pid_running "$pid"; then
|
||||||
kill -9 "$pid" 2>/dev/null || true
|
kill -9 "$pid" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -1,28 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
exec "$PROJECT_DIR/clashctl" status "$@"
|
||||||
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
|
|
||||||
@ -1,23 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
exec "$PROJECT_DIR/clashctl" stop "$@"
|
||||||
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
|
|
||||||
@ -1,19 +1,33 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Clash for Linux
|
Description=Clash for Linux (Mihomo)
|
||||||
After=network.target
|
Documentation=https://github.com/wnlen/clash-for-linux
|
||||||
|
After=network-online.target nss-lookup.target
|
||||||
|
Wants=network-online.target
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
StartLimitBurst=10
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
EnvironmentFile=-/etc/default/clash-for-linux
|
User=root
|
||||||
Environment=CLASH_HOME=/opt/clash-for-linux
|
Group=root
|
||||||
ExecStart=/bin/bash -c 'exec /bin/bash "${CLASH_HOME}/start.sh"'
|
WorkingDirectory=/opt/clash-for-linux
|
||||||
ExecStop=/bin/bash -c 'exec /bin/bash "${CLASH_HOME}/shutdown.sh"'
|
Environment=HOME=/root
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
ExecStart=/bin/bash /opt/clash-for-linux/clashctl start
|
||||||
User=clash
|
ExecStop=/bin/bash /opt/clash-for-linux/clashctl --from-systemd stop
|
||||||
Group=clash
|
|
||||||
PIDFile=%E{CLASH_HOME}/temp/clash.pid
|
Restart=always
|
||||||
Environment=CLASH_ENV_FILE=%E{CLASH_HOME}/temp/clash-for-linux.sh
|
RestartSec=5s
|
||||||
|
|
||||||
|
KillMode=mixed
|
||||||
|
TimeoutStartSec=120
|
||||||
|
TimeoutStopSec=30
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
UMask=0022
|
||||||
|
NoNewPrivileges=false
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@ -82,7 +82,7 @@ fi
|
|||||||
# 2) stop process by pid file from all likely dirs
|
# 2) stop process by pid file from all likely dirs
|
||||||
for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do
|
for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do
|
||||||
[ -n "$d" ] || continue
|
[ -n "$d" ] || continue
|
||||||
PID_FILE="$d/temp/clash.pid"
|
PID_FILE="$d/runtime/clash.pid"
|
||||||
if [ -f "$PID_FILE" ]; then
|
if [ -f "$PID_FILE" ]; then
|
||||||
PID="$(cat "$PID_FILE" 2>/dev/null || true)"
|
PID="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||||||
if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then
|
if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then
|
||||||
|
|||||||