diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..723ef36
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea
\ No newline at end of file
diff --git a/.shellcheckrc b/.shellcheckrc
deleted file mode 100644
index ce29cbe..0000000
--- a/.shellcheckrc
+++ /dev/null
@@ -1 +0,0 @@
-disable=SC3036,SC3010,SC3014,SC3015,SC3020,SC3003
\ No newline at end of file
diff --git a/README.md b/README.md
index bf36f08..7046be9 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/mai
## Рефактор
- [ ] Очевидные повторения в `/usr/bin/podkop` загнать в переменые
-- [ ] Возможно поменять структуру
+- [x] Возможно поменять структуру
## Списки
- [ ] CloudFront
@@ -41,9 +41,9 @@ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/mai
- [ ] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables.
- [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме.
- [x] Поддержка Source format. Нужна расшифровка в json и если присуствуют подсети, заносить их в custom subnet nftset.
-- [ ] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
+- [x] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
-- [ ] Формирование конфига sing-box в /tmp
+- [x] Формирование конфига sing-box в /tmp
- [ ] Галочка, которая режет доступ к doh серверам.
- [ ] IPv6. Только после наполнения Wiki.
diff --git a/String-example.md b/String-example.md
index bd0d803..a07395f 100644
--- a/String-example.md
+++ b/String-example.md
@@ -20,11 +20,11 @@ ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Y21lZklCdDhwMTJaZm1QWUplMnNCNThRd3R3NXNKeVp
## Reality
```
-vless://eb445f4b-ddb4-4c79-86d5-0833fc674379@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=yahoo.com&sid=6cabf01472a3&spx=%2F&flow=xtls-rprx-vision#vless-reality
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=sni.server.com&sid=6cabf01472a3&spx=%2F&flow=xtls-rprx-vision#vless-reality
```
```
-vless://UUID@IP:2082?security=reality&sni=dash.cloudflare.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=pukkey&sid=id&type=grpc&encryption=none#vless-reality-strange
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@123.123.123.123:2082?security=reality&sni=sni.server.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&sid=6cabf01472a3&type=grpc&encryption=none#vless-reality-strange
```
## TLS
@@ -35,34 +35,34 @@ vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=t
2.
```
-vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?security=tls&sni=SITE&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
```
3.
```
-vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws
```
4.
```
-vless://[someid]@[someserver]?security=tls&sni=[somesni]&type=ws&path=/?ed%3D2560&host=[somesni]&encryption=none#vless-tls-ws-2
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&type=ws&path=/?ed%3D2560&host=sni.server.com&encryption=none#vless-tls-ws-2
```
5.
```
-vless://uuid@server:443?security=tls&sni=server&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?security=tls&sni=sni.server.com&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3
```
6.
```
-vless://33333@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=example.com&fp=chrome#vless-tls-ws-4
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws-4
```
7.
```
-vless://id@sub.domain.example:443?type=ws&path=%2Fdir%2Fpath&host=sub.domain.example&security=tls#configname
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@sub.example.com:443?type=ws&path=%2Fdir%2Fpath&host=sub.example.com&security=tls#configname
```
## No security
```
-vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?type=tcp&security=none#vless-tls-no-encrypt
+vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=none#vless-tls-no-encrypt
```
\ No newline at end of file
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js
index 9b96b0d..a95c4ae 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js
@@ -50,20 +50,10 @@ function createAdditionalSection(mainSection, network) {
return _('DNS server address cannot be empty');
}
- const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
- if (ipRegex.test(value)) {
- const parts = value.split('.');
- for (const part of parts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) {
- return _('IP address parts must be between 0 and 255');
- }
- }
- return true;
- }
+ const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
+ const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
- const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
- if (!domainRegex.test(value)) {
+ if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
@@ -97,20 +87,10 @@ function createAdditionalSection(mainSection, network) {
return _('DNS server address cannot be empty');
}
- const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
- if (ipRegex.test(value)) {
- const parts = value.split('.');
- for (const part of parts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) {
- return _('IP address parts must be between 0 and 255');
- }
- }
- return true;
- }
+ const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
+ const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
- const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
- if (!domainRegex.test(value)) {
+ if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
@@ -134,10 +114,17 @@ function createAdditionalSection(mainSection, network) {
return true;
};
- o = mainSection.taboption('additional', form.Value, 'cache_file', _('Cache File Path'), _('Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing'));
- o.value('/tmp/cache.db', 'RAM (/tmp/cache.db)');
+ o = mainSection.taboption('additional', form.ListValue, 'config_path', _('Config File Path'), _('Select path for sing-box config file. Change this ONLY if you know what you are doing'));
+ o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)');
+ o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)');
+ o.default = '/etc/sing-box/config.json';
+ o.rmempty = false;
+ o.ucisection = 'main';
+
+ o = mainSection.taboption('additional', form.Value, 'cache_path', _('Cache File Path'), _('Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing'));
+ o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)');
o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)');
- o.default = '/tmp/cache.db';
+ o.default = '/tmp/sing-box/cache.db';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
@@ -220,6 +207,7 @@ function createAdditionalSection(mainSection, network) {
o.rmempty = false;
o.ucisection = 'main';
+ // TODO(ampetelin): Can be moved to advanced settings in luci
// Extra IPs and exclusions (main section)
o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route'));
o.default = '0';
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
index 8cbc2b6..6d5894d 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
@@ -234,18 +234,17 @@ function createConfigSection(section, map, network) {
return true;
};
- o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists'));
+ o = s.taboption('basic', form.Flag, 'community_lists_enabled', _('Community Lists'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
- o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains');
+ o = s.taboption('basic', form.DynamicList, 'community_lists', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains');
o.placeholder = 'Service list';
Object.entries(constants.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
-
- o.depends('domain_list_enabled', '1');
+ o.depends('community_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
@@ -302,7 +301,7 @@ function createConfigSection(section, map, network) {
}
};
- o = s.taboption('basic', form.ListValue, 'custom_domains_list_type', _('User Domain List Type'), _('Select how to add your custom domains'));
+ o = s.taboption('basic', form.ListValue, 'user_domain_list_type', _('User Domain List Type'), _('Select how to add your custom domains'));
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
o.value('text', _('Text List'));
@@ -310,9 +309,9 @@ function createConfigSection(section, map, network) {
o.rmempty = false;
o.ucisection = s.section;
- o = s.taboption('basic', form.DynamicList, 'custom_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)'));
+ o = s.taboption('basic', form.DynamicList, 'user_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)'));
o.placeholder = 'Domains list';
- o.depends('custom_domains_list_type', 'dynamic');
+ o.depends('user_domain_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -324,9 +323,9 @@ function createConfigSection(section, map, network) {
return true;
};
- o = s.taboption('basic', form.TextValue, 'custom_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline. You can add comments after //'));
+ o = s.taboption('basic', form.TextValue, 'user_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline. You can add comments after //'));
o.placeholder = 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
- o.depends('custom_domains_list_type', 'text');
+ o.depends('user_domain_list_type', 'text');
o.rows = 8;
o.rmempty = false;
o.ucisection = s.section;
@@ -365,14 +364,14 @@ function createConfigSection(section, map, network) {
return true;
};
- o = s.taboption('basic', form.Flag, 'custom_local_domains_list_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem'));
+ o = s.taboption('basic', form.Flag, 'local_domain_lists_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
- o = s.taboption('basic', form.DynamicList, 'custom_local_domains', _('Local Domain Lists Path'), _('Enter the list file path'));
+ o = s.taboption('basic', form.DynamicList, 'local_domain_lists', _('Local Domain List Paths'), _('Enter the list file path'));
o.placeholder = '/path/file.lst';
- o.depends('custom_local_domains_list_enabled', '1');
+ o.depends('local_domain_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -384,14 +383,14 @@ function createConfigSection(section, map, network) {
return true;
};
- o = s.taboption('basic', form.Flag, 'custom_download_domains_list_enabled', _('Remote Domain Lists'), _('Download and use domain lists from remote URLs'));
+ o = s.taboption('basic', form.Flag, 'remote_domain_lists_enabled', _('Remote Domain Lists'), _('Download and use domain lists from remote URLs'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
- o = s.taboption('basic', form.DynamicList, 'custom_download_domains', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://'));
+ o = s.taboption('basic', form.DynamicList, 'remote_domain_lists', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://'));
o.placeholder = 'URL';
- o.depends('custom_download_domains_list_enabled', '1');
+ o.depends('remote_domain_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -399,7 +398,26 @@ function createConfigSection(section, map, network) {
return validateUrl(value);
};
- o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets'));
+ o = s.taboption('basic', form.Flag, 'local_subnet_lists_enabled', _('Local Subnet Lists'), _('Use the list from the router filesystem'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = s.section;
+
+ o = s.taboption('basic', form.DynamicList, 'local_subnet_lists', _('Local Subnet List Paths'), _('Enter the list file path'));
+ o.placeholder = '/path/file.lst';
+ o.depends('local_subnet_lists_enabled', '1');
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/;
+ if (!pathRegex.test(value)) {
+ return _('Invalid path format. Path must start with "/" and contain valid characters');
+ }
+ return true;
+ };
+
+ o = s.taboption('basic', form.ListValue, 'user_subnet_list_type', _('User Subnet List Type'), _('Select how to add your custom subnets'));
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
o.value('text', _('Text List (comma/space/newline separated)'));
@@ -407,9 +425,9 @@ function createConfigSection(section, map, network) {
o.rmempty = false;
o.ucisection = s.section;
- o = s.taboption('basic', form.DynamicList, 'custom_subnets', _('User Subnets'), _('Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses'));
+ o = s.taboption('basic', form.DynamicList, 'user_subnets', _('User Subnets'), _('Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses'));
o.placeholder = 'IP or subnet';
- o.depends('custom_subnets_list_enabled', 'dynamic');
+ o.depends('user_subnet_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
@@ -432,9 +450,9 @@ function createConfigSection(section, map, network) {
return true;
};
- o = s.taboption('basic', form.TextValue, 'custom_subnets_text', _('User Subnets List'), _('Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //'));
+ o = s.taboption('basic', form.TextValue, 'user_subnets_text', _('User Subnets List'), _('Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //'));
o.placeholder = '103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9';
- o.depends('custom_subnets_list_enabled', 'text');
+ o.depends('user_subnet_list_type', 'text');
o.rows = 10;
o.rmempty = false;
o.ucisection = s.section;
@@ -489,14 +507,14 @@ function createConfigSection(section, map, network) {
return true;
};
- o = s.taboption('basic', form.Flag, 'custom_download_subnets_list_enabled', _('Remote Subnet Lists'), _('Download and use subnet lists from remote URLs'));
+ o = s.taboption('basic', form.Flag, 'remote_subnet_lists_enabled', _('Remote Subnet Lists'), _('Download and use subnet lists from remote URLs'));
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
- o = s.taboption('basic', form.DynamicList, 'custom_download_subnets', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://'));
+ o = s.taboption('basic', form.DynamicList, 'remote_subnet_lists', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://'));
o.placeholder = 'URL';
- o.depends('custom_download_subnets_list_enabled', '1');
+ o.depends('remote_subnet_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po
index 88c144e..04ab973 100644
--- a/luci-app-podkop/po/ru/podkop.po
+++ b/luci-app-podkop/po/ru/podkop.po
@@ -97,8 +97,8 @@ msgstr "Локальные списки доменов"
msgid "Use the list from the router filesystem"
msgstr "Использовать список из файловой системы роутера"
-msgid "Local Domain Lists Path"
-msgstr "Путь к локальным спискам доменов"
+msgid "Local Domain List Paths"
+msgstr "Пути к локальным спискам доменов"
msgid "Enter to the list file path"
msgstr "Введите путь к файлу списка"
@@ -896,4 +896,13 @@ msgid "Delay in milliseconds before reloading podkop after interface UP"
msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса"
msgid "Delay value cannot be empty"
-msgstr "Значение не может быть пустым"
\ No newline at end of file
+msgstr "Значение не может быть пустым"
+
+msgid "Local Subnet Lists"
+msgstr "Локальные списки подсетей"
+
+msgid "Local Subnet List Paths"
+msgstr "Пути к локальным спискам подсетей"
+
+msgid "Config File Path"
+msgstr "Путь к файлу конфигурации"
\ No newline at end of file
diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot
index c48e153..16eed7a 100644
--- a/luci-app-podkop/po/templates/podkop.pot
+++ b/luci-app-podkop/po/templates/podkop.pot
@@ -97,7 +97,7 @@ msgstr ""
msgid "Use the list from the router filesystem"
msgstr ""
-msgid "Local Domain Lists Path"
+msgid "Local Domain List Paths"
msgstr ""
msgid "Enter to the list file path"
@@ -1250,4 +1250,13 @@ msgid "Delay in milliseconds before reloading podkop after interface UP"
msgstr ""
msgid "Delay value cannot be empty"
+msgstr ""
+
+msgid "Local Subnet Lists"
+msgstr ""
+
+msgid "Local Subnet List Paths"
+msgstr ""
+
+msgid "Config File Path"
msgstr ""
\ No newline at end of file
diff --git a/podkop/Makefile b/podkop/Makefile
index e86de4d..d5c7d66 100644
--- a/podkop/Makefile
+++ b/podkop/Makefile
@@ -55,6 +55,9 @@ define Package/podkop/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) ./files/usr/bin/podkop $(1)/usr/bin/podkop
+
+ $(INSTALL_DIR) $(1)/usr/lib/podkop
+ $(CP) ./files/usr/lib/* $(1)/usr/lib/podkop/
endef
$(eval $(call BuildPackage,podkop))
diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop
index 2961f41..6e2269e 100644
--- a/podkop/files/etc/config/podkop
+++ b/podkop/files/etc/config/podkop
@@ -4,21 +4,22 @@ config main 'main'
option proxy_config_type 'url'
#option outbound_json ''
option proxy_string ''
- option domain_list_enabled '1'
- list domain_list 'russia_inside'
- option subnets_list_enabled '0'
- option custom_domains_list_type 'disabled'
- #list custom_domains ''
- #option custom_domains_text ''
- option custom_local_domains_list_enabled '0'
- #list custom_local_domains ''
- option custom_download_domains_list_enabled '0'
- #list custom_download_domains ''
- option custom_domains_list_type 'disable'
- #list custom_subnets ''
- #custom_subnets_text ''
- option custom_download_subnets_list_enabled '0'
- #list custom_download_subnets ''
+ option community_lists_enabled '1'
+ list community_lists 'russia_inside'
+ option user_domain_list_type 'disabled'
+ #list user_domains ''
+ #option user_domains_text ''
+ option local_domain_lists_enabled '0'
+ #list local_domain_lists ''
+ option remote_domain_lists_enabled '0'
+ #list remote_domain_lists ''
+ option user_subnet_list_type 'disable'
+ #list user_subnets ''
+ #option user_subnets_text ''
+ option local_subnet_lists_enabled '0'
+ #list local_subnet_lists ''
+ option remote_subnet_lists_enabled '0'
+ #list remote_subnet_lists ''
option all_traffic_from_ip_enabled '0'
#list all_traffic_ip ''
option exclude_from_ip_enabled '0'
@@ -35,10 +36,12 @@ config main 'main'
option split_dns_type 'udp'
option split_dns_server '1.1.1.1'
option dns_rewrite_ttl '60'
- option cache_file '/tmp/cache.db'
+ option config_path '/etc/sing-box/config.json'
+ option cache_path '/tmp/sing-box/cache.db'
list iface 'br-lan'
option mon_restart_ifaces '0'
#list restart_ifaces 'wan'
option procd_reload_delay '2000'
option ss_uot '0'
- option detour '0'
\ No newline at end of file
+ option detour '0'
+ option shutdown_correctly '1'
\ No newline at end of file
diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop
index 79bd5d2..32592a4 100755
--- a/podkop/files/usr/bin/podkop
+++ b/podkop/files/usr/bin/podkop
@@ -1,81 +1,48 @@
#!/bin/ash
-# shellcheck shell=dash
-[ -r /lib/functions.sh ] && . /lib/functions.sh
-[ -r /lib/config/uci.sh ] && . /lib/config/uci.sh
+check_required_file() {
+ local file="$1"
-config_load "/etc/config/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_TELERAM="${GITHUB_RAW_URL}/Subnets/IPv4/telegram.lst"
-SUBNETS_CLOUDFLARE="${GITHUB_RAW_URL}/Subnets/IPv4/cloudflare.lst"
-SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst"
-SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst"
-SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst"
-SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst"
-SING_BOX_CONFIG="/etc/sing-box/config.json"
-FAKEIP="198.18.0.0/15"
-VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube discord meta twitter hdrezka tiktok telegram cloudflare google_ai google_play hetzner ovh hodca digitalocean cloudfront"
-DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8"
-TEST_DOMAIN="fakeip.podkop.fyi"
-INTERFACES_LIST=""
-SRC_INTERFACE=""
-RESOLV_CONF="/etc/resolv.conf"
-
-# Endpoints https://github.com/ampetelin/warp-endpoint-checker
-CLOUDFLARE_OCTETS="8.47 162.159 188.114"
-
-# Color constants
-COLOR_CYAN="\033[0;36m"
-COLOR_GREEN="\033[0;32m"
-COLOR_RESET="\033[0m"
-
-log() {
- local message="$1"
- local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
-
- logger -t "podkop" "$timestamp $message"
+ if [ ! -r "$file" ]; then
+ echo "Error: required file '$file' is missing or not readable" >&2
+ exit 1
+ fi
}
-nolog() {
- local message="$1"
- local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
+PODKOP_LIB="/usr/lib/podkop"
+check_required_file /lib/functions.sh
+check_required_file /lib/config/uci.sh
+check_required_file "$PODKOP_LIB/constants.sh"
+check_required_file "$PODKOP_LIB/nft.sh"
+check_required_file "$PODKOP_LIB/helpers.sh"
+check_required_file "$PODKOP_LIB/sing_box_config_manager.sh"
+check_required_file "$PODKOP_LIB/sing_box_config_facade.sh"
+check_required_file "$PODKOP_LIB/logging.sh"
+. /lib/config/uci.sh
+. /lib/functions.sh
+. "$PODKOP_LIB/constants.sh"
+. "$PODKOP_LIB/nft.sh"
+. "$PODKOP_LIB/helpers.sh"
+. "$PODKOP_LIB/sing_box_config_manager.sh"
+. "$PODKOP_LIB/sing_box_config_facade.sh"
+. "$PODKOP_LIB/logging.sh"
- echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
-}
-
-echolog() {
- local message="$1"
- log "$message"
- nolog "$message"
-}
-
-build_sing_box_config() {
- cat > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
-}
+config_load "$PODKOP_CONFIG"
start_main() {
log "Starting podkop"
# checking
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
- required_version="1.11.1"
+ required_version="1.12.0"
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
- log "[critical] The version of sing-box ($sing_box_version) is lower than the minimum version. Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box"
+ log "The version of sing-box ($sing_box_version) is lower than the minimum version. Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box" "critical"
exit 1
fi
if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
- log "[critical] Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp"
+ log "Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp" "warn"
fi
migration
@@ -89,7 +56,8 @@ start_main() {
sleep 1
- mkdir -p /tmp/podkop
+ mkdir -p "$TMP_SING_BOX_FOLDER"
+ mkdir -p "$TMP_RULESET_FOLDER"
# base
route_table_rule_mark
@@ -97,83 +65,41 @@ start_main() {
sing_box_uci
# sing-box
- sing_box_inbound_proxy 1602
- sing_box_dns
- sing_box_dns_rule_fakeip
- sing_box_rule_dns
- sing_box_create_bypass_ruleset
- sing_box_add_secure_dns_probe_domain
- sing_box_cache_file
- process_socks5
-
- # sing-box outbounds and rules
- config_foreach sing_box_outdound
- config_foreach process_domains_for_section
- config_foreach sing_box_rule_preset
- config_foreach process_domains_list_local
- config_foreach process_subnet_for_section
- config_foreach configure_community_lists
- config_foreach configure_remote_domain_lists
- config_foreach configure_remote_subnet_lists
- config_foreach process_all_traffic_for_section
+ sing_box_init_config
+ sing_box_config_check
config_foreach add_cron_job
+ /etc/init.d/sing-box start
- config_foreach prepare_custom_ruleset
- list_update &
- echo $! > /var/run/podkop_list_update.pid
-
- # 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 sing_box_rules_source_ip_cidr $exclude_traffic_ip direct-out
- fi
-
- config_get_bool yacd "main" "yacd" "0"
- if [ "$yacd" -eq 1 ]; then
- log "Yacd enable"
- jq '.experimental.clash_api = {
- "external_ui": "ui",
- "external_controller": "0.0.0.0:9090"
- }' "$SING_BOX_CONFIG" | build_sing_box_config
- fi
-
+ local exclude_ntp
config_get_bool exclude_ntp "main" "exclude_ntp" "0"
if [ "$exclude_ntp" -eq 1 ]; then
log "NTP traffic exclude for proxy"
- nft insert rule inet PodkopTable mangle udp dport 123 return
+ nft insert rule inet "$NFT_TABLE_NAME" mangle udp dport 123 return
fi
- config_get_bool quic_disable "main" "quic_disable" "0"
- if [ "$quic_disable" -eq 1 ]; then
- log "Rule for disable QUIC"
- sing_box_quic_reject
- fi
-
- config_get_bool detour "main" "detour" "0"
- if [ "$detour" -eq 1 ]; then
- log "Detour mixed enable"
- detour_mixed
- fi
-
- sing_box_config_check
- /etc/init.d/sing-box start
log "Nice"
+ list_update &
+ echo $! > /var/run/podkop_list_update.pid
}
start() {
- start_main
-
+ local proxy_string interface outbound_json dont_touch_dhcp
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
- if [ -n "$proxy_string" ] || [ -n "$interface" ] || [ -n "$outbound_json" ]; then
- config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0"
- if [ "$dont_touch_dhcp" -eq 0 ]; then
- dnsmasq_add_resolver
- fi
+ if [ -z "$proxy_string" ] && [ -z "$interface" ] && [ -z "$outbound_json" ]; then
+ log "Podkop start aborted: required options (proxy_string, interface, outbound_json) are missing in 'main' section"
+ exit 1
fi
+
+ start_main
+ config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" 0
+ if [ "$dont_touch_dhcp" -eq 0 ]; then
+ dnsmasq_add_resolver
+ fi
+ uci_set "podkop" "main" "shutdown_correctly" 0
+ uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
stop_main() {
@@ -181,8 +107,8 @@ stop_main() {
if [ -f /var/run/podkop_list_update.pid ]; then
pid=$(cat /var/run/podkop_list_update.pid)
- if kill -0 "$pid" 2>/dev/null; then
- kill "$pid" 2>/dev/null
+ if kill -0 "$pid" 2> /dev/null; then
+ kill "$pid" 2> /dev/null
log "Stopped list_update"
fi
rm -f /var/run/podkop_list_update.pid
@@ -190,11 +116,11 @@ stop_main() {
remove_cron_job
- rm -rf /tmp/podkop/*.lst
+ rm -f "$TMP_RULESET_FOLDER"/*
log "Flush nft"
- if nft list table inet PodkopTable >/dev/null 2>&1; then
- nft delete table inet PodkopTable
+ if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
+ nft delete table inet "$NFT_TABLE_NAME"
fi
log "Flush ip rule"
@@ -203,22 +129,23 @@ stop_main() {
fi
log "Flush ip route"
- if ip route list table podkop >/dev/null 2>&1; then
+ if ip route list table podkop > /dev/null 2>&1; then
ip route flush table podkop
fi
log "Stop sing-box"
/etc/init.d/sing-box stop
- #/etc/init.d/sing-box disable
}
stop() {
- config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0"
+ local dont_touch_dhcp
+ config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_restore
fi
-
stop_main
+ uci_set "podkop" "main" "shutdown_correctly" 1
+ uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
reload() {
@@ -239,35 +166,35 @@ migration() {
local CONFIG="/etc/config/podkop"
if grep -q "ru_inside" $CONFIG; then
- log "Depricated list found: ru_inside"
+ log "Deprecated list found: ru_inside"
sed -i '/ru_inside/d' $CONFIG
fi
if grep -q "list domain_list 'ru_outside'" $CONFIG; then
- log "Depricated list found: sru_outside"
+ log "Deprecated list found: sru_outside"
sed -i '/ru_outside/d' $CONFIG
fi
if grep -q "list domain_list 'ua'" $CONFIG; then
- log "Depricated list found: ua"
+ log "Deprecated list found: ua"
sed -i '/ua/d' $CONFIG
fi
# Subnet list
if grep -q "list subnets" $CONFIG; then
- log "Depricated second section found"
+ log "Deprecated second section found"
sed -i '/list subnets/d' $CONFIG
fi
# second remove
if grep -q "config second 'second'" $CONFIG; then
- log "Depricated second section found"
+ log "Deprecated second section found"
sed -i '/second/d' $CONFIG
fi
# cron update
if grep -qE "^\s*option update_interval '[0-9*/,-]+( [0-9*/,-]+){4}'" $CONFIG; then
- log "Depricated update_interval"
+ log "Deprecated update_interval"
sed -i "s|^\(\s*option update_interval\) '[0-9*/,-]\+\( [0-9*/,-]\+\)\{4\}'|\1 '1d'|" $CONFIG
fi
@@ -285,6 +212,29 @@ migration() {
# corntab init.d
(crontab -l | grep -v "/etc/init.d/podkop list_update") | crontab -
+
+ migration_rename_config_key "$CONFIG" "option" "domain_list_enabled" "community_lists_enabled"
+ migration_rename_config_key "$CONFIG" "list" "domain_list" "community_lists"
+
+ migration_rename_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domain_list_type"
+ migration_rename_config_key "$CONFIG" "option" "custom_domains_text" "user_domains_text"
+ migration_rename_config_key "$CONFIG" "list" "custom_domains" "user_domains"
+
+ migration_rename_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnet_list_type"
+ migration_rename_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets_text"
+ migration_rename_config_key "$CONFIG" "list" "custom_subnets" "user_subnets"
+
+ migration_rename_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domain_lists_enabled"
+ migration_rename_config_key "$CONFIG" "list" "custom_local_domains" "local_domain_lists"
+
+ migration_rename_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domain_lists_enabled"
+ migration_rename_config_key "$CONFIG" "list" "custom_download_domains" "remote_domain_lists"
+
+ migration_rename_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnet_lists_enabled"
+ migration_rename_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnet_lists"
+
+ migration_rename_config_key "$CONFIG" "option" "cache_file" "cache_path"
+ migration_add_new_option "podkop" "main" "config_path" "/etc/sing-box/config.json" && config_load "$PODKOP_CONFIG"
}
validate_service() {
@@ -301,18 +251,19 @@ validate_service() {
}
process_validate_service() {
- config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
+ local domain_list_enabled
+ config_get_bool domain_list_enabled "$section" "domain_list_enabled" 0
if [ "$domain_list_enabled" -eq 1 ]; then
config_list_foreach "$section" domain_list validate_service
fi
}
br_netfilter_disable() {
- if lsmod | grep -q br_netfilter && [ "$(sysctl -n net.bridge.bridge-nf-call-iptables 2>/dev/null)" = "1" ]; then
+ if lsmod | grep -q br_netfilter && [ "$(sysctl -n net.bridge.bridge-nf-call-iptables 2> /dev/null)" = "1" ]; then
log "br_netfilter enabled detected. Disabling"
sysctl -w net.bridge.bridge-nf-call-iptables=0
sysctl -w net.bridge.bridge-nf-call-ip6tables=0
- fi
+ fi
}
# Main funcs
@@ -320,7 +271,7 @@ br_netfilter_disable() {
route_table_rule_mark() {
local table=podkop
- grep -q "105 $table" /etc/iproute2/rt_tables || echo "105 $table" >>/etc/iproute2/rt_tables
+ 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"
@@ -337,232 +288,185 @@ route_table_rule_mark() {
fi
}
-process_interfaces() {
- local iface="$1"
- INTERFACES_LIST="$INTERFACES_LIST $iface"
- iface_flag=1
-}
+nft_init_interfaces_set() {
+ nft_create_ifname_set "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME"
-nft_interfaces() {
- local table=PodkopTable
- iface_flag=0
+ local interface_list
+ config_get interface_list "main" "iface" "br-lan"
- config_list_foreach "main" "iface" "process_interfaces"
- if [ "$iface_flag" -eq 0 ]; then
- SRC_INTERFACE="br-lan"
- elif [ $(echo "$INTERFACES_LIST" | wc -w) -eq 1 ]; then
- SRC_INTERFACE=$INTERFACES_LIST
- else
- local set_name="interfaces"
- if ! nft list set inet $table $set_name &>/dev/null; then
- nft add set inet $table $set_name { type ifname\; flags interval\; }
- fi
-
- for interface in $INTERFACES_LIST; do
- if ! nft list element inet $table $set_name { $interface } &>/dev/null; then
- nft add element inet $table $set_name { $interface }
- fi
- done
-
- SRC_INTERFACE=@$set_name
- fi
+ for interface in $interface_list; do
+ nft add element inet "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" "{ $interface }"
+ done
}
create_nft_table() {
- local table="PodkopTable"
+ log "Create nft table"
+ nft_create_table "$NFT_TABLE_NAME"
- nft add table inet $table
-
- nft_interfaces
+ nft_init_interfaces_set
log "Create localv4 set"
- nft add set inet $table localv4 { type ipv4_addr\; flags interval\; }
- nft add element inet $table 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_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_LOCALV4_SET_NAME"
+ nft add element inet "$NFT_TABLE_NAME" 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
+ }'
+
+ log "Create common set"
+ nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
+
+ log "Create interface set"
+ nft_init_interfaces_set
log "Create nft rules"
- nft add chain inet $table mangle { type filter hook prerouting priority -150 \; policy accept \;}
- nft add chain inet $table mangle_output { type route hook output priority -150 \; policy accept\; }
- nft add chain inet $table proxy { type filter hook prerouting priority -100 \; policy accept \;}
+ nft add chain inet "$NFT_TABLE_NAME" mangle '{ type filter hook prerouting priority -150; policy accept; }'
+ nft add chain inet "$NFT_TABLE_NAME" mangle_output '{ type route hook output priority -150; policy accept; }'
+ nft add chain inet "$NFT_TABLE_NAME" proxy '{ type filter hook prerouting priority -100; policy accept; }'
- nft add set inet $table podkop_subnets { type ipv4_addr\; flags interval\; auto-merge\; }
+ nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set 0x105 counter
- nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x105 counter
- nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_subnets meta l4proto udp meta mark set 0x105 counter
- nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$FAKEIP" meta l4proto tcp meta mark set 0x105 counter
- nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$FAKEIP" meta l4proto udp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" proxy meta mark 0x105 meta l4proto tcp tproxy ip to 127.0.0.1:1602 counter
+ nft add rule inet "$NFT_TABLE_NAME" proxy meta mark 0x105 meta l4proto udp tproxy ip to 127.0.0.1:1602 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
-
- nft add rule inet $table mangle_output ip daddr @localv4 return
- nft add rule inet $table mangle_output ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x00000105 counter
- nft add rule inet $table mangle_output ip daddr @podkop_subnets meta l4proto udp meta mark set 0x00000105 counter
- nft add rule inet $table mangle_output ip daddr 198.18.0.0/15 meta l4proto tcp meta mark set 0x00000105 counter
- nft add rule inet $table mangle_output ip daddr 198.18.0.0/15 meta l4proto udp meta mark set 0x00000105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_LOCALV4_SET_NAME" return
+ nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter
+ nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter
}
-save_dnsmasq_config() {
+backup_dnsmasq_config_option() {
local key="$1"
local backup_key="$2"
- value=$(uci get "$key" 2>/dev/null)
+ local value
+ value="$(uci_get "dhcp" "@dnsmasq[0]" "$key")"
- if [ -z "$value" ]; then
- uci set "$backup_key"="unset"
- else
- uci set "$backup_key"="$value"
+ if [ -n "$value" ]; then
+ uci_set "dhcp" "@dnsmasq[0]" "$backup_key" "$value"
fi
}
dnsmasq_add_resolver() {
- log "Save dnsmasq config"
+ local shutdown_correctly
+ config_get shutdown_correctly "main" "shutdown_correctly"
+ if [ "$shutdown_correctly" -eq 0 ]; then
+ log "Previous shutdown of podkop was not correct, reconfiguration of dnsmasq is not required"
+ return 0
+ fi
- uci -q delete dhcp.@dnsmasq[0].podkop_server
- for server in $(uci get dhcp.@dnsmasq[0].server 2>/dev/null); do
- if [[ "$server" == "127.0.0.42" ]]; then
- log "Dnsmasq save config error: server=127.0.0.42 is already configured. Skip editing DHCP"
- return
- else
- uci add_list dhcp.@dnsmasq[0].podkop_server="$server"
- fi
- done
+ log "Backup dnsmasq configuration"
+ current_servers="$(uci_get "dhcp" "@dnsmasq[0]" "server")"
+ if [ -n "$current_servers" ]; then
+ for server in $(uci_get "dhcp" "@dnsmasq[0]" "server"); do
+ if ! [ "$server" == "$SB_DNS_INBOUND_ADDRESS" ]; then
+ uci_add_list "dhcp" "@dnsmasq[0]" "podkop_server" "$server"
+ fi
+ done
+ uci_remove "dhcp" "@dnsmasq[0]" "server"
+ fi
- save_dnsmasq_config "dhcp.@dnsmasq[0].noresolv" "dhcp.@dnsmasq[0].podkop_noresolv"
- save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize"
+ backup_dnsmasq_config_option "noresolv" "podkop_noresolv"
+ backup_dnsmasq_config_option "cachesize" "podkop_cachesize"
log "Configure dnsmasq for sing-box"
- uci set dhcp.@dnsmasq[0].noresolv="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.42"
- uci commit dhcp
+ uci_add_list "dhcp" "@dnsmasq[0]" "server" "$SB_DNS_INBOUND_ADDRESS"
+ uci_set "dhcp" "@dnsmasq[0]" "noresolv" 1
+ uci_set "dhcp" "@dnsmasq[0]" "cachesize" 0
+ uci_commit "dhcp"
/etc/init.d/dnsmasq restart
}
dnsmasq_restore() {
- log "Removing configuration for dnsmasq"
-
- local cachesize=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null)
- if [[ "$cachesize" == "unset" ]]; then
- log "dnsmasq revert: cachesize is unset"
- uci -q delete dhcp.@dnsmasq[0].cachesize
- else
- uci set dhcp.@dnsmasq[0].cachesize="$cachesize"
+ log "Restoring the dnsmasq configuration"
+ local shutdown_correctly
+ config_get shutdown_correctly "main" "shutdown_correctly"
+ if [ "$shutdown_correctly" -eq 1 ]; then
+ log "Previous shutdown of podkop was correct, reconfiguration of dnsmasq is not required"
+ return 0
fi
- local noresolv=$(uci get dhcp.@dnsmasq[0].podkop_noresolv 2>/dev/null)
- if [[ "$noresolv" == "unset" ]]; then
- log "dnsmasq revert: noresolv is unset"
- uci -q delete dhcp.@dnsmasq[0].noresolv
+ local cachesize noresolv backup_servers
+ log "Restoring cachesize" "debug"
+ cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_cachesize")"
+ if [ -z "$cachesize" ]; then
+ uci_remove "dhcp" "@dnsmasq[0]" "cachesize"
else
- uci set dhcp.@dnsmasq[0].noresolv="$noresolv"
+ uci_set "dhcp" "@dnsmasq[0]" "cachesize" "$cachesize"
+ uci_remove "dhcp" "@dnsmasq[0]" "podkop_cachesize"
fi
- local server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)
- if [[ "$server" == "127.0.0.42" ]]; then
- uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null
- for server in $(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null); do
- uci add_list dhcp.@dnsmasq[0].server="$server"
+ log "Restoring noresolv" "debug"
+ noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_noresolv")"
+ if [ -z "$noresolv" ]; then
+ uci_remove "dhcp" "@dnsmasq[0]" "noresolv"
+ else
+ uci_set "dhcp" "@dnsmasq[0]" "noresolv" "$noresolv"
+ uci_remove "dhcp" "@dnsmasq[0]" "podkop_noresolv"
+ fi
+
+ log "Restoring DNS servers" "debug"
+ uci_remove "dhcp" "@dnsmasq[0]" "server"
+ backup_servers="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_server")"
+ if [ -n "$backup_servers" ]; then
+ for server in $backup_servers; do
+ uci_add_list "dhcp" "@dnsmasq[0]" "server" "$server"
done
- uci delete dhcp.@dnsmasq[0].podkop_server 2>/dev/null
+ uci_remove "dhcp" "@dnsmasq[0]" "podkop_server"
fi
- uci delete dhcp.@dnsmasq[0].podkop_cachesize
- uci delete dhcp.@dnsmasq[0].podkop_noresolv
-
- uci commit dhcp
+ uci_commit "dhcp"
/etc/init.d/dnsmasq restart
}
-process_domains_text() {
- local text="$1"
- local name="$2"
-
- local tmp_file=$(mktemp)
- echo "$text" > "$tmp_file"
-
- # First filter out full comment lines and remove comments after domains
- grep -v "^[[:space:]]*\/\/" "$tmp_file" | sed 's/\/\/.*$//' > "${tmp_file}.filtered"
-
- sed 's/[, ]\+/\n/g' "${tmp_file}.filtered" | 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" "${tmp_file}.filtered"
-}
-
-process_subnets_text() {
- local text="$1"
- local name="$2"
-
- local tmp_file=$(mktemp)
- echo "$text" > "$tmp_file"
-
- # First filter out full comment lines and remove comments after subnets
- grep -v "^[[:space:]]*\/\/" "$tmp_file" | sed 's/\/\/.*$//' > "${tmp_file}.filtered"
-
- sed 's/[, ]\+/\n/g' "${tmp_file}.filtered" | 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" "${tmp_file}.filtered"
-}
-
add_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"
+ local community_lists_enabled remote_domain_lists_enabled remote_subnet_lists_enabled update_interval
+ config_get community_lists_enabled "$section" "community_lists_enabled"
+ config_get remote_domain_lists_enabled "$section" "remote_domain_lists_enabled"
+ config_get remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled"
config_get update_interval "main" "update_interval"
case "$update_interval" in
- "1h")
- cron_job="13 * * * * /usr/bin/podkop list_update"
- ;;
- "3h")
- cron_job="13 */3 * * * /usr/bin/podkop list_update"
- ;;
- "12h")
- cron_job="13 */12 * * * /usr/bin/podkop list_update"
- ;;
- "1d")
- cron_job="13 9 * * * /usr/bin/podkop list_update"
- ;;
- "3d")
- cron_job="13 9 */3 * * /usr/bin/podkop list_update"
- ;;
- *)
- log "Invalid update_interval value: $update_interval"
- return
- ;;
+ "1h")
+ cron_job="13 * * * * /usr/bin/podkop list_update"
+ ;;
+ "3h")
+ cron_job="13 */3 * * * /usr/bin/podkop list_update"
+ ;;
+ "12h")
+ cron_job="13 */12 * * * /usr/bin/podkop list_update"
+ ;;
+ "1d")
+ cron_job="13 9 * * * /usr/bin/podkop list_update"
+ ;;
+ "3d")
+ cron_job="13 9 */3 * * /usr/bin/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
+ if [ "$community_lists_enabled" -eq 1 ] ||
+ [ "$remote_domain_lists_enabled" -eq 1 ] ||
+ [ "$remote_subnet_lists_enabled" -eq 1 ]; then
remove_cron_job
crontab -l | {
cat
@@ -577,43 +481,13 @@ remove_cron_job() {
log "The cron job removed"
}
-prepare_custom_ruleset() {
- 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 [ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ]; then
- local file="/tmp/podkop/$section-custom-domains-subnets.json"
- local tag="custom-$section"
- rm -f $file
-
- jq -n '
- {
- "version": 3,
- "rules": []
- }' > $file
-
- jq --arg tag "$tag" \
- --arg file "$file" \
- '.route.rule_set += [{
- "tag": $tag,
- "type": "local",
- "format": "source",
- "path": $file
- }]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- sing_box_rules $tag $section
- sing_box_dns_rule_fakeip_section $tag $tag
-
- log "Added $tag rule_set to sing-box config"
- fi
-}
-
list_update() {
echolog "🔄 Starting lists update..."
local i
for i in $(seq 1 60); do
- if nslookup -timeout=1 openwrt.org >/dev/null 2>&1; then
+ if nslookup -timeout=1 openwrt.org > /dev/null 2>&1; then
echolog "✅ DNS check passed"
break
fi
@@ -629,12 +503,12 @@ list_update() {
for i in $(seq 1 60); do
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
- if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com >/dev/null; then
+ if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed (via proxy)"
break
fi
else
- if curl -s -m 3 https://github.com >/dev/null; then
+ if curl -s -m 3 https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed"
break
fi
@@ -663,9 +537,8 @@ list_update() {
}
find_working_resolver() {
- local resolver_found=""
for resolver in $DNS_RESOLVERS; do
- if nslookup -timeout=2 $TEST_DOMAIN $resolver >/dev/null 2>&1; then
+ if nslookup -timeout=2 $FAKEIP_TEST_DOMAIN $resolver > /dev/null 2>&1; then
echo "$resolver"
return 0
fi
@@ -676,1287 +549,718 @@ find_working_resolver() {
# sing-box funcs
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"
+ local sing_box_enabled sing_box_user sing_box_config_path sing_box_conffile
+ sing_box_enabled=$(uci get "sing-box.main.enabled")
+ sing_box_user=$(uci get "sing-box.main.user")
+ if [ "$sing_box_enabled" -ne 1 ]; then
+ uci set "sing-box.main.enabled=1"
+ uci commit "sing-box"
+ log "sing-box service has been enabled"
+ fi
+
+ if [ "$sing_box_user" != "root" ]; then
+ uci set "sing-box.main.user=root"
+ uci commit "sing-box"
+ log "sing-box service user has been changed to root"
+ fi
+
+ config_get sing_box_config_path "main" "config_path"
+ sing_box_conffile=$(uci get "sing-box.main.conffile")
+ log "sing-box config path: $sing_box_config_path" "debug"
+ log "sing-box service conffile: $sing_box_conffile" "debug"
+ if [ "$sing_box_conffile" != "$sing_box_config_path" ]; then
+ uci set "sing-box.main.conffile=$sing_box_config_path"
+ uci commit "sing-box"
+ log "Configuration file path has been set to $sing_box_config_path"
fi
[ -f /etc/rc.d/S99sing-box ] && log "Disable sing-box" && /etc/init.d/sing-box disable
-
- # if grep -q '#\s*list ifaces' "$config"; then
- # sed -i '/ifaces/s/#//g' $config
- # log "Uncommented list ifaces"
- # fi
}
-add_socks5_for_section() {
- local section="$1"
- local port="$2"
- local tag="$section-mixed-in"
+sing_box_init_config() {
+ local config='{"log":{},"dns":{},"ntp":{},"certificate":{},"endpoints":[],"inbounds":[],"outbounds":[],"route":{},"services":[],"experimental":{}}'
- log "Adding Socks5 for $section on port $port"
-
- jq \
- --arg tag "$tag" \
- --arg port "$port" \
- --arg section "$section" \
- '.inbounds += [{
- "tag": $tag,
- "type": "mixed",
- "listen": "0.0.0.0",
- "listen_port": ($port|tonumber),
- "set_system_proxy": false
- }] |
- .route.rules += [{
- "inbound": [$tag],
- "outbound": $section,
- "action": "route"
- }]' "$SING_BOX_CONFIG" | build_sing_box_config
+ sing_box_configure_log
+ sing_box_configure_inbounds
+ sing_box_configure_outbounds
+ sing_box_configure_dns
+ sing_box_configure_route
+ sing_box_configure_experimental
+ sing_box_additional_inbounds
+ sing_box_save_config
}
-process_socks5() {
- config_get_bool main_socks5 "main" "socks5" "0"
- if [ "$main_socks5" -eq 1 ]; then
- add_socks5_for_section "main" "2080"
- fi
+sing_box_configure_log() {
+ log "Configure the log section of a sing-box JSON configuration"
- local port=2081
- for section in $(uci show podkop | awk -F'[.=]' '/=extra/ {print $2}'); do
- config_get_bool section_socks5 "$section" "socks5" "0"
- if [ "$section_socks5" -eq 1 ]; then
- add_socks5_for_section "$section" "$port"
- port=$((port + 1))
- fi
- done
+ config=$(sing_box_cm_configure_log "$config" false "$SB_DEFAULT_LOG_LEVEL" false)
}
-sing_box_inbound_proxy() {
- local listen_port="$1"
+sing_box_configure_inbounds() {
+ log "Configure the inbounds section of a sing-box JSON configuration"
- 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.42",
- "listen_port": 53
- }
- ],
- "outbounds": [
- {
- "tag": "direct-out",
- "type": "direct"
- }
- ]
- }' > $SING_BOX_CONFIG
+ config=$(
+ sing_box_cm_add_tproxy_inbound \
+ "$config" "$SB_TPROXY_INBOUND_TAG" "$SB_TPROXY_INBOUND_ADDRESS" "$SB_TPROXY_INBOUND_PORT" true true
+ )
+ config=$(
+ sing_box_cm_add_direct_inbound "$config" "$SB_DNS_INBOUND_TAG" "$SB_DNS_INBOUND_ADDRESS" "$SB_DNS_INBOUND_PORT"
+ )
}
-sing_box_dns() {
- local dns_type
- local dns_server
- local resolver_tag="resolver"
- local split_resolver_tag="split-resolver"
+sing_box_configure_outbounds() {
+ log "Configure the outbounds section of a sing-box JSON configuration"
- config_get dns_type "main" "dns_type" "doh"
- config_get dns_server "main" "dns_server" "1.1.1.1"
- config_get split_dns_enabled "main" "split_dns_enabled" "0"
- config_get split_dns_type "main" "split_dns_type" "udp"
- config_get split_dns_server "main" "split_dns_server" "1.1.1.1"
+ config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG")
- local server_json
- local is_ip=$(echo "$dns_server" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && echo "1" || echo "0")
-
- if [ "$is_ip" = "0" ]; then
- log "Finding working DNS resolver"
- local dns_resolver=$(find_working_resolver)
- if [ -z "$dns_resolver" ]; then
- log "No working resolver found, using default DNS server"
- dns_resolver="1.1.1.1"
- else
- log "Found working resolver: $dns_resolver"
- fi
- fi
-
- log "Configure DNS in sing-box"
-
- server_json=$(jq -n \
- --arg type "$dns_type" \
- --arg server "$dns_server" \
- --arg resolver "$resolver_tag" \
- --arg is_ip "$is_ip" \
- '{
- "servers": [
- {
- "tag": "dns-server",
- "address": (
- if $type == "doh" then
- "https://" + $server + "/dns-query"
- elif $type == "dot" then
- "tls://" + $server
- else
- $server
- end
- ),
- "detour": "direct-out"
- } + (
- if $is_ip == "0" then
- {"address_resolver": $resolver}
- else
- {}
- end
- )
- ]
- }')
-
- if [ "$is_ip" = "0" ]; then
- server_json=$(echo "$server_json" | jq \
- --arg resolver "$resolver_tag" \
- --arg address "$dns_resolver" \
- '.servers += [{
- "tag": $resolver,
- "address": $address
- }]')
- fi
-
- if [ "$split_dns_enabled" = "1" ]; then
- local split_is_ip=$(echo "$split_dns_server" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && echo "1" || echo "0")
- if [ "$split_is_ip" = "0" ]; then
- log "Finding working resolver for split DNS"
- local split_dns_resolver=$(find_working_resolver)
- if [ -z "$split_dns_resolver" ]; then
- log "No working resolver found for split DNS, using default"
- split_dns_resolver="1.1.1.1"
- else
- log "Found working resolver for split DNS: $split_dns_resolver"
- fi
- fi
-
- server_json=$(echo "$server_json" | jq \
- --arg type "$split_dns_type" \
- --arg server "$split_dns_server" \
- --arg split_is_ip "$split_is_ip" \
- --arg split_resolver_tag "$split_resolver_tag" \
- ' .servers += [
- {
- "tag": "split-dns-server",
- "address": (
- if $type == "doh" then
- "https://" + $server + "/dns-query"
- elif $type == "dot" then
- "tls://" + $server
- else
- $server
- end
- ),
- "detour": "main"
- } + (
- if $split_is_ip == "0" then
- {"address_resolver": $split_resolver_tag}
- else
- {}
- end
- )
- ]')
-
- if [ "$split_is_ip" = "0" ]; then
- server_json=$(echo "$server_json" | jq \
- --arg split_resolver_tag "$split_resolver_tag" \
- --arg split_dns_resolver "$split_dns_resolver" \
- '.servers += [{
- "tag": $split_resolver_tag,
- "address": $split_dns_resolver
- }]')
- fi
- fi
-
- server_json=$(echo "$server_json" | jq '.servers += [{"tag": "fakeip-server", "address": "fakeip"}]')
-
- jq \
- --argjson dns_config "$server_json" \
- --arg fakeip "$FAKEIP" \
- --argjson split_dns_enabled "$split_dns_enabled" \
- '.dns = {
- "strategy": "ipv4_only",
- "independent_cache": true,
- "final": (
- if $split_dns_enabled == 1 then
- "split-dns-server"
- else
- "dns-server"
- end
- ),
- "fakeip": {
- "enabled": true,
- "inet4_range": $fakeip
- },
- "servers": $dns_config.servers
- }' "$SING_BOX_CONFIG" | build_sing_box_config
+ config_foreach configure_outbound_handler
}
-sing_box_create_bypass_ruleset() {
- log "Creating bypass ruleset for direct access"
-
- jq '
- .route.rule_set += [{
- "tag": "bypass",
- "type": "inline",
- "rules": [
- {
- "domain_suffix": [
- "ip.podkop.fyi"
- ]
- }
- ]
- }]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- # Add a rule to route bypass domains to direct-out outbound
- jq '
- .route.rules += [{
- "inbound": ["tproxy-in"],
- "rule_set": ["bypass"],
- "outbound": "main",
- "action": "route"
- }]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- # Make sure the bypass ruleset is in the fakeip DNS rule
- jq '
- .dns.rules = (.dns.rules | map(
- if (.server == "fakeip-server" or (.server == "dns-server" and .invert == true)) then
- if any(.rule_set[]?; . == "bypass") then
- .
- else
- .rule_set += ["bypass"]
- end
- else
- .
- end
- ))' "$SING_BOX_CONFIG" | build_sing_box_config
-}
-
-sing_box_dns_rule_fakeip() {
- local rewrite_ttl
- config_get rewrite_ttl "main" "dns_rewrite_ttl" "60"
- config_get split_dns_enabled "main" "split_dns_enabled" "0"
-
- log "Configure fakeip route in sing-box and set TTL to $rewrite_ttl seconds"
-
- jq \
- --arg ttl "$rewrite_ttl" \
- --argjson split_dns_enabled "$split_dns_enabled" \
- '.dns.rules = [
- {
- "query_type": [
- "HTTPS"
- ],
- "action": "reject"
- },
- {
- "domain_suffix": [
- "use-application-dns.net"
- ],
- "action": "reject"
- },
- {
- "server": "fakeip-server",
- "domain": "",
- "rewrite_ttl": ($ttl | tonumber),
- "rule_set": []
- }
- ]
- + (
- if $split_dns_enabled == 1 then
- [{
- "server": "dns-server",
- "domain": "",
- "invert": true,
- "rule_set": []
- }]
- else []
- end
- )' "$SING_BOX_CONFIG" | build_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" or (.server == "dns-server" and .invert == true)) then
- if any(.rule_set[]?; . == $rule_set) then
- .
- else
- .rule_set += [$rule_set]
- end
- else
- .
- end
- )' "$SING_BOX_CONFIG" | build_sing_box_config
-}
-
-sing_box_cache_file() {
- config_get cache_file "main" "cache_file" "/tmp/cache.db"
-
- log "Configure sing-box cache.db path"
-
- jq \
- --arg cache_file "$cache_file" \
- '.experimental = {
- "cache_file": {
- "enabled": true,
- "store_fakeip": true,
- "path": $cache_file
- }
- }' "$SING_BOX_CONFIG" | build_sing_box_config
-}
-
-sing_box_outdound() {
+configure_outbound_handler() {
local section="$1"
- config_get mode "$section" "mode"
- case "$mode" in
- "vpn")
- log "VPN mode"
- log "You are using VPN mode, make sure you have installed all the necessary packages and configured."
- config_get interface "$section" "interface"
-
- if [ -z "$interface" ]; then
- log "[critical] VPN interface is not set. Exit"
- exit 1
- fi
-
- sing_box_outbound_interface $section $interface
- ;;
- "proxy")
- log "Proxy mode"
+ local connection_mode
+ config_get connection_mode "$section" "mode"
+ case "$connection_mode" in
+ proxy)
+ log "Configuring outbound in proxy connection mode for the $section section"
+ local proxy_config_type
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" "$section"
- else
- log "Missing outbound JSON configuration"
- return
- fi
- else
- config_get proxy_string $section "proxy_string"
-
+ case "$proxy_config_type" in
+ url)
+ log "Detected proxy configuration type: url"
+ local proxy_string udp_over_tcp
+ config_get proxy_string "$section" "proxy_string"
+ config_get udp_over_tcp "$section" "ss_uot"
# Extract the first non-comment line as the active configuration
active_proxy_string=$(echo "$proxy_string" | grep -v "^[[:space:]]*\/\/" | head -n 1)
if [ -z "$active_proxy_string" ]; then
- log "[critical] Proxy string is not set. Exit"
+ log "Proxy string is not set. Aborted." "fatal"
exit 1
fi
-
- if [[ "$active_proxy_string" =~ ^ss:// ]]; then
- config_get ss_uot $section "ss_uot"
- sing_box_config_shadowsocks "$section" "$active_proxy_string" "$ss_uot"
- elif [[ "$active_proxy_string" =~ ^vless:// ]]; then
- sing_box_config_vless "$section" "$active_proxy_string"
- else
- log "Unsupported proxy type or missing configuration"
- return
- fi
- fi
+ config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$active_proxy_string" "$udp_over_tcp")
+ ;;
+ outbound)
+ log "Detected proxy configuration type: outbound"
+ local json_outbound
+ config_get json_outbound "$section" "outbound_json"
+ config=$(sing_box_cf_add_json_outbound "$config" "$section" "$json_outbound")
+ ;;
+ *)
+ log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal"
+ exit 1
+ ;;
+ esac
;;
- "block")
- log "Block mode"
+ vpn)
+ log "Configuring outbound in VPN connection mode for the $section section"
+ local interface_name
+ config_get interface_name "$section" "interface"
+
+ if [ -z "$interface_name" ]; then
+ log "VPN interface is not set. Aborted." "fatal"
+ exit 1
+ fi
+
+ config=$(sing_box_cf_add_interface_outbound "$config" "$section" "$interface_name")
+ ;;
+ block)
+ log "Connection mode 'block' detected for the $section section – no outbound will be created (handled via reject route rules)"
;;
*)
- log "Requires *vpn* or *proxy* value"
- return
+ log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "fatal"
+ exit 1
;;
esac
}
-sing_box_outbound_interface() {
- local section="$1"
- local interface="$2"
-
- 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" | build_sing_box_config
-
- if [ $? -eq 0 ]; then
- log "Config updated successfully"
+sing_box_configure_dns() {
+ log "Configure the DNS section of a sing-box JSON configuration"
+ local split_dns_enabled final_dns_server
+ config_get_bool split_dns_enabled "main" "split_dns_enabled" 0
+ if [ "$split_dns_enabled" -eq 1 ]; then
+ final_dns_server="$SB_SPLIT_DNS_SERVER_TAG"
else
- log "Error: Invalid JSON config generated"
- return 1
+ final_dns_server="$SB_DNS_SERVER_TAG"
fi
-}
+ config=$(sing_box_cm_configure_dns "$config" "$final_dns_server" "ipv4_only" true)
-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" | build_sing_box_config
-}
+ local dns_type dns_server split_dns_type split_dns_server dns_server_address split_dns_server_address
+ config_get dns_type "main" "dns_type" "doh"
+ config_get dns_server "main" "dns_server" "1.1.1.1"
+ config_get split_dns_type "main" "split_dns_type" "udp"
+ config_get split_dns_server "main" "split_dns_server" "1.1.1.1"
+ dns_server_address=$(url_get_host "$dns_server")
+ split_dns_server_address=$(url_get_host "$split_dns_server")
-sing_box_config_check() {
- if ! sing-box -c $SING_BOX_CONFIG check >/dev/null 2>&1; then
- log "[critical] Sing-box configuration is invalid"
- exit 1
- fi
-}
-
-sing_box_config_outbound_json() {
- local json_config="$1"
- local section="$2"
-
- # Create new object with tag first, then merge with the rest of the config
- local modified_config=$(echo "$json_config" | jq --arg section "$section" \
- 'del(.tag) | {"tag": $section} + .')
-
- jq --argjson outbound "$modified_config" \
- --arg section "$section" \
- '. |
- .outbounds |= (
- map(
- if .tag == $section then
- $outbound
- else . end
- ) +
- (
- if (map(select(.tag == $section)) | length) == 0 then
- [$outbound]
- else [] end
- )
- )' "$SING_BOX_CONFIG" | build_sing_box_config
-
- if [ $? -eq 0 ]; then
- log "Outbound config updated successfully"
- else
- log "Error: Outbound invalid JSON config generated"
- return 1
- fi
-}
-
-sing_box_config_shadowsocks() {
- local section="$1"
- local STRING="$2"
- ss_uot="${3:-0}"
-
- if echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | base64 -d 2>/dev/null | grep -q ":"; then
- 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
- 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')
- if echo "$method" | base64 -d ; then
- method=$(echo "$method" | base64 -d)
- fi
+ local need_dns_domain_resolver=0
+ if ! is_ipv4 "$dns_server_address" || ! is_ipv4 "$split_dns_server_address"; then
+ need_dns_domain_resolver=1
fi
- local server=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f1)
- local port=$(echo "$STRING" | sed -n 's|.*:\([0-9]\+\).*|\1|p')
+ log "Adding DNS Servers"
+ config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE")
- jq \
- --arg section "$section" \
- --arg server "$server" \
- --argjson port "$port" \
- --arg method "$method" \
- --arg password "$password" \
- --argjson ss_uot "$ss_uot" \
- '. |
- .outbounds |= (
- map(
- if .tag == $section then
- . + {
- "type": "shadowsocks",
- "server": $server,
- "server_port": ($port | tonumber),
- "method": $method,
- "password": $password
- } + (if $ss_uot == 1 then { "udp_over_tcp": { "enabled": true, "version": 2 } } else {} end)
- else . end
- ) +
- (
- if (map(select(.tag == $section)) | length) == 0 then
- [{
- "tag": $section,
- "type": "shadowsocks",
- "server": $server,
- "server_port": ($port | tonumber),
- "method": $method,
- "password": $password
- } + (if $ss_uot == 1 then { "udp_over_tcp": { "enabled": true, "version": 2 } } else {} end)]
- else [] end
- )
- )' "$SING_BOX_CONFIG" | build_sing_box_config
-
- if [ $? -eq 0 ]; then
- log "Config Shadowsocks updated successfully"
- else
- log "Error: Shadowsocks invalid JSON config generated"
- return 1
- fi
-}
-
-sing_box_config_vless() {
- local section="$1"
- local STRING="$2"
-
- get_param() {
- local param="$1"
- local value=$(echo "$STRING" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
- value=$(echo "$value" | sed 's/%2F/\//g; s/%2C/,/g; s/%3D/=/g; s/%2B/+/g; s/%20/ /g; s/%3B/;/g' | tr -d '\n' | tr -d '\r')
- echo "$value"
- }
-
- uuid=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | tr -d '\n' | tr -d '\r' | sed 's/False//g')
- 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 \
- --arg server "$server" \
- --argjson port "$port" \
- --arg uuid "$uuid" \
- --arg type "$(get_param "type")" \
- --arg flow "$(get_param "flow")" \
- --arg sni "$(get_param "sni")" \
- --arg fp "$(get_param "fp")" \
- --arg security "$(get_param "security")" \
- --arg pbk "$(get_param "pbk")" \
- --arg sid "$(get_param "sid")" \
- --arg alpn "$(get_param "alpn")" \
- --arg path "$(get_param "path")" \
- --arg host "$(get_param "host")" \
- --arg spx "$(get_param "spx")" \
- --arg insecure "$(get_param "allowInsecure")" \
- --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
- }
- 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
- .tls.alpn = ($alpn | split(","))
- else . end |
- if $security == "reality" then
- .tls.reality = {
- "enabled": true,
- "public_key": $pbk,
- "short_id": $sid
- }
- else . end
- else . end
- )
- else . end' "$SING_BOX_CONFIG" | build_sing_box_config
-
-
- if [ $? -eq 0 ]; then
- log "Config VLESS created successfully"
- else
- log "[critical] Error: VLESS invalid JSON config generated"
- exit 1
- fi
-}
-
-# 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]
+ local dns_domain_resolver
+ if [ "$need_dns_domain_resolver" -eq 1 ]; then
+ log "One of the DNS server addresses is a domain. Searching for a working DNS server..."
+ dns_domain_resolver=$(find_working_resolver)
+ if [ -z "$dns_domain_resolver" ]; then
+ log "Working DNS server not found, using default DNS server"
+ dns_domain_resolver="1.1.1.1"
else
- .
- end
- ' "$SING_BOX_CONFIG" | build_sing_box_config
+ log "Working DNS server has been found: $dns_domain_resolver"
+ fi
+ config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$dns_domain_resolver" 53)
+ dns_domain_resolver="$SB_DNS_DOMAIN_RESOLVER_TAG"
+ fi
- log "$domain added to the list for tag $tag"
+ config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver")
+
+ if [ "$split_dns_enabled" -eq 1 ]; then
+ config=$(
+ sing_box_cf_add_dns_server "$config" "$split_dns_type" "$SB_SPLIT_DNS_SERVER_TAG" "$split_dns_server" \
+ "$dns_domain_resolver" "$SB_MAIN_OUTBOUND_TAG"
+ )
+ fi
+
+ log "Adding DNS Rules"
+ local rewrite_ttl service_domains
+ config_get rewrite_ttl "main" "dns_rewrite_ttl" "60"
+
+ config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "HTTPS")
+ config=$(sing_box_cm_add_dns_reject_rule "$config" "domain_suffix" '"use-application-dns.net"')
+ config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_DNS_RULE_TAG")
+ config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rewrite_ttl" "$rewrite_ttl")
+ service_domains=$(comma_string_to_json_array "$FAKEIP_TEST_DOMAIN,$CHECK_PROXY_IP_DOMAIN")
+ config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
+ if [ "$split_dns_enabled" -eq 1 ]; then
+ config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_DNS_SERVER_TAG" "$SB_INVERT_FAKEIP_DNS_RULE_TAG")
+ config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "invert" true)
+ config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
+ fi
+}
+
+sing_box_configure_route() {
+ log "Configure the route section of a sing-box JSON configuration"
+
+ config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG")
+
+ local sniff_inbounds mixed_inbound_enabled
+ config_get_bool mixed_inbound_enabled "main" "socks5" 0
+ if [ "$mixed_inbound_enabled" -eq 1 ]; then
+ sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG,$SB_MIXED_INBOUND_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]
- }
- ]
- }
- ]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- log "$domain added as a new rule set for tag $tag"
+ sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG")
fi
-}
+ config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "$sniff_inbounds")
-sing_box_ruleset_subnets() {
- log "Configure ruleset domains in sing-box"
+ config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns")
- 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
- ' "$SING_BOX_CONFIG" | build_sing_box_config
-
- 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]
- }
- ]
- }
- ]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- log "$subnet added as a new rule set for tag $tag"
+ local quic_disable
+ config_get_bool quic_disable "main" "quic_disable" 0
+ if [ "$quic_disable" -eq 1 ]; then
+ config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "protocol" "quic")
fi
-}
-sing_box_ruleset_domains_json() {
- local domain="$1"
- local section="$2"
+ config=$(
+ sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$SB_MAIN_OUTBOUND_TAG"
+ )
+ config=$(sing_box_cf_override_domain_port "$config" "$FAKEIP_TEST_DOMAIN" 8443)
- local file="/tmp/podkop/$section-custom-domains-subnets.json"
+ config_foreach include_source_ips_in_routing_handler
- jq --arg domain "$domain" '
- .rules[0].domain_suffix += if .rules[0].domain_suffix | index($domain) then [] else [$domain] end
- ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
+ configure_common_reject_route_rule
- log "$domain added to $section-custom-domains-subnets.json"
-}
-
-sing_box_ruleset_subnets_json() {
- local subnet="$1"
- local section="$2"
-
- local file="/tmp/podkop/$section-custom-domains-subnets.json"
-
- jq --arg subnet "$subnet" '
- .rules[0].ip_cidr += if .rules[0].ip_cidr | index($subnet) then [] else [$subnet] end
- ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
-
- log "$subnet added to $section-custom-domains-subnets.json"
-}
-
-#######################################
-# Adds a new remote ruleset to the sing-box configuration.
-# https://sing-box.sagernet.org/configuration/rule-set/#__tabbed_1_3
-#
-# Arguments:
-# tag: unique identifier for the ruleset.
-# format: format of the ruleset (e.g., "source" or "binary").
-# url: URL from which the ruleset can be fetched.
-# update_interval: update interval for the ruleset (e.g., "1d").
-# detour: flag indicating whether to use a download detour ("1" or "0").
-#
-# Outputs:
-# Modifies the sing-box configuration file by appending a new ruleset entry.
-#
-# Returns:
-# None. Always returns 0. If a ruleset with the same tag exists, it is skipped.
-#######################################
-sing_box_config_add_remote_ruleset() {
- local tag=$1
- local format=$2
- local url=$3
- local update_interval=$4
- local detour=$5
-
- local tag_exists
- tag_exists=$(jq -r --arg tag "$tag" '
- .route.rule_set[]? | select(.tag == $tag) | .tag
- ' "$SING_BOX_CONFIG")
-
- if [[ -n "$tag_exists" ]]; then
- log "Ruleset with tag $tag already exists. Skipping addition."
- else
- jq \
- --arg tag "$tag" \
- --arg format "$format" \
- --arg url "$url" \
- --arg update_interval "$update_interval" \
- --arg detour "$detour" \
- '
- .route.rule_set += [
- (
- {
- "tag": $tag,
- "type": "remote",
- "format": $format,
- "url": $url,
- "update_interval": $update_interval
- } +
- (if $detour == "1" then {"download_detour": "main"} else {} end)
- )
- ]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- log "Added new remote ruleset with tag $tag"
+ local exclude_from_ip_enabled
+ config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" 0
+ if [ "$exclude_from_ip_enabled" -eq 1 ]; then
+ rule_tag="$(gen_id)"
+ config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG")
+ config_list_foreach "main" "exclude_traffic_ip" exclude_source_ip_from_routing_handler "$rule_tag"
fi
+
+ config_foreach configure_routing_for_section_lists
}
-#######################################
-# Adds a remote ruleset to the sing-box configuration and applies route and dns rules.
-#
-# Arguments:
-# url: remote ruleset URL.
-# section: configuration section where rules will be applied.
-# ruleset_content_type: Type of ruleset content (e.g., "domains" or "subnets").
-#
-# Returns:
-# 0 on success, non-zero if the file extension is unsupported.
-#######################################
-sing_box_add_remote_ruleset_and_rules() {
- local url="$1"
- local section="$2"
- local ruleset_content_type="$3"
-
- local tag
- local format
- local update_interval='1d'
- local detour
-
- case "$(get_url_file_extension "$url")" in
- json) format="source" ;;
- srs) format="binary" ;;
- *)
- log "Unsupported file extension: .$file_extension"
- return 1
- ;;
- esac
-
- tag=$(get_ruleset_tag_from_url "$url" "$section-remote-$ruleset_content_type")
- config_get_bool detour "main" "detour" "0"
-
- sing_box_config_add_remote_ruleset "$tag" "$format" "$url" "$update_interval" "$detour"
- sing_box_rules "$tag" "$section"
- if [[ "$ruleset_content_type" = "domains" ]]; then
- sing_box_dns_rule_fakeip_section "$tag"
- fi
-}
-
-process_domains_for_section() {
+include_source_ips_in_routing_handler() {
local section="$1"
- config_get custom_domains_list_type "$section" "custom_domains_list_type" "disabled"
+ local all_traffic_from_ip_enabled rule_tag
+ config_get all_traffic_from_ip_enabled "$section" "all_traffic_from_ip_enabled" 0
+ if [ "$all_traffic_from_ip_enabled" -eq 1 ]; then
+ rule_tag="$(gen_id)"
+ config=$(
+ sing_box_cm_add_route_rule \
+ "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$(get_outbound_tag_by_section "$section")"
+ )
+ config_list_foreach "$section" "all_traffic_ip" include_source_ip_in_routing_handler "$rule_tag"
+ fi
+}
- 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"
+configure_common_reject_route_rule() {
+ local block_sections block_section_lists_enabled
+ block_sections="$(get_block_sections)"
+ block_section_lists_enabled=0
+
+ if [ -n "$block_sections" ]; then
+ for block_section in $block_sections; do
+ if section_has_enabled_lists "$block_section"; then
+ block_section_lists_enabled=1
+ break
+ fi
+ done
+ if [ "$block_section_lists_enabled" -eq 1 ]; then
+ config=$(sing_box_cm_add_reject_route_rule "$config" "$SB_REJECT_RULE_TAG" "$SB_TPROXY_INBOUND_TAG")
+ else
+ log "Block sections does not have any enabled list, reject rule is not required" "warn"
fi
fi
}
-sing_box_rules() {
- log "Configure rule in sing-box"
- local rule_set="$1"
- local outbound="$2"
+include_source_ip_in_routing_handler() {
+ local source_ip="$1"
+ local rule_tag="$2"
+ nft_list_all_traffic_from_ip "$source_ip"
+ config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip")
+}
- config_get mode "$section" "mode"
+exclude_source_ip_from_routing_handler() {
+ local source_ip="$1"
+ local rule_tag="$2"
- if [[ "$mode" == "block" ]]; then
- # Action reject
- # Check if there is an rule with reject"
- local rule_exists=$(jq -r '.route.rules[] | select(.inbound == ["tproxy-in"] and .action == "reject")' "$SING_BOX_CONFIG")
+ config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip")
+}
- if [[ -n "$rule_exists" ]]; then
- # If a rule for rejectexists, add a new rule_set to the existing rule
- jq \
- --arg rule_set "$rule_set" \
- '(.route.rules[] | select(.inbound == ["tproxy-in"] and .action == "reject") .rule_set) += [$rule_set]' \
- "$SING_BOX_CONFIG" | build_sing_box_config
- else
- # If there is no rule for reject, create a new one with rule_set
- jq \
- --arg rule_set "$rule_set" \
- '.route.rules += [{
- "inbound": ["tproxy-in"],
- "rule_set": [$rule_set],
- "action": "reject"
- }]' "$SING_BOX_CONFIG" | build_sing_box_config
- fi
- return
+configure_routing_for_section_lists() {
+ local section="$1"
+
+ if ! section_has_enabled_lists "$section"; then
+ log "Section '$section' does not have any enabled list, skipping..." "warn"
+ return 0
+ fi
+
+ local community_lists_enabled user_domain_list_type local_domain_lists_enabled remote_domain_lists_enabled \
+ user_subnet_list_type local_subnet_lists_enabled remote_subnet_lists_enabled section_mode_type route_rule_tag
+ config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0
+ config_get user_domain_list_type "$section" "user_domain_list_type" "disabled"
+ config_get_bool local_domain_lists_enabled "$section" "local_domain_lists_enabled" 0
+ config_get_bool remote_domain_lists_enabled "$section" "remote_domain_lists_enabled" 0
+ config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled"
+ config_get_bool local_subnet_lists_enabled "$section" "local_subnet_lists_enabled" 0
+ config_get_bool remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" 0
+ config_get section_mode_type "$section" "mode"
+
+ if [ "$section_mode_type" = "block" ]; then
+ route_rule_tag="$SB_REJECT_RULE_TAG"
else
- # Action route
- # Check if there is an outbound rule for "tproxy-in"
- local rule_exists=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .inbound == ["tproxy-in"])' "$SING_BOX_CONFIG")
+ route_rule_tag="$(gen_id)"
+ outbound_tag=$(get_outbound_tag_by_section "$section")
+ config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag")
+ fi
- 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" | build_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" | build_sing_box_config
- fi
+ if [ "$community_lists_enabled" -eq 1 ]; then
+ log "Processing community list routing rules for '$section' section"
+ config_list_foreach "$section" "community_lists" configure_community_list_handler "$section" "$route_rule_tag"
+ fi
+
+ if [ "$user_domain_list_type" != "disabled" ]; then
+ log "Processing user domains routing rules for '$section' section"
+ prepare_common_ruleset "$section" "domains" "$route_rule_tag"
+ configure_user_domain_or_subnets_list "$section" "domains" "$route_rule_tag"
+ fi
+
+ if [ "$local_domain_lists_enabled" -eq 1 ]; then
+ log "Processing local domains routing rules for '$section' section"
+ configure_local_domain_or_subnet_lists "$section" "domains" "$route_rule_tag"
+ fi
+
+ if [ "$remote_domain_lists_enabled" -eq 1 ]; then
+ log "Processing remote domains routing rules for '$section' section"
+ prepare_common_ruleset "$section" "domains" "$route_rule_tag"
+ config_list_foreach "$section" "remote_domain_lists" configure_remote_domain_or_subnet_list_handler \
+ "domains" "$section" "$route_rule_tag"
+ fi
+
+ if [ "$user_subnet_list_type" != "disabled" ]; then
+ log "Processing user subnets routing rules for '$section' section"
+ prepare_common_ruleset "$section" "subnets" "$route_rule_tag"
+ configure_user_domain_or_subnets_list "$section" "subnets" "$route_rule_tag"
+ fi
+
+ if [ "$local_subnet_lists_enabled" -eq 1 ]; then
+ log "Processing local subnets routing rules for '$section' section"
+ configure_local_domain_or_subnet_lists "$section" "subnets" "$route_rule_tag"
+ fi
+
+ if [ "$remote_subnet_lists_enabled" -eq 1 ]; then
+ log "Processing remote subnets routing rules for '$section' section"
+ prepare_common_ruleset "$section" "subnets" "$route_rule_tag"
+ config_list_foreach "$section" "remote_subnet_lists" configure_remote_domain_or_subnet_list_handler \
+ "subnets" "$section" "$route_rule_tag"
fi
}
-sing_box_quic_reject() {
- local quic_rule_exists=$(jq -e '.route.rules[] | select(.protocol == "quic" and .action == "reject")' "$SING_BOX_CONFIG")
-
- if [[ -z "$quic_rule_exists" ]]; then
- jq '
- .route.rules |= (
- reduce .[] as $rule ([];
- if $rule.protocol == "dns" and $rule.action == "hijack-dns" then
- . + [$rule, {"protocol": "quic", "action": "reject"}]
- else
- . + [$rule]
- end
- )
- )' "$SING_BOX_CONFIG" | build_sing_box_config
-
- log "QUIC reject rule added successfully"
- fi
-}
-
-# TODO(ampetelin): function needs refactoring
-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"
- config_get custom_local_domains_list_enabled "$section" "custom_local_domains_list_enabled"
-
- if [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_subnets_list_enabled" != "disabled" ] ||
- [ "$custom_local_domains_list_enabled" = "1" ]; then
- sing_box_rules "$section" "$section"
- fi
-
- if [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_local_domains_list_enabled" = "1" ]; then
- 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
- domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
- if [ -n "$domain" ] && echo "$domain" | grep -E -q '^([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$'; then
- log "Added $domain from local file"
- sing_box_ruleset_domains "$domain" "$section"
- else
- log "Invalid domain skipped: $domain"
- fi
- done <"$local_file"
-}
-
-process_domains_list_local() {
+prepare_common_ruleset() {
local section="$1"
+ local type="$2"
+ local route_rule_tag="$3"
- 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
-}
-
-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
-}
-
-configure_community_lists() {
- config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
- if [ "$domain_list_enabled" -eq 1 ]; then
- log "Configuring community lists for $section section"
- config_list_foreach "$section" domain_list configure_community_list_handler
+ log "Preparing a common $type ruleset for '$section' section" "debug"
+ ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
+ ruleset_filename="$ruleset_tag.json"
+ ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
+ if file_exists "$ruleset_filepath"; then
+ log "Ruleset $ruleset_filepath already exists. Skipping." "debug"
+ else
+ sing_box_cm_create_local_source_ruleset "$ruleset_filepath"
+ config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
+ config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
+ case "$type" in
+ domains) _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" ;;
+ subnets) ;;
+ *) log "Unsupported remote rule set type: $type" "warn" ;;
+ esac
fi
}
configure_community_list_handler() {
- local tag=$1
-
- local format="binary"
- local update_interval="1d"
- config_get_bool detour "main" "detour" "0"
- local url="$SRS_MAIN_URL/$tag.srs"
-
- sing_box_config_add_remote_ruleset "$tag" "$format" "$url" "$update_interval" "$detour"
-}
-
-configure_remote_domain_lists() {
- 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 "Configuring remote domain lists for $section section"
- config_list_foreach "$section" custom_download_domains configure_remote_domain_list_handler "$section"
- fi
-}
-
-configure_remote_domain_list_handler() {
- local url="$1"
+ local tag="$1"
local section="$2"
-
- log "Configuring remote domain list from URL: $url"
+ local route_rule_tag="$3"
- local file_extension
- file_extension=$(get_url_file_extension "$url")
- case "$file_extension" in
- json|srs)
- log "Detected file extension: .$file_extension → proceeding with processing"
- sing_box_add_remote_ruleset_and_rules "$url" "$section" "domains"
- ;;
- *)
- log "Detected file extension: .$file_extension → no processing needed, managed on list_update"
- ;;
+ local ruleset_tag format url update_interval detour
+ ruleset_tag="$(get_ruleset_tag "$section" "$tag" "community")"
+ format="binary"
+ url="$SRS_MAIN_URL/$tag.srs"
+ detour="$(get_download_detour_tag)"
+ config_get update_interval "main" "update_interval" "1d"
+
+ config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval")
+ _add_ruleset_to_dns_rules "$ruleset_tag"
+ config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
+}
+
+configure_user_domain_or_subnets_list() {
+ local section="$1"
+ local type="$2"
+
+ local items ruleset_tag ruleset_filename ruleset_filepath json_array
+ case "$type" in
+ domains)
+ local user_domain_list_type
+ config_get user_domain_list_type "$section" "user_domain_list_type"
+ case "$user_domain_list_type" in
+ dynamic) config_get items "$section" "user_domains" ;;
+ text) config_get items "$section" "user_domains_text" ;;
+ esac
+ ;;
+ subnets)
+ local user_subnet_list_type
+ config_get user_subnet_list_type "$section" "user_subnet_list_type"
+ case "$user_subnet_list_type" in
+ dynamic) config_get items "$section" "user_subnets" ;;
+ text) config_get items "$section" "user_subnets_text" ;;
+ esac
+ ;;
+ esac
+
+ ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
+ ruleset_filename="$ruleset_tag.json"
+ ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
+ items="$(parse_domain_or_subnet_string_to_commas_string "$items" "$type")"
+ json_array="$(comma_string_to_json_array "$items")"
+ case "$type" in
+ domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
+ subnets)
+ sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
+ ;;
esac
}
-configure_remote_subnet_lists() {
+configure_local_domain_or_subnet_lists() {
local section="$1"
+ local type="$2"
+ local route_rule_tag="$3"
- config_get custom_download_subnets_list_enabled "$section" custom_download_subnets_list_enabled disabled
- if [ "$custom_download_subnets_list_enabled" -eq "1" ]; then
- log "Configuring remote subnet lists for $section section"
- config_list_foreach "$section" custom_download_subnets configure_remote_subnet_list_handler "$section"
- fi
-}
+ local ruleset_tag ruleset_filename ruleset_filepath
+ ruleset_tag="$(get_ruleset_tag "$section" "local" "$type")"
+ ruleset_filename="$ruleset_tag.json"
+ ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
-configure_remote_subnet_list_handler() {
- local url="$1"
- local section="$2"
+ sing_box_cm_create_local_source_ruleset "$ruleset_filepath"
+ config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
+ config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
- log "Configuring remote subnet list from URL: $url"
-
- local file_extension
- file_extension=$(get_url_file_extension "$url")
- case "$file_extension" in
- json|srs)
- log "Detected file extension: .$file_extension → proceeding with processing"
- sing_box_add_remote_ruleset_and_rules "$url" "$section" "subnets"
- ;;
- *)
- log "Detected file extension: .$file_extension → no processing needed, managed on list_update"
- ;;
+ case "$type" in
+ domains)
+ config_list_foreach "$section" "local_domain_lists" import_local_domain_or_subnet_list "$type" \
+ "$section" "$ruleset_filepath"
+ _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag"
+ ;;
+ subnets)
+ config_list_foreach "$section" "local_subnet_lists" import_local_domain_or_subnet_list "$type" \
+ "$section" "$ruleset_filepath"
+ ;;
+ *) log "Unsupported local rule set type: $type" "warn" ;;
esac
}
-process_all_traffic_for_section() {
- local section="$1"
+import_local_domain_or_subnet_list() {
+ local filepath="$1"
+ local type="$2"
+ local section="$3"
+ local ruleset_filepath="$4"
- 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 nft_list_all_traffic_from_ip
- config_list_foreach $section all_traffic_ip sing_box_rules_source_ip_cidr $all_traffic_ip $section
+ if ! file_exists "$filepath"; then
+ log "File $filepath not found" "warn"
+ return 1
+ fi
+
+ local items json_array
+ items="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "domains")"
+
+ if [ -z "$items" ]; then
+ log "No valid $type found in $filepath"
+ return 0
+ fi
+
+ json_array="$(comma_string_to_json_array "$items")"
+ case "$type" in
+ domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
+ subnets)
+ sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
+ ;;
+ esac
+}
+
+configure_remote_domain_or_subnet_list_handler() {
+ local url="$1"
+ local type="$2"
+ local section="$3"
+ local route_rule_tag="$4"
+
+ local file_extension
+ file_extension=$(url_get_file_extension "$url")
+ case "$file_extension" in
+ json | srs)
+ log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
+ local basename ruleset_tag format detour update_interval
+ basename=$(url_get_basename "$url")
+ ruleset_tag=$(get_ruleset_tag "$section" "$basename" "remote-$type")
+ format="$(get_ruleset_format_by_file_extension "$file_extension")"
+ detour="$(get_download_detour_tag)"
+ config_get update_interval "main" "update_interval" "1d"
+
+ config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval")
+ config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
+ case "$type" in
+ domains) _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" ;;
+ subnets) ;;
+ *) log "Unsupported remote rule set type: $type" "warn" ;;
+ esac
+ ;;
+ *)
+ log "Detected file extension: '$file_extension' → no processing needed, managed on list_update" "debug"
+ ;;
+ esac
+}
+
+_add_ruleset_to_dns_rules() {
+ local ruleset_tag="$1"
+
+ config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
+ local split_dns_enabled final_dns_server
+ config_get_bool split_dns_enabled "main" "split_dns_enabled" 0
+ if [ "$split_dns_enabled" -eq 1 ]; then
+ config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
+ fi
+}
+
+sing_box_configure_experimental() {
+ log "Configure the experimental section of a sing-box JSON configuration"
+
+ log "Configuring cache database"
+ local cache_file
+ config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db"
+ config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)
+
+ local yacd_enabled
+ config_get_bool yacd_enabled "main" "yacd" 0
+ if [ "$yacd_enabled" -eq 1 ]; then
+ log "Configuring Clash API (yacd)"
+ local external_controller="0.0.0.0:9090"
+ local external_controller_ui="ui"
+ config=$(sing_box_cm_configure_clash_api "$config" "$external_controller" "$external_controller_ui")
+ else
+ log "Clash API (yacd) is disabled, skipping configuration."
+ fi
+}
+
+sing_box_additional_inbounds() {
+ log "Configure the additional inbounds of a sing-box JSON configuration"
+
+ local mixed_inbound_enabled
+ config_get_bool mixed_inbound_enabled "main" "socks5" 0
+ if [ "$mixed_inbound_enabled" -eq 1 ]; then
+ config=$(
+ sing_box_cf_add_mixed_inbound_and_route_rule \
+ "$config" \
+ "$SB_MIXED_INBOUND_TAG" \
+ "$SB_MIXED_INBOUND_ADDRESS" \
+ "$SB_MIXED_INBOUND_PORT" \
+ "$SB_MAIN_OUTBOUND_TAG"
+ )
+ fi
+
+ config=$(
+ sing_box_cf_add_mixed_inbound_and_route_rule \
+ "$config" \
+ "$SB_SERVICE_MIXED_INBOUND_TAG" \
+ "$SB_SERVICE_MIXED_INBOUND_ADDRESS" \
+ "$SB_SERVICE_MIXED_INBOUND_PORT" \
+ "$SB_MAIN_OUTBOUND_TAG"
+ )
+}
+
+sing_box_save_config() {
+ local sing_box_config_path temp_file_path current_config_hash temp_config_hash
+ config_get sing_box_config_path "main" "config_path"
+ temp_file_path="$(mktemp)"
+
+ log "Save sing-box temporary config to $temp_file_path" "debug"
+ sing_box_cm_save_config_to_file "$config" "$temp_file_path"
+
+ current_config_hash=$(md5sum "$sing_box_config_path" 2> /dev/null | awk '{print $1}')
+ temp_config_hash=$(md5sum "$temp_file_path" | awk '{print $1}')
+ log "Current sing-box config hash: $current_config_hash" "debug"
+ log "Temporary sing-box config hash: $temp_config_hash" "debug"
+ if [ "$current_config_hash" != "$temp_config_hash" ]; then
+ log "sing-box configuration has changed and will be updated"
+ mv "$temp_file_path" "$sing_box_config_path"
+ else
+ log "sing-box configuration is unchanged"
+ rm "$temp_file_path"
+ fi
+}
+
+sing_box_config_check() {
+ local sing_box_config_path
+ config_get sing_box_config_path "main" "config_path"
+ if ! sing-box -c "$sing_box_config_path" check > /dev/null 2>&1; then
+ log "Sing-box configuration is invalid" "fatal"
+ exit 1
fi
}
import_community_subnet_lists() {
- config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
- if [ "$domain_list_enabled" -eq 1 ]; then
- log "Importing community subnet lists for $section section"
- config_list_foreach "$section" domain_list import_community_service_subnet_list_handler
+ local section="$1"
+ local community_lists_enabled
+ config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0
+ if [ "$community_lists_enabled" -eq 1 ]; then
+ log "Importing community subnet lists for '$section' section"
+ config_list_foreach "$section" "community_lists" import_community_service_subnet_list_handler
fi
}
import_community_service_subnet_list_handler() {
local service="$1"
- local table="PodkopTable"
case "$service" in
- "twitter")
- URL=$SUBNETS_TWITTER
- ;;
- "meta")
- URL=$SUBNETS_META
- ;;
- "telegram")
- URL=$SUBNETS_TELERAM
- ;;
- "cloudflare")
- URL=$SUBNETS_CLOUDFLARE
- ;;
- "hetzner")
- URL=$SUBNETS_HETZNER
- ;;
- "ovh")
- URL=$SUBNETS_OVH
- ;;
- "digitalocean")
- URL=$SUBNETS_DIGITALOCEAN
- ;;
- "cloudfront")
- URL=$SUBNETS_CLOUDFRONT
- ;;
- "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 "$SRC_INTERFACE" ip daddr @podkop_discord_subnets udp dport { 50000-65535 } meta mark set 0x105 counter
- ;;
- *)
- return
- ;;
+ "twitter")
+ URL=$SUBNETS_TWITTER
+ ;;
+ "meta")
+ URL=$SUBNETS_META
+ ;;
+ "telegram")
+ URL=$SUBNETS_TELERAM
+ ;;
+ "cloudflare")
+ URL=$SUBNETS_CLOUDFLARE
+ ;;
+ "hetzner")
+ URL=$SUBNETS_HETZNER
+ ;;
+ "ovh")
+ URL=$SUBNETS_OVH
+ ;;
+ "digitalocean")
+ URL=$SUBNETS_DIGITALOCEAN
+ ;;
+ "cloudfront")
+ URL=$SUBNETS_CLOUDFRONT
+ ;;
+ "discord")
+ URL=$SUBNETS_DISCORD
+ nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME"
+ nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr \
+ "@$NFT_DISCORD_SET_NAME" udp dport '{ 50000-65535 }' meta mark set 0x105 counter
+ ;;
+ *) return 0 ;;
esac
- local filename=$(basename "$URL")
+ local tmpfile detour http_proxy_address subnets
+ tmpfile=$(mktemp)
+ http_proxy_address="$(get_service_proxy_address)"
- config_get_bool detour "main" "detour" "0"
- if [ "$detour" -eq 1 ]; then
- http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "/tmp/podkop/$filename" "$URL"
- else
- wget -O "/tmp/podkop/$filename" "$URL"
+ download_to_file "$URL" "$tmpfile" "$http_proxy_address"
+
+ if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
+ log "Download $service list failed" "error"
+ return 1
fi
- 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"
+ subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")"
+ rm -f "$tmpfile"
+
+ if [ "$service" = "discord" ]; then
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" "$subnets"
+ else
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
+ fi
}
import_domains_from_remote_domain_lists() {
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 "Importing domains from remote domain lists for $section section"
- config_list_foreach "$section" custom_download_domains import_domains_from_remote_domain_list_handler "$section"
+ local remote_domain_lists_enabled
+ config_get remote_domain_lists_enabled "$section" "remote_domain_lists_enabled"
+ if [ "$remote_domain_lists_enabled" -eq 1 ]; then
+ log "Importing domains from remote domain lists for '$section' section"
+ config_list_foreach "$section" "remote_domain_lists" import_domains_from_remote_domain_list_handler "$section"
fi
}
import_domains_from_remote_domain_list_handler() {
local url="$1"
local section="$2"
-
+
log "Importing domains from URL: $url"
local file_extension
- file_extension=$(get_url_file_extension "$url")
+ file_extension=$(url_get_file_extension "$url")
case "$file_extension" in
- json|srs)
- log "Detected file extension: .$file_extension → no update needed, sing-box manages updates"
- ;;
- *)
- log "Detected file extension: .$file_extension → proceeding with processing"
- import_domains_from_remote_lst_file "$url" "$section"
- ;;
+ json | srs)
+ log "Detected file extension: '$file_extension' → no update needed, sing-box manages updates" "debug"
+ ;;
+ *)
+ log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
+ import_domains_or_subnets_from_remote_file "$url" "$section" "domains"
+ ;;
esac
}
-import_domains_from_remote_lst_file() {
- local url="$1"
- local section="$2"
-
- local filename
- filename=$(basename "$url")
- local filepath="/tmp/podkop/${filename}"
-
- download_to_tempfile "$url" "$filepath"
-
- while IFS= read -r domain; do
- sing_box_ruleset_domains_json $domain $section
- done <"$filepath"
-
- rm -f "$filepath"
-}
-
import_subnets_from_remote_subnet_lists() {
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 "Importing subnets from remote subnet lists for $section section"
- config_list_foreach "$section" custom_download_subnets import_subnets_from_remote_subnet_list_handler "$section"
+ config_get remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled"
+ if [ "$remote_subnet_lists_enabled" -eq 1 ]; then
+ log "Importing subnets from remote subnet lists for '$section' section"
+ config_list_foreach "$section" "remote_subnet_lists" import_subnets_from_remote_subnet_list_handler "$section"
fi
}
@@ -1967,182 +1271,192 @@ import_subnets_from_remote_subnet_list_handler() {
log "Importing subnets from URL: $url"
local file_extension
- file_extension=$(get_url_file_extension "$url")
+ file_extension="$(url_get_file_extension "$url")"
case "$file_extension" in
- lst)
- log "Detected file extension: .$file_extension → proceeding with processing"
- import_subnets_from_remote_lst_file "$url" "$section"
- ;;
- json)
- log "Detected file extension: .$file_extension → proceeding with processing"
- import_subnets_from_remote_json_file "$url"
- ;;
- srs)
- log "Detected file extension: .$file_extension → proceeding with processing"
- import_subnets_from_remote_srs_file "$url"
- ;;
- *)
- log "Detected file extension: .$file_extension → unsupported, skipping"
- return 1
- ;;
+ json)
+ log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
+ import_subnets_from_remote_json_file "$url"
+ ;;
+ srs)
+ log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
+ import_subnets_from_remote_srs_file "$url"
+ ;;
+ *)
+ log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
+ import_domains_or_subnets_from_remote_file "$url" "$section" "subnets"
+ ;;
esac
}
-import_subnets_from_remote_lst_file() {
+import_domains_or_subnets_from_remote_file() {
local url="$1"
local section="$2"
+ local type="$3"
- local filename
- filename=$(basename "$url")
- local filepath="/tmp/podkop/${filename}"
+ local tmpfile http_proxy_address items json_array
+ tmpfile=$(mktemp)
+ http_proxy_address="$(get_service_proxy_address)"
- download_to_tempfile "$url" "$filepath"
+ download_to_file "$url" "$tmpfile" "$http_proxy_address"
- while IFS= read -r subnet; do
- sing_box_ruleset_subnets_json "$subnet" "$section"
- nft_add_podkop_subnet "$subnet"
- done <"$filepath"
+ if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
+ log "Download $url list failed" "error"
+ return 1
+ fi
- rm -f "$filepath"
+ items="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "$type")"
+ rm -f "$tmpfile"
+
+ if [ -z "$items" ]; then
+ log "No valid $type found in $url"
+ return 0
+ fi
+
+ ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
+ ruleset_filename="$ruleset_tag.json"
+ ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
+ json_array="$(comma_string_to_json_array "$items")"
+ case "$type" in
+ domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
+ subnets)
+ sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
+ ;;
+ esac
}
import_subnets_from_remote_json_file() {
local url="$1"
+ local tmpfile subnets http_proxy_address
+ tmpfile="$(mktemp)"
+ http_proxy_address="$(get_service_proxy_address)"
- download_to_stream "$url" | jq -r '.rules[].ip_cidr[]?' | while read -r subnet; do
- nft_add_podkop_subnet "$subnet"
- done
+ download_to_stream "$url" "$http_proxy_address" | jq -r '.rules[].ip_cidr[]?' > "$tmpfile"
+
+ if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
+ log "Download $url list failed" "error"
+ return 1
+ fi
+
+ subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")"
+ rm -f "$tmpfile"
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
}
import_subnets_from_remote_srs_file() {
local url="$1"
-
- local filename
- filename=$(basename "$url")
- local binary_filepath="/tmp/podkop/${filename}"
- local json_filepath="/tmp/podkop/decompiled-${filename%%.*}.json"
-
- download_to_tempfile "$url" "$binary_filepath"
- if ! decompile_srs_file "$binary_filepath" "$json_filepath"; then
+ local binary_tmpfile json_tmpfile subnets_tmpfile subnets http_proxy_address
+ binary_tmpfile="$(mktemp)"
+ json_tmpfile="$(mktemp)"
+ subnets_tmpfile="$(mktemp)"
+ http_proxy_address="$(get_service_proxy_address)"
+
+ download_to_file "$url" "$binary_tmpfile" "$http_proxy_address"
+
+ if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then
+ log "Download $url list failed" "error"
return 1
fi
- jq -r '.rules[].ip_cidr[]' "$json_filepath" | while read -r subnet; do
- nft_add_podkop_subnet "$subnet"
- done
+ if ! decompile_srs_file "$binary_tmpfile" "$json_tmpfile"; then
+ log "Failed to decompile SRS file" "error"
+ return 1
+ fi
- rm -f "$binary_filepath" "$json_filepath"
+ jq -r '.rules[].ip_cidr[]' "$json_tmpfile" > "$subnets_tmpfile"
+ subnets="$(parse_domain_or_subnet_file_to_comma_string "$subnets_tmpfile" "subnets")"
+ rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile"
+ nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
}
-# Decompiles a sing-box SRS binary file into a JSON ruleset file
-decompile_srs_file() {
- local binary_filepath="$1"
- local output_filepath="$2"
-
- log "Decompiling $binary_filepath to $output_filepath"
-
- if ! file_exists "$binary_filepath"; then
- log "File $binary_filepath not found"
- return 1
- fi
-
- sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
- if [[ $? -ne 0 ]]; then
- log "Decompilation command failed for $binary_filepath"
- return 1
- fi
-}
-
-sing_box_rules_source_ip_cidr() {
- log "Configure source_ip_cidr rule in sing-box"
- local source_ip_cidr="$1"
- local outbound="$2"
-
- local current_source_ip_cidr=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .action == "route" and .source_ip_cidr and (.inbound // [] | contains(["tproxy-in"])))' $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 and .action == "route" and .source_ip_cidr and (.inbound // [] | contains(["tproxy-in"]))) | .source_ip_cidr) += [$source_ip_cidr]' "$SING_BOX_CONFIG" | build_sing_box_config
+## Support functions
+get_service_proxy_address() {
+ local detour
+ config_get_bool detour "main" "detour" 0
+ if [ "$detour" -eq 1 ]; then
+ echo "$SB_SERVICE_MIXED_INBOUND_TAG:$SB_SERVICE_MIXED_INBOUND_PORT"
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,
- "action": "route"
- }
- ] + .route.rules' "$SING_BOX_CONFIG" | build_sing_box_config
+ echo ""
fi
}
-detour_mixed() {
- local section="main"
- local port="4534"
- local tag="detour"
+get_download_detour_tag() {
+ config_get_bool detour "main" "detour" 0
+ if [ "${detour:-0}" -eq 1 ]; then
+ echo "$SB_MAIN_OUTBOUND_TAG"
+ else
+ echo ""
+ fi
+}
- log "Adding detour Socks5 for $section on port $port"
+get_block_sections() {
+ uci show podkop | grep "\.mode='block'" | cut -d'.' -f2
+}
- jq \
- --arg tag "$tag" \
- --arg port "$port" \
- --arg section "$section" \
- '.inbounds += [{
- "tag": $tag,
- "type": "mixed",
- "listen": "127.0.0.1",
- "listen_port": ($port|tonumber),
- "set_system_proxy": false
- }] |
- .route.rules += [{
- "inbound": [$tag],
- "outbound": $section,
- "action": "route"
- }]' "$SING_BOX_CONFIG" | build_sing_box_config
+block_section_exists() {
+ if uci show podkop | grep -q "\.mode='block'"; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+section_has_enabled_lists() {
+ local section="$1"
+ local community_lists_enabled user_domain_list_type local_domain_lists_enabled remote_domain_lists_enabled \
+ user_subnet_list_type local_subnet_lists_enabled remote_subnet_lists_enabled
+
+ config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0
+ config_get user_domain_list_type "$section" "user_domain_list_type" "disabled"
+ config_get_bool local_domain_lists_enabled "$section" "local_domain_lists_enabled" 0
+ config_get_bool remote_domain_lists_enabled "$section" "remote_domain_lists_enabled" 0
+ config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled"
+ config_get_bool local_subnet_lists_enabled "$section" "local_subnet_lists_enabled" 0
+ config_get_bool remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" 0
+
+ if [ "$community_lists_enabled" -ne 0 ] ||
+ [ "$user_domain_list_type" != "disabled" ] ||
+ [ "$local_domain_lists_enabled" -ne 0 ] ||
+ [ "$remote_domain_lists_enabled" -ne 0 ] ||
+ [ "$user_subnet_list_type" != "disabled" ] ||
+ [ "$local_subnet_lists_enabled" -ne 0 ] ||
+ [ "$remote_subnet_lists_enabled" -ne 0 ]; then
+ return 0
+ else
+ return 1
+ fi
}
## nftables
nft_list_all_traffic_from_ip() {
local ip="$1"
- local table="PodkopTable"
- if ! nft list chain inet $table mangle | grep -q "ip saddr $ip"; then
- nft insert rule inet $table mangle iifname "$SRC_INTERFACE" ip saddr $ip meta l4proto { tcp, udp } meta mark set 0x105 counter
- nft insert rule inet $table mangle ip saddr $ip ip daddr @localv4 return
- fi
-}
-
-# Adds an IPv4 subnet to nftables firewall set
-nft_add_podkop_subnet() {
- local subnet="$1"
-
- if is_ipv4_cidr "$subnet"; then
- nft add element inet PodkopTable podkop_subnets { "$subnet" }
- else
- log "Invalid subnet format. $subnet is not a valid IPv4 CIDR"
+ if ! nft list chain inet "$NFT_TABLE_NAME" mangle | grep -q "ip saddr $ip"; then
+ nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" meta l4proto tcp meta mark set 0x105 counter
+ nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" meta l4proto udp meta mark set 0x105 counter
+ nft insert rule inet "$NFT_TABLE_NAME" mangle ip saddr "$ip" ip daddr @localv4 return
fi
}
# Diagnotics
check_proxy() {
- if ! command -v sing-box >/dev/null 2>&1; then
+ local sing_box_config_path
+ config_get sing_box_config_path "main" "config_path"
+
+ if ! command -v sing-box > /dev/null 2>&1; then
nolog "sing-box is not installed"
return 1
fi
- if [ ! -f $SING_BOX_CONFIG ]; then
+ if [ ! -f "$sing_box_config_path" ]; then
nolog "Configuration file not found"
return 1
fi
nolog "Checking sing-box configuration..."
- if ! sing-box -c $SING_BOX_CONFIG check >/dev/null; then
+ if ! sing-box -c "$sing_box_config_path" check > /dev/null; then
nolog "Invalid configuration"
return 1
fi
@@ -2170,48 +1484,47 @@ check_proxy() {
else . end
)
else . end
- )' $SING_BOX_CONFIG
+ )' "$sing_box_config_path"
nolog "Checking proxy connection..."
-
- for attempt in `seq 1 5`; do
- response=$(sing-box tools fetch ifconfig.me -D /etc/sing-box 2>/dev/null)
- if echo "$response" | grep -q "^ /dev/null)
+ if echo "$response" | grep -q "^/dev/null 2>&1; then
+ if ! command -v nft > /dev/null 2>&1; then
nolog "nft is not installed"
return 1
fi
- nolog "Checking PodkopTable rules..."
+ nolog "Checking $NFT_TABLE_NAME rules..."
# Check if table exists
- if ! nft list table inet PodkopTable >/dev/null 2>&1; then
- nolog "❌ PodkopTable not found"
+ if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
+ nolog "❌ $NFT_TABLE_NAME not found"
return 1
fi
@@ -2245,9 +1558,9 @@ check_nft() {
nolog "Sets statistics:"
for set_name in $sets; do
- if nft list set inet PodkopTable $set_name >/dev/null 2>&1; then
+ if nft list set inet "$NFT_TABLE_NAME" $set_name > /dev/null 2>&1; then
# Count elements using grep to count commas and add 1 (last element has no comma)
- local count=$(nft list set inet PodkopTable $set_name 2>/dev/null | grep -o ',\|{' | wc -l)
+ local count=$(nft list set inet "$NFT_TABLE_NAME" $set_name 2> /dev/null | grep -o ',\|{' | wc -l)
echo "- $set_name: $count elements"
fi
done
@@ -2256,7 +1569,7 @@ check_nft() {
# Create a temporary file for processing
local tmp_file=$(mktemp)
- nft list table inet PodkopTable > "$tmp_file"
+ nft list table inet "$NFT_TABLE_NAME" > "$tmp_file"
# Extract chain configurations without element listings
sed -n '/chain mangle {/,/}/p' "$tmp_file" | grep -v "elements" | grep -v "^[[:space:]]*[0-9]"
@@ -2267,7 +1580,7 @@ check_nft() {
else
# Simple view as originally implemented
nolog "Sets configuration:"
- nft list table inet PodkopTable
+ nft list table inet "$NFT_TABLE_NAME"
fi
nolog "NFT check completed"
@@ -2284,7 +1597,7 @@ check_github() {
nolog "Checking lists availability:"
for url in "$DOMAINS_RU_INSIDE" "$DOMAINS_RU_OUTSIDE" "$DOMAINS_UA" "$DOMAINS_YOUTUBE" \
- "$SUBNETS_TWITTER" "$SUBNETS_META" "$SUBNETS_DISCORD"; do
+ "$SUBNETS_TWITTER" "$SUBNETS_META" "$SUBNETS_DISCORD"; do
local list_name=$(basename "$url")
config_get_bool detour "main" "detour" "0"
@@ -2319,7 +1632,7 @@ check_dnsmasq() {
check_sing_box_connections() {
nolog "Checking sing-box connections..."
- if ! command -v netstat >/dev/null 2>&1; then
+ if ! command -v netstat > /dev/null 2>&1; then
nolog "netstat is not installed"
return 1
fi
@@ -2347,76 +1660,10 @@ check_sing_box_logs() {
echo "$logs"
}
-check_fakeip() {
- # Not used
- nolog "Checking fakeip functionality..."
-
- if ! command -v nslookup >/dev/null 2>&1; then
- nolog "nslookup is not installed"
- return 1
- fi
-
- local test_domain="$TEST_DOMAIN"
-
- nolog "Testing DNS resolution with default DNS server"
- echo "=== Testing with default DNS server ==="
- nslookup -timeout=2 $test_domain
- echo ""
-
- nolog "Finding a working DNS resolver..."
- local working_resolver=$(find_working_resolver)
- if [ -z "$working_resolver" ]; then
- nolog "No working resolver found, skipping resolver check"
- else
- nolog "Using resolver: $working_resolver"
-
- nolog "Testing DNS resolution with working resolver ($working_resolver)"
- echo "=== Testing with working resolver ($working_resolver) ==="
- nslookup -timeout=2 $test_domain $working_resolver
- echo ""
- fi
-
- # Main FakeIP check
- nolog "Testing DNS resolution for $test_domain using 127.0.0.42"
- echo "=== Testing with FakeIP DNS (127.0.0.42) ==="
- local result=$(nslookup -timeout=2 $test_domain 127.0.0.42 2>&1)
- echo "$result"
-
- if echo "$result" | grep -q "198.18"; then
- nolog "✅ FakeIP is working correctly! Domain resolved to FakeIP range (198.18.x.x)"
- return 0
- else
- nolog "❌ FakeIP test failed. Domain did not resolve to FakeIP range"
- nolog "Checking if sing-box is running..."
-
- if ! pgrep -f "sing-box" >/dev/null; then
- nolog "sing-box is not running"
- else
- nolog "sing-box is running, but FakeIP might not be configured correctly"
- nolog "Checking DNS configuration in sing-box..."
-
- if [ -f "$SING_BOX_CONFIG" ]; then
- local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SING_BOX_CONFIG")
- local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SING_BOX_CONFIG")
-
- nolog "FakeIP enabled: $fakeip_enabled"
- nolog "FakeIP range: $fakeip_range"
-
- local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SING_BOX_CONFIG")
- nolog "FakeIP domain: $dns_rules"
- else
- nolog "sing-box config file not found"
- fi
- fi
-
- return 1
- fi
-}
-
check_logs() {
nolog "Showing podkop logs from system journal..."
- if ! command -v logread >/dev/null 2>&1; then
+ if ! command -v logread > /dev/null 2>&1; then
nolog "Error: logread command not found"
return 1
fi
@@ -2437,9 +1684,11 @@ check_logs() {
}
show_sing_box_config() {
+ local sing_box_config_path
+ config_get sing_box_config_path "main" "config_path"
nolog "Current sing-box configuration:"
- if [ ! -f "$SING_BOX_CONFIG" ]; then
+ if [ ! -f "$sing_box_config_path" ]; then
nolog "Configuration file not found"
return 1
fi
@@ -2467,7 +1716,7 @@ show_sing_box_config() {
else . end
)
else . end
- )' "$SING_BOX_CONFIG"
+ )' "$sing_box_config_path"
}
show_config() {
@@ -2489,7 +1738,7 @@ show_config() {
-e 's/\(sid=[^&]*\)/sid=MASKED/g' \
-e 's/\(option dns_server '\''[^'\'']*\.dns\.nextdns\.io'\''\)/option dns_server '\''MASKED.dns.nextdns.io'\''/g' \
-e "s|\(option dns_server 'dns\.nextdns\.io\)/[^']*|\1/MASKED|"
- > "$tmp_config"
+ > "$tmp_config"
cat "$tmp_config"
rm -f "$tmp_config"
@@ -2531,13 +1780,13 @@ get_sing_box_status() {
fi
# Check if service is running
- if pgrep -f "sing-box" >/dev/null; then
+ if pgrep -f "sing-box" > /dev/null; then
running=1
version=$(sing-box version | head -n 1 | awk '{print $3}')
fi
# Check DNS configuration
- local dns_server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)
+ local dns_server=$(uci get dhcp.@dnsmasq[0].server 2> /dev/null)
if [ "$dns_server" = "127.0.0.42" ]; then
dns_configured=1
fi
@@ -2576,8 +1825,8 @@ get_status() {
}
check_dns_available() {
- local dns_type=$(uci get podkop.main.dns_type 2>/dev/null)
- local dns_server=$(uci get podkop.main.dns_server 2>/dev/null)
+ local dns_type=$(uci get podkop.main.dns_type 2> /dev/null)
+ local dns_server=$(uci get podkop.main.dns_server 2> /dev/null)
local is_available=0
local status="unavailable"
local local_dns_working=0
@@ -2595,37 +1844,37 @@ check_dns_available() {
if [ "$dns_type" = "doh" ]; then
# Generate random DNS query ID (2 bytes)
- local random_id=$(head -c2 /dev/urandom | hexdump -ve '1/1 "%.2x"' 2>/dev/null)
+ local random_id=$(head -c2 /dev/urandom | hexdump -ve '1/1 "%.2x"' 2> /dev/null)
if [ $? -ne 0 ]; then
error_message="Failed to generate random ID"
status="internal error"
else
# Create DNS wire format query for google.com A record with random ID
- local dns_query=$(printf "\x${random_id:0:2}\x${random_id:2:2}\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01" | base64 2>/dev/null)
+ local dns_query=$(printf "\x${random_id:0:2}\x${random_id:2:2}\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01" | base64 2> /dev/null)
if [ $? -ne 0 ]; then
error_message="Failed to generate DNS query"
status="internal error"
else
# Try POST method first (RFC 8484 compliant) with shorter timeout
- local result=$(echo "$dns_query" | base64 -d 2>/dev/null | curl -H "Content-Type: application/dns-message" \
+ local result=$(echo "$dns_query" | base64 -d 2> /dev/null | curl -H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @- \
--max-time 2 \
--connect-timeout 1 \
-s \
- "https://$dns_server/dns-query" 2>/dev/null)
+ "https://$dns_server/dns-query" 2> /dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
is_available=1
status="available"
else
# Try GET method as fallback with shorter timeout
- local dns_query_no_padding=$(echo "$dns_query" | tr -d '=' 2>/dev/null)
+ local dns_query_no_padding=$(echo "$dns_query" | tr -d '=' 2> /dev/null)
result=$(curl -H "accept: application/dns-message" \
--max-time 2 \
--connect-timeout 1 \
-s \
- "https://$dns_server/dns-query?dns=$dns_query_no_padding" 2>/dev/null)
+ "https://$dns_server/dns-query?dns=$dns_query_no_padding" 2> /dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
is_available=1
@@ -2637,24 +1886,25 @@ check_dns_available() {
fi
fi
elif [ "$dns_type" = "dot" ]; then
- (nc "$dns_server" 853 /dev/null 2>&1) & pid=$!
+ (nc "$dns_server" 853 < /dev/null > /dev/null 2>&1) &
+ pid=$!
sleep 2
- if kill -0 $pid 2>/dev/null; then
- kill $pid 2>/dev/null
- wait $pid 2>/dev/null
+ if kill -0 $pid 2> /dev/null; then
+ kill $pid 2> /dev/null
+ wait $pid 2> /dev/null
else
is_available=1
status="available"
fi
elif [ "$dns_type" = "udp" ]; then
- if nslookup -timeout=2 itdog.info $dns_server >/dev/null 2>&1; then
+ if nslookup -timeout=2 itdog.info $dns_server > /dev/null 2>&1; then
is_available=1
status="available"
fi
fi
# Check if local DNS resolver is working
- if nslookup -timeout=2 $TEST_DOMAIN 127.0.0.1 >/dev/null 2>&1; then
+ if nslookup -timeout=2 $FAKEIP_TEST_DOMAIN 127.0.0.1 > /dev/null 2>&1; then
local_dns_working=1
local_dns_status="available"
fi
@@ -2662,35 +1912,6 @@ check_dns_available() {
echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"is_available\":$is_available,\"status\":\"$status\",\"local_dns_working\":$local_dns_working,\"local_dns_status\":\"$local_dns_status\"}"
}
-sing_box_add_secure_dns_probe_domain() {
- local domain="$TEST_DOMAIN"
- local override_port=8443
-
- log "Adding DNS probe domain ${domain} to fakeip-server configuration"
-
- jq \
- --arg domain "$domain" \
- --argjson override_port "$override_port" \
- '.dns.rules |= map(
- if (.server == "fakeip-server" or (.server == "dns-server" and .invert == true)) then
- . + {
- "domain": $domain
- }
- else
- .
- end
- ) |
- .route.rules |= . + [
- {
- "domain": $domain,
- "action": "route-options",
- "override_port": $override_port
- }
- ]' "$SING_BOX_CONFIG" | build_sing_box_config
-
- log "DNS probe domain ${domain} configured with override to port ${override_port}"
-}
-
print_global() {
local message="$1"
echo "$message"
@@ -2721,21 +1942,21 @@ global_check() {
print_global "✅ /etc/resolv.conf"
fi
- cachesize="$(uci get dhcp.@dnsmasq[0].cachesize 2>/dev/null)"
- noresolv="$(uci get dhcp.@dnsmasq[0].noresolv 2>/dev/null)"
- server="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
+ cachesize="$(uci get dhcp.@dnsmasq[0].cachesize 2> /dev/null)"
+ noresolv="$(uci get dhcp.@dnsmasq[0].noresolv 2> /dev/null)"
+ server="$(uci get dhcp.@dnsmasq[0].server 2> /dev/null)"
if [ "$cachesize" != "0" ] || [ "$noresolv" != "1" ] || [ "$server" != "127.0.0.42" ]; then
print_global "❌ DHCP configuration differs from template. 📄 DHCP config:"
awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
- elif [ "$(uci get podkop.main.dont_touch_dhcp 2>/dev/null)" = "1" ]; then
+ elif [ "$(uci get podkop.main.dont_touch_dhcp 2> /dev/null)" = "1" ]; then
print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:"
awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
else
print_global "✅ /etc/config/dhcp"
fi
- if ! pgrep -f "sing-box" >/dev/null; then
+ if ! pgrep -f "sing-box" > /dev/null; then
print_global "❌ sing-box is not running"
else
print_global "✅ sing-box is running"
@@ -2747,7 +1968,7 @@ global_check() {
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "📄 WAN config"
- if uci show network.wan >/dev/null 2>&1; then
+ if uci show network.wan > /dev/null 2>&1; then
awk '
/^config / {
p = ($2 == "interface" && $3 == "'\''wan'\''")
@@ -2812,125 +2033,34 @@ global_check() {
print_global "🔁 FakeIP"
print_global "➡️ DNS resolution: system DNS server"
- nslookup -timeout=2 $TEST_DOMAIN
+ nslookup -timeout=2 $FAKEIP_TEST_DOMAIN
- local working_resolver=$(find_working_resolver)
+ local working_resolver
+ working_resolver=$(find_working_resolver)
if [ -z "$working_resolver" ]; then
print_global "❌ No working external resolver found"
else
print_global "➡️ DNS resolution: external resolver ($working_resolver)"
- nslookup -timeout=2 $TEST_DOMAIN $working_resolver
+ nslookup -timeout=2 $FAKEIP_TEST_DOMAIN $working_resolver
fi
print_global "➡️ DNS resolution: sing-box DNS server (127.0.0.42)"
- local result=$(nslookup -timeout=2 $TEST_DOMAIN 127.0.0.42 2>&1)
+ local result
+ result=$(nslookup -timeout=2 $FAKEIP_TEST_DOMAIN 127.0.0.42 2>&1)
echo "$result"
if echo "$result" | grep -q "198.18"; then
print_global "✅ FakeIP is working correctly on router (198.18.x.x)"
else
print_global "❌ FakeIP test failed: Domain did not resolve to FakeIP range"
- if ! pgrep -f "sing-box" >/dev/null; then
+ if ! pgrep -f "sing-box" > /dev/null; then
print_global " ❌ sing-box is not running"
else
- print_global " 🤔 sing-box is running, checking configuration"
-
- if [ -f "$SING_BOX_CONFIG" ]; then
- local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SING_BOX_CONFIG")
- local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SING_BOX_CONFIG")
- local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SING_BOX_CONFIG")
-
- print_global " 📦 FakeIP enabled: $fakeip_enabled"
- print_global " 📦 FakeIP range: $fakeip_range"
- print_global " 📦 FakeIP domain: $dns_rules"
- else
- print_global " ⛔ sing-box config file not found"
- fi
+ print_global " 🤔 sing-box is running"
fi
fi
}
-# Download URL content directly
-download_to_stream() {
- local url="$1"
-
- config_get_bool detour "main" "detour" "0"
- if [ "$detour" -eq 1 ]; then
- http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -qO- "$url" | sed 's/\r$//'
- else
- wget -qO- "$url" | sed 's/\r$//'
- fi
-}
-
-# Download URL to temporary file
-download_to_tempfile() {
- local url="$1"
- local filepath="$2"
-
- config_get_bool detour "main" "detour" "0"
- if [ "$detour" -eq 1 ]; then
- http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "$filepath" "$url"
- else
- wget -O "$filepath" "$url"
- fi
-
- if grep -q $'\r' "$filepath"; then
- log "$filename has Windows line endings (CRLF). Converting to Unix (LF)"
- sed -i 's/\r$//' "$filepath"
- fi
-}
-
-# helper function
-
-# check if file exists
-file_exists() {
- local filepath="$1"
-
- if [[ -f "$filepath" ]]; then
- return 0 # success
- else
- return 1 # failure
- fi
-}
-
-# extracts file extension from URL
-get_url_file_extension() {
- local url="$1"
-
- local file_extension="${url##*.}"
-
- echo "$file_extension"
-}
-
-# extracts file extension from URL
-get_ruleset_tag_from_url() {
- local url="$1"
- local prefix="${2:-}"
- local postfix="${3:-}"
-
- local filename="${url##*/}"
- local basename="${filename%%.*}"
-
- local tag="$basename"
-
- if [ -n "$prefix" ]; then
- tag="${prefix}-${tag}"
- fi
-
- if [ -n "$postfix" ]; then
- tag="${tag}-${postfix}"
- fi
-
- echo "$tag"
-}
-
-# check if string is valid IPv4 with CIDR mask
-is_ipv4_cidr() {
- local ip="$1"
- local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(\/(3[0-2]|2[0-9]|1[0-9]|[0-9]))$"
- [[ $ip =~ $regex ]]
-}
-
show_help() {
cat << EOF
Usage: $0 COMMAND
@@ -2950,7 +2080,6 @@ Available commands:
check_logs Show podkop logs from system journal
check_sing_box_connections Show active sing-box connections
check_sing_box_logs Show sing-box logs
- check_fakeip Check FakeIP DNS functionality
check_dnsmasq Check DNSMasq configuration
show_config Display current podkop configuration
show_version Show podkop version
@@ -2966,80 +2095,77 @@ EOF
}
case "$1" in
- start)
- start
- ;;
- stop)
- stop
- ;;
- reload)
- reload
- ;;
- restart)
- restart
- ;;
- main)
- main
- ;;
- list_update)
- list_update
- ;;
- check_proxy)
- check_proxy
- ;;
- check_nft)
- check_nft
- ;;
- check_github)
- check_github
- ;;
- check_logs)
- check_logs
- ;;
- check_sing_box_connections)
- check_sing_box_connections
- ;;
- check_sing_box_logs)
- check_sing_box_logs
- ;;
- check_fakeip)
- check_fakeip
- ;;
- check_dnsmasq)
- check_dnsmasq
- ;;
- show_config)
- show_config
- ;;
- show_version)
- show_version
- ;;
- show_sing_box_config)
- show_sing_box_config
- ;;
- show_luci_version)
- show_luci_version
- ;;
- show_sing_box_version)
- show_sing_box_version
- ;;
- show_system_info)
- show_system_info
- ;;
- get_status)
- get_status
- ;;
- get_sing_box_status)
- get_sing_box_status
- ;;
- check_dns_available)
- check_dns_available
- ;;
- global_check)
- global_check
- ;;
- *)
- show_help
- exit 1
- ;;
-esac
\ No newline at end of file
+start)
+ start
+ ;;
+stop)
+ stop
+ ;;
+reload)
+ reload
+ ;;
+restart)
+ restart
+ ;;
+main)
+ main
+ ;;
+list_update)
+ list_update
+ ;;
+check_proxy)
+ check_proxy
+ ;;
+check_nft)
+ check_nft
+ ;;
+check_github)
+ check_github
+ ;;
+check_logs)
+ check_logs
+ ;;
+check_sing_box_connections)
+ check_sing_box_connections
+ ;;
+check_sing_box_logs)
+ check_sing_box_logs
+ ;;
+check_dnsmasq)
+ check_dnsmasq
+ ;;
+show_config)
+ show_config
+ ;;
+show_version)
+ show_version
+ ;;
+show_sing_box_config)
+ show_sing_box_config
+ ;;
+show_luci_version)
+ show_luci_version
+ ;;
+show_sing_box_version)
+ show_sing_box_version
+ ;;
+show_system_info)
+ show_system_info
+ ;;
+get_status)
+ get_status
+ ;;
+get_sing_box_status)
+ get_sing_box_status
+ ;;
+check_dns_available)
+ check_dns_available
+ ;;
+global_check)
+ global_check
+ ;;
+*)
+ show_help
+ exit 1
+ ;;
+esac
diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh
new file mode 100644
index 0000000..eaa3572
--- /dev/null
+++ b/podkop/files/usr/lib/constants.sh
@@ -0,0 +1,66 @@
+# shellcheck disable=SC2034
+
+## Common
+PODKOP_CONFIG="/etc/config/podkop"
+RESOLV_CONF="/etc/resolv.conf"
+DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8"
+CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi"
+FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi"
+TMP_SING_BOX_FOLDER="/tmp/sing-box"
+TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets"
+CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker
+
+## nft
+NFT_TABLE_NAME="PodkopTable"
+NFT_LOCALV4_SET_NAME="localv4"
+NFT_COMMON_SET_NAME="podkop_subnets"
+NFT_DISCORD_SET_NAME="podkop_discord_subnets"
+NFT_INTERFACE_SET_NAME="interfaces"
+
+## sing-box
+# Log
+SB_DEFAULT_LOG_LEVEL="warn"
+# DNS
+SB_DNS_SERVER_TAG="dns-server"
+SB_SPLIT_DNS_SERVER_TAG="split-dns-server"
+SB_FAKEIP_DNS_SERVER_TAG="fakeip-server"
+SB_FAKEIP_INET4_RANGE="198.18.0.0/15"
+SB_DNS_DOMAIN_RESOLVER_TAG="dns-domain-resolver"
+SB_FAKEIP_DNS_RULE_TAG="fakeip-dns-rule-tag"
+SB_INVERT_FAKEIP_DNS_RULE_TAG="invert-fakeip-dns-rule-tag"
+# Inbounds
+SB_TPROXY_INBOUND_TAG="tproxy-in"
+SB_TPROXY_INBOUND_ADDRESS="127.0.0.1"
+SB_TPROXY_INBOUND_PORT=1602
+SB_DNS_INBOUND_TAG="dns-in"
+SB_DNS_INBOUND_ADDRESS="127.0.0.42"
+SB_DNS_INBOUND_PORT=53
+SB_MIXED_INBOUND_TAG="mixed-in"
+SB_MIXED_INBOUND_ADDRESS="0.0.0.0" # TODO(ampetelin): maybe to determine address?
+SB_MIXED_INBOUND_PORT=2080
+SB_SERVICE_MIXED_INBOUND_TAG="service-mixed-in"
+SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1"
+SB_SERVICE_MIXED_INBOUND_PORT=4534
+# Outbounds
+SB_DIRECT_OUTBOUND_TAG="direct-out"
+SB_MAIN_OUTBOUND_TAG="main-out"
+# Route
+SB_REJECT_RULE_TAG="reject-rule-tag"
+
+## Lists
+GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
+SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download"
+DOMAINS_RU_INSIDE="${GITHUB_RAW_URL}/Russia/inside-dnsmasq-nfset.lst"
+DOMAINS_RU_OUTSIDE="${GITHUB_RAW_URL}/Russia/outside-dnsmasq-nfset.lst"
+DOMAINS_UA="${GITHUB_RAW_URL}/Ukraine/inside-dnsmasq-nfset.lst"
+DOMAINS_YOUTUBE="${GITHUB_RAW_URL}/Services/youtube.lst"
+SUBNETS_TWITTER="${GITHUB_RAW_URL}/Subnets/IPv4/twitter.lst"
+SUBNETS_META="${GITHUB_RAW_URL}/Subnets/IPv4/meta.lst"
+SUBNETS_DISCORD="${GITHUB_RAW_URL}/Subnets/IPv4/discord.lst"
+SUBNETS_TELERAM="${GITHUB_RAW_URL}/Subnets/IPv4/telegram.lst"
+SUBNETS_CLOUDFLARE="${GITHUB_RAW_URL}/Subnets/IPv4/cloudflare.lst"
+SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst"
+SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst"
+SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst"
+SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst"
+VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube discord meta twitter hdrezka tiktok telegram cloudflare google_ai google_play hetzner ovh hodca digitalocean cloudfront"
\ No newline at end of file
diff --git a/podkop/files/usr/lib/helpers.jq b/podkop/files/usr/lib/helpers.jq
new file mode 100644
index 0000000..d914feb
--- /dev/null
+++ b/podkop/files/usr/lib/helpers.jq
@@ -0,0 +1,14 @@
+def extend_key_value(current_value; new_value):
+ if (current_value | type) == "array" then
+ if (new_value | type) == "array" then
+ current_value + new_value
+ else
+ current_value + [new_value]
+ end
+ else
+ if (new_value | type) == "array" then
+ [current_value] + new_value
+ else
+ [current_value, new_value]
+ end
+ end;
\ No newline at end of file
diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh
new file mode 100644
index 0000000..2795cb4
--- /dev/null
+++ b/podkop/files/usr/lib/helpers.sh
@@ -0,0 +1,369 @@
+# Check if string is valid IPv4
+is_ipv4() {
+ local ip="$1"
+ local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"
+ [[ "$ip" =~ $regex ]]
+}
+
+# Check if string is valid IPv4 with CIDR mask
+is_ipv4_cidr() {
+ local ip="$1"
+ local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(\/(3[0-2]|2[0-9]|1[0-9]|[0-9]))$"
+ [[ "$ip" =~ $regex ]]
+}
+
+is_ipv4_ip_or_ipv4_cidr() {
+ is_ipv4 "$1" || is_ipv4_cidr "$1"
+}
+
+# Check if string is valid domain
+is_domain() {
+ local str="$1"
+ echo "$str" | grep -Eq '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9]))+$'
+}
+
+# Checks if the given string is a valid base64-encoded sequence
+is_base64() {
+ local str="$1"
+
+ if echo "$str" | base64 -d > /dev/null 2>&1; then
+ return 0
+ fi
+ return 1
+}
+
+# Checks if the given file exists
+file_exists() {
+ local filepath="$1"
+
+ if [[ -f "$filepath" ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# Returns the inbound tag name by appending the postfix to the given section
+get_inbound_tag_by_section() {
+ local section="$1"
+ local postfix="in"
+
+ echo "$section-$postfix"
+}
+
+# Returns the outbound tag name by appending the postfix to the given section
+get_outbound_tag_by_section() {
+ local section="$1"
+ local postfix="out"
+
+ echo "$section-$postfix"
+}
+
+# Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix
+get_ruleset_tag() {
+ local section="$1"
+ local name="$2"
+ local type="$3"
+ local postfix="ruleset"
+
+ if [ -n "$type" ]; then
+ echo "$section-$name-$type-$postfix"
+ else
+ echo "$section-$name-$postfix"
+ fi
+}
+
+# Determines the ruleset format based on the file extension (json → source, srs → binary)
+get_ruleset_format_by_file_extension() {
+ local file_extension="$1"
+
+ local format
+ case "$file_extension" in
+ json) format="source" ;;
+ srs) format="binary" ;;
+ *)
+ log "Unsupported file extension: .$file_extension"
+ return 1
+ ;;
+ esac
+
+ echo "$format"
+}
+
+# Converts a comma-separated string into a JSON array string
+comma_string_to_json_array() {
+ local input="$1"
+
+ if [ -z "$input" ]; then
+ echo "[]"
+ return
+ fi
+
+ local replaced="${input//,/\",\"}"
+
+ echo "[\"$replaced\"]"
+}
+
+# Decodes a URL-encoded string
+url_decode() {
+ local encoded="$1"
+ printf '%b' "$(echo "$encoded" | sed 's/+/ /g; s/%/\\x/g')"
+}
+
+# Extracts the userinfo (username[:password]) part from a URL
+url_get_userinfo() {
+ local url="$1"
+ echo "$url" | sed -n -e 's#^[^:/?]*://##' -e '/@/!d' -e 's/@.*//p'
+}
+
+# Extracts the host part from a URL
+url_get_host() {
+ local url="$1"
+ echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#\([:/].*\|$\)##p'
+}
+
+# Extracts the port number from a URL
+url_get_port() {
+ local url="$1"
+ echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#^[^/]*:\([0-9][0-9]*\).*#\1#p'
+}
+
+# Extracts the path from a URL (without query or fragment; returns "/" if empty)
+url_get_path() {
+ local url="$1"
+ echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*##' -e 's#\([^?]*\).*#\1#p'
+}
+
+# Extracts the value of a specific query parameter from a URL
+url_get_query_param() {
+ local url="$1"
+ local param="$2"
+
+ local raw
+ raw=$(echo "$url" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
+
+ [ -z "$raw" ] && echo "" && return
+
+ echo "$raw"
+}
+
+# Extracts the basename (filename without extension) from a URL
+url_get_basename() {
+ local url="$1"
+
+ local filename="${url##*/}"
+ local basename="${filename%%.*}"
+
+ echo "$basename"
+}
+
+# Extracts and returns the file extension from the given URL
+url_get_file_extension() {
+ local url="$1"
+
+ local basename="${url##*/}"
+ case "$basename" in
+ *.*) echo "${basename##*.}" ;;
+ *) echo "" ;;
+ esac
+}
+
+# Decodes and returns a base64-encoded string
+base64_decode() {
+ local str="$1"
+ local decoded_url
+
+ decoded_url="$(echo "$str" | base64 -d 2> /dev/null)"
+
+ echo "$decoded_url"
+}
+
+# Generates a unique 16-character ID based on the current timestamp and a random number
+gen_id() {
+ printf '%s%s' "$(date +%s)" "$RANDOM" | md5sum | cut -c1-16
+}
+
+# Adds a missing UCI option with the given value if it does not exist
+migration_add_new_option() {
+ local package="$1"
+ local section="$2"
+ local option="$3"
+ local value="$4"
+
+ local current
+ current="$(uci -q get "$package.$section.$option")"
+ if [ -z "$current" ]; then
+ log "Adding missing option '$option' with value '$value'"
+ uci set "$package.$section.$option=$value"
+ uci commit "$package"
+ return 0
+ else
+ return 1
+ fi
+}
+
+# Migrates a configuration key in an OpenWrt config file from old_key_name to new_key_name
+migration_rename_config_key() {
+ local config="$1"
+ local key_type="$2"
+ local old_key_name="$3"
+ local new_key_name="$4"
+
+ if grep -q "$key_type $old_key_name" "$config"; then
+ log "Deprecated $key_type found: $old_key_name migrating to $new_key_name"
+ sed -i "s/$key_type $old_key_name/$key_type $new_key_name/g" "$config"
+ fi
+}
+
+# Download URL content directly
+download_to_stream() {
+ local url="$1"
+ local http_proxy_address="$2"
+ local retries="${3:-3}"
+ local wait="${4:-2}"
+
+ for attempt in $(seq 1 "$retries"); do
+ if [ -n "$http_proxy_address" ]; then
+ http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -qO- "$url" | sed 's/\r$//' && break
+ else
+ wget -qO- "$url" | sed 's/\r$//' && break
+ fi
+
+ log "Attempt $attempt/$retries to download $url failed" "warn"
+ sleep "$wait"
+ done
+}
+
+# Download URL to file
+download_to_file() {
+ local url="$1"
+ local filepath="$2"
+ local http_proxy_address="$3"
+ local retries="${4:-3}"
+ local wait="${5:-2}"
+
+ for attempt in $(seq 1 "$retries"); do
+ if [ -n "$http_proxy_address" ]; then
+ http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -O "$filepath" "$url" && break
+ else
+ wget -O "$filepath" "$url" && break
+ fi
+
+ log "Attempt $attempt/$retries to download $url failed" "warn"
+ sleep "$wait"
+ done
+
+ if grep -q $'\r' "$filepath"; then
+ log "Downloaded file has Windows line endings (CRLF). Converting to Unix (LF)"
+ sed -i 's/\r$//' "$filepath"
+ fi
+}
+
+# Decompiles a sing-box SRS binary file into a JSON ruleset file
+decompile_srs_file() {
+ local binary_filepath="$1"
+ local output_filepath="$2"
+
+ log "Decompiling $binary_filepath to $output_filepath" "debug"
+
+ if ! file_exists "$binary_filepath"; then
+ log "File $binary_filepath not found" "error"
+ return 1
+ fi
+
+ sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
+ if [[ $? -ne 0 ]]; then
+ log "Decompilation command failed for $binary_filepath" "error"
+ return 1
+ fi
+}
+
+#######################################
+# Parses a whitespace-separated string, validates items as either domains
+# or IPv4 addresses/subnets, and returns a comma-separated string of valid items.
+# Arguments:
+# $1 - Input string (space-separated list of items)
+# $2 - Type of validation ("domains" or "subnets")
+# Outputs:
+# Comma-separated string of valid domains or subnets
+#######################################
+parse_domain_or_subnet_string_to_commas_string() {
+ local string="$1"
+ local type="$2"
+
+ local result
+ for item in $string; do
+ case "$type" in
+ domains)
+ if ! is_domain "$item"; then
+ log "'$item' is not a valid domain" "debug"
+ continue
+ fi
+ ;;
+ subnets)
+ if ! is_ipv4_ip_or_ipv4_cidr "$item"; then
+ log "'$item' is not IPv4 or IPv4 CIDR" "debug"
+ continue
+ fi
+ ;;
+ *)
+ log "Unknown type: $type" "error"
+ return 1
+ ;;
+ esac
+
+ if [ -z "$result" ]; then
+ result="$item"
+ else
+ result="$result,$item"
+ fi
+ done
+
+ echo "$result"
+}
+
+#######################################
+# Parses a file line by line, validates entries as either domains or subnets,
+# and returns a single comma-separated string of valid items.
+# Arguments:
+# $1 - Path to the input file
+# $2 - Type of validation ("domains" or "subnets")
+# Outputs:
+# Comma-separated string of valid domains or subnets
+#######################################
+parse_domain_or_subnet_file_to_comma_string() {
+ local filepath="$1"
+ local type="$2"
+
+ local result
+ while IFS= read -r line; do
+ [ -z "$line" ] && continue
+
+ case "$type" in
+ domains)
+ if ! is_domain "$line"; then
+ log "'$line' is not a valid domain" "debug"
+ continue
+ fi
+ ;;
+ subnets)
+ if ! is_ipv4 "$line" && ! is_ipv4_cidr "$line"; then
+ log "'$line' is not IPv4 or IPv4 CIDR" "debug"
+ continue
+ fi
+ ;;
+ *)
+ log "Unknown type: $type" "error"
+ return 1
+ ;;
+ esac
+
+ if [ -z "$result" ]; then
+ result="$line"
+ else
+ result="$result,$line"
+ fi
+ done < "$filepath"
+
+ echo "$result"
+}
diff --git a/podkop/files/usr/lib/logging.sh b/podkop/files/usr/lib/logging.sh
new file mode 100644
index 0000000..6a3ad6c
--- /dev/null
+++ b/podkop/files/usr/lib/logging.sh
@@ -0,0 +1,30 @@
+COLOR_CYAN="\033[0;36m"
+COLOR_GREEN="\033[0;32m"
+COLOR_RESET="\033[0m"
+
+log() {
+ local message="$1"
+ local level="$2"
+
+ if [ "$level" == "" ]; then
+ level="info"
+ fi
+
+ logger -t "podkop" "[$level] $message"
+}
+
+nolog() {
+ local message="$1"
+ local timestamp
+ timestamp=$(date +"%Y-%m-%d %H:%M:%S")
+
+ echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
+}
+
+echolog() {
+ local message="$1"
+ local level="$2"
+
+ log "$message" "$level"
+ nolog "$message"
+}
diff --git a/podkop/files/usr/lib/nft.sh b/podkop/files/usr/lib/nft.sh
new file mode 100644
index 0000000..3a2a3c2
--- /dev/null
+++ b/podkop/files/usr/lib/nft.sh
@@ -0,0 +1,30 @@
+# Create an nftables table in the inet family
+nft_create_table() {
+ local name="$1"
+
+ nft add table inet "$name"
+}
+
+# Create a set within a table for storing IPv4 addresses
+nft_create_ipv4_set() {
+ local table="$1"
+ local name="$2"
+
+ nft add set inet "$table" "$name" '{ type ipv4_addr; flags interval; auto-merge; }'
+}
+
+nft_create_ifname_set() {
+ local table="$1"
+ local name="$2"
+
+ nft add set inet "$table" "$name" '{ type ifname; flags interval; }'
+}
+
+# Add one or more elements to a set
+nft_add_set_elements() {
+ local table="$1"
+ local set="$2"
+ local elements="$3"
+
+ nft add element inet "$table" "$set" "{ $elements }"
+}
\ No newline at end of file
diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh
new file mode 100644
index 0000000..657ca7e
--- /dev/null
+++ b/podkop/files/usr/lib/sing_box_config_facade.sh
@@ -0,0 +1,228 @@
+PODKOP_LIB="/usr/lib/podkop"
+. "$PODKOP_LIB/helpers.sh"
+. "$PODKOP_LIB/sing_box_config_manager.sh"
+
+sing_box_cf_add_dns_server() {
+ local config="$1"
+ local type="$2"
+ local tag="$3"
+ local server="$4"
+ local domain_resolver="$5"
+ local detour="$6"
+
+ local server_address server_port
+ server_address=$(url_get_host "$server")
+ server_port=$(url_get_port "$server")
+
+ case "$type" in
+ udp)
+ [ -z "$server_port" ] && server_port=53
+ config=$(sing_box_cm_add_udp_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \
+ "$detour")
+ ;;
+ dot)
+ [ -z "$server_port" ] && server_port=853
+ config=$(sing_box_cm_add_tls_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \
+ "$detour")
+ ;;
+ doh)
+ [ -z "$server_port" ] && server_port=443
+ local path headers
+ path=$(url_get_path "$server")
+ headers="" # TODO(ampetelin): implement it if necessary
+ config=$(sing_box_cm_add_https_dns_server "$config" "$tag" "$server_address" "$server_port" "$path" "$headers" \
+ "$domain_resolver" "$detour")
+ ;;
+ *)
+ log "Unsupported DNS server type: $type"
+ exit 1
+ ;;
+ esac
+
+ echo "$config"
+}
+
+sing_box_cf_add_mixed_inbound_and_route_rule() {
+ local config="$1"
+ local tag="$2"
+ local listen_address="$3"
+ local listen_port="$4"
+ local outbound="$5"
+
+ config=$(sing_box_cm_add_mixed_inbound "$config" "$tag" "$listen_address" "$listen_port")
+ config=$(sing_box_cm_add_route_rule "$config" "" "$tag" "$outbound")
+
+ echo "$config"
+}
+
+sing_box_cf_add_proxy_outbound() {
+ local config="$1"
+ local section="$2"
+ local url="$3"
+ local udp_over_tcp="$4"
+
+ url=$(url_decode "$url")
+
+ local scheme="${url%%://*}"
+ case "$scheme" in
+ vless)
+ local tag host port uuid flow packet_encoding
+ tag=$(get_outbound_tag_by_section "$section")
+ host=$(url_get_host "$url")
+ port=$(url_get_port "$url")
+ uuid=$(url_get_userinfo "$url")
+ flow=$(url_get_query_param "$url" "flow")
+ packet_encoding=$(url_get_query_param "$url" "packetEncoding")
+
+ config=$(sing_box_cm_add_vless_outbound "$config" "$tag" "$host" "$port" "$uuid" "$flow" "" "$packet_encoding")
+
+ local transport
+ transport=$(url_get_query_param "$url" "type")
+ case "$transport" in
+ tcp | raw) ;;
+ ws)
+ local ws_path ws_host ws_early_data
+ ws_path=$(url_get_query_param "$url" "path")
+ ws_host=$(url_get_query_param "$url" "host")
+ ws_early_data=$(url_get_query_param "$url" "ed")
+
+ config=$(sing_box_cm_set_vless_ws_transport "$config" "$tag" "$ws_path" "$ws_host" "$ws_early_data")
+ ;;
+ grpc)
+ # TODO(ampetelin): Add handling of optional gRPC parameters; example links are needed.
+ config=$(sing_box_cm_set_vless_grpc_transport "$config" "$tag")
+ ;;
+ *)
+ log "Unknown transport '$transport' detected." "error"
+ ;;
+ esac
+
+ local security
+ security=$(url_get_query_param "$url" "security")
+ case "$security" in
+ tls | reality)
+ local sni insecure alpn fingerprint public_key short_id
+ sni=$(url_get_query_param "$url" "sni")
+ insecure=$(url_get_query_param "$url" "allowInsecure")
+ alpn=$(comma_string_to_json_array "$(url_get_query_param "$url" "alpn")")
+ fingerprint=$(url_get_query_param "$url" "fp")
+ public_key=$(url_get_query_param "$url" "pbk")
+ short_id=$(url_get_query_param "$url" "sid")
+
+ config=$(
+ sing_box_cm_set_vless_tls \
+ "$config" \
+ "$tag" \
+ "$sni" \
+ "$([ "$insecure" == "1" ] && echo true)" \
+ "$([ "$alpn" == "[]" ] && echo null || echo "$alpn")" \
+ "$fingerprint" \
+ "$public_key" \
+ "$short_id"
+ )
+ ;;
+ none) ;;
+ *)
+ log "Unknown security '$security' detected." "error"
+ ;;
+ esac
+ ;;
+ ss)
+ local userinfo tag host port method password udp_over_tcp
+
+ userinfo=$(url_get_userinfo "$url")
+ if is_base64 "$userinfo"; then
+ userinfo=$(base64_decode "$userinfo")
+ fi
+
+ tag=$(get_outbound_tag_by_section "$section")
+ host=$(url_get_host "$url")
+ port=$(url_get_port "$url")
+ method="${userinfo%%:*}"
+ password="${userinfo#*:}"
+
+ config=$(
+ sing_box_cm_add_shadowsocks_outbound \
+ "$config" \
+ "$tag" \
+ "$host" \
+ "$port" \
+ "$method" \
+ "$password" \
+ "" \
+ "$([ "$udp_over_tcp" == "1" ] && echo 2)" # if udp_over_tcp is enabled, enable version 2
+ )
+ ;;
+ *)
+ log "Unsupported proxy $scheme type"
+ exit 1
+ ;;
+ esac
+
+ echo "$config"
+}
+
+sing_box_cf_add_json_outbound() {
+ local config="$1"
+ local section="$2"
+ local json_outbound="$3"
+
+ local tag
+ tag=$(get_outbound_tag_by_section "$section")
+
+ config=$(sing_box_cm_add_raw_outbound "$config" "$tag" "$json_outbound")
+
+ echo "$config"
+}
+
+sing_box_cf_add_interface_outbound() {
+ local config="$1"
+ local section="$2"
+ local interface_name="$3"
+
+ local tag
+ tag=$(get_outbound_tag_by_section "$section")
+
+ config=$(sing_box_cm_add_interface_outbound "$config" "$tag" "$interface_name")
+
+ echo "$config"
+}
+
+sing_box_cf_proxy_domain() {
+ local config="$1"
+ local inbound="$2"
+ local domain="$3"
+ local outbound="$4"
+
+ tag="$(gen_id)"
+ config=$(sing_box_cm_add_route_rule "$config" "$tag" "$inbound" "$outbound")
+ config=$(sing_box_cm_patch_route_rule "$config" "$tag" "domain" "$domain")
+
+ echo "$config"
+}
+
+sing_box_cf_override_domain_port() {
+ local config="$1"
+ local domain="$2"
+ local port="$3"
+
+ tag="$(gen_id)"
+ config=$(sing_box_cm_add_options_route_rule "$config" "$tag")
+ config=$(sing_box_cm_patch_route_rule "$config" "$tag" "domain" "$domain")
+ config=$(sing_box_cm_patch_route_rule "$config" "$tag" "override_port" "$port")
+
+ echo "$config"
+}
+
+sing_box_cf_add_single_key_reject_rule() {
+ local config="$1"
+ local inbound="$2"
+ local key="$3"
+ local value="$4"
+
+ tag="$(gen_id)"
+ config=$(sing_box_cm_add_reject_route_rule "$config" "$tag" "$inbound")
+ config=$(sing_box_cm_patch_route_rule "$config" "$tag" "$key" "$value")
+
+ echo "$config"
+}
diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh
new file mode 100644
index 0000000..3cf8e46
--- /dev/null
+++ b/podkop/files/usr/lib/sing_box_config_manager.sh
@@ -0,0 +1,1301 @@
+#
+# Module: sing_box_config_manager.sh
+#
+# Purpose:
+# This script provides a set of helper functions to simplify creation and management
+# of sing-box configuration.
+#
+# Conventions:
+# - All functions are prefixed with: sing_box_cm_*
+# - "cm" stands for "config manager", shortened by convention
+#
+# Usage:
+# Include this script in your ash script with:
+# . /usr/lib/podkop/sing_box_config_manager.sh
+#
+# After that, sing_box_cm_* functions are available for generating
+# and modifying sing-box JSON configuration.
+
+SERVICE_TAG="__service_tag"
+
+#######################################
+# Configure the logging section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# disabled: boolean, true to disable logging
+# level: string, e.g., "info", "debug", "warn"
+# timestamp: boolean, true to include timestamps
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_configure_log "$CONFIG" false "info" true)
+#######################################
+sing_box_cm_configure_log() {
+ local config="$1"
+ local disabled="$2"
+ local level="$3"
+ local timestamp="$4"
+
+ echo "$config" | jq \
+ --argjson disabled "$disabled" \
+ --arg level "$level" \
+ --argjson timestamp "$timestamp" \
+ '.log = {
+ disabled: $disabled,
+ level: $level,
+ timestamp: $timestamp
+ }'
+}
+
+#######################################
+# Configure the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# final: string, default dns server tag
+# strategy: string, default domain strategy for resolving the domain names
+# independent_cache: boolean, whether to use an independent DNS cache
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_configure_dns "$CONFIG" "direct-out" "ipv4_only" true)
+#######################################
+sing_box_cm_configure_dns() {
+ local config="$1"
+ local final="$2"
+ local strategy="$3"
+ local independent_cache="$4"
+
+ echo "$config" | jq \
+ --arg final "$final" \
+ --arg strategy "$strategy" \
+ --argjson independent_cache "$independent_cache" \
+ '.dns = {
+ servers: (.dns.servers // []),
+ rules: (.dns.rules // []),
+ final: $final,
+ strategy: $strategy,
+ independent_cache: $independent_cache
+ }'
+
+}
+
+#######################################
+# Add a UDP DNS server to the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the DNS server
+# server_address: string, IP address or hostname of the DNS server
+# server_port: string or number, port of the DNS server
+# domain_resolver: string, domain resolver to use for resolving domain names
+# detour: string, tag of the upstream outbound
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_udp_dns_server "$CONFIG" "udp-server" "8.8.8.8" 53)
+#######################################
+sing_box_cm_add_udp_dns_server() {
+ local config="$1"
+ local tag="$2"
+ local server_address="$3"
+ local server_port="$4"
+ local domain_resolver="$5"
+ local detour="$6"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_address "$server_address" \
+ --arg server_port "$server_port" \
+ --arg domain_resolver "$domain_resolver" \
+ --arg detour "$detour" \
+ '.dns.servers += [(
+ {
+ type: "udp",
+ tag: $tag,
+ server: $server_address,
+ server_port: ($server_port | tonumber)
+ }
+ + (if $detour != "" then { detour: $detour } else {} end)
+ + (if $domain_resolver != "" then { domain_resolver: $domain_resolver } else {} end)
+ )]'
+}
+
+#######################################
+# Add a TLS DNS server to the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the DNS server
+# server_address: string, IP address or hostname of the DNS server
+# server_port: string or number, port of the DNS server
+# domain_resolver: string, domain resolver to use for resolving domain names
+# detour: string, tag of the upstream outbound
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_tls_dns_server "$CONFIG" "dot-server" "1.1.1.1" 853)
+#######################################
+sing_box_cm_add_tls_dns_server() {
+ local config="$1"
+ local tag="$2"
+ local server_address="$3"
+ local server_port="$4"
+ local domain_resolver="$5"
+ local detour="$6"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_address "$server_address" \
+ --arg server_port "$server_port" \
+ --arg domain_resolver "$domain_resolver" \
+ --arg detour "$detour" \
+ '.dns.servers += [(
+ {
+ type: "tls",
+ tag: $tag,
+ server: $server_address,
+ server_port: ($server_port | tonumber)
+ }
+ + (if $detour != "" then { detour: $detour } else {} end)
+ + (if $domain_resolver != "" then { domain_resolver: $domain_resolver } else {} end)
+ )]'
+}
+
+#######################################
+# Add an HTTPS DNS server to the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the DNS server
+# server_address: string, IP address or hostname of the DNS server
+# server_port: string or number, port of the DNS server
+# path: string, optional URL path for HTTPS DNS requests
+# headers: string, optional additional headers for HTTPS DNS requests
+# domain_resolver: string, domain resolver to use for resolving domain names
+# detour: string, tag of the upstream outbound
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_https_dns_server "$CONFIG" "doh-server" "1.1.1.1" 443)
+#######################################
+sing_box_cm_add_https_dns_server() {
+ local config="$1"
+ local tag="$2"
+ local server_address="$3"
+ local server_port="$4"
+ local path="$5"
+ local headers="$6"
+ local domain_resolver="$7"
+ local detour="$8"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_address "$server_address" \
+ --arg server_port "$server_port" \
+ --arg path "$path" \
+ --arg headers "$headers" \
+ --arg domain_resolver "$domain_resolver" \
+ --arg detour "$detour" \
+ '.dns.servers += [(
+ {
+ type: "https",
+ tag: $tag,
+ server: $server_address,
+ server_port: ($server_port |tonumber)
+ }
+ + (if $path != "" then { path: $path } else {} end)
+ + (if $headers != "" then { headers: $headers } else {} end)
+ + (if $detour != "" then { detour: $detour } else {} end)
+ + (if $domain_resolver != "" then { domain_resolver: $domain_resolver } else {} end)
+ )]'
+}
+
+#######################################
+# Add a FakeIP DNS server to the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the DNS server
+# inet4_range: string, IPv4 range used for fake IP mapping
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_fakeip_dns_server "$CONFIG" "fakeip-server" "198.18.0.0/15")
+#######################################
+sing_box_cm_add_fakeip_dns_server() {
+ local config="$1"
+ local tag="$2"
+ local inet4_range="$3"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg inet4_range "$inet4_range" \
+ '.dns.servers += [{
+ type: "fakeip",
+ tag: $tag,
+ inet4_range: $inet4_range,
+ }]'
+}
+
+#######################################
+# Add a DNS routing rule to the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# server: string, target DNS server for the rule
+# tag: string, identifier for the route rule
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_dns_route_rule "$CONFIG" "fakeip-server" "fakeip-dns-rule-id")
+#######################################
+sing_box_cm_add_dns_route_rule() {
+ local config="$1"
+ local server="$2"
+ local tag="$3"
+
+ echo "$config" | jq \
+ --arg server "$server" \
+ --arg service_tag "$SERVICE_TAG" \
+ --arg tag "$tag" \
+ '.dns.rules += [{
+ action: "route",
+ server: $server,
+ $service_tag: $tag
+ }]'
+}
+
+#######################################
+# Patch a DNS routing rule in the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier of the rule to patch
+# key: string, the key in the rule to update or add
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_patch_dns_route_rule "$CONFIG" "fakeip-dns-rule-id" "rule_set" "main")
+# CONFIG=$(sing_box_cm_patch_dns_route_rule "$CONFIG" "fakeip-dns-rule-id" "rule_set" '["main","second"]')
+# CONFIG=$(sing_box_cm_patch_dns_route_rule "$CONFIG" "fakeip-dns-rule-id" "domain" "example.com")
+#######################################
+sing_box_cm_patch_dns_route_rule() {
+ local config="$1"
+ local tag="$2"
+ local key="$3"
+ local value="$4"
+
+ value=$(_normalize_arg "$value")
+
+ echo "$config" | jq \
+ --arg service_tag "$SERVICE_TAG" \
+ --arg tag "$tag" \
+ --arg key "$key" \
+ --argjson value "$value" \
+ 'import "helpers" as h {"search": "/usr/lib/podkop"};
+ .dns.rules |= map(
+ if .[$service_tag] == $tag then
+ if has($key) then
+ .[$key] |= h::extend_key_value(.; $value)
+ else
+ . + { ($key): $value }
+ end
+ else
+ .
+ end
+ )'
+}
+
+#######################################
+# Add a DNS reject rule to the DNS section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# key: string, the key to set for the reject rule
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_dns_reject_rule "$CONFIG" "query_type" "HTTPS")
+#######################################
+sing_box_cm_add_dns_reject_rule() {
+ local config="$1"
+ local key="$2"
+ local value="$3"
+
+ value=$(_normalize_arg "$value")
+
+ echo "$config" | jq \
+ --arg key "$key" \
+ --argjson value "$value" \
+ '.dns.rules += [{
+ action: "reject",
+ ($key): $value
+ }]'
+}
+
+#######################################
+# Add a TProxy inbound to the inbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the inbound
+# listen_address: string, IP address to listen on
+# listen_port: number, port to listen on
+# tcp_fast_open: boolean, enable or disable TCP Fast Open
+# udp_fragment: boolean, enable or disable UDP fragmentation
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_tproxy_inbound "$CONFIG" "tproxy-in" "127.0.0.1" 6969 true true)
+#######################################
+sing_box_cm_add_tproxy_inbound() {
+ local config="$1"
+ local tag="$2"
+ local listen_address="$3"
+ local listen_port="$4"
+ local tcp_fast_open="$5"
+ local udp_fragment="$6"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg listen_address "$listen_address" \
+ --argjson listen_port "$listen_port" \
+ --argjson tcp_fast_open "$tcp_fast_open" \
+ --argjson udp_fragment "$udp_fragment" \
+ '.inbounds += [{
+ type: "tproxy",
+ tag: $tag,
+ listen: $listen_address,
+ listen_port: $listen_port,
+ tcp_fast_open: $tcp_fast_open,
+ udp_fragment: $udp_fragment
+ }]'
+}
+
+#######################################
+# Add a Direct inbound to the inbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the inbound
+# listen_address: string, IP address to listen on
+# listen_port: number, port to listen on
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_direct_inbound "$CONFIG" "dns-in" "127.0.0.42" 53)
+#######################################
+sing_box_cm_add_direct_inbound() {
+ local config="$1"
+ local tag="$2"
+ local listen_address="$3"
+ local listen_port="$4"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg listen_address "$listen_address" \
+ --argjson listen_port "$listen_port" \
+ '.inbounds += [{
+ type: "direct",
+ tag: $tag,
+ listen: $listen_address,
+ listen_port: $listen_port,
+ }]'
+}
+
+#######################################
+# Add a Mixed inbound to the inbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the inbound
+# listen_address: string, IP address to listen on
+# listen_port: number, port to listen on
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_mixed_inbound "$CONFIG" "tproxy-in" "192.168.1.1" 2080)
+#######################################
+sing_box_cm_add_mixed_inbound() {
+ local config="$1"
+ local tag="$2"
+ local listen_address="$3"
+ local listen_port="$4"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg listen_address "$listen_address" \
+ --argjson listen_port "$listen_port" \
+ '.inbounds += [{
+ type: "mixed",
+ tag: $tag,
+ listen: $listen_address,
+ listen_port: $listen_port,
+ }]'
+}
+
+#######################################
+# Add a Direct outbound to the outbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the outbound
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_direct_outbound "$CONFIG" "direct-out")
+#######################################
+sing_box_cm_add_direct_outbound() {
+ local config="$1"
+ local tag="$2"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ '.outbounds += [{
+ type: "direct",
+ tag: $tag
+ }]'
+}
+
+#######################################
+# Add a SOCKS5 outbound to the outbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the outbound
+# server_address: string, IP address or hostname of the SOCKS5 server
+# server_port: number, port of the SOCKS5 server
+# version: string, optional SOCKS version
+# username: string, optional username for authentication
+# password: string, optional password for authentication
+# network: string, optional network type (e.g., "tcp")
+# udp_over_tcp: number, optional version for UDP over TCP
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_socks5_outbound "$CONFIG" "socks5-out" "192.168.1.10" 1080)
+#######################################
+sing_box_cm_add_socks5_outbound() {
+ local config="$1"
+ local tag="$2"
+ local server_address="$3"
+ local server_port="$4"
+ local version="$5"
+ local username="$6"
+ local password="$7"
+ local network="$8"
+ local udp_over_tcp="$9"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_address "$server_address" \
+ --arg server_port "$server_port" \
+ --arg version "$version" \
+ --arg username "$username" \
+ --arg password "$password" \
+ --arg network "$network" \
+ --arg udp_over_tcp "$udp_over_tcp" \
+ '.outbounds += [(
+ {
+ type: "socks",
+ tag: $tag,
+ server: $server_address,
+ server_port: ($server_port | tonumber)
+ }
+ + (if $version != "" then {version: $version} else {} end)
+ + (if $username != "" then {username: $username} else {} end)
+ + (if $password != "" then {password: $password} else {} end)
+ + (if $network != "" then {network: $network} else {} end)
+ + (if $udp_over_tcp != "" then {
+ udp_over_tcp: {
+ enabled: true,
+ version: ($udp_over_tcp | tonumber)
+ }
+ } else {} end)
+
+ )]'
+}
+
+#######################################
+# Add a Shadowsocks outbound to the outbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the outbound
+# server_address: string, IP address or hostname of the Shadowsocks server
+# server_port: number, port of the Shadowsocks server
+# method: string, encryption method
+# password: string, password for encryption
+# network: string, optional network type (e.g., "tcp")
+# udp_over_tcp: number, optional version for UDP over TCP
+# plugin: string, optional plugin name
+# plugin_opts: string, optional plugin options
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(
+# sing_box_cm_add_shadowsocks_outbound "$CONFIG" "ss-out" "127.0.0.1" 443 \
+# "2022-blake3-aes-128-gcm" "8JCsPssfgS8tiRwiMlhARg=="
+# )
+#######################################
+sing_box_cm_add_shadowsocks_outbound() {
+ local config="$1"
+ local tag="$2"
+ local server_address="$3"
+ local server_port="$4"
+ local method="$5"
+ local password="$6"
+ local network="$7"
+ local udp_over_tcp="$8"
+ local plugin="$9"
+ local plugin_opts="${10}"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_address "$server_address" \
+ --arg server_port "$server_port" \
+ --arg method "$method" \
+ --arg password "$password" \
+ --arg plugin "$plugin" \
+ --arg plugin_opts "$plugin_opts" \
+ --arg network "$network" \
+ --arg udp_over_tcp "$udp_over_tcp" \
+ '.outbounds += [(
+ {
+ type: "shadowsocks",
+ tag: $tag,
+ server: $server_address,
+ server_port: ($server_port | tonumber),
+ method: $method,
+ password: $password
+ }
+ + (if $plugin != "" then {plugin: $plugin} else {} end)
+ + (if $plugin_opts != "" then {plugin_opts: $plugin_opts} else {} end)
+ + (if $network != "" then {network: $network} else {} end)
+ + (if $udp_over_tcp != "" then {
+ udp_over_tcp: {
+ enabled: true,
+ version: ($udp_over_tcp | tonumber)
+ }
+ } else {} end)
+ )]'
+}
+
+#######################################
+# Add a VLESS outbound to the outbounds section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the outbound
+# server_address: string, IP address or hostname of the VLESS server
+# server_port: number, port of the VLESS server
+# uuid: string, user UUID
+# flow: string, optional flow setting
+# network: string, optional network type (e.g., "tcp")
+# packet_encoding: string, optional packet encoding method
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(
+# sing_box_cm_add_vless_outbound "$CONFIG" "vless-reality-out" "example.com" 443 \
+# "bf000d23-0752-40b4-affe-68f7707a9661" "xtls-rprx-vision"
+# )
+#######################################
+sing_box_cm_add_vless_outbound() {
+ local config="$1"
+ local tag="$2"
+ local server_address="$3"
+ local server_port="$4"
+ local uuid="$5"
+ local flow="$6"
+ local network="$7"
+ local packet_encoding="$8"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_address "$server_address" \
+ --arg server_port "$server_port" \
+ --arg uuid "$uuid" \
+ --arg flow "$flow" \
+ --arg network "$network" \
+ --arg packet_encoding "$packet_encoding" \
+ '.outbounds += [(
+ {
+ type: "vless",
+ tag: $tag,
+ server: $server_address,
+ server_port: ($server_port | tonumber),
+ uuid: $uuid
+ }
+ + (if $flow != "" then {flow: $flow} else {} end)
+ + (if $network != "" then {network: $network} else {} end)
+ + (if $packet_encoding != "" then {packet_encoding: $packet_encoding} else {} end)
+ )]'
+}
+
+#######################################
+# Set gRPC transport settings for a VLESS outbound in a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier of the outbound to modify
+# service_name: string, optional gRPC service name
+# idle_timeout: string or number, optional idle timeout
+# ping_timeout: string or number, optional ping timeout
+# permit_without_stream: boolean, optional flag for permitting without stream
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_set_vless_grpc_transport "$CONFIG" "vless-tls-grpc-out")
+#######################################
+sing_box_cm_set_vless_grpc_transport() {
+ local config="$1"
+ local tag="$2"
+ local service_name="$3"
+ local idle_timeout="$4"
+ local ping_timeout="$5"
+ local permit_without_stream="$6"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg service_name "$service_name" \
+ --arg idle_timeout "$idle_timeout" \
+ --arg ping_timeout "$ping_timeout" \
+ --arg permit_without_stream "$permit_without_stream" \
+ '.outbounds |= map(
+ if .tag == $tag then
+ . + {
+ transport: (
+ { type: "grpc" }
+ + (if $service_name != "" then {service_name: $service_name} else {} end)
+ + (if $idle_timeout != "" then {idle_timeout: $idle_timeout} else {} end)
+ + (if $ping_timeout != "" then {ping_timeout: $ping_timeout} else {} end)
+ + (if $permit_without_stream != "" then {permit_without_stream: $permit_without_stream} else {} end)
+ )
+ }
+ else
+ .
+ end
+ )'
+}
+
+#######################################
+# Set WebSocket transport settings for a VLESS outbound in a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier of the outbound to modify
+# path: string, WebSocket path
+# host: string, optional Host header for WebSocket
+# max_early_data: number, optional maximum early data
+# early_data_header_name: string, optional header name for early data
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_set_vless_ws_transport "$CONFIG" "vless-tls-ws-out" "/path" "example.com")
+#######################################
+sing_box_cm_set_vless_ws_transport() {
+ local config="$1"
+ local tag="$2"
+ local path="$3"
+ local host="$4"
+ local max_early_data="$5"
+ local early_data_header_name="$6"
+
+ # Not sure if the "Host" parameter in headers is correct — needs verification
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg path "$path" \
+ --arg host "$host" \
+ --arg max_early_data "$max_early_data" \
+ --arg early_data_header_name "$early_data_header_name" \
+ '.outbounds |= map(
+ if .tag == $tag then
+ . + {
+ transport: (
+ {
+ type: "ws",
+ path: $path
+ }
+ + (if $host != "" then {headers: {Host: $host}} else {} end)
+ + (if $max_early_data != "" then {max_early_data: $max_early_data | tonumber} else {} end)
+ + (if $early_data_header_name != "" then
+ {early_data_header_name: $early_data_header_name}
+ else {} end)
+ )
+ }
+ else
+ .
+ end
+ )'
+}
+
+#######################################
+# Set TLS settings for a VLESS outbound in a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier of the outbound to modify
+# server_name: string, optional, used to verify the hostname on the returned certificates
+# insecure: boolean, accept any server certificate
+# alpn: JSON value or null, optional supported application level protocols
+# utls_fingerprint: string, optional uTLS fingerprint
+# reality_public_key: string, optional Reality public key
+# reality_short_id: string, optional Reality short ID
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(
+# sing_box_cm_set_vless_tls "$CONFIG" "vless-reality-out" "example.com" false null "chrome" \
+# "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0" "0123456789abcdef"
+# )
+#######################################
+sing_box_cm_set_vless_tls() {
+ local config="$1"
+ local tag="$2"
+ local server_name="$3"
+ local insecure="$4"
+ local alpn="$5"
+ local utls_fingerprint="$6"
+ local reality_public_key="$7"
+ local reality_short_id="$8"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg server_name "$server_name" \
+ --arg insecure "$insecure" \
+ --argjson alpn "$alpn" \
+ --arg utls_fingerprint "$utls_fingerprint" \
+ --arg reality_public_key "$reality_public_key" \
+ --arg reality_short_id "$reality_short_id" \
+ '.outbounds |= map(
+ if .tag == $tag then
+ . + {
+ tls: (
+ { enabled: true }
+ + (if $server_name != "" then {server_name: $server_name} else {} end)
+ + (if $insecure == "true" then {insecure: true} else {} end)
+ + (if $alpn != null then {alpn: $alpn} else {} end)
+ + (if $utls_fingerprint != "" then {
+ utls: {
+ enabled: true,
+ fingerprint: $utls_fingerprint
+ }
+ } else {} end)
+ + (if $reality_public_key != "" then {
+ reality: {
+ enabled: true,
+ public_key: $reality_public_key,
+ short_id: $reality_short_id
+ }
+ } else {} end)
+ )
+ }
+ else
+ .
+ end
+ )'
+}
+
+#######################################
+# Add a Direct outbound with a specific network interface to a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the outbound
+# interface: string, network interface to bind the outbound
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_interface_outbound "$CONFIG" "warp-out" "awg0")
+#######################################
+sing_box_cm_add_interface_outbound() {
+ local config="$1"
+ local tag="$2"
+ local interface="$3"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg interface "$interface" \
+ '.outbounds += [{
+ type: "direct",
+ tag: $tag,
+ bind_interface: $interface
+ }]'
+}
+
+#######################################
+# Add a raw outbound JSON object to the outbounds section of a sing-box configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the outbound
+# raw_outbound: JSON object, the raw outbound configuration to add
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_raw_outbound "$CONFIG" "raw-out" '{"type":"trojan","tag":"trojan-out","server":"127.0.0.1","server_port":1080,"password":"8JCsPssfgS8tiRwiMlhARg==","network":"tcp"}')
+#######################################
+sing_box_cm_add_raw_outbound() {
+ local config="$1"
+ local tag="$2"
+ local raw_outbound="$3"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --argjson raw_outbound "$raw_outbound" \
+ '.outbounds += [(
+ $raw_outbound + {tag: $tag}
+ )]'
+}
+
+#######################################
+# Configure the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# final: string, final outbound tag for unmatched traffic
+# auto_detect_interface: boolean, enable or disable automatic interface detection
+# default_domain_resolver: string, default DNS resolver for domain-based routing
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_configure_route "$CONFIG" "direct-out" true "udp-server")
+#######################################
+sing_box_cm_configure_route() {
+ local config="$1"
+ local final="$2"
+ local auto_detect_interface="$3"
+ local default_domain_resolver="$4"
+
+ echo "$config" | jq \
+ --arg final "$final" \
+ --argjson auto_detect_interface "$auto_detect_interface" \
+ --arg default_domain_resolver "$default_domain_resolver" \
+ '.route = {
+ rules: (.route.rules // []),
+ rule_set: (.route.rule_set // []),
+ final: $final,
+ auto_detect_interface: $auto_detect_interface,
+ default_domain_resolver: $default_domain_resolver
+ }'
+}
+
+#######################################
+# Add a routing rule to the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the route rule
+# inbound: string, inbound tag to match
+# outbound: string, outbound tag to route matched traffic to
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_route_rule "$CONFIG" "main-route-rule" "tproxy-in" "main")
+#######################################
+sing_box_cm_add_route_rule() {
+ local config="$1"
+ local tag="$2"
+ local inbound="$3"
+ local outbound="$4"
+
+ echo "$config" | jq \
+ --arg service_tag "$SERVICE_TAG" \
+ --arg tag "$tag" \
+ --arg inbound "$inbound" \
+ --arg outbound "$outbound" \
+ '.route.rules += [{
+ action: "route",
+ inbound: $inbound,
+ outbound: $outbound,
+ $service_tag: $tag
+ }]'
+}
+
+#######################################
+# Patch a routing rule in the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier of the route rule to patch
+# key: string, the key in the rule to update or add
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_patch_route_rule "$CONFIG" "main-route-rule" "rule_set" "inline-ruleset")
+#######################################
+sing_box_cm_patch_route_rule() {
+ local config="$1"
+ local tag="$2"
+ local key="$3"
+ local value="$4"
+
+ value=$(_normalize_arg "$value")
+
+ echo "$config" | jq \
+ --arg service_tag "$SERVICE_TAG" \
+ --arg tag "$tag" \
+ --arg key "$key" \
+ --argjson value "$value" \
+ 'import "helpers" as h {"search": "/usr/lib/podkop"};
+ .route.rules |= map(
+ if .[$service_tag] == $tag then
+ if has($key) then
+ .[$key] |= h::extend_key_value(.; $value)
+ else
+ . + { ($key): $value }
+ end
+ else
+ .
+ end
+ )'
+}
+
+#######################################
+# Add a reject rule to the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# key: string, the key to set for the reject rule
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_reject_route_rule "$CONFIG" "reject-rule-tag")
+#######################################
+sing_box_cm_add_reject_route_rule() {
+ local config="$1"
+ local tag="$2"
+ local inbound="$3"
+
+ echo "$config" | jq \
+ --arg service_tag "$SERVICE_TAG" \
+ --arg tag "$tag" \
+ --arg inbound "$inbound" \
+ '.route.rules += [{
+ action: "reject",
+ inbound: $inbound,
+ $service_tag: $tag
+ }]'
+}
+
+#######################################
+# Add a hijack-dns rule to the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# key: string, the key to set for the hijack-dns rule
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_hijack_dns_route_rule "$CONFIG" "protocol" "dns")
+#######################################
+sing_box_cm_add_hijack_dns_route_rule() {
+ local config="$1"
+ local key="$2"
+ local value="$3"
+
+ value=$(_normalize_arg "$value")
+
+ echo "$config" | jq \
+ --arg key "$key" \
+ --argjson value "$value" \
+ '.route.rules += [{
+ action: "hijack-dns",
+ ($key): $value
+ }]'
+}
+
+#######################################
+# Add a route-options rule to the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the route-options rule
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_options_route_rule "$CONFIG" "override-fakeip-port")
+#######################################
+sing_box_cm_add_options_route_rule() {
+ local config="$1"
+ local tag="$2"
+
+ echo "$config" | jq \
+ --arg service_tag "$SERVICE_TAG" \
+ --arg tag "$tag" \
+ '.route.rules += [{
+ action: "route-options",
+ $service_tag: $tag
+ }]'
+}
+
+#######################################
+# Add a sniff rule to the route section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# key: string, the key to set for the sniff rule
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_sniff_route_rule "$CONFIG" "inbound" "tproxy-in")
+# CONFIG=$(sing_box_cm_sniff_route_rule "$CONFIG" "inbound" '["tproxy-in","dns-in"]')
+#######################################
+sing_box_cm_sniff_route_rule() {
+ local config="$1"
+ local key="$2"
+ local value="$3"
+
+ value=$(_normalize_arg "$value")
+
+ echo "$config" | jq \
+ --arg key "$key" \
+ --argjson value "$value" \
+ '.route.rules += [{
+ action: "sniff",
+ ($key): $value
+ }]'
+}
+
+#######################################
+# Add an inline ruleset to the route.rule_set section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the inline ruleset
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_inline_ruleset "$CONFIG" "inline-ruleset")
+#######################################
+sing_box_cm_add_inline_ruleset() {
+ local config="$1"
+ local tag="$2"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ '.route.rule_set += [{
+ type: "inline",
+ tag: $tag
+ }]'
+}
+
+#######################################
+# Add or update a rule in an inline ruleset within the route.rule_set section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier of the inline ruleset
+# key: string, the key in the ruleset to update or add
+# value: JSON value to assign to the key
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_inline_ruleset_rule "$CONFIG" "inline-ruleset" "domain_suffix" "telegram.org")
+# CONFIG=$(sing_box_cm_add_inline_ruleset_rule "$CONFIG" "inline-ruleset" "domain_suffix" "discord.com")
+# CONFIG=$(sing_box_cm_add_inline_ruleset_rule "$CONFIG" "inline-ruleset" "ip_cidr" "111.111.111.111/32")
+#######################################
+sing_box_cm_add_inline_ruleset_rule() {
+ local config="$1"
+ local tag="$2"
+ local key="$3"
+ local value="$4"
+
+ value=$(_normalize_arg "$value")
+
+ echo "$config" | jq -L /usr/lib/podkop \
+ --arg tag "$tag" \
+ --arg key "$key" \
+ --argjson value "$value" \
+ 'import "helpers" as h {"search": "/usr/lib/podkop"};
+ .route.rule_set |= map(
+ if .tag == $tag then
+ if has($key) then
+ .[$key] |= h::extend_key_value(.; $value)
+ else
+ . + { ($key): $value }
+ end
+ else
+ .
+ end
+ )'
+}
+
+#######################################
+# Add a local ruleset to the route.rule_set section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the local ruleset
+# format: string, format of the local ruleset ("source" or "binary")
+# path: string, file path to the local ruleset
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_local_ruleset "$CONFIG" "local-source-ruleset" "source" "/tmp/local-ruleset.json")
+# CONFIG=$(sing_box_cm_add_local_ruleset "$CONFIG" "local-binary-ruleset" "binary" "/tmp/local-ruleset.srs")
+#######################################
+sing_box_cm_add_local_ruleset() {
+ local config="$1"
+ local tag="$2"
+ local format="$3"
+ local path="$4"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg format "$format" \
+ --arg path "$path" \
+ '.route.rule_set += [{
+ type: "local",
+ tag: $tag,
+ format: $format,
+ path: $path
+ }]'
+}
+
+#######################################
+# Add a remote ruleset to the route.rule_set section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# tag: string, identifier for the remote ruleset
+# format: string, format of the remote ruleset ("source" or "binary")
+# url: string, URL to download the ruleset from
+# download_detour: string, optional detour tag for downloading
+# update_interval: string, optional update interval for the ruleset
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_add_remote_ruleset "$CONFIG" "remote-source-ruleset" "source" "https://example.com/telegram.json")
+#######################################
+sing_box_cm_add_remote_ruleset() {
+ local config="$1"
+ local tag="$2"
+ local format="$3"
+ local url="$4"
+ local download_detour="$5"
+ local update_interval="$6"
+
+ echo "$config" | jq \
+ --arg tag "$tag" \
+ --arg format "$format" \
+ --arg url "$url" \
+ --arg download_detour "$download_detour" \
+ --arg update_interval "$update_interval" \
+ '.route.rule_set += [(
+ {
+ type: "remote",
+ tag: $tag,
+ format: $format,
+ url: $url
+ }
+ + (if $download_detour != "" then { download_detour: $download_detour } else {} end)
+ + (if $update_interval != "" then { update_interval: $update_interval } else {} end)
+ )]'
+
+}
+
+#######################################
+# Configure the experimental cache_file section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# enabled: boolean, enable or disable file caching
+# path: string, file path for cache storage
+# store_fakeip: boolean, whether to store fake IPs in the cache
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_configure_cache_file "$CONFIG" true "/tmp/cache.db" true)
+#######################################
+sing_box_cm_configure_cache_file() {
+ local config="$1"
+ local enabled="$2"
+ local path="$3"
+ local store_fakeip="$4"
+
+ echo "$config" | jq \
+ --argjson enabled "$enabled" \
+ --arg path "$path" \
+ --argjson store_fakeip "$store_fakeip" \
+ '.experimental.cache_file = {
+ enabled: $enabled,
+ path: $path,
+ store_fakeip: $store_fakeip
+ }'
+}
+
+#######################################
+# Configure the experimental clash_api section of a sing-box JSON configuration.
+# Arguments:
+# config: JSON configuration (string)
+# external_controller: string, URL or path for the external controller
+# external_ui: string, URL or path for the external UI
+# Outputs:
+# Writes updated JSON configuration to stdout
+# Example:
+# CONFIG=$(sing_box_cm_configure_clash_api "$CONFIG" "192.168.1.1:9090" "ui")
+#######################################
+sing_box_cm_configure_clash_api() {
+ local config="$1"
+ local external_controller="$2"
+ local external_ui="$3"
+
+ echo "$config" | jq \
+ --arg external_controller "$external_controller" \
+ --arg external_ui "$external_ui" \
+ '.experimental.clash_api = {
+ external_controller: $external_controller,
+ external_ui: $external_ui
+ }'
+}
+
+#######################################
+# Create a local source ruleset JSON file for sing-box.
+# Arguments:
+# filepath: path to the JSON file to create
+# Example:
+# sing_box_cm_create_local_source_ruleset "/tmp/sing-box/ruleset.json"
+#######################################
+sing_box_cm_create_local_source_ruleset() {
+ local filepath="$1"
+
+ jq -n '{version: 3, rules: []}' > "$filepath"
+}
+
+#######################################
+# Patch a local source ruleset JSON file for sing-box by adding unique! values to a given key.
+# Arguments:
+# filepath: path to the JSON file to patch
+# key: the ruleset key to update (e.g., "ip_cidr")
+# value: a JSON array of values to add to the key
+# Example:
+# sing_box_cm_patch_local_source_ruleset_rules "/tmp/sing-box/ruleset.json" "ip_cidr" '["1.1.1.1","2.2.2.2"]'
+#######################################
+sing_box_cm_patch_local_source_ruleset_rules() {
+ local filepath="$1"
+ local key="$2"
+ local value="$3"
+
+ value=$(_normalize_arg "$value")
+
+ local content
+ content="$(cat "$filepath")"
+
+ echo "$content" | jq \
+ --arg key "$key" \
+ --argjson value "$value" '
+ ([.rules[]?[$key][]] | unique) as $existing
+ | ($value - $existing) as $value
+ | if ($value | length) > 0 then
+ .rules += [{($key): $value}]
+ else
+ .
+ end
+ ' > "$filepath"
+}
+
+#######################################
+# Save a sing-box JSON configuration to a file, removing service-specific tags.
+# Arguments:
+# config: JSON configuration (string)
+# file_path: string, path to save the configuration file
+# Outputs:
+# Writes the cleaned JSON configuration to the specified file
+# Example:
+# sing_box_cm_save_config_to_file "$CONFIG" "/tmp/sing-box-config.json"
+#######################################
+sing_box_cm_save_config_to_file() {
+ local config="$1"
+ local filepath="$2"
+
+ echo "$config" | jq \
+ --arg tag "$SERVICE_TAG" \
+ 'walk(if type == "object" then del(.[$tag]) else . end)' \
+ > "$filepath"
+}
+
+_normalize_arg() {
+ local value="$1"
+ if echo "$value" | jq -e . > /dev/null 2>&1; then
+ printf '%s' "$value"
+ else
+ printf '%s' "$value" | jq -R .
+ fi
+}