Files
podkop/podkop/files/etc/init.d/podkop

1608 lines
49 KiB
Bash
Executable File

#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
script=$(readlink "$initscript")
NAME="$(basename ${script:-$initscript})"
config_load "$NAME"
EXTRA_COMMANDS="main list_update check_proxy check_nft check_github check_logs check_sing_box_connections check_sing_box_logs check_dnsmasq show_config show_version show_sing_box_config"
EXTRA_HELP=" list_update Updating domain and subnet lists
check_proxy Check if sing-box proxy works correctly
check_nft Show PodkopTable nftables rules
check_github Check GitHub connectivity and lists availability
check_logs Show podkop logs from system journal
check_sing_box_connections Show active sing-box network connections
check_sing_box_logs Show recent sing-box logs
check_dnsmasq Show current DNSMasq configuration
show_config Show current configuration with masked sensitive data
show_version Show current version
show_sing_box_config Show current sing-box configuration"
[ ! -L /usr/sbin/podkop ] && ln -s /etc/init.d/podkop /usr/sbin/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"
SING_BOX_CONFIG="/etc/sing-box/config.json"
CACHE_FILE_PATH="/tmp/cache.db"
FAKEIP="198.18.0.0/15"
VALID_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube discord meta twitter hdrezka tiktok telegram"
start_service() {
log "Start podkop"
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
echo "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"
exit 1
fi
if opkg list-installed | grep -qE "iptables|kmod-iptab"; then
printf "\033[31;1mFound incompatible iptables packages. If you're using FriendlyWrt: https://t.me/itdogchat/44512/181082\033[0m\n"
fi
if ! ip addr | grep -q "br-lan"; then
log "Interface br-lan not found"
exit 1
fi
migration
config_foreach process_validate_service
procd_open_instance
procd_set_param command /bin/sh -c "/etc/init.d/podkop main &"
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
log "Stopping the podkop"
remove_cron_job
dnsmasq_rm
rm -rf /tmp/podkop/*
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
}
restart_service() {
stop
start
}
reload_service() {
stop
start
}
service_triggers() {
log "service_triggers start"
procd_add_config_trigger "config.change" "$NAME" "$initscript" reload 'on_config_change'
}
log() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local CYAN="\033[0;36m"
local GREEN="\033[0;32m"
local RESET="\033[0m"
echo -e "${CYAN}[$timestamp]${RESET} ${GREEN}$message${RESET}"
logger -t "podkop" "$timestamp $message"
}
nolog() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local CYAN="\033[0;36m"
local GREEN="\033[0;32m"
local RESET="\033[0m"
echo -e "${CYAN}[$timestamp]${RESET} ${GREEN}$message${RESET}"
}
main() {
sleep 5
config_foreach wget_github
mkdir -p /tmp/podkop
# base
route_table_rule_mark
create_nft_table
sing_box_uci
# sing-box
sing_box_inbound_proxy 1602
sing_box_dns
sing_box_dns_rule_fakeip
sing_box_rule_dns
sing_box_cache_file
process_socks5
# sing-box outbounds and rules
config_foreach sing_box_outdound
config_foreach process_domains_for_section
config_foreach process_remote_ruleset
config_foreach sing_box_rule_preset
config_foreach process_domains_list_local
config_foreach process_domains_list_url
config_foreach process_subnet_for_section
config_foreach process_subnet_for_section_remote
config_foreach process_all_traffic_for_section
config_foreach add_cron_job
# Future: exclude at the fakeip?
config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" "0"
if [ "$exclude_from_ip_enabled" -eq 1 ]; then
log "Adding an IP for exclusion"
config_list_foreach main exclude_traffic_ip sing_box_rules_source_ip_cidr $exclude_traffic_ip direct-out
fi
config_get_bool yacd "main" "yacd" "0"
if [ "$yacd" -eq 1 ]; then
log "Yacd enable"
jq '.experimental.clash_api = {
"external_ui": "ui",
"external_controller": "0.0.0.0:9090"
}' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
fi
config_get_bool exclude_ntp "main" "exclude_ntp" "0"
if [ "$exclude_ntp" -eq 1 ]; then
log "NTP traffic exclude for proxy"
nft insert rule inet PodkopTable mangle udp dport 123 return
fi
config_get_bool quic_disable "main" "quic_disable" "0"
if [ "$quic_disable" -eq 1 ]; then
log "Rule for disable QUIC"
sing_box_quic_reject
fi
sing_box_config_check
/etc/init.d/sing-box restart
/etc/init.d/sing-box enable
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
if [ -n "$proxy_string" ] || [ -n "$interface" ]; then
dnsmasq_add
fi
}
# 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
}
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() {
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
}
# 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
}
create_nft_table() {
local table="PodkopTable"
nft add table inet $table
log "Create nft rules"
nft add chain inet $table mangle { type filter hook prerouting 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 "br-lan" ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "br-lan" ip daddr @podkop_subnets meta l4proto udp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "br-lan" ip daddr "$FAKEIP" meta l4proto tcp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "br-lan" 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
}
dnsmasq_add() {
## Future: Check config and skip restart
log "Configure dnsmasq for sing-box"
uci set dhcp.@dnsmasq[0].noresolv="1"
uci set dhcp.@dnsmasq[0].filter_aaaa="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 add_list dhcp.@dnsmasq[0].server='/use-application-dns.net/'
uci commit dhcp
/etc/init.d/dnsmasq restart
}
dnsmasq_rm() {
log "Removing configuration for dnsmasq"
uci set dhcp.@dnsmasq[0].noresolv="0"
uci set dhcp.@dnsmasq[0].filter_aaaa="0"
uci set dhcp.@dnsmasq[0].cachesize="1000"
uci -q delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server="8.8.8.8"
uci commit dhcp
/etc/init.d/dnsmasq restart
}
process_domains_text() {
local text="$1"
local name="$2"
local tmp_file=$(mktemp)
echo "$text" > "$tmp_file"
sed 's/[, ]\+/\n/g' "$tmp_file" | while IFS= read -r domain; do
domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$domain" ]; then
sing_box_ruleset_domains "$domain" "$name"
fi
done
rm -f "$tmp_file"
}
process_subnets_text() {
local text="$1"
local name="$2"
local tmp_file=$(mktemp)
echo "$text" > "$tmp_file"
sed 's/[, ]\+/\n/g' "$tmp_file" | while IFS= read -r subnet; do
subnet=$(echo "$subnet" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$subnet" ]; then
if ! echo "$subnet" | grep -q "/"; then
subnet="$subnet/32"
fi
sing_box_ruleset_subnets "$subnet" "$name"
fi
done
rm -f "$tmp_file"
}
wget_github() {
local count_nslookup=0
local count_curl=0
config_get domain_list_enabled "$section" "domain_list_enabled"
config_get subnets_list_enabled "$section" "subnets_list_enabled"
config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled"
config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled"
if [ "$domain_list_enabled" -eq 1 ] || [ "$subnets_list_enabled" -eq 1 ] ||
[ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ] ; then
while true; do
if ! nslookup google.com >/dev/null 2>&1; then
log "DNS not working. Retrying... [$count_nslookup sec]"
count_nslookup=$((count_nslookup + 1))
else
return
fi
if [ $count_nslookup -lt 30 ]; then
sleep_interval=1
elif [ $count_nslookup -ge 30 ] && [ $count_nslookup -lt 60 ]; then
sleep_interval=5
elif [ $count_nslookup -ge 60 ] && [ $count_nslookup -lt 90 ]; then
sleep_interval=10
else
sleep_interval=30
fi
sleep $sleep_interval
done
while true; do
if ! curl -m 3 github.com; then
log "GitHub is not available. Check the internet availability [$count_curl sec]"
count_curl=$((count_curl + 1))
else
return
fi
if [ $count_curl -lt 30 ]; then
sleep_interval=1
elif [ $count_curl -ge 30 ] && [ $count_curl -lt 60 ]; then
sleep_interval=5
elif [ $count_curl -ge 60 ] && [ $count_curl -lt 90 ]; then
sleep_interval=10
else
sleep_interval=30
fi
sleep $sleep_interval
done
fi
}
add_cron_job() {
## Future: make a check so that it doesn't recreate many times
config_get domain_list_enabled "$section" "domain_list_enabled"
config_get subnets_list_enabled "$section" "subnets_list_enabled"
config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled"
config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled"
config_get update_interval "main" "update_interval"
case "$update_interval" in
"1h")
cron_job="13 * * * * /etc/init.d/podkop list_update"
;;
"3h")
cron_job="13 */3 * * * /etc/init.d/podkop list_update"
;;
"12h")
cron_job="13 */12 * * * /etc/init.d/podkop list_update"
;;
"1d")
cron_job="13 9 * * * /etc/init.d/podkop list_update"
;;
"3d")
cron_job="13 9 */3 * * /etc/init.d/podkop list_update"
;;
*)
log "Invalid update_interval value: $update_interval"
return
;;
esac
if [ "$domain_list_enabled" -eq 1 ] || [ "$subnets_list_enabled" -eq 1 ] ||
[ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ] ; then
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 "/etc/init.d/podkop list_update") | crontab -
log "The cron job removed"
}
list_update() {
log "Update remote lists"
config_foreach process_remote_ruleset
config_foreach process_domains_list_url
config_foreach process_subnet_for_section_remote
}
# 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"
else
log "Sing-box UCI config OK"
fi
}
# Future: for every section. +1 port?
process_socks5() {
config_get_bool socks5 "main" "socks5" "0"
if [ "$socks5" -eq 1 ]; then
log "Socks5 local enable port 2080"
jq '.inbounds += [{
"tag": "mixed-in",
"type": "mixed",
"listen": "0.0.0.0",
"listen_port": 2080,
"set_system_proxy": false
}]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
#local rule_exists=$(jq -r '.route.rules[] | select(.inbound[] == "mixed-in")' $SING_BOX_CONFIG)
local rule_exists=$(jq -r '.route.rules // [] | map(select(.inbound // [] | index("mixed-in"))) | length' $SING_BOX_CONFIG)
if [ -z "$rule_exists" ]; then
jq '.route.rules += [{
"inbound": ["mixed-in"],
"outbound": "main",
"action": "route"
}]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
fi
fi
}
sing_box_inbound_proxy() {
local listen_port="$1"
jq -n \
--arg listen_port "$listen_port" \
'{
"log": {
"level": "warn"
},
"inbounds": [
{
"tag": "tproxy-in",
"type": "tproxy",
"listen": "::",
"listen_port": ($listen_port|tonumber),
"tcp_fast_open": true,
"udp_fragment": true
},
{
"tag": "dns-in",
"type": "direct",
"listen": "127.0.0.42",
"listen_port": 53
}
],
"outbounds": [
{
"tag": "direct-out",
"type": "direct"
}
]
}' > $SING_BOX_CONFIG
}
sing_box_dns() {
log "Configure DNS in sing-box"
jq \
--arg FAKEIP "$FAKEIP" \
'.dns = {
"strategy": "ipv4_only",
"fakeip": {
"enabled": true,
"inet4_range": $FAKEIP
},
"servers": [
{
"tag": "cloudflare-doh-server",
"address": "https://1.1.1.1/dns-query",
"detour": "direct-out"
},
{
"tag": "fakeip-server",
"address": "fakeip"
}
]
}' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
}
sing_box_dns_rule_fakeip() {
log "Configure fakeip route in sing-box"
jq \
'.dns += {
"rules": [
{
"query_type": [
"HTTPS"
],
"action": "reject"
},
{
"server": "fakeip-server",
"rule_set": []
}
]
}' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
}
sing_box_dns_rule_fakeip_section() {
local rule_set=$1
echo $rule_set
log "Adding section to fakeip route rules in sing-box"
jq \
--arg rule_set "$rule_set" \
'.dns.rules |= map(
if .server == "fakeip-server" then
if any(.rule_set[]?; . == $rule_set) then
.
else
.rule_set += [$rule_set]
end
else
.
end
)' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
}
sing_box_cache_file() {
log "Configure cache.db in sing-box"
jq \
--arg CACHE_FILE_PATH "$CACHE_FILE_PATH" \
'.experimental = {
"cache_file": {
"enabled": true,
"store_fakeip": true,
"path": $CACHE_FILE_PATH
}
}' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
}
sing_box_outdound() {
local section="$1"
config_get mode "$section" "mode"
case "$mode" in
"vpn")
log "VPN mode"
log "You are using VPN mode, make sure you have installed all the necessary packages and configured."
config_get interface "$section" "interface"
sing_box_outbound_interface $section $interface
;;
"proxy")
log "Proxy mode"
config_get proxy_config_type "$section" "proxy_config_type"
if [ "$proxy_config_type" = "outbound" ]; then
config_get outbound_json $section "outbound_json"
if [ -n "$outbound_json" ]; then
log "Using JSON outbound configuration"
sing_box_config_outbound_json "$outbound_json"
else
log "Missing outbound JSON configuration"
return
fi
else
config_get proxy_string $section "proxy_string"
if [[ "$proxy_string" =~ ^ss:// ]]; then
sing_box_config_shadowsocks "$section" "$proxy_string"
elif [[ "$proxy_string" =~ ^vless:// ]]; then
sing_box_config_vless "$section" "$proxy_string"
else
log "Unsupported proxy type or missing configuration"
return
fi
fi
;;
*)
log "Requires *vpn* or *proxy* value"
return
;;
esac
}
sing_box_outbound_interface() {
local section="$1"
local interface="$2"
jq --arg section "$section" \
--arg interface "$interface" \
'. |
.outbounds |= (
map(
if .tag == $section then
. + {"type": "direct", "bind_interface": $interface}
else . end
) +
(
if (map(select(.tag == $section)) | length) == 0 then
[{"tag": $section, "type": "direct", "bind_interface": $interface}]
else [] end
)
)' "$SING_BOX_CONFIG" > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
if [ $? -eq 0 ]; then
log "Config updated successfully"
else
log "Error: Invalid JSON config generated"
return 1
fi
}
sing_box_rule_dns() {
log "Configure rule dns in sing-box"
jq \
'.route += {
"rules": [
{
"inbound": [
"dns-in",
"tproxy-in"
],
"action": "sniff"
},
{
"protocol": "dns",
"action": "hijack-dns"
}
],
"auto_detect_interface": true
}' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
}
sing_box_config_check() {
if ! sing-box -c $SING_BOX_CONFIG check >/dev/null 2>&1; then
log "Sing-box configuration is invalid"
exit 1
fi
}
sing_box_config_outbound_json() {
local json_config="$1"
local listen_port="$2"
cat > /tmp/base_config.json << EOF
{
"log": {
"level": "warn"
},
"inbounds": [
{
"type": "tproxy",
"listen": "::",
"listen_port": $listen_port,
"tcp_fast_open": true,
"udp_fragment": true
},
{
"tag": "dns-in",
"type": "direct",
"listen": "127.0.0.42",
"listen_port": 53
}
],
"outbounds": [],
"route": {
"auto_detect_interface": true
}
}
EOF
jq --argjson outbound "$json_config" '.outbounds += [$outbound]' /tmp/base_config.json > $SING_BOX_CONFIG
rm -f /tmp/base_config.json
}
sing_box_config_shadowsocks() {
local section="$1"
local STRING="$2"
if echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | base64 -d 2>/dev/null | grep -q ":"; then
local encrypted_part=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | base64 -d 2>/dev/null )
local method=$(echo "$encrypted_part" | cut -d':' -f1)
local password=$(echo "$encrypted_part" | cut -d':' -f2-)
else
local method_and_password=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1)
local method=$(echo "$method_and_password" | cut -d':' -f1)
local password=$(echo "$method_and_password" | cut -d':' -f2- | sed 's/%3D/=/g')
if echo "$method" | base64 -d ; then
method=$(echo "$method" | base64 -d)
fi
fi
local server=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f1)
local port=$(echo "$STRING" | sed -n 's|.*:\([0-9]\+\).*|\1|p')
jq \
--arg section "$section" \
--arg server "$server" \
--argjson port "$port" \
--arg method "$method" \
--arg password "$password" \
'. |
.outbounds |= (
map(
if .tag == $section then
. + {
"type": "shadowsocks",
"server": $server,
"server_port": ($port | tonumber),
"method": $method,
"password": $password,
"udp_over_tcp": { "enabled": true, "version": 2 }
}
else . end
) +
(
if (map(select(.tag == $section)) | length) == 0 then
[{
"tag": $section,
"type": "shadowsocks",
"server": $server,
"server_port": ($port | tonumber),
"method": $method,
"password": $password,
"udp_over_tcp": { "enabled": true, "version": 2 }
}]
else [] end
)
)' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
if [ $? -eq 0 ]; then
log "Config updated successfully"
else
log "Error: Invalid JSON config generated"
return 1
fi
}
sing_box_config_vless() {
local section="$1"
local STRING="$2"
get_param() {
local param="$1"
local value=$(echo "$STRING" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
value=$(echo "$value" | sed 's/%2F/\//g; s/%2C/,/g; s/%3D/=/g; s/%2B/+/g; s/%20/ /g' | tr -d '\n' | tr -d '\r')
echo "$value"
}
uuid=$(echo "$STRING" | cut -d'/' -f3 | cut -d'@' -f1 | tr -d '\n' | tr -d '\r' | sed 's/False//g')
server=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f1 | tr -d '\n' | tr -d '\r' | sed 's/False//g')
port=$(echo "$STRING" | cut -d'@' -f2 | cut -d':' -f2 | cut -d'?' -f1 | cut -d'/' -f1 | cut -d'#' -f1 | tr -d '\n' | tr -d '\r' | sed 's/False//g')
jq \
--arg server "$server" \
--argjson port "$port" \
--arg uuid "$uuid" \
--arg type "$(get_param "type")" \
--arg flow "$(get_param "flow")" \
--arg sni "$(get_param "sni")" \
--arg fp "$(get_param "fp")" \
--arg security "$(get_param "security")" \
--arg pbk "$(get_param "pbk")" \
--arg sid "$(get_param "sid")" \
--arg alpn "$(get_param "alpn")" \
--arg path "$(get_param "path")" \
--arg host "$(get_param "host")" \
--arg spx "$(get_param "spx")" \
--arg insecure "$(get_param "allowInsecure")" \
--arg section "$section" \
'. |
# Updating an existing outbound by tag or adding a new one
.outbounds |= (
# If an element with the required tag is found, update it
map(
if .tag == $section then
. + {
"type": "vless",
"server": $server,
"server_port": ($port | tonumber),
"uuid": $uuid,
"packet_encoding": "",
"domain_strategy": "",
"flow": $flow
}
else . end
) +
# Add a new outbound if the required tag is not present
(
if (map(select(.tag == $section)) | length) == 0 then
[{
"tag": $section,
"type": "vless",
"server": $server,
"server_port": ($port | tonumber),
"uuid": $uuid,
"packet_encoding": "",
"domain_strategy": "",
"flow": $flow
}]
else [] end
)
) |
# Additional parameters such as transport and tls
if $flow != "" then
.outbounds |= map(
if .tag == $section then
.flow = $flow
else . end
)
else . end |
if $type == "ws" then
.outbounds |= map(
if .tag == $section then
.transport = {
"type": "ws",
"path": $path
} |
if $host != "" then
.transport.headers = { "Host": $host }
else . end
else . end
)
elif $type == "grpc" then
.outbounds |= map(
if .tag == $section then
.transport = { "type": "grpc" }
else . end
)
else . end |
if $security == "reality" or $security == "tls" then
.outbounds |= map(
if .tag == $section then
.tls = {
"enabled": true,
"server_name": $sni,
"utls": {
"enabled": true,
"fingerprint": $fp
},
"insecure": ($insecure == "1")
} |
if $alpn != "" then
.tls.alpn = ($alpn | split(","))
else . end |
if $security == "reality" then
.tls.reality = {
"enabled": true,
"public_key": $pbk,
"short_id": $sid
}
else . end
else . end
)
else . end' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
if [ $? -eq 0 ]; then
log "Config created successfully"
else
log "Error: Invalid JSON config generated"
return 1
fi
}
# Process. Sing-box rules
sing_box_ruleset_domains() {
log "Configure ruleset domains in sing-box"
local domain=$1
local tag=$2
# Check if there is a route.rule_set for the specified tag
local tag_exists=$(jq -r --arg tag "$tag" '
.route.rule_set[]? | select(.tag == $tag) | .tag
' /etc/sing-box/config.json)
# If the tag exists, add the domain
if [[ -n "$tag_exists" ]]; then
jq \
--arg tag "$tag" \
--arg domain "$domain" \
'
.route.rule_set[] |=
if .tag == $tag then
.rules[0].domain_suffix += [$domain]
else
.
end
' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json
log "$domain added to the list for tag $tag"
else
# If tag does not exist, add a new set of rules
jq \
--arg tag "$tag" \
--arg domain "$domain" \
'
.route.rule_set += [
{
"tag": $tag,
"type": "inline",
"rules": [
{
"domain_suffix": [$domain]
}
]
}
]' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json
log "$domain added as a new rule set for tag $tag"
fi
}
sing_box_ruleset_subnets() {
log "Configure ruleset domains in sing-box"
local subnet=$1
local tag=$2
# nft
nft add element inet PodkopTable podkop_subnets { $subnet }
# Check if there is a route.rule_set for the specified tag
local tag_exists=$(jq -r --arg tag "$tag" '
.route.rule_set[]? | select(.tag == $tag) | .tag
' /etc/sing-box/config.json)
# If tag exists, add the domain
if [[ -n "$tag_exists" ]]; then
jq \
--arg tag "$tag" \
--arg subnet "$subnet" \
'
.route.rule_set[] |=
if .tag == $tag then
.rules[0].ip_cidr += [$subnet]
else
.
end
' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json
log "$subnet added to the list for tag $tag"
else
# If tag does not exist, add a new set of rules
jq \
--arg tag "$tag" \
--arg subnet "$subnet" \
'
.route.rule_set += [
{
"tag": $tag,
"type": "inline",
"rules": [
{
"ip_cidr": [$subnet]
}
]
}
]' /etc/sing-box/config.json > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json /etc/sing-box/config.json
log "$subnet added as a new rule set for tag $tag"
fi
}
process_domains_for_section() {
local section="$1"
config_get custom_domains_list_type "$section" "custom_domains_list_type" "disabled"
if [ "$custom_domains_list_type" != "disabled" ]; then
log "Adding a custom domains list for section $section"
if [ "$custom_domains_list_type" = "dynamic" ]; then
# Handle list domains from custom_domains
config_list_foreach "$section" custom_domains "sing_box_ruleset_domains" "$section"
elif [ "$custom_domains_list_type" = "text" ]; then
# Handle domains from text
config_get custom_domains_text "$section" "custom_domains_text"
process_domains_text "$custom_domains_text" "$section"
fi
fi
}
sing_box_ruleset_remote() {
local tag=$1
local type=$2
local update_interval=$3
url="$SRS_MAIN_URL/$tag.srs"
local tag_exists=$(jq -r --arg tag "$tag" '
.route.rule_set[]? | select(.tag == $tag) | .tag
' "$SING_BOX_CONFIG")
if [[ -n "$tag_exists" ]]; then
log "Ruleset with tag $tag already exists. Skipping addition."
else
jq \
--arg tag "$tag" \
--arg type "$type" \
--arg url "$url" \
--arg update_interval "$update_interval" \
'
.route.rule_set += [
{
"tag": $tag,
"type": $type,
"format": "binary",
"url": $url,
"update_interval": $update_interval
}
]' "$SING_BOX_CONFIG" > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
log "Added new ruleset with tag $tag"
fi
}
list_subnets_download() {
local service="$1"
local table="PodkopTable"
case "$service" in
"twitter")
URL=$SUBNETS_TWITTER
;;
"meta")
URL=$SUBNETS_META
;;
"telegram")
URL=$SUBNETS_TELERAM
;;
"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 "br-lan" ip daddr @podkop_discord_subnets udp dport { 50000-65535 } meta mark set 0x105 counter
;;
*)
return
;;
esac
local filename=$(basename "$URL")
wget -q -O "/tmp/podkop/$filename" "$URL"
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"
}
sing_box_rules() {
log "Configure rule in sing-box"
local rule_set="$1"
local outbound="$2"
# Check if there is an outbound rule for "tproxy-in"
local rule_exists=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .inbound == ["tproxy-in"])' "$SING_BOX_CONFIG")
if [[ -n "$rule_exists" ]]; then
# If a rule for tproxy-in exists, add a new rule_set to the existing rule
jq \
--arg rule_set "$rule_set" \
--arg outbound "$outbound" \
'(.route.rules[] | select(.outbound == $outbound and .inbound == ["tproxy-in"]) .rule_set) += [$rule_set]' \
"$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
else
# If there is no rule for tproxy-in, create a new one with rule_set
jq \
--arg rule_set "$rule_set" \
--arg outbound "$outbound" \
'.route.rules += [{
"inbound": ["tproxy-in"],
"rule_set": [$rule_set],
"outbound": $outbound,
"action": "route"
}]' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
fi
}
sing_box_quic_reject() {
local quic_rule_exists=$(jq -e '.route.rules[] | select(.protocol == "quic" and .action == "reject")' "$SING_BOX_CONFIG")
if [[ -z "$quic_rule_exists" ]]; then
jq '
.route.rules |= (
reduce .[] as $rule ([];
if $rule.protocol == "dns" and $rule.action == "hijack-dns" then
. + [$rule, {"protocol": "quic", "action": "reject"}]
else
. + [$rule]
end
)
)' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG"
log "QUIC reject rule added successfully"
fi
}
process_remote_ruleset() {
config_get_bool domain_list_enabled "$section" "domain_list_enabled" "0"
if [ "$domain_list_enabled" -eq 1 ]; then
log "Adding a srs list for $section"
config_list_foreach "$section" domain_list "sing_box_ruleset_remote" "remote" "1d"
config_list_foreach "$section" domain_list "list_subnets_download" "$section" "$domain_list"
fi
}
sing_box_rule_preset() {
config_get custom_domains_list_type "$section" "custom_domains_list_type"
config_get custom_subnets_list_enabled "$section" "custom_subnets_list_enabled"
if [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_subnets_list_enabled" != "disabled" ]; then
sing_box_rules "$section" "$section"
sing_box_dns_rule_fakeip_section "$section" "$section"
fi
config_get domain_list_enabled "$section" "domain_list_enabled"
config_get domain_list "$section" "domain_list"
if [ "$domain_list_enabled" -eq 1 ]; then
config_list_foreach $section domain_list sing_box_rules $section
config_list_foreach $section domain_list sing_box_dns_rule_fakeip_section domain_list
fi
}
list_custom_local_domains_create() {
local section="$2"
local local_file="$1"
local filename=$(basename "$local_file" | cut -d. -f1)
while IFS= read -r domain; do
log "From local file: $domain"
sing_box_ruleset_domains $domain $section
done <"$local_file"
}
process_domains_list_local() {
local section="$1"
config_get custom_local_domains_list_enabled "$section" "custom_local_domains_list_enabled"
if [ "$custom_local_domains_list_enabled" -eq 1 ]; then
log "Adding a custom domains list from file in $section"
config_list_foreach "$section" "custom_local_domains" list_custom_local_domains_create "$section"
fi
}
list_custom_url_domains_create() {
local section="$2"
local URL="$1"
local filename=$(basename "$URL")
wget -q -O "/tmp/podkop/${filename}" "$URL"
while IFS= read -r domain; do
log "From local file: $domain"
sing_box_ruleset_domains $domain $section
done <"/tmp/podkop/$filename"
}
process_domains_list_url() {
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 "Adding a custom domains list from URL in $section"
config_list_foreach "$section" "custom_download_domains" list_custom_url_domains_create "$section"
fi
}
process_subnet_for_section() {
local section="$1"
config_get custom_subnets_list_enabled "$section" "custom_subnets_list_enabled" "disabled"
if [ "$custom_subnets_list_enabled" != "disabled" ]; then
log "Adding a custom subnet list for section $section"
if [ "$custom_subnets_list_enabled" = "dynamic" ]; then
# Handle list domains from custom_domains
config_list_foreach "$section" custom_subnets "sing_box_ruleset_subnets" "$section"
elif [ "$custom_subnets_list_enabled" = "text" ]; then
# Handle domains from text
config_get custom_subnets_text "$section" "custom_subnets_text"
process_subnets_text "$custom_subnets_text" "$section"
fi
fi
}
list_custom_url_subnets_create() {
local section="$2"
local URL="$1"
local filename=$(basename "$URL")
wget -q -O "/tmp/podkop/${filename}" "$URL"
while IFS= read -r subnet; do
log "From local file: $subnet"
sing_box_ruleset_subnets $subnet $section
done <"/tmp/podkop/$filename"
}
process_subnet_for_section_remote() {
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 "Adding a custom SUBNET list from URL in $section"
config_list_foreach "$section" "custom_download_subnets" list_custom_url_subnets_create "$section"
fi
}
process_all_traffic_for_section() {
local section="$1"
config_get all_traffic_from_ip_enabled "$section" "all_traffic_from_ip_enabled"
if [ "$all_traffic_from_ip_enabled" -eq "1" ]; then
log "Adding an IP to redirect all traffic"
config_list_foreach $section all_traffic_ip list_all_traffic_from_ip
config_list_foreach $section all_traffic_ip sing_box_rules_source_ip_cidr "$section" "$all_traffic_ip"
fi
}
sing_box_rules_source_ip_cidr() {
log "Configure source_ip_cidr rule in sing-box"
local outbound="$2"
local source_ip_cidr="$1"
local current_source_ip_cidr=$(jq -r ".route.rules[] | select(.outbound == \"$outbound\" and .source_ip_cidr) | .rule_set" $SING_BOX_CONFIG)
if [[ -n "$current_source_ip_cidr" ]]; then
jq \
--arg source_ip_cidr "$source_ip_cidr" \
--arg outbound "$outbound" \
'(.route.rules[] | select(.outbound == $outbound) | .source_ip_cidr) += [$source_ip_cidr]' \
$SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
else
jq \
--arg source_ip_cidr "$source_ip_cidr" \
--arg outbound "$outbound" \
'.route.rules = [
{
"inbound": ["tproxy-in"],
"source_ip_cidr": [$source_ip_cidr],
"outbound": $outbound
}
] + .route.rules' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
fi
}
## nftables
list_all_traffic_from_ip() {
local ip="$1"
if ! nft list chain inet PodkopTable mangle | grep -q "ip saddr $ip"; then
nft add set inet PodkopTable localv4 { type ipv4_addr\; flags interval\; }
nft add element inet PodkopTable localv4 { \
0.0.0.0/8, \
10.0.0.0/8, \
127.0.0.0/8, \
169.254.0.0/16, \
172.16.0.0/12, \
192.0.0.0/24, \
192.0.2.0/24, \
192.88.99.0/24, \
192.168.0.0/16, \
198.51.100.0/24, \
203.0.113.0/24, \
224.0.0.0/4, \
240.0.0.0-255.255.255.255 }
nft insert rule inet PodkopTable mangle iifname "br-lan" ip saddr $ip meta l4proto { tcp, udp } meta mark set 0x105 counter
nft insert rule inet PodkopTable mangle ip saddr $ip ip daddr @localv4 return
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 $SING_BOX_CONFIG ]; then
nolog "Configuration file not found"
return 1
fi
nolog "Checking sing-box configuration..."
if ! sing-box -c $SING_BOX_CONFIG check >/dev/null; then
nolog "Invalid configuration"
return 1
fi
jq '
walk(
if type == "object" then
with_entries(
if .key == "uuid" then
.value = "MASKED"
elif .key == "server" then
.value = "MASKED"
elif .key == "server_name" then
.value = "MASKED"
elif .key == "password" then
.value = "MASKED"
elif .key == "public_key" then
.value = "MASKED"
elif .key == "short_id" then
.value = "MASKED"
elif .key == "fingerprint" then
.value = "MASKED"
elif .key == "server_port" then
.value = "MASKED"
else . end
)
else . end
)' $SING_BOX_CONFIG
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
# Get all sets
nolog "\nSets configuration:"
nft list table inet PodkopTable
nolog "\nNFT 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")
wget -q -O /dev/null "$url"
if [ $? -eq 0 ]; then
nolog "- $list_name: available"
else
nolog "- $list_name: not available"
fi
done
}
check_dnsmasq() {
nolog "Checking dnsmasq configuration..."
local config=$(uci show dhcp.@dnsmasq[0])
if [ -z "$config" ]; then
nolog "No dnsmasq configuration found"
return 1
fi
echo "$config" | while IFS='=' read -r key value; do
nolog "$key = $value"
done
}
check_sing_box_connections() {
nolog "Checking sing-box connections..."
if ! command -v netstat >/dev/null 2>&1; then
nolog "netstat is not installed"
return 1
fi
local connections=$(netstat -tuanp | grep sing-box)
if [ -z "$connections" ]; then
nolog "No active sing-box connections found"
return 1
fi
echo "$connections" | while read -r line; do
nolog "$line"
done
}
check_sing_box_logs() {
nolog "Showing sing-box logs from system journal..."
local logs=$(logread -e sing-box | tail -n 50)
if [ -z "$logs" ]; then
nolog "No sing-box logs found"
return 1
fi
echo "$logs"
}
check_logs() {
nolog "Showing podkop logs from system journal..."
if command -v logread >/dev/null 2>&1; then
logread -e podkop | tail -n 50
else
nolog "Error: logread command not found"
return 1
fi
}
show_sing_box_config() {
nolog "Current sing-box configuration:"
if [ ! -f "$SING_BOX_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
)' "$SING_BOX_CONFIG"
}
show_config() {
nolog "Current podkop configuration:"
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' \
> "$tmp_config"
cat "$tmp_config"
rm -f "$tmp_config"
}
show_version() {
local version=$(opkg info podkop | grep -m 1 "Version:" | cut -d' ' -f2)
echo "$version"
}