Compare commits

..

72 Commits

Author SHA1 Message Date
Kirill Sobakin
f722a513d0 Merge pull request #163 from itdoginfo/fix
fix: Use correct variable for detour service address
2025-09-15 17:27:15 +03:00
Andrey Petelin
a71707f174 fix: Use correct variable for detour service address 2025-09-15 19:22:52 +05:00
Kirill Sobakin
983f05345b Merge pull request #161 from itdoginfo/refactoring
Refactoring
2025-09-15 15:52:22 +03:00
Andrey Petelin
ee246895de fix: Redirect base64 decode errors to /dev/null 2025-09-15 17:41:17 +05:00
Andrey Petelin
27719f90ee feat: Add support for DoH URLs with paths and UDP port specification 2025-09-14 17:51:20 +05:00
Andrey Petelin
4a17cf66a3 refactor: Add file existence checks and improve startup reliability 2025-09-14 09:26:26 +05:00
Andrey Petelin
db956452d1 refactor: Move logging functions to library file 2025-09-14 09:10:42 +05:00
Andrey Petelin
4897d3d292 refactor: Split nftables rules for TCP and UDP protocols separately 2025-09-14 08:55:34 +05:00
itdoginfo
0aa0a4a9c8 Add /usr/lib/podkop path 2025-09-13 19:23:09 +03:00
Andrey Petelin
7d082c5def chore: Update README task status from done to pending 2025-09-12 14:05:11 +05:00
Andrey Petelin
8845749517 chore: Update README.md to mark completed tasks 2025-09-12 14:03:32 +05:00
Andrey Petelin
054ed355cf fix: Add packet_encoding support for VLESS outbound configuration 2025-09-11 17:52:47 +05:00
Andrey Petelin
304c57edfa fix: Fix translation for "Local Subnet List Paths" 2025-09-11 17:04:22 +05:00
Andrey Petelin
8dd33cdde2 fix: Remove non-existent filename variable 2025-09-11 17:01:39 +05:00
Andrey Petelin
3d3fbe3bfb fix: Fix variable name in HTTP proxy check for wget command 2025-09-11 16:58:40 +05:00
Andrey Petelin
427ea3bc9a fix: Remove unused server_address variable from DNS configuration 2025-09-11 16:56:30 +05:00
Andrey Petelin
a7f6a993ac chore: shfmt formatting 2025-09-11 16:40:06 +05:00
Andrey Petelin
074c1a9349 chore: Remove TODO comments from user domains and subnets text input fields 2025-09-11 15:32:39 +05:00
Andrey Petelin
b6a6db71a8 refactor: Implement user domain and subnet list handling 2025-09-11 15:31:21 +05:00
Andrey Petelin
38fcb59ed7 fix: Reload config after commit to ensure runtime consistency 2025-09-11 13:37:49 +05:00
Andrey Petelin
5a2ffcfd38 refactor: Add graceful shutdown handling for dnsmasq reconfiguration 2025-09-11 11:43:58 +05:00
Andrey Petelin
49f12b212d chore: Update required sing-box version to 1.12.0 2025-09-10 19:44:31 +05:00
Andrey Petelin
489c61baa2 refactor: Move constants to constants.sh 2025-09-10 19:40:12 +05:00
Andrey Petelin
d4b5431db4 refactor: Refactor nft rules to use named sets for interfaces and localv4 2025-09-10 17:52:14 +05:00
Andrey Petelin
d0ea39abd0 refactor: Rename TEST_DOMAIN variable to FAKEIP_TEST_DOMAIN 2025-09-10 15:39:23 +05:00
Andrey Petelin
d4e754d2eb refactor: Rename FAKEIP constant to SB_FAKEIP_INET4_RANG 2025-09-10 14:14:15 +05:00
Andrey Petelin
82f9ae4c6a refactor: Remove redundant FakeIP config verification 2025-09-10 14:10:17 +05:00
Andrey Petelin
775b0073d3 refactor: Fixed nft rule for routing tagged traffic to localhost tproxy 2025-09-10 13:33:07 +05:00
Andrey Petelin
b477a8abc0 fix: Assign domain resolver tag correctly to DNS servers 2025-09-09 16:30:11 +05:00
Andrey Petelin
81e0c86060 refactor: Add block section support 2025-09-09 15:51:09 +05:00
Andrey Petelin
191522f396 chore: Rename download_to_tempfile to download_to_file for clarity 2025-09-09 11:48:03 +05:00
Andrey Petelin
79cea7a31a chore: remove .shellcheckrc file 2025-09-09 11:20:19 +05:00
Andrey Petelin
6c094aceae fix: Ensure unique values when patching local source ruleset 2025-09-09 11:15:29 +05:00
Andrey Petelin
1e8c2b50f7 chore: Rename NFT_GENERAL_SET_NAME to NFT_COMMON_SET_NAME 2025-09-08 23:10:53 +05:00
Andrey Petelin
27d2366208 chore: Remove list_update refactor TODO 2025-09-08 23:03:28 +05:00
Andrey Petelin
c1133827a2 fix: Pass HTTP proxy address to download functions for remote subnet imports 2025-09-08 23:00:26 +05:00
Andrey Petelin
a187192a88 chore: Move and rename _get_download_detour_tag to get_download_detour_tag 2025-09-08 22:55:42 +05:00
Andrey Petelin
fe30cf9e55 refactor: Refactoring list_update function 2025-09-08 22:43:27 +05:00
Andrey Petelin
9496a88774 refactor: Add ip addresses to nft set for local ruleset handling 2025-09-08 10:46:29 +05:00
Andrey Petelin
f54e92cd7a fix: Update config key from cache_file to cache_path in cache file configuration 2025-09-08 10:38:49 +05:00
Andrey Petelin
d70a04b144 refactor: Improve dnsmasq configuration logic for DNS handling 2025-09-07 18:11:46 +05:00
Andrey Petelin
e5be9c3fd1 refactor: Avoid unnecessary sing-box config writes by comparing hashes before saving (#128) 2025-09-07 12:45:27 +05:00
Andrey Petelin
9762b9cca4 refactor: Remove unused functions 2025-09-07 12:14:02 +05:00
Andrey Petelin
9d861cf3e0 feat: Add sing-box config path option (#128) 2025-09-07 12:12:08 +05:00
Andrey Petelin
49836e4adc feat: Add local subnet lists support with UI and backend integration (#156) 2025-09-05 21:23:55 +05:00
Andrey Petelin
5273935d25 chore: fix my perfect English 2025-09-05 17:01:36 +05:00
Andrey Petelin
d03167f49d chore: fix my perfect English 2025-09-05 16:49:05 +05:00
Andrey Petelin
da89c5c7df refactor: rename parameters for migration 2025-09-05 15:02:24 +05:00
Andrey Petelin
acfc95e86d refactor: Refactoring configuration of local domain lists 2025-09-05 14:50:43 +05:00
Andrey Petelin
17c1d09aa8 refactor: Enable cron job scheduling 2025-09-04 20:11:16 +05:00
Andrey Petelin
c7e21010bd refactor: Add download helper functions to helpers.sh and remove from main script 2025-09-04 20:09:44 +05:00
Andrey Petelin
f70e2ac557 refactor: Simplify cron job logic and renaming variable 2025-09-04 20:02:43 +05:00
Andrey Petelin
cb4e3036be chore: Remove module logging from log function 2025-09-04 18:00:28 +05:00
Andrey Petelin
12fc6bd9ac chore: undo renaming of taboption classarg 2025-09-04 16:43:39 +05:00
Andrey Petelin
2794cad533 fix: Remove direct outbound from DNS server configuration to prevent invalid detour 2025-09-04 12:35:53 +05:00
Andrey Petelin
9b182a3045 fix: fix detour parameter for remote ruleset 2025-09-04 12:28:38 +05:00
Andrey Petelin
f07d90a524 refactor: intermediate refactoring commit 2025-09-04 12:10:05 +05:00
Andrey Petelin
75fc377c22 Merge branch 'main' into refactoring 2025-09-04 11:52:52 +05:00
itdoginfo
33ecb771f9 Fix UDP over TCP for extra section 2025-09-02 14:44:14 +03:00
itdoginfo
86038e2756 Delay setting option 2025-09-02 14:09:35 +03:00
Andrey Petelin
db91c628c8 Merge remote-tracking branch 'origin/refactoring' into refactoring
# Conflicts:
#	podkop/files/usr/lib/sing_box_config_manager.sh
2025-08-31 20:25:16 +05:00
Andrey Petelin
41ce41945c feat: Add domain_resolver and detour parameters to DNS server configurations in sing-box manager script 2025-08-31 20:22:57 +05:00
Andrey Petelin
2753a44440 feat: Add domain_resolver parameter to DNS server configurations in sing-box manager script 2025-08-31 19:43:21 +05:00
Andrey Petelin
cd1a4e2a8e chore: update example links with valid values 2025-08-31 13:19:04 +05:00
Andrey Petelin
7e041da8c6 feat: add sing-box configuration manager script and jq helpers 2025-08-31 13:09:55 +05:00
itdoginfo
f3f5bca555 Add HODCA, CloudFront, DO 2025-08-29 15:02:43 +03:00
itdoginfo
174f16bc76 Update 2025-08-28 19:11:37 +03:00
itdoginfo
7c63a35faa Old value #131 2025-08-28 18:40:53 +03:00
itdoginfo
86a86df982 Fix template 2025-08-28 18:12:35 +03:00
itdoginfo
ac445bc227 Fix template config 2025-08-28 18:08:10 +03:00
itdoginfo
4398e6885b Add template for issues and PR 2025-08-28 18:06:08 +03:00
itdoginfo
9974b42cc2 NFT: output chain for traffic from the router 2025-08-27 00:31:56 +03:00
24 changed files with 3477 additions and 2042 deletions

74
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
---
name: 🐛 Сообщение об ошибке
description: Создавайте только, если проблема точно не на вашей стороне.
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
Спасибо за создание отчета об ошибке!
Перед отправкой, пожалуйста:
- Проверьте [существующие issues](https://github.com/itdoginfo/podkop/issues)
- Просмотрите [документацию](https://podkop.net)
- type: textarea
id: description
attributes:
label: 📝 Описание проблемы
description: Четкое и краткое описание того, что не работает
placeholder: Опишите проблему
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Шаги для воспроизведения
description: Шаги для воспроизведения проблемы. Если вы настраваете что-то по мануалу, приложите ссылку на него.
placeholder: |
1.
2.
3.
4.
validations:
required: true
- type: textarea
id: expected
attributes:
label: ✅ Ожидаемое поведение
description: Четкое и краткое описание того, что должно было произойти
placeholder: Опишите ожидаемое поведение
validations:
required: true
- type: textarea
id: environment
attributes:
label: 🖥️ Информация о системе
description: |
Информация о вашей системе (заполните всё применимое)
value: |
- **OpenWrt версия**:
- **Podkop версия**:
- **Роутер модель**:
- **Sing-box версия**:
render: markdown
validations:
required: true
- type: textarea
id: config
attributes:
label: ⚙️ Конфигурация
description: |
Релевантные части конфигурации (удалите чувствительную информацию!)
placeholder: |
Например:
- Содержимое /etc/config/podkop
- Конфигурация sing-box (если релевантно)
- Дополнительные конфиги, которые потребуются wireless/network/dhcp и т.д.
render: shell

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Если у вас что-то не работает, прежде всего прочитайте README проекта
url: https://github.com/itdoginfo/podkop
about: README проекта
- name: 📚 Если вы не нашли в README документацию, то вот ссылка на неё
url: https://podkop.net
about: Официальная документация PodKop

View File

@@ -0,0 +1,68 @@
---
name: ✨ Запрос новой функции
description: Предложите новую функцию или улучшение для Podkop
title: "[FEATURE] "
labels: ["enhancement", "needs-discussion"]
assignees: []
body:
- type: markdown
attributes:
value: |
Спасибо за предложение новой функции!
Перед отправкой, пожалуйста:
- Проверьте [существующие запросы](https://github.com/itdoginfo/podkop/issues?q=is%3Aissue+label%3Aenhancement)
- Убедитесь, что функции не существует в [документации](https://podkop.net)
- type: textarea
id: summary
attributes:
label: Краткое описание
description: Краткое описание предлагаемой функции
placeholder: В одном предложении опишите, что вы хотите добавить...
validations:
required: true
- type: textarea
id: problem
attributes:
label: Проблема, которую решает
description: |
Описание проблемы или неудобства, которое решит эта функция
placeholder: |
Сейчас нет возможности [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: 💡 Предлагаемое решение
description: Четкое и краткое описание того, что вы хотите реализовать
placeholder: |
Я хочу, чтобы Podkop мог [...]
Предлагаю добавить функцию, которая [...]
Можно было бы улучшить [...] путем [...]
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Workaround
description: |
Опишите альтернативные решения или функции, которые вы рассматривали
Есть ли обходные пути, которые вы используете сейчас?
placeholder: |
Сейчас я решаю это проблему путем [...]
Альтернативой могло бы быть [...]
Пробовал использовать [...], но это не подходит потому что [...]
- type: textarea
id: implementation
attributes:
label: Идеи реализации (опционально)
description: |
Если у вас есть идеи о том, как это можно реализовать, поделитесь ими. Помните про ограничения LuCI.
placeholder: |
Это можно реализовать с помощью [...]

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
# Описание изменений
Краткое описание ваших изменений и их цель.
## Что изменено
Детальное описание изменений:
-
-
-
(Этим вы экономите время ревьювера)

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

View File

@@ -1 +0,0 @@
disable=SC3036,SC3010,SC3014,SC3015,SC3020,SC3003

View File

@@ -29,18 +29,21 @@ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/mai
## Рефактор
- [ ] Очевидные повторения в `/usr/bin/podkop` загнать в переменые
- [ ] Возможно поменять структуру
- [x] Возможно поменять структуру
## Списки
- [ ] CloudFront
- [ ] DO
- [ ] HODCA
## Будущее
- [ ] Подписка. Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
- [ ] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables.
- [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме.
- [x] Поддержка Source format. Нужна расшифровка в json и если присуствуют подсети, заносить их в custom subnet nftset.
- [ ] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d.
- [x] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
- [x] Формирование конфига sing-box в /tmp
- [ ] Галочка, которая режет доступ к doh серверам.
- [ ] IPv6. Только после наполнения Wiki.

View File

@@ -20,11 +20,11 @@ ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Y21lZklCdDhwMTJaZm1QWUplMnNCNThRd3R3NXNKeVp
## Reality
```
vless://eb445f4b-ddb4-4c79-86d5-0833fc674379@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=yahoo.com&sid=6cabf01472a3&spx=%2F&flow=xtls-rprx-vision#vless-reality
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=sni.server.com&sid=6cabf01472a3&spx=%2F&flow=xtls-rprx-vision#vless-reality
```
```
vless://UUID@IP:2082?security=reality&sni=dash.cloudflare.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=pukkey&sid=id&type=grpc&encryption=none#vless-reality-strange
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@123.123.123.123:2082?security=reality&sni=sni.server.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&sid=6cabf01472a3&type=grpc&encryption=none#vless-reality-strange
```
## TLS
@@ -35,34 +35,34 @@ vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=t
2.
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?security=tls&sni=SITE&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
```
3.
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws
```
4.
```
vless://[someid]@[someserver]?security=tls&sni=[somesni]&type=ws&path=/?ed%3D2560&host=[somesni]&encryption=none#vless-tls-ws-2
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&type=ws&path=/?ed%3D2560&host=sni.server.com&encryption=none#vless-tls-ws-2
```
5.
```
vless://uuid@server:443?security=tls&sni=server&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3
```
6.
```
vless://33333@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=example.com&fp=chrome#vless-tls-ws-4
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws-4
```
7.
```
vless://id@sub.domain.example:443?type=ws&path=%2Fdir%2Fpath&host=sub.domain.example&security=tls#configname
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@sub.example.com:443?type=ws&path=%2Fdir%2Fpath&host=sub.example.com&security=tls#configname
```
## No security
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?type=tcp&security=none#vless-tls-no-encrypt
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=none#vless-tls-no-encrypt
```

View File

@@ -50,20 +50,10 @@ function createAdditionalSection(mainSection, network) {
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 ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
if (!domainRegex.test(value)) {
if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
@@ -97,20 +87,10 @@ function createAdditionalSection(mainSection, network) {
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 ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
if (!domainRegex.test(value)) {
if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
@@ -134,10 +114,17 @@ function createAdditionalSection(mainSection, network) {
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 = mainSection.taboption('additional', form.ListValue, 'config_path', _('Config File Path'), _('Select path for sing-box config file. Change this ONLY if you know what you are doing'));
o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)');
o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)');
o.default = '/etc/sing-box/config.json';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Value, 'cache_path', _('Cache File Path'), _('Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing'));
o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)');
o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)');
o.default = '/tmp/cache.db';
o.default = '/tmp/sing-box/cache.db';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
@@ -198,6 +185,18 @@ function createAdditionalSection(mainSection, network) {
return ['lan', 'loopback'].indexOf(value) === -1 && !value.startsWith('@');
};
o = mainSection.taboption('additional', form.Value, 'procd_reload_delay', _('Interface Monitoring Delay'), _('Delay in milliseconds before reloading podkop after interface UP'));
o.ucisection = 'main';
o.depends('mon_restart_ifaces', '1');
o.default = '2000';
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value) {
return _('Delay value cannot be empty');
}
return true;
};
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;
@@ -208,6 +207,7 @@ function createAdditionalSection(mainSection, network) {
o.rmempty = false;
o.ucisection = 'main';
// TODO(ampetelin): Can be moved to advanced settings in luci
// 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';

View File

@@ -209,7 +209,7 @@ function createConfigSection(section, map, network) {
o.default = '0';
o.depends('mode', 'proxy');
o.rmempty = false;
o.ucisection = 'main';
o.ucisection = s.section;
o = s.taboption('basic', widgets.DeviceSelect, 'interface', _('Network Interface'), _('Select network interface for VPN connection'));
o.depends('mode', 'vpn');
@@ -234,18 +234,17 @@ function createConfigSection(section, map, network) {
return true;
};
o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists'));
o = s.taboption('basic', form.Flag, 'community_lists_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 = s.taboption('basic', form.DynamicList, 'community_lists', _('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.depends('community_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
@@ -302,7 +301,7 @@ function createConfigSection(section, map, network) {
}
};
o = s.taboption('basic', form.ListValue, 'custom_domains_list_type', _('User Domain List Type'), _('Select how to add your custom domains'));
o = s.taboption('basic', form.ListValue, 'user_domain_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'));
@@ -310,9 +309,9 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.DynamicList, 'user_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.depends('user_domain_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -324,9 +323,9 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.TextValue, 'user_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.depends('user_domain_list_type', 'text');
o.rows = 8;
o.rmempty = false;
o.ucisection = s.section;
@@ -365,14 +364,14 @@ function createConfigSection(section, map, network) {
return true;
};
o = s.taboption('basic', form.Flag, 'custom_local_domains_list_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem'));
o = s.taboption('basic', form.Flag, 'local_domain_lists_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 = s.taboption('basic', form.DynamicList, 'local_domain_lists', _('Local Domain List Paths'), _('Enter the list file path'));
o.placeholder = '/path/file.lst';
o.depends('custom_local_domains_list_enabled', '1');
o.depends('local_domain_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -384,14 +383,14 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.Flag, 'remote_domain_lists_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 = s.taboption('basic', form.DynamicList, 'remote_domain_lists', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://'));
o.placeholder = 'URL';
o.depends('custom_download_domains_list_enabled', '1');
o.depends('remote_domain_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -399,7 +398,26 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.Flag, 'local_subnet_lists_enabled', _('Local Subnet Lists'), _('Use the list from the router filesystem'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption('basic', form.DynamicList, 'local_subnet_lists', _('Local Subnet List Paths'), _('Enter the list file path'));
o.placeholder = '/path/file.lst';
o.depends('local_subnet_lists_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.ListValue, 'user_subnet_list_type', _('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)'));
@@ -407,9 +425,9 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.DynamicList, 'user_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.depends('user_subnet_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -432,9 +450,9 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.TextValue, 'user_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.depends('user_subnet_list_type', 'text');
o.rows = 10;
o.rmempty = false;
o.ucisection = s.section;
@@ -489,14 +507,14 @@ function createConfigSection(section, map, network) {
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 = s.taboption('basic', form.Flag, 'remote_subnet_lists_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 = s.taboption('basic', form.DynamicList, 'remote_subnet_lists', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://'));
o.placeholder = 'URL';
o.depends('custom_download_subnets_list_enabled', '1');
o.depends('remote_subnet_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {

View File

@@ -21,7 +21,10 @@ const ALLOWED_WITH_RUSSIA_INSIDE = [
'google_ai',
'google_play',
'hetzner',
'ovh'
'ovh',
'hodca',
'digitalocean',
'cloudfront'
];
const DOMAIN_LIST_OPTIONS = {
@@ -43,8 +46,11 @@ const DOMAIN_LIST_OPTIONS = {
cloudflare: 'Cloudflare',
google_ai: 'Google AI',
google_play: 'Google Play',
hodca: 'H.O.D.C.A',
hetzner: 'Hetzner ASN',
ovh: 'OVH ASN'
ovh: 'OVH ASN',
digitalocean: 'Digital Ocean ASN',
cloudfront: 'CloudFront ASN'
};
const UPDATE_INTERVAL_OPTIONS = {

View File

@@ -97,8 +97,8 @@ msgstr "Локальные списки доменов"
msgid "Use the list from the router filesystem"
msgstr "Использовать список из файловой системы роутера"
msgid "Local Domain Lists Path"
msgstr "Путь к локальным спискам доменов"
msgid "Local Domain List Paths"
msgstr "Пути к локальным спискам доменов"
msgid "Enter to the list file path"
msgstr "Введите путь к файлу списка"
@@ -875,4 +875,34 @@ msgid "Lists update failed"
msgstr "Обновление списков не удалось"
msgid "Error: "
msgstr "Ошибка: "
msgstr "Ошибка: "
msgid "Interface monitoring"
msgstr "Мониторинг интерфейсов"
msgid "Interface monitoring for bad WAN"
msgstr "Мониторинг интерфейсов для плохого WAN"
msgid "Interface for monitoring"
msgstr "Интерфейс для мониторинга"
msgid "Select the WAN interfaces to be monitored"
msgstr "Выберите WAN интерфейсы для мониторинга"
msgid "Interface Monitoring Delay"
msgstr "Задержка при мониторинге интерфейсов"
msgid "Delay in milliseconds before reloading podkop after interface UP"
msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса"
msgid "Delay value cannot be empty"
msgstr "Значение не может быть пустым"
msgid "Local Subnet Lists"
msgstr "Локальные списки подсетей"
msgid "Local Subnet List Paths"
msgstr "Пути к локальным спискам подсетей"
msgid "Config File Path"
msgstr "Путь к файлу конфигурации"

View File

@@ -97,7 +97,7 @@ msgstr ""
msgid "Use the list from the router filesystem"
msgstr ""
msgid "Local Domain Lists Path"
msgid "Local Domain List Paths"
msgstr ""
msgid "Enter to the list file path"
@@ -1229,4 +1229,34 @@ msgid "Loading..."
msgstr ""
msgid "Error: "
msgstr ""
msgid "Interface monitoring"
msgstr ""
msgid "Interface monitoring for bad WAN"
msgstr ""
msgid "Interface for monitoring"
msgstr ""
msgid "Select the WAN interfaces to be monitored"
msgstr ""
msgid "Interface Monitoring Delay"
msgstr ""
msgid "Delay in milliseconds before reloading podkop after interface UP"
msgstr ""
msgid "Delay value cannot be empty"
msgstr ""
msgid "Local Subnet Lists"
msgstr ""
msgid "Local Subnet List Paths"
msgstr ""
msgid "Config File Path"
msgstr ""

View File

@@ -55,6 +55,9 @@ define Package/podkop/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) ./files/usr/bin/podkop $(1)/usr/bin/podkop
$(INSTALL_DIR) $(1)/usr/lib/podkop
$(CP) ./files/usr/lib/* $(1)/usr/lib/podkop/
endef
$(eval $(call BuildPackage,podkop))

View File

@@ -4,25 +4,24 @@ config main 'main'
option proxy_config_type 'url'
#option outbound_json ''
option proxy_string ''
option domain_list_enabled '1'
list domain_list 'russia_inside'
option subnets_list_enabled '0'
option custom_domains_list_type 'disabled'
#list custom_domains ''
#option custom_domains_text ''
option custom_local_domains_list_enabled '0'
#list custom_local_domains ''
option custom_download_domains_list_enabled '0'
#list custom_download_domains ''
option custom_domains_list_type 'disable'
#list custom_subnets ''
#custom_subnets_text ''
option custom_download_subnets_list_enabled '0'
#list custom_download_subnets ''
option community_lists_enabled '1'
list community_lists 'russia_inside'
option user_domain_list_type 'disabled'
#list user_domains ''
#option user_domains_text ''
option local_domain_lists_enabled '0'
#list local_domain_lists ''
option remote_domain_lists_enabled '0'
#list remote_domain_lists ''
option user_subnet_list_type 'disable'
#list user_subnets ''
#option user_subnets_text ''
option local_subnet_lists_enabled '0'
#list local_subnet_lists ''
option remote_subnet_lists_enabled '0'
#list remote_subnet_lists ''
option all_traffic_from_ip_enabled '0'
#list all_traffic_ip ''
option delist_domains_enabled '0'
#list delist_domains ''
option exclude_from_ip_enabled '0'
#list exclude_traffic_ip ''
option yacd '0'
@@ -37,9 +36,12 @@ config main 'main'
option split_dns_type 'udp'
option split_dns_server '1.1.1.1'
option dns_rewrite_ttl '60'
option cache_file '/tmp/cache.db'
option config_path '/etc/sing-box/config.json'
option cache_path '/tmp/sing-box/cache.db'
list iface 'br-lan'
option mon_restart_ifaces '0'
#list restart_ifaces 'wan'
option procd_reload_delay '2000'
option ss_uot '0'
option detour '0'
option detour '0'
option shutdown_correctly '1'

View File

@@ -34,9 +34,9 @@ service_triggers() {
config_get mon_restart_ifaces "main" "mon_restart_ifaces"
config_get restart_ifaces "main" "restart_ifaces"
config_get procd_reload_delay "main" "procd_reload_delay" "2000"
# Test without delay
#PROCD_RELOAD_DELAY=2000
PROCD_RELOAD_DELAY=$procd_reload_delay
procd_open_trigger
procd_add_config_trigger "config.change" "$NAME" "$initscript" restart 'on_config_change'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
# shellcheck disable=SC2034
## Common
PODKOP_CONFIG="/etc/config/podkop"
RESOLV_CONF="/etc/resolv.conf"
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"
CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi"
FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi"
TMP_SING_BOX_FOLDER="/tmp/sing-box"
TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets"
CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker
## nft
NFT_TABLE_NAME="PodkopTable"
NFT_LOCALV4_SET_NAME="localv4"
NFT_COMMON_SET_NAME="podkop_subnets"
NFT_DISCORD_SET_NAME="podkop_discord_subnets"
NFT_INTERFACE_SET_NAME="interfaces"
## sing-box
# Log
SB_DEFAULT_LOG_LEVEL="warn"
# DNS
SB_DNS_SERVER_TAG="dns-server"
SB_SPLIT_DNS_SERVER_TAG="split-dns-server"
SB_FAKEIP_DNS_SERVER_TAG="fakeip-server"
SB_FAKEIP_INET4_RANGE="198.18.0.0/15"
SB_DNS_DOMAIN_RESOLVER_TAG="dns-domain-resolver"
SB_FAKEIP_DNS_RULE_TAG="fakeip-dns-rule-tag"
SB_INVERT_FAKEIP_DNS_RULE_TAG="invert-fakeip-dns-rule-tag"
# Inbounds
SB_TPROXY_INBOUND_TAG="tproxy-in"
SB_TPROXY_INBOUND_ADDRESS="127.0.0.1"
SB_TPROXY_INBOUND_PORT=1602
SB_DNS_INBOUND_TAG="dns-in"
SB_DNS_INBOUND_ADDRESS="127.0.0.42"
SB_DNS_INBOUND_PORT=53
SB_MIXED_INBOUND_TAG="mixed-in"
SB_MIXED_INBOUND_ADDRESS="0.0.0.0" # TODO(ampetelin): maybe to determine address?
SB_MIXED_INBOUND_PORT=2080
SB_SERVICE_MIXED_INBOUND_TAG="service-mixed-in"
SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1"
SB_SERVICE_MIXED_INBOUND_PORT=4534
# Outbounds
SB_DIRECT_OUTBOUND_TAG="direct-out"
SB_MAIN_OUTBOUND_TAG="main-out"
# Route
SB_REJECT_RULE_TAG="reject-rule-tag"
## Lists
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download"
DOMAINS_RU_INSIDE="${GITHUB_RAW_URL}/Russia/inside-dnsmasq-nfset.lst"
DOMAINS_RU_OUTSIDE="${GITHUB_RAW_URL}/Russia/outside-dnsmasq-nfset.lst"
DOMAINS_UA="${GITHUB_RAW_URL}/Ukraine/inside-dnsmasq-nfset.lst"
DOMAINS_YOUTUBE="${GITHUB_RAW_URL}/Services/youtube.lst"
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"
SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst"
SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst"
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 hodca digitalocean cloudfront"

View File

@@ -0,0 +1,14 @@
def extend_key_value(current_value; new_value):
if (current_value | type) == "array" then
if (new_value | type) == "array" then
current_value + new_value
else
current_value + [new_value]
end
else
if (new_value | type) == "array" then
[current_value] + new_value
else
[current_value, new_value]
end
end;

View File

@@ -0,0 +1,369 @@
# Check if string is valid IPv4
is_ipv4() {
local ip="$1"
local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"
[[ "$ip" =~ $regex ]]
}
# Check if string is valid IPv4 with CIDR mask
is_ipv4_cidr() {
local ip="$1"
local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(\/(3[0-2]|2[0-9]|1[0-9]|[0-9]))$"
[[ "$ip" =~ $regex ]]
}
is_ipv4_ip_or_ipv4_cidr() {
is_ipv4 "$1" || is_ipv4_cidr "$1"
}
# Check if string is valid domain
is_domain() {
local str="$1"
echo "$str" | grep -Eq '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9]))+$'
}
# Checks if the given string is a valid base64-encoded sequence
is_base64() {
local str="$1"
if echo "$str" | base64 -d > /dev/null 2>&1; then
return 0
fi
return 1
}
# Checks if the given file exists
file_exists() {
local filepath="$1"
if [[ -f "$filepath" ]]; then
return 0
else
return 1
fi
}
# Returns the inbound tag name by appending the postfix to the given section
get_inbound_tag_by_section() {
local section="$1"
local postfix="in"
echo "$section-$postfix"
}
# Returns the outbound tag name by appending the postfix to the given section
get_outbound_tag_by_section() {
local section="$1"
local postfix="out"
echo "$section-$postfix"
}
# Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix
get_ruleset_tag() {
local section="$1"
local name="$2"
local type="$3"
local postfix="ruleset"
if [ -n "$type" ]; then
echo "$section-$name-$type-$postfix"
else
echo "$section-$name-$postfix"
fi
}
# Determines the ruleset format based on the file extension (json → source, srs → binary)
get_ruleset_format_by_file_extension() {
local file_extension="$1"
local format
case "$file_extension" in
json) format="source" ;;
srs) format="binary" ;;
*)
log "Unsupported file extension: .$file_extension"
return 1
;;
esac
echo "$format"
}
# Converts a comma-separated string into a JSON array string
comma_string_to_json_array() {
local input="$1"
if [ -z "$input" ]; then
echo "[]"
return
fi
local replaced="${input//,/\",\"}"
echo "[\"$replaced\"]"
}
# Decodes a URL-encoded string
url_decode() {
local encoded="$1"
printf '%b' "$(echo "$encoded" | sed 's/+/ /g; s/%/\\x/g')"
}
# Extracts the userinfo (username[:password]) part from a URL
url_get_userinfo() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e '/@/!d' -e 's/@.*//p'
}
# Extracts the host part from a URL
url_get_host() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#\([:/].*\|$\)##p'
}
# Extracts the port number from a URL
url_get_port() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#^[^/]*:\([0-9][0-9]*\).*#\1#p'
}
# Extracts the path from a URL (without query or fragment; returns "/" if empty)
url_get_path() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*##' -e 's#\([^?]*\).*#\1#p'
}
# Extracts the value of a specific query parameter from a URL
url_get_query_param() {
local url="$1"
local param="$2"
local raw
raw=$(echo "$url" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
[ -z "$raw" ] && echo "" && return
echo "$raw"
}
# Extracts the basename (filename without extension) from a URL
url_get_basename() {
local url="$1"
local filename="${url##*/}"
local basename="${filename%%.*}"
echo "$basename"
}
# Extracts and returns the file extension from the given URL
url_get_file_extension() {
local url="$1"
local basename="${url##*/}"
case "$basename" in
*.*) echo "${basename##*.}" ;;
*) echo "" ;;
esac
}
# Decodes and returns a base64-encoded string
base64_decode() {
local str="$1"
local decoded_url
decoded_url="$(echo "$str" | base64 -d 2> /dev/null)"
echo "$decoded_url"
}
# Generates a unique 16-character ID based on the current timestamp and a random number
gen_id() {
printf '%s%s' "$(date +%s)" "$RANDOM" | md5sum | cut -c1-16
}
# Adds a missing UCI option with the given value if it does not exist
migration_add_new_option() {
local package="$1"
local section="$2"
local option="$3"
local value="$4"
local current
current="$(uci -q get "$package.$section.$option")"
if [ -z "$current" ]; then
log "Adding missing option '$option' with value '$value'"
uci set "$package.$section.$option=$value"
uci commit "$package"
return 0
else
return 1
fi
}
# Migrates a configuration key in an OpenWrt config file from old_key_name to new_key_name
migration_rename_config_key() {
local config="$1"
local key_type="$2"
local old_key_name="$3"
local new_key_name="$4"
if grep -q "$key_type $old_key_name" "$config"; then
log "Deprecated $key_type found: $old_key_name migrating to $new_key_name"
sed -i "s/$key_type $old_key_name/$key_type $new_key_name/g" "$config"
fi
}
# Download URL content directly
download_to_stream() {
local url="$1"
local http_proxy_address="$2"
local retries="${3:-3}"
local wait="${4:-2}"
for attempt in $(seq 1 "$retries"); do
if [ -n "$http_proxy_address" ]; then
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -qO- "$url" | sed 's/\r$//' && break
else
wget -qO- "$url" | sed 's/\r$//' && break
fi
log "Attempt $attempt/$retries to download $url failed" "warn"
sleep "$wait"
done
}
# Download URL to file
download_to_file() {
local url="$1"
local filepath="$2"
local http_proxy_address="$3"
local retries="${4:-3}"
local wait="${5:-2}"
for attempt in $(seq 1 "$retries"); do
if [ -n "$http_proxy_address" ]; then
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -O "$filepath" "$url" && break
else
wget -O "$filepath" "$url" && break
fi
log "Attempt $attempt/$retries to download $url failed" "warn"
sleep "$wait"
done
if grep -q $'\r' "$filepath"; then
log "Downloaded file has Windows line endings (CRLF). Converting to Unix (LF)"
sed -i 's/\r$//' "$filepath"
fi
}
# Decompiles a sing-box SRS binary file into a JSON ruleset file
decompile_srs_file() {
local binary_filepath="$1"
local output_filepath="$2"
log "Decompiling $binary_filepath to $output_filepath" "debug"
if ! file_exists "$binary_filepath"; then
log "File $binary_filepath not found" "error"
return 1
fi
sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
if [[ $? -ne 0 ]]; then
log "Decompilation command failed for $binary_filepath" "error"
return 1
fi
}
#######################################
# Parses a whitespace-separated string, validates items as either domains
# or IPv4 addresses/subnets, and returns a comma-separated string of valid items.
# Arguments:
# $1 - Input string (space-separated list of items)
# $2 - Type of validation ("domains" or "subnets")
# Outputs:
# Comma-separated string of valid domains or subnets
#######################################
parse_domain_or_subnet_string_to_commas_string() {
local string="$1"
local type="$2"
local result
for item in $string; do
case "$type" in
domains)
if ! is_domain "$item"; then
log "'$item' is not a valid domain" "debug"
continue
fi
;;
subnets)
if ! is_ipv4_ip_or_ipv4_cidr "$item"; then
log "'$item' is not IPv4 or IPv4 CIDR" "debug"
continue
fi
;;
*)
log "Unknown type: $type" "error"
return 1
;;
esac
if [ -z "$result" ]; then
result="$item"
else
result="$result,$item"
fi
done
echo "$result"
}
#######################################
# Parses a file line by line, validates entries as either domains or subnets,
# and returns a single comma-separated string of valid items.
# Arguments:
# $1 - Path to the input file
# $2 - Type of validation ("domains" or "subnets")
# Outputs:
# Comma-separated string of valid domains or subnets
#######################################
parse_domain_or_subnet_file_to_comma_string() {
local filepath="$1"
local type="$2"
local result
while IFS= read -r line; do
[ -z "$line" ] && continue
case "$type" in
domains)
if ! is_domain "$line"; then
log "'$line' is not a valid domain" "debug"
continue
fi
;;
subnets)
if ! is_ipv4 "$line" && ! is_ipv4_cidr "$line"; then
log "'$line' is not IPv4 or IPv4 CIDR" "debug"
continue
fi
;;
*)
log "Unknown type: $type" "error"
return 1
;;
esac
if [ -z "$result" ]; then
result="$line"
else
result="$result,$line"
fi
done < "$filepath"
echo "$result"
}

View File

@@ -0,0 +1,30 @@
COLOR_CYAN="\033[0;36m"
COLOR_GREEN="\033[0;32m"
COLOR_RESET="\033[0m"
log() {
local message="$1"
local level="$2"
if [ "$level" == "" ]; then
level="info"
fi
logger -t "podkop" "[$level] $message"
}
nolog() {
local message="$1"
local timestamp
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
}
echolog() {
local message="$1"
local level="$2"
log "$message" "$level"
nolog "$message"
}

View File

@@ -0,0 +1,30 @@
# Create an nftables table in the inet family
nft_create_table() {
local name="$1"
nft add table inet "$name"
}
# Create a set within a table for storing IPv4 addresses
nft_create_ipv4_set() {
local table="$1"
local name="$2"
nft add set inet "$table" "$name" '{ type ipv4_addr; flags interval; auto-merge; }'
}
nft_create_ifname_set() {
local table="$1"
local name="$2"
nft add set inet "$table" "$name" '{ type ifname; flags interval; }'
}
# Add one or more elements to a set
nft_add_set_elements() {
local table="$1"
local set="$2"
local elements="$3"
nft add element inet "$table" "$set" "{ $elements }"
}

View File

@@ -0,0 +1,228 @@
PODKOP_LIB="/usr/lib/podkop"
. "$PODKOP_LIB/helpers.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
sing_box_cf_add_dns_server() {
local config="$1"
local type="$2"
local tag="$3"
local server="$4"
local domain_resolver="$5"
local detour="$6"
local server_address server_port
server_address=$(url_get_host "$server")
server_port=$(url_get_port "$server")
case "$type" in
udp)
[ -z "$server_port" ] && server_port=53
config=$(sing_box_cm_add_udp_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \
"$detour")
;;
dot)
[ -z "$server_port" ] && server_port=853
config=$(sing_box_cm_add_tls_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \
"$detour")
;;
doh)
[ -z "$server_port" ] && server_port=443
local path headers
path=$(url_get_path "$server")
headers="" # TODO(ampetelin): implement it if necessary
config=$(sing_box_cm_add_https_dns_server "$config" "$tag" "$server_address" "$server_port" "$path" "$headers" \
"$domain_resolver" "$detour")
;;
*)
log "Unsupported DNS server type: $type"
exit 1
;;
esac
echo "$config"
}
sing_box_cf_add_mixed_inbound_and_route_rule() {
local config="$1"
local tag="$2"
local listen_address="$3"
local listen_port="$4"
local outbound="$5"
config=$(sing_box_cm_add_mixed_inbound "$config" "$tag" "$listen_address" "$listen_port")
config=$(sing_box_cm_add_route_rule "$config" "" "$tag" "$outbound")
echo "$config"
}
sing_box_cf_add_proxy_outbound() {
local config="$1"
local section="$2"
local url="$3"
local udp_over_tcp="$4"
url=$(url_decode "$url")
local scheme="${url%%://*}"
case "$scheme" in
vless)
local tag host port uuid flow packet_encoding
tag=$(get_outbound_tag_by_section "$section")
host=$(url_get_host "$url")
port=$(url_get_port "$url")
uuid=$(url_get_userinfo "$url")
flow=$(url_get_query_param "$url" "flow")
packet_encoding=$(url_get_query_param "$url" "packetEncoding")
config=$(sing_box_cm_add_vless_outbound "$config" "$tag" "$host" "$port" "$uuid" "$flow" "" "$packet_encoding")
local transport
transport=$(url_get_query_param "$url" "type")
case "$transport" in
tcp | raw) ;;
ws)
local ws_path ws_host ws_early_data
ws_path=$(url_get_query_param "$url" "path")
ws_host=$(url_get_query_param "$url" "host")
ws_early_data=$(url_get_query_param "$url" "ed")
config=$(sing_box_cm_set_vless_ws_transport "$config" "$tag" "$ws_path" "$ws_host" "$ws_early_data")
;;
grpc)
# TODO(ampetelin): Add handling of optional gRPC parameters; example links are needed.
config=$(sing_box_cm_set_vless_grpc_transport "$config" "$tag")
;;
*)
log "Unknown transport '$transport' detected." "error"
;;
esac
local security
security=$(url_get_query_param "$url" "security")
case "$security" in
tls | reality)
local sni insecure alpn fingerprint public_key short_id
sni=$(url_get_query_param "$url" "sni")
insecure=$(url_get_query_param "$url" "allowInsecure")
alpn=$(comma_string_to_json_array "$(url_get_query_param "$url" "alpn")")
fingerprint=$(url_get_query_param "$url" "fp")
public_key=$(url_get_query_param "$url" "pbk")
short_id=$(url_get_query_param "$url" "sid")
config=$(
sing_box_cm_set_vless_tls \
"$config" \
"$tag" \
"$sni" \
"$([ "$insecure" == "1" ] && echo true)" \
"$([ "$alpn" == "[]" ] && echo null || echo "$alpn")" \
"$fingerprint" \
"$public_key" \
"$short_id"
)
;;
none) ;;
*)
log "Unknown security '$security' detected." "error"
;;
esac
;;
ss)
local userinfo tag host port method password udp_over_tcp
userinfo=$(url_get_userinfo "$url")
if is_base64 "$userinfo"; then
userinfo=$(base64_decode "$userinfo")
fi
tag=$(get_outbound_tag_by_section "$section")
host=$(url_get_host "$url")
port=$(url_get_port "$url")
method="${userinfo%%:*}"
password="${userinfo#*:}"
config=$(
sing_box_cm_add_shadowsocks_outbound \
"$config" \
"$tag" \
"$host" \
"$port" \
"$method" \
"$password" \
"" \
"$([ "$udp_over_tcp" == "1" ] && echo 2)" # if udp_over_tcp is enabled, enable version 2
)
;;
*)
log "Unsupported proxy $scheme type"
exit 1
;;
esac
echo "$config"
}
sing_box_cf_add_json_outbound() {
local config="$1"
local section="$2"
local json_outbound="$3"
local tag
tag=$(get_outbound_tag_by_section "$section")
config=$(sing_box_cm_add_raw_outbound "$config" "$tag" "$json_outbound")
echo "$config"
}
sing_box_cf_add_interface_outbound() {
local config="$1"
local section="$2"
local interface_name="$3"
local tag
tag=$(get_outbound_tag_by_section "$section")
config=$(sing_box_cm_add_interface_outbound "$config" "$tag" "$interface_name")
echo "$config"
}
sing_box_cf_proxy_domain() {
local config="$1"
local inbound="$2"
local domain="$3"
local outbound="$4"
tag="$(gen_id)"
config=$(sing_box_cm_add_route_rule "$config" "$tag" "$inbound" "$outbound")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "domain" "$domain")
echo "$config"
}
sing_box_cf_override_domain_port() {
local config="$1"
local domain="$2"
local port="$3"
tag="$(gen_id)"
config=$(sing_box_cm_add_options_route_rule "$config" "$tag")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "domain" "$domain")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "override_port" "$port")
echo "$config"
}
sing_box_cf_add_single_key_reject_rule() {
local config="$1"
local inbound="$2"
local key="$3"
local value="$4"
tag="$(gen_id)"
config=$(sing_box_cm_add_reject_route_rule "$config" "$tag" "$inbound")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "$key" "$value")
echo "$config"
}

File diff suppressed because it is too large Load Diff