Compare commits

...

94 Commits

Author SHA1 Message Date
itdoginfo
2aee77b9a2 v0.4.4 Added independent_cache 2025-06-02 15:40:14 +03:00
itdoginfo
2a1a220dc8 v0.4.3 2025-05-22 12:07:08 +03:00
itdoginfo
608caba090 Merge pull request #115 from itdoginfo/fix/comments
♻️ refactor(podkop): update command scheduling and priority handling
2025-05-22 12:01:53 +03:00
Ivan K
04af8c9649 ♻️ refactor(podkop): update command scheduling and priority handling 2025-05-22 11:09:02 +03:00
itdoginfo
88d108e5ab Fix i18n version 2025-05-21 19:43:27 +03:00
itdoginfo
8ce6790355 v0.4.2 2025-05-21 19:18:07 +03:00
itdoginfo
8e7b40cf56 Ready image 2025-05-21 19:17:55 +03:00
itdoginfo
21fa017443 Merge pull request #114 from itdoginfo/fix/comments
Fix/comments
2025-05-21 15:43:58 +03:00
Ivan K
f1954df83b ♻️ refactor(diagnosticTab): move command execution helpers to utils.js 2025-05-21 15:09:42 +03:00
Ivan K
8573bd99b5 ♻️ refactor(diagnosticTab): remove unused debug 2025-05-21 14:21:37 +03:00
Ivan K
c3f44bd124 fix(diagnosticTab): add error polling and notification system 2025-05-21 14:18:23 +03:00
itdoginfo
59e394c4f2 v0.4.1 JS refactoring 2025-05-21 14:16:16 +03:00
itdoginfo
c897c90371 Merge pull request #110 from itdoginfo/fix/comments
♻️ refactor(podkop): modularize configuration and diagnostics sections
2025-05-21 13:48:30 +03:00
Ivan K
bcab66f88c ♻️ refactor(podkop): enhance check_nft function for domain-specific set statistics 2025-05-21 09:48:53 +03:00
Ivan K
05a551e5e3 💄 style(podkop): remove extra newline in NFT check completed message 2025-05-20 19:28:40 +03:00
Ivan K
1f81ec8403 🔧 chore(podkop): use check_nft in global_check 2025-05-20 17:32:04 +03:00
Ivan K
9748178562 ♻️ refactor(podkop): enhance nft set statistics and chain configurations 2025-05-20 17:28:00 +03:00
Ivan K
1411e7d403 ♻️ refactor(diagnosticTab): improve command execution and UI updates 2025-05-19 20:32:08 +03:00
Ivan K
d81a90bd28 ♻️ refactor(diagnosticTab): improve status updates and caching 2025-05-19 19:59:29 +03:00
Ivan K
82f4720326 ♻️ refactor(podkop): rename sections into correct files 2025-05-18 15:58:59 +03:00
Ivan K
10f246ea61 ♻️ refactor(podkop): move URL validation to config.js 2025-05-16 23:30:23 +03:00
Ivan K
c0571320f1 ♻️ refactor(networkUtils): remove custom network functions 2025-05-16 22:22:07 +03:00
Ivan K
a658ca5518 💄 style(podkop): remove unused networkUtils import 2025-05-16 18:40:29 +03:00
Ivan K
08709c93c7 ♻️ refactor(podkop): rename section variables for clarity 2025-05-16 18:28:06 +03:00
Ivan K
cf5b2216be ♻️ refactor(podkop): reorganize sections into subdirectory 2025-05-16 18:23:16 +03:00
Ivan K
682913ade0 ♻️ refactor(podkop): remove unused parameter from createAdditionalSection 2025-05-16 18:08:29 +03:00
Ivan K
3b2cbd0332 ♻️ refactor(podkop): modularize configuration and diagnostics sections 2025-05-16 18:04:33 +03:00
itdoginfo
8f9dcf2c55 Merge pull request #109 from itdoginfo/fix/comments
♻️ refactor(podkop): refactor domain list and API endpoints
2025-05-16 14:50:59 +03:00
Ivan K
91d027b5fe ♻️ refactor(podkop): refactor domain list and API endpoints 2025-05-16 14:46:06 +03:00
itdoginfo
f90ab7f468 v0.4.0 beta 2025-05-15 13:26:57 +03:00
itdoginfo
e4bfd447ce Merge pull request #108 from itdoginfo/fix/comments
fix(podkop): add dont touch my dhcp logic to fake IP check functions
2025-05-15 12:07:08 +03:00
Ivan K
fbdd759b83 ♻️ refactor(podkop): remove redundant DNS/sing-box checks in fakeip status 2025-05-15 11:53:30 +03:00
Ivan K
2488bc30b1 fix(podkop): add dont touch my dhcp logic to fake IP check functions 2025-05-15 11:31:33 +03:00
itdoginfo
dcc12cf920 Merge pull request #107 from itdoginfo/fix/comments
💄 style(podkop): update modal button titles and clipboard content
2025-05-14 14:33:53 +03:00
Ivan K
c99cef9f27 💄 style(podkop): update modal button titles and clipboard content 2025-05-14 14:30:52 +03:00
itdoginfo
8a68f3fcc2 Update 2025-05-13 14:26:50 +03:00
itdoginfo
ed2994be3a v0.3.50 Adde Google AI, Play, HTZ, OVH of list 2025-05-12 23:31:06 +03:00
itdoginfo
77ff5ab781 Merge pull request #105 from itdoginfo/fix/comments
️ perf(dns): improve DNS query performance and error handling
2025-05-12 18:17:50 +03:00
Ivan K
1c80bc5a5e ️ perf(dns): improve DNS query performance and error handling 2025-05-12 17:50:50 +03:00
itdoginfo
f688d74c32 v0.3.49 Improved diagnostics 2025-05-12 17:32:51 +03:00
itdoginfo
7bc50d58d3 Merge pull request #104 from itdoginfo/fix/comments
Fix/comments
2025-05-12 17:25:59 +03:00
Ivan K
77ce0c380b 🐛 fix(dns): improve DNS availability check logic 2025-05-12 17:21:38 +03:00
Ivan K
47d1b349c7 ♻️ refactor(podkop): add local var 2025-05-12 16:51:59 +03:00
Ivan K
e9face1f4a ♻️ refactor(podkop): simplify logging function 2025-05-12 16:48:59 +03:00
itdoginfo
e5bf7d9bed Merge pull request #103 from itdoginfo/fix/comments
♻️ refactor(podkop): improve diagnostics and error handling
2025-05-12 16:48:15 +03:00
Ivan K
dd4722f3e1 ♻️ refactor(podkop): simplify logging functions 2025-05-12 16:40:44 +03:00
Ivan K
1e945dafe7 feat(logging): add colored logging to stdout and syslog 2025-05-12 15:54:12 +03:00
itdoginfo
b080521a58 Update 2025-05-12 00:57:52 +03:00
itdoginfo
6a96a85773 Update 2025-05-12 00:49:19 +03:00
itdoginfo
6fb3a36974 Update 2025-05-12 00:46:30 +03:00
Ivan K
b3dbee1dbe 💄 style(podkop): adjust margin styles in status panel 2025-05-11 20:30:16 +03:00
Ivan K
916321578d ️ feat(dns): add random DNS query ID generation 2025-05-11 19:33:08 +03:00
Ivan K
c74d733717 ♻️ refactor(podkop): improve diagnostics and error handling 2025-05-11 19:26:29 +03:00
itdoginfo
433724f762 v0.3.48 Custom URL CRLF 2025-05-10 18:55:35 +03:00
itdoginfo
6378aa9910 Update 2025-05-10 16:15:53 +03:00
itdoginfo
68f5f123ca v0.3.47 Fix noresolv 1 2025-05-10 12:50:01 +03:00
itdoginfo
fae43d0471 v0.3.46 2025-05-08 19:24:08 +03:00
itdoginfo
9d6dc45fdb #99 Block mode 2025-05-08 19:23:45 +03:00
itdoginfo
9aa5a2d242 Fix site. #100 added ntpd 2025-05-08 10:14:58 +03:00
itdoginfo
63dc86fca4 v0.3.45 Update checker domain 2025-05-07 22:39:23 +03:00
itdoginfo
4d9cedaf4c Return upgrade command 2025-05-07 17:58:19 +03:00
itdoginfo
14e7cbae01 v0.3.44 2025-05-07 17:26:57 +03:00
itdoginfo
c9f610bb1e Change to ip.podkop.net. Fix log. Added restart 2025-05-07 17:24:32 +03:00
itdoginfo
19671c7f67 Stop btn. Change to ip.podkop.net 2025-05-07 17:23:20 +03:00
itdoginfo
6d1e4091e5 Detour 2025-05-07 00:22:04 +03:00
itdoginfo
96d661c49f Fixed default values ttl in comment 2025-05-03 18:52:57 +03:00
itdoginfo
da8dd06b34 Move doc to wiki 2025-05-03 18:12:00 +03:00
itdoginfo
2c1bcffb6d fix iptables 2025-05-03 18:11:49 +03:00
itdoginfo
3040ce7286 v0.3.43 2025-05-02 14:53:11 +03:00
itdoginfo
e025271a14 Added to global check: DNS check and proxy check. From VizzleTF 2025-05-02 14:50:06 +03:00
itdoginfo
2b8208186d Fix global check text 2025-05-02 13:55:07 +03:00
itdoginfo
17fb11baf0 Fixed diagnostics from VizzleTF 2025-05-02 13:34:11 +03:00
itdoginfo
3c1b041b52 Edited text from #96 2025-05-01 22:52:41 +03:00
itdoginfo
38acac1a31 Merge pull request #96 from itdoginfo/chore/sing-box-status
Issue #91 , Issue #94
2025-05-01 22:16:38 +03:00
Ivan K
2939229df3 back to the future 2025-05-01 19:30:05 +03:00
Ivan K
26c3d0bc7e ♻️ refactor(podkop): simplify DoH URL determination logic 2025-05-01 19:26:21 +03:00
Ivan K
b364363b1b feat(dns): add DoH URL resolution function 2025-05-01 19:20:36 +03:00
itdoginfo
d85caf0c0c Fix https-dns-proxy i18 2025-05-01 19:10:18 +03:00
Ivan K
65f72e1e04 ♻️ refactor(podkop): update WARP detection logic 2025-05-01 18:29:42 +03:00
Ivan K
e59ef6dd6f 💄 style(podkop): remove unnecessary sed commands in global_check 2025-05-01 17:57:51 +03:00
Ivan K
05272de650 💄 style(podkop): update formatting and messages 2025-05-01 17:48:25 +03:00
Ivan K
48716e7156 Enhance Podkop functionality with global check feature and improved diagnostics. Added support for FakeIP tests in both browser and router contexts. Updated UI elements for better status reporting and added localization for new messages. 2025-05-01 17:18:07 +03:00
itdoginfo
f29b97e495 v0.3.42 2025-05-01 14:17:18 +03:00
itdoginfo
41c21cebcd Fixed validation for ws 2025-04-30 23:43:36 +03:00
itdoginfo
238e99a547 Update 2025-04-30 19:02:31 +03:00
itdoginfo
4f44fcfe99 Update 2025-04-30 14:48:12 +03:00
itdoginfo
9fd2fb9b6e Update 2025-04-30 00:19:42 +03:00
itdoginfo
c0591b25b9 Fix 2025-04-30 00:16:09 +03:00
itdoginfo
97fd392334 Fixed read. Added upgrade flag 2025-04-30 00:11:55 +03:00
itdoginfo
848c784cc0 Fix 2025-04-29 23:49:28 +03:00
itdoginfo
ab971dcd36 Update 2025-04-29 23:48:49 +03:00
itdoginfo
b8d96f28cd Added CF. Fixed https-dns-proxy warning. Masked for static wan 2025-04-29 18:54:50 +03:00
itdoginfo
f2268fd494 v0.3.41. Improved Diagnotics: WAN, WARP, versions, etc 2025-04-29 12:53:29 +03:00
itdoginfo
19897afcdd v0.3.40. Improved Diagnotics 2025-04-28 00:33:07 +03:00
16 changed files with 2567 additions and 1861 deletions

View File

@@ -1,6 +1,4 @@
FROM openwrt/sdk:x86_64-v23.05.5
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/
FROM itdoginfo/openwrt-sdk:24.10.1
COPY ./podkop /builder/package/feeds/utilites/podkop
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop

3
Dockerfile-SDK Normal file
View File

@@ -0,0 +1,3 @@
FROM openwrt/sdk:x86_64-v24.10.1
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/

209
README.md
View File

@@ -1,196 +1,59 @@
# Вещи, которые вам нужно знать перед установкой
- Это альфа версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- Основной функционал работает, но побочные штуки сейчас могут сбоить.
- При обновлении **обязательно** сбрасывайте кэш LuCI.
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- При возникновении проблем, нужен технически грамотный фидбэк в чат.
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clearbrowsercache/).
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
- Необходимо минимум 15МБ свободного места на роутере. Роутерами с флешками на 16МБ сразу мимо.
- Необходимо минимум 15МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
- При старте программы редактируется конфиг Dnsmasq.
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
- Информация здесь может быть устаревшей. Все изменения фиксируются в телеграм-чате https://t.me/itdogchat - топик **Podkop**.
- Если у вас не что-то не работает, то следуюет сходить в телеграм чат, прочитать закрепы и выполнить что там написано..
- Если у вас установлен Getdomains, его следует удалить.
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](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).
# Удаление GetDomains скриптом
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/domain-routing-openwrt/refs/heads/master/getdomains-uninstall.sh)
```
Оставляет туннели, зоны, forwarding. А также stubby и dnscrypt. Они не помешают. Конфиг sing-box будет перезаписан в podkop.
# Документация
https://podkop.net/
# Установка Podkop
Пакет работает на всех архитектурах.
Тестировался на **ванильной** OpenWrt 23.05 и OpenWrt 24.10.
На FriendlyWrt 23.05 присуствуют зависимости от iptables, которые ломают tproxy. Если у вас появляется warning про это в логах, следуйте инструкции по приведённой там ссылке.
Полная информация в [документации](https://podkop.net/docs/install/)
Поддержки APK на данный момент нет. APK будет сделан после того как разгребу основное.
## Автоматическая
Вкратце, достаточно одного скрипта для установки:
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
```
Скрипт также предложит выбрать, какой туннель будет использоваться. Для выбранного туннеля будут установлены нужные пакеты, а для Wireguard и AmneziaWG также будет предложена автоматическая настройка - прямо в консоли скрипт запросит данные конфига. Для AmneziaWG можно также выбрать вариант с использованием конфига обычного Wireguard и автоматической обфускацией до AmneziaWG.
Для AmneziaWG скрипт проверяет наличие пакетов под вашу платформу в [стороннем репозитории](https://github.com/Slava-Shchipunov/awg-openwrt/releases), так как в официальном репозитории OpenWRT они отсутствуют, и автоматически их устанавливает.
## Вручную
Сделать `opkg update`, чтоб установились зависимости.
Скачать пакеты `podkop_*.ipk` и `luci-app-podkop_*.ipk` из релиза. `opkg install` сначала первый, потом второй.
# Обновление
Та же самая команда, что для установки. Скрипт обнаружит уже установленный podkop и предложит обновиться.
Для обновления:
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
sh <(wget -qO- https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) --upgrade
```
# Удаление
```
opkg remove luci-i18n-podkop-ru luci-app-podkop podkop
```
# Использование
Конфиг: /etc/config/podkop
Luci: Services/podkop
## Режимы
### Proxy
Для VLESS и Shadowsocks. Другие протоколы тоже будут, кидайте в чат примеры строк без чувствительных данных.
В этом режиме просто копируйте строку в **Proxy String** и из неё автоматически настроится sing-box.
### VPN
Здесь у вас должен быть уже настроен WG/OpenVPN/OpenConnect etc, зона Zone и Forwarding не обязательны.
Просто выбрать интерфейс из списка.
## Настройка доменов и подсетей
**Community Lists** - Включить списки комьюнити
**Custom domains enable** - Добавить свои домены
**Custom subnets enable** - Добавить подсети или IP-адреса. Для подсетей задать маску.
# Известные баги
- [x] Не отрабатывает service podkop stop, если podkop запущен и не может, к пример, зарезолвить домен с сломанным DNS
- [x] Update list из remote url domain не удаляет старые домены. А добавляет новые. Для подсетей тоже самое скорее всего. Пересоздавать ruleset?
# ToDo
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
- [x] Interface trigger
- [x] Управление sing-box с помощью podkop. sing-box disable
- [x] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn (нужно wiki)
- [x] Рестарт сервиса без рестарта dnsmasq
- [x] `ash: can't kill pid 9848: No such process` при обновлении
- [x] Luci: Добавить валидацию "Proxy Configuration URL". Если пустое, то ошибка. Как с интерфейсом.
- [ ] Не грузится диагностика полностью при одной нерабочей комманде. Подумать как это можно дебажить легко. https://t.me/itdogchat/142500/378956
- [x] DoH возможность добавлять сервера c path. Взять пример из NextDNS
- [ ] При добавлении github ломается скачивание скрипта установки и любые другие скрипты с github соотвественно. Скорее всего нужно делать опцией добавление в nft самого роутера как src.
Основные задачи в issues.
Диагностика
- [ ] Используется ли warp. Сравнивать endpoint с префиксами CF
## Рефактор
- [ ] Очевидные повторения в `/usr/bin/podkop` загнать в переменые
- [ ] Возможно поменять структуру
Низкий приоритет
- [ ] Галочка, которая режет доступ к doh серверам
- [ ] IPv6. Только после наполнения Wiki
## Списки
- [ ] Speedtest
- [x] Google AI
- [x] Google PlayMarket. Здесь уточнить, что точно не работает через корректную настройку FakeIP, а не dnsmasq+nft.
- [x] Hetzner ASN (AS24940)
- [x] OVH ASN (AS16276)
Рефактор
- [ ] Handle для sing-box
- [ ] Handle для dnsmasq
## Будущее
- [ ] После наполнения вики про туннели, убрать всё что связано с их установкой из скрипта. Только с AWG что-то решить, лучше чтоб был скрипт в сторонем репозитории.
- [ ] Подписка. Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
- [ ] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables.
- [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме.
- [ ] Поддержка Source format. Нужна расшифровка в json и если присуствуют подсети, заносить их в custom subnet nftset.
- [ ] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d.
- [ ] Галочка, которая режет доступ к doh серверам.
- [ ] IPv6. Только после наполнения Wiki.
## Тесты
- [ ] Unit тесты (BATS)
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)
# Don't touch my dhcp
Нужно в первую очередь, чтоб использовать опцию `server`.
В случае если опция активна, podkop не трогает /etc/config/dhcp. И вам требуется самостоятельно указать следующие значения:
```
option noresolv '1'
option cachesize '0'
list server '127.0.0.42'
```
Без этого podkop работать не будет.
# Bad WAN
При использовании опции **Interface monitoring** необходимо рестартовать podkop, чтоб init.d подхватил это
```
service podkop restart
```
# Разработка
Есть два варианта:
- Просто поставить пакет на роутер или виртуалку и прям редактировать через SFTP (opkg install openssh-sftp-server)
- SDK, чтоб собирать пакеты
Для сборки пакетов нужен SDK, один из вариантов скачать прям файл и разархивировать
https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/
Нужен файл с SDK в имени
```
wget https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
tar xf openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
mv openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64 SDK
```
Последнее для удобства.
Создаём директорию для пакета
```
mkdir package/utilites
```
Симлинк из репозитория
```
ln -s ~/podkop/podkop package/utilites/podkop
ln -s ~/podkop/luci-app-podkop package/luci-app-podkop
```
В первый раз для сборки luci-app необходимо обновить пакеты
```
./scripts/feeds update -a
```
Для make можно добавить флаг -j N, где N - количество ядер для сборки. Первый раз пройдёт быстрее.
При первом make выводится менюшка, можно просто save, exit и всё. Первый раз долго грузит зависимости.
Сборка пакета. Сами пакеты собираются быстро.
```
make package/podkop/{clean,compile} V=s
```
Также для luci
```
make package/luci-app-podkop/{clean,compile} V=s
```
.ipk лежат в `bin/packages/x86_64/base/`
## Примеры строк
https://github.com/itdoginfo/podkop/blob/main/String-example.md
## Ошибки
```
Makefile:17: /SDK/feeds/luci/luci.mk: No such file or directory
make[2]: *** No rule to make target '/SDK/feeds/luci/luci.mk'. Stop.
time: package/luci/luci-app-podkop/clean#0.00#0.00#0.00
ERROR: package/luci/luci-app-podkop failed to build.
make[1]: *** [package/Makefile:129: package/luci/luci-app-podkop/clean] Error 1
make[1]: Leaving directory '/SDK'
make: *** [/SDK/include/toplevel.mk:226: package/luci-app-podkop/clean] Error 2
```
Не загружены пакеты для luci
## make зависимости
https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem
Ubuntu
```
sudo apt update
sudo apt install build-essential clang flex bison g++ gawk \
gcc-multilib g++-multilib gettext git libncurses-dev libssl-dev \
python3-distutils rsync unzip zlib1g-dev file wget
```
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)

View File

@@ -5,10 +5,17 @@ REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest"
IS_SHOULD_RESTART_NETWORK=
DOWNLOAD_DIR="/tmp/podkop"
COUNT=3
UPGRADE=0
rm -rf "$DOWNLOAD_DIR"
mkdir -p "$DOWNLOAD_DIR"
for arg in "$@"; do
if [ "$arg" = "--upgrade" ]; then
UPGRADE=1
fi
done
main() {
check_system
sing_box
@@ -16,28 +23,34 @@ main() {
opkg update
if [ -f "/etc/init.d/podkop" ]; then
printf "\033[32;1mPodkop is already installed. Just upgrade it? (y/n)\033[0m\n"
printf "\033[32;1my - Only upgrade podkop\033[0m\n"
printf "\033[32;1mn - Upgrade and install tunnels (WG, AWG, OpenVPN, OC)\033[0m\n"
if [ "$UPGRADE" -eq 1 ]; then
echo "Upgraded podkop with flag..."
break
else
printf "\033[32;1mPodkop is already installed. Just upgrade it?\033[0m\n"
printf "\033[32;1my - Only upgrade podkop\033[0m\n"
printf "\033[32;1mn - Upgrade and install tunnels (WG, AWG, OpenVPN, OC)\033[0m\n"
while true; do
read -r -p '' UPDATE
case $UPDATE in
y)
echo "Upgraded podkop..."
break
;;
while true; do
printf "\033[32;1mEnter (y/n): \033[0m"
read -r -p '' UPDATE
case $UPDATE in
y)
echo "Upgraded podkop..."
break
;;
n)
add_tunnel
break
;;
n)
add_tunnel
break
;;
*)
echo "Please enter y or n"
;;
esac
done
*)
echo "Please enter y or n"
;;
esac
done
fi
else
echo "Installed podkop..."
add_tunnel
@@ -98,6 +111,7 @@ main() {
read -r -p '' RUS
case $RUS in
y)
opkg remove luci-i18n-podkop*
opkg install "$DOWNLOAD_DIR/$ru"
break
;;
@@ -433,7 +447,7 @@ check_system() {
case $DNSPROXY in
yes|y|Y|yes)
opkg remove --force-depends luci-app-https-dns-proxy https-dns-proxy
opkg remove --force-depends luci-app-https-dns-proxy https-dns-proxy luci-i18n-https-dns-proxy*
break
;;
*)
@@ -444,7 +458,7 @@ check_system() {
done
fi
if opkg list-installed | grep -qE "iptables|kmod-iptab"; then
if opkg list-installed | grep -q "iptables-mod-extra"; then
printf "\033[31;1mFound incompatible iptables packages. If you're using FriendlyWrt: https://t.me/itdogchat/44512/181082\033[0m\n"
fi
}

View File

@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-podkop
PKG_VERSION:=0.3.39
PKG_VERSION:=0.4.4
PKG_RELEASE:=1
LUCI_TITLE:=LuCI podkop app

View File

@@ -0,0 +1,195 @@
'use strict';
'require form';
'require baseclass';
'require view.podkop.constants as constants';
'require tools.widgets as widgets';
function createAdditionalSection(mainSection, network) {
let o = mainSection.tab('additional', _('Additional Settings'));
o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('<a href="http://openwrt.lan:9090/ui" target="_blank">openwrt.lan:9090/ui</a>'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('For issues with open connections sing-box'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Flag, 'quic_disable', _('QUIC disable'), _('For issues with the video stream'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.ListValue, 'update_interval', _('List Update Frequency'), _('Select how often the lists will be updated'));
Object.entries(constants.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '1d';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', 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 = 'doh';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Value, 'dns_server', _('DNS Server'), _('Select or enter DNS server address'));
Object.entries(constants.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '8.8.8.8';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
if (!value) {
return _('DNS server address cannot be empty');
}
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipRegex.test(value)) {
const parts = value.split('.');
for (const part of parts) {
const num = parseInt(part);
if (num < 0 || num > 255) {
return _('IP address parts must be between 0 and 255');
}
}
return true;
}
const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
if (!domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
return true;
};
o = mainSection.taboption('additional', form.Value, 'dns_rewrite_ttl', _('DNS Rewrite TTL'), _('Time in seconds for DNS record caching (default: 60)'));
o.default = '60';
o.rmempty = false;
o.ucisection = 'main';
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 = mainSection.taboption('additional', form.Value, 'cache_file', _('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/cache.db', 'RAM (/tmp/cache.db)');
o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)');
o.default = '/tmp/cache.db';
o.rmempty = false;
o.ucisection = 'main';
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 = mainSection.taboption('additional', widgets.DeviceSelect, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate'));
o.ucisection = 'main';
o.default = 'br-lan';
o.noaliases = true;
o.nobridges = false;
o.noinactive = false;
o.multiple = true;
o.filter = function (section_id, value) {
if (['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'].indexOf(value) !== -1) {
return false;
}
var device = this.devices.filter(function (dev) {
return dev.getName() === value;
})[0];
if (device) {
var type = device.getType();
return type !== 'wifi' && type !== 'wireless' && !type.includes('wlan');
}
return true;
};
o = mainSection.taboption('additional', form.Flag, 'mon_restart_ifaces', _('Interface monitoring'), _('Interface monitoring for bad WAN'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', widgets.NetworkSelect, 'restart_ifaces', _('Interface for monitoring'), _('Select the WAN interfaces to be monitored'));
o.ucisection = 'main';
o.depends('mon_restart_ifaces', '1');
o.multiple = true;
o.filter = function (section_id, value) {
return ['lan', 'loopback'].indexOf(value) === -1 && !value.startsWith('@');
};
o = mainSection.taboption('additional', form.Flag, 'dont_touch_dhcp', _('Dont touch my DHCP!'), _('Podkop will not change the DHCP config'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Flag, 'detour', _('Proxy download of lists'), _('Downloading all lists via main Proxy/VPN'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
// Extra IPs and exclusions (main section)
o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('basic', form.DynamicList, 'exclude_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses'));
o.placeholder = 'IP';
o.depends('exclude_from_ip_enabled', '1');
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
const ipParts = value.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
}
return true;
};
o = mainSection.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
}
return baseclass.extend({
createAdditionalSection
});

View File

@@ -0,0 +1,533 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require network';
'require view.podkop.constants as constants';
'require tools.widgets as widgets';
function validateUrl(url, protocols = ['http:', 'https:']) {
try {
const parsedUrl = new URL(url);
if (!protocols.includes(parsedUrl.protocol)) {
return _('URL must use one of the following protocols: ') + protocols.join(', ');
}
return true;
} catch (e) {
return _('Invalid URL format');
}
}
function createConfigSection(section, map, network) {
const s = section;
let o = s.tab('basic', _('Basic Settings'));
o = s.taboption('basic', form.ListValue, 'mode', _('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.ucisection = s.section;
o = s.taboption('basic', form.ListValue, 'proxy_config_type', _('Configuration Type'), _('Select how to configure the proxy'));
o.value('url', _('Connection URL'));
o.value('outbound', _('Outbound Config'));
o.default = 'url';
o.depends('mode', 'proxy');
o.ucisection = s.section;
o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), _(''));
o.depends('proxy_config_type', 'url');
o.rows = 5;
o.rmempty = false;
o.ucisection = s.section;
o.sectionDescriptions = new Map();
o.placeholder = 'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt';
o.renderWidget = function (section_id, option_index, cfgvalue) {
const original = form.TextValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
const container = E('div', {});
container.appendChild(original);
if (cfgvalue) {
try {
const activeConfig = cfgvalue.split('\n')
.map(line => line.trim())
.find(line => line && !line.startsWith('//'));
if (activeConfig) {
if (activeConfig.includes('#')) {
const label = activeConfig.split('#').pop();
if (label && label.trim()) {
const decodedLabel = decodeURIComponent(label);
const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel);
container.appendChild(descDiv);
} else {
const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
container.appendChild(descDiv);
}
} else {
const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
container.appendChild(descDiv);
}
}
} catch (e) {
console.error('Error parsing config label:', e);
const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
container.appendChild(descDiv);
}
} else {
const defaultDesc = E('div', { 'class': 'cbi-value-description' },
_('Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs'));
container.appendChild(defaultDesc);
}
return container;
};
o.validate = function (section_id, value) {
if (!value || value.length === 0) {
return true;
}
try {
const activeConfig = value.split('\n')
.map(line => line.trim())
.find(line => line && !line.startsWith('//'));
if (!activeConfig) {
return _('No active configuration found. At least one non-commented line is required.');
}
if (!activeConfig.startsWith('vless://') && !activeConfig.startsWith('ss://')) {
return _('URL must start with vless:// or ss://');
}
if (activeConfig.startsWith('ss://')) {
let encrypted_part;
try {
let mainPart = activeConfig.includes('?') ? activeConfig.split('?')[0] : activeConfig.split('#')[0];
encrypted_part = mainPart.split('/')[2].split('@')[0];
try {
let decoded = atob(encrypted_part);
if (!decoded.includes(':')) {
if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
}
}
} catch (e) {
if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
}
}
} catch (e) {
return _('Invalid Shadowsocks URL format');
}
try {
let serverPart = activeConfig.split('@')[1];
if (!serverPart) return _('Invalid Shadowsocks URL: missing server address');
let [server, portAndRest] = serverPart.split(':');
if (!server) return _('Invalid Shadowsocks URL: missing server');
let port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
if (!port) return _('Invalid Shadowsocks URL: missing port');
let portNum = parseInt(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return _('Invalid port number. Must be between 1 and 65535');
}
} catch (e) {
return _('Invalid Shadowsocks URL: missing or invalid server/port format');
}
}
if (activeConfig.startsWith('vless://')) {
let uuid = activeConfig.split('/')[2].split('@')[0];
if (!uuid || uuid.length === 0) return _('Invalid VLESS URL: missing UUID');
try {
let serverPart = activeConfig.split('@')[1];
if (!serverPart) return _('Invalid VLESS URL: missing server address');
let [server, portAndRest] = serverPart.split(':');
if (!server) return _('Invalid VLESS URL: missing server');
let port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null;
if (!port) return _('Invalid VLESS URL: missing port');
let portNum = parseInt(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return _('Invalid port number. Must be between 1 and 65535');
}
} catch (e) {
return _('Invalid VLESS URL: missing or invalid server/port format');
}
let queryString = activeConfig.split('?')[1];
if (!queryString) return _('Invalid VLESS URL: missing query parameters');
let params = new URLSearchParams(queryString.split('#')[0]);
let type = params.get('type');
const validTypes = ['tcp', 'raw', 'udp', 'grpc', 'http', 'ws'];
if (!type || !validTypes.includes(type)) {
return _('Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws');
}
let security = params.get('security');
const validSecurities = ['tls', 'reality', 'none'];
if (!security || !validSecurities.includes(security)) {
return _('Invalid VLESS URL: security must be one of tls, reality, none');
}
if (security === 'reality') {
if (!params.get('pbk')) return _('Invalid VLESS URL: missing pbk parameter for reality security');
if (!params.get('fp')) return _('Invalid VLESS URL: missing fp parameter for reality security');
}
if (security === 'tls' && type !== 'tcp' && !params.get('sni')) {
return _('Invalid VLESS URL: missing sni parameter for tls security');
}
}
return true;
} catch (e) {
console.error('Validation error:', e);
return _('Invalid URL format: ') + e.message;
}
};
o = s.taboption('basic', form.TextValue, 'outbound_json', _('Outbound Configuration'), _('Enter complete outbound configuration in JSON format'));
o.depends('proxy_config_type', 'outbound');
o.rows = 10;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
try {
const parsed = JSON.parse(value);
if (!parsed.type || !parsed.server || !parsed.server_port) {
return _('JSON must contain at least type, server and server_port fields');
}
return true;
} catch (e) {
return _('Invalid JSON format');
}
};
o = s.taboption('basic', form.Flag, 'ss_uot', _('Shadowsocks UDP over TCP'), _('Apply for SS2022'));
o.default = '0';
o.depends('mode', 'proxy');
o.rmempty = false;
o.ucisection = 'main';
o = s.taboption('basic', widgets.DeviceSelect, 'interface', _('Network Interface'), _('Select network interface for VPN connection'));
o.depends('mode', 'vpn');
o.ucisection = s.section;
o.noaliases = true;
o.nobridges = false;
o.noinactive = false;
o.filter = function (section_id, value) {
if (['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan', 'lan'].indexOf(value) !== -1) {
return false;
}
var device = this.devices.filter(function (dev) {
return dev.getName() === value;
})[0];
if (device) {
var type = device.getType();
return type !== 'wifi' && type !== 'wireless' && !type.includes('wlan');
}
return true;
};
o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service for routing') + ' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>');
o.placeholder = 'Service list';
Object.entries(constants.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.depends('domain_list_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
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 = constants.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 || !constants.REGIONAL_OPTIONS.includes(v));
notifications.push(E('p', { class: 'alert-message warning' }, [
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 => !constants.ALLOWED_WITH_RUSSIA_INSIDE.includes(v));
if (removedServices.length > 0) {
newValues = newValues.filter(v => constants.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(
constants.ALLOWED_WITH_RUSSIA_INSIDE.map(key => constants.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 = s.taboption('basic', form.ListValue, 'custom_domains_list_type', _('User Domain List Type'), _('Select how to add your custom domains'));
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
o.value('text', _('Text List'));
o.default = 'disabled';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'custom_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)'));
o.placeholder = 'Domains list';
o.depends('custom_domains_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
if (!domainRegex.test(value)) {
return _('Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)');
}
return true;
};
o = s.taboption('basic', form.TextValue, 'custom_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline. You can add comments after //'));
o.placeholder = 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
o.depends('custom_domains_list_type', 'text');
o.rows = 8;
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
const lines = value.split(/\n/).map(line => line.trim());
let hasValidDomain = false;
for (const line of lines) {
// Skip empty lines
if (!line) continue;
// Extract domain part (before any //)
const domainPart = line.split('//')[0].trim();
// Skip if line is empty after removing comments
if (!domainPart) continue;
// Process each domain in the line (separated by comma or space)
const domains = domainPart.split(/[,\s]+/).map(d => d.trim()).filter(d => d.length > 0);
for (const domain of domains) {
if (!domainRegex.test(domain)) {
return _('Invalid domain format: %s. Enter domain without protocol').format(domain);
}
hasValidDomain = true;
}
}
if (!hasValidDomain) {
return _('At least one valid domain must be specified. Comments-only content is not allowed.');
}
return true;
};
o = s.taboption('basic', form.Flag, 'custom_local_domains_list_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'custom_local_domains', _('Local Domain Lists Path'), _('Enter the list file path'));
o.placeholder = '/path/file.lst';
o.depends('custom_local_domains_list_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/;
if (!pathRegex.test(value)) {
return _('Invalid path format. Path must start with "/" and contain valid characters');
}
return true;
};
o = s.taboption('basic', form.Flag, 'custom_download_domains_list_enabled', _('Remote Domain Lists'), _('Download and use domain lists from remote URLs'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'custom_download_domains', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://'));
o.placeholder = 'URL';
o.depends('custom_download_domains_list_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
return validateUrl(value);
};
o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets'));
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
o.value('text', _('Text List (comma/space/newline separated)'));
o.default = 'disabled';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'custom_subnets', _('User Subnets'), _('Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses'));
o.placeholder = 'IP or subnet';
o.depends('custom_subnets_list_enabled', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
if (!subnetRegex.test(value)) return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y');
const [ip, cidr] = value.split('/');
const ipParts = ip.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
}
if (cidr !== undefined) {
const cidrNum = parseInt(cidr);
if (cidrNum < 0 || cidrNum > 32) return _('CIDR must be between 0 and 32');
}
return true;
};
o = s.taboption('basic', form.TextValue, 'custom_subnets_text', _('User Subnets List'), _('Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //'));
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('custom_subnets_list_enabled', 'text');
o.rows = 10;
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
const lines = value.split(/\n/).map(line => line.trim());
let hasValidSubnet = false;
for (const line of lines) {
// Skip empty lines
if (!line) continue;
// Extract subnet part (before any //)
const subnetPart = line.split('//')[0].trim();
// Skip if line is empty after removing comments
if (!subnetPart) continue;
// Process each subnet in the line (separated by comma or space)
const subnets = subnetPart.split(/[,\s]+/).map(s => s.trim()).filter(s => s.length > 0);
for (const subnet of subnets) {
if (!subnetRegex.test(subnet)) {
return _('Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y').format(subnet);
}
const [ip, cidr] = subnet.split('/');
const ipParts = ip.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) {
return _('IP parts must be between 0 and 255 in: %s').format(subnet);
}
}
if (cidr !== undefined) {
const cidrNum = parseInt(cidr);
if (cidrNum < 0 || cidrNum > 32) {
return _('CIDR must be between 0 and 32 in: %s').format(subnet);
}
}
hasValidSubnet = true;
}
}
if (!hasValidSubnet) {
return _('At least one valid subnet or IP must be specified. Comments-only content is not allowed.');
}
return true;
};
o = s.taboption('basic', form.Flag, 'custom_download_subnets_list_enabled', _('Remote Subnet Lists'), _('Download and use subnet lists from remote URLs'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'custom_download_subnets', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://'));
o.placeholder = 'URL';
o.depends('custom_download_subnets_list_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
return validateUrl(value);
};
o = s.taboption('basic', form.Flag, 'all_traffic_from_ip_enabled', _('IP for full redirection'), _('Specify local IP addresses whose traffic will always use the configured route'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'all_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses'));
o.placeholder = 'IP';
o.depends('all_traffic_from_ip_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
const ipParts = value.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
}
return true;
};
}
return baseclass.extend({
createConfigSection
});

View File

@@ -0,0 +1,107 @@
'use strict';
'require baseclass';
const STATUS_COLORS = {
SUCCESS: '#4caf50',
ERROR: '#f44336',
WARNING: '#ff9800'
};
const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
const REGIONAL_OPTIONS = ['russia_inside', 'russia_outside', 'ukraine_inside'];
const ALLOWED_WITH_RUSSIA_INSIDE = [
'russia_inside',
'meta',
'twitter',
'discord',
'telegram',
'cloudflare',
'google_ai',
'google_play',
'hetzner',
'ovh'
];
const DOMAIN_LIST_OPTIONS = {
russia_inside: 'Russia inside',
russia_outside: 'Russia outside',
ukraine_inside: 'Ukraine',
geoblock: 'Geo Block',
block: 'Block',
porn: 'Porn',
news: 'News',
anime: 'Anime',
youtube: 'Youtube',
discord: 'Discord',
meta: 'Meta',
twitter: 'Twitter (X)',
hdrezka: 'HDRezka',
tiktok: 'Tik-Tok',
telegram: 'Telegram',
cloudflare: 'Cloudflare',
google_ai: 'Google AI',
google_play: 'Google Play',
hetzner: 'Hetzner ASN',
ovh: 'OVH ASN'
};
const UPDATE_INTERVAL_OPTIONS = {
'1h': 'Every hour',
'3h': 'Every 3 hours',
'12h': 'Every 12 hours',
'1d': 'Every day',
'3d': 'Every 3 days'
};
const DNS_SERVER_OPTIONS = {
'1.1.1.1': 'Cloudflare (1.1.1.1)',
'8.8.8.8': 'Google (8.8.8.8)',
'9.9.9.9': 'Quad9 (9.9.9.9)',
'dns.adguard-dns.com': 'AdGuard Default (dns.adguard-dns.com)',
'unfiltered.adguard-dns.com': 'AdGuard Unfiltered (unfiltered.adguard-dns.com)',
'family.adguard-dns.com': 'AdGuard Family (family.adguard-dns.com)'
};
const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
const CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1000; // 9 seconds
const ERROR_POLL_INTERVAL = 10000; // 10 seconds
const COMMAND_TIMEOUT = 10000; // 10 seconds
const FETCH_TIMEOUT = 10000; // 10 seconds
const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
// Интервалы планирования команд в диагностике (в миллисекундах)
const COMMAND_SCHEDULING = {
P0_PRIORITY: 0, // Наивысший приоритет (без задержки)
P1_PRIORITY: 100, // Очень высокий приоритет
P2_PRIORITY: 300, // Высокий приоритет
P3_PRIORITY: 500, // Выше среднего
P4_PRIORITY: 700, // Стандартный приоритет
P5_PRIORITY: 900, // Ниже среднего
P6_PRIORITY: 1100, // Низкий приоритет
P7_PRIORITY: 1300, // Очень низкий приоритет
P8_PRIORITY: 1500, // Фоновое выполнение
P9_PRIORITY: 1700, // Выполнение в режиме простоя
P10_PRIORITY: 1900 // Наименьший приоритет
};
return baseclass.extend({
STATUS_COLORS,
FAKEIP_CHECK_DOMAIN,
IP_CHECK_DOMAIN,
REGIONAL_OPTIONS,
ALLOWED_WITH_RUSSIA_INSIDE,
DOMAIN_LIST_OPTIONS,
UPDATE_INTERVAL_OPTIONS,
DNS_SERVER_OPTIONS,
DIAGNOSTICS_UPDATE_INTERVAL,
ERROR_POLL_INTERVAL,
COMMAND_TIMEOUT,
FETCH_TIMEOUT,
BUTTON_FEEDBACK_TIMEOUT,
DIAGNOSTICS_INITIAL_DELAY,
COMMAND_SCHEDULING,
CACHE_TIMEOUT
});

View File

@@ -0,0 +1,887 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require uci';
'require fs';
'require view.podkop.constants as constants';
'require view.podkop.utils as utils';
// Cache system for network requests
const fetchCache = {};
// Helper function to fetch with cache
async function cachedFetch(url, options = {}) {
const cacheKey = url;
const currentTime = Date.now();
// If we have a valid cached response, return it
if (fetchCache[cacheKey] && currentTime - fetchCache[cacheKey].timestamp < constants.CACHE_TIMEOUT) {
console.log(`Using cached response for ${url}`);
return Promise.resolve(fetchCache[cacheKey].response.clone());
}
// Otherwise, make a new request
try {
const response = await fetch(url, options);
// Cache the response
fetchCache[cacheKey] = {
response: response.clone(),
timestamp: currentTime
};
return response;
} catch (error) {
throw error;
}
}
// Helper functions for command execution with prioritization - Using from utils.js now
function safeExec(command, args, priority, callback, timeout = constants.COMMAND_TIMEOUT) {
return utils.safeExec(command, args, priority, callback, timeout);
}
// Helper functions for handling checks
function runCheck(checkFunction, priority, callback) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) {
schedulingDelay = constants.COMMAND_SCHEDULING[priority];
}
const executeCheck = async () => {
try {
const result = await checkFunction();
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
} catch (error) {
if (callback && typeof callback === 'function') {
callback({ error });
}
return { error };
}
};
if (callback && typeof callback === 'function') {
setTimeout(executeCheck, schedulingDelay);
return;
} else {
return executeCheck();
}
}
function runAsyncTask(taskFunction, priority) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) {
schedulingDelay = constants.COMMAND_SCHEDULING[priority];
}
setTimeout(async () => {
try {
await taskFunction();
} catch (error) {
console.error('Async task error:', error);
}
}, schedulingDelay);
}
// Helper Functions for UI and formatting
function createStatus(state, message, color) {
return {
state,
message: _(message),
color: constants.STATUS_COLORS[color]
};
}
function formatDiagnosticOutput(output) {
if (typeof output !== 'string') return '';
return output.trim()
.replace(/\x1b\[[0-9;]*m/g, '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
}
function copyToClipboard(text, button) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
const originalText = button.textContent;
button.textContent = _('Copied!');
setTimeout(() => button.textContent = originalText, constants.BUTTON_FEEDBACK_TIMEOUT);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
// IP masking function
function maskIP(ip) {
if (!ip) return '';
const parts = ip.split('.');
if (parts.length !== 4) return ip;
return ['XX', 'XX', 'XX', parts[3]].join('.');
}
// Status Check Functions
async function checkFakeIP() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
try {
const response = await cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
const data = await response.json();
clearTimeout(timeoutId);
if (data.fakeip === true) {
return createStatus('working', 'working', 'SUCCESS');
} else {
return createStatus('not_working', 'not working', 'ERROR');
}
} catch (fetchError) {
clearTimeout(timeoutId);
const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
return createStatus('error', message, 'WARNING');
}
} catch (error) {
return createStatus('error', 'check error', 'WARNING');
}
}
async function checkFakeIPCLI() {
try {
return new Promise((resolve) => {
safeExec('nslookup', ['-timeout=2', constants.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], 'P0_PRIORITY', result => {
if (result.stdout && result.stdout.includes('198.18')) {
resolve(createStatus('working', 'working on router', 'SUCCESS'));
} else {
resolve(createStatus('not_working', 'not working on router', 'ERROR'));
}
});
});
} catch (error) {
return createStatus('error', 'CLI check error', 'WARNING');
}
}
function checkDNSAvailability() {
return new Promise(async (resolve) => {
try {
safeExec('/usr/bin/podkop', ['check_dns_available'], 'P0_PRIORITY', dnsStatusResult => {
if (!dnsStatusResult || !dnsStatusResult.stdout) {
return resolve({
remote: createStatus('error', 'DNS check timeout', 'WARNING'),
local: createStatus('error', 'DNS check timeout', 'WARNING')
});
}
try {
const dnsStatus = JSON.parse(dnsStatusResult.stdout);
const remoteStatus = dnsStatus.is_available ?
createStatus('available', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, 'SUCCESS') :
createStatus('unavailable', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, 'ERROR');
const localStatus = dnsStatus.local_dns_working ?
createStatus('available', 'Router DNS working', 'SUCCESS') :
createStatus('unavailable', 'Router DNS not working', 'ERROR');
return resolve({
remote: remoteStatus,
local: localStatus
});
} catch (parseError) {
return resolve({
remote: createStatus('error', 'DNS check parse error', 'WARNING'),
local: createStatus('error', 'DNS check parse error', 'WARNING')
});
}
});
} catch (error) {
return resolve({
remote: createStatus('error', 'DNS check error', 'WARNING'),
local: createStatus('error', 'DNS check error', 'WARNING')
});
}
});
}
async function checkBypass() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
try {
const response1 = await cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
const data1 = await response1.json();
const response2 = await cachedFetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal });
const data2 = await response2.json();
clearTimeout(timeoutId);
if (data1.IP && data2.IP) {
if (data1.IP !== data2.IP) {
return createStatus('working', 'working', 'SUCCESS');
} else {
return createStatus('not_working', 'same IP for both domains', 'ERROR');
}
} else {
return createStatus('error', 'check error (no IP)', 'WARNING');
}
} catch (fetchError) {
clearTimeout(timeoutId);
const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
return createStatus('error', message, 'WARNING');
}
} catch (error) {
return createStatus('error', 'check error', 'WARNING');
}
}
// Modal Functions
function createModalContent(title, content) {
return [
E('div', {
'class': 'panel-body',
style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
'line-height: 1.5; font-size: 14px;'
}, [
E('pre', { style: 'margin: 0;' }, content)
]),
E('div', {
'class': 'right',
style: 'margin-top: 1em;'
}, [
E('button', {
'class': 'btn',
'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target)
}, _('Copy to Clipboard')),
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
];
}
function showConfigModal(command, title) {
// Create and show modal immediately with loading state
const modalContent = E('div', { 'class': 'panel-body' }, [
E('div', {
'class': 'panel-body',
style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
'line-height: 1.5; font-size: 14px;'
}, [
E('pre', {
'id': 'modal-content-pre',
style: 'margin: 0;'
}, _('Loading...'))
]),
E('div', {
'class': 'right',
style: 'margin-top: 1em;'
}, [
E('button', {
'class': 'btn',
'id': 'copy-button',
'click': ev => copyToClipboard('```txt\n' + document.getElementById('modal-content-pre').innerText + '\n```', ev.target)
}, _('Copy to Clipboard')),
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
ui.showModal(_(title), modalContent);
// Function to update modal content
const updateModalContent = (content) => {
const pre = document.getElementById('modal-content-pre');
if (pre) {
pre.textContent = content;
}
};
try {
let formattedOutput = '';
if (command === 'global_check') {
safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => {
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal })
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId);
if (data.fakeip === true) {
formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n';
} else {
formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n';
formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n';
formattedOutput += _('Its must be router!') + '\n';
}
// Bypass check
cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal })
.then(bypassResponse => bypassResponse.json())
.then(bypassData => {
cachedFetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal })
.then(bypassResponse2 => bypassResponse2.json())
.then(bypassData2 => {
formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) {
formattedOutput += '✅ ' + _('Proxy working correctly') + '\n';
formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n';
formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n';
} else if (bypassData.IP === bypassData2.IP) {
formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n';
formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n';
} else {
formattedOutput += '❌ ' + _('Proxy check failed') + '\n';
}
updateModalContent(formattedOutput);
})
.catch(error => {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
});
})
.catch(error => {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
});
})
.catch(error => {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
});
} catch (error) {
formattedOutput += '\n❌ ' + _('Check failed: ') + error.message + '\n';
updateModalContent(formattedOutput);
}
});
} else {
safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => {
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
updateModalContent(formattedOutput);
});
}
} catch (error) {
updateModalContent(_('Error: ') + error.message);
}
}
// Button Factory
const ButtonFactory = {
createButton: function (config) {
return E('button', {
'class': `btn ${config.additionalClass || ''}`.trim(),
'click': config.onClick,
'style': config.style || ''
}, _(config.label));
},
createActionButton: function (config) {
return this.createButton({
label: config.label,
additionalClass: `cbi-button-${config.type || ''}`,
onClick: () => safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY')
.then(() => config.reload && location.reload()),
style: config.style
});
},
createInitActionButton: function (config) {
return this.createButton({
label: config.label,
additionalClass: `cbi-button-${config.type || ''}`,
onClick: () => safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY')
.then(() => config.reload && location.reload()),
style: config.style
});
},
createModalButton: function (config) {
return this.createButton({
label: config.label,
onClick: () => showConfigModal(config.command, config.title),
additionalClass: `cbi-button-${config.type || ''}`,
style: config.style
});
}
};
// Create a loading placeholder for status text
function createLoadingStatusText() {
return E('span', { 'class': 'loading-indicator' }, _('Loading...'));
}
// Create the status section with buttons loaded immediately but status indicators loading asynchronously
let createStatusSection = async function () {
// Get initial podkop status
let initialPodkopStatus = { enabled: false };
try {
const result = await fs.exec('/usr/bin/podkop', ['get_status']);
if (result && result.stdout) {
const status = JSON.parse(result.stdout);
initialPodkopStatus.enabled = status.enabled === 1;
}
} catch (e) {
console.error('Error getting initial podkop status:', e);
}
return E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [
// Podkop Status Panel
E('div', { 'id': 'podkop-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('Podkop Status')),
E('br'),
E('span', { 'id': 'podkop-status-text' }, createLoadingStatusText())
]),
E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
ButtonFactory.createActionButton({
label: 'Restart Podkop',
type: 'apply',
action: 'restart',
reload: true
}),
ButtonFactory.createActionButton({
label: 'Stop Podkop',
type: 'apply',
action: 'stop',
reload: true
}),
// Autostart button - create with initial state
ButtonFactory.createInitActionButton({
label: initialPodkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart',
type: initialPodkopStatus.enabled ? 'remove' : 'apply',
action: initialPodkopStatus.enabled ? 'disable' : 'enable',
reload: true
}),
ButtonFactory.createModalButton({
label: _('Global check'),
command: 'global_check',
title: _('Click here for all the info')
}),
ButtonFactory.createModalButton({
label: 'View Logs',
command: 'check_logs',
title: 'Podkop Logs'
}),
ButtonFactory.createModalButton({
label: _('Update Lists'),
command: 'list_update',
title: _('Lists Update Results')
})
])
]),
// Sing-box Status Panel
E('div', { 'id': 'singbox-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('Sing-box Status')),
E('br'),
E('span', { 'id': 'singbox-status-text' }, createLoadingStatusText())
]),
E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
ButtonFactory.createModalButton({
label: 'Show Config',
command: 'show_sing_box_config',
title: 'Sing-box Configuration'
}),
ButtonFactory.createModalButton({
label: 'View Logs',
command: 'check_sing_box_logs',
title: 'Sing-box Logs'
}),
ButtonFactory.createModalButton({
label: 'Check Connections',
command: 'check_sing_box_connections',
title: 'Active Connections'
}),
ButtonFactory.createModalButton({
label: _('Check NFT Rules'),
command: 'check_nft',
title: _('NFT Rules')
}),
ButtonFactory.createModalButton({
label: _('Check DNSMasq'),
command: 'check_dnsmasq',
title: _('DNSMasq Configuration')
})
])
]),
// FakeIP Status Panel
E('div', { 'id': 'fakeip-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('FakeIP Status'))
]),
E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('span', { 'id': 'fakeip-browser-status' }, createLoadingStatusText())
]),
E('div', {}, [
E('span', { 'id': 'fakeip-router-status' }, createLoadingStatusText())
])
]),
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('strong', {}, _('DNS Status')),
E('br'),
E('span', { 'id': 'dns-remote-status' }, createLoadingStatusText()),
E('br'),
E('span', { 'id': 'dns-local-status' }, createLoadingStatusText())
])
]),
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('strong', { 'id': 'config-name-text' }, _('Main config')),
E('br'),
E('span', { 'id': 'bypass-status' }, createLoadingStatusText())
])
])
])
]),
// Version Information Panel
E('div', { 'id': 'version-info-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('Version Information'))
]),
E('div', { 'class': 'panel-body' }, [
E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [
E('strong', {}, _('Podkop: ')), E('span', { 'id': 'podkop-version' }, _('Loading...')), '\n',
E('strong', {}, _('LuCI App: ')), E('span', { 'id': 'luci-version' }, _('Loading...')), '\n',
E('strong', {}, _('Sing-box: ')), E('span', { 'id': 'singbox-version' }, _('Loading...')), '\n',
E('strong', {}, _('OpenWrt Version: ')), E('span', { 'id': 'openwrt-version' }, _('Loading...')), '\n',
E('strong', {}, _('Device Model: ')), E('span', { 'id': 'device-model' }, _('Loading...'))
])
])
])
])
]);
};
// Global variables for tracking state
let diagnosticsUpdateTimer = null;
let isInitialCheck = true;
showConfigModal.busy = false;
function startDiagnosticsUpdates() {
if (diagnosticsUpdateTimer) {
clearInterval(diagnosticsUpdateTimer);
}
// Immediately update when started
updateDiagnostics();
// Then set up periodic updates
diagnosticsUpdateTimer = setInterval(updateDiagnostics, constants.DIAGNOSTICS_UPDATE_INTERVAL);
}
function stopDiagnosticsUpdates() {
if (diagnosticsUpdateTimer) {
clearInterval(diagnosticsUpdateTimer);
diagnosticsUpdateTimer = null;
}
}
// Update individual text element with new content
function updateTextElement(elementId, content) {
const element = document.getElementById(elementId);
if (element) {
element.innerHTML = '';
element.appendChild(content);
}
}
async function updateDiagnostics() {
// Podkop Status check
safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', result => {
try {
const parsedPodkopStatus = JSON.parse(result.stdout || '{"enabled":0,"status":"error"}');
// Update Podkop status text
updateTextElement('podkop-status-text',
E('span', {
'style': `color: ${parsedPodkopStatus.enabled ? constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR}`
}, [
parsedPodkopStatus.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled'
])
);
// Update autostart button
const autostartButton = parsedPodkopStatus.enabled ?
ButtonFactory.createInitActionButton({
label: 'Disable Autostart',
type: 'remove',
action: 'disable',
reload: true
}) :
ButtonFactory.createInitActionButton({
label: 'Enable Autostart',
type: 'apply',
action: 'enable',
reload: true
});
// Find the autostart button and replace it
const panel = document.getElementById('podkop-status-panel');
if (panel) {
const buttons = panel.querySelectorAll('.cbi-button');
if (buttons.length >= 3) {
buttons[2].parentNode.replaceChild(autostartButton, buttons[2]);
}
}
} catch (error) {
updateTextElement('podkop-status-text',
E('span', { 'style': `color: ${constants.STATUS_COLORS.ERROR}` }, '✘ Error')
);
}
});
// Sing-box Status check
safeExec('/usr/bin/podkop', ['get_sing_box_status'], 'P0_PRIORITY', result => {
try {
const parsedSingboxStatus = JSON.parse(result.stdout || '{"running":0,"enabled":0,"status":"error"}');
// Update Sing-box status text
updateTextElement('singbox-status-text',
E('span', {
'style': `color: ${parsedSingboxStatus.running && !parsedSingboxStatus.enabled ?
constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR}`
}, [
parsedSingboxStatus.running && !parsedSingboxStatus.enabled ?
'✔ running' : '✘ ' + parsedSingboxStatus.status
])
);
} catch (error) {
updateTextElement('singbox-status-text',
E('span', { 'style': `color: ${constants.STATUS_COLORS.ERROR}` }, '✘ Error')
);
}
});
// Version Information checks
safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', result => {
updateTextElement('podkop-version',
document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
);
});
safeExec('/usr/bin/podkop', ['show_luci_version'], 'P2_PRIORITY', result => {
updateTextElement('luci-version',
document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
);
});
safeExec('/usr/bin/podkop', ['show_sing_box_version'], 'P2_PRIORITY', result => {
updateTextElement('singbox-version',
document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
);
});
safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', result => {
if (result.stdout) {
updateTextElement('openwrt-version',
document.createTextNode(result.stdout.split('\n')[1].trim())
);
updateTextElement('device-model',
document.createTextNode(result.stdout.split('\n')[4].trim())
);
} else {
updateTextElement('openwrt-version', document.createTextNode(_('Unknown')));
updateTextElement('device-model', document.createTextNode(_('Unknown')));
}
});
// FakeIP and DNS status checks
runCheck(checkFakeIP, 'P3_PRIORITY', result => {
updateTextElement('fakeip-browser-status',
E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [
result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
result.error ? 'check error' : result.state === 'working' ? _('works in browser') : _('not works in browser')
])
);
});
runCheck(checkFakeIPCLI, 'P8_PRIORITY', result => {
updateTextElement('fakeip-router-status',
E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [
result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
result.error ? 'check error' : result.state === 'working' ? _('works on router') : _('not works on router')
])
);
});
runCheck(checkDNSAvailability, 'P4_PRIORITY', result => {
if (result.error) {
updateTextElement('dns-remote-status',
E('span', { style: `color: ${constants.STATUS_COLORS.WARNING}` }, '! DNS check error')
);
updateTextElement('dns-local-status',
E('span', { style: `color: ${constants.STATUS_COLORS.WARNING}` }, '! DNS check error')
);
} else {
updateTextElement('dns-remote-status',
E('span', { style: `color: ${result.remote.color}` }, [
result.remote.state === 'available' ? '✔ ' : result.remote.state === 'unavailable' ? '✘ ' : '! ',
result.remote.message
])
);
updateTextElement('dns-local-status',
E('span', { style: `color: ${result.local.color}` }, [
result.local.state === 'available' ? '✔ ' : result.local.state === 'unavailable' ? '✘ ' : '! ',
result.local.message
])
);
}
});
runCheck(checkBypass, 'P1_PRIORITY', result => {
updateTextElement('bypass-status',
E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [
result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
result.error ? 'check error' : result.message
])
);
}, 'P1_PRIORITY');
// Config name
runAsyncTask(async () => {
try {
let configName = _('Main config');
const data = await uci.load('podkop');
const proxyString = uci.get('podkop', 'main', 'proxy_string');
if (proxyString) {
const activeConfig = proxyString.split('\n')
.map(line => line.trim())
.find(line => line && !line.startsWith('//'));
if (activeConfig) {
if (activeConfig.includes('#')) {
const label = activeConfig.split('#').pop();
if (label && label.trim()) {
configName = _('Config: ') + decodeURIComponent(label);
}
}
}
}
updateTextElement('config-name-text', document.createTextNode(configName));
} catch (e) {
console.error('Error getting config name from UCI:', e);
}
}, 'P1_PRIORITY');
}
function createDiagnosticsSection(mainSection) {
let o = mainSection.tab('diagnostics', _('Diagnostics'));
o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => E('div', {
id: 'diagnostics-status',
'data-loading': 'true'
});
}
function setupDiagnosticsEventHandlers(node) {
const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop'));
node.insertBefore(titleDiv, node.firstChild);
// Function to initialize diagnostics
function initDiagnostics(container) {
if (container && container.hasAttribute('data-loading')) {
container.innerHTML = '';
showConfigModal.busy = false;
createStatusSection().then(section => {
container.appendChild(section);
startDiagnosticsUpdates();
// Start error polling when diagnostics tab is active
utils.startErrorPolling();
});
}
}
document.addEventListener('visibilitychange', function () {
const diagnosticsContainer = document.getElementById('diagnostics-status');
const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]');
if (document.hidden || !diagnosticsTab || !diagnosticsTab.classList.contains('cbi-tab-active')) {
stopDiagnosticsUpdates();
// Don't stop error polling here - it's managed in podkop.js for all tabs
} else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) {
startDiagnosticsUpdates();
// Ensure error polling is running when diagnostics tab is active
utils.startErrorPolling();
}
});
setTimeout(() => {
const diagnosticsContainer = document.getElementById('diagnostics-status');
const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]');
const otherTabs = document.querySelectorAll('.cbi-tab:not([data-tab="diagnostics"])');
// Check for direct page load case
const noActiveTabsExist = !Array.from(otherTabs).some(tab => tab.classList.contains('cbi-tab-active'));
if (diagnosticsContainer && diagnosticsTab && (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist)) {
initDiagnostics(diagnosticsContainer);
}
const tabs = node.querySelectorAll('.cbi-tabmenu');
if (tabs.length > 0) {
tabs[0].addEventListener('click', function (e) {
const tab = e.target.closest('.cbi-tab');
if (tab) {
const tabName = tab.getAttribute('data-tab');
if (tabName === 'diagnostics') {
const container = document.getElementById('diagnostics-status');
container.setAttribute('data-loading', 'true');
initDiagnostics(container);
} else {
stopDiagnosticsUpdates();
// Don't stop error polling - it should continue on all tabs
}
}
});
}
}, constants.DIAGNOSTICS_INITIAL_DELAY);
node.classList.add('fade-in');
return node;
}
return baseclass.extend({
createDiagnosticsSection,
setupDiagnosticsEventHandlers
});

View File

@@ -0,0 +1,152 @@
'use strict';
'require baseclass';
'require ui';
'require fs';
'require view.podkop.constants as constants';
// Flag to track if this is the first error check
let isInitialCheck = true;
// Set to track which errors we've already seen
const lastErrorsSet = new Set();
// Timer for periodic error polling
let errorPollTimer = null;
// Helper function to fetch errors from the podkop command
async function getPodkopErrors() {
return new Promise(resolve => {
safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', result => {
if (!result || !result.stdout) return resolve([]);
const logs = result.stdout.split('\n');
const errors = logs.filter(log =>
log.includes('[critical]')
);
resolve(errors);
});
});
}
// Show error notification to the user
function showErrorNotification(error, isMultiple = false) {
const notificationContent = E('div', { 'class': 'alert-message error' }, [
E('pre', { 'class': 'error-log' }, error)
]);
ui.addNotification(null, notificationContent);
}
// Helper function for command execution with prioritization
function safeExec(command, args, priority, callback, timeout = constants.COMMAND_TIMEOUT) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) {
schedulingDelay = constants.COMMAND_SCHEDULING[priority];
}
const executeCommand = async () => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fs.exec(command, args),
new Promise((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Command execution timed out'));
});
})
]);
clearTimeout(timeoutId);
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
} catch (error) {
console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`);
const errorResult = { stdout: '', stderr: error.message, error: error };
if (callback && typeof callback === 'function') {
callback(errorResult);
}
return errorResult;
}
};
if (callback && typeof callback === 'function') {
setTimeout(executeCommand, schedulingDelay);
return;
}
else {
return executeCommand();
}
}
// Check for critical errors and show notifications
async function checkForCriticalErrors() {
try {
const errors = await getPodkopErrors();
if (errors && errors.length > 0) {
// Filter out errors we've already seen
const newErrors = errors.filter(error => !lastErrorsSet.has(error));
if (newErrors.length > 0) {
// On initial check, just store errors without showing notifications
if (!isInitialCheck) {
// Show each new error as a notification
newErrors.forEach(error => {
showErrorNotification(error, newErrors.length > 1);
});
}
// Add new errors to our set of seen errors
newErrors.forEach(error => lastErrorsSet.add(error));
}
}
// After first check, mark as no longer initial
isInitialCheck = false;
} catch (error) {
console.error('Error checking for critical messages:', error);
}
}
// Start polling for errors at regular intervals
function startErrorPolling() {
if (errorPollTimer) {
clearInterval(errorPollTimer);
}
// Reset initial check flag to make sure we show errors
isInitialCheck = false;
// Immediately check for errors on start
checkForCriticalErrors();
// Then set up periodic checks
errorPollTimer = setInterval(checkForCriticalErrors, constants.ERROR_POLL_INTERVAL);
}
// Stop polling for errors
function stopErrorPolling() {
if (errorPollTimer) {
clearInterval(errorPollTimer);
errorPollTimer = null;
}
}
return baseclass.extend({
startErrorPolling,
stopErrorPolling,
checkForCriticalErrors,
safeExec
});

View File

@@ -819,3 +819,57 @@ msgstr "недоступен"
msgid "Apply for SS2022"
msgstr "Применить для SS2022"
msgid "PODKOP CONFIGURATION"
msgstr "КОНФИГУРАЦИЯ PODKOP"
msgid "FAKEIP ROUTER TEST"
msgstr "ПРОВЕРКА FAKEIP НА РОУТЕРЕ"
msgid "FAKEIP BROWSER TEST"
msgstr "ПРОВЕРКА FAKEIP В БРАУЗЕРЕ"
msgid "FakeIP is working correctly on router (198.18.x.x)"
msgstr "FakeIP работает корректно на роутере (198.18.x.x)"
msgid "Click here for all the info"
msgstr "Нажмите для просмотра всей информации"
msgid "Check DNS server on current device (PC, phone)"
msgstr "Проверьте DNS сервер на текущем устройстве (ПК, телефон)"
msgid "Its must be router!"
msgstr "Это должен быть роутер!"
msgid "Global check"
msgstr "Глобальная проверка"
msgid "Starting lists update..."
msgstr "Начало обновления списков..."
msgid "DNS check passed"
msgstr "Проверка DNS пройдена"
msgid "DNS check failed after 60 attempts"
msgstr "Проверка DNS не удалась после 60 попыток"
msgid "GitHub connection check passed"
msgstr "Проверка подключения к GitHub пройдена"
msgid "GitHub connection check passed (via proxy)"
msgstr "Проверка подключения к GitHub пройдена (через прокси)"
msgid "GitHub connection check failed after 60 attempts"
msgstr "Проверка подключения к GitHub не удалась после 60 попыток"
msgid "Downloading and processing lists..."
msgstr "Загрузка и обработка списков..."
msgid "Lists update completed successfully"
msgstr "Обновление списков успешно завершено"
msgid "Lists update failed"
msgstr "Обновление списков не удалось"
msgid "Error: "
msgstr "Ошибка: "

View File

@@ -1169,4 +1169,61 @@ msgid "available"
msgstr ""
msgid "unavailable"
msgstr ""
msgid "PODKOP CONFIGURATION"
msgstr ""
msgid "FAKEIP ROUTER TEST"
msgstr ""
msgid "FAKEIP BROWSER TEST"
msgstr ""
msgid "FakeIP is working correctly on router (198.18.x.x)"
msgstr ""
msgid "Click here for all the info"
msgstr ""
msgid "Check DNS server on current device (PC, phone)"
msgstr ""
msgid "Its must be router!"
msgstr ""
msgid "Global check"
msgstr ""
msgid "Starting lists update..."
msgstr ""
msgid "DNS check passed"
msgstr ""
msgid "DNS check failed after 60 attempts"
msgstr ""
msgid "GitHub connection check passed"
msgstr ""
msgid "GitHub connection check passed (via proxy)"
msgstr ""
msgid "GitHub connection check failed after 60 attempts"
msgstr ""
msgid "Downloading and processing lists..."
msgstr ""
msgid "Lists update completed successfully"
msgstr ""
msgid "Lists update failed"
msgstr ""
msgid "Loading..."
msgstr ""
msgid "Error: "
msgstr ""

View File

@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=podkop
PKG_VERSION:=0.3.39
PKG_VERSION:=0.4.4
PKG_RELEASE:=1
PKG_MAINTAINER:=ITDog <podkop@itdog.info>
@@ -15,7 +15,7 @@ define Package/podkop
DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64
CONFLICTS:=https-dns-proxy
TITLE:=Domain routing app
URL:=https://itdog.info
URL:=https://podkop.net
PKGARCH:=all
endef

View File

@@ -38,4 +38,5 @@ config main 'main'
list iface 'br-lan'
option mon_restart_ifaces '0'
#list restart_ifaces 'wan'
option ss_uot '0'
option ss_uot '0'
option detour '0'

View File

@@ -15,34 +15,42 @@ SUBNETS_TWITTER="${GITHUB_RAW_URL}/Subnets/IPv4/twitter.lst"
SUBNETS_META="${GITHUB_RAW_URL}/Subnets/IPv4/meta.lst"
SUBNETS_DISCORD="${GITHUB_RAW_URL}/Subnets/IPv4/discord.lst"
SUBNETS_TELERAM="${GITHUB_RAW_URL}/Subnets/IPv4/telegram.lst"
SUBNETS_CLOUDFLARE="${GITHUB_RAW_URL}/Subnets/IPv4/cloudflare.lst"
SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst"
SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst"
SING_BOX_CONFIG="/etc/sing-box/config.json"
FAKEIP="198.18.0.0/15"
VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube discord meta twitter hdrezka tiktok telegram"
VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube discord meta twitter hdrezka tiktok telegram cloudflare google_ai google_play hetzner ovh"
DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8"
TEST_DOMAIN="fakeip.tech-domain.club"
TEST_DOMAIN="fakeip.podkop.fyi"
INTERFACES_LIST=""
SRC_INTERFACE=""
RESOLV_CONF="/etc/resolv.conf"
CLOUDFLARE_OCTETS="103.21 103.22 103.31 104.16 104.17 104.18 104.19 104.20 104.21 104.22 104.23 104.24 104.25 104.26 104.27 104.28 108.162 131.0 141.101 162.158 162.159 172.64 172.65 172.66 172.67 172.68 172.69 172.70 172.71 173.245 188.114 190.93 197.234 198.41"
# Color constants
COLOR_CYAN="\033[0;36m"
COLOR_GREEN="\033[0;32m"
COLOR_RESET="\033[0m"
log() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local CYAN="\033[0;36m"
local GREEN="\033[0;32m"
local RESET="\033[0m"
echo -e "${CYAN}[$timestamp]${RESET} ${GREEN}$message${RESET}"
logger -t "podkop" "$timestamp $message"
}
nolog() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local CYAN="\033[0;36m"
local GREEN="\033[0;32m"
local RESET="\033[0m"
echo -e "${CYAN}[$timestamp]${RESET} ${GREEN}$message${RESET}"
echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
}
echolog() {
local message="$1"
log "$message"
nolog "$message"
}
start_main() {
@@ -62,14 +70,17 @@ start_main() {
fi
if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
log "[critical] Detected https-dns-proxy. Disable or uninstall it for correct functionality."
log "[critical] Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp"
fi
migration
config_foreach process_validate_service
sleep 3
# Sync time for DoH/DoT
/usr/sbin/ntpd -q -p 194.190.168.1 -p 216.239.35.0 -p 216.239.35.4 -p 162.159.200.1 -p 162.159.200.123
sleep 2
mkdir -p /tmp/podkop
@@ -130,6 +141,12 @@ start_main() {
sing_box_quic_reject
fi
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
log "Detour mixed enable"
detour_mixed
fi
sing_box_config_check
/etc/init.d/sing-box start
#/etc/init.d/sing-box enable
@@ -156,7 +173,7 @@ stop_main() {
if [ -f /var/run/podkop_list_update.pid ]; then
pid=$(cat /var/run/podkop_list_update.pid)
if kill -0 "$pid"; then
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null
log "Stopped list_update"
fi
@@ -202,6 +219,12 @@ reload() {
start_main
}
restart() {
log "Podkop restart"
stop
start
}
# Migrations and validation funcs
migration() {
# list migrate
@@ -365,8 +388,6 @@ save_dnsmasq_config() {
dnsmasq_add_resolver() {
log "Save dnsmasq config"
save_dnsmasq_config "dhcp.@dnsmasq[0].noresolv" "dhcp.@dnsmasq[0].podkop_noresolv"
save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize"
uci -q delete dhcp.@dnsmasq[0].podkop_server
for server in $(uci get dhcp.@dnsmasq[0].server 2>/dev/null); do
@@ -378,6 +399,9 @@ dnsmasq_add_resolver() {
fi
done
save_dnsmasq_config "dhcp.@dnsmasq[0].noresolv" "dhcp.@dnsmasq[0].podkop_noresolv"
save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize"
log "Configure dnsmasq for sing-box"
uci set dhcp.@dnsmasq[0].noresolv="1"
uci set dhcp.@dnsmasq[0].cachesize="0"
@@ -408,11 +432,11 @@ dnsmasq_restore() {
local server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)
if [[ "$server" == "127.0.0.42" ]]; then
uci -q delete dhcp.@dnsmasq[0].server
uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null
for server in $(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null); do
uci add_list dhcp.@dnsmasq[0].server="$server"
done
uci delete dhcp.@dnsmasq[0].podkop_server
uci delete dhcp.@dnsmasq[0].podkop_server 2>/dev/null
fi
uci delete dhcp.@dnsmasq[0].podkop_cachesize
@@ -543,13 +567,13 @@ prepare_custom_ruleset() {
}
list_update() {
log "Update remote lists"
echolog "🔄 Starting lists update..."
local i
for i in $(seq 1 60); do
if nslookup -timeout=1 openwrt.org >/dev/null 2>&1; then
log "DNS is available"
echolog "DNS check passed"
break
fi
log "DNS is unavailable [$i/60]"
@@ -557,27 +581,44 @@ list_update() {
done
if [ "$i" -eq 60 ]; then
log "Error: DNS check failed after 10 attempts"
echolog " DNS check failed after 60 attempts"
return 1
fi
for i in $(seq 1 60); do
if curl -s -m 3 https://github.com >/dev/null; then
log "GitHub is available"
break
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com >/dev/null; then
echolog "✅ GitHub connection check passed (via proxy)"
break
fi
else
if curl -s -m 3 https://github.com >/dev/null; then
echolog "✅ GitHub connection check passed"
break
fi
fi
log "GitHub is unavailable [$i/60]"
echolog "GitHub is unavailable [$i/60]"
sleep 3
done
if [ "$i" -eq 60 ]; then
log "Error: Cannot connect to GitHub after 10 attempts"
echolog "❌ GitHub connection check failed after 60 attempts"
return 1
fi
echolog "📥 Downloading and processing lists..."
config_foreach process_remote_ruleset_subnet
config_foreach process_domains_list_url
config_foreach process_subnet_for_section_remote
if [ $? -eq 0 ]; then
echolog "✅ Lists update completed successfully"
else
echolog "❌ Lists update failed"
fi
}
find_working_resolver() {
@@ -756,6 +797,7 @@ sing_box_dns() {
--arg fakeip "$FAKEIP" \
'.dns = {
"strategy": "ipv4_only",
"independent_cache": true,
"fakeip": {
"enabled": true,
"inet4_range": $fakeip
@@ -774,7 +816,7 @@ sing_box_create_bypass_ruleset() {
"rules": [
{
"domain_suffix": [
"ip.tech-domain.club"
"ip.podkop.fyi"
]
}
]
@@ -920,6 +962,9 @@ sing_box_outdound() {
fi
fi
;;
"block")
log "Block mode"
;;
*)
log "Requires *vpn* or *proxy* value"
return
@@ -1010,7 +1055,7 @@ sing_box_config_outbound_json() {
if [ $? -eq 0 ]; then
log "Outbound config updated successfully"
else
log "Error: Invalid JSON config generated"
log "Error: Outbound invalid JSON config generated"
return 1
fi
}
@@ -1071,9 +1116,9 @@ sing_box_config_shadowsocks() {
)' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
if [ $? -eq 0 ]; then
log "Config updated successfully"
log "Config Shadowsocks updated successfully"
else
log "Error: Invalid JSON config generated"
log "Error: Shadowsocks invalid JSON config generated"
return 1
fi
}
@@ -1198,10 +1243,10 @@ sing_box_config_vless() {
if [ $? -eq 0 ]; then
log "Config created successfully"
log "Config VLESS created successfully"
else
log "Error: Invalid JSON config generated"
return 1
log "[critical] Error: VLESS invalid JSON config generated"
exit 1
fi
}
@@ -1354,6 +1399,7 @@ sing_box_ruleset_remote() {
local tag=$1
local type=$2
local update_interval=$3
local detour=$4
url="$SRS_MAIN_URL/$tag.srs"
@@ -1369,15 +1415,19 @@ sing_box_ruleset_remote() {
--arg type "$type" \
--arg url "$url" \
--arg update_interval "$update_interval" \
--arg detour "$detour" \
'
.route.rule_set += [
{
"tag": $tag,
"type": $type,
"format": "binary",
"url": $url,
"update_interval": $update_interval
}
(
{
"tag": $tag,
"type": $type,
"format": "binary",
"url": $url,
"update_interval": $update_interval
} +
(if $detour == "1" then {"download_detour": "main"} else {} end)
)
]' "$SING_BOX_CONFIG" > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
log "Added new ruleset with tag $tag"
@@ -1398,6 +1448,15 @@ list_subnets_download() {
"telegram")
URL=$SUBNETS_TELERAM
;;
"cloudflare")
URL=$SUBNETS_CLOUDFLARE
;;
"hetzner")
URL=$SUBNETS_HETZNER
;;
"ovh")
URL=$SUBNETS_OVH
;;
"discord")
URL=$SUBNETS_DISCORD
nft add set inet $table podkop_discord_subnets { type ipv4_addr\; flags interval\; auto-merge\; }
@@ -1409,7 +1468,13 @@ list_subnets_download() {
esac
local filename=$(basename "$URL")
wget -O "/tmp/podkop/$filename" "$URL"
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "/tmp/podkop/$filename" "$URL"
else
wget -O "/tmp/podkop/$filename" "$URL"
fi
while IFS= read -r subnet; do
if [ "$service" = "discord" ]; then
@@ -1425,27 +1490,54 @@ sing_box_rules() {
local rule_set="$1"
local outbound="$2"
# Check if there is an outbound rule for "tproxy-in"
local rule_exists=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .inbound == ["tproxy-in"])' "$SING_BOX_CONFIG")
config_get mode "$section" "mode"
if [[ -n "$rule_exists" ]]; then
# If a rule for tproxy-in exists, add a new rule_set to the existing rule
jq \
--arg rule_set "$rule_set" \
--arg outbound "$outbound" \
'(.route.rules[] | select(.outbound == $outbound and .inbound == ["tproxy-in"]) .rule_set) += [$rule_set]' \
"$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
if [[ "$mode" == "block" ]]; then
# Action reject
# Check if there is an rule with reject"
local rule_exists=$(jq -r '.route.rules[] | select(.inbound == ["tproxy-in"] and .action == "reject")' "$SING_BOX_CONFIG")
if [[ -n "$rule_exists" ]]; then
# If a rule for rejectexists, add a new rule_set to the existing rule
jq \
--arg rule_set "$rule_set" \
'(.route.rules[] | select(.inbound == ["tproxy-in"] and .action == "reject") .rule_set) += [$rule_set]' \
"$SING_BOX_CONFIG" > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
else
# If there is no rule for reject, create a new one with rule_set
jq \
--arg rule_set "$rule_set" \
'.route.rules += [{
"inbound": ["tproxy-in"],
"rule_set": [$rule_set],
"action": "reject"
}]' "$SING_BOX_CONFIG" > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
fi
return
else
# If there is no rule for tproxy-in, create a new one with rule_set
jq \
--arg rule_set "$rule_set" \
--arg outbound "$outbound" \
'.route.rules += [{
"inbound": ["tproxy-in"],
"rule_set": [$rule_set],
"outbound": $outbound,
"action": "route"
}]' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
# Action route
# Check if there is an outbound rule for "tproxy-in"
local rule_exists=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .inbound == ["tproxy-in"])' "$SING_BOX_CONFIG")
if [[ -n "$rule_exists" ]]; then
# If a rule for tproxy-in exists, add a new rule_set to the existing rule
jq \
--arg rule_set "$rule_set" \
--arg outbound "$outbound" \
'(.route.rules[] | select(.outbound == $outbound and .inbound == ["tproxy-in"]) .rule_set) += [$rule_set]' \
"$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
else
# If there is no rule for tproxy-in, create a new one with rule_set
jq \
--arg rule_set "$rule_set" \
--arg outbound "$outbound" \
'.route.rules += [{
"inbound": ["tproxy-in"],
"rule_set": [$rule_set],
"outbound": $outbound,
"action": "route"
}]' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
fi
fi
}
@@ -1471,8 +1563,9 @@ sing_box_quic_reject() {
process_remote_ruleset_srs() {
config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
if [ "$domain_list_enabled" -eq 1 ]; then
config_get_bool detour "main" "detour" "0"
log "Adding a srs list for $section"
config_list_foreach "$section" domain_list "sing_box_ruleset_remote" "remote" "1d"
config_list_foreach "$section" domain_list "sing_box_ruleset_remote" "remote" "1d" "$detour"
fi
}
@@ -1538,13 +1631,24 @@ list_custom_url_domains_create() {
local section="$2"
local URL="$1"
local filename=$(basename "$URL")
local filepath="/tmp/podkop/${filename}"
wget -q -O "/tmp/podkop/${filename}" "$URL"
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "$filepath" "$URL"
else
wget -O "$filepath" "$URL"
fi
if grep -q $'\r' "$filepath"; then
log "$filename has Windows line endings (CRLF). Converting to Unix (LF)"
sed -i 's/\r$//' "$filepath"
fi
while IFS= read -r domain; do
log "From downloaded file: $domain"
sing_box_ruleset_domains_json $domain $section
done <"/tmp/podkop/$filename"
done <"$filepath"
}
process_domains_list_url() {
@@ -1578,14 +1682,25 @@ list_custom_url_subnets_create() {
local section="$2"
local URL="$1"
local filename=$(basename "$URL")
local filepath="/tmp/podkop/${filename}"
wget -q -O "/tmp/podkop/${filename}" "$URL"
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "$filepath" "$URL"
else
wget -O "$filepath" "$URL"
fi
if grep -q $'\r' "$filepath"; then
log "$filename has Windows line endings (CRLF). Converting to Unix (LF)"
sed -i 's/\r$//' "$filepath"
fi
while IFS= read -r subnet; do
log "From local file: $subnet"
sing_box_ruleset_subnets_json $subnet $section
nft add element inet PodkopTable podkop_subnets { $subnet }
done <"/tmp/podkop/$filename"
done <"$filepath"
}
process_subnet_for_section_remote() {
@@ -1638,6 +1753,31 @@ sing_box_rules_source_ip_cidr() {
fi
}
detour_mixed() {
local section="main"
local port="4534"
local tag="detour"
log "Adding detour Socks5 for $section on port $port"
jq \
--arg tag "$tag" \
--arg port "$port" \
--arg section "$section" \
'.inbounds += [{
"tag": $tag,
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": ($port|tonumber),
"set_system_proxy": false
}] |
.route.rules += [{
"inbound": [$tag],
"outbound": $section,
"action": "route"
}]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
}
## nftables
list_all_traffic_from_ip() {
local ip="$1"
@@ -1747,16 +1887,66 @@ check_nft() {
# Check if table exists
if ! nft list table inet PodkopTable >/dev/null 2>&1; then
nolog "PodkopTable not found"
nolog "PodkopTable not found"
return 1
fi
local found_hetzner=0
local found_ovh=0
check_domain_list_contains() {
local section="$1"
config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
if [ "$domain_list_enabled" -eq 1 ]; then
config_list_foreach "$section" "domain_list" check_domain_value
fi
}
check_domain_value() {
local domain_value="$1"
if [ "$domain_value" = "hetzner" ]; then
found_hetzner=1
elif [ "$domain_value" = "ovh" ]; then
found_ovh=1
fi
}
config_foreach check_domain_list_contains
if [ "$found_hetzner" -eq 1 ] || [ "$found_ovh" -eq 1 ]; then
# Get all sets
nolog "\nSets configuration:"
local sets="podkop_subnets podkop_domains interfaces podkop_discord_subnets localv4"
nolog "Sets statistics:"
for set_name in $sets; do
if nft list set inet PodkopTable $set_name >/dev/null 2>&1; then
# Count elements using grep to count commas and add 1 (last element has no comma)
local count=$(nft list set inet PodkopTable $set_name 2>/dev/null | grep -o ',\|{' | wc -l)
echo "- $set_name: $count elements"
fi
done
nft list table inet PodkopTable
nolog "\nNFT check completed"
nolog "Chain configurations:"
# Create a temporary file for processing
local tmp_file=$(mktemp)
nft list table inet PodkopTable > "$tmp_file"
# Extract chain configurations without element listings
sed -n '/chain mangle {/,/}/p' "$tmp_file" | grep -v "elements" | grep -v "^[[:space:]]*[0-9]"
sed -n '/chain proxy {/,/}/p' "$tmp_file" | grep -v "elements" | grep -v "^[[:space:]]*[0-9]"
# Clean up
rm -f "$tmp_file"
else
# Simple view as originally implemented
nolog "Sets configuration:"
nft list table inet PodkopTable
fi
nolog "NFT check completed"
}
check_github() {
@@ -1772,7 +1962,14 @@ check_github() {
for url in "$DOMAINS_RU_INSIDE" "$DOMAINS_RU_OUTSIDE" "$DOMAINS_UA" "$DOMAINS_YOUTUBE" \
"$SUBNETS_TWITTER" "$SUBNETS_META" "$SUBNETS_DISCORD"; do
local list_name=$(basename "$url")
wget -q -O /dev/null "$url"
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -q -O /dev/null "$url"
else
wget -q -O /dev/null "$url"
fi
if [ $? -eq 0 ]; then
nolog "- $list_name: available"
else
@@ -1949,9 +2146,7 @@ show_sing_box_config() {
)' "$SING_BOX_CONFIG"
}
show_config() {
nolog "📄 Current podkop configuration:"
show_config() {
if [ ! -f /etc/config/podkop ]; then
nolog "Configuration file not found"
return 1
@@ -1969,6 +2164,7 @@ show_config() {
-e 's/\(pbk=[^&]*\)/pbk=MASKED/g' \
-e 's/\(sid=[^&]*\)/sid=MASKED/g' \
-e 's/\(option dns_server '\''[^'\'']*\.dns\.nextdns\.io'\''\)/option dns_server '\''MASKED.dns.nextdns.io'\''/g' \
-e "s|\(option dns_server 'dns\.nextdns\.io\)/[^']*|\1/MASKED|"
> "$tmp_config"
cat "$tmp_config"
@@ -1976,17 +2172,17 @@ show_config() {
}
show_version() {
local version=$(opkg info podkop | grep -m 1 "Version:" | cut -d' ' -f2)
local version=$(opkg list-installed podkop | awk '{print $3}')
echo "$version"
}
show_luci_version() {
local version=$(opkg info luci-app-podkop | grep -m 1 "Version:" | cut -d' ' -f2)
local version=$(opkg list-installed luci-app-podkop | awk '{print $3}')
echo "$version"
}
show_sing_box_version() {
local version=$(opkg info sing-box | grep -m 1 "Version:" | cut -d' ' -f2)
local version=$(sing-box version | head -n 1 | awk '{print $3}')
echo "$version"
}
@@ -2041,36 +2237,18 @@ get_sing_box_status() {
}
get_status() {
local running=0
local enabled=0
local status=""
# Check if service is enabled
if [ -x /etc/rc.d/S99podkop ]; then
enabled=1
fi
# Check if service is running
if pgrep -f "sing-box" >/dev/null; then
running=1
fi
# Format status message
if [ $running -eq 1 ]; then
if [ $enabled -eq 1 ]; then
status="running & enabled"
else
status="running but disabled"
fi
status="enabled"
else
if [ $enabled -eq 1 ]; then
status="stopped but enabled"
else
status="stopped & disabled"
fi
status="disabled"
fi
echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\"}"
echo "{\"enabled\":$enabled,\"status\":\"$status\"}"
}
check_dns_available() {
@@ -2086,28 +2264,54 @@ check_dns_available() {
if echo "$dns_server" | grep -q "\.dns\.nextdns\.io$"; then
local nextdns_id=$(echo "$dns_server" | cut -d'.' -f1)
display_dns_server="$(echo "$nextdns_id" | sed 's/./*/g').dns.nextdns.io"
elif echo "$dns_server" | grep -q "^dns\.nextdns\.io/"; then
local masked_path=$(echo "$dns_server" | cut -d'/' -f2- | sed 's/./*/g')
display_dns_server="dns.nextdns.io/$masked_path"
fi
if [ "$dns_type" = "doh" ]; then
local result=""
if echo "$dns_server" | grep -q "quad9.net" || \
echo "$dns_server" | grep -qE "^9\.9\.9\.(9|10|11)$|^149\.112\.112\.(112|10|11)$|^2620:fe::(fe|9|10|11)$|^2620:fe::fe:(10|11)$"; then
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server:5053/dns-query?name=itdog.info&type=A")
# Generate random DNS query ID (2 bytes)
local random_id=$(head -c2 /dev/urandom | hexdump -ve '1/1 "%.2x"' 2>/dev/null)
if [ $? -ne 0 ]; then
error_message="Failed to generate random ID"
status="internal error"
else
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/dns-query?name=itdog.info&type=A")
if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then
is_available=1
status="available"
# Create DNS wire format query for google.com A record with random ID
local dns_query=$(printf "\x${random_id:0:2}\x${random_id:2:2}\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01" | base64 2>/dev/null)
if [ $? -ne 0 ]; then
error_message="Failed to generate DNS query"
status="internal error"
else
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/resolve?name=itdog.info&type=A")
# Try POST method first (RFC 8484 compliant) with shorter timeout
local result=$(echo "$dns_query" | base64 -d 2>/dev/null | curl -H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @- \
--max-time 2 \
--connect-timeout 1 \
-s \
"https://$dns_server/dns-query" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
is_available=1
status="available"
else
# Try GET method as fallback with shorter timeout
local dns_query_no_padding=$(echo "$dns_query" | tr -d '=' 2>/dev/null)
result=$(curl -H "accept: application/dns-message" \
--max-time 2 \
--connect-timeout 1 \
-s \
"https://$dns_server/dns-query?dns=$dns_query_no_padding" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
is_available=1
status="available"
else
error_message="DoH server not responding"
fi
fi
fi
fi
if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then
is_available=1
status="available"
fi
elif [ "$dns_type" = "dot" ]; then
(nc "$dns_server" 853 </dev/null >/dev/null 2>&1) & pid=$!
sleep 2
@@ -2163,106 +2367,182 @@ sing_box_add_secure_dns_probe_domain() {
log "DNS probe domain ${domain} configured with override to port ${override_port}"
}
global_check() {
nolog "Global check run!"
printf "\n"
show_config
printf "\n\n"
print_global() {
local message="$1"
echo "$message"
}
nolog "Checking fakeip functionality..."
nolog "Testing DNS resolution with default DNS server"
echo "=== Testing with default DNS server ==="
nslookup -timeout=2 $TEST_DOMAIN
printf "\n"
nolog "Finding a working DNS resolver..."
local working_resolver=$(find_working_resolver)
if [ -z "$working_resolver" ]; then
nolog "No working resolver found, skipping resolver check"
else
nolog "Using resolver: $working_resolver"
nolog "Testing DNS resolution with working resolver ($working_resolver)"
echo "=== Testing with working resolver ($working_resolver) ==="
nslookup -timeout=2 $TEST_DOMAIN $working_resolver
printf "\n"
fi
# Main FakeIP check
nolog "Testing DNS resolution for $TEST_DOMAIN using 127.0.0.42"
echo "=== Testing with FakeIP DNS (127.0.0.42) ==="
local result=$(nslookup -timeout=2 $TEST_DOMAIN 127.0.0.42 2>&1)
echo "$result"
if echo "$result" | grep -q "198.18"; then
nolog "✅ FakeIP is working correctly! Domain resolved to FakeIP range (198.18.x.x)"
else
nolog "❌ FakeIP test failed. Domain did not resolve to FakeIP range"
nolog "Checking if sing-box is running..."
if ! pgrep -f "sing-box" >/dev/null; then
nolog "sing-box is not running"
else
nolog "sing-box is running, but FakeIP might not be configured correctly"
nolog "Checking DNS configuration in sing-box..."
if [ -f "$SING_BOX_CONFIG" ]; then
local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SING_BOX_CONFIG")
local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SING_BOX_CONFIG")
nolog "FakeIP enabled: $fakeip_enabled"
nolog "FakeIP range: $fakeip_range"
local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SING_BOX_CONFIG")
nolog "FakeIP domain: $dns_rules"
else
nolog "sing-box config file not found"
fi
fi
fi
printf "\n"
global_check() {
print_global "📡 Global check run!"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🛠️ System info"
print_global "🕳️ Podkop: $(opkg list-installed podkop | awk '{print $3}')"
print_global "🕳️ LuCI App: $(opkg list-installed luci-app-podkop | awk '{print $3}')"
print_global "📦 Sing-box: $(sing-box version | head -n 1 | awk '{print $3}')"
print_global "🛜 OpenWrt: $(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)"
print_global "🛜 Device: $(cat /tmp/sysinfo/model)"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "📄 Podkop config"
show_config
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🔧 System check"
if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then
nolog "❌ /etc/resolv.conf contains an external nameserver:"
print_global "❌ /etc/resolv.conf contains external nameserver:"
cat /etc/resolv.conf
echo ""
else
nolog "✅ /etc/resolv.conf OK"
print_global "✅ /etc/resolv.conf"
fi
nolog "Checking dnsmasq configuration..."
cachesize="$(uci get dhcp.@dnsmasq[0].cachesize 2>/dev/null)"
noresolv="$(uci get dhcp.@dnsmasq[0].noresolv 2>/dev/null)"
server="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
if [ "$cachesize" != "0" ] || [ "$noresolv" != "1" ] || [ "$server" != "127.0.0.42" ]; then
nolog "❌ The configuration differs from the template. 📄 DHCP config:"
print_global "❌ DHCP configuration differs from template. 📄 DHCP config:"
awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
elif [ "$(uci get podkop.main.dont_touch_dhcp 2>/dev/null)" = "1" ]; then
print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:"
awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
else
nolog "✅ /etc/config/dhcp"
print_global "✅ /etc/config/dhcp"
fi
if ! pgrep -f "sing-box" >/dev/null; then
nolog "❌ sing-box is not running"
print_global "❌ sing-box is not running"
else
nolog "✅ sing-box is running"
print_global "✅ sing-box is running"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🧱 NFT table"
check_nft
if ! command -v nft >/dev/null 2>&1; then
nolog "nft is not installed"
return 1
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "📄 WAN config"
if uci show network.wan >/dev/null 2>&1; then
awk '
/^config / {
p = ($2 == "interface" && $3 == "'\''wan'\''")
proto = ""
}
p {
if ($1 == "option" && $2 == "proto") {
proto = $3
print
} else if (proto == "'\''static'\''" && $1 == "option" && ($2 == "ipaddr" || $2 == "netmask" || $2 == "gateway")) {
print " option", $2, "'\''******'\''"
} else if (proto == "'\''pppoe'\''" && $1 == "option" && ($2 == "username" || $2 == "password")) {
print " option", $2, "'\''******'\''"
} else {
print
}
}
' /etc/config/network
else
print_global "❌ WAN configuration not found"
fi
nolog "📄 NFT Table Podkop"
if uci show network | grep -q endpoint_host; then
uci show network | grep endpoint_host | cut -d'=' -f2 | tr -d "'\" " | while read -r host; do
if [ "$host" = "engage.cloudflareclient.com" ]; then
print_global "⚠️ WARP detected: $host"
continue
fi
ip_prefix=$(echo "$host" | cut -d'.' -f1,2)
if echo "$CLOUDFLARE_OCTETS" | grep -wq "$ip_prefix"; then
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "⚠️ WARP detected: $host"
fi
done
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "➡️ DNS status"
dns_info=$(check_dns_available)
dns_type=$(echo "$dns_info" | jq -r '.dns_type')
dns_server=$(echo "$dns_info" | jq -r '.dns_server')
status=$(echo "$dns_info" | jq -r '.status')
print_global "$dns_type ($dns_server) is $status"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🔁 FakeIP"
print_global "➡️ DNS resolution: system DNS server"
nslookup -timeout=2 $TEST_DOMAIN
# Check if table exists
if ! nft list table inet PodkopTable >/dev/null 2>&1; then
nolog "PodkopTable not found"
else
nft list table inet PodkopTable
local working_resolver=$(find_working_resolver)
if [ -z "$working_resolver" ]; then
print_global "❌ No working external resolver found"
else
print_global "➡️ DNS resolution: external resolver ($working_resolver)"
nslookup -timeout=2 $TEST_DOMAIN $working_resolver
fi
print_global "➡️ DNS resolution: sing-box DNS server (127.0.0.42)"
local result=$(nslookup -timeout=2 $TEST_DOMAIN 127.0.0.42 2>&1)
echo "$result"
if echo "$result" | grep -q "198.18"; then
print_global "✅ FakeIP is working correctly on router (198.18.x.x)"
else
print_global "❌ FakeIP test failed: Domain did not resolve to FakeIP range"
if ! pgrep -f "sing-box" >/dev/null; then
print_global " ❌ sing-box is not running"
else
print_global " 🤔 sing-box is running, checking configuration"
if [ -f "$SING_BOX_CONFIG" ]; then
local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SING_BOX_CONFIG")
local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SING_BOX_CONFIG")
local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SING_BOX_CONFIG")
print_global " 📦 FakeIP enabled: $fakeip_enabled"
print_global " 📦 FakeIP range: $fakeip_range"
print_global " 📦 FakeIP domain: $dns_rules"
else
print_global " ⛔ sing-box config file not found"
fi
fi
fi
}
show_help() {
cat << EOF
Usage: $0 COMMAND
Available commands:
start Start podkop service
stop Stop podkop service
reload Reload podkop configuration
restart Restart podkop service
enable Enable podkop autostart
disable Disable podkop autostart
main Run main podkop process
list_update Update domain lists
check_proxy Check proxy connectivity
check_nft Check NFT rules
check_github Check GitHub connectivity
check_logs Show podkop logs from system journal
check_sing_box_connections Show active sing-box connections
check_sing_box_logs Show sing-box logs
check_fakeip Check FakeIP DNS functionality
check_dnsmasq Check DNSMasq configuration
show_config Display current podkop configuration
show_version Show podkop version
show_sing_box_config Show sing-box configuration
show_luci_version Show LuCI app version
show_sing_box_version Show sing-box version
show_system_info Show system information
get_status Get podkop service status
get_sing_box_status Get sing-box service status
check_dns_available Check DNS server availability
global_check Run global system check
EOF
}
case "$1" in
@@ -2275,6 +2555,9 @@ case "$1" in
reload)
reload
;;
restart)
restart
;;
main)
main
;;
@@ -2336,7 +2619,7 @@ case "$1" in
global_check
;;
*)
echo "Usage: $0 {start|stop|reload|enable|disable|main|list_update|check_proxy|check_nft|check_github|check_logs|check_sing_box_connections|check_sing_box_logs|check_fakeip|check_dnsmasq|show_config|show_version|show_sing_box_config|show_luci_version|show_sing_box_version|show_system_info|get_status|get_sing_box_status|check_dns_available|global_check}"
show_help
exit 1
;;
esac