From 49836e4adcbbf00b007ccd4a6e4f262ec46c5487 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 5 Sep 2025 21:23:55 +0500 Subject: [PATCH] feat: Add local subnet lists support with UI and backend integration (#156) --- .../resources/view/podkop/configSection.js | 21 ++++- luci-app-podkop/po/ru/podkop.po | 12 ++- luci-app-podkop/po/templates/podkop.pot | 8 +- podkop/files/etc/config/podkop | 2 + podkop/files/usr/bin/podkop | 79 +++++++++++++------ podkop/files/usr/lib/helpers.sh | 3 +- 6 files changed, 94 insertions(+), 31 deletions(-) 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 f55aded..370e745 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 @@ -370,7 +370,7 @@ function createConfigSection(section, map, network) { o.rmempty = false; o.ucisection = s.section; - o = s.taboption('basic', form.DynamicList, 'local_domain_lists', _('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('local_domain_lists_enabled', '1'); o.rmempty = false; @@ -399,6 +399,25 @@ function createConfigSection(section, map, network) { return validateUrl(value); }; + 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')); diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index 88c144e..be99d67 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,10 @@ 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 "Пути к локальным спискам доменов" \ 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..763c411 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,10 @@ 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 "" \ No newline at end of file diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index ad2a35b..1527ee0 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -16,6 +16,8 @@ config main 'main' 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' diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 9bb2c4f..904e04c 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -870,13 +870,15 @@ exclude_source_ip_from_routing_handler() { configure_routing_for_section_lists() { local section="$1" - local community_lists_enabled local_domain_lists_enabled remote_domain_lists_enabled remote_subnet_lists_enabled + local community_lists_enabled local_domain_lists_enabled remote_domain_lists_enabled remote_subnet_lists_enabled \ + local_subnet_lists_enabled local user_domain_list_type user_subnet_list_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 if [ "$community_lists_enabled" -eq 0 ] && \ @@ -884,6 +886,7 @@ configure_routing_for_section_lists() { [ "$local_domain_lists_enabled" -eq 0 ] && \ [ "$remote_domain_lists_enabled" -eq 0 ] && \ [ "$user_subnet_list_type" == "disabled" ] && \ + [ "$local_subnet_lists_enabled" -eq 0 ] && \ [ "$remote_subnet_lists_enabled" == 0 ] ; then log "Section $section does not have any enabled list, skipping..." "warn" return 0 @@ -906,7 +909,7 @@ configure_routing_for_section_lists() { if [ "$local_domain_lists_enabled" -eq 1 ]; then log "Processing local domains routing rules for $section section" - configure_local_domain_lists "$section" "$route_rule_tag" + configure_local_domain_or_subnet_lists "$section" "domains" "$route_rule_tag" fi if [ "$remote_domain_lists_enabled" -eq 1 ]; then @@ -921,6 +924,11 @@ configure_routing_for_section_lists() { # configure_user_subnet_list_handler 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" config_list_foreach "$section" "remote_subnet_lists" configure_remote_domain_or_subnet_list_handler \ @@ -950,58 +958,81 @@ configure_user_domain_list_handler() { # TODO(ampetelin): it is necessary to implement } -configure_local_domain_lists() { +configure_local_domain_or_subnet_lists() { local section="$1" - local route_rule_tag="$2" + local type="$2" + local route_rule_tag="$3" local ruleset_tag ruleset_filename ruleset_filepath - ruleset_tag="$(get_ruleset_tag "$section" "local" "domains")" + ruleset_tag="$(get_ruleset_tag "$section" "local" "$type")" ruleset_filename="$ruleset_tag.json" ruleset_filepath="$TMP_FOLDER/$ruleset_filename" 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") - _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" - config_list_foreach "$section" "local_domains_list" import_local_domain_list_to_ruleset "$section" "$ruleset_filepath" + case "$type" in + domains) + config_list_foreach "$section" "local_domain_lists" import_local_domain_or_subnet_list_to_ruleset "$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_to_ruleset "$type" \ + "$section" "$ruleset_filepath";; + *) log "Unsupported local rule set type: $type" "warn" ;; + esac } -import_local_domain_list_to_ruleset() { +import_local_domain_or_subnet_list_to_ruleset() { local filepath="$1" - local section="$2" - local ruleset_filepath="$3" + local type="$2" + local section="$3" + local ruleset_filepath="$4" if ! file_exists "$filepath"; then log "File $filepath not found" "warn" return 1 fi - local domains="" - while IFS= read -r domain; do - if [ -z "$domain" ]; then + local items="" + while IFS= read -r item; do + if [ -z "$item" ]; then continue fi - if ! is_domain "$domain"; then - log "$domain is not domain" "debug" - continue - fi + case "$type" in + domains) + if ! is_domain "$item"; then + log "$item is not domain" "debug" + continue + fi + ;; + subnets) + if ! is_ipv4 "$item" && ! is_ipv4_cidr "$item"; then + log "$item is not IPv4 IP or CIDR" "debug" + continue + fi + ;; + esac - if [ -z "$domains" ]; then - domains="$domain" + if [ -z "$items" ]; then + items="$item" else - domains="$domains,$domain" + items="$items,$item" fi done < "$filepath" - if [ -z "$domains" ]; then - log "No valid domains found in $filepath" + if [ -z "$items" ]; then + log "No valid $type found in $filepath" return 0 fi - domains="$(comma_string_to_json_array "$domains")" - sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$domains" + items="$(comma_string_to_json_array "$items")" + case "$type" in + domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$items" ;; + subnets) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$items" ;; + esac } configure_remote_domain_or_subnet_list_handler() { diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index b759ce4..656dc54 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -12,11 +12,10 @@ is_ipv4_cidr() { [[ "$ip" =~ $regex ]] } +# Check if string is valid domain is_domain() { local str="$1" - #local regex="^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$" 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]))+$' - #[[ $str =~ $regex ]] } # Checks if the given string is a valid base64-encoded sequence