refactor
6
.env
@ -27,7 +27,7 @@ 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'
|
||||
CLASH_DOWNLOAD_URL_TEMPLATE='https://github.com/MetaCubeX/mihomo/releases/download/{version}/mihomo-{arch}-{version}.gz'
|
||||
|
||||
# 订阅请求头(可选)
|
||||
# 常见机场需要 User-Agent;如不需要可留空
|
||||
@ -73,10 +73,10 @@ export CLASH_REDIR_PORT=7892
|
||||
# ⚠️ 安全建议:
|
||||
# - 服务器自用推荐:127.0.0.1
|
||||
# - 需要局域网设备使用再改:0.0.0.0
|
||||
export CLASH_LISTEN_IP='127.0.0.1'
|
||||
export CLASH_LISTEN_IP='0.0.0.0'
|
||||
|
||||
# 是否允许局域网访问(配合 CLASH_LISTEN_IP 使用)
|
||||
export CLASH_ALLOW_LAN=false
|
||||
export CLASH_ALLOW_LAN=true
|
||||
|
||||
# 是否即使 MMDB 下载失败也继续启动
|
||||
export CLASH_SKIP_GEOIP_TEST_FAILURE="true"
|
||||
|
||||
12
clashctl
@ -286,7 +286,7 @@ cmd_ui() {
|
||||
|
||||
case "$host" in
|
||||
0.0.0.0|::|localhost)
|
||||
host="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
host="$(curl -fsS --max-time 5 ifconfig.me 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
[ -n "${host:-}" ] || host="127.0.0.1"
|
||||
;;
|
||||
esac
|
||||
@ -311,7 +311,7 @@ cmd_secret() {
|
||||
|
||||
if [ ! -s "$RUNTIME_CONFIG" ]; then
|
||||
err "runtime config not found: $RUNTIME_CONFIG"
|
||||
echo "Please run: clashctl generate" >&2
|
||||
echo "Please run install.sh or clashctl generate" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -330,7 +330,13 @@ cmd_sub() {
|
||||
case "$subcmd" in
|
||||
show)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || echo "CLASH_URL=未配置"
|
||||
local current_url
|
||||
current_url="$(sed -nE "s/^[[:space:]]*(export[[:space:]]+)?CLASH_URL=['\"]?([^'\"]*)['\"]?$/\2/p" "$ENV_FILE" | head -n 1)"
|
||||
if [ -n "${current_url:-}" ]; then
|
||||
echo "[1] $current_url"
|
||||
else
|
||||
echo "未配置订阅"
|
||||
fi
|
||||
else
|
||||
err "未找到 .env"
|
||||
exit 1
|
||||
|
||||
145
install.sh
@ -31,7 +31,7 @@ chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true
|
||||
chmod +x "$Install_Dir"/bin/* 2>/dev/null || true
|
||||
|
||||
# =========================
|
||||
# 目录初始化(新结构)
|
||||
# 目录初始化
|
||||
# =========================
|
||||
mkdir -p \
|
||||
"$Install_Dir/runtime" \
|
||||
@ -50,6 +50,120 @@ source "$Install_Dir/scripts/get_cpu_arch.sh"
|
||||
# shellcheck disable=SC1090
|
||||
source "$Install_Dir/scripts/resolve_clash.sh"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
read_env_value() {
|
||||
local key="$1"
|
||||
sed -nE "s/^[[:space:]]*(export[[:space:]]+)?${key}=['\"]?([^'\"]*)['\"]?$/\2/p" "$Install_Dir/.env" | head -n 1
|
||||
}
|
||||
|
||||
get_public_ip() {
|
||||
curl -fsS --max-time 5 ifconfig.me 2>/dev/null \
|
||||
|| curl -fsS --max-time 5 ip.sb 2>/dev/null \
|
||||
|| curl -fsS --max-time 5 api.ipify.org 2>/dev/null \
|
||||
|| true
|
||||
}
|
||||
|
||||
show_dashboard_info() {
|
||||
local secret="$1"
|
||||
local public_ip="$2"
|
||||
local dashboard_port="9090"
|
||||
local ui_url=""
|
||||
|
||||
if [ -n "${public_ip:-}" ]; then
|
||||
ui_url="http://${public_ip}:${dashboard_port}/ui/#/setup?hostname=${public_ip}&port=${dashboard_port}&secret=${secret}"
|
||||
else
|
||||
ui_url="http://127.0.0.1:${dashboard_port}/ui/#/setup?hostname=127.0.0.1&port=${dashboard_port}&secret=${secret}"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "╔═══════════════════════════════════════════════╗"
|
||||
echo "║ 😼 Web 控制台 ║"
|
||||
echo "║═══════════════════════════════════════════════║"
|
||||
echo "║ ║"
|
||||
echo "║ 🔓 注意放行端口:9090 ║"
|
||||
if [ -n "${public_ip:-}" ]; then
|
||||
printf "║ 🌏 公网:http://%-27s║\n" "${public_ip}:9090/ui"
|
||||
else
|
||||
printf "║ 🏠 本地:http://%-27s║\n" "127.0.0.1:9090/ui"
|
||||
fi
|
||||
echo "║ ║"
|
||||
echo "╚═══════════════════════════════════════════════╝"
|
||||
echo
|
||||
echo "😼 当前密钥:${secret}"
|
||||
echo "🎯 面板地址:${ui_url}"
|
||||
}
|
||||
|
||||
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=""
|
||||
|
||||
while true; do
|
||||
echo
|
||||
read -r -p "✈️ 请输入要添加的订阅链接:" sub_url
|
||||
|
||||
if [ -z "${sub_url:-}" ]; then
|
||||
echo "❌ 订阅链接不能为空"
|
||||
continue
|
||||
fi
|
||||
|
||||
write_env_value "CLASH_URL" "$sub_url"
|
||||
|
||||
echo "⏳ 正在下载订阅..."
|
||||
echo "🍃 验证订阅配置..."
|
||||
if ! "$Install_Dir/scripts/generate_config.sh" >/dev/null 2>&1; then
|
||||
echo "❌ 订阅不可用或转换失败,请检查链接后重试"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "🎉 订阅已添加:[1] $sub_url"
|
||||
echo "🔥 订阅已生效"
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart "${Service_Name}.service"
|
||||
else
|
||||
"$Install_Dir/scripts/run_clash.sh" --daemon
|
||||
fi
|
||||
|
||||
secret="$(read_env_value "CLASH_SECRET")"
|
||||
public_ip="$(get_public_ip)"
|
||||
|
||||
show_dashboard_info "$secret" "$public_ip"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
# =========================
|
||||
# 内核检查
|
||||
# =========================
|
||||
@ -107,9 +221,6 @@ chmod 644 /etc/profile.d/clash-for-linux.sh
|
||||
# =========================
|
||||
# 安装 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" "$Install_Dir"
|
||||
@ -117,39 +228,19 @@ if command -v systemctl >/dev/null 2>&1; then
|
||||
if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then
|
||||
systemctl enable "${Service_Name}.service" || true
|
||||
fi
|
||||
|
||||
if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then
|
||||
systemctl start "${Service_Name}.service" || true
|
||||
fi
|
||||
|
||||
if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then
|
||||
Service_Enabled="enabled"
|
||||
else
|
||||
Service_Enabled="disabled"
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then
|
||||
Service_Started="active"
|
||||
else
|
||||
Service_Started="inactive"
|
||||
fi
|
||||
else
|
||||
echo "[WARN] systemd not found, will use script mode"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# 输出(全部收敛到 clashctl)
|
||||
# 输出 + 订阅录入
|
||||
# =========================
|
||||
echo
|
||||
echo "=== Install Complete ==="
|
||||
echo "Install Dir : $Install_Dir"
|
||||
echo "clashctl : /usr/local/bin/clashctl"
|
||||
|
||||
echo
|
||||
echo "Next:"
|
||||
echo " clashctl generate"
|
||||
echo " clashctl start"
|
||||
echo " clashctl doctor"
|
||||
prompt_and_apply_subscription
|
||||
|
||||
echo
|
||||
echo "Commands:"
|
||||
@ -157,3 +248,5 @@ echo " clashctl status"
|
||||
echo " clashctl logs"
|
||||
echo " clashctl restart"
|
||||
echo " clashctl stop"
|
||||
echo " clashctl ui"
|
||||
echo " clashctl secret"
|
||||
@ -59,6 +59,24 @@ LAST_GENERATE_AT=$(date -Iseconds)
|
||||
EOF
|
||||
}
|
||||
|
||||
write_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local env_file="$PROJECT_DIR/.env"
|
||||
local escaped="${value//\\/\\\\}"
|
||||
escaped="${escaped//&/\\&}"
|
||||
escaped="${escaped//|/\\|}"
|
||||
escaped="${escaped//\'/\'\\\'\'}"
|
||||
|
||||
[ -f "$env_file" ] || return 1
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
generate_secret() {
|
||||
if [ -n "${CLASH_SECRET:-}" ]; then
|
||||
echo "$CLASH_SECRET"
|
||||
@ -75,13 +93,14 @@ generate_secret() {
|
||||
fi
|
||||
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex 16
|
||||
openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16
|
||||
else
|
||||
head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||||
tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16
|
||||
fi
|
||||
}
|
||||
|
||||
SECRET="$(generate_secret)"
|
||||
write_env_value "CLASH_SECRET" "$SECRET" || true
|
||||
|
||||
upsert_yaml_kv_local() {
|
||||
local file="$1"
|
||||
@ -97,6 +116,14 @@ upsert_yaml_kv_local() {
|
||||
fi
|
||||
}
|
||||
|
||||
remove_yaml_key_local() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
|
||||
[ -f "$file" ] || return 0
|
||||
sed -i -E "/^[[:space:]]*${key}:/d" "$file"
|
||||
}
|
||||
|
||||
apply_secret_to_config() {
|
||||
local file="$1"
|
||||
upsert_yaml_kv_local "$file" "secret" "$SECRET"
|
||||
@ -104,19 +131,16 @@ apply_secret_to_config() {
|
||||
|
||||
apply_controller_to_config() {
|
||||
local file="$1"
|
||||
local ui_dir="$RUNTIME_DIR/ui"
|
||||
local ui_dir="$PROJECT_DIR/ui/dist"
|
||||
|
||||
if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then
|
||||
upsert_yaml_kv_local "$file" "external-controller" "$EXTERNAL_CONTROLLER"
|
||||
|
||||
rm -rf "$ui_dir"
|
||||
mkdir -p "$ui_dir"
|
||||
|
||||
if [ -d "$PROJECT_DIR/dashboard/public" ]; then
|
||||
cp -a "$PROJECT_DIR/dashboard/public/." "$ui_dir/"
|
||||
if [ -f "$ui_dir/index.html" ]; then
|
||||
upsert_yaml_kv_local "$file" "external-ui" "$ui_dir"
|
||||
else
|
||||
remove_yaml_key_local "$file" "external-ui"
|
||||
echo "[ERROR] UI not found: $ui_dir/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
remove_yaml_key_local "$file" "external-controller"
|
||||
@ -129,8 +153,14 @@ download_subscription() {
|
||||
|
||||
local curl_cmd=(curl -fL -S --retry 2 --connect-timeout 10 -m 30 -o "$TMP_DOWNLOAD")
|
||||
[ "$ALLOW_INSECURE_TLS" = "true" ] && curl_cmd+=(-k)
|
||||
curl_cmd+=("$CLASH_URL")
|
||||
|
||||
if [ -n "${CLASH_HEADERS:-}" ]; then
|
||||
while IFS= read -r header; do
|
||||
[ -n "$header" ] && curl_cmd+=(-H "$header")
|
||||
done < <(printf '%s\n' "$CLASH_HEADERS" | tr ';' '\n')
|
||||
fi
|
||||
|
||||
curl_cmd+=("$CLASH_URL")
|
||||
"${curl_cmd[@]}"
|
||||
}
|
||||
|
||||
@ -140,7 +170,7 @@ is_complete_clash_config() {
|
||||
}
|
||||
|
||||
cleanup_tmp_files() {
|
||||
rm -f "$TMP_PROXY_FRAGMENT" "$TMP_CONFIG"
|
||||
rm -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED" "$TMP_PROXY_FRAGMENT" "$TMP_CONFIG"
|
||||
}
|
||||
|
||||
build_fragment_config() {
|
||||
@ -164,14 +194,6 @@ finalize_config() {
|
||||
mv -f "$file" "$RUNTIME_CONFIG"
|
||||
}
|
||||
|
||||
remove_yaml_key_local() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
|
||||
[ -f "$file" ] || return 0
|
||||
sed -i -E "/^[[:space:]]*${key}:/d" "$file"
|
||||
}
|
||||
|
||||
main() {
|
||||
local template_file="$CONFIG_DIR/template.yaml"
|
||||
|
||||
@ -186,13 +208,14 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! download_subscription; then
|
||||
if [ -s "$RUNTIME_CONFIG" ]; then
|
||||
write_state "success" "download_failed_keep_runtime" "runtime_existing"
|
||||
exit 0
|
||||
if [ -z "${CLASH_URL:-}" ]; then
|
||||
echo "[ERROR] CLASH_URL is empty" >&2
|
||||
write_state "failed" "url_missing" "none"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ERROR] failed to download subscription and runtime config missing" >&2
|
||||
if ! download_subscription; then
|
||||
echo "[ERROR] failed to download subscription" >&2
|
||||
write_state "failed" "download_failed" "none"
|
||||
exit 1
|
||||
fi
|
||||
@ -205,14 +228,12 @@ main() {
|
||||
apply_secret_to_config "$TMP_CONFIG"
|
||||
finalize_config "$TMP_CONFIG"
|
||||
write_state "success" "subscription_full" "subscription_full"
|
||||
cleanup_tmp_files
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -s "$template_file" ]; then
|
||||
echo "[ERROR] missing template config file: $template_file" >&2
|
||||
write_state "failed" "missing_template" "none"
|
||||
cleanup_tmp_files
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -222,7 +243,6 @@ main() {
|
||||
|
||||
finalize_config "$TMP_CONFIG"
|
||||
write_state "success" "subscription_fragment_merged" "subscription_fragment"
|
||||
cleanup_tmp_files
|
||||
}
|
||||
|
||||
trap cleanup_tmp_files EXIT
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 900 B |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 622 B After Width: | Height: | Size: 622 B |