[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n\n# Unix files that are always LF\n*.md text eol=lf\n*.sh text eol=lf\n*.json text eol=lf\nxkeen text eol=lf\n"
  },
  {
    "path": ".github/workflows/package-folder.yaml",
    "content": "name: Create Test build to `main/test/` folder\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'scripts/**'\n  workflow_dispatch:\n\njobs:\n  build-and-push-to-main-test-folder:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Import GPG key\n        uses: crazy-max/ghaction-import-gpg@v7\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          git_user_signingkey: true\n          git_commit_gpgsign: true\n          git_tag_gpgsign: true\n          git_config_global: true\n\n      - name: Prepare scripts with build timestamp\n        run: |\n          export TZ=\"Europe/Moscow\"\n          BUILD_TIMESTAMP=$(date \"+%Y-%m-%d %H:%M:%S MSK\")\n          mkdir -p scripts_for_build\n          cp -r scripts/* scripts_for_build/\n\n          sed -i \"s/^build_timestamp=\\\".*\\\"/build_timestamp=\\\"$BUILD_TIMESTAMP\\\"/\" \\\n            scripts_for_build/_xkeen/01_info/01_info_variable.sh\n\n      - name: Create tar.gz archive\n        run: |\n          mkdir -p output\n          cd scripts_for_build\n          chmod +x xkeen\n          find . -type f -o -type l | sed 's|^\\./||' | tar -czf \"../output/xkeen.tar.gz\" -T -\n\n      - name: Move archive to test folder in main\n        run: |\n          mkdir -p test\n          mv output/xkeen.tar.gz test/\n\n      - name: Clean up temporary files\n        run: |\n          rm -rf scripts_for_build output\n\n      - name: Commit and push signed archive to test folder\n        run: |\n          git add test/xkeen.tar.gz\n          git commit -S -m \"[github-actions] automated compiling build\"\n          git push origin main"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Create Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version number (e.g., 1.0.0)'\n        required: true\n        type: string\n      prerelease:\n        description: 'Is this a pre-release?'\n        required: true\n        default: false\n        type: boolean\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Import GPG key\n        uses: crazy-max/ghaction-import-gpg@v7\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          git_user_signingkey: true\n          git_commit_gpgsign: true\n          git_tag_gpgsign: true\n          git_config_global: true\n\n      - name: Set up variables\n        run: |\n          echo \"VERSION=${{ github.event.inputs.version }}\" >> $GITHUB_ENV\n          echo \"ARCHIVE_NAME=xkeen.tar.gz\" >> $GITHUB_ENV\n          echo \"ARCHIVE_NAME_TAR=xkeen.tar\" >> $GITHUB_ENV\n\n      - name: Prepare scripts with build timestamp\n        run: |\n          export TZ=\"Europe/Moscow\"\n          BUILD_TIMESTAMP=$(date \"+%Y-%m-%d %H:%M:%S MSK\")\n          mkdir -p scripts_for_release\n          cp -r scripts/* scripts_for_release/\n\n          sed -i \"s/^build_timestamp=\\\".*\\\"/build_timestamp=\\\"$BUILD_TIMESTAMP\\\"/\" \\\n            scripts_for_release/_xkeen/01_info/01_info_variable.sh\n\n      - name: Create release archive\n        run: |\n          mkdir -p dist\n          cd scripts_for_release\n          chmod +x xkeen\n\n          find . -type f -o -type l | sed 's|^\\./||' | tar -czf \"../dist/${ARCHIVE_NAME}\" -T -\n          find . -type f -o -type l | sed 's|^\\./||' | tar -cf \"../dist/${ARCHIVE_NAME_TAR}\" -T -\n\n      - name: Clean up temporary files\n        run: rm -rf scripts_for_release\n\n      - name: Generate release notes with downloads badge\n        run: |\n          cat > /tmp/release_notes.md << EOF\n          ![downloads](https://img.shields.io/github/downloads/jameszeroX/Xkeen/${{ env.VERSION }}/total?label=downloads)\n          EOF\n\n      - name: Create signed tag and release\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          TAG_NAME=\"${{ env.VERSION }}\"\n\n          # Удаляем старый тег если существует\n          git push origin --delete \"$TAG_NAME\" 2>/dev/null || true\n          git tag -d \"$TAG_NAME\" 2>/dev/null || true\n\n          # Создаем подписанный тег\n          git tag -s \"$TAG_NAME\" -m \"Release $TAG_NAME\"\n          git push origin \"$TAG_NAME\"\n\n          # Создаем релиз\n          gh release create \"$TAG_NAME\" \\\n            dist/*.tar.gz \\\n            dist/*.tar \\\n            --title \"${{ env.VERSION }}\" \\\n            --notes-file /tmp/release_notes.md \\\n            ${{ github.event.inputs.prerelease == 'true' && '--prerelease' || '' }} \\\n            --verify-tag\n\n      - name: Verify signed tag\n        run: |\n          if git tag -v \"${{ env.VERSION }}\" 2>&1; then\n            echo \"✅ Тег ${{ env.VERSION }} успешно подписан и верифицирован!\"\n          else\n            echo \"⚠️ Предупреждение: Не удалось верифицировать подпись тега\"\n          fi"
  },
  {
    "path": ".github/workflows/wiki-sync.yaml",
    "content": "name: Sync GitHub Wiki\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'wiki/**'\n      - '.github/workflows/wiki-sync.yaml'\n  workflow_dispatch:\n\njobs:\n  sync-wiki:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 1\n\n      - name: Import GPG key\n        uses: crazy-max/ghaction-import-gpg@v7\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          git_user_signingkey: true\n          git_commit_gpgsign: true\n          git_tag_gpgsign: true\n          git_config_global: true\n\n      - name: Clone wiki repository\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git clone \"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.wiki.git\" wiki-repo\n\n      - name: Sync wiki content\n        run: |\n          rsync -a --delete --exclude='.git' wiki/ wiki-repo/\n\n      - name: Commit and push signed wiki update\n        working-directory: wiki-repo\n        run: |\n          git add -A\n          if git diff --staged --quiet; then\n            echo \"No wiki changes to commit\"\n            exit 0\n          fi\n          SHORT_SHA=\"${GITHUB_SHA:0:7}\"\n          git commit -S -m \"[github-actions] sync wiki from main@${SHORT_SHA}\"\n          BRANCH=$(git symbolic-ref --short HEAD)\n          git push origin \"$BRANCH\"\n"
  },
  {
    "path": ".gitignore",
    "content": ".claude\ngraphify-out\ndone\nsrc/graphify\nsrc/output\n"
  },
  {
    "path": "01_info_variable.sh",
    "content": "# -------------------------------------\n# Цвета\n# -------------------------------------\ngreen=\"\\033[92m\"\t# Зеленый\nred=\"\\033[91m\"\t\t# Красный\nyellow=\"\\033[93m\"\t# Желтый\nlight_blue=\"\\033[96m\"\t# Голубой\nitalic=\"\\033[3m\"\t# Курсив\nreset=\"\\033[0m\"\t\t# Сброс цветов\n\n# -------------------------------------\n# Директории\n# -------------------------------------\ntmp_dir_global=\"/opt/tmp\"\t\t # Временная директория общая\ntmp_dir=\"/opt/tmp/xkeen\"\t\t # Временная директория XKeen\nxtmp_dir=\"/opt/tmp/xray\"\t\t # Временная директория Xray\nmtmp_dir=\"/opt/tmp/mihomo\"\t\t # Временная директория Mihomo\nxkeen_dir=\"/opt/sbin/.xkeen\"\t\t # Директория скриптов XKeen\nxkeen_cfg=\"/opt/etc/xkeen\"\t\t # Директория конфигурации XKeen\nxkeen_log_dir=\"/opt/var/log/xkeen\"\t # Директория логов XKeen\nxray_log_dir=\"/opt/var/log/xray\"\t # Директория логов Xray\ninitd_dir=\"/opt/etc/init.d\"\t\t # Директория init.d\npid_dir=\"/opt/var/run\"\t\t\t # Директория pid файлов\nbackups_dir=\"/opt/backups\"\t\t # Директория бекапов\ninstall_dir=\"/opt/sbin\"\t\t\t # Директория установки\ngeo_dir=\"/opt/etc/xray/dat\"\t\t # Директория для dat\ncron_dir=\"/opt/var/spool/cron/crontabs\"\t # Директория планировщика\ncron_file=\"root\"\t\t\t # Файл планировщика\ninstall_conf_dir=\"/opt/etc/xray/configs\" # Директория конфигурации Xray\nmihomo_conf_dir=\"/opt/etc/mihomo\"\t # Директория конфигурации Mihomo\nxray_conf_dir=\"$xkeen_dir/02_install/08_install_configs/02_configs_dir\"\nxkeen_var_file=\"$xkeen_dir/01_info/01_info_variable.sh\"\nregister_dir=\"/opt/lib/opkg/info\"\nstatus_file=\"/opt/lib/opkg/status\"\nos_modules=\"/lib/modules/$(uname -r)\"\nuser_modules=\"/opt/lib/modules\"\nxkeen_current_version=\"1.1.3.9\"\nxkeen_build=\"Stable\"\nbuild_timestamp=\"2026-02-07 08:58:11 MSK (fix yq)\"\n\n# -------------------------------------\n# Время\n# -------------------------------------\nexisting_content=$(cat \"$status_file\")\ninstalled_size=$(du -s \"$install_dir\" | cut -f1)\nsource_date_epoch=$(date +%s)\ncurrent_datetime=$(date \"+%d-%b-%y_%H-%M\")\n\n# -------------------------------------\n# IP для проверки доступа в интернет\n# -------------------------------------\nconn_IP1=\"195.208.4.1\"\nconn_IP2=\"77.88.44.55\"\n\n# -------------------------------------\n# URL\n# -------------------------------------\nxkeen_api_url=\"https://api.github.com/repos/jameszeroX/xkeen/releases/latest\"\t\t\t# url api для XKeen\nxkeen_jsd_url=\"https://data.jsdelivr.com/v1/package/gh/jameszeroX/xkeen\"\t\t\t# резервный url api для XKeen\nxkeen_tar_url=\"https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz\"\t# url для загрузки XKeen\nxkeen_dev_url=\"https://raw.githubusercontent.com/jameszeroX/xkeen/main/test/xkeen.tar.gz\"\t# url для загрузки XKeen dev\nxray_api_url=\"https://api.github.com/repos/XTLS/Xray-core/releases\"\t\t\t\t# url api для Xray\nxray_jsd_url=\"https://data.jsdelivr.com/v1/package/gh/XTLS/Xray-core\"\t\t\t\t# резервный url api для Xray\nxray_zip_url=\"https://github.com/XTLS/Xray-core/releases/download\"\t\t\t\t# url для загрузки Xray\nmihomo_api_url=\"https://api.github.com/repos/MetaCubeX/mihomo/releases\"\t\t\t\t# url api для Mihomo\nmihomo_jsd_url=\"https://data.jsdelivr.com/v1/package/gh/MetaCubeX/mihomo\"\t\t\t# резервный url api для Mihomo\nmihomo_gz_url=\"https://github.com/MetaCubeX/mihomo/releases/download\"\t\t\t\t# url для загрузки Mihomo\nyq_dist_url=\"https://github.com/jameszeroX/yq/releases/latest/download\"\t\t\t\t# url для загрузки Yq\ngh_proxy1=\"https://ghfast.top\"\t\t\t\t\t\t\t\t        # 1 прокси для загрузок с GitHub\ngh_proxy2=\"https://gh-proxy.com\"\t\t\t\t\t\t\t\t# 2 прокси для загрузок с GitHub\n\n# url для загрузки геофайлов\nrefilter_url=\"https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geosite.dat\"\nrefilterip_url=\"https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geoip.dat\"\nv2fly_url=\"https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat\"\nv2flyip_url=\"https://github.com/loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\"\nzkeen_url=\"https://github.com/jameszeroX/zkeen-domains/releases/latest/download/zkeen.dat\"\nzkeenip_url=\"https://github.com/jameszeroX/zkeen-ip/releases/latest/download/zkeenip.dat\"\n\n# -------------------------------------\n# Создание директорий и файлов\n# -------------------------------------\nmkdir -p \"$xray_log_dir\" || { echo \"Ошибка: Не удалось создать директорию $xray_log_dir\"; exit 1; }\nmkdir -p \"$initd_dir\" || { echo \"Ошибка: Не удалось создать директорию $initd_dir\"; exit 1; }\nmkdir -p \"$pid_dir\" || { echo \"Ошибка: Не удалось создать директорию $pid_dir\"; exit 1; }\nmkdir -p \"$backups_dir\" || { echo \"Ошибка: Не удалось создать директорию $backups_dir\"; exit 1; }\nmkdir -p \"$install_dir\" || { echo \"Ошибка: Не удалось создать директорию $install_dir\"; exit 1; }\nmkdir -p \"$cron_dir\" || { echo \"Ошибка: Не удалось создать директорию $cron_dir\"; exit 1; }\n\n# -------------------------------------\n# Журналы\n# -------------------------------------\nxray_access_log=\"$xray_log_dir/access.log\"\nxray_error_log=\"$xray_log_dir/error.log\"\n\ntouch \"$xray_access_log\" || { echo \"Ошибка: Не удалось создать файл $xray_access_log\"; exit 1; }\ntouch \"$xray_error_log\" || { echo \"Ошибка: Не удалось создать файл $xray_error_log\"; exit 1; }\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2026, jameszeroX\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# XKeen 1.1.3.9\n\n> **XKeen** — утилита для выборочной маршрутизации сетевого трафика через прокси‑движки **Xray** и **Mihomo** на роутерах **Keenetic**/**Netcraze**.  \n> Позволяет прозрачно направлять TCP/UDP‑трафик только выбранных клиентов, не затрагивая остальную сеть.\n\n---\n\n## Основные возможности\n\n- Выборочная маршрутизация для клиентов в политике доступа в интернет\n- Сохранение прямого выхода в интернет для остальных клиентов\n- Маршрутизация без политики для всех клиентов роутера\n- Поддержка режимов **TProxy**, **Mixed**, **Redirect**, **Other** (socks5/http)\n- Прозрачное проксирование **TCP** и **UDP**\n- Поддержка ядер-проксирования **Xray** и **Mihomo**\n- Совместимость с **KeeneticOS 5+**\n- Управление через shell и [веб-панели](https://github.com/jameszeroX/XKeen?tab=readme-ov-file#дополнения) сторонних разработчиков\n\nXKeen работает полностью на стороне роутера, не меняет настройки клиентов и не требует установки на них дополнительных программ.\n\n---\n\n## Предупреждения\n\n> [!WARNING]\n> Данный материал подготовлен в научно‑технических целях. XKeen предназначен для управления межсетевым экраном роутера Keenetic, защищающим домашнюю сеть. Разработчик не несёт ответственности за иное использование утилиты. Перед применением убедитесь, что ваши действия соответствуют законодательству вашей страны.\n\n> [!CAUTION]\n> В некоторых случаях протокол IPv6 создаёт проблемы при проксировании. В KeeneticOS IPv6 нельзя полностью отключить стандартными средствами. В XKeen реализован альтернативный механизм его отключения, который полностью убирает IPv6‑трафик на роутере. Это **экспериментальная функция** и может привести к некорректной работе отдельных сервисов Keenetic. Используйте её только при необходимости.\n\n> [!NOTE]\n> Установка XKeen гарантируется на внешние USB‑накопители. Установка во внутреннюю память роутера возможна, но требует опыта пользователя. Проблемы, связанные с установкой во внутреннюю память, не считаются ошибками XKeen.\n\n---\n\nДанный репозиторий является форком оригинального XKeen с исправлениями, расширенной функциональностью и поддержкой актуальных версий KeeneticOS.\n\n## Ключевые изменения форка\n\n### Исправлено\n\n- автозапуск XKeen\n- сняты ограничения на количество используемых портов\n\n### Добавлено\n\n- поддержка **KeeneticOS 5+**\n- управление IPv6\n- поддержка ядра **Mihomo**\n- быстрое переключение Xray / Mihomo\n- контроль [файловых дескрипторов](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#контроль-файловых-дескрипторов)\n- [внешние списки](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#внешние-списки-портов-и-ip) IP и портов\n- [OffLine](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#offline-установка)‑установка\n- [Self-Hosted](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#self-hosted-прокси-для-загрузки)-прокси для загрузки компонентов\n\n### Удалено\n\n- не актуальные и повреждённые геобазы\n- неиспользуемые конфигурационные файлы\n- устаревшие параметры запуска и задачи планировщика\n\n---\n\n### Подробное [описание изменений](https://github.com/jameszeroX/XKeen/blob/main/forkinfo.md)\n\n---\n\nСписок параметров запуска XKeen доступен в справке:\n```bash\nxkeen -h\n```\n\n---\n\n## Порядок установки\n\nТребуется роутер **Keenetic**/**Netcraze** с предварительно установленной средой Entware и компонентом `Модули ядра подсистемы Netfilter`\n\n```bash\nopkg update && opkg upgrade && opkg install curl tar && cd /tmp\nsh -c \"$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/install.sh)\"\n```\n\n---\n\n## Поддержка проекта\n\nФорк XKeen, как и оригинал, совершено бесплатен и не имеет каких либо ограничений по использованию. Надеюсь, доработки XKeen, многие из которых я сделал по Вашим просьбам, оказались полезны, так же, как и мои сообщения в [телеграм-чате](https://t.me/+8Cvh7oVf6cE0MWRi). Для меня очень важно понимать, что труд и время потрачены не зря. Буду благодарен за любую Вашу поддержку на развитие проекта:\n\n- [CloudTips](https://pay.cloudtips.ru/p/7edb30ec)\n- [ЮMoney](https://yoomoney.ru/to/41001350776240)\n- Карта МИР: `2204 1201 2976 4110`\n- USDT, сеть TRC20: `TQhy1LbuGe3Bz7EVrDYn67ZFLDjDBa2VNX`\n- USDT, сеть ERC20: `0x6a5DF3b5c67E1f90dF27Ff3bd2a7691Fad234EE2`\n\n<sup>Уточните актуальность крипто-адресов перед переводом</sup>\n\n---\n\n## Дополнения\n\n- XKeen UI — https://github.com/zxc-rv/XKeen-UI\n- XKeen UI — https://github.com/umarcheh001/Xkeen-UI\n- XKeen UI — https://github.com/fan92rus/xkeen-ui\n- Генератор Outbound — https://zxc-rv.github.io/XKeen-UI/Outbound_Generator/\n- Парсер подписок - https://github.com/tkukushkin/xkeen-subscription-watcher\n- Парсер подписок — https://github.com/V2as/SubKeen\n- Mihomo Studio — https://github.com/l-ptrol/mihomo_studio\n- Конвертер JSON-подписок — https://sngvy.github.io/json-sub-to-outbounds\n- Mihomo HWID Subscription Installer — https://github.com/dorian6996/Mihomo-HWID-Subscription\n\n---\n\n## Источники и ссылки\n\n- Origin XKeen — https://github.com/Skrill0/XKeen\n- Xray-core — https://github.com/XTLS/Xray-core\n- Mihomo — https://github.com/MetaCubeX/mihomo\n- Yq — https://github.com/mikefarah/yq\n- FAQ — https://jameszero.net/faq-xkeen.htm\n- Telegram‑чат — https://t.me/+8Cvh7oVf6cE0MWRi\n"
  },
  {
    "path": "configuration.md",
    "content": "---\n\n## Внешние списки портов и IP\n\nПредусмотрена возможность добавить в конфигурационные файлы XKeen необходимые порты проксирования или исключения из проксирования, а также IP-адреса и подсети, проксирование которых не требуется. Файлы находятся в директории `/opt/etc/xkeen/`\n\n- `port_proxying.lst` - порты проксирования, например 80 и 443\n- `port_exclude.lst` - порты, которые необходимо исключить из проксирования, например, 3389. \n- `ip_exclude.lst` - IP-адреса и подсети, которые необходимо исключить из проксирования, например, 77.88.8.8\n\nКаждый порт и IP указываются в этих файлах с новой строки. Пустые строки и строки, начинающиеся со знака комментария `#` игнорируются. Порты проксирования и порты исключенные из проксирования не могут применяться вместе, используйте или то, или другое. Если по ошибке будут заполнены оба списка, то приоритет имеют порты проксирования, а список исключенных портов игнорируется. При чистой установке XKeen создаются шаблоны вышеуказанных файлов с примерами их заполнения.\n\n---\n\n## Контроль файловых дескрипторов\n\nВ среде entware Keenetic довольно низкий предел файловых дескрипторов (fd) выделяемых на один процесс - 1024. Превышение этого значения приводит к отказу прокси-клиенту создавать новые коннекты. На процессорах arm64 суммарно на все процессы отведено около 50000 fd, а на mips процессорах около 12000 fd (цифры могут отличаться для разных моделей роутеров). Оригинал XKeen выделяет для процесса xray 1 миллион! дескрипторов и если xray попытается их занять, роутер может просто зависнуть. Форк XKeen версии 1.1.3.6+, в зависимости от процессора, устанавливает лимит 40000 fd для arm64 и 10000 fd для остальных процессоров, а при достижении 90% от этой цифры, перезапускает прокси-клиент. В параметрах `arm64_fd` и `other_fd` стартового скрипта XKeen, при необходимости, можно откорректировать лимиты, установив другие значения. Контроль fd может быть полезен для высоконагруженных роутеров с большим количеством клиентов. Для типичного self-use применения XKeen, данный функционал использовать необязательно, поэтому по умолчанию контроль открытых файловых дескрипторов прокси-клиентом отключен (включить/отключить можно командой `xkeen -fd`). Возможностью контролировать открытые файловые дескрипторы следует пользоваться в крайних ситуациях, когда ничего уже не помогает, так как при этом прокси-клиент будет постоянно перезапускаться для сброса дескрипторов. Рекомендуется вместо этого найти и устранить причину. Это может быть устройство или приложение, открывающее множество подключений.\n\n---\n\n## Self-Hosted-прокси для загрузки\n\nВ базовый конфиг добавлены два GitHub-прокси, через которые возможна загрузка XKeen и его компонентов в случае недоступности GitHub. Если же и они окажутся недоступны, можете установить [Self-Hosted прокси](https://github.com/hunshcn/gh-proxy) на своём сервере и указать его в переменной `gh_proxy1` или `gh_proxy2` файла `/opt/sbin/.xkeen/01_info/01_info_variable.sh`\n\n---\n\n## OffLine-установка\nОбычная установка XKeen и необходимых компонентов выполняется в OnLine режиме и жёстко привязана к GitHub, а в случае его недоступности будет невозможна. Поэтому в форк дополнительно к способу установки через [Self-Hosted](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#self-hosted-прокси-для-загрузки)-прокси добавлен режим OffLine-установки по команде `xkeen -io`\n\nДля OffLine-установки необходимо заранее любым способом скачать установочный архив XKeen версии 1.1.3.9+, ядро проксирования [xray](https://github.com/XTLS/Xray-core/releases/latest) и(или) [mihomo](https://github.com/MetaCubeX/mihomo/releases/latest) + парсер yaml-файлов [yq](https://github.com/jameszeroX/yq/releases/latest) подходящей архитектуры. Если планируте использовать xray и геофайлы в роутинге, то загрузите и их. Следующим шагом поместите в папку /opt/sbin/ архив XKeen (не распаковывая) и предварительно извлечённые из архива и при необходимости переименованные в `xray` `mihomo` и `yq` бинарники, затем выполните OffLine-установку командами в ssh-консоли entware Keenetic:\n\n```\ncd /opt/sbin\ntar -xvzf xkeen.tar.gz && rm xkeen.tar.gz\nxkeen -io\n#\n```\n\nКопирование файлов конфигурации xray, mihomo и необходимых геофайлов в директории /opt/etc/xray/configs, /opt/etc/mihomo, /opt/etc/xray/dat выполните вручную, после чего можете запустить проксирование командой `xkeen -start`\n\nПри OffLine-установке XKeen не проверяет соответствие архитектуры процессора и бинарников, поэтому выбирайте совместимые бинарники внимательно. Если затрудняетесь в выборе, запустите `xkeen -io` без xray и mihomo в папке /opt/sbin/ и XKeen сообщит, какая архитектура требуется для вашего роутера.\n\nПри недоступности GitHub, обновление геофайлов по планировщику работать не будет, выполняйте его вручную.\n\nЕсли недоступен не только GitHub, но и [репозиторий Entware](http://bin.entware.net), то перед OffLine установкой XKeen требуется вручную установить недостающие пакеты из следующего списка:\n```\ncurl, tar, lscpu, jq, libc, libssp, librt, libpthread, iptables, ca-bundle, coreutils-uname, coreutils-nohup\n```\nлибо прописать в файл `/opt/etc/opkg.conf` рабочее зеркало репозитория\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Документация XKeen\n\nЭтот каталог содержит техническую документацию для разработчиков и контрибьюторов. Если вы пользователь и просто хотите установить XKeen — начните с корневого [`README.md`](../README.md) и [`configuration.md`](../configuration.md).\n\n## Содержание\n\n| Документ | О чём |\n| --- | --- |\n| [architecture.md](architecture.md) | Точка входа, фазы импорта модулей, режимы проксирования, SSoT-переменные |\n| [build-and-release.md](build-and-release.md) | GitHub Actions: пакет в Beta-канал, релиз, синхронизация Wiki. Каналы обновлений |\n| [runtime-paths.md](runtime-paths.md) | Раскладка файлов и каталогов на роутере (`/opt/...`) |\n| [commands.md](commands.md) | Справочник флагов `xkeen` |\n| [contributing.md](contributing.md) | Правила правки кода, ограничения POSIX-`sh`, рабочий цикл проверки |\n\n## Связанные документы в корне репозитория\n\n- [`README.md`](../README.md) — обзор и установка для пользователей.\n- [`configuration.md`](../configuration.md) — внешние списки портов/IP, fd-контроль, Self-Hosted прокси, OffLine-установка.\n- [`forkinfo.md`](../forkinfo.md) — отличия форка от оригинала Skrill0/XKeen.\n- [`knownissues.md`](../knownissues.md) — известные ограничения. Читать перед триажом багов.\n- [`test/README.md`](../test/README.md) — release-notes 2.0 Beta, новые параметры и инварианты.\n\n## Wiki\n\nИсходники GitHub Wiki лежат в [`../wiki/`](../wiki) и автоматически синхронизируются в `<repo>.wiki.git` через workflow `.github/workflows/wiki-sync.yaml`. См. [build-and-release.md](build-and-release.md#workflow-wiki-syncyaml).\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# Архитектура XKeen\n\nXKeen — POSIX-shell утилита (`sh`, не `bash`) для роутеров Keenetic/Netcraze под Entware. Кода на компилируемых языках нет. Целевые архитектуры — `arm64-v8a`, `mips32le`, `mips32`. Запускается на роутере; в этом репозитории — только исходники и упаковка.\n\n## Точка входа\n\n[`scripts/xkeen`](../scripts/xkeen) — монолитный POSIX-`sh` диспетчер (~1450 строк). Парсит флаги через большой `while/case` начиная со строки 119. Каждый флаг — самостоятельная команда.\n\n### Скрытие установочного каталога\n\nПри первом запуске функция `install_xkeen_rename` ([`scripts/xkeen:7-21`](../scripts/xkeen)) переименовывает `_xkeen/` → `.xkeen/`. **Все runtime-пути в коде ссылаются на `.xkeen`. В репозитории каталог называется `_xkeen` — путать легко.** CI пакует именно `_xkeen`.\n\n### Self-detach\n\nЕсли процесс запущен без TTY (cron, ssh без `-t`, CGI) и команда из `{-start,-stop,-restart}`, `xkeen` форкается через `start-stop-daemon -b` в новую session/pgid с логом в `/opt/var/log/xkeen-detached.log`. Это защищает от обрыва родительской сессии. См. [`scripts/xkeen:43-70`](../scripts/xkeen). Переменная `XKEEN_FOREGROUND=1` отключает детач для скриптов с синхронной семантикой (`xkeen -start && cleanup`).\n\n## Импорт модулей\n\nВсе модули — это `. file.sh`-импортируемые библиотеки функций. Точка сборки — [`scripts/_xkeen/import.sh`](../scripts/_xkeen/import.sh), которая последовательно тянет `00_*_import.sh` каждой фазы:\n\n| Фаза | Каталог | Назначение |\n| --- | --- | --- |\n| 01 | [`01_info/`](../scripts/_xkeen/01_info) | Переменные (SSoT — `01_info_variable.sh`), детекция CPU, проверка установленных Xray/Mihomo/geofile, версии, консольный вывод, cron-статус |\n| 02 | [`02_install/`](../scripts/_xkeen/02_install) | Установка: opkg-пакеты → ядра (Xray, Mihomo) → XKeen → geofile/IPSET → cron → регистрация (`07_install_register/`) → шаблоны конфигов (`08_install_configs/02_configs_dir/`) |\n| 03 | [`03_delete/`](../scripts/_xkeen/03_delete) | Точечное удаление компонентов + полная деинсталляция (`-remove`) |\n| 04 | [`04_tools/`](../scripts/_xkeen/04_tools) | Сервис: управление портами, модули ядра, диагностика, задержка автозапуска, интерактивный выбор (`05_tools_choice/`), бэкапы (`06_tools_backups/`), загрузчики через GH-proxy fallback (`07_tools_downloaders/`) |\n| 05 | [`05_tests/`](../scripts/_xkeen/05_tests) | Runtime-проверки сети, портов, носителя |\n\n## Single Source of Truth: `01_info_variable.sh`\n\nФайл [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh) — единственное место, где определены:\n\n- Версия и канал: `xkeen_current_version`, `xkeen_build`, `build_timestamp` (последнее — подставляется CI).\n- Все runtime-каталоги: `xkeen_dir=/opt/sbin/.xkeen`, `xkeen_cfg=/opt/etc/xkeen`, `geo_dir=/opt/etc/xray/dat`, и др.\n- Все внешние URL: GitHub API для XKeen/Xray/Mihomo, прямые URL архивов, geofile-репозитории.\n- GitHub-прокси для регионов с ограничениями: `gh_proxy1=https://gh-proxy.com`, `gh_proxy2=https://ghfast.top`.\n\nПри смене версии или URL правится только этот файл. Релизный workflow перезаписывает в нём только `build_timestamp`.\n\n## GH-proxy fallback\n\nЛюбая загрузка с GitHub в [`04_tools/07_tools_downloaders/`](../scripts/_xkeen/04_tools/07_tools_downloaders) и в корневом [`install.sh`](../install.sh) идёт по цепочке:\n\n1. Прямой URL (например, `github.com/.../xkeen.tar.gz`).\n2. `gh_proxy1` префиксом — `https://gh-proxy.com/<github_url>`.\n3. `gh_proxy2` префиксом — `https://ghfast.top/<github_url>`.\n\nС версии 2.0 Beta параметр `gh_proxy` из `/opt/etc/xkeen/xkeen.json` имеет приоритет над встроенными значениями.\n\nМаркер `/tmp/toff` (создаётся при запуске с флагом `-toff`) отключает `curl -m 180` на одну сессию — полезно для медленных каналов.\n\n## Режимы проксирования\n\nВ рантайме определяется один из четырёх режимов:\n\n| Режим | Признак |\n| --- | --- |\n| TProxy | Inbound с `streamSettings.sockopt.tproxy == \"tproxy\"` (Xray) или `listeners[].type == \"tproxy\"` (Mihomo) |\n| Hybrid | Бывший Mixed — комбинация TProxy + Redirect |\n| Redirect | Inbound с `sockopt.tproxy == \"redirect\"`. Самый быстрый, но без UDP |\n| Other | socks5/http inbound |\n\nОпределение режима — в [`scripts/_xkeen/02_install/07_install_register/04_register_init.sh`](../scripts/_xkeen/02_install/07_install_register/04_register_init.sh) парсингом конфигов Xray (`tproxy`/`redirect` в `streamSettings.sockopt`) и Mihomo (`tproxy-port`, `listeners[].type == \"tproxy\"`).\n\n**Имена inbound-тегов больше не влияют на режим — исправление 2.0 Beta.** Ранее теги вроде `tproxy-in`/`redirect-in` использовались как fallback, что приводило к ошибкам при кастомных тегах.\n\n## Beta-функции\n\nОписаны в [`test/README.md`](../test/README.md). Кратко:\n\n- Кастомные политики маршрутизации в `xkeen.json`.\n- IPSET `ru-exclude` — исключение российских IP из проксирования на уровне ipset.\n- DSCP-метки 62 (исключение) и 63 (проксирование) — маршрутизация по приоритетам QoS-пакетов. См. также wiki-страницу [Маршрутизация по DSCP](../wiki/Маршрутизация-по-DSCP.md).\n- Проксирование трафика Entware-пакетов с `routing-mark: 255` (Xray) / `mark: 255` (Mihomo).\n"
  },
  {
    "path": "docs/build-and-release.md",
    "content": "# Сборка и релиз\n\nЛокальной сборки нет. Всё делает CI на GitHub Actions. В этом разделе — три workflow-а и две схемы каналов обновлений.\n\n## Workflow-ы\n\n### `package-folder.yaml`\n\n[`.github/workflows/package-folder.yaml`](../.github/workflows/package-folder.yaml)\n\n| Параметр | Значение |\n| --- | --- |\n| Триггер | `push` в `main` с изменениями в `scripts/**`, либо `workflow_dispatch` |\n| Результат | `test/xkeen.tar.gz` — Beta-канал, упакован из `scripts/*` |\n| Подпись | GPG-подписанный автокоммит `[github-actions] automated compiling build` |\n\nШаги:\n\n1. Checkout с `fetch-depth: 0`.\n2. Импорт GPG-ключа через `crazy-max/ghaction-import-gpg@v7` с `git_config_global: true`.\n3. Подмена `build_timestamp=\"…\"` в `scripts/_xkeen/01_info/01_info_variable.sh` на текущее MSK-время.\n4. Упаковка: `cd scripts_for_build && find . -type f -o -type l | sed 's|^\\./||' | tar -czf .../xkeen.tar.gz -T -`. На верхнем уровне архива — `xkeen` и `_xkeen/`, без вложенного `scripts/`.\n5. Перемещение архива в `test/` и подписанный коммит обратно в `main`.\n\n**Файл `test/xkeen.tar.gz` — артефакт CI, руками не редактировать.**\n\n### `release.yaml`\n\n[`.github/workflows/release.yaml`](../.github/workflows/release.yaml)\n\n| Параметр | Значение |\n| --- | --- |\n| Триггер | `workflow_dispatch` с входами `version` (string) и `prerelease` (boolean) |\n| Результат | `dist/xkeen.tar.gz` + `dist/xkeen.tar` + GitHub Release + подписанный GPG-тег |\n\nШаги:\n\n1. Checkout с `fetch-depth: 0`.\n2. Импорт GPG-ключа.\n3. Подмена `build_timestamp` (как в `package-folder.yaml`).\n4. Двойная упаковка: `.tar.gz` (для роутеров с `tar`+gzip) и `.tar` (для альтернативных распаковщиков).\n5. Удаление существующего тега, создание подписанного `git tag -s \"$VERSION\"`, push.\n6. `gh release create` с обоими архивами. При `prerelease=true` — флаг `--prerelease`.\n7. Верификация подписи `git tag -v`.\n\n### `wiki-sync.yaml`\n\n[`.github/workflows/wiki-sync.yaml`](../.github/workflows/wiki-sync.yaml)\n\n| Параметр | Значение |\n| --- | --- |\n| Триггер | `push` в `main` с изменениями в `wiki/**` или сам workflow, либо `workflow_dispatch` |\n| Результат | Содержимое `wiki/` синхронизировано в `<repo>.wiki.git` подписанным коммитом |\n\nШаги:\n\n1. Checkout главного репо.\n2. Импорт GPG-ключа (тот же `crazy-max/ghaction-import-gpg@v7`).\n3. Клонирование `<repo>.wiki.git` через `https://x-access-token:${GITHUB_TOKEN}@github.com/<repo>.wiki.git`.\n4. `rsync -a --delete --exclude='.git' wiki/ wiki-repo/` — добавление, обновление, удаление.\n5. Подписанный коммит `[github-actions] sync wiki from main@<short-sha>` и push в дефолтную ветку Wiki.\n\nПререкизиты для прода:\n\n- В Settings → Features → Wikis: ✅ enabled.\n- В Wiki создана хотя бы одна страница через UI (иначе `<repo>.wiki.git` отдаёт 404).\n- В Settings → Actions → General → Workflow permissions: `Read and write permissions`.\n- Secret `GPG_PRIVATE_KEY` (passphrase не используется).\n\n## Каналы обновлений\n\n| Канал | Источник | Триггер |\n| --- | --- | --- |\n| Stable | GitHub Release с тегом, `xkeen_tar_url` | Прогон `release.yaml` |\n| Beta | `test/xkeen.tar.gz` в ветке `main`, `xkeen_dev_url` | Любой merge в `main` с изменениями `scripts/**` |\n\nНа роутере переключение каналов — `xkeen -channel`. Текущая версия и канал хранятся в `01_info_variable.sh` (`xkeen_current_version`, `xkeen_build`).\n\n## Воспроизвести локальную сборку\n\nБез CI, для отладки упаковки:\n\n```sh\ncd scripts && find . -type f -o -type l | sed 's|^\\./||' | tar -czf /tmp/xkeen.tar.gz -T -\n```\n\nРезультат идентичен тому, что генерирует `package-folder.yaml` (за исключением подменённого `build_timestamp`).\n"
  },
  {
    "path": "docs/commands.md",
    "content": "# Справочник флагов `xkeen`\n\nПолный список флагов диспетчера [`scripts/xkeen`](../scripts/xkeen). Извлечён из `help_xkeen()` в [`scripts/_xkeen/about.sh`](../scripts/_xkeen/about.sh). Деструктивные флаги отмечены ⚠ — они интерактивные, требуют подтверждения, не имеют опции «тихого» режима.\n\n## Установка\n\n| Флаг | Действие |\n| --- | --- |\n| `-i`, `-install` | Полный цикл: XKeen + Xray + GeoFile/GeoIPSET + Mihomo |\n| `-io` | OffLine-установка XKeen из локальной флешки |\n| `-toff` | Отключить таймаут `curl` для медленных каналов: `xkeen -i -toff` |\n\n## Переустановка\n\n| Флаг | Действие |\n| --- | --- |\n| `-k` | XKeen |\n| `-g` | GeoFile (GeoSite + GeoIP) |\n| `-gips` | GeoIPSET |\n\n## Обновление\n\n| Флаг | Действие |\n| --- | --- |\n| `-uk` | XKeen (получает Stable или Beta — см. `-channel`) |\n| `-ug` | GeoFile/GeoIPSET |\n| `-ux` | Xray — повышение или понижение версии |\n| `-um` | Mihomo — повышение или понижение версии |\n\n## Автообновление GeoFile/GeoIPSET (cron-задача)\n\n| Флаг | Действие |\n| --- | --- |\n| `-ugc` | Создать cron-задачу |\n| `-dgc` | Удалить cron-задачу |\n\n## Резервные копии\n\n| Флаг | Действие |\n| --- | --- |\n| `-kb` | Создать резервную копию XKeen |\n| `-kbr` | Восстановить XKeen из резервной копии |\n| `-xb` | Создать резервную копию конфигурации Xray |\n| `-xbr` | Восстановить конфигурацию Xray |\n| `-mb` | Создать резервную копию конфигурации Mihomo |\n| `-mbr` | Восстановить конфигурацию Mihomo |\n\n## Удаление ⚠\n\n| Флаг | Действие |\n| --- | --- |\n| `-remove` | ⚠ Полная деинсталляция XKeen |\n| `-dgs` | ⚠ Удалить GeoSite |\n| `-dgi` | ⚠ Удалить GeoIP |\n| `-dgips` | ⚠ Удалить GeoIPSET |\n| `-dx` | ⚠ Удалить Xray |\n| `-dm` | ⚠ Удалить Mihomo |\n| `-dk` | ⚠ Удалить XKeen (сохраняет ядра) |\n\n## Порты проксирования\n\n| Флаг | Действие |\n| --- | --- |\n| `-ap` | Добавить порт |\n| `-dp` | Удалить порт |\n| `-cp` | Посмотреть список |\n\n## Порты, исключённые из проксирования\n\n| Флаг | Действие |\n| --- | --- |\n| `-ape` | Добавить порт-исключение |\n| `-dpe` | Удалить порт-исключение |\n| `-cpe` | Посмотреть список |\n\n## Управление прокси-клиентом\n\n| Флаг | Действие |\n| --- | --- |\n| `-start` | Запуск |\n| `-stop` | Остановка |\n| `-restart` | Перезапуск |\n| `-status` | Статус работы |\n| `-tp` | Порты, шлюз и протокол прокси-клиента |\n| `-auto` | Включить / отключить автозапуск |\n| `-d` | Установить задержку автозапуска: `xkeen -d 30` (секунд) |\n| `-fd` | Включить / отключить контроль файловых дескрипторов |\n| `-cfd` | Посчитать количество открытых файловых дескрипторов прокси-клиента |\n| `-diag` | Выполнить полную диагностику (единственный поддерживаемый канал для отчёта о проблеме) |\n| `-channel` | Переключить канал обновлений (Stable / Beta) |\n| `-xray` | Переключить XKeen на ядро Xray |\n| `-mihomo` | Переключить XKeen на ядро Mihomo |\n| `-ipv6` | Включить / отключить протокол IPv6 в KeeneticOS |\n| `-dns` | Включить / отключить перенаправление DNS в прокси |\n| `-pr` | Включить / отключить проксирование трафика Entware |\n| `-extmsg` | Включить / отключить расширенные сообщения при запуске |\n| `-cbk` | Включить / отключить резервное копирование XKeen при обновлении |\n| `-aghfix` | Включить / отключить отображение клиентов под своими IP в журнале AdGuard Home |\n\n## Информация\n\n| Флаг | Действие |\n| --- | --- |\n| `-about` | О программе |\n| `-ad` | Поддержать разработчиков |\n| `-af` | Обратная связь / контакты |\n| `-v` | Версия XKeen |\n| `-h`, `-help` | Показать встроенную справку (`help_xkeen` из `about.sh`) |\n\n## Переменные окружения\n\n| Переменная | Значение | Эффект |\n| --- | --- | --- |\n| `XKEEN_FOREGROUND` | `1` | Отключает self-detach при запуске без TTY. Использовать в скриптах с синхронной семантикой (`xkeen -start && cleanup`) |\n| `XKEEN_DETACHED` | `1` | Внутренний маркер — выставляется самим `xkeen` после форка через `start-stop-daemon -b`. Руками не выставлять |\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# Правила правки\n\n## Язык — POSIX `sh`\n\nЦелевая среда — Entware на BusyBox `ash`. **Никаких bash-измов:**\n\n| Запрещено | Использовать вместо |\n| --- | --- |\n| `[[ … ]]` | `[ … ]` (POSIX `test`) |\n| `${var,,}`, `${var^^}` | `echo \"$var\" \\| tr 'A-Z' 'a-z'` |\n| Массивы (`arr=(a b c)`, `${arr[i]}`) | Позиционные параметры, IFS-split строки |\n| `<<<` (here-string) | `echo \"…\" \\| cmd` или `<< EOF` |\n| `function name()` | `name()` |\n| `local var` | Не использовать — `local` не POSIX |\n| `(( … ))` арифметика | `$(( … ))` или `expr` |\n| `read -p` | `printf '...'; read var` |\n\nПроверка перед PR:\n\n```sh\nshellcheck scripts/xkeen scripts/_xkeen/**/*.sh\n```\n\n## Пути и URL — только из переменных\n\nВсе пути и URL определены в [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh). **Не хардкодить ни одного `/opt/...` пути и ни одного `https://github.com/...` URL** в других файлах. Если нужен новый путь — добавить переменную в `01_info_variable.sh`.\n\n## Добавление модуля\n\n1. Определить, к какой фазе относится: `01_info/`, `02_install/`, `03_delete/`, `04_tools/`, `05_tests/`. Если новый раздел внутри `02_install/` (например, поддиректория `09_install_X/`) — создать каталог и поместить туда `00_<phase>_import.sh`.\n2. Создать файл `NN_<purpose>.sh` в нужном каталоге (нумерация — следующая свободная).\n3. Подключить через `.` в соответствующем `00_*_import.sh` родительского каталога.\n4. Если модуль зависит от других — следить за порядком импорта.\n\n## Добавление новой команды\n\n1. Case-ветка в [`scripts/xkeen`](../scripts/xkeen) в большом `while/case` (начинается со строки 119).\n2. Если команда из `{-start, -stop, -restart}` или другая, требующая self-detach в фоне — добавить в проверку на строках 43-48 (`detach_eligible=true`).\n3. Описание флага — в `help_xkeen()` функции [`scripts/_xkeen/about.sh`](../scripts/_xkeen/about.sh) под подходящим разделом.\n4. Если команда деструктивная — обязательно интерактивное подтверждение перед действием. Не делать «тихие» деструктивные операции.\n\n## Лимиты файловых дескрипторов\n\nЗначения в стартовом скрипте: `arm64_fd=40000`, `other_fd=10000`. Не править наугад — увеличение влечёт расход RAM, уменьшение — обрывы соединений на пиках. См. также соответствующий раздел в [`configuration.md`](../configuration.md).\n\n## Self-detach\n\nБлок в [`scripts/xkeen:43-70`](../scripts/xkeen) — критичный для cron-перезапусков. Без него родитель убивает дочерний процесс по SIGHUP при обрыве ssh-сессии. Трогать только осознанно.\n\n## Проверка перед PR\n\n1. `shellcheck scripts/xkeen scripts/_xkeen/**/*.sh` — нулевая толерантность к новым warning-ам.\n2. Деплой архива на тестовый роутер и прогон сценариев: `xkeen -i`, `-start`, `-stop`, `-restart`, `-uk`, `-diag`.\n3. Если правились флаги управления (`-ap`, `-dp`, `-ape`, `-dpe`) или режимы проксирования — отдельно прогнать с обоими ядрами (Xray и Mihomo) и в каждом из режимов TProxy/Hybrid/Redirect.\n4. `xkeen -diag` — единственный поддерживаемый канал для отчёта о проблеме.\n\n## CI-файлы — не трогать руками\n\n- [`.github/workflows/package-folder.yaml`](../.github/workflows/package-folder.yaml) и сам артефакт [`test/xkeen.tar.gz`](../test/xkeen.tar.gz) — генерируются CI. Любые ручные правки будут перезаписаны при следующем push в `main` с изменениями `scripts/**`.\n- [`.github/workflows/release.yaml`](../.github/workflows/release.yaml) — менять только если действительно меняется процесс релиза.\n- [`.github/workflows/wiki-sync.yaml`](../.github/workflows/wiki-sync.yaml) — синхронизирует [`wiki/`](../wiki) в GitHub Wiki. Менять только при изменении логики синхронизации.\n\n## Документация\n\n- Корневые `README.md`, `configuration.md`, `forkinfo.md`, `knownissues.md` — пользовательская документация. При фичах, затрагивающих пользователя, — обновлять.\n- [`test/README.md`](../test/README.md) — release-notes 2.0 Beta. При новой Beta-фиче — добавить запись.\n- [`docs/`](.) — техническая документация для контрибьюторов. При структурных изменениях кода — обновлять `architecture.md` / `runtime-paths.md` / `commands.md`.\n- [`wiki/`](../wiki) — публичная Wiki для пользователей. Обновления синхронизируются автоматически.\n\n## Каналы и версии\n\n- Ветка `main` → Beta-канал, `test/xkeen.tar.gz`, автоматически после push.\n- GitHub Release с подписанным тегом → Stable-канал.\n- Версия и канал хранятся в [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh): `xkeen_current_version`, `xkeen_build`.\n"
  },
  {
    "path": "docs/runtime-paths.md",
    "content": "# Раскладка на роутере\n\nВсе runtime-пути на роутере. В этом репозитории каталог `_xkeen/`; после установки на роутер он переименовывается в `.xkeen/` функцией `install_xkeen_rename`. Все переменные путей определены в [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh) — не хардкодить.\n\n## Исполняемые файлы и модули\n\n| Путь | Назначение |\n| --- | --- |\n| `/opt/sbin/xkeen` | Диспетчер (исполняемый, монолитный POSIX-sh) |\n| `/opt/sbin/.xkeen/` | Каталог импортируемых модулей XKeen |\n| `/opt/sbin/.xkeen/import.sh` | Точка сборки модулей |\n| `/opt/etc/init.d/S05xkeen` | Init-скрипт XKeen, генерируется `04_register_init.sh` |\n| `/opt/etc/init.d/S05crond` | Cron-демон, обслуживает автообновления geofile |\n\n## Пользовательский конфиг\n\n`/opt/etc/xkeen/` — все настройки, которые правит пользователь.\n\n| Файл | Назначение |\n| --- | --- |\n| `xkeen.json` | Главный конфиг: `gh_proxy`, политики, расширения 2.0 Beta |\n| `ip_exclude.lst` | IP/подсети, исключённые из проксирования (с маской `/32` для одиночных адресов) |\n| `port_proxying.lst` | Порты, направляемые в прокси. С 2.0 Beta — единственный источник, старая `port_donor` упразднена |\n| `port_exclude.lst` | Порты, исключённые из проксирования. С 2.0 Beta — единственный источник, старая `port_exclude` (как переменная) упразднена |\n| `ipset/ru_exclude_ipv4.lst` | IPv4-сеты для российских IP — Beta-функция исключения по ipset |\n| `ipset/ru_exclude_ipv6.lst` | То же для IPv6 |\n\n## Конфиги ядер\n\n| Путь | Назначение |\n| --- | --- |\n| `/opt/etc/xray/configs/` | Все JSON-конфиги Xray (`inbounds.json`, `outbounds.json`, `routing.json`, `dns.json`) |\n| `/opt/etc/xray/dat/` | GeoSite (`*.dat`) и GeoIP (`*.dat`) базы |\n| `/opt/etc/mihomo/` | Конфигурация Mihomo (`config.yaml` и подключаемые) |\n\n## Логи\n\n| Путь | Назначение |\n| --- | --- |\n| `/opt/var/log/xkeen/` | Логи самого XKeen |\n| `/opt/var/log/xray/access.log` | Access-лог Xray |\n| `/opt/var/log/xray/error.log` | Error-лог Xray |\n| `/opt/var/log/xkeen-detached.log` | Лог фоновых запусков (self-detach из `-start/-stop/-restart` без TTY) |\n\n## Runtime-state\n\n| Путь | Назначение |\n| --- | --- |\n| `/opt/var/run/` | PID-файлы (`xkeen.pid`, `xray.pid`, `mihomo.pid`) |\n| `/opt/tmp/xkeen/` | Временная директория XKeen |\n| `/opt/tmp/xray/`, `/opt/tmp/mihomo/` | Временные директории ядер |\n| `/opt/backups/` | Архивы резервных копий (флаги `-kb`, `-xb`, `-mb`) |\n| `/opt/var/spool/cron/crontabs/root` | Cron-задачи (создаются флагом `-ugc`) |\n\n## Хуки в netfilter.d / schedule.d\n\n| Путь | Назначение |\n| --- | --- |\n| `/opt/etc/ndm/netfilter.d/proxy.sh` | Хук при пересборке правил межсетевого экрана Keenetic — переставляет iptables-правила прокси |\n| `/opt/etc/ndm/schedule.d/00-xkeen-hotspot-sync.sh` | Хук на смену клиентов hotspot — обновляет ipset `xkeen_deny_mac` |\n\n## Маркеры\n\n| Файл | Что значит |\n| --- | --- |\n| `/tmp/toff` | Маркер сессии: отключает таймаут `curl -m 180`. Создаётся флагом `-toff`, очищается trap-ом INT/TERM |\n| `/opt/etc/ndm/netfilter.d/aghfix.sh` | Опциональный фикс отображения клиентов в AdGuard Home (флаг `-aghfix`) |\n"
  },
  {
    "path": "forkinfo.md",
    "content": "## Сравнение форка с оригинальным XKeen\n\nИзменения:\n- Исправлено добавление портов в исключения (ранее команду `xkeen -ape` нужно было прерывать по ctrl+c)\n- Исправлена совместная работа режима TProxy и socks5 (ранее Xkeen запускался в Mixed режиме, что приводило к неработоспособности прозрачного проксирования)\n- Исправлен автозапуск XKeen при старте роутера (ранее XKeen в некоторых случаях не запускался или запускался для всего устройства, а не только для своей политики - [FAQ п.12](https://jameszero.net/faq-xkeen.htm#12))\n- Снято техническое ограничение, позволявшее использовать не более 15 портов проксирования и портов исключенных из проксирования\n- Переработана логика загрузки XKeen, Xray, Mihomo и GeoFile из интернета, уменьшающая вероятность их повреждения\n- Переработана логика применения правил iptables и ip6tables (ранее XKeen применял все правила, даже при не установленном компоненте IPv6)\n- Переработана логика добавления и удаления портов проксирования и исключаемых портов\n- При обновлении геофайлов, добавлении/удалении портов проксирования или портов исключений, а также выполнении других настроек, требующих перезапуск XKeen, прокси-клиент теперь перезапускается если был до этого запущен\n- При запуске `xkeen -d` без цифрового параметра, теперь отображается информация о текущей задержке автозапуска\n- При запуске или перезапуске XKeen теперь отображается информация о режиме работы - TProxy, Mixed, Redirect, Other\n- Не актуальные GeoSite и GeoIP antifilter-community заменены на базы [Re:filter](https://github.com/1andrevich/Re-filter-lists)\n- Объединены задачи планировщика по обновлению GeoSite и GeoIP. В связи с этим упразднены параметры запуска `-ugs`, `-ugi`, `-ugsc`, `-ugic`, `-dgsc`, `-dgic`\n- Параметр запуска `-ux` для обновления ядра Xray теперь поддерживает повышение/понижение версии\n- Корректная деинсталляция xray-core (ранее пакет xray не удалялся при деинсталляции)\n- Справка (`xkeen -h`) выровнена по табуляции и повышен контраст текста\n- Скрипт запуска S24xray переименован в S99xkeen\n- Рефакторинг кода скриптов\n- Актуализация конфигурационных файлов xray-core\n\nДобавлено:\n- Совместимость с прошивкой KeeneticOS 5+\n- Возможность отключить/включить протокол IPv6 в KeeneticOS (параметр запуска `-ipv6`)\n- Поддержка ядра Mihomo\n- Возможность сменить ядро проксирования (Xray/Mihomo) параметрами `-xray` и `-mihomo`\n- При обновлении Xray и Mihomo теперь отображается версия уже установленного в роутере бинарника\n- Добавлена возможность отключить/включить перехват DNS запросов при соответствующей настройке прокси-клиента (параметр запуска `-dns`)\n- Поддержка внешних файлов `ip_exclude.lst`, `port_proxying.lst` и `port_exclude.lst` в директории `/opt/etc/xkeen/` для указания IP и портов (проксирования/исключения из проксирования)\n- Возможность загружать компоненты XKeen через [Self-Hosted прокси](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#self-hosted-прокси-для-загрузки-компонентов) при недоступности GitHub (переменные `gh_proxy(1|2)` в файле `01_info_variable.sh`)\n- Возможность отключить резервное копирование XKeen при обновлении (переменная `backup` в стартовом скрипте)\n- Возможность [OffLine установки](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#offline-установка) (параметр `-io`)\n- Возможность установки GeoIP базы [zkeenip.dat](https://github.com/jameszeroX/zkeen-ip)\n- Обновление [zkeen.dat](https://github.com/jameszeroX/zkeen-domains) и [zkeenip.dat](https://github.com/jameszeroX/zkeen-ip) по расписанию средствами XKeen\n- При недоступности GitHub API используется резервный источник релизов для XKeen, Xray и Mihomo \n- При установке теперь можно выбрать, добавлять ли XKeen в автозагрузку при включении роутера или нет\n- При пропуске установки Xray, его конфигурационные файлы и геобазы так же пропускаются и не устанавливаются\n- Mihomo и парсер yaml-файлов Yq устанавливаются и регистрируются в entware, как полноценные ipk-пакеты\n- Параметр запуска `-remove` для полной деинсталляции XKeen (ранее деинсталляцию нужно было выполнять покомпонентно)\n- Параметры запуска `-ug` (обновление геофайлов), `-ugc` (управление заданием Cron, обновляющим геофайлы), `-dgc` (удаление задания Cron, обновляющего геофайлы)\n- Параметр запуска `-um` для обновления/установки ядра Mihomo (поддерживается повышение/понижение версии)\n- Параметры запуска: `-rrm` (обновить регистрацию Mihomo), `-drm` (удалить регистрацию Mihomo)\n- Параметр запуска `-dm` для деинсталляции ядра Mihomo\n- Параметр запуска `-g`, позволяющий переустановить (добавить/удалить) геофайлы для Xray\n- Параметр запуска `-channel`, позволяющий выбрать канал обновления XKeen между Stable и Dev ветками\n- Возможность резервного копирования и восстановления конфигурации Mihomo (параметры `-mb`, `-mbr`)\n- Возможность контролировать число открытых файловых дескрипторов, используемых прокси-клиентом и перезапускать процесс при исчерпании лимита  [подробнее](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#контроль-файловых-дескрипторов)\n\nУдалено:\n- Поддержка внешнего файла `/opt/etc/xkeen_exclude.lst` c IP-адресами и подсетями для исключения из проксирования\n- Возможность установки GeoSite Antizapret (база повреждена в репозитории)\n- Конфигурационный файл `02_transport.json` (не используется новыми ядрами xray-core)\n- Запрос на перезапись и сама перезапись конфигурационных файлов Xray, если они уже существуют на момент установки XKeen\n- Создание резервных копий Xray, так как теперь можно интерактивно установить предыдущую версию ядра параметром `-ux`. В связи с этим упразднены параметры запуска `-xb` и `-xbr`\n- Логирование процесса установки XKeen в директорию `/opt/var/log/xkeen` (на практике не использовалось)\n- Задачи планировщика по автообновлению XKeen/Xray. В связи с этим упразднены параметры запуска `-uac`, `-ukc`, `-uxc`, `-dac`, `-dkc` и `-dxc`\n- Параметры запуска: `-x` (заменён на `-ux`), `-rk` (заменён на `-rrk`), `-rx` (заменён на `-rrx`), `-rc` (не актуален)\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/sh\n\ngreen=\"\\033[92m\"\nred=\"\\033[91m\"\nyellow=\"\\033[93m\"\nlight_blue=\"\\033[96m\"\nreset=\"\\033[0m\"\n\nurl_stable=\"https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz\"\nurl_beta=\"https://raw.githubusercontent.com/jameszeroX/XKeen/main/test/xkeen.tar.gz\"\narchive_name=\"xkeen.tar.gz\"\nrelease_fix_url=\"https://raw.githubusercontent.com/jameszeroX/XKeen/main/01_info_variable.sh\"\n\nclear\necho\nprintf \"  Какую версию ${yellow}XKeen${reset} вы хотите установить?\\n\\n\"\nprintf \"  1) Стабильную версию (${light_blue}Stable${reset})\\n\"\nprintf \"  2) Новую Бета-версию (${light_blue}Beta${reset})\\n\\n\"\nprintf \"  Выберите 1 или 2 [по умолчанию 1]: \"\nread -r version_choice\n\ncase \"$version_choice\" in\n    2)\n        url=\"$url_beta\"\n        echo\n        printf \"  Выбрана ${light_blue}Бета-версия${reset}\\n\"\n        ;;\n    *)\n        url=\"$url_stable\"\n        echo\n        printf \"  Выбрана ${light_blue}Стабильная версия${reset}\\n\"\n        ;;\nesac\necho\n\nget_release_var_file() {\n    if [ -f /opt/sbin/_xkeen/01_info/01_info_variable.sh ]; then\n        printf '%s\\n' \"/opt/sbin/_xkeen/01_info/01_info_variable.sh\"\n        return 0\n    fi\n\n    if [ -f /opt/sbin/.xkeen/01_info/01_info_variable.sh ]; then\n        printf '%s\\n' \"/opt/sbin/.xkeen/01_info/01_info_variable.sh\"\n        return 0\n    fi\n\n    return 1\n}\n\ndownload_xkeen_release() {\n    if curl -fLo \"$archive_name\" --connect-timeout 10 -m 15 \"$url\"; then\n        return 0\n    fi\n\n    if curl -fLo \"$archive_name\" --connect-timeout 10 -m 15 \"https://gh-proxy.com/$url\"; then\n        return 0\n    fi\n\n    if curl -fLo \"$archive_name\" --connect-timeout 10 -m 15 \"https://ghfast.top/$url\"; then\n        return 0\n    fi\n\n    printf \"  ${red}Ошибка${reset}: не удалось загрузить ${yellow}xkeen.tar.gz${reset}\\n\"\n    return 1\n}\n\ndownload_release_fix() {\n    target_file=\"$1\"\n\n    if curl -fLo \"$target_file\" --connect-timeout 10 -m 15 \"$release_fix_url\"; then\n        return 0\n    fi\n\n    if curl -fLo \"$target_file\" --connect-timeout 10 -m 15 \"https://gh-proxy.com/$release_fix_url\"; then\n        return 0\n    fi\n\n    if curl -fLo \"$target_file\" --connect-timeout 10 -m 15 \"https://ghfast.top/$release_fix_url\"; then\n        return 0\n    fi\n\n    printf \"  ${red}Ошибка${reset}: не удалось применить исправление ${yellow}01_info_variable.sh${reset} для релиза ${green}1.1.3.9${reset}\\n\"\n    return 1\n}\n\napply_release_1139_yq_fix() {\n    release_var_file=\"$(get_release_var_file)\" || {\n        printf \"  ${red}Ошибка${reset}: после распаковки не найден файл ${yellow}01_info_variable.sh${reset}\\n\"\n        return 1\n    }\n\n    release_version=$(sed -n 's/^xkeen_current_version=\"\\([^\"]*\\)\".*/\\1/p' \"$release_var_file\" | head -n 1)\n    release_build=$(sed -n 's/^xkeen_build=\"\\([^\"]*\\)\".*/\\1/p' \"$release_var_file\" | head -n 1)\n\n    if [ \"$release_version\" = \"1.1.3.9\" ] && [ \"$release_build\" = \"Stable\" ]; then\n        if ! download_release_fix \"$release_var_file\"; then\n            return 1\n        fi\n    fi\n}\n\nif ! download_xkeen_release; then\n    exit 1\nfi\n\nif ! tar -xzf \"$archive_name\" -C /opt/sbin; then\n    rm -f \"$archive_name\"\n    printf \"  ${red}Ошибка${reset}: не удалось распаковать ${yellow}xkeen.tar.gz${reset}\\n\"\n    exit 1\nfi\n\nrm -f \"$archive_name\"\n\nif [ ! -x /opt/sbin/xkeen ]; then\n    printf \"  ${red}Ошибка${reset}: после распаковки не найден исполняемый файл ${yellow}/opt/sbin/xkeen${reset}\\n\"\n    exit 1\nfi\n\nif ! apply_release_1139_yq_fix; then\n    exit 1\nfi\n\nexec /opt/sbin/xkeen -i"
  },
  {
    "path": "knownissues.md",
    "content": "- При проксировании DNS с помощью XKeen, в профиле \"Политика по умолчанию\" отсутствует интернет, создайте пользовательсую политкику вместо этого профиля\n- При подключении к роутеру нескольких провайдеров, XKeen работает через \"Основное подключение\", даже если в политике XKeen отметить галкой \"Резервное\"\n"
  },
  {
    "path": "scripts/_xkeen/01_info/00_info_import.sh",
    "content": "# Импорт информационных модулей\n\n# Модуль информации\n. \"$xinfo_dir/01_info_variable.sh\"\n. \"$xinfo_dir/02_info_packages.sh\"\n. \"$xinfo_dir/03_info_cpu.sh\"\n. \"$xinfo_dir/04_info_mihomo.sh\"\n. \"$xinfo_dir/04_info_xray.sh\"\n. \"$xinfo_dir/05_info_geofile.sh\"\n. \"$xinfo_dir/06_info_console.sh\"\n. \"$xinfo_dir/07_info_cron.sh\"\n\n. \"$xinfo_dir/08_info_version/00_version_import.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/01_info/01_info_variable.sh",
    "content": "# -------------------------------------\n# Цвета\n# -------------------------------------\ngreen=\"\\033[92m\"\t# Зеленый\nred=\"\\033[91m\"\t\t# Красный\nyellow=\"\\033[93m\"\t# Желтый\nlight_blue=\"\\033[96m\"\t# Голубой\nitalic=\"\\033[3m\"\t# Курсив\nreset=\"\\033[0m\"\t\t# Сброс цветов\n\n# -------------------------------------\n# Информация\n# -------------------------------------\ncurrent_datetime=$(date +\"%Y-%m-%d_%H-%M\")\nxkeen_current_version=\"2.0\"\nxkeen_build=\"Beta\"\nbuild_timestamp=\"\"\n\n# -------------------------------------\n# Директории\n# -------------------------------------\ntmp_dir=\"/opt/tmp\"\t\t\t # Временная директория\nktmp_dir=\"$tmp_dir/xkeen\"\t\t # Временная директория XKeen\nxtmp_dir=\"$tmp_dir/xray\"\t\t # Временная директория Xray\nmtmp_dir=\"$tmp_dir/mihomo\"\t\t # Временная директория Mihomo\ninstall_dir=\"/opt/sbin\"\t\t\t # Директория установки\nxkeen_dir=\"$install_dir/.xkeen\"\t\t # Директория скриптов XKeen\nxkeen_cfg=\"/opt/etc/xkeen\"\t\t # Директория конфигурации XKeen\nipset_cfg=\"$xkeen_cfg/ipset\"\t\t # Директория IPSET\nlog_dir=\"/opt/var/log\"\t\t\t # Директория логов\nxray_log_dir=\"$log_dir/xray\"\t\t # Директория логов Xray\ninitd_dir=\"/opt/etc/init.d\"\t\t # Директория init.d\nbackups_dir=\"/opt/backups\"\t\t # Директория бекапов\ngeo_dir=\"/opt/etc/xray/dat\"\t\t # Директория для dat\ncron_dir=\"/opt/var/spool/cron/crontabs\"\t # Директория планировщика\nmihomo_conf_dir=\"/opt/etc/mihomo\"\t # Директория конфигурации Mihomo\nxray_conf_dir=\"/opt/etc/xray/configs\"\t # Директория конфигурации Xray\nxray_conf_smpl=\"$xkeen_dir/02_install/08_install_configs/02_configs_dir\"\nregister_dir=\"/opt/lib/opkg/info\"\nos_modules=\"/lib/modules/$(uname -r)\"\nuser_modules=\"/opt/lib/modules\"\n\n# -------------------------------------\n# Файлы\n# -------------------------------------\nxkeen_var_file=\"$xkeen_dir/01_info/01_info_variable.sh\"\nfile_port_proxying=\"$xkeen_cfg/port_proxying.lst\"\nfile_port_exclude=\"$xkeen_cfg/port_exclude.lst\"\nfile_ip_exclude=\"$xkeen_cfg/ip_exclude.lst\"\nru_exclude_ipv4=\"$ipset_cfg/ru_exclude_ipv4.lst\"\nru_exclude_ipv6=\"$ipset_cfg/ru_exclude_ipv6.lst\"\nxkeen_config=\"$xkeen_cfg/xkeen.json\"\nstatus_file=\"/opt/lib/opkg/status\"\ninitd_file=\"$initd_dir/S05xkeen\"\ninitd_cron=\"$initd_dir/S05crond\"\ncron_file=\"root\"\nfile_netfilter_hook=\"/opt/etc/ndm/netfilter.d/proxy.sh\"\nfile_schedule_hook=\"/opt/etc/ndm/schedule.d/00-xkeen-hotspot-sync.sh\"\nname_ipset_deny_mac=\"xkeen_deny_mac\"\n\n# -------------------------------------\n# Ресурсы для проверки доступа в интернет\n# -------------------------------------\nconn_URL=\"ya.ru\"\nconn_IP1=\"195.208.4.1\"\nconn_IP2=\"77.88.44.55\"\n\n# -------------------------------------\n# URL\n# -------------------------------------\nxkeen_api_url=\"https://api.github.com/repos/jameszeroX/xkeen/releases/latest\"\t\t\t# url api для XKeen\nxkeen_jsd_url=\"https://data.jsdelivr.com/v1/package/gh/jameszeroX/xkeen\"\t\t\t# резервный url api для XKeen\nxkeen_tar_url=\"https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz\"\t# url для загрузки XKeen\nxkeen_dev_url=\"https://raw.githubusercontent.com/jameszeroX/xkeen/main/test/xkeen.tar.gz\"\t# url для загрузки XKeen dev\nxray_api_url=\"https://api.github.com/repos/XTLS/Xray-core/releases\"\t\t\t\t# url api для Xray\nxray_jsd_url=\"https://data.jsdelivr.com/v1/package/gh/XTLS/Xray-core\"\t\t\t\t# резервный url api для Xray\nxray_zip_url=\"https://github.com/XTLS/Xray-core/releases/download\"\t\t\t\t# url для загрузки Xray\nmihomo_api_url=\"https://api.github.com/repos/MetaCubeX/mihomo/releases\"\t\t\t\t# url api для Mihomo\nmihomo_jsd_url=\"https://data.jsdelivr.com/v1/package/gh/MetaCubeX/mihomo\"\t\t\t# резервный url api для Mihomo\nmihomo_gz_url=\"https://github.com/MetaCubeX/mihomo/releases/download\"\t\t\t\t# url для загрузки Mihomo\nyq_upstream_dist_url=\"https://github.com/mikefarah/yq/releases/latest/download\"\t\t\t# url для загрузки оригинального Yq\nyq_workaround_dist_url=\"https://github.com/jameszeroX/yq/releases/latest/download\"\t\t# url для загрузки рабочего Yq\ngh_proxy1=\"https://gh-proxy.com\"\t\t\t\t\t\t\t\t# 1 прокси для загрузок с GitHub\ngh_proxy2=\"https://ghfast.top\"\t\t\t\t\t\t\t\t\t# 2 прокси для загрузок с GitHub\n\nyq_use_workaround=\"true\"\t\t\t\t\t\t\t\t\t# отключить после исправления issue 2609 (по желанию)\nyq_workaround_issue_url=\"https://github.com/mikefarah/yq/issues/2609\"\t\t\t\t# issue с поломанным релизом Yq\nget_yq_dist_url() {\n    if [ \"$yq_use_workaround\" = \"true\" ]; then\n        printf '%s\\n' \"$yq_workaround_dist_url\"\n    else\n        printf '%s\\n' \"$yq_upstream_dist_url\"\n    fi\n}\n\n# url для загрузки геофайлов\nrefilter_url=\"https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geosite.dat\"\nrefilterip_url=\"https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geoip.dat\"\nv2fly_url=\"https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat\"\nv2flyip_url=\"https://github.com/loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\"\nzkeen_url=\"https://github.com/jameszeroX/zkeen-domains/releases/latest/download/zkeen.dat\"\nzkeenip_url=\"https://github.com/jameszeroX/zkeen-ip/releases/latest/download/zkeenip.dat\"\ngeoipv4_url=\"https://github.com/jameszeroX/zkeen-ip/releases/latest/download/ru\"\ngeoipv6_url=\"https://github.com/jameszeroX/zkeen-ip/releases/latest/download/ru6\"\n\n# -------------------------------------\n# Журналы\n# -------------------------------------\nxray_access_log=\"$xray_log_dir/access.log\"\nxray_error_log=\"$xray_log_dir/error.log\"\n\n# -------------------------------------\n# Создание директорий и файлов\n# -------------------------------------\ninit_directories() {\n    mkdir -p \"$xray_log_dir\" || { echo \"Ошибка: Не удалось создать директорию $xray_log_dir\"; exit 1; }\n    mkdir -p \"$initd_dir\" || { echo \"Ошибка: Не удалось создать директорию $initd_dir\"; exit 1; }\n    mkdir -p \"$backups_dir\" || { echo \"Ошибка: Не удалось создать директорию $backups_dir\"; exit 1; }\n    mkdir -p \"$install_dir\" || { echo \"Ошибка: Не удалось создать директорию $install_dir\"; exit 1; }\n    mkdir -p \"$cron_dir\" || { echo \"Ошибка: Не удалось создать директорию $cron_dir\"; exit 1; }\n    touch \"$xray_access_log\" || { echo \"Ошибка: Не удалось создать файл $xray_access_log\"; exit 1; }\n    touch \"$xray_error_log\" || { echo \"Ошибка: Не удалось создать файл $xray_error_log\"; exit 1; }\n}\n\n# Таймаут curl\n[ -e \"/tmp/toff\" ] && curl_timeout=\"\" || curl_timeout=\"-m 180\"\n\n# Дополнительные параметры curl\ncurl_extra=\"\"\n"
  },
  {
    "path": "scripts/_xkeen/01_info/02_info_packages.sh",
    "content": "# Кэшируем список установленных пакетов один раз вместо opkg-форка на каждую проверку\n_packages_cache=$(opkg list-installed 2>/dev/null)\n\n# Функция для проверки наличия необходимых пакетов\ninfo_packages() {\n    package_name=\"$1\"\n\n    # Newline-prefix эмулирует якорь \"^pkg \", чтобы libc не матчил libcurl\n    case \"\n$_packages_cache\" in\n        *\"\n$package_name \"*) package_status=\"installed\" ;;\n        *) package_status=\"not_installed\" ;;\n    esac\n}\n\n# Проверка наличия пакета \"coreutils-uname\"\ninfo_packages \"coreutils-uname\"\ninfo_packages_uname=$package_status\n\n# Проверка наличия пакета \"coreutils-nohup\"\ninfo_packages \"coreutils-nohup\"\ninfo_packages_nohup=$package_status\n\n# Проверка наличия пакета \"curl\"\ninfo_packages \"curl\"\ninfo_packages_curl=$package_status\n\n# Проверка наличия пакета \"jq\"\ninfo_packages \"jq\"\ninfo_packages_jq=$package_status\n\n# Проверка наличия пакета \"libc\"\ninfo_packages \"libc\"\ninfo_packages_libc=$package_status\n\n# Проверка наличия пакета \"libssp\"\ninfo_packages \"libssp\"\ninfo_packages_libssp=$package_status\n\n# Проверка наличия пакета \"librt\"\ninfo_packages \"librt\"\ninfo_packages_librt=$package_status\n\n# Проверка наличия пакета \"libpthread\"\ninfo_packages \"libpthread\"\ninfo_packages_libpthread=$package_status\n\n# Проверка наличия пакета \"ca-bundle\"\ninfo_packages \"ca-bundle\"\ninfo_packages_cabundle=$package_status\n\n# Проверка наличия пакета \"iptables\"\ninfo_packages \"iptables\"\ninfo_packages_iptables=$package_status\n\n# Проверка наличия пакета \"ipset\"\ninfo_packages \"ipset\"\ninfo_packages_ipset=$package_status\n"
  },
  {
    "path": "scripts/_xkeen/01_info/03_info_cpu.sh",
    "content": "# Функция для получения информации о процессоре\ninfo_cpu() {\n    if command -v opkg >/dev/null 2>&1; then\n        opkg_arch=$(opkg print-architecture | awk '!/all/ {print $2; exit}' | cut -d- -f1)\n        \n        case \"$opkg_arch\" in\n            *'aarch64'*) architecture='arm64-v8a' ;;\n            *'mipsel'*) architecture='mips32le' ;;\n            *'mips'*) architecture='mips32' ;;\n            *) architecture=\"$opkg_arch\" ;;\n        esac\n    fi\n\n    # Получение информации о архитектуре из файла состояния (status_file)\n    status_architecture=$(grep -m 1 '^Architecture:' \"${status_file}\" | awk '{print $2}')\n\n    # Получение информации о необходимости softfloat банарников\n    [ \"$architecture\" != \"mips32le\" ] && echo && return\n    version=\"$(curl -kfsS \"localhost:79/rci/show/version\" 2>/dev/null)\"\n\n    case \"$version\" in\n        *KN-1212*|*KN-2910*) softfloat=\"true\" ;;\n        *) echo; return ;;\n    esac\n}\n"
  },
  {
    "path": "scripts/_xkeen/01_info/04_info_mihomo.sh",
    "content": "# Функция для проверки установки Mihomo\n\ninfo_mihomo() {\n    if [ -f \"$install_dir/mihomo\" ] && [ -f \"$install_dir/yq\" ]; then\n        mihomo_installed=\"installed\"\n    else\n        mihomo_installed=\"not_installed\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/01_info/04_info_xray.sh",
    "content": "# Функция для проверки установки Xray\n\ninfo_xray() {\n    if [ -f \"$install_dir/xray\" ]; then\n        xray_installed=\"installed\"\n    else\n        xray_installed=\"not_installed\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/01_info/05_info_geofile.sh",
    "content": "# Функция для проверки наличия и записи информации о базах GeoSite\ninfo_geosite() {\n    update_refilter_geosite=false\n    update_v2fly_geosite=false\n    update_zkeen_geosite=false\n    [ -f \"$geo_dir/geosite_refilter.dat\" ] && update_refilter_geosite=true\n    [ -f \"$geo_dir/geosite_v2fly.dat\" ] && update_v2fly_geosite=true\n    [ -f \"$geo_dir/geosite_zkeen.dat\" ] || [ -f \"$geo_dir/zkeen.dat\" ] && update_zkeen_geosite=true\n}\n\n# Функция для проверки наличия и записи информации о базах GeoIP\ninfo_geoip() {\n    update_refilter_geoip=false\n    update_v2fly_geoip=false\n    update_zkeenip_geoip=false\n    [ -f \"$geo_dir/geoip_refilter.dat\" ] && update_refilter_geoip=true\n    [ -f \"$geo_dir/geoip_v2fly.dat\" ] && update_v2fly_geoip=true\n    [ -f \"$geo_dir/geoip_zkeenip.dat\" ] || [ -f \"$geo_dir/zkeenip.dat\" ] && update_zkeenip_geoip=true\n}"
  },
  {
    "path": "scripts/_xkeen/01_info/06_info_console.sh",
    "content": "print_log_status() {\n    local status_code=$1\n    local success_msg=$2\n    local error_msg=$3\n\n    if [ \"$status_code\" -eq 0 ]; then\n        echo -e \"  ${green}Успешно${reset}: $success_msg\"\n    else\n        echo -e \"  ${red}Ошибка${reset}: $error_msg\"\n    fi\n}\n\n# Обратная связь в консоль\nlogs_cpu_info_console() {\n    echo -e \"  Набор инструкций процессора: ${yellow}$architecture${reset}\"\n    \n    case \"$architecture\" in\n        arm64-v8a|mips32le|mips32)\n            echo -e \"  Процессор ${green}поддерживается${reset} XKeen\"\n            ;;\n        *)\n            echo -e \"  Процессор ${red}не поддерживается${reset} XKeen\"\n            ;;\n    esac\n}\n\nlogs_delete_configs_info_console() {\n    local deleted_files=\"\"\n    \n    if [ -d \"$xray_conf_dir\" ]; then\n        deleted_files=$(find \"$xray_conf_dir\" -maxdepth 1 -name '*.json' -type f)\n    fi\n\n    if [ -z \"$deleted_files\" ]; then\n        echo -e \"  ${green}Успешно${reset}: Все конфигурационные файлы Xray удалены\"\n    else\n        echo -e \"  ${red}Ошибка${reset}: Не удалены следующие конфигурационные файлы:\"\n        for file in $deleted_files; do\n            echo -e \"    $file\"\n        done\n    fi\n}\n\nlogs_delete_geosite_info_console() {\n    echo -e \"  ${yellow}Проверка${reset} выполнения операции\"\n    # antifilter переименован в refilter в install/delete, имя verification отстало\n    for file in \"geosite_refilter.dat\" \"geosite_v2fly.dat\" \"geosite_zkeen.dat\"; do\n        [ ! -f \"$geo_dir/$file\" ]\n        print_log_status $? \"Файл $file отсутствует в директории '$geo_dir'\" \"Файл $file не удален\"\n    done\n}\n\nlogs_delete_geoip_info_console() {\n    echo -e \"  ${yellow}Проверка${reset} выполнения операции\"\n    for file in \"geoip_refilter.dat\" \"geoip_v2fly.dat\" \"geoip_zkeenip.dat\"; do\n        [ ! -f \"$geo_dir/$file\" ]\n        print_log_status $? \"Файл $file отсутствует в директории '$geo_dir'\" \"Файл $file не удален\"\n    done\n}\n\nlogs_delete_geoipset_info_console() {\n    echo -e \"  ${yellow}Проверка${reset} выполнения операции\"\n    \n    [ ! -f \"$ru_exclude_ipv4\" ]\n    print_log_status $? \"Файл ru_exclude_ipv4.lst отсутствует в директории '$ipset_cfg'\" \"Файл ru_exclude_ipv4.lst не удален\"\n    \n    [ ! -f \"$ru_exclude_ipv6\" ]\n    print_log_status $? \"Файл ru_exclude_ipv6.lst отсутствует в директории '$ipset_cfg'\" \"Файл ru_exclude_ipv6.lst не удален\"\n}\n\n# Проверки регистрации XKeen\n\nlogs_register_xkeen_status_info_console() {\n    grep -q \"Package: xkeen\" \"$status_file\"\n    print_log_status $? \"Запись XKeen найдена в '$status_file'\" \"Запись XKeen не найдена в '$status_file'\"\n}\n\nlogs_register_xkeen_control_info_console() {\n    [ -f \"$register_dir/xkeen.control\" ]\n    print_log_status $? \"Файл xkeen.control найден в директории '$register_dir/'\" \"Файл xkeen.control не найден в директории '$register_dir/'\"\n}\n\nlogs_register_xkeen_list_info_console() {\n    [ -f \"$register_dir/xkeen.list\" ]\n    print_log_status $? \"Файл xkeen.list найден в директории '$register_dir/'\" \"Файл xkeen.list не найден в директории '$register_dir/'\"\n}\n\nlogs_register_xkeen_initd_info_console() {\n    [ -f \"$initd_file\" ]\n    print_log_status $? \"init скрипт XKeen найден в директории '$initd_dir/'\" \"init скрипт XKeen не найден в директории '$initd_dir/'\"\n}\n\nlogs_delete_register_xkeen_info_console() {\n    [ ! -f \"$register_dir/xkeen.list\" ]\n    print_log_status $? \"Файл xkeen.list не найден в директории '$register_dir/'\" \"Файл xkeen.list найден в директории '$register_dir/'\"\n\n    [ ! -f \"$register_dir/xkeen.control\" ]\n    print_log_status $? \"Файл xkeen.control не найден в директории '$register_dir/'\" \"Файл xkeen.control найден в директории '$register_dir/'\"\n\n    ! grep -q 'Package: xkeen' \"$status_file\"\n    print_log_status $? \"Регистрация пакета xkeen не обнаружена в '$status_file'\" \"Регистрация пакета xkeen обнаружена в '$status_file'\"\n}\n\n# Проверки регистрации Xray\n\nlogs_register_xray_status_info_console() {\n    grep -q \"Package: xray_s\" \"$status_file\"\n    print_log_status $? \"Запись Xray найдена в '$status_file'\" \"Запись Xray не найдена в '$status_file'\"\n}\n\nlogs_register_xray_control_info_console() {\n    [ -f \"$register_dir/xray_s.control\" ]\n    print_log_status $? \"Файл xray_s.control найден в директории '$register_dir/'\" \"Файл xray_s.control не найден в директории '$register_dir/'\"\n}\n\nlogs_register_xray_list_info_console() {\n    [ -f \"$register_dir/xray_s.list\" ]\n    print_log_status $? \"Файл xray_s.list найден в директории '$register_dir/'\" \"Файл xray_s.list не найден в директории '$register_dir/'\"\n}\n\nlogs_delete_register_xray_info_console() {\n    [ ! -f \"$register_dir/xray_s.list\" ]\n    print_log_status $? \"Файл xray_s.list не найден в директории '$register_dir/'\" \"Файл xray_s.list найден в директории '$register_dir/'\"\n\n    [ ! -f \"$register_dir/xray_s.control\" ]\n    print_log_status $? \"Файл xray_s.control не найден в директории '$register_dir/'\" \"Файл xray_s.control найден в директории '$register_dir/'\"\n\n    ! grep -q 'Package: xray_s' \"$status_file\"\n    print_log_status $? \"Регистрация пакета xray не обнаружена в '$status_file'\" \"Регистрация пакета xray обнаружена в '$status_file'\"\n}\n\n# Проверки регистрации Mihomo\n\nlogs_register_mihomo_status_info_console() {\n    grep -q \"Package: mihomo\" \"$status_file\"\n    print_log_status $? \"Запись mihomo найдена в '$status_file'\" \"Запись mihomo не найдена в '$status_file'\"\n}\n\nlogs_register_mihomo_control_info_console() {\n    [ -f \"$register_dir/mihomo_s.control\" ]\n    print_log_status $? \"Файл mihomo_s.control найден в директории '$register_dir/'\" \"Файл mihomo_s.control не найден в директории '$register_dir/'\"\n}\n\nlogs_register_mihomo_list_info_console() {\n    [ -f \"$register_dir/mihomo_s.list\" ]\n    print_log_status $? \"Файл mihomo_s.list найден в директории '$register_dir/'\" \"Файл mihomo_s.list не найден в директории '$register_dir/'\"\n}\n\nlogs_delete_register_mihomo_info_console() {\n    [ ! -f \"$register_dir/mihomo_s.list\" ]\n    print_log_status $? \"Файл mihomo_s.list не найден в директории '$register_dir/'\" \"Файл mihomo_s.list найден в директории '$register_dir/'\"\n\n    [ ! -f \"$register_dir/mihomo_s.control\" ]\n    print_log_status $? \"Файл mihomo_s.control не найден в директории '$register_dir/'\" \"Файл mihomo_s.control найден в директории '$register_dir/'\"\n\n    ! grep -q 'Package: mihomo_s' \"$status_file\"\n    print_log_status $? \"Регистрация пакета mihomo не обнаружена в '$status_file'\" \"Регистрация пакета mihomo обнаружена в '$status_file'\"\n}\n\n# Проверки регистрации YQ\n\nlogs_register_yq_status_info_console() {\n    grep -q \"Package: yq\" \"$status_file\"\n    print_log_status $? \"Запись yq найдена в '$status_file'\" \"Запись yq не найдена в '$status_file'\"\n}\n\nlogs_register_yq_control_info_console() {\n    [ -f \"$register_dir/yq_s.control\" ]\n    print_log_status $? \"Файл yq_s.control найден в директории '$register_dir/'\" \"Файл yq_s.control не найден в директории '$register_dir/'\"\n}\n\nlogs_register_yq_list_info_console() {\n    [ -f \"$register_dir/yq_s.list\" ]\n    print_log_status $? \"Файл yq_s.list найден в директории '$register_dir/'\" \"Файл yq_s.list не найден в директории '$register_dir/'\"\n}\n\nlogs_delete_register_yq_info_console() {\n    [ ! -f \"$register_dir/yq_s.list\" ]\n    print_log_status $? \"Файл yq_s.list не найден в директории '$register_dir/'\" \"Файл yq_s.list найден в директории '$register_dir/'\"\n\n    [ ! -f \"$register_dir/yq_s.control\" ]\n    print_log_status $? \"Файл yq_s.control не найден в директории '$register_dir/'\" \"Файл yq_s.control найден в директории '$register_dir/'\"\n\n    ! grep -q 'Package: yq_s' \"$status_file\"\n    print_log_status $? \"Регистрация пакета yq не обнаружена в '$status_file'\" \"Регистрация пакета yq обнаружена в '$status_file'\"\n}\n\n# Остальные проверки\n\nlogs_delete_cron_geofile_info_console() {\n    if [ -f \"$cron_dir/$cron_file\" ]; then\n        ! grep -q \"ug\" \"$cron_dir/$cron_file\"\n        print_log_status $? \"Задача автоматического обновления GeoFile удалена из cron\" \"Задача автоматического обновления GeoFile не удалена из cron\"\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/01_info/07_info_cron.sh",
    "content": "# Проверка наличия задач автоматического обновления в cron\ninfo_cron() {\n    # Получаем текущую crontab конфигурацию для пользователя root\n    cron_output=$(crontab -l -u root 2>/dev/null)\n\n    # Проверяем наличие задачи с ключевым словом \"ug\" в crontab\n    if echo \"$cron_output\" | grep -q \"ug\"; then\n        info_update_geofile_cron=\"installed\"\n    else\n        info_update_geofile_cron=\"not_installed\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/00_version_import.sh",
    "content": "# Импорт модулей проверки версий\n\n# Модули проверки версий\n. \"$xinfo_dir/08_info_version/01_version_xkeen.sh\"\n. \"$xinfo_dir/08_info_version/02_version_mihomo.sh\"\n. \"$xinfo_dir/08_info_version/02_version_xray.sh\""
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/01_version_xkeen.sh",
    "content": "# Функция для получения версии из xkeen API и сохранения ее в переменной\ninfo_version_xkeen() {\n    version=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s \"$xkeen_api_url\" | jq -r '.tag_name // .name // \"\"' 2>/dev/null)\n\n    if [ -z \"$version\" ]; then\n        echo\n        printf \"${red}Нет доступа${reset} к ${yellow}GitHub API${reset}, пробуем ${yellow}jsDelivr${reset}...\\n\"\n        version=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s \"$xkeen_jsd_url\" | jq -r '.versions | first' 2>/dev/null)\n\n        if [ -z \"$version\" ]; then\n            echo\n            printf \"  ${red}Нет доступа${reset} к ${yellow}jsDelivr${reset}\\n\"\n            echo\n            printf \"${red}Ошибка${reset}: Не удалось получить версию ни с ${yellow}GitHub${reset}, ни с ${yellow}jsDelivr${reset}\\n\n  Проверьте соединение с интернетом или повторите позже\\n\n  Если ошибка сохраняется, воспользуйтесь возможностью OffLine установки:\\n\n  https://github.com/jameszeroX/XKeen/blob/main/OffLine_install.md\\n\"\n            echo\n            exit 1\n        fi\n    fi\n\n    xkeen_github_version=\"${version}\"\n}\n\n# Функция для сравнения версий XKeen\ninfo_compare_xkeen() {\n    if [ \"$xkeen_current_version\" = \"$xkeen_github_version\" ]; then\n        info_compare_xkeen=\"actual\"\n    else\n        info_compare_xkeen=\"update\"\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/02_version_mihomo.sh",
    "content": "# Функции для получения информации о версиях Mihomo и Yq\ninfo_version_mihomo() {\n    if [ \"$mihomo_installed\" = \"installed\" ]; then\n        mihomo_current_version=$(\"$install_dir/mihomo\" -v 2>&1 | grep -oE 'v?[0-9]+\\.[0-9]+\\.[0-9]+' | sed 's/^v//' | head -1)\n        mihomo_current_version=${mihomo_current_version:-\"unknown\"}\n    else\n        mihomo_current_version=\"unknown\"\n    fi\n}\n\ninfo_version_yq() {\n    if [ \"$mihomo_installed\" = \"installed\" ]; then\n        yq_current_version=$(\"$install_dir/yq\" -V 2>&1 | grep -oE 'v?[0-9]+\\.[0-9]+\\.[0-9]+' | sed 's/^v//' | head -1)\n        yq_current_version=${yq_current_version:-\"unknown\"}\n    else\n        yq_current_version=\"unknown\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/02_version_xray.sh",
    "content": "# Функция для получения информации о версии Xray\ninfo_version_xray() {\n\n    # Проверяем, установлен ли Xray\n    if [ \"$xray_installed\" = \"installed\" ]; then\n        # Если Xray установлен, получаем текущую версию\n        xray_current_version=$(\"$install_dir/xray\" version 2>&1 | grep -oE 'v?[0-9]+\\.[0-9]+\\.[0-9]+' | sed 's/^v//' | head -1)\n        xray_current_version=${xray_current_version:-\"unknown\"}\n    else\n        xray_current_version=\"unknown\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/00_install_import.sh",
    "content": "# Импорт модулей установки\n\n# Модули установки\n. \"$xinstall_dir/01_install_packages.sh\"\n. \"$xinstall_dir/02_install_xray.sh\"\n. \"$xinstall_dir/02_install_mihomo.sh\"\n. \"$xinstall_dir/03_install_xkeen.sh\"\n. \"$xinstall_dir/04_install_geofile.sh\"\n. \"$xinstall_dir/05_install_geoipset.sh\"\n. \"$xinstall_dir/06_install_cron.sh\"\n\n. \"$xinstall_dir/07_install_register/00_register_import.sh\"\n. \"$xinstall_dir/08_install_configs/00_configs_import.sh\""
  },
  {
    "path": "scripts/_xkeen/02_install/01_install_packages.sh",
    "content": "# Установка необходимых пакетов\ninstall_packages() {\n    package_status=\"$1\"\n    package_name=\"$2\"\n\n    if [ \"${package_status}\" = \"not_installed\" ]; then\n        opkg install \"$package_name\" &>/dev/null\n    fi\n}\n\ninstall_packages \"$info_packages_curl\" \"curl\"\ninstall_packages \"$info_packages_jq\" \"jq\"\ninstall_packages \"$info_packages_libc\" \"libc\"\ninstall_packages \"$info_packages_libssp\" \"libssp\"\ninstall_packages \"$info_packages_librt\" \"librt\"\ninstall_packages \"$info_packages_iptables\" \"iptables\"\ninstall_packages \"$info_packages_libpthread\" \"libpthread\"\ninstall_packages \"$info_packages_ipset\" \"ipset\"\ninstall_packages \"$info_packages_cabundle\" \"ca-bundle\"\ninstall_packages \"$info_packages_uname\" \"coreutils-uname\"\ninstall_packages \"$info_packages_nohup\" \"coreutils-nohup\""
  },
  {
    "path": "scripts/_xkeen/02_install/02_install_mihomo.sh",
    "content": "# Функция для установки Mihomo\ninstall_mihomo() {\n    echo -e \"  ${yellow}Выполняется установка${reset} Mihomo. Пожалуйста, подождите...\"\n\n    # Определение переменных\n    mihomo_archive=\"${mtmp_dir}/mihomo.gz\"\n\n    # Проверка наличия архива Mihomo\n    if [ ! -f \"${mihomo_archive}\" ]; then\n        echo -e \"  ${red}Ошибка${reset}: Архив Mihomo не найден в '${mtmp_dir}'\"\n        return 1\n    fi\n\n    if [ -f \"$install_dir/mihomo\" ]; then\n        mv \"$install_dir/mihomo\" \"$install_dir/mihomo_bak\"\n    fi\n\n    if ! gzip -d \"${mihomo_archive}\" || [ ! -f \"${mtmp_dir}/mihomo\" ]; then\n        echo -e \"  ${red}Ошибка${reset}: Не удалось распаковать архив или файл отсутствует\"\n        if [ -f \"$install_dir/mihomo_bak\" ]; then\n            mv \"$install_dir/mihomo_bak\" \"$install_dir/mihomo\"\n            echo -e \"  ${yellow}Восстановлен${reset} предыдущий бинарник Mihomo\"\n        fi\n        rm -f \"${mtmp_dir}/mihomo.gz\" \"${mtmp_dir}/mihomo\"\n        return 1\n    fi\n\n    if ! mv \"${mtmp_dir}/mihomo\" \"$install_dir/\"; then\n        echo -e \"  ${red}Ошибка${reset}: Не удалось установить Mihomo\"\n        [ -f \"$install_dir/mihomo_bak\" ] && mv \"$install_dir/mihomo_bak\" \"$install_dir/mihomo\"\n        return 1\n    fi\n\n    chmod +x \"$install_dir/mihomo\"\n    echo -e \"  Mihomo ${green}успешно установлен${reset}\"\n\n    return 0\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/02_install_xray.sh",
    "content": "# Функция для установки Xray\ninstall_xray() {\n    echo -e \"  ${yellow}Выполняется установка${reset} Xray. Пожалуйста, подождите...\"\n\n    # Определение переменных\n    xray_archive=\"${xtmp_dir}/xray.zip\"\n\n    # Проверка наличия архива Xray\n    if [ ! -f \"${xray_archive}\" ]; then\n        echo -e \"  ${red}Ошибка${reset}: Архив Xray не найден в '${xtmp_dir}'\"\n        return 1\n    fi\n\n    if [ -f \"$install_dir/xray\" ]; then\n        mv \"$install_dir/xray\" \"$install_dir/xray_bak\"\n    fi\n\n    # Распаковка архива Xray\n    if [ -d \"${xtmp_dir}/xray\" ]; then\n        rm -rf \"${xtmp_dir}/xray\"\n    fi\n\n    if ! unzip -q \"${xray_archive}\" -d \"${xtmp_dir}/xray\"; then\n        echo -e \"  ${red}Ошибка${reset}: Не удалось распаковать архив\"\n        [ -f \"$install_dir/xray_bak\" ] && mv \"$install_dir/xray_bak\" \"$install_dir/xray\"\n        return 1\n    fi\n\n    bin_source=\"${xtmp_dir}/xray/xray\"\n\n    if [ \"$softfloat\" = \"true\" ]; then\n        if [ -f \"${xtmp_dir}/xray/xray_softfloat\" ]; then\n            bin_source=\"${xtmp_dir}/xray/xray_softfloat\"\n        fi\n    fi\n\n    if [ ! -f \"$bin_source\" ]; then\n        echo -e \"  ${red}Ошибка${reset}: Бинарный файл Xray не найден в архиве\"\n        if [ -f \"$install_dir/xray_bak\" ]; then\n            mv \"$install_dir/xray_bak\" \"$install_dir/xray\"\n            echo -e \"  ${yellow}Восстановлен${reset} предыдущий бинарник Xray\"\n        fi\n        rm -f \"$xray_archive\"\n        rm -rf \"${xtmp_dir}/xray\"\n        return 1\n    fi\n\n    mv \"$bin_source\" \"$install_dir/xray\"\n    chmod +x \"$install_dir/xray\"\n    echo -e \"  Xray ${green}успешно установлен${reset}\"\n\n    rm -f \"$xray_archive\"\n    rm -rf \"${xtmp_dir}/xray\"\n\n    # Фикс для новых ядер xray\n    if [ -d \"$xray_conf_dir\" ]; then\n        for file in \"$xray_conf_dir\"/*.json; do\n            [ -f \"$file\" ] || continue\n            if grep -qE '\"transport\"\\s*:' \"$file\"; then\n                mv \"$file\" \"${file}.obsolete\"\n            fi\n        done\n    fi\n\n    return 0\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/03_install_xkeen.sh",
    "content": "# Функция для установки XKeen\ninstall_xkeen() {\n    xkeen_archive=\"$ktmp_dir/xkeen.tar.gz\"\n\n    # Проверка наличия архива XKeen\n    if [ -f \"$xkeen_archive\" ]; then\n        # Распаковка архива\n        tar -xzf \"$xkeen_archive\" -C \"$install_dir\" xkeen _xkeen\n\n        # Проверка наличия _xkeen в install_dir и его перемещение\n        if [ -d \"$install_dir/_xkeen\" ]; then\n            rm -rf \"$install_dir/.xkeen\"\n            mv \"$install_dir/_xkeen\" \"$install_dir/.xkeen\"\n        else\n            echo -e \"  ${red}Ошибка${reset}: _xkeen не была правильно перенесена\"\n        fi\n\n        # Удаление архива\n        rm \"$xkeen_archive\"\n    fi\n    [ -d \"$log_dir/xkeen\" ] && rm -rf \"$log_dir/xkeen\"\n}\n\ncheck_keen_mode() {\n    [ \"$(sysctl -n net.ipv4.ip_forward 2>/dev/null)\" = \"1\" ] && return 0\n    keen_mode=\"unsupported\"\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/04_install_geofile.sh",
    "content": "# Функция для загрузки и обработки геофайлов\nprocess_geo_file() {\n    local url=\"$1\"\n    local filename=\"$2\"\n    local display_name=\"$3\"\n    local update_flag=\"$4\"\n\n    # Защита от path traversal\n    if case \"$filename\" in */*|*\\\\*|..|.) true;; *) false;; esac; then\n        printf \"  ${red}Ошибка${reset}: Недопустимое имя файла %s (path traversal)\\n\" \"$filename\"\n        return 1\n    fi\n\n    local min_size=24576  # 24 KB\n\n    printf \"  Загрузка %s...\\n\" \"$display_name\"\n\n    if ! fetch_with_mirrors \"$url\" \"$geo_dir/$filename\" \"$min_size\"; then\n        case \"$_last_error\" in\n            size)\n                printf \"  ${red}Ошибка${reset}: загруженный файл слишком мал (%s bytes) или повреждён\\n  Невозможно обновить. Оставляем старый файл\\n\\n\" \"$_last_size\"\n                ;;\n            html_stub)\n                printf \"  ${red}Ошибка${reset}: получена HTML-страница вместо dat-файла\\n  Невозможно обновить. Оставляем старый файл\\n\\n\"\n                ;;\n            *)\n                printf \"  ${red}Ошибка${reset}: не удалось загрузить %s\\n\" \"$display_name\"\n                ;;\n        esac\n        return 1\n    fi\n\n    if [ \"$update_flag\" = \"true\" ]; then\n        printf \"  %s ${green}успешно обновлён${reset}\\n\\n\" \"$display_name\"\n    else\n        printf \"  %s ${green}успешно установлен${reset}\\n\\n\" \"$display_name\"\n    fi\n\n    return 0\n}\n\n# Функция для установки и обновления GeoSite\ninstall_geosite() {\n    mkdir -p \"$geo_dir\" || { echo \"Ошибка: Не удалось создать директорию $geo_dir\"; exit 1; }\n\n    local zkeen_datfile=\"\"\n    if [ \"$install_zkeen_geosite\" = \"true\" ] || [ \"$update_zkeen_geosite\" = \"true\" ]; then\n        zkeen_datfile=\"geosite_zkeen.dat\"\n        if [ -L \"$geo_dir/geosite_zkeen.dat\" ]; then\n            zkeen_datfile=\"zkeen.dat\"\n        elif [ -L \"$geo_dir/zkeen.dat\" ]; then\n            zkeen_datfile=\"geosite_zkeen.dat\"\n        elif [ -f \"$geo_dir/zkeen.dat\" ] && ! [ -f \"$geo_dir/geosite_zkeen.dat\" ]; then\n            zkeen_datfile=\"zkeen.dat\"\n        fi\n    fi\n\n    # Параллельная загрузка независимых геофайлов\n    local _pids=\"\"\n    if [ \"$install_refilter_geosite\" = \"true\" ] || [ \"$update_refilter_geosite\" = \"true\" ]; then\n        process_geo_file \"$refilter_url\" \"geosite_refilter.dat\" \\\n            \"GeoSite Re:filter\" \"$update_refilter_geosite\" &\n        _pids=\"$_pids $!\"\n    fi\n\n    if [ \"$install_v2fly_geosite\" = \"true\" ] || [ \"$update_v2fly_geosite\" = \"true\" ]; then\n        process_geo_file \"$v2fly_url\" \"geosite_v2fly.dat\" \\\n            \"GeoSite V2Fly\" \"$update_v2fly_geosite\" &\n        _pids=\"$_pids $!\"\n    fi\n\n    if [ -n \"$zkeen_datfile\" ]; then\n        process_geo_file \"$zkeen_url\" \"$zkeen_datfile\" \\\n            \"GeoSite ZKeen\" \"$update_zkeen_geosite\" &\n        _pids=\"$_pids $!\"\n    fi\n\n    [ -n \"$_pids\" ] && wait $_pids\n\n    # Симлинки zkeen после успешной загрузки\n    if [ -n \"$zkeen_datfile\" ]; then\n        if [ \"$zkeen_datfile\" = \"geosite_zkeen.dat\" ]; then\n            rm -f \"$geo_dir/zkeen.dat\"\n            ln -sf \"$geo_dir/geosite_zkeen.dat\" \"$geo_dir/zkeen.dat\"\n        else\n            rm -f \"$geo_dir/geosite_zkeen.dat\"\n            ln -sf \"$geo_dir/zkeen.dat\" \"$geo_dir/geosite_zkeen.dat\"\n        fi\n    fi\n}\n\n# Функция для установки и обновления GeoIP\ninstall_geoip() {\n    mkdir -p \"$geo_dir\" || { echo \"Ошибка: Не удалось создать директорию $geo_dir\"; exit 1; }\n\n    local zkeenip_datfile=\"\"\n    if [ \"$install_zkeenip_geoip\" = \"true\" ] || [ \"$update_zkeenip_geoip\" = \"true\" ]; then\n        zkeenip_datfile=\"geoip_zkeenip.dat\"\n        if [ -L \"$geo_dir/geoip_zkeenip.dat\" ]; then\n            zkeenip_datfile=\"zkeenip.dat\"\n        elif [ -L \"$geo_dir/zkeenip.dat\" ]; then\n            zkeenip_datfile=\"geoip_zkeenip.dat\"\n        elif [ -f \"$geo_dir/zkeenip.dat\" ] && ! [ -f \"$geo_dir/geoip_zkeenip.dat\" ]; then\n            zkeenip_datfile=\"zkeenip.dat\"\n        fi\n    fi\n\n    # Параллельная загрузка независимых геофайлов\n    local _pids=\"\"\n    if [ \"$install_refilter_geoip\" = \"true\" ] || [ \"$update_refilter_geoip\" = \"true\" ]; then\n        process_geo_file \"$refilterip_url\" \"geoip_refilter.dat\" \\\n            \"GeoIP Re:filter\" \"$update_refilter_geoip\" &\n        _pids=\"$_pids $!\"\n    fi\n\n    if [ \"$install_v2fly_geoip\" = \"true\" ] || [ \"$update_v2fly_geoip\" = \"true\" ]; then\n        process_geo_file \"$v2flyip_url\" \"geoip_v2fly.dat\" \\\n            \"GeoIP V2Fly\" \"$update_v2fly_geoip\" &\n        _pids=\"$_pids $!\"\n    fi\n\n    if [ -n \"$zkeenip_datfile\" ]; then\n        process_geo_file \"$zkeenip_url\" \"$zkeenip_datfile\" \\\n            \"GeoIP ZKeenIP\" \"$update_zkeenip_geoip\" &\n        _pids=\"$_pids $!\"\n    fi\n\n    [ -n \"$_pids\" ] && wait $_pids\n\n    # Симлинки zkeenip после успешной загрузки\n    if [ -n \"$zkeenip_datfile\" ]; then\n        if [ \"$zkeenip_datfile\" = \"geoip_zkeenip.dat\" ]; then\n            rm -f \"$geo_dir/zkeenip.dat\"\n            ln -sf \"$geo_dir/geoip_zkeenip.dat\" \"$geo_dir/zkeenip.dat\"\n        else\n            rm -f \"$geo_dir/geoip_zkeenip.dat\"\n            ln -sf \"$geo_dir/zkeenip.dat\" \"$geo_dir/geoip_zkeenip.dat\"\n        fi\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/05_install_geoipset.sh",
    "content": "# Валидаторы для fetch_with_mirrors: проверяют размер + базовый синтаксис\n# содержимого (catch HTML-stub и мусор от proxy-error-page).\n_validate_geoipset_v4() {\n    _validate_default \"$1\" \"$2\" || return 1\n    if ! grep -q \"^[0-9]\" \"$1\"; then\n        _last_error=\"content_v4\"\n        return 1\n    fi\n    return 0\n}\n\n_validate_geoipset_v6() {\n    _validate_default \"$1\" \"$2\" || return 1\n    if ! grep -q \":\" \"$1\"; then\n        _last_error=\"content_v6\"\n        return 1\n    fi\n    return 0\n}\n\n# Функция для установки и обновления GeoIPSET\ninstall_geoipset_lst() {\n    mkdir -p \"$ipset_cfg\" || { echo \"Ошибка: Не удалось создать директорию $ipset_cfg\"; exit 1; }\n\n    url=\"$1\"\n    dest_file=\"$2\"\n    display_name=\"$3\"\n    ip_type=\"$4\"\n\n    printf \"  Загрузка %s...\\n\" \"$display_name\"\n\n    if [ \"$ip_type\" = \"ipv4\" ]; then\n        _validator_name=\"_validate_geoipset_v4\"\n    else\n        _validator_name=\"_validate_geoipset_v6\"\n    fi\n\n    if ! fetch_with_mirrors \"$url\" \"$dest_file\" 0 \"$_validator_name\"; then\n        case \"$_last_error\" in\n            html_stub)\n                printf \"  ${red}Ошибка${reset}: получена HTML-страница вместо списка IP\\n  Оставляем старый файл\\n\\n\"\n                ;;\n            content_v4)\n                printf \"  ${red}Ошибка${reset}: %s не содержит корректных IPv4-адресов\\n  Оставляем старый файл\\n\\n\" \"$display_name\"\n                ;;\n            content_v6)\n                printf \"  ${red}Ошибка${reset}: %s не содержит корректных IPv6-адресов\\n  Оставляем старый файл\\n\\n\" \"$display_name\"\n                ;;\n            *)\n                printf \"  ${red}Ошибка${reset}: не удалось загрузить %s\\n\\n\" \"$display_name\"\n                ;;\n        esac\n        return 1\n    fi\n\n    [ \"$action\" = \"init\" ] && msg_geoipset=\"установлен\" || msg_geoipset=\"обновлён\"\n    printf \"  %s ${green}успешно $msg_geoipset${reset}\\n\\n\" \"$display_name\"\n    return 0\n}\n\nload_geoipset() {\n    set=\"$1\"\n    file=\"$2\"\n    family=\"$3\"\n    tmp=\"${set}_tmp\"\n\n    # Заполняем tmp; основной набор подменяется только после успешного restore\n    ipset create \"$set\" hash:net family \"$family\" -exist\n    ipset create \"$tmp\" hash:net family \"$family\" -exist\n    ipset flush \"$tmp\"\n\n    if [ -f \"$file\" ] && awk '/^[0-9a-fA-F]/ {print \"add '\"$tmp\"' \"$1}' \"$file\" | ipset restore -exist; then\n        ipset swap \"$set\" \"$tmp\"\n    fi\n    ipset destroy \"$tmp\"\n}\n\ninstall_geoipset() {\n    action=\"$1\"\n\n    if [ \"$action\" = \"init\" ]; then\n        # Без TTY (cron, ssh -T) read получает EOF, default-case крутит while true\n        # бесконечно: процесс висит в R-state с CPU-spin. Дефолтим выбор на \"1\"\n        # (установить), потому что xkeen -gips из cron это типичный\n        # non-interactive caller, где пользователь явно ожидает установку.\n        if [ ! -t 0 ]; then\n            printf \"  Не интерактивный режим (нет TTY): автоматическая установка GeoIPSET\\n\"\n            bypass_cron_geoipset=false\n        else\n            while true; do\n                printf \"\\n  Желаете исключить российские IP-адреса из проксирования?\\n\\n\"\n                printf \"     1. Загрузить и установить в исключения IP-подсети России (${yellow}GeoIPSET${reset})\\n\"\n                printf \"     0. Пропустить\\n\\n\"\n                printf \"  Ваш выбор: \"\n                read -r choice\n\n                case \"$choice\" in\n                    0)\n                        printf \"  Пропуск установки списков GeoIPSET\\n\\n\"\n\n                        if [ ! -f \"$ru_exclude_ipv4\" ] && [ ! -f \"$ru_exclude_ipv6\" ]; then\n                            bypass_cron_geoipset=true\n                        fi\n                        return 0\n                        ;;\n                    1)\n                        bypass_cron_geoipset=false\n                        break\n                        ;;\n                    *)\n                        printf \"  Неверный ввод. Пожалуйста, введите 1 или 0.\\n\"\n                        ;;\n                esac\n            done\n        fi\n    fi\n\n    local do_v4=0 do_v6=0\n    if ip -4 addr show 2>/dev/null | grep -q \"inet \" && command -v iptables >/dev/null 2>&1; then\n        if [ \"$action\" = \"init\" ] || [ -f \"$ru_exclude_ipv4\" ]; then\n            do_v4=1\n        fi\n    fi\n    if ip -6 addr show 2>/dev/null | grep -q \"inet6 fe80::\" && command -v ip6tables >/dev/null 2>&1; then\n        if [ \"$action\" = \"init\" ] || [ -f \"$ru_exclude_ipv6\" ]; then\n            do_v6=1\n        fi\n    fi\n\n    # Параллельная загрузка независимых списков\n    local _pids=\"\"\n    [ \"$do_v4\" = \"1\" ] && { install_geoipset_lst \"$geoipv4_url\" \"$ru_exclude_ipv4\" \"IPv4 (IPSet)\" \"ipv4\" & _pids=\"$_pids $!\"; }\n    [ \"$do_v6\" = \"1\" ] && { install_geoipset_lst \"$geoipv6_url\" \"$ru_exclude_ipv6\" \"IPv6 (IPSet)\" \"ipv6\" & _pids=\"$_pids $!\"; }\n    [ -n \"$_pids\" ] && wait $_pids\n\n    [ \"$do_v4\" = \"1\" ] && load_geoipset geo_exclude \"$ru_exclude_ipv4\" inet\n    [ \"$do_v6\" = \"1\" ] && load_geoipset geo_exclude6 \"$ru_exclude_ipv6\" inet6\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/06_install_cron.sh",
    "content": "# Функция для установки задач Cron\ninstall_cron() {\n    cron_entry=\n\n    # Добавление задачи Cron для обновления GeoFile\n    if [ -n \"$choice_geofile_cron_time\" ]; then\n        cron_entry=\"$choice_geofile_cron_time $install_dir/xkeen -ug\"\n    fi\n\n    # Если есть записи для задач Cron, то сохраняем их\n    if [ -n \"$cron_entry\" ] || [ -n \"$choice_cancel_cron_select\" ]; then\n        cron_file_path=\"$cron_dir/$cron_file\"\n\n        touch \"$cron_file_path\"\n        chmod +x \"$cron_file_path\"\n\n        if [ -n \"$cron_entry\" ]; then\n            grep -v \"$install_dir/xkeen -ug\" \"$cron_file_path\" > \"$cron_file_path.tmp\"\n            mv \"$cron_file_path.tmp\" \"$cron_file_path\"\n            printf \"%s\\n\" \"$cron_entry\" >> \"$cron_file_path\"\n        fi\n        sed -i '/^$/d' \"$cron_file_path\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/00_register_common.sh",
    "content": "# Общие функции для регистрации пакетов в opkg\n\nwrite_opkg_control() {\n    package_name=\"$1\"\n    package_version=\"$2\"\n    package_depends=\"$3\"\n    package_source=\"$4\"\n    package_source_name=\"$5\"\n    package_maintainer=\"$6\"\n    package_description=\"$7\"\n\n    _installed_size=$(du -s \"$install_dir\" | cut -f1)\n    _source_date_epoch=$(date +%s)\n\n    {\n        echo \"Package: $package_name\"\n        echo \"Version: $package_version\"\n        [ -n \"$package_depends\" ] && echo \"Depends: $package_depends\"\n        echo \"Source: $package_source\"\n        echo \"SourceName: $package_source_name\"\n        echo \"Section: net\"\n        echo \"SourceDateEpoch: $_source_date_epoch\"\n        echo \"Maintainer: $package_maintainer\"\n        echo \"Architecture: $status_architecture\"\n        echo \"Installed-Size: $_installed_size\"\n        echo \"Description: $package_description\"\n    } > \"$register_dir/$package_name.control\"\n}\n\nwrite_opkg_status() {\n    package_name=\"$1\"\n    package_version=\"$2\"\n    package_depends=\"$3\"\n    status_entry=\"$(mktemp)\"\n\n    {\n        echo \"Package: $package_name\"\n        echo \"Version: $package_version\"\n        [ -n \"$package_depends\" ] && echo \"Depends: $package_depends\"\n        echo \"Status: install user installed\"\n        echo \"Architecture: $status_architecture\"\n        echo \"Installed-Time: $(date +%s)\"\n    } > \"$status_entry\"\n\n    echo \"\" >> \"$status_file\"\n    cat \"$status_entry\" >> \"$status_file\"\n    echo \"\" >> \"$status_file\"\n    rm -f \"$status_entry\"\n    sed -i '/^$/{N;/^\\n$/D}' \"$status_file\"\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/00_register_import.sh",
    "content": "# Импорт модулей регистраций\n\t\n# Модули регистрации\n. \"$xinstall_dir/07_install_register/00_register_common.sh\"\n. \"$xinstall_dir/07_install_register/01_register_xray.sh\"\n. \"$xinstall_dir/07_install_register/01_register_mihomo.sh\"\n. \"$xinstall_dir/07_install_register/02_register_xkeen.sh\"\n. \"$xinstall_dir/07_install_register/03_register_cron.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/01_register_mihomo.sh",
    "content": "# Регистрация Mihomo\n\nregister_mihomo_list() {\n    cd \"$register_dir/\" || exit\n    touch mihomo_s.list\n    echo \"/opt/sbin/mihomo\" >> mihomo_s.list\n    echo \"/opt/etc/mihomo/config.yaml\" >> mihomo_s.list\n    echo \"/opt/etc/mihomo\" >> mihomo_s.list\n}\n\nregister_mihomo_control() {\n    write_opkg_control \\\n        \"mihomo_s\" \\\n        \"$mihomo_current_version\" \\\n        \"yq_s\" \\\n        \"MetaCubeX\" \\\n        \"mihomo_s\" \\\n        \"jameszero\" \\\n        \"A unified platform for anti-censorship.\"\n}\n\nregister_mihomo_status() {\n    write_opkg_status \\\n        \"mihomo_s\" \\\n        \"$mihomo_current_version\" \\\n        \"yq_s\"\n}\n\nregister_yq_list() {\n    cd \"$register_dir/\" || exit\n    touch yq_s.list\n    echo \"/opt/sbin/yq\" >> yq_s.list\n}\n\nregister_yq_control() {\n    write_opkg_control \\\n        \"yq_s\" \\\n        \"$yq_current_version\" \\\n        \"\" \\\n        \"mikefarah\" \\\n        \"yq_s\" \\\n        \"jameszero\" \\\n        \"A lightweight and portable command-line YAML, JSON, INI and XML processor.\"\n}\n\nregister_yq_status() {\n    write_opkg_status \\\n        \"yq_s\" \\\n        \"$yq_current_version\" \\\n        \"\"\n}\n\nadd_mihomo_config() {\n    if [ -f $install_dir/mihomo ]; then\n        if [ -f \"$mihomo_conf_dir/config.yaml\" ]; then\n            return 0\n        elif [ ! -d $mihomo_conf_dir ]; then\n            mkdir $mihomo_conf_dir\n        fi\n            cat << EOF > \"$mihomo_conf_dir/config.yaml\"\ntproxy-port: 1181\nredir-port: 1182\n# Руководство по конфигурации Mihomo - https://wiki.metacubex.one/ru/config/\nEOF\n\n        echo\n        echo \"  Добавлен шаблон конфигурационного файла Mihomo:\"\n        echo -e \"  ${yellow}config.yaml${reset}\"\n        sleep 2\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/01_register_xray.sh",
    "content": "# Регистрация xray\nregister_xray_control() {\n    write_opkg_control \\\n        \"xray_s\" \\\n        \"$xray_current_version\" \\\n        \"libc, libssp, librt, libpthread, ca-bundle\" \\\n        \"XTLS Team\" \\\n        \"xray_s\" \\\n        \"Skrill / jameszero\" \\\n        \"A unified platform for anti-censorship.\"\n}\n\nregister_xray_list() {\n    cd \"$register_dir/\" || exit\n    touch xray_s.list\n\n    # Генерация списка файлов\n    find /opt/etc/xray/dat -maxdepth 1 -name \"*.dat\" -type f | while read -r file; do\n        echo \"$file\" >> xray_s.list\n    done\n\n    find /opt/etc/xray/configs -maxdepth 1 -name \"*.json\" -type f | while read -r file; do\n        echo \"$file\" >> xray_s.list\n    done\n\n    find /opt/var/log/xray -maxdepth 1 -name \"*.log\" -type f | while read -r file; do\n        echo \"$file\" >> xray_s.list\n    done\n\n    # Добавление дополнительных путей\n    echo \"/opt/var/log/xray\" >> xray_s.list\n    echo \"/opt/etc/xray/configs\" >> xray_s.list\n    echo \"/opt/etc/xray/dat\" >> xray_s.list\n    echo \"/opt/etc/xray\" >> xray_s.list\n    echo \"/opt/sbin/xray\" >> xray_s.list\n}\n\nregister_xray_status() {\n    write_opkg_status \\\n        \"xray_s\" \\\n        \"$xray_current_version\" \\\n        \"libc, libssp, librt, libpthread, ca-bundle\"\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/02_register_xkeen.sh",
    "content": "# Регистрация XKeen\n\n# Функция для создания файла xkeen.control\nregister_xkeen_control() {\n    write_opkg_control \\\n        \"xkeen\" \\\n        \"$xkeen_current_version\" \\\n        \"jq, curl, coreutils-uname, coreutils-nohup, iptables, ipset\" \\\n        \"Skrill\" \\\n        \"xkeen\" \\\n        \"Skrill / jameszero\" \\\n        \"The platform that makes Xray work.\"\n}\n\nregister_xkeen_list() {\n    cd \"$register_dir/\" || exit\n\n    # Создание файла xkeen.list\n    touch xkeen.list\n\n    # Генерация списка файлов и директорий\n    find \"$xkeen_dir\" -mindepth 1 | while read -r entry; do\n        echo \"$entry\" >> xkeen.list\n    done\n\n    # Добавление дополнительных путей\n    echo \"$install_dir/xkeen\" >> xkeen.list\n    echo \"$xkeen_dir\" >> xkeen.list\n    echo \"$initd_file\" >> xkeen.list\n    echo \"$log_dir/xkeen-detached.log\" >> xkeen.list\n}\n\nregister_xkeen_status() {\n    write_opkg_status \\\n        \"xkeen\" \\\n        \"$xkeen_current_version\" \\\n        \"jq, curl, coreutils-uname, coreutils-nohup, iptables, ipset\"\n}\n\n\nfixed_register_packages() {\n\tawk 'BEGIN {RS=\"\"; ORS=\"\\n\\n\"} {gsub(/\\n\\n+/,\"\\n\\n\")}1' \"$status_file\" > tmp_status_file && mv tmp_status_file \"$status_file\"\n}\n\nregister_xkeen_initd() {\n    old_initd_file=\"${initd_dir}/S24xray\"\n    pre_initd_file=\"${initd_dir}/S99xkeen\"\n    old_start_file=\"${initd_dir}/S99xkeenstart\"\n    script_file=\"${xinstall_dir}/07_install_register/04_register_init.sh\" \n    current_datetime=$(date \"+%Y-%m-%d_%H-%M-%S\")\n    variables_to_extract=\"name_client name_policy table_id table_mark custom_mark dscp_exclude dscp_proxy ipv4_proxy ipv4_exclude ipv6_proxy ipv6_exclude proxy_dns proxy_router start_attempts check_fd arm64_fd other_fd delay_fd ipv6_support extended_msg backup aghfix\"\n    source_main_backup=\"\"\n    source_start_backup=\"\"\n\n    if [ -f \"$initd_file\" ]; then\n        source_main_backup=\"${backups_dir}/${current_datetime}_$(basename \"$initd_file\")\"\n        mv \"$initd_file\" \"$source_main_backup\"\n    elif [ -f \"$pre_initd_file\" ]; then\n        source_main_backup=\"${backups_dir}/${current_datetime}_$(basename \"$pre_initd_file\")\"\n        mv \"$pre_initd_file\" \"$source_main_backup\"\n    elif [ -f \"$old_initd_file\" ] || [ -f \"$old_start_file\" ]; then\n        if [ -f \"$old_initd_file\" ]; then\n            source_main_backup=\"${backups_dir}/${current_datetime}_$(basename \"$old_initd_file\")\"\n            mv \"$old_initd_file\" \"$source_main_backup\"\n        fi\n        if [ -f \"$old_start_file\" ]; then\n            source_start_backup=\"${backups_dir}/${current_datetime}_$(basename \"$old_start_file\")\"\n            mv \"$old_start_file\" \"$source_start_backup\"\n        fi\n    fi\n\n    cp \"$script_file\" \"$initd_file\" || exit 1\n\n    if [ -n \"$source_main_backup\" ] || [ -n \"$source_start_backup\" ]; then\n        autostart_val=\"\"\n        start_delay_val=\"\"\n\n        if [ -n \"$source_start_backup\" ] && [ -f \"$source_start_backup\" ]; then\n            autostart_val=$(grep '^autostart=' \"$source_start_backup\" | head -n 1 | cut -d'=' -f2)\n            start_delay_val=$(grep '^start_delay=' \"$source_start_backup\" | head -n 1 | cut -d'=' -f2)\n        fi\n\n        if [ -n \"$source_main_backup\" ] && [ -f \"$source_main_backup\" ]; then\n            [ -z \"$autostart_val\" ] && autostart_val=$(grep '^start_auto=' \"$source_main_backup\" | head -n 1 | cut -d'=' -f2)\n            [ -z \"$start_delay_val\" ] && start_delay_val=$(grep '^start_delay=' \"$source_main_backup\" | head -n 1 | cut -d'=' -f2)\n        fi\n\n        if [ -n \"$autostart_val\" ]; then\n             sed -i \"s|^start_auto=.*|start_auto=$autostart_val|\" \"$initd_file\"\n        fi\n        if [ -n \"$start_delay_val\" ]; then\n             sed -i \"s|^start_delay=.*|start_delay=$start_delay_val|\" \"$initd_file\"\n        fi\n\n        if [ -n \"$source_main_backup\" ] && [ -f \"$source_main_backup\" ]; then\n            for var in $variables_to_extract; do\n                value=$(grep -m1 \"^${var}=\" \"$source_main_backup\") || continue\n                escaped_value=$(printf '%s\\n' \"$value\" | sed 's:[&#/]:\\\\&:g')\n                position=$(grep -n \"^${var}=\" \"$initd_file\" | head -n 1 | cut -d: -f1)\n                [ -n \"$position\" ] && sed -i \"${position}s#.*#${escaped_value}#\" \"$initd_file\"\n            done\n        fi\n    fi\n\n    chmod +x \"$initd_file\"\n    if choice_backup_xkeen; then\n        rm -f \"$source_main_backup\" \"$source_start_backup\"\n    fi\n    # Пропущенный $ ломал очистку легаси S99xkeenstart при апгрейде с 1.x\n    rm -f \"$old_initd_file\" \"$old_start_file\" \"$pre_initd_file\"\n}\n\n# Миграция скрипта\nregister_xray_initd() {\n    register_xkeen_initd\n}\nregister_autostart() {\n    :\n}\n\n# Создание конфигурации XKeen\ncreate_xkeen_cfg() {\n    mkdir -p \"$xkeen_cfg\" || { echo \"Ошибка: Не удалось создать директорию $xkeen_cfg\"; exit 1; }\n    if [ -f \"/opt/etc/xkeen_exclude.lst\" ] && [ ! -f \"$file_ip_exclude\" ]; then\n        mv \"/opt/etc/xkeen_exclude.lst\" \"$file_ip_exclude\"\n    elif [ ! -f \"$file_ip_exclude\" ]; then\n        cat << EOF > \"$file_ip_exclude\"\n#192.168.0.0/16\n#2001:db8::/32\n\n# Добавьте необходимые IP и подсети без комментария # для исключения их из проксирования\nEOF\n    fi\n\n    if [ ! -f \"$file_port_exclude\" ]; then\n        cat << EOF > \"$file_port_exclude\"\n#\n\n# Одновременно использовать порты проксирования и исключать порты нельзя\n# Приоритет у портов проксирования\nEOF\n    fi\n\n    if [ ! -f \"$file_port_proxying\" ]; then\n        cat << EOF > \"$file_port_proxying\"\n#80\n#443\n#596:599\n\n# (Раскомментируйте/добавьте по образцу) единичные порты и диапазоны для проскирования\nEOF\n    fi\n    if [ ! -f \"$xkeen_config\" ]; then\n        cat << EOF > \"$xkeen_config\"\n{\n}\nEOF\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/03_register_cron.sh",
    "content": "# Функция для регистрации инициализационного скрипта cron\nregister_cron_initd() {\n    # Проверка наличия пакета cron\n    opkg list-installed 2>/dev/null | grep -q \"^cron \" && return\n\n    # Определение переменных\n    s05crond_filename=\"${current_datetime}_S05crond\"\n    required_script_version=\"0.6\"\n\n    # Получение текущей версии скрипта\n    if [ -e \"${initd_cron}\" ]; then\n        script_version=$(grep 'version=' \"${initd_cron}\" | grep -o '[0-9.]\\+')\n    fi\n\n    # Содержимое скрипта\n    script_content='#!/bin/sh\n\n# Информация о службе: Запуск / Остановка Cron\n# version=\"0.6\"\n\ngreen=\"\\\\033[32m\"\nred=\"\\\\033[31m\"\nyellow=\"\\\\033[33m\"\nreset=\"\\\\033[0m\" \n\ncron_initd=\"/opt/sbin/crond\"\n\n# Функция для проверки статуса cron\ncron_status() {\n    if pidof crond > /dev/null; then\n        return 0 # Процесс существует и работает\n    else\n        return 1 # Процесс не существует\n    fi\n}\n\n# Функция для запуска cron\nstart() {\n    if cron_status; then\n        printf \"  Cron ${yellow}уже запущен${reset}\\\\n\"\n    else\n        $cron_initd -L /dev/null\n        printf \"  Cron ${green}запущен${reset}\\\\n\"\n    fi\n}\n\n# Функция для остановки cron\nstop() {\n    if cron_status; then\n        killall crond\n        printf \"  Cron ${yellow}остановлен${reset}\\\\n\"\n    else\n        printf \"  Cron ${red}не запущен${reset}\\\\n\"\n    fi\n}\n\n# Функция для перезапуска cron\nrestart() {\n    stop > /dev/null 2>&1\n    sleep 1\n    start > /dev/null 2>&1\n    printf \"  Cron ${green}перезапущен${reset}\\\\n\"\n}\n\n# Обработка аргументов командной строки\ncase \"$1\" in\n    start)\n        start\n        ;;\n    stop)\n        stop\n        ;;\n    restart)\n        restart\n        ;;\n    status)\n        if cron_status; then\n            printf \"  Cron ${green}запущен${reset}\\\\n\"\n        else\n            printf \"  Cron ${red}не запущен${reset}\\\\n\"\n        fi\n        ;;\n    *)\n        printf \"  Команды: ${green}start${reset} | ${red}stop${reset} | ${yellow}restart${reset} | status\\\\n\"\n        ;;\nesac\n\nexit 0'\n    \n    # Создание или замена файла, если версия скрипта не соответствует требуемой версии \n    if [ \"${script_version}\" != \"${required_script_version}\" ]; then \n        echo -e \"${script_content}\" > \"${initd_cron}\" \n        chmod +x \"${initd_cron}\" \n    fi \n}\n\n# Обновление cron задач\nupdate_cron_geofile_task() {\n    if [ -f \"$cron_dir/$cron_file\" ]; then\n        tmp_file=\"$cron_dir/${cron_file}.tmp\"\n        cp \"$cron_dir/$cron_file\" \"$tmp_file\"\n        \n        if [ -z \"$choice_cancel_cron_select\" ]; then\n            grep -v -e \"ug\" -e \"ux\" -e \"uk\" -e '^\\s*$' \"$tmp_file\" > \"$cron_dir/$cron_file\"\n        else\n            grep -v -e \"ugi\" -e \"ugs\" -e \"ux\" -e \"uk\" -e '^\\s*$' \"$tmp_file\" > \"$cron_dir/$cron_file\"\n        fi\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/04_register_init.sh",
    "content": "#!/bin/sh\n\n# Информация о службе: Запуск / Остановка XKeen\n# Версия: 2.30\n\n# Окружение\nPATH=\"/opt/bin:/opt/sbin:/sbin:/bin:/usr/sbin:/usr/bin\"\n\n# Цвета\ngreen=\"\\033[92m\"\nred=\"\\033[91m\"\nyellow=\"\\033[93m\"\nlight_blue=\"\\033[96m\"\nreset=\"\\033[0m\"\n\n# Имена\nname_client=\"xray\"\nname_app=\"XKeen\"\nname_policy=\"xkeen\"\nname_profile=\"xkeen\"\nname_chain=\"xkeen\"\nname_ipset_deny_mac=\"xkeen_deny_mac\"\n\n# Директории\ndirectory_os_modules=\"/lib/modules/$(uname -r)\"\ndirectory_user_modules=\"/opt/lib/modules\"\ndirectory_configs_app=\"/opt/etc/$name_client\"\ndirectory_xray_config=\"$directory_configs_app/configs\"\ndirectory_xray_asset=\"$directory_configs_app/dat\"\ndirectory_logs=\"/opt/var/log\"\nxkeen_cfg=\"/opt/etc/xkeen\"\nipset_cfg=\"$xkeen_cfg/ipset\"\ninstall_dir=\"/opt/sbin\"\n\n# Файлы\nfile_netfilter_hook=\"/opt/etc/ndm/netfilter.d/proxy.sh\"\nfile_schedule_hook=\"/opt/etc/ndm/schedule.d/00-xkeen-hotspot-sync.sh\"\nlog_access=\"$directory_logs/$name_client/access.log\"\nlog_error=\"$directory_logs/$name_client/error.log\"\nmihomo_config=\"$directory_configs_app/config.yaml\"\nfile_port_proxying=\"$xkeen_cfg/port_proxying.lst\"\nfile_port_exclude=\"$xkeen_cfg/port_exclude.lst\"\nfile_ip_exclude=\"$xkeen_cfg/ip_exclude.lst\"\nxkeen_config=\"$xkeen_cfg/xkeen.json\"\nfile_pid_fd=\"/var/run/xkeen_fd.pid\"\nru_exclude_ipv4=\"$ipset_cfg/ru_exclude_ipv4.lst\"\nru_exclude_ipv6=\"$ipset_cfg/ru_exclude_ipv6.lst\"\n\n# URL\nurl_server=\"localhost:79\"\nurl_policy=\"rci/show/ip/policy\"\nurl_keenetic_port=\"rci/ip/http\"\nurl_redirect_port=\"rci/ip/static\"\nurl_hotspot=\"rci/show/ip/hotspot\"\n\n# Настройки правил iptables\ntable_id=\"111\"\ntable_mark=\"0x111\"\ntable_redirect=\"nat\"\ntable_tproxy=\"mangle\"\ncomment_tag=\"xkeen_rule\"\ncomment=\"-m comment --comment $comment_tag\"\ncustom_mark=\"\"\n\n# DSCP-метки\ndscp_exclude=\"62\"\ndscp_proxy=\"63\"\n\nipv4_proxy=\"127.0.0.1\"\nipv4_exclude=\"0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 255.255.255.255\"\nipv6_proxy=\"::1\"\nipv6_exclude=\"::/128 ::1/128 64:ff9b::/96 2001::/32 2002::/16 fd00::/8 ff00::/8 fe80::/10\"\n\n# Перехват DNS в прокси\nproxy_dns=\"off\"\n\n# Проксирование трафика Entware\nproxy_router=\"off\"\n\n# Настройки запуска\nstart_attempts=10\nstart_auto=\"on\"\nstart_delay=20\n\n# Контроль файловых дескрипторов\ncheck_fd=\"off\"\narm64_fd=40000\nother_fd=10000\ndelay_fd=60\n\n# Поддержка IPv6\nipv6_support=\"on\"\n\n## Расширенные сообщения запуска\nextended_msg=\"off\"\n\n## Резервное копирование XKeen при обновлении\nbackup=\"on\"\n\n## Клиенты XKeen под своими IP в журнале AdGuard Home\naghfix=\"off\"\n\n# Функции журналирования\nlog_info_router() { logger -p notice -t \"$name_app\" \"$1\"; }\nlog_warning_router() { logger -p warning -t \"$name_app\" \"$1\"; }\nlog_error_router() { logger -p error -t \"$name_app\" \"$1\"; }\n\nlog_info_terminal() { echo -e \"\\n${green}Информация${reset}: $1\" >&2; }\nlog_warning_terminal() { echo -e \"\\n${yellow}Предупреждение${reset}: $1\" >&2; }\nlog_error_terminal() { echo -e \"\\n${red}Ошибка${reset}: $1\" >&2; exit 1; }\n\nprint_policy_info() {\n    found=\"$1\"\n    has_custom=\"$2\"\n    ignored_custom=\"$3\"\n\n    ignore_line=\"\"\n    if [ \"$ignored_custom\" = \"yes\" ]; then\n        ignore_line=\"\n  Пользовательские политики из '${yellow}xkeen.json${reset}' будут проигнорированы\"\n    fi\n\n    if [ \"$extended_msg\" != \"on\" ]; then\n        if [ \"$found\" = \"no\" ]; then\n            log_info_terminal \"\n  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}\n  Прокси будет запущен для всего устройства\n\"\n        fi\n        return\n    fi\n\n    if [ \"$found\" = \"yes\" ]; then\n\n        if [ \"$has_custom\" = \"yes\" ]; then\n            custom_names=$(echo \"$user_policies\" | cut -d'|' -f1 | tr '\\n' ',' | sed 's/,$//; s/,/, /g')\n            policies=\"${name_policy}, ${custom_names}\"\n\n            detail_list=\"\"\n            if [ -n \"$port_donor\" ]; then\n                detail_list=\"  - ${yellow}$name_policy${reset} на портах ${green}${port_donor}${reset}\"\n            elif [ -n \"$port_exclude\" ]; then\n                detail_list=\"  - ${yellow}$name_policy${reset} на всех портах кроме ${green}${port_exclude}${reset}\"\n            else\n                detail_list=\"  - ${yellow}$name_policy${reset} на всех портах\"\n            fi\n\n            custom_details=$(echo \"$user_policies\" | while IFS='|' read -r p_name p_mark p_mode p_ports; do\n                if [ \"$p_mode\" = \"include\" ]; then\n                    echo \"  - ${yellow}$p_name${reset} на портах ${green}${p_ports}${reset}\"\n                elif [ \"$p_mode\" = \"exclude\" ]; then\n                    echo \"  - ${yellow}$p_name${reset} на всех портах кроме ${green}${p_ports}${reset}\"\n                else\n                    echo \"  - ${yellow}$p_name${reset} на всех портах\"\n                fi\n            done)\n\n            log_info_terminal \"\n  Найдены политики '${yellow}${policies}${reset}'\n  Прокси будет запущен для клиентов политик:\n${detail_list}\n${custom_details}\n\"\n        else\n            if [ -z \"$port_donor\" ] && [ -z \"$port_exclude\" ]; then\n                log_info_terminal \"\n  Найдена политика '${yellow}$name_policy${reset}'\n  Не определены целевые порты для XKeen\n  Прокси будет запущен для клиентов политики '${yellow}$name_policy${reset}' на всех портах\n\"\n            elif [ -n \"$port_donor\" ]; then\n                log_info_terminal \"\n  Найдена политика '${yellow}$name_policy${reset}'\n  Определены целевые порты для XKeen\n  Прокси будет запущен для клиентов политики '${yellow}$name_policy${reset}'\n  на портах ${green}${port_donor}${reset}\n\"\n            else\n                log_info_terminal \"\n  Найдена политика '${yellow}$name_policy${reset}'\n  Определены порты исключения для XKeen\n  Прокси будет запущен для клиентов политики '${yellow}$name_policy${reset}'\n  на всех портах кроме ${green}${port_exclude}${reset}\n\"\n            fi\n        fi\n    else\n        if [ -n \"$port_donor\" ]; then\n            log_info_terminal \"\n  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}\n  Определены целевые порты для XKeen\n  Прокси будет запущен для всех клиентов\n  на портах ${green}${port_donor}${reset}\n\"\n        elif [ -n \"$port_exclude\" ]; then\n            log_info_terminal \"\n  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}\n  Определены порты исключения для XKeen\n  Прокси будет запущен для всех клиентов\n  на всех портах кроме ${green}${port_exclude}${reset}\n\"\n        else\n            log_info_terminal \"\n  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}\n  Не определены целевые порты для XKeen\n  Прокси будет запущен для всех клиентов на всех портах\n\"\n        fi\n    fi\n}\n\nutils=\"jq curl grep awk sed ipset\"\n[ \"$name_client\" = \"mihomo\" ] && utils=\"$utils yq\"\nfor cmd in $utils; do\n    command -v \"$cmd\" >/dev/null 2>&1 || log_error_terminal \"Не найдена необходимая утилита: ${yellow}$cmd${reset}\"\ndone\n\nlog_clean() { [ \"$name_client\" = \"xray\" ] && : > \"$log_access\" && : > \"$log_error\"; }\n\napi_cache_init() {\n    api_policy_json=$(curl -kfsS \"${url_server}/${url_policy}\" 2>/dev/null)\n    api_port_json=$(curl -kfsS \"${url_server}/${url_keenetic_port}\" 2>/dev/null)\n    api_static_json=$(curl -kfsS \"${url_server}/${url_redirect_port}\" 2>/dev/null)\n}\n\nrefresh_port_cache() { api_port_json=$(curl -kfsS \"${url_server}/${url_keenetic_port}\" 2>/dev/null); }\n\njson_get_ports() { [ -n \"$api_port_json\" ] && printf '%s' \"$api_port_json\" | jq -r '.port, (.ssl.port // empty)' 2>/dev/null; }\n\n# Получение портов Keenetic\nget_keenetic_port() {\n    ports=\"\"\n    ports=$(json_get_ports)\n\n    case \" $ports \" in\n        *\" 443 \"*) return 1 ;;\n    esac\n\n    if [ -z \"$ports\" ]; then\n        ndmc -c 'ip http port 8080' >/dev/null 2>&1\n        ndmc -c 'ip http port 80' >/dev/null 2>&1\n        ndmc -c 'system configuration save' >/dev/null 2>&1\n        sleep 2\n        refresh_port_cache\n        ports=$(json_get_ports)\n    fi\n\n    [ -n \"$ports\" ] || return 1\n\n    echo \"$ports\"\n    return 0\n}\n\nwait_for_webui() {\n    max_wait=10\n    i=0\n\n    while [ \"$i\" -lt \"$max_wait\" ]; do\n        pidof nginx >/dev/null 2>&1 && return 0\n        sleep 1\n        i=$((i + 1))\n    done\n\n    return 1\n}\n\napply_ipv6_state() {\n    ipv6_disabled=\n    ipv6_disabled=$(sysctl -n net.ipv6.conf.default.disable_ipv6 2>/dev/null || echo \"0\")\n\n    [ \"$ipv6_disabled\" -eq 1 ] && return 0\n\n    [ \"$ipv6_support\" != \"off\" ] && return 0\n\n    ip -6 addr show 2>/dev/null | grep -q \"inet6 fe80::\" || return 0\n\n    wait_for_webui || { log_error_router \"Веб-интерфейс недоступен\"; return 1; }\n\n    sleep 5\n\n    sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1\n\n    for dir in /proc/sys/net/ipv6/conf/*; do\n        [ -d \"$dir\" ] || continue\n        iface=\"${dir##*/}\"\n\n        case \"$iface\" in\n            all|ezcfg0|t2s*)\n                continue\n                ;;\n            *)\n                [ -f \"$dir/disable_ipv6\" ] && echo \"1\" > \"$dir/disable_ipv6\" 2>/dev/null\n                ;;\n        esac\n    done\n\n    sleep 2\n\n    if [ \"$(sysctl -n net.ipv6.conf.default.disable_ipv6 2>/dev/null)\" -eq 1 ]; then\n        log_info_router \"Отключение IPv6 выполнено\"\n        return 0\n    fi\n}\n\nget_ipver_support() {\n    ip4_supported=$(ip -4 addr show 2>/dev/null | grep -q \"inet \" && echo true || echo false)\n    ip6_supported=$(ip -6 addr show 2>/dev/null | grep -q \"inet6 fe80::\" && echo true || echo false)\n\n    iptables_supported=$([ \"$ip4_supported\" = \"true\" ] && command -v iptables >/dev/null 2>&1 && echo true || echo false)\n    ip6tables_supported=$([ \"$ip6_supported\" = \"true\" ] && command -v ip6tables >/dev/null 2>&1 && echo true || echo false)\n}\n\nstrip_json_comments() {\n    sed -e ':a; s:/\\*[^*]*\\*[^/]*\\*/::g; ta' \\\n        -e 's/^[[:space:]]*\\/\\/.*$//' \\\n        -e 's/[[:space:]]\\{1,\\}\\/\\/.*$//' \"$@\"\n}\n\n# Функция валидации xkeen.json\nvalidate_xkeen_json() {\n    [ ! -f \"$xkeen_config\" ] && return 0\n    if ! jq -e . \"$xkeen_config\" >/dev/null 2>&1; then\n            log_error_terminal \"\n  Валидация JSON: файл '${yellow}xkeen.json${reset}' содержит синтаксические ошибки\n  Запуск прокси невозможен\n\"\n    fi\n\n    if ! jq -e '.xkeen.policy[]? | .name' \"$xkeen_config\" >/dev/null 2>&1; then\n        if jq -e '.xkeen' \"$xkeen_config\" >/dev/null 2>&1; then\n            log_error_terminal \"\n  Файл '${yellow}xkeen.json${reset}' имеет неверную структуру\n  Запуск прокси невозможен\n\"\n        fi\n    fi\n\n    return 0\n}\n\n# Функция поиска резервных копий конфигурационных файлов Xray\ncheck_xray_backups() {\n    [ \"$name_client\" != \"xray\" ] && return 0\n\n    # Ищем json-файлы с типичными признаками копий\n    bad_files=$(find \"$directory_xray_config\" -maxdepth 1 -type f \\( -iname \"*bak*.json\" -o -iname \"*old*.json\" -o -iname \"*copy*.json\" -o -iname \"*копия*.json\" -o -iname \"*orig*.json\" -o -iname \"*save*.json\" -o -iname \"*temp*.json\" -o -iname \"*tmp*.json\" -o -name \"*(*).json\" \\))\n\n    if [ -n \"$bad_files\" ]; then\n        bad_list=$(printf '%s\\n' \"$bad_files\" | awk -F/ '{print \"  - \" $NF}')\n        \n        log_error_terminal \"\n  В директории конфигурации Xray найдены резервные копии:\n${light_blue}${bad_list}${reset}\n\n  Измените расширение резервных копий, например, на ${yellow}.bak${reset}\n  Либо переместите их в поддиректорию\n  Запуск ${yellow}$name_client${reset} ${red}отменен${reset}\n\"\n    fi\n    return 0\n}\n\n# Функция проверки наличия метки 255\nvalidate_routing_mark() {\n    [ \"$proxy_router\" != \"on\" ] && return 0\n\n    mark_valid=\"false\"\n    mark_msg=\"\"\n    bad_items=\"\"\n    has_items=\"false\"\n    all_marks_ok=\"true\"\n\n    if [ \"$name_client\" = \"xray\" ]; then\n        mark_msg=\"mark\"\n\n        for file in \"$directory_xray_config\"/*.json; do\n            [ -f \"$file\" ] || continue\n\n            if strip_json_comments \"$file\" | jq -e '.outbounds != null' >/dev/null 2>&1; then\n                has_items=\"true\"\n\n                current_bad=$(strip_json_comments \"$file\" | jq -r '\n                    .outbounds[]? |\n                    select(.protocol != \"blackhole\" and .protocol != \"dns\") |\n                    select(.streamSettings.sockopt.mark != 255) |\n                    (.tag // .protocol)\n                ')\n\n                if [ -n \"$current_bad\" ]; then\n                     bad_items=\"${bad_items}${bad_items:+\\n}$current_bad\"\n                    all_marks_ok=\"false\"\n                fi\n            fi\n        done\n\n    elif [ \"$name_client\" = \"mihomo\" ]; then\n        mark_msg=\"routing-mark\"\n\n        if [ -f \"$mihomo_config\" ]; then\n\n            if yq -e '.[\"routing-mark\"] == 255' \"$mihomo_config\" >/dev/null 2>&1; then\n                mark_valid=\"true\"\n            elif yq -e '\n                .proxy-providers[]? |\n                select(.override.\"routing-mark\" == 255)\n            ' \"$mihomo_config\" >/dev/null 2>&1; then\n                mark_valid=\"true\"\n            else\n\n                if yq -e '.proxies != null' \"$mihomo_config\" >/dev/null 2>&1; then\n                    has_items=\"true\"\n                    current_bad=$(yq -r '\n                        .proxies[]? |\n                        select(.\"routing-mark\" != 255) |\n                        .name\n                    ' \"$mihomo_config\")\n\n                    if [ -n \"$current_bad\" ]; then\n                        bad_items=\"${bad_items}${bad_items:+\\n}$current_bad\"\n                        all_marks_ok=\"false\"\n                    fi\n                fi\n            fi\n        fi\n    fi\n\n    if [ \"$mark_valid\" != \"true\" ]; then\n        if [ \"$has_items\" = \"true\" ] && [ \"$all_marks_ok\" = \"true\" ]; then\n            mark_valid=\"true\"\n        fi\n    fi\n\n    if [ \"$mark_valid\" != \"true\" ]; then\n        error_details=\"\"\n\n        if [ -n \"$bad_items\" ]; then\n            bad_list=$(printf \"%b\\n\" \"$bad_items\" | awk '!seen[$0]++ {print \"  - \" $0}')\n\n            if [ \"$name_client\" = \"xray\" ]; then\n                error_details=\"\n  Подключения без метки:\n${light_blue}${bad_list}${reset}\"\n                proxy_hint=\"  Добавьте маркировку во ВСЕ исходящие подключения (кроме blackhole и dns)\"\n            else\n                error_details=\"\n  Прокси без метки:\n${light_blue}${bad_list}${reset}\"\n                proxy_hint=\"  Добавьте в config.yaml маркировку трафика глобально либо в каждое исходящее подключение\"\n            fi\n        fi\n\n        log_warning_terminal \"\n  Для проксирования трафика Entware требуется его маркировка\n  В конфигурации ${yellow}$name_client${reset} параметр ${green}$mark_msg: 255${reset} прописан не везде$error_details\n\n$proxy_hint\n\n  Проксирование трафика Entware ${red}отключено${reset}\n\"\n        proxy_router=\"off\"\n    fi\n\n    return 0\n}\n\nload_user_ipset_family() {\n    set_name=\"$1\"\n    family=\"$2\"\n    addr_regex=\"$3\"\n    tmp=\"${set_name}_tmp\"\n\n    # Заполняем tmp; основной набор подменяется только после успешного pipeline\n    ipset create \"$set_name\" hash:net family \"$family\" -exist\n    ipset create \"$tmp\" hash:net family \"$family\" -exist\n    ipset flush \"$tmp\"\n\n    if sed -e 's/\\r$//' -e 's/#.*//' -e '/^[[:space:]]*$/d' \"$file_ip_exclude\" |\n       grep -Eo \"$addr_regex\" |\n       awk -v s=\"$tmp\" '{print \"add \"s\" \"$1}' | ipset restore -exist; then\n        ipset swap \"$set_name\" \"$tmp\"\n    fi\n    ipset destroy \"$tmp\"\n}\n\n# Функция загрузки пользовательских исключений в ipset\nload_user_ipset() {\n    [ ! -f \"$file_ip_exclude\" ] && return\n    [ \"$iptables_supported\" = \"true\" ] && load_user_ipset_family user_exclude inet '([0-9]{1,3}\\.){3}[0-9]{1,3}(/[0-9]{1,2})?'\n    [ \"$ip6tables_supported\" = \"true\" ] && load_user_ipset_family user_exclude6 inet6 '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/[0-9]{1,3})?'\n}\n\n# Функция чтения пользовательских портов из файлов\nread_ports_from_file() {\n    file_ports=\"$1\"\n    [ -f \"$file_ports\" ] || return\n\n    sed -e 's/\\r$//' -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' \"$file_ports\"\n}\n\n# Функция обработки, валидации и нормализации списка портов\nvalidate_and_clean_ports() {\n    input_ports=\"$1\"\n    mandatory_ports=\"$2\"\n    [ -z \"$input_ports\" ] && [ -z \"$mandatory_ports\" ] && return 1\n\n    echo \"${mandatory_ports}${mandatory_ports:+,}${input_ports}\" | tr ',' '\\n' | awk '\n        function is_valid(p) {\n            return p ~ /^[0-9]+$/ && p > 0 && p <= 65535\n        }\n        {\n            gsub(/[[:space:]]/, \"\", $0)\n            gsub(/-/, \":\", $0)\n            if ($0 == \"\") next\n\n            n = split($0, a, \":\")\n\n            if (n == 1) {\n                if (is_valid(a[1])) {\n                    print a[1]\n                }\n            }\n\n            else if (n == 2) {\n                if (is_valid(a[1]) && is_valid(a[2])) {\n                    start = a[1]\n                    end   = a[2]\n\n                    if (start > end) {\n                        tmp = start\n                        start = end\n                        end = tmp\n                    }\n\n                    if (start <= end) {\n                        print start \":\" end\n                    }\n                }\n            }\n        }\n    ' | sort -n -u | tr '\\n' ',' | sed 's/,$//'\n}\n\n# Функция обработки пользовательских портов\nprocess_user_ports() {\n    raw_donor=$(read_ports_from_file \"$file_port_proxying\")\n    [ -n \"$raw_donor\" ] && port_donor=$(validate_and_clean_ports \"$raw_donor\" \"80,443\") || port_donor=\"\"\n    port_exclude=$(validate_and_clean_ports \"$(read_ports_from_file \"$file_port_exclude\")\")\n\n    if [ -n \"$port_donor\" ] && [ -n \"$port_exclude\" ]; then\n        log_warning_terminal \"\n  Заданы и порты проксирования, и порты исключения\n  Прокси будет запущен на портах проксирования, порты исключения игнорируются\n\"\n        port_exclude=\"\"\n    fi\n}\n\n# Функция нормализации сторонних политик\nprocess_custom_mark() {\n    [ -z \"$custom_mark\" ] && return\n\n    clean_mark=\"\"\n    for mark in $(echo \"$custom_mark\" | tr ',' ' '); do\n        val=\"${mark#0x}\"\n        echo \"$val\" | grep -Eq '^[0-9a-fA-F]+$' && clean_mark=\"$clean_mark 0x$val\"\n    done\n\n    custom_mark=\"${clean_mark# }\"\n}\n\n# Проверка статуса прокси-клиента\nproxy_status() { pidof \"$name_client\" >/dev/null; }\n\n# Поиск конфигураций DNS\ncheck_dns_config() {\n    [ \"$proxy_dns\" != \"on\" ] && echo \"false\" && return\n\n    if [ \"$name_client\" = \"xray\" ]; then\n        for file in \"$directory_xray_config\"/*.json; do\n            [ -f \"$file\" ] || continue\n            strip_json_comments \"$file\" | jq -e '.dns.servers? != null' >/dev/null 2>&1 && { echo \"true\"; return; }\n        done\n    elif [ \"$name_client\" = \"mihomo\" ]; then\n        [ -f \"$mihomo_config\" ] && yq -e '.dns.enable == true' \"$mihomo_config\" >/dev/null 2>&1 && { echo \"true\"; return; }\n    fi\n\n    echo \"false\"\n}\nfile_dns=$(check_dns_config)\n\n# Кэш списка загруженных модулей; is_module_loaded читает его без форков\n_loaded_modules=\"\"\n_refresh_modules_cache() { _loaded_modules=\" $(lsmod 2>/dev/null | awk '{print $1}' | tr '\\n' ' ') \"; }\n\nis_module_loaded() {\n    case \"$_loaded_modules\" in\n        *\" $1 \"*) return 0 ;;\n        *) return 1 ;;\n    esac\n}\n\n# Загрузка модулей\nload_modules() {\n    name=\"${1%.ko}\"\n    if ! is_module_loaded \"$name\"; then\n        for dir in \"$directory_os_modules\" \"$directory_user_modules\"; do\n            [ -f \"$dir/$1\" ] && insmod \"$dir/$1\" >/dev/null 2>&1 && return\n        done\n    fi\n}\n\n# Обработка модулей и портов\nget_modules() {\n    _refresh_modules_cache\n    load_modules xt_comment.ko\n    load_modules xt_TPROXY.ko\n    load_modules xt_socket.ko\n    load_modules xt_multiport.ko\n    load_modules xt_dscp.ko\n    _refresh_modules_cache  # подхватить только что insmod-нутые модули\n\n    if ! is_module_loaded xt_comment; then\n        log_error_router \"Модуль xt_comment не загружен\"\n        log_error_terminal \"\n  Модуль '${light_blue}xt_comment${reset}' не загружен\n  Невозможно запустить XKeen без него\n  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'\n\"\n    fi\n\n    if [ \"$mode_proxy\" = \"TProxy\" ] || [ \"$mode_proxy\" = \"Hybrid\" ]; then\n        for module in xt_TPROXY.ko xt_socket.ko; do\n            if ! is_module_loaded \"${module%.ko}\"; then\n                proxy_stop\n                log_error_router \"Модуль ${module} не загружен\"\n                log_error_terminal \"\n  Модуль '${light_blue}${module}${reset}' не загружен\n  Невозможно запустить XKeen в режиме ${mode_proxy} без него\n  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'\n\"\n            fi\n        done\n    fi\n\n    if [ -n \"$port_donor\" ] || [ -n \"$port_exclude\" ]; then\n        if ! is_module_loaded xt_multiport; then\n            log_warning_router \"Модуль xt_multiport не загружен\"\n            log_warning_terminal \"\n  Модуль '${light_blue}xt_multiport${reset}' не загружен\n  Невозможно использовать выбранные порты без него\n  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'\n\n  Прокси будет запущен на всех портах\n\"\n            port_donor=\"\"\n            port_exclude=\"\"\n        fi\n    fi\n\n    if [ -n \"$dscp_exclude\" ] || [ -n \"$dscp_proxy\" ]; then\n        if ! is_module_loaded xt_dscp; then\n            log_warning_router \"Модуль xt_dscp не загружен\"\n            log_warning_terminal \"\n  Модуль '${light_blue}xt_dscp${reset}' не загружен\n  Работа с DSCP-метками невозможна\n  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'\n\"\n            dscp_exclude=\"\"\n            dscp_proxy=\"\"\n        fi\n    fi\n}\n\n# Получение transparent inbound'ов Xray\n_invalidate_inbounds_cache() { rm -f /tmp/xkeen-inbounds-cache; }\n\nget_xray_transparent_inbounds() {\n    cache_file=\"/tmp/xkeen-inbounds-cache\"\n    cache_valid=0\n    if [ -f \"$cache_file\" ]; then\n        newer=$(find \"$directory_xray_config\" -maxdepth 1 -name '*.json' -newer \"$cache_file\" 2>/dev/null | head -n 1)\n        [ -z \"$newer\" ] && cache_valid=1\n    fi\n    if [ \"$cache_valid\" = \"1\" ]; then\n        cat \"$cache_file\"\n        return 0\n    fi\n    cache_tmp=\"${cache_file}.tmp.$$\"\n    {\n        for file in \"$directory_xray_config\"/*.json; do\n            [ -f \"$file\" ] || continue\n\n            strip_json_comments \"$file\" |\n            jq -r --arg file \"$file\" '\n                .inbounds[]? |\n                select(\n                    (.protocol == \"dokodemo-door\" or .protocol == \"tunnel\") and\n                    ((.settings.followRedirect? // false) == true)\n                ) |\n                (.streamSettings.sockopt.tproxy? // \"\") as $tproxy |\n                select($tproxy == \"\" or $tproxy == \"redirect\" or $tproxy == \"tproxy\") |\n                [\n                    (if $tproxy == \"tproxy\" then \"tproxy\" else \"redirect\" end),\n                    (.port // \"\"),\n                    (.settings.network // \"\"),\n                    (.tag // \"\"),\n                    $file\n                ] | @tsv\n            ' 2>/dev/null\n        done\n    } > \"$cache_tmp\"\n    mv \"$cache_tmp\" \"$cache_file\"\n    cat \"$cache_file\"\n}\n\nget_xray_port_by_mode() {\n    mode=\"$1\"\n    port=$(\n        get_xray_transparent_inbounds |\n        awk -F '\\t' -v mode=\"$mode\" '\n            $1 == mode && $2 != \"\" {\n                print $2\n                exit\n            }\n        '\n    )\n\n    echo \"$port\"\n}\n\nget_xray_network_by_mode() {\n    mode=\"$1\"\n    network=$(\n        get_xray_transparent_inbounds |\n        awk -F '\\t' -v mode=\"$mode\" '\n            function add_networks(value, count, i, item) {\n                gsub(/,/, \" \", value)\n                gsub(/^[[:space:]]+|[[:space:]]+$/, \"\", value)\n                if (value == \"\") {\n                    return\n                }\n\n                count = split(value, items, /[[:space:]]+/)\n                for (i = 1; i <= count; i++) {\n                    item = items[i]\n                    if (item != \"\" && !seen[item]++) {\n                        order[++order_count] = item\n                    }\n                }\n            }\n\n            $1 == mode {\n                add_networks($3)\n            }\n\n            END {\n                for (i = 1; i <= order_count; i++) {\n                    printf \"%s%s\", order[i], (i < order_count ? \" \" : \"\")\n                }\n            }\n        '\n    )\n\n    echo \"$network\"\n}\n\n# Получение порта для Redirect\nget_port_redirect() {\n    if [ \"$name_client\" = \"xray\" ]; then\n        port=$(get_xray_port_by_mode \"redirect\")\n        [ -n \"$port\" ] && echo \"$port\" && return 0\n    elif [ \"$name_client\" = \"mihomo\" ]; then\n        port=$(yq eval '.redir-port // \"\"' \"$mihomo_config\" 2>/dev/null)\n        if [ -z \"$port\" ]; then\n            port=$(yq eval '.listeners[] | select(.type == \"redir\") | .port // \"\"' \"$mihomo_config\" 2>/dev/null)\n        fi\n        [ -n \"$port\" ] && echo \"$port\" && return 0\n    else\n\treturn 1\n    fi\n}\n\n# Получение порта для TProxy\nget_port_tproxy() {\n    if [ \"$name_client\" = \"xray\" ]; then\n        port=$(get_xray_port_by_mode \"tproxy\")\n        [ -n \"$port\" ] && echo \"$port\" && return 0\n    elif [ \"$name_client\" = \"mihomo\" ]; then\n        port=$(yq eval '.tproxy-port // \"\"' \"$mihomo_config\" 2>/dev/null)\n        if [ -z \"$port\" ]; then\n            port=$(yq eval '.listeners[] | select(.type == \"tproxy\") | .port // \"\"' \"$mihomo_config\" 2>/dev/null)\n        fi\n        [ -n \"$port\" ] && echo \"$port\" && return 0\n    else\n\treturn 1\n    fi\n}\n\n# Получение сети для Redirect\nget_network_redirect() {\n    if [ \"$name_client\" = \"xray\" ]; then\n        network=$(get_xray_network_by_mode \"redirect\")\n        [ -n \"$network\" ] && echo \"$network\" && return 0\n    elif [ \"$name_client\" = \"mihomo\" ]; then\n        [ -n \"$port_redirect\" ] && echo \"tcp\" && return 0\n        echo \"\" && return 0\n    else\n\treturn 1\n    fi\n}\n\n# Получение сети для TProxy\nget_network_tproxy() {\n    if [ \"$name_client\" = \"xray\" ]; then\n        network=$(get_xray_network_by_mode \"tproxy\")\n        [ -n \"$network\" ] && echo \"$network\" && return 0\n    elif [ \"$name_client\" = \"mihomo\" ]; then\n        if [ -n \"$port_redirect\" ] && [ -n \"$port_tproxy\" ]; then\n            echo \"udp\"\n        elif [ -z \"$port_redirect\" ] && [ -n \"$port_tproxy\" ]; then\n            echo \"tcp udp\"\n        else\n            echo \"\"\n        fi\n        return 0\n    else\n\treturn 1\n    fi\n}\n\n# Получение портов исключения из статических пробросов\nget_api_exclude_ports() {\n    api_redir_result=\"\"\n\n    if [ -n \"$api_static_json\" ]; then\n        api_redir_result=$(echo \"$api_static_json\" | jq -r '\n          [\n            .[] | \n            select(.disable != true) | \n            if has(\"end-port\") then \n              \"\\(.port):\\(.[\"end-port\"])\" \n            else \n              .port \n            end |\n            select(. != \"80\" and . != \"443\")\n          ] | \n          sort | \n          join(\",\")')\n    fi\n\n    echo \"$api_redir_result\"\n}\n\n\n# Получение исключенных портов\nget_port_exclude() {\n    port_exclude_redirect=\"\"\n    port_exclude_result=\"\"\n\n    port_exclude_redirect=$(get_api_exclude_ports)\n\n    if [ -n \"$port_exclude\" ]; then\n        if [ -n \"$port_exclude_redirect\" ]; then\n            port_exclude_result=\"$port_exclude,$port_exclude_redirect\"\n        else\n            port_exclude_result=\"$port_exclude\"\n        fi\n    else\n        port_exclude_result=\"$port_exclude_redirect\"\n    fi\n\n    port_exclude_result=$(printf '%s\\n' \"$port_exclude_result\" | tr -dc '0-9,:' | tr -s ',' | sed 's/^,//; s/,$//')\n    echo \"$port_exclude_result\"\n}\n\n# Получение исключений IPv4\nget_exclude_ip4() {\n    [ \"$iptables_supported\" != \"true\" ] && return\n\n    # Получаем провайдерский IPv4\n    ipv4_eth=$(ip -o route get 195.208.4.1 2>/dev/null | sed -n 's/.*src \\([^ ]*\\).*/\\1/p' || \\\n               ip -o route get 77.88.8.8 2>/dev/null | sed -n 's/.*src \\([^ ]*\\).*/\\1/p')\n    [ -n \"$ipv4_eth\" ] && ipv4_eth=\"${ipv4_eth}/32\"\n    echo \"${ipv4_eth} ${ipv4_exclude}\" | tr ' ' '\\n' | awk '!seen[$0]++' | tr '\\n' ' ' | sed 's/^ //; s/ $//'\n}\n\n# Получение исключений IPv6\nget_exclude_ip6() {\n    [ \"$ip6tables_supported\" != \"true\" ] && return\n\n    # Получаем провайдерский IPv6\n    ipv6_eth=$(ip -o -6 route get 2a0c:a9c7:8::1 2>/dev/null | sed -n 's/.*src \\([^ ]*\\).*/\\1/p' || \\\n               ip -o -6 route get 2a02:6b8::feed:0ff 2>/dev/null | sed -n 's/.*src \\([^ ]*\\).*/\\1/p')\n    [ -n \"$ipv6_eth\" ] && ipv6_eth=\"${ipv6_eth}/128\"\n    echo \"${ipv6_eth} ${ipv6_exclude}\" | tr ' ' '\\n' | awk '!seen[$0]++' | tr '\\n' ' ' | sed 's/^ //; s/ $//'\n}\n\n# Получение метки политики\nget_policy_mark() {\n    if [ -n \"$api_policy_json\" ]; then\n        policy_mark=$(echo \"$api_policy_json\" | jq -r --arg pname \"$name_policy\" '.[] | select(.description | ascii_downcase == ($pname | ascii_downcase)) | .mark' 2>/dev/null)\n    fi\n\n    if [ -n \"$policy_mark\" ]; then\n        echo \"0x${policy_mark}\"\n    else\n        echo \"\"\n    fi\n}\n\n# Атомарная синхронизация ipset xkeen_deny_mac с текущим состоянием hotspot API.\n# Идемпотентна: создаёт основной набор при первом вызове, в дальнейшем\n# наполняет tmp-набор и делает ipset swap. Вызывается на старте XKeen и\n# на каждой netfilter.d/schedule.d-инвокации — это даёт динамику без\n# `xkeen -restart` при работе Keenetic-расписаний (родительский контроль).\nsync_deny_mac_ipset() {\n    command -v ipset >/dev/null 2>&1 || return 0\n    ipset create \"$name_ipset_deny_mac\" hash:mac -exist 2>/dev/null || return 0\n    _xkeen_deny_tmp=\"${name_ipset_deny_mac}_tmp\"\n    ipset create \"$_xkeen_deny_tmp\" hash:mac -exist 2>/dev/null\n    ipset flush \"$_xkeen_deny_tmp\" >/dev/null 2>&1\n    _xkeen_hotspot_json=$(curl -kfsS \"${url_server}/${url_hotspot}\" 2>/dev/null)\n    if [ -n \"$_xkeen_hotspot_json\" ]; then\n        printf '%s' \"$_xkeen_hotspot_json\" | jq -r '\n            ((.host // . // []) |\n             (if type == \"array\" then .[] else . end)) |\n            select((.access // \"\") == \"deny\" and (.mac // \"\") != \"\") |\n            .mac\n        ' 2>/dev/null | tr '[:lower:]' '[:upper:]' | while IFS= read -r _xkeen_mac; do\n            [ -n \"$_xkeen_mac\" ] && ipset add \"$_xkeen_deny_tmp\" \"$_xkeen_mac\" -exist 2>/dev/null\n        done\n    fi\n    ipset swap \"$_xkeen_deny_tmp\" \"$name_ipset_deny_mac\" 2>/dev/null\n    ipset destroy \"$_xkeen_deny_tmp\" 2>/dev/null\n    unset _xkeen_deny_tmp _xkeen_hotspot_json _xkeen_mac\n}\n\n# Получаем пользовательские политики\nget_user_policies() {\n    [ ! -f \"$xkeen_config\" ] && return\n    jq -r '.xkeen.policy[]? | \"\\(.name)|\\(.port // \"\")\" ' \"$xkeen_config\" 2>/dev/null\n}\n\n# Проверка на конфликт имен политик\ncheck_policy_name_conflict() {\n    if [ -f \"$xkeen_config\" ]; then\n        conflict=$(jq -r --arg main \"$name_policy\" '.xkeen.policy[] | select((.name | ascii_downcase) == ($main | ascii_downcase)) | .name' \"$xkeen_config\" 2>/dev/null | head -n 1)\n\n        if [ -n \"$conflict\" ]; then\n            log_error_router \"Ошибка конфигурации: Имя политики в xkeen.json совпадает с зарезервированным\"\n            log_error_terminal \"\n  В файле '${yellow}xkeen.json${reset}' найдена политика с именем '${red}${conflict}${reset}'\n  Это имя зарезервировано основной службой XKeen\n\n  Переименуйте пользовательскую политику в json-файле\n  Запуск ${yellow}$name_client${reset} ${red}отменен${reset}\n\"\n        fi\n    fi\n}\n\n# Получаем порты пользовательских политик\nresolve_user_policies() {\n    [ -f \"$xkeen_config\" ] && [ -n \"$api_policy_json\" ] || return\n\n    api_exclude_ports=$(get_api_exclude_ports)\n\n    # Получаем сопоставленные политики одним вызовом jq\n    matched_policies=$(printf '%s' \"$api_policy_json\" | jq -r --argjson user_cfg \"$(cat \"$xkeen_config\")\" '\n        ($user_cfg.xkeen.policy // []) as $up |\n        .[] as $api |\n        $up[] | \n        select(\n            (.name | ascii_downcase) == \n            ($api.description | ascii_downcase)\n        ) |\n        \"\\(.name)|\\($api.mark)|\\(.port // \"\")\"\n    ' 2>/dev/null)\n\n    [ -z \"$matched_policies\" ] && return\n\n    # Обрабатываем каждую политику в одном цикле\n    echo \"$matched_policies\" | while IFS='|' read -r pname mark pports; do\n        if [ -z \"$pports\" ]; then\n            # Порты не указаны -> режим \"all\" (все порты)\n            if [ -n \"$api_exclude_ports\" ]; then\n                echo \"${pname}|${mark}|exclude|${api_exclude_ports}\"\n            else\n                echo \"${pname}|${mark}|all|\"\n            fi\n        else\n\n            case \"$pports\" in\n                !*) mode=\"exclude\"; ports=\"${pports#!}\"\n                    [ -n \"$api_exclude_ports\" ] && ports=\"${ports:+$ports,}$api_exclude_ports\" ;;\n                *) mode=\"include\"; ports=\"$pports\"\n                    if [ \"$file_dns\" = \"true\" ] && [ \"$proxy_dns\" = \"on\" ]; then\n                        case \",$ports,\" in\n                            *,53,*) ;;\n                            *) ports=\"53,$ports\" ;;\n                        esac\n                    fi\n                    ;;\n            esac\n\n            clean_ports=$(validate_and_clean_ports \"$ports\")\n            [ -n \"$clean_ports\" ] && echo \"${pname}|${mark}|${mode}|${clean_ports}\"\n        fi\n    done\n}\n\n# Получение режима прокси-клиента\nget_mode_proxy() {\n    if [ -n \"$port_redirect\" ] && [ -n \"$port_tproxy\" ]; then\n        mode_proxy=\"Hybrid\"\n    elif [ -n \"$port_tproxy\" ]; then\n        mode_proxy=\"TProxy\"\n    elif [ -n \"$port_redirect\" ]; then\n        mode_proxy=\"Redirect\"\n    else\n        mode_proxy=\"Other\"\n    fi\n    echo \"$mode_proxy\"\n}\n\n# Настройка брандмауэра\nconfigure_firewall() {\n    : > \"$file_netfilter_hook\"\n\n    # Pre-evaluate dynamic variables\n    val_exclude_ip6=\"$(get_exclude_ip6)\"\n    val_exclude_ip4=\"$(get_exclude_ip4)\"\n\n    cat > \"$file_netfilter_hook\" <<'EOL'\n#!/bin/sh\n# XKeen: Auto-generated file. DO NOT EDIT!\n[ -f /tmp/xkeen_ready ] || exit 0\nEOL\n\n    # Securely inject variables into the script\n    inject_var() {\n        local name=\"$1\"\n        local val=\"$2\"\n        local safe_val\n        safe_val=\"${val//\\'/\\'\\\\\\'\\'}\"\n        printf \"%s='%s'\\n\" \"$name\" \"$safe_val\" >> \"$file_netfilter_hook\"\n    }\n\n    inject_var name_client \"$name_client\"\n    inject_var name_profile \"$name_profile\"\n    inject_var mode_proxy \"$mode_proxy\"\n    inject_var network_redirect \"$network_redirect\"\n    inject_var network_tproxy \"$network_tproxy\"\n    inject_var networks \"$networks\"\n    inject_var name_chain \"$name_chain\"\n    inject_var port_redirect \"$port_redirect\"\n    inject_var port_tproxy \"$port_tproxy\"\n    inject_var port_donor \"$port_donor\"\n    inject_var port_exclude \"$port_exclude\"\n    inject_var policy_mark \"$policy_mark\"\n    inject_var comment_tag \"$comment_tag\"\n    inject_var comment \"$comment\"\n    inject_var custom_mark \"$custom_mark\"\n    inject_var dscp_exclude \"$dscp_exclude\"\n    inject_var dscp_proxy \"$dscp_proxy\"\n    inject_var user_policies \"$user_policies\"\n    inject_var table_redirect \"$table_redirect\"\n    inject_var table_tproxy \"$table_tproxy\"\n    inject_var table_mark \"$table_mark\"\n    inject_var table_id \"$table_id\"\n    inject_var file_dns \"$file_dns\"\n    inject_var proxy_dns \"$proxy_dns\"\n    inject_var proxy_router \"$proxy_router\"\n    inject_var directory_os_modules \"$directory_os_modules\"\n    inject_var directory_user_modules \"$directory_user_modules\"\n    inject_var directory_configs_app \"$directory_configs_app\"\n    inject_var directory_xray_config \"$directory_xray_config\"\n    inject_var directory_xray_asset \"$directory_xray_asset\"\n    inject_var iptables_supported \"$iptables_supported\"\n    inject_var ip6tables_supported \"$ip6tables_supported\"\n    inject_var arm64_fd \"$arm64_fd\"\n    inject_var other_fd \"$other_fd\"\n    inject_var aghfix \"$aghfix\"\n    \n    inject_var ipv6_proxy \"$ipv6_proxy\"\n    inject_var ipv4_proxy \"$ipv4_proxy\"\n    inject_var val_exclude_ip6 \"$val_exclude_ip6\"\n    inject_var val_exclude_ip4 \"$val_exclude_ip4\"\n    inject_var name_ipset_deny_mac \"$name_ipset_deny_mac\"\n    inject_var url_server \"$url_server\"\n    inject_var url_hotspot \"$url_hotspot\"\n\n    cat >> \"$file_netfilter_hook\" <<'EOL'\n\n# Перезапуск скрипта\nrestart_script() {\n    exec /bin/sh \"$0\" \"$@\"\n}\n\nif pidof \"$name_client\" >/dev/null; then\n\n    # Динамическая синхронизация ipset с deny-MAC из hotspot API.\n    # Закрывает обход built-in политики «Без доступа в интернет» при включенном\n    # проксировании: PREROUTING на эти MAC делает RETURN до TPROXY, пакет идёт\n    # в FORWARD, где штатно дропается NDM-цепочкой _NDM_HOTSPOT_FWD.\n    # Хук перезапускается NDM при netfilter rewrite, schedule.d дёргает этот же\n    # скрипт на start/stop расписаний — список MAC всегда актуален.\n    _xkeen_sync_deny_mac_ipset() {\n        command -v ipset >/dev/null 2>&1 || return 0\n        ipset create \"$name_ipset_deny_mac\" hash:mac -exist 2>/dev/null || return 0\n        _tmp=\"${name_ipset_deny_mac}_tmp\"\n        ipset create \"$_tmp\" hash:mac -exist 2>/dev/null\n        ipset flush \"$_tmp\" >/dev/null 2>&1\n        _hjson=$(curl -kfsS \"${url_server}/${url_hotspot}\" 2>/dev/null)\n        if [ -n \"$_hjson\" ]; then\n            printf '%s' \"$_hjson\" | jq -r '\n                ((.host // . // []) |\n                 (if type == \"array\" then .[] else . end)) |\n                select((.access // \"\") == \"deny\" and (.mac // \"\") != \"\") |\n                .mac\n            ' 2>/dev/null | tr '[:lower:]' '[:upper:]' | while IFS= read -r _m; do\n                [ -n \"$_m\" ] && ipset add \"$_tmp\" \"$_m\" -exist 2>/dev/null\n            done\n        fi\n        ipset swap \"$_tmp\" \"$name_ipset_deny_mac\" 2>/dev/null\n        ipset destroy \"$_tmp\" 2>/dev/null\n    }\n    _xkeen_sync_deny_mac_ipset\n\n    # Аккумулируем правила в строки, применяем атомарно одним\n    # iptables-restore --noflush на (family, table) в _xkeen_apply.\n    # Сохраняем семантику старого ipt() для всех существующих helper'ов.\n    _xkeen_v4_nat_rules=\"\"\n    _xkeen_v4_mangle_rules=\"\"\n    _xkeen_v6_nat_rules=\"\"\n    _xkeen_v6_mangle_rules=\"\"\n\n    ipt() {\n        [ \"$family\" = \"iptables\" ] && [ \"$iptables_supported\" != \"true\" ] && return 0\n        [ \"$family\" = \"ip6tables\" ] && [ \"$ip6tables_supported\" != \"true\" ] && return 0\n\n        case \"$1\" in\n            -A|-I|-D)\n                _line=$*\n                case \"${family}_${table}\" in\n                    iptables_nat)     _xkeen_v4_nat_rules=\"${_xkeen_v4_nat_rules}${_line}\n\" ;;\n                    iptables_mangle)  _xkeen_v4_mangle_rules=\"${_xkeen_v4_mangle_rules}${_line}\n\" ;;\n                    ip6tables_nat)    _xkeen_v6_nat_rules=\"${_xkeen_v6_nat_rules}${_line}\n\" ;;\n                    ip6tables_mangle) _xkeen_v6_mangle_rules=\"${_xkeen_v6_mangle_rules}${_line}\n\" ;;\n                esac\n                return 0\n                ;;\n            *)\n                # Прочие операции (-F, -X) - в реальный iptables.\n                if [ \"$family\" = \"iptables\" ]; then\n                    iptables -w -t \"$table\" \"$@\"\n                else\n                    ip6tables -w -t \"$table\" \"$@\"\n                fi\n                return $?\n                ;;\n        esac\n    }\n\n    # Применяет аккумулированные правила одной таблицы атомарно через\n    # iptables-restore --noflush. Custom chain $name_chain flush'ится\n    # объявлением \":$name_chain -\" перед добавлением новых правил.\n    _xkeen_apply_table() {\n        _family=\"$1\"\n        _table=\"$2\"\n        _rules_var=\"$3\"\n\n        eval \"_rules=\\${$_rules_var}\"\n        [ -z \"$_rules\" ] && return 0\n\n        # Удаляем устаревшие xkeen-tagged правила из built-in/system chain'ов\n        # (PREROUTING, OUTPUT, _NDM_HOTSPOT_DNSREDIR), правила из самой $name_chain\n        # игнорируются - там \":chain -\" в blob их сам flush'ит.\n        save_cmd=\"\"\n        [ \"$_family\" = \"iptables\" ] && [ \"$iptables_supported\" = \"true\" ] && save_cmd=\"iptables-save\"\n        [ \"$_family\" = \"ip6tables\" ] && [ \"$ip6tables_supported\" = \"true\" ] && save_cmd=\"ip6tables-save\"\n        [ -z \"$save_cmd\" ] && { _deletes=\"\"; return; }\n\n        _deletes=$($save_cmd -t \"$_table\" 2>/dev/null | awk \\\n            -v tag=\"$comment_tag\" \\\n            -v c1=\"$name_chain\" \\\n            -v c2=\"${name_chain}_out\" '\n            index($0, tag) &&\n            $1 == \"-A\" &&\n            $2 != c1 &&\n            $2 != c2 {\n                sub(/^-A /, \"-D \")\n                print\n            }\n        ')\n\n        {\n            printf '*%s\\n' \"$_table\"\n            printf ':%s -\\n' \"$name_chain\"\n            [ \"$proxy_router\" = \"on\" ] && printf ':%s_out -\\n' \"$name_chain\"\n            [ -n \"$_deletes\" ] && printf '%s\\n' \"$_deletes\"\n            printf '%s' \"$_rules\"\n            printf 'COMMIT\\n'\n        } | if [ \"$_family\" = \"iptables\" ]; then\n            iptables-restore --noflush\n        else\n            ip6tables-restore --noflush\n        fi\n    }\n\n    _xkeen_apply() {\n        [ \"$iptables_supported\" = \"true\" ] && _xkeen_apply_table iptables nat _xkeen_v4_nat_rules || true\n        [ \"$iptables_supported\" = \"true\" ] && _xkeen_apply_table iptables mangle _xkeen_v4_mangle_rules || true\n        [ \"$ip6tables_supported\" = \"true\" ] && _xkeen_apply_table ip6tables nat _xkeen_v6_nat_rules || true\n        [ \"$ip6tables_supported\" = \"true\" ] && _xkeen_apply_table ip6tables mangle _xkeen_v6_mangle_rules || true\n    }\n\n    # Добавление правил-исключений\n    add_exclude_rules() {\n        chain=\"$1\"\n        for exclude in $exclude_list; do\n            if [ \"$file_dns\" = \"true\" ] && [ \"$proxy_dns\" = \"on\" ] && [ \"$chain\" != \"${name_chain}_out\" ]; then\n                case \"$exclude\" in\n                    10.0.0.0/8|172.16.0.0/12|192.168.0.0/16|fd00::/8|fe80::/10)\n                    if [ \"$table\" = \"mangle\" ] && [ \"$mode_proxy\" = \"Hybrid\" ]; then\n                        ipt -A \"$chain\" -d \"$exclude\" -p tcp --dport 53 $comment -j RETURN >/dev/null 2>&1\n                        ipt -A \"$chain\" -d \"$exclude\" -p udp ! --dport 53 $comment -j RETURN >/dev/null 2>&1\n                    elif [ \"$table\" = \"nat\" ] && [ \"$mode_proxy\" = \"Hybrid\" ]; then\n                        ipt -A \"$chain\" -d \"$exclude\" -p tcp ! --dport 53 $comment -j RETURN >/dev/null 2>&1\n                        ipt -A \"$chain\" -d \"$exclude\" -p udp --dport 53 $comment -j RETURN >/dev/null 2>&1\n                    elif [ \"$table\" = \"mangle\" ] && [ \"$mode_proxy\" = \"TProxy\" ]; then\n                        ipt -A \"$chain\" -d \"$exclude\" -p tcp ! --dport 53 $comment -j RETURN >/dev/null 2>&1\n                        ipt -A \"$chain\" -d \"$exclude\" -p udp ! --dport 53 $comment -j RETURN >/dev/null 2>&1\n                    fi\n                    ;;\n                esac\n            else\n                ipt -A \"$chain\" -d \"$exclude\" $comment -j RETURN >/dev/null 2>&1\n            fi\n        done\n    }\n\n    add_ipset_exclude() {\n        base_set=\"$1\"\n        set_type=\"${2:-hash:net}\"\n\n        if [ \"$family\" = \"ip6tables\" ]; then\n            set_name=\"${base_set}6\"\n            ipset_family=\"inet6\"\n        else\n            set_name=\"$base_set\"\n            ipset_family=\"inet\"\n        fi\n\n        ipset create \"$set_name\" \"$set_type\" family \"$ipset_family\" -exist || return\n\n        ipt -I \"$chain\" 1 -m set --match-set \"$set_name\" dst $comment -j RETURN >/dev/null 2>&1\n    }\n\n    # Добавление правил iptables\n    add_ipt_rule() {\n        family=\"$1\"\n        table=\"$2\"\n        chain=\"$3\"\n        shift 3\n        [ \"$family\" = \"iptables\" ] && [ \"$iptables_supported\" = \"false\" ] && return\n        [ \"$family\" = \"ip6tables\" ] && [ \"$ip6tables_supported\" = \"false\" ] && return\n\n        # Custom chain создаётся/flush'ится одной строкой \":$name_chain -\" в blob,\n        # поэтому ни -nL guard, ни -N не нужны - всегда заполняем body.\n        add_exclude_rules \"$chain\"\n\n        if [ \"$table\" = \"$table_tproxy\" ]; then\n            if [ \"$mode_proxy\" = \"Hybrid\" ]; then\n                set -- -p udp -m conntrack --ctstate ESTABLISHED,RELATED $comment -j CONNMARK --restore-mark\n            else\n                set -- -m conntrack --ctstate ESTABLISHED,RELATED $comment -j CONNMARK --restore-mark\n            fi\n            ipt -I \"$chain\" 1 \"$@\" >/dev/null 2>&1\n        fi\n\n        case \"$mode_proxy\" in\n            Hybrid)\n                if [ \"$table\" = \"$table_redirect\" ]; then\n                    ipt -I \"$chain\" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1\n                    add_ipset_exclude ext_exclude hash:ip\n                    add_ipset_exclude geo_exclude hash:net\n                    add_ipset_exclude user_exclude hash:net\n                    ipt -A \"$chain\" -p tcp $comment -j REDIRECT --to-port \"$port_redirect\" >/dev/null 2>&1\n                else\n                    ipt -I \"$chain\" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1\n                    add_ipset_exclude ext_exclude hash:ip\n                    add_ipset_exclude geo_exclude hash:net\n                    add_ipset_exclude user_exclude hash:net\n                    ipt -A \"$chain\" -p udp -m socket --transparent $comment -j MARK --set-mark \"$table_mark\" >/dev/null 2>&1\n                    ipt -A \"$chain\" -p udp -m mark ! --mark 0 $comment -j CONNMARK --save-mark >/dev/null 2>&1\n                    ipt -A \"$chain\" -p udp $comment -j TPROXY --on-ip \"$proxy_ip\" --on-port \"$port_tproxy\" --tproxy-mark \"$table_mark\" >/dev/null 2>&1\n                fi\n                ;;\n            TProxy)\n                ipt -I \"$chain\" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1\n                for net in $network_tproxy; do\n                    add_ipset_exclude ext_exclude hash:ip\n                    add_ipset_exclude geo_exclude hash:net\n                    add_ipset_exclude user_exclude hash:net\n                    ipt -A \"$chain\" -p \"$net\" -m socket --transparent $comment -j MARK --set-mark \"$table_mark\" >/dev/null 2>&1\n                    ipt -A \"$chain\" -p \"$net\" -m mark ! --mark 0 $comment -j CONNMARK --save-mark >/dev/null 2>&1\n                    ipt -A \"$chain\" -p \"$net\" $comment -j TPROXY --on-ip \"$proxy_ip\" --on-port \"$port_tproxy\" --tproxy-mark \"$table_mark\" >/dev/null 2>&1\n                done\n                ;;\n            Redirect)\n                ipt -I \"$chain\" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1\n                add_ipset_exclude ext_exclude hash:ip\n                add_ipset_exclude geo_exclude hash:net\n                add_ipset_exclude user_exclude hash:net\n                for net in $network_redirect; do\n                    ipt -A \"$chain\" -p \"$net\" $comment -j REDIRECT --to-port \"$port_redirect\" >/dev/null 2>&1\n                done\n                ;;\n            *) exit 0 ;;\n        esac\n\n        if [ -n \"$dscp_exclude\" ]; then\n            for dscp in $dscp_exclude; do\n                ipt -I \"$chain\" -m dscp --dscp \"$dscp\" $comment -j RETURN >/dev/null 2>&1\n            done\n        fi\n    }\n\n    # Настройка таблицы маршрутов\n    configure_route() {\n        ip_version=\"$1\"\n\n        # Определяем таблицу маршрутизации\n        if [ -n \"$policy_mark\" ]; then\n            policy_table=$(ip rule show | awk -v policy=\"$policy_mark\" '$0 ~ policy && /lookup/ && !/blackhole/ {print $(NF); exit}')\n        fi\n        source_table=\"${policy_table:-main}\"\n\n        # Проверяем есть ли default маршрут\n        check_default() {\n            if [ \"$ip_version\" = \"6\" ] && ! ip -6 route show default 2>/dev/null | grep -q .; then\n                return 0\n            fi\n            if [ \"$source_table\" = \"main\" ]; then\n                ip -\"$ip_version\" route show default 2>/dev/null | grep -q '^default'\n            else\n                ip -\"$ip_version\" route show table all 2>/dev/null | grep -E \"^[[:space:]]*default .* table $policy_table([[:space:]]|$)\" | grep -vq 'unreachable' >/dev/null\n            fi\n        }\n\n        attempts=0\n        max_attempts=4\n        until check_default; do\n            attempts=$((attempts + 1))\n            if [ \"$attempts\" -ge \"$max_attempts\" ]; then\n                [ \"$ip_version\" = \"4\" ] && touch \"/tmp/noinet\"\n                return 1\n            fi\n            sleep 1\n        done\n        [ \"$ip_version\" = \"4\" ] && rm -f \"/tmp/noinet\"\n\n        ip -\"$ip_version\" rule del fwmark \"$table_mark\" lookup \"$table_id\" >/dev/null 2>&1 || true\n        ip -\"$ip_version\" route flush table \"$table_id\" >/dev/null 2>&1 || true\n        ip -\"$ip_version\" route add local default dev lo table \"$table_id\" >/dev/null 2>&1 || true\n        ip -\"$ip_version\" rule add fwmark \"$table_mark\" lookup \"$table_id\" >/dev/null 2>&1 || true\n\n        # Копируем маршруты\n        ip -\"$ip_version\" route show table \"$source_table\" 2>/dev/null | while read -r route_line; do\n            case \"$route_line\" in\n                default*|unreachable*|blackhole*) continue ;;\n                *) ip -\"$ip_version\" route add table \"$table_id\" $route_line >/dev/null 2>&1 || true ;;\n            esac\n        done\n        return 0\n    }\n\n    # Создание множественных правил multiport\n    add_multiport_rules() {\n        family=\"$1\"\n        table=\"$2\"\n        net=\"$3\"\n        mark=\"$4\"\n        ports=\"$5\"\n        target=\"$6\"\n\n        [ -z \"$ports\" ] && return\n\n        num_ports=$(echo \"$ports\" | tr ',' '\\n' | wc -l)\n        i=1\n        while [ \"$i\" -le \"$num_ports\" ]; do\n            end=$((i + 6))\n            chunk=$(echo \"$ports\" | tr ',' '\\n' | sed -n \"${i},${end}p\" | tr '\\n' ',' | sed 's/,$//')\n            [ -z \"$chunk\" ] && break\n            if [ -n \"$mark\" ]; then\n                set -- -m connmark --mark \"$mark\" -m conntrack ! --ctstate INVALID -p \"$net\" -m multiport --dports \"$chunk\" $comment -j \"$target\"\n            else\n                set -- -m conntrack ! --ctstate INVALID -p \"$net\" -m multiport --dports \"$chunk\" $comment -j \"$target\"\n            fi\n            ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n            i=$((i + 7))\n        done\n    }\n\n    # Добавление цепочек PREROUTING\n    add_prerouting() {\n        family=\"$1\"\n        table=\"$2\"\n\n        # MAC-bypass для built-in «Без доступа в интернет»: RETURN из PREROUTING\n        # до xkeen-jumps, пакет минует TPROXY/REDIRECT/MARK и попадает в FORWARD,\n        # где NDM-цепочка _NDM_HOTSPOT_FWD его дропнет штатно. -m mac --mac-source\n        # видит L2-MAC только для устройств в одном broadcast-домене с роутером\n        # (LAN/Wi-Fi/guest-bridge); за L3-VLAN правило безвредно неактивно.\n        ipt -I PREROUTING 1 -m set --match-set \"$name_ipset_deny_mac\" src $comment -j RETURN >/dev/null 2>&1\n\n        for net in $networks; do\n            if [ \"$mode_proxy\" = \"Hybrid\" ]; then\n                [ \"$table\" = \"nat\"    ] && [ \"$net\" != \"tcp\" ] && continue\n                [ \"$table\" = \"mangle\" ] && [ \"$net\" != \"udp\" ] && continue\n            fi\n\n            if [ \"$mode_proxy\" = \"TProxy\" ]; then\n                proto_match=\"\"\n            else\n                proto_match=\"-p $net\"\n            fi\n\n            for dscp in $dscp_proxy; do\n                set -- -m conntrack ! --ctstate INVALID $proto_match -m dscp --dscp \"$dscp\" $comment -j \"$name_chain\"\n                ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n            done\n\n            if [ \"$proxy_router\" = \"on\" ]; then\n                set -- -i lo -m mark --mark \"$table_mark\" $proto_match $comment -j \"$name_chain\"\n                ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n            fi\n\n            # Пользовательские политики из xkeen.json\n            # Heredoc вместо echo|while - while должен исполниться в parent shell,\n            # чтобы аккумуляторы _xkeen_*_rules в ipt() модифицировались в нужном scope.\n            while IFS='|' read -r pname pmark pmode pports; do\n                [ -z \"$pmark\" ] && continue\n\n                pmark=$(echo \"$pmark\" | tr -d ' \\r\\n')\n                pmode=$(echo \"$pmode\" | tr -d ' \\r\\n')\n                pports=$(echo \"$pports\" | tr -d ' \\r\\n')\n\n                if [ \"$pmode\" = \"all\" ]; then\n                    set -- -m connmark --mark 0x\"$pmark\" -m conntrack ! --ctstate INVALID $comment -j \"$name_chain\"\n                    ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n                elif [ \"$pmode\" = \"include\" ]; then\n                    add_multiport_rules \"$family\" \"$table\" \"$net\" \"0x$pmark\" \"$pports\" \"$name_chain\"\n                elif [ \"$pmode\" = \"exclude\" ]; then\n                    add_multiport_rules \"$family\" \"$table\" \"$net\" \"0x$pmark\" \"$pports\" \"RETURN\"\n                    set -- -m connmark --mark 0x\"$pmark\" -m conntrack ! --ctstate INVALID -p \"$net\" $comment -j \"$name_chain\"\n                    ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n                fi\n            done <<USER_POLICIES_EOF\n$user_policies\nUSER_POLICIES_EOF\n\n            # Политика xkeen (стандартная)\n            if [ -n \"$policy_mark\" ]; then\n                # заданы порты проксирования\n                if [ -n \"$port_donor\" ]; then\n                    add_multiport_rules \"$family\" \"$table\" \"$net\" \"$policy_mark\" \"$port_donor\" \"$name_chain\"\n                # заданы порты исключения\n                elif [ -n \"$port_exclude\" ]; then\n                    add_multiport_rules \"$family\" \"$table\" \"$net\" \"$policy_mark\" \"$port_exclude\" \"RETURN\"\n                    set -- -m connmark --mark \"$policy_mark\" -m conntrack ! --ctstate INVALID -p \"$net\" $comment -j \"$name_chain\"\n                    ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n                else\n                    # Политика xkeen, когда порты не указаны (проксирование на всех портах)\n                    set -- -m connmark --mark \"$policy_mark\" -m conntrack ! --ctstate INVALID $comment -j \"$name_chain\"\n                    ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n                fi\n            # НЕТ политики xkeen\n            else\n                # заданы порты проксирования\n                if [ -n \"$port_donor\" ]; then\n                    add_multiport_rules \"$family\" \"$table\" \"$net\" \"\" \"$port_donor\" \"$name_chain\"\n                # заданы порты исключения\n                elif [ -n \"$port_exclude\" ]; then\n                    add_multiport_rules \"$family\" \"$table\" \"$net\" \"\" \"$port_exclude\" \"RETURN\"\n                    set -- -m conntrack ! --ctstate INVALID -p \"$net\" $comment -j \"$name_chain\"\n                    ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n                # Если нет ни xkeen, ни пользовательских политик -> перехватываем всё\n                else\n                    set -- -m conntrack ! --ctstate INVALID $comment -j \"$name_chain\"\n                    ipt -A PREROUTING \"$@\" >/dev/null 2>&1\n                fi\n            fi\n        done\n    }\n\n    # Добавление цепочек для проксирования трафика Entware\n    add_output() {\n        family=\"$1\"\n        table=\"$2\"\n\n        [ \"$proxy_router\" != \"on\" ] && return\n\n        out_chain=\"${name_chain}_out\"\n\n        # \":${name_chain}_out -\" в blob создаст/flush'ит chain атомарно,\n        # body заполняется всегда.\n        orig_chain=\"$chain\"\n        chain=\"$out_chain\"\n\n        ipt -A \"$out_chain\" -o lo $comment -j RETURN >/dev/null 2>&1\n        ipt -A \"$out_chain\" -m mark --mark 255 $comment -j RETURN >/dev/null 2>&1\n\n        add_exclude_rules \"$out_chain\"\n\n        add_ipset_exclude ext_exclude hash:ip\n        add_ipset_exclude geo_exclude hash:net\n        add_ipset_exclude user_exclude hash:net\n\n        chain=\"$orig_chain\"\n\n        for net in $networks; do\n            if [ \"$mode_proxy\" = \"Hybrid\" ]; then\n                [ \"$table\" = \"nat\"    ] && [ \"$net\" != \"tcp\" ] && continue\n                [ \"$table\" = \"mangle\" ] && [ \"$net\" != \"udp\" ] && continue\n            fi\n\n            if [ \"$mode_proxy\" = \"TProxy\" ]; then\n                proto_match=\"\"\n            else\n                proto_match=\"-p $net\"\n            fi\n\n            set -- -m conntrack ! --ctstate INVALID $proto_match $comment -j \"$out_chain\"\n            ipt -A OUTPUT \"$@\" >/dev/null 2>&1\n\n            if [ \"$table\" = \"$table_redirect\" ]; then\n                set -- -p \"$net\" $comment -j REDIRECT --to-port \"$port_redirect\"\n                ipt -A \"$out_chain\" \"$@\" >/dev/null 2>&1\n            elif [ \"$table\" = \"$table_tproxy\" ]; then\n                set -- -p \"$net\" $comment -j MARK --set-mark \"$table_mark\"\n                ipt -A \"$out_chain\" \"$@\" >/dev/null 2>&1\n            fi\n        done\n    }\n\n    dns_redir() {\n        family=\"$1\"\n        table=\"nat\"\n\n        [ \"$aghfix\" != \"on\" ] && return\n        [ \"$file_dns\" = \"true\" ] && [ \"$proxy_dns\" = \"on\" ] && return\n\n        all_marks=\"\"\n        [ -n \"$policy_mark\" ] && all_marks=\"$policy_mark\"\n\n        [ -n \"$custom_mark\" ] && all_marks=\"$custom_mark $all_marks\"\n\n        if [ -n \"$user_policies\" ]; then\n            user_marks=$(echo \"$user_policies\" | awk -F'|' '{if ($2 != \"\") print \"0x\"$2}')\n            all_marks=\"$all_marks $user_marks\"\n        fi\n\n        for mark in $all_marks; do\n            mark=$(echo \"$mark\" | tr -d ' \\r\\n')\n            [ -z \"$mark\" ] && continue\n\n            for proto in udp tcp; do\n                set -- -p \"$proto\" -m mark --mark \"$mark\" -m pkttype --pkt-type unicast -m \"$proto\" --dport 53 $comment -j REDIRECT --to-ports 53\n                ipt -I _NDM_HOTSPOT_DNSREDIR \"$@\" >/dev/null 2>&1\n            done\n        done\n    }\n\n    if [ -n \"$port_donor\" ] || [ -n \"$port_exclude\" ]; then\n        [ \"$file_dns\" = \"true\" ] && [ \"$proxy_dns\" = \"on\" ] && [ -n \"$port_donor\" ] && port_donor=\"53,$port_donor\"\n    fi\n    for family in iptables ip6tables; do\n\n        [ \"$family\" = \"ip6tables\" ] && [ \"$ip6tables_supported\" != \"true\" ] && continue\n        [ \"$family\" = \"iptables\" ] && [ \"$iptables_supported\" != \"true\" ] && continue\n\n        if [ \"$family\" = \"ip6tables\" ]; then\n            exclude_list=\"$val_exclude_ip6\"\n            proxy_ip=\"$ipv6_proxy\"\n            configure_route 6\n        else\n            exclude_list=\"$val_exclude_ip4\"\n            proxy_ip=\"$ipv4_proxy\"\n            configure_route 4\n        fi\n        if [ -n \"$port_redirect\" ] && [ -n \"$port_tproxy\" ]; then\n            for table in \"$table_tproxy\" \"$table_redirect\"; do\n                add_ipt_rule \"$family\" \"$table\" \"$name_chain\"\n                add_prerouting \"$family\" \"$table\"\n                add_output \"$family\" \"$table\"\n            done\n        elif [ -z \"$port_redirect\" ] && [ -n \"$port_tproxy\" ]; then\n            table=\"$table_tproxy\"\n            add_ipt_rule \"$family\" \"$table\" \"$name_chain\"\n            add_prerouting \"$family\" \"$table\"\n            add_output \"$family\" \"$table\"\n        elif [ -n \"$port_redirect\" ] && [ -z \"$port_tproxy\" ]; then\n            table=\"$table_redirect\"\n            add_ipt_rule \"$family\" \"$table\" \"$name_chain\"\n            add_prerouting \"$family\" \"$table\"\n            add_output \"$family\" \"$table\"\n        fi\n\n        dns_redir \"$family\"\n    done\n\n    # Атомарно применяем все аккумулированные правила одним\n    # iptables-restore --noflush per (family, table).\n    _xkeen_apply\nelse\n    [ -f \"/tmp/xkeen_starting.lock\" ] && exit 0\n    touch \"/tmp/xkeen_starting.lock\"\n    . \"/opt/sbin/.xkeen/01_info/03_info_cpu.sh\"\n    status_file=\"/opt/lib/opkg/status\"\n    info_cpu\n\n    fd_limit=\"$other_fd\"\n    [ \"$architecture\" = \"arm64-v8a\" ] && fd_limit=\"$arm64_fd\"\n    ulimit -SHn \"$fd_limit\"\n\n    case \"$name_client\" in\n        xray)\n            export XRAY_LOCATION_CONFDIR=\"$directory_xray_config\"\n            export XRAY_LOCATION_ASSET=\"$directory_xray_asset\"\n            \"$name_client\" run >/dev/null 2>&1 &\n        ;;\n        mihomo)\n            export CLASH_HOME_DIR=\"$directory_configs_app\"\n            \"$name_client\" >/dev/null 2>&1 &\n        ;;\n    esac\n    _probe=0\n    while [ \"$_probe\" -lt 60 ]; do\n        pidof \"$name_client\" >/dev/null 2>&1 && break\n        _probe=$((_probe + 1))\n        usleep 100000\n    done\n    unset _probe\n    rm -f \"/tmp/xkeen_starting.lock\"\n    if pidof \"$name_client\" >/dev/null; then\n        restart_script \"$@\"\n    else\n        exit 1\n    fi\nfi\nEOL\n    sed -i '1,2!{/^[[:space:]]*#/d; /^[[:space:]]*$/d}' \"$file_netfilter_hook\"\n    chmod 700 \"$file_netfilter_hook\"\n\n    # Schedule.d-хук: NDM вызывает scripts/schedule.d при start/stop расписаний\n    # (родительский контроль). Хук дёргает netfilter.d/proxy.sh, который\n    # ре-синхронизирует ipset deny-MAC из актуального hotspot API.\n    mkdir -p \"$(dirname \"$file_schedule_hook\")\" 2>/dev/null\n    cat > \"$file_schedule_hook\" <<'SCHEDULE_EOL'\n#!/bin/sh\n# XKeen: re-sync deny MAC ipset on schedule start/stop. Auto-generated. DO NOT EDIT!\n[ \"$1\" = \"start\" ] || [ \"$1\" = \"stop\" ] || exit 0\n[ -x /opt/etc/ndm/netfilter.d/proxy.sh ] && /opt/etc/ndm/netfilter.d/proxy.sh\nSCHEDULE_EOL\n    chmod 755 \"$file_schedule_hook\"\n\n    sh \"$file_netfilter_hook\"\n}\n\n# Удаление правил iptables\nclean_firewall() {\n    [ -f \"$file_netfilter_hook\" ] && : > \"$file_netfilter_hook\"\n\n    get_ipver_support\n\n    for family in iptables ip6tables; do\n        [ \"$family\" = \"iptables\" ] && [ \"$iptables_supported\" != \"true\" ] && continue\n        [ \"$family\" = \"ip6tables\" ] && [ \"$ip6tables_supported\" != \"true\" ] && continue\n\n        if \"$family\" -w -t nat -nL _NDM_HOTSPOT_DNSREDIR >/dev/null 2>&1; then\n            \"$family\" -w -t nat -S _NDM_HOTSPOT_DNSREDIR | grep -E -- \"$comment_tag\" | sed 's/^-A /-D /' | while IFS= read -r rule; do\n                [ -n \"$rule\" ] && \"$family\" -w -t nat $rule >/dev/null 2>&1\n            done\n        fi\n    done\n\n    clean_run() {\n        family=\"$1\"\n        table=\"$2\"\n        name_chain=\"$3\"\n\n        for sys_chain in PREROUTING OUTPUT; do\n            \"$family\" -w -t \"$table\" -S \"$sys_chain\" 2>/dev/null | grep -E -- \"$comment_tag\" | sed 's/^-A /-D /' | while IFS= read -r rule; do\n                [ -n \"$rule\" ] && \"$family\" -w -t \"$table\" $rule >/dev/null 2>&1\n            done\n        done\n\n        if \"$family\" -w -t \"$table\" -nL \"$name_chain\" >/dev/null 2>&1; then\n            \"$family\" -w -t \"$table\" -F \"$name_chain\" >/dev/null 2>&1\n            \"$family\" -w -t \"$table\" -X \"$name_chain\" >/dev/null 2>&1\n        fi\n\n        out_chain=\"${name_chain}_out\"\n        if \"$family\" -w -t \"$table\" -nL \"$out_chain\" >/dev/null 2>&1; then\n            \"$family\" -w -t \"$table\" -F \"$out_chain\" >/dev/null 2>&1\n            \"$family\" -w -t \"$table\" -X \"$out_chain\" >/dev/null 2>&1\n        fi\n    }\n\n    for family in iptables ip6tables; do\n        for chain in nat mangle; do\n            clean_run \"$family\" \"$chain\" \"$name_chain\"\n        done\n    done\n\n    if command -v ip >/dev/null 2>&1; then\n        for family in 4 6; do\n            while ip -\"$family\" rule del fwmark \"$table_mark\" lookup \"$table_id\" >/dev/null 2>&1; do :; done\n            ip -\"$family\" route flush table \"$table_id\" >/dev/null 2>&1 || true\n        done\n    fi\n\n    # Очистка и удаление списков ipset\n    if command -v ipset >/dev/null 2>&1; then\n        for set in geo_exclude geo_exclude6 user_exclude user_exclude6 \"$name_ipset_deny_mac\"; do\n            ipset flush \"$set\" >/dev/null 2>&1\n            ipset destroy \"$set\" >/dev/null 2>&1\n        done\n    fi\n\n    # Schedule.d-hook идемпотентно перегенерируется в configure_firewall,\n    # на остановке убираем чтобы NDM не дёргал мёртвый netfilter.d/proxy.sh.\n    [ -f \"$file_schedule_hook\" ] && rm -f \"$file_schedule_hook\"\n}\n\n# Мониторинг файловых дескрипторов\nmonitor_fd() {\n    while true; do\n        client_pid=$(pidof \"$name_client\" | awk '{print $1}')\n        if [ -n \"$client_pid\" ] && [ -d \"/proc/$client_pid/fd\" ]; then\n            limit=$(awk '/Max open files/ {print $4}' \"/proc/$client_pid/limits\")\n            set -- /proc/$client_pid/fd/*\n            [ -e \"$1\" ] || set --\n            current=$#\n            if [ \"$limit\" -gt 0 ] && [ \"$current\" -gt $((limit * 90 / 100)) ]; then\n                log_warning_router \"$name_client открыл $current из $limit файловых дескрипторов, инициирован перезапуск\"\n                rm -f \"$file_pid_fd\"\n                fd_out=true\n                proxy_stop\n                proxy_start \"on\"\n                exit 0\n            fi\n        fi\n        sleep \"$delay_fd\"\n    done\n}\n\nload_ipset() {\n    set=\"$1\"\n    file=\"$2\"\n    family=\"$3\"\n    tmp=\"${set}_tmp\"\n\n    # Заполняем tmp; основной набор подменяется только после успешного restore\n    ipset create \"$set\" hash:net family \"$family\" -exist\n    ipset create \"$tmp\" hash:net family \"$family\" -exist\n    ipset flush \"$tmp\"\n\n    if [ -f \"$file\" ] && sed -e 's/\\r$//' -e 's/#.*//' -e '/^[[:space:]]*$/d' \"$file\" | awk '{print \"add '\"$tmp\"' \"$1}' | ipset restore -exist; then\n        ipset swap \"$set\" \"$tmp\"\n    fi\n    ipset destroy \"$tmp\"\n}\n\napply_fd_limit() {\n    fd_limit=\"$other_fd\"\n    [ \"$architecture\" = \"arm64-v8a\" ] && fd_limit=\"$arm64_fd\"\n    ulimit -SHn \"$fd_limit\"\n}\n\ncleanup_fd_monitor() {\n    [ -f \"$file_pid_fd\" ] || return 0\n    kill \"$(cat \"$file_pid_fd\")\" 2>/dev/null\n    rm -f \"$file_pid_fd\"\n}\n\nmissing_files_template='\n  '\"${light_blue}\"'Отсутствуют исполняемые файлы:'\"${reset}\"'\n  '\"${yellow}\"'%b'\"${reset}\"'\n\n  '\"${green}\"'Возможные причины:'\"${reset}\"'\n  • XKeen установлен во внутреннюю память и на ней недостаточно места\n  • У файла отсутствуют права на выполнение\n\n  '\"${green}\"'Рекомендуемые действия:'\"${reset}\"'\n  • Переустановите XKeen на внешний накопитель\n  • Скопируйте недостающий файл вручную и сделайте исполняемым\n'\n\ncheck_binary() {\n    file=\"$1\"\n    path=\"$install_dir/$file\"\n\n    if [ ! -f \"$path\" ] || [ ! -x \"$path\" ]; then\n        return 1\n    fi\n\n    check_cmd=\"version\"\n    [ \"$file\" = \"xray\" ] && check_cmd=\"version\"\n    [ \"$file\" = \"yq\" ] && check_cmd=\"--version\"\n    [ \"$file\" = \"mihomo\" ] && check_cmd=\"-v\"\n\n    if ! \"$file\" $check_cmd >/dev/null 2>&1; then\n        log_error_router \"Бинарный файл $file аварийно остановлен\"\n        log_error_terminal \"\n  Бинарный файл ${yellow}$file${reset} аварийно остановлен\n  ${red}Файл повреждён или несовместим с процессором${reset} вашего роутера\n  Установите другую версию ${yellow}$file${reset}\n\"\n    fi\n\n    return 0\n}\n\ninfo_health_binary() {\n    missing_files=\"\"\n\n    add_to_missing() {\n        file_name=\"$1\"\n        prefix=\"  - \" \n        \n        if [ -z \"$missing_files\" ]; then\n            missing_files=\"${prefix}${yellow}${file_name}${reset}\"\n        else\n            missing_files=\"${missing_files}\\n  ${prefix}${yellow}${file_name}${reset}\"\n        fi\n    }\n\n    case \"$name_client\" in\n        xray)\n            if ! check_binary xray; then add_to_missing \"xray\"; fi\n            ;;\n       mihomo)\n            for file in mihomo yq; do\n                if ! check_binary \"$file\"; then add_to_missing \"$file\"; fi\n            done\n            ;;\n        esac\n\n    if [ -n \"$missing_files\" ]; then\n        log_error_terminal \"$(printf \"$missing_files_template\" \"$missing_files\")\"\n    fi\n}\n\n# Очистка при аварийной остановке прокси-клиента\nemergency_clear() {\n    rm -f \"/tmp/xkeen_ready\"\n    cleanup_fd_monitor\n    clean_firewall\n}\n\n# Запуск прокси-клиента\nproxy_start() {\n    start_manual=\"$1\"\n    if [ \"$start_manual\" = \"on\" ] || [ \"$start_auto\" = \"on\" ]; then\n        _invalidate_inbounds_cache\n        apply_ipv6_state\n        get_ipver_support\n        info_health_binary\n        validate_xkeen_json\n        check_policy_name_conflict\n        check_xray_backups\n        validate_routing_mark\n        log_clean\n        api_cache_init\n        sync_deny_mac_ipset\n        process_user_ports\n        process_custom_mark\n        port_redirect=$(get_port_redirect)\n        network_redirect=$(get_network_redirect)\n        port_tproxy=$(get_port_tproxy)\n        network_tproxy=$(get_network_tproxy)\n        mode_proxy=$(get_mode_proxy)\n        if [ \"$mode_proxy\" != \"Other\" ]; then\n            policy_mark=$(get_policy_mark)\n\n            if [ -n \"$policy_mark\" ]; then\n                user_policies=$(resolve_user_policies)\n\n                if [ -n \"$user_policies\" ]; then\n                    print_policy_info \"yes\" \"yes\"\n                else\n                    print_policy_info \"yes\" \"no\"\n                fi\n            else\n                raw_user_policies=$(get_user_policies)\n                ignored_custom=\"no\"\n\n                if [ -n \"$raw_user_policies\" ]; then\n                    ignored_custom=\"yes\"\n                fi\n\n                print_policy_info \"no\" \"no\" \"$ignored_custom\"\n\n                user_policies=\"\"\n            fi\n\n            networks=$(printf '%s\\n' $network_redirect $network_tproxy | tr ',' ' ' | tr -s ' ' '\\n' | sort -u | tr '\\n' ' ')\n            networks=${networks% }\n\n            if [ -n \"$policy_mark\" ] && [ -z \"$port_donor\" ]; then\n                port_exclude=$(get_port_exclude)\n            fi\n            if ! proxy_status && { [ -n \"$port_donor\" ] || [ -n \"$port_exclude\" ] || [ \"$mode_proxy\" = \"TProxy\" ] || [ \"$mode_proxy\" = \"Hybrid\" ]; }; then\n                get_modules\n            fi\n            if [ \"$mode_proxy\" = \"TProxy\" ]; then\n                keenetic_ssl=\"$(get_keenetic_port)\" || {\n                    proxy_stop\n                    log_error_router \"Порт 443 занят сервисами Keenetic\"\n                    log_error_terminal \"\n  Необходимый для режима ${light_blue}TProxy${reset} ${red}443 порт занят${reset} сервисами Keenetic\n\n  Освободите его на странице 'Пользователи и доступ' веб-интерфейса роутера\n\"\n                }\n            fi\n        fi\n        if proxy_status; then\n            echo -e \"  Прокси-клиент уже ${green}запущен${reset}\"\n            # Marker до configure_firewall: тот завершается `sh proxy.sh`,\n            # gate в хуке читает /tmp/xkeen_ready.\n            touch \"/tmp/xkeen_ready\"\n            [ \"$mode_proxy\" != \"Other\" ] && configure_firewall\n            if [ \"$start_manual\" = \"on\" ]; then\n                log_error_terminal \"Не удалось запустить ${yellow}$name_client${reset}, так как он уже запущен\"\n            else\n                log_info_router \"Прокси-клиент успешно запущен в режиме $mode_proxy\"\n                rm -f \"/tmp/xkeen_coldstart.lock\"\n            fi\n        else\n            log_info_router \"Инициирован запуск прокси-клиента\"\n            attempt=1\n            . \"/opt/sbin/.xkeen/01_info/03_info_cpu.sh\"\n            status_file=\"/opt/lib/opkg/status\"\n            info_cpu\n            while [ \"$attempt\" -le \"$start_attempts\" ]; do\n                case \"$name_client\" in\n                    xray)\n                        export XRAY_LOCATION_CONFDIR=\"$directory_xray_config\"\n                        export XRAY_LOCATION_ASSET=\"$directory_xray_asset\"\n                        find \"$directory_xray_config\" -maxdepth 1 -name '._*.json' -type f -delete\n                        apply_fd_limit\n                        if [ -n \"$fd_out\" ]; then\n                            nohup \"$name_client\" run >/dev/null 2>&1 &\n                            unset fd_out\n                        else\n                            \"$name_client\" run &\n                        fi\n                    ;;\n                    mihomo)\n                        export CLASH_HOME_DIR=\"$directory_configs_app\"\n                        apply_fd_limit\n                        if [ -n \"$fd_out\" ]; then\n                            nohup \"$name_client\" >/dev/null 2>&1 &\n                            unset fd_out\n                        else\n                            \"$name_client\" &\n                        fi\n                        ;;\n                    *) log_error_terminal \"Неизвестный прокси-клиент: ${yellow}$name_client${reset}\" ;;\n                esac\n                _probe_attempt=0\n                while [ \"$_probe_attempt\" -lt 60 ]; do\n                    proxy_status && break\n                    _probe_attempt=$((_probe_attempt + 1))\n                    usleep 50000\n                done\n                unset _probe_attempt\n                if proxy_status; then\n                    # См. alive-branch: marker до configure_firewall.\n                    touch \"/tmp/xkeen_ready\"\n                    [ \"$mode_proxy\" != \"Other\" ] && configure_firewall\n                    _pids=\"\"\n                    [ \"$iptables_supported\" = \"true\" ] && [ -f \"$ru_exclude_ipv4\" ] && { load_ipset geo_exclude \"$ru_exclude_ipv4\" inet & _pids=\"$_pids $!\"; }\n                    [ \"$ip6tables_supported\" = \"true\" ] && [ -f \"$ru_exclude_ipv6\" ] && { load_ipset geo_exclude6 \"$ru_exclude_ipv6\" inet6 & _pids=\"$_pids $!\"; }\n                    load_user_ipset & _pids=\"$_pids $!\"\n                    [ -n \"$_pids\" ] && wait $_pids\n                    unset _pids\n                    echo -e \"  Прокси-клиент ${green}запущен${reset} в режиме ${light_blue}${mode_proxy}${reset}\"\n                    (\n                        # Даём ядру прокси время полностью инициализироваться\n                        # Это защищает от ситуаций, когда xray/mihomo\n                        # успевает создать PID, но затем аварийно завершается,\n                        # например, из-за битой конфигурации\n                        sleep 3\n\n                        if ! proxy_status; then\n                            echo\n                            echo -e \"  Прокси-клиент ${red}аварийно завершился${reset}\"\n                            echo -e \"  ${green}Выполняется очистка${reset} правил прозрачного проксирования\"\n                            log_error_router \"Прокси-клиент аварийно завершился после запуска\"\n                            emergency_clear\n                            printf '\\n~ # '\n                        fi\n                    ) &\n                    if [ -n \"$api_policy_json\" ]; then\n                        if echo \"$api_policy_json\" | jq --arg policy \"$name_policy\" -e 'any(.[]; .description | ascii_downcase == $policy)' > /dev/null; then\n                            if [ -e \"/tmp/noinet\" ]; then\n                                echo\n                                echo -e \"  У политики ${yellow}$name_policy${reset} ${red}нет доступа в интернет${reset}\"\n                                echo \"  Проверьте, установлена ли галка на подключении к провайдеру\"\n                            fi\n                        fi\n                    fi\n                    [ \"$mode_proxy\" = \"Other\" ] && echo -e \"  Функция прозрачного прокси ${red}не активна${reset}. Направляйте соединения на ${yellow}${name_client}${reset} вручную\"\n                    log_info_router \"Прокси-клиент успешно запущен в режиме $mode_proxy\"\n                    rm -f \"/tmp/xkeen_coldstart.lock\"\n                    if [ \"$check_fd\" = \"on\" ]; then\n                        cleanup_fd_monitor\n                        monitor_fd &\n                        echo $! > \"$file_pid_fd\"\n                        log_info_router \"Запущен контроль файловых дескрипторов $name_client\"\n                    fi\n                    return 0\n                fi\n                attempt=$((attempt + 1))\n            done\n            echo -e \"  ${red}Не удалось запустить${reset} прокси-клиент\"\n            log_error_terminal \"Не удалось запустить прокси-клиент\"\n        fi\n    else\n        clean_firewall\n    fi\n}\n\n# Активная проба готовности окружения вместо sleep $start_delay.\n# Ждём ndmc, default route и insmod-ability xt_TPROXY (deps ndm\n# подгружает асинхронно уже после ndmc-ready). $start_delay сохранён\n# как safety cap (FAQ #12).\nwait_for_ready() {\n    _max=$(( ${start_delay:-60} * 2 ))\n    _attempt=0\n    _probe_ko=\"$directory_os_modules/xt_TPROXY.ko\"\n    while [ \"$_attempt\" -lt \"$_max\" ]; do\n        if ndmc -c \"show version\" >/dev/null 2>&1 \\\n           && ip route show default 2>/dev/null | grep -q '^default'; then\n            # .ko отсутствует (не TProxy/Hybrid), уже загружен, либо insmod удался\n            if [ ! -f \"$_probe_ko\" ] \\\n               || grep -q '^xt_TPROXY ' /proc/modules 2>/dev/null \\\n               || insmod \"$_probe_ko\" >/dev/null 2>&1; then\n                return 0\n            fi\n        fi\n        usleep 500000\n        _attempt=$((_attempt + 1))\n    done\n    return 0\n}\n\n# Остановка прокси-клиента\nproxy_stop() {\n    rm -f \"/tmp/xkeen_ready\"\n    if ! proxy_status; then\n        echo -e \"  Прокси-клиент ${red}не запущен${reset}\"\n        cleanup_fd_monitor\n    else\n        [ -f \"/tmp/xkeen_coldstart.lock\" ] || log_info_router \"Инициирована остановка прокси-клиента\"\n        cleanup_fd_monitor\n        attempt=1\n        while [ \"$attempt\" -le \"$start_attempts\" ]; do\n            clean_firewall\n            killall -q \"$name_client\" 2>/dev/null\n            _stop_attempt=0\n            while [ \"$_stop_attempt\" -lt 30 ]; do\n                pidof \"$name_client\" >/dev/null 2>&1 || break\n                _stop_attempt=$((_stop_attempt + 1))\n                usleep 50000\n            done\n            unset _stop_attempt\n            if pidof \"$name_client\" >/dev/null 2>&1; then\n                killall -q -9 \"$name_client\" 2>/dev/null\n                usleep 200000\n            fi\n            if ! proxy_status; then\n                echo -e \"  Прокси-клиент ${red}остановлен${reset}\"\n                [ -f \"/tmp/xkeen_coldstart.lock\" ] || log_info_router \"Прокси-клиент успешно остановлен\"\n                rm -f \"/tmp/xkeen_coldstart.lock\"\n                return 0\n            fi\n            attempt=$((attempt + 1))\n        done\n        echo -e \"  Прокси-клиент ${red}не удалось остановить${reset}\"\n        log_error_terminal \"Не удалось остановить прокси-клиент\"\n    fi\n}\n\n# Менеджер команд\ncase \"$1\" in\n    start)\n        ipset create ext_exclude hash:ip family inet -exist\n        ipset create ext_exclude6 hash:ip family inet6 -exist\n        if [ -z \"$2\" ]; then\n            [ \"$start_auto\" != \"on\" ] && exit 0\n            log_info_router \"Подготовка к запуску прокси-клиента\"\n            nohup \"$0\" cold_start >/dev/null 2>&1 &\n            touch \"/tmp/xkeen_coldstart.lock\"\n            exit 0\n        fi\n        proxy_start \"$2\"\n    ;;\n    stop) proxy_stop ;;\n    status)\n        if proxy_status; then\n            mode_proxy=\"\"\n            if [ -f \"$file_netfilter_hook\" ]; then\n                mode_proxy=$(grep '^mode_proxy=' \"$file_netfilter_hook\" | awk -F\"=\" '{print $2}' | tr -d \"'\" 2>/dev/null)\n            fi\n            [ -z \"$mode_proxy\" ] && mode_proxy=\"Other\"\n            echo -e \"  Прокси-клиент ${yellow}$name_client${reset} ${green}запущен${reset} в режиме ${light_blue}$mode_proxy${reset}\"\n        else\n            echo -e \"  Прокси-клиент ${red}не запущен${reset}\"\n        fi\n        ;;\n    restart) proxy_stop; proxy_start \"$2\" ;;\n    cold_start)\n        # Re-spawn в чистый S05xkeen: sh-функции (wait_for_ready) не\n        # наследуются через nohup sh -c, поэтому пробу зовём отсюда.\n        wait_for_ready\n        proxy_start \"\"\n        ;;\n    *) echo -e \"  Команды: ${green}start${reset} | ${red}stop${reset} | ${yellow}restart${reset} | status\" ;;\nesac\n\nexit 0"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/00_configs_import.sh",
    "content": "# Импорт модулей конфигураций\n\t\n# Модуль конфигурации\n. \"$xinstall_dir/08_install_configs/01_configs_install.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/01_configs_install.sh",
    "content": "# Функция для установки файлов конфигурации Xray\ninstall_configs() {\n    if [ ! -d \"$xray_conf_dir\" ]; then\n        mkdir -p \"$xray_conf_dir\"\n    fi\n\n    if ls \"$xray_conf_dir\"/*.json >/dev/null 2>&1; then\n        return 0\n    fi\n\n    xray_files=\"$xray_conf_smpl\"/*.json\n    for file in $xray_files; do\n        filename=$(basename \"$file\")\n        cp \"$file\" \"$xray_conf_dir/\"\n        echo \"  Добавлен шаблон конфигурационного файла Xray:\"\n        echo -e \"  ${yellow}$filename${reset}\"\n        sleep 1\n    done\n}\n"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/01_log.json",
    "content": "{\n  \"log\": {\n    \"access\": \"/opt/var/log/xray/access.log\",\n    \"error\": \"/opt/var/log/xray/error.log\",\n    \"dnsLog\": true,\n    \"loglevel\": \"none\"\n  }\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/02_dns.json",
    "content": "{\n// Пример настройки - https://jameszero.net/3398.htm\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/03_inbounds.json",
    "content": "{\n  \"inbounds\": [\n    {\n      \"port\": 1181,\n      \"protocol\": \"tunnel\",\n      \"settings\": {\n        \"network\": \"tcp\",\n        \"followRedirect\": true\n      },\n      \"sniffing\": {\n        \"enabled\": true,\n        \"routeOnly\": true,\n        \"destOverride\": [\"http\",\"tls\"]\n      },\n      \"tag\": \"redirect\"\n    },\n    {\n      \"port\": 1181,\n      \"protocol\": \"tunnel\",\n      \"settings\": {\n        \"network\": \"udp\",\n        \"followRedirect\": true\n      },\n      \"streamSettings\": {\n        \"sockopt\": {\"tproxy\": \"tproxy\"}\n      },\n      \"sniffing\": {\n        \"enabled\": true,\n        \"routeOnly\": true,\n        \"destOverride\": [\"quic\"]\n      },\n      \"tag\": \"tproxy\"\n    }\n  ]\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/04_outbounds.json",
    "content": "{\n// Создайте файл по ссылке https://zxc-rv.github.io/XKeen-UI/Outbound_Generator/\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/05_routing.json",
    "content": "{\n// Создайте файл по ссылке https://xray-routing-generator.netlify.app\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/06_policy.json",
    "content": "{\n  \"policy\": {\n    \"levels\": {\n      \"0\": {\n        \"uplinkOnly\": 0,\n        \"downlinkOnly\": 0\n      }\n    }\n  }\n}"
  },
  {
    "path": "scripts/_xkeen/03_delete/00_delete_import.sh",
    "content": "# Импорт модулей удаления\n\n# Модули удаления\n. \"$xdelete_dir/01_delete_geofile.sh\"\n. \"$xdelete_dir/02_delete_geoipset.sh\"\n. \"$xdelete_dir/03_delete_cron.sh\"\n. \"$xdelete_dir/04_delete_configs.sh\"\n. \"$xdelete_dir/05_delete_register.sh\"\n. \"$xdelete_dir/06_delete_tmp.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/03_delete/01_delete_geofile.sh",
    "content": "# Функция для удаления выбранных файлов GeoSite\ndelete_geosite() {\n    [ \"$choice_delete_geosite_refilter_select\" = \"true\" ] && rm -f \"$geo_dir/geosite_refilter.dat\"\n    [ \"$choice_delete_geosite_v2fly_select\" = \"true\" ] && rm -f \"$geo_dir/geosite_v2fly.dat\"\n    [ \"$choice_delete_geosite_zkeen_select\" = \"true\" ] && rm -f \"$geo_dir/\"geosite_zkeen.dat \"$geo_dir/\"zkeen.dat\n}\n\n# Функция для удаления всех файлов GeoSite\ndelete_geosite_key() {\n    rm -f \"$geo_dir/geosite_refilter.dat\" \\\n          \"$geo_dir/geosite_v2fly.dat\" \\\n          \"$geo_dir/geosite_zkeen.dat\" \\\n          \"$geo_dir/zkeen.dat\"\n}\n\n# Функция для удаления выбранных файлов GeoIP\ndelete_geoip() {\n    [ \"$choice_delete_geoip_refilter_select\" = \"true\" ] && rm -f \"$geo_dir/geoip_refilter.dat\"\n    [ \"$choice_delete_geoip_v2fly_select\" = \"true\" ] && rm -f \"$geo_dir/geoip_v2fly.dat\"\n    [ \"$choice_delete_geoip_zkeenip_select\" = \"true\" ] && rm -f \"$geo_dir/geoip_zkeenip.dat\" \"$geo_dir/zkeenip.dat\"\n}\n\n# Функция для удаления всех файлов GeoIP\ndelete_geoip_key() {\n    rm -f \"$geo_dir/geoip_refilter.dat\" \\\n          \"$geo_dir/geoip_v2fly.dat\" \\\n          \"$geo_dir/geoip_zkeenip.dat\" \\\n          \"$geo_dir/zkeenip.dat\"\n}"
  },
  {
    "path": "scripts/_xkeen/03_delete/02_delete_geoipset.sh",
    "content": "# Функция для удаления GeoIPSET\ndelete_geoipset() {\n    while true; do\n        printf \"\\n  Желаете удалить российские IP-адреса из исключений проксирования?\\n\\n\"\n        printf \"     1. Да. Загруженные файлы подсетей будут удалены, а списки очищены\\n\"\n        printf \"     0. Нет. Отмена удаления\\n\\n\"\n        printf \"  Ваш выбор: \"\n        read -r choice\n        \n        case \"$choice\" in\n            0)\n                echo\n                printf \"  Отмена удаления списков GeoIPSET.\\n\\n\"\n                return 0\n                ;;\n            1)\n                echo\n                break\n                ;;\n            *)\n                printf \"  Неверный ввод. Пожалуйста, введите 1 или 0.\\n\"\n                ;;\n        esac\n    done\n\n    ipset flush geo_exclude 2>/dev/null\n    ipset flush geo_exclude6 2>/dev/null\n\n    [ -f \"$ru_exclude_ipv4\" ] && rm -f \"$ru_exclude_ipv4\" 2>/dev/null\n    [ -f \"$ru_exclude_ipv6\" ] && rm -f \"$ru_exclude_ipv6\" 2>/dev/null\n    # [ -d \"$ipset_cfg\" ] && rm -rf \"$ipset_cfg\"\n\n    printf \"  Списки исключений GeoIPSET ${green}успешно удалены${reset}\\n\\n\"\n    return 0\n}\n\ndelete_geoipset_key() {\n    ipset flush geo_exclude 2>/dev/null\n    ipset flush geo_exclude6 2>/dev/null\n\n    [ -f \"$ru_exclude_ipv4\" ] && rm -f \"$ru_exclude_ipv4\" 2>/dev/null\n    [ -f \"$ru_exclude_ipv6\" ] && rm -f \"$ru_exclude_ipv6\" 2>/dev/null\n    # [ -d \"$ipset_cfg\" ] && rm -rf \"$ipset_cfg\"\n}"
  },
  {
    "path": "scripts/_xkeen/03_delete/03_delete_cron.sh",
    "content": "# Функция для удаления cron задачи для GeoFile\ndelete_cron_geofile() {\n    if [ -f \"$cron_dir/$cron_file\" ]; then\n        tmp_file=\"$cron_dir/${cron_file}.tmp\"\n        cp \"$cron_dir/$cron_file\" \"$tmp_file\"\n        grep -v \"ug\" \"$tmp_file\" | grep -v '^\\s*$' > \"$cron_dir/$cron_file\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/03_delete/04_delete_configs.sh",
    "content": "# Удаление всех конфигураций Xray\n\ndelete_configs() {\n    if [ -d \"$xray_conf_dir\" ]; then\n        find \"$xray_conf_dir\" -maxdepth 1 -name '*.json' -type f -delete\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/03_delete/05_delete_register.sh",
    "content": "# Удаление регистрации Xray\ndelete_register_xray() {\n    # Удаляем соответствующие записи из файла статуса opkg\n    sed -i -e '/Package: xray_s/,/Installed-Time:/d' \"/opt/lib/opkg/status\"\n    \n    # Удаляем файлы регистрации, если они существуют\n    if [ -f \"$register_dir/xray_s.control\" ] || [ -f \"$register_dir/xray_s.list\" ]; then\n        rm -f \"$register_dir/xray_s.control\" \"$register_dir/xray_s.list\"\n    fi\n}\n\n# Удаление регистрации Mihomo\ndelete_register_mihomo() {\n    # Удаляем соответствующие записи из файла статуса opkg\n    sed -i -e '/Package: mihomo_s/,/Installed-Time:/d' \"/opt/lib/opkg/status\"\n    sed -i -e '/Package: yq_s/,/Installed-Time:/d' \"/opt/lib/opkg/status\"\n    \n    # Удаляем файлы регистрации, если они существуют\n    if [ -f \"$register_dir/mihomo_s.control\" ] || [ -f \"$register_dir/mihomo_s.list\" ]; then\n        rm -f \"$register_dir/mihomo_s.control\" \"$register_dir/mihomo_s.list\"\n    fi\n    if [ -f \"$register_dir/yq_s.control\" ] || [ -f \"$register_dir/yq_s.list\" ]; then\n        rm -f \"$register_dir/yq_s.control\" \"$register_dir/yq_s.list\"\n    fi\n}\n\n# Удаление регистрации XKeen\ndelete_register_xkeen() {\n    # Удаляем соответствующие записи из файла статуса opkg\n    sed -i -e '/Package: xkeen/,/Installed-Time:/d' \"/opt/lib/opkg/status\"\n    \n    # Удаляем файлы регистрации, если они существуют\n    if [ -f \"$register_dir/xkeen.control\" ] || [ -f \"$register_dir/xkeen.list\" ]; then\n        rm -f \"$register_dir/xkeen.control\" \"$register_dir/xkeen.list\"\n    fi\n}\n"
  },
  {
    "path": "scripts/_xkeen/03_delete/06_delete_tmp.sh",
    "content": "# Удаление временных файлов и директорий\ndelete_tmp() {\n    [ -d \"$ktmp_dir\" ] && rm -rf \"$ktmp_dir\"\n    [ -d \"$xtmp_dir\" ] && rm -rf \"$xtmp_dir\"\n    [ -d \"$mtmp_dir\" ] && rm -rf \"$mtmp_dir\"\n    [ -f \"$cron_dir/root.tmp\" ] && rm -f \"$cron_dir/root.tmp\"\n    [ -f \"$register_dir/new_entry.txt\" ] && rm -f \"$register_dir/new_entry.txt\"\n    [ -f \"$install_dir/xray_bak\" ] && rm -f \"$install_dir/xray_bak\"\n    [ -f \"$install_dir/mihomo_bak\" ] && rm -f \"$install_dir/mihomo_bak\"\n    [ -f \"/tmp/xkrun\" ] && rm -f \"/tmp/xkrun\"\n    [ -f \"/tmp/toff\" ] && rm -f \"/tmp/toff\"\n\n    if ! pidof xray >/dev/null && ! pidof mihomo >/dev/null ; then\n        [ -f \"$file_netfilter_hook\" ] && rm \"$file_netfilter_hook\"\n        [ -f \"$file_schedule_hook\" ] && rm \"$file_schedule_hook\"\n        if command -v ipset >/dev/null 2>&1; then\n            ipset flush \"$name_ipset_deny_mac\" >/dev/null 2>&1\n            ipset destroy \"$name_ipset_deny_mac\" >/dev/null 2>&1\n        fi\n    fi\n\n    echo\n    echo -e \"  Очистка временных файлов ${green}выполнена${reset}\"\n}\n\ndelete_all() {\n    echo\n    echo -e \"  Удалить резервные копии и пользовательские настройки?\"\n    echo -e \"  ${yellow}$backups_dir${reset}\"\n    echo -e \"  ${yellow}$xkeen_cfg${reset}\"\n    echo\n    echo \"     1. Да, удалить\"\n    echo \"     0. Нет, оставить\"\n    echo\n\n    while true; do\n        read -r -p \"  Ваш выбор: \" choice\n        case \"$choice\" in\n            1)\n                [ -d \"$backups_dir\" ] && rm -rf \"$backups_dir\"\n                [ -d \"$xkeen_cfg\" ] && rm -rf \"$xkeen_cfg\"\n                return 0\n                ;;\n            0)\n                return 0\n                ;;\n            *)\n                echo -e \"  ${red}Некорректный ввод${reset}\"\n                ;;\n        esac\n    done\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/00_tools_import.sh",
    "content": "\n# Дополнительные инструменты\n. \"$xtools_dir/01_tools_ports.sh\"\n. \"$xtools_dir/02_tools_modules.sh\"\n. \"$xtools_dir/03_tools_diagnostic.sh\"\n. \"$xtools_dir/04_tools_delay.sh\"\n\n# Модуль выбора\n. \"$xtools_dir/05_tools_choice/00_choice_import.sh\"\n\n# Модуль резервного копирования\n. \"$xtools_dir/06_tools_backups/00_backups_import.sh\"\n\n# Модуль загрузок\n. \"$xtools_dir/07_tools_downloaders/00_downloaders_import.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/01_tools_ports.sh",
    "content": "read_ports_file() {\n    file=\"$1\"\n\n    [ -f \"$file\" ] || return\n\n    sed 's/\\r$//' \"$file\" | \\\n    sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | \\\n    grep -v '^#' | \\\n    grep -v '^$' | \\\n    sed 's/-/:/g' | \\\n    grep -E '^[0-9]+(:[0-9]+)?$' | \\\n    tr '\\n' ',' | \\\n    sed 's/,$//'\n}\n\nwrite_ports_file() {\n    file=\"$1\"\n    ports=\"$2\"\n\n    tmpfile=$(mktemp)\n\n    echo \"# XKeen ports list\" > \"$tmpfile\"\n    echo \"$ports\" | tr ',' '\\n' >> \"$tmpfile\"\n\n    mv \"$tmpfile\" \"$file\"\n}\n\nports_conflict_check() {\n    file1=\"$1\"\n    file2=\"$2\"\n\n    ports1=$(read_ports_file \"$file1\")\n    ports2=$(read_ports_file \"$file2\")\n\n    if [ -n \"$ports1\" ] && [ -n \"$ports2\" ]; then\n        return 0\n    fi\n\n    return 1\n}\n\ndata_is_updated_donor() {\n    file=$1\n    new_ports=$2\n    current_ports=$(\n        awk -F= '/^port_donor/{print $2; exit}' \"$file\" \\\n        | tr -d '\"'\n    )\n    if [ \"$current_ports\" = \"$new_ports\" ]; then\n        return 0\n    else\n        return 1\n    fi\n}\n\ndata_is_updated_excluded() {\n    file=$1\n    new_ports=$2\n    current_ports=$(\n        awk -F= '/^port_exclude/{print $2; exit}' \"$file\" \\\n        | tr -d '\"'\n    )\n    if [ \"$current_ports\" = \"$new_ports\" ]; then\n        return 0\n    else\n        return 1\n    fi\n}\n\nnormalize_ports() {\n    echo \"$1\" | tr ',' '\\n' | awk '\n    function valid(p) {\n        return (p ~ /^[0-9]+$/ && p >= 0 && p <= 65535)\n    }\n\n    {\n        gsub(/[[:space:]]/, \"\")\n        if ($0 == \"\") next\n\n        gsub(/-/, \":\")\n\n        n = split($0, a, \":\")\n\n        if (n == 1) {\n            if (valid(a[1])) ports[a[1]]\n        }\n\n        else if (n == 2) {\n            if (valid(a[1]) && valid(a[2])) {\n                start = a[1]\n                end   = a[2]\n\n                if (start > end) {\n                    tmp = start\n                    start = end\n                    end = tmp\n                }\n\n                ports[start \":\" end]\n            }\n        }\n    }\n\n    END {\n        for (p in ports)\n            print p\n    }\n    ' | sort -n | tr '\\n' ',' | sed 's/,$//'\n}\n\nensure_web_ports() {\n    ports=\"$1\"\n\n    echo \"$ports\" | tr ',' '\\n' | grep -qx \"80\"  || ports=\"$ports,80\"\n    echo \"$ports\" | tr ',' '\\n' | grep -qx \"443\" || ports=\"$ports,443\"\n\n    normalize_ports \"$ports\"\n}\n\nadd_ports_donor() {\n    [ -z \"$1\" ] && {\n        echo -e \"  ${red}Ошибка${reset}: список портов не может быть пустым\"\n        return 1\n    }\n\n    if ports_conflict_check \"$file_port_proxying\" \"$file_port_exclude\"; then\n        echo -e \"  ${yellow}Внимание${reset}: вы добавляете порты проксирования, но уже заданы порты исключения\n  Приоритет у портов проксирования, порты исключения будут проигнорированы\"\n    fi\n\n    new_ports=$(normalize_ports \"$1\")\n    current_ports=$(read_ports_file \"$file_port_proxying\")\n    current_ports=$(normalize_ports \"$current_ports\")\n\n    if [ -n \"$current_ports\" ]; then\n        all_ports=$(echo \"$current_ports,$new_ports\" | tr ',' '\\n' | sort -n -u | tr '\\n' ',' | sed 's/,$//')\n    else\n        all_ports=\"$new_ports\"\n    fi\n\n    all_ports=$(ensure_web_ports \"$all_ports\")\n\n    write_ports_file \"$file_port_proxying\" \"$all_ports\"\n\n    echo -e \"  ${green}Порты проксирования обновлены${reset}\"\n}\n\ndell_ports_donor() {\n    ports_to_del=$(normalize_ports \"$1\")\n    current_ports=$(read_ports_file \"$file_port_proxying\")\n\n    [ -z \"$current_ports\" ] && {\n        echo -e \"  ${yellow}Файл пуст${reset}\"\n        return\n    }\n\n    if [ -z \"$ports_to_del\" ]; then\n        > \"$file_port_proxying\"\n        echo -e \"  ${green}Все порты удалены${reset}\"\n        return\n    fi\n\n    new_ports=\"$current_ports\"\n\n    for port in $(echo \"$ports_to_del\" | tr ',' '\\n'); do\n        new_ports=$(echo \"$new_ports\" | tr ',' '\\n' | grep -vFx \"$port\" | tr '\\n' ',' | sed 's/,$//')\n    done\n\n    write_ports_file \"$file_port_proxying\" \"$new_ports\"\n\n    echo -e \"  ${green}Порты удалены${reset}\"\n}\n\nadd_ports_exclude() {\n    [ -z \"$1\" ] && {\n        echo -e \"  ${red}Ошибка${reset}: список портов не может быть пустым\"\n        return 1\n    }\n\n    if ports_conflict_check \"$file_port_proxying\" \"$file_port_exclude\"; then\n        echo -e \"  ${yellow}Внимание${reset}: вы добавляете порты исключения, но уже заданы порты проксирования\n  Приоритет у портов проксирования, порты исключения будут проигнорированы\"\n    fi\n\n    new_ports=$(normalize_ports \"$1\")\n    current_ports=$(read_ports_file \"$file_port_exclude\")\n    current_ports=$(normalize_ports \"$current_ports\")\n\n    if [ -n \"$current_ports\" ]; then\n        all_ports=$(echo \"$current_ports,$new_ports\" | tr ',' '\\n' | sort -n -u | tr '\\n' ',' | sed 's/,$//')\n    else\n        all_ports=\"$new_ports\"\n    fi\n\n    write_ports_file \"$file_port_exclude\" \"$all_ports\"\n\n    echo -e \"  ${green}Порты исключения обновлены${reset}\"\n}\n\ndell_ports_exclude() {\n    ports_to_del=$(normalize_ports \"$1\")\n    current_ports=$(read_ports_file \"$file_port_exclude\")\n\n    [ -z \"$current_ports\" ] && {\n        echo -e \"  ${yellow}Файл пуст${reset}\"\n        return\n    }\n\n    if [ -z \"$ports_to_del\" ]; then\n        > \"$file_port_exclude\"\n        echo -e \"  ${green}Все исключения удалены${reset}\"\n        return\n    fi\n\n    new_ports=\"$current_ports\"\n\n    for port in $(echo \"$ports_to_del\" | tr ',' '\\n'); do\n        new_ports=$(echo \"$new_ports\" | tr ',' '\\n' | grep -vFx \"$port\" | tr '\\n' ',' | sed 's/,$//')\n    done\n\n    write_ports_file \"$file_port_exclude\" \"$new_ports\"\n\n    echo -e \"  ${green}Порты исключения удалены${reset}\"\n}\n\n# Получить список портов проксирования\nget_ports_donor() {\n    ports=$(read_ports_file \"$file_port_proxying\")\n\n    if [ -z \"$ports\" ]; then\n        echo -e \"  Прокси-клиент работает ${yellow}на всех портах${reset}\"\n    else\n        echo \"$ports\" | tr ',' '\\n' | sed 's/^/     /'\n    fi\n}\n\n# Получить список портов, исключённых из проксирования\nget_ports_exclude() {\n    ports=$(read_ports_file \"$file_port_exclude\")\n\n    if [ -z \"$ports\" ]; then\n        echo -e \"  Нет портов исключённых из проксирования\"\n    else\n        echo \"$ports\" | tr ',' '\\n' | sed 's/^/     /'\n    fi\n}\n\nmigrate_ports_from_initd() {\n    legacy_initd=\"\"\n\n    for f in \"/opt/etc/init.d/S99xkeen\" \"/opt/etc/init.d/S24xray\"; do\n        [ -f \"$f\" ] && { legacy_initd=\"$f\"; break; }\n    done\n\n    [ -n \"$legacy_initd\" ] || return\n\n    # Читаем старые значения\n    port_donor_val=$(\n        awk -F= '/^port_donor=/{print $2; exit}' \"$legacy_initd\" | tr -d '\"'\n    )\n\n    port_exclude_val=$(\n        awk -F= '/^port_exclude=/{print $2; exit}' \"$legacy_initd\" | tr -d '\"'\n    )\n\n    port_donor_val=$(normalize_ports \"$port_donor_val\")\n    port_exclude_val=$(normalize_ports \"$port_exclude_val\")\n\n    migrated=0\n\n    # Миграция port_donor\n    if [ -n \"$port_donor_val\" ]; then\n\n        current_proxy=$(normalize_ports \"$(read_ports_file \"$file_port_proxying\")\")\n\n        combined=$(normalize_ports \"$current_proxy,$port_donor_val\")\n\n        if [ \"$combined\" != \"$current_proxy\" ]; then\n            tmpfile=$(mktemp)\n            echo \"# XKeen port proxying list (migrated)\" > \"$tmpfile\"\n            echo \"$combined\" | tr ',' '\\n' >> \"$tmpfile\"\n            mv \"$tmpfile\" \"$file_port_proxying\"\n        fi\n    fi\n\n    # Миграция port_exclude\n    if [ -n \"$port_exclude_val\" ]; then\n\n        current_exclude=$(normalize_ports \"$(read_ports_file \"$file_port_exclude\")\")\n\n        combined=$(normalize_ports \"$current_exclude,$port_exclude_val\")\n\n        if [ \"$combined\" != \"$current_exclude\" ]; then\n            tmpfile=$(mktemp)\n            echo \"# XKeen port exclude list (migrated)\" > \"$tmpfile\"\n            echo \"$combined\" | tr ',' '\\n' >> \"$tmpfile\"\n            mv \"$tmpfile\" \"$file_port_exclude\"\n        fi\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/02_tools_modules.sh",
    "content": "show_deprecation_warning() {\n    echo -e \"  ${red}Внимание!${reset} Команда устарела и удалена из XKeen\"\n    echo -e \"  Компонент '${yellow}Модули ядра подсистемы Netfilter${reset}' обязателен\"\n    echo\n}\n\nmigration_modules() {\n    show_deprecation_warning && return\n}\n\nremove_modules() {\n    show_deprecation_warning && return\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/03_tools_diagnostic.sh",
    "content": "diagnostic() {\n    # Установка пути к файлу diagnostic\n    diagnostic=\"/opt/diagnostic.txt\"\n\n    if pidof \"xray\" >/dev/null; then\n        name_client=xray\n    elif pidof \"mihomo\" >/dev/null; then\n        name_client=mihomo\n    else\n        echo\n        echo -e \"  Диагностика возможна только при работающем ${yellow}XKeen${reset}\"\n        echo -e \"  Запустите ${yellow}XKeen${reset} командой '${green}xkeen -start${reset}'\"\n        exit 1\n    fi\n\n    ip4_supported=$(ip -4 addr show | grep -q \"inet \" && echo true || echo false)\n    ip6_supported=$(ip -6 addr show | grep -q \"inet6 fe80::\" && echo true || echo false)\n\n    iptables_supported=$([ \"$ip4_supported\" = \"true\" ] && command -v iptables >/dev/null 2>&1 && echo true || echo false)\n    ip6tables_supported=$([ \"$ip6_supported\" = \"true\" ] && command -v ip6tables >/dev/null 2>&1 && echo true || echo false)\n\n    echo\n    echo \"  Выполняется диагностика. Пожалуйста, подождите...\"\n\n    # Очищаем файл diagnostic перед записью новых данных\n    > \"$diagnostic\"\n    chmod 600 \"$diagnostic\"\n\n    # Функция записи заголовка\n    write_header() {\n        echo \"-------------------------\" >> \"$diagnostic\"\n        echo -e \"$1\" >> \"$diagnostic\"\n        echo \"-------------------------\" >> \"$diagnostic\"\n        echo >> \"$diagnostic\"\n    }\n\n    # Функция логирования блоков\n    log_block() {\n        write_header \"$1\"\n        cat >> \"$diagnostic\"\n        echo >> \"$diagnostic\"; echo >> \"$diagnostic\"\n    }\n\n    # Функция маскировки чувствительных данных в конфигах Xray\n    mask_xray_sensitive_data() {\n        sed -E \\\n            -e 's/(\"(id|uuid|password|user|pass|auth|secretKey|preSharedKey)\")[[:space:]]*:[[:space:]]*\"?[^\",[:space:]]+\"?(,?)/\\1: \"***MASKED***\"\\3/g' \\\n            -e 's/(\"(address|host|serverName|sni|path|token|spiderX)\")[[:space:]]*:[[:space:]]*\"?[^\",[:space:]]+\"?(,?)/\\1: \"***MASKED***\"\\3/g' \\\n            -e 's/(\"(publicKey|privateKey|shortId|mldsa65Verify|encryption)\")[[:space:]]*:[[:space:]]*\"?[^\",[:space:]]+\"?(,?)/\\1: \"***MASKED***\"\\3/g'\n    }\n\n    # Функция маскировки чувствительных данных в конфигах Mihomo\n    mask_mihomo_sensitive_data() {\n        sed -E \\\n            -e 's/^([[:space:]]*(- )?(password|username|uuid|pre-shared-key|private-key|private-key-passphrase):).*/\\1 ***MASKED***/i' \\\n            -e 's/^([[:space:]]*(- )?(server|servername|sni|host|query-server-name|external-controller):).*/\\1 ***MASKED***/i' \\\n            -e 's/^([[:space:]]*(- )?(url|path|certificate|config|public-key|short-id|client-id|auth-str):).*/\\1 ***MASKED***/i' \\\n            -e 's/^([[:space:]]*(- )?(obfs-password|encryption|token|secret|psk):).*/\\1 ***MASKED***/i'\n    }\n\n    # Функция логирования файлов\n    log_file() {\n        local file=\"$1\"\n        local title=\"$2\"\n        if [ -f \"$file\" ]; then\n            cat \"$file\" | log_block \"$title\"\n        else\n            echo \"Файл $file не найден\" | log_block \"$title\"\n        fi\n    }\n\n    # Функция дампа iptables/ip6tables\n    dump_tables() {\n        local cmd=\"$1\"\n        local ver=\"$2\"\n        for chain in PREROUTING xkeen xkeen_out OUTPUT; do\n            $cmd -w -t nat -nvL \"$chain\" 2>&1 | log_block \"Результат таблицы NAT цепи $chain $ver\"\n            $cmd -w -t mangle -nvL \"$chain\" 2>&1 | log_block \"Результат таблицы MANGLE цепи $chain $ver\"\n        done\n        $cmd -w -t nat -nvL \"_NDM_HOTSPOT_DNSREDIR\" 2>&1 | log_block \"Результат таблицы NAT цепи _NDM_HOTSPOT_DNSREDIR $ver\"\n    }\n\n    # Сбор данных\n    write_header \"XKeen работает на ядре ${name_client}\\nи установлен ${entware_storage}\"\n\n    {\n        echo \"Поддержка IPv4 - $ip4_supported\"\n        echo \"Поддержка IPv6 - $ip6_supported\"\n        echo\n        echo \"Поддержка iptables - $iptables_supported\"\n        echo \"Поддержка ip6tables - $ip6tables_supported\"\n    } | log_block \"Доступность IPv4 и IPv6\"\n\n    [ \"$iptables_supported\" = \"true\" ] && dump_tables \"iptables\" \"IPv4\"\n    [ \"$ip6tables_supported\" = \"true\" ] && dump_tables \"ip6tables\" \"IPv6\"\n\n    if command -v ipset >/dev/null 2>&1; then\n        sets=$(ipset list -n 2>/dev/null | grep -v '^_NDM_' | grep -v '^_UPNP')\n        if [ -n \"$sets\" ]; then\n            echo \"$sets\" | {\n                total=0\n                while read -r set; do\n                    count=$(ipset save \"$set\" 2>/dev/null | grep -c '^add')\n                    printf \"%-30s %s\\n\" \"$set\" \"$count\"\n                    total=$((total + count))\n                done\n                echo\n                echo \"Всего записей во всех списках: $total\"\n            } | log_block \"Списки ipset и количество записей в каждом\"\n        fi\n    fi\n\n    log_file \"/opt/etc/ndm/netfilter.d/proxy.sh\" \"Содержимое файла /opt/etc/ndm/netfilter.d/proxy.sh\"\n\n    curl -kfsS \"localhost:79/rci/ip/http/ssl\" | jq -r '.port' | log_block \"Проверка использования SSL порта\"\n    curl -kfsS \"localhost:79/rci/show/ip/policy\" | jq -r '.[] | select(.description | ascii_downcase == \"xkeen\")' | log_block \"Данные о политике доступа\"\n    \n    ip rule show | log_block \"Результат команды ip rule show\"\n    ip route show table main | log_block \"Результат команды ip route show table main\"\n    \n    curl -kfsS \"localhost:79/rci/show/version\" | jq -r '.title, .model, .region' | log_block \"Данные из localhost:79/rci/show/version\"\n\n    {\n        if [ \"${name_client}\" = \"xray\" ]; then xray version; else mihomo -v; fi\n        echo\n        echo \"Открыто файловых дескрипторов:\"\n        ls \"/proc/$(pidof ${name_client})/fd\" | wc -l\n        echo \"Лимит файловых дескрипторов:\"\n        grep 'Max open files' \"/proc/$(pidof ${name_client})/limits\" | awk '{print $4}'\n    } | log_block \"Версия $name_client и файловые дескрипторы\"\n\n    echo \"Версия XKeen $xkeen_current_version $xkeen_build (время сборки: $build_timestamp)\" | log_block \"Версия XKeen\"\n\n    [ -f \"$xkeen_config\" ] && log_file \"$xkeen_config\" \"Файл xkeen.json\"\n\n    if [ \"${name_client}\" = \"xray\" ] && [ -d \"$xray_conf_dir\" ]; then\n        ls -p \"$xray_conf_dir\" | log_block \"Содержимое директории configs\"\n\n        for conf in dns inbounds routing outbounds; do\n            file=$(ls \"$xray_conf_dir\"/*${conf}*.json 2>/dev/null | head -n 1)\n            if [ -n \"$file\" ]; then\n                write_header \"Содержимое файла $file\"\n                mask_xray_sensitive_data < \"$file\" >> \"$diagnostic\"\n                echo >> \"$diagnostic\"; echo >> \"$diagnostic\"\n            fi\n        done\n    fi\n\n    if [ \"${name_client}\" = \"mihomo\" ]; then\n        for conf_file in \"$mihomo_conf_dir/config.yaml\" \"$mihomo_conf_dir/config.yml\"; do\n            if [ -f \"$conf_file\" ]; then\n                write_header \"Содержимое файла $conf_file\"\n                mask_mihomo_sensitive_data < \"$conf_file\" >> \"$diagnostic\"\n                echo >> \"$diagnostic\"; echo >> \"$diagnostic\"\n            fi\n        done\n    fi\n\n    echo\n    echo -e \"  Диагностика ${green}выполнена${reset}\"\n    echo -e \"  Отправьте файл '${yellow}$diagnostic${reset}' в телеграм-чат ${yellow}XKeen${reset}, подробно описав возникшую проблему\"\n    echo\n    echo -e \"  ${red}Примечание${reset}: Диагностика не проверяет доступ к прокси-серверу, правильность заполнения конфигов\"\n    echo -e \"  и настройки роутера/сервера. Она проверяет ${green}только${reset} корректность инициализации ${yellow}XKeen${reset} в роутере\"\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/04_tools_delay.sh",
    "content": "get_current_delay() {\n    awk -F= '/^[[:space:]]*start_delay=/{print $2; exit}' \"$1\" | tr -d '[:space:]\"'\n}\n\ndelay_autostart() {\n    new_delay=\"$1\"\n\n    if [ ! -f \"$initd_file\" ]; then\n        echo -e \"  ${red}Ошибка${reset}: Не найден файл автозапуска ${yellow}S05xkeen${reset}\"\n        return 1\n    fi\n\n    current_delay=$(get_current_delay \"$initd_file\")\n\n    if [ -z \"$new_delay\" ]; then\n        echo -e \"  Текущая задержка автозапуска XKeen ${yellow}${current_delay} секунд(ы)${reset}\"\n        return 0\n    fi\n\n    case \"$new_delay\" in\n        ''|*[!0-9]*)\n            echo -e \"  ${red}Ошибка${reset}\"\n            echo \"  Новая задержка должна быть числом\"\n            return 1\n        ;;\n    esac\n\n    if [ \"$current_delay\" = \"$new_delay\" ]; then\n        echo \"  Обновление задержки автозапуска XKeen не требуется\"\n        return 0\n    fi\n\n    tmpfile=$(mktemp) || return 1\n\n    awk -v d=\"$new_delay\" '\n    /^[[:space:]]*start_delay=/ && !done {\n        sub(/=.*/, \"=\" d)\n        done=1\n    }\n    {print}\n    ' \"$initd_file\" > \"$tmpfile\" && mv \"$tmpfile\" \"$initd_file\"\n\n    if [ \"$(get_current_delay \"$initd_file\")\" = \"$new_delay\" ]; then\n        echo -e \"  Установлена задержка автозапуска XKeen ${yellow}${new_delay} секунд(ы)${reset}\"\n    else\n        echo -e \"  ${red}Ошибка${reset}: не удалось обновить параметр\"\n        return 1\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/00_choice_import.sh",
    "content": "# Импорт модулей выбора пользователя\n\n# Модули выбора\n. \"$xtools_dir/05_tools_choice/01_choice_cores.sh\"\n. \"$xtools_dir/05_tools_choice/02_choice_xkeen.sh\"\n. \"$xtools_dir/05_tools_choice/03_choice_geofile.sh\"\n. \"$xtools_dir/05_tools_choice/04_choice_input.sh\"\n\n. \"$xtools_dir/05_tools_choice/05_choice_cron/00_cron_import.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/01_choice_cores.sh",
    "content": "# Запрос на добавление ядер проксирования\nchoice_add_proxy_cores() {\n    while true; do\n        echo\n        echo -e \"  Выберите ${yellow}ядро проксирования${reset} для загрузки и установки:\"\n        echo\n        echo \"     1. Xray\"\n        echo \"     2. Mihomo\"\n        echo \"     3. Xray + Mihomo\"\n        echo\n        echo \"     0. Пропустить загрузку ядра проксирования, если оно уже установлено\"\n        echo\n\n        valid_input=true\n        add_xray=false\n        add_mihomo=false\n\n        while true; do\n            read -r -p \"  Ваш выбор: \" proxy_choice\n            proxy_choice=$(echo \"$proxy_choice\" | sed 's/,/, /g')\n\n            if echo \"$proxy_choice\" | grep -qE '^[0-3]$'; then\n                break\n            else\n                echo -e \"  ${red}Некорректный ввод.${reset} Выберите один из предложенных вариантов\"\n            fi\n        done\n\n        case \"$proxy_choice\" in\n            1)\n                add_xray=true\n                ;;\n            2)\n                add_mihomo=true\n                ;;\n            3)\n                add_xray=true\n                add_mihomo=true\n                ;;\n            0)\n                add_xray=false\n                add_mihomo=false\n                ;;\n            *)\n                echo -e \"  ${red}Некорректный ввод${reset}\"\n                valid_input=false\n                ;;\n        esac\n\n        [ \"$valid_input\" = \"true\" ] && break\n    done\n}\n\n# Смена ядра проксирования на Xray\nchoice_xray_core() {  \n    command -v xray >/dev/null 2>&1 || { echo -e \"  ${red}Ошибка${reset}: Ядро Xray не установлено. Выполните установку командой ${yellow}xkeen -ux${reset}\"; exit 1; }\n    if [ -f \"$initd_file\" ]; then\n        if grep -q 'name_client=\"xray\"' $initd_file; then\n            echo -e \" Смена ядра ${red}не выполнена${reset}. Устройство уже работает на ядре ${yellow}Xray${reset}\"\n        elif grep -q 'name_client=\"mihomo\"' $initd_file; then\n            if pidof \"mihomo\" >/dev/null; then\n                $initd_file stop\n            fi\n            sed -i 's/name_client=\"mihomo\"/name_client=\"xray\"/' $initd_file\n            add_chmod_init\n            echo -e \"  ${green}Выполнена${reset} смена ядра на ${yellow}Xray${reset}\"\n            echo -e \"  Настройте конфигурацию по пути '${yellow}$xray_conf_dir/${reset}'\"\n            echo -e \"  И запустите проксирование командой ${yellow}xkeen -start${reset}\"\n        else\n            echo -e \" Произошла ${red}ошибка${reset} при смене ядра проксирования\"\n        fi\n    else\n        echo -e \"  ${red}Ошибка${reset}: Не найден файл автозапуска ${yellow}S05xkeen${reset}\"\n        return 1\n    fi\n}\n\n# Смена ядра проксирования на Mihomo\nchoice_mihomo_core() {\n    command -v mihomo >/dev/null 2>&1 || { echo -e \"  ${red}Ошибка${reset}: Ядро Mihomo не установлено. Выполните установку командой ${yellow}xkeen -um${reset}\"; exit 1; }\n    command -v yq >/dev/null 2>&1 || { echo -e \"  ${red}Ошибка${reset}: не установлен парсер конфигурационных файлов Mihomo - ${yellow}Yq${reset}\"; exit 1; }\n    if [ -f \"$initd_file\" ]; then\n        if grep -q 'name_client=\"mihomo\"' $initd_file; then\n            echo -e \" Смена ядра ${red}не выполнена${reset}. Устройство уже работает на ядре ${yellow}Mihomo${reset}\"\n        elif [ -f \"$install_dir/mihomo\" ] && [ -f \"$install_dir/yq\" ] && grep -q 'name_client=\"xray\"' $initd_file; then\n            if pidof \"xray\" >/dev/null; then\n                $initd_file stop\n            fi\n            sed -i 's/name_client=\"xray\"/name_client=\"mihomo\"/' $initd_file\n            add_chmod_init\n            echo -e \"  ${green}Выполнена${reset} смена ядра на ${yellow}Mihomo${reset}\"\n            echo -e \"  Настройте конфигурацию по пути '${yellow}$mihomo_conf_dir/${reset}'\"\n            echo -e \"  И запустите проксирование командой ${yellow}xkeen -start${reset}\"\n        else\n            echo -e \" Произошла ${red}ошибка${reset} при смене ядра проксирования\"\n        fi\n    else\n        echo -e \"  ${red}Ошибка${reset}: Не найден файл автозапуска ${yellow}S05xkeen${reset}\"\n        return 1\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/02_choice_xkeen.sh",
    "content": "# Запрос на смену канала обновлений XKeen (Stable/Dev)\nchoice_channel_xkeen() {\n    echo\n    echo -e \"  Текущий канал обновлений ${yellow}XKeen${reset}:\"\n    \n    if [ \"$xkeen_build\" = \"Stable\" ]; then\n        echo -e \"  Стабильная версия (${green}Stable${reset})\"\n        echo\n        echo \"     1. Переключиться на канал разработки\"\n        echo \"     0. Остаться на стабильной версии\"\n    else\n        echo -e \"  Версия в разработке (${green}$xkeen_build${reset})\"\n        echo\n        echo \"     1. Переключиться на стабильную версию\"\n        echo \"     0. Остаться на версии разработки\"\n    fi\n\n    echo\n    while true; do\n        read -r -p \"  Ваш выбор: \" choice\n        if echo \"$choice\" | grep -qE '^[0-1]$'; then\n            case \"$choice\" in\n                1)\n                    if [ \"$xkeen_build\" = \"Stable\" ]; then\n                        choice_build=\"Dev\"\n                    else\n                        choice_build=\"Stable\"\n                    fi\n                    return 0\n                    ;;\n                0)\n                    echo \"  Остаёмся на текущей ветке XKeen\"\n                    return 0\n                    ;;\n            esac\n        else\n            echo -e \"  ${red}Некорректный ввод${reset}\"\n        fi\n    done\n}\n\nchange_channel_xkeen() {\n    echo\n    if [ \"$choice_build\" = \"Stable\" ]; then\n        sed -i 's/^xkeen_build=\"[^\"]*\"/xkeen_build=\"Stable\"/' \"$xkeen_var_file\"\n        if grep -q '^xkeen_build=\"Stable\"$' \"$xkeen_var_file\"; then\n            echo -e \"  Канал получения обновлений ${yellow}XKeen${reset} переключен на ${green}стабильную ветку${reset}\"\n        else\n            echo -e \"  ${red}Возникла ошибка${reset} при переключении канала обновлений\"\n            unset choice_build\n        fi\n    elif [ \"$choice_build\" = \"Dev\" ]; then\n        sed -i 's/xkeen_build=\"Stable\"/xkeen_build=\"Dev\"/' $xkeen_var_file\n        if grep -q '^xkeen_build=\"Dev\"$' \"$xkeen_var_file\"; then\n            echo -e \"  Канал получения обновлений ${yellow}XKeen${reset} переключен на ${green}ветку разработки${reset}\"\n        else\n            echo -e \"  ${red}Возникла ошибка${reset} при переключении канала обновлений\"\n            unset choice_build\n        fi\n    fi\n    if [ -n \"$choice_build\" ]; then\n        echo\n        echo -e \"  Командой ${green}xkeen -uk${reset} вы можете обновить ${yellow}XKeen${reset} до последней версии в выбраной ветке\"\n    fi\n}\n\nchange_ipv6_support() {\n    ip -6 addr show 2>/dev/null | grep -q \"inet6 fe80::\" && ip6_supported=\"true\" || ip6_supported=\"false\"\n\n    if [ \"$1\" = \"on\" ]; then\n        [ \"$ip6_supported\" = \"true\" ] && return 0\n        desired_state=\"on\"\n    elif [ \"$1\" = \"off\" ]; then\n        [ \"$ip6_supported\" = \"false\" ] && return 0\n        desired_state=\"off\"\n    else\n        echo\n        echo -e \"  Текущее состояние IPv6 в ${yellow}KeeneticOS${reset}:\"\n        if [ \"$ip6_supported\" = \"true\" ]; then\n            echo -e \"  IPv6 ${green}включён${reset}\"\n            echo\n            echo \"     1. Отключить IPv6\"\n            echo \"     0. Оставить без изменений\"\n            desired_state=\"off\"\n        else\n            echo -e \"  IPv6 ${green}отключён${reset}\"\n            echo\n            echo \"     1. Включить IPv6\"\n            echo \"     0. Оставить без изменений\"\n            desired_state=\"on\"\n        fi\n\n        echo\n        while true; do\n            read -r -p \"  Ваш выбор: \" choice\n            if echo \"$choice\" | grep -qE '^[0-1]$'; then\n                case \"$choice\" in\n                    0) return 0 ;;\n                    1) break ;;\n                esac\n            else\n                echo -e \"  ${red}Некорректный ввод${reset}\"\n            fi\n        done\n    fi\n\n    if [ -f \"$initd_file\" ]; then\n        sed -i \"s/ipv6_support=\\\"[a-z]*\\\"/ipv6_support=\\\"$desired_state\\\"/\" \"$initd_file\"\n\n        if [ \"$desired_state\" = \"off\" ]; then\n            sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1\n            for dir in /proc/sys/net/ipv6/conf/*; do\n                [ -d \"$dir\" ] || continue\n                iface=\"${dir##*/}\"\n                case \"$iface\" in\n                    all|ezcfg0|t2s*) continue ;;\n                    *) [ -f \"$dir/disable_ipv6\" ] && echo \"1\" > \"$dir/disable_ipv6\" 2>/dev/null ;;\n                esac\n            done\n        else\n            sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1\n            sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1\n        fi\n\n        # Перезапуск прокси-клиента, если запущен\n        if pidof xray >/dev/null || pidof mihomo >/dev/null; then\n            echo -e \"  ${yellow}Выполняется${reset}. Пожалуйста, подождите...\"\n            \"$initd_file\" restart on >/dev/null 2>&1\n        fi\n\n        # Проверка и вывод результата\n        if [ \"$desired_state\" = \"off\" ]; then\n            if ! ip -6 addr show 2>/dev/null | grep -q \"inet6 fe80::\"; then\n                echo -e \"  Поддержка IPv6 в KeeneticOS ${green}отключена${reset}\"\n                echo -e \"  ${red}Дополнительно убедитесь, что IPv6 отключен в веб-интерфейсе роутера${reset}\"\n            else\n                echo -e \"  ${red}Ошибка${reset} при выключении IPv6\"\n            fi\n        else\n            if [ \"$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null)\" -eq 0 ]; then\n                echo -e \"  Поддержка IPv6 в KeeneticOS ${green}включена${reset}\"\n            else\n                echo -e \"  ${red}Ошибка${reset} при включении IPv6\"\n            fi\n        fi\n    else\n        echo -e \"  ${red}Ошибка${reset}: Не найден файл автозапуска ${yellow}S05xkeen${reset}\"\n        return 1\n    fi\n}\n\nchoice_backup_xkeen() {\n    [ -f \"$initd_file\" ] || return 1\n    backup_value=$(awk -F= '/^[[:space:]]*backup[[:space:]]*=/ { gsub(/\"| /,\"\",$2); print tolower($2); exit }' \"$initd_file\")\n    [ \"$backup_value\" = \"off\" ]\n}\n\nchoice_autostart_xkeen() {\n    if [ -f \"$initd_file\" ] && grep -q 'start_auto=\"off\"' \"$initd_file\"; then\n        return 1\n    fi\n\n    if choice_menu \\\n        \"Добавить ${yellow}XKeen${reset} в автозагрузку при включении роутера?\" \\\n        \"Да\" \\\n        \"Нет\"; then\n        echo -e \"  Автозагрузка XKeen ${green}включена${reset}\"\n        return 0\n    else\n        bypass_autostart_msg=\"yes\"\n        change_autostart_xkeen\n        unset bypass_autostart_msg\n        return 0\n    fi\n}\n\nchoice_redownload_xkeen() {\n    if choice_menu \\\n        \"Выберите вариант переустановки ${yellow}XKeen${reset}\" \\\n        \"Загрузить дистрибутив XKeen из интернета\" \\\n        \"Локальная переустановка XKeen\"; then\n        redownload_xkeen=\"yes\"\n    fi\n}\n\nchoice_remove() {\n    if choice_menu \\\n        \"Вы действительно хотите ${red}удалить ${choice_for_remove}${reset}?\" \\\n        \"Да, хочу удалить\" \\\n        \"Нет, передумал(а)\"; then\n        return 0\n    else\n        exit 0\n    fi\n}\n\ncheck_file_descriptors() {\n    pid=\"\"\n    if pid=$(pidof xray | awk '{print $1}') && [ -n \"$pid\" ]; then\n        name_client=\"xray\"\n    elif pid=$(pidof mihomo | awk '{print $1}') && [ -n \"$pid\" ]; then\n        name_client=\"mihomo\"\n    else\n        echo -e \"\\n  Команда работает только при работающем ${yellow}XKeen${reset}\"\n        return 1\n    fi\n\n    fd_count=$(ls /proc/\"$pid\"/fd | wc -l)\n\n    maxfd=$(grep 'Max open files' \"/proc/$pid/limits\" | awk '{print $4}')\n\n    echo -e \"\\n  Прокси-клиент ${light_blue}$name_client${reset} открыл файловых дескрипторов - ${green}$fd_count${reset}\"\n    echo -e \"  Лимит файловых дескрипторов для вашего роутера  - ${green}$maxfd${reset}\"\n    echo -e \"\\n  При высоких значениях открытых файловых дескрипторов,\"\n    echo -e \"  можете включить их контроль командой ${yellow}xkeen -fd${reset}\"\n}\n\nwarn_proxy_dns() {\n    echo\n    echo -e \"  ${red}Внимание!${reset} Значение данного параметра без соответствующих настроек прокси-клиента ${green}игнорируется${reset}\"\n}\n\nchange_proxy_dns() {\n    toggle_param \"proxy_dns\" \"перехвата DNS\" \"restart\" \"$1\"\n}\n\nchange_autostart_xkeen() {\n    toggle_param \"start_auto\" \"автозапуска XKeen\" \"none\" \"$1\"\n}\n\nchange_file_descriptors() {\n    toggle_param \"check_fd\" \"контроля файловых дескрипторов\" \"restart\" \"$1\"\n}\n\nchange_proxy_router() {\n    toggle_param \"proxy_router\" \"проксирования трафика Entware\" \"restart\" \"$1\"\n}\n\nchange_extended_msg() {\n    toggle_param \"extended_msg\" \"расширенных сообщений при запуcке XKeen\" \"none\" \"$1\"\n}\n\nchange_backup_xkeen() {\n    toggle_param \"backup\" \"резервного копирования XKeen при обновлении\" \"none\" \"$1\"\n}\n\nchange_aghfix_xkeen() {\n    toggle_param \"aghfix\" \"отображения клиентов XKeen под своими IP в журнале AaGuard Home\" \"restart\" \"$1\"\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/03_choice_geofile.sh",
    "content": "choice_geodata() {\n    type=\"$1\"\n    type_name=\"$2\"\n    src3=\"$3\"\n    src3_name=\"$4\"\n    var_bypass=\"$5\"\n\n    has_missing_bases=false\n    has_updatable_bases=false\n\n    for source in refilter v2fly \"$src3\"; do\n        var=\"update_${source}_${type}\"\n        msg_var=\"update_${source}_${type}_msg\"\n\n        if [ \"$(eval echo \\$$var)\" = \"false\" ]; then\n            has_missing_bases=true\n        else\n            eval \"$msg_var=true\"\n            has_updatable_bases=true\n        fi\n    done\n\n    while true; do\n        eval \"install_refilter_${type}=false\"\n        eval \"install_v2fly_${type}=false\"\n        eval \"install_${src3}_${type}=false\"\n        eval \"update_refilter_${type}=false\"\n        eval \"update_v2fly_${type}=false\"\n        eval \"update_${src3}_${type}=false\"\n        eval \"choice_delete_${type}_refilter_select=false\"\n        eval \"choice_delete_${type}_v2fly_select=false\"\n        eval \"choice_delete_${type}_${src3}_select=false\"\n        invalid_choice=false\n\n        echo \n        echo -e \"  Выберите номер или номера действий через пробел для ${yellow}${type_name}${reset}\"\n        echo \n\n        [ \"$has_missing_bases\" = true ] && echo \"     1. Установить отсутствующие и обновить установленные ${type_name}\" || echo -e \"     1. ${italic}Все доступные ${type_name} установлены${reset}\"\n        [ \"$has_updatable_bases\" = true ] && echo \"     2. Обновить установленные ${type_name}\" || echo -e \"     2. ${italic}Нет доступных ${type_name} для обновления${reset}\"\n\n        [ \"$(eval echo \\$update_refilter_${type}_msg)\" = \"true\" ] && refilter_choice=\"Обновить\" || refilter_choice=\"Установить\"\n        [ \"$(eval echo \\$update_v2fly_${type}_msg)\" = \"true\" ] && v2fly_choice=\"Обновить\" || v2fly_choice=\"Установить\"\n        [ \"$(eval echo \\$update_${src3}_${type}_msg)\" = \"true\" ] && src3_choice=\"Обновить\" || src3_choice=\"Установить\"\n\n        echo \"     3. $refilter_choice Re:filter\"\n        echo \"     4. $v2fly_choice v2fly\"\n        echo \"     5. $src3_choice ${src3_name}\"\n        echo \n        echo \"     0. Пропустить\"\n\n        [ \"$has_updatable_bases\" = true ] && echo && echo \"     6. Удалить установленные ${type_name}\"\n\n        echo\n        valid_input=true\n\n        while true; do\n            read -r -p \"  Ваш выбор: \" data_choices\n            data_choices=$(echo \"$data_choices\" | sed 's/,/, /g')\n\n            if echo \"$data_choices\" | grep -qE '^[0-6 ]+$'; then\n                break\n            else\n                echo -e \"  ${red}Некорректный ввод.${reset} Пожалуйста, выберите снова\"\n            fi\n        done\n\n        for choice in $data_choices; do\n            case \"$choice\" in\n                1)\n                    if [ \"$has_missing_bases\" = \"false\" ]; then\n                        echo -e \"  Все ${type_name} ${green}уже установлены${reset}\"\n                        if input_concordance_list \"Вы хотите обновить их?\"; then\n                            eval \"update_refilter_${type}=true\"\n                            eval \"update_v2fly_${type}=true\"\n                            eval \"update_${src3}_${type}=true\"\n                        else\n                            invalid_choice=true\n                        fi\n                    else\n                        [ \"$(eval echo \\$update_refilter_${type}_msg)\" != \"true\" ] && eval \"install_refilter_${type}=true\"\n                        [ \"$(eval echo \\$update_v2fly_${type}_msg)\" != \"true\" ] && eval \"install_v2fly_${type}=true\"\n                        [ \"$(eval echo \\$update_${src3}_${type}_msg)\" != \"true\" ] && eval \"install_${src3}_${type}=true\"\n\n                        [ \"$(eval echo \\$update_refilter_${type}_msg)\" = \"true\" ] && eval \"update_refilter_${type}=true\"\n                        [ \"$(eval echo \\$update_v2fly_${type}_msg)\" = \"true\" ] && eval \"update_v2fly_${type}=true\"\n                        [ \"$(eval echo \\$update_${src3}_${type}_msg)\" = \"true\" ] && eval \"update_${src3}_${type}=true\"\n                    fi\n                    ;;\n                2)\n                    if [ \"$has_updatable_bases\" = \"false\" ]; then\n                        echo -e \"  ${red}Нет установленных ${type_name}${reset} для обновления\"\n                        if input_concordance_list \"Вы хотите установить их?\"; then\n                            eval \"install_refilter_${type}=true\"\n                            eval \"install_v2fly_${type}=true\"\n                            eval \"install_${src3}_${type}=true\"\n                        else\n                            invalid_choice=true\n                        fi\n                    else\n                        [ \"$(eval echo \\$update_refilter_${type}_msg)\" = \"true\" ] && eval \"update_refilter_${type}=true\"\n                        [ \"$(eval echo \\$update_v2fly_${type}_msg)\" = \"true\" ] && eval \"update_v2fly_${type}=true\"\n                        [ \"$(eval echo \\$update_${src3}_${type}_msg)\" = \"true\" ] && eval \"update_${src3}_${type}=true\"\n                    fi\n                    ;;\n                3)\n                    [ \"$(eval echo \\$update_refilter_${type}_msg)\" != \"true\" ] && eval \"install_refilter_${type}=true\" || eval \"update_refilter_${type}=true\"\n                    ;;\n                4)\n                    [ \"$(eval echo \\$update_v2fly_${type}_msg)\" != \"true\" ] && eval \"install_v2fly_${type}=true\" || eval \"update_v2fly_${type}=true\"\n                    ;;\n                5)\n                    [ \"$(eval echo \\$update_${src3}_${type}_msg)\" != \"true\" ] && eval \"install_${src3}_${type}=true\" || eval \"update_${src3}_${type}=true\"\n                    ;;\n                6)\n                    if [ \"$has_updatable_bases\" = \"false\" ]; then\n                        echo -e \"  ${red}Нет установленных ${type_name} для удаления${reset}. Выберите другой пункт\"\n                        invalid_choice=true\n                    else\n                        eval \"choice_delete_${type}_refilter_select=true\"\n                        eval \"choice_delete_${type}_v2fly_select=true\"\n                        eval \"choice_delete_${type}_${src3}_select=true\"\n                    fi\n                    ;;\n                0)\n                    echo \"  Выполнен пропуск установки / обновления ${type_name}\"\n                    if [ \"$has_updatable_bases\" = \"true\" ]; then\n                        eval \"$var_bypass=false\"\n                    else\n                        eval \"$var_bypass=true\"\n                    fi\n                    return\n                    ;;\n\n                *)\n                    echo -e \"  ${red}Некорректный ввод.${reset} Пожалуйста, выберите снова\"\n                    invalid_choice=true\n                    ;;\n            esac\n        done\n\n        [ \"$invalid_choice\" = true ] && continue\n\n        install_list=\"\"\n        update_list=\"\"\n        delete_list=\"\"\n\n        [ \"$(eval echo \\$install_refilter_${type})\" = \"true\" ] && install_list=\"$install_list ${yellow}Re:filter${reset},\"\n        [ \"$(eval echo \\$install_v2fly_${type})\" = \"true\" ] && install_list=\"$install_list ${yellow}v2fly${reset},\"\n        [ \"$(eval echo \\$install_${src3}_${type})\" = \"true\" ] && install_list=\"$install_list ${yellow}${src3_name}${reset},\"\n\n        [ \"$(eval echo \\$update_refilter_${type})\" = \"true\" ] && update_list=\"$update_list ${yellow}Re:filter${reset},\"\n        [ \"$(eval echo \\$update_v2fly_${type})\" = \"true\" ] && update_list=\"$update_list ${yellow}v2fly${reset},\"\n        [ \"$(eval echo \\$update_${src3}_${type})\" = \"true\" ] && update_list=\"$update_list ${yellow}${src3_name}${reset},\"\n\n        [ \"$(eval echo \\$choice_delete_${type}_refilter_select)\" = \"true\" ] && delete_list=\"$delete_list ${yellow}Re:filter${reset},\"\n        [ \"$(eval echo \\$choice_delete_${type}_v2fly_select)\" = \"true\" ] && delete_list=\"$delete_list ${yellow}v2fly${reset},\"\n        [ \"$(eval echo \\$choice_delete_${type}_${src3}_select)\" = \"true\" ] && delete_list=\"$delete_list ${yellow}${src3_name}${reset},\"\n\n        if [ -n \"$install_list\" ]; then\n            echo -e \"  Устанавливаются следующие ${type_name}: ${install_list%,}\"\n        fi\n\n        if [ -n \"$update_list\" ]; then\n            echo -e \"  Обновляются следующие ${type_name}: ${update_list%,}\"\n        fi\n\n        if [ -n \"$delete_list\" ]; then\n            echo -e \"  Удаляются следующие ${type_name}: ${delete_list%,}\"\n        fi\n\n        break\n    done\n\n    if [ -z \"$install_list\" ] && [ -z \"$update_list\" ] && [ -z \"$delete_list\" ]; then\n        eval \"$var_bypass=true\"\n    else\n        eval \"$var_bypass=false\"\n    fi\n}\n\nchoice_geosite() {\n    choice_geodata \"geosite\" \"GeoSite\" \"zkeen\" \"ZKeen\" \"bypass_cron_geosite\"\n}\n\nchoice_geoip() {\n    choice_geodata \"geoip\" \"GeoIP\" \"zkeenip\" \"ZKeenIP\" \"bypass_cron_geoip\"\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/04_choice_input.sh",
    "content": "# Функция для выбора пользователя между \"Да\" и \"Нет\" с номерами 0 и 1\ninput_concordance_list() {\n    prompt_message=\"  $1\"\n    error_message=\"  ${yellow}Пожалуйста, выберите вариант, введя номер 0 (Нет) или 1 (Да)${reset}\"\n\n    echo\n    echo -e \"$prompt_message\"\n    echo \"     0. Нет\"\n    echo \"     1. Да\"\n\n    while true; do\n        echo\n        read -r -p \"  Введите номер: \" user_input\n\n        case \"$user_input\" in\n            0) return 1 ;;\n            1) return 0 ;;\n            *)\n                echo\n                echo -e \"  $error_message\"\n                continue\n                ;;\n        esac\n    done\n}\n\ntoggle_param() {\n    param=\"$1\"\n    description=\"$2\"\n    restart_needed=\"$3\"\n    force_state=\"$4\"\n\n    echo\n    if [ ! -f \"$initd_file\" ]; then\n        echo -e \"  ${red}Ошибка${reset}: Не найден файл ${yellow}S05xkeen${reset}\"\n        return 1\n    fi\n\n    current_state=$(grep -m 1 -E \"^[[:space:]]*$param=\" \"$initd_file\" | cut -d'=' -f2 | tr -d '\"[:space:]')\n\n    if [ \"$force_state\" = \"on\" ] || [ \"$force_state\" = \"off\" ]; then\n        if [ \"$current_state\" = \"$force_state\" ]; then\n            if [ \"$current_state\" = \"on\" ]; then\n                echo -e \"  Состояние ${description} уже ${green}включено${reset}\"\n            else\n                echo -e \"  Состояние ${description} уже ${red}отключено${reset}\"\n            fi\n            [ \"$apply\" = \"restart\" ] && echo\n            return 0\n        fi\n        desired_state=\"$force_state\"\n    elif [ \"$bypass_autostart_msg\" = \"yes\" ]; then\n        if [ \"$current_state\" = \"on\" ]; then\n            desired_state=\"off\"\n        else\n            desired_state=\"on\"\n        fi\n    else\n        echo -e \"  Текущее состояние ${description}:\"\n\n        if [ \"$current_state\" = \"on\" ]; then\n            echo -e \"  ${green}Включено${reset}\"\n            echo\n            echo \"     1. Отключить\"\n            echo \"     0. Оставить без изменений\"\n            desired_state=\"off\"\n        else\n            echo -e \"  ${red}Отключено${reset}\"\n            echo\n            echo \"     1. Включить\"\n            echo \"     0. Оставить без изменений\"\n            desired_state=\"on\"\n        fi\n\n        echo\n        while true; do\n            read -r -p \"  Ваш выбор: \" choice\n            case \"$choice\" in\n                0) return 0 ;;\n                1) break ;;\n                *) echo -e \"  ${red}Некорректный ввод${reset}\" ;;\n            esac\n        done\n    fi\n\n    if awk -v param=\"$param\" -v value=\"$desired_state\" '\n        !found && $0 ~ \"^[[:space:]]*\" param \"=\" {\n            sub(/\"[^\"]*\"/, \"\\\"\" value \"\\\"\")\n            found=1\n        }\n        {print}\n    ' \"$initd_file\" > \"$initd_file.tmp\" && mv \"$initd_file.tmp\" \"$initd_file\"; then\n\n        [ \"$bypass_autostart_msg\" = \"yes\" ] && return 0\n\n        if [ \"$desired_state\" = \"on\" ]; then\n            echo -e \"  Новое состояние ${description} ${green}включено${reset}\"\n        else\n            echo -e \"  Новое состояние ${description} ${red}отключено${reset}\"\n        fi\n\n        if [ \"$restart_needed\" = \"reboot\" ]; then\n            echo\n            echo -e \"  ${yellow}Перезагрузите роутер для применения изменений${reset}\"\n        elif [ \"$restart_needed\" = \"restart\" ] && [ \"$apply\" != \"restart\" ]; then\n            echo\n            echo -e \"  ${yellow}Перезапустите XKeen для применения изменений${reset}\"\n        fi\n\n        add_chmod_init\n    else\n        echo\n        echo -e \"  ${red}Ошибка${reset} при изменении параметра $param\"\n        return 1\n    fi\n}\n\nchoice_menu() {\n    title=\"$1\"\n    option_yes=\"$2\"\n    option_no=\"$3\"\n\n    echo\n    [ -n \"$title\" ] && echo -e \"  $title\"\n    echo\n    echo \"     1. $option_yes\"\n    echo \"     0. $option_no\"\n    echo\n\n    while true; do\n        read -r -p \"  Ваш выбор: \" choice\n        case \"$choice\" in\n            1) return 0 ;;\n            0) return 1 ;;\n            *) echo -e \"  ${red}Некорректный ввод${reset}\" ;;\n        esac\n    done\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/05_choice_cron/00_cron_import.sh",
    "content": "# Импорт модулей вопросов cron\n\n# Модули вопросов cron\n. \"$xtools_dir/05_tools_choice/05_choice_cron/01_cron_status.sh\"\n. \"$xtools_dir/05_tools_choice/05_choice_cron/02_cron_time.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/05_choice_cron/01_cron_status.sh",
    "content": "# Определение статуса для задач cron\nget_existing_cron_time() {\n    crontab -l 2>/dev/null | grep 'xkeen -ug' | head -n1 | awk '{print $1,$2,$3,$4,$5}'\n}\n\nformat_cron_time() {\n    cron=\"$1\"\n\n    minute=$(echo \"$cron\" | awk '{print $1}')\n    hour=$(echo \"$cron\" | awk '{print $2}')\n    dow=$(echo \"$cron\" | awk '{print $5}')\n\n    formatted_hour=$(printf \"%02d\" \"$hour\")\n    formatted_minute=$(printf \"%02d\" \"$minute\")\n\n    case \"$dow\" in\n        \"*\") day=\"Ежедневно\" ;;\n        1) day=\"Понедельник\" ;;\n        2) day=\"Вторник\" ;;\n        3) day=\"Среда\" ;;\n        4) day=\"Четверг\" ;;\n        5) day=\"Пятница\" ;;\n        6) day=\"Суббота\" ;;\n        0) day=\"Воскресенье\" ;;\n        *) day=\"Неизвестно\" ;;\n    esac\n\n    echo \"$day в $formatted_hour:$formatted_minute\"\n}\n\nchoice_update_cron() {\n    has_updatable_cron_tasks=false\n    [ \"$info_update_geofile_cron\" = \"installed\" ] && has_updatable_cron_tasks=true\n\n    existing_cron=$(get_existing_cron_time)\n    \n    if [ -n \"$existing_cron\" ]; then\n        echo\n        echo -e \"  Время обновления ${yellow}геофайлов${reset} установлено на: ${green}$(format_cron_time \"$existing_cron\")${reset}\"\n    fi\n\n    while true; do\n        choice_cancel_cron_select=false\n        choice_geofile_cron_select=false\n        choice_delete_all_cron_select=false\n        invalid_choice=false\n\n        echo\n        echo -e \"  Выберите номер действия для автообновления ${yellow}GeoFile/GeoIPSET${reset}\"\n        echo\n\n        [ \"$info_update_geofile_cron\" != \"installed\" ] && geofile_choice=\"Включить\" || geofile_choice=\"Обновить\"\n        echo \"     1. $geofile_choice задачу\"\n        echo \"     0. Пропустить\"\n\n        [ \"$has_updatable_cron_tasks\" = true ] && echo && echo \"     2. Выключить автообновление\"\n        echo\n\n        while true; do\n            read -r -p \"  Ваш выбор: \" update_choices\n            update_choices=$(echo \"$update_choices\" | sed 's/,/, /g')\n\n            if echo \"$update_choices\" | grep -qE '^[0-2]$'; then\n                break\n            else\n                echo -e \"  ${red}Некорректный ввод.${reset} Выберите один из предложенных вариантов\"\n            fi\n        done\n\n        for choice in $update_choices; do\n            case \"$choice\" in\n                1)\n                    choice_geofile_cron_select=true\n                    if [ \"$info_update_geofile_cron\" = \"installed\" ]; then\n                        echo -e \"  ${yellow}Будет выполнено${reset} обновление задачи GeoFile/GeoIPSET\"\n                    else\n                        echo -e \"  ${yellow}Будет выполнено${reset} включение задачи GeoFile/GeoIPSET\"\n                    fi\n                    ;;\n                0)\n                    choice_cancel_cron_select=true\n                    echo \"  Выполнен пропуск настройки автообновления\"\n                    return\n                    ;;\n                2)\n                    if [ \"$has_updatable_cron_tasks\" = true ]; then\n                        delete_cron_geofile\n                        echo -e \"  Автообновление баз GeoFile/GeoIPSET ${green}выключено${reset}\"\n                    else\n                        echo -e \"  ${red}Автообновление баз GeoFile/GeoIPSET не включено${reset}. Выберите другой пункт\"\n                        invalid_choice=true\n                    fi\n                    ;;\n                *)\n                    echo -e \"  ${red}Некорректный ввод${reset}\"\n                    invalid_choice=true\n                    ;;\n            esac\n        done\n\n        [ \"$invalid_choice\" = true ] || break\n    done\n}\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/05_choice_cron/02_cron_time.sh",
    "content": "# Определение времени для задач cron\nchoice_cron_time() {\n    [ \"$choice_geofile_cron_select\" = true ] || return\n\n    echo\n    echo -e \"  Время автоматического обновления ${yellow}геофайлов${reset}:\"\n    echo\n    echo \"  Выберите день\"\n    echo \"     0. Отмена\"\n    echo \"     1. Понедельник\"\n    echo \"     2. Вторник\"\n    echo \"     3. Среда\"\n    echo \"     4. Четверг\"\n    echo \"     5. Пятница\"\n    echo \"     6. Суббота\"\n    echo \"     7. Воскресенье\"\n    echo \"     8. Ежедневно\"\n    echo\n\n    while :; do\n        read -r -p \"  Ваш выбор: \" day_choice\n        echo \"$day_choice\" | grep -qE '^[0-8]$' && break\n        echo -e \"  ${red}Некорректный номер действия.${reset} Пожалуйста, выберите снова\"\n    done\n\n    [ \"$day_choice\" -eq 0 ] && {\n        echo -e \"  Включение автоматического обновления ${yellow}геофайлов${reset} отменено.\"\n        return\n    }\n\n    echo\n\n    while :; do\n        read -r -p \"  Выберите час (0-23): \" hour\n        case \"$hour\" in\n            ''|*[!0-9]*) ;;\n            *) [ \"$hour\" -ge 0 ] && [ \"$hour\" -le 23 ] && break ;;\n        esac\n        echo -e \"  ${red}Некорректный час.${reset} Пожалуйста, попробуйте снова\"\n    done\n\n    while :; do\n        read -r -p \"  Выберите минуту (0-59): \" minute\n        case \"$minute\" in\n            ''|*[!0-9]*) ;;\n            *) [ \"$minute\" -ge 0 ] && [ \"$minute\" -le 59 ] && break ;;\n        esac\n        echo -e \"  ${red}Некорректные минуты.${reset} Пожалуйста, попробуйте снова\"\n    done\n\n    if [ \"$day_choice\" -eq 8 ]; then\n        cron_expression=\"$minute $hour * * *\"\n        day_name=\"Ежедневно\"\n    else\n        case \"$day_choice\" in\n            1) dow=1; day_name=\"Понедельник\" ;;\n            2) dow=2; day_name=\"Вторник\" ;;\n            3) dow=3; day_name=\"Среда\" ;;\n            4) dow=4; day_name=\"Четверг\" ;;\n            5) dow=5; day_name=\"Пятница\" ;;\n            6) dow=6; day_name=\"Суббота\" ;;\n            7) dow=0; day_name=\"Воскресенье\" ;;\n        esac\n        cron_expression=\"$minute $hour * * $dow\"\n    fi\n\n    formatted_hour=$(printf \"%02d\" \"$hour\")\n    formatted_minute=$(printf \"%02d\" \"$minute\")\n\n    echo\n    echo -e \"  Выбранное время обновления ${yellow}геофайлов${reset}: $day_name в $formatted_hour:$formatted_minute\"\n\n    choice_geofile_cron_time=\"$cron_expression\"\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/00_backups_import.sh",
    "content": "# Импорт модулей резервного копирования\n\n# Модули резервного копирования\n. \"$xtools_dir/06_tools_backups/01_backups_xkeen.sh\"\n. \"$xtools_dir/06_tools_backups/02_backups_configs_xray.sh\"\n. \"$xtools_dir/06_tools_backups/02_backups_configs_mihomo.sh\""
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/01_backups_xkeen.sh",
    "content": "# Создание резервной копии XKeen\nbackup_xkeen() {\n    if choice_backup_xkeen && [ -z \"$manual_backup\" ]; then\n        return 0\n    fi\n\n    backup_filename=\"${current_datetime}_xkeen_v${xkeen_current_version}\"\n    backup_dir=\"${backups_dir}/${backup_filename}\"\n    mkdir -p \"$backup_dir\"\n\n    # Копирование файлов. Проверяем успех всей операции копирования\n    if cp -r \"$install_dir/.xkeen\" \"$install_dir/xkeen\" \"$backup_dir/\"; then\n        # Переименование скрытой директории для удобства хранения\n        mv \"$backup_dir/.xkeen\" \"$backup_dir/_xkeen\"\n        echo -e \"  Резервная копия XKeen создана: ${yellow}${backup_filename}${reset}\"\n    else\n        echo -e \"  ${red}Ошибка${reset} при создании резервной копии XKeen\"\n    fi\n}\n\n# Восстановление XKeen из резервной копии\nrestore_backup_xkeen() {\n    latest_backup_dir=\"\"\n\n    for entry in \"$backups_dir\"/*xkeen*; do\n        if [ -d \"$entry\" ]; then\n            latest_backup_dir=\"$entry\"\n        fi\n    done\n\n    if [ -n \"$latest_backup_dir\" ]; then\n        # Используем временную директорию для безопасности при восстановлении\n        # Чтобы не удалить старый .xkeen, пока не убедимся, что копия цела\n\n        if cp -r \"$latest_backup_dir/_xkeen\" \"$install_dir/\" && \\\n           cp -f \"$latest_backup_dir/xkeen\" \"$install_dir/\"; then\n\n            rm -rf \"${install_dir:?}/.xkeen\"\n            mv \"$install_dir/_xkeen\" \"$install_dir/.xkeen\"\n            \n            echo -e \"  XKeen ${green}успешно восстановлен${reset} из: $(basename \"$latest_backup_dir\")\"\n        else\n            echo -e \"  ${red}Ошибка:${reset} Не удалось скопировать файлы из резервной копии\"\n        fi\n    else\n        echo -e \"  ${red}Ошибка:${reset} Подходящая резервная копия XKeen не найдена\"\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/02_backups_configs_mihomo.sh",
    "content": "backup_configs_mihomo() {\n    backup_filename=\"${current_datetime}_configs_mihomo\"\n    backup_configs_dir=\"$backups_dir/$backup_filename\"\n    mkdir -p \"$backup_configs_dir\"\n\n    if cp -r \"$mihomo_conf_dir\"/* \"$backup_configs_dir/\"; then\n        echo -e \"  Резервная копия конфигурации Mihomo создана: ${yellow}$backup_filename${reset}\"\n    else\n        echo -e \"  ${red}Ошибка${reset} при создании резервной копии конфигураций Mihomo\"\n    fi\n}\n\nrestore_backup_configs_mihomo() {\n    latest_backup=\"\"\n\n    for entry in \"$backups_dir\"/*_configs_mihomo; do\n        if [ -e \"$entry\" ]; then\n            latest_backup=\"$entry\"\n        fi\n    done\n\n    if [ -n \"$latest_backup\" ]; then\n        rm -rf \"${mihomo_conf_dir:?}\"/*\n\n        if cp -r \"$latest_backup\"/* \"$mihomo_conf_dir/\"; then\n            echo -e \"  Конфигурация Mihomo ${green}успешно восстановлена${reset} из: $(basename \"$latest_backup\")\"\n        else\n            echo -e \"  ${red}Ошибка${reset} при восстановлении файлов\"\n        fi\n    else\n        echo -e \"  ${red}Ошибка:${reset} Резервные копии не найдены в $backups_dir\"\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/02_backups_configs_xray.sh",
    "content": "backup_configs_xray() {\n    backup_filename=\"${current_datetime}_configs_xray\"\n    backup_configs_dir=\"$backups_dir/$backup_filename\"\n    mkdir -p \"$backup_configs_dir\"\n\n    if cp -r \"$xray_conf_dir\"/* \"$backup_configs_dir/\"; then\n        echo -e \"  Резервная копия конфигурации Xray создана: ${yellow}$backup_filename${reset}\"\n    else\n        echo -e \"  ${red}Ошибка${reset} при создании резервной копии конфигураций Xray\"\n    fi\n}\n\nrestore_backup_configs_xray() {\n    latest_backup=\"\"\n\n    for entry in \"$backups_dir\"/*_configs_xray; do\n        if [ -e \"$entry\" ]; then\n            latest_backup=\"$entry\"\n        fi\n    done\n\n    if [ -n \"$latest_backup\" ]; then\n        rm -rf \"${xray_conf_dir:?}\"/*\n\n        if cp -r \"$latest_backup\"/* \"$xray_conf_dir/\"; then\n            echo -e \"  Конфигурация Xray ${green}успешно восстановлена${reset} из: $(basename \"$latest_backup\")\"\n        else\n            echo -e \"  ${red}Ошибка${reset} при восстановлении файлов\"\n        fi\n    else\n        echo -e \"  ${red}Ошибка:${reset} Резервные копии не найдены в $backups_dir\"\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/00_downloaders_import.sh",
    "content": "# Импорт модулей загрузки\n\n# fetch_with_mirrors / probe_with_mirrors уже подгружены из xkeen/import.sh\n# (раньше install-модуля, который тоже их вызывает).\n\n# Модули загрузки\n. \"$xtools_dir/07_tools_downloaders/01_downloaders_xray.sh\"\n. \"$xtools_dir/07_tools_downloaders/01_downloaders_mihomo.sh\"\n. \"$xtools_dir/07_tools_downloaders/02_donwloaders_xkeen.sh\""
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/00_fetch_with_mirrors.sh",
    "content": "# Загрузка с per-call mirror-fallback'ом.\n#\n# Заменяет паттерн \"test_github -> один gh_proxy на сессию -> один curl\n# без fallback'а\". Старый flow ломается когда выбранный mirror транзиентно\n# падает между test_github и фактической загрузкой: curl получает 5xx или\n# таймаут, caller тихо return 1, geo-файлы устаревают.\n#\n# fetch_with_mirrors пробует префиксы по очереди (gh_proxy_user\n# exclusive, иначе direct + gh_proxy1 + gh_proxy2), кэширует удачный\n# выбор на TTL_SEC, валидирует ответ (HTTP-код + min-size + HTML-stub\n# detect). После неудачного вызова caller может прочитать причину из\n# глобальных переменных _last_error / _last_size (для fetch) и\n# _last_http (для probe), чтобы напечатать осмысленное сообщение.\n\n_mirror_cache=\"/tmp/.xkeen_mirror_cache\"\n_mirror_ttl=60\n_DIRECT_TOKEN=\"__direct__\"\n\n# Чтение закэшированного префикса. stdout = префикс (\"\" для direct),\n# rc = 0 если кэш свежий, 1 если просрочен/garbage/отсутствует.\n_mirror_cache_read() {\n    [ -r \"$_mirror_cache\" ] || return 1\n    _cache_ts=\"\"\n    _cache_pfx=\"\"\n    IFS=' ' read -r _cache_ts _cache_pfx < \"$_mirror_cache\" 2>/dev/null || return 1\n    case \"$_cache_ts\" in\n        ''|*[!0-9]*) return 1 ;;\n    esac\n    _cache_now=$(date +%s 2>/dev/null) || return 1\n    [ $((_cache_now - _cache_ts)) -lt \"$_mirror_ttl\" ] || return 1\n    [ \"$_cache_pfx\" = \"$_DIRECT_TOKEN\" ] && _cache_pfx=\"\"\n    printf '%s' \"$_cache_pfx\"\n    return 0\n}\n\n# Сохранение удачного префикса в кэш. $1 = \"\" для direct, иначе url.\n_mirror_cache_write() {\n    _w_pfx=\"$1\"\n    [ -z \"$_w_pfx\" ] && _w_pfx=\"$_DIRECT_TOKEN\"\n    printf '%s %s\\n' \"$(date +%s)\" \"$_w_pfx\" > \"$_mirror_cache\" 2>/dev/null\n}\n\n# Список префиксов для попыток, по одному на строку, в порядке приоритета.\n# Используется token __direct__ для direct GitHub (пустая строка ломала\n# бы heredoc-итерацию).\n_mirror_order() {\n    if [ -n \"$gh_proxy_user\" ]; then\n        printf '%s\\n' \"${gh_proxy_user%/}\"\n        return\n    fi\n    _order_cached_set=0\n    if _order_cached=$(_mirror_cache_read); then\n        _order_cached_set=1\n        printf '%s\\n' \"${_order_cached:-$_DIRECT_TOKEN}\"\n    fi\n    # direct и дефолтные mirror'ы, пропуская тот что уже в кэше\n    if [ \"$_order_cached_set\" = \"0\" ] || [ -n \"$_order_cached\" ]; then\n        printf '%s\\n' \"$_DIRECT_TOKEN\"\n    fi\n    if [ -n \"$gh_proxy1\" ] && [ \"$_order_cached\" != \"${gh_proxy1%/}\" ]; then\n        printf '%s\\n' \"${gh_proxy1%/}\"\n    fi\n    if [ -n \"$gh_proxy2\" ] && [ \"$_order_cached\" != \"${gh_proxy2%/}\" ]; then\n        printf '%s\\n' \"${gh_proxy2%/}\"\n    fi\n}\n\n# Дефолтный валидатор для скачанного файла.\n# $1 = path, $2 = min_size (байт, 0 = без проверки размера).\n# Сетит _last_error и _last_size для caller-сообщений.\n#\n# HTML-stub detect: cloudflare challenge, jsdelivr \"429: Too Many\n# Requests\", proxy-error 404-page под HTTP 200. Маркеры якорные (^...)\n# чтобы не словить false-positive на байтах в gzip/zip/ELF метадате.\n_validate_default() {\n    _v_f=\"$1\"\n    _v_min=\"${2:-0}\"\n    _last_error=\"\"\n    _last_size=0\n    if [ ! -s \"$_v_f\" ]; then\n        _last_error=\"curl_failed\"\n        return 1\n    fi\n    _last_size=$(wc -c < \"$_v_f\" 2>/dev/null | tr -d ' ')\n    if [ \"$_v_min\" -gt 0 ]; then\n        [ -n \"$_last_size\" ] && [ \"$_last_size\" -ge \"$_v_min\" ] || {\n            _last_error=\"size\"\n            return 1\n        }\n    fi\n    if head -c 100 \"$_v_f\" 2>/dev/null | grep -iqE '^(<!doctype|<html|<head|<body|404|error|not found)'; then\n        _last_error=\"html_stub\"\n        return 1\n    fi\n    return 0\n}\n\n# fetch_with_mirrors <url> <dest> [min_size] [validator]\n#\n# Качает <url> в <dest> через цепочку префиксов, валидирует.\n# Атомарная замена: запись в \"${dest}.tmp.$$\" + mv.\n#\n# Возврат: 0 на успех, 1 на полный провал (все попытки failed/invalid).\n# При rc != 0: _last_error содержит причину последней неудачи\n# (curl_failed / size / html_stub), _last_size содержит размер файла\n# при size-fail.\nfetch_with_mirrors() {\n    _fwm_url=\"$1\"\n    _fwm_dest=\"$2\"\n    _fwm_min=\"${3:-0}\"\n    _fwm_validator=\"${4:-_validate_default}\"\n    _fwm_tmp=\"${_fwm_dest}.tmp.$$\"\n    _fwm_winner=\"\"\n    _last_error=\"\"\n    _last_size=0\n\n    rm -f \"$_fwm_tmp\"\n    _fwm_orders=$(_mirror_order)\n    while IFS= read -r _fwm_prefix; do\n        [ \"$_fwm_prefix\" = \"$_DIRECT_TOKEN\" ] && _fwm_prefix=\"\"\n        if [ -n \"$_fwm_prefix\" ]; then\n            _fwm_fetch=\"$_fwm_prefix/$_fwm_url\"\n        else\n            _fwm_fetch=\"$_fwm_url\"\n        fi\n        if eval curl $curl_extra --connect-timeout 10 $curl_timeout \\\n               -fL -o \"$_fwm_tmp\" \"$_fwm_fetch\" >/dev/null 2>&1; then\n            if \"$_fwm_validator\" \"$_fwm_tmp\" \"$_fwm_min\"; then\n                _fwm_winner=\"$_fwm_prefix\"\n                break\n            fi\n        else\n            _last_error=\"curl_failed\"\n        fi\n        rm -f \"$_fwm_tmp\"\n    done <<EOF\n$_fwm_orders\nEOF\n\n    if [ -f \"$_fwm_tmp\" ]; then\n        mv -f \"$_fwm_tmp\" \"$_fwm_dest\" || { rm -f \"$_fwm_tmp\"; return 1; }\n        _mirror_cache_write \"$_fwm_winner\"\n        _last_error=\"\"\n        return 0\n    fi\n    return 1\n}\n\n# probe_with_mirrors <url>\n#\n# HEAD-probe (с fallback на range-byte для mirror'ов которые не разрешают\n# HEAD и отдают 405). Используется в xray/mihomo downloader'ах для\n# быстрой проверки \"существует ли такая версия\" перед полной загрузкой.\n#\n# Возврат: 0 на 2xx; 2 если все попытки получили 4xx (definitive miss,\n# например пользователь ввёл неверную версию); 1 на прочие транзиентные\n# ошибки. _last_http содержит HTTP-код последней значимой попытки (для\n# error сообщений caller'а), _last_curl_rc содержит exit-код curl\n# последней попытки (28 = таймаут, остальные см. man curl).\nprobe_with_mirrors() {\n    _pwm_url=\"$1\"\n    _pwm_attempts=0\n    _pwm_fail_4xx=0\n    _last_http=\"\"\n    _last_curl_rc=0\n\n    _pwm_orders=$(_mirror_order)\n    while IFS= read -r _pwm_prefix; do\n        [ \"$_pwm_prefix\" = \"$_DIRECT_TOKEN\" ] && _pwm_prefix=\"\"\n        if [ -n \"$_pwm_prefix\" ]; then\n            _pwm_probe=\"$_pwm_prefix/$_pwm_url\"\n        else\n            _pwm_probe=\"$_pwm_url\"\n        fi\n        _pwm_attempts=$((_pwm_attempts + 1))\n        _pwm_code=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout \\\n            -I -s -L -w '%{http_code}' -o /dev/null \"$_pwm_probe\" 2>/dev/null)\n        _last_curl_rc=$?\n        if [ \"$_pwm_code\" = \"405\" ]; then\n            _pwm_code=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout \\\n                -s -L -r 0-0 -w '%{http_code}' -o /dev/null \"$_pwm_probe\" 2>/dev/null)\n            _last_curl_rc=$?\n        fi\n        _last_http=\"$_pwm_code\"\n        case \"$_pwm_code\" in\n            2[0-9][0-9])\n                _mirror_cache_write \"$_pwm_prefix\"\n                return 0\n                ;;\n            40[0-9])\n                _pwm_fail_4xx=$((_pwm_fail_4xx + 1))\n                ;;\n        esac\n    done <<EOF\n$_pwm_orders\nEOF\n\n    [ \"$_pwm_attempts\" -gt 0 ] && [ \"$_pwm_fail_4xx\" = \"$_pwm_attempts\" ] && return 2\n    return 1\n}\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/01_downloaders_mihomo.sh",
    "content": "# Загрузка Mihomo\ndownload_mihomo() {\n    USE_JSDELIVR=\"\"\n    printf \"  ${green}Запрос информации${reset} о релизах ${yellow}Mihomo${reset}\\n\"\n\n    # Получаем список релизов через GitHub API\n    RELEASE_TAGS=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s \"${mihomo_api_url}?per_page=20\" 2>/dev/null | jq -r '.[] | select(.prerelease == false) | .tag_name' | head -n 8)\n\n    if [ -z \"$RELEASE_TAGS\" ]; then\n        echo\n        printf \"  ${red}Нет доступа${reset} к ${yellow}GitHub API${reset}. Пробуем ${yellow}jsDelivr${reset}...\\n\"\n\n        # Получаем список релизов через jsDelivr\n        RELEASE_TAGS=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s \"$mihomo_jsd_url\" 2>/dev/null | jq -r '.versions[]' | head -n 8)\n\n        if [ -z \"$RELEASE_TAGS\" ]; then\n            echo\n            printf \"  ${red}Нет доступа${reset} к ${yellow}jsDelivr${reset}\\n\"\n            echo\n            printf \"  ${red}Ошибка${reset}: Не удалось получить список релизов ни через ${yellow}GitHub API${reset}, ни через ${yellow}jsDelivr${reset}\\n  Проверьте соединение с интернетом или повторите позже\\n  Если ошибка сохраняется, воспользуйтесь возможностью OffLine установки:\\n  https://github.com/jameszeroX/XKeen/blob/main/OffLine_install.md\\n\"\n            echo\n            exit 1\n        fi\n        echo\n        printf \"  Список релизов получен с использованием ${yellow}jsDelivr${reset}:\\n\"\n        USE_JSDELIVR=\"true\"\n    else\n        echo\n        printf \"  Список релизов получен с использованием ${yellow}GitHub API${reset}:\\n\"\n    fi\n\n    while true; do\n        echo\n        echo \"$RELEASE_TAGS\" | awk '{printf \"    %2d. %s\\n\", NR, $0}'\n        echo\n        echo \"     9. Ручной ввод версии\"\n        echo\n        echo \"     0. Пропустить загрузку Mihomo\"\n\n        printf \"\\n  Введите порядковый номер релиза (0 - пропустить, 9 - ручной ввод): \"\n        read -r choice\n\n        case \"$choice\" in\n            [0-9]) ;;\n            *) \n                printf \"  ${red}Некорректный${reset} ввод. Пожалуйста, введите число\\n\"\n                sleep 1\n                continue\n                ;;\n        esac\n\n        if [ \"$choice\" = \"0\" ]; then\n            bypass_mihomo=\"true\"\n            printf \"  Загрузка Mihomo ${yellow}пропущена${reset}\\n\"\n            return\n        fi\n\n        if [ \"$choice\" = \"9\" ]; then\n            printf \"  Введите версию Mihomo для загрузки (например: v1.19.6): \"\n            read -r version_selected\n            if [ -z \"$version_selected\" ]; then\n                printf \"  ${red}Ошибка${reset}: Версия не может быть пустой\\n\"\n                sleep 1\n                continue\n            fi\n\n            version_selected=$(echo \"$version_selected\" | sed 's/^v//')\n            version_selected=\"v$version_selected\"\n\n        else\n            version_selected=$(echo \"$RELEASE_TAGS\" | awk -v line=\"$choice\" 'NR == line {print $0; exit}')\n            if [ -z \"$version_selected\" ]; then\n                printf \"  Выбранный номер ${red}вне диапазона.${reset} Пожалуйста, попробуйте снова\\n\"\n                sleep 1\n                continue\n            fi\n            if [ \"$USE_JSDELIVR\" = \"true\" ]; then\n                version_selected=\"v$version_selected\"\n            fi\n        fi\n\n        VERSION_ARG=\"$version_selected\"\n\n        URL_BASE=\"${mihomo_gz_url}/$VERSION_ARG\"\n\n        yq_download_base_url=\"$(get_yq_dist_url)\"\n\n        case $architecture in\n            \"arm64-v8a\")\n                download_url=\"$URL_BASE/mihomo-linux-arm64-$VERSION_ARG.gz\"\n                download_yq=\"$yq_download_base_url/yq_linux_arm64\"\n            ;;\n            \"mips32le\")\n                if [ \"$softfloat\" = \"true\" ]; then\n                    download_url=\"$URL_BASE/mihomo-linux-mipsle-softfloat-$VERSION_ARG.gz\"\n                else\n                    download_url=\"$URL_BASE/mihomo-linux-mipsle-hardfloat-$VERSION_ARG.gz\"\n                fi\n                download_yq=\"$yq_download_base_url/yq_linux_mipsle\"\n            ;;\n            \"mips32\")\n                download_url=\"$URL_BASE/mihomo-linux-mips-hardfloat-$VERSION_ARG.gz\"\n                download_yq=\"$yq_download_base_url/yq_linux_mips\"\n            ;;\n            *)\n                download_url=\n                download_yq=\n            ;;\n        esac\n\n        if [ -z \"$download_url\" ] || [ -z \"$download_yq\" ]; then\n            printf \"  ${red}Ошибка${reset}: Не удалось получить URL для загрузки Mihomo\\n\"\n            exit 1\n        fi\n\n        filename=$(basename \"$download_url\")\n        extension=\"${filename##*.}\"\n        mkdir -p \"$mtmp_dir\"\n        yq_available=\"false\"\n\n        printf \"  ${yellow}Проверка${reset} доступности версии $version_selected...\\n\"\n\n        probe_with_mirrors \"$download_url\"\n        _rc=$?\n        case \"$_rc\" in\n            0)\n                printf \"  Файл ${green}доступен${reset}\\n\"\n                ;;\n            2)\n                case \"$_last_http\" in\n                    403) printf \"  ${red}Доступ запрещен${reset} (403)\\n\" ;;\n                    404) printf \"  Файл ${red}не найден${reset} (404)\\n\" ;;\n                    *)   printf \"  ${yellow}Проблема с доступом${reset} (HTTP: %s)\\n\" \"$_last_http\" ;;\n                esac\n                printf \"  ${red}Ошибка${reset}: Версия Mihomo $version_selected недоступна\\n\"\n                continue\n                ;;\n            *)\n                if [ \"$_last_curl_rc\" = \"28\" ]; then  # curl OPERATION_TIMEDOUT\n                    printf \"  ${red}Таймаут${reset} при проверке\\n\"\n                elif [ \"$_last_curl_rc\" != \"0\" ]; then\n                    printf \"  ${red}Ошибка curl (%s)${reset} при проверке\\n\" \"$_last_curl_rc\"\n                elif [ -n \"$_last_http\" ] && [ \"$_last_http\" != \"000\" ]; then\n                    printf \"  ${yellow}Проблема с доступом${reset} (HTTP: %s)\\n\" \"$_last_http\"\n                else\n                    printf \"  ${red}Нет соединения${reset}\\n\"\n                fi\n                printf \"  ${red}Ошибка${reset}: Версия Mihomo $version_selected недоступна\\n\"\n                continue\n                ;;\n        esac\n\n        printf \"  ${yellow}Выполняется загрузка${reset} парсера конфигурационных файлов Mihomo - Yq\\n\"\n\n        if probe_with_mirrors \"$download_yq\"; then\n            if fetch_with_mirrors \"$download_yq\" \"$install_dir/yq\" 1024; then\n                chmod +x \"$install_dir/yq\"\n                yq_available=\"true\"\n                printf \"  Yq ${green}успешно загружен и установлен${reset}\\n\"\n            else\n                printf \"  ${red}Ошибка${reset}: Не удалось загрузить Yq\\n\"\n            fi\n        else\n            printf \"  ${yellow}Предупреждение${reset}: Yq недоступен для загрузки, продолжение без него\\n\"\n        fi\n\n        printf \"  ${yellow}Выполняется загрузка${reset} выбранной версии Mihomo\\n\"\n\n        if [ \"$yq_available\" != \"true\" ] && [ -x \"$install_dir/yq\" ]; then\n            yq_available=\"true\"\n            printf \"  ${yellow}Используется${reset} уже установленный Yq\\n\"\n        fi\n\n        if [ \"$yq_available\" != \"true\" ]; then\n            printf \"  ${red}Ошибка${reset}: Для работы Mihomo требуется Yq. Установка прервана\\n\"\n            return 1\n        fi\n\n        if ! fetch_with_mirrors \"$download_url\" \"$mtmp_dir/mihomo.$extension\" 1024; then\n            printf \"  ${red}Ошибка${reset}: Не удалось загрузить Mihomo $version_selected\\n\"\n            continue\n        fi\n        printf \"  Mihomo ${green}успешно загружен${reset}\\n\"\n        return 0\n    done\n}\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/01_downloaders_xray.sh",
    "content": "# Загрузка Xray\ndownload_xray() {\n    USE_JSDELIVR=\"\"\n    printf \"  ${green}Запрос информации${reset} о релизах ${yellow}Xray${reset}\\n\"\n\n    # Получаем список релизов через GitHub API\n    RELEASE_TAGS=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s \"${xray_api_url}?per_page=50\" 2>/dev/null | jq -r '.[] | select(.prerelease == false) | .tag_name' | head -n 8)\n\n    if [ -z \"$RELEASE_TAGS\" ]; then\n        echo\n        printf \"  ${red}Нет доступа${reset} к ${yellow}GitHub API${reset}. Пробуем ${yellow}jsDelivr${reset}...\\n\"\n\n        # Получаем список релизов через jsDelivr\n        RELEASE_TAGS=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s \"$xray_jsd_url\" 2>/dev/null | jq -r '.versions[]' | head -n 8)\n\n        if [ -z \"$RELEASE_TAGS\" ]; then\n            echo\n            printf \"  ${red}Нет доступа${reset} к ${yellow}jsDelivr${reset}\\n\"\n            echo\n            printf \"  ${red}Ошибка${reset}: Не удалось получить список релизов ни через ${yellow}GitHub API${reset}, ни через ${yellow}jsDelivr${reset}\\n  Проверьте соединение с интернетом или повторите позже\\n  Если ошибка сохраняется, воспользуйтесь возможностью OffLine установки:\\n  https://github.com/jameszeroX/XKeen/blob/main/OffLine_install.md\\n\"\n            echo\n            exit 1\n        fi\n        echo\n        printf \"  Список релизов получен с использованием ${yellow}jsDelivr${reset}:\\n\"\n        USE_JSDELIVR=\"true\"\n    else\n        echo\n        printf \"  Список релизов получен с использованием ${yellow}GitHub API${reset}:\\n\"\n    fi\n\n    if [ \"$autoinstall_mode\" = \"true\" ]; then\n        version_selected=$(echo \"$RELEASE_TAGS\" | head -1)\n        [ \"$USE_JSDELIVR\" = \"true\" ] && version_selected=\"v$version_selected\"\n        printf \"  ${green}Авто-режим${reset}: выбрана последняя версия ${yellow}%s${reset}\\n\" \"$version_selected\"\n\n        VERSION_ARG=\"$version_selected\"\n        URL_BASE=\"${xray_zip_url}/$VERSION_ARG\"\n\n        case $architecture in\n            \"arm64-v8a\") download_url=\"$URL_BASE/Xray-linux-arm64-v8a.zip\" ;;\n            \"mips32le\")  download_url=\"$URL_BASE/Xray-linux-mips32le.zip\" ;;\n            \"mips32\")    download_url=\"$URL_BASE/Xray-linux-mips32.zip\" ;;\n            *)           download_url= ;;\n        esac\n\n        if [ -z \"$download_url\" ]; then\n            printf \"  ${red}Ошибка${reset}: Не удалось получить URL для загрузки Xray\\n\"\n            exit 1\n        fi\n\n        filename=$(basename \"$download_url\")\n        extension=\"${filename##*.}\"\n        mkdir -p \"$xtmp_dir\"\n\n        printf \"  ${yellow}Проверка${reset} доступности версии %s...\\n\" \"$version_selected\"\n\n        probe_with_mirrors \"$download_url\"\n        _rc=$?\n        case \"$_rc\" in\n            0)\n                printf \"  Файл ${green}доступен${reset}\\n\"\n                ;;\n            2)\n                case \"$_last_http\" in\n                    403) printf \"  ${red}Доступ запрещен${reset} (403)\\n\" ;;\n                    404) printf \"  Файл ${red}не найден${reset} (404)\\n\" ;;\n                    *)   printf \"  ${yellow}Проблема с доступом${reset} (HTTP: %s)\\n\" \"$_last_http\" ;;\n                esac\n                printf \"  ${red}Ошибка${reset}: Версия %s недоступна\\n\" \"$version_selected\"\n                exit 1\n                ;;\n            *)\n                if [ \"$_last_curl_rc\" = \"28\" ]; then  # curl OPERATION_TIMEDOUT\n                    printf \"  ${red}Таймаут${reset} при проверке\\n\"\n                elif [ \"$_last_curl_rc\" != \"0\" ]; then\n                    printf \"  ${red}Ошибка curl (%s)${reset} при проверке\\n\" \"$_last_curl_rc\"\n                elif [ -n \"$_last_http\" ] && [ \"$_last_http\" != \"000\" ]; then\n                    printf \"  ${yellow}Проблема с доступом${reset} (HTTP: %s)\\n\" \"$_last_http\"\n                else\n                    printf \"  ${red}Нет соединения${reset}\\n\"\n                fi\n                printf \"  ${red}Ошибка${reset}: Версия %s недоступна\\n\" \"$version_selected\"\n                exit 1\n                ;;\n        esac\n\n        printf \"  ${yellow}Выполняется загрузка${reset} последней версии Xray\\n\"\n\n        if ! fetch_with_mirrors \"$download_url\" \"$xtmp_dir/xray.$extension\" 1024; then\n            printf \"  ${red}Ошибка${reset}: Не удалось загрузить Xray %s\\n\" \"$version_selected\"\n            exit 1\n        fi\n        printf \"  Xray ${green}успешно загружен${reset}\\n\"\n        return 0\n    fi\n\n    while true; do\n        echo\n        echo \"$RELEASE_TAGS\" | awk '{printf \"    %2d. %s\\n\", NR, $0}'\n        echo\n        echo \"     9. Ручной ввод версии\"\n        echo\n        echo \"     0. Пропустить загрузку Xray\"\n\n        printf \"\\n  Введите порядковый номер релиза (0 - пропустить, 9 - ручной ввод): \"\n        read -r choice\n\n        case \"$choice\" in\n            [0-9]) ;;\n            *) \n                printf \"  ${red}Некорректный${reset} ввод. Пожалуйста, введите число\\n\"\n                sleep 1\n                continue\n                ;;\n        esac\n\n        if [ \"$choice\" = \"0\" ]; then\n            bypass_xray=\"true\"\n            printf \"  Загрузка Xray ${yellow}пропущена${reset}\\n\"\n            return\n        fi\n\n        if [ \"$choice\" = \"9\" ]; then\n            printf \"  Введите версию Xray для загрузки (например: v25.4.30): \"\n            read -r version_selected\n            if [ -z \"$version_selected\" ]; then\n                printf \"  ${red}Ошибка${reset}: Версия не может быть пустой\\n\"\n                sleep 1\n                continue\n            fi\n\n            version_selected=$(echo \"$version_selected\" | sed 's/^v//')\n            version_selected=\"v$version_selected\"\n\n        else\n            version_selected=$(echo \"$RELEASE_TAGS\" | awk -v line=\"$choice\" 'NR == line {print $0; exit}')\n            if [ -z \"$version_selected\" ]; then\n                printf \"  Выбранный номер ${red}вне диапазона.${reset} Пожалуйста, попробуйте снова\\n\"\n                sleep 1\n                continue\n            fi\n            if [ \"$USE_JSDELIVR\" = \"true\" ]; then\n                version_selected=\"v$version_selected\"\n            fi\n        fi\n\n        VERSION_ARG=\"$version_selected\"\n\n        URL_BASE=\"${xray_zip_url}/$VERSION_ARG\"\n\n        case $architecture in\n            \"arm64-v8a\") download_url=\"$URL_BASE/Xray-linux-arm64-v8a.zip\" ;;\n            \"mips32le\") download_url=\"$URL_BASE/Xray-linux-mips32le.zip\" ;;\n            \"mips32\") download_url=\"$URL_BASE/Xray-linux-mips32.zip\" ;;\n            *) download_url= ;;\n        esac\n\n        if [ -z \"$download_url\" ]; then\n            printf \"  ${red}Ошибка${reset}: Не удалось получить URL для загрузки Xray\\n\"\n            exit 1\n        fi\n\n        filename=$(basename \"$download_url\")\n        extension=\"${filename##*.}\"\n        mkdir -p \"$xtmp_dir\"\n\n        printf \"  ${yellow}Проверка${reset} доступности версии $version_selected...\\n\"\n\n        probe_with_mirrors \"$download_url\"\n        _rc=$?\n        case \"$_rc\" in\n            0)\n                printf \"  Файл ${green}доступен${reset}\\n\"\n                ;;\n            2)\n                case \"$_last_http\" in\n                    403) printf \"  ${red}Доступ запрещен${reset} (403)\\n\" ;;\n                    404) printf \"  Файл ${red}не найден${reset} (404)\\n\" ;;\n                    *)   printf \"  ${yellow}Проблема с доступом${reset} (HTTP: %s)\\n\" \"$_last_http\" ;;\n                esac\n                printf \"  ${red}Ошибка${reset}: Версия $version_selected недоступна\\n\"\n                continue\n                ;;\n            *)\n                if [ \"$_last_curl_rc\" = \"28\" ]; then  # curl OPERATION_TIMEDOUT\n                    printf \"  ${red}Таймаут${reset} при проверке\\n\"\n                elif [ \"$_last_curl_rc\" != \"0\" ]; then\n                    printf \"  ${red}Ошибка curl (%s)${reset} при проверке\\n\" \"$_last_curl_rc\"\n                elif [ -n \"$_last_http\" ] && [ \"$_last_http\" != \"000\" ]; then\n                    printf \"  ${yellow}Проблема с доступом${reset} (HTTP: %s)\\n\" \"$_last_http\"\n                else\n                    printf \"  ${red}Нет соединения${reset}\\n\"\n                fi\n                printf \"  ${red}Ошибка${reset}: Версия $version_selected недоступна\\n\"\n                continue\n                ;;\n        esac\n\n        printf \"  ${yellow}Выполняется загрузка${reset} выбранной версии Xray\\n\"\n\n        if ! fetch_with_mirrors \"$download_url\" \"$xtmp_dir/xray.$extension\" 1024; then\n            printf \"  ${red}Ошибка${reset}: Не удалось загрузить Xray $version_selected\\n\"\n            continue\n        fi\n        printf \"  Xray ${green}успешно загружен${reset}\\n\"\n        return 0\n    done\n}\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/02_donwloaders_xkeen.sh",
    "content": "# Загрузка XKeen\ndownload_xkeen() {\n    mkdir -p \"$ktmp_dir\"\n    printf \"  ${yellow}Выполняется загрузка${reset} XKeen\\n\"\n\n    if ! fetch_with_mirrors \"$xkeen_tar_url\" \"$ktmp_dir/xkeen.tar.gz\" 1024; then\n        printf \"  ${red}Ошибка${reset}: Не удалось загрузить XKeen\\n\"\n        exit 1\n    fi\n\n    printf \"  XKeen ${green}успешно загружен${reset}\\n\"\n    return 0\n}\n\ndownload_xkeen_dev() {\n    xkeen_tar_url=\"$xkeen_dev_url\"\n    download_xkeen\n}"
  },
  {
    "path": "scripts/_xkeen/05_tests/00_tests_import.sh",
    "content": "# Импорт модулей тестирования\n\n# Модули тестирования\n. \"$xtests_dir/01_tests_connected.sh\"\n. \"$xtests_dir/02_tests_xports.sh\"\n. \"$xtests_dir/03_tests_storage.sh\""
  },
  {
    "path": "scripts/_xkeen/05_tests/01_tests_connected.sh",
    "content": "# Функция проверки доступности интернета\ntest_connection() {\n    nslookup \"$conn_URL\" >/dev/null 2>&1 && return 0\n    curl -Is --connect-timeout 1 \"$conn_URL\" >/dev/null && return 0\n    ping -c 1 -W 1 \"$conn_IP1\" >/dev/null 2>&1 && return 0\n    ping -c 1 -W 1 \"$conn_IP2\" >/dev/null 2>&1 && return 0\n\n    printf \"  ${red}Отсутствует${reset} интернет-соединение\\n\"\n    exit 1\n}\n\n# Функция загрузки\ndownload_with_check() {\n    url=\"$1\"\n    output_file=\"$2\"\n    min_size=\"${3:-50000}\"\n\n    eval curl $curl_extra --connect-timeout 5 -m 15 -y 1000 -Y 5 -s -L \"$url\" -o \"$output_file\" 2>/dev/null\n\n    if [ -f \"$output_file\" ]; then\n        size=$(wc -c < \"$output_file\" 2>/dev/null || echo 0)\n        if [ \"$size\" -gt \"$min_size\" ]; then\n            return 0\n        fi\n    fi\n\n    rm -f \"$output_file\" 2>/dev/null\n    return 1\n}\n\n# Функция проверки доступности Entware\ntest_entware() {\n    printf \"  ${yellow}Проверка доступности${reset} репозитория Entware. Подождите, пожалуйста...\\n\"\n    repo_url=$(awk '/^src/ {print $3; exit}' /opt/etc/opkg.conf 2>/dev/null)\n\n    if [ -z \"$repo_url\" ]; then\n        printf \"  ${red}Не удалось${reset} определить используемый репозиторий Entware\\n\"\n        exit 1\n    fi\n\n    repo_url=\"$repo_url/Packages.gz\"\n    tmp_file=\"/tmp/pkg_check_$$\"\n\n    if download_with_check \"$repo_url\" \"$tmp_file\"; then\n        printf \"  Репозиторий Entware ${green}доступен${reset}. Продолжаем...\\n\"\n\n        opkg update >/dev/null 2>&1\n        info_packages\n        install_packages\n        rm -f \"$tmp_file\" 2>/dev/null\n        return 0\n    else\n        printf \"  Репозиторий Entware ${red}недоступен${reset}\\n\"\n        printf \"  Укажите рабочее зеркало репозитория в файле ${yellow}/opt/etc/opkg.conf${reset}\\n\"\n        exit 1\n    fi\n}\n\n# Функция определения пользовательского прокси для GitHub\nget_user_proxy() {\n    gh_proxy_user=\"\"\n    [ ! -f \"$xkeen_config\" ] && return 1\n\n    if command -v jq >/dev/null 2>&1; then\n        gh_proxy_user=$(jq -r '.xkeen.gh_proxy // empty' \"$xkeen_config\" 2>/dev/null)\n    fi\n\n    if [ -z \"$gh_proxy_user\" ]; then\n        gh_proxy_user=$(sed -n 's/.*\"gh_proxy\": *\"\\([^\"]*\\)\".*/\\1/p' \"$xkeen_config\" | xargs 2>/dev/null)\n    fi\n\n    [ \"$gh_proxy_user\" = \"null\" ] && gh_proxy_user=\"\"\n}\n\n# Функция проверки доступности GitHub.\n# Тонкая обёртка над probe_with_mirrors: идентичная философия (пара URL\n# через одну цепочку префиксов, exclusive gh_proxy_user, exit 1 на\n# полную недоступность), но один engine fallback в helper'е вместо\n# дублирования логики direct/proxy1/proxy2 здесь.\ntest_github() {\n    [ -n \"$_gh_probed\" ] && return 0\n\n    get_user_proxy\n\n    printf \"  ${yellow}Проверка доступности${reset} GitHub. Подождите, пожалуйста...\\n\"\n\n    # Pair probe: github.com (releases) + raw.githubusercontent.com\n    # (для dev-обновления). Разные CDN, разный uptime, проверяем оба.\n    if probe_with_mirrors \"$xkeen_tar_url\" && probe_with_mirrors \"$xkeen_dev_url\"; then\n        _gh_probed=1\n        if [ -n \"$gh_proxy_user\" ]; then\n            printf \"  GitHub ${green}доступен через ваш прокси${reset}: ${yellow}$gh_proxy_user${reset}. Продолжаем...\\n\"\n        elif [ -r /tmp/.xkeen_mirror_cache ] && grep -q \"__direct__\" /tmp/.xkeen_mirror_cache 2>/dev/null; then\n            printf \"  GitHub ${green}доступен${reset}. Продолжаем...\\n\"\n        else\n            printf \"  GitHub ${green}доступен через прокси${reset}. Продолжаем...\\n\"\n        fi\n        return 0\n    fi\n\n    if [ -n \"$gh_proxy_user\" ]; then\n        printf \"  ${red}Ошибка${reset}: Указанный вами прокси $gh_proxy_user недоступен\\n\"\n    else\n        printf \"  ${red}Ошибка${reset}: GitHub недоступен\\n\"\n    fi\n    exit 1\n}"
  },
  {
    "path": "scripts/_xkeen/05_tests/02_tests_xports.sh",
    "content": "# Определение на каких портах слушает ядро прокси\ntests_ports_client() {\n\n    if pidof \"xray\" >/dev/null; then\n        name_client=xray\n    elif pidof \"mihomo\" >/dev/null; then\n        name_client=mihomo\n    else\n        echo\n        echo \"  Определение портов прослушивания возможно только при работающем XKeen\"\n        echo \"  Запустите XKeen командой 'xkeen -start'\"\n        exit 1\n    fi\n\n    listening_ports_tcp=\n    listening_ports_udp=\n    output=\"  $name_client ${green}слушает${reset}\"\n\n    listening_ports_tcp=$(netstat -ltunp | grep \"$name_client\" | grep \"tcp\")\n    listening_ports_udp=$(netstat -ltunp | grep \"$name_client\" | grep \"udp\")\n\n    if [ -n \"$listening_ports_tcp\" ] || [ -n \"$listening_ports_udp\" ]; then\n        printed=false\n        IFS='\n'\n        for line in $listening_ports_tcp $listening_ports_udp; do\n            gateway=\n            port=\n            protocol=\n            \n            if [ -n \"$(echo \"$line\" | grep \"tcp\")\" ]; then\n                protocol=\"TCP\"\n            fi\n            if [ -n \"$(echo \"$line\" | grep \"udp\")\" ]; then\n                if [ -n \"$protocol\" ]; then\n                    protocol=\"$protocol и UDP\"\n                else\n                    protocol=\"UDP\"\n                fi\n            fi\n            \n            full_address=$(echo \"$line\" | awk '{print $4}')\n            \n            if echo \"$full_address\" | grep -q '^:::[0-9]'; then\n                # Если IPv4 отображается как :::port\n                gateway=\"0.0.0.0\"\n                port=$(echo \"$full_address\" | awk -F':::' '{print $2}')\n            elif echo \"$full_address\" | grep -q '^\\[::\\]'; then\n                # Явный IPv6 [::]:port\n                gateway=\"[::]\"\n                port=$(echo \"$full_address\" | awk -F'\\\\]:' '{print $2}')\n            elif echo \"$full_address\" | grep -q '\\\\]:'; then\n                # Обычный IPv6 [addr]:port\n                gateway=$(echo \"$full_address\" | awk -F'\\\\]:' '{print $1}')\"]\"\n                port=$(echo \"$full_address\" | awk -F'\\\\]:' '{print $2}')\n            elif echo \"$full_address\" | grep -q ':'; then\n                # Обычный IPv4\n                gateway=$(echo \"$full_address\" | cut -d':' -f1)\n                port=$(echo \"$full_address\" | cut -d':' -f2)\n            fi\n            \n            if [ \"$printed\" = false ]; then\n                printf \"%b\\n\" \"$output\"\n                printed=true\n            fi\n            printf \"\\n     %bШлюз%b %s\\n     %bПорт%b %s\\n     %bПротокол%b %s\\n\" \\\n                   \"$italic\" \"$reset\" \"$gateway\" \\\n                   \"$italic\" \"$reset\" \"$port\" \\\n                   \"$italic\" \"$reset\" \"$protocol\"\n        done\n    else\n        printf \"%b\\n\" \"  $name_client ${red}не слушает${reset} на каких-либо портах\"\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/05_tests/03_tests_storage.sh",
    "content": "# Определение места установки Entware\nlocation_entware_storage() {\n    mount_point=$(mount | grep 'on /opt ')\n    device=$(echo \"$mount_point\" | awk '{print $1}')\n\n    if echo \"$device\" | grep -q \"^/dev/sd\"; then\n        entware_storage=\"на внешний USB-накопитель\"\n    elif echo \"$device\" | grep -q \"^/dev/ubi\"; then\n        entware_storage=\"во внутреннюю память роутера\"\n        preinstall_warn=\"true\"\n    else\n        entware_storage=\"на неидентифицированный носитель информации\"\n    fi\n}\n\npreinstall_warn() {\n    if [ -n \"$preinstall_warn\" ]; then\n        echo\n        echo -e \"  ${red}Внимание${reset}: Инициирована установка XKeen $entware_storage\"\n        echo \"  Убедитесь, что на ней достаточно свободного места. Сбой при такой\"\n        echo \"  установке не является проблемой XKeen и багрепорт не будет рассмотрен\"\n        echo -e \"  XKeen ${green}рекомендуется${reset} устанавливать на внешний ${green}USB-накопитель${reset}\"\n        echo\n        echo \"  1. Продолжить установку $entware_storage\"\n        echo \"  2. Выйти из установщика\"\n        echo\n\n    while true; do\n        read -p \"  Выберите действие: \" choice\n\n        case $choice in\n            1)\n                clear\n                break\n                ;;\n            2)\n                echo\n                echo -e \"  ${red}Установка отменена${reset}\"\n                exit 0\n                ;;\n            *)\n                echo -e \"  ${red}Некорректный ввод.${reset} Выберите один из предложенных вариантов\"\n                ;;\n        esac\n    done\n    fi\n}"
  },
  {
    "path": "scripts/_xkeen/about.sh",
    "content": "about_xkeen() {\n    echo\n    printf \"  Утилита ${green}XKeen${reset} предназначена для управления межсетевым\\n  экраном роутера ${yellow}Keenetic${reset}, защищающим домашнюю сеть.\\n  Разработчики ${red}не несут ответственности${reset} за использование\\n  ${green}XKeen${reset} вне прямого назначения. Перед использованием убедитесь,\\n  что ваши действия соответствуют законодательству вашей страны.\\n  Использование ${green}XKeen${reset} в противоправных целях ${red}строго запрещено${reset}.\\n\"\n}\n\nauthor_donate() {\n    echo\n    echo \"  Выберите удобный для Вас способ:\"\n    echo\n    echo -e \"  Поддержать автора оригинального XKeen (${green}Skrill0${reset})\"\n    echo \"     1. Т-Банк\"\n    echo \"     2. DonationAlerts/ЮMoney\"\n    echo \"     3. Crypto\"\n    echo\n    echo -e \"  Поддержать разработчика форка XKeen (${green}jameszero${reset})\"\n    echo \"     4. Карта МИР\"\n    echo \"     5. CloudTips/ЮMoney\"\n    echo \"     6. Crypto\"\n    echo\n    echo \"     0. Отмена\"\n    echo\n\n    while true; do\n        read -r -p \"  Ваш выбор: \" choice\n        case \"$choice\" in\n            1)\n                echo\n                echo -e \"  ${yellow}Прямая ссылка${reset}\"\n                echo \"     https://www.tbank.ru/rm/krasilnikova.alina18/G4Z9433893\"\n                echo\n                echo -e \"  ${yellow}Номер карты${reset}\"\n                echo \"     2200 7008 8716 3128\"\n                echo\n                return 0\n                ;;\n            2)\n                echo\n                echo -e \"  ${yellow}Прямая ссылка DonationAlerts${reset}\"\n                echo \"     https://www.donationalerts.com/r/skrill0\"\n                echo\n                echo -e \"  ${yellow}Прямая ссылка ЮMoney${reset}\"\n                echo \"     https://yoomoney.ru/to/410018052017678\"\n                echo\n                echo -e \"  ${yellow}Номер ЮMoney-кошелька${reset}\"\n                echo \"     4100 1805 201 7678\"\n                echo\n                return 0\n                ;;\n            3)\n                echo\n                echo -e \"  ${yellow}USDT${reset}, TRC20\"\n                echo \"     tsc6emx5khk4cpyfkwj7dusybokravxs3m\"\n                echo\n                echo -e \"  ${yellow}USDT${reset}, ERC20 и BEP20\"\n                echo \"     0x4a0369a762e3a23cc08f0bbbf39e169a647a5661\"\n                echo\n                echo -e \"  ${light_blue}Уточните актуальность реквизитов перед переводом${reset}\"\n                echo\n                return 0\n                ;;\n            4)\n                echo\n                echo -e \"  ${yellow}Карта МИР${reset} ЮMoney\"\n                echo \"     2204 1201 2976 4110\"\n                echo\n                return 0\n                ;;\n            5)\n                echo\n                echo -e \"  ${yellow}Прямая ссылка CloudTips${reset}\"\n                echo \"     https://pay.cloudtips.ru/p/7edb30ec\"\n                echo\n                echo -e \"  ${yellow}Прямая ссылка ЮMoney${reset}\"\n                echo \"     https://yoomoney.ru/to/41001350776240\"\n                echo\n                echo -e \"  ${yellow}Номер ЮMoney-кошелька${reset}\"\n                echo \"     4100 1350 7762 40\"\n                echo\n                return 0\n                ;;\n            6)\n                echo\n                echo -e \"  ${yellow}USDT${reset}, TRC20\"\n                echo \"     TQhy1LbuGe3Bz7EVrDYn67ZFLDjDBa2VNX\"\n                echo\n                echo -e \"  ${yellow}USDT${reset}, ERC20\"\n                echo \"     0x6a5DF3b5c67E1f90dF27Ff3bd2a7691Fad234EE2\"\n                echo\n                echo -e \"  ${light_blue}Уточните актуальность реквизитов перед переводом${reset}\"\n                echo\n                return 0\n                ;;\n            0)\n                echo\n                echo -e \"  ${yellow}Спасибо${reset}, что ознакомились с возможностью поддержать разработчиков\"\n                echo\n                return 0\n                ;;\n            *)\n                echo -e \"  ${red}Некорректный ввод${reset}\"\n                ;;\n        esac\n    done\n}\n\nauthor_feedback() {\n    echo\n    echo -e \"  ${green}Контакты разработчиков${reset}\"\n    echo\n    echo -e \"  ${light_blue}Автор оригинального XKeen${reset}:\"\n    echo -e \"  ${yellow}Профиль на форуме keenetic${reset}:\"\n    echo \"     https://forum.keenetic.ru/profile/73583-skrill0\"\n    echo -e \"  ${yellow}e-mail${reset}:\"\n    echo \"     alinajoeyone@gmail.com\"\n    echo -e \"  ${yellow}telegram${reset}:\"\n    echo \"     @Skrill_zerro\"\n    echo -e \"  ${yellow}telegram помощника${reset}:\"\n    echo \"     @skride\"\n    echo\n    echo -e \"  ${light_blue}Разработчик форка XKeen${reset}:\"\n    echo -e \"  ${yellow}Профиль на форуме keenetic${reset}:\"\n    echo \"     https://forum.keenetic.ru/profile/20945-jameszero\"\n    echo -e \"  ${yellow}e-mail${reset}:\"\n    echo \"     admin@jameszero.net\"\n    echo -e \"  ${yellow}telegram${reset}:\"\n    echo \"     @jameszero\"\n    echo -e \"  ${yellow}сайт${reset}:\"\n    echo \"     https://jameszero.net\"\n    echo -e \"  ${yellow}GitHub${reset}:\"\n    echo \"     https://github.com/jameszeroX\"\n    echo\n    echo -e \"  Предоставленные выше контакты предназначены ${green}для личной переписки${reset}, а ${red}не для консультаций${reset}\"\n    echo \"  Возникающие вопросы по XKeen, задавайте в телеграм-чате https://t.me/+8Cvh7oVf6cE0MWRi\"\n}\n\nhelp_xkeen() {\n        echo\n        echo -e \"${yellow}Установка${reset}\"\n        echo -e \"\t-i\t${italic}\tОсновной режим установки XKeen + Xray + GeoFile/GeoIPSET + Mihomo${reset}\"\n        echo -e \"\t-io\t${italic}\tOffLine установка XKeen${reset}\"\n        echo -e \"\t-toff\t${italic}\tОтключение таймаута при меделенной загрузке с GitHub (xkeen -i -toff)${reset}\"\n        echo\n        echo -e \"${green}Переустановка${reset}\"\n        echo -e \"\t-k\t${italic}\tXKeen${reset}\"\n        echo -e \"\t-g\t${italic}\tGeoFile${reset}\"\n        echo -e \"\t-gips\t${italic}\tGeoIPSET${reset}\"\n        echo\n        echo -e \"${yellow}Обновление${reset}\"\n        echo -e \"\t-uk\t${italic}\tXKeen${reset}\"\n        echo -e \"\t-ug\t${italic}\tGeoFile/GeoIPSET${reset}\"\n        echo -e \"\t-ux\t${italic}\tXray (повышение/понижение версии)${reset}\"\n        echo -e \"\t-um\t${italic}\tMihomo (повышение/понижение версии)${reset}\"\n        echo\n        echo -e \"${yellow}Запланированная здача автообновления GeoFile/GeoIPSET${reset}\"\n        echo -e \"\t-ugc\t${italic}\tСоздание${reset}\"\n        echo -e \"\t-dgc\t${italic}\tУдаление${reset}\"\n        echo\n        echo -e \"${green}Резервная копия XKeen${reset}\"\n        echo -e \"\t-kb\t${italic}\tСоздание${reset}\"\n        echo -e \"\t-kbr\t${italic}\tВосстановление${reset}\"\n        echo\n        echo -e \"${green}Резервная копия конфигурации Xray${reset}\"\n        echo -e \"\t-xb\t${italic}\tСоздание${reset}\"\n        echo -e \"\t-xbr\t${italic}\tВосстановление${reset}\"\n        echo\n        echo -e \"${green}Резервная копия конфигурации Mihomo${reset}\"\n        echo -e \"\t-mb\t${italic}\tСоздание${reset}\"\n        echo -e \"\t-mbr\t${italic}\tВосстановление${reset}\"\n        echo\n        echo -e \"${red}Удаление${reset}\"\n        echo -e \"\t-remove\t${italic}\tПолная деинсталляция XKeen${reset}\"\n        echo -e \"\t-dgs\t${italic}\tGeoSite${reset}\"\n        echo -e \"\t-dgi\t${italic}\tGeoIP${reset}\"\n        echo -e \"\t-dgips\t${italic}\tGeoIPSET${reset}\"\n        echo -e \"\t-dx\t${italic}\tXray${reset}\"\n        echo -e \"\t-dm\t${italic}\tMihomo${reset}\"\n        echo -e \"\t-dk\t${italic}\tXKeen${reset}\"\n        echo\n        echo -e \"${green}Порты проксирования${reset}\"\n        echo -e \"\t-ap\t${italic}\tДобавить${reset}\"\n        echo -e \"\t-dp\t${italic}\tУдалить${reset}\"\n        echo -e \"\t-cp\t${italic}\tПосмотреть${reset}\"\n        echo\n        echo -e \"${green}Порты, исключённые из проксирования${reset}\"\n        echo -e \"\t-ape\t${italic}\tДобавить${reset}\"\n        echo -e \"\t-dpe\t${italic}\tУдалить${reset}\"\n        echo -e \"\t-cpe\t${italic}\tПосмотреть${reset}\"\n        echo\n        echo -e \"${light_blue}Управление прокси-клиентом${reset}\"\n        echo -e \"\t-start\t${italic}\tЗапуск${reset}\"\n        echo -e \"\t-stop\t${italic}\tОстановка${reset}\"\n        echo -e \"\t-restart${italic}\tПерезапуск${reset}\"\n        echo -e \"\t-status\t${italic}\tСтатус работы${reset}\"\n        echo -e \"\t-tp\t${italic}\tПорты, шлюз и протокол прокси-клиента${reset}\"\n        echo -e \"\t-auto\t${italic}\tВключить | Отключить автозапуск прокси-клиента${reset}\"\n        echo -e \"\t-d\t${italic}\tУстановить задержку автозапуска прокси-клиента${reset}\"\n        echo -e \"\t-fd\t${italic}\tВключить | Отключить контроль файловых дескрипторов прокси-клиента${reset}\"\n        echo -e \"\t-cfd\t${italic}\tПроверить количество файловых дескрипторов открытых прокси-клиентом${reset}\"\n        echo -e \"\t-diag\t${italic}\tВыполнить диагностику${reset}\"\n        echo -e \"\t-channel${italic}\tПереключить канал получения обновлений XKeen (Stable/Dev версия)${reset}\"\n        echo -e \"\t-xray\t${italic}\tПереключить XKeen на ядро Xray${reset}\"\n        echo -e \"\t-mihomo\t${italic}\tПереключить XKeen на ядро Mihomo${reset}\"\n        echo -e \"\t-ipv6\t${italic}\tВключить | Отключить протокол IPv6 в KeeneticOS${reset}\"\n        echo -e \"\t-dns\t${italic}\tВключить | Отключить перенаправление DNS в прокси${reset}\"\n        echo -e \"\t-pr\t${italic}\tВключить | Отключить проксирование трафика Entware${reset}\"\n        echo -e \"\t-extmsg\t${italic}\tВключить | Отключить расширенные сообщения при запуске XKeen${reset}\"\n        echo -e \"\t-cbk\t${italic}\tВключить | Отключить резервное копирование XKeen при обновлении${reset}\"\n        echo -e \"\t-aghfix\t${italic}\tВключить | Отключить отображение клиентов XKeen под своими IP в журнале AdGuard Home${reset}\"\n        echo\n        echo -e \"${light_blue}Информация${reset}\"\n        echo -e \"\t-about\t${italic}\tО программе${reset}\"\n        echo -e \"\t-ad\t${italic}\tПоддержать разработчиков${reset}\"\n        echo -e \"\t-af\t${italic}\tОбратная связь${reset}\"\n        echo -e \"\t-v\t${italic}\tВерсия XKeen${reset}\"\n}"
  },
  {
    "path": "scripts/_xkeen/import.sh",
    "content": "# Импорт основных модулей и определение их путей\n\nscript_dir=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nxinfo_dir=\"$script_dir/.xkeen/01_info\"\nxinstall_dir=\"$script_dir/.xkeen/02_install\"\nxdelete_dir=\"$script_dir/.xkeen/03_delete\"\nxtools_dir=\"$script_dir/.xkeen/04_tools\"\nxtests_dir=\"$script_dir/.xkeen/05_tests\"\nmain_dir=\"$script_dir/.xkeen\"\n\n# Модуль информации\n. \"$xinfo_dir/00_info_import.sh\"\n\n# Mirror-fallback helper. Загружается до install-модуля, потому что\n# install-модуль (04_install_geofile, 05_install_geoipset) вызывает\n# fetch_with_mirrors напрямую.\n. \"$xtools_dir/07_tools_downloaders/00_fetch_with_mirrors.sh\"\n\n# Модуль установки\n. \"$xinstall_dir/00_install_import.sh\"\n\n# Модуль удаления\n. \"$xdelete_dir/00_delete_import.sh\"\n\n# Модуль инструментария\n. \"$xtools_dir/00_tools_import.sh\"\n\n# Модуль тестирования\n. \"$xtests_dir/00_tests_import.sh\"\n\n# Модуль справки\n. \"$main_dir/about.sh\""
  },
  {
    "path": "scripts/xkeen",
    "content": "#!/bin/sh\n\n# Определение директории, где находится xkeen\nscript_dir=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\n# Скрываем основную директорию xkeen\ninstall_xkeen_rename() {\n    source_dir=\"_xkeen\"\n    target_dir=\".xkeen\"\n    source_path=\"$script_dir/$source_dir\"\n    target_path=\"$script_dir/$target_dir\"\n\n    if [ -d \"$source_path\" ]; then\n        if [ -d \"$target_path\" ]; then\n            rm -rf \"$target_path\" 2>/dev/null\n        fi\n        mv \"$source_path\" \"$target_path\"\n    fi\n    rm \"/opt/root/install.sh\" 2>/dev/null\n}\ninstall_xkeen_rename\n\nadd_chmod_init() {\n    [ -f \"$initd_file\" ] && chmod +x \"$initd_file\"\n}\n\n# Подсчет количества логических команд (параметров, начинающихся с - или --)\nLOGICAL_CMD_COUNT=0\nfor arg in \"$@\"; do\n    case \"$arg\" in\n        -*) LOGICAL_CMD_COUNT=$((LOGICAL_CMD_COUNT + 1)) ;;\n    esac\ndone\n\n# Функция умной очистки экрана\nsmart_clear() {\n    # Очищаем экран, только если запущена одна логическая команда\n    if [ \"$LOGICAL_CMD_COUNT\" -le 1 ]; then\n        clear\n    fi\n}\n\nfor arg in \"$@\"; do\n    [ \"$arg\" = \"-toff\" ] && touch \"/tmp/toff\"\n    [ \"$arg\" = \"-restart\" ] && apply=\"restart\"\n    case \"$arg\" in\n        -restart|-start|-stop) detach_eligible=true ;;\n    esac\ndone\n\n# Self-detach в новую session/pgid когда вызвали без TTY (ssh без -t, cron, CGI)\n# чтобы форсированный обрыв caller-сессии не убивал начальные проверки.\n# Подробности в commit-message. XKEEN_FOREGROUND=1 отключает детач для callers\n# требующих синхронной семантики (скрипты с && cleanup).\nif [ -n \"$detach_eligible\" ] && [ -z \"$XKEEN_DETACHED\" ] \\\n   && [ ! -t 0 ] && [ ! -t 1 ] && [ -z \"$XKEEN_FOREGROUND\" ]; then\n    export XKEEN_DETACHED=1\n    log_dir=\"/opt/var/log\"\n    [ -d \"$log_dir\" ] || mkdir -p \"$log_dir\"\n    log=\"$log_dir/xkeen-detached.log\"\n    {\n        echo\n        echo \"=== $(date '+%F %T') $0 $* ===\"\n    } >>\"$log\"\n    start-stop-daemon -S -b -n \"xkdtch.$$\" \\\n        -x \"$0\" -- \"$@\" \\\n        >>\"$log\" 2>&1\n    echo \"  Запуск в фоновом режиме, лог: $log\" >&2\n    exit 0\nfi\n\nget_state_arg() {\n    if [ \"$1\" = \"on\" ] || [ \"$1\" = \"off\" ]; then\n        echo \"$1\"\n    fi\n}\n\n# Импортируем модули\n. \"$script_dir/.xkeen/import.sh\"\n\n# Маркер /tmp/toff отключает curl_timeout — снимаем при прерывании\ntrap 'rm -f /tmp/toff; exit 1' INT TERM\n\t\nxkeen_info() {\n    # Проверяем версию XKeen\n    info_version_xkeen\n\n    # Проверяем актуальность XKeen\n    info_compare_xkeen\n}\n\nxkeen_set_info() {\n    # Определяем архитектуру процессора\n    info_cpu\n\n    # Проверяем установку Xray\n    info_xray\n\n    # Проверяем установку Mihomo\n    info_mihomo\n\n    # Проверяем установленные базы GeoSite\n    info_geosite\n\n    # Проверяем установленные базы GeoiIP\n    info_geoip\n\n    # Проверяем статус автообновления\n    info_cron\n\n    # Проверяем версию Xray\n    info_version_xray\n\n    # Проверяем версию Mihomo и Yq\n    info_version_mihomo\n    info_version_yq\n}\n\nwhile [ $# -gt 0 ]; do\n    case \"$1\" in\n\n        -i|-install)    # Запуск полного цикла установки\n            test_connection\n            test_entware\n            test_github\n            sleep 2\n\n            smart_clear\n            echo\n            check_keen_mode\n            if [ -n \"$keen_mode\" ]; then\n            echo -e \"  ${red}Ошибка${reset}: Установка XKeen возможна только на Keenetic в режиме ${green}роутера${reset}\"\n                exit 1\n            fi\n\n            echo -e \"  Запуск полного цикла установки ${yellow}XKeen${reset}\"\n\n            init_directories\n            xkeen_info\n            xkeen_set_info\n            logs_cpu_info_console\n\n            case \"$architecture\" in\n                arm64-v8a|mips32le|mips32) ;;\n                *) exit 1 ;;\n            esac\n\n            location_entware_storage\n            preinstall_warn\n\n            choice_add_proxy_cores\n\n           if [ \"$xray_installed\" != \"installed\" ] && [ \"$mihomo_installed\" != \"installed\" ] &&\n              [ \"$add_xray\" = \"false\" ] && [ \"$add_mihomo\" = \"false\" ]; then\n               echo -e \"  ${red}Невозможно установить${reset} XKeen без ядра проксирования\"\n               exit 1\n           fi\n\n            if [ \"$add_xray\" = \"true\" ]; then\n                smart_clear\n                echo\n                download_xray\n            else\n                command -v xray >/dev/null 2>&1 || bypass_xray=\"true\"\n            fi\n\n            if [ -z \"$bypass_xray\" ]; then\n                install_xray\n                info_xray\n            fi\n\n            if [ \"$xray_installed\" = \"installed\" ]; then\n                smart_clear\n                # Устанавливаем GeoSite\n                choice_geosite\n                delete_geosite\n                install_geosite\n                sleep 2\n\n                smart_clear\n                # Устанавливаем GeoIP\n                choice_geoip\n                delete_geoip\n                install_geoip\n                sleep 2\n            fi\n\n                smart_clear\n                # Устанавливаем GeoIPSET\n                install_geoipset init\n                sleep 2\n\n                if [ \"$bypass_cron_geosite\" = \"false\" ] || [ \"$bypass_cron_geoip\" = \"false\" ] || [ \"$bypass_cron_geoipset\" = \"false\" ]; then\n                    smart_clear\n                    # Настраиваем автоматические обновления\n                    info_cron\n                    choice_update_cron\n                    update_cron_geofile_task\n                    smart_clear\n                    choice_cron_time\n                    install_cron\n                    sleep 2\n                fi\n\n                if [ \"$xray_installed\" = \"installed\" ]; then\n                    smart_clear\n                    echo\n                    install_configs\n                fi\n\n                # Создаем init для cron\n                \"$initd_dir/S05crond\" stop >/dev/null 2>&1\n                [ -e \"$initd_dir/S05crond\" ] && rm -f \"$initd_dir/S05crond\"\n                register_cron_initd\n                \"$initd_dir/S05crond\" start >/dev/null 2>&1\n\n            if [ -z \"$bypass_xray\" ]; then\n                info_version_xray\n                delete_register_xray\n\n                echo\n                echo -e \"  Выполняется регистрация ${yellow}Xray${reset}\"\n                register_xray_list\n                logs_register_xray_list_info_console\n\n                register_xray_control\n                logs_register_xray_control_info_console\n\n                register_xray_status\n                logs_register_xray_status_info_console\n                sleep 2\n            fi\n\n            if [ \"$add_mihomo\" = \"true\" ]; then\n                smart_clear\n                echo\n                if ! download_mihomo; then\n                    bypass_mihomo=\"true\"\n                fi\n            else\n                command -v mihomo >/dev/null 2>&1 || bypass_mihomo=\"true\"\n            fi\n\n            if [ -z \"$bypass_mihomo\" ]; then\n                install_mihomo\n                info_mihomo\n            fi\n\n            if [ \"$mihomo_installed\" = \"installed\" ]; then\n                add_mihomo_config\n            fi\n\n            if [ -z \"$bypass_mihomo\" ]; then\n                info_version_mihomo\n                info_version_yq\n\n                delete_register_mihomo\n                echo\n                echo -e \"  Выполняется регистрация ${yellow}Mihomo${reset}\"\n                register_mihomo_list\n                logs_register_mihomo_list_info_console\n                register_mihomo_control\n                logs_register_mihomo_control_info_console\n                register_mihomo_status\n                logs_register_mihomo_status_info_console\n                register_yq_list\n                logs_register_yq_list_info_console\n                register_yq_control\n                logs_register_yq_control_info_console\n                register_yq_status\n                logs_register_yq_status_info_console\n                sleep 2\n            fi\n\n            if [ \"$mihomo_installed\" != \"installed\" ] && [ \"$xray_installed\" != \"installed\" ]; then\n                echo\n                echo -e \"  ${red}Ошибка${reset}: Установка прервана. Не установлено ни одного ядра проксирования (Xray или Mihomo)\"\n                exit 1\n            fi\n\n            if [ \"$xray_installed\" = \"installed\" ] || [ \"$mihomo_installed\" = \"installed\" ]; then\n                delete_register_xkeen\n                smart_clear\n                echo\n                echo -e \"  Выполняется регистрация ${yellow}XKeen${reset}\"\n                register_xkeen_list\n                logs_register_xkeen_list_info_console\n\n                register_xkeen_control\n                logs_register_xkeen_control_info_console\n\n                register_xkeen_status\n                logs_register_xkeen_status_info_console\n\n                fixed_register_packages\n\n                migrate_ports_from_initd\n                register_xkeen_initd\n                create_xkeen_cfg\n                sleep 2\n\n                smart_clear\n                choice_autostart_xkeen\n            fi\n\n            if [ \"$xray_installed\" != \"installed\" ] && [ \"$mihomo_installed\" = \"installed\" ]; then\n                if [ -f \"$install_dir/mihomo\" ] && [ -f \"$install_dir/yq\" ] && grep -q 'name_client=\"xray\"' \"$initd_file\"; then\n                    sed -i 's/name_client=\"xray\"/name_client=\"mihomo\"/' \"$initd_file\"\n                fi\n            elif  [ \"$xray_installed\" = \"installed\" ] && [ \"$mihomo_installed\" != \"installed\" ]; then\n                if [ -f \"$install_dir/xray\" ] && grep -q 'name_client=\"mihomo\"' \"$initd_file\"; then\n                    sed -i 's/name_client=\"mihomo\"/name_client=\"xray\"/' \"$initd_file\"\n                fi\n            fi\n\n            add_chmod_init\n\n            if pidof xray >/dev/null || pidof mihomo >/dev/null ; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n\n            # Удаляем временные файлы\n            delete_tmp\n            sleep 2\n\n            smart_clear\n            echo\n            echo -e \"  ${green}Установка XKeen завершена!${reset}\"\n\n            if [ \"$xray_installed\" = \"installed\" ] || [ \"$mihomo_installed\" = \"installed\" ]; then\n                if grep -q 'name_client=\"xray\"' \"$initd_file\"; then\n                    echo -e \"  1. Настройте конфигурацию Xray по пути '${yellow}$xray_conf_dir/${reset}'\"\n                    echo -e \"  2. Запустите XKeen командой ${yellow}xkeen -start${reset}\"\n                    echo -e \"  3. ${green}Enjoy!${reset}\"\n                    echo\n                elif grep -q 'name_client=\"mihomo\"' \"$initd_file\"; then\n                    echo -e \"  1. Настройте конфигурацию Mihomo в файле '${yellow}$mihomo_conf_dir/config.yaml${reset}'\"\n                    echo -e \"  2. Запустите XKeen командой ${yellow}xkeen -start${reset}\"\n                    echo -e \"  3. ${green}Enjoy!${reset}\"\n                    echo\n                fi\n            fi\n\n            if [ \"$xray_installed\" = \"installed\" ] && [ \"$mihomo_installed\" = \"installed\" ]; then\n                if grep -q 'name_client=\"xray\"' \"$initd_file\"; then\n                    echo -e \"  Если хотите переключить XKeen на ядро ${yellow}Mihomo${reset}\"\n                    echo\n                    echo -e \"  1. Настройте конфигурацию Mihomo в файле '${yellow}$mihomo_conf_dir/config.yaml${reset}'\"\n                    echo -e \"  2. Переключите ядро проксирования командой ${yellow}xkeen -mihomo${reset}\"\n                    echo -e \"  3. Запустите XKeen командой ${yellow}xkeen -start${reset}\"\n                    echo -e \"  4. ${green}Enjoy!${reset}\"\n                elif grep -q 'name_client=\"mihomo\"' \"$initd_file\"; then\n                    echo -e \"  Если хотите переключить XKeen на ядро ${yellow}Xray${reset}\"\n                    echo\n                    echo -e \"  1. Настройте конфигурацию Xray по пути '${yellow}$xray_conf_dir/${reset}'\"\n                    echo -e \"  2. Переключите ядро проксирования командой ${yellow}xkeen -xray${reset}\"\n                    echo -e \"  3. Запустите XKeen командой ${yellow}xkeen -start${reset}\"\n                    echo -e \"  4. ${green}Enjoy!${reset}\"\n                fi\n            echo\n            fi\n\n            echo -e \"  Для вывода Справки выполните ${yellow}xkeen -h${reset}\"\n        ;;\n\n\n        -io)    # Установка XKeen OffLine\n            smart_clear\n            echo\n            check_keen_mode\n            if [ -n \"$keen_mode\" ]; then\n            echo -e \"  ${red}Ошибка${reset}: Установка XKeen возможна только на Keenetic в режиме ${green}роутера${reset}\"\n                exit 1\n            fi\n            echo \"  Установка XKeen OffLine\"\n\n            init_directories\n            xkeen_set_info\n            logs_cpu_info_console\n\n            case \"$architecture\" in\n                arm64-v8a|mips32le|mips32) ;;\n                *) exit 1 ;;\n            esac\n\n            if [ -f \"$install_dir/xray\" ]; then\n                chmod +x \"$install_dir/xray\"\n            elif [ -f \"$install_dir/mihomo\" ]; then\n                chmod +x \"$install_dir/mihomo\"\n                if [ -f \"$install_dir/yq\" ]; then\n                    chmod +x \"$install_dir/yq\"\n                else\n                    echo -e \"  ${red}Не найден${reset} парсер конфигурационных файлов Mihomo - Yq\"\n                    exit 1\n                fi\n            else\n                smart_clear\n                echo\n                echo -e \"  ${red}Не найдено ядро проксирования xray или mihomo${reset}\"\n                echo\n                echo -e \"  Если планируете использовать ядро xray, поместите бинарник ${yellow}xray${reset}\\n  архитектуры ${green}$architecture${reset} в директорию /opt/sbin/ и начните установку снова\"\n                echo -e \"  Страница загрузок xray: ${yellow}https://github.com/XTLS/Xray-core/releases/latest${reset}\"\n                echo\n                echo -e \"  Если планируете использовать ядро mihomo, поместите бинарники ${yellow}mihomo${reset} и ${yellow}yq${reset}\\n  архитектуры ${green}$architecture${reset} в директорию /opt/sbin/ и начните установку снова\"\n                echo -e \"  Страница загрузок mihomo: ${yellow}https://github.com/MetaCubeX/mihomo/releases/latest${reset}\"\n                echo -e \"  Страница загрузок yq: ${yellow}https://github.com/mikefarah/yq/releases/latest${reset}\"\n                echo\n                exit 1\n            fi\n\n            if [ -f \"$install_dir/xray\" ]; then\n                install_configs\n\n                if [ ! -d \"$geo_dir\" ]; then\n                    mkdir -p \"$geo_dir\"\n                fi\n\n                smart_clear\n                delete_register_xray\n                info_xray\n                info_version_xray\n                echo\n                echo -e \"  Выполняется регистрация ${yellow}Xray${reset}\"\n                register_xray_list\n                logs_register_xray_list_info_console\n                register_xray_control\n                logs_register_xray_control_info_console\n                register_xray_status\n                logs_register_xray_status_info_console\n            fi\n\n            if [ -f \"$install_dir/mihomo\" ]; then\n                add_mihomo_config\n                delete_register_mihomo\n                info_mihomo\n                info_version_mihomo\n                info_version_yq\n                echo\n                echo -e \"  Выполняется регистрация ${yellow}Mihomo${reset}\"\n                register_mihomo_list\n                logs_register_mihomo_list_info_console\n                register_mihomo_control\n                logs_register_mihomo_control_info_console\n                register_mihomo_status\n                logs_register_mihomo_status_info_console\n                register_yq_list\n                logs_register_yq_list_info_console\n                register_yq_control\n                logs_register_yq_control_info_console\n                register_yq_status\n                logs_register_yq_status_info_console\n                sleep 2\n            fi\n\n            smart_clear\n            delete_register_xkeen\n            echo\n            echo -e \"  Выполняется регистрация ${yellow}XKeen${reset}\"\n            register_xkeen_list\n            logs_register_xkeen_list_info_console\n\n            register_xkeen_control\n            logs_register_xkeen_control_info_console\n\n            register_xkeen_status\n            logs_register_xkeen_status_info_console\n\n            migrate_ports_from_initd\n            register_xkeen_initd\n            create_xkeen_cfg\n\n            fixed_register_packages\n\n            smart_clear\n            choice_autostart_xkeen\n            add_chmod_init\n\n            if pidof xray >/dev/null || pidof mihomo >/dev/null ; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n\n            # Удаляем временные файлы\n            delete_tmp\n            sleep 2\n\n            smart_clear\n            echo\n            echo -e \"  ${green}Установка XKeen завершена!${reset}\"\n            echo\n            echo -e \"  Для использования ядра '${yellow}$Xray${reset}'\"\n            echo -e \"  1. Поместите необходимые геофайлы в директорию '${yellow}$geo_dir/${reset}'\"\n            echo -e \"  2. Настройте конфигурацию Xray по пути '${yellow}$xray_conf_dir/${reset}'\"\n            echo -e \"  3. Запустите XKeen командой ${yellow}xkeen -start${reset}\"\n            echo -e \"  4. ${green}Enjoy!${reset}\"\n            echo\n            echo -e \"  Для использования ядра ${yellow}Mihomo${reset}\"\n            echo -e \"  1. Настройте конфигурацию Mihomo в файле '${yellow}$mihomo_conf_dir/config.yaml${reset}'\"\n            echo -e \"  2. Переключите ядро проксирования командой ${yellow}xkeen -mihomo${reset}\"\n            echo -e \"  3. Запустите XKeen командой ${yellow}xkeen -start${reset}\"\n            echo -e \"  4. ${green}Enjoy!${reset}\"\n            echo\n            echo -e \"  Для вывода Справки выполните ${yellow}xkeen -h${reset}\"\n        ;;\n\n\n        -ug)    # Обновление баз GeoFile/GeoIPSET\n            test_connection\n            test_github\n            sleep 2\n\n            smart_clear\n            echo\n            echo \"  Обновление установленных баз GeoFile/GeoIPSET\"\n            info_geosite\n            info_geoip\n            if \n                [ \"$update_refilter_geosite\" = \"true\" ] || \\\n                [ \"$update_v2fly_geosite\" = \"true\" ] || \\\n                [ \"$update_zkeen_geosite\" = \"true\" ] || \\\n                [ \"$update_refilter_geoip\" = \"true\" ] || \\\n                [ \"$update_v2fly_geoip\" = \"true\" ] || \\\n                [ \"$update_zkeenip_geoip\" = \"true\" ]; then\n\n                echo\n                install_geosite\n                install_geoip\n                install_geoipset update\n\n                if pidof xray >/dev/null; then\n                    \"$initd_file\" restart on >/dev/null 2>&1\n                fi\n\n                echo -e \"  Обновление установленных баз GeoFile/GeoIPSET ${green}завершено${reset}\"\n            else\n                echo -e \"  ${red}Не обнаружены${reset} базы GeoFile/GeoIPSET для обновления\"\n            fi\n        ;;\n\n\n        -uk)    # Обновление XKeen\n            test_connection\n            test_github\n            sleep 2\n\n            smart_clear\n            echo\n            echo \"  Проверка обновлений XKeen\"\n            xkeen_info\n            xkeen_set_info\n\n            if [ \"$xkeen_build\" != \"Stable\" ]; then\n                download_func=\"download_xkeen_dev\"\n            else\n                if [ \"$info_compare_xkeen\" = \"actual\" ]; then\n                    echo \"  Нет доступных обновлений XKeen\"\n                    exit 0\n                else\n                    echo -e \"  Найдена новая версия ${yellow}XKeen${reset}\"\n                    download_func=\"download_xkeen\"\n                fi\n            fi\n\n            backup_xkeen\n            \"$download_func\"\n            install_xkeen\n\n            # Перезапуск скрипта\n            grep -E \"^\\s*-uk_post_update\\s*\\)\" \"$0\" > /dev/null && exec sh \"$0\" -uk_post_update\n        ;;\n\n\n        -uk_post_update)\n            . \"/opt/sbin/.xkeen/import.sh\"\n            init_directories\n            xkeen_info\n            xkeen_set_info\n            info_packages\n            install_packages\n\n            echo -e \"  Выполняется отмена регистрации предыдущей версии ${yellow}XKeen${reset}\"\n            delete_register_xkeen\n            logs_delete_register_xkeen_info_console\n\n            echo -e \"  Выполняется регистрация новой версии ${yellow}XKeen${reset}\"\n            register_xkeen_list\n            logs_register_xkeen_list_info_console\n\n            register_xkeen_control\n            logs_register_xkeen_control_info_console\n\n            register_xkeen_status\n            logs_register_xkeen_status_info_console\n\n            register_cron_initd\n            migrate_ports_from_initd\n            register_xkeen_initd\n            create_xkeen_cfg\n            choice_cancel_cron_select=true\n            update_cron_geofile_task\n            fixed_register_packages\n\n            if pidof xray >/dev/null || pidof mihomo >/dev/null ; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n\n            delete_tmp\n\n            echo -e \"  Обновление XKeen ${green}выполнено${reset}\"\n        ;;\n\n\n        -ux)    # Обновление или установка ядра Xray\n            test_connection\n            test_github\n            sleep 2\n\n            . \"/opt/sbin/.xkeen/01_info/03_info_cpu.sh\"\n            status_file=\"/opt/lib/opkg/status\"\n            info_cpu\n            smart_clear\n            info_xray\n            info_version_xray\n\n            [ \"$xray_installed\" = \"installed\" ] && echo -e \"  В роутере установлен Xray версии ${yellow}$xray_current_version${reset}\" && echo\n            download_xray\n\n            if [ -z \"$bypass_xray\" ]; then\n                install_xray\n\n                if [ \"$xray_installed\" = \"installed\" ]; then\n                    echo -e \"  Выполняется отмена регистрации предыдущей версии ${yellow}Xray${reset}\"\n                    delete_register_xray\n                    logs_delete_register_xray_info_console\n\n                    info_version_xray\n                    echo -e \"  Выполняется регистрация новой версии ${yellow}Xray${reset}\"\n                    register_xray_list\n                    logs_register_xray_list_info_console\n                    register_xray_control\n                    logs_register_xray_control_info_console\n                    register_xray_status\n                    logs_register_xray_status_info_console\n\n                    sleep 2\n                    if pidof xray >/dev/null; then\n                        \"$initd_file\" restart on >/dev/null 2>&1\n                    fi\n\n                    echo\n                    echo -e \"  Обновление ядра Xray ${green}выполнено${reset}\"\n                else\n                    xray_installed=\"installed\"\n                    info_version_xray\n\n                    if [ -f \"$install_dir/xray\" ]; then\n                        install_configs\n        \n                        if [ ! -d \"$geo_dir\" ]; then\n                            mkdir -p \"$geo_dir\"\n                        fi\n\n                        delete_register_xray\n                        echo\n                        echo -e \"  Выполняется регистрация ${yellow}Xray${reset}\"\n                        register_xray_list\n                        logs_register_xray_list_info_console\n                        register_xray_control\n                        logs_register_xray_control_info_console\n                        register_xray_status\n                        logs_register_xray_status_info_console\n                        sleep 2\n                        smart_clear\n                        echo\n                        echo -e \"  Установка ядра ${yellow}Xray${reset} ${green}выполнена${reset}\"\n                    fi\n                fi\n            fi\n\n            fixed_register_packages\n            delete_tmp\n        ;;\n\n\n        -um)    # Обновление или установка ядра Mihomo\n            test_connection\n            test_github\n            sleep 2\n\n            . \"/opt/sbin/.xkeen/01_info/03_info_cpu.sh\"\n            status_file=\"/opt/lib/opkg/status\"\n            info_cpu\n            smart_clear\n            info_mihomo\n            info_version_mihomo\n            mihomo_was_installed=\"$mihomo_installed\"\n\n            [ \"$mihomo_installed\" = \"installed\" ] && echo -e \"  В роутере установлен Mihomo версии ${yellow}$mihomo_current_version${reset}\" && echo\n            if ! download_mihomo; then\n                delete_tmp\n                exit 1\n            fi\n\n            if [ -z \"$bypass_mihomo\" ]; then\n                install_mihomo\n                info_mihomo\n\n                if [ \"$mihomo_installed\" != \"installed\" ]; then\n                    delete_tmp\n                    echo -e \"  ${red}Ошибка${reset}: Mihomo не установлен, так как отсутствует обязательный Yq\"\n                    exit 1\n                elif [ \"$mihomo_was_installed\" = \"installed\" ]; then\n                    echo -e \"  Выполняется отмена регистрации предыдущей версии ${yellow}Mihomo${reset}\"\n                    delete_register_mihomo\n                    logs_delete_register_mihomo_info_console\n                    logs_delete_register_yq_info_console\n\n                    info_version_mihomo\n                    info_version_yq\n                    echo -e \"  Выполняется регистрация новой версии ${yellow}Mihomo${reset}\"\n                    register_mihomo_list\n                    logs_register_mihomo_list_info_console\n                    register_mihomo_control\n                    logs_register_mihomo_control_info_console\n                    register_mihomo_status\n                    logs_register_mihomo_status_info_console\n                    register_yq_list\n                    logs_register_yq_list_info_console\n                    register_yq_control\n                    logs_register_yq_control_info_console\n                    register_yq_status\n                    logs_register_yq_status_info_console\n\n                    if pidof mihomo >/dev/null; then\n                        \"$initd_file\" restart on >/dev/null 2>&1\n                    fi\n                    echo\n                    echo -e \"  Обновление ядра ${yellow}Mihomo${reset} ${green}выполнено${reset}\"\n                else\n                    info_version_mihomo\n                    info_version_yq\n                    add_mihomo_config\n                    delete_register_mihomo\n                    echo\n                    echo -e \"  Выполняется регистрация ${yellow}Mihomo${reset}\"\n                    register_mihomo_list\n                    logs_register_mihomo_list_info_console\n                    register_mihomo_control\n                    logs_register_mihomo_control_info_console\n                    register_mihomo_status\n                    logs_register_mihomo_status_info_console\n                    register_yq_list\n                    logs_register_yq_list_info_console\n                    register_yq_control\n                    logs_register_yq_control_info_console\n                    register_yq_status\n                    logs_register_yq_status_info_console\n                    sleep 2\n                    smart_clear\n                    echo\n                    echo -e \"  Установка ядра ${yellow}Mihomo${reset} ${green}выполнена${reset}\"\n                fi\n            fi\n\n            fixed_register_packages\n            delete_tmp\n        ;;\n\n\n        -ugc)    # Создать или изменить существующюю задачу автообновления баз GeoFile/GeoIPSET\n            info_cron\n            smart_clear\n            echo -e \"  Создание или изменение задачи автообновления баз ${yellow}GeoFile/GeoIPSET${reset}\"\n            choice_update_cron\n            update_cron_geofile_task\n            choice_cron_time\n            install_cron\n            delete_tmp\n            echo -e \"  Создание или изменение задачи автообновления баз GeoFile/GeoIPSET ${green}выполнено${reset}\"\n        ;;\n\n\n        -ri)    # Пересоздать файл автозапуска XKeen\n            smart_clear\n            \"$initd_file\" stop >/dev/null 2>&1\n            [ -e \"$initd_file\" ] && rm -f \"$initd_file\"\n\n            echo -e \"  Создание файла автозапуска ${yellow}XKeen${reset}\"\n            sleep 1\n\n            migrate_ports_from_initd\n            register_xkeen_initd\n            logs_register_xkeen_initd_info_console\n\n            echo\n            echo -e \"  Создание файла автозапуска XKeen ${green}выполнено${reset}\"\n            echo -e \"  Если конфигурация настроена, то можете запустить проксирование командой '${yellow}xkeen -start${reset}'\"\n        ;;\n\n\n        -dgc)    # Удалить задачу автообновления баз GeoFile/GeoIPSET\n            smart_clear\n            echo\n            choice_for_remove=\"задачу автообновления баз GeoFile/GeoIPSET\"\n            choice_remove\n\n            info_cron\n\n            smart_clear\n            echo\n            echo -e \"  Удаление задачи автообновления баз ${yellow}GeoFile/GeoIPSET${reset}\"\n\n            delete_cron_geofile\n            logs_delete_cron_geofile_info_console\n            delete_tmp\n\n            echo -e \"  Удаление задачи автообновления баз GeoFile/GeoIPSET ${green}выполнено${reset}\"\n        ;;\n\n\n        -dx)    # Удалить Xray\n            smart_clear\n            echo\n            choice_for_remove=\"Xray\"\n            choice_remove\n            smart_clear\n            echo\n            command -v xray >/dev/null 2>&1 || { echo -e \"  Xray ${red}не установлен${reset}\"; exit 1; }\n            echo -e \"  Удаление ${yellow}Xray${reset}\"\n\n            \"$initd_file\" stop >/dev/null 2>&1\n            opkg remove xray_s\n            rm -f \"$install_dir/xray\"\n\n            echo\n            echo -e \"  Удаление ${yellow}конфигурационных файлов Xray${reset}\"\n\n            delete_configs\n            logs_delete_configs_info_console\n\n            echo\n            echo -e \"  Удаление Xray ${green}выполнено${reset}\"\n        ;;\n\n\n        -dm)    # Удалить Mihomo\n            smart_clear\n            echo\n            choice_for_remove=\"Mihomo\"\n            choice_remove\n            smart_clear\n            echo\n            command -v mihomo >/dev/null 2>&1 || { echo -e \"  Mihomo ${red}не установлен${reset}\"; exit 1; }\n            echo -e \"  Удаление ${yellow}Mihomo${reset}\"\n\n            \"$initd_file\" stop >/dev/null 2>&1\n            opkg remove mihomo_s\n            opkg remove yq_s\n            rm -f \"$install_dir/mihomo\" \"$install_dir/yq\"\n            rm -rf \"$mihomo_conf_dir\"\n\n            echo\n            echo -e \"  Удаление Mihomo ${green}выполнено${reset}\"\n        ;;\n\n\n        -dk)    # Удалить XKeen\n            smart_clear\n            echo\n            choice_for_remove=\"XKeen\"\n            choice_remove\n\n            smart_clear\n            echo\n            echo -e \"  Удаление ${yellow}XKeen${reset}\"\n            opkg remove xkeen\n            delete_tmp\n\n            smart_clear\n            echo\n            delete_all\n\n            smart_clear\n            echo\n            echo -e \"  Удаление XKeen ${green}выполнено${reset}\"\n            echo\n            echo -e \"  Установить ${yellow}XKeen${reset} заново можно командами:\"\n            echo\n            echo -e \"  ${green}opkg update && opkg upgrade && opkg install curl tar && cd /tmp${reset}\"\n            echo -e \"  ${green}sh -c \\\"\\$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/install.sh)\\\"${reset}\"\n\n\n        ;;\n\n\n        -dgi)    # Удалить GeoIP\n            smart_clear\n            echo\n            choice_for_remove=\"GeoIP\"\n            choice_remove\n\n            smart_clear\n            echo\n            echo -e \"  Удаление всех баз ${yellow}GeoIP${reset}\"\n\n            delete_geoip_key\n            logs_delete_geoip_info_console\n\n            echo\n            echo -e \"  Удаление всех баз GeoIP ${green}выполнено${reset}\"\n        ;;\n\n\n        -dgs)    # Удалить GeoSite\n            smart_clear\n            echo\n            choice_for_remove=\"GeoSite\"\n            choice_remove\n\n            smart_clear\n            echo\n            echo -e \"  Удаление всех баз ${yellow}GeoSite${reset}\"\n\n            delete_geosite_key\n            logs_delete_geosite_info_console\n\n            echo\n            echo -e \"  Удаление всех баз GeoSite ${green}выполнено${reset}\"\n        ;;\n\n\n        -remove)    # Полная деинсталляция XKeen\n            smart_clear\n            choice_for_remove=\"XKeen полностью со всеми зависимостями\"\n            choice_remove\n\n            info_cron\n            smart_clear\n            echo\n            echo -e \"  Удаление задачи автообновления баз ${yellow}GeoFile${reset}\"\n            delete_cron_geofile\n            logs_delete_cron_geofile_info_console\n\n            echo\n            echo -e \"  Удаление задачи автообновления баз GeoFile ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление GeoSite's\n            smart_clear\n            echo\n            echo -e \"  Удаление всех баз ${yellow}GeoSite${reset}\"\n\n            delete_geosite_key\n            logs_delete_geosite_info_console\n\n            echo -e \"  Удаление всех баз GeoSite ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление GeoIP's\n            smart_clear\n            echo\n            echo -e \"  Удаление всех баз ${yellow}GeoIP${reset}\"\n\n            delete_geoip_key\n            logs_delete_geoip_info_console\n\n            echo -e \"  Удаление всех баз GeoIP ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление GeoIPSET\n            smart_clear\n            echo\n            echo -e \"  Удаление ${yellow}GeoIPSET${reset}\"\n\n            delete_geoipset_key\n            logs_delete_geoipset_info_console\n\n            echo -e \"  Удаление GeoIPSET ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление файлов конфигурации Xray\n            smart_clear\n            echo\n            echo -e \"  Удаление ${yellow}конфигурационных файлов Xray${reset}\"\n\n            delete_configs\n            logs_delete_configs_info_console\n\n            echo\n            echo -e \"  Удаление конфигурационных файлов Xray ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление Xray\n            smart_clear\n            echo\n            echo -e \"  ${yellow}Удаление${reset} Xray\"\n\n            \"$initd_file\" stop >/dev/null 2>&1\n            opkg remove xray_s\n            rm -f \"$install_dir/xray\"\n            rm -rf \"/opt/etc/xray\"\n\n            echo\n            echo -e \"  Удаление Xray ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление Mihomo\n            smart_clear\n            echo\n            echo -e \"  ${yellow}Удаление${reset} Mihomo\"\n            opkg remove mihomo_s\n            opkg remove yq_s\n            rm -f \"$install_dir/mihomo\" \"$install_dir/yq\"\n            rm -rf \"$mihomo_conf_dir\"\n\n            echo\n            echo -e \"  Удаление Mihomo ${green}выполнено${reset}\"\n            sleep 2\n\n            # Удаление XKeen\n            smart_clear\n            echo\n            echo -e \"  Удаление ${yellow}XKeen${reset}\"\n            opkg remove xkeen\n            delete_tmp\n\n            smart_clear\n            delete_all\n\n            smart_clear\n            echo\n            echo -e \"  Полная деинсталляция ${yellow}XKeen${reset} и всех зависимостей ${green}выполнена${reset}\"\n            echo\n            echo -e \"  Установить ${yellow}XKeen${reset} заново можно командами:\"\n            echo\n            echo -e \"  ${green}opkg update && opkg upgrade && opkg install curl tar && cd /tmp${reset}\"\n            echo -e \"  ${green}sh -c \\\"\\$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/install.sh)\\\"${reset}\"\n        ;;\n\n\n        -k)    # Переустановка XKeen\n            . \"/opt/sbin/.xkeen/01_info/03_info_cpu.sh\"\n            status_file=\"/opt/lib/opkg/status\"\n            xkeen_info\n            xkeen_set_info\n\n            smart_clear\n            echo\n            echo -e \"  Переустановка ${yellow}XKeen${reset}\"\n\n            choice_redownload_xkeen\n            if [ -n \"$redownload_xkeen\" ]; then\n                if [ \"$xkeen_build\" = \"Stable\" ]; then\n                    download_func=\"download_xkeen\"\n                else\n                    download_func=\"download_xkeen_dev\"\n                fi\n                test_connection\n                test_entware\n                test_github\n                sleep 2\n                \"$download_func\"\n            else\n                opkg update >/dev/null 2>&1\n                info_packages\n                install_packages\n            fi\n\n            echo\n            install_xkeen\n\n            # Перезапуск скрипта\n            grep -E \"^\\s*-k_post_install\\s*\\)\" \"$0\" > /dev/null && exec sh \"$0\" -k_post_install\n        ;;\n\n\n        -k_post_install)\n            . \"/opt/sbin/.xkeen/import.sh\"\n            init_directories\n            xkeen_info\n            xkeen_set_info\n            info_packages\n            install_packages\n\n            echo -e \"  Выполняется отмена регистрации предыдущей версии ${yellow}XKeen${reset}\"\n            delete_register_xkeen\n            logs_delete_register_xkeen_info_console\n\n            echo -e \"  Выполняется регистрация новой версии ${yellow}XKeen${reset}\"\n            register_xkeen_list\n            logs_register_xkeen_list_info_console\n\n            register_xkeen_control\n            logs_register_xkeen_control_info_console\n\n            register_xkeen_status\n            logs_register_xkeen_status_info_console\n\n            register_cron_initd\n            migrate_ports_from_initd\n            register_xkeen_initd\n            create_xkeen_cfg\n            choice_cancel_cron_select=true\n            update_cron_geofile_task\n            fixed_register_packages\n\n            if pidof xray >/dev/null || pidof mihomo >/dev/null ; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n\n            delete_tmp\n\n            echo\n            echo -e \"  Переустановка XKeen ${green}выполнена${reset}\"\n        ;;\n\n\n        -g)    # Установка баз GeoFile\n            command -v xray >/dev/null 2>&1 || { echo -e \"  ${red}Не обнаружено${reset} ядро проксирования Xray\"; exit 1; }\n            test_connection\n            test_github\n            sleep 2\n\n            smart_clear\n            info_geosite\n            info_geoip\n\n            choice_geosite\n            delete_geosite\n            install_geosite\n            sleep 2\n\n            smart_clear\n            choice_geoip\n            delete_geoip\n            install_geoip\n            sleep 2\n\n            smart_clear\n            echo\n            echo -e \"  Установка баз GeoFile ${green}выполнена${reset}\"\n        ;;\n\n\n        -gips)    # Установка GeoIPSET\n            test_connection\n            test_github\n\n            smart_clear\n            install_geoipset init\n        ;;\n\n\n        -dgips)    # Удаление GeoIPSET\n            smart_clear\n            delete_geoipset\n        ;;\n\n\n        -kb)    # Резервное копирование XKeen\n            echo -e \"  Создание резервной копии ${yellow}XKeen${reset}\"\n            info_version_xkeen\n            manual_backup=\"on\"\n            backup_xkeen\n        ;;\n\n\n        -kbr)    # Восстановление XKeen из резервной копии\n            echo -e \"  Восстановление ${yellow}XKeen${reset} из резервной копии\"\n            restore_backup_xkeen\n        ;;\n\n\n        -xb)    # Резервное копирование конфигурации Xray\n            command -v xray >/dev/null 2>&1 || { echo -e \"  ${red}Не обнаружено${reset} ядро проксирования Xray\"; exit 1; }\n            echo -e \"  Создание резервной копии ${yellow}конфигурации Xray${reset}\"\n            backup_configs_xray\n        ;;\n\n\n        -xbr)    # Восстановление конфигурации Xray из резервной копии\n            command -v xray >/dev/null 2>&1 || { echo -e \"  ${red}Не обнаружено${reset} ядро проксирования Xray\"; exit 1; }\n            echo -e \"  Восстановление ${yellow}конфигурации Xray${reset} из резервной копии\"\n            restore_backup_configs_xray\n        ;;\n\n\n        -mb)    # Резервное копирование конфигурации Mihomo\n            command -v mihomo >/dev/null 2>&1 || { echo -e \"  ${red}Не обнаружено${reset} ядро проксирования Mihomo\"; exit 1; }\n            echo -e \"  Создание резервной копии ${yellow}конфигурации Mihomo${reset}\"\n            backup_configs_mihomo\n        ;;\n\n\n        -mbr)    # Восстановление конфигурации Mihomo из резервной копии\n            command -v mihomo >/dev/null 2>&1 || { echo -e \"  ${red}Не обнаружено${reset} ядро проксирования Mihomo\"; exit 1; }\n            echo -e \"  Восстановление ${yellow}конфигурации Mihomo${reset} из резервной копии\"\n            restore_backup_configs_mihomo\n        ;;\n\n\n        -tp)    # Показать прослушиваемые порты\n            echo \"  Определение прослушиваемых портов\"\n            tests_ports_client\n        ;;\n\n\n        -v|-version)    # Показать версию XKeen\n            echo -e \"  Версия ${yellow}XKeen $xkeen_current_version $xkeen_build${reset} (время сборки: ${light_blue}$build_timestamp${reset})\"\n\n            info_xray\n            info_version_xray\n            info_mihomo\n            info_version_mihomo\n            info_version_yq\n\n            if [ -f \"$install_dir/xray\" ] && grep -q 'name_client=\"xray\"' \"$initd_file\"; then\n                echo -e \"  Ядро проксирования Xray версии ${yellow}$xray_current_version${reset}\"\n            elif [ -f \"$install_dir/mihomo\" ] && grep -q 'name_client=\"mihomo\"' \"$initd_file\"; then\n                echo -e \"  Ядро проксирования Mihomo версии ${yellow}$mihomo_current_version${reset}\"\n                echo -e \"  Парсер конфигурационных файлов Yq версии ${yellow}$yq_current_version${reset}\"\n            else\n                echo -e \"  Ядро проксирования ${red}не установлено${reset}\"\n            fi\n        ;;\n\n\n        -about)    # О программе\n            smart_clear\n            about_xkeen\n        ;;\n\n\n        -ad|-donate)    # Поддержать разработчиков\n            smart_clear\n            author_donate\n        ;;\n\n\n        -af|-feedback)    # Обратная связь\n            smart_clear\n            author_feedback\n        ;;\n\n\n        -h|-help)    # Помощь\n            smart_clear\n            help_xkeen\n        ;;\n\n\n        -start)    # Запуск XKeen\n            add_chmod_init\n            \"$initd_file\" start on\n        ;;\n\n\n        -stop)    # Остановка XKeen\n            add_chmod_init\n            \"$initd_file\" stop\n        ;;\n\n\n        -restart)    # Перезапуск XKeen\n            add_chmod_init\n            \"$initd_file\" restart on\n        ;;\n\n\n        -status)    # Состояние XKeen\n            \"$initd_file\" status\n        ;;\n\n\n        -auto)    # Смена режима запуска XKeen\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_autostart_xkeen \"$action\"\n            add_chmod_init\n        ;;\n\n\n        -fd)    # Смена режима контроля файловых дескрипторов\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_file_descriptors \"$action\"\n        ;;\n\n\n        -cfd)    # Проверка количества открытых файловых дескрипторов\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            check_file_descriptors\n        ;;\n\n\n        -ap)    # Добавить порт проксирования\n            shift\n            add_ports_donor \"$*\"\n            sleep 2\n            add_chmod_init\n            if pidof xray >/dev/null || pidof mihomo >/dev/null; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n        ;;\n\n\n        -dp)    # Удалить порт проксирования\n            shift\n            dell_ports_donor \"$*\"\n            sleep 2\n            add_chmod_init\n            if pidof xray >/dev/null || pidof mihomo >/dev/null; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n        ;;\n\n\n        -cp)    # Получить список портов проксирования\n            get_ports_donor\n        ;;\n\n\n        -ape)    # Добавить порт-исключение проксирования\n            shift\n            add_ports_exclude \"$*\" \n            sleep 2\n            add_chmod_init\n            if pidof xray >/dev/null || pidof mihomo >/dev/null; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n        ;;\n\n\n        -dpe)    # Удалить порт-исключение проксирования\n            shift\n            dell_ports_exclude \"$*\"\n            sleep 2\n            add_chmod_init\n            if pidof xray >/dev/null || pidof mihomo >/dev/null; then\n                \"$initd_file\" restart on >/dev/null 2>&1\n            fi\n        ;;\n\n\n        -cpe)    # Получить список портов, исключёных из проксирования\n            get_ports_exclude\n        ;;\n\n\n        -modules)    # Obsolete\n            smart_clear\n            echo\n            migration_modules\n        ;;\n\n        -delmodules)    # Obsolete\n            smart_clear\n            echo\n            remove_modules\n        ;;\n\n\n        -d)    # Установка задержки автозапуска в секундах\n            shift\n            delay_autostart \"$1\"\n            add_chmod_init\n        ;;\n\n\n        -diag)    # Диагностика XKeen\n            location_entware_storage\n            smart_clear\n            diagnostic\n        ;;\n\n\n        -channel)    # Смена канала обновлений XKeen (Stable/Dev)\n            smart_clear\n            choice_channel_xkeen\n            change_channel_xkeen\n        ;;\n\n\n        -xray)    # Смена ядра проксирования на Xray\n            choice_xray_core\n        ;;\n\n\n        -mihomo)    # Смена ядра проксирования на Mihomo\n            choice_mihomo_core\n        ;;\n\n\n        -ipv6)    # Включение/отключение IPv6 в KeeneticOS\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_ipv6_support \"$action\"\n        ;;\n\n\n        -dns)    # Включение/отключение перенаправления DNS в прокси\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            [ -n \"$action\" ] || warn_proxy_dns\n            change_proxy_dns \"$action\"\n        ;;\n\n\n        -pr)    # Включение/отключение проксирования трафика Entware\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_proxy_router \"$action\"\n        ;;\n\n\n        -extmsg)    # Включение/отключение расширенных сообщений запуска XKeen\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_extended_msg \"$action\"\n        ;;\n\n\n        -cbk)    # Включение/отключение резервного копирования XKeen при обновлении\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_backup_xkeen \"$action\"\n        ;;\n\n\n        -aghfix)    # Включение/отключение исправления для AdGuard Home\n            action=$(get_state_arg \"$2\")\n            [ -n \"$action\" ] && shift\n            smart_clear\n            change_aghfix_xkeen \"$action\"\n        ;;\n\n\n        -toff)    # Отключение таймаута при меделенной загрузке с GitHub\n            :\n        ;;\n\n\n        *)\n            echo -e \"     Неизвестный ключ: ${red}$1${reset}\"\n            echo -e \"     Список доступных ключей: ${yellow}xkeen -h${reset}\"\n        ;;\n    esac\n    shift\ndone"
  },
  {
    "path": "test/README.md",
    "content": "## XKeen 2.0 Beta\n\n> [!NOTE]\n> Это версия из канала разработки. Она регулярно дорабатывается, содержит новейшие функции, возможности и исправления, но может иметь не выявленные ошибки. Если столкнулись с проблемой - обязательно обновитесь командой `xkeen -uk`, возможно ошибка уже известна и исправлена. Если же проблема сохранилась, выполните `xkeen -diag` и покажите диагностический отчёт в телеграм-чате https://t.me/+8Cvh7oVf6cE0MWRi, подробно описав возникшую проблему\n\n### Изменения\n- Реализована работа с пользовательскими политиками <sup>1</sup>\n- Доработан модуль работы с DNS <sup>2</sup>\n- Реализована работа с IPSET и возможность исключать из проксирования IP-подсети России (параметры `-gips`, `-dgips`) <sup>3</sup>\n- Добавлена поддержка [DSCP-меток QoS](https://jameszero.net/4509.htm) (`62` - исключение из проксирования, `63` - проксирование)\n- Добавлена возможность проксирования трафика Entware (параметр `-pr`) <sup>4</sup>\n- Режим работы Mixed переименован в Hybrid\n- Порт 443 в интерфейсе роутера теперь требуется освобождать только для режима TProxy, пользователям Hybrid (Mixed) режима это делать не обязательно\n- На роутерах Keenetic Skipper 4G ( KN-2910) и Keenetic 4G (KN-1212) после установки теперь не требуется подменять бинарник прокси-клиента, сразу устанавливается совместимый\n- Версия утилиты yq зафиксирована для стабильности\n- Порты проксирования и исключения полностью перенесены в `port_proxying.lst` и `port_exclude.lst`. Параметры `-ap`, `-dp`, `-cp`, `-ape`, `-dpe`, `-cpe` теперь работают только с этими файлам. Переменные `port_donor` и `port_exclude` больше не используются\n- Добавлен параметр `-toff` для отключения таймаута загрузок при замедлении GitHub. Пример использования: `xkeen -i -toff`\n- Пользовательский прокси для загрузок с GitHub теперь можно задать в параметре `gh_proxy` конфигурационного файла `xkeen.json`\n- В файл `01_info_variable.sh` добавлена переменная `curl_extra` для дополнительных параметров, например, для загрузок через socks5 inbound\n- DNS-запросы клиентов политик XKeen в журнале AdGuard Home теперь могут отображаться со своими IP-адресами, а не с IP роутера (параметр `-aghfix`)\n- Предусмотрен вывод стандартной либо расширенной информации при запуске прокси-клиента (параметр `-extmsg`)\n- XKeen переведён на использование актуальных модулей Netfilter из прошивки\n- При включении/отключении контроля файловых дескрипторов теперь не требуется перезагружать роутер, достаточно перезапустить XKeen\n- Задержка автозапуска XKeen теперь не влияет на запуск остальных пакетов, установленных в Entware\n- Интерактивные параметры `-auto`, `-fd`, `-dns`, `-pr`, `-ipv6`, `-extmsg`, `-cbk`, `-aghfix` теперь умеют работать в автоматическом режиме (`-dns on`, `-auto off`,... ), а так же поддерживают перезапуск XKeen (`-dns on -restart`), если это необходимо\n- Доработан сценарий установки. Корректное определение режима работы XKeen, не зависящее он имен входящих тегов `redirect` и `tproxy` [@UltraFeed](https://github.com/UltraFeed)\n- XKeen теперь корректно работает со встроенной политикой Кинетика \"Без доступа в интернет\", часто используемой при настройке родительского контроля. При создании расписания, доступ в интернет прекращается и восстанавливается согласно заданных интервалов времени [#53](https://github.com/jameszeroX/XKeen/pull/53) - [@kittylabassistant](https://github.com/kittylabassistant)\n- Доработки согласно PR [#32](https://github.com/jameszeroX/XKeen/pull/32) - [@kittylabassistant](https://github.com/kittylabassistant)\n- Доработки согласно PR [#33](https://github.com/jameszeroX/XKeen/pull/33), [#34](https://github.com/jameszeroX/XKeen/pull/34), [#35](https://github.com/jameszeroX/XKeen/pull/35), [#36](https://github.com/jameszeroX/XKeen/pull/36), [#37](https://github.com/jameszeroX/XKeen/pull/37), [#38](https://github.com/jameszeroX/XKeen/pull/38), [#39](https://github.com/jameszeroX/XKeen/pull/39), [#40](https://github.com/jameszeroX/XKeen/pull/40), [#41](https://github.com/jameszeroX/XKeen/pull/41), [#42](https://github.com/jameszeroX/XKeen/pull/42), [#43](https://github.com/jameszeroX/XKeen/pull/43), [#44](https://github.com/jameszeroX/XKeen/pull/44), [#45](https://github.com/jameszeroX/XKeen/pull/45), [#46](https://github.com/jameszeroX/XKeen/pull/46), [#47](https://github.com/jameszeroX/XKeen/pull/47), [#48](https://github.com/jameszeroX/XKeen/pull/48), [#49](https://github.com/jameszeroX/XKeen/pull/49), [#50](https://github.com/jameszeroX/XKeen/pull/50), [#51](https://github.com/jameszeroX/XKeen/pull/51), [#52](https://github.com/jameszeroX/XKeen/pull/52) - [@oviron](https://github.com/oviron)\n- Удалены неиспользуемые параметры запуска `-rrk`, `-rrx`, `-rrm`, `-drk`, `-drx`, `-drm`\n- Рефакторинг кода скриптов\n\n<sup>1</sup> В роутинге, используя параметр `source`, вы можете определить разные правила маршрутизации для разных устройств, а пользовательские политики дают возможность задать для них разные порты проксирования, например, для торрент-клиента можно сделать политику с `80,443` портами проксирования, для телефонов политику с портами `80,443,596:599,1400,3478,5222`, а для игровых устройств с более широким набором портов. Для работы с пользовательскими политиками, они должы быть определены в конфигурационном файле `/opt/etc/xkeen/xkeen.json`, а также созданы в интерфейсе роутера с теми же именами. Пользовательские политики подключаются только если в интерфейсе роутера создана дефолтная политика `xkeen`, иначе пользовательские политики игнорируются и проксирование запускается для всех клиентов роутера. В пользовательских политиках, в отличие от политики `xkeen`, не проверяется наличие обязательных портов проксирования `80,443`, и вы можете формировать произвольный их список. Пример конфигурационного файла (допустимы любые имена пользовательских политик):\n```\n{\n  \"xkeen\": {\n    \"gh_proxy\": \"\",\n    \"policy\": [\n      {\n        \"name\": \"xkeen0\",\n        \"port\": \"\"\n      },\n      {\n        \"name\": \"xkeen1\",\n        \"port\": \"80,443,596:599,1400,3478,5222\"\n      },\n      {\n        \"name\": \"xkeen2\",\n        \"port\": \"!7777,8888:9999\"\n      }\n    ]\n  }\n}\n```\n\n- Политика `xkeen0` - проксирование на всех портах\n- Политика `xkeen1` - проксирование на перечисленных портах\n- Политика `xkeen2` - проксирование на всех портах, кроме перечисленных\n\n<sup>2</sup> При включенном перехвате и проксировании DNS, корректная работа устройств вне политик XKeen возможна только, когда они находятся не в \"Политике по умолчанию\", а в кастомной политике с произвольным именем и в роутере не игнорируется DNS провайдера, либо добавлен любой внешний не шифрованный DNS (даже при использовании AdGuardHome и отключении прошивочного резолвера командой `opkg dns-override`)\n\n<sup>3</sup> XKeen создаёт и использует 3 сета IPv4 и 3 сета IPv6 IPSET\n\n- `user_exclude`, `user_exclude6` - наполняются пользовательскими исключениями IP из файла `ip_exclude.lst`\n- `geo_exclude`, `geo_exclude6` - наполняются подсетями России из загружаемых файлов `ru_exclude_ipv4.lst` и `ru_exclude_ipv6.lst`\n- `ext_exclude`, `ext_exclude6` - наполняются любым сторонним приложением на ваш выбор, например, AdGuard Home или dnsmasq (см. документацию к этим приложениям). Данная возможность позволяет добавлять в исключения проксирования не IP, а доменные имена. Для совместимости со сторонними приложениями скрипт запуска XKeen переименован в S05xkeen\n\nФайлы `ru_exclude_ipv4.lst` и `ru_exclude_ipv6.lst` загружаются при установке XKeen или командой `xkeen -gips`. Обновление этих файлов выполняется одновременно с геофайлами по планировщику либо вручную запуском `xkeen -ug`\n\n<sup>4</sup> Проксирование трафика Entware можно использовать для обновления компонентов XKeen при недоступности GitHub, либо для любых других приложений в Entware, не связанных с XKeen. Для проксирования трафика Entware требуется установка 255 метки на все исходящие подключения кроме blackhole и dns. При использовании ядра Xray добавьте маркировку к каждому подключению в outbounds включая direct:\n```\n\"streamSettings\": {\n  \"sockopt\": {\n    \"mark\": 255\n  }\n}\n```\nПри использовании ядра Mihomo добавьте маркировку глобально:\n```\ntproxy-port: 1181\nlog-level: silent\n...\nrouting-mark: 255\n```\nлибо к каждому proxies, proxy-providers и direct подключениям (см. документацию к Mihomo)\n\n---\n\nВ связи с добавлением нового компонента IPSET и нового диалогового окна в мастер установки XKeen, рекомендуется для обновления XKeen не использовать штатную возможность по `-uk`, а установить Beta версию поверх предыдущей. В мастере установки при этом можете пропускать уже установленные компоненты, все ваши конфиги и настройки сохранятся\n\n### Рекомендуемый порядок установки/обновления\n```\nopkg update && opkg upgrade && opkg install curl tar && cd /tmp\nsh -c \"$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/install.sh)\"\n```\n\n### Альтернативный вариант установки/обновления\n```bash\nopkg update && opkg upgrade && opkg install curl tar && cd /tmp\nsh -c \"$(curl -sSL https://cdn.jsdelivr.net/gh/jameszeroX/XKeen@main/install.sh)\"\n```\n\nШтатный механизм обновления тоже работает, но он рекомендуется только для опытных пользователей\n\n### Порядок обновления с предыдущей версии форка XKeen (только для опытных пользователей)\n```\nxkeen -stop\t\t# остановите прокси-клиент\nxkeen -channel\t# переключитесь на канал разработки\nxkeen -uk\t\t# проверьте и загрузите обновление\nxkeen -k \t\t# выполните локальную переустановку (пункт 0)\nxkeen -gips\t\t# установите, если нужно, сеты IPSET\nxkeen -ugc\t\t# включите автообновление GeoIPSET по планировщику (опционально)\nxkeen -start\t# запустите прокси-клиент\n```\nПоследующие запуски команды `xkeen -uk` в канале разработки каждый раз загружают и обновляют бету XKeen на актуальную версию\n"
  },
  {
    "path": "wiki/DNS-over-VLESS.md",
    "content": "# DNS-over-VLESS — направляем DNS-трафик через прокси xray\n\n> Источник: [jameszero.net/3398.htm](https://jameszero.net/3398.htm)\n\nБезопасность в интернете с каждым днём становится всё более насущной необходимостью. Утечка личных данных может обойтись слишком дорого, поэтому о своей цифровой неприкосновенности лучше позаботиться заранее. И начать стоит, например, с защиты DNS-запросов. Обычно для этого используются такие протоколы, как **DNS-over-TLS**, **DNS-over-HTTPS** и другие. Я же предлагаю рассмотреть ещё один способ — направить DNS-трафик через прокси-сервер на базе Xray, назовём этот метод **DNS-over-VLESS**.\n\nВ сети немало инструкций по настройке самого Xray, но вот проброс DNS-трафика через него раскрыт недостаточно подробно. Попробуем восполнить этот пробел.\n\n## Предварительные условия\n\nПервоначальное условие — у вас уже должен быть настроен и функционировать прокси xray VLESS с XTLS Reality по одной из многочисленных инструкций. Все действия по его \"прокачке\" выполняем локально на компьютере или роутере, сервер xray не трогаем.\n\n**Замечание от пользователя:**\n\n> Во многих конфигурациях серверов xray есть секция для предотвращения проблем с локальной маршрутизацией, в ней блокируется \"geoip:private\", куда попадает дефолтный адрес DNS-резолвера - 127.0.0.53. Для нашей задачи данное правило необходимо удалить, либо направить 127.0.0.53 выше этого правила в директ.\n\n## Структура конфигурации\n\nРазобьём единый файл настроек xray **config.json** на файлы по разделам: **inbounds.json**, **outbounds.json**, **dns.json**, **routing.json**. Этих четырёх файлов достаточно для нашей задачи.\n\nДля реализации задуманного необходимо использовать входящее подключение (inbounds), поддерживающее UDP-протокол, например, TProxy.\n\n## Конфигурационные файлы\n\n### inbounds.json\n\n```json\n{\n  \"inbounds\": [\n    {\n      \"port\": 1181,\n      \"protocol\": \"dokodemo-door\",\n      \"settings\": {\n        \"network\": \"tcp,udp\",\n        \"followRedirect\": true\n      },\n      \"sniffing\": {\n        \"enabled\": true,\n        \"routeOnly\": true,\n        \"destOverride\": [\"http\",\"tls\",\"quic\"]\n      },\n      \"streamSettings\": {\n        \"sockopt\": {\"tproxy\": \"tproxy\"}\n      },\n      \"tag\": \"tproxy\"\n    }\n  ]\n}\n```\n\n### outbounds.json\n\n```json\n{\n  \"outbounds\": [\n    {\n      \"protocol\": \"vless\",\n      \"settings\": {\n        \"address\": \"***.***.***.***\",\n        \"port\": 443,\n        \"id\": \"****************************\",\n        \"encryption\": \"none\",\n        \"flow\": \"xtls-rprx-vision\"\n      },\n      \"streamSettings\": {\n        \"network\": \"tcp\",\n        \"security\": \"reality\",\n        \"realitySettings\": {\n          \"serverName\": \"*********\",\n          \"publicKey\": \"****************************\",\n          \"shortId\": \"********\",\n          \"spiderX\": \"/\"\n        },\n        \"sockopt\": {\n          \"domainStrategy\": \"ForceIP\"\n        }\n      },\n      \"tag\": \"proxy\"\n    },\n    {\n      \"protocol\": \"freedom\",\n      \"streamSettings\": {\n        \"sockopt\": {\n          \"domainStrategy\": \"ForceIP\"\n        }\n      },\n      \"tag\": \"direct\"\n    },\n    {\n      \"protocol\": \"dns\",\n      \"tag\": \"dns-out\"\n    }\n  ]\n}\n```\n\n### dns.json\n\n```json\n{\n  \"dns\": {\n    \"tag\": \"dns-in\",\n    \"servers\": [\n      \"8.8.8.8\"\n    ],\n    \"queryStrategy\": \"UseIP\"\n  }\n}\n```\n\n### routing.json\n\n```json\n{\n  \"routing\": {\n    \"rules\": [\n      {\n        \"inboundTag\": [\"dns-in\"],\n        \"outboundTag\": \"proxy\"\n      },\n      {\n        \"port\": 53,\n        \"outboundTag\": \"dns-out\"\n      },\n      {\n        \"domain\": [\n          \"browserleaks\",\n          \"ip.me\"\n        ],\n        \"outboundTag\": \"proxy\"\n      },\n      {\n        \"network\": \"tcp,udp\",\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}\n```\n\n## Проверка работоспособности\n\nНастройка завершена, выполняем проверку.\n\nЕсли ошибок не допущено, то проксируемый ресурс **browserleaks.com** покажет DNS страны нахождения вашего VPS. Не стоит бояться использования обычных DNS в рассмотренных конфигах, их трафик будет передан на VPS-сервер транспортом VLESS с TLS-шифрованием.\n\n## Результат\n\nВ итоге результат получен, **DNS-over-VLESS** настроен и работает. Никто не прослушает ваши DNS-запросы к проксируемым сайтам и не подменит ответы по своему усмотрению. Для дополнительной защиты в интернете обратите внимание на возможность отключения WebRTC и QUIC в браузере.\n"
  },
  {
    "path": "wiki/FAQ.md",
    "content": "# FAQ по XKeen\n\n> Источник: [jameszero.net/faq-xkeen.htm](https://jameszero.net/faq-xkeen.htm)\n\n## Введение\n\n**XKeen** — это набор скриптов для роутеров Keenetic с прокси-клиентом Xray-core/Mihomo, предназначенный для оптимизации производительности интернет-подключения и обеспечения его безопасности.\n\n**Важно!** Если после обновления или установки XKeen получаете ошибку SIGSEGV с сегментацией, откатите ядро Xray командой `xkeen -ux`.\n\n---\n\n## Часто задаваемые вопросы\n\n### 0. Внезапные проблемы с XKeen\n\n**Q:** Настроил xkeen/xray, всё работало, ничего не трогал. Внезапно прокси упал. Что можно предпринять?\n\n**A:** При внезапных проблемах причина часто в устаревшем ядре Xray. Рекомендуется перейти на форк XKeen или обновить ядро другим способом. Не выбирайте последние версии — они часто требуют отдельной настройки.\n\nОбщий порядок действий:\n\n- Если хостер Aeza — обращайтесь к нему, а не на форум\n- Роутеры mipsle-архитектуры: не используйте ядро xray 26.3.27\n- Проверьте подключение к прокси через WiFi с другого устройства\n- Убедитесь в актуальных версиях XKeen и Xray\n- Измените uTLS отпечаток на `\"fingerprint\": \"firefox\"`\n- Подберите другой SNI (маскировочный домен)\n- Проверьте галку политики XKeen на провайдере\n- Убедитесь, что QUIC не заблокирован\n- Проверьте DoH/DoT серверы в роутере\n- Отключен ли транзит DNS запросов\n- Нет ли ошибок DNS в журнале\n- Удалён ли компонент \"Протокол IPv6\"\n- Проверьте параметр `\"routeOnly\": true` в inbounds\n- Отключите частный MAC и приватный DNS на устройствах\n- Удалите политику XKeen и запустите для всех клиентов\n- Проверьте подключение к другому прокси-серверу\n\nЕсли ничего не помогло, выполните `xkeen -diag` и прикрепите отчёт в телеграм-чат.\n\n### 1. Обновление с оригинала на форк XKeen\n\n**Q:** Как правильно обновляться с XKeen 1.1.3 на форк?\n\n**A:** Рекомендуется установить форк поверх оригинала. Конфиги Xray сохранятся:\n\n```shell\nopkg update && opkg upgrade && opkg install curl tar\ncurl -OL https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz\ntar -xvzf xkeen.tar.gz -C /opt/sbin > /dev/null && rm xkeen.tar.gz\nxkeen -i\n```\n\n### 2. Различия в замерах скорости\n\n**Q:** Почему при использовании xkeen замеры скорости сильно различаются или низкие?\n\n**A:** Причина в производительности процессора роутера и особенностях работы Xray с потоком xtls-rprx-vision.\n\n**Важно для достоверного результата:**\n\n- Не ограничивайте порты проксирования 80 и 443\n- Не используйте роутинг — направьте весь трафик на VPS\n\nXray не повторно шифрует HTTPS-трафик, но шифрует HTTP-трафик силами процессора. Различные сайты используют разные методики тестирования, поэтому результаты могут не быть корректными.\n\n**Советы для повышения скорости:**\n\n- Выполните оптимизацию сетевого стека с режимом BBR\n- Обязательно используйте поток **xtls-rprx-vision**\n- Используйте режим TProxy (самый быстрый — Redirect, но без UDP)\n- Не тестируйте торрентами — это неправильная практика\n\n### 3. Низкая скорость на бюджетных моделях\n\n**Q:** На KN-3810/KN-3610 после установки XKeen скорость упала до 30-40 Мбит/с.\n\n**A:** В бюджетных моделях установлены процессоры EcoNet (EN****) с очень низкой производительностью при Xray. Исправить нельзя, но можно:\n\n- Ограничить порты проксирования 80 и 443\n- Не подключать чрезмерное количество geosite/geoip баз\n- Не использовать компонент \"Классификация трафика\"\n\n### 4. Периодические разрывы соединения\n\n**Q:** Почему при использовании xkeen меня периодически выбрасывает из SSH/RDP/VPN/звонков?\n\n**A:** Xray не предназначен для потокового трафика и может обрывать устаревшие сессии.\n\n**Решения:**\n\n- Добавьте IP-адрес критичного сервера с маской /32 в `/opt/etc/xkeen/ip_exclude.lst`\n- Ограничьте порты проксирования в `/opt/etc/xkeen/port_exclude.lst`\n- Попробуйте переключиться с Xray на Mihomo (форк XKeen поддерживает оба)\n\n### 5. Невозможно подключиться к серверу с приложения на телефоне\n\n**Q:** Добавил телефон в политику XKeen, но не могу подключить к серверу приложением.\n\n**A:** Вы пытаетесь подключиться к прокси, будучи уже подключённым к прокси. Исключите IP сервера из проксирования, добавив в `/opt/etc/xkeen/ip_exclude.lst` с маской /32.\n\n### 6. Сайт определяет мой провайдерский IP\n\n**Q:** Почему ChatGPT/whoer/dell определяют мой провайдерский IP, хотя ресурс добавлен в роутинг?\n\n**A:** Некоторые ресурсы используют QUIC или WebRTC, через которые возможна утечка IP.\n\n**Решения:**\n\n- Отключите WebRTC и QUIC в браузере\n- Заблокируйте UDP 443 правилом роутинга (TProxy/Mixed):\n\n```json\n{\n  \"network\": \"udp\",\n  \"port\": \"443\",\n  \"outboundTag\": \"block\"\n}\n```\n\n- Заблокируйте в межсетевом экране Keenetic\n- Некоторые сайты блокируют по языку браузера — переключите на иностранный\n\n### 7. Как заблокировать рекламу на YouTube\n\n**Q:** Как заблокировать рекламу на Youtube?\n\n**A:** Роутингом и DNS этого не сделать — реклама вшита в видеопоток. Помогут только приложения (SmartTube, YouTube ReVanced, AdGuard) или покупка Premium.\n\n### 8. Пропал доступ к SSH после перезагрузки\n\n**Q:** После сбоя пропал доступ к SSH на роутере.\n\n**A:** Перед установкой entware удалите компонент прошивки \"Сервер SSH\". Если ssh перестал работать:\n\n1. Удалите файл `/opt/var/run/dropbear.pid`\n2. Убедитесь в наличии `PORT=22` или `PORT=222` в `/opt/etc/config/dropbear.conf`\n3. Перезагрузите роутер или выполните:\n\n```shell\nexec sh\nexec /opt/etc/init.d/S51dropbear restart\n```\n\n**Для предотвращения сбоев** отредактируйте `/opt/etc/config/dropbear.conf`:\n\n```shell\nPIDFILE=\"/var/run/dropbear.pid\"\n```\n\n### 9. ICMP пинги не проходят через прокси\n\n**Q:** Ping и tracert к ресурсам из роутинга почему-то идут напрямую.\n\n**A:** ICMP пинги работают на Сетевом уровне OSI, а прокси — на Прикладном. Они несовместимы.\n\n### 10. Сайт tmdb не открывается\n\n**Q:** Прописал tmdb.org в роутинг, но сайт всё равно не открывается.\n\n**A:** Ресурс блокирует по GeoDNS. Решения:\n\n- Используйте DNS без EDNS Client Subnet (например, dns.sb)\n- В AdGuardHome включите подмену EDNS Client Subnet с IP прокси\n- В Keenetic создайте запись для каждого домена с IP прокси-сервера\n\n### 11. Забыл пароль панели 3x-ui (IP 127.0.0.1)\n\n**Q:** Указал IP 127.0.0.1 в панели 3x-ui, теперь не могу зайти.\n\n**A:** Используйте PuTTY с портфорвардингом:\n\n1. SSH → Tunnels: Source port 65345, Destination localhost:65345\n2. Нажмите Add и подключитесь\n3. В браузере откройте `localhost:65345/путь` (не закрывая PuTTY)\n\n### 12. XKeen не запускается после перезагрузки\n\n**Q:** После перезагрузки XKeen не запускается или работает для всего устройства.\n\n**A:** Entware может стартовать раньше инициализации прошивки. Подберите задержку:\n\n```shell\nxkeen -d 30\n```\n\nИли добавьте скрипт `/opt/etc/init.d/S99xkeenrestart` для перезапуска после включения интернета.\n\n### 13. AdGuardHome показывает IP роутера вместо клиента\n\n**Q:** В журнале AdGuard все клиенты имеют IP роутера, а не свои.\n\n**A:** Убедитесь в параметре `\"routeOnly\": true` (Xray) или `\"override-destination\": false` (Mihomo).\n\nДля XKeen 1.1.3.9 поместите скрипт `aghfix.sh` в `/opt/etc/ndm/netfilter.d/` и сделайте исполняемым:\n\n```shell\nchmod +x /opt/etc/ndm/netfilter.d/aghfix.sh\n```\n\nПерезагрузите роутер.\n\n### 14. Использование Wireguard/OpenConnect вместо Xray\n\n**Q:** Есть только Wireguard/OpenConnect. Можно ли использовать маршрутизацию Xray для них?\n\n**A:** Да. Определите наименование интерфейса (`ip a`), затем в `outbounds.json` добавьте:\n\n```json\n{\n  \"protocol\": \"freedom\",\n  \"streamSettings\": {\n    \"sockopt\": {\n      \"interface\": \"nwg0\"\n    }\n  },\n  \"tag\": \"newtag\"\n}\n```\n\nВ `routing.json` добавьте правило с тегом `newtag`.\n\n### 15. Раздача интернета через XKeen клиентам Wireguard-сервера\n\n**Q:** На роутере Wireguard-сервер. Можно ли раздать через XKeen интернет его клиентам?\n\n**A:** При XKeen для всего устройства — дополнительных настроек нет. При политике найдите номер политики и интерфейса в `startup-config`, затем:\n\n```shell\nip hotspot policy Wireguard0 Policy0\nsystem configuration save\n```\n\nОтменить:\n\n```shell\nip hotspot policy Wireguard0 permit\nsystem configuration save\n```\n\n### 16. Удалённый доступ к SSH через серый IP\n\n**Q:** Использую KeenDNS. Можно ли получить удалённый доступ к SSH через серый IP?\n\n**A:** Поднимите SSTP VPN-сервер. К нему подключайтесь по домену KeenDNS, после чего SSH роутера доступен по 192.168.1.1:22.\n\n### 17. Разные маршруты для разных устройств в одной политике\n\n**Q:** Можно ли настроить разные маршруты в одной политике XKeen?\n\n**A:** Для WiFi-устройств создайте несколько домашних сетей и добавьте параметр `source` в правила маршрутизации.\n\nПример — разная маршрутизация для подсетей:\n\n```json\n{\n  \"routing\": {\n    \"rules\": [\n      {\n        \"source\": [\"192.168.1.0/24\"],\n        \"domain\": [\"site1.ru\", \"site2.ru\"],\n        \"outboundTag\": \"vless-reality\"\n      },\n      {\n        \"source\": [\"192.168.2.0/24\"],\n        \"outboundTag\": \"vless-reality\"\n      },\n      {\n        \"network\": \"tcp,udp\",\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}\n```\n\nИли разные маршруты по IP-адресам устройств:\n\n```json\n{\n  \"routing\": {\n    \"rules\": [\n      {\n        \"source\": [\"192.168.1.5\"],\n        \"outboundTag\": \"vless-reality-eu\"\n      },\n      {\n        \"source\": [\"192.168.1.6\", \"192.168.1.7\"],\n        \"outboundTag\": \"vless-reality-us\"\n      }\n    ]\n  }\n}\n```\n\n### 18. Резервное копирование и восстановление\n\n**Q:** Как выполнить резервное копирование XKeen?\n\n**A:** Создайте архив:\n\n```shell\nopkg update && opkg upgrade && opkg install tar\ntar --exclude=entware_backup.tar.gz --exclude=*.pid --warning=no-file-changed -cvzf /opt/entware_backup.tar.gz -C /opt .\n```\n\nДля восстановления отформатируйте флешку в ext4, создайте папку `install`, поместите архив, выберите флешку в OPKG.\n\n### 19. Интернет пропадает через несколько часов работы\n\n**Q:** Через часы работы интернет пропадает, хотя `xkeen -status` показывает работу прокси.\n\n**A:** Прокси-клиент исчерпал лимит файловых дескрипторов. Проверьте:\n\n```shell\nls -l /proc/$(pidof xray)/fd | wc -l\n```\n\nФорк XKeen с версии 1.1.3.6 контролирует fd. Включите:\n\n```shell\nxkeen -fd\n```\n\nИ перезагрузите роутер.\n\n### 20. Два прокси-сервера для разных сайтов\n\n**Q:** Есть два прокси. Можно ли открывать одни сайты через первый, другие — через второй?\n\n**A:** В `outbounds.json` добавьте оба сервера с разными тегами:\n\n```json\n{\n  \"outbounds\": [\n    {\n      \"protocol\": \"vless\",\n      \"settings\": {},\n      \"tag\": \"proxy1\"\n    },\n    {\n      \"protocol\": \"vless\",\n      \"settings\": {},\n      \"tag\": \"proxy2\"\n    },\n    {\n      \"protocol\": \"freedom\",\n      \"tag\": \"direct\"\n    }\n  ]\n}\n```\n\nВ `routing.json` создайте правила:\n\n```json\n{\n  \"routing\": {\n    \"rules\": [\n      {\n        \"domain\": [\"site1.ru\"],\n        \"outboundTag\": \"proxy1\"\n      },\n      {\n        \"domain\": [\"site2.ru\"],\n        \"outboundTag\": \"proxy2\"\n      },\n      {\n        \"network\": \"tcp,udp\",\n        \"outboundTag\": \"direct\"\n      }\n    ]\n  }\n}\n```\n\n### 21. Отказоустойчивость прокси\n\n**Q:** Можно ли сделать fallback на второй прокси, если первый недоступен?\n\n**A:** Да, используйте встроенный механизм балансировки Xray. Подробности в статьях сайта об отказоустойчивом прокси-сервере.\n\n### 22. Два провайдера — подключение через один\n\n**Q:** К роутеру подключены два провайдера. Как сделать подключение к прокси только через одного?\n\n**A:** Аналогично пункту 14. В `outbounds.json` добавьте параметр `\"interface\"` с указанием интерфейса.\n\nПример для direct:\n\n```json\n{\n  \"protocol\": \"freedom\",\n  \"streamSettings\": {\n    \"sockopt\": {\n      \"interface\": \"lte_br0\"\n    }\n  },\n  \"tag\": \"direct\"\n}\n```\n\nПример для прокси:\n\n```json\n{\n  \"protocol\": \"vless\",\n  \"settings\": {},\n  \"streamSettings\": {\n    \"network\": \"raw\",\n    \"security\": \"reality\",\n    \"realitySettings\": {},\n    \"sockopt\": {\n      \"interface\": \"lte_br0\"\n    }\n  },\n  \"tag\": \"proxy\"\n}\n```\n\nУточните название интерфейса командой `ifconfig`.\n\n### 23. Не работает голос в Discord/Telegram/WhatsApp\n\n**Q:** Не работают голосовые чаты/пинг 5000 etc.\n\n**A:** Discord использует UDP 50000-50030, Telegram/WhatsApp используют UDP 596:599, 1400, 3478, 5222. Чтобы голос заработал, добавьте в `routing.json` правило для этого порта, если у вас в конце все подключения идут direct:\n\n```json\n{\n  \"routing\": {\n    \"rules\": [\n      {\n        \"network\": \"udp\",\n        \"port\": \"50000-50030,596-599,1400,3478,5222\",\n        \"outboundTag\": \"vless-reality\"\n      }\n    ]\n  }\n}\n```\n\nЕсли подключения всё в vless-reality, кроме РФ, то добавить порты в проксирование будет достаточно:\n\n```shell\nxkeen -ap 80,443,2053:2096,8443,19200:19400,50000:50030,596:599,1400,3478,5222\n```\n\n**Примечание:** Если пинг всё равно высокий или идут обрывы, то есть проблемы с маршрутизацией UDP-трафика - альтернатива:\n\n<https://github.com/runetfreedom/discord-voice-proxy>\nПлюсы такого решения - можно не добавлять порты проксирования 50000+ и оставить только 80,443 (на socks5 эти ограничения не распространяются), таким образом торренты у вас точно не пойдут в прокси, а может ещё стабильность и пинг в дискорде улучшатся, но это не точно)\n\nПосле установки Discord, запустите DiscordProxyInstaller.exe он спросит IP роутера и порт socks5-прокси, и Discord станет работать через него. Socks5-прокси, разумеется, должен быть поднят в xray на роутере (не прокси клиент Кинетика, а просто прописан в inbounds.json). для этого добавьте в inbounds.json:\n\n```json\n\n      {\n        \"port\": 1080,\n        \"protocol\": \"mixed\",\n        \"settings\": {\"udp\": true},\n        \"tag\": \"socks\"\n      },\n```\n\nrouting.json:\n\n```json\n      {\n        \"inboundTag\": [\"socks\"],\n        \"outboundTag\": \"vless-reality\",\n        \"type\": \"field\"\n      },\n```\n\n### 24. Gemini определяет страну РФ\n\n**Q:** Почему Gemini определяет страну РФ?\n\n**Причина:** Если использовать ру аккаунт google через proxy, то google помечает ip как proxy, и определяет страну как РФ. Смена аккаунта не всегда помогает.\n\n**A:** Проксировать gemini через warp-native <https://github.com/distillium/warp-native> . Дефолтный Warp xray/3x-ui не всегда отрабатывает корректно. Warp-native устанавливается на VPS.\n\n### 25. Хочу перейти на ядро mihomo, какие rule sets использовать?\n\n**A:** Вот несколько вариантов:\n\n- rule-sets от \"zxc-rv\" <https://github.com/zxc-rv/assets/tree/main/rules>\n- rule-sets от \"legiz-ru\" <https://github.com/legiz-ru/mihomo-rule-sets>\n\n### 26. Не работают правила mihomo, что делать?\n\n**A:** <https://wiki.metacubex.one/ru/config/> - проверяйте корректность настроек\n"
  },
  {
    "path": "wiki/Home.md",
    "content": "# XKeen\n\n**XKeen** — POSIX-shell утилита для прозрачной маршрутизации сетевого трафика через прокси-движки **Xray** и **Mihomo** на роутерах **Keenetic** / **Netcraze**. Позволяет выборочно направлять TCP/UDP-трафик отдельных клиентов через прокси, не затрагивая остальную сеть.\n\n## Зачем нужен XKeen\n\n- **Обход блокировок** на уровне роутера: одна точка настройки для всех домашних устройств — телефоны, ТВ, консоли, ПК.\n- **Гибкая маршрутизация**: разные клиенты могут идти через разные прокси-серверы или напрямую, в зависимости от политик XKeen, IP-адресов и доменов.\n- **Защита DNS-запросов**: возможна организация DNS-over-VLESS — DNS-трафик передаётся туннелем Xray, что исключает прослушку и подмену ответов на пути от роутера до прокси. См. [DNS-over-VLESS](DNS-over-VLESS).\n- **DSCP-маршрутизация**: проксирование/исключение конкретных приложений по QoS-меткам Windows. См. [Маршрутизация по DSCP](Маршрутизация-по-DSCP).\n- **Два ядра в одном инструменте**: переключение между Xray и Mihomo через `xkeen -xray` / `xkeen -mihomo`.\n\n## Поддерживаемые архитектуры\n\n- `arm64-v8a` — старшие модели Keenetic/Netcraze\n- `mips32le` — большинство моделей\n- `mips32` — старые модели\n\n## Быстрый старт\n\nНа роутере под Entware:\n\n```sh\nopkg update && opkg upgrade && opkg install curl tar\ncurl -OL https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz\ntar -xvzf xkeen.tar.gz -C /opt/sbin > /dev/null && rm xkeen.tar.gz\nxkeen -i\n```\n\nДальше следуйте интерактивному установщику: выбор ядра (Xray / Mihomo / оба), установка geofile, настройка автообновлений.\n\n## Дальнейшие шаги\n\n- **[FAQ](FAQ)** — 22 ответа на самые частые вопросы: проблемы с производительностью, DNS, утечки IP, восстановление после сбоев, маршруты для разных подсетей.\n- **[DNS-over-VLESS](DNS-over-VLESS)** — пошаговая настройка проксирования DNS через VLESS.\n- **[Маршрутизация по DSCP](Маршрутизация-по-DSCP)** — маршрутизация приложений Windows по QoS-меткам (XKeen 2.0+).\n\n## Поддержка и обратная связь\n\nКоманды на роутере:\n\n- `xkeen -h` — встроенная справка по флагам.\n- `xkeen -diag` — полная диагностика. **Единственный поддерживаемый канал для отчёта о проблеме.**\n- `xkeen -af` — контакты разработчиков.\n- `xkeen -ad` — поддержать разработчиков.\n"
  },
  {
    "path": "wiki/_Footer.md",
    "content": "---\n\n[Сайт автора форка — jameszero.net](https://jameszero.net/) · [Обсуждение на forum.keenetic.ru](https://forum.keenetic.ru/topic/16899-xkeen/)\n"
  },
  {
    "path": "wiki/_Sidebar.md",
    "content": "**Навигация**\n\n- [Главная](Home)\n- [FAQ](FAQ)\n- [DNS-over-VLESS](DNS-over-VLESS)\n- [Маршрутизация по DSCP](Маршрутизация-по-DSCP)\n"
  },
  {
    "path": "wiki/Маршрутизация-по-DSCP.md",
    "content": "# Маршрутизация по DSCP-меткам в XKeen\n\n> Источник: [jameszero.net/4509.htm](https://jameszero.net/4509.htm)\n\nВ XKeen 2.0 появилась возможность маршрутизации по DSCP-меткам QoS. Это позволяет исключить конкретные приложения из проксирования или направить их трафик через прокси на всех портах, что полезно когда компьютер имеет ограничения на порты 80 и 443.\n\n## Настройка в Windows\n\n### Предварительные требования\n\nТребуется полная редакция Windows с поддержкой Group Policy. В начальных редакциях настройка возможна только через реестр.\n\n### Этап 1: Изменение реестра\n\nПримените следующий твик реестра:\n\n```\nWindows Registry Editor Version 5.00\n\n[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\QoS]\n\"Do not use NLA\"=\"1\"\n```\n\nПерезагрузите компьютер после применения.\n\n### Этап 2: Проверка параметров сетевой карты\n\nУбедитесь, что включена опция \"Планировщик пакетов QoS\" в настройках сетевого адаптера.\n\n### Этап 3: Создание QoS-политики\n\n1. Нажмите Win+R и выполните `gpedit.msc`\n2. Откройте \"QoS на основе политики\"\n3. В меню \"Действие\" выберите \"Создать новую политику\"\n4. Пройдите мастер, указав имя политики, DSCP-метку, приложение, IP-адреса, протоколы и порты\n\nXKeen поддерживает метки 62 (исключение) и 63 (проксирование) по умолчанию, но их можно заменить в переменных стартового скрипта.\n\nПосле создания политики маршрутизация работает сразу без перезагрузки (достаточно перезапустить приложение).\n\nКаждое приложение требует отдельной политики.\n"
  }
]