Browse Source

first commit

Gogs 1 week ago
commit
eb06ffd9a6

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+.idea
+.vscode
+config.yaml
+resources/bin/**
+!.gitkeep

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024-2025 libm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 184 - 0
README.md

@@ -0,0 +1,184 @@
+# Linux 一键安装 Clash
+
+![GitHub License](https://img.shields.io/github/license/nelvko/clash-for-linux-install)
+![GitHub top language](https://img.shields.io/github/languages/top/nelvko/clash-for-linux-install)
+![GitHub Repo stars](https://img.shields.io/github/stars/nelvko/clash-for-linux-install)
+
+![preview](resources/preview.png)
+
+- 默认安装 `mihomo` 内核,[可选安装](https://github.com/nelvko/clash-for-linux-install/wiki/FAQ#%E5%AE%89%E8%A3%85-clash-%E5%86%85%E6%A0%B8) `clash`。
+- 支持使用 [subconverter](https://github.com/tindy2013/subconverter) 进行本地订阅转换。
+- 多架构支持,适配主流 `Linux` 发行版:`CentOS 7.6`、`Debian 12`、`Ubuntu 24.04.1 LTS`。
+
+## 快速开始
+
+### 环境要求
+
+- 用户权限:`root` 或 `sudo` 用户。普通用户请戳:[#91](https://github.com/nelvko/clash-for-linux-install/issues/91)
+- `shell` 支持:`bash`、`zsh`、`fish`。
+
+### 一键安装
+
+下述命令适用于 `x86_64` 架构,其他架构请戳:[一键安装-多架构](https://github.com/nelvko/clash-for-linux-install/wiki#%E4%B8%80%E9%94%AE%E5%AE%89%E8%A3%85-%E5%A4%9A%E6%9E%B6%E6%9E%84)
+
+```bash
+git clone --branch master --depth 1 https://gh-proxy.com/https://github.com/nelvko/clash-for-linux-install.git \
+  && cd clash-for-linux-install \
+  && sudo bash install.sh
+```
+
+> 如遇问题,请在查阅[常见问题](https://github.com/nelvko/clash-for-linux-install/wiki/FAQ)及 [issue](https://github.com/nelvko/clash-for-linux-install/issues?q=is%3Aissue) 未果后进行反馈。
+
+- 上述克隆命令使用了[加速前缀](https://gh-proxy.com/),如失效请更换其他[可用链接](https://ghproxy.link/)。
+- 默认通过远程订阅获取配置进行安装,本地配置安装详见:[#39](https://github.com/nelvko/clash-for-linux-install/issues/39)
+- 没有订阅?[click me](https://次元.net/auth/register?code=oUbI)
+
+### 命令一览
+
+执行 `clashctl` 列出开箱即用的快捷命令。
+
+
+```bash
+$ clashctl
+Usage:
+    clashctl    COMMAND [OPTION]
+    
+Commands:
+    on                   开启代理
+    off                  关闭代理
+    ui                   面板地址
+    status               内核状况
+    proxy    [on|off]    系统代理
+    tun      [on|off]    Tun 模式
+    mixin    [-e|-r]     Mixin 配置
+    secret   [SECRET]    Web 密钥
+    update   [auto|log]  更新订阅
+```
+
+💡`clashon` 等同于 `clashctl on`,`Tab` 补全更方便!
+
+### 优雅启停
+
+```bash
+$ clashon
+😼 已开启代理环境
+
+$ clashoff
+😼 已关闭代理环境
+```
+- 启停代理内核的同时,设置系统代理。
+- 亦可通过 `clashproxy` 单独控制系统代理。
+
+### Web 控制台
+
+```bash
+$ clashui
+╔═══════════════════════════════════════════════╗
+║                😼 Web 控制台                  ║
+║═══════════════════════════════════════════════║
+║                                               ║
+║     🔓 注意放行端口:9090                      ║
+║     🏠 内网:http://192.168.0.1:9090/ui       ║
+║     🌏 公网:http://255.255.255.255:9090/ui   ║
+║     ☁️ 公共:http://board.zash.run.place      ║
+║                                               ║
+╚═══════════════════════════════════════════════╝
+
+$ clashsecret 666
+😼 密钥更新成功,已重启生效
+
+$ clashsecret
+😼 当前密钥:666
+```
+
+- 通过浏览器打开 Web 控制台,实现可视化操作:切换节点、查看日志等。
+- 若暴露到公网使用建议定期更换密钥。
+
+### 更新订阅
+
+```bash
+$ clashupdate https://example.com
+👌 正在下载:原配置已备份...
+🍃 下载成功:内核验证配置...
+🍃 订阅更新成功
+
+$ clashupdate auto [url]
+😼 已设置定时更新订阅
+
+$ clashupdate log
+✅ [2025-02-23 22:45:23] 订阅更新成功:https://example.com
+```
+
+- `clashupdate` 会记住上次更新成功的订阅链接,后续执行无需再指定。
+- 可通过 `crontab -e` 修改定时更新频率及订阅链接。
+- 通过配置文件进行更新:[pr#24](https://github.com/nelvko/clash-for-linux-install/pull/24#issuecomment-2565054701)
+
+### `Tun` 模式
+
+```bash
+$ clashtun
+😾 Tun 状态:关闭
+
+$ clashtun on
+😼 Tun 模式已开启
+```
+
+- 作用:实现本机及 `Docker` 等容器的所有流量路由到 `clash` 代理、DNS 劫持等。
+- 原理:[clash-verge-rev](https://www.clashverge.dev/guide/term.html#tun)、 [clash.wiki](https://clash.wiki/premium/tun-device.html)。
+- 注意事项:[#100](https://github.com/nelvko/clash-for-linux-install/issues/100#issuecomment-2782680205)
+
+### `Mixin` 配置
+
+```bash
+$ clashmixin
+😼 less 查看 mixin 配置
+
+$ clashmixin -e
+😼 vim 编辑 mixin 配置
+
+$ clashmixin -r
+😼 less 查看 运行时 配置
+```
+
+- 持久化:将自定义配置项写入`Mixin`(`mixin.yaml`),而非原订阅配置(`config.yaml`),可避免更新订阅后丢失。
+- 配置加载:代理内核启动时使用 `runtime.yaml`,它是订阅配置与 `Mixin` 配置的合并结果集,相同配置项以 `Mixin` 为准。
+- 注意:因此直接修改 `config.yaml` 并不会生效。
+
+### 卸载
+
+```bash
+sudo bash uninstall.sh
+```
+
+## 常见问题
+
+[wiki](https://github.com/nelvko/clash-for-linux-install/wiki/FAQ)
+
+## 引用
+
+- [Clash 知识库](https://clash.wiki/)
+- [Clash 家族下载](https://www.clash.la/releases/)
+- [Clash Premium](https://downloads.clash.wiki/ClashPremium/)
+- [mihomo](https://github.com/MetaCubeX/mihomo)
+- [subconverter: 订阅转换](https://github.com/tindy2013/subconverter)
+- [yacd: Web 控制台](https://github.com/haishanh/yacd)
+- [yq: 处理 yaml](https://github.com/mikefarah/yq)
+
+## Star History
+
+<a href="https://www.star-history.com/#nelvko/clash-for-linux-install&Date">
+ <picture>
+   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=nelvko/clash-for-linux-install&type=Date&theme=dark" />
+   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=nelvko/clash-for-linux-install&type=Date" />
+   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=nelvko/clash-for-linux-install&type=Date" />
+ </picture>
+</a>
+
+## Thanks
+
+[@鑫哥](https://github.com/TrackRay)
+
+## 特别声明
+
+1. 编写本项目主要目的为学习和研究 `Shell` 编程,不得将本项目中任何内容用于违反国家/地区/组织等的法律法规或相关规定的其他用途。
+2. 本项目保留随时对免责声明进行补充或更改的权利,直接或间接使用本项目内容的个人或组织,视为接受本项目的特别声明。

+ 61 - 0
install.sh

@@ -0,0 +1,61 @@
+# shellcheck disable=SC2148
+# shellcheck disable=SC1091
+. script/common.sh >&/dev/null
+. script/clashctl.sh >&/dev/null
+
+_valid_env
+
+[ -d "$CLASH_BASE_DIR" ] && _error_quit "请先执行卸载脚本,以清除安装路径:$CLASH_BASE_DIR"
+
+_get_kernel
+
+/usr/bin/install -D <(gzip -dc "$ZIP_KERNEL") "${RESOURCES_BIN_DIR}/$BIN_KERNEL_NAME"
+tar -xf "$ZIP_SUBCONVERTER" -C "$RESOURCES_BIN_DIR"
+tar -xf "$ZIP_YQ" -C "${RESOURCES_BIN_DIR}"
+# shellcheck disable=SC2086
+/bin/mv -f ${RESOURCES_BIN_DIR}/yq_* "${RESOURCES_BIN_DIR}/yq"
+
+_set_bin "$RESOURCES_BIN_DIR"
+_valid_config "$RESOURCES_CONFIG" || {
+    echo -n "$(_okcat '✈️ ' '输入订阅:')"
+    read -r url
+    _okcat '⏳' '正在下载...'
+    _download_config "$RESOURCES_CONFIG" "$url" || _error_quit "下载失败: 请将配置内容写入 $RESOURCES_CONFIG 后重新安装"
+    _valid_config "$RESOURCES_CONFIG" || _error_quit "配置无效,请检查配置:$RESOURCES_CONFIG,转换日志:$BIN_SUBCONVERTER_LOG"
+}
+_okcat '✅' '配置可用'
+mkdir -p "$CLASH_BASE_DIR"
+echo "$url" >"$CLASH_CONFIG_URL"
+
+/bin/cp -rf "$SCRIPT_BASE_DIR" "$CLASH_BASE_DIR"
+/bin/ls "$RESOURCES_BASE_DIR" | grep -Ev 'zip|png' | xargs -I {} /bin/cp -rf "${RESOURCES_BASE_DIR}/{}" "$CLASH_BASE_DIR"
+tar -xf "$ZIP_UI" -C "$CLASH_BASE_DIR"
+
+_set_rc
+_set_bin
+_merge_config_restart
+cat <<EOF >"/usr/lib/systemd/system/${BIN_KERNEL_NAME}.service"
+[Unit]
+Description=$BIN_KERNEL_NAME Daemon, A[nother] Clash Kernel.
+
+[Service]
+Type=simple
+Restart=always
+ExecStart=${BIN_KERNEL} -d ${CLASH_BASE_DIR} -f ${CLASH_CONFIG_RUNTIME}
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+systemctl enable "$BIN_KERNEL_NAME" >&/dev/null || _failcat '💥' "设置自启失败" && _okcat '🚀' "已设置开机自启"
+
+clashui
+# 使用mixin.yaml中已配置的密钥,不再生成随机密钥
+clashsecret
+clashctl
+# shellcheck disable=SC2016
+[ "$SUDO_USER" != 'root' ] && _okcat '请执行 clashon 开启代理环境'
+_okcat '🎉' 'enjoy 🎉'
+clashupgrade
+_quit

BIN
resources/Country.mmdb


+ 44 - 0
resources/mixin.yaml

@@ -0,0 +1,44 @@
+# 系统代理配置
+system-proxy:
+  enable: true
+
+mixed-port: 7890
+
+# Web 控制台配置
+external-controller: "0.0.0.0:54321"
+external-ui: public
+secret: 15629747218hsjH
+
+# 代理服务器配置
+allow-lan: false # 若开启务必设置用户验证以防暴露公网后被滥用
+authentication:
+  # - "username:password" # 用户验证(clashon 会自动填充验证信息)
+
+# 自定义规则
+rules:
+  - DOMAIN,api64.ipify.org,DIRECT # 用于 clashui 获取真实公网 IP
+
+# tun 配置
+tun:
+  enable: false
+  stack: system
+  auto-route: true
+  auto-redir: true # clash
+  auto-redirect: true # mihomo
+  auto-detect-interface: true
+  dns-hijack:
+    - any:53
+    - tcp://any:53
+  strict-route: true
+  exclude-interface:
+    # - docker0
+    # - podman0
+
+# DNS 配置
+dns:
+  enable: true
+  listen: 0.0.0.0:1053
+  enhanced-mode: fake-ip
+  nameserver:
+    - 114.114.114.114
+    - 8.8.8.8

BIN
resources/preview.png


BIN
resources/zip/mihomo-linux-amd64-v1-v1.19.14.gz


BIN
resources/zip/subconverter_linux64.tar.gz


BIN
resources/zip/yacd.tar.xz


BIN
resources/zip/yq_linux_amd64.tar.gz


+ 70 - 0
script/clashctl.fish

@@ -0,0 +1,70 @@
+set fn_arr \
+clashui \
+clashstatus \
+clashsecret \
+clashtun \
+clashmixin \
+clashupdate \
+clashhelp
+
+set -gx fish_version $FISH_VERSION
+
+for fn in $fn_arr
+    eval "
+    function $fn
+        bash -i -c '$fn \"\$@\"' -- \$argv
+    end
+    "
+end
+
+
+function clashctl
+    if test -z "$argv"
+        clashhelp
+        return
+    end
+
+
+    set suffix $argv[1]
+    set argv $argv[2..-1]
+
+    switch $suffix
+        case on
+            clashon $argv
+        case off
+            clashoff $argv
+        case '*'
+            clash"$suffix" $argv
+    end
+end
+
+function clashon
+    bash -i -c 'clashon; sudo tee /var/proxy >/dev/null <<EOF
+export http_proxy=$http_proxy
+export https_proxy=$http_proxy
+export HTTP_PROXY=$http_proxy
+export HTTPS_PROXY=$http_proxy
+
+export all_proxy=$all_proxy
+export ALL_PROXY=$all_proxy
+
+export no_proxy=$no_proxy
+export NO_PROXY=$no_proxy
+EOF'
+
+    source /var/proxy
+end
+
+function clashoff
+    bash -i -c 'clashoff'
+
+    set -e \
+    http_proxy \
+    https_proxy \
+    HTTP_PROXY \
+    HTTPS_PROXY \
+    all_proxy \
+    ALL_PROXY \
+    no_proxy \
+    NO_PROXY
+end

+ 381 - 0
script/clashctl.sh

@@ -0,0 +1,381 @@
+# shellcheck disable=SC2148
+# shellcheck disable=SC2155
+
+_set_system_proxy() {
+    local auth=$(sudo "$BIN_YQ" '.authentication[0] // ""' "$CLASH_CONFIG_RUNTIME")
+    [ -n "$auth" ] && auth=$auth@
+
+    local bind_addr=$(sudo "$BIN_YQ" '.bind-address // ""' "$CLASH_CONFIG_RUNTIME")
+    case $bind_addr in "" | "*" | "0.0.0.0") bind_addr=127.0.0.1 ;; esac
+    local http_proxy_addr="http://${auth}${bind_addr}:${MIXED_PORT}"
+    local socks_proxy_addr="socks5h://${auth}${bind_addr}:${MIXED_PORT}"
+    local no_proxy_addr="localhost,127.0.0.1,::1"
+
+    export http_proxy=$http_proxy_addr
+    export https_proxy=$http_proxy
+    export HTTP_PROXY=$http_proxy
+    export HTTPS_PROXY=$http_proxy
+
+    export all_proxy=$socks_proxy_addr
+    export ALL_PROXY=$all_proxy
+
+    export no_proxy=$no_proxy_addr
+    export NO_PROXY=$no_proxy
+}
+
+_unset_system_proxy() {
+    unset http_proxy
+    unset https_proxy
+    unset HTTP_PROXY
+    unset HTTPS_PROXY
+    unset all_proxy
+    unset ALL_PROXY
+    unset no_proxy
+    unset NO_PROXY
+}
+
+function clashon() {
+    _get_proxy_port
+    systemctl is-active "$BIN_KERNEL_NAME" >&/dev/null || {
+        sudo systemctl start "$BIN_KERNEL_NAME" >/dev/null || {
+            _failcat '启动失败: 执行 clashstatus 查看日志'
+            return 1
+        }
+    }
+    clashproxy status >/dev/null && _set_system_proxy
+    _okcat '已开启代理环境'
+}
+
+watch_proxy() {
+    # 新开交互式shell,且无代理变量时
+    [ -z "$http_proxy" ] && [[ $- == *i* ]] && {
+        # root用户自动开启代理环境(普通用户会触发sudo验证密码导致卡住)
+        _is_root && clashon
+    }
+}
+
+function clashoff() {
+    sudo systemctl stop "$BIN_KERNEL_NAME" && _okcat '已关闭代理环境' ||
+        _failcat '关闭失败: 执行 "clashstatus" 查看日志' || return 1
+    _unset_system_proxy
+}
+
+clashrestart() {
+    { clashoff && clashon; } >&/dev/null
+}
+
+function clashproxy() {
+    case "$1" in
+    on)
+        systemctl is-active "$BIN_KERNEL_NAME" >&/dev/null || {
+            _failcat '代理程序未运行,请执行 clashon 开启代理环境'
+            return 1
+        }
+        sudo "$BIN_YQ" -i '.system-proxy.enable = true' "$CLASH_CONFIG_MIXIN"
+        _set_system_proxy
+        _okcat '已开启系统代理'
+        ;;
+    off)
+        sudo "$BIN_YQ" -i '.system-proxy.enable = false' "$CLASH_CONFIG_MIXIN"
+        _unset_system_proxy
+        _okcat '已关闭系统代理'
+        ;;
+    status)
+        local system_proxy_status=$(sudo "$BIN_YQ" '.system-proxy.enable' "$CLASH_CONFIG_MIXIN" 2>/dev/null)
+        [ "$system_proxy_status" = "false" ] && {
+            _failcat "系统代理:关闭"
+            return 1
+        }
+        _okcat "系统代理:开启
+http_proxy: $http_proxy
+socks_proxy:$all_proxy"
+        ;;
+    *)
+        cat <<EOF
+用法: clashproxy [on|off|status]
+    on      开启系统代理
+    off     关闭系统代理
+    status  查看系统代理状态
+EOF
+        ;;
+    esac
+}
+
+function clashstatus() {
+    sudo systemctl status "$BIN_KERNEL_NAME" "$@"
+}
+
+function clashui() {
+    _get_ui_port
+    # 公网ip
+    # ifconfig.me
+    local query_url='api64.ipify.org'
+    local public_ip=$(curl -s --noproxy "*" --location --max-time 2 $query_url)
+    local public_address="http://${public_ip:-公网}:${EXT_PORT}/ui"
+
+    local local_ip=$EXT_IP
+    local local_address="http://${local_ip}:${EXT_PORT}/ui"
+    printf "\n"
+    printf "╔═══════════════════════════════════════════════╗\n"
+    printf "║                %s                  ║\n" "$(_okcat 'Web 控制台')"
+    printf "║═══════════════════════════════════════════════║\n"
+    printf "║                                               ║\n"
+    printf "║     🔓 注意放行端口:%-5s                    ║\n" "$EXT_PORT"
+    printf "║     🏠 内网:%-31s  ║\n" "$local_address"
+    printf "║     🌏 公网:%-31s  ║\n" "$public_address"
+    printf "║     ☁️  公共:%-31s  ║\n" "$URL_CLASH_UI"
+    printf "║                                               ║\n"
+    printf "╚═══════════════════════════════════════════════╝\n"
+    printf "\n"
+}
+
+_merge_config_restart() {
+    local backup="/tmp/rt.backup"
+    sudo cat "$CLASH_CONFIG_RUNTIME" 2>/dev/null | sudo tee $backup >&/dev/null
+    sudo "$BIN_YQ" eval-all '. as $item ireduce ({}; . *+ $item) | (.. | select(tag == "!!seq")) |= unique' \
+        "$CLASH_CONFIG_MIXIN" "$CLASH_CONFIG_RAW" "$CLASH_CONFIG_MIXIN" | sudo tee "$CLASH_CONFIG_RUNTIME" >&/dev/null
+    _valid_config "$CLASH_CONFIG_RUNTIME" || {
+        sudo cat $backup | sudo tee "$CLASH_CONFIG_RUNTIME" >&/dev/null
+        _error_quit "验证失败:请检查 Mixin 配置"
+    }
+    clashrestart
+}
+
+function clashsecret() {
+    case "$#" in
+    0)
+        _okcat "当前密钥:$(sudo "$BIN_YQ" '.secret // ""' "$CLASH_CONFIG_RUNTIME")"
+        ;;
+    1)
+        sudo "$BIN_YQ" -i ".secret = \"$1\"" "$CLASH_CONFIG_MIXIN" || {
+            _failcat "密钥更新失败,请重新输入"
+            return 1
+        }
+        _merge_config_restart
+        _okcat "密钥更新成功,已重启生效"
+        ;;
+    *)
+        _failcat "密钥不要包含空格或使用引号包围"
+        ;;
+    esac
+}
+
+_tunstatus() {
+    local tun_status=$(sudo "$BIN_YQ" '.tun.enable' "${CLASH_CONFIG_RUNTIME}")
+    # shellcheck disable=SC2015
+    [ "$tun_status" = 'true' ] && _okcat 'Tun 状态:启用' || _failcat 'Tun 状态:关闭'
+}
+
+_tunoff() {
+    _tunstatus >/dev/null || return 0
+    sudo "$BIN_YQ" -i '.tun.enable = false' "$CLASH_CONFIG_MIXIN"
+    _merge_config_restart && _okcat "Tun 模式已关闭"
+}
+
+_tunon() {
+    _tunstatus 2>/dev/null && return 0
+    sudo "$BIN_YQ" -i '.tun.enable = true' "$CLASH_CONFIG_MIXIN"
+    _merge_config_restart
+    sleep 0.5s
+    sudo journalctl -u "$BIN_KERNEL_NAME" --since "1 min ago" | grep -E -m1 'unsupported kernel version|Start TUN listening error' && {
+        _tunoff >&/dev/null
+        _error_quit '不支持的内核版本'
+    }
+    _okcat "Tun 模式已开启"
+}
+
+function clashtun() {
+    case "$1" in
+    on)
+        _tunon
+        ;;
+    off)
+        _tunoff
+        ;;
+    *)
+        _tunstatus
+        ;;
+    esac
+}
+
+function clashupdate() {
+    local url=$(cat "$CLASH_CONFIG_URL")
+    local is_auto
+
+    case "$1" in
+    auto)
+        is_auto=true
+        [ -n "$2" ] && url=$2
+        ;;
+    log)
+        sudo tail "${CLASH_UPDATE_LOG}" 2>/dev/null || _failcat "暂无更新日志"
+        return 0
+        ;;
+    *)
+        [ -n "$1" ] && url=$1
+        ;;
+    esac
+
+    # 如果没有提供有效的订阅链接(url为空或者不是http开头),则使用默认配置文件
+    [ "${url:0:4}" != "http" ] && {
+        _failcat "没有提供有效的订阅链接:使用 ${CLASH_CONFIG_RAW} 进行更新..."
+        url="file://$CLASH_CONFIG_RAW"
+    }
+
+    # 如果是自动更新模式,则设置定时任务
+    [ "$is_auto" = true ] && {
+        sudo grep -qs 'clashupdate' "$CLASH_CRON_TAB" || echo "0 0 */2 * * $_SHELL -i -c 'clashupdate $url'" | sudo tee -a "$CLASH_CRON_TAB" >&/dev/null
+        _okcat "已设置定时更新订阅" && return 0
+    }
+
+    _okcat '👌' "正在下载:原配置已备份..."
+    sudo cat "$CLASH_CONFIG_RAW" | sudo tee "$CLASH_CONFIG_RAW_BAK" >&/dev/null
+
+    _rollback() {
+        _failcat '🍂' "$1"
+        sudo cat "$CLASH_CONFIG_RAW_BAK" | sudo tee "$CLASH_CONFIG_RAW" >&/dev/null
+        _failcat '❌' "[$(date +"%Y-%m-%d %H:%M:%S")] 订阅更新失败:$url" 2>&1 | sudo tee -a "${CLASH_UPDATE_LOG}" >&/dev/null
+        _error_quit
+    }
+
+    _download_config "$CLASH_CONFIG_RAW" "$url" || _rollback "下载失败:已回滚配置"
+    _valid_config "$CLASH_CONFIG_RAW" || _rollback "转换失败:已回滚配置,转换日志:$BIN_SUBCONVERTER_LOG"
+
+    _merge_config_restart && _okcat '🍃' '订阅更新成功'
+    echo "$url" | sudo tee "$CLASH_CONFIG_URL" >&/dev/null
+    _okcat '✅' "[$(date +"%Y-%m-%d %H:%M:%S")] 订阅更新成功:$url" | sudo tee -a "${CLASH_UPDATE_LOG}" >&/dev/null
+}
+
+function clashmixin() {
+    case "$1" in
+    -e)
+        sudo vim "$CLASH_CONFIG_MIXIN" && {
+            _merge_config_restart && _okcat "配置更新成功,已重启生效"
+        }
+        ;;
+    -r)
+        less -f "$CLASH_CONFIG_RUNTIME"
+        ;;
+    *)
+        less -f "$CLASH_CONFIG_MIXIN"
+        ;;
+    esac
+}
+
+function clashupgrade() {
+    case "$1" in
+    -h | --help)
+        cat <<EOF
+
+- 升级当前版本
+  clashupgrade
+
+- 升级到稳定版
+  clashupgrade release
+
+- 升级到测试版
+  clashupgrade alpha
+
+EOF
+        return 0
+        ;;
+    release)
+        channel="release"
+        ;;
+    alpha)
+        channel="alpha"
+        ;;
+    *)
+        channel=""
+        ;;
+    esac
+
+    _okcat "请求内核升级..."
+    _get_ui_port
+    local secret=$(sudo "$BIN_YQ" '.secret // ""' "$CLASH_CONFIG_RUNTIME")
+    local res=$(
+        curl -X POST \
+            --silent \
+            --noproxy "*" \
+            --location \
+            -H "Authorization: Bearer $secret" \
+            "http://${EXT_IP}:${EXT_PORT}/upgrade?channel=$channel"
+    )
+
+    grep -qs '"status":"ok"' <<<"$res" && {
+        _okcat "内核升级成功"
+        return 0
+    }
+    grep 'already using latest version' <<<"$res" && {
+        _okcat "已是最新版本"
+        return 0
+    }
+    _failcat "升级请求失败,请检查网络或稍后重试"
+
+}
+
+function clashctl() {
+    case "$1" in
+    on)
+        clashon
+        ;;
+    off)
+        clashoff
+        ;;
+    ui)
+        clashui
+        ;;
+    status)
+        shift
+        clashstatus "$@"
+        ;;
+    proxy)
+        shift
+        clashproxy "$@"
+        ;;
+    tun)
+        shift
+        clashtun "$@"
+        ;;
+    mixin)
+        shift
+        clashmixin "$@"
+        ;;
+    secret)
+        shift
+        clashsecret "$@"
+        ;;
+    update)
+        shift
+        clashupdate "$@"
+        ;;
+    upgrade)
+        shift
+        clashupgrade "$@"
+        ;;
+    *)
+        clashhelp "$@"
+        ;;
+    esac
+}
+
+clashhelp() {
+    cat <<EOF
+    
+Usage:
+    clashctl COMMAND  [OPTION]
+
+Commands:
+    on                      开启代理
+    off                     关闭代理
+    proxy    [on|off]       系统代理
+    ui                      面板地址
+    status                  内核状况
+    tun      [on|off]       Tun 模式
+    mixin    [-e|-r]        Mixin 配置
+    secret   [SECRET]       Web 密钥
+    update   [auto|log]     更新订阅
+    upgrade                 升级内核
+
+EOF
+}

+ 372 - 0
script/common.sh

@@ -0,0 +1,372 @@
+# shellcheck disable=SC2148
+# shellcheck disable=SC2034
+# shellcheck disable=SC2155
+[ -n "$BASH_VERSION" ] && set +o noglob
+[ -n "$ZSH_VERSION" ] && setopt glob no_nomatch
+
+URL_GH_PROXY='https://gh-proxy.com/'
+URL_CLASH_UI="http://board.zash.run.place"
+
+SCRIPT_BASE_DIR='./script'
+SCRIPT_FISH="${SCRIPT_BASE_DIR}/clashctl.fish"
+
+RESOURCES_BASE_DIR='./resources'
+RESOURCES_BIN_DIR="${RESOURCES_BASE_DIR}/bin"
+RESOURCES_CONFIG="${RESOURCES_BASE_DIR}/config.yaml"
+RESOURCES_CONFIG_MIXIN="${RESOURCES_BASE_DIR}/mixin.yaml"
+
+ZIP_BASE_DIR="${RESOURCES_BASE_DIR}/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*)
+ZIP_UI="${ZIP_BASE_DIR}/yacd.tar.xz"
+
+CLASH_BASE_DIR='/develop/clash'
+CLASH_SCRIPT_DIR="${CLASH_BASE_DIR}/$(basename $SCRIPT_BASE_DIR)"
+CLASH_CONFIG_URL="${CLASH_BASE_DIR}/url"
+CLASH_CONFIG_RAW="${CLASH_BASE_DIR}/$(basename $RESOURCES_CONFIG)"
+CLASH_CONFIG_RAW_BAK="${CLASH_CONFIG_RAW}.bak"
+CLASH_CONFIG_MIXIN="${CLASH_BASE_DIR}/$(basename $RESOURCES_CONFIG_MIXIN)"
+CLASH_CONFIG_RUNTIME="${CLASH_BASE_DIR}/runtime.yaml"
+CLASH_UPDATE_LOG="${CLASH_BASE_DIR}/clashupdate.log"
+
+_set_var() {
+    local user=$USER
+    local home=$HOME
+    [ -n "$SUDO_USER" ] && {
+        user=$SUDO_USER
+        home=$(awk -F: -v user="$SUDO_USER" '$1==user{print $6}' /etc/passwd)
+    }
+
+    [ -n "$BASH_VERSION" ] && {
+        _SHELL=bash
+    }
+    [ -n "$ZSH_VERSION" ] && {
+        _SHELL=zsh
+    }
+    [ -n "$fish_version" ] && {
+        _SHELL=fish
+    }
+
+    # rc文件路径
+    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"
+    }
+
+    # 定时任务路径
+    local os_info=$(cat /etc/os-release)
+    echo "$os_info" | grep -iqsE "rhel|centos|openEuler|Rocky|AlmaLinux" && CLASH_CRON_TAB="/var/spool/cron/$user"
+    echo "$os_info" | grep -iqsE "debian|ubuntu" && CLASH_CRON_TAB="/var/spool/cron/crontabs/$user"
+}
+_set_var
+
+# shellcheck disable=SC2120
+_set_bin() {
+    local bin_base_dir="${CLASH_BASE_DIR}/bin"
+    [ -n "$1" ] && bin_base_dir=$1
+    BIN_CLASH="${bin_base_dir}/clash"
+    BIN_MIHOMO="${bin_base_dir}/mihomo"
+    BIN_YQ="${bin_base_dir}/yq"
+    BIN_SUBCONVERTER_DIR="${bin_base_dir}/subconverter"
+    BIN_SUBCONVERTER_CONFIG="$BIN_SUBCONVERTER_DIR/pref.yml"
+    BIN_SUBCONVERTER_PORT="25500"
+    BIN_SUBCONVERTER="${BIN_SUBCONVERTER_DIR}/subconverter"
+    BIN_SUBCONVERTER_LOG="${BIN_SUBCONVERTER_DIR}/latest.log"
+
+    [ -f "$BIN_CLASH" ] && {
+        BIN_KERNEL=$BIN_CLASH
+    }
+    [ -f "$BIN_MIHOMO" ] && {
+        BIN_KERNEL=$BIN_MIHOMO
+    }
+    BIN_KERNEL_NAME=$(basename "$BIN_KERNEL")
+}
+_set_bin
+
+_set_rc() {
+    [ "$1" = "unset" ] && {
+        sed -i "\|$CLASH_SCRIPT_DIR|d" "$SHELL_RC_BASH" "$SHELL_RC_ZSH" 2>/dev/null
+        rm -f "$SHELL_RC_FISH" 2>/dev/null
+        return
+    }
+
+    echo "source $CLASH_SCRIPT_DIR/common.sh && source $CLASH_SCRIPT_DIR/clashctl.sh" |
+        tee -a "$SHELL_RC_BASH" "$SHELL_RC_ZSH" >&/dev/null
+    [ -n "$SHELL_RC_FISH" ] && /usr/bin/install $SCRIPT_FISH "$SHELL_RC_FISH"
+}
+
+# 默认集成、安装mihomo内核
+# 移除/删除mihomo:下载安装clash内核
+function _get_kernel() {
+    [ -f "$ZIP_CLASH" ] && {
+        ZIP_KERNEL=$ZIP_CLASH
+        BIN_KERNEL=$BIN_CLASH
+    }
+
+    [ -f "$ZIP_MIHOMO" ] && {
+        ZIP_KERNEL=$ZIP_MIHOMO
+        BIN_KERNEL=$BIN_MIHOMO
+    }
+
+    [ ! -f "$ZIP_MIHOMO" ] && [ ! -f "$ZIP_CLASH" ] && {
+        local arch=$(uname -m)
+        _failcat "${ZIP_BASE_DIR}:未检测到可用的内核压缩包"
+        _download_clash "$arch"
+        ZIP_KERNEL=$ZIP_CLASH
+        BIN_KERNEL=$BIN_CLASH
+    }
+
+    BIN_KERNEL_NAME=$(basename "$BIN_KERNEL")
+    _okcat "安装内核:$BIN_KERNEL_NAME"
+}
+
+_get_random_port() {
+    local randomPort=$(shuf -i 1024-65535 -n 1)
+    ! _is_bind "$randomPort" && { echo "$randomPort" && return; }
+    _get_random_port
+}
+
+function _get_proxy_port() {
+    MIXED_PORT=$(sudo "$BIN_YQ" '.mixed-port' $CLASH_CONFIG_RUNTIME)
+
+    _is_already_in_use "$MIXED_PORT" "$BIN_KERNEL_NAME" && {
+        local newPort=$(_get_random_port)
+        local msg="端口占用:${MIXED_PORT} 🎲 随机分配:$newPort"
+        sudo "$BIN_YQ" -i ".mixed-port = $newPort" $CLASH_CONFIG_RUNTIME
+        MIXED_PORT=$newPort
+        _failcat '🎯' "$msg"
+    }
+}
+
+function _get_ui_port() {
+    local ext_addr=$(sudo "$BIN_YQ" '.external-controller // ""' $CLASH_CONFIG_RUNTIME)
+    local ext_ip=${ext_addr%%:*}
+    EXT_IP=$ext_ip
+    EXT_PORT=${ext_addr##*:}
+    # ip route get 1.1.1.1 | grep -oP 'src \K\S+'
+    [ "$ext_ip" = '0.0.0.0' ] && EXT_IP=$(hostname -I | awk '{print $1}')
+    _is_already_in_use "$EXT_PORT" "$BIN_KERNEL_NAME" && {
+        local newPort=$(_get_random_port)
+        local msg="端口占用:${EXT_PORT} 🎲 随机分配:$newPort"
+        sudo "$BIN_YQ" -i ".external-controller = \"$ext_ip:$newPort\"" $CLASH_CONFIG_RUNTIME
+        EXT_PORT=$newPort
+        _failcat '🎯' "$msg"
+    }
+}
+
+_get_color() {
+    local hex="${1#\#}"
+    local r=$((16#${hex:0:2}))
+    local g=$((16#${hex:2:2}))
+    local b=$((16#${hex:4:2}))
+    printf "\e[38;2;%d;%d;%dm" "$r" "$g" "$b"
+}
+_get_color_msg() {
+    local color=$(_get_color "$1")
+    local msg=$2
+    local reset="\033[0m"
+    printf "%b%s%b\n" "$color" "$msg" "$reset"
+}
+
+_get_random_val() {
+    cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 6
+}
+
+function _okcat() {
+    local color=#c8d6e5
+    local emoji=😼
+    [ $# -gt 1 ] && emoji=$1 && shift
+    local msg="${emoji} $1"
+    _get_color_msg "$color" "$msg" && return 0
+}
+
+function _failcat() {
+    local color=#fd79a8
+    local emoji=😾
+    [ $# -gt 1 ] && emoji=$1 && shift
+    local msg="${emoji} $1"
+    _get_color_msg "$color" "$msg" >&2 && return 1
+}
+
+function _quit() {
+    local user=root
+    [ -n "$SUDO_USER" ] && user=$SUDO_USER
+    exec sudo -u "$user" -- "$_SHELL" -i
+}
+
+function _error_quit() {
+    [ $# -gt 0 ] && {
+        local color=#f92f60
+        local emoji=📢
+        [ $# -gt 1 ] && emoji=$1 && shift
+        local msg="${emoji} $1"
+        _get_color_msg "$color" "$msg"
+    }
+    exec $_SHELL -i
+}
+
+_is_bind() {
+    local port=$1
+    { sudo ss -lnptu || sudo netstat -lnptu; } | grep ":${port}\b"
+}
+
+_is_already_in_use() {
+    local port=$1
+    local progress=$2
+    _is_bind "$port" | grep -qs -v "$progress"
+}
+
+function _is_root() {
+    [ "$(whoami)" = "root" ]
+}
+
+function _valid_env() {
+    _is_root || _error_quit "需要 root 或 sudo 权限执行"
+    [ "$(ps -p 1 -o comm=)" != "systemd" ] && _error_quit "系统不具备 systemd"
+}
+
+function _valid_config() {
+    [ -e "$1" ] && [ "$(wc -l <"$1")" -gt 1 ] && {
+        local cmd msg
+        cmd="sudo $BIN_KERNEL -d $(dirname "$1") -f $1 -t"
+        local is_dat=$(sudo "$BIN_YQ" '.geodata-mode // false' "$1")
+        [ "$is_dat" = "true" ] && {
+            sudo "$BIN_YQ" -i ".geodata-mode = false" "$1"
+        }
+        msg=$(eval "$cmd") || {
+            eval "$cmd"
+            echo "$msg" | grep -qs "unsupport proxy type" && {
+                local prefix="检测到订阅中包含不受支持的代理协议"
+                [ "$BIN_KERNEL_NAME" = "clash" ] && _error_quit "${prefix}, 推荐安装使用 mihomo 内核"
+                _error_quit "${prefix}, 请检查并升级内核版本"
+            }
+        }
+    }
+}
+
+_download_clash() {
+    local arch=$1
+    local url sha256sum
+    case "$arch" in
+    x86_64)
+        url=https://downloads.clash.wiki/ClashPremium/clash-linux-amd64-2023.08.17.gz
+        sha256sum='92380f053f083e3794c1681583be013a57b160292d1d9e1056e7fa1c2d948747'
+        ;;
+    *86*)
+        url=https://downloads.clash.wiki/ClashPremium/clash-linux-386-2023.08.17.gz
+        sha256sum='254125efa731ade3c1bf7cfd83ae09a824e1361592ccd7c0cccd2a266dcb92b5'
+        ;;
+    armv*)
+        url=https://downloads.clash.wiki/ClashPremium/clash-linux-armv5-2023.08.17.gz
+        sha256sum='622f5e774847782b6d54066f0716114a088f143f9bdd37edf3394ae8253062e8'
+        ;;
+    aarch64)
+        url=https://downloads.clash.wiki/ClashPremium/clash-linux-arm64-2023.08.17.gz
+        sha256sum='c45b39bb241e270ae5f4498e2af75cecc0f03c9db3c0db5e55c8c4919f01afdd'
+        ;;
+    *)
+        _error_quit "未知的架构版本:$arch,请自行下载对应版本至 ${ZIP_BASE_DIR} 目录下:https://downloads.clash.wiki/ClashPremium/"
+        ;;
+    esac
+
+    _okcat '⏳' "正在下载:clash:${arch} 架构..."
+    ZIP_CLASH="${ZIP_BASE_DIR}/$(basename $url)"
+    curl \
+        --progress-bar \
+        --show-error \
+        --fail \
+        --insecure \
+        --location \
+        --connect-timeout 5 \
+        --max-time 15 \
+        --retry 1 \
+        --output "$ZIP_CLASH" \
+        "$url"
+    echo $sha256sum "$ZIP_CLASH" | sha256sum -c ||
+        _error_quit "下载失败:请自行下载对应版本至 ${ZIP_BASE_DIR} 目录下:https://downloads.clash.wiki/ClashPremium/"
+}
+
+_download_raw_config() {
+    local dest=$1
+    local url=$2
+    local agent='clash-verge/v2.0.4'
+    sudo curl \
+        --silent \
+        --show-error \
+        --insecure \
+        --location \
+        --max-time 5 \
+        --retry 1 \
+        --user-agent "$agent" \
+        --output "$dest" \
+        "$url" ||
+        sudo wget \
+            --no-verbose \
+            --no-check-certificate \
+            --timeout 3 \
+            --tries 1 \
+            --user-agent "$agent" \
+            --output-document "$dest" \
+            "$url"
+}
+_download_convert_config() {
+    local dest=$1
+    local url=$2
+    _start_convert
+    local convert_url=$(
+        target='clash'
+        base_url="http://127.0.0.1:${BIN_SUBCONVERTER_PORT}/sub"
+        curl \
+            --get \
+            --silent \
+            --location \
+            --output /dev/null \
+            --data-urlencode "target=$target" \
+            --data-urlencode "url=$url" \
+            --write-out '%{url_effective}' \
+            "$base_url"
+    )
+    _download_raw_config "$dest" "$convert_url"
+    _stop_convert
+}
+function _download_config() {
+    local dest=$1
+    local url=$2
+    [ "${url:0:4}" = 'file' ] && return 0
+    _download_raw_config "$dest" "$url" || return 1
+    _okcat '🍃' '下载成功:内核验证配置...'
+    _valid_config "$dest" || {
+        _failcat '🍂' "验证失败:尝试订阅转换..."
+        _download_convert_config "$dest" "$url" || _failcat '🍂' "转换失败:请检查日志:$BIN_SUBCONVERTER_LOG"
+    }
+}
+
+_start_convert() {
+    _is_already_in_use $BIN_SUBCONVERTER_PORT 'subconverter' && {
+        local newPort=$(_get_random_port)
+        _failcat '🎯' "端口占用:$BIN_SUBCONVERTER_PORT 🎲 随机分配:$newPort"
+        [ ! -e "$BIN_SUBCONVERTER_CONFIG" ] && {
+            sudo /bin/cp -f "$BIN_SUBCONVERTER_DIR/pref.example.yml" "$BIN_SUBCONVERTER_CONFIG"
+        }
+        sudo "$BIN_YQ" -i ".server.port = $newPort" "$BIN_SUBCONVERTER_CONFIG"
+        BIN_SUBCONVERTER_PORT=$newPort
+    }
+    local start=$(date +%s)
+    # 子shell运行,屏蔽kill时的输出
+    (sudo "$BIN_SUBCONVERTER" 2>&1 | sudo tee "$BIN_SUBCONVERTER_LOG" >/dev/null &)
+    while ! _is_bind "$BIN_SUBCONVERTER_PORT" >&/dev/null; do
+        sleep 1s
+        local now=$(date +%s)
+        [ $((now - start)) -gt 1 ] && _error_quit "订阅转换服务未启动,请检查日志:$BIN_SUBCONVERTER_LOG"
+    done
+}
+_stop_convert() {
+    sudo pkill -9 -f "$BIN_SUBCONVERTER" >&/dev/null
+}

+ 20 - 0
uninstall.sh

@@ -0,0 +1,20 @@
+# shellcheck disable=SC2148
+# shellcheck disable=SC1091
+. script/common.sh >&/dev/null
+. script/clashctl.sh >&/dev/null
+
+_valid_env
+
+clashoff >&/dev/null
+
+systemctl disable "$BIN_KERNEL_NAME" >&/dev/null
+rm -f "/etc/systemd/system/${BIN_KERNEL_NAME}.service"
+systemctl daemon-reload
+
+rm -rf "$CLASH_BASE_DIR"
+rm -rf "$RESOURCES_BIN_DIR"
+sed -i '/clashupdate/d' "$CLASH_CRON_TAB" >&/dev/null
+_set_rc unset
+
+_okcat '✨' '已卸载,相关配置已清除'
+_quit