#!/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