Full Code of jameszeroX/XKeen for AI

main 115123510a92 cached
97 files
356.9 KB
107.7k tokens
1 requests
Download .txt
Showing preview only (456K chars total). Download the full file or copy to clipboard to get everything.
Repository: jameszeroX/XKeen
Branch: main
Commit: 115123510a92
Files: 97
Total size: 356.9 KB

Directory structure:
gitextract_qdk3i1xx/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── package-folder.yaml
│       ├── release.yaml
│       └── wiki-sync.yaml
├── .gitignore
├── 01_info_variable.sh
├── LICENSE
├── README.md
├── configuration.md
├── docs/
│   ├── README.md
│   ├── architecture.md
│   ├── build-and-release.md
│   ├── commands.md
│   ├── contributing.md
│   └── runtime-paths.md
├── forkinfo.md
├── install.sh
├── knownissues.md
├── scripts/
│   ├── _xkeen/
│   │   ├── 01_info/
│   │   │   ├── 00_info_import.sh
│   │   │   ├── 01_info_variable.sh
│   │   │   ├── 02_info_packages.sh
│   │   │   ├── 03_info_cpu.sh
│   │   │   ├── 04_info_mihomo.sh
│   │   │   ├── 04_info_xray.sh
│   │   │   ├── 05_info_geofile.sh
│   │   │   ├── 06_info_console.sh
│   │   │   ├── 07_info_cron.sh
│   │   │   └── 08_info_version/
│   │   │       ├── 00_version_import.sh
│   │   │       ├── 01_version_xkeen.sh
│   │   │       ├── 02_version_mihomo.sh
│   │   │       └── 02_version_xray.sh
│   │   ├── 02_install/
│   │   │   ├── 00_install_import.sh
│   │   │   ├── 01_install_packages.sh
│   │   │   ├── 02_install_mihomo.sh
│   │   │   ├── 02_install_xray.sh
│   │   │   ├── 03_install_xkeen.sh
│   │   │   ├── 04_install_geofile.sh
│   │   │   ├── 05_install_geoipset.sh
│   │   │   ├── 06_install_cron.sh
│   │   │   ├── 07_install_register/
│   │   │   │   ├── 00_register_common.sh
│   │   │   │   ├── 00_register_import.sh
│   │   │   │   ├── 01_register_mihomo.sh
│   │   │   │   ├── 01_register_xray.sh
│   │   │   │   ├── 02_register_xkeen.sh
│   │   │   │   ├── 03_register_cron.sh
│   │   │   │   └── 04_register_init.sh
│   │   │   └── 08_install_configs/
│   │   │       ├── 00_configs_import.sh
│   │   │       ├── 01_configs_install.sh
│   │   │       └── 02_configs_dir/
│   │   │           ├── 01_log.json
│   │   │           ├── 02_dns.json
│   │   │           ├── 03_inbounds.json
│   │   │           ├── 04_outbounds.json
│   │   │           ├── 05_routing.json
│   │   │           └── 06_policy.json
│   │   ├── 03_delete/
│   │   │   ├── 00_delete_import.sh
│   │   │   ├── 01_delete_geofile.sh
│   │   │   ├── 02_delete_geoipset.sh
│   │   │   ├── 03_delete_cron.sh
│   │   │   ├── 04_delete_configs.sh
│   │   │   ├── 05_delete_register.sh
│   │   │   └── 06_delete_tmp.sh
│   │   ├── 04_tools/
│   │   │   ├── 00_tools_import.sh
│   │   │   ├── 01_tools_ports.sh
│   │   │   ├── 02_tools_modules.sh
│   │   │   ├── 03_tools_diagnostic.sh
│   │   │   ├── 04_tools_delay.sh
│   │   │   ├── 05_tools_choice/
│   │   │   │   ├── 00_choice_import.sh
│   │   │   │   ├── 01_choice_cores.sh
│   │   │   │   ├── 02_choice_xkeen.sh
│   │   │   │   ├── 03_choice_geofile.sh
│   │   │   │   ├── 04_choice_input.sh
│   │   │   │   └── 05_choice_cron/
│   │   │   │       ├── 00_cron_import.sh
│   │   │   │       ├── 01_cron_status.sh
│   │   │   │       └── 02_cron_time.sh
│   │   │   ├── 06_tools_backups/
│   │   │   │   ├── 00_backups_import.sh
│   │   │   │   ├── 01_backups_xkeen.sh
│   │   │   │   ├── 02_backups_configs_mihomo.sh
│   │   │   │   └── 02_backups_configs_xray.sh
│   │   │   └── 07_tools_downloaders/
│   │   │       ├── 00_downloaders_import.sh
│   │   │       ├── 00_fetch_with_mirrors.sh
│   │   │       ├── 01_downloaders_mihomo.sh
│   │   │       ├── 01_downloaders_xray.sh
│   │   │       └── 02_donwloaders_xkeen.sh
│   │   ├── 05_tests/
│   │   │   ├── 00_tests_import.sh
│   │   │   ├── 01_tests_connected.sh
│   │   │   ├── 02_tests_xports.sh
│   │   │   └── 03_tests_storage.sh
│   │   ├── about.sh
│   │   └── import.sh
│   └── xkeen
├── test/
│   └── README.md
└── wiki/
    ├── DNS-over-VLESS.md
    ├── FAQ.md
    ├── Home.md
    ├── _Footer.md
    ├── _Sidebar.md
    └── Маршрутизация-по-DSCP.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto

# Unix files that are always LF
*.md text eol=lf
*.sh text eol=lf
*.json text eol=lf
xkeen text eol=lf


================================================
FILE: .github/workflows/package-folder.yaml
================================================
name: Create Test build to `main/test/` folder

on:
  push:
    branches:
      - main
    paths:
      - 'scripts/**'
  workflow_dispatch:

jobs:
  build-and-push-to-main-test-folder:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Import GPG key
        uses: crazy-max/ghaction-import-gpg@v7
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          git_user_signingkey: true
          git_commit_gpgsign: true
          git_tag_gpgsign: true
          git_config_global: true

      - name: Prepare scripts with build timestamp
        run: |
          export TZ="Europe/Moscow"
          BUILD_TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S MSK")
          mkdir -p scripts_for_build
          cp -r scripts/* scripts_for_build/

          sed -i "s/^build_timestamp=\".*\"/build_timestamp=\"$BUILD_TIMESTAMP\"/" \
            scripts_for_build/_xkeen/01_info/01_info_variable.sh

      - name: Create tar.gz archive
        run: |
          mkdir -p output
          cd scripts_for_build
          chmod +x xkeen
          find . -type f -o -type l | sed 's|^\./||' | tar -czf "../output/xkeen.tar.gz" -T -

      - name: Move archive to test folder in main
        run: |
          mkdir -p test
          mv output/xkeen.tar.gz test/

      - name: Clean up temporary files
        run: |
          rm -rf scripts_for_build output

      - name: Commit and push signed archive to test folder
        run: |
          git add test/xkeen.tar.gz
          git commit -S -m "[github-actions] automated compiling build"
          git push origin main

================================================
FILE: .github/workflows/release.yaml
================================================
name: Create Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version number (e.g., 1.0.0)'
        required: true
        type: string
      prerelease:
        description: 'Is this a pre-release?'
        required: true
        default: false
        type: boolean

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout repo
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Import GPG key
        uses: crazy-max/ghaction-import-gpg@v7
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          git_user_signingkey: true
          git_commit_gpgsign: true
          git_tag_gpgsign: true
          git_config_global: true

      - name: Set up variables
        run: |
          echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
          echo "ARCHIVE_NAME=xkeen.tar.gz" >> $GITHUB_ENV
          echo "ARCHIVE_NAME_TAR=xkeen.tar" >> $GITHUB_ENV

      - name: Prepare scripts with build timestamp
        run: |
          export TZ="Europe/Moscow"
          BUILD_TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S MSK")
          mkdir -p scripts_for_release
          cp -r scripts/* scripts_for_release/

          sed -i "s/^build_timestamp=\".*\"/build_timestamp=\"$BUILD_TIMESTAMP\"/" \
            scripts_for_release/_xkeen/01_info/01_info_variable.sh

      - name: Create release archive
        run: |
          mkdir -p dist
          cd scripts_for_release
          chmod +x xkeen

          find . -type f -o -type l | sed 's|^\./||' | tar -czf "../dist/${ARCHIVE_NAME}" -T -
          find . -type f -o -type l | sed 's|^\./||' | tar -cf "../dist/${ARCHIVE_NAME_TAR}" -T -

      - name: Clean up temporary files
        run: rm -rf scripts_for_release

      - name: Generate release notes with downloads badge
        run: |
          cat > /tmp/release_notes.md << EOF
          ![downloads](https://img.shields.io/github/downloads/jameszeroX/Xkeen/${{ env.VERSION }}/total?label=downloads)
          EOF

      - name: Create signed tag and release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG_NAME="${{ env.VERSION }}"

          # Удаляем старый тег если существует
          git push origin --delete "$TAG_NAME" 2>/dev/null || true
          git tag -d "$TAG_NAME" 2>/dev/null || true

          # Создаем подписанный тег
          git tag -s "$TAG_NAME" -m "Release $TAG_NAME"
          git push origin "$TAG_NAME"

          # Создаем релиз
          gh release create "$TAG_NAME" \
            dist/*.tar.gz \
            dist/*.tar \
            --title "${{ env.VERSION }}" \
            --notes-file /tmp/release_notes.md \
            ${{ github.event.inputs.prerelease == 'true' && '--prerelease' || '' }} \
            --verify-tag

      - name: Verify signed tag
        run: |
          if git tag -v "${{ env.VERSION }}" 2>&1; then
            echo "✅ Тег ${{ env.VERSION }} успешно подписан и верифицирован!"
          else
            echo "⚠️ Предупреждение: Не удалось верифицировать подпись тега"
          fi

================================================
FILE: .github/workflows/wiki-sync.yaml
================================================
name: Sync GitHub Wiki

on:
  push:
    branches:
      - main
    paths:
      - 'wiki/**'
      - '.github/workflows/wiki-sync.yaml'
  workflow_dispatch:

jobs:
  sync-wiki:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Import GPG key
        uses: crazy-max/ghaction-import-gpg@v7
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          git_user_signingkey: true
          git_commit_gpgsign: true
          git_tag_gpgsign: true
          git_config_global: true

      - name: Clone wiki repository
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git clone "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.wiki.git" wiki-repo

      - name: Sync wiki content
        run: |
          rsync -a --delete --exclude='.git' wiki/ wiki-repo/

      - name: Commit and push signed wiki update
        working-directory: wiki-repo
        run: |
          git add -A
          if git diff --staged --quiet; then
            echo "No wiki changes to commit"
            exit 0
          fi
          SHORT_SHA="${GITHUB_SHA:0:7}"
          git commit -S -m "[github-actions] sync wiki from main@${SHORT_SHA}"
          BRANCH=$(git symbolic-ref --short HEAD)
          git push origin "$BRANCH"


================================================
FILE: .gitignore
================================================
.claude
graphify-out
done
src/graphify
src/output


================================================
FILE: 01_info_variable.sh
================================================
# -------------------------------------
# Цвета
# -------------------------------------
green="\033[92m"	# Зеленый
red="\033[91m"		# Красный
yellow="\033[93m"	# Желтый
light_blue="\033[96m"	# Голубой
italic="\033[3m"	# Курсив
reset="\033[0m"		# Сброс цветов

# -------------------------------------
# Директории
# -------------------------------------
tmp_dir_global="/opt/tmp"		 # Временная директория общая
tmp_dir="/opt/tmp/xkeen"		 # Временная директория XKeen
xtmp_dir="/opt/tmp/xray"		 # Временная директория Xray
mtmp_dir="/opt/tmp/mihomo"		 # Временная директория Mihomo
xkeen_dir="/opt/sbin/.xkeen"		 # Директория скриптов XKeen
xkeen_cfg="/opt/etc/xkeen"		 # Директория конфигурации XKeen
xkeen_log_dir="/opt/var/log/xkeen"	 # Директория логов XKeen
xray_log_dir="/opt/var/log/xray"	 # Директория логов Xray
initd_dir="/opt/etc/init.d"		 # Директория init.d
pid_dir="/opt/var/run"			 # Директория pid файлов
backups_dir="/opt/backups"		 # Директория бекапов
install_dir="/opt/sbin"			 # Директория установки
geo_dir="/opt/etc/xray/dat"		 # Директория для dat
cron_dir="/opt/var/spool/cron/crontabs"	 # Директория планировщика
cron_file="root"			 # Файл планировщика
install_conf_dir="/opt/etc/xray/configs" # Директория конфигурации Xray
mihomo_conf_dir="/opt/etc/mihomo"	 # Директория конфигурации Mihomo
xray_conf_dir="$xkeen_dir/02_install/08_install_configs/02_configs_dir"
xkeen_var_file="$xkeen_dir/01_info/01_info_variable.sh"
register_dir="/opt/lib/opkg/info"
status_file="/opt/lib/opkg/status"
os_modules="/lib/modules/$(uname -r)"
user_modules="/opt/lib/modules"
xkeen_current_version="1.1.3.9"
xkeen_build="Stable"
build_timestamp="2026-02-07 08:58:11 MSK (fix yq)"

# -------------------------------------
# Время
# -------------------------------------
existing_content=$(cat "$status_file")
installed_size=$(du -s "$install_dir" | cut -f1)
source_date_epoch=$(date +%s)
current_datetime=$(date "+%d-%b-%y_%H-%M")

# -------------------------------------
# IP для проверки доступа в интернет
# -------------------------------------
conn_IP1="195.208.4.1"
conn_IP2="77.88.44.55"

# -------------------------------------
# URL
# -------------------------------------
xkeen_api_url="https://api.github.com/repos/jameszeroX/xkeen/releases/latest"			# url api для XKeen
xkeen_jsd_url="https://data.jsdelivr.com/v1/package/gh/jameszeroX/xkeen"			# резервный url api для XKeen
xkeen_tar_url="https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz"	# url для загрузки XKeen
xkeen_dev_url="https://raw.githubusercontent.com/jameszeroX/xkeen/main/test/xkeen.tar.gz"	# url для загрузки XKeen dev
xray_api_url="https://api.github.com/repos/XTLS/Xray-core/releases"				# url api для Xray
xray_jsd_url="https://data.jsdelivr.com/v1/package/gh/XTLS/Xray-core"				# резервный url api для Xray
xray_zip_url="https://github.com/XTLS/Xray-core/releases/download"				# url для загрузки Xray
mihomo_api_url="https://api.github.com/repos/MetaCubeX/mihomo/releases"				# url api для Mihomo
mihomo_jsd_url="https://data.jsdelivr.com/v1/package/gh/MetaCubeX/mihomo"			# резервный url api для Mihomo
mihomo_gz_url="https://github.com/MetaCubeX/mihomo/releases/download"				# url для загрузки Mihomo
yq_dist_url="https://github.com/jameszeroX/yq/releases/latest/download"				# url для загрузки Yq
gh_proxy1="https://ghfast.top"								        # 1 прокси для загрузок с GitHub
gh_proxy2="https://gh-proxy.com"								# 2 прокси для загрузок с GitHub

# url для загрузки геофайлов
refilter_url="https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geosite.dat"
refilterip_url="https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geoip.dat"
v2fly_url="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
v2flyip_url="https://github.com/loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
zkeen_url="https://github.com/jameszeroX/zkeen-domains/releases/latest/download/zkeen.dat"
zkeenip_url="https://github.com/jameszeroX/zkeen-ip/releases/latest/download/zkeenip.dat"

# -------------------------------------
# Создание директорий и файлов
# -------------------------------------
mkdir -p "$xray_log_dir" || { echo "Ошибка: Не удалось создать директорию $xray_log_dir"; exit 1; }
mkdir -p "$initd_dir" || { echo "Ошибка: Не удалось создать директорию $initd_dir"; exit 1; }
mkdir -p "$pid_dir" || { echo "Ошибка: Не удалось создать директорию $pid_dir"; exit 1; }
mkdir -p "$backups_dir" || { echo "Ошибка: Не удалось создать директорию $backups_dir"; exit 1; }
mkdir -p "$install_dir" || { echo "Ошибка: Не удалось создать директорию $install_dir"; exit 1; }
mkdir -p "$cron_dir" || { echo "Ошибка: Не удалось создать директорию $cron_dir"; exit 1; }

# -------------------------------------
# Журналы
# -------------------------------------
xray_access_log="$xray_log_dir/access.log"
xray_error_log="$xray_log_dir/error.log"

touch "$xray_access_log" || { echo "Ошибка: Не удалось создать файл $xray_access_log"; exit 1; }
touch "$xray_error_log" || { echo "Ошибка: Не удалось создать файл $xray_error_log"; exit 1; }


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2026, jameszeroX

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
# XKeen 1.1.3.9

> **XKeen** — утилита для выборочной маршрутизации сетевого трафика через прокси‑движки **Xray** и **Mihomo** на роутерах **Keenetic**/**Netcraze**.  
> Позволяет прозрачно направлять TCP/UDP‑трафик только выбранных клиентов, не затрагивая остальную сеть.

---

## Основные возможности

- Выборочная маршрутизация для клиентов в политике доступа в интернет
- Сохранение прямого выхода в интернет для остальных клиентов
- Маршрутизация без политики для всех клиентов роутера
- Поддержка режимов **TProxy**, **Mixed**, **Redirect**, **Other** (socks5/http)
- Прозрачное проксирование **TCP** и **UDP**
- Поддержка ядер-проксирования **Xray** и **Mihomo**
- Совместимость с **KeeneticOS 5+**
- Управление через shell и [веб-панели](https://github.com/jameszeroX/XKeen?tab=readme-ov-file#дополнения) сторонних разработчиков

XKeen работает полностью на стороне роутера, не меняет настройки клиентов и не требует установки на них дополнительных программ.

---

## Предупреждения

> [!WARNING]
> Данный материал подготовлен в научно‑технических целях. XKeen предназначен для управления межсетевым экраном роутера Keenetic, защищающим домашнюю сеть. Разработчик не несёт ответственности за иное использование утилиты. Перед применением убедитесь, что ваши действия соответствуют законодательству вашей страны.

> [!CAUTION]
> В некоторых случаях протокол IPv6 создаёт проблемы при проксировании. В KeeneticOS IPv6 нельзя полностью отключить стандартными средствами. В XKeen реализован альтернативный механизм его отключения, который полностью убирает IPv6‑трафик на роутере. Это **экспериментальная функция** и может привести к некорректной работе отдельных сервисов Keenetic. Используйте её только при необходимости.

> [!NOTE]
> Установка XKeen гарантируется на внешние USB‑накопители. Установка во внутреннюю память роутера возможна, но требует опыта пользователя. Проблемы, связанные с установкой во внутреннюю память, не считаются ошибками XKeen.

---

Данный репозиторий является форком оригинального XKeen с исправлениями, расширенной функциональностью и поддержкой актуальных версий KeeneticOS.

## Ключевые изменения форка

### Исправлено

- автозапуск XKeen
- сняты ограничения на количество используемых портов

### Добавлено

- поддержка **KeeneticOS 5+**
- управление IPv6
- поддержка ядра **Mihomo**
- быстрое переключение Xray / Mihomo
- контроль [файловых дескрипторов](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#контроль-файловых-дескрипторов)
- [внешние списки](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#внешние-списки-портов-и-ip) IP и портов
- [OffLine](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#offline-установка)‑установка
- [Self-Hosted](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#self-hosted-прокси-для-загрузки)-прокси для загрузки компонентов

### Удалено

- не актуальные и повреждённые геобазы
- неиспользуемые конфигурационные файлы
- устаревшие параметры запуска и задачи планировщика

---

### Подробное [описание изменений](https://github.com/jameszeroX/XKeen/blob/main/forkinfo.md)

---

Список параметров запуска XKeen доступен в справке:
```bash
xkeen -h
```

---

## Порядок установки

Требуется роутер **Keenetic**/**Netcraze** с предварительно установленной средой Entware и компонентом `Модули ядра подсистемы Netfilter`

```bash
opkg update && opkg upgrade && opkg install curl tar && cd /tmp
sh -c "$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/install.sh)"
```

---

## Поддержка проекта

Форк XKeen, как и оригинал, совершено бесплатен и не имеет каких либо ограничений по использованию. Надеюсь, доработки XKeen, многие из которых я сделал по Вашим просьбам, оказались полезны, так же, как и мои сообщения в [телеграм-чате](https://t.me/+8Cvh7oVf6cE0MWRi). Для меня очень важно понимать, что труд и время потрачены не зря. Буду благодарен за любую Вашу поддержку на развитие проекта:

- [CloudTips](https://pay.cloudtips.ru/p/7edb30ec)
- [ЮMoney](https://yoomoney.ru/to/41001350776240)
- Карта МИР: `2204 1201 2976 4110`
- USDT, сеть TRC20: `TQhy1LbuGe3Bz7EVrDYn67ZFLDjDBa2VNX`
- USDT, сеть ERC20: `0x6a5DF3b5c67E1f90dF27Ff3bd2a7691Fad234EE2`

<sup>Уточните актуальность крипто-адресов перед переводом</sup>

---

## Дополнения

- XKeen UI — https://github.com/zxc-rv/XKeen-UI
- XKeen UI — https://github.com/umarcheh001/Xkeen-UI
- XKeen UI — https://github.com/fan92rus/xkeen-ui
- Генератор Outbound — https://zxc-rv.github.io/XKeen-UI/Outbound_Generator/
- Парсер подписок - https://github.com/tkukushkin/xkeen-subscription-watcher
- Парсер подписок — https://github.com/V2as/SubKeen
- Mihomo Studio — https://github.com/l-ptrol/mihomo_studio
- Конвертер JSON-подписок — https://sngvy.github.io/json-sub-to-outbounds
- Mihomo HWID Subscription Installer — https://github.com/dorian6996/Mihomo-HWID-Subscription

---

## Источники и ссылки

- Origin XKeen — https://github.com/Skrill0/XKeen
- Xray-core — https://github.com/XTLS/Xray-core
- Mihomo — https://github.com/MetaCubeX/mihomo
- Yq — https://github.com/mikefarah/yq
- FAQ — https://jameszero.net/faq-xkeen.htm
- Telegram‑чат — https://t.me/+8Cvh7oVf6cE0MWRi


================================================
FILE: configuration.md
================================================
---

## Внешние списки портов и IP

Предусмотрена возможность добавить в конфигурационные файлы XKeen необходимые порты проксирования или исключения из проксирования, а также IP-адреса и подсети, проксирование которых не требуется. Файлы находятся в директории `/opt/etc/xkeen/`

- `port_proxying.lst` - порты проксирования, например 80 и 443
- `port_exclude.lst` - порты, которые необходимо исключить из проксирования, например, 3389. 
- `ip_exclude.lst` - IP-адреса и подсети, которые необходимо исключить из проксирования, например, 77.88.8.8

Каждый порт и IP указываются в этих файлах с новой строки. Пустые строки и строки, начинающиеся со знака комментария `#` игнорируются. Порты проксирования и порты исключенные из проксирования не могут применяться вместе, используйте или то, или другое. Если по ошибке будут заполнены оба списка, то приоритет имеют порты проксирования, а список исключенных портов игнорируется. При чистой установке XKeen создаются шаблоны вышеуказанных файлов с примерами их заполнения.

---

## Контроль файловых дескрипторов

В среде 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`). Возможностью контролировать открытые файловые дескрипторы следует пользоваться в крайних ситуациях, когда ничего уже не помогает, так как при этом прокси-клиент будет постоянно перезапускаться для сброса дескрипторов. Рекомендуется вместо этого найти и устранить причину. Это может быть устройство или приложение, открывающее множество подключений.

---

## Self-Hosted-прокси для загрузки

В базовый конфиг добавлены два GitHub-прокси, через которые возможна загрузка XKeen и его компонентов в случае недоступности GitHub. Если же и они окажутся недоступны, можете установить [Self-Hosted прокси](https://github.com/hunshcn/gh-proxy) на своём сервере и указать его в переменной `gh_proxy1` или `gh_proxy2` файла `/opt/sbin/.xkeen/01_info/01_info_variable.sh`

---

## OffLine-установка
Обычная установка XKeen и необходимых компонентов выполняется в OnLine режиме и жёстко привязана к GitHub, а в случае его недоступности будет невозможна. Поэтому в форк дополнительно к способу установки через [Self-Hosted](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#self-hosted-прокси-для-загрузки)-прокси добавлен режим OffLine-установки по команде `xkeen -io`

Для 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:

```
cd /opt/sbin
tar -xvzf xkeen.tar.gz && rm xkeen.tar.gz
xkeen -io
#
```

Копирование файлов конфигурации xray, mihomo и необходимых геофайлов в директории /opt/etc/xray/configs, /opt/etc/mihomo, /opt/etc/xray/dat выполните вручную, после чего можете запустить проксирование командой `xkeen -start`

При OffLine-установке XKeen не проверяет соответствие архитектуры процессора и бинарников, поэтому выбирайте совместимые бинарники внимательно. Если затрудняетесь в выборе, запустите `xkeen -io` без xray и mihomo в папке /opt/sbin/ и XKeen сообщит, какая архитектура требуется для вашего роутера.

При недоступности GitHub, обновление геофайлов по планировщику работать не будет, выполняйте его вручную.

Если недоступен не только GitHub, но и [репозиторий Entware](http://bin.entware.net), то перед OffLine установкой XKeen требуется вручную установить недостающие пакеты из следующего списка:
```
curl, tar, lscpu, jq, libc, libssp, librt, libpthread, iptables, ca-bundle, coreutils-uname, coreutils-nohup
```
либо прописать в файл `/opt/etc/opkg.conf` рабочее зеркало репозитория


================================================
FILE: docs/README.md
================================================
# Документация XKeen

Этот каталог содержит техническую документацию для разработчиков и контрибьюторов. Если вы пользователь и просто хотите установить XKeen — начните с корневого [`README.md`](../README.md) и [`configuration.md`](../configuration.md).

## Содержание

| Документ | О чём |
| --- | --- |
| [architecture.md](architecture.md) | Точка входа, фазы импорта модулей, режимы проксирования, SSoT-переменные |
| [build-and-release.md](build-and-release.md) | GitHub Actions: пакет в Beta-канал, релиз, синхронизация Wiki. Каналы обновлений |
| [runtime-paths.md](runtime-paths.md) | Раскладка файлов и каталогов на роутере (`/opt/...`) |
| [commands.md](commands.md) | Справочник флагов `xkeen` |
| [contributing.md](contributing.md) | Правила правки кода, ограничения POSIX-`sh`, рабочий цикл проверки |

## Связанные документы в корне репозитория

- [`README.md`](../README.md) — обзор и установка для пользователей.
- [`configuration.md`](../configuration.md) — внешние списки портов/IP, fd-контроль, Self-Hosted прокси, OffLine-установка.
- [`forkinfo.md`](../forkinfo.md) — отличия форка от оригинала Skrill0/XKeen.
- [`knownissues.md`](../knownissues.md) — известные ограничения. Читать перед триажом багов.
- [`test/README.md`](../test/README.md) — release-notes 2.0 Beta, новые параметры и инварианты.

## Wiki

Исходники GitHub Wiki лежат в [`../wiki/`](../wiki) и автоматически синхронизируются в `<repo>.wiki.git` через workflow `.github/workflows/wiki-sync.yaml`. См. [build-and-release.md](build-and-release.md#workflow-wiki-syncyaml).


================================================
FILE: docs/architecture.md
================================================
# Архитектура XKeen

XKeen — POSIX-shell утилита (`sh`, не `bash`) для роутеров Keenetic/Netcraze под Entware. Кода на компилируемых языках нет. Целевые архитектуры — `arm64-v8a`, `mips32le`, `mips32`. Запускается на роутере; в этом репозитории — только исходники и упаковка.

## Точка входа

[`scripts/xkeen`](../scripts/xkeen) — монолитный POSIX-`sh` диспетчер (~1450 строк). Парсит флаги через большой `while/case` начиная со строки 119. Каждый флаг — самостоятельная команда.

### Скрытие установочного каталога

При первом запуске функция `install_xkeen_rename` ([`scripts/xkeen:7-21`](../scripts/xkeen)) переименовывает `_xkeen/` → `.xkeen/`. **Все runtime-пути в коде ссылаются на `.xkeen`. В репозитории каталог называется `_xkeen` — путать легко.** CI пакует именно `_xkeen`.

### Self-detach

Если процесс запущен без 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`).

## Импорт модулей

Все модули — это `. file.sh`-импортируемые библиотеки функций. Точка сборки — [`scripts/_xkeen/import.sh`](../scripts/_xkeen/import.sh), которая последовательно тянет `00_*_import.sh` каждой фазы:

| Фаза | Каталог | Назначение |
| --- | --- | --- |
| 01 | [`01_info/`](../scripts/_xkeen/01_info) | Переменные (SSoT — `01_info_variable.sh`), детекция CPU, проверка установленных Xray/Mihomo/geofile, версии, консольный вывод, cron-статус |
| 02 | [`02_install/`](../scripts/_xkeen/02_install) | Установка: opkg-пакеты → ядра (Xray, Mihomo) → XKeen → geofile/IPSET → cron → регистрация (`07_install_register/`) → шаблоны конфигов (`08_install_configs/02_configs_dir/`) |
| 03 | [`03_delete/`](../scripts/_xkeen/03_delete) | Точечное удаление компонентов + полная деинсталляция (`-remove`) |
| 04 | [`04_tools/`](../scripts/_xkeen/04_tools) | Сервис: управление портами, модули ядра, диагностика, задержка автозапуска, интерактивный выбор (`05_tools_choice/`), бэкапы (`06_tools_backups/`), загрузчики через GH-proxy fallback (`07_tools_downloaders/`) |
| 05 | [`05_tests/`](../scripts/_xkeen/05_tests) | Runtime-проверки сети, портов, носителя |

## Single Source of Truth: `01_info_variable.sh`

Файл [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh) — единственное место, где определены:

- Версия и канал: `xkeen_current_version`, `xkeen_build`, `build_timestamp` (последнее — подставляется CI).
- Все runtime-каталоги: `xkeen_dir=/opt/sbin/.xkeen`, `xkeen_cfg=/opt/etc/xkeen`, `geo_dir=/opt/etc/xray/dat`, и др.
- Все внешние URL: GitHub API для XKeen/Xray/Mihomo, прямые URL архивов, geofile-репозитории.
- GitHub-прокси для регионов с ограничениями: `gh_proxy1=https://gh-proxy.com`, `gh_proxy2=https://ghfast.top`.

При смене версии или URL правится только этот файл. Релизный workflow перезаписывает в нём только `build_timestamp`.

## GH-proxy fallback

Любая загрузка с GitHub в [`04_tools/07_tools_downloaders/`](../scripts/_xkeen/04_tools/07_tools_downloaders) и в корневом [`install.sh`](../install.sh) идёт по цепочке:

1. Прямой URL (например, `github.com/.../xkeen.tar.gz`).
2. `gh_proxy1` префиксом — `https://gh-proxy.com/<github_url>`.
3. `gh_proxy2` префиксом — `https://ghfast.top/<github_url>`.

С версии 2.0 Beta параметр `gh_proxy` из `/opt/etc/xkeen/xkeen.json` имеет приоритет над встроенными значениями.

Маркер `/tmp/toff` (создаётся при запуске с флагом `-toff`) отключает `curl -m 180` на одну сессию — полезно для медленных каналов.

## Режимы проксирования

В рантайме определяется один из четырёх режимов:

| Режим | Признак |
| --- | --- |
| TProxy | Inbound с `streamSettings.sockopt.tproxy == "tproxy"` (Xray) или `listeners[].type == "tproxy"` (Mihomo) |
| Hybrid | Бывший Mixed — комбинация TProxy + Redirect |
| Redirect | Inbound с `sockopt.tproxy == "redirect"`. Самый быстрый, но без UDP |
| Other | socks5/http inbound |

Определение режима — в [`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"`).

**Имена inbound-тегов больше не влияют на режим — исправление 2.0 Beta.** Ранее теги вроде `tproxy-in`/`redirect-in` использовались как fallback, что приводило к ошибкам при кастомных тегах.

## Beta-функции

Описаны в [`test/README.md`](../test/README.md). Кратко:

- Кастомные политики маршрутизации в `xkeen.json`.
- IPSET `ru-exclude` — исключение российских IP из проксирования на уровне ipset.
- DSCP-метки 62 (исключение) и 63 (проксирование) — маршрутизация по приоритетам QoS-пакетов. См. также wiki-страницу [Маршрутизация по DSCP](../wiki/Маршрутизация-по-DSCP.md).
- Проксирование трафика Entware-пакетов с `routing-mark: 255` (Xray) / `mark: 255` (Mihomo).


================================================
FILE: docs/build-and-release.md
================================================
# Сборка и релиз

Локальной сборки нет. Всё делает CI на GitHub Actions. В этом разделе — три workflow-а и две схемы каналов обновлений.

## Workflow-ы

### `package-folder.yaml`

[`.github/workflows/package-folder.yaml`](../.github/workflows/package-folder.yaml)

| Параметр | Значение |
| --- | --- |
| Триггер | `push` в `main` с изменениями в `scripts/**`, либо `workflow_dispatch` |
| Результат | `test/xkeen.tar.gz` — Beta-канал, упакован из `scripts/*` |
| Подпись | GPG-подписанный автокоммит `[github-actions] automated compiling build` |

Шаги:

1. Checkout с `fetch-depth: 0`.
2. Импорт GPG-ключа через `crazy-max/ghaction-import-gpg@v7` с `git_config_global: true`.
3. Подмена `build_timestamp="…"` в `scripts/_xkeen/01_info/01_info_variable.sh` на текущее MSK-время.
4. Упаковка: `cd scripts_for_build && find . -type f -o -type l | sed 's|^\./||' | tar -czf .../xkeen.tar.gz -T -`. На верхнем уровне архива — `xkeen` и `_xkeen/`, без вложенного `scripts/`.
5. Перемещение архива в `test/` и подписанный коммит обратно в `main`.

**Файл `test/xkeen.tar.gz` — артефакт CI, руками не редактировать.**

### `release.yaml`

[`.github/workflows/release.yaml`](../.github/workflows/release.yaml)

| Параметр | Значение |
| --- | --- |
| Триггер | `workflow_dispatch` с входами `version` (string) и `prerelease` (boolean) |
| Результат | `dist/xkeen.tar.gz` + `dist/xkeen.tar` + GitHub Release + подписанный GPG-тег |

Шаги:

1. Checkout с `fetch-depth: 0`.
2. Импорт GPG-ключа.
3. Подмена `build_timestamp` (как в `package-folder.yaml`).
4. Двойная упаковка: `.tar.gz` (для роутеров с `tar`+gzip) и `.tar` (для альтернативных распаковщиков).
5. Удаление существующего тега, создание подписанного `git tag -s "$VERSION"`, push.
6. `gh release create` с обоими архивами. При `prerelease=true` — флаг `--prerelease`.
7. Верификация подписи `git tag -v`.

### `wiki-sync.yaml`

[`.github/workflows/wiki-sync.yaml`](../.github/workflows/wiki-sync.yaml)

| Параметр | Значение |
| --- | --- |
| Триггер | `push` в `main` с изменениями в `wiki/**` или сам workflow, либо `workflow_dispatch` |
| Результат | Содержимое `wiki/` синхронизировано в `<repo>.wiki.git` подписанным коммитом |

Шаги:

1. Checkout главного репо.
2. Импорт GPG-ключа (тот же `crazy-max/ghaction-import-gpg@v7`).
3. Клонирование `<repo>.wiki.git` через `https://x-access-token:${GITHUB_TOKEN}@github.com/<repo>.wiki.git`.
4. `rsync -a --delete --exclude='.git' wiki/ wiki-repo/` — добавление, обновление, удаление.
5. Подписанный коммит `[github-actions] sync wiki from main@<short-sha>` и push в дефолтную ветку Wiki.

Пререкизиты для прода:

- В Settings → Features → Wikis: ✅ enabled.
- В Wiki создана хотя бы одна страница через UI (иначе `<repo>.wiki.git` отдаёт 404).
- В Settings → Actions → General → Workflow permissions: `Read and write permissions`.
- Secret `GPG_PRIVATE_KEY` (passphrase не используется).

## Каналы обновлений

| Канал | Источник | Триггер |
| --- | --- | --- |
| Stable | GitHub Release с тегом, `xkeen_tar_url` | Прогон `release.yaml` |
| Beta | `test/xkeen.tar.gz` в ветке `main`, `xkeen_dev_url` | Любой merge в `main` с изменениями `scripts/**` |

На роутере переключение каналов — `xkeen -channel`. Текущая версия и канал хранятся в `01_info_variable.sh` (`xkeen_current_version`, `xkeen_build`).

## Воспроизвести локальную сборку

Без CI, для отладки упаковки:

```sh
cd scripts && find . -type f -o -type l | sed 's|^\./||' | tar -czf /tmp/xkeen.tar.gz -T -
```

Результат идентичен тому, что генерирует `package-folder.yaml` (за исключением подменённого `build_timestamp`).


================================================
FILE: docs/commands.md
================================================
# Справочник флагов `xkeen`

Полный список флагов диспетчера [`scripts/xkeen`](../scripts/xkeen). Извлечён из `help_xkeen()` в [`scripts/_xkeen/about.sh`](../scripts/_xkeen/about.sh). Деструктивные флаги отмечены ⚠ — они интерактивные, требуют подтверждения, не имеют опции «тихого» режима.

## Установка

| Флаг | Действие |
| --- | --- |
| `-i`, `-install` | Полный цикл: XKeen + Xray + GeoFile/GeoIPSET + Mihomo |
| `-io` | OffLine-установка XKeen из локальной флешки |
| `-toff` | Отключить таймаут `curl` для медленных каналов: `xkeen -i -toff` |

## Переустановка

| Флаг | Действие |
| --- | --- |
| `-k` | XKeen |
| `-g` | GeoFile (GeoSite + GeoIP) |
| `-gips` | GeoIPSET |

## Обновление

| Флаг | Действие |
| --- | --- |
| `-uk` | XKeen (получает Stable или Beta — см. `-channel`) |
| `-ug` | GeoFile/GeoIPSET |
| `-ux` | Xray — повышение или понижение версии |
| `-um` | Mihomo — повышение или понижение версии |

## Автообновление GeoFile/GeoIPSET (cron-задача)

| Флаг | Действие |
| --- | --- |
| `-ugc` | Создать cron-задачу |
| `-dgc` | Удалить cron-задачу |

## Резервные копии

| Флаг | Действие |
| --- | --- |
| `-kb` | Создать резервную копию XKeen |
| `-kbr` | Восстановить XKeen из резервной копии |
| `-xb` | Создать резервную копию конфигурации Xray |
| `-xbr` | Восстановить конфигурацию Xray |
| `-mb` | Создать резервную копию конфигурации Mihomo |
| `-mbr` | Восстановить конфигурацию Mihomo |

## Удаление ⚠

| Флаг | Действие |
| --- | --- |
| `-remove` | ⚠ Полная деинсталляция XKeen |
| `-dgs` | ⚠ Удалить GeoSite |
| `-dgi` | ⚠ Удалить GeoIP |
| `-dgips` | ⚠ Удалить GeoIPSET |
| `-dx` | ⚠ Удалить Xray |
| `-dm` | ⚠ Удалить Mihomo |
| `-dk` | ⚠ Удалить XKeen (сохраняет ядра) |

## Порты проксирования

| Флаг | Действие |
| --- | --- |
| `-ap` | Добавить порт |
| `-dp` | Удалить порт |
| `-cp` | Посмотреть список |

## Порты, исключённые из проксирования

| Флаг | Действие |
| --- | --- |
| `-ape` | Добавить порт-исключение |
| `-dpe` | Удалить порт-исключение |
| `-cpe` | Посмотреть список |

## Управление прокси-клиентом

| Флаг | Действие |
| --- | --- |
| `-start` | Запуск |
| `-stop` | Остановка |
| `-restart` | Перезапуск |
| `-status` | Статус работы |
| `-tp` | Порты, шлюз и протокол прокси-клиента |
| `-auto` | Включить / отключить автозапуск |
| `-d` | Установить задержку автозапуска: `xkeen -d 30` (секунд) |
| `-fd` | Включить / отключить контроль файловых дескрипторов |
| `-cfd` | Посчитать количество открытых файловых дескрипторов прокси-клиента |
| `-diag` | Выполнить полную диагностику (единственный поддерживаемый канал для отчёта о проблеме) |
| `-channel` | Переключить канал обновлений (Stable / Beta) |
| `-xray` | Переключить XKeen на ядро Xray |
| `-mihomo` | Переключить XKeen на ядро Mihomo |
| `-ipv6` | Включить / отключить протокол IPv6 в KeeneticOS |
| `-dns` | Включить / отключить перенаправление DNS в прокси |
| `-pr` | Включить / отключить проксирование трафика Entware |
| `-extmsg` | Включить / отключить расширенные сообщения при запуске |
| `-cbk` | Включить / отключить резервное копирование XKeen при обновлении |
| `-aghfix` | Включить / отключить отображение клиентов под своими IP в журнале AdGuard Home |

## Информация

| Флаг | Действие |
| --- | --- |
| `-about` | О программе |
| `-ad` | Поддержать разработчиков |
| `-af` | Обратная связь / контакты |
| `-v` | Версия XKeen |
| `-h`, `-help` | Показать встроенную справку (`help_xkeen` из `about.sh`) |

## Переменные окружения

| Переменная | Значение | Эффект |
| --- | --- | --- |
| `XKEEN_FOREGROUND` | `1` | Отключает self-detach при запуске без TTY. Использовать в скриптах с синхронной семантикой (`xkeen -start && cleanup`) |
| `XKEEN_DETACHED` | `1` | Внутренний маркер — выставляется самим `xkeen` после форка через `start-stop-daemon -b`. Руками не выставлять |


================================================
FILE: docs/contributing.md
================================================
# Правила правки

## Язык — POSIX `sh`

Целевая среда — Entware на BusyBox `ash`. **Никаких bash-измов:**

| Запрещено | Использовать вместо |
| --- | --- |
| `[[ … ]]` | `[ … ]` (POSIX `test`) |
| `${var,,}`, `${var^^}` | `echo "$var" \| tr 'A-Z' 'a-z'` |
| Массивы (`arr=(a b c)`, `${arr[i]}`) | Позиционные параметры, IFS-split строки |
| `<<<` (here-string) | `echo "…" \| cmd` или `<< EOF` |
| `function name()` | `name()` |
| `local var` | Не использовать — `local` не POSIX |
| `(( … ))` арифметика | `$(( … ))` или `expr` |
| `read -p` | `printf '...'; read var` |

Проверка перед PR:

```sh
shellcheck scripts/xkeen scripts/_xkeen/**/*.sh
```

## Пути и URL — только из переменных

Все пути и 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`.

## Добавление модуля

1. Определить, к какой фазе относится: `01_info/`, `02_install/`, `03_delete/`, `04_tools/`, `05_tests/`. Если новый раздел внутри `02_install/` (например, поддиректория `09_install_X/`) — создать каталог и поместить туда `00_<phase>_import.sh`.
2. Создать файл `NN_<purpose>.sh` в нужном каталоге (нумерация — следующая свободная).
3. Подключить через `.` в соответствующем `00_*_import.sh` родительского каталога.
4. Если модуль зависит от других — следить за порядком импорта.

## Добавление новой команды

1. Case-ветка в [`scripts/xkeen`](../scripts/xkeen) в большом `while/case` (начинается со строки 119).
2. Если команда из `{-start, -stop, -restart}` или другая, требующая self-detach в фоне — добавить в проверку на строках 43-48 (`detach_eligible=true`).
3. Описание флага — в `help_xkeen()` функции [`scripts/_xkeen/about.sh`](../scripts/_xkeen/about.sh) под подходящим разделом.
4. Если команда деструктивная — обязательно интерактивное подтверждение перед действием. Не делать «тихие» деструктивные операции.

## Лимиты файловых дескрипторов

Значения в стартовом скрипте: `arm64_fd=40000`, `other_fd=10000`. Не править наугад — увеличение влечёт расход RAM, уменьшение — обрывы соединений на пиках. См. также соответствующий раздел в [`configuration.md`](../configuration.md).

## Self-detach

Блок в [`scripts/xkeen:43-70`](../scripts/xkeen) — критичный для cron-перезапусков. Без него родитель убивает дочерний процесс по SIGHUP при обрыве ssh-сессии. Трогать только осознанно.

## Проверка перед PR

1. `shellcheck scripts/xkeen scripts/_xkeen/**/*.sh` — нулевая толерантность к новым warning-ам.
2. Деплой архива на тестовый роутер и прогон сценариев: `xkeen -i`, `-start`, `-stop`, `-restart`, `-uk`, `-diag`.
3. Если правились флаги управления (`-ap`, `-dp`, `-ape`, `-dpe`) или режимы проксирования — отдельно прогнать с обоими ядрами (Xray и Mihomo) и в каждом из режимов TProxy/Hybrid/Redirect.
4. `xkeen -diag` — единственный поддерживаемый канал для отчёта о проблеме.

## CI-файлы — не трогать руками

- [`.github/workflows/package-folder.yaml`](../.github/workflows/package-folder.yaml) и сам артефакт [`test/xkeen.tar.gz`](../test/xkeen.tar.gz) — генерируются CI. Любые ручные правки будут перезаписаны при следующем push в `main` с изменениями `scripts/**`.
- [`.github/workflows/release.yaml`](../.github/workflows/release.yaml) — менять только если действительно меняется процесс релиза.
- [`.github/workflows/wiki-sync.yaml`](../.github/workflows/wiki-sync.yaml) — синхронизирует [`wiki/`](../wiki) в GitHub Wiki. Менять только при изменении логики синхронизации.

## Документация

- Корневые `README.md`, `configuration.md`, `forkinfo.md`, `knownissues.md` — пользовательская документация. При фичах, затрагивающих пользователя, — обновлять.
- [`test/README.md`](../test/README.md) — release-notes 2.0 Beta. При новой Beta-фиче — добавить запись.
- [`docs/`](.) — техническая документация для контрибьюторов. При структурных изменениях кода — обновлять `architecture.md` / `runtime-paths.md` / `commands.md`.
- [`wiki/`](../wiki) — публичная Wiki для пользователей. Обновления синхронизируются автоматически.

## Каналы и версии

- Ветка `main` → Beta-канал, `test/xkeen.tar.gz`, автоматически после push.
- GitHub Release с подписанным тегом → Stable-канал.
- Версия и канал хранятся в [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh): `xkeen_current_version`, `xkeen_build`.


================================================
FILE: docs/runtime-paths.md
================================================
# Раскладка на роутере

Все runtime-пути на роутере. В этом репозитории каталог `_xkeen/`; после установки на роутер он переименовывается в `.xkeen/` функцией `install_xkeen_rename`. Все переменные путей определены в [`scripts/_xkeen/01_info/01_info_variable.sh`](../scripts/_xkeen/01_info/01_info_variable.sh) — не хардкодить.

## Исполняемые файлы и модули

| Путь | Назначение |
| --- | --- |
| `/opt/sbin/xkeen` | Диспетчер (исполняемый, монолитный POSIX-sh) |
| `/opt/sbin/.xkeen/` | Каталог импортируемых модулей XKeen |
| `/opt/sbin/.xkeen/import.sh` | Точка сборки модулей |
| `/opt/etc/init.d/S05xkeen` | Init-скрипт XKeen, генерируется `04_register_init.sh` |
| `/opt/etc/init.d/S05crond` | Cron-демон, обслуживает автообновления geofile |

## Пользовательский конфиг

`/opt/etc/xkeen/` — все настройки, которые правит пользователь.

| Файл | Назначение |
| --- | --- |
| `xkeen.json` | Главный конфиг: `gh_proxy`, политики, расширения 2.0 Beta |
| `ip_exclude.lst` | IP/подсети, исключённые из проксирования (с маской `/32` для одиночных адресов) |
| `port_proxying.lst` | Порты, направляемые в прокси. С 2.0 Beta — единственный источник, старая `port_donor` упразднена |
| `port_exclude.lst` | Порты, исключённые из проксирования. С 2.0 Beta — единственный источник, старая `port_exclude` (как переменная) упразднена |
| `ipset/ru_exclude_ipv4.lst` | IPv4-сеты для российских IP — Beta-функция исключения по ipset |
| `ipset/ru_exclude_ipv6.lst` | То же для IPv6 |

## Конфиги ядер

| Путь | Назначение |
| --- | --- |
| `/opt/etc/xray/configs/` | Все JSON-конфиги Xray (`inbounds.json`, `outbounds.json`, `routing.json`, `dns.json`) |
| `/opt/etc/xray/dat/` | GeoSite (`*.dat`) и GeoIP (`*.dat`) базы |
| `/opt/etc/mihomo/` | Конфигурация Mihomo (`config.yaml` и подключаемые) |

## Логи

| Путь | Назначение |
| --- | --- |
| `/opt/var/log/xkeen/` | Логи самого XKeen |
| `/opt/var/log/xray/access.log` | Access-лог Xray |
| `/opt/var/log/xray/error.log` | Error-лог Xray |
| `/opt/var/log/xkeen-detached.log` | Лог фоновых запусков (self-detach из `-start/-stop/-restart` без TTY) |

## Runtime-state

| Путь | Назначение |
| --- | --- |
| `/opt/var/run/` | PID-файлы (`xkeen.pid`, `xray.pid`, `mihomo.pid`) |
| `/opt/tmp/xkeen/` | Временная директория XKeen |
| `/opt/tmp/xray/`, `/opt/tmp/mihomo/` | Временные директории ядер |
| `/opt/backups/` | Архивы резервных копий (флаги `-kb`, `-xb`, `-mb`) |
| `/opt/var/spool/cron/crontabs/root` | Cron-задачи (создаются флагом `-ugc`) |

## Хуки в netfilter.d / schedule.d

| Путь | Назначение |
| --- | --- |
| `/opt/etc/ndm/netfilter.d/proxy.sh` | Хук при пересборке правил межсетевого экрана Keenetic — переставляет iptables-правила прокси |
| `/opt/etc/ndm/schedule.d/00-xkeen-hotspot-sync.sh` | Хук на смену клиентов hotspot — обновляет ipset `xkeen_deny_mac` |

## Маркеры

| Файл | Что значит |
| --- | --- |
| `/tmp/toff` | Маркер сессии: отключает таймаут `curl -m 180`. Создаётся флагом `-toff`, очищается trap-ом INT/TERM |
| `/opt/etc/ndm/netfilter.d/aghfix.sh` | Опциональный фикс отображения клиентов в AdGuard Home (флаг `-aghfix`) |


================================================
FILE: forkinfo.md
================================================
## Сравнение форка с оригинальным XKeen

Изменения:
- Исправлено добавление портов в исключения (ранее команду `xkeen -ape` нужно было прерывать по ctrl+c)
- Исправлена совместная работа режима TProxy и socks5 (ранее Xkeen запускался в Mixed режиме, что приводило к неработоспособности прозрачного проксирования)
- Исправлен автозапуск XKeen при старте роутера (ранее XKeen в некоторых случаях не запускался или запускался для всего устройства, а не только для своей политики - [FAQ п.12](https://jameszero.net/faq-xkeen.htm#12))
- Снято техническое ограничение, позволявшее использовать не более 15 портов проксирования и портов исключенных из проксирования
- Переработана логика загрузки XKeen, Xray, Mihomo и GeoFile из интернета, уменьшающая вероятность их повреждения
- Переработана логика применения правил iptables и ip6tables (ранее XKeen применял все правила, даже при не установленном компоненте IPv6)
- Переработана логика добавления и удаления портов проксирования и исключаемых портов
- При обновлении геофайлов, добавлении/удалении портов проксирования или портов исключений, а также выполнении других настроек, требующих перезапуск XKeen, прокси-клиент теперь перезапускается если был до этого запущен
- При запуске `xkeen -d` без цифрового параметра, теперь отображается информация о текущей задержке автозапуска
- При запуске или перезапуске XKeen теперь отображается информация о режиме работы - TProxy, Mixed, Redirect, Other
- Не актуальные GeoSite и GeoIP antifilter-community заменены на базы [Re:filter](https://github.com/1andrevich/Re-filter-lists)
- Объединены задачи планировщика по обновлению GeoSite и GeoIP. В связи с этим упразднены параметры запуска `-ugs`, `-ugi`, `-ugsc`, `-ugic`, `-dgsc`, `-dgic`
- Параметр запуска `-ux` для обновления ядра Xray теперь поддерживает повышение/понижение версии
- Корректная деинсталляция xray-core (ранее пакет xray не удалялся при деинсталляции)
- Справка (`xkeen -h`) выровнена по табуляции и повышен контраст текста
- Скрипт запуска S24xray переименован в S99xkeen
- Рефакторинг кода скриптов
- Актуализация конфигурационных файлов xray-core

Добавлено:
- Совместимость с прошивкой KeeneticOS 5+
- Возможность отключить/включить протокол IPv6 в KeeneticOS (параметр запуска `-ipv6`)
- Поддержка ядра Mihomo
- Возможность сменить ядро проксирования (Xray/Mihomo) параметрами `-xray` и `-mihomo`
- При обновлении Xray и Mihomo теперь отображается версия уже установленного в роутере бинарника
- Добавлена возможность отключить/включить перехват DNS запросов при соответствующей настройке прокси-клиента (параметр запуска `-dns`)
- Поддержка внешних файлов `ip_exclude.lst`, `port_proxying.lst` и `port_exclude.lst` в директории `/opt/etc/xkeen/` для указания IP и портов (проксирования/исключения из проксирования)
- Возможность загружать компоненты XKeen через [Self-Hosted прокси](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#self-hosted-прокси-для-загрузки-компонентов) при недоступности GitHub (переменные `gh_proxy(1|2)` в файле `01_info_variable.sh`)
- Возможность отключить резервное копирование XKeen при обновлении (переменная `backup` в стартовом скрипте)
- Возможность [OffLine установки](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#offline-установка) (параметр `-io`)
- Возможность установки GeoIP базы [zkeenip.dat](https://github.com/jameszeroX/zkeen-ip)
- Обновление [zkeen.dat](https://github.com/jameszeroX/zkeen-domains) и [zkeenip.dat](https://github.com/jameszeroX/zkeen-ip) по расписанию средствами XKeen
- При недоступности GitHub API используется резервный источник релизов для XKeen, Xray и Mihomo 
- При установке теперь можно выбрать, добавлять ли XKeen в автозагрузку при включении роутера или нет
- При пропуске установки Xray, его конфигурационные файлы и геобазы так же пропускаются и не устанавливаются
- Mihomo и парсер yaml-файлов Yq устанавливаются и регистрируются в entware, как полноценные ipk-пакеты
- Параметр запуска `-remove` для полной деинсталляции XKeen (ранее деинсталляцию нужно было выполнять покомпонентно)
- Параметры запуска `-ug` (обновление геофайлов), `-ugc` (управление заданием Cron, обновляющим геофайлы), `-dgc` (удаление задания Cron, обновляющего геофайлы)
- Параметр запуска `-um` для обновления/установки ядра Mihomo (поддерживается повышение/понижение версии)
- Параметры запуска: `-rrm` (обновить регистрацию Mihomo), `-drm` (удалить регистрацию Mihomo)
- Параметр запуска `-dm` для деинсталляции ядра Mihomo
- Параметр запуска `-g`, позволяющий переустановить (добавить/удалить) геофайлы для Xray
- Параметр запуска `-channel`, позволяющий выбрать канал обновления XKeen между Stable и Dev ветками
- Возможность резервного копирования и восстановления конфигурации Mihomo (параметры `-mb`, `-mbr`)
- Возможность контролировать число открытых файловых дескрипторов, используемых прокси-клиентом и перезапускать процесс при исчерпании лимита  [подробнее](https://github.com/jameszeroX/XKeen/blob/main/configuration.md#контроль-файловых-дескрипторов)

Удалено:
- Поддержка внешнего файла `/opt/etc/xkeen_exclude.lst` c IP-адресами и подсетями для исключения из проксирования
- Возможность установки GeoSite Antizapret (база повреждена в репозитории)
- Конфигурационный файл `02_transport.json` (не используется новыми ядрами xray-core)
- Запрос на перезапись и сама перезапись конфигурационных файлов Xray, если они уже существуют на момент установки XKeen
- Создание резервных копий Xray, так как теперь можно интерактивно установить предыдущую версию ядра параметром `-ux`. В связи с этим упразднены параметры запуска `-xb` и `-xbr`
- Логирование процесса установки XKeen в директорию `/opt/var/log/xkeen` (на практике не использовалось)
- Задачи планировщика по автообновлению XKeen/Xray. В связи с этим упразднены параметры запуска `-uac`, `-ukc`, `-uxc`, `-dac`, `-dkc` и `-dxc`
- Параметры запуска: `-x` (заменён на `-ux`), `-rk` (заменён на `-rrk`), `-rx` (заменён на `-rrx`), `-rc` (не актуален)


================================================
FILE: install.sh
================================================
#!/bin/sh

green="\033[92m"
red="\033[91m"
yellow="\033[93m"
light_blue="\033[96m"
reset="\033[0m"

url_stable="https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz"
url_beta="https://raw.githubusercontent.com/jameszeroX/XKeen/main/test/xkeen.tar.gz"
archive_name="xkeen.tar.gz"
release_fix_url="https://raw.githubusercontent.com/jameszeroX/XKeen/main/01_info_variable.sh"

clear
echo
printf "  Какую версию ${yellow}XKeen${reset} вы хотите установить?\n\n"
printf "  1) Стабильную версию (${light_blue}Stable${reset})\n"
printf "  2) Новую Бета-версию (${light_blue}Beta${reset})\n\n"
printf "  Выберите 1 или 2 [по умолчанию 1]: "
read -r version_choice

case "$version_choice" in
    2)
        url="$url_beta"
        echo
        printf "  Выбрана ${light_blue}Бета-версия${reset}\n"
        ;;
    *)
        url="$url_stable"
        echo
        printf "  Выбрана ${light_blue}Стабильная версия${reset}\n"
        ;;
esac
echo

get_release_var_file() {
    if [ -f /opt/sbin/_xkeen/01_info/01_info_variable.sh ]; then
        printf '%s\n' "/opt/sbin/_xkeen/01_info/01_info_variable.sh"
        return 0
    fi

    if [ -f /opt/sbin/.xkeen/01_info/01_info_variable.sh ]; then
        printf '%s\n' "/opt/sbin/.xkeen/01_info/01_info_variable.sh"
        return 0
    fi

    return 1
}

download_xkeen_release() {
    if curl -fLo "$archive_name" --connect-timeout 10 -m 15 "$url"; then
        return 0
    fi

    if curl -fLo "$archive_name" --connect-timeout 10 -m 15 "https://gh-proxy.com/$url"; then
        return 0
    fi

    if curl -fLo "$archive_name" --connect-timeout 10 -m 15 "https://ghfast.top/$url"; then
        return 0
    fi

    printf "  ${red}Ошибка${reset}: не удалось загрузить ${yellow}xkeen.tar.gz${reset}\n"
    return 1
}

download_release_fix() {
    target_file="$1"

    if curl -fLo "$target_file" --connect-timeout 10 -m 15 "$release_fix_url"; then
        return 0
    fi

    if curl -fLo "$target_file" --connect-timeout 10 -m 15 "https://gh-proxy.com/$release_fix_url"; then
        return 0
    fi

    if curl -fLo "$target_file" --connect-timeout 10 -m 15 "https://ghfast.top/$release_fix_url"; then
        return 0
    fi

    printf "  ${red}Ошибка${reset}: не удалось применить исправление ${yellow}01_info_variable.sh${reset} для релиза ${green}1.1.3.9${reset}\n"
    return 1
}

apply_release_1139_yq_fix() {
    release_var_file="$(get_release_var_file)" || {
        printf "  ${red}Ошибка${reset}: после распаковки не найден файл ${yellow}01_info_variable.sh${reset}\n"
        return 1
    }

    release_version=$(sed -n 's/^xkeen_current_version="\([^"]*\)".*/\1/p' "$release_var_file" | head -n 1)
    release_build=$(sed -n 's/^xkeen_build="\([^"]*\)".*/\1/p' "$release_var_file" | head -n 1)

    if [ "$release_version" = "1.1.3.9" ] && [ "$release_build" = "Stable" ]; then
        if ! download_release_fix "$release_var_file"; then
            return 1
        fi
    fi
}

if ! download_xkeen_release; then
    exit 1
fi

if ! tar -xzf "$archive_name" -C /opt/sbin; then
    rm -f "$archive_name"
    printf "  ${red}Ошибка${reset}: не удалось распаковать ${yellow}xkeen.tar.gz${reset}\n"
    exit 1
fi

rm -f "$archive_name"

if [ ! -x /opt/sbin/xkeen ]; then
    printf "  ${red}Ошибка${reset}: после распаковки не найден исполняемый файл ${yellow}/opt/sbin/xkeen${reset}\n"
    exit 1
fi

if ! apply_release_1139_yq_fix; then
    exit 1
fi

exec /opt/sbin/xkeen -i

================================================
FILE: knownissues.md
================================================
- При проксировании DNS с помощью XKeen, в профиле "Политика по умолчанию" отсутствует интернет, создайте пользовательсую политкику вместо этого профиля
- При подключении к роутеру нескольких провайдеров, XKeen работает через "Основное подключение", даже если в политике XKeen отметить галкой "Резервное"


================================================
FILE: scripts/_xkeen/01_info/00_info_import.sh
================================================
# Импорт информационных модулей

# Модуль информации
. "$xinfo_dir/01_info_variable.sh"
. "$xinfo_dir/02_info_packages.sh"
. "$xinfo_dir/03_info_cpu.sh"
. "$xinfo_dir/04_info_mihomo.sh"
. "$xinfo_dir/04_info_xray.sh"
. "$xinfo_dir/05_info_geofile.sh"
. "$xinfo_dir/06_info_console.sh"
. "$xinfo_dir/07_info_cron.sh"

. "$xinfo_dir/08_info_version/00_version_import.sh"


================================================
FILE: scripts/_xkeen/01_info/01_info_variable.sh
================================================
# -------------------------------------
# Цвета
# -------------------------------------
green="\033[92m"	# Зеленый
red="\033[91m"		# Красный
yellow="\033[93m"	# Желтый
light_blue="\033[96m"	# Голубой
italic="\033[3m"	# Курсив
reset="\033[0m"		# Сброс цветов

# -------------------------------------
# Информация
# -------------------------------------
current_datetime=$(date +"%Y-%m-%d_%H-%M")
xkeen_current_version="2.0"
xkeen_build="Beta"
build_timestamp=""

# -------------------------------------
# Директории
# -------------------------------------
tmp_dir="/opt/tmp"			 # Временная директория
ktmp_dir="$tmp_dir/xkeen"		 # Временная директория XKeen
xtmp_dir="$tmp_dir/xray"		 # Временная директория Xray
mtmp_dir="$tmp_dir/mihomo"		 # Временная директория Mihomo
install_dir="/opt/sbin"			 # Директория установки
xkeen_dir="$install_dir/.xkeen"		 # Директория скриптов XKeen
xkeen_cfg="/opt/etc/xkeen"		 # Директория конфигурации XKeen
ipset_cfg="$xkeen_cfg/ipset"		 # Директория IPSET
log_dir="/opt/var/log"			 # Директория логов
xray_log_dir="$log_dir/xray"		 # Директория логов Xray
initd_dir="/opt/etc/init.d"		 # Директория init.d
backups_dir="/opt/backups"		 # Директория бекапов
geo_dir="/opt/etc/xray/dat"		 # Директория для dat
cron_dir="/opt/var/spool/cron/crontabs"	 # Директория планировщика
mihomo_conf_dir="/opt/etc/mihomo"	 # Директория конфигурации Mihomo
xray_conf_dir="/opt/etc/xray/configs"	 # Директория конфигурации Xray
xray_conf_smpl="$xkeen_dir/02_install/08_install_configs/02_configs_dir"
register_dir="/opt/lib/opkg/info"
os_modules="/lib/modules/$(uname -r)"
user_modules="/opt/lib/modules"

# -------------------------------------
# Файлы
# -------------------------------------
xkeen_var_file="$xkeen_dir/01_info/01_info_variable.sh"
file_port_proxying="$xkeen_cfg/port_proxying.lst"
file_port_exclude="$xkeen_cfg/port_exclude.lst"
file_ip_exclude="$xkeen_cfg/ip_exclude.lst"
ru_exclude_ipv4="$ipset_cfg/ru_exclude_ipv4.lst"
ru_exclude_ipv6="$ipset_cfg/ru_exclude_ipv6.lst"
xkeen_config="$xkeen_cfg/xkeen.json"
status_file="/opt/lib/opkg/status"
initd_file="$initd_dir/S05xkeen"
initd_cron="$initd_dir/S05crond"
cron_file="root"
file_netfilter_hook="/opt/etc/ndm/netfilter.d/proxy.sh"
file_schedule_hook="/opt/etc/ndm/schedule.d/00-xkeen-hotspot-sync.sh"
name_ipset_deny_mac="xkeen_deny_mac"

# -------------------------------------
# Ресурсы для проверки доступа в интернет
# -------------------------------------
conn_URL="ya.ru"
conn_IP1="195.208.4.1"
conn_IP2="77.88.44.55"

# -------------------------------------
# URL
# -------------------------------------
xkeen_api_url="https://api.github.com/repos/jameszeroX/xkeen/releases/latest"			# url api для XKeen
xkeen_jsd_url="https://data.jsdelivr.com/v1/package/gh/jameszeroX/xkeen"			# резервный url api для XKeen
xkeen_tar_url="https://github.com/jameszeroX/XKeen/releases/latest/download/xkeen.tar.gz"	# url для загрузки XKeen
xkeen_dev_url="https://raw.githubusercontent.com/jameszeroX/xkeen/main/test/xkeen.tar.gz"	# url для загрузки XKeen dev
xray_api_url="https://api.github.com/repos/XTLS/Xray-core/releases"				# url api для Xray
xray_jsd_url="https://data.jsdelivr.com/v1/package/gh/XTLS/Xray-core"				# резервный url api для Xray
xray_zip_url="https://github.com/XTLS/Xray-core/releases/download"				# url для загрузки Xray
mihomo_api_url="https://api.github.com/repos/MetaCubeX/mihomo/releases"				# url api для Mihomo
mihomo_jsd_url="https://data.jsdelivr.com/v1/package/gh/MetaCubeX/mihomo"			# резервный url api для Mihomo
mihomo_gz_url="https://github.com/MetaCubeX/mihomo/releases/download"				# url для загрузки Mihomo
yq_upstream_dist_url="https://github.com/mikefarah/yq/releases/latest/download"			# url для загрузки оригинального Yq
yq_workaround_dist_url="https://github.com/jameszeroX/yq/releases/latest/download"		# url для загрузки рабочего Yq
gh_proxy1="https://gh-proxy.com"								# 1 прокси для загрузок с GitHub
gh_proxy2="https://ghfast.top"									# 2 прокси для загрузок с GitHub

yq_use_workaround="true"									# отключить после исправления issue 2609 (по желанию)
yq_workaround_issue_url="https://github.com/mikefarah/yq/issues/2609"				# issue с поломанным релизом Yq
get_yq_dist_url() {
    if [ "$yq_use_workaround" = "true" ]; then
        printf '%s\n' "$yq_workaround_dist_url"
    else
        printf '%s\n' "$yq_upstream_dist_url"
    fi
}

# url для загрузки геофайлов
refilter_url="https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geosite.dat"
refilterip_url="https://github.com/1andrevich/Re-filter-lists/releases/latest/download/geoip.dat"
v2fly_url="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
v2flyip_url="https://github.com/loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
zkeen_url="https://github.com/jameszeroX/zkeen-domains/releases/latest/download/zkeen.dat"
zkeenip_url="https://github.com/jameszeroX/zkeen-ip/releases/latest/download/zkeenip.dat"
geoipv4_url="https://github.com/jameszeroX/zkeen-ip/releases/latest/download/ru"
geoipv6_url="https://github.com/jameszeroX/zkeen-ip/releases/latest/download/ru6"

# -------------------------------------
# Журналы
# -------------------------------------
xray_access_log="$xray_log_dir/access.log"
xray_error_log="$xray_log_dir/error.log"

# -------------------------------------
# Создание директорий и файлов
# -------------------------------------
init_directories() {
    mkdir -p "$xray_log_dir" || { echo "Ошибка: Не удалось создать директорию $xray_log_dir"; exit 1; }
    mkdir -p "$initd_dir" || { echo "Ошибка: Не удалось создать директорию $initd_dir"; exit 1; }
    mkdir -p "$backups_dir" || { echo "Ошибка: Не удалось создать директорию $backups_dir"; exit 1; }
    mkdir -p "$install_dir" || { echo "Ошибка: Не удалось создать директорию $install_dir"; exit 1; }
    mkdir -p "$cron_dir" || { echo "Ошибка: Не удалось создать директорию $cron_dir"; exit 1; }
    touch "$xray_access_log" || { echo "Ошибка: Не удалось создать файл $xray_access_log"; exit 1; }
    touch "$xray_error_log" || { echo "Ошибка: Не удалось создать файл $xray_error_log"; exit 1; }
}

# Таймаут curl
[ -e "/tmp/toff" ] && curl_timeout="" || curl_timeout="-m 180"

# Дополнительные параметры curl
curl_extra=""


================================================
FILE: scripts/_xkeen/01_info/02_info_packages.sh
================================================
# Кэшируем список установленных пакетов один раз вместо opkg-форка на каждую проверку
_packages_cache=$(opkg list-installed 2>/dev/null)

# Функция для проверки наличия необходимых пакетов
info_packages() {
    package_name="$1"

    # Newline-prefix эмулирует якорь "^pkg ", чтобы libc не матчил libcurl
    case "
$_packages_cache" in
        *"
$package_name "*) package_status="installed" ;;
        *) package_status="not_installed" ;;
    esac
}

# Проверка наличия пакета "coreutils-uname"
info_packages "coreutils-uname"
info_packages_uname=$package_status

# Проверка наличия пакета "coreutils-nohup"
info_packages "coreutils-nohup"
info_packages_nohup=$package_status

# Проверка наличия пакета "curl"
info_packages "curl"
info_packages_curl=$package_status

# Проверка наличия пакета "jq"
info_packages "jq"
info_packages_jq=$package_status

# Проверка наличия пакета "libc"
info_packages "libc"
info_packages_libc=$package_status

# Проверка наличия пакета "libssp"
info_packages "libssp"
info_packages_libssp=$package_status

# Проверка наличия пакета "librt"
info_packages "librt"
info_packages_librt=$package_status

# Проверка наличия пакета "libpthread"
info_packages "libpthread"
info_packages_libpthread=$package_status

# Проверка наличия пакета "ca-bundle"
info_packages "ca-bundle"
info_packages_cabundle=$package_status

# Проверка наличия пакета "iptables"
info_packages "iptables"
info_packages_iptables=$package_status

# Проверка наличия пакета "ipset"
info_packages "ipset"
info_packages_ipset=$package_status


================================================
FILE: scripts/_xkeen/01_info/03_info_cpu.sh
================================================
# Функция для получения информации о процессоре
info_cpu() {
    if command -v opkg >/dev/null 2>&1; then
        opkg_arch=$(opkg print-architecture | awk '!/all/ {print $2; exit}' | cut -d- -f1)
        
        case "$opkg_arch" in
            *'aarch64'*) architecture='arm64-v8a' ;;
            *'mipsel'*) architecture='mips32le' ;;
            *'mips'*) architecture='mips32' ;;
            *) architecture="$opkg_arch" ;;
        esac
    fi

    # Получение информации о архитектуре из файла состояния (status_file)
    status_architecture=$(grep -m 1 '^Architecture:' "${status_file}" | awk '{print $2}')

    # Получение информации о необходимости softfloat банарников
    [ "$architecture" != "mips32le" ] && echo && return
    version="$(curl -kfsS "localhost:79/rci/show/version" 2>/dev/null)"

    case "$version" in
        *KN-1212*|*KN-2910*) softfloat="true" ;;
        *) echo; return ;;
    esac
}


================================================
FILE: scripts/_xkeen/01_info/04_info_mihomo.sh
================================================
# Функция для проверки установки Mihomo

info_mihomo() {
    if [ -f "$install_dir/mihomo" ] && [ -f "$install_dir/yq" ]; then
        mihomo_installed="installed"
    else
        mihomo_installed="not_installed"
    fi
}


================================================
FILE: scripts/_xkeen/01_info/04_info_xray.sh
================================================
# Функция для проверки установки Xray

info_xray() {
    if [ -f "$install_dir/xray" ]; then
        xray_installed="installed"
    else
        xray_installed="not_installed"
    fi
}


================================================
FILE: scripts/_xkeen/01_info/05_info_geofile.sh
================================================
# Функция для проверки наличия и записи информации о базах GeoSite
info_geosite() {
    update_refilter_geosite=false
    update_v2fly_geosite=false
    update_zkeen_geosite=false
    [ -f "$geo_dir/geosite_refilter.dat" ] && update_refilter_geosite=true
    [ -f "$geo_dir/geosite_v2fly.dat" ] && update_v2fly_geosite=true
    [ -f "$geo_dir/geosite_zkeen.dat" ] || [ -f "$geo_dir/zkeen.dat" ] && update_zkeen_geosite=true
}

# Функция для проверки наличия и записи информации о базах GeoIP
info_geoip() {
    update_refilter_geoip=false
    update_v2fly_geoip=false
    update_zkeenip_geoip=false
    [ -f "$geo_dir/geoip_refilter.dat" ] && update_refilter_geoip=true
    [ -f "$geo_dir/geoip_v2fly.dat" ] && update_v2fly_geoip=true
    [ -f "$geo_dir/geoip_zkeenip.dat" ] || [ -f "$geo_dir/zkeenip.dat" ] && update_zkeenip_geoip=true
}

================================================
FILE: scripts/_xkeen/01_info/06_info_console.sh
================================================
print_log_status() {
    local status_code=$1
    local success_msg=$2
    local error_msg=$3

    if [ "$status_code" -eq 0 ]; then
        echo -e "  ${green}Успешно${reset}: $success_msg"
    else
        echo -e "  ${red}Ошибка${reset}: $error_msg"
    fi
}

# Обратная связь в консоль
logs_cpu_info_console() {
    echo -e "  Набор инструкций процессора: ${yellow}$architecture${reset}"
    
    case "$architecture" in
        arm64-v8a|mips32le|mips32)
            echo -e "  Процессор ${green}поддерживается${reset} XKeen"
            ;;
        *)
            echo -e "  Процессор ${red}не поддерживается${reset} XKeen"
            ;;
    esac
}

logs_delete_configs_info_console() {
    local deleted_files=""
    
    if [ -d "$xray_conf_dir" ]; then
        deleted_files=$(find "$xray_conf_dir" -maxdepth 1 -name '*.json' -type f)
    fi

    if [ -z "$deleted_files" ]; then
        echo -e "  ${green}Успешно${reset}: Все конфигурационные файлы Xray удалены"
    else
        echo -e "  ${red}Ошибка${reset}: Не удалены следующие конфигурационные файлы:"
        for file in $deleted_files; do
            echo -e "    $file"
        done
    fi
}

logs_delete_geosite_info_console() {
    echo -e "  ${yellow}Проверка${reset} выполнения операции"
    # antifilter переименован в refilter в install/delete, имя verification отстало
    for file in "geosite_refilter.dat" "geosite_v2fly.dat" "geosite_zkeen.dat"; do
        [ ! -f "$geo_dir/$file" ]
        print_log_status $? "Файл $file отсутствует в директории '$geo_dir'" "Файл $file не удален"
    done
}

logs_delete_geoip_info_console() {
    echo -e "  ${yellow}Проверка${reset} выполнения операции"
    for file in "geoip_refilter.dat" "geoip_v2fly.dat" "geoip_zkeenip.dat"; do
        [ ! -f "$geo_dir/$file" ]
        print_log_status $? "Файл $file отсутствует в директории '$geo_dir'" "Файл $file не удален"
    done
}

logs_delete_geoipset_info_console() {
    echo -e "  ${yellow}Проверка${reset} выполнения операции"
    
    [ ! -f "$ru_exclude_ipv4" ]
    print_log_status $? "Файл ru_exclude_ipv4.lst отсутствует в директории '$ipset_cfg'" "Файл ru_exclude_ipv4.lst не удален"
    
    [ ! -f "$ru_exclude_ipv6" ]
    print_log_status $? "Файл ru_exclude_ipv6.lst отсутствует в директории '$ipset_cfg'" "Файл ru_exclude_ipv6.lst не удален"
}

# Проверки регистрации XKeen

logs_register_xkeen_status_info_console() {
    grep -q "Package: xkeen" "$status_file"
    print_log_status $? "Запись XKeen найдена в '$status_file'" "Запись XKeen не найдена в '$status_file'"
}

logs_register_xkeen_control_info_console() {
    [ -f "$register_dir/xkeen.control" ]
    print_log_status $? "Файл xkeen.control найден в директории '$register_dir/'" "Файл xkeen.control не найден в директории '$register_dir/'"
}

logs_register_xkeen_list_info_console() {
    [ -f "$register_dir/xkeen.list" ]
    print_log_status $? "Файл xkeen.list найден в директории '$register_dir/'" "Файл xkeen.list не найден в директории '$register_dir/'"
}

logs_register_xkeen_initd_info_console() {
    [ -f "$initd_file" ]
    print_log_status $? "init скрипт XKeen найден в директории '$initd_dir/'" "init скрипт XKeen не найден в директории '$initd_dir/'"
}

logs_delete_register_xkeen_info_console() {
    [ ! -f "$register_dir/xkeen.list" ]
    print_log_status $? "Файл xkeen.list не найден в директории '$register_dir/'" "Файл xkeen.list найден в директории '$register_dir/'"

    [ ! -f "$register_dir/xkeen.control" ]
    print_log_status $? "Файл xkeen.control не найден в директории '$register_dir/'" "Файл xkeen.control найден в директории '$register_dir/'"

    ! grep -q 'Package: xkeen' "$status_file"
    print_log_status $? "Регистрация пакета xkeen не обнаружена в '$status_file'" "Регистрация пакета xkeen обнаружена в '$status_file'"
}

# Проверки регистрации Xray

logs_register_xray_status_info_console() {
    grep -q "Package: xray_s" "$status_file"
    print_log_status $? "Запись Xray найдена в '$status_file'" "Запись Xray не найдена в '$status_file'"
}

logs_register_xray_control_info_console() {
    [ -f "$register_dir/xray_s.control" ]
    print_log_status $? "Файл xray_s.control найден в директории '$register_dir/'" "Файл xray_s.control не найден в директории '$register_dir/'"
}

logs_register_xray_list_info_console() {
    [ -f "$register_dir/xray_s.list" ]
    print_log_status $? "Файл xray_s.list найден в директории '$register_dir/'" "Файл xray_s.list не найден в директории '$register_dir/'"
}

logs_delete_register_xray_info_console() {
    [ ! -f "$register_dir/xray_s.list" ]
    print_log_status $? "Файл xray_s.list не найден в директории '$register_dir/'" "Файл xray_s.list найден в директории '$register_dir/'"

    [ ! -f "$register_dir/xray_s.control" ]
    print_log_status $? "Файл xray_s.control не найден в директории '$register_dir/'" "Файл xray_s.control найден в директории '$register_dir/'"

    ! grep -q 'Package: xray_s' "$status_file"
    print_log_status $? "Регистрация пакета xray не обнаружена в '$status_file'" "Регистрация пакета xray обнаружена в '$status_file'"
}

# Проверки регистрации Mihomo

logs_register_mihomo_status_info_console() {
    grep -q "Package: mihomo" "$status_file"
    print_log_status $? "Запись mihomo найдена в '$status_file'" "Запись mihomo не найдена в '$status_file'"
}

logs_register_mihomo_control_info_console() {
    [ -f "$register_dir/mihomo_s.control" ]
    print_log_status $? "Файл mihomo_s.control найден в директории '$register_dir/'" "Файл mihomo_s.control не найден в директории '$register_dir/'"
}

logs_register_mihomo_list_info_console() {
    [ -f "$register_dir/mihomo_s.list" ]
    print_log_status $? "Файл mihomo_s.list найден в директории '$register_dir/'" "Файл mihomo_s.list не найден в директории '$register_dir/'"
}

logs_delete_register_mihomo_info_console() {
    [ ! -f "$register_dir/mihomo_s.list" ]
    print_log_status $? "Файл mihomo_s.list не найден в директории '$register_dir/'" "Файл mihomo_s.list найден в директории '$register_dir/'"

    [ ! -f "$register_dir/mihomo_s.control" ]
    print_log_status $? "Файл mihomo_s.control не найден в директории '$register_dir/'" "Файл mihomo_s.control найден в директории '$register_dir/'"

    ! grep -q 'Package: mihomo_s' "$status_file"
    print_log_status $? "Регистрация пакета mihomo не обнаружена в '$status_file'" "Регистрация пакета mihomo обнаружена в '$status_file'"
}

# Проверки регистрации YQ

logs_register_yq_status_info_console() {
    grep -q "Package: yq" "$status_file"
    print_log_status $? "Запись yq найдена в '$status_file'" "Запись yq не найдена в '$status_file'"
}

logs_register_yq_control_info_console() {
    [ -f "$register_dir/yq_s.control" ]
    print_log_status $? "Файл yq_s.control найден в директории '$register_dir/'" "Файл yq_s.control не найден в директории '$register_dir/'"
}

logs_register_yq_list_info_console() {
    [ -f "$register_dir/yq_s.list" ]
    print_log_status $? "Файл yq_s.list найден в директории '$register_dir/'" "Файл yq_s.list не найден в директории '$register_dir/'"
}

logs_delete_register_yq_info_console() {
    [ ! -f "$register_dir/yq_s.list" ]
    print_log_status $? "Файл yq_s.list не найден в директории '$register_dir/'" "Файл yq_s.list найден в директории '$register_dir/'"

    [ ! -f "$register_dir/yq_s.control" ]
    print_log_status $? "Файл yq_s.control не найден в директории '$register_dir/'" "Файл yq_s.control найден в директории '$register_dir/'"

    ! grep -q 'Package: yq_s' "$status_file"
    print_log_status $? "Регистрация пакета yq не обнаружена в '$status_file'" "Регистрация пакета yq обнаружена в '$status_file'"
}

# Остальные проверки

logs_delete_cron_geofile_info_console() {
    if [ -f "$cron_dir/$cron_file" ]; then
        ! grep -q "ug" "$cron_dir/$cron_file"
        print_log_status $? "Задача автоматического обновления GeoFile удалена из cron" "Задача автоматического обновления GeoFile не удалена из cron"
    fi
}

================================================
FILE: scripts/_xkeen/01_info/07_info_cron.sh
================================================
# Проверка наличия задач автоматического обновления в cron
info_cron() {
    # Получаем текущую crontab конфигурацию для пользователя root
    cron_output=$(crontab -l -u root 2>/dev/null)

    # Проверяем наличие задачи с ключевым словом "ug" в crontab
    if echo "$cron_output" | grep -q "ug"; then
        info_update_geofile_cron="installed"
    else
        info_update_geofile_cron="not_installed"
    fi
}


================================================
FILE: scripts/_xkeen/01_info/08_info_version/00_version_import.sh
================================================
# Импорт модулей проверки версий

# Модули проверки версий
. "$xinfo_dir/08_info_version/01_version_xkeen.sh"
. "$xinfo_dir/08_info_version/02_version_mihomo.sh"
. "$xinfo_dir/08_info_version/02_version_xray.sh"

================================================
FILE: scripts/_xkeen/01_info/08_info_version/01_version_xkeen.sh
================================================
# Функция для получения версии из xkeen API и сохранения ее в переменной
info_version_xkeen() {
    version=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s "$xkeen_api_url" | jq -r '.tag_name // .name // ""' 2>/dev/null)

    if [ -z "$version" ]; then
        echo
        printf "${red}Нет доступа${reset} к ${yellow}GitHub API${reset}, пробуем ${yellow}jsDelivr${reset}...\n"
        version=$(eval curl $curl_extra --connect-timeout 10 $curl_timeout -s "$xkeen_jsd_url" | jq -r '.versions | first' 2>/dev/null)

        if [ -z "$version" ]; then
            echo
            printf "  ${red}Нет доступа${reset} к ${yellow}jsDelivr${reset}\n"
            echo
            printf "${red}Ошибка${reset}: Не удалось получить версию ни с ${yellow}GitHub${reset}, ни с ${yellow}jsDelivr${reset}\n
  Проверьте соединение с интернетом или повторите позже\n
  Если ошибка сохраняется, воспользуйтесь возможностью OffLine установки:\n
  https://github.com/jameszeroX/XKeen/blob/main/OffLine_install.md\n"
            echo
            exit 1
        fi
    fi

    xkeen_github_version="${version}"
}

# Функция для сравнения версий XKeen
info_compare_xkeen() {
    if [ "$xkeen_current_version" = "$xkeen_github_version" ]; then
        info_compare_xkeen="actual"
    else
        info_compare_xkeen="update"
    fi
}

================================================
FILE: scripts/_xkeen/01_info/08_info_version/02_version_mihomo.sh
================================================
# Функции для получения информации о версиях Mihomo и Yq
info_version_mihomo() {
    if [ "$mihomo_installed" = "installed" ]; then
        mihomo_current_version=$("$install_dir/mihomo" -v 2>&1 | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | sed 's/^v//' | head -1)
        mihomo_current_version=${mihomo_current_version:-"unknown"}
    else
        mihomo_current_version="unknown"
    fi
}

info_version_yq() {
    if [ "$mihomo_installed" = "installed" ]; then
        yq_current_version=$("$install_dir/yq" -V 2>&1 | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | sed 's/^v//' | head -1)
        yq_current_version=${yq_current_version:-"unknown"}
    else
        yq_current_version="unknown"
    fi
}


================================================
FILE: scripts/_xkeen/01_info/08_info_version/02_version_xray.sh
================================================
# Функция для получения информации о версии Xray
info_version_xray() {

    # Проверяем, установлен ли Xray
    if [ "$xray_installed" = "installed" ]; then
        # Если Xray установлен, получаем текущую версию
        xray_current_version=$("$install_dir/xray" version 2>&1 | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | sed 's/^v//' | head -1)
        xray_current_version=${xray_current_version:-"unknown"}
    else
        xray_current_version="unknown"
    fi
}


================================================
FILE: scripts/_xkeen/02_install/00_install_import.sh
================================================
# Импорт модулей установки

# Модули установки
. "$xinstall_dir/01_install_packages.sh"
. "$xinstall_dir/02_install_xray.sh"
. "$xinstall_dir/02_install_mihomo.sh"
. "$xinstall_dir/03_install_xkeen.sh"
. "$xinstall_dir/04_install_geofile.sh"
. "$xinstall_dir/05_install_geoipset.sh"
. "$xinstall_dir/06_install_cron.sh"

. "$xinstall_dir/07_install_register/00_register_import.sh"
. "$xinstall_dir/08_install_configs/00_configs_import.sh"

================================================
FILE: scripts/_xkeen/02_install/01_install_packages.sh
================================================
# Установка необходимых пакетов
install_packages() {
    package_status="$1"
    package_name="$2"

    if [ "${package_status}" = "not_installed" ]; then
        opkg install "$package_name" &>/dev/null
    fi
}

install_packages "$info_packages_curl" "curl"
install_packages "$info_packages_jq" "jq"
install_packages "$info_packages_libc" "libc"
install_packages "$info_packages_libssp" "libssp"
install_packages "$info_packages_librt" "librt"
install_packages "$info_packages_iptables" "iptables"
install_packages "$info_packages_libpthread" "libpthread"
install_packages "$info_packages_ipset" "ipset"
install_packages "$info_packages_cabundle" "ca-bundle"
install_packages "$info_packages_uname" "coreutils-uname"
install_packages "$info_packages_nohup" "coreutils-nohup"

================================================
FILE: scripts/_xkeen/02_install/02_install_mihomo.sh
================================================
# Функция для установки Mihomo
install_mihomo() {
    echo -e "  ${yellow}Выполняется установка${reset} Mihomo. Пожалуйста, подождите..."

    # Определение переменных
    mihomo_archive="${mtmp_dir}/mihomo.gz"

    # Проверка наличия архива Mihomo
    if [ ! -f "${mihomo_archive}" ]; then
        echo -e "  ${red}Ошибка${reset}: Архив Mihomo не найден в '${mtmp_dir}'"
        return 1
    fi

    if [ -f "$install_dir/mihomo" ]; then
        mv "$install_dir/mihomo" "$install_dir/mihomo_bak"
    fi

    if ! gzip -d "${mihomo_archive}" || [ ! -f "${mtmp_dir}/mihomo" ]; then
        echo -e "  ${red}Ошибка${reset}: Не удалось распаковать архив или файл отсутствует"
        if [ -f "$install_dir/mihomo_bak" ]; then
            mv "$install_dir/mihomo_bak" "$install_dir/mihomo"
            echo -e "  ${yellow}Восстановлен${reset} предыдущий бинарник Mihomo"
        fi
        rm -f "${mtmp_dir}/mihomo.gz" "${mtmp_dir}/mihomo"
        return 1
    fi

    if ! mv "${mtmp_dir}/mihomo" "$install_dir/"; then
        echo -e "  ${red}Ошибка${reset}: Не удалось установить Mihomo"
        [ -f "$install_dir/mihomo_bak" ] && mv "$install_dir/mihomo_bak" "$install_dir/mihomo"
        return 1
    fi

    chmod +x "$install_dir/mihomo"
    echo -e "  Mihomo ${green}успешно установлен${reset}"

    return 0
}

================================================
FILE: scripts/_xkeen/02_install/02_install_xray.sh
================================================
# Функция для установки Xray
install_xray() {
    echo -e "  ${yellow}Выполняется установка${reset} Xray. Пожалуйста, подождите..."

    # Определение переменных
    xray_archive="${xtmp_dir}/xray.zip"

    # Проверка наличия архива Xray
    if [ ! -f "${xray_archive}" ]; then
        echo -e "  ${red}Ошибка${reset}: Архив Xray не найден в '${xtmp_dir}'"
        return 1
    fi

    if [ -f "$install_dir/xray" ]; then
        mv "$install_dir/xray" "$install_dir/xray_bak"
    fi

    # Распаковка архива Xray
    if [ -d "${xtmp_dir}/xray" ]; then
        rm -rf "${xtmp_dir}/xray"
    fi

    if ! unzip -q "${xray_archive}" -d "${xtmp_dir}/xray"; then
        echo -e "  ${red}Ошибка${reset}: Не удалось распаковать архив"
        [ -f "$install_dir/xray_bak" ] && mv "$install_dir/xray_bak" "$install_dir/xray"
        return 1
    fi

    bin_source="${xtmp_dir}/xray/xray"

    if [ "$softfloat" = "true" ]; then
        if [ -f "${xtmp_dir}/xray/xray_softfloat" ]; then
            bin_source="${xtmp_dir}/xray/xray_softfloat"
        fi
    fi

    if [ ! -f "$bin_source" ]; then
        echo -e "  ${red}Ошибка${reset}: Бинарный файл Xray не найден в архиве"
        if [ -f "$install_dir/xray_bak" ]; then
            mv "$install_dir/xray_bak" "$install_dir/xray"
            echo -e "  ${yellow}Восстановлен${reset} предыдущий бинарник Xray"
        fi
        rm -f "$xray_archive"
        rm -rf "${xtmp_dir}/xray"
        return 1
    fi

    mv "$bin_source" "$install_dir/xray"
    chmod +x "$install_dir/xray"
    echo -e "  Xray ${green}успешно установлен${reset}"

    rm -f "$xray_archive"
    rm -rf "${xtmp_dir}/xray"

    # Фикс для новых ядер xray
    if [ -d "$xray_conf_dir" ]; then
        for file in "$xray_conf_dir"/*.json; do
            [ -f "$file" ] || continue
            if grep -qE '"transport"\s*:' "$file"; then
                mv "$file" "${file}.obsolete"
            fi
        done
    fi

    return 0
}

================================================
FILE: scripts/_xkeen/02_install/03_install_xkeen.sh
================================================
# Функция для установки XKeen
install_xkeen() {
    xkeen_archive="$ktmp_dir/xkeen.tar.gz"

    # Проверка наличия архива XKeen
    if [ -f "$xkeen_archive" ]; then
        # Распаковка архива
        tar -xzf "$xkeen_archive" -C "$install_dir" xkeen _xkeen

        # Проверка наличия _xkeen в install_dir и его перемещение
        if [ -d "$install_dir/_xkeen" ]; then
            rm -rf "$install_dir/.xkeen"
            mv "$install_dir/_xkeen" "$install_dir/.xkeen"
        else
            echo -e "  ${red}Ошибка${reset}: _xkeen не была правильно перенесена"
        fi

        # Удаление архива
        rm "$xkeen_archive"
    fi
    [ -d "$log_dir/xkeen" ] && rm -rf "$log_dir/xkeen"
}

check_keen_mode() {
    [ "$(sysctl -n net.ipv4.ip_forward 2>/dev/null)" = "1" ] && return 0
    keen_mode="unsupported"
}


================================================
FILE: scripts/_xkeen/02_install/04_install_geofile.sh
================================================
# Функция для загрузки и обработки геофайлов
process_geo_file() {
    local url="$1"
    local filename="$2"
    local display_name="$3"
    local update_flag="$4"

    # Защита от path traversal
    if case "$filename" in */*|*\\*|..|.) true;; *) false;; esac; then
        printf "  ${red}Ошибка${reset}: Недопустимое имя файла %s (path traversal)\n" "$filename"
        return 1
    fi

    local min_size=24576  # 24 KB

    printf "  Загрузка %s...\n" "$display_name"

    if ! fetch_with_mirrors "$url" "$geo_dir/$filename" "$min_size"; then
        case "$_last_error" in
            size)
                printf "  ${red}Ошибка${reset}: загруженный файл слишком мал (%s bytes) или повреждён\n  Невозможно обновить. Оставляем старый файл\n\n" "$_last_size"
                ;;
            html_stub)
                printf "  ${red}Ошибка${reset}: получена HTML-страница вместо dat-файла\n  Невозможно обновить. Оставляем старый файл\n\n"
                ;;
            *)
                printf "  ${red}Ошибка${reset}: не удалось загрузить %s\n" "$display_name"
                ;;
        esac
        return 1
    fi

    if [ "$update_flag" = "true" ]; then
        printf "  %s ${green}успешно обновлён${reset}\n\n" "$display_name"
    else
        printf "  %s ${green}успешно установлен${reset}\n\n" "$display_name"
    fi

    return 0
}

# Функция для установки и обновления GeoSite
install_geosite() {
    mkdir -p "$geo_dir" || { echo "Ошибка: Не удалось создать директорию $geo_dir"; exit 1; }

    local zkeen_datfile=""
    if [ "$install_zkeen_geosite" = "true" ] || [ "$update_zkeen_geosite" = "true" ]; then
        zkeen_datfile="geosite_zkeen.dat"
        if [ -L "$geo_dir/geosite_zkeen.dat" ]; then
            zkeen_datfile="zkeen.dat"
        elif [ -L "$geo_dir/zkeen.dat" ]; then
            zkeen_datfile="geosite_zkeen.dat"
        elif [ -f "$geo_dir/zkeen.dat" ] && ! [ -f "$geo_dir/geosite_zkeen.dat" ]; then
            zkeen_datfile="zkeen.dat"
        fi
    fi

    # Параллельная загрузка независимых геофайлов
    local _pids=""
    if [ "$install_refilter_geosite" = "true" ] || [ "$update_refilter_geosite" = "true" ]; then
        process_geo_file "$refilter_url" "geosite_refilter.dat" \
            "GeoSite Re:filter" "$update_refilter_geosite" &
        _pids="$_pids $!"
    fi

    if [ "$install_v2fly_geosite" = "true" ] || [ "$update_v2fly_geosite" = "true" ]; then
        process_geo_file "$v2fly_url" "geosite_v2fly.dat" \
            "GeoSite V2Fly" "$update_v2fly_geosite" &
        _pids="$_pids $!"
    fi

    if [ -n "$zkeen_datfile" ]; then
        process_geo_file "$zkeen_url" "$zkeen_datfile" \
            "GeoSite ZKeen" "$update_zkeen_geosite" &
        _pids="$_pids $!"
    fi

    [ -n "$_pids" ] && wait $_pids

    # Симлинки zkeen после успешной загрузки
    if [ -n "$zkeen_datfile" ]; then
        if [ "$zkeen_datfile" = "geosite_zkeen.dat" ]; then
            rm -f "$geo_dir/zkeen.dat"
            ln -sf "$geo_dir/geosite_zkeen.dat" "$geo_dir/zkeen.dat"
        else
            rm -f "$geo_dir/geosite_zkeen.dat"
            ln -sf "$geo_dir/zkeen.dat" "$geo_dir/geosite_zkeen.dat"
        fi
    fi
}

# Функция для установки и обновления GeoIP
install_geoip() {
    mkdir -p "$geo_dir" || { echo "Ошибка: Не удалось создать директорию $geo_dir"; exit 1; }

    local zkeenip_datfile=""
    if [ "$install_zkeenip_geoip" = "true" ] || [ "$update_zkeenip_geoip" = "true" ]; then
        zkeenip_datfile="geoip_zkeenip.dat"
        if [ -L "$geo_dir/geoip_zkeenip.dat" ]; then
            zkeenip_datfile="zkeenip.dat"
        elif [ -L "$geo_dir/zkeenip.dat" ]; then
            zkeenip_datfile="geoip_zkeenip.dat"
        elif [ -f "$geo_dir/zkeenip.dat" ] && ! [ -f "$geo_dir/geoip_zkeenip.dat" ]; then
            zkeenip_datfile="zkeenip.dat"
        fi
    fi

    # Параллельная загрузка независимых геофайлов
    local _pids=""
    if [ "$install_refilter_geoip" = "true" ] || [ "$update_refilter_geoip" = "true" ]; then
        process_geo_file "$refilterip_url" "geoip_refilter.dat" \
            "GeoIP Re:filter" "$update_refilter_geoip" &
        _pids="$_pids $!"
    fi

    if [ "$install_v2fly_geoip" = "true" ] || [ "$update_v2fly_geoip" = "true" ]; then
        process_geo_file "$v2flyip_url" "geoip_v2fly.dat" \
            "GeoIP V2Fly" "$update_v2fly_geoip" &
        _pids="$_pids $!"
    fi

    if [ -n "$zkeenip_datfile" ]; then
        process_geo_file "$zkeenip_url" "$zkeenip_datfile" \
            "GeoIP ZKeenIP" "$update_zkeenip_geoip" &
        _pids="$_pids $!"
    fi

    [ -n "$_pids" ] && wait $_pids

    # Симлинки zkeenip после успешной загрузки
    if [ -n "$zkeenip_datfile" ]; then
        if [ "$zkeenip_datfile" = "geoip_zkeenip.dat" ]; then
            rm -f "$geo_dir/zkeenip.dat"
            ln -sf "$geo_dir/geoip_zkeenip.dat" "$geo_dir/zkeenip.dat"
        else
            rm -f "$geo_dir/geoip_zkeenip.dat"
            ln -sf "$geo_dir/zkeenip.dat" "$geo_dir/geoip_zkeenip.dat"
        fi
    fi
}

================================================
FILE: scripts/_xkeen/02_install/05_install_geoipset.sh
================================================
# Валидаторы для fetch_with_mirrors: проверяют размер + базовый синтаксис
# содержимого (catch HTML-stub и мусор от proxy-error-page).
_validate_geoipset_v4() {
    _validate_default "$1" "$2" || return 1
    if ! grep -q "^[0-9]" "$1"; then
        _last_error="content_v4"
        return 1
    fi
    return 0
}

_validate_geoipset_v6() {
    _validate_default "$1" "$2" || return 1
    if ! grep -q ":" "$1"; then
        _last_error="content_v6"
        return 1
    fi
    return 0
}

# Функция для установки и обновления GeoIPSET
install_geoipset_lst() {
    mkdir -p "$ipset_cfg" || { echo "Ошибка: Не удалось создать директорию $ipset_cfg"; exit 1; }

    url="$1"
    dest_file="$2"
    display_name="$3"
    ip_type="$4"

    printf "  Загрузка %s...\n" "$display_name"

    if [ "$ip_type" = "ipv4" ]; then
        _validator_name="_validate_geoipset_v4"
    else
        _validator_name="_validate_geoipset_v6"
    fi

    if ! fetch_with_mirrors "$url" "$dest_file" 0 "$_validator_name"; then
        case "$_last_error" in
            html_stub)
                printf "  ${red}Ошибка${reset}: получена HTML-страница вместо списка IP\n  Оставляем старый файл\n\n"
                ;;
            content_v4)
                printf "  ${red}Ошибка${reset}: %s не содержит корректных IPv4-адресов\n  Оставляем старый файл\n\n" "$display_name"
                ;;
            content_v6)
                printf "  ${red}Ошибка${reset}: %s не содержит корректных IPv6-адресов\n  Оставляем старый файл\n\n" "$display_name"
                ;;
            *)
                printf "  ${red}Ошибка${reset}: не удалось загрузить %s\n\n" "$display_name"
                ;;
        esac
        return 1
    fi

    [ "$action" = "init" ] && msg_geoipset="установлен" || msg_geoipset="обновлён"
    printf "  %s ${green}успешно $msg_geoipset${reset}\n\n" "$display_name"
    return 0
}

load_geoipset() {
    set="$1"
    file="$2"
    family="$3"
    tmp="${set}_tmp"

    # Заполняем tmp; основной набор подменяется только после успешного restore
    ipset create "$set" hash:net family "$family" -exist
    ipset create "$tmp" hash:net family "$family" -exist
    ipset flush "$tmp"

    if [ -f "$file" ] && awk '/^[0-9a-fA-F]/ {print "add '"$tmp"' "$1}' "$file" | ipset restore -exist; then
        ipset swap "$set" "$tmp"
    fi
    ipset destroy "$tmp"
}

install_geoipset() {
    action="$1"

    if [ "$action" = "init" ]; then
        # Без TTY (cron, ssh -T) read получает EOF, default-case крутит while true
        # бесконечно: процесс висит в R-state с CPU-spin. Дефолтим выбор на "1"
        # (установить), потому что xkeen -gips из cron это типичный
        # non-interactive caller, где пользователь явно ожидает установку.
        if [ ! -t 0 ]; then
            printf "  Не интерактивный режим (нет TTY): автоматическая установка GeoIPSET\n"
            bypass_cron_geoipset=false
        else
            while true; do
                printf "\n  Желаете исключить российские IP-адреса из проксирования?\n\n"
                printf "     1. Загрузить и установить в исключения IP-подсети России (${yellow}GeoIPSET${reset})\n"
                printf "     0. Пропустить\n\n"
                printf "  Ваш выбор: "
                read -r choice

                case "$choice" in
                    0)
                        printf "  Пропуск установки списков GeoIPSET\n\n"

                        if [ ! -f "$ru_exclude_ipv4" ] && [ ! -f "$ru_exclude_ipv6" ]; then
                            bypass_cron_geoipset=true
                        fi
                        return 0
                        ;;
                    1)
                        bypass_cron_geoipset=false
                        break
                        ;;
                    *)
                        printf "  Неверный ввод. Пожалуйста, введите 1 или 0.\n"
                        ;;
                esac
            done
        fi
    fi

    local do_v4=0 do_v6=0
    if ip -4 addr show 2>/dev/null | grep -q "inet " && command -v iptables >/dev/null 2>&1; then
        if [ "$action" = "init" ] || [ -f "$ru_exclude_ipv4" ]; then
            do_v4=1
        fi
    fi
    if ip -6 addr show 2>/dev/null | grep -q "inet6 fe80::" && command -v ip6tables >/dev/null 2>&1; then
        if [ "$action" = "init" ] || [ -f "$ru_exclude_ipv6" ]; then
            do_v6=1
        fi
    fi

    # Параллельная загрузка независимых списков
    local _pids=""
    [ "$do_v4" = "1" ] && { install_geoipset_lst "$geoipv4_url" "$ru_exclude_ipv4" "IPv4 (IPSet)" "ipv4" & _pids="$_pids $!"; }
    [ "$do_v6" = "1" ] && { install_geoipset_lst "$geoipv6_url" "$ru_exclude_ipv6" "IPv6 (IPSet)" "ipv6" & _pids="$_pids $!"; }
    [ -n "$_pids" ] && wait $_pids

    [ "$do_v4" = "1" ] && load_geoipset geo_exclude "$ru_exclude_ipv4" inet
    [ "$do_v6" = "1" ] && load_geoipset geo_exclude6 "$ru_exclude_ipv6" inet6
}

================================================
FILE: scripts/_xkeen/02_install/06_install_cron.sh
================================================
# Функция для установки задач Cron
install_cron() {
    cron_entry=

    # Добавление задачи Cron для обновления GeoFile
    if [ -n "$choice_geofile_cron_time" ]; then
        cron_entry="$choice_geofile_cron_time $install_dir/xkeen -ug"
    fi

    # Если есть записи для задач Cron, то сохраняем их
    if [ -n "$cron_entry" ] || [ -n "$choice_cancel_cron_select" ]; then
        cron_file_path="$cron_dir/$cron_file"

        touch "$cron_file_path"
        chmod +x "$cron_file_path"

        if [ -n "$cron_entry" ]; then
            grep -v "$install_dir/xkeen -ug" "$cron_file_path" > "$cron_file_path.tmp"
            mv "$cron_file_path.tmp" "$cron_file_path"
            printf "%s\n" "$cron_entry" >> "$cron_file_path"
        fi
        sed -i '/^$/d' "$cron_file_path"
    fi
}


================================================
FILE: scripts/_xkeen/02_install/07_install_register/00_register_common.sh
================================================
# Общие функции для регистрации пакетов в opkg

write_opkg_control() {
    package_name="$1"
    package_version="$2"
    package_depends="$3"
    package_source="$4"
    package_source_name="$5"
    package_maintainer="$6"
    package_description="$7"

    _installed_size=$(du -s "$install_dir" | cut -f1)
    _source_date_epoch=$(date +%s)

    {
        echo "Package: $package_name"
        echo "Version: $package_version"
        [ -n "$package_depends" ] && echo "Depends: $package_depends"
        echo "Source: $package_source"
        echo "SourceName: $package_source_name"
        echo "Section: net"
        echo "SourceDateEpoch: $_source_date_epoch"
        echo "Maintainer: $package_maintainer"
        echo "Architecture: $status_architecture"
        echo "Installed-Size: $_installed_size"
        echo "Description: $package_description"
    } > "$register_dir/$package_name.control"
}

write_opkg_status() {
    package_name="$1"
    package_version="$2"
    package_depends="$3"
    status_entry="$(mktemp)"

    {
        echo "Package: $package_name"
        echo "Version: $package_version"
        [ -n "$package_depends" ] && echo "Depends: $package_depends"
        echo "Status: install user installed"
        echo "Architecture: $status_architecture"
        echo "Installed-Time: $(date +%s)"
    } > "$status_entry"

    echo "" >> "$status_file"
    cat "$status_entry" >> "$status_file"
    echo "" >> "$status_file"
    rm -f "$status_entry"
    sed -i '/^$/{N;/^\n$/D}' "$status_file"
}


================================================
FILE: scripts/_xkeen/02_install/07_install_register/00_register_import.sh
================================================
# Импорт модулей регистраций
	
# Модули регистрации
. "$xinstall_dir/07_install_register/00_register_common.sh"
. "$xinstall_dir/07_install_register/01_register_xray.sh"
. "$xinstall_dir/07_install_register/01_register_mihomo.sh"
. "$xinstall_dir/07_install_register/02_register_xkeen.sh"
. "$xinstall_dir/07_install_register/03_register_cron.sh"


================================================
FILE: scripts/_xkeen/02_install/07_install_register/01_register_mihomo.sh
================================================
# Регистрация Mihomo

register_mihomo_list() {
    cd "$register_dir/" || exit
    touch mihomo_s.list
    echo "/opt/sbin/mihomo" >> mihomo_s.list
    echo "/opt/etc/mihomo/config.yaml" >> mihomo_s.list
    echo "/opt/etc/mihomo" >> mihomo_s.list
}

register_mihomo_control() {
    write_opkg_control \
        "mihomo_s" \
        "$mihomo_current_version" \
        "yq_s" \
        "MetaCubeX" \
        "mihomo_s" \
        "jameszero" \
        "A unified platform for anti-censorship."
}

register_mihomo_status() {
    write_opkg_status \
        "mihomo_s" \
        "$mihomo_current_version" \
        "yq_s"
}

register_yq_list() {
    cd "$register_dir/" || exit
    touch yq_s.list
    echo "/opt/sbin/yq" >> yq_s.list
}

register_yq_control() {
    write_opkg_control \
        "yq_s" \
        "$yq_current_version" \
        "" \
        "mikefarah" \
        "yq_s" \
        "jameszero" \
        "A lightweight and portable command-line YAML, JSON, INI and XML processor."
}

register_yq_status() {
    write_opkg_status \
        "yq_s" \
        "$yq_current_version" \
        ""
}

add_mihomo_config() {
    if [ -f $install_dir/mihomo ]; then
        if [ -f "$mihomo_conf_dir/config.yaml" ]; then
            return 0
        elif [ ! -d $mihomo_conf_dir ]; then
            mkdir $mihomo_conf_dir
        fi
            cat << EOF > "$mihomo_conf_dir/config.yaml"
tproxy-port: 1181
redir-port: 1182
# Руководство по конфигурации Mihomo - https://wiki.metacubex.one/ru/config/
EOF

        echo
        echo "  Добавлен шаблон конфигурационного файла Mihomo:"
        echo -e "  ${yellow}config.yaml${reset}"
        sleep 2
    fi
}


================================================
FILE: scripts/_xkeen/02_install/07_install_register/01_register_xray.sh
================================================
# Регистрация xray
register_xray_control() {
    write_opkg_control \
        "xray_s" \
        "$xray_current_version" \
        "libc, libssp, librt, libpthread, ca-bundle" \
        "XTLS Team" \
        "xray_s" \
        "Skrill / jameszero" \
        "A unified platform for anti-censorship."
}

register_xray_list() {
    cd "$register_dir/" || exit
    touch xray_s.list

    # Генерация списка файлов
    find /opt/etc/xray/dat -maxdepth 1 -name "*.dat" -type f | while read -r file; do
        echo "$file" >> xray_s.list
    done

    find /opt/etc/xray/configs -maxdepth 1 -name "*.json" -type f | while read -r file; do
        echo "$file" >> xray_s.list
    done

    find /opt/var/log/xray -maxdepth 1 -name "*.log" -type f | while read -r file; do
        echo "$file" >> xray_s.list
    done

    # Добавление дополнительных путей
    echo "/opt/var/log/xray" >> xray_s.list
    echo "/opt/etc/xray/configs" >> xray_s.list
    echo "/opt/etc/xray/dat" >> xray_s.list
    echo "/opt/etc/xray" >> xray_s.list
    echo "/opt/sbin/xray" >> xray_s.list
}

register_xray_status() {
    write_opkg_status \
        "xray_s" \
        "$xray_current_version" \
        "libc, libssp, librt, libpthread, ca-bundle"
}


================================================
FILE: scripts/_xkeen/02_install/07_install_register/02_register_xkeen.sh
================================================
# Регистрация XKeen

# Функция для создания файла xkeen.control
register_xkeen_control() {
    write_opkg_control \
        "xkeen" \
        "$xkeen_current_version" \
        "jq, curl, coreutils-uname, coreutils-nohup, iptables, ipset" \
        "Skrill" \
        "xkeen" \
        "Skrill / jameszero" \
        "The platform that makes Xray work."
}

register_xkeen_list() {
    cd "$register_dir/" || exit

    # Создание файла xkeen.list
    touch xkeen.list

    # Генерация списка файлов и директорий
    find "$xkeen_dir" -mindepth 1 | while read -r entry; do
        echo "$entry" >> xkeen.list
    done

    # Добавление дополнительных путей
    echo "$install_dir/xkeen" >> xkeen.list
    echo "$xkeen_dir" >> xkeen.list
    echo "$initd_file" >> xkeen.list
    echo "$log_dir/xkeen-detached.log" >> xkeen.list
}

register_xkeen_status() {
    write_opkg_status \
        "xkeen" \
        "$xkeen_current_version" \
        "jq, curl, coreutils-uname, coreutils-nohup, iptables, ipset"
}


fixed_register_packages() {
	awk 'BEGIN {RS=""; ORS="\n\n"} {gsub(/\n\n+/,"\n\n")}1' "$status_file" > tmp_status_file && mv tmp_status_file "$status_file"
}

register_xkeen_initd() {
    old_initd_file="${initd_dir}/S24xray"
    pre_initd_file="${initd_dir}/S99xkeen"
    old_start_file="${initd_dir}/S99xkeenstart"
    script_file="${xinstall_dir}/07_install_register/04_register_init.sh" 
    current_datetime=$(date "+%Y-%m-%d_%H-%M-%S")
    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"
    source_main_backup=""
    source_start_backup=""

    if [ -f "$initd_file" ]; then
        source_main_backup="${backups_dir}/${current_datetime}_$(basename "$initd_file")"
        mv "$initd_file" "$source_main_backup"
    elif [ -f "$pre_initd_file" ]; then
        source_main_backup="${backups_dir}/${current_datetime}_$(basename "$pre_initd_file")"
        mv "$pre_initd_file" "$source_main_backup"
    elif [ -f "$old_initd_file" ] || [ -f "$old_start_file" ]; then
        if [ -f "$old_initd_file" ]; then
            source_main_backup="${backups_dir}/${current_datetime}_$(basename "$old_initd_file")"
            mv "$old_initd_file" "$source_main_backup"
        fi
        if [ -f "$old_start_file" ]; then
            source_start_backup="${backups_dir}/${current_datetime}_$(basename "$old_start_file")"
            mv "$old_start_file" "$source_start_backup"
        fi
    fi

    cp "$script_file" "$initd_file" || exit 1

    if [ -n "$source_main_backup" ] || [ -n "$source_start_backup" ]; then
        autostart_val=""
        start_delay_val=""

        if [ -n "$source_start_backup" ] && [ -f "$source_start_backup" ]; then
            autostart_val=$(grep '^autostart=' "$source_start_backup" | head -n 1 | cut -d'=' -f2)
            start_delay_val=$(grep '^start_delay=' "$source_start_backup" | head -n 1 | cut -d'=' -f2)
        fi

        if [ -n "$source_main_backup" ] && [ -f "$source_main_backup" ]; then
            [ -z "$autostart_val" ] && autostart_val=$(grep '^start_auto=' "$source_main_backup" | head -n 1 | cut -d'=' -f2)
            [ -z "$start_delay_val" ] && start_delay_val=$(grep '^start_delay=' "$source_main_backup" | head -n 1 | cut -d'=' -f2)
        fi

        if [ -n "$autostart_val" ]; then
             sed -i "s|^start_auto=.*|start_auto=$autostart_val|" "$initd_file"
        fi
        if [ -n "$start_delay_val" ]; then
             sed -i "s|^start_delay=.*|start_delay=$start_delay_val|" "$initd_file"
        fi

        if [ -n "$source_main_backup" ] && [ -f "$source_main_backup" ]; then
            for var in $variables_to_extract; do
                value=$(grep -m1 "^${var}=" "$source_main_backup") || continue
                escaped_value=$(printf '%s\n' "$value" | sed 's:[&#/]:\\&:g')
                position=$(grep -n "^${var}=" "$initd_file" | head -n 1 | cut -d: -f1)
                [ -n "$position" ] && sed -i "${position}s#.*#${escaped_value}#" "$initd_file"
            done
        fi
    fi

    chmod +x "$initd_file"
    if choice_backup_xkeen; then
        rm -f "$source_main_backup" "$source_start_backup"
    fi
    # Пропущенный $ ломал очистку легаси S99xkeenstart при апгрейде с 1.x
    rm -f "$old_initd_file" "$old_start_file" "$pre_initd_file"
}

# Миграция скрипта
register_xray_initd() {
    register_xkeen_initd
}
register_autostart() {
    :
}

# Создание конфигурации XKeen
create_xkeen_cfg() {
    mkdir -p "$xkeen_cfg" || { echo "Ошибка: Не удалось создать директорию $xkeen_cfg"; exit 1; }
    if [ -f "/opt/etc/xkeen_exclude.lst" ] && [ ! -f "$file_ip_exclude" ]; then
        mv "/opt/etc/xkeen_exclude.lst" "$file_ip_exclude"
    elif [ ! -f "$file_ip_exclude" ]; then
        cat << EOF > "$file_ip_exclude"
#192.168.0.0/16
#2001:db8::/32

# Добавьте необходимые IP и подсети без комментария # для исключения их из проксирования
EOF
    fi

    if [ ! -f "$file_port_exclude" ]; then
        cat << EOF > "$file_port_exclude"
#

# Одновременно использовать порты проксирования и исключать порты нельзя
# Приоритет у портов проксирования
EOF
    fi

    if [ ! -f "$file_port_proxying" ]; then
        cat << EOF > "$file_port_proxying"
#80
#443
#596:599

# (Раскомментируйте/добавьте по образцу) единичные порты и диапазоны для проскирования
EOF
    fi
    if [ ! -f "$xkeen_config" ]; then
        cat << EOF > "$xkeen_config"
{
}
EOF
    fi
}


================================================
FILE: scripts/_xkeen/02_install/07_install_register/03_register_cron.sh
================================================
# Функция для регистрации инициализационного скрипта cron
register_cron_initd() {
    # Проверка наличия пакета cron
    opkg list-installed 2>/dev/null | grep -q "^cron " && return

    # Определение переменных
    s05crond_filename="${current_datetime}_S05crond"
    required_script_version="0.6"

    # Получение текущей версии скрипта
    if [ -e "${initd_cron}" ]; then
        script_version=$(grep 'version=' "${initd_cron}" | grep -o '[0-9.]\+')
    fi

    # Содержимое скрипта
    script_content='#!/bin/sh

# Информация о службе: Запуск / Остановка Cron
# version="0.6"

green="\\033[32m"
red="\\033[31m"
yellow="\\033[33m"
reset="\\033[0m" 

cron_initd="/opt/sbin/crond"

# Функция для проверки статуса cron
cron_status() {
    if pidof crond > /dev/null; then
        return 0 # Процесс существует и работает
    else
        return 1 # Процесс не существует
    fi
}

# Функция для запуска cron
start() {
    if cron_status; then
        printf "  Cron ${yellow}уже запущен${reset}\\n"
    else
        $cron_initd -L /dev/null
        printf "  Cron ${green}запущен${reset}\\n"
    fi
}

# Функция для остановки cron
stop() {
    if cron_status; then
        killall crond
        printf "  Cron ${yellow}остановлен${reset}\\n"
    else
        printf "  Cron ${red}не запущен${reset}\\n"
    fi
}

# Функция для перезапуска cron
restart() {
    stop > /dev/null 2>&1
    sleep 1
    start > /dev/null 2>&1
    printf "  Cron ${green}перезапущен${reset}\\n"
}

# Обработка аргументов командной строки
case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    status)
        if cron_status; then
            printf "  Cron ${green}запущен${reset}\\n"
        else
            printf "  Cron ${red}не запущен${reset}\\n"
        fi
        ;;
    *)
        printf "  Команды: ${green}start${reset} | ${red}stop${reset} | ${yellow}restart${reset} | status\\n"
        ;;
esac

exit 0'
    
    # Создание или замена файла, если версия скрипта не соответствует требуемой версии 
    if [ "${script_version}" != "${required_script_version}" ]; then 
        echo -e "${script_content}" > "${initd_cron}" 
        chmod +x "${initd_cron}" 
    fi 
}

# Обновление cron задач
update_cron_geofile_task() {
    if [ -f "$cron_dir/$cron_file" ]; then
        tmp_file="$cron_dir/${cron_file}.tmp"
        cp "$cron_dir/$cron_file" "$tmp_file"
        
        if [ -z "$choice_cancel_cron_select" ]; then
            grep -v -e "ug" -e "ux" -e "uk" -e '^\s*$' "$tmp_file" > "$cron_dir/$cron_file"
        else
            grep -v -e "ugi" -e "ugs" -e "ux" -e "uk" -e '^\s*$' "$tmp_file" > "$cron_dir/$cron_file"
        fi
    fi
}

================================================
FILE: scripts/_xkeen/02_install/07_install_register/04_register_init.sh
================================================
#!/bin/sh

# Информация о службе: Запуск / Остановка XKeen
# Версия: 2.30

# Окружение
PATH="/opt/bin:/opt/sbin:/sbin:/bin:/usr/sbin:/usr/bin"

# Цвета
green="\033[92m"
red="\033[91m"
yellow="\033[93m"
light_blue="\033[96m"
reset="\033[0m"

# Имена
name_client="xray"
name_app="XKeen"
name_policy="xkeen"
name_profile="xkeen"
name_chain="xkeen"
name_ipset_deny_mac="xkeen_deny_mac"

# Директории
directory_os_modules="/lib/modules/$(uname -r)"
directory_user_modules="/opt/lib/modules"
directory_configs_app="/opt/etc/$name_client"
directory_xray_config="$directory_configs_app/configs"
directory_xray_asset="$directory_configs_app/dat"
directory_logs="/opt/var/log"
xkeen_cfg="/opt/etc/xkeen"
ipset_cfg="$xkeen_cfg/ipset"
install_dir="/opt/sbin"

# Файлы
file_netfilter_hook="/opt/etc/ndm/netfilter.d/proxy.sh"
file_schedule_hook="/opt/etc/ndm/schedule.d/00-xkeen-hotspot-sync.sh"
log_access="$directory_logs/$name_client/access.log"
log_error="$directory_logs/$name_client/error.log"
mihomo_config="$directory_configs_app/config.yaml"
file_port_proxying="$xkeen_cfg/port_proxying.lst"
file_port_exclude="$xkeen_cfg/port_exclude.lst"
file_ip_exclude="$xkeen_cfg/ip_exclude.lst"
xkeen_config="$xkeen_cfg/xkeen.json"
file_pid_fd="/var/run/xkeen_fd.pid"
ru_exclude_ipv4="$ipset_cfg/ru_exclude_ipv4.lst"
ru_exclude_ipv6="$ipset_cfg/ru_exclude_ipv6.lst"

# URL
url_server="localhost:79"
url_policy="rci/show/ip/policy"
url_keenetic_port="rci/ip/http"
url_redirect_port="rci/ip/static"
url_hotspot="rci/show/ip/hotspot"

# Настройки правил iptables
table_id="111"
table_mark="0x111"
table_redirect="nat"
table_tproxy="mangle"
comment_tag="xkeen_rule"
comment="-m comment --comment $comment_tag"
custom_mark=""

# DSCP-метки
dscp_exclude="62"
dscp_proxy="63"

ipv4_proxy="127.0.0.1"
ipv4_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"
ipv6_proxy="::1"
ipv6_exclude="::/128 ::1/128 64:ff9b::/96 2001::/32 2002::/16 fd00::/8 ff00::/8 fe80::/10"

# Перехват DNS в прокси
proxy_dns="off"

# Проксирование трафика Entware
proxy_router="off"

# Настройки запуска
start_attempts=10
start_auto="on"
start_delay=20

# Контроль файловых дескрипторов
check_fd="off"
arm64_fd=40000
other_fd=10000
delay_fd=60

# Поддержка IPv6
ipv6_support="on"

## Расширенные сообщения запуска
extended_msg="off"

## Резервное копирование XKeen при обновлении
backup="on"

## Клиенты XKeen под своими IP в журнале AdGuard Home
aghfix="off"

# Функции журналирования
log_info_router() { logger -p notice -t "$name_app" "$1"; }
log_warning_router() { logger -p warning -t "$name_app" "$1"; }
log_error_router() { logger -p error -t "$name_app" "$1"; }

log_info_terminal() { echo -e "\n${green}Информация${reset}: $1" >&2; }
log_warning_terminal() { echo -e "\n${yellow}Предупреждение${reset}: $1" >&2; }
log_error_terminal() { echo -e "\n${red}Ошибка${reset}: $1" >&2; exit 1; }

print_policy_info() {
    found="$1"
    has_custom="$2"
    ignored_custom="$3"

    ignore_line=""
    if [ "$ignored_custom" = "yes" ]; then
        ignore_line="
  Пользовательские политики из '${yellow}xkeen.json${reset}' будут проигнорированы"
    fi

    if [ "$extended_msg" != "on" ]; then
        if [ "$found" = "no" ]; then
            log_info_terminal "
  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}
  Прокси будет запущен для всего устройства
"
        fi
        return
    fi

    if [ "$found" = "yes" ]; then

        if [ "$has_custom" = "yes" ]; then
            custom_names=$(echo "$user_policies" | cut -d'|' -f1 | tr '\n' ',' | sed 's/,$//; s/,/, /g')
            policies="${name_policy}, ${custom_names}"

            detail_list=""
            if [ -n "$port_donor" ]; then
                detail_list="  - ${yellow}$name_policy${reset} на портах ${green}${port_donor}${reset}"
            elif [ -n "$port_exclude" ]; then
                detail_list="  - ${yellow}$name_policy${reset} на всех портах кроме ${green}${port_exclude}${reset}"
            else
                detail_list="  - ${yellow}$name_policy${reset} на всех портах"
            fi

            custom_details=$(echo "$user_policies" | while IFS='|' read -r p_name p_mark p_mode p_ports; do
                if [ "$p_mode" = "include" ]; then
                    echo "  - ${yellow}$p_name${reset} на портах ${green}${p_ports}${reset}"
                elif [ "$p_mode" = "exclude" ]; then
                    echo "  - ${yellow}$p_name${reset} на всех портах кроме ${green}${p_ports}${reset}"
                else
                    echo "  - ${yellow}$p_name${reset} на всех портах"
                fi
            done)

            log_info_terminal "
  Найдены политики '${yellow}${policies}${reset}'
  Прокси будет запущен для клиентов политик:
${detail_list}
${custom_details}
"
        else
            if [ -z "$port_donor" ] && [ -z "$port_exclude" ]; then
                log_info_terminal "
  Найдена политика '${yellow}$name_policy${reset}'
  Не определены целевые порты для XKeen
  Прокси будет запущен для клиентов политики '${yellow}$name_policy${reset}' на всех портах
"
            elif [ -n "$port_donor" ]; then
                log_info_terminal "
  Найдена политика '${yellow}$name_policy${reset}'
  Определены целевые порты для XKeen
  Прокси будет запущен для клиентов политики '${yellow}$name_policy${reset}'
  на портах ${green}${port_donor}${reset}
"
            else
                log_info_terminal "
  Найдена политика '${yellow}$name_policy${reset}'
  Определены порты исключения для XKeen
  Прокси будет запущен для клиентов политики '${yellow}$name_policy${reset}'
  на всех портах кроме ${green}${port_exclude}${reset}
"
            fi
        fi
    else
        if [ -n "$port_donor" ]; then
            log_info_terminal "
  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}
  Определены целевые порты для XKeen
  Прокси будет запущен для всех клиентов
  на портах ${green}${port_donor}${reset}
"
        elif [ -n "$port_exclude" ]; then
            log_info_terminal "
  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}
  Определены порты исключения для XKeen
  Прокси будет запущен для всех клиентов
  на всех портах кроме ${green}${port_exclude}${reset}
"
        else
            log_info_terminal "
  Политика '${yellow}$name_policy${reset}' не найдена в веб-интерфейсе роутера${ignore_line}
  Не определены целевые порты для XKeen
  Прокси будет запущен для всех клиентов на всех портах
"
        fi
    fi
}

utils="jq curl grep awk sed ipset"
[ "$name_client" = "mihomo" ] && utils="$utils yq"
for cmd in $utils; do
    command -v "$cmd" >/dev/null 2>&1 || log_error_terminal "Не найдена необходимая утилита: ${yellow}$cmd${reset}"
done

log_clean() { [ "$name_client" = "xray" ] && : > "$log_access" && : > "$log_error"; }

api_cache_init() {
    api_policy_json=$(curl -kfsS "${url_server}/${url_policy}" 2>/dev/null)
    api_port_json=$(curl -kfsS "${url_server}/${url_keenetic_port}" 2>/dev/null)
    api_static_json=$(curl -kfsS "${url_server}/${url_redirect_port}" 2>/dev/null)
}

refresh_port_cache() { api_port_json=$(curl -kfsS "${url_server}/${url_keenetic_port}" 2>/dev/null); }

json_get_ports() { [ -n "$api_port_json" ] && printf '%s' "$api_port_json" | jq -r '.port, (.ssl.port // empty)' 2>/dev/null; }

# Получение портов Keenetic
get_keenetic_port() {
    ports=""
    ports=$(json_get_ports)

    case " $ports " in
        *" 443 "*) return 1 ;;
    esac

    if [ -z "$ports" ]; then
        ndmc -c 'ip http port 8080' >/dev/null 2>&1
        ndmc -c 'ip http port 80' >/dev/null 2>&1
        ndmc -c 'system configuration save' >/dev/null 2>&1
        sleep 2
        refresh_port_cache
        ports=$(json_get_ports)
    fi

    [ -n "$ports" ] || return 1

    echo "$ports"
    return 0
}

wait_for_webui() {
    max_wait=10
    i=0

    while [ "$i" -lt "$max_wait" ]; do
        pidof nginx >/dev/null 2>&1 && return 0
        sleep 1
        i=$((i + 1))
    done

    return 1
}

apply_ipv6_state() {
    ipv6_disabled=
    ipv6_disabled=$(sysctl -n net.ipv6.conf.default.disable_ipv6 2>/dev/null || echo "0")

    [ "$ipv6_disabled" -eq 1 ] && return 0

    [ "$ipv6_support" != "off" ] && return 0

    ip -6 addr show 2>/dev/null | grep -q "inet6 fe80::" || return 0

    wait_for_webui || { log_error_router "Веб-интерфейс недоступен"; return 1; }

    sleep 5

    sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1

    for dir in /proc/sys/net/ipv6/conf/*; do
        [ -d "$dir" ] || continue
        iface="${dir##*/}"

        case "$iface" in
            all|ezcfg0|t2s*)
                continue
                ;;
            *)
                [ -f "$dir/disable_ipv6" ] && echo "1" > "$dir/disable_ipv6" 2>/dev/null
                ;;
        esac
    done

    sleep 2

    if [ "$(sysctl -n net.ipv6.conf.default.disable_ipv6 2>/dev/null)" -eq 1 ]; then
        log_info_router "Отключение IPv6 выполнено"
        return 0
    fi
}

get_ipver_support() {
    ip4_supported=$(ip -4 addr show 2>/dev/null | grep -q "inet " && echo true || echo false)
    ip6_supported=$(ip -6 addr show 2>/dev/null | grep -q "inet6 fe80::" && echo true || echo false)

    iptables_supported=$([ "$ip4_supported" = "true" ] && command -v iptables >/dev/null 2>&1 && echo true || echo false)
    ip6tables_supported=$([ "$ip6_supported" = "true" ] && command -v ip6tables >/dev/null 2>&1 && echo true || echo false)
}

strip_json_comments() {
    sed -e ':a; s:/\*[^*]*\*[^/]*\*/::g; ta' \
        -e 's/^[[:space:]]*\/\/.*$//' \
        -e 's/[[:space:]]\{1,\}\/\/.*$//' "$@"
}

# Функция валидации xkeen.json
validate_xkeen_json() {
    [ ! -f "$xkeen_config" ] && return 0
    if ! jq -e . "$xkeen_config" >/dev/null 2>&1; then
            log_error_terminal "
  Валидация JSON: файл '${yellow}xkeen.json${reset}' содержит синтаксические ошибки
  Запуск прокси невозможен
"
    fi

    if ! jq -e '.xkeen.policy[]? | .name' "$xkeen_config" >/dev/null 2>&1; then
        if jq -e '.xkeen' "$xkeen_config" >/dev/null 2>&1; then
            log_error_terminal "
  Файл '${yellow}xkeen.json${reset}' имеет неверную структуру
  Запуск прокси невозможен
"
        fi
    fi

    return 0
}

# Функция поиска резервных копий конфигурационных файлов Xray
check_xray_backups() {
    [ "$name_client" != "xray" ] && return 0

    # Ищем json-файлы с типичными признаками копий
    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" \))

    if [ -n "$bad_files" ]; then
        bad_list=$(printf '%s\n' "$bad_files" | awk -F/ '{print "  - " $NF}')
        
        log_error_terminal "
  В директории конфигурации Xray найдены резервные копии:
${light_blue}${bad_list}${reset}

  Измените расширение резервных копий, например, на ${yellow}.bak${reset}
  Либо переместите их в поддиректорию
  Запуск ${yellow}$name_client${reset} ${red}отменен${reset}
"
    fi
    return 0
}

# Функция проверки наличия метки 255
validate_routing_mark() {
    [ "$proxy_router" != "on" ] && return 0

    mark_valid="false"
    mark_msg=""
    bad_items=""
    has_items="false"
    all_marks_ok="true"

    if [ "$name_client" = "xray" ]; then
        mark_msg="mark"

        for file in "$directory_xray_config"/*.json; do
            [ -f "$file" ] || continue

            if strip_json_comments "$file" | jq -e '.outbounds != null' >/dev/null 2>&1; then
                has_items="true"

                current_bad=$(strip_json_comments "$file" | jq -r '
                    .outbounds[]? |
                    select(.protocol != "blackhole" and .protocol != "dns") |
                    select(.streamSettings.sockopt.mark != 255) |
                    (.tag // .protocol)
                ')

                if [ -n "$current_bad" ]; then
                     bad_items="${bad_items}${bad_items:+\n}$current_bad"
                    all_marks_ok="false"
                fi
            fi
        done

    elif [ "$name_client" = "mihomo" ]; then
        mark_msg="routing-mark"

        if [ -f "$mihomo_config" ]; then

            if yq -e '.["routing-mark"] == 255' "$mihomo_config" >/dev/null 2>&1; then
                mark_valid="true"
            elif yq -e '
                .proxy-providers[]? |
                select(.override."routing-mark" == 255)
            ' "$mihomo_config" >/dev/null 2>&1; then
                mark_valid="true"
            else

                if yq -e '.proxies != null' "$mihomo_config" >/dev/null 2>&1; then
                    has_items="true"
                    current_bad=$(yq -r '
                        .proxies[]? |
                        select(."routing-mark" != 255) |
                        .name
                    ' "$mihomo_config")

                    if [ -n "$current_bad" ]; then
                        bad_items="${bad_items}${bad_items:+\n}$current_bad"
                        all_marks_ok="false"
                    fi
                fi
            fi
        fi
    fi

    if [ "$mark_valid" != "true" ]; then
        if [ "$has_items" = "true" ] && [ "$all_marks_ok" = "true" ]; then
            mark_valid="true"
        fi
    fi

    if [ "$mark_valid" != "true" ]; then
        error_details=""

        if [ -n "$bad_items" ]; then
            bad_list=$(printf "%b\n" "$bad_items" | awk '!seen[$0]++ {print "  - " $0}')

            if [ "$name_client" = "xray" ]; then
                error_details="
  Подключения без метки:
${light_blue}${bad_list}${reset}"
                proxy_hint="  Добавьте маркировку во ВСЕ исходящие подключения (кроме blackhole и dns)"
            else
                error_details="
  Прокси без метки:
${light_blue}${bad_list}${reset}"
                proxy_hint="  Добавьте в config.yaml маркировку трафика глобально либо в каждое исходящее подключение"
            fi
        fi

        log_warning_terminal "
  Для проксирования трафика Entware требуется его маркировка
  В конфигурации ${yellow}$name_client${reset} параметр ${green}$mark_msg: 255${reset} прописан не везде$error_details

$proxy_hint

  Проксирование трафика Entware ${red}отключено${reset}
"
        proxy_router="off"
    fi

    return 0
}

load_user_ipset_family() {
    set_name="$1"
    family="$2"
    addr_regex="$3"
    tmp="${set_name}_tmp"

    # Заполняем tmp; основной набор подменяется только после успешного pipeline
    ipset create "$set_name" hash:net family "$family" -exist
    ipset create "$tmp" hash:net family "$family" -exist
    ipset flush "$tmp"

    if sed -e 's/\r$//' -e 's/#.*//' -e '/^[[:space:]]*$/d' "$file_ip_exclude" |
       grep -Eo "$addr_regex" |
       awk -v s="$tmp" '{print "add "s" "$1}' | ipset restore -exist; then
        ipset swap "$set_name" "$tmp"
    fi
    ipset destroy "$tmp"
}

# Функция загрузки пользовательских исключений в ipset
load_user_ipset() {
    [ ! -f "$file_ip_exclude" ] && return
    [ "$iptables_supported" = "true" ] && load_user_ipset_family user_exclude inet '([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?'
    [ "$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})?'
}

# Функция чтения пользовательских портов из файлов
read_ports_from_file() {
    file_ports="$1"
    [ -f "$file_ports" ] || return

    sed -e 's/\r$//' -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' "$file_ports"
}

# Функция обработки, валидации и нормализации списка портов
validate_and_clean_ports() {
    input_ports="$1"
    mandatory_ports="$2"
    [ -z "$input_ports" ] && [ -z "$mandatory_ports" ] && return 1

    echo "${mandatory_ports}${mandatory_ports:+,}${input_ports}" | tr ',' '\n' | awk '
        function is_valid(p) {
            return p ~ /^[0-9]+$/ && p > 0 && p <= 65535
        }
        {
            gsub(/[[:space:]]/, "", $0)
            gsub(/-/, ":", $0)
            if ($0 == "") next

            n = split($0, a, ":")

            if (n == 1) {
                if (is_valid(a[1])) {
                    print a[1]
                }
            }

            else if (n == 2) {
                if (is_valid(a[1]) && is_valid(a[2])) {
                    start = a[1]
                    end   = a[2]

                    if (start > end) {
                        tmp = start
                        start = end
                        end = tmp
                    }

                    if (start <= end) {
                        print start ":" end
                    }
                }
            }
        }
    ' | sort -n -u | tr '\n' ',' | sed 's/,$//'
}

# Функция обработки пользовательских портов
process_user_ports() {
    raw_donor=$(read_ports_from_file "$file_port_proxying")
    [ -n "$raw_donor" ] && port_donor=$(validate_and_clean_ports "$raw_donor" "80,443") || port_donor=""
    port_exclude=$(validate_and_clean_ports "$(read_ports_from_file "$file_port_exclude")")

    if [ -n "$port_donor" ] && [ -n "$port_exclude" ]; then
        log_warning_terminal "
  Заданы и порты проксирования, и порты исключения
  Прокси будет запущен на портах проксирования, порты исключения игнорируются
"
        port_exclude=""
    fi
}

# Функция нормализации сторонних политик
process_custom_mark() {
    [ -z "$custom_mark" ] && return

    clean_mark=""
    for mark in $(echo "$custom_mark" | tr ',' ' '); do
        val="${mark#0x}"
        echo "$val" | grep -Eq '^[0-9a-fA-F]+$' && clean_mark="$clean_mark 0x$val"
    done

    custom_mark="${clean_mark# }"
}

# Проверка статуса прокси-клиента
proxy_status() { pidof "$name_client" >/dev/null; }

# Поиск конфигураций DNS
check_dns_config() {
    [ "$proxy_dns" != "on" ] && echo "false" && return

    if [ "$name_client" = "xray" ]; then
        for file in "$directory_xray_config"/*.json; do
            [ -f "$file" ] || continue
            strip_json_comments "$file" | jq -e '.dns.servers? != null' >/dev/null 2>&1 && { echo "true"; return; }
        done
    elif [ "$name_client" = "mihomo" ]; then
        [ -f "$mihomo_config" ] && yq -e '.dns.enable == true' "$mihomo_config" >/dev/null 2>&1 && { echo "true"; return; }
    fi

    echo "false"
}
file_dns=$(check_dns_config)

# Кэш списка загруженных модулей; is_module_loaded читает его без форков
_loaded_modules=""
_refresh_modules_cache() { _loaded_modules=" $(lsmod 2>/dev/null | awk '{print $1}' | tr '\n' ' ') "; }

is_module_loaded() {
    case "$_loaded_modules" in
        *" $1 "*) return 0 ;;
        *) return 1 ;;
    esac
}

# Загрузка модулей
load_modules() {
    name="${1%.ko}"
    if ! is_module_loaded "$name"; then
        for dir in "$directory_os_modules" "$directory_user_modules"; do
            [ -f "$dir/$1" ] && insmod "$dir/$1" >/dev/null 2>&1 && return
        done
    fi
}

# Обработка модулей и портов
get_modules() {
    _refresh_modules_cache
    load_modules xt_comment.ko
    load_modules xt_TPROXY.ko
    load_modules xt_socket.ko
    load_modules xt_multiport.ko
    load_modules xt_dscp.ko
    _refresh_modules_cache  # подхватить только что insmod-нутые модули

    if ! is_module_loaded xt_comment; then
        log_error_router "Модуль xt_comment не загружен"
        log_error_terminal "
  Модуль '${light_blue}xt_comment${reset}' не загружен
  Невозможно запустить XKeen без него
  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'
"
    fi

    if [ "$mode_proxy" = "TProxy" ] || [ "$mode_proxy" = "Hybrid" ]; then
        for module in xt_TPROXY.ko xt_socket.ko; do
            if ! is_module_loaded "${module%.ko}"; then
                proxy_stop
                log_error_router "Модуль ${module} не загружен"
                log_error_terminal "
  Модуль '${light_blue}${module}${reset}' не загружен
  Невозможно запустить XKeen в режиме ${mode_proxy} без него
  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'
"
            fi
        done
    fi

    if [ -n "$port_donor" ] || [ -n "$port_exclude" ]; then
        if ! is_module_loaded xt_multiport; then
            log_warning_router "Модуль xt_multiport не загружен"
            log_warning_terminal "
  Модуль '${light_blue}xt_multiport${reset}' не загружен
  Невозможно использовать выбранные порты без него
  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'

  Прокси будет запущен на всех портах
"
            port_donor=""
            port_exclude=""
        fi
    fi

    if [ -n "$dscp_exclude" ] || [ -n "$dscp_proxy" ]; then
        if ! is_module_loaded xt_dscp; then
            log_warning_router "Модуль xt_dscp не загружен"
            log_warning_terminal "
  Модуль '${light_blue}xt_dscp${reset}' не загружен
  Работа с DSCP-метками невозможна
  Установите компонент роутера '${yellow}Модули ядра подсистемы Netfilter${reset}'
"
            dscp_exclude=""
            dscp_proxy=""
        fi
    fi
}

# Получение transparent inbound'ов Xray
_invalidate_inbounds_cache() { rm -f /tmp/xkeen-inbounds-cache; }

get_xray_transparent_inbounds() {
    cache_file="/tmp/xkeen-inbounds-cache"
    cache_valid=0
    if [ -f "$cache_file" ]; then
        newer=$(find "$directory_xray_config" -maxdepth 1 -name '*.json' -newer "$cache_file" 2>/dev/null | head -n 1)
        [ -z "$newer" ] && cache_valid=1
    fi
    if [ "$cache_valid" = "1" ]; then
        cat "$cache_file"
        return 0
    fi
    cache_tmp="${cache_file}.tmp.$$"
    {
        for file in "$directory_xray_config"/*.json; do
            [ -f "$file" ] || continue

            strip_json_comments "$file" |
            jq -r --arg file "$file" '
                .inbounds[]? |
                select(
                    (.protocol == "dokodemo-door" or .protocol == "tunnel") and
                    ((.settings.followRedirect? // false) == true)
                ) |
                (.streamSettings.sockopt.tproxy? // "") as $tproxy |
                select($tproxy == "" or $tproxy == "redirect" or $tproxy == "tproxy") |
                [
                    (if $tproxy == "tproxy" then "tproxy" else "redirect" end),
                    (.port // ""),
                    (.settings.network // ""),
                    (.tag // ""),
                    $file
                ] | @tsv
            ' 2>/dev/null
        done
    } > "$cache_tmp"
    mv "$cache_tmp" "$cache_file"
    cat "$cache_file"
}

get_xray_port_by_mode() {
    mode="$1"
    port=$(
        get_xray_transparent_inbounds |
        awk -F '\t' -v mode="$mode" '
            $1 == mode && $2 != "" {
                print $2
                exit
            }
        '
    )

    echo "$port"
}

get_xray_network_by_mode() {
    mode="$1"
    network=$(
        get_xray_transparent_inbounds |
        awk -F '\t' -v mode="$mode" '
            function add_networks(value, count, i, item) {
                gsub(/,/, " ", value)
                gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
                if (value == "") {
                    return
                }

                count = split(value, items, /[[:space:]]+/)
                for (i = 1; i <= count; i++) {
                    item = items[i]
                    if (item != "" && !seen[item]++) {
                        order[++order_count] = item
                    }
                }
            }

            $1 == mode {
                add_networks($3)
            }

            END {
                for (i = 1; i <= order_count; i++) {
                    printf "%s%s", order[i], (i < order_count ? " " : "")
                }
            }
        '
    )

    echo "$network"
}

# Получение порта для Redirect
get_port_redirect() {
    if [ "$name_client" = "xray" ]; then
        port=$(get_xray_port_by_mode "redirect")
        [ -n "$port" ] && echo "$port" && return 0
    elif [ "$name_client" = "mihomo" ]; then
        port=$(yq eval '.redir-port // ""' "$mihomo_config" 2>/dev/null)
        if [ -z "$port" ]; then
            port=$(yq eval '.listeners[] | select(.type == "redir") | .port // ""' "$mihomo_config" 2>/dev/null)
        fi
        [ -n "$port" ] && echo "$port" && return 0
    else
	return 1
    fi
}

# Получение порта для TProxy
get_port_tproxy() {
    if [ "$name_client" = "xray" ]; then
        port=$(get_xray_port_by_mode "tproxy")
        [ -n "$port" ] && echo "$port" && return 0
    elif [ "$name_client" = "mihomo" ]; then
        port=$(yq eval '.tproxy-port // ""' "$mihomo_config" 2>/dev/null)
        if [ -z "$port" ]; then
            port=$(yq eval '.listeners[] | select(.type == "tproxy") | .port // ""' "$mihomo_config" 2>/dev/null)
        fi
        [ -n "$port" ] && echo "$port" && return 0
    else
	return 1
    fi
}

# Получение сети для Redirect
get_network_redirect() {
    if [ "$name_client" = "xray" ]; then
        network=$(get_xray_network_by_mode "redirect")
        [ -n "$network" ] && echo "$network" && return 0
    elif [ "$name_client" = "mihomo" ]; then
        [ -n "$port_redirect" ] && echo "tcp" && return 0
        echo "" && return 0
    else
	return 1
    fi
}

# Получение сети для TProxy
get_network_tproxy() {
    if [ "$name_client" = "xray" ]; then
        network=$(get_xray_network_by_mode "tproxy")
        [ -n "$network" ] && echo "$network" && return 0
    elif [ "$name_client" = "mihomo" ]; then
        if [ -n "$port_redirect" ] && [ -n "$port_tproxy" ]; then
            echo "udp"
        elif [ -z "$port_redirect" ] && [ -n "$port_tproxy" ]; then
            echo "tcp udp"
        else
            echo ""
        fi
        return 0
    else
	return 1
    fi
}

# Получение портов исключения из статических пробросов
get_api_exclude_ports() {
    api_redir_result=""

    if [ -n "$api_static_json" ]; then
        api_redir_result=$(echo "$api_static_json" | jq -r '
          [
            .[] | 
            select(.disable != true) | 
            if has("end-port") then 
              "\(.port):\(.["end-port"])" 
            else 
              .port 
            end |
            select(. != "80" and . != "443")
          ] | 
          sort | 
          join(",")')
    fi

    echo "$api_redir_result"
}


# Получение исключенных портов
get_port_exclude() {
    port_exclude_redirect=""
    port_exclude_result=""

    port_exclude_redirect=$(get_api_exclude_ports)

    if [ -n "$port_exclude" ]; then
        if [ -n "$port_exclude_redirect" ]; then
            port_exclude_result="$port_exclude,$port_exclude_redirect"
        else
            port_exclude_result="$port_exclude"
        fi
    else
        port_exclude_result="$port_exclude_redirect"
    fi

    port_exclude_result=$(printf '%s\n' "$port_exclude_result" | tr -dc '0-9,:' | tr -s ',' | sed 's/^,//; s/,$//')
    echo "$port_exclude_result"
}

# Получение исключений IPv4
get_exclude_ip4() {
    [ "$iptables_supported" != "true" ] && return

    # Получаем провайдерский IPv4
    ipv4_eth=$(ip -o route get 195.208.4.1 2>/dev/null | sed -n 's/.*src \([^ ]*\).*/\1/p' || \
               ip -o route get 77.88.8.8 2>/dev/null | sed -n 's/.*src \([^ ]*\).*/\1/p')
    [ -n "$ipv4_eth" ] && ipv4_eth="${ipv4_eth}/32"
    echo "${ipv4_eth} ${ipv4_exclude}" | tr ' ' '\n' | awk '!seen[$0]++' | tr '\n' ' ' | sed 's/^ //; s/ $//'
}

# Получение исключений IPv6
get_exclude_ip6() {
    [ "$ip6tables_supported" != "true" ] && return

    # Получаем провайдерский IPv6
    ipv6_eth=$(ip -o -6 route get 2a0c:a9c7:8::1 2>/dev/null | sed -n 's/.*src \([^ ]*\).*/\1/p' || \
               ip -o -6 route get 2a02:6b8::feed:0ff 2>/dev/null | sed -n 's/.*src \([^ ]*\).*/\1/p')
    [ -n "$ipv6_eth" ] && ipv6_eth="${ipv6_eth}/128"
    echo "${ipv6_eth} ${ipv6_exclude}" | tr ' ' '\n' | awk '!seen[$0]++' | tr '\n' ' ' | sed 's/^ //; s/ $//'
}

# Получение метки политики
get_policy_mark() {
    if [ -n "$api_policy_json" ]; then
        policy_mark=$(echo "$api_policy_json" | jq -r --arg pname "$name_policy" '.[] | select(.description | ascii_downcase == ($pname | ascii_downcase)) | .mark' 2>/dev/null)
    fi

    if [ -n "$policy_mark" ]; then
        echo "0x${policy_mark}"
    else
        echo ""
    fi
}

# Атомарная синхронизация ipset xkeen_deny_mac с текущим состоянием hotspot API.
# Идемпотентна: создаёт основной набор при первом вызове, в дальнейшем
# наполняет tmp-набор и делает ipset swap. Вызывается на старте XKeen и
# на каждой netfilter.d/schedule.d-инвокации — это даёт динамику без
# `xkeen -restart` при работе Keenetic-расписаний (родительский контроль).
sync_deny_mac_ipset() {
    command -v ipset >/dev/null 2>&1 || return 0
    ipset create "$name_ipset_deny_mac" hash:mac -exist 2>/dev/null || return 0
    _xkeen_deny_tmp="${name_ipset_deny_mac}_tmp"
    ipset create "$_xkeen_deny_tmp" hash:mac -exist 2>/dev/null
    ipset flush "$_xkeen_deny_tmp" >/dev/null 2>&1
    _xkeen_hotspot_json=$(curl -kfsS "${url_server}/${url_hotspot}" 2>/dev/null)
    if [ -n "$_xkeen_hotspot_json" ]; then
        printf '%s' "$_xkeen_hotspot_json" | jq -r '
            ((.host // . // []) |
             (if type == "array" then .[] else . end)) |
            select((.access // "") == "deny" and (.mac // "") != "") |
            .mac
        ' 2>/dev/null | tr '[:lower:]' '[:upper:]' | while IFS= read -r _xkeen_mac; do
            [ -n "$_xkeen_mac" ] && ipset add "$_xkeen_deny_tmp" "$_xkeen_mac" -exist 2>/dev/null
        done
    fi
    ipset swap "$_xkeen_deny_tmp" "$name_ipset_deny_mac" 2>/dev/null
    ipset destroy "$_xkeen_deny_tmp" 2>/dev/null
    unset _xkeen_deny_tmp _xkeen_hotspot_json _xkeen_mac
}

# Получаем пользовательские политики
get_user_policies() {
    [ ! -f "$xkeen_config" ] && return
    jq -r '.xkeen.policy[]? | "\(.name)|\(.port // "")" ' "$xkeen_config" 2>/dev/null
}

# Проверка на конфликт имен политик
check_policy_name_conflict() {
    if [ -f "$xkeen_config" ]; then
        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)

        if [ -n "$conflict" ]; then
            log_error_router "Ошибка конфигурации: Имя политики в xkeen.json совпадает с зарезервированным"
            log_error_terminal "
  В файле '${yellow}xkeen.json${reset}' найдена политика с именем '${red}${conflict}${reset}'
  Это имя зарезервировано основной службой XKeen

  Переименуйте пользовательскую политику в json-файле
  Запуск ${yellow}$name_client${reset} ${red}отменен${reset}
"
        fi
    fi
}

# Получаем порты пользовательских политик
resolve_user_policies() {
    [ -f "$xkeen_config" ] && [ -n "$api_policy_json" ] || return

    api_exclude_ports=$(get_api_exclude_ports)

    # Получаем сопоставленные политики одним вызовом jq
    matched_policies=$(printf '%s' "$api_policy_json" | jq -r --argjson user_cfg "$(cat "$xkeen_config")" '
        ($user_cfg.xkeen.policy // []) as $up |
        .[] as $api |
        $up[] | 
        select(
            (.name | ascii_downcase) == 
            ($api.description | ascii_downcase)
        ) |
        "\(.name)|\($api.mark)|\(.port // "")"
    ' 2>/dev/null)

    [ -z "$matched_policies" ] && return

    # Обрабатываем каждую политику в одном цикле
    echo "$matched_policies" | while IFS='|' read -r pname mark pports; do
        if [ -z "$pports" ]; then
            # Порты не указаны -> режим "all" (все порты)
            if [ -n "$api_exclude_ports" ]; then
                echo "${pname}|${mark}|exclude|${api_exclude_ports}"
            else
                echo "${pname}|${mark}|all|"
            fi
        else

            case "$pports" in
                !*) mode="exclude"; ports="${pports#!}"
                    [ -n "$api_exclude_ports" ] && ports="${ports:+$ports,}$api_exclude_ports" ;;
                *) mode="include"; ports="$pports"
                    if [ "$file_dns" = "true" ] && [ "$proxy_dns" = "on" ]; then
                        case ",$ports," in
                            *,53,*) ;;
                            *) ports="53,$ports" ;;
                        esac
                    fi
                    ;;
            esac

            clean_ports=$(validate_and_clean_ports "$ports")
            [ -n "$clean_ports" ] && echo "${pname}|${mark}|${mode}|${clean_ports}"
        fi
    done
}

# Получение режима прокси-клиента
get_mode_proxy() {
    if [ -n "$port_redirect" ] && [ -n "$port_tproxy" ]; then
        mode_proxy="Hybrid"
    elif [ -n "$port_tproxy" ]; then
        mode_proxy="TProxy"
    elif [ -n "$port_redirect" ]; then
        mode_proxy="Redirect"
    else
        mode_proxy="Other"
    fi
    echo "$mode_proxy"
}

# Настройка брандмауэра
configure_firewall() {
    : > "$file_netfilter_hook"

    # Pre-evaluate dynamic variables
    val_exclude_ip6="$(get_exclude_ip6)"
    val_exclude_ip4="$(get_exclude_ip4)"

    cat > "$file_netfilter_hook" <<'EOL'
#!/bin/sh
# XKeen: Auto-generated file. DO NOT EDIT!
[ -f /tmp/xkeen_ready ] || exit 0
EOL

    # Securely inject variables into the script
    inject_var() {
        local name="$1"
        local val="$2"
        local safe_val
        safe_val="${val//\'/\'\\\'\'}"
        printf "%s='%s'\n" "$name" "$safe_val" >> "$file_netfilter_hook"
    }

    inject_var name_client "$name_client"
    inject_var name_profile "$name_profile"
    inject_var mode_proxy "$mode_proxy"
    inject_var network_redirect "$network_redirect"
    inject_var network_tproxy "$network_tproxy"
    inject_var networks "$networks"
    inject_var name_chain "$name_chain"
    inject_var port_redirect "$port_redirect"
    inject_var port_tproxy "$port_tproxy"
    inject_var port_donor "$port_donor"
    inject_var port_exclude "$port_exclude"
    inject_var policy_mark "$policy_mark"
    inject_var comment_tag "$comment_tag"
    inject_var comment "$comment"
    inject_var custom_mark "$custom_mark"
    inject_var dscp_exclude "$dscp_exclude"
    inject_var dscp_proxy "$dscp_proxy"
    inject_var user_policies "$user_policies"
    inject_var table_redirect "$table_redirect"
    inject_var table_tproxy "$table_tproxy"
    inject_var table_mark "$table_mark"
    inject_var table_id "$table_id"
    inject_var file_dns "$file_dns"
    inject_var proxy_dns "$proxy_dns"
    inject_var proxy_router "$proxy_router"
    inject_var directory_os_modules "$directory_os_modules"
    inject_var directory_user_modules "$directory_user_modules"
    inject_var directory_configs_app "$directory_configs_app"
    inject_var directory_xray_config "$directory_xray_config"
    inject_var directory_xray_asset "$directory_xray_asset"
    inject_var iptables_supported "$iptables_supported"
    inject_var ip6tables_supported "$ip6tables_supported"
    inject_var arm64_fd "$arm64_fd"
    inject_var other_fd "$other_fd"
    inject_var aghfix "$aghfix"
    
    inject_var ipv6_proxy "$ipv6_proxy"
    inject_var ipv4_proxy "$ipv4_proxy"
    inject_var val_exclude_ip6 "$val_exclude_ip6"
    inject_var val_exclude_ip4 "$val_exclude_ip4"
    inject_var name_ipset_deny_mac "$name_ipset_deny_mac"
    inject_var url_server "$url_server"
    inject_var url_hotspot "$url_hotspot"

    cat >> "$file_netfilter_hook" <<'EOL'

# Перезапуск скрипта
restart_script() {
    exec /bin/sh "$0" "$@"
}

if pidof "$name_client" >/dev/null; then

    # Динамическая синхронизация ipset с deny-MAC из hotspot API.
    # Закрывает обход built-in политики «Без доступа в интернет» при включенном
    # проксировании: PREROUTING на эти MAC делает RETURN до TPROXY, пакет идёт
    # в FORWARD, где штатно дропается NDM-цепочкой _NDM_HOTSPOT_FWD.
    # Хук перезапускается NDM при netfilter rewrite, schedule.d дёргает этот же
    # скрипт на start/stop расписаний — список MAC всегда актуален.
    _xkeen_sync_deny_mac_ipset() {
        command -v ipset >/dev/null 2>&1 || return 0
        ipset create "$name_ipset_deny_mac" hash:mac -exist 2>/dev/null || return 0
        _tmp="${name_ipset_deny_mac}_tmp"
        ipset create "$_tmp" hash:mac -exist 2>/dev/null
        ipset flush "$_tmp" >/dev/null 2>&1
        _hjson=$(curl -kfsS "${url_server}/${url_hotspot}" 2>/dev/null)
        if [ -n "$_hjson" ]; then
            printf '%s' "$_hjson" | jq -r '
                ((.host // . // []) |
                 (if type == "array" then .[] else . end)) |
                select((.access // "") == "deny" and (.mac // "") != "") |
                .mac
            ' 2>/dev/null | tr '[:lower:]' '[:upper:]' | while IFS= read -r _m; do
                [ -n "$_m" ] && ipset add "$_tmp" "$_m" -exist 2>/dev/null
            done
        fi
        ipset swap "$_tmp" "$name_ipset_deny_mac" 2>/dev/null
        ipset destroy "$_tmp" 2>/dev/null
    }
    _xkeen_sync_deny_mac_ipset

    # Аккумулируем правила в строки, применяем атомарно одним
    # iptables-restore --noflush на (family, table) в _xkeen_apply.
    # Сохраняем семантику старого ipt() для всех существующих helper'ов.
    _xkeen_v4_nat_rules=""
    _xkeen_v4_mangle_rules=""
    _xkeen_v6_nat_rules=""
    _xkeen_v6_mangle_rules=""

    ipt() {
        [ "$family" = "iptables" ] && [ "$iptables_supported" != "true" ] && return 0
        [ "$family" = "ip6tables" ] && [ "$ip6tables_supported" != "true" ] && return 0

        case "$1" in
            -A|-I|-D)
                _line=$*
                case "${family}_${table}" in
                    iptables_nat)     _xkeen_v4_nat_rules="${_xkeen_v4_nat_rules}${_line}
" ;;
                    iptables_mangle)  _xkeen_v4_mangle_rules="${_xkeen_v4_mangle_rules}${_line}
" ;;
                    ip6tables_nat)    _xkeen_v6_nat_rules="${_xkeen_v6_nat_rules}${_line}
" ;;
                    ip6tables_mangle) _xkeen_v6_mangle_rules="${_xkeen_v6_mangle_rules}${_line}
" ;;
                esac
                return 0
                ;;
            *)
                # Прочие операции (-F, -X) - в реальный iptables.
                if [ "$family" = "iptables" ]; then
                    iptables -w -t "$table" "$@"
                else
                    ip6tables -w -t "$table" "$@"
                fi
                return $?
                ;;
        esac
    }

    # Применяет аккумулированные правила одной таблицы атомарно через
    # iptables-restore --noflush. Custom chain $name_chain flush'ится
    # объявлением ":$name_chain -" перед добавлением новых правил.
    _xkeen_apply_table() {
        _family="$1"
        _table="$2"
        _rules_var="$3"

        eval "_rules=\${$_rules_var}"
        [ -z "$_rules" ] && return 0

        # Удаляем устаревшие xkeen-tagged правила из built-in/system chain'ов
        # (PREROUTING, OUTPUT, _NDM_HOTSPOT_DNSREDIR), правила из самой $name_chain
        # игнорируются - там ":chain -" в blob их сам flush'ит.
        save_cmd=""
        [ "$_family" = "iptables" ] && [ "$iptables_supported" = "true" ] && save_cmd="iptables-save"
        [ "$_family" = "ip6tables" ] && [ "$ip6tables_supported" = "true" ] && save_cmd="ip6tables-save"
        [ -z "$save_cmd" ] && { _deletes=""; return; }

        _deletes=$($save_cmd -t "$_table" 2>/dev/null | awk \
            -v tag="$comment_tag" \
            -v c1="$name_chain" \
            -v c2="${name_chain}_out" '
            index($0, tag) &&
            $1 == "-A" &&
            $2 != c1 &&
            $2 != c2 {
                sub(/^-A /, "-D ")
                print
            }
        ')

        {
            printf '*%s\n' "$_table"
            printf ':%s -\n' "$name_chain"
            [ "$proxy_router" = "on" ] && printf ':%s_out -\n' "$name_chain"
            [ -n "$_deletes" ] && printf '%s\n' "$_deletes"
            printf '%s' "$_rules"
            printf 'COMMIT\n'
        } | if [ "$_family" = "iptables" ]; then
            iptables-restore --noflush
        else
            ip6tables-restore --noflush
        fi
    }

    _xkeen_apply() {
        [ "$iptables_supported" = "true" ] && _xkeen_apply_table iptables nat _xkeen_v4_nat_rules || true
        [ "$iptables_supported" = "true" ] && _xkeen_apply_table iptables mangle _xkeen_v4_mangle_rules || true
        [ "$ip6tables_supported" = "true" ] && _xkeen_apply_table ip6tables nat _xkeen_v6_nat_rules || true
        [ "$ip6tables_supported" = "true" ] && _xkeen_apply_table ip6tables mangle _xkeen_v6_mangle_rules || true
    }

    # Добавление правил-исключений
    add_exclude_rules() {
        chain="$1"
        for exclude in $exclude_list; do
            if [ "$file_dns" = "true" ] && [ "$proxy_dns" = "on" ] && [ "$chain" != "${name_chain}_out" ]; then
                case "$exclude" in
                    10.0.0.0/8|172.16.0.0/12|192.168.0.0/16|fd00::/8|fe80::/10)
                    if [ "$table" = "mangle" ] && [ "$mode_proxy" = "Hybrid" ]; then
                        ipt -A "$chain" -d "$exclude" -p tcp --dport 53 $comment -j RETURN >/dev/null 2>&1
                        ipt -A "$chain" -d "$exclude" -p udp ! --dport 53 $comment -j RETURN >/dev/null 2>&1
                    elif [ "$table" = "nat" ] && [ "$mode_proxy" = "Hybrid" ]; then
                        ipt -A "$chain" -d "$exclude" -p tcp ! --dport 53 $comment -j RETURN >/dev/null 2>&1
                        ipt -A "$chain" -d "$exclude" -p udp --dport 53 $comment -j RETURN >/dev/null 2>&1
                    elif [ "$table" = "mangle" ] && [ "$mode_proxy" = "TProxy" ]; then
                        ipt -A "$chain" -d "$exclude" -p tcp ! --dport 53 $comment -j RETURN >/dev/null 2>&1
                        ipt -A "$chain" -d "$exclude" -p udp ! --dport 53 $comment -j RETURN >/dev/null 2>&1
                    fi
                    ;;
                esac
            else
                ipt -A "$chain" -d "$exclude" $comment -j RETURN >/dev/null 2>&1
            fi
        done
    }

    add_ipset_exclude() {
        base_set="$1"
        set_type="${2:-hash:net}"

        if [ "$family" = "ip6tables" ]; then
            set_name="${base_set}6"
            ipset_family="inet6"
        else
            set_name="$base_set"
            ipset_family="inet"
        fi

        ipset create "$set_name" "$set_type" family "$ipset_family" -exist || return

        ipt -I "$chain" 1 -m set --match-set "$set_name" dst $comment -j RETURN >/dev/null 2>&1
    }

    # Добавление правил iptables
    add_ipt_rule() {
        family="$1"
        table="$2"
        chain="$3"
        shift 3
        [ "$family" = "iptables" ] && [ "$iptables_supported" = "false" ] && return
        [ "$family" = "ip6tables" ] && [ "$ip6tables_supported" = "false" ] && return

        # Custom chain создаётся/flush'ится одной строкой ":$name_chain -" в blob,
        # поэтому ни -nL guard, ни -N не нужны - всегда заполняем body.
        add_exclude_rules "$chain"

        if [ "$table" = "$table_tproxy" ]; then
            if [ "$mode_proxy" = "Hybrid" ]; then
                set -- -p udp -m conntrack --ctstate ESTABLISHED,RELATED $comment -j CONNMARK --restore-mark
            else
                set -- -m conntrack --ctstate ESTABLISHED,RELATED $comment -j CONNMARK --restore-mark
            fi
            ipt -I "$chain" 1 "$@" >/dev/null 2>&1
        fi

        case "$mode_proxy" in
            Hybrid)
                if [ "$table" = "$table_redirect" ]; then
                    ipt -I "$chain" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1
                    add_ipset_exclude ext_exclude hash:ip
                    add_ipset_exclude geo_exclude hash:net
                    add_ipset_exclude user_exclude hash:net
                    ipt -A "$chain" -p tcp $comment -j REDIRECT --to-port "$port_redirect" >/dev/null 2>&1
                else
                    ipt -I "$chain" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1
                    add_ipset_exclude ext_exclude hash:ip
                    add_ipset_exclude geo_exclude hash:net
                    add_ipset_exclude user_exclude hash:net
                    ipt -A "$chain" -p udp -m socket --transparent $comment -j MARK --set-mark "$table_mark" >/dev/null 2>&1
                    ipt -A "$chain" -p udp -m mark ! --mark 0 $comment -j CONNMARK --save-mark >/dev/null 2>&1
                    ipt -A "$chain" -p udp $comment -j TPROXY --on-ip "$proxy_ip" --on-port "$port_tproxy" --tproxy-mark "$table_mark" >/dev/null 2>&1
                fi
                ;;
            TProxy)
                ipt -I "$chain" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1
                for net in $network_tproxy; do
                    add_ipset_exclude ext_exclude hash:ip
                    add_ipset_exclude geo_exclude hash:net
                    add_ipset_exclude user_exclude hash:net
                    ipt -A "$chain" -p "$net" -m socket --transparent $comment -j MARK --set-mark "$table_mark" >/dev/null 2>&1
                    ipt -A "$chain" -p "$net" -m mark ! --mark 0 $comment -j CONNMARK --save-mark >/dev/null 2>&1
                    ipt -A "$chain" -p "$net" $comment -j TPROXY --on-ip "$proxy_ip" --on-port "$port_tproxy" --tproxy-mark "$table_mark" >/dev/null 2>&1
                done
                ;;
            Redirect)
                ipt -I "$chain" 1 -m conntrack --ctstate DNAT $comment -j RETURN >/dev/null 2>&1
                add_ipset_exclude ext_exclude hash:ip
                add_ipset_exclude geo_exclude hash:net
                add_ipset_exclude user_exclude hash:net
                for net in $network_redirect; do
                    ipt -A "$chain" -p "$net" $comment -j REDIRECT --to-port "$port_redirect" >/dev/null 2>&1
                done
                ;;
            *) exit 0 ;;
        esac

        if [ -n "$dscp_exclude" ]; then
            for dscp in $dscp_exclude; do
                ipt -I "$chain" -m dscp --dscp "$dscp" $comment -j RETURN >/dev/null 2>&1
            done
        fi
    }

    # Настройка таблицы маршрутов
    configure_route() {
        ip_version="$1"

        # Определяем таблицу маршрутизации
        if [ -n "$policy_mark" ]; then
            policy_table=$(ip rule show | awk -v policy="$policy_mark" '$0 ~ policy && /lookup/ && !/blackhole/ {print $(NF); exit}')
        fi
        source_table="${policy_table:-main}"

        # Проверяем есть ли default маршрут
        check_default() {
            if [ "$ip_version" = "6" ] && ! ip -6 route show default 2>/dev/null | grep -q .; then
                return 0
            fi
            if [ "$source_table" = "main" ]; then
                ip -"$ip_version" route show default 2>/dev/null | grep -q '^default'
            else
                ip -"$ip_version" route show table all 2>/dev/null | grep -E "^[[:space:]]*default .* table $policy_table([[:space:]]|$)" | grep -vq 'unreachable' >/dev/null
            fi
        }

        attempts=0
        max_attempts=4
        until check_default; do
            attempts=$((attempts + 1))
            if [ "$attempts" -ge "$max_attempts" ]; then
                [ "$ip_version" = "4" ] && touch "/tmp/noinet"
                return 1
            fi
            sleep 1
        done
        [ "$ip_version" = "4" ] && rm -f "/tmp/noinet"

        ip -"$ip_version" rule del fwmark "$table_mark" lookup "$table_id" >/dev/null 2>&1 || true
        ip -"$ip_version" route flush table "$table_id" >/dev/null 2>&1 || true
        ip -"$ip_version" route add local default dev lo table "$table_id" >/dev/null 2>&1 || true
        ip -"$ip_version" rule add fwmark "$table_mark" lookup "$table_id" >/dev/null 2>&1 || true

        # Копируем маршруты
        ip -"$ip_version" route show table "$source_table" 2>/dev/null | while read -r route_line; do
            case "$route_line" in
                default*|unreachable*|blackhole*) continue ;;
                *) ip -"$ip_version" route add table "$table_id" $route_line >/dev/null 2>&1 || true ;;
            esac
        done
        return 0
    }

    # Создание множественных правил multiport
    add_multiport_rules() {
        family="$1"
        table="$2"
        net="$3"
        mark="$4"
        ports="$5"
        target="$6"

        [ -z "$ports" ] && return

        num_ports=$(echo "$ports" | tr ',' '\n' | wc -l)
        i=1
        while [ "$i" -le "$num_ports" ]; do
            end=$((i + 6))
            chunk=$(echo "$ports" | tr ',' '\n' | sed -n "${i},${end}p" | tr '\n' ',' | sed 's/,$//')
            [ -z "$chunk" ] && break
            if [ -n "$mark" ]; then
                set -- -m connmark --mark "$mark" -m conntrack ! --ctstate INVALID -p "$net" -m multiport --dports "$chunk" $comment -j "$target"
            else
                set -- -m conntrack ! --ctstate INVALID -p "$net" -m multiport --dports "$chunk" $comment -j "$target"
            fi
            ipt -A PREROUTING "$@" >/dev/null 2>&1
            i=$((i + 7))
        done
    }

    # Добавление цепочек PREROUTING
    add_prerouting() {
        family="$1"
        table="$2"

        # MAC-bypass для built-in «Без доступа в интернет»: RETURN из PREROUTING
        # до xkeen-jumps, пакет минует TPROXY/REDIRECT/MARK и попадает в FORWARD,
        # где NDM-цепочка _NDM_HOTSPOT_FWD его дропнет штатно. -m mac --mac-source
        # видит L2-MAC только для устройств в одном broadcast-домене с роутером
        # (LAN/Wi-Fi/guest-bridge); за L3-VLAN правило безвредно неактивно.
        ipt -I PREROUTING 1 -m set --match-set "$name_ipset_deny_mac" src $comment -j RETURN >/dev/null 2>&1

        for net in $networks; do
            if [ "$mode_proxy" = "Hybrid" ]; then
                [ "$table" = "nat"    ] && [ "$net" != "tcp" ] && continue
                [ "$table" = "mangle" ] && [ "$net" != "udp" ] && continue
            fi

            if [ "$mode_proxy" = "TProxy" ]; then
                proto_match=""
            else
                proto_match="-p $net"
            fi

            for dscp in $dscp_proxy; do
                set -- -m conntrack ! --ctstate INVALID $proto_match -m dscp --dscp "$dscp" $comment -j "$name_chain"
                ipt -A PREROUTING "$@" >/dev/null 2>&1
            done

            if [ "$proxy_router" = "on" ]; then
                set -- -i lo -m mark --mark "$table_mark" $proto_match $comment -j "$name_chain"
                ipt -A PREROUTING "$@" >/dev/null 2>&1
            fi

            # Пользовательские политики из xkeen.json
            # Heredoc вместо echo|while - while должен исполниться в parent shell,
            # чтобы аккумуляторы _xkeen_*_rules в ipt() модифицировались в нужном scope.
            while IFS='|' read -r pname pmark pmode pports; do
                [ -z "$pmark" ] && continue

                pmark=$(echo "$pmark" | tr -d ' \r\n')
                pmode=$(echo "$pmode" | tr -d ' \r\n')
                pports=$(echo "$pports" | tr -d ' \r\n')

                if [ "$pmode" = "all" ]; then
                    set -- -m connmark --mark 0x"$pmark" -m conntrack ! --ctstate INVALID $comment -j "$name_chain"
                    ipt -A PREROUTING "$@" >/dev/null 2>&1
                elif [ "$pmode" = "include" ]; then
                    add_multiport_rules "$family" "$table" "$net" "0x$pmark" "$pports" "$name_chain"
                elif [ "$pmode" = "exclude" ]; then
                    add_multiport_rules "$family" "$table" "$net" "0x$pmark" "$pports" "RETURN"
                    set -- -m connmark --mark 0x"$pmark" -m conntrack ! --ctstate INVALID -p "$net" $comment -j "$name_chain"
                    ipt -A PREROUTING "$@" >/dev/null 2>&1
                fi
            done <<USER_POLICIES_EOF
$user_policies
USER_POLICIES_EOF

            # Политика xkeen (стандартная)
            if [ -n "$policy_mark" ]; then
                # заданы порты проксирования
                if [ -n "$port_donor" ]; then
                    add_multiport_rules "$family" "$table" "$net" "$policy_mark" "$port_donor" "$name_chain"
                # заданы порты исключения
                elif [ -n "$port_exclude" ]; then
                    add_multiport_rules "$family" "$table" "$net" "$policy_mark" "$port_exclude" "RETURN"
                    set -- -m connmark --mark "$policy_mark" -m conntrack ! --ctstate INVALID -p "$net" $comment -j "$name_chain"
                    ipt -A PREROUTING "$@" >/dev/null 2>&1
                else
                    # Политика xkeen, когда порты не указаны (проксирование на всех портах)
                    set -- -m connmark --mark "$policy_mark" -m conntrack ! --ctstate INVALID $comment -j "$name_chain"
                    ipt -A PREROUTING "$@" >/dev/null 2>&1
                fi
            # НЕТ политики xkeen
            else
                # заданы порты проксирования
                if [ -n "$port_donor" ]; then
                    add_multiport_rules "$family" "$table" "$net" "" "$port_donor" "$name_chain"
                # заданы порты исключения
                elif [ -n "$port_exclude" ]; then
                    add_multiport_rules "$family" "$table" "$net" "" "$port_exclude" "RETURN"
                    set -- -m conntrack ! --ctstate INVALID -p "$net" $comment -j "$name_chain"
                    ipt -A PREROUTING "$@" >/dev/null 2>&1
                # Если нет ни xkeen, ни пользовательских политик -> перехватываем всё
                else
                    set -- -m conntrack ! --ctstate INVALID $comment -j "$name_chain"
                    ipt -A PREROUTING "$@" >/dev/null 2>&1
                fi
            fi
        done
    }

    # Добавление цепочек для проксирования трафика Entware
    add_output() {
        family="$1"
        table="$2"

        [ "$proxy_router" != "on" ] && return

        out_chain="${name_chain}_out"

        # ":${name_chain}_out -" в blob создаст/flush'ит chain атомарно,
        # body заполняется всегда.
        orig_chain="$chain"
        chain="$out_chain"

        ipt -A "$out_chain" -o lo $comment -j RETURN >/dev/null 2>&1
        ipt -A "$out_chain" -m mark --mark 255 $comment -j RETURN >/dev/null 2>&1

        add_exclude_rules "$out_chain"

        add_ipset_exclude ext_exclude hash:ip
        add_ipset_exclude geo_exclude hash:net
        add_ipset_exclude user_exclude hash:net

        chain="$orig_chain"

        for net in $networks; do
            if [ "$mode_proxy" = "Hybrid" ]; then
                [ "$table" = "nat"    ] && [ "$net" != "tcp" ] && continue
                [ "$table" = "mangle" ] && [ "$net" != "udp" ] && continue
            fi

            if [ "$mode_proxy" = "TProxy" ]; then
                proto_match=""
            else
                proto_match="-p $net"
            fi

            set -- -m conntrack ! --ctstate INVALID $proto_match $comment -j "$out_chain"
            ipt -A OUTPUT "$@" >/dev/null 2>&1

            if [ "$table" = "$table_redirect" ]; then
                set -- -p "$net" $comment -j REDIRECT --to-port "$port_redirect"
                ipt -A "$out_chain" "$@" >/dev/null 2>&1
            elif [ "$table" = "$table_tproxy" ]; then
                set -- -p "$net" $comment -j MARK --set-mark "$table_mark"
                ipt -A "$out_chain" "$@" >/dev/null 2>&1
            fi
        done
    }

    dns_redir() {
        family="$1"
        table="nat"

        [ "$aghfix" != "on" ] && return
        [ "$file_dns" = "true" ] && [ "$proxy_dns" = "on" ] && return

        all_marks=""
        [ -n "$policy_mark" ] && all_marks="$policy_mark"

        [ -n "$custom_mark" ] && all_marks="$custom_mark $all_marks"

        if [ -n "$user_policies" ]; then
            user_marks=$(echo "$user_policies" | awk -F'|' '{if ($2 != "") print "0x"$2}')
            all_marks="$all_marks $user_marks"
        fi

        for mark in $all_marks; do
            mark=$(echo "$mark" | tr -d ' \r\n')
            [ -z "$mark" ] && continue

            for proto in udp tcp; do
                set -- -p "$proto" -m mark --mark "$mark" -m pkttype --pkt-type unicast -m "$proto" --dport 53 $comment -j REDIRECT --to-ports 53
                ipt -I _NDM_HOTSPOT_DNSREDIR "$@" >/dev/null 2>&1
            done
        done
    }

    if [ -n "$port_donor" ] || [ -n "$port_exclude" ]; then
        [ "$file_dns" = "true" ] && [ "$proxy_dns" = "on" ] && [ -n "$port_donor" ] && port_donor="53,$port_donor"
    fi
    for family in iptables ip6tables; do

        [ "$family" = "ip6tables" ] && [ "$ip6tables_supported" != "true" ] && continue
        [ "$family" = "iptables" ] && [ "$iptables_supported" != "true" ] && continue

        if [ "$family" = "ip6tables" ]; then
            exclude_list="$val_exclude_ip6"
            proxy_ip="$ipv6_proxy"
            configure_route 6
        else
            exclude_list="$val_exclude_ip4"
            proxy_ip="$ipv4_proxy"
            configure_route 4
        fi
        if [ -n "$port_redirect" ] && [ -n "$port_tproxy" ]; then
            for table in "$table_tproxy" "$table_redirect"; do
                add_ipt_rule "$family" "$table" "$name_chain"
                add_prerouting "$family" "$table"
                add_output "$family" "$table"
            done
        elif [ -z "$port_redirect" ] && [ -n "$port_tproxy" ]; then
            table="$table_tproxy"
            add_ipt_rule "$family" "$table" "$name_chain"
            add_prerouting "$family" "$table"
            add_output "$family" "$table"
        elif [ -n "$port_redirect" ] && [ -z "$port_tproxy" ]; then
            table="$table_redirect"
            add_ipt_rule "$family" "$table" "$name_chain"
            add_prerouting "$family" "$table"
            add_output "$family" "$table"
        fi

        dns_redir "$family"
    done

    # Атомарно применяем все аккумулированные правила одним
    # iptables-restore --noflush per (family, table).
    _xkeen_apply
else
    [ -f "/tmp/xkeen_starting.lock" ] && exit 0
    touch "/tmp/xkeen_starting.lock"
    . "/opt/sbin/.xkeen/01_info/03_info_cpu.sh"
    status_file="/opt/lib/opkg/status"
    info_cpu

    fd_limit="$other_fd"
    [ "$architecture" = "arm64-v8a" ] && fd_limit="$arm64_fd"
    ulimit -SHn "$fd_limit"

    case "$name_client" in
        xray)
            export XRAY_LOCATION_CONFDIR="$directory_xray_config"
            export XRAY_LOCATION_ASSET="$directory_xray_asset"
            "$name_client" run >/dev/null 2>&1 &
        ;;
        mihomo)
            export CLASH_HOME_DIR="$directory_configs_app"
            "$name_client" >/dev/null 2>&1 &
        ;;
    esac
    _probe=0
    while [ "$_probe" -lt 60 ]; do
        pidof "$name_client" >/dev/null 2>&1 && break
        _probe=$((_probe + 1))
        usleep 100000
    done
    unset _probe
    rm -f "/tmp/xkeen_starting.lock"
    if pidof "$name_client" >/dev/null; then
        restart_script "$@"
    else
        exit 1
    fi
fi
EOL
    sed -i '1,2!{/^[[:space:]]*#/d; /^[[:space:]]*$/d}' "$file_netfilter_hook"
    chmod 700 "$file_netfilter_hook"

    # Schedule.d-хук: NDM вызывает scripts/schedule.d при start/stop расписаний
    # (родительский контроль). Хук дёргает netfilter.d/proxy.sh, который
    # ре-синхронизирует ipset deny-MAC из актуального hotspot API.
    mkdir -p "$(dirname "$file_schedule_hook")" 2>/dev/null
    cat > "$file_schedule_hook" <<'SCHEDULE_EOL'
#!/bin/sh
# XKeen: re-sync deny MAC ipset on schedule start/stop. Auto-generated. DO NOT EDIT!
[ "$1" = "start" ] || [ "$1" = "stop" ] || exit 0
[ -x /opt/etc/ndm/netfilter.d/proxy.sh ] && /opt/etc/ndm/netfilter.d/proxy.sh
SCHEDULE_EOL
    chmod 755 "$file_schedule_hook"

    sh "$file_netfilter_hook"
}

# Удаление правил iptables
clean_firewall() {
    [ -f "$file_netfilter_hook" ] && : > "$file_netfilter_hook"

    get_ipver_support

    for family in iptables ip6tables; do
        [ "$family" = "iptables" ] && [ "$iptables_supported" != "true" ] && continue
        [ "$family" = "ip6tables" ] && [ "$ip6tables_supported" != "true" ] && continue

        if "$family" -w -t nat -nL _NDM_HOTSPOT_DNSREDIR >/dev/null 2>&1; then
            "$family" -w -t nat -S _NDM_HOTSPOT_DNSREDIR | grep -E -- "$comment_tag" | sed 's/^-A /-D /' | while IFS= read -r rule; do
                [ -n "$rule" ] && "$family" -w -t nat $rule >/dev/null 2>&1
            done
        fi
    done

    clean_run() {
        family="$1"
        table="$2"
        name_chain="$3"

        for sys_chain in PREROUTING OUTPUT; do
            "$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 "$rule" ] && "$family" -w -t "$table" $rule >/dev/null 2>&1
            done
        done

        if "$family" -w -t "$table" -nL "$name_chain" >/dev/null 2>&1; then
            "$family" -w -t "$table" -F "$name_chain" >/dev/null 2>&1
            "$family" -w -t "$table" -X "$name_chain" >/dev/null 2>&1
        fi

        out_chain="${name_chain}_out"
        if "$family" -w -t "$table" -nL "$out_chain" >/dev/null 2>&1; then
            "$family" -w -t "$table" -F "$out_chain" >/dev/null 2>&1
            "$family" -w -t "$table" -X "$out_chain" >/dev/null 2>&1
        fi
    }

    for family in iptables ip6tables; do
        for chain in nat mangle; do
            clean_run "$family" "$chain" "$name_chain"
        done
    done

    if command -v ip >/dev/null 2>&1; then
        for family in 4 6; do
            while ip -"$family" rule del fwmark "$table_mark" lookup "$table_id" >/dev/null 2>&1; do :; done
            ip -"$family" route flush table "$table_id" >/dev/null 2>&1 || true
        done
    fi

    # Очистка и удаление списков ipset
    if command -v ipset >/dev/null 2>&1; then
        for set in geo_exclude geo_exclude6 user_exclude user_exclude6 "$name_ipset_deny_mac"; do
            ipset flush "$set" >/dev/null 2>&1
            ipset destroy "$set" >/dev/null 2>&1
        done
    fi

    # Schedule.d-hook идемпотентно перегенерируется в configure_firewall,
    # на остановке убираем чтобы NDM не дёргал мёртвый netfilter.d/proxy.sh.
    [ -f "$file_schedule_hook" ] && rm -f "$file_schedule_hook"
}

# Мониторинг файловых дескрипторов
monitor_fd() {
    while true; do
        client_pid=$(pidof "$name_client" | awk '{print $1}')
        if [ -n "$client_pid" ] && [ -d "/proc/$client_pid/fd" ]; then
            limit=$(awk '/Max open files/ {print $4}' "/proc/$client_pid/limits")
            set -- /proc/$client_pid/fd/*
            [ -e "$1" ] || set --
            current=$#
            if [ "$limit" -gt 0 ] && [ "$current" -gt $((limit * 90 / 100)) ]; then
                log_warning_router "$name_client открыл $current из $limit файловых дескрипторов, инициирован перезапуск"
                rm -f "$file_pid_fd"
                fd_out=true
                proxy_stop
                proxy_start "on"
                exit 0
            fi
        fi
        sleep "$delay_fd"
    done
}

load_ipset() {
    set="$1"
    file="$2"
    family="$3"
    tmp="${set}_tmp"

    # Заполняем tmp; основной набор подменяется только после успешного restore
    ipset create "$set" hash:net family "$family" -exist
    ipset create "$tmp" hash:net family "$family" -exist
    ipset flush "$tmp"

    if [ -f "$file" ] && sed -e 's/\r$//' -e 's/#.*//' -e '/^[[:space:]]*$/d' "$file" | awk '{print "add '"$tmp"' "$1}' | ipset restore -exist; then
        ipset swap "$set" "$tmp"
    fi
    ipset destroy "$tmp"
}

apply_fd_limit() {
    fd_limit="$other_fd"
    [ "$architecture" = "arm64-v8a" ] && fd_limit="$arm64_fd"
    ulimit -SHn "$fd_limit"
}

cleanup_fd_monitor() {
    [ -f "$file_pid_fd" ] || return 0
    kill "$(cat "$file_pid_fd")" 2>/dev/null
    rm -f "$file_pid_fd"
}

missing_files_template='
  '"${light_blue}"'Отсутствуют исполняемые файлы:'"${reset}"'
  '"${yellow}"'%b'"${reset}"'

  '"${green}"'Возможные причины:'"${reset}"'
  • XKeen установлен во внутреннюю память и на ней недостаточно места
  • У файла отсутствуют права на выполнение

  '"${green}"'Рекомендуемые действия:'"${reset}"'
  • Переустановите XKeen на внешний накопитель
  • Скопируйте недостающий файл вручную и сделайте исполняемым
'

check_binary() {
    file="$1"
    path="$install_dir/$file"

    if [ ! -f "$path" ] || [ ! -x "$path" ]; then
        return 1
    fi

    check_cmd="version"
    [ "$file" = "xray" ] && check_cmd="version"
    [ "$file" = "yq" ] && check_cmd="--version"
    [ "$file" = "mihomo" ] && check_cmd="-v"

    if ! "$file" $check_cmd >/dev/null 2>&1; then
        log_error_router "Бинарный файл $file аварийно остановлен"
        log_error_terminal "
  Бинарный файл ${yellow}$file${reset} аварийно остановлен
  ${red}Файл повреждён или несовместим с процессором${reset} вашего роутера
  Установите другую версию ${yellow}$file${reset}
"
    fi

    return 0
}

info_health_binary() {
    missing_files=""

    add_to_missing() {
        file_name="$1"
        prefix="  - " 
        
        if [ -z "$missing_files" ]; then
            missing_files="${prefix}${yellow}${file_name}${reset}"
        else
            missing_files="${missing_files}\n  ${prefix}${yellow}${file_name}${reset}"
        fi
    }

    case "$name_client" in
        xray)
            if ! check_binary xray; then add_to_missing "xray"; fi
            ;;
       mihomo)
            for file in mihomo yq; do
                if ! check_binary "$file"; then add_to_missing "$file"; fi
            done
            ;;
        esac

    if [ -n "$missing_files" ]; then
        log_error_terminal "$(printf "$missing_files_template" "$missing_files")"
    fi
}

# Очистка при аварийной остановке прокси-клиента
emergency_clear() {
    rm -f "/tmp/xkeen_ready"
    cleanup_fd_monitor
    clean_firewall
}

# Запуск прокси-клиента
proxy_start() {
    start_manual="$1"
    if [ "$start_manual" = "on" ] || [ "$start_auto" = "on" ]; then
        _invalidate_inbounds_cache
        apply_ipv6_state
        get_ipver_support
        info_health_binary
        validate_xkeen_json
        check_policy_name_conflict
        check_xray_backups
        validate_routing_mark
        log_clean
        api_cache_init
        sync_deny_mac_ipset
        process_user_ports
        process_custom_mark
        port_redirect=$(get_port_redirect)
        network_redirect=$(get_network_redirect)
        port_tproxy=$(get_port_tproxy)
        network_tproxy=$(get_network_tproxy)
        mode_proxy=$(get_mode_proxy)
        if [ "$mode_proxy" != "Other" ]; then
            policy_mark=$(get_policy_mark)

            if [ -n "$policy_mark" ]; then
                user_policies=$(resolve_user_policies)

                if [ -n "$user_policies" ]; then
                    print_policy_info "yes" "yes"
                else
                    print_policy_info "yes" "no"
                fi
            else
                raw_user_policies=$(get_user_policies)
                ignored_custom="no"

                if [ -n "$raw_user_policies" ]; then
                    ignored_custom="yes"
                fi

                print_policy_info "no" "no" "$ignored_custom"

                user_policies=""
            fi

            networks=$(printf '%s\n' $network_redirect $network_tproxy | tr ',' ' ' | tr -s ' ' '\n' | sort -u | tr '\n' ' ')
            networks=${networks% }

            if [ -n "$policy_mark" ] && [ -z "$port_donor" ]; then
                port_exclude=$(get_port_exclude)
            fi
            if ! proxy_status && { [ -n "$port_donor" ] || [ -n "$port_exclude" ] || [ "$mode_proxy" = "TProxy" ] || [ "$mode_proxy" = "Hybrid" ]; }; then
                get_modules
            fi
            if [ "$mode_proxy" = "TProxy" ]; then
                keenetic_ssl="$(get_keenetic_port)" || {
                    proxy_stop
                    log_error_router "Порт 443 занят сервисами Keenetic"
                    log_error_terminal "
  Необходимый для режима ${light_blue}TProxy${reset} ${red}443 порт занят${reset} сервисами Keenetic

  Освободите его на странице 'Пользователи и доступ' веб-интерфейса роутера
"
                }
            fi
        fi
        if proxy_status; then
            echo -e "  Прокси-клиент уже ${green}запущен${reset}"
            # Marker до configure_firewall: тот завершается `sh proxy.sh`,
            # gate в хуке читает /tmp/xkeen_ready.
            touch "/tmp/xkeen_ready"
            [ "$mode_proxy" != "Other" ] && configure_firewall
            if [ "$start_manual" = "on" ]; then
                log_error_terminal "Не удалось запустить ${yellow}$name_client${reset}, так как он уже запущен"
            else
                log_info_router "Прокси-клиент успешно запущен в режиме $mode_proxy"
                rm -f "/tmp/xkeen_coldstart.lock"
            fi
        else
            log_info_router "Инициирован запуск прокси-клиента"
            attempt=1
            . "/opt/sbin/.xkeen/01_info/03_info_cpu.sh"
            status_file="/opt/lib/opkg/status"
            info_cpu
            while [ "$attempt" -le "$start_attempts" ]; do
                case "$name_client" in
                    xray)
                        export XRAY_LOCATION_CONFDIR="$directory_xray_config"
                        export XRAY_LOCATION_ASSET="$directory_xray_asset"
                        find "$directory_xray_config" -maxdepth 1 -name '._*.json' -type f -delete
                        apply_fd_limit
                        if [ -n "$fd_out" ]; then
                            nohup "$name_client" run >/dev/null 2>&1 &
                            unset fd_out
                        else
                            "$name_client" run &
                        fi
                    ;;
                    mihomo)
                        export CLASH_HOME_DIR="$directory_configs_app"
                        apply_fd_limit
                        if [ -n "$fd_out" ]; then
                            nohup "$name_client" >/dev/null 2>&1 &
                            unset fd_out
                        else
                            "$name_client" &
                        fi
                        ;;
                    *) log_error_terminal "Неизвестный прокси-клиент: ${yellow}$name_client${reset}" ;;
                esac
                _probe_attempt=0
                while [ "$_probe_attempt" -lt 60 ]; do
                    proxy_status && break
                    _probe_attempt=$((_probe_attempt + 1))
                    usleep 50000
                done
                unset _probe_attempt
                if proxy_status; then
                    # См. alive-branch: marker до configure_firewall.
                    touch "/tmp/xkeen_ready"
                    [ "$mode_proxy" != "Other" ] && configure_firewall
                    _pids=""
                    [ "$iptables_supported" = "true" ] && [ -f "$ru_exclude_ipv4" ] && { load_ipset geo_exclude "$ru_exclude_ipv4" inet & _pids="$_pids $!"; }
                    [ "$ip6tables_supported" = "true" ] && [ -f "$ru_exclude_ipv6" ] && { load_ipset geo_exclude6 "$ru_exclude_ipv6" inet6 & _pids="$_pids $!"; }
                    load_user_ipset & _pids="$_pids $!"
                    [ -n "$_pids" ] && wait $_pids
                    unset _pids
                    echo -e "  Прокси-клиент ${green}запущен${reset} в режиме ${light_blue}${mode_proxy}${reset}"
                    (
                        # Даём ядру прокси время полностью инициализироваться
                        # Это защищает от ситуаций, когда xray/mihomo
                        # успевает создать PID, но затем аварийно завершается,
                        # например, из-за битой конфигурации
                        sleep 3

                        if ! proxy_status; then
                            echo
                            echo -e "  Прокси-клиент ${red}аварийно завершился${reset}"
                            echo -e "  ${green}Выполняется очистка${reset} правил прозрачного проксирования"
                            log_error_router "Прокси-клиент аварийно завершился после запуска"
                            emergency_clear
                            printf '\n~ # '
                        fi
                    ) &
                    if [ -n "$api_policy_json" ]; then
                        if echo "$api_policy_json" | jq --arg policy "$name_policy" -e 'any(.[]; .description | ascii_downcase == $policy)' > /dev/null; then
                            if [ -e "/tmp/noinet" ]; then
                                echo
                                echo -e "  У политики ${yellow}$name_policy${reset} ${red}нет доступа в интернет${reset}"
                                echo "  Проверьте, установлена ли галка на подключении к провайдеру"
                            fi
                        fi
                    fi
                    [ "$mode_proxy" = "Other" ] && echo -e "  Функция прозрачного прокси ${red}не активна${reset}. Направляйте соединения на ${yellow}${name_client}${reset} вручную"
                    log_info_router "Прокси-клиент успешно запущен в режиме $mode_proxy"
                    rm -f "/tmp/xkeen_coldstart.lock"
                    if [ "$check_fd" = "on" ]; then
                        cleanup_fd_monitor
                        monitor_fd &
                        echo $! > "$file_pid_fd"
                        log_info_router "Запущен контроль файловых дескрипторов $name_client"
                    fi
                    return 0
                fi
                attempt=$((attempt + 1))
            done
            echo -e "  ${red}Не удалось запустить${reset} прокси-клиент"
            log_error_terminal "Не удалось запустить прокси-клиент"
        fi
    else
        clean_firewall
    fi
}

# Активная проба готовности окружения вместо sleep $start_delay.
# Ждём ndmc, default route и insmod-ability xt_TPROXY (deps ndm
# подгружает асинхронно уже после ndmc-ready). $start_delay сохранён
# как safety cap (FAQ #12).
wait_for_ready() {
    _max=$(( ${start_delay:-60} * 2 ))
    _attempt=0
    _probe_ko="$directory_os_modules/xt_TPROXY.ko"
    while [ "$_attempt" -lt "$_max" ]; do
        if ndmc -c "show version" >/dev/null 2>&1 \
           && ip route show default 2>/dev/null | grep -q '^default'; then
            # .ko отсутствует (не TProxy/Hybrid), уже загружен, либо insmod удался
            if [ ! -f "$_probe_ko" ] \
               || grep -q '^xt_TPROXY ' /proc/modules 2>/dev/null \
               || insmod "$_probe_ko" >/dev/null 2>&1; then
                return 0
            fi
        fi
        usleep 500000
        _attempt=$((_attempt + 1))
    done
    return 0
}

# Остановка прокси-клиента
proxy_stop() {
    rm -f "/tmp/xkeen_ready"
    if ! proxy_status; then
        echo -e "  Прокси-клиент ${red}не запущен${reset}"
        cleanup_fd_monitor
    else
        [ -f "/tmp/xkeen_coldstart.lock" ] || log_info_router "Инициирована остановка прокси-клиента"
        cleanup_fd_monitor
        attempt=1
        while [ "$attempt" -le "$start_attempts" ]; do
            clean_firewall
            killall -q "$name_client" 2>/dev/null
            _stop_attempt=0
            while [ "$_stop_attempt" -lt 30 ]; do
                pidof "$name_client" >/dev/null 2>&1 || break
                _stop_attempt=$((_stop_attempt + 1))
                usleep 50000
            done
            unset _stop_attempt
            if pidof "$name_client" >/dev/null 2>&1; then
                killall -q -9 "$name_client" 2>/dev/null
                usleep 200000
            fi
            if ! proxy_status; then
                echo -e "  Прокси-клиент ${red}остановлен${reset}"
                [ -f "/tmp/xkeen_coldstart.lock" ] || log_info_router "Прокси-клиент успешно остановлен"
                rm -f "/tmp/xkeen_coldstart.lock"
                return 0
            fi
            attempt=$((attempt + 1))
        done
        echo -e "  Прокси-клиент ${red}не удалось остановить${reset}"
        log_error_terminal "Не удалось остановить прокси-клиент"
    fi
}

# Менеджер команд
case "$1" in
    start)
        ipset create ext_exclude hash:ip family inet -exist
        ipset create ext_exclude6 hash:ip family inet6 -exist
        if [ -z "$2" ]; then
            [ "$start_auto" != "on" ] && exit 0
            log_info_router "Подготовка к запуску прокси-клиента"
            nohup "$0" cold_start >/dev/null 2>&1 &
            touch "/tmp/xkeen_coldstart.lock"
            exit 0
        fi
        proxy_start "$2"
    ;;
    stop) proxy_stop ;;
    status)
        if proxy_status; then
            mode_proxy=""
            if [ -f "$file_netfilter_hook" ]; then
                mode_proxy=$(grep '^mode_proxy=' "$file_netfilter_hook" | awk -F"=" '{print $2}' | tr -d "'" 2>/dev/null)
            fi
            [ -z "$mode_proxy" ] && mode_proxy="Other"
            echo -e "  Прокси-клиент ${yellow}$name_client${reset} ${green}запущен${reset} в режиме ${light_blue}$mode_proxy${reset}"
        else
            echo -e "  Прокси-клиент ${red}не запущен${reset}"
        fi
        ;;
    restart) proxy_stop; proxy_start "$2" ;;
    cold_start)
        # Re-spawn в чистый S05xkeen: sh-функции (wait_for_ready) не
        # наследуются через nohup sh -c, поэтому пробу зовём отсюда.
        wait_for_ready
        proxy_start ""
        ;;
    *) echo -e "  Команды: ${green}start${reset} | ${red}stop${reset} | ${yellow}restart${reset} | status" ;;
esac

exit 0

================================================
FILE: scripts/_xkeen/02_install/08_install_configs/00_configs_import.sh
================================================
# Импорт модулей конфигураций
	
# Модуль конфигурации
. "$xinstall_dir/08_install_configs/01_configs_install.sh"


================================================
FILE: scripts/_xkeen/02_install/08_install_configs/01_configs_install.sh
================================================
# Функция для установки файлов конфигурации Xray
install_configs() {
    if [ ! -d "$xray_conf_dir" ]; then
        mkdir -p "$xray_conf_dir"
    fi

    if ls "$xray_conf_dir"/*.json >/dev/null 2>&1; then
        return 0
    fi

    xray_files="$xray_conf_smpl"/*.json
    for file in $xray_files; do
        filename=$(basename "$file")
        cp "$file" "$xray_conf_dir/"
        echo "  Добавлен шаблон конфигурационного файла Xray:"
        echo -e "  ${yellow}$filename${reset}"
        sleep 1
    done
}


================================================
FILE: scripts/_xkeen/02_install/08_install_configs/02_configs_dir/01_log.json
================================================
{
  "log": {
    "access": "/opt/var/log/xray/access.log",
    "error": "/opt/var/log/xray/error.log",
    "dnsLog": true,
    "loglevel": "none"
  }
}

================================================
FILE: scripts/_xkeen/02_install/08_install_configs/02_configs_dir/02_dns.json
================================================
{
// Пример настройки - https://jameszero.net/3398.htm
}

================================================
FILE: scripts/_xkeen/02_install/08_install_configs/02_configs_dir/03_inbounds.json
================================================
{
  "inbounds": [
    {
      "port": 1181,
      "protocol": "tunnel",
      "settings": {
        "network": "tcp",
        "followRedirect": true
      },
      "sniffing": {
        "enabled": true,
        "routeOnly": true,
        "destOverride": ["http","tls"]
      },
      "tag": "redirect"
    },
    {
      "port": 1181,
      "protocol": "tunnel",
      "settings": {
        "network": "udp",
        "followRedirect": true
      },
      "streamSettings": {
        "sockopt": {"tproxy": "tproxy"}
      },
      "sniffing": {
        "enabled": true,
        "routeOnly": true,
        "destOverride": ["quic"]
      },
      "tag": "tproxy"
    }
  ]
}

================================================
FILE: scripts/_xkeen/02_install/08_install_configs/02_configs_dir/04_outbounds.json
================================================
{
// Создайте файл по ссылке https://zxc-rv.github.io/XKeen-UI/Outbound_Generator/
}

================================================
FILE: scripts/_xkeen/02_install/08_install_configs/02_configs_dir/05_routing.json
================================================
{
// Создайте файл по ссылке https://xray-routing-generator.netlify.app
}

================================================
FILE: scripts/_xkeen/02_install/08_install_configs/02_configs_dir/06_policy.json
================================================
{
  "policy": {
    "levels": {
      "0": {
        "uplinkOnly": 0,
        "downlinkOnly": 0
      }
    }
  }
}

================================================
FILE: scripts/_xkeen/03_delete/00_delete_import.sh
================================================
# Импорт модулей удаления

# Модули удаления
. "$xdelete_dir/01_delete_geofile.sh"
. "$xdelete_dir/02_delete_geoipset.sh"
. "$xdelete_dir/03_delete_cron.sh"
. "$xdelete_dir/04_delete_configs.sh"
. "$xdelete_dir/05_delete_register.sh"
. "$xdelete_dir/06_delete_tmp.sh"


================================================
FILE: scripts/_xkeen/03_delete/01_delete_geofile.sh
================================================
# Функция для удаления выбранных файлов GeoSite
delete_geosite() {
    [ "$choice_delete_geosite_refilter_select" = "true" ] && rm -f "$geo_dir/geosite_refilter.dat"
    [ "$choice_delete_geosite_v2fly_select" = "true" ] && rm -f "$geo_dir/geosite_v2fly.dat"
    [ "$choice_delete_geosite_zkeen_select" = "true" ] && rm -f "$geo_dir/"geosite_zkeen.dat "$geo_dir/"zkeen.dat
}

# Функция для удаления всех файлов GeoSite
delete_geosite_key() {
    rm -f "$geo_dir/geosite_refilter.dat" \
          "$geo_dir/geosite_v2fly.dat" \
          "$geo_dir/geosite_zkeen.dat" \
          "$geo_dir/zkeen.dat"
}

# Функция для удаления выбранных файлов GeoIP
delete_geoip() {
    [ "$choice_delete_geoip_refilter_select" = "true" ] && rm -f "$geo_dir/geoip_refilter.dat"
    [ "$choice_delete_geoip_v2fly_select" = "true" ] && rm -f "$geo_dir/geoip_v2fly.dat"
    [ "$choice_delete_geoip_zkeenip_select" = "true" ] && rm -f "$geo_dir/geoip_zkeenip.dat" "$geo_dir/zkeenip.dat"
}

# Функция для удаления всех файлов GeoIP
delete_geoip_key() {
    rm -f "$geo_dir/geoip_refilter.dat" \
          "$geo_dir/geoip_v2fly.dat" \
          "$geo_dir/geoip_zkeenip.dat" \
          "$geo_dir/zkeenip.dat"
}

================================================
FILE: scripts/_xkeen/03_delete/02_delete_geoipset.sh
================================================
# Функция для удаления GeoIPSET
delete_geoipset() {
    while true; do
        printf "\n  Желаете удалить российские IP-адреса из исключений проксирования?\n\n"
        printf "     1. Да. Загруженные файлы подсетей будут удалены, а списки очищены\n"
        printf "     0. Нет. Отмена удаления\n\n"
        printf "  Ваш выбор: "
        read -r choice
        
        case "$choice" in
            0)
                echo
                printf "  Отмена удаления списков GeoIPSET.\n\n"
                return 0
                ;;
            1)
                echo
                break
                ;;
            *)
                printf "  Неверный ввод. Пожалуйста, введите 1 или 0.\n"
                ;;
        esac
    done

    ipset flush geo_exclude 2>/dev/null
    ipset flush geo_exclude6 2>/dev/null

    [ -f "$ru_exclude_ipv4" ] && rm -f "$ru_exclude_ipv4" 2>/dev/null
    [ -f "$ru_exclude_ipv6" ] && rm -f "$ru_exclude_ipv6" 2>/dev/null
    # [ -d "$ipset_cfg" ] && rm -rf "$ipset_cfg"

    printf "  Списки исключений GeoIPSET ${green}успешно удалены${reset}\n\n"
    return 0
}

delete_geoipset_key() {
    ipset flush geo_exclude 2>/dev/null
    ipset flush geo_exclude6 2>/dev/null

    [ -f "$ru_exclude_ipv4" ] && rm -f "$ru_exclude_ipv4" 2>/dev/null
    [ -f "$ru_exclude_ipv6" ] && rm -f "$ru_exclude_ipv6" 2>/dev/null
    # [ -d "$ipset_cfg" ] && rm -rf "$ipset_cfg"
}

================================================
FILE: scripts/_xkeen/03_delete/03_delete_cron.sh
================================================
# Функция для удаления cron задачи для GeoFile
delete_cron_geofile() {
    if [ -f "$cron_dir/$cron_file" ]; then
        tmp_file="$cron_dir/${cron_file}.tmp"
        cp "$cron_dir/$cron_file" "$tmp_file"
        grep -v "ug" "$tmp_file" | grep -v '^\s*$' > "$cron_dir/$cron_file"
    fi
}


================================================
FILE: scripts/_xkeen/03_delete/04_delete_configs.sh
================================================
# Удаление всех конфигураций Xray

delete_configs() {
    if [ -d "$xray_conf_dir" ]; then
        find "$xray_conf_dir" -maxdepth 1 -name '*.json' -type f -delete
    fi
}


================================================
FILE: scripts/_xkeen/03_delete/05_delete_register.sh
================================================
# Удаление регистрации Xray
delete_register_xray() {
    # Удаляем соответствующие записи из файла статуса opkg
    sed -i -e '/Package: xray_s/,/Installed-Time:/d' "/opt/lib/opkg/status"
    
    # Удаляем файлы регистрации, если они существуют
    if [ -f "$register_dir/xray_s.control" ] || [ -f "$register_dir/xray_s.list" ]; then
        rm -f "$register_dir/xray_s.control" "$register_dir/xray_s.list"
    fi
}

# Удаление регистрации Mihomo
delete_register_mihomo() {
    # Удаляем соответствующие записи из файла статуса opkg
    sed -i -e '/Package: mihomo_s/,/Installed-Time:/d' "/opt/lib/opkg/status"
    sed -i -e '/Package: yq_s/,/Installed-Time:/d' "/opt/lib/opkg/status"
    
    # Удаляем файлы регистрации, если они существуют
    if [ -f "$register_dir/mihomo_s.control" ] || [ -f "$register_dir/mihomo_s.list" ]; then
        rm -f "$register_dir/mihomo_s.control" "$register_dir/mihomo_s.list"
    fi
    if [ -f "$register_dir/yq_s.control" ] || [ -f "$register_dir/yq_s.list" ]; then
        rm -f "$register_dir/yq_s.control" "$register_dir/yq_s.list"
    fi
}

# Удаление регистрации XKeen
delete_register_xkeen() {
    # Удаляем соответствующие записи из файла статуса opkg
    sed -i -e '/Package: xkeen/,/Installed-Time:/d' "/opt/lib/opkg/status"
    
    # Удаляем файлы регистрации, если они существуют
    if [ -f "$register_dir/xkeen.control" ] || [ -f "$register_dir/xkeen.list" ]; then
        rm -f "$register_dir/xkeen.control" "$register_dir/xkeen.list"
    fi
}


================================================
FILE: scripts/_xkeen/03_delete/06_delete_tmp.sh
================================================
# Удаление временных файлов и директорий
delete_tmp() {
    [ -d "$ktmp_dir" ] && rm -rf "$ktmp_dir"
    [ -d "$xtmp_dir" ] && rm -rf "$xtmp_dir"
    [ -d "$mtmp_dir" ] && rm -rf "$mtmp_dir"
    [ -f "$cron_dir/root.tmp" ] && rm -f "$cron_dir/root.tmp"
    [ -f "$register_dir/new_entry.txt" ] && rm -f "$register_dir/new_entry.txt"
    [ -f "$install_dir/xray_bak" ] && rm -f "$install_dir/xray_bak"
    [ -f "$install_dir/mihomo_bak" ] && rm -f "$install_dir/mihomo_bak"
    [ -f "/tmp/xkrun" ] && rm -f "/tmp/xkrun"
    [ -f "/tmp/toff" ] && rm -f "/tmp/toff"

    if ! pidof xray >/dev/null && ! pidof mihomo >/dev/null ; then
        [ -f "$file_netfilter_hook" ] && rm "$file_netfilter_hook"
        [ -f "$file_schedule_hook" ] && rm "$file_schedule_hook"
        if command -v ipset >/dev/null 2>&1; then
            ipset flush "$name_ipset_deny_mac" >/dev/null 2>&1
            ipset destroy "$name_ipset_deny_mac" >/dev/null 2>&1
        fi
    fi

    echo
    echo -e "  Очистка временных файлов ${green}выполнена${reset}"
}

delete_all() {
    echo
    echo -e "  Удалить резервные копии и пользовательские настройки?"
    echo -e "  ${yellow}$backups_dir${reset}"
    echo -e "  ${yellow}$xkeen_cfg${reset}"
    echo
    echo "     1. Да, удалить"
    echo "     0. Нет, оставить"
    echo

    while true; do
        read -r -p " 
Download .txt
gitextract_qdk3i1xx/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── package-folder.yaml
│       ├── release.yaml
│       └── wiki-sync.yaml
├── .gitignore
├── 01_info_variable.sh
├── LICENSE
├── README.md
├── configuration.md
├── docs/
│   ├── README.md
│   ├── architecture.md
│   ├── build-and-release.md
│   ├── commands.md
│   ├── contributing.md
│   └── runtime-paths.md
├── forkinfo.md
├── install.sh
├── knownissues.md
├── scripts/
│   ├── _xkeen/
│   │   ├── 01_info/
│   │   │   ├── 00_info_import.sh
│   │   │   ├── 01_info_variable.sh
│   │   │   ├── 02_info_packages.sh
│   │   │   ├── 03_info_cpu.sh
│   │   │   ├── 04_info_mihomo.sh
│   │   │   ├── 04_info_xray.sh
│   │   │   ├── 05_info_geofile.sh
│   │   │   ├── 06_info_console.sh
│   │   │   ├── 07_info_cron.sh
│   │   │   └── 08_info_version/
│   │   │       ├── 00_version_import.sh
│   │   │       ├── 01_version_xkeen.sh
│   │   │       ├── 02_version_mihomo.sh
│   │   │       └── 02_version_xray.sh
│   │   ├── 02_install/
│   │   │   ├── 00_install_import.sh
│   │   │   ├── 01_install_packages.sh
│   │   │   ├── 02_install_mihomo.sh
│   │   │   ├── 02_install_xray.sh
│   │   │   ├── 03_install_xkeen.sh
│   │   │   ├── 04_install_geofile.sh
│   │   │   ├── 05_install_geoipset.sh
│   │   │   ├── 06_install_cron.sh
│   │   │   ├── 07_install_register/
│   │   │   │   ├── 00_register_common.sh
│   │   │   │   ├── 00_register_import.sh
│   │   │   │   ├── 01_register_mihomo.sh
│   │   │   │   ├── 01_register_xray.sh
│   │   │   │   ├── 02_register_xkeen.sh
│   │   │   │   ├── 03_register_cron.sh
│   │   │   │   └── 04_register_init.sh
│   │   │   └── 08_install_configs/
│   │   │       ├── 00_configs_import.sh
│   │   │       ├── 01_configs_install.sh
│   │   │       └── 02_configs_dir/
│   │   │           ├── 01_log.json
│   │   │           ├── 02_dns.json
│   │   │           ├── 03_inbounds.json
│   │   │           ├── 04_outbounds.json
│   │   │           ├── 05_routing.json
│   │   │           └── 06_policy.json
│   │   ├── 03_delete/
│   │   │   ├── 00_delete_import.sh
│   │   │   ├── 01_delete_geofile.sh
│   │   │   ├── 02_delete_geoipset.sh
│   │   │   ├── 03_delete_cron.sh
│   │   │   ├── 04_delete_configs.sh
│   │   │   ├── 05_delete_register.sh
│   │   │   └── 06_delete_tmp.sh
│   │   ├── 04_tools/
│   │   │   ├── 00_tools_import.sh
│   │   │   ├── 01_tools_ports.sh
│   │   │   ├── 02_tools_modules.sh
│   │   │   ├── 03_tools_diagnostic.sh
│   │   │   ├── 04_tools_delay.sh
│   │   │   ├── 05_tools_choice/
│   │   │   │   ├── 00_choice_import.sh
│   │   │   │   ├── 01_choice_cores.sh
│   │   │   │   ├── 02_choice_xkeen.sh
│   │   │   │   ├── 03_choice_geofile.sh
│   │   │   │   ├── 04_choice_input.sh
│   │   │   │   └── 05_choice_cron/
│   │   │   │       ├── 00_cron_import.sh
│   │   │   │       ├── 01_cron_status.sh
│   │   │   │       └── 02_cron_time.sh
│   │   │   ├── 06_tools_backups/
│   │   │   │   ├── 00_backups_import.sh
│   │   │   │   ├── 01_backups_xkeen.sh
│   │   │   │   ├── 02_backups_configs_mihomo.sh
│   │   │   │   └── 02_backups_configs_xray.sh
│   │   │   └── 07_tools_downloaders/
│   │   │       ├── 00_downloaders_import.sh
│   │   │       ├── 00_fetch_with_mirrors.sh
│   │   │       ├── 01_downloaders_mihomo.sh
│   │   │       ├── 01_downloaders_xray.sh
│   │   │       └── 02_donwloaders_xkeen.sh
│   │   ├── 05_tests/
│   │   │   ├── 00_tests_import.sh
│   │   │   ├── 01_tests_connected.sh
│   │   │   ├── 02_tests_xports.sh
│   │   │   └── 03_tests_storage.sh
│   │   ├── about.sh
│   │   └── import.sh
│   └── xkeen
├── test/
│   └── README.md
└── wiki/
    ├── DNS-over-VLESS.md
    ├── FAQ.md
    ├── Home.md
    ├── _Footer.md
    ├── _Sidebar.md
    └── Маршрутизация-по-DSCP.md
Condensed preview — 97 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (463K chars).
[
  {
    "path": ".gitattributes",
    "chars": 170,
    "preview": "# 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"
  },
  {
    "path": ".github/workflows/package-folder.yaml",
    "chars": 1719,
    "preview": "name: Create Test build to `main/test/` folder\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'scripts/**'\n "
  },
  {
    "path": ".github/workflows/release.yaml",
    "chars": 3160,
    "preview": "name: Create Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version number (e.g., 1."
  },
  {
    "path": ".github/workflows/wiki-sync.yaml",
    "chars": 1428,
    "preview": "name: Sync GitHub Wiki\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'wiki/**'\n      - '.github/workflows/w"
  },
  {
    "path": ".gitignore",
    "chars": 50,
    "preview": ".claude\ngraphify-out\ndone\nsrc/graphify\nsrc/output\n"
  },
  {
    "path": "01_info_variable.sh",
    "chars": 5112,
    "preview": "# -------------------------------------\n# Цвета\n# -------------------------------------\ngreen=\"\\033[92m\"\t# Зеленый\nred=\""
  },
  {
    "path": "LICENSE",
    "chars": 1497,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2026, jameszeroX\n\nRedistribution and use in source and binary forms, with or without"
  },
  {
    "path": "README.md",
    "chars": 5176,
    "preview": "# XKeen 1.1.3.9\n\n> **XKeen** — утилита для выборочной маршрутизации сетевого трафика через прокси‑движки **Xray** и **Mi"
  },
  {
    "path": "configuration.md",
    "chars": 5147,
    "preview": "---\n\n## Внешние списки портов и IP\n\nПредусмотрена возможность добавить в конфигурационные файлы XKeen необходимые порты "
  },
  {
    "path": "docs/README.md",
    "chars": 1558,
    "preview": "# Документация XKeen\n\nЭтот каталог содержит техническую документацию для разработчиков и контрибьюторов. Если вы пользов"
  },
  {
    "path": "docs/architecture.md",
    "chars": 5109,
    "preview": "# Архитектура XKeen\n\nXKeen — POSIX-shell утилита (`sh`, не `bash`) для роутеров Keenetic/Netcraze под Entware. Кода на к"
  },
  {
    "path": "docs/build-and-release.md",
    "chars": 3587,
    "preview": "# Сборка и релиз\n\nЛокальной сборки нет. Всё делает CI на GitHub Actions. В этом разделе — три workflow-а и две схемы кан"
  },
  {
    "path": "docs/commands.md",
    "chars": 3823,
    "preview": "# Справочник флагов `xkeen`\n\nПолный список флагов диспетчера [`scripts/xkeen`](../scripts/xkeen). Извлечён из `help_xkee"
  },
  {
    "path": "docs/contributing.md",
    "chars": 4464,
    "preview": "# Правила правки\n\n## Язык — POSIX `sh`\n\nЦелевая среда — Entware на BusyBox `ash`. **Никаких bash-измов:**\n\n| Запрещено |"
  },
  {
    "path": "docs/runtime-paths.md",
    "chars": 3110,
    "preview": "# Раскладка на роутере\n\nВсе runtime-пути на роутере. В этом репозитории каталог `_xkeen/`; после установки на роутер он "
  },
  {
    "path": "forkinfo.md",
    "chars": 5972,
    "preview": "## Сравнение форка с оригинальным XKeen\n\nИзменения:\n- Исправлено добавление портов в исключения (ранее команду `xkeen -a"
  },
  {
    "path": "install.sh",
    "chars": 3459,
    "preview": "#!/bin/sh\n\ngreen=\"\\033[92m\"\nred=\"\\033[91m\"\nyellow=\"\\033[93m\"\nlight_blue=\"\\033[96m\"\nreset=\"\\033[0m\"\n\nurl_stable=\"https://"
  },
  {
    "path": "knownissues.md",
    "chars": 305,
    "preview": "- При проксировании DNS с помощью XKeen, в профиле \"Политика по умолчанию\" отсутствует интернет, создайте пользовательсу"
  },
  {
    "path": "scripts/_xkeen/01_info/00_info_import.sh",
    "chars": 369,
    "preview": "# Импорт информационных модулей\n\n# Модуль информации\n. \"$xinfo_dir/01_info_variable.sh\"\n. \"$xinfo_dir/02_info_packages.s"
  },
  {
    "path": "scripts/_xkeen/01_info/01_info_variable.sh",
    "chars": 6277,
    "preview": "# -------------------------------------\n# Цвета\n# -------------------------------------\ngreen=\"\\033[92m\"\t# Зеленый\nred=\""
  },
  {
    "path": "scripts/_xkeen/01_info/02_info_packages.sh",
    "chars": 1538,
    "preview": "# Кэшируем список установленных пакетов один раз вместо opkg-форка на каждую проверку\n_packages_cache=$(opkg list-instal"
  },
  {
    "path": "scripts/_xkeen/01_info/03_info_cpu.sh",
    "chars": 919,
    "preview": "# Функция для получения информации о процессоре\ninfo_cpu() {\n    if command -v opkg >/dev/null 2>&1; then\n        opkg_a"
  },
  {
    "path": "scripts/_xkeen/01_info/04_info_mihomo.sh",
    "chars": 223,
    "preview": "# Функция для проверки установки Mihomo\n\ninfo_mihomo() {\n    if [ -f \"$install_dir/mihomo\" ] && [ -f \"$install_dir/yq\" ]"
  },
  {
    "path": "scripts/_xkeen/01_info/04_info_xray.sh",
    "chars": 185,
    "preview": "# Функция для проверки установки Xray\n\ninfo_xray() {\n    if [ -f \"$install_dir/xray\" ]; then\n        xray_installed=\"ins"
  },
  {
    "path": "scripts/_xkeen/01_info/05_info_geofile.sh",
    "chars": 838,
    "preview": "# Функция для проверки наличия и записи информации о базах GeoSite\ninfo_geosite() {\n    update_refilter_geosite=false\n  "
  },
  {
    "path": "scripts/_xkeen/01_info/06_info_console.sh",
    "chars": 7956,
    "preview": "print_log_status() {\n    local status_code=$1\n    local success_msg=$2\n    local error_msg=$3\n\n    if [ \"$status_code\" -"
  },
  {
    "path": "scripts/_xkeen/01_info/07_info_cron.sh",
    "chars": 414,
    "preview": "# Проверка наличия задач автоматического обновления в cron\ninfo_cron() {\n    # Получаем текущую crontab конфигурацию для"
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/00_version_import.sh",
    "chars": 211,
    "preview": "# Импорт модулей проверки версий\n\n# Модули проверки версий\n. \"$xinfo_dir/08_info_version/01_version_xkeen.sh\"\n. \"$xinfo_"
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/01_version_xkeen.sh",
    "chars": 1327,
    "preview": "# Функция для получения версии из xkeen API и сохранения ее в переменной\ninfo_version_xkeen() {\n    version=$(eval curl "
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/02_version_mihomo.sh",
    "chars": 692,
    "preview": "# Функции для получения информации о версиях Mihomo и Yq\ninfo_version_mihomo() {\n    if [ \"$mihomo_installed\" = \"install"
  },
  {
    "path": "scripts/_xkeen/01_info/08_info_version/02_version_xray.sh",
    "chars": 462,
    "preview": "# Функция для получения информации о версии Xray\ninfo_version_xray() {\n\n    # Проверяем, установлен ли Xray\n    if [ \"$x"
  },
  {
    "path": "scripts/_xkeen/02_install/00_install_import.sh",
    "chars": 438,
    "preview": "# Импорт модулей установки\n\n# Модули установки\n. \"$xinstall_dir/01_install_packages.sh\"\n. \"$xinstall_dir/02_install_xray"
  },
  {
    "path": "scripts/_xkeen/02_install/01_install_packages.sh",
    "chars": 776,
    "preview": "# Установка необходимых пакетов\ninstall_packages() {\n    package_status=\"$1\"\n    package_name=\"$2\"\n\n    if [ \"${package_"
  },
  {
    "path": "scripts/_xkeen/02_install/02_install_mihomo.sh",
    "chars": 1317,
    "preview": "# Функция для установки Mihomo\ninstall_mihomo() {\n    echo -e \"  ${yellow}Выполняется установка${reset} Mihomo. Пожалуйс"
  },
  {
    "path": "scripts/_xkeen/02_install/02_install_xray.sh",
    "chars": 1954,
    "preview": "# Функция для установки Xray\ninstall_xray() {\n    echo -e \"  ${yellow}Выполняется установка${reset} Xray. Пожалуйста, по"
  },
  {
    "path": "scripts/_xkeen/02_install/03_install_xkeen.sh",
    "chars": 820,
    "preview": "# Функция для установки XKeen\ninstall_xkeen() {\n    xkeen_archive=\"$ktmp_dir/xkeen.tar.gz\"\n\n    # Проверка наличия архив"
  },
  {
    "path": "scripts/_xkeen/02_install/04_install_geofile.sh",
    "chars": 5033,
    "preview": "# Функция для загрузки и обработки геофайлов\nprocess_geo_file() {\n    local url=\"$1\"\n    local filename=\"$2\"\n    local d"
  },
  {
    "path": "scripts/_xkeen/02_install/05_install_geoipset.sh",
    "chars": 4915,
    "preview": "# Валидаторы для fetch_with_mirrors: проверяют размер + базовый синтаксис\n# содержимого (catch HTML-stub и мусор от prox"
  },
  {
    "path": "scripts/_xkeen/02_install/06_install_cron.sh",
    "chars": 792,
    "preview": "# Функция для установки задач Cron\ninstall_cron() {\n    cron_entry=\n\n    # Добавление задачи Cron для обновления GeoFile"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/00_register_common.sh",
    "chars": 1526,
    "preview": "# Общие функции для регистрации пакетов в opkg\n\nwrite_opkg_control() {\n    package_name=\"$1\"\n    package_version=\"$2\"\n  "
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/00_register_import.sh",
    "chars": 347,
    "preview": "# Импорт модулей регистраций\n\t\n# Модули регистрации\n. \"$xinstall_dir/07_install_register/00_register_common.sh\"\n. \"$xins"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/01_register_mihomo.sh",
    "chars": 1659,
    "preview": "# Регистрация Mihomo\n\nregister_mihomo_list() {\n    cd \"$register_dir/\" || exit\n    touch mihomo_s.list\n    echo \"/opt/sb"
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/01_register_xray.sh",
    "chars": 1227,
    "preview": "# Регистрация xray\nregister_xray_control() {\n    write_opkg_control \\\n        \"xray_s\" \\\n        \"$xray_current_version\""
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/02_register_xkeen.sh",
    "chars": 5581,
    "preview": "# Регистрация XKeen\n\n# Функция для создания файла xkeen.control\nregister_xkeen_control() {\n    write_opkg_control \\\n    "
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/03_register_cron.sh",
    "chars": 2707,
    "preview": "# Функция для регистрации инициализационного скрипта cron\nregister_cron_initd() {\n    # Проверка наличия пакета cron\n   "
  },
  {
    "path": "scripts/_xkeen/02_install/07_install_register/04_register_init.sh",
    "chars": 77550,
    "preview": "#!/bin/sh\n\n# Информация о службе: Запуск / Остановка XKeen\n# Версия: 2.30\n\n# Окружение\nPATH=\"/opt/bin:/opt/sbin:/sbin:/b"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/00_configs_import.sh",
    "chars": 113,
    "preview": "# Импорт модулей конфигураций\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",
    "chars": 514,
    "preview": "# Функция для установки файлов конфигурации Xray\ninstall_configs() {\n    if [ ! -d \"$xray_conf_dir\" ]; then\n        mkdi"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/01_log.json",
    "chars": 151,
    "preview": "{\n  \"log\": {\n    \"access\": \"/opt/var/log/xray/access.log\",\n    \"error\": \"/opt/var/log/xray/error.log\",\n    \"dnsLog\": tru"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/02_dns.json",
    "chars": 56,
    "preview": "{\n// Пример настройки - https://jameszero.net/3398.htm\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/03_inbounds.json",
    "chars": 671,
    "preview": "{\n  \"inbounds\": [\n    {\n      \"port\": 1181,\n      \"protocol\": \"tunnel\",\n      \"settings\": {\n        \"network\": \"tcp\",\n  "
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/04_outbounds.json",
    "chars": 84,
    "preview": "{\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",
    "chars": 73,
    "preview": "{\n// Создайте файл по ссылке https://xray-routing-generator.netlify.app\n}"
  },
  {
    "path": "scripts/_xkeen/02_install/08_install_configs/02_configs_dir/06_policy.json",
    "chars": 115,
    "preview": "{\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",
    "chars": 268,
    "preview": "# Импорт модулей удаления\n\n# Модули удаления\n. \"$xdelete_dir/01_delete_geofile.sh\"\n. \"$xdelete_dir/02_delete_geoipset.sh"
  },
  {
    "path": "scripts/_xkeen/03_delete/01_delete_geofile.sh",
    "chars": 1186,
    "preview": "# Функция для удаления выбранных файлов GeoSite\ndelete_geosite() {\n    [ \"$choice_delete_geosite_refilter_select\" = \"tru"
  },
  {
    "path": "scripts/_xkeen/03_delete/02_delete_geoipset.sh",
    "chars": 1405,
    "preview": "# Функция для удаления GeoIPSET\ndelete_geoipset() {\n    while true; do\n        printf \"\\n  Желаете удалить российские IP"
  },
  {
    "path": "scripts/_xkeen/03_delete/03_delete_cron.sh",
    "chars": 291,
    "preview": "# Функция для удаления cron задачи для GeoFile\ndelete_cron_geofile() {\n    if [ -f \"$cron_dir/$cron_file\" ]; then\n      "
  },
  {
    "path": "scripts/_xkeen/03_delete/04_delete_configs.sh",
    "chars": 173,
    "preview": "# Удаление всех конфигураций Xray\n\ndelete_configs() {\n    if [ -d \"$xray_conf_dir\" ]; then\n        find \"$xray_conf_dir\""
  },
  {
    "path": "scripts/_xkeen/03_delete/05_delete_register.sh",
    "chars": 1499,
    "preview": "# Удаление регистрации Xray\ndelete_register_xray() {\n    # Удаляем соответствующие записи из файла статуса opkg\n    sed "
  },
  {
    "path": "scripts/_xkeen/03_delete/06_delete_tmp.sh",
    "chars": 1750,
    "preview": "# Удаление временных файлов и директорий\ndelete_tmp() {\n    [ -d \"$ktmp_dir\" ] && rm -rf \"$ktmp_dir\"\n    [ -d \"$xtmp_dir"
  },
  {
    "path": "scripts/_xkeen/04_tools/00_tools_import.sh",
    "chars": 410,
    "preview": "\n# Дополнительные инструменты\n. \"$xtools_dir/01_tools_ports.sh\"\n. \"$xtools_dir/02_tools_modules.sh\"\n. \"$xtools_dir/03_to"
  },
  {
    "path": "scripts/_xkeen/04_tools/01_tools_ports.sh",
    "chars": 7605,
    "preview": "read_ports_file() {\n    file=\"$1\"\n\n    [ -f \"$file\" ] || return\n\n    sed 's/\\r$//' \"$file\" | \\\n    sed 's/^[[:space:]]*/"
  },
  {
    "path": "scripts/_xkeen/04_tools/02_tools_modules.sh",
    "chars": 329,
    "preview": "show_deprecation_warning() {\n    echo -e \"  ${red}Внимание!${reset} Команда устарела и удалена из XKeen\"\n    echo -e \"  "
  },
  {
    "path": "scripts/_xkeen/04_tools/03_tools_diagnostic.sh",
    "chars": 7243,
    "preview": "diagnostic() {\n    # Установка пути к файлу diagnostic\n    diagnostic=\"/opt/diagnostic.txt\"\n\n    if pidof \"xray\" >/dev/n"
  },
  {
    "path": "scripts/_xkeen/04_tools/04_tools_delay.sh",
    "chars": 1349,
    "preview": "get_current_delay() {\n    awk -F= '/^[[:space:]]*start_delay=/{print $2; exit}' \"$1\" | tr -d '[:space:]\"'\n}\n\ndelay_autos"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/00_choice_import.sh",
    "chars": 326,
    "preview": "# Импорт модулей выбора пользователя\n\n# Модули выбора\n. \"$xtools_dir/05_tools_choice/01_choice_cores.sh\"\n. \"$xtools_dir/"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/01_choice_cores.sh",
    "chars": 4038,
    "preview": "# Запрос на добавление ядер проксирования\nchoice_add_proxy_cores() {\n    while true; do\n        echo\n        echo -e \"  "
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/02_choice_xkeen.sh",
    "chars": 8529,
    "preview": "# Запрос на смену канала обновлений XKeen (Stable/Dev)\nchoice_channel_xkeen() {\n    echo\n    echo -e \"  Текущий канал об"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/03_choice_geofile.sh",
    "chars": 8634,
    "preview": "choice_geodata() {\n    type=\"$1\"\n    type_name=\"$2\"\n    src3=\"$3\"\n    src3_name=\"$4\"\n    var_bypass=\"$5\"\n\n    has_missin"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/04_choice_input.sh",
    "chars": 3918,
    "preview": "# Функция для выбора пользователя между \"Да\" и \"Нет\" с номерами 0 и 1\ninput_concordance_list() {\n    prompt_message=\"  $"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/05_choice_cron/00_cron_import.sh",
    "chars": 183,
    "preview": "# Импорт модулей вопросов cron\n\n# Модули вопросов cron\n. \"$xtools_dir/05_tools_choice/05_choice_cron/01_cron_status.sh\"\n"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/05_choice_cron/01_cron_status.sh",
    "chars": 3521,
    "preview": "# Определение статуса для задач cron\nget_existing_cron_time() {\n    crontab -l 2>/dev/null | grep 'xkeen -ug' | head -n1"
  },
  {
    "path": "scripts/_xkeen/04_tools/05_tools_choice/05_choice_cron/02_cron_time.sh",
    "chars": 2270,
    "preview": "# Определение времени для задач cron\nchoice_cron_time() {\n    [ \"$choice_geofile_cron_select\" = true ] || return\n\n    ec"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/00_backups_import.sh",
    "chars": 247,
    "preview": "# Импорт модулей резервного копирования\n\n# Модули резервного копирования\n. \"$xtools_dir/06_tools_backups/01_backups_xkee"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/01_backups_xkeen.sh",
    "chars": 1734,
    "preview": "# Создание резервной копии XKeen\nbackup_xkeen() {\n    if choice_backup_xkeen && [ -z \"$manual_backup\" ]; then\n        re"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/02_backups_configs_mihomo.sh",
    "chars": 1094,
    "preview": "backup_configs_mihomo() {\n    backup_filename=\"${current_datetime}_configs_mihomo\"\n    backup_configs_dir=\"$backups_dir/"
  },
  {
    "path": "scripts/_xkeen/04_tools/06_tools_backups/02_backups_configs_xray.sh",
    "chars": 1074,
    "preview": "backup_configs_xray() {\n    backup_filename=\"${current_datetime}_configs_xray\"\n    backup_configs_dir=\"$backups_dir/$bac"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/00_downloaders_import.sh",
    "chars": 357,
    "preview": "# Импорт модулей загрузки\n\n# fetch_with_mirrors / probe_with_mirrors уже подгружены из xkeen/import.sh\n# (раньше install"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/00_fetch_with_mirrors.sh",
    "chars": 6915,
    "preview": "# Загрузка с per-call mirror-fallback'ом.\n#\n# Заменяет паттерн \"test_github -> один gh_proxy на сессию -> один curl\n# бе"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/01_downloaders_mihomo.sh",
    "chars": 7435,
    "preview": "# Загрузка Mihomo\ndownload_mihomo() {\n    USE_JSDELIVR=\"\"\n    printf \"  ${green}Запрос информации${reset} о релизах ${ye"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/01_downloaders_xray.sh",
    "chars": 8523,
    "preview": "# Загрузка Xray\ndownload_xray() {\n    USE_JSDELIVR=\"\"\n    printf \"  ${green}Запрос информации${reset} о релизах ${yellow"
  },
  {
    "path": "scripts/_xkeen/04_tools/07_tools_downloaders/02_donwloaders_xkeen.sh",
    "chars": 447,
    "preview": "# Загрузка XKeen\ndownload_xkeen() {\n    mkdir -p \"$ktmp_dir\"\n    printf \"  ${yellow}Выполняется загрузка${reset} XKeen\\n"
  },
  {
    "path": "scripts/_xkeen/05_tests/00_tests_import.sh",
    "chars": 161,
    "preview": "# Импорт модулей тестирования\n\n# Модули тестирования\n. \"$xtests_dir/01_tests_connected.sh\"\n. \"$xtests_dir/02_tests_xport"
  },
  {
    "path": "scripts/_xkeen/05_tests/01_tests_connected.sh",
    "chars": 3643,
    "preview": "# Функция проверки доступности интернета\ntest_connection() {\n    nslookup \"$conn_URL\" >/dev/null 2>&1 && return 0\n    cu"
  },
  {
    "path": "scripts/_xkeen/05_tests/02_tests_xports.sh",
    "chars": 2740,
    "preview": "# Определение на каких портах слушает ядро прокси\ntests_ports_client() {\n\n    if pidof \"xray\" >/dev/null; then\n        n"
  },
  {
    "path": "scripts/_xkeen/05_tests/03_tests_storage.sh",
    "chars": 1535,
    "preview": "# Определение места установки Entware\nlocation_entware_storage() {\n    mount_point=$(mount | grep 'on /opt ')\n    device"
  },
  {
    "path": "scripts/_xkeen/about.sh",
    "chars": 9808,
    "preview": "about_xkeen() {\n    echo\n    printf \"  Утилита ${green}XKeen${reset} предназначена для управления межсетевым\\n  экраном "
  },
  {
    "path": "scripts/_xkeen/import.sh",
    "chars": 890,
    "preview": "# Импорт основных модулей и определение их путей\n\nscript_dir=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nxinfo_dir=\"$script_dir/.xk"
  },
  {
    "path": "scripts/xkeen",
    "chars": 46925,
    "preview": "#!/bin/sh\n\n# Определение директории, где находится xkeen\nscript_dir=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\n# Скрываем основну"
  },
  {
    "path": "test/README.md",
    "chars": 9702,
    "preview": "## XKeen 2.0 Beta\n\n> [!NOTE]\n> Это версия из канала разработки. Она регулярно дорабатывается, содержит новейшие функции,"
  },
  {
    "path": "wiki/DNS-over-VLESS.md",
    "chars": 4295,
    "preview": "# DNS-over-VLESS — направляем DNS-трафик через прокси xray\n\n> Источник: [jameszero.net/3398.htm](https://jameszero.net/3"
  },
  {
    "path": "wiki/FAQ.md",
    "chars": 14354,
    "preview": "# FAQ по XKeen\n\n> Источник: [jameszero.net/faq-xkeen.htm](https://jameszero.net/faq-xkeen.htm)\n\n## Введение\n\n**XKeen** —"
  },
  {
    "path": "wiki/Home.md",
    "chars": 2241,
    "preview": "# XKeen\n\n**XKeen** — POSIX-shell утилита для прозрачной маршрутизации сетевого трафика через прокси-движки **Xray** и **"
  },
  {
    "path": "wiki/_Footer.md",
    "chars": 147,
    "preview": "---\n\n[Сайт автора форка — jameszero.net](https://jameszero.net/) · [Обсуждение на forum.keenetic.ru](https://forum.keene"
  },
  {
    "path": "wiki/_Sidebar.md",
    "chars": 130,
    "preview": "**Навигация**\n\n- [Главная](Home)\n- [FAQ](FAQ)\n- [DNS-over-VLESS](DNS-over-VLESS)\n- [Маршрутизация по DSCP](Маршрутизация"
  },
  {
    "path": "wiki/Маршрутизация-по-DSCP.md",
    "chars": 1469,
    "preview": "# Маршрутизация по DSCP-меткам в XKeen\n\n> Источник: [jameszero.net/4509.htm](https://jameszero.net/4509.htm)\n\nВ XKeen 2."
  }
]

About this extraction

This page contains the full source code of the jameszeroX/XKeen GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 97 files (356.9 KB), approximately 107.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!