- Fix #50: Discord community subnets hijacking Cloudflare Anycast traffic: * Filter broad Cloudflare shared subnets (104.16.0.0/12, 172.64.0.0/13, etc.) for Discord preset in generator_routes.uc, nft/apply.uc, and updates.uc. * Add tachyon resolve_domain CLI and runtime.uc helper for DNS queries with rpcd ACL support. * Update LuCI Routing Rule Tracer to resolve queried domains against section subnets/CIDRs (including community subnets) and show shared CDN/Anycast warnings. * Add unit tests in tests/community_subnets_sanitization.sh. - Fix #51: Telegram bot spamming WAN up/down notifications on hotplug: * Check enable_badwan_interface_monitoring, telegram.enabled, and badwan_monitored_interfaces list in 99-tachyon-wan-monitor. * Add background debounce with atomic directory locking and ubus interface status verification. * Implement state transition tracking to only notify on actual UP->DOWN and DOWN->UP changes without duplicates. * Preserve retry_start_on_wan_up behavior on ifup. * Add comprehensive integration test suite in tests/wan_monitor_hotplug.sh.
This commit is contained in:
+504
-461
File diff suppressed because it is too large
Load Diff
+1431
-1372
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-11 18:51+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:51+0300\n"
|
||||
"POT-Creation-Date: 2026-09-11 22:54+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 22:54+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -26,6 +26,9 @@ msgstr "Списки .lst"
|
||||
msgid "(provider default)"
|
||||
msgstr "(по умолчанию провайдера)"
|
||||
|
||||
msgid "+ add"
|
||||
msgstr "+ добавить"
|
||||
|
||||
msgid "+ Add JSON outbound"
|
||||
msgstr "+ Добавить JSON outbound"
|
||||
|
||||
@@ -62,6 +65,9 @@ msgstr "⚠️ Если все Bootstrap DNS завершатся с ошибк
|
||||
msgid "⚠️ If all Main 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 "⚠️ Если все основные DNS завершатся с ошибкой 3 раза, запросы будут отправлены в DNS вашего провайдера в открытом виде. Используйте только в крайнем случае для предотвращения полной потери интернета."
|
||||
|
||||
msgid "⚠️ Server received fakes"
|
||||
msgstr "⚠️ Сервер получил фейки"
|
||||
|
||||
msgid "⚡ Fast (>1MB/s)"
|
||||
msgstr "⚡ Быстрые (>1 МБ/с)"
|
||||
|
||||
@@ -86,12 +92,18 @@ msgstr "✅ Только рабочие"
|
||||
msgid "✈️ Telegram Suite (Web + Bot API)"
|
||||
msgstr "✈️ Telegram (Веб + Bot API)"
|
||||
|
||||
msgid "✓ 32KB Streamed"
|
||||
msgstr "✓ 32KB Прокачано"
|
||||
|
||||
msgid "✔ Online"
|
||||
msgstr "✔ В сети"
|
||||
|
||||
msgid "✔ Running"
|
||||
msgstr "✔ Работает"
|
||||
|
||||
msgid "✗ Throttled at 16KB"
|
||||
msgstr "✗ Задушено на 16KB"
|
||||
|
||||
msgid "✘ Offline"
|
||||
msgstr "✘ Не в сети"
|
||||
|
||||
@@ -170,12 +182,6 @@ 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)"
|
||||
|
||||
msgid "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "Accept external proxy connections and route them with sing-box."
|
||||
msgstr "Принимать внешние прокси-подключения и маршрутизировать их через sing-box."
|
||||
|
||||
@@ -518,6 +524,9 @@ msgstr "Версия AmneziaWG"
|
||||
msgid "An unexpected error occurred during test"
|
||||
msgstr "Произошла непредвиденная ошибка во время проверки"
|
||||
|
||||
msgid "Analysis inconclusive."
|
||||
msgstr "Анализ не дал однозначного результата."
|
||||
|
||||
msgid "Anonymized support report copied to clipboard"
|
||||
msgstr "Анонимизированный отчет скопирован в буфер обмена"
|
||||
|
||||
@@ -755,6 +764,9 @@ msgstr "Заблокированные сайты"
|
||||
msgid "Blocked Sites (Domains)"
|
||||
msgstr "Заблокированные сайты (домены)"
|
||||
|
||||
msgid "Blocked Sites (Only block domains specified below)"
|
||||
msgstr "Заблокированные сайты (Блокировать только указанные домены)"
|
||||
|
||||
msgid "Blocks access to private/LAN IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) for selected users before applying routing mode."
|
||||
msgstr "Блокирует доступ к приватным/локальным IP-адресам (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 и др.) для указанных пользователей перед применением режима маршрутизации."
|
||||
|
||||
@@ -1382,6 +1394,9 @@ msgstr "DNS"
|
||||
msgid "DNS Auto-Tuner & Benchmark"
|
||||
msgstr "Автоподбор и бенчмарк DNS"
|
||||
|
||||
msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS Check Interval"
|
||||
msgstr "Интервал проверки DNS"
|
||||
|
||||
@@ -1418,6 +1433,9 @@ msgstr "Протокол DNS, используемый резолвером"
|
||||
msgid "DNS requests through section"
|
||||
msgstr "DNS-запросы через секцию"
|
||||
|
||||
msgid "DNS resolution failed — likely DNS-level blocking or hijacking."
|
||||
msgstr "Сбой разрешения DNS — вероятно, блокировка или перехват на уровне DNS."
|
||||
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr "TTL DNS-записей"
|
||||
|
||||
@@ -1466,6 +1484,12 @@ msgstr "Таймаут недоступности DNS"
|
||||
msgid "DNS Upstream Resolver Analysis (bash.ws protocol)"
|
||||
msgstr "Анализ восходящих DNS-резолверов (протокол bash.ws)"
|
||||
|
||||
msgid "DNS: Resolved %s to %s"
|
||||
msgstr "DNS: Разрешено %s в %s"
|
||||
|
||||
msgid "DNS: Resolved to %s (no subnet rules matched)"
|
||||
msgstr "DNS: Разрешено в %s (нет совпадений по правилам подсетей)"
|
||||
|
||||
msgid "Do not panic, everything can be fixed, just..."
|
||||
msgstr "Не паникуйте, всё можно исправить, просто..."
|
||||
|
||||
@@ -1568,6 +1592,9 @@ msgstr "Загружено"
|
||||
msgid "Downloading and applying rule sets and lists..."
|
||||
msgstr "Загрузка и применение наборов правил и списков..."
|
||||
|
||||
msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer."
|
||||
msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных."
|
||||
|
||||
msgid "DPI Detected"
|
||||
msgstr "DPI обнаружен"
|
||||
|
||||
@@ -1730,6 +1757,9 @@ msgstr "Конец интервала расписания (например, 08
|
||||
msgid "End Time (HH:MM)"
|
||||
msgstr "Время окончания (ЧЧ:ММ)"
|
||||
|
||||
msgid "endpoints OK"
|
||||
msgstr "эндпоинтов доступно"
|
||||
|
||||
msgid "Enforce SafeSearch & YouTube Restricted Mode"
|
||||
msgstr "Безопасный поиск (SafeSearch) и строгий режим YouTube"
|
||||
|
||||
@@ -2747,9 +2777,15 @@ msgstr "IP-адреса (IP CIDR)"
|
||||
msgid "IP and DNS leak check timed out"
|
||||
msgstr "Время ожидания проверки утечек IP и DNS истекло"
|
||||
|
||||
msgid "IP Block (TCP SYN Timeout)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
msgid "IP Frag Bounds (--ip-frag)"
|
||||
msgstr "Границы фрагментации IP (--ip-frag)"
|
||||
|
||||
msgid "IP-level blocking detected! The resource is blocked at the network level (no TCP SYN response). DPI bypass methods (Zapret / ByeDPI) cannot bypass this address — configure proxy/VPN routing (Sing-box) for this domain!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "IPs"
|
||||
msgstr "IP"
|
||||
|
||||
@@ -3218,6 +3254,9 @@ msgstr "История бенчмарков пуста. Запустите бе
|
||||
msgid "No benchmark results yet. Configure options and click \"Start Benchmark\"."
|
||||
msgstr "Результаты тестирования пока отсутствуют. Настройте параметры и нажмите \"Запустить тест\"."
|
||||
|
||||
msgid "No Blocking Detected"
|
||||
msgstr "Блокировка не обнаружена"
|
||||
|
||||
msgid "No check targets found in the configuration."
|
||||
msgstr "Цели для проверки не найдены в конфигурации."
|
||||
|
||||
@@ -4505,6 +4544,9 @@ msgstr "Настройки"
|
||||
msgid "Settings have been reset to defaults. The page will reload."
|
||||
msgstr "Настройки сброшены к значениям по умолчанию. Страница будет перезагружена."
|
||||
|
||||
msgid "Shared CDN / Anycast Warning"
|
||||
msgstr "Предупреждение: общий диапазон CDN / Anycast"
|
||||
|
||||
msgid "Shopping"
|
||||
msgstr "Магазины"
|
||||
|
||||
@@ -4772,6 +4814,12 @@ msgstr "Строгий ECH + DoQ DNS Enforcer"
|
||||
msgid "STUN (P2P Calls)"
|
||||
msgstr "STUN (P2P-звонки)"
|
||||
|
||||
msgid "Subnet %s belongs to a shared Anycast/CDN range (%s). Third-party websites hosted on this CDN will also be routed through this section."
|
||||
msgstr "Подсеть %s входит в публичный Anycast/CDN-диапазон (%s). Сторонние сайты, размещенные на этом CDN, также будут направляться через эту секцию."
|
||||
|
||||
msgid "Subnet %s is a shared Anycast/CDN range (%s). Third-party websites hosted on this CDN will also be routed through this section."
|
||||
msgstr "Подсеть %s является публичным Anycast/CDN-диапазоном (%s). Сторонние сайты, размещенные на этом CDN, также будут направляться через эту секцию."
|
||||
|
||||
msgid "Subnets from the list will be extracted and added to nftables"
|
||||
msgstr "Подсети из списка будут извлечены и добавлены в nftables"
|
||||
|
||||
@@ -4940,6 +4988,9 @@ msgstr "Пакет Tailscale установлен, но ни одна серве
|
||||
msgid "Target"
|
||||
msgstr "Цель"
|
||||
|
||||
msgid "Target accessible — no DPI blocking detected."
|
||||
msgstr "Цель доступна — блокировок DPI не обнаружено."
|
||||
|
||||
msgid "Target Family Profiles"
|
||||
msgstr "Семейные профили"
|
||||
|
||||
@@ -4949,6 +5000,15 @@ msgstr "Область действия"
|
||||
msgid "Target Service / Suite"
|
||||
msgstr "Целевой сервис / Набор"
|
||||
|
||||
msgid "TCP connect timed out before TLS handshake — host is blocked at the IP layer. DPI bypass cannot unblock this; route via Sing-box VPN/Proxy outbound instead."
|
||||
msgstr "Таймаут TCP-подключения до рукопожатия TLS — узел заблокирован на уровне IP. Методы обхода DPI не помогут; настройте маршрутизацию через прокси/VPN Sing-box."
|
||||
|
||||
msgid "TCP Reset Injection"
|
||||
msgstr "Внедрение пакетов TCP Reset (RST)"
|
||||
|
||||
msgid "TCP RST received from DPI — active TCP reset injection detected."
|
||||
msgstr "Получен TCP RST от ТСПУ/DPI — зафиксирована активная инъекция сброса соединения."
|
||||
|
||||
msgid "TCP Window Sizes (wsize)"
|
||||
msgstr "Размеры окна TCP (wsize)"
|
||||
|
||||
@@ -5021,6 +5081,9 @@ msgstr "Тестирование стратегии"
|
||||
msgid "Testing URL"
|
||||
msgstr "URL проверки"
|
||||
|
||||
msgid "Testing:"
|
||||
msgstr "Тестирование:"
|
||||
|
||||
msgid "Testing..."
|
||||
msgstr "Тестирование..."
|
||||
|
||||
@@ -5087,6 +5150,9 @@ msgstr "Все настройки Tachyon будут удалены и восс
|
||||
msgid "This will replace the current Tachyon settings with the snapshot"
|
||||
msgstr "Это заменит текущие настройки Tachyon на снапшот"
|
||||
|
||||
msgid "Throttling / Deep Inspection"
|
||||
msgstr "Замедление / Глубокий анализ пакетов"
|
||||
|
||||
msgid "Thu"
|
||||
msgstr "Чт"
|
||||
|
||||
@@ -5318,6 +5384,9 @@ msgstr "Неизвестный параметр ByeDPI: %s"
|
||||
msgid "Unknown country"
|
||||
msgstr "Неизвестная страна"
|
||||
|
||||
msgid "Unknown DPI Pattern"
|
||||
msgstr "Неизвестный тип DPI"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "Неизвестная ошибка"
|
||||
|
||||
@@ -5708,9 +5777,15 @@ msgstr "sing-box outbound для Zapret настроен"
|
||||
msgid "Zapret sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret не настроен"
|
||||
|
||||
msgid "Zapret v1"
|
||||
msgstr "Zapret v1"
|
||||
|
||||
msgid "Zapret v1 (nfqws)"
|
||||
msgstr "Zapret v1 (nfqws)"
|
||||
|
||||
msgid "Zapret v2"
|
||||
msgstr "Zapret v2"
|
||||
|
||||
msgid "Zapret v2 (nfqws2)"
|
||||
msgstr "Zapret v2 (nfqws2)"
|
||||
|
||||
@@ -5744,6 +5819,12 @@ msgstr "Правила маршрутизации sing-box для Zapret2 нас
|
||||
msgid "Zapret2 sing-box route rules are not configured"
|
||||
msgstr "Правила маршрутизации sing-box для Zapret2 не настроены"
|
||||
|
||||
msgid "Блокировка по IP (Таймаут TCP SYN)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
msgid "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "Block (RKN)"
|
||||
msgstr "RKN Block"
|
||||
|
||||
@@ -6506,72 +6587,6 @@ msgstr "Скачивание и обработка списков"
|
||||
msgid "Applying rules and subnet lists"
|
||||
msgstr "Применение правил и списков подсетей"
|
||||
|
||||
msgid "+ add"
|
||||
msgstr "+ добавить"
|
||||
|
||||
msgid "⚠️ Server received fakes"
|
||||
msgstr "⚠️ Сервер получил фейки"
|
||||
|
||||
msgid "✓ 32KB Streamed"
|
||||
msgstr "✓ 32KB Прокачано"
|
||||
|
||||
msgid "✗ Throttled at 16KB"
|
||||
msgstr "✗ Задушено на 16KB"
|
||||
|
||||
msgid "Analysis inconclusive."
|
||||
msgstr "Анализ не дал однозначного результата."
|
||||
|
||||
msgid "Blocked Sites (Only block domains specified below)"
|
||||
msgstr "Заблокированные сайты (Блокировать только указанные домены)"
|
||||
|
||||
msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS resolution failed — likely DNS-level blocking or hijacking."
|
||||
msgstr "Сбой разрешения DNS — вероятно, блокировка или перехват на уровне DNS."
|
||||
|
||||
msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer."
|
||||
msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных."
|
||||
|
||||
msgid "endpoints OK"
|
||||
msgstr "эндпоинтов доступно"
|
||||
|
||||
msgid "IP Block (TCP SYN Timeout)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
msgid "IP-level blocking detected! The resource is blocked at the network level (no TCP SYN response). DPI bypass methods (Zapret / ByeDPI) cannot bypass this address — configure proxy/VPN routing (Sing-box) for this domain!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "No Blocking Detected"
|
||||
msgstr "Блокировка не обнаружена"
|
||||
|
||||
msgid "Target accessible — no DPI blocking detected."
|
||||
msgstr "Цель доступна — блокировок DPI не обнаружено."
|
||||
|
||||
msgid "TCP connect timed out before TLS handshake — host is blocked at the IP layer. DPI bypass cannot unblock this; route via Sing-box VPN/Proxy outbound instead."
|
||||
msgstr "Таймаут TCP-подключения до рукопожатия TLS — узел заблокирован на уровне IP. Методы обхода DPI не помогут; настройте маршрутизацию через прокси/VPN Sing-box."
|
||||
|
||||
msgid "TCP Reset Injection"
|
||||
msgstr "Внедрение пакетов TCP Reset (RST)"
|
||||
|
||||
msgid "TCP RST received from DPI — active TCP reset injection detected."
|
||||
msgstr "Получен TCP RST от ТСПУ/DPI — зафиксирована активная инъекция сброса соединения."
|
||||
|
||||
msgid "Testing:"
|
||||
msgstr "Тестирование:"
|
||||
|
||||
msgid "Throttling / Deep Inspection"
|
||||
msgstr "Замедление / Глубокий анализ пакетов"
|
||||
|
||||
msgid "Unknown DPI Pattern"
|
||||
msgstr "Неизвестный тип DPI"
|
||||
|
||||
msgid "Zapret v1"
|
||||
msgstr "Zapret v1"
|
||||
|
||||
msgid "Zapret v2"
|
||||
msgstr "Zapret v2"
|
||||
|
||||
msgid "Allow only: "
|
||||
msgstr "Разрешить только: "
|
||||
|
||||
|
||||
@@ -11238,26 +11238,171 @@ const ACTION_COLORS = {
|
||||
hosts: "#2ecc71",
|
||||
};
|
||||
|
||||
const CLOUDFLARE_SHARED_CIDRS = [
|
||||
"104.16.0.0/12",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"162.158.0.0/15",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.159.0.0/16",
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"2606:4700::/32",
|
||||
"2400:cb00::/32",
|
||||
"2405:b500::/32",
|
||||
"2803:f800::/32",
|
||||
"2a06:98c0::/29",
|
||||
"2c0f:f248::/32",
|
||||
];
|
||||
|
||||
function ip4ToInt(ip) {
|
||||
return (
|
||||
ip
|
||||
.split(".")
|
||||
.reduce((int, octet) => (int << 8) + parseInt(octet, 10), 0) >>> 0
|
||||
);
|
||||
const octets = ip.split(".");
|
||||
if (octets.length !== 4) return null;
|
||||
let res = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const oct = parseInt(octets[i], 10);
|
||||
if (isNaN(oct) || oct < 0 || oct > 255) return null;
|
||||
res = ((res << 8) + oct) >>> 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function ipv6ToBigInt(ip) {
|
||||
try {
|
||||
ip = ip.toLowerCase();
|
||||
if (ip.includes(".")) {
|
||||
const lastColon = ip.lastIndexOf(":");
|
||||
if (lastColon === -1) return null;
|
||||
const v4Part = ip.substring(lastColon + 1);
|
||||
const v4Octets = v4Part.split(".");
|
||||
if (v4Octets.length !== 4) return null;
|
||||
const hexPart1 = (
|
||||
(parseInt(v4Octets[0], 10) << 8) |
|
||||
parseInt(v4Octets[1], 10)
|
||||
).toString(16);
|
||||
const hexPart2 = (
|
||||
(parseInt(v4Octets[2], 10) << 8) |
|
||||
parseInt(v4Octets[3], 10)
|
||||
).toString(16);
|
||||
ip = ip.substring(0, lastColon + 1) + hexPart1 + ":" + hexPart2;
|
||||
}
|
||||
|
||||
let parts = ip.split("::");
|
||||
if (parts.length > 2) return null;
|
||||
let head = parts[0] ? parts[0].split(":").filter(Boolean) : [];
|
||||
let tail =
|
||||
parts.length === 2 && parts[1] ? parts[1].split(":").filter(Boolean) : [];
|
||||
if (parts.length === 1 && head.length !== 8) return null;
|
||||
if (parts.length === 2 && head.length + tail.length > 7) return null;
|
||||
|
||||
let middleCount = 8 - (head.length + tail.length);
|
||||
let full = [...head];
|
||||
for (let i = 0; i < middleCount; i++) full.push("0");
|
||||
full = full.concat(tail);
|
||||
|
||||
let res = 0n;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let val = BigInt(parseInt(full[i], 16));
|
||||
if (val < 0n || val > 0xffffn) return null;
|
||||
res = (res << 16n) | val;
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function ipMatchesCidr(ip, cidr) {
|
||||
const parts = cidr.split("/");
|
||||
const subnet = parts[0];
|
||||
const bits = parts[1] ? parseInt(parts[1], 10) : 32;
|
||||
if (!ip || !cidr) return false;
|
||||
const parts = cidr.trim().split("/");
|
||||
const subnet = parts[0].trim();
|
||||
const bitsStr = parts[1];
|
||||
|
||||
if (bits === 0) return true;
|
||||
const isV6 = ip.includes(":") || subnet.includes(":");
|
||||
if (isV6) {
|
||||
const bits = bitsStr !== undefined ? parseInt(bitsStr, 10) : 128;
|
||||
if (isNaN(bits) || bits < 0 || bits > 128) return false;
|
||||
if (bits === 0) return true;
|
||||
const ipBig = ipv6ToBigInt(ip);
|
||||
const subnetBig = ipv6ToBigInt(subnet);
|
||||
if (ipBig === null || subnetBig === null) return false;
|
||||
const shift = 128n - BigInt(bits);
|
||||
return (ipBig >> shift) === (subnetBig >> shift);
|
||||
} else {
|
||||
const bits = bitsStr !== undefined ? parseInt(bitsStr, 10) : 32;
|
||||
if (isNaN(bits) || bits < 0 || bits > 32) return false;
|
||||
if (bits === 0) return true;
|
||||
const ipInt = ip4ToInt(ip);
|
||||
const subnetInt = ip4ToInt(subnet);
|
||||
if (ipInt === null || subnetInt === null) return false;
|
||||
const mask = (0xffffffff << (32 - bits)) >>> 0;
|
||||
return (ipInt & mask) === (subnetInt & mask);
|
||||
}
|
||||
}
|
||||
|
||||
const ipInt = ip4ToInt(ip);
|
||||
const subnetInt = ip4ToInt(subnet);
|
||||
function getSharedCdnWarning(cidr) {
|
||||
if (!cidr) return null;
|
||||
const normalized = cidr.trim();
|
||||
for (const cfCidr of CLOUDFLARE_SHARED_CIDRS) {
|
||||
if (normalized === cfCidr) {
|
||||
return _(
|
||||
"Subnet %s is a shared Anycast/CDN range (%s). Third-party websites hosted on this CDN will also be routed through this section.",
|
||||
).format(normalized, "Cloudflare");
|
||||
}
|
||||
}
|
||||
const ipOnly = normalized.split("/")[0];
|
||||
for (const cfCidr of CLOUDFLARE_SHARED_CIDRS) {
|
||||
if (ipMatchesCidr(ipOnly, cfCidr)) {
|
||||
return _(
|
||||
"Subnet %s belongs to a shared Anycast/CDN range (%s). Third-party websites hosted on this CDN will also be routed through this section.",
|
||||
).format(normalized, `Cloudflare ${cfCidr}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const mask = (~0 << (32 - bits)) >>> 0;
|
||||
return (ipInt & mask) === (subnetInt & mask);
|
||||
let _subnetListCache = {};
|
||||
|
||||
async function readSubnetListFile(service) {
|
||||
if (!service) return [];
|
||||
if (_subnetListCache[service]) return _subnetListCache[service];
|
||||
|
||||
const paths = [
|
||||
`/tmp/sing-box/rulesets/community-subnets-${service}.lst`,
|
||||
`/etc/tachyon/rulesets/community-subnets-${service}.lst`,
|
||||
];
|
||||
|
||||
for (const p of paths) {
|
||||
try {
|
||||
let content = await fs.read(p).catch(() => null);
|
||||
if (!content) {
|
||||
const res = await fs.exec("/bin/cat", [p]).catch(() => null);
|
||||
if (res && res.code === 0 && res.stdout) {
|
||||
content = res.stdout;
|
||||
}
|
||||
}
|
||||
if (content) {
|
||||
const lines = content
|
||||
.split("\n")
|
||||
.map((l) => l.trim().replace(/\r/g, ""))
|
||||
.filter((l) => l && !l.startsWith("#"));
|
||||
if (lines.length > 0) {
|
||||
_subnetListCache[service] = lines;
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_subnetListCache[service] = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
function matchIpInCidrs(ip, cidrs) {
|
||||
@@ -11470,6 +11615,48 @@ async function matchCommunityList(secName, community, query, type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function resolveDomainToIps(domain) {
|
||||
domain = (domain || "").trim().toLowerCase();
|
||||
if (!domain) return [];
|
||||
|
||||
// 1. Try Tachyon CLI: /usr/bin/tachyon resolve_domain <domain>
|
||||
try {
|
||||
const res = await fs
|
||||
.exec("/usr/bin/tachyon", ["resolve_domain", domain])
|
||||
.catch(() => null);
|
||||
if (res && res.code === 0 && res.stdout) {
|
||||
const parsed = JSON.parse(res.stdout.trim());
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Fallback: nslookup via /usr/bin/nslookup
|
||||
try {
|
||||
const res = await fs
|
||||
.exec("/usr/bin/nslookup", [domain])
|
||||
.catch(() => null);
|
||||
if (res && res.stdout) {
|
||||
const ips = [];
|
||||
const lines = res.stdout.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
const m = trimmed.match(/^Address\s*\d*:\s*([0-9a-fA-F:.]+)/);
|
||||
if (m) {
|
||||
const addr = m[1].split("#")[0].trim();
|
||||
if (!addr.startsWith("127.") && addr !== "::1" && addr !== "") {
|
||||
if (!ips.includes(addr)) ips.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ips.length > 0) return ips;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function performTrace(query) {
|
||||
query = (query || "").trim();
|
||||
if (!query) return { matched: false };
|
||||
@@ -11645,6 +11832,22 @@ async function performTrace(query) {
|
||||
uci.get(UCI_PACKAGE, secName, "community_lists"),
|
||||
);
|
||||
for (const community of communityLists) {
|
||||
const subnets = await readSubnetListFile(community);
|
||||
const matchedCidr = matchIpInCidrs(queryForMatching, subnets);
|
||||
if (matchedCidr) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Community Subnets (" + community + ")",
|
||||
pattern: matchedCidr,
|
||||
matchedIp: queryForMatching,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections,
|
||||
sharedCdnWarning: getSharedCdnWarning(matchedCidr),
|
||||
};
|
||||
}
|
||||
if (
|
||||
await matchCommunityList(secName, community, queryForMatching, type)
|
||||
) {
|
||||
@@ -11779,6 +11982,149 @@ async function performTrace(query) {
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "domain") {
|
||||
// No direct domain rule matched. Resolve domain to IP addresses and check IP routing rules
|
||||
const resolvedIps = await resolveDomainToIps(queryForMatching);
|
||||
if (resolvedIps && resolvedIps.length > 0) {
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const sec = sections[i];
|
||||
const secName = sec[".name"];
|
||||
if (uci.get(UCI_PACKAGE, secName, "enabled") === "0") continue;
|
||||
|
||||
const label = uci.get(UCI_PACKAGE, secName, "label") || secName;
|
||||
const action = uci.get(UCI_PACKAGE, secName, "action") || "connection";
|
||||
|
||||
const ipCidr = normalizeOptionValues(
|
||||
uci.get(UCI_PACKAGE, secName, "ip_cidr"),
|
||||
);
|
||||
const listsRuleset = await readRulesetFile(
|
||||
`/tmp/sing-box/rulesets/${secName}-lists-ruleset.json`,
|
||||
);
|
||||
const remoteSubnetRuleset = await readRulesetFile(
|
||||
`/tmp/sing-box/rulesets/${secName}-remote-subnet-ruleset.json`,
|
||||
);
|
||||
const communitySubnetsEnabled =
|
||||
uci.get(UCI_PACKAGE, secName, "community_subnets") !== "0";
|
||||
const communityLists = communitySubnetsEnabled
|
||||
? normalizeOptionValues(
|
||||
uci.get(UCI_PACKAGE, secName, "community_lists"),
|
||||
)
|
||||
: [];
|
||||
|
||||
for (const ip of resolvedIps) {
|
||||
const matchedCidr = matchIpInCidrs(ip, ipCidr);
|
||||
if (matchedCidr) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Resolved IP -> IPs (UCI)",
|
||||
pattern: `${matchedCidr} (resolved: ${ip})`,
|
||||
resolvedDomain: queryForMatching,
|
||||
resolvedIps: resolvedIps,
|
||||
matchedIp: ip,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections,
|
||||
sharedCdnWarning: getSharedCdnWarning(matchedCidr),
|
||||
};
|
||||
}
|
||||
|
||||
if (listsRuleset) {
|
||||
const matchedListIp = matchIpInRuleset(ip, listsRuleset);
|
||||
if (matchedListIp) {
|
||||
const domainIpLists = normalizeOptionValues(
|
||||
uci.get(UCI_PACKAGE, secName, "domain_ip_lists"),
|
||||
);
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Resolved IP -> IP/Subnet List (community/plain)",
|
||||
pattern: `${matchedListIp} (resolved: ${ip})`,
|
||||
sourceUrls: domainIpLists,
|
||||
resolvedDomain: queryForMatching,
|
||||
resolvedIps: resolvedIps,
|
||||
matchedIp: ip,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections,
|
||||
sharedCdnWarning: getSharedCdnWarning(matchedListIp),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteSubnetRuleset) {
|
||||
const matchedRemoteIp = matchIpInRuleset(ip, remoteSubnetRuleset);
|
||||
if (matchedRemoteIp) {
|
||||
const domainIpLists = normalizeOptionValues(
|
||||
uci.get(UCI_PACKAGE, secName, "domain_ip_lists"),
|
||||
);
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Resolved IP -> Remote Subnet List",
|
||||
pattern: `${matchedRemoteIp} (resolved: ${ip})`,
|
||||
sourceUrls: domainIpLists,
|
||||
resolvedDomain: queryForMatching,
|
||||
resolvedIps: resolvedIps,
|
||||
matchedIp: ip,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections,
|
||||
sharedCdnWarning: getSharedCdnWarning(matchedRemoteIp),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const community of communityLists) {
|
||||
const subnets = await readSubnetListFile(community);
|
||||
const matchedCommunityCidr = matchIpInCidrs(ip, subnets);
|
||||
if (matchedCommunityCidr) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: `Resolved IP -> Community Subnets (${community})`,
|
||||
pattern: `${matchedCommunityCidr} (resolved: ${ip})`,
|
||||
resolvedDomain: queryForMatching,
|
||||
resolvedIps: resolvedIps,
|
||||
matchedIp: ip,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections,
|
||||
sharedCdnWarning: getSharedCdnWarning(matchedCommunityCidr),
|
||||
};
|
||||
}
|
||||
if (await matchCommunityList(secName, community, ip, "ip")) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: `Resolved IP -> Community List (${community})`,
|
||||
pattern: `${ip} (resolved: ${ip})`,
|
||||
resolvedDomain: queryForMatching,
|
||||
resolvedIps: resolvedIps,
|
||||
matchedIp: ip,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections,
|
||||
sharedCdnWarning: getSharedCdnWarning(ip),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
matched: false,
|
||||
resolvedDomain: queryForMatching,
|
||||
resolvedIps: resolvedIps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
@@ -11932,9 +12278,46 @@ function createTracerSearchWidget(sectionRef) {
|
||||
]),
|
||||
],
|
||||
),
|
||||
...(result.resolvedIps && result.resolvedIps.length > 0
|
||||
? [
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style:
|
||||
"margin-top: 4px; font-size: 85%; color: var(--text-color-medium, #666);",
|
||||
},
|
||||
_("DNS: Resolved %s to %s").format(
|
||||
result.resolvedDomain,
|
||||
result.resolvedIps.join(", "),
|
||||
),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
);
|
||||
resultsEl.appendChild(matchDiv);
|
||||
|
||||
if (result.sharedCdnWarning) {
|
||||
const warnDiv = E(
|
||||
"div",
|
||||
{
|
||||
style:
|
||||
"margin-top: 8px; padding: 0.5rem 0.7rem; border-radius: 4px; background: rgba(243, 156, 18, 0.12); border-left: 3px solid var(--warning-color, #e67e22); font-size: 85%; color: var(--text-color-high, #333); line-height: 1.4;",
|
||||
},
|
||||
[
|
||||
E(
|
||||
"strong",
|
||||
{
|
||||
style:
|
||||
"color: var(--warning-color, #d35400); display: block; margin-bottom: 2px;",
|
||||
},
|
||||
["⚠️ ", _("Shared CDN / Anycast Warning")],
|
||||
),
|
||||
E("span", {}, result.sharedCdnWarning),
|
||||
],
|
||||
);
|
||||
resultsEl.appendChild(warnDiv);
|
||||
}
|
||||
} else {
|
||||
const noMatchDiv = E(
|
||||
"div",
|
||||
@@ -11945,6 +12328,20 @@ function createTracerSearchWidget(sectionRef) {
|
||||
[
|
||||
E("strong", {}, _("No Custom Rules Matched") + ": "),
|
||||
_("Traffic will go through the default route (Direct/Default)."),
|
||||
...(result && result.resolvedIps && result.resolvedIps.length > 0
|
||||
? [
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style:
|
||||
"margin-top: 4px; font-size: 85%; color: var(--text-color-medium, #666);",
|
||||
},
|
||||
_("DNS: Resolved to %s (no subnet rules matched)").format(
|
||||
result.resolvedIps.join(", "),
|
||||
),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
);
|
||||
resultsEl.appendChild(noMatchDiv);
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-11 18:51+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 18:51+0300\n"
|
||||
"POT-Creation-Date: 2026-09-11 22:54+0300\n"
|
||||
"PO-Revision-Date: 2026-09-11 22:54+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -26,6 +26,9 @@ msgstr "Списки .lst"
|
||||
msgid "(provider default)"
|
||||
msgstr "(по умолчанию провайдера)"
|
||||
|
||||
msgid "+ add"
|
||||
msgstr "+ добавить"
|
||||
|
||||
msgid "+ Add JSON outbound"
|
||||
msgstr "+ Добавить JSON outbound"
|
||||
|
||||
@@ -62,6 +65,9 @@ msgstr "⚠️ Если все Bootstrap DNS завершатся с ошибк
|
||||
msgid "⚠️ If all Main 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 "⚠️ Если все основные DNS завершатся с ошибкой 3 раза, запросы будут отправлены в DNS вашего провайдера в открытом виде. Используйте только в крайнем случае для предотвращения полной потери интернета."
|
||||
|
||||
msgid "⚠️ Server received fakes"
|
||||
msgstr "⚠️ Сервер получил фейки"
|
||||
|
||||
msgid "⚡ Fast (>1MB/s)"
|
||||
msgstr "⚡ Быстрые (>1 МБ/с)"
|
||||
|
||||
@@ -86,12 +92,18 @@ msgstr "✅ Только рабочие"
|
||||
msgid "✈️ Telegram Suite (Web + Bot API)"
|
||||
msgstr "✈️ Telegram (Веб + Bot API)"
|
||||
|
||||
msgid "✓ 32KB Streamed"
|
||||
msgstr "✓ 32KB Прокачано"
|
||||
|
||||
msgid "✔ Online"
|
||||
msgstr "✔ В сети"
|
||||
|
||||
msgid "✔ Running"
|
||||
msgstr "✔ Работает"
|
||||
|
||||
msgid "✗ Throttled at 16KB"
|
||||
msgstr "✗ Задушено на 16KB"
|
||||
|
||||
msgid "✘ Offline"
|
||||
msgstr "✘ Не в сети"
|
||||
|
||||
@@ -170,12 +182,6 @@ 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)"
|
||||
|
||||
msgid "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "Accept external proxy connections and route them with sing-box."
|
||||
msgstr "Принимать внешние прокси-подключения и маршрутизировать их через sing-box."
|
||||
|
||||
@@ -518,6 +524,9 @@ msgstr "Версия AmneziaWG"
|
||||
msgid "An unexpected error occurred during test"
|
||||
msgstr "Произошла непредвиденная ошибка во время проверки"
|
||||
|
||||
msgid "Analysis inconclusive."
|
||||
msgstr "Анализ не дал однозначного результата."
|
||||
|
||||
msgid "Anonymized support report copied to clipboard"
|
||||
msgstr "Анонимизированный отчет скопирован в буфер обмена"
|
||||
|
||||
@@ -755,6 +764,9 @@ msgstr "Заблокированные сайты"
|
||||
msgid "Blocked Sites (Domains)"
|
||||
msgstr "Заблокированные сайты (домены)"
|
||||
|
||||
msgid "Blocked Sites (Only block domains specified below)"
|
||||
msgstr "Заблокированные сайты (Блокировать только указанные домены)"
|
||||
|
||||
msgid "Blocks access to private/LAN IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) for selected users before applying routing mode."
|
||||
msgstr "Блокирует доступ к приватным/локальным IP-адресам (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 и др.) для указанных пользователей перед применением режима маршрутизации."
|
||||
|
||||
@@ -1382,6 +1394,9 @@ msgstr "DNS"
|
||||
msgid "DNS Auto-Tuner & Benchmark"
|
||||
msgstr "Автоподбор и бенчмарк DNS"
|
||||
|
||||
msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS Check Interval"
|
||||
msgstr "Интервал проверки DNS"
|
||||
|
||||
@@ -1418,6 +1433,9 @@ msgstr "Протокол DNS, используемый резолвером"
|
||||
msgid "DNS requests through section"
|
||||
msgstr "DNS-запросы через секцию"
|
||||
|
||||
msgid "DNS resolution failed — likely DNS-level blocking or hijacking."
|
||||
msgstr "Сбой разрешения DNS — вероятно, блокировка или перехват на уровне DNS."
|
||||
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr "TTL DNS-записей"
|
||||
|
||||
@@ -1466,6 +1484,12 @@ msgstr "Таймаут недоступности DNS"
|
||||
msgid "DNS Upstream Resolver Analysis (bash.ws protocol)"
|
||||
msgstr "Анализ восходящих DNS-резолверов (протокол bash.ws)"
|
||||
|
||||
msgid "DNS: Resolved %s to %s"
|
||||
msgstr "DNS: Разрешено %s в %s"
|
||||
|
||||
msgid "DNS: Resolved to %s (no subnet rules matched)"
|
||||
msgstr "DNS: Разрешено в %s (нет совпадений по правилам подсетей)"
|
||||
|
||||
msgid "Do not panic, everything can be fixed, just..."
|
||||
msgstr "Не паникуйте, всё можно исправить, просто..."
|
||||
|
||||
@@ -1568,6 +1592,9 @@ msgstr "Загружено"
|
||||
msgid "Downloading and applying rule sets and lists..."
|
||||
msgstr "Загрузка и применение наборов правил и списков..."
|
||||
|
||||
msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer."
|
||||
msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных."
|
||||
|
||||
msgid "DPI Detected"
|
||||
msgstr "DPI обнаружен"
|
||||
|
||||
@@ -1730,6 +1757,9 @@ msgstr "Конец интервала расписания (например, 08
|
||||
msgid "End Time (HH:MM)"
|
||||
msgstr "Время окончания (ЧЧ:ММ)"
|
||||
|
||||
msgid "endpoints OK"
|
||||
msgstr "эндпоинтов доступно"
|
||||
|
||||
msgid "Enforce SafeSearch & YouTube Restricted Mode"
|
||||
msgstr "Безопасный поиск (SafeSearch) и строгий режим YouTube"
|
||||
|
||||
@@ -2747,9 +2777,15 @@ msgstr "IP-адреса (IP CIDR)"
|
||||
msgid "IP and DNS leak check timed out"
|
||||
msgstr "Время ожидания проверки утечек IP и DNS истекло"
|
||||
|
||||
msgid "IP Block (TCP SYN Timeout)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
msgid "IP Frag Bounds (--ip-frag)"
|
||||
msgstr "Границы фрагментации IP (--ip-frag)"
|
||||
|
||||
msgid "IP-level blocking detected! The resource is blocked at the network level (no TCP SYN response). DPI bypass methods (Zapret / ByeDPI) cannot bypass this address — configure proxy/VPN routing (Sing-box) for this domain!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "IPs"
|
||||
msgstr "IP"
|
||||
|
||||
@@ -3218,6 +3254,9 @@ msgstr "История бенчмарков пуста. Запустите бе
|
||||
msgid "No benchmark results yet. Configure options and click \"Start Benchmark\"."
|
||||
msgstr "Результаты тестирования пока отсутствуют. Настройте параметры и нажмите \"Запустить тест\"."
|
||||
|
||||
msgid "No Blocking Detected"
|
||||
msgstr "Блокировка не обнаружена"
|
||||
|
||||
msgid "No check targets found in the configuration."
|
||||
msgstr "Цели для проверки не найдены в конфигурации."
|
||||
|
||||
@@ -4505,6 +4544,9 @@ msgstr "Настройки"
|
||||
msgid "Settings have been reset to defaults. The page will reload."
|
||||
msgstr "Настройки сброшены к значениям по умолчанию. Страница будет перезагружена."
|
||||
|
||||
msgid "Shared CDN / Anycast Warning"
|
||||
msgstr "Предупреждение: общий диапазон CDN / Anycast"
|
||||
|
||||
msgid "Shopping"
|
||||
msgstr "Магазины"
|
||||
|
||||
@@ -4772,6 +4814,12 @@ msgstr "Строгий ECH + DoQ DNS Enforcer"
|
||||
msgid "STUN (P2P Calls)"
|
||||
msgstr "STUN (P2P-звонки)"
|
||||
|
||||
msgid "Subnet %s belongs to a shared Anycast/CDN range (%s). Third-party websites hosted on this CDN will also be routed through this section."
|
||||
msgstr "Подсеть %s входит в публичный Anycast/CDN-диапазон (%s). Сторонние сайты, размещенные на этом CDN, также будут направляться через эту секцию."
|
||||
|
||||
msgid "Subnet %s is a shared Anycast/CDN range (%s). Third-party websites hosted on this CDN will also be routed through this section."
|
||||
msgstr "Подсеть %s является публичным Anycast/CDN-диапазоном (%s). Сторонние сайты, размещенные на этом CDN, также будут направляться через эту секцию."
|
||||
|
||||
msgid "Subnets from the list will be extracted and added to nftables"
|
||||
msgstr "Подсети из списка будут извлечены и добавлены в nftables"
|
||||
|
||||
@@ -4940,6 +4988,9 @@ msgstr "Пакет Tailscale установлен, но ни одна серве
|
||||
msgid "Target"
|
||||
msgstr "Цель"
|
||||
|
||||
msgid "Target accessible — no DPI blocking detected."
|
||||
msgstr "Цель доступна — блокировок DPI не обнаружено."
|
||||
|
||||
msgid "Target Family Profiles"
|
||||
msgstr "Семейные профили"
|
||||
|
||||
@@ -4949,6 +5000,15 @@ msgstr "Область действия"
|
||||
msgid "Target Service / Suite"
|
||||
msgstr "Целевой сервис / Набор"
|
||||
|
||||
msgid "TCP connect timed out before TLS handshake — host is blocked at the IP layer. DPI bypass cannot unblock this; route via Sing-box VPN/Proxy outbound instead."
|
||||
msgstr "Таймаут TCP-подключения до рукопожатия TLS — узел заблокирован на уровне IP. Методы обхода DPI не помогут; настройте маршрутизацию через прокси/VPN Sing-box."
|
||||
|
||||
msgid "TCP Reset Injection"
|
||||
msgstr "Внедрение пакетов TCP Reset (RST)"
|
||||
|
||||
msgid "TCP RST received from DPI — active TCP reset injection detected."
|
||||
msgstr "Получен TCP RST от ТСПУ/DPI — зафиксирована активная инъекция сброса соединения."
|
||||
|
||||
msgid "TCP Window Sizes (wsize)"
|
||||
msgstr "Размеры окна TCP (wsize)"
|
||||
|
||||
@@ -5021,6 +5081,9 @@ msgstr "Тестирование стратегии"
|
||||
msgid "Testing URL"
|
||||
msgstr "URL проверки"
|
||||
|
||||
msgid "Testing:"
|
||||
msgstr "Тестирование:"
|
||||
|
||||
msgid "Testing..."
|
||||
msgstr "Тестирование..."
|
||||
|
||||
@@ -5087,6 +5150,9 @@ msgstr "Все настройки Tachyon будут удалены и восс
|
||||
msgid "This will replace the current Tachyon settings with the snapshot"
|
||||
msgstr "Это заменит текущие настройки Tachyon на снапшот"
|
||||
|
||||
msgid "Throttling / Deep Inspection"
|
||||
msgstr "Замедление / Глубокий анализ пакетов"
|
||||
|
||||
msgid "Thu"
|
||||
msgstr "Чт"
|
||||
|
||||
@@ -5318,6 +5384,9 @@ msgstr "Неизвестный параметр ByeDPI: %s"
|
||||
msgid "Unknown country"
|
||||
msgstr "Неизвестная страна"
|
||||
|
||||
msgid "Unknown DPI Pattern"
|
||||
msgstr "Неизвестный тип DPI"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "Неизвестная ошибка"
|
||||
|
||||
@@ -5708,9 +5777,15 @@ msgstr "sing-box outbound для Zapret настроен"
|
||||
msgid "Zapret sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret не настроен"
|
||||
|
||||
msgid "Zapret v1"
|
||||
msgstr "Zapret v1"
|
||||
|
||||
msgid "Zapret v1 (nfqws)"
|
||||
msgstr "Zapret v1 (nfqws)"
|
||||
|
||||
msgid "Zapret v2"
|
||||
msgstr "Zapret v2"
|
||||
|
||||
msgid "Zapret v2 (nfqws2)"
|
||||
msgstr "Zapret v2 (nfqws2)"
|
||||
|
||||
@@ -5744,6 +5819,12 @@ msgstr "Правила маршрутизации sing-box для Zapret2 нас
|
||||
msgid "Zapret2 sing-box route rules are not configured"
|
||||
msgstr "Правила маршрутизации sing-box для Zapret2 не настроены"
|
||||
|
||||
msgid "Блокировка по IP (Таймаут TCP SYN)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
msgid "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "Block (RKN)"
|
||||
msgstr "RKN Block"
|
||||
|
||||
@@ -6506,72 +6587,6 @@ msgstr "Скачивание и обработка списков"
|
||||
msgid "Applying rules and subnet lists"
|
||||
msgstr "Применение правил и списков подсетей"
|
||||
|
||||
msgid "+ add"
|
||||
msgstr "+ добавить"
|
||||
|
||||
msgid "⚠️ Server received fakes"
|
||||
msgstr "⚠️ Сервер получил фейки"
|
||||
|
||||
msgid "✓ 32KB Streamed"
|
||||
msgstr "✓ 32KB Прокачано"
|
||||
|
||||
msgid "✗ Throttled at 16KB"
|
||||
msgstr "✗ Задушено на 16KB"
|
||||
|
||||
msgid "Analysis inconclusive."
|
||||
msgstr "Анализ не дал однозначного результата."
|
||||
|
||||
msgid "Blocked Sites (Only block domains specified below)"
|
||||
msgstr "Заблокированные сайты (Блокировать только указанные домены)"
|
||||
|
||||
msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS resolution failed — likely DNS-level blocking or hijacking."
|
||||
msgstr "Сбой разрешения DNS — вероятно, блокировка или перехват на уровне DNS."
|
||||
|
||||
msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer."
|
||||
msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных."
|
||||
|
||||
msgid "endpoints OK"
|
||||
msgstr "эндпоинтов доступно"
|
||||
|
||||
msgid "IP Block (TCP SYN Timeout)"
|
||||
msgstr "Блокировка по IP (Таймаут TCP SYN)"
|
||||
|
||||
msgid "IP-level blocking detected! The resource is blocked at the network level (no TCP SYN response). DPI bypass methods (Zapret / ByeDPI) cannot bypass this address — configure proxy/VPN routing (Sing-box) for this domain!"
|
||||
msgstr "Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!"
|
||||
|
||||
msgid "No Blocking Detected"
|
||||
msgstr "Блокировка не обнаружена"
|
||||
|
||||
msgid "Target accessible — no DPI blocking detected."
|
||||
msgstr "Цель доступна — блокировок DPI не обнаружено."
|
||||
|
||||
msgid "TCP connect timed out before TLS handshake — host is blocked at the IP layer. DPI bypass cannot unblock this; route via Sing-box VPN/Proxy outbound instead."
|
||||
msgstr "Таймаут TCP-подключения до рукопожатия TLS — узел заблокирован на уровне IP. Методы обхода DPI не помогут; настройте маршрутизацию через прокси/VPN Sing-box."
|
||||
|
||||
msgid "TCP Reset Injection"
|
||||
msgstr "Внедрение пакетов TCP Reset (RST)"
|
||||
|
||||
msgid "TCP RST received from DPI — active TCP reset injection detected."
|
||||
msgstr "Получен TCP RST от ТСПУ/DPI — зафиксирована активная инъекция сброса соединения."
|
||||
|
||||
msgid "Testing:"
|
||||
msgstr "Тестирование:"
|
||||
|
||||
msgid "Throttling / Deep Inspection"
|
||||
msgstr "Замедление / Глубокий анализ пакетов"
|
||||
|
||||
msgid "Unknown DPI Pattern"
|
||||
msgstr "Неизвестный тип DPI"
|
||||
|
||||
msgid "Zapret v1"
|
||||
msgstr "Zapret v1"
|
||||
|
||||
msgid "Zapret v2"
|
||||
msgstr "Zapret v2"
|
||||
|
||||
msgid "Allow only: "
|
||||
msgstr "Разрешить только: "
|
||||
|
||||
|
||||
+1431
-1372
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,12 @@
|
||||
],
|
||||
"/bin/cat": [
|
||||
"exec"
|
||||
],
|
||||
"/usr/bin/nslookup": [
|
||||
"exec"
|
||||
],
|
||||
"/bin/busybox": [
|
||||
"exec"
|
||||
]
|
||||
},
|
||||
"ubus": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Only process ifup and ifdown events for WAN interfaces
|
||||
# Hotplug handler for WAN retry on boot and interface state monitoring
|
||||
|
||||
case "$INTERFACE" in
|
||||
wan*|wwan*|lte*|modem*)
|
||||
@@ -11,25 +11,80 @@ case "$INTERFACE" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Rate limit Telegram notifications: max 1 notification per 60 seconds
|
||||
COOLDOWN_FILE="/tmp/tachyon_wan_monitor_cooldown"
|
||||
NOW=$(date +%s)
|
||||
if [ -f "$COOLDOWN_FILE" ]; then
|
||||
LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null || echo 0)
|
||||
DIFF=$((NOW - LAST))
|
||||
[ "$DIFF" -lt 60 ] && exit 0
|
||||
fi
|
||||
echo "$NOW" > "$COOLDOWN_FILE"
|
||||
|
||||
case "$INTERFACE" in
|
||||
wan*|wwan*|lte*|modem*)
|
||||
case "$ACTION" in
|
||||
ifup)
|
||||
/usr/bin/tachyon telegram send "🟢 Интерфейс *${INTERFACE}* поднялся!"
|
||||
;;
|
||||
ifdown)
|
||||
/usr/bin/tachyon telegram send "⚠️ Интерфейс *${INTERFACE}* упал!"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
case "$ACTION" in
|
||||
ifup|ifdown) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# 1. Check if interface monitoring is enabled in settings
|
||||
[ "$(uci -q get tachyon.settings.enable_badwan_interface_monitoring)" = "1" ] || exit 0
|
||||
|
||||
# 2. Check if Telegram bot is enabled
|
||||
[ "$(uci -q get tachyon.telegram.enabled)" = "1" ] || exit 0
|
||||
|
||||
# 3. Check if current interface is in badwan_monitored_interfaces
|
||||
MONITORED="$(uci -q get tachyon.settings.badwan_monitored_interfaces)"
|
||||
[ -n "$MONITORED" ] || exit 0
|
||||
|
||||
MATCHED=0
|
||||
for iface in $MONITORED; do
|
||||
if [ "$iface" = "$INTERFACE" ]; then
|
||||
MATCHED=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ "$MATCHED" -eq 1 ] || exit 0
|
||||
|
||||
# 4. Handle debounce, state tracking, and notifications in background to avoid blocking hotplug queue
|
||||
(
|
||||
STATE_DIR="/var/run/tachyon"
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||
LOCK_DIR="${STATE_DIR}/wan_monitor_${INTERFACE}.lock"
|
||||
|
||||
# Acquire per-interface lock with retry (max 5 seconds)
|
||||
acquired=0
|
||||
for _ in 1 2 3 4 5; do
|
||||
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
acquired=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$acquired" -eq 1 ] || exit 0
|
||||
|
||||
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
|
||||
|
||||
# Debounce delay to let interface state settle
|
||||
sleep 2
|
||||
|
||||
# Query actual interface status from ubus
|
||||
STATUS_JSON="$(ubus -S call "network.interface.${INTERFACE}" status 2>/dev/null)"
|
||||
CURRENT_STATE="down"
|
||||
case "$STATUS_JSON" in
|
||||
*'"up": true'*|*'"up":true'*)
|
||||
CURRENT_STATE="up"
|
||||
;;
|
||||
esac
|
||||
|
||||
STATE_FILE="${STATE_DIR}/wan_state_${INTERFACE}"
|
||||
PREV_STATE=""
|
||||
[ -f "$STATE_FILE" ] && PREV_STATE="$(cat "$STATE_FILE" 2>/dev/null)"
|
||||
|
||||
TACHYON_BIN="/usr/bin/tachyon"
|
||||
[ -x "$TACHYON_BIN" ] || TACHYON_BIN="$(command -v tachyon 2>/dev/null || echo /usr/bin/tachyon)"
|
||||
|
||||
if [ "$CURRENT_STATE" = "up" ]; then
|
||||
if [ "$PREV_STATE" = "down" ]; then
|
||||
echo "up" > "$STATE_FILE"
|
||||
"$TACHYON_BIN" telegram send "🟢 Интерфейс *${INTERFACE}* поднялся!"
|
||||
elif [ -z "$PREV_STATE" ]; then
|
||||
echo "up" > "$STATE_FILE"
|
||||
fi
|
||||
elif [ "$CURRENT_STATE" = "down" ]; then
|
||||
if [ "$PREV_STATE" != "down" ]; then
|
||||
echo "down" > "$STATE_FILE"
|
||||
"$TACHYON_BIN" telegram send "⚠️ Интерфейс *${INTERFACE}* упал!"
|
||||
fi
|
||||
fi
|
||||
) >/dev/null 2>&1 &
|
||||
|
||||
|
||||
@@ -318,7 +318,9 @@ function command_spec(command) {
|
||||
dns_benchmark_stop: [ "dns/benchmark.uc", "benchmark_stop", 0 ],
|
||||
dns_benchmark_apply: [ "dns/benchmark.uc", "benchmark_apply", 0 ],
|
||||
import_settings: [ "config/migration.uc", "import-settings", 1 ],
|
||||
"import-settings": [ "config/migration.uc", "import-settings", 1 ]
|
||||
"import-settings": [ "config/migration.uc", "import-settings", 1 ],
|
||||
resolve_domain: [ "diagnostics/runtime.uc", "resolve-domain", 1 ],
|
||||
"resolve-domain": [ "diagnostics/runtime.uc", "resolve-domain", 1 ]
|
||||
};
|
||||
|
||||
return commands[as_string(command)];
|
||||
|
||||
@@ -6,6 +6,14 @@ let helpers = require("core.helpers");
|
||||
let uci_core = require("core.uci");
|
||||
let connections = require("config.connections");
|
||||
|
||||
function core_ip_module_or_null() {
|
||||
try {
|
||||
return require("core.ip");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function core_url_module_or_null() {
|
||||
try {
|
||||
return require("core.url");
|
||||
@@ -2518,11 +2526,15 @@ function import_all_preset_lists(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
let core_ip = core_ip_module_or_null();
|
||||
let lines = split(as_string(fs.readfile(tmpfile)), "\n");
|
||||
for (let l in lines) {
|
||||
l = trim(replace(as_string(l), /\r/g, ""));
|
||||
if (l != "" && substr(l, 0, 1) != "#")
|
||||
if (l != "" && substr(l, 0, 1) != "#") {
|
||||
if (as_string(service) == "discord" && core_ip && core_ip.is_cloudflare_shared_cidr(l))
|
||||
continue;
|
||||
push(combined_lines, l);
|
||||
}
|
||||
}
|
||||
|
||||
if (downloaded)
|
||||
@@ -2584,12 +2596,20 @@ function import_builtin_subnets_from_rule(section, settings) {
|
||||
}
|
||||
}
|
||||
|
||||
let core_ip = core_ip_module_or_null();
|
||||
let lines = split(as_string(fs.readfile(tmpfile)), "\n");
|
||||
let filtered_for_nft = [];
|
||||
for (let l in lines) {
|
||||
l = trim(replace(as_string(l), /\r/g, ""));
|
||||
if (l != "" && substr(l, 0, 1) != "#")
|
||||
if (l != "" && substr(l, 0, 1) != "#") {
|
||||
if (as_string(service) == "discord" && core_ip && core_ip.is_cloudflare_shared_cidr(l))
|
||||
continue;
|
||||
push(combined_lines, l);
|
||||
push(filtered_for_nft, l);
|
||||
}
|
||||
}
|
||||
if (as_string(service) == "discord")
|
||||
write_file(tmpfile, join("\n", filtered_for_nft) + "\n");
|
||||
|
||||
nft_module_success([
|
||||
"nft-add-community-subnet-file-for-uci-section",
|
||||
|
||||
@@ -254,6 +254,24 @@ function normalize_to_cidrs(items) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const CLOUDFLARE_SHARED_CIDRS = [
|
||||
"104.16.0.0/12", "104.24.0.0/14", "172.64.0.0/13", "162.158.0.0/15",
|
||||
"108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
|
||||
"198.41.128.0/17", "162.159.0.0/16", "173.245.48.0/20", "103.21.244.0/22",
|
||||
"103.22.200.0/22", "103.31.4.0/22", "141.101.64.0/18",
|
||||
"2606:4700::/32", "2400:cb00::/32", "2405:b500::/32", "2803:f800::/32",
|
||||
"2a06:98c0::/29", "2c0f:f248::/32"
|
||||
];
|
||||
|
||||
function is_cloudflare_shared_cidr(value) {
|
||||
value = trim(as_string(value));
|
||||
for (let c in CLOUDFLARE_SHARED_CIDRS) {
|
||||
if (value == c)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
valid_ipv4,
|
||||
valid_ipv4_cidr,
|
||||
@@ -268,5 +286,8 @@ return {
|
||||
format_ipv6_tproxy_target,
|
||||
ipv6_supported,
|
||||
resolve_mac_to_ips,
|
||||
normalize_to_cidrs
|
||||
normalize_to_cidrs,
|
||||
CLOUDFLARE_SHARED_CIDRS,
|
||||
is_cloudflare_shared_cidr
|
||||
};
|
||||
|
||||
|
||||
@@ -5115,6 +5115,58 @@ function extract_ruleset(tag) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function resolve_domain_cli(domain) {
|
||||
domain = trim(as_string(domain));
|
||||
if (domain == "") {
|
||||
print("[]\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let m_dom = match(domain, /^https?:\/\/([^/:]+)/i);
|
||||
if (m_dom && m_dom[1])
|
||||
domain = m_dom[1];
|
||||
else {
|
||||
let parts = split(domain, "/");
|
||||
domain = split(parts[0], ":")[0];
|
||||
}
|
||||
domain = trim(domain);
|
||||
|
||||
let ips = [];
|
||||
let seen = {};
|
||||
|
||||
let add_ip = function(ip) {
|
||||
ip = trim(as_string(ip));
|
||||
if (ip == "" || ip == "127.0.0.1" || ip == "::1" || ip == "0.0.0.0" || seen[ip])
|
||||
return;
|
||||
if (!core_ip.valid_ip(ip))
|
||||
return;
|
||||
seen[ip] = true;
|
||||
push(ips, ip);
|
||||
};
|
||||
|
||||
let cmd_local = command_output_from_args([ "nslookup", domain, "127.0.0.1" ]);
|
||||
let cmd_def = command_output_from_args([ "nslookup", domain ]);
|
||||
let outputs = [ cmd_local, cmd_def ];
|
||||
|
||||
for (let out in outputs) {
|
||||
if (!out) continue;
|
||||
let past_header = false;
|
||||
for (let line in split(out, "\n")) {
|
||||
line = trim(as_string(line));
|
||||
if (match(line, /^(Name:|Non-authoritative answer:)/i))
|
||||
past_header = true;
|
||||
if (past_header) {
|
||||
let m = match(line, /Address(?:[ \t]+[0-9]+)?:[ \t]*([0-9a-fA-F:.]+)/);
|
||||
if (m && m[1] && m[1] != "127.0.0.1" && m[1] != "::1")
|
||||
add_ip(m[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print(sprintf("%J\n", ips));
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mode = ARGV[0] || "";
|
||||
|
||||
if (mode == "extract-ruleset")
|
||||
@@ -5230,6 +5282,8 @@ else if (mode == "validate-nfqws2-strategy-json")
|
||||
exit(validate_nfqws2_strategy_json(ARGV[1] || ""));
|
||||
else if (mode == "validate-byedpi-strategy-json")
|
||||
exit(validate_byedpi_strategy_json(ARGV[1] || ""));
|
||||
else if (mode == "resolve-domain")
|
||||
exit(resolve_domain_cli(ARGV[1] || ""));
|
||||
else {
|
||||
warn("Usage: diagnostics/runtime.uc <operation> ...\n");
|
||||
exit(1);
|
||||
|
||||
@@ -2005,6 +2005,39 @@ function nft_add_csv_chunks_to_family_sets(csv, table, ipv4_set, ipv6_set, kind,
|
||||
nft_add_csv_chunks_to_set(csv, table, ipv6_set, kind, ports_csv, chunk_size_text, 6);
|
||||
}
|
||||
|
||||
function nft_community_subnet_lines(path, service) {
|
||||
let data = fs.readfile(path);
|
||||
if (data == null)
|
||||
return [];
|
||||
|
||||
let result = [];
|
||||
for (let line in split(as_string(data), "\n")) {
|
||||
line = trim(replace(as_string(line), /\r/g, ""));
|
||||
if (line == "" || substr(line, 0, 1) == "#")
|
||||
continue;
|
||||
if (as_string(service) == "discord" && core_ip.is_cloudflare_shared_cidr(line))
|
||||
continue;
|
||||
push(result, line);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function nft_add_values_to_family_sets(values, table, ipv4_set, ipv6_set, kind, ports_csv, chunk_size_text) {
|
||||
let prep4 = nft_build_chunks_from_values(values, kind, ports_csv, chunk_size_text, 4);
|
||||
let ok4 = nft_add_chunks_to_set(table, ipv4_set, prep4.chunks, prep4.invalid);
|
||||
let prep6 = nft_build_chunks_from_values(values, kind, ports_csv, chunk_size_text, 6);
|
||||
let ok6 = nft_add_chunks_to_set(table, ipv6_set, prep6.chunks, prep6.invalid);
|
||||
return ok4 && ok6;
|
||||
}
|
||||
|
||||
function nft_add_community_subnet_file_to_family_sets(path, table, ipv4_set, ipv6_set, service, chunk_size_text) {
|
||||
let lines = nft_community_subnet_lines(path, service);
|
||||
if (length(lines) == 0)
|
||||
return true;
|
||||
return nft_add_values_to_family_sets(lines, table, ipv4_set, ipv6_set, "ips", "", chunk_size_text);
|
||||
}
|
||||
|
||||
function nft_add_inline_ip_cidr_matchers(csv, ports_csv, table, common_set, ip_port_set, chunk_size_text, common6_set, ip_port6_set) {
|
||||
if (as_string(csv) == "")
|
||||
return true;
|
||||
@@ -2658,7 +2691,7 @@ function nft_populate_runtime_set_for_section(section, deferred_sections, table,
|
||||
];
|
||||
for (let path in cached_paths) {
|
||||
if (helpers.file_is_usable(path, 50)) {
|
||||
nft_add_file_chunks_to_family_sets(path, table, sets.subnets, sets.subnets6, "ips", "", "5000");
|
||||
nft_add_community_subnet_file_to_family_sets(path, table, sets.subnets, sets.subnets6, service, "5000");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2739,10 +2772,17 @@ function nft_add_community_subnet_file_for_section(section, service, filepath, t
|
||||
if (!bool_option(section, "community_subnets", true))
|
||||
return true;
|
||||
|
||||
if (section_needs_priority_sets(section))
|
||||
return nft_add_subnet_file_for_section(section, filepath, table, common_set, ip_port_set, chunk_size_text, common6_set, ip_port6_set);
|
||||
let ports = section_rule_ports_csv(section);
|
||||
let sets = section_priority_sets(section);
|
||||
let common_v4 = section_needs_priority_sets(section) ? sets.subnets : common_set;
|
||||
let common_v6 = section_needs_priority_sets(section) ? sets.subnets6 : default_arg(common6_set, "tachyon_subnets6");
|
||||
|
||||
return nft_add_file_chunks_to_family_sets(filepath, table, common_set, default_arg(common6_set, "tachyon_subnets6"), "ips", "", chunk_size_text);
|
||||
if (section_needs_priority_sets(section) && ports != "" && !section_has_destination_matchers(section)) {
|
||||
let lines = nft_community_subnet_lines(filepath, service);
|
||||
return nft_add_values_to_family_sets(lines, table, sets.ip_ports, sets.ip6_ports, "ip-port-from-ip", ports, chunk_size_text);
|
||||
}
|
||||
|
||||
return nft_add_community_subnet_file_to_family_sets(filepath, table, common_v4, common_v6, service, chunk_size_text);
|
||||
}
|
||||
|
||||
function nft_add_subnet_file_for_uci_section(section_name, filepath, table, common_set, ip_port_set, chunk_size_text, common6_set, ip_port6_set) {
|
||||
|
||||
@@ -1456,8 +1456,11 @@ function load_community_subnet_cidrs(community) {
|
||||
let cidrs = [];
|
||||
for (let line in split(as_string(content), "\n")) {
|
||||
line = trim(replace(as_string(line), /\r/g, ""));
|
||||
if (line != "" && substr(line, 0, 1) != "#" && match(line, /^[0-9a-fA-F:.]+(\/[0-9]+)?$/) && !match(line, /\.$/))
|
||||
if (line != "" && substr(line, 0, 1) != "#" && match(line, /^[0-9a-fA-F:.]+(\/[0-9]+)?$/) && !match(line, /\.$/)) {
|
||||
if (service == "discord" && core_ip.is_cloudflare_shared_cidr(line))
|
||||
continue;
|
||||
push(cidrs, line);
|
||||
}
|
||||
}
|
||||
if (length(cidrs) > 0)
|
||||
return cidrs;
|
||||
@@ -2023,5 +2026,6 @@ return {
|
||||
add_server_routes,
|
||||
outbound_supports_udp,
|
||||
push_section_route_rule,
|
||||
is_valid_detour
|
||||
is_valid_detour,
|
||||
load_community_subnet_cidrs
|
||||
};
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LIB_DIR="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
TMP_DIR="$(mktemp -d /tmp/tachyon_community_subnets_test.XXXXXX)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
DISCORD_LST="$TMP_DIR/community-subnets-discord.lst"
|
||||
OTHER_LST="$TMP_DIR/community-subnets-other.lst"
|
||||
|
||||
cat > "$DISCORD_LST" << 'EOF'
|
||||
# Discord community subnets
|
||||
66.22.196.0/22
|
||||
104.16.0.0/12
|
||||
172.64.0.0/13
|
||||
162.158.0.0/15
|
||||
2606:4700::/32
|
||||
162.159.128.0/21
|
||||
EOF
|
||||
|
||||
cat > "$OTHER_LST" << 'EOF'
|
||||
# Other service
|
||||
104.16.0.0/12
|
||||
1.2.3.0/24
|
||||
EOF
|
||||
|
||||
# 1. Test core.ip is_cloudflare_shared_cidr
|
||||
ucode -L "$LIB_DIR" -e '
|
||||
let core_ip = require("core.ip");
|
||||
if (!core_ip.is_cloudflare_shared_cidr("104.16.0.0/12")) exit(1);
|
||||
if (!core_ip.is_cloudflare_shared_cidr("172.64.0.0/13")) exit(2);
|
||||
if (!core_ip.is_cloudflare_shared_cidr("162.158.0.0/15")) exit(3);
|
||||
if (!core_ip.is_cloudflare_shared_cidr("2606:4700::/32")) exit(4);
|
||||
if (core_ip.is_cloudflare_shared_cidr("66.22.196.0/22")) exit(5);
|
||||
if (core_ip.is_cloudflare_shared_cidr("162.159.128.0/21")) exit(6);
|
||||
if (core_ip.is_cloudflare_shared_cidr("1.1.1.1/32")) exit(7);
|
||||
' || fail "core.ip is_cloudflare_shared_cidr validation failed"
|
||||
|
||||
# 2. Test singbox.generator_routes load_community_subnet_cidrs filtering
|
||||
# Create temporary /tmp/sing-box/rulesets if needed or link to TMP_DIR
|
||||
mkdir -p /tmp/sing-box/rulesets
|
||||
cp "$DISCORD_LST" /tmp/sing-box/rulesets/community-subnets-discord.lst
|
||||
cp "$OTHER_LST" /tmp/sing-box/rulesets/community-subnets-other.lst
|
||||
|
||||
ucode -L "$LIB_DIR" -e '
|
||||
let generator_routes = require("singbox.generator_routes");
|
||||
let discord_cidrs = generator_routes.load_community_subnet_cidrs("discord");
|
||||
if (!discord_cidrs || length(discord_cidrs) != 2) {
|
||||
warn("Unexpected discord_cidrs length: " + length(discord_cidrs) + "\n");
|
||||
exit(1);
|
||||
}
|
||||
for (let c in discord_cidrs) {
|
||||
if (c == "104.16.0.0/12" || c == "172.64.0.0/13" || c == "162.158.0.0/15" || c == "2606:4700::/32") {
|
||||
warn("Found Cloudflare CIDR in discord_cidrs: " + c + "\n");
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
if (discord_cidrs[0] != "66.22.196.0/22" || discord_cidrs[1] != "162.159.128.0/21") {
|
||||
warn("Expected subnets not found: " + discord_cidrs + "\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
// Ensure non-discord service is NOT stripped of Cloudflare CIDRs
|
||||
let other_cidrs = generator_routes.load_community_subnet_cidrs("other");
|
||||
if (length(other_cidrs) != 2) exit(4);
|
||||
' || fail "singbox.generator_routes load_community_subnet_cidrs filtering failed"
|
||||
|
||||
# Clean up /tmp test files
|
||||
rm -f /tmp/sing-box/rulesets/community-subnets-discord.lst /tmp/sing-box/rulesets/community-subnets-other.lst
|
||||
|
||||
# 3. Test core.ip against discord file lines
|
||||
ucode -L "$LIB_DIR" -e '
|
||||
let fs = require("fs");
|
||||
let core_ip = require("core.ip");
|
||||
|
||||
let data = fs.readfile("'"$DISCORD_LST"'");
|
||||
let result = [];
|
||||
for (let line in split(data, "\n")) {
|
||||
line = trim(replace(line, /\r/g, ""));
|
||||
if (line == "" || substr(line, 0, 1) == "#") continue;
|
||||
if (core_ip.is_cloudflare_shared_cidr(line)) continue;
|
||||
push(result, line);
|
||||
}
|
||||
if (length(result) != 2) exit(1);
|
||||
if (result[0] != "66.22.196.0/22" || result[1] != "162.159.128.0/21") exit(2);
|
||||
' || fail "community subnet lines filtering failed"
|
||||
|
||||
# 4. Test diagnostics/runtime.uc resolve-domain mode
|
||||
output="$(ucode -L "$LIB_DIR" "$LIB_DIR/diagnostics/runtime.uc" resolve-domain "127.0.0.1" 2>/dev/null || true)"
|
||||
# Output should be valid JSON array (e.g. ["127.0.0.1"] or [] if nslookup filters loopback)
|
||||
echo "$output" | grep -q '^\[' || fail "diagnostics/runtime.uc resolve-domain must output a JSON array"
|
||||
|
||||
printf 'All community subnets sanitization tests PASSED!\n'
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MONITOR_SCRIPT="$ROOT_DIR/tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -f "$MONITOR_SCRIPT" ] || fail "99-tachyon-wan-monitor not found"
|
||||
|
||||
TMP_DIR="$(mktemp -d /tmp/tachyon_wan_monitor_test.XXXXXX)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
BIN_DIR="$TMP_DIR/bin"
|
||||
VAR_RUN="$TMP_DIR/var_run"
|
||||
mkdir -p "$BIN_DIR" "$VAR_RUN"
|
||||
|
||||
# Mock uci
|
||||
cat > "$BIN_DIR/uci" << 'EOF'
|
||||
#!/bin/sh
|
||||
cmd="$1"
|
||||
shift
|
||||
case "$cmd" in
|
||||
-q)
|
||||
subcmd="$1"
|
||||
shift
|
||||
if [ "$subcmd" = "get" ]; then
|
||||
key="$1"
|
||||
case "$key" in
|
||||
tachyon.settings.enable_badwan_interface_monitoring)
|
||||
cat "${TEST_UCI_DIR}/enable_badwan" 2>/dev/null || echo "0"
|
||||
;;
|
||||
tachyon.telegram.enabled)
|
||||
cat "${TEST_UCI_DIR}/telegram_enabled" 2>/dev/null || echo "0"
|
||||
;;
|
||||
tachyon.settings.badwan_monitored_interfaces)
|
||||
cat "${TEST_UCI_DIR}/monitored_ifaces" 2>/dev/null || echo ""
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/uci"
|
||||
|
||||
# Mock ubus
|
||||
cat > "$BIN_DIR/ubus" << 'EOF'
|
||||
#!/bin/sh
|
||||
# Check if call network.interface.<iface> status
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
network.interface.*)
|
||||
iface="${arg#network.interface.}"
|
||||
state="$(cat "${TEST_UCI_DIR}/ubus_state_${iface}" 2>/dev/null || echo "up")"
|
||||
if [ "$state" = "up" ]; then
|
||||
echo '{"up": true, "pending": false, "available": true}'
|
||||
else
|
||||
echo '{"up": false, "pending": false, "available": false}'
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo '{}'
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/ubus"
|
||||
|
||||
# Mock logger
|
||||
cat > "$BIN_DIR/logger" << 'EOF'
|
||||
#!/bin/sh
|
||||
echo "$@" >> "${TEST_LOG_FILE}"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/logger"
|
||||
|
||||
# Mock tachyon cli (for send_telegram_notification)
|
||||
cat > "$BIN_DIR/tachyon" << 'EOF'
|
||||
#!/bin/sh
|
||||
echo "$@" >> "${TEST_TELEGRAM_LOG}"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/tachyon"
|
||||
|
||||
export PATH="$BIN_DIR:$PATH"
|
||||
export TEST_UCI_DIR="$TMP_DIR/uci"
|
||||
export TEST_LOG_FILE="$TMP_DIR/logger.log"
|
||||
export TEST_TELEGRAM_LOG="$TMP_DIR/telegram.log"
|
||||
mkdir -p "$TEST_UCI_DIR"
|
||||
|
||||
# Patch monitor script temporarily to use our isolated /var/run/tachyon dir and fast debounce (0.1s instead of 2s)
|
||||
TEST_MONITOR="$TMP_DIR/monitor_under_test.sh"
|
||||
sed -e "s|/var/run/tachyon|${VAR_RUN}|g" \
|
||||
-e "s|sleep 2|sleep 0.1|g" \
|
||||
"$MONITOR_SCRIPT" > "$TEST_MONITOR"
|
||||
chmod +x "$TEST_MONITOR"
|
||||
|
||||
# Test 1: Monitoring disabled (enable_badwan = 0)
|
||||
echo "0" > "$TEST_UCI_DIR/enable_badwan"
|
||||
echo "1" > "$TEST_UCI_DIR/telegram_enabled"
|
||||
echo "wan" > "$TEST_UCI_DIR/monitored_ifaces"
|
||||
echo "down" > "$TEST_UCI_DIR/ubus_state_wan"
|
||||
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.2
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 1 failed: notification sent when enable_badwan=0"
|
||||
|
||||
# Test 2: Telegram disabled (telegram_enabled = 0)
|
||||
echo "1" > "$TEST_UCI_DIR/enable_badwan"
|
||||
echo "0" > "$TEST_UCI_DIR/telegram_enabled"
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.2
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 2 failed: notification sent when telegram.enabled=0"
|
||||
|
||||
# Test 3: Unmonitored interface (wan6 triggered, only wan is monitored)
|
||||
echo "1" > "$TEST_UCI_DIR/enable_badwan"
|
||||
echo "1" > "$TEST_UCI_DIR/telegram_enabled"
|
||||
echo "wan" > "$TEST_UCI_DIR/monitored_ifaces"
|
||||
echo "down" > "$TEST_UCI_DIR/ubus_state_wan6"
|
||||
ACTION="ifdown" INTERFACE="wan6" sh "$TEST_MONITOR"
|
||||
sleep 0.2
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 3 failed: notification sent for unmonitored interface wan6"
|
||||
|
||||
# Test 4: Monitored interface drops (wan ifdown, initial up -> down transition)
|
||||
echo "1" > "$TEST_UCI_DIR/enable_badwan"
|
||||
echo "1" > "$TEST_UCI_DIR/telegram_enabled"
|
||||
echo "wan" > "$TEST_UCI_DIR/monitored_ifaces"
|
||||
echo "up" > "$VAR_RUN/wan_state_wan" # interface was previously UP
|
||||
echo "down" > "$TEST_UCI_DIR/ubus_state_wan"
|
||||
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
# Wait for background verification
|
||||
sleep 0.3
|
||||
|
||||
[ -f "$TEST_TELEGRAM_LOG" ] || fail "Test 4 failed: no notification sent on UP -> DOWN transition"
|
||||
grep -Fq "Интерфейс *wan* упал!" "$TEST_TELEGRAM_LOG" || fail "Test 4 failed: expected drop message not found in log"
|
||||
[ "$(cat "$VAR_RUN/wan_state_wan")" = "down" ] || fail "Test 4 failed: state file not set to down"
|
||||
|
||||
# Test 5: Duplicate hotplug ifdown while state is already down (NO repeated spam)
|
||||
rm -f "$TEST_TELEGRAM_LOG"
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.3
|
||||
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 5 failed: duplicate notification sent on repeated ifdown without state change"
|
||||
|
||||
# Test 6: Interface recovers (wan ifup, transition down -> up)
|
||||
rm -f "$TEST_TELEGRAM_LOG"
|
||||
echo "up" > "$TEST_UCI_DIR/ubus_state_wan"
|
||||
|
||||
ACTION="ifup" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.3
|
||||
|
||||
[ -f "$TEST_TELEGRAM_LOG" ] || fail "Test 6 failed: no notification sent on DOWN -> UP recovery"
|
||||
grep -Fq "Интерфейс *wan* поднялся!" "$TEST_TELEGRAM_LOG" || fail "Test 6 failed: expected recovery message not found in log"
|
||||
[ "$(cat "$VAR_RUN/wan_state_wan")" = "up" ] || fail "Test 6 failed: state file not set to up"
|
||||
|
||||
# Test 7: Duplicate hotplug ifup while state is already up (NO duplicate message)
|
||||
rm -f "$TEST_TELEGRAM_LOG"
|
||||
ACTION="ifup" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.3
|
||||
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 7 failed: duplicate notification sent on repeated ifup without state change"
|
||||
|
||||
printf 'All 7 WAN monitor hotplug tests PASSED!\n'
|
||||
Reference in New Issue
Block a user