Files
podkop/podkop/files/usr/bin/podkop
2025-10-12 14:55:57 +03:00

2393 lines
82 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/ash
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"
. "$PODKOP_LIB/sing_box_config_manager.sh"
. "$PODKOP_LIB/sing_box_config_facade.sh"
. "$PODKOP_LIB/logging.sh"
config_load "$PODKOP_CONFIG"
check_requirements() {
log "Check Requirements"
local sing_box_version jq_version coreutils_base64_version
sing_box_version="$(sing-box version | head -n1 | awk '{print $3}')"
jq_version="$(jq --version | awk -F- '{print $2}')"
coreutils_base64_version="$(base64 --version | head -n1 | awk '{print $4}')"
if [ -z "$sing_box_version" ]; then
log "Package 'sing-box' is not installed." "error"
exit 1
else
if ! is_min_package_version "$sing_box_version" "$SB_REQUIRED_VERSION"; then
log "Package 'sing-box' version ($sing_box_version) is lower than the required minimum ($SB_REQUIRED_VERSION). Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box" "error"
exit 1
fi
if ! service_exists "sing-box"; then
log "Service 'sing-box' is missing. Please install the official package to ensure the service is available." "error"
exit 1
fi
fi
if [ -z "$jq_version" ]; then
log "Package 'jq' is not installed." "error"
exit 1
elif ! is_min_package_version "$jq_version" "$JQ_REQUIRED_VERSION"; then
log "Package 'jq' version ($jq_version) is lower than the required minimum ($JQ_REQUIRED_VERSION)." "error"
exit 1
fi
if [ -z "$coreutils_base64_version" ]; then
log "Package 'coreutils-base64' is not installed." "error"
exit 1
elif ! is_min_package_version "$coreutils_base64_version" "$COREUTILS_BASE64_REQUIRED_VERSION"; then
log "Package 'coreutils-base64' version ($coreutils_base64_version) is lower than the required minimum ($COREUTILS_BASE64_REQUIRED_VERSION). This may cause issues when decoding base64 streams with missing padding, as automatic padding support is not available in older versions." "warn"
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" "warn"
fi
local proxy_string interface outbound_json urltest_proxy_links
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
config_get urltest_proxy_links "main" "urltest_proxy_links"
if [ -z "$proxy_string" ] && [ -z "$interface" ] && [ -z "$outbound_json" ] && [ -z "$urltest_proxy_links" ]; then
log "Required options (proxy_string, interface, outbound_json, urltest_proxy_links) are missing in 'main' section. Aborted." "error"
exit 1
fi
}
start_main() {
log "Starting podkop"
check_requirements
migration
config_foreach process_validate_service "section"
br_netfilter_disable
# Sync time for DoH/DoT
/usr/sbin/ntpd -q -p 194.190.168.1 -p 216.239.35.0 -p 216.239.35.4 -p 162.159.200.1 -p 162.159.200.123
sleep 1
mkdir -p "$TMP_SING_BOX_FOLDER"
mkdir -p "$TMP_RULESET_FOLDER"
# base
route_table_rule_mark
create_nft_table
sing_box_uci
# sing-box
sing_box_init_config
config_foreach add_cron_job "section"
/etc/init.d/sing-box start
local exclude_ntp
config_get_bool exclude_ntp "settings" "exclude_ntp" "0"
if [ "$exclude_ntp" -eq 1 ]; then
log "NTP traffic exclude for proxy"
nft insert rule inet "$NFT_TABLE_NAME" mangle udp dport 123 return
fi
log "Nice"
list_update &
echo $! > /var/run/podkop_list_update.pid
}
start() {
start_main
config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_add_resolver
fi
uci_set "podkop" "settings" "shutdown_correctly" 0
uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
stop_main() {
log "Stopping the podkop"
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
log "Stopped list_update"
fi
rm -f /var/run/podkop_list_update.pid
fi
remove_cron_job
rm -f "$TMP_RULESET_FOLDER"/*
log "Flush nft"
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"
if ip rule list | grep -q "podkop"; then
ip rule del fwmark 0x105 table podkop priority 105
fi
log "Flush ip route"
if ip route list table podkop > /dev/null 2>&1; then
ip route flush table podkop
fi
log "Stop sing-box"
/etc/init.d/sing-box stop
}
stop() {
local dont_touch_dhcp
config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_restore
fi
stop_main
uci_set "podkop" "settings" "shutdown_correctly" 1
uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
reload() {
log "Podkop reload"
stop_main
start_main
}
restart() {
log "Podkop restart"
stop
start
}
# Migrations and validation funcs
migration() {
:
}
validate_service() {
local service="$1"
for community_service in $COMMUNITY_SERVICES; do
if [ "$service" = "$community_service" ]; then
return 0
fi
done
log "Invalid service in community lists: $service. Check config and LuCI cache. Aborted." "fatal"
exit 1
}
process_validate_service() {
local section="$1"
local community_lists
config_get community_lists "$section" "community_lists"
if [ -n "$community_lists" ]; then
config_list_foreach "$section" "community_lists" validate_service
fi
}
br_netfilter_disable() {
if lsmod | grep -q br_netfilter && [ "$(sysctl -n net.bridge.bridge-nf-call-iptables 2> /dev/null)" = "1" ]; then
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
}
# Main funcs
route_table_rule_mark() {
local table=podkop
grep -q "105 $table" /etc/iproute2/rt_tables || echo "105 $table" >> /etc/iproute2/rt_tables
if ! ip route list table $table | grep -q "local default dev lo scope host"; then
log "Added route for tproxy"
ip route add local 0.0.0.0/0 dev lo table $table
else
log "Route for tproxy exists"
fi
if ! ip rule list | grep -q "from all fwmark 0x105 lookup $table"; then
log "Create marking rule"
ip -4 rule add fwmark 0x105 table $table priority 105
else
log "Marking rule exist"
fi
}
nft_init_interfaces_set() {
nft_create_ifname_set "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME"
local source_network_interfaces
config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan"
for interface in $source_network_interfaces; do
nft add element inet "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" "{ $interface }"
done
}
create_nft_table() {
log "Create nft table"
nft_create_table "$NFT_TABLE_NAME"
nft_init_interfaces_set
log "Create localv4 set"
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 "$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 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 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 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() {
local key="$1"
local backup_key="$2"
local value
value="$(uci_get "dhcp" "@dnsmasq[0]" "$key")"
if [ -n "$value" ]; then
uci_set "dhcp" "@dnsmasq[0]" "$backup_key" "$value"
fi
}
dnsmasq_add_resolver() {
local shutdown_correctly
config_get shutdown_correctly "settings" "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
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
backup_dnsmasq_config_option "noresolv" "podkop_noresolv"
backup_dnsmasq_config_option "cachesize" "podkop_cachesize"
log "Configure dnsmasq for sing-box"
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 shutdown_correctly
config_get shutdown_correctly "settings" "shutdown_correctly"
if [ "$shutdown_correctly" -eq 1 ]; then
log "Previous shutdown of podkop was correct, reconfiguration of dnsmasq is not required"
return 0
fi
local cachesize noresolv backup_servers resolvfile
log "Restoring cachesize" "debug"
cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_cachesize")"
if [ -z "$cachesize" ]; then
uci_remove "dhcp" "@dnsmasq[0]" "cachesize"
uci_set "dhcp" "@dnsmasq[0]" "cachesize" 150
else
uci_set "dhcp" "@dnsmasq[0]" "cachesize" "$cachesize"
uci_remove "dhcp" "@dnsmasq[0]" "podkop_cachesize"
fi
log "Restoring noresolv" "debug"
noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_noresolv")"
if [ -z "$noresolv" ]; then
uci_remove "dhcp" "@dnsmasq[0]" "noresolv"
uci_set "dhcp" "@dnsmasq[0]" "noresolv" 0
else
uci_set "dhcp" "@dnsmasq[0]" "noresolv" "$noresolv"
uci_remove "dhcp" "@dnsmasq[0]" "podkop_noresolv"
fi
log "Restoring DNS servers" "debug"
uci_remove "dhcp" "@dnsmasq[0]" "server"
resolvfile="/tmp/resolv.conf.d/resolv.conf.auto"
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"
elif file_exists "$resolvfile"; then
log "Backup DNS servers not found, using default resolvfile" "debug"
uci_set "dhcp" "@dnsmasq[0]" "resolvfile" "$resolvfile"
else
log "Backup DNS servers and default resolvfile not found, possible resolving issues" "warn"
fi
uci_commit "dhcp"
/etc/init.d/dnsmasq restart
}
add_cron_job() {
## Future: make a check so that it doesn't recreate many times
local community_lists remote_domain_lists remote_subnet_lists update_interval
config_get community_lists "$section" "community_lists"
config_get remote_domain_lists "$section" "remote_domain_lists"
config_get remote_subnet_lists "$section" "remote_subnet_lists"
config_get update_interval "settings" "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
;;
esac
if [ -n "$community_lists" ] ||
[ -n "$remote_domain_lists" ] ||
[ -n "$remote_subnet_lists" ]; then
remove_cron_job
crontab -l | {
cat
echo "$cron_job"
} | crontab -
log "The cron job has been created: $cron_job"
fi
}
remove_cron_job() {
(crontab -l | grep -v "/usr/bin/podkop list_update") | crontab -
log "The cron job removed"
}
list_update() {
echolog "🔄 Starting lists update..."
local i
for i in $(seq 1 60); do
if nslookup -timeout=1 openwrt.org > /dev/null 2>&1; then
echolog "✅ DNS check passed"
break
fi
log "DNS is unavailable [$i/60]"
sleep 3
done
if [ "$i" -eq 60 ]; then
echolog "❌ DNS check failed after 60 attempts"
return 1
fi
for i in $(seq 1 60); do
config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" "0"
if [ "$download_lists_via_proxy" -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
echolog "✅ GitHub connection check passed (via proxy)"
break
fi
else
if curl -s -m 3 https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed"
break
fi
fi
echolog "GitHub is unavailable [$i/60]"
sleep 3
done
if [ "$i" -eq 60 ]; then
echolog "❌ GitHub connection check failed after 60 attempts"
return 1
fi
echolog "📥 Downloading and processing lists..."
config_foreach import_community_subnet_lists "section"
config_foreach import_domains_from_remote_domain_lists "section"
config_foreach import_subnets_from_remote_subnet_lists "section"
if [ $? -eq 0 ]; then
echolog "✅ Lists update completed successfully"
else
echolog "❌ Lists update failed"
fi
}
# sing-box funcs
sing_box_uci() {
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 "settings" "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
}
sing_box_init_config() {
local config='{"log":{},"dns":{},"ntp":{},"certificate":{},"endpoints":[],"inbounds":[],"outbounds":[],"route":{},"services":[],"experimental":{}}'
sing_box_configure_log
sing_box_configure_inbounds
sing_box_configure_outbounds
sing_box_configure_dns
sing_box_configure_route
sing_box_configure_experimental
sing_box_additional_inbounds
sing_box_save_config
}
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)
}
sing_box_configure_inbounds() {
log "Configure the inbounds section of a sing-box JSON configuration"
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_configure_outbounds() {
log "Configure the outbounds section of a sing-box JSON configuration"
config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG")
config_foreach configure_outbound_handler "section"
}
configure_outbound_handler() {
local section="$1"
local connection_type
config_get connection_type "$section" "connection_type"
case "$connection_type" in
proxy)
log "Configuring outbound in proxy connection type 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" "debug"
local proxy_string udp_over_tcp
config_get proxy_string "$section" "proxy_string"
config_get udp_over_tcp "$section" "enable_shadowsocks_udp_over_tcp"
# 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 "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" "debug"
local json_outbound
config_get json_outbound "$section" "outbound_json"
config=$(sing_box_cf_add_json_outbound "$config" "$section" "$json_outbound")
;;
urltest)
log "Detected proxy configuration type: urltest" "debug"
local urltest_proxy_links udp_over_tcp i urltest_tag selector_tag outbound_tag outbound_tags \
urltest_outbounds selector_outbounds
config_get urltest_proxy_links "$section" "urltest_proxy_links"
config_get udp_over_tcp "$section" "enable_shadowsocks_udp_over_tcp"
if [ -z "$urltest_proxy_links" ]; then
log "URLTest proxy links is not set. Aborted." "fatal"
exit 1
fi
i=1
for link in $urltest_proxy_links; do
config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")"
outbound_tag="$(get_outbound_tag_by_section "$section-$i")"
if [ -z "$outbound_tags" ]; then
outbound_tags="$outbound_tag"
else
outbound_tags="$outbound_tags,$outbound_tag"
fi
i=$((i + 1))
done
urltest_tag="$(get_outbound_tag_by_section "$section-urltest")"
selector_tag="$(get_outbound_tag_by_section "$section")"
urltest_outbounds="$(comma_string_to_json_array "$outbound_tags")"
selector_outbounds="$(comma_string_to_json_array "$outbound_tags,$urltest_tag")"
config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds")"
config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag")"
;;
*)
log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal"
exit 1
;;
esac
;;
vpn)
log "Configuring outbound in VPN connection type for the $section section"
local interface_name domain_resolver_enabled domain_resolver_dns_type domain_resolver_dns_server \
domain_resolver_dns_server_address outbound_tag domain_resolver_tag dns_domain_resolver
config_get interface_name "$section" "interface"
config_get domain_resolver_enabled "$section" "domain_resolver_enabled"
config_get domain_resolver_dns_type "$section" "domain_resolver_dns_type"
config_get domain_resolver_dns_server "$section" "domain_resolver_dns_server"
if [ -z "$interface_name" ]; then
log "VPN interface is not set. Aborted." "fatal"
exit 1
fi
local outbound_tag
outbound_tag="$(get_outbound_tag_by_section "$section")"
if [ "$domain_resolver_enabled" -eq 1 ]; then
domain_resolver_dns_server_address="$(url_get_host "$dns_server")"
if ! is_ipv4 "$domain_resolver_dns_server_address"; then
dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG
fi
domain_resolver_tag="$(get_domain_resolver_tag "$section")"
config=$(sing_box_cf_add_dns_server "$config" "$domain_resolver_dns_type" "$domain_resolver_tag" \
"$domain_resolver_dns_server" "$dns_domain_resolver" "$outbound_tag")
fi
config=$(sing_box_cm_add_interface_outbound "$config" "$outbound_tag" "$interface_name" "$domain_resolver_tag")
;;
block)
log "Connection type 'block' detected for the $section section no outbound will be created (handled via reject route rules)"
;;
*)
log "Unknown connection type '$connection_type' for the $section section. Aborted." "fatal"
exit 1
;;
esac
}
sing_box_configure_dns() {
log "Configure the DNS section of a sing-box JSON configuration"
config=$(sing_box_cm_configure_dns "$config" "$SB_DNS_SERVER_TAG" "ipv4_only" true)
log "Adding DNS Servers" "debug"
local dns_type dns_server bootstrap_dns_server dns_domain_resolver dns_server_address
config_get dns_type "settings" "dns_type" "doh"
config_get dns_server "settings" "dns_server" "1.1.1.1"
config_get bootstrap_dns_server "settings" "bootstrap_dns_server" "77.88.8.8"
dns_server_address="$(url_get_host "$dns_server")"
if ! is_ipv4 "$dns_server_address"; then
dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG
fi
config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_BOOTSTRAP_SERVER_TAG" "$bootstrap_dns_server" 53)
config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver")
config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE")
log "Adding DNS Rules"
local rewrite_ttl service_domains
config_get rewrite_ttl "settings" "dns_rewrite_ttl" "60"
config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "HTTPS")
config=$(sing_box_cm_add_dns_reject_rule "$config" "domain_suffix" '"use-application-dns.net"')
config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_DNS_RULE_TAG")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rewrite_ttl" "$rewrite_ttl")
service_domains=$(comma_string_to_json_array "$FAKEIP_TEST_DOMAIN,$CHECK_PROXY_IP_DOMAIN")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
}
sing_box_configure_route() {
log "Configure the route section of a sing-box JSON configuration"
config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG")
local sniff_inbounds
sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG")
config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "$sniff_inbounds")
config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns")
local disable_quic
config_get_bool disable_quic "settings" "disable_quic" 0
if [ "$disable_quic" -eq 1 ]; then
config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "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" "$FAKEIP_TEST_DOMAIN" 8443)
config_foreach include_source_ips_in_routing_handler "section"
configure_common_reject_route_rule
local routing_excluded_ips
config_get_bool routing_excluded_ips "settings" "routing_excluded_ips"
if [ -n "$routing_excluded_ips" ]; 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 "settings" "routing_excluded_ips" exclude_source_ip_from_routing_handler "$rule_tag"
fi
config_foreach configure_routing_for_section_lists "section"
}
include_source_ips_in_routing_handler() {
local section="$1"
local fully_routed_ips rule_tag
config_get fully_routed_ips "$section" "fully_routed_ips"
if [ -n "$fully_routed_ips" ]; 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" "fully_routed_ips" include_source_ip_in_routing_handler "$rule_tag"
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"
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"
log "Configuring routing for '$section' section"
if ! section_has_enabled_lists "$section"; then
log "Section '$section' does not have any enabled list, skipping..." "warn"
return 0
fi
local community_lists user_domain_list_type user_subnet_list_type local_domain_lists local_subnet_lists \
remote_domain_lists remote_subnet_lists section_connection_type route_rule_tag
config_get_bool community_lists "$section" "community_lists"
config_get user_domain_list_type "$section" "user_domain_list_type" "disabled"
config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled"
config_get_bool local_domain_lists "$section" "local_domain_lists"
config_get_bool local_subnet_lists "$section" "local_subnet_lists"
config_get_bool remote_domain_lists "$section" "remote_domain_lists"
config_get_bool remote_subnet_lists "$section" "remote_subnet_lists"
config_get section_connection_type "$section" "connection_type"
if [ "$section_connection_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
if [ -n "$community_lists" ]; then
log "Processing community list routing rules for '$section' section"
config_list_foreach "$section" "community_lists" configure_community_list_handler "$section" "$route_rule_tag"
fi
if [ "$user_domain_list_type" != "disabled" ]; then
log "Processing user domains routing rules for '$section' section"
prepare_common_ruleset "$section" "domains" "$route_rule_tag"
configure_user_domain_or_subnets_list "$section" "domains" "$route_rule_tag"
fi
if [ "$user_subnet_list_type" != "disabled" ]; then
log "Processing user subnets routing rules for '$section' section"
prepare_common_ruleset "$section" "subnets" "$route_rule_tag"
configure_user_domain_or_subnets_list "$section" "subnets" "$route_rule_tag"
fi
if [ -n "$local_domain_lists" ]; then
log "Processing local domains routing rules for '$section' section"
configure_local_domain_or_subnet_lists "$section" "domains" "$route_rule_tag"
fi
if [ -n "$local_subnet_lists" ]; then
log "Processing local subnets routing rules for '$section' section"
configure_local_domain_or_subnet_lists "$section" "subnets" "$route_rule_tag"
fi
if [ -n "$remote_domain_lists" ]; then
log "Processing remote domains routing rules for '$section' section"
prepare_common_ruleset "$section" "domains" "$route_rule_tag"
config_list_foreach "$section" "remote_domain_lists" configure_remote_domain_or_subnet_list_handler \
"domains" "$section" "$route_rule_tag"
fi
if [ -n "$remote_subnet_lists" ]; 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" "$type")
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
if file_exists "$ruleset_filepath"; then
log "Ruleset $ruleset_filepath already exists. Skipping." "debug"
else
sing_box_cm_create_local_source_ruleset "$ruleset_filepath"
config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
case "$type" in
domains)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets) ;;
*) log "Unsupported remote rule set type: $type" "warn" ;;
esac
fi
}
configure_community_list_handler() {
local tag="$1"
local section="$2"
local route_rule_tag="$3"
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 "settings" "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")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
}
configure_user_domain_or_subnets_list() {
local section="$1"
local type="$2"
local items ruleset_tag ruleset_filename ruleset_filepath json_array
case "$type" in
domains)
local user_domain_list_type
config_get user_domain_list_type "$section" "user_domain_list_type"
case "$user_domain_list_type" in
dynamic) config_get items "$section" "user_domains" ;;
text) config_get items "$section" "user_domains_text" ;;
esac
;;
subnets)
local user_subnet_list_type
config_get user_subnet_list_type "$section" "user_subnet_list_type"
case "$user_subnet_list_type" in
dynamic) config_get items "$section" "user_subnets" ;;
text) config_get items "$section" "user_subnets_text" ;;
esac
;;
esac
ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
items="$(parse_domain_or_subnet_string_to_commas_string "$items" "$type")"
json_array="$(comma_string_to_json_array "$items")"
case "$type" in
domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
subnets)
sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
;;
esac
}
configure_local_domain_or_subnet_lists() {
local section="$1"
local type="$2"
local route_rule_tag="$3"
local ruleset_tag ruleset_filename ruleset_filepath
ruleset_tag="$(get_ruleset_tag "$section" "local" "$type")"
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
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)
config_list_foreach "$section" "local_domain_lists" import_local_domain_or_subnet_list "$type" \
"$section" "$ruleset_filepath"
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets)
config_list_foreach "$section" "local_subnet_lists" import_local_domain_or_subnet_list "$type" \
"$section" "$ruleset_filepath"
;;
*) log "Unsupported local rule set type: $type" "warn" ;;
esac
}
import_local_domain_or_subnet_list() {
local filepath="$1"
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 items json_array
items="$(parse_domain_or_subnet_file_to_comma_string "$filepath" "$type")"
if [ -z "$items" ]; then
log "No valid $type found in $filepath"
return 0
fi
json_array="$(comma_string_to_json_array "$items")"
case "$type" in
domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
subnets)
sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
;;
esac
}
configure_remote_domain_or_subnet_list_handler() {
local url="$1"
local type="$2"
local section="$3"
local route_rule_tag="$4"
local file_extension
file_extension=$(url_get_file_extension "$url")
case "$file_extension" in
json | srs)
log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
local basename ruleset_tag format detour update_interval
basename=$(url_get_basename "$url")
ruleset_tag=$(get_ruleset_tag "$section" "$basename" "remote-$type")
format="$(get_ruleset_format_by_file_extension "$file_extension")"
detour="$(get_download_detour_tag)"
config_get update_interval "settings" "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)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_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
}
sing_box_configure_experimental() {
log "Configure the experimental section of a sing-box JSON configuration"
log "Configuring cache database"
local cache_file
config_get cache_file "settings" "cache_path" "/tmp/sing-box/cache.db"
config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)
local enable_yacd external_controller_ui
config_get_bool enable_yacd "settings" "enable_yacd" 0
log "Configuring Clash API"
if [ "$enable_yacd" -eq 1 ]; then
log "YACD is enabled, enabling Clash API with downloadable YACD" "debug"
local external_controller_ui="ui"
config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER" "$external_controller_ui")
else
log "YACD is disabled, enabling Clash API in online mode" "debug"
config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER")
fi
}
sing_box_additional_inbounds() {
log "Configure the additional inbounds of a sing-box JSON configuration"
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"
)
config_foreach configure_section_mixed_proxy "section"
}
configure_section_mixed_proxy() {
local section="$1"
local mixed_inbound_enabled mixed_proxy_port mixed_inbound_tag mixed_outbound_tag
config_get_bool mixed_inbound_enabled "$section" "mixed_proxy_enabled" 0
config_get mixed_proxy_port "$section" "mixed_proxy_port"
if [ "$mixed_inbound_enabled" -eq 1 ]; then
mixed_inbound_tag="$(get_inbound_tag_by_section "$section-mixed")"
mixed_outbound_tag="$(get_outbound_tag_by_section "$section")"
config=$(
sing_box_cf_add_mixed_inbound_and_route_rule \
"$config" \
"$mixed_inbound_tag" \
"$SB_MIXED_INBOUND_ADDRESS" \
"$mixed_proxy_port" \
"$mixed_outbound_tag"
)
fi
}
sing_box_save_config() {
local sing_box_config_path temp_file_path current_config_hash temp_config_hash
config_get sing_box_config_path "settings" "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"
sing_box_config_check "$temp_file_path"
current_config_hash=$(md5sum "$sing_box_config_path" 2> /dev/null | awk '{print $1}')
temp_config_hash=$(md5sum "$temp_file_path" | awk '{print $1}')
log "Current sing-box config hash: $current_config_hash" "debug"
log "Temporary sing-box config hash: $temp_config_hash" "debug"
if [ "$current_config_hash" != "$temp_config_hash" ]; then
log "sing-box configuration has changed and will be updated"
mv "$temp_file_path" "$sing_box_config_path"
else
log "sing-box configuration is unchanged"
rm "$temp_file_path"
fi
}
sing_box_config_check() {
local config_path="$1"
if ! sing-box -c "$config_path" check > /dev/null 2>&1; then
log "Sing-box configuration $config_path is invalid" "fatal"
exit 1
fi
}
import_community_subnet_lists() {
local section="$1"
local community_lists
config_get community_lists "$section" "community_lists"
if [ -n "$community_lists" ]; then
log "Importing community subnet lists for '$section' section"
config_list_foreach "$section" "community_lists" import_community_service_subnet_list_handler
fi
}
import_community_service_subnet_list_handler() {
local service="$1"
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 \
"@$NFT_DISCORD_SET_NAME" udp dport '{ 50000-65535 }' meta mark set 0x105 counter
;;
*) return 0 ;;
esac
local tmpfile http_proxy_address subnets
tmpfile=$(mktemp)
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$URL" "$tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
log "Download $service list failed" "error"
return 1
fi
subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")"
rm -f "$tmpfile"
if [ "$service" = "discord" ]; then
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" "$subnets"
else
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
fi
}
import_domains_from_remote_domain_lists() {
local section="$1"
local remote_domain_lists
config_get remote_domain_lists "$section" "remote_domain_lists"
if [ -n "$remote_domain_lists" ]; then
log "Importing domains from remote domain lists for '$section' section"
config_list_foreach "$section" "remote_domain_lists" import_domains_from_remote_domain_list_handler "$section"
fi
}
import_domains_from_remote_domain_list_handler() {
local url="$1"
local section="$2"
log "Importing domains from URL: $url"
local file_extension
file_extension=$(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"
;;
esac
}
import_subnets_from_remote_subnet_lists() {
local section="$1"
local remote_subnet_lists
config_get remote_subnet_lists "$section" "remote_subnet_lists"
if [ -n "$remote_subnet_lists" ]; then
log "Importing subnets from remote subnet lists for '$section' section"
config_list_foreach "$section" "remote_subnet_lists" import_subnets_from_remote_subnet_list_handler "$section"
fi
}
import_subnets_from_remote_subnet_list_handler() {
local url="$1"
local section="$2"
log "Importing subnets from URL: $url"
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"
;;
esac
}
import_domains_or_subnets_from_remote_file() {
local url="$1"
local section="$2"
local type="$3"
local tmpfile http_proxy_address items json_array
tmpfile=$(mktemp)
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$url" "$tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
items="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "$type")"
rm -f "$tmpfile"
if [ -z "$items" ]; then
log "No valid $type found in $url"
return 0
fi
ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
json_array="$(comma_string_to_json_array "$items")"
case "$type" in
domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
subnets)
sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
;;
esac
}
import_subnets_from_remote_json_file() {
local url="$1"
local tmpfile subnets http_proxy_address
tmpfile="$(mktemp)"
http_proxy_address="$(get_service_proxy_address)"
download_to_stream "$url" "$http_proxy_address" | jq -r '.rules[].ip_cidr[]?' > "$tmpfile"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")"
rm -f "$tmpfile"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
}
import_subnets_from_remote_srs_file() {
local url="$1"
local binary_tmpfile json_tmpfile subnets_tmpfile subnets http_proxy_address
binary_tmpfile="$(mktemp)"
json_tmpfile="$(mktemp)"
subnets_tmpfile="$(mktemp)"
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$url" "$binary_tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
if ! decompile_srs_file "$binary_tmpfile" "$json_tmpfile"; then
log "Failed to decompile SRS file" "error"
return 1
fi
jq -r '.rules[].ip_cidr[]' "$json_tmpfile" > "$subnets_tmpfile"
subnets="$(parse_domain_or_subnet_file_to_comma_string "$subnets_tmpfile" "subnets")"
rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
}
## Support functions
get_service_proxy_address() {
local download_lists_via_proxy
config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
if [ "$download_lists_via_proxy" -eq 1 ]; then
echo "$SB_SERVICE_MIXED_INBOUND_ADDRESS:$SB_SERVICE_MIXED_INBOUND_PORT"
else
echo ""
fi
}
get_download_detour_tag() {
config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0
if [ "$download_lists_via_proxy" -eq 1 ]; then
echo "$SB_MAIN_OUTBOUND_TAG"
else
echo ""
fi
}
get_block_sections() {
uci show podkop | grep "\.connection_type='block'" | cut -d'.' -f2
}
block_section_exists() {
if uci show podkop | grep -q "\.connection_type='block'"; then
return 0
else
return 1
fi
}
section_has_enabled_lists() {
local section="$1"
local community_lists user_domain_list_type user_subnet_list_type local_domain_lists local_subnet_lists \
remote_domain_lists remote_subnet_lists
config_get_bool community_lists "$section" "community_lists"
config_get user_domain_list_type "$section" "user_domain_list_type" "disabled"
config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled"
config_get_bool local_domain_lists "$section" "local_domain_lists"
config_get_bool local_subnet_lists "$section" "local_subnet_lists"
config_get_bool remote_domain_lists "$section" "remote_domain_lists"
config_get_bool remote_subnet_lists "$section" "remote_subnet_lists"
if [ -n "$community_lists" ] ||
[ "$user_domain_list_type" != "disabled" ] ||
[ "$user_subnet_list_type" != "disabled" ] ||
[ -n "$local_domain_lists" ] ||
[ -n "$local_subnet_lists" ] ||
[ -n "$remote_domain_lists" ] ||
[ -n "$remote_subnet_lists" ]; then
return 0
else
return 1
fi
}
## nftables
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 meta mark set 0x105 counter
nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" meta l4proto udp meta mark set 0x105 counter
nft insert rule inet "$NFT_TABLE_NAME" mangle ip saddr "$ip" ip daddr @localv4 return
fi
}
# Diagnotics
check_proxy() {
local sing_box_config_path
config_get sing_box_config_path "settings" "config_path"
if ! command -v sing-box > /dev/null 2>&1; then
nolog "sing-box is not installed"
return 1
fi
if [ ! -f "$sing_box_config_path" ]; then
nolog "Configuration file not found"
return 1
fi
nolog "Checking sing-box configuration..."
if ! sing-box -c "$sing_box_config_path" check > /dev/null; then
nolog "Invalid configuration"
return 1
fi
jq '
walk(
if type == "object" then
with_entries(
if .key == "uuid" then
.value = "MASKED"
elif .key == "server" then
.value = "MASKED"
elif .key == "server_name" then
.value = "MASKED"
elif .key == "password" then
.value = "MASKED"
elif .key == "public_key" then
.value = "MASKED"
elif .key == "short_id" then
.value = "MASKED"
elif .key == "fingerprint" then
.value = "MASKED"
elif .key == "server_port" then
.value = "MASKED"
else . end
)
else . end
)' "$sing_box_config_path"
nolog "Checking proxy connection..."
for attempt in $(seq 1 5); do
response=$(sing-box tools fetch ifconfig.me -D /etc/sing-box 2> /dev/null)
if echo "$response" | grep -q "^<html\|403 Forbidden"; then
continue
fi
if [[ $response =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
ip=$(echo "$response" | sed -n 's/^[0-9]\+\.[0-9]\+\.[0-9]\+\.\([0-9]\+\)$/X.X.X.\1/p')
nolog "$ip - should match proxy IP"
return 0
elif echo "$response" | grep -q "^[0-9a-fA-F:]*::[0-9a-fA-F:]*$\|^[0-9a-fA-F:]\+$"; then
ip=$(echo "$response" | sed 's/\([0-9a-fA-F]\+:[0-9a-fA-F]\+:[0-9a-fA-F]\+\):.*/\1:XXXX:XXXX:XXXX/')
nolog "$ip - should match proxy IP"
return 0
fi
if [ $attempt -eq 5 ]; then
nolog "Failed to get valid IP address after 5 attempts"
if [ -z "$response" ]; then
nolog "Error: Empty response"
else
nolog "Error response: $response"
fi
return 1
fi
done
}
check_nft() {
if ! command -v nft > /dev/null 2>&1; then
nolog "nft is not installed"
return 1
fi
nolog "Checking $NFT_TABLE_NAME rules..."
# Check if table exists
if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
nolog "$NFT_TABLE_NAME not found"
return 1
fi
local found_hetzner=0
local found_ovh=0
check_domain_list_contains() {
local section="$1"
config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
if [ "$domain_list_enabled" -eq 1 ]; then
config_list_foreach "$section" "domain_list" check_domain_value
fi
}
check_domain_value() {
local domain_value="$1"
if [ "$domain_value" = "hetzner" ]; then
found_hetzner=1
elif [ "$domain_value" = "ovh" ]; then
found_ovh=1
fi
}
config_foreach check_domain_list_contains
if [ "$found_hetzner" -eq 1 ] || [ "$found_ovh" -eq 1 ]; then
local sets="podkop_subnets podkop_domains interfaces podkop_discord_subnets localv4"
nolog "Sets statistics:"
for set_name in $sets; do
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)
echo "- $set_name: $count elements"
fi
done
nolog "Chain configurations:"
# Create a temporary file for processing
local tmp_file=$(mktemp)
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]"
sed -n '/chain proxy {/,/}/p' "$tmp_file" | grep -v "elements" | grep -v "^[[:space:]]*[0-9]"
# Clean up
rm -f "$tmp_file"
else
# Simple view as originally implemented
nolog "Sets configuration:"
nft list table inet "$NFT_TABLE_NAME"
fi
nolog "NFT check completed"
}
check_github() {
nolog "Checking GitHub connectivity..."
if ! curl -m 3 github.com; then
nolog "Error: Cannot connect to GitHub"
return 1
fi
nolog "GitHub is accessible"
nolog "Checking lists availability:"
for url in "$DOMAINS_RU_INSIDE" "$DOMAINS_RU_OUTSIDE" "$DOMAINS_UA" "$DOMAINS_YOUTUBE" \
"$SUBNETS_TWITTER" "$SUBNETS_META" "$SUBNETS_DISCORD"; do
local list_name=$(basename "$url")
config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" "0"
if [ "$download_lists_via_proxy" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -q -O /dev/null "$url"
else
wget -q -O /dev/null "$url"
fi
if [ $? -eq 0 ]; then
nolog "- $list_name: available"
else
nolog "- $list_name: not available"
fi
done
}
check_dnsmasq() {
nolog "Checking dnsmasq configuration..."
local config=$(uci show dhcp.@dnsmasq[0])
if [ -z "$config" ]; then
nolog "No dnsmasq configuration found"
return 1
fi
echo "$config" | while IFS='=' read -r key value; do
nolog "$key = $value"
done
}
check_sing_box_connections() {
nolog "Checking sing-box connections..."
if ! command -v netstat > /dev/null 2>&1; then
nolog "netstat is not installed"
return 1
fi
local connections=$(netstat -tuanp | grep sing-box)
if [ -z "$connections" ]; then
nolog "No active sing-box connections found"
return 1
fi
echo "$connections" | while read -r line; do
nolog "$line"
done
}
check_sing_box_logs() {
nolog "Showing sing-box logs from system journal..."
local logs=$(logread -e sing-box | tail -n 50)
if [ -z "$logs" ]; then
nolog "No sing-box logs found"
return 1
fi
echo "$logs"
}
check_logs() {
nolog "Showing podkop logs from system journal..."
if ! command -v logread > /dev/null 2>&1; then
nolog "Error: logread command not found"
return 1
fi
# Get all logs first
local all_logs=$(logread)
# Find the last occurrence of "Starting podkop"
local start_line=$(echo "$all_logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1)
if [ -z "$start_line" ]; then
nolog "No 'Starting podkop' message found in logs"
return 1
fi
# Output all logs from the last start
echo "$all_logs" | tail -n +"$start_line"
}
show_sing_box_config() {
local sing_box_config_path
config_get sing_box_config_path "settings" "config_path"
nolog "Current sing-box configuration:"
if [ ! -f "$sing_box_config_path" ]; then
nolog "Configuration file not found"
return 1
fi
jq '
walk(
if type == "object" then
with_entries(
if .key == "uuid" then
.value = "MASKED"
elif .key == "server" then
.value = "MASKED"
elif .key == "server_name" then
.value = "MASKED"
elif .key == "password" then
.value = "MASKED"
elif .key == "public_key" then
.value = "MASKED"
elif .key == "short_id" then
.value = "MASKED"
elif .key == "fingerprint" then
.value = "MASKED"
elif .key == "server_port" then
.value = "MASKED"
else . end
)
else . end
)' "$sing_box_config_path"
}
show_config() {
if [ ! -f "$PODKOP_CONFIG" ]; then
nolog "Configuration file not found"
return 1
fi
tmp_config=$(mktemp)
sed -e 's/\(option proxy_string\).*/\1 '\''MASKED'\''/g' \
-e 's/\(option outbound_json\).*/\1 '\''MASKED'\''/g' \
-e 's/\(option second_proxy_string\).*/\1 '\''MASKED'\''/g' \
-e 's/\(option second_outbound_json\).*/\1 '\''MASKED'\''/g' \
-e 's/\(vless:\/\/[^@]*@\)/vless:\/\/MASKED@/g' \
-e 's/\(ss:\/\/[^@]*@\)/ss:\/\/MASKED@/g' \
-e 's/\(pbk=[^&]*\)/pbk=MASKED/g' \
-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|" \
-e 's/\(list urltest_proxy_links\).*/\1 '\''MASKED'\''/g' \
"$PODKOP_CONFIG" > "$tmp_config"
cat "$tmp_config"
rm -f "$tmp_config"
}
show_version() {
echo "$PODKOP_VERSION"
}
show_sing_box_version() {
local version
version=$(sing-box version | head -n 1 | awk '{print $3}')
echo "$version"
}
show_system_info() {
echo "=== OpenWrt Version ==="
grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2
echo
echo "=== Device Model ==="
cat /tmp/sysinfo/model
}
get_sing_box_status() {
local running=0
local enabled=0
local status=""
local version=""
local dns_configured=0
# Check if service is enabled
if [ -x /etc/rc.d/S99sing-box ]; then
enabled=1
fi
# Check if service is running
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
dns_server=$(uci get dhcp.@dnsmasq[0].server 2> /dev/null)
if [ "$dns_server" = "127.0.0.42" ]; then
dns_configured=1
fi
# Format status message
if [ $running -eq 1 ]; then
if [ $enabled -eq 1 ]; then
status="running & enabled"
else
status="running but disabled"
fi
else
if [ $enabled -eq 1 ]; then
status="stopped but enabled"
else
status="stopped & disabled"
fi
fi
echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\",\"dns_configured\":$dns_configured}"
}
get_status() {
local enabled=0
local status=""
# Check if service is enabled
if [ -x /etc/rc.d/S99podkop ]; then
enabled=1
status="enabled"
else
status="disabled"
fi
echo "{\"enabled\":$enabled,\"status\":\"$status\"}"
}
check_dns_available() {
local dns_type dns_server bootstrap_dns_server
config_get dns_type "settings" "dns_type"
config_get dns_server "settings" "dns_server"
config_get bootstrap_dns_server "settings" "bootstrap_dns_server"
local dns_status=0
local local_dns_status=0
local bootstrap_dns_status=0
local dhcp_has_dns_server=0
local domain="google.com"
# Mask NextDNS ID if present
local display_dns_server="$dns_server"
if echo "$dns_server" | grep -q "\.dns\.nextdns\.io$"; then
local nextdns_id
nextdns_id=$(echo "$dns_server" | cut -d'.' -f1)
display_dns_server="$(echo "$nextdns_id" | sed 's/./*/g').dns.nextdns.io"
elif echo "$dns_server" | grep -q "^dns\.nextdns\.io/"; then
local masked_path
masked_path=$(echo "$dns_server" | cut -d'/' -f2- | sed 's/./*/g')
display_dns_server="dns.nextdns.io/$masked_path"
fi
if [ "$dns_type" = "doh" ]; then
# Check if dns_server already contains a path
local doh_path="/dns-query"
if echo "$dns_server" | grep -q "/"; then
# Path is already present, extract it
doh_path="/$(echo "$dns_server" | cut -d'/' -f2-)"
dns_server="$(echo "$dns_server" | cut -d'/' -f1)"
fi
if dig @"$dns_server" "$domain" +https="$doh_path" +timeout=2 +tries=1 > /dev/null 2>&1; then
dns_status=1
fi
elif [ "$dns_type" = "dot" ]; then
if dig @"$dns_server" "$domain" +tls +timeout=2 +tries=1 > /dev/null 2>&1; then
dns_status=1
fi
elif [ "$dns_type" = "udp" ]; then
if dig @"$dns_server" "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then
dns_status=1
fi
fi
# Check if local DNS resolver is working
if dig @127.0.0.1 "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then
local_dns_status=1
fi
# Check bootstrap DNS server
if [ -n "$bootstrap_dns_server" ]; then
if dig @"$bootstrap_dns_server" "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then
bootstrap_dns_status=1
fi
fi
# Check if /etc/config/dhcp has server 127.0.0.42
config_load dhcp
config_foreach check_dhcp_has_podkop_dns dnsmasq
config_load "$PODKOP_CONFIG"
echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"dns_status\":$dns_status,\"local_dns_status\":$local_dns_status,\"bootstrap_dns_server\":\"$bootstrap_dns_server\",\"bootstrap_dns_status\":$bootstrap_dns_status,\"dhcp_has_dns_server\":$dhcp_has_dns_server}" | jq .
}
check_dhcp_has_podkop_dns() {
local server_list
config_get server_list "$1" "server"
if [ -n "$server_list" ]; then
for server in $server_list; do
if [ "$server" = "127.0.0.42" ]; then
dhcp_has_dns_server=1
return 0
fi
done
fi
}
check_nft_rules() {
local table_exist=0
local rules_mangle_exist=0
local rules_mangle_counters=0
local rules_mangle_output_exist=0
local rules_mangle_output_counters=0
local rules_proxy_exist=0
local rules_proxy_counters=0
local rules_other_mark_exist=0
# Generate traffic through PodkopTable
curl -m 3 -s "http://ip.podkop.fyi/check" > /dev/null 2>&1 &
local pid1=$!
curl -m 3 -s "http://fakeip.podkop.fyi/check" > /dev/null 2>&1 &
local pid2=$!
wait $pid1 2>/dev/null
wait $pid2 2>/dev/null
sleep 1
# Check if PodkopTable exists
if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then
table_exist=1
# Check mangle chain rules
if nft list chain inet "$NFT_TABLE_NAME" mangle > /dev/null 2>&1; then
local mangle_output
mangle_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle)
if echo "$mangle_output" | grep -q "counter"; then
rules_mangle_exist=1
if echo "$mangle_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then
rules_mangle_counters=1
fi
fi
fi
# Check mangle_output chain rules
if nft list chain inet "$NFT_TABLE_NAME" mangle_output > /dev/null 2>&1; then
local mangle_output_output
mangle_output_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle_output)
if echo "$mangle_output_output" | grep -q "counter"; then
rules_mangle_output_exist=1
if echo "$mangle_output_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then
rules_mangle_output_counters=1
fi
fi
fi
# Check proxy chain rules
if nft list chain inet "$NFT_TABLE_NAME" proxy > /dev/null 2>&1; then
local proxy_output
proxy_output=$(nft list chain inet "$NFT_TABLE_NAME" proxy)
if echo "$proxy_output" | grep -q "counter"; then
rules_proxy_exist=1
if echo "$proxy_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then
rules_proxy_counters=1
fi
fi
fi
fi
# Check for other mark rules outside PodkopTable
nft list tables 2>/dev/null | while read -r _ family table_name; do
[ -z "$table_name" ] && continue
[ "$table_name" = "$NFT_TABLE_NAME" ] && continue
if nft list table "$family" "$table_name" 2>/dev/null | grep -q "meta mark set"; then
touch /tmp/podkop_mark_check.$$
break
fi
done
if [ -f /tmp/podkop_mark_check.$$ ]; then
rules_other_mark_exist=1
rm -f /tmp/podkop_mark_check.$$
fi
echo "{\"table_exist\":$table_exist,\"rules_mangle_exist\":$rules_mangle_exist,\"rules_mangle_counters\":$rules_mangle_counters,\"rules_mangle_output_exist\":$rules_mangle_output_exist,\"rules_mangle_output_counters\":$rules_mangle_output_counters,\"rules_proxy_exist\":$rules_proxy_exist,\"rules_proxy_counters\":$rules_proxy_counters,\"rules_other_mark_exist\":$rules_other_mark_exist}" | jq .
}
check_sing_box() {
local sing_box_installed=0
local sing_box_version_ok=0
local sing_box_service_exist=0
local sing_box_autostart_disabled=0
local sing_box_process_running=0
local sing_box_ports_listening=0
# Check if sing-box is installed
if command -v sing-box > /dev/null 2>&1; then
sing_box_installed=1
# Check version (must be >= 1.12.4)
local version
version=$(sing-box version 2>/dev/null | head -n 1 | awk '{print $3}')
if [ -n "$version" ]; then
version=$(echo "$version" | sed 's/^v//')
local major
local minor
local patch
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
patch=$(echo "$version" | cut -d. -f3)
# Compare version: must be >= 1.12.4
if [ "$major" -gt 1 ] || \
[ "$major" -eq 1 ] && [ "$minor" -gt 12 ] || \
[ "$major" -eq 1 ] && [ "$minor" -eq 12 ] && [ "$patch" -ge 4 ]; then
sing_box_version_ok=1
fi
fi
fi
# Check if service exists
if [ -f /etc/init.d/sing-box ]; then
sing_box_service_exist=1
if ! /etc/init.d/sing-box enabled 2>/dev/null; then
sing_box_autostart_disabled=1
fi
fi
# Check if process is running
if pgrep "sing-box" > /dev/null 2>&1; then
sing_box_process_running=1
fi
# Check if sing-box is listening on required ports
local port_53_ok=0
local port_1602_ok=0
if netstat -ln 2>/dev/null | grep -q "127.0.0.42:53"; then
port_53_ok=1
fi
if netstat -ln 2>/dev/null | grep -q "127.0.0.1:1602"; then
port_1602_ok=1
fi
# Both ports must be listening
if [ "$port_53_ok" = "1" ] && [ "$port_1602_ok" = "1" ]; then
sing_box_ports_listening=1
fi
echo "{\"sing_box_installed\":$sing_box_installed,\"sing_box_version_ok\":$sing_box_version_ok,\"sing_box_service_exist\":$sing_box_service_exist,\"sing_box_autostart_disabled\":$sing_box_autostart_disabled,\"sing_box_process_running\":$sing_box_process_running,\"sing_box_ports_listening\":$sing_box_ports_listening}" | jq .
}
#######################################
# Clash API interface for managing proxies and groups
# Arguments:
# $1 - Action: get_proxies, get_proxy_latency, get_group_latency, set_group_proxy
# $2 - Proxy/Group tag (required for latency and set operations)
# $3 - Timeout in ms (optional, defaults: 2000 for proxy, 5000 for group) or target proxy tag for set_group_proxy
# Outputs:
# JSON formatted response
# Usage:
# clash_api get_proxies
# clash_api get_proxy_latency <proxy_tag> [timeout]
# clash_api get_group_latency <group_tag> [timeout]
# clash_api set_group_proxy <group_tag> <proxy_tag>
#######################################
clash_api() {
local CLASH_URL="127.0.0.1:9090"
local TEST_URL="https://www.gstatic.com/generate_204"
local action="$1"
case "$action" in
get_proxies)
curl -s "$CLASH_URL/proxies" | jq .
;;
get_proxy_latency)
local proxy_tag="$2"
local timeout="${3:-2000}"
if [ -z "$proxy_tag" ]; then
echo '{"error":"proxy_tag required"}' | jq .
return 1
fi
curl -G -s "$CLASH_URL/proxies/$proxy_tag/delay" \
--data-urlencode "url=$TEST_URL" \
--data-urlencode "timeout=$timeout" | jq .
;;
get_group_latency)
local group_tag="$2"
local timeout="${3:-5000}"
if [ -z "$group_tag" ]; then
echo '{"error":"group_tag required"}' | jq .
return 1
fi
curl -G -s "$CLASH_URL/group/$group_tag/delay" \
--data-urlencode "url=$TEST_URL" \
--data-urlencode "timeout=$timeout" | jq .
;;
set_group_proxy)
local group_tag="$2"
local proxy_tag="$3"
if [ -z "$group_tag" ] || [ -z "$proxy_tag" ]; then
echo '{"error":"group_tag and proxy_tag required"}' | jq .
return 1
fi
local response
response=$(curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$group_tag" \
--data-raw "{\"name\":\"$proxy_tag\"}")
local http_code
local body
http_code=$(echo "$response" | tail -n 1)
body=$(echo "$response" | sed '$d')
case "$http_code" in
204)
echo "{\"success\":true,\"group\":\"$group_tag\",\"proxy\":\"$proxy_tag\"}" | jq .
;;
404)
echo "{\"success\":false,\"error\":\"group_not_found\",\"message\":\"$group_tag does not exist\"}" | jq .
return 1
;;
400)
if echo "$body" | grep -q "not found"; then
echo "{\"success\":false,\"error\":\"proxy_not_found\",\"message\":\"$proxy_tag not found in group $group_tag\"}" | jq .
else
echo '{"success":false,"error":"bad_request","message":"Invalid request"}' | jq .
fi
return 1
;;
*)
if [ -n "$body" ]; then
local body_json
body_json=$(echo "$body" | jq -c .)
echo "{\"success\":false,\"http_code\":$http_code,\"body\":$body_json}" | jq .
else
echo "{\"success\":false,\"http_code\":$http_code}" | jq .
fi
return 1
;;
esac
;;
*)
echo '{"error":"unknown action","available":["get_proxies","get_proxy_latency","get_group_latency","set_group_proxy"]}' | jq .
return 1
;;
esac
}
print_global() {
local message="$1"
echo "$message"
}
global_check() {
local PODKOP_LUCI_VERSION="Unknown"
[ -n "$1" ] && PODKOP_LUCI_VERSION="$1"
print_global "📡 Global check run!"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🛠️ System info"
print_global "🕳️ Podkop: ${PODKOP_VERSION}"
print_global "🕳️ LuCI App: ${PODKOP_LUCI_VERSION}"
print_global "📦 Sing-box: $(sing-box version | head -n 1 | awk '{print $3}')"
print_global "🛜 OpenWrt: $(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)"
print_global "🛜 Device: $(cat /tmp/sysinfo/model)"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "📄 Podkop config"
show_config
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🔧 System check"
if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then
print_global "❌ /etc/resolv.conf contains external nameserver:"
cat /etc/resolv.conf
echo ""
else
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)"
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
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
print_global "❌ sing-box is not running"
else
print_global "✅ sing-box is running"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🧱 NFT table"
check_nft
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "📄 WAN config"
if uci show network.wan > /dev/null 2>&1; then
awk '
/^config / {
p = ($2 == "interface" && $3 == "'\''wan'\''")
proto = ""
}
p {
if ($1 == "option" && $2 == "proto") {
proto = $3
print
} else if (proto == "'\''static'\''" && $1 == "option" && ($2 == "ipaddr" || $2 == "netmask" || $2 == "gateway")) {
print " option", $2, "'\''******'\''"
} else if (proto == "'\''pppoe'\''" && $1 == "option" && ($2 == "username" || $2 == "password")) {
print " option", $2, "'\''******'\''"
} else {
print
}
}
' /etc/config/network
else
print_global "❌ WAN configuration not found"
fi
if uci show network | grep -q endpoint_host; then
uci show network | grep endpoint_host | cut -d'=' -f2 | tr -d "'\" " | while read -r host; do
if [ "$host" = "engage.cloudflareclient.com" ]; then
print_global "⚠️ WARP detected: $host"
continue
fi
ip_prefix=$(echo "$host" | cut -d'.' -f1,2)
if echo "$CLOUDFLARE_OCTETS" | grep -wq "$ip_prefix"; then
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "⚠️ WARP detected: $host"
fi
done
fi
if uci show network | grep -q route_allowed_ips; then
uci show network | grep route_allowed_ips | cut -d"'" -f2 | while read -r value; do
if [ "$value" = "1" ]; then
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "⚠️ WG Route allowed IP enabled"
continue
fi
done
fi
if [ -f "/etc/init.d/zapret" ]; then
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "⚠️ Zapret detected"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "➡️ DNS status"
dns_info=$(check_dns_available)
dns_type=$(echo "$dns_info" | jq -r '.dns_type')
dns_server=$(echo "$dns_info" | jq -r '.dns_server')
status=$(echo "$dns_info" | jq -r '.status')
print_global "$dns_type ($dns_server) is $status"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🔁 FakeIP"
print_global "➡️ DNS resolution: system DNS server"
nslookup -timeout=2 $FAKEIP_TEST_DOMAIN
print_global "➡️ DNS resolution: sing-box DNS server (127.0.0.42)"
local result
result=$(nslookup -timeout=2 $FAKEIP_TEST_DOMAIN 127.0.0.42 2>&1)
echo "$result"
if echo "$result" | grep -q "198.18"; then
print_global "✅ FakeIP is working correctly on router (198.18.x.x)"
else
print_global "❌ FakeIP test failed: Domain did not resolve to FakeIP range"
if ! pgrep -f "sing-box" > /dev/null; then
print_global " ❌ sing-box is not running"
else
print_global " 🤔 sing-box is running"
fi
fi
}
show_help() {
cat << EOF
Usage: $0 COMMAND
Available commands:
start Start podkop service
stop Stop podkop service
reload Reload podkop configuration
restart Restart podkop service
enable Enable podkop autostart
disable Disable podkop autostart
main Run main podkop process
list_update Update domain lists
check_proxy Check proxy connectivity
check_nft Check NFT rules
check_nft_rules Check NFT rules status
check_sing_box Check sing-box installation and status
check_github Check GitHub connectivity
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_dnsmasq Check DNSMasq configuration
clash_api Clash API interface for managing proxies and groups
show_config Display current podkop configuration
show_version Show podkop version
show_sing_box_config Show sing-box configuration
show_sing_box_version Show sing-box version
show_system_info Show system information
get_status Get podkop service status
get_sing_box_status Get sing-box service status
check_dns_available Check DNS server availability
global_check Run global system check
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_nft_rules)
check_nft_rules
;;
check_sing_box)
check_sing_box
;;
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
;;
clash_api)
clash_api "$2" "$3" "$4"
;;
show_config)
show_config
;;
show_version)
show_version
;;
show_sing_box_config)
show_sing_box_config
;;
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 "${2:-}"
;;
*)
show_help
exit 1
;;
esac