mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-13 23:16:53 +03:00
2172 lines
72 KiB
Bash
Executable File
2172 lines
72 KiB
Bash
Executable File
#!/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 |