Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b467baba8 | |||
| 40d2be72db | |||
| ce3c917f8a | |||
| a70339bf32 | |||
| da72c64b50 | |||
| 883811bd55 | |||
| ba75930510 | |||
| f63e0b9fd9 | |||
| 0ac0a36598 | |||
| 996eb7ab29 | |||
| 8e40c49fa4 | |||
| a7a9f720e1 | |||
| a062f41a2e |
151
README.md
151
README.md
@ -6,7 +6,7 @@
|
||||
<img src="./docs/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://github.com/yandexru45/netshift/releases">
|
||||
<a href="https://uralgit.ru/ural/netshift/releases">
|
||||
<img src="https://img.shields.io/github/release/yandexru45/netshift/all.svg">
|
||||
</a>
|
||||
</p>
|
||||
@ -32,10 +32,14 @@
|
||||
|
||||
## Функции
|
||||
|
||||
- [x] **Маршрутизация по доменам и подсетям** - нужное в туннель, остальное напрямую<br><sub>VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки</sub>
|
||||
- [x] **Subscription URL** - ссылки подписки от провайдера с автообновлением и автовыбором лучшего сервера<br><sub>любая подписка remnawave · 3x-ui · marzban · github</sub>
|
||||
- [x] **Переключаемое ядро sing-box** - стабильное ↔ sing-box-extended прямо из веб-интерфейса<br><sub>клиентский транспорт xhttp · установка и откат в один клик</sub>
|
||||
- [x] **Веб-интерфейс LuCI** - дашборд, диагностика и настройки без ручной правки конфигов<br><sub>статус серверов · проверка соединения · логи</sub>
|
||||
- [x] **Маршрутизация по доменам и подсетям** - нужное в туннель, остальное напрямую<br><sub>VLESS · Shadowsocks · Trojan · Hysteria2 · VMess · SOCKS · готовые community-списки</sub>
|
||||
- [x] **Subscription URL** - ссылки подписки от провайдера с автообновлением и автовыбором лучшего сервера<br><sub>любая подписка remnawave · 3x-ui · marzban · github · форматы base64 / URI / Clash / Xray JSON</sub>
|
||||
- [x] **Несколько подписок и фильтры** - несколько фидов в одной секции, фильтр серверов по ключевым словам (include / exclude)<br><sub>объединение без дублей · регистронезависимо · работает и по эмодзи</sub>
|
||||
- [x] **Группировка серверов** - по флагу страны или по префиксу имени, с авто-выбором «⚡ Самый быстрый» среди всех групп<br><sub>URLTest внутри группы · URLTest над группами · ручной выбор сохранён</sub>
|
||||
- [x] **Переключаемое ядро sing-box** - стабильное ↔ sing-box-extended прямо из веб-интерфейса<br><sub>клиентский транспорт xhttp · самовосстановление и автооткат · установка в один клик</sub>
|
||||
- [x] **Самообновление из веб-интерфейса** - проверка и установка обновлений NetShift прямо из LuCI<br><sub>асинхронно · бэкап конфига · без риска «окирпичивания»</sub>
|
||||
- [x] **Веб-интерфейс LuCI** - дашборд, менеджер компонентов, диагностика и настройки без ручной правки конфигов<br><sub>статус серверов · проверка соединения · логи · вкладки-карточки</sub>
|
||||
- [x] **IPv6, блокировка DoH, глобальный прокси** - полная маршрутизация v6 через туннель, защита DNS роутера, режим «весь трафик в туннель»<br><sub>v6 tproxy / DNS / FakeIP · DNS через прокси · фоновый watchdog sing-box</sub>
|
||||
- [x] **Автоматическая миграция** - обновление со старого podkop переносит конфиг без перенастройки
|
||||
|
||||
|
||||
@ -54,8 +58,9 @@
|
||||
<details open>
|
||||
<summary><b>Системные требования</b></summary>
|
||||
|
||||
- OpenWrt **24.10** или выше.
|
||||
- OpenWrt **24.10** или выше (поддерживаются и сборки на `opkg`/`.ipk`, и новые на `apk`/`.apk` - OpenWrt 25.12+).
|
||||
- Минимум **25 МБ** свободного места. Устройства с флеш-памятью 16 МБ не поддерживаются.
|
||||
- На устройстве: `sing-box >= 1.12.0`, `jq >= 1.7.1`, `coreutils-base64 >= 9.7` (ставятся как зависимости пакета).
|
||||
|
||||
</details>
|
||||
|
||||
@ -95,7 +100,7 @@
|
||||
|
||||
```sh
|
||||
mv /etc/config/netshift /etc/config/netshift-070
|
||||
wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/netshift/files/etc/config/netshift
|
||||
wget -O /etc/config/netshift https://uralgit.ru/ural/netshift/raw/branch/main/netshift/files/etc/config/netshift
|
||||
# затем настроить заново через LuCI или UCI
|
||||
```
|
||||
|
||||
@ -108,19 +113,34 @@ wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/netshi
|
||||
Для установки и обновления достаточно одного скрипта:
|
||||
|
||||
```sh
|
||||
sh <(wget -O - https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/install.sh)
|
||||
sh <(wget -O - https://uralgit.ru/ural/netshift/raw/branch/main/install.sh)
|
||||
```
|
||||
|
||||
Интерфейс появится в LuCI: **Services → NetShift**.
|
||||
|
||||
<details>
|
||||
<summary><b>Готовые community-списки</b></summary>
|
||||
|
||||
Готовые наборы доменов/подсетей, которые можно добавить в секцию через `community_lists` (в UI - чекбоксами). Списки обновляются автоматически:
|
||||
|
||||
`russia_inside` · `russia_outside` · `ukraine_inside` · `geoblock` · `block` · `porn` · `news` · `anime` · `youtube` · `hdrezka` · `tiktok` · `google_ai` · `google_play` · `hodca` · `discord` · `meta` · `twitter` · `cloudflare` · `cloudfront` · `digitalocean` · `hetzner` · `ovh` · `telegram` · `roblox`
|
||||
|
||||
```sh
|
||||
uci add_list netshift.my_sub.community_lists='youtube'
|
||||
uci add_list netshift.my_sub.community_lists='telegram'
|
||||
uci commit netshift
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Настройка подписки (Subscription URL) через UCI</b></summary>
|
||||
|
||||
При скачивании подписки отправляются заголовки:
|
||||
Поддерживаются любые подписки (remnawave · 3x-ui · marzban · github) в форматах **base64 · список URI · Clash · Xray JSON**, в т.ч. **gzip-сжатые** ответы. При скачивании подписки отправляются заголовки:
|
||||
|
||||
| Заголовок | Значение |
|
||||
|---|---|
|
||||
| `User-Agent` | `singbox/<версия>` |
|
||||
| `User-Agent` | подбирается автоматически (`singbox/<версия>` или клиентский, см. формат) |
|
||||
| `X-HWID` | уникальный идентификатор роутера |
|
||||
| `X-Device-OS` | `OpenWrt Linux` |
|
||||
| `X-Device-Model` | модель роутера |
|
||||
@ -136,26 +156,113 @@ uci add_list netshift.my_sub.community_lists='russia_inside'
|
||||
uci commit netshift
|
||||
```
|
||||
|
||||
Ручное обновление подписки:
|
||||
**Несколько подписок** в одной секции - добавьте `subscription_url` списком (в UI - поле с «+»); все фиды скачиваются и объединяются в один набор узлов без дублей:
|
||||
|
||||
```sh
|
||||
/usr/bin/netshift subscription_update
|
||||
uci add_list netshift.my_sub.subscription_url='https://provider-a.com/sub'
|
||||
uci add_list netshift.my_sub.subscription_url='https://provider-b.com/sub'
|
||||
```
|
||||
|
||||
**Фильтр серверов** по ключевым словам - белый/чёрный список (регистр не важен, работает и по эмодзи):
|
||||
|
||||
```sh
|
||||
uci add_list netshift.my_sub.subscription_filter_include='🇩🇪'
|
||||
uci add_list netshift.my_sub.subscription_filter_exclude='trial'
|
||||
```
|
||||
|
||||
**Группировка серверов** - собирает узлы в URLTest-группы и добавляет авто-выбор «⚡ Самый быстрый» среди всех групп (при ≥2 группах он же выбор по умолчанию; ручной выбор группы сохраняется):
|
||||
|
||||
```sh
|
||||
# off | country (по флагу страны) | prefix (по первым N символам имени)
|
||||
uci set netshift.my_sub.subscription_group_mode='country'
|
||||
# для prefix: сколько первых символов имени брать (по умолчанию 2)
|
||||
uci set netshift.my_sub.subscription_group_prefix_len='2'
|
||||
```
|
||||
|
||||
**Предпочтительный формат** - для панелей, которые отдают нужные узлы (например xhttp / Hysteria2) только под определённым клиентом:
|
||||
|
||||
```sh
|
||||
# auto | xray (Xray JSON, UA как у Happ) | singbox
|
||||
uci set netshift.my_sub.subscription_format_preference='auto'
|
||||
```
|
||||
|
||||
**Подписки по IP-хосту и «кривой» HTTPS** - можно указать подписку с IP вместо домена (например `https://22.23.43.52:2096/sub/xxxx`); для панелей с самоподписанным / несовпадающим сертификатом включите небезопасный TLS:
|
||||
|
||||
```sh
|
||||
uci set netshift.my_sub.subscription_allow_insecure='1'
|
||||
```
|
||||
|
||||
Ручное обновление подписки и очистка кеша:
|
||||
|
||||
```sh
|
||||
/usr/bin/netshift subscription_update # перечитать и применить
|
||||
# Очистка кеша всех подписок и повторное скачивание - кнопка во вкладке «Диагностика»
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Ядро sing-box-extended (xhttp)</b></summary>
|
||||
<summary><b>Менеджер компонентов: ядро sing-box-extended (xhttp) и самообновление</b></summary>
|
||||
|
||||
Переключение ядра между стабильным sing-box и сборкой **sing-box-extended** прямо из вкладки **Diagnostics** в LuCI:
|
||||
Вкладка **Менеджер компонентов** в LuCI управляет NetShift и ядром sing-box в одном месте - три карточки: **NetShift** / **sing-box (stock)** / **sing-box (extended)**. Установленная версия видна сразу, статус (актуально / устарело / не установлено) и кнопка «Проверить обновление» - по нажатию.
|
||||
|
||||
- **Install extended** - установить расширенное ядро sing-box-extended.
|
||||
**Переключение ядра** между стабильным sing-box и сборкой **sing-box-extended**:
|
||||
|
||||
- **Install extended** - расширенное ядро (даёт клиентский транспорт **xhttp**, только клиентский режим). Также поддерживается **VMess**.
|
||||
- **Install stable** - вернуться на стабильное ядро.
|
||||
|
||||
После установки расширенного ядра становится доступен клиентский транспорт **xhttp** (только клиентский режим, не серверный). По умолчанию ставится стабильное ядро - extended включается по желанию.
|
||||
Смена ядра безопасна: перед переключением проверяется и при необходимости чинится связь, делается бэкап; при сбое - **автооткат**, роутер никогда не остаётся без рабочего ядра. По умолчанию стоит стабильное - extended включается по желанию.
|
||||
|
||||
**Самообновление NetShift** - кнопка обновления прямо из веб-интерфейса: асинхронно, с бэкапом конфига, проверкой фактической версии после установки и без риска «окирпичивания». Русская локализация обновляется только если уже установлена.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Дополнительные настройки (IPv6, блокировка DoH, глобальный прокси, DNS через прокси)</b></summary>
|
||||
|
||||
Все опции - в секции `settings` (`0` - выкл, `1` - вкл):
|
||||
|
||||
```sh
|
||||
# Полная маршрутизация IPv6 через туннель (v6 tproxy / DNS / FakeIP). По умолчанию выкл.
|
||||
uci set netshift.settings.enable_ipv6='1'
|
||||
|
||||
# Блокировка DoH: клиенты в сети не обойдут DNS роутера через DNS-over-HTTPS
|
||||
# (режет известные DoH-эндпоинты IPv4 + IPv6 на уровне маршрутов sing-box).
|
||||
uci set netshift.settings.block_doh='1'
|
||||
|
||||
# Глобальный прокси: ВЕСЬ трафик через выбранный outbound (а не только избранное).
|
||||
# Только при явном включении - иначе действует выборочная маршрутизация.
|
||||
uci set netshift.settings.global_proxy='1'
|
||||
|
||||
# DNS через прокси (detour): DNS-запросы идут через туннель.
|
||||
uci set netshift.settings.dns_via_outbound='1'
|
||||
|
||||
# Блокировать QUIC (заставляет приложения откатываться на TCP/TLS).
|
||||
uci set netshift.settings.disable_quic='1'
|
||||
|
||||
uci commit netshift
|
||||
```
|
||||
|
||||
> По умолчанию NetShift гонит в sing-box **только** проксируемые подсети/домены, остальное - напрямую (выборочная маркировка). Режим «весь трафик в туннель» включается **только** опцией `global_proxy`.
|
||||
|
||||
</details>
|
||||
|
||||
## История изменений
|
||||
|
||||
Полный список изменений по версиям - на странице [Releases](https://github.com/yandexru45/netshift/releases). Анонсы обновлений публикуются в [Telegram-канале](https://t.me/netshift_news).
|
||||
|
||||
Коротко о крупных вехах:
|
||||
|
||||
| Версия | Главное |
|
||||
|---|---|
|
||||
| **0.9.1** | Авто-выбор «⚡ Самый быстрый» среди групп (URLTest над URLTest'ами) |
|
||||
| **0.9.0** | Меньше ошибок «лимит GitHub API» (обход через redirect-путь github.com); фикс старого `option subscription_url` |
|
||||
| **0.8.9** | Универсальная группировка подписки (страна / префикс имени); поддержка gzip-подписок; фикс ложного «версия устарела» |
|
||||
| **0.8.7-0.8.8** | Критфикс маршрутизации 2-й секции; выборочная маркировка (меньше нагрузки CPU); Hysteria2 + xhttp везде; несколько подписок; надёжное самообновление |
|
||||
| **0.8.6** | IPv6 · блокировка DoH · вкладка «Менеджер компонентов» · самообновление · подписки по IP / небезопасный TLS · глобальный прокси · DNS через прокси · watchdog |
|
||||
| **0.8.5** | VMess (extended) · надёжная смена ядра с автооткатом · фильтр серверов по ключевым словам · Xray JSON + автоподбор User-Agent |
|
||||
| **0.8.0** | Переименование podkop → NetShift с авто-миграцией конфигов; sing-box-extended (xhttp) из веб-интерфейса |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
@ -188,7 +295,7 @@ uci commit netshift
|
||||
|
||||
## Build Artifacts
|
||||
|
||||
Пакеты собираются в Docker-образе OpenWrt SDK (24.10) и публикуются как релиз при push git-тега ([`.github/workflows/build.yml`](.github/workflows/build.yml)).
|
||||
Пакеты собираются в Docker-образах OpenWrt SDK (`.ipk` - 24.10, `.apk` - 25.12) и публикуются как релиз при push git-тега ([`.github/workflows/build.yml`](.github/workflows/build.yml)).
|
||||
|
||||
| Пакет | Формат | Назначение |
|
||||
|---|---|---|
|
||||
@ -199,14 +306,14 @@ uci commit netshift
|
||||
Локальная сборка:
|
||||
|
||||
```sh
|
||||
# ipk (большинство устройств OpenWrt 24.10)
|
||||
docker build -f Dockerfile-ipk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:ipk .
|
||||
# ipk (OpenWrt 24.10, opkg)
|
||||
docker build -f Dockerfile-ipk --build-arg NETSHIFT_VERSION=0.9.1 -t netshift:ipk .
|
||||
|
||||
# apk (новые сборки OpenWrt на apk)
|
||||
docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:apk .
|
||||
# apk (новые сборки OpenWrt 25.12+, apk)
|
||||
docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=0.9.1 -t netshift:apk .
|
||||
```
|
||||
|
||||
> Требуется sing-box >= 1.12.0 и jq >= 1.7.1 на целевом устройстве.
|
||||
> Требуется sing-box >= 1.12.0, jq >= 1.7.1 и coreutils-base64 >= 9.7 на целевом устройстве.
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
@ -1052,3 +1052,400 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure ->
|
||||
["component_action_async","subscription","clear_cache"] in built main.js match;
|
||||
smoke all 166/0 (11 new cc-case all green); 472 vitest; no yarn pollution. All
|
||||
UNCOMMITTED, stacked with tasks 027..038 — operator commits manually.
|
||||
|
||||
## DIAGNOSED: self-update silently no-ops on opkg/ipk routers with a v-prefixed installed build (2026-06-11)
|
||||
|
||||
- USER (main router ssh root@192.168.1.1, OWRT 24.10.5 mediatek/filogic aarch64,
|
||||
OPKG/ipk, Xiaomi AX3000T): NetShift self-update from the web UI reports
|
||||
"updated to 0.8.7" but the installed version stays v0.8.6. Logs show the whole
|
||||
self_update worker succeeding ("installing netshift-0.8.7-r1-all.ipk" ...
|
||||
"NetShift updated to 0.8.7").
|
||||
- ROOT CAUSE (empirically proven on the router): the installed build is the OLD
|
||||
ipk that carried a leading `v` (constants `NETSHIFT_VERSION="v0.8.6"`, opkg
|
||||
pkg version `v0.8.6-r1`). The new release is `0.8.7-r1` (no v, post task-028).
|
||||
`opkg install <file.ipk>` REFUSES to "downgrade": it prints
|
||||
`Not downgrading package ... from v0.8.6-r1 to 0.8.7-r1.` and RETURNS rc=0
|
||||
(NOT an error for opkg). So updates_pkg_install_file (updater.sh:1410,
|
||||
`opkg install "$f" >/dev/null 2>&1`) sees rc=0 -> the worker logs success and
|
||||
emits {"success":true,...} while NOTHING was installed.
|
||||
- WHY opkg thinks it's a downgrade: `opkg compare-versions "v0.8.6-r1" ">>"
|
||||
"0.8.7-r1"` => rc=0 (TRUE). The leading `v` sorts ABOVE the digit, so
|
||||
v0.8.6 > 0.8.7 in opkg's dpkg-style compare. Without the v,
|
||||
`0.8.6-r1 << 0.8.7-r1` => true (correct). This is the packaging.md §3 / task-028
|
||||
fragility realized: routers still running a pre-028 v-build can never self-update
|
||||
to a no-v release.
|
||||
- TWO distinct bugs to fix in updater.sh:
|
||||
1. PRIMARY (silent success): `updates_pkg_install_file` swallows opkg's
|
||||
"Not downgrading" no-op as rc=0. The install path never VERIFIES the
|
||||
post-install version actually changed. FIX directions:
|
||||
(a) opkg path add `--force-downgrade` (and/or `--force-reinstall`) so a
|
||||
v->no-v transition actually installs; AND/OR
|
||||
(b) post-install VERIFY: after install, re-read the installed pkg version
|
||||
and compare to the target; if unchanged, treat as failure (honest JSON
|
||||
{"success":false}) instead of reporting success. Verify-after-install is
|
||||
the robust belt — opkg "already installed"/"not downgrading"/"up to date"
|
||||
all return rc=0, so rc alone is NOT a reliable success signal on opkg.
|
||||
2. CONTRIBUTING: the v-prefix legacy build. The compare in
|
||||
_updates_self_update_netshift_core ALREADY v-strips both sides
|
||||
(`${installed#v}` = `${latest#v}`, :1679) so it correctly decides "need
|
||||
update"; the failure is purely at the opkg install step.
|
||||
- MANUAL FIX applied on the router (recovery, verified): downloaded all 3 ipks
|
||||
from the latest release and `opkg install --force-downgrade <file>` each ->
|
||||
netshift + luci-app-netshift now 0.8.7-r1, constants NETSHIFT_VERSION="0.8.7"
|
||||
(no v), get_system_info netshift_version 0.8.7. Future 0.8.7->0.8.8 upgrades
|
||||
will work normally (both sides no-v). Note: install drops
|
||||
/etc/config/netshift-opkg conffile-diff artifact (harmless; rm'd).
|
||||
- apk SIDE NOTE (not the user's box but same helper): apk uses `-r` for release,
|
||||
treats a dashed UPSTREAM version specially (task-034 landmine #A: equal-version
|
||||
no-overwrite). The same verify-after-install belt would harden apk too. Whatever
|
||||
fix is chosen must be tested on BOTH opkg and apk paths (packaging gate).
|
||||
- LANDMINE for the fix: `updates_pkg_install_file` redirects stdout+stderr to
|
||||
/dev/null, so the "Not downgrading" message is invisible — never rely on opkg
|
||||
text; rely on rc PLUS an explicit post-install version re-check. opkg
|
||||
compare-versions is available on-device for a robust numeric compare if needed
|
||||
(but the installer should not need the leading v at all once verify is added).
|
||||
|
||||
## task-043 Xray subscription UA — versioned UAs (2026-06-11)
|
||||
|
||||
- USER reported: with subscription_format_preference=xray, the panel returns 502
|
||||
and NetShift falls back to singbox/<ver> (sing-box JSON WITHOUT xhttp/hysteria2
|
||||
— the original task-031 complaint). PRIVACY: operator required NO
|
||||
subscription-identifying data anywhere (no panel host/IP/URL/path/query/server/
|
||||
keys) in code/tests/comments/memory — only generic client UA strings are safe.
|
||||
Diagnosis lived in the ephemeral SSH session only; never committed.
|
||||
- ROOT CAUSE (reproduced on a live panel, abstractly): the Xray-probe constant
|
||||
SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES was bare/version-less ("v2rayN Happ").
|
||||
That panel UA-gates its Xray branch on a <client>/<version> shape: a BARE Happ
|
||||
(and v2rayN in any form) → 502 Bad Gateway; a VERSIONED Happ/<x.y.z> → 200 with
|
||||
the wanted Xray-JSON ARRAY (multi-profile). The download path/headers
|
||||
(_wget_subscription_request, helpers.sh:803) are correct & unchanged — only the
|
||||
UA VALUE decides 502-vs-200.
|
||||
- DIAGNOSIS METHOD (reusable, privacy-safe): on the router, replay the exact
|
||||
request per UA with `curl -A "$UA" -H <same X-* headers netshift sends> -w
|
||||
"HTTP %{http_code}"` to isolate UA-vs-headers; the headers were identical for
|
||||
all UAs, so the UA alone is the variable. Sniff the 200 body's FORMAT (first
|
||||
char `[`/`{`, jq top-keys) to confirm it's the Xray-JSON array
|
||||
(dns,inbounds,log,outbounds,remarks,routing per element) — NOT to read values.
|
||||
Clean up /tmp on the router after.
|
||||
- FIX (task-043, APPROVED W/ CONDITIONS, smoke 170/0): constants-only —
|
||||
SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES -> versioned ("Happ/1.0.0 v2rayN/7.0.0
|
||||
v2rayNG/1.9.0", versioned Happ FIRST). The xray-mode probe order in
|
||||
build_subscription_user_agent_candidates (helpers.sh:765-771) front-loads these
|
||||
before default+cached+whitelist, dedup whole-entry, so the working versioned
|
||||
Happ wins first; non-working UAs just fall through. Coupled smoke
|
||||
fb-caseI-xraypref-* updated to DERIVE expected first/second/third from the
|
||||
constant (won't rot) + a generic guard that the first candidate contains "/"
|
||||
(bare-UA regression catch). Operator REJECTED making it a UI option (overkill /
|
||||
system-level) and rejected touching headers / auto-mode list.
|
||||
- DEDUP NOTE: versioned "Happ/1.0.0" is DISTINCT from the bare "Happ" still in the
|
||||
main auto-mode SUBSCRIPTION_USER_AGENT_CANDIDATES whitelist, so neither is
|
||||
dropped by the whole-entry dedup. The bare v2rayN/Happ in the MAIN list still
|
||||
502 on this panel but are only reached AFTER the versioned UAs already win, so
|
||||
no wasted probes in practice. Did NOT change the main list (broader/auto-mode).
|
||||
- The CONDITION (M1) is commit-hygiene only: keep the pre-existing unrelated
|
||||
.opencode/agent/*.md churn (model rename + bash permission) OUT of the task-043
|
||||
commit — not a code issue. Those were already in the tree at session start.
|
||||
|
||||
## task-044/045 Universal subscription grouper (2026-06-12)
|
||||
|
||||
- FEATURE: generalize the country-flag subscription grouper into modes
|
||||
off/country/prefix. Operator wanted "group by first N chars of the remark, N
|
||||
configurable, default 2"; chose Variant A (single mode dropdown off/country/
|
||||
prefix + length, count by Unicode CODEPOINTS), full backend+frontend.
|
||||
- ARCHITECTURE SEAM (reusable insight): the urltest-per-group + main-selector
|
||||
tree builder in bin/netshift `subscription)` branch is mode-agnostic — only the
|
||||
KEY EXTRACTOR + the on/off decision needed generalizing.
|
||||
`sing_box_build_subscription_country_groups` -> `sing_box_build_subscription_
|
||||
groups <tags_json> <mode> <prefix_len>` returning {group_order,groups,ungrouped}.
|
||||
prefix key = `(.|explode)|.[0:$n]|implode` (codepoint slice, Unicode-safe, no
|
||||
Oniguruma). country path kept byte-identical (regional-indicator gate). N=2
|
||||
over a flag tag == the flag, so country is a consistent special case.
|
||||
- LANDMINE CAUGHT IN REVIEW LOOP (architect spotted, not the dev): the inherited
|
||||
group loop `for k in $(jq -r '.group_order[]')` WORD-SPLITS on IFS. Safe for
|
||||
country flags (no spaces) but BREAKS prefix mode whose keys can contain spaces
|
||||
(e.g. tag "A 1" prefix-2 -> key "A "). Fix: mktemp + `while IFS= read -r k ...
|
||||
done < "$tmp"` in the CURRENT shell (NOT `cmd | while read` — that subshell
|
||||
would lose the `config=`/`selector_outbounds_json=` mutations; same landmine
|
||||
class as the smoke `cmd|while read` trap). Mirror the existing
|
||||
`subscription_urls_tmp` mktemp+read<file pattern already in that branch. mktemp
|
||||
failure must fatal+exit 1. ALWAYS audit reused loops when the key domain widens.
|
||||
- prefix_len hardening: jq side `try tonumber catch default | <1 -> default |
|
||||
floor`; shell side `case '' | *[!0-9]* -> default ;; *) [ x -ge 1 ] 2>/dev/null`.
|
||||
short tag (<N) keys by whole self (NOT ungrouped); empty tag -> ungrouped.
|
||||
- LEGACY MIGRATION done BACKEND-side (no JS migration): subscription_group_mode
|
||||
present OUTRANKS legacy; absent -> read subscription_group_by_countries (then
|
||||
alias group_by_countries) truthy->country else off. Frontend just writes the
|
||||
new options. New constant SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN=2.
|
||||
- FRONTEND: form.Flag -> form.ListValue subscription_group_mode (off/country/
|
||||
prefix) + form.Value subscription_group_prefix_len (datatype and(uinteger,
|
||||
min(1)), depends subscription_group_mode:"prefix"); both taboption() in the
|
||||
`subscription` tab (tabbed-CBI completeness). types.ts old key removed (no TS
|
||||
reader). 7 i18n msgids + RU, fe<->luci byte-identical. main.js NO diff
|
||||
(type-only change + hand-written view) — so the rendered tab needs a HUMAN
|
||||
visual check (carried as APPROVED-WITH-CONDITIONS [M1]).
|
||||
- GATES: shellcheck error clean; smoke 170->174/0 (country regression kept +
|
||||
prefix len-2/consistency/bad-len/space-key); whole-chain sing-box check PASS
|
||||
for mode=prefix; yarn ci green (472 vitest), main.js no-diff. Both APPROVED
|
||||
(045 with the human-eyeball condition). PRIVACY: synthetic tags only, full
|
||||
diff privacy-scan clean. Subagents truncated their final messages TWICE this
|
||||
session (frontend stopped before section.js; reviewer stopped mid-analysis) —
|
||||
ALWAYS verify on-disk state + run/inspect gates yourself rather than trusting
|
||||
a truncated "done".
|
||||
|
||||
## task-046 gzip subscription bodies — issue #13 (2026-06-12)
|
||||
|
||||
- ISSUE #13: a subscription panel sometimes returns a gzip-compressed body;
|
||||
NetShift parsed the binary as text -> all nodes skipped -> "No subscription
|
||||
User-Agent candidate produced valid outbounds". Reporter's workaround
|
||||
gzip-decompressed only inside normalize_subscription_to_singbox — INSUFFICIENT
|
||||
because validate_subscription_file runs BEFORE normalize and also chokes on a
|
||||
gzipped sing-box JSON. Correct seam = DOWNLOAD path (decompress once, both
|
||||
consumers see text).
|
||||
- DEVICE FACTS (verified, OWRT 24.10 + 25.12): gzip/gunzip/zcat present
|
||||
(busybox); zstd/unzstd ABSENT (would need a new DEPENDS pkg). wget sends NO
|
||||
Accept-Encoding and does NOT transparently decompress, so gzip = server
|
||||
unconditionally compressing. gzip magic 1f 8b; `gzip -dc` on non-gzip rc=1.
|
||||
NO od/hexdump/xxd on device.
|
||||
- OPERATOR DECISIONS: gzip ONLY (not zstd/deflate — separate future task if
|
||||
panels actually send them); detect at download time; + NUL-byte guard (reject
|
||||
still-binary body to next UA). Full backend+smoke.
|
||||
- FIX (task-046, APPROVED W/ CONDITIONS, smoke 174/0): two helpers.sh helpers
|
||||
(modeled on convert_crlf_to_lf, mktemp+mv, all local, best-effort return 0):
|
||||
* maybe_gunzip_subscription_file <f>: ATTEMPT-DECOMPRESS detector (NO od) —
|
||||
`gzip -dc f > tmp` accept ONLY if rc=0 AND non-empty AND NUL-free, else leave
|
||||
original byte-identical (plain text makes gzip -dc rc!=0 -> untouched, can
|
||||
never corrupt text). This doubles as the gzip detector, avoids byte-fiddling.
|
||||
* subscription_body_is_binary <f>: NUL detector via `wc -c < f` vs
|
||||
`tr -d '\000' < f | wc -c` (counts differ -> NUL). Portable, no special grep
|
||||
flag, no od.
|
||||
Wired into download_subscription_into_cache per-UA while-read loop AFTER
|
||||
download_subscription, BEFORE validate (netshift:585-594): gunzip then if
|
||||
binary -> warn + continue to next UA (same control flow as a validation fail).
|
||||
file_size/debug log MOVED to after decompress so size reflects decompressed
|
||||
body. NO wget/Accept-Encoding change, NO Makefile DEPENDS change.
|
||||
- REUSABLE PATTERNS: file-transform helper = mktemp+transform+mv-on-success/
|
||||
rm-on-fail (convert_crlf_to_lf is the template). NUL/binary detect without od =
|
||||
wc-vs-tr-d-NUL byte-count. attempt-decompress is a clean od-free magic detector.
|
||||
- CONDITION (M1) is commit-hygiene only: keep the pre-existing unrelated
|
||||
.opencode/agent/*.md churn (model rename + `bash "*": ask`->`allow`) OUT of the
|
||||
commit; the permission loosening is a separate human decision.
|
||||
|
||||
## task-047 false-"Outdated" — minified-JSON tag parse (2026-06-12)
|
||||
|
||||
- ISSUE (user, AX3000T, OWRT 25.12.2 apk, NetShift 0.8.8): "check for updates"
|
||||
always says Outdated; "update" -> error; reinstall via Putty doesn't help. OCR
|
||||
of the UI showed the "latest version" as a URL
|
||||
`https://api.github.com/repos/yandexru45/netshift/releases/<id>` instead of a
|
||||
version number.
|
||||
- ROOT CAUSE (PROVEN by reproduction): updates_netshift_latest_tag
|
||||
(updater.sh) parsed the tag with `grep '"tag_name":' | head -n1 |
|
||||
cut -d'"' -f4`. That ONLY works on PRETTY-PRINTED JSON. GitHub API/CDN/proxies
|
||||
often return MINIFIED JSON (whole object on one line) -> grep matches the
|
||||
entire line, `cut -d'"' -f4` returns the FIRST key's value = the release
|
||||
"url". So latest="<url>", `0.8.8 != <url>` -> false "outdated"; and the
|
||||
self-update worker (same fn for $latest) logged "downloading NetShift <url>
|
||||
release packages" then failed. Reinstall is useless: bug is in PARSING.
|
||||
Repro: `printf '{"url":".../releases/338202209","id":...,"tag_name":"0.8.8",...}'
|
||||
| grep '"tag_name":'|head -1|cut -d'"' -f4` => the url.
|
||||
- WHY ONLY THIS FN: sing-box-extended path already uses jq (.tag_name); asset
|
||||
download uses `grep -o "https://...\.(ipk|apk)"` (newline-agnostic, robust);
|
||||
install.sh only rate-limit-greps + asset grep -o. All unaffected. jq is a hard
|
||||
dep used 23x in updater.sh.
|
||||
- FIX (task-047, APPROVED W/ CONDITIONS): replace the body with
|
||||
`tag="$(printf '%s' "$response" | jq -r '.tag_name // empty' 2>/dev/null)";
|
||||
[ -n "$tag" ] || return 1; printf '%s' "$tag"`. Format-independent; `.tag_name
|
||||
// empty` -> empty on rate-limit/error object (callers already handle
|
||||
"could not determine latest"). Fixes BOTH check_update and self-update (shared
|
||||
fn). Callers untouched. No Oniguruma.
|
||||
- TEST (smoke `latesttag`, registered main all)+case+usage): stub the network
|
||||
boundary `updates_http_get_once` (NOT the fn under test), run the REAL
|
||||
updates_netshift_latest_tag. Cases: MINIFIED (exact bug shape, url before
|
||||
tag_name) -> 0.8.8 not url (regression guard); pretty -> 0.8.8; rate-limit ->
|
||||
empty+nonzero; e2e minified through updates_check_netshift -> status latest.
|
||||
set -e rc capture via `&& printf 0 || printf %s "$?"`. Guard self-proven.
|
||||
- LESSON (reusable): NEVER field-position-parse JSON (`grep KEY|head|cut -d'"'
|
||||
-fN`) — it silently breaks on minified responses by grabbing the first key's
|
||||
value. Use jq (already a dep) for any tag/scalar extraction; reserve `grep -o
|
||||
<url-regex>` for URL-list extraction (that IS newline-agnostic and fine).
|
||||
- WHOLE-CHAIN verified on the user's class of box earlier: get_system_info keeps
|
||||
latest="unknown" (no net); check_update is the on-demand fetch; the diagnostic
|
||||
row + manager card flag outdated when installed != latest, so a bad latest
|
||||
poisons BOTH UI spots — fixing the parse fixes all consumers.
|
||||
- CONDITION [S1] is commit-hygiene: keep the pre-existing unrelated
|
||||
.opencode/agent/*.md churn (model rename + `bash "*": ask`->`allow`) OUT of the
|
||||
commit; the bash-permission widening is security-relevant and needs a separate
|
||||
explicit human decision. (Recurring across tasks 043/046/047 — these 5 files
|
||||
were already dirty at session start.)
|
||||
|
||||
## task-048 scalar option subscription_url — sing-box won't start (2026-06-12)
|
||||
|
||||
- ISSUE (Nick, Cudy WR3000E, OWRT 25.12.4, 0.8.9): urltest section works; switch
|
||||
to subscription -> whole chain dead (sing-box not running, no nft table,
|
||||
FakeIP 127.0.0.42:53 refused). Startup log: "Outbound section not found.
|
||||
Aborted." despite config having a subscription_url.
|
||||
- DIAGNOSIS METHOD: user sent 2x3 diagnostic txt (global_check/show_sing_box_config/
|
||||
view_logs) for working vs broken. The view_logs filenames were SWAPPED (the
|
||||
tiny 435B file was the broken run, the big one was the working urltest run) —
|
||||
read by CONTENT not filename. Broken view_logs: single "Outbound section not
|
||||
found. Aborted." line. global_check(broken): proxy_config_type 'subscription'
|
||||
with `option subscription_url '...'` (scalar, NOT list).
|
||||
- ROOT CAUSE (PROVEN on hardware): get_subscription_urls_for_section reads
|
||||
subscription_url ONLY via config_list_foreach, which iterates ONLY UCI `list`
|
||||
values and returns EMPTY for a scalar `option`. Proven: config_list_foreach
|
||||
over option => []; config_get => the value; over list => works. The UI writes
|
||||
`list` (form.DynamicList) so new configs are fine; legacy/CLI/podkop-migrated
|
||||
configs use `option` and broke. Every subscription consumer funnels through
|
||||
this ONE helper -> fixing it fixes the whole chain.
|
||||
- FIX (task-048, APPROVED round 2): (1) load-bearing read-fallback in
|
||||
get_subscription_urls_for_section: if the list read is empty, config_get the
|
||||
scalar and feed it through _collect_subscription_url_handler. (2) one-time
|
||||
idempotent option->list migration at top of start_main (after config_load,
|
||||
before check_requirements), only on the broken shape, never exits.
|
||||
- REVIEW LOOP (2 rounds, important): round 1 REQUIRES CHANGES — BLOCKER [B1]
|
||||
data loss: the migration used `uci add_list "key=value"` which SPLITS ON THE
|
||||
FIRST `=` and loses query-string URLs (?token=abc&x=1) — reproduced on hardware
|
||||
(rc=1, list empty, scalar already deleted => URL gone on disk). Fix: use the
|
||||
`uci_add_list <cfg> <sec> <opt> "<val>"` SHELL HELPER (separate-arg, preserves
|
||||
=/&), delete-then-add with scalar RESTORE on add failure, flag gates the
|
||||
commit. Plus [S1] the new test ran assertions on the RHS of a pipe (subshell
|
||||
counter loss) so it didn't gate CI -> fixed to `while read < tmpfile`. Round 2
|
||||
APPROVED.
|
||||
- LESSONS (reusable): (a) NEVER trust user-supplied filenames for which-is-which —
|
||||
read by content. (b) `config_list_foreach` does NOT read scalar options; any
|
||||
list-option reader needs a scalar config_get fallback for back-compat with
|
||||
legacy/CLI/migrated configs. (c) `uci add_list "k=v"` CLI form is unsafe for
|
||||
values containing `=` — use the uci_add_list shell helper. (d) a smoke test
|
||||
that pipes into `while read; pass/fail` does NOT gate CI (subshell) — the count
|
||||
jump (178->190) when fixed is the tell. (e) RE-check the dev's memory note on a
|
||||
fix round — it's often pre-fix and re-seeds the anti-pattern.
|
||||
- PRIVACY: Nick's dump contained a real subscription URL; moved all 6 txt to
|
||||
/tmp/opencode/nick-diag (out of git); code/tests/specs/memory use ONLY synthetic
|
||||
https://example.com/sub. Final whole-tree sweep for the real host => clean.
|
||||
- GATES: shellcheck -S error clean; smoke `all` 178->190/0 (the +12 are the now-
|
||||
gating suburlopt tokens incl. 4 =-URL guards); whole-chain verified (option
|
||||
config -> has_outbound_section TRUE -> gen -> sing-box check). Runtime contract
|
||||
intact (UCI schema = back-compat repr normalization only).
|
||||
|
||||
## GitHub API rate-limit — research + task-049 (2026-06-12)
|
||||
|
||||
- HOW IT WORKS (official docs, verified): anonymous api.github.com = 60 req/HOUR
|
||||
per IP (authed=5000). On 403/429 the body is {"message":"API rate limit
|
||||
exceeded for <IP>..."} and headers x-ratelimit-remaining:0 + x-ratelimit-reset
|
||||
(UTC epoch). GET /rate_limit shows budget and does NOT cost primary quota.
|
||||
- WHY users hit it a lot: limit is PER IP. Routers behind CGNAT / shared ISP
|
||||
IPs / shared-VPN egress share ONE IP's 60/hour with many strangers -> the
|
||||
budget is often already drained by others. Not the user's fault; we're
|
||||
anonymous so we can't raise it without a token.
|
||||
- KEY LEVER (PROVEN on hardware, curl): github.com/<repo>/releases/latest is
|
||||
served by the github.com FRONTEND, NOT the rate-limited API. It 302-redirects
|
||||
to /releases/tag/<tag>. `curl -sI -o /dev/null -w '%{redirect_url}'` returns
|
||||
`.../releases/tag/0.8.9` -> tag extracted WITHOUT touching api.github.com.
|
||||
And github.com/<repo>/releases/download/<tag>/<asset> 302s to the CDN
|
||||
(release-assets.githubusercontent.com) -> direct asset download, no API.
|
||||
(We already use this redirect path for SRS_MAIN_URL = releases/latest/download.)
|
||||
- BUSYBOX wget on-device is STRIPPED: no -S, no --max-redirect, can't read
|
||||
Location/headers. So tag extraction MUST use curl (a hard DEPENDS: +curl),
|
||||
via %{redirect_url} (or -w %{url_effective} with -L). updates_http_get_once
|
||||
already prefers curl.
|
||||
- ASSET NAMING is deterministic from the tag: ipk = netshift-<ver>-r1-all.ipk,
|
||||
luci-app-netshift-<ver>-r1-all.ipk, luci-i18n-netshift-ru-<ver>.ipk; apk =
|
||||
netshift-<ver>-r1.apk, luci-app-netshift-<ver>-r1.apk,
|
||||
luci-i18n-netshift-ru-<ver>.apk. (<ver> = tag.) All 302 on the direct path.
|
||||
- SCOPE DECISION (task-049): migrate the 3 NETSHIFT-repo touchpoints off
|
||||
api.github.com -> redirect path: version check (updater.sh:1638
|
||||
updates_netshift_latest_tag), self-update asset download
|
||||
(updater.sh:1657 _updates_self_update_download_assets), and install.sh:259.
|
||||
KEEP the sing-box-EXTENDED path (updater.sh:553 releases?per_page=30) on the
|
||||
API + proxy-fallback: it genuinely needs the releases LIST (draft/prerelease
|
||||
flags + per-arch asset selection) which a redirect can't give; it's also a
|
||||
rarer, on-demand action. Proxy-fallback stays as the safety net there.
|
||||
- ALSO: honor x-ratelimit-reset / show honest "GitHub limit, retry after HH:MM"
|
||||
instead of generic error; optionally TTL-cache the latest tag. (Secondary.)
|
||||
- itdoginfo's podkop historically had the SAME complaint class; this redirect
|
||||
approach is the standard fix.
|
||||
|
||||
## task-049 CLOSED — APPROVED (2026-06-12)
|
||||
- Implemented: redirect-first tag fetch (updates_github_resolve_redirect +
|
||||
updates_netshift_latest_tag), deterministic asset-URL download
|
||||
(updates_netshift_asset_filename + _updates_self_update_download_assets), and
|
||||
install.sh redirect path (+ shared download_release_asset helper). API path
|
||||
kept as graceful fallback everywhere; sing-box-extended path untouched.
|
||||
- Gates: shellcheck -S error clean; smoke 190->196/0 (+6 ghredirect, gating);
|
||||
self-proved. Tag is rejected if empty/`/`-containing (injection/traversal
|
||||
guard) and only used quoted in URL strings (no eval) — reviewed safe.
|
||||
- Review: 1 round, APPROVED. Only finding [M1] = a comment typo
|
||||
(updates_http_get_once -> updates_download_to_file); fixed during review,
|
||||
shellcheck re-clean, ghredirect re-run 6/0.
|
||||
- Net result for the user complaint: the normal version-check/self-update/install
|
||||
path no longer touches the 60/hr-per-IP api.github.com, so CGNAT/shared-IP
|
||||
rate-limit errors should largely disappear; API remains the fallback.
|
||||
|
||||
## task-050 "Fastest" urltest-of-urltests — CLOSED, APPROVED (2026-06-13)
|
||||
- RESEARCH (confirmed): sing-box urltest.outbounds = "list of outbound tags to
|
||||
test" with NO type restriction -> a urltest CAN nest other urltest tags. At
|
||||
runtime it probes each member THROUGH that member's dial path, so a nested
|
||||
group-urltest member is measured via that group's currently-selected (fastest)
|
||||
node => "fastest country/group" emerges correctly. Maintainer confirms nested
|
||||
urltest runs (SagerNet/sing-box #2130; the failover caveat there is unrelated).
|
||||
`sing-box check` accepts a urltest referencing urltest tags (PROVEN by the
|
||||
smoke's real in-container check).
|
||||
- FEATURE: when subscription grouping is ON (country/prefix) AND there are >=2
|
||||
groups, build a top-level urltest "⚡ Fastest" (SB_SUBSCRIPTION_FASTEST_GROUP_TAG)
|
||||
over the per-group urltests, PREPEND it to the main selector, make it the
|
||||
selector DEFAULT. Specific groups + ungrouped stay selectable. ==1 group ->
|
||||
skip the redundant nested layer (default = lone group). ==0 -> no fastest
|
||||
urltest. OFF mode unchanged. Operator: this becomes the default-on-grouping.
|
||||
- KEY IMPL DETAIL: capture group_tags_json from selector_outbounds_json BEFORE
|
||||
ungrouped is appended, so the nested urltest nests ONLY group urltests (not raw
|
||||
ungrouped nodes). Reuse the section's urltest knobs (interval/tolerance) — no
|
||||
hardcoded aggressive cadence; documented that it adds one probe layer.
|
||||
- NO FRONTEND CHANGE: the dashboard (getDashboardSections.ts) renders selector
|
||||
members live from the Clash structure and shows each member's name verbatim
|
||||
(only legacy `<section>-urltest-out` maps to _('Fastest')). The new deduped
|
||||
"⚡ Fastest" tag (code==tag, not -urltest-out) renders raw like per-group
|
||||
"🇷🇺 Fastest", urltests sort first so it leads + is selectable. main.js untouched.
|
||||
- GATES: shellcheck -S error clean; smoke 196->202/0 (+6 fastest tokens, gating;
|
||||
test calls pass/fail directly in the fn body, not a pipe-subshell). Real
|
||||
`sing-box -c check` on the nested config passes (skips cleanly when sing-box
|
||||
absent). Self-proved. Review 1 round APPROVED, no conditions.
|
||||
- This is a natural extension of task-044/045 (universal grouper). Performance
|
||||
caveat on record: nested probing = extra load; tunable via the section's
|
||||
urltest_check_interval/tolerance.
|
||||
|
||||
## task-051 Text-list Selector/URLTest — issue #10 — CLOSED, APPROVED (2026-06-13)
|
||||
- REQUEST (Naxeron #10 + killyourgod TG): paste many proxy links (one per line)
|
||||
into a textarea -> build a Selector or URLTest. KEY FINDING: the backend ALREADY
|
||||
built selector/urltest from a LIST of links (proxy_config_type selector/urltest
|
||||
reading selector_proxy_links/urltest_proxy_links via `for link in $...`). The
|
||||
only gap was the INPUT shape (DynamicList = add-one-by-one). The `url` type
|
||||
already used a textarea (scalar proxy_string).
|
||||
- OPERATOR DECISION: TWO NEW types `selector_text`/`urltest_text` (keep the
|
||||
existing DynamicList ones), labels "Selector (text list)"/"URLTest (text list)",
|
||||
textarea storing SCALAR options selector_proxy_links_text/urltest_proxy_links_text.
|
||||
- BACKEND: refactored the duplicated per-link build loop into ONE shared helper
|
||||
`_build_proxy_member_outbounds <section> <blob> <udp_over_tcp> <label>` used by
|
||||
ALL FOUR branches. It MUTATES global $config in place + reports via globals
|
||||
_member_outbound_tags/_member_default_outbound which it RESETS at the top of
|
||||
each call (no stale leak across config_foreach sections — the key refactor
|
||||
risk, verified safe). Line parsing: `for link in $blob` IFS-splits newlines +
|
||||
spaces; trailing CR stripped (`cr=$(printf '\r'); link="${link%"$cr"}"`); blanks
|
||||
skipped. Empty->fatal+exit1; all-unsupported->mark_section_outbound_unavailable.
|
||||
section_has_configured_outbound + the missing-fields message updated.
|
||||
- FRONTEND: 2 dropdown values + 2 textareas (taboption in the SAME tab as their
|
||||
list-typed siblings: selector_text->connection, urltest_text->subscription);
|
||||
urltest tuning fields (interval/tolerance/testing_url) got urltest_text
|
||||
depends-twins. New validator validateProxyUrlList (split \n, trim/CRLF, skip
|
||||
blanks, validateProxyUrl per line, 1-based line-context error) barrel-exported
|
||||
-> main.validateProxyUrlList; 13-case vitest. 7 i18n msgids + RU, fe<->luci
|
||||
byte-identical. main.js +28 (validator), fresh build = no further diff.
|
||||
- PARALLEL EXECUTION: backend + frontend launched in parallel (disjoint files,
|
||||
shared only by the UCI contract). Both subagents truncated mid-work (recurring
|
||||
this session) -> resumed each + verified on-disk state myself.
|
||||
- GATES: shellcheck error clean; smoke 202->218/0 (+16 textlist tokens, gating;
|
||||
real in-container sing-box check for BOTH new types); yarn ci green (485
|
||||
vitest); main.js freshly-built. Reviews: backend APPROVED; frontend APPROVED
|
||||
WITH CONDITIONS ([C1] non-code: human visual check of the rendered tabs — no
|
||||
browser in CI, main.js only carries the validator delta).
|
||||
- REUSABLE: when a "new input type" request lands, FIRST check if the backend
|
||||
already supports the underlying data (it often does) — the work may be purely
|
||||
a FE input-shape change + a thin backend branch reusing the existing build.
|
||||
|
||||
@ -64,3 +64,12 @@ append recurring findings; keep under ~200 lines.
|
||||
- Frontend barrel exposure: anything added to src/helpers/index.ts (or any export* barrel reaching main.ts) AND actually used appears in the generated main.js baseclass.extend block as a main.* symbol; unused re-exports get tree-shaken. So internal-only helper + added to barrel + used = it WILL leak to main.*. To keep a helper truly internal, place it in the consuming module, not the barrel.
|
||||
|
||||
- OpenWrt jq ascii_downcase only folds ASCII A-Z; case-insensitive matching on Cyrillic/Unicode names needs an inline codepoint fold (explode/map/implode: ASCII 65-90 +32, Cyrillic 1040-1071 +32, Yo 1025->1105). When reviewing such a fold: (a) already-lowercase ranges excluded (no double-fold), (b) def before first use when the program does NOT import helpers.jq, (c) a pure-emoji-keyword exact-match test proves non-folded codepoints pass through unchanged on both sides. (task-010)
|
||||
|
||||
- Package-manager rc is NOT a reliable success signal on opkg: rc=0 for "Not downgrading"/"already installed"/"up to date". A self-update/install that trusts only rc silently no-ops (the v→no-v rename trap: legacy `v0.8.6` sorts ABOVE `0.8.7` in opkg's compare, so `opkg install` refuses the "downgrade" and returns 0). When reviewing a package-install path, require: (a) `--force-downgrade --force-reinstall` on the opkg branch (apk overwrites by default); AND (b) verify-after-install — RE-READ the installed version (opkg `list-installed | grep "^pkg "`, apk `list --installed`; grep/awk only, NO Oniguruma jq) and compare v-stripped semver (`${x#v}`, `${x%%-*}`) with `==` OR `is_min_package_version installed target`; empty-installed must fail-safe to success:false. Keep install.sh `pkg_install` and updater.sh `updates_pkg_install_file` opkg branches ALIGNED. (task-041/042)
|
||||
- Async self-update worker landmine: the `_*_core` worker MUST `return 1` (NEVER `exit`) on failure so the public wrapper's always-run `updates_restore_after_swap` epilogue + finished-job-state write still execute. Verify the wrapper captures core rc/JSON to a temp file then unconditionally restores. Smoke assertions for these must be in the MAIN shell body (direct `if…pass/fail`), never inside `cmd | while read` (subshell swallows PASS/FAIL — harness-wide landmine). (task-041)
|
||||
|
||||
- UCI option→list rewrites: the `uci add_list "key=value"` CLI form splits on the FIRST `=` and SILENTLY LOSES query-string URLs (`?token=abc&x=1`) — reproduced on hardware (rc=1, list empty). Require the `uci_add_list <cfg> <sec> <opt> "<val>"` SHELL HELPER (separate-arg, preserves `=`/`&`). For delete-then-add rewrites, verify a failed add RESTORES the scalar AND that the change-flag gates the `uci commit` (an uncommitted in-memory delete must never persist). (task-048 [B1])
|
||||
- When RE-reviewing a fix round, also diff the developer's MEMORY note: it is frequently written against the PRE-fix code and re-seeds the very anti-pattern that was just fixed (task-048 [M2]: note still showed the `key=value` form + "non-gating piped-while" after both were fixed). Flag a stale memory note as a (minor) condition.
|
||||
- Test-gating landmine: a smoke test whose assertions run on the RHS of a pipe (`cmd | while read; pass/fail`) does NOT gate CI (subshell counter loss) — a FAIL token prints red but the suite exits 0. Require current-shell parsing (`while read < tmpfile`). The 178→190 count jump when task-048 fixed this is the tell. (task-048 [S1])
|
||||
|
||||
- Rate-limit avoidance via redirect path (task-049): version-check/self-update/install can read the latest tag from `github.com/<repo>/releases/latest` (302 -> /releases/tag/<tag>, served by the github.com FRONTEND, NOT the 60/hr-per-IP api.github.com) instead of the API. Tag extracted with `curl -sI -o /dev/null -w '%{redirect_url}'` then `case`/param-expansion `${r##*/releases/tag/}` — when reviewing such code REQUIRE: (a) the tag is rejected if empty OR `/`-containing (path-traversal/injection guard) via `case "$tag" in ''|*/*) tag="" ;;`; (b) the tag is only used quoted inside a URL string / passed quoted to helpers, never `eval`'d or used as a bare filesystem path; (c) curl-absent / non-match degrades to the API fallback (no hard-fail/exit); (d) the file-download helper uses `curl -fsSL`/`-L` so the CDN 302 on `releases/download/<tag>/<asset>` is followed. busybox wget on-device is STRIPPED (no -S/--max-redirect/header read) so redirect reading MUST use curl (hard +curl dep). Keep the sing-box-EXTENDED releases-LIST path on the API (a redirect can't give draft/prerelease/per-arch).
|
||||
|
||||
@ -811,3 +811,114 @@ append findings; keep under ~200 lines.
|
||||
task-039 backend changes (netshift bin, updater.sh, tests/entrypoint.sh) +
|
||||
.opencode/agent edits — NOT mine. FLAG (no browser): button render + toast
|
||||
sequence verified by reasoning + the gate, NOT screenshotted.
|
||||
|
||||
## task-045 — universal subscription grouper (mode dropdown + prefix length)
|
||||
|
||||
- REPLACED the single `subscription_group_by_countries` form.Flag (section.js
|
||||
~190-201) with TWO taboptions in the SAME `subscription` tab (mandatory —
|
||||
tabbed section, a plain option() renders nothing):
|
||||
(1) `form.ListValue subscription_group_mode` — values off/country/prefix
|
||||
(`_("Off")`/`_("By country flag")`/`_("By name prefix")`), `o.default="off"`,
|
||||
`o.rmempty=false`, depends `{connection_type:"proxy",proxy_config_type:
|
||||
"subscription"}`; title `_("Subscription grouping")` + single-literal help.
|
||||
(2) `form.Value subscription_group_prefix_len` — title `_("Prefix length")`,
|
||||
`o.default="2"`, `o.datatype="and(uinteger,min(1))"`, `o.rmempty=false`,
|
||||
depends ADDS `subscription_group_mode:"prefix"` (3-key object) so it shows
|
||||
ONLY when mode=prefix. CBI cross-field depends within the same section/tab
|
||||
works fine; a fully-hidden field is OK.
|
||||
- CROSS-LAYER CONTRACT (task-044 backend, DONE): UCI options EXACTLY
|
||||
`subscription_group_mode` ∈ {off,country,prefix} default off, and
|
||||
`subscription_group_prefix_len` positive-int string default 2 (meaningful
|
||||
only when mode=prefix). Backend falls back to the LEGACY
|
||||
`subscription_group_by_countries` boolean ONLY when the new option is ABSENT
|
||||
→ the UI writes only the NEW options; NO JS migration written.
|
||||
- types.ts: swapped `subscription_group_by_countries?: '0'|'1'` →
|
||||
`subscription_group_mode?: 'off'|'country'|'prefix'` +
|
||||
`subscription_group_prefix_len?: string`. Grepped src first — NOTHING in TS
|
||||
reads the old key (only the type decl), so removing it is safe (backend reads
|
||||
the legacy UCI key directly, not via UI). Pure type-only → erased at build.
|
||||
- main.js: ZERO diff (section.js hand-written + not bundled; types.ts type-only).
|
||||
md5 unchanged across the build (aa89dfc5…). Confirmed via
|
||||
`git diff --exit-code main.js`. This is the EXPECTED/correct outcome — a diff
|
||||
there would mean an unexpected src change.
|
||||
- i18n: ran `node {extract-calls,generate-pot,generate-po ru,distribute-
|
||||
locales}.js` (yarn classic 1.22.22, but used node to avoid corepack). msgid
|
||||
delta = clean SWAP: removed 2 (`Group by countries` + its long description),
|
||||
added 7 (Off / By country flag / By name prefix / Subscription grouping /
|
||||
Prefix length / the grouping description / the prefix-length description).
|
||||
Filled 7 RU msgstr in SOURCE locales/netshift.ru.po then re-ran distribute →
|
||||
po/ru + po/templates byte-identical to source (diff -q both pairs). 0 empty
|
||||
non-header msgstr after. RU: Off→Выключено, By country flag→По флагу страны,
|
||||
By name prefix→По префиксу имени, Subscription grouping→Группировка подписки,
|
||||
Prefix length→Длина префикса.
|
||||
- yarn ci GREEN: format no-diff, eslint --max-warnings=0, vitest 472 pass, tsup
|
||||
build. yarn.lock unchanged, no .yarn/.yarnrc.yml. No new vitest (no new pure
|
||||
TS logic — datatype validation is LuCI client-side).
|
||||
- PRIVACY: no subscription-identifying data (hosts/IPs/URLs/keys/node names) in
|
||||
any code/comment/i18n/test/memory — generic "proxy name"/"country flag"
|
||||
wording only.
|
||||
- FLAG (no browser in env): the rendered Subscription tab (dropdown +
|
||||
conditional prefix-length field appearing only on mode=prefix, taboption
|
||||
auto-hide) needs a HUMAN VISUAL CHECK before merge — verified structurally
|
||||
only (taboption completeness, depends preserved).
|
||||
|
||||
## task-051 — text-list Selector/URLTest (paste links, one per line)
|
||||
|
||||
- CROSS-LAYER CONTRACT (backend done first): proxy_config_type values
|
||||
`selector_text` / `urltest_text`; scalar UCI options (textarea, one link per
|
||||
line) `selector_proxy_links_text` / `urltest_proxy_links_text`. Matched VERBATIM.
|
||||
- section.js (HAND-WRITTEN, NOT bundled → 0 main.js diff): (a) 2 new
|
||||
`o.value("selector_text",_("Selector (text list)"))` /
|
||||
`o.value("urltest_text",_("URLTest (text list)"))` after the `urltest` value.
|
||||
(b) 2 `form.TextValue` textareas modelled on the `url`-type `proxy_string`
|
||||
one (`o.textarea=true; o.rows=5; o.wrap="soft"; o.rmempty=false`): placed
|
||||
`selector_proxy_links_text` in the **connection** tab next to the existing
|
||||
`selector_proxy_links` DynamicList, and `urltest_proxy_links_text` in the
|
||||
**subscription** tab next to `urltest_proxy_links` (mirror the tab each
|
||||
list-typed sibling already lives in — they differ!). Each `o.validate` calls
|
||||
`main.validateProxyUrlList`.
|
||||
- URLTEST-TWIN GATING: the 3 urltest tuning fields (urltest_check_interval,
|
||||
urltest_tolerance, urltest_testing_url) each had `depends urltest` + `depends
|
||||
subscription`; added a 3rd `o.depends({connection_type:"proxy",
|
||||
proxy_config_type:"urltest_text"})` to each (CBI ORs depends). DID NOT touch
|
||||
`enable_udp_over_tcp` (gated on `connection_type:"proxy"` only → already shows
|
||||
for urltest_text) nor the subscription-only grouping/filter fields. The
|
||||
`urltest_proxy_links` DynamicList itself stays urltest-only (its text variant
|
||||
is the NEW separate field) — grep `proxy_config_type:"urltest"` leaves exactly
|
||||
4 hits: the DynamicList + 3 tuning fields.
|
||||
- NEW VALIDATOR `validateProxyUrlList(value:string):ValidationResult` — splits on
|
||||
`\n`, `.trim()` each line (so CRLF `\r` is stripped), skips blank lines, runs
|
||||
the EXISTING `validateProxyUrl` per line, returns first failure as
|
||||
`{valid:false, message:`${_('Line')} ${i+1}: ${msg}`}` (1-based incl. blank
|
||||
lines in the count) or `{valid:true,message:''}`. Empty/blank-only →
|
||||
`_('At least one proxy link must be specified.')`. ValidationResult REQUIRES
|
||||
`message:string` so valid branch sets `message:''`. BARREL-EXPORTED via
|
||||
`validators/index.ts` (`export * from './validateProxyUrlList'`) → reaches
|
||||
`main.validateProxyUrlList`. This is an EXPORTED leaf (NOT dispatcher-only like
|
||||
validateHysteria2Url/validateVmessUrl) because section.js calls it directly.
|
||||
- main.js: EXPECTED +28-line diff (the bundled validator fn + 1 export-block
|
||||
entry). Export-symbol set delta vs HEAD = EXACTLY `+ validateProxyUrlList`
|
||||
(no leak). Build IDEMPOTENT (md5 e5273ea1… across 2 builds), banner +
|
||||
`return baseclass.extend({` intact. The regenerated main.js IS the deliverable.
|
||||
- TEST `validators/tests/validateProxyUrlList.test.js`: table-driven describe.each
|
||||
(valid blobs incl. CRLF/blank-line/whitespace; invalid incl. empty/unsupported/
|
||||
garbage) + line-number-context assertions. SS fixture is the KNOWN-VALID
|
||||
`ss://2022-blake3-aes-256-gcm:dmCly/…=@127.0.0.1:27214?type=tcp` form copied
|
||||
from validateShadowsocksUrl.test.js (do NOT invent base64 that may fail). VLESS
|
||||
fixture copied from validateVlessUrl.test.js. 13 tests; total 485 pass.
|
||||
- i18n: 7 NEW msgids (Selector (text list); URLTest (text list); Selector Proxy
|
||||
Links (one per line); URLTest Proxy Links (one per line); the shared scheme-doc
|
||||
desc "…links — one per line"; "Line"; "At least one proxy link must be
|
||||
specified."). RU filled in SOURCE locales/netshift.ru.po then distribute →
|
||||
po/ru + po/templates byte-identical (diff -q). msgid count 352→359 purely
|
||||
additive. Ran `node {extract-calls,generate-pot,generate-po ru,distribute}.js`
|
||||
(generate-pot needs git user.name set). 1 empty msgstr remains = header only.
|
||||
- PRIVACY: all link strings synthetic (`127.0.0.1` hosts + scheme-doc literals);
|
||||
no real proxy/subscription data anywhere.
|
||||
- GATES GREEN: prettier --write src (all unchanged → no format churn beyond my
|
||||
files), eslint --max-warnings=0, vitest 485 pass, tsup build. yarn classic
|
||||
1.22.22 → ran via node_modules/.bin; yarn.lock unchanged, no .yarn/.yarnrc.yml.
|
||||
- FLAG (no browser in env): the rendered Connection/Subscription tabs (2 new
|
||||
dropdown choices, the 2 textareas appearing only for their type, the urltest
|
||||
tuning fields now appearing for urltest_text) need a HUMAN VISUAL CHECK —
|
||||
verified structurally only (taboption completeness, depends grep).
|
||||
|
||||
@ -105,3 +105,12 @@ artifacts out of the container -> **ipk underscore->dash rename**
|
||||
unsupported; needs >=15 MB on `/overlay`; NO uninstall path (removal lives in
|
||||
package `prerm`). GitHub API rate-limit is a known fragility (wget path has no
|
||||
guard).
|
||||
- `pkg_install` opkg branch uses `opkg install --force-downgrade
|
||||
--force-reinstall "$pkg_file"` (task-042). Plain `opkg install` silently
|
||||
no-op'd (rc=0) when re-run on a router with an older build: the legacy
|
||||
v-prefixed version (`v0.8.6-r1`) sorts ABOVE the no-v release (`0.8.7-r1`) so
|
||||
opkg "won't downgrade", and equal versions report "up to date". Both force
|
||||
flags make opkg remove+reinstall (proven on OWRT 24.10.5 aarch64). apk branch
|
||||
unchanged — `apk add --allow-untrusted` overwrites by default. This is the
|
||||
install.sh twin of the task-041 `updates_pkg_install_file` updater.sh fix;
|
||||
keep both upgrade paths (README script + in-app self-update) aligned.
|
||||
|
||||
@ -1334,3 +1334,398 @@ findings; keep under ~200 lines.
|
||||
"$out"` so counts are EXACT). Pre-existing `rh-case1/2/6:FAIL` red marks persist
|
||||
(task-031 piped-while quirk; suite EXIT=0). No sacred value/port/mark/path/ACL/
|
||||
frontend/async-machinery/download-guard change.
|
||||
|
||||
## task-041: self-update verify-after-install (opkg silent no-op fix)
|
||||
|
||||
- **Root cause (proven on router):** opkg returns rc=0 for "Not downgrading"/
|
||||
"already installed"/"up to date". A legacy v-prefixed build (`v0.8.6-r1`) sorts
|
||||
ABOVE the no-v target (`0.8.7-r1`) in opkg's dpkg-style compare, so a plain
|
||||
`opkg install` refuses AND returns rc=0. The self-update worker trusted rc and
|
||||
emitted `{"success":true}` while NOTHING installed. **opkg install rc is NOT a
|
||||
reliable success signal** — the only robust check is to RE-READ the installed
|
||||
version after the call.
|
||||
- Fix #1 (`updates_pkg_install_file` opkg branch): `opkg install
|
||||
--force-downgrade --force-reinstall "$pkg_file"`. `--force-downgrade` lands the
|
||||
v→no-v transition; `--force-reinstall` covers the same-exact-version no-op. apk
|
||||
branch unchanged (`apk add --allow-untrusted`; apk overwrites by default).
|
||||
- Fix #2 new helper `updates_pkg_installed_version <pkg>` (mirrors
|
||||
`updates_pkg_is_installed`/`_candidate_version` but reads INSTALLED not feed):
|
||||
opkg `list-installed | grep "^<pkg> " | head -n1 | awk -F' - ' '{print $2}'`;
|
||||
apk `list --installed <pkg> | awk '{print $1}' | head -n1` then strip `<pkg>-`
|
||||
prefix via `${line#"$pkg"-}`. grep/awk only, NO Oniguruma.
|
||||
- Fix #3 verify-after-install belt in `_updates_self_update_netshift_core` (after
|
||||
the install loop, BEFORE the defensive restore + success cleanup): re-read the
|
||||
CORE installed version, normalize with the SAME rules the version-decision uses
|
||||
(`${x#v}` then `${x%%-*}` to drop `-rN`), and gate on
|
||||
`[ "$inst_semver" != "$latest_semver" ] && ! is_min_package_version
|
||||
"$inst_semver" "$latest_semver"` (i.e. fail unless installed == target OR
|
||||
installed >= target). On fail: `_updates_self_update_restore_config
|
||||
"$backup_made"` + `rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR"` + `updates_log ...
|
||||
"error"` + `echo '{"success":false,"message":"NetShift core package did not
|
||||
upgrade ...; configuration preserved"}'` + **`return 1` (NEVER exit** — async
|
||||
worker; exit skips the wrapper's `updates_restore_after_swap` epilogue + the
|
||||
finished-job-state write). Only a verified change reaches the existing success
|
||||
JSON. Gates the CORE pkg only (luci/ru stay non-critical).
|
||||
- `is_min_package_version current required` returns 0 when `current >= required`
|
||||
(it's a `sort -V | head -1 == required` test) — so call it as
|
||||
`is_min_package_version "$installed" "$target"`.
|
||||
- All new vars declared at the TOP of the core fn (`core_installed
|
||||
core_installed_semver latest_semver`) — shellcheck `-S error` clean.
|
||||
- **Smoke (test_self_update_netshift, alias `selfupdate`, no new top-level test):**
|
||||
extended the fake opkg stub: `install` arm now skips leading `--*` flags
|
||||
(`while ...; case --*) shift;; *) break;; esac`) so `$1` is the file path even
|
||||
with the new force flags; on a REAL success it rewrites the `netshift -` line in
|
||||
`$SU_INSTALLED_LIST` to `$SU_TARGET_INSTALLED_VER` (so verify passes) UNLESS
|
||||
`$SU_NOOP` is set (then list keeps the OLD version → simulates "Not
|
||||
downgrading"). luci-* files excluded from the rewrite via nested case. Each
|
||||
scenario now seeds `installed.list` with `netshift - 0.8.0-r1` (the install arm
|
||||
mutates it, so reset per-scenario). New Scenario 6 (`SU_NOOP=1`, all other
|
||||
markers ok): asserts `selfupdate-noop-detected-successfalse` (success:false),
|
||||
`-noop-install-attempted`, `-noop-config-intact`, `-noop-download-dir-cleaned`.
|
||||
Happy path Scenario 3 still `success:true version 0.8.1` (verify passes because
|
||||
the stub reports the target post-install). SELF-PROVED the guard: temporarily
|
||||
prefixed the verify `if` with `false &&` → `selfupdate-noop-detected-successfalse`
|
||||
FAILED (worker falsely reported success on the no-op), then restored → passes.
|
||||
- shellcheck -S error clean (bin+libs+install.sh); `smoke-tests all` = 170 passed
|
||||
/ 0 failed (166 baseline + 4 new no-op assertions; selfupdate category 13→17).
|
||||
No constants.sh / frontend / async-machinery / version-decision(:1679) /
|
||||
sing-box-install-path / sacred-value change. apk path: the same verify belt
|
||||
covers apk's equal-version no-overwrite quirk (reasoned; opkg covered in smoke).
|
||||
|
||||
## task-043 — versioned Xray-JSON probe User-Agents (constants-only)
|
||||
|
||||
- Finding (abstract, no identifiers): some panels gate their Xray-JSON branch on
|
||||
a `<client>/<version>` UA SHAPE. A bare/version-less UA can be rejected (server
|
||||
502); a VERSIONED UA of the same client yields the wanted Xray-JSON array body
|
||||
(the multi-profile format `xray_json_to_uri_lines` parses, carrying
|
||||
xhttp/hysteria2). Fix = make the probe send versioned UAs first.
|
||||
- `SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES` (constants.sh) is the ordered set
|
||||
probed FIRST in `xray` format-preference mode by
|
||||
`build_subscription_user_agent_candidates` (helpers.sh:765-771;
|
||||
order = XRAY_CANDIDATES → default(singbox/<ver>) → cached-winner → main
|
||||
whitelist, deduped). Changed it from bare `"v2rayN Happ"` to versioned
|
||||
`"Happ/1.0.0 v2rayN/7.0.0 v2rayNG/1.9.0"` (versioned client first). The
|
||||
separate auto-mode `SUBSCRIPTION_USER_AGENT_CANDIDATES` (still has bare names)
|
||||
and the request headers in `_wget_subscription_request` were untouched — only
|
||||
the UA VALUE decides the 502-vs-200 outcome, no header change needed.
|
||||
- Coupled smoke test: `fb-caseI-xraypref-*` (tests/entrypoint.sh CASE I, run via
|
||||
the `subscription` category) hardcoded the old bare literals. Rewrote them to
|
||||
DERIVE the expected first/second/third candidates from
|
||||
`$SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES` (sourced in the CASE-I subshell via
|
||||
`set -- $SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES`) so a future version bump
|
||||
won't rot the test. Added one generic guard `fb-caseI-xraypref-first-versioned`
|
||||
(`case "$first" in */*)`) so we never regress to a bare UA. The auto-mode
|
||||
`fb-caseI-auto-has-v2rayN` assertion is unrelated (tests the auto whitelist).
|
||||
- shellcheck -S error clean (entrypoint.sh is OUT of the lint scope — only
|
||||
bin/lib/install.sh). `smoke-tests all` = 170 passed / 0 failed (same total
|
||||
before+after; the standalone `subscription` category went 86→87 from the +1
|
||||
guard, but the aggregate `all` "Results:" counter reported 170 either way — a
|
||||
harness counting quirk, not a regression; all four `fb-caseI-xraypref-*` lines
|
||||
print :OK in both runs). No ports/marks/paths/schema touched; runtime contract
|
||||
intact.
|
||||
|
||||
## task-046 — gzip subscription body decompress + NUL guard (issue #13)
|
||||
|
||||
- Some panels return a gzip-compressed HTTP body unconditionally; busybox wget
|
||||
does NOT transparently decompress and NetShift sends no Accept-Encoding, so the
|
||||
raw bytes are binary and validate/normalize choke ("No subscription User-Agent
|
||||
candidate produced valid outbounds").
|
||||
- Added two best-effort helpers to `helpers.sh` (next to `convert_crlf_to_lf`):
|
||||
- `maybe_gunzip_subscription_file <f>`: attempt-based detection (no
|
||||
od/hexdump/xxd — none on device). `gzip -dc` (busybox built-in) into a
|
||||
mktemp; accept ONLY if rc=0 AND result non-empty AND NUL-free, then `mv` into
|
||||
place (else `rm`). `gzip -dc` on plain text returns rc!=0 cleanly, so plain
|
||||
text is left byte-for-byte untouched — never corrupts text. Always returns 0.
|
||||
- `subscription_body_is_binary <f>`: returns 0 (true) if file has a NUL byte.
|
||||
Busybox-safe, no od: compare `wc -c < f` to `tr -d '\000' < f | wc -c` (differ
|
||||
⇒ had NUL). All vars local.
|
||||
- Wired in `bin/netshift` `download_subscription_into_cache` right after a
|
||||
successful `download_subscription` (now ~:590-591), BEFORE
|
||||
`validate_subscription_file`: call `maybe_gunzip_subscription_file`, then if
|
||||
`subscription_body_is_binary` log a warn and `continue` (inside the per-UA
|
||||
`while read` loop → falls to next UA, same flow as a validation failure). The
|
||||
file_size/debug log stays AFTER so the logged size reflects the decompressed
|
||||
body. mv/cache-persist below unchanged.
|
||||
- NO Accept-Encoding/wget change, NO Makefile DEPENDS change (gzip/gunzip/zcat
|
||||
are busybox built-ins), NO schema/ports/marks/frontend. zstd/unzstd are NOT on
|
||||
device — gzip-only by design; deflate/zstd are a future task if panels send
|
||||
them.
|
||||
- Smoke: extended the `test_subscription` fb harness (sources helpers via the
|
||||
facade) with synthetic-only fixtures: caseP gzip→text (cmp byte-equal),
|
||||
text-passthrough (cmp unchanged), gzip→validate (validate_subscription_file
|
||||
passes); caseQ `printf 'abc\000def'`→binary true, plain text→false. The smoke
|
||||
container has gzip (tests/Dockerfile apk add gzip). No new test_* fn — folded
|
||||
into existing test_subscription, so no main()/case/usage registration needed.
|
||||
- Gates: shellcheck -S error clean on bin + all libs + install.sh; `smoke-tests
|
||||
all` 174 passed / 0 failed (aggregate counter folds the 5 new tokens into the
|
||||
test_subscription header group — count unchanged, the 5 fb-caseP/Q :OK lines
|
||||
print explicitly in the `subscription` run).
|
||||
|
||||
## task-047 — latest-tag jq parse (false "Outdated" fix)
|
||||
|
||||
- `updates_netshift_latest_tag` (`updater.sh:~1635`) used
|
||||
`grep '"tag_name":' | head -n1 | cut -d'"' -f4`. That ONLY works on
|
||||
pretty-printed JSON. On MINIFIED GitHub JSON (whole object on one line, `"url"`
|
||||
before `"tag_name"`), grep matches the whole line and `cut -f4` returns the
|
||||
FIRST key's value = the release `"url"` → false "outdated" + self-update
|
||||
downloads a garbage "version". Fix: `jq -r '.tag_name // empty'`
|
||||
(format-independent; `// empty` → empty on rate-limit/error objects; no
|
||||
Oniguruma). jq is a hard dep (used 23× in updater.sh) and the sibling
|
||||
sing-box-extended path already used `.tag_name`.
|
||||
- LESSON (whitespace-fragile grep|cut on JSON): never field-position-`cut` JSON
|
||||
that may be minified. Prefer jq when it's already a dep.
|
||||
- New smoke test `test_netshift_latest_tag` (token `latesttag`): driver sources
|
||||
updater.sh, stubs ONLY the network boundary `updates_http_get_once` (NOT the
|
||||
parse fn), runs the REAL `updates_netshift_latest_tag`. 4 cases: minified→tag
|
||||
(regression guard), pretty→tag, rate-limit→empty+nonzero, e2e via
|
||||
`updates_check_netshift`→status "latest". Registered in main() all)+case+usage
|
||||
+docker-compose.yml comment.
|
||||
- GOTCHA: under harness `set -e`, a stub-driver `ash "$drv"` that returns
|
||||
non-zero (the rate-limit case) aborts the whole suite — guard rc capture with
|
||||
`ash ... && printf 0 >rc || printf $? >rc`.
|
||||
- Guard self-proven: restoring the old grep|cut line made
|
||||
`latesttag-minified-returns-tag-not-url` FAIL (returned the .../releases/<id>
|
||||
url), then restored jq. Gates: shellcheck -S error clean (bin + libs +
|
||||
install.sh); `smoke-tests all` 174→178 passed / 0 failed (+4 latesttag).
|
||||
|
||||
## task-048: scalar `option subscription_url` read-fallback + option->list migration
|
||||
|
||||
- Root cause (PROVEN on hardware, Cudy WR3000E / OWRt 25.12.4 / NetShift 0.8.9):
|
||||
a section storing `option subscription_url '<url>'` (legacy / CLI /
|
||||
podkop-migrated configs) made `get_subscription_urls_for_section`
|
||||
(`bin/netshift`) return EMPTY → `has_outbound_section` false → "Outbound
|
||||
section not found. Aborted." → sing-box never starts → whole chain down (no nft
|
||||
table, FakeIP 127.0.0.42:53 refused). `config_list_foreach` iterates ONLY UCI
|
||||
`list` values; over a scalar `option` it iterates NOTHING. `config_get` reads
|
||||
the scalar. Regression from task-022 (multi-URL feature made subscription_url a
|
||||
list / form.DynamicList); the task-022 memory note "a lone legacy option reads
|
||||
as a 1-element list — NO migration code" was the FALSE assumption that shipped
|
||||
the bug. EVERY subscription-URL reader funnels through this one helper.
|
||||
- Fix 1 (load-bearing, single source): in `get_subscription_urls_for_section`,
|
||||
AFTER the `config_list_foreach`, if `SUBSCRIPTION_URLS_COLLECTED` is still
|
||||
empty, `config_get scalar_url "$section" "subscription_url"` and (if non-empty)
|
||||
`_collect_subscription_url_handler "$scalar_url"` (reuse the handler so
|
||||
dedup/format stays identical). All new vars `local`. Must stand alone on
|
||||
read-only fs / when migration is skipped. Corrected the false comment at
|
||||
`section_has_configured_outbound` (subscription branch) and the collector
|
||||
header.
|
||||
- Fix 2 (hygiene, idempotent): `migrate_legacy_subscription_url_option` +
|
||||
`_migrate_legacy_subscription_url_option_handler` (config_foreach callback).
|
||||
Detects the broken shape robustly: LIST read empty AND scalar config_get
|
||||
non-empty (an already-correct list is never touched). Rewrites via
|
||||
`uci -q delete netshift.<sec>.subscription_url` then the `uci_add_list netshift
|
||||
"$sec" subscription_url "$url"` SHELL HELPER (from /lib/functions.sh) — NOT the
|
||||
`uci add_list "key=value"` CLI form, which splits on the first `=` and SILENTLY
|
||||
LOSES query-string URLs (`?token=abc&x=1`) [code-review BLOCKER B1, reproduced
|
||||
on hardware: CLI add_list rc=1, list empty, scalar already deleted => URL lost
|
||||
on disk]. On add_list FAILURE the else branch `uci_set`s the scalar back so a
|
||||
failed migration never leaves the section with NO url (and the flag stays 0 =>
|
||||
no commit => uncommitted in-memory delete never persists; on-disk URL survives).
|
||||
sets a module-level flag `SUBSCRIPTION_URL_OPTION_MIGRATED`; a SINGLE `uci commit netshift` +
|
||||
`config_load "$NETSHIFT_CONFIG"` only if anything changed (mirrors the
|
||||
:956/:1099 commit+reload). NEVER exits — uci failures log `warn` and continue
|
||||
(the read-fallback covers correctness). Invoked ONCE at the TOP of `start_main`
|
||||
BEFORE `check_requirements` (which reads URLs via has_outbound_section), AFTER
|
||||
the file-scope `config_load`.
|
||||
- Smoke landmine confirmed (again): the multi-url `test_subscription` harness
|
||||
STUBS `config_list_foreach` (feeds MU_URLS) and does NOT touch real UCI / does
|
||||
NOT stub `config_get` — so it can NEVER catch this bug (it bypasses the broken
|
||||
primitive). The regression guard MUST be a REAL-UCI test that `config_load`s a
|
||||
fixture and runs the SHIPPED (awk-extracted) functions.
|
||||
- New top-level smoke test `test_sub_url_option` (alias `suburlopt`). 12 tokens:
|
||||
`suburlopt:scalar-read` (regression guard — empty before fix),
|
||||
`:scalar-hasoutbound`, `:list-read` (no-regression), `:migrate-flag`,
|
||||
`:migrate-value`, `:migrate-islist`, `:migrate-idempotent`,
|
||||
`:migrate-idempotent-value`, PLUS the `=`-URL [B1] guards
|
||||
`:migrate-equrl-preserved`, `:migrate-equrl-islist`, `:migrate-equrl-single`
|
||||
(asserts exactly 1 list element), `:migrate-idempotent-equrl` — fixture URL
|
||||
`https://example.com/sub?token=abc&x=1`. The driver output is parsed in the
|
||||
CURRENT shell (temp file + `while read < "$out"`, NO pipe) so the tokens
|
||||
ACTUALLY GATE CI (fixed the harness-wide piped-while counter-quirk for this
|
||||
test). Migration is tested against a throwaway
|
||||
`/etc/config/netshift` (the function hardcodes the `netshift` config name) with
|
||||
NETSHIFT_CONFIG=netshift; the caller backs up + restores any real one. Skips
|
||||
cleanly if /lib/functions.sh or uci unavailable. Registered in all)+case alias+
|
||||
"Available:" usage line + docker-compose.yml comment. Synthetic
|
||||
`https://example.com/sub` ONLY (operator privacy rule: a user dump leaked a
|
||||
real URL — never write a real subscription URL/host/id anywhere).
|
||||
- Self-prove DONE: removing the read-fallback block made `suburlopt:scalar-read`
|
||||
go empty (its `:OK` disappeared) and `suburlopt:scalar-hasoutbound:FAIL`
|
||||
appeared; restored and all tokens green again. Also self-proved [B1]: the old
|
||||
`key=value` CLI add_list made `:migrate-equrl-preserved` FAIL (URL lost);
|
||||
`uci_add_list` helper fixed it.
|
||||
- Whole-chain verified in-container: `has_outbound_section` returns TRUE for an
|
||||
option-shaped config → requirements gate passes → config gen + sing-box check
|
||||
proceed.
|
||||
- PRE-EXISTING (NOT mine): `test_rejected_hash` rh-case1/2/6 fail on the BASELINE
|
||||
bin/netshift too (verified via git stash) — an existing container/env issue,
|
||||
unrelated to task-048.
|
||||
- Gates: shellcheck -S error clean (bin + libs + install.sh); `smoke-tests all`
|
||||
178→190 passed / 0 failed (the +12 is the 12 `suburlopt` tokens, which now
|
||||
count because the test parses driver output in the CURRENT shell, NOT a pipe).
|
||||
NO sacred constant/port/mark/path changed; UCI schema only normalizes an
|
||||
existing key's representation (option→list, back-compat).
|
||||
- code-review round 2: APPROVED WITH CONDITIONS — [B1]/[S1]/[M1] all resolved;
|
||||
the only condition was fixing THIS stale memory note (done).
|
||||
|
||||
## task-049: avoid api.github.com rate-limit via github.com redirect (curl)
|
||||
|
||||
- Anonymous api.github.com = 60 req/HOUR/IP; CGNAT/shared-IP/shared-VPN routers
|
||||
share that budget → frequent "API rate limit exceeded". LEVER (proven on HW):
|
||||
github.com/<repo>/releases/latest is the github.com FRONTEND (NOT the API) and
|
||||
302-redirects to /releases/tag/<tag>; releases/download/<tag>/<asset> 302s to
|
||||
the CDN. Neither hits the rate-limited API.
|
||||
- New constants (constants.sh, repo slug ONLY here): NETSHIFT_REPO_RELEASES_LATEST_URL
|
||||
(.../releases/latest), NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE (.../releases/download).
|
||||
Kept NETSHIFT_RELEASE_API_URL as the fallback.
|
||||
- New STUBBABLE resolver `updates_github_resolve_redirect <url>` (updater.sh):
|
||||
`command -v curl || return 1; curl -sI -o /dev/null -w '%{redirect_url}'
|
||||
--connect-timeout 5 -m 15 -A 'netshift-updater' "$url"`. busybox wget is
|
||||
STRIPPED (no -S/header read) so tag extraction MUST be curl; curl is a hard dep.
|
||||
- `updates_netshift_latest_tag` rewrite: PRIMARY resolve redirect, parse with
|
||||
`case "$redirect" in */releases/tag/*) tag="${redirect##*/releases/tag/}";
|
||||
case "$tag" in ''|*/*) tag="" ;; esac ;; *) tag="" ;; esac` (NO Oniguruma) —
|
||||
a trailing-slash redirect leaves a `/` in tag → rejected → empty → fallback.
|
||||
FALLBACK = the task-047 api.github.com + `jq -r '.tag_name // empty'` path,
|
||||
kept intact. Bare-tag/non-zero contract preserved (feeds updates_check_netshift
|
||||
+ self-update worker).
|
||||
- `updates_netshift_asset_filename <pkg> <tag> <ext>` single-source naming helper:
|
||||
i18n = `<pkg>-<tag>.<ext>` (no -r1); core/luci ipk = `<pkg>-<tag>-r1-all.ipk`,
|
||||
apk = `<pkg>-<tag>-r1.apk`. `_updates_self_update_download_assets` now resolves
|
||||
the tag and builds deterministic `$DOWNLOAD_BASE/<tag>/<filename>` URLs (core+luci
|
||||
always, i18n only if updates_pkg_is_installed), downloads via updates_http_get_once
|
||||
(follows the 302 to CDN), got_core=1 when core `-s "$dest"`. OLD api-JSON grep -o
|
||||
loop KEPT verbatim as the else branch when tag unresolved.
|
||||
- install.sh: added RELEASES_LATEST_REDIRECT + RELEASES_DOWNLOAD_BASE literals
|
||||
(install.sh has its own REPO, not constants.sh). PRIMARY: curl -sI redirect →
|
||||
case/param-expansion tag → deterministic releases/download/<tag>/<asset> URLs
|
||||
(core+luci, RU i18n if pkg_is_installed). FALLBACK: existing API scrape + the
|
||||
"API rate limit" message kept intact. Factored the retry-download into a new
|
||||
`download_release_asset url filename` helper reused by both paths. name-prefix
|
||||
install loop semantics unchanged.
|
||||
- GOTCHA: the EXISTING test_netshift_latest_tag driver had to ALSO stub
|
||||
`updates_github_resolve_redirect() { printf ''; }` — else the new primary would
|
||||
shell out to real curl in CI (network) and bypass the API path that test targets.
|
||||
- EXTENDED PATH UNTOUCHED: updates_fetch_sing_box_extended_releases
|
||||
(releases?per_page=30) + updates_extended_release_* — they need the releases LIST
|
||||
(draft/prerelease/per-arch) a redirect can't give. Left on API + proxy-fallback.
|
||||
- New smoke test `test_github_redirect_tag` (alias `ghredirect`, 6 tokens): stubs
|
||||
the resolver + updates_http_get_once, parses driver output in the CURRENT shell
|
||||
(gates). tag-from-redirect, tag-trailing-slash-rejected (→fallback empty),
|
||||
nonmatch-falls-back (login URL→API stub→tag), ratelimit-empty (curl-absent +
|
||||
rate-limit object→empty+nonzero), asset-ipk, asset-apk. Registered all 5 points.
|
||||
- SELF-PROVEN: `if false && [ -n "$tag" ]` on the primary return made
|
||||
ghredirect:tag-from-redirect FAIL (5/1), restored→6/0.
|
||||
- Gates: shellcheck -S error clean (bin+libs+install.sh); `smoke-tests all`
|
||||
190→196 passed / 0 failed (+6 ghredirect). NO sacred constant/port/mark/path/
|
||||
schema/frontend change.
|
||||
|
||||
## task-050: "Fastest" cross-group urltest of urltests (grouping-on default)
|
||||
|
||||
- New constant `SB_SUBSCRIPTION_FASTEST_GROUP_TAG="⚡ Fastest"` (constants.sh,
|
||||
sing-box Outbounds group, valid UTF-8) — single source for the top-level
|
||||
cross-group urltest tag. Per-group tags stay the inline literal
|
||||
`"$group_key Fastest"` (NOT a constant; the spec only added the cross-group
|
||||
one). Lightning glyph is deliberately distinct from a per-group `<flag>
|
||||
Fastest` so the auto choice is tellable apart in the dashboard.
|
||||
- Grouped branch (bin/netshift `configure_outbound_handler`, subscription
|
||||
`group_mode != off`): after the per-group urltest loop fills
|
||||
`selector_outbounds_json` with ONLY group tags (before ungrouped is
|
||||
appended), capture `group_tags_json="$selector_outbounds_json"` +
|
||||
`group_tags_count=$(... | jq -r 'length')`. If `>= 2`:
|
||||
`fastest_tag=$(sing_box_get_unique_outbound_tag "$config" "$SB_..._TAG")`,
|
||||
add the nested urltest via `sing_box_cm_add_urltest_outbound "$config"
|
||||
"$fastest_tag" "$group_tags_json" <section's url/interval/tolerance>` (reuse
|
||||
the SECTION's urltest knobs — user-tunable, no hardcoded aggressive
|
||||
interval), prepend with `jq -acn --arg t --argjson rest '[$t] + $rest'`, and
|
||||
set `selector_default="$fastest_tag"`. EDGE: `==1` group → skip nest (lone
|
||||
group already IS fastest; default = `.[0]`); `==0` → no nest, default =
|
||||
first ungrouped. Never emits an empty-member urltest. New locals
|
||||
`group_tags_json group_tags_count fastest_tag`. Existing fatal+exit 1 guards
|
||||
intact. No Oniguruma.
|
||||
- WHOLE-CHAIN proven: `sing-box check` ACCEPTS a urltest whose members are
|
||||
other urltest tags (nesting works) — asserted live in-container in the new
|
||||
test. Runtime-contract impact NONE (pure outbound-tree shape).
|
||||
- FRONTEND: ZERO change needed. The subscription dashboard
|
||||
(`getDashboardSections.ts` `proxy_config_type === 'subscription'`) maps the
|
||||
LIVE `selector.value.all` and shows each member's `value.name` VERBATIM,
|
||||
EXCEPT it maps ONLY the legacy `${section}-urltest-out` code to `_('Fastest')`
|
||||
(`isLegacyFastest`). The new "⚡ Fastest" gets a DEDUPED synthetic tag (code =
|
||||
the tag, NOT `-urltest-out`), so it renders raw `⚡ Fastest` — same treatment
|
||||
as the per-group `🇷🇺 Fastest` tags. Urltests sort first → it leads the list
|
||||
and is selectable automatically. main.js untouched (correct).
|
||||
- TEST `test_fastest_group` (alias `fastest`, after test_subscription; 6
|
||||
tokens). The grouped branch is INLINE shell (not a function), so the driver
|
||||
awk-extracts the WHOLE `if [ "$group_mode" != "off" ]; then ... else ... fi`
|
||||
region VERBATIM (from the `if`-opener through the off-branch's
|
||||
`"$urltest_tag" "true")"` line + the following `fi`; awk q-style: set
|
||||
`seen_else_end` on the off selector line, exit on the next `^\s*fi$`) and
|
||||
wraps it in a driver `_grouped_branch()` so the leading `if ...; then local`
|
||||
is valid. Sources real constants.sh + sing_box_config_manager.sh; awk-extracts
|
||||
`sing_box_get_unique_outbound_tag` + `sing_box_build_subscription_groups`
|
||||
verbatim; stubs `get_outbound_tag_by_section`/`log`. Synthetic flag tags
|
||||
built by codepoint (RU=flag(17,20), DE=flag(3,4)) + a `plain-node` ungrouped
|
||||
+ shadowsocks/aes-256-gcm so `sing-box check` accepts. Asserts: (a) one
|
||||
top-level urltest tagged the constant whose outbounds == [ru,de] group tags;
|
||||
(b) selector default == fastest + outbounds == [fastest, ru, de,
|
||||
plain-node]; (c) live `sing-box check` passes WITH the nest; (d) groups==1 →
|
||||
no nested urltest, default = lone group; (e) off → flat 1 urltest, no
|
||||
fastest tag, default == `<section>-urltest-out`. Parsed in the CURRENT shell
|
||||
via per-run `ash "$work/runN.sh" > out.json` (each run sources the spliced
|
||||
driver) → tokens GATE. Registered all 5 points (all)/case alias/usage/compose
|
||||
comment).
|
||||
- SPLICE PATTERN (reusable for inline-region extraction): write the driver with
|
||||
a placeholder line `EXTRACT_GROUPED`, then rebuild it as
|
||||
`{ sed '/MARK/q' drv | sed '$d'; cat region; sed -n '/MARK/,$p' drv | sed
|
||||
'1d'; } > drv.spliced; mv`. Replaces exactly the one placeholder line with the
|
||||
arbitrary-content region (no s/// escaping hazard).
|
||||
- SELF-PROVED twice: (1) comment out the prepend line → only
|
||||
`fastest-selector-default-membership` FAILs; (2) change the guard to `-ge 99`
|
||||
(never nest) → BOTH `fastest-nested-urltest-members` AND
|
||||
`-selector-default-membership` FAIL. Restored → 6/0.
|
||||
- Gates: shellcheck -S error clean (bin + libs + install.sh). `smoke-tests all`
|
||||
196→202 passed / 0 failed (+6 fastest, all counted — current-shell parse).
|
||||
Pre-existing `rh-case1/2/6:FAIL` red marks persist (documented task-031/048
|
||||
env quirk; suite EXIT=0). PRIVACY: synthetic codepoint-built flag tags only,
|
||||
no real subscription URL/host/id anywhere. NO sacred constant/port/mark/path/
|
||||
UCI-schema/frontend/main.js change.
|
||||
|
||||
## task-051 — text-list selector/urltest (selector_text / urltest_text)
|
||||
- TWO new proxy_config_type values + TWO scalar UCI options
|
||||
(`selector_proxy_links_text` / `urltest_proxy_links_text`): a multi-line
|
||||
textarea blob, one link per line. Behaviour identical to the LIST-based
|
||||
`selector`/`urltest`; ONLY the input shape differs.
|
||||
- Refactored the duplicated per-link member-build loop (was inline in `selector)`
|
||||
and `urltest)`) into ONE shared helper
|
||||
`_build_proxy_member_outbounds <section> <links_blob> <udp_over_tcp> <label>`.
|
||||
It mutates GLOBAL `$config` in place (documented, same discipline the
|
||||
subscription in-shell loop uses) and reports via TWO globals the caller reads:
|
||||
`_member_outbound_tags` (comma-joined) + `_member_default_outbound` (first
|
||||
member = selector default). Used by all FOUR branches. Echo-and-reassign for
|
||||
the FINAL cm_add_selector/urltest stays in each branch.
|
||||
- LINE PARSING: `for link in $blob` already word-splits on IFS incl. newlines.
|
||||
KEY GOTCHA: a BLANK line is collapsed by IFS BEFORE the loop body, so it does
|
||||
NOT consume the `$i` index — members are numbered by NON-blank tokens only
|
||||
(blank between link2 and link3 ⇒ ss is `<section>-3`, not `-4`). CRLF: strip a
|
||||
trailing CR per link with `cr="$(printf '\r')"; link="${link%"$cr"}"` then skip
|
||||
empties. A CR buried in a query string is harmless (facade tolerates it); put
|
||||
the CRLF on a bare `ss://host:port` line to make CR-strip a DECISIVE gate.
|
||||
- `section_has_configured_outbound` (the fn `_check_outbound_section` delegates
|
||||
to) got `selector_text)`/`urltest_text)` cases returning 0 when the text option
|
||||
is non-empty; added both option names to the "Outbound section not found …
|
||||
missing …" error string.
|
||||
- Empty links = `fatal`+`exit 1` (mirrors existing branches verbatim);
|
||||
all-unsupported = `mark_section_outbound_unavailable` + clear error.
|
||||
- TEST `test_text_list_outbound` (alias `textlist`): awk-extracts the SHIPPED
|
||||
helper+handler+marker+`section_has_configured_outbound` chain, table-driven
|
||||
config_get stub, real `sing-box check` via `check_full`. GATING FIX vs
|
||||
`test_unsupported_skip` (which uses `cmd | while read` ⇒ counters lost in
|
||||
subshell): write driver output to a FILE, then `while read … < file` in the
|
||||
CURRENT shell so pass/fail mutate real PASS/FAIL and the suite gates.
|
||||
SELF-PROVED: comment out the CR-strip line ⇒ `tl-seltxt-ss-crlf-present`
|
||||
(+ downstream members-clean) FAIL, suite EXIT=1. Restored ⇒ 16/0.
|
||||
- Gates: shellcheck -S error CLEAN (bin+libs+install.sh+tests). `smoke-tests all`
|
||||
202→218 passed / 0 failed (+16). PRIVACY: synthetic vless://uuid@…/ss://b64@…
|
||||
/tuic:// placeholders only — no real link/sub data. NO sacred
|
||||
constant/port/mark/path change; UCI schema ADDITIVE + back-compat; FRONTEND
|
||||
untouched (parallel agent owns section.js/TS/i18n/main.js).
|
||||
|
||||
BIN
docs/screenshot.png
Normal file
BIN
docs/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 303 KiB |
@ -38,21 +38,21 @@
|
||||
"call": "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.",
|
||||
"key": "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:125"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip",
|
||||
"key": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:670"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip",
|
||||
"key": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:758"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -74,7 +74,7 @@
|
||||
"call": "Allow insecure TLS for subscription fetch",
|
||||
"key": "Allow insecure TLS for subscription fetch",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -88,28 +88,35 @@
|
||||
"call": "Applicable for SOCKS and Shadowsocks proxy",
|
||||
"key": "Applicable for SOCKS and Shadowsocks proxy",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "At least one proxy link must be specified.",
|
||||
"key": "At least one proxy link must be specified.",
|
||||
"places": [
|
||||
"src/validators/validateProxyUrlList.ts:39"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "At least one valid domain must be specified. Comments-only content is not allowed.",
|
||||
"key": "At least one valid domain must be specified. Comments-only content is not allowed.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.",
|
||||
"key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:819"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Auto",
|
||||
"key": "Auto",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -161,6 +168,20 @@
|
||||
"src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:57"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "By country flag",
|
||||
"key": "By country flag",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:202"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "By name prefix",
|
||||
"key": "By name prefix",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:203"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Cache File Path",
|
||||
"key": "Cache File Path",
|
||||
@ -258,7 +279,7 @@
|
||||
"call": "Community Lists",
|
||||
"key": "Community Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -350,14 +371,14 @@
|
||||
"call": "Custom domains",
|
||||
"key": "Custom domains",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Custom subnets",
|
||||
"key": "Custom subnets",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -435,15 +456,15 @@
|
||||
"call": "Disabled",
|
||||
"key": "Disabled",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:674",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:762"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Disables TLS certificate verification when downloading the subscription.",
|
||||
"key": "Disables TLS certificate verification when downloading the subscription.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -471,7 +492,7 @@
|
||||
"call": "DNS over HTTPS (DoH)",
|
||||
"key": "DNS over HTTPS (DoH)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:542",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48"
|
||||
]
|
||||
},
|
||||
@ -479,7 +500,7 @@
|
||||
"call": "DNS over TLS (DoT)",
|
||||
"key": "DNS over TLS (DoT)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49"
|
||||
]
|
||||
},
|
||||
@ -487,7 +508,7 @@
|
||||
"call": "DNS Protocol Type",
|
||||
"key": "DNS Protocol Type",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:539",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45"
|
||||
]
|
||||
},
|
||||
@ -502,7 +523,7 @@
|
||||
"call": "DNS Server",
|
||||
"key": "DNS Server",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:553",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58"
|
||||
]
|
||||
},
|
||||
@ -531,7 +552,7 @@
|
||||
"call": "Domain Resolver",
|
||||
"key": "Domain Resolver",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -582,15 +603,15 @@
|
||||
"call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).",
|
||||
"key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:241"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Dynamic List",
|
||||
"key": "Dynamic List",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:763"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -604,14 +625,14 @@
|
||||
"call": "Enable built-in DNS resolver for domains handled by this section",
|
||||
"key": "Enable built-in DNS resolver for domains handled by this section",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:529"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Enable DNS resolve to get real IP when routing",
|
||||
"key": "Enable DNS resolve to get real IP when routing",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:996"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -632,7 +653,7 @@
|
||||
"call": "Enable Mixed Proxy",
|
||||
"key": "Enable Mixed Proxy",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:965"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -646,7 +667,7 @@
|
||||
"call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies",
|
||||
"key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:966"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -667,98 +688,98 @@
|
||||
"call": "Enter complete outbound configuration in JSON format",
|
||||
"key": "Enter complete outbound configuration in JSON format",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:99"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:101"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //",
|
||||
"key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:712"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Enter domain names without protocols, e.g. example.com or sub.example.com",
|
||||
"key": "Enter domain names without protocols, e.g. example.com or sub.example.com",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses",
|
||||
"key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:773"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 1 minute",
|
||||
"key": "Every 1 minute",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 12 hours",
|
||||
"key": "Every 12 hours",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:187"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 3 hours",
|
||||
"key": "Every 3 hours",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 3 minutes",
|
||||
"key": "Every 3 minutes",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 30 minutes",
|
||||
"key": "Every 30 minutes",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 30 seconds",
|
||||
"key": "Every 30 seconds",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:365"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 5 minutes",
|
||||
"key": "Every 5 minutes",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:368"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every 6 hours",
|
||||
"key": "Every 6 hours",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every day",
|
||||
"key": "Every day",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:188"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Every hour",
|
||||
"key": "Every hour",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:182"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -779,7 +800,7 @@
|
||||
"call": "Exclude servers by keyword",
|
||||
"key": "Exclude servers by keyword",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:240"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -832,7 +853,7 @@
|
||||
"call": "Fully Routed IPs",
|
||||
"key": "Fully Routed IPs",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:937"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -853,28 +874,21 @@
|
||||
"call": "Global Proxy",
|
||||
"key": "Global Proxy",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:463"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Group by countries",
|
||||
"key": "Group by countries",
|
||||
"call": "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters.",
|
||||
"key": "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag",
|
||||
"key": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:197"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "How often to automatically update the subscription",
|
||||
"key": "How often to automatically update the subscription",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -888,7 +902,7 @@
|
||||
"call": "Include servers by keyword",
|
||||
"key": "Include servers by keyword",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:228"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1314,7 +1328,7 @@
|
||||
"call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.",
|
||||
"key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1332,6 +1346,13 @@
|
||||
"src/netshift/tabs/manager/initController.ts:103"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Line",
|
||||
"key": "Line",
|
||||
"places": [
|
||||
"src/validators/validateProxyUrlList.ts:31"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "List Update Frequency",
|
||||
"key": "List Update Frequency",
|
||||
@ -1357,14 +1378,14 @@
|
||||
"call": "Local Domain Lists",
|
||||
"key": "Local Domain Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Local Subnet Lists",
|
||||
"key": "Local Subnet Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:865"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1406,7 +1427,7 @@
|
||||
"call": "Mixed Proxy Port",
|
||||
"key": "Mixed Proxy Port",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:979"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1420,7 +1441,7 @@
|
||||
"call": "Must be a number in the range of 50 - 1000",
|
||||
"key": "Must be a number in the range of 50 - 1000",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:405"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1462,7 +1483,7 @@
|
||||
"call": "Network Interface",
|
||||
"key": "Network Interface",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1509,11 +1530,25 @@
|
||||
"src/netshift/tabs/diagnostic/diagnostic.store.ts:91"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Number of leading characters of each proxy name to group by.",
|
||||
"key": "Number of leading characters of each proxy name to group by.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:213"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Off",
|
||||
"key": "Off",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:201"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Only one section can be global at a time.",
|
||||
"key": "Only one section can be global at a time.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1527,14 +1562,14 @@
|
||||
"call": "Outbound Config",
|
||||
"key": "Outbound Config",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:60"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Outbound Configuration",
|
||||
"key": "Outbound Configuration",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:98"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:100"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1591,6 +1626,13 @@
|
||||
"src/netshift/tabs/diagnostic/diagnostic.store.ts:139"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Prefix length",
|
||||
"key": "Prefix length",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:212"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT.",
|
||||
"key": "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT.",
|
||||
@ -1602,7 +1644,7 @@
|
||||
"call": "Proxy Configuration URL",
|
||||
"key": "Proxy Configuration URL",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:66"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1623,28 +1665,28 @@
|
||||
"call": "Regional options cannot be used together",
|
||||
"key": "Regional options cannot be used together",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:610"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Remote Domain Lists",
|
||||
"key": "Remote Domain Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:889"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Remote Subnet Lists",
|
||||
"key": "Remote Subnet Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Resolve real IP for routing",
|
||||
"key": "Resolve real IP for routing",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:995"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1658,7 +1700,7 @@
|
||||
"call": "Route all unmatched traffic through this section's outbound.",
|
||||
"key": "Route all unmatched traffic through this section's outbound.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:464"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1742,7 +1784,7 @@
|
||||
"call": "Russia inside restrictions",
|
||||
"key": "Russia inside restrictions",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1763,7 +1805,7 @@
|
||||
"call": "Select a predefined list for routing",
|
||||
"key": "Select a predefined list for routing",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1798,14 +1840,14 @@
|
||||
"call": "Select network interface for VPN connection",
|
||||
"key": "Select network interface for VPN connection",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:482"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Select or enter DNS server address",
|
||||
"key": "Select or enter DNS server address",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:554",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59"
|
||||
]
|
||||
},
|
||||
@ -1827,7 +1869,7 @@
|
||||
"call": "Select the DNS protocol type for the domain resolver",
|
||||
"key": "Select the DNS protocol type for the domain resolver",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1865,11 +1907,25 @@
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:55"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Selector (text list)",
|
||||
"key": "Selector (text list)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Selector Proxy Links",
|
||||
"key": "Selector Proxy Links",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:252"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Selector Proxy Links (one per line)",
|
||||
"key": "Selector Proxy Links (one per line)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:278"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1912,7 +1968,7 @@
|
||||
"call": "Sing-box",
|
||||
"key": "Sing-box",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157",
|
||||
"src/netshift/tabs/dashboard/initController.ts:354"
|
||||
]
|
||||
},
|
||||
@ -1990,29 +2046,29 @@
|
||||
"call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route",
|
||||
"key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:938"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Specify remote URLs to download and use domain lists",
|
||||
"key": "Specify remote URLs to download and use domain lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:890"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Specify remote URLs to download and use subnet lists",
|
||||
"key": "Specify remote URLs to download and use subnet lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Specify the path to the list file located on the router filesystem",
|
||||
"key": "Specify the path to the list file located on the router filesystem",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:842",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:866"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2034,7 +2090,7 @@
|
||||
"key": "Subscription",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:21",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:59"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2055,21 +2111,28 @@
|
||||
"call": "Subscription format",
|
||||
"key": "Subscription format",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:150"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Subscription grouping",
|
||||
"key": "Subscription grouping",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:196"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Subscription Update Interval",
|
||||
"key": "Subscription Update Interval",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Subscription URLs",
|
||||
"key": "Subscription URLs",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2132,8 +2195,8 @@
|
||||
"call": "Text List",
|
||||
"key": "Text List",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:764"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2147,28 +2210,28 @@
|
||||
"call": "The interval between connectivity tests",
|
||||
"key": "The interval between connectivity tests",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:363"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "The maximum difference in response times (ms) allowed when comparing servers",
|
||||
"key": "The maximum difference in response times (ms) allowed when comparing servers",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "The URL used to test server connectivity",
|
||||
"key": "The URL used to test server connectivity",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:413"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "This is a security trade-off: an attacker could intercept the fetch.",
|
||||
"key": "This is a security trade-off: an attacker could intercept the fetch.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2217,7 +2280,7 @@
|
||||
"call": "UDP (Unprotected DNS)",
|
||||
"key": "UDP (Unprotected DNS)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50"
|
||||
]
|
||||
},
|
||||
@ -2225,7 +2288,7 @@
|
||||
"call": "UDP over TCP",
|
||||
"key": "UDP over TCP",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:452"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2319,39 +2382,53 @@
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:56"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "URLTest (text list)",
|
||||
"key": "URLTest (text list)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "URLTest Check Interval",
|
||||
"key": "URLTest Check Interval",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:362"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "URLTest Proxy Links",
|
||||
"key": "URLTest Proxy Links",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:307"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "URLTest Proxy Links (one per line)",
|
||||
"key": "URLTest Proxy Links (one per line)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:333"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "URLTest Testing URL",
|
||||
"key": "URLTest Testing URL",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:412"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "URLTest Tolerance",
|
||||
"key": "URLTest Tolerance",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:378"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Use only for IP-host panels that serve an invalid or self-signed certificate.",
|
||||
"key": "Use only for IP-host panels that serve an invalid or self-signed certificate.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2365,35 +2442,35 @@
|
||||
"call": "Use with Exclusion sections to route specific domains directly.",
|
||||
"key": "Use with Exclusion sections to route specific domains directly.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "User Domains",
|
||||
"key": "User Domains",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "User Domains List",
|
||||
"key": "User Domains List",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "User Subnets",
|
||||
"key": "User Subnets",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:772"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "User Subnets List",
|
||||
"key": "User Subnets List",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:799"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2425,8 +2502,8 @@
|
||||
"call": "Validation errors:",
|
||||
"key": "Validation errors:",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2455,30 +2532,38 @@
|
||||
"call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
|
||||
"key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:253",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:308"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line",
|
||||
"key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:279",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:334"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Warning: %s cannot be used together with %s. Previous selections have been removed.",
|
||||
"key": "Warning: %s cannot be used together with %s. Previous selections have been removed.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.",
|
||||
"key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:631"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "When enabled, traffic not matching any other section's lists will go through this proxy.",
|
||||
"key": "When enabled, traffic not matching any other section's lists will go through this proxy.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2492,14 +2577,14 @@
|
||||
"call": "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format.",
|
||||
"key": "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format.",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Xray JSON (Happ)",
|
||||
"key": "Xray JSON (Happ)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:156"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: NETSHIFT\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-11 17:31+0300\n"
|
||||
"PO-Revision-Date: 2026-06-11 17:31+0300\n"
|
||||
"POT-Creation-Date: 2026-06-13 11:14+0300\n"
|
||||
"PO-Revision-Date: 2026-06-13 11:14+0300\n"
|
||||
"Last-Translator: yandexru45 <>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
@ -36,15 +36,15 @@ msgstr ""
|
||||
msgid "Active Connections"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:125
|
||||
msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:670
|
||||
msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:758
|
||||
msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip"
|
||||
msgstr ""
|
||||
|
||||
@ -57,7 +57,7 @@ msgstr ""
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165
|
||||
msgid "Allow insecure TLS for subscription fetch"
|
||||
msgstr ""
|
||||
|
||||
@ -65,19 +65,23 @@ msgstr ""
|
||||
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453
|
||||
msgid "Applicable for SOCKS and Shadowsocks proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648
|
||||
#: src/validators/validateProxyUrlList.ts:39
|
||||
msgid "At least one proxy link must be specified."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730
|
||||
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:819
|
||||
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
|
||||
@ -109,6 +113,14 @@ msgstr ""
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:202
|
||||
msgid "By country flag"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:203
|
||||
msgid "By name prefix"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517
|
||||
msgid "Cache File Path"
|
||||
msgstr ""
|
||||
@ -166,7 +178,7 @@ msgstr ""
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576
|
||||
msgid "Community Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -219,11 +231,11 @@ msgstr ""
|
||||
msgid "Currently unavailable"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669
|
||||
msgid "Custom domains"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757
|
||||
msgid "Custom subnets"
|
||||
msgstr ""
|
||||
|
||||
@ -268,12 +280,12 @@ msgstr ""
|
||||
msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:674
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:762
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166
|
||||
msgid "Disables TLS certificate verification when downloading the subscription."
|
||||
msgstr ""
|
||||
|
||||
@ -289,17 +301,17 @@ msgstr ""
|
||||
msgid "DNS outbound section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:542
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48
|
||||
msgid "DNS over HTTPS (DoH)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49
|
||||
msgid "DNS over TLS (DoT)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:539
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45
|
||||
msgid "DNS Protocol Type"
|
||||
msgstr ""
|
||||
@ -308,7 +320,7 @@ msgstr ""
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:553
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58
|
||||
msgid "DNS Server"
|
||||
msgstr ""
|
||||
@ -325,7 +337,7 @@ msgstr ""
|
||||
msgid "Domain and subnet lists that decide which traffic uses this section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528
|
||||
msgid "Domain Resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -355,12 +367,12 @@ msgstr ""
|
||||
msgid "Downloading all lists via specific Proxy/VPN"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:241
|
||||
msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:763
|
||||
msgid "Dynamic List"
|
||||
msgstr ""
|
||||
|
||||
@ -368,11 +380,11 @@ msgstr ""
|
||||
msgid "Enable autostart"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:529
|
||||
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:996
|
||||
msgid "Enable DNS resolve to get real IP when routing"
|
||||
msgstr ""
|
||||
|
||||
@ -384,7 +396,7 @@ msgstr ""
|
||||
msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:965
|
||||
msgid "Enable Mixed Proxy"
|
||||
msgstr ""
|
||||
|
||||
@ -392,7 +404,7 @@ msgstr ""
|
||||
msgid "Enable Output Network Interface"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:966
|
||||
msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies"
|
||||
msgstr ""
|
||||
|
||||
@ -404,59 +416,59 @@ msgstr ""
|
||||
msgid "Enable YACD WAN Access"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:99
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:101
|
||||
msgid "Enter complete outbound configuration in JSON format"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:712
|
||||
msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685
|
||||
msgid "Enter domain names without protocols, e.g. example.com or sub.example.com"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:773
|
||||
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366
|
||||
msgid "Every 1 minute"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:187
|
||||
msgid "Every 12 hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185
|
||||
msgid "Every 3 hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367
|
||||
msgid "Every 3 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183
|
||||
msgid "Every 30 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:365
|
||||
msgid "Every 30 seconds"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:368
|
||||
msgid "Every 5 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186
|
||||
msgid "Every 6 hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:188
|
||||
msgid "Every day"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:182
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184
|
||||
msgid "Every hour"
|
||||
msgstr ""
|
||||
|
||||
@ -468,7 +480,7 @@ msgstr ""
|
||||
msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:240
|
||||
msgid "Exclude servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -506,7 +518,7 @@ msgstr ""
|
||||
msgid "Fastest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:937
|
||||
msgid "Fully Routed IPs"
|
||||
msgstr ""
|
||||
|
||||
@ -518,19 +530,15 @@ msgstr ""
|
||||
msgid "Global check"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:463
|
||||
msgid "Global Proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194
|
||||
msgid "Group by countries"
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:197
|
||||
msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195
|
||||
msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181
|
||||
msgid "How often to automatically update the subscription"
|
||||
msgstr ""
|
||||
|
||||
@ -538,7 +546,7 @@ msgstr ""
|
||||
msgid "HTTP error"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:228
|
||||
msgid "Include servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -784,7 +792,7 @@ msgstr ""
|
||||
msgid "Issues detected"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229
|
||||
msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all."
|
||||
msgstr ""
|
||||
|
||||
@ -797,6 +805,10 @@ msgstr ""
|
||||
msgid "Latest version is installed"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateProxyUrlList.ts:31
|
||||
msgid "Line"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327
|
||||
msgid "List Update Frequency"
|
||||
msgstr ""
|
||||
@ -809,11 +821,11 @@ msgstr ""
|
||||
msgid "Lists & Updates"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841
|
||||
msgid "Local Domain Lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:865
|
||||
msgid "Local Subnet Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -837,7 +849,7 @@ msgstr ""
|
||||
msgid "Mixed proxy and DNS resolution tuning"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:979
|
||||
msgid "Mixed Proxy Port"
|
||||
msgstr ""
|
||||
|
||||
@ -845,7 +857,7 @@ msgstr ""
|
||||
msgid "Monitored Interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:405
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr ""
|
||||
|
||||
@ -869,7 +881,7 @@ msgstr ""
|
||||
msgid "Network"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481
|
||||
msgid "Network Interface"
|
||||
msgstr ""
|
||||
|
||||
@ -902,7 +914,15 @@ msgstr ""
|
||||
msgid "Not running"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:213
|
||||
msgid "Number of leading characters of each proxy name to group by."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:201
|
||||
msgid "Off"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472
|
||||
msgid "Only one section can be global at a time."
|
||||
msgstr ""
|
||||
|
||||
@ -910,11 +930,11 @@ msgstr ""
|
||||
msgid "Operation timed out"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:60
|
||||
msgid "Outbound Config"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:98
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:100
|
||||
msgid "Outbound Configuration"
|
||||
msgstr ""
|
||||
|
||||
@ -951,11 +971,15 @@ msgstr ""
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:212
|
||||
msgid "Prefix length"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37
|
||||
msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:66
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68
|
||||
msgid "Proxy Configuration URL"
|
||||
msgstr ""
|
||||
|
||||
@ -967,19 +991,19 @@ msgstr ""
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:610
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:889
|
||||
msgid "Remote Domain Lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913
|
||||
msgid "Remote Subnet Lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:995
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -987,7 +1011,7 @@ msgstr ""
|
||||
msgid "Restart NetShift"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:464
|
||||
msgid "Route all unmatched traffic through this section's outbound."
|
||||
msgstr ""
|
||||
|
||||
@ -1035,7 +1059,7 @@ msgstr ""
|
||||
msgid "Run Diagnostic"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629
|
||||
msgid "Russia inside restrictions"
|
||||
msgstr ""
|
||||
|
||||
@ -1047,7 +1071,7 @@ msgstr ""
|
||||
msgid "Sections"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577
|
||||
msgid "Select a predefined list for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -1067,11 +1091,11 @@ msgstr ""
|
||||
msgid "Select how to configure the proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:482
|
||||
msgid "Select network interface for VPN connection"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:554
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59
|
||||
msgid "Select or enter DNS server address"
|
||||
msgstr ""
|
||||
@ -1084,7 +1108,7 @@ msgstr ""
|
||||
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540
|
||||
msgid "Select the DNS protocol type for the domain resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -1108,10 +1132,18 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57
|
||||
msgid "Selector (text list)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:252
|
||||
msgid "Selector Proxy Links"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:278
|
||||
msgid "Selector Proxy Links (one per line)"
|
||||
msgstr ""
|
||||
|
||||
#: src/netshift/methods/shell/index.ts:303
|
||||
msgid "Self-update failed"
|
||||
msgstr ""
|
||||
@ -1133,7 +1165,7 @@ msgstr ""
|
||||
msgid "Show sing-box config"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157
|
||||
#: src/netshift/tabs/dashboard/initController.ts:354
|
||||
msgid "Sing-box"
|
||||
msgstr ""
|
||||
@ -1178,20 +1210,20 @@ msgstr ""
|
||||
msgid "Specify a local IP address to be excluded from routing"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:938
|
||||
msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:890
|
||||
msgid "Specify remote URLs to download and use domain lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914
|
||||
msgid "Specify remote URLs to download and use subnet lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:842
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:866
|
||||
msgid "Specify the path to the list file located on the router filesystem"
|
||||
msgstr ""
|
||||
|
||||
@ -1204,7 +1236,7 @@ msgid "Stop NetShift"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:21
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:59
|
||||
msgid "Subscription"
|
||||
msgstr ""
|
||||
|
||||
@ -1216,15 +1248,19 @@ msgstr ""
|
||||
msgid "Subscription feeds, server filters and URLTest tuning"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:150
|
||||
msgid "Subscription format"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:196
|
||||
msgid "Subscription grouping"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180
|
||||
msgid "Subscription Update Interval"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124
|
||||
msgid "Subscription URLs"
|
||||
msgstr ""
|
||||
|
||||
@ -1260,8 +1296,8 @@ msgstr ""
|
||||
msgid "Test latency"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:764
|
||||
msgid "Text List"
|
||||
msgstr ""
|
||||
|
||||
@ -1269,19 +1305,19 @@ msgstr ""
|
||||
msgid "The DNS server used to look up the IP address of an upstream DNS server"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:363
|
||||
msgid "The interval between connectivity tests"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379
|
||||
msgid "The maximum difference in response times (ms) allowed when comparing servers"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:413
|
||||
msgid "The URL used to test server connectivity"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170
|
||||
msgid "This is a security trade-off: an attacker could intercept the fetch."
|
||||
msgstr ""
|
||||
|
||||
@ -1309,12 +1345,12 @@ msgstr ""
|
||||
msgid "TTL value cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50
|
||||
msgid "UDP (Unprotected DNS)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:452
|
||||
msgid "UDP over TCP"
|
||||
msgstr ""
|
||||
|
||||
@ -1376,23 +1412,31 @@ msgstr ""
|
||||
msgid "URLTest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58
|
||||
msgid "URLTest (text list)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:362
|
||||
msgid "URLTest Check Interval"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:307
|
||||
msgid "URLTest Proxy Links"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:333
|
||||
msgid "URLTest Proxy Links (one per line)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:412
|
||||
msgid "URLTest Testing URL"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:378
|
||||
msgid "URLTest Tolerance"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168
|
||||
msgid "Use only for IP-host panels that serve an invalid or self-signed certificate."
|
||||
msgstr ""
|
||||
|
||||
@ -1400,23 +1444,23 @@ msgstr ""
|
||||
msgid "Use this only when the router has working IPv6 connectivity."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470
|
||||
msgid "Use with Exclusion sections to route specific domains directly."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684
|
||||
msgid "User Domains"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711
|
||||
msgid "User Domains List"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:772
|
||||
msgid "User Subnets"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:799
|
||||
msgid "User Subnets List"
|
||||
msgstr ""
|
||||
|
||||
@ -1442,8 +1486,8 @@ msgstr ""
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831
|
||||
msgid "Validation errors:"
|
||||
msgstr ""
|
||||
|
||||
@ -1460,21 +1504,26 @@ msgstr ""
|
||||
msgid "Visit Wiki"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:253
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:308
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:279
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:334
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612
|
||||
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:631
|
||||
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466
|
||||
msgid "When enabled, traffic not matching any other section's lists will go through this proxy."
|
||||
msgstr ""
|
||||
|
||||
@ -1482,11 +1531,11 @@ msgstr ""
|
||||
msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151
|
||||
msgid "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:156
|
||||
msgid "Xray JSON (Happ)"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: NETSHIFT\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-11 20:31+0300\n"
|
||||
"PO-Revision-Date: 2026-06-11 20:31+0300\n"
|
||||
"POT-Creation-Date: 2026-06-13 14:14+0300\n"
|
||||
"PO-Revision-Date: 2026-06-13 14:14+0300\n"
|
||||
"Last-Translator: yandexru45\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@ -56,6 +56,9 @@ msgstr "Обеспечивает доступ к YACD из WAN. Убедитес
|
||||
msgid "Applicable for SOCKS and Shadowsocks proxy"
|
||||
msgstr "Применимо для SOCKS и Shadowsocks прокси"
|
||||
|
||||
msgid "At least one proxy link must be specified."
|
||||
msgstr "Необходимо указать хотя бы одну прокси-ссылку."
|
||||
|
||||
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||
|
||||
@ -86,6 +89,12 @@ msgstr "Браузер не использует FakeIP"
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr "Браузер использует FakeIP"
|
||||
|
||||
msgid "By country flag"
|
||||
msgstr "По флагу страны"
|
||||
|
||||
msgid "By name prefix"
|
||||
msgstr "По префиксу имени"
|
||||
|
||||
msgid "Cache File Path"
|
||||
msgstr "Путь к файлу кэша"
|
||||
|
||||
@ -368,11 +377,8 @@ msgstr "Глобальная проверка"
|
||||
msgid "Global Proxy"
|
||||
msgstr "Глобальный прокси"
|
||||
|
||||
msgid "Group by countries"
|
||||
msgstr "Группировать по странам"
|
||||
|
||||
msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag"
|
||||
msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы"
|
||||
msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters."
|
||||
msgstr "Группировать прокси из подписки в группы URLTest. «По флагу страны» использует эмодзи флага в начале каждого имени; «По префиксу имени» группирует по первым N символам."
|
||||
|
||||
msgid "How often to automatically update the subscription"
|
||||
msgstr "Как часто автоматически обновлять подписку"
|
||||
@ -569,6 +575,9 @@ msgstr "Последняя"
|
||||
msgid "Latest version is installed"
|
||||
msgstr "Установлена последняя версия"
|
||||
|
||||
msgid "Line"
|
||||
msgstr "Строка"
|
||||
|
||||
msgid "List Update Frequency"
|
||||
msgstr "Частота обновления списков"
|
||||
|
||||
@ -641,6 +650,12 @@ msgstr "Не отвечает"
|
||||
msgid "Not running"
|
||||
msgstr "Не запущено"
|
||||
|
||||
msgid "Number of leading characters of each proxy name to group by."
|
||||
msgstr "Количество начальных символов имени каждого прокси для группировки."
|
||||
|
||||
msgid "Off"
|
||||
msgstr "Выключено"
|
||||
|
||||
msgid "Only one section can be global at a time."
|
||||
msgstr "Только одна секция может быть глобальной одновременно."
|
||||
|
||||
@ -674,6 +689,9 @@ msgstr "Путь должен заканчиваться на cache.db"
|
||||
msgid "Pending"
|
||||
msgstr "Ожидает запуска"
|
||||
|
||||
msgid "Prefix length"
|
||||
msgstr "Длина префикса"
|
||||
|
||||
msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT."
|
||||
msgstr "Переключатели протоколов, пути к файлам и журналирование. Включайте блокировку DoH только после переключения вышестоящего DNS на UDP или DoT."
|
||||
|
||||
@ -791,9 +809,15 @@ msgstr "Выберите WAN интерфейсы для мониторинга"
|
||||
msgid "Selector"
|
||||
msgstr "Selector"
|
||||
|
||||
msgid "Selector (text list)"
|
||||
msgstr "Selector (текстовый список)"
|
||||
|
||||
msgid "Selector Proxy Links"
|
||||
msgstr "Ссылки прокси для Selector"
|
||||
|
||||
msgid "Selector Proxy Links (one per line)"
|
||||
msgstr "Прокси-ссылки Selector (по одной в строке)"
|
||||
|
||||
msgid "Self-update failed"
|
||||
msgstr "Не удалось обновить"
|
||||
|
||||
@ -872,6 +896,9 @@ msgstr "Источники подписок, фильтры серверов и
|
||||
msgid "Subscription format"
|
||||
msgstr "Формат подписки"
|
||||
|
||||
msgid "Subscription grouping"
|
||||
msgstr "Группировка подписки"
|
||||
|
||||
msgid "Subscription Update Interval"
|
||||
msgstr "Интервал обновления подписки"
|
||||
|
||||
@ -977,12 +1004,18 @@ msgstr "URL должен использовать один из следующи
|
||||
msgid "URLTest"
|
||||
msgstr "URLTest"
|
||||
|
||||
msgid "URLTest (text list)"
|
||||
msgstr "URLTest (текстовый список)"
|
||||
|
||||
msgid "URLTest Check Interval"
|
||||
msgstr "Интервал проверки URLTest"
|
||||
|
||||
msgid "URLTest Proxy Links"
|
||||
msgstr "Ссылки прокси для URLTest"
|
||||
|
||||
msgid "URLTest Proxy Links (one per line)"
|
||||
msgstr "Прокси-ссылки URLTest (по одной в строке)"
|
||||
|
||||
msgid "URLTest Testing URL"
|
||||
msgstr "URLTest ссылка для проверки"
|
||||
|
||||
@ -1028,6 +1061,9 @@ msgstr "Перейти в wiki"
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||
msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://"
|
||||
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line"
|
||||
msgstr "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// — по одной в строке"
|
||||
|
||||
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
|
||||
|
||||
|
||||
@ -120,7 +120,8 @@ export namespace NetShift {
|
||||
subscription_url: string[];
|
||||
subscription_format_preference?: 'auto' | 'xray' | 'singbox';
|
||||
subscription_update_interval?: string;
|
||||
subscription_group_by_countries?: '0' | '1';
|
||||
subscription_group_mode?: 'off' | 'country' | 'prefix';
|
||||
subscription_group_prefix_len?: string;
|
||||
subscription_filter_include_keywords?: string[];
|
||||
subscription_filter_exclude_keywords?: string[];
|
||||
}
|
||||
|
||||
@ -10,4 +10,5 @@ export * from './validateVlessUrl';
|
||||
export * from './validateOutboundJson';
|
||||
export * from './validateTrojanUrl';
|
||||
export * from './validateProxyUrl';
|
||||
export * from './validateProxyUrlList';
|
||||
export * from './validateSocksUrl';
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateProxyUrlList } from '../validateProxyUrlList';
|
||||
|
||||
// Synthetic placeholder links only — never real proxy/subscription data.
|
||||
const VLESS =
|
||||
'vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#node-a';
|
||||
const SS =
|
||||
'ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#node-b';
|
||||
|
||||
const validBlobs = [
|
||||
['single vless line', VLESS],
|
||||
['single ss line', SS],
|
||||
['two links', `${VLESS}\n${SS}`],
|
||||
['blank lines ignored', `\n${VLESS}\n\n${SS}\n`],
|
||||
['leading/trailing whitespace trimmed', ` ${VLESS} \n\t${SS}\t`],
|
||||
['CRLF tolerated', `${VLESS}\r\n${SS}\r`],
|
||||
];
|
||||
|
||||
const invalidBlobs = [
|
||||
['empty string', ''],
|
||||
['whitespace/blank only', ' \n\t\n '],
|
||||
['unsupported scheme', 'tuic://127.0.0.1:443#node'],
|
||||
['second line invalid', `${VLESS}\ntuic://127.0.0.1:443`],
|
||||
['garbage line', 'not-a-link'],
|
||||
];
|
||||
|
||||
describe('validateProxyUrlList', () => {
|
||||
describe.each(validBlobs)('Valid blob: %s', (_desc, blob) => {
|
||||
it('returns valid=true', () => {
|
||||
const res = validateProxyUrlList(blob);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidBlobs)('Invalid blob: %s', (_desc, blob) => {
|
||||
it('returns valid=false', () => {
|
||||
const res = validateProxyUrlList(blob);
|
||||
expect(res.valid).toBe(false);
|
||||
expect(typeof res.message).toBe('string');
|
||||
expect(res.message.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the 1-based line number of the first failing line', () => {
|
||||
const res = validateProxyUrlList(`${VLESS}\n${SS}\ntuic://127.0.0.1:443`);
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toContain('Line 3');
|
||||
});
|
||||
|
||||
it('counts blank lines toward the reported line number', () => {
|
||||
const res = validateProxyUrlList(`${VLESS}\n\ntuic://127.0.0.1:443`);
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toContain('Line 3');
|
||||
});
|
||||
});
|
||||
44
fe-app-netshift/src/validators/validateProxyUrlList.ts
Normal file
44
fe-app-netshift/src/validators/validateProxyUrlList.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { validateProxyUrl } from './validateProxyUrl';
|
||||
|
||||
/**
|
||||
* Validate a textarea blob of proxy links (one per line).
|
||||
*
|
||||
* Splits on newlines, trims each line, ignores blank lines, then runs the
|
||||
* single-link `validateProxyUrl` on every remaining line. Returns the first
|
||||
* error encountered (annotated with the 1-based line number) or
|
||||
* `{ valid: true }` when every non-blank line is a valid proxy link.
|
||||
*/
|
||||
export function validateProxyUrlList(value: string): ValidationResult {
|
||||
const lines = value.split('\n');
|
||||
|
||||
let hasLink = false;
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index].trim();
|
||||
|
||||
if (line.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hasLink = true;
|
||||
|
||||
const validation = validateProxyUrl(line);
|
||||
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `${_('Line')} ${index + 1}: ${validation.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasLink) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('At least one proxy link must be specified.'),
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
}
|
||||
123
install.sh
123
install.sh
@ -1,7 +1,13 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=dash
|
||||
|
||||
REPO="https://api.github.com/repos/yandexru45/netshift/releases/latest"
|
||||
REPO="https://uralgit.ru/api/v1/repos/ural/netshift/releases/latest"
|
||||
# uralgit.ru FRONTEND redirect path (NOT the rate-limited api.github.com).
|
||||
# /releases/latest 303s to /releases/tag/<tag>; /releases/download/<tag>/<asset>
|
||||
# direct download. Primary install path so CGNAT / shared-IP routers avoid the
|
||||
# 60/hour/IP API limit; REPO stays as the fallback.
|
||||
RELEASES_LATEST_REDIRECT="https://uralgit.ru/ural/netshift/releases/latest"
|
||||
RELEASES_DOWNLOAD_BASE="https://uralgit.ru/ural/netshift/releases/download"
|
||||
DOWNLOAD_DIR="/tmp/netshift"
|
||||
COUNT=3
|
||||
|
||||
@ -57,7 +63,7 @@ pkg_install() {
|
||||
# If you're installing a non-standard (self-built) package, use the --allow-untrusted option:
|
||||
apk add --allow-untrusted "$pkg_file"
|
||||
else
|
||||
opkg install "$pkg_file"
|
||||
opkg install --force-downgrade --force-reinstall "$pkg_file"
|
||||
fi
|
||||
}
|
||||
|
||||
@ -66,7 +72,7 @@ update_config() {
|
||||
printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия NetShift. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить NetShift заново.║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/netshift-070 ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Подробности: https://uralgit.ru/ural/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n"
|
||||
|
||||
@ -76,7 +82,7 @@ update_config() {
|
||||
printf "\033[48;5;196m\033[1m║ ! Detected old NetShift version. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE NetShift. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/netshift-070 ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Details: https://uralgit.ru/ural/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n"
|
||||
|
||||
@ -88,7 +94,7 @@ update_config() {
|
||||
|
||||
yes|y|Y)
|
||||
mv /etc/config/netshift /etc/config/netshift-070
|
||||
wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/netshift/files/etc/config/netshift
|
||||
wget -O /etc/config/netshift https://uralgit.ru/ural/netshift/raw/branch/main/netshift/files/etc/config/netshift
|
||||
msg "NetShift config has been reset to default. Your old config saved in /etc/config/netshift-070"
|
||||
break
|
||||
;;
|
||||
@ -129,7 +135,7 @@ migrate_from_podkop() {
|
||||
printf "\033[48;5;196m\033[1m║ Ваша конфигурация будет перенесена автоматически. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Старая конфигурация сохранится в /etc/config/podkop.bak.pre-netshift║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Старый пакет podkop будет удалён, NetShift будет установлен. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Подробности: https://uralgit.ru/ural/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n"
|
||||
|
||||
@ -140,7 +146,7 @@ migrate_from_podkop() {
|
||||
printf "\033[48;5;196m\033[1m║ Your configuration will be carried over automatically. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Old config will be backed up to /etc/config/podkop.bak.pre-netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ The old podkop package will be removed, NetShift installed. ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Details: https://uralgit.ru/ural/netshift ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n"
|
||||
printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n"
|
||||
|
||||
@ -241,6 +247,30 @@ migrate_from_podkop() {
|
||||
msg "Your old config is preserved at /etc/config/podkop.bak.pre-netshift"
|
||||
}
|
||||
|
||||
# Download one release asset URL into $DOWNLOAD_DIR with retry. POSIX sh.
|
||||
download_release_asset() {
|
||||
url="$1"
|
||||
filename="$2"
|
||||
filepath="$DOWNLOAD_DIR/$filename"
|
||||
|
||||
attempt=0
|
||||
while [ $attempt -lt $COUNT ]; do
|
||||
msg "Download $filename (count $((attempt + 1)))..."
|
||||
if wget -q -O "$filepath" "$url"; then
|
||||
if [ -s "$filepath" ]; then
|
||||
msg "$filename successfully downloaded"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
msg "Download error for $filename. Retrying..."
|
||||
rm -f "$filepath"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
msg "Failed to download $filename after $COUNT attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
check_system
|
||||
sing_box
|
||||
@ -255,44 +285,63 @@ main() {
|
||||
msg "Installing NetShift..."
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
check_response=$(curl -s "https://api.github.com/repos/yandexru45/netshift/releases/latest")
|
||||
|
||||
if echo "$check_response" | grep -q 'API rate limit '; then
|
||||
msg "You've reached the GitHub rate limit. Repeat in five minutes."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local grep_url_pattern
|
||||
local ext release_tag redirect_url
|
||||
if [ "$PKG_IS_APK" -eq 1 ]; then
|
||||
grep_url_pattern='https://[^"[:space:]]*\.apk'
|
||||
ext="apk"
|
||||
else
|
||||
grep_url_pattern='https://[^"[:space:]]*\.ipk'
|
||||
ext="ipk"
|
||||
fi
|
||||
|
||||
wget -qO- "$REPO" | grep -o "$grep_url_pattern" | while read -r url; do
|
||||
filename=$(basename "$url")
|
||||
filepath="$DOWNLOAD_DIR/$filename"
|
||||
# PRIMARY: resolve the latest tag via the uralgit.ru frontend redirect (no
|
||||
# api.github.com hit → not subject to the 60/hour/IP rate limit), then build
|
||||
# the deterministic releases/download/<tag>/<asset> URLs and download them.
|
||||
release_tag=""
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
redirect_url=$(curl -sI -o /dev/null -w '%{redirect_url}' \
|
||||
--connect-timeout 5 -m 15 -A 'netshift-installer' \
|
||||
"$RELEASES_LATEST_REDIRECT" 2>/dev/null)
|
||||
case "$redirect_url" in
|
||||
*/releases/tag/*)
|
||||
release_tag="${redirect_url##*/releases/tag/}"
|
||||
case "$release_tag" in '' | */*) release_tag="" ;; esac
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
attempt=0
|
||||
while [ $attempt -lt $COUNT ]; do
|
||||
msg "Download $filename (count $((attempt+1)))..."
|
||||
if wget -q -O "$filepath" "$url"; then
|
||||
if [ -s "$filepath" ]; then
|
||||
msg "$filename successfully downloaded"
|
||||
break
|
||||
fi
|
||||
if [ -n "$release_tag" ]; then
|
||||
msg "Latest NetShift release: $release_tag (direct download, no GitHub API)"
|
||||
for pkg in netshift luci-app-netshift; do
|
||||
if [ "$ext" = "ipk" ]; then
|
||||
filename="${pkg}-${release_tag}-r1-all.${ext}"
|
||||
else
|
||||
filename="${pkg}-${release_tag}-r1.${ext}"
|
||||
fi
|
||||
msg "Download error for $filename. Retrying..."
|
||||
rm -f "$filepath"
|
||||
attempt=$((attempt+1))
|
||||
download_release_asset "$RELEASES_DOWNLOAD_BASE/$release_tag/$filename" "$filename"
|
||||
done
|
||||
|
||||
if [ $attempt -eq $COUNT ]; then
|
||||
msg "Failed to download $filename after $COUNT attempts"
|
||||
# RU i18n only if already installed (mirrors the install flow below).
|
||||
if pkg_is_installed luci-i18n-netshift-ru; then
|
||||
filename="luci-i18n-netshift-ru-${release_tag}.${ext}"
|
||||
download_release_asset "$RELEASES_DOWNLOAD_BASE/$release_tag/$filename" "$filename"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# FALLBACK: scrape the api.github.com release JSON for .ipk/.apk URLs.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
check_response=$(curl -s "$REPO")
|
||||
|
||||
if echo "$check_response" | grep -q 'API rate limit '; then
|
||||
msg "You've reached the GitHub rate limit. Repeat in five minutes."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local grep_url_pattern
|
||||
grep_url_pattern="https://[^\"[:space:]]*\.${ext}"
|
||||
|
||||
wget -qO- "$REPO" | grep -o "$grep_url_pattern" | while read -r url; do
|
||||
filename=$(basename "$url")
|
||||
download_release_asset "$url" "$filename"
|
||||
done
|
||||
fi
|
||||
|
||||
# Check if any files were downloaded
|
||||
if ! ls "$DOWNLOAD_DIR"/*netshift* >/dev/null 2>&1; then
|
||||
|
||||
@ -753,6 +753,33 @@ function validateProxyUrl(url) {
|
||||
};
|
||||
}
|
||||
|
||||
// src/validators/validateProxyUrlList.ts
|
||||
function validateProxyUrlList(value) {
|
||||
const lines = value.split("\n");
|
||||
let hasLink = false;
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index].trim();
|
||||
if (line.length === 0) {
|
||||
continue;
|
||||
}
|
||||
hasLink = true;
|
||||
const validation = validateProxyUrl(line);
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `${_("Line")} ${index + 1}: ${validation.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!hasLink) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("At least one proxy link must be specified.")
|
||||
};
|
||||
}
|
||||
return { valid: true, message: "" };
|
||||
}
|
||||
|
||||
// src/helpers/parseValueList.ts
|
||||
function parseValueList(value) {
|
||||
return value.split(/\n/).map((line) => line.split("//")[0]).join(" ").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
||||
@ -6164,6 +6191,7 @@ return baseclass.extend({
|
||||
validateOutboundJson,
|
||||
validatePath,
|
||||
validateProxyUrl,
|
||||
validateProxyUrlList,
|
||||
validateShadowsocksUrl,
|
||||
validateSocksUrl,
|
||||
validateSubnet,
|
||||
|
||||
@ -54,6 +54,8 @@ function createSectionContent(section) {
|
||||
o.value("url", _("Connection URL"));
|
||||
o.value("selector", _("Selector"));
|
||||
o.value("urltest", _("URLTest"));
|
||||
o.value("selector_text", _("Selector (text list)"));
|
||||
o.value("urltest_text", _("URLTest (text list)"));
|
||||
o.value("subscription", _("Subscription"));
|
||||
o.value("outbound", _("Outbound Config"));
|
||||
o.default = "url";
|
||||
@ -189,17 +191,36 @@ function createSectionContent(section) {
|
||||
|
||||
o = section.taboption(
|
||||
"subscription",
|
||||
form.Flag,
|
||||
"subscription_group_by_countries",
|
||||
_("Group by countries"),
|
||||
form.ListValue,
|
||||
"subscription_group_mode",
|
||||
_("Subscription grouping"),
|
||||
_(
|
||||
"Group subscription proxies into separate URLTest groups by the country flag at the start of each tag",
|
||||
"Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters.",
|
||||
),
|
||||
);
|
||||
o.default = "0";
|
||||
o.value("off", _("Off"));
|
||||
o.value("country", _("By country flag"));
|
||||
o.value("prefix", _("By name prefix"));
|
||||
o.default = "off";
|
||||
o.rmempty = false;
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "subscription" });
|
||||
|
||||
o = section.taboption(
|
||||
"subscription",
|
||||
form.Value,
|
||||
"subscription_group_prefix_len",
|
||||
_("Prefix length"),
|
||||
_("Number of leading characters of each proxy name to group by."),
|
||||
);
|
||||
o.default = "2";
|
||||
o.datatype = "and(uinteger,min(1))";
|
||||
o.rmempty = false;
|
||||
o.depends({
|
||||
connection_type: "proxy",
|
||||
proxy_config_type: "subscription",
|
||||
subscription_group_mode: "prefix",
|
||||
});
|
||||
|
||||
o = section.taboption(
|
||||
"subscription",
|
||||
form.DynamicList,
|
||||
@ -250,6 +271,35 @@ function createSectionContent(section) {
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.taboption(
|
||||
"connection",
|
||||
form.TextValue,
|
||||
"selector_proxy_links_text",
|
||||
_("Selector Proxy Links (one per line)"),
|
||||
_(
|
||||
"vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line",
|
||||
),
|
||||
);
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "selector_text" });
|
||||
o.rows = 5;
|
||||
o.wrap = "soft";
|
||||
o.textarea = true;
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateProxyUrlList(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.taboption(
|
||||
"subscription",
|
||||
form.DynamicList,
|
||||
@ -276,6 +326,35 @@ function createSectionContent(section) {
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.taboption(
|
||||
"subscription",
|
||||
form.TextValue,
|
||||
"urltest_proxy_links_text",
|
||||
_("URLTest Proxy Links (one per line)"),
|
||||
_(
|
||||
"vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line",
|
||||
),
|
||||
);
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest_text" });
|
||||
o.rows = 5;
|
||||
o.wrap = "soft";
|
||||
o.textarea = true;
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateProxyUrlList(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.taboption(
|
||||
"subscription",
|
||||
form.ListValue,
|
||||
@ -289,6 +368,7 @@ function createSectionContent(section) {
|
||||
o.value("5m", _("Every 5 minutes"));
|
||||
o.default = "3m";
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest" });
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest_text" });
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "subscription" });
|
||||
|
||||
o = section.taboption(
|
||||
@ -303,6 +383,7 @@ function createSectionContent(section) {
|
||||
o.default = "50";
|
||||
o.rmempty = false;
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest" });
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest_text" });
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "subscription" });
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
@ -347,6 +428,7 @@ function createSectionContent(section) {
|
||||
o.default = "https://www.gstatic.com/generate_204";
|
||||
o.rmempty = false;
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest" });
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "urltest_text" });
|
||||
o.depends({ connection_type: "proxy", proxy_config_type: "subscription" });
|
||||
|
||||
o.validate = function (section_id, value) {
|
||||
|
||||
@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: NETSHIFT\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-11 20:31+0300\n"
|
||||
"PO-Revision-Date: 2026-06-11 20:31+0300\n"
|
||||
"POT-Creation-Date: 2026-06-13 14:14+0300\n"
|
||||
"PO-Revision-Date: 2026-06-13 14:14+0300\n"
|
||||
"Last-Translator: yandexru45\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@ -56,6 +56,9 @@ msgstr "Обеспечивает доступ к YACD из WAN. Убедитес
|
||||
msgid "Applicable for SOCKS and Shadowsocks proxy"
|
||||
msgstr "Применимо для SOCKS и Shadowsocks прокси"
|
||||
|
||||
msgid "At least one proxy link must be specified."
|
||||
msgstr "Необходимо указать хотя бы одну прокси-ссылку."
|
||||
|
||||
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||
|
||||
@ -86,6 +89,12 @@ msgstr "Браузер не использует FakeIP"
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr "Браузер использует FakeIP"
|
||||
|
||||
msgid "By country flag"
|
||||
msgstr "По флагу страны"
|
||||
|
||||
msgid "By name prefix"
|
||||
msgstr "По префиксу имени"
|
||||
|
||||
msgid "Cache File Path"
|
||||
msgstr "Путь к файлу кэша"
|
||||
|
||||
@ -368,11 +377,8 @@ msgstr "Глобальная проверка"
|
||||
msgid "Global Proxy"
|
||||
msgstr "Глобальный прокси"
|
||||
|
||||
msgid "Group by countries"
|
||||
msgstr "Группировать по странам"
|
||||
|
||||
msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag"
|
||||
msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы"
|
||||
msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters."
|
||||
msgstr "Группировать прокси из подписки в группы URLTest. «По флагу страны» использует эмодзи флага в начале каждого имени; «По префиксу имени» группирует по первым N символам."
|
||||
|
||||
msgid "How often to automatically update the subscription"
|
||||
msgstr "Как часто автоматически обновлять подписку"
|
||||
@ -569,6 +575,9 @@ msgstr "Последняя"
|
||||
msgid "Latest version is installed"
|
||||
msgstr "Установлена последняя версия"
|
||||
|
||||
msgid "Line"
|
||||
msgstr "Строка"
|
||||
|
||||
msgid "List Update Frequency"
|
||||
msgstr "Частота обновления списков"
|
||||
|
||||
@ -641,6 +650,12 @@ msgstr "Не отвечает"
|
||||
msgid "Not running"
|
||||
msgstr "Не запущено"
|
||||
|
||||
msgid "Number of leading characters of each proxy name to group by."
|
||||
msgstr "Количество начальных символов имени каждого прокси для группировки."
|
||||
|
||||
msgid "Off"
|
||||
msgstr "Выключено"
|
||||
|
||||
msgid "Only one section can be global at a time."
|
||||
msgstr "Только одна секция может быть глобальной одновременно."
|
||||
|
||||
@ -674,6 +689,9 @@ msgstr "Путь должен заканчиваться на cache.db"
|
||||
msgid "Pending"
|
||||
msgstr "Ожидает запуска"
|
||||
|
||||
msgid "Prefix length"
|
||||
msgstr "Длина префикса"
|
||||
|
||||
msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT."
|
||||
msgstr "Переключатели протоколов, пути к файлам и журналирование. Включайте блокировку DoH только после переключения вышестоящего DNS на UDP или DoT."
|
||||
|
||||
@ -791,9 +809,15 @@ msgstr "Выберите WAN интерфейсы для мониторинга"
|
||||
msgid "Selector"
|
||||
msgstr "Selector"
|
||||
|
||||
msgid "Selector (text list)"
|
||||
msgstr "Selector (текстовый список)"
|
||||
|
||||
msgid "Selector Proxy Links"
|
||||
msgstr "Ссылки прокси для Selector"
|
||||
|
||||
msgid "Selector Proxy Links (one per line)"
|
||||
msgstr "Прокси-ссылки Selector (по одной в строке)"
|
||||
|
||||
msgid "Self-update failed"
|
||||
msgstr "Не удалось обновить"
|
||||
|
||||
@ -872,6 +896,9 @@ msgstr "Источники подписок, фильтры серверов и
|
||||
msgid "Subscription format"
|
||||
msgstr "Формат подписки"
|
||||
|
||||
msgid "Subscription grouping"
|
||||
msgstr "Группировка подписки"
|
||||
|
||||
msgid "Subscription Update Interval"
|
||||
msgstr "Интервал обновления подписки"
|
||||
|
||||
@ -977,12 +1004,18 @@ msgstr "URL должен использовать один из следующи
|
||||
msgid "URLTest"
|
||||
msgstr "URLTest"
|
||||
|
||||
msgid "URLTest (text list)"
|
||||
msgstr "URLTest (текстовый список)"
|
||||
|
||||
msgid "URLTest Check Interval"
|
||||
msgstr "Интервал проверки URLTest"
|
||||
|
||||
msgid "URLTest Proxy Links"
|
||||
msgstr "Ссылки прокси для URLTest"
|
||||
|
||||
msgid "URLTest Proxy Links (one per line)"
|
||||
msgstr "Прокси-ссылки URLTest (по одной в строке)"
|
||||
|
||||
msgid "URLTest Testing URL"
|
||||
msgstr "URLTest ссылка для проверки"
|
||||
|
||||
@ -1028,6 +1061,9 @@ msgstr "Перейти в wiki"
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||
msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://"
|
||||
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line"
|
||||
msgstr "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// — по одной в строке"
|
||||
|
||||
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: NETSHIFT\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-11 17:31+0300\n"
|
||||
"PO-Revision-Date: 2026-06-11 17:31+0300\n"
|
||||
"POT-Creation-Date: 2026-06-13 11:14+0300\n"
|
||||
"PO-Revision-Date: 2026-06-13 11:14+0300\n"
|
||||
"Last-Translator: yandexru45 <>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
@ -36,15 +36,15 @@ msgstr ""
|
||||
msgid "Active Connections"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:125
|
||||
msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:670
|
||||
msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:758
|
||||
msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip"
|
||||
msgstr ""
|
||||
|
||||
@ -57,7 +57,7 @@ msgstr ""
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165
|
||||
msgid "Allow insecure TLS for subscription fetch"
|
||||
msgstr ""
|
||||
|
||||
@ -65,19 +65,23 @@ msgstr ""
|
||||
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453
|
||||
msgid "Applicable for SOCKS and Shadowsocks proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648
|
||||
#: src/validators/validateProxyUrlList.ts:39
|
||||
msgid "At least one proxy link must be specified."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730
|
||||
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:819
|
||||
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
|
||||
@ -109,6 +113,14 @@ msgstr ""
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:202
|
||||
msgid "By country flag"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:203
|
||||
msgid "By name prefix"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517
|
||||
msgid "Cache File Path"
|
||||
msgstr ""
|
||||
@ -166,7 +178,7 @@ msgstr ""
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576
|
||||
msgid "Community Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -219,11 +231,11 @@ msgstr ""
|
||||
msgid "Currently unavailable"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669
|
||||
msgid "Custom domains"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757
|
||||
msgid "Custom subnets"
|
||||
msgstr ""
|
||||
|
||||
@ -268,12 +280,12 @@ msgstr ""
|
||||
msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:674
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:762
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166
|
||||
msgid "Disables TLS certificate verification when downloading the subscription."
|
||||
msgstr ""
|
||||
|
||||
@ -289,17 +301,17 @@ msgstr ""
|
||||
msgid "DNS outbound section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:542
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48
|
||||
msgid "DNS over HTTPS (DoH)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49
|
||||
msgid "DNS over TLS (DoT)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:539
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45
|
||||
msgid "DNS Protocol Type"
|
||||
msgstr ""
|
||||
@ -308,7 +320,7 @@ msgstr ""
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:553
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58
|
||||
msgid "DNS Server"
|
||||
msgstr ""
|
||||
@ -325,7 +337,7 @@ msgstr ""
|
||||
msgid "Domain and subnet lists that decide which traffic uses this section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528
|
||||
msgid "Domain Resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -355,12 +367,12 @@ msgstr ""
|
||||
msgid "Downloading all lists via specific Proxy/VPN"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:241
|
||||
msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:763
|
||||
msgid "Dynamic List"
|
||||
msgstr ""
|
||||
|
||||
@ -368,11 +380,11 @@ msgstr ""
|
||||
msgid "Enable autostart"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:529
|
||||
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:996
|
||||
msgid "Enable DNS resolve to get real IP when routing"
|
||||
msgstr ""
|
||||
|
||||
@ -384,7 +396,7 @@ msgstr ""
|
||||
msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:965
|
||||
msgid "Enable Mixed Proxy"
|
||||
msgstr ""
|
||||
|
||||
@ -392,7 +404,7 @@ msgstr ""
|
||||
msgid "Enable Output Network Interface"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:966
|
||||
msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies"
|
||||
msgstr ""
|
||||
|
||||
@ -404,59 +416,59 @@ msgstr ""
|
||||
msgid "Enable YACD WAN Access"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:99
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:101
|
||||
msgid "Enter complete outbound configuration in JSON format"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:712
|
||||
msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685
|
||||
msgid "Enter domain names without protocols, e.g. example.com or sub.example.com"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:773
|
||||
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366
|
||||
msgid "Every 1 minute"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:187
|
||||
msgid "Every 12 hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185
|
||||
msgid "Every 3 hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367
|
||||
msgid "Every 3 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183
|
||||
msgid "Every 30 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:365
|
||||
msgid "Every 30 seconds"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:368
|
||||
msgid "Every 5 minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186
|
||||
msgid "Every 6 hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:188
|
||||
msgid "Every day"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:182
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184
|
||||
msgid "Every hour"
|
||||
msgstr ""
|
||||
|
||||
@ -468,7 +480,7 @@ msgstr ""
|
||||
msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:240
|
||||
msgid "Exclude servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -506,7 +518,7 @@ msgstr ""
|
||||
msgid "Fastest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:937
|
||||
msgid "Fully Routed IPs"
|
||||
msgstr ""
|
||||
|
||||
@ -518,19 +530,15 @@ msgstr ""
|
||||
msgid "Global check"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:463
|
||||
msgid "Global Proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194
|
||||
msgid "Group by countries"
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:197
|
||||
msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195
|
||||
msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181
|
||||
msgid "How often to automatically update the subscription"
|
||||
msgstr ""
|
||||
|
||||
@ -538,7 +546,7 @@ msgstr ""
|
||||
msgid "HTTP error"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:228
|
||||
msgid "Include servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -784,7 +792,7 @@ msgstr ""
|
||||
msgid "Issues detected"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229
|
||||
msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all."
|
||||
msgstr ""
|
||||
|
||||
@ -797,6 +805,10 @@ msgstr ""
|
||||
msgid "Latest version is installed"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateProxyUrlList.ts:31
|
||||
msgid "Line"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327
|
||||
msgid "List Update Frequency"
|
||||
msgstr ""
|
||||
@ -809,11 +821,11 @@ msgstr ""
|
||||
msgid "Lists & Updates"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841
|
||||
msgid "Local Domain Lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:865
|
||||
msgid "Local Subnet Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -837,7 +849,7 @@ msgstr ""
|
||||
msgid "Mixed proxy and DNS resolution tuning"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:979
|
||||
msgid "Mixed Proxy Port"
|
||||
msgstr ""
|
||||
|
||||
@ -845,7 +857,7 @@ msgstr ""
|
||||
msgid "Monitored Interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:405
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr ""
|
||||
|
||||
@ -869,7 +881,7 @@ msgstr ""
|
||||
msgid "Network"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481
|
||||
msgid "Network Interface"
|
||||
msgstr ""
|
||||
|
||||
@ -902,7 +914,15 @@ msgstr ""
|
||||
msgid "Not running"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:213
|
||||
msgid "Number of leading characters of each proxy name to group by."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:201
|
||||
msgid "Off"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472
|
||||
msgid "Only one section can be global at a time."
|
||||
msgstr ""
|
||||
|
||||
@ -910,11 +930,11 @@ msgstr ""
|
||||
msgid "Operation timed out"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:60
|
||||
msgid "Outbound Config"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:98
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:100
|
||||
msgid "Outbound Configuration"
|
||||
msgstr ""
|
||||
|
||||
@ -951,11 +971,15 @@ msgstr ""
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:212
|
||||
msgid "Prefix length"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37
|
||||
msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:66
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68
|
||||
msgid "Proxy Configuration URL"
|
||||
msgstr ""
|
||||
|
||||
@ -967,19 +991,19 @@ msgstr ""
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:610
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:889
|
||||
msgid "Remote Domain Lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913
|
||||
msgid "Remote Subnet Lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:995
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -987,7 +1011,7 @@ msgstr ""
|
||||
msgid "Restart NetShift"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:464
|
||||
msgid "Route all unmatched traffic through this section's outbound."
|
||||
msgstr ""
|
||||
|
||||
@ -1035,7 +1059,7 @@ msgstr ""
|
||||
msgid "Run Diagnostic"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629
|
||||
msgid "Russia inside restrictions"
|
||||
msgstr ""
|
||||
|
||||
@ -1047,7 +1071,7 @@ msgstr ""
|
||||
msgid "Sections"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577
|
||||
msgid "Select a predefined list for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -1067,11 +1091,11 @@ msgstr ""
|
||||
msgid "Select how to configure the proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:482
|
||||
msgid "Select network interface for VPN connection"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:554
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59
|
||||
msgid "Select or enter DNS server address"
|
||||
msgstr ""
|
||||
@ -1084,7 +1108,7 @@ msgstr ""
|
||||
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540
|
||||
msgid "Select the DNS protocol type for the domain resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -1108,10 +1132,18 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57
|
||||
msgid "Selector (text list)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:252
|
||||
msgid "Selector Proxy Links"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:278
|
||||
msgid "Selector Proxy Links (one per line)"
|
||||
msgstr ""
|
||||
|
||||
#: src/netshift/methods/shell/index.ts:303
|
||||
msgid "Self-update failed"
|
||||
msgstr ""
|
||||
@ -1133,7 +1165,7 @@ msgstr ""
|
||||
msgid "Show sing-box config"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157
|
||||
#: src/netshift/tabs/dashboard/initController.ts:354
|
||||
msgid "Sing-box"
|
||||
msgstr ""
|
||||
@ -1178,20 +1210,20 @@ msgstr ""
|
||||
msgid "Specify a local IP address to be excluded from routing"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:938
|
||||
msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:890
|
||||
msgid "Specify remote URLs to download and use domain lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914
|
||||
msgid "Specify remote URLs to download and use subnet lists"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:842
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:866
|
||||
msgid "Specify the path to the list file located on the router filesystem"
|
||||
msgstr ""
|
||||
|
||||
@ -1204,7 +1236,7 @@ msgid "Stop NetShift"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:21
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:59
|
||||
msgid "Subscription"
|
||||
msgstr ""
|
||||
|
||||
@ -1216,15 +1248,19 @@ msgstr ""
|
||||
msgid "Subscription feeds, server filters and URLTest tuning"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:150
|
||||
msgid "Subscription format"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:196
|
||||
msgid "Subscription grouping"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180
|
||||
msgid "Subscription Update Interval"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124
|
||||
msgid "Subscription URLs"
|
||||
msgstr ""
|
||||
|
||||
@ -1260,8 +1296,8 @@ msgstr ""
|
||||
msgid "Test latency"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:764
|
||||
msgid "Text List"
|
||||
msgstr ""
|
||||
|
||||
@ -1269,19 +1305,19 @@ msgstr ""
|
||||
msgid "The DNS server used to look up the IP address of an upstream DNS server"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:363
|
||||
msgid "The interval between connectivity tests"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379
|
||||
msgid "The maximum difference in response times (ms) allowed when comparing servers"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:413
|
||||
msgid "The URL used to test server connectivity"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170
|
||||
msgid "This is a security trade-off: an attacker could intercept the fetch."
|
||||
msgstr ""
|
||||
|
||||
@ -1309,12 +1345,12 @@ msgstr ""
|
||||
msgid "TTL value cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50
|
||||
msgid "UDP (Unprotected DNS)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:452
|
||||
msgid "UDP over TCP"
|
||||
msgstr ""
|
||||
|
||||
@ -1376,23 +1412,31 @@ msgstr ""
|
||||
msgid "URLTest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58
|
||||
msgid "URLTest (text list)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:362
|
||||
msgid "URLTest Check Interval"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:307
|
||||
msgid "URLTest Proxy Links"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:333
|
||||
msgid "URLTest Proxy Links (one per line)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:412
|
||||
msgid "URLTest Testing URL"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:378
|
||||
msgid "URLTest Tolerance"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168
|
||||
msgid "Use only for IP-host panels that serve an invalid or self-signed certificate."
|
||||
msgstr ""
|
||||
|
||||
@ -1400,23 +1444,23 @@ msgstr ""
|
||||
msgid "Use this only when the router has working IPv6 connectivity."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470
|
||||
msgid "Use with Exclusion sections to route specific domains directly."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684
|
||||
msgid "User Domains"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711
|
||||
msgid "User Domains List"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:772
|
||||
msgid "User Subnets"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:799
|
||||
msgid "User Subnets List"
|
||||
msgstr ""
|
||||
|
||||
@ -1442,8 +1486,8 @@ msgstr ""
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831
|
||||
msgid "Validation errors:"
|
||||
msgstr ""
|
||||
|
||||
@ -1460,21 +1504,26 @@ msgstr ""
|
||||
msgid "Visit Wiki"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:253
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:308
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:279
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:334
|
||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links — one per line"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612
|
||||
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:631
|
||||
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466
|
||||
msgid "When enabled, traffic not matching any other section's lists will go through this proxy."
|
||||
msgstr ""
|
||||
|
||||
@ -1482,11 +1531,11 @@ msgstr ""
|
||||
msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151
|
||||
msgid "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:156
|
||||
msgid "Xray JSON (Happ)"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=netshift
|
||||
|
||||
PKG_VERSION := $(if $(NETSHIFT_VERSION),$(NETSHIFT_VERSION),0.$(shell date +%d%m%Y))
|
||||
PKG_VERSION := $(if $(NETSHIFT_VERSION),$(NETSHIFT_VERSION),0.9.6)
|
||||
|
||||
PKG_RELEASE:=1
|
||||
|
||||
|
||||
@ -70,6 +70,12 @@ config section 'main'
|
||||
# # is set (an explicit UA always wins).
|
||||
# #option subscription_format_preference 'auto'
|
||||
# option subscription_update_interval '1h'
|
||||
# # Node grouping: off | country | prefix. 'country' clusters by leading
|
||||
# # flag emoji; 'prefix' clusters by the first N codepoints of each node
|
||||
# # name (N = subscription_group_prefix_len). subscription_group_mode
|
||||
# # outranks the legacy subscription_group_by_countries boolean below.
|
||||
# #option subscription_group_mode 'off'
|
||||
# #option subscription_group_prefix_len '2'
|
||||
# #option subscription_group_by_countries '0'
|
||||
# #option urltest_check_interval '3m'
|
||||
# #option urltest_tolerance '50'
|
||||
@ -80,5 +86,28 @@ config section 'main'
|
||||
# #list subscription_filter_include_keywords 'grpc'
|
||||
# # Keyword blacklist: drop any node whose display name contains any of
|
||||
# # these (OR). Empty/absent = no exclusion.
|
||||
# #list subscription_filter_exclude_keywords 'expired'
|
||||
# list community_lists 'russia_inside'
|
||||
# #list subscription_filter_exclude_keywords 'expired'
|
||||
# list community_lists 'russia_inside'
|
||||
|
||||
# Text-list Selector / URLTest: paste proxy links into a single multi-line
|
||||
# textarea option (one link per line) instead of one DynamicList item each. The
|
||||
# supported links (vless/vmess/ss/trojan/hysteria2/socks) are built into a
|
||||
# Selector ('selector_text') or a URLTest + selector ('urltest_text'); blank
|
||||
# lines and unsupported schemes are skipped. The value is a scalar newline-
|
||||
# delimited blob — NOT a UCI list.
|
||||
#config section 'selector_text_example'
|
||||
# option connection_type 'proxy'
|
||||
# option proxy_config_type 'selector_text'
|
||||
# # one link per line in a single multi-line option value
|
||||
# option selector_proxy_links_text 'vless://example-node-1
|
||||
#vless://example-node-2'
|
||||
|
||||
#config section 'urltest_text_example'
|
||||
# option connection_type 'proxy'
|
||||
# option proxy_config_type 'urltest_text'
|
||||
# # one link per line in a single multi-line option value
|
||||
# option urltest_proxy_links_text 'vless://example-node-1
|
||||
#vless://example-node-2'
|
||||
# #option urltest_check_interval '3m'
|
||||
# #option urltest_tolerance '50'
|
||||
# #option urltest_testing_url 'https://www.gstatic.com/generate_204'
|
||||
|
||||
@ -80,7 +80,7 @@ check_requirements() {
|
||||
if has_outbound_section; then
|
||||
log "Outbound section found" "debug"
|
||||
else
|
||||
log "Outbound section not found. Please check your configuration file (missing proxy_string, selector_proxy_links, urltest_proxy_links, subscription_url, outbound_json, or interface according to connection_type/proxy_config_type). Aborted." "error"
|
||||
log "Outbound section not found. Please check your configuration file (missing proxy_string, selector_proxy_links, urltest_proxy_links, selector_proxy_links_text, urltest_proxy_links_text, subscription_url, outbound_json, or interface according to connection_type/proxy_config_type). Aborted." "error"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@ -111,15 +111,26 @@ section_has_configured_outbound() {
|
||||
config_get urltest_proxy_links "$section" "urltest_proxy_links"
|
||||
[ -n "$urltest_proxy_links" ] && return 0
|
||||
;;
|
||||
selector_text)
|
||||
local selector_proxy_links_text
|
||||
config_get selector_proxy_links_text "$section" "selector_proxy_links_text"
|
||||
[ -n "$selector_proxy_links_text" ] && return 0
|
||||
;;
|
||||
urltest_text)
|
||||
local urltest_proxy_links_text
|
||||
config_get urltest_proxy_links_text "$section" "urltest_proxy_links_text"
|
||||
[ -n "$urltest_proxy_links_text" ] && return 0
|
||||
;;
|
||||
outbound)
|
||||
local outbound_json
|
||||
config_get outbound_json "$section" "outbound_json"
|
||||
[ -n "$outbound_json" ] && return 0
|
||||
;;
|
||||
subscription)
|
||||
# subscription_url is now a UCI list (a lone legacy option reads as a
|
||||
# 1-element list). The section has a configured outbound if at least
|
||||
# one URL is present.
|
||||
# get_subscription_urls_for_section handles both the UCI `list` shape
|
||||
# (new UI configs) and a scalar `option` shape (legacy / CLI /
|
||||
# migrated configs), so the section has a configured outbound if it
|
||||
# returns at least one URL.
|
||||
[ -n "$(get_subscription_urls_for_section "$section")" ] && return 0
|
||||
;;
|
||||
esac
|
||||
@ -197,9 +208,10 @@ get_subscription_user_agent_cache_path() {
|
||||
echo "$SUBSCRIPTION_CACHE_FOLDER/${section}${urlhash:+.$urlhash}.user_agent"
|
||||
}
|
||||
|
||||
# Collect a section's subscription_url entries (a UCI list, but a lone legacy
|
||||
# `option subscription_url` reads as a 1-element list exactly like
|
||||
# community_lists) into the newline-delimited global SUBSCRIPTION_URLS_COLLECTED.
|
||||
# Collect a section's subscription_url entries (a UCI list; a legacy / CLI /
|
||||
# migrated `option subscription_url` is a scalar that config_list_foreach does
|
||||
# NOT iterate — get_subscription_urls_for_section handles that shape with a
|
||||
# scalar fallback) into the newline-delimited global SUBSCRIPTION_URLS_COLLECTED.
|
||||
# URLs are opaque user text and may contain shell-special chars, so they are
|
||||
# accumulated newline-delimited (URLs cannot contain a newline) and consumers
|
||||
# read them with `while IFS= read -r`, never via word-splitting.
|
||||
@ -217,9 +229,23 @@ $url"
|
||||
|
||||
get_subscription_urls_for_section() {
|
||||
local section="$1"
|
||||
local scalar_url
|
||||
|
||||
SUBSCRIPTION_URLS_COLLECTED=""
|
||||
config_list_foreach "$section" "subscription_url" _collect_subscription_url_handler
|
||||
|
||||
# Backward compat: legacy / CLI / podkop-migrated configs store
|
||||
# subscription_url as a scalar `option` (not a `list`). config_list_foreach
|
||||
# iterates ONLY list values, so it returns nothing for a scalar option. Fall
|
||||
# back to a scalar read and treat it as a 1-element list. (PROVEN on hardware:
|
||||
# config_list_foreach over an option => empty; config_get => the value.) This
|
||||
# is the load-bearing fix and must stand alone even when the option->list
|
||||
# migration is skipped (read-only fs / uci failure).
|
||||
if [ -z "$SUBSCRIPTION_URLS_COLLECTED" ]; then
|
||||
config_get scalar_url "$section" "subscription_url"
|
||||
[ -n "$scalar_url" ] && _collect_subscription_url_handler "$scalar_url"
|
||||
fi
|
||||
|
||||
printf '%s' "$SUBSCRIPTION_URLS_COLLECTED"
|
||||
}
|
||||
|
||||
@ -583,6 +609,16 @@ download_subscription_into_cache() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# Some panels return a gzip-compressed body unconditionally (we send no
|
||||
# Accept-Encoding and busybox wget does not transparently decompress).
|
||||
# Decompress in place before validate/normalize so every consumer sees
|
||||
# text. Best-effort: a plain-text body is left untouched.
|
||||
maybe_gunzip_subscription_file "$tmpfile"
|
||||
if subscription_body_is_binary "$tmpfile"; then
|
||||
log "Subscription body for section '$section' is binary/undecodable after gzip handling (not text, not gzip) with User-Agent '$effective_user_agent'; trying next candidate" "warn"
|
||||
continue
|
||||
fi
|
||||
|
||||
file_size="$(wc -c < "$tmpfile" 2>/dev/null | tr -d ' ')"
|
||||
log "Downloaded subscription body for section '$section': bytes=${file_size:-unknown}, User-Agent='$effective_user_agent'" "debug"
|
||||
|
||||
@ -829,6 +865,12 @@ start_subscription_startup_retry_worker() {
|
||||
start_main() {
|
||||
log "Starting netshift"
|
||||
|
||||
# Normalize legacy scalar `option subscription_url` to the canonical `list`
|
||||
# shape BEFORE check_requirements / config generation read the URLs. The
|
||||
# read-fallback in get_subscription_urls_for_section already covers
|
||||
# correctness; this is hygiene that converges stored configs to a list.
|
||||
migrate_legacy_subscription_url_option
|
||||
|
||||
check_requirements
|
||||
|
||||
migration
|
||||
@ -1160,6 +1202,71 @@ migration() {
|
||||
:
|
||||
}
|
||||
|
||||
# config_foreach callback for migrate_legacy_subscription_url_option. For a
|
||||
# subscription section whose subscription_url is stored as a scalar `option`
|
||||
# (legacy / CLI / podkop-migrated configs) rather than a UCI `list`, rewrite it
|
||||
# in place as a `list` via uci. Detects the broken shape robustly: the LIST read
|
||||
# (config_list_foreach) yields nothing AND a scalar config_get is non-empty —
|
||||
# exactly the option-only shape, so an already-correct list is never touched.
|
||||
# Sets the module-level SUBSCRIPTION_URL_OPTION_MIGRATED flag when it changes
|
||||
# anything so the caller commits + reloads exactly once. Never exits: any uci
|
||||
# failure is logged at warn and skipped (the read-fallback in
|
||||
# get_subscription_urls_for_section covers correctness regardless).
|
||||
_migrate_legacy_subscription_url_option_handler() {
|
||||
local section="$1"
|
||||
local connection_type proxy_config_type scalar_url
|
||||
|
||||
config_get connection_type "$section" "connection_type"
|
||||
[ "$connection_type" = "proxy" ] || return 0
|
||||
|
||||
config_get proxy_config_type "$section" "proxy_config_type" "url"
|
||||
[ "$proxy_config_type" = "subscription" ] || return 0
|
||||
|
||||
# Only the broken shape: empty via the list path but present as a scalar.
|
||||
SUBSCRIPTION_URLS_COLLECTED=""
|
||||
config_list_foreach "$section" "subscription_url" _collect_subscription_url_handler
|
||||
[ -z "$SUBSCRIPTION_URLS_COLLECTED" ] || return 0
|
||||
|
||||
config_get scalar_url "$section" "subscription_url"
|
||||
[ -n "$scalar_url" ] || return 0
|
||||
|
||||
# Rewrite the scalar option as a list. Use the uci_add_list SHELL HELPER
|
||||
# (from /lib/functions.sh), NOT the `uci add_list "key=value"` CLI form: the
|
||||
# CLI form splits on the FIRST `=`, so a URL with a query string (very common,
|
||||
# e.g. "...?token=abc&x=1") makes the CLI add_list fail and lose the value.
|
||||
# The helper passes the value as a separate argument, preserving `=`/`&`
|
||||
# byte-for-byte. Delete the scalar first so the result is a CLEAN single-
|
||||
# element list (adding while the scalar option still exists would duplicate
|
||||
# it into a 2-element list); if the add then fails, RESTORE the scalar option
|
||||
# so a failed migration can never leave the section with NO url. Never exits.
|
||||
uci -q delete "netshift.${section}.subscription_url" 2>/dev/null
|
||||
if uci_add_list netshift "$section" subscription_url "$scalar_url" 2>/dev/null; then
|
||||
SUBSCRIPTION_URL_OPTION_MIGRATED=1
|
||||
log "Migrated legacy scalar subscription_url to list for section '$section'" "info"
|
||||
else
|
||||
uci_set netshift "$section" subscription_url "$scalar_url"
|
||||
log "Failed to migrate scalar subscription_url to list for section '$section'; restored the original option and continuing (read fallback covers correctness)" "warn"
|
||||
fi
|
||||
}
|
||||
|
||||
# One-time, idempotent normalization of a legacy scalar `option subscription_url`
|
||||
# into the canonical `list subscription_url`. Runs once at startup AFTER
|
||||
# config_load and BEFORE config generation reads the URLs. Idempotent: a config
|
||||
# already using `list` is left untouched (no commit, no churn). Never exits.
|
||||
migrate_legacy_subscription_url_option() {
|
||||
SUBSCRIPTION_URL_OPTION_MIGRATED=0
|
||||
|
||||
config_foreach _migrate_legacy_subscription_url_option_handler "section"
|
||||
|
||||
if [ "$SUBSCRIPTION_URL_OPTION_MIGRATED" -eq 1 ]; then
|
||||
if uci commit "netshift" 2>/dev/null; then
|
||||
config_load "$NETSHIFT_CONFIG"
|
||||
else
|
||||
log "Failed to commit subscription_url option->list migration; continuing (read fallback covers correctness)" "warn"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
validate_service() {
|
||||
local service="$1"
|
||||
|
||||
@ -2141,10 +2248,25 @@ sing_box_get_unique_outbound_tag() {
|
||||
echo "$candidate"
|
||||
}
|
||||
|
||||
sing_box_build_subscription_country_groups() {
|
||||
# Mode-aware subscription group-key builder (task-044). Generalizes the former
|
||||
# country-only grouper into off/country/prefix modes. Returns the shape
|
||||
# {group_order: [...], groups: {key: [tags]}, ungrouped: [...]}.
|
||||
# - mode=country: byte-identical to the legacy flag extractor (regional-
|
||||
# indicator gate; non-flag tags -> ungrouped).
|
||||
# - mode=prefix: group key = first N codepoints of the tag (N = $3). A tag
|
||||
# shorter than N codepoints keys by its WHOLE tag (still groups with
|
||||
# identical short tags); an empty tag -> ungrouped. $3 is coerced via
|
||||
# tonumber, floored to 1; bad/0/empty -> default 2.
|
||||
# No Oniguruma jq (explode/implode/slice/index/reduce only).
|
||||
sing_box_build_subscription_groups() {
|
||||
local subscription_outbound_tags_json="$1"
|
||||
local mode="$2"
|
||||
local prefix_len="$3"
|
||||
|
||||
printf '%s' "$subscription_outbound_tags_json" | jq -c '
|
||||
printf '%s' "$subscription_outbound_tags_json" | jq -c \
|
||||
--arg mode "$mode" \
|
||||
--arg prefix_len "$prefix_len" \
|
||||
--argjson default_len "$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN" '
|
||||
def is_regional_indicator: . >= 127462 and . <= 127487;
|
||||
def extract_country_flag:
|
||||
(. | explode) as $codepoints
|
||||
@ -2154,17 +2276,33 @@ sing_box_build_subscription_country_groups() {
|
||||
then ($codepoints[0:2] | implode)
|
||||
else ""
|
||||
end;
|
||||
# extract_prefix($n): group key = first $n codepoints, or "" (ungrouped)
|
||||
# for an empty tag. A short tag keys by its whole self.
|
||||
def extract_prefix($n):
|
||||
(. | explode) as $codepoints
|
||||
| if ($codepoints | length) == 0
|
||||
then ""
|
||||
else ($codepoints[0:$n] | implode)
|
||||
end;
|
||||
|
||||
(if type == "array" then . else [] end) as $tags
|
||||
# Effective prefix length: coerce, floor to 1, fall back to default.
|
||||
(try ($prefix_len | tonumber) catch $default_len) as $raw_len
|
||||
| (if ($raw_len | type) != "number" or $raw_len < 1
|
||||
then $default_len
|
||||
else ($raw_len | floor)
|
||||
end) as $n
|
||||
|
||||
| (if type == "array" then . else [] end) as $tags
|
||||
| reduce $tags[] as $tag (
|
||||
{country_order: [], country_groups: {}, ungrouped: []};
|
||||
($tag | extract_country_flag) as $country_flag
|
||||
| if $country_flag == "" then
|
||||
{group_order: [], groups: {}, ungrouped: []};
|
||||
(if $mode == "prefix" then ($tag | extract_prefix($n))
|
||||
else ($tag | extract_country_flag) end) as $key
|
||||
| if $key == "" then
|
||||
.ungrouped += [$tag]
|
||||
else
|
||||
.country_groups[$country_flag] = ((.country_groups[$country_flag] // []) + [$tag])
|
||||
| if (.country_order | index($country_flag)) == null then
|
||||
.country_order += [$country_flag]
|
||||
.groups[$key] = ((.groups[$key] // []) + [$tag])
|
||||
| if (.group_order | index($key)) == null then
|
||||
.group_order += [$key]
|
||||
else
|
||||
.
|
||||
end
|
||||
@ -2186,6 +2324,61 @@ is_truthy_option() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Shared per-link member-outbound builder for the selector/urltest/selector_text/
|
||||
# urltest_text proxy types. Iterates a whitespace/newline-separated blob of proxy
|
||||
# links, creating one outbound per supported link via the facade and collecting
|
||||
# their tags. Unsupported schemes are skipped with a warn (the facade echoes the
|
||||
# config UNCHANGED and returns non-zero, so a single bad link never wipes the
|
||||
# config nor leaves a selector referencing a non-existent outbound). A trailing
|
||||
# CR (pasted CRLF blobs) is stripped per link and blank lines are skipped, so the
|
||||
# text-list types tolerate pasted multi-line input. POSIX ash, no Oniguruma.
|
||||
#
|
||||
# Mutates the GLOBAL $config in place (same echo-and-reassign discipline as the
|
||||
# subscription branch uses for its in-shell loop) and reports results via two
|
||||
# GLOBALS the caller reads after the call:
|
||||
# _member_outbound_tags comma-joined member outbound tags (empty if none)
|
||||
# _member_default_outbound first member tag (selector default; empty if none)
|
||||
# Args: <section> <links_blob> <udp_over_tcp> <label>
|
||||
_build_proxy_member_outbounds() {
|
||||
local section="$1"
|
||||
local links_blob="$2"
|
||||
local udp_over_tcp="$3"
|
||||
local label="$4"
|
||||
local link i outbound_tag _new_config cr
|
||||
|
||||
_member_outbound_tags=""
|
||||
_member_default_outbound=""
|
||||
cr="$(printf '\r')"
|
||||
|
||||
i=1
|
||||
for link in $links_blob; do
|
||||
# Tolerate pasted CRLF blobs: strip a trailing CR and skip blank lines.
|
||||
link="${link%"$cr"}"
|
||||
[ -n "$link" ] || continue
|
||||
|
||||
# The facade returns non-zero (config echoed UNCHANGED) for an
|
||||
# unsupported scheme. Only add the member tag when the outbound was
|
||||
# actually created, so the selector/urltest never references a
|
||||
# non-existent outbound; a single bad link is skipped and the remaining
|
||||
# links still build. Reassign $config only on a non-empty result so a
|
||||
# skip never wipes the config.
|
||||
if _new_config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" \
|
||||
&& [ -n "$_new_config" ]; then
|
||||
config="$_new_config"
|
||||
outbound_tag="$(get_outbound_tag_by_section "$section-$i")"
|
||||
if [ -z "$_member_outbound_tags" ]; then
|
||||
_member_outbound_tags="$outbound_tag"
|
||||
_member_default_outbound="$outbound_tag"
|
||||
else
|
||||
_member_outbound_tags="$_member_outbound_tags,$outbound_tag"
|
||||
fi
|
||||
else
|
||||
log "$label section '$section' link #$i uses an unsupported scheme; skipping it" "warn"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
configure_outbound_handler() {
|
||||
local section="$1"
|
||||
|
||||
@ -2232,7 +2425,7 @@ configure_outbound_handler() {
|
||||
;;
|
||||
selector)
|
||||
log "Detected proxy configuration type: selector" "debug"
|
||||
local selector_proxy_links udp_over_tcp i outbound_tags outbound_tag default_outbound _new_config
|
||||
local selector_proxy_links udp_over_tcp selector_tag selector_outbounds
|
||||
config_get selector_proxy_links "$section" "selector_proxy_links"
|
||||
config_get udp_over_tcp "$section" "enable_udp_over_tcp"
|
||||
|
||||
@ -2241,44 +2434,22 @@ configure_outbound_handler() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
i=1
|
||||
for link in $selector_proxy_links; do
|
||||
# The facade returns non-zero (config echoed UNCHANGED) for an
|
||||
# unsupported scheme. Only add the member tag when the outbound
|
||||
# was actually created, so the selector never references a
|
||||
# non-existent outbound; a single bad link is skipped and the
|
||||
# remaining links still build. Reassign $config only on a
|
||||
# non-empty result so a skip never wipes the config.
|
||||
if _new_config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" \
|
||||
&& [ -n "$_new_config" ]; then
|
||||
config="$_new_config"
|
||||
outbound_tag="$(get_outbound_tag_by_section "$section-$i")"
|
||||
if [ -z "$outbound_tags" ]; then
|
||||
outbound_tags="$outbound_tag"
|
||||
default_outbound="$outbound_tag"
|
||||
else
|
||||
outbound_tags="$outbound_tags,$outbound_tag"
|
||||
fi
|
||||
else
|
||||
log "Selector section '$section' link #$i uses an unsupported scheme; skipping it" "warn"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
_build_proxy_member_outbounds "$section" "$selector_proxy_links" "$udp_over_tcp" "Selector"
|
||||
|
||||
if [ -z "$outbound_tags" ]; then
|
||||
if [ -z "$_member_outbound_tags" ]; then
|
||||
echolog "Selector section '$section' has no usable links (all unsupported); its traffic will be rejected until a supported link is configured" "error"
|
||||
mark_section_outbound_unavailable "$section"
|
||||
else
|
||||
selector_tag="$(get_outbound_tag_by_section "$section")"
|
||||
selector_outbounds="$(comma_string_to_json_array "$outbound_tags")"
|
||||
selector_outbounds="$(comma_string_to_json_array "$_member_outbound_tags")"
|
||||
config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" \
|
||||
"$default_outbound" "true")"
|
||||
"$_member_default_outbound" "true")"
|
||||
fi
|
||||
;;
|
||||
urltest)
|
||||
log "Detected proxy configuration type: urltest" "debug"
|
||||
local urltest_proxy_links udp_over_tcp i urltest_tag selector_tag outbound_tag outbound_tags \
|
||||
urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url _new_config
|
||||
local urltest_proxy_links udp_over_tcp urltest_tag selector_tag \
|
||||
urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url
|
||||
config_get urltest_proxy_links "$section" "urltest_proxy_links"
|
||||
config_get udp_over_tcp "$section" "enable_udp_over_tcp"
|
||||
config_get urltest_check_interval "$section" "urltest_check_interval" "3m"
|
||||
@ -2290,37 +2461,69 @@ configure_outbound_handler() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
i=1
|
||||
for link in $urltest_proxy_links; do
|
||||
# The facade returns non-zero (config echoed UNCHANGED) for an
|
||||
# unsupported scheme. Only add the member tag when the outbound
|
||||
# was actually created, so the urltest/selector never references
|
||||
# a non-existent outbound; a single bad link is skipped and the
|
||||
# remaining links still build. Reassign $config only on a
|
||||
# non-empty result so a skip never wipes the config.
|
||||
if _new_config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" \
|
||||
&& [ -n "$_new_config" ]; then
|
||||
config="$_new_config"
|
||||
outbound_tag="$(get_outbound_tag_by_section "$section-$i")"
|
||||
if [ -z "$outbound_tags" ]; then
|
||||
outbound_tags="$outbound_tag"
|
||||
else
|
||||
outbound_tags="$outbound_tags,$outbound_tag"
|
||||
fi
|
||||
else
|
||||
log "URLTest section '$section' link #$i uses an unsupported scheme; skipping it" "warn"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
_build_proxy_member_outbounds "$section" "$urltest_proxy_links" "$udp_over_tcp" "URLTest"
|
||||
|
||||
if [ -z "$outbound_tags" ]; then
|
||||
if [ -z "$_member_outbound_tags" ]; then
|
||||
echolog "URLTest section '$section' has no usable links (all unsupported); its traffic will be rejected until a supported link is configured" "error"
|
||||
mark_section_outbound_unavailable "$section"
|
||||
else
|
||||
urltest_tag="$(get_outbound_tag_by_section "$section-urltest")"
|
||||
selector_tag="$(get_outbound_tag_by_section "$section")"
|
||||
urltest_outbounds="$(comma_string_to_json_array "$outbound_tags")"
|
||||
selector_outbounds="$(comma_string_to_json_array "$outbound_tags,$urltest_tag")"
|
||||
urltest_outbounds="$(comma_string_to_json_array "$_member_outbound_tags")"
|
||||
selector_outbounds="$(comma_string_to_json_array "$_member_outbound_tags,$urltest_tag")"
|
||||
config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \
|
||||
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")"
|
||||
config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")"
|
||||
fi
|
||||
;;
|
||||
selector_text)
|
||||
log "Detected proxy configuration type: selector_text" "debug"
|
||||
local selector_text_links udp_over_tcp selector_tag selector_outbounds
|
||||
config_get selector_text_links "$section" "selector_proxy_links_text"
|
||||
config_get udp_over_tcp "$section" "enable_udp_over_tcp"
|
||||
|
||||
if [ -z "$selector_text_links" ]; then
|
||||
log "URLTest proxy links is not set. Aborted." "fatal"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_build_proxy_member_outbounds "$section" "$selector_text_links" "$udp_over_tcp" "Selector"
|
||||
|
||||
if [ -z "$_member_outbound_tags" ]; then
|
||||
echolog "Selector section '$section' has no usable links (all unsupported); its traffic will be rejected until a supported link is configured" "error"
|
||||
mark_section_outbound_unavailable "$section"
|
||||
else
|
||||
selector_tag="$(get_outbound_tag_by_section "$section")"
|
||||
selector_outbounds="$(comma_string_to_json_array "$_member_outbound_tags")"
|
||||
config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" \
|
||||
"$_member_default_outbound" "true")"
|
||||
fi
|
||||
;;
|
||||
urltest_text)
|
||||
log "Detected proxy configuration type: urltest_text" "debug"
|
||||
local urltest_text_links udp_over_tcp urltest_tag selector_tag \
|
||||
urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url
|
||||
config_get urltest_text_links "$section" "urltest_proxy_links_text"
|
||||
config_get udp_over_tcp "$section" "enable_udp_over_tcp"
|
||||
config_get urltest_check_interval "$section" "urltest_check_interval" "3m"
|
||||
config_get urltest_tolerance "$section" "urltest_tolerance" 50
|
||||
config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204"
|
||||
|
||||
if [ -z "$urltest_text_links" ]; then
|
||||
log "URLTest proxy links is not set. Aborted." "fatal"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_build_proxy_member_outbounds "$section" "$urltest_text_links" "$udp_over_tcp" "URLTest"
|
||||
|
||||
if [ -z "$_member_outbound_tags" ]; then
|
||||
echolog "URLTest section '$section' has no usable links (all unsupported); its traffic will be rejected until a supported link is configured" "error"
|
||||
mark_section_outbound_unavailable "$section"
|
||||
else
|
||||
urltest_tag="$(get_outbound_tag_by_section "$section-urltest")"
|
||||
selector_tag="$(get_outbound_tag_by_section "$section")"
|
||||
urltest_outbounds="$(comma_string_to_json_array "$_member_outbound_tags")"
|
||||
selector_outbounds="$(comma_string_to_json_array "$_member_outbound_tags,$urltest_tag")"
|
||||
config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \
|
||||
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")"
|
||||
config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")"
|
||||
@ -2330,7 +2533,7 @@ configure_outbound_handler() {
|
||||
log "Detected proxy configuration type: subscription" "debug"
|
||||
local subscription_urls_tmp subscription_url subscription_url_count urltest_tag selector_tag \
|
||||
urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \
|
||||
urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw \
|
||||
urltest_testing_url group_mode group_mode_raw prefix_len prefix_len_raw legacy_group_raw \
|
||||
subscription_outbound_tags_json service_proxy_address subscription_ready \
|
||||
subscription_filter_include_keywords_json subscription_filter_exclude_keywords_json \
|
||||
subscription_keyword_filter_active urlhash subscription_json_path subscription_url_cache_path \
|
||||
@ -2340,19 +2543,54 @@ configure_outbound_handler() {
|
||||
config_get urltest_check_interval "$section" "urltest_check_interval" "3m"
|
||||
config_get urltest_tolerance "$section" "urltest_tolerance" 50
|
||||
config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204"
|
||||
config_get subscription_group_by_countries_raw "$section" "subscription_group_by_countries" ""
|
||||
if [ -z "$subscription_group_by_countries_raw" ]; then
|
||||
# Backward-compatible alias in case custom builds used another key
|
||||
config_get subscription_group_by_countries_raw "$section" "group_by_countries" ""
|
||||
fi
|
||||
# Grouping mode (task-044): off | country | prefix. The new
|
||||
# subscription_group_mode option OUTRANKS the legacy boolean. When
|
||||
# the new option is absent we fall back to the legacy boolean
|
||||
# subscription_group_by_countries (and its older alias
|
||||
# group_by_countries): truthy => country, else off.
|
||||
config_get group_mode_raw "$section" "subscription_group_mode" ""
|
||||
config_get prefix_len_raw "$section" "subscription_group_prefix_len" "$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN"
|
||||
|
||||
if is_truthy_option "$subscription_group_by_countries_raw"; then
|
||||
subscription_group_by_countries=1
|
||||
if [ -z "$group_mode_raw" ]; then
|
||||
config_get legacy_group_raw "$section" "subscription_group_by_countries" ""
|
||||
if [ -z "$legacy_group_raw" ]; then
|
||||
config_get legacy_group_raw "$section" "group_by_countries" ""
|
||||
fi
|
||||
if is_truthy_option "$legacy_group_raw"; then
|
||||
group_mode="country"
|
||||
else
|
||||
group_mode="off"
|
||||
fi
|
||||
else
|
||||
subscription_group_by_countries=0
|
||||
case "$group_mode_raw" in
|
||||
off | country | prefix)
|
||||
group_mode="$group_mode_raw"
|
||||
;;
|
||||
*)
|
||||
group_mode="off"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
log "Subscription country grouping for section '$section': raw='${subscription_group_by_countries_raw:-<empty>}', enabled=$subscription_group_by_countries" "debug"
|
||||
# Sanitize the prefix length: positive integer only, else default.
|
||||
case "$prefix_len_raw" in
|
||||
'' | *[!0-9]*)
|
||||
prefix_len="$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN"
|
||||
;;
|
||||
*)
|
||||
if [ "$prefix_len_raw" -ge 1 ] 2>/dev/null; then
|
||||
prefix_len="$prefix_len_raw"
|
||||
else
|
||||
prefix_len="$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$group_mode" = "prefix" ]; then
|
||||
log "Subscription grouping for section '$section': mode=$group_mode, prefix_len=$prefix_len" "debug"
|
||||
else
|
||||
log "Subscription grouping for section '$section': mode=$group_mode" "debug"
|
||||
fi
|
||||
|
||||
# Keyword whitelist/blacklist filtering of subscription nodes by
|
||||
# display name. Both are optional UCI lists of opaque match strings.
|
||||
@ -2519,41 +2757,84 @@ configure_outbound_handler() {
|
||||
subscription_outbound_tags_json="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")"
|
||||
fi
|
||||
|
||||
if [ "$subscription_group_by_countries" -eq 1 ]; then
|
||||
local grouping_json country_flag country_group_outbounds country_group_tag \
|
||||
selector_outbounds_json selector_default ungrouped_outbounds_json grouped_count ungrouped_count
|
||||
if [ "$group_mode" != "off" ]; then
|
||||
local grouping_json group_key group_outbounds group_tag group_keys_tmp \
|
||||
selector_outbounds_json selector_default ungrouped_outbounds_json grouped_count ungrouped_count \
|
||||
group_tags_json group_tags_count fastest_tag
|
||||
|
||||
grouping_json="$(sing_box_build_subscription_country_groups "$subscription_outbound_tags_json")"
|
||||
grouping_json="$(sing_box_build_subscription_groups "$subscription_outbound_tags_json" "$group_mode" "$prefix_len")"
|
||||
if [ -z "$grouping_json" ]; then
|
||||
log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grouped_count="$(echo "$grouping_json" | jq -r '.country_order | length' 2>/dev/null)"
|
||||
grouped_count="$(echo "$grouping_json" | jq -r '.group_order | length' 2>/dev/null)"
|
||||
ungrouped_count="$(echo "$grouping_json" | jq -r '.ungrouped | length' 2>/dev/null)"
|
||||
log "Country grouping prepared for section '$section': groups=$grouped_count, ungrouped=$ungrouped_count" "debug"
|
||||
log "Subscription grouping prepared for section '$section' (mode=$group_mode): groups=$grouped_count, ungrouped=$ungrouped_count" "debug"
|
||||
|
||||
selector_outbounds_json="[]"
|
||||
|
||||
for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do
|
||||
country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)"
|
||||
if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then
|
||||
# Iterate group keys WITHOUT word-splitting: prefix-mode keys
|
||||
# can legitimately contain spaces (e.g. a "letter+space"
|
||||
# prefix), which a `for k in $(...)` loop would shatter. Use
|
||||
# the same mktemp + `while read < file` pattern as the URL
|
||||
# loop above so the body runs in the CURRENT shell and its
|
||||
# mutations to $config / $selector_outbounds_json survive
|
||||
# (a `... | while read` body would run in a subshell and lose
|
||||
# them). One key per line; group keys never contain newlines.
|
||||
group_keys_tmp="$(mktemp "${TMPDIR:-/tmp}/netshift-cfg-groupkeys.XXXXXX")" || {
|
||||
log "Failed to enumerate subscription group keys for section '$section'. Aborted." "fatal"
|
||||
exit 1
|
||||
}
|
||||
echo "$grouping_json" | jq -r '.group_order[]' 2>/dev/null > "$group_keys_tmp"
|
||||
while IFS= read -r group_key || [ -n "$group_key" ]; do
|
||||
group_outbounds="$(echo "$grouping_json" | jq -c --arg group_key "$group_key" '.groups[$group_key] // []' 2>/dev/null)"
|
||||
if [ -z "$group_outbounds" ] || [ "$group_outbounds" = "[]" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
country_group_tag="$(sing_box_get_unique_outbound_tag "$config" "$country_flag Fastest")"
|
||||
config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \
|
||||
group_tag="$(sing_box_get_unique_outbound_tag "$config" "$group_key Fastest")"
|
||||
config="$(sing_box_cm_add_urltest_outbound "$config" "$group_tag" "$group_outbounds" \
|
||||
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")"
|
||||
|
||||
selector_outbounds_json=$(
|
||||
printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$country_group_tag" '. + [$tag]' 2>/dev/null
|
||||
printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$group_tag" '. + [$tag]' 2>/dev/null
|
||||
)
|
||||
done
|
||||
done < "$group_keys_tmp"
|
||||
rm -f "$group_keys_tmp"
|
||||
|
||||
if [ -z "$selector_outbounds_json" ]; then
|
||||
selector_outbounds_json="[]"
|
||||
fi
|
||||
|
||||
# At this point $selector_outbounds_json holds ONLY the
|
||||
# per-group urltest tags ("<key> Fastest"), before ungrouped
|
||||
# nodes are appended below. Capture that group-only list so
|
||||
# we can build a top-level "Fastest" urltest OVER the groups
|
||||
# (a urltest of urltests) and default the selector to it.
|
||||
group_tags_json="$selector_outbounds_json"
|
||||
group_tags_count="$(printf '%s' "$group_tags_json" | jq -r 'length' 2>/dev/null)"
|
||||
[ -n "$group_tags_count" ] || group_tags_count=0
|
||||
|
||||
fastest_tag=""
|
||||
# Only nest when there are >= 2 groups. With exactly 1 group
|
||||
# the lone group urltest IS already the fastest (a urltest
|
||||
# over a single member is redundant), so we skip the extra
|
||||
# layer and fall back to the lone group as the default. With
|
||||
# 0 groups (everything ungrouped) there is nothing to nest;
|
||||
# never emit an empty-member urltest.
|
||||
if [ "$group_tags_count" -ge 2 ]; then
|
||||
fastest_tag="$(sing_box_get_unique_outbound_tag "$config" "$SB_SUBSCRIPTION_FASTEST_GROUP_TAG")"
|
||||
# Reuse the section's urltest probe knobs (testing URL,
|
||||
# check interval, tolerance) so the extra probe layer's
|
||||
# cadence stays user-tunable; do NOT hardcode an
|
||||
# aggressive interval. This adds one more probe layer
|
||||
# (Fastest tests each group; each group tests its nodes).
|
||||
config="$(sing_box_cm_add_urltest_outbound "$config" "$fastest_tag" "$group_tags_json" \
|
||||
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")"
|
||||
selector_outbounds_json="$(jq -acn --arg t "$fastest_tag" --argjson rest "$selector_outbounds_json" '[$t] + $rest')"
|
||||
fi
|
||||
|
||||
ungrouped_outbounds_json="$(echo "$grouping_json" | jq -c '.ungrouped // []' 2>/dev/null)"
|
||||
if [ -n "$ungrouped_outbounds_json" ] && [ "$ungrouped_outbounds_json" != "[]" ]; then
|
||||
selector_outbounds_json=$(
|
||||
@ -2567,7 +2848,16 @@ configure_outbound_handler() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
selector_default="$(echo "$selector_outbounds_json" | jq -r '.[0] // ""' 2>/dev/null)"
|
||||
if [ -n "$fastest_tag" ]; then
|
||||
# >= 2 groups: the cross-group "Fastest" urltest is the
|
||||
# default (it was prepended above, so it is also .[0]).
|
||||
selector_default="$fastest_tag"
|
||||
else
|
||||
# 0 or 1 groups: keep the existing "first element"
|
||||
# default (the lone group urltest, or the first
|
||||
# ungrouped node).
|
||||
selector_default="$(echo "$selector_outbounds_json" | jq -r '.[0] // ""' 2>/dev/null)"
|
||||
fi
|
||||
if [ -z "$selector_default" ] || [ "$selector_default" = "null" ]; then
|
||||
log "Unable to determine default selector outbound for section '$section'. Aborted." "fatal"
|
||||
exit 1
|
||||
|
||||
@ -28,12 +28,16 @@ TMP_SUBSCRIPTION_MERGE_FOLDER="$TMP_SING_BOX_FOLDER/subscription-merge"
|
||||
# "singbox/<version>" candidate is prepended at runtime (it depends on the
|
||||
# installed sing-box). Order matters: most-likely-to-work first.
|
||||
SUBSCRIPTION_USER_AGENT_CANDIDATES="v2rayN Happ Hiddify Clash.Meta ClashMetaForAndroid"
|
||||
# Subset of SUBSCRIPTION_USER_AGENT_CANDIDATES that well-known panels answer with
|
||||
# an Xray JSON body (which carries xhttp/transport nodes the default sing-box JSON
|
||||
# may omit). Used by build_subscription_user_agent_candidates when a section's
|
||||
# Versioned client UAs that well-known panels answer with an Xray JSON body
|
||||
# (which carries xhttp/transport nodes the default sing-box JSON may omit). Used
|
||||
# by build_subscription_user_agent_candidates when a section's
|
||||
# subscription_format_preference is "xray": these UAs are probed FIRST so an
|
||||
# Xray-JSON feed is recovered before a sing-box JSON under the default UA wins.
|
||||
SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="v2rayN Happ"
|
||||
# Panels commonly gate their Xray branch on a "<client>/<version>" UA shape, so
|
||||
# these are VERSIONED (a bare/version-less UA can be rejected, e.g. with a 502).
|
||||
# Order matters: a versioned Happ is first (empirically yields the Xray-JSON
|
||||
# array body), then versioned v2rayN/v2rayNG forms as panel-agnostic fallbacks.
|
||||
SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="Happ/1.0.0 v2rayN/7.0.0 v2rayNG/1.9.0"
|
||||
CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker
|
||||
JQ_REQUIRED_VERSION="1.7.1"
|
||||
COREUTILS_BASE64_REQUIRED_VERSION="9.7"
|
||||
@ -87,7 +91,16 @@ UPDATES_LIBCRONET_LIB="/usr/lib/libcronet.so"
|
||||
# Component Manager — NetShift self-update (task-017). The GitHub latest-release
|
||||
# API for NetShift itself (same endpoint install.sh and get_system_info use);
|
||||
# the self-update worker downloads the release .ipk/.apk assets from it.
|
||||
NETSHIFT_RELEASE_API_URL="https://api.github.com/repos/yandexru45/netshift/releases/latest"
|
||||
NETSHIFT_RELEASE_API_URL="https://uralgit.ru/api/v1/repos/ural/netshift/releases/latest"
|
||||
# uralgit.ru FRONTEND redirect path (NOT the rate-limited api.github.com)
|
||||
# for the NetShift repo. /releases/latest 303-redirects to /releases/tag/<tag>
|
||||
# (resolve with curl -w '%{redirect_url}' — no API hit, not subject to the
|
||||
# 60/hour/IP anonymous API limit); /releases/download/<tag>/<asset> for direct
|
||||
# asset download. Primary path for version-check + self-update;
|
||||
# NETSHIFT_RELEASE_API_URL stays as the graceful fallback. Repo slug lives here
|
||||
# only — do not hardcode it elsewhere.
|
||||
NETSHIFT_REPO_RELEASES_LATEST_URL="https://uralgit.ru/ural/netshift/releases/latest"
|
||||
NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE="https://uralgit.ru/ural/netshift/releases/download"
|
||||
# tmpfs scratch dir for the self-update download (release packages) — RAM, never
|
||||
# the tiny overlay; reaped on success and on reboot.
|
||||
UPDATES_NETSHIFT_DOWNLOAD_DIR="/tmp/netshift/selfupdate"
|
||||
@ -123,6 +136,15 @@ SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1"
|
||||
SB_SERVICE_MIXED_INBOUND_PORT=4534
|
||||
# Outbounds
|
||||
SB_DIRECT_OUTBOUND_TAG="direct-out"
|
||||
# Subscription grouping (task-044). Default codepoint count for prefix-mode
|
||||
# grouping when subscription_group_prefix_len is unset/invalid.
|
||||
SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN=2
|
||||
# Subscription grouping (task-050). Tag/label for the top-level "Fastest"
|
||||
# urltest that probes ACROSS the per-group urltests (a urltest of urltests)
|
||||
# when grouping is on. Valid UTF-8 emoji + English; deliberately distinct from
|
||||
# a per-group "<flag> Fastest" tag so the cross-group auto choice is tellable
|
||||
# apart in the dashboard. Single source for the tag (keep this file UTF-8).
|
||||
SB_SUBSCRIPTION_FASTEST_GROUP_TAG="⚡ Fastest"
|
||||
# Route
|
||||
SB_REJECT_RULE_TAG="reject-rule-tag"
|
||||
SB_EXCLUSION_RULE_TAG="exclusion-rule-tag"
|
||||
@ -135,8 +157,8 @@ DOH_BLOCK_IPV4_CIDRS="1.1.1.1/32 1.0.0.1/32 8.8.8.8/32 8.8.4.4/32 9.9.9.9/32 9.9
|
||||
DOH_BLOCK_IPV6_CIDRS="2606:4700:4700::1111/128 2606:4700:4700::1001/128 2001:4860:4860::8888/128 2001:4860:4860::8844/128 2620:fe::fe/128 2620:fe::9/128 2620:119:35::35/128 2620:119:53::53/128 2a10:50c0::ad1:ff/128 2a10:50c0::ad2:ff/128 2a02:6b8::feed:0ff/128 2a02:6b8:0:1::feed:0ff/128"
|
||||
|
||||
## Lists
|
||||
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
|
||||
SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download"
|
||||
GITHUB_RAW_URL="https://uralgit.ru/ural/allow-domains/raw/branch/main"
|
||||
SRS_MAIN_URL="https://uralgit.ru/ural/allow-domains/releases/latest/download"
|
||||
SUBNETS_TWITTER="${GITHUB_RAW_URL}/Subnets/IPv4/twitter.lst"
|
||||
SUBNETS_META="${GITHUB_RAW_URL}/Subnets/IPv4/meta.lst"
|
||||
SUBNETS_DISCORD="${GITHUB_RAW_URL}/Subnets/IPv4/discord.lst"
|
||||
|
||||
@ -572,6 +572,58 @@ convert_crlf_to_lf() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Best-effort, in-place gzip decompression of a downloaded subscription body.
|
||||
#
|
||||
# Some panels unconditionally return a gzip-compressed HTTP body (busybox wget
|
||||
# does NOT transparently decompress and we send no Accept-Encoding), so the raw
|
||||
# bytes are binary and every downstream consumer (validate/normalize) chokes.
|
||||
# This decompresses once at download time so all consumers see text.
|
||||
#
|
||||
# Detection is attempt-based (no od/hexdump/xxd, none of which exist on device):
|
||||
# we try `gzip -dc` (busybox built-in) into a temp file and accept the result
|
||||
# ONLY if (a) gzip returned 0, (b) the result is non-empty, and (c) the result
|
||||
# is NUL-free. gzip -dc on plain-text input returns rc!=0 cleanly, so a
|
||||
# plain-text body is left byte-for-byte untouched; this can never corrupt text.
|
||||
# Modeled on convert_crlf_to_lf: mktemp -> transform -> mv on success / rm on
|
||||
# failure. Best-effort: always returns 0 (never aborts the caller).
|
||||
maybe_gunzip_subscription_file() {
|
||||
local filepath="$1"
|
||||
local tmpfile
|
||||
|
||||
[ -s "$filepath" ] || return 0
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
if gzip -dc "$filepath" > "$tmpfile" 2>/dev/null &&
|
||||
[ -s "$tmpfile" ] &&
|
||||
! subscription_body_is_binary "$tmpfile"; then
|
||||
log "Decompressed gzip subscription body for '$filepath'" "debug"
|
||||
mv "$tmpfile" "$filepath"
|
||||
else
|
||||
rm -f "$tmpfile"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Returns 0 (true) if the file contains at least one NUL byte (i.e. it is
|
||||
# binary / undecodable, not text). Busybox-safe, no od/hexdump/xxd: `tr -d`
|
||||
# strips NUL bytes and we compare the resulting byte count to the original; a
|
||||
# difference means a NUL was present. All vars local.
|
||||
subscription_body_is_binary() {
|
||||
local filepath="$1"
|
||||
local raw_count stripped_count
|
||||
|
||||
[ -s "$filepath" ] || return 1
|
||||
|
||||
raw_count="$(wc -c < "$filepath" 2>/dev/null | tr -d ' ')"
|
||||
stripped_count="$(tr -d '\000' < "$filepath" 2>/dev/null | wc -c 2>/dev/null | tr -d ' ')"
|
||||
|
||||
[ -n "$raw_count" ] || raw_count=0
|
||||
[ -n "$stripped_count" ] || stripped_count=0
|
||||
|
||||
[ "$raw_count" != "$stripped_count" ]
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Parses a whitespace-separated string, validates items as either domains
|
||||
# or IPv4 addresses/subnets, and returns a comma-separated string of valid items.
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
# This file is sourced from /usr/bin/netshift, so log() is available.
|
||||
|
||||
SB_EXT_ARCH_SUFFIX=""
|
||||
UPDATES_SING_BOX_EXTENDED_REPO="shtorm-7/sing-box-extended"
|
||||
UPDATES_SING_BOX_EXTENDED_REPO="ural/sing-box-extended"
|
||||
|
||||
# Async component-action job state. State lives on tmpfs (/var/run): it survives
|
||||
# the rpcd call that started the worker but is intentionally transient (cleared
|
||||
@ -550,7 +550,7 @@ updates_http_get_once() {
|
||||
# mistaken for a releases list.
|
||||
updates_fetch_sing_box_extended_releases() {
|
||||
local url response proxy
|
||||
url="https://api.github.com/repos/${UPDATES_SING_BOX_EXTENDED_REPO}/releases?per_page=30"
|
||||
url="https://uralgit.ru/api/v1/repos/${UPDATES_SING_BOX_EXTENDED_REPO}/releases?per_page=30"
|
||||
|
||||
response="$(updates_http_get_once "$url" "")"
|
||||
if updates_response_is_release_array "$response"; then
|
||||
@ -1407,7 +1407,15 @@ updates_pkg_install_file() {
|
||||
if updates_pkg_is_apk; then
|
||||
apk add --allow-untrusted "$pkg_file" </dev/null >/dev/null 2>&1
|
||||
else
|
||||
opkg install "$pkg_file" </dev/null >/dev/null 2>&1
|
||||
# --force-downgrade: a legacy v-prefixed build (e.g. v0.8.6) sorts ABOVE
|
||||
# the no-v target (0.8.7) in opkg's dpkg-style compare, so a plain
|
||||
# `opkg install` returns rc=0 and refuses ("Not downgrading ..."). The
|
||||
# flag forces the v->no-v transition to actually land.
|
||||
# --force-reinstall: covers the "already installed at this exact version"
|
||||
# no-op. opkg rc is NOT a reliable success signal either way — the
|
||||
# verify-after-install belt in _updates_self_update_netshift_core is the
|
||||
# authoritative check.
|
||||
opkg install --force-downgrade --force-reinstall "$pkg_file" </dev/null >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
@ -1447,6 +1455,32 @@ updates_pkg_candidate_version() {
|
||||
printf '%s' "$version"
|
||||
}
|
||||
|
||||
# Echoes the INSTALLED version of a package (what is on the system right now),
|
||||
# or nothing if the package is not installed. Distinct from
|
||||
# updates_pkg_candidate_version (that reads the FEED candidate). Parsed with
|
||||
# grep/awk only — NEVER Oniguruma jq. Mirrors updates_pkg_is_installed.
|
||||
# opkg list-installed -> "<name> - <version>" (field after " - ")
|
||||
# apk list --installed <pkg> -> "<name>-<version> <arch> {...} ..." (strip "<name>-")
|
||||
updates_pkg_installed_version() {
|
||||
local pkg_name="$1"
|
||||
local line version=""
|
||||
|
||||
if updates_pkg_is_apk; then
|
||||
# First installed-list token is "<name>-<version>"; strip the leading
|
||||
# "<pkg>-" so only the version (e.g. "0.8.7-r1") remains.
|
||||
line="$(apk list --installed "$pkg_name" 2>/dev/null | awk '{print $1}' | head -n1)"
|
||||
case "$line" in
|
||||
"$pkg_name"-*) version="${line#"$pkg_name"-}" ;;
|
||||
esac
|
||||
else
|
||||
# opkg list-installed prints "<name> - <version>"; take the field after
|
||||
# " - " for the exact package name.
|
||||
version="$(opkg list-installed 2>/dev/null | grep "^${pkg_name} " | head -n1 | awk -F' - ' '{print $2}')"
|
||||
fi
|
||||
|
||||
printf '%s' "$version"
|
||||
}
|
||||
|
||||
# Checks whether a newer STOCK (stable) sing-box is available via the system
|
||||
# package manager. SYNC (quick call → stays on the synchronous component_action
|
||||
# path). Graceful on an unreachable feed / parse failure: echoes
|
||||
@ -1592,38 +1626,124 @@ updates_self_update_netshift() {
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Echoes the GitHub latest-release tag for NetShift (e.g. "v0.8.1"), or nothing.
|
||||
# Reuses the same API endpoint as get_system_info / install.sh; parsed with
|
||||
# grep/cut (the tag is needed only as a display/compare string, no jq array).
|
||||
updates_netshift_latest_tag() {
|
||||
local response
|
||||
# Resolve a URL's HTTP redirect target via curl WITHOUT hitting the rate-limited
|
||||
# API or downloading the body. Echoes the redirect URL (empty if curl absent or
|
||||
# no redirect). Stubbable in tests.
|
||||
updates_github_resolve_redirect() {
|
||||
local url="$1"
|
||||
command -v curl >/dev/null 2>&1 || return 1
|
||||
curl -sI -o /dev/null -w '%{redirect_url}' --connect-timeout 5 -m 15 -A 'netshift-updater' "$url" 2>/dev/null
|
||||
}
|
||||
|
||||
# Echoes the GitHub latest-release tag for NetShift (e.g. "0.8.8"), or nothing.
|
||||
# PRIMARY: resolve the uralgit.ru frontend redirect of /releases/latest — it
|
||||
# 303s to /releases/tag/<tag>. That frontend is NOT the 60/hour-per-IP
|
||||
# api.github.com, so it sidesteps the anonymous rate limit entirely (the common
|
||||
# failure on CGNAT / shared-IP / shared-VPN-egress routers). FALLBACK: the
|
||||
# uralgit.ru API release object parsed with jq (task-047) so a curl-less box or a
|
||||
# changed-redirect still degrades gracefully instead of hard-failing.
|
||||
# jq is format-independent (minified or pretty); a field-positional grep|cut
|
||||
# grabbed the wrong key on minified JSON, causing a false "outdated".
|
||||
# Bare tag on success / non-zero otherwise (contract consumed by
|
||||
# updates_check_netshift and the self-update worker).
|
||||
updates_netshift_latest_tag() {
|
||||
local response tag redirect
|
||||
|
||||
# PRIMARY: uralgit.ru/<repo>/releases/latest 303-redirects to
|
||||
# /releases/tag/<tag>. Parse with case/param-expansion (no Oniguruma).
|
||||
redirect="$(updates_github_resolve_redirect "$NETSHIFT_REPO_RELEASES_LATEST_URL")"
|
||||
case "$redirect" in
|
||||
*/releases/tag/*)
|
||||
tag="${redirect##*/releases/tag/}"
|
||||
case "$tag" in '' | */*) tag="" ;; esac
|
||||
;;
|
||||
*) tag="" ;;
|
||||
esac
|
||||
if [ -n "$tag" ]; then
|
||||
printf '%s' "$tag"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# FALLBACK: api.github.com (rate-limited) parsed with jq.
|
||||
response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")"
|
||||
if [ -z "$response" ]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$response" | grep '"tag_name":' | head -n1 | cut -d'"' -f4
|
||||
|
||||
tag="$(printf '%s' "$response" | jq -r '.tag_name // empty' 2>/dev/null)"
|
||||
[ -n "$tag" ] || return 1
|
||||
printf '%s' "$tag"
|
||||
}
|
||||
|
||||
# Downloads the NetShift release assets matching the package-name prefixes for
|
||||
# the active package manager into $dir. Echoes nothing; returns 0 if at least
|
||||
# the core "netshift" package was downloaded, non-zero otherwise. The asset URL
|
||||
# list comes from the same latest-release JSON, filtered to .ipk or .apk by the
|
||||
# package manager (busybox grep -o, no jq array walk required).
|
||||
# Echo the deterministic release asset filename for a package + tag + ext.
|
||||
# ipk core/luci carry "-r1-all"; apk core/luci carry "-r1"; the i18n package
|
||||
# carries neither suffix (just "<pkg>-<tag>.<ext>"). Single source of the asset
|
||||
# naming pattern so it lives in one place, not scattered.
|
||||
updates_netshift_asset_filename() {
|
||||
local pkg="$1" tag="$2" ext="$3"
|
||||
case "$pkg" in
|
||||
"$UPDATES_NETSHIFT_PKG_I18N_RU") printf '%s-%s.%s' "$pkg" "$tag" "$ext" ;;
|
||||
*)
|
||||
if [ "$ext" = "ipk" ]; then
|
||||
printf '%s-%s-r1-all.%s' "$pkg" "$tag" "$ext"
|
||||
else
|
||||
printf '%s-%s-r1.%s' "$pkg" "$tag" "$ext"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Downloads the NetShift release assets for the active package manager into $dir.
|
||||
# Echoes nothing; returns 0 if at least the core "netshift" package was
|
||||
# downloaded, non-zero otherwise.
|
||||
# PRIMARY: resolve the latest tag (redirect-based, rate-limit-free) and build the
|
||||
# deterministic github.com/<repo>/releases/download/<tag>/<asset> URLs — the
|
||||
# CDN 302 is followed by updates_download_to_file (curl -L / wget both follow it).
|
||||
# FALLBACK: if the tag can't be resolved, scrape the api.github.com release JSON
|
||||
# for .ipk/.apk URLs (busybox grep -o) as before, so a curl-less box still works.
|
||||
_updates_self_update_download_assets() {
|
||||
local dir="$1"
|
||||
local response ext pattern url filename dest attempt got_core=0
|
||||
|
||||
response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")"
|
||||
if [ -z "$response" ]; then
|
||||
return 1
|
||||
fi
|
||||
local tag pkg
|
||||
|
||||
if updates_pkg_is_apk; then
|
||||
ext="apk"
|
||||
else
|
||||
ext="ipk"
|
||||
fi
|
||||
|
||||
tag="$(updates_netshift_latest_tag)"
|
||||
if [ -n "$tag" ]; then
|
||||
# Direct deterministic asset URLs (no API). Core + luci always; the RU
|
||||
# i18n package only if already installed.
|
||||
for pkg in "$UPDATES_NETSHIFT_PKG_CORE" "$UPDATES_NETSHIFT_PKG_LUCI" "$UPDATES_NETSHIFT_PKG_I18N_RU"; do
|
||||
if [ "$pkg" = "$UPDATES_NETSHIFT_PKG_I18N_RU" ]; then
|
||||
updates_pkg_is_installed "$UPDATES_NETSHIFT_PKG_I18N_RU" || continue
|
||||
fi
|
||||
filename="$(updates_netshift_asset_filename "$pkg" "$tag" "$ext")"
|
||||
url="$NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE/$tag/$filename"
|
||||
dest="$dir/$filename"
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 3 ]; do
|
||||
if updates_download_to_file "$url" "$dest"; then
|
||||
break
|
||||
fi
|
||||
rm -f "$dest" 2>/dev/null
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
if [ "$pkg" = "$UPDATES_NETSHIFT_PKG_CORE" ] && [ -s "$dest" ]; then
|
||||
got_core=1
|
||||
fi
|
||||
done
|
||||
[ "$got_core" -eq 1 ]
|
||||
return $?
|
||||
fi
|
||||
|
||||
# FALLBACK: scrape the API release JSON for direct asset URLs.
|
||||
response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")"
|
||||
if [ -z "$response" ]; then
|
||||
return 1
|
||||
fi
|
||||
pattern="https://[^\"[:space:]]*\.${ext}"
|
||||
|
||||
# Iterate the matching browser_download_url values. Only keep assets whose
|
||||
@ -1663,6 +1783,7 @@ _updates_self_update_download_assets() {
|
||||
# failure so the wrapper still runs the restore epilogue).
|
||||
_updates_self_update_netshift_core() {
|
||||
local installed latest pkg file_path candidate_file
|
||||
local core_installed core_installed_semver latest_semver
|
||||
local backup_made=0
|
||||
|
||||
installed="$NETSHIFT_VERSION"
|
||||
@ -1741,6 +1862,31 @@ _updates_self_update_netshift_core() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify-after-install for the CORE package (authoritative success signal).
|
||||
# opkg returns rc=0 for "already installed"/"up to date"/"Not downgrading",
|
||||
# so the install rc above is NOT trustworthy. RE-READ the installed version
|
||||
# and confirm it actually became the target before declaring success. apk's
|
||||
# equal-version no-overwrite quirk is caught by this same belt.
|
||||
core_installed="$(updates_pkg_installed_version "$UPDATES_NETSHIFT_PKG_CORE")"
|
||||
# Normalize with the SAME rules the version-decision uses: drop a leading "v"
|
||||
# and any "-rN"/"-suffix" so we compare semver-to-semver.
|
||||
core_installed_semver="${core_installed#v}"
|
||||
core_installed_semver="${core_installed_semver%%-*}"
|
||||
latest_semver="${latest#v}"
|
||||
latest_semver="${latest_semver%%-*}"
|
||||
|
||||
# The install took iff the installed semver equals the target, OR the
|
||||
# installed semver is now >= the target (is_min_package_version current
|
||||
# required → 0 when current >= required).
|
||||
if [ "$core_installed_semver" != "$latest_semver" ] \
|
||||
&& ! is_min_package_version "$core_installed_semver" "$latest_semver"; then
|
||||
_updates_self_update_restore_config "$backup_made"
|
||||
rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR" 2>/dev/null
|
||||
updates_log "Self-update: core package version did not change after install (package manager reported success but no upgrade occurred)" "error"
|
||||
echo '{"success":false,"message":"NetShift core package did not upgrade (package manager refused or no-op); configuration preserved"}'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Defensive: if the config got clobbered/emptied, restore from the backup.
|
||||
_updates_self_update_restore_config "$backup_made"
|
||||
|
||||
|
||||
@ -9,9 +9,10 @@
|
||||
# docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name>
|
||||
#
|
||||
# Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft,
|
||||
# nftv6, selmark, isolation, monfd, unsupported, diagnostics, subscription, insecure, rejected,
|
||||
# jobstate, selfheal, dnsdetour, globalproxy, stablecheck,
|
||||
# extcheck, netshiftcheck, selfupdate, backupguard
|
||||
# nftv6, selmark, isolation, monfd, unsupported, textlist, diagnostics, subscription, fastest, insecure, rejected,
|
||||
# jobstate, selfheal, dnsdetour, suburlopt, globalproxy, stablecheck,
|
||||
# extcheck, netshiftcheck, latesttag, ghredirect, selfupdate,
|
||||
# backupguard
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
services:
|
||||
|
||||
1231
tests/entrypoint.sh
1231
tests/entrypoint.sh
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user