Repository: nelvko/clash-for-linux-install Branch: master Commit: 39ca338dbd19 Files: 26 Total size: 54.1 KB Directory structure: gitextract_4jtgepcj/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feat_report.yml │ │ └── q&a.yml │ └── workflows/ │ └── stale-issues.yml ├── .gitignore ├── .shellcheckrc ├── LICENSE ├── README.md ├── install.sh ├── resources/ │ ├── Country.mmdb │ ├── mixin.yaml │ ├── profiles/ │ │ └── .gitkeep │ └── profiles.yaml ├── scripts/ │ ├── cmd/ │ │ ├── clashctl.fish │ │ ├── clashctl.sh │ │ └── common.sh │ ├── init/ │ │ ├── OpenRC.sh │ │ ├── SysVinit.sh │ │ ├── runit.sh │ │ └── systemd.sh │ └── preflight.sh └── uninstall.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf indent_style = space indent_size = 2 ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: ['https://afdian.com/a/nelvko']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug 反馈 description: "提交项目 Bug 报告" title: "[Bug] " labels: ["bug"] body: - type: checkboxes id: ensure attributes: label: 前置确认 description: 在提交之前,请勾选以下选项以确认您已完成必要的自助排查 options: - label: 我确认这是项目 **Bug**, 而非操作不当或使用不清楚。否则请使用其他 Issue 模板或移步[讨论区](https://github.com/nelvko/clash-for-linux-install/discussions) required: true - label: 我已查阅 [常见问题](https://github.com/nelvko/clash-for-linux-install/wiki/FAQ),但仍未找到解决方案 required: true - label: 我已使用关键字查询相关 [Issues](https://github.com/nelvko/clash-for-linux-install/issues?q=is%3Aissue),但仍未找到解决方案 required: true - label: 我已确认本地与远程仓库最新提交的哈希一致。 (`git rev-parse --short HEAD`) required: true - type: input id: branch attributes: label: 使用的分支 placeholder: master / ... validations: required: true - type: input id: commit_id attributes: label: Commit ID description: | 请提供当前 Commit 哈希:`git rev-parse --short HEAD` validations: required: true - type: input id: shell attributes: label: Shell 环境 placeholder: bash / zsh / ... validations: required: true - type: textarea id: system attributes: label: 系统信息 description: | 请提供系统相关信息,可任选以下命令输出之一: ```bash hostnamectl lsb_release -a uname -a ``` render: shell validations: required: true - type: textarea id: desc attributes: label: 描述 & 复现 description: | 请详细描述: 1. 操作步骤 2. 期望结果 3. 实际表现 4. ~~必要时~~最好附上截图 **请勿泄露订阅、IP 等敏感信息** # placeholder: | validations: required: true - type: textarea id: logs attributes: label: 日志 description: 请提供相关日志输出或截图 render: shell ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feat_report.yml ================================================ name: 功能建议 description: 为该项目提出建议 title: "[Feature] " labels: ["enhancement"] body: - type: textarea attributes: label: 描述 description: 请提供对于该功能的详细描述。 validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/q&a.yml ================================================ name: Q&A description: "问题反馈与解答" title: "[Q&A] " labels: [] body: - type: checkboxes id: ensure attributes: label: 前置确认 description: 大部分使用问题可以在下述选项中得到解决,在提交问题前,请先进行自助排查。 options: - label: 我已查阅 [常见问题](https://github.com/nelvko/clash-for-linux-install/wiki/FAQ),但仍未找到解决方案 required: true - label: 我已使用关键字查询相关 [Issues](https://github.com/nelvko/clash-for-linux-install/issues?q=is%3Aissue),但仍未找到解决方案 required: true - type: input id: branch attributes: label: 使用的分支 placeholder: master / ... validations: required: true - type: input id: commit_id attributes: label: Commit ID description: | 请提供当前 Commit 哈希:`git rev-parse --short HEAD` validations: required: true - type: input id: shell attributes: label: Shell 环境 placeholder: bash / zsh / ... validations: required: true - type: textarea id: desc attributes: label: 问题描述 description: | ~~必要时~~最好附上截图。 **请勿泄露订阅、IP 等敏感信息** validations: required: true ================================================ FILE: .github/workflows/stale-issues.yml ================================================ name: Close inactive issues on: schedule: - cron: "0 9 * * 1-5" workflow_dispatch: # 允许手动触发 jobs: close-issues: runs-on: ubuntu-latest permissions: issues: write actions: write steps: - uses: actions/stale@v9 with: exempt-issue-labels: "bug,documentation,enhancement" days-before-issue-stale: 7 days-before-issue-close: 7 stale-issue-label: "stale" stale-issue-message: "长时间未活跃,将在 7 天后自动关闭。若仍需讨论,请回复以保持活跃。" close-issue-message: "已自动关闭,如未解决,可重新打开。" repo-token: ${{ secrets.GITHUB_TOKEN }} # 自动生成 ================================================ FILE: .gitignore ================================================ .idea .vscode config.yaml* resources/zip/* !resources/zip/dist.zip resources/dist/ test.sh !.gitkeep ================================================ FILE: .shellcheckrc ================================================ disable=SC1091 disable=SC2155 disable=SC2296 disable=SC2153 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024-2026 nelvko 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. ================================================ FILE: README.md ================================================ # 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` 与 `clash` 代理内核。 - 兼容 `root` 与普通用户环境。 - 适配主流 `Linux` 发行版,并兼容 `AutoDL` 等容器化环境。 - 自动检测端口占用情况,在冲突时随机分配可用端口。 - 自动识别系统架构与初始化系统,下载匹配的内核与依赖,并生成对应的服务管理配置。 - 在需要时调用 [subconverter](https://github.com/tindy2013/subconverter) 进行本地订阅转换。 ## 🚀 一键安装 在终端中执行以下命令即可完成安装: ```bash git clone --branch master --depth 1 https://gh-proxy.org/https://github.com/nelvko/clash-for-linux-install.git \ && cd clash-for-linux-install \ && bash install.sh ``` - 上述命令使用了[加速前缀](https://gh-proxy.org/),如失效可更换其他[可用链接](https://ghproxy.link/)。 - 可通过 `.env` 文件或脚本参数自定义安装选项。 - 没有订阅?[click me](https://次元.net/auth/register?code=oUbI) ## ⌨️ 命令一览 ```bash Usage: clashctl COMMAND [OPTIONS] Commands: on 开启代理 off 关闭代理 status 内核状况 proxy 系统代理 ui Web 面板 secret Web 密钥 sub 订阅管理 upgrade 升级内核 tun Tun 模式 mixin Mixin 配置 Global Options: -h, --help 显示帮助信息 ``` 💡`clashon` 同 `clashctl on`,`Tab` 补全更方便! ### 优雅启停 ```bash $ clashon 😼 已开启代理环境 $ clashoff 😼 已关闭代理环境 ``` - 在启停代理内核的同时,同步设置系统代理。 - 亦可通过 `clashproxy` 单独控制系统代理。 ### Web 控制台 ```bash $ clashui ╔═══════════════════════════════════════════════╗ ║ 😼 Web 控制台 ║ ║═══════════════════════════════════════════════║ ║ ║ ║ 🔓 注意放行端口:9090 ║ ║ 🏠 内网:http://192.168.0.1:9090/ui ║ ║ 🌏 公网:http://8.8.8.8:9090/ui ║ ║ ☁️ 公共:http://board.zash.run.place ║ ║ ║ ╚═══════════════════════════════════════════════╝ $ clashsecret mysecret 😼 密钥更新成功,已重启生效 $ clashsecret 😼 当前密钥:mysecret ``` - 可通过浏览器打开 `Web` 控制台进行可视化操作,例如切换节点、查看日志等。 - 默认使用 [zashboard](https://github.com/Zephyruso/zashboard) 作为控制台前端,如需更换可自行配置。 - 若需将控制台暴露到公网,建议定期更换访问密钥,或通过 `SSH` 端口转发方式进行安全访问。 ### `Mixin` 配置 ```bash $ clashmixin 😼 查看 Mixin 配置 $ clashmixin -e 😼 编辑 Mixin 配置 $ clashmixin -c 😼 查看原始订阅配置 $ clashmixin -r 😼 查看运行时配置 ``` - 通过 `Mixin` 自定义的配置内容会与原始订阅进行深度合并,且 `Mixin` 具有最高优先级,最终生成内核启动时加载的运行时配置。 - `Mixin` 支持以前置、后置或覆盖的方式,对原始订阅中的规则、节点及策略组进行新增或修改。 ### 升级内核 ```bash $ clashupgrade 😼 请求内核升级... {"status":"ok"} 😼 内核升级成功 ``` - 升级过程由代理内核自动完成;如需查看详细的升级日志,可添加 `-v` 参数。 - 建议通过 `clashmixin` 为 `github` 配置代理规则,以避免因网络问题导致请求失败。 ### 管理订阅 ```bash $ clashsub -h Usage: clashsub COMMAND [OPTIONS] Commands: add 添加订阅 ls 查看订阅 del 删除订阅 use 使用订阅 update [id] 更新订阅 log 订阅日志 Options: update: --auto 配置自动更新 --convert 使用订阅转换 ``` - 支持添加本地订阅,例如:`file:///root/clashctl/resources/config.yaml` - 当订阅链接解析失败或包含特殊字符时,请使用引号包裹以避免被错误解析。 - 自动更新任务可通过 `crontab -e` 进行修改和管理。 ### `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) ## 🗑️ 卸载 ```bash bash uninstall.sh ``` ## 📖 常见问题 👉 [Wiki · FAQ](https://github.com/nelvko/clash-for-linux-install/wiki/FAQ) ## 🔗 引用 - [clash](https://clash.wiki/) - [mihomo](https://github.com/MetaCubeX/mihomo) - [subconverter](https://github.com/tindy2013/subconverter) - [yq](https://github.com/mikefarah/yq) - [zashboard](https://github.com/Zephyruso/zashboard) ## ⭐ Star History Star History Chart ## 🙏 Thanks [@鑫哥](https://github.com/TrackRay) ## ⚠️ 特别声明 1. 编写本项目主要目的为学习和研究 `Shell` 编程,不得将本项目中任何内容用于违反国家/地区/组织等的法律法规或相关规定的其他用途。 2. 本项目保留随时对免责声明进行补充或更改的权利,直接或间接使用本项目内容的个人或组织,视为接受本项目的特别声明。 ================================================ FILE: install.sh ================================================ #!/usr/bin/env bash . scripts/cmd/clashctl.sh . scripts/preflight.sh _valid _parse_args "$@" _prepare_zip _detect_init _okcat "安装内核:$KERNEL_NAME by ${INIT_TYPE}" _okcat '📦' "安装路径:$CLASH_BASE_DIR" /bin/cp -rf . "$CLASH_BASE_DIR" touch "$CLASH_CONFIG_BASE" _set_envs _is_regular_sudo && chown -R "$SUDO_USER" "$CLASH_BASE_DIR" _install_service _apply_rc _merge_config _detect_proxy_port clashui clashsecret "$(_get_random_val)" >/dev/null clashsecret _okcat '🎉' 'enjoy 🎉' clashctl _valid_config "$CLASH_CONFIG_BASE" && CLASH_CONFIG_URL="file://$CLASH_CONFIG_BASE" _quit "clashsub add $CLASH_CONFIG_URL && clashsub use 1" ================================================ FILE: resources/mixin.yaml ================================================ _custom: system-proxy: enable: true mixed-port: 7890 external-controller: "0.0.0.0:9090" external-ui: dist external-ui-url: https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip secret: allow-lan: false # 若开启务必设置用户验证以防暴露公网后被滥用 authentication: # - "username:password" # 用户验证(clashon 会自动填充) # prefix:前置插入 # suffix:后置插入 # override:根据 name 匹配并替换原项 rules: prefix: - DOMAIN,api64.ipify.org,DIRECT # 用于 clashui 获取真实公网 IP # - DOMAIN-KEYWORD,github,proxy # 用于 clashupgrade 升级内核 suffix: proxies: prefix: suffix: override: proxy-groups: prefix: suffix: override: # 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 route-exclude-address: - 1.1.1.1/32 # 用于 clashui 获取真实内网 IP - 127.0.0.1/32 exclude-interface: - docker0 # 避免无法访问容器内服务 - podman0 # 详见:https://github.com/nelvko/clash-for-linux-install/issues/100#issuecomment-2995667368 # DNS 配置 dns: enable: true listen: 0.0.0.0:1053 enhanced-mode: fake-ip nameserver: - 114.114.114.114 - 8.8.8.8 ================================================ FILE: resources/profiles/.gitkeep ================================================ ================================================ FILE: resources/profiles.yaml ================================================ # 当前使用的订阅 use: # 订阅列表 profiles: ================================================ FILE: scripts/cmd/clashctl.fish ================================================ set fn_arr \ clashui \ clashstatus \ clashsecret \ clashtun \ clashmixin \ clashsub \ clashlog \ clashupgrade \ 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 proxy clashproxy $argv case '*' clash"$suffix" $argv end end function clashon bash -i -c 'clashon; sudo tee /var/proxy >/dev/null <&/dev/null && local isActive='true' for entry in "${port_list[@]}"; do local var_name="${entry%|*}" local yaml_key="${entry#*|}" eval "local var_val=\${$var_name}" [ -n "$var_val" ] && _is_port_used "$var_val" && [ "$isActive" != "true" ] && { newPort=$(_get_random_port) ((count++)) _failcat '🎯' "端口冲突:[$yaml_key] $var_val 🎲 随机分配 $newPort" "$BIN_YQ" -i ".${yaml_key} = $newPort" "$CLASH_CONFIG_MIXIN" } done ((count)) && _merge_config } function clashon() { _detect_proxy_port clashstatus >&/dev/null || placeholder_start clashstatus >&/dev/null || { _failcat '启动失败: 执行 clashlog 查看日志' return 1 } clashproxy >/dev/null && _set_system_proxy _okcat '已开启代理环境' } watch_proxy() { [ -z "$http_proxy" ] && { # [[ "$0" == -* ]] && { # 登录式shell [[ $- == *i* ]] && { # 交互式shell placeholder_watch_proxy } } } function clashoff() { clashstatus >&/dev/null && { placeholder_stop >/dev/null clashstatus >&/dev/null && _tunstatus >&/dev/null && { _tunoff || _error_quit "请先关闭 Tun 模式" } placeholder_stop >/dev/null clashstatus >&/dev/null && { _failcat '代理环境关闭失败' return 1 } } _unset_system_proxy _okcat '已关闭代理环境' } clashrestart() { clashoff >/dev/null clashon } function clashproxy() { case "$1" in -h | --help) cat <&/dev/null || { _failcat "$KERNEL_NAME 未运行,请先执行 clashon" return 1 } "$BIN_YQ" -i '._custom.system-proxy.enable = true' "$CLASH_CONFIG_MIXIN" _set_system_proxy _okcat '已开启系统代理' ;; off) "$BIN_YQ" -i '._custom.system-proxy.enable = false' "$CLASH_CONFIG_MIXIN" _unset_system_proxy _okcat '已关闭系统代理' ;; *) local system_proxy_enable=$("$BIN_YQ" '._custom.system-proxy.enable' "$CLASH_CONFIG_MIXIN" 2>/dev/null) case $system_proxy_enable in true) _okcat "系统代理:开启 $(env | grep -i 'proxy=')" ;; *) _failcat "系统代理:关闭" ;; esac ;; esac } function clashstatus() { placeholder_status "$@" placeholder_is_active >&/dev/null } function clashlog() { placeholder_log "$@" } function clashui() { _detect_ext_addr clashstatus >&/dev/null || clashon >/dev/null local query_url='api64.ipify.org' # ifconfig.me 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() { cat "$CLASH_CONFIG_RUNTIME" >"$CLASH_CONFIG_TEMP" 2>/dev/null # shellcheck disable=SC2016 "$BIN_YQ" eval-all ' ######################################## # Load Files # ######################################## select(fileIndex==0) as $config | select(fileIndex==1) as $mixin | ######################################## # Deep Merge # ######################################## $mixin |= del(._custom) | (($config // {}) * $mixin) as $runtime | $runtime | ######################################## # Rules # ######################################## .rules = ( ($mixin.rules.prefix // []) + ($config.rules // []) + ($mixin.rules.suffix // []) ) | ######################################## # Proxies # ######################################## .proxies = ( ($mixin.proxies.prefix // []) + ( ($config.proxies // []) as $configList | ($mixin.proxies.override // []) as $overrideList | $configList | map( . as $configItem | ( $overrideList[] | select(.name == $configItem.name) ) // $configItem ) ) + ($mixin.proxies.suffix // []) ) | ######################################## # ProxyGroups # ######################################## .proxy-groups = ( ($mixin.proxy-groups.prefix // []) + ( ($config.proxy-groups // []) as $configList | ($mixin.proxy-groups.override // []) as $overrideList | $configList | map( . as $configItem | ( $overrideList[] | select(.name == $configItem.name) ) // $configItem ) ) + ($mixin.proxy-groups.suffix // []) ) ' "$CLASH_CONFIG_BASE" "$CLASH_CONFIG_MIXIN" >"$CLASH_CONFIG_RUNTIME" _valid_config "$CLASH_CONFIG_RUNTIME" || { cat "$CLASH_CONFIG_TEMP" >"$CLASH_CONFIG_RUNTIME" _error_quit "验证失败:请检查 Mixin 配置" } } _merge_config_restart() { _merge_config placeholder_stop >/dev/null clashstatus >&/dev/null && _tunstatus >&/dev/null && { _tunoff || _error_quit "请先关闭 Tun 模式" } placeholder_stop >/dev/null sleep 0.1 placeholder_start >/dev/null sleep 0.1 } _get_secret() { "$BIN_YQ" '.secret // ""' "$CLASH_CONFIG_RUNTIME" } function clashsecret() { case "$1" in -h | --help) cat < EOF return 0 ;; esac case $# in 0) _okcat "当前密钥:$(_get_secret)" ;; 1) "$BIN_YQ" -i ".secret = \"$1\"" "$CLASH_CONFIG_MIXIN" || { _failcat "密钥更新失败,请重新输入" return 1 } _merge_config_restart _okcat "密钥更新成功,已重启生效" ;; *) _failcat "密钥不要包含空格或使用引号包围" ;; esac } _tunstatus() { local tun_status=$("$BIN_YQ" '.tun.enable' "${CLASH_CONFIG_RUNTIME}") case $tun_status in true) _okcat 'Tun 状态:启用' ;; *) _failcat 'Tun 状态:关闭' ;; esac } _tunoff() { _tunstatus >/dev/null || return 0 sudo placeholder_stop clashstatus >&/dev/null || { "$BIN_YQ" -i '.tun.enable = false' "$CLASH_CONFIG_MIXIN" _merge_config clashon >/dev/null _okcat "Tun 模式已关闭" return 0 } _tunstatus >&/dev/null && _failcat "Tun 模式关闭失败" } _sudo_restart() { sudo placeholder_stop placeholder_sudo_start sleep 0.5 } _tunon() { _tunstatus 2>/dev/null && return 0 sudo placeholder_stop "$BIN_YQ" -i '.tun.enable = true' "$CLASH_CONFIG_MIXIN" _merge_config placeholder_sudo_start sleep 0.5 clashstatus >&/dev/null || _error_quit "Tun 模式开启失败" local fail_msg="Start TUN listening error|unsupported kernel version" local ok_msg="Tun adapter listening at|TUN listening iface" clashlog | grep -E -m1 -qs "$fail_msg" && { [ "$KERNEL_NAME" = 'mihomo' ] && { "$BIN_YQ" -i '.tun.auto-redirect = false' "$CLASH_CONFIG_MIXIN" _merge_config _sudo_restart } clashlog | grep -E -m1 -qs "$ok_msg" || { clashlog | grep -E -m1 "$fail_msg" _tunoff >&/dev/null _error_quit '系统内核版本不支持 Tun 模式' } } _okcat "Tun 模式已开启" } function clashtun() { case "$1" in -h | --help) cat <&/dev/null || clashon >/dev/null _okcat '⏳' "请求内核升级..." [ "$log_flag" = true ] && { log_cmd=(placeholder_follow_log) ("${log_cmd[@]}" &) } local res=$( curl -X POST \ --silent \ --noproxy "*" \ --location \ -H "Authorization: Bearer $(_get_secret)" \ "http://${EXT_IP}:${EXT_PORT}/upgrade?channel=$channel" ) [ "$log_flag" = true ] && pkill -9 -f "${log_cmd[*]}" grep '"status":"ok"' <<<"$res" && { _okcat "内核升级成功" return 0 } grep 'already using latest version' <<<"$res" && { _okcat "已是最新版本" return 0 } _failcat "内核升级失败,请检查网络或稍后重试" } function clashsub() { case "$1" in add) shift _sub_add "$@" ;; del) shift _sub_del "$@" ;; list | ls | '') shift _sub_list "$@" ;; use) shift _sub_use "$@" ;; update) shift _sub_update "$@" ;; log) shift _sub_log "$@" ;; -h | --help | *) cat < 添加订阅 ls 查看订阅 del 删除订阅 use 使用订阅 update [id] 更新订阅 log 订阅日志 Options: update: --auto 配置自动更新 --convert 使用订阅转换 EOF ;; esac } _sub_add() { local url=$1 [ -z "$url" ] && { echo -n "$(_okcat '✈️ ' '请输入要添加的订阅链接:')" read -r url [ -z "$url" ] && _error_quit "订阅链接不能为空" } _get_url_by_id "$id" >/dev/null && _error_quit "该订阅链接已存在" _download_config "$CLASH_CONFIG_TEMP" "$url" _valid_config "$CLASH_CONFIG_TEMP" || _error_quit "订阅无效,请检查: 原始订阅:${CLASH_CONFIG_TEMP}.raw 转换订阅:$CLASH_CONFIG_TEMP 转换日志:$BIN_SUBCONVERTER_LOG" local id=$("$BIN_YQ" '.profiles // [] | (map(.id) | max) // 0 | . + 1' "$CLASH_PROFILES_META") local profile_path="${CLASH_PROFILES_DIR}/${id}.yaml" mv "$CLASH_CONFIG_TEMP" "$profile_path" "$BIN_YQ" -i " .profiles = (.profiles // []) + [{ \"id\": $id, \"path\": \"$profile_path\", \"url\": \"$url\" }] " "$CLASH_PROFILES_META" _logging_sub "➕ 已添加订阅:[$id] $url" _okcat '🎉' "订阅已添加:[$id] $url" } _sub_del() { local id=$1 [ -z "$id" ] && { echo -n "$(_okcat '✈️ ' '请输入要删除的订阅 id:')" read -r id [ -z "$id" ] && _error_quit "订阅 id 不能为空" } local profile_path url profile_path=$(_get_path_by_id "$id") || _error_quit "订阅 id 不存在,请检查" url=$(_get_url_by_id "$id") use=$("$BIN_YQ" '.use // ""' "$CLASH_PROFILES_META") [ "$use" = "$id" ] && _error_quit "删除失败:订阅 $id 正在使用中,请先切换订阅" /usr/bin/rm -f "$profile_path" "$BIN_YQ" -i "del(.profiles[] | select(.id == \"$id\"))" "$CLASH_PROFILES_META" _logging_sub "➖ 已删除订阅:[$id] $url" _okcat '🎉' "订阅已删除:[$id] $url" } _sub_list() { "$BIN_YQ" "$CLASH_PROFILES_META" } _sub_use() { "$BIN_YQ" -e '.profiles // [] | length == 0' "$CLASH_PROFILES_META" >&/dev/null && _error_quit "当前无可用订阅,请先添加订阅" local id=$1 [ -z "$id" ] && { clashsub ls echo -n "$(_okcat '✈️ ' '请输入要使用的订阅 id:')" read -r id [ -z "$id" ] && _error_quit "订阅 id 不能为空" } local profile_path url profile_path=$(_get_path_by_id "$id") || _error_quit "订阅 id 不存在,请检查" url=$(_get_url_by_id "$id") cat "$profile_path" >"$CLASH_CONFIG_BASE" _merge_config_restart "$BIN_YQ" -i ".use = $id" "$CLASH_PROFILES_META" _logging_sub "🔥 订阅已切换为:[$id] $url" _okcat '🔥' '订阅已生效' } _get_path_by_id() { "$BIN_YQ" -e ".profiles[] | select(.id == \"$1\") | .path" "$CLASH_PROFILES_META" 2>/dev/null } _get_url_by_id() { "$BIN_YQ" -e ".profiles[] | select(.id == \"$1\") | .url" "$CLASH_PROFILES_META" 2>/dev/null } _sub_update() { local arg is_convert for arg in "$@"; do case $arg in --auto) command -v crontab >/dev/null || _error_quit "未检测到 crontab 命令,请先安装 cron 服务" crontab -l | grep -qs 'clashsub update' || { ( crontab -l 2>/dev/null echo "0 0 */2 * * $SHELL -i -c 'clashsub update'" ) | crontab - } _okcat "已设置定时更新订阅" return 0 ;; --convert) is_convert=true shift ;; esac done local id=$1 [ -z "$id" ] && id=$("$BIN_YQ" '.use // 1' "$CLASH_PROFILES_META") local url profile_path url=$(_get_url_by_id "$id") || _error_quit "订阅 id 不存在,请检查" profile_path=$(_get_path_by_id "$id") _okcat "✈️ " "更新订阅:[$id] $url" [ "$is_convert" = true ] && { _download_convert_config "$CLASH_CONFIG_TEMP" "$url" } [ "$is_convert" != true ] && { _download_config "$CLASH_CONFIG_TEMP" "$url" } _valid_config "$CLASH_CONFIG_TEMP" || { _logging_sub "❌ 订阅更新失败:[$id] $url" _error_quit "订阅无效:请检查: 原始订阅:${CLASH_CONFIG_TEMP}.raw 转换订阅:$CLASH_CONFIG_TEMP 转换日志:$BIN_SUBCONVERTER_LOG" } _logging_sub "✅ 订阅更新成功:[$id] $url" cat "$CLASH_CONFIG_TEMP" >"$profile_path" use=$("$BIN_YQ" '.use // ""' "$CLASH_PROFILES_META") [ "$use" = "$id" ] && clashsub use "$use" && return _okcat '订阅已更新' } _logging_sub() { echo "$(date +"%Y-%m-%d %H:%M:%S") $1" >>"${CLASH_PROFILES_LOG}" } _sub_log() { tail <"${CLASH_PROFILES_LOG}" "$@" } function clashctl() { case "$1" in on) shift clashon ;; off) shift clashoff ;; ui) shift clashui ;; status) shift clashstatus "$@" ;; log) shift clashlog "$@" ;; proxy) shift clashproxy "$@" ;; tun) shift clashtun "$@" ;; mixin) shift clashmixin "$@" ;; secret) shift clashsecret "$@" ;; sub) shift clashsub "$@" ;; upgrade) shift clashupgrade "$@" ;; *) (($#)) && shift clashhelp "$@" ;; esac } clashhelp() { cat </dev/null || netstat -tunl; } | grep -qs ":${port}\b" } _get_random_port() { local randomPort=$(shuf -i 1024-65535 -n 1) ! _is_port_used "$randomPort" && { echo "$randomPort" && return; } _get_random_port } _get_bind_addr() { local allowLan bindAddr bindAddr=$("$BIN_YQ" '.bind-address // "*"' "$CLASH_CONFIG_RUNTIME") allowLan=$("$BIN_YQ" '.allow-lan // false' "$CLASH_CONFIG_RUNTIME") case $allowLan in true) [ "$bindAddr" = "*" ] && bindAddr=$(_get_local_ip) ;; false) bindAddr=127.0.0.1 ;; esac echo "$bindAddr" } _get_local_ip() { local local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}') [ -z "$local_ip" ] && local_ip=$(hostname -I | awk '{print $1}') echo "$local_ip" } function _detect_ext_addr() { local ext_addr=$("$BIN_YQ" '.external-controller // ""' "$CLASH_CONFIG_RUNTIME") local ext_ip=${ext_addr%%:*} EXT_IP=$ext_ip EXT_PORT=${ext_addr##*:} [ "$ext_ip" = '0.0.0.0' ] && EXT_IP=$(_get_local_ip) _is_port_used "$EXT_PORT" && { curl -s --noproxy "*" -H "Authorization: Bearer $(_get_secret)" "127.0.0.1:${EXT_PORT}" | grep -qs "${KERNEL_NAME}" && return 0 local newPort=$(_get_random_port) _failcat '🎯' "端口冲突:[external-controller] ${EXT_PORT} 🎲 随机分配 $newPort" EXT_PORT=$newPort "$BIN_YQ" -i ".external-controller = \"$ext_ip:$newPort\"" "$CLASH_CONFIG_MIXIN" _merge_config } } _color_log() { local color="$1" local msg="$2" local hex="${color#\#}" local r=$((16#${hex:0:2})) local g=$((16#${hex:2:2})) local b=$((16#${hex:4:2})) local color_code="\033[38;2;${r};${g};${b}m" local reset_code="\033[0m" printf "%b%s%b\n" "$color_code" "$msg" "$reset_code" } function _okcat() { local color=#c8d6e5 local emoji=😼 [ $# -gt 1 ] && emoji=$1 && shift local msg="${emoji} $1" _color_log "$color" "$msg" return 0 } function _failcat() { local color=#fd79a8 local emoji=😾 [ $# -gt 1 ] && emoji=$1 && shift local msg="${emoji} $1" _color_log "$color" "$msg" >&2 return 1 } function _error_quit() { [ $# -gt 0 ] && { local color=#f92f60 local emoji=📢 [ $# -gt 1 ] && emoji=$1 && shift local msg="${emoji} $1" _color_log "$color" "$msg" } exec $SHELL -i } function _valid_config() { local config="$1" [[ ! -e "$config" || "$(wc -l <"$config")" -lt 1 ]] && return 1 local test_cmd test_log test_cmd=("$BIN_KERNEL" -d "$(dirname "$config")" -f "$config" -t) test_log=$("${test_cmd[@]}") || { "${test_cmd[@]}" grep -qs "unsupport proxy type" <<<"$test_log" && { local prefix="检测到订阅中包含不受支持的代理协议" [ "$KERNEL_NAME" = "clash" ] && _error_quit "${prefix}, 推荐安装使用 mihomo 内核" _error_quit "${prefix}, 请检查并升级内核版本" } } } function _download_config() { local dest=$1 local url=$2 [ "${url:0:4}" = 'file' ] || _okcat '⏳' '正在下载...' _download_raw_config "$dest" "$url" || return 1 _okcat '🍃' '验证订阅配置...' _valid_config "$dest" || { _failcat '🍂' "验证失败:尝试订阅转换..." cat "$dest" >"${dest}.raw" _download_convert_config "$dest" "$url" } } _download_raw_config() { local dest=$1 local url=$2 curl \ --silent \ --show-error \ --fail \ --insecure \ --location \ --max-time 5 \ --retry 1 \ --user-agent "$CLASH_SUB_UA" \ --output "$dest" \ "$url" || wget \ --no-verbose \ --no-check-certificate \ --timeout 5 \ --tries 1 \ --user-agent "$CLASH_SUB_UA" \ --output-document "$dest" \ "$url" } _download_convert_config() { local dest=$1 local url=$2 local flag [ "${url:0:4}" = 'file' ] && return 0 _start_convert local convert_url=$( target='clash' base_url="http://127.0.0.1:${BIN_SUBCONVERTER_PORT}/sub" curl \ --get \ --silent \ --show-error \ --location \ --output /dev/null \ --data-urlencode "target=$target" \ --data-urlencode "url=$url" \ --write-out '%{url_effective}' \ "$base_url" ) curl --user-agent "$CLASH_SUB_UA" --silent --output "$dest" "$convert_url" flag=$? _stop_convert return $flag } _detect_subconverter_port() { BIN_SUBCONVERTER_PORT=$("$BIN_YQ" '.server.port' "$BIN_SUBCONVERTER_CONFIG") _is_port_used "$BIN_SUBCONVERTER_PORT" && { local newPort=$(_get_random_port) _failcat '🎯' "端口冲突:[subconverter] ${BIN_SUBCONVERTER_PORT} 🎲 随机分配:$newPort" BIN_SUBCONVERTER_PORT=$newPort "$BIN_YQ" -i ".server.port = $newPort" "$BIN_SUBCONVERTER_CONFIG" 2>/dev/null } } _start_convert() { _detect_subconverter_port local check_cmd="curl http://localhost:${BIN_SUBCONVERTER_PORT}/version" $check_cmd >&/dev/null && return 0 ("$BIN_SUBCONVERTER_START" >&"$BIN_SUBCONVERTER_LOG" &) local start=$(date +%s) while ! $check_cmd >&/dev/null; do sleep 0.5s local now=$(date +%s) [ $((now - start)) -gt 2 ] && _error_quit "订阅转换服务未启动,请检查日志:$BIN_SUBCONVERTER_LOG" done } _stop_convert() { $BIN_SUBCONVERTER_STOP >/dev/null } _set_env() { local key=$1 local value=$2 local env_path="${CLASH_BASE_DIR}/.env" grep -qE "^${key}=" "$env_path" && { value=${value//&/\\&} sed -i "s|^${key}=.*|${key}=${value}|" "$env_path" return $? } echo "${key}=${value}" >>"$env_path" } ================================================ FILE: scripts/init/OpenRC.sh ================================================ #!/sbin/openrc-run description="placeholder_kernel_desc" command="placeholder_cmd_path" command_args="placeholder_cmd_args" pidfile="placeholder_pid_file" output_log="placeholder_log_file" error_log="placeholder_log_file" command_background=true ================================================ FILE: scripts/init/SysVinit.sh ================================================ ### BEGIN INIT INFO # Provides: placeholder_kernel_name # Required-Start: $network $local_fs $remote_fs # Required-Stop: $network $local_fs $remote_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: placeholder_kernel_desc # Description: placeholder_kernel_desc ### END INIT INFO pidfile="placeholder_pid_file" logfile="placeholder_log_file" cmd="placeholder_cmd_full" case "$1" in start) $0 status >&/dev/null && exit 0 $cmd >&$logfile & echo $! >$pidfile ;; stop) pid=$(cat $pidfile 2>/dev/null) [ -n "$pid" ] && kill -9 "$pid" rm -f $pidfile ;; restart | reload) $0 stop sleep 0.5 $0 start ;; status) pid=$(cat $pidfile 2>/dev/null) isStart=$(ps ax | awk '{ print $1 }' | grep -e "^${pid}$") [ -n "$isStart" ] && { echo "placeholder_kernel_name is running with PID: $pid" exit 0 } echo "placeholder_kernel_name is not running." exit 1 ;; *) echo "Usage: $0 {start|stop|restart|status}" ;; esac ================================================ FILE: scripts/init/runit.sh ================================================ #!/bin/sh exec placeholder_cmd_full >placeholder_log_file 2>&1 ================================================ FILE: scripts/init/systemd.sh ================================================ [Unit] Description=placeholder_kernel_desc After=network.target NetworkManager.service systemd-networkd.service iwd.service [Service] Type=simple LimitNPROC=500 LimitNOFILE=1000000 CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE Restart=always ExecStartPre=/usr/bin/sleep 1s ExecStart=placeholder_cmd_full ExecReload=/bin/kill -HUP $MAINPID [Install] WantedBy=multi-user.target ================================================ FILE: scripts/preflight.sh ================================================ #!/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://downloads.clash.wiki/ClashPremium/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://downloads.clash.wiki/ClashPremium/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://downloads.clash.wiki/ClashPremium/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://downloads.clash.wiki/ClashPremium/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}" [ "$item" != 'clash' ] && 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 "$*" } ================================================ FILE: uninstall.sh ================================================ #!/usr/bin/env bash . .env . "$CLASH_BASE_DIR/scripts/cmd/clashctl.sh" 2>/dev/null . scripts/preflight.sh pgrep -f "$BIN_KERNEL" -u 0 >/dev/null && ! _is_root && _error_quit "请先关闭 Tun 模式" clashoff 2>/dev/null _uninstall_service _revoke_rc command -v crontab >&/dev/null && crontab -l | grep -v "clashsub" | crontab - /usr/bin/rm -rf "$CLASH_BASE_DIR" echo '✨' '已卸载,相关配置已清除' _quit