Add clashctl and auto port selection

This commit is contained in:
wnlen
2026-01-14 13:31:05 +08:00
parent 0607ff6adc
commit 90f63e54e8
8 changed files with 297 additions and 18 deletions

2
.env
View File

@ -15,3 +15,5 @@ export CLASH_ALLOW_LAN=true
# External Controller (RESTful API) 配置 # External Controller (RESTful API) 配置
export EXTERNAL_CONTROLLER_ENABLED=true export EXTERNAL_CONTROLLER_ENABLED=true
export EXTERNAL_CONTROLLER=0.0.0.0:9090 export EXTERNAL_CONTROLLER=0.0.0.0:9090
# 端口可设置为 auto自动分配随机端口

View File

@ -45,6 +45,7 @@ $ vim .env
> **注意:** `.env` 文件中的变量 `CLASH_SECRET` 为自定义 Clash Secret值为空时脚本将自动生成随机字符串。 > **注意:** `.env` 文件中的变量 `CLASH_SECRET` 为自定义 Clash Secret值为空时脚本将自动生成随机字符串。
> 如需使用其它架构,请将对应 Clash 二进制放入 `bin/` 并在 `.env` 中设置 `CLASH_BIN`,或命名为 `clash-linux-<arch>`(如 `clash-linux-riscv64`)。 > 如需使用其它架构,请将对应 Clash 二进制放入 `bin/` 并在 `.env` 中设置 `CLASH_BIN`,或命名为 `clash-linux-<arch>`(如 `clash-linux-riscv64`)。
> 端口支持设置为 `auto`,脚本会自动检测冲突并随机分配可用端口。
<br> <br>
@ -90,6 +91,26 @@ $ proxy_on
<br> <br>
## 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
```
<br>
## 一键安装/卸载 ## 一键安装/卸载
脚本会自动识别安装路径、创建低权限用户、检测端口冲突,并根据架构自动下载 Clash 内核(可通过 `CLASH_DOWNLOAD_URL_TEMPLATE` 自定义下载地址)。 脚本会自动识别安装路径、创建低权限用户、检测端口冲突,并根据架构自动下载 Clash 内核(可通过 `CLASH_DOWNLOAD_URL_TEMPLATE` 自定义下载地址)。

146
clashctl Executable file
View File

@ -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 <<USAGE
Usage: clashctl <command> [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 <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

View File

@ -30,10 +30,12 @@ fi
chmod +x "$Install_Dir"/*.sh 2>/dev/null || true chmod +x "$Install_Dir"/*.sh 2>/dev/null || true
chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true
chmod +x "$Install_Dir"/bin/* 2>/dev/null || true chmod +x "$Install_Dir"/bin/* 2>/dev/null || true
chmod +x "$Install_Dir"/clashctl 2>/dev/null || true
source "$Install_Dir/.env" source "$Install_Dir/.env"
source "$Install_Dir/scripts/get_cpu_arch.sh" source "$Install_Dir/scripts/get_cpu_arch.sh"
source "$Install_Dir/scripts/resolve_clash.sh" source "$Install_Dir/scripts/resolve_clash.sh"
source "$Install_Dir/scripts/port_utils.sh"
if [[ -z "${CpuArch:-}" ]]; then if [[ -z "${CpuArch:-}" ]]; then
echo -e "\033[31m[ERROR] 无法识别 CPU 架构\033[0m" echo -e "\033[31m[ERROR] 无法识别 CPU 架构\033[0m"
@ -51,22 +53,11 @@ parse_port() {
echo "$raw" 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=() Port_Conflicts=()
for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_port "$EXTERNAL_CONTROLLER")"; do 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 [[ "$port" =~ ^[0-9]+$ ]]; then
if is_port_in_use "$port"; then if is_port_in_use "$port"; then
Port_Conflicts+=("$port") Port_Conflicts+=("$port")
@ -75,9 +66,7 @@ for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_
done done
if [ "${#Port_Conflicts[@]}" -ne 0 ]; then if [ "${#Port_Conflicts[@]}" -ne 0 ]; then
echo -e "\033[31m[ERROR] 检测到端口冲突: ${Port_Conflicts[*]}\033[0m" echo -e "\033[33m[WARN] 检测到端口冲突: ${Port_Conflicts[*]},运行时将自动分配可用端口\033[0m"
echo -e "请修改 .env 中的端口设置后重新安装。"
exit 1
fi fi
if ! getent group "$Service_Group" >/dev/null 2>&1; then if ! getent group "$Service_Group" >/dev/null 2>&1; then
@ -108,5 +97,9 @@ else
echo -e "\033[33m[WARN] 未检测到 systemd已跳过服务单元生成\033[0m" echo -e "\033[33m[WARN] 未检测到 systemd已跳过服务单元生成\033[0m"
fi 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 "\033[32m[OK] Clash for Linux 已安装至: ${Install_Dir}\033[0m"
echo -e "请编辑 ${Install_Dir}/.env 配置订阅地址后启动服务。" echo -e "请编辑 ${Install_Dir}/.env 配置订阅地址后启动服务。"

102
scripts/port_utils.sh Normal file
View File

@ -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}"
}

View File

@ -43,6 +43,12 @@ EXTERNAL_CONTROLLER_ENABLED=${EXTERNAL_CONTROLLER_ENABLED:-true}
EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090}
ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false} 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")
#################### 函数定义 #################### #################### 函数定义 ####################

View File

@ -27,10 +27,13 @@ if [ -f "/etc/profile.d/clash-for-linux.sh" ]; then
rm -f "/etc/profile.d/clash-for-linux.sh" rm -f "/etc/profile.d/clash-for-linux.sh"
fi fi
if [ -f "/usr/local/bin/clashctl" ]; then
rm -f "/usr/local/bin/clashctl"
fi
if [ -d "$Install_Dir" ]; then if [ -d "$Install_Dir" ]; then
rm -rf "$Install_Dir" rm -rf "$Install_Dir"
echo -e "\033[32m[OK] 已移除安装目录: ${Install_Dir}\033[0m" echo -e "\033[32m[OK] 已移除安装目录: ${Install_Dir}\033[0m"
else else
echo -e "\033[33m[WARN] 未找到安装目录: ${Install_Dir}\033[0m" echo -e "\033[33m[WARN] 未找到安装目录: ${Install_Dir}\033[0m"
fi fi

View File

@ -36,6 +36,12 @@ EXTERNAL_CONTROLLER_ENABLED=${EXTERNAL_CONTROLLER_ENABLED:-true}
EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090}
ALLOW_INSECURE_TLS=${ALLOW_INSECURE_TLS:-false} 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功能 # 自定义action函数实现通用action功能