From 7f6cc66eb10e7bea92690718cc40002bb52f56aa Mon Sep 17 00:00:00 2001 From: itdoginfo Date: Fri, 14 Feb 2025 16:26:28 +0300 Subject: [PATCH] Move to fakeip --- README.md | 45 +- install.sh | 51 +- luci-app-podkop/Makefile | 2 +- .../resources/view/podkop/podkop.js | 634 +++--- podkop/Makefile | 9 +- podkop/files/etc/config/podkop | 29 +- podkop/files/etc/hotplug.d/iface/50-podkop | 28 - podkop/files/etc/init.d/podkop | 1901 +++++++++-------- 8 files changed, 1387 insertions(+), 1312 deletions(-) delete mode 100755 podkop/files/etc/hotplug.d/iface/50-podkop diff --git a/README.md b/README.md index e85655d..a09e793 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -Это альфа версия, может не работать. Обсуждение https://t.me/itdogchat - топик **Podkop** +# Вещи, которые вам нужно знать перед установкой -Если у вас установлен Getdomains, то его следует удалить. +- Это альфа версия, которая находится в активной разработке. Из версии в версию что-то может меняться. +- Основной функционал работает, но побочные штуки сейчас могут сбоить. +- При обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться. +- Необходимо минимум 15МБ свободного места на роутере. Роутерами с флешками на 16МБ сразу мимо. +- При старте программы редактируется конфиг Dnsmasq. +- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен. +- Информация здесь может быть устаревшей. Все изменения фиксируются в телеграм-чате https://t.me/itdogchat - топик **Podkop**. +- Если у вас установлен Getdomains, его следует удалить. # Удаление GetDomains скриптом ``` @@ -11,9 +18,9 @@ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/domain-routing-openwr # Установка Podkop Пакет работает на всех архитектурах. -Будет точно работать только на OpenWrt 23.05. +Тестировался на OpenWrt 23.05 и OpenWrt 24.10. -Нужен dnsmasq-full. В автоматическом режиме ставится сам. Вручную надо поставить [самостоятельно](https://github.com/itdoginfo/podkop/blob/952dd6215a2a83d65937cf9e33534c42809091ed/install.sh#L20). +Поддержки APK на данный момент нет. APK будет сделан после того как разгребу основное. ## Автоматическая ``` @@ -53,26 +60,20 @@ Luci: Services/podkop ### Proxy Для VLESS и Shadowsocks. Другие протоколы тоже будут, кидайте в чат примеры строк без чувствительных данных. -Для использования этого режима нужен sing-box: -``` -opkg update && opkg install sing-box -``` В этом режиме просто копируйте строку в **Proxy String** и из неё автоматически настроится sing-box. ### VPN -Здесь у вас должен быть уже настроен WG/OpenVPN/OpenConnect etc, создана Zone и Forwarding. +Здесь у вас должен быть уже настроен WG/OpenVPN/OpenConnect etc, зона Zone и Forwarding не обязательны. Просто выбрать интерфейс из списка. ## Настройка доменов и подсетей -**Domain list enable** - Включить общий список. - -**Delist domains from main list enable** - Выключение заданных доменов из общего списка. Задавать списком. +**Community Lists** - Включить списки комьюнити **Subnets list enable** - Включить подсети из общего списка, выбрать из предложенных. -**Custom domains enable** - Добавить свои домены. Задавать списком. +**Custom domains enable** - Добавить свои домены **Custom subnets enable** - Добавить подсети или IP-адреса. Для подсетей задать маску. @@ -84,10 +85,15 @@ opkg update && opkg install sing-box - [x] awg работает не стабильно - [x] Сеть рестартится при любом раскладе - [x] Выкл-вкл wg через luci не отрабатывает поднятие маршрута -- [ ] Проблема скачивания списка из github release. Нужен curl -L - [ ] Если eof после последней строки в rt_tables, то скрипт не добавляет перенос строки +- [ ] Парсинг VLESS не отрабатывает, если в SNI два домена. Пример `sni=telegram.org%3Bwww.telegram.org` +- [ ] `service network restart` ломает маршруты при sing-box +- [ ] Совпадение секции с ruleset ломает конфиг sing-box +- [ ] В каких-то случаях плохо отрабатывает localfile # ToDo +Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме. + Сделано - [x] Скрипт для автоматической установки. - [x] Подсети дискорда. @@ -127,13 +133,18 @@ opkg update && opkg install sing-box - [x] Проверка места в скрипте install. Если доступно меньше 20MB - exit 1 c выводом колько надо и сколько доступно. + показ модели роутера - [ ] Правило запрещающее QUIC - [ ] Проверить обновление списков, отрабатывает ли +- [ ] Проверка на ванильную openwrt +- [ ] Проверка откуда установлен sing-box. Например, проверять установлен ли он из официального репозитория +- [x] TG в сервисы +- [ ] Выбор ткуда направлять трафик в туннель. В том числе чтоб откуда угодно, а не только br-lan +- [ ] Диагностика: Proxy check completed successfully предположительно не показывает IP, если вернулся это IPv6. +- [ ] Диагностика: podkop_domains: 0 elements как проверять что доходят запросы при fakeip? Мб врубать логи dnsmasq и их чекать. +- [ ] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn Приоритет 2 - [x] Списки доменов и подсетей с роутера - [ ] Кнопка обновления списка доменов и подсетей. Запихнуть в главное меню - [ ] IPv6 -- [ ] Придумать автонастройку DNS через stubby итд. Как лучше это реализовать. Для sing-box не нужно -- [ ] Удаление подсетей CF из domain sets раз в N часов Wiki - [x] Тема @@ -143,7 +154,7 @@ Wiki - [x] Переменная, раз во сколько часов обновлять списки - [ ] Галочка, которая режет доступ к doh серверам - [ ] Свой конфиг sing-box -- [ ] Поменять curl на wget, убрать зависимость. Проверять доступность списков лучше всего curl`ом +- [x] Поменять curl на wget, убрать зависимость. Проверять доступность списков лучше всего curl`ом Рефактор - [ ] Handle для sing-box diff --git a/install.sh b/install.sh index f91e6a9..3592e70 100755 --- a/install.sh +++ b/install.sh @@ -17,28 +17,7 @@ main() { echo "opkg update" opkg update - - if opkg list-installed | grep -q dnsmasq-full; then - echo "dnsmasq-full already installed" - else - echo "Installed dnsmasq-full" - cd /tmp/ && opkg download dnsmasq-full - opkg remove dnsmasq && opkg install dnsmasq-full --cache /tmp/ - - [ -f /etc/config/dhcp-opkg ] && cp /etc/config/dhcp /etc/config/dhcp-old && mv /etc/config/dhcp-opkg /etc/config/dhcp - fi - - openwrt_release=$(cat /etc/openwrt_release | grep -Eo [0-9]{2}[.][0-9]{2}[.][0-9]* | cut -d '.' -f 1 | tail -n 1) - if [ $openwrt_release -ge 24 ]; then - if uci get dhcp.@dnsmasq[0].confdir | grep -q /tmp/dnsmasq.d; then - echo "confdir alreadt set" - else - printf "Setting confdir" - uci set dhcp.@dnsmasq[0].confdir='/tmp/dnsmasq.d' - uci commit dhcp - fi - fi - + 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" @@ -49,6 +28,7 @@ main() { case $UPDATE in y) echo "Upgraded podkop..." + sed -i '/second/d' /etc/config/podkop break ;; @@ -99,23 +79,17 @@ main() { add_tunnel() { echo "What type of VPN or proxy will be used? We also can automatically configure Wireguard and Amnezia WireGuard." - echo "1) VLESS, Shadowsocks (A sing-box will be installed)" - echo "2) Wireguard" - echo "3) AmneziaWG" - echo "4) OpenVPN" - echo "5) OpenConnect" - echo "6) Skip this step" + echo "1) Wireguard" + echo "2) AmneziaWG" + echo "3) OpenVPN" + echo "4) OpenConnect" + echo "5) Skip this step" while true; do read -r -p '' TUNNEL case $TUNNEL in 1) - opkg install sing-box - break - ;; - - 2) opkg install wireguard-tools luci-proto-wireguard luci-app-wireguard printf "\033[32;1mDo you want to configure the wireguard interface? (y/n): \033[0m\n" @@ -130,7 +104,7 @@ add_tunnel() { break ;; - 3) + 2) install_awg_packages printf "\033[32;1mThere are no instructions for manual configure yet. Do you want to configure the amneziawg interface? (y/n): \033[0m\n" @@ -143,19 +117,19 @@ add_tunnel() { break ;; - 4) + 3) opkg install opkg install openvpn-openssl luci-app-openvpn printf "\e[1;32mUse these instructions to configure https://itdog.info/nastrojka-klienta-openvpn-na-openwrt/\e[0m\n" break ;; - 5) + 4) opkg install opkg install openconnect luci-proto-openconnect printf "\e[1;32mUse these instructions to configure https://itdog.info/nastrojka-klienta-openconnect-na-openwrt/\e[0m\n" break ;; - 6) + 5) echo "Skip. Use this if you're installing an upgrade." break ;; @@ -395,8 +369,7 @@ check_system() { # Check available space AVAILABLE_SPACE=$(df /tmp | awk 'NR==2 {print $4}') - # Change after switch sing-box - REQUIRED_SPACE=1024 # 20MB in KB + REQUIRED_SPACE=15360 # 15MB in KB echo "Available space: $((AVAILABLE_SPACE/1024))MB" echo "Required space: $((REQUIRED_SPACE/1024))MB" diff --git a/luci-app-podkop/Makefile b/luci-app-podkop/Makefile index 56d7489..581167f 100644 --- a/luci-app-podkop/Makefile +++ b/luci-app-podkop/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-podkop -PKG_VERSION:=0.2.5 +PKG_VERSION:=0.3.0 PKG_RELEASE:=1 LUCI_TITLE:=LuCI podkop app diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js index 3326bdf..d4affbf 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js @@ -18,8 +18,8 @@ return view.extend({ 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('vpn', ('VPN')); o.value('proxy', ('Proxy')); + o.value('vpn', ('VPN')); o.ucisection = 'main'; o = s.taboption('basic', form.ListValue, 'proxy_config_type', _('Configuration Type'), _('Select how to configure the proxy')); @@ -76,47 +76,33 @@ return view.extend({ console.error('Error fetching devices:', error); } - o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Domain Lists')); + o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists')); o.default = '0'; o.rmempty = false; o.ucisection = 'main'; - o = s.taboption('basic', form.ListValue, 'domain_list', _('Domain List'), _('Select a list') + ' github.com/itdoginfo/allow-domains'); - o.placeholder = 'placeholder'; - o.value('ru_inside', 'Russia inside'); - o.value('ru_outside', 'Russia outside'); - o.value('ua', 'Ukraine'); - o.depends('domain_list_enabled', '1'); - o.rmempty = false; - o.ucisection = 'main'; - - o = s.taboption('basic', form.Flag, 'delist_domains_enabled', _('Domain Exclusions'), _('Exclude specific domains from routing rules')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - o.depends('domain_list_enabled', '1'); - - o = s.taboption('basic', form.DynamicList, 'delist_domains', _('Excluded Domains'), _('Domains to be excluded from routing')); - o.placeholder = 'Delist domains'; - o.depends('delist_domains_enabled', '1'); - o.rmempty = false; - o.ucisection = 'main'; - - o = s.taboption('basic', form.Flag, 'subnets_list_enabled', _('Community Subnet Lists'), _('Enable routing for popular services like Twitter, Meta, and Discord')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = s.taboption('basic', form.DynamicList, 'subnets', _('Service Networks'), _('Select predefined service networks for routing')); - o.placeholder = 'Service network list'; - o.value('twitter', 'Twitter(x.com)'); + o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains'); + o.placeholder = 'Service list'; + o.value('russia_inside', 'Russia inside'); + o.value('russia_outside', 'Russia outside'); + o.value('ukraine_inside', 'Ukraine'); + o.value('geoblock', 'GEO Block'); + o.value('block', 'Block'); + o.value('porn', 'Porn'); + o.value('news', 'News'); + o.value('anime', 'Anime'); + o.value('youtube', 'Youtube'); + o.value('discord', 'Discord'); o.value('meta', 'Meta'); - o.value('discord', 'Discord(voice)'); - o.depends('subnets_list_enabled', '1'); + o.value('twitter', 'Twitter (X)'); + o.value('hdrezka', 'HDRezka'); + o.value('tiktok', 'Tik-Tok'); + o.value('telegram', 'Telegram'); + o.depends('domain_list_enabled', '1'); o.rmempty = false; o.ucisection = 'main'; - o = s.taboption('basic', form.ListValue, 'custom_domains_list_enabled', _('User Domain List Type'), _('Select how to add your custom domains')); + 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')); @@ -126,7 +112,7 @@ return view.extend({ 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_enabled', 'dynamic'); + o.depends('custom_domains_list_type', 'dynamic'); o.rmempty = false; o.ucisection = 'main'; o.validate = function (section_id, value) { @@ -144,7 +130,7 @@ return view.extend({ o = s.taboption('basic', form.TextValue, 'custom_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline (example: sub.example.com, example.com or one domain per line)')); o.placeholder = 'example.com, sub.example.com\ndomain.com test.com\nsubdomain.domain.com another.com, third.com'; - o.depends('custom_domains_list_enabled', 'text'); + o.depends('custom_domains_list_type', 'text'); o.rows = 10; o.rmempty = false; o.ucisection = 'main'; @@ -244,7 +230,6 @@ return view.extend({ return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y'); } - // Разбираем IP и маску const [ip, cidr] = value.split('/'); const ipParts = ip.split('.'); @@ -398,6 +383,11 @@ return view.extend({ return true; }; + o = s.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080')); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; + // Additional Settings Tab o = s.tab('additional', _('Additional Settings')); @@ -408,12 +398,6 @@ return view.extend({ o.rmempty = false; o.ucisection = 'main'; - o = s.taboption('additional', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080')); - o.default = '0'; - o.depends('mode', 'proxy'); - o.rmempty = false; - o.ucisection = 'main'; - o = s.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('For issues with open connections sing-box')); o.default = '0'; o.depends('mode', 'proxy'); @@ -421,238 +405,15 @@ return view.extend({ o.ucisection = 'main'; o = s.taboption('additional', form.ListValue, 'update_interval', _('List Update Frequency'), _('Select how often the lists will be updated')); - o.value('0 */1 * * *', _('Every hour')); - o.value('0 */2 * * *', _('Every 2 hours')); - o.value('0 */4 * * *', _('Every 4 hours')); - o.value('0 */6 * * *', _('Every 6 hours')); - o.value('0 */12 * * *', _('Every 12 hours')); - o.value('0 4 * * *', _('Once a day at 04:00')); - o.value('0 4 * * 0', _('Once a week on Sunday at 04:00')); - o.default = '0 4 * * *'; + o.value('1h', _('Every hour')); + o.value('3h', _('Every 3 hours')); + o.value('12h', _('Every 12 hours')); + o.value('1d', _('Every day')); + o.value('3d', _('Every 3 days')); + o.default = '1d'; o.rmempty = false; o.ucisection = 'main'; - // Secondary Settings Tab - - o = s.tab('secondary_config', _('Secondary Config')); - - o = s.taboption('secondary_config', form.Flag, 'second_enable', _('Secondary VPN/Proxy Enable'), _('Enable secondary VPN/Proxy configuration')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.ListValue, 'second_mode', _('Connection Type'), _('Select between VPN and Proxy connection methods for traffic routing')); - o.value('vpn', ('VPN')); - o.value('proxy', ('Proxy')); - o.depends('second_enable', '1'); - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.ListValue, 'second_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('second_mode', 'proxy'); - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.TextValue, 'second_proxy_string', _('Proxy Configuration URL'), _('Enter connection string starting with vless:// or ss:// for proxy configuration')); - o.depends('second_proxy_config_type', 'url'); - o.rows = 5; - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.TextValue, 'second_outbound_json', _('Outbound Configuration'), _('Enter complete outbound configuration in JSON format')); - o.depends('second_proxy_config_type', 'outbound'); - o.rows = 10; - o.ucisection = 'second'; - 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('secondary_config', form.ListValue, 'second_interface', _('Network Interface'), _('Select network interface for VPN connection')); - o.depends('second_mode', 'vpn'); - o.ucisection = 'second'; - - try { - const devices = await network.getDevices(); - const excludeInterfaces = ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0']; - - devices.forEach(function (device) { - if (device.dev && device.dev.name) { - const deviceName = device.dev.name; - const isExcluded = excludeInterfaces.includes(deviceName) || /^lan\d+$/.test(deviceName); - - if (!isExcluded) { - o.value(deviceName, deviceName); - } - } - }); - } catch (error) { - console.error('Error fetching devices:', error); - } - - o = s.taboption('secondary_config', form.Flag, 'second_domain_service_enabled', _('Service Domain List Enable'), _('Enable predefined service domain lists for routing')); - o.default = '0'; - o.rmempty = false; - o.depends('second_enable', '1'); - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.ListValue, 'second_service_list', _('Service List'), _('Select predefined services for routing')); - o.placeholder = 'placeholder'; - o.value('youtube', 'Youtube'); - o.depends('second_domain_service_enabled', '1'); - o.rmempty = false; - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.ListValue, 'second_custom_domains_list_enabled', _('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.depends('second_enable', '1'); - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.DynamicList, 'second_custom_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)')); - o.placeholder = 'Domains list'; - o.depends('second_custom_domains_list_enabled', 'dynamic'); - o.rmempty = false; - o.ucisection = 'second'; - 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)'); - } - return true; - }; - - o = s.taboption('secondary_config', form.TextValue, 'second_custom_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline (example: sub.example.com, example.com or one domain per line)')); - o.placeholder = 'example.com, sub.example.com\ndomain.com test.com\nsubdomain.domain.com another.com, third.com'; - o.depends('second_custom_domains_list_enabled', 'text'); - o.rows = 10; - o.rmempty = false; - o.ucisection = 'second'; - o.validate = function (section_id, value) { - if (!value || value.length === 0) { - return true; - } - - const domains = value.split(/[,\s\n]/) - .map(d => d.trim()) - .filter(d => d.length > 0); - - const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/; - - for (const domain of domains) { - if (!domainRegex.test(domain)) { - return _('Invalid domain format: ' + domain + '. Enter domain without protocol'); - } - } - return true; - }; - - o = s.taboption('secondary_config', form.ListValue, 'second_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')); - o.default = 'disabled'; - o.rmempty = false; - o.depends('second_enable', '1'); - o.ucisection = 'second'; - - o = s.taboption('secondary_config', form.DynamicList, 'second_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('second_custom_subnets_list_enabled', 'dynamic'); - o.rmempty = false; - o.ucisection = 'second'; - 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('secondary_config', form.TextValue, 'second_custom_subnets_text', _('User Subnets List'), _('Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline')); - o.placeholder = '103.21.244.0/22\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 10.10.10.10'; - o.depends('second_custom_subnets_list_enabled', 'text'); - o.rows = 10; - o.rmempty = false; - o.ucisection = 'second'; - o.validate = function (section_id, value) { - if (!value || value.length === 0) { - return true; - } - - const subnets = value.split(/[,\s\n]/) - .map(s => s.trim()) - .filter(s => s.length > 0); - - const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; - - for (const subnet of subnets) { - if (!subnetRegex.test(subnet)) { - return _('Invalid format: ' + subnet + '. Use format: X.X.X.X or X.X.X.X/Y'); - } - - 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: ' + subnet); - } - } - - if (cidr !== undefined) { - const cidrNum = parseInt(cidr); - if (cidrNum < 0 || cidrNum > 32) { - return _('CIDR must be between 0 and 32 in: ' + subnet); - } - } - } - return true; - }; - o = s.tab('diagnostics', _('Diagnostics')); function formatDiagnosticOutput(output) { @@ -668,7 +429,7 @@ return view.extend({ `: ${status === 'available' ? '✓' : '✗'}`); } - // Check All - полная диагностика + // Check All - full diagnostic o = s.taboption('diagnostics', form.Button, '_check_all'); o.title = _('Main Check'); o.description = _('Run a comprehensive diagnostic check of all components'); @@ -810,6 +571,333 @@ return view.extend({ ]); }; + + // Add new section 'extra' + var s = m.section(form.TypedSection, 'extra', _('Extra configurations')); + s.anonymous = false; + s.addremove = true; + s.addbtntitle = _('Add Section'); + + o = s.tab('basic', _('Extra configuration')); + + 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 = 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 = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), _('Enter connection string starting with vless:// or ss:// for proxy configuration')); + o.depends('proxy_config_type', 'url'); + o.rows = 5; + + 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.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.ListValue, 'interface', _('Network Interface'), _('Select network interface for VPN connection')); + o.depends('mode', 'vpn'); + + try { + const devices = await network.getDevices(); + const excludeInterfaces = ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0']; + + devices.forEach(function (device) { + if (device.dev && device.dev.name) { + const deviceName = device.dev.name; + const isExcluded = excludeInterfaces.includes(deviceName) || /^lan\d+$/.test(deviceName); + + if (!isExcluded) { + o.value(deviceName, deviceName); + } + } + }); + } catch (error) { + console.error('Error fetching devices:', error); + } + + o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists')); + o.default = '0'; + o.rmempty = false; + + o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service networks for routing') + ' github.com/itdoginfo/allow-domains'); + o.placeholder = 'Service list'; + o.value('russia_inside', 'Russia inside'); + o.value('russia_outside', 'Russia outside'); + o.value('ukraine_inside', 'Ukraine'); + o.value('geoblock', 'GEO Block'); + o.value('block', 'Block'); + o.value('porn', 'Porn'); + o.value('news', 'News'); + o.value('anime', 'Anime'); + o.value('youtube', 'Youtube'); + o.value('discord', 'Discord'); + o.value('meta', 'Meta'); + o.value('twitter', 'Twitter (X)'); + o.value('hdrezka', 'HDRezka'); + o.value('tiktok', 'Tik-Tok'); + o.value('telegram', 'Telegram'); + o.depends('domain_list_enabled', '1'); + o.rmempty = 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 = 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.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)'); + } + return true; + }; + + o = s.taboption('basic', form.TextValue, 'custom_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline (example: sub.example.com, example.com or one domain per line)')); + o.placeholder = 'example.com, sub.example.com\ndomain.com test.com\nsubdomain.domain.com another.com, third.com'; + o.depends('custom_domains_list_type', 'text'); + o.rows = 10; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + const domains = value.split(/[,\s\n]/) + .map(d => d.trim()) + .filter(d => d.length > 0); + + const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/; + + for (const domain of domains) { + if (!domainRegex.test(domain)) { + return _('Invalid domain format: ' + domain + '. Enter domain without protocol'); + } + } + 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 = s.taboption('basic', form.DynamicList, 'custom_local_domains', _('Local Domain Lists Path'), _('Enter to the list file path')); + o.placeholder = '/path/file.lst'; + o.depends('custom_local_domains_list_enabled', '1'); + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + try { + const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/; + if (!pathRegex.test(value)) { + throw new Error(_('Invalid path format. Path must start with "/" and contain only valid characters (letters, numbers, "-", "_", "/", ".")')); + } + return true; + } catch (e) { + return _('Invalid path format'); + } + }; + + 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 = 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.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + try { + const url = new URL(value); + if (!['http:', 'https:'].includes(url.protocol)) { + return _('URL must use http:// or https:// protocol'); + } + return true; + } catch (e) { + return _('Invalid URL format. URL must start with http:// or https://'); + } + }; + + + 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 = 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.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'); + } + + // Разбираем IP и маску + 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')); + o.placeholder = '103.21.244.0/22\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 10.10.10.10'; + o.depends('custom_subnets_list_enabled', 'text'); + o.rows = 10; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + // Split by commas, spaces and newlines + const subnets = value.split(/[,\s\n]/) + .map(s => s.trim()) + .filter(s => s.length > 0); + + const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; + + for (const subnet of subnets) { + if (!subnetRegex.test(subnet)) { + return _('Invalid format: ' + subnet + '. Use format: X.X.X.X or X.X.X.X/Y'); + } + + 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: ' + subnet); + } + } + + if (cidr !== undefined) { + const cidrNum = parseInt(cidr); + if (cidrNum < 0 || cidrNum > 32) { + return _('CIDR must be between 0 and 32 in: ' + subnet); + } + } + } + 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 = 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.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + try { + const url = new URL(value); + if (!['http:', 'https:'].includes(url.protocol)) { + return _('URL must use http:// or https:// protocol'); + } + return true; + } catch (e) { + return _('Invalid URL format. URL must start with http:// or https://'); + } + }; + + 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 = 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.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; + }; + + // For future + // o = s.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080 (extra +1)')); + // o.default = '0'; + // o.rmempty = false; return m.render(); } }); \ No newline at end of file diff --git a/podkop/Makefile b/podkop/Makefile index 6eb2fe7..e6b38ad 100644 --- a/podkop/Makefile +++ b/podkop/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=podkop -PKG_VERSION:=0.2.5 +PKG_VERSION:=0.3.0 PKG_RELEASE:=1 PKG_MAINTAINER:=ITDog @@ -12,7 +12,7 @@ include $(INCLUDE_DIR)/package.mk define Package/podkop SECTION:=net CATEGORY:=Network - DEPENDS:=+dnsmasq-full +curl +jq +kmod-nft-tproxy +coreutils-base64 + DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64 TITLE:=Domain routing app URL:=https://itdog.info PKGARCH:=all @@ -33,8 +33,6 @@ define Package/podkop/prerm grep -q "105 podkop" /etc/iproute2/rt_tables && sed -i "/105 podkop/d" /etc/iproute2/rt_tables -rm -f /etc/hotplug.d/iface/50-podkop - exit 0 endef @@ -49,9 +47,6 @@ define Package/podkop/install $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/etc/config/podkop $(1)/etc/config/podkop - - $(INSTALL_DIR) $(1)/etc/hotplug.d/iface - $(INSTALL_DATA) ./files/etc/hotplug.d/iface/50-podkop $(1)/etc/hotplug.d/iface/50-podkop endef $(eval $(call BuildPackage,podkop)) diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index 3268af8..0976aa8 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -1,13 +1,12 @@ config main 'main' - option mode '' - option interface '' + option mode 'proxy' + #option interface '' option proxy_config_type '' #option outbound_json '' - #option proxy_string '' + option proxy_string '' option domain_list_enabled '1' - option domain_list 'ru_inside' + option domain_list 'russia_inside' option subnets_list_enabled '0' - #list subnets 'twitter' option custom_domains_list_type 'disable' #list custom_domains '' #option custom_domains_text '' @@ -29,21 +28,5 @@ config main 'main' option yacd '0' option socks5 '0' option exclude_ntp '0' - option update_interval '' - option custom_domains_text - -config second 'second' - option second_enable '0' - option second_mode 'proxy' - option second_interface '' - option second_proxy_config_type '' - #option second_outbound_json '' - #option second_proxy_string '' - option second_domain_service_enabled '0' - #list second_service_list 'youtube' - option second_custom_domains_type 'disable' - #list second_custom_domains 'ifconfig.io' - #option second_custom_domains_text '' - option second_custom_subnets_type 'disable' - #list second_custom_subnets '' - #porion second_custom_subnets_text '' \ No newline at end of file + option update_interval '1d' + option custom_domains_text \ No newline at end of file diff --git a/podkop/files/etc/hotplug.d/iface/50-podkop b/podkop/files/etc/hotplug.d/iface/50-podkop deleted file mode 100755 index 8c7fa66..0000000 --- a/podkop/files/etc/hotplug.d/iface/50-podkop +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh - -. /lib/functions.sh -config_load "/etc/config/podkop" -. /etc/init.d/podkop - -config_get "interface" "main" "interface" "0" -config_get "mode" "main" "mode" "0" -if [ "$mode" = "vpn" ] && [ -n "$interface" ]; then - add_route_interface "$interface" "podkop" -fi - -if [ "$mode" = "proxy" ]; then - echo "Add route for main tproxy" - add_route_tproxy podkop -fi - -config_get second_enable "second" "second_enable" "0" -config_get second_interface "second" "second_interface" "0" -config_get "second_mode" "second" "second_mode" "0" -if [ "$second_enable" -eq "1" ] && [ "$second_mode" = "vpn" ] && [ -n "$second_interface" ]; then - add_route_interface "$second_interface" "podkop2" -fi - -if [ "$second_enable" -eq "1" ] && [ "$second_mode" = "proxy" ]; then - echo "Add route for second tproxy" - add_route_tproxy podkop2 -fi \ No newline at end of file diff --git a/podkop/files/etc/init.d/podkop b/podkop/files/etc/init.d/podkop index 01a04c7..065af15 100755 --- a/podkop/files/etc/init.d/podkop +++ b/podkop/files/etc/init.d/podkop @@ -7,9 +7,8 @@ script=$(readlink "$initscript") NAME="$(basename ${script:-$initscript})" config_load "$NAME" -EXTRA_COMMANDS="list_update add_route_interface check_proxy check_nft check_github check_logs check_all check_three" +EXTRA_COMMANDS="list_update check_proxy check_nft check_github check_logs check_all check_three" EXTRA_HELP=" list_update Updating domain and subnet lists - add_route_interface Adding route for interface sing_box_config_vless For test vless string check_proxy Check if sing-box proxy works correctly check_nft Show PodkopTable nftables rules @@ -21,260 +20,64 @@ EXTRA_HELP=" list_update Updating domain and subnet lists [ ! -L /usr/sbin/podkop ] && ln -s /etc/init.d/podkop /usr/sbin/podkop 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_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" SING_BOX_CONFIG="/etc/sing-box/config.json" - -config_get update_interval "main" "update_interval" "0 4 * * *" -cron_job="${update_interval} /etc/init.d/podkop list_update" +CACHE_FILE_PATH="/tmp/cache.db" +FAKEIP="198.18.0.0/15" start_service() { log "Start podkop" - dnsmasqfull - routing_table_create - add_mark + sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}') + required_version="1.11.1" - config_get mode "main" "mode" - case "$mode" in - "vpn") - log "VPN mode" - log "You are using VPN mode, make sure you have installed all the necessary packages, configured, created the zone and forwarding." - config_get interface "main" "interface" "0" - if [ -n "$interface" ]; then - add_route_interface "$interface" "podkop" - else - log "Interface undefined" - fi - - config_get_bool second_enable "second" "second_enable" "0" - config_get second_mode "second" "second_mode" "0" - if [ "$second_enable" -eq "1" ] && [ "$second_mode" = "proxy" ]; then - config_get proxy_config_type "second" "second_proxy_config_type" - - if [ "$proxy_config_type" = "outbound" ]; then - config_get outbound_json "second" "second_outbound_json" - if [ -n "$outbound_json" ]; then - log "Using JSON outbound configuration for second proxy" - sing_box_config_outbound_json "$outbound_json" "1603" - else - log "Missing outbound JSON configuration" - return - fi - else - config_get proxy_string "second" "second_proxy_string" - if [[ "$proxy_string" =~ ^ss:// ]]; then - sing_box_config_shadowsocks "$proxy_string" "1603" - elif [[ "$proxy_string" =~ ^vless:// ]]; then - sing_box_config_vless "$proxy_string" "1603" - else - log "Unsupported proxy type or missing configuration" - return - fi - fi - add_route_tproxy podkop2 - sing_box_config_check - sing_box_uci - /etc/init.d/sing-box restart - /etc/init.d/sing-box enable - fi - - if [ "$second_enable" -eq "1" ] && [ "$second_mode" = "vpn" ]; then - log "VPN mode for second" - config_get interface "second" "second_interface" "0" - if [ -n "$interface" ]; then - add_route_interface "$interface" "podkop2" - else - log "Interface undefined" - fi - fi - ;; - "proxy") - log "Proxy mode" - if ! command -v sing-box >/dev/null 2>&1; then - log "Sing-box isn't installed. Proxy mode works with sing-box" - return - fi - - # Main - proxy, Second - proxy - config_get_bool second_enable "second" "second_enable" "0" - config_get second_mode "second" "second_mode" "0" - if [ "$second_enable" -eq "1" ] && [ "$second_mode" = "proxy" ]; then - log "Two proxy enable" - outbound_main=$(mktemp) - outbound_second=$(mktemp) - - # Main proxy config - config_get proxy_config_type main "proxy_config_type" - if [ "$proxy_config_type" = "outbound" ]; then - config_get outbound_json main "outbound_json" - if [ -n "$outbound_json" ]; then - echo '{"outbounds":[' > "$outbound_main" - echo "$outbound_json" | jq '. + {tag: "main"}' >> "$outbound_main" - echo ']}' >> "$outbound_main" - else - log "Missing main outbound JSON configuration" - rm -f "$outbound_main" "$outbound_second" - return - fi - else - config_get proxy_string main "proxy_string" - if [[ "$proxy_string" =~ ^ss:// ]]; then - sing_box_config_shadowsocks "$proxy_string" "1602" - jq '.outbounds[0] + {tag: "main"} | {outbounds: [.]}' $SING_BOX_CONFIG > "$outbound_main" - elif [[ "$proxy_string" =~ ^vless:// ]]; then - sing_box_config_vless "$proxy_string" "1602" - jq '.outbounds[0] + {tag: "main"} | {outbounds: [.]}' $SING_BOX_CONFIG > "$outbound_main" - else - log "Unsupported proxy type or missing configuration for main" - rm -f "$outbound_main" "$outbound_second" - return - fi - fi - - # Second proxy config - config_get proxy_config_type second "second_proxy_config_type" - if [ "$proxy_config_type" = "outbound" ]; then - config_get outbound_json second "second_outbound_json" - if [ -n "$outbound_json" ]; then - echo '{"outbounds":[' > "$outbound_second" - echo "$outbound_json" | jq '. + {tag: "second"}' >> "$outbound_second" - echo ']}' >> "$outbound_second" - else - log "Missing second outbound JSON configuration" - rm -f "$outbound_main" "$outbound_second" - return - fi - else - config_get proxy_string "second" "second_proxy_string" - if [[ "$proxy_string" =~ ^ss:// ]]; then - sing_box_config_shadowsocks "$proxy_string" "1603" - jq '.outbounds[0] + {tag: "second"} | {outbounds: [.]}' $SING_BOX_CONFIG > "$outbound_second" - elif [[ "$proxy_string" =~ ^vless:// ]]; then - sing_box_config_vless "$proxy_string" "1603" - jq '.outbounds[0] + {tag: "second"} | {outbounds: [.]}' $SING_BOX_CONFIG > "$outbound_second" - else - log "Unsupported proxy type or missing configuration for second" - rm -f "$outbound_main" "$outbound_second" - return - fi - fi - - jq -s '{ - "log": {"level": "warn"}, - "inbounds": [ - { - "type": "tproxy", - "listen": "::", - "listen_port": 1602, - "sniff": false, - "tag": "main" - }, - { - "type": "tproxy", - "listen": "::", - "listen_port": 1603, - "sniff": false, - "tag": "second" - } - ], - "outbounds": (.[0].outbounds + .[1].outbounds), - "route": { - "rules": [ - { - "inbound": "main", - "outbound": "main" - }, - { - "inbound": "second", - "outbound": "second" - } - ], - "auto_detect_interface": true - } - }' "$outbound_main" "$outbound_second" > $SING_BOX_CONFIG - - rm -f "$outbound_main" "$outbound_second" - - add_route_tproxy podkop - add_route_tproxy podkop2 - fi - - # Main proxy, second disable/vpn - config_get_bool second_enable "second" "second_enable" "0" - config_get second_mode "second" "second_mode" "0" - if [ "$second_enable" -eq "0" ] || [ "$second_mode" = "vpn" ]; then - config_get proxy_config_type main "proxy_config_type" - - if [ "$proxy_config_type" = "outbound" ]; then - config_get outbound_json main "outbound_json" - if [ -n "$outbound_json" ]; then - log "Using JSON outbound configuration" - sing_box_config_outbound_json "$outbound_json" "1602" - else - log "Missing outbound JSON configuration" - return - fi - else - config_get proxy_string main "proxy_string" - if [[ "$proxy_string" =~ ^ss:// ]]; then - sing_box_config_shadowsocks "$proxy_string" "1602" - elif [[ "$proxy_string" =~ ^vless:// ]]; then - sing_box_config_vless "$proxy_string" "1602" - else - log "Unsupported proxy type or missing configuration" - return - fi - fi - add_route_tproxy podkop - fi - - sing_box_config_check - sing_box_uci - /etc/init.d/sing-box restart - /etc/init.d/sing-box enable - - # Main proxy, Second VPN - config_get_bool second_enable "second" "second_enable" "0" - config_get second_mode "second" "second_mode" "0" - if [ "$second_enable" -eq "1" ] && [ "$second_mode" = "vpn" ]; then - log "VPN mode for seconds" - log "You are using VPN mode, make sure you have installed all the necessary packages, configured, created the zone and forwarding." - config_get interface "second" "second_interface" "0" - if [ -n "$interface" ]; then - add_route_interface "$interface" "podkop2" - else - log "Interface undefined" - fi - fi - ;; - *) - log "Requires *vpn* or *proxy* value" - return - ;; - esac - - list_update - - if [ "$domain_list_enabled" -eq 1 ] || [ "$subnets_list_enabled" -eq 1 ]; then - add_cron_job + if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then + echo "The version of sing-box ($sing_box_version) is lower than the minimum version. Update sing-box: opkg update && opkg install sing-box" + exit 1 fi + + config_foreach wget_github - config_get_bool all_traffic_from_ip_enabled "main" "all_traffic_from_ip_enabled" "0" - if [ "$all_traffic_from_ip_enabled" -eq 1 ]; then - log "Adding an IP to redirect all traffic" - config_list_foreach main all_traffic_ip list_all_traffic_from_ip - fi + mkdir -p /tmp/podkop + # base + route_table_rule_mark + create_nft_table + sing_box_uci + + # sing-box + sing_box_inbound_proxy 1602 + sing_box_dns + sing_box_dns_rule_fakeip + sing_box_rule_dns + sing_box_cache_file + process_socks5 + + # sing-box outbounds and rules + config_foreach sing_box_outdound + config_foreach process_domains_for_section + config_foreach process_remote_ruleset + config_foreach sing_box_rule_preset + config_foreach process_domains_list_local + config_foreach process_domains_list_url + config_foreach process_subnet_for_section + config_foreach process_subnet_for_section_remote + config_foreach process_all_traffic_for_section + config_foreach add_cron_job + + #Future: exclude at the fakeip? config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" "0" if [ "$exclude_from_ip_enabled" -eq 1 ]; then log "Adding an IP for exclusion" - config_list_foreach main exclude_traffic_ip list_exclude_traffic_from_ip + config_list_foreach main exclude_traffic_ip sing_box_rules_source_ip_cidr $exclude_traffic_ip direct-out fi config_get_bool yacd "main" "yacd" "0" @@ -284,19 +87,6 @@ start_service() { "external_ui": "ui", "external_controller": "0.0.0.0:9090" }' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG - /etc/init.d/sing-box restart - fi - - config_get_bool socks5 "main" "socks5" "0" - if [ "$socks5" -eq 1 ]; then - log "Socks5 local enable port 2080" - jq '.inbounds += [{ - "type": "mixed", - "listen": "0.0.0.0", - "listen_port": 2080, - "set_system_proxy": false - }]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG - /etc/init.d/sing-box restart fi config_get_bool exclude_ntp "main" "exclude_ntp" "0" @@ -304,12 +94,25 @@ start_service() { log "NTP traffic exclude for proxy" nft insert rule inet PodkopTable mangle udp dport 123 return fi + + sing_box_config_check + /etc/init.d/sing-box restart + /etc/init.d/sing-box enable + + config_get proxy_string "main" "proxy_string" + config_get interface "main" "interface" + + if [ -n "$proxy_string" ] || [ -n "$interface" ]; then + dnsmasq_add + fi } stop_service() { log "Stopping the podkop" - rm -f /tmp/dnsmasq.d/podkop* remove_cron_job + dnsmasq_rm + + rm -rf /tmp/podkop/* log "Flush nft" if nft list table inet PodkopTable >/dev/null 2>&1; then @@ -321,27 +124,15 @@ stop_service() { ip rule del fwmark 0x105 table podkop priority 105 fi - if ip rule list | grep -q "podkop2"; then - ip rule del fwmark 0x106 table podkop2 priority 106 - fi - log "Flush ip route" - if ip route list table podkop; then + if ip route list table podkop >/dev/null 2>&1; then ip route flush table podkop fi - if ip route list table podkop2; then - ip route flush table podkop2 - fi - log "Stop sing-box" - config_get mode_main "main" "mode" "0" - config_get mode_second "second" "second_mode" "0" + /etc/init.d/sing-box stop + /etc/init.d/sing-box disable - if [ "$mode_main" = "proxy" ] || [ "$mode_second" = "proxy" ]; then - /etc/init.d/sing-box stop - /etc/init.d/sing-box disable - fi } restart_service() { @@ -380,13 +171,187 @@ nolog() { echo -e "${CYAN}[$timestamp]${RESET} ${GREEN}$message${RESET}" } +# Main funcs + +route_table_rule_mark() { + local table=podkop + + grep -q "105 $table" /etc/iproute2/rt_tables || echo "105 $table" >>/etc/iproute2/rt_tables + + if ! ip route list table $table | grep -q "local default dev lo scope host"; then + log "Added route for tproxy" + ip route add local 0.0.0.0/0 dev lo table $table + else + log "Route for tproxy exists" + fi + + if ! ip rule list | grep -q "from all fwmark 0x105 lookup $table"; then + log "Create marking rule" + ip -4 rule add fwmark 0x105 table $table priority 105 + else + log "Marking rule exist" + fi +} + +create_nft_table() { + local table="PodkopTable" + + nft add table inet $table + log "Create nft rules" + nft add chain inet $table mangle { type filter hook prerouting priority -150 \; policy accept \;} + nft add chain inet $table proxy { type filter hook prerouting priority -100 \; policy accept \;} + + nft add set inet $table podkop_subnets { type ipv4_addr\; flags interval\; auto-merge\; } + + nft add rule inet $table mangle iifname "br-lan" ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x105 counter + nft add rule inet $table mangle iifname "br-lan" ip daddr @podkop_subnets meta l4proto udp meta mark set 0x105 counter + nft add rule inet $table mangle iifname "br-lan" ip daddr "$FAKEIP" meta l4proto tcp meta mark set 0x105 counter + nft add rule inet $table mangle iifname "br-lan" ip daddr "$FAKEIP" meta l4proto udp meta mark set 0x105 counter + + nft add rule inet $table proxy meta mark 0x105 meta l4proto tcp tproxy ip to :1602 counter + nft add rule inet $table proxy meta mark 0x105 meta l4proto udp tproxy ip to :1602 counter +} + +dnsmasq_add() { + ## Future: Check config and skip restart + log "Configure dnsmasq for sing-box" + uci set dhcp.@dnsmasq[0].noresolv="1" + uci set dhcp.@dnsmasq[0].filter_aaaa="1" + uci set dhcp.@dnsmasq[0].cachesize="0" + uci -q delete dhcp.@dnsmasq[0].server + uci add_list dhcp.@dnsmasq[0].server="127.0.0.1#5353" + uci add_list dhcp.@dnsmasq[0].server='/use-application-dns.net/' + uci commit dhcp + + grep -q "filter-rr=HTTPS" /etc/dnsmasq.conf || echo "filter-rr=HTTPS" >> /etc/dnsmasq.conf + + /etc/init.d/dnsmasq restart +} + +dnsmasq_rm() { + log "Removing configuration for dnsmasq" + uci set dhcp.@dnsmasq[0].noresolv="0" + uci set dhcp.@dnsmasq[0].filter_aaaa="0" + uci set dhcp.@dnsmasq[0].cachesize="1000" + uci -q delete dhcp.@dnsmasq[0].server + uci commit dhcp + + sed -i '/filter-rr=HTTPS/d' /etc/dnsmasq.conf + + /etc/init.d/dnsmasq restart +} + +process_domains_text() { + local text="$1" + local name="$2" + + local tmp_file=$(mktemp) + echo "$text" > "$tmp_file" + + sed 's/[, ]\+/\n/g' "$tmp_file" | while IFS= read -r domain; do + domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$domain" ]; then + sing_box_ruleset_domains "$domain" "$name" + fi + done + + rm -f "$tmp_file" +} + +process_subnets_text() { + local text="$1" + local name="$2" + + local tmp_file=$(mktemp) + echo "$text" > "$tmp_file" + + sed 's/[, ]\+/\n/g' "$tmp_file" | while IFS= read -r subnet; do + subnet=$(echo "$subnet" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$subnet" ]; then + if ! echo "$subnet" | grep -q "/"; then + subnet="$subnet/32" + fi + sing_box_ruleset_subnets "$subnet" "$name" + fi + done + + rm -f "$tmp_file" +} + +wget_github() { + local count=0 + + config_get domain_list_enabled "$section" "domain_list_enabled" + config_get subnets_list_enabled "$section" "subnets_list_enabled" + config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled" + config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled" + + if [ "$domain_list_enabled" -eq 1 ] || [ "$subnets_list_enabled" -eq 1 ] || + [ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ] ; then + + while true; do + if ! curl -m 3 github.com; then + log "GitHub is not available. Check the internet availability [$count sec]" + count=$((count + 1)) + else + return + fi + + if [ $count -lt 30 ]; then + sleep_interval=1 + elif [ $count -ge 30 ] && [ $count -lt 60 ]; then + sleep_interval=5 + elif [ $count -ge 60 ] && [ $count -lt 90 ]; then + sleep_interval=10 + else + sleep_interval=30 + fi + + sleep $sleep_interval + done + fi +} + + add_cron_job() { - remove_cron_job - crontab -l | { - cat - echo "$cron_job" - } | crontab - - log "The cron job has been created: $cron_job" + ## Future: make a check so that it doesn't recreate many times + config_get domain_list_enabled "$section" "domain_list_enabled" + config_get subnets_list_enabled "$section" "subnets_list_enabled" + config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled" + config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled" + config_get update_interval "main" "update_interval" + + case "$update_interval" in + "1h") + cron_job="13 * * * * /etc/init.d/podkop list_update" + ;; + "3h") + cron_job="13 */3 * * * /etc/init.d/podkop list_update" + ;; + "12h") + cron_job="13 */12 * * * /etc/init.d/podkop list_update" + ;; + "1d") + cron_job="13 9 * * * /etc/init.d/podkop list_update" + ;; + "3d") + cron_job="13 9 */3 * * /etc/init.d/podkop list_update" + ;; + *) + log "Invalid update_interval value: $update_interval" + return + ;; + esac + + if [ "$domain_list_enabled" -eq 1 ] || [ "$subnets_list_enabled" -eq 1 ] || + [ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ] ; then + remove_cron_job + crontab -l | { + cat + echo "$cron_job" + } | crontab - + log "The cron job has been created: $cron_job" + fi } remove_cron_job() { @@ -395,506 +360,249 @@ remove_cron_job() { } list_update() { - # Main domains processing - config_get_bool domain_list_enabled "main" "domain_list_enabled" "0" - if [ "$domain_list_enabled" -eq 1 ]; then - log "Adding a common domains list" - add_set "podkop_domains" "main" - config_get domain_list main "domain_list" - lists_domains_download "$domain_list" - dnsmasq_config_check podkop-domains.lst - fi - - # Main custom domains processing - config_get custom_domains_list_type "main" "custom_domains_list_enabled" "disabled" - if [ "$custom_domains_list_type" != "disabled" ]; then - log "Adding a custom domains list" - add_set "podkop_domains" "main" - rm -f /tmp/dnsmasq.d/podkop-custom-domains.lst - - if [ "$custom_domains_list_type" = "dynamic" ]; then - config_list_foreach main custom_domains "list_custom_domains_create" "podkop" - elif [ "$custom_domains_list_type" = "text" ]; then - config_get custom_domains_text main "custom_domains_text" - process_domains_text "$custom_domains_text" "podkop" - fi - - dnsmasq_config_check podkop-custom-domains.lst - fi - - # Main custom download domains - config_get_bool custom_download_domains_list_enabled "main" "custom_download_domains_list_enabled" "0" - if [ "$custom_download_domains_list_enabled" -eq 1 ]; then - log "Adding a custom domains list from URL" - add_set "podkop_domains" "main" - config_list_foreach main custom_download_domains "list_custom_download_domains_create" "podkop" - fi - - # Main domains delist - config_get_bool custom_local_domains_list_enabled "main" "custom_local_domains_list_enabled" "0" - if [ "$custom_local_domains_list_enabled" -eq 1 ]; then - log "Adding a custom local domain list" - add_set "podkop_domains" "main" - config_list_foreach main custom_local_domains "list_custom_local_domains_create" "podkop" - fi - - config_get_bool delist_domains_enabled "main" "delist_domains_enabled" "0" - if [ "$delist_domains_enabled" -eq 1 ] && [ "$domain_list_enabled" -eq 1 ]; then - log "Exclude domains from the common list" - config_list_foreach main delist_domains "list_delist_domains" - dnsmasq_config_check podkop-domains.lst - fi - - # Main subnets processing - config_get_bool subnets_list_enabled "main" "subnets_list_enabled" "0" - if [ "$subnets_list_enabled" -eq 1 ]; then - log "Adding a subnets from list" - mkdir -p /tmp/podkop - add_set "podkop_subnets" "main" - config_list_foreach main subnets "list_subnets_download" - fi - - # Main custom subnets - config_get custom_subnets_list_type "main" "custom_subnets_list_enabled" "disabled" - if [ "$custom_subnets_list_type" != "disabled" ]; then - log "Adding a custom subnets list" - add_set "podkop_subnets" "main" - - if [ "$custom_subnets_list_type" = "dynamic" ]; then - config_list_foreach main custom_subnets list_custom_subnets_preprocess "podkop" - elif [ "$custom_subnets_list_type" = "text" ]; then - config_get custom_subnets_text main "custom_subnets_text" - process_subnets_text "$custom_subnets_text" "podkop" - fi - fi - - # Main custom download subnets - config_get_bool custom_download_subnets_list_enabled "main" "custom_download_subnets_list_enabled" "0" - if [ "$custom_download_subnets_list_enabled" -eq 1 ]; then - log "Adding a subnets from URL" - mkdir -p /tmp/podkop - add_set "podkop_subnets" "main" - config_list_foreach main custom_download_subnets "list_subnets_download" - fi - - # Second custom domains processing - config_get second_custom_domains_list_type "second" "second_custom_domains_list_enabled" "disabled" - if [ "$second_custom_domains_list_type" != "disabled" ]; then - log "Adding a custom domains list. Second podkop" - add_set "podkop2_domains" "second" - rm -f /tmp/dnsmasq.d/podkop2-custom-domains.lst - - if [ "$second_custom_domains_list_type" = "dynamic" ]; then - config_list_foreach second second_custom_domains "list_custom_domains_create" "podkop2" - elif [ "$second_custom_domains_list_type" = "text" ]; then - config_get second_custom_domains_text second "second_custom_domains_text" - process_domains_text "$second_custom_domains_text" "podkop2" - fi - - dnsmasq_config_check podkop2-custom-domains.lst - fi - - # Second service domains - config_get_bool second_domain_service_enabled "second" "second_domain_service_enabled" "0" - if [ "$second_domain_service_enabled" -eq 1 ]; then - log "Adding a service for podkop2" - add_set "podkop2_domains" "second" - config_get second_service_list second "second_service_list" - lists_services_download "$second_service_list" - config_list_foreach second second_custom_domains "list_delist_domains" - dnsmasq_config_check podkop2-domains.lst - fi - - # Second custom subnets - config_get second_custom_subnets_list_type "second" "second_custom_subnets_list_enabled" "disabled" - if [ "$second_custom_subnets_list_type" != "disabled" ]; then - log "Adding a custom subnets list. Second" - add_set "podkop2_subnets" "second" - - if [ "$second_custom_subnets_list_type" = "dynamic" ]; then - config_list_foreach second second_custom_subnets list_custom_subnets_preprocess "podkop2" - elif [ "$second_custom_subnets_list_type" = "text" ]; then - config_get second_custom_subnets_text second "second_custom_subnets_text" - process_subnets_text "$second_custom_subnets_text" "podkop2" - fi - fi - - # Restart dnsmasq if needed - if [ "$domain_list_enabled" -eq 1 ] || [ "$custom_domains_list_type" != "disabled" ] || \ - [ "$custom_download_domains_list_enabled" -eq 1 ] || \ - [ "$second_custom_domains_list_type" != "disabled" ] || \ - [ "$second_domain_service_enabled" -eq 1 ]; then - /etc/init.d/dnsmasq restart - fi + log "Update remote lists" + config_foreach process_remote_ruleset + config_foreach process_domains_list_url + config_foreach process_subnet_for_section_remote } -dnsmasqfull() { - if /usr/sbin/dnsmasq -v | grep -q "no-nftset"; then - log "Dnsmasq-full is not installed. Future: link only" - log "Use script or:" - log "cd /tmp/ && /bin/opkg download dnsmasq-full && /bin/opkg remove dnsmasq && /bin/opkg install dnsmasq-full --cache /tmp/ && cp /etc/config/dhcp /etc/config/dhcp-old && mv /etc/config/dhcp-opkg /etc/config/dhcp" - return - fi -} -routing_table_create() { - grep -q "105 podkop" /etc/iproute2/rt_tables || echo '105 podkop' >>/etc/iproute2/rt_tables - config_get_bool second_enable "second" "second_enable" "0" - if [ "$second_enable" -eq 1 ]; then - grep -q "106 podkop2" /etc/iproute2/rt_tables || echo '106 podkop2' >>/etc/iproute2/rt_tables - fi -} +# sing-box funcs -add_set() { - local set_name="$1" - local connect="$2" - - nft add table inet PodkopTable - log "Create set $set_name" - nft add chain inet PodkopTable mangle { type filter hook prerouting priority -150 \; policy accept \;} - nft add set inet PodkopTable "$set_name" { type ipv4_addr\; flags interval\; auto-merge\; } - if [ "$connect" = "main" ]; then - config_get mode "$connect" "mode" +sing_box_uci() { + local config="/etc/config/sing-box" + if grep -q "option enabled '0'" "$config" || + grep -q "option user 'sing-box'" "$config"; then + sed -i \ + -e "s/option enabled '0'/option enabled '1'/" \ + -e "s/option user 'sing-box'/option user 'root'/" $config + log "Change sing-box UCI config" else - config_get mode "$connect" "second_mode" + log "Sing-box UCI config OK" fi +} + +# Future: for every section. +1 port? +process_socks5() { + config_get_bool socks5 "main" "socks5" "0" + if [ "$socks5" -eq 1 ]; then + log "Socks5 local enable port 2080" + jq '.inbounds += [{ + "tag": "mixed-in", + "type": "mixed", + "listen": "0.0.0.0", + "listen_port": 2080, + "set_system_proxy": false + }]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + + #local rule_exists=$(jq -r '.route.rules[] | select(.inbound[] == "mixed-in")' $SING_BOX_CONFIG) + local rule_exists=$(jq -r '.route.rules // [] | map(select(.inbound // [] | index("mixed-in"))) | length' $SING_BOX_CONFIG) + + if [ -z "$rule_exists" ]; then + jq '.route.rules += [{ + "inbound": ["mixed-in"], + "outbound": "main", + "action": "route" + }]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + fi + fi +} + +sing_box_inbound_proxy() { + local listen_port="$1" + + jq -n \ + --arg listen_port "$listen_port" \ + '{ + "log": { + "level": "warn" + }, + "inbounds": [ + { + "tag": "tproxy-in", + "type": "tproxy", + "listen": "::", + "listen_port": ($listen_port|tonumber), + "tcp_fast_open": true, + "udp_fragment": true + }, + { + "tag": "dns-in", + "type": "direct", + "listen": "127.0.0.1", + "listen_port": 5353 + } + ], + "outbounds": [ + { + "tag": "direct-out", + "type": "direct" + } + ] + }' > $SING_BOX_CONFIG +} + +sing_box_dns() { + log "Configure DNS in sing-box" + jq \ + --arg FAKEIP "$FAKEIP" \ + '.dns = { + "strategy": "ipv4_only", + "fakeip": { + "enabled": true, + "inet4_range": $FAKEIP + }, + "servers": [ + { + "tag": "cloudflare-doh-server", + "address": "https://1.1.1.1/dns-query", + "detour": "direct-out" + }, + { + "tag": "fakeip-server", + "address": "fakeip" + } + ] + }' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG +} + +sing_box_dns_rule_fakeip() { + log "Configure fakeip route in sing-box" + jq \ + '.dns += { + "rules": [ + { + "server": "fakeip-server", + "rule_set": [] + } + ] + }' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG +} + +sing_box_dns_rule_fakeip_section() { + local rule_set=$1 + + log "Adding section to fakeip route rules in sing-box" + jq \ + --arg rule_set "$rule_set" \ + '.dns.rules |= map( + if .server == "fakeip-server" then + .rule_set += [$rule_set] + else + . + end + )' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG +} + + +sing_box_cache_file() { + log "Configure cache.db in sing-box" + jq \ + --arg CACHE_FILE_PATH "$CACHE_FILE_PATH" \ + '.experimental = { + "cache_file": { + "enabled": true, + "store_fakeip": true, + "path": $CACHE_FILE_PATH + } + }' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG +} + +sing_box_outdound() { + local section="$1" + + config_get mode "$section" "mode" case "$mode" in "vpn") - if ! nft list chain inet PodkopTable mangle | grep -q "ip daddr @"$set_name" meta mark set"; then - if [ "$connect" = "main" ]; then - nft add rule inet PodkopTable mangle iifname "br-lan" ip daddr @"$set_name" meta mark set 0x105 counter - elif [ "$connect" = "second" ]; then - nft add rule inet PodkopTable mangle iifname "br-lan" ip daddr @"$set_name" meta mark set 0x106 counter - fi - fi + log "VPN mode" + log "You are using VPN mode, make sure you have installed all the necessary packages, configured, created the zone and forwarding." + config_get interface "$section" "interface" + sing_box_outbound_interface $section $interface ;; - "proxy") - nft add chain inet PodkopTable proxy { type filter hook prerouting priority -100 \; } - if nft list table inet PodkopTable | grep -q "ip daddr @"$set_name" meta l4proto"; then - log "Nft rule tproxy exists" + log "Proxy mode" + config_get proxy_config_type "$section" "proxy_config_type" + + if [ "$proxy_config_type" = "outbound" ]; then + config_get outbound_json $section "outbound_json" + if [ -n "$outbound_json" ]; then + log "Using JSON outbound configuration" + sing_box_config_outbound_json "$outbound_json" + else + log "Missing outbound JSON configuration" + return + fi else - log "Added nft rule tproxy" - if [ "$connect" = "main" ]; then - nft add rule inet PodkopTable mangle iifname "br-lan" ip daddr @"$set_name" meta l4proto tcp meta mark set 0x105 counter - nft add rule inet PodkopTable mangle iifname "br-lan" ip daddr @"$set_name" meta l4proto udp meta mark set 0x105 counter - if ! ( nft list table inet PodkopTable | grep -q "meta mark 0x00000105 meta l4proto tcp tproxy" ); then - nft add rule inet PodkopTable proxy meta mark 0x105 meta l4proto tcp tproxy ip to :1602 counter - nft add rule inet PodkopTable proxy meta mark 0x105 meta l4proto udp tproxy ip to :1602 counter - fi - elif [ "$connect" = "second" ]; then - nft add rule inet PodkopTable mangle iifname "br-lan" ip daddr @"$set_name" meta l4proto tcp meta mark set 0x106 counter - nft add rule inet PodkopTable mangle iifname "br-lan" ip daddr @"$set_name" meta l4proto udp meta mark set 0x106 counter - if ! ( nft list table inet PodkopTable | grep -q "meta mark 0x00000106 meta l4proto tcp tproxy" ); then - nft add rule inet PodkopTable proxy meta mark 0x106 meta l4proto tcp tproxy ip to :1603 counter - nft add rule inet PodkopTable proxy meta mark 0x106 meta l4proto udp tproxy ip to :1603 counter - fi + config_get proxy_string $section "proxy_string" + if [[ "$proxy_string" =~ ^ss:// ]]; then + sing_box_config_shadowsocks "$section" "$proxy_string" + elif [[ "$proxy_string" =~ ^vless:// ]]; then + sing_box_config_vless "$section" "$proxy_string" + else + log "Unsupported proxy type or missing configuration" + return fi fi ;; - *) log "Requires *vpn* or *proxy* value" return ;; - esac + esac } -add_route_interface() { - local interface="$1" - local table="$2" - local retry_count_route=0 - local max_retries=10 +sing_box_outbound_interface() { + local section="$1" + local interface="$2" - if ! ip link show "$interface" >/dev/null 2>&1; then - log "Interface "$interface" undetected, wait 10 sec..." - sleep 10 + jq --arg section "$section" \ + --arg interface "$interface" \ + '. | + .outbounds |= ( + map( + if .tag == $section then + . + {"type": "direct", "bind_interface": $interface} + else . end + ) + + ( + if (map(select(.tag == $section)) | length) == 0 then + [{"tag": $section, "type": "direct", "bind_interface": $interface}] + else [] end + ) + )' "$SING_BOX_CONFIG" > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG" - if ! ip link show "$interface" >/dev/null 2>&1; then - log "Interface "$interface" undetected. exit" - return - fi - fi - - if ! ip link show "$interface" >/dev/null 2>&1; then - log "Interface "$interface" does not exist, not possible to create a route" - return - fi - - if ip route show table $table | grep -q "^default dev"; then - log "Route for "$interface" exists" - return 0 - fi - - log "Added route for "$interface"" - while [ $retry_count_route -lt $max_retries ]; do - if ip route add table $table default dev "$interface" 2>&1 | grep -q "Network is down"; then - log "Attempt $retry_count_route: Interface "$interface" is down, retrying in 3 seconds..." - sleep 3 - retry_count_route=$((retry_count_route + 1)) - else - log "Route for "$interface" added" - return 0 - fi - done - - log "The maximum number of attempts has been exceeded. Failed to add a route." - return -} - -add_route_tproxy() { - local table=$1 - if ! ip route list table $table | grep -q "local default dev lo scope host"; then - log "Added route for tproxy" - ip route add local 0.0.0.0/0 dev lo table $table + if [ $? -eq 0 ]; then + log "Config updated successfully" else - log "Route for tproxy exists" + log "Error: Invalid JSON config generated" + return 1 fi } -add_mark() { - if ! ip rule list | grep -q "from all fwmark 0x105 lookup podkop"; then - log "Create marking rule" - ip -4 rule add fwmark 0x105 table podkop priority 105 - else - log "Marking rule exist" - fi - - config_get_bool second_enable "second" "second_enable" "0" - if [ "$second_enable" -eq 1 ]; then - if ! ip rule list | grep -q "from all fwmark 0x106 lookup podkop2"; then - log "Create marking rule for podkop second" - ip -4 rule add fwmark 0x106 table podkop2 priority 106 - else - log "Podkop second marking rule exist" - fi - fi +sing_box_rule_dns() { + log "Configure rule dns in sing-box" + jq \ + '.route += { + "rules": [ + { + "inbound": [ + "dns-in", + "tproxy-in" + ], + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ], + "auto_detect_interface": true + }' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG } -lists_domains_download() { - local URL="$1" - - case "$URL" in - "ru_inside") - URL=$DOMAINS_RU_INSIDE - ;; - "ru_outside") - URL=$DOMAINS_RU_OUTSIDE - ;; - "ua") - URL=$DOMAINS_UA - ;; - *) - log "Unidentified list of domains" - return - ;; - esac - - count=0 - while true; do - if curl -m 3 github.com; then - wget -q -O /tmp/dnsmasq.d/podkop-domains.lst $URL - if [ $? -eq 0 ]; then - sed -i 's/fw4#vpn_domains/PodkopTable#podkop_domains/g' /tmp/dnsmasq.d/podkop-domains.lst - return 0 - fi - else - log "GitHub is not available. Check the internet availability [$count sec]" - count=$((count + 1)) - fi - - if [ $count -lt 30 ]; then - sleep_interval=1 - elif [ $count -ge 30 ] && [ $count -lt 60 ]; then - sleep_interval=5 - elif [ $count -ge 60 ] && [ $count -lt 90 ]; then - sleep_interval=10 - else - sleep_interval=30 - fi - - sleep $sleep_interval - done -} - -lists_services_download() { - local URL="$1" - - case "$URL" in - "youtube") - URL=$DOMAINS_YOUTUBE - ;; - *) - log "Unidentified list of domains" - return - ;; - esac - - count=0 - while true; do - if ping -c 1 -W 3 github.com >/dev/null 2>&1; then - wget -q -O /tmp/dnsmasq.d/podkop2-domains.lst $URL - if [ $? -eq 0 ]; then - delist_downloaded_domains - sed -i 's/.*/nftset=\/&\/4#inet#PodkopTable#podkop2_domains/g' /tmp/dnsmasq.d/podkop2-domains.lst - return 0 - fi - else - log "GitHub is not available. Check the internet availability [$count sec]" - count=$((count + 1)) - fi - - if [ $count -lt 30 ]; then - sleep_interval=1 - elif [ $count -ge 30 ] && [ $count -lt 60 ]; then - sleep_interval=5 - elif [ $count -ge 60 ] && [ $count -lt 90 ]; then - sleep_interval=10 - else - sleep_interval=30 - fi - - sleep $sleep_interval - done -} - -list_subnets_download() { - local URL="$1" - - case "$URL" in - "twitter") - URL=$SUBNETS_TWITTER - ;; - "meta") - URL=$SUBNETS_META - ;; - "discord") - URL=$SUBNETS_DISCORD - ;; - *) - log "Custom URL for subnet" - if wget -q --spider "$URL"; then - log "URL is valid" - else - log "URL $URL is not valid" - return - fi - ;; - esac - - local filename=$(basename "$URL") - mkdir -p /tmp/podkop - wget -q -O "/tmp/podkop/$filename" "$URL" - while IFS= read -r subnet; do - nft add element inet PodkopTable podkop_subnets { $subnet } - done <"/tmp/podkop/$filename" -} - -list_custom_domains_create() { - local domain="$1" - local name="$2" - echo "nftset=/$domain/4#inet#PodkopTable#${name}_domains" >>"/tmp/dnsmasq.d/${name}-custom-domains.lst" - log "$domain added to the list" -} - -list_custom_local_domains_create() { - local local_file="$1" - local name="$2" - local filename=$(basename "$local_file" | cut -d. -f1) - local config="/tmp/dnsmasq.d/${name}-${filename}.lst" - - rm -f "$config" - while IFS= read -r domain; do - echo "nftset=/$domain/4#inet#PodkopTable#${name}_domains" >>$config - done <"$local_file" - dnsmasq_config_check ${name}-${filename}.lst -} - -list_custom_download_domains_create() { - local URL="$1" - local name="$2" - local filename=$(basename "$URL") - local config="/tmp/dnsmasq.d/${name}-${filename}.lst" - - rm -f "$config" - mkdir -p /tmp/podkop - wget -q -O "/tmp/podkop/${filename}" "$URL" - while IFS= read -r domain; do - echo "nftset=/$domain/4#inet#PodkopTable#${name}_domains" >>$config - done <"/tmp/podkop/$filename" - dnsmasq_config_check ${name}-${filename}.lst -} - -list_custom_subnets_create() { - local subnet="$1" - local name="$2" - nft add element inet PodkopTable ${name}_subnets { $subnet } -} - -list_all_traffic_from_ip() { - local ip="$1" - if ! nft list chain inet PodkopTable mangle | grep -q "ip saddr $ip"; then - config_get mode "main" "mode" "0" - if [ "$mode" = "vpn" ]; then - nft insert rule inet PodkopTable mangle iifname "br-lan" ip saddr $ip meta mark set 0x105 counter - elif [ "$mode" = "proxy" ]; then - nft add set inet PodkopTable localv4 { type ipv4_addr\; flags interval\; } - nft add element inet PodkopTable localv4 { \ - 0.0.0.0/8, \ - 10.0.0.0/8, \ - 127.0.0.0/8, \ - 169.254.0.0/16, \ - 172.16.0.0/12, \ - 192.0.0.0/24, \ - 192.0.2.0/24, \ - 192.88.99.0/24, \ - 192.168.0.0/16, \ - 198.18.0.0/15, \ - 198.51.100.0/24, \ - 203.0.113.0/24, \ - 224.0.0.0/4, \ - 240.0.0.0-255.255.255.255 } - nft insert rule inet PodkopTable mangle iifname "br-lan" ip saddr $ip meta l4proto { tcp, udp } meta mark set 0x105 counter - nft insert rule inet PodkopTable mangle ip saddr $ip ip daddr @localv4 return - fi - fi -} - -list_exclude_traffic_from_ip() { - local ip="$1" - if ! nft list chain inet PodkopTable mangle | grep -q "ip saddr $ip"; then - nft insert rule inet PodkopTable mangle ip saddr $ip return - fi -} - -list_delist_domains() { - local domain="$1" - - if [ -f "/tmp/dnsmasq.d/podkop-domains.lst" ]; then - sed -i "/$domain/d" /tmp/dnsmasq.d/podkop-domains.lst - nft flush set inet PodkopTable podkop_domains - log "Strings containing '$domain' have been excluded from the list" - else - log "Config /tmp/dnsmasq.d/podkop-domains.lst not exists" - fi -} - -delist_downloaded_domains() { - local domains="/tmp/dnsmasq.d/podkop2-domains.lst" - - if [ -f "$domains" ]; then - while IFS= read -r line; do - list_delist_domains "$line" - done <"$domains" - else - log "$domains not found" - fi -} - -dnsmasq_config_check() { - local config="$1" - if ! /usr/sbin/dnsmasq --conf-file=/tmp/dnsmasq.d/$config --test 2>&1 | grep -q "syntax check OK"; then - log "Dnsmasq config $config contains errors. Break" +sing_box_config_check() { + if ! sing-box -c $SING_BOX_CONFIG check >/dev/null 2>&1; then + log "Sing-box configuration is invalid" return fi } @@ -910,11 +618,18 @@ sing_box_config_outbound_json() { }, "inbounds": [ { - "type": "tproxy", - "listen": "::", - "listen_port": $listen_port, - "sniff": false - } + "type": "tproxy", + "listen": "::", + "listen_port": $listen_port, + "tcp_fast_open": true, + "udp_fragment": true + }, + { + "tag": "dns-in", + "type": "direct", + "listen": "127.0.0.1", + "listen_port": 5353 + } ], "outbounds": [], "route": { @@ -927,37 +642,21 @@ EOF rm -f /tmp/base_config.json } -sing_box_uci() { - local config="/etc/config/sing-box" - if grep -q "option enabled '0'" "$config" || - grep -q "option user 'sing-box'" "$config"; then - sed -i \ - -e "s/option enabled '0'/option enabled '1'/" \ - -e "s/option user 'sing-box'/option user 'root'/" $config - log "Change sing-box UCI config" - else - log "Sing-box UCI config OK" - fi -} + sing_box_config_shadowsocks() { - local STRING="$1" - local listen_port="$2" + local section="$1" + local STRING="$2" - # Определяем тип SS (2022 или old) по наличию : в base64 части if echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | base64 -d 2>/dev/null | grep -q ":"; then - # Old SS format - local encrypted_part=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | base64 --decode) + local encrypted_part=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | base64 -d 2>/dev/null ) local method=$(echo "$encrypted_part" | cut -d':' -f1) local password=$(echo "$encrypted_part" | cut -d':' -f2-) else - # SS 2022 format local method_and_password=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1) local method=$(echo "$method_and_password" | cut -d':' -f1) local password=$(echo "$method_and_password" | cut -d':' -f2- | sed 's/%3D/=/g') - - # Если method в base64, декодируем - if echo "$method" | base64 -d &>/dev/null; then + if echo "$method" | base64 -d ; then method=$(echo "$method" | base64 -d) fi fi @@ -965,45 +664,52 @@ sing_box_config_shadowsocks() { local server=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f1) local port=$(echo "$STRING" | sed -n 's|.*:\([0-9]\+\).*|\1|p') - # Create base config - cat > /tmp/ss_config.json << EOF -{ - "log": { - "level": "warn" - }, - "inbounds": [ - { - "type": "tproxy", - "listen": "::", - "listen_port": $listen_port, - "sniff": false - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "server": "$server", - "server_port": $port, - "method": "$method", - "password": "$password", - "udp_over_tcp": { - "enabled": true, - "version": 2 - } - } - ], - "route": { - "auto_detect_interface": true - } -} -EOF + jq \ + --arg section "$section" \ + --arg server "$server" \ + --argjson port "$port" \ + --arg method "$method" \ + --arg password "$password" \ + '. | + .outbounds |= ( + map( + if .tag == $section then + . + { + "type": "shadowsocks", + "server": $server, + "server_port": ($port | tonumber), + "method": $method, + "password": $password, + "udp_over_tcp": { "enabled": true, "version": 2 } + } + else . end + ) + + ( + if (map(select(.tag == $section)) | length) == 0 then + [{ + "tag": $section, + "type": "shadowsocks", + "server": $server, + "server_port": ($port | tonumber), + "method": $method, + "password": $password, + "udp_over_tcp": { "enabled": true, "version": 2 } + }] + else [] end + ) + )' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG - mv /tmp/ss_config.json $SING_BOX_CONFIG + if [ $? -eq 0 ]; then + log "Config updated successfully" + else + log "Error: Invalid JSON config generated" + return 1 + fi } sing_box_config_vless() { - local STRING="$1" - local listen_port="$2" + local section="$1" + local STRING="$2" get_param() { local param="$1" @@ -1016,8 +722,7 @@ sing_box_config_vless() { server=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f1 | tr -d '\n' | tr -d '\r' | sed 's/False//g') port=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f2 | cut -d'?' -f1 | cut -d'/' -f1 | cut -d'#' -f1 | tr -d '\n' | tr -d '\r' | sed 's/False//g') - jq -n \ - --arg listen_port "$listen_port" \ + jq \ --arg server "$server" \ --argjson port "$port" \ --arg uuid "$uuid" \ @@ -1033,88 +738,483 @@ sing_box_config_vless() { --arg host "$(get_param "host")" \ --arg spx "$(get_param "spx")" \ --arg insecure "$(get_param "allowInsecure")" \ - '{ - "log": { - "level": "warn" - }, - "inbounds": [ - { - "type": "tproxy", - "listen": "::", - "listen_port": ($listen_port|tonumber), - "sniff": false + --arg section "$section" \ + '. | + # Updating an existing outbound by tag or adding a new one + .outbounds |= ( + # If an element with the required tag is found, update it + map( + if .tag == $section then + . + { + "type": "vless", + "server": $server, + "server_port": ($port | tonumber), + "uuid": $uuid, + "packet_encoding": "", + "domain_strategy": "", + "flow": $flow } - ], - "outbounds": [ - { - "type": "vless", - "server": $server, - "server_port": ($port|tonumber), - "uuid": $uuid, - "packet_encoding": "", - "domain_strategy": "" - } - ], - "route": { - "auto_detect_interface": true - } - } | - - if $flow != "" then .outbounds[0].flow = $flow else . end | - - if $type == "ws" then - .outbounds[0].transport = { - "type": "ws", - "path": $path - } | - if $host != "" then - .outbounds[0].transport.headers = { - "Host": $host - } - else . end - elif $type == "grpc" then - .outbounds[0].transport = { - "type": "grpc" - } - else . end | - - if $security == "reality" or $security == "tls" then - .outbounds[0].tls = { - "enabled": true, - "server_name": $sni, - "utls": { + else . end + ) + + # Add a new outbound if the required tag is not present + ( + if (map(select(.tag == $section)) | length) == 0 then + [{ + "tag": $section, + "type": "vless", + "server": $server, + "server_port": ($port | tonumber), + "uuid": $uuid, + "packet_encoding": "", + "domain_strategy": "", + "flow": $flow + }] + else [] end + ) + ) | + # Additional parameters such as transport and tls + if $flow != "" then + .outbounds |= map( + if .tag == $section then + .flow = $flow + else . end + ) + else . end | + if $type == "ws" then + .outbounds |= map( + if .tag == $section then + .transport = { + "type": "ws", + "path": $path + } | + if $host != "" then + .transport.headers = { "Host": $host } + else . end + else . end + ) + elif $type == "grpc" then + .outbounds |= map( + if .tag == $section then + .transport = { "type": "grpc" } + else . end + ) + else . end | + if $security == "reality" or $security == "tls" then + .outbounds |= map( + if .tag == $section then + .tls = { + "enabled": true, + "server_name": $sni, + "utls": { "enabled": true, "fingerprint": $fp - }, - "insecure": ($insecure == "1") - } | - if $alpn != "" then - .outbounds[0].tls.alpn = ($alpn | split(",")) - else . end | - if $security == "reality" then - .outbounds[0].tls.reality = { + }, + "insecure": ($insecure == "1") + } | + if $alpn != "" then + .tls.alpn = ($alpn | split(",")) + else . end | + if $security == "reality" then + .tls.reality = { "enabled": true, "public_key": $pbk, "short_id": $sid - } - else . end - else . end' > $SING_BOX_CONFIG + } + else . end + else . end + ) + else . end' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + if [ $? -eq 0 ]; then - echo "Config created successfully" + log "Config created successfully" else - echo "Error: Invalid JSON config generated" + log "Error: Invalid JSON config generated" return 1 fi } -sing_box_config_check() { - if ! sing-box -c $SING_BOX_CONFIG check >/dev/null 2>&1; then - log "Sing-box configuration is invalid" - return +# Process. Sing-box rules + +sing_box_ruleset_domains() { + log "Configure ruleset domains in sing-box" + + local domain=$1 + local tag=$2 + + # Check if there is a route.rule_set for the specified tag + local tag_exists=$(jq -r --arg tag "$tag" ' + .route.rule_set[]? | select(.tag == $tag) | .tag + ' /etc/sing-box/config.json) + + # If the tag exists, add the domain + if [[ -n "$tag_exists" ]]; then + jq \ + --arg tag "$tag" \ + --arg domain "$domain" \ + ' + .route.rule_set[] |= + if .tag == $tag then + .rules[0].domain_suffix += [$domain] + else + . + end + ' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json + + log "$domain added to the list for tag $tag" + else + # If tag does not exist, add a new set of rules + jq \ + --arg tag "$tag" \ + --arg domain "$domain" \ + ' + .route.rule_set += [ + { + "tag": $tag, + "type": "inline", + "rules": [ + { + "domain_suffix": [$domain] + } + ] + } + ]' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json + + log "$domain added as a new rule set for tag $tag" fi } +sing_box_ruleset_subnets() { + log "Configure ruleset domains in sing-box" + + local subnet=$1 + local tag=$2 + + # nft + nft add element inet PodkopTable podkop_subnets { $subnet } + + # Check if there is a route.rule_set for the specified tag + local tag_exists=$(jq -r --arg tag "$tag" ' + .route.rule_set[]? | select(.tag == $tag) | .tag + ' /etc/sing-box/config.json) + + # If tag exists, add the domain + if [[ -n "$tag_exists" ]]; then + jq \ + --arg tag "$tag" \ + --arg subnet "$subnet" \ + ' + .route.rule_set[] |= + if .tag == $tag then + .rules[0].ip_cidr += [$subnet] + else + . + end + ' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json + + log "$subnet added to the list for tag $tag" + else + # If tag does not exist, add a new set of rules + jq \ + --arg tag "$tag" \ + --arg subnet "$subnet" \ + ' + .route.rule_set += [ + { + "tag": $tag, + "type": "inline", + "rules": [ + { + "ip_cidr": [$subnet] + } + ] + } + ]' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json + + log "$subnet added as a new rule set for tag $tag" + fi +} + +process_domains_for_section() { + local section="$1" + + config_get custom_domains_list_type "$section" "custom_domains_list_type" "disabled" + + if [ "$custom_domains_list_type" != "disabled" ]; then + log "Adding a custom domains list for section $section" + if [ "$custom_domains_list_type" = "dynamic" ]; then + # Handle list domains from custom_domains + config_list_foreach "$section" custom_domains "sing_box_ruleset_domains" "$section" + elif [ "$custom_domains_list_type" = "text" ]; then + # Handle domains from text + config_get custom_domains_text "$section" "custom_domains_text" + process_domains_text "$custom_domains_text" "$section" + fi + fi +} + +sing_box_ruleset_remote() { + log "Configure ruleset remote in sing-box" + + local tag=$1 + local type=$2 + local update_interval=$3 + + url="$SRS_MAIN_URL/$tag.srs" + + jq \ + --arg tag "$tag" \ + --arg type "$type" \ + --arg url "$url" \ + --arg update_interval "$update_interval" \ + '.route |= (if . == null then {rule_set: []} else . end) | + .route.rule_set += [{ + "tag": $tag, + "type": $type, + "format": "binary", + "url": $url, + "update_interval": $update_interval + }]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG +} + +list_subnets_download() { + local service="$1" + local table="PodkopTable" + + case "$service" in + "twitter") + URL=$SUBNETS_TWITTER + ;; + "meta") + URL=$SUBNETS_META + ;; + "telegram") + URL=$SUBNETS_TELERAM + ;; + "discord") + URL=$SUBNETS_DISCORD + nft add set inet $table podkop_discord_subnets { type ipv4_addr\; flags interval\; auto-merge\; } + nft add rule inet $table mangle iifname "br-lan" ip daddr @podkop_discord_subnets udp dport { 50000-65535 } meta mark set 0x105 counter + ;; + *) + return + ;; + esac + + local filename=$(basename "$URL") + wget -q -O "/tmp/podkop/$filename" "$URL" + + while IFS= read -r subnet; do + if [ "$service" = "discord" ]; then + nft add element inet $table podkop_discord_subnets { $subnet } + else + nft add element inet $table podkop_subnets { $subnet } + fi + done <"/tmp/podkop/$filename" +} + +sing_box_rules() { + log "Configure rule in sing-box" + 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") + + 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 +} + +process_remote_ruleset() { + config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0" + if [ "$domain_list_enabled" -eq 1 ]; then + 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 "list_subnets_download" "$section" "$domain_list" + fi +} + +sing_box_rule_preset() { + config_get custom_domains_list_type "$section" "custom_domains_list_type" + config_get custom_subnets_list_enabled "$section" "custom_subnets_list_enabled" + + if [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_subnets_list_enabled" != "disabled" ]; then + sing_box_rules "$section" "$section" + sing_box_dns_rule_fakeip_section "$section" "$section" + fi + + config_get domain_list_enabled "$section" "domain_list_enabled" + config_get domain_list "$section" "domain_list" + if [ "$domain_list_enabled" -eq 1 ]; then + config_list_foreach $section domain_list sing_box_rules $section + config_list_foreach $section domain_list sing_box_dns_rule_fakeip_section domain_list + fi +} + +list_custom_local_domains_create() { + local section="$2" + local local_file="$1" + local filename=$(basename "$local_file" | cut -d. -f1) + + while IFS= read -r domain; do + log "From local file: $domain" + sing_box_ruleset_domains $domain $section + done <"$local_file" +} + +process_domains_list_local() { + local section="$1" + + config_get custom_local_domains_list_enabled "$section" "custom_local_domains_list_enabled" + if [ "$custom_local_domains_list_enabled" -eq 1 ]; then + log "Adding a custom domains list from file in $section" + config_list_foreach "$section" "custom_local_domains" list_custom_local_domains_create "$section" + fi +} + +list_custom_url_domains_create() { + local section="$2" + local URL="$1" + local filename=$(basename "$URL") + + wget -q -O "/tmp/podkop/${filename}" "$URL" + + while IFS= read -r domain; do + log "From local file: $domain" + sing_box_ruleset_domains $domain $section + done <"/tmp/podkop/$filename" +} + +process_domains_list_url() { + local section="$1" + + config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled" + if [ "$custom_download_domains_list_enabled" -eq 1 ]; then + log "Adding a custom domains list from URL in $section" + config_list_foreach "$section" "custom_download_domains" list_custom_url_domains_create "$section" + fi +} + +process_subnet_for_section() { + local section="$1" + + config_get custom_subnets_list_enabled "$section" "custom_subnets_list_enabled" "disabled" + if [ "$custom_subnets_list_enabled" != "disabled" ]; then + log "Adding a custom subnet list for section $section" + if [ "$custom_subnets_list_enabled" = "dynamic" ]; then + # Handle list domains from custom_domains + config_list_foreach "$section" custom_subnets "sing_box_ruleset_subnets" "$section" + elif [ "$custom_subnets_list_enabled" = "text" ]; then + # Handle domains from text + config_get custom_subnets_text "$section" "custom_subnets_text" + process_subnets_text "$custom_subnets_text" "$section" + fi + fi +} + +list_custom_url_subnets_create() { + local section="$2" + local URL="$1" + local filename=$(basename "$URL") + + wget -q -O "/tmp/podkop/${filename}" "$URL" + + while IFS= read -r subnet; do + log "From local file: $subnet" + sing_box_ruleset_subnets $subnet $section + done <"/tmp/podkop/$filename" +} + +process_subnet_for_section_remote() { + local section="$1" + + config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled" "disabled" + if [ "$custom_download_subnets_list_enabled" -eq "1" ]; then + log "Adding a custom SUBNET list from URL in $section" + config_list_foreach "$section" "custom_download_subnets" list_custom_url_subnets_create "$section" + fi +} + +process_all_traffic_for_section() { + local section="$1" + + config_get all_traffic_from_ip_enabled "$section" "all_traffic_from_ip_enabled" + if [ "$all_traffic_from_ip_enabled" -eq "1" ]; then + log "Adding an IP to redirect all traffic" + config_list_foreach $section all_traffic_ip list_all_traffic_from_ip + config_list_foreach $section all_traffic_ip sing_box_rules_source_ip_cidr "$section" "$all_traffic_ip" + fi +} + +sing_box_rules_source_ip_cidr() { + log "Configure source_ip_cidr rule in sing-box" + local outbound="$2" + local source_ip_cidr="$1" + + local current_source_ip_cidr=$(jq -r ".route.rules[] | select(.outbound == \"$outbound\" and .source_ip_cidr) | .rule_set" $SING_BOX_CONFIG) + + if [[ -n "$current_source_ip_cidr" ]]; then + jq \ + --arg source_ip_cidr "$source_ip_cidr" \ + --arg outbound "$outbound" \ + '(.route.rules[] | select(.outbound == $outbound) | .source_ip_cidr) += [$source_ip_cidr]' \ + $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + else + jq \ + --arg source_ip_cidr "$source_ip_cidr" \ + --arg outbound "$outbound" \ + '.route.rules = [ + { + "inbound": ["tproxy-in"], + "source_ip_cidr": [$source_ip_cidr], + "outbound": $outbound + } + ] + .route.rules' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + fi +} + +## nftables +list_all_traffic_from_ip() { + local ip="$1" + if ! nft list chain inet PodkopTable mangle | grep -q "ip saddr $ip"; then + nft add set inet PodkopTable localv4 { type ipv4_addr\; flags interval\; } + nft add element inet PodkopTable localv4 { \ + 0.0.0.0/8, \ + 10.0.0.0/8, \ + 127.0.0.0/8, \ + 169.254.0.0/16, \ + 172.16.0.0/12, \ + 192.0.0.0/24, \ + 192.0.2.0/24, \ + 192.88.99.0/24, \ + 192.168.0.0/16, \ + 198.51.100.0/24, \ + 203.0.113.0/24, \ + 224.0.0.0/4, \ + 240.0.0.0-255.255.255.255 } + nft insert rule inet PodkopTable mangle iifname "br-lan" ip saddr $ip meta l4proto { tcp, udp } meta mark set 0x105 counter + nft insert rule inet PodkopTable mangle ip saddr $ip ip daddr @localv4 return + fi +} + +# Diagnotics check_proxy() { if ! command -v sing-box >/dev/null 2>&1; then nolog "sing-box is not installed" @@ -1169,7 +1269,7 @@ check_nft() { nolog "Checking PodkopTable rules..." # Список всех возможных сетов - local sets="podkop_domains podkop_subnets podkop2_domains podkop2_subnets localv4" + local sets="podkop_domains podkop_subnets podkop_subnets_discord localv4" nolog "Sets statistics:" for set_name in $sets; do @@ -1243,50 +1343,3 @@ check_all() { check_three } - -process_domains_text() { - local text="$1" - local name="$2" - - local tmp_file=$(mktemp) - echo "$text" > "$tmp_file" - - sed 's/[, ]\+/\n/g' "$tmp_file" | while IFS= read -r domain; do - domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - if [ -n "$domain" ]; then - list_custom_domains_create "$domain" "$name" - fi - done - - rm -f "$tmp_file" -} - -process_subnets_text() { - local text="$1" - local name="$2" - - local tmp_file=$(mktemp) - echo "$text" > "$tmp_file" - - sed 's/[, ]\+/\n/g' "$tmp_file" | while IFS= read -r subnet; do - subnet=$(echo "$subnet" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - if [ -n "$subnet" ]; then - if ! echo "$subnet" | grep -q "/"; then - subnet="$subnet/32" - fi - list_custom_subnets_create "$subnet" "$name" - fi - done - - rm -f "$tmp_file" -} - -list_custom_subnets_preprocess() { - local subnet="$1" - local name="$2" - - if ! echo "$subnet" | grep -q "/"; then - subnet="$subnet/32" - fi - list_custom_subnets_create "$subnet" "$name" -} \ No newline at end of file