Files
podkop/podkop/files/usr/bin/podkop
2025-09-04 20:02:43 +05:00

2172 lines
72 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
[ -r /lib/functions.sh ] && . /lib/functions.sh
[ -r /lib/config/uci.sh ] && . /lib/config/uci.sh
PODKOP_LIB="/usr/lib/podkop"
. "$PODKOP_LIB/constants.sh"
. "$PODKOP_LIB/helpers.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
. "$PODKOP_LIB/sing_box_config_facade.sh"
config_load "/etc/config/podkop"
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download"
DOMAINS_RU_INSIDE="${GITHUB_RAW_URL}/Russia/inside-dnsmasq-nfset.lst"
DOMAINS_RU_OUTSIDE="${GITHUB_RAW_URL}/Russia/outside-dnsmasq-nfset.lst"
DOMAINS_UA="${GITHUB_RAW_URL}/Ukraine/inside-dnsmasq-nfset.lst"
DOMAINS_YOUTUBE="${GITHUB_RAW_URL}/Services/youtube.lst"
SUBNETS_TWITTER="${GITHUB_RAW_URL}/Subnets/IPv4/twitter.lst"
SUBNETS_META="${GITHUB_RAW_URL}/Subnets/IPv4/meta.lst"
SUBNETS_DISCORD="${GITHUB_RAW_URL}/Subnets/IPv4/discord.lst"
SUBNETS_TELERAM="${GITHUB_RAW_URL}/Subnets/IPv4/telegram.lst"
SUBNETS_CLOUDFLARE="${GITHUB_RAW_URL}/Subnets/IPv4/cloudflare.lst"
SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst"
SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst"
SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst"
SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst"
VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube discord meta twitter hdrezka tiktok telegram cloudflare google_ai google_play hetzner ovh hodca digitalocean cloudfront"
DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8"
CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi"
TEST_DOMAIN="fakeip.podkop.fyi"
INTERFACES_LIST=""
SRC_INTERFACE=""
RESOLV_CONF="/etc/resolv.conf"
# Endpoints https://github.com/ampetelin/warp-endpoint-checker
CLOUDFLARE_OCTETS="8.47 162.159 188.114"
# Color constants
COLOR_CYAN="\033[0;36m"
COLOR_GREEN="\033[0;32m"
COLOR_RESET="\033[0m"
log() {
local message="$1"
local level="$2"
if [ "$level" == "" ]; then
level="info"
fi
logger -t "podkop" "[$level] $message"
}
nolog() {
local message="$1"
local timestamp
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
}
echolog() {
local message="$1"
local module="$2"
log "$message" "$module"
nolog "$message"
}
build_sing_box_config() {
cat > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SB_CONFIG"
}
start_main() {
log "Starting podkop"
# checking
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
required_version="1.11.1"
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
log "The version of sing-box ($sing_box_version) is lower than the minimum version. Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box" "critical"
exit 1
fi
if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
log "Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp" "warn"
fi
migration
config_foreach process_validate_service
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/podkop
# base
route_table_rule_mark
create_nft_table
sing_box_uci
# sing-box
sing_box_init_config
sing_box_config_check
# TODO(ampetelin): refactoring is needed
# config_foreach add_cron_job # need refactoring
/etc/init.d/sing-box start
# sing_box_inbound_proxy 1602 #refactored
# sing_box_dns #refactored
# sing_box_dns_rule_fakeip #refactored
# sing_box_rule_dns #refactored
# sing_box_create_bypass_ruleset #refactored
# sing_box_add_secure_dns_probe_domain #refactored
# sing_box_cache_file #refactored
# process_socks5 #refactored
#
# # sing-box outbounds and rules
# config_foreach sing_box_outdound #refactored
# config_foreach process_domains_for_section #refactored, implementation is needed
# config_foreach sing_box_rule_preset #refactored
# config_foreach process_domains_list_local #refactored, implementation is needed
# config_foreach process_subnet_for_section #refactored, implementation is needed
# config_foreach configure_community_lists #refactored
# config_foreach configure_remote_domain_lists #refactored
# config_foreach configure_remote_subnet_lists #refactored
# config_foreach process_all_traffic_for_section #refactored
local exclude_ntp
config_get_bool exclude_ntp "main" "exclude_ntp" "0"
if [ "$exclude_ntp" -eq 1 ]; then
log "NTP traffic exclude for proxy"
nft insert rule inet PodkopTable mangle udp dport 123 return
fi
# TODO(ampetelin): refactoring is needed
# list_update &
# echo $! > /var/run/podkop_list_update.pid
log "Nice"
}
start() {
start_main
local proxy_string interface outbound_json dont_touch_dhcp
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
if [ -n "$proxy_string" ] || [ -n "$interface" ] || [ -n "$outbound_json" ]; then
config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0"
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_add_resolver
fi
fi
}
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 -rf /tmp/podkop/*.lst
log "Flush nft"
if nft list table inet PodkopTable >/dev/null 2>&1; then
nft delete table inet PodkopTable
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
#/etc/init.d/sing-box disable
}
stop() {
local dont_touch_dhcp
config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0"
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_restore
fi
stop_main
}
reload() {
log "Podkop reload"
stop_main
start_main
}
restart() {
log "Podkop restart"
stop
start
}
# Migrations and validation funcs
migration() {
# list migrate
local CONFIG="/etc/config/podkop"
if grep -q "ru_inside" $CONFIG; then
log "Depricated list found: ru_inside"
sed -i '/ru_inside/d' $CONFIG
fi
if grep -q "list domain_list 'ru_outside'" $CONFIG; then
log "Depricated list found: sru_outside"
sed -i '/ru_outside/d' $CONFIG
fi
if grep -q "list domain_list 'ua'" $CONFIG; then
log "Depricated list found: ua"
sed -i '/ua/d' $CONFIG
fi
# Subnet list
if grep -q "list subnets" $CONFIG; then
log "Depricated second section found"
sed -i '/list subnets/d' $CONFIG
fi
# second remove
if grep -q "config second 'second'" $CONFIG; then
log "Depricated second section found"
sed -i '/second/d' $CONFIG
fi
# cron update
if grep -qE "^\s*option update_interval '[0-9*/,-]+( [0-9*/,-]+){4}'" $CONFIG; then
log "Depricated update_interval"
sed -i "s|^\(\s*option update_interval\) '[0-9*/,-]\+\( [0-9*/,-]\+\)\{4\}'|\1 '1d'|" $CONFIG
fi
# dnsmasq https
if grep -q "^filter-rr=HTTPS" "/etc/dnsmasq.conf"; then
log "Found and removed filter-rr=HTTPS in dnsmasq config"
sed -i '/^filter-rr=HTTPS/d' "/etc/dnsmasq.conf"
fi
# dhcp use-application-dns.net
if grep -q "use-application-dns.net" "/etc/config/dhcp"; then
log "Found and removed use-application-dns.net in dhcp config"
sed -i '/use-application-dns/d' "/etc/config/dhcp"
fi
# corntab init.d
(crontab -l | grep -v "/etc/init.d/podkop list_update") | crontab -
migrate_config_key "$CONFIG" "option" "domain_list_enabled" "community_list_enabled"
migrate_config_key "$CONFIG" "list" "domain_list" "community_list"
migrate_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domains_list_type"
migrate_config_key "$CONFIG" "option" "custom_domains_text" "user_domains_text"
migrate_config_key "$CONFIG" "list" "custom_domains" "user_domains"
migrate_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnets_list_type"
migrate_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets_text"
migrate_config_key "$CONFIG" "list" "custom_subnets" "user_subnets"
migrate_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domains_list_enabled"
migrate_config_key "$CONFIG" "list" "custom_local_domains" "local_domains_list"
migrate_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domains_list_enabled"
migrate_config_key "$CONFIG" "list" "custom_download_domains" "remote_domains_list"
migrate_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnets_list_enabled"
migrate_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnets_list"
}
validate_service() {
local domain="$1"
for valid_service in $VALID_SERVICES; do
if [ "$domain" = "$valid_service" ]; then
return 0
fi
done
log "Invalid service in domain_list: $domain. Exiting. Check config and LuCI cache"
exit 1
}
process_validate_service() {
local domain_list_enabled
config_get_bool domain_list_enabled "$section" "domain_list_enabled" 0
if [ "$domain_list_enabled" -eq 1 ]; then
config_list_foreach "$section" domain_list validate_service
fi
}
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
}
process_interfaces() {
local iface="$1"
INTERFACES_LIST="$INTERFACES_LIST $iface"
iface_flag=1
}
nft_interfaces() {
local table=PodkopTable
iface_flag=0
config_list_foreach "main" "iface" "process_interfaces"
if [ "$iface_flag" -eq 0 ]; then
SRC_INTERFACE="br-lan"
elif [ $(echo "$INTERFACES_LIST" | wc -w) -eq 1 ]; then
SRC_INTERFACE=$INTERFACES_LIST
else
local set_name="interfaces"
if ! nft list set inet $table $set_name &>/dev/null; then
nft add set inet $table $set_name { type ifname\; flags interval\; }
fi
for interface in $INTERFACES_LIST; do
if ! nft list element inet $table $set_name { $interface } &>/dev/null; then
nft add element inet $table $set_name { $interface }
fi
done
SRC_INTERFACE=@$set_name
fi
}
create_nft_table() {
local table="PodkopTable"
nft add table inet $table
nft_interfaces
log "Create localv4 set"
nft add set inet $table localv4 { type ipv4_addr\; flags interval\; }
nft add element inet $table localv4 { \
0.0.0.0/8, \
10.0.0.0/8, \
127.0.0.0/8, \
169.254.0.0/16, \
172.16.0.0/12, \
192.0.0.0/24, \
192.0.2.0/24, \
192.88.99.0/24, \
192.168.0.0/16, \
198.51.100.0/24, \
203.0.113.0/24, \
224.0.0.0/4, \
240.0.0.0-255.255.255.255 }
log "Create nft rules"
nft add chain inet $table mangle { type filter hook prerouting priority -150 \; policy accept \;}
nft add chain inet $table mangle_output { type route hook output priority -150 \; policy accept\; }
nft add chain inet $table proxy { type filter hook prerouting priority -100 \; policy accept \;}
nft add set inet $table podkop_subnets { type ipv4_addr\; flags interval\; auto-merge\; }
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_subnets meta l4proto udp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$FAKEIP" meta l4proto tcp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$FAKEIP" meta l4proto udp meta mark set 0x105 counter
nft add rule inet $table proxy meta mark 0x105 meta l4proto tcp tproxy ip to :1602 counter
nft add rule inet $table proxy meta mark 0x105 meta l4proto udp tproxy ip to :1602 counter
nft add rule inet $table mangle_output ip daddr @localv4 return
nft add rule inet $table mangle_output ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x00000105 counter
nft add rule inet $table mangle_output ip daddr @podkop_subnets meta l4proto udp meta mark set 0x00000105 counter
nft add rule inet $table mangle_output ip daddr 198.18.0.0/15 meta l4proto tcp meta mark set 0x00000105 counter
nft add rule inet $table mangle_output ip daddr 198.18.0.0/15 meta l4proto udp meta mark set 0x00000105 counter
}
save_dnsmasq_config() {
local key="$1"
local backup_key="$2"
value=$(uci get "$key" 2>/dev/null)
if [ -z "$value" ]; then
uci set "$backup_key"="unset"
else
uci set "$backup_key"="$value"
fi
}
dnsmasq_add_resolver() {
log "Save dnsmasq config"
uci -q delete dhcp.@dnsmasq[0].podkop_server
for server in $(uci get dhcp.@dnsmasq[0].server 2>/dev/null); do
if [[ "$server" == "127.0.0.42" ]]; then
log "Dnsmasq save config error: server=127.0.0.42 is already configured. Skip editing DHCP"
return
else
uci add_list dhcp.@dnsmasq[0].podkop_server="$server"
fi
done
save_dnsmasq_config "dhcp.@dnsmasq[0].noresolv" "dhcp.@dnsmasq[0].podkop_noresolv"
save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize"
log "Configure dnsmasq for sing-box"
uci set dhcp.@dnsmasq[0].noresolv="1"
uci set dhcp.@dnsmasq[0].cachesize="0"
uci -q delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server="127.0.0.42"
uci commit dhcp
/etc/init.d/dnsmasq restart
}
dnsmasq_restore() {
log "Removing configuration for dnsmasq"
local cachesize noresolv server
cachesize=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null)
if [[ "$cachesize" == "unset" ]]; then
log "dnsmasq revert: cachesize is unset"
uci -q delete dhcp.@dnsmasq[0].cachesize
else
uci set dhcp.@dnsmasq[0].cachesize="$cachesize"
fi
noresolv=$(uci get dhcp.@dnsmasq[0].podkop_noresolv 2>/dev/null)
if [[ "$noresolv" == "unset" ]]; then
log "dnsmasq revert: noresolv is unset"
uci -q delete dhcp.@dnsmasq[0].noresolv
else
uci set dhcp.@dnsmasq[0].noresolv="$noresolv"
fi
server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)
if [[ "$server" == "127.0.0.42" ]]; then
uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null
for server in $(uci get dhcp.@dnsmasq[0].podkop_server 2>/dev/null); do
uci add_list dhcp.@dnsmasq[0].server="$server"
done
uci delete dhcp.@dnsmasq[0].podkop_server 2>/dev/null
fi
uci delete dhcp.@dnsmasq[0].podkop_cachesize
uci delete dhcp.@dnsmasq[0].podkop_noresolv
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_list_enabled remote_domains_list_enabled remote_subnets_list_enabled update_interval
config_get community_list_enabled "$section" "community_list_enabled"
config_get remote_domains_list_enabled "$section" "remote_domains_list_enabled"
config_get remote_subnets_list_enabled "$section" "remote_subnets_list_enabled"
config_get update_interval "main" "update_interval"
case "$update_interval" in
"1h")
cron_job="13 * * * * /usr/bin/podkop list_update"
;;
"3h")
cron_job="13 */3 * * * /usr/bin/podkop list_update"
;;
"12h")
cron_job="13 */12 * * * /usr/bin/podkop list_update"
;;
"1d")
cron_job="13 9 * * * /usr/bin/podkop list_update"
;;
"3d")
cron_job="13 9 */3 * * /usr/bin/podkop list_update"
;;
*)
log "Invalid update_interval value: $update_interval"
return
;;
esac
if [ "$community_list_enabled" -eq 1 ] || \
[ "$remote_domains_list_enabled" -eq 1 ] || \
[ "$remote_subnets_list_enabled" -eq 1 ]; 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"
}
# TODO(ampetelin): refactoring is needed
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 detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com >/dev/null; then
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
config_foreach import_domains_from_remote_domain_lists
config_foreach import_subnets_from_remote_subnet_lists
if [ $? -eq 0 ]; then
echolog "✅ Lists update completed successfully"
else
echolog "❌ Lists update failed"
fi
}
find_working_resolver() {
for resolver in $DNS_RESOLVERS; do
if nslookup -timeout=2 $TEST_DOMAIN $resolver >/dev/null 2>&1; then
echo "$resolver"
return 0
fi
done
return 1
}
# sing-box funcs
sing_box_uci() {
local config="/etc/config/sing-box"
if grep -q "option enabled '0'" "$config" ||
grep -q "option user 'sing-box'" "$config"; then
sed -i \
-e "s/option enabled '0'/option enabled '1'/" \
-e "s/option user 'sing-box'/option user 'root'/" $config
log "Change sing-box UCI config"
fi
[ -f /etc/rc.d/S99sing-box ] && log "Disable sing-box" && /etc/init.d/sing-box disable
# if grep -q '#\s*list ifaces' "$config"; then
# sed -i '/ifaces/s/#//g' $config
# log "Uncommented list ifaces"
# fi
}
sing_box_init_config() {
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
# TODO: remove after refactoring
nolog "$config"
sing_box_cm_save_config_to_file "$config" "$SB_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
}
configure_outbound_handler() {
local section="$1"
local connection_mode
config_get connection_mode "$section" "mode"
case "$connection_mode" in
proxy)
log "Configuring outbound in proxy connection mode for the $section section"
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"
local proxy_string udp_over_tcp
config_get proxy_string "$section" "proxy_string"
config_get udp_over_tcp "$section" "ss_uot"
# Extract the first non-comment line as the active configuration
active_proxy_string=$(echo "$proxy_string" | grep -v "^[[:space:]]*\/\/" | head -n 1)
if [ -z "$active_proxy_string" ]; then
log "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"
local json_outbound
config_get json_outbound "$section" "outbound_json"
config=$(sing_box_cf_add_json_outbound "$config" "$section" "$json_outbound")
;;
*)
log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal"
exit 1
;;
esac
;;
vpn)
log "Configuring outbound in VPN connection mode for the $section section"
local interface_name
config_get interface_name "$section" "interface"
if [ -z "$interface_name" ]; then
log "VPN interface is not set. Aborted." "fatal"
exit 1
fi
config=$(sing_box_cf_add_interface_outbound "$config" "$section" "$interface_name")
;;
block)
log "Connection mode 'block' detected for the $section section no outbound will be created (handled via reject route rules)"
;;
*)
log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "fatal"
exit 1
;;
esac
}
sing_box_configure_dns() {
log "Configure the DNS section of a sing-box JSON configuration"
local split_dns_enabled final_dns_server
config_get_bool split_dns_enabled "main" "split_dns_enabled" 0
if [ "$split_dns_enabled" -eq 1 ]; then
final_dns_server="$SB_SPLIT_DNS_SERVER_TAG"
else
final_dns_server="$SB_DNS_SERVER_TAG"
fi
config=$(sing_box_cm_configure_dns "$config" "$final_dns_server" "ipv4_only" true)
local dns_type dns_server split_dns_type split_dns_server
config_get dns_type "main" "dns_type" "doh"
config_get dns_server "main" "dns_server" "1.1.1.1"
config_get split_dns_type "main" "split_dns_type" "udp"
config_get split_dns_server "main" "split_dns_server" "1.1.1.1"
local need_dns_domain_resolver=0
if ! is_ipv4 "$dns_server" || ! is_ipv4 "$split_dns_server"; then
need_dns_domain_resolver=1
fi
log "Adding DNS Servers"
config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$FAKEIP")
local dns_domain_resolver
if [ "$need_dns_domain_resolver" -eq 1 ]; then
log "One of the DNS server addresses is a domain. Searching for a working DNS server..."
dns_domain_resolver=$(find_working_resolver)
if [ -z "$dns_domain_resolver" ]; then
log "Working DNS server not found, using default DNS server"
dns_domain_resolver="1.1.1.1"
else
log "Working DNS server has been found: $dns_domain_resolver"
fi
config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$dns_domain_resolver" 53)
fi
config=$(
sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "" "" \
"$SB_DNS_DOMAIN_RESOLVER_TAG"
)
if [ "$split_dns_enabled" -eq 1 ]; then
config=$(
sing_box_cf_add_dns_server "$config" "$split_dns_type" "$SB_SPLIT_DNS_SERVER_TAG" "$split_dns_server" \
"" "" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$SB_MAIN_OUTBOUND_TAG"
)
fi
log "Adding DNS Rules"
local rewrite_ttl service_domains
config_get rewrite_ttl "main" "dns_rewrite_ttl" "60"
config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "HTTPS")
config=$(sing_box_cm_add_dns_reject_rule "$config" "domain_suffix" '"use-application-dns.net"')
config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_DNS_RULE_TAG")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rewrite_ttl" "$rewrite_ttl")
service_domains=$(comma_string_to_json_array "$TEST_DOMAIN,$CHECK_PROXY_IP_DOMAIN")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
if [ "$split_dns_enabled" -eq 1 ]; then
config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_DNS_SERVER_TAG" "$SB_INVERT_FAKEIP_DNS_RULE_TAG")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "invert" true)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
fi
}
sing_box_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 mixed_inbound_enabled
config_get_bool mixed_inbound_enabled "main" "socks5" 0
if [ "$mixed_inbound_enabled" -eq 1 ]; then
sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG,$SB_MIXED_INBOUND_TAG")
else
sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG")
fi
config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "$sniff_inbounds")
config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns")
local quic_disable
config_get_bool quic_disable "main" "quic_disable" 0
if [ "$quic_disable" -eq 1 ]; then
config=$(sing_box_cm_add_reject_route_rule "$config" "protocol" "quic")
fi
config=$(
sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$SB_MAIN_OUTBOUND_TAG"
)
config=$(sing_box_cf_override_domain_port "$config" "$TEST_DOMAIN" 8443)
config_foreach include_source_ips_in_routing_handler
# TODO(ampetelin): Add block rules
config_foreach
local exclude_from_ip_enabled
config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" 0
if [ "$exclude_from_ip_enabled" -eq 1 ]; then
rule_tag="$(gen_id)"
config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG")
config_list_foreach "main" "exclude_traffic_ip" exclude_source_ip_from_routing_handler "$rule_tag"
fi
config_foreach configure_routing_for_section_lists
}
include_source_ips_in_routing_handler() {
local section="$1"
local all_traffic_from_ip_enabled rule_tag
config_get all_traffic_from_ip_enabled "$section" "all_traffic_from_ip_enabled" 0
if [ "$all_traffic_from_ip_enabled" -eq 1 ]; then
rule_tag="$(gen_id)"
config=$(
sing_box_cm_add_route_rule \
"$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$(get_outbound_tag_by_section "$section")"
)
config_list_foreach "$section" "all_traffic_ip" include_source_ip_in_routing_handler "$rule_tag"
fi
}
include_source_ip_in_routing_handler() {
local source_ip="$1"
local rule_tag="$2"
nft_list_all_traffic_from_ip "$source_ip"
config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip")
}
exclude_source_ip_from_routing_handler() {
local source_ip="$1"
local rule_tag="$2"
config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip")
}
configure_routing_for_section_lists() {
local section="$1"
local community_list_enabled local_domains_list_enabled remote_domains_list_enabled remote_subnets_list_enabled
local user_domains_list_type user_subnets_list_type route_rule_tag
config_get_bool community_list_enabled "$section" "community_list_enabled" 0
config_get user_domains_list_type "$section" "user_domains_list_type" "disabled"
config_get_bool local_domains_list_enabled "$section" "local_domains_list_enabled" 0
config_get_bool remote_domains_list_enabled "$section" "remote_domains_list_enabled" 0
config_get user_subnets_list_type "$section" "user_subnets_list_type" "disabled"
config_get_bool remote_subnets_list_enabled "$section" "remote_subnets_list_enabled" 0
if [ "$community_list_enabled" -eq 0 ] && \
[ "$user_domains_list_type" == "disabled" ] && \
[ "$local_domains_list_enabled" -eq 0 ] && \
[ "$remote_domains_list_enabled" -eq 0 ] && \
[ "$user_subnets_list_type" == "disabled" ] && \
[ "$remote_subnets_list_enabled" == 0 ] ; then
log "Section $section does not have any enabled list, skipping..." "warn"
return 0
fi
route_rule_tag="$(gen_id)"
outbound_tag=$(get_outbound_tag_by_section "$section")
config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag")
if [ "$community_list_enabled" -eq 1 ]; then
log "Processing community list routing rules for $section section"
config_list_foreach "$section" "community_list" configure_community_list_handler "$section" "$route_rule_tag"
fi
if [ "$user_domains_list_type" != "disabled" ]; then
log "Processing user domains routing rules for $section section"
# TODO(ampetelin): it is necessary to implement
# configure_user_domains_list_handler
fi
if [ "$local_domains_list_enabled" -eq 1 ]; then
log "Processing local domains routing rules for $section section"
# TODO(ampetelin): it is necessary to implement
# configure_local_domains_list_handler "$section" "$route_rule_tag"
fi
if [ "$remote_domains_list_enabled" -eq 1 ]; then
log "Processing local domains routing rules for $section section"
config_list_foreach "$section" "remote_domains_list" configure_remote_domains_or_subnets_list_handler \
"domains" "$section" "$route_rule_tag"
fi
if [ "$user_subnets_list_type" != "disabled" ]; then
log "Processing user subnets routing rules for $section section"
# TODO(ampetelin): it is necessary to implement
# configure_user_subnets_list_handler
fi
if [ "$remote_subnets_list_enabled" -eq 1 ]; then
log "Processing remote subnets routing rules for $section section"
config_list_foreach "$section" "remote_subnets_list" configure_remote_domains_or_subnets_list_handler \
"subnets" "$section" "$route_rule_tag"
fi
}
configure_community_list_handler() {
local tag="$1"
local section="$2"
local route_rule_tag="$3"
local rule_set_tag format url update_interval detour
rule_set_tag="$(get_rule_set_tag "$section" "$tag" "community")"
format="binary"
url="$SRS_MAIN_URL/$tag.srs"
detour="$(_get_download_detour_tag)"
config_get update_interval "main" "update_interval" "1d"
config=$(sing_box_cm_add_remote_ruleset "$config" "$rule_set_tag" "$format" "$url" "$detour" "$update_interval")
_add_rule_set_to_dns_rules "$rule_set_tag"
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag")
}
configure_user_domains_list_handler() {
local section="$1"
# TODO(ampetelin): it is necessary to implement
}
configure_local_domains_list_handler() {
local section="$1"
# TODO(ampetelin): it is necessary to implement
}
configure_remote_domains_or_subnets_list_handler() {
local url="$1"
local type="$2"
local section="$3"
local route_rule_tag="$4"
local file_extension
file_extension=$(get_url_file_extension "$url")
case "$file_extension" in
json|srs)
log "Detected file extension: .$file_extension → proceeding with processing" "debug"
local basename rule_set_tag format detour update_interval
basename=$(url_get_basename "$url")
rule_set_tag=$(get_rule_set_tag "$section" "$basename" "remote-$type")
format="$(get_rule_set_format_by_file_extension "$file_extension")"
detour="$(_get_download_detour_tag)"
config_get update_interval "main" "update_interval" "1d"
config=$(sing_box_cm_add_remote_ruleset "$config" "$rule_set_tag" "$format" "$url" "$detour" "$update_interval")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$rule_set_tag")
case "$type" in
domains) _add_rule_set_to_dns_rules "$rule_set_tag" "$route_rule_tag" ;;
subnets) ;;
*) log "Unsupported remote rule set type: $type" "warn" ;;
esac
;;
*)
log "Detected file extension: .$file_extension → no processing needed, managed on list_update"
;;
esac
}
configure_user_subnets_list_handler() {
local section="$1"
# TODO(ampetelin): it is necessary to implement
}
_get_download_detour_tag() {
config_get_bool detour "main" "detour" 0
if [ "${detour:-0}" -eq 1 ]; then
echo "$SB_MAIN_OUTBOUND_TAG"
else
echo ""
fi
}
_add_rule_set_to_dns_rules() {
local rule_set_tag="$1"
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$rule_set_tag")
local split_dns_enabled final_dns_server
config_get_bool split_dns_enabled "main" "split_dns_enabled" 0
if [ "$split_dns_enabled" -eq 1 ]; then
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "rule_set" "$rule_set_tag")
fi
}
sing_box_configure_experimental() {
log "Configure the experimental section of a sing-box JSON configuration"
log "Configuring cache database"
local cache_file
config_get cache_file "main" "cache_file" "/tmp/cache.db"
config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)
local yacd_enabled
config_get_bool yacd_enabled "main" "yacd" 0
if [ "$yacd_enabled" -eq 1 ]; then
log "Configuring Clash API (yacd)"
local external_controller="0.0.0.0:9090"
local external_controller_ui="ui"
config=$(sing_box_cm_configure_clash_api "$config" "$external_controller" "$external_controller_ui")
else
log "Clash API (yacd) is disabled, skipping configuration."
fi
}
sing_box_additional_inbounds() {
log "Configure the additional inbounds of a sing-box JSON configuration"
local mixed_inbound_enabled
config_get_bool mixed_inbound_enabled "main" "socks5" 0
if [ "$mixed_inbound_enabled" -eq 1 ]; then
config=$(
sing_box_cf_add_mixed_inbound_and_route_rule \
"$config" \
"$SB_MIXED_INBOUND_TAG" \
"$SB_MIXED_INBOUND_ADDRESS" \
"$SB_MIXED_INBOUND_PORT" \
"$SB_MAIN_OUTBOUND_TAG"
)
fi
config=$(
sing_box_cf_add_mixed_inbound_and_route_rule \
"$config" \
"$SB_SERVICE_MIXED_INBOUND_TAG" \
"$SB_SERVICE_MIXED_INBOUND_ADDRESS" \
"$SB_SERVICE_MIXED_INBOUND_PORT" \
"$SB_MAIN_OUTBOUND_TAG"
)
}
sing_box_config_check() {
if ! sing-box -c $SB_CONFIG check >/dev/null 2>&1; then
log "Sing-box configuration is invalid" "[fatal]"
exit 1
fi
}
sing_box_ruleset_domains_json() {
local domain="$1"
local section="$2"
local file="/tmp/podkop/$section-custom-domains-subnets.json"
jq --arg domain "$domain" '
.rules[0].domain_suffix += if .rules[0].domain_suffix | index($domain) then [] else [$domain] end
' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
log "$domain added to $section-custom-domains-subnets.json"
}
sing_box_ruleset_subnets_json() {
local subnet="$1"
local section="$2"
local file="/tmp/podkop/$section-custom-domains-subnets.json"
jq --arg subnet "$subnet" '
.rules[0].ip_cidr += if .rules[0].ip_cidr | index($subnet) then [] else [$subnet] end
' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
log "$subnet added to $section-custom-domains-subnets.json"
}
import_community_subnet_lists() {
config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
if [ "$domain_list_enabled" -eq 1 ]; then
log "Importing community subnet lists for $section section"
config_list_foreach "$section" domain_list import_community_service_subnet_list_handler
fi
}
import_community_service_subnet_list_handler() {
local service="$1"
local table="PodkopTable"
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 add set inet $table podkop_discord_subnets { type ipv4_addr\; flags interval\; auto-merge\; }
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_discord_subnets udp dport { 50000-65535 } meta mark set 0x105 counter
;;
*)
return
;;
esac
local filename=$(basename "$URL")
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "/tmp/podkop/$filename" "$URL"
else
wget -O "/tmp/podkop/$filename" "$URL"
fi
while IFS= read -r subnet; do
if [ "$service" = "discord" ]; then
nft add element inet $table podkop_discord_subnets { $subnet }
else
nft add element inet $table podkop_subnets { $subnet }
fi
done <"/tmp/podkop/$filename"
}
import_domains_from_remote_domain_lists() {
local section="$1"
config_get custom_download_domains_list_enabled "$section" custom_download_domains_list_enabled
if [ "$custom_download_domains_list_enabled" -eq 1 ]; then
log "Importing domains from remote domain lists for $section section"
config_list_foreach "$section" custom_download_domains import_domains_from_remote_domain_list_handler "$section"
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=$(get_url_file_extension "$url")
case "$file_extension" in
json|srs)
log "Detected file extension: .$file_extension → no update needed, sing-box manages updates"
;;
*)
log "Detected file extension: .$file_extension → proceeding with processing"
import_domains_from_remote_lst_file "$url" "$section"
;;
esac
}
import_domains_from_remote_lst_file() {
local url="$1"
local section="$2"
local filename
filename=$(basename "$url")
local filepath="/tmp/podkop/${filename}"
download_to_tempfile "$url" "$filepath"
while IFS= read -r domain; do
sing_box_ruleset_domains_json $domain $section
done <"$filepath"
rm -f "$filepath"
}
import_subnets_from_remote_subnet_lists() {
local section="$1"
config_get custom_download_subnets_list_enabled "$section" custom_download_subnets_list_enabled disabled
if [ "$custom_download_subnets_list_enabled" -eq "1" ]; then
log "Importing subnets from remote subnet lists for $section section"
config_list_foreach "$section" custom_download_subnets import_subnets_from_remote_subnet_list_handler "$section"
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=$(get_url_file_extension "$url")
case "$file_extension" in
lst)
log "Detected file extension: .$file_extension → proceeding with processing"
import_subnets_from_remote_lst_file "$url" "$section"
;;
json)
log "Detected file extension: .$file_extension → proceeding with processing"
import_subnets_from_remote_json_file "$url"
;;
srs)
log "Detected file extension: .$file_extension → proceeding with processing"
import_subnets_from_remote_srs_file "$url"
;;
*)
log "Detected file extension: .$file_extension → unsupported, skipping"
return 1
;;
esac
}
import_subnets_from_remote_lst_file() {
local url="$1"
local section="$2"
local filename
filename=$(basename "$url")
local filepath="/tmp/podkop/${filename}"
download_to_tempfile "$url" "$filepath"
while IFS= read -r subnet; do
sing_box_ruleset_subnets_json "$subnet" "$section"
nft_add_podkop_subnet "$subnet"
done <"$filepath"
rm -f "$filepath"
}
import_subnets_from_remote_json_file() {
local url="$1"
download_to_stream "$url" | jq -r '.rules[].ip_cidr[]?' | while read -r subnet; do
nft_add_podkop_subnet "$subnet"
done
}
import_subnets_from_remote_srs_file() {
local url="$1"
local filename
filename=$(basename "$url")
local binary_filepath="/tmp/podkop/${filename}"
local json_filepath="/tmp/podkop/decompiled-${filename%%.*}.json"
download_to_tempfile "$url" "$binary_filepath"
if ! decompile_srs_file "$binary_filepath" "$json_filepath"; then
return 1
fi
jq -r '.rules[].ip_cidr[]' "$json_filepath" | while read -r subnet; do
nft_add_podkop_subnet "$subnet"
done
rm -f "$binary_filepath" "$json_filepath"
}
# Decompiles a sing-box SRS binary file into a JSON ruleset file
decompile_srs_file() {
local binary_filepath="$1"
local output_filepath="$2"
log "Decompiling $binary_filepath to $output_filepath"
if ! file_exists "$binary_filepath"; then
log "File $binary_filepath not found"
return 1
fi
sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
if [[ $? -ne 0 ]]; then
log "Decompilation command failed for $binary_filepath"
return 1
fi
}
## nftables
nft_list_all_traffic_from_ip() {
local ip="$1"
local table="PodkopTable"
if ! nft list chain inet $table mangle | grep -q "ip saddr $ip"; then
nft insert rule inet $table mangle iifname "$SRC_INTERFACE" ip saddr $ip meta l4proto { tcp, udp } meta mark set 0x105 counter
nft insert rule inet $table mangle ip saddr $ip ip daddr @localv4 return
fi
}
# Adds an IPv4 subnet to nftables firewall set
nft_add_podkop_subnet() {
local subnet="$1"
if is_ipv4_cidr "$subnet"; then
nft add element inet PodkopTable podkop_subnets { "$subnet" }
else
log "Invalid subnet format. $subnet is not a valid IPv4 CIDR"
fi
}
# Diagnotics
check_proxy() {
if ! command -v sing-box >/dev/null 2>&1; then
nolog "sing-box is not installed"
return 1
fi
if [ ! -f $SB_CONFIG ]; then
nolog "Configuration file not found"
return 1
fi
nolog "Checking sing-box configuration..."
if ! sing-box -c $SB_CONFIG 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
)' $SB_CONFIG
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 PodkopTable rules..."
# Check if table exists
if ! nft list table inet PodkopTable >/dev/null 2>&1; then
nolog "❌ PodkopTable 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 PodkopTable $set_name >/dev/null 2>&1; then
# Count elements using grep to count commas and add 1 (last element has no comma)
local count=$(nft list set inet PodkopTable $set_name 2>/dev/null | grep -o ',\|{' | wc -l)
echo "- $set_name: $count elements"
fi
done
nolog "Chain configurations:"
# Create a temporary file for processing
local tmp_file=$(mktemp)
nft list table inet PodkopTable > "$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 PodkopTable
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 detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -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"
}
# TODO(ampetelin): need fix after refactoring
check_fakeip() {
# Not used
nolog "Checking fakeip functionality..."
if ! command -v nslookup >/dev/null 2>&1; then
nolog "nslookup is not installed"
return 1
fi
local test_domain="$TEST_DOMAIN"
nolog "Testing DNS resolution with default DNS server"
echo "=== Testing with default DNS server ==="
nslookup -timeout=2 $test_domain
echo ""
nolog "Finding a working DNS resolver..."
local working_resolver=$(find_working_resolver)
if [ -z "$working_resolver" ]; then
nolog "No working resolver found, skipping resolver check"
else
nolog "Using resolver: $working_resolver"
nolog "Testing DNS resolution with working resolver ($working_resolver)"
echo "=== Testing with working resolver ($working_resolver) ==="
nslookup -timeout=2 $test_domain $working_resolver
echo ""
fi
# Main FakeIP check
nolog "Testing DNS resolution for $test_domain using 127.0.0.42"
echo "=== Testing with FakeIP DNS (127.0.0.42) ==="
local result=$(nslookup -timeout=2 $test_domain 127.0.0.42 2>&1)
echo "$result"
if echo "$result" | grep -q "198.18"; then
nolog "✅ FakeIP is working correctly! Domain resolved to FakeIP range (198.18.x.x)"
return 0
else
nolog "❌ FakeIP test failed. Domain did not resolve to FakeIP range"
nolog "Checking if sing-box is running..."
if ! pgrep -f "sing-box" >/dev/null; then
nolog "sing-box is not running"
else
nolog "sing-box is running, but FakeIP might not be configured correctly"
nolog "Checking DNS configuration in sing-box..."
if [ -f "$SB_CONFIG" ]; then
local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SB_CONFIG")
local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SB_CONFIG")
nolog "FakeIP enabled: $fakeip_enabled"
nolog "FakeIP range: $fakeip_range"
local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SB_CONFIG")
nolog "FakeIP domain: $dns_rules"
else
nolog "sing-box config file not found"
fi
fi
return 1
fi
}
check_logs() {
nolog "Showing podkop logs from system journal..."
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() {
nolog "Current sing-box configuration:"
if [ ! -f "$SB_CONFIG" ]; 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
)' "$SB_CONFIG"
}
show_config() {
if [ ! -f /etc/config/podkop ]; then
nolog "Configuration file not found"
return 1
fi
tmp_config=$(mktemp)
cat /etc/config/podkop | 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|"
> "$tmp_config"
cat "$tmp_config"
rm -f "$tmp_config"
}
show_version() {
local version=$(opkg list-installed podkop | awk '{print $3}')
echo "$version"
}
show_luci_version() {
local version=$(opkg list-installed luci-app-podkop | awk '{print $3}')
echo "$version"
}
show_sing_box_version() {
local 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=$(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=$(uci get podkop.main.dns_type 2>/dev/null)
local dns_server=$(uci get podkop.main.dns_server 2>/dev/null)
local is_available=0
local status="unavailable"
local local_dns_working=0
local local_dns_status="unavailable"
# Mask NextDNS ID if present
local display_dns_server="$dns_server"
if echo "$dns_server" | grep -q "\.dns\.nextdns\.io$"; then
local 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=$(echo "$dns_server" | cut -d'/' -f2- | sed 's/./*/g')
display_dns_server="dns.nextdns.io/$masked_path"
fi
if [ "$dns_type" = "doh" ]; then
# Generate random DNS query ID (2 bytes)
local random_id=$(head -c2 /dev/urandom | hexdump -ve '1/1 "%.2x"' 2>/dev/null)
if [ $? -ne 0 ]; then
error_message="Failed to generate random ID"
status="internal error"
else
# Create DNS wire format query for google.com A record with random ID
local dns_query=$(printf "\x${random_id:0:2}\x${random_id:2:2}\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01" | base64 2>/dev/null)
if [ $? -ne 0 ]; then
error_message="Failed to generate DNS query"
status="internal error"
else
# Try POST method first (RFC 8484 compliant) with shorter timeout
local result=$(echo "$dns_query" | base64 -d 2>/dev/null | curl -H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @- \
--max-time 2 \
--connect-timeout 1 \
-s \
"https://$dns_server/dns-query" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
is_available=1
status="available"
else
# Try GET method as fallback with shorter timeout
local dns_query_no_padding=$(echo "$dns_query" | tr -d '=' 2>/dev/null)
result=$(curl -H "accept: application/dns-message" \
--max-time 2 \
--connect-timeout 1 \
-s \
"https://$dns_server/dns-query?dns=$dns_query_no_padding" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ]; then
is_available=1
status="available"
else
error_message="DoH server not responding"
fi
fi
fi
fi
elif [ "$dns_type" = "dot" ]; then
(nc "$dns_server" 853 </dev/null >/dev/null 2>&1) & pid=$!
sleep 2
if kill -0 $pid 2>/dev/null; then
kill $pid 2>/dev/null
wait $pid 2>/dev/null
else
is_available=1
status="available"
fi
elif [ "$dns_type" = "udp" ]; then
if nslookup -timeout=2 itdog.info $dns_server >/dev/null 2>&1; then
is_available=1
status="available"
fi
fi
# Check if local DNS resolver is working
if nslookup -timeout=2 $TEST_DOMAIN 127.0.0.1 >/dev/null 2>&1; then
local_dns_working=1
local_dns_status="available"
fi
echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"is_available\":$is_available,\"status\":\"$status\",\"local_dns_working\":$local_dns_working,\"local_dns_status\":\"$local_dns_status\"}"
}
print_global() {
local message="$1"
echo "$message"
}
global_check() {
print_global "📡 Global check run!"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_global "🛠️ System info"
print_global "🕳️ Podkop: $(opkg list-installed podkop | awk '{print $3}')"
print_global "🕳️ LuCI App: $(opkg list-installed luci-app-podkop | awk '{print $3}')"
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 $TEST_DOMAIN
local working_resolver=$(find_working_resolver)
if [ -z "$working_resolver" ]; then
print_global "❌ No working external resolver found"
else
print_global "➡️ DNS resolution: external resolver ($working_resolver)"
nslookup -timeout=2 $TEST_DOMAIN $working_resolver
fi
print_global "➡️ DNS resolution: sing-box DNS server (127.0.0.42)"
local result=$(nslookup -timeout=2 $TEST_DOMAIN 127.0.0.42 2>&1)
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, checking configuration"
if [ -f "$SB_CONFIG" ]; then
local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SB_CONFIG")
local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SB_CONFIG")
local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SB_CONFIG")
print_global " 📦 FakeIP enabled: $fakeip_enabled"
print_global " 📦 FakeIP range: $fakeip_range"
print_global " 📦 FakeIP domain: $dns_rules"
else
print_global " ⛔ sing-box config file not found"
fi
fi
fi
}
# TODO: create helper functon
# Download URL content directly
download_to_stream() {
local url="$1"
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -qO- "$url" | sed 's/\r$//'
else
wget -qO- "$url" | sed 's/\r$//'
fi
}
# TODO: create helper functon
# Download URL to temporary file
download_to_tempfile() {
local url="$1"
local filepath="$2"
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -O "$filepath" "$url"
else
wget -O "$filepath" "$url"
fi
if grep -q $'\r' "$filepath"; then
log "$filename has Windows line endings (CRLF). Converting to Unix (LF)"
sed -i 's/\r$//' "$filepath"
fi
}
show_help() {
cat << EOF
Usage: $0 COMMAND
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_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_fakeip Check FakeIP DNS functionality
check_dnsmasq Check DNSMasq configuration
show_config Display current podkop configuration
show_version Show podkop version
show_sing_box_config Show sing-box configuration
show_luci_version Show LuCI app version
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_github)
check_github
;;
check_logs)
check_logs
;;
check_sing_box_connections)
check_sing_box_connections
;;
check_sing_box_logs)
check_sing_box_logs
;;
check_fakeip)
check_fakeip
;;
check_dnsmasq)
check_dnsmasq
;;
show_config)
show_config
;;
show_version)
show_version
;;
show_sing_box_config)
show_sing_box_config
;;
show_luci_version)
show_luci_version
;;
show_sing_box_version)
show_sing_box_version
;;
show_system_info)
show_system_info
;;
get_status)
get_status
;;
get_sing_box_status)
get_sing_box_status
;;
check_dns_available)
check_dns_available
;;
global_check)
global_check
;;
*)
show_help
exit 1
;;
esac