Compare commits

...

24 Commits

Author SHA1 Message Date
divocatt
235368417b feat(fe-sandbox): add sandbox mode for fe development 2026-07-21 00:32:49 +03:00
divocatt
5e9d036eca feat(utils): update src structure 2026-07-20 22:33:18 +03:00
Kirill Sobakin
a64923db5a Merge pull request #403 from itdoginfo/argument-list-too-long
fix: reduce default chunk_size to 1000
2026-07-01 10:28:17 +03:00
Andrey Petelin
0c99dddf11 fix: reduce default chunk_size to 1000 for domain and subnet import functions 2026-07-01 12:00:56 +05:00
Kirill Sobakin
922d6d4638 Merge pull request #395 from itdoginfo/fix/list-update
fix: use service_proxy_address for GitHub proxy connectivity check
2026-06-13 13:33:48 +03:00
Andrey Petelin
34f49c392e fix: use service_proxy_address for GitHub proxy connectivity check 2026-06-13 12:21:59 +05:00
Kirill Sobakin
de7b73af3c Merge pull request #384 from itdoginfo/fix/dnat
fix: skip connections with DNAT status in NFT mangle chain
2026-06-01 12:22:26 +03:00
Andrey Petelin
3c34bd1fd0 fix: skip connections with DNAT status in NFT mangle chain 2026-06-01 14:11:14 +05:00
Kirill Sobakin
847895598d Merge pull request #382 from itdoginfo/fix/hy2
Fixes
2026-05-29 17:41:50 +03:00
divocatt
4146804e5a feat(extra): hide block & exclusion from download_lists_via_proxy_section 2026-05-29 17:03:44 +03:00
divocatt
daef80b7b8 feat(debug): add debug data for download_lists_via_proxy_section 2026-05-29 17:00:02 +03:00
divocatt
6e7b8a6be2 feat(exclusion): hide section with type exclusion 2026-05-29 16:53:47 +03:00
divocatt
1365febaa4 feat(hy2): add port range support for validator 2026-05-29 16:40:44 +03:00
Andrey Petelin
1543d974cc fix: support multiple server ports for hysteria2 outbounds 2026-05-29 17:45:13 +05:00
Andrey Petelin
4b03c28c41 fix: initialize scheme before checking security and clear fingerprint for hysteria2 (hy2) schemes 2026-05-29 15:58:02 +05:00
Kirill Sobakin
c3281773d5 Merge pull request #380 from itdoginfo/docs/trademark
Docs
2026-05-29 10:13:03 +03:00
Andrey Petelin
c9e0b54f2c docs: remove leading Markdown heading from Russian trademark guide 2026-05-29 12:03:43 +05:00
Andrey Petelin
7f33cfcb98 docs: Rewrite README.md to add project intro, warnings, requirements, install & support; remove outdated notes 2026-05-28 20:41:35 +05:00
Andrey Petelin
177469e212 docs: add trademark guidelines in English and Russian (TRADEMARK.md, TRADEMARK_RU.md) 2026-05-28 20:40:23 +05:00
Kirill Sobakin
1802818044 Merge pull request #369 from itdoginfo/copilot/add-invalid-tag-handling
ci: auto-close issues labeled as `Close as invalid`
2026-05-16 06:50:47 +03:00
copilot-swe-agent[bot]
c62cb2de14 fix: use correct label name 'Close as invalid' in workflow
Agent-Logs-Url: https://github.com/itdoginfo/podkop/sessions/db87c50a-cdfd-441f-bf0c-128adfe11f91

Co-authored-by: itdoginfo <11143109+itdoginfo@users.noreply.github.com>
2026-05-16 03:48:02 +00:00
copilot-swe-agent[bot]
f253e4e36f Add workflow to auto-close issues with invalid label
Agent-Logs-Url: https://github.com/itdoginfo/podkop/sessions/455a9a26-bc0f-4f3e-99ea-eff524519e85

Co-authored-by: itdoginfo <11143109+itdoginfo@users.noreply.github.com>
2026-05-16 03:46:01 +00:00
Kirill Sobakin
41e696c9b2 Merge pull request #366 from itdoginfo/discord
fix: include UDP port range 19000-20000 in Discord nft rule
2026-05-14 10:29:52 +03:00
Andrey Petelin
835b455915 fix: include UDP port range 19000-20000 in Discord nft rule 2026-05-14 12:23:50 +05:00
48 changed files with 9320 additions and 161 deletions

View 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'
});

View File

@@ -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]
> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели.
>
> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий.
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop)

63
TRADEMARK.md Normal file
View 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
View 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

View File

@@ -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",

View File

@@ -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

View File

@@ -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"

View File

@@ -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') {

View File

@@ -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;

View File

@@ -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 = [

View File

@@ -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') };

View File

@@ -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") {

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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" \

View File

@@ -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
View File

4
src/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
.vite/
.env

12
src/frontend/package.json Normal file
View 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"
}
}

View 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)))

View 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"]

View 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.

View 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)`);
});

View 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']

View 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'

View 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'

View 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"}'
}

View 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

View 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

View 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 "$@"

View 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"

View 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"

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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."

View 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 "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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 }
}
}
}

View File

@@ -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"
]
}
}
}

View 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"