From 7e041da8c61803992c4711aa32a5cfb3eba7ac80 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 31 Aug 2025 13:09:55 +0500 Subject: [PATCH 01/58] feat: add sing-box configuration manager script and jq helpers --- podkop/files/usr/lib/helpers.jq | 14 + .../files/usr/lib/sing_box_config_manager.sh | 1207 +++++++++++++++++ 2 files changed, 1221 insertions(+) create mode 100644 podkop/files/usr/lib/helpers.jq create mode 100644 podkop/files/usr/lib/sing_box_config_manager.sh 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/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh new file mode 100644 index 0000000..2cb8d08 --- /dev/null +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -0,0 +1,1207 @@ +#!/bin/ash +# +# 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 +# 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" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg server_address "$server_address" \ + --arg server_port "$server_port" \ + '.dns.servers += [{ + type: "udp", + tag: $tag, + server: $server_address, + server_port: ($server_port | tonumber) + }]' +} + +####################################### +# 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 +# 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" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg server_address "$server_address" \ + --arg server_port "$server_port" \ + '.dns.servers += [{ + type: "tls", + tag: $tag, + server: $server_address, + server_port: ($server_port | tonumber) + }]' +} + +####################################### +# 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 +# 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" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg server_address "$server_address" \ + --arg server_port "$server_port" \ + --arg path "$path" \ + --arg headers "$headers" \ + '.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) + )]' +} + +####################################### +# 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 server_address "$server_address" \ + --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" + + echo "$config" | jq \ + --arg service_tag "$SERVICE_TAG" \ + --arg tag "$tag" \ + --arg key "$key" \ + --argjson value "$value" \ + 'import "helpers" as h; + .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" + + 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 { + ults: { + 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" + + echo "$config" | jq \ + --arg service_tag "$SERVICE_TAG" \ + --arg tag "$tag" \ + --arg key "$key" \ + --argjson value "$value" \ + 'import "helpers" as h; + .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" "protocol" '"quic"') +####################################### +sing_box_cm_add_reject_route_rule() { + local config="$1" + local key="$2" + local value="$3" + + echo "$config" | jq \ + --arg key "$key" \ + --argjson value "$value" \ + '.route.rules += [{ + action: "reject", + ($key): $value + }]' +} + +####################################### +# 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" + + 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" + + 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" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg key "$key" \ + --argjson value "$value" \ + 'import "helpers" as h; + .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 + }' +} + +####################################### +# 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 file_path="$2" + + echo "$config" | jq \ + --arg tag "$SERVICE_TAG" \ + 'walk(if type == "object" then del(.[$tag]) else . end)' \ + > "$file_path" +} \ No newline at end of file From cd1a4e2a8e9e1d4ad1e641d54d5da6b3201a4b61 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 31 Aug 2025 13:19:04 +0500 Subject: [PATCH 02/58] chore: update example links with valid values --- String-example.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 From 2753a44440157f9a9077e99ab6bd8dd65a9ff631 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 31 Aug 2025 19:43:21 +0500 Subject: [PATCH 03/58] feat: Add domain_resolver parameter to DNS server configurations in sing-box manager script --- .../files/usr/lib/sing_box_config_manager.sh | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 2cb8d08..19a1534 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -97,17 +97,22 @@ sing_box_cm_add_udp_dns_server() { local tag="$2" local server_address="$3" local server_port="$4" + local domain_resolver="$5" echo "$config" | jq \ --arg tag "$tag" \ --arg server_address "$server_address" \ --arg server_port "$server_port" \ - '.dns.servers += [{ - type: "udp", - tag: $tag, - server: $server_address, - server_port: ($server_port | tonumber) - }]' + --arg domain_resolver "$domain_resolver" \ + '.dns.servers += [( + { + type: "udp", + tag: $tag, + server: $server_address, + server_port: ($server_port | tonumber) + } + + (if $domain_resolver != "" then { domain_resolver: $domain_resolver } else {} end) + )]' } ####################################### @@ -127,17 +132,22 @@ sing_box_cm_add_tls_dns_server() { local tag="$2" local server_address="$3" local server_port="$4" + local domain_resolver="$5" echo "$config" | jq \ --arg tag "$tag" \ --arg server_address "$server_address" \ --arg server_port "$server_port" \ - '.dns.servers += [{ - type: "tls", - tag: $tag, - server: $server_address, - server_port: ($server_port | tonumber) - }]' + --arg domain_resolver "$domain_resolver" \ + '.dns.servers += [( + { + type: "tls", + tag: $tag, + server: $server_address, + server_port: ($server_port | tonumber) + } + + (if $domain_resolver != "" then { domain_resolver: $domain_resolver } else {} end) + )]' } ####################################### @@ -161,6 +171,7 @@ sing_box_cm_add_https_dns_server() { local server_port="$4" local path="$5" local headers="$6" + local domain_resolver="$7" echo "$config" | jq \ --arg tag "$tag" \ @@ -168,6 +179,7 @@ sing_box_cm_add_https_dns_server() { --arg server_port "$server_port" \ --arg path "$path" \ --arg headers "$headers" \ + --arg domain_resolver "$domain_resolver" \ '.dns.servers += [( { type: "https", @@ -177,6 +189,7 @@ sing_box_cm_add_https_dns_server() { } + (if $path != "" then { path: $path } else {} end) + (if $headers != "" then { headers: $headers } else {} end) + + (if $domain_resolver != "" then { domain_resolver: $domain_resolver } else {} end) )]' } @@ -1204,4 +1217,4 @@ sing_box_cm_save_config_to_file() { --arg tag "$SERVICE_TAG" \ 'walk(if type == "object" then del(.[$tag]) else . end)' \ > "$file_path" -} \ No newline at end of file +} From 41ce41945cc10a1efa9f60c571b05b186dfaac68 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 31 Aug 2025 20:22:57 +0500 Subject: [PATCH 04/58] feat: Add domain_resolver and detour parameters to DNS server configurations in sing-box manager script --- .../files/usr/lib/sing_box_config_manager.sh | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 2cb8d08..0380103 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -87,6 +87,8 @@ sing_box_cm_configure_dns() { # 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: @@ -97,17 +99,25 @@ sing_box_cm_add_udp_dns_server() { 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" \ - '.dns.servers += [{ - type: "udp", - tag: $tag, - server: $server_address, - server_port: ($server_port | tonumber) - }]' + --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) + )]' } ####################################### @@ -117,6 +127,8 @@ sing_box_cm_add_udp_dns_server() { # 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: @@ -127,17 +139,25 @@ sing_box_cm_add_tls_dns_server() { 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" \ - '.dns.servers += [{ - type: "tls", - tag: $tag, - server: $server_address, - server_port: ($server_port | tonumber) - }]' + --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) + )]' } ####################################### @@ -149,6 +169,8 @@ sing_box_cm_add_tls_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: @@ -161,6 +183,8 @@ sing_box_cm_add_https_dns_server() { local server_port="$4" local path="$5" local headers="$6" + local domain_resolver="$7" + local detour="$6" echo "$config" | jq \ --arg tag "$tag" \ @@ -168,6 +192,8 @@ sing_box_cm_add_https_dns_server() { --arg server_port "$server_port" \ --arg path "$path" \ --arg headers "$headers" \ + --arg domain_resolver "$domain_resolver" \ + --arg detour "$detour" \ '.dns.servers += [( { type: "https", @@ -177,6 +203,8 @@ sing_box_cm_add_https_dns_server() { } + (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) )]' } @@ -1204,4 +1232,4 @@ sing_box_cm_save_config_to_file() { --arg tag "$SERVICE_TAG" \ 'walk(if type == "object" then del(.[$tag]) else . end)' \ > "$file_path" -} \ No newline at end of file +} From f07d90a5244dc8c71203cd804ca7753c1bdec458 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 12:10:05 +0500 Subject: [PATCH 05/58] refactor: intermediate refactoring commit --- .gitignore | 1 + .../resources/view/podkop/additionalTab.js | 1 + .../resources/view/podkop/configSection.js | 46 +- podkop/files/usr/bin/podkop | 1861 +++++------------ podkop/files/usr/lib/constants.sh | 27 + podkop/files/usr/lib/helpers.sh | 178 ++ .../files/usr/lib/sing_box_config_facade.sh | 210 ++ .../files/usr/lib/sing_box_config_manager.sh | 56 +- 8 files changed, 975 insertions(+), 1405 deletions(-) create mode 100644 .gitignore create mode 100644 podkop/files/usr/lib/constants.sh create mode 100644 podkop/files/usr/lib/helpers.sh create mode 100644 podkop/files/usr/lib/sing_box_config_facade.sh 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/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..00fe10e 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 @@ -220,6 +220,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..a6b20c7 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,18 @@ 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_list_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_list', _('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_list_enabled', '1'); o.rmempty = false; o.ucisection = s.section; @@ -302,7 +302,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_domains_list_type', _('User Domain List Type'), _('Select how to add your custom domains')); o.value('disabled', _('Disabled')); o.value('dynamic', _('Dynamic List')); o.value('text', _('Text List')); @@ -310,9 +310,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_domains_list_type', 'dynamic'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -324,9 +324,10 @@ 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 //')); + // TODO: Is it possible to save not as an option (but as a split list)? + o = s.taboption('basic', form.TextValue, 'user_domains', _('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_domains_list_type', 'text'); o.rows = 8; o.rmempty = false; o.ucisection = s.section; @@ -365,14 +366,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_domains_list_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_domains_list', _('Local Domain Lists Path'), _('Enter the list file path')); o.placeholder = '/path/file.lst'; - o.depends('custom_local_domains_list_enabled', '1'); + o.depends('local_domains_list_enabled', '1'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -384,14 +385,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_domains_list_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_domains_list', _('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_domains_list_enabled', '1'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -399,7 +400,7 @@ 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.ListValue, 'user_subnets_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 +408,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_subnets_list_type', 'dynamic'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -432,9 +433,10 @@ 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 //')); + // TODO: Is it possible to save not as an option (but as a split list)? + o = s.taboption('basic', form.TextValue, 'user_subnets', _('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_subnets_list_type', 'text'); o.rows = 10; o.rmempty = false; o.ucisection = s.section; @@ -489,14 +491,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_subnets_list_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_subnets_list', _('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_subnets_list_enabled', '1'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 79bd5d2..54447b8 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1,8 +1,12 @@ #!/bin/ash -# shellcheck shell=dash [ -r /lib/functions.sh ] && . /lib/functions.sh [ -r /lib/config/uci.sh ] && . /lib/config/uci.sh +PODKOP_LIB="/usr/lib/podkop" +. "$PODKOP_LIB/constants.sh" +. "$PODKOP_LIB/helpers.sh" +. "$PODKOP_LIB/sing_box_config_manager.sh" +. "$PODKOP_LIB/sing_box_config_facade.sh" config_load "/etc/config/podkop" @@ -21,10 +25,9 @@ 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" +CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi" TEST_DOMAIN="fakeip.podkop.fyi" INTERFACES_LIST="" SRC_INTERFACE="" @@ -40,26 +43,34 @@ COLOR_RESET="\033[0m" log() { local message="$1" - local timestamp=$(date +"%Y-%m-%d %H:%M:%S") + local module="$2" + local level="$3" - logger -t "podkop" "$timestamp $message" + if [ "$level" == "" ]; then + level="info" + fi + + logger -t "podkop" "[$level] [$module] $message" } nolog() { local message="$1" - local timestamp=$(date +"%Y-%m-%d %H:%M:%S") + 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" - log "$message" + local module="$2" + + log "$message" "$module" nolog "$message" } build_sing_box_config() { - cat > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG" + cat > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SB_CONFIG" } start_main() { @@ -70,12 +81,12 @@ start_main() { required_version="1.11.1" 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" "main" "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" "main" "warn" fi migration @@ -97,73 +108,48 @@ 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 - config_foreach add_cron_job - - 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 + sing_box_init_config + sing_box_config_check + # TODO(ampetelin): refactoring is needed +# config_foreach add_cron_job # need refactoring + /etc/init.d/sing-box start + log "Nice" "main" +# sing_box_inbound_proxy 1602 #refactored +# sing_box_dns #refactored +# sing_box_dns_rule_fakeip #refactored +# sing_box_rule_dns #refactored +# sing_box_create_bypass_ruleset #refactored +# sing_box_add_secure_dns_probe_domain #refactored +# sing_box_cache_file #refactored +# process_socks5 #refactored +# +# # sing-box outbounds and rules +# config_foreach sing_box_outdound #refactored +# config_foreach process_domains_for_section #refactored, implementation is needed +# config_foreach sing_box_rule_preset #refactored +# config_foreach process_domains_list_local #refactored, implementation is needed +# config_foreach process_subnet_for_section #refactored, implementation is needed +# config_foreach configure_community_lists #refactored +# config_foreach configure_remote_domain_lists #refactored +# config_foreach configure_remote_subnet_lists #refactored +# config_foreach process_all_traffic_for_section #refactored + 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 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" + # TODO(ampetelin): refactoring is needed +# 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" @@ -213,6 +199,7 @@ stop_main() { } stop() { + local dont_touch_dhcp config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0" if [ "$dont_touch_dhcp" -eq 0 ]; then dnsmasq_restore @@ -285,6 +272,26 @@ migration() { # corntab init.d (crontab -l | grep -v "/etc/init.d/podkop list_update") | crontab - + + migrate_config_key "$CONFIG" "option" "domain_list_enabled" "community_list_enabled" + migrate_config_key "$CONFIG" "list" "domain_list" "community_list" + + migrate_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domains_list_type" + migrate_config_key "$CONFIG" "option" "custom_domains_text" "user_domains" + migrate_config_key "$CONFIG" "list" "custom_domains" "user_domains" + + migrate_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnets_list_type" + migrate_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets" + migrate_config_key "$CONFIG" "list" "custom_subnets" "user_subnets" + + migrate_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domains_list_enabled" + migrate_config_key "$CONFIG" "list" "custom_local_domains" "local_domains_list" + + migrate_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domains_list_enabled" + migrate_config_key "$CONFIG" "list" "custom_download_domains" "remote_domains_list" + + migrate_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnets_list_enabled" + migrate_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnets_list" } validate_service() { @@ -301,7 +308,8 @@ 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 @@ -455,7 +463,8 @@ dnsmasq_add_resolver() { dnsmasq_restore() { log "Removing configuration for dnsmasq" - local cachesize=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null) + local cachesize noresolv server + 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 @@ -463,7 +472,7 @@ dnsmasq_restore() { uci set dhcp.@dnsmasq[0].cachesize="$cachesize" fi - local noresolv=$(uci get dhcp.@dnsmasq[0].podkop_noresolv 2>/dev/null) + 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 @@ -471,7 +480,7 @@ dnsmasq_restore() { uci set dhcp.@dnsmasq[0].noresolv="$noresolv" fi - local server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null) + 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 @@ -488,49 +497,7 @@ dnsmasq_restore() { /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" -} - +# TODO(ampetelin): refactoring is needed add_cron_job() { ## Future: make a check so that it doesn't recreate many times config_get domain_list_enabled "$section" "domain_list_enabled" @@ -577,36 +544,7 @@ 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 -} - +# TODO(ampetelin): refactoring is needed list_update() { echolog "🔄 Starting lists update..." @@ -663,7 +601,6 @@ 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 echo "$resolver" @@ -693,783 +630,447 @@ sing_box_uci() { # 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" + 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 - 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 + # TODO: remove after refactoring + nolog "$config" + + sing_box_cm_save_config_to_file "$config" "$SB_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" "sing-box" - 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" "sing-box" - 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" "sing-box" - 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" "sing-box" + 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" "sing-box" + 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." "sing-box" "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" "sing-box" + 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." "sing-box" "fatal" + exit 1 + ;; + esac ;; - "block") - log "Block mode" + vpn) + log "Configuring outbound in VPN connection mode for the $section section" "sing-box" + local interface_name + config_get interface_name "$section" "interface" + + if [ -z "$interface_name" ]; then + log "VPN interface is not set. Aborted." "sing-box" "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)" "sing-box" + # TODO(ampetelin): Надо не забыть ;; *) - log "Requires *vpn* or *proxy* value" - return + log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "sing-box" "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" "sing-box" + 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) + + local dns_type dns_server split_dns_type split_dns_server + 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" + + local need_dns_domain_resolver=0 + if ! is_ipv4 "$dns_server" || ! is_ipv4 "$split_dns_server"; then + need_dns_domain_resolver=1 + fi + + log "Adding DNS Servers" "sing-box" + config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$FAKEIP") + + 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..." "sing-box" + dns_domain_resolver=$(find_working_resolver) + if [ -z "$dns_domain_resolver" ]; then + log "Working DNS server not found, using default DNS server" "sing-box" + dns_domain_resolver="1.1.1.1" + else + log "Working DNS server has been found: $dns_domain_resolver" "sing-box" + fi + config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$dns_domain_resolver" 53) + fi + + config=$( + sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "" "" \ + "$SB_DNS_DOMAIN_RESOLVER_TAG" "$SB_DIRECT_OUTBOUND_TAG" + ) + + 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" \ + "" "" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$SB_MAIN_OUTBOUND_TAG" + ) + fi + + log "Adding DNS Rules" "sing-box" + 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 "$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_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 +sing_box_configure_route() { + log "Configure the route section of a sing-box JSON configuration" "sing-box" + + 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 + 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") + + config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns") + + local quic_disable + config_get_bool quic_disable "main" "quic_disable" 0 + if [ "$quic_disable" -eq 1 ]; then + config=$(sing_box_cm_add_reject_route_rule "$config" "protocol" "quic") + fi + + 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" "$TEST_DOMAIN" 8443) + + config_foreach include_source_ips_in_routing_handler + + 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 +} + +include_source_ips_in_routing_handler() { + local section="$1" + + 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 +} + +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") +} + +exclude_source_ip_from_routing_handler() { + local source_ip="$1" + local rule_tag="$2" + + config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip") +} + +configure_routing_for_section_lists() { + local section="$1" + local community_list_enabled local_domains_list_enabled remote_domains_list_enabled remote_subnets_list_enabled + local user_domains_list_type user_subnets_list_type route_rule_tag + config_get_bool community_list_enabled "$section" "community_list_enabled" 0 + config_get user_domains_list_type "$section" "user_domains_list_type" "disabled" + config_get_bool local_domains_list_enabled "$section" "local_domains_list_enabled" 0 + config_get_bool remote_domains_list_enabled "$section" "remote_domains_list_enabled" 0 + config_get user_subnets_list_type "$section" "user_subnets_list_type" "disabled" + config_get_bool remote_subnets_list_enabled "$section" "remote_subnets_list_enabled" 0 + + if [ "$community_list_enabled" -eq 0 ] && \ + [ "$user_domains_list_type" == "disabled" ] && \ + [ "$local_domains_list_enabled" -eq 0 ] && \ + [ "$remote_domains_list_enabled" -eq 0 ] && \ + [ "$user_subnets_list_type" == "disabled" ] && \ + [ "$remote_subnets_list_enabled" == 0 ] ; then + log "Section $section does not have any enabled list, skipping..." "sing-box" "warn" + return 0 + fi + + 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") + + if [ "$community_list_enabled" -eq 1 ]; then + log "Processing community list routing rules for $section section" "sing-box" + config_list_foreach "$section" "community_list" configure_community_list_handler "$section" "$route_rule_tag" + fi + + if [ "$user_domains_list_type" != "disabled" ]; then + log "Processing user domains routing rules for $section section" "sing-box" + # TODO(ampetelin): it is necessary to implement + # configure_user_domains_list_handler + fi + + if [ "$local_domains_list_enabled" -eq 1 ]; then + log "Processing local domains routing rules for $section section" "sing-box" + # TODO(ampetelin): it is necessary to implement + # configure_local_domains_list_handler "$section" "$route_rule_tag" + fi + + if [ "$remote_domains_list_enabled" -eq 1 ]; then + log "Processing local domains routing rules for $section section" "sing-box" + config_list_foreach "$section" "remote_domains_list" configure_remote_domains_or_subnets_list_handler \ + "domains" "$section" "$route_rule_tag" + fi + + if [ "$user_subnets_list_type" != "disabled" ]; then + log "Processing user subnets routing rules for $section section" "sing-box" + # TODO(ampetelin): it is necessary to implement + # configure_user_subnets_list_handler + fi + + if [ "$remote_subnets_list_enabled" -eq 1 ]; then + log "Processing remote subnets routing rules for $section section" "sing-box" + config_list_foreach "$section" "remote_subnets_list" configure_remote_domains_or_subnets_list_handler \ + "subnets" "$section" "$route_rule_tag" + fi +} + +configure_community_list_handler() { + local tag="$1" + local section="$2" + local route_rule_tag="$3" + + local rule_set_tag format url update_interval detour + rule_set_tag="$(get_rule_set_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" "$rule_set_tag" "$format" "$url" "$detour" "$update_interval") + _add_rule_set_to_dns_rules "$rule_set_tag" + config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag") +} + +configure_user_domains_list_handler() { + local section="$1" + # TODO(ampetelin): it is necessary to implement +} + +configure_local_domains_list_handler() { + local section="$1" + # TODO(ampetelin): it is necessary to implement +} + +configure_remote_domains_or_subnets_list_handler() { + local url="$1" + local type="$2" + local section="$3" + local route_rule_tag="$4" + + 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" "debug" + + local basename rule_set_tag format detour update_interval + basename=$(url_get_basename "$url") + rule_set_tag=$(get_rule_set_tag "$section" "$basename" "remote-$type") + format="$(get_rule_set_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" "$rule_set_tag" "$format" "$url" "$update_interval") + config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag") + + case "$type" in + domains) _add_rule_set_to_dns_rules "$rule_set_tag" "$route_rule_tag" ;; + subnets) ;; + *) log "Unsupported remote rule set type: $type" "sing-box" "warn" ;; + esac + ;; + *) + log "Detected file extension: .$file_extension → no processing needed, managed on list_update" "sing-box" + # TODO(ampetelin): create rule set here? + ;; + esac +} + +configure_user_subnets_list_handler() { + local section="$1" + # TODO(ampetelin): it is necessary to implement +} + +_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 +} + +_add_rule_set_to_dns_rules() { + local rule_set_tag="$1" + + config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$rule_set_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" "$rule_set_tag") + fi +} + +sing_box_configure_experimental() { + log "Configure the experimental section of a sing-box JSON configuration" "sing-box" + + log "Configuring cache database" "sing-box" + local cache_file + config_get cache_file "main" "cache_file" "/tmp/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)" "sing-box" + 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." "sing-box" + fi +} + +sing_box_additional_inbounds() { + log "Configure the additional inbounds of a sing-box JSON configuration" "sing-box" + + 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_config_check() { - if ! sing-box -c $SING_BOX_CONFIG check >/dev/null 2>&1; then - log "[critical] Sing-box configuration is invalid" + if ! sing-box -c $SB_CONFIG check >/dev/null 2>&1; then + log "Sing-box configuration is invalid" "sing-box" "[fatal]" 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 - fi - - local server=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f1) - local port=$(echo "$STRING" | sed -n 's|.*:\([0-9]\+\).*|\1|p') - - 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] - else - . - end - ' "$SING_BOX_CONFIG" | build_sing_box_config - - log "$domain added to the list for tag $tag" - else - # If tag does not exist, add a new set of rules - jq \ - --arg tag "$tag" \ - --arg domain "$domain" \ - ' - .route.rule_set += [ - { - "tag": $tag, - "type": "inline", - "rules": [ - { - "domain_suffix": [$domain] - } - ] - } - ]' "$SING_BOX_CONFIG" | build_sing_box_config - - log "$domain added as a new rule set for tag $tag" - fi -} - -sing_box_ruleset_subnets() { - log "Configure ruleset domains in sing-box" - - local subnet=$1 - local tag=$2 - - # nft - nft add element inet PodkopTable podkop_subnets { $subnet } - - # Check if there is a route.rule_set for the specified tag - local tag_exists=$(jq -r --arg tag "$tag" ' - .route.rule_set[]? | select(.tag == $tag) | .tag - ' /etc/sing-box/config.json) - - # If tag exists, add the domain - if [[ -n "$tag_exists" ]]; then - jq \ - --arg tag "$tag" \ - --arg subnet "$subnet" \ - ' - .route.rule_set[] |= - if .tag == $tag then - .rules[0].ip_cidr += [$subnet] - else - . - end - ' "$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" - fi -} - sing_box_ruleset_domains_json() { local domain="$1" local section="$2" @@ -1496,349 +1097,6 @@ sing_box_ruleset_subnets_json() { 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" - fi -} - -####################################### -# 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() { - local section="$1" - - config_get custom_domains_list_type "$section" "custom_domains_list_type" "disabled" - - if [ "$custom_domains_list_type" != "disabled" ]; then - log "Adding a custom domains list for $section section" - if [ "$custom_domains_list_type" = "dynamic" ]; then - # Handle list domains from custom_domains - config_list_foreach "$section" custom_domains "sing_box_ruleset_domains" "$section" - elif [ "$custom_domains_list_type" = "text" ]; then - # Handle domains from text - config_get custom_domains_text "$section" "custom_domains_text" - process_domains_text "$custom_domains_text" "$section" - fi - fi -} - -sing_box_rules() { - log "Configure rule in sing-box" - local rule_set="$1" - local outbound="$2" - - config_get mode "$section" "mode" - - 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") - - 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 - 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") - - 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 - 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() { - local section="$1" - - config_get custom_local_domains_list_enabled "$section" "custom_local_domains_list_enabled" - if [ "$custom_local_domains_list_enabled" -eq 1 ]; then - log "Adding a custom domains list from file in $section" - config_list_foreach "$section" "custom_local_domains" list_custom_local_domains_create "$section" - fi -} - -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 - 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 section="$2" - - log "Configuring remote domain 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" "domains" - ;; - *) - log "Detected file extension: .$file_extension → no processing needed, managed on list_update" - ;; - esac -} - -configure_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 "Configuring remote subnet lists for $section section" - config_list_foreach "$section" custom_download_subnets configure_remote_subnet_list_handler "$section" - fi -} - -configure_remote_subnet_list_handler() { - local url="$1" - local section="$2" - - 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" - ;; - esac -} - -process_all_traffic_for_section() { - local section="$1" - - config_get all_traffic_from_ip_enabled "$section" "all_traffic_from_ip_enabled" - if [ "$all_traffic_from_ip_enabled" -eq "1" ]; then - log "Adding an IP to redirect all traffic" - config_list_foreach $section all_traffic_ip nft_list_all_traffic_from_ip - config_list_foreach $section all_traffic_ip sing_box_rules_source_ip_cidr $all_traffic_ip $section - fi -} - import_community_subnet_lists() { config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0" if [ "$domain_list_enabled" -eq 1 ]; then @@ -2054,58 +1312,6 @@ decompile_srs_file() { 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 - 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 - fi -} - -detour_mixed() { - local section="main" - local port="4534" - local tag="detour" - - log "Adding detour Socks5 for $section on port $port" - - 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 -} - ## nftables nft_list_all_traffic_from_ip() { local ip="$1" @@ -2135,14 +1341,14 @@ check_proxy() { return 1 fi - if [ ! -f $SING_BOX_CONFIG ]; then + if [ ! -f $SB_CONFIG ]; 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 $SB_CONFIG check >/dev/null; then nolog "Invalid configuration" return 1 fi @@ -2170,7 +1376,7 @@ check_proxy() { else . end ) else . end - )' $SING_BOX_CONFIG + )' $SB_CONFIG nolog "Checking proxy connection..." @@ -2347,6 +1553,7 @@ check_sing_box_logs() { echo "$logs" } +# TODO(ampetelin): need fix after refactoring check_fakeip() { # Not used nolog "Checking fakeip functionality..." @@ -2395,14 +1602,14 @@ check_fakeip() { 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") + if [ -f "$SB_CONFIG" ]; then + local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SB_CONFIG") + local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SB_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") + local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SB_CONFIG") nolog "FakeIP domain: $dns_rules" else nolog "sing-box config file not found" @@ -2439,7 +1646,7 @@ check_logs() { show_sing_box_config() { nolog "Current sing-box configuration:" - if [ ! -f "$SING_BOX_CONFIG" ]; then + if [ ! -f "$SB_CONFIG" ]; then nolog "Configuration file not found" return 1 fi @@ -2467,7 +1674,7 @@ show_sing_box_config() { else . end ) else . end - )' "$SING_BOX_CONFIG" + )' "$SB_CONFIG" } show_config() { @@ -2662,35 +1869,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" @@ -2835,10 +2013,10 @@ global_check() { 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") + if [ -f "$SB_CONFIG" ]; then + local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SB_CONFIG") + local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SB_CONFIG") + local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SB_CONFIG") print_global " 📦 FakeIP enabled: $fakeip_enabled" print_global " 📦 FakeIP range: $fakeip_range" @@ -2850,6 +2028,7 @@ global_check() { fi } +# TODO: create helper functon # Download URL content directly download_to_stream() { local url="$1" @@ -2862,6 +2041,7 @@ download_to_stream() { fi } +# TODO: create helper functon # Download URL to temporary file download_to_tempfile() { local url="$1" @@ -2880,57 +2060,6 @@ download_to_tempfile() { 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 diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh new file mode 100644 index 0000000..ee60ebf --- /dev/null +++ b/podkop/files/usr/lib/constants.sh @@ -0,0 +1,27 @@ +SB_CONFIG="/etc/sing-box/config.json" +# 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" +FAKEIP="198.18.0.0/15" # TODO(ampetelin): renaming is needed +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" +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" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh new file mode 100644 index 0000000..3de5bfc --- /dev/null +++ b/podkop/files/usr/lib/helpers.sh @@ -0,0 +1,178 @@ +# 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 ]] +} + +# 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 +} + +# Extracts and returns the file extension from the given URL +get_url_file_extension() { + local url="$1" + local file_extension="${url##*.}" + + echo "$file_extension" +} + +# 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_rule_set_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_rule_set_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 's#^[^:]*://\([^@]*\)@.*#\1#p' +} + +# Extracts the host part from a URL +url_get_host() { + local url="$1" + echo "$url" | sed -n 's#^[^:]*://[^@]*@\([^:/?#]*\).*#\1#p' +} + +# Extracts the port number from a URL +url_get_port() { + local url="$1" + echo "$url" | sed -n 's#^[^:]*://[^@]*@[^:/?#]*:\([0-9]*\).*#\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" +} + +# 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 +} + +# Migrates a configuration key in an OpenWrt config file from old_key_name to new_key_name +migrate_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" "migration" + sed -i "s/$key_type $old_key_name/$key_type $new_key_name/g" "$config" + fi +} 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..c76ce25 --- /dev/null +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -0,0 +1,210 @@ +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_address="$4" + local path="$5" + local headers="$6" + local domain_resolver="$7" + local detour="$8" + + case "$type" in + udp) + config=$(sing_box_cm_add_udp_dns_server "$config" "$tag" "$server_address" 53 "$domain_resolver" "$detour") + ;; + dot) + config=$(sing_box_cm_add_tls_dns_server "$config" "$tag" "$server_address" 853 "$domain_resolver" "$detour") + ;; + doh) + config=$( + sing_box_cm_add_https_dns_server "$config" "$tag" "$server_address" 443 "$path" "$headers" \ + "$domain_resolver" "$detour" + ) + ;; + *) + log "Unsupported DNS server type: $type" "sing-box" + 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 + 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") + + config=$(sing_box_cm_add_vless_outbound "$config" "$tag" "$host" "$port" "$uuid" "$flow") + + 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." "sing-box" "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." "sing-box" "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" "sing-box" + 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_remote_ruleset_with_dns_and_route_rule() { + local config="$1" +} diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 565203b..0cb3e0d 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -1,4 +1,3 @@ -#!/bin/ash # # Module: sing_box_config_manager.sh # @@ -272,9 +271,9 @@ sing_box_cm_add_dns_route_rule() { # 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") # 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"') +# 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" @@ -282,12 +281,14 @@ sing_box_cm_patch_dns_route_rule() { 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; + 'import "helpers" as h {"search": "/usr/lib/podkop"}; .dns.rules |= map( if .[$service_tag] == $tag then if has($key) then @@ -310,13 +311,15 @@ sing_box_cm_patch_dns_route_rule() { # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_add_dns_reject_rule "$CONFIG" "query_type" '"HTTPS"') +# 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" \ @@ -760,7 +763,7 @@ sing_box_cm_set_vless_tls() { + (if $insecure == "true" then {insecure: true} else {} end) + (if $alpn != null then {alpn: $alpn} else {} end) + (if $utls_fingerprint != "" then { - ults: { + utls: { enabled: true, fingerprint: $utls_fingerprint } @@ -902,7 +905,7 @@ sing_box_cm_add_route_rule() { # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_patch_route_rule "$CONFIG" "main-route-rule" "rule_set" '"inline-ruleset"') +# CONFIG=$(sing_box_cm_patch_route_rule "$CONFIG" "main-route-rule" "rule_set" "inline-ruleset") ####################################### sing_box_cm_patch_route_rule() { local config="$1" @@ -910,12 +913,14 @@ sing_box_cm_patch_route_rule() { 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; + 'import "helpers" as h {"search": "/usr/lib/podkop"}; .route.rules |= map( if .[$service_tag] == $tag then if has($key) then @@ -938,13 +943,15 @@ sing_box_cm_patch_route_rule() { # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_add_reject_route_rule "$CONFIG" "protocol" '"quic"') +# CONFIG=$(sing_box_cm_add_reject_route_rule "$CONFIG" "protocol" "quic") ####################################### sing_box_cm_add_reject_route_rule() { local config="$1" local key="$2" local value="$3" + value=$(_normalize_arg "$value") + echo "$config" | jq \ --arg key "$key" \ --argjson value "$value" \ @@ -963,13 +970,15 @@ sing_box_cm_add_reject_route_rule() { # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_add_hijack_dns_route_rule "$CONFIG" "protocol" '"dns"') +# 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" \ @@ -1011,7 +1020,7 @@ sing_box_cm_add_options_route_rule() { # 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") # CONFIG=$(sing_box_cm_sniff_route_rule "$CONFIG" "inbound" '["tproxy-in","dns-in"]') ####################################### sing_box_cm_sniff_route_rule() { @@ -1019,6 +1028,8 @@ sing_box_cm_sniff_route_rule() { local key="$2" local value="$3" + value=$(_normalize_arg "$value") + echo "$config" | jq \ --arg key "$key" \ --argjson value "$value" \ @@ -1060,9 +1071,9 @@ sing_box_cm_add_inline_ruleset() { # 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"') +# 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" @@ -1070,11 +1081,13 @@ sing_box_cm_add_inline_ruleset_rule() { local key="$3" local value="$4" - echo "$config" | jq \ + value=$(_normalize_arg "$value") + + echo "$config" | jq -L /usr/lib/podkop \ --arg tag "$tag" \ --arg key "$key" \ --argjson value "$value" \ - 'import "helpers" as h; + 'import "helpers" as h {"search": "/usr/lib/podkop"}; .route.rule_set |= map( if .tag == $tag then if has($key) then @@ -1232,4 +1245,13 @@ sing_box_cm_save_config_to_file() { --arg tag "$SERVICE_TAG" \ 'walk(if type == "object" then del(.[$tag]) else . end)' \ > "$file_path" -} \ No newline at end of file +} + +_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 +} From 9b182a3045f177764f47345b8a9b4632ea660844 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 12:28:38 +0500 Subject: [PATCH 06/58] fix: fix detour parameter for remote ruleset --- podkop/files/usr/bin/podkop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 54447b8..aae2ad2 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -977,7 +977,7 @@ configure_remote_domains_or_subnets_list_handler() { detour="$(_get_download_detour_tag)" config_get update_interval "main" "update_interval" "1d" - config=$(sing_box_cm_add_remote_ruleset "$config" "$rule_set_tag" "$format" "$url" "$update_interval") + config=$(sing_box_cm_add_remote_ruleset "$config" "$rule_set_tag" "$format" "$url" "$detour" "$update_interval") config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag") case "$type" in From 2794cad5330e709b7e88ab636b79652880672b6d Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 12:35:53 +0500 Subject: [PATCH 07/58] fix: Remove direct outbound from DNS server configuration to prevent invalid detour --- podkop/files/usr/bin/podkop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index aae2ad2..ba8ba82 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -774,7 +774,7 @@ sing_box_configure_dns() { config=$( sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "" "" \ - "$SB_DNS_DOMAIN_RESOLVER_TAG" "$SB_DIRECT_OUTBOUND_TAG" + "$SB_DNS_DOMAIN_RESOLVER_TAG" ) if [ "$split_dns_enabled" -eq 1 ]; then From 12fc6bd9ac147962acda06816da96b13a20298fe Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 16:43:39 +0500 Subject: [PATCH 08/58] chore: undo renaming of taboption classarg --- .../htdocs/luci-static/resources/view/podkop/configSection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 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 a6b20c7..8a0d292 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 @@ -325,7 +325,7 @@ function createConfigSection(section, map, network) { }; // TODO: Is it possible to save not as an option (but as a split list)? - o = s.taboption('basic', form.TextValue, 'user_domains', _('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('user_domains_list_type', 'text'); o.rows = 8; @@ -434,7 +434,7 @@ function createConfigSection(section, map, network) { }; // TODO: Is it possible to save not as an option (but as a split list)? - o = s.taboption('basic', form.TextValue, 'user_subnets', _('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('user_subnets_list_type', 'text'); o.rows = 10; From cb4e3036be7e59a82c218840a7d427708894238a Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 18:00:28 +0500 Subject: [PATCH 09/58] chore: Remove module logging from log function --- podkop/files/usr/bin/podkop | 86 +++++++++---------- .../files/usr/lib/sing_box_config_facade.sh | 8 +- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index ba8ba82..3a26e03 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -43,14 +43,13 @@ COLOR_RESET="\033[0m" log() { local message="$1" - local module="$2" - local level="$3" + local level="$2" if [ "$level" == "" ]; then level="info" fi - logger -t "podkop" "[$level] [$module] $message" + logger -t "podkop" "[$level] $message" } nolog() { @@ -81,12 +80,12 @@ start_main() { required_version="1.11.1" if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then - 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" "main" "critical" + 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 "Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp" "main" "warn" + log "Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp" "warn" fi migration @@ -113,8 +112,6 @@ start_main() { # TODO(ampetelin): refactoring is needed # config_foreach add_cron_job # need refactoring /etc/init.d/sing-box start - log "Nice" "main" - # sing_box_inbound_proxy 1602 #refactored # sing_box_dns #refactored # sing_box_dns_rule_fakeip #refactored @@ -144,6 +141,7 @@ start_main() { # TODO(ampetelin): refactoring is needed # list_update & # echo $! > /var/run/podkop_list_update.pid + log "Nice" } start() { @@ -277,11 +275,11 @@ migration() { migrate_config_key "$CONFIG" "list" "domain_list" "community_list" migrate_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domains_list_type" - migrate_config_key "$CONFIG" "option" "custom_domains_text" "user_domains" + migrate_config_key "$CONFIG" "option" "custom_domains_text" "user_domains_text" migrate_config_key "$CONFIG" "list" "custom_domains" "user_domains" migrate_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnets_list_type" - migrate_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets" + migrate_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets_text" migrate_config_key "$CONFIG" "list" "custom_subnets" "user_subnets" migrate_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domains_list_enabled" @@ -648,13 +646,13 @@ sing_box_init_config() { } sing_box_configure_log() { - log "Configure the log section of a sing-box JSON configuration" "sing-box" + log "Configure the log section of a sing-box JSON configuration" config=$(sing_box_cm_configure_log "$config" false "$SB_DEFAULT_LOG_LEVEL" false) } sing_box_configure_inbounds() { - log "Configure the inbounds section of a sing-box JSON configuration" "sing-box" + log "Configure the inbounds section of a sing-box JSON configuration" config=$( sing_box_cm_add_tproxy_inbound \ @@ -666,7 +664,7 @@ sing_box_configure_inbounds() { } sing_box_configure_outbounds() { - log "Configure the outbounds section of a sing-box JSON configuration" "sing-box" + log "Configure the outbounds section of a sing-box JSON configuration" config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG") @@ -680,13 +678,13 @@ configure_outbound_handler() { config_get connection_mode "$section" "mode" case "$connection_mode" in proxy) - log "Configuring outbound in proxy connection mode for the $section section" "sing-box" + log "Configuring outbound in proxy connection mode for the $section section" local proxy_config_type config_get proxy_config_type "$section" "proxy_config_type" case "$proxy_config_type" in url) - log "Detected proxy configuration type: url" "sing-box" + 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" @@ -694,48 +692,48 @@ configure_outbound_handler() { active_proxy_string=$(echo "$proxy_string" | grep -v "^[[:space:]]*\/\/" | head -n 1) if [ -z "$active_proxy_string" ]; then - log "Proxy string is not set. Aborted." "sing-box" "fatal" + log "Proxy string is not set. Aborted." "fatal" exit 1 fi config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$active_proxy_string" "$udp_over_tcp") ;; outbound) - log "Detected proxy configuration type: outbound" "sing-box" + 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." "sing-box" "fatal" + log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal" exit 1 ;; esac ;; vpn) - log "Configuring outbound in VPN connection mode for the $section section" "sing-box" + 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." "sing-box" "fatal" + 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)" "sing-box" + log "Connection mode 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" # TODO(ampetelin): Надо не забыть ;; *) - log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "sing-box" "fatal" + log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "fatal" exit 1 ;; esac } sing_box_configure_dns() { - log "Configure the DNS section of a sing-box JSON configuration" "sing-box" + 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 @@ -756,18 +754,18 @@ sing_box_configure_dns() { need_dns_domain_resolver=1 fi - log "Adding DNS Servers" "sing-box" + log "Adding DNS Servers" config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$FAKEIP") 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..." "sing-box" + 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" "sing-box" + log "Working DNS server not found, using default DNS server" dns_domain_resolver="1.1.1.1" else - log "Working DNS server has been found: $dns_domain_resolver" "sing-box" + 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) fi @@ -784,7 +782,7 @@ sing_box_configure_dns() { ) fi - log "Adding DNS Rules" "sing-box" + log "Adding DNS Rules" local rewrite_ttl service_domains config_get rewrite_ttl "main" "dns_rewrite_ttl" "60" @@ -802,7 +800,7 @@ sing_box_configure_dns() { } sing_box_configure_route() { - log "Configure the route section of a sing-box JSON configuration" "sing-box" + 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") @@ -887,7 +885,7 @@ configure_routing_for_section_lists() { [ "$remote_domains_list_enabled" -eq 0 ] && \ [ "$user_subnets_list_type" == "disabled" ] && \ [ "$remote_subnets_list_enabled" == 0 ] ; then - log "Section $section does not have any enabled list, skipping..." "sing-box" "warn" + log "Section $section does not have any enabled list, skipping..." "warn" return 0 fi @@ -896,36 +894,36 @@ configure_routing_for_section_lists() { config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag") if [ "$community_list_enabled" -eq 1 ]; then - log "Processing community list routing rules for $section section" "sing-box" + log "Processing community list routing rules for $section section" config_list_foreach "$section" "community_list" configure_community_list_handler "$section" "$route_rule_tag" fi if [ "$user_domains_list_type" != "disabled" ]; then - log "Processing user domains routing rules for $section section" "sing-box" + log "Processing user domains routing rules for $section section" # TODO(ampetelin): it is necessary to implement # configure_user_domains_list_handler fi if [ "$local_domains_list_enabled" -eq 1 ]; then - log "Processing local domains routing rules for $section section" "sing-box" + log "Processing local domains routing rules for $section section" # TODO(ampetelin): it is necessary to implement # configure_local_domains_list_handler "$section" "$route_rule_tag" fi if [ "$remote_domains_list_enabled" -eq 1 ]; then - log "Processing local domains routing rules for $section section" "sing-box" + log "Processing local domains routing rules for $section section" config_list_foreach "$section" "remote_domains_list" configure_remote_domains_or_subnets_list_handler \ "domains" "$section" "$route_rule_tag" fi if [ "$user_subnets_list_type" != "disabled" ]; then - log "Processing user subnets routing rules for $section section" "sing-box" + log "Processing user subnets routing rules for $section section" # TODO(ampetelin): it is necessary to implement # configure_user_subnets_list_handler fi if [ "$remote_subnets_list_enabled" -eq 1 ]; then - log "Processing remote subnets routing rules for $section section" "sing-box" + log "Processing remote subnets routing rules for $section section" config_list_foreach "$section" "remote_subnets_list" configure_remote_domains_or_subnets_list_handler \ "subnets" "$section" "$route_rule_tag" fi @@ -968,7 +966,7 @@ configure_remote_domains_or_subnets_list_handler() { file_extension=$(get_url_file_extension "$url") case "$file_extension" in json|srs) - log "Detected file extension: .$file_extension → proceeding with processing" "sing-box" "debug" + log "Detected file extension: .$file_extension → proceeding with processing" "debug" local basename rule_set_tag format detour update_interval basename=$(url_get_basename "$url") @@ -983,11 +981,11 @@ configure_remote_domains_or_subnets_list_handler() { case "$type" in domains) _add_rule_set_to_dns_rules "$rule_set_tag" "$route_rule_tag" ;; subnets) ;; - *) log "Unsupported remote rule set type: $type" "sing-box" "warn" ;; + *) log "Unsupported remote rule set type: $type" "warn" ;; esac ;; *) - log "Detected file extension: .$file_extension → no processing needed, managed on list_update" "sing-box" + log "Detected file extension: .$file_extension → no processing needed, managed on list_update" # TODO(ampetelin): create rule set here? ;; esac @@ -1019,9 +1017,9 @@ _add_rule_set_to_dns_rules() { } sing_box_configure_experimental() { - log "Configure the experimental section of a sing-box JSON configuration" "sing-box" + log "Configure the experimental section of a sing-box JSON configuration" - log "Configuring cache database" "sing-box" + log "Configuring cache database" local cache_file config_get cache_file "main" "cache_file" "/tmp/cache.db" config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true) @@ -1029,17 +1027,17 @@ sing_box_configure_experimental() { local yacd_enabled config_get_bool yacd_enabled "main" "yacd" 0 if [ "$yacd_enabled" -eq 1 ]; then - log "Configuring Clash API (yacd)" "sing-box" + 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." "sing-box" + log "Clash API (yacd) is disabled, skipping configuration." fi } sing_box_additional_inbounds() { - log "Configure the additional inbounds of a sing-box JSON configuration" "sing-box" + log "Configure the additional inbounds of a sing-box JSON configuration" local mixed_inbound_enabled config_get_bool mixed_inbound_enabled "main" "socks5" 0 @@ -1066,7 +1064,7 @@ sing_box_additional_inbounds() { sing_box_config_check() { if ! sing-box -c $SB_CONFIG check >/dev/null 2>&1; then - log "Sing-box configuration is invalid" "sing-box" "[fatal]" + log "Sing-box configuration is invalid" "[fatal]" exit 1 fi } diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index c76ce25..2a42040 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -26,7 +26,7 @@ sing_box_cf_add_dns_server() { ) ;; *) - log "Unsupported DNS server type: $type" "sing-box" + log "Unsupported DNS server type: $type" exit 1 ;; esac @@ -84,7 +84,7 @@ sing_box_cf_add_proxy_outbound() { config=$(sing_box_cm_set_vless_grpc_transport "$config" "$tag") ;; *) - log "Unknown transport '$transport' detected." "sing-box" "error" + log "Unknown transport '$transport' detected." "error" ;; esac @@ -114,7 +114,7 @@ sing_box_cf_add_proxy_outbound() { ;; none) ;; *) - log "Unknown security '$security' detected." "sing-box" "error" + log "Unknown security '$security' detected." "error" ;; esac ;; @@ -145,7 +145,7 @@ sing_box_cf_add_proxy_outbound() { ) ;; *) - log "Unsupported proxy $scheme type" "sing-box" + log "Unsupported proxy $scheme type" exit 1 ;; esac From f70e2ac5577fe900b9a582133c4699f8a44d05a2 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 20:02:43 +0500 Subject: [PATCH 10/58] refactor: Simplify cron job logic and renaming variable --- podkop/files/usr/bin/podkop | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 3a26e03..8eeaca2 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -495,13 +495,12 @@ dnsmasq_restore() { /etc/init.d/dnsmasq restart } -# TODO(ampetelin): refactoring is needed 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_list_enabled remote_domains_list_enabled remote_subnets_list_enabled update_interval + config_get community_list_enabled "$section" "community_list_enabled" + config_get remote_domains_list_enabled "$section" "remote_domains_list_enabled" + config_get remote_subnets_list_enabled "$section" "remote_subnets_list_enabled" config_get update_interval "main" "update_interval" case "$update_interval" in @@ -526,8 +525,9 @@ add_cron_job() { ;; 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_list_enabled" -eq 1 ] || \ + [ "$remote_domains_list_enabled" -eq 1 ] || \ + [ "$remote_subnets_list_enabled" -eq 1 ]; then remove_cron_job crontab -l | { cat @@ -723,7 +723,6 @@ configure_outbound_handler() { ;; block) log "Connection mode 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" - # TODO(ampetelin): Надо не забыть ;; *) log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "fatal" @@ -827,6 +826,8 @@ sing_box_configure_route() { config=$(sing_box_cf_override_domain_port "$config" "$TEST_DOMAIN" 8443) config_foreach include_source_ips_in_routing_handler + # TODO(ampetelin): Add block rules + config_foreach local exclude_from_ip_enabled config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" 0 @@ -986,7 +987,6 @@ configure_remote_domains_or_subnets_list_handler() { ;; *) log "Detected file extension: .$file_extension → no processing needed, managed on list_update" - # TODO(ampetelin): create rule set here? ;; esac } From c7e21010bd6b22c4b0d95184e7d310a24fb16176 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 20:09:44 +0500 Subject: [PATCH 11/58] refactor: Add download helper functions to helpers.sh and remove from main script --- podkop/files/usr/bin/podkop | 32 -------------------------------- podkop/files/usr/lib/helpers.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 8eeaca2..c5e904a 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -2026,38 +2026,6 @@ global_check() { fi } -# TODO: create helper functon -# 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 -} - -# TODO: create helper functon -# 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 -} - show_help() { cat << EOF Usage: $0 COMMAND diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 3de5bfc..1d21ca4 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -176,3 +176,33 @@ migrate_config_key() { 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_url="${2}" + + if [ -n "$http_proxy_url" ]; then + http_proxy="$http_proxy_url" https_proxy="$http_proxy_url" 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" + local http_proxy_url="${3}" + + if [ -n "$http_proxy_url" ]; then + http_proxy="$http_proxy_url" https_proxy="$http_proxy_url" 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 +} \ No newline at end of file From 17c1d09aa82507ea682b1f68aa881c58b2fa7f02 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 4 Sep 2025 20:11:16 +0500 Subject: [PATCH 12/58] refactor: Enable cron job scheduling --- podkop/files/usr/bin/podkop | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index c5e904a..8b54c62 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -109,8 +109,7 @@ start_main() { # sing-box sing_box_init_config sing_box_config_check - # TODO(ampetelin): refactoring is needed -# config_foreach add_cron_job # need refactoring + config_foreach add_cron_job /etc/init.d/sing-box start # sing_box_inbound_proxy 1602 #refactored # sing_box_dns #refactored From acfc95e86d68ca03126899be8d05592eb6b579b7 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 5 Sep 2025 14:50:43 +0500 Subject: [PATCH 13/58] refactor: Refactoring configuration of local domain lists --- podkop/files/usr/bin/podkop | 97 ++++++++++++++----- podkop/files/usr/lib/constants.sh | 2 +- podkop/files/usr/lib/helpers.sh | 27 ++++-- .../files/usr/lib/sing_box_config_manager.sh | 26 ++++- 4 files changed, 115 insertions(+), 37 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 8b54c62..84ad889 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -32,6 +32,7 @@ TEST_DOMAIN="fakeip.podkop.fyi" INTERFACES_LIST="" SRC_INTERFACE="" RESOLV_CONF="/etc/resolv.conf" +TMP_FOLDER="/tmp/podkop" # Endpoints https://github.com/ampetelin/warp-endpoint-checker CLOUDFLARE_OCTETS="8.47 162.159 188.114" @@ -62,9 +63,9 @@ nolog() { echolog() { local message="$1" - local module="$2" + local level="$2" - log "$message" "$module" + log "$message" "$level" nolog "$message" } @@ -826,7 +827,6 @@ sing_box_configure_route() { config_foreach include_source_ips_in_routing_handler # TODO(ampetelin): Add block rules - config_foreach local exclude_from_ip_enabled config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" 0 @@ -906,12 +906,11 @@ configure_routing_for_section_lists() { if [ "$local_domains_list_enabled" -eq 1 ]; then log "Processing local domains routing rules for $section section" - # TODO(ampetelin): it is necessary to implement - # configure_local_domains_list_handler "$section" "$route_rule_tag" + configure_local_domains_lists "$section" "$route_rule_tag" fi if [ "$remote_domains_list_enabled" -eq 1 ]; then - log "Processing local domains routing rules for $section section" + log "Processing remote domains routing rules for $section section" config_list_foreach "$section" "remote_domains_list" configure_remote_domains_or_subnets_list_handler \ "domains" "$section" "$route_rule_tag" fi @@ -934,16 +933,16 @@ configure_community_list_handler() { local section="$2" local route_rule_tag="$3" - local rule_set_tag format url update_interval detour - rule_set_tag="$(get_rule_set_tag "$section" "$tag" "community")" + 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" "$rule_set_tag" "$format" "$url" "$detour" "$update_interval") - _add_rule_set_to_dns_rules "$rule_set_tag" - config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag") + 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_domains_list_handler() { @@ -951,9 +950,58 @@ configure_user_domains_list_handler() { # TODO(ampetelin): it is necessary to implement } -configure_local_domains_list_handler() { +configure_local_domains_lists() { local section="$1" - # TODO(ampetelin): it is necessary to implement + local route_rule_tag="$2" + + local ruleset_tag ruleset_filename ruleset_filepath + ruleset_tag="$(get_ruleset_tag "$section" "local" "domains")" + 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_domains_ruleset "$section" "$ruleset_filepath" +} + +import_local_domains_ruleset() { + local filepath="$1" + local section="$2" + local ruleset_filepath="$3" + + 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 + continue + fi + + if ! is_domain "$domain"; then + log "$domain is not domain" "debug" + continue + fi + + if [ -z "$domains" ]; then + domains="$domain" + else + domains="$domains,$domain" + fi + done < "$filepath" + + if [ -z "$domains" ]; then + log "No valid domains 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" } configure_remote_domains_or_subnets_list_handler() { @@ -968,24 +1016,24 @@ configure_remote_domains_or_subnets_list_handler() { json|srs) log "Detected file extension: .$file_extension → proceeding with processing" "debug" - local basename rule_set_tag format detour update_interval + local basename ruleset_tag format detour update_interval basename=$(url_get_basename "$url") - rule_set_tag=$(get_rule_set_tag "$section" "$basename" "remote-$type") - format="$(get_rule_set_format_by_file_extension "$file_extension")" + 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" "$rule_set_tag" "$format" "$url" "$detour" "$update_interval") - config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag") + 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_rule_set_to_dns_rules "$rule_set_tag" "$route_rule_tag" ;; + 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" + log "Detected file extension: .$file_extension → no processing needed, managed on list_update" "debug" ;; esac } @@ -1004,14 +1052,14 @@ _get_download_detour_tag() { fi } -_add_rule_set_to_dns_rules() { - local rule_set_tag="$1" +_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" "$rule_set_tag") + 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" "$rule_set_tag") + config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag") fi } @@ -1290,6 +1338,7 @@ import_subnets_from_remote_srs_file() { rm -f "$binary_filepath" "$json_filepath" } +## Support functions # Decompiles a sing-box SRS binary file into a JSON ruleset file decompile_srs_file() { local binary_filepath="$1" diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index ee60ebf..713e748 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,6 +1,6 @@ SB_CONFIG="/etc/sing-box/config.json" # Log -SB_DEFAULT_LOG_LEVEL="warn" +SB_DEFAULT_LOG_LEVEL="info" # DNS SB_DNS_SERVER_TAG="dns-server" SB_SPLIT_DNS_SERVER_TAG="split-dns-server" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 1d21ca4..b759ce4 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -2,21 +2,28 @@ is_ipv4() { local ip="$1" local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$" - [[ $ip =~ $regex ]] + [[ "$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 ]] + [[ "$ip" =~ $regex ]] +} + +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 is_base64() { local str="$1" - if echo "$str" | base64 -d > /dev/null 2>&1; then + if echo "$str" | base64 -d >/dev/null 2>&1; then return 0 fi return 1 @@ -58,10 +65,10 @@ get_outbound_tag_by_section() { } # Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix -get_rule_set_tag() { +get_ruleset_tag() { local section="$1" local name="$2" - local type="${3:-}" + local type="$3" local postfix="ruleset" if [ -n "$type" ]; then @@ -72,7 +79,7 @@ get_rule_set_tag() { } # Determines the ruleset format based on the file extension (json → source, srs → binary) -get_rule_set_format_by_file_extension() { +get_ruleset_format_by_file_extension() { local file_extension="$1" local format @@ -154,7 +161,7 @@ base64_decode() { local str="$1" local decoded_url - decoded_url="$(echo "$str" | base64 -d 2> /dev/null)" + decoded_url="$(echo "$str" | base64 -d 2>/dev/null)" echo "$decoded_url" } @@ -180,7 +187,7 @@ migrate_config_key() { # Download URL content directly download_to_stream() { local url="$1" - local http_proxy_url="${2}" + local http_proxy_url="$2" if [ -n "$http_proxy_url" ]; then http_proxy="$http_proxy_url" https_proxy="$http_proxy_url" wget -qO- "$url" | sed 's/\r$//' @@ -193,7 +200,7 @@ download_to_stream() { download_to_tempfile() { local url="$1" local filepath="$2" - local http_proxy_url="${3}" + local http_proxy_url="$3" if [ -n "$http_proxy_url" ]; then http_proxy="$http_proxy_url" https_proxy="$http_proxy_url" wget -O "$filepath" "$url" @@ -205,4 +212,4 @@ download_to_tempfile() { log "$filename has Windows line endings (CRLF). Converting to Unix (LF)" sed -i 's/\r$//' "$filepath" fi -} \ No newline at end of file +} diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 0cb3e0d..a959034 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -1227,6 +1227,28 @@ sing_box_cm_configure_clash_api() { }' } +sing_box_cm_create_local_source_ruleset() { + local filepath="$1" + + jq -n '{version: 3, rules: []}' > "$filepath" +} + +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): $value}]' > "$filepath" +} + ####################################### # Save a sing-box JSON configuration to a file, removing service-specific tags. # Arguments: @@ -1239,12 +1261,12 @@ sing_box_cm_configure_clash_api() { ####################################### sing_box_cm_save_config_to_file() { local config="$1" - local file_path="$2" + local filepath="$2" echo "$config" | jq \ --arg tag "$SERVICE_TAG" \ 'walk(if type == "object" then del(.[$tag]) else . end)' \ - > "$file_path" + > "$filepath" } _normalize_arg() { From da89c5c7df7dd03d49aa7eedb1353099b2417c7b Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 5 Sep 2025 15:02:24 +0500 Subject: [PATCH 14/58] refactor: rename parameters for migration --- podkop/files/etc/config/podkop | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index 2961f41..e3337ba 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -4,21 +4,20 @@ 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_list_enabled '1' + list community_list 'russia_inside' + option user_domains_list_type 'disabled' + #list user_domains '' + #option user_domains_text '' + option local_domains_list_enabled '0' + #list local_domains_list '' + option remote_domains_list_enabled '0' + #list remote_domains_list '' + option user_subnets_list_type 'disable' + #list user_subnets '' + #option user_subnets_text '' + option remote_subnets_list_enabled '0' + #list remote_subnets_list '' option all_traffic_from_ip_enabled '0' #list all_traffic_ip '' option exclude_from_ip_enabled '0' From d03167f49dfd51e63ee7f6b916418b44a5d33337 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 5 Sep 2025 16:49:05 +0500 Subject: [PATCH 15/58] chore: fix my perfect English --- .../resources/view/podkop/configSection.js | 37 +++--- podkop/files/usr/bin/podkop | 110 +++++++++--------- 2 files changed, 73 insertions(+), 74 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 8a0d292..f55aded 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, 'community_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, 'community_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('community_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, 'user_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')); @@ -312,7 +311,7 @@ function createConfigSection(section, map, network) { 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('user_domains_list_type', 'dynamic'); + o.depends('user_domain_list_type', 'dynamic'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -327,7 +326,7 @@ function createConfigSection(section, map, network) { // TODO: Is it possible to save not as an option (but as a split list)? 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('user_domains_list_type', 'text'); + o.depends('user_domain_list_type', 'text'); o.rows = 8; o.rmempty = false; o.ucisection = s.section; @@ -366,14 +365,14 @@ function createConfigSection(section, map, network) { return true; }; - o = s.taboption('basic', form.Flag, '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, 'local_domains_list', _('Local Domain Lists Path'), _('Enter the list file path')); + o = s.taboption('basic', form.DynamicList, 'local_domain_lists', _('Local Domain Lists Path'), _('Enter the list file path')); o.placeholder = '/path/file.lst'; - o.depends('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) { @@ -385,14 +384,14 @@ function createConfigSection(section, map, network) { return true; }; - o = s.taboption('basic', form.Flag, 'remote_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, 'remote_domains_list', _('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('remote_domains_list_enabled', '1'); + o.depends('remote_domain_lists_enabled', '1'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -400,7 +399,7 @@ function createConfigSection(section, map, network) { return validateUrl(value); }; - o = s.taboption('basic', form.ListValue, 'user_subnets_list_type', _('User Subnet List Type'), _('Select how to add your custom subnets')); + 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)')); @@ -410,7 +409,7 @@ function createConfigSection(section, map, network) { 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('user_subnets_list_type', 'dynamic'); + o.depends('user_subnet_list_type', 'dynamic'); o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { @@ -436,7 +435,7 @@ function createConfigSection(section, map, network) { // TODO: Is it possible to save not as an option (but as a split list)? 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('user_subnets_list_type', 'text'); + o.depends('user_subnet_list_type', 'text'); o.rows = 10; o.rmempty = false; o.ucisection = s.section; @@ -491,14 +490,14 @@ function createConfigSection(section, map, network) { return true; }; - o = s.taboption('basic', form.Flag, 'remote_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, 'remote_subnets_list', _('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('remote_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/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 84ad889..9bb2c4f 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -224,35 +224,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 @@ -271,25 +271,25 @@ migration() { # corntab init.d (crontab -l | grep -v "/etc/init.d/podkop list_update") | crontab - - migrate_config_key "$CONFIG" "option" "domain_list_enabled" "community_list_enabled" - migrate_config_key "$CONFIG" "list" "domain_list" "community_list" + migrate_config_key "$CONFIG" "option" "domain_list_enabled" "community_lists_enabled" + migrate_config_key "$CONFIG" "list" "domain_list" "community_lists" - migrate_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domains_list_type" + migrate_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domain_list_type" migrate_config_key "$CONFIG" "option" "custom_domains_text" "user_domains_text" migrate_config_key "$CONFIG" "list" "custom_domains" "user_domains" - migrate_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnets_list_type" + migrate_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnet_list_type" migrate_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets_text" migrate_config_key "$CONFIG" "list" "custom_subnets" "user_subnets" - migrate_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domains_list_enabled" - migrate_config_key "$CONFIG" "list" "custom_local_domains" "local_domains_list" + migrate_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domain_lists_enabled" + migrate_config_key "$CONFIG" "list" "custom_local_domains" "local_domain_lists" - migrate_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domains_list_enabled" - migrate_config_key "$CONFIG" "list" "custom_download_domains" "remote_domains_list" + migrate_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domain_lists_enabled" + migrate_config_key "$CONFIG" "list" "custom_download_domains" "remote_domain_lists" - migrate_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnets_list_enabled" - migrate_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnets_list" + migrate_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnet_lists_enabled" + migrate_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnet_lists" } validate_service() { @@ -497,10 +497,10 @@ dnsmasq_restore() { add_cron_job() { ## Future: make a check so that it doesn't recreate many times - local community_list_enabled remote_domains_list_enabled remote_subnets_list_enabled update_interval - config_get community_list_enabled "$section" "community_list_enabled" - config_get remote_domains_list_enabled "$section" "remote_domains_list_enabled" - config_get remote_subnets_list_enabled "$section" "remote_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 @@ -525,9 +525,9 @@ add_cron_job() { ;; esac - if [ "$community_list_enabled" -eq 1 ] || \ - [ "$remote_domains_list_enabled" -eq 1 ] || \ - [ "$remote_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 @@ -870,21 +870,21 @@ exclude_source_ip_from_routing_handler() { configure_routing_for_section_lists() { local section="$1" - local community_list_enabled local_domains_list_enabled remote_domains_list_enabled remote_subnets_list_enabled - local user_domains_list_type user_subnets_list_type route_rule_tag - config_get_bool community_list_enabled "$section" "community_list_enabled" 0 - config_get user_domains_list_type "$section" "user_domains_list_type" "disabled" - config_get_bool local_domains_list_enabled "$section" "local_domains_list_enabled" 0 - config_get_bool remote_domains_list_enabled "$section" "remote_domains_list_enabled" 0 - config_get user_subnets_list_type "$section" "user_subnets_list_type" "disabled" - config_get_bool remote_subnets_list_enabled "$section" "remote_subnets_list_enabled" 0 + local community_lists_enabled local_domain_lists_enabled remote_domain_lists_enabled remote_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 remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" 0 - if [ "$community_list_enabled" -eq 0 ] && \ - [ "$user_domains_list_type" == "disabled" ] && \ - [ "$local_domains_list_enabled" -eq 0 ] && \ - [ "$remote_domains_list_enabled" -eq 0 ] && \ - [ "$user_subnets_list_type" == "disabled" ] && \ - [ "$remote_subnets_list_enabled" == 0 ] ; then + if [ "$community_lists_enabled" -eq 0 ] && \ + [ "$user_domain_list_type" == "disabled" ] && \ + [ "$local_domain_lists_enabled" -eq 0 ] && \ + [ "$remote_domain_lists_enabled" -eq 0 ] && \ + [ "$user_subnet_list_type" == "disabled" ] && \ + [ "$remote_subnet_lists_enabled" == 0 ] ; then log "Section $section does not have any enabled list, skipping..." "warn" return 0 fi @@ -893,37 +893,37 @@ configure_routing_for_section_lists() { 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") - if [ "$community_list_enabled" -eq 1 ]; then + if [ "$community_lists_enabled" -eq 1 ]; then log "Processing community list routing rules for $section section" - config_list_foreach "$section" "community_list" configure_community_list_handler "$section" "$route_rule_tag" + config_list_foreach "$section" "community_lists" configure_community_list_handler "$section" "$route_rule_tag" fi - if [ "$user_domains_list_type" != "disabled" ]; then + if [ "$user_domain_list_type" != "disabled" ]; then log "Processing user domains routing rules for $section section" # TODO(ampetelin): it is necessary to implement - # configure_user_domains_list_handler + # configure_user_domain_list_handler fi - if [ "$local_domains_list_enabled" -eq 1 ]; then + if [ "$local_domain_lists_enabled" -eq 1 ]; then log "Processing local domains routing rules for $section section" - configure_local_domains_lists "$section" "$route_rule_tag" + configure_local_domain_lists "$section" "$route_rule_tag" fi - if [ "$remote_domains_list_enabled" -eq 1 ]; then + if [ "$remote_domain_lists_enabled" -eq 1 ]; then log "Processing remote domains routing rules for $section section" - config_list_foreach "$section" "remote_domains_list" configure_remote_domains_or_subnets_list_handler \ + config_list_foreach "$section" "remote_domain_lists" configure_remote_domain_or_subnet_list_handler \ "domains" "$section" "$route_rule_tag" fi - if [ "$user_subnets_list_type" != "disabled" ]; then + if [ "$user_subnet_list_type" != "disabled" ]; then log "Processing user subnets routing rules for $section section" # TODO(ampetelin): it is necessary to implement - # configure_user_subnets_list_handler + # configure_user_subnet_list_handler fi - if [ "$remote_subnets_list_enabled" -eq 1 ]; then + if [ "$remote_subnet_lists_enabled" -eq 1 ]; then log "Processing remote subnets routing rules for $section section" - config_list_foreach "$section" "remote_subnets_list" configure_remote_domains_or_subnets_list_handler \ + config_list_foreach "$section" "remote_subnet_lists" configure_remote_domain_or_subnet_list_handler \ "subnets" "$section" "$route_rule_tag" fi } @@ -945,12 +945,12 @@ configure_community_list_handler() { config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag") } -configure_user_domains_list_handler() { +configure_user_domain_list_handler() { local section="$1" # TODO(ampetelin): it is necessary to implement } -configure_local_domains_lists() { +configure_local_domain_lists() { local section="$1" local route_rule_tag="$2" @@ -964,10 +964,10 @@ configure_local_domains_lists() { 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_domains_ruleset "$section" "$ruleset_filepath" + config_list_foreach "$section" "local_domains_list" import_local_domain_list_to_ruleset "$section" "$ruleset_filepath" } -import_local_domains_ruleset() { +import_local_domain_list_to_ruleset() { local filepath="$1" local section="$2" local ruleset_filepath="$3" @@ -1004,7 +1004,7 @@ import_local_domains_ruleset() { sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$domains" } -configure_remote_domains_or_subnets_list_handler() { +configure_remote_domain_or_subnet_list_handler() { local url="$1" local type="$2" local section="$3" @@ -1038,7 +1038,7 @@ configure_remote_domains_or_subnets_list_handler() { esac } -configure_user_subnets_list_handler() { +configure_user_subnet_list_handler() { local section="$1" # TODO(ampetelin): it is necessary to implement } From 5273935d25bafa5ac6146bec8a2c65ab172402c4 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 5 Sep 2025 17:01:36 +0500 Subject: [PATCH 16/58] chore: fix my perfect English --- podkop/files/etc/config/podkop | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index e3337ba..ad2a35b 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -4,20 +4,20 @@ config main 'main' option proxy_config_type 'url' #option outbound_json '' option proxy_string '' - option community_list_enabled '1' - list community_list 'russia_inside' - option user_domains_list_type 'disabled' + 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_domains_list_enabled '0' - #list local_domains_list '' - option remote_domains_list_enabled '0' - #list remote_domains_list '' - option user_subnets_list_type 'disable' + 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 remote_subnets_list_enabled '0' - #list remote_subnets_list '' + 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' From 49836e4adcbbf00b007ccd4a6e4f262ec46c5487 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 5 Sep 2025 21:23:55 +0500 Subject: [PATCH 17/58] 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 From 9d861cf3e00ab2f6635f752eccfa5fde6df00ce5 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 7 Sep 2025 12:12:08 +0500 Subject: [PATCH 18/58] feat: Add sing-box config path option (#128) --- .../resources/view/podkop/additionalTab.js | 13 +- luci-app-podkop/po/ru/podkop.po | 5 +- luci-app-podkop/po/templates/podkop.pot | 3 + podkop/files/etc/config/podkop | 3 +- podkop/files/usr/bin/podkop | 115 +++++++++++------- podkop/files/usr/lib/constants.sh | 3 +- podkop/files/usr/lib/helpers.sh | 23 +++- 7 files changed, 114 insertions(+), 51 deletions(-) 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 00fe10e..8c42e9f 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 @@ -134,10 +134,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) { diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index be99d67..ad3be69 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -902,4 +902,7 @@ msgid "Local Subnet Lists" msgstr "Локальные списки подсетей" msgid "Local Subnet List Paths" -msgstr "Пути к локальным спискам доменов" \ No newline at end of file +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 763c411..16eed7a 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -1256,4 +1256,7 @@ 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/files/etc/config/podkop b/podkop/files/etc/config/podkop index 1527ee0..f0cc1b1 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -36,7 +36,8 @@ 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' diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 904e04c..2dfd21e 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -7,8 +7,8 @@ PODKOP_LIB="/usr/lib/podkop" . "$PODKOP_LIB/helpers.sh" . "$PODKOP_LIB/sing_box_config_manager.sh" . "$PODKOP_LIB/sing_box_config_facade.sh" - -config_load "/etc/config/podkop" +PODKOP_CONFIG="/etc/config/podkop" +config_load "$PODKOP_CONFIG" GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main" SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download" @@ -101,6 +101,7 @@ start_main() { sleep 1 mkdir -p /tmp/podkop + mkdir -p /tmp/sing-box # base route_table_rule_mark @@ -145,15 +146,14 @@ start_main() { } 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" + start_main + config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then dnsmasq_add_resolver fi @@ -271,25 +271,28 @@ migration() { # corntab init.d (crontab -l | grep -v "/etc/init.d/podkop list_update") | crontab - - migrate_config_key "$CONFIG" "option" "domain_list_enabled" "community_lists_enabled" - migrate_config_key "$CONFIG" "list" "domain_list" "community_lists" + migration_rename_config_key "$CONFIG" "option" "domain_list_enabled" "community_lists_enabled" + migration_rename_config_key "$CONFIG" "list" "domain_list" "community_lists" - migrate_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domain_list_type" - migrate_config_key "$CONFIG" "option" "custom_domains_text" "user_domains_text" - migrate_config_key "$CONFIG" "list" "custom_domains" "user_domains" + 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" - migrate_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnet_list_type" - migrate_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets_text" - migrate_config_key "$CONFIG" "list" "custom_subnets" "user_subnets" + 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" - migrate_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domain_lists_enabled" - migrate_config_key "$CONFIG" "list" "custom_local_domains" "local_domain_lists" + 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" - migrate_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domain_lists_enabled" - migrate_config_key "$CONFIG" "list" "custom_download_domains" "remote_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" - migrate_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnet_lists_enabled" - migrate_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnet_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() { @@ -611,21 +614,32 @@ 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 } sing_box_init_config() { @@ -642,7 +656,7 @@ sing_box_init_config() { # TODO: remove after refactoring nolog "$config" - sing_box_cm_save_config_to_file "$config" "$SB_CONFIG" + sing_box_save_config } sing_box_configure_log() { @@ -1140,8 +1154,17 @@ sing_box_additional_inbounds() { ) } +sing_box_save_config() { + local sing_box_config_path + config_get sing_box_config_path "main" "config_path" + log "Save sing-box config to $sing_box_config_path" + sing_box_cm_save_config_to_file "$config" "$sing_box_config_path" +} + sing_box_config_check() { - if ! sing-box -c $SB_CONFIG check >/dev/null 2>&1; then + 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 @@ -1413,19 +1436,22 @@ nft_add_podkop_subnet() { # Diagnotics check_proxy() { + 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 $SB_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 $SB_CONFIG check >/dev/null; then + if ! sing-box -c "$sing_box_config_path" check >/dev/null; then nolog "Invalid configuration" return 1 fi @@ -1453,7 +1479,7 @@ check_proxy() { else . end ) else . end - )' $SB_CONFIG + )' "$sing_box_config_path" nolog "Checking proxy connection..." @@ -1721,9 +1747,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 "$SB_CONFIG" ]; then + if [ ! -f "$sing_box_config_path" ]; then nolog "Configuration file not found" return 1 fi @@ -1751,7 +1779,7 @@ show_sing_box_config() { else . end ) else . end - )' "$SB_CONFIG" + )' "$sing_box_config_path" } show_config() { @@ -2090,10 +2118,13 @@ global_check() { else print_global " 🤔 sing-box is running, checking configuration" - if [ -f "$SB_CONFIG" ]; then - local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SB_CONFIG") - local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SB_CONFIG") - local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SB_CONFIG") + local sing_box_config_path + config_get sing_box_config_path "main" "config_path" + if [ -f "$sing_box_config_path" ]; then + # TODO(ampetelin): need fix jq after refactoring + local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$sing_box_config_path") + local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$sing_box_config_path") + local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$sing_box_config_path") print_global " 📦 FakeIP enabled: $fakeip_enabled" print_global " 📦 FakeIP range: $fakeip_range" diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 713e748..ec744f7 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,6 +1,5 @@ -SB_CONFIG="/etc/sing-box/config.json" # Log -SB_DEFAULT_LOG_LEVEL="info" +SB_DEFAULT_LOG_LEVEL="warn" # DNS SB_DNS_SERVER_TAG="dns-server" SB_SPLIT_DNS_SERVER_TAG="split-dns-server" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 656dc54..67ae697 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -170,15 +170,34 @@ 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 -migrate_config_key() { +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" "migration" + 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 } From 9762b9cca4294a84630157939fb8d8ff544a5590 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 7 Sep 2025 12:14:02 +0500 Subject: [PATCH 19/58] refactor: Remove unused functions --- podkop/files/usr/bin/podkop | 75 ------------------------------------- 1 file changed, 75 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 2dfd21e..386555b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -69,10 +69,6 @@ echolog() { nolog "$message" } -build_sing_box_config() { - cat > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SB_CONFIG" -} - start_main() { log "Starting podkop" @@ -1656,73 +1652,6 @@ check_sing_box_logs() { echo "$logs" } -# TODO(ampetelin): need fix after refactoring -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 "$SB_CONFIG" ]; then - local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SB_CONFIG") - local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SB_CONFIG") - - nolog "FakeIP enabled: $fakeip_enabled" - nolog "FakeIP range: $fakeip_range" - - local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SB_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..." @@ -2155,7 +2084,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 @@ -2207,9 +2135,6 @@ case "$1" in check_sing_box_logs) check_sing_box_logs ;; - check_fakeip) - check_fakeip - ;; check_dnsmasq) check_dnsmasq ;; From e5be9c3fd18e976d2eed2b2f1fd8ce74eb07012e Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 7 Sep 2025 12:45:27 +0500 Subject: [PATCH 20/58] refactor: Avoid unnecessary sing-box config writes by comparing hashes before saving (#128) --- podkop/files/usr/bin/podkop | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 386555b..58ef17d 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1151,10 +1151,24 @@ sing_box_additional_inbounds() { } sing_box_save_config() { - local sing_box_config_path + local sing_box_config_path temp_file_path current_config_hash temp_config_hash config_get sing_box_config_path "main" "config_path" - log "Save sing-box config to $sing_box_config_path" - sing_box_cm_save_config_to_file "$config" "$sing_box_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() { From d70a04b1447052e71681d408943aebe90c9b2298 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 7 Sep 2025 18:11:46 +0500 Subject: [PATCH 21/58] refactor: Improve dnsmasq configuration logic for DNS handling --- podkop/files/usr/bin/podkop | 54 ++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 58ef17d..5590cd1 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -436,10 +436,7 @@ dnsmasq_add_resolver() { 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 + if ! [ "$server" == "$SB_DNS_INBOUND_ADDRESS" ]; then uci add_list dhcp.@dnsmasq[0].podkop_server="$server" fi done @@ -448,47 +445,62 @@ dnsmasq_add_resolver() { save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize" log "Configure dnsmasq for sing-box" + uci -q delete dhcp.@dnsmasq[0].server + 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 -q delete dhcp.@dnsmasq[0].server - uci add_list dhcp.@dnsmasq[0].server="127.0.0.42" uci commit dhcp /etc/init.d/dnsmasq restart } dnsmasq_restore() { - log "Removing configuration for dnsmasq" + log "Restoring the dnsmasq configuration" - local cachesize noresolv server + local cachesize noresolv current_servers backup_servers found_in_backup cachesize=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null) if [[ "$cachesize" == "unset" ]]; then - log "dnsmasq revert: cachesize is unset" + log "Cachesize is unset" "debug" uci -q delete dhcp.@dnsmasq[0].cachesize else uci set dhcp.@dnsmasq[0].cachesize="$cachesize" fi + uci delete dhcp.@dnsmasq[0].podkop_cachesize noresolv=$(uci get dhcp.@dnsmasq[0].podkop_noresolv 2>/dev/null) if [[ "$noresolv" == "unset" ]]; then - log "dnsmasq revert: noresolv is unset" + log "Noresolv is unset" "debug" uci -q delete dhcp.@dnsmasq[0].noresolv else uci set dhcp.@dnsmasq[0].noresolv="$noresolv" fi - - 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" - done - uci delete dhcp.@dnsmasq[0].podkop_server 2>/dev/null - fi - - uci delete dhcp.@dnsmasq[0].podkop_cachesize uci delete dhcp.@dnsmasq[0].podkop_noresolv + current_servers=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null) + backup_servers=$(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null) + uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null + for server in $current_servers; do + [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ] && continue + + found_in_backup=0 + for backup_server in $backup_servers; do + if [ "$server" = "$backup_server" ]; then + found_in_backup=1 + break + fi + done + + [ "$found_in_backup" -eq 1 ] && continue + + uci add_list dhcp.@dnsmasq[0].server="$server" + done + + backup_servers=$(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null) + 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 commit dhcp /etc/init.d/dnsmasq restart From f54e92cd7a9e0239a119d8918067d738044a1ee1 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 10:38:49 +0500 Subject: [PATCH 22/58] fix: Update config key from cache_file to cache_path in cache file configuration --- podkop/files/usr/bin/podkop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 5590cd1..43fd032 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1121,7 +1121,7 @@ sing_box_configure_experimental() { log "Configuring cache database" local cache_file - config_get cache_file "main" "cache_file" "/tmp/cache.db" + 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 From 9496a8877406b1cdadff75345de5aaf821191864 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 10:46:29 +0500 Subject: [PATCH 23/58] refactor: Add ip addresses to nft set for local ruleset handling --- podkop/files/usr/bin/podkop | 16 ++++++++++------ podkop/files/usr/lib/constants.sh | 5 +++++ podkop/files/usr/lib/nft.sh | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 podkop/files/usr/lib/nft.sh diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 43fd032..dfa97b2 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -4,6 +4,7 @@ [ -r /lib/config/uci.sh ] && . /lib/config/uci.sh PODKOP_LIB="/usr/lib/podkop" . "$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" @@ -996,17 +997,17 @@ configure_local_domain_or_subnet_lists() { case "$type" in domains) - config_list_foreach "$section" "local_domain_lists" import_local_domain_or_subnet_list_to_ruleset "$type" \ + 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_to_ruleset "$type" \ + 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 } -import_local_domain_or_subnet_list_to_ruleset() { +import_local_domain_or_subnet_list() { local filepath="$1" local type="$2" local section="$3" @@ -1050,10 +1051,13 @@ import_local_domain_or_subnet_list_to_ruleset() { return 0 fi - items="$(comma_string_to_json_array "$items")" + items_json="$(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" ;; + domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$items_json" ;; + subnets) + sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$items_json" + nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_GENERAL_SET_NAME" "$items" + ;; esac } diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index ec744f7..1c21fa8 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,3 +1,8 @@ +## nft +NFT_TABLE_NAME="PodkopTable" +NFT_GENERAL_SET_NAME="podkop_subnets" + +## sing-box # Log SB_DEFAULT_LOG_LEVEL="warn" # DNS diff --git a/podkop/files/usr/lib/nft.sh b/podkop/files/usr/lib/nft.sh new file mode 100644 index 0000000..6efed17 --- /dev/null +++ b/podkop/files/usr/lib/nft.sh @@ -0,0 +1,23 @@ +# 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; }' +} + +# 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 From fe30cf9e5545232c7ca04b5b0370d2b2fc374103 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 22:43:27 +0500 Subject: [PATCH 24/58] refactor: Refactoring list_update function --- podkop/files/usr/bin/podkop | 357 +++++++++++++----------------- podkop/files/usr/lib/constants.sh | 1 + podkop/files/usr/lib/helpers.sh | 126 +++++++++-- 3 files changed, 258 insertions(+), 226 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index dfa97b2..7c3cd57 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -33,7 +33,8 @@ TEST_DOMAIN="fakeip.podkop.fyi" INTERFACES_LIST="" SRC_INTERFACE="" RESOLV_CONF="/etc/resolv.conf" -TMP_FOLDER="/tmp/podkop" +TMP_SING_BOX_FOLDER="/tmp/sing-box" +TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" # Endpoints https://github.com/ampetelin/warp-endpoint-checker CLOUDFLARE_OCTETS="8.47 162.159 188.114" @@ -97,8 +98,8 @@ start_main() { sleep 1 - mkdir -p /tmp/podkop - mkdir -p /tmp/sing-box + mkdir -p "$TMP_SING_BOX_FOLDER" + mkdir -p "$TMP_RULESET_FOLDER" # base route_table_rule_mark @@ -110,25 +111,7 @@ start_main() { sing_box_config_check config_foreach add_cron_job /etc/init.d/sing-box start -# sing_box_inbound_proxy 1602 #refactored -# sing_box_dns #refactored -# sing_box_dns_rule_fakeip #refactored -# sing_box_rule_dns #refactored -# sing_box_create_bypass_ruleset #refactored -# sing_box_add_secure_dns_probe_domain #refactored -# sing_box_cache_file #refactored -# process_socks5 #refactored -# -# # sing-box outbounds and rules -# config_foreach sing_box_outdound #refactored -# config_foreach process_domains_for_section #refactored, implementation is needed -# config_foreach sing_box_rule_preset #refactored -# config_foreach process_domains_list_local #refactored, implementation is needed -# config_foreach process_subnet_for_section #refactored, implementation is needed -# config_foreach configure_community_lists #refactored -# config_foreach configure_remote_domain_lists #refactored -# config_foreach configure_remote_subnet_lists #refactored -# config_foreach process_all_traffic_for_section #refactored + local exclude_ntp config_get_bool exclude_ntp "main" "exclude_ntp" "0" if [ "$exclude_ntp" -eq 1 ]; then @@ -136,10 +119,10 @@ start_main() { nft insert rule inet PodkopTable mangle udp dport 123 return fi - # TODO(ampetelin): refactoring is needed -# list_update & -# echo $! > /var/run/podkop_list_update.pid log "Nice" + # TODO(ampetelin): refactoring is needed + list_update & + echo $! > /var/run/podkop_list_update.pid } start() { @@ -171,7 +154,7 @@ stop_main() { remove_cron_job - rm -rf /tmp/podkop/*.lst + rm "$TMP_RULESET_FOLDER"/* log "Flush nft" if nft list table inet PodkopTable >/dev/null 2>&1; then @@ -190,7 +173,6 @@ stop_main() { log "Stop sing-box" /etc/init.d/sing-box stop - #/etc/init.d/sing-box disable } stop() { @@ -554,7 +536,6 @@ remove_cron_job() { log "The cron job removed" } -# TODO(ampetelin): refactoring is needed list_update() { echolog "🔄 Starting lists update..." @@ -849,6 +830,7 @@ sing_box_configure_route() { config=$(sing_box_cf_override_domain_port "$config" "$TEST_DOMAIN" 8443) config_foreach include_source_ips_in_routing_handler + # TODO(ampetelin): Add block rules local exclude_from_ip_enabled @@ -937,6 +919,7 @@ configure_routing_for_section_lists() { 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 @@ -954,11 +937,30 @@ configure_routing_for_section_lists() { 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 } +prepare_common_ruleset() { + local section="$1" + local type="$2" + local route_rule_tag="$3" + + log "Preparing a common $type ruleset for $section section" "debug" + ruleset_tag=$(get_ruleset_tag "$section" "common" "remote-$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") + fi +} + configure_community_list_handler() { local tag="$1" local section="$2" @@ -989,7 +991,7 @@ configure_local_domain_or_subnet_lists() { local ruleset_tag ruleset_filename ruleset_filepath ruleset_tag="$(get_ruleset_tag "$section" "local" "$type")" ruleset_filename="$ruleset_tag.json" - ruleset_filepath="$TMP_FOLDER/$ruleset_filename" + ruleset_filepath="$TMP_RULESET_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") @@ -1018,44 +1020,19 @@ import_local_domain_or_subnet_list() { return 1 fi - local items="" - while IFS= read -r item; do - if [ -z "$item" ]; then - 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 "$items" ]; then - items="$item" - else - items="$items,$item" - fi - done < "$filepath" + 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 - items_json="$(comma_string_to_json_array "$items")" + json_array="$(comma_string_to_json_array "$items")" case "$type" in - domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$items_json" ;; + 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" "$items_json" + sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array" nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_GENERAL_SET_NAME" "$items" ;; esac @@ -1068,11 +1045,10 @@ configure_remote_domain_or_subnet_list_handler() { local route_rule_tag="$4" 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 → proceeding with processing" "debug" - + 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") @@ -1082,17 +1058,17 @@ configure_remote_domain_or_subnet_list_handler() { 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" + log "Detected file extension: '$file_extension' → no processing needed, managed on list_update" "debug" ;; esac + + case "$type" in + domains) _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" ;; + subnets) ;; + *) log "Unsupported remote rule set type: $type" "warn" ;; + esac } configure_user_subnet_list_handler() { @@ -1191,48 +1167,23 @@ 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]" + log "Sing-box configuration is invalid" "fatal" exit 1 fi } -sing_box_ruleset_domains_json() { - local domain="$1" - local section="$2" - - local file="/tmp/podkop/$section-custom-domains-subnets.json" - - 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" - - 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" -} - import_community_subnet_lists() { - config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0" - if [ "$domain_list_enabled" -eq 1 ]; then + 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" domain_list import_community_service_subnet_list_handler + 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") @@ -1261,39 +1212,41 @@ import_community_service_subnet_list_handler() { ;; "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 + nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "$SRC_INTERFACE" ip daddr @podkop_discord_subnets \ + 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_tempfile "$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_GENERAL_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 + 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" custom_download_domains import_domains_from_remote_domain_list_handler "$section" + config_list_foreach "$section" "remote_domain_lists" import_domains_from_remote_domain_list_handler "$section" fi } @@ -1304,42 +1257,25 @@ import_domains_from_remote_domain_list_handler() { 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' → no update needed, sing-box manages updates" "debug" ;; *) - log "Detected file extension: .$file_extension → proceeding with processing" - import_domains_from_remote_lst_file "$url" "$section" + 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 + 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" custom_download_subnets import_subnets_from_remote_subnet_list_handler "$section" + config_list_foreach "$section" "remote_subnet_lists" import_subnets_from_remote_subnet_list_handler "$section" fi } @@ -1350,91 +1286,111 @@ 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" + 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" + log "Detected file extension: '$file_extension' → proceeding with processing" "debug" import_subnets_from_remote_srs_file "$url" ;; *) - log "Detected file extension: .$file_extension → unsupported, skipping" - return 1 + 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_tempfile "$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" "remote-$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_GENERAL_SET_NAME" "$items" + ;; + esac } import_subnets_from_remote_json_file() { local url="$1" + local tmpfile subnets + tmpfile="$(mktemp)" - download_to_stream "$url" | jq -r '.rules[].ip_cidr[]?' | while read -r subnet; do - nft_add_podkop_subnet "$subnet" - done + download_to_stream "$url" | 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_GENERAL_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" + local binary_tmpfile json_tmpfile subnets_tmpfile subnets + binary_tmpfile="$(mktemp)" + json_tmpfile="$(mktemp)" + subnets_tmpfile="$(mktemp)" - if ! decompile_srs_file "$binary_filepath" "$json_filepath"; then + download_to_tempfile "$url" "$binary_tmpfile" + + 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_GENERAL_SET_NAME" "$subnets" } ## Support functions -# 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 +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 + echo "" fi } @@ -1449,17 +1405,6 @@ nft_list_all_traffic_from_ip() { 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" - fi -} - # Diagnotics check_proxy() { local sing_box_config_path diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 1c21fa8..5a95e1d 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,6 +1,7 @@ ## nft NFT_TABLE_NAME="PodkopTable" NFT_GENERAL_SET_NAME="podkop_subnets" +NFT_DISCORD_SET_NAME="podkop_discord_subnets" ## sing-box # Log diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 67ae697..aa9ecb0 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -12,6 +12,10 @@ is_ipv4_cidr() { [[ "$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" @@ -39,14 +43,6 @@ file_exists() { fi } -# Extracts and returns the file extension from the given URL -get_url_file_extension() { - local url="$1" - local file_extension="${url##*.}" - - echo "$file_extension" -} - # Returns the inbound tag name by appending the postfix to the given section get_inbound_tag_by_section() { local section="$1" @@ -155,6 +151,17 @@ url_get_basename() { 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" @@ -205,29 +212,108 @@ migration_rename_config_key() { # Download URL content directly download_to_stream() { local url="$1" - local http_proxy_url="$2" + local http_proxy_address="$2" + local retries="${3:-3}" + local wait="${4:-2}" - if [ -n "$http_proxy_url" ]; then - http_proxy="$http_proxy_url" https_proxy="$http_proxy_url" wget -qO- "$url" | sed 's/\r$//' - else - wget -qO- "$url" | sed 's/\r$//' - fi + for attempt in $(seq 1 "$retries"); do + if [ -n "$http_proxy_url" ]; 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 temporary file download_to_tempfile() { local url="$1" local filepath="$2" - local http_proxy_url="$3" + local http_proxy_address="$3" + local retries="${4:-3}" + local wait="${5:-2}" - if [ -n "$http_proxy_url" ]; then - http_proxy="$http_proxy_url" https_proxy="$http_proxy_url" wget -O "$filepath" "$url" - else - wget -O "$filepath" "$url" - fi + for attempt in $(seq 1 "$retries"); do + if [ -n "$http_proxy_url" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_url" 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 "$filename 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 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" +} \ No newline at end of file From a187192a88648cc5f04e69a50e85ec2e43357e7b Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 22:55:42 +0500 Subject: [PATCH 25/58] chore: Move and rename _get_download_detour_tag to get_download_detour_tag --- podkop/files/usr/bin/podkop | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 7c3cd57..1323e68 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -970,7 +970,7 @@ configure_community_list_handler() { ruleset_tag="$(get_ruleset_tag "$section" "$tag" "community")" format="binary" url="$SRS_MAIN_URL/$tag.srs" - detour="$(_get_download_detour_tag)" + 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") @@ -1053,7 +1053,7 @@ configure_remote_domain_or_subnet_list_handler() { 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)" + 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") @@ -1076,15 +1076,6 @@ configure_user_subnet_list_handler() { # TODO(ampetelin): it is necessary to implement } -_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 -} - _add_ruleset_to_dns_rules() { local ruleset_tag="$1" @@ -1394,6 +1385,15 @@ get_service_proxy_address() { fi } +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 +} + ## nftables nft_list_all_traffic_from_ip() { local ip="$1" From c1133827a24b8d4168b5582d8e10d14acc659d0d Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 23:00:26 +0500 Subject: [PATCH 26/58] fix: Pass HTTP proxy address to download functions for remote subnet imports --- podkop/files/usr/bin/podkop | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 1323e68..480a2cd 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1333,10 +1333,11 @@ import_domains_or_subnets_from_remote_file() { import_subnets_from_remote_json_file() { local url="$1" - local tmpfile subnets + local tmpfile subnets http_proxy_address tmpfile="$(mktemp)" + http_proxy_address="$(get_service_proxy_address)" - download_to_stream "$url" | jq -r '.rules[].ip_cidr[]?' > "$tmpfile" + 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" @@ -1351,12 +1352,13 @@ import_subnets_from_remote_json_file() { import_subnets_from_remote_srs_file() { local url="$1" - local binary_tmpfile json_tmpfile subnets_tmpfile subnets + 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_tempfile "$url" "$binary_tmpfile" + download_to_tempfile "$url" "$binary_tmpfile" "$http_proxy_address" if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then log "Download $url list failed" "error" From 27d2366208d5559a98624f470a13cb99cf621e84 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 23:03:28 +0500 Subject: [PATCH 27/58] chore: Remove list_update refactor TODO --- podkop/files/usr/bin/podkop | 1 - 1 file changed, 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 480a2cd..310abac 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -120,7 +120,6 @@ start_main() { fi log "Nice" - # TODO(ampetelin): refactoring is needed list_update & echo $! > /var/run/podkop_list_update.pid } From 1e8c2b50f771ee99d9bbdb3313056d75eaaad976 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 8 Sep 2025 23:10:53 +0500 Subject: [PATCH 28/58] chore: Rename NFT_GENERAL_SET_NAME to NFT_COMMON_SET_NAME --- podkop/files/usr/bin/podkop | 10 +++++----- podkop/files/usr/lib/constants.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 310abac..d9052b6 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1032,7 +1032,7 @@ import_local_domain_or_subnet_list() { 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_GENERAL_SET_NAME" "$items" + nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items" ;; esac } @@ -1226,7 +1226,7 @@ import_community_service_subnet_list_handler() { 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_GENERAL_SET_NAME" "$subnets" + nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets" fi } @@ -1325,7 +1325,7 @@ import_domains_or_subnets_from_remote_file() { 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_GENERAL_SET_NAME" "$items" + nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items" ;; esac } @@ -1345,7 +1345,7 @@ import_subnets_from_remote_json_file() { subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")" rm -f "$tmpfile" - nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_GENERAL_SET_NAME" "$subnets" + nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets" } import_subnets_from_remote_srs_file() { @@ -1372,7 +1372,7 @@ import_subnets_from_remote_srs_file() { 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_GENERAL_SET_NAME" "$subnets" + nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets" } ## Support functions diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 5a95e1d..f0f7648 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,6 +1,6 @@ ## nft NFT_TABLE_NAME="PodkopTable" -NFT_GENERAL_SET_NAME="podkop_subnets" +NFT_COMMON_SET_NAME="podkop_subnets" NFT_DISCORD_SET_NAME="podkop_discord_subnets" ## sing-box From 6c094aceaea7d1da138a1630da751da5898ac1c7 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 9 Sep 2025 11:15:29 +0500 Subject: [PATCH 29/58] fix: Ensure unique values when patching local source ruleset --- .../files/usr/lib/sing_box_config_manager.sh | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index a959034..8c9a0c1 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -1227,12 +1227,28 @@ sing_box_cm_configure_clash_api() { }' } +####################################### +# 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" @@ -1244,9 +1260,16 @@ sing_box_cm_patch_local_source_ruleset_rules() { content="$(cat "$filepath")" echo "$content" | jq \ - --arg key "$key" \ - --argjson value "$value" \ - '.rules += [{($key): $value}]' > "$filepath" + --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" } ####################################### From 79cea7a31a702c47f441d8d8998c5ba2d5185764 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 9 Sep 2025 11:20:19 +0500 Subject: [PATCH 30/58] chore: remove .shellcheckrc file --- .shellcheckrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .shellcheckrc 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 From 191522f3968c7ac999fe5d6f00a063567a12cb3f Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 9 Sep 2025 11:48:03 +0500 Subject: [PATCH 31/58] chore: Rename download_to_tempfile to download_to_file for clarity --- podkop/files/usr/bin/podkop | 6 +++--- podkop/files/usr/lib/helpers.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index d9052b6..5cf3ac6 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1213,7 +1213,7 @@ import_community_service_subnet_list_handler() { tmpfile=$(mktemp) http_proxy_address="$(get_service_proxy_address)" - download_to_tempfile "$URL" "$tmpfile" "$http_proxy_address" + download_to_file "$URL" "$tmpfile" "$http_proxy_address" if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then log "Download $service list failed" "error" @@ -1302,7 +1302,7 @@ import_domains_or_subnets_from_remote_file() { tmpfile=$(mktemp) http_proxy_address="$(get_service_proxy_address)" - download_to_tempfile "$url" "$tmpfile" "$http_proxy_address" + download_to_file "$url" "$tmpfile" "$http_proxy_address" if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then log "Download $url list failed" "error" @@ -1357,7 +1357,7 @@ import_subnets_from_remote_srs_file() { subnets_tmpfile="$(mktemp)" http_proxy_address="$(get_service_proxy_address)" - download_to_tempfile "$url" "$binary_tmpfile" "$http_proxy_address" + download_to_file "$url" "$binary_tmpfile" "$http_proxy_address" if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then log "Download $url list failed" "error" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index aa9ecb0..3494e20 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -228,8 +228,8 @@ download_to_stream() { done } -# Download URL to temporary file -download_to_tempfile() { +# Download URL to file +download_to_file() { local url="$1" local filepath="$2" local http_proxy_address="$3" From 81e0c86060b53665b8b0c7a42470dd918d5675bb Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 9 Sep 2025 15:51:09 +0500 Subject: [PATCH 32/58] refactor: Add block section support --- podkop/files/usr/bin/podkop | 121 +++++++++++++----- podkop/files/usr/lib/constants.sh | 2 + .../files/usr/lib/sing_box_config_facade.sh | 11 +- .../files/usr/lib/sing_box_config_manager.sh | 16 +-- 4 files changed, 109 insertions(+), 41 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 5cf3ac6..0460ccb 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -153,7 +153,7 @@ stop_main() { remove_cron_job - rm "$TMP_RULESET_FOLDER"/* + rm -f "$TMP_RULESET_FOLDER"/* log "Flush nft" if nft list table inet PodkopTable >/dev/null 2>&1; then @@ -820,7 +820,7 @@ sing_box_configure_route() { local quic_disable config_get_bool quic_disable "main" "quic_disable" 0 if [ "$quic_disable" -eq 1 ]; then - config=$(sing_box_cm_add_reject_route_rule "$config" "protocol" "quic") + config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "protocol" "quic") fi config=$( @@ -830,7 +830,7 @@ sing_box_configure_route() { config_foreach include_source_ips_in_routing_handler - # TODO(ampetelin): Add block rules + configure_common_reject_route_rule local exclude_from_ip_enabled config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" 0 @@ -858,6 +858,26 @@ include_source_ips_in_routing_handler() { fi } +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 +} + include_source_ip_in_routing_handler() { local source_ip="$1" local rule_tag="$2" @@ -874,9 +894,14 @@ 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_subnet_lists_enabled - local user_domain_list_type user_subnet_list_type route_rule_tag + + 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 @@ -884,59 +909,53 @@ configure_routing_for_section_lists() { 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 [ "$community_lists_enabled" -eq 0 ] && \ - [ "$user_domain_list_type" == "disabled" ] && \ - [ "$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 + if [ "$section_mode_type" = "block" ]; then + route_rule_tag="$SB_REJECT_RULE_TAG" + else + 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 - 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") - if [ "$community_lists_enabled" -eq 1 ]; then - log "Processing community list routing rules for $section section" + 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" + log "Processing user domains routing rules for '$section' section" # TODO(ampetelin): it is necessary to implement # configure_user_domain_list_handler fi if [ "$local_domain_lists_enabled" -eq 1 ]; then - log "Processing local domains routing rules for $section section" + 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" + 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" + log "Processing user subnets routing rules for '$section' section" # TODO(ampetelin): it is necessary to implement # configure_user_subnet_list_handler fi if [ "$local_subnet_lists_enabled" -eq 1 ]; then - log "Processing local subnets routing rules for $section section" + 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" + 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 @@ -947,7 +966,7 @@ prepare_common_ruleset() { local type="$2" local route_rule_tag="$3" - log "Preparing a common $type ruleset for $section section" "debug" + log "Preparing a common $type ruleset for '$section' section" "debug" ruleset_tag=$(get_ruleset_tag "$section" "common" "remote-$type") ruleset_filename="$ruleset_tag.json" ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename" @@ -1167,7 +1186,7 @@ import_community_subnet_lists() { 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" + log "Importing community subnet lists for '$section' section" config_list_foreach "$section" "community_lists" import_community_service_subnet_list_handler fi } @@ -1235,7 +1254,7 @@ import_domains_from_remote_domain_lists() { 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" + 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 } @@ -1264,7 +1283,7 @@ import_subnets_from_remote_subnet_lists() { 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" + 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 } @@ -1395,6 +1414,44 @@ get_download_detour_tag() { fi } +get_block_sections() { + uci show podkop | grep "\.mode='block'" | cut -d'.' -f2 +} + +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" diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index f0f7648..1bf735a 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -30,3 +30,5 @@ 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" \ 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 index 2a42040..601afde 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -205,6 +205,15 @@ sing_box_cf_override_domain_port() { echo "$config" } -sing_box_cf_add_remote_ruleset_with_dns_and_route_rule() { +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 index 8c9a0c1..6bc5274 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -943,21 +943,21 @@ sing_box_cm_patch_route_rule() { # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_add_reject_route_rule "$CONFIG" "protocol" "quic") +# CONFIG=$(sing_box_cm_add_reject_route_rule "$CONFIG" "reject-rule-tag") ####################################### sing_box_cm_add_reject_route_rule() { local config="$1" - local key="$2" - local value="$3" - - value=$(_normalize_arg "$value") + local tag="$2" + local inbound="$3" echo "$config" | jq \ - --arg key "$key" \ - --argjson value "$value" \ + --arg service_tag "$SERVICE_TAG" \ + --arg tag "$tag" \ + --arg inbound "$inbound" \ '.route.rules += [{ action: "reject", - ($key): $value + inbound: $inbound, + $service_tag: $tag }]' } From b477a8abc05782d06b55e03f769759dd031b175c Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 9 Sep 2025 16:30:11 +0500 Subject: [PATCH 33/58] fix: Assign domain resolver tag correctly to DNS servers --- podkop/files/usr/bin/podkop | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 0460ccb..e66f8cb 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -770,17 +770,18 @@ sing_box_configure_dns() { 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 config=$( sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "" "" \ - "$SB_DNS_DOMAIN_RESOLVER_TAG" + "$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" \ - "" "" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$SB_MAIN_OUTBOUND_TAG" + "" "" "$dns_domain_resolver" "$SB_MAIN_OUTBOUND_TAG" ) fi From 775b0073d332da12f3c0c2c78c0fe7501e9059cf Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 13:33:07 +0500 Subject: [PATCH 34/58] refactor: Fixed nft rule for routing tagged traffic to localhost tproxy --- podkop/files/usr/bin/podkop | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index e66f8cb..38060e5 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -116,7 +116,7 @@ start_main() { 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 log "Nice" @@ -156,8 +156,8 @@ stop_main() { 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" @@ -331,7 +331,7 @@ process_interfaces() { } nft_interfaces() { - local table=PodkopTable + local table="$NFT_TABLE_NAME" iface_flag=0 config_list_foreach "main" "iface" "process_interfaces" @@ -356,7 +356,7 @@ nft_interfaces() { } create_nft_table() { - local table="PodkopTable" + local table="$NFT_TABLE_NAME" nft add table inet $table @@ -391,8 +391,7 @@ create_nft_table() { 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 $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 proxy meta mark 0x105 meta l4proto { tcp, udp } tproxy ip to 127.0.0.1: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 @@ -1456,7 +1455,7 @@ section_has_enabled_lists() { ## nftables nft_list_all_traffic_from_ip() { local ip="$1" - local table="PodkopTable" + local table="$NFT_TABLE_NAME" 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 @@ -1546,11 +1545,11 @@ check_nft() { 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 @@ -1584,9 +1583,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 @@ -1595,7 +1594,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]" @@ -1606,7 +1605,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" From 82f9ae4c6ae9f2129a9abef02f417e305e7eec77 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 14:10:17 +0500 Subject: [PATCH 35/58] refactor: Remove redundant FakeIP config verification --- podkop/files/usr/bin/podkop | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 38060e5..361debc 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -2059,7 +2059,8 @@ global_check() { print_global "➡️ DNS resolution: system DNS server" nslookup -timeout=2 $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 @@ -2068,7 +2069,8 @@ global_check() { 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 $TEST_DOMAIN 127.0.0.42 2>&1) echo "$result" if echo "$result" | grep -q "198.18"; then @@ -2078,22 +2080,7 @@ global_check() { if ! pgrep -f "sing-box" >/dev/null; then print_global " ❌ sing-box is not running" else - print_global " 🤔 sing-box is running, checking configuration" - - local sing_box_config_path - config_get sing_box_config_path "main" "config_path" - if [ -f "$sing_box_config_path" ]; then - # TODO(ampetelin): need fix jq after refactoring - local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$sing_box_config_path") - local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$sing_box_config_path") - local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$sing_box_config_path") - - 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 } From d4e754d2eb13c9dc8ea2ec6ff25d0aad8422f536 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 14:14:15 +0500 Subject: [PATCH 36/58] refactor: Rename FAKEIP constant to SB_FAKEIP_INET4_RANG --- podkop/files/usr/bin/podkop | 6 +++--- podkop/files/usr/lib/constants.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 361debc..5622fef 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -388,8 +388,8 @@ create_nft_table() { 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 $table mangle iifname "$SRC_INTERFACE" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter + nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set 0x105 counter nft add rule inet $table proxy meta mark 0x105 meta l4proto { tcp, udp } tproxy ip to 127.0.0.1:1602 counter @@ -756,7 +756,7 @@ sing_box_configure_dns() { fi log "Adding DNS Servers" - config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$FAKEIP") + config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE") local dns_domain_resolver if [ "$need_dns_domain_resolver" -eq 1 ]; then diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 1bf735a..ccf8615 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -10,7 +10,7 @@ SB_DEFAULT_LOG_LEVEL="warn" SB_DNS_SERVER_TAG="dns-server" SB_SPLIT_DNS_SERVER_TAG="split-dns-server" SB_FAKEIP_DNS_SERVER_TAG="fakeip-server" -FAKEIP="198.18.0.0/15" # TODO(ampetelin): renaming is needed +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" @@ -22,7 +22,7 @@ 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" +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" From d0ea39abd026038275030fe53b06daa360471aab Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 15:39:23 +0500 Subject: [PATCH 37/58] refactor: Rename TEST_DOMAIN variable to FAKEIP_TEST_DOMAIN --- podkop/files/usr/bin/podkop | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 5622fef..0727665 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -29,7 +29,7 @@ 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" 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" -TEST_DOMAIN="fakeip.podkop.fyi" +FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi" INTERFACES_LIST="" SRC_INTERFACE="" RESOLV_CONF="/etc/resolv.conf" @@ -591,7 +591,7 @@ list_update() { find_working_resolver() { 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 @@ -792,7 +792,7 @@ sing_box_configure_dns() { 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 "$TEST_DOMAIN,$CHECK_PROXY_IP_DOMAIN") + 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") @@ -826,7 +826,7 @@ sing_box_configure_route() { 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" "$TEST_DOMAIN" 8443) + config=$(sing_box_cf_override_domain_port "$config" "$FAKEIP_TEST_DOMAIN" 8443) config_foreach include_source_ips_in_routing_handler @@ -1928,7 +1928,7 @@ check_dns_available() { 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 @@ -2057,7 +2057,7 @@ 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 working_resolver=$(find_working_resolver) @@ -2065,12 +2065,12 @@ global_check() { 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 - result=$(nslookup -timeout=2 $TEST_DOMAIN 127.0.0.42 2>&1) + result=$(nslookup -timeout=2 $FAKEIP_TEST_DOMAIN 127.0.0.42 2>&1) echo "$result" if echo "$result" | grep -q "198.18"; then From d4b5431db41205eb7a3cd05d642e04a5745f557d Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 17:52:14 +0500 Subject: [PATCH 38/58] refactor: Refactor nft rules to use named sets for interfaces and localv4 --- podkop/files/usr/bin/podkop | 113 ++++++++++++------------------ podkop/files/usr/lib/constants.sh | 2 + podkop/files/usr/lib/nft.sh | 7 ++ 3 files changed, 54 insertions(+), 68 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 0727665..12d4036 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -30,8 +30,6 @@ VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn 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" -INTERFACES_LIST="" -SRC_INTERFACE="" RESOLV_CONF="/etc/resolv.conf" TMP_SING_BOX_FOLDER="/tmp/sing-box" TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" @@ -324,80 +322,60 @@ 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="$NFT_TABLE_NAME" - 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="$NFT_TABLE_NAME" - - nft add table inet $table + log "Create nft table" + nft_create_table "$NFT_TABLE_NAME" nft_interfaces 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, 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, 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 "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter - nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set 0x105 counter + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark 0x105 meta l4proto '{ tcp, udp }' tproxy ip to 127.0.0.1:1602 counter - nft add rule inet $table proxy meta mark 0x105 meta l4proto { tcp, udp } tproxy ip to 127.0.0.1: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, udp }' meta mark set 0x105 counter + nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto '{ tcp, udp }' meta mark set 0x105 counter } save_dnsmasq_config() { @@ -1222,8 +1200,8 @@ import_community_service_subnet_list_handler() { "discord") URL=$SUBNETS_DISCORD nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "$SRC_INTERFACE" ip daddr @podkop_discord_subnets \ - udp dport '{ 50000-65535 }' meta mark set 0x105 counter + 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 @@ -1455,11 +1433,10 @@ section_has_enabled_lists() { ## nftables nft_list_all_traffic_from_ip() { local ip="$1" - local table="$NFT_TABLE_NAME" - 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 + 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, udp }' meta mark set 0x105 counter + nft insert rule inet "$NFT_TABLE_NAME" mangle ip saddr "$ip" ip daddr @localv4 return fi } diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index ccf8615..33d4175 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,7 +1,9 @@ ## 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 diff --git a/podkop/files/usr/lib/nft.sh b/podkop/files/usr/lib/nft.sh index 6efed17..3a2a3c2 100644 --- a/podkop/files/usr/lib/nft.sh +++ b/podkop/files/usr/lib/nft.sh @@ -13,6 +13,13 @@ nft_create_ipv4_set() { 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" From 489c61baa2013c92749eee92aaa3ed68359d8189 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 19:40:12 +0500 Subject: [PATCH 39/58] refactor: Move constants to constants.sh --- podkop/files/usr/bin/podkop | 33 +--------------------------- podkop/files/usr/lib/constants.sh | 36 ++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 12d4036..ea94b38 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -8,40 +8,9 @@ PODKOP_LIB="/usr/lib/podkop" . "$PODKOP_LIB/helpers.sh" . "$PODKOP_LIB/sing_box_config_manager.sh" . "$PODKOP_LIB/sing_box_config_facade.sh" -PODKOP_CONFIG="/etc/config/podkop" + config_load "$PODKOP_CONFIG" -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" -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" -RESOLV_CONF="/etc/resolv.conf" -TMP_SING_BOX_FOLDER="/tmp/sing-box" -TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" - -# 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 level="$2" diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 33d4175..d42aac2 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -1,3 +1,19 @@ +# 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 +# Color constants +COLOR_CYAN="\033[0;36m" +COLOR_GREEN="\033[0;32m" +COLOR_RESET="\033[0m" + ## nft NFT_TABLE_NAME="PodkopTable" NFT_LOCALV4_SET_NAME="localv4" @@ -33,4 +49,22 @@ SB_SERVICE_MIXED_INBOUND_PORT=4534 SB_DIRECT_OUTBOUND_TAG="direct-out" SB_MAIN_OUTBOUND_TAG="main-out" # Route -SB_REJECT_RULE_TAG="reject-rule-tag" \ No newline at end of file +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 From 49f12b212dbf63d4ca081e1517f00933eadea675 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Wed, 10 Sep 2025 19:44:31 +0500 Subject: [PATCH 40/58] chore: Update required sing-box version to 1.12.0 --- podkop/files/usr/bin/podkop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index ea94b38..df9db05 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -43,7 +43,7 @@ start_main() { # 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 "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" From 5a2ffcfd38071f80bdbb7934035f0dffb94b35f8 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 11:43:58 +0500 Subject: [PATCH 41/58] refactor: Add graceful shutdown handling for dnsmasq reconfiguration --- podkop/files/etc/config/podkop | 3 +- podkop/files/usr/bin/podkop | 142 ++++++++++++++++----------------- 2 files changed, 73 insertions(+), 72 deletions(-) diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index f0cc1b1..6e2269e 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -43,4 +43,5 @@ config main 'main' #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 df9db05..d3ea319 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -97,13 +97,18 @@ start() { config_get interface "main" "interface" config_get outbound_json "main" "outbound_json" - if [ -n "$proxy_string" ] || [ -n "$interface" ] || [ -n "$outbound_json" ]; then - start_main - 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" } stop_main() { @@ -143,12 +148,13 @@ stop_main() { stop() { local dont_touch_dhcp - config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0" + 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" } reload() { @@ -306,7 +312,7 @@ create_nft_table() { log "Create nft table" nft_create_table "$NFT_TABLE_NAME" - nft_interfaces + nft_init_interfaces_set log "Create localv4 set" nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_LOCALV4_SET_NAME" @@ -347,89 +353,87 @@ create_nft_table() { nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto '{ tcp, udp }' 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" == "$SB_DNS_INBOUND_ADDRESS" ]; then - 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 -q delete dhcp.@dnsmasq[0].server - 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 + 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 "Restoring the dnsmasq configuration" - - local cachesize noresolv current_servers backup_servers found_in_backup - cachesize=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null) - if [[ "$cachesize" == "unset" ]]; then - log "Cachesize is unset" "debug" - uci -q delete dhcp.@dnsmasq[0].cachesize - else - uci set dhcp.@dnsmasq[0].cachesize="$cachesize" + 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 - uci delete dhcp.@dnsmasq[0].podkop_cachesize - noresolv=$(uci get dhcp.@dnsmasq[0].podkop_noresolv 2>/dev/null) - if [[ "$noresolv" == "unset" ]]; then - log "Noresolv is unset" "debug" - 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 - uci delete dhcp.@dnsmasq[0].podkop_noresolv - current_servers=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null) - backup_servers=$(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null) - uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null - for server in $current_servers; do - [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ] && continue + 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 - found_in_backup=0 - for backup_server in $backup_servers; do - if [ "$server" = "$backup_server" ]; then - found_in_backup=1 - break - 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_remove "dhcp" "@dnsmasq[0]" "podkop_server" + fi - [ "$found_in_backup" -eq 1 ] && continue - - uci add_list dhcp.@dnsmasq[0].server="$server" - done - - backup_servers=$(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null) - 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 commit dhcp + uci_commit "dhcp" /etc/init.d/dnsmasq restart } @@ -587,10 +591,6 @@ sing_box_init_config() { sing_box_configure_route sing_box_configure_experimental sing_box_additional_inbounds - - # TODO: remove after refactoring - nolog "$config" - sing_box_save_config } From 38fcb59ed76b125b528a96354cf429694f7300c5 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 13:37:49 +0500 Subject: [PATCH 42/58] fix: Reload config after commit to ensure runtime consistency --- podkop/files/usr/bin/podkop | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index d3ea319..c1b472b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -108,7 +108,7 @@ start() { dnsmasq_add_resolver fi uci_set "podkop" "main" "shutdown_correctly" 0 - uci commit "podkop" + uci commit "podkop" && config_load "$PODKOP_CONFIG" } stop_main() { @@ -154,7 +154,7 @@ stop() { fi stop_main uci_set "podkop" "main" "shutdown_correctly" 1 - uci commit "podkop" + uci commit "podkop" && config_load "$PODKOP_CONFIG" } reload() { From b6a6db71a852c31f823e7aa55e9eb520fef42169 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 15:31:21 +0500 Subject: [PATCH 43/58] refactor: Implement user domain and subnet list handling --- podkop/files/usr/bin/podkop | 70 ++++++++++++++++++++++++--------- podkop/files/usr/lib/helpers.sh | 44 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index c1b472b..b5bc8db 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -873,8 +873,8 @@ configure_routing_for_section_lists() { if [ "$user_domain_list_type" != "disabled" ]; then log "Processing user domains routing rules for '$section' section" - # TODO(ampetelin): it is necessary to implement - # configure_user_domain_list_handler + 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 @@ -891,8 +891,8 @@ configure_routing_for_section_lists() { if [ "$user_subnet_list_type" != "disabled" ]; then log "Processing user subnets routing rules for '$section' section" - # TODO(ampetelin): it is necessary to implement - # configure_user_subnet_list_handler + 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 @@ -914,7 +914,7 @@ prepare_common_ruleset() { local route_rule_tag="$3" log "Preparing a common $type ruleset for '$section' section" "debug" - ruleset_tag=$(get_ruleset_tag "$section" "common" "remote-$type") + 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 @@ -923,6 +923,11 @@ prepare_common_ruleset() { 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 } @@ -943,9 +948,42 @@ configure_community_list_handler() { config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag") } -configure_user_domain_list_handler() { +configure_user_domain_or_subnets_list() { local section="$1" - # TODO(ampetelin): it is necessary to implement + 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_local_domain_or_subnet_lists() { @@ -1023,22 +1061,16 @@ configure_remote_domain_or_subnet_list_handler() { 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 - - case "$type" in - domains) _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" ;; - subnets) ;; - *) log "Unsupported remote rule set type: $type" "warn" ;; - esac -} - -configure_user_subnet_list_handler() { - local section="$1" - # TODO(ampetelin): it is necessary to implement } _add_ruleset_to_dns_rules() { @@ -1283,7 +1315,7 @@ import_domains_or_subnets_from_remote_file() { return 0 fi - ruleset_tag=$(get_ruleset_tag "$section" "common" "remote-$type") + 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")" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 3494e20..6a80f05 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -272,6 +272,50 @@ decompile_srs_file() { 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. From 074c1a93498e6de072aa0194b5c7abc446461a5e Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 15:32:39 +0500 Subject: [PATCH 44/58] chore: Remove TODO comments from user domains and subnets text input fields --- .../htdocs/luci-static/resources/view/podkop/configSection.js | 2 -- 1 file changed, 2 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 370e745..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 @@ -323,7 +323,6 @@ function createConfigSection(section, map, network) { return true; }; - // TODO: Is it possible to save not as an option (but as a split list)? 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('user_domain_list_type', 'text'); @@ -451,7 +450,6 @@ function createConfigSection(section, map, network) { return true; }; - // TODO: Is it possible to save not as an option (but as a split list)? 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('user_subnet_list_type', 'text'); From a7f6a993acdd4e7d323d41417de524d788500056 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 16:40:06 +0500 Subject: [PATCH 45/58] chore: shfmt formatting --- podkop/files/usr/bin/podkop | 520 ++++++++++++++++++------------------ 1 file changed, 261 insertions(+), 259 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index b5bc8db..8a377ff 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -116,8 +116,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 @@ -128,7 +128,7 @@ stop_main() { rm -f "$TMP_RULESET_FOLDER"/* log "Flush nft" - if nft list table inet "$NFT_TABLE_NAME" >/dev/null 2>&1; then + if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then nft delete table inet "$NFT_TABLE_NAME" fi @@ -138,7 +138,7 @@ 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 @@ -268,11 +268,11 @@ process_validate_service() { } 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 @@ -280,7 +280,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" @@ -447,29 +447,29 @@ add_cron_job() { 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 [ "$community_lists_enabled" -eq 1 ] || \ - [ "$remote_domain_lists_enabled" -eq 1 ] || \ + if [ "$community_lists_enabled" -eq 1 ] || + [ "$remote_domain_lists_enabled" -eq 1 ] || [ "$remote_subnet_lists_enabled" -eq 1 ]; then remove_cron_job crontab -l | { @@ -491,7 +491,7 @@ list_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 @@ -507,12 +507,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 @@ -542,7 +542,7 @@ list_update() { find_working_resolver() { for resolver in $DNS_RESOLVERS; do - if nslookup -timeout=2 $FAKEIP_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 @@ -597,7 +597,7 @@ sing_box_init_config() { sing_box_configure_log() { log "Configure the log section of a sing-box JSON configuration" - config=$(sing_box_cm_configure_log "$config" false "$SB_DEFAULT_LOG_LEVEL" false) + config=$(sing_box_cm_configure_log "$config" false "$SB_DEFAULT_LOG_LEVEL" false) } sing_box_configure_inbounds() { @@ -605,7 +605,7 @@ sing_box_configure_inbounds() { config=$( sing_box_cm_add_tproxy_inbound \ - "$config" "$SB_TPROXY_INBOUND_TAG" "$SB_TPROXY_INBOUND_ADDRESS" "$SB_TPROXY_INBOUND_PORT" true true + "$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" @@ -668,7 +668,7 @@ configure_outbound_handler() { exit 1 fi - config=$(sing_box_cf_add_interface_outbound "$config" "$section" "$interface_name") + 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)" @@ -721,13 +721,13 @@ sing_box_configure_dns() { config=$( sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "" "" \ - "$dns_domain_resolver" + "$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" + "" "" "$dns_domain_resolver" "$SB_MAIN_OUTBOUND_TAG" ) fi @@ -799,7 +799,7 @@ include_source_ips_in_routing_handler() { 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" "$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 @@ -848,7 +848,7 @@ configure_routing_for_section_lists() { 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 + 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 @@ -886,7 +886,7 @@ configure_routing_for_section_lists() { 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" + "domains" "$section" "$route_rule_tag" fi if [ "$user_subnet_list_type" != "disabled" ]; then @@ -904,7 +904,7 @@ configure_routing_for_section_lists() { 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" + "subnets" "$section" "$route_rule_tag" fi } @@ -1003,11 +1003,13 @@ configure_local_domain_or_subnet_lists() { 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" ;; + "$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";; + "$section" "$ruleset_filepath" + ;; *) log "Unsupported local rule set type: $type" "warn" ;; esac } @@ -1050,26 +1052,26 @@ configure_remote_domain_or_subnet_list_handler() { 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" + 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" - ;; + 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 } @@ -1138,7 +1140,7 @@ sing_box_save_config() { 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}') + 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" @@ -1154,7 +1156,7 @@ sing_box_save_config() { 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 + if ! sing-box -c "$sing_box_config_path" check > /dev/null 2>&1; then log "Sing-box configuration is invalid" "fatal" exit 1 fi @@ -1174,37 +1176,37 @@ import_community_service_subnet_list_handler() { local service="$1" 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_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 \ + "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 ;; + ;; + *) return 0 ;; esac local tmpfile detour http_proxy_address subnets @@ -1241,19 +1243,19 @@ import_domains_from_remote_domain_lists() { import_domains_from_remote_domain_list_handler() { local url="$1" local section="$2" - + log "Importing domains from URL: $url" local file_extension 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" "debug" - ;; - *) - log "Detected file extension: '$file_extension' → proceeding with processing" "debug" - import_domains_or_subnets_from_remote_file "$url" "$section" "domains" - ;; + 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 } @@ -1262,8 +1264,8 @@ import_subnets_from_remote_subnet_lists() { 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" + 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 } @@ -1276,18 +1278,18 @@ import_subnets_from_remote_subnet_list_handler() { local file_extension file_extension="$(url_get_file_extension "$url")" case "$file_extension" in - 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" - ;; + 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 } @@ -1320,7 +1322,7 @@ import_domains_or_subnets_from_remote_file() { 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";; + 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" @@ -1348,7 +1350,7 @@ import_subnets_from_remote_json_file() { import_subnets_from_remote_srs_file() { local url="$1" - + local binary_tmpfile json_tmpfile subnets_tmpfile subnets http_proxy_address binary_tmpfile="$(mktemp)" json_tmpfile="$(mktemp)" @@ -1408,7 +1410,7 @@ block_section_exists() { 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 + 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" @@ -1418,13 +1420,13 @@ section_has_enabled_lists() { 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 + 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 @@ -1446,7 +1448,7 @@ check_proxy() { local sing_box_config_path config_get sing_box_config_path "main" "config_path" - if ! command -v sing-box >/dev/null 2>&1; then + if ! command -v sing-box > /dev/null 2>&1; then nolog "sing-box is not installed" return 1 fi @@ -1458,7 +1460,7 @@ check_proxy() { nolog "Checking sing-box configuration..." - if ! sing-box -c "$sing_box_config_path" check >/dev/null; then + if ! sing-box -c "$sing_box_config_path" check > /dev/null; then nolog "Invalid configuration" return 1 fi @@ -1490,35 +1492,34 @@ check_proxy() { 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 @@ -1526,7 +1527,7 @@ check_nft() { nolog "Checking $NFT_TABLE_NAME rules..." # Check if table exists - if ! nft list table inet "$NFT_TABLE_NAME" >/dev/null 2>&1; then + if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then nolog "❌ $NFT_TABLE_NAME not found" return 1 fi @@ -1561,9 +1562,9 @@ check_nft() { nolog "Sets statistics:" for set_name in $sets; do - if nft list set inet "$NFT_TABLE_NAME" $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 "$NFT_TABLE_NAME" $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 @@ -1600,7 +1601,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" @@ -1635,7 +1636,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 @@ -1666,7 +1667,7 @@ check_sing_box_logs() { 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 @@ -1741,7 +1742,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" @@ -1783,13 +1784,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 @@ -1828,8 +1829,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 @@ -1847,37 +1848,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 @@ -1889,24 +1890,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 $FAKEIP_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 @@ -1944,21 +1946,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" @@ -1970,7 +1972,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'\''") @@ -2055,7 +2057,7 @@ global_check() { 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" @@ -2097,77 +2099,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_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 From 427ea3bc9ab96cf8b8462ed1d8ce83ac67698b99 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 16:56:30 +0500 Subject: [PATCH 46/58] fix: Remove unused server_address variable from DNS configuration --- podkop/files/usr/lib/sing_box_config_manager.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 6bc5274..3cf8e46 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -225,7 +225,6 @@ sing_box_cm_add_fakeip_dns_server() { echo "$config" | jq \ --arg tag "$tag" \ - --arg server_address "$server_address" \ --arg inet4_range "$inet4_range" \ '.dns.servers += [{ type: "fakeip", From 3d3fbe3bfbe5f3f124718c88fbbd447b9fccfead Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 16:58:40 +0500 Subject: [PATCH 47/58] fix: Fix variable name in HTTP proxy check for wget command --- podkop/files/usr/lib/helpers.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 6a80f05..268efde 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -217,7 +217,7 @@ download_to_stream() { local wait="${4:-2}" for attempt in $(seq 1 "$retries"); do - if [ -n "$http_proxy_url" ]; then + 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 @@ -237,8 +237,8 @@ download_to_file() { local wait="${5:-2}" for attempt in $(seq 1 "$retries"); do - if [ -n "$http_proxy_url" ]; then - http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_url" wget -O "$filepath" "$url" && break + 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 From 8dd33cdde23a278322d128ad9a8e0bae9b2279c7 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 17:01:39 +0500 Subject: [PATCH 48/58] fix: Remove non-existent filename variable --- podkop/files/usr/lib/helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 268efde..430c771 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -248,7 +248,7 @@ download_to_file() { done if grep -q $'\r' "$filepath"; then - log "$filename has Windows line endings (CRLF). Converting to Unix (LF)" + log "Downloaded file has Windows line endings (CRLF). Converting to Unix (LF)" sed -i 's/\r$//' "$filepath" fi } From 304c57edfaa2ecd69f32b7b1e93e80cb0aa30cac Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 17:04:22 +0500 Subject: [PATCH 49/58] fix: Fix translation for "Local Subnet List Paths" --- luci-app-podkop/po/ru/podkop.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index ad3be69..04ab973 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -902,7 +902,7 @@ msgid "Local Subnet Lists" msgstr "Локальные списки подсетей" msgid "Local Subnet List Paths" -msgstr "Пути к локальным спискам доменов" +msgstr "Пути к локальным спискам подсетей" msgid "Config File Path" msgstr "Путь к файлу конфигурации" \ No newline at end of file From 054ed355cf1fc0956c06921ff3870b6793493403 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Thu, 11 Sep 2025 17:52:47 +0500 Subject: [PATCH 50/58] fix: Add packet_encoding support for VLESS outbound configuration --- podkop/files/usr/lib/sing_box_config_facade.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index 601afde..63a590c 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -58,14 +58,15 @@ sing_box_cf_add_proxy_outbound() { local scheme="${url%%://*}" case "$scheme" in vless) - local tag host port uuid flow + 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") + config=$(sing_box_cm_add_vless_outbound "$config" "$tag" "$host" "$port" "$uuid" "$flow" "" "$packet_encoding") local transport transport=$(url_get_query_param "$url" "type") From 884574951727c6f648e2d5c36fdcd3d41a19ebd2 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 12 Sep 2025 14:03:32 +0500 Subject: [PATCH 51/58] chore: Update README.md to mark completed tasks --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bf36f08..3cbbc55 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/mai Основные задачи в issues. ## Рефактор -- [ ] Очевидные повторения в `/usr/bin/podkop` загнать в переменые -- [ ] Возможно поменять структуру +- [x] Очевидные повторения в `/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. From 7d082c5def29db79a706ef8b15f511600aa52cdc Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Fri, 12 Sep 2025 14:05:11 +0500 Subject: [PATCH 52/58] chore: Update README task status from done to pending --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cbbc55..7046be9 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/mai Основные задачи в issues. ## Рефактор -- [x] Очевидные повторения в `/usr/bin/podkop` загнать в переменые +- [ ] Очевидные повторения в `/usr/bin/podkop` загнать в переменые - [x] Возможно поменять структуру ## Списки From 0aa0a4a9c81fd339a13c5d6d721bb7251a4e3c2c Mon Sep 17 00:00:00 2001 From: itdoginfo Date: Sat, 13 Sep 2025 19:23:09 +0300 Subject: [PATCH 53/58] Add /usr/lib/podkop path --- podkop/Makefile | 3 +++ 1 file changed, 3 insertions(+) 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)) From 4897d3d2920c8adf84cedc1546770b71b6911391 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 14 Sep 2025 08:55:34 +0500 Subject: [PATCH 54/58] refactor: Split nftables rules for TCP and UDP protocols separately --- podkop/files/usr/bin/podkop | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 8a377ff..c9c5672 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -343,14 +343,19 @@ create_nft_table() { 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 rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto '{ tcp, 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, udp }' 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 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 "$NFT_TABLE_NAME" proxy meta mark 0x105 meta l4proto '{ tcp, udp }' tproxy ip to 127.0.0.1:1602 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 "$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, udp }' meta mark set 0x105 counter - nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto '{ tcp, udp }' meta mark set 0x105 counter + 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 } backup_dnsmasq_config_option() { @@ -1438,7 +1443,8 @@ nft_list_all_traffic_from_ip() { local ip="$1" 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, udp }' meta mark set 0x105 counter + 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 } From db956452d1a0f03690eacf949c8c018e5843e4fe Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 14 Sep 2025 09:10:42 +0500 Subject: [PATCH 55/58] refactor: Move logging functions to library file --- podkop/files/usr/bin/podkop | 28 +--------------------------- podkop/files/usr/lib/constants.sh | 4 ---- podkop/files/usr/lib/logging.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 podkop/files/usr/lib/logging.sh diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index c9c5672..965bba0 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -8,36 +8,10 @@ PODKOP_LIB="/usr/lib/podkop" . "$PODKOP_LIB/helpers.sh" . "$PODKOP_LIB/sing_box_config_manager.sh" . "$PODKOP_LIB/sing_box_config_facade.sh" +. "$PODKOP_LIB/logging.sh" config_load "$PODKOP_CONFIG" -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" -} - start_main() { log "Starting podkop" diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index d42aac2..eaa3572 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -9,10 +9,6 @@ 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 -# Color constants -COLOR_CYAN="\033[0;36m" -COLOR_GREEN="\033[0;32m" -COLOR_RESET="\033[0m" ## nft NFT_TABLE_NAME="PodkopTable" 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" +} From 4a17cf66a31510b36fccf205216d435ab8bf8826 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 14 Sep 2025 09:26:26 +0500 Subject: [PATCH 56/58] refactor: Add file existence checks and improve startup reliability --- podkop/files/usr/bin/podkop | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 965bba0..bd83231 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1,8 +1,25 @@ #!/bin/ash -[ -r /lib/functions.sh ] && . /lib/functions.sh -[ -r /lib/config/uci.sh ] && . /lib/config/uci.sh +check_required_file() { + local file="$1" + + if [ ! -r "$file" ]; then + echo "Error: required file '$file' is missing or not readable" >&2 + exit 1 + fi +} + 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" From 27719f90eeddfbf9e58954754c8a07357595f8e6 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Sun, 14 Sep 2025 17:51:20 +0500 Subject: [PATCH 57/58] feat: Add support for DoH URLs with paths and UDP port specification --- .../resources/view/podkop/additionalTab.js | 32 ++----- podkop/files/usr/bin/podkop | 13 ++- podkop/files/usr/lib/helpers.sh | 86 ++++++++++--------- .../files/usr/lib/sing_box_config_facade.sh | 30 ++++--- 4 files changed, 77 insertions(+), 84 deletions(-) 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 8c42e9f..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'); } diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index bd83231..32592a4 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -687,14 +687,16 @@ sing_box_configure_dns() { fi config=$(sing_box_cm_configure_dns "$config" "$final_dns_server" "ipv4_only" true) - local dns_type dns_server split_dns_type split_dns_server + 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") local need_dns_domain_resolver=0 - if ! is_ipv4 "$dns_server" || ! is_ipv4 "$split_dns_server"; then + if ! is_ipv4 "$dns_server_address" || ! is_ipv4 "$split_dns_server_address"; then need_dns_domain_resolver=1 fi @@ -715,15 +717,12 @@ sing_box_configure_dns() { dns_domain_resolver="$SB_DNS_DOMAIN_RESOLVER_TAG" fi - config=$( - sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "" "" \ - "$dns_domain_resolver" - ) + 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" + "$dns_domain_resolver" "$SB_MAIN_OUTBOUND_TAG" ) fi diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 430c771..efaf29a 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -26,7 +26,7 @@ is_domain() { is_base64() { local str="$1" - if echo "$str" | base64 -d >/dev/null 2>&1; then + if echo "$str" | base64 -d > /dev/null 2>&1; then return 0 fi return 1 @@ -113,19 +113,25 @@ url_decode() { # Extracts the userinfo (username[:password]) part from a URL url_get_userinfo() { local url="$1" - echo "$url" | sed -n 's#^[^:]*://\([^@]*\)@.*#\1#p' + 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 's#^[^:]*://[^@]*@\([^:/?#]*\).*#\1#p' + 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 's#^[^:]*://[^@]*@[^:/?#]*:\([0-9]*\).*#\1#p' + 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 @@ -157,8 +163,8 @@ url_get_file_extension() { local basename="${url##*/}" case "$basename" in - *.*) echo "${basename##*.}" ;; - *) echo "" ;; + *.*) echo "${basename##*.}" ;; + *) echo "" ;; esac } @@ -167,7 +173,7 @@ base64_decode() { local str="$1" local decoded_url - decoded_url="$(echo "$str" | base64 -d 2>/dev/null)" + decoded_url="$(echo "$str" | base64 -d 2 > /dev/null)" echo "$decoded_url" } @@ -288,22 +294,22 @@ parse_domain_or_subnet_string_to_commas_string() { 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 - ;; + 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 @@ -334,22 +340,22 @@ parse_domain_or_subnet_file_to_comma_string() { [ -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 - ;; + 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 @@ -360,4 +366,4 @@ parse_domain_or_subnet_file_to_comma_string() { done < "$filepath" echo "$result" -} \ 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 index 63a590c..657ca7e 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -6,24 +6,32 @@ sing_box_cf_add_dns_server() { local config="$1" local type="$2" local tag="$3" - local server_address="$4" - local path="$5" - local headers="$6" - local domain_resolver="$7" - local detour="$8" + 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) - config=$(sing_box_cm_add_udp_dns_server "$config" "$tag" "$server_address" 53 "$domain_resolver" "$detour") + [ -z "$server_port" ] && server_port=53 + config=$(sing_box_cm_add_udp_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \ + "$detour") ;; dot) - config=$(sing_box_cm_add_tls_dns_server "$config" "$tag" "$server_address" 853 "$domain_resolver" "$detour") + [ -z "$server_port" ] && server_port=853 + config=$(sing_box_cm_add_tls_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \ + "$detour") ;; doh) - config=$( - sing_box_cm_add_https_dns_server "$config" "$tag" "$server_address" 443 "$path" "$headers" \ - "$domain_resolver" "$detour" - ) + [ -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" From ee246895de1468f3029a9d808d3d7d8eb0f103bb Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Mon, 15 Sep 2025 17:41:17 +0500 Subject: [PATCH 58/58] fix: Redirect base64 decode errors to /dev/null --- podkop/files/usr/lib/helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index efaf29a..2795cb4 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -173,7 +173,7 @@ base64_decode() { local str="$1" local decoded_url - decoded_url="$(echo "$str" | base64 -d 2 > /dev/null)" + decoded_url="$(echo "$str" | base64 -d 2> /dev/null)" echo "$decoded_url" }