#!/usr/bin/env bash set -euo pipefail Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=scripts/ui.sh . "$Server_Dir/scripts/ui.sh" Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" Service_Name="clash-for-linux" Service_User="root" Service_Group="root" # ========================= # 基础校验 # ========================= ui_ok "[1/3] 准备环境..." if [ "$(id -u)" -ne 0 ]; then die "需要 root 权限" fi # ui_ok "已确认 root 权限" if [ ! -f "${Server_Dir}/.env" ]; then die_with_reason \ ".env 文件不存在" \ "缺少文件: ${Server_Dir}/.env" \ "请确认项目目录完整" fi # ui_ok "已检测到 .env 配置文件" # ========================= # 同步文件 # ========================= # ui_blank # ui_info "[2/5] 初始化目录" mkdir -p "$Install_Dir" # ui_ok "安装目录已就绪: $Install_Dir" 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 # ========================= # 目录初始化 # ========================= mkdir -p \ "$Install_Dir/runtime" \ "$Install_Dir/logs" \ "$Install_Dir/config/mixin.d" # ui_ok "runtime 目录已创建" # ui_ok "logs 目录已创建" # ui_ok "mixin 目录已创建" # ========================= # 加载 env # ========================= # shellcheck disable=SC1090 # ui_blank ui_ok "[2/3] 生成配置..." source "$Install_Dir/.env" # ui_ok ".env 配置已加载" # shellcheck disable=SC1090 source "$Install_Dir/scripts/get_cpu_arch.sh" # ui_ok "CPU 架构识别成功: ${CpuArch:-unknown}" # shellcheck disable=SC1090 source "$Install_Dir/scripts/resolve_clash.sh" # ui_blank # ui_info "准备内核" if ! bash "$Install_Dir/scripts/resolve_clash.sh"; then ui_error "Clash 内核准备失败" ui_fix_block \ "resolve_clash.sh 执行失败" \ "请检查下载地址或网络连接" ui_debug_block \ "bash $Install_Dir/scripts/resolve_clash.sh" exit 1 fi write_env_value() { local key="$1" local value="$2" local env_file="$Install_Dir/.env" local escaped="${value//\\/\\\\}" escaped="${escaped//&/\\&}" escaped="${escaped//|/\\|}" escaped="${escaped//\'/\'\\\'\'}" if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$env_file"; then sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*$|export ${key}='${escaped}'|g" "$env_file" else printf "export %s='%s'\n" "$key" "$value" >> "$env_file" fi } if [ -z "${CLASH_SECRET:-}" ]; then if command -v openssl >/dev/null 2>&1; then CLASH_SECRET="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16)" else CLASH_SECRET="$(tr -dc 'A-Za-z0-9' /dev/null \ || curl -fsS --max-time 5 ip.sb 2>/dev/null \ || curl -fsS --max-time 5 api.ipify.org 2>/dev/null \ || true } get_lan_ip() { hostname -I 2>/dev/null | awk '{print $1}' || true } show_install_usage() { cat <<'EOF' 用法: clashctl 命令 [选项] 指令: on 开启代理 off 关闭代理 start 启动 Clash stop 停止 Clash restart 重启并自动应用当前配置 status 查看当前状态 update 更新到最新版本并自动应用配置 mode 查看当前运行模式(systemd/script/none) ui 输出 Dashboard 地址 secret 输出当前 secret doctor 健康检查 logs [-f] [-n 100] 查看日志 sub show|update 查看订阅地址 / 输入或更新订阅并立即生效 tun status|on|off 查看/启用/关闭 Tun mixin status|on|off 查看/启用/关闭 Mixin 选 -h, --help 显示帮助信息 EOF } show_dashboard_info() { local secret="${1:-}" local public_ip="${2:-}" local controller_addr="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" local host="${controller_addr%:*}" local port="${controller_addr##*:}" local lan_ip="" lan_ip="$(get_lan_ip)" local local_ui="http://127.0.0.1:${port}/ui" local lan_ui="" local public_ui="" local custom_ui="${CLASH_DASHBOARD_PUBLIC_URL:-}" [ -n "$lan_ip" ] && lan_ui="http://${lan_ip}:${port}/ui" [ -n "$public_ip" ] && public_ui="http://${public_ip}:${port}/ui" ui_blank ui_summary_begin "😼 Clash Web 控制台" ui_summary_row "🔓 注意放行端口" "$port" [ -n "$lan_ui" ] && ui_summary_row "💻 内网" "$lan_ui" [ -n "$custom_ui" ] && ui_summary_row "🌐 公共" "$custom_ui" [ -n "$public_ui" ] && ui_summary_row "🌏 公网" "$public_ui" [ -n "$secret" ] && ui_summary_row "🔑 密钥" "$secret" ui_summary_end } wait_dashboard_ready() { local host="$1" local port="${2:-9090}" local max_retry="${3:-20}" local i for ((i=1; i<=max_retry; i++)); do if curl -fsS --max-time 2 "http://${host}:${port}/ui/" >/dev/null 2>&1; then return 0 fi sleep 1 done return 1 } prompt_and_apply_subscription() { local sub_url="" local secret="" local public_ip="" local retry_choice="" while true; do echo read -r -p "👉 请输入要添加的订阅链接:" sub_url if [ -z "${sub_url:-}" ]; then ui_warn "已跳过订阅设置,可稍后使用 clashctl sub 进行配置" return 0 fi write_env_value "CLASH_URL" "$sub_url" echo "⏳ 正在下载订阅..." if ! "$Install_Dir/scripts/generate_config.sh" >/dev/null 2>&1; then ui_error "订阅不可用或转换失败" read -r -p "是否重新输入订阅链接?[Y/n]: " retry_choice case "${retry_choice:-Y}" in n|N) ui_warn "已跳过订阅设置,可稍后使用 clashctl sub 进行配置" return 0 ;; *) continue ;; esac fi if command -v systemctl >/dev/null 2>&1; then systemctl restart "${Service_Name}.service" else "$Install_Dir/scripts/run_clash.sh" --daemon fi echo "🎉 订阅添加成功:$sub_url" echo "🔥 订阅已生效" # show_dashboard_info "$secret" "$public_ip" return 0 done } # ========================= # 内核检查 # ========================= if ! resolve_clash_bin "$Install_Dir" "${CpuArch:-}" >/dev/null 2>&1; then die_with_reason \ "Clash 内核未就绪" \ "二进制校验失败" \ "请检查下载结果或 CPU 架构是否匹配" fi # ui_ok "Clash 内核校验通过" # ========================= # 安装 clashctl # ========================= # ===== 安装 / 覆盖 clashctl 命令 ===== OLD_CLASHCTL_PATH="$(command -v clashctl 2>/dev/null || true)" OLD_CLASHCTL_REAL="" if [ -n "$OLD_CLASHCTL_PATH" ]; then OLD_CLASHCTL_REAL="$(readlink -f "$OLD_CLASHCTL_PATH" 2>/dev/null || true)" fi # 如果存在旧版本,打印提示 if [ -n "$OLD_CLASHCTL_REAL" ] && [ "$OLD_CLASHCTL_REAL" != "$(readlink -f "$Install_Dir/clashctl")" ]; then ui_warn "检测到旧版本 clashctl: $OLD_CLASHCTL_REAL" ui_info "将覆盖为当前版本: $Install_Dir/clashctl" fi # 强制清理旧入口 cleanup_legacy_clashctl() { rm -f /usr/local/bin/clashctl 2>/dev/null || true rm -f /usr/bin/clashctl 2>/dev/null || true [ -f /root/clashctl ] && rm -f /root/clashctl 2>/dev/null || true [ -f "$HOME/clashctl" ] && rm -f "$HOME/clashctl" 2>/dev/null || true [ -d /root/clashctl ] && rm -rf /root/clashctl 2>/dev/null || true [ -d "$HOME/clashctl" ] && rm -rf "$HOME/clashctl" 2>/dev/null || true } # ===== 安装/覆盖 clashctl 命令 ===== cleanup_legacy_clashctl chmod +x "$Install_Dir/clashctl" ln -s "$Install_Dir/clashctl" /usr/local/bin/clashctl # 清理当前 shell 污染(关键) unset -f clashctl clashhelp clashlog clashmixin clashoff clashon clashproxy clashrestart clashsecret clashstatus clashsub clashtun clashui clashupgrade 2>/dev/null || true unalias clashctl 2>/dev/null || true hash -r # 校验 NEW_REAL="$(readlink -f /usr/local/bin/clashctl 2>/dev/null || true)" EXPECT_REAL="$(readlink -f "$Install_Dir/clashctl" 2>/dev/null || true)" if [ "$NEW_REAL" != "$EXPECT_REAL" ]; then ui_error "clashctl 安装失败" >&2 exit 1 fi # 清理 shell 缓存(非常关键) hash -r # 强校验(防止假覆盖) NEW_CLASHCTL_REAL="$(readlink -f /usr/local/bin/clashctl 2>/dev/null || true)" EXPECTED_CLASHCTL_REAL="$(readlink -f "$Install_Dir/clashctl" 2>/dev/null || true)" if [ "$NEW_CLASHCTL_REAL" != "$EXPECTED_CLASHCTL_REAL" ]; then ui_error "clashctl 安装失败:系统命令未指向当前安装目录" >&2 exit 1 fi # ui_ok "clashctl 已更新: /usr/local/bin/clashctl → $EXPECTED_CLASHCTL_REAL" # ui_ok "clashctl: /usr/local/bin/clashctl" # ========================= # 安装 proxy helper # ========================= # 不再注入 shell function,clashctl 统一走 /usr/local/bin/clashctl rm -f /etc/profile.d/clash-for-linux.sh >/dev/null 2>&1 || true rm -f /etc/profile.d/clash.sh >/dev/null 2>&1 || true rm -f /etc/profile.d/clashctl.sh >/dev/null 2>&1 || true # 清理当前 shell 的旧函数污染(当前终端立即生效) unset -f clashctl clashhelp clashlog clashmixin clashoff clashon clashproxy clashrestart clashsecret clashstatus clashsub clashtun clashui clashupgrade 2>/dev/null || true unalias clashctl 2>/dev/null || true # 写入 profile 文件(新终端自动清理污染) cat >/etc/profile.d/clash-for-linux.sh </dev/null || true unalias clashctl 2>/dev/null || true CLASH_INSTALL_DIR="${Install_Dir}" ENV_FILE="\${CLASH_INSTALL_DIR}/.env" if [ -f "\$ENV_FILE" ]; then set +u . "\$ENV_FILE" >/dev/null 2>&1 || true set -u fi CLASH_LISTEN_IP="\${CLASH_LISTEN_IP:-127.0.0.1}" CLASH_HTTP_PORT="\${CLASH_HTTP_PORT:-7890}" CLASH_SOCKS_PORT="\${CLASH_SOCKS_PORT:-7891}" # 开启代理 proxy_on() { export http_proxy="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}" export https_proxy="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}" export HTTP_PROXY="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}" export HTTPS_PROXY="http://\${CLASH_LISTEN_IP}:\${CLASH_HTTP_PORT}" export all_proxy="socks5://\${CLASH_LISTEN_IP}:\${CLASH_SOCKS_PORT}" export ALL_PROXY="socks5://\${CLASH_LISTEN_IP}:\${CLASH_SOCKS_PORT}" export no_proxy="127.0.0.1,localhost,::1" export NO_PROXY="127.0.0.1,localhost,::1" ui_ok "已开启代理" } # 关闭代理 proxy_off() { unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY unset all_proxy ALL_PROXY no_proxy NO_PROXY ui_ok "已关闭代理" } EOF chmod 644 /etc/profile.d/clash-for-linux.sh # ========================= # 安装 systemd # ========================= 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" "$Install_Dir" if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then systemctl enable "${Service_Name}.service" || true fi else ui_warn "未找到 systemd,回退到脚本模式" fi ui_ok "[3/3] 启动服务..." # ========================= # 输出 + 订阅录入 # ========================= secret="$(read_env_value "CLASH_SECRET")" public_ip="$(get_public_ip)" echo show_dashboard_info "$secret" "$public_ip" show_install_usage clashctl on >/dev/null 2>&1 || true echo "🚀 代理已开启" prompt_and_apply_subscription