Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce3c917f8a | |||
| a70339bf32 | |||
| da72c64b50 | |||
| 883811bd55 | |||
| ba75930510 | |||
| f63e0b9fd9 |
145
README.md
145
README.md
@ -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>
|
||||
|
||||
@ -113,14 +118,29 @@ sh <(wget -O - https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/
|
||||
|
||||
Интерфейс появится в 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
|
||||
|
||||
|
||||
@ -1276,3 +1276,176 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure ->
|
||||
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.
|
||||
|
||||
@ -67,3 +67,9 @@ append recurring findings; keep under ~200 lines.
|
||||
|
||||
- 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).
|
||||
|
||||
@ -861,3 +861,64 @@ append findings; keep under ~200 lines.
|
||||
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).
|
||||
|
||||
@ -1487,3 +1487,245 @@ findings; keep under ~200 lines.
|
||||
`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:607"
|
||||
"../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:695"
|
||||
"../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:390"
|
||||
"../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:667"
|
||||
"../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:756"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -165,14 +172,14 @@
|
||||
"call": "By country flag",
|
||||
"key": "By country flag",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:200"
|
||||
"../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:201"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:203"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -272,7 +279,7 @@
|
||||
"call": "Community Lists",
|
||||
"key": "Community Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -364,14 +371,14 @@
|
||||
"call": "Custom domains",
|
||||
"key": "Custom domains",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:606"
|
||||
"../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:694"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -449,15 +456,15 @@
|
||||
"call": "Disabled",
|
||||
"key": "Disabled",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:699"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -485,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:479",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:542",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48"
|
||||
]
|
||||
},
|
||||
@ -493,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:480",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49"
|
||||
]
|
||||
},
|
||||
@ -501,7 +508,7 @@
|
||||
"call": "DNS Protocol Type",
|
||||
"key": "DNS Protocol Type",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:476",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:539",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45"
|
||||
]
|
||||
},
|
||||
@ -516,7 +523,7 @@
|
||||
"call": "DNS Server",
|
||||
"key": "DNS Server",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:490",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:553",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58"
|
||||
]
|
||||
},
|
||||
@ -545,7 +552,7 @@
|
||||
"call": "Domain Resolver",
|
||||
"key": "Domain Resolver",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -596,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:239"
|
||||
"../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:612",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:700"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:763"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -618,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:466"
|
||||
"../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:933"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:996"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -646,7 +653,7 @@
|
||||
"call": "Enable Mixed Proxy",
|
||||
"key": "Enable Mixed Proxy",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:902"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:965"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -660,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:903"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:966"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -681,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:649"
|
||||
"../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:622"
|
||||
"../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:710"
|
||||
"../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:306"
|
||||
"../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:307"
|
||||
"../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:305"
|
||||
"../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:308"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -793,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:238"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:240"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -816,6 +823,12 @@
|
||||
"call": "Failed to execute!",
|
||||
"key": "Failed to execute!",
|
||||
"places": [
|
||||
"src/netshift/tabs/diagnostic/initController.ts:229",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:233",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:263",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:267",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:304",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:308",
|
||||
"src/netshift/tabs/manager/initController.ts:122",
|
||||
"src/netshift/tabs/manager/initController.ts:132",
|
||||
"src/netshift/tabs/manager/initController.ts:150",
|
||||
@ -823,13 +836,7 @@
|
||||
"src/netshift/tabs/manager/initController.ts:188",
|
||||
"src/netshift/tabs/manager/initController.ts:192",
|
||||
"src/netshift/tabs/manager/initController.ts:225",
|
||||
"src/netshift/tabs/manager/initController.ts:229",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:229",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:233",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:263",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:267",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:304",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:308"
|
||||
"src/netshift/tabs/manager/initController.ts:229"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -846,7 +853,7 @@
|
||||
"call": "Fully Routed IPs",
|
||||
"key": "Fully Routed IPs",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:874"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:937"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -867,21 +874,21 @@
|
||||
"call": "Global Proxy",
|
||||
"key": "Global Proxy",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:463"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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: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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -895,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:226"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:228"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1321,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:227"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1339,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",
|
||||
@ -1364,14 +1378,14 @@
|
||||
"call": "Local Domain Lists",
|
||||
"key": "Local Domain Lists",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:778"
|
||||
"../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:802"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:865"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1413,7 +1427,7 @@
|
||||
"call": "Mixed Proxy Port",
|
||||
"key": "Mixed Proxy Port",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:916"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:979"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1427,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:343"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:405"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1469,7 +1483,7 @@
|
||||
"call": "Network Interface",
|
||||
"key": "Network Interface",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1520,21 +1534,21 @@
|
||||
"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:211"
|
||||
"../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:199"
|
||||
"../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:409"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1548,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1616,7 +1630,7 @@
|
||||
"call": "Prefix length",
|
||||
"key": "Prefix length",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:210"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:212"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1630,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1651,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:547"
|
||||
"../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:826"
|
||||
"../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:850"
|
||||
"../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:932"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:995"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1686,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:401"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:464"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1770,7 +1784,7 @@
|
||||
"call": "Russia inside restrictions",
|
||||
"key": "Russia inside restrictions",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:566"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1791,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:514"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1826,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:419"
|
||||
"../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:491",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:554",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59"
|
||||
]
|
||||
},
|
||||
@ -1855,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:477"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1893,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:250"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1940,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"
|
||||
]
|
||||
},
|
||||
@ -2018,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:875"
|
||||
"../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:827"
|
||||
"../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:851"
|
||||
"../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:779",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:803"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:842",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:866"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2062,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2083,28 +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:194"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2167,8 +2195,8 @@
|
||||
"call": "Text List",
|
||||
"key": "Text List",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:613",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:764"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2182,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:303"
|
||||
"../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:318"
|
||||
"../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:351"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2252,7 +2280,7 @@
|
||||
"call": "UDP (Unprotected DNS)",
|
||||
"key": "UDP (Unprotected DNS)",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50"
|
||||
]
|
||||
},
|
||||
@ -2260,25 +2288,25 @@
|
||||
"call": "UDP over TCP",
|
||||
"key": "UDP over TCP",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:389"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:452"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "unknown",
|
||||
"key": "unknown",
|
||||
"places": [
|
||||
"src/netshift/tabs/manager/initController.ts:37",
|
||||
"src/netshift/tabs/manager/initController.ts:38",
|
||||
"src/netshift/tabs/manager/initController.ts:39",
|
||||
"src/netshift/tabs/manager/initController.ts:40",
|
||||
"src/netshift/tabs/manager/initController.ts:41",
|
||||
"src/netshift/tabs/manager/initController.ts:42",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:39",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:40",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:41",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:42",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:43",
|
||||
"src/netshift/tabs/diagnostic/initController.ts:44",
|
||||
"src/netshift/tabs/manager/initController.ts:37",
|
||||
"src/netshift/tabs/manager/initController.ts:38",
|
||||
"src/netshift/tabs/manager/initController.ts:39",
|
||||
"src/netshift/tabs/manager/initController.ts:40",
|
||||
"src/netshift/tabs/manager/initController.ts:41",
|
||||
"src/netshift/tabs/manager/initController.ts:42",
|
||||
"src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7"
|
||||
]
|
||||
},
|
||||
@ -2354,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:302"
|
||||
"../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:276"
|
||||
"../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:350"
|
||||
"../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:317"
|
||||
"../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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2400,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:407"
|
||||
"../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:621"
|
||||
"../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:648"
|
||||
"../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:709"
|
||||
"../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:736"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:799"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2460,8 +2502,8 @@
|
||||
"call": "Validation errors:",
|
||||
"key": "Validation errors:",
|
||||
"places": [
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2490,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:251",
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:277"
|
||||
"../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:549"
|
||||
"../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:568"
|
||||
"../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:403"
|
||||
"../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -2527,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-12 05:52+0300\n"
|
||||
"PO-Revision-Date: 2026-06-12 05:52+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:607
|
||||
#: ../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:695
|
||||
#: ../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:390
|
||||
#: ../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:667
|
||||
#: 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:756
|
||||
#: ../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,11 +113,11 @@ msgstr ""
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:200
|
||||
#: ../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:201
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:203
|
||||
msgid "By name prefix"
|
||||
msgstr ""
|
||||
|
||||
@ -174,7 +178,7 @@ msgstr ""
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576
|
||||
msgid "Community Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -227,11 +231,11 @@ msgstr ""
|
||||
msgid "Currently unavailable"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:606
|
||||
#: ../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:694
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757
|
||||
msgid "Custom subnets"
|
||||
msgstr ""
|
||||
|
||||
@ -276,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:611
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:699
|
||||
#: ../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 ""
|
||||
|
||||
@ -297,17 +301,17 @@ msgstr ""
|
||||
msgid "DNS outbound section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479
|
||||
#: ../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:480
|
||||
#: ../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:476
|
||||
#: ../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 ""
|
||||
@ -316,7 +320,7 @@ msgstr ""
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:490
|
||||
#: ../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 ""
|
||||
@ -333,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:465
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528
|
||||
msgid "Domain Resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -363,12 +367,12 @@ msgstr ""
|
||||
msgid "Downloading all lists via specific Proxy/VPN"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:239
|
||||
#: ../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:612
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:700
|
||||
#: ../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 ""
|
||||
|
||||
@ -376,11 +380,11 @@ msgstr ""
|
||||
msgid "Enable autostart"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466
|
||||
#: ../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:933
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:996
|
||||
msgid "Enable DNS resolve to get real IP when routing"
|
||||
msgstr ""
|
||||
|
||||
@ -392,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:902
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:965
|
||||
msgid "Enable Mixed Proxy"
|
||||
msgstr ""
|
||||
|
||||
@ -400,7 +404,7 @@ msgstr ""
|
||||
msgid "Enable Output Network Interface"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:903
|
||||
#: ../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 ""
|
||||
|
||||
@ -412,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:649
|
||||
#: ../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:622
|
||||
#: ../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:710
|
||||
#: ../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:306
|
||||
#: ../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:307
|
||||
#: ../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:305
|
||||
#: ../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:308
|
||||
#: ../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 ""
|
||||
|
||||
@ -476,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:238
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:240
|
||||
msgid "Exclude servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -490,6 +494,12 @@ msgstr ""
|
||||
msgid "Failed to copy!"
|
||||
msgstr ""
|
||||
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:229
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:233
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:263
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:267
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:304
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:308
|
||||
#: src/netshift/tabs/manager/initController.ts:122
|
||||
#: src/netshift/tabs/manager/initController.ts:132
|
||||
#: src/netshift/tabs/manager/initController.ts:150
|
||||
@ -498,12 +508,6 @@ msgstr ""
|
||||
#: src/netshift/tabs/manager/initController.ts:192
|
||||
#: src/netshift/tabs/manager/initController.ts:225
|
||||
#: src/netshift/tabs/manager/initController.ts:229
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:229
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:233
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:263
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:267
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:304
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:308
|
||||
msgid "Failed to execute!"
|
||||
msgstr ""
|
||||
|
||||
@ -514,7 +518,7 @@ msgstr ""
|
||||
msgid "Fastest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:874
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:937
|
||||
msgid "Fully Routed IPs"
|
||||
msgstr ""
|
||||
|
||||
@ -526,15 +530,15 @@ msgstr ""
|
||||
msgid "Global check"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400
|
||||
#: ../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:195
|
||||
#: ../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:179
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181
|
||||
msgid "How often to automatically update the subscription"
|
||||
msgstr ""
|
||||
|
||||
@ -542,7 +546,7 @@ msgstr ""
|
||||
msgid "HTTP error"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:226
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:228
|
||||
msgid "Include servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -788,7 +792,7 @@ msgstr ""
|
||||
msgid "Issues detected"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:227
|
||||
#: ../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 ""
|
||||
|
||||
@ -801,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 ""
|
||||
@ -813,11 +821,11 @@ msgstr ""
|
||||
msgid "Lists & Updates"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:778
|
||||
#: ../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:802
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:865
|
||||
msgid "Local Subnet Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -841,7 +849,7 @@ msgstr ""
|
||||
msgid "Mixed proxy and DNS resolution tuning"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:916
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:979
|
||||
msgid "Mixed Proxy Port"
|
||||
msgstr ""
|
||||
|
||||
@ -849,7 +857,7 @@ msgstr ""
|
||||
msgid "Monitored Interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:343
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:405
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr ""
|
||||
|
||||
@ -873,7 +881,7 @@ msgstr ""
|
||||
msgid "Network"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481
|
||||
msgid "Network Interface"
|
||||
msgstr ""
|
||||
|
||||
@ -906,15 +914,15 @@ msgstr ""
|
||||
msgid "Not running"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:211
|
||||
#: ../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:199
|
||||
#: ../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:409
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472
|
||||
msgid "Only one section can be global at a time."
|
||||
msgstr ""
|
||||
|
||||
@ -922,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 ""
|
||||
|
||||
@ -963,7 +971,7 @@ msgstr ""
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:210
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:212
|
||||
msgid "Prefix length"
|
||||
msgstr ""
|
||||
|
||||
@ -971,7 +979,7 @@ msgstr ""
|
||||
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 ""
|
||||
|
||||
@ -983,19 +991,19 @@ msgstr ""
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547
|
||||
#: ../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:826
|
||||
#: ../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:850
|
||||
#: ../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:932
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:995
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -1003,7 +1011,7 @@ msgstr ""
|
||||
msgid "Restart NetShift"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:464
|
||||
msgid "Route all unmatched traffic through this section's outbound."
|
||||
msgstr ""
|
||||
|
||||
@ -1051,7 +1059,7 @@ msgstr ""
|
||||
msgid "Run Diagnostic"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:566
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629
|
||||
msgid "Russia inside restrictions"
|
||||
msgstr ""
|
||||
|
||||
@ -1063,7 +1071,7 @@ msgstr ""
|
||||
msgid "Sections"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577
|
||||
msgid "Select a predefined list for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -1083,11 +1091,11 @@ msgstr ""
|
||||
msgid "Select how to configure the proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:419
|
||||
#: ../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:491
|
||||
#: ../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 ""
|
||||
@ -1100,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:477
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540
|
||||
msgid "Select the DNS protocol type for the domain resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -1124,10 +1132,18 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:250
|
||||
#: ../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 ""
|
||||
@ -1149,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 ""
|
||||
@ -1194,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:875
|
||||
#: ../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:827
|
||||
#: ../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:851
|
||||
#: ../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:779
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:803
|
||||
#: ../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 ""
|
||||
|
||||
@ -1220,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 ""
|
||||
|
||||
@ -1232,19 +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:194
|
||||
#: ../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:178
|
||||
#: ../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 ""
|
||||
|
||||
@ -1280,8 +1296,8 @@ msgstr ""
|
||||
msgid "Test latency"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:613
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701
|
||||
#: ../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 ""
|
||||
|
||||
@ -1289,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:303
|
||||
#: ../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:318
|
||||
#: ../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:351
|
||||
#: ../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 ""
|
||||
|
||||
@ -1329,27 +1345,27 @@ msgstr ""
|
||||
msgid "TTL value cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481
|
||||
#: ../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:389
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:452
|
||||
msgid "UDP over TCP"
|
||||
msgstr ""
|
||||
|
||||
#: src/netshift/tabs/manager/initController.ts:37
|
||||
#: src/netshift/tabs/manager/initController.ts:38
|
||||
#: src/netshift/tabs/manager/initController.ts:39
|
||||
#: src/netshift/tabs/manager/initController.ts:40
|
||||
#: src/netshift/tabs/manager/initController.ts:41
|
||||
#: src/netshift/tabs/manager/initController.ts:42
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:39
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:40
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:41
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:42
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:43
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:44
|
||||
#: src/netshift/tabs/manager/initController.ts:37
|
||||
#: src/netshift/tabs/manager/initController.ts:38
|
||||
#: src/netshift/tabs/manager/initController.ts:39
|
||||
#: src/netshift/tabs/manager/initController.ts:40
|
||||
#: src/netshift/tabs/manager/initController.ts:41
|
||||
#: src/netshift/tabs/manager/initController.ts:42
|
||||
#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
@ -1396,23 +1412,31 @@ msgstr ""
|
||||
msgid "URLTest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:302
|
||||
#: ../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:276
|
||||
#: ../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:350
|
||||
#: ../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:317
|
||||
#: ../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 ""
|
||||
|
||||
@ -1420,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:407
|
||||
#: ../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:621
|
||||
#: ../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:648
|
||||
#: ../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:709
|
||||
#: ../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:736
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:799
|
||||
msgid "User Subnets List"
|
||||
msgstr ""
|
||||
|
||||
@ -1462,8 +1486,8 @@ msgstr ""
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768
|
||||
#: ../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 ""
|
||||
|
||||
@ -1480,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:251
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:277
|
||||
#: ../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:549
|
||||
#: ../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:568
|
||||
#: ../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:403
|
||||
#: ../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 ""
|
||||
|
||||
@ -1502,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-12 08:52+0300\n"
|
||||
"PO-Revision-Date: 2026-06-12 08:52+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 "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||
|
||||
@ -572,6 +575,9 @@ msgstr "Последняя"
|
||||
msgid "Latest version is installed"
|
||||
msgstr "Установлена последняя версия"
|
||||
|
||||
msgid "Line"
|
||||
msgstr "Строка"
|
||||
|
||||
msgid "List Update Frequency"
|
||||
msgstr "Частота обновления списков"
|
||||
|
||||
@ -803,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 "Не удалось обновить"
|
||||
|
||||
@ -992,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 ссылка для проверки"
|
||||
|
||||
@ -1043,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. Предыдущие варианты были удалены."
|
||||
|
||||
|
||||
@ -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: '' };
|
||||
}
|
||||
109
install.sh
109
install.sh
@ -2,6 +2,12 @@
|
||||
# shellcheck shell=dash
|
||||
|
||||
REPO="https://api.github.com/repos/yandexru45/netshift/releases/latest"
|
||||
# github.com FRONTEND redirect path (NOT the rate-limited api.github.com).
|
||||
# /releases/latest 302s to /releases/tag/<tag>; /releases/download/<tag>/<asset>
|
||||
# 302s to the CDN. Primary install path so CGNAT / shared-IP routers avoid the
|
||||
# 60/hour/IP API limit; REPO stays as the fallback.
|
||||
RELEASES_LATEST_REDIRECT="https://github.com/yandexru45/netshift/releases/latest"
|
||||
RELEASES_DOWNLOAD_BASE="https://github.com/yandexru45/netshift/releases/download"
|
||||
DOWNLOAD_DIR="/tmp/netshift"
|
||||
COUNT=3
|
||||
|
||||
@ -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 github.com 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";
|
||||
@ -269,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,
|
||||
@ -295,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,
|
||||
@ -308,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(
|
||||
@ -322,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) {
|
||||
@ -366,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-12 08:52+0300\n"
|
||||
"PO-Revision-Date: 2026-06-12 08:52+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 "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||
|
||||
@ -572,6 +575,9 @@ msgstr "Последняя"
|
||||
msgid "Latest version is installed"
|
||||
msgstr "Установлена последняя версия"
|
||||
|
||||
msgid "Line"
|
||||
msgstr "Строка"
|
||||
|
||||
msgid "List Update Frequency"
|
||||
msgstr "Частота обновления списков"
|
||||
|
||||
@ -803,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 "Не удалось обновить"
|
||||
|
||||
@ -992,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 ссылка для проверки"
|
||||
|
||||
@ -1043,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-12 05:52+0300\n"
|
||||
"PO-Revision-Date: 2026-06-12 05:52+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:607
|
||||
#: ../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:695
|
||||
#: ../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:390
|
||||
#: ../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:667
|
||||
#: 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:756
|
||||
#: ../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,11 +113,11 @@ msgstr ""
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:200
|
||||
#: ../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:201
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:203
|
||||
msgid "By name prefix"
|
||||
msgstr ""
|
||||
|
||||
@ -174,7 +178,7 @@ msgstr ""
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576
|
||||
msgid "Community Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -227,11 +231,11 @@ msgstr ""
|
||||
msgid "Currently unavailable"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:606
|
||||
#: ../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:694
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757
|
||||
msgid "Custom subnets"
|
||||
msgstr ""
|
||||
|
||||
@ -276,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:611
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:699
|
||||
#: ../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 ""
|
||||
|
||||
@ -297,17 +301,17 @@ msgstr ""
|
||||
msgid "DNS outbound section"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479
|
||||
#: ../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:480
|
||||
#: ../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:476
|
||||
#: ../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 ""
|
||||
@ -316,7 +320,7 @@ msgstr ""
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:490
|
||||
#: ../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 ""
|
||||
@ -333,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:465
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528
|
||||
msgid "Domain Resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -363,12 +367,12 @@ msgstr ""
|
||||
msgid "Downloading all lists via specific Proxy/VPN"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:239
|
||||
#: ../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:612
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:700
|
||||
#: ../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 ""
|
||||
|
||||
@ -376,11 +380,11 @@ msgstr ""
|
||||
msgid "Enable autostart"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466
|
||||
#: ../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:933
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:996
|
||||
msgid "Enable DNS resolve to get real IP when routing"
|
||||
msgstr ""
|
||||
|
||||
@ -392,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:902
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:965
|
||||
msgid "Enable Mixed Proxy"
|
||||
msgstr ""
|
||||
|
||||
@ -400,7 +404,7 @@ msgstr ""
|
||||
msgid "Enable Output Network Interface"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:903
|
||||
#: ../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 ""
|
||||
|
||||
@ -412,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:649
|
||||
#: ../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:622
|
||||
#: ../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:710
|
||||
#: ../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:306
|
||||
#: ../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:307
|
||||
#: ../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:305
|
||||
#: ../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:308
|
||||
#: ../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 ""
|
||||
|
||||
@ -476,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:238
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:240
|
||||
msgid "Exclude servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -490,6 +494,12 @@ msgstr ""
|
||||
msgid "Failed to copy!"
|
||||
msgstr ""
|
||||
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:229
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:233
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:263
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:267
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:304
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:308
|
||||
#: src/netshift/tabs/manager/initController.ts:122
|
||||
#: src/netshift/tabs/manager/initController.ts:132
|
||||
#: src/netshift/tabs/manager/initController.ts:150
|
||||
@ -498,12 +508,6 @@ msgstr ""
|
||||
#: src/netshift/tabs/manager/initController.ts:192
|
||||
#: src/netshift/tabs/manager/initController.ts:225
|
||||
#: src/netshift/tabs/manager/initController.ts:229
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:229
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:233
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:263
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:267
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:304
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:308
|
||||
msgid "Failed to execute!"
|
||||
msgstr ""
|
||||
|
||||
@ -514,7 +518,7 @@ msgstr ""
|
||||
msgid "Fastest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:874
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:937
|
||||
msgid "Fully Routed IPs"
|
||||
msgstr ""
|
||||
|
||||
@ -526,15 +530,15 @@ msgstr ""
|
||||
msgid "Global check"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400
|
||||
#: ../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:195
|
||||
#: ../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:179
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181
|
||||
msgid "How often to automatically update the subscription"
|
||||
msgstr ""
|
||||
|
||||
@ -542,7 +546,7 @@ msgstr ""
|
||||
msgid "HTTP error"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:226
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:228
|
||||
msgid "Include servers by keyword"
|
||||
msgstr ""
|
||||
|
||||
@ -788,7 +792,7 @@ msgstr ""
|
||||
msgid "Issues detected"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:227
|
||||
#: ../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 ""
|
||||
|
||||
@ -801,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 ""
|
||||
@ -813,11 +821,11 @@ msgstr ""
|
||||
msgid "Lists & Updates"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:778
|
||||
#: ../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:802
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:865
|
||||
msgid "Local Subnet Lists"
|
||||
msgstr ""
|
||||
|
||||
@ -841,7 +849,7 @@ msgstr ""
|
||||
msgid "Mixed proxy and DNS resolution tuning"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:916
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:979
|
||||
msgid "Mixed Proxy Port"
|
||||
msgstr ""
|
||||
|
||||
@ -849,7 +857,7 @@ msgstr ""
|
||||
msgid "Monitored Interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:343
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:405
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr ""
|
||||
|
||||
@ -873,7 +881,7 @@ msgstr ""
|
||||
msgid "Network"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481
|
||||
msgid "Network Interface"
|
||||
msgstr ""
|
||||
|
||||
@ -906,15 +914,15 @@ msgstr ""
|
||||
msgid "Not running"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:211
|
||||
#: ../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:199
|
||||
#: ../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:409
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472
|
||||
msgid "Only one section can be global at a time."
|
||||
msgstr ""
|
||||
|
||||
@ -922,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 ""
|
||||
|
||||
@ -963,7 +971,7 @@ msgstr ""
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:210
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:212
|
||||
msgid "Prefix length"
|
||||
msgstr ""
|
||||
|
||||
@ -971,7 +979,7 @@ msgstr ""
|
||||
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 ""
|
||||
|
||||
@ -983,19 +991,19 @@ msgstr ""
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547
|
||||
#: ../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:826
|
||||
#: ../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:850
|
||||
#: ../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:932
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:995
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -1003,7 +1011,7 @@ msgstr ""
|
||||
msgid "Restart NetShift"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:464
|
||||
msgid "Route all unmatched traffic through this section's outbound."
|
||||
msgstr ""
|
||||
|
||||
@ -1051,7 +1059,7 @@ msgstr ""
|
||||
msgid "Run Diagnostic"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:566
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629
|
||||
msgid "Russia inside restrictions"
|
||||
msgstr ""
|
||||
|
||||
@ -1063,7 +1071,7 @@ msgstr ""
|
||||
msgid "Sections"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577
|
||||
msgid "Select a predefined list for routing"
|
||||
msgstr ""
|
||||
|
||||
@ -1083,11 +1091,11 @@ msgstr ""
|
||||
msgid "Select how to configure the proxy"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:419
|
||||
#: ../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:491
|
||||
#: ../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 ""
|
||||
@ -1100,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:477
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540
|
||||
msgid "Select the DNS protocol type for the domain resolver"
|
||||
msgstr ""
|
||||
|
||||
@ -1124,10 +1132,18 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:250
|
||||
#: ../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 ""
|
||||
@ -1149,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 ""
|
||||
@ -1194,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:875
|
||||
#: ../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:827
|
||||
#: ../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:851
|
||||
#: ../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:779
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:803
|
||||
#: ../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 ""
|
||||
|
||||
@ -1220,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 ""
|
||||
|
||||
@ -1232,19 +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:194
|
||||
#: ../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:178
|
||||
#: ../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 ""
|
||||
|
||||
@ -1280,8 +1296,8 @@ msgstr ""
|
||||
msgid "Test latency"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:613
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701
|
||||
#: ../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 ""
|
||||
|
||||
@ -1289,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:303
|
||||
#: ../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:318
|
||||
#: ../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:351
|
||||
#: ../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 ""
|
||||
|
||||
@ -1329,27 +1345,27 @@ msgstr ""
|
||||
msgid "TTL value cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481
|
||||
#: ../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:389
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:452
|
||||
msgid "UDP over TCP"
|
||||
msgstr ""
|
||||
|
||||
#: src/netshift/tabs/manager/initController.ts:37
|
||||
#: src/netshift/tabs/manager/initController.ts:38
|
||||
#: src/netshift/tabs/manager/initController.ts:39
|
||||
#: src/netshift/tabs/manager/initController.ts:40
|
||||
#: src/netshift/tabs/manager/initController.ts:41
|
||||
#: src/netshift/tabs/manager/initController.ts:42
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:39
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:40
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:41
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:42
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:43
|
||||
#: src/netshift/tabs/diagnostic/initController.ts:44
|
||||
#: src/netshift/tabs/manager/initController.ts:37
|
||||
#: src/netshift/tabs/manager/initController.ts:38
|
||||
#: src/netshift/tabs/manager/initController.ts:39
|
||||
#: src/netshift/tabs/manager/initController.ts:40
|
||||
#: src/netshift/tabs/manager/initController.ts:41
|
||||
#: src/netshift/tabs/manager/initController.ts:42
|
||||
#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
@ -1396,23 +1412,31 @@ msgstr ""
|
||||
msgid "URLTest"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:302
|
||||
#: ../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:276
|
||||
#: ../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:350
|
||||
#: ../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:317
|
||||
#: ../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 ""
|
||||
|
||||
@ -1420,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:407
|
||||
#: ../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:621
|
||||
#: ../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:648
|
||||
#: ../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:709
|
||||
#: ../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:736
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:799
|
||||
msgid "User Subnets List"
|
||||
msgstr ""
|
||||
|
||||
@ -1462,8 +1486,8 @@ msgstr ""
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768
|
||||
#: ../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 ""
|
||||
|
||||
@ -1480,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:251
|
||||
#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:277
|
||||
#: ../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:549
|
||||
#: ../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:568
|
||||
#: ../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:403
|
||||
#: ../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 ""
|
||||
|
||||
@ -1502,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 ""
|
||||
|
||||
|
||||
@ -86,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"
|
||||
}
|
||||
|
||||
@ -839,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
|
||||
@ -1170,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"
|
||||
|
||||
@ -2227,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"
|
||||
|
||||
@ -2273,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"
|
||||
|
||||
@ -2282,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"
|
||||
@ -2331,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")"
|
||||
@ -2597,7 +2759,8 @@ configure_outbound_handler() {
|
||||
|
||||
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
|
||||
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_groups "$subscription_outbound_tags_json" "$group_mode" "$prefix_len")"
|
||||
if [ -z "$grouping_json" ]; then
|
||||
@ -2644,6 +2807,34 @@ configure_outbound_handler() {
|
||||
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=$(
|
||||
@ -2657,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
|
||||
|
||||
@ -92,6 +92,15 @@ UPDATES_LIBCRONET_LIB="/usr/lib/libcronet.so"
|
||||
# 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"
|
||||
# GitHub FRONTEND (github.com, NOT the rate-limited api.github.com) redirect path
|
||||
# for the NetShift repo. /releases/latest 302-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> 302s to the
|
||||
# CDN 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://github.com/yandexru45/netshift/releases/latest"
|
||||
NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE="https://github.com/yandexru45/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"
|
||||
@ -130,6 +139,12 @@ 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"
|
||||
|
||||
@ -1626,15 +1626,45 @@ updates_self_update_netshift() {
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Echoes the GitHub latest-release tag for NetShift (e.g. "0.8.8"), or nothing.
|
||||
# Reuses the same API endpoint as get_system_info / install.sh. Parsed with jq
|
||||
# (NOT grep/cut): GitHub may return the release object pretty-printed OR minified
|
||||
# (single line); a field-positional grep|cut grabs the first key's value (the
|
||||
# release "url") on minified JSON, which caused a false "outdated" + a self-update
|
||||
# that downloaded a garbage "version". jq is format-independent.
|
||||
updates_netshift_latest_tag() {
|
||||
local response tag
|
||||
# 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 github.com frontend redirect of /releases/latest — it
|
||||
# 302s 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
|
||||
# api.github.com release object parsed with jq (task-047) so a curl-less box or a
|
||||
# changed-redirect github 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: github.com/<repo>/releases/latest 302-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
|
||||
@ -1645,25 +1675,75 @@ updates_netshift_latest_tag() {
|
||||
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
|
||||
|
||||
@ -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, latesttag, 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:
|
||||
|
||||
@ -1272,6 +1272,201 @@ USEOF
|
||||
rm -f "$drv"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: Text-list Selector / URLTest (task-051)
|
||||
#
|
||||
# Drives the SHIPPED configure_outbound_handler (awk-extracted verbatim) for the
|
||||
# NEW selector_text / urltest_text branches with a table-driven config_get stub,
|
||||
# the REAL facade/manager/helpers, and a real `sing-box check`. The textarea
|
||||
# value is a multi-line blob: two synthetic vless:// + one ss:// + a blank line +
|
||||
# one unsupported tuic://, plus a CRLF-suffixed line to prove trailing-\r
|
||||
# tolerance. All values are synthetic placeholders (nothing private).
|
||||
#
|
||||
# IMPORTANT (gating): the driver writes name:OK/FAIL/SKIP tokens to a RESULT FILE
|
||||
# and the assertions are consumed in the CURRENT shell via `while read < file`
|
||||
# (NOT `cmd | while read`), so pass/fail mutate the real PASS/FAIL counters and
|
||||
# this test actually GATES the suite.
|
||||
test_text_list_outbound() {
|
||||
header "Text-list Selector / URLTest (task-051)"
|
||||
|
||||
if ! command -v sing-box > /dev/null 2>&1; then
|
||||
skip "sing-box not installed"
|
||||
return
|
||||
fi
|
||||
|
||||
local lib="${NETSHIFT_LIB_DIR}"
|
||||
local bin="${NETSHIFT_SRC}/usr/bin/netshift"
|
||||
local facade_lib="$lib/sing_box_config_facade.sh"
|
||||
if [ ! -r "$facade_lib" ] || [ ! -r "$bin" ]; then
|
||||
fail "facade lib / bin not found"
|
||||
return
|
||||
fi
|
||||
|
||||
# The facade hardcodes NETSHIFT_LIB="/usr/lib/netshift" for its own sourcing
|
||||
# of helpers + manager; bind the bind-mounted sources to that path.
|
||||
mkdir -p /usr/lib/netshift
|
||||
ln -sf "$lib/helpers.sh" /usr/lib/netshift/helpers.sh
|
||||
ln -sf "$lib/sing_box_config_manager.sh" /usr/lib/netshift/sing_box_config_manager.sh
|
||||
|
||||
local drv="/tmp/test-text-list-$$.sh"
|
||||
local out="/tmp/test-text-list-out-$$.txt"
|
||||
cat > "$drv" << 'TLEOF'
|
||||
. "CONST_LIB"
|
||||
. "FACADE_LIB"
|
||||
|
||||
WARN_LOG="/tmp/tl-warn-$$.log"
|
||||
: > "$WARN_LOG"
|
||||
log() { printf '%s|%s\n' "${2:-info}" "$1" >> "$WARN_LOG"; }
|
||||
echolog() { printf '%s|%s\n' "${2:-info}" "$1" >> "$WARN_LOG"; }
|
||||
nolog() { :; }
|
||||
|
||||
is_sing_box_extended() { return 0; }
|
||||
|
||||
# awk-extract the SHIPPED helper + handler + unavailable marker verbatim.
|
||||
eval "$(awk '/^_build_proxy_member_outbounds\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
eval "$(awk '/^configure_outbound_handler\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
eval "$(awk '/^mark_section_outbound_unavailable\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
|
||||
_tl_key() { printf 'TL_%s_%s' "$(printf '%s' "$1" | tr '.-' '__')" "$2"; }
|
||||
config_get() {
|
||||
local _k _v
|
||||
_k="$(_tl_key "$2" "$3")"
|
||||
eval "_v=\"\${$_k:-}\""
|
||||
[ -n "$_v" ] || _v="$4"
|
||||
eval "$1=\"\$_v\""
|
||||
return 0
|
||||
}
|
||||
|
||||
warn_logged() { grep -q "$1" "$WARN_LOG"; }
|
||||
|
||||
check_full() {
|
||||
local cfgjson="$1" label="$2" full
|
||||
full="/tmp/tl-full-$$-${label}.json"
|
||||
printf '%s' "$cfgjson" | jq '{
|
||||
log: { level: "error" },
|
||||
dns: { servers: [ { tag: "dns-server", type: "udp", server: "1.1.1.1" } ], final: "dns-server" },
|
||||
inbounds: [ { type: "tproxy", tag: "tproxy-in", listen: "127.0.0.1", listen_port: 1602 } ],
|
||||
outbounds: (.outbounds + [ { type: "direct", tag: "direct-out" } ]),
|
||||
route: { rules: [], final: "direct-out" }
|
||||
}' > "$full" 2>/dev/null
|
||||
if sing-box -c "$full" check > /dev/null 2>&1; then
|
||||
echo "${label}:OK"
|
||||
else
|
||||
echo "${label}:FAIL"
|
||||
fi
|
||||
rm -f "$full"
|
||||
}
|
||||
|
||||
# Multi-line synthetic blob: vless (line1) + vless (line2) + blank line +
|
||||
# ss+CRLF (line3 carries a trailing \r) + unsupported tuic (line4). The CRLF on
|
||||
# the ss line proves the trailing \r is stripped: it sits right after the
|
||||
# `:8388` port, so an un-stripped \r would corrupt the port and the member would
|
||||
# NOT build (a decisive gate, unlike a CR buried in a query string). Built with
|
||||
# printf so the \r and the blank line are real bytes inside one scalar value.
|
||||
TL_BLOB="$(printf '%s\n%s\n\n%s\r\n%s\n' \
|
||||
'vless://11111111-2222-3333-4444-555555555555@v1.example.com:443?security=tls&sni=v1.example.com' \
|
||||
'vless://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee@v2.example.com:443?security=tls&sni=v2.example.com' \
|
||||
'ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@s1.example.com:8388' \
|
||||
'tuic://uuid:pw@t1.example.com:443')"
|
||||
|
||||
# ── selector_text ────────────────────────────────────────────────────────────
|
||||
config='{"outbounds":[]}'
|
||||
SUBSCRIPTION_UNAVAILABLE_SECTIONS=""
|
||||
TL_seltxt_connection_type="proxy"
|
||||
TL_seltxt_proxy_config_type="selector_text"
|
||||
TL_seltxt_selector_proxy_links_text="$TL_BLOB"
|
||||
configure_outbound_handler "seltxt"
|
||||
seltxt_rc=$?
|
||||
[ "$seltxt_rc" = "0" ] && echo 'tl-seltxt-no-abort:OK' || echo "tl-seltxt-no-abort:FAIL (rc=$seltxt_rc)"
|
||||
|
||||
# 3 supported members built (seltxt-1 vless, seltxt-2 vless [CRLF line], seltxt-4 ss).
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="seltxt-1-out" and .type=="vless")] | length==1' >/dev/null 2>&1 \
|
||||
&& echo 'tl-seltxt-vless1-present:OK' || echo 'tl-seltxt-vless1-present:FAIL'
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="seltxt-2-out" and .type=="vless")] | length==1' >/dev/null 2>&1 \
|
||||
&& echo 'tl-seltxt-vless2-present:OK' || echo 'tl-seltxt-vless2-present:FAIL'
|
||||
# The ss line carries a trailing CR (CRLF); it must still build with the \r
|
||||
# stripped (decisive CRLF-tolerance gate).
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="seltxt-3-out" and .type=="shadowsocks")] | length==1' >/dev/null 2>&1 \
|
||||
&& echo 'tl-seltxt-ss-crlf-present:OK' || echo 'tl-seltxt-ss-crlf-present:FAIL'
|
||||
|
||||
# Unsupported tuic (line5 → seltxt-4; blank line is collapsed by IFS so it does
|
||||
# NOT consume an index) NOT created.
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="seltxt-4-out")] | length==0' >/dev/null 2>&1 \
|
||||
&& echo 'tl-seltxt-tuic-absent:OK' || echo 'tl-seltxt-tuic-absent:FAIL'
|
||||
|
||||
# Selector references exactly the 3 real members, default = first (seltxt-1-out).
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.type=="selector")][0].outbounds | (index("seltxt-1-out")!=null and index("seltxt-2-out")!=null and index("seltxt-3-out")!=null and index("seltxt-4-out")==null)' >/dev/null 2>&1 \
|
||||
&& echo 'tl-seltxt-members-clean:OK' || echo 'tl-seltxt-members-clean:FAIL'
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.type=="selector")][0].default=="seltxt-1-out"' >/dev/null 2>&1 \
|
||||
&& echo 'tl-seltxt-default-first:OK' || echo 'tl-seltxt-default-first:FAIL'
|
||||
|
||||
warn_logged "unsupported scheme" && echo 'tl-seltxt-warning-logged:OK' || echo 'tl-seltxt-warning-logged:FAIL'
|
||||
check_full "$config" "tl-seltxt-singbox-check"
|
||||
|
||||
# ── urltest_text ─────────────────────────────────────────────────────────────
|
||||
: > "$WARN_LOG"
|
||||
config='{"outbounds":[]}'
|
||||
SUBSCRIPTION_UNAVAILABLE_SECTIONS=""
|
||||
TL_urltxt_connection_type="proxy"
|
||||
TL_urltxt_proxy_config_type="urltest_text"
|
||||
TL_urltxt_urltest_proxy_links_text="$TL_BLOB"
|
||||
configure_outbound_handler "urltxt"
|
||||
urltxt_rc=$?
|
||||
[ "$urltxt_rc" = "0" ] && echo 'tl-urltxt-no-abort:OK' || echo "tl-urltxt-no-abort:FAIL (rc=$urltxt_rc)"
|
||||
|
||||
# urltest built over the 3 real members (no dangling unsupported tag).
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.type=="urltest")][0].outbounds | (index("urltxt-1-out")!=null and index("urltxt-2-out")!=null and index("urltxt-3-out")!=null and index("urltxt-4-out")==null)' >/dev/null 2>&1 \
|
||||
&& echo 'tl-urltxt-urltest-members-clean:OK' || echo 'tl-urltxt-urltest-members-clean:FAIL'
|
||||
|
||||
# selector built over [members + urltest tag].
|
||||
printf '%s' "$config" | jq -e '[.outbounds[] | select(.type=="selector")][0].outbounds as $o | ($o | index("urltxt-1-out")!=null) and ($o | index("urltxt-urltest-out")!=null)' >/dev/null 2>&1 \
|
||||
&& echo 'tl-urltxt-selector-over-urltest:OK' || echo 'tl-urltxt-selector-over-urltest:FAIL'
|
||||
|
||||
check_full "$config" "tl-urltxt-singbox-check"
|
||||
|
||||
# ── _check_outbound_section returns 0 for a non-empty text option ────────────
|
||||
# Pull in the requirements-check chain verbatim and stub config_foreach to drive
|
||||
# our single section through it.
|
||||
eval "$(awk '/^section_has_configured_outbound\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
eval "$(awk '/^_check_outbound_section\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
eval "$(awk '/^has_outbound_section\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
get_subscription_urls_for_section() { :; }
|
||||
config_foreach() { "$1" "chk_seltxt"; "$1" "chk_urltxt"; }
|
||||
|
||||
TL_chk_seltxt_connection_type="proxy"
|
||||
TL_chk_seltxt_proxy_config_type="selector_text"
|
||||
TL_chk_seltxt_selector_proxy_links_text="vless://11111111-2222-3333-4444-555555555555@v1.example.com:443"
|
||||
section_has_configured_outbound "chk_seltxt" \
|
||||
&& echo 'tl-check-seltxt-found:OK' || echo 'tl-check-seltxt-found:FAIL'
|
||||
|
||||
TL_chk_urltxt_connection_type="proxy"
|
||||
TL_chk_urltxt_proxy_config_type="urltest_text"
|
||||
TL_chk_urltxt_urltest_proxy_links_text="vless://11111111-2222-3333-4444-555555555555@v1.example.com:443"
|
||||
section_has_configured_outbound "chk_urltxt" \
|
||||
&& echo 'tl-check-urltxt-found:OK' || echo 'tl-check-urltxt-found:FAIL'
|
||||
|
||||
rm -f "$WARN_LOG"
|
||||
echo 'DONE'
|
||||
TLEOF
|
||||
sed -i "s|CONST_LIB|$lib/constants.sh|g; s|FACADE_LIB|$facade_lib|g; s|BIN_PATH|$bin|g" "$drv"
|
||||
|
||||
# Run the driver to a RESULT FILE, then consume tokens in the CURRENT shell
|
||||
# (while read < file — NO pipe) so pass/fail mutate the real counters/gate.
|
||||
sh "$drv" > "$out" 2>/dev/null
|
||||
local saw_done=0 line
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*:OK) pass "$line" ;;
|
||||
*:FAIL) fail "$line" ;;
|
||||
*:SKIP) skip "$line" ;;
|
||||
DONE) saw_done=1 ;;
|
||||
*) ;;
|
||||
esac
|
||||
done < "$out"
|
||||
[ "$saw_done" = "1" ] && pass "tl-driver-completed:OK" || fail "tl-driver-completed:FAIL (driver aborted early)"
|
||||
rm -f "$drv" "$out"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: Monitor procd-lock fd hygiene (task-035) + monitor-leak (task-036)
|
||||
#
|
||||
@ -3744,6 +3939,259 @@ CCEOF
|
||||
rm -f "$cc" "$cc_out"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: "Fastest" cross-group urltest of urltests (task-050)
|
||||
#
|
||||
# When subscription grouping is ON (country/prefix) and there are >= 2 groups,
|
||||
# the grouped branch in bin/netshift adds a top-level urltest tagged
|
||||
# $SB_SUBSCRIPTION_FASTEST_GROUP_TAG whose members are the per-group urltests
|
||||
# ("<key> Fastest"), PREPENDS it to the main selector, and makes it the selector
|
||||
# default. Groups + ungrouped stay selectable. groups==1 -> no nested layer
|
||||
# (default = lone group). off -> flat urltest+selector unchanged.
|
||||
#
|
||||
# The grouped branch is inline shell inside configure_outbound_handler (not its
|
||||
# own function), so we awk-extract that exact code region VERBATIM out of the
|
||||
# live bin (from the branch's `local grouping_json ...` decl through the final
|
||||
# grouped selector build) and wrap it in a driver function — the test exercises
|
||||
# the SHIPPED logic, not a copy. We seed $config with synthetic flag-tagged
|
||||
# shadowsocks outbounds (no real subscription data) so the generated config can
|
||||
# be fed to `sing-box check`. Tokens use the name:OK/FAIL convention; the driver
|
||||
# output is parsed in the CURRENT shell (no pipe) so the tokens GATE CI.
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
test_fastest_group() {
|
||||
header "Fastest Cross-Group urltest (task-050)"
|
||||
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
skip "jq not available"
|
||||
return
|
||||
fi
|
||||
|
||||
local bin="${NETSHIFT_SRC}/usr/bin/netshift"
|
||||
local constants="${NETSHIFT_LIB_DIR}/constants.sh"
|
||||
local manager="${NETSHIFT_LIB_DIR}/sing_box_config_manager.sh"
|
||||
if [ ! -r "$bin" ] || [ ! -r "$constants" ] || [ ! -r "$manager" ]; then
|
||||
skip "netshift bin / constants.sh / config_manager.sh not found"
|
||||
return
|
||||
fi
|
||||
|
||||
local work="/tmp/netshift-fastest-$$"
|
||||
mkdir -p "$work"
|
||||
local drv="$work/driver.sh"
|
||||
|
||||
cat > "$drv" << 'FGEOF'
|
||||
# Quiet logger (the grouped branch logs at info/debug/fatal; never let a fatal
|
||||
# log mask the real exit code — the branch calls `exit 1` itself on failure).
|
||||
log() { :; }
|
||||
echolog() { :; }
|
||||
nolog() { :; }
|
||||
|
||||
# Real constant ($SB_SUBSCRIPTION_FASTEST_GROUP_TAG) + cm primitives.
|
||||
. "CONSTANTS_PATH"
|
||||
. "MANAGER_PATH"
|
||||
|
||||
# Pull the shipped helpers VERBATIM out of the live bin so we test shipped code.
|
||||
eval "$(awk '/^sing_box_get_unique_outbound_tag\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
eval "$(awk '/^sing_box_build_subscription_groups\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")"
|
||||
|
||||
# Extract the WHOLE grouping if/else region VERBATIM (the grouped `then` branch
|
||||
# AND the flat `off` `else` branch) and wrap it as a function so we can drive
|
||||
# both modes against the SHIPPED code. The leading `if ...; then local ...`
|
||||
# line is valid inside this wrapper.
|
||||
_grouped_branch() {
|
||||
EXTRACT_GROUPED
|
||||
}
|
||||
|
||||
# The off branch derives its urltest tag via get_outbound_tag_by_section; the
|
||||
# grouped branch derives only $selector_tag (which we set ourselves). Stub it
|
||||
# deterministically (synthetic, no real data).
|
||||
get_outbound_tag_by_section() { printf '%s-out' "$1"; }
|
||||
|
||||
run_case() {
|
||||
# $1 = group_mode, $2 = prefix_len, $3 = tags-json, $4 = base config (with
|
||||
# the synthetic outbounds), $5 = selector tag. Echoes the resulting config.
|
||||
group_mode="$1"
|
||||
prefix_len="$2"
|
||||
subscription_outbound_tags_json="$3"
|
||||
config="$4"
|
||||
selector_tag="$5"
|
||||
section="syn"
|
||||
urltest_testing_url="https://www.gstatic.com/generate_204"
|
||||
urltest_check_interval="3m0s"
|
||||
urltest_tolerance="50"
|
||||
selector_outbounds=""
|
||||
selector_default=""
|
||||
_grouped_branch
|
||||
printf '%s' "$config"
|
||||
}
|
||||
FGEOF
|
||||
|
||||
# Build the synthetic outbound set: two country groups (RU/DE flags) with two
|
||||
# nodes each + one ungrouped node. Flags are regional-indicator pairs built
|
||||
# by codepoint so NO real subscription identifiers appear anywhere.
|
||||
local synth_json
|
||||
synth_json="$(jq -cn '
|
||||
def flag($a; $b): ([127462 + $a, 127462 + $b] | implode);
|
||||
(flag(17; 20)) as $ru # RU
|
||||
| (flag(3; 4)) as $de # DE
|
||||
| {
|
||||
outbounds: [
|
||||
{type:"shadowsocks", tag:($ru + " N1"), server:"10.0.0.1", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"shadowsocks", tag:($ru + " N2"), server:"10.0.0.2", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"shadowsocks", tag:($de + " N1"), server:"10.0.0.3", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"shadowsocks", tag:($de + " N2"), server:"10.0.0.4", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"shadowsocks", tag:"plain-node", server:"10.0.0.5", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"direct", tag:"direct-out"}
|
||||
]
|
||||
}')"
|
||||
local tags_json
|
||||
tags_json="$(printf '%s' "$synth_json" | jq -c '[.outbounds[] | select(.type=="shadowsocks") | .tag]')"
|
||||
|
||||
# Single-group set: only RU nodes (no DE, no ungrouped).
|
||||
local synth1_json synth1_tags
|
||||
synth1_json="$(jq -cn '
|
||||
def flag($a; $b): ([127462 + $a, 127462 + $b] | implode);
|
||||
(flag(17; 20)) as $ru
|
||||
| {
|
||||
outbounds: [
|
||||
{type:"shadowsocks", tag:($ru + " N1"), server:"10.0.1.1", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"shadowsocks", tag:($ru + " N2"), server:"10.0.1.2", server_port:443, method:"aes-256-gcm", password:"p"},
|
||||
{type:"direct", tag:"direct-out"}
|
||||
]
|
||||
}')"
|
||||
synth1_tags="$(printf '%s' "$synth1_json" | jq -c '[.outbounds[] | select(.type=="shadowsocks") | .tag]')"
|
||||
|
||||
# Substitute the awk-extracted grouped-branch region into the driver. The
|
||||
# region is plain shell statements; sed reads it from the live bin between
|
||||
# the unique markers and writes it where EXTRACT_GROUPED sits.
|
||||
local region="$work/region.sh"
|
||||
# Capture from the `if [ "$group_mode" != "off" ]; then` opener through the
|
||||
# off-branch's final selector build line and the immediately following `fi`
|
||||
# that closes the if/else (q-flag stops after that fi).
|
||||
awk '
|
||||
/if \[ "\$group_mode" != "off" \]; then/{p=1}
|
||||
p{print}
|
||||
p && /"\$urltest_tag" "true"\)"/{seen_else_end=1; next}
|
||||
seen_else_end && /^[[:space:]]*fi$/{exit}
|
||||
' "$bin" > "$region"
|
||||
# Confirm the region captured BOTH branches: the fastest prepend (grouped),
|
||||
# the cm urltest call, and the off-branch closing.
|
||||
if grep -q 'SB_SUBSCRIPTION_FASTEST_GROUP_TAG' "$region" \
|
||||
&& grep -q 'sing_box_cm_add_urltest_outbound' "$region" \
|
||||
&& grep -q 'Create urltest + selector' "$region"; then
|
||||
pass "fastest-region-extracted:OK"
|
||||
else
|
||||
fail "fastest-region-extracted:FAIL" "$(head -5 "$region" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# Splice region into the driver in place of the EXTRACT_GROUPED placeholder
|
||||
# (use an r-command via a temp because the region contains arbitrary chars).
|
||||
{
|
||||
sed '/EXTRACT_GROUPED/q' "$drv" | sed '$d'
|
||||
cat "$region"
|
||||
sed -n '/EXTRACT_GROUPED/,$p' "$drv" | sed '1d'
|
||||
} > "$drv.spliced"
|
||||
mv "$drv.spliced" "$drv"
|
||||
sed -i "s|CONSTANTS_PATH|$constants|g;s|MANAGER_PATH|$manager|g;s|BIN_PATH|$bin|g" "$drv"
|
||||
|
||||
# ── >= 2 groups (country mode): nested Fastest urltest + selector default ──
|
||||
local out2="$work/out2.json"
|
||||
{
|
||||
echo ". \"$drv\""
|
||||
echo "run_case country 2 '$tags_json' '$synth_json' 'syn-out'"
|
||||
} > "$work/run2.sh"
|
||||
ash "$work/run2.sh" > "$out2" 2>/dev/null || true
|
||||
|
||||
# The deduped fastest tag (the constant; no collision in our synthetic set).
|
||||
local fastest_expected ru_tag de_tag
|
||||
fastest_expected="$(. "$constants"; printf '%s' "$SB_SUBSCRIPTION_FASTEST_GROUP_TAG")"
|
||||
ru_tag="$(printf '%s' "$synth_json" | jq -r '.outbounds[0].tag' | sed 's/ N1$//') Fastest"
|
||||
de_tag="$(printf '%s' "$synth_json" | jq -r '.outbounds[2].tag' | sed 's/ N1$//') Fastest"
|
||||
|
||||
# (a) Top-level urltest tagged the fastest tag whose outbounds are EXACTLY
|
||||
# the per-group urltest tags.
|
||||
if jq -e --arg t "$fastest_expected" --arg g1 "$ru_tag" --arg g2 "$de_tag" '
|
||||
([.outbounds[] | select(.type=="urltest" and .tag==$t)]) as $f
|
||||
| ($f | length) == 1
|
||||
and ($f[0].outbounds == [$g1, $g2])
|
||||
' "$out2" > /dev/null 2>&1; then
|
||||
pass "fastest-nested-urltest-members:OK"
|
||||
else
|
||||
fail "fastest-nested-urltest-members:FAIL" "$(jq -c '[.outbounds[]|select(.type=="urltest")|{tag,outbounds}]' "$out2" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# (b) Main selector default == fastest tag, and outbounds ==
|
||||
# [fastest, group1, group2, ungrouped...].
|
||||
if jq -e --arg t "$fastest_expected" --arg g1 "$ru_tag" --arg g2 "$de_tag" '
|
||||
([.outbounds[] | select(.type=="selector" and .tag=="syn-out")]) as $s
|
||||
| ($s | length) == 1
|
||||
and ($s[0].default == $t)
|
||||
and ($s[0].outbounds == [$t, $g1, $g2, "plain-node"])
|
||||
' "$out2" > /dev/null 2>&1; then
|
||||
pass "fastest-selector-default-membership:OK"
|
||||
else
|
||||
fail "fastest-selector-default-membership:FAIL" "$(jq -c '.outbounds[]|select(.type=="selector")|{tag,default,outbounds}' "$out2" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# (c) sing-box check PASSES on the generated config WITH the nested urltest.
|
||||
if command -v sing-box > /dev/null 2>&1; then
|
||||
local chk2="$work/check2.json"
|
||||
# Wrap the outbounds into a minimal full config sing-box can validate.
|
||||
jq '{
|
||||
log: {disabled:true},
|
||||
dns: {servers: [], rules: [], final: "direct"},
|
||||
inbounds: [{type:"direct", tag:"dns-in", listen:"127.0.0.42", listen_port:53}],
|
||||
outbounds: .outbounds,
|
||||
route: {rules: [], rule_set: [], final: "direct-out", auto_detect_interface: true}
|
||||
}' "$out2" > "$chk2" 2>/dev/null
|
||||
if sing-box -c "$chk2" check > /dev/null 2>&1; then
|
||||
pass "fastest-singbox-check-passes:OK"
|
||||
else
|
||||
fail "fastest-singbox-check-passes:FAIL" "$(sing-box -c "$chk2" check 2>&1 | head -3)"
|
||||
fi
|
||||
else
|
||||
skip "fastest-singbox-check-passes (sing-box not installed)"
|
||||
fi
|
||||
|
||||
# (d) groups==1 -> NO redundant nested urltest; default = lone group.
|
||||
local out1="$work/out1.json"
|
||||
{
|
||||
echo ". \"$drv\""
|
||||
echo "run_case country 2 '$synth1_tags' '$synth1_json' 'syn1-out'"
|
||||
} > "$work/run1.sh"
|
||||
ash "$work/run1.sh" > "$out1" 2>/dev/null || true
|
||||
local lone_group
|
||||
lone_group="$(printf '%s' "$synth1_json" | jq -r '.outbounds[0].tag' | sed 's/ N1$//') Fastest"
|
||||
if jq -e --arg t "$fastest_expected" --arg lone "$lone_group" '
|
||||
([.outbounds[] | select(.type=="urltest" and .tag==$t)] | length) == 0
|
||||
and ([.outbounds[] | select(.type=="selector" and .tag=="syn1-out")][0].default == $lone)
|
||||
' "$out1" > /dev/null 2>&1; then
|
||||
pass "fastest-single-group-no-nest:OK"
|
||||
else
|
||||
fail "fastest-single-group-no-nest:FAIL" "$(jq -c '[.outbounds[]|select(.type=="urltest" or .type=="selector")|{type,tag,default}]' "$out1" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# (e) off mode unchanged (regression): flat urltest + selector, NO fastest
|
||||
# tag, selector default == the flat urltest tag (<section>-urltest-out).
|
||||
local outoff="$work/outoff.json"
|
||||
{
|
||||
echo ". \"$drv\""
|
||||
echo "run_case off 2 '$tags_json' '$synth_json' 'syn-out'"
|
||||
} > "$work/runoff.sh"
|
||||
ash "$work/runoff.sh" > "$outoff" 2>/dev/null || true
|
||||
if jq -e --arg t "$fastest_expected" '
|
||||
([.outbounds[] | select(.type=="urltest" and .tag==$t)] | length) == 0
|
||||
and ([.outbounds[] | select(.type=="urltest")] | length) == 1
|
||||
and ([.outbounds[] | select(.type=="selector" and .tag=="syn-out")][0].default
|
||||
== "syn-urltest-out")
|
||||
' "$outoff" > /dev/null 2>&1; then
|
||||
pass "fastest-off-mode-unchanged:OK"
|
||||
else
|
||||
fail "fastest-off-mode-unchanged:FAIL" "$(jq -c '[.outbounds[]|select(.type=="urltest" or .type=="selector")|{type,tag,default}]' "$outoff" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
rm -rf "$work"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: Insecure subscription fetch flag (task-021b)
|
||||
#
|
||||
@ -4836,6 +5284,187 @@ DDEOF
|
||||
rm -f "$drv"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: scalar `option subscription_url` read-fallback + option->list migration
|
||||
# (task-048)
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# REAL-UCI regression guard for the hardware bug: a section storing
|
||||
# subscription_url as a scalar UCI `option` (legacy / CLI / podkop-migrated
|
||||
# configs) made get_subscription_urls_for_section return EMPTY (config_list_foreach
|
||||
# iterates ONLY list values), so has_outbound_section failed and sing-box never
|
||||
# started. This must use the SHIPPED functions against an actual config_load — NOT
|
||||
# the stubbed config_list_foreach in test_subscription (which honors MU_URLS
|
||||
# directly and therefore cannot catch the broken primitive). Synthetic URL only.
|
||||
test_sub_url_option() {
|
||||
header "Scalar option subscription_url read-fallback + migration (task-048)"
|
||||
|
||||
local bin="${NETSHIFT_SRC}/usr/bin/netshift"
|
||||
if [ ! -r "$bin" ]; then
|
||||
skip "suburlopt — bin/netshift not found"
|
||||
return
|
||||
fi
|
||||
if [ ! -r /lib/functions.sh ] || [ ! -r /lib/config/uci.sh ] || ! command -v uci > /dev/null 2>&1; then
|
||||
skip "suburlopt — LuCI config_load / uci not available"
|
||||
return
|
||||
fi
|
||||
|
||||
local lib="${NETSHIFT_LIB_DIR}"
|
||||
local drv="/tmp/netshift-suburlopt-$$.sh"
|
||||
cat > "$drv" << 'SUBOPTEOF'
|
||||
BIN="BIN_PATH_PLACEHOLDER"
|
||||
LIB="LIB_DIR_PLACEHOLDER"
|
||||
. /lib/functions.sh
|
||||
. /lib/config/uci.sh 2>/dev/null || true
|
||||
# shellcheck disable=SC1090
|
||||
. "$LIB/constants.sh"
|
||||
# shellcheck disable=SC1090
|
||||
. "$LIB/helpers.sh"
|
||||
log() { :; }
|
||||
echolog() { :; }
|
||||
nolog() { :; }
|
||||
# Exercise the SHIPPED functions verbatim (awk-extracted) against a real
|
||||
# config_load — this is the whole point: the real LuCI config_list_foreach /
|
||||
# config_get primitives, not a stub.
|
||||
for fn in get_subscription_urls_for_section _collect_subscription_url_handler \
|
||||
section_has_configured_outbound \
|
||||
migrate_legacy_subscription_url_option \
|
||||
_migrate_legacy_subscription_url_option_handler; do
|
||||
eval "$(awk -v f="$fn" '$0 ~ "^"f"\\(\\) \\{"{p=1} p{print} p&&/^\}/{exit}' "$BIN")"
|
||||
done
|
||||
|
||||
mkdir -p /etc/config
|
||||
|
||||
# ── Fixture A: SCALAR option subscription_url (the exact broken shape) ──
|
||||
cat > /etc/config/netshift_suboptscalar <<'CFGEOF'
|
||||
config section 'main'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'subscription'
|
||||
option subscription_url 'https://example.com/sub'
|
||||
CFGEOF
|
||||
config_load netshift_suboptscalar
|
||||
urls="$(get_subscription_urls_for_section main)"
|
||||
[ "$urls" = "https://example.com/sub" ] && echo 'suburlopt:scalar-read:OK' || echo "suburlopt:scalar-read:FAIL [$urls]"
|
||||
if section_has_configured_outbound main; then
|
||||
echo 'suburlopt:scalar-hasoutbound:OK'
|
||||
else
|
||||
echo 'suburlopt:scalar-hasoutbound:FAIL'
|
||||
fi
|
||||
rm -f /etc/config/netshift_suboptscalar
|
||||
|
||||
# ── Fixture B: LIST subscription_url (must still work — no regression) ──
|
||||
cat > /etc/config/netshift_suboptlist <<'CFGEOF'
|
||||
config section 'main'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'subscription'
|
||||
list subscription_url 'https://example.com/sub'
|
||||
CFGEOF
|
||||
config_load netshift_suboptlist
|
||||
urls="$(get_subscription_urls_for_section main)"
|
||||
[ "$urls" = "https://example.com/sub" ] && echo 'suburlopt:list-read:OK' || echo "suburlopt:list-read:FAIL [$urls]"
|
||||
rm -f /etc/config/netshift_suboptlist
|
||||
|
||||
# ── Migration: option -> list, idempotent. The migration function hardcodes
|
||||
# the `netshift` config name, so write a throwaway /etc/config/netshift (the
|
||||
# caller backs up + restores any real one). Two sections: a plain URL AND a URL
|
||||
# with a query string containing `=`/`&`/`?` — the latter is the [B1] regression
|
||||
# guard: the old `uci add_list "key=value"` CLI form splits on the first `=` and
|
||||
# LOSES the value, while uci_add_list preserves it byte-for-byte. ──
|
||||
NETSHIFT_CONFIG="netshift"
|
||||
EQ_URL='https://example.com/sub?token=abc&x=1'
|
||||
cat > /etc/config/netshift <<CFGEOF
|
||||
config section 'main'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'subscription'
|
||||
option subscription_url 'https://example.com/sub'
|
||||
|
||||
config section 'query'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'subscription'
|
||||
option subscription_url '$EQ_URL'
|
||||
CFGEOF
|
||||
config_load netshift
|
||||
|
||||
# First run: must migrate both scalar options -> lists and flip the flag.
|
||||
migrate_legacy_subscription_url_option
|
||||
if [ "$SUBSCRIPTION_URL_OPTION_MIGRATED" = "1" ]; then
|
||||
echo 'suburlopt:migrate-flag:OK'
|
||||
else
|
||||
echo "suburlopt:migrate-flag:FAIL [$SUBSCRIPTION_URL_OPTION_MIGRATED]"
|
||||
fi
|
||||
# The stored values must be preserved.
|
||||
migrated_val="$(uci -q get netshift.main.subscription_url)"
|
||||
[ "$migrated_val" = "https://example.com/sub" ] && echo 'suburlopt:migrate-value:OK' || echo "suburlopt:migrate-value:FAIL [$migrated_val]"
|
||||
# [B1] regression guard: the `=`/`&` URL survives byte-for-byte.
|
||||
migrated_eq="$(uci -q get netshift.query.subscription_url)"
|
||||
[ "$migrated_eq" = "$EQ_URL" ] && echo 'suburlopt:migrate-equrl-preserved:OK' || echo "suburlopt:migrate-equrl-preserved:FAIL [$migrated_eq]"
|
||||
|
||||
# After a fresh config_load the LIST path (config_list_foreach) returns each URL,
|
||||
# and the committed state must be a CLEAN single-element list (no leftover scalar
|
||||
# option and no duplicate element).
|
||||
config_load netshift
|
||||
SUBSCRIPTION_URLS_COLLECTED=""
|
||||
config_list_foreach main subscription_url _collect_subscription_url_handler
|
||||
[ "$SUBSCRIPTION_URLS_COLLECTED" = "https://example.com/sub" ] && echo 'suburlopt:migrate-islist:OK' || echo "suburlopt:migrate-islist:FAIL [$SUBSCRIPTION_URLS_COLLECTED]"
|
||||
SUBSCRIPTION_URLS_COLLECTED=""
|
||||
config_list_foreach query subscription_url _collect_subscription_url_handler
|
||||
[ "$SUBSCRIPTION_URLS_COLLECTED" = "$EQ_URL" ] && echo 'suburlopt:migrate-equrl-islist:OK' || echo "suburlopt:migrate-equrl-islist:FAIL [$SUBSCRIPTION_URLS_COLLECTED]"
|
||||
# Clean single element: `uci show` must render exactly one list value per section
|
||||
# (no leftover scalar option, no duplicate). uci renders a list element with the
|
||||
# index-bearing `[0]` syntax; assert exactly one line each.
|
||||
eq_lines="$(uci -q show netshift.query.subscription_url | grep -c "subscription_url")"
|
||||
[ "$eq_lines" = "1" ] && echo 'suburlopt:migrate-equrl-single:OK' || echo "suburlopt:migrate-equrl-single:FAIL [$eq_lines]"
|
||||
|
||||
# Second run: idempotent no-op (already a list -> flag stays 0, no churn).
|
||||
migrate_legacy_subscription_url_option
|
||||
if [ "$SUBSCRIPTION_URL_OPTION_MIGRATED" = "0" ]; then
|
||||
echo 'suburlopt:migrate-idempotent:OK'
|
||||
else
|
||||
echo "suburlopt:migrate-idempotent:FAIL [$SUBSCRIPTION_URL_OPTION_MIGRATED]"
|
||||
fi
|
||||
idem_val="$(uci -q get netshift.main.subscription_url)"
|
||||
[ "$idem_val" = "https://example.com/sub" ] && echo 'suburlopt:migrate-idempotent-value:OK' || echo "suburlopt:migrate-idempotent-value:FAIL [$idem_val]"
|
||||
idem_eq="$(uci -q get netshift.query.subscription_url)"
|
||||
[ "$idem_eq" = "$EQ_URL" ] && echo 'suburlopt:migrate-idempotent-equrl:OK' || echo "suburlopt:migrate-idempotent-equrl:FAIL [$idem_eq]"
|
||||
|
||||
rm -f /etc/config/netshift
|
||||
echo 'DONE'
|
||||
SUBOPTEOF
|
||||
sed -i "s|LIB_DIR_PLACEHOLDER|$lib|g; s|BIN_PATH_PLACEHOLDER|$bin|g" "$drv"
|
||||
|
||||
# Protect any real /etc/config/netshift the container may carry: the
|
||||
# migration path writes a throwaway one under that exact name.
|
||||
local netshift_cfg_backup=""
|
||||
if [ -f /etc/config/netshift ]; then
|
||||
netshift_cfg_backup="/tmp/netshift-cfg-backup-$$"
|
||||
cp /etc/config/netshift "$netshift_cfg_backup"
|
||||
fi
|
||||
|
||||
# Parse in the CURRENT shell (temp file + `while read < "$out"`, NO pipe) so
|
||||
# pass/fail update the global counters and a suburlopt:*:FAIL actually gates
|
||||
# the suite (a pipe would run the while-body in a subshell — non-gating).
|
||||
local sub_out="/tmp/netshift-suburlopt-out-$$"
|
||||
sh "$drv" > "$sub_out" 2>/dev/null
|
||||
# FAIL/SKIP tokens carry a trailing " [diagnostic]" suffix, so match with a
|
||||
# trailing glob (*:FAIL*) — a bare "*:FAIL)" would miss them and silently
|
||||
# drop the failure, defeating S1's gating.
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*:FAIL*) fail "$line" ;;
|
||||
*:SKIP*) skip "$line" ;;
|
||||
*:OK) pass "$line" ;;
|
||||
DONE) ;;
|
||||
*) ;;
|
||||
esac
|
||||
done < "$sub_out"
|
||||
rm -f "$drv" "$sub_out"
|
||||
|
||||
if [ -n "$netshift_cfg_backup" ]; then
|
||||
mv "$netshift_cfg_backup" /etc/config/netshift
|
||||
else
|
||||
rm -f /etc/config/netshift
|
||||
fi
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: global_proxy route rule semantics
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
@ -5348,6 +5977,9 @@ updates_log() { :; }
|
||||
. "DRV_UPDATER"
|
||||
NETSHIFT_RELEASE_API_URL="https://api.test/latest"
|
||||
NETSHIFT_VERSION="$STUBLT_INSTALLED"
|
||||
# Force the API-fallback path this test targets: the redirect resolver returns
|
||||
# empty so updates_netshift_latest_tag falls back to the stubbed API body.
|
||||
updates_github_resolve_redirect() { printf ''; }
|
||||
updates_http_get_once() { printf '%s' "$STUBLT_BODY"; }
|
||||
"$STUBLT_FN"
|
||||
DRVEOF
|
||||
@ -5422,6 +6054,138 @@ DRVEOF
|
||||
rm -rf "$work"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: GitHub redirect-based latest-tag + deterministic asset URLs (task-049)
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Sidestepping the api.github.com 60/hour/IP rate limit: the version-check + the
|
||||
# self-update asset download now resolve github.com/<repo>/releases/latest via a
|
||||
# redirect (curl -w '%{redirect_url}') → /releases/tag/<tag>, with the API + jq
|
||||
# path kept only as a graceful fallback. The network boundary is STUBBED here
|
||||
# (override updates_github_resolve_redirect / updates_http_get_once), so no curl
|
||||
# shell-out and no real network in CI. Synthetic data only.
|
||||
test_github_redirect_tag() {
|
||||
header "GitHub redirect latest-tag + asset URLs (task-049)"
|
||||
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
skip "jq not available"
|
||||
return
|
||||
fi
|
||||
|
||||
local updater="${NETSHIFT_LIB_DIR}/updater.sh"
|
||||
if [ ! -r "$updater" ]; then
|
||||
skip "updater.sh not found in ${NETSHIFT_LIB_DIR}"
|
||||
return
|
||||
fi
|
||||
|
||||
local work="/tmp/netshift-ghredirect-$$"
|
||||
rm -rf "$work"
|
||||
mkdir -p "$work"
|
||||
|
||||
# Driver: source helpers.sh + updater.sh, silence logging, pin the redirect
|
||||
# + API constants, OVERRIDE the redirect resolver ($STUBGR_REDIRECT) and the
|
||||
# API boundary ($STUBGR_BODY), then run the function/expr named in $STUBGR_FN.
|
||||
local drv="$work/driver.sh"
|
||||
cat > "$drv" << 'DRVEOF'
|
||||
log() { :; }
|
||||
echolog() { :; }
|
||||
nolog() { :; }
|
||||
updates_log() { :; }
|
||||
. "DRV_HELPERS"
|
||||
. "DRV_UPDATER"
|
||||
NETSHIFT_REPO_RELEASES_LATEST_URL="https://github.com/yandexru45/netshift/releases/latest"
|
||||
NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE="https://github.com/yandexru45/netshift/releases/download"
|
||||
NETSHIFT_RELEASE_API_URL="https://api.test/latest"
|
||||
UPDATES_NETSHIFT_PKG_CORE="netshift"
|
||||
UPDATES_NETSHIFT_PKG_LUCI="luci-app-netshift"
|
||||
UPDATES_NETSHIFT_PKG_I18N_RU="luci-i18n-netshift-ru"
|
||||
updates_github_resolve_redirect() { printf '%s' "$STUBGR_REDIRECT"; }
|
||||
updates_http_get_once() { printf '%s' "$STUBGR_BODY"; }
|
||||
eval "$STUBGR_FN"
|
||||
DRVEOF
|
||||
sed -i "s|DRV_UPDATER|$updater|g;s|DRV_HELPERS|${NETSHIFT_LIB_DIR}/helpers.sh|g" "$drv"
|
||||
|
||||
local out="$work/out.txt"
|
||||
local rc_file="$work/rc.txt"
|
||||
run_gr() {
|
||||
ash "$drv" > "$out" 2>/dev/null && printf '0' > "$rc_file" || printf '%s' "$?" > "$rc_file"
|
||||
}
|
||||
|
||||
export STUBGR_FN="updates_netshift_latest_tag"
|
||||
export STUBGR_REDIRECT=""
|
||||
export STUBGR_BODY=""
|
||||
|
||||
# ── Case 1: clean redirect → tag 0.8.9 (primary path, no API) ────────────
|
||||
export STUBGR_REDIRECT="https://github.com/yandexru45/netshift/releases/tag/0.8.9"
|
||||
export STUBGR_BODY=""
|
||||
run_gr
|
||||
if [ "$(cat "$out" 2>/dev/null)" = "0.8.9" ] && [ "$(cat "$rc_file" 2>/dev/null)" = "0" ]; then
|
||||
pass "ghredirect:tag-from-redirect:OK"
|
||||
else
|
||||
fail "ghredirect:tag-from-redirect:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# ── Case 2: trailing-slash redirect → parse rejects (slash) → falls back ──
|
||||
# A trailing slash makes the stripped tag contain "/", which the guard
|
||||
# rejects; with NO API body it then yields empty + non-zero.
|
||||
export STUBGR_REDIRECT="https://github.com/yandexru45/netshift/releases/tag/0.8.9/"
|
||||
export STUBGR_BODY=""
|
||||
run_gr
|
||||
if [ -z "$(cat "$out" 2>/dev/null)" ] && [ "$(cat "$rc_file" 2>/dev/null)" != "0" ]; then
|
||||
pass "ghredirect:tag-trailing-slash-rejected:OK"
|
||||
else
|
||||
fail "ghredirect:tag-trailing-slash-rejected:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# ── Case 3: non-matching redirect (login page) → primary empty → API
|
||||
# FALLBACK returns the release object → still yields the tag. ─────────────
|
||||
export STUBGR_REDIRECT="https://github.com/login?return_to=%2Fyandexru45%2Fnetshift"
|
||||
export STUBGR_BODY='{"url":"https://api.github.com/repos/yandexru45/netshift/releases/1","tag_name":"0.8.9"}'
|
||||
run_gr
|
||||
if [ "$(cat "$out" 2>/dev/null)" = "0.8.9" ] && [ "$(cat "$rc_file" 2>/dev/null)" = "0" ]; then
|
||||
pass "ghredirect:nonmatch-falls-back:OK"
|
||||
else
|
||||
fail "ghredirect:nonmatch-falls-back:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# ── Case 4: curl-absent (resolver empty) + API rate-limit object → empty +
|
||||
# non-zero (honest failure, no false tag). ───────────────────────────────
|
||||
export STUBGR_REDIRECT=""
|
||||
export STUBGR_BODY='{"message":"API rate limit exceeded for 1.2.3.4"}'
|
||||
run_gr
|
||||
if [ -z "$(cat "$out" 2>/dev/null)" ] && [ "$(cat "$rc_file" 2>/dev/null)" != "0" ]; then
|
||||
pass "ghredirect:ratelimit-empty:OK"
|
||||
else
|
||||
fail "ghredirect:ratelimit-empty:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# ── Case 5: asset-URL builder, ipk → deterministic names ─────────────────
|
||||
export STUBGR_REDIRECT=""
|
||||
export STUBGR_BODY=""
|
||||
export STUBGR_FN='c="$(updates_netshift_asset_filename netshift 0.8.9 ipk)"; l="$(updates_netshift_asset_filename luci-app-netshift 0.8.9 ipk)"; i="$(updates_netshift_asset_filename luci-i18n-netshift-ru 0.8.9 ipk)"; printf "%s\n%s\n%s\n" "$c" "$l" "$i"'
|
||||
run_gr
|
||||
if [ "$(sed -n 1p "$out" 2>/dev/null)" = "netshift-0.8.9-r1-all.ipk" ] &&
|
||||
[ "$(sed -n 2p "$out" 2>/dev/null)" = "luci-app-netshift-0.8.9-r1-all.ipk" ] &&
|
||||
[ "$(sed -n 3p "$out" 2>/dev/null)" = "luci-i18n-netshift-ru-0.8.9.ipk" ]; then
|
||||
pass "ghredirect:asset-ipk:OK"
|
||||
else
|
||||
fail "ghredirect:asset-ipk:FAIL" "got=[$(cat "$out" 2>/dev/null)]"
|
||||
fi
|
||||
|
||||
# ── Case 6: asset-URL builder, apk → deterministic names ─────────────────
|
||||
export STUBGR_FN='c="$(updates_netshift_asset_filename netshift 0.8.9 apk)"; l="$(updates_netshift_asset_filename luci-app-netshift 0.8.9 apk)"; i="$(updates_netshift_asset_filename luci-i18n-netshift-ru 0.8.9 apk)"; printf "%s\n%s\n%s\n" "$c" "$l" "$i"'
|
||||
run_gr
|
||||
if [ "$(sed -n 1p "$out" 2>/dev/null)" = "netshift-0.8.9-r1.apk" ] &&
|
||||
[ "$(sed -n 2p "$out" 2>/dev/null)" = "luci-app-netshift-0.8.9-r1.apk" ] &&
|
||||
[ "$(sed -n 3p "$out" 2>/dev/null)" = "luci-i18n-netshift-ru-0.8.9.apk" ]; then
|
||||
pass "ghredirect:asset-apk:OK"
|
||||
else
|
||||
fail "ghredirect:asset-apk:FAIL" "got=[$(cat "$out" 2>/dev/null)]"
|
||||
fi
|
||||
|
||||
unset STUBGR_FN STUBGR_REDIRECT STUBGR_BODY
|
||||
rm -rf "$work"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Test: NetShift self-update (task-017)
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
@ -5957,18 +6721,22 @@ main() {
|
||||
test_section_isolation
|
||||
test_monitor_fd_hygiene
|
||||
test_unsupported_skip
|
||||
test_text_list_outbound
|
||||
test_diagnostics
|
||||
test_subscription
|
||||
test_fastest_group
|
||||
test_insecure_fetch
|
||||
test_rejected_hash
|
||||
test_jobstate
|
||||
test_selfheal
|
||||
test_dns_via_outbound
|
||||
test_sub_url_option
|
||||
test_global_proxy
|
||||
test_check_update_stable
|
||||
test_check_update_extended
|
||||
test_check_update_netshift
|
||||
test_netshift_latest_tag
|
||||
test_github_redirect_tag
|
||||
test_self_update_netshift
|
||||
test_backup_integrity
|
||||
;;
|
||||
@ -5982,18 +6750,22 @@ main() {
|
||||
isolation) test_section_isolation ;;
|
||||
monfd) test_monitor_fd_hygiene ;;
|
||||
unsupported) test_unsupported_skip ;;
|
||||
textlist) test_text_list_outbound ;;
|
||||
diagnostics) test_diagnostics ;;
|
||||
subscription) test_subscription ;;
|
||||
fastest) test_fastest_group ;;
|
||||
insecure) test_insecure_fetch ;;
|
||||
rejected) test_rejected_hash ;;
|
||||
jobstate) test_jobstate ;;
|
||||
selfheal) test_selfheal ;;
|
||||
dnsdetour) test_dns_via_outbound ;;
|
||||
suburlopt) test_sub_url_option ;;
|
||||
globalproxy) test_global_proxy ;;
|
||||
stablecheck) test_check_update_stable ;;
|
||||
extcheck) test_check_update_extended ;;
|
||||
netshiftcheck) test_check_update_netshift ;;
|
||||
latesttag) test_netshift_latest_tag ;;
|
||||
ghredirect) test_github_redirect_tag ;;
|
||||
selfupdate) test_self_update_netshift ;;
|
||||
backupguard) test_backup_integrity ;;
|
||||
jq) test_jq_helpers ;;
|
||||
@ -6001,7 +6773,7 @@ main() {
|
||||
sb) test_sing_box_config ;;
|
||||
*)
|
||||
echo "Unknown test: $target"
|
||||
echo "Available: 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 latesttag selfupdate backupguard"
|
||||
echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported textlist diagnostics subscription fastest insecure rejected jobstate selfheal dnsdetour suburlopt globalproxy stablecheck extcheck netshiftcheck latesttag ghredirect selfupdate backupguard"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user