diff --git a/luci-app-podkop/Makefile b/luci-app-podkop/Makefile index 0829d75..c74090b 100644 --- a/luci-app-podkop/Makefile +++ b/luci-app-podkop/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-podkop -PKG_VERSION:=0.3.22 +PKG_VERSION:=0.3.23 PKG_RELEASE:=1 LUCI_TITLE:=LuCI podkop app diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js index 5af0fb7..d5a56ae 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js @@ -589,7 +589,7 @@ const createModalContent = (title, content) => { }; const showConfigModal = async (command, title) => { - const res = await safeExec('/etc/init.d/podkop', [command]); + const res = await safeExec('/usr/bin/podkop', [command]); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); ui.showModal(_(title), createModalContent(title, formattedOutput)); }; @@ -608,7 +608,7 @@ const ButtonFactory = { return this.createButton({ label: config.label, additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => safeExec('/etc/init.d/podkop', [config.action]) + onClick: () => safeExec('/usr/bin/podkop', [config.action]) .then(() => config.reload && location.reload()), style: config.style }); @@ -787,7 +787,7 @@ return view.extend({ // Additional Settings Tab (main section) let o = mainSection.tab('additional', _('Additional Settings')); - o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('http://openwrt.lan:9090/ui')); + o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('openwrt.lan:9090/ui')); o.default = '0'; o.rmempty = false; o.ucisection = 'main'; @@ -943,7 +943,7 @@ return view.extend({ return new Promise(async (resolve) => { try { - const singboxStatusResult = await safeExec('/etc/init.d/podkop', ['get_sing_box_status']); + const singboxStatusResult = await safeExec('/usr/bin/podkop', ['get_sing_box_status']); const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}'); if (!singboxStatus.running) { @@ -991,12 +991,12 @@ return view.extend({ system, fakeipStatus ] = await Promise.all([ - safeExec('/etc/init.d/podkop', ['get_status']), - safeExec('/etc/init.d/podkop', ['get_sing_box_status']), - safeExec('/etc/init.d/podkop', ['show_version']), - safeExec('/etc/init.d/podkop', ['show_luci_version']), - safeExec('/etc/init.d/podkop', ['show_sing_box_version']), - safeExec('/etc/init.d/podkop', ['show_system_info']), + safeExec('/usr/bin/podkop', ['get_status']), + safeExec('/usr/bin/podkop', ['get_sing_box_status']), + safeExec('/usr/bin/podkop', ['show_version']), + safeExec('/usr/bin/podkop', ['show_luci_version']), + safeExec('/usr/bin/podkop', ['show_sing_box_version']), + safeExec('/usr/bin/podkop', ['show_system_info']), checkFakeIP() ]); @@ -1039,14 +1039,14 @@ return view.extend({ const updateStatus = async () => { try { if (!versionReceived) { - const version = await safeExec('/etc/init.d/podkop', ['show_version'], 2000); + const version = await safeExec('/usr/bin/podkop', ['show_version'], 2000); if (version.stdout) { versionText = _('Podkop') + ' v' + version.stdout.trim(); versionReceived = true; } } - const singboxStatusResult = await safeExec('/etc/init.d/podkop', ['get_sing_box_status']); + const singboxStatusResult = await safeExec('/usr/bin/podkop', ['get_sing_box_status']); const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}'); const fakeipStatus = await checkFakeIP(); diff --git a/luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json b/luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json index 10ede7c..6d0eabc 100644 --- a/luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json +++ b/luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json @@ -5,6 +5,9 @@ "file": { "/etc/init.d/podkop": [ "exec" + ], + "/usr/bin/podkop": [ + "exec" ] }, "ubus": { diff --git a/podkop/Makefile b/podkop/Makefile index 93836b1..c6d82d6 100644 --- a/podkop/Makefile +++ b/podkop/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=podkop -PKG_VERSION:=0.3.22 +PKG_VERSION:=0.3.23 PKG_RELEASE:=1 PKG_MAINTAINER:=ITDog @@ -49,6 +49,9 @@ define Package/podkop/install $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/etc/config/podkop $(1)/etc/config/podkop + + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/podkop $(1)/usr/bin/podkop endef $(eval $(call BuildPackage,podkop)) diff --git a/podkop/files/etc/init.d/podkop b/podkop/files/etc/init.d/podkop index 7d0f10b..7f0d8e2 100755 --- a/podkop/files/etc/init.d/podkop +++ b/podkop/files/etc/init.d/podkop @@ -7,41 +7,8 @@ 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 show_luci_version show_sing_box_version show_system_info get_status get_sing_box_status" -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 - show_luci_version Show LuCI app version - show_sing_box_version Show sing-box version - show_system_info Show OpenWrt version and device model - get_sing_box_status Get sing-box status" - -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" -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" -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" -TEST_DOMAIN="google.com" - start_service() { - log "Start podkop" + echo "Start podkop" sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}') required_version="1.11.1" @@ -60,51 +27,19 @@ start_service() { fi if ! ip addr | grep -q "br-lan"; then - log "Interface br-lan not found" + echo "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 command /bin/sh -c "/usr/bin/podkop start" procd_set_param stdout 1 procd_set_param stderr 1 procd_close_instance } stop_service() { - log "Stopping the podkop" - remove_cron_job - - config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0" - if [ "$dont_touch_dhcp" -eq 0 ]; then - dnsmasq_restore - fi - - 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 - + /usr/bin/podkop stop } restart_service() { @@ -118,1728 +53,6 @@ reload_service() { } service_triggers() { - log "service_triggers start" + echo "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 3 - - 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_add_secure_dns_probe_domain - 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" - 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 -} - -# 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 -} - -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 -} - -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" - save_dnsmasq_config "dhcp.@dnsmasq[0].noresolv" "dhcp.@dnsmasq[0].podkop_noresolv" - save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize" - - 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" - else - uci add_list dhcp.@dnsmasq[0].podkop_server="$server" - fi - done - - 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=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null) - if [ -z "$cachesize" ]; then - log "dnsmasq revert: cachesize is unset" - else - uci set dhcp.@dnsmasq[0].cachesize="$cachesize" - fi - - local 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 - - local server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null) - if [[ "$server" == "127.0.0.42" ]]; then - uci -q delete dhcp.@dnsmasq[0].server - 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 - fi - - uci delete dhcp.@dnsmasq[0].podkop_cachesize - uci delete dhcp.@dnsmasq[0].podkop_noresolv - - 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" - - # First filter out full comment lines and remove comments after domains - grep -v "^[[:space:]]*\/\/" "$tmp_file" | sed 's/\/\/.*$//' > "${tmp_file}.filtered" - - sed 's/[, ]\+/\n/g' "${tmp_file}.filtered" | 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" "${tmp_file}.filtered" -} - -process_subnets_text() { - local text="$1" - local name="$2" - - local tmp_file=$(mktemp) - echo "$text" > "$tmp_file" - - # First filter out full comment lines and remove comments after subnets - grep -v "^[[:space:]]*\/\/" "$tmp_file" | sed 's/\/\/.*$//' > "${tmp_file}.filtered" - - sed 's/[, ]\+/\n/g' "${tmp_file}.filtered" | 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" "${tmp_file}.filtered" -} - -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 -} - -find_working_resolver() { - local resolver_found="" - for resolver in $DNS_RESOLVERS; do - if nslookup $TEST_DOMAIN $resolver >/dev/null 2>&1; then - echo "$resolver" - return 0 - fi - done - echo "8.8.8.8" - 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" - 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() { - local dns_type - local dns_server - local resolver_tag="resolver" - - config_get dns_type "main" "dns_type" "doh" - config_get dns_server "main" "dns_server" "1.1.1.1" - - local server_json - local is_ip=$(echo "$dns_server" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && echo "1" || echo "0") - - if [ "$is_ip" = "0" ]; then - log "Finding working DNS resolver" - local dns_resolver=$(find_working_resolver) - log "Found working resolver: $dns_resolver" - fi - - log "Configure DNS in sing-box" - - server_json=$(jq -n \ - --arg type "$dns_type" \ - --arg server "$dns_server" \ - --arg resolver "$resolver_tag" \ - --arg is_ip "$is_ip" \ - '{ - "servers": [ - { - "tag": "dns-server", - "address": ( - if $type == "doh" then - "https://" + $server + "/dns-query" - elif $type == "dot" then - "tls://" + $server - else - $server - end - ), - "detour": "direct-out" - } + ( - if $is_ip == "0" then - {"address_resolver": $resolver} - else - {} - end - ) - ] - }') - - if [ "$is_ip" = "0" ]; then - server_json=$(echo "$server_json" | jq \ - --arg resolver "$resolver_tag" \ - --arg address "$dns_resolver" \ - '.servers += [{ - "tag": $resolver, - "address": $address - }]') - fi - - server_json=$(echo "$server_json" | jq '.servers += [{"tag": "fakeip-server", "address": "fakeip"}]') - - jq \ - --argjson dns_config "$server_json" \ - --arg fakeip "$FAKEIP" \ - '.dns = { - "strategy": "ipv4_only", - "fakeip": { - "enabled": true, - "inet4_range": $fakeip - }, - "servers": $dns_config.servers - }' $SING_BOX_CONFIG > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG -} - -sing_box_dns_rule_fakeip() { - local rewrite_ttl - config_get rewrite_ttl "main" "dns_rewrite_ttl" "600" - - log "Configure fakeip route in sing-box and set TTL to $rewrite_ttl seconds" - - jq \ - --arg ttl "$rewrite_ttl" \ - '.dns += { - "rules": [ - { - "query_type": [ - "HTTPS" - ], - "action": "reject" - }, - { - "domain_suffix": [ - "use-application-dns.net" - ], - "action": "reject" - }, - { - "server": "fakeip-server", - "domain": "", - "rewrite_ttl": ($ttl | tonumber), - "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() { - config_get cache_file "main" "cache_file" "/tmp/cache.db" - - log "Configure sing-box cache.db path" - - jq \ - --arg cache_file "$cache_file" \ - '.experimental = { - "cache_file": { - "enabled": true, - "store_fakeip": true, - "path": $cache_file - } - }' $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" "$section" - else - log "Missing outbound JSON configuration" - return - fi - else - config_get proxy_string $section "proxy_string" - - # 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 "No active proxy configuration found" - return - fi - - if [[ "$active_proxy_string" =~ ^ss:// ]]; then - sing_box_config_shadowsocks "$section" "$active_proxy_string" - elif [[ "$active_proxy_string" =~ ^vless:// ]]; then - sing_box_config_vless "$section" "$active_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 section="$2" - - # Create new object with tag first, then merge with the rest of the config - local modified_config=$(echo "$json_config" | jq --arg section "$section" \ - 'del(.tag) | {"tag": $section} + .') - - jq --argjson outbound "$modified_config" \ - --arg section "$section" \ - '. | - .outbounds |= ( - map( - if .tag == $section then - $outbound - else . end - ) + - ( - if (map(select(.tag == $section)) | length) == 0 then - [$outbound] - 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 "Outbound config updated successfully" - else - log "Error: Invalid JSON config generated" - return 1 - fi -} - -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; s/%3B/;/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 -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" - config_get custom_local_domains_list_enabled "$section" "custom_local_domains_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 [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_subnets_list_enabled" != "disabled" ] || - [ "$custom_local_domains_list_enabled" = "1" ] || [ "$custom_download_domains_list_enabled" = "1" ] || - [ "$custom_download_subnets_list_enabled" = "1" ]; then - sing_box_rules "$section" "$section" - fi - - if [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_local_domains_list_enabled" = "1" ] || - [ "$custom_download_domains_list_enabled" = "1" ]; then - 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 - domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - if [ -n "$domain" ] && echo "$domain" | grep -E -q '^([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$'; then - log "Added $domain from local file" - sing_box_ruleset_domains "$domain" "$section" - else - log "Invalid domain skipped: $domain" - fi - 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 $all_traffic_ip $section - fi -} - -sing_box_rules_source_ip_cidr() { - log "Configure source_ip_cidr rule in sing-box" - local source_ip_cidr="$1" - local outbound="$2" - - local current_source_ip_cidr=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .action == "route" and (.rule_set | not))' $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 and .action == "route" and (.rule_set | not)) | .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, - "action": "route" - } - ] + .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 "^/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" -} - -show_luci_version() { - local version=$(opkg info luci-app-podkop | grep -m 1 "Version:" | cut -d' ' -f2) - echo "$version" -} - -show_sing_box_version() { - local version=$(opkg info sing-box | grep -m 1 "Version:" | cut -d' ' -f2) - 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 running=0 - local enabled=0 - local status="" - - # Check if service is enabled - if [ -x /etc/rc.d/S99podkop ]; then - enabled=1 - fi - - # Check if service is running - if pgrep -f "sing-box" >/dev/null; then - running=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\"}" -} - -sing_box_add_secure_dns_probe_domain() { - local domain="httpbin.org" - local override_address="numbersapi.com" - - if [ -z "$override_address" ]; then - log "Error: Could not get br-lan IP address" - return 1 - fi - - log "Adding DNS probe domain ${domain} to fakeip-server configuration" - - jq \ - --arg domain "$domain" \ - --arg override "$override_address" \ - '.dns.rules |= map( - if .server == "fakeip-server" then - . + { - "domain": $domain - } - else - . - end - ) | - .route.rules |= . + [ - { - "domain": $domain, - "action": "route-options", - "override_address": $override - } - ]' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG" - - log "DNS probe domain ${domain} configured with override to ${override_address}" } \ No newline at end of file diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop new file mode 100755 index 0000000..89dd8de --- /dev/null +++ b/podkop/files/usr/bin/podkop @@ -0,0 +1,1848 @@ +#!/bin/ash + +[ -r /lib/functions.sh ] && . /lib/functions.sh +[ -r /lib/config/uci.sh ] && . /lib/config/uci.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" +SING_BOX_CONFIG="/etc/sing-box/config.json" +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" +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" +TEST_DOMAIN="google.com" + +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}" +} + +start() { + migration + + config_foreach process_validate_service + + sleep 3 + + 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_add_secure_dns_probe_domain + 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" + 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() { + log "Stopping the podkop" + remove_cron_job + + config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0" + if [ "$dont_touch_dhcp" -eq 0 ]; then + dnsmasq_restore + fi + + 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 + +} + +# 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 +} + +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 +} + +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" + save_dnsmasq_config "dhcp.@dnsmasq[0].noresolv" "dhcp.@dnsmasq[0].podkop_noresolv" + save_dnsmasq_config "dhcp.@dnsmasq[0].cachesize" "dhcp.@dnsmasq[0].podkop_cachesize" + + 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" + else + uci add_list dhcp.@dnsmasq[0].podkop_server="$server" + fi + done + + 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=$(uci get dhcp.@dnsmasq[0].podkop_cachesize 2>/dev/null) + if [ -z "$cachesize" ]; then + log "dnsmasq revert: cachesize is unset" + else + uci set dhcp.@dnsmasq[0].cachesize="$cachesize" + fi + + local 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 + + local server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null) + if [[ "$server" == "127.0.0.42" ]]; then + uci -q delete dhcp.@dnsmasq[0].server + 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 + fi + + uci delete dhcp.@dnsmasq[0].podkop_cachesize + uci delete dhcp.@dnsmasq[0].podkop_noresolv + + 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" + + # First filter out full comment lines and remove comments after domains + grep -v "^[[:space:]]*\/\/" "$tmp_file" | sed 's/\/\/.*$//' > "${tmp_file}.filtered" + + sed 's/[, ]\+/\n/g' "${tmp_file}.filtered" | 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" "${tmp_file}.filtered" +} + +process_subnets_text() { + local text="$1" + local name="$2" + + local tmp_file=$(mktemp) + echo "$text" > "$tmp_file" + + # First filter out full comment lines and remove comments after subnets + grep -v "^[[:space:]]*\/\/" "$tmp_file" | sed 's/\/\/.*$//' > "${tmp_file}.filtered" + + sed 's/[, ]\+/\n/g' "${tmp_file}.filtered" | 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" "${tmp_file}.filtered" +} + +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 +} + +find_working_resolver() { + local resolver_found="" + for resolver in $DNS_RESOLVERS; do + if nslookup $TEST_DOMAIN $resolver >/dev/null 2>&1; then + echo "$resolver" + return 0 + fi + done + echo "8.8.8.8" + 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" + 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() { + local dns_type + local dns_server + local resolver_tag="resolver" + + config_get dns_type "main" "dns_type" "doh" + config_get dns_server "main" "dns_server" "1.1.1.1" + + local server_json + local is_ip=$(echo "$dns_server" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && echo "1" || echo "0") + + if [ "$is_ip" = "0" ]; then + log "Finding working DNS resolver" + local dns_resolver=$(find_working_resolver) + log "Found working resolver: $dns_resolver" + fi + + log "Configure DNS in sing-box" + + server_json=$(jq -n \ + --arg type "$dns_type" \ + --arg server "$dns_server" \ + --arg resolver "$resolver_tag" \ + --arg is_ip "$is_ip" \ + '{ + "servers": [ + { + "tag": "dns-server", + "address": ( + if $type == "doh" then + "https://" + $server + "/dns-query" + elif $type == "dot" then + "tls://" + $server + else + $server + end + ), + "detour": "direct-out" + } + ( + if $is_ip == "0" then + {"address_resolver": $resolver} + else + {} + end + ) + ] + }') + + if [ "$is_ip" = "0" ]; then + server_json=$(echo "$server_json" | jq \ + --arg resolver "$resolver_tag" \ + --arg address "$dns_resolver" \ + '.servers += [{ + "tag": $resolver, + "address": $address + }]') + fi + + server_json=$(echo "$server_json" | jq '.servers += [{"tag": "fakeip-server", "address": "fakeip"}]') + + jq \ + --argjson dns_config "$server_json" \ + --arg fakeip "$FAKEIP" \ + '.dns = { + "strategy": "ipv4_only", + "fakeip": { + "enabled": true, + "inet4_range": $fakeip + }, + "servers": $dns_config.servers + }' $SING_BOX_CONFIG > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG +} + +sing_box_dns_rule_fakeip() { + local rewrite_ttl + config_get rewrite_ttl "main" "dns_rewrite_ttl" "600" + + log "Configure fakeip route in sing-box and set TTL to $rewrite_ttl seconds" + + jq \ + --arg ttl "$rewrite_ttl" \ + '.dns += { + "rules": [ + { + "query_type": [ + "HTTPS" + ], + "action": "reject" + }, + { + "domain_suffix": [ + "use-application-dns.net" + ], + "action": "reject" + }, + { + "server": "fakeip-server", + "domain": "", + "rewrite_ttl": ($ttl | tonumber), + "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() { + config_get cache_file "main" "cache_file" "/tmp/cache.db" + + log "Configure sing-box cache.db path" + + jq \ + --arg cache_file "$cache_file" \ + '.experimental = { + "cache_file": { + "enabled": true, + "store_fakeip": true, + "path": $cache_file + } + }' $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" "$section" + else + log "Missing outbound JSON configuration" + return + fi + else + config_get proxy_string $section "proxy_string" + + # 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 "No active proxy configuration found" + return + fi + + if [[ "$active_proxy_string" =~ ^ss:// ]]; then + sing_box_config_shadowsocks "$section" "$active_proxy_string" + elif [[ "$active_proxy_string" =~ ^vless:// ]]; then + sing_box_config_vless "$section" "$active_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 section="$2" + + # Create new object with tag first, then merge with the rest of the config + local modified_config=$(echo "$json_config" | jq --arg section "$section" \ + 'del(.tag) | {"tag": $section} + .') + + jq --argjson outbound "$modified_config" \ + --arg section "$section" \ + '. | + .outbounds |= ( + map( + if .tag == $section then + $outbound + else . end + ) + + ( + if (map(select(.tag == $section)) | length) == 0 then + [$outbound] + 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 "Outbound config updated successfully" + else + log "Error: Invalid JSON config generated" + return 1 + fi +} + +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; s/%3B/;/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 -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" + config_get custom_local_domains_list_enabled "$section" "custom_local_domains_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 [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_subnets_list_enabled" != "disabled" ] || + [ "$custom_local_domains_list_enabled" = "1" ] || [ "$custom_download_domains_list_enabled" = "1" ] || + [ "$custom_download_subnets_list_enabled" = "1" ]; then + sing_box_rules "$section" "$section" + fi + + if [ "$custom_domains_list_type" != "disabled" ] || [ "$custom_local_domains_list_enabled" = "1" ] || + [ "$custom_download_domains_list_enabled" = "1" ]; then + 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 + domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$domain" ] && echo "$domain" | grep -E -q '^([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$'; then + log "Added $domain from local file" + sing_box_ruleset_domains "$domain" "$section" + else + log "Invalid domain skipped: $domain" + fi + 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 $all_traffic_ip $section + fi +} + +sing_box_rules_source_ip_cidr() { + log "Configure source_ip_cidr rule in sing-box" + local source_ip_cidr="$1" + local outbound="$2" + + local current_source_ip_cidr=$(jq -r '.route.rules[] | select(.outbound == "'"$outbound"'" and .action == "route" and (.rule_set | not))' $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 and .action == "route" and (.rule_set | not)) | .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, + "action": "route" + } + ] + .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 "^/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" +} + +show_luci_version() { + local version=$(opkg info luci-app-podkop | grep -m 1 "Version:" | cut -d' ' -f2) + echo "$version" +} + +show_sing_box_version() { + local version=$(opkg info sing-box | grep -m 1 "Version:" | cut -d' ' -f2) + 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 running=0 + local enabled=0 + local status="" + + # Check if service is enabled + if [ -x /etc/rc.d/S99podkop ]; then + enabled=1 + fi + + # Check if service is running + if pgrep -f "sing-box" >/dev/null; then + running=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\"}" +} + +sing_box_add_secure_dns_probe_domain() { + local domain="httpbin.org" + local override_address="numbersapi.com" + + if [ -z "$override_address" ]; then + log "Error: Could not get br-lan IP address" + return 1 + fi + + log "Adding DNS probe domain ${domain} to fakeip-server configuration" + + jq \ + --arg domain "$domain" \ + --arg override "$override_address" \ + '.dns.rules |= map( + if .server == "fakeip-server" then + . + { + "domain": $domain + } + else + . + end + ) | + .route.rules |= . + [ + { + "domain": $domain, + "action": "route-options", + "override_address": $override + } + ]' "$SING_BOX_CONFIG" >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json "$SING_BOX_CONFIG" + + log "DNS probe domain ${domain} configured with override to ${override_address}" +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + 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_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 + ;; + *) + echo "Usage: $0 {start|stop|restart|reload|enable|disable|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|show_luci_version|show_sing_box_version|show_system_info|get_status|get_sing_box_status}" + exit 1 + ;; +esac