mirror of
https://github.com/wnlen/clash-for-linux.git
synced 2026-02-04 10:11:28 +08:00
Merge pull request #102 from wnlen/codex/add-clashctl-command-and-port-conflict-detection
新增 `clashctl` 管理命令并实现端口冲突自动检测与分配
This commit is contained in:
2
.env
2
.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(自动分配随机端口)
|
||||
|
||||
21
README.md
21
README.md
@ -45,6 +45,7 @@ $ vim .env
|
||||
|
||||
> **注意:** `.env` 文件中的变量 `CLASH_SECRET` 为自定义 Clash Secret,值为空时,脚本将自动生成随机字符串。
|
||||
> 如需使用其它架构,请将对应 Clash 二进制放入 `bin/` 并在 `.env` 中设置 `CLASH_BIN`,或命名为 `clash-linux-<arch>`(如 `clash-linux-riscv64`)。
|
||||
> 端口支持设置为 `auto`,脚本会自动检测冲突并随机分配可用端口。
|
||||
|
||||
<br>
|
||||
|
||||
@ -90,6 +91,26 @@ $ proxy_on
|
||||
|
||||
<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` 自定义下载地址)。
|
||||
|
||||
146
clashctl
Executable file
146
clashctl
Executable 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
|
||||
27
install.sh
27
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 配置订阅地址后启动服务。"
|
||||
|
||||
102
scripts/port_utils.sh
Normal file
102
scripts/port_utils.sh
Normal 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}"
|
||||
}
|
||||
6
start.sh
6
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")
|
||||
|
||||
|
||||
|
||||
#################### 函数定义 ####################
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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功能
|
||||
|
||||
Reference in New Issue
Block a user