Files
podkop/podkop/files/usr/bin/podkop

2676 lines
92 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"
check_required_file "$PODKOP_LIB/rulesets.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"
. "$PODKOP_LIB/rulesets.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. Aborted." "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. Aborted." "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. Aborted." "error"
exit 1
fi
fi
if [ -z "$jq_version" ]; then
log "Package 'jq' is not installed. Aborted." "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). Aborted." "error"
exit 1
fi
if [ -z "$coreutils_base64_version" ]; then
log "Package 'coreutils-base64' is not installed. Aborted." "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" "error"
fi
if has_outbound_section; then
log "Outbound section found" "debug"
else
log "Outbound section not found. Please check your configuration file (missing proxy_string, interface, outbound_json, or urltest_proxy_links). Aborted." "error"
exit 1
fi
}
_check_outbound_section() {
local section="$1"
local proxy_string interface outbound_json urltest_proxy_links
config_get proxy_string "$section" "proxy_string"
config_get interface "$section" "interface"
config_get outbound_json "$section" "outbound_json"
config_get urltest_proxy_links "$section" "urltest_proxy_links"
if [ -n "$proxy_string" ] || [ -n "$interface" ] ||
[ -n "$outbound_json" ] || [ -n "$urltest_proxy_links" ]; then
section_exists=0
fi
}
has_outbound_section() {
local section_exists=1
config_foreach _check_outbound_section "section"
return $section_exists
}
start() {
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_rules
sing_box_configure_service
# sing-box
sing_box_init_config
config_foreach add_cron_job "section"
/etc/init.d/sing-box start
if [ $? -ne 0 ]; then
echo "Failed to start sing-box service"
exit 1
fi
config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_configure
fi
uci_set "podkop" "settings" "shutdown_correctly" 0
uci commit "podkop" && config_load "$PODKOP_CONFIG"
log "Nice"
list_update &
echo $! > /var/run/podkop_list_update.pid
}
stop() {
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
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
log "Stop sing-box"
/etc/init.d/sing-box stop
uci_set "podkop" "settings" "shutdown_correctly" 1
uci commit "podkop"
}
reload() {
log "Podkop reload"
stop
start
}
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" "debug"
ip route add local 0.0.0.0/0 dev lo table $table
else
log "Route for tproxy exists" "debug"
fi
if ! ip rule list | grep -q "from all fwmark 0x105 lookup $table"; then
log "Create marking rule" "debug"
ip -4 rule add fwmark 0x105 table $table priority 105
else
log "Marking rule exist" "debug"
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_rules() {
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
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
}
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_configure() {
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 nslookup_timeout=3
local nslookup_attempts=10
local curl_timeout=5
local curl_attempts=10
local curl_max_timeout=10
local delay=3
local i
# DNS Check
for i in $(seq 1 $nslookup_timeout); do
if nslookup -timeout=$nslookup_timeout openwrt.org > /dev/null 2>&1; then
echolog "✅ DNS check passed"
break
fi
echolog "DNS is unavailable [$i/$nslookup_attempts]"
sleep $delay
done
if [ "$i" -eq $nslookup_attempts ]; then
echolog "❌ DNS check failed after $nslookup_attempts attempts"
return 1
fi
# Github Check
for i in $(seq 1 $curl_attempts); do
local service_proxy_address
service_proxy_address="$(get_service_proxy_address)"
if [ -n "$http_proxy_address" ]; then
if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed (via proxy)"
break
fi
else
if curl -s -m $curl_timeout https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed"
break
fi
fi
echolog "GitHub is unavailable [$i/$curl_attempts] (max-timeout=$curl_timeout)"
if [ "$curl_timeout" -lt $curl_max_timeout ]; then
curl_timeout=$((curl_timeout + 1))
fi
sleep $delay
done
if [ "$i" -eq $curl_attempts ]; then
echolog "❌ GitHub connection check failed after $curl_attempts 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_configure_service() {
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_udp_over_tcp"
if [ -z "$proxy_string" ]; then
log "Proxy string is not set. Aborted." "fatal"
exit 1
fi
config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$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 urltest_check_interval urltest_tolerance urltest_testing_url
config_get urltest_proxy_links "$section" "urltest_proxy_links"
config_get udp_over_tcp "$section" "enable_udp_over_tcp"
config_get urltest_check_interval "$section" "urltest_check_interval" "3m"
config_get urltest_tolerance "$section" "urltest_tolerance" 50
config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204"
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" \
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")"
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"
local output_network_interface
config_get output_network_interface "settings" "output_network_interface"
if [ -z "$output_network_interface" ]; then
config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG")
else
config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" false "$SB_DNS_SERVER_TAG" \
"$output_network_interface")
fi
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
local first_outbound_section
first_outbound_section="$(get_first_outbound_section)"
first_outbound_tag="$(get_outbound_tag_by_section "$first_outbound_section")"
config=$(sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$first_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 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 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 local_domain_lists "$section" "local_domain_lists"
config_get local_subnet_lists "$section" "local_subnet_lists"
config_get remote_domain_lists "$section" "remote_domain_lists"
config_get 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"
configure_user_domain_list "$section" "$route_rule_tag"
fi
if [ "$user_subnet_list_type" != "disabled" ]; then
log "Processing user subnets routing rules for '$section' section"
configure_user_subnet_list "$section" "$route_rule_tag"
fi
if [ -n "$local_domain_lists" ]; then
log "Processing local domains routing rules for '$section' section"
configure_local_domain_lists "$section" "$route_rule_tag"
fi
if [ -n "$local_subnet_lists" ]; then
log "Processing local subnets routing rules for '$section' section"
configure_local_subnet_lists "$section" "$route_rule_tag"
fi
if [ -n "$remote_domain_lists" ]; then
log "Processing remote domains routing rules for '$section' section"
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"
config_list_foreach "$section" "remote_subnet_lists" configure_remote_domain_or_subnet_list_handler \
"subnets" "$section" "$route_rule_tag"
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")
}
prepare_source_ruleset() {
local section="$1"
local name="$2"
local type="$3"
local route_rule_tag="$4"
log "Preparing a $name $type rule set for '$section' section" "debug"
ruleset_tag=$(get_ruleset_tag "$section" "$name" "$type")
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
create_source_rule_set "$ruleset_filepath"
case $? in
0)
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" "error"
return 1
;;
esac
;;
3) log "Source rule set $ruleset_filepath already exists, skipping." "debug" ;;
esac
}
configure_user_domain_list() {
local section="$1"
local route_rule_tag="$2"
prepare_source_ruleset "$section" "user" "domains" "$route_rule_tag"
local user_domain_list_type items json_array
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
items="$(parse_domain_or_subnet_string_to_commas_string "$items" "domains")"
json_array="$(comma_string_to_json_array "$items")"
patch_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array"
}
configure_user_subnet_list() {
local section="$1"
local route_rule_tag="$2"
prepare_source_ruleset "$section" "user" "subnets" "$route_rule_tag"
local user_subnet_list_type items json_array
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
items="$(parse_domain_or_subnet_string_to_commas_string "$items" "subnets")"
json_array="$(comma_string_to_json_array "$items")"
patch_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
}
configure_local_domain_lists() {
local section="$1"
local route_rule_tag="$2"
prepare_source_ruleset "$section" "local" "domains" "$route_rule_tag"
config_list_foreach "$section" "local_domain_lists" import_local_domain_list_handler "$ruleset_filepath"
}
import_local_domain_list_handler() {
local local_domain_list_filepath="$1"
local ruleset_filepath="$2"
if ! file_exists "$local_domain_list_filepath"; then
log "Local domain list file $local_domain_list_filepath not found" "error"
return 1
fi
import_plain_domain_list_to_local_source_ruleset_chunked "$local_domain_list_filepath" "$ruleset_filepath"
}
configure_local_subnet_lists() {
local section="$1"
local route_rule_tag="$2"
prepare_source_ruleset "$section" "local" "subnets" "$route_rule_tag"
config_list_foreach "$section" "local_subnet_lists" import_local_subnets_list_handler "$ruleset_filepath"
}
import_local_subnets_list_handler() {
local local_subnet_list_filepath="$1"
local ruleset_filepath="$2"
if ! file_exists "$local_subnet_list_filepath"; then
log "Local subnet list file $local_subnet_list_filepath not found" "error"
return 1
fi
import_plain_subnet_list_to_local_source_ruleset_chunked "$local_subnet_list_filepath" "$ruleset_filepath"
nft_add_set_elements_from_file_chunked "$local_subnet_list_filepath" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
}
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")
log "Detected file extension: '$file_extension'" "debug"
case "$file_extension" in
json | srs)
log "Creating a remote $type ruleset from the source URL" "info"
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" "error" ;;
esac
;;
*)
prepare_source_ruleset "$section" "remote" "$type" "$route_rule_tag"
;;
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)
log "Configuring Clash API"
local enable_yacd enable_yacd_wan_access clash_api_controller_address
config_get_bool enable_yacd "settings" "enable_yacd" 0
config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0
if [ "$enable_yacd" -eq 1 ] && [ "$enable_yacd_wan_access" -eq 1 ]; then
clash_api_controller_address="0.0.0.0"
else
clash_api_controller_address="$(get_service_listen_address)"
if [ -z "$clash_api_controller_address" ]; then
log "Could not determine the listening IP address for the Clash API controller. It will run only on localhost." "warn"
clash_api_controller_address="127.0.0.1"
fi
fi
if [ "$enable_yacd" -eq 1 ]; then
log "YACD is enabled, enabling Clash API with downloadable YACD" "debug"
local yacd_secret_key external_controller_ui
config_get yacd_secret_key "settings" "yacd_secret_key"
external_controller_ui="ui"
config=$(
sing_box_cm_configure_clash_api \
"$config" \
"$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT" \
"$external_controller_ui" \
"$yacd_secret_key"
)
else
log "YACD is disabled, enabling Clash API in online mode" "debug"
config=$(
sing_box_cm_configure_clash_api "$config" "$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT"
)
fi
}
sing_box_additional_inbounds() {
log "Configure the additional inbounds of a sing-box JSON configuration"
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
local download_lists_via_proxy_section section_outbound_tag
config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")"
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" \
"$section_outbound_tag"
)
fi
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 mixed_proxy_address
config_get_bool mixed_inbound_enabled "$section" "mixed_proxy_enabled" 0
mixed_proxy_address="$(get_service_listen_address)"
if [ -z "$mixed_proxy_address" ]; then
log "Could not determine the listening IP address for the Mixed Proxy. The proxy will not be created." "warn"
return 1
fi
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" \
"$mixed_proxy_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. Aborted." "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
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
if [ "$service" = "discord" ]; then
nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME"
else
nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
fi
rm -f "$tmpfile"
}
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")
log "Detected file extension: '$file_extension'" "debug"
case "$file_extension" in
json | srs)
log "No update needed - sing-box manages updates automatically."
;;
*)
log "Import domains from a remote plain-text list"
import_domains_from_remote_plain_file "$url" "$section"
;;
esac
}
import_domains_from_remote_plain_file() {
local url="$1"
local section="$2"
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
convert_crlf_to_lf "$tmpfile"
ruleset_tag=$(get_ruleset_tag "$section" "remote" "domains")
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
import_plain_domain_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath"
rm -f "$tmpfile"
}
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")"
log "Detected file extension: '$file_extension'" "debug"
case "$file_extension" in
json)
log "Import subnets from a remote JSON list" "info"
import_subnets_from_remote_json_file "$url"
;;
srs)
log "Import subnets from a remote SRS list" "info"
import_subnets_from_remote_srs_file "$url"
;;
*)
log "Import subnets from a remote plain-text list" "info"
import_subnets_from_remote_plain_file "$url" "$section"
;;
esac
}
import_subnets_from_remote_json_file() {
local url="$1"
local json_tmpfile subnets_tmpfile http_proxy_address
json_tmpfile="$(mktemp)"
subnets_tmpfile="$(mktemp)"
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$url" "$json_tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$json_tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile"
nft_add_set_elements_from_file_chunked "$subnets_tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
rm -f "$json_tmpfile" "$subnets_tmpfile"
}
import_subnets_from_remote_srs_file() {
local url="$1"
local binary_tmpfile json_tmpfile subnets_tmpfile 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_binary_ruleset "$binary_tmpfile" "$json_tmpfile"; then
log "Failed to decompile binary rule set file" "error"
return 1
fi
extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile"
nft_add_set_elements_from_file_chunked "$subnets_tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile"
}
import_subnets_from_remote_plain_file() {
local url="$1"
local section="$2"
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
convert_crlf_to_lf "$tmpfile"
ruleset_tag=$(get_ruleset_tag "$section" "remote" "subnets")
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
import_plain_subnet_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath"
nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
rm -f "$tmpfile"
}
## 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
local download_lists_via_proxy_section section_outbound_tag
config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section"
section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")"
echo "$section_outbound_tag"
else
echo ""
fi
}
_determine_first_outbound_section() {
local section="$1"
local connection_type
config_get connection_type "$section" "connection_type"
if [ "$connection_type" = "proxy" ] || [ "$connection_type" = "vpn" ]; then
[ -z "$first_section" ] && first_section="$1"
fi
}
get_first_outbound_section() {
local first_section=""
config_foreach _determine_first_outbound_section "section"
echo "$first_section"
}
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 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 local_domain_lists "$section" "local_domain_lists"
config_get local_subnet_lists "$section" "local_subnet_lists"
config_get remote_domain_lists "$section" "remote_domain_lists"
config_get 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
}
get_service_listen_address() {
local service_listen_address
service_listen_address="$(uci_get "network" "lan" "ipaddr")"
if [ -z "$service_listen_address" ]; then
config_get service_listen_address "settings" "service_listen_address" # TODO(ampetelin): Remove after testing
fi
if [ -z "$service_listen_address" ]; then
log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/itdoginfo/podkop/issues" "error"
return 1
fi
echo "$service_listen_address"
}
## 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_logs() {
if ! command -v logread > /dev/null 2>&1; then
nolog "Error: logread command not found"
return 1
fi
local logs
logs=$(logread | grep -E "podkop|sing-box")
if [ -z "$logs" ]; then
nolog "Logs not found"
return 1
fi
# Find the last occurrence of "Starting podkop"
local start_line
start_line=$(echo "$logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1)
if [ -n "$start_line" ]; then
echo "$logs" | tail -n +"$start_line"
else
nolog "No 'Starting podkop' message found, showing last 100 lines"
echo "$logs" | tail -n 100
fi
}
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 '/option outbound_json/,/^}/c\ option outbound_json '\''MASKED'\''' \
-e 's/\(list urltest_proxy_links\).*/\1 '\''MASKED'\''/g' \
-e "s@\\(option dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \
-e "s@\\(option domain_resolver_dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \
-e 's/\(option yacd_secret_key\).*/\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_system_info() {
local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model
podkop_version="$PODKOP_VERSION"
podkop_latest_version=$(curl -m 3 -s https://api.github.com/repos/itdoginfo/podkop/releases/latest | grep '"tag_name":' | cut -d'"' -f4)
[ -z "$podkop_latest_version" ] && podkop_latest_version="unknown"
if [ -f /www/luci-static/resources/view/podkop/main.js ]; then
luci_app_version=$(grep 'var PODKOP_LUCI_APP_VERSION' /www/luci-static/resources/view/podkop/main.js | cut -d'"' -f2)
else
luci_app_version="not installed"
fi
if command -v sing-box > /dev/null 2>&1; then
sing_box_version=$(sing-box version 2> /dev/null | head -n 1 | awk '{print $3}')
[ -z "$sing_box_version" ] && sing_box_version="unknown"
else
sing_box_version="not installed"
fi
if [ -f /etc/os-release ]; then
openwrt_version=$(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)
[ -z "$openwrt_version" ] && openwrt_version="unknown"
else
openwrt_version="unknown"
fi
if [ -f /tmp/sysinfo/model ]; then
device_model=$(cat /tmp/sysinfo/model)
[ -z "$device_model" ] && device_model="unknown"
else
device_model="unknown"
fi
echo "{\"podkop_version\": \"$podkop_version\", \"podkop_latest_version\": \"$podkop_latest_version\", \"luci_app_version\": \"$luci_app_version\", \"sing_box_version\": \"$sing_box_version\", \"openwrt_version\": \"$openwrt_version\", \"device_model\": \"$device_model\"}" | jq .
}
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 dns_on_router=0
local bootstrap_dns_status=0
local dhcp_config_status=1
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
dns_on_router=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,\"dns_on_router\":$dns_on_router,\"bootstrap_dns_server\":\"$bootstrap_dns_server\",\"bootstrap_dns_status\":$bootstrap_dns_status,\"dhcp_config_status\":$dhcp_config_status}" | jq .
}
check_dhcp_has_podkop_dns() {
local server_list cachesize noresolv server_found
config_get server_list "$1" "server"
config_get cachesize "$1" "cachesize"
config_get noresolv "$1" "noresolv"
server_found=0
if [ -n "$server_list" ]; then
for server in $server_list; do
if [ "$server" = "127.0.0.42" ]; then
server_found=1
break
fi
done
fi
if [ "$cachesize" != "0" ] || [ "$noresolv" != "1" ] || [ "$server_found" != "1" ]; then
dhcp_config_status=0
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 "https://$CHECK_PROXY_IP_DOMAIN/check" > /dev/null 2>&1 &
local pid1=$!
curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/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 .
}
check_fakeip() {
curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/check" | 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 action="$1"
local clash_api_controller_address CLASH_URL TEST_URL
clash_api_controller_address="$(get_service_listen_address)"
if [ -z "$clash_api_controller_address" ]; then
clash_api_controller_address="127.0.0.1"
fi
CLASH_URL="$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT"
TEST_URL="https://www.gstatic.com/generate_204"
local enable_yacd_wan_access yacd_secret_key auth_header
config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0
config_get yacd_secret_key "settings" "yacd_secret_key"
if [ "$enable_yacd_wan_access" -eq 1 ]; then
auth_header="Authorization: Bearer $yacd_secret_key"
else
auth_header=""
fi
case "$action" in
get_proxies)
curl -s --header "$auth_header" "$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" \
--header "$auth_header" \
--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" \
--header "$auth_header" \
--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" \
--header "$auth_header" \
--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"
local system_info_json
system_info_json=$(get_system_info)
if [ -n "$system_info_json" ]; then
local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model
podkop_version=$(echo "$system_info_json" | jq -r '.podkop_version // "unknown"')
podkop_latest_version=$(echo "$system_info_json" | jq -r '.podkop_latest_version // "unknown"')
luci_app_version=$(echo "$system_info_json" | jq -r '.luci_app_version // "unknown"')
sing_box_version=$(echo "$system_info_json" | jq -r '.sing_box_version // "unknown"')
openwrt_version=$(echo "$system_info_json" | jq -r '.openwrt_version // "unknown"')
device_model=$(echo "$system_info_json" | jq -r '.device_model // "unknown"')
print_global "🕳️ Podkop: $podkop_version (latest: $podkop_latest_version)"
print_global "🕳️ LuCI App: $luci_app_version"
print_global "📦 Sing-box: $sing_box_version"
print_global "🛜 OpenWrt: $openwrt_version"
print_global "🛜 Device: $device_model"
else
print_global "❌ Failed to get system info"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "➡️ DNS status"
local dns_check_json
dns_check_json=$(check_dns_available)
if [ -n "$dns_check_json" ]; then
local dns_type dns_server dns_status dns_on_router bootstrap_dns_server bootstrap_dns_status dhcp_config_status
dns_type=$(echo "$dns_check_json" | jq -r '.dns_type // "unknown"')
dns_server=$(echo "$dns_check_json" | jq -r '.dns_server // "unknown"')
dns_status=$(echo "$dns_check_json" | jq -r '.dns_status // 0')
dns_on_router=$(echo "$dns_check_json" | jq -r '.dns_on_router // 0')
bootstrap_dns_server=$(echo "$dns_check_json" | jq -r '.bootstrap_dns_server // ""')
bootstrap_dns_status=$(echo "$dns_check_json" | jq -r '.bootstrap_dns_status // 0')
dhcp_config_status=$(echo "$dns_check_json" | jq -r '.dhcp_config_status // 0')
# Bootstrap DNS
if [ -n "$bootstrap_dns_server" ]; then
if [ "$bootstrap_dns_status" -eq 1 ]; then
print_global "✅ Bootstrap DNS: $bootstrap_dns_server"
else
print_global "❌ Bootstrap DNS: $bootstrap_dns_server"
fi
fi
# DNS server status
if [ "$dns_status" -eq 1 ]; then
print_global "✅ Main DNS: $dns_server [$dns_type]"
else
print_global "❌ Main DNS: $dns_server [$dns_type]"
fi
# DNS on router
if [ "$dns_on_router" -eq 1 ]; then
print_global "✅ DNS on router"
else
print_global "❌ DNS on router"
fi
# DHCP configuration check
local dont_touch_dhcp
config_get dont_touch_dhcp "settings" "dont_touch_dhcp"
if [ "$dont_touch_dhcp" = "1" ]; then
print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:"
awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
elif [ "$dhcp_config_status" -eq 0 ]; then
print_global "❌ DHCP configuration differs from template. 📄 DHCP config:"
awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp
else
print_global "✅ /etc/config/dhcp"
fi
else
print_global "❌ Failed to get DNS info"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "📦 Sing-box status"
local singbox_check_json
singbox_check_json=$(check_sing_box)
if [ -n "$singbox_check_json" ]; then
local sing_box_installed sing_box_version_ok sing_box_service_exist sing_box_autostart_disabled sing_box_process_running sing_box_ports_listening
sing_box_installed=$(echo "$singbox_check_json" | jq -r '.sing_box_installed // 0')
sing_box_version_ok=$(echo "$singbox_check_json" | jq -r '.sing_box_version_ok // 0')
sing_box_service_exist=$(echo "$singbox_check_json" | jq -r '.sing_box_service_exist // 0')
sing_box_autostart_disabled=$(echo "$singbox_check_json" | jq -r '.sing_box_autostart_disabled // 0')
sing_box_process_running=$(echo "$singbox_check_json" | jq -r '.sing_box_process_running // 0')
sing_box_ports_listening=$(echo "$singbox_check_json" | jq -r '.sing_box_ports_listening // 0')
if [ "$sing_box_installed" -eq 1 ]; then
print_global "✅ Sing-box installed"
else
print_global "❌ Sing-box installed"
fi
if [ "$sing_box_version_ok" -eq 1 ]; then
print_global "✅ Sing-box version is compatible (newer than 1.12.4)"
else
print_global "❌ Sing-box version is not compatible (older than 1.12.4)"
fi
if [ "$sing_box_service_exist" -eq 1 ]; then
print_global "✅ Sing-box service exist"
else
print_global "❌ Sing-box service exist"
fi
if [ "$sing_box_autostart_disabled" -eq 1 ]; then
print_global "✅ Sing-box autostart disabled"
else
print_global "❌ Sing-box autostart disabled"
fi
if [ "$sing_box_process_running" -eq 1 ]; then
print_global "✅ Sing-box process running"
else
print_global "❌ Sing-box process running"
fi
if [ "$sing_box_ports_listening" -eq 1 ]; then
print_global "✅ Sing-box listening ports"
else
print_global "❌ Sing-box listening ports"
fi
else
print_global "❌ Failed to get sing-box info"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🧱 NFT rules status"
local nft_check_json
nft_check_json=$(check_nft_rules)
if [ -n "$nft_check_json" ]; then
local table_exist rules_mangle_exist rules_mangle_counters rules_mangle_output_exist rules_mangle_output_counters rules_proxy_exist rules_proxy_counters rules_other_mark_exist
table_exist=$(echo "$nft_check_json" | jq -r '.table_exist // 0')
rules_mangle_exist=$(echo "$nft_check_json" | jq -r '.rules_mangle_exist // 0')
rules_mangle_counters=$(echo "$nft_check_json" | jq -r '.rules_mangle_counters // 0')
rules_mangle_output_exist=$(echo "$nft_check_json" | jq -r '.rules_mangle_output_exist // 0')
rules_mangle_output_counters=$(echo "$nft_check_json" | jq -r '.rules_mangle_output_counters // 0')
rules_proxy_exist=$(echo "$nft_check_json" | jq -r '.rules_proxy_exist // 0')
rules_proxy_counters=$(echo "$nft_check_json" | jq -r '.rules_proxy_counters // 0')
rules_other_mark_exist=$(echo "$nft_check_json" | jq -r '.rules_other_mark_exist // 0')
if [ "$table_exist" -eq 1 ]; then
print_global "✅ Table exist"
else
print_global "❌ Table exist"
fi
if [ "$rules_mangle_exist" -eq 1 ]; then
print_global "✅ Rules mangle exist"
else
print_global "❌ Rules mangle exist"
fi
if [ "$rules_mangle_counters" -eq 1 ]; then
print_global "✅ Rules mangle counters"
else
print_global "⚠️ Rules mangle counters"
fi
if [ "$rules_mangle_output_exist" -eq 1 ]; then
print_global "✅ Rules mangle output exist"
else
print_global "❌ Rules mangle output exist"
fi
if [ "$rules_mangle_output_counters" -eq 1 ]; then
print_global "✅ Rules mangle output counters"
else
print_global "⚠️ Rules mangle output counters"
fi
if [ "$rules_proxy_exist" -eq 1 ]; then
print_global "✅ Rules proxy exist"
else
print_global "❌ Rules proxy exist"
fi
if [ "$rules_proxy_counters" -eq 1 ]; then
print_global "✅ Rules proxy counters"
else
print_global "⚠️ Rules proxy counters"
fi
if [ "$rules_other_mark_exist" -eq 1 ]; then
print_global "⚠️ Additional marking rules found:"
nft list ruleset | awk '/table inet '"$NFT_TABLE_NAME"'/{flag=1; next} /^table/{flag=0} !flag' | grep -E "mark set|meta mark"
else
print_global "✅ Additional marking rules found"
fi
else
print_global "❌ Failed to get NFT rules info"
fi
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
# 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 "wireguard_.*\.route_allowed_ips='1'" | cut -d'.' -f1-2 | while read -r peer_section; do
local allowed_ips
allowed_ips=$(uci get "${peer_section}.allowed_ips" 2> /dev/null)
if [ "$allowed_ips" = "0.0.0.0/0" ]; then
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "⚠️ WG Route allowed IP enabled with 0.0.0.0/0"
fi
done
fi
if [ -f "/etc/init.d/zapret" ]; then
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "⚠️ Zapret detected"
fi
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🥸 FakeIP status"
local fakeip_check_json
fakeip_check_json=$(check_fakeip)
if [ -n "$fakeip_check_json" ]; then
local fakeip_status
fakeip_status=$(echo "$fakeip_check_json" | jq -r '.fakeip // false')
if [ "$fakeip_status" = "true" ]; then
print_global "✅ Router DNS is routed through sing-box"
else
print_global "⚠️ Router DNS is NOT routed through sing-box"
fi
else
print_global "❌ Failed to get FakeIP info"
fi
local fakeip_address
fakeip_address=$(dig +short @127.0.0.42 $FAKEIP_TEST_DOMAIN)
if echo "$fakeip_address" | grep -q "^198\.18\."; then
print_global "✅ Sing-box works with FakeIP: $fakeip_address"
else
print_global "❌ Sing-box does NOT work with FakeIP: $fakeip_address"
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
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_logs Show podkop logs from system journal
check_sing_box_logs Show sing-box logs
check_fakeip Test FakeIP on router
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
get_system_info Get system information in JSON format
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_logs)
check_logs
;;
check_sing_box_logs)
check_sing_box_logs
;;
check_fakeip)
check_fakeip
;;
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
;;
get_system_info)
get_system_info
;;
check_dns_available)
check_dns_available
;;
global_check)
global_check "${2:-}"
;;
*)
show_help
exit 1
;;
esac