diff --git a/.env b/.env index a1e4a56..ea8bedd 100644 --- a/.env +++ b/.env @@ -20,6 +20,15 @@ export CLASH_URL='' # false = 禁用自动更新,直接使用本地已有 config.yaml export CLASH_AUTO_UPDATE="true" +# 自动下载 Mihomo 内核 +CLASH_AUTO_DOWNLOAD=auto + +# 内核固定版本 +MIHOMO_VERSION=v1.19.21 + +# 内核自定义下载地址 +CLASH_DOWNLOAD_URL_TEMPLATE='https://github.com/MetaCubeX/mihomo/releases/download/v1.19.21/mihomo-linux-amd64-v1.19.21.gz' + # 订阅请求头(可选) # 常见机场需要 User-Agent;如不需要可留空 export CLASH_HEADERS='User-Agent: ClashforWindows/0.20.39' @@ -42,6 +51,8 @@ CLASH_SHOW_SECRET=true # 是否显示脱敏 Secret(推荐) CLASH_SHOW_SECRET_MASKED=true + + # External Controller(Clash RESTful API) # ⚠️ 安全建议: # - 默认仅监听本机:127.0.0.1:9090 (推荐) @@ -84,10 +95,10 @@ export CLASH_CONFIG_TEST_TIMEOUT="30" # ------------------------- # 可叠加多个 YAML 文件,后者覆盖前者(逗号分隔) -# export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml' +# export CLASH_MIXIN_PATHS='config/mixin.d/base.yaml,config/mixin.d/rules.yaml' -# 指定 Mixin 目录(默认:conf/mixin.d) -# export CLASH_MIXIN_DIR='conf/mixin.d' +# 指定 Mixin 目录(默认:config/mixin.d) +# export CLASH_MIXIN_DIR='config/mixin.d' # ------------------------- # 6) Tun 模式(高级可选,需要 Clash Meta / Premium) diff --git a/README.md b/README.md index 9de6a87..fbb7081 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ ``` git clone --branch master --depth 1 https://github.com/wnlen/clash-for-linux.git cd clash-for-linux -sudo bash install.sh +bash install.sh ``` 安装脚本将自动完成: @@ -68,13 +68,13 @@ sudo bash install.sh 编辑 `.env` 文件,设置订阅地址: ``` -sudo bash -c 'echo "CLASH_URL=<订阅地址>" > /root/clash-for-linux/.env' +bash -c 'echo "CLASH_URL=<订阅地址>" > /root/clash-for-linux/.env' ``` 配置完成后,**重启服务使配置生效**: ``` -sudo systemctl restart clash-for-linux.service +systemctl restart clash-for-linux.service ``` 说明: @@ -91,7 +91,7 @@ sudo systemctl restart clash-for-linux.service true = 启动时检查订阅并重新下载/转换配置 false = 禁用自动更新,直接使用本地已有 config.yaml ``` -sudo bash -c 'echo "CLASH_AUTO_UPDATE=false" > /opt/clash-for-linux/.env' +bash -c 'echo "CLASH_AUTO_UPDATE=false" > /opt/clash-for-linux/.env' ``` ------ @@ -122,19 +122,19 @@ http://127.0.0.1:9090/ui 编辑 `.env` 文件,设置公网访问(对外端口不用改,改了机器人也能扫到,密钥设置长点就行): ``` -sudo bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9090" > /opt/clash-for-linux/.env' +bash -c 'echo "EXTERNAL_CONTROLLER=0.0.0.0:9090" > /opt/clash-for-linux/.env' ``` 配置完成后,**重启服务使配置生效**: ``` -sudo systemctl restart clash-for-linux.service +systemctl restart clash-for-linux.service ``` 密钥留空时:脚本可自动生成随机值 获取密钥命令: ``` -sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "/opt/clash-for-linux/conf/config.yaml" | head -n 1 +sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "/opt/clash-for-linux/runtime/config.yaml" | head -n 1 ``` @@ -190,7 +190,7 @@ clashctl sub log ### 修改 Clash 配置并重启 ``` -vim conf/config.yaml +vim runtime/config.yaml clashctl restart ``` @@ -214,12 +214,12 @@ clashctl sub update personal 用于追加或覆盖 Clash 配置。 -- 默认读取:`conf/mixin.d/*.yaml`(按文件名排序) +- 默认读取:`config/mixin.d/*.yaml`(按文件名排序) - 也可在 `.env` 中指定: ``` -export CLASH_MIXIN_DIR='conf/mixin.d' -export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml' +export CLASH_MIXIN_DIR='config/mixin.d' +export CLASH_MIXIN_PATHS='config/mixin.d/base.yaml,config/mixin.d/rules.yaml' ``` ------ @@ -267,7 +267,7 @@ env | grep -E 'http_proxy|https_proxy' ## 🧹 卸载 ``` -sudo bash uninstall.sh +bash uninstall.sh ``` ------ @@ -314,25 +314,25 @@ export SUBCONVERTER_DOWNLOAD_URL_TEMPLATE='https://example.com/subconverter_{arc 1. 开启 IP 转发 ```bash -echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf -sudo sysctl -p +echo "net.ipv4.ip_forward = 1" | tee -a /etc/sysctl.conf +sysctl -p ``` 2.配置iptables ```bash # 先清空旧规则 -sudo iptables -t nat -F +iptables -t nat -F # 允许本机访问代理端口 -sudo iptables -t nat -A OUTPUT -p tcp --dport 7890 -j RETURN -sudo iptables -t nat -A OUTPUT -p tcp --dport 7891 -j RETURN -sudo iptables -t nat -A OUTPUT -p tcp --dport 7892 -j RETURN +iptables -t nat -A OUTPUT -p tcp --dport 7890 -j RETURN +iptables -t nat -A OUTPUT -p tcp --dport 7891 -j RETURN +iptables -t nat -A OUTPUT -p tcp --dport 7892 -j RETURN # 让所有 TCP 流量通过 7892 代理 -sudo iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 7892 +iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 7892 # 保存规则 -sudo iptables-save | sudo tee /etc/iptables.rules +iptables-save | tee /etc/iptables.rules ``` 3. 让 iptables 规则开机生效 @@ -345,7 +345,7 @@ exit 0 ``` ```bash -sudo chmod +x /etc/rc.local +chmod +x /etc/rc.local ``` diff --git a/clashctl b/clashctl index 1d114ec..d77e463 100755 --- a/clashctl +++ b/clashctl @@ -65,44 +65,27 @@ usage() { Usage: clashctl COMMAND [OPTIONS] -Core Commands: +Commands: on 开启当前终端代理 off 关闭当前终端代理 start 启动 Clash stop 停止 Clash restart 重新生成配置并重启 status 查看当前状态 - update git pull + 生成配置 + 重启 generate 仅生成配置,不启动 mode 查看当前运行模式(systemd/script/none) - -Utility Commands: ui 输出 Dashboard 地址 secret 输出当前 secret - sub show 查看订阅地址 - sub update 重新生成配置并重启 - tun status|on|off 查看/启用/关闭 Tun - mixin status|on|off 查看/启用/关闭 Mixin doctor 健康检查 logs [-f] [-n 100] 查看日志 + update git pull + 重新生成配置并重启 + sub show|update 查看订阅地址 / 重新生成配置并重启 + tun status|on|off 查看/启用/关闭 Tun + mixin status|on|off 查看/启用/关闭 Mixin Options: --from-systemd 内部使用,避免 stop 递归调用 systemctl -h, --help 显示帮助信息 - -Examples: - clashctl on - clashctl off - clashctl start - clashctl stop - clashctl restart - clashctl status - clashctl update - clashctl generate - clashctl ui - clashctl secret - clashctl sub show - clashctl tun on EOF } @@ -145,6 +128,66 @@ read_state_value() { sed -nE "s/^${key}=(.*)$/\1/p" "$STATE_FILE" | head -n 1 } +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +port_from_controller() { + local controller + controller="$(read_runtime_config_value "external-controller" || true)" + if [ -n "${controller:-}" ]; then + printf '%s\n' "${controller##*:}" + else + printf '9090\n' + fi +} + +http_port_from_config() { + local v + + v="$(read_runtime_config_value "mixed-port" || true)" + if [ -n "${v:-}" ]; then + printf '%s\n' "$v" + return 0 + fi + + v="$(read_runtime_config_value "port" || true)" + if [ -n "${v:-}" ]; then + printf '%s\n' "$v" + return 0 + fi + + printf '7890\n' +} + +check_port_listening() { + local port="$1" + + if command_exists ss; then + ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$" + return $? + elif command_exists netstat; then + netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$" + return $? + fi + + return 2 +} + +check_dashboard_http() { + local url="$1" + + if command_exists curl; then + curl -fsS --max-time 3 "$url" >/dev/null 2>&1 + return $? + elif command_exists wget; then + wget -q -T 3 -O /dev/null "$url" >/dev/null 2>&1 + return $? + fi + + return 2 +} + cmd_on() { require_profiled # shellcheck disable=SC1090 @@ -163,23 +206,29 @@ cmd_mode() { detect_mode } -cmd_start() { - local mode - mode="$(detect_mode)" +cmd_generate() { + "$PROJECT_DIR/scripts/generate_config.sh" + ok "Config generated" +} - case "$mode" in - systemd|systemd-installed) - start_via_systemd - ok "Clash started via systemd" - ;; - script|none) - start_via_script - ;; - *) - err "未知模式: $mode" - exit 1 - ;; - esac +cmd_start() { + local mode + mode="$(detect_mode)" + + case "$mode" in + systemd|systemd-installed) + start_via_systemd + ok "Clash started via systemd" + ;; + script|none) + start_via_script + ok "Clash started via script mode" + ;; + *) + err "未知模式: $mode" + exit 1 + ;; + esac } cmd_stop() { @@ -190,13 +239,15 @@ cmd_stop() { case "$mode" in systemd) if [ "$from_systemd" = "true" ]; then - # 被 systemd ExecStop 调用时,不能再反向 systemctl stop 自己 ok "Stop requested from systemd, skip recursive systemctl stop" - exit 0 + return 0 fi stop_via_systemd ok "Clash stopped via systemd" ;; + systemd-installed) + info "systemd service installed but not running" + ;; script) stop_via_script ok "Clash stopped via script mode" @@ -211,51 +262,142 @@ cmd_stop() { esac } -cmd_generate() { - "$PROJECT_DIR/scripts/generate_config.sh" - ok "Config generated" -} - cmd_restart() { - "$PROJECT_DIR/scripts/generate_config.sh" - - local mode - 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_generate + cmd_stop "${1:-false}" || true + cmd_start } cmd_update() { git -C "$PROJECT_DIR" pull - "$PROJECT_DIR/scripts/generate_config.sh" + cmd_restart + ok "Project updated" +} - local mode - mode="$(detect_mode)" +cmd_ui() { + local raw="${1:-}" + local controller host port secret base_url - case "$mode" in - systemd) - restart_via_systemd - ok "Project updated and restarted via systemd" + controller="$(read_runtime_config_value "external-controller" || true)" + [ -n "${controller:-}" ] || controller="127.0.0.1:9090" + + host="${controller%:*}" + port="${controller##*:}" + + case "$host" in + 0.0.0.0|::|localhost) + host="$(hostname -I 2>/dev/null | awk '{print $1}')" + [ -n "${host:-}" ] || host="127.0.0.1" ;; - script|none) - restart_via_script - ok "Project updated and restarted via script mode" + esac + + secret="$(read_runtime_config_value "secret" || true)" + base_url="http://${host}:${port}/ui" + + if [ -n "${secret:-}" ]; then + base_url="${base_url}/#/setup?hostname=${host}&port=${port}&secret=${secret}" + fi + + if [ "$raw" = "--raw" ]; then + printf '%s\n' "$base_url" + return 0 + fi + + printf '%s\n' "$base_url" +} + +cmd_secret() { + local secret + + if [ ! -s "$RUNTIME_CONFIG" ]; then + err "runtime config not found: $RUNTIME_CONFIG" + echo "Please run: clashctl generate" >&2 + exit 1 + fi + + secret="$(read_runtime_config_value "secret" || true)" + if [ -z "${secret:-}" ]; then + err "secret not found in $RUNTIME_CONFIG" + exit 1 + fi + + printf '%s\n' "$secret" +} + +cmd_sub() { + local subcmd="${1:-show}" + + case "$subcmd" in + show) + if [ -f "$ENV_FILE" ]; then + grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || echo "CLASH_URL=未配置" + else + err "未找到 .env" + exit 1 + fi + ;; + update) + cmd_restart ;; *) - err "未知模式: $mode" + err "未知 sub 子命令: $subcmd" + echo "用法: clashctl sub [show|update]" + exit 1 + ;; + esac +} + +cmd_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 @@ -387,23 +529,6 @@ cmd_status() { fi echo "Secret : $secret_exists" - - echo - case "$running" in - yes) - ok "status summary: running" - ;; - no) - if [ "$mode" = "systemd-installed" ]; then - warn "status summary: installed but not running" - else - warn "status summary: not running" - fi - ;; - *) - warn "status summary: unknown" - ;; - esac } doctor_ok() { @@ -418,182 +543,6 @@ doctor_err() { printf "\033[31m[ERROR]\033[0m %s\n" "$*" } -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -port_from_controller() { - local controller - controller="$(read_runtime_config_value "external-controller" || true)" - if [ -n "${controller:-}" ]; then - printf '%s\n' "${controller##*:}" - else - printf '9090\n' - fi -} - -http_port_from_config() { - local v - - v="$(read_runtime_config_value "mixed-port" || true)" - if [ -n "${v:-}" ]; then - printf '%s\n' "$v" - return 0 - fi - - v="$(read_runtime_config_value "port" || true)" - if [ -n "${v:-}" ]; then - printf '%s\n' "$v" - return 0 - fi - - printf '7890\n' -} - -check_port_listening() { - local port="$1" - - if command_exists ss; then - ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$" - return $? - elif command_exists netstat; then - netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$" - return $? - fi - - return 2 -} - -check_dashboard_http() { - local url="$1" - - if command_exists curl; then - curl -fsS --max-time 3 "$url" >/dev/null 2>&1 - return $? - elif command_exists wget; then - wget -q -T 3 -O /dev/null "$url" >/dev/null 2>&1 - return $? - fi - - return 2 -} - -cmd_ui() { - local raw="${1:-}" - local controller host port - - controller="$(read_runtime_config_value "external-controller" || true)" - [ -n "${controller:-}" ] || controller="127.0.0.1:9090" - - host="${controller%:*}" - port="${controller##*:}" - - case "$host" in - 0.0.0.0|::|localhost) - host="$(hostname -I 2>/dev/null | awk '{print $1}')" - [ -n "${host:-}" ] || host="127.0.0.1" - ;; - esac - - if [ "$raw" = "--raw" ]; then - printf 'http://%s:%s/ui\n' "$host" "$port" - return 0 - fi - - echo "Dashboard URL:" - printf 'http://%s:%s/ui\n' "$host" "$port" -} - -cmd_secret() { - local secret - secret="$(read_runtime_config_value "secret" || true)" - if [ -n "${secret:-}" ]; then - echo "$secret" - else - err "未读取到 secret" - exit 1 - fi -} - -cmd_sub() { - local subcmd="${1:-show}" - - case "$subcmd" in - show) - if [ -f "$ENV_FILE" ]; then - grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || echo "CLASH_URL=未配置" - else - err "未找到 .env" - exit 1 - fi - ;; - update) - cmd_restart - ;; - *) - err "未知 sub 子命令: $subcmd" - echo "用法: clashctl sub [show|update]" - exit 1 - ;; - esac -} - -cmd_tun() { - local subcmd="${1:-status}" - - case "$subcmd" in - status) - if [ -f "$ENV_FILE" ]; then - grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置' - else - err "未找到 .env" - exit 1 - fi - ;; - on) - write_env_bool "CLASH_TUN" "true" - ok "已写入 CLASH_TUN=true" - ;; - off) - write_env_bool "CLASH_TUN" "false" - ok "已写入 CLASH_TUN=false" - ;; - *) - err "未知 tun 子命令: $subcmd" - echo "用法: clashctl tun [status|on|off]" - exit 1 - ;; - esac -} - -cmd_mixin() { - local subcmd="${1:-status}" - - case "$subcmd" in - status) - if [ -f "$ENV_FILE" ]; then - grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_MIXIN=' "$ENV_FILE" || echo 'CLASH_MIXIN=未配置' - else - err "未找到 .env" - exit 1 - fi - ;; - on) - write_env_bool "CLASH_MIXIN" "true" - ok "已写入 CLASH_MIXIN=true" - ;; - off) - write_env_bool "CLASH_MIXIN" "false" - ok "已写入 CLASH_MIXIN=false" - ;; - *) - err "未知 mixin 子命令: $subcmd" - echo "用法: clashctl mixin [status|on|off]" - exit 1 - ;; - esac -} - cmd_doctor() { local mode running="no" local failed=0 @@ -612,7 +561,6 @@ cmd_doctor() { echo "Config : $RUNTIME_CONFIG" echo - # 1. 检查运行配置是否存在 if [ -s "$RUNTIME_CONFIG" ]; then doctor_ok "runtime config exists: $RUNTIME_CONFIG" else @@ -620,7 +568,6 @@ cmd_doctor() { failed=1 fi - # 2. 检查配置里是否还有未渲染占位符 if [ -f "$RUNTIME_CONFIG" ]; then if grep -q '\${' "$RUNTIME_CONFIG"; then doctor_err "runtime config contains unresolved placeholders" @@ -630,7 +577,6 @@ cmd_doctor() { fi fi - # 3. 检查 state.env if [ -f "$STATE_FILE" ]; then doctor_ok "state file exists: $STATE_FILE" @@ -658,7 +604,6 @@ cmd_doctor() { warned=1 fi - # 4. 检查运行模式 / 进程状态 case "$mode" in systemd) if systemctl is-active --quiet "$SERVICE_NAME"; then @@ -692,7 +637,6 @@ cmd_doctor() { ;; esac - # 5. 检查 dashboard 地址 dashboard_url="$(cmd_ui --raw 2>/dev/null || true)" controller="$(read_runtime_config_value "external-controller" || true)" dashboard_port="$(port_from_controller)" @@ -711,7 +655,6 @@ cmd_doctor() { warned=1 fi - # 6. 检查 HTTP 代理端口 http_port="$(http_port_from_config)" if check_port_listening "$http_port"; then doctor_ok "proxy port is listening: $http_port" @@ -731,7 +674,6 @@ cmd_doctor() { fi fi - # 7. 检查 dashboard 端口 if check_port_listening "$dashboard_port"; then doctor_ok "dashboard port is listening: $dashboard_port" else @@ -750,7 +692,6 @@ cmd_doctor() { fi fi - # 8. 检查 dashboard HTTP 可访问性 if [ -n "${dashboard_url:-}" ]; then if check_dashboard_http "$dashboard_url"; then doctor_ok "dashboard http reachable" @@ -766,7 +707,6 @@ cmd_doctor() { fi fi - # 9. 检查 secret secret="$(read_runtime_config_value "secret" || true)" if [ -n "${secret:-}" ]; then doctor_ok "secret exists in runtime config" @@ -775,7 +715,6 @@ cmd_doctor() { warned=1 fi - # 10. 检查 clashctl 安装位置(可选) if command_exists clashctl; then doctor_ok "clashctl command available: $(command -v clashctl)" else @@ -783,7 +722,6 @@ cmd_doctor() { warned=1 fi - # 11. 检查代理快捷函数文件 if [ -f "$PROFILED_FILE" ]; then doctor_ok "shell proxy helper exists: $PROFILED_FILE" else @@ -807,13 +745,13 @@ cmd_doctor() { } cmd_logs() { + shift || true + local follow="false" local lines="50" local mode local arg - shift || true - while [ $# -gt 0 ]; do arg="$1" case "$arg" in @@ -893,7 +831,7 @@ main() { cmd_stop "$from_systemd" ;; restart) - cmd_restart + cmd_restart "$from_systemd" ;; status) cmd_status @@ -926,10 +864,10 @@ main() { shift || true cmd_mixin "${1:-status}" ;; - doctor) + doctor) cmd_doctor ;; - logs) + logs) cmd_logs "$@" ;; ""|-h|--help) diff --git a/config/mixin.d/README.md b/config/mixin.d/README.md index 6bb5cf2..19332e8 100644 --- a/config/mixin.d/README.md +++ b/config/mixin.d/README.md @@ -5,5 +5,5 @@ 如需手动指定顺序或使用自定义路径,请在 `.env` 中设置: ```bash -export CLASH_MIXIN_PATHS='conf/mixin.d/base.yaml,conf/mixin.d/rules.yaml' +export CLASH_MIXIN_PATHS='config/mixin.d/base.yaml,config/mixin.d/rules.yaml' ``` diff --git a/docs/advanced.md b/docs/advanced.md index f647f60..b9977e7 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -25,14 +25,14 @@ Mixin 用于在 **不直接修改主配置文件** 的情况下, ### Default Behavior -- 默认读取目录:`conf/mixin.d/` +- 默认读取目录:`config/mixin.d/` - 按文件名排序后依次合并 - 后加载的文件会覆盖前面的配置 ### Example ```yaml -# conf/mixin.d/10-rules.yaml +# config/mixin.d/10-rules.yaml rules: - DOMAIN-SUFFIX,example.com,DIRECT ``` @@ -170,7 +170,7 @@ clash-for-linux 以 **安全默认配置** 为原则: ### Service Keeps Restarting -- 检查 `conf/config.yaml` 是否存在语法错误 +- 检查 `runtime/config.yaml` 是否存在语法错误 - 查看 systemd 日志: ``` diff --git a/docs/install.md b/docs/install.md index 005e88b..a0b0ed4 100644 --- a/docs/install.md +++ b/docs/install.md @@ -87,16 +87,15 @@ CLASH_START_SERVICE=true ### `CLASH_AUTO_DOWNLOAD` -``` +```env CLASH_AUTO_DOWNLOAD=auto -``` -- 是否自动下载 Clash 内核 +- 是否在本地未检测到可用内核时自动下载 Mihomo 内核 - 可选值: - - `auto`(默认):检测不到内核时自动下载 - - `true`:强制重新下载 - - `false`:关闭自动下载 - + - `auto`(默认):当未检测到可用内核时自动下载(已有内核则不覆盖) + - `false`:不进行任何自动下载,仅使用本地已有内核(找不到则报错) + - `true`:强制重新下载内核(即使本地已有也会覆盖) + 适用于: - 离线环境 @@ -107,12 +106,13 @@ CLASH_AUTO_DOWNLOAD=auto ### `CLASH_DOWNLOAD_URL_TEMPLATE` -``` -CLASH_DOWNLOAD_URL_TEMPLATE=https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz -``` +```env +CLASH_DOWNLOAD_URL_TEMPLATE=https://your-mirror.example.com/{version}/mihomo-{arch}-{version}.gz + +- Mihomo 内核下载地址模板(可选,高级配置) +- 仅在 CLASH_AUTO_DOWNLOAD=true 或 auto 且本地无内核时生效 +- 默认情况下无需配置,脚本会自动使用官方 GitHub Release 地址 -- Clash 内核下载地址模板 -- `{arch}` 会自动替换为当前系统架构(如 `amd64`、`arm64`) 适用于: @@ -173,13 +173,13 @@ Mixin 用于在不修改主配置的情况下,追加或覆盖 Clash 配置项 ### 默认行为 -- 默认读取目录:`conf/mixin.d/` +- 默认读取目录:`config/mixin.d/` - 按文件名排序后依次合并 ### 示例 ``` -# conf/mixin.d/rules.yaml +# config/mixin.d/rules.yaml rules: - DOMAIN-SUFFIX,example.com,DIRECT ``` diff --git a/install.sh b/install.sh index 8735b10..8ae4a29 100755 --- a/install.sh +++ b/install.sh @@ -1,113 +1,33 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail -# ========================= -# 基础参数 -# ========================= -Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" +Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}" + Service_Name="clash-for-linux" Service_User="root" Service_Group="root" # ========================= -# 彩色输出(统一 printf + 自动降级 + 手动关色) -# ========================= - -# ---- 关色开关(优先级最高)---- -NO_COLOR_FLAG=0 -for arg in "$@"; do - case "$arg" in - --no-color|--nocolor) - NO_COLOR_FLAG=1 - ;; - esac -done - -if [[ -n "${NO_COLOR:-}" ]] || [[ -n "${CLASH_NO_COLOR:-}" ]]; then - NO_COLOR_FLAG=1 -fi - -# ---- 初始化颜色 ---- -if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then - if tput setaf 1 >/dev/null 2>&1; then - C_RED="$(tput setaf 1)" - C_GREEN="$(tput setaf 2)" - C_YELLOW="$(tput setaf 3)" - C_BLUE="$(tput setaf 4)" - C_CYAN="$(tput setaf 6)" - C_GRAY="$(tput setaf 8 2>/dev/null || true)" - C_BOLD="$(tput bold)" - C_UL="$(tput smul)" - C_NC="$(tput sgr0)" - fi -fi - -# ---- ANSI fallback ---- -if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && [[ -z "${C_NC:-}" ]]; then - C_RED=$'\033[31m' - C_GREEN=$'\033[32m' - C_YELLOW=$'\033[33m' - C_BLUE=$'\033[34m' - C_CYAN=$'\033[36m' - C_GRAY=$'\033[90m' - C_BOLD=$'\033[1m' - C_UL=$'\033[4m' - C_NC=$'\033[0m' -fi - -# ---- 强制无色 ---- -if [[ "$NO_COLOR_FLAG" -eq 1 ]] || [[ ! -t 1 ]]; then - C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN='' C_GRAY='' C_BOLD='' C_UL='' C_NC='' -fi - -# ========================= -# 基础输出函数 -# ========================= -log() { printf "%b\n" "$*"; } -info() { log "${C_CYAN}[INFO]${C_NC} $*"; } -ok() { log "${C_GREEN}[OK]${C_NC} $*"; } -warn() { log "${C_YELLOW}[WARN]${C_NC} $*"; } -err() { log "${C_RED}[ERROR]${C_NC} $*"; } - -# ========================= -# 样式助手 -# ========================= -path() { printf "%b" "${C_BOLD}$*${C_NC}"; } -cmd() { printf "%b" "${C_GRAY}$*${C_NC}"; } -url() { printf "%b" "${C_UL}$*${C_NC}"; } -good() { printf "%b" "${C_GREEN}$*${C_NC}"; } -bad() { printf "%b" "${C_RED}$*${C_NC}"; } - -# ========================= -# 分段标题(CLI 风格 section) -# ========================= -section() { - local title="$*" - log "" - log "${C_BOLD}▶ ${title}${C_NC}" - log "${C_GRAY}────────────────────────────────────────${C_NC}" -} - -# ========================= -# 前置校验 +# 基础校验 # ========================= if [ "$(id -u)" -ne 0 ]; then - err "需要 root 权限执行安装脚本(请使用 bash install.sh)" + echo "[ERROR] root required" >&2 exit 1 fi if [ ! -f "${Server_Dir}/.env" ]; then - err "未找到 .env 文件,请确认脚本所在目录:${Server_Dir}" + echo "[ERROR] .env not found in ${Server_Dir}" >&2 exit 1 fi # ========================= -# 同步到安装目录(保持你原逻辑) +# 同步文件 # ========================= mkdir -p "$Install_Dir" + if [ "$Server_Dir" != "$Install_Dir" ]; then - info "同步项目文件到安装目录:${Install_Dir}" + echo "[INFO] sync project to ${Install_Dir}" if command -v rsync >/dev/null 2>&1; then rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/" else @@ -115,253 +35,80 @@ if [ "$Server_Dir" != "$Install_Dir" ]; then fi fi -chmod +x "$Install_Dir"/*.sh 2>/dev/null || true +chmod +x "$Install_Dir"/clashctl 2>/dev/null || true chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true chmod +x "$Install_Dir"/bin/* 2>/dev/null || true -chmod +x "$Install_Dir"/clashctl 2>/dev/null || true # ========================= -# 加载环境与依赖脚本 +# 目录初始化(新结构) +# ========================= +mkdir -p \ + "$Install_Dir/runtime" \ + "$Install_Dir/logs" \ + "$Install_Dir/config/mixin.d" + +# ========================= +# 加载 env # ========================= # shellcheck disable=SC1090 source "$Install_Dir/.env" + # shellcheck disable=SC1090 source "$Install_Dir/scripts/get_cpu_arch.sh" + # shellcheck disable=SC1090 source "$Install_Dir/scripts/resolve_clash.sh" -# shellcheck disable=SC1090 -source "$Install_Dir/scripts/port_utils.sh" -if [[ -z "${CpuArch:-}" ]]; then - err "无法识别 CPU 架构" +# ========================= +# 内核检查 +# ========================= +if ! resolve_clash_bin "$Install_Dir" "${CpuArch:-}" >/dev/null 2>&1; then + echo "[ERROR] clash core not ready" >&2 exit 1 fi # ========================= -# .env 写入工具:write_env_kv(必须在 prompt 之前定义) -# - 自动创建文件 -# - 存在则替换,不存在则追加 -# - 统一写成:export KEY="VALUE" -# - 自动转义双引号/反斜杠 +# 安装 clashctl # ========================= -escape_env_value() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -write_env_kv() { - local file="$1" - local key="$2" - local val="$3" - - mkdir -p "$(dirname "$file")" 2>/dev/null || true - [ -f "$file" ] || touch "$file" - - val="$(printf '%s' "$val" | tr -d '\r')" - local esc - esc="$(escape_env_value "$val")" - - if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file"; then - sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*|export ${key}=\"${esc}\"|g" "$file" - else - printf 'export %s="%s"\n' "$key" "$esc" >> "$file" - fi -} +install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl # ========================= -# 交互式填写订阅地址(仅在 CLASH_URL 为空时触发) -# - 若非 TTY(CI/管道)则跳过交互 -# - 若用户回车跳过,则保持原行为:装完提示手动配置 +# 安装 proxy helper # ========================= -prompt_clash_url_if_empty() { - # 兼容 .env 里可能是 CLASH_URL= / export CLASH_URL= / 带引号 - local cur="${CLASH_URL:-}" - cur="${cur%\"}"; cur="${cur#\"}" - - if [ -n "$cur" ]; then - return 0 - fi - - # 非交互环境:不阻塞 - if [ ! -t 0 ]; then - warn "CLASH_URL 为空且当前为非交互环境(stdin 非 TTY),将跳过输入引导。" - return 0 - fi - - echo - warn "未检测到订阅地址(CLASH_URL 为空)" - echo "请粘贴你的 Clash 订阅地址(直接回车跳过,稍后手动编辑 .env):" - read -r -p "Clash URL: " input_url - - input_url="$(printf '%s' "$input_url" | tr -d '\r')" - - # 回车跳过:保持原行为(不写入) - if [ -z "$input_url" ]; then - warn "已跳过填写订阅地址,安装完成后请手动编辑:${Install_Dir}/.env" - return 0 - fi - - # 先校验再写入,避免污染 .env - if ! echo "$input_url" | grep -Eq '^https?://'; then - err "订阅地址格式不正确(必须以 http:// 或 https:// 开头)" - exit 1 - fi - - ENV_FILE="${Install_Dir}/.env" - mkdir -p "$Install_Dir" - [ -f "$ENV_FILE" ] || touch "$ENV_FILE" - - # ✅ 只用这一套写入逻辑(统一 export KEY="...",兼容旧格式) - write_env_kv "$ENV_FILE" "CLASH_URL" "$input_url" - - export CLASH_URL="$input_url" - ok "已写入订阅地址到:${ENV_FILE}" +cat >/etc/profile.d/clash-for-linux.sh </dev/null 2>&1; then - err "Clash 内核未就绪,请检查下载配置或手动放置二进制" - exit 1 -fi - -# ========================= -# fonction 工具函数区 -# ========================= -# 等待 config.yaml 出现并写入 secret(默认最多等 6 秒) -wait_secret_ready() { - local conf_file="$1" - local timeout_sec="${2:-6}" - - local end=$((SECONDS + timeout_sec)) - while [ "$SECONDS" -lt "$end" ]; do - if [ -s "$conf_file" ] && grep -qE '^[[:space:]]*secret:' "$conf_file"; then - return 0 - fi - sleep 0.2 - done - return 1 -} - -# 计算字符串可视宽度:中文大概率按 2 宽处理(简单够用版) -# 注:终端宽度/字体不统一时,中文宽度估算永远只能“近似” -vis_width() { - python3 - <<'PY' "$1" -import sys -s=sys.argv[1] -w=0 -for ch in s: - # East Asian Wide/FullWidth 近似当 2 - w += 2 if ord(ch) >= 0x2E80 else 1 -print(w) -PY -} - -pad_right() { # pad_right "text" width - local s="$1" w="$2" - local cur - cur="$(vis_width "$s")" - local pad=$(( w - cur )) - (( pad < 0 )) && pad=0 - printf "%s%*s" "$s" "$pad" "" -} - -box_title() { # box_title "标题" width - local title="$1" width="$2" - local inner=$((width-2)) - printf "┌%s┐\n" "$(printf '─%.0s' $(seq 1 $inner))" - # 标题居中(近似) - local t=" $title " - local tw; tw="$(vis_width "$t")" - local left=$(( (inner - tw)/2 )); ((left<0)) && left=0 - local right=$(( inner - tw - left )); ((right<0)) && right=0 - printf "│%*s%s%*s│\n" "$left" "" "$t" "$right" "" - printf "├%s┤\n" "$(printf '─%.0s' $(seq 1 $inner))" -} - -box_row() { # box_row "key" "value" width keyw - local k="$1" v="$2" width="$3" keyw="$4" - local inner=$((width-2)) - # 形如:│ key: value │ - local left="$(pad_right "$k" "$keyw")" - local line=" ${left} ${v}" - local lw; lw="$(vis_width "$line")" - local pad=$(( inner - lw )); ((pad<0)) && pad=0 - printf "│%s%*s│\n" "$line" "$pad" "" -} - -box_end() { # box_end width - local width="$1" inner=$((width-2)) - printf "└%s┘\n" "$(printf '─%.0s' $(seq 1 $inner))" -} - -# 从 config.yaml 提取 secret(强韧:支持缩进/引号/CRLF/尾空格) -read_secret_from_config() { - local conf_file="$1" - [ -f "$conf_file" ] || return 1 - - # 1) 找到 secret 行 -> 2) 去掉 key 和空格 -> 3) 去掉首尾引号 -> 4) 去掉 CR - local s - s="$( - sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "$conf_file" \ - | head -n 1 \ - | sed -E 's/^[[:space:]]*"(.*)"[[:space:]]*$/\1/; s/^[[:space:]]*'\''(.*)'\''[[:space:]]*$/\1/' \ - | tr -d '\r' - )" - - # 去掉纯空格 - s="$(printf '%s' "$s" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" - - [ -n "$s" ] || return 1 - printf '%s' "$s" -} - -# ========================= -# systemd 安装与启动 +# 安装 systemd # ========================= Service_Enabled="unknown" Service_Started="unknown" if command -v systemctl >/dev/null 2>&1; then - CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh" + CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" \ + "$Install_Dir/scripts/install_systemd.sh" "$Install_Dir" if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then - systemctl start "${Service_Name}.service" || true + systemctl enable "${Service_Name}.service" || true fi + if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then systemctl start "${Service_Name}.service" || true fi @@ -378,270 +125,26 @@ if command -v systemctl >/dev/null 2>&1; then Service_Started="inactive" fi else - warn "未检测到 systemd,已跳过服务单元生成" + echo "[WARN] systemd not found, will use script mode" fi # ========================= -# Shell 代理快捷命令 -# 生成:/etc/profile.d/clash-for-linux.sh +# 输出(全部收敛到 clashctl) # ========================= -PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" +echo +echo "=== Install Complete ===" +echo "Install Dir : $Install_Dir" +echo "clashctl : /usr/local/bin/clashctl" -install_profiled() { - local install_dir="$Install_Dir" +echo +echo "Next:" +echo " clashctl generate" +echo " clashctl start" +echo " clashctl doctor" - cat >"$PROFILED_FILE" </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:-}" - echo "https_proxy=\${https_proxy:-}" - echo "all_proxy=\${all_proxy:-}" - 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 \ No newline at end of file +echo +echo "Commands:" +echo " clashctl status" +echo " clashctl logs" +echo " clashctl restart" +echo " clashctl stop" \ No newline at end of file diff --git a/runtime/proxy.env b/runtime/proxy.env deleted file mode 100644 index e69de29..0000000 diff --git a/runtime/state.env b/runtime/state.env index e69de29..c3db4ac 100644 --- a/runtime/state.env +++ b/runtime/state.env @@ -0,0 +1,6 @@ +LAST_GENERATE_STATUS= +LAST_GENERATE_AT= +LAST_RUN_STATUS= +LAST_RUN_MODE= +LAST_RUN_AT= +LAST_RUN_PID= \ No newline at end of file diff --git a/dashboard/public/200.html b/runtime/ui/200.html similarity index 100% rename from dashboard/public/200.html rename to runtime/ui/200.html diff --git a/dashboard/public/404.html b/runtime/ui/404.html similarity index 100% rename from dashboard/public/404.html rename to runtime/ui/404.html diff --git a/dashboard/public/_nuxt/27QtE9hq.js b/runtime/ui/_nuxt/27QtE9hq.js similarity index 100% rename from dashboard/public/_nuxt/27QtE9hq.js rename to runtime/ui/_nuxt/27QtE9hq.js diff --git a/dashboard/public/_nuxt/4Us7Cdat.js b/runtime/ui/_nuxt/4Us7Cdat.js similarity index 100% rename from dashboard/public/_nuxt/4Us7Cdat.js rename to runtime/ui/_nuxt/4Us7Cdat.js diff --git a/dashboard/public/_nuxt/83RCacat.js b/runtime/ui/_nuxt/83RCacat.js similarity index 100% rename from dashboard/public/_nuxt/83RCacat.js rename to runtime/ui/_nuxt/83RCacat.js diff --git a/dashboard/public/_nuxt/B3GymFtZ.js b/runtime/ui/_nuxt/B3GymFtZ.js similarity index 100% rename from dashboard/public/_nuxt/B3GymFtZ.js rename to runtime/ui/_nuxt/B3GymFtZ.js diff --git a/dashboard/public/_nuxt/B7rlnwkb.js b/runtime/ui/_nuxt/B7rlnwkb.js similarity index 100% rename from dashboard/public/_nuxt/B7rlnwkb.js rename to runtime/ui/_nuxt/B7rlnwkb.js diff --git a/dashboard/public/_nuxt/BE9s5YKM.js b/runtime/ui/_nuxt/BE9s5YKM.js similarity index 100% rename from dashboard/public/_nuxt/BE9s5YKM.js rename to runtime/ui/_nuxt/BE9s5YKM.js diff --git a/dashboard/public/_nuxt/BEg6euoa.js b/runtime/ui/_nuxt/BEg6euoa.js similarity index 100% rename from dashboard/public/_nuxt/BEg6euoa.js rename to runtime/ui/_nuxt/BEg6euoa.js diff --git a/dashboard/public/_nuxt/BEgqDXY5.js b/runtime/ui/_nuxt/BEgqDXY5.js similarity index 100% rename from dashboard/public/_nuxt/BEgqDXY5.js rename to runtime/ui/_nuxt/BEgqDXY5.js diff --git a/dashboard/public/_nuxt/BLICcaEN.js b/runtime/ui/_nuxt/BLICcaEN.js similarity index 100% rename from dashboard/public/_nuxt/BLICcaEN.js rename to runtime/ui/_nuxt/BLICcaEN.js diff --git a/dashboard/public/_nuxt/BO5M1c2D.js b/runtime/ui/_nuxt/BO5M1c2D.js similarity index 100% rename from dashboard/public/_nuxt/BO5M1c2D.js rename to runtime/ui/_nuxt/BO5M1c2D.js diff --git a/dashboard/public/_nuxt/BaY9ddW5.js b/runtime/ui/_nuxt/BaY9ddW5.js similarity index 100% rename from dashboard/public/_nuxt/BaY9ddW5.js rename to runtime/ui/_nuxt/BaY9ddW5.js diff --git a/dashboard/public/_nuxt/C1Z2nZ_q.js b/runtime/ui/_nuxt/C1Z2nZ_q.js similarity index 100% rename from dashboard/public/_nuxt/C1Z2nZ_q.js rename to runtime/ui/_nuxt/C1Z2nZ_q.js diff --git a/dashboard/public/_nuxt/CAimjrE6.js b/runtime/ui/_nuxt/CAimjrE6.js similarity index 100% rename from dashboard/public/_nuxt/CAimjrE6.js rename to runtime/ui/_nuxt/CAimjrE6.js diff --git a/dashboard/public/_nuxt/CDMImveV.js b/runtime/ui/_nuxt/CDMImveV.js similarity index 100% rename from dashboard/public/_nuxt/CDMImveV.js rename to runtime/ui/_nuxt/CDMImveV.js diff --git a/dashboard/public/_nuxt/CGgL16y1.js b/runtime/ui/_nuxt/CGgL16y1.js similarity index 100% rename from dashboard/public/_nuxt/CGgL16y1.js rename to runtime/ui/_nuxt/CGgL16y1.js diff --git a/dashboard/public/_nuxt/CQh-OLya.js b/runtime/ui/_nuxt/CQh-OLya.js similarity index 100% rename from dashboard/public/_nuxt/CQh-OLya.js rename to runtime/ui/_nuxt/CQh-OLya.js diff --git a/dashboard/public/_nuxt/CXTORPKN.js b/runtime/ui/_nuxt/CXTORPKN.js similarity index 100% rename from dashboard/public/_nuxt/CXTORPKN.js rename to runtime/ui/_nuxt/CXTORPKN.js diff --git a/dashboard/public/_nuxt/CcIdf__h.js b/runtime/ui/_nuxt/CcIdf__h.js similarity index 100% rename from dashboard/public/_nuxt/CcIdf__h.js rename to runtime/ui/_nuxt/CcIdf__h.js diff --git a/dashboard/public/_nuxt/CdlJ_YxQ.js b/runtime/ui/_nuxt/CdlJ_YxQ.js similarity index 100% rename from dashboard/public/_nuxt/CdlJ_YxQ.js rename to runtime/ui/_nuxt/CdlJ_YxQ.js diff --git a/dashboard/public/_nuxt/CtJjAUi2.js b/runtime/ui/_nuxt/CtJjAUi2.js similarity index 100% rename from dashboard/public/_nuxt/CtJjAUi2.js rename to runtime/ui/_nuxt/CtJjAUi2.js diff --git a/dashboard/public/_nuxt/D3q0vhlH.js b/runtime/ui/_nuxt/D3q0vhlH.js similarity index 100% rename from dashboard/public/_nuxt/D3q0vhlH.js rename to runtime/ui/_nuxt/D3q0vhlH.js diff --git a/dashboard/public/_nuxt/DAsfgl9S.js b/runtime/ui/_nuxt/DAsfgl9S.js similarity index 100% rename from dashboard/public/_nuxt/DAsfgl9S.js rename to runtime/ui/_nuxt/DAsfgl9S.js diff --git a/dashboard/public/_nuxt/DQAL75xK.js b/runtime/ui/_nuxt/DQAL75xK.js similarity index 100% rename from dashboard/public/_nuxt/DQAL75xK.js rename to runtime/ui/_nuxt/DQAL75xK.js diff --git a/dashboard/public/_nuxt/DUDTy2Jn.js b/runtime/ui/_nuxt/DUDTy2Jn.js similarity index 100% rename from dashboard/public/_nuxt/DUDTy2Jn.js rename to runtime/ui/_nuxt/DUDTy2Jn.js diff --git a/dashboard/public/_nuxt/DahUvDF2.js b/runtime/ui/_nuxt/DahUvDF2.js similarity index 100% rename from dashboard/public/_nuxt/DahUvDF2.js rename to runtime/ui/_nuxt/DahUvDF2.js diff --git a/dashboard/public/_nuxt/Dd0ap0OJ.js b/runtime/ui/_nuxt/Dd0ap0OJ.js similarity index 100% rename from dashboard/public/_nuxt/Dd0ap0OJ.js rename to runtime/ui/_nuxt/Dd0ap0OJ.js diff --git a/dashboard/public/_nuxt/Du-6i27Y.js b/runtime/ui/_nuxt/Du-6i27Y.js similarity index 100% rename from dashboard/public/_nuxt/Du-6i27Y.js rename to runtime/ui/_nuxt/Du-6i27Y.js diff --git a/dashboard/public/_nuxt/KAQ4TgpA.js b/runtime/ui/_nuxt/KAQ4TgpA.js similarity index 100% rename from dashboard/public/_nuxt/KAQ4TgpA.js rename to runtime/ui/_nuxt/KAQ4TgpA.js diff --git a/dashboard/public/_nuxt/TwemojiMozilla-flags.B12sb_Bp.woff2 b/runtime/ui/_nuxt/TwemojiMozilla-flags.B12sb_Bp.woff2 similarity index 100% rename from dashboard/public/_nuxt/TwemojiMozilla-flags.B12sb_Bp.woff2 rename to runtime/ui/_nuxt/TwemojiMozilla-flags.B12sb_Bp.woff2 diff --git a/dashboard/public/_nuxt/ZxKQ9y5G.js b/runtime/ui/_nuxt/ZxKQ9y5G.js similarity index 100% rename from dashboard/public/_nuxt/ZxKQ9y5G.js rename to runtime/ui/_nuxt/ZxKQ9y5G.js diff --git a/dashboard/public/_nuxt/_v7-ePgP.js b/runtime/ui/_nuxt/_v7-ePgP.js similarity index 100% rename from dashboard/public/_nuxt/_v7-ePgP.js rename to runtime/ui/_nuxt/_v7-ePgP.js diff --git a/dashboard/public/_nuxt/builds/latest.json b/runtime/ui/_nuxt/builds/latest.json similarity index 100% rename from dashboard/public/_nuxt/builds/latest.json rename to runtime/ui/_nuxt/builds/latest.json diff --git a/dashboard/public/_nuxt/builds/meta/6951ff71-94c6-4cf0-ae64-f6eed15cc18d.json b/runtime/ui/_nuxt/builds/meta/6951ff71-94c6-4cf0-ae64-f6eed15cc18d.json similarity index 100% rename from dashboard/public/_nuxt/builds/meta/6951ff71-94c6-4cf0-ae64-f6eed15cc18d.json rename to runtime/ui/_nuxt/builds/meta/6951ff71-94c6-4cf0-ae64-f6eed15cc18d.json diff --git a/dashboard/public/_nuxt/entry.A2e2demF.css b/runtime/ui/_nuxt/entry.A2e2demF.css similarity index 100% rename from dashboard/public/_nuxt/entry.A2e2demF.css rename to runtime/ui/_nuxt/entry.A2e2demF.css diff --git a/dashboard/public/_nuxt/error-404.ajhBH0J0.css b/runtime/ui/_nuxt/error-404.ajhBH0J0.css similarity index 100% rename from dashboard/public/_nuxt/error-404.ajhBH0J0.css rename to runtime/ui/_nuxt/error-404.ajhBH0J0.css diff --git a/dashboard/public/_nuxt/error-500.CkMssXr1.css b/runtime/ui/_nuxt/error-500.CkMssXr1.css similarity index 100% rename from dashboard/public/_nuxt/error-500.CkMssXr1.css rename to runtime/ui/_nuxt/error-500.CkMssXr1.css diff --git a/dashboard/public/_nuxt/mHCEPkxl.js b/runtime/ui/_nuxt/mHCEPkxl.js similarity index 100% rename from dashboard/public/_nuxt/mHCEPkxl.js rename to runtime/ui/_nuxt/mHCEPkxl.js diff --git a/dashboard/public/_nuxt/vESnrIlh.js b/runtime/ui/_nuxt/vESnrIlh.js similarity index 100% rename from dashboard/public/_nuxt/vESnrIlh.js rename to runtime/ui/_nuxt/vESnrIlh.js diff --git a/dashboard/public/_nuxt/z4Bt3ftW.js b/runtime/ui/_nuxt/z4Bt3ftW.js similarity index 100% rename from dashboard/public/_nuxt/z4Bt3ftW.js rename to runtime/ui/_nuxt/z4Bt3ftW.js diff --git a/dashboard/public/apple-touch-icon-180x180.png b/runtime/ui/apple-touch-icon-180x180.png similarity index 100% rename from dashboard/public/apple-touch-icon-180x180.png rename to runtime/ui/apple-touch-icon-180x180.png diff --git a/dashboard/public/config.js b/runtime/ui/config.js similarity index 100% rename from dashboard/public/config.js rename to runtime/ui/config.js diff --git a/dashboard/public/favicon.ico b/runtime/ui/favicon.ico similarity index 100% rename from dashboard/public/favicon.ico rename to runtime/ui/favicon.ico diff --git a/dashboard/public/favicon.svg b/runtime/ui/favicon.svg similarity index 100% rename from dashboard/public/favicon.svg rename to runtime/ui/favicon.svg diff --git a/dashboard/public/index.html b/runtime/ui/index.html similarity index 100% rename from dashboard/public/index.html rename to runtime/ui/index.html diff --git a/dashboard/public/maskable-icon-512x512.png b/runtime/ui/maskable-icon-512x512.png similarity index 100% rename from dashboard/public/maskable-icon-512x512.png rename to runtime/ui/maskable-icon-512x512.png diff --git a/dashboard/public/pwa-192x192.png b/runtime/ui/pwa-192x192.png similarity index 100% rename from dashboard/public/pwa-192x192.png rename to runtime/ui/pwa-192x192.png diff --git a/dashboard/public/pwa-512x512.png b/runtime/ui/pwa-512x512.png similarity index 100% rename from dashboard/public/pwa-512x512.png rename to runtime/ui/pwa-512x512.png diff --git a/dashboard/public/pwa-64x64.png b/runtime/ui/pwa-64x64.png similarity index 100% rename from dashboard/public/pwa-64x64.png rename to runtime/ui/pwa-64x64.png diff --git a/scripts/config_utils.sh b/scripts/config_utils.sh index de9e95c..87cd6a4 100644 --- a/scripts/config_utils.sh +++ b/scripts/config_utils.sh @@ -1,16 +1,36 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail trim_value() { local value="$1" echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' } +# ========================= +# 安全写入:避免重复块 +# ========================= +remove_block_if_exists() { + local file="$1" + local marker="$2" + + [ -f "$file" ] || return 0 + + # 删除已有 block(从 marker 到文件结束) + if grep -q "$marker" "$file"; then + sed -i "/$marker/,\$d" "$file" + fi +} + +# ========================= +# TUN 配置 +# ========================= apply_tun_config() { local config_path="$1" + local enable="${CLASH_TUN_ENABLE:-false}" - if [ "$enable" != "true" ]; then - return 0 - fi + [ "$enable" = "true" ] || return 0 + + remove_block_if_exists "$config_path" "# ==== TUN CONFIG START ====" local stack="${CLASH_TUN_STACK:-system}" local auto_route="${CLASH_TUN_AUTO_ROUTE:-true}" @@ -22,65 +42,91 @@ apply_tun_config() { { echo "" + echo "# ==== TUN CONFIG START ====" echo "tun:" echo " enable: true" echo " stack: ${stack}" echo " auto-route: ${auto_route}" echo " auto-redirect: ${auto_redirect}" echo " strict-route: ${strict_route}" - if [ -n "$device" ]; then - echo " device: ${device}" - fi - if [ -n "$mtu" ]; then - echo " mtu: ${mtu}" - fi + + [ -n "$device" ] && echo " device: ${device}" + [ -n "$mtu" ] && echo " mtu: ${mtu}" + if [ -n "$dns_hijack" ]; then echo " dns-hijack:" IFS=',' read -r -a hijacks <<< "$dns_hijack" for item in "${hijacks[@]}"; do - local trimmed - trimmed=$(trim_value "$item") - if [ -n "$trimmed" ]; then - echo " - ${trimmed}" - fi + item="$(trim_value "$item")" + [ -n "$item" ] && echo " - ${item}" done fi + + echo "# ==== TUN CONFIG END ====" } >> "$config_path" } +# ========================= +# MIXIN 配置 +# ========================= apply_mixin_config() { local config_path="$1" - local base_dir="${2:-$Server_Dir}" - local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/conf/mixin.d}" + local base_dir="$2" + + local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/config/mixin.d}" local mixin_paths=() + remove_block_if_exists "$config_path" "# ==== MIXIN CONFIG START ====" + + # 用户手动指定优先 if [ -n "${CLASH_MIXIN_PATHS:-}" ]; then IFS=',' read -r -a mixin_paths <<< "$CLASH_MIXIN_PATHS" fi + # 自动扫描目录(补充) if [ -d "$mixin_dir" ]; then while IFS= read -r -d '' file; do mixin_paths+=("$file") - done < <(find "$mixin_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -z) + done < <( + find "$mixin_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) \ + -print0 | sort -z + ) fi + # 去重 + local uniq_paths=() + local seen="" + for path in "${mixin_paths[@]}"; do - local trimmed - trimmed=$(trim_value "$path") - if [ -z "$trimmed" ]; then - continue + path="$(trim_value "$path")" + [ -z "$path" ] && continue + + # 相对路径转绝对 + if [ "${path:0:1}" != "/" ]; then + path="$base_dir/$path" fi - if [ "${trimmed:0:1}" != "/" ]; then - trimmed="$base_dir/$trimmed" - fi - if [ -f "$trimmed" ]; then - { - echo "" - echo "# ---- mixin: ${trimmed} ----" - cat "$trimmed" - } >> "$config_path" - else - echo "[WARN] Mixin file not found: $trimmed" >&2 + + if [[ "$seen" != *"|$path|"* ]]; then + uniq_paths+=("$path") + seen="${seen}|$path|" fi done -} + + # 写入 + { + echo "" + echo "# ==== MIXIN CONFIG START ====" + + for path in "${uniq_paths[@]}"; do + if [ -f "$path" ]; then + echo "" + echo "# ---- mixin: ${path} ----" + cat "$path" + else + echo "[WARN] Mixin not found: $path" >&2 + fi + done + + echo "# ==== MIXIN CONFIG END ====" + } >> "$config_path" +} \ No newline at end of file diff --git a/scripts/doctor.sh b/scripts/doctor.sh index f2a3d43..becbbcc 100644 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -1,21 +1,6 @@ #!/usr/bin/env bash -set -e +set -euo pipefail -PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -RUNTIME_DIR="$PROJECT_DIR/runtime" +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -echo "=== Clash Doctor ===" - -if [ -f "$RUNTIME_DIR/config.yaml" ]; then - echo "[OK] config exists" -else - echo "[ERROR] config missing" -fi - -if command -v systemctl >/dev/null 2>&1; then - if systemctl is-active --quiet clash-for-linux.service; then - echo "[OK] service running" - else - echo "[WARN] service not running" - fi -fi \ No newline at end of file +exec "$PROJECT_DIR/clashctl" doctor "$@" \ No newline at end of file diff --git a/scripts/generate_config.sh b/scripts/generate_config.sh index 10fe630..aceef0a 100644 --- a/scripts/generate_config.sh +++ b/scripts/generate_config.sh @@ -12,6 +12,7 @@ STATE_FILE="$RUNTIME_DIR/state.env" TMP_DOWNLOAD="$RUNTIME_DIR/subscription.raw.yaml" TMP_NORMALIZED="$RUNTIME_DIR/subscription.normalized.yaml" TMP_PROXY_FRAGMENT="$RUNTIME_DIR/proxy.fragment.yaml" +TMP_CONFIG="$RUNTIME_DIR/config.yaml.tmp" mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR" @@ -64,6 +65,15 @@ generate_secret() { return 0 fi + if [ -s "$RUNTIME_CONFIG" ]; then + local old_secret + old_secret="$(sed -nE 's/^[[:space:]]*secret:[[:space:]]*"?([^"#]+)"?.*$/\1/p' "$RUNTIME_CONFIG" | head -n 1)" + if [ -n "${old_secret:-}" ]; then + echo "$old_secret" + return 0 + fi + fi + if command -v openssl >/dev/null 2>&1; then openssl rand -hex 16 else @@ -103,9 +113,11 @@ apply_controller_to_config() { rm -rf "$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 } @@ -121,11 +133,32 @@ download_subscription() { is_complete_clash_config() { local file="$1" - grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$file" + grep -qE '^[[:space:]]*(proxies:|proxy-providers:|mixed-port:|port:)' "$file" } cleanup_tmp_files() { - rm -f "$TMP_NORMALIZED" "$TMP_PROXY_FRAGMENT" + rm -f "$TMP_PROXY_FRAGMENT" "$TMP_CONFIG" +} + +build_fragment_config() { + local template_file="$1" + local target_file="$2" + + sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT" + + cat "$template_file" > "$target_file" + cat "$TMP_PROXY_FRAGMENT" >> "$target_file" + + sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$target_file" + sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$target_file" + sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$target_file" + sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$target_file" + sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$target_file" +} + +finalize_config() { + local file="$1" + mv -f "$file" "$RUNTIME_CONFIG" } main() { @@ -156,9 +189,10 @@ main() { cp -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED" if is_complete_clash_config "$TMP_NORMALIZED"; then - cp -f "$TMP_NORMALIZED" "$RUNTIME_CONFIG" - apply_controller_to_config "$RUNTIME_CONFIG" - apply_secret_to_config "$RUNTIME_CONFIG" + cp -f "$TMP_NORMALIZED" "$TMP_CONFIG" + apply_controller_to_config "$TMP_CONFIG" + apply_secret_to_config "$TMP_CONFIG" + finalize_config "$TMP_CONFIG" write_state "success" "subscription_full" "subscription_full" cleanup_tmp_files exit 0 @@ -171,22 +205,14 @@ main() { exit 1 fi - sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT" - - cat "$template_file" > "$RUNTIME_CONFIG" - cat "$TMP_PROXY_FRAGMENT" >> "$RUNTIME_CONFIG" - - sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$RUNTIME_CONFIG" - - apply_controller_to_config "$RUNTIME_CONFIG" - apply_secret_to_config "$RUNTIME_CONFIG" + build_fragment_config "$template_file" "$TMP_CONFIG" + apply_controller_to_config "$TMP_CONFIG" + apply_secret_to_config "$TMP_CONFIG" + finalize_config "$TMP_CONFIG" write_state "success" "subscription_fragment_merged" "subscription_fragment" cleanup_tmp_files } +trap cleanup_tmp_files EXIT main "$@" \ No newline at end of file diff --git a/scripts/install_systemd.sh b/scripts/install_systemd.sh index 2dcd33e..77e86f0 100755 --- a/scripts/install_systemd.sh +++ b/scripts/install_systemd.sh @@ -10,14 +10,14 @@ SERVICE_GROUP="${CLASH_SERVICE_GROUP:-root}" RUNTIME_DIR="$PROJECT_DIR/runtime" LOG_DIR="$PROJECT_DIR/logs" -CONF_DIR="$PROJECT_DIR/conf" +CONFIG_DIR="$PROJECT_DIR/config" if [ "$(id -u)" -ne 0 ]; then echo "[ERROR] root required to install systemd unit" >&2 exit 1 fi -install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONF_DIR" +install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONFIG_DIR" "$CONFIG_DIR/mixin.d" cat >"$UNIT_PATH" </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 \ No newline at end of file +exec "$PROJECT_DIR/clashctl" logs "$@" \ No newline at end of file diff --git a/scripts/port_utils.sh b/scripts/port_utils.sh index 789979e..efa2298 100644 --- a/scripts/port_utils.sh +++ b/scripts/port_utils.sh @@ -1,36 +1,49 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail PORT_CHECK_WARNED=${PORT_CHECK_WARNED:-0} +# ========================= +# 判断端口是否被占用(更稳) +# ========================= is_port_in_use() { local port="$1" + if command -v ss >/dev/null 2>&1; then - ss -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1 + ss -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1 return $? fi + if command -v netstat >/dev/null 2>&1; then - netstat -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1 + netstat -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1 return $? fi + if command -v lsof >/dev/null 2>&1; then - lsof -iTCP -sTCP:LISTEN -P -n | awk '{print $9}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1 + lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | awk '{print $9}' | grep -E "[:.]${port}$" >/dev/null 2>&1 return $? fi + if [ "$PORT_CHECK_WARNED" -eq 0 ]; then - echo -e "\033[33m[WARN] 未找到端口检测工具,端口冲突检测可能不准确\033[0m" >&2 + echo "[WARN] no port check tool found (ss/netstat/lsof)" >&2 PORT_CHECK_WARNED=1 fi + return 1 } +# ========================= +# 找可用端口(优化版) +# ========================= find_available_port() { - local start_port=${1:-20000} - local end_port=${2:-65000} + local start="${1:-20000}" + local end="${2:-65000}" local port + # 优先随机尝试 if command -v shuf >/dev/null 2>&1; then - for _ in {1..50}; do - port=$(shuf -i "${start_port}-${end_port}" -n 1) + for _ in {1..30}; do + port=$(shuf -i "${start}-${end}" -n 1) if ! is_port_in_use "$port"; then echo "$port" return 0 @@ -38,7 +51,8 @@ find_available_port() { done fi - for port in $(seq "$start_port" "$end_port"); do + # fallback 顺序扫描(限制范围避免慢) + for port in $(seq "$start" "$((start + 2000))"); do if ! is_port_in_use "$port"; then echo "$port" return 0 @@ -48,43 +62,56 @@ find_available_port() { return 1 } +# ========================= +# 解析端口值(核心函数) +# ========================= resolve_port_value() { local name="$1" local value="$2" local resolved + # auto / 空 if [ -z "$value" ] || [ "$value" = "auto" ]; then - resolved=$(find_available_port) - if [ -z "$resolved" ]; then + resolved=$(find_available_port) || { + echo "[ERROR] ${name} failed to allocate port" >&2 return 1 - fi - echo -e "\033[33m[WARN] ${name} 端口已自动分配为 ${resolved}\033[0m" >&2 + } + echo "[WARN] ${name} auto assigned: ${resolved}" >&2 echo "$resolved" return 0 fi - if [[ "$value" =~ ^[0-9]+$ ]]; then - if is_port_in_use "$value"; then - resolved=$(find_available_port) - if [ -n "$resolved" ]; then - echo -e "\033[33m[WARN] ${name} 端口 ${value} 已被占用,已自动切换为 ${resolved}\033[0m" >&2 - echo "$resolved" - return 0 - fi + # 非数字 + if ! [[ "$value" =~ ^[0-9]+$ ]]; then + echo "[ERROR] invalid port: $value" >&2 + return 1 + fi + + # 被占用 → 自动替换 + if is_port_in_use "$value"; then + resolved=$(find_available_port) + if [ -n "$resolved" ]; then + echo "[WARN] ${name} port ${value} in use, switched to ${resolved}" >&2 + echo "$resolved" + return 0 fi fi echo "$value" } +# ========================= +# 解析 host:port +# ========================= resolve_host_port() { local name="$1" local raw="$2" local default_host="$3" + local host local port - if [ "$raw" = "auto" ] || [ -z "$raw" ]; then + if [ -z "$raw" ] || [ "$raw" = "auto" ]; then host="$default_host" port="auto" else @@ -97,6 +124,10 @@ resolve_host_port() { fi fi + # host 兜底 + [ -z "$host" ] && host="$default_host" + port=$(resolve_port_value "$name" "$port") || return 1 + echo "${host}:${port}" -} +} \ No newline at end of file diff --git a/scripts/resolve_clash.sh b/scripts/resolve_clash.sh index d1edb6f..d24e1aa 100755 --- a/scripts/resolve_clash.sh +++ b/scripts/resolve_clash.sh @@ -1,119 +1,214 @@ -#!/bin/bash - resolve_clash_arch() { local raw_arch="$1" case "$raw_arch" in - x86_64|amd64) - echo "linux-amd64" - ;; - aarch64|arm64) - echo "linux-arm64" - ;; - armv7*|armv7l) - echo "linux-armv7" - ;; - *) - echo "linux-${raw_arch}" - ;; + x86_64|amd64) echo "linux-amd64" ;; + aarch64|arm64) echo "linux-arm64" ;; + armv7*|armv7l) echo "linux-armv7" ;; + *) echo "linux-${raw_arch}" ;; esac } +get_latest_mihomo_version() { + local url="https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" + + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" \ + | grep '"tag_name"' \ + | sed -E 's/.*"([^"]+)".*/\1/' \ + | head -n 1 + elif command -v wget >/dev/null 2>&1; then + wget -qO- "$url" \ + | grep '"tag_name"' \ + | sed -E 's/.*"([^"]+)".*/\1/' \ + | head -n 1 + fi +} + download_clash_bin() { local server_dir="$1" local detected_arch="$2" + local resolved_arch + local version local download_url + local download_target local archive_file + local tmp_bin + + resolved_arch="$(resolve_clash_arch "$detected_arch")" - resolved_arch=$(resolve_clash_arch "$detected_arch") if [ -z "$resolved_arch" ]; then - echo -e "\033[33m[WARN] 无法识别 CPU 架构,跳过 Clash 内核自动下载\033[0m" + echo "[WARN] 无法识别 CPU 架构" >&2 return 1 fi - if [ "${CLASH_AUTO_DOWNLOAD:-auto}" = "false" ]; then + version="${MIHOMO_VERSION:-}" + if [ -z "$version" ]; then + version="$(get_latest_mihomo_version || true)" + fi + + if [ -z "$version" ]; then + echo "[ERROR] 无法获取 Mihomo 版本" >&2 return 1 fi - local _default_url="https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz" - download_url="${CLASH_DOWNLOAD_URL_TEMPLATE:-$_default_url}" - if [ -z "$download_url" ]; then - echo -e "\033[33m[WARN] 未设置 CLASH_DOWNLOAD_URL_TEMPLATE,跳过 Clash 内核自动下载\033[0m" + if [ -z "${CLASH_DOWNLOAD_URL_TEMPLATE:-}" ]; then + echo "[ERROR] CLASH_DOWNLOAD_URL_TEMPLATE 未设置" >&2 return 1 fi - download_url="${download_url//\{arch\}/${resolved_arch}}" + download_url="${CLASH_DOWNLOAD_URL_TEMPLATE//\{arch\}/${resolved_arch}}" + download_url="${download_url//\{version\}/${version}}" + download_target="${server_dir}/bin/clash-${resolved_arch}" - archive_file="${server_dir}/temp/clash-${resolved_arch}.download" + archive_file="${server_dir}/runtime/.clash_download.tmp" + tmp_bin="${server_dir}/runtime/.clash_bin.tmp" - mkdir -p "${server_dir}/bin" "${server_dir}/temp" + mkdir -p "${server_dir}/bin" "${server_dir}/runtime" + rm -f "$archive_file" "$tmp_bin" + + echo "[INFO] downloading: $download_url" + + # ========================= + # 下载 + # ========================= if command -v curl >/dev/null 2>&1; then - curl -L -sS -o "${archive_file}" "${download_url}" + if ! curl -fL -sS -o "$archive_file" "$download_url"; then + echo "[ERROR] 下载失败: $download_url" >&2 + return 1 + fi elif command -v wget >/dev/null 2>&1; then - wget -q -O "${archive_file}" "${download_url}" + if ! wget -q -O "$archive_file" "$download_url"; then + echo "[ERROR] 下载失败: $download_url" >&2 + return 1 + fi else - echo -e "\033[33m[WARN] 未找到 curl 或 wget,无法自动下载 Clash 内核\033[0m" + echo "[ERROR] 未找到 curl 或 wget" >&2 return 1 fi - if [ -f "${archive_file}" ]; then - if gzip -t "${archive_file}" >/dev/null 2>&1; then - gzip -dc "${archive_file}" >"${download_target}" - else - mv "${archive_file}" "${download_target}" - fi - chmod +x "${download_target}" - echo "${download_target}" - return 0 + # ========================= + # 基础校验(防 404 / HTML) + # ========================= + if [ ! -s "$archive_file" ]; then + echo "[ERROR] 下载文件为空" >&2 + return 1 fi - echo -e "\033[33m[WARN] Clash 内核自动下载失败\033[0m" - return 1 + if head -c 200 "$archive_file" | grep -qiE "not found|html"; then + echo "[ERROR] 下载内容疑似错误页面(404/HTML)" >&2 + return 1 + fi + + # ========================= + # 解压 / 直写 + # ========================= + if gzip -t "$archive_file" >/dev/null 2>&1; then + if ! gzip -dc "$archive_file" > "$tmp_bin"; then + echo "[ERROR] gzip 解压失败" >&2 + return 1 + fi + else + cp "$archive_file" "$tmp_bin" + fi + + # ========================= + # ELF 校验(关键) + # ========================= + if ! file "$tmp_bin" | grep -q "ELF"; then + echo "[ERROR] 非有效 ELF 二进制" >&2 + echo "[DEBUG] file result: $(file "$tmp_bin")" >&2 + return 1 + fi + + chmod +x "$tmp_bin" + mv "$tmp_bin" "$download_target" + + rm -f "$archive_file" + + echo "[OK] downloaded: $download_target" + echo "$download_target" } resolve_clash_bin() { local server_dir="$1" local detected_arch="$2" + local resolved_arch local candidates=() local candidate local downloaded_bin + local mode if [ -n "${CLASH_BIN:-}" ]; then if [ -x "$CLASH_BIN" ]; then echo "$CLASH_BIN" return 0 fi - echo -e "\033[31m[ERROR] CLASH_BIN 指定的文件不可执行: $CLASH_BIN\033[0m" + echo "[ERROR] CLASH_BIN 不可执行: $CLASH_BIN" >&2 return 1 fi - resolved_arch=$(resolve_clash_arch "$detected_arch") + resolved_arch="$(resolve_clash_arch "$detected_arch")" + if [ -n "$resolved_arch" ]; then candidates+=("${server_dir}/bin/clash-${resolved_arch}") fi + candidates+=( "${server_dir}/bin/clash-${detected_arch}" "${server_dir}/bin/clash" ) - for candidate in "${candidates[@]}"; do - if [ -x "$candidate" ]; then - echo "$candidate" - return 0 - fi - done + mode="${CLASH_AUTO_DOWNLOAD:-auto}" - if downloaded_bin=$(download_clash_bin "$server_dir" "$detected_arch"); then - echo "$downloaded_bin" - return 0 - fi + case "$mode" in + false) + for candidate in "${candidates[@]}"; do + if [ -x "$candidate" ]; then + echo "$candidate" + return 0 + fi + done + ;; - echo -e "\033[31m\n[ERROR] 未找到可用的 Clash 二进制。\033[0m" - echo -e "请将对应架构的二进制放入: $server_dir/bin/" - echo -e "可用命名示例: clash-${resolved_arch} 或 clash-${detected_arch}" - echo -e "或通过 CLASH_BIN 指定自定义路径。" + auto) + for candidate in "${candidates[@]}"; do + if [ -x "$candidate" ]; then + 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 -} +} \ No newline at end of file diff --git a/scripts/run_clash.sh b/scripts/run_clash.sh index 4c1bc7f..cf1ecb3 100644 --- a/scripts/run_clash.sh +++ b/scripts/run_clash.sh @@ -12,7 +12,9 @@ mkdir -p "$RUNTIME_DIR" "$LOG_DIR" FOREGROUND=false DAEMON=false -# 解析参数 +# ========================= +# 参数解析 +# ========================= for arg in "$@"; do case "$arg" in --foreground) FOREGROUND=true ;; @@ -29,6 +31,14 @@ if [ "$FOREGROUND" = true ] && [ "$DAEMON" = true ]; then exit 2 fi +if [ "$FOREGROUND" = false ] && [ "$DAEMON" = false ]; then + echo "[ERROR] Must specify --foreground or --daemon" >&2 + exit 2 +fi + +# ========================= +# 基础校验 +# ========================= if [ ! -s "$CONFIG_FILE" ]; then echo "[ERROR] runtime config not found: $CONFIG_FILE" >&2 exit 2 @@ -39,6 +49,9 @@ if grep -q '\${' "$CONFIG_FILE"; then exit 2 fi +# ========================= +# 加载依赖 +# ========================= # shellcheck disable=SC1091 source "$PROJECT_DIR/scripts/get_cpu_arch.sh" # shellcheck disable=SC1091 @@ -46,40 +59,49 @@ source "$PROJECT_DIR/scripts/resolve_clash.sh" # shellcheck disable=SC1091 source "$PROJECT_DIR/scripts/service_lib.sh" +# ========================= +# 获取二进制 +# ========================= CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")" if [ ! -x "$CLASH_BIN" ]; then - echo "[ERROR] clash binary not found or not executable: $CLASH_BIN" >&2 + echo "[ERROR] clash binary not executable: $CLASH_BIN" >&2 exit 2 fi -test_config() { - local bin="$1" - local config="$2" - local runtime_dir="$3" - "$bin" -d "$runtime_dir" -t -f "$config" >/dev/null 2>&1 -} - -if ! test_config "$CLASH_BIN" "$CONFIG_FILE" "$RUNTIME_DIR"; then - echo "[ERROR] config test failed: $CONFIG_FILE" >&2 +# ========================= +# config 测试(唯一一次) +# ========================= +if ! "$CLASH_BIN" -t -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >/dev/null 2>&1; then + echo "[ERROR] clash config test failed: $CONFIG_FILE" >&2 + write_run_state "failed" "config-test" exit 2 fi -# systemd 模式 +# ========================= +# 前台模式(systemd) +# ========================= if [ "$FOREGROUND" = true ]; then write_run_state "running" "systemd" exec "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" fi -# script / daemon 模式 -if [ "$DAEMON" = true ]; then - nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 & - pid=$! - echo "$pid" > "$PID_FILE" - write_run_state "running" "script" "$pid" - echo "[OK] Clash started in script mode, pid=$pid" +# ========================= +# 后台模式(script) +# ========================= +cleanup_dead_pid + +if is_script_running; then + pid="$(read_pid 2>/dev/null || true)" + echo "[INFO] clash already running, pid=${pid:-unknown}" exit 0 fi -echo "[ERROR] Must specify --foreground or --daemon" >&2 -exit 2 \ No newline at end of file +nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 & + +pid=$! +echo "$pid" > "$PID_FILE" + +write_run_state "running" "script" "$pid" + +echo "[OK] Clash started in script mode, pid=$pid" \ No newline at end of file diff --git a/scripts/service_lib.sh b/scripts/service_lib.sh index 2dda03e..1b338db 100644 --- a/scripts/service_lib.sh +++ b/scripts/service_lib.sh @@ -9,6 +9,9 @@ SERVICE_NAME="clash-for-linux.service" mkdir -p "$RUNTIME_DIR" +# ========================= +# 基础能力 +# ========================= has_systemd() { command -v systemctl >/dev/null 2>&1 } @@ -19,17 +22,39 @@ service_unit_exists() { } read_pid() { - [ -f "$PID_FILE" ] || return 1 - cat "$PID_FILE" + [ -s "$PID_FILE" ] || return 1 + tr -d '[:space:]' < "$PID_FILE" +} + +is_pid_running() { + local pid="$1" + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null } is_script_running() { local pid pid="$(read_pid 2>/dev/null || true)" - [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null + is_pid_running "$pid" } +# ========================= +# 清理僵尸 PID(关键) +# ========================= +cleanup_dead_pid() { + local pid + pid="$(read_pid 2>/dev/null || true)" + + if [ -n "${pid:-}" ] && ! is_pid_running "$pid"; then + rm -f "$PID_FILE" + fi +} + +# ========================= +# 模式检测(统一) +# ========================= detect_mode() { + cleanup_dead_pid + if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then echo "systemd" elif is_script_running; then @@ -41,59 +66,65 @@ detect_mode() { fi } -write_run_state() { - local status="$1" - local mode="${2:-unknown}" - local pid="${3:-}" +# ========================= +# state 写入(唯一实现) +# ========================= +write_state_kv() { + local key="$1" + local value="$2" + mkdir -p "$RUNTIME_DIR" touch "$STATE_FILE" - if grep -q '^LAST_RUN_STATUS=' "$STATE_FILE" 2>/dev/null; then - sed -i -E "s/^LAST_RUN_STATUS=.*/LAST_RUN_STATUS=${status}/" "$STATE_FILE" + if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then + sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_FILE" else - echo "LAST_RUN_STATUS=${status}" >> "$STATE_FILE" - fi - - if grep -q '^LAST_RUN_MODE=' "$STATE_FILE" 2>/dev/null; then - sed -i -E "s/^LAST_RUN_MODE=.*/LAST_RUN_MODE=${mode}/" "$STATE_FILE" - else - echo "LAST_RUN_MODE=${mode}" >> "$STATE_FILE" - fi - - if grep -q '^LAST_RUN_AT=' "$STATE_FILE" 2>/dev/null; then - sed -i -E "s/^LAST_RUN_AT=.*/LAST_RUN_AT=$(date -Iseconds)/" "$STATE_FILE" - else - echo "LAST_RUN_AT=$(date -Iseconds)" >> "$STATE_FILE" - fi - - if [ -n "$pid" ]; then - if grep -q '^LAST_RUN_PID=' "$STATE_FILE" 2>/dev/null; then - sed -i -E "s/^LAST_RUN_PID=.*/LAST_RUN_PID=${pid}/" "$STATE_FILE" - else - echo "LAST_RUN_PID=${pid}" >> "$STATE_FILE" - fi + echo "${key}=${value}" >> "$STATE_FILE" fi } +write_run_state() { + local status="${1:-unknown}" + local mode="${2:-unknown}" + local pid="${3:-}" + + write_state_kv "LAST_RUN_STATUS" "$status" + write_state_kv "LAST_RUN_MODE" "$mode" + write_state_kv "LAST_RUN_AT" "$(date -Iseconds)" + + if [ -n "$pid" ]; then + write_state_kv "LAST_RUN_PID" "$pid" + fi +} + +# ========================= +# systemd 模式 +# ========================= start_via_systemd() { systemctl start "$SERVICE_NAME" } stop_via_systemd() { - systemctl stop "$SERVICE_NAME" + systemctl stop "$SERVICE_NAME" || true + cleanup_dead_pid write_run_state "stopped" "systemd" - rm -f "$PID_FILE" } restart_via_systemd() { systemctl restart "$SERVICE_NAME" } +# ========================= +# script 模式 +# ========================= start_via_script() { + cleanup_dead_pid + if is_script_running; then - echo "[INFO] clash already running (script mode)" + echo "[INFO] clash already running (script)" return 0 fi + "$PROJECT_DIR/scripts/run_clash.sh" --daemon } @@ -101,11 +132,19 @@ stop_via_script() { local pid pid="$(read_pid 2>/dev/null || true)" - if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then + if [ -n "${pid:-}" ] && is_pid_running "$pid"; then echo "[INFO] stopping clash pid=$pid" - kill "$pid" - sleep 1 - if kill -0 "$pid" 2>/dev/null; then + + kill "$pid" 2>/dev/null || true + + 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 fi fi diff --git a/scripts/status.sh b/scripts/status.sh index c31403d..848f4ef 100644 --- a/scripts/status.sh +++ b/scripts/status.sh @@ -1,28 +1,6 @@ #!/usr/bin/env bash -set -e +set -euo pipefail -PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# shellcheck disable=SC1091 -source "$PROJECT_DIR/scripts/service_lib.sh" - -mode="$(detect_mode)" - -echo "=== Clash Status ===" -echo "Project : $PROJECT_DIR" -echo "Mode : $mode" - -case "$mode" in - systemd) - echo "Running : yes (systemd)" - ;; - script) - echo "Running : yes (script)" - ;; - systemd-installed) - echo "Running : no (installed but not started)" - ;; - *) - echo "Running : no" - ;; -esac \ No newline at end of file +exec "$PROJECT_DIR/clashctl" status "$@" \ No newline at end of file diff --git a/scripts/stop_clash.sh b/scripts/stop_clash.sh index d045ffe..1c76cd4 100644 --- a/scripts/stop_clash.sh +++ b/scripts/stop_clash.sh @@ -1,23 +1,6 @@ #!/usr/bin/env bash -set -e +set -euo pipefail -PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# shellcheck disable=SC1091 -source "$PROJECT_DIR/scripts/service_lib.sh" - -mode="$(detect_mode)" - -case "$mode" in - systemd) - stop_via_systemd - echo "[OK] stopped via systemd" - ;; - script) - stop_via_script - echo "[OK] stopped via script" - ;; - *) - echo "[WARN] nothing is running" - ;; -esac \ No newline at end of file +exec "$PROJECT_DIR/clashctl" stop "$@" \ No newline at end of file diff --git a/systemd/clash-for-linux.service b/systemd/clash-for-linux.service index 0cab98e..fe4d0ad 100644 --- a/systemd/clash-for-linux.service +++ b/systemd/clash-for-linux.service @@ -1,19 +1,33 @@ [Unit] -Description=Clash for Linux -After=network.target +Description=Clash for Linux (Mihomo) +Documentation=https://github.com/wnlen/clash-for-linux +After=network-online.target nss-lookup.target +Wants=network-online.target +StartLimitIntervalSec=0 +StartLimitBurst=10 [Service] Type=simple -EnvironmentFile=-/etc/default/clash-for-linux -Environment=CLASH_HOME=/opt/clash-for-linux -ExecStart=/bin/bash -c 'exec /bin/bash "${CLASH_HOME}/start.sh"' -ExecStop=/bin/bash -c 'exec /bin/bash "${CLASH_HOME}/shutdown.sh"' -Restart=on-failure -RestartSec=5 -User=clash -Group=clash -PIDFile=%E{CLASH_HOME}/temp/clash.pid -Environment=CLASH_ENV_FILE=%E{CLASH_HOME}/temp/clash-for-linux.sh +User=root +Group=root +WorkingDirectory=/opt/clash-for-linux +Environment=HOME=/root + +ExecStart=/bin/bash /opt/clash-for-linux/clashctl start +ExecStop=/bin/bash /opt/clash-for-linux/clashctl --from-systemd stop + +Restart=always +RestartSec=5s + +KillMode=mixed +TimeoutStartSec=120 +TimeoutStopSec=30 + +StandardOutput=journal +StandardError=journal + +UMask=0022 +NoNewPrivileges=false [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target \ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh index 2ae7030..4b61277 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -82,7 +82,7 @@ fi # 2) stop process by pid file from all likely dirs for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do [ -n "$d" ] || continue - PID_FILE="$d/temp/clash.pid" + PID_FILE="$d/runtime/clash.pid" if [ -f "$PID_FILE" ]; then PID="$(cat "$PID_FILE" 2>/dev/null || true)" if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then