mirror of
https://github.com/itdoginfo/podkop.git
synced 2026-07-21 23:03:26 +03:00
Compare commits
24 Commits
6089ef4c9b
...
0.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235368417b | ||
|
|
5e9d036eca | ||
|
|
a64923db5a | ||
|
|
0c99dddf11 | ||
|
|
922d6d4638 | ||
|
|
34f49c392e | ||
|
|
de7b73af3c | ||
|
|
3c34bd1fd0 | ||
|
|
847895598d | ||
|
|
4146804e5a | ||
|
|
daef80b7b8 | ||
|
|
6e7b8a6be2 | ||
|
|
1365febaa4 | ||
|
|
1543d974cc | ||
|
|
4b03c28c41 | ||
|
|
c3281773d5 | ||
|
|
c9e0b54f2c | ||
|
|
7f33cfcb98 | ||
|
|
177469e212 | ||
|
|
1802818044 | ||
|
|
c62cb2de14 | ||
|
|
f253e4e36f | ||
|
|
41e696c9b2 | ||
|
|
835b455915 |
29
.github/workflows/close-invalid-issues.yml
vendored
Normal file
29
.github/workflows/close-invalid-issues.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Close Invalid Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
close-invalid:
|
||||
if: github.event.label.name == 'Close as invalid'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Close issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: 'This issue has been marked as invalid and is being closed automatically.'
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
83
README.md
83
README.md
@@ -1,61 +1,54 @@
|
||||
# Вещи, которые вам нужно знать перед установкой
|
||||
# Podkop
|
||||
|
||||
Маршрутизация трафика для OpenWrt.
|
||||
|
||||
Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое программное обеспечение на базе [sing-box](https://github.com/SagerNet/sing-box).
|
||||
|
||||
> [!WARNING]
|
||||
> Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности.
|
||||
|
||||
# Вещи, которые необходимо знать перед установкой
|
||||
|
||||
### Обновления и конфигурация
|
||||
- При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
|
||||
- После обновления проверяйте конфигурацию — она может изменяться между версиями.
|
||||
- При старте Podkop модифицируется конфигурация Dnsmasq.
|
||||
- Podkop изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её.
|
||||
|
||||
### Системные требования
|
||||
- Требуется OpenWrt 24.10 или выше.
|
||||
- Необходимо минимум 25 МБ свободного места на устройстве. Устройства с флеш-памятью 16 МБ не поддерживаются.
|
||||
|
||||
### Важные ограничения и особенности
|
||||
- Если установлен Getdomains, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления)
|
||||
- Dashboard доступен только при подключении по HTTP (из-за особенностей Clash API). При использовании HTTPS или домена работа может быть недоступна.
|
||||
|
||||
### Поддержка и диагностика
|
||||
- [Руководство по диагностике](https://podkop.net/docs/diagnostics/)
|
||||
- Актуальные изменения публикуются в [Telegram-чате](https://t.me/itdogchat/81758/420321). Пожалуйста, ознакомьтесь с закрепленными сообщениями.
|
||||
- При возникновении проблем оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате.
|
||||
|
||||
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
|
||||
- При возникновении проблем, нужен технически грамотный фидбэк в чат. Ознакомьтесь с закрепом в топике.
|
||||
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
|
||||
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
|
||||
- Необходимо минимум 25МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
|
||||
- При старте программы редактируется конфиг Dnsmasq.
|
||||
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
|
||||
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](https://t.me/itdogchat/81758/420321).
|
||||
- [Если у вас что-то не работает.](https://podkop.net/docs/diagnostics/)
|
||||
- Если у вас установлен Getdomains, [его следует удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82-%D0%B4%D0%BB%D1%8F-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F).
|
||||
- Требуется версия OpenWrt 24.10.
|
||||
- Dashboard доступен, если вы заходите по http (из-за особенностей clash api). И не будет работать, если вы заходите по https и/или домену.
|
||||
|
||||
# Документация
|
||||
https://podkop.net/
|
||||
|
||||
# Установка Podkop
|
||||
Полная информация в [документации](https://podkop.net/docs/install/)
|
||||
Полное руководство доступно в [документации](https://podkop.net/docs/install/)
|
||||
|
||||
Вкратце, достаточно одного скрипта для установки и обновления:
|
||||
Для установки и обновления достаточно выполнить один скрипт:
|
||||
```
|
||||
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
|
||||
```
|
||||
|
||||
## Изменения 0.7.0
|
||||
Начиная с версии 0.7.0 изменена структура конфига `/etc/config/podkop`. Старые значения несовместимы с новыми. Нужно заново настроить Podkop.
|
||||
|
||||
Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже.
|
||||
|
||||
При обновлении вручную нужно:
|
||||
|
||||
0. Не ныть в issue и чатик.
|
||||
1. Забэкапить старый конфиг:
|
||||
```
|
||||
mv /etc/config/podkop /etc/config/podkop-070
|
||||
```
|
||||
2. Стянуть новый дефолтный конфиг:
|
||||
```
|
||||
wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop
|
||||
```
|
||||
3. Настроить заново ваш Podkop через Luci или UCI.
|
||||
|
||||
# ToDo
|
||||
## Будущее
|
||||
Планы развития проекта в настоящее время не публикуются в виде открытого roadmap. Обсуждение направлений и задач разработки ведётся авторами и контрибьюторами.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> PR принимаются только по согласованию с авторами в ТГ-чате. Остальные PR на данный момент не рассматриваются. Не тратьте зря своё время.
|
||||
> Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются.
|
||||
|
||||
## Будущее
|
||||
- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
|
||||
- [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне.
|
||||
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусственно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
|
||||
- [ ] Галочка, которая режет доступ к doh серверам.
|
||||
- [ ] IPv6. Только после наполнения Wiki.
|
||||
|
||||
## Тесты
|
||||
- [ ] Unit тесты (BATS)
|
||||
- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS)
|
||||
> [!WARNING]
|
||||
> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели.
|
||||
>
|
||||
> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий.
|
||||
|
||||
[](https://deepwiki.com/itdoginfo/podkop)
|
||||
63
TRADEMARK.md
Normal file
63
TRADEMARK.md
Normal file
@@ -0,0 +1,63 @@
|
||||
Trademark Guidelines
|
||||
|
||||
Version 1.0 dated May 28, 2026
|
||||
|
||||
This trademark policy was prepared to help you understand how to use the Podkop trademarks, service marks, and logos in connection with the Podkop open source project and related software.
|
||||
|
||||
While the Podkop software is available under an open source license, that license does not grant permission to use the Podkop trademarks, service marks, or logos. This policy explains acceptable use of the Podkop brand and related marks.
|
||||
|
||||
This Policy covers:
|
||||
1. Our word trademarks and service marks: Podkop
|
||||
2. Our logos, icons, and other Podkop brand assets
|
||||
|
||||
This policy encompasses all trademarks and service marks, whether they are registered or not.
|
||||
|
||||
## 1. General Guidelines
|
||||
|
||||
Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are distributing Podkop software when you are distributing a modified version of it, because recipients may not understand the differences between your modified versions and our own.
|
||||
|
||||
You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website.
|
||||
|
||||
You can, however, say that you like the Podkop project, that you participate in the Podkop community, or that you are providing an unmodified version of the Podkop software.
|
||||
|
||||
You may not use or register our marks, or variations of them as part of your own trademark, service mark, domain name, company name, trade name, product name or service name.
|
||||
|
||||
Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. For example, we would consider the following too similar to one of our Marks:
|
||||
|
||||
- MyPodkop
|
||||
- Open-Podkop
|
||||
- PodkopX
|
||||
- Podkop Lite
|
||||
- Podkop Pro
|
||||
|
||||
## 2. Acceptable Uses
|
||||
### Unmodified Code
|
||||
|
||||
When you redistribute an unmodified copy of Podkop software, you must not remove any Podkop trademarks, notices, or branding included in the original distribution.
|
||||
|
||||
### Modified Code
|
||||
|
||||
If you distribute a modified version of Podkop software, you may not use the Podkop name, trademarks, or logos in connection with your modified version, except to accurately describe the origin of the software in factual statements.
|
||||
|
||||
You must replace any Podkop branding, including names displayed in user interfaces, logs, documentation, and other user-facing elements, with your own distinct name and branding, so that your modified version is clearly distinguishable from the original Podkop software.
|
||||
|
||||
You must remove all Podkop logos and any other brand assets from the modified version.
|
||||
|
||||
You may not present your modified version as Podkop or as an official Podkop release, nor may you use the Podkop name in a way that suggests endorsement, affiliation, or official status.
|
||||
|
||||
You may only refer to Podkop in a factual and descriptive manner, for example: “This software is derived from Podkop open-source software.”
|
||||
|
||||
### Statements about Compatibility
|
||||
|
||||
You may use the word marks, exclusively for descriptive and factual purposes, to truthfully and accurately describe the relationship between your software and ours, including compatibility or origin. Any other use may imply that we have certified or approved your software.
|
||||
|
||||
### No Domain Names
|
||||
|
||||
You must not register any domain that includes our word marks or any variant or combination of them.
|
||||
|
||||
## 3. How to Display our Marks
|
||||
### Use of Trademarks in Text
|
||||
|
||||
Always use trademarks in their exact form with correct spelling. They must not be abbreviated, modified, hyphenated, or combined with other words in a way that creates a new product or service name.
|
||||
|
||||
Unacceptable: Podcop
|
||||
65
TRADEMARK_RU.md
Normal file
65
TRADEMARK_RU.md
Normal file
@@ -0,0 +1,65 @@
|
||||
Руководство по использованию товарных знаков
|
||||
|
||||
Версия 1.0 от 28 мая 2026 года
|
||||
|
||||
Настоящая политика в отношении товарных знаков подготовлена для того, чтобы помочь вам понять, как использовать товарные знаки, знаки обслуживания и логотипы Podkop в связи с открытым исходным кодом проекта Podkop и связанным программным обеспечением.
|
||||
|
||||
Хотя программное обеспечение Podkop распространяется под лицензией с открытым исходным кодом, эта лицензия не предоставляет разрешения на использование товарных знаков Podkop, знаков обслуживания или логотипов. Данная политика объясняет допустимое использование бренда Podkop и связанных обозначений.
|
||||
|
||||
Настоящая Политика охватывает:
|
||||
1. Наши словесные товарные знаки и знаки обслуживания: Podkop
|
||||
2. Наши логотипы, иконки и другие бренд-активы Podkop
|
||||
|
||||
Данная политика распространяется на все товарные знаки и знаки обслуживания, независимо от того, зарегистрированы они или нет.
|
||||
|
||||
## 1. Общие рекомендации
|
||||
|
||||
При использовании любого из наших знаков вы всегда должны делать это таким образом, чтобы никого не вводить в заблуждение относительно того, что именно они получают и от кого. Например, вы не можете утверждать, что распространяете программное обеспечение Podkop, если вы распространяете его модифицированную версию, поскольку получатели могут не понимать различий между вашей модифицированной версией и нашей оригинальной.
|
||||
|
||||
Вы также не можете использовать наш логотип на своём сайте таким образом, чтобы это создавало впечатление, что ваш сайт является официальным сайтом или что мы одобряем ваш сайт.
|
||||
|
||||
Однако вы можете указывать, что вам нравится проект Podkop, что вы участвуете в сообществе Podkop или что вы распространяете немодифицированную версию программного обеспечения Podkop.
|
||||
|
||||
Вы не имеете права использовать или регистрировать наши знаки, а также их вариации, как часть вашего собственного товарного знака, знака обслуживания, доменного имени, названия компании, коммерческого наименования, названия продукта или услуги.
|
||||
|
||||
Закон о товарных знаках не допускает использование названий или знаков, которые слишком похожи на наши. Поэтому вы не можете использовать очевидные вариации наших знаков или любые фонетически, иностранно-языковые эквиваленты, производные, аббревиатуры для похожего или совместимого продукта или услуги. Например, мы считаем слишком похожими на наши знаки следующие варианты:
|
||||
|
||||
- MyPodkop
|
||||
- Open-Podkop
|
||||
- PodkopX
|
||||
- Podkop Lite
|
||||
- Podkop Pro
|
||||
|
||||
## 2. Допустимое использование
|
||||
|
||||
### Немодифицированный код
|
||||
|
||||
При распространении немодифицированной копии программного обеспечения Podkop вы не должны удалять товарные знаки, уведомления или брендинг Podkop, включённые в исходное распространение.
|
||||
|
||||
### Модифицированный код
|
||||
|
||||
Если вы распространяете модифицированную версию программного обеспечения Podkop, вы не можете использовать название Podkop, товарные знаки или логотипы в связи с вашей модифицированной версией, за исключением точного описания происхождения программного обеспечения в фактических утверждениях.
|
||||
|
||||
Вы обязаны заменить все элементы брендинга Podkop, включая названия, отображаемые в пользовательском интерфейсе, логах, документации и других пользовательских элементах, на собственное отличительное название и брендинг, чтобы ваша модифицированная версия была явно отличима от оригинального программного обеспечения Podkop.
|
||||
|
||||
Вы должны удалить все логотипы Podkop и любые другие бренд-материалы из модифицированной версии.
|
||||
|
||||
Вы не можете представлять вашу модифицированную версию как Podkop или как официальную версию Podkop, а также использовать название Podkop таким образом, чтобы это подразумевало одобрение, аффилированность или официальный статус.
|
||||
|
||||
Вы можете ссылаться на Podkop только в фактическом и описательном контексте, например: «Это программное обеспечение основано на программном обеспечении Podkop с открытым исходным кодом».
|
||||
|
||||
### Упоминания о совместимости
|
||||
|
||||
Вы можете использовать словесные знаки исключительно в описательных и фактических целях для правдивого и точного описания связи вашего программного обеспечения с нашим, включая совместимость или происхождение. Любое другое использование может создавать впечатление, что мы сертифицировали или одобрили ваше программное обеспечение.
|
||||
|
||||
### Запрет на доменные имена
|
||||
|
||||
Вы не должны регистрировать доменные имена, содержащие наши словесные знаки или любые их варианты или комбинации.
|
||||
|
||||
## 3. Как отображать наши знаки
|
||||
|
||||
### Использование товарных знаков в тексте
|
||||
|
||||
Всегда используйте товарные знаки в их точной форме с корректным написанием. Их нельзя сокращать, изменять, соединять дефисами или объединять с другими словами таким образом, чтобы это создавало новое название продукта или услуги.
|
||||
|
||||
Недопустимо: Podcop
|
||||
@@ -648,56 +648,56 @@
|
||||
"call": "Invalid HY2 URL: insecure must be 0 or 1",
|
||||
"key": "Invalid HY2 URL: insecure must be 0 or 1",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:76"
|
||||
"src/validators/validateHysteriaUrl.ts:90"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: invalid port number",
|
||||
"key": "Invalid HY2 URL: invalid port number",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:62"
|
||||
"src/validators/validateHysteriaUrl.ts:77"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: missing credentials/server",
|
||||
"key": "Invalid HY2 URL: missing credentials/server",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:32"
|
||||
"src/validators/validateHysteriaUrl.ts:30"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: missing host",
|
||||
"key": "Invalid HY2 URL: missing host",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:49"
|
||||
"src/validators/validateHysteriaUrl.ts:47"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: missing host & port",
|
||||
"key": "Invalid HY2 URL: missing host & port",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:43"
|
||||
"src/validators/validateHysteriaUrl.ts:41"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: missing password",
|
||||
"key": "Invalid HY2 URL: missing password",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:38"
|
||||
"src/validators/validateHysteriaUrl.ts:36"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: missing port",
|
||||
"key": "Invalid HY2 URL: missing port",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:53"
|
||||
"src/validators/validateHysteriaUrl.ts:50"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: must not contain spaces",
|
||||
"key": "Invalid HY2 URL: must not contain spaces",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:19"
|
||||
"src/validators/validateHysteriaUrl.ts:18"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -711,28 +711,28 @@
|
||||
"call": "Invalid HY2 URL: obfs-password required when obfs is set",
|
||||
"key": "Invalid HY2 URL: obfs-password required when obfs is set",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:99"
|
||||
"src/validators/validateHysteriaUrl.ts:108"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: parsing failed",
|
||||
"key": "Invalid HY2 URL: parsing failed",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:115"
|
||||
"src/validators/validateHysteriaUrl.ts:122"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: sni cannot be empty",
|
||||
"key": "Invalid HY2 URL: sni cannot be empty",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:108"
|
||||
"src/validators/validateHysteriaUrl.ts:116"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Invalid HY2 URL: unsupported obfs type",
|
||||
"key": "Invalid HY2 URL: unsupported obfs type",
|
||||
"places": [
|
||||
"src/validators/validateHysteriaUrl.ts:88"
|
||||
"src/validators/validateHysteriaUrl.ts:98"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -943,7 +943,7 @@
|
||||
"call": "Latest",
|
||||
"key": "Latest",
|
||||
"places": [
|
||||
"src/podkop/tabs/diagnostic/initController.ts:456"
|
||||
"src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:48"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1075,7 +1075,7 @@
|
||||
"call": "Outdated",
|
||||
"key": "Outdated",
|
||||
"places": [
|
||||
"src/podkop/tabs/diagnostic/initController.ts:446"
|
||||
"src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:38"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1685,7 +1685,7 @@
|
||||
"src/podkop/tabs/diagnostic/initController.ts:41",
|
||||
"src/podkop/tabs/diagnostic/initController.ts:42",
|
||||
"src/podkop/tabs/diagnostic/initController.ts:43",
|
||||
"src/podkop/tabs/diagnostic/initController.ts:420"
|
||||
"src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:7"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1802,7 +1802,7 @@
|
||||
"src/validators/validateDns.ts:18",
|
||||
"src/validators/validateDomain.ts:13",
|
||||
"src/validators/validateDomain.ts:30",
|
||||
"src/validators/validateHysteriaUrl.ts:113",
|
||||
"src/validators/validateHysteriaUrl.ts:120",
|
||||
"src/validators/validateIp.ts:8",
|
||||
"src/validators/validateOutboundJson.ts:7",
|
||||
"src/validators/validatePath.ts:16",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PODKOP package.
|
||||
# romanvht <romanvht@gmail.com>, 2026.
|
||||
# divocatt <210179590+divocatt@users.noreply.github.com>, 2026.
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-09 02:49+1000\n"
|
||||
"PO-Revision-Date: 2026-05-09 02:49+1000\n"
|
||||
"Last-Translator: romanvht <romanvht@gmail.com>\n"
|
||||
"POT-Creation-Date: 2026-05-29 13:40+0300\n"
|
||||
"PO-Revision-Date: 2026-05-29 13:40+0300\n"
|
||||
"Last-Translator: divocatt <210179590+divocatt@users.noreply.github.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -394,35 +394,35 @@ msgstr ""
|
||||
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:76
|
||||
#: src/validators/validateHysteriaUrl.ts:90
|
||||
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:62
|
||||
#: src/validators/validateHysteriaUrl.ts:77
|
||||
msgid "Invalid HY2 URL: invalid port number"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:32
|
||||
#: src/validators/validateHysteriaUrl.ts:30
|
||||
msgid "Invalid HY2 URL: missing credentials/server"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:49
|
||||
#: src/validators/validateHysteriaUrl.ts:47
|
||||
msgid "Invalid HY2 URL: missing host"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:43
|
||||
#: src/validators/validateHysteriaUrl.ts:41
|
||||
msgid "Invalid HY2 URL: missing host & port"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:38
|
||||
#: src/validators/validateHysteriaUrl.ts:36
|
||||
msgid "Invalid HY2 URL: missing password"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:53
|
||||
#: src/validators/validateHysteriaUrl.ts:50
|
||||
msgid "Invalid HY2 URL: missing port"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:19
|
||||
#: src/validators/validateHysteriaUrl.ts:18
|
||||
msgid "Invalid HY2 URL: must not contain spaces"
|
||||
msgstr ""
|
||||
|
||||
@@ -430,19 +430,19 @@ msgstr ""
|
||||
msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:99
|
||||
#: src/validators/validateHysteriaUrl.ts:108
|
||||
msgid "Invalid HY2 URL: obfs-password required when obfs is set"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:115
|
||||
#: src/validators/validateHysteriaUrl.ts:122
|
||||
msgid "Invalid HY2 URL: parsing failed"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:108
|
||||
#: src/validators/validateHysteriaUrl.ts:116
|
||||
msgid "Invalid HY2 URL: sni cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:88
|
||||
#: src/validators/validateHysteriaUrl.ts:98
|
||||
msgid "Invalid HY2 URL: unsupported obfs type"
|
||||
msgstr ""
|
||||
|
||||
@@ -563,7 +563,7 @@ msgstr ""
|
||||
msgid "Issues detected"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:456
|
||||
#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:48
|
||||
msgid "Latest"
|
||||
msgstr ""
|
||||
|
||||
@@ -641,7 +641,7 @@ msgstr ""
|
||||
msgid "Outbound Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:446
|
||||
#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:38
|
||||
msgid "Outdated"
|
||||
msgstr ""
|
||||
|
||||
@@ -996,7 +996,7 @@ msgstr ""
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:41
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:42
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:43
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:420
|
||||
#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:7
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
|
||||
@@ -1065,7 +1065,7 @@ msgstr ""
|
||||
#: src/validators/validateDns.ts:18
|
||||
#: src/validators/validateDomain.ts:13
|
||||
#: src/validators/validateDomain.ts:30
|
||||
#: src/validators/validateHysteriaUrl.ts:113
|
||||
#: src/validators/validateHysteriaUrl.ts:120
|
||||
#: src/validators/validateIp.ts:8
|
||||
#: src/validators/validateOutboundJson.ts:7
|
||||
#: src/validators/validatePath.ts:16
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# RU translations for PODKOP package.
|
||||
# Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PODKOP package.
|
||||
# romanvht, 2026.
|
||||
# divocatt, 2026.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-09 12:50+1000\n"
|
||||
"PO-Revision-Date: 2026-05-09 12:50+1000\n"
|
||||
"Last-Translator: romanvht\n"
|
||||
"POT-Creation-Date: 2026-05-29 16:40+0300\n"
|
||||
"PO-Revision-Date: 2026-05-29 16:40+0300\n"
|
||||
"Last-Translator: divocatt\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -29,7 +29,9 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
||||
const data = configSections
|
||||
.filter(
|
||||
(section) =>
|
||||
section.connection_type !== 'block' && section['.type'] !== 'settings',
|
||||
section.connection_type !== 'block' &&
|
||||
section.connection_type !== 'exclusion' &&
|
||||
section['.type'] !== 'settings',
|
||||
)
|
||||
.map((section) => {
|
||||
if (section.connection_type === 'proxy') {
|
||||
|
||||
@@ -122,13 +122,18 @@ export namespace Podkop {
|
||||
connection_type: 'block';
|
||||
}
|
||||
|
||||
export interface ConfigExclusionSection {
|
||||
connection_type: 'exclusion';
|
||||
}
|
||||
|
||||
export type ConfigBaseSection =
|
||||
| ConfigProxyUrlTestSection
|
||||
| ConfigProxySelectorSection
|
||||
| ConfigProxyUrlSection
|
||||
| ConfigProxyOutboundSection
|
||||
| ConfigVpnSection
|
||||
| ConfigBlockSection;
|
||||
| ConfigBlockSection
|
||||
| ConfigExclusionSection;
|
||||
|
||||
export type ConfigSection = ConfigBaseSection & {
|
||||
'.name': string;
|
||||
|
||||
@@ -28,6 +28,11 @@ const validUrls = [
|
||||
|
||||
// Explicit obfs=none (valid)
|
||||
['obfs none = ok', 'hysteria2://pw@example.com:443/?obfs=none#hy2-none'],
|
||||
|
||||
[
|
||||
'port range 5000-6000',
|
||||
'hysteria2://letmein@example.com:123,5000-6000/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com',
|
||||
],
|
||||
];
|
||||
|
||||
const invalidUrls = [
|
||||
|
||||
@@ -6,19 +6,17 @@ export function validateHysteria2Url(url: string): ValidationResult {
|
||||
const isHY2 = url.startsWith('hysteria2://');
|
||||
const isHY2Short = url.startsWith('hy2://');
|
||||
|
||||
if (!isHY2 && !isHY2Short) {
|
||||
if (!isHY2 && !isHY2Short)
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: must start with hysteria2:// or hy2://'),
|
||||
};
|
||||
}
|
||||
|
||||
if (/\s/.test(url)) {
|
||||
if (/\s/.test(url))
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: must not contain spaces'),
|
||||
};
|
||||
}
|
||||
|
||||
const prefix = isHY2 ? 'hysteria2://' : 'hy2://';
|
||||
const body = url.slice(prefix.length);
|
||||
@@ -45,23 +43,39 @@ export function validateHysteria2Url(url: string): ValidationResult {
|
||||
|
||||
const [host, port] = hostPortPart.split(':');
|
||||
|
||||
if (!host) {
|
||||
if (!host)
|
||||
return { valid: false, message: _('Invalid HY2 URL: missing host') };
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
if (!port)
|
||||
return { valid: false, message: _('Invalid HY2 URL: missing port') };
|
||||
}
|
||||
|
||||
const cleanedPort = port.replace('/', '');
|
||||
const portNum = Number(cleanedPort);
|
||||
const portEntries = cleanedPort.split(',');
|
||||
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
const isValidPortNumber = (value: string) => {
|
||||
if (!/^\d+$/.test(value)) return false;
|
||||
const num = Number(value);
|
||||
return num >= 1 && num <= 65535;
|
||||
};
|
||||
|
||||
const isValidPortEntry = (entry: string) => {
|
||||
if (!entry) return false;
|
||||
if (!entry.includes('-')) return isValidPortNumber(entry);
|
||||
|
||||
const rangeParts = entry.split('-');
|
||||
if (rangeParts.length !== 2) return false;
|
||||
|
||||
const [start, end] = rangeParts;
|
||||
if (!isValidPortNumber(start) || !isValidPortNumber(end)) return false;
|
||||
|
||||
return Number(start) <= Number(end);
|
||||
};
|
||||
|
||||
if (!portEntries.every(isValidPortEntry))
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: invalid port number'),
|
||||
};
|
||||
}
|
||||
|
||||
if (queryString) {
|
||||
const params = parseQueryString(queryString);
|
||||
@@ -70,44 +84,37 @@ export function validateHysteria2Url(url: string): ValidationResult {
|
||||
if (
|
||||
paramsKeys.includes('insecure') &&
|
||||
!['0', '1'].includes(params.insecure)
|
||||
) {
|
||||
)
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: insecure must be 0 or 1'),
|
||||
};
|
||||
}
|
||||
|
||||
const validObfsTypes = ['none', 'salamander'];
|
||||
|
||||
if (
|
||||
paramsKeys.includes('obfs') &&
|
||||
!validObfsTypes.includes(params.obfs)
|
||||
) {
|
||||
if (paramsKeys.includes('obfs') && !validObfsTypes.includes(params.obfs))
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: unsupported obfs type'),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
paramsKeys.includes('obfs') &&
|
||||
params.obfs !== 'none' &&
|
||||
!params['obfs-password']
|
||||
) {
|
||||
)
|
||||
return {
|
||||
valid: false,
|
||||
message: _(
|
||||
'Invalid HY2 URL: obfs-password required when obfs is set',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (paramsKeys.includes('sni') && !params.sni) {
|
||||
if (paramsKeys.includes('sni') && !params.sni)
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: sni cannot be empty'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, message: _('Valid') };
|
||||
|
||||
@@ -453,18 +453,16 @@ function validateHysteria2Url(url) {
|
||||
try {
|
||||
const isHY2 = url.startsWith("hysteria2://");
|
||||
const isHY2Short = url.startsWith("hy2://");
|
||||
if (!isHY2 && !isHY2Short) {
|
||||
if (!isHY2 && !isHY2Short)
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HY2 URL: must start with hysteria2:// or hy2://")
|
||||
};
|
||||
}
|
||||
if (/\s/.test(url)) {
|
||||
if (/\s/.test(url))
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HY2 URL: must not contain spaces")
|
||||
};
|
||||
}
|
||||
const prefix = isHY2 ? "hysteria2://" : "hy2://";
|
||||
const body = url.slice(prefix.length);
|
||||
const [mainPart] = body.split("#");
|
||||
@@ -483,50 +481,57 @@ function validateHysteria2Url(url) {
|
||||
message: _("Invalid HY2 URL: missing host & port")
|
||||
};
|
||||
const [host, port] = hostPortPart.split(":");
|
||||
if (!host) {
|
||||
if (!host)
|
||||
return { valid: false, message: _("Invalid HY2 URL: missing host") };
|
||||
}
|
||||
if (!port) {
|
||||
if (!port)
|
||||
return { valid: false, message: _("Invalid HY2 URL: missing port") };
|
||||
}
|
||||
const cleanedPort = port.replace("/", "");
|
||||
const portNum = Number(cleanedPort);
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
const portEntries = cleanedPort.split(",");
|
||||
const isValidPortNumber = (value) => {
|
||||
if (!/^\d+$/.test(value)) return false;
|
||||
const num = Number(value);
|
||||
return num >= 1 && num <= 65535;
|
||||
};
|
||||
const isValidPortEntry = (entry) => {
|
||||
if (!entry) return false;
|
||||
if (!entry.includes("-")) return isValidPortNumber(entry);
|
||||
const rangeParts = entry.split("-");
|
||||
if (rangeParts.length !== 2) return false;
|
||||
const [start, end] = rangeParts;
|
||||
if (!isValidPortNumber(start) || !isValidPortNumber(end)) return false;
|
||||
return Number(start) <= Number(end);
|
||||
};
|
||||
if (!portEntries.every(isValidPortEntry))
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HY2 URL: invalid port number")
|
||||
};
|
||||
}
|
||||
if (queryString) {
|
||||
const params = parseQueryString(queryString);
|
||||
const paramsKeys = Object.keys(params);
|
||||
if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure)) {
|
||||
if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure))
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HY2 URL: insecure must be 0 or 1")
|
||||
};
|
||||
}
|
||||
const validObfsTypes = ["none", "salamander"];
|
||||
if (paramsKeys.includes("obfs") && !validObfsTypes.includes(params.obfs)) {
|
||||
if (paramsKeys.includes("obfs") && !validObfsTypes.includes(params.obfs))
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HY2 URL: unsupported obfs type")
|
||||
};
|
||||
}
|
||||
if (paramsKeys.includes("obfs") && params.obfs !== "none" && !params["obfs-password"]) {
|
||||
if (paramsKeys.includes("obfs") && params.obfs !== "none" && !params["obfs-password"])
|
||||
return {
|
||||
valid: false,
|
||||
message: _(
|
||||
"Invalid HY2 URL: obfs-password required when obfs is set"
|
||||
)
|
||||
};
|
||||
}
|
||||
if (paramsKeys.includes("sni") && !params.sni) {
|
||||
if (paramsKeys.includes("sni") && !params.sni)
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Invalid HY2 URL: sni cannot be empty")
|
||||
};
|
||||
}
|
||||
}
|
||||
return { valid: true, message: _("Valid") };
|
||||
} catch (_e) {
|
||||
@@ -711,7 +716,7 @@ async function getDashboardSections() {
|
||||
})
|
||||
);
|
||||
const data = configSections.filter(
|
||||
(section) => section.connection_type !== "block" && section[".type"] !== "settings"
|
||||
(section) => section.connection_type !== "block" && section.connection_type !== "exclusion" && section[".type"] !== "settings"
|
||||
).map((section) => {
|
||||
if (section.connection_type === "proxy") {
|
||||
if (section.proxy_config_type === "url") {
|
||||
|
||||
@@ -311,7 +311,7 @@ function createSettingsContent(section) {
|
||||
|
||||
for (const secName in sections) {
|
||||
const sec = sections[secName];
|
||||
if (sec[".type"] === "section") {
|
||||
if (sec[".type"] === "section" && sec['connection_type'] !== 'block' && sec['connection_type'] !== 'exclusion') {
|
||||
this.keylist.push(secName);
|
||||
this.vallist.push(secName);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# RU translations for PODKOP package.
|
||||
# Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PODKOP package.
|
||||
# romanvht, 2026.
|
||||
# divocatt, 2026.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-09 12:50+1000\n"
|
||||
"PO-Revision-Date: 2026-05-09 12:50+1000\n"
|
||||
"Last-Translator: romanvht\n"
|
||||
"POT-Creation-Date: 2026-05-29 16:40+0300\n"
|
||||
"PO-Revision-Date: 2026-05-29 16:40+0300\n"
|
||||
"Last-Translator: divocatt\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PODKOP package.
|
||||
# romanvht <romanvht@gmail.com>, 2026.
|
||||
# divocatt <210179590+divocatt@users.noreply.github.com>, 2026.
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-09 02:49+1000\n"
|
||||
"PO-Revision-Date: 2026-05-09 02:49+1000\n"
|
||||
"Last-Translator: romanvht <romanvht@gmail.com>\n"
|
||||
"POT-Creation-Date: 2026-05-29 13:40+0300\n"
|
||||
"PO-Revision-Date: 2026-05-29 13:40+0300\n"
|
||||
"Last-Translator: divocatt <210179590+divocatt@users.noreply.github.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -394,35 +394,35 @@ msgstr ""
|
||||
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:76
|
||||
#: src/validators/validateHysteriaUrl.ts:90
|
||||
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:62
|
||||
#: src/validators/validateHysteriaUrl.ts:77
|
||||
msgid "Invalid HY2 URL: invalid port number"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:32
|
||||
#: src/validators/validateHysteriaUrl.ts:30
|
||||
msgid "Invalid HY2 URL: missing credentials/server"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:49
|
||||
#: src/validators/validateHysteriaUrl.ts:47
|
||||
msgid "Invalid HY2 URL: missing host"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:43
|
||||
#: src/validators/validateHysteriaUrl.ts:41
|
||||
msgid "Invalid HY2 URL: missing host & port"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:38
|
||||
#: src/validators/validateHysteriaUrl.ts:36
|
||||
msgid "Invalid HY2 URL: missing password"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:53
|
||||
#: src/validators/validateHysteriaUrl.ts:50
|
||||
msgid "Invalid HY2 URL: missing port"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:19
|
||||
#: src/validators/validateHysteriaUrl.ts:18
|
||||
msgid "Invalid HY2 URL: must not contain spaces"
|
||||
msgstr ""
|
||||
|
||||
@@ -430,19 +430,19 @@ msgstr ""
|
||||
msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:99
|
||||
#: src/validators/validateHysteriaUrl.ts:108
|
||||
msgid "Invalid HY2 URL: obfs-password required when obfs is set"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:115
|
||||
#: src/validators/validateHysteriaUrl.ts:122
|
||||
msgid "Invalid HY2 URL: parsing failed"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:108
|
||||
#: src/validators/validateHysteriaUrl.ts:116
|
||||
msgid "Invalid HY2 URL: sni cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: src/validators/validateHysteriaUrl.ts:88
|
||||
#: src/validators/validateHysteriaUrl.ts:98
|
||||
msgid "Invalid HY2 URL: unsupported obfs type"
|
||||
msgstr ""
|
||||
|
||||
@@ -563,7 +563,7 @@ msgstr ""
|
||||
msgid "Issues detected"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:456
|
||||
#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:48
|
||||
msgid "Latest"
|
||||
msgstr ""
|
||||
|
||||
@@ -641,7 +641,7 @@ msgstr ""
|
||||
msgid "Outbound Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:446
|
||||
#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:38
|
||||
msgid "Outdated"
|
||||
msgstr ""
|
||||
|
||||
@@ -996,7 +996,7 @@ msgstr ""
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:41
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:42
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:43
|
||||
#: src/podkop/tabs/diagnostic/initController.ts:420
|
||||
#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:7
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
|
||||
@@ -1065,7 +1065,7 @@ msgstr ""
|
||||
#: src/validators/validateDns.ts:18
|
||||
#: src/validators/validateDomain.ts:13
|
||||
#: src/validators/validateDomain.ts:30
|
||||
#: src/validators/validateHysteriaUrl.ts:113
|
||||
#: src/validators/validateHysteriaUrl.ts:120
|
||||
#: src/validators/validateIp.ts:8
|
||||
#: src/validators/validateOutboundJson.ts:7
|
||||
#: src/validators/validatePath.ts:16
|
||||
|
||||
@@ -312,6 +312,7 @@ create_nft_rules() {
|
||||
nft add chain inet "$NFT_TABLE_NAME" mangle_output '{ type route hook output priority -150; policy accept; }'
|
||||
nft add chain inet "$NFT_TABLE_NAME" proxy '{ type filter hook prerouting priority -100; policy accept; }'
|
||||
|
||||
nft add rule inet "$NFT_TABLE_NAME" mangle ct status dnat return
|
||||
nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
|
||||
nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter
|
||||
nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter
|
||||
@@ -509,7 +510,7 @@ list_update() {
|
||||
local service_proxy_address
|
||||
service_proxy_address="$(get_service_proxy_address)"
|
||||
|
||||
if [ -n "$http_proxy_address" ]; then
|
||||
if [ -n "$service_proxy_address" ]; then
|
||||
if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then
|
||||
echolog "✅ GitHub connection check passed (via proxy)"
|
||||
break
|
||||
@@ -1316,7 +1317,7 @@ import_community_service_subnet_list_handler() {
|
||||
URL=$SUBNETS_DISCORD
|
||||
nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME"
|
||||
nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr \
|
||||
"@$NFT_DISCORD_SET_NAME" udp dport '{ 50000-65535 }' meta mark set "$NFT_FAKEIP_MARK" counter
|
||||
"@$NFT_DISCORD_SET_NAME" udp dport '{ 19000-20000, 50000-65535 }' meta mark set "$NFT_FAKEIP_MARK" counter
|
||||
;;
|
||||
"roblox")
|
||||
URL=$SUBNETS_ROBLOX
|
||||
|
||||
@@ -60,7 +60,7 @@ patch_source_ruleset_rules() {
|
||||
import_plain_domain_list_to_local_source_ruleset_chunked() {
|
||||
local plain_list_filepath="$1"
|
||||
local ruleset_filepath="$2"
|
||||
local chunk_size="${3:-5000}"
|
||||
local chunk_size="${3:-1000}"
|
||||
|
||||
local array count json_array
|
||||
count=0
|
||||
@@ -102,7 +102,7 @@ import_plain_domain_list_to_local_source_ruleset_chunked() {
|
||||
import_plain_subnet_list_to_local_source_ruleset_chunked() {
|
||||
local plain_list_filepath="$1"
|
||||
local ruleset_filepath="$2"
|
||||
local chunk_size="${3:-5000}"
|
||||
local chunk_size="${3:-1000}"
|
||||
|
||||
local array count json_array
|
||||
count=0
|
||||
|
||||
@@ -178,8 +178,9 @@ _add_outbound_security() {
|
||||
|
||||
local security scheme
|
||||
security=$(url_get_query_param "$url" "security")
|
||||
scheme="$(url_get_scheme "$url")"
|
||||
|
||||
if [ -z "$security" ]; then
|
||||
scheme="$(url_get_scheme "$url")"
|
||||
if [ "$scheme" = "hysteria2" ] || [ "$scheme" = "hy2" ]; then
|
||||
security="tls"
|
||||
fi
|
||||
@@ -195,6 +196,10 @@ _add_outbound_security() {
|
||||
public_key=$(url_get_query_param "$url" "pbk")
|
||||
short_id=$(url_get_query_param "$url" "sid")
|
||||
|
||||
if [ "$scheme" = "hysteria2" ] || [ "$scheme" = "hy2" ]; then
|
||||
fingerprint=""
|
||||
fi
|
||||
|
||||
config=$(
|
||||
sing_box_cm_set_tls_for_outbound \
|
||||
"$config" \
|
||||
|
||||
@@ -692,6 +692,11 @@ sing_box_cm_add_hysteria2_outbound() {
|
||||
local download_mbps="$9"
|
||||
local network="${10}"
|
||||
|
||||
local is_multi_port="false"
|
||||
case "$server_port" in
|
||||
*","*|*"-"*) is_multi_port="true" ;;
|
||||
esac
|
||||
|
||||
echo "$config" | jq \
|
||||
--arg tag "$tag" \
|
||||
--arg server_address "$server_address" \
|
||||
@@ -702,14 +707,25 @@ sing_box_cm_add_hysteria2_outbound() {
|
||||
--arg upload_mbps "$upload_mbps" \
|
||||
--arg download_mbps "$download_mbps" \
|
||||
--arg network "$network" \
|
||||
'.outbounds += [(
|
||||
--argjson is_multi_port "$is_multi_port" \
|
||||
'
|
||||
def parse_port_entry:
|
||||
if index("-") then split("-") | join(":")
|
||||
else . + ":" + .
|
||||
end;
|
||||
|
||||
.outbounds += [(
|
||||
{
|
||||
type: "hysteria2",
|
||||
tag: $tag,
|
||||
server: $server_address,
|
||||
server_port: ($server_port | tonumber),
|
||||
password: $password
|
||||
}
|
||||
+ (if $is_multi_port then {
|
||||
server_ports: ($server_port | split(",") | map(ltrimstr(" ") | rtrimstr(" ") | parse_port_entry))
|
||||
} else {
|
||||
server_port: ($server_port | tonumber)
|
||||
} end)
|
||||
+ (if $obfuscator_type != "" and $obfuscator_password != "" then {
|
||||
obfs: {
|
||||
type: $obfuscator_type,
|
||||
|
||||
0
src/backend/.keep
Normal file
0
src/backend/.keep
Normal file
4
src/frontend/.gitignore
vendored
Normal file
4
src/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
.env
|
||||
12
src/frontend/package.json
Normal file
12
src/frontend/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "podkop-frontend",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:sandbox": "cd sandbox/docker && ./up.sh",
|
||||
"sandbox:down": "cd sandbox/docker && docker compose down",
|
||||
"sandbox:logs": "cd sandbox/docker && docker compose logs -f",
|
||||
"sandbox:shell": "docker exec -it podkop-dev-router sh",
|
||||
"sandbox:scenario": "cd sandbox/docker && ./scenario.sh"
|
||||
}
|
||||
}
|
||||
32
src/frontend/sandbox/Makefile
Normal file
32
src/frontend/sandbox/Makefile
Normal file
@@ -0,0 +1,32 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-podkop
|
||||
|
||||
PKG_VERSION := $(if $(PODKOP_VERSION),$(PODKOP_VERSION),0.$(shell date +%d%m%Y))
|
||||
|
||||
PKG_RELEASE:=1
|
||||
|
||||
LUCI_TITLE:=LuCI podkop app
|
||||
# +podkop dropped while the backend is rewritten in Go; the frontend runs
|
||||
# against a stub (docker/files/usr/bin/podkop). Restore before release.
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_LANG.ru:=Русский (Russian)
|
||||
LUCI_LANG.en:=English
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-or-later
|
||||
PKG_MAINTAINER:=ITDog <podkop@itdog.info>
|
||||
|
||||
LUCI_LANGUAGES:=en ru
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)$(HTDOCS)
|
||||
$(CP) $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/
|
||||
$(INSTALL_DIR) $(1)/
|
||||
$(CP) $(PKG_BUILD_DIR)/root/* $(1)/
|
||||
sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)$(HTDOCS)/luci-static/resources/view/podkop/main.js || true
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
26
src/frontend/sandbox/docker/Dockerfile
Normal file
26
src/frontend/sandbox/docker/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# Dev stand for luci-app-podkop: stock OpenWrt userland + real LuCI.
|
||||
#
|
||||
# The app is loaded by LuCI's own require loader and reaches the system through
|
||||
# rpcd with real ACL checks — neither is exercised by a JS-level mock.
|
||||
# No backend: /usr/bin/podkop is a shell stub serving fixtures.
|
||||
ARG OPENWRT_TAG=x86-64-25.12.4
|
||||
FROM openwrt/rootfs:${OPENWRT_TAG}
|
||||
|
||||
# Absent from the rootfs tarball; on a router procd creates them under the /var
|
||||
# tmpfs. apk needs them earlier.
|
||||
RUN mkdir -p /var/lock /var/run /var/log /var/state
|
||||
|
||||
# firewall/opkg are not about podkop — a page in an empty menu shows nothing
|
||||
# about how the app sits next to its neighbours.
|
||||
RUN set -eu; \
|
||||
apk update; \
|
||||
for p in luci luci-app-firewall luci-app-opkg; do \
|
||||
apk add "$p" || echo "skipped (not in feed): $p"; \
|
||||
done
|
||||
|
||||
COPY files/ /
|
||||
RUN chmod +x /usr/bin/podkop /etc/init.d/podkop /usr/lib/podkop-dev/entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["/usr/lib/podkop-dev/entrypoint.sh"]
|
||||
CMD ["/sbin/init"]
|
||||
73
src/frontend/sandbox/docker/README.md
Normal file
73
src/frontend/sandbox/docker/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Dev stand for luci-app-podkop
|
||||
|
||||
Real OpenWrt 25.12 with real LuCI in a container. The app is mounted from the
|
||||
host, so the loop is: edit js, hit F5.
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
yarn dev:sandbox # from src/frontend
|
||||
./up.sh # or directly from here
|
||||
```
|
||||
|
||||
UI: <http://localhost:8080> → **Services → Podkop** (`root`, empty password).
|
||||
Stop with `docker compose down`.
|
||||
|
||||
Use `up.sh`, not `docker compose up`: `openwrt/rootfs` has no multiarch
|
||||
manifest, and the arm image declares `aarch64_generic` rather than `arm64`.
|
||||
`up.sh` derives both the tag and the platform from `uname -m`.
|
||||
|
||||
## Why a container instead of a browser mock
|
||||
|
||||
A JS mock only exercises our own code. Here everything below the app is real:
|
||||
|
||||
| mocked in the browser | real here |
|
||||
| --- | --- |
|
||||
| `fs.exec` | rpcd + **ACL checks** from `acl.d/luci-app-podkop.json` |
|
||||
| `uci` | uci over `/etc/config/podkop` |
|
||||
| `_()` | translations loaded from `.po` |
|
||||
| theme | `cascade.css` served by LuCI |
|
||||
| "the file parses" | **LuCI's require loader** |
|
||||
|
||||
The last two are the point. An ACL mistake returns 403 only on a router.
|
||||
|
||||
## What is not here
|
||||
|
||||
**The backend.** It is being rewritten in Go; `/usr/bin/podkop` and
|
||||
`/etc/init.d/podkop` are shell stubs serving fixtures. The mock boundary is the
|
||||
binary, not the JS, so the whole frontend above it stays real.
|
||||
|
||||
**Traffic interception.** `kmod-nft-tproxy` is built against OpenWrt's kernel
|
||||
and cannot load in a container. `check_nft_rules` reports some rules missing on
|
||||
purpose — that is the stand's honest state.
|
||||
|
||||
**The luci-app version.** The package Makefile substitutes it at build time;
|
||||
here the js is mounted as-is, so diagnostics shows
|
||||
`__COMPILED_VERSION_VARIABLE__`.
|
||||
|
||||
## Scenarios
|
||||
|
||||
```sh
|
||||
./scenario.sh # current
|
||||
./scenario.sh ok # everything works
|
||||
./scenario.sh fail # commands fail — exercises failed states
|
||||
./scenario.sh stopped # services down, Clash unreachable
|
||||
```
|
||||
|
||||
Reload the page afterwards.
|
||||
|
||||
## Layout
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `Dockerfile` | OpenWrt 25.12.4 + luci (+ firewall/opkg for menu neighbours) |
|
||||
| `compose.yml` | `router` (8080) and `clash-mock` (9090) |
|
||||
| `files/usr/bin/podkop` | backend stub |
|
||||
| `files/etc/init.d/podkop` | service stub for Start/Stop/Restart |
|
||||
| `files/etc/config/podkop` | uci fixture, one section per parse branch |
|
||||
| `files/etc/rc.local` | seeds runtime state after boot |
|
||||
| `clash-mock/server.mjs` | `/traffic` and `/connections` websockets |
|
||||
|
||||
`/etc/config/podkop` is required: `menu.d` declares
|
||||
`"depends": { "uci": { "podkop": true } }`, so without it LuCI hides the menu
|
||||
entry entirely.
|
||||
109
src/frontend/sandbox/docker/clash-mock/server.mjs
Normal file
109
src/frontend/sandbox/docker/clash-mock/server.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Clash API metrics for the dev stand: /traffic and /connections on :9090.
|
||||
*
|
||||
* Handshake and framing are written by hand so node:22-alpine needs no
|
||||
* npm install.
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createServer } from 'node:http';
|
||||
|
||||
const PORT = 9090;
|
||||
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
|
||||
/** Text frame: FIN + opcode 1, unmasked (servers do not mask). */
|
||||
function encodeFrame(text) {
|
||||
const payload = Buffer.from(text, 'utf8');
|
||||
const length = payload.length;
|
||||
|
||||
let header;
|
||||
|
||||
if (length < 126) {
|
||||
header = Buffer.from([0x81, length]);
|
||||
} else if (length < 65536) {
|
||||
header = Buffer.alloc(4);
|
||||
header[0] = 0x81;
|
||||
header[1] = 126;
|
||||
header.writeUInt16BE(length, 2);
|
||||
} else {
|
||||
header = Buffer.alloc(10);
|
||||
header[0] = 0x81;
|
||||
header[1] = 127;
|
||||
header.writeBigUInt64BE(BigInt(length), 2);
|
||||
}
|
||||
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
const state = {
|
||||
uploadTotal: 4_800_000_000,
|
||||
downloadTotal: 51_200_000_000,
|
||||
};
|
||||
|
||||
function trafficFrame() {
|
||||
return {
|
||||
up: Math.floor(20_000 + Math.random() * 900_000),
|
||||
down: Math.floor(80_000 + Math.random() * 6_000_000),
|
||||
};
|
||||
}
|
||||
|
||||
function connectionsFrame() {
|
||||
state.uploadTotal += Math.floor(Math.random() * 900_000);
|
||||
state.downloadTotal += Math.floor(Math.random() * 6_000_000);
|
||||
|
||||
return {
|
||||
uploadTotal: state.uploadTotal,
|
||||
downloadTotal: state.downloadTotal,
|
||||
memory: Math.floor(28_000_000 + Math.random() * 12_000_000),
|
||||
connections: Array.from(
|
||||
{ length: 8 + Math.floor(Math.random() * 24) },
|
||||
(_unused, index) => ({ id: `conn-${index}` }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
// The dashboard only uses the websocket; GET just needs to say something
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ hello: 'podkop dev clash mock', path: req.url }));
|
||||
});
|
||||
|
||||
server.on('upgrade', (req, socket) => {
|
||||
const key = req.headers['sec-websocket-key'];
|
||||
|
||||
if (!key) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const accept = createHash('sha1')
|
||||
.update(key + GUID)
|
||||
.digest('base64');
|
||||
|
||||
socket.write(
|
||||
'HTTP/1.1 101 Switching Protocols\r\n' +
|
||||
'Upgrade: websocket\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
|
||||
);
|
||||
|
||||
const path = (req.url || '').split('?')[0];
|
||||
const build = path === '/traffic' ? trafficFrame : connectionsFrame;
|
||||
|
||||
console.log(`[clash-mock] connected: ${path}`);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
socket.write(encodeFrame(JSON.stringify(build())));
|
||||
}, 1000);
|
||||
|
||||
const stop = () => {
|
||||
clearInterval(timer);
|
||||
socket.destroy();
|
||||
};
|
||||
|
||||
socket.on('close', stop);
|
||||
socket.on('error', stop);
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[clash-mock] listening on :${PORT} (/traffic, /connections)`);
|
||||
});
|
||||
37
src/frontend/sandbox/docker/compose.yml
Normal file
37
src/frontend/sandbox/docker/compose.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: podkop-dev
|
||||
|
||||
services:
|
||||
router:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# No multiarch manifest — one tag per arch. Set by ./up.sh.
|
||||
OPENWRT_TAG: ${OPENWRT_TAG:-x86-64-25.12.4}
|
||||
# The arm image declares OpenWrt's own arch name (aarch64_generic),
|
||||
# which Docker does not match to linux/arm64 on its own.
|
||||
platform: ${OPENWRT_PLATFORM:-linux/amd64}
|
||||
container_name: podkop-dev-router
|
||||
hostname: OpenWrt
|
||||
privileged: true # procd as PID 1
|
||||
init: false
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '8080:80'
|
||||
volumes:
|
||||
# Mounted from the host: edit js, hit F5, no rebuild.
|
||||
- ../htdocs/luci-static/resources/view/podkop:/www/luci-static/resources/view/podkop:ro
|
||||
- ../root/usr/share/luci/menu.d/luci-app-podkop.json:/usr/share/luci/menu.d/luci-app-podkop.json:ro
|
||||
- ../root/usr/share/rpcd/acl.d/luci-app-podkop.json:/usr/share/rpcd/acl.d/luci-app-podkop.json:ro
|
||||
|
||||
# Nothing in the OpenWrt container can serve a websocket (sing-box is
|
||||
# deliberately absent), so Clash API metrics live in their own service.
|
||||
clash-mock:
|
||||
image: node:22-alpine
|
||||
container_name: podkop-dev-clash
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '9090:9090'
|
||||
volumes:
|
||||
- ./clash-mock:/app:ro
|
||||
command: ['node', '/app/server.mjs']
|
||||
47
src/frontend/sandbox/docker/files/etc/config/podkop
Normal file
47
src/frontend/sandbox/docker/files/etc/config/podkop
Normal file
@@ -0,0 +1,47 @@
|
||||
# uci fixture for the dev stand.
|
||||
#
|
||||
# Required: menu.d declares "depends": { "uci": { "podkop": true } }, so LuCI
|
||||
# hides the menu entry until this file exists.
|
||||
#
|
||||
# Sections cover every parse branch on the dashboard — url / outbound /
|
||||
# selector / urltest / vpn — plus block and exclusion, which must be filtered out.
|
||||
|
||||
config settings 'settings'
|
||||
option enable_yacd '1'
|
||||
option yacd_secret_key 'dev-stand-secret'
|
||||
option dns_type 'doh'
|
||||
option dns_server 'dns.adguard-dns.com'
|
||||
|
||||
config section 'youtube'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'url'
|
||||
option proxy_string 'vless://uuid@example.com:443?type=tcp#YouTube%20VLESS'
|
||||
list domain_list 'youtube'
|
||||
|
||||
config section 'netflix'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'outbound'
|
||||
option outbound_json '{"tag":"Netflix%20Shadowsocks","type":"shadowsocks"}'
|
||||
|
||||
config section 'media'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'selector'
|
||||
list selector_proxy_links 'vless://uuid@nl.example.com:443#Netherlands'
|
||||
list selector_proxy_links 'vless://uuid@de.example.com:443#Germany'
|
||||
list selector_proxy_links 'trojan://uuid@fi.example.com:443#Finland'
|
||||
|
||||
config section 'fast'
|
||||
option connection_type 'proxy'
|
||||
option proxy_config_type 'urltest'
|
||||
list urltest_proxy_links 'vless://uuid@se.example.com:443#Sweden'
|
||||
list urltest_proxy_links 'vless://uuid@pl.example.com:443#Poland'
|
||||
|
||||
config section 'home_vpn'
|
||||
option connection_type 'vpn'
|
||||
option interface 'wg0'
|
||||
|
||||
config section 'ads'
|
||||
option connection_type 'block'
|
||||
|
||||
config section 'local'
|
||||
option connection_type 'exclusion'
|
||||
9
src/frontend/sandbox/docker/files/etc/config/system
Normal file
9
src/frontend/sandbox/docker/files/etc/config/system
Normal file
@@ -0,0 +1,9 @@
|
||||
config system
|
||||
option hostname 'OpenWrt'
|
||||
option timezone 'UTC'
|
||||
option ttylogin '0'
|
||||
option log_size '64'
|
||||
option urandom_seed '0'
|
||||
|
||||
config timeserver 'ntp'
|
||||
option enabled '0'
|
||||
32
src/frontend/sandbox/docker/files/etc/init.d/podkop
Executable file
32
src/frontend/sandbox/docker/files/etc/init.d/podkop
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Service stub. No daemon behind it — just enough for the Start/Stop/Restart/
|
||||
# Enable/Disable buttons to go through rpcd and change observable state.
|
||||
START=99
|
||||
USE_PROCD=0
|
||||
|
||||
STATE=/tmp/podkop-service-state
|
||||
|
||||
start() {
|
||||
echo running >"$STATE"
|
||||
echo '{"result":"started"}'
|
||||
}
|
||||
|
||||
stop() {
|
||||
echo stopped >"$STATE"
|
||||
echo '{"result":"stopped"}'
|
||||
}
|
||||
|
||||
restart() {
|
||||
echo running >"$STATE"
|
||||
echo '{"result":"restarted"}'
|
||||
}
|
||||
|
||||
enable() {
|
||||
touch /etc/rc.d/S99podkop 2>/dev/null
|
||||
echo '{"result":"enabled"}'
|
||||
}
|
||||
|
||||
disable() {
|
||||
rm -f /etc/rc.d/S99podkop 2>/dev/null
|
||||
echo '{"result":"disabled"}'
|
||||
}
|
||||
9
src/frontend/sandbox/docker/files/etc/rc.local
Normal file
9
src/frontend/sandbox/docker/files/etc/rc.local
Normal file
@@ -0,0 +1,9 @@
|
||||
# Seed runtime state for the backend stub.
|
||||
#
|
||||
# Not in the entrypoint: that runs before procd, which mounts a tmpfs over /tmp
|
||||
# and wipes anything written there earlier.
|
||||
|
||||
echo ok >/tmp/podkop-scenario
|
||||
echo running >/tmp/podkop-service-state
|
||||
|
||||
exit 0
|
||||
203
src/frontend/sandbox/docker/files/usr/bin/podkop
Executable file
203
src/frontend/sandbox/docker/files/usr/bin/podkop
Executable file
@@ -0,0 +1,203 @@
|
||||
#!/bin/sh
|
||||
# Backend stub for the dev stand. The real backend is being rewritten in Go.
|
||||
#
|
||||
# The mock boundary sits here rather than in JS, so rpcd, ACL, fs.exec, uci and
|
||||
# LuCI's require loader all stay real.
|
||||
#
|
||||
# Commands mirror what main.js actually calls, nothing more.
|
||||
#
|
||||
# Scenario is read from a file (see docker/scenario.sh):
|
||||
# ok everything works (default)
|
||||
# fail commands fail with empty stdout
|
||||
# stopped services down, Clash unreachable
|
||||
SCENARIO_FILE=/tmp/podkop-scenario
|
||||
SCENARIO=$(cat "$SCENARIO_FILE" 2>/dev/null || echo ok)
|
||||
|
||||
# Latency tests and node selection must persist between calls
|
||||
LATENCY_DIR=/tmp/podkop-latency
|
||||
SELECTED_DIR=/tmp/podkop-selected
|
||||
mkdir -p "$LATENCY_DIR" "$SELECTED_DIR"
|
||||
|
||||
# Empty stdout is how the frontend detects failure
|
||||
die_if_failing() {
|
||||
[ "$SCENARIO" = "fail" ] && exit 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# 40..2000 ms. Not `date +%s%N` (%N is not portable, emits a literal "N") and
|
||||
# not `od` (absent from the OpenWrt image; empty output pinned every node to 40).
|
||||
rand_latency() {
|
||||
_r="$RANDOM"
|
||||
|
||||
if [ -z "$_r" ]; then
|
||||
_r=$(awk 'BEGIN{srand();print int(rand()*32768)}')
|
||||
fi
|
||||
|
||||
echo $((_r % 1960 + 40))
|
||||
}
|
||||
|
||||
latency_of() { # $1=code $2=fallback
|
||||
if [ -f "$LATENCY_DIR/$1" ]; then cat "$LATENCY_DIR/$1"; else echo "$2"; fi
|
||||
}
|
||||
|
||||
selected_of() { # $1=group $2=fallback
|
||||
if [ -f "$SELECTED_DIR/$1" ]; then cat "$SELECTED_DIR/$1"; else echo "$2"; fi
|
||||
}
|
||||
|
||||
history_of() { # $1=delay
|
||||
if [ "$1" -eq 0 ]; then
|
||||
echo '[]'
|
||||
else
|
||||
echo "[{\"time\":\"2026-07-20T10:00:00Z\",\"delay\":$1}]"
|
||||
fi
|
||||
}
|
||||
|
||||
proxy_json() { # $1=name $2=type $3=delay
|
||||
printf '"%s":{"type":"%s","name":"%s","udp":true,"history":%s}' \
|
||||
"$1" "$2" "$1" "$(history_of "$3")"
|
||||
}
|
||||
|
||||
group_json() { # $1=name $2=type $3=now $4=all(json) $5=delay
|
||||
printf '"%s":{"type":"%s","name":"%s","udp":false,"now":"%s","all":%s,"history":%s}' \
|
||||
"$1" "$2" "$1" "$3" "$4" "$(history_of "$5")"
|
||||
}
|
||||
|
||||
# Mirrors the sections in /etc/config/podkop; covers every parse branch on the
|
||||
# dashboard: url / outbound / selector / urltest / vpn.
|
||||
emit_proxies() {
|
||||
printf '{"proxies":{'
|
||||
proxy_json 'youtube-out' 'vless' "$(latency_of youtube-out 142)"
|
||||
printf ','
|
||||
proxy_json 'netflix-out' 'shadowsocks' "$(latency_of netflix-out 980)"
|
||||
printf ','
|
||||
group_json 'media-out' 'Selector' "$(selected_of media-out 'media-2-out')" \
|
||||
'["media-1-out","media-2-out","media-3-out"]' 0
|
||||
printf ','
|
||||
proxy_json 'media-1-out' 'vless' "$(latency_of media-1-out 210)"
|
||||
printf ','
|
||||
proxy_json 'media-2-out' 'vless' "$(latency_of media-2-out 1320)"
|
||||
printf ','
|
||||
proxy_json 'media-3-out' 'trojan' "$(latency_of media-3-out 2400)"
|
||||
printf ','
|
||||
group_json 'fast-out' 'Selector' "$(selected_of fast-out 'fast-urltest-out')" \
|
||||
'["fast-urltest-out","fast-1-out","fast-2-out"]' 0
|
||||
printf ','
|
||||
group_json 'fast-urltest-out' 'URLTest' '' \
|
||||
'["fast-1-out","fast-2-out"]' "$(latency_of fast-urltest-out 96)"
|
||||
printf ','
|
||||
proxy_json 'fast-1-out' 'vless' "$(latency_of fast-1-out 96)"
|
||||
printf ','
|
||||
proxy_json 'fast-2-out' 'vless' "$(latency_of fast-2-out 0)"
|
||||
printf ','
|
||||
proxy_json 'home_vpn-out' 'wireguard' "$(latency_of home_vpn-out 18)"
|
||||
printf '}}\n'
|
||||
}
|
||||
|
||||
members_of() {
|
||||
case "$1" in
|
||||
media-out) echo 'media-1-out media-2-out media-3-out' ;;
|
||||
fast-out) echo 'fast-urltest-out fast-1-out fast-2-out' ;;
|
||||
fast-urltest-out) echo 'fast-1-out fast-2-out' ;;
|
||||
*) echo '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
clash_api() {
|
||||
[ "$SCENARIO" = "stopped" ] && exit 1
|
||||
|
||||
case "$1" in
|
||||
get_proxies)
|
||||
emit_proxies
|
||||
;;
|
||||
get_proxy_latency)
|
||||
delay=$(rand_latency)
|
||||
echo "$delay" >"$LATENCY_DIR/$2"
|
||||
echo "{\"delay\":$delay}"
|
||||
;;
|
||||
get_group_latency)
|
||||
i=0
|
||||
printf '{'
|
||||
for member in $(members_of "$2"); do
|
||||
i=$((i + 1))
|
||||
delay=$(rand_latency)
|
||||
echo "$delay" >"$LATENCY_DIR/$member"
|
||||
[ "$i" -gt 1 ] && printf ','
|
||||
printf '"%s":%s' "$member" "$delay"
|
||||
done
|
||||
printf '}\n'
|
||||
;;
|
||||
set_group_proxy)
|
||||
echo "$3" >"$SELECTED_DIR/$2"
|
||||
echo '{"ok":true}'
|
||||
;;
|
||||
*)
|
||||
echo "{\"error\":\"unknown clash_api method: $1\"}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
die_if_failing
|
||||
|
||||
case "$1" in
|
||||
get_status)
|
||||
# Reads the autostart flag so Enable/Disable have a visible effect
|
||||
if [ "$SCENARIO" = "stopped" ] || [ ! -e /etc/rc.d/S99podkop ]; then
|
||||
echo '{"enabled":0,"status":"disabled"}'
|
||||
else
|
||||
echo '{"enabled":1,"status":"enabled"}'
|
||||
fi
|
||||
;;
|
||||
|
||||
get_sing_box_status)
|
||||
# Same for Start/Stop/Restart
|
||||
if [ "$SCENARIO" = "stopped" ] ||
|
||||
[ "$(cat /tmp/podkop-service-state 2>/dev/null)" = "stopped" ]; then
|
||||
echo '{"running":0,"enabled":0,"status":"stopped"}'
|
||||
else
|
||||
echo '{"running":1,"enabled":1,"status":"running"}'
|
||||
fi
|
||||
;;
|
||||
|
||||
get_system_info)
|
||||
echo '{"podkop_version":"0.8.1-dev","podkop_latest_version":"0.8.2","luci_app_version":"0.8.1-dev","sing_box_version":"1.11.4","openwrt_version":"OpenWrt 25.12.4","device_model":"Docker dev stand"}'
|
||||
;;
|
||||
|
||||
check_dns_available)
|
||||
echo '{"dns_type":"doh","dns_server":"dns.adguard-dns.com","dns_status":1,"dns_on_router":1,"bootstrap_dns_server":"1.1.1.1","bootstrap_dns_status":1,"dhcp_config_status":1}'
|
||||
;;
|
||||
|
||||
check_nft_rules)
|
||||
# No tproxy here: OpenWrt kernel modules cannot load in a container, so some
|
||||
# rules are reported missing on purpose.
|
||||
echo '{"table_exist":1,"rules_mangle_exist":1,"rules_mangle_counters":1,"rules_mangle_output_exist":1,"rules_mangle_output_counters":0,"rules_proxy_exist":1,"rules_proxy_counters":0,"rules_other_mark_exist":0}'
|
||||
;;
|
||||
|
||||
check_sing_box)
|
||||
echo '{"sing_box_installed":1,"sing_box_version_ok":1,"sing_box_service_exist":1,"sing_box_autostart_disabled":1,"sing_box_process_running":1,"sing_box_ports_listening":1}'
|
||||
;;
|
||||
|
||||
check_fakeip)
|
||||
echo '{"fakeip":true,"IP":"198.18.0.1"}'
|
||||
;;
|
||||
|
||||
check_logs)
|
||||
echo "$(date '+%a %b %e %H:%M:%S %Y') [info] podkop dev stand: backend stub"
|
||||
echo "$(date '+%a %b %e %H:%M:%S %Y') [info] scenario: $SCENARIO"
|
||||
;;
|
||||
|
||||
global_check)
|
||||
echo '{"result":"ok","note":"dev stand"}'
|
||||
;;
|
||||
|
||||
show_sing_box_config)
|
||||
echo '{"log":{"level":"info"},"experimental":{"clash_api":{"external_controller":"0.0.0.0:9090"}},"note":"dev stand stub"}'
|
||||
;;
|
||||
|
||||
clash_api)
|
||||
clash_api "$2" "$3" "$4"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "{\"error\":\"unknown method: $1\"}"
|
||||
;;
|
||||
esac
|
||||
58
src/frontend/sandbox/docker/files/usr/lib/podkop-dev/entrypoint.sh
Executable file
58
src/frontend/sandbox/docker/files/usr/lib/podkop-dev/entrypoint.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
# Prepare the stand, then hand over to procd.
|
||||
#
|
||||
# Networking has to happen HERE, before `exec /sbin/init`: config_generate
|
||||
# writes its own /etc/config/network from board.json (br-lan, hardcoded
|
||||
# 192.168.1.1), netifd applies it and Docker's address is gone. By the time
|
||||
# uci-defaults run, eth0 is already down and flushed.
|
||||
#
|
||||
# config_generate's guard is `[ -s /etc/config/network -a -s /etc/config/system ]`,
|
||||
# so both files must exist — hence /etc/config/system in the image.
|
||||
#
|
||||
# The interface is named 'lan' because OpenWrt's default firewall allows input
|
||||
# in that zone; that is what makes port 80 reachable from the host.
|
||||
#
|
||||
# Approach borrowed from luci-theme-footstrap.
|
||||
set -e
|
||||
|
||||
addr="$(ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4; exit}')"
|
||||
gw="$(ip -4 route show default 2>/dev/null | awk '{print $3; exit}')"
|
||||
dns="${PODKOP_DEV_DNS:-1.1.1.1 8.8.8.8}"
|
||||
|
||||
# Docker's resolv.conf points at 127.0.0.11, which works via NAT rules that
|
||||
# OpenWrt's boot flushes.
|
||||
for ns in $dns; do echo "nameserver $ns"; done >/etc/resolv.conf
|
||||
|
||||
if [ -n "$addr" ]; then
|
||||
netmask="$(ipcalc.sh "$addr" | sed -n 's/^NETMASK=//p')"
|
||||
cat >/etc/config/network <<-EOF
|
||||
config interface 'loopback'
|
||||
option device 'lo'
|
||||
option proto 'static'
|
||||
option ipaddr '127.0.0.1'
|
||||
option netmask '255.0.0.0'
|
||||
|
||||
config globals 'globals'
|
||||
|
||||
config interface 'lan'
|
||||
option device 'eth0'
|
||||
option proto 'static'
|
||||
option ipaddr '${addr%/*}'
|
||||
option netmask '$netmask'
|
||||
option gateway '$gw'
|
||||
option dns '$dns'
|
||||
option delegate '0'
|
||||
EOF
|
||||
else
|
||||
echo "podkop-dev: eth0 has no IPv4 — /etc/config/network left alone" >&2
|
||||
fi
|
||||
|
||||
# Autostart on by default; get_status reads this. /etc survives boot, /tmp does
|
||||
# not — runtime state is seeded from /etc/rc.local instead.
|
||||
mkdir -p /etc/rc.d
|
||||
touch /etc/rc.d/S99podkop
|
||||
|
||||
# Until the cache is dropped, the mounted menu.d/acl.d stay invisible.
|
||||
rm -f /tmp/luci-indexcache* /var/luci-indexcache* 2>/dev/null || true
|
||||
|
||||
exec "$@"
|
||||
32
src/frontend/sandbox/docker/scenario.sh
Executable file
32
src/frontend/sandbox/docker/scenario.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# Switch the backend stub's scenario on a running stand.
|
||||
#
|
||||
# ./scenario.sh show current
|
||||
# ./scenario.sh ok everything works
|
||||
# ./scenario.sh fail commands fail (empty stdout)
|
||||
# ./scenario.sh stopped services down, Clash unreachable
|
||||
#
|
||||
# Reload the page afterwards.
|
||||
set -e
|
||||
|
||||
CONTAINER=podkop-dev-router
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
# A missing file is fine — the stub falls back to ok
|
||||
current=$(docker exec "$CONTAINER" cat /tmp/podkop-scenario 2>/dev/null || true)
|
||||
echo "scenario: ${current:-ok (default)}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
ok | fail | stopped) ;;
|
||||
*)
|
||||
echo "unknown scenario: $1 (ok | fail | stopped)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
docker exec "$CONTAINER" sh -c "echo '$1' > /tmp/podkop-scenario"
|
||||
# Drop accumulated state so the scenario starts clean
|
||||
docker exec "$CONTAINER" sh -c 'rm -rf /tmp/podkop-latency /tmp/podkop-selected'
|
||||
echo "scenario: $1 — reload the page"
|
||||
36
src/frontend/sandbox/docker/up.sh
Executable file
36
src/frontend/sandbox/docker/up.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
# Start the stand, picking the OpenWrt image for the host arch.
|
||||
#
|
||||
# openwrt/rootfs has no multiarch manifest, and the arm image declares
|
||||
# aarch64_generic rather than arm64 — without both the tag and the platform,
|
||||
# the build fails with "no match for platform in manifest".
|
||||
#
|
||||
# Override: OPENWRT_TAG=... ./up.sh
|
||||
set -e
|
||||
|
||||
OPENWRT_RELEASE=25.12.4
|
||||
|
||||
if [ -z "$OPENWRT_TAG" ]; then
|
||||
case "$(uname -m)" in
|
||||
arm64 | aarch64)
|
||||
OPENWRT_TAG="armsr-armv8-$OPENWRT_RELEASE"
|
||||
OPENWRT_PLATFORM="linux/aarch64_generic"
|
||||
;;
|
||||
x86_64 | amd64)
|
||||
OPENWRT_TAG="x86-64-$OPENWRT_RELEASE"
|
||||
OPENWRT_PLATFORM="linux/amd64"
|
||||
;;
|
||||
*)
|
||||
echo "unknown arch $(uname -m); set OPENWRT_TAG manually" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "image: openwrt/rootfs:$OPENWRT_TAG ($OPENWRT_PLATFORM)"
|
||||
OPENWRT_TAG="$OPENWRT_TAG" OPENWRT_PLATFORM="$OPENWRT_PLATFORM" \
|
||||
docker compose up -d --build "$@"
|
||||
|
||||
echo
|
||||
echo "ui: http://localhost:8080 -> Services -> Podkop (root, empty password)"
|
||||
echo "scenarios: ./scenario.sh ok | fail | stopped"
|
||||
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require form";
|
||||
"require ui";
|
||||
"require uci";
|
||||
"require fs";
|
||||
"require view.podkop.main as main";
|
||||
|
||||
function createDashboardContent(section) {
|
||||
const o = section.option(form.DummyValue, "_mount_node");
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = () => {
|
||||
main.DashboardTab.initController();
|
||||
return main.DashboardTab.render();
|
||||
};
|
||||
}
|
||||
|
||||
const EntryPoint = {
|
||||
createDashboardContent,
|
||||
};
|
||||
|
||||
return baseclass.extend(EntryPoint);
|
||||
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
"require form";
|
||||
"require ui";
|
||||
"require uci";
|
||||
"require fs";
|
||||
"require view.podkop.main as main";
|
||||
|
||||
function createDiagnosticContent(section) {
|
||||
const o = section.option(form.DummyValue, "_mount_node");
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = () => {
|
||||
main.DiagnosticTab.initController();
|
||||
return main.DiagnosticTab.render();
|
||||
};
|
||||
}
|
||||
|
||||
const EntryPoint = {
|
||||
createDiagnosticContent,
|
||||
};
|
||||
|
||||
return baseclass.extend(EntryPoint);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
"use strict";
|
||||
"require view";
|
||||
"require form";
|
||||
"require baseclass";
|
||||
"require network";
|
||||
"require view.podkop.main as main";
|
||||
|
||||
// Settings content
|
||||
"require view.podkop.settings as settings";
|
||||
|
||||
// Sections content
|
||||
"require view.podkop.section as section";
|
||||
|
||||
// Dashboard content
|
||||
"require view.podkop.dashboard as dashboard";
|
||||
|
||||
// Diagnostic content
|
||||
"require view.podkop.diagnostic as diagnostic";
|
||||
|
||||
const EntryPoint = {
|
||||
async render() {
|
||||
main.injectGlobalStyles();
|
||||
|
||||
const podkopMap = new form.Map(
|
||||
"podkop",
|
||||
_("Podkop Settings"),
|
||||
_("Configuration for Podkop service"),
|
||||
);
|
||||
// Enable tab views
|
||||
podkopMap.tabbed = true;
|
||||
|
||||
// Sections tab
|
||||
const sectionsSection = podkopMap.section(
|
||||
form.TypedSection,
|
||||
"section",
|
||||
_("Sections"),
|
||||
);
|
||||
sectionsSection.anonymous = false;
|
||||
sectionsSection.addremove = true;
|
||||
sectionsSection.template = "cbi/simpleform";
|
||||
|
||||
// Render section content
|
||||
section.createSectionContent(sectionsSection);
|
||||
|
||||
// Settings tab
|
||||
const settingsSection = podkopMap.section(
|
||||
form.TypedSection,
|
||||
"settings",
|
||||
_("Settings"),
|
||||
);
|
||||
settingsSection.anonymous = true;
|
||||
settingsSection.addremove = false;
|
||||
// Make it named [ config settings 'settings' ]
|
||||
settingsSection.cfgsections = function () {
|
||||
return ["settings"];
|
||||
};
|
||||
|
||||
// Render settings content
|
||||
settings.createSettingsContent(settingsSection);
|
||||
|
||||
// Diagnostic tab
|
||||
const diagnosticSection = podkopMap.section(
|
||||
form.TypedSection,
|
||||
"diagnostic",
|
||||
_("Diagnostics"),
|
||||
);
|
||||
diagnosticSection.anonymous = true;
|
||||
diagnosticSection.addremove = false;
|
||||
diagnosticSection.cfgsections = function () {
|
||||
return ["diagnostic"];
|
||||
};
|
||||
|
||||
// Render diagnostic content
|
||||
diagnostic.createDiagnosticContent(diagnosticSection);
|
||||
|
||||
// Dashboard tab
|
||||
const dashboardSection = podkopMap.section(
|
||||
form.TypedSection,
|
||||
"dashboard",
|
||||
_("Dashboard"),
|
||||
);
|
||||
dashboardSection.anonymous = true;
|
||||
dashboardSection.addremove = false;
|
||||
dashboardSection.cfgsections = function () {
|
||||
return ["dashboard"];
|
||||
};
|
||||
|
||||
// Render dashboard content
|
||||
dashboard.createDashboardContent(dashboardSection);
|
||||
|
||||
// Inject core service
|
||||
main.coreService();
|
||||
|
||||
return podkopMap.render();
|
||||
},
|
||||
};
|
||||
|
||||
return view.extend(EntryPoint);
|
||||
@@ -0,0 +1,703 @@
|
||||
"use strict";
|
||||
"require form";
|
||||
"require baseclass";
|
||||
"require ui";
|
||||
"require tools.widgets as widgets";
|
||||
"require view.podkop.main as main";
|
||||
|
||||
function createSectionContent(section) {
|
||||
let o = section.option(
|
||||
form.ListValue,
|
||||
"connection_type",
|
||||
_("Connection Type"),
|
||||
_("Select between VPN and Proxy connection methods for traffic routing"),
|
||||
);
|
||||
o.value("proxy", "Proxy");
|
||||
o.value("vpn", "VPN");
|
||||
o.value("block", "Block");
|
||||
o.value("exclusion", "Exclusion");
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"proxy_config_type",
|
||||
_("Configuration Type"),
|
||||
_("Select how to configure the proxy"),
|
||||
);
|
||||
o.value("url", _("Connection URL"));
|
||||
o.value("selector", _("Selector"));
|
||||
o.value("urltest", _("URLTest"));
|
||||
o.value("outbound", _("Outbound Config"));
|
||||
o.default = "url";
|
||||
o.depends("connection_type", "proxy");
|
||||
|
||||
o = section.option(
|
||||
form.TextValue,
|
||||
"proxy_string",
|
||||
_("Proxy Configuration URL"),
|
||||
_("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links")
|
||||
);
|
||||
o.depends("proxy_config_type", "url");
|
||||
o.rows = 5;
|
||||
// Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links)
|
||||
o.wrap = "soft";
|
||||
// Render as a textarea to allow multiple proxy URLs/configs
|
||||
o.textarea = true;
|
||||
o.rmempty = false;
|
||||
o.sectionDescriptions = new Map();
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateProxyUrl(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.TextValue,
|
||||
"outbound_json",
|
||||
_("Outbound Configuration"),
|
||||
_("Enter complete outbound configuration in JSON format"),
|
||||
);
|
||||
o.depends("proxy_config_type", "outbound");
|
||||
o.rows = 10;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateOutboundJson(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"selector_proxy_links",
|
||||
_("Selector Proxy Links"),
|
||||
_("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links")
|
||||
);
|
||||
o.depends("proxy_config_type", "selector");
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateProxyUrl(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"urltest_proxy_links",
|
||||
_("URLTest Proxy Links"),
|
||||
_("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links")
|
||||
);
|
||||
o.depends("proxy_config_type", "urltest");
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateProxyUrl(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"urltest_check_interval",
|
||||
_("URLTest Check Interval"),
|
||||
_("The interval between connectivity tests")
|
||||
);
|
||||
o.value("30s", _("Every 30 seconds"));
|
||||
o.value("1m", _("Every 1 minute"));
|
||||
o.value("3m", _("Every 3 minutes"));
|
||||
o.value("5m", _("Every 5 minutes"));
|
||||
o.default = "3m";
|
||||
o.depends("proxy_config_type", "urltest");
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"urltest_tolerance",
|
||||
_("URLTest Tolerance"),
|
||||
_("The maximum difference in response times (ms) allowed when comparing servers")
|
||||
);
|
||||
o.default = "50";
|
||||
o.rmempty = false;
|
||||
o.depends("proxy_config_type", "urltest");
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parsed = parseFloat(value);
|
||||
|
||||
if (/^[0-9]+$/.test(value) && !isNaN(parsed) && isFinite(parsed) && parsed >= 50 && parsed <= 1000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _('Must be a number in the range of 50 - 1000');
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"urltest_testing_url",
|
||||
_("URLTest Testing URL"),
|
||||
_("The URL used to test server connectivity")
|
||||
);
|
||||
o.value("https://www.gstatic.com/generate_204", "https://www.gstatic.com/generate_204 (Google)");
|
||||
o.value("https://cp.cloudflare.com/generate_204", "https://cp.cloudflare.com/generate_204 (Cloudflare)");
|
||||
o.value("https://captive.apple.com", "https://captive.apple.com (Apple)");
|
||||
o.value("https://connectivity-check.ubuntu.com", "https://connectivity-check.ubuntu.com (Ubuntu)")
|
||||
o.default = "https://www.gstatic.com/generate_204";
|
||||
o.rmempty = false;
|
||||
o.depends("proxy_config_type", "urltest");
|
||||
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateUrl(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"enable_udp_over_tcp",
|
||||
_("UDP over TCP"),
|
||||
_("Applicable for SOCKS and Shadowsocks proxy"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.depends("connection_type", "proxy");
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
widgets.DeviceSelect,
|
||||
"interface",
|
||||
_("Network Interface"),
|
||||
_("Select network interface for VPN connection"),
|
||||
);
|
||||
o.depends("connection_type", "vpn");
|
||||
o.noaliases = true;
|
||||
o.nobridges = false;
|
||||
o.noinactive = false;
|
||||
o.filter = function (section_id, value) {
|
||||
// Blocked interface names that should never be selectable
|
||||
const blockedInterfaces = [
|
||||
"br-lan",
|
||||
"eth0",
|
||||
"eth1",
|
||||
"wan",
|
||||
"phy0-ap0",
|
||||
"phy1-ap0",
|
||||
"pppoe-wan",
|
||||
"lan",
|
||||
];
|
||||
|
||||
// Reject immediately if the value matches any blocked interface
|
||||
if (blockedInterfaces.includes(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find the device object with the given name
|
||||
const device = this.devices.find((dev) => dev.getName() === value);
|
||||
|
||||
// If no device is found, allow the value
|
||||
if (!device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the device type (e.g., "wifi", "ethernet", etc.)
|
||||
const type = device.getType();
|
||||
|
||||
// Reject wireless-related devices
|
||||
const isWireless =
|
||||
type === "wifi" || type === "wireless" || type.includes("wlan");
|
||||
|
||||
return !isWireless;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"domain_resolver_enabled",
|
||||
_("Domain Resolver"),
|
||||
_("Enable built-in DNS resolver for domains handled by this section"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
o.depends("connection_type", "vpn");
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"domain_resolver_dns_type",
|
||||
_("DNS Protocol Type"),
|
||||
_("Select the DNS protocol type for the domain resolver"),
|
||||
);
|
||||
o.value("doh", _("DNS over HTTPS (DoH)"));
|
||||
o.value("dot", _("DNS over TLS (DoT)"));
|
||||
o.value("udp", _("UDP (Unprotected DNS)"));
|
||||
o.default = "udp";
|
||||
o.rmempty = false;
|
||||
o.depends("domain_resolver_enabled", "1");
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"domain_resolver_dns_server",
|
||||
_("DNS Server"),
|
||||
_("Select or enter DNS server address"),
|
||||
);
|
||||
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
||||
o.value(key, _(label));
|
||||
});
|
||||
o.default = "8.8.8.8";
|
||||
o.rmempty = false;
|
||||
o.depends("domain_resolver_enabled", "1");
|
||||
o.validate = function (section_id, value) {
|
||||
const validation = main.validateDNS(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"community_lists",
|
||||
_("Community Lists"),
|
||||
_("Select a predefined list for routing") +
|
||||
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
|
||||
);
|
||||
o.placeholder = "Service list";
|
||||
Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
|
||||
o.value(key, _(label));
|
||||
});
|
||||
o.rmempty = true;
|
||||
let lastValues = [];
|
||||
let isProcessing = false;
|
||||
|
||||
o.onchange = function (ev, section_id, value) {
|
||||
if (isProcessing) return;
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
let newValues = [...values];
|
||||
let notifications = [];
|
||||
|
||||
const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) =>
|
||||
newValues.includes(opt),
|
||||
);
|
||||
|
||||
if (selectedRegionalOptions.length > 1) {
|
||||
const lastSelected =
|
||||
selectedRegionalOptions[selectedRegionalOptions.length - 1];
|
||||
const removedRegions = selectedRegionalOptions.slice(0, -1);
|
||||
newValues = newValues.filter(
|
||||
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
|
||||
);
|
||||
notifications.push(
|
||||
E("p", {}, [
|
||||
E("strong", {}, _("Regional options cannot be used together")),
|
||||
E("br"),
|
||||
_(
|
||||
"Warning: %s cannot be used together with %s. Previous selections have been removed.",
|
||||
).format(removedRegions.join(", "), lastSelected),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (newValues.includes("russia_inside")) {
|
||||
const removedServices = newValues.filter(
|
||||
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
|
||||
);
|
||||
if (removedServices.length > 0) {
|
||||
newValues = newValues.filter((v) =>
|
||||
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
|
||||
);
|
||||
notifications.push(
|
||||
E("p", { class: "alert-message warning" }, [
|
||||
E("strong", {}, _("Russia inside restrictions")),
|
||||
E("br"),
|
||||
_(
|
||||
"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.",
|
||||
).format(
|
||||
main.ALLOWED_WITH_RUSSIA_INSIDE.map(
|
||||
(key) => main.DOMAIN_LIST_OPTIONS[key],
|
||||
)
|
||||
.filter((label) => label !== "Russia inside")
|
||||
.join(", "),
|
||||
removedServices.join(", "),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) {
|
||||
this.getUIElement(section_id).setValue(newValues);
|
||||
}
|
||||
|
||||
notifications.forEach((notification) =>
|
||||
ui.addNotification(null, notification),
|
||||
);
|
||||
lastValues = newValues;
|
||||
} catch (e) {
|
||||
console.error("Error in onchange handler:", e);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"user_domain_list_type",
|
||||
_("User Domain List Type"),
|
||||
_("Select the list type for adding custom domains"),
|
||||
);
|
||||
o.value("disabled", _("Disabled"));
|
||||
o.value("dynamic", _("Dynamic List"));
|
||||
o.value("text", _("Text List"));
|
||||
o.default = "disabled";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"user_domains",
|
||||
_("User Domains"),
|
||||
_(
|
||||
"Enter domain names without protocols, e.g. example.com or sub.example.com",
|
||||
),
|
||||
);
|
||||
o.placeholder = "Domains list";
|
||||
o.depends("user_domain_list_type", "dynamic");
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateDomain(value, true);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.TextValue,
|
||||
"user_domains_text",
|
||||
_("User Domains List"),
|
||||
_(
|
||||
"Enter domain names separated by commas, spaces, or newlines. You can add comments using //",
|
||||
),
|
||||
);
|
||||
o.placeholder =
|
||||
"example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains";
|
||||
o.depends("user_domain_list_type", "text");
|
||||
o.rows = 8;
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const domains = main.parseValueList(value);
|
||||
|
||||
if (!domains.length) {
|
||||
return _(
|
||||
"At least one valid domain must be specified. Comments-only content is not allowed.",
|
||||
);
|
||||
}
|
||||
|
||||
const { valid, results } = main.bulkValidate(domains, (row) =>
|
||||
main.validateDomain(row, true),
|
||||
);
|
||||
|
||||
if (!valid) {
|
||||
const errors = results
|
||||
.filter((validation) => !validation.valid) // Leave only failed validations
|
||||
.map((validation) => `${validation.value}: ${validation.message}`); // Collect validation errors
|
||||
|
||||
return [_("Validation errors:"), ...errors].join("\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"user_subnet_list_type",
|
||||
_("User Subnet List Type"),
|
||||
_("Select the list type for adding custom subnets"),
|
||||
);
|
||||
o.value("disabled", _("Disabled"));
|
||||
o.value("dynamic", _("Dynamic List"));
|
||||
o.value("text", _("Text List"));
|
||||
o.default = "disabled";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"user_subnets",
|
||||
_("User Subnets"),
|
||||
_(
|
||||
"Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses",
|
||||
),
|
||||
);
|
||||
o.placeholder = "IP or subnet";
|
||||
o.depends("user_subnet_list_type", "dynamic");
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateSubnet(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.TextValue,
|
||||
"user_subnets_text",
|
||||
_("User Subnets List"),
|
||||
_(
|
||||
"Enter subnets in CIDR notation or single IP addresses, separated by commas, spaces, or newlines. " +
|
||||
"You can add comments using //",
|
||||
),
|
||||
);
|
||||
o.placeholder =
|
||||
"103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9";
|
||||
o.depends("user_subnet_list_type", "text");
|
||||
o.rows = 10;
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const subnets = main.parseValueList(value);
|
||||
|
||||
if (!subnets.length) {
|
||||
return _(
|
||||
"At least one valid subnet or IP must be specified. Comments-only content is not allowed.",
|
||||
);
|
||||
}
|
||||
|
||||
const { valid, results } = main.bulkValidate(subnets, main.validateSubnet);
|
||||
|
||||
if (!valid) {
|
||||
const errors = results
|
||||
.filter((validation) => !validation.valid) // Leave only failed validations
|
||||
.map((validation) => `${validation.value}: ${validation.message}`); // Collect validation errors
|
||||
|
||||
return [_("Validation errors:"), ...errors].join("\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"local_domain_lists",
|
||||
_("Local Domain Lists"),
|
||||
_("Specify the path to the list file located on the router filesystem"),
|
||||
);
|
||||
o.placeholder = "/path/file.lst";
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validatePath(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"local_subnet_lists",
|
||||
_("Local Subnet Lists"),
|
||||
_("Specify the path to the list file located on the router filesystem"),
|
||||
);
|
||||
o.placeholder = "/path/file.lst";
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validatePath(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"remote_domain_lists",
|
||||
_("Remote Domain Lists"),
|
||||
_("Specify remote URLs to download and use domain lists"),
|
||||
);
|
||||
o.placeholder = "https://example.com/domains.srs";
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateUrl(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"remote_subnet_lists",
|
||||
_("Remote Subnet Lists"),
|
||||
_("Specify remote URLs to download and use subnet lists"),
|
||||
);
|
||||
o.placeholder = "https://example.com/subnets.srs";
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateUrl(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"fully_routed_ips",
|
||||
_("Fully Routed IPs"),
|
||||
_(
|
||||
"Specify local IP addresses or subnets whose traffic will always be routed through the configured route",
|
||||
),
|
||||
);
|
||||
o.placeholder = "192.168.1.2 or 192.168.1.0/24";
|
||||
o.rmempty = true;
|
||||
o.depends("connection_type", "proxy");
|
||||
o.depends("connection_type", "vpn");
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateSubnet(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"mixed_proxy_enabled",
|
||||
_("Enable Mixed Proxy"),
|
||||
_(
|
||||
"Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies",
|
||||
),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
o.depends("connection_type", "proxy");
|
||||
o.depends("connection_type", "vpn");
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"mixed_proxy_port",
|
||||
_("Mixed Proxy Port"),
|
||||
_(
|
||||
"Specify the port number on which the mixed proxy will run for this section. " +
|
||||
"Make sure the selected port is not used by another service",
|
||||
),
|
||||
);
|
||||
o.rmempty = false;
|
||||
o.depends("mixed_proxy_enabled", "1");
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"resolve_real_ip_for_routing",
|
||||
_("Resolve real IP for routing"),
|
||||
_("Enable DNS resolve to get real IP when routing"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
o.depends("connection_type", "proxy");
|
||||
o.depends("connection_type", "vpn");
|
||||
}
|
||||
|
||||
const EntryPoint = {
|
||||
createSectionContent,
|
||||
};
|
||||
|
||||
return baseclass.extend(EntryPoint);
|
||||
@@ -0,0 +1,438 @@
|
||||
"use strict";
|
||||
"require form";
|
||||
"require uci";
|
||||
"require baseclass";
|
||||
"require tools.widgets as widgets";
|
||||
"require view.podkop.main as main";
|
||||
|
||||
function createSettingsContent(section) {
|
||||
let o = section.option(
|
||||
form.ListValue,
|
||||
"dns_type",
|
||||
_("DNS Protocol Type"),
|
||||
_("Select DNS protocol to use"),
|
||||
);
|
||||
o.value("doh", _("DNS over HTTPS (DoH)"));
|
||||
o.value("dot", _("DNS over TLS (DoT)"));
|
||||
o.value("udp", _("UDP (Unprotected DNS)"));
|
||||
o.default = "udp";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"dns_server",
|
||||
_("DNS Server"),
|
||||
_("Select or enter DNS server address"),
|
||||
);
|
||||
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
||||
o.value(key, _(label));
|
||||
});
|
||||
o.default = "8.8.8.8";
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
const validation = main.validateDNS(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"bootstrap_dns_server",
|
||||
_("Bootstrap DNS server"),
|
||||
_(
|
||||
"The DNS server used to look up the IP address of an upstream DNS server",
|
||||
),
|
||||
);
|
||||
Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
||||
o.value(key, _(label));
|
||||
});
|
||||
o.default = "77.88.8.8";
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
const validation = main.validateDNS(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"dns_rewrite_ttl",
|
||||
_("DNS Rewrite TTL"),
|
||||
_("Time in seconds for DNS record caching (default: 60)"),
|
||||
);
|
||||
o.default = "60";
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _("TTL value cannot be empty");
|
||||
}
|
||||
|
||||
const ttl = parseInt(value);
|
||||
if (isNaN(ttl) || ttl < 0) {
|
||||
return _("TTL must be a positive number");
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
widgets.DeviceSelect,
|
||||
"source_network_interfaces",
|
||||
_("Source Network Interface"),
|
||||
_("Select the network interface from which the traffic will originate"),
|
||||
);
|
||||
o.default = "br-lan";
|
||||
o.noaliases = true;
|
||||
o.nobridges = false;
|
||||
o.noinactive = false;
|
||||
o.multiple = true;
|
||||
o.filter = function (section_id, value) {
|
||||
// Block specific interface names from being selectable
|
||||
const blocked = ["wan", "phy0-ap0", "phy1-ap0", "pppoe-wan"];
|
||||
if (blocked.includes(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find the device object by its name
|
||||
const device = this.devices.find((dev) => dev.getName() === value);
|
||||
|
||||
// If no device is found, allow the value
|
||||
if (!device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the type of the device
|
||||
const type = device.getType();
|
||||
|
||||
// Consider any Wi-Fi / wireless / wlan device as invalid
|
||||
const isWireless =
|
||||
type === "wifi" || type === "wireless" || type.includes("wlan");
|
||||
|
||||
// Allow only non-wireless devices
|
||||
return !isWireless;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"enable_output_network_interface",
|
||||
_("Enable Output Network Interface"),
|
||||
_("You can select Output Network Interface, by default autodetect"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
widgets.DeviceSelect,
|
||||
"output_network_interface",
|
||||
_("Output Network Interface"),
|
||||
_("Select the network interface to which the traffic will originate"),
|
||||
);
|
||||
o.noaliases = true;
|
||||
o.multiple = false;
|
||||
o.depends("enable_output_network_interface", "1");
|
||||
o.filter = function (section_id, value) {
|
||||
// Blocked interface names that should never be selectable
|
||||
const blockedInterfaces = ["br-lan"];
|
||||
|
||||
// Reject immediately if the value matches any blocked interface
|
||||
if (blockedInterfaces.includes(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject lan*
|
||||
if (
|
||||
value.startsWith("lan")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject tun*, wg*, vpn*, awg*, oc*
|
||||
if (
|
||||
value.startsWith("tun") ||
|
||||
value.startsWith("wg") ||
|
||||
value.startsWith("vpn") ||
|
||||
value.startsWith("awg") ||
|
||||
value.startsWith("oc")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find the device object with the given name
|
||||
const device = this.devices.find((dev) => dev.getName() === value);
|
||||
|
||||
// If no device is found, allow the value
|
||||
if (!device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the device type (e.g., "wifi", "ethernet", etc.)
|
||||
const type = device.getType();
|
||||
|
||||
// Reject wireless-related devices
|
||||
const isWireless =
|
||||
type === "wifi" || type === "wireless" || type.includes("wlan");
|
||||
|
||||
return !isWireless;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"enable_badwan_interface_monitoring",
|
||||
_("Interface Monitoring"),
|
||||
_("Interface monitoring for Bad WAN"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
widgets.NetworkSelect,
|
||||
"badwan_monitored_interfaces",
|
||||
_("Monitored Interfaces"),
|
||||
_("Select the WAN interfaces to be monitored"),
|
||||
);
|
||||
o.depends("enable_badwan_interface_monitoring", "1");
|
||||
o.multiple = true;
|
||||
o.filter = function (section_id, value) {
|
||||
// Reject if the value is in the blocked list ['lan', 'loopback']
|
||||
if (["lan", "loopback"].includes(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if the value starts with '@' (means it's an alias/reference)
|
||||
if (value.startsWith("@")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise allow it
|
||||
return true;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"badwan_reload_delay",
|
||||
_("Interface Monitoring Delay"),
|
||||
_("Delay in milliseconds before reloading podkop after interface UP"),
|
||||
);
|
||||
o.depends("enable_badwan_interface_monitoring", "1");
|
||||
o.default = "2000";
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _("Delay value cannot be empty");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"enable_yacd",
|
||||
_("Enable YACD"),
|
||||
`<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`,
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"enable_yacd_wan_access",
|
||||
_("Enable YACD WAN Access"),
|
||||
_("Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."),
|
||||
);
|
||||
o.depends("enable_yacd", "1");
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"yacd_secret_key",
|
||||
_("YACD Secret Key"),
|
||||
_("Secret key for authenticating remote access to YACD when WAN access is enabled."),
|
||||
);
|
||||
o.depends("enable_yacd_wan_access", "1");
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"disable_quic",
|
||||
_("Disable QUIC"),
|
||||
_(
|
||||
"Disable the QUIC protocol to improve compatibility or fix issues with video streaming",
|
||||
),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"update_interval",
|
||||
_("List Update Frequency"),
|
||||
_("Select how often the domain or subnet lists are updated automatically"),
|
||||
);
|
||||
Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => {
|
||||
o.value(key, _(label));
|
||||
});
|
||||
o.default = "1d";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"download_lists_via_proxy",
|
||||
_("Download Lists via Proxy/VPN"),
|
||||
_("Downloading all lists via specific Proxy/VPN"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"download_lists_via_proxy_section",
|
||||
_("Download Lists via specific proxy section"),
|
||||
_("Downloading all lists via specific Proxy/VPN"),
|
||||
);
|
||||
|
||||
o.rmempty = false;
|
||||
o.depends("download_lists_via_proxy", "1");
|
||||
o.cfgvalue = function (section_id) {
|
||||
return uci.get("podkop", section_id, "download_lists_via_proxy_section");
|
||||
};
|
||||
o.load = function () {
|
||||
const sections = this.map?.data?.state?.values?.podkop ?? {};
|
||||
|
||||
this.keylist = [];
|
||||
this.vallist = [];
|
||||
|
||||
for (const secName in sections) {
|
||||
const sec = sections[secName];
|
||||
if (sec[".type"] === "section" && sec['connection_type'] !== 'block' && sec['connection_type'] !== 'exclusion') {
|
||||
this.keylist.push(secName);
|
||||
this.vallist.push(secName);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"dont_touch_dhcp",
|
||||
_("Dont Touch My DHCP!"),
|
||||
_("Podkop will not modify your DHCP configuration"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"config_path",
|
||||
_("Config File Path"),
|
||||
_(
|
||||
"Select path for sing-box config file. Change this ONLY if you know what you are doing",
|
||||
),
|
||||
);
|
||||
o.value("/etc/sing-box/config.json", "Flash (/etc/sing-box/config.json)");
|
||||
o.value("/tmp/sing-box/config.json", "RAM (/tmp/sing-box/config.json)");
|
||||
o.default = "/etc/sing-box/config.json";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"cache_path",
|
||||
_("Cache File Path"),
|
||||
_(
|
||||
"Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing",
|
||||
),
|
||||
);
|
||||
o.value("/tmp/sing-box/cache.db", "RAM (/tmp/sing-box/cache.db)");
|
||||
o.value(
|
||||
"/usr/share/sing-box/cache.db",
|
||||
"Flash (/usr/share/sing-box/cache.db)",
|
||||
);
|
||||
o.default = "/tmp/sing-box/cache.db";
|
||||
o.rmempty = false;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _("Cache file path cannot be empty");
|
||||
}
|
||||
|
||||
if (!value.startsWith("/")) {
|
||||
return _("Path must be absolute (start with /)");
|
||||
}
|
||||
|
||||
if (!value.endsWith("cache.db")) {
|
||||
return _("Path must end with cache.db");
|
||||
}
|
||||
|
||||
const parts = value.split("/").filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
return _("Path must contain at least one directory (like /tmp/cache.db)");
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.ListValue,
|
||||
"log_level",
|
||||
_("Log Level"),
|
||||
_(
|
||||
"Select the log level for sing-box",
|
||||
),
|
||||
);
|
||||
o.value("trace", "Trace");
|
||||
o.value("debug", "Debug");
|
||||
o.value("info", "Info");
|
||||
o.value("warn", "Warn");
|
||||
o.value("error", "Error");
|
||||
o.value("fatal", "Fatal");
|
||||
o.value("panic", "Panic");
|
||||
o.default = "warn";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"exclude_ntp",
|
||||
_("Exclude NTP"),
|
||||
_(
|
||||
"Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN",
|
||||
),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"routing_excluded_ips",
|
||||
_("Routing Excluded IPs"),
|
||||
_("Specify a local IP address to be excluded from routing"),
|
||||
);
|
||||
o.placeholder = "IP";
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateIPV4(value);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return validation.message;
|
||||
};
|
||||
}
|
||||
|
||||
const EntryPoint = {
|
||||
createSettingsContent,
|
||||
};
|
||||
|
||||
return baseclass.extend(EntryPoint);
|
||||
30
src/frontend/sandbox/msgmerge.sh
Normal file
30
src/frontend/sandbox/msgmerge.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PODIR="po"
|
||||
POTFILE="$PODIR/templates/podkop.pot"
|
||||
WIDTH=120
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <language_code> (e.g., ru, de, fr)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LANG="$1"
|
||||
POFILE="$PODIR/$LANG/podkop.po"
|
||||
|
||||
if [ ! -f "$POTFILE" ]; then
|
||||
echo "Template $POTFILE not found. Run xgettext first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$POFILE" ]; then
|
||||
echo "Updating $POFILE"
|
||||
msgmerge --update --width="$WIDTH" --no-location "$POFILE" "$POTFILE"
|
||||
else
|
||||
echo "Creating new $POFILE using msginit"
|
||||
mkdir -p "$PODIR/$LANG"
|
||||
msginit --no-translator --no-location --locale="$LANG" --width="$WIDTH" --input="$POTFILE" --output-file="$POFILE"
|
||||
fi
|
||||
|
||||
echo "Translation file for $LANG updated."
|
||||
795
src/frontend/sandbox/po/ru/podkop.po
Normal file
795
src/frontend/sandbox/po/ru/podkop.po
Normal file
@@ -0,0 +1,795 @@
|
||||
# RU translations for PODKOP package.
|
||||
# Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PODKOP package.
|
||||
# divocatt, 2026.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-29 16:40+0300\n"
|
||||
"PO-Revision-Date: 2026-05-29 16:40+0300\n"
|
||||
"Last-Translator: divocatt\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
msgid "✔ Enabled"
|
||||
msgstr "✔ Включено"
|
||||
|
||||
msgid "✔ Running"
|
||||
msgstr "✔ Работает"
|
||||
|
||||
msgid "✘ Disabled"
|
||||
msgstr "✘ Отключено"
|
||||
|
||||
msgid "✘ Stopped"
|
||||
msgstr "✘ Остановлен"
|
||||
|
||||
msgid "Active Connections"
|
||||
msgstr "Активные соединения"
|
||||
|
||||
msgid "Additional marking rules found"
|
||||
msgstr "Найдены дополнительные правила маркировки"
|
||||
|
||||
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
|
||||
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
|
||||
|
||||
msgid "Applicable for SOCKS and Shadowsocks proxy"
|
||||
msgstr "Применимо для SOCKS и Shadowsocks прокси"
|
||||
|
||||
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||
|
||||
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
|
||||
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
|
||||
|
||||
msgid "Available actions"
|
||||
msgstr "Доступные действия"
|
||||
|
||||
msgid "Bootsrap DNS"
|
||||
msgstr "Bootstrap DNS"
|
||||
|
||||
msgid "Bootstrap DNS server"
|
||||
msgstr "Bootstrap DNS-сервер"
|
||||
|
||||
msgid "Browser is not using FakeIP"
|
||||
msgstr "Браузер не использует FakeIP"
|
||||
|
||||
msgid "Browser is using FakeIP correctly"
|
||||
msgstr "Браузер использует FakeIP"
|
||||
|
||||
msgid "Cache File Path"
|
||||
msgstr "Путь к файлу кэша"
|
||||
|
||||
msgid "Cache file path cannot be empty"
|
||||
msgstr "Путь к файлу кэша не может быть пустым"
|
||||
|
||||
msgid "Cannot receive checks result"
|
||||
msgstr "Не удалось получить результаты проверки"
|
||||
|
||||
msgid "Checking, please wait"
|
||||
msgstr "Проверяем, пожалуйста подождите"
|
||||
|
||||
msgid "checks"
|
||||
msgstr "проверки"
|
||||
|
||||
msgid "Checks failed"
|
||||
msgstr "Проверки не выполнены"
|
||||
|
||||
msgid "Checks passed"
|
||||
msgstr "Проверки пройдены"
|
||||
|
||||
msgid "CIDR must be between 0 and 32"
|
||||
msgstr "CIDR должен быть между 0 и 32"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "Закрыть"
|
||||
|
||||
msgid "Community Lists"
|
||||
msgstr "Списки сообщества"
|
||||
|
||||
msgid "Config File Path"
|
||||
msgstr "Путь к файлу конфигурации"
|
||||
|
||||
msgid "Configuration for Podkop service"
|
||||
msgstr "Настройки сервиса Podkop"
|
||||
|
||||
msgid "Configuration Type"
|
||||
msgstr "Тип конфигурации"
|
||||
|
||||
msgid "Connection Type"
|
||||
msgstr "Тип подключения"
|
||||
|
||||
msgid "Connection URL"
|
||||
msgstr "URL подключения"
|
||||
|
||||
msgid "Copy"
|
||||
msgstr "Копировать"
|
||||
|
||||
msgid "Currently unavailable"
|
||||
msgstr "Временно недоступно"
|
||||
|
||||
msgid "Dashboard"
|
||||
msgstr "Дашборд"
|
||||
|
||||
msgid "Dashboard currently unavailable"
|
||||
msgstr "Дашборд сейчас недоступен"
|
||||
|
||||
msgid "Delay in milliseconds before reloading podkop after interface UP"
|
||||
msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса"
|
||||
|
||||
msgid "Delay value cannot be empty"
|
||||
msgstr "Значение задержки не может быть пустым"
|
||||
|
||||
msgid "DHCP has DNS server"
|
||||
msgstr "DHCP содержит DNS сервер"
|
||||
|
||||
msgid "Diagnostics"
|
||||
msgstr "Диагностика"
|
||||
|
||||
msgid "Disable autostart"
|
||||
msgstr "Отключить автостарт"
|
||||
|
||||
msgid "Disable QUIC"
|
||||
msgstr "Отключить QUIC"
|
||||
|
||||
msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming"
|
||||
msgstr "Отключить QUIC протокол для улучшения совместимости или исправления видео стриминга"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Отключено"
|
||||
|
||||
msgid "DNS on router"
|
||||
msgstr "DNS на роутере"
|
||||
|
||||
msgid "DNS over HTTPS (DoH)"
|
||||
msgstr "DNS через HTTPS (DoH)"
|
||||
|
||||
msgid "DNS over TLS (DoT)"
|
||||
msgstr "DNS через TLS (DoT)"
|
||||
|
||||
msgid "DNS Protocol Type"
|
||||
msgstr "Тип протокола DNS"
|
||||
|
||||
msgid "DNS Rewrite TTL"
|
||||
msgstr "Перезапись TTL для DNS"
|
||||
|
||||
msgid "DNS Server"
|
||||
msgstr "DNS-сервер"
|
||||
|
||||
msgid "DNS server address cannot be empty"
|
||||
msgstr "Адрес DNS-сервера не может быть пустым"
|
||||
|
||||
msgid "Do not panic, everything can be fixed, just..."
|
||||
msgstr "Не паникуйте, всё можно исправить, просто..."
|
||||
|
||||
msgid "Domain Resolver"
|
||||
msgstr "Резолвер доменов"
|
||||
|
||||
msgid "Dont Touch My DHCP!"
|
||||
msgstr "Dont Touch My DHCP!"
|
||||
|
||||
msgid "Downlink"
|
||||
msgstr "Входящий"
|
||||
|
||||
msgid "Download"
|
||||
msgstr "Скачать"
|
||||
|
||||
msgid "Download Lists via Proxy/VPN"
|
||||
msgstr "Скачивать списки через Proxy/VPN"
|
||||
|
||||
msgid "Download Lists via specific proxy section"
|
||||
msgstr "Скачивать списки через выбранную секцию"
|
||||
|
||||
msgid "Downloading all lists via specific Proxy/VPN"
|
||||
msgstr "Загрузка всех списков через указанный прокси/VPN"
|
||||
|
||||
msgid "Dynamic List"
|
||||
msgstr "Динамический список"
|
||||
|
||||
msgid "Enable autostart"
|
||||
msgstr "Включить автостарт"
|
||||
|
||||
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
|
||||
|
||||
msgid "Enable DNS resolve to get real IP when routing"
|
||||
msgstr "Разрешать домены в реальные IP-адреса перед маршрутизацией в outbound"
|
||||
|
||||
msgid "Enable Mixed Proxy"
|
||||
msgstr "Включить смешанный прокси"
|
||||
|
||||
msgid "Enable Output Network Interface"
|
||||
msgstr "Включить выходной сетевой интерфейс"
|
||||
|
||||
msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies"
|
||||
msgstr "Включить смешанный прокси-сервер, разрешив этому разделу маршрутизировать трафик как через HTTP, так и через SOCKS-прокси."
|
||||
|
||||
msgid "Enable YACD"
|
||||
msgstr "Включить YACD"
|
||||
|
||||
msgid "Enable YACD WAN Access"
|
||||
msgstr "Включить доступ YACD WAN"
|
||||
|
||||
msgid "Enter complete outbound configuration in JSON format"
|
||||
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
|
||||
|
||||
msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //"
|
||||
msgstr "Введите доменные имена, разделяя их запятыми, пробелами или переносами строк. Вы можете добавлять комментарии, используя //"
|
||||
|
||||
msgid "Enter domain names without protocols, e.g. example.com or sub.example.com"
|
||||
msgstr "Введите доменные имена без протоколов, например example.com или sub.example.com"
|
||||
|
||||
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
|
||||
msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса"
|
||||
|
||||
msgid "Every 1 minute"
|
||||
msgstr "Каждую минуту"
|
||||
|
||||
msgid "Every 3 minutes"
|
||||
msgstr "Каждые 3 минуты"
|
||||
|
||||
msgid "Every 30 seconds"
|
||||
msgstr "Каждые 30 секунд"
|
||||
|
||||
msgid "Every 5 minutes"
|
||||
msgstr "Каждые 5 минут"
|
||||
|
||||
msgid "Exclude NTP"
|
||||
msgstr "Исключить NTP"
|
||||
|
||||
msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN"
|
||||
msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN."
|
||||
|
||||
msgid "Failed to copy!"
|
||||
msgstr "Не удалось скопировать!"
|
||||
|
||||
msgid "Failed to execute!"
|
||||
msgstr "Не удалось выполнить!"
|
||||
|
||||
msgid "Fastest"
|
||||
msgstr "Самый быстрый"
|
||||
|
||||
msgid "Fully Routed IPs"
|
||||
msgstr "Полностью маршрутизированные IP-адреса"
|
||||
|
||||
msgid "Get global check"
|
||||
msgstr "Получить глобальную проверку"
|
||||
|
||||
msgid "Global check"
|
||||
msgstr "Глобальная проверка"
|
||||
|
||||
msgid "HTTP error"
|
||||
msgstr "Ошибка HTTP"
|
||||
|
||||
msgid "Interface Monitoring"
|
||||
msgstr "Мониторинг интерфейса"
|
||||
|
||||
msgid "Interface Monitoring Delay"
|
||||
msgstr "Задержка при мониторинге интерфейсов"
|
||||
|
||||
msgid "Interface monitoring for Bad WAN"
|
||||
msgstr "Мониторинг интерфейса для Bad WAN"
|
||||
|
||||
msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
|
||||
msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH"
|
||||
|
||||
msgid "Invalid domain address"
|
||||
msgstr "Неверный домен"
|
||||
|
||||
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
|
||||
msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y"
|
||||
|
||||
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
|
||||
|
||||
msgid "Invalid HY2 URL: invalid port number"
|
||||
msgstr "Неверный URL Hysteria2: неверный номер порта"
|
||||
|
||||
msgid "Invalid HY2 URL: missing credentials/server"
|
||||
msgstr "Неверный URL Hysteria2: отсутствуют учетные данные/сервер"
|
||||
|
||||
msgid "Invalid HY2 URL: missing host"
|
||||
msgstr "Неверный URL Hysteria2: отсутствует хост"
|
||||
|
||||
msgid "Invalid HY2 URL: missing host & port"
|
||||
msgstr "Неверный URL Hysteria2: отсутствуют хост и порт"
|
||||
|
||||
msgid "Invalid HY2 URL: missing password"
|
||||
msgstr "Неверный URL Hysteria2: отсутствует пароль"
|
||||
|
||||
msgid "Invalid HY2 URL: missing port"
|
||||
msgstr "Неверный URL Hysteria2: отсутствует порт"
|
||||
|
||||
msgid "Invalid HY2 URL: must not contain spaces"
|
||||
msgstr "Неверный URL Hysteria2: не должен содержать пробелов"
|
||||
|
||||
msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://"
|
||||
msgstr "Неверный URL Hysteria2: должен начинаться с hysteria2:// или hy2://"
|
||||
|
||||
msgid "Invalid HY2 URL: obfs-password required when obfs is set"
|
||||
msgstr "Неверный URL Hysteria2: требуется obfs-password, когда установлен obfs"
|
||||
|
||||
msgid "Invalid HY2 URL: parsing failed"
|
||||
msgstr "Неверный URL Hysteria2: ошибка разбора"
|
||||
|
||||
msgid "Invalid HY2 URL: sni cannot be empty"
|
||||
msgstr "Неверный URL Hysteria2: sni не может быть пустым"
|
||||
|
||||
msgid "Invalid HY2 URL: unsupported obfs type"
|
||||
msgstr "Неверный URL Hysteria2: неподдерживаемый тип obfs"
|
||||
|
||||
msgid "Invalid IP address"
|
||||
msgstr "Неверный IP-адрес"
|
||||
|
||||
msgid "Invalid JSON format"
|
||||
msgstr "Неверный формат JSON"
|
||||
|
||||
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
|
||||
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
|
||||
|
||||
msgid "Invalid port number. Must be between 1 and 65535"
|
||||
msgstr "Неверный номер порта. Допустимо от 1 до 65535"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password"
|
||||
msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing credentials"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing port"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует порт"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing server"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует сервер"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: missing server address"
|
||||
msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: must not contain spaces"
|
||||
msgstr "Неверный URL Shadowsocks: не должен содержать пробелов"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: must start with ss://"
|
||||
msgstr "Неверный URL Shadowsocks: должен начинаться с ss://"
|
||||
|
||||
msgid "Invalid Shadowsocks URL: parsing failed"
|
||||
msgstr "Неверный URL Shadowsocks: ошибка разбора"
|
||||
|
||||
msgid "Invalid SOCKS URL: invalid host format"
|
||||
msgstr "Неверный URL SOCKS: неверный формат хоста"
|
||||
|
||||
msgid "Invalid SOCKS URL: invalid port number"
|
||||
msgstr "Неверный URL SOCKS: неверный номер порта"
|
||||
|
||||
msgid "Invalid SOCKS URL: missing host and port"
|
||||
msgstr "Неверный URL SOCKS: отсутствует хост и порт"
|
||||
|
||||
msgid "Invalid SOCKS URL: missing hostname or IP"
|
||||
msgstr "Неверный URL SOCKS: отсутствует имя хоста или IP-адрес"
|
||||
|
||||
msgid "Invalid SOCKS URL: missing port"
|
||||
msgstr "Неверный URL SOCKS: отсутствует порт"
|
||||
|
||||
msgid "Invalid SOCKS URL: missing username"
|
||||
msgstr "Неверный URL SOCKS: отсутствует имя пользователя"
|
||||
|
||||
msgid "Invalid SOCKS URL: must not contain spaces"
|
||||
msgstr "Неверный URL SOCKS: не должен содержать пробелов"
|
||||
|
||||
msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://"
|
||||
msgstr "Неверный URL-адрес SOCKS: должен начинаться с socks4://, socks4a:// или socks5://"
|
||||
|
||||
msgid "Invalid SOCKS URL: parsing failed"
|
||||
msgstr "Неверный URL SOCKS: парсинг не удался"
|
||||
|
||||
msgid "Invalid Trojan URL: must not contain spaces"
|
||||
msgstr "Неверный URL Trojan: не должен содержать пробелов"
|
||||
|
||||
msgid "Invalid Trojan URL: must start with trojan://"
|
||||
msgstr "Неверный URL Trojan: должен начинаться с trojan://"
|
||||
|
||||
msgid "Invalid Trojan URL: parsing failed"
|
||||
msgstr "Неверный URL Trojan: ошибка разбора"
|
||||
|
||||
msgid "Invalid URL format"
|
||||
msgstr "Неверный формат URL"
|
||||
|
||||
msgid "Invalid VLESS URL: parsing failed"
|
||||
msgstr "Неверный URL VLESS: ошибка разбора"
|
||||
|
||||
msgid "IP address 0.0.0.0 is not allowed"
|
||||
msgstr "IP-адрес 0.0.0.0 не допускается"
|
||||
|
||||
msgid "Issues detected"
|
||||
msgstr "Обнаружены проблемы"
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "Последняя"
|
||||
|
||||
msgid "List Update Frequency"
|
||||
msgstr "Частота обновления списков"
|
||||
|
||||
msgid "Local Domain Lists"
|
||||
msgstr "Локальные списки доменов"
|
||||
|
||||
msgid "Local Subnet Lists"
|
||||
msgstr "Локальные списки подсетей"
|
||||
|
||||
msgid "Log Level"
|
||||
msgstr "Уровень логов"
|
||||
|
||||
msgid "Main DNS"
|
||||
msgstr "Основной DNS"
|
||||
|
||||
msgid "Memory Usage"
|
||||
msgstr "Использование памяти"
|
||||
|
||||
msgid "Mixed Proxy Port"
|
||||
msgstr "Порт смешанного прокси"
|
||||
|
||||
msgid "Monitored Interfaces"
|
||||
msgstr "Наблюдаемые интерфейсы"
|
||||
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr "Должно быть числом от 50 до 1000"
|
||||
|
||||
msgid "Network Interface"
|
||||
msgstr "Сетевой интерфейс"
|
||||
|
||||
msgid "No other marking rules found"
|
||||
msgstr "Другие правила маркировки не найдены"
|
||||
|
||||
msgid "Not implement yet"
|
||||
msgstr "Ещё не реализовано"
|
||||
|
||||
msgid "Not responding"
|
||||
msgstr "Не отвечает"
|
||||
|
||||
msgid "Not running"
|
||||
msgstr "Не запущено"
|
||||
|
||||
msgid "Operation timed out"
|
||||
msgstr "Время ожидания истекло"
|
||||
|
||||
msgid "Outbound Config"
|
||||
msgstr "Конфигурация Outbound"
|
||||
|
||||
msgid "Outbound Configuration"
|
||||
msgstr "Конфигурация исходящего соединения"
|
||||
|
||||
msgid "Outdated"
|
||||
msgstr "Устаревшая"
|
||||
|
||||
msgid "Output Network Interface"
|
||||
msgstr "Выходной сетевой интерфейс"
|
||||
|
||||
msgid "Path cannot be empty"
|
||||
msgstr "Путь не может быть пустым"
|
||||
|
||||
msgid "Path must be absolute (start with /)"
|
||||
msgstr "Путь должен быть абсолютным (начинаться с /)"
|
||||
|
||||
msgid "Path must contain at least one directory (like /tmp/cache.db)"
|
||||
msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)"
|
||||
|
||||
msgid "Path must end with cache.db"
|
||||
msgstr "Путь должен заканчиваться на cache.db"
|
||||
|
||||
msgid "Pending"
|
||||
msgstr "Ожидает запуска"
|
||||
|
||||
msgid "Podkop"
|
||||
msgstr "Podkop"
|
||||
|
||||
msgid "Podkop Settings"
|
||||
msgstr "Настройки podkop"
|
||||
|
||||
msgid "Podkop will not modify your DHCP configuration"
|
||||
msgstr "Podkop не будет изменять вашу конфигурацию DHCP."
|
||||
|
||||
msgid "Proxy Configuration URL"
|
||||
msgstr "URL конфигурации прокси"
|
||||
|
||||
msgid "Proxy traffic is not routed via FakeIP"
|
||||
msgstr "Прокси-трафик не маршрутизируется через FakeIP"
|
||||
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr "Прокси-трафик направляется через FakeIP"
|
||||
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||
|
||||
msgid "Remote Domain Lists"
|
||||
msgstr "Внешние списки доменов"
|
||||
|
||||
msgid "Remote Subnet Lists"
|
||||
msgstr "Внешние списки подсетей"
|
||||
|
||||
msgid "Resolve real IP for routing"
|
||||
msgstr "Разрешение реальных IP-адресов"
|
||||
|
||||
msgid "Restart podkop"
|
||||
msgstr "Перезапустить Podkop"
|
||||
|
||||
msgid "Router DNS is not routed through sing-box"
|
||||
msgstr "DNS роутера не проходит через sing-box"
|
||||
|
||||
msgid "Router DNS is routed through sing-box"
|
||||
msgstr "DNS роутера проходит через sing-box"
|
||||
|
||||
msgid "Routing Excluded IPs"
|
||||
msgstr "Исключённые из маршрутизации IP-адреса"
|
||||
|
||||
msgid "Rules mangle counters"
|
||||
msgstr "Счётчики правил mangle"
|
||||
|
||||
msgid "Rules mangle exist"
|
||||
msgstr "Правила mangle существуют"
|
||||
|
||||
msgid "Rules mangle output counters"
|
||||
msgstr "Счётчики правил mangle output"
|
||||
|
||||
msgid "Rules mangle output exist"
|
||||
msgstr "Правила mangle output существуют"
|
||||
|
||||
msgid "Rules proxy counters"
|
||||
msgstr "Счётчики правил proxy"
|
||||
|
||||
msgid "Rules proxy exist"
|
||||
msgstr "Правила прокси существуют"
|
||||
|
||||
msgid "Run Diagnostic"
|
||||
msgstr "Запустить диагностику"
|
||||
|
||||
msgid "Russia inside restrictions"
|
||||
msgstr "Ограничения Russia inside"
|
||||
|
||||
msgid "Secret key for authenticating remote access to YACD when WAN access is enabled."
|
||||
msgstr "Секретный ключ для аутентификации удаленного доступа к YACD при включенном доступе через WAN."
|
||||
|
||||
msgid "Sections"
|
||||
msgstr "Секции"
|
||||
|
||||
msgid "Select a predefined list for routing"
|
||||
msgstr "Выберите предопределенный список для маршрутизации"
|
||||
|
||||
msgid "Select between VPN and Proxy connection methods for traffic routing"
|
||||
msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика"
|
||||
|
||||
msgid "Select DNS protocol to use"
|
||||
msgstr "Выберите протокол DNS"
|
||||
|
||||
msgid "Select how often the domain or subnet lists are updated automatically"
|
||||
msgstr "Выберите частоту автоматического обновления списков доменов или подсетей."
|
||||
|
||||
msgid "Select how to configure the proxy"
|
||||
msgstr "Выберите способ настройки прокси"
|
||||
|
||||
msgid "Select network interface for VPN connection"
|
||||
msgstr "Выберите сетевой интерфейс для VPN подключения"
|
||||
|
||||
msgid "Select or enter DNS server address"
|
||||
msgstr "Выберите или введите адрес DNS-сервера"
|
||||
|
||||
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
|
||||
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
|
||||
|
||||
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
|
||||
msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
|
||||
|
||||
msgid "Select the DNS protocol type for the domain resolver"
|
||||
msgstr "Выберите тип протокола DNS для резолвера доменов"
|
||||
|
||||
msgid "Select the list type for adding custom domains"
|
||||
msgstr "Выберите тип списка для добавления пользовательских доменов"
|
||||
|
||||
msgid "Select the list type for adding custom subnets"
|
||||
msgstr "Выберите тип списка для добавления пользовательских подсетей"
|
||||
|
||||
msgid "Select the log level for sing-box"
|
||||
msgstr "Выберите уровень логов для sing-box"
|
||||
|
||||
msgid "Select the network interface from which the traffic will originate"
|
||||
msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик"
|
||||
|
||||
msgid "Select the network interface to which the traffic will originate"
|
||||
msgstr "Выберите сетевой интерфейс, на который будет поступать трафик."
|
||||
|
||||
msgid "Select the WAN interfaces to be monitored"
|
||||
msgstr "Выберите WAN интерфейсы для мониторинга"
|
||||
|
||||
msgid "Selector"
|
||||
msgstr "Selector"
|
||||
|
||||
msgid "Selector Proxy Links"
|
||||
msgstr "Ссылки прокси для Selector"
|
||||
|
||||
msgid "Services info"
|
||||
msgstr "Информация о сервисах"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgid "Show sing-box config"
|
||||
msgstr "Показать sing-box конфигурацию"
|
||||
|
||||
msgid "Sing-box"
|
||||
msgstr "Sing-box"
|
||||
|
||||
msgid "Sing-box autostart disabled"
|
||||
msgstr "Автостарт sing-box отключен"
|
||||
|
||||
msgid "Sing-box installed"
|
||||
msgstr "Sing-box установлен"
|
||||
|
||||
msgid "Sing-box listening ports"
|
||||
msgstr "Sing-box слушает порты"
|
||||
|
||||
msgid "Sing-box process running"
|
||||
msgstr "Процесс sing-box запущен"
|
||||
|
||||
msgid "Sing-box service exist"
|
||||
msgstr "Сервис sing-box существует"
|
||||
|
||||
msgid "Sing-box version is compatible (newer than 1.12.4)"
|
||||
msgstr "Версия Sing-box совместима (новее 1.12.4)"
|
||||
|
||||
msgid "Source Network Interface"
|
||||
msgstr "Сетевой интерфейс источника"
|
||||
|
||||
msgid "Specify a local IP address to be excluded from routing"
|
||||
msgstr "Укажите локальный IP-адрес, который следует исключить из маршрутизации."
|
||||
|
||||
msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route"
|
||||
msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут."
|
||||
|
||||
msgid "Specify remote URLs to download and use domain lists"
|
||||
msgstr "Укажите URL-адреса для загрузки и использования списков доменов."
|
||||
|
||||
msgid "Specify remote URLs to download and use subnet lists"
|
||||
msgstr "Укажите URL-адреса для загрузки и использования списков подсетей."
|
||||
|
||||
msgid "Specify the path to the list file located on the router filesystem"
|
||||
msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора."
|
||||
|
||||
msgid "Start podkop"
|
||||
msgstr "Запустить podkop"
|
||||
|
||||
msgid "Stop podkop"
|
||||
msgstr "Остановить podkop"
|
||||
|
||||
msgid "Successfully copied!"
|
||||
msgstr "Успешно скопировано!"
|
||||
|
||||
msgid "System info"
|
||||
msgstr "Системная информация"
|
||||
|
||||
msgid "System information"
|
||||
msgstr "Системная информация"
|
||||
|
||||
msgid "Table exist"
|
||||
msgstr "Таблица существует"
|
||||
|
||||
msgid "Test latency"
|
||||
msgstr "Тестирование задержки"
|
||||
|
||||
msgid "Text List"
|
||||
msgstr "Текстовый список"
|
||||
|
||||
msgid "The DNS server used to look up the IP address of an upstream DNS server"
|
||||
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
|
||||
|
||||
msgid "The interval between connectivity tests"
|
||||
msgstr "Интервал между тестами подключения"
|
||||
|
||||
msgid "The maximum difference in response times (ms) allowed when comparing servers"
|
||||
msgstr "Максимально допустимая разница во времени отклика (мс) при сравнении серверов"
|
||||
|
||||
msgid "The URL used to test server connectivity"
|
||||
msgstr "URL-адрес, используемый для проверки подключения к серверу"
|
||||
|
||||
msgid "Time in seconds for DNS record caching (default: 60)"
|
||||
msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)"
|
||||
|
||||
msgid "Traffic"
|
||||
msgstr "Трафик"
|
||||
|
||||
msgid "Traffic Total"
|
||||
msgstr "Всего трафика"
|
||||
|
||||
msgid "Troubleshooting"
|
||||
msgstr "Устранение неполадок"
|
||||
|
||||
msgid "TTL must be a positive number"
|
||||
msgstr "TTL должно быть положительным числом"
|
||||
|
||||
msgid "TTL value cannot be empty"
|
||||
msgstr "Значение TTL не может быть пустым"
|
||||
|
||||
msgid "UDP (Unprotected DNS)"
|
||||
msgstr "UDP (Незащищённый DNS)"
|
||||
|
||||
msgid "UDP over TCP"
|
||||
msgstr "UDP через TCP"
|
||||
|
||||
msgid "unknown"
|
||||
msgstr "неизвестно"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "Неизвестная ошибка"
|
||||
|
||||
msgid "Uplink"
|
||||
msgstr "Исходящий"
|
||||
|
||||
msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://"
|
||||
msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://"
|
||||
|
||||
msgid "URL must use one of the following protocols:"
|
||||
msgstr "URL должен использовать один из следующих протоколов:"
|
||||
|
||||
msgid "URLTest"
|
||||
msgstr "URLTest"
|
||||
|
||||
msgid "URLTest Check Interval"
|
||||
msgstr "Интервал проверки URLTest"
|
||||
|
||||
msgid "URLTest Proxy Links"
|
||||
msgstr "Ссылки прокси для URLTest"
|
||||
|
||||
msgid "URLTest Testing URL"
|
||||
msgstr "URLTest ссылка для проверки"
|
||||
|
||||
msgid "URLTest Tolerance"
|
||||
msgstr "URLTest допустимое отклонение"
|
||||
|
||||
msgid "User Domain List Type"
|
||||
msgstr "Тип пользовательского списка доменов"
|
||||
|
||||
msgid "User Domains"
|
||||
msgstr "Пользовательские домены"
|
||||
|
||||
msgid "User Domains List"
|
||||
msgstr "Список пользовательских доменов"
|
||||
|
||||
msgid "User Subnet List Type"
|
||||
msgstr "Тип пользовательского списка подсетей"
|
||||
|
||||
msgid "User Subnets"
|
||||
msgstr "Пользовательские подсети"
|
||||
|
||||
msgid "User Subnets List"
|
||||
msgstr "Список пользовательских подсетей"
|
||||
|
||||
msgid "Valid"
|
||||
msgstr "Валидно"
|
||||
|
||||
msgid "Validation errors:"
|
||||
msgstr "Ошибки валидации:"
|
||||
|
||||
msgid "View logs"
|
||||
msgstr "Посмотреть логи"
|
||||
|
||||
msgid "Visit Wiki"
|
||||
msgstr "Перейти в wiki"
|
||||
|
||||
msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
|
||||
|
||||
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
|
||||
msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных."
|
||||
|
||||
msgid "YACD Secret Key"
|
||||
msgstr "Секретный ключ YACD"
|
||||
|
||||
msgid "You can select Output Network Interface, by default autodetect"
|
||||
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."
|
||||
1115
src/frontend/sandbox/po/templates/podkop.pot
Normal file
1115
src/frontend/sandbox/po/templates/podkop.pot
Normal file
File diff suppressed because it is too large
Load Diff
10
src/frontend/sandbox/root/etc/uci-defaults/50_luci-podkop
Normal file
10
src/frontend/sandbox/root/etc/uci-defaults/50_luci-podkop
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
rm -f /var/luci-indexcache*
|
||||
rm -f /tmp/luci-indexcache*
|
||||
|
||||
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload
|
||||
|
||||
logger -t "podkop" "$timestamp uci-defaults script executed"
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"admin/services/podkop": {
|
||||
"title": "Podkop",
|
||||
"order": 42,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podkop/podkop"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-podkop" ],
|
||||
"uci": { "podkop": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"luci-app-podkop": {
|
||||
"description": "Grant UCI and RPC access to LuCI app podkop",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/init.d/podkop": [
|
||||
"exec"
|
||||
],
|
||||
"/usr/bin/podkop": [
|
||||
"exec"
|
||||
]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [
|
||||
"list"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"podkop"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"uci": [
|
||||
"podkop"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/frontend/sandbox/xgettext.sh
Normal file
27
src/frontend/sandbox/xgettext.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SRC_DIR="htdocs/luci-static/resources/view/podkop"
|
||||
OUT_POT="po/templates/podkop.pot"
|
||||
ENCODING="UTF-8"
|
||||
WIDTH=120
|
||||
|
||||
mapfile -t FILES < <(find "$SRC_DIR" -type f -name "*.js")
|
||||
if [ ${#FILES[@]} -eq 0 ]; then
|
||||
echo "No JS files found in $SRC_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t FILES < <(printf '%s\n' "${FILES[@]}" | sort)
|
||||
mkdir -p "$(dirname "$OUT_POT")"
|
||||
|
||||
echo "Generating POT template from JS files in $SRC_DIR"
|
||||
xgettext --language=JavaScript \
|
||||
--keyword=_ \
|
||||
--from-code="$ENCODING" \
|
||||
--output="$OUT_POT" \
|
||||
--width="$WIDTH" \
|
||||
--package-name="PODKOP" \
|
||||
"${FILES[@]}"
|
||||
|
||||
echo "POT template generated: $OUT_POT"
|
||||
Reference in New Issue
Block a user