diff --git a/.env b/.env index 76cc65e..7492a69 100644 --- a/.env +++ b/.env @@ -15,3 +15,5 @@ export CLASH_ALLOW_LAN=true # External Controller (RESTful API) 配置 export EXTERNAL_CONTROLLER_ENABLED=true export EXTERNAL_CONTROLLER=0.0.0.0:9090 + +# 端口可设置为 auto(自动分配随机端口) diff --git a/README.md b/README.md index 23fce5e..73f033a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ $ vim .env > **注意:** `.env` 文件中的变量 `CLASH_SECRET` 为自定义 Clash Secret,值为空时,脚本将自动生成随机字符串。 > 如需使用其它架构,请将对应 Clash 二进制放入 `bin/` 并在 `.env` 中设置 `CLASH_BIN`,或命名为 `clash-linux-`(如 `clash-linux-riscv64`)。 +> 端口支持设置为 `auto`,脚本会自动检测冲突并随机分配可用端口。
@@ -90,6 +91,26 @@ $ proxy_on
+## clashctl 命令 + +统一管理入口,支持启动/停止/重启/状态/更新/修改订阅: + +```bash +$ sudo ./clashctl status +$ sudo ./clashctl start +$ sudo ./clashctl restart +$ sudo ./clashctl update +$ sudo ./clashctl set-url "https://example.com/your-subscribe" +``` + +安装脚本会将 `clashctl` 安装到 `/usr/local/bin/clashctl`,安装后可直接使用: + +```bash +$ sudo clashctl status +``` + +
+ ## 一键安装/卸载 脚本会自动识别安装路径、创建低权限用户、检测端口冲突,并根据架构自动下载 Clash 内核(可通过 `CLASH_DOWNLOAD_URL_TEMPLATE` 自定义下载地址)。 diff --git a/clashctl b/clashctl new file mode 100755 index 0000000..d277cbd --- /dev/null +++ b/clashctl @@ -0,0 +1,146 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +SERVICE_NAME="clash-for-linux" + +resolve_clash_home() { + local candidates + if [ -n "${CLASH_HOME:-}" ] && [ -d "$CLASH_HOME" ]; then + echo "$CLASH_HOME" + return 0 + fi + if [ -f "/etc/default/${SERVICE_NAME}" ]; then + candidates=$(awk -F= '/^CLASH_HOME=/{print $2}' "/etc/default/${SERVICE_NAME}" | tr -d '"') + if [ -n "$candidates" ] && [ -d "$candidates" ]; then + echo "$candidates" + return 0 + fi + fi + if [ -f "$SCRIPT_DIR/.env" ]; then + echo "$SCRIPT_DIR" + return 0 + fi + if [ -f "/opt/clash-for-linux/.env" ]; then + echo "/opt/clash-for-linux" + return 0 + fi + echo "$SCRIPT_DIR" +} + +CLASH_HOME=$(resolve_clash_home) +ENV_FILE="$CLASH_HOME/.env" +PID_FILE="$CLASH_HOME/temp/clash.pid" + +use_systemd() { + command -v systemctl >/dev/null 2>&1 +} + +action_with_systemd() { + local action="$1" + if use_systemd; then + if systemctl "$action" "${SERVICE_NAME}.service" >/dev/null 2>&1; then + return 0 + fi + fi + return 1 +} + +print_usage() { + cat < [args] + +Commands: + start Start Clash service + stop Stop Clash service + restart Restart Clash service + status Show Clash service status + update Refresh subscription config + set-url Update CLASH_URL in .env + +Environment: + CLASH_HOME Override Clash installation directory +USAGE +} + +set_url() { + local url="$1" + if [ -z "$url" ]; then + echo "[ERROR] 请提供订阅地址" >&2 + exit 1 + fi + if [ ! -f "$ENV_FILE" ]; then + echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2 + exit 1 + fi + local escaped + escaped=$(printf "%s" "$url" | sed "s/'/'\"'\"'/g") + if grep -q '^export CLASH_URL=' "$ENV_FILE"; then + sed -i "s@^export CLASH_URL=.*@export CLASH_URL='${escaped}'@" "$ENV_FILE" + else + echo "export CLASH_URL='${escaped}'" >> "$ENV_FILE" + fi + echo "[OK] 已更新 CLASH_URL" +} + +status_fallback() { + if [ -f "$PID_FILE" ]; then + local pid + pid=$(cat "$PID_FILE") + if [ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1; then + echo "[OK] Clash 进程运行中 (PID: $pid)" + return 0 + fi + fi + if pgrep -f "clash-linux-" >/dev/null 2>&1; then + echo "[OK] Clash 进程运行中" + return 0 + fi + echo "[WARN] 未检测到 Clash 进程" + return 1 +} + +run_script() { + local script="$1" + if [ ! -x "$CLASH_HOME/$script" ]; then + echo "[ERROR] 未找到脚本: $CLASH_HOME/$script" >&2 + exit 1 + fi + ( cd "$CLASH_HOME" && bash "$CLASH_HOME/$script" ) +} + +command=${1:-} +case "$command" in + start) + action_with_systemd start || run_script start.sh + ;; + stop) + action_with_systemd stop || run_script shutdown.sh + ;; + restart) + action_with_systemd restart || run_script restart.sh + ;; + status) + if use_systemd; then + if systemctl status "${SERVICE_NAME}.service" --no-pager; then + exit 0 + fi + fi + status_fallback + ;; + update) + run_script update.sh + ;; + set-url) + set_url "${2:-}" + ;; + -h|--help|help|'') + print_usage + ;; + *) + echo "[ERROR] 未知命令: $command" >&2 + print_usage + exit 1 + ;; +esac diff --git a/install.sh b/install.sh index ec1818f..996a3ce 100755 --- a/install.sh +++ b/install.sh @@ -30,10 +30,12 @@ fi chmod +x "$Install_Dir"/*.sh 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 source "$Install_Dir/.env" source "$Install_Dir/scripts/get_cpu_arch.sh" source "$Install_Dir/scripts/resolve_clash.sh" +source "$Install_Dir/scripts/port_utils.sh" if [[ -z "${CpuArch:-}" ]]; then echo -e "\033[31m[ERROR] 无法识别 CPU 架构\033[0m" @@ -51,22 +53,11 @@ parse_port() { echo "$raw" } -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 - elif command -v netstat >/dev/null 2>&1; then - netstat -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1 - elif command -v lsof >/dev/null 2>&1; then - lsof -iTCP -sTCP:LISTEN -P -n | awk '{print $9}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1 - else - echo -e "\033[33m[WARN] 未找到端口检测工具,跳过端口冲突检测\033[0m" - return 1 - fi -} - 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") @@ -75,9 +66,7 @@ for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_ done if [ "${#Port_Conflicts[@]}" -ne 0 ]; then - echo -e "\033[31m[ERROR] 检测到端口冲突: ${Port_Conflicts[*]}\033[0m" - echo -e "请修改 .env 中的端口设置后重新安装。" - exit 1 + echo -e "\033[33m[WARN] 检测到端口冲突: ${Port_Conflicts[*]},运行时将自动分配可用端口\033[0m" fi if ! getent group "$Service_Group" >/dev/null 2>&1; then @@ -108,5 +97,9 @@ else echo -e "\033[33m[WARN] 未检测到 systemd,已跳过服务单元生成\033[0m" fi +if [ -f "$Install_Dir/clashctl" ]; then + install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl +fi + echo -e "\033[32m[OK] Clash for Linux 已安装至: ${Install_Dir}\033[0m" echo -e "请编辑 ${Install_Dir}/.env 配置订阅地址后启动服务。" diff --git a/scripts/port_utils.sh b/scripts/port_utils.sh new file mode 100644 index 0000000..789979e --- /dev/null +++ b/scripts/port_utils.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +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 + return $? + fi + if command -v netstat >/dev/null 2>&1; then + netstat -lnt | 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 + return $? + fi + if [ "$PORT_CHECK_WARNED" -eq 0 ]; then + echo -e "\033[33m[WARN] 未找到端口检测工具,端口冲突检测可能不准确\033[0m" >&2 + PORT_CHECK_WARNED=1 + fi + return 1 +} + +find_available_port() { + local start_port=${1:-20000} + local end_port=${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) + if ! is_port_in_use "$port"; then + echo "$port" + return 0 + fi + done + fi + + for port in $(seq "$start_port" "$end_port"); do + if ! is_port_in_use "$port"; then + echo "$port" + return 0 + fi + done + + return 1 +} + +resolve_port_value() { + local name="$1" + local value="$2" + local resolved + + if [ -z "$value" ] || [ "$value" = "auto" ]; then + resolved=$(find_available_port) + if [ -z "$resolved" ]; then + return 1 + fi + echo -e "\033[33m[WARN] ${name} 端口已自动分配为 ${resolved}\033[0m" >&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 + fi + fi + + echo "$value" +} + +resolve_host_port() { + local name="$1" + local raw="$2" + local default_host="$3" + local host + local port + + if [ "$raw" = "auto" ] || [ -z "$raw" ]; then + host="$default_host" + port="auto" + else + if [[ "$raw" == *:* ]]; then + host="${raw%:*}" + port="${raw##*:}" + else + host="$default_host" + port="$raw" + fi + fi + + port=$(resolve_port_value "$name" "$port") || return 1 + echo "${host}:${port}" +} diff --git a/start.sh b/start.sh index cb817b2..dea88e2 100644 --- a/start.sh +++ b/start.sh @@ -43,6 +43,12 @@ EXTERNAL_CONTROLLER_ENABLED=${EXTERNAL_CONTROLLER_ENABLED:-true} EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false} +source "$Server_Dir/scripts/port_utils.sh" +CLASH_HTTP_PORT=$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT") +CLASH_SOCKS_PORT=$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT") +CLASH_REDIR_PORT=$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT") +EXTERNAL_CONTROLLER=$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "0.0.0.0") + #################### 函数定义 #################### diff --git a/uninstall.sh b/uninstall.sh index 107165c..cbc572d 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -27,10 +27,13 @@ if [ -f "/etc/profile.d/clash-for-linux.sh" ]; then rm -f "/etc/profile.d/clash-for-linux.sh" fi +if [ -f "/usr/local/bin/clashctl" ]; then + rm -f "/usr/local/bin/clashctl" +fi + if [ -d "$Install_Dir" ]; then rm -rf "$Install_Dir" echo -e "\033[32m[OK] 已移除安装目录: ${Install_Dir}\033[0m" else echo -e "\033[33m[WARN] 未找到安装目录: ${Install_Dir}\033[0m" fi - diff --git a/update.sh b/update.sh index c954483..b166f8e 100644 --- a/update.sh +++ b/update.sh @@ -36,6 +36,12 @@ EXTERNAL_CONTROLLER_ENABLED=${EXTERNAL_CONTROLLER_ENABLED:-true} EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false} +source "$Server_Dir/scripts/port_utils.sh" +CLASH_HTTP_PORT=$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT") +CLASH_SOCKS_PORT=$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT") +CLASH_REDIR_PORT=$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT") +EXTERNAL_CONTROLLER=$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "0.0.0.0") + #################### 函数定义 #################### # 自定义action函数,实现通用action功能