Honor section order and combined domains

This commit is contained in:
ushan0v
2026-06-18 08:40:19 +03:00
parent 3b4d9fd7aa
commit 3b0c7e0fc7
5 changed files with 315 additions and 14 deletions
+67
View File
@@ -611,6 +611,43 @@ migrate_0_7_17_8_zapret_nfqws_default() {
podkop_uci_set_option "$section" "nfqws_opt" "$ZAPRET_DEFAULT_NFQWS_OPT"
}
podkop_uci_add_domain_values_with_prefix() {
local section="$1"
local option="$2"
local prefix="$3"
local kind="$4"
local values value
values="$(get_legacy_domain_condition_commas_string "$section" "$option" "$kind")"
[ -n "$values" ] || return 0
while IFS= read -r value || [ -n "$value" ]; do
value="$(printf '%s' "$value" | trim_string)"
[ -n "$value" ] || continue
podkop_uci_add_list_unique "$section" "domain_suffix" "${prefix}${value}"
done <<EOF
$(printf '%s' "$values" | tr ',' '\n')
EOF
}
migrate_combined_domain_conditions() {
local section="$1"
podkop_uci_add_domain_values_with_prefix "$section" "domain" "full:" "domains"
podkop_uci_add_domain_values_with_prefix "$section" "domain_keyword" "keyword:" "generic"
podkop_uci_add_domain_values_with_prefix "$section" "domain_regex" "regex:" "generic"
podkop_uci_delete_option "$section" "domain"
podkop_uci_delete_option "$section" "domain_keyword"
podkop_uci_delete_option "$section" "domain_regex"
podkop_uci_delete_option "$section" "domain_text"
podkop_uci_delete_option "$section" "domain_keyword_text"
podkop_uci_delete_option "$section" "domain_regex_text"
podkop_uci_delete_option "$section" "domain_text_mode"
podkop_uci_delete_option "$section" "domain_keyword_text_mode"
podkop_uci_delete_option "$section" "domain_regex_text_mode"
}
migrate_0_7_17_8_rule() {
local section="$1"
local converted_from_rule="${2:-0}"
@@ -835,6 +872,7 @@ migration() {
config_foreach migrate_0_7_17_8_rule_section "rule"
config_foreach migrate_0_7_17_8_rule "section"
config_foreach migrate_combined_domain_conditions "section"
if [ "$podkop_config_migration_changed" -eq 1 ]; then
log "Migrated Podkop Plus UCI config"
@@ -983,6 +1021,33 @@ validate_urltest_regex_option() {
exit 1
}
validate_combined_domain_option() {
local value="$1"
local section="$2"
[ -n "$value" ] || return 0
if config_validation_ucode combined-domain-valid "$value" >/dev/null 2>&1; then
return 0
fi
log "Invalid domain condition '$value' in rule '$section'. Use plain domains or full:, keyword:, regex: prefixes. Aborted." "fatal"
exit 1
}
validate_combined_domain_text_option() {
local section="$1"
local text_value
config_get text_value "$section" "domain_suffix_text"
[ -n "$text_value" ] || return 0
if config_validation_ucode combined-domain-text-valid "$text_value" >/dev/null 2>&1; then
return 0
fi
log "Invalid domain conditions in rule '$section'. Use plain domains or full:, keyword:, regex: prefixes. Aborted." "fatal"
exit 1
}
ruleset_kind_from_reference_hint() {
local reference="$1"
@@ -1237,6 +1302,8 @@ process_validate_rule() {
validate_outbound_detour_rule "$section"
config_list_foreach "$section" "domain_suffix" validate_combined_domain_option "$section"
validate_combined_domain_text_option "$section"
config_list_foreach "$section" "community_lists" validate_service
config_list_foreach "$section" "rule_set" validate_ruleset_reference
config_list_foreach "$section" "rule_set_with_subnets" validate_ruleset_reference
+66
View File
@@ -80,6 +80,68 @@ function regex_valid(pattern) {
}
}
function strip_list_comment(line) {
line = replace(as_string(line), /[[:space:]]*\/\/.*$/, "");
return replace(line, /[[:space:]]*#.*$/, "");
}
function text_list_values(value) {
let result = [];
for (let line in split(as_string(value), "\n")) {
line = replace(strip_list_comment(line), /[ ,]/g, "\n");
for (let item in split(line, "\n")) {
item = trim(replace(item, /\r/g, ""));
if (item != "")
push(result, item);
}
}
return result;
}
function valid_domain_suffix(value) {
value = lc(as_string(value));
if (substr(value, 0, 1) == ".")
value = substr(value, 1);
return match(value, /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/) != null;
}
function combined_domain_valid(value) {
value = trim(as_string(value));
if (value == "")
return true;
let colon = index(value, ":");
if (colon < 0)
return valid_domain_suffix(value);
let prefix = lc(substr(value, 0, colon));
let body = substr(value, colon + 1);
if (body == "")
return false;
if (prefix == "full")
return valid_domain_suffix(body);
if (prefix == "keyword")
return match(body, /[,[:space:]]/) == null;
if (prefix == "regex")
return match(body, /[,[:space:]]/) == null && regex_valid(body);
return false;
}
function combined_domain_text_valid(value) {
for (let item in text_list_values(value))
if (!combined_domain_valid(item))
return false;
return true;
}
function valid_outbound() {
let value = read_stdin_json();
return type(value) == "object" && type(value.type) == "string";
@@ -160,6 +222,10 @@ else if (mode == "enum-valid")
exit(enum_valid(ARGV[1], 2) ? 0 : 1);
else if (mode == "regex-valid")
exit(regex_valid(ARGV[1]) ? 0 : 1);
else if (mode == "combined-domain-valid")
exit(combined_domain_valid(ARGV[1]) ? 0 : 1);
else if (mode == "combined-domain-text-valid")
exit(combined_domain_text_valid(ARGV[1]) ? 0 : 1);
else if (mode == "valid-outbound")
exit(valid_outbound() ? 0 : 1);
else if (mode == "outbound-detour-supported")
+107
View File
@@ -41,6 +41,20 @@ parse_domain_or_subnet_file_to_comma_string() {
rules_nft_runtime_ucode domain-subnet-file-csv "$filepath" "$type"
}
parse_combined_domain_string_to_commas_string() {
local string="$1"
local type="$2"
rules_nft_runtime_ucode combined-domain-text-csv "$string" "$type"
}
parse_combined_domain_list_to_commas_string() {
local list_value="$1"
local type="$2"
rules_nft_runtime_ucode combined-domain-csv "$(rule_list_value_to_commas_string "$list_value")" "$type"
}
split_domain_or_subnet_file() {
local filepath="$1"
local domains_output="$2"
@@ -53,12 +67,104 @@ rule_list_value_to_commas_string() {
printf '%s\n' "$1" | tr ' ' ','
}
get_legacy_domain_condition_commas_string() {
local section="$1"
local key="$2"
local kind="$3"
local text_mode conditions_text_mode text_value list_value
config_get_bool text_mode "$section" "${key}_text_mode" 0
config_get_bool conditions_text_mode "$section" "conditions_text_mode" 0
config_get text_value "$section" "${key}_text"
config_get list_value "$section" "$key"
if [ "$text_mode" -eq 1 ] || [ "$conditions_text_mode" -eq 1 ]; then
case "$kind" in
domains) parse_domain_or_subnet_string_to_commas_string "$text_value" "domains" ;;
*)
parse_generic_string_to_commas_string "$text_value"
;;
esac
return 0
fi
if [ -n "$list_value" ]; then
rule_list_value_to_commas_string "$list_value"
return 0
fi
if [ -n "$text_value" ]; then
case "$kind" in
domains) parse_domain_or_subnet_string_to_commas_string "$text_value" "domains" ;;
*)
parse_generic_string_to_commas_string "$text_value"
;;
esac
return 0
fi
rule_list_value_to_commas_string "$list_value"
}
get_combined_domain_condition_commas_string() {
local section="$1"
local requested_key="$2"
local legacy_kind="$3"
local combined_text_mode conditions_text_mode combined_text_value combined_list_value
local combined_text_values combined_list_values combined_values legacy_values
config_get_bool combined_text_mode "$section" "domain_suffix_text_mode" 0
config_get_bool conditions_text_mode "$section" "conditions_text_mode" 0
config_get combined_text_value "$section" "domain_suffix_text"
config_get combined_list_value "$section" "domain_suffix"
combined_text_values="$(parse_combined_domain_string_to_commas_string "$combined_text_value" "$requested_key")"
combined_list_values="$(parse_combined_domain_list_to_commas_string "$combined_list_value" "$requested_key")"
if [ -n "$combined_text_values" ] && [ -n "$combined_list_values" ]; then
combined_values="${combined_text_values},${combined_list_values}"
else
combined_values="${combined_text_values}${combined_list_values}"
fi
if [ "$requested_key" = "domain_suffix" ]; then
printf '%s\n' "$combined_values"
return 0
fi
legacy_values="$(get_legacy_domain_condition_commas_string "$section" "$requested_key" "$legacy_kind")"
if [ -n "$combined_values" ] && [ -n "$legacy_values" ]; then
printf '%s,%s\n' "$combined_values" "$legacy_values"
else
printf '%s%s\n' "$combined_values" "$legacy_values"
fi
}
get_rule_condition_commas_string() {
local section="$1"
local key="$2"
local kind="$3"
local text_mode conditions_text_mode text_value list_value
case "$key" in
domain)
get_combined_domain_condition_commas_string "$section" "domain" "domains"
return 0
;;
domain_suffix)
get_combined_domain_condition_commas_string "$section" "domain_suffix" "domains"
return 0
;;
domain_keyword)
get_combined_domain_condition_commas_string "$section" "domain_keyword" "generic"
return 0
;;
domain_regex)
get_combined_domain_condition_commas_string "$section" "domain_regex" "generic"
return 0
;;
esac
config_get_bool text_mode "$section" "${key}_text_mode" 0
config_get_bool conditions_text_mode "$section" "conditions_text_mode" 0
config_get text_value "$section" "${key}_text"
@@ -395,6 +501,7 @@ create_nft_rules() {
nft add chain inet "$NFT_TABLE_NAME" proxy '{ type filter hook prerouting priority -100; policy accept; }'
nft add rule inet "$NFT_TABLE_NAME" mangle ct status dnat return
nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_LOCALV4_SET_NAME" return
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 "$NFT_FAKEIP_MARK" 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 "$NFT_FAKEIP_MARK" counter
nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr . tcp dport "@$NFT_IP_PORT_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter
+73
View File
@@ -221,6 +221,51 @@ function domain_subnet_value_valid(value, kind) {
exit(1);
}
function prefixed_domain_value(value, requested_kind) {
value = trim(as_string(value));
requested_kind = as_string(requested_kind);
if (value == "")
return null;
let colon = index(value, ":");
let prefix = "";
let body = value;
if (colon > 0) {
let candidate = ascii_lower(substr(value, 0, colon));
if (candidate == "full" || candidate == "keyword" || candidate == "regex") {
prefix = candidate;
body = substr(value, colon + 1);
}
else {
return null;
}
}
body = trim(body);
if (body == "")
return null;
if (prefix == "") {
if (requested_kind != "domain_suffix")
return null;
let normalized = ascii_lower(body);
return valid_domain_suffix(normalized) ? normalized : null;
}
if (prefix == "full") {
if (requested_kind != "domain")
return null;
let normalized = ascii_lower(body);
return valid_domain_suffix(normalized) ? normalized : null;
}
if (prefix == "keyword")
return requested_kind == "domain_keyword" ? body : null;
return requested_kind == "domain_regex" ? body : null;
}
function normalize_domain_subnet_value(value, kind) {
kind = as_string(kind);
if (kind == "domains") {
@@ -249,6 +294,30 @@ function filter_domain_subnet_values(values, kind) {
return result;
}
function combined_domain_text_csv(value, requested_kind) {
let result = [];
for (let item in text_list_values(value, "comma-space")) {
let normalized = prefixed_domain_value(item, requested_kind);
if (normalized != null)
push(result, normalized);
}
print_csv(result);
}
function combined_domain_csv(value, requested_kind) {
let result = [];
for (let item in split(as_string(value), ",")) {
let normalized = prefixed_domain_value(item, requested_kind);
if (normalized != null)
push(result, normalized);
}
print_csv(result);
}
function domain_subnet_text_csv(value, kind) {
print_csv(filter_domain_subnet_values(text_list_values(value, "comma-space"), kind));
}
@@ -650,6 +719,10 @@ else if (mode == "stdin-contains")
exit(stdin_contains(ARGV[1]) ? 0 : 1);
else if (mode == "domain-subnet-text-csv")
domain_subnet_text_csv(ARGV[1], ARGV[2]);
else if (mode == "combined-domain-text-csv")
combined_domain_text_csv(ARGV[1], ARGV[2]);
else if (mode == "combined-domain-csv")
combined_domain_csv(ARGV[1], ARGV[2]);
else if (mode == "domain-subnet-file-csv")
domain_subnet_file_csv(ARGV[1], ARGV[2]);
else if (mode == "split-domain-subnet-file")
+2 -14
View File
@@ -1178,7 +1178,6 @@ sing_box_configure_route() {
config_list_foreach "settings" "routing_excluded_ips" exclude_source_ip_from_routing_handler "$rule_tag"
fi
config_foreach configure_fully_routed_rule_handler "section"
config_foreach configure_route_rule_handler "section"
config_foreach configure_server_route "server"
}
@@ -1579,6 +1578,8 @@ configure_route_rule_handler() {
log "Configuring route rule '$section' with action '$action'"
configure_fully_routed_ips_for_section "$section" "$action"
if ! rule_has_primary_matchers "$section"; then
log "Rule '$section' has no destination matchers and action '$action', skipping regular route creation" "warn"
return 0
@@ -1661,19 +1662,6 @@ configure_route_rule_handler() {
fi
}
configure_fully_routed_rule_handler() {
local section="$1"
rule_is_enabled "$section" || return 0
subscription_section_is_deferred "$section" && return 0
local action
action="$(get_rule_action "$section")"
[ -n "$action" ] || return 0
configure_fully_routed_ips_for_section "$section" "$action"
}
configure_fully_routed_ips_for_section() {
local section="$1"
local action="$2"