#!/bin/sh CONFIG_PATH="/opt/etc/HydraRoute/hrneo.conf" DOMAIN_CONF="/opt/etc/HydraRoute/domain.conf" PID_FILE="/var/run/hrneo.pid" INIT_SCRIPT="/opt/etc/init.d/S99hrneo" API_PORT=79 API_URL="http://localhost:${API_PORT}/rci/show/ip/policy/" VPN_PREFIXES="tun tap wg ppp l2tp vless ss ssr vmess trojan nwg t2s" KNOWN_DNS_IPS="8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 208.67.222.222 208.67.220.220 77.88.8.8 77.88.8.1 94.140.14.14 94.140.15.15" log_info() { echo "[INFO] $1" >&2 } log_warn() { echo "[WARN] $1" >&2 } log_error() { echo "[ERROR] $1" >&2 } log_success() { echo "[OK] $1" >&2 } log_step() { echo "" >&2 echo "==> $1" >&2 } check_hrneo_installed() { log_step "Проверка установки Hydra Route Neo" if opkg list-installed | grep -q '^hrneo '; then local version=$(opkg list-installed | grep '^hrneo ' | awk '{print $3}') log_success "Hydra Route Neo установлен (версия: $version)" else log_error "Hydra Route Neo не установлен" log_info "Установите пакет hrneo для работы выборочной маршрутизации" return 1 fi if opkg list-installed | grep -q '^hrweb '; then local version=$(opkg list-installed | grep '^hrweb ' | awk '{print $3}') log_success "Hydra Route Web-UI установлен (версия: $version)" else log_warn "Hydra Route Web-UI не установлен" log_info "Web-интерфейс недоступен, но диагностика может быть продолжена" fi return 0 } check_critical_files() { log_step "Проверка критически важных файлов" local all_files_exist=1 if [ ! -f "/opt/etc/HydraRoute/domain.conf" ]; then log_error "Критический файл отсутствует: /opt/etc/HydraRoute/domain.conf" log_info "Этот файл содержит список доменов для маршрутизации" all_files_exist=0 else log_success "Файл доменов" fi if [ ! -f "/opt/etc/HydraRoute/hrneo.conf" ]; then log_error "Критический файл отсутствует: /opt/etc/HydraRoute/hrneo.conf" log_info "Этот файл содержит основную конфигурацию hrneo" all_files_exist=0 else log_success "Файл конфигурации" fi if [ ! -f "/opt/etc/HydraRoute/ip.list" ]; then log_error "Критический файл отсутствует: /opt/etc/HydraRoute/ip.list" log_info "Этот файл содержит список IP-адресов/подсетей для маршрутизации" all_files_exist=0 else log_success "Файл CIDR" fi if [ ! -f "/opt/etc/ndm/netfilter.d/015-hrneo.sh" ]; then log_error "Критический файл отсутствует: /opt/etc/ndm/netfilter.d/015-hrneo.sh" log_info "Этот файл поддерживает в актуальном состоянии правила маршрутизации hrneo" all_files_exist=0 else log_success "Файл актуализации iptables" fi if [ ! -f "/opt/etc/init.d/S99hrneo" ]; then log_error "Критический файл отсутствует: /opt/etc/init.d/S99hrneo" log_info "Этот файл отвечает за управление работой службы hrneo" all_files_exist=0 else log_success "Инит скрипт" fi if [ ! -f "/opt/bin/hrneo" ]; then log_error "Критический файл отсутствует: /opt/bin/hrneo" log_info "Это исполняемый файл hrneo" all_files_exist=0 else log_success "Бинарник" fi if [ $all_files_exist -eq 0 ]; then log_info "Переустановите пакет hrneo или восстановите файлы из резервной копии" return 1 fi return 0 } check_nflog_module() { log_step "Проверка модуля ядра NFLOG" if [ ! -f "/lib/modules/4.9-ndm-5/xt_NFLOG.ko" ]; then log_error "Компонент ОС: Пакет расширения «Xtables-addons для Netfilter» не установлен" log_info "Маршрутизация не будет работать!" log_info "Установите его в роутере: Управление -> Параметры системы -> Показать компоненты" return 1 else log_success "Модуль ядра xt_NFLOG.ko найден" return 0 fi } check_conflicts() { log_step "Проверка конфликтующего ПО" local conflict_found=0 if [ -f "/opt/sbin/xkeen" ] || [ -d "/opt/sbin/.xkeen" ] || [ -f "/opt/sbin/mihomo_bak" ]; then conflict_found=1 fi if [ -d "/opt/etc/xray/configs" ] || [ -d "/opt/etc/xray/dat" ] || [ -d "/opt/etc/xkeen" ]; then conflict_found=1 fi if [ -f "/opt/etc/ndm/netfilter.d/proxy.sh" ]; then conflict_found=1 fi if [ -d "/opt/var/log/xkeen" ] || [ -d "/opt/tmp/xkeen" ]; then conflict_found=1 fi if grep -q '^xkeen:' /etc/passwd 2>/dev/null; then conflict_found=1 fi if [ $conflict_found -eq 1 ]; then log_error "Обнаружен конфликт: в системе найдены компоненты xKeen" log_info "Hydra Route Neo и xKeen не могут работать одновременно" log_info "Даже если пакет xkeen удален, в системе могут оставаться его файлы и настройки" return 1 fi log_success "Конфликтующее ПО не обнаружено" return 0 } check_dependencies() { log_step "Проверка зависимостей" local need_update=0 if ! command -v jq >/dev/null 2>&1; then log_warn "Пакет jq не установлен" need_update=1 else log_success "Пакет jq установлен" fi if ! command -v curl >/dev/null 2>&1; then log_warn "Пакет curl не установлен" need_update=1 else log_success "Пакет curl установлен" fi if [ $need_update -eq 1 ]; then log_info "Установка недостающих пакетов..." if ! opkg update >/dev/null 2>&1; then log_error "Не удалось обновить список пакетов" return 1 fi if ! command -v jq >/dev/null 2>&1; then if opkg install jq >/dev/null 2>&1; then log_success "Пакет jq успешно установлен" else log_error "Не удалось установить jq" return 1 fi fi if ! command -v curl >/dev/null 2>&1; then if opkg install curl >/dev/null 2>&1; then log_success "Пакет curl успешно установлен" else log_error "Не удалось установить curl" return 1 fi fi fi return 0 } read_config() { if [ ! -f "$CONFIG_PATH" ]; then log_error "Файл конфигурации не найден: $CONFIG_PATH" return 1 fi DIRECT_ROUTE_ENABLED=$(grep '^DirectRouteEnabled=' "$CONFIG_PATH" | cut -d'=' -f2) INTERFACE_FWMARK_START=$(grep '^InterfaceFwMarkStart=' "$CONFIG_PATH" | cut -d'=' -f2) INTERFACE_TABLE_START=$(grep '^InterfaceTableStart=' "$CONFIG_PATH" | cut -d'=' -f2) IPSET_ENABLE_TIMEOUT=$(grep '^IpsetEnableTimeout=' "$CONFIG_PATH" | cut -d'=' -f2) if [ -z "$DIRECT_ROUTE_ENABLED" ]; then DIRECT_ROUTE_ENABLED="true" fi if [ -z "$INTERFACE_FWMARK_START" ]; then INTERFACE_FWMARK_START="12289" fi if [ -z "$INTERFACE_TABLE_START" ]; then INTERFACE_TABLE_START="301" fi if [ -z "$IPSET_ENABLE_TIMEOUT" ]; then IPSET_ENABLE_TIMEOUT="false" fi log_success "Конфигурация загружена (DirectRoute: $DIRECT_ROUTE_ENABLED)" return 0 } check_domain_in_config() { local domain="$1" log_step "Проверка наличия домена в конфигурации" if [ ! -f "$DOMAIN_CONF" ]; then log_error "Файл $DOMAIN_CONF не найден" return 1 fi local found=0 local found_disabled=0 local target_name="" while IFS= read -r line || [ -n "$line" ]; do line=$(echo "$line" | sed 's/^[ \t]*//;s/[ \t]*$//') if [ -z "$line" ]; then continue fi local is_disabled=0 case "$line" in \#*) is_disabled=1 line=$(echo "$line" | sed 's/^#//' | sed 's/^[ \t]*//') ;; esac case "$line" in */*) ;; *) continue ;; esac local domains_part=$(echo "$line" | cut -d'/' -f1) local target_part=$(echo "$line" | cut -d'/' -f2- | cut -d',' -f1) local old_ifs="$IFS" IFS=',' for d in $domains_part; do d=$(echo "$d" | sed 's/^[ \t]*//;s/[ \t]*$//') if [ "$d" = "$domain" ]; then if [ $is_disabled -eq 1 ]; then found_disabled=1 else found=1 target_name="$target_part" fi IFS="$old_ifs" break fi done IFS="$old_ifs" if [ $found -eq 1 ]; then break fi done < "$DOMAIN_CONF" if [ $found -eq 1 ]; then log_success "Домен найден в конфигурации" log_info "Назначен целевой объект: $target_name" echo "$target_name" return 0 elif [ $found_disabled -eq 1 ]; then log_error "Домен '$domain' есть в Hydra Route, но отключен" log_info "Активируйте '$domain' в Web-интерфейсе Hydra Route" return 1 else log_error "Домен '$domain' отсутствует в Hydra Route" log_info "Добавьте домен в Web-интерфейсе Hydra Route для его маршрутизации" return 1 fi } classify_target() { local target="$1" log_step "Определение типа целевого объекта" if [ -d "/sys/class/net/$target" ]; then log_success "Целевой объект '$target' является сетевым интерфейсом" echo "interface" else log_success "Целевой объект '$target' является политикой доступа" echo "policy" fi } restart_service() { log_step "Перезапуск сервиса hrneo" if [ ! -x "$INIT_SCRIPT" ]; then log_error "Скрипт инициализации не найден или не исполняемый: $INIT_SCRIPT" return 1 fi log_info "Выполняется перезапуск..." $INIT_SCRIPT restart >/dev/null 2>&1 || true log_info "Ожидание запуска сервиса (5 секунд)..." sleep 5 if [ -f "$PID_FILE" ]; then local pid=$(cat "$PID_FILE" | tr -d '\n') if [ -d "/proc/$pid" ]; then log_success "Сервис успешно запущен (PID: $pid)" else log_error "PID файл существует, но процесс не запущен (stale PID: $pid)" return 1 fi else log_error "PID файл не создан, сервис не запустился" return 1 fi return 0 } check_policy_created() { local policy_name="$1" log_step "Проверка создания политики в роутере" log_info "Запрос информации о политике '$policy_name'..." local response=$(curl -s "$API_URL" 2>/dev/null || echo "{}") if echo "$response" | jq -e ".\"$policy_name\"" >/dev/null 2>&1; then local mark=$(echo "$response" | jq -r ".\"$policy_name\".mark // empty") local description=$(echo "$response" | jq -r ".\"$policy_name\".description // empty") log_success "Политика '$policy_name' создана в роутере" [ -n "$mark" ] && [ "$mark" != "empty" ] && log_info "Mark ID: $mark" [ -n "$description" ] && [ "$description" != "empty" ] && log_info "Описание: $description" return 0 else log_error "Политика '$policy_name' не найдена в роутере" return 1 fi } get_interface_from_policy() { local policy_name="$1" log_step "Извлечение интерфейса из политики" local response=$(curl -s "$API_URL" 2>/dev/null || echo "{}") local keenetic_interface=$(echo "$response" | jq -r ".\"$policy_name\" | .. | .route? // empty | .[] | select(.destination == \"0.0.0.0/0\") | .interface" 2>/dev/null | head -n1) if [ -z "$keenetic_interface" ] || [ "$keenetic_interface" = "null" ]; then log_error "В политике '$policy_name' не указано VPN подключение" log_info "Политика существует, но не содержит маршрутов" log_info "Необходимо добавить подключение в политику '$policy_name' через веб-интерфейс роутера:" log_info " Интернет -> Приоритеты подключений -> $policy_name -> активировать необходимое подключение" return 1 fi if echo "$keenetic_interface" | grep -qE '^(GigabitEthernet|Bridge)'; then log_error "В политике '$policy_name' указан локальный интерфейс вместо VPN" log_info "Найден интерфейс: $keenetic_interface" log_info "Необходимо указать VPN подключение (Wireguard, VLESS, Proxy и т.д.)" log_info "Через веб-интерфейс: Интернет-фильтр -> Списки -> $policy_name -> Подключение" return 1 fi log_info "Интерфейс Keenetic: $keenetic_interface" if command -v ndmc >/dev/null 2>&1; then local system_interface=$(ndmc -c "show interface $keenetic_interface system-name" 2>/dev/null | grep 'system-name:' | awk '{print $2}') if [ -n "$system_interface" ]; then log_success "Системное имя интерфейса: $system_interface" echo "$system_interface" return 0 else log_warn "Не удалось получить системное имя интерфейса '$keenetic_interface'" log_info "Пропуск проверки интерфейса" echo "unknown" return 0 fi else log_warn "ndmc не установлен, невозможно преобразовать имя интерфейса" log_info "Пропуск проверки интерфейса" echo "unknown" return 0 fi } check_interface_exists() { local interface="$1" log_step "Проверка существования интерфейса" if [ ! -d "/sys/class/net/$interface" ]; then log_error "Интерфейс '$interface' отсутствует в системе" return 1 fi log_success "Интерфейс '$interface' существует" return 0 } check_vpn_protocol() { local interface="$1" log_step "Проверка типа VPN протокола" local is_vpn=0 for prefix in $VPN_PREFIXES; do if echo "$interface" | grep -q "^${prefix}"; then is_vpn=1 log_success "Интерфейс '$interface' определен как VPN (префикс: $prefix)" break fi done if [ $is_vpn -eq 0 ]; then log_warn "Интерфейс '$interface' не определен как VPN-подключение" log_info "Префикс интерфейса не соответствует известным VPN протоколам" log_info "Известные префиксы: $VPN_PREFIXES" fi return 0 } check_vpn_connectivity() { local interface="$1" log_step "Проверка связности через VPN интерфейс" log_info "Проверка подключения через '$interface'..." local test_urls="http://connectivitycheck.gstatic.com/generate_204 http://www.msftconnecttest.com/connecttest.txt http://detectportal.firefox.com/success.txt" local connection_ok=0 local last_response_code="" for test_url in $test_urls; do local response_code=$(curl -s -o /dev/null -w "%{http_code}" --interface "$interface" --connect-timeout 10 --max-time 15 "$test_url" 2>/dev/null) last_response_code="$response_code" if [ "$response_code" = "204" ] || [ "$response_code" = "200" ]; then log_success "Связь через интерфейс '$interface' работает" connection_ok=1 break elif [ -n "$response_code" ] && [ "$response_code" != "000" ]; then log_success "Связь через интерфейс '$interface' работает (HTTP $response_code)" connection_ok=1 break fi done if [ $connection_ok -eq 0 ]; then log_error "Нет связи через интерфейс '$interface'" log_info "Проверено несколько тестовых ресурсов - все недоступны" log_info "Возможные причины:" log_info " - VPN подключение не установлено" log_info " - Проблемы с маршрутизацией" log_info " - Firewall блокирует исходящие соединения" return 1 fi return 0 } check_ipset_exists() { local ipset_name="$1" log_step "Проверка создания ipset" if ipset list "$ipset_name" >/dev/null 2>&1; then local count=$(ipset list "$ipset_name" | awk '/^Number of entries:/ {print $4; exit}') if [ -z "$count" ]; then count="0" fi log_success "IPSet '$ipset_name' создан (записей: $count)" local ipset_header=$(ipset list "$ipset_name" | grep '^Header:') local ipset_has_timeout=0 if echo "$ipset_header" | grep -q 'timeout'; then ipset_has_timeout=1 fi if [ "$IPSET_ENABLE_TIMEOUT" = "true" ] && [ $ipset_has_timeout -eq 0 ]; then log_error "Несоответствие конфигурации ipset '$ipset_name'" log_info "В конфигурации: IpsetEnableTimeout=true (таймаут включен)" log_info "В системе: ipset без таймаута" log_info "Необходимо перезагрузить роутер для применения настроек" return 1 elif [ "$IPSET_ENABLE_TIMEOUT" = "false" ] && [ $ipset_has_timeout -eq 1 ]; then log_error "Несоответствие конфигурации ipset '$ipset_name'" log_info "В конфигурации: IpsetEnableTimeout=false (таймаут отключен)" log_info "В системе: ipset с таймаутом" log_info "Необходимо перезагрузить роутер для применения настроек" return 1 fi else log_error "IPSet '$ipset_name' не создан" return 1 fi local ipset_name_v6="${ipset_name}v6" if ipset list "$ipset_name_v6" >/dev/null 2>&1; then local count=$(ipset list "$ipset_name_v6" | awk '/^Number of entries:/ {print $4; exit}') if [ -z "$count" ]; then count="0" fi log_success "IPSet '$ipset_name_v6' создан (записей: $count)" local ipset_header=$(ipset list "$ipset_name_v6" | grep '^Header:') local ipset_has_timeout=0 if echo "$ipset_header" | grep -q 'timeout'; then ipset_has_timeout=1 fi if [ "$IPSET_ENABLE_TIMEOUT" = "true" ] && [ $ipset_has_timeout -eq 0 ]; then log_error "Несоответствие конфигурации ipset '$ipset_name_v6'" log_info "В конфигурации: IpsetEnableTimeout=true (таймаут включен)" log_info "В системе: ipset без таймаута" log_info "Необходимо перезагрузить роутер для применения настроек" return 1 elif [ "$IPSET_ENABLE_TIMEOUT" = "false" ] && [ $ipset_has_timeout -eq 1 ]; then log_error "Несоответствие конфигурации ipset '$ipset_name_v6'" log_info "В конфигурации: IpsetEnableTimeout=false (таймаут отключен)" log_info "В системе: ipset с таймаутом" log_info "Необходимо перезагрузить роутер для применения настроек" return 1 fi else log_warn "IPSet '$ipset_name_v6' не создан (IPv6)" fi return 0 } check_nflog_rules() { log_step "Проверка правил NFLOG для мониторинга DNS" local nflog_found_ipv4=0 local nflog_found_ipv6=0 if iptables -w -t mangle -S OUTPUT 2>/dev/null | grep -q -- "--nflog-group 100"; then nflog_found_ipv4=1 log_success "Правила NFLOG для мониторинга DNS (IPv4) найдены" else log_warn "Правила NFLOG для мониторинга DNS (IPv4) отсутствуют" fi if ip6tables -w -t mangle -S OUTPUT 2>/dev/null | grep -q -- "--nflog-group 100"; then nflog_found_ipv6=1 log_success "Правила NFLOG для мониторинга DNS (IPv6) найдены" else log_warn "Правила NFLOG для мониторинга DNS (IPv6) отсутствуют" fi return 0 } check_iptables_rules() { local target_name="$1" local target_type="$2" log_step "Проверка правил iptables" local ipset_name="$target_name" local found_ipv4=0 local found_ipv6=0 if iptables -w -t mangle -S PREROUTING 2>/dev/null | grep -q -- "--match-set $ipset_name "; then found_ipv4=1 log_success "Правила iptables для '$ipset_name' (IPv4) найдены" local rule_count=$(iptables -w -t mangle -S PREROUTING 2>/dev/null | grep -c -- "--match-set $ipset_name " || echo "0") log_info "Количество правил IPv4: $rule_count" else log_error "Правила iptables для '$ipset_name' (IPv4) не найдены" return 1 fi if ip6tables -w -t mangle -S PREROUTING 2>/dev/null | grep -q -- "--match-set ${ipset_name}v6 "; then found_ipv6=1 log_success "Правила ip6tables для '${ipset_name}v6' (IPv6) найдены" local rule_count=$(ip6tables -w -t mangle -S PREROUTING 2>/dev/null | grep -c -- "--match-set ${ipset_name}v6 " || echo "0") log_info "Количество правил IPv6: $rule_count" else log_warn "Правила ip6tables для '${ipset_name}v6' (IPv6) не найдены" fi return 0 } resolve_domain() { local domain="$1" log_step "Проверка DNS разрешения домена" log_info "Разрешение домена '$domain'..." local ip_address="" if command -v nslookup >/dev/null 2>&1; then local nslookup_output=$(nslookup "$domain" 2>/dev/null) ip_address=$(echo "$nslookup_output" | awk 'BEGIN {found_name=0; ipv4=""; ipv6=""} /^Name:/ {found_name=1; next} found_name && /^Address [0-9]+:/ { ip=$3 if (ip !~ /:/) { if (ipv4 == "") ipv4=ip } else { if (ipv6 == "") ipv6=ip } } END { if (ipv4 != "") print ipv4 else if (ipv6 != "") print ipv6 }') elif command -v host >/dev/null 2>&1; then ip_address=$(host "$domain" 2>/dev/null | awk '/has address/ {print $4; exit} /has IPv6 address/ {print $5; exit}') elif command -v dig >/dev/null 2>&1; then ip_address=$(dig +short "$domain" 2>/dev/null | grep -v ':' | head -n1) if [ -z "$ip_address" ]; then ip_address=$(dig +short "$domain" 2>/dev/null | head -n1) fi else log_error "Не найден ни один инструмент DNS разрешения (nslookup, host, dig)" return 1 fi ip_address=$(echo "$ip_address" | head -n1 | tr -d '\n\r') if [ -z "$ip_address" ]; then log_error "Не удалось разрешить домен '$domain'" return 1 fi log_info "Получен IP адрес: $ip_address" local br0_ip=$(ip addr show br0 2>/dev/null | awk '/inet / {print $2}' | cut -d'/' -f1) local all_dns_ips="127.0.0.1 0.0.0.0 $br0_ip $KNOWN_DNS_IPS" for dns_ip in $all_dns_ips; do if [ "$ip_address" = "$dns_ip" ]; then log_error "Домен разрешается некорректно (получен IP DNS сервера: $ip_address)" return 1 fi done if echo "$ip_address" | grep -q '^10\.\|^192\.168\.\|^172\.1[6-9]\.\|^172\.2[0-9]\.\|^172\.3[0-1]\.'; then log_error "Домен разрешается в приватный IP адрес: $ip_address" return 1 fi if echo "$ip_address" | grep -qE '^(127\.|0\.0\.0\.0)'; then log_error "Домен разрешается в локальный/некорректный IP адрес: $ip_address" return 1 fi log_success "Домен в IP разрешается корректно" echo "$ip_address" } check_ip_in_ipset() { local ipset_name="$1" local ip_address="$2" log_step "Проверка добавления IP в ipset" log_info "Ожидание обработки DNS ответа hrneo..." local max_attempts=5 local attempt=0 local found=0 while [ $attempt -lt $max_attempts ]; do attempt=$((attempt + 1)) sleep 1 if ipset test "$ipset_name" "$ip_address" >/dev/null 2>&1; then found=1 break fi if [ $attempt -lt $max_attempts ]; then log_info "Попытка $attempt/$max_attempts - IP ещё не добавлен, ожидание..." fi done if [ $found -eq 1 ]; then log_success "IP адрес $ip_address добавлен в ipset '$ipset_name'" else log_error "IP адрес $ip_address НЕ добавлен в ipset '$ipset_name' после $max_attempts попыток" log_info "Сервис hrneo не обнаружил DNS ответ, возможная причина:" log_info " - DNS-запрос не прошел через интерфейс br0. Вероятна утечка DNS" log_info " через VPN подклчюение, проверьте настройки маршрутизации DNS." return 1 fi return 0 } check_routing() { local ip_address="$1" local target_name="$2" local target_type="$3" log_step "Проверка конфигурации маршрутизации" if [ "$target_type" = "policy" ]; then local response=$(curl -s "$API_URL" 2>/dev/null || echo "{}") local mark=$(echo "$response" | jq -r ".\"$target_name\".mark // empty" 2>/dev/null) local table=$(echo "$response" | jq -r ".\"$target_name\".table4 // empty" 2>/dev/null) local gateway=$(echo "$response" | jq -r ".\"$target_name\" | .. | .route? // empty | .[] | select(.destination == \"0.0.0.0/0\") | .gateway" 2>/dev/null | head -n1) local vpn_interface=$(echo "$response" | jq -r ".\"$target_name\" | .. | .route? // empty | .[] | select(.destination == \"0.0.0.0/0\") | .interface" 2>/dev/null | head -n1) log_success "Конфигурация политики '$target_name':" [ -n "$mark" ] && [ "$mark" != "empty" ] && log_info " • fwmark: $mark" [ -n "$table" ] && [ "$table" != "empty" ] && log_info " • Таблица маршрутизации: $table" [ -n "$vpn_interface" ] && [ "$vpn_interface" != "empty" ] && log_info " • VPN интерфейс: $vpn_interface" [ -n "$gateway" ] && [ "$gateway" != "empty" ] && [ "$gateway" != "0.0.0.0" ] && log_info " • Шлюз: $gateway" else local interface_gateway="" local route_via_interface=$(ip route show dev "$target_name" 2>/dev/null | grep 'via' | head -n1) if [ -n "$route_via_interface" ]; then interface_gateway=$(echo "$route_via_interface" | awk '{print $3}') fi if [ -z "$interface_gateway" ]; then local default_route=$(ip route show table all dev "$target_name" 2>/dev/null | grep 'default' | head -n1) if [ -n "$default_route" ]; then interface_gateway=$(echo "$default_route" | awk '{print $3}') fi fi log_success "Конфигурация интерфейса '$target_name':" [ -n "$interface_gateway" ] && log_info " • Шлюз: $interface_gateway" fi return 0 } main() { echo "==========================================" >&2 echo " Диагностика Hydra Route Neo (hrneo)" >&2 echo "==========================================" >&2 echo "" >&2 check_hrneo_installed || exit 1 check_critical_files || exit 1 check_nflog_module || exit 1 check_conflicts || exit 1 check_dependencies read_config printf "\nВведите домен для диагностики: " >&2 read domain if [ -z "$domain" ]; then log_error "Домен не может быть пустым" exit 1 fi target_name=$(check_domain_in_config "$domain") || exit 1 target_type=$(classify_target "$target_name") || exit 1 restart_service || exit 1 if [ "$target_type" = "policy" ]; then check_policy_created "$target_name" || exit 1 interface=$(get_interface_from_policy "$target_name") || exit 1 if [ "$interface" != "unknown" ]; then check_interface_exists "$interface" || exit 1 check_vpn_protocol "$interface" || exit 1 check_vpn_connectivity "$interface" || exit 1 fi else check_interface_exists "$target_name" || exit 1 check_vpn_protocol "$target_name" || exit 1 check_vpn_connectivity "$target_name" || exit 1 fi check_ipset_exists "$target_name" || exit 1 check_iptables_rules "$target_name" "$target_type" || exit 1 check_nflog_rules ip_address=$(resolve_domain "$domain") || exit 1 check_ip_in_ipset "$target_name" "$ip_address" || exit 1 check_routing "$ip_address" "$target_name" "$target_type" || exit 1 show_device_routing_table echo "" >&2 echo "==========================================" >&2 log_success "ПРОВЕРКА ЗАВЕРШЕНА" echo "==========================================" >&2 echo "" >&2 log_info "Конфигурация выборочной маршрутизации на роутере корректна." echo "" >&2 log_info "Диагностика на роутере проверяет только конфигурацию, но не может" log_info "проверить маршрутизацию на клиентских устройствах (ПК/смарфон/Smatr-TV)" echo "" >&2 log_info "Для проверки маршрутизации выполните с клиентского устройства:" log_info " Windows: tracert $domain" log_info " Linux/Mac: traceroute $domain" echo "" >&2 log_info "Ожидаемый результат:" log_info " 1-й хоп: IP роутера" log_info " 2-й хоп: VPN шлюз" echo "" >&2 log_info "ВАЖНО: traceroute НЕ работает для Proxy интерфейсов (vless, vmess," log_info "trojan, ss, socks, http) из-за отсутствия поддержки ICMP протокола." echo "" >&2 log_info "Если маршрутизация не работает, проверьте настройки устройства:" log_info " - DNS сервер: IP роутера" log_info " - Браузер/смартфон НЕ используют собственный DNS сервер" log_info " - Отсутствие включенного VPN/прокси на устройстве" echo "" >&2 } show_device_routing_table() { log_step "Устройства и сегменты сети" local RCI="localhost:79/rci" local hotspot_json=$(curl -s "$RCI/show/ip/hotspot" 2>/dev/null) local config_data=$(curl -s "$RCI/show/running-config" 2>/dev/null) if [ -z "$hotspot_json" ] || [ -z "$config_data" ]; then log_warn "Не удалось получить данные о хостах и сегментах сети" return 0 fi echo "$config_data" | jq -r '.message[]' | awk ' BEGIN { OFS="\t" } { sub(/\r/, "") } /^ip policy / { last_policy = $3 } /description / && last_policy != "" { desc = $0; sub(/^[ \t]*description /, "", desc); gsub(/"/, "", desc); print "N", last_policy, desc; } /^!/ { last_policy = "" } /^interface / { cur_iface = $2 } /description / && cur_iface != "" { desc = $0; sub(/^[ \t]*description /, "", desc); gsub(/"/, "", desc); print "D", cur_iface, desc; } /^interface Bridge/ || /^interface GuestBridge/ { print "I", $2; } /^!/ { cur_iface = "" } /^ip hotspot/ { in_hs = 1 } in_hs && /host [0-9a-fA-F:]{17} policy / { match($0, /[0-9a-fA-F:]{17}/); mac = substr($0, RSTART, RLENGTH); temp = $0; sub(/.*policy /, "", temp); split(temp, a, " "); print "B", mac, a[1]; } in_hs && /policy / && !/host / { split($0, parts, " "); for (i=1; i<=NF; i++) { if ($i == "policy") { iface = $(i+1); mode = $(i+2); if (iface != "") print "S", iface, mode; break; } } } /^!/ { in_hs = 0 } ' | jq -R -s -r --argjson hs "$hotspot_json" ' (split("\n") | map(select(length > 0) | split("\t")) | reduce .[] as $row ( {names:{}, bindings:{}, iface_descs:{}, iface_pols:{}, bridges:[]}; if $row[0] == "N" then .names[$row[1]] = $row[2] elif $row[0] == "D" then .iface_descs[$row[1]] = $row[2] elif $row[0] == "I" then .bridges += [$row[1]] elif $row[0] == "B" then .bindings[$row[1]] = $row[2] elif $row[0] == "S" then .iface_pols[$row[1]] = $row[2] else . end )) as $rules | ( [ $hs.host[] | { name: ((.name | select(length > 0)) // (.hostname | select(length > 0)) // "Без имени"), info: ("IP: " + (.ip // "-") + " | MAC: " + .mac), active: .active, type: "host", policy_id: ($rules.bindings[.mac] // "default") } ] + [ ($rules.bridges | unique)[] | { key: ., pol: ($rules.iface_pols[.] // "default") } | { name: ($rules.iface_descs[.key] // .key), info: ("Интерфейс: " + .key), active: true, type: "segment", policy_id: (if .pol == "permit" then "default" else .pol end) } ] ) | map( .policy_name = ( if .policy_id == "default" then "default" else ($rules.names[.policy_id] // .policy_id) end ) ) | { default: map(select(.policy_name == "default")), other: map(select(.policy_name != "default")) | group_by(.policy_name) } | if (.default | length) > 0 then ( "МАРШРУТИЗАЦИЯ АКТИВНА ДЛЯ ЭТИХ УСТРОЙСТВ И СЕГМЕНТОВ СЕТИ:", "==================================================", ( .default | sort_by( (.type != "segment"), (.active | not), .name )[] | if .type == "segment" then " [СЕГМЕНТ] \(.name)", " \(.info)" else " • \(.name)", " \(.info) | \((if .active then "ONLINE" else "offline" end))" end ), "" ) else empty end, if (.other | length) > 0 then ( "МАРШРУТИЗАЦИЯ ->НЕ<- АКТИВНА ДЛЯ ЭТИХ УСТРОЙСТВ И СЕГМЕНТОВ СЕТИ:", "==================================================", ( .other[] | "Политика: \(.[0].policy_name)", "--------------------------------------------------", ( sort_by( (.type != "segment"), (.active | not), .name )[] | if .type == "segment" then " [СЕГМЕНТ] \(.name)", " \(.info)" else " • \(.name)", " \(.info) | \((if .active then "ONLINE" else "offline" end))" end ), "" ) ) else empty end ' >&2 return 0 } main