feat(fuzzer): автоматический подбор и тюнинг DPI-стратегий для Zapret v2, Zapret v1 и ByeDPI
This commit is contained in:
+2
-1
@@ -15,4 +15,5 @@ scratch/
|
||||
tmp_*/
|
||||
tmp_test*.uc
|
||||
test_out.txt
|
||||
uc.out
|
||||
uc.out
|
||||
filtered-bin/
|
||||
@@ -1,53 +1,55 @@
|
||||
# 04. DPI Bypass & Desynchronization Engines
|
||||
# 04. Движки обхода DPI и десинхронизации пакетов
|
||||
|
||||
Tachyon integrates three distinct local packet desynchronization engines that operate entirely on the router hardware without requiring an external proxy server (Zero-VPS mode).
|
||||
Tachyon интегрирует три независимых локальных движка десинхронизации сетевых пакетов, которые функционируют непосредственно на оборудовании роутера без необходимости использования внешнего VPS или прокси-сервера (режим «Zero-VPS»).
|
||||
|
||||
---
|
||||
|
||||
## 1. Comparison Matrix: Zapret v1 vs Zapret v2 vs ByeDPI
|
||||
## 1. Сравнительная матрица: Zapret v1 vs Zapret v2 vs ByeDPI
|
||||
|
||||
| Feature | Zapret v1 (`nfqws`) | Zapret v2 (`nfqws2`) | ByeDPI (`ciadpi`) |
|
||||
| Параметр | Zapret v1 (`nfqws`) | Zapret v2 (`nfqws2`) | ByeDPI (`ciadpi`) |
|
||||
|---|---|---|---|
|
||||
| **Binary** | `/usr/bin/nfqws` | `/usr/bin/nfqws2` | `/usr/bin/ciadpi` |
|
||||
| **Operating Model** | Kernel NFQueue Intercept | Kernel NFQueue Multi-Q | Local SOCKS5 / TProxy |
|
||||
| **Target Protocols** | HTTP, TLS (ClientHello) | HTTP, TLS, QUIC, WireGuard | HTTP, TLS |
|
||||
| **Packet Splitting** | `split2`, `disorder`, `fake` | `multisplit`, `seqovl`, `wsize`, `fake` | `--split-pos`, `--disorder`, `--fake` |
|
||||
| **Multi-Vector Chains** | Single strategy per rule | Multi-phase composite chains | Composite SOCKS args |
|
||||
| **CPU Overhead** | Minimal (< 1%) | Low (< 2%) | Low (< 2%) |
|
||||
| **Best For** | Legacy OpenWrt, Basic DPI | YouTube 4K, Discord, TSPU | Non-root users, SOCKS routing |
|
||||
| **Исполняемый файл** | `/usr/bin/nfqws` | `/usr/bin/nfqws2` | `/usr/bin/ciadpi` |
|
||||
| **Модель работы** | Перехват через Kernel NFQueue | Multi-Queue NFQueue с Lua | Локальный SOCKS5 / TProxy |
|
||||
| **Целевые протоколы** | HTTP, TLS (ClientHello) | HTTP, TLS, QUIC, WireGuard, UDP | HTTP, TLS |
|
||||
| **Методы фрагментации** | `split2`, `disorder`, `fake` | `multisplit`, `seqovl`, `wsize`, `fake` | `--split-pos`, `--disorder`, `--fake` |
|
||||
| **Многоуровневые цепочки** | 1 стратегия на правило | Сложные составные цепочки | Составные аргументы SOCKS |
|
||||
| **Нагрузка на CPU** | Минимальная (< 1%) | Низкая (< 2%) | Низкая (< 2%) |
|
||||
| **Оптимально для** | Базового DPI, старых роутеров | YouTube 4K, Discord, ТСПУ | Не-root окружений, SOCKS-роутинга |
|
||||
|
||||
---
|
||||
|
||||
## 2. Desynchronization Techniques Explained
|
||||
## 2. Принципы и техники десинхронизации пакетов
|
||||
|
||||
Deep Packet Inspection (DPI) systems inspect packet headers (such as TLS Server Name Indication `SNI` or HTTP `Host` headers) to detect and block access. Desynchronization tricks the DPI into losing track of the connection state while the destination server reconstructs the stream normally.
|
||||
Системы глубокого анализа пакетов (DPI / ТСПУ) анализируют заголовки сетевых протоколов (такие как `SNI` в расширении TLS ClientHello или заголовок `Host` в HTTP) для блокировки или замедления трафика.
|
||||
|
||||
Десинхронизация обманывает конечный автомат DPI, заставляя его потерять контекст TCP-сессии или пропустить целевой заголовок, в то время как целевой сервер корректно собирает исходный поток данных.
|
||||
|
||||
```
|
||||
Original Stream: [TLS ClientHello with SNI] ──────────► (Blocked by DPI)
|
||||
Исходный поток: [TLS ClientHello с SNI] ──────────► (Блокируется ТСПУ / DPI)
|
||||
|
||||
Desynchronized: [Fake Bad Checksum Packet] ──────────► (DPI resets state)
|
||||
[Payload Part 1: Snippet] ──────────► (DPI misses SNI)
|
||||
[Payload Part 2: Remainder] ─────────► (Server merges stream)
|
||||
Десинхронизация: [Fake-пакет с неверной суммой/TTL] ──► (DPI сбрасывает состояние)
|
||||
[Фрагмент 1: Начало ClientHello] ───► (DPI не видит полный SNI)
|
||||
[Фрагмент 2: Окончание данных] ─────► (Сервер собирает TCP-поток)
|
||||
```
|
||||
|
||||
### 2.1. Key Desync Strategies
|
||||
### 2.1. Основные стратегии обхода
|
||||
|
||||
1. **`split2` / `multisplit`**:
|
||||
* Splits the TCP segment right inside the TLS SNI or HTTP Host header. The DPI does not reassemble small fragments, while the end server's TCP stack reconstructs the full buffer seamlessly.
|
||||
2. **`disorder` / `disorder2`**:
|
||||
* Sends the second half of the packet before the first half with appropriate TCP sequence numbers. The DPI sees out-of-order data and skips inspection.
|
||||
3. **`fake` / `fake_sni`**:
|
||||
* Sends a fake TLS ClientHello packet with a low TTL (Time To Live). The DPI intercepts the fake packet and approves the connection; the packet dies before reaching the destination server.
|
||||
4. **`seqovl` (Sequence Overlap)**:
|
||||
* Sends a fake packet with overlapping sequence numbers that the server's TCP stack discards while the DPI's state machine is fooled.
|
||||
5. **`wsize` (Window Size Manipulation)**:
|
||||
* Forces a small TCP window size to prevent DPI from receiving full payloads in single bursts.
|
||||
1. **`split2` / `multisplit` (Многопозиционное разделение)**:
|
||||
* Разрезает TCP-сегмент непосредственно внутри поля TLS SNI или заголовка HTTP Host. Оборудование DPI не выполняет глубокую реассемблинг-сборку мелких фрагментов, а стек TCP целевого сервера склеивает поток без потерь.
|
||||
2. **`disorder` / `disorder2` (Изменение порядка доставки)**:
|
||||
* Отправляет вторую часть пакета раньше первой с корректными TCP Sequence Numbers. DPI видит фрагменты не по порядку и пропускает инспекцию.
|
||||
3. **`fake` / `fake_sni` (Внедрение фейковых пакетов)**:
|
||||
* Отправляет поддельный пакет TLS ClientHello с намеренно заниженным TTL (Time To Live). DPI перехватывает поддельный пакет и считает соединение легитимным, после чего пакет угасает в сети, не доходя до целевого сервера.
|
||||
4. **`seqovl` (Наложение порядковых номеров / Sequence Overlap)**:
|
||||
* Посылает пакет с перекрывающимся диапазоном TCP Sequence. Стек TCP на сервере отбрасывает дублирующиеся байты, тогда как DPI парсит неверные данные и теряет контроль над сессией.
|
||||
5. **`wsize` (Манипуляция размером окна TCP Window Size)**:
|
||||
* Принудительно устанавливает размер TCP-окна в 1–2 байта, заставляя клиент слать данные микросегментами, что ломает эвристики DPI.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration & Runtime Management
|
||||
## 3. Конфигурация и управление в UCI
|
||||
|
||||
### 3.1. Zapret v2 Configuration Example (`/etc/config/tachyon`)
|
||||
### 3.1. Пример настройки Zapret v2 (`/etc/config/tachyon`)
|
||||
|
||||
```ini
|
||||
config provider 'zapret2'
|
||||
@@ -68,7 +70,7 @@ config rule 'youtube_rule'
|
||||
list domain_pattern 'discord.com'
|
||||
```
|
||||
|
||||
### 3.2. ByeDPI Configuration Example
|
||||
### 3.2. Пример настройки ByeDPI
|
||||
|
||||
```ini
|
||||
config provider 'byedpi'
|
||||
@@ -81,42 +83,54 @@ config provider 'byedpi'
|
||||
|
||||
---
|
||||
|
||||
## 4. NFQueue Multiplexing & Conflict Prevention
|
||||
## 4. Мультиплексирование NFQueue и изоляция очередей
|
||||
|
||||
* Kernel `nfnetlink_queue` requires unique queue numbers per daemon instance.
|
||||
* Tachyon's `providers/nfqueue/runtime.uc` dynamically manages queue allocation:
|
||||
* Default Zapret v1 queue: `200`
|
||||
* Default Zapret v2 queue: `201`
|
||||
* If both Zapret v1 and v2 are enabled simultaneously for different rules, Tachyon enforces strict queue isolation and prevents PID collisions.
|
||||
* Подсистема ядра Linux `nfnetlink_queue` требует уникальный номер очереди (`qnum`) для каждого запущенного процесса демона.
|
||||
* Модуль `providers/nfqueue/runtime.uc` в Tachyon динамически управляет выделением очередей:
|
||||
* Базовая очередь Zapret v1: `200`
|
||||
* Базовая очередь Zapret v2: `201`
|
||||
* Очередь Strategy Fuzzer: `298` / `299`
|
||||
* При одновременной активации Zapret v1 и Zapret v2 для разных правил маршрутизации Tachyon гарантирует строгую изоляцию очередей и исключает конфликты PID и fwmark.
|
||||
|
||||
## 4. AmneziaWG в sing-box-extended: верификация (2026-08)
|
||||
---
|
||||
|
||||
Проверено против бинарника релиза `v1.13.18-extended-2.6.5` (shtorm-7/sing-box-extended):
|
||||
## 5. Интеграция AmneziaWG в sing-box-extended (2026)
|
||||
|
||||
- `endpoint.amnezia` полностью реализован: `AmneziaOptions` (`transport/wireguard/endpoint_options.go`)
|
||||
→ IPC `jc/jmin/jmax/s1-s4/h1-h4/i1-i5` (`transport/wireguard/endpoint.go`)
|
||||
→ wireguard-go форк `shtorm-7/wireguard-go v0.0.4-extended-1.5.3`:
|
||||
I1-I5 шлются отдельными пакетами ПЕРЕД handshake initiation
|
||||
(`device/send.go SendHandshakeInitiation`), формат значения — hex-строка.
|
||||
- Парсер конфига строгий: неизвестные поля и мусорные значения в `amnezia.*`
|
||||
дают FATAL при старте, «молчаливого игнорирования» не существует.
|
||||
- Генерация Tachyon (`generator_outbounds.uc add_awg_endpoint`) совпадает со
|
||||
схемой форка: peer содержит required `allowed_ips`, `pre_shared_key` и
|
||||
`persistent_keepalive_interval` опциональны, hex i1-i5 проходит
|
||||
`uci_bin_to_hex` без двойного кодирования.
|
||||
Проверено против релизного бинарника `v1.13.18-extended-2.6.5` (`shtorm-7/sing-box-extended`):
|
||||
|
||||
Следствие для диагностики: если AWG/WARP-туннель «TX есть, RX нет» на
|
||||
sing-box-extended, конфиг секции применён корректно — причина вне Tachyon
|
||||
(сервер, провайдер, UDP-фильтрация). Чек 6c доктора (1.2.91+) сообщает это
|
||||
пользователю по свидетельствам из лога ядра.
|
||||
- Секция `endpoint.amnezia` полностью реализована в форке: `AmneziaOptions` (`transport/wireguard/endpoint_options.go`) → параметры IPC `jc / jmin / jmax / s1-s4 / h1-h4 / i1-i5`.
|
||||
- Форк `shtorm-7/wireguard-go v0.0.4-extended-1.5.3`: пакеты `I1–I5` отправляются отдельными дейтаграммами **перед** инициализацией рукопожатия (`SendHandshakeInitiation`), значение задается в hex-формате.
|
||||
- Генератор Tachyon (`singbox/generator_outbounds.uc` -> `add_awg_endpoint`) формирует конфигурацию, полностью совместимую со схемой форка:
|
||||
- Секция `peer` содержит обязательный `allowed_ips`.
|
||||
- Параметры `pre_shared_key` и `persistent_keepalive_interval` опциональны.
|
||||
- Hex-значения `i1–i5` передаются через `uci_bin_to_hex` без повторного кодирования.
|
||||
- **Диагностика:** если AWG/WARP-туннель имеет состояние «TX есть, RX нет» на sing-box-extended, конфиг применен корректно — причина во внешней UDP-фильтрации провайдера или блокировке сервера.
|
||||
|
||||
Кейс «kernel работает, sing-box нет» с идентичным conf остаётся внешним:
|
||||
различие в реализации special-handshake между amneziawg-linux kernel-модом
|
||||
и userspace-форком может быть критично только для конкретных строгих
|
||||
серверов. Шаги диагностики для таких случаев:
|
||||
---
|
||||
|
||||
1. `tcpdump -i any -X udp port <порт>` — сравнить первые байты I1-пакетов
|
||||
kernel vs sing-box (у special-handshake пакетов префикс `c7`).
|
||||
2. Временно убрать i1-i5 из секции: если handshake поднимется — сервер
|
||||
не требует special handshake, а требует его некорректную передачу.
|
||||
3. Попробовать вариант ядра sing-box-lx как эксперимент.
|
||||
## 6. Strategy Fuzzer и автоматический подбор стратегий
|
||||
|
||||
Tachyon включает встроенный модуль автоматического тестирования стратегий обхода — **Strategy Fuzzer & Auto-Tuner** (`diagnostics/fuzzer.uc`), который избавляет от необходимости вручную подбирать флаги `nfqws2` или `ciadpi`.
|
||||
|
||||
### 6.1. Механизм работы
|
||||
- **Изолированный сэндбокс:** Тестирование выполняется через временные очереди `qnum 298/299` с сокетной меткой `0x08000000` (для Zapret) либо через локальный порт `127.0.0.1:11089` (для ByeDPI), не затрагивая активный домашний трафик.
|
||||
- **Метрический скоринг:** Замеряются время TLS-рукопожатия, TTFB (время до первого байта), битрейт загрузки чанка потокового видео (MB/s) и HTTP-код ответа.
|
||||
- **1-Click Apply:** Победившая стратегия мгновенно применяется к правилу YouTube/Discord или в глобальные параметры провайдера с автоматической перезагрузкой службы.
|
||||
|
||||
### 6.2. Команды управления через CLI
|
||||
```sh
|
||||
# Запуск бенчмарка стратегий
|
||||
tachyon fuzzer_start <engine> <target> [custom_url] [rule_section]
|
||||
|
||||
# Просмотр текущего прогресса и результатов
|
||||
tachyon fuzzer_status
|
||||
|
||||
# Остановка фонового бенчмарка
|
||||
tachyon fuzzer_stop
|
||||
|
||||
# Применение выбранной стратегии в UCI
|
||||
tachyon fuzzer_apply <engine> "<strategy_args>" [target_rule_or_global]
|
||||
|
||||
# Вывод доступной матрицы стратегий
|
||||
tachyon fuzzer_strategies
|
||||
```
|
||||
|
||||
@@ -2,6 +2,51 @@ import fs from 'fs/promises';
|
||||
|
||||
const dictionary = {
|
||||
"Service Health Check": "Проверка доступности сервисов",
|
||||
"Automated DPI Strategy Fuzzer & Auto-Tuner": "Автоматический подборщик и оптимизатор стратегий DPI",
|
||||
"Benchmarks a suite of packet desynchronization strategies in an isolated sandbox without disrupting active traffic, ranking them by latency, TTFB, and throughput.": "Тестирует набор стратегий десинхронизации пакетов в изолированном сэндбоксе без влияния на активный трафик и ранжирует их по задержке, TTFB и скорости.",
|
||||
"DPI Engine": "Движок DPI",
|
||||
"Zapret v2 (nfqws2)": "Zapret v2 (nfqws2)",
|
||||
"Zapret v1 (nfqws)": "Zapret v1 (nfqws)",
|
||||
"ByeDPI (ciadpi)": "ByeDPI (ciadpi)",
|
||||
"All Engines (Full Matrix)": "Все движки (Полная матрица)",
|
||||
"Installed": "Установлен",
|
||||
"Not installed": "Не установлен",
|
||||
"Target Service": "Целевой сервис",
|
||||
"YouTube (GoogleVideo 4K stream)": "YouTube (4K видеопоток GoogleVideo)",
|
||||
"YouTube Web (Interface)": "YouTube Web (Интерфейс)",
|
||||
"Discord (Gateway / API / Voice)": "Discord (Шлюз / API / Голосовые каналы)",
|
||||
"Instagram / Meta (TLS 1.3)": "Instagram / Meta (TLS 1.3)",
|
||||
"RuTracker (HTTP/HTTPS)": "RuTracker (HTTP/HTTPS)",
|
||||
"Telegram Web": "Telegram Web",
|
||||
"Custom Target URL...": "Свой URL для теста...",
|
||||
"Apply Strategy To": "Применить стратегию к",
|
||||
"Provider Global Default": "Глобальные настройки провайдера",
|
||||
"Rule:": "Правило:",
|
||||
"Benchmarking in progress...": "Выполняется бенчмарк...",
|
||||
"Testing strategy": "Тестирование стратегии",
|
||||
"Strategy & Parameters": "Стратегия и параметры",
|
||||
"Status": "Статус",
|
||||
"TTFB": "TTFB",
|
||||
"Speed": "Скорость",
|
||||
"Score": "Баллы",
|
||||
"Action": "Действие",
|
||||
"No benchmark results yet. Select engine and click \"Start Benchmark\".": "Нет результатов бенчмарка. Выберите движок и нажмите «Начать бенчмарк».",
|
||||
"🚀 Start Benchmark": "🚀 Начать бенчмарк",
|
||||
"🚀 Run Benchmark Again": "🚀 Запустить бенчмарк снова",
|
||||
"🛑 Stop Benchmark": "🛑 Остановить бенчмарк",
|
||||
"🏆 Apply Best Strategy": "🏆 Применить лучшую стратегию",
|
||||
"Apply": "Применить",
|
||||
"Applied": "Применено",
|
||||
"Benchmark stopped": "Бенчмарк остановлен",
|
||||
"Strategy benchmark started": "Бенчмарк стратегий запущен",
|
||||
"Failed to start benchmark": "Не удалось запустить бенчмарк",
|
||||
"Failed to apply strategy": "Не удалось применить стратегию",
|
||||
"⚡ Strategy Fuzzer & Auto-Tuner": "⚡ Подборщик стратегий и авто-тюнер",
|
||||
"⚡ DPI Strategy Fuzzer": "⚡ DPI Strategy Fuzzer",
|
||||
"Find optimal DPI bypass strategies with isolated live probing": "Автоматический подбор оптимальных параметров обхода DPI в изолированном сэндбоксе",
|
||||
"✅ Benchmark completed! Optimal strategy identified.": "✅ Бенчмарк завершен! Оптимальная стратегия определена.",
|
||||
"⚠️ Benchmark completed. No working bypass found for this target.": "⚠️ Бенчмарк завершен. Не найдено рабочих стратегий для данной цели.",
|
||||
"Running initial strategy probe...": "Запуск первого зонда стратегии...",
|
||||
"Total Targets": "Всего целей",
|
||||
"Passed": "Успешно",
|
||||
"Failed": "Ошибки",
|
||||
|
||||
+1535
-978
File diff suppressed because it is too large
Load Diff
+1328
-978
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-08-25 21:32+0300\n"
|
||||
"PO-Revision-Date: 2026-08-25 21:32+0300\n"
|
||||
"POT-Creation-Date: 2026-08-26 21:45+0300\n"
|
||||
"PO-Revision-Date: 2026-08-26 21:45+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -47,21 +47,51 @@ msgstr "▶ Старт"
|
||||
msgid "⚙ Advanced Settings"
|
||||
msgstr "⚙ Настройки"
|
||||
|
||||
msgid "⚠️ Benchmark completed. No working bypass found for this target."
|
||||
msgstr "⚠️ Бенчмарк завершен. Не найдено рабочих стратегий для данной цели."
|
||||
|
||||
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 вашего провайдера в открытом виде. Используйте только в крайнем случае для предотвращения полной потери интернета."
|
||||
|
||||
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 "⚡ DPI Strategy Fuzzer"
|
||||
msgstr "⚡ DPI Strategy Fuzzer"
|
||||
|
||||
msgid "⚡ Strategy Fuzzer & Auto-Tuner"
|
||||
msgstr "⚡ Подборщик стратегий и авто-тюнер"
|
||||
|
||||
msgid "✅ Benchmark completed! Optimal strategy identified."
|
||||
msgstr "✅ Бенчмарк завершен! Оптимальная стратегия определена."
|
||||
|
||||
msgid "✔ Online"
|
||||
msgstr ""
|
||||
|
||||
msgid "✔ Running"
|
||||
msgstr "✔ Работает"
|
||||
|
||||
msgid "✘ Offline"
|
||||
msgstr ""
|
||||
|
||||
msgid "✘ Stopped"
|
||||
msgstr "✘ Остановлено"
|
||||
|
||||
msgid "🏆 Apply Best Strategy"
|
||||
msgstr "🏆 Применить лучшую стратегию"
|
||||
|
||||
msgid "🔑 Generate"
|
||||
msgstr "🔑 Сгенерировать"
|
||||
|
||||
msgid "🚀 Run Benchmark Again"
|
||||
msgstr "🚀 Запустить бенчмарк снова"
|
||||
|
||||
msgid "🚀 Start Benchmark"
|
||||
msgstr "🚀 Начать бенчмарк"
|
||||
|
||||
msgid "🛑 Stop Benchmark"
|
||||
msgstr "🛑 Остановить бенчмарк"
|
||||
|
||||
msgid "Accept external proxy connections and route them with sing-box."
|
||||
msgstr "Принимать внешние прокси-подключения и маршрутизировать их через sing-box."
|
||||
|
||||
@@ -224,6 +254,9 @@ msgstr "Все проверки пройдены — бот должен раб
|
||||
msgid "All devices"
|
||||
msgstr "Все устройства"
|
||||
|
||||
msgid "All Engines (Full Matrix)"
|
||||
msgstr "Все движки (Полная матрица)"
|
||||
|
||||
msgid "All except selected"
|
||||
msgstr "Все кроме выбранных"
|
||||
|
||||
@@ -305,12 +338,21 @@ msgstr "Другое действие уже выполняется"
|
||||
msgid "API Key for OpenAI (sk-...) or DeepSeek (optional for Ollama / LM Studio)"
|
||||
msgstr "API-ключ для OpenAI (sk-...) или DeepSeek (опционально для Ollama / LM Studio)"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "Применено"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "Применить"
|
||||
|
||||
msgid "Apply rules of this section only to selected protocols."
|
||||
msgstr "Применять правила этой секции только к выбранным протоколам."
|
||||
|
||||
msgid "Apply section rules only to the specified local IP addresses"
|
||||
msgstr "Применять правила секции только к указанным локальным IP-адресам"
|
||||
|
||||
msgid "Apply Strategy To"
|
||||
msgstr "Применить стратегию к"
|
||||
|
||||
msgid "Applying fix"
|
||||
msgstr "Применение исправления"
|
||||
|
||||
@@ -350,6 +392,9 @@ msgstr "Автоопределённые домены"
|
||||
msgid "Auto-detects blocked domains from logs and adds them to the first section where they work via proxy."
|
||||
msgstr "Автоматически определяет заблокированные домены из логов и добавляет их в первую секцию, где они работают через прокси."
|
||||
|
||||
msgid "Auto-failover of proxy sections"
|
||||
msgstr "Авто-failover прокси-секций"
|
||||
|
||||
msgid "Auto-generate HWID"
|
||||
msgstr "Авто-генерация HWID"
|
||||
|
||||
@@ -359,6 +404,9 @@ msgstr "Автолечение сетевого стека"
|
||||
msgid "Auto-update Hosts Lists"
|
||||
msgstr "Автообновление списков Hosts"
|
||||
|
||||
msgid "Automated DPI Strategy Fuzzer & Auto-Tuner"
|
||||
msgstr "Автоматический подборщик и оптимизатор стратегий DPI"
|
||||
|
||||
msgid "Automatic component update checks"
|
||||
msgstr "Автоматическая проверка обновлений компонентов"
|
||||
|
||||
@@ -407,6 +455,15 @@ msgstr "Начало интервала расписания (например,
|
||||
msgid "Belarus (by)"
|
||||
msgstr "Беларусь (by)"
|
||||
|
||||
msgid "Benchmark stopped"
|
||||
msgstr "Бенчмарк остановлен"
|
||||
|
||||
msgid "Benchmarking in progress..."
|
||||
msgstr "Выполняется бенчмарк..."
|
||||
|
||||
msgid "Benchmarks a suite of packet desynchronization strategies in an isolated sandbox without disrupting active traffic, ranking them by latency, TTFB, and throughput."
|
||||
msgstr "Тестирует набор стратегий десинхронизации пакетов в изолированном сэндбоксе без влияния на активный трафик и ранжирует их по задержке, TTFB и скорости."
|
||||
|
||||
msgid "BitTorrent / P2P"
|
||||
msgstr "BitTorrent / P2P"
|
||||
|
||||
@@ -470,6 +527,9 @@ msgstr "Встроенные наборы правил"
|
||||
msgid "By flag emoji from name"
|
||||
msgstr "По эмодзи флага из имени"
|
||||
|
||||
msgid "ByeDPI (ciadpi)"
|
||||
msgstr "ByeDPI (ciadpi)"
|
||||
|
||||
msgid "ByeDPI listen address and port are assigned by Tachyon and must not be set in the strategy."
|
||||
msgstr "Адрес и порт прослушивания ByeDPI назначаются Tachyon и не должны задаваться в стратегии."
|
||||
|
||||
@@ -827,6 +887,15 @@ msgstr "Пользовательский URL эндпоинта OpenAI-совм
|
||||
msgid "Custom outbound configurations in JSON format"
|
||||
msgstr "Пользовательские конфигурации outbound в формате JSON"
|
||||
|
||||
msgid "Custom Target URL..."
|
||||
msgstr "Свой URL для теста..."
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Дневная квота"
|
||||
|
||||
msgid "Daily time quota (minutes)"
|
||||
msgstr "Дневной лимит времени (минуты)"
|
||||
|
||||
msgid "Dashboard"
|
||||
msgstr "Панель управления"
|
||||
|
||||
@@ -836,33 +905,6 @@ msgstr "Дашборд сейчас недоступен"
|
||||
msgid "Days of Week"
|
||||
msgstr "Дни недели"
|
||||
|
||||
msgid "Auto-failover of proxy sections"
|
||||
msgstr "Авто-failover прокси-секций"
|
||||
|
||||
msgid "Watchdog probes every Connection section; when the active one fails several checks in a row, traffic switches to a healthy backup and Telegram is notified. Requires a config reload to take effect."
|
||||
msgstr "Watchdog проверяет каждую Connection-секцию; если активная падает несколько проверок подряд, трафик переключается на живую резервную, а Telegram получает уведомление. Нужен reload конфигурации."
|
||||
|
||||
msgid "Failover failure threshold"
|
||||
msgstr "Порог срабатывания failover"
|
||||
|
||||
msgid "Number of consecutive failed probes before switching sections"
|
||||
msgstr "Сколько неудачных проверок подряд до переключения секции"
|
||||
|
||||
msgid "Daily time quota (minutes)"
|
||||
msgstr "Дневной лимит времени (минуты)"
|
||||
|
||||
msgid "Limit device network usage per day. When the quota is exhausted, the device is blocked until midnight. 0 disables the limit."
|
||||
msgstr "Ограничьте использование сети устройством за день. Когда квота исчерпана, устройство блокируется до полуночи. 0 — без лимита."
|
||||
|
||||
msgid "Enter a whole number of minutes between 0 and 1440"
|
||||
msgstr "Введите целое число минут от 0 до 1440"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Дневная квота"
|
||||
|
||||
msgid "min/day"
|
||||
msgstr "мин/день"
|
||||
|
||||
msgid "Default (auto)"
|
||||
msgstr "По умолчанию (авто)"
|
||||
|
||||
@@ -959,6 +1001,9 @@ msgstr "Отключено"
|
||||
msgid "Disabling interception is incompatible with action=zapret2 because Tachyon sends matched traffic through NFQUEUE."
|
||||
msgstr "Отключение перехвата несовместимо с action=zapret2, потому что Tachyon направляет совпавший трафик через NFQUEUE."
|
||||
|
||||
msgid "Discord (Gateway / API / Voice)"
|
||||
msgstr "Discord (Шлюз / API / Голосовые каналы)"
|
||||
|
||||
msgid "Display name"
|
||||
msgstr "Отображаемое имя"
|
||||
|
||||
@@ -1142,6 +1187,9 @@ msgstr "Загружено"
|
||||
msgid "Downloading and applying rule sets and lists..."
|
||||
msgstr "Загрузка и применение наборов правил и списков..."
|
||||
|
||||
msgid "DPI Engine"
|
||||
msgstr "Движок DPI"
|
||||
|
||||
msgid "Drag rows to change priority. The rule at the top is checked first."
|
||||
msgstr "Перетащите строки для изменения приоритета. Правило вверху проверяется первым."
|
||||
|
||||
@@ -1274,6 +1322,9 @@ msgstr "Конец интервала расписания (например, 08
|
||||
msgid "End Time (HH:MM)"
|
||||
msgstr "Время окончания (ЧЧ:ММ)"
|
||||
|
||||
msgid "Engine"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a complete sing-box inbound object"
|
||||
msgstr "Введите полный JSON объект inbound для sing-box"
|
||||
|
||||
@@ -1289,6 +1340,9 @@ msgstr "Введите домен или IP для проверки (напр. e
|
||||
msgid "Enter a level name"
|
||||
msgstr "Введите имя уровня"
|
||||
|
||||
msgid "Enter a whole number of minutes between 0 and 1440"
|
||||
msgstr "Введите целое число минут от 0 до 1440"
|
||||
|
||||
msgid "Enter HWID"
|
||||
msgstr "Введите HWID"
|
||||
|
||||
@@ -1367,6 +1421,9 @@ msgstr "Внешние файлы конфигурации nfqws2 обходят
|
||||
msgid "Failed to apply fix"
|
||||
msgstr "Не удалось применить исправление"
|
||||
|
||||
msgid "Failed to apply strategy"
|
||||
msgstr "Не удалось применить стратегию"
|
||||
|
||||
msgid "Failed to close connection"
|
||||
msgstr "Не удалось закрыть подключение"
|
||||
|
||||
@@ -1391,6 +1448,12 @@ msgstr "Не удалось создать отчёт об ошибке"
|
||||
msgid "Failed to generate WARP config:"
|
||||
msgstr "Не удалось сгенерировать конфигурацию WARP:"
|
||||
|
||||
msgid "Failed to get fuzzer status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to get fuzzer strategies"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load snapshots"
|
||||
msgstr "Не удалось загрузить снапшоты"
|
||||
|
||||
@@ -1436,9 +1499,15 @@ msgstr "Ошибка сохранения настроек Smart Detect"
|
||||
msgid "Failed to save snapshot"
|
||||
msgstr "Не удалось сохранить снапшот"
|
||||
|
||||
msgid "Failed to start benchmark"
|
||||
msgstr "Не удалось запустить бенчмарк"
|
||||
|
||||
msgid "Failed to start bot"
|
||||
msgstr "Не удалось запустить бота"
|
||||
|
||||
msgid "Failed to start fuzzer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start service check."
|
||||
msgstr "Не удалось запустить проверку сервисов."
|
||||
|
||||
@@ -1448,6 +1517,9 @@ msgstr "Не удалось запустить watchdog"
|
||||
msgid "Failed to stop bot"
|
||||
msgstr "Не удалось остановить бота"
|
||||
|
||||
msgid "Failed to stop fuzzer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to stop watchdog"
|
||||
msgstr "Не удалось остановить watchdog"
|
||||
|
||||
@@ -1460,6 +1532,9 @@ msgstr "Не удалось обновить устройство"
|
||||
msgid "Failed to update subscriptions"
|
||||
msgstr "Не удалось обновить подписки"
|
||||
|
||||
msgid "Failover failure threshold"
|
||||
msgstr "Порог срабатывания failover"
|
||||
|
||||
msgid "FakeIP and control checks use different public IPs"
|
||||
msgstr "FakeIP и контрольная проверка используют разные публичные IP"
|
||||
|
||||
@@ -1604,6 +1679,9 @@ msgstr "Глобальная проверка"
|
||||
msgid "Global check failed"
|
||||
msgstr "Не удалось выполнить глобальную проверку"
|
||||
|
||||
msgid "Global Default"
|
||||
msgstr ""
|
||||
|
||||
msgid "gRPC service"
|
||||
msgstr "Сервис gRPC"
|
||||
|
||||
@@ -1811,12 +1889,18 @@ msgstr "Полезный груз init (I1)"
|
||||
msgid "Init timeout (Itime)"
|
||||
msgstr "Таймаут init (Itime)"
|
||||
|
||||
msgid "Instagram / Meta (TLS 1.3)"
|
||||
msgstr "Instagram / Meta (TLS 1.3)"
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Install another build:"
|
||||
msgstr "Установить другую сборку:"
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Установлен"
|
||||
|
||||
msgid "Installed build → Available build"
|
||||
msgstr "Установленная сборка → Доступная сборка"
|
||||
|
||||
@@ -2186,6 +2270,9 @@ msgstr "Оставьте пустым, чтобы пропустить этот
|
||||
msgid "Level name"
|
||||
msgstr "Имя уровня"
|
||||
|
||||
msgid "Limit device network usage per day. When the quota is exhausted, the device is blocked until midnight. 0 disables the limit."
|
||||
msgstr "Ограничьте использование сети устройством за день. Когда квота исчерпана, устройство блокируется до полуночи. 0 — без лимита."
|
||||
|
||||
msgid "List must be an HTTP(S) URL or a local .lst path"
|
||||
msgstr "Список должен быть HTTP(S) URL или локальным путём .lst"
|
||||
|
||||
@@ -2273,6 +2360,9 @@ msgstr "ID аккаунта MASQUE"
|
||||
msgid "MASQUE Private Key"
|
||||
msgstr "Приватный ключ MASQUE"
|
||||
|
||||
msgid "Masquerade LAN traffic into tailnet"
|
||||
msgstr "Маскарадинг LAN-трафика в тейлнет"
|
||||
|
||||
msgid "Match destination IPs or subnets"
|
||||
msgstr "Сопоставлять IP-адреса или подсети назначения"
|
||||
|
||||
@@ -2306,6 +2396,9 @@ msgstr "Метод"
|
||||
msgid "Metrics Retention (hours)"
|
||||
msgstr "Хранение метрик (часы)"
|
||||
|
||||
msgid "min/day"
|
||||
msgstr "мин/день"
|
||||
|
||||
msgid "Minimum latency difference in milliseconds that triggers switching to a faster server."
|
||||
msgstr "Минимальная разница задержки в миллисекундах, при которой происходит переключение на более быстрый сервер."
|
||||
|
||||
@@ -2369,6 +2462,9 @@ msgstr "Имя в списке уровней приоритета"
|
||||
msgid "Native internet restored. Tachyon stopped."
|
||||
msgstr "Обычный интернет восстановлен. Tachyon остановлен."
|
||||
|
||||
msgid "Native tailscaled (tailnet reachable from LAN without client)"
|
||||
msgstr "Нативный tailscaled (тейлнет доступен из LAN без клиента)"
|
||||
|
||||
msgid "Netherlands (nl)"
|
||||
msgstr "Нидерланды (nl)"
|
||||
|
||||
@@ -2420,6 +2516,9 @@ msgstr "Активные устройства в локальной сети н
|
||||
msgid "No active routing sections found."
|
||||
msgstr "Активные секции маршрутизации не найдены."
|
||||
|
||||
msgid "No benchmark results yet. Select engine and click \"Start Benchmark\"."
|
||||
msgstr "Нет результатов бенчмарка. Выберите движок и нажмите «Начать бенчмарк»."
|
||||
|
||||
msgid "No check targets found in the configuration."
|
||||
msgstr "Цели для проверки не найдены в конфигурации."
|
||||
|
||||
@@ -2537,6 +2636,9 @@ msgstr "Уведомлять об обновлениях подписок"
|
||||
msgid "Number of consecutive failed checks required before switching DNS servers."
|
||||
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
|
||||
|
||||
msgid "Number of consecutive failed probes before switching sections"
|
||||
msgstr "Сколько неудачных проверок подряд до переключения секции"
|
||||
|
||||
msgid "Number of consecutive failures before restarting (default 3)."
|
||||
msgstr "Количество последовательных ошибок до перезапуска (по умолчанию 3)."
|
||||
|
||||
@@ -2642,6 +2744,9 @@ msgstr "Опциональные значения должны быть прис
|
||||
msgid "Or paste hosts file content:"
|
||||
msgstr "Или вставьте содержимое файла hosts:"
|
||||
|
||||
msgid "Other peers"
|
||||
msgstr ""
|
||||
|
||||
msgid "outbounds must be a non-empty array of strings"
|
||||
msgstr "outbounds должен быть непустым массивом строк"
|
||||
|
||||
@@ -2828,6 +2933,9 @@ msgstr "Защита"
|
||||
msgid "Protocol"
|
||||
msgstr "Протокол"
|
||||
|
||||
msgid "Provider Global Default"
|
||||
msgstr "Глобальные настройки провайдера"
|
||||
|
||||
msgid "Proxy"
|
||||
msgstr "Прокси"
|
||||
|
||||
@@ -3059,6 +3167,9 @@ msgstr "Возвращать NXDOMAIN для заблокированных до
|
||||
msgid "Route"
|
||||
msgstr "Маршрут"
|
||||
|
||||
msgid "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines."
|
||||
msgstr "Направлять весь не-локальный LAN-трафик через exit-node тейлнета вместо прокси-движков."
|
||||
|
||||
msgid "Route bot API requests through this section. Leave empty to use the default mixed proxy."
|
||||
msgstr "Маршрутизировать запросы API бота через эту секцию. Оставьте пустым для использования смешанного прокси по умолчанию."
|
||||
|
||||
@@ -3101,6 +3212,9 @@ msgstr "Наборы правил"
|
||||
msgid "Rule Sets (.srs / .json)"
|
||||
msgstr "Наборы правил (.srs / .json)"
|
||||
|
||||
msgid "Rule:"
|
||||
msgstr "Правило:"
|
||||
|
||||
msgid "Rules"
|
||||
msgstr "Правила"
|
||||
|
||||
@@ -3137,9 +3251,15 @@ msgstr "Запущен"
|
||||
msgid "Running diagnostics…"
|
||||
msgstr "Выполняется диагностика…"
|
||||
|
||||
msgid "Running initial strategy probe..."
|
||||
msgstr "Запуск первого зонда стратегии..."
|
||||
|
||||
msgid "Russia (ru)"
|
||||
msgstr "Россия (ru)"
|
||||
|
||||
msgid "RuTracker (HTTP/HTTPS)"
|
||||
msgstr "RuTracker (HTTP/HTTPS)"
|
||||
|
||||
msgid "Sat"
|
||||
msgstr "Сб"
|
||||
|
||||
@@ -3170,6 +3290,9 @@ msgstr "Расписание"
|
||||
msgid "Schedule Rule"
|
||||
msgstr "Правило расписания"
|
||||
|
||||
msgid "Score"
|
||||
msgstr "Баллы"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
@@ -3464,6 +3587,9 @@ msgstr "Показывать метаданные подписки для это
|
||||
msgid "Sing-box autostart disabled"
|
||||
msgstr "Автостарт sing-box отключен"
|
||||
|
||||
msgid "sing-box endpoint (userspace, default)"
|
||||
msgstr "Endpoint в sing-box (userspace, по умолчанию)"
|
||||
|
||||
msgid "Sing-box FakeIP DNS does not work"
|
||||
msgstr "FakeIP DNS sing-box не работает"
|
||||
|
||||
@@ -3512,6 +3638,9 @@ msgstr "Имя снапшота"
|
||||
msgid "Snapshot restored. The service restarts and the page will reload."
|
||||
msgstr "Снапшот восстановлен. Сервис перезапускается, страница будет перезагружена."
|
||||
|
||||
msgid "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes."
|
||||
msgstr "Подменять LAN-адреса при выходе через tailscale0, чтобы пиры могли отвечать. Нужно, если не все пиры принимают маршруты."
|
||||
|
||||
msgid "Snell Version"
|
||||
msgstr "Версия Snell"
|
||||
|
||||
@@ -3545,6 +3674,9 @@ msgstr "Укажите путь к файлу"
|
||||
msgid "Specify different files"
|
||||
msgstr "Укажите разные файлы"
|
||||
|
||||
msgid "Speed"
|
||||
msgstr "Скорость"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
@@ -3605,6 +3737,9 @@ msgstr "Сначала запустите Watchdog"
|
||||
msgid "Starting…"
|
||||
msgstr "Запуск…"
|
||||
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Статус"
|
||||
|
||||
@@ -3629,6 +3764,12 @@ msgstr "Остановка…"
|
||||
msgid "Store auto-detected domains in /etc instead of /tmp to survive reboots."
|
||||
msgstr "Хранить автоопределённые домены в /etc вместо /tmp для сохранения после перезагрузки."
|
||||
|
||||
msgid "Strategy & Parameters"
|
||||
msgstr "Стратегия и параметры"
|
||||
|
||||
msgid "Strategy benchmark started"
|
||||
msgstr "Бенчмарк стратегий запущен"
|
||||
|
||||
msgid "Strict ECH + DoQ DNS Enforcer"
|
||||
msgstr "Строгий ECH + DoQ DNS Enforcer"
|
||||
|
||||
@@ -3752,32 +3893,11 @@ msgstr "NFQWS2 Tachyon не запущен"
|
||||
msgid "Tachyon-managed nfqws2 runtime is ready"
|
||||
msgstr "NFQWS2 Tachyon готов"
|
||||
|
||||
msgid "Tailscale auth key"
|
||||
msgid "Tailscale mode"
|
||||
msgstr "Режим Tailscale"
|
||||
|
||||
msgid "sing-box endpoint (userspace, default)"
|
||||
msgstr "Endpoint в sing-box (userspace, по умолчанию)"
|
||||
|
||||
msgid "Native tailscaled (tailnet reachable from LAN without client)"
|
||||
msgstr "Нативный tailscaled (тейлнет доступен из LAN без клиента)"
|
||||
|
||||
msgid "Masquerade LAN traffic into tailnet"
|
||||
msgstr "Маскарадинг LAN-трафика в тейлнет"
|
||||
|
||||
msgid "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes."
|
||||
msgstr "Подменять LAN-адреса при выходе через tailscale0, чтобы пиры могли отвечать. Нужно, если не все пиры принимают маршруты."
|
||||
|
||||
msgid "Use tailnet exit node for LAN"
|
||||
msgstr "Использовать exit-node тейлнета для LAN"
|
||||
|
||||
msgid "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines."
|
||||
msgstr "Направлять весь не-локальный LAN-трафик через exit-node тейлнета вместо прокси-движков."
|
||||
|
||||
msgid "Tailscale"
|
||||
msgstr "Tailscale"
|
||||
|
||||
msgstr "Ключ аутентификации Tailscale"
|
||||
msgid "Tailscale auth key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale control URL"
|
||||
msgstr "Control URL Tailscale"
|
||||
@@ -3788,6 +3908,9 @@ msgstr "Tailscale endpoint"
|
||||
msgid "Tailscale hostname"
|
||||
msgstr "Имя хоста Tailscale"
|
||||
|
||||
msgid "Tailscale mode"
|
||||
msgstr "Режим Tailscale"
|
||||
|
||||
msgid "Target"
|
||||
msgstr "Цель"
|
||||
|
||||
@@ -3797,6 +3920,9 @@ msgstr "Целевые устройства (IP / MAC)"
|
||||
msgid "Target Scope"
|
||||
msgstr "Область действия"
|
||||
|
||||
msgid "Target Service"
|
||||
msgstr "Целевой сервис"
|
||||
|
||||
msgid "Telegram Bot"
|
||||
msgstr "Telegram Бот"
|
||||
|
||||
@@ -3806,6 +3932,9 @@ msgstr "Токен Telegram-бота"
|
||||
msgid "Telegram Channel"
|
||||
msgstr "Telegram канал"
|
||||
|
||||
msgid "Telegram Web"
|
||||
msgstr "Telegram Web"
|
||||
|
||||
msgid "Test Connection"
|
||||
msgstr "Проверить подключение"
|
||||
|
||||
@@ -3833,6 +3962,9 @@ msgstr "Тестировать серверы, чьи имена соответ
|
||||
msgid "Test Telegram Connection"
|
||||
msgstr "Проверить подключение к Telegram"
|
||||
|
||||
msgid "Testing strategy"
|
||||
msgstr "Тестирование стратегии"
|
||||
|
||||
msgid "Testing URL"
|
||||
msgstr "URL проверки"
|
||||
|
||||
@@ -3890,6 +4022,9 @@ msgstr "Этот флаг не принимает значение."
|
||||
msgid "This option requires a value."
|
||||
msgstr "Этот параметр требует значение."
|
||||
|
||||
msgid "This router"
|
||||
msgstr ""
|
||||
|
||||
msgid "This will erase all Tachyon settings and restore the factory defaults. The service will restart. This action cannot be undone."
|
||||
msgstr "Все настройки Tachyon будут удалены и восстановлены заводские значения. Сервис будет перезапущен. Это действие нельзя отменить."
|
||||
|
||||
@@ -3998,6 +4133,9 @@ msgstr "Устранение неполадок"
|
||||
msgid "Try compatible User-Agent profiles automatically when downloading this subscription"
|
||||
msgstr "Пробовать совместимые профили User-Agent автоматически при загрузке этой подписки"
|
||||
|
||||
msgid "TTFB"
|
||||
msgstr "TTFB"
|
||||
|
||||
msgid "TTL must be a positive number"
|
||||
msgstr "TTL должен быть положительным числом"
|
||||
|
||||
@@ -4232,6 +4370,9 @@ msgstr "Используйте формат длительности sing-box,
|
||||
msgid "Use sing-box duration format like 1d, 12h or 6h"
|
||||
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 6h"
|
||||
|
||||
msgid "Use tailnet exit node for LAN"
|
||||
msgstr "Использовать exit-node тейлнета для LAN"
|
||||
|
||||
msgid "Used by"
|
||||
msgstr "Используется"
|
||||
|
||||
@@ -4316,6 +4457,9 @@ msgstr "Секция прокси WARP"
|
||||
msgid "Watchdog"
|
||||
msgstr "Сторожевой таймер"
|
||||
|
||||
msgid "Watchdog probes every Connection section; when the active one fails several checks in a row, traffic switches to a healthy backup and Telegram is notified. Requires a config reload to take effect."
|
||||
msgstr "Watchdog проверяет каждую Connection-секцию; если активная падает несколько проверок подряд, трафик переключается на живую резервную, а Telegram получает уведомление. Нужен reload конфигурации."
|
||||
|
||||
msgid "Watchdog Status"
|
||||
msgstr "Статус Watchdog"
|
||||
|
||||
@@ -4361,6 +4505,12 @@ msgstr "Да"
|
||||
msgid "You can select Output Network Interface, by default autodetect"
|
||||
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
|
||||
|
||||
msgid "YouTube (GoogleVideo 4K stream)"
|
||||
msgstr "YouTube (4K видеопоток GoogleVideo)"
|
||||
|
||||
msgid "YouTube Web (Interface)"
|
||||
msgstr "YouTube Web (Интерфейс)"
|
||||
|
||||
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
|
||||
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
|
||||
|
||||
@@ -4382,6 +4532,12 @@ msgstr "sing-box outbound для Zapret настроен"
|
||||
msgid "Zapret sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret не настроен"
|
||||
|
||||
msgid "Zapret v1 (nfqws)"
|
||||
msgstr "Zapret v1 (nfqws)"
|
||||
|
||||
msgid "Zapret v2 (nfqws2)"
|
||||
msgstr "Zapret v2 (nfqws2)"
|
||||
|
||||
msgid "Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies."
|
||||
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
|
||||
|
||||
@@ -4690,3 +4846,6 @@ msgstr "Предотвращать множественные вызовы reloa
|
||||
|
||||
msgid "Restoring native direct internet..."
|
||||
msgstr "Восстановление прямого интернета..."
|
||||
|
||||
msgid "Find optimal DPI bypass strategies with isolated live probing"
|
||||
msgstr "Автоматический подбор оптимальных параметров обхода DPI в изолированном сэндбоксе"
|
||||
|
||||
@@ -1031,4 +1031,160 @@ export const TachyonShellMethods = {
|
||||
success: (response.code ?? 1) === 0,
|
||||
} as Tachyon.MethodResponse<void>;
|
||||
},
|
||||
|
||||
startFuzzer: async (
|
||||
engine: Tachyon.FuzzerEngine = 'zapret2',
|
||||
target: Tachyon.FuzzerTarget = 'youtube',
|
||||
customUrl?: string,
|
||||
ruleSection?: string,
|
||||
): Promise<Tachyon.MethodResponse<Tachyon.FuzzerStartResponse>> => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [
|
||||
Tachyon.AvailableMethods.FUZZER_START,
|
||||
engine,
|
||||
target,
|
||||
customUrl || '',
|
||||
ruleSection || '',
|
||||
],
|
||||
timeout: 10000,
|
||||
});
|
||||
let parsed: Tachyon.FuzzerStartResponse | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(response.stdout?.trim() || '{}');
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed && parsed.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: parsed?.error || response.stderr || _('Failed to start fuzzer'),
|
||||
};
|
||||
},
|
||||
|
||||
getFuzzerStatus: async (): Promise<
|
||||
Tachyon.MethodResponse<Tachyon.FuzzerState>
|
||||
> => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [Tachyon.AvailableMethods.FUZZER_STATUS],
|
||||
timeout: 8000,
|
||||
});
|
||||
let parsed: Tachyon.FuzzerState | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(response.stdout?.trim() || '{}');
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _('Failed to get fuzzer status'),
|
||||
};
|
||||
},
|
||||
|
||||
stopFuzzer: async (): Promise<Tachyon.MethodResponse<void>> => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [Tachyon.AvailableMethods.FUZZER_STOP],
|
||||
timeout: 8000,
|
||||
});
|
||||
if ((response.code ?? 1) === 0) {
|
||||
return { success: true, data: undefined };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _('Failed to stop fuzzer'),
|
||||
};
|
||||
},
|
||||
|
||||
applyFuzzerStrategy: async (
|
||||
engine: string,
|
||||
args: string,
|
||||
targetRuleOrGlobal?: string,
|
||||
): Promise<Tachyon.MethodResponse<Tachyon.FuzzerApplyResponse>> => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [
|
||||
Tachyon.AvailableMethods.FUZZER_APPLY,
|
||||
engine,
|
||||
args,
|
||||
targetRuleOrGlobal || 'global',
|
||||
],
|
||||
timeout: 10000,
|
||||
});
|
||||
let parsed: Tachyon.FuzzerApplyResponse | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(response.stdout?.trim() || '{}');
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed && parsed.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: parsed?.error || response.stderr || _('Failed to apply strategy'),
|
||||
};
|
||||
},
|
||||
|
||||
getFuzzerStrategies: async (): Promise<
|
||||
Tachyon.MethodResponse<{
|
||||
available_engines?: {
|
||||
zapret2: boolean;
|
||||
zapret: boolean;
|
||||
byedpi: boolean;
|
||||
};
|
||||
zapret2: Tachyon.FuzzerStrategyDefinition[];
|
||||
zapret: Tachyon.FuzzerStrategyDefinition[];
|
||||
byedpi: Tachyon.FuzzerStrategyDefinition[];
|
||||
}>
|
||||
> => {
|
||||
type FuzzerStrategiesData = {
|
||||
available_engines?: {
|
||||
zapret2: boolean;
|
||||
zapret: boolean;
|
||||
byedpi: boolean;
|
||||
};
|
||||
zapret2: Tachyon.FuzzerStrategyDefinition[];
|
||||
zapret: Tachyon.FuzzerStrategyDefinition[];
|
||||
byedpi: Tachyon.FuzzerStrategyDefinition[];
|
||||
};
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [Tachyon.AvailableMethods.FUZZER_STRATEGIES],
|
||||
timeout: 8000,
|
||||
});
|
||||
let parsed: FuzzerStrategiesData | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(
|
||||
response.stdout?.trim() || '{}',
|
||||
) as FuzzerStrategiesData;
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _('Failed to get fuzzer strategies'),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeShellCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../../helpers', () => ({
|
||||
executeShellCommand: mocks.executeShellCommand,
|
||||
}));
|
||||
|
||||
import { TachyonShellMethods } from '../index';
|
||||
|
||||
describe('TachyonShellMethods Fuzzer Actions', () => {
|
||||
beforeEach(() => {
|
||||
mocks.executeShellCommand.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('starts fuzzer with specified engine and target', async () => {
|
||||
mocks.executeShellCommand.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
success: true,
|
||||
job_id: 'fuzz_12345',
|
||||
engine: 'zapret2',
|
||||
target: 'youtube',
|
||||
}),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
|
||||
const res = await TachyonShellMethods.startFuzzer('zapret2', 'youtube');
|
||||
expect(res.success).toBe(true);
|
||||
if (res.success) {
|
||||
expect(res.data.job_id).toBe('fuzz_12345');
|
||||
}
|
||||
expect(mocks.executeShellCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
args: ['fuzzer_start', 'zapret2', 'youtube', '', ''],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('gets fuzzer status and results', async () => {
|
||||
mocks.executeShellCommand.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
running: false,
|
||||
job_id: 'fuzz_12345',
|
||||
engine: 'zapret2',
|
||||
target: 'youtube',
|
||||
target_url: 'https://www.youtube.com',
|
||||
progress_pct: 100,
|
||||
results: [
|
||||
{
|
||||
id: 'z2_yt_multisplit_midsld',
|
||||
name: 'YouTube 4K Multisplit + MidSLD',
|
||||
engine: 'zapret2',
|
||||
args: '--dpi-desync=multisplit',
|
||||
success: true,
|
||||
http_code: 200,
|
||||
ttfb_ms: 120,
|
||||
speed_kbps: 4500,
|
||||
score: 180,
|
||||
badge: '🏆 Best Match',
|
||||
},
|
||||
],
|
||||
best_strategy: {
|
||||
id: 'z2_yt_multisplit_midsld',
|
||||
score: 180,
|
||||
},
|
||||
}),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
|
||||
const res = await TachyonShellMethods.getFuzzerStatus();
|
||||
expect(res.success).toBe(true);
|
||||
if (res.success) {
|
||||
expect(res.data.running).toBe(false);
|
||||
expect(res.data.results).toHaveLength(1);
|
||||
expect(res.data.results[0].badge).toBe('🏆 Best Match');
|
||||
}
|
||||
});
|
||||
|
||||
it('applies winning strategy', async () => {
|
||||
mocks.executeShellCommand.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
success: true,
|
||||
engine: 'zapret2',
|
||||
applied_to: 'global',
|
||||
args: '--dpi-desync=multisplit',
|
||||
}),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
|
||||
const res = await TachyonShellMethods.applyFuzzerStrategy(
|
||||
'zapret2',
|
||||
'--dpi-desync=multisplit',
|
||||
'global',
|
||||
);
|
||||
expect(res.success).toBe(true);
|
||||
if (res.success) {
|
||||
expect(res.data.applied_to).toBe('global');
|
||||
}
|
||||
expect(mocks.executeShellCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
args: ['fuzzer_apply', 'zapret2', '--dpi-desync=multisplit', 'global'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -40,8 +40,10 @@ import {
|
||||
renderSystemInfo,
|
||||
renderServiceCheckModal,
|
||||
renderAiChatModal,
|
||||
renderStrategyFuzzerModal,
|
||||
} from './partials';
|
||||
import { TachyonShellMethods } from '../../methods';
|
||||
import { getConfigSections } from '../../methods/custom/getConfigSections';
|
||||
import { fetchServicesInfo } from '../../fetchers/fetchServicesInfo';
|
||||
import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion';
|
||||
import { renderModal, renderButton } from '../../../partials';
|
||||
@@ -1582,6 +1584,20 @@ function handleOpenAiChat() {
|
||||
renderAiChatModal();
|
||||
}
|
||||
|
||||
function handleOpenStrategyFuzzer() {
|
||||
getConfigSections()
|
||||
.then((sections) => {
|
||||
const ruleNames = sections
|
||||
.filter((s) => s['.type'] === 'section' || s['.type'] === 'rule')
|
||||
.map((s) => s.name || s['.name'])
|
||||
.filter((n): n is string => Boolean(n));
|
||||
renderStrategyFuzzerModal(ruleNames);
|
||||
})
|
||||
.catch(() => {
|
||||
renderStrategyFuzzerModal([]);
|
||||
});
|
||||
}
|
||||
|
||||
function handleRestoreNativeInternet() {
|
||||
ui.showModal(
|
||||
_('Restore Native Internet'),
|
||||
@@ -1870,6 +1886,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
||||
onClick: handleOpenAiChat,
|
||||
disabled: false,
|
||||
},
|
||||
strategyFuzzer: {
|
||||
loading: false,
|
||||
visible: true,
|
||||
onClick: handleOpenStrategyFuzzer,
|
||||
disabled: false,
|
||||
},
|
||||
checkServices: {
|
||||
visible: true,
|
||||
loading: diagnosticsActions.checkServices.loading,
|
||||
|
||||
@@ -3,4 +3,5 @@ export * from './renderCheckSection';
|
||||
export * from './renderRunAction';
|
||||
export * from './renderServiceCheckModal';
|
||||
export * from './renderAiChatModal';
|
||||
export * from './renderStrategyFuzzerModal';
|
||||
export * from './renderSystemInfo';
|
||||
|
||||
@@ -29,6 +29,7 @@ interface IRenderAvailableActionsProps {
|
||||
doctor: ActionProps;
|
||||
aiDoctor: ActionProps;
|
||||
aiChat?: ActionProps;
|
||||
strategyFuzzer?: ActionProps;
|
||||
restoreNativeInternet?: ActionProps;
|
||||
viewLogs: ActionProps;
|
||||
showSingBoxConfig: ActionProps;
|
||||
@@ -46,6 +47,7 @@ export function renderAvailableActions({
|
||||
doctor,
|
||||
aiDoctor,
|
||||
aiChat,
|
||||
strategyFuzzer,
|
||||
restoreNativeInternet,
|
||||
viewLogs,
|
||||
showSingBoxConfig,
|
||||
@@ -152,6 +154,16 @@ export function renderAvailableActions({
|
||||
disabled: aiChat!.disabled,
|
||||
}),
|
||||
]),
|
||||
...insertIf(!!strategyFuzzer?.visible, [
|
||||
renderButton({
|
||||
classNames: ['cbi-button-action'],
|
||||
onClick: strategyFuzzer!.onClick,
|
||||
icon: renderCirclePlayIcon24,
|
||||
text: _('⚡ DPI Strategy Fuzzer'),
|
||||
loading: strategyFuzzer!.loading,
|
||||
disabled: strategyFuzzer!.disabled,
|
||||
}),
|
||||
]),
|
||||
...insertIf(checkServices.visible, [
|
||||
renderButton({
|
||||
classNames: ['cbi-button-action'],
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
import { TachyonShellMethods } from '../../../methods';
|
||||
import { renderButton } from '../../../../partials';
|
||||
import { showToast } from '../../../../helpers/showToast';
|
||||
import { Tachyon } from '../../../types';
|
||||
|
||||
export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
||||
let pollingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let isPolling = false;
|
||||
|
||||
// Modal State
|
||||
let selectedEngine: Tachyon.FuzzerEngine = 'zapret2';
|
||||
let selectedTarget: Tachyon.FuzzerTarget = 'youtube';
|
||||
let customUrl = '';
|
||||
let selectedRuleSection = '';
|
||||
let isRunning = false;
|
||||
let currentState: Tachyon.FuzzerState | null = null;
|
||||
|
||||
const modalContainer = E('div', {
|
||||
class: 'tachyon_fuzzer_modal',
|
||||
style:
|
||||
'display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 860px; box-sizing: border-box;',
|
||||
});
|
||||
|
||||
// ── Header & Description ──────────────────────────────────────────────────
|
||||
const headerEl = E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'padding: 12px 16px; background: var(--background-color-secondary, rgba(0,0,0,0.25)); border: 1px solid var(--border-color, rgba(255,255,255,0.12)); border-radius: 8px;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'font-size: 15px; font-weight: bold; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;',
|
||||
},
|
||||
[
|
||||
E('span', {}, '⚡'),
|
||||
E('span', {}, _('Automated DPI Strategy Fuzzer & Auto-Tuner')),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style: 'font-size: 12px; opacity: 0.75; line-height: 1.4;',
|
||||
},
|
||||
_(
|
||||
'Benchmarks a suite of packet desynchronization strategies in an isolated sandbox without disrupting active traffic, ranking them by latency, TTFB, and throughput.',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Configuration Controls ────────────────────────────────────────────────
|
||||
const controlsGrid = E('div', {
|
||||
style:
|
||||
'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; align-items: end;',
|
||||
});
|
||||
|
||||
// 1. Engine Select
|
||||
const engineSelect = E(
|
||||
'select',
|
||||
{ class: 'cbi-input-select', style: 'width: 100%;' },
|
||||
[
|
||||
E(
|
||||
'option',
|
||||
{ value: 'zapret2', selected: true },
|
||||
_('Zapret v2 (nfqws2)'),
|
||||
),
|
||||
E('option', { value: 'zapret' }, _('Zapret v1 (nfqws)')),
|
||||
E('option', { value: 'byedpi' }, _('ByeDPI (ciadpi)')),
|
||||
E('option', { value: 'all' }, _('All Engines (Full Matrix)')),
|
||||
],
|
||||
);
|
||||
engineSelect.addEventListener('change', () => {
|
||||
selectedEngine = (engineSelect as HTMLSelectElement)
|
||||
.value as Tachyon.FuzzerEngine;
|
||||
});
|
||||
|
||||
const engineGroup = E(
|
||||
'div',
|
||||
{ style: 'display: flex; flex-direction: column; gap: 4px;' },
|
||||
[
|
||||
E(
|
||||
'label',
|
||||
{ style: 'font-size: 12px; font-weight: 600;' },
|
||||
_('DPI Engine'),
|
||||
),
|
||||
engineSelect,
|
||||
],
|
||||
);
|
||||
|
||||
// Dynamically query available engines and annotate options
|
||||
TachyonShellMethods.getFuzzerStrategies()
|
||||
.then((res) => {
|
||||
if (res.success && res.data?.available_engines) {
|
||||
const av = res.data.available_engines;
|
||||
const opts = (engineSelect as HTMLSelectElement).options;
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
const opt = opts[i];
|
||||
if (opt.value === 'zapret2') {
|
||||
opt.text = av.zapret2
|
||||
? _('Zapret v2 (nfqws2)') + ' — ' + _('Installed')
|
||||
: _('Zapret v2 (nfqws2)') + ' — ' + _('Not installed');
|
||||
if (!av.zapret2) opt.disabled = true;
|
||||
} else if (opt.value === 'zapret') {
|
||||
opt.text = av.zapret
|
||||
? _('Zapret v1 (nfqws)') + ' — ' + _('Installed')
|
||||
: _('Zapret v1 (nfqws)') + ' — ' + _('Not installed');
|
||||
if (!av.zapret) opt.disabled = true;
|
||||
} else if (opt.value === 'byedpi') {
|
||||
opt.text = av.byedpi
|
||||
? _('ByeDPI (ciadpi)') + ' — ' + _('Installed')
|
||||
: _('ByeDPI (ciadpi)') + ' — ' + _('Not installed');
|
||||
if (!av.byedpi) opt.disabled = true;
|
||||
}
|
||||
}
|
||||
if (av.zapret2) {
|
||||
selectedEngine = 'zapret2';
|
||||
(engineSelect as HTMLSelectElement).value = 'zapret2';
|
||||
} else if (av.byedpi) {
|
||||
selectedEngine = 'byedpi';
|
||||
(engineSelect as HTMLSelectElement).value = 'byedpi';
|
||||
} else if (av.zapret) {
|
||||
selectedEngine = 'zapret';
|
||||
(engineSelect as HTMLSelectElement).value = 'zapret';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 2. Target Preset Select
|
||||
const targetSelect = E(
|
||||
'select',
|
||||
{ class: 'cbi-input-select', style: 'width: 100%;' },
|
||||
[
|
||||
E(
|
||||
'option',
|
||||
{ value: 'youtube', selected: true },
|
||||
_('YouTube (GoogleVideo 4K stream)'),
|
||||
),
|
||||
E('option', { value: 'youtube_web' }, _('YouTube Web (Interface)')),
|
||||
E('option', { value: 'discord' }, _('Discord (Gateway / API / Voice)')),
|
||||
E('option', { value: 'instagram' }, _('Instagram / Meta (TLS 1.3)')),
|
||||
E('option', { value: 'rutracker' }, _('RuTracker (HTTP/HTTPS)')),
|
||||
E('option', { value: 'telegram' }, _('Telegram Web')),
|
||||
E('option', { value: 'custom' }, _('Custom Target URL...')),
|
||||
],
|
||||
);
|
||||
|
||||
const customUrlInput = E('input', {
|
||||
type: 'text',
|
||||
class: 'cbi-input-text',
|
||||
placeholder: 'https://example.com',
|
||||
style: 'width: 100%; display: none; margin-top: 4px;',
|
||||
});
|
||||
|
||||
targetSelect.addEventListener('change', () => {
|
||||
selectedTarget = (targetSelect as HTMLSelectElement)
|
||||
.value as Tachyon.FuzzerTarget;
|
||||
if (selectedTarget === 'custom') {
|
||||
(customUrlInput as HTMLElement).style.display = 'block';
|
||||
} else {
|
||||
(customUrlInput as HTMLElement).style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
customUrlInput.addEventListener('input', () => {
|
||||
customUrl = (customUrlInput as HTMLInputElement).value.trim();
|
||||
});
|
||||
|
||||
const targetGroup = E(
|
||||
'div',
|
||||
{ style: 'display: flex; flex-direction: column; gap: 4px;' },
|
||||
[
|
||||
E(
|
||||
'label',
|
||||
{ style: 'font-size: 12px; font-weight: 600;' },
|
||||
_('Target Service'),
|
||||
),
|
||||
targetSelect,
|
||||
customUrlInput,
|
||||
],
|
||||
);
|
||||
|
||||
// 3. Rule / Apply Target Select
|
||||
const ruleSelectOptions = [
|
||||
E('option', { value: '', selected: true }, _('Provider Global Default')),
|
||||
...ruleNames.map((r) => E('option', { value: r }, `${_('Rule:')} ${r}`)),
|
||||
];
|
||||
const ruleSelect = E(
|
||||
'select',
|
||||
{ class: 'cbi-input-select', style: 'width: 100%;' },
|
||||
ruleSelectOptions,
|
||||
);
|
||||
ruleSelect.addEventListener('change', () => {
|
||||
selectedRuleSection = (ruleSelect as HTMLSelectElement).value;
|
||||
});
|
||||
|
||||
const ruleGroup = E(
|
||||
'div',
|
||||
{ style: 'display: flex; flex-direction: column; gap: 4px;' },
|
||||
[
|
||||
E(
|
||||
'label',
|
||||
{ style: 'font-size: 12px; font-weight: 600;' },
|
||||
_('Apply Strategy To'),
|
||||
),
|
||||
ruleSelect,
|
||||
],
|
||||
);
|
||||
|
||||
controlsGrid.append(engineGroup, targetGroup, ruleGroup);
|
||||
|
||||
// ── Progress & Status Bar ─────────────────────────────────────────────────
|
||||
const progressContainer = E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: none; flex-direction: column; gap: 6px; padding: 12px; background: var(--background-color-secondary, rgba(0,0,0,0.18)); border-radius: 8px; border: 1px solid var(--border-color, rgba(255,255,255,0.1));',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; justify-content: space-between; font-size: 12px; font-weight: 600;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ id: 'tachyon-fuzzer-status-text' },
|
||||
_('Benchmarking in progress...'),
|
||||
),
|
||||
E('span', { id: 'tachyon-fuzzer-progress-pct' }, '0%'),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'width: 100%; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;',
|
||||
},
|
||||
[
|
||||
E('div', {
|
||||
id: 'tachyon-fuzzer-progress-bar',
|
||||
style:
|
||||
'height: 100%; width: 0%; background: linear-gradient(90deg, #007bff, #00d2ff); transition: width 0.3s ease;',
|
||||
}),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: 'tachyon-fuzzer-current-strategy',
|
||||
style: 'font-size: 11px; opacity: 0.7; font-family: monospace;',
|
||||
},
|
||||
'',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Results Table ─────────────────────────────────────────────────────────
|
||||
const resultsContainer = E('div', {
|
||||
style:
|
||||
'max-height: 380px; overflow-y: auto; border: 1px solid var(--border-color, rgba(255,255,255,0.12)); border-radius: 8px;',
|
||||
});
|
||||
|
||||
const tableEl = E(
|
||||
'table',
|
||||
{
|
||||
class: 'table',
|
||||
style:
|
||||
'width: 100%; margin: 0; font-size: 12px; text-align: left; border-collapse: collapse;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'thead',
|
||||
{
|
||||
style:
|
||||
'background: var(--background-color-secondary, rgba(0,0,0,0.3)); position: sticky; top: 0; z-index: 2;',
|
||||
},
|
||||
[
|
||||
E('tr', {}, [
|
||||
E('th', { style: 'padding: 8px 10px; width: 40px;' }, '#'),
|
||||
E('th', { style: 'padding: 8px 10px; width: 80px;' }, _('Engine')),
|
||||
E(
|
||||
'th',
|
||||
{ style: 'padding: 8px 10px;' },
|
||||
_('Strategy & Parameters'),
|
||||
),
|
||||
E('th', { style: 'padding: 8px 10px; width: 70px;' }, _('Status')),
|
||||
E('th', { style: 'padding: 8px 10px; width: 65px;' }, _('TTFB')),
|
||||
E('th', { style: 'padding: 8px 10px; width: 75px;' }, _('Speed')),
|
||||
E('th', { style: 'padding: 8px 10px; width: 90px;' }, _('Score')),
|
||||
E(
|
||||
'th',
|
||||
{ style: 'padding: 8px 10px; width: 80px; text-align: center;' },
|
||||
_('Action'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
E('tbody', { id: 'tachyon-fuzzer-results-tbody' }, [
|
||||
E(
|
||||
'tr',
|
||||
{},
|
||||
E(
|
||||
'td',
|
||||
{
|
||||
colSpan: 8,
|
||||
style: 'padding: 24px; text-align: center; opacity: 0.6;',
|
||||
},
|
||||
_(
|
||||
'No benchmark results yet. Select engine and click "Start Benchmark".',
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
resultsContainer.appendChild(tableEl);
|
||||
|
||||
// ── Action Buttons & Footer ───────────────────────────────────────────────
|
||||
const startBtn = renderButton({
|
||||
text: _('🚀 Start Benchmark'),
|
||||
classNames: ['cbi-button-action'],
|
||||
onClick: () => handleToggleRun(),
|
||||
});
|
||||
|
||||
const applyBestBtn = renderButton({
|
||||
text: _('🏆 Apply Best Strategy'),
|
||||
classNames: ['cbi-button-save'],
|
||||
disabled: true,
|
||||
onClick: () => handleApplyBest(),
|
||||
});
|
||||
|
||||
const closeBtn = renderButton({
|
||||
text: _('Close'),
|
||||
classNames: ['cbi-button-neutral'],
|
||||
onClick: () => {
|
||||
stopPolling();
|
||||
ui.hideModal();
|
||||
},
|
||||
});
|
||||
|
||||
const footerActions = E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; justify-content: space-between; align-items: center; padding-top: 8px; border-top: 1px solid var(--border-color, rgba(255,255,255,0.1));',
|
||||
},
|
||||
[
|
||||
E('div', { style: 'display: flex; gap: 8px;' }, [startBtn]),
|
||||
E('div', { style: 'display: flex; gap: 8px;' }, [applyBestBtn, closeBtn]),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Render Helpers ────────────────────────────────────────────────────────
|
||||
const renderResults = (state: Tachyon.FuzzerState) => {
|
||||
const tbody = document.getElementById('tachyon-fuzzer-results-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!state.results || state.results.length === 0) {
|
||||
if (state.running) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" style="padding: 20px; text-align: center;">${_(
|
||||
'Running initial strategy probe...',
|
||||
)}</td></tr>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
state.results.forEach((item, idx) => {
|
||||
const isBest = state.best_strategy && state.best_strategy.id === item.id;
|
||||
const statusBadge = item.success
|
||||
? `<span class="badge" style="background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">${item.http_code || 200} OK</span>`
|
||||
: `<span class="badge" style="background: #dc3545; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">${item.error || 'Blocked'}</span>`;
|
||||
|
||||
let badgeHtml = '';
|
||||
if (item.badge) {
|
||||
badgeHtml = `<span style="display: inline-block; font-size: 10px; font-weight: bold; color: #ffc107; margin-left: 6px;">${item.badge}</span>`;
|
||||
}
|
||||
|
||||
const tr = E(
|
||||
'tr',
|
||||
{
|
||||
style: `border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06)); ${
|
||||
isBest
|
||||
? 'background: rgba(40, 167, 69, 0.12); font-weight: 500;'
|
||||
: ''
|
||||
}`,
|
||||
},
|
||||
[
|
||||
E('td', { style: 'padding: 8px 10px;' }, String(idx + 1)),
|
||||
E(
|
||||
'td',
|
||||
{
|
||||
style:
|
||||
'padding: 8px 10px; font-family: monospace; font-size: 11px;',
|
||||
},
|
||||
item.engine,
|
||||
),
|
||||
E('td', { style: 'padding: 8px 10px;' }, [
|
||||
E('div', { style: 'font-weight: 600;' }, [
|
||||
E('span', {}, item.name),
|
||||
E('span', { innerHTML: badgeHtml }),
|
||||
]),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'font-size: 10px; opacity: 0.7; font-family: monospace; word-break: break-all; margin-top: 2px;',
|
||||
},
|
||||
item.args,
|
||||
),
|
||||
]),
|
||||
E('td', { style: 'padding: 8px 10px;', innerHTML: statusBadge }),
|
||||
E(
|
||||
'td',
|
||||
{ style: 'padding: 8px 10px;' },
|
||||
item.success ? `${item.ttfb_ms}ms` : '—',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ style: 'padding: 8px 10px;' },
|
||||
item.success ? `${(item.speed_kbps / 1024).toFixed(1)}MB/s` : '—',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ style: 'padding: 8px 10px; font-weight: bold;' },
|
||||
item.success ? String(item.score) : '0',
|
||||
),
|
||||
E('td', { style: 'padding: 8px 10px; text-align: center;' }, [
|
||||
renderButton({
|
||||
text: _('Apply'),
|
||||
classNames: ['cbi-button-action'],
|
||||
disabled: !item.success,
|
||||
onClick: () => handleApplySingle(item),
|
||||
}),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
};
|
||||
|
||||
const updateProgressUI = (state: Tachyon.FuzzerState) => {
|
||||
const statusText = document.getElementById('tachyon-fuzzer-status-text');
|
||||
const pctText = document.getElementById('tachyon-fuzzer-progress-pct');
|
||||
const progressBar = document.getElementById('tachyon-fuzzer-progress-bar');
|
||||
const currentStratEl = document.getElementById(
|
||||
'tachyon-fuzzer-current-strategy',
|
||||
);
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display =
|
||||
state.running || state.results?.length ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (pctText) pctText.innerText = `${state.progress_pct}%`;
|
||||
if (progressBar) progressBar.style.width = `${state.progress_pct}%`;
|
||||
|
||||
if (statusText) {
|
||||
if (state.running) {
|
||||
statusText.innerText = `${_('Testing strategy')} ${state.current_index} / ${state.total_strategies}...`;
|
||||
} else if (state.finished_at > 0) {
|
||||
statusText.innerText = state.best_strategy
|
||||
? _('✅ Benchmark completed! Optimal strategy identified.')
|
||||
: _(
|
||||
'⚠️ Benchmark completed. No working bypass found for this target.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStratEl && state.current_strategy) {
|
||||
currentStratEl.innerText = `Testing: [${state.current_strategy.name}] -> ${state.current_strategy.args}`;
|
||||
} else if (currentStratEl && !state.running) {
|
||||
currentStratEl.innerText = '';
|
||||
}
|
||||
|
||||
if (state.best_strategy && applyBestBtn) {
|
||||
(applyBestBtn as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Polling & Execution Logic ─────────────────────────────────────────────
|
||||
const pollStatus = async () => {
|
||||
if (isPolling) return;
|
||||
isPolling = true;
|
||||
try {
|
||||
const res = await TachyonShellMethods.getFuzzerStatus();
|
||||
if (res.success && res.data) {
|
||||
currentState = res.data;
|
||||
isRunning = res.data.running;
|
||||
|
||||
updateProgressUI(res.data);
|
||||
renderResults(res.data);
|
||||
|
||||
if (!isRunning) {
|
||||
stopPolling();
|
||||
startBtn.innerText = _('🚀 Run Benchmark Again');
|
||||
(startBtn as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// transient error ignore
|
||||
} finally {
|
||||
isPolling = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
stopPolling();
|
||||
pollingInterval = setInterval(pollStatus, 1000);
|
||||
pollStatus();
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleRun = async () => {
|
||||
if (isRunning) {
|
||||
await TachyonShellMethods.stopFuzzer();
|
||||
showToast(_('Benchmark stopped'), 'success');
|
||||
stopPolling();
|
||||
pollStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
(startBtn as HTMLButtonElement).disabled = true;
|
||||
startBtn.innerText = _('🛑 Stop Benchmark');
|
||||
|
||||
const res = await TachyonShellMethods.startFuzzer(
|
||||
selectedEngine,
|
||||
selectedTarget,
|
||||
customUrl,
|
||||
selectedRuleSection,
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
showToast(_('Strategy benchmark started'), 'success');
|
||||
(startBtn as HTMLButtonElement).disabled = false;
|
||||
isRunning = true;
|
||||
startPolling();
|
||||
} else {
|
||||
const errMsg = !res.success ? res.error : _('Unknown error');
|
||||
showToast(`${_('Failed to start benchmark')}: ${errMsg}`, 'error');
|
||||
(startBtn as HTMLButtonElement).disabled = false;
|
||||
startBtn.innerText = _('🚀 Start Benchmark');
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplySingle = async (item: Tachyon.FuzzerStrategyResult) => {
|
||||
const res = await TachyonShellMethods.applyFuzzerStrategy(
|
||||
item.engine,
|
||||
item.args,
|
||||
selectedRuleSection,
|
||||
);
|
||||
if (res.success) {
|
||||
showToast(
|
||||
`${_('Applied')} "${item.name}" -> ${selectedRuleSection || _('Global Default')}`,
|
||||
'success',
|
||||
);
|
||||
} else {
|
||||
showToast(_('Failed to apply strategy'), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyBest = async () => {
|
||||
if (!currentState?.best_strategy) return;
|
||||
await handleApplySingle(currentState.best_strategy);
|
||||
};
|
||||
|
||||
// Build Modal Tree
|
||||
modalContainer.append(
|
||||
headerEl,
|
||||
controlsGrid,
|
||||
progressContainer,
|
||||
resultsContainer,
|
||||
footerActions,
|
||||
);
|
||||
|
||||
// Check initial state
|
||||
pollStatus();
|
||||
|
||||
ui.showModal(_('⚡ Strategy Fuzzer & Auto-Tuner'), modalContainer);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/* 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>,
|
||||
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).rpc = { declare: vi.fn() };
|
||||
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
|
||||
(globalThis as any).localStorage = { getItem: vi.fn(), setItem: vi.fn() };
|
||||
|
||||
(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 (typeof style === 'string') {
|
||||
el.style.cssText = style;
|
||||
} else if (style && typeof style === 'object') {
|
||||
Object.assign(el.style, style);
|
||||
}
|
||||
}
|
||||
if (children) {
|
||||
if (Array.isArray(children)) {
|
||||
children.forEach((c) =>
|
||||
el.appendChild(typeof c === 'string' ? { textContent: c } : c),
|
||||
);
|
||||
} else {
|
||||
el.appendChild(
|
||||
typeof children === 'string' ? { textContent: children } : children,
|
||||
);
|
||||
}
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
(globalThis as any)._ = (s: string) => s;
|
||||
(globalThis as any).sprintf = (s: string, ...args: any[]) => {
|
||||
let res = s;
|
||||
args.forEach((arg) => {
|
||||
res = res.replace(/%[sdj]/, String(arg));
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
const showModal = vi.fn();
|
||||
const hideModal = vi.fn();
|
||||
const executeShellCommand = vi.fn();
|
||||
|
||||
(globalThis as any).ui = {
|
||||
showModal,
|
||||
hideModal,
|
||||
};
|
||||
|
||||
return {
|
||||
showModal,
|
||||
hideModal,
|
||||
executeShellCommand,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../helpers', async (importOriginal) => ({
|
||||
...((await importOriginal()) as any),
|
||||
executeShellCommand: mocks.executeShellCommand,
|
||||
}));
|
||||
|
||||
import { renderStrategyFuzzerModal } from '../partials/renderStrategyFuzzerModal';
|
||||
|
||||
describe('renderStrategyFuzzerModal', () => {
|
||||
beforeEach(() => {
|
||||
mocks.showModal.mockReset();
|
||||
mocks.executeShellCommand.mockReset();
|
||||
mocks.executeShellCommand.mockResolvedValue({
|
||||
stdout: JSON.stringify({
|
||||
running: false,
|
||||
results: [],
|
||||
}),
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders and displays the Strategy Fuzzer modal', () => {
|
||||
renderStrategyFuzzerModal(['youtube_rule', 'discord_rule']);
|
||||
|
||||
expect(mocks.showModal).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.showModal).toHaveBeenCalledWith(
|
||||
'⚡ Strategy Fuzzer & Auto-Tuner',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -98,6 +98,11 @@ export namespace Tachyon {
|
||||
SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async',
|
||||
SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status',
|
||||
SERVICE_HEALTH_CHECK = 'service_health_check',
|
||||
FUZZER_START = 'fuzzer_start',
|
||||
FUZZER_STATUS = 'fuzzer_status',
|
||||
FUZZER_STOP = 'fuzzer_stop',
|
||||
FUZZER_APPLY = 'fuzzer_apply',
|
||||
FUZZER_STRATEGIES = 'fuzzer_strategies',
|
||||
}
|
||||
|
||||
export enum AvailableClashAPIMethods {
|
||||
@@ -822,4 +827,74 @@ export namespace Tachyon {
|
||||
}
|
||||
|
||||
export type GetClashApiGroupLatency = Record<string, number>;
|
||||
|
||||
export type FuzzerEngine = 'zapret2' | 'zapret' | 'byedpi' | 'all';
|
||||
export type FuzzerTarget =
|
||||
| 'youtube'
|
||||
| 'youtube_web'
|
||||
| 'discord'
|
||||
| 'instagram'
|
||||
| 'rutracker'
|
||||
| 'telegram'
|
||||
| 'custom';
|
||||
|
||||
export interface FuzzerStrategyItem {
|
||||
id: string;
|
||||
name: string;
|
||||
engine: 'zapret2' | 'zapret' | 'byedpi';
|
||||
args: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type FuzzerStrategyDefinition = FuzzerStrategyItem;
|
||||
|
||||
export interface FuzzerStrategyResult {
|
||||
id: string;
|
||||
name: string;
|
||||
engine: 'zapret2' | 'zapret' | 'byedpi';
|
||||
args: string;
|
||||
description: string;
|
||||
success: boolean;
|
||||
http_code: number;
|
||||
handshake_ms: number;
|
||||
ttfb_ms: number;
|
||||
speed_kbps: number;
|
||||
score: number;
|
||||
error?: string;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
export interface FuzzerState {
|
||||
running: boolean;
|
||||
job_id: string;
|
||||
engine: FuzzerEngine;
|
||||
target: FuzzerTarget;
|
||||
target_url: string;
|
||||
rule_section?: string;
|
||||
progress_pct: number;
|
||||
current_index: number;
|
||||
total_strategies: number;
|
||||
current_strategy?: FuzzerStrategyItem | null;
|
||||
results: FuzzerStrategyResult[];
|
||||
best_strategy?: FuzzerStrategyResult | null;
|
||||
error?: string | null;
|
||||
started_at: number;
|
||||
finished_at: number;
|
||||
}
|
||||
|
||||
export interface FuzzerStartResponse {
|
||||
success: boolean;
|
||||
job_id?: string;
|
||||
engine?: string;
|
||||
target?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface FuzzerApplyResponse {
|
||||
success: boolean;
|
||||
engine: string;
|
||||
applied_to: string;
|
||||
args: string;
|
||||
error?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3114,6 +3114,11 @@ var Tachyon;
|
||||
AvailableMethods2["SUBSCRIPTION_UPDATE_ASYNC"] = "subscription_update_async";
|
||||
AvailableMethods2["SUBSCRIPTION_UPDATE_STATUS"] = "subscription_update_status";
|
||||
AvailableMethods2["SERVICE_HEALTH_CHECK"] = "service_health_check";
|
||||
AvailableMethods2["FUZZER_START"] = "fuzzer_start";
|
||||
AvailableMethods2["FUZZER_STATUS"] = "fuzzer_status";
|
||||
AvailableMethods2["FUZZER_STOP"] = "fuzzer_stop";
|
||||
AvailableMethods2["FUZZER_APPLY"] = "fuzzer_apply";
|
||||
AvailableMethods2["FUZZER_STRATEGIES"] = "fuzzer_strategies";
|
||||
})(AvailableMethods = Tachyon2.AvailableMethods || (Tachyon2.AvailableMethods = {}));
|
||||
let AvailableClashAPIMethods;
|
||||
((AvailableClashAPIMethods2) => {
|
||||
@@ -3916,6 +3921,125 @@ var TachyonShellMethods = {
|
||||
return {
|
||||
success: (response.code ?? 1) === 0
|
||||
};
|
||||
},
|
||||
startFuzzer: async (engine = "zapret2", target = "youtube", customUrl, ruleSection) => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [
|
||||
Tachyon.AvailableMethods.FUZZER_START,
|
||||
engine,
|
||||
target,
|
||||
customUrl || "",
|
||||
ruleSection || ""
|
||||
],
|
||||
timeout: 1e4
|
||||
});
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(response.stdout?.trim() || "{}");
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed && parsed.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: parsed?.error || response.stderr || _("Failed to start fuzzer")
|
||||
};
|
||||
},
|
||||
getFuzzerStatus: async () => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [Tachyon.AvailableMethods.FUZZER_STATUS],
|
||||
timeout: 8e3
|
||||
});
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(response.stdout?.trim() || "{}");
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _("Failed to get fuzzer status")
|
||||
};
|
||||
},
|
||||
stopFuzzer: async () => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [Tachyon.AvailableMethods.FUZZER_STOP],
|
||||
timeout: 8e3
|
||||
});
|
||||
if ((response.code ?? 1) === 0) {
|
||||
return { success: true, data: void 0 };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _("Failed to stop fuzzer")
|
||||
};
|
||||
},
|
||||
applyFuzzerStrategy: async (engine, args, targetRuleOrGlobal) => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [
|
||||
Tachyon.AvailableMethods.FUZZER_APPLY,
|
||||
engine,
|
||||
args,
|
||||
targetRuleOrGlobal || "global"
|
||||
],
|
||||
timeout: 1e4
|
||||
});
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(response.stdout?.trim() || "{}");
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed && parsed.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: parsed?.error || response.stderr || _("Failed to apply strategy")
|
||||
};
|
||||
},
|
||||
getFuzzerStrategies: async () => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [Tachyon.AvailableMethods.FUZZER_STRATEGIES],
|
||||
timeout: 8e3
|
||||
});
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(
|
||||
response.stdout?.trim() || "{}"
|
||||
);
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if ((response.code ?? 1) === 0 && parsed) {
|
||||
return {
|
||||
success: true,
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: response.stderr || _("Failed to get fuzzer strategies")
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10309,6 +10433,7 @@ function renderAvailableActions({
|
||||
doctor,
|
||||
aiDoctor,
|
||||
aiChat,
|
||||
strategyFuzzer,
|
||||
restoreNativeInternet,
|
||||
viewLogs,
|
||||
showSingBoxConfig,
|
||||
@@ -10415,6 +10540,16 @@ function renderAvailableActions({
|
||||
disabled: aiChat.disabled
|
||||
})
|
||||
]),
|
||||
...insertIf(!!strategyFuzzer?.visible, [
|
||||
renderButton({
|
||||
classNames: ["cbi-button-action"],
|
||||
onClick: strategyFuzzer.onClick,
|
||||
icon: renderCirclePlayIcon24,
|
||||
text: _("\u26A1 DPI Strategy Fuzzer"),
|
||||
loading: strategyFuzzer.loading,
|
||||
disabled: strategyFuzzer.disabled
|
||||
})
|
||||
]),
|
||||
...insertIf(checkServices.visible, [
|
||||
renderButton({
|
||||
classNames: ["cbi-button-action"],
|
||||
@@ -11563,6 +11698,502 @@ function renderAiChatModal() {
|
||||
ui.showModal(_("AI Chat & Tachyon Assistant"), modalContent);
|
||||
}
|
||||
|
||||
// src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||
function renderStrategyFuzzerModal(ruleNames = []) {
|
||||
let pollingInterval = null;
|
||||
let isPolling = false;
|
||||
let selectedEngine = "zapret2";
|
||||
let selectedTarget = "youtube";
|
||||
let customUrl = "";
|
||||
let selectedRuleSection = "";
|
||||
let isRunning = false;
|
||||
let currentState = null;
|
||||
const modalContainer = E("div", {
|
||||
class: "tachyon_fuzzer_modal",
|
||||
style: "display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 860px; box-sizing: border-box;"
|
||||
});
|
||||
const headerEl = E(
|
||||
"div",
|
||||
{
|
||||
style: "padding: 12px 16px; background: var(--background-color-secondary, rgba(0,0,0,0.25)); border: 1px solid var(--border-color, rgba(255,255,255,0.12)); border-radius: 8px;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "font-size: 15px; font-weight: bold; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;"
|
||||
},
|
||||
[
|
||||
E("span", {}, "\u26A1"),
|
||||
E("span", {}, _("Automated DPI Strategy Fuzzer & Auto-Tuner"))
|
||||
]
|
||||
),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "font-size: 12px; opacity: 0.75; line-height: 1.4;"
|
||||
},
|
||||
_(
|
||||
"Benchmarks a suite of packet desynchronization strategies in an isolated sandbox without disrupting active traffic, ranking them by latency, TTFB, and throughput."
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
const controlsGrid = E("div", {
|
||||
style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; align-items: end;"
|
||||
});
|
||||
const engineSelect = E(
|
||||
"select",
|
||||
{ class: "cbi-input-select", style: "width: 100%;" },
|
||||
[
|
||||
E(
|
||||
"option",
|
||||
{ value: "zapret2", selected: true },
|
||||
_("Zapret v2 (nfqws2)")
|
||||
),
|
||||
E("option", { value: "zapret" }, _("Zapret v1 (nfqws)")),
|
||||
E("option", { value: "byedpi" }, _("ByeDPI (ciadpi)")),
|
||||
E("option", { value: "all" }, _("All Engines (Full Matrix)"))
|
||||
]
|
||||
);
|
||||
engineSelect.addEventListener("change", () => {
|
||||
selectedEngine = engineSelect.value;
|
||||
});
|
||||
const engineGroup = E(
|
||||
"div",
|
||||
{ style: "display: flex; flex-direction: column; gap: 4px;" },
|
||||
[
|
||||
E(
|
||||
"label",
|
||||
{ style: "font-size: 12px; font-weight: 600;" },
|
||||
_("DPI Engine")
|
||||
),
|
||||
engineSelect
|
||||
]
|
||||
);
|
||||
TachyonShellMethods.getFuzzerStrategies().then((res) => {
|
||||
if (res.success && res.data?.available_engines) {
|
||||
const av = res.data.available_engines;
|
||||
const opts = engineSelect.options;
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
const opt = opts[i];
|
||||
if (opt.value === "zapret2") {
|
||||
opt.text = av.zapret2 ? _("Zapret v2 (nfqws2)") + " \u2014 " + _("Installed") : _("Zapret v2 (nfqws2)") + " \u2014 " + _("Not installed");
|
||||
if (!av.zapret2) opt.disabled = true;
|
||||
} else if (opt.value === "zapret") {
|
||||
opt.text = av.zapret ? _("Zapret v1 (nfqws)") + " \u2014 " + _("Installed") : _("Zapret v1 (nfqws)") + " \u2014 " + _("Not installed");
|
||||
if (!av.zapret) opt.disabled = true;
|
||||
} else if (opt.value === "byedpi") {
|
||||
opt.text = av.byedpi ? _("ByeDPI (ciadpi)") + " \u2014 " + _("Installed") : _("ByeDPI (ciadpi)") + " \u2014 " + _("Not installed");
|
||||
if (!av.byedpi) opt.disabled = true;
|
||||
}
|
||||
}
|
||||
if (av.zapret2) {
|
||||
selectedEngine = "zapret2";
|
||||
engineSelect.value = "zapret2";
|
||||
} else if (av.byedpi) {
|
||||
selectedEngine = "byedpi";
|
||||
engineSelect.value = "byedpi";
|
||||
} else if (av.zapret) {
|
||||
selectedEngine = "zapret";
|
||||
engineSelect.value = "zapret";
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
});
|
||||
const targetSelect = E(
|
||||
"select",
|
||||
{ class: "cbi-input-select", style: "width: 100%;" },
|
||||
[
|
||||
E(
|
||||
"option",
|
||||
{ value: "youtube", selected: true },
|
||||
_("YouTube (GoogleVideo 4K stream)")
|
||||
),
|
||||
E("option", { value: "youtube_web" }, _("YouTube Web (Interface)")),
|
||||
E("option", { value: "discord" }, _("Discord (Gateway / API / Voice)")),
|
||||
E("option", { value: "instagram" }, _("Instagram / Meta (TLS 1.3)")),
|
||||
E("option", { value: "rutracker" }, _("RuTracker (HTTP/HTTPS)")),
|
||||
E("option", { value: "telegram" }, _("Telegram Web")),
|
||||
E("option", { value: "custom" }, _("Custom Target URL..."))
|
||||
]
|
||||
);
|
||||
const customUrlInput = E("input", {
|
||||
type: "text",
|
||||
class: "cbi-input-text",
|
||||
placeholder: "https://example.com",
|
||||
style: "width: 100%; display: none; margin-top: 4px;"
|
||||
});
|
||||
targetSelect.addEventListener("change", () => {
|
||||
selectedTarget = targetSelect.value;
|
||||
if (selectedTarget === "custom") {
|
||||
customUrlInput.style.display = "block";
|
||||
} else {
|
||||
customUrlInput.style.display = "none";
|
||||
}
|
||||
});
|
||||
customUrlInput.addEventListener("input", () => {
|
||||
customUrl = customUrlInput.value.trim();
|
||||
});
|
||||
const targetGroup = E(
|
||||
"div",
|
||||
{ style: "display: flex; flex-direction: column; gap: 4px;" },
|
||||
[
|
||||
E(
|
||||
"label",
|
||||
{ style: "font-size: 12px; font-weight: 600;" },
|
||||
_("Target Service")
|
||||
),
|
||||
targetSelect,
|
||||
customUrlInput
|
||||
]
|
||||
);
|
||||
const ruleSelectOptions = [
|
||||
E("option", { value: "", selected: true }, _("Provider Global Default")),
|
||||
...ruleNames.map((r) => E("option", { value: r }, `${_("Rule:")} ${r}`))
|
||||
];
|
||||
const ruleSelect = E(
|
||||
"select",
|
||||
{ class: "cbi-input-select", style: "width: 100%;" },
|
||||
ruleSelectOptions
|
||||
);
|
||||
ruleSelect.addEventListener("change", () => {
|
||||
selectedRuleSection = ruleSelect.value;
|
||||
});
|
||||
const ruleGroup = E(
|
||||
"div",
|
||||
{ style: "display: flex; flex-direction: column; gap: 4px;" },
|
||||
[
|
||||
E(
|
||||
"label",
|
||||
{ style: "font-size: 12px; font-weight: 600;" },
|
||||
_("Apply Strategy To")
|
||||
),
|
||||
ruleSelect
|
||||
]
|
||||
);
|
||||
controlsGrid.append(engineGroup, targetGroup, ruleGroup);
|
||||
const progressContainer = E(
|
||||
"div",
|
||||
{
|
||||
style: "display: none; flex-direction: column; gap: 6px; padding: 12px; background: var(--background-color-secondary, rgba(0,0,0,0.18)); border-radius: 8px; border: 1px solid var(--border-color, rgba(255,255,255,0.1));"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; justify-content: space-between; font-size: 12px; font-weight: 600;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"span",
|
||||
{ id: "tachyon-fuzzer-status-text" },
|
||||
_("Benchmarking in progress...")
|
||||
),
|
||||
E("span", { id: "tachyon-fuzzer-progress-pct" }, "0%")
|
||||
]
|
||||
),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "width: 100%; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;"
|
||||
},
|
||||
[
|
||||
E("div", {
|
||||
id: "tachyon-fuzzer-progress-bar",
|
||||
style: "height: 100%; width: 0%; background: linear-gradient(90deg, #007bff, #00d2ff); transition: width 0.3s ease;"
|
||||
})
|
||||
]
|
||||
),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
id: "tachyon-fuzzer-current-strategy",
|
||||
style: "font-size: 11px; opacity: 0.7; font-family: monospace;"
|
||||
},
|
||||
""
|
||||
)
|
||||
]
|
||||
);
|
||||
const resultsContainer = E("div", {
|
||||
style: "max-height: 380px; overflow-y: auto; border: 1px solid var(--border-color, rgba(255,255,255,0.12)); border-radius: 8px;"
|
||||
});
|
||||
const tableEl = E(
|
||||
"table",
|
||||
{
|
||||
class: "table",
|
||||
style: "width: 100%; margin: 0; font-size: 12px; text-align: left; border-collapse: collapse;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"thead",
|
||||
{
|
||||
style: "background: var(--background-color-secondary, rgba(0,0,0,0.3)); position: sticky; top: 0; z-index: 2;"
|
||||
},
|
||||
[
|
||||
E("tr", {}, [
|
||||
E("th", { style: "padding: 8px 10px; width: 40px;" }, "#"),
|
||||
E("th", { style: "padding: 8px 10px; width: 80px;" }, _("Engine")),
|
||||
E(
|
||||
"th",
|
||||
{ style: "padding: 8px 10px;" },
|
||||
_("Strategy & Parameters")
|
||||
),
|
||||
E("th", { style: "padding: 8px 10px; width: 70px;" }, _("Status")),
|
||||
E("th", { style: "padding: 8px 10px; width: 65px;" }, _("TTFB")),
|
||||
E("th", { style: "padding: 8px 10px; width: 75px;" }, _("Speed")),
|
||||
E("th", { style: "padding: 8px 10px; width: 90px;" }, _("Score")),
|
||||
E(
|
||||
"th",
|
||||
{ style: "padding: 8px 10px; width: 80px; text-align: center;" },
|
||||
_("Action")
|
||||
)
|
||||
])
|
||||
]
|
||||
),
|
||||
E("tbody", { id: "tachyon-fuzzer-results-tbody" }, [
|
||||
E(
|
||||
"tr",
|
||||
{},
|
||||
E(
|
||||
"td",
|
||||
{
|
||||
colSpan: 8,
|
||||
style: "padding: 24px; text-align: center; opacity: 0.6;"
|
||||
},
|
||||
_(
|
||||
'No benchmark results yet. Select engine and click "Start Benchmark".'
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
]
|
||||
);
|
||||
resultsContainer.appendChild(tableEl);
|
||||
const startBtn = renderButton({
|
||||
text: _("\u{1F680} Start Benchmark"),
|
||||
classNames: ["cbi-button-action"],
|
||||
onClick: () => handleToggleRun()
|
||||
});
|
||||
const applyBestBtn = renderButton({
|
||||
text: _("\u{1F3C6} Apply Best Strategy"),
|
||||
classNames: ["cbi-button-save"],
|
||||
disabled: true,
|
||||
onClick: () => handleApplyBest()
|
||||
});
|
||||
const closeBtn = renderButton({
|
||||
text: _("Close"),
|
||||
classNames: ["cbi-button-neutral"],
|
||||
onClick: () => {
|
||||
stopPolling();
|
||||
ui.hideModal();
|
||||
}
|
||||
});
|
||||
const footerActions = E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; justify-content: space-between; align-items: center; padding-top: 8px; border-top: 1px solid var(--border-color, rgba(255,255,255,0.1));"
|
||||
},
|
||||
[
|
||||
E("div", { style: "display: flex; gap: 8px;" }, [startBtn]),
|
||||
E("div", { style: "display: flex; gap: 8px;" }, [applyBestBtn, closeBtn])
|
||||
]
|
||||
);
|
||||
const renderResults = (state) => {
|
||||
const tbody = document.getElementById("tachyon-fuzzer-results-tbody");
|
||||
if (!tbody) return;
|
||||
if (!state.results || state.results.length === 0) {
|
||||
if (state.running) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" style="padding: 20px; text-align: center;">${_(
|
||||
"Running initial strategy probe..."
|
||||
)}</td></tr>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = "";
|
||||
state.results.forEach((item, idx) => {
|
||||
const isBest = state.best_strategy && state.best_strategy.id === item.id;
|
||||
const statusBadge = item.success ? `<span class="badge" style="background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">${item.http_code || 200} OK</span>` : `<span class="badge" style="background: #dc3545; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">${item.error || "Blocked"}</span>`;
|
||||
let badgeHtml = "";
|
||||
if (item.badge) {
|
||||
badgeHtml = `<span style="display: inline-block; font-size: 10px; font-weight: bold; color: #ffc107; margin-left: 6px;">${item.badge}</span>`;
|
||||
}
|
||||
const tr = E(
|
||||
"tr",
|
||||
{
|
||||
style: `border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06)); ${isBest ? "background: rgba(40, 167, 69, 0.12); font-weight: 500;" : ""}`
|
||||
},
|
||||
[
|
||||
E("td", { style: "padding: 8px 10px;" }, String(idx + 1)),
|
||||
E(
|
||||
"td",
|
||||
{
|
||||
style: "padding: 8px 10px; font-family: monospace; font-size: 11px;"
|
||||
},
|
||||
item.engine
|
||||
),
|
||||
E("td", { style: "padding: 8px 10px;" }, [
|
||||
E("div", { style: "font-weight: 600;" }, [
|
||||
E("span", {}, item.name),
|
||||
E("span", { innerHTML: badgeHtml })
|
||||
]),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "font-size: 10px; opacity: 0.7; font-family: monospace; word-break: break-all; margin-top: 2px;"
|
||||
},
|
||||
item.args
|
||||
)
|
||||
]),
|
||||
E("td", { style: "padding: 8px 10px;", innerHTML: statusBadge }),
|
||||
E(
|
||||
"td",
|
||||
{ style: "padding: 8px 10px;" },
|
||||
item.success ? `${item.ttfb_ms}ms` : "\u2014"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ style: "padding: 8px 10px;" },
|
||||
item.success ? `${(item.speed_kbps / 1024).toFixed(1)}MB/s` : "\u2014"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ style: "padding: 8px 10px; font-weight: bold;" },
|
||||
item.success ? String(item.score) : "0"
|
||||
),
|
||||
E("td", { style: "padding: 8px 10px; text-align: center;" }, [
|
||||
renderButton({
|
||||
text: _("Apply"),
|
||||
classNames: ["cbi-button-action"],
|
||||
disabled: !item.success,
|
||||
onClick: () => handleApplySingle(item)
|
||||
})
|
||||
])
|
||||
]
|
||||
);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
};
|
||||
const updateProgressUI = (state) => {
|
||||
const statusText = document.getElementById("tachyon-fuzzer-status-text");
|
||||
const pctText = document.getElementById("tachyon-fuzzer-progress-pct");
|
||||
const progressBar = document.getElementById("tachyon-fuzzer-progress-bar");
|
||||
const currentStratEl = document.getElementById(
|
||||
"tachyon-fuzzer-current-strategy"
|
||||
);
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = state.running || state.results?.length ? "flex" : "none";
|
||||
}
|
||||
if (pctText) pctText.innerText = `${state.progress_pct}%`;
|
||||
if (progressBar) progressBar.style.width = `${state.progress_pct}%`;
|
||||
if (statusText) {
|
||||
if (state.running) {
|
||||
statusText.innerText = `${_("Testing strategy")} ${state.current_index} / ${state.total_strategies}...`;
|
||||
} else if (state.finished_at > 0) {
|
||||
statusText.innerText = state.best_strategy ? _("\u2705 Benchmark completed! Optimal strategy identified.") : _(
|
||||
"\u26A0\uFE0F Benchmark completed. No working bypass found for this target."
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentStratEl && state.current_strategy) {
|
||||
currentStratEl.innerText = `Testing: [${state.current_strategy.name}] -> ${state.current_strategy.args}`;
|
||||
} else if (currentStratEl && !state.running) {
|
||||
currentStratEl.innerText = "";
|
||||
}
|
||||
if (state.best_strategy && applyBestBtn) {
|
||||
applyBestBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
const pollStatus = async () => {
|
||||
if (isPolling) return;
|
||||
isPolling = true;
|
||||
try {
|
||||
const res = await TachyonShellMethods.getFuzzerStatus();
|
||||
if (res.success && res.data) {
|
||||
currentState = res.data;
|
||||
isRunning = res.data.running;
|
||||
updateProgressUI(res.data);
|
||||
renderResults(res.data);
|
||||
if (!isRunning) {
|
||||
stopPolling();
|
||||
startBtn.innerText = _("\u{1F680} Run Benchmark Again");
|
||||
startBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
isPolling = false;
|
||||
}
|
||||
};
|
||||
const startPolling = () => {
|
||||
stopPolling();
|
||||
pollingInterval = setInterval(pollStatus, 1e3);
|
||||
pollStatus();
|
||||
};
|
||||
const stopPolling = () => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
};
|
||||
const handleToggleRun = async () => {
|
||||
if (isRunning) {
|
||||
await TachyonShellMethods.stopFuzzer();
|
||||
showToast(_("Benchmark stopped"), "success");
|
||||
stopPolling();
|
||||
pollStatus();
|
||||
return;
|
||||
}
|
||||
startBtn.disabled = true;
|
||||
startBtn.innerText = _("\u{1F6D1} Stop Benchmark");
|
||||
const res = await TachyonShellMethods.startFuzzer(
|
||||
selectedEngine,
|
||||
selectedTarget,
|
||||
customUrl,
|
||||
selectedRuleSection
|
||||
);
|
||||
if (res.success) {
|
||||
showToast(_("Strategy benchmark started"), "success");
|
||||
startBtn.disabled = false;
|
||||
isRunning = true;
|
||||
startPolling();
|
||||
} else {
|
||||
const errMsg = !res.success ? res.error : _("Unknown error");
|
||||
showToast(`${_("Failed to start benchmark")}: ${errMsg}`, "error");
|
||||
startBtn.disabled = false;
|
||||
startBtn.innerText = _("\u{1F680} Start Benchmark");
|
||||
}
|
||||
};
|
||||
const handleApplySingle = async (item) => {
|
||||
const res = await TachyonShellMethods.applyFuzzerStrategy(
|
||||
item.engine,
|
||||
item.args,
|
||||
selectedRuleSection
|
||||
);
|
||||
if (res.success) {
|
||||
showToast(
|
||||
`${_("Applied")} "${item.name}" -> ${selectedRuleSection || _("Global Default")}`,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
showToast(_("Failed to apply strategy"), "error");
|
||||
}
|
||||
};
|
||||
const handleApplyBest = async () => {
|
||||
if (!currentState?.best_strategy) return;
|
||||
await handleApplySingle(currentState.best_strategy);
|
||||
};
|
||||
modalContainer.append(
|
||||
headerEl,
|
||||
controlsGrid,
|
||||
progressContainer,
|
||||
resultsContainer,
|
||||
footerActions
|
||||
);
|
||||
pollStatus();
|
||||
ui.showModal(_("\u26A1 Strategy Fuzzer & Auto-Tuner"), modalContainer);
|
||||
}
|
||||
|
||||
// src/tachyon/tabs/diagnostic/partials/renderSystemInfo.ts
|
||||
function renderSystemInfo({ items }) {
|
||||
return E(
|
||||
@@ -13311,6 +13942,14 @@ async function handleRunAiDoctor() {
|
||||
function handleOpenAiChat() {
|
||||
renderAiChatModal();
|
||||
}
|
||||
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));
|
||||
renderStrategyFuzzerModal(ruleNames);
|
||||
}).catch(() => {
|
||||
renderStrategyFuzzerModal([]);
|
||||
});
|
||||
}
|
||||
function handleRestoreNativeInternet() {
|
||||
ui.showModal(
|
||||
_("Restore Native Internet"),
|
||||
@@ -13558,6 +14197,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
||||
onClick: handleOpenAiChat,
|
||||
disabled: false
|
||||
},
|
||||
strategyFuzzer: {
|
||||
loading: false,
|
||||
visible: true,
|
||||
onClick: handleOpenStrategyFuzzer,
|
||||
disabled: false
|
||||
},
|
||||
checkServices: {
|
||||
visible: true,
|
||||
loading: diagnosticsActions.checkServices.loading,
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-25 21:32+0300\n"
|
||||
"PO-Revision-Date: 2026-08-25 21:32+0300\n"
|
||||
"POT-Creation-Date: 2026-08-26 21:45+0300\n"
|
||||
"PO-Revision-Date: 2026-08-26 21:45+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -47,21 +47,51 @@ msgstr "▶ Старт"
|
||||
msgid "⚙ Advanced Settings"
|
||||
msgstr "⚙ Настройки"
|
||||
|
||||
msgid "⚠️ Benchmark completed. No working bypass found for this target."
|
||||
msgstr "⚠️ Бенчмарк завершен. Не найдено рабочих стратегий для данной цели."
|
||||
|
||||
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 вашего провайдера в открытом виде. Используйте только в крайнем случае для предотвращения полной потери интернета."
|
||||
|
||||
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 "⚡ DPI Strategy Fuzzer"
|
||||
msgstr "⚡ DPI Strategy Fuzzer"
|
||||
|
||||
msgid "⚡ Strategy Fuzzer & Auto-Tuner"
|
||||
msgstr "⚡ Подборщик стратегий и авто-тюнер"
|
||||
|
||||
msgid "✅ Benchmark completed! Optimal strategy identified."
|
||||
msgstr "✅ Бенчмарк завершен! Оптимальная стратегия определена."
|
||||
|
||||
msgid "✔ Online"
|
||||
msgstr ""
|
||||
|
||||
msgid "✔ Running"
|
||||
msgstr "✔ Работает"
|
||||
|
||||
msgid "✘ Offline"
|
||||
msgstr ""
|
||||
|
||||
msgid "✘ Stopped"
|
||||
msgstr "✘ Остановлено"
|
||||
|
||||
msgid "🏆 Apply Best Strategy"
|
||||
msgstr "🏆 Применить лучшую стратегию"
|
||||
|
||||
msgid "🔑 Generate"
|
||||
msgstr "🔑 Сгенерировать"
|
||||
|
||||
msgid "🚀 Run Benchmark Again"
|
||||
msgstr "🚀 Запустить бенчмарк снова"
|
||||
|
||||
msgid "🚀 Start Benchmark"
|
||||
msgstr "🚀 Начать бенчмарк"
|
||||
|
||||
msgid "🛑 Stop Benchmark"
|
||||
msgstr "🛑 Остановить бенчмарк"
|
||||
|
||||
msgid "Accept external proxy connections and route them with sing-box."
|
||||
msgstr "Принимать внешние прокси-подключения и маршрутизировать их через sing-box."
|
||||
|
||||
@@ -224,6 +254,9 @@ msgstr "Все проверки пройдены — бот должен раб
|
||||
msgid "All devices"
|
||||
msgstr "Все устройства"
|
||||
|
||||
msgid "All Engines (Full Matrix)"
|
||||
msgstr "Все движки (Полная матрица)"
|
||||
|
||||
msgid "All except selected"
|
||||
msgstr "Все кроме выбранных"
|
||||
|
||||
@@ -305,12 +338,21 @@ msgstr "Другое действие уже выполняется"
|
||||
msgid "API Key for OpenAI (sk-...) or DeepSeek (optional for Ollama / LM Studio)"
|
||||
msgstr "API-ключ для OpenAI (sk-...) или DeepSeek (опционально для Ollama / LM Studio)"
|
||||
|
||||
msgid "Applied"
|
||||
msgstr "Применено"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr "Применить"
|
||||
|
||||
msgid "Apply rules of this section only to selected protocols."
|
||||
msgstr "Применять правила этой секции только к выбранным протоколам."
|
||||
|
||||
msgid "Apply section rules only to the specified local IP addresses"
|
||||
msgstr "Применять правила секции только к указанным локальным IP-адресам"
|
||||
|
||||
msgid "Apply Strategy To"
|
||||
msgstr "Применить стратегию к"
|
||||
|
||||
msgid "Applying fix"
|
||||
msgstr "Применение исправления"
|
||||
|
||||
@@ -350,6 +392,9 @@ msgstr "Автоопределённые домены"
|
||||
msgid "Auto-detects blocked domains from logs and adds them to the first section where they work via proxy."
|
||||
msgstr "Автоматически определяет заблокированные домены из логов и добавляет их в первую секцию, где они работают через прокси."
|
||||
|
||||
msgid "Auto-failover of proxy sections"
|
||||
msgstr "Авто-failover прокси-секций"
|
||||
|
||||
msgid "Auto-generate HWID"
|
||||
msgstr "Авто-генерация HWID"
|
||||
|
||||
@@ -359,6 +404,9 @@ msgstr "Автолечение сетевого стека"
|
||||
msgid "Auto-update Hosts Lists"
|
||||
msgstr "Автообновление списков Hosts"
|
||||
|
||||
msgid "Automated DPI Strategy Fuzzer & Auto-Tuner"
|
||||
msgstr "Автоматический подборщик и оптимизатор стратегий DPI"
|
||||
|
||||
msgid "Automatic component update checks"
|
||||
msgstr "Автоматическая проверка обновлений компонентов"
|
||||
|
||||
@@ -407,6 +455,15 @@ msgstr "Начало интервала расписания (например,
|
||||
msgid "Belarus (by)"
|
||||
msgstr "Беларусь (by)"
|
||||
|
||||
msgid "Benchmark stopped"
|
||||
msgstr "Бенчмарк остановлен"
|
||||
|
||||
msgid "Benchmarking in progress..."
|
||||
msgstr "Выполняется бенчмарк..."
|
||||
|
||||
msgid "Benchmarks a suite of packet desynchronization strategies in an isolated sandbox without disrupting active traffic, ranking them by latency, TTFB, and throughput."
|
||||
msgstr "Тестирует набор стратегий десинхронизации пакетов в изолированном сэндбоксе без влияния на активный трафик и ранжирует их по задержке, TTFB и скорости."
|
||||
|
||||
msgid "BitTorrent / P2P"
|
||||
msgstr "BitTorrent / P2P"
|
||||
|
||||
@@ -470,6 +527,9 @@ msgstr "Встроенные наборы правил"
|
||||
msgid "By flag emoji from name"
|
||||
msgstr "По эмодзи флага из имени"
|
||||
|
||||
msgid "ByeDPI (ciadpi)"
|
||||
msgstr "ByeDPI (ciadpi)"
|
||||
|
||||
msgid "ByeDPI listen address and port are assigned by Tachyon and must not be set in the strategy."
|
||||
msgstr "Адрес и порт прослушивания ByeDPI назначаются Tachyon и не должны задаваться в стратегии."
|
||||
|
||||
@@ -827,6 +887,15 @@ msgstr "Пользовательский URL эндпоинта OpenAI-совм
|
||||
msgid "Custom outbound configurations in JSON format"
|
||||
msgstr "Пользовательские конфигурации outbound в формате JSON"
|
||||
|
||||
msgid "Custom Target URL..."
|
||||
msgstr "Свой URL для теста..."
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Дневная квота"
|
||||
|
||||
msgid "Daily time quota (minutes)"
|
||||
msgstr "Дневной лимит времени (минуты)"
|
||||
|
||||
msgid "Dashboard"
|
||||
msgstr "Панель управления"
|
||||
|
||||
@@ -836,33 +905,6 @@ msgstr "Дашборд сейчас недоступен"
|
||||
msgid "Days of Week"
|
||||
msgstr "Дни недели"
|
||||
|
||||
msgid "Auto-failover of proxy sections"
|
||||
msgstr "Авто-failover прокси-секций"
|
||||
|
||||
msgid "Watchdog probes every Connection section; when the active one fails several checks in a row, traffic switches to a healthy backup and Telegram is notified. Requires a config reload to take effect."
|
||||
msgstr "Watchdog проверяет каждую Connection-секцию; если активная падает несколько проверок подряд, трафик переключается на живую резервную, а Telegram получает уведомление. Нужен reload конфигурации."
|
||||
|
||||
msgid "Failover failure threshold"
|
||||
msgstr "Порог срабатывания failover"
|
||||
|
||||
msgid "Number of consecutive failed probes before switching sections"
|
||||
msgstr "Сколько неудачных проверок подряд до переключения секции"
|
||||
|
||||
msgid "Daily time quota (minutes)"
|
||||
msgstr "Дневной лимит времени (минуты)"
|
||||
|
||||
msgid "Limit device network usage per day. When the quota is exhausted, the device is blocked until midnight. 0 disables the limit."
|
||||
msgstr "Ограничьте использование сети устройством за день. Когда квота исчерпана, устройство блокируется до полуночи. 0 — без лимита."
|
||||
|
||||
msgid "Enter a whole number of minutes between 0 and 1440"
|
||||
msgstr "Введите целое число минут от 0 до 1440"
|
||||
|
||||
msgid "Daily Quota"
|
||||
msgstr "Дневная квота"
|
||||
|
||||
msgid "min/day"
|
||||
msgstr "мин/день"
|
||||
|
||||
msgid "Default (auto)"
|
||||
msgstr "По умолчанию (авто)"
|
||||
|
||||
@@ -959,6 +1001,9 @@ msgstr "Отключено"
|
||||
msgid "Disabling interception is incompatible with action=zapret2 because Tachyon sends matched traffic through NFQUEUE."
|
||||
msgstr "Отключение перехвата несовместимо с action=zapret2, потому что Tachyon направляет совпавший трафик через NFQUEUE."
|
||||
|
||||
msgid "Discord (Gateway / API / Voice)"
|
||||
msgstr "Discord (Шлюз / API / Голосовые каналы)"
|
||||
|
||||
msgid "Display name"
|
||||
msgstr "Отображаемое имя"
|
||||
|
||||
@@ -1142,6 +1187,9 @@ msgstr "Загружено"
|
||||
msgid "Downloading and applying rule sets and lists..."
|
||||
msgstr "Загрузка и применение наборов правил и списков..."
|
||||
|
||||
msgid "DPI Engine"
|
||||
msgstr "Движок DPI"
|
||||
|
||||
msgid "Drag rows to change priority. The rule at the top is checked first."
|
||||
msgstr "Перетащите строки для изменения приоритета. Правило вверху проверяется первым."
|
||||
|
||||
@@ -1274,6 +1322,9 @@ msgstr "Конец интервала расписания (например, 08
|
||||
msgid "End Time (HH:MM)"
|
||||
msgstr "Время окончания (ЧЧ:ММ)"
|
||||
|
||||
msgid "Engine"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a complete sing-box inbound object"
|
||||
msgstr "Введите полный JSON объект inbound для sing-box"
|
||||
|
||||
@@ -1289,6 +1340,9 @@ msgstr "Введите домен или IP для проверки (напр. e
|
||||
msgid "Enter a level name"
|
||||
msgstr "Введите имя уровня"
|
||||
|
||||
msgid "Enter a whole number of minutes between 0 and 1440"
|
||||
msgstr "Введите целое число минут от 0 до 1440"
|
||||
|
||||
msgid "Enter HWID"
|
||||
msgstr "Введите HWID"
|
||||
|
||||
@@ -1367,6 +1421,9 @@ msgstr "Внешние файлы конфигурации nfqws2 обходят
|
||||
msgid "Failed to apply fix"
|
||||
msgstr "Не удалось применить исправление"
|
||||
|
||||
msgid "Failed to apply strategy"
|
||||
msgstr "Не удалось применить стратегию"
|
||||
|
||||
msgid "Failed to close connection"
|
||||
msgstr "Не удалось закрыть подключение"
|
||||
|
||||
@@ -1391,6 +1448,12 @@ msgstr "Не удалось создать отчёт об ошибке"
|
||||
msgid "Failed to generate WARP config:"
|
||||
msgstr "Не удалось сгенерировать конфигурацию WARP:"
|
||||
|
||||
msgid "Failed to get fuzzer status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to get fuzzer strategies"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load snapshots"
|
||||
msgstr "Не удалось загрузить снапшоты"
|
||||
|
||||
@@ -1436,9 +1499,15 @@ msgstr "Ошибка сохранения настроек Smart Detect"
|
||||
msgid "Failed to save snapshot"
|
||||
msgstr "Не удалось сохранить снапшот"
|
||||
|
||||
msgid "Failed to start benchmark"
|
||||
msgstr "Не удалось запустить бенчмарк"
|
||||
|
||||
msgid "Failed to start bot"
|
||||
msgstr "Не удалось запустить бота"
|
||||
|
||||
msgid "Failed to start fuzzer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start service check."
|
||||
msgstr "Не удалось запустить проверку сервисов."
|
||||
|
||||
@@ -1448,6 +1517,9 @@ msgstr "Не удалось запустить watchdog"
|
||||
msgid "Failed to stop bot"
|
||||
msgstr "Не удалось остановить бота"
|
||||
|
||||
msgid "Failed to stop fuzzer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to stop watchdog"
|
||||
msgstr "Не удалось остановить watchdog"
|
||||
|
||||
@@ -1460,6 +1532,9 @@ msgstr "Не удалось обновить устройство"
|
||||
msgid "Failed to update subscriptions"
|
||||
msgstr "Не удалось обновить подписки"
|
||||
|
||||
msgid "Failover failure threshold"
|
||||
msgstr "Порог срабатывания failover"
|
||||
|
||||
msgid "FakeIP and control checks use different public IPs"
|
||||
msgstr "FakeIP и контрольная проверка используют разные публичные IP"
|
||||
|
||||
@@ -1604,6 +1679,9 @@ msgstr "Глобальная проверка"
|
||||
msgid "Global check failed"
|
||||
msgstr "Не удалось выполнить глобальную проверку"
|
||||
|
||||
msgid "Global Default"
|
||||
msgstr ""
|
||||
|
||||
msgid "gRPC service"
|
||||
msgstr "Сервис gRPC"
|
||||
|
||||
@@ -1811,12 +1889,18 @@ msgstr "Полезный груз init (I1)"
|
||||
msgid "Init timeout (Itime)"
|
||||
msgstr "Таймаут init (Itime)"
|
||||
|
||||
msgid "Instagram / Meta (TLS 1.3)"
|
||||
msgstr "Instagram / Meta (TLS 1.3)"
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Install another build:"
|
||||
msgstr "Установить другую сборку:"
|
||||
|
||||
msgid "Installed"
|
||||
msgstr "Установлен"
|
||||
|
||||
msgid "Installed build → Available build"
|
||||
msgstr "Установленная сборка → Доступная сборка"
|
||||
|
||||
@@ -2186,6 +2270,9 @@ msgstr "Оставьте пустым, чтобы пропустить этот
|
||||
msgid "Level name"
|
||||
msgstr "Имя уровня"
|
||||
|
||||
msgid "Limit device network usage per day. When the quota is exhausted, the device is blocked until midnight. 0 disables the limit."
|
||||
msgstr "Ограничьте использование сети устройством за день. Когда квота исчерпана, устройство блокируется до полуночи. 0 — без лимита."
|
||||
|
||||
msgid "List must be an HTTP(S) URL or a local .lst path"
|
||||
msgstr "Список должен быть HTTP(S) URL или локальным путём .lst"
|
||||
|
||||
@@ -2273,6 +2360,9 @@ msgstr "ID аккаунта MASQUE"
|
||||
msgid "MASQUE Private Key"
|
||||
msgstr "Приватный ключ MASQUE"
|
||||
|
||||
msgid "Masquerade LAN traffic into tailnet"
|
||||
msgstr "Маскарадинг LAN-трафика в тейлнет"
|
||||
|
||||
msgid "Match destination IPs or subnets"
|
||||
msgstr "Сопоставлять IP-адреса или подсети назначения"
|
||||
|
||||
@@ -2306,6 +2396,9 @@ msgstr "Метод"
|
||||
msgid "Metrics Retention (hours)"
|
||||
msgstr "Хранение метрик (часы)"
|
||||
|
||||
msgid "min/day"
|
||||
msgstr "мин/день"
|
||||
|
||||
msgid "Minimum latency difference in milliseconds that triggers switching to a faster server."
|
||||
msgstr "Минимальная разница задержки в миллисекундах, при которой происходит переключение на более быстрый сервер."
|
||||
|
||||
@@ -2369,6 +2462,9 @@ msgstr "Имя в списке уровней приоритета"
|
||||
msgid "Native internet restored. Tachyon stopped."
|
||||
msgstr "Обычный интернет восстановлен. Tachyon остановлен."
|
||||
|
||||
msgid "Native tailscaled (tailnet reachable from LAN without client)"
|
||||
msgstr "Нативный tailscaled (тейлнет доступен из LAN без клиента)"
|
||||
|
||||
msgid "Netherlands (nl)"
|
||||
msgstr "Нидерланды (nl)"
|
||||
|
||||
@@ -2420,6 +2516,9 @@ msgstr "Активные устройства в локальной сети н
|
||||
msgid "No active routing sections found."
|
||||
msgstr "Активные секции маршрутизации не найдены."
|
||||
|
||||
msgid "No benchmark results yet. Select engine and click \"Start Benchmark\"."
|
||||
msgstr "Нет результатов бенчмарка. Выберите движок и нажмите «Начать бенчмарк»."
|
||||
|
||||
msgid "No check targets found in the configuration."
|
||||
msgstr "Цели для проверки не найдены в конфигурации."
|
||||
|
||||
@@ -2537,6 +2636,9 @@ msgstr "Уведомлять об обновлениях подписок"
|
||||
msgid "Number of consecutive failed checks required before switching DNS servers."
|
||||
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
|
||||
|
||||
msgid "Number of consecutive failed probes before switching sections"
|
||||
msgstr "Сколько неудачных проверок подряд до переключения секции"
|
||||
|
||||
msgid "Number of consecutive failures before restarting (default 3)."
|
||||
msgstr "Количество последовательных ошибок до перезапуска (по умолчанию 3)."
|
||||
|
||||
@@ -2642,6 +2744,9 @@ msgstr "Опциональные значения должны быть прис
|
||||
msgid "Or paste hosts file content:"
|
||||
msgstr "Или вставьте содержимое файла hosts:"
|
||||
|
||||
msgid "Other peers"
|
||||
msgstr ""
|
||||
|
||||
msgid "outbounds must be a non-empty array of strings"
|
||||
msgstr "outbounds должен быть непустым массивом строк"
|
||||
|
||||
@@ -2828,6 +2933,9 @@ msgstr "Защита"
|
||||
msgid "Protocol"
|
||||
msgstr "Протокол"
|
||||
|
||||
msgid "Provider Global Default"
|
||||
msgstr "Глобальные настройки провайдера"
|
||||
|
||||
msgid "Proxy"
|
||||
msgstr "Прокси"
|
||||
|
||||
@@ -3059,6 +3167,9 @@ msgstr "Возвращать NXDOMAIN для заблокированных до
|
||||
msgid "Route"
|
||||
msgstr "Маршрут"
|
||||
|
||||
msgid "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines."
|
||||
msgstr "Направлять весь не-локальный LAN-трафик через exit-node тейлнета вместо прокси-движков."
|
||||
|
||||
msgid "Route bot API requests through this section. Leave empty to use the default mixed proxy."
|
||||
msgstr "Маршрутизировать запросы API бота через эту секцию. Оставьте пустым для использования смешанного прокси по умолчанию."
|
||||
|
||||
@@ -3101,6 +3212,9 @@ msgstr "Наборы правил"
|
||||
msgid "Rule Sets (.srs / .json)"
|
||||
msgstr "Наборы правил (.srs / .json)"
|
||||
|
||||
msgid "Rule:"
|
||||
msgstr "Правило:"
|
||||
|
||||
msgid "Rules"
|
||||
msgstr "Правила"
|
||||
|
||||
@@ -3137,9 +3251,15 @@ msgstr "Запущен"
|
||||
msgid "Running diagnostics…"
|
||||
msgstr "Выполняется диагностика…"
|
||||
|
||||
msgid "Running initial strategy probe..."
|
||||
msgstr "Запуск первого зонда стратегии..."
|
||||
|
||||
msgid "Russia (ru)"
|
||||
msgstr "Россия (ru)"
|
||||
|
||||
msgid "RuTracker (HTTP/HTTPS)"
|
||||
msgstr "RuTracker (HTTP/HTTPS)"
|
||||
|
||||
msgid "Sat"
|
||||
msgstr "Сб"
|
||||
|
||||
@@ -3170,6 +3290,9 @@ msgstr "Расписание"
|
||||
msgid "Schedule Rule"
|
||||
msgstr "Правило расписания"
|
||||
|
||||
msgid "Score"
|
||||
msgstr "Баллы"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
@@ -3464,6 +3587,9 @@ msgstr "Показывать метаданные подписки для это
|
||||
msgid "Sing-box autostart disabled"
|
||||
msgstr "Автостарт sing-box отключен"
|
||||
|
||||
msgid "sing-box endpoint (userspace, default)"
|
||||
msgstr "Endpoint в sing-box (userspace, по умолчанию)"
|
||||
|
||||
msgid "Sing-box FakeIP DNS does not work"
|
||||
msgstr "FakeIP DNS sing-box не работает"
|
||||
|
||||
@@ -3512,6 +3638,9 @@ msgstr "Имя снапшота"
|
||||
msgid "Snapshot restored. The service restarts and the page will reload."
|
||||
msgstr "Снапшот восстановлен. Сервис перезапускается, страница будет перезагружена."
|
||||
|
||||
msgid "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes."
|
||||
msgstr "Подменять LAN-адреса при выходе через tailscale0, чтобы пиры могли отвечать. Нужно, если не все пиры принимают маршруты."
|
||||
|
||||
msgid "Snell Version"
|
||||
msgstr "Версия Snell"
|
||||
|
||||
@@ -3545,6 +3674,9 @@ msgstr "Укажите путь к файлу"
|
||||
msgid "Specify different files"
|
||||
msgstr "Укажите разные файлы"
|
||||
|
||||
msgid "Speed"
|
||||
msgstr "Скорость"
|
||||
|
||||
msgid "SSH"
|
||||
msgstr "SSH"
|
||||
|
||||
@@ -3605,6 +3737,9 @@ msgstr "Сначала запустите Watchdog"
|
||||
msgid "Starting…"
|
||||
msgstr "Запуск…"
|
||||
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Статус"
|
||||
|
||||
@@ -3629,6 +3764,12 @@ msgstr "Остановка…"
|
||||
msgid "Store auto-detected domains in /etc instead of /tmp to survive reboots."
|
||||
msgstr "Хранить автоопределённые домены в /etc вместо /tmp для сохранения после перезагрузки."
|
||||
|
||||
msgid "Strategy & Parameters"
|
||||
msgstr "Стратегия и параметры"
|
||||
|
||||
msgid "Strategy benchmark started"
|
||||
msgstr "Бенчмарк стратегий запущен"
|
||||
|
||||
msgid "Strict ECH + DoQ DNS Enforcer"
|
||||
msgstr "Строгий ECH + DoQ DNS Enforcer"
|
||||
|
||||
@@ -3752,32 +3893,11 @@ msgstr "NFQWS2 Tachyon не запущен"
|
||||
msgid "Tachyon-managed nfqws2 runtime is ready"
|
||||
msgstr "NFQWS2 Tachyon готов"
|
||||
|
||||
msgid "Tailscale auth key"
|
||||
msgid "Tailscale mode"
|
||||
msgstr "Режим Tailscale"
|
||||
|
||||
msgid "sing-box endpoint (userspace, default)"
|
||||
msgstr "Endpoint в sing-box (userspace, по умолчанию)"
|
||||
|
||||
msgid "Native tailscaled (tailnet reachable from LAN without client)"
|
||||
msgstr "Нативный tailscaled (тейлнет доступен из LAN без клиента)"
|
||||
|
||||
msgid "Masquerade LAN traffic into tailnet"
|
||||
msgstr "Маскарадинг LAN-трафика в тейлнет"
|
||||
|
||||
msgid "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes."
|
||||
msgstr "Подменять LAN-адреса при выходе через tailscale0, чтобы пиры могли отвечать. Нужно, если не все пиры принимают маршруты."
|
||||
|
||||
msgid "Use tailnet exit node for LAN"
|
||||
msgstr "Использовать exit-node тейлнета для LAN"
|
||||
|
||||
msgid "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines."
|
||||
msgstr "Направлять весь не-локальный LAN-трафик через exit-node тейлнета вместо прокси-движков."
|
||||
|
||||
msgid "Tailscale"
|
||||
msgstr "Tailscale"
|
||||
|
||||
msgstr "Ключ аутентификации Tailscale"
|
||||
msgid "Tailscale auth key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tailscale control URL"
|
||||
msgstr "Control URL Tailscale"
|
||||
@@ -3788,6 +3908,9 @@ msgstr "Tailscale endpoint"
|
||||
msgid "Tailscale hostname"
|
||||
msgstr "Имя хоста Tailscale"
|
||||
|
||||
msgid "Tailscale mode"
|
||||
msgstr "Режим Tailscale"
|
||||
|
||||
msgid "Target"
|
||||
msgstr "Цель"
|
||||
|
||||
@@ -3797,6 +3920,9 @@ msgstr "Целевые устройства (IP / MAC)"
|
||||
msgid "Target Scope"
|
||||
msgstr "Область действия"
|
||||
|
||||
msgid "Target Service"
|
||||
msgstr "Целевой сервис"
|
||||
|
||||
msgid "Telegram Bot"
|
||||
msgstr "Telegram Бот"
|
||||
|
||||
@@ -3806,6 +3932,9 @@ msgstr "Токен Telegram-бота"
|
||||
msgid "Telegram Channel"
|
||||
msgstr "Telegram канал"
|
||||
|
||||
msgid "Telegram Web"
|
||||
msgstr "Telegram Web"
|
||||
|
||||
msgid "Test Connection"
|
||||
msgstr "Проверить подключение"
|
||||
|
||||
@@ -3833,6 +3962,9 @@ msgstr "Тестировать серверы, чьи имена соответ
|
||||
msgid "Test Telegram Connection"
|
||||
msgstr "Проверить подключение к Telegram"
|
||||
|
||||
msgid "Testing strategy"
|
||||
msgstr "Тестирование стратегии"
|
||||
|
||||
msgid "Testing URL"
|
||||
msgstr "URL проверки"
|
||||
|
||||
@@ -3890,6 +4022,9 @@ msgstr "Этот флаг не принимает значение."
|
||||
msgid "This option requires a value."
|
||||
msgstr "Этот параметр требует значение."
|
||||
|
||||
msgid "This router"
|
||||
msgstr ""
|
||||
|
||||
msgid "This will erase all Tachyon settings and restore the factory defaults. The service will restart. This action cannot be undone."
|
||||
msgstr "Все настройки Tachyon будут удалены и восстановлены заводские значения. Сервис будет перезапущен. Это действие нельзя отменить."
|
||||
|
||||
@@ -3998,6 +4133,9 @@ msgstr "Устранение неполадок"
|
||||
msgid "Try compatible User-Agent profiles automatically when downloading this subscription"
|
||||
msgstr "Пробовать совместимые профили User-Agent автоматически при загрузке этой подписки"
|
||||
|
||||
msgid "TTFB"
|
||||
msgstr "TTFB"
|
||||
|
||||
msgid "TTL must be a positive number"
|
||||
msgstr "TTL должен быть положительным числом"
|
||||
|
||||
@@ -4232,6 +4370,9 @@ msgstr "Используйте формат длительности sing-box,
|
||||
msgid "Use sing-box duration format like 1d, 12h or 6h"
|
||||
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 6h"
|
||||
|
||||
msgid "Use tailnet exit node for LAN"
|
||||
msgstr "Использовать exit-node тейлнета для LAN"
|
||||
|
||||
msgid "Used by"
|
||||
msgstr "Используется"
|
||||
|
||||
@@ -4316,6 +4457,9 @@ msgstr "Секция прокси WARP"
|
||||
msgid "Watchdog"
|
||||
msgstr "Сторожевой таймер"
|
||||
|
||||
msgid "Watchdog probes every Connection section; when the active one fails several checks in a row, traffic switches to a healthy backup and Telegram is notified. Requires a config reload to take effect."
|
||||
msgstr "Watchdog проверяет каждую Connection-секцию; если активная падает несколько проверок подряд, трафик переключается на живую резервную, а Telegram получает уведомление. Нужен reload конфигурации."
|
||||
|
||||
msgid "Watchdog Status"
|
||||
msgstr "Статус Watchdog"
|
||||
|
||||
@@ -4361,6 +4505,12 @@ msgstr "Да"
|
||||
msgid "You can select Output Network Interface, by default autodetect"
|
||||
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
|
||||
|
||||
msgid "YouTube (GoogleVideo 4K stream)"
|
||||
msgstr "YouTube (4K видеопоток GoogleVideo)"
|
||||
|
||||
msgid "YouTube Web (Interface)"
|
||||
msgstr "YouTube Web (Интерфейс)"
|
||||
|
||||
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
|
||||
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
|
||||
|
||||
@@ -4382,6 +4532,12 @@ msgstr "sing-box outbound для Zapret настроен"
|
||||
msgid "Zapret sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret не настроен"
|
||||
|
||||
msgid "Zapret v1 (nfqws)"
|
||||
msgstr "Zapret v1 (nfqws)"
|
||||
|
||||
msgid "Zapret v2 (nfqws2)"
|
||||
msgstr "Zapret v2 (nfqws2)"
|
||||
|
||||
msgid "Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies."
|
||||
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
|
||||
|
||||
@@ -4690,3 +4846,6 @@ msgstr "Предотвращать множественные вызовы reloa
|
||||
|
||||
msgid "Restoring native direct internet..."
|
||||
msgstr "Восстановление прямого интернета..."
|
||||
|
||||
msgid "Find optimal DPI bypass strategies with isolated live probing"
|
||||
msgstr "Автоматический подбор оптимальных параметров обхода DPI в изолированном сэндбоксе"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -259,6 +259,11 @@ 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: [ "providers/byedpi/validator.uc", "validate-json", 1 ],
|
||||
fuzzer_start: [ "diagnostics/fuzzer.uc", "start", 4 ],
|
||||
fuzzer_status: [ "diagnostics/fuzzer.uc", "status", 0 ],
|
||||
fuzzer_stop: [ "diagnostics/fuzzer.uc", "stop", 0 ],
|
||||
fuzzer_apply: [ "diagnostics/fuzzer.uc", "apply", 3 ],
|
||||
fuzzer_strategies: [ "diagnostics/fuzzer.uc", "strategies", 0 ],
|
||||
generate_warp: [ "service/warp_generator.uc", "", 1 ],
|
||||
package_prerm: [ "service/package.uc", "prerm", 1 ],
|
||||
package_postinst: [ "service/package.uc", "postinst", 0 ],
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let common = require("core.common");
|
||||
let uci_core = require("core.uci");
|
||||
|
||||
let as_string = common.as_string;
|
||||
let read_json_file = common.read_json_file;
|
||||
let write_json_file = common.write_json_file;
|
||||
let command_from_args = common.command_from_args;
|
||||
let command_output = common.command_output;
|
||||
let command_status = common.command_status;
|
||||
let command_success = common.command_success;
|
||||
let command_success_from_args = common.command_success_from_args;
|
||||
let shell_quote = common.shell_quote;
|
||||
let object_or_empty = common.object_or_empty;
|
||||
let array_or_empty = common.array_or_empty;
|
||||
|
||||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||
const STATE_DIR = getenv("TACHYON_FUZZER_STATE_DIR") || "/var/run/tachyon";
|
||||
const STATE_FILE = STATE_DIR + "/fuzzer-state.json";
|
||||
const PID_FILE = STATE_DIR + "/fuzzer-worker.pid";
|
||||
const BYEDPI_PORT = 11089;
|
||||
const NFQUEUE_QNUM_ZAPRET = 298;
|
||||
const NFQUEUE_QNUM_ZAPRET2 = 299;
|
||||
const FUZZER_FWMARK = "0x08000000";
|
||||
|
||||
function resolve_binary(paths) {
|
||||
for (let p in paths) {
|
||||
if (p && fs.stat(p) != null)
|
||||
return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function get_zapret2_bin() {
|
||||
return resolve_binary([
|
||||
getenv("ZAPRET2_NFQWS2_BIN"),
|
||||
"/opt/zapret2/nfq2/nfqws2",
|
||||
"/opt/zapret2/nfqws2",
|
||||
"/usr/bin/nfqws2"
|
||||
]);
|
||||
}
|
||||
|
||||
function get_zapret_bin() {
|
||||
return resolve_binary([
|
||||
getenv("ZAPRET_NFQWS_BIN"),
|
||||
"/opt/zapret/nfq/nfqws",
|
||||
"/opt/zapret/nfqws",
|
||||
"/usr/bin/nfqws"
|
||||
]);
|
||||
}
|
||||
|
||||
function get_byedpi_bin() {
|
||||
return resolve_binary([
|
||||
getenv("BYEDPI_BIN"),
|
||||
"/opt/byedpi/ciadpi",
|
||||
"/usr/bin/ciadpi"
|
||||
]);
|
||||
}
|
||||
|
||||
// Target definitions
|
||||
const TARGET_URLS = {
|
||||
youtube: "https://rr1---sn-4g5edn6r.googlevideo.com/generate_204",
|
||||
youtube_web: "https://www.youtube.com",
|
||||
discord: "https://discord.com/api/v9/gateway",
|
||||
instagram: "https://www.instagram.com",
|
||||
rutracker: "https://rutracker.org",
|
||||
telegram: "https://web.telegram.org"
|
||||
};
|
||||
|
||||
// Strategy Matrices
|
||||
const STRATEGIES_ZAPRET2 = [
|
||||
{
|
||||
id: "z2_yt_multisplit_midsld",
|
||||
name: "YouTube 4K Multisplit + MidSLD",
|
||||
engine: "zapret2",
|
||||
args: "--lua-desync=multisplit:pos=1,midsld:seqovl=1:fooling=badseq",
|
||||
description: "Optimized for GoogleVideo 4K chunk streams and TSPU TLS desync."
|
||||
},
|
||||
{
|
||||
id: "z2_yt_multisplit_sniext",
|
||||
name: "SNI Extension Split + Badseq",
|
||||
engine: "zapret2",
|
||||
args: "--lua-desync=multisplit:pos=1,sniext+4:seqovl=1:fooling=badseq",
|
||||
description: "Splits deep into SNI extensions to fool next-gen DPI signatures."
|
||||
},
|
||||
{
|
||||
id: "z2_fake_badseq_mid",
|
||||
name: "Fake Packet + MidSLD Disorder",
|
||||
engine: "zapret2",
|
||||
args: "--lua-desync=fake:ttl=8 --lua-desync=multisplit:pos=midsld:fooling=badseq",
|
||||
description: "Injects low-TTL fake ClientHello before segmented payload."
|
||||
},
|
||||
{
|
||||
id: "z2_wsize_multisplit",
|
||||
name: "Window Size Clamp (wsize=1)",
|
||||
engine: "zapret2",
|
||||
args: "--lua-desync=multisplit:pos=1,midsld:wsize=1:fooling=badseq",
|
||||
description: "Forces single-byte TCP window segments to evade reassembly."
|
||||
},
|
||||
{
|
||||
id: "z2_aggressive_combo",
|
||||
name: "Aggressive Triple-Split + SeqOvl",
|
||||
engine: "zapret2",
|
||||
args: "--lua-desync=multisplit:pos=1,midsld,sniext+2:seqovl=2:fooling=badseq",
|
||||
description: "High-entropy triple fragmentation for heavily filtered regions."
|
||||
},
|
||||
{
|
||||
id: "z2_split2_pos1",
|
||||
name: "Classic Split2 (pos=1)",
|
||||
engine: "zapret2",
|
||||
args: "--lua-desync=split2:pos=1:fooling=badseq",
|
||||
description: "Standard 2-fragment desync for compatibility."
|
||||
},
|
||||
{
|
||||
id: "z2_discord_udp",
|
||||
name: "Discord Voice UDP Desync",
|
||||
engine: "zapret2",
|
||||
args: "--filter-udp=50000-65535 --lua-desync=fake:ttl=8",
|
||||
description: "UDP fake packet desync for Discord RTC and Voice channels."
|
||||
}
|
||||
];
|
||||
|
||||
const STRATEGIES_ZAPRET = [
|
||||
{
|
||||
id: "z1_split2_pos2",
|
||||
name: "Standard Split2 (pos=2)",
|
||||
engine: "zapret",
|
||||
args: "--dpi-desync=split2 --dpi-desync-split-pos=2",
|
||||
description: "Base HTTP/TLS split inside SNI header."
|
||||
},
|
||||
{
|
||||
id: "z1_disorder2_badseq",
|
||||
name: "Disorder2 + BadSeq",
|
||||
engine: "zapret",
|
||||
args: "--dpi-desync=disorder2 --dpi-desync-split-pos=1 --dpi-desync-fooling=badseq",
|
||||
description: "Sends out-of-order packets with invalid TCP sequence fooling."
|
||||
},
|
||||
{
|
||||
id: "z1_fake_split2_ttl",
|
||||
name: "Fake SNI + Split2 (TTL=8)",
|
||||
engine: "zapret",
|
||||
args: "--dpi-desync=fake,split2 --dpi-desync-split-pos=1 --dpi-desync-ttl=8 --dpi-desync-fooling=badseq",
|
||||
description: "Sends low-TTL fake packet followed by segmented ClientHello."
|
||||
},
|
||||
{
|
||||
id: "z1_seqovl_split2",
|
||||
name: "Sequence Overlap (SeqOvl=1)",
|
||||
engine: "zapret",
|
||||
args: "--dpi-desync=split2 --dpi-desync-split-seqovl=1 --dpi-desync-fooling=badseq",
|
||||
description: "Overlapping TCP payload to confuse stateful DPI."
|
||||
},
|
||||
{
|
||||
id: "z1_md5sig_disorder",
|
||||
name: "MD5Sig Fooling + Disorder",
|
||||
engine: "zapret",
|
||||
args: "--dpi-desync=fake,disorder2 --dpi-desync-fooling=md5sig --dpi-desync-ttl=6",
|
||||
description: "Injects TCP MD5 signature option to trigger DPI packet drop."
|
||||
}
|
||||
];
|
||||
|
||||
const STRATEGIES_BYEDPI = [
|
||||
{
|
||||
id: "bd_auto_tr_d2",
|
||||
name: "ByeDPI Auto (t,r,a,s)",
|
||||
engine: "byedpi",
|
||||
args: "-o 2 --auto=t,r,a,s -d 2",
|
||||
description: "Standard adaptive ByeDPI auto-mode with disorder."
|
||||
},
|
||||
{
|
||||
id: "bd_disorder_fake_ttl8",
|
||||
name: "Disorder + Fake (TTL=8)",
|
||||
engine: "byedpi",
|
||||
args: "--split 1 --disorder 1 --fake -1 --ttl 8",
|
||||
description: "1-byte split with reverse disorder and fake handshake packet."
|
||||
},
|
||||
{
|
||||
id: "bd_midsld_fake_frag",
|
||||
name: "SNI Extension + Fake (TTL=6)",
|
||||
engine: "byedpi",
|
||||
args: "-s 1+sniext -f -1 -t 6",
|
||||
description: "SNI extension split with low-TTL fake payload."
|
||||
},
|
||||
{
|
||||
id: "bd_tls_sni_split2",
|
||||
name: "TLS SNI Split (pos=2)",
|
||||
engine: "byedpi",
|
||||
args: "--split 2 --disorder 2",
|
||||
description: "Direct TLS SNI offset split with out-of-order delivery."
|
||||
},
|
||||
{
|
||||
id: "bd_low_ttl_aggressive",
|
||||
name: "Aggressive Fake (TTL=4)",
|
||||
engine: "byedpi",
|
||||
args: "--fake -1 --ttl 4 --disorder 1 --split 1",
|
||||
description: "Low-TTL aggressive fake packet injection."
|
||||
}
|
||||
];
|
||||
|
||||
function get_strategies_for_engine(engine) {
|
||||
engine = lc(as_string(engine));
|
||||
if (engine == "zapret2")
|
||||
return STRATEGIES_ZAPRET2;
|
||||
if (engine == "zapret")
|
||||
return STRATEGIES_ZAPRET;
|
||||
if (engine == "byedpi")
|
||||
return STRATEGIES_BYEDPI;
|
||||
|
||||
// Default or 'all': combine all
|
||||
let all = [];
|
||||
for (let s in STRATEGIES_ZAPRET2) push(all, s);
|
||||
for (let s in STRATEGIES_ZAPRET) push(all, s);
|
||||
for (let s in STRATEGIES_BYEDPI) push(all, s);
|
||||
return all;
|
||||
}
|
||||
|
||||
function resolve_target_url(target, custom_url) {
|
||||
if (custom_url && trim(as_string(custom_url)) != "")
|
||||
return trim(as_string(custom_url));
|
||||
|
||||
target = lc(as_string(target));
|
||||
if (TARGET_URLS[target])
|
||||
return TARGET_URLS[target];
|
||||
|
||||
return TARGET_URLS.youtube;
|
||||
}
|
||||
|
||||
function ensure_state_dir() {
|
||||
common.ensure_dir(STATE_DIR);
|
||||
}
|
||||
|
||||
function save_fuzzer_state(state) {
|
||||
ensure_state_dir();
|
||||
common.write_json_file(STATE_FILE, state);
|
||||
}
|
||||
|
||||
function get_fuzzer_state() {
|
||||
let state = common.read_json_file(STATE_FILE);
|
||||
if (!state || type(state) != "object") {
|
||||
return {
|
||||
running: false,
|
||||
job_id: null,
|
||||
engine: "zapret2",
|
||||
target: "youtube",
|
||||
progress_pct: 0,
|
||||
current_index: 0,
|
||||
total_strategies: 0,
|
||||
current_strategy: null,
|
||||
results: [],
|
||||
best_strategy: null,
|
||||
error: null,
|
||||
started_at: 0,
|
||||
finished_at: 0
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function kill_pid_file(path) {
|
||||
let pid_str = fs.readfile(path);
|
||||
if (pid_str) {
|
||||
let pid = trim(as_string(pid_str));
|
||||
if (pid != "") {
|
||||
system(sprintf("kill %s >/dev/null 2>&1 || kill -9 %s >/dev/null 2>&1", pid, pid));
|
||||
}
|
||||
try { fs.unlink(path); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup_temp_daemons() {
|
||||
kill_pid_file(STATE_DIR + "/fuzzer_byedpi.pid");
|
||||
kill_pid_file(STATE_DIR + "/fuzzer_zapret.pid");
|
||||
kill_pid_file(STATE_DIR + "/fuzzer_zapret2.pid");
|
||||
system("nft delete table inet tachyon_fuzzer 2>/dev/null");
|
||||
}
|
||||
|
||||
function parse_curl_output(output, result) {
|
||||
output = trim(as_string(output));
|
||||
if (output == "") {
|
||||
result.error = "Probe timeout or connection refused";
|
||||
return result;
|
||||
}
|
||||
|
||||
let parts = split(output, "\t");
|
||||
if (length(parts) < 4) {
|
||||
result.error = "Malformed probe metrics output";
|
||||
return result;
|
||||
}
|
||||
|
||||
let http_code = int(parts[0]);
|
||||
let appconnect = double(parts[1]);
|
||||
let starttransfer = double(parts[2]);
|
||||
let speed_bytes = double(parts[3]);
|
||||
|
||||
result.http_code = http_code;
|
||||
result.handshake_ms = int(appconnect * 1000.0);
|
||||
result.ttfb_ms = int(starttransfer * 1000.0);
|
||||
result.speed_kbps = int(speed_bytes / 1024.0);
|
||||
|
||||
if (http_code >= 200 && http_code < 400) {
|
||||
result.success = true;
|
||||
let speed_factor = (result.speed_kbps / 200.0) + 1.0;
|
||||
let ttfb_factor = 1000.0 / (result.ttfb_ms + 1.0);
|
||||
result.score = int(ttfb_factor * speed_factor);
|
||||
} else {
|
||||
result.success = false;
|
||||
result.score = 0;
|
||||
result.error = sprintf("HTTP Status %d", http_code);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function run_probe(engine, args_str, target_url) {
|
||||
cleanup_temp_daemons();
|
||||
|
||||
let result = {
|
||||
success: false,
|
||||
http_code: 0,
|
||||
handshake_ms: 0,
|
||||
ttfb_ms: 0,
|
||||
speed_kbps: 0,
|
||||
score: 0,
|
||||
error: ""
|
||||
};
|
||||
|
||||
engine = lc(as_string(engine));
|
||||
|
||||
if (engine == "byedpi") {
|
||||
let bin = get_byedpi_bin();
|
||||
if (!bin) {
|
||||
result.error = "ByeDPI binary not found";
|
||||
return result;
|
||||
}
|
||||
|
||||
let pid_path = STATE_DIR + "/fuzzer_byedpi.pid";
|
||||
let spawn_cmd = sprintf("%s -i 127.0.0.1 -p %d %s", bin, BYEDPI_PORT, args_str);
|
||||
system(common.background_command_with_pid(spawn_cmd, ">/dev/null", ">" + shell_quote(pid_path)));
|
||||
system("sleep 0.15");
|
||||
|
||||
let curl_cmd = sprintf(
|
||||
"curl -x socks5h://127.0.0.1:%d -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' --connect-timeout 4 --max-time 6 %s 2>/dev/null",
|
||||
BYEDPI_PORT,
|
||||
shell_quote(target_url)
|
||||
);
|
||||
|
||||
let pipe = fs.popen(curl_cmd, "r");
|
||||
let output = pipe ? pipe.read("all") : "";
|
||||
if (pipe) pipe.close();
|
||||
|
||||
cleanup_temp_daemons();
|
||||
|
||||
return parse_curl_output(output, result);
|
||||
}
|
||||
|
||||
if (engine == "zapret" || engine == "zapret2") {
|
||||
let is_z2 = engine == "zapret2";
|
||||
let bin = is_z2 ? get_zapret2_bin() : get_zapret_bin();
|
||||
let qnum = is_z2 ? NFQUEUE_QNUM_ZAPRET2 : NFQUEUE_QNUM_ZAPRET;
|
||||
let pid_path = is_z2 ? (STATE_DIR + "/fuzzer_zapret2.pid") : (STATE_DIR + "/fuzzer_zapret.pid");
|
||||
|
||||
if (!bin) {
|
||||
result.error = (is_z2 ? "Zapret v2" : "Zapret v1") + " binary not found";
|
||||
return result;
|
||||
}
|
||||
|
||||
let lua_init_flags = "";
|
||||
if (is_z2 && fs.stat("/opt/zapret2/lua/zapret-lib.lua") != null && index(args_str, "--lua-init") < 0) {
|
||||
lua_init_flags = "--lua-init=@/opt/zapret2/lua/zapret-lib.lua --lua-init=@/opt/zapret2/lua/zapret-antidpi.lua --lua-init=@/opt/zapret2/lua/zapret-auto.lua ";
|
||||
}
|
||||
|
||||
let spawn_cmd = sprintf("%s --qnum=%d --fwmark=%s %s%s --pidfile=%s --daemon", bin, qnum, FUZZER_FWMARK, lua_init_flags, args_str, pid_path);
|
||||
system(common.background_command(spawn_cmd));
|
||||
system("sleep 0.15");
|
||||
|
||||
// Setup temporary isolated nftables queue for probe
|
||||
system("nft add table inet tachyon_fuzzer 2>/dev/null");
|
||||
system("nft 'add chain inet tachyon_fuzzer output { type filter hook output priority -150 ; }' 2>/dev/null");
|
||||
system(sprintf("nft add rule inet tachyon_fuzzer output meta mark %s counter return 2>/dev/null", FUZZER_FWMARK));
|
||||
system(sprintf("nft 'add rule inet tachyon_fuzzer output meta l4proto tcp tcp dport { 80, 443 } counter queue num %d bypass' 2>/dev/null", qnum));
|
||||
|
||||
// Execute probe with interface-mark if supported, or standard direct curl
|
||||
let curl_cmd = sprintf(
|
||||
"curl -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' --connect-timeout 4 --max-time 6 %s 2>/dev/null",
|
||||
shell_quote(target_url)
|
||||
);
|
||||
|
||||
let pipe = fs.popen(curl_cmd, "r");
|
||||
let output = pipe ? pipe.read("all") : "";
|
||||
if (pipe) pipe.close();
|
||||
|
||||
cleanup_temp_daemons();
|
||||
|
||||
return parse_curl_output(output, result);
|
||||
}
|
||||
|
||||
result.error = "Unknown engine: " + engine;
|
||||
return result;
|
||||
}
|
||||
|
||||
function run_fuzzer_worker(engine, target, custom_url, rule_section) {
|
||||
let target_url = resolve_target_url(target, custom_url);
|
||||
let strategies = get_strategies_for_engine(engine);
|
||||
let total = length(strategies);
|
||||
|
||||
let state = {
|
||||
running: true,
|
||||
job_id: sprintf("fuzz_%d", clock()[0]),
|
||||
engine,
|
||||
target,
|
||||
target_url,
|
||||
rule_section: as_string(rule_section),
|
||||
progress_pct: 0,
|
||||
current_index: 0,
|
||||
total_strategies: total,
|
||||
current_strategy: null,
|
||||
results: [],
|
||||
best_strategy: null,
|
||||
error: null,
|
||||
started_at: clock()[0],
|
||||
finished_at: 0
|
||||
};
|
||||
|
||||
save_fuzzer_state(state);
|
||||
|
||||
try {
|
||||
let highest_score = -1;
|
||||
let best = null;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
let strat = strategies[i];
|
||||
state.current_index = i + 1;
|
||||
state.current_strategy = strat;
|
||||
state.progress_pct = int(((i) / double(total)) * 100.0);
|
||||
save_fuzzer_state(state);
|
||||
|
||||
let probe = run_probe(strat.engine, strat.args, target_url);
|
||||
|
||||
let item_result = {
|
||||
id: strat.id,
|
||||
name: strat.name,
|
||||
engine: strat.engine,
|
||||
args: strat.args,
|
||||
description: strat.description,
|
||||
success: probe.success,
|
||||
http_code: probe.http_code,
|
||||
handshake_ms: probe.handshake_ms,
|
||||
ttfb_ms: probe.ttfb_ms,
|
||||
speed_kbps: probe.speed_kbps,
|
||||
score: probe.score,
|
||||
error: probe.error,
|
||||
badge: ""
|
||||
};
|
||||
|
||||
if (item_result.score > highest_score && item_result.success) {
|
||||
highest_score = item_result.score;
|
||||
best = item_result;
|
||||
}
|
||||
|
||||
push(state.results, item_result);
|
||||
state.progress_pct = int(((i + 1) / double(total)) * 100.0);
|
||||
save_fuzzer_state(state);
|
||||
}
|
||||
|
||||
// Assign badges
|
||||
if (best) {
|
||||
best.badge = "🏆 Best Match";
|
||||
state.best_strategy = best;
|
||||
}
|
||||
|
||||
// Mark fastest and most stable
|
||||
let min_ttfb = 999999;
|
||||
let fastest = null;
|
||||
for (let r in state.results) {
|
||||
if (r.success && r.ttfb_ms > 0 && r.ttfb_ms < min_ttfb) {
|
||||
min_ttfb = r.ttfb_ms;
|
||||
fastest = r;
|
||||
}
|
||||
}
|
||||
if (fastest && fastest.id != (best ? best.id : "")) {
|
||||
fastest.badge = "⚡ Ultra Fast";
|
||||
}
|
||||
|
||||
state.running = false;
|
||||
state.current_strategy = null;
|
||||
state.finished_at = clock()[0];
|
||||
save_fuzzer_state(state);
|
||||
} catch (err) {
|
||||
state.running = false;
|
||||
state.error = as_string(err);
|
||||
state.finished_at = clock()[0];
|
||||
save_fuzzer_state(state);
|
||||
}
|
||||
|
||||
cleanup_temp_daemons();
|
||||
}
|
||||
|
||||
function start_fuzzer(engine, target, custom_url, rule_section) {
|
||||
let current = get_fuzzer_state();
|
||||
if (current.running) {
|
||||
print(sprintf("%J\n", { success: false, error: "Fuzzer is already running", job_id: current.job_id }));
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_state_dir();
|
||||
|
||||
let job_id = sprintf("fuzz_%d", clock()[0]);
|
||||
let cmd = sprintf(
|
||||
"ucode -L /usr/lib/tachyon /usr/lib/tachyon/diagnostics/fuzzer.uc worker %s %s %s %s",
|
||||
shell_quote(engine || "zapret2"),
|
||||
shell_quote(target || "youtube"),
|
||||
shell_quote(custom_url || ""),
|
||||
shell_quote(rule_section || "")
|
||||
);
|
||||
|
||||
system(common.background_command_with_pid(cmd, ">/dev/null", ">" + shell_quote(PID_FILE)));
|
||||
|
||||
print(sprintf("%J\n", { success: true, job_id, engine: engine || "zapret2", target: target || "youtube" }));
|
||||
}
|
||||
|
||||
function stop_fuzzer() {
|
||||
kill_pid_file(PID_FILE);
|
||||
cleanup_temp_daemons();
|
||||
|
||||
let state = get_fuzzer_state();
|
||||
state.running = false;
|
||||
state.error = "Stopped by user";
|
||||
state.finished_at = clock()[0];
|
||||
save_fuzzer_state(state);
|
||||
|
||||
print(sprintf("%J\n", { success: true, message: "Fuzzer stopped" }));
|
||||
}
|
||||
|
||||
function get_available_engines() {
|
||||
return {
|
||||
zapret2: get_zapret2_bin() != null,
|
||||
zapret: get_zapret_bin() != null,
|
||||
byedpi: get_byedpi_bin() != null
|
||||
};
|
||||
}
|
||||
|
||||
function apply_strategy(engine, args_val, target_rule) {
|
||||
engine = lc(as_string(engine));
|
||||
args_val = trim(as_string(args_val));
|
||||
target_rule = trim(as_string(target_rule));
|
||||
|
||||
if (args_val == "") {
|
||||
print(sprintf("%J\n", { success: false, error: "Empty strategy arguments" }));
|
||||
return;
|
||||
}
|
||||
|
||||
let uci = uci_core.cursor();
|
||||
let applied = false;
|
||||
|
||||
// If target rule section is specified, update that rule
|
||||
if (target_rule != "" && target_rule != "global") {
|
||||
uci.set(CONFIG_NAME, target_rule, "action", engine);
|
||||
if (engine == "zapret2")
|
||||
uci.set(CONFIG_NAME, target_rule, "nfqws2_opt", args_val);
|
||||
else if (engine == "zapret")
|
||||
uci.set(CONFIG_NAME, target_rule, "nfqws_opt", args_val);
|
||||
else if (engine == "byedpi")
|
||||
uci.set(CONFIG_NAME, target_rule, "byedpi_cmd_opts", args_val);
|
||||
applied = true;
|
||||
} else {
|
||||
// Apply as provider global setting
|
||||
let provider_sec = engine;
|
||||
let sec_obj = uci.get_all(CONFIG_NAME, provider_sec);
|
||||
if (sec_obj == null) {
|
||||
uci.set(CONFIG_NAME, provider_sec, "provider");
|
||||
}
|
||||
uci.set(CONFIG_NAME, provider_sec, "enabled", "1");
|
||||
if (engine == "zapret2")
|
||||
uci.set(CONFIG_NAME, provider_sec, "nfqws2_opt", args_val);
|
||||
else if (engine == "zapret")
|
||||
uci.set(CONFIG_NAME, provider_sec, "nfqws_opt", args_val);
|
||||
else if (engine == "byedpi")
|
||||
uci.set(CONFIG_NAME, provider_sec, "byedpi_cmd_opts", args_val);
|
||||
applied = true;
|
||||
}
|
||||
|
||||
uci.commit(CONFIG_NAME);
|
||||
|
||||
// Mark reload pending
|
||||
system(common.background_command("tachyon reload"));
|
||||
|
||||
print(sprintf("%J\n", {
|
||||
success: true,
|
||||
engine,
|
||||
applied_to: target_rule != "" ? target_rule : "global",
|
||||
args: args_val
|
||||
}));
|
||||
}
|
||||
|
||||
// CLI Dispatcher
|
||||
let op = ARGV[0] || "status";
|
||||
|
||||
if (op == "start") {
|
||||
start_fuzzer(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||||
} else if (op == "worker") {
|
||||
run_fuzzer_worker(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||||
} else if (op == "status") {
|
||||
print(sprintf("%J\n", get_fuzzer_state()));
|
||||
} else if (op == "stop") {
|
||||
stop_fuzzer();
|
||||
} else if (op == "apply") {
|
||||
apply_strategy(ARGV[1], ARGV[2], ARGV[3]);
|
||||
} else if (op == "strategies") {
|
||||
print(sprintf("%J\n", {
|
||||
available_engines: get_available_engines(),
|
||||
zapret2: STRATEGIES_ZAPRET2,
|
||||
zapret: STRATEGIES_ZAPRET,
|
||||
byedpi: STRATEGIES_BYEDPI
|
||||
}));
|
||||
} else {
|
||||
warn("Usage: fuzzer.uc [start|status|stop|apply|strategies|worker] ...\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ let as_string = common.as_string;
|
||||
let read_stdin = common.read_stdin;
|
||||
let read_stdin_json = common.read_stdin_json;
|
||||
let read_json_file = common.read_json_file;
|
||||
let write_json_file = common.write_json_file;
|
||||
|
||||
function url_encode(value) {
|
||||
value = as_string(value);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
FUZZER="$ROOT_DIR/tachyon/files/usr/lib/diagnostics/fuzzer.uc"
|
||||
BYEDPI_VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/byedpi/validator.uc"
|
||||
ZAPRET_VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/zapret/validator.uc"
|
||||
ZAPRET2_VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/zapret2/validator.uc"
|
||||
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1. Check strategies JSON output
|
||||
strategies_json="$(ucode -L "$TACHYON_LIB" -- "$FUZZER" strategies)"
|
||||
[ -n "$strategies_json" ] || fail "fuzzer strategies returned empty output"
|
||||
|
||||
# Validate JSON structure using node
|
||||
JSON_VALUE="$strategies_json" node <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
if (typeof val.available_engines !== 'object') {
|
||||
console.error("Missing available_engines");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!Array.isArray(val.zapret2) || val.zapret2.length === 0) {
|
||||
console.error("Missing zapret2 strategies");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!Array.isArray(val.zapret) || val.zapret.length === 0) {
|
||||
console.error("Missing zapret strategies");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!Array.isArray(val.byedpi) || val.byedpi.length === 0) {
|
||||
console.error("Missing byedpi strategies");
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
# 2. Check each strategy passes its respective engine validator
|
||||
byedpi_args="$(JSON_VALUE="$strategies_json" node - <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
for (const s of val.byedpi) {
|
||||
console.log(s.args);
|
||||
}
|
||||
NODE
|
||||
)"
|
||||
|
||||
while IFS= read -r args; do
|
||||
[ -n "$args" ] || continue
|
||||
check="$(ucode -L "$TACHYON_LIB" -- "$BYEDPI_VALIDATOR" validate-json "$args")"
|
||||
JSON_VALUE="$check" node - <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
if (!val.valid) {
|
||||
console.error("ByeDPI strategy invalid:", val);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
done <<< "$byedpi_args"
|
||||
|
||||
zapret_args="$(JSON_VALUE="$strategies_json" node - <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
for (const s of val.zapret) {
|
||||
console.log(s.args);
|
||||
}
|
||||
NODE
|
||||
)"
|
||||
|
||||
while IFS= read -r args; do
|
||||
[ -n "$args" ] || continue
|
||||
check="$(ucode -L "$TACHYON_LIB" -- "$ZAPRET_VALIDATOR" validate-json nfqws "$args")"
|
||||
JSON_VALUE="$check" node - <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
if (!val.valid) {
|
||||
console.error("Zapret strategy invalid:", val);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
done <<< "$zapret_args"
|
||||
|
||||
zapret2_args="$(JSON_VALUE="$strategies_json" node - <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
for (const s of val.zapret2) {
|
||||
console.log(s.args);
|
||||
}
|
||||
NODE
|
||||
)"
|
||||
|
||||
while IFS= read -r args; do
|
||||
[ -n "$args" ] || continue
|
||||
check="$(ucode -L "$TACHYON_LIB" -- "$ZAPRET2_VALIDATOR" validate-json nfqws2 "$args")"
|
||||
JSON_VALUE="$check" node - <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
if (!val.valid) {
|
||||
console.error("Zapret2 strategy invalid:", val);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
done <<< "$zapret2_args"
|
||||
|
||||
# 3. Check status returns clean default state
|
||||
status_json="$(ucode -L "$TACHYON_LIB" -- "$FUZZER" status)"
|
||||
JSON_VALUE="$status_json" node <<'NODE'
|
||||
const val = JSON.parse(process.env.JSON_VALUE);
|
||||
if (val.running !== false) {
|
||||
console.error("Default fuzzer status should have running: false");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!Array.isArray(val.results)) {
|
||||
console.error("Results should be an array");
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
# 4. Check tachyon CLI command map contains fuzzer commands
|
||||
grep -q 'fuzzer_start:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_start"
|
||||
grep -q 'fuzzer_status:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_status"
|
||||
grep -q 'fuzzer_stop:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_stop"
|
||||
grep -q 'fuzzer_apply:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_apply"
|
||||
grep -q 'fuzzer_strategies:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_strategies"
|
||||
|
||||
printf 'PASS: fuzzer_strategy_cli\n'
|
||||
Reference in New Issue
Block a user