#!/usr/bin/env bash RESOURCES_BASE_DIR=".${CLASH_RESOURCES_DIR#"$CLASH_BASE_DIR"}" ZIP_BASE_DIR=".${CLASH_RESOURCES_DIR#"$CLASH_BASE_DIR"}/zip" SCRIPT_BASE_DIR='scripts' SCRIPT_INIT_DIR="${SCRIPT_BASE_DIR}/init" SCRIPT_CMD_DIR="${SCRIPT_BASE_DIR}/cmd" SCRIPT_CMD_FISH="${SCRIPT_CMD_DIR}/clashctl.fish" CLASH_CMD_DIR="${CLASH_BASE_DIR}/$SCRIPT_CMD_DIR" FILE_LOG="/var/log/${KERNEL_NAME}.log" FILE_PID="/run/${KERNEL_NAME}.pid" _valid_required() { local required_cmds=("xz" "pgrep" "curl" "tar" 'unzip') local missing=() for cmd in "${required_cmds[@]}"; do command -v "$cmd" >&/dev/null || missing+=("$cmd") done [ "${#missing[@]}" -gt 0 ] && _error_quit "请先安装以下命令:${missing[*]}" } _valid() { _valid_required [ -d "$CLASH_BASE_DIR" ] && _error_quit "请先执行卸载脚本,以清除安装路径:$CLASH_BASE_DIR" local msg="${CLASH_BASE_DIR}:当前路径不可用,请在 .env 中更换安装路径。" mkdir -p "$CLASH_BASE_DIR" || _error_quit "$msg" _is_regular_sudo && [[ $CLASH_BASE_DIR == /root* ]] && _error_quit "$msg" [ -z "$ZSH_VERSION" ] && [ -z "$BASH_VERSION" ] && _error_quit "仅支持:bash、zsh 执行" } _parse_args() { for arg in "$@"; do case $arg in mihomo) KERNEL_NAME=mihomo ;; clash) KERNEL_NAME=clash ;; http*) CLASH_CONFIG_URL=$arg ;; esac done } _prepare_zip() { _load_zip >&/dev/null local required_zips=() case "${KERNEL_NAME}" in clash) [ ! -f "$ZIP_CLASH" ] && required_zips+=("clash") ;; mihomo | *) [ ! -f "$ZIP_MIHOMO" ] && required_zips+=("mihomo") ;; esac [ ! -f "$ZIP_YQ" ] && required_zips+=("yq") [ ! -f "$ZIP_SUBCONVERTER" ] && required_zips+=("subconverter") _download_zip "${required_zips[@]}" case "${KERNEL_NAME}" in clash) ZIP_KERNEL="$ZIP_CLASH" ;; mihomo | *) ZIP_KERNEL="$ZIP_MIHOMO" ;; esac BIN_KERNEL="${BIN_BASE_DIR}/$KERNEL_NAME" _unzip_zip } _load_zip() { ZIP_CLASH=$(echo "${ZIP_BASE_DIR}"/clash*) ZIP_MIHOMO=$(echo "${ZIP_BASE_DIR}"/mihomo*) ZIP_YQ=$(echo "${ZIP_BASE_DIR}"/yq*) ZIP_SUBCONVERTER=$(echo "${ZIP_BASE_DIR}"/subconverter*) } _download_zip() { (($#)) || return 0 local url_clash url_mihomo url_yq url_subconverter local arch=$(uname -m) case "$arch" in x86_64) local flags=$(grep -m1 '^flags' /proc/cpuinfo) local level=v1 grep -qw sse4_2 <<<"$flags" && grep -qw popcnt <<<"$flags" && level=v2 grep -qw avx2 <<<"$flags" && grep -qw fma <<<"$flags" && level=v3 VERSION_MIHOMO=${level}-$VERSION_MIHOMO url_clash=https://github.com/nelvko/clash-for-linux-install/releases/download/clash/clash-linux-amd64-2023.08.17.gz url_mihomo=https://github.com/MetaCubeX/mihomo/releases/download/${VERSION_MIHOMO##*-}/mihomo-linux-amd64-${VERSION_MIHOMO}.gz url_yq=https://github.com/mikefarah/yq/releases/download/${VERSION_YQ}/yq_linux_amd64.tar.gz url_subconverter=https://github.com/tindy2013/subconverter/releases/download/${VERSION_SUBCONVERTER}/subconverter_linux64.tar.gz ;; *86*) url_clash=https://github.com/nelvko/clash-for-linux-install/releases/download/clash/clash-linux-386-2023.08.17.gz url_mihomo=https://github.com/MetaCubeX/mihomo/releases/download/${VERSION_MIHOMO##*-}/mihomo-linux-386-${VERSION_MIHOMO}.gz url_yq=https://github.com/mikefarah/yq/releases/download/${VERSION_YQ}/yq_linux_386.tar.gz url_subconverter=https://github.com/tindy2013/subconverter/releases/download/${VERSION_SUBCONVERTER}/subconverter_linux32.tar.gz ;; armv*) url_clash=https://github.com/nelvko/clash-for-linux-install/releases/download/clash/clash-linux-armv5-2023.08.17.gz url_mihomo=https://github.com/MetaCubeX/mihomo/releases/download/${VERSION_MIHOMO##*-}/mihomo-linux-armv7-${VERSION_MIHOMO}.gz url_yq=https://github.com/mikefarah/yq/releases/download/${VERSION_YQ}/yq_linux_arm.tar.gz url_subconverter=https://github.com/tindy2013/subconverter/releases/download/${VERSION_SUBCONVERTER}/subconverter_armv7.tar.gz ;; aarch64) url_clash=https://github.com/nelvko/clash-for-linux-install/releases/download/clash/clash-linux-arm64-2023.08.17.gz url_mihomo=https://github.com/MetaCubeX/mihomo/releases/download/${VERSION_MIHOMO##*-}/mihomo-linux-arm64-${VERSION_MIHOMO}.gz url_yq=https://github.com/mikefarah/yq/releases/download/${VERSION_YQ}/yq_linux_arm64.tar.gz url_subconverter=https://github.com/tindy2013/subconverter/releases/download/${VERSION_SUBCONVERTER}/subconverter_aarch64.tar.gz ;; *) _error_quit "未知的架构版本:$arch,请自行下载对应版本至 ${ZIP_BASE_DIR} 目录" ;; esac local -A urls=( [clash]="$url_clash" [mihomo]="$url_mihomo" [yq]="$url_yq" [subconverter]="$url_subconverter" ) local item target_zips=() _okcat '🖥️ ' "系统架构:$arch $level" for item in "$@"; do local url="${urls[$item]}" local proxy_url="${URL_GH_PROXY:+${URL_GH_PROXY%/}/}${url}" url="$proxy_url" _okcat '⏳' "正在下载:${item}:$url" local target="${ZIP_BASE_DIR}/$(basename "$url")" curl \ --progress-bar \ --show-error \ --fail \ --insecure \ --location \ --retry 1 \ --output "$target" \ "$url" target_zips+=("$target") done _valid_zip "${target_zips[@]}" _load_zip >&/dev/null } _valid_zip() { (($#)) || return 1 local zip fail_zips=() for zip in "$@"; do gzip -tq "$zip" || unzip -tqq "$zip" || fail_zips+=("$zip") done ((${#fail_zips[@]})) && _error_quit "文件验证失败:${fail_zips[*]} 请删除后重试,或自行下载对应版本至 ${ZIP_BASE_DIR} 目录" } _unzip_zip() { _valid_zip "$ZIP_KERNEL" "$ZIP_YQ" "$ZIP_SUBCONVERTER" "$ZIP_UI" /usr/bin/install -D <(gzip -dc "$ZIP_KERNEL") "$BIN_KERNEL" tar -xf "$ZIP_YQ" -C "${BIN_BASE_DIR}" /bin/mv -f "${BIN_BASE_DIR}"/yq_* "${BIN_BASE_DIR}/yq" tar -xf "$ZIP_SUBCONVERTER" -C "$BIN_BASE_DIR" /bin/cp "$BIN_SUBCONVERTER_DIR/pref.example.yml" "$BIN_SUBCONVERTER_CONFIG" unzip -oqq "$ZIP_UI" -d "$RESOURCES_BASE_DIR" 2>/dev/null || tar -xf "$ZIP_UI" -C "$RESOURCES_BASE_DIR" } # shellcheck disable=SC2206 _detect_init() { [ -z "$INIT_TYPE" ] && INIT_TYPE=$(readlink /proc/1/exe) grep -qsE "docker|kubepods|containerd|podman|lxc" /proc/1/cgroup && INIT_TYPE='nohup' _is_root || { INIT_TYPE='nohup' FILE_LOG="${CLASH_RESOURCES_DIR}/${KERNEL_NAME}.log" FILE_PID="${CLASH_RESOURCES_DIR}/${KERNEL_NAME}.pid" } service_log=(less '<' $FILE_LOG) service_follow_log=(tail -f -n 0 $FILE_LOG) service_watch_proxy=(clashon) _is_regular_sudo && { service_watch_proxy=(_failcat "'未检测到代理变量,可执行 clashon 开启代理环境'") _SUDO=sudo } case "${INIT_TYPE}" in *systemd) service_log=($_SUDO journalctl -u "$KERNEL_NAME") service_follow_log=("${service_log[@]}" -q -f -n 0) _systemd ;; *init) _sysvinit ;; *busybox) command -v openrc-init >&/dev/null && _openrc ;; *openrc*) _openrc ;; *runit) _runit ;; nohup | *) INIT_TYPE='nohup' _nohup ;; esac INIT_TYPE=$(basename "$INIT_TYPE") } _openrc() { service_src="${SCRIPT_INIT_DIR}/OpenRC.sh" service_target="/etc/init.d/$KERNEL_NAME" service_enable=(rc-update add "$KERNEL_NAME" default) service_disable=(rc-update del "$KERNEL_NAME" default) service_start=(rc-service "$KERNEL_NAME" start) service_stop=(rc-service "$KERNEL_NAME" stop) service_restart=(rc-service "$KERNEL_NAME" restart) service_status=(rc-service "$KERNEL_NAME" status) service_is_active=(rc-service "$KERNEL_NAME" status) } _runit() { service_src="${SCRIPT_INIT_DIR}/runit.sh" service_target="/etc/sv/${KERNEL_NAME}/run" service_del=(rm -rf "/etc/sv/${KERNEL_NAME:-mihomo}") service_reload=(sleep 2) service_enable=(ln -s "$(dirname "$service_target")" "/etc/runit/runsvdir/default/${KERNEL_NAME}") service_disable=(rm -f "/etc/runit/runsvdir/current/${KERNEL_NAME}") service_start=(sv up "$KERNEL_NAME") service_stop=(sv down "$KERNEL_NAME") service_restart=(sv restart "$KERNEL_NAME") service_status=(sv status "$KERNEL_NAME") service_is_active=(sv status "$KERNEL_NAME" \| grep -qs '^run') } _sysvinit() { service_src="${SCRIPT_INIT_DIR}/SysVinit.sh" service_target="/etc/init.d/$KERNEL_NAME" command -v chkconfig >&/dev/null && { service_add=(chkconfig --add "$KERNEL_NAME") service_del=(chkconfig --del "$KERNEL_NAME") service_enable=(chkconfig "$KERNEL_NAME" on) service_disable=(chkconfig "$KERNEL_NAME" off) } command -v update-rc.d >&/dev/null && { service_add=(update-rc.d "$KERNEL_NAME" defaults) service_del=(update-rc.d "$KERNEL_NAME" remove) service_enable=(update-rc.d "$KERNEL_NAME" enable) service_disable=(update-rc.d "$KERNEL_NAME" disable) } service_start=(service "$KERNEL_NAME" start) service_stop=(service "$KERNEL_NAME" stop) service_restart=(service "$KERNEL_NAME" restart) service_status=(service "$KERNEL_NAME" status) service_is_active=(service "$KERNEL_NAME" status) } # shellcheck disable=SC2206 _systemd() { service_src="${SCRIPT_INIT_DIR}/systemd.sh" service_target="/etc/systemd/system/${KERNEL_NAME}.service" service_reload=($_SUDO systemctl daemon-reload) service_enable=($_SUDO systemctl enable "$KERNEL_NAME") service_disable=($_SUDO systemctl disable "$KERNEL_NAME") service_start=($_SUDO systemctl start "$KERNEL_NAME") service_stop=($_SUDO systemctl stop "$KERNEL_NAME") service_restart=($_SUDO systemctl restart "$KERNEL_NAME") service_status=($_SUDO systemctl status "$KERNEL_NAME") service_is_active=($_SUDO systemctl is-active "$KERNEL_NAME") } _nohup() { service_enable=(false) service_disable=(false) service_start=('(' nohup "$BIN_KERNEL" -d "$CLASH_RESOURCES_DIR" -f "$CLASH_CONFIG_RUNTIME" '>\&' "$FILE_LOG" '\&' ')') service_sudo_start=(sudo nohup "$BIN_KERNEL" -d "$CLASH_RESOURCES_DIR" -f "$CLASH_CONFIG_RUNTIME" '>\&' "$FILE_LOG" '\&') service_status=(pgrep -fa "$BIN_KERNEL") service_is_active=(pgrep -fa "$BIN_KERNEL") service_stop=(pkill -9 -f "$BIN_KERNEL") } _install_service() { local kernel_desc="$KERNEL_NAME Daemon, A[nother] Clash Kernel." local cmd_path="${BIN_KERNEL}" local cmd_arg="-d ${CLASH_RESOURCES_DIR} -f ${CLASH_CONFIG_RUNTIME}" local cmd_full="${BIN_KERNEL} -d ${CLASH_RESOURCES_DIR} -f ${CLASH_CONFIG_RUNTIME}" [ -n "$service_src" ] && { /usr/bin/install -D -m +x "$service_src" "$service_target" ((${#service_add[@]})) && "${service_add[@]}" sed -i \ -e "s#placeholder_cmd_path#$cmd_path#g" \ -e "s#placeholder_cmd_args#$cmd_arg#g" \ -e "s#placeholder_cmd_full#$cmd_full#g" \ -e "s#placeholder_log_file#$FILE_LOG#g" \ -e "s#placeholder_pid_file#$FILE_PID#g" \ -e "s#placeholder_kernel_name#$KERNEL_NAME#g" \ -e "s#placeholder_kernel_desc#$kernel_desc#g" \ "$service_target" } [ "$INIT_TYPE" != "nohup" ] && service_sudo_start=("${service_start[@]}") sed -i \ -e "s#placeholder_start#${service_start[*]}#g" \ -e "s#placeholder_sudo_start#${service_sudo_start[*]}#g" \ -e "s#placeholder_status#${service_status[*]}#g" \ -e "s#placeholder_is_active#${service_is_active[*]}#g" \ -e "s#placeholder_stop#${service_stop[*]}#g" \ -e "s#placeholder_log#${service_log[*]}#g" \ -e "s#placeholder_follow_log#${service_follow_log[*]}#g" \ -e "s#placeholder_watch_proxy#${service_watch_proxy[*]}#g" \ "$CLASH_CMD_DIR/clashctl.sh" "$CLASH_CMD_DIR/common.sh" "${service_enable[@]}" >&/dev/null && _okcat '🚀' '已设置开机自启' ((${#service_reload[@]})) && "${service_reload[@]}" } _uninstall_service() { _detect_init "${service_disable[@]}" >&/dev/null ((${#service_del[@]})) && "${service_del[@]}" rm -f "$service_target" ((${#service_reload[@]})) && "${service_reload[@]}" } _detect_rc() { local home=$HOME _is_regular_sudo && home=$(awk -F: -v user="$SUDO_USER" '$1==user{print $6}' /etc/passwd) command -v bash >&/dev/null && { SHELL_RC_BASH="${home}/.bashrc" } command -v zsh >&/dev/null && { SHELL_RC_ZSH="${home}/.zshrc" } command -v fish >&/dev/null && { SHELL_RC_FISH="${home}/.config/fish/conf.d/clashctl.fish" } start_flag="# clashctl START" end_flag="# clashctl END" } _apply_rc() { _detect_rc local source_clashctl=". $CLASH_CMD_DIR/clashctl.sh" # shellcheck disable=SC2086 tee -a "$SHELL_RC_BASH" $SHELL_RC_ZSH >/dev/null </dev/null [ -n "$SHELL_RC_FISH" ] && rm -f "$SHELL_RC_FISH" 2>/dev/null } _set_envs() { _set_env INIT_TYPE "$INIT_TYPE" _set_env KERNEL_NAME "$KERNEL_NAME" _set_env CLASH_BASE_DIR "$CLASH_BASE_DIR" _set_env VERSION_MIHOMO "$VERSION_MIHOMO" } _get_random_val() { cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 6 } _is_regular_sudo() { _is_root && [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != 'root' ] } _is_root() { [ "$(id -u)" -eq 0 ] } _quit() { _is_regular_sudo && exec su "$SUDO_USER" exec "$SHELL" -i -c "$*" }