release: 1.3.24 — тест утечки IP/DNS из роутера, оптимизация запуска списков хостов и локализация
- feat(diagnostics): router-level IP & DNS leak detection * одновременный опрос прямого WAN сокета (SO_MARK 0x08000000) и локального прокси (127.0.0.1:4534) * выявление утечки реального публичного IP адреса * тестирование восходящих DNS-резолверов по протоколу bash.ws и обнаружение DNS провайдера * CLI команды: tachyon leak_check [--pretty], check_ip_leak, check_dns_leak * LuCI модальное окно с детализированными таблицами изоляции IP и анализа резолверов - fix(hosts): устранение ложного предупреждения [warn] No hosts list URLs configured * запуск HOSTS_UC в lifecycle.uc только при наличии активных списков хостов * hosts.uc возвращает success:true и код 0 без лишних записей в syslog при отсутствии URL * добавлена проверка has-hosts-list-update-sources в state.uc - i18n: полная синхронизация русской локализации в каталогах gettext PO/POT - bump: версия пакета 1.3.24
This commit is contained in:
+2281
-1951
File diff suppressed because it is too large
Load Diff
+2149
-1942
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-08 09:57+0300\n"
|
||||
"PO-Revision-Date: 2026-09-08 09:57+0300\n"
|
||||
"POT-Creation-Date: 2026-09-09 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-09 17:09+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -50,6 +50,12 @@ msgstr "⚠️ Бенчмарк завершен. Не найдено рабоч
|
||||
msgid "⚠️ Benchmark stopped"
|
||||
msgstr "⚠️ Бенчмарк остановлен"
|
||||
|
||||
msgid "⚠️ CRITICAL IP LEAK: Your real public IP is exposed through the proxy outbound!"
|
||||
msgstr "⚠️ КРИТИЧЕСКАЯ УТЕЧКА IP: Ваш реальный публичный IP раскрыт через исходящий прокси!"
|
||||
|
||||
msgid "⚠️ DNS LEAK DETECTED: DNS queries are leaking to your local Internet Service Provider!"
|
||||
msgstr "⚠️ ОБНАРУЖЕНА УТЕЧКА DNS: DNS-запросы утекают к вашему местному интернет-провайдеру!"
|
||||
|
||||
msgid "⚠️ If all Bootstrap DNS fail 3 times, queries will be sent to your ISP's DNS in plaintext. Only use as a last resort to prevent complete internet loss."
|
||||
msgstr "⚠️ Если все Bootstrap DNS завершатся с ошибкой 3 раза, запросы будут отправлены в DNS вашего провайдера в открытом виде. Используйте только в крайнем случае для предотвращения полной потери интернета."
|
||||
|
||||
@@ -155,9 +161,21 @@ msgstr "🛑 Остановить бенчмарк"
|
||||
msgid "🛠️ My Custom Strategies Only"
|
||||
msgstr "🛠️ Только мои стратегии"
|
||||
|
||||
msgid "🛡️ IP & DNS Leak Test"
|
||||
msgstr "🛡️ Проверка утечек IP и DNS"
|
||||
|
||||
msgid "🛡️ SECURE: No DNS leaks detected. All queries resolve through non-ISP upstream resolvers."
|
||||
msgstr "🛡️ ЗАЩИЩЕНО: Утечек DNS не обнаружено. Все запросы обрабатываются независимыми резолверами."
|
||||
|
||||
msgid "🛡️ SECURE: No IP leak detected. Real WAN IP is concealed behind proxy outbound."
|
||||
msgstr "🛡️ ЗАЩИЩЕНО: Утечек IP не обнаружено. Реальный WAN IP скрыт за исходящим прокси."
|
||||
|
||||
msgid "🟣 Twitch Suite (Live Stream + HLS CDN)"
|
||||
msgstr "🟣 Twitch Набор (Прямой эфир + HLS CDN)"
|
||||
|
||||
msgid "127.0.0.1:4534 (sing-box mixed)"
|
||||
msgstr "127.0.0.1:4534 (смешанный sing-box)"
|
||||
|
||||
msgid "Блокировка по IP (Таймаут TCP SYN)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
@@ -485,6 +503,9 @@ msgstr "Генератор AmneziaWG"
|
||||
msgid "AmneziaWG Version"
|
||||
msgstr "Версия AmneziaWG"
|
||||
|
||||
msgid "An unexpected error occurred during test"
|
||||
msgstr "Произошла непредвиденная ошибка во время проверки"
|
||||
|
||||
msgid "Anonymized support report copied to clipboard"
|
||||
msgstr "Анонимизированный отчет скопирован в буфер обмена"
|
||||
|
||||
@@ -641,6 +662,9 @@ msgstr "Ср. задержка"
|
||||
msgid "Backend validation failed: %s"
|
||||
msgstr "Ошибка проверки бэкенда: %s"
|
||||
|
||||
msgid "Baseline"
|
||||
msgstr "Базовый уровень"
|
||||
|
||||
msgid "Basic connectivity"
|
||||
msgstr "Базовая связность"
|
||||
|
||||
@@ -791,6 +815,9 @@ msgstr "Стратегия ByeDPI"
|
||||
msgid "ByeDPI strategy cannot be empty"
|
||||
msgstr "Стратегия ByeDPI не может быть пустой"
|
||||
|
||||
msgid "Bypasses proxy (SO_MARK 0x08000000)"
|
||||
msgstr "В обход прокси (SO_MARK 0x08000000)"
|
||||
|
||||
msgid "Bypasses UDP traffic for selected game consoles (PS5/Xbox) to achieve NAT Type 1 / 2 (Full Cone NAT) for P2P matchmaking, while keeping TCP (PSN/Auth) routed through the proxy."
|
||||
msgstr "Обходит UDP-трафик для выбранных игровых консолей (PS5/Xbox) для достижения NAT Type 1 / 2 (Full Cone NAT) для P2P-матчмейкинга, сохраняя TCP (PSN/Auth) через прокси."
|
||||
|
||||
@@ -1055,6 +1082,9 @@ msgstr "Подключено"
|
||||
msgid "Connected Devices"
|
||||
msgstr "Подключенные устройства"
|
||||
|
||||
msgid "Connection Path"
|
||||
msgstr "Маршрут соединения"
|
||||
|
||||
msgid "Connection URL"
|
||||
msgstr "URL подключения"
|
||||
|
||||
@@ -1100,6 +1130,12 @@ msgstr "Основные настройки и параметры"
|
||||
msgid "Could not compare FakeIP and control public IPs"
|
||||
msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки"
|
||||
|
||||
msgid "Could not contact leak test endpoints. Please ensure router has internet access."
|
||||
msgstr "Не удалось связаться с серверами проверки утечек. Убедитесь, что у роутера есть доступ в интернет."
|
||||
|
||||
msgid "Country"
|
||||
msgstr "Страна"
|
||||
|
||||
msgid "Country code (e.g. ru, us, de)"
|
||||
msgstr "Код страны (напр. ru, us, de)"
|
||||
|
||||
@@ -1271,6 +1307,9 @@ msgstr "Прямой прокси"
|
||||
msgid "Direct WAN"
|
||||
msgstr "Прямой WAN"
|
||||
|
||||
msgid "Direct WAN (ISP)"
|
||||
msgstr "Напрямую в WAN (провайдер)"
|
||||
|
||||
msgid "Disable"
|
||||
msgstr "Отключить"
|
||||
|
||||
@@ -1382,6 +1421,9 @@ msgstr "Быстрый DNS-кэш"
|
||||
msgid "DNS Unavailability Timeout"
|
||||
msgstr "Таймаут недоступности DNS"
|
||||
|
||||
msgid "DNS Upstream Resolver Analysis (bash.ws protocol)"
|
||||
msgstr "Анализ восходящих DNS-резолверов (протокол bash.ws)"
|
||||
|
||||
msgid "Do not panic, everything can be fixed, just..."
|
||||
msgstr "Не паникуйте, всё можно исправить, просто..."
|
||||
|
||||
@@ -1524,7 +1566,7 @@ msgid "e.g. SHA256 — leave empty for default"
|
||||
msgstr "напр. SHA256 — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "Each entry must be \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp), e.g. tcp:6881,udp:6881-6889"
|
||||
msgstr "Каждая запись — \"proto\", \"proto:port\" или \"proto:port-port\" (tcp/udp), напр. tcp:6881,udp:6881-6889"
|
||||
msgstr "Каждая запись должна быть \"proto\", \"proto:port\" или \"proto:port-port\" (tcp/udp), например: tcp:6881,udp:6881-6889"
|
||||
|
||||
msgid "Edit"
|
||||
msgstr "Изменить"
|
||||
@@ -1802,6 +1844,9 @@ msgstr "Не удалось определить тип DPI"
|
||||
msgid "Failed to execute"
|
||||
msgstr "Не удалось выполнить"
|
||||
|
||||
msgid "Failed to execute IP and DNS leak check"
|
||||
msgstr "Не удалось выполнить проверку утечек IP и DNS"
|
||||
|
||||
msgid "Failed to fetch releases"
|
||||
msgstr "Не удалось получить список релизов"
|
||||
|
||||
@@ -2348,6 +2393,9 @@ msgstr "Полезный груз init (I1)"
|
||||
msgid "Init timeout (Itime)"
|
||||
msgstr "Таймаут init (Itime)"
|
||||
|
||||
msgid "Initializing router-level IP & DNS leak test..."
|
||||
msgstr "Инициализация проверки утечек IP и DNS с роутера..."
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
@@ -2517,7 +2565,7 @@ msgid "Invalid Shadowsocks URL: missing credentials"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
|
||||
msgstr "Неверный Shadowsocks URL: отсутствует разделитель метода и пароля \":\""
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing port"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует порт"
|
||||
@@ -2660,6 +2708,12 @@ msgstr "Изолировать трафик BitTorrent и направить е
|
||||
msgid "Isolate Local LAN"
|
||||
msgstr "Изолировать локальную сеть"
|
||||
|
||||
msgid "ISP / Organization"
|
||||
msgstr "Провайдер / Организация"
|
||||
|
||||
msgid "ISP DNS LEAK"
|
||||
msgstr "УТЕЧКА DNS К ПРОВАЙДЕРУ"
|
||||
|
||||
msgid "Issues"
|
||||
msgstr "Проблемы"
|
||||
|
||||
@@ -2780,6 +2834,15 @@ msgstr "Установлена последняя версия"
|
||||
msgid "Latest version:"
|
||||
msgstr "Последняя версия:"
|
||||
|
||||
msgid "Leak detection completed"
|
||||
msgstr "Проверка утечек завершена"
|
||||
|
||||
msgid "Leak detection failed to complete"
|
||||
msgstr "Не удалось завершить проверку утечек"
|
||||
|
||||
msgid "LEAKED"
|
||||
msgstr "УТЕЧКА"
|
||||
|
||||
msgid "Leave empty to omit this header"
|
||||
msgstr "Оставьте пустым, чтобы пропустить этот заголовок"
|
||||
|
||||
@@ -2858,6 +2921,9 @@ msgstr "Загрузка..."
|
||||
msgid "Local Address"
|
||||
msgstr "Локальный адрес"
|
||||
|
||||
msgid "Location"
|
||||
msgstr "Местоположение"
|
||||
|
||||
msgid "Log Level"
|
||||
msgstr "Уровень логирования"
|
||||
|
||||
@@ -3078,13 +3144,13 @@ msgid "No active routing sections found."
|
||||
msgstr "Активные секции маршрутизации не найдены."
|
||||
|
||||
msgid "No benchmark data yet. Click \"Start Benchmark\" to begin."
|
||||
msgstr "Нет данных бенчмарка. Нажмите «Начать тест», чтобы запустить проверку."
|
||||
msgstr "Данные тестирования отсутствуют. Нажмите \"Запустить тест\" для начала."
|
||||
|
||||
msgid "No benchmark history yet. Run a benchmark to see results here."
|
||||
msgstr "История бенчмарков пуста. Запустите бенчмарк, чтобы увидеть результаты здесь."
|
||||
|
||||
msgid "No benchmark results yet. Configure options and click \"Start Benchmark\"."
|
||||
msgstr "Результатов бенчмарка пока нет. Настройте параметры и нажмите «Запустить бенчмарк»."
|
||||
msgstr "Результаты тестирования пока отсутствуют. Настройте параметры и нажмите \"Запустить тест\"."
|
||||
|
||||
msgid "No check targets found in the configuration."
|
||||
msgstr "Цели для проверки не найдены в конфигурации."
|
||||
@@ -3113,6 +3179,12 @@ msgstr "DHCP-клиенты не обнаружены"
|
||||
msgid "No diagnostic history available yet."
|
||||
msgstr "История проверок пока пуста."
|
||||
|
||||
msgid "No DNS resolvers captured via proxy test. Proxy may be offline or blocking test subdomains."
|
||||
msgstr "В тесте через прокси резолверы не зафиксированы. Возможно, прокси не работает или блокирует тестовые поддомены."
|
||||
|
||||
msgid "No DNS resolvers recorded"
|
||||
msgstr "DNS-резолверы не зафиксированы"
|
||||
|
||||
msgid "No domains detected yet"
|
||||
msgstr "Домены пока не обнаружены"
|
||||
|
||||
@@ -3242,6 +3314,12 @@ msgstr "Обфускация"
|
||||
msgid "Obfuscation password"
|
||||
msgstr "Пароль обфускации"
|
||||
|
||||
msgid "Observed Public IP"
|
||||
msgstr "Внешний публичный IP"
|
||||
|
||||
msgid "OFFLINE"
|
||||
msgstr "ОФЛАЙН"
|
||||
|
||||
msgid "Ollama (Local PC / Server)"
|
||||
msgstr "Ollama (локальный ПК / сервер)"
|
||||
|
||||
@@ -3312,13 +3390,13 @@ msgid "Optimize RAM memory"
|
||||
msgstr "Оптимизировать память (RAM)"
|
||||
|
||||
msgid "Option \"%s\" contains an invalid input value."
|
||||
msgstr "Параметр \"%s\" содержит недопустимое значение."
|
||||
msgstr "Опция \"%s\" содержит недопустимое значение."
|
||||
|
||||
msgid "Optional — leave empty for default"
|
||||
msgstr "Опционально — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "Optional context (e.g. \"Rostelecom, YouTube 4K buffering is slow\")..."
|
||||
msgstr "Дополнительный контекст (напр. «Ростелеком, YouTube 4K долго буферизуется»)..."
|
||||
msgstr "Дополнительный контекст (напр. \"Ростелеком, долго грузится YouTube 4K\")..."
|
||||
|
||||
msgid "Optional tls-auth or tls-crypt key"
|
||||
msgstr "Опциональный ключ tls-auth или tls-crypt"
|
||||
@@ -3440,6 +3518,9 @@ msgstr "Ожидает запуска"
|
||||
msgid "Pending..."
|
||||
msgstr "Ожидание..."
|
||||
|
||||
msgid "Performs simultaneous outbound checks via direct WAN (bypassing Sing-box redirect) and via proxy (127.0.0.1:4534) to verify that your real IP and DNS queries are not leaking to your ISP."
|
||||
msgstr "Выполняет одновременные исходящие запросы напрямую в WAN (в обход правил Sing-box) и через прокси (127.0.0.1:4534), проверяя, что ваш реальный IP и DNS-запросы не утекают провайдеру."
|
||||
|
||||
msgid "Periodically check the current level and switch to a faster server even when the current one works."
|
||||
msgstr "Периодически проверять текущий уровень и переключаться на более быстрый сервер, даже если текущий работает."
|
||||
|
||||
@@ -3462,7 +3543,7 @@ msgid "Pin on dashboard"
|
||||
msgstr "Закрепить на дашборде"
|
||||
|
||||
msgid "Pin P2P isolation to the torrent client's own ports, comma separated. Each entry is \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp). Without a port the whole protocol is routed direct."
|
||||
msgstr "Закрепить изоляцию P2P за портами торрент-клиента, через запятую. Каждая запись — \"протокол\", \"протокол:порт\" или \"протокол:порт-порт\" (tcp/udp). Без порта весь протокол уходит напрямую."
|
||||
msgstr "Привязка изоляции P2P к портам торрент-клиента через запятую. Каждая запись: \"proto\", \"proto:port\" или \"proto:port-port\" (tcp/udp). Без указания порта весь протокол направляется напрямую."
|
||||
|
||||
msgid "Plain-UDP DNS servers used as a last resort when all primary DNS fail. These are always unencrypted (no DoH/DoT). Example: 1.1.1.1, 8.8.8.8. Unlike the ISP fallback toggle, these are your own chosen servers."
|
||||
msgstr "Обычные UDP DNS-серверы, используемые как резерв при недоступности всех основных DNS. Всегда работают без шифрования (без DoH/DoT). Пример: 1.1.1.1, 8.8.8.8. В отличие от аварийного DNS провайдера, это выбранные вами серверы."
|
||||
@@ -3620,9 +3701,15 @@ msgstr "Интервал проверки здоровья прокси (с)"
|
||||
msgid "Proxy Health Check URL"
|
||||
msgstr "URL проверки здоровья прокси"
|
||||
|
||||
msgid "Proxy is offline or unreachable on 127.0.0.1:4534."
|
||||
msgstr "Прокси выключен или недоступен на 127.0.0.1:4534."
|
||||
|
||||
msgid "Proxy link is unavailable"
|
||||
msgstr "Ссылка прокси недоступна"
|
||||
|
||||
msgid "Proxy Outbound"
|
||||
msgstr "Исходящий прокси (туннель)"
|
||||
|
||||
msgid "Proxy Section for Bot"
|
||||
msgstr "Секция прокси для бота"
|
||||
|
||||
@@ -3635,9 +3722,15 @@ msgstr "PSK"
|
||||
msgid "Public host"
|
||||
msgstr "Публичный хост"
|
||||
|
||||
msgid "Public IP Address Isolation"
|
||||
msgstr "Изоляция публичного IP-адреса"
|
||||
|
||||
msgid "QR code is too large"
|
||||
msgstr "QR-код слишком большой"
|
||||
|
||||
msgid "Querying WAN direct socket and proxy outbound on 127.0.0.1:4534..."
|
||||
msgstr "Опрос прямого WAN-сокета и исходящего прокси на 127.0.0.1:4534..."
|
||||
|
||||
msgid "QUIC (HTTP/3)"
|
||||
msgstr "QUIC (HTTP/3)"
|
||||
|
||||
@@ -3653,6 +3746,9 @@ msgstr "Оценка"
|
||||
msgid "RDP"
|
||||
msgstr "RDP"
|
||||
|
||||
msgid "Re-run Leak Test"
|
||||
msgstr "Повторить проверку утечек"
|
||||
|
||||
msgid "Read error"
|
||||
msgstr "Ошибка чтения"
|
||||
|
||||
@@ -3791,6 +3887,9 @@ msgstr "Определить шлюз"
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr "Разрешать реальный IP для маршрутизации"
|
||||
|
||||
msgid "Resolver IP"
|
||||
msgstr "IP резолвера"
|
||||
|
||||
msgid "Resource selection by hostname inside nfqws is not supported here; sing-box selects resources before NFQUEUE."
|
||||
msgstr "Выбор ресурсов по имени хоста внутри nfqws не поддерживается здесь; sing-box выбирает ресурсы до NFQUEUE."
|
||||
|
||||
@@ -3884,6 +3983,12 @@ msgstr "Направлять весь не-локальный LAN-трафик
|
||||
msgid "Route bot API requests through this section. Leave empty to use the default mixed proxy."
|
||||
msgstr "Маршрутизировать запросы API бота через эту секцию. Оставьте пустым для использования смешанного прокси по умолчанию."
|
||||
|
||||
msgid "Route community list subnets (IP/CIDR)"
|
||||
msgstr "Маршрутизировать подсети списков сообщества (IP/CIDR)"
|
||||
|
||||
msgid "Route IP subnets associated with built-in community lists (e.g. Discord voice/media). Disable this if community subnets overlap with CDN/Anycast networks (such as Cloudflare/Google Cloud) and interfere with unrelated services (e.g. game launchers)."
|
||||
msgstr "Маршрутизировать IP-подсети из списков сообщества (например, голос/медиа Discord). Отключите, если подсети сообщества пересекаются с CDN/Anycast сетями (Cloudflare/Google Cloud) и мешают другим сервисам."
|
||||
|
||||
msgid "Route main DNS requests through the selected section."
|
||||
msgstr "Маршрутизировать основные DNS-запросы через выбранную секцию."
|
||||
|
||||
@@ -3971,6 +4076,9 @@ msgstr "Выполняется диагностика…"
|
||||
msgid "Russia (ru)"
|
||||
msgstr "Россия (ru)"
|
||||
|
||||
msgid "SAFE"
|
||||
msgstr "БЕЗОПАСНО"
|
||||
|
||||
msgid "SafeSearch"
|
||||
msgstr "SafeSearch"
|
||||
|
||||
@@ -4040,6 +4148,9 @@ msgstr "Секция для регистрации Cloudflare WARP API. Если
|
||||
msgid "Sections"
|
||||
msgstr "Секции"
|
||||
|
||||
msgid "SECURE"
|
||||
msgstr "ЗАЩИЩЕНО"
|
||||
|
||||
msgid "Security"
|
||||
msgstr "Безопасность"
|
||||
|
||||
@@ -4625,6 +4736,9 @@ msgstr "Tachyon: Бенчмарк и автоподбор DNS"
|
||||
msgid "Tachyon Error"
|
||||
msgstr "Ошибка Tachyon"
|
||||
|
||||
msgid "Tachyon IP & DNS Leak Detection"
|
||||
msgstr "Проверка утечек IP и DNS Tachyon"
|
||||
|
||||
msgid "Tachyon manages ciadpi pid files itself, so pidfile options are not allowed here."
|
||||
msgstr "Tachyon управляет файлами pid ciadpi самостоятельно, поэтому параметры pidfile здесь не допускаются."
|
||||
|
||||
@@ -4769,6 +4883,9 @@ msgstr "Тестирование"
|
||||
msgid "Testing bypass strategies in isolated sandbox..."
|
||||
msgstr "Тестирование стратегий обхода в изолированной песочнице..."
|
||||
|
||||
msgid "Testing DNS leak upstream resolvers with bash.ws protocol..."
|
||||
msgstr "Тестирование восходящих DNS-резолверов по протоколу bash.ws..."
|
||||
|
||||
msgid "Testing strategy"
|
||||
msgstr "Тестирование стратегии"
|
||||
|
||||
@@ -5156,6 +5273,9 @@ msgstr "Отдано"
|
||||
msgid "Upstream Mode"
|
||||
msgstr "Режим DNS Upstream"
|
||||
|
||||
msgid "Upstream Provider / ASN"
|
||||
msgstr "Восходящий провайдер / ASN"
|
||||
|
||||
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, hy2://, or tuic://"
|
||||
msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, hy2:// или tuic://"
|
||||
|
||||
@@ -5273,6 +5393,9 @@ msgstr "Значение должно быть положительным цел
|
||||
msgid "Value must not contain control characters"
|
||||
msgstr "Значение не должно содержать символы управления"
|
||||
|
||||
msgid "Verdict"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Verifying router and service readiness..."
|
||||
msgstr "Проверка готовности роутера и служб..."
|
||||
|
||||
@@ -5837,9 +5960,6 @@ msgstr "Маршрут"
|
||||
msgid "Resolved IP / DNS"
|
||||
msgstr "IP / DNS"
|
||||
|
||||
msgid "Verdict"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Check domain or IP (e.g. example.com)..."
|
||||
msgstr "Проверить домен или IP (напр. example.com)..."
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.23",
|
||||
"version": "1.3.24",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1664,4 +1664,28 @@ export const TachyonShellMethods = {
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
leakCheck: async (): Promise<
|
||||
Tachyon.MethodResponse<Tachyon.LeakCheckResult>
|
||||
> => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [Tachyon.AvailableMethods.LEAK_CHECK],
|
||||
timeout: 25000,
|
||||
});
|
||||
const parsed = parseJsonObjectOutput<Tachyon.LeakCheckResult>(
|
||||
response.stdout,
|
||||
);
|
||||
|
||||
if ((response.code ?? 0) !== 0 || !parsed) {
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _('Failed to execute IP and DNS leak check'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: parsed,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
renderServiceCheckModal,
|
||||
renderAiChatModal,
|
||||
renderStrategyFuzzerModal,
|
||||
renderLeakCheckModal,
|
||||
} from './partials';
|
||||
import { TachyonShellMethods } from '../../methods';
|
||||
import { getConfigSections } from '../../methods/custom/getConfigSections';
|
||||
@@ -1584,6 +1585,10 @@ function handleOpenAiChat() {
|
||||
renderAiChatModal();
|
||||
}
|
||||
|
||||
function handleOpenLeakCheck() {
|
||||
renderLeakCheckModal();
|
||||
}
|
||||
|
||||
function handleOpenStrategyFuzzer() {
|
||||
getConfigSections()
|
||||
.then((sections) => {
|
||||
@@ -1919,6 +1924,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
||||
disabled: utilityActionsDisabled,
|
||||
onClick: handleCheckServicesAction,
|
||||
},
|
||||
testLeaks: {
|
||||
visible: true,
|
||||
loading: false,
|
||||
disabled: false,
|
||||
onClick: handleOpenLeakCheck,
|
||||
},
|
||||
viewLogs: {
|
||||
loading: diagnosticsActions.viewLogs.loading,
|
||||
visible: true,
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './renderAiChatModal';
|
||||
export * from './renderStrategyFuzzerModal';
|
||||
export * from './renderSystemInfo';
|
||||
export * from './renderDnsBenchmarkModal';
|
||||
export * from './renderLeakCheckModal';
|
||||
|
||||
@@ -35,6 +35,7 @@ interface IRenderAvailableActionsProps {
|
||||
showSingBoxConfig: ActionProps;
|
||||
generateBugReport: ActionProps;
|
||||
checkServices: ActionProps;
|
||||
testLeaks?: ActionProps;
|
||||
}
|
||||
|
||||
export function renderAvailableActions({
|
||||
@@ -53,6 +54,7 @@ export function renderAvailableActions({
|
||||
showSingBoxConfig,
|
||||
generateBugReport,
|
||||
checkServices,
|
||||
testLeaks,
|
||||
}: IRenderAvailableActionsProps) {
|
||||
return E('div', { class: 'tachyon_diagnostic-page__right-bar__actions' }, [
|
||||
E('b', {}, _('Available actions')),
|
||||
@@ -174,6 +176,16 @@ export function renderAvailableActions({
|
||||
disabled: checkServices.disabled,
|
||||
}),
|
||||
]),
|
||||
...insertIf(!!testLeaks?.visible, [
|
||||
renderButton({
|
||||
classNames: ['cbi-button-action'],
|
||||
onClick: testLeaks!.onClick,
|
||||
icon: renderCircleCheckBigIcon24,
|
||||
text: _('🛡️ IP & DNS Leak Test'),
|
||||
loading: testLeaks!.loading,
|
||||
disabled: testLeaks!.disabled,
|
||||
}),
|
||||
]),
|
||||
...insertIf(viewLogs.visible, [
|
||||
renderButton({
|
||||
onClick: viewLogs.onClick,
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
import { TachyonShellMethods } from '../../../methods/shell';
|
||||
import { renderButton } from '../../../../partials';
|
||||
import { Tachyon } from '../../../types';
|
||||
|
||||
export function renderLeakCheckModal() {
|
||||
let isRunning = false;
|
||||
|
||||
const progressBar = E('div', {
|
||||
style:
|
||||
'width: 0%; height: 6px; background: linear-gradient(90deg, #007bff, #28a745); border-radius: 3px; transition: width 0.4s ease;',
|
||||
});
|
||||
|
||||
const progressContainer = E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'width: 100%; height: 6px; background: rgba(128,128,128,0.2); border-radius: 3px; overflow: hidden; margin-bottom: 14px;',
|
||||
},
|
||||
[progressBar],
|
||||
);
|
||||
|
||||
const statusLabel = E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'font-size: 13px; font-weight: 500; margin-bottom: 12px; color: var(--text-color-medium, #6c757d);',
|
||||
},
|
||||
_('Initializing router-level IP & DNS leak test...'),
|
||||
);
|
||||
|
||||
const resultsContainer = E('div', {
|
||||
style: 'display: none; margin-bottom: 16px;',
|
||||
});
|
||||
|
||||
const startTest = async () => {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
|
||||
resultsContainer.style.display = 'none';
|
||||
progressContainer.style.display = 'block';
|
||||
progressBar.style.width = '30%';
|
||||
statusLabel.textContent = _(
|
||||
'Querying WAN direct socket and proxy outbound on 127.0.0.1:4534...',
|
||||
);
|
||||
|
||||
if (retryBtn) (retryBtn as HTMLButtonElement).disabled = true;
|
||||
|
||||
// Advance progress smoothly
|
||||
const timer = setTimeout(() => {
|
||||
progressBar.style.width = '70%';
|
||||
statusLabel.textContent = _(
|
||||
'Testing DNS leak upstream resolvers with bash.ws protocol...',
|
||||
);
|
||||
}, 1500);
|
||||
|
||||
try {
|
||||
const response = await TachyonShellMethods.leakCheck();
|
||||
clearTimeout(timer);
|
||||
progressBar.style.width = '100%';
|
||||
|
||||
if (response.success && response.data) {
|
||||
statusLabel.textContent = _('Leak detection completed');
|
||||
setTimeout(() => {
|
||||
progressContainer.style.display = 'none';
|
||||
progressBar.style.width = '0%';
|
||||
}, 400);
|
||||
|
||||
renderResults(response.data);
|
||||
} else {
|
||||
const err =
|
||||
!response.success && response.error
|
||||
? response.error
|
||||
: _('Leak detection failed to complete');
|
||||
progressContainer.style.display = 'none';
|
||||
statusLabel.textContent = err;
|
||||
resultsContainer.innerHTML = '';
|
||||
resultsContainer.appendChild(
|
||||
E(
|
||||
'div',
|
||||
{ class: 'alert-message warning' },
|
||||
err ||
|
||||
_(
|
||||
'Could not contact leak test endpoints. Please ensure router has internet access.',
|
||||
),
|
||||
),
|
||||
);
|
||||
resultsContainer.style.display = 'block';
|
||||
}
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
progressContainer.style.display = 'none';
|
||||
statusLabel.textContent = _('An unexpected error occurred during test');
|
||||
resultsContainer.innerHTML = '';
|
||||
resultsContainer.appendChild(
|
||||
E(
|
||||
'div',
|
||||
{ class: 'alert-message warning' },
|
||||
e instanceof Error ? e.message : String(e),
|
||||
),
|
||||
);
|
||||
resultsContainer.style.display = 'block';
|
||||
} finally {
|
||||
isRunning = false;
|
||||
if (retryBtn) (retryBtn as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderResults = (data: Tachyon.LeakCheckResult) => {
|
||||
resultsContainer.innerHTML = '';
|
||||
const { ip_leak, dns_leak } = data;
|
||||
|
||||
// --- 1. IP Leak Section ---
|
||||
const ipAlertClass = !ip_leak.proxy_online
|
||||
? 'alert-message warning'
|
||||
: ip_leak.leaked
|
||||
? 'alert-message danger'
|
||||
: 'alert-message success';
|
||||
|
||||
const ipAlertText = !ip_leak.proxy_online
|
||||
? _('Proxy is offline or unreachable on 127.0.0.1:4534.')
|
||||
: ip_leak.leaked
|
||||
? _(
|
||||
'⚠️ CRITICAL IP LEAK: Your real public IP is exposed through the proxy outbound!',
|
||||
)
|
||||
: _(
|
||||
'🛡️ SECURE: No IP leak detected. Real WAN IP is concealed behind proxy outbound.',
|
||||
);
|
||||
|
||||
const ipTable = E(
|
||||
'table',
|
||||
{
|
||||
class: 'table cbi-section-table',
|
||||
style: 'width: 100%; margin-bottom: 12px; font-size: 12px;',
|
||||
},
|
||||
[
|
||||
E('thead', {}, [
|
||||
E('tr', { class: 'tr cbi-section-table-titles' }, [
|
||||
E('th', { class: 'th' }, _('Connection Path')),
|
||||
E('th', { class: 'th' }, _('Observed Public IP')),
|
||||
E('th', { class: 'th' }, _('Location')),
|
||||
E('th', { class: 'th' }, _('ISP / Organization')),
|
||||
E('th', { class: 'th', style: 'text-align: center;' }, _('Status')),
|
||||
]),
|
||||
]),
|
||||
E('tbody', {}, [
|
||||
E('tr', { class: 'tr cbi-section-table-row' }, [
|
||||
E('td', { class: 'td' }, [
|
||||
E('b', {}, _('Direct WAN (ISP)')),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'font-size: 11px; color: var(--text-color-medium, #6c757d);',
|
||||
},
|
||||
_('Bypasses proxy (SO_MARK 0x08000000)'),
|
||||
),
|
||||
]),
|
||||
E('td', { class: 'td' }, [E('code', {}, ip_leak.direct_ip || '—')]),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td' },
|
||||
[ip_leak.direct_country, ip_leak.direct_city]
|
||||
.filter(Boolean)
|
||||
.join(', ') || '—',
|
||||
),
|
||||
E('td', { class: 'td' }, ip_leak.direct_isp || '—'),
|
||||
E('td', { class: 'td', style: 'text-align: center;' }, [
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
class: 'badge',
|
||||
style:
|
||||
'background: var(--text-color-medium, #6c757d); color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;',
|
||||
},
|
||||
_('Baseline'),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
E('tr', { class: 'tr cbi-section-table-row' }, [
|
||||
E('td', { class: 'td' }, [
|
||||
E('b', {}, _('Proxy Outbound')),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'font-size: 11px; color: var(--text-color-medium, #6c757d);',
|
||||
},
|
||||
_('127.0.0.1:4534 (sing-box mixed)'),
|
||||
),
|
||||
]),
|
||||
E('td', { class: 'td' }, [E('code', {}, ip_leak.proxy_ip || '—')]),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td' },
|
||||
[ip_leak.proxy_country, ip_leak.proxy_city]
|
||||
.filter(Boolean)
|
||||
.join(', ') || '—',
|
||||
),
|
||||
E('td', { class: 'td' }, ip_leak.proxy_org || '—'),
|
||||
E('td', { class: 'td', style: 'text-align: center;' }, [
|
||||
!ip_leak.proxy_online
|
||||
? E(
|
||||
'span',
|
||||
{
|
||||
class: 'badge',
|
||||
style:
|
||||
'background: #fd7e14; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;',
|
||||
},
|
||||
_('OFFLINE'),
|
||||
)
|
||||
: ip_leak.leaked
|
||||
? E(
|
||||
'span',
|
||||
{
|
||||
class: 'badge',
|
||||
style:
|
||||
'background: #dc3545; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;',
|
||||
},
|
||||
_('LEAKED'),
|
||||
)
|
||||
: E(
|
||||
'span',
|
||||
{
|
||||
class: 'badge',
|
||||
style:
|
||||
'background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;',
|
||||
},
|
||||
_('SECURE'),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
const ipSection = E(
|
||||
'div',
|
||||
{
|
||||
class: 'cbi-section',
|
||||
style:
|
||||
'margin-bottom: 20px; border: 1px solid var(--border-color, rgba(128,128,128,0.2)); border-radius: 6px; padding: 12px;',
|
||||
},
|
||||
[
|
||||
E('h4', { style: 'margin-top: 0; margin-bottom: 8px;' }, [
|
||||
'🌐 ',
|
||||
_('Public IP Address Isolation'),
|
||||
]),
|
||||
E('div', { class: ipAlertClass, style: 'margin-bottom: 12px;' }, [
|
||||
ipAlertText,
|
||||
]),
|
||||
ipTable,
|
||||
],
|
||||
);
|
||||
|
||||
// --- 2. DNS Leak Section ---
|
||||
const dnsAlertClass = dns_leak.dns_leaked
|
||||
? 'alert-message danger'
|
||||
: dns_leak.dns_servers.length > 0
|
||||
? 'alert-message success'
|
||||
: 'alert-message info';
|
||||
|
||||
const dnsAlertText = dns_leak.dns_leaked
|
||||
? _(
|
||||
'⚠️ DNS LEAK DETECTED: DNS queries are leaking to your local Internet Service Provider!',
|
||||
)
|
||||
: dns_leak.dns_servers.length > 0
|
||||
? _(
|
||||
'🛡️ SECURE: No DNS leaks detected. All queries resolve through non-ISP upstream resolvers.',
|
||||
)
|
||||
: _(
|
||||
'No DNS resolvers captured via proxy test. Proxy may be offline or blocking test subdomains.',
|
||||
);
|
||||
|
||||
const dnsTableRows = (dns_leak.dns_servers || []).map((s) =>
|
||||
E('tr', { class: 'tr cbi-section-table-row' }, [
|
||||
E('td', { class: 'td' }, [E('code', {}, s.ip)]),
|
||||
E('td', { class: 'td' }, s.country || '—'),
|
||||
E('td', { class: 'td' }, s.isp || '—'),
|
||||
E('td', { class: 'td', style: 'text-align: center;' }, [
|
||||
s.is_isp
|
||||
? E(
|
||||
'span',
|
||||
{
|
||||
class: 'badge',
|
||||
style:
|
||||
'background: #dc3545; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;',
|
||||
},
|
||||
_('ISP DNS LEAK'),
|
||||
)
|
||||
: E(
|
||||
'span',
|
||||
{
|
||||
class: 'badge',
|
||||
style:
|
||||
'background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;',
|
||||
},
|
||||
_('SAFE'),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
);
|
||||
|
||||
const dnsTable = E(
|
||||
'table',
|
||||
{
|
||||
class: 'table cbi-section-table',
|
||||
style: 'width: 100%; font-size: 12px; margin-bottom: 12px;',
|
||||
},
|
||||
[
|
||||
E('thead', {}, [
|
||||
E('tr', { class: 'tr cbi-section-table-titles' }, [
|
||||
E('th', { class: 'th' }, _('Resolver IP')),
|
||||
E('th', { class: 'th' }, _('Country')),
|
||||
E('th', { class: 'th' }, _('Upstream Provider / ASN')),
|
||||
E(
|
||||
'th',
|
||||
{ class: 'th', style: 'text-align: center;' },
|
||||
_('Verdict'),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
E(
|
||||
'tbody',
|
||||
{},
|
||||
dnsTableRows.length > 0
|
||||
? dnsTableRows
|
||||
: [
|
||||
E('tr', { class: 'tr' }, [
|
||||
E(
|
||||
'td',
|
||||
{
|
||||
class: 'td',
|
||||
colSpan: 4,
|
||||
style: 'text-align: center; opacity: 0.7;',
|
||||
},
|
||||
_('No DNS resolvers recorded'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const dnsSection = E(
|
||||
'div',
|
||||
{
|
||||
class: 'cbi-section',
|
||||
style:
|
||||
'border: 1px solid var(--border-color, rgba(128,128,128,0.2)); border-radius: 6px; padding: 12px;',
|
||||
},
|
||||
[
|
||||
E('h4', { style: 'margin-top: 0; margin-bottom: 8px;' }, [
|
||||
'🔍 ',
|
||||
_('DNS Upstream Resolver Analysis (bash.ws protocol)'),
|
||||
]),
|
||||
E('div', { class: dnsAlertClass, style: 'margin-bottom: 12px;' }, [
|
||||
dnsAlertText,
|
||||
]),
|
||||
dnsTable,
|
||||
],
|
||||
);
|
||||
|
||||
resultsContainer.appendChild(ipSection);
|
||||
resultsContainer.appendChild(dnsSection);
|
||||
resultsContainer.style.display = 'block';
|
||||
};
|
||||
|
||||
const retryBtn = renderButton({
|
||||
classNames: ['cbi-button-action'],
|
||||
onClick: startTest,
|
||||
text: `🔄 ${_('Re-run Leak Test')}`,
|
||||
});
|
||||
|
||||
const closeBtn = renderButton({
|
||||
classNames: ['cbi-button'],
|
||||
onClick: () => {
|
||||
if (ui.hideModal) ui.hideModal();
|
||||
},
|
||||
text: _('Close'),
|
||||
});
|
||||
|
||||
const modalContent = E('div', { style: 'padding: 8px;' }, [
|
||||
E(
|
||||
'p',
|
||||
{
|
||||
style:
|
||||
'font-size: 13px; color: var(--text-color-medium, #6c757d); margin-bottom: 14px;',
|
||||
},
|
||||
_(
|
||||
'Performs simultaneous outbound checks via direct WAN (bypassing Sing-box redirect) and via proxy (127.0.0.1:4534) to verify that your real IP and DNS queries are not leaking to your ISP.',
|
||||
),
|
||||
),
|
||||
statusLabel,
|
||||
progressContainer,
|
||||
resultsContainer,
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; border-top: 1px solid var(--border-color, rgba(128,128,128,0.2)); padding-top: 12px;',
|
||||
},
|
||||
[retryBtn, closeBtn],
|
||||
),
|
||||
]);
|
||||
|
||||
ui.showModal(`🛡️ ${_('Tachyon IP & DNS Leak Detection')}`, modalContent);
|
||||
|
||||
// Auto-run test on open
|
||||
startTest();
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
class MockMutationObserver {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
(globalThis as any).MutationObserver = MockMutationObserver;
|
||||
|
||||
function createDummyElement(tag: string) {
|
||||
return {
|
||||
tagName: tag.toUpperCase(),
|
||||
children: [] as any[],
|
||||
style: {} as Record<string, string>,
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
setAttribute() {},
|
||||
getAttribute() {
|
||||
return null;
|
||||
},
|
||||
addEventListener() {},
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
replaceChildren: function (...nodes: any[]) {
|
||||
(this as any).children = nodes;
|
||||
},
|
||||
appendChild: function (node: any) {
|
||||
(this as any).children.push(node);
|
||||
},
|
||||
append: function (...nodes: any[]) {
|
||||
nodes.forEach((node) => (this as any).children.push(node));
|
||||
},
|
||||
classList: {
|
||||
add() {},
|
||||
remove() {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
(globalThis as any).document = {
|
||||
body: {} as any,
|
||||
createElement: (tag: string) => createDummyElement(tag) as any,
|
||||
createElementNS: (_ns: string, tag: string) =>
|
||||
createDummyElement(tag) as any,
|
||||
getElementById: () => null,
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
|
||||
(globalThis as any).E = (tag: string, attrs?: any, children?: any) => {
|
||||
const el = (globalThis as any).document.createElement(tag);
|
||||
if (attrs) {
|
||||
const { style, ...rest } = attrs;
|
||||
Object.assign(el, rest);
|
||||
if (style && typeof style === 'string') {
|
||||
style.split(';').forEach((rule: string) => {
|
||||
const [k, v] = rule.split(':').map((s: string) => s.trim());
|
||||
if (k && v) {
|
||||
el.style[k] = v;
|
||||
}
|
||||
});
|
||||
} else if (style && typeof style === 'object') {
|
||||
Object.assign(el.style, style);
|
||||
}
|
||||
}
|
||||
if (children) {
|
||||
if (Array.isArray(children)) {
|
||||
children.forEach((c) => {
|
||||
if (c) el.appendChild(c);
|
||||
});
|
||||
} else {
|
||||
el.appendChild(children);
|
||||
}
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
(globalThis as any)._ = (str: string) => str;
|
||||
|
||||
(globalThis as any).rpc = { declare: vi.fn() };
|
||||
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
|
||||
(globalThis as any).localStorage = { getItem: vi.fn(), setItem: vi.fn() };
|
||||
|
||||
const showModal = vi.fn();
|
||||
const hideModal = vi.fn();
|
||||
(globalThis as any).ui = {
|
||||
showModal,
|
||||
hideModal,
|
||||
addNotification: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
showModal,
|
||||
hideModal,
|
||||
};
|
||||
});
|
||||
|
||||
import { renderLeakCheckModal } from '../partials/renderLeakCheckModal';
|
||||
import { TachyonShellMethods } from '../../../methods/shell';
|
||||
|
||||
describe('renderLeakCheckModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.showModal.mockReset();
|
||||
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
ip_leak: {
|
||||
leaked: false,
|
||||
direct_ip: '95.173.136.25',
|
||||
direct_country: 'Russia',
|
||||
direct_isp: 'Rostelecom',
|
||||
proxy_ip: '185.220.101.5',
|
||||
proxy_country: 'Netherlands',
|
||||
proxy_org: 'Mullvad',
|
||||
proxy_online: true,
|
||||
},
|
||||
dns_leak: {
|
||||
dns_leaked: false,
|
||||
direct_ip: '95.173.136.25',
|
||||
proxy_ip: '185.220.101.5',
|
||||
dns_servers: [
|
||||
{
|
||||
ip: '1.1.1.1',
|
||||
country: 'United States',
|
||||
isp: 'Cloudflare',
|
||||
is_isp: false,
|
||||
},
|
||||
],
|
||||
direct_dns_servers: [
|
||||
{
|
||||
ip: '212.188.4.10',
|
||||
country: 'Russia',
|
||||
isp: 'Rostelecom',
|
||||
is_isp: true,
|
||||
},
|
||||
],
|
||||
proxy_online: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders and displays the IP & DNS Leak Detection modal', () => {
|
||||
renderLeakCheckModal();
|
||||
|
||||
expect(mocks.showModal).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.showModal).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Tachyon IP & DNS Leak Detection'),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls TachyonShellMethods.leakCheck on start', async () => {
|
||||
const leakCheckSpy = vi.spyOn(TachyonShellMethods, 'leakCheck');
|
||||
renderLeakCheckModal();
|
||||
|
||||
expect(leakCheckSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles leak detection when IP is leaked', async () => {
|
||||
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
ip_leak: {
|
||||
leaked: true,
|
||||
direct_ip: '95.173.136.25',
|
||||
proxy_ip: '95.173.136.25',
|
||||
proxy_online: true,
|
||||
},
|
||||
dns_leak: {
|
||||
dns_leaked: true,
|
||||
direct_ip: '95.173.136.25',
|
||||
proxy_ip: '95.173.136.25',
|
||||
dns_servers: [
|
||||
{
|
||||
ip: '212.188.4.10',
|
||||
country: 'Russia',
|
||||
isp: 'Rostelecom',
|
||||
is_isp: true,
|
||||
},
|
||||
],
|
||||
direct_dns_servers: [],
|
||||
proxy_online: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderLeakCheckModal();
|
||||
expect(mocks.showModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles offline proxy gracefully', async () => {
|
||||
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
ip_leak: {
|
||||
leaked: false,
|
||||
direct_ip: '95.173.136.25',
|
||||
proxy_ip: '—',
|
||||
proxy_online: false,
|
||||
},
|
||||
dns_leak: {
|
||||
dns_leaked: false,
|
||||
direct_ip: '95.173.136.25',
|
||||
proxy_ip: '—',
|
||||
dns_servers: [],
|
||||
direct_dns_servers: [],
|
||||
proxy_online: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderLeakCheckModal();
|
||||
expect(mocks.showModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -117,6 +117,9 @@ export namespace Tachyon {
|
||||
DNS_BENCHMARK_STATUS = 'dns_benchmark_status',
|
||||
DNS_BENCHMARK_STOP = 'dns_benchmark_stop',
|
||||
DNS_BENCHMARK_APPLY = 'dns_benchmark_apply',
|
||||
LEAK_CHECK = 'leak_check',
|
||||
CHECK_IP_LEAK = 'check_ip_leak',
|
||||
CHECK_DNS_LEAK = 'check_dns_leak',
|
||||
}
|
||||
|
||||
export enum AvailableClashAPIMethods {
|
||||
@@ -1166,4 +1169,41 @@ export namespace Tachyon {
|
||||
started_at: number;
|
||||
finished_at: number | null;
|
||||
}
|
||||
|
||||
export interface IPLeakResult {
|
||||
leaked: boolean;
|
||||
direct_ip: string;
|
||||
direct_country?: string;
|
||||
direct_country_code?: string;
|
||||
direct_city?: string;
|
||||
direct_isp?: string;
|
||||
proxy_ip: string;
|
||||
proxy_country?: string;
|
||||
proxy_country_code?: string;
|
||||
proxy_city?: string;
|
||||
proxy_org?: string;
|
||||
proxy_online: boolean;
|
||||
}
|
||||
|
||||
export interface DNSResolverInfo {
|
||||
ip: string;
|
||||
country: string;
|
||||
isp: string;
|
||||
is_isp: boolean;
|
||||
}
|
||||
|
||||
export interface DNSLeakResult {
|
||||
dns_leaked: boolean;
|
||||
direct_ip: string;
|
||||
proxy_ip: string;
|
||||
dns_servers: DNSResolverInfo[];
|
||||
direct_dns_servers: DNSResolverInfo[];
|
||||
proxy_online: boolean;
|
||||
}
|
||||
|
||||
export interface LeakCheckResult {
|
||||
ip_leak: IPLeakResult;
|
||||
dns_leak: DNSLeakResult;
|
||||
timestamp?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3433,6 +3433,9 @@ var Tachyon;
|
||||
AvailableMethods2["DNS_BENCHMARK_STATUS"] = "dns_benchmark_status";
|
||||
AvailableMethods2["DNS_BENCHMARK_STOP"] = "dns_benchmark_stop";
|
||||
AvailableMethods2["DNS_BENCHMARK_APPLY"] = "dns_benchmark_apply";
|
||||
AvailableMethods2["LEAK_CHECK"] = "leak_check";
|
||||
AvailableMethods2["CHECK_IP_LEAK"] = "check_ip_leak";
|
||||
AvailableMethods2["CHECK_DNS_LEAK"] = "check_dns_leak";
|
||||
})(AvailableMethods = Tachyon2.AvailableMethods || (Tachyon2.AvailableMethods = {}));
|
||||
let AvailableClashAPIMethods;
|
||||
((AvailableClashAPIMethods2) => {
|
||||
@@ -4724,6 +4727,26 @@ var TachyonShellMethods = {
|
||||
recommendation: parsed.recommendation
|
||||
}
|
||||
};
|
||||
},
|
||||
leakCheck: async () => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [Tachyon.AvailableMethods.LEAK_CHECK],
|
||||
timeout: 25e3
|
||||
});
|
||||
const parsed = parseJsonObjectOutput(
|
||||
response.stdout
|
||||
);
|
||||
if ((response.code ?? 0) !== 0 || !parsed) {
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _("Failed to execute IP and DNS leak check")
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11320,7 +11343,8 @@ function renderAvailableActions({
|
||||
viewLogs,
|
||||
showSingBoxConfig,
|
||||
generateBugReport,
|
||||
checkServices
|
||||
checkServices,
|
||||
testLeaks
|
||||
}) {
|
||||
return E("div", { class: "tachyon_diagnostic-page__right-bar__actions" }, [
|
||||
E("b", {}, _("Available actions")),
|
||||
@@ -11442,6 +11466,16 @@ function renderAvailableActions({
|
||||
disabled: checkServices.disabled
|
||||
})
|
||||
]),
|
||||
...insertIf(!!testLeaks?.visible, [
|
||||
renderButton({
|
||||
classNames: ["cbi-button-action"],
|
||||
onClick: testLeaks.onClick,
|
||||
icon: renderCircleCheckBigIcon24,
|
||||
text: _("\u{1F6E1}\uFE0F IP & DNS Leak Test"),
|
||||
loading: testLeaks.loading,
|
||||
disabled: testLeaks.disabled
|
||||
})
|
||||
]),
|
||||
...insertIf(viewLogs.visible, [
|
||||
renderButton({
|
||||
onClick: viewLogs.onClick,
|
||||
@@ -14709,6 +14743,336 @@ function renderDnsBenchmarkModal() {
|
||||
startBenchmark();
|
||||
}
|
||||
|
||||
// src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts
|
||||
function renderLeakCheckModal() {
|
||||
let isRunning = false;
|
||||
const progressBar = E("div", {
|
||||
style: "width: 0%; height: 6px; background: linear-gradient(90deg, #007bff, #28a745); border-radius: 3px; transition: width 0.4s ease;"
|
||||
});
|
||||
const progressContainer = E(
|
||||
"div",
|
||||
{
|
||||
style: "width: 100%; height: 6px; background: rgba(128,128,128,0.2); border-radius: 3px; overflow: hidden; margin-bottom: 14px;"
|
||||
},
|
||||
[progressBar]
|
||||
);
|
||||
const statusLabel = E(
|
||||
"div",
|
||||
{
|
||||
style: "font-size: 13px; font-weight: 500; margin-bottom: 12px; color: var(--text-color-medium, #6c757d);"
|
||||
},
|
||||
_("Initializing router-level IP & DNS leak test...")
|
||||
);
|
||||
const resultsContainer = E("div", {
|
||||
style: "display: none; margin-bottom: 16px;"
|
||||
});
|
||||
const startTest = async () => {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
resultsContainer.style.display = "none";
|
||||
progressContainer.style.display = "block";
|
||||
progressBar.style.width = "30%";
|
||||
statusLabel.textContent = _(
|
||||
"Querying WAN direct socket and proxy outbound on 127.0.0.1:4534..."
|
||||
);
|
||||
if (retryBtn) retryBtn.disabled = true;
|
||||
const timer = setTimeout(() => {
|
||||
progressBar.style.width = "70%";
|
||||
statusLabel.textContent = _(
|
||||
"Testing DNS leak upstream resolvers with bash.ws protocol..."
|
||||
);
|
||||
}, 1500);
|
||||
try {
|
||||
const response = await TachyonShellMethods.leakCheck();
|
||||
clearTimeout(timer);
|
||||
progressBar.style.width = "100%";
|
||||
if (response.success && response.data) {
|
||||
statusLabel.textContent = _("Leak detection completed");
|
||||
setTimeout(() => {
|
||||
progressContainer.style.display = "none";
|
||||
progressBar.style.width = "0%";
|
||||
}, 400);
|
||||
renderResults(response.data);
|
||||
} else {
|
||||
const err = !response.success && response.error ? response.error : _("Leak detection failed to complete");
|
||||
progressContainer.style.display = "none";
|
||||
statusLabel.textContent = err;
|
||||
resultsContainer.innerHTML = "";
|
||||
resultsContainer.appendChild(
|
||||
E(
|
||||
"div",
|
||||
{ class: "alert-message warning" },
|
||||
err || _(
|
||||
"Could not contact leak test endpoints. Please ensure router has internet access."
|
||||
)
|
||||
)
|
||||
);
|
||||
resultsContainer.style.display = "block";
|
||||
}
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
progressContainer.style.display = "none";
|
||||
statusLabel.textContent = _("An unexpected error occurred during test");
|
||||
resultsContainer.innerHTML = "";
|
||||
resultsContainer.appendChild(
|
||||
E(
|
||||
"div",
|
||||
{ class: "alert-message warning" },
|
||||
e instanceof Error ? e.message : String(e)
|
||||
)
|
||||
);
|
||||
resultsContainer.style.display = "block";
|
||||
} finally {
|
||||
isRunning = false;
|
||||
if (retryBtn) retryBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
const renderResults = (data) => {
|
||||
resultsContainer.innerHTML = "";
|
||||
const { ip_leak, dns_leak } = data;
|
||||
const ipAlertClass = !ip_leak.proxy_online ? "alert-message warning" : ip_leak.leaked ? "alert-message danger" : "alert-message success";
|
||||
const ipAlertText = !ip_leak.proxy_online ? _("Proxy is offline or unreachable on 127.0.0.1:4534.") : ip_leak.leaked ? _(
|
||||
"\u26A0\uFE0F CRITICAL IP LEAK: Your real public IP is exposed through the proxy outbound!"
|
||||
) : _(
|
||||
"\u{1F6E1}\uFE0F SECURE: No IP leak detected. Real WAN IP is concealed behind proxy outbound."
|
||||
);
|
||||
const ipTable = E(
|
||||
"table",
|
||||
{
|
||||
class: "table cbi-section-table",
|
||||
style: "width: 100%; margin-bottom: 12px; font-size: 12px;"
|
||||
},
|
||||
[
|
||||
E("thead", {}, [
|
||||
E("tr", { class: "tr cbi-section-table-titles" }, [
|
||||
E("th", { class: "th" }, _("Connection Path")),
|
||||
E("th", { class: "th" }, _("Observed Public IP")),
|
||||
E("th", { class: "th" }, _("Location")),
|
||||
E("th", { class: "th" }, _("ISP / Organization")),
|
||||
E("th", { class: "th", style: "text-align: center;" }, _("Status"))
|
||||
])
|
||||
]),
|
||||
E("tbody", {}, [
|
||||
E("tr", { class: "tr cbi-section-table-row" }, [
|
||||
E("td", { class: "td" }, [
|
||||
E("b", {}, _("Direct WAN (ISP)")),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "font-size: 11px; color: var(--text-color-medium, #6c757d);"
|
||||
},
|
||||
_("Bypasses proxy (SO_MARK 0x08000000)")
|
||||
)
|
||||
]),
|
||||
E("td", { class: "td" }, [E("code", {}, ip_leak.direct_ip || "\u2014")]),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td" },
|
||||
[ip_leak.direct_country, ip_leak.direct_city].filter(Boolean).join(", ") || "\u2014"
|
||||
),
|
||||
E("td", { class: "td" }, ip_leak.direct_isp || "\u2014"),
|
||||
E("td", { class: "td", style: "text-align: center;" }, [
|
||||
E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: var(--text-color-medium, #6c757d); color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("Baseline")
|
||||
)
|
||||
])
|
||||
]),
|
||||
E("tr", { class: "tr cbi-section-table-row" }, [
|
||||
E("td", { class: "td" }, [
|
||||
E("b", {}, _("Proxy Outbound")),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "font-size: 11px; color: var(--text-color-medium, #6c757d);"
|
||||
},
|
||||
_("127.0.0.1:4534 (sing-box mixed)")
|
||||
)
|
||||
]),
|
||||
E("td", { class: "td" }, [E("code", {}, ip_leak.proxy_ip || "\u2014")]),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td" },
|
||||
[ip_leak.proxy_country, ip_leak.proxy_city].filter(Boolean).join(", ") || "\u2014"
|
||||
),
|
||||
E("td", { class: "td" }, ip_leak.proxy_org || "\u2014"),
|
||||
E("td", { class: "td", style: "text-align: center;" }, [
|
||||
!ip_leak.proxy_online ? E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #fd7e14; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("OFFLINE")
|
||||
) : ip_leak.leaked ? E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #dc3545; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("LEAKED")
|
||||
) : E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("SECURE")
|
||||
)
|
||||
])
|
||||
])
|
||||
])
|
||||
]
|
||||
);
|
||||
const ipSection = E(
|
||||
"div",
|
||||
{
|
||||
class: "cbi-section",
|
||||
style: "margin-bottom: 20px; border: 1px solid var(--border-color, rgba(128,128,128,0.2)); border-radius: 6px; padding: 12px;"
|
||||
},
|
||||
[
|
||||
E("h4", { style: "margin-top: 0; margin-bottom: 8px;" }, [
|
||||
"\u{1F310} ",
|
||||
_("Public IP Address Isolation")
|
||||
]),
|
||||
E("div", { class: ipAlertClass, style: "margin-bottom: 12px;" }, [
|
||||
ipAlertText
|
||||
]),
|
||||
ipTable
|
||||
]
|
||||
);
|
||||
const dnsAlertClass = dns_leak.dns_leaked ? "alert-message danger" : dns_leak.dns_servers.length > 0 ? "alert-message success" : "alert-message info";
|
||||
const dnsAlertText = dns_leak.dns_leaked ? _(
|
||||
"\u26A0\uFE0F DNS LEAK DETECTED: DNS queries are leaking to your local Internet Service Provider!"
|
||||
) : dns_leak.dns_servers.length > 0 ? _(
|
||||
"\u{1F6E1}\uFE0F SECURE: No DNS leaks detected. All queries resolve through non-ISP upstream resolvers."
|
||||
) : _(
|
||||
"No DNS resolvers captured via proxy test. Proxy may be offline or blocking test subdomains."
|
||||
);
|
||||
const dnsTableRows = (dns_leak.dns_servers || []).map(
|
||||
(s) => E("tr", { class: "tr cbi-section-table-row" }, [
|
||||
E("td", { class: "td" }, [E("code", {}, s.ip)]),
|
||||
E("td", { class: "td" }, s.country || "\u2014"),
|
||||
E("td", { class: "td" }, s.isp || "\u2014"),
|
||||
E("td", { class: "td", style: "text-align: center;" }, [
|
||||
s.is_isp ? E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #dc3545; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("ISP DNS LEAK")
|
||||
) : E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("SAFE")
|
||||
)
|
||||
])
|
||||
])
|
||||
);
|
||||
const dnsTable = E(
|
||||
"table",
|
||||
{
|
||||
class: "table cbi-section-table",
|
||||
style: "width: 100%; font-size: 12px; margin-bottom: 12px;"
|
||||
},
|
||||
[
|
||||
E("thead", {}, [
|
||||
E("tr", { class: "tr cbi-section-table-titles" }, [
|
||||
E("th", { class: "th" }, _("Resolver IP")),
|
||||
E("th", { class: "th" }, _("Country")),
|
||||
E("th", { class: "th" }, _("Upstream Provider / ASN")),
|
||||
E(
|
||||
"th",
|
||||
{ class: "th", style: "text-align: center;" },
|
||||
_("Verdict")
|
||||
)
|
||||
])
|
||||
]),
|
||||
E(
|
||||
"tbody",
|
||||
{},
|
||||
dnsTableRows.length > 0 ? dnsTableRows : [
|
||||
E("tr", { class: "tr" }, [
|
||||
E(
|
||||
"td",
|
||||
{
|
||||
class: "td",
|
||||
colSpan: 4,
|
||||
style: "text-align: center; opacity: 0.7;"
|
||||
},
|
||||
_("No DNS resolvers recorded")
|
||||
)
|
||||
])
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
const dnsSection = E(
|
||||
"div",
|
||||
{
|
||||
class: "cbi-section",
|
||||
style: "border: 1px solid var(--border-color, rgba(128,128,128,0.2)); border-radius: 6px; padding: 12px;"
|
||||
},
|
||||
[
|
||||
E("h4", { style: "margin-top: 0; margin-bottom: 8px;" }, [
|
||||
"\u{1F50D} ",
|
||||
_("DNS Upstream Resolver Analysis (bash.ws protocol)")
|
||||
]),
|
||||
E("div", { class: dnsAlertClass, style: "margin-bottom: 12px;" }, [
|
||||
dnsAlertText
|
||||
]),
|
||||
dnsTable
|
||||
]
|
||||
);
|
||||
resultsContainer.appendChild(ipSection);
|
||||
resultsContainer.appendChild(dnsSection);
|
||||
resultsContainer.style.display = "block";
|
||||
};
|
||||
const retryBtn = renderButton({
|
||||
classNames: ["cbi-button-action"],
|
||||
onClick: startTest,
|
||||
text: `\u{1F504} ${_("Re-run Leak Test")}`
|
||||
});
|
||||
const closeBtn = renderButton({
|
||||
classNames: ["cbi-button"],
|
||||
onClick: () => {
|
||||
if (ui.hideModal) ui.hideModal();
|
||||
},
|
||||
text: _("Close")
|
||||
});
|
||||
const modalContent = E("div", { style: "padding: 8px;" }, [
|
||||
E(
|
||||
"p",
|
||||
{
|
||||
style: "font-size: 13px; color: var(--text-color-medium, #6c757d); margin-bottom: 14px;"
|
||||
},
|
||||
_(
|
||||
"Performs simultaneous outbound checks via direct WAN (bypassing Sing-box redirect) and via proxy (127.0.0.1:4534) to verify that your real IP and DNS queries are not leaking to your ISP."
|
||||
)
|
||||
),
|
||||
statusLabel,
|
||||
progressContainer,
|
||||
resultsContainer,
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; border-top: 1px solid var(--border-color, rgba(128,128,128,0.2)); padding-top: 12px;"
|
||||
},
|
||||
[retryBtn, closeBtn]
|
||||
)
|
||||
]);
|
||||
ui.showModal(`\u{1F6E1}\uFE0F ${_("Tachyon IP & DNS Leak Detection")}`, modalContent);
|
||||
startTest();
|
||||
}
|
||||
|
||||
// src/helpers/normalizeCompiledVersion.ts
|
||||
function normalizeCompiledVersion(version, commitSha) {
|
||||
if (!version || version.includes("COMPILED")) {
|
||||
@@ -16424,6 +16788,9 @@ async function handleRunAiDoctor() {
|
||||
function handleOpenAiChat() {
|
||||
renderAiChatModal();
|
||||
}
|
||||
function handleOpenLeakCheck() {
|
||||
renderLeakCheckModal();
|
||||
}
|
||||
function handleOpenStrategyFuzzer() {
|
||||
getConfigSections().then((sections) => {
|
||||
const ruleNames = sections.filter((s) => s[".type"] === "section" || s[".type"] === "rule").map((s) => s.name || s[".name"]).filter((n) => Boolean(n));
|
||||
@@ -16711,6 +17078,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
||||
disabled: utilityActionsDisabled,
|
||||
onClick: handleCheckServicesAction
|
||||
},
|
||||
testLeaks: {
|
||||
visible: true,
|
||||
loading: false,
|
||||
disabled: false,
|
||||
onClick: handleOpenLeakCheck
|
||||
},
|
||||
viewLogs: {
|
||||
loading: diagnosticsActions.viewLogs.loading,
|
||||
visible: true,
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-08 09:57+0300\n"
|
||||
"PO-Revision-Date: 2026-09-08 09:57+0300\n"
|
||||
"POT-Creation-Date: 2026-09-09 17:09+0300\n"
|
||||
"PO-Revision-Date: 2026-09-09 17:09+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -50,6 +50,12 @@ msgstr "⚠️ Бенчмарк завершен. Не найдено рабоч
|
||||
msgid "⚠️ Benchmark stopped"
|
||||
msgstr "⚠️ Бенчмарк остановлен"
|
||||
|
||||
msgid "⚠️ CRITICAL IP LEAK: Your real public IP is exposed through the proxy outbound!"
|
||||
msgstr "⚠️ КРИТИЧЕСКАЯ УТЕЧКА IP: Ваш реальный публичный IP раскрыт через исходящий прокси!"
|
||||
|
||||
msgid "⚠️ DNS LEAK DETECTED: DNS queries are leaking to your local Internet Service Provider!"
|
||||
msgstr "⚠️ ОБНАРУЖЕНА УТЕЧКА DNS: DNS-запросы утекают к вашему местному интернет-провайдеру!"
|
||||
|
||||
msgid "⚠️ If all Bootstrap DNS fail 3 times, queries will be sent to your ISP's DNS in plaintext. Only use as a last resort to prevent complete internet loss."
|
||||
msgstr "⚠️ Если все Bootstrap DNS завершатся с ошибкой 3 раза, запросы будут отправлены в DNS вашего провайдера в открытом виде. Используйте только в крайнем случае для предотвращения полной потери интернета."
|
||||
|
||||
@@ -155,9 +161,21 @@ msgstr "🛑 Остановить бенчмарк"
|
||||
msgid "🛠️ My Custom Strategies Only"
|
||||
msgstr "🛠️ Только мои стратегии"
|
||||
|
||||
msgid "🛡️ IP & DNS Leak Test"
|
||||
msgstr "🛡️ Проверка утечек IP и DNS"
|
||||
|
||||
msgid "🛡️ SECURE: No DNS leaks detected. All queries resolve through non-ISP upstream resolvers."
|
||||
msgstr "🛡️ ЗАЩИЩЕНО: Утечек DNS не обнаружено. Все запросы обрабатываются независимыми резолверами."
|
||||
|
||||
msgid "🛡️ SECURE: No IP leak detected. Real WAN IP is concealed behind proxy outbound."
|
||||
msgstr "🛡️ ЗАЩИЩЕНО: Утечек IP не обнаружено. Реальный WAN IP скрыт за исходящим прокси."
|
||||
|
||||
msgid "🟣 Twitch Suite (Live Stream + HLS CDN)"
|
||||
msgstr "🟣 Twitch Набор (Прямой эфир + HLS CDN)"
|
||||
|
||||
msgid "127.0.0.1:4534 (sing-box mixed)"
|
||||
msgstr "127.0.0.1:4534 (смешанный sing-box)"
|
||||
|
||||
msgid "Блокировка по IP (Таймаут TCP SYN)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
@@ -485,6 +503,9 @@ msgstr "Генератор AmneziaWG"
|
||||
msgid "AmneziaWG Version"
|
||||
msgstr "Версия AmneziaWG"
|
||||
|
||||
msgid "An unexpected error occurred during test"
|
||||
msgstr "Произошла непредвиденная ошибка во время проверки"
|
||||
|
||||
msgid "Anonymized support report copied to clipboard"
|
||||
msgstr "Анонимизированный отчет скопирован в буфер обмена"
|
||||
|
||||
@@ -641,6 +662,9 @@ msgstr "Ср. задержка"
|
||||
msgid "Backend validation failed: %s"
|
||||
msgstr "Ошибка проверки бэкенда: %s"
|
||||
|
||||
msgid "Baseline"
|
||||
msgstr "Базовый уровень"
|
||||
|
||||
msgid "Basic connectivity"
|
||||
msgstr "Базовая связность"
|
||||
|
||||
@@ -791,6 +815,9 @@ msgstr "Стратегия ByeDPI"
|
||||
msgid "ByeDPI strategy cannot be empty"
|
||||
msgstr "Стратегия ByeDPI не может быть пустой"
|
||||
|
||||
msgid "Bypasses proxy (SO_MARK 0x08000000)"
|
||||
msgstr "В обход прокси (SO_MARK 0x08000000)"
|
||||
|
||||
msgid "Bypasses UDP traffic for selected game consoles (PS5/Xbox) to achieve NAT Type 1 / 2 (Full Cone NAT) for P2P matchmaking, while keeping TCP (PSN/Auth) routed through the proxy."
|
||||
msgstr "Обходит UDP-трафик для выбранных игровых консолей (PS5/Xbox) для достижения NAT Type 1 / 2 (Full Cone NAT) для P2P-матчмейкинга, сохраняя TCP (PSN/Auth) через прокси."
|
||||
|
||||
@@ -1055,6 +1082,9 @@ msgstr "Подключено"
|
||||
msgid "Connected Devices"
|
||||
msgstr "Подключенные устройства"
|
||||
|
||||
msgid "Connection Path"
|
||||
msgstr "Маршрут соединения"
|
||||
|
||||
msgid "Connection URL"
|
||||
msgstr "URL подключения"
|
||||
|
||||
@@ -1100,6 +1130,12 @@ msgstr "Основные настройки и параметры"
|
||||
msgid "Could not compare FakeIP and control public IPs"
|
||||
msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки"
|
||||
|
||||
msgid "Could not contact leak test endpoints. Please ensure router has internet access."
|
||||
msgstr "Не удалось связаться с серверами проверки утечек. Убедитесь, что у роутера есть доступ в интернет."
|
||||
|
||||
msgid "Country"
|
||||
msgstr "Страна"
|
||||
|
||||
msgid "Country code (e.g. ru, us, de)"
|
||||
msgstr "Код страны (напр. ru, us, de)"
|
||||
|
||||
@@ -1271,6 +1307,9 @@ msgstr "Прямой прокси"
|
||||
msgid "Direct WAN"
|
||||
msgstr "Прямой WAN"
|
||||
|
||||
msgid "Direct WAN (ISP)"
|
||||
msgstr "Напрямую в WAN (провайдер)"
|
||||
|
||||
msgid "Disable"
|
||||
msgstr "Отключить"
|
||||
|
||||
@@ -1382,6 +1421,9 @@ msgstr "Быстрый DNS-кэш"
|
||||
msgid "DNS Unavailability Timeout"
|
||||
msgstr "Таймаут недоступности DNS"
|
||||
|
||||
msgid "DNS Upstream Resolver Analysis (bash.ws protocol)"
|
||||
msgstr "Анализ восходящих DNS-резолверов (протокол bash.ws)"
|
||||
|
||||
msgid "Do not panic, everything can be fixed, just..."
|
||||
msgstr "Не паникуйте, всё можно исправить, просто..."
|
||||
|
||||
@@ -1524,7 +1566,7 @@ msgid "e.g. SHA256 — leave empty for default"
|
||||
msgstr "напр. SHA256 — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "Each entry must be \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp), e.g. tcp:6881,udp:6881-6889"
|
||||
msgstr "Каждая запись — \"proto\", \"proto:port\" или \"proto:port-port\" (tcp/udp), напр. tcp:6881,udp:6881-6889"
|
||||
msgstr "Каждая запись должна быть \"proto\", \"proto:port\" или \"proto:port-port\" (tcp/udp), например: tcp:6881,udp:6881-6889"
|
||||
|
||||
msgid "Edit"
|
||||
msgstr "Изменить"
|
||||
@@ -1802,6 +1844,9 @@ msgstr "Не удалось определить тип DPI"
|
||||
msgid "Failed to execute"
|
||||
msgstr "Не удалось выполнить"
|
||||
|
||||
msgid "Failed to execute IP and DNS leak check"
|
||||
msgstr "Не удалось выполнить проверку утечек IP и DNS"
|
||||
|
||||
msgid "Failed to fetch releases"
|
||||
msgstr "Не удалось получить список релизов"
|
||||
|
||||
@@ -2348,6 +2393,9 @@ msgstr "Полезный груз init (I1)"
|
||||
msgid "Init timeout (Itime)"
|
||||
msgstr "Таймаут init (Itime)"
|
||||
|
||||
msgid "Initializing router-level IP & DNS leak test..."
|
||||
msgstr "Инициализация проверки утечек IP и DNS с роутера..."
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
@@ -2517,7 +2565,7 @@ msgid "Invalid Shadowsocks URL: missing credentials"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
|
||||
msgstr "Неверный Shadowsocks URL: отсутствует разделитель метода и пароля \":\""
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing port"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует порт"
|
||||
@@ -2660,6 +2708,12 @@ msgstr "Изолировать трафик BitTorrent и направить е
|
||||
msgid "Isolate Local LAN"
|
||||
msgstr "Изолировать локальную сеть"
|
||||
|
||||
msgid "ISP / Organization"
|
||||
msgstr "Провайдер / Организация"
|
||||
|
||||
msgid "ISP DNS LEAK"
|
||||
msgstr "УТЕЧКА DNS К ПРОВАЙДЕРУ"
|
||||
|
||||
msgid "Issues"
|
||||
msgstr "Проблемы"
|
||||
|
||||
@@ -2780,6 +2834,15 @@ msgstr "Установлена последняя версия"
|
||||
msgid "Latest version:"
|
||||
msgstr "Последняя версия:"
|
||||
|
||||
msgid "Leak detection completed"
|
||||
msgstr "Проверка утечек завершена"
|
||||
|
||||
msgid "Leak detection failed to complete"
|
||||
msgstr "Не удалось завершить проверку утечек"
|
||||
|
||||
msgid "LEAKED"
|
||||
msgstr "УТЕЧКА"
|
||||
|
||||
msgid "Leave empty to omit this header"
|
||||
msgstr "Оставьте пустым, чтобы пропустить этот заголовок"
|
||||
|
||||
@@ -2858,6 +2921,9 @@ msgstr "Загрузка..."
|
||||
msgid "Local Address"
|
||||
msgstr "Локальный адрес"
|
||||
|
||||
msgid "Location"
|
||||
msgstr "Местоположение"
|
||||
|
||||
msgid "Log Level"
|
||||
msgstr "Уровень логирования"
|
||||
|
||||
@@ -3078,13 +3144,13 @@ msgid "No active routing sections found."
|
||||
msgstr "Активные секции маршрутизации не найдены."
|
||||
|
||||
msgid "No benchmark data yet. Click \"Start Benchmark\" to begin."
|
||||
msgstr "Нет данных бенчмарка. Нажмите «Начать тест», чтобы запустить проверку."
|
||||
msgstr "Данные тестирования отсутствуют. Нажмите \"Запустить тест\" для начала."
|
||||
|
||||
msgid "No benchmark history yet. Run a benchmark to see results here."
|
||||
msgstr "История бенчмарков пуста. Запустите бенчмарк, чтобы увидеть результаты здесь."
|
||||
|
||||
msgid "No benchmark results yet. Configure options and click \"Start Benchmark\"."
|
||||
msgstr "Результатов бенчмарка пока нет. Настройте параметры и нажмите «Запустить бенчмарк»."
|
||||
msgstr "Результаты тестирования пока отсутствуют. Настройте параметры и нажмите \"Запустить тест\"."
|
||||
|
||||
msgid "No check targets found in the configuration."
|
||||
msgstr "Цели для проверки не найдены в конфигурации."
|
||||
@@ -3113,6 +3179,12 @@ msgstr "DHCP-клиенты не обнаружены"
|
||||
msgid "No diagnostic history available yet."
|
||||
msgstr "История проверок пока пуста."
|
||||
|
||||
msgid "No DNS resolvers captured via proxy test. Proxy may be offline or blocking test subdomains."
|
||||
msgstr "В тесте через прокси резолверы не зафиксированы. Возможно, прокси не работает или блокирует тестовые поддомены."
|
||||
|
||||
msgid "No DNS resolvers recorded"
|
||||
msgstr "DNS-резолверы не зафиксированы"
|
||||
|
||||
msgid "No domains detected yet"
|
||||
msgstr "Домены пока не обнаружены"
|
||||
|
||||
@@ -3242,6 +3314,12 @@ msgstr "Обфускация"
|
||||
msgid "Obfuscation password"
|
||||
msgstr "Пароль обфускации"
|
||||
|
||||
msgid "Observed Public IP"
|
||||
msgstr "Внешний публичный IP"
|
||||
|
||||
msgid "OFFLINE"
|
||||
msgstr "ОФЛАЙН"
|
||||
|
||||
msgid "Ollama (Local PC / Server)"
|
||||
msgstr "Ollama (локальный ПК / сервер)"
|
||||
|
||||
@@ -3312,13 +3390,13 @@ msgid "Optimize RAM memory"
|
||||
msgstr "Оптимизировать память (RAM)"
|
||||
|
||||
msgid "Option \"%s\" contains an invalid input value."
|
||||
msgstr "Параметр \"%s\" содержит недопустимое значение."
|
||||
msgstr "Опция \"%s\" содержит недопустимое значение."
|
||||
|
||||
msgid "Optional — leave empty for default"
|
||||
msgstr "Опционально — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "Optional context (e.g. \"Rostelecom, YouTube 4K buffering is slow\")..."
|
||||
msgstr "Дополнительный контекст (напр. «Ростелеком, YouTube 4K долго буферизуется»)..."
|
||||
msgstr "Дополнительный контекст (напр. \"Ростелеком, долго грузится YouTube 4K\")..."
|
||||
|
||||
msgid "Optional tls-auth or tls-crypt key"
|
||||
msgstr "Опциональный ключ tls-auth или tls-crypt"
|
||||
@@ -3440,6 +3518,9 @@ msgstr "Ожидает запуска"
|
||||
msgid "Pending..."
|
||||
msgstr "Ожидание..."
|
||||
|
||||
msgid "Performs simultaneous outbound checks via direct WAN (bypassing Sing-box redirect) and via proxy (127.0.0.1:4534) to verify that your real IP and DNS queries are not leaking to your ISP."
|
||||
msgstr "Выполняет одновременные исходящие запросы напрямую в WAN (в обход правил Sing-box) и через прокси (127.0.0.1:4534), проверяя, что ваш реальный IP и DNS-запросы не утекают провайдеру."
|
||||
|
||||
msgid "Periodically check the current level and switch to a faster server even when the current one works."
|
||||
msgstr "Периодически проверять текущий уровень и переключаться на более быстрый сервер, даже если текущий работает."
|
||||
|
||||
@@ -3462,7 +3543,7 @@ msgid "Pin on dashboard"
|
||||
msgstr "Закрепить на дашборде"
|
||||
|
||||
msgid "Pin P2P isolation to the torrent client's own ports, comma separated. Each entry is \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp). Without a port the whole protocol is routed direct."
|
||||
msgstr "Закрепить изоляцию P2P за портами торрент-клиента, через запятую. Каждая запись — \"протокол\", \"протокол:порт\" или \"протокол:порт-порт\" (tcp/udp). Без порта весь протокол уходит напрямую."
|
||||
msgstr "Привязка изоляции P2P к портам торрент-клиента через запятую. Каждая запись: \"proto\", \"proto:port\" или \"proto:port-port\" (tcp/udp). Без указания порта весь протокол направляется напрямую."
|
||||
|
||||
msgid "Plain-UDP DNS servers used as a last resort when all primary DNS fail. These are always unencrypted (no DoH/DoT). Example: 1.1.1.1, 8.8.8.8. Unlike the ISP fallback toggle, these are your own chosen servers."
|
||||
msgstr "Обычные UDP DNS-серверы, используемые как резерв при недоступности всех основных DNS. Всегда работают без шифрования (без DoH/DoT). Пример: 1.1.1.1, 8.8.8.8. В отличие от аварийного DNS провайдера, это выбранные вами серверы."
|
||||
@@ -3620,9 +3701,15 @@ msgstr "Интервал проверки здоровья прокси (с)"
|
||||
msgid "Proxy Health Check URL"
|
||||
msgstr "URL проверки здоровья прокси"
|
||||
|
||||
msgid "Proxy is offline or unreachable on 127.0.0.1:4534."
|
||||
msgstr "Прокси выключен или недоступен на 127.0.0.1:4534."
|
||||
|
||||
msgid "Proxy link is unavailable"
|
||||
msgstr "Ссылка прокси недоступна"
|
||||
|
||||
msgid "Proxy Outbound"
|
||||
msgstr "Исходящий прокси (туннель)"
|
||||
|
||||
msgid "Proxy Section for Bot"
|
||||
msgstr "Секция прокси для бота"
|
||||
|
||||
@@ -3635,9 +3722,15 @@ msgstr "PSK"
|
||||
msgid "Public host"
|
||||
msgstr "Публичный хост"
|
||||
|
||||
msgid "Public IP Address Isolation"
|
||||
msgstr "Изоляция публичного IP-адреса"
|
||||
|
||||
msgid "QR code is too large"
|
||||
msgstr "QR-код слишком большой"
|
||||
|
||||
msgid "Querying WAN direct socket and proxy outbound on 127.0.0.1:4534..."
|
||||
msgstr "Опрос прямого WAN-сокета и исходящего прокси на 127.0.0.1:4534..."
|
||||
|
||||
msgid "QUIC (HTTP/3)"
|
||||
msgstr "QUIC (HTTP/3)"
|
||||
|
||||
@@ -3653,6 +3746,9 @@ msgstr "Оценка"
|
||||
msgid "RDP"
|
||||
msgstr "RDP"
|
||||
|
||||
msgid "Re-run Leak Test"
|
||||
msgstr "Повторить проверку утечек"
|
||||
|
||||
msgid "Read error"
|
||||
msgstr "Ошибка чтения"
|
||||
|
||||
@@ -3791,6 +3887,9 @@ msgstr "Определить шлюз"
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr "Разрешать реальный IP для маршрутизации"
|
||||
|
||||
msgid "Resolver IP"
|
||||
msgstr "IP резолвера"
|
||||
|
||||
msgid "Resource selection by hostname inside nfqws is not supported here; sing-box selects resources before NFQUEUE."
|
||||
msgstr "Выбор ресурсов по имени хоста внутри nfqws не поддерживается здесь; sing-box выбирает ресурсы до NFQUEUE."
|
||||
|
||||
@@ -3884,6 +3983,12 @@ msgstr "Направлять весь не-локальный LAN-трафик
|
||||
msgid "Route bot API requests through this section. Leave empty to use the default mixed proxy."
|
||||
msgstr "Маршрутизировать запросы API бота через эту секцию. Оставьте пустым для использования смешанного прокси по умолчанию."
|
||||
|
||||
msgid "Route community list subnets (IP/CIDR)"
|
||||
msgstr "Маршрутизировать подсети списков сообщества (IP/CIDR)"
|
||||
|
||||
msgid "Route IP subnets associated with built-in community lists (e.g. Discord voice/media). Disable this if community subnets overlap with CDN/Anycast networks (such as Cloudflare/Google Cloud) and interfere with unrelated services (e.g. game launchers)."
|
||||
msgstr "Маршрутизировать IP-подсети из списков сообщества (например, голос/медиа Discord). Отключите, если подсети сообщества пересекаются с CDN/Anycast сетями (Cloudflare/Google Cloud) и мешают другим сервисам."
|
||||
|
||||
msgid "Route main DNS requests through the selected section."
|
||||
msgstr "Маршрутизировать основные DNS-запросы через выбранную секцию."
|
||||
|
||||
@@ -3971,6 +4076,9 @@ msgstr "Выполняется диагностика…"
|
||||
msgid "Russia (ru)"
|
||||
msgstr "Россия (ru)"
|
||||
|
||||
msgid "SAFE"
|
||||
msgstr "БЕЗОПАСНО"
|
||||
|
||||
msgid "SafeSearch"
|
||||
msgstr "SafeSearch"
|
||||
|
||||
@@ -4040,6 +4148,9 @@ msgstr "Секция для регистрации Cloudflare WARP API. Если
|
||||
msgid "Sections"
|
||||
msgstr "Секции"
|
||||
|
||||
msgid "SECURE"
|
||||
msgstr "ЗАЩИЩЕНО"
|
||||
|
||||
msgid "Security"
|
||||
msgstr "Безопасность"
|
||||
|
||||
@@ -4625,6 +4736,9 @@ msgstr "Tachyon: Бенчмарк и автоподбор DNS"
|
||||
msgid "Tachyon Error"
|
||||
msgstr "Ошибка Tachyon"
|
||||
|
||||
msgid "Tachyon IP & DNS Leak Detection"
|
||||
msgstr "Проверка утечек IP и DNS Tachyon"
|
||||
|
||||
msgid "Tachyon manages ciadpi pid files itself, so pidfile options are not allowed here."
|
||||
msgstr "Tachyon управляет файлами pid ciadpi самостоятельно, поэтому параметры pidfile здесь не допускаются."
|
||||
|
||||
@@ -4769,6 +4883,9 @@ msgstr "Тестирование"
|
||||
msgid "Testing bypass strategies in isolated sandbox..."
|
||||
msgstr "Тестирование стратегий обхода в изолированной песочнице..."
|
||||
|
||||
msgid "Testing DNS leak upstream resolvers with bash.ws protocol..."
|
||||
msgstr "Тестирование восходящих DNS-резолверов по протоколу bash.ws..."
|
||||
|
||||
msgid "Testing strategy"
|
||||
msgstr "Тестирование стратегии"
|
||||
|
||||
@@ -5156,6 +5273,9 @@ msgstr "Отдано"
|
||||
msgid "Upstream Mode"
|
||||
msgstr "Режим DNS Upstream"
|
||||
|
||||
msgid "Upstream Provider / ASN"
|
||||
msgstr "Восходящий провайдер / ASN"
|
||||
|
||||
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, hy2://, or tuic://"
|
||||
msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, hy2:// или tuic://"
|
||||
|
||||
@@ -5273,6 +5393,9 @@ msgstr "Значение должно быть положительным цел
|
||||
msgid "Value must not contain control characters"
|
||||
msgstr "Значение не должно содержать символы управления"
|
||||
|
||||
msgid "Verdict"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Verifying router and service readiness..."
|
||||
msgstr "Проверка готовности роутера и служб..."
|
||||
|
||||
@@ -5837,9 +5960,6 @@ msgstr "Маршрут"
|
||||
msgid "Resolved IP / DNS"
|
||||
msgstr "IP / DNS"
|
||||
|
||||
msgid "Verdict"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Check domain or IP (e.g. example.com)..."
|
||||
msgstr "Проверить домен или IP (напр. example.com)..."
|
||||
|
||||
|
||||
+2149
-1942
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,9 @@ function show_help() {
|
||||
print(" Validate an NFQWS2 strategy and print JSON result\n");
|
||||
print(" validate_byedpi_strategy_json\n");
|
||||
print(" Validate a ByeDPI ciadpi strategy and print JSON result\n");
|
||||
print(" leak_check [--pretty] Test for IP and DNS leaks (direct vs proxy outbound)\n");
|
||||
print(" check_ip_leak Check IP leak status (JSON)\n");
|
||||
print(" check_dns_leak Check DNS leak status via bash.ws (JSON)\n");
|
||||
}
|
||||
|
||||
function command_spec(command) {
|
||||
@@ -275,6 +278,9 @@ function command_spec(command) {
|
||||
validate_nfqws_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws-strategy-json", 1 ],
|
||||
validate_nfqws2_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws2-strategy-json", 1 ],
|
||||
validate_byedpi_strategy_json: [ "diagnostics/runtime.uc", "validate-byedpi-strategy-json", 1 ],
|
||||
leak_check: [ "diagnostics/leak_check.uc", "leak-check", 2 ],
|
||||
check_ip_leak: [ "diagnostics/leak_check.uc", "ip-leak", 0 ],
|
||||
check_dns_leak: [ "diagnostics/leak_check.uc", "dns-leak", 0 ],
|
||||
fuzzer_start: [ "diagnostics/fuzzer.uc", "start", 6 ],
|
||||
fuzzer_status: [ "diagnostics/fuzzer.uc", "status", 0 ],
|
||||
fuzzer_stop: [ "diagnostics/fuzzer.uc", "stop", 0 ],
|
||||
|
||||
@@ -227,9 +227,13 @@ function get_hosts_urls() {
|
||||
function hosts_list_update(target_url) {
|
||||
let urls = target_url != null && target_url != "" ? [target_url] : get_hosts_urls();
|
||||
if (length(urls) == 0) {
|
||||
log("No hosts list URLs configured", "warn");
|
||||
print('{"success":false,"error":"no_urls","entries":0}');
|
||||
return false;
|
||||
if (target_url != null && target_url != "") {
|
||||
log("Specified hosts list URL is empty", "warn");
|
||||
print('{"success":false,"error":"no_urls","entries":0}');
|
||||
return false;
|
||||
}
|
||||
print('{"success":true,"entries":0,"sources":0}');
|
||||
return true;
|
||||
}
|
||||
|
||||
mkdir_p(HOSTS_TMP_DIR);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let common = require("core.common");
|
||||
let constants = require("core.constants");
|
||||
let uci_core = require("core.uci");
|
||||
|
||||
let as_string = common.as_string;
|
||||
let command_capture = common.command_capture;
|
||||
let command_status = common.command_status;
|
||||
let shell_quote = common.shell_quote;
|
||||
|
||||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||
|
||||
let _has_so_mark_cached = null;
|
||||
function curl_supports_so_mark() {
|
||||
if (_has_so_mark_cached != null)
|
||||
return _has_so_mark_cached;
|
||||
_has_so_mark_cached = (command_status("curl --so-mark 0 -V >/dev/null 2>&1") == 0);
|
||||
return _has_so_mark_cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect active WAN network interface/device name.
|
||||
*/
|
||||
function get_wan_interface() {
|
||||
try {
|
||||
let cursor = uci_core.cursor();
|
||||
if (cursor) {
|
||||
cursor.load("network");
|
||||
let dev = cursor.get("network", "wan", "device") || cursor.get("network", "wan", "ifname");
|
||||
if (dev != null && dev != "")
|
||||
return trim(as_string(dev));
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to routing inspection below
|
||||
}
|
||||
|
||||
let route_res = command_capture("ip -4 route show default 2>/dev/null");
|
||||
if (route_res && route_res.status == 0 && route_res.output != "") {
|
||||
let m = match(route_res.output, /dev\s+([a-zA-Z0-9_\.\-]+)/);
|
||||
if (m && m[1])
|
||||
return trim(as_string(m[1]));
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read upstream ISP DNS servers assigned by DHCP/PPPoE from standard OpenWrt resolv files.
|
||||
*/
|
||||
function get_direct_dns_servers() {
|
||||
let servers = [];
|
||||
let seen = {};
|
||||
|
||||
let candidate_paths = [
|
||||
"/tmp/resolv.conf.d/resolv.conf.auto",
|
||||
"/tmp/resolv.conf.auto",
|
||||
"/tmp/resolv.conf"
|
||||
];
|
||||
|
||||
for (let path in candidate_paths) {
|
||||
let content = fs.readfile(path);
|
||||
if (content == null)
|
||||
continue;
|
||||
|
||||
let lines = split(as_string(content), "\n");
|
||||
for (let line in lines) {
|
||||
line = trim(line);
|
||||
if (substr(line, 0, 11) == "nameserver ") {
|
||||
let ns = trim(substr(line, 11));
|
||||
if (ns != "" && !seen[ns] &&
|
||||
ns != "127.0.0.1" && ns != "127.0.0.42" && ns != "127.0.0.43" &&
|
||||
ns != "::1" && index(ns, "127.0.") < 0) {
|
||||
seen[ns] = true;
|
||||
push(servers, {
|
||||
ip: ns,
|
||||
country: "",
|
||||
isp: "ISP / WAN Local",
|
||||
is_isp: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length(servers) > 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate curl CLI flags for direct WAN routing bypassing Tachyon tproxy.
|
||||
*/
|
||||
function get_direct_curl_flags(wan_iface) {
|
||||
let parts = [];
|
||||
if (wan_iface != null && wan_iface != "") {
|
||||
push(parts, "--interface " + shell_quote(wan_iface));
|
||||
}
|
||||
|
||||
if (curl_supports_so_mark()) {
|
||||
let mark = constants.NFT_OUTBOUND_MARK || "0x08000000";
|
||||
push(parts, "--so-mark " + shell_quote(mark));
|
||||
}
|
||||
|
||||
return join(" ", parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query public IP details with fallback services.
|
||||
*/
|
||||
function fetch_ip_info(use_proxy, wan_iface, mixed_port) {
|
||||
let proxy_flag = use_proxy ? ("--proxy http://127.0.0.1:" + mixed_port + " ") : "";
|
||||
let direct_flag = use_proxy ? "" : (get_direct_curl_flags(wan_iface) + " ");
|
||||
|
||||
let flags = "-s -m 7 --connect-timeout 4 " + proxy_flag + direct_flag;
|
||||
|
||||
// 1. Try ipleak.net JSON API
|
||||
let res = command_capture("curl " + flags + "https://ipleak.net/json/ 2>/dev/null");
|
||||
if (res && res.status == 0 && res.output != "") {
|
||||
try {
|
||||
let data = json(res.output);
|
||||
if (type(data) == "object" && data.ip != null && data.ip != "") {
|
||||
return {
|
||||
ip: as_string(data.ip),
|
||||
country: as_string(data.country_name || ""),
|
||||
country_code: as_string(data.country_code || ""),
|
||||
city: as_string(data.city_name || ""),
|
||||
isp: as_string(data.isp_name || ""),
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall through to fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to api.ipify.org
|
||||
res = command_capture("curl -s -m 5 --connect-timeout 3 " + proxy_flag + direct_flag + "'https://api.ipify.org?format=json' 2>/dev/null");
|
||||
if (res && res.status == 0 && res.output != "") {
|
||||
try {
|
||||
let data = json(res.output);
|
||||
if (type(data) == "object" && data.ip != null && data.ip != "") {
|
||||
return {
|
||||
ip: as_string(data.ip),
|
||||
country: "",
|
||||
country_code: "",
|
||||
city: "",
|
||||
isp: "",
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to ifconfig.me
|
||||
res = command_capture("curl -s -m 5 --connect-timeout 3 " + proxy_flag + direct_flag + "https://ifconfig.me/ip 2>/dev/null");
|
||||
if (res && res.status == 0 && res.output != "") {
|
||||
let ip = trim(as_string(res.output));
|
||||
if (ip != "" && match(ip, /^[0-9a-fA-F\.\:]+$/)) {
|
||||
return {
|
||||
ip: ip,
|
||||
country: "",
|
||||
country_code: "",
|
||||
city: "",
|
||||
isp: "",
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ip: "—",
|
||||
country: "",
|
||||
country_code: "",
|
||||
city: "",
|
||||
isp: "",
|
||||
ok: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for IP Leaks by comparing direct WAN IP and proxy outbound IP.
|
||||
*/
|
||||
function check_ip_leak(wan_iface, mixed_port) {
|
||||
mixed_port = mixed_port || common.get_mixed_port();
|
||||
wan_iface = wan_iface != null ? wan_iface : get_wan_interface();
|
||||
|
||||
let direct = fetch_ip_info(false, wan_iface, mixed_port);
|
||||
let proxy = fetch_ip_info(true, wan_iface, mixed_port);
|
||||
|
||||
let proxy_online = (proxy.ok && proxy.ip != "—");
|
||||
let leaked = true;
|
||||
|
||||
if (proxy_online && direct.ok && direct.ip != "—") {
|
||||
leaked = (proxy.ip == direct.ip);
|
||||
} else if (proxy_online) {
|
||||
leaked = false;
|
||||
} else {
|
||||
leaked = false;
|
||||
}
|
||||
|
||||
return {
|
||||
leaked: leaked,
|
||||
direct_ip: direct.ip,
|
||||
direct_country: direct.country,
|
||||
direct_country_code: direct.country_code,
|
||||
direct_city: direct.city,
|
||||
direct_isp: direct.isp,
|
||||
proxy_ip: proxy.ip,
|
||||
proxy_country: proxy.country,
|
||||
proxy_country_code: proxy.country_code,
|
||||
proxy_city: proxy.city,
|
||||
proxy_org: proxy.isp,
|
||||
proxy_online: proxy_online
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for DNS Leaks using the bash.ws DNS Leak detection protocol.
|
||||
*/
|
||||
function check_dns_leak(wan_iface, mixed_port, direct_ip, proxy_ip) {
|
||||
mixed_port = mixed_port || common.get_mixed_port();
|
||||
wan_iface = wan_iface != null ? wan_iface : get_wan_interface();
|
||||
|
||||
let direct_dns_servers = get_direct_dns_servers();
|
||||
|
||||
let direct_flags = get_direct_curl_flags(wan_iface);
|
||||
let proxy_flag = "--proxy http://127.0.0.1:" + mixed_port;
|
||||
|
||||
// 1. Get unique leak tokens from bash.ws/id
|
||||
let res_direct_id = command_capture("curl -s -m 6 --connect-timeout 4 " + direct_flags + " https://bash.ws/id 2>/dev/null");
|
||||
let direct_id = (res_direct_id && res_direct_id.status == 0) ? trim(as_string(res_direct_id.output)) : "";
|
||||
|
||||
let res_proxy_id = command_capture("curl -s -m 6 --connect-timeout 4 " + proxy_flag + " https://bash.ws/id 2>/dev/null");
|
||||
let proxy_id = (res_proxy_id && res_proxy_id.status == 0) ? trim(as_string(res_proxy_id.output)) : "";
|
||||
|
||||
// 2. Fire parallel DNS queries for bash.ws subdomains
|
||||
if (direct_id != "" || proxy_id != "") {
|
||||
let parallel_cmd = "{ ";
|
||||
if (direct_id != "") {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
parallel_cmd += sprintf("curl -s -m 3 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", direct_flags, i, direct_id);
|
||||
}
|
||||
}
|
||||
if (proxy_id != "") {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
parallel_cmd += sprintf("curl -s -m 3 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", proxy_flag, i, proxy_id);
|
||||
}
|
||||
}
|
||||
parallel_cmd += "wait; } 2>/dev/null";
|
||||
system(parallel_cmd);
|
||||
|
||||
// Allow 1.5s propagation time for upstream resolvers
|
||||
system("sleep 1.5 2>/dev/null || sleep 2 2>/dev/null");
|
||||
}
|
||||
|
||||
// 3. Fetch test results from bash.ws
|
||||
let direct_resolvers = [];
|
||||
if (direct_id != "") {
|
||||
let res = command_capture("curl -s -m 6 --connect-timeout 4 " + direct_flags + " 'https://bash.ws/dnsleak/test/" + direct_id + "?json' 2>/dev/null");
|
||||
if (res && res.status == 0 && res.output != "") {
|
||||
try {
|
||||
let parsed = json(res.output);
|
||||
if (type(parsed) == "array")
|
||||
direct_resolvers = parsed;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
let proxy_resolvers = [];
|
||||
if (proxy_id != "") {
|
||||
let res = command_capture("curl -s -m 6 --connect-timeout 4 " + proxy_flag + " 'https://bash.ws/dnsleak/test/" + proxy_id + "?json' 2>/dev/null");
|
||||
if (res && res.status == 0 && res.output != "") {
|
||||
try {
|
||||
let parsed = json(res.output);
|
||||
if (type(parsed) == "array")
|
||||
proxy_resolvers = parsed;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Build lookup table of direct / ISP DNS resolvers
|
||||
let direct_map = {};
|
||||
for (let s in direct_dns_servers) {
|
||||
if (s.ip) direct_map[s.ip] = true;
|
||||
}
|
||||
for (let r in direct_resolvers) {
|
||||
if (r.ip) direct_map[r.ip] = true;
|
||||
}
|
||||
|
||||
let formatted_proxy_servers = [];
|
||||
let leak_found = false;
|
||||
|
||||
for (let pr in proxy_resolvers) {
|
||||
if (type(pr) != "object" || !pr.ip) continue;
|
||||
|
||||
let is_direct_leak = (direct_map[pr.ip] == true);
|
||||
if (is_direct_leak) {
|
||||
leak_found = true;
|
||||
}
|
||||
|
||||
push(formatted_proxy_servers, {
|
||||
ip: pr.ip,
|
||||
country: as_string(pr.country || pr.country_name || ""),
|
||||
isp: as_string(pr.name || pr.asn || "Unknown"),
|
||||
is_isp: is_direct_leak
|
||||
});
|
||||
}
|
||||
|
||||
let formatted_direct_servers = [];
|
||||
for (let dr in direct_resolvers) {
|
||||
if (type(dr) != "object" || !dr.ip) continue;
|
||||
push(formatted_direct_servers, {
|
||||
ip: dr.ip,
|
||||
country: as_string(dr.country || dr.country_name || ""),
|
||||
isp: as_string(dr.name || dr.asn || "ISP Upstream"),
|
||||
is_isp: true
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: If bash.ws returned no direct resolvers, populate with OpenWrt system resolv servers
|
||||
if (length(formatted_direct_servers) == 0 && length(direct_dns_servers) > 0) {
|
||||
formatted_direct_servers = direct_dns_servers;
|
||||
}
|
||||
|
||||
let proxy_online = (proxy_id != "" || (proxy_ip != null && proxy_ip != "" && proxy_ip != "—"));
|
||||
let dns_leaked = false;
|
||||
|
||||
if (proxy_online && length(formatted_proxy_servers) > 0) {
|
||||
dns_leaked = leak_found;
|
||||
} else if (proxy_online && length(formatted_proxy_servers) == 0) {
|
||||
dns_leaked = false;
|
||||
}
|
||||
|
||||
// If proxy IP equals direct IP, then DNS is definitely not shielded
|
||||
if (proxy_ip != null && direct_ip != null && proxy_ip != "—" && direct_ip != "—" && proxy_ip == direct_ip) {
|
||||
dns_leaked = true;
|
||||
}
|
||||
|
||||
return {
|
||||
dns_leaked: dns_leaked,
|
||||
direct_ip: direct_ip || "—",
|
||||
proxy_ip: proxy_ip || "—",
|
||||
dns_servers: formatted_proxy_servers,
|
||||
direct_dns_servers: formatted_direct_servers,
|
||||
proxy_online: proxy_online
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified diagnostic check for both IP and DNS leaks.
|
||||
*/
|
||||
function run_leak_check(wan_iface, mixed_port) {
|
||||
mixed_port = mixed_port || common.get_mixed_port();
|
||||
wan_iface = wan_iface != null ? wan_iface : get_wan_interface();
|
||||
|
||||
let ip_res = check_ip_leak(wan_iface, mixed_port);
|
||||
let dns_res = check_dns_leak(wan_iface, mixed_port, ip_res.direct_ip, ip_res.proxy_ip);
|
||||
|
||||
return {
|
||||
ip_leak: ip_res,
|
||||
dns_leak: dns_res,
|
||||
timestamp: time()
|
||||
};
|
||||
}
|
||||
|
||||
function print_cli_summary(res) {
|
||||
print("\n=== TACHYON IP & DNS LEAK TEST REPORT ===\n\n");
|
||||
print(sprintf("Direct WAN IP : %s (%s, %s)\n", res.ip_leak.direct_ip, res.ip_leak.direct_country || "Unknown", res.ip_leak.direct_isp || "ISP"));
|
||||
print(sprintf("Proxy Outbound: %s (%s, %s)\n", res.ip_leak.proxy_ip, res.ip_leak.proxy_country || "Unknown", res.ip_leak.proxy_org || "Proxy"));
|
||||
|
||||
if (res.ip_leak.proxy_online) {
|
||||
if (res.ip_leak.leaked) {
|
||||
print("IP Status : ❌ LEAK DETECTED (Direct and Proxy IP match!)\n");
|
||||
} else {
|
||||
print("IP Status : ✅ SECURE (Real WAN IP is hidden behind proxy)\n");
|
||||
}
|
||||
} else {
|
||||
print("IP Status : ⚪ Proxy is offline or unreachable\n");
|
||||
}
|
||||
|
||||
print("\n--- DNS Resolvers Detected ---\n");
|
||||
if (length(res.dns_leak.dns_servers) > 0) {
|
||||
for (let s in res.dns_leak.dns_servers) {
|
||||
let flag = s.is_isp ? "❌ ISP LEAK" : "✅ SECURE";
|
||||
print(sprintf(" [%s] %s (%s - %s)\n", flag, s.ip, s.country, s.isp));
|
||||
}
|
||||
} else {
|
||||
print(" (No proxy DNS queries recorded or proxy offline)\n");
|
||||
}
|
||||
|
||||
if (res.dns_leak.dns_leaked) {
|
||||
print("\nDNS Status : ❌ DNS LEAK DETECTED (Queries reach your local ISP!)\n\n");
|
||||
} else {
|
||||
print("\nDNS Status : ✅ SECURE (All DNS routed through encrypted/proxy resolvers)\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
// --- CLI Dispatcher ---
|
||||
let mode = ARGV[0] || "";
|
||||
|
||||
if (mode == "leak-check" || mode == "leak_check") {
|
||||
let res = run_leak_check(null, null);
|
||||
if (ARGV[1] == "--pretty" || ARGV[1] == "-p") {
|
||||
print_cli_summary(res);
|
||||
} else {
|
||||
print(sprintf("%J\n", res));
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
else if (mode == "ip-leak" || mode == "check_ip_leak") {
|
||||
let res = check_ip_leak(null, null);
|
||||
print(sprintf("%J\n", res));
|
||||
exit(0);
|
||||
}
|
||||
else if (mode == "dns-leak" || mode == "check_dns_leak") {
|
||||
let res = check_dns_leak(null, null, null, null);
|
||||
print(sprintf("%J\n", res));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
return {
|
||||
get_wan_interface,
|
||||
get_direct_dns_servers,
|
||||
get_direct_curl_flags,
|
||||
fetch_ip_info,
|
||||
check_ip_leak,
|
||||
check_dns_leak,
|
||||
run_leak_check
|
||||
};
|
||||
@@ -257,6 +257,24 @@ function run_speedtest() {
|
||||
};
|
||||
}
|
||||
|
||||
function run_leak_check() {
|
||||
let leak_mod = require("diagnostics.leak_check");
|
||||
if (!leak_mod) return null;
|
||||
return leak_mod.run_leak_check(null, null);
|
||||
}
|
||||
|
||||
function run_ip_leak_check() {
|
||||
let leak_mod = require("diagnostics.leak_check");
|
||||
if (!leak_mod) return null;
|
||||
return leak_mod.check_ip_leak(null, null);
|
||||
}
|
||||
|
||||
function run_dns_leak_check() {
|
||||
let leak_mod = require("diagnostics.leak_check");
|
||||
if (!leak_mod) return null;
|
||||
return leak_mod.check_dns_leak(null, null, null, null);
|
||||
}
|
||||
|
||||
function manage_domain_list(action_type, domain, do_delete) {
|
||||
let c = uci_core.cursor();
|
||||
if (!c) return { success: false, error: "Не удалось инициализировать UCI" };
|
||||
@@ -372,6 +390,9 @@ return {
|
||||
get_clash_connections,
|
||||
check_connection,
|
||||
run_speedtest,
|
||||
run_leak_check,
|
||||
run_ip_leak_check,
|
||||
run_dns_leak_check,
|
||||
manage_domain_list,
|
||||
manage_domain_list_by_section,
|
||||
get_sections,
|
||||
|
||||
@@ -847,7 +847,8 @@ function start_main() {
|
||||
if (status != 0)
|
||||
return status;
|
||||
|
||||
module_background(HOSTS_UC, [ "list-update" ]);
|
||||
if (module_success(STATE_UC, [ "has-hosts-list-update-sources" ]))
|
||||
module_background(HOSTS_UC, [ "list-update" ]);
|
||||
|
||||
status = module_status(SINGBOX_UC, [ "configure-service" ]);
|
||||
if (status != 0)
|
||||
@@ -1402,7 +1403,7 @@ function reload(reason) {
|
||||
if (plan.needs_byedpi_restart == 1)
|
||||
module_success(BYEDPI_UC, [ "stop-runtime" ]);
|
||||
|
||||
if (plan.needs_hosts_update == 1)
|
||||
if (plan.needs_hosts_update == 1 && module_success(STATE_UC, [ "has-hosts-list-update-sources" ]))
|
||||
module_success(HOSTS_UC, [ "list-update" ]);
|
||||
|
||||
if (plan.needs_nft_rebuild == 1) {
|
||||
|
||||
@@ -1787,6 +1787,27 @@ function has_subscription_update_sources_from_sections(sections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function has_hosts_list_update_sources_from_sections(sections) {
|
||||
for (let section in sections) {
|
||||
section = object_or_empty(section);
|
||||
if (option(section, "action", "") != "hosts")
|
||||
continue;
|
||||
if (!bool_option(section, "enabled", true))
|
||||
continue;
|
||||
let urls = option(section, "hosts_list_urls", "");
|
||||
if (type(urls) == "array") {
|
||||
for (let u in urls) {
|
||||
if (trim(as_string(u)) != "")
|
||||
return true;
|
||||
}
|
||||
} else if (trim(as_string(urls)) != "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function fixture_sections(path) {
|
||||
let data = object_or_empty(read_json_file(path));
|
||||
connections.set_item_sections_from_data(data);
|
||||
@@ -2143,6 +2164,10 @@ else if (mode == "has-nft-list-update-sources")
|
||||
exit(has_nft_list_update_sources_from_sections(uci_sections("section")) ? 0 : 1);
|
||||
else if (mode == "has-nft-list-update-sources-fixture")
|
||||
exit(has_nft_list_update_sources_from_sections(fixture_sections(ARGV[1])) ? 0 : 1);
|
||||
else if (mode == "has-hosts-list-update-sources")
|
||||
exit(has_hosts_list_update_sources_from_sections(uci_sections("section")) ? 0 : 1);
|
||||
else if (mode == "has-hosts-list-update-sources-fixture")
|
||||
exit(has_hosts_list_update_sources_from_sections(fixture_sections(ARGV[1])) ? 0 : 1);
|
||||
else {
|
||||
warn("Usage: service/state.uc <operation> ...\n");
|
||||
exit(1);
|
||||
|
||||
@@ -74,6 +74,11 @@ echo "=== Test: hosts_list_status with no URLs ==="
|
||||
output=$(ucode "$HOSTS_UC" list-status 2>/dev/null || true)
|
||||
assert_eq "false" "$(echo "$output" | grep -o '"cache_exists":[a-z]*' | cut -d: -f2)" "cache_exists should be false initially"
|
||||
|
||||
# Run hosts_list_update with no URLs (should succeed cleanly with success: true and exit 0)
|
||||
echo "=== Test: hosts_list_update with no URLs ==="
|
||||
update_output=$(ucode "$HOSTS_UC" list-update 2>/dev/null || true)
|
||||
assert_eq "true" "$(echo "$update_output" | grep -o '"success":[a-z]*' | cut -d: -f2)" "list-update should succeed when no URLs configured"
|
||||
|
||||
# --- Test 5: Safe name length for long percent-encoded URLs ---
|
||||
echo "=== Test: safe_name length for long URLs ==="
|
||||
long_url_safe_len=$(ucode -e '
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
LEAK_CHECK="$TACHYON_LIB/diagnostics/leak_check.uc"
|
||||
BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
||||
API_UC="$TACHYON_LIB/service/api.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── 1. Syntax check & module load ──────────────────────────────────────────
|
||||
ucode -L "$TACHYON_LIB" -c "$LEAK_CHECK" || fail "leak_check.uc must pass ucode syntax check"
|
||||
ucode -S -L "$TACHYON_LIB" -c "$LEAK_CHECK" || fail "leak_check.uc must pass strict ucode syntax check"
|
||||
|
||||
# ─── 2. Test logic with synthetic inputs ────────────────────────────────────
|
||||
cat >"$WORK_DIR/test_leak_logic.uc" <<'UCODE'
|
||||
let leak_mod = require("diagnostics.leak_check");
|
||||
|
||||
if (type(leak_mod) != "object") {
|
||||
print("ERR: leak_check module must export object\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Test exports
|
||||
if (type(leak_mod.check_ip_leak) != "function" ||
|
||||
type(leak_mod.check_dns_leak) != "function" ||
|
||||
type(leak_mod.run_leak_check) != "function") {
|
||||
print("ERR: leak_check module missing required functions\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Test direct curl flags generation
|
||||
let flags = leak_mod.get_direct_curl_flags("eth0");
|
||||
if (index(flags, "--interface 'eth0'") < 0 && index(flags, "--interface eth0") < 0) {
|
||||
print("ERR: direct curl flags must specify wan interface\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
print("OK\n");
|
||||
exit(0);
|
||||
UCODE
|
||||
|
||||
ucode -L "$TACHYON_LIB" "$WORK_DIR/test_leak_logic.uc" | grep -q "OK" ||
|
||||
fail "leak_check.uc unit logic validation failed"
|
||||
|
||||
# ─── 3. Test CLI registration in /usr/bin/tachyon ───────────────────────────
|
||||
grep -Fq "leak_check" "$BIN" || fail "/usr/bin/tachyon must register leak_check"
|
||||
grep -Fq "check_ip_leak" "$BIN" || fail "/usr/bin/tachyon must register check_ip_leak"
|
||||
grep -Fq "check_dns_leak" "$BIN" || fail "/usr/bin/tachyon must register check_dns_leak"
|
||||
|
||||
# ─── 4. Test api.uc integration ─────────────────────────────────────────────
|
||||
grep -Fq "run_leak_check" "$API_UC" || fail "api.uc must export run_leak_check"
|
||||
grep -Fq "run_ip_leak_check" "$API_UC" || fail "api.uc must export run_ip_leak_check"
|
||||
grep -Fq "run_dns_leak_check" "$API_UC" || fail "api.uc must export run_dns_leak_check"
|
||||
|
||||
printf 'PASS: leak_check\n'
|
||||
@@ -1598,4 +1598,43 @@ if state_ucode has-remote-sing-box-ruleset-sources-fixture "$WORK_DIR/disabled-o
|
||||
fail "disabled rule should not require remote sing-box ruleset refresh"
|
||||
fi
|
||||
|
||||
cat >"$WORK_DIR/hosts-source.json" <<'JSON'
|
||||
{
|
||||
"section": [
|
||||
{
|
||||
".name": "my_hosts",
|
||||
".type": "section",
|
||||
"action": "hosts",
|
||||
"enabled": "1",
|
||||
"hosts_list_urls": [ "https://example.com/hosts.txt" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
state_ucode has-hosts-list-update-sources-fixture "$WORK_DIR/hosts-source.json" >/dev/null ||
|
||||
fail "enabled hosts section with URL should require hosts list update"
|
||||
|
||||
cat >"$WORK_DIR/hosts-no-url.json" <<'JSON'
|
||||
{
|
||||
"section": [
|
||||
{
|
||||
".name": "my_hosts",
|
||||
".type": "section",
|
||||
"action": "hosts",
|
||||
"enabled": "1",
|
||||
"dns_hosts": "127.0.0.1 example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
if state_ucode has-hosts-list-update-sources-fixture "$WORK_DIR/hosts-no-url.json" >/dev/null 2>&1; then
|
||||
fail "hosts section without remote URLs should not require hosts list update"
|
||||
fi
|
||||
|
||||
if state_ucode has-hosts-list-update-sources-fixture "$WORK_DIR/general-only-source.json" >/dev/null 2>&1; then
|
||||
fail "proxy sections without hosts should not require hosts list update"
|
||||
fi
|
||||
|
||||
printf 'runtime state predicate checks passed\n'
|
||||
|
||||
Reference in New Issue
Block a user