Compare commits

..

30 Commits

Author SHA1 Message Date
Kirill Sobakin
470f11699c Merge pull request #184 from itdoginfo/split_dns
Replacing Split DNS with Domain Resolver and Bootstrap DNS
2025-10-02 18:18:28 +03:00
Andrey Petelin
852b6c043a i18n: update Russian translations and template with new DNS and domain resolver entries 2025-10-02 19:52:21 +05:00
Andrey Petelin
f5cafd5573 chore: add --no-location option to msgmerge and msginit to omit source code references in PO files 2025-10-02 19:44:39 +05:00
Andrey Petelin
3562b913a2 chore: update DNS protocol and server field labels 2025-10-02 19:35:40 +05:00
Andrey Petelin
f4ac9dcc77 feat: add domain resolver support to VPN mode 2025-10-02 17:49:23 +05:00
Andrey Petelin
f5a629afcf feat: add optional domain_resolver parameter to interface outbound config function 2025-10-02 17:48:08 +05:00
Andrey Petelin
aea201bf24 fix: replace non-working split DNS with bootstrap DNS for upstream DNS resolution 2025-10-02 15:58:26 +05:00
Kirill Sobakin
1313c3b26f Merge pull request #182 from itdoginfo/translation
Translation
2025-10-02 10:54:26 +03:00
Andrey Petelin
a3f4e942c3 chore: update Russian translation file encoding to UTF-8 and reformat multiline strings for better readability 2025-10-02 11:17:33 +05:00
Andrey Petelin
4d8e4c1c13 chore: set width variable to 120 for consistent msgmerge and xgettext formatting in localization scripts 2025-10-02 11:16:50 +05:00
itdoginfo
0cb5c2daae Stop podkop before update sing-box 2025-10-01 14:38:32 +03:00
Kirill Sobakin
19fbfff555 Merge pull request #180 from itdoginfo/translation
Translation
2025-10-01 10:33:57 +03:00
Kirill Sobakin
75a2ed1e29 Merge pull request #179 from itdoginfo/fix
refactor: Add version checks and service existence validation
2025-09-30 19:26:45 +03:00
Andrey Petelin
759b6748c6 refactor: Replace opkg version checks with direct command execution 2025-09-30 19:55:25 +05:00
Andrey Petelin
0a27784f85 chore: Update translation template and Russian translations 2025-09-30 19:30:23 +05:00
Andrey Petelin
3b95ac2bc3 chore: Add width option and package name to xgettext and msgmerge scripts 2025-09-30 19:29:46 +05:00
Andrey Petelin
5c51d99d73 chore: Improve NTP exclusion option description for clarity 2025-09-30 13:25:12 +05:00
Andrey Petelin
904b90e012 fix: Remove empty string translations from UI labels 2025-09-30 13:06:52 +05:00
Andrey Petelin
5fb8343cf8 fix: Remove translation function from Yacd link in additional settings tab 2025-09-30 13:05:56 +05:00
Andrey Petelin
014f0f4bdf feat: Add scripts for generating and updating translation templates 2025-09-30 13:04:44 +05:00
Andrey Petelin
dd44e0156e fix: restore default cachesize and noresolv values in dnsmasq configuration if unset 2025-09-27 12:22:50 +05:00
Andrey Petelin
927b8a53b0 fix: restore default resolvfile in DNS settings if backup servers are missing to prevent resolution issues 2025-09-27 11:47:01 +05:00
itdoginfo
7ba20905d5 Fix sing-box remove 2025-09-26 13:21:53 +03:00
Andrey Petelin
5b15a56502 fix: Add local declaration for lowest variable and improve opkg status error redirection spacing 2025-09-25 11:43:03 +05:00
Andrey Petelin
c31df68bec refactor: Add version checks and service existence validation for required packages before starting podkop 2025-09-25 11:11:41 +05:00
Kirill Sobakin
0a5229f4f6 Merge pull request #173 from itdoginfo/fix
fix: Remove URL fragment before parsing VLESS links
2025-09-18 11:13:50 +03:00
Andrey Petelin
5ecb6ef997 fix: Remove URL fragment before parsing VLESS links 2025-09-18 12:59:17 +05:00
Kirill Sobakin
340c2b3505 Merge pull request #171 from itdoginfo/fix
Fix
2025-09-17 19:17:58 +03:00
Andrey Petelin
515c0be38b fix: revert changes from issue #148 2025-09-17 21:14:57 +05:00
Andrey Petelin
59c59bcb17 fix: Improve shadowsocks userinfo decoding with format validation and error handling` 2025-09-17 21:09:03 +05:00
14 changed files with 1505 additions and 2052 deletions

View File

@@ -164,7 +164,8 @@ sing_box() {
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
msg "sing-box version $sing_box_version is older than required $required_version"
msg "Removing old version..."
opkg remove sing-box
service podkop stop
opkg remove sing-box --force-depends
fi
}

View File

@@ -7,12 +7,12 @@
function createAdditionalSection(mainSection, network) {
let o = mainSection.tab('additional', _('Additional Settings'));
o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('<a href="http://openwrt.lan:9090/ui" target="_blank">openwrt.lan:9090/ui</a>'));
o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), '<a href="http://openwrt.lan:9090/ui" target="_blank">openwrt.lan:9090/ui</a>');
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('For issues with open connections sing-box'));
o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('Allows you to exclude NTP protocol traffic from the tunnel'));
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
@@ -60,38 +60,27 @@ function createAdditionalSection(mainSection, network) {
return true;
};
o = mainSection.taboption('additional', form.Flag, 'split_dns_enabled', _('Split DNS'), _('DNS for the list via proxy'));
o.default = '1';
o = mainSection.taboption('additional', form.Value, 'bootstrap_dns_server', _('Bootstrap DNS server'), _('The DNS server used to look up the IP address of an upstream DNS server'));
o.value('77.88.8.8', '77.88.8.8 (Yandex DNS)');
o.value('77.88.8.1', '77.88.8.1 (Yandex DNS)');
o.value('1.1.1.1', '1.1.1.1 (Cloudflare DNS)');
o.value('1.0.0.1', '1.0.0.1 (Cloudflare DNS)');
o.value('8.8.8.8', '8.8.8.8 (Google DNS)');
o.value('8.8.4.4', '8.8.4.4 (Google DNS)');
o.value('9.9.9.9', '9.9.9.9 (Quad9 DNS)');
o.value('9.9.9.11', '9.9.9.11 (Quad9 DNS)');
o.default = '77.88.8.8';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.ListValue, 'split_dns_type', _('Split DNS Protocol Type'), _('Select DNS protocol for split'));
o.value('doh', _('DNS over HTTPS (DoH)'));
o.value('dot', _('DNS over TLS (DoT)'));
o.value('udp', _('UDP (Unprotected DNS)'));
o.default = 'udp';
o.rmempty = false;
o.depends('split_dns_enabled', '1');
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Value, 'split_dns_server', _('Split DNS Server'), _('Select or enter DNS server address'));
Object.entries(constants.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '1.1.1.1';
o.rmempty = false;
o.depends('split_dns_enabled', '1');
o.ucisection = 'main';
o.validate = function (section_id, value) {
if (!value) {
return _('DNS server address cannot be empty');
}
const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
if (!ipRegex.test(value)) {
return _('Invalid DNS server format. Example: 8.8.8.8');
}
return true;
@@ -207,7 +196,6 @@ function createAdditionalSection(mainSection, network) {
o.rmempty = false;
o.ucisection = 'main';
// TODO(ampetelin): Can be moved to advanced settings in luci
// Extra IPs and exclusions (main section)
o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route'));
o.default = '0';

View File

@@ -37,7 +37,7 @@ function createConfigSection(section, map, network) {
o.depends('mode', 'proxy');
o.ucisection = s.section;
o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), _(''));
o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), '');
o.depends('proxy_config_type', 'url');
o.rows = 5;
o.rmempty = false;
@@ -240,6 +240,44 @@ function createConfigSection(section, map, network) {
return true;
};
o = s.taboption('basic', form.Flag, 'domain_resolver_enabled', _('Domain Resolver'), _('Enable built-in DNS resolver for domains handled by this section'));
o.default = '0';
o.rmempty = false;
o.depends('mode', 'vpn');
o.ucisection = s.section;
o = s.taboption('basic', form.ListValue, 'domain_resolver_dns_type', _('DNS Protocol Type'), _('Select the DNS protocol type for the domain resolver'));
o.value('doh', _('DNS over HTTPS (DoH)'));
o.value('dot', _('DNS over TLS (DoT)'));
o.value('udp', _('UDP (Unprotected DNS)'));
o.default = 'udp';
o.rmempty = false;
o.depends('domain_resolver_enabled', '1');
o.ucisection = s.section;
o = s.taboption('basic', form.Value, 'domain_resolver_dns_server', _('DNS Server'), _('Select or enter DNS server address'));
Object.entries(constants.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '8.8.8.8';
o.rmempty = false;
o.depends('domain_resolver_enabled', '1');
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value) {
return _('DNS server address cannot be empty');
}
const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
return true;
};
o = s.taboption('basic', form.Flag, 'community_lists_enabled', _('Community Lists'));
o.default = '0';
o.rmempty = false;

View File

@@ -62,12 +62,12 @@ const UPDATE_INTERVAL_OPTIONS = {
};
const DNS_SERVER_OPTIONS = {
'1.1.1.1': 'Cloudflare (1.1.1.1)',
'8.8.8.8': 'Google (8.8.8.8)',
'9.9.9.9': 'Quad9 (9.9.9.9)',
'dns.adguard-dns.com': 'AdGuard Default (dns.adguard-dns.com)',
'unfiltered.adguard-dns.com': 'AdGuard Unfiltered (unfiltered.adguard-dns.com)',
'family.adguard-dns.com': 'AdGuard Family (family.adguard-dns.com)'
'1.1.1.1': '1.1.1.1 (Cloudflare)',
'8.8.8.8': '8.8.8.8 (Google)',
'9.9.9.9': '9.9.9.9 (Quad9)',
'dns.adguard-dns.com': 'dns.adguard-dns.com (AdGuard Default)',
'unfiltered.adguard-dns.com': 'unfiltered.adguard-dns.com (AdGuard Unfiltered)',
'family.adguard-dns.com': 'family.adguard-dns.com (AdGuard Family)'
};
const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds

View File

@@ -37,7 +37,7 @@ return view.extend({
</style>
`);
const m = new form.Map('podkop', _(''), null, ['main', 'extra']);
const m = new form.Map('podkop', '', null, ['main', 'extra']);
// Main Section
const mainSection = m.section(form.TypedSection, 'main');

View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
PODIR="po"
POTFILE="$PODIR/templates/podkop.pot"
WIDTH=120
if [ $# -ne 1 ]; then
echo "Usage: $0 <language_code> (e.g., ru, de, fr)"
exit 1
fi
LANG="$1"
POFILE="$PODIR/$LANG/podkop.po"
if [ ! -f "$POTFILE" ]; then
echo "Template $POTFILE not found. Run xgettext first."
exit 1
fi
if [ -f "$POFILE" ]; then
echo "Updating $POFILE"
msgmerge --update --width="$WIDTH" --no-location "$POFILE" "$POTFILE"
else
echo "Creating new $POFILE using msginit"
mkdir -p "$PODIR/$LANG"
msginit --no-translator --no-location --locale="$LANG" --width="$WIDTH" --input="$POTFILE" --output-file="$POFILE"
fi
echo "Translation file for $LANG updated."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
#!/bin/bash
SRC_DIR="htdocs/luci-static/resources/view/podkop"
OUT_POT="po/templates/podkop.pot"
ENCODING="UTF-8"
WIDTH=120
mapfile -t FILES < <(find "$SRC_DIR" -type f -name "*.js")
if [ ${#FILES[@]} -eq 0 ]; then
echo "No JS files found in $SRC_DIR"
exit 1
fi
mkdir -p "$(dirname "$OUT_POT")"
echo "Generating POT template from JS files in $SRC_DIR"
xgettext --language=JavaScript \
--keyword=_ \
--from-code="$ENCODING" \
--output="$OUT_POT" \
--width="$WIDTH" \
--package-name="PODKOP" \
"${FILES[@]}"
echo "POT template generated: $OUT_POT"

View File

@@ -29,22 +29,65 @@ check_required_file "$PODKOP_LIB/logging.sh"
config_load "$PODKOP_CONFIG"
start_main() {
log "Starting podkop"
check_requirements() {
log "Check Requirements"
# checking
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
required_version="1.12.0"
local sing_box_version jq_version coreutils_base64_version
sing_box_version="$(sing-box version | head -n1 | awk '{print $3}')"
jq_version="$(jq --version | awk -F- '{print $2}')"
coreutils_base64_version="$(base64 --version | head -n1 | awk '{print $4}')"
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
log "The version of sing-box ($sing_box_version) is lower than the minimum version. Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box" "critical"
if [ -z "$sing_box_version" ]; then
log "Package 'sing-box' is not installed." "error"
exit 1
else
if ! is_min_package_version "$sing_box_version" "$SB_REQUIRED_VERSION"; then
log "Package 'sing-box' version ($sing_box_version) is lower than the required minimum ($SB_REQUIRED_VERSION). Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box" "error"
exit 1
fi
if ! service_exists "sing-box"; then
log "Service 'sing-box' is missing. Please install the official package to ensure the service is available." "error"
exit 1
fi
fi
if [ -z "$jq_version" ]; then
log "Package 'jq' is not installed." "error"
exit 1
elif ! is_min_package_version "$jq_version" "$JQ_REQUIRED_VERSION"; then
log "Package 'jq' version ($jq_version) is lower than the required minimum ($JQ_REQUIRED_VERSION)." "error"
exit 1
fi
if [ -z "$coreutils_base64_version" ]; then
log "Package 'coreutils-base64' is not installed." "error"
exit 1
elif ! is_min_package_version "$coreutils_base64_version" "$COREUTILS_BASE64_REQUIRED_VERSION"; then
log "Package 'coreutils-base64' version ($coreutils_base64_version) is lower than the required minimum ($COREUTILS_BASE64_REQUIRED_VERSION). This may cause issues when decoding base64 streams with missing padding, as automatic padding support is not available in older versions." "warn"
fi
if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
log "Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp" "warn"
fi
local proxy_string interface outbound_json urltest_proxy_links dont_touch_dhcp
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
config_get urltest_proxy_links "main" "urltest_proxy_links"
if [ -z "$proxy_string" ] && [ -z "$interface" ] && [ -z "$outbound_json" ] && [ -z "$urltest_proxy_links" ]; then
log "Required options (proxy_string, interface, outbound_json, urltest_proxy_links) are missing in 'main' section. Aborted." "error"
exit 1
fi
}
start_main() {
log "Starting podkop"
check_requirements
migration
config_foreach process_validate_service
@@ -82,17 +125,6 @@ start_main() {
}
start() {
local proxy_string interface outbound_json urltest_proxy_links dont_touch_dhcp
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
config_get urltest_proxy_links "main" "urltest_proxy_links"
if [ -z "$proxy_string" ] && [ -z "$interface" ] && [ -z "$outbound_json" ] && [ -z "$urltest_proxy_links" ]; then
log "Required options (proxy_string, interface, outbound_json, urltest_proxy_links) are missing in 'main' section. Aborted." "fatal"
exit 1
fi
start_main
config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
@@ -240,14 +272,8 @@ migration() {
validate_service() {
local service="$1"
for domain_service in $COMMUNITY_DOMAIN_SERVICES; do
if [ "$service" = "$domain_service" ]; then
return 0
fi
done
for subnet_service in $COMMUNITY_SUBNET_SERVICES; do
if [ "$service" = "$subnet_service" ]; then
for community_service in $COMMUNITY_SERVICES; do
if [ "$service" = "$community_service" ]; then
return 0
fi
done
@@ -406,11 +432,12 @@ dnsmasq_restore() {
return 0
fi
local cachesize noresolv backup_servers
local cachesize noresolv backup_servers resolvfile
log "Restoring cachesize" "debug"
cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_cachesize")"
if [ -z "$cachesize" ]; then
uci_remove "dhcp" "@dnsmasq[0]" "cachesize"
uci_set "dhcp" "@dnsmasq[0]" "cachesize" 150
else
uci_set "dhcp" "@dnsmasq[0]" "cachesize" "$cachesize"
uci_remove "dhcp" "@dnsmasq[0]" "podkop_cachesize"
@@ -420,6 +447,7 @@ dnsmasq_restore() {
noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_noresolv")"
if [ -z "$noresolv" ]; then
uci_remove "dhcp" "@dnsmasq[0]" "noresolv"
uci_set "dhcp" "@dnsmasq[0]" "noresolv" 0
else
uci_set "dhcp" "@dnsmasq[0]" "noresolv" "$noresolv"
uci_remove "dhcp" "@dnsmasq[0]" "podkop_noresolv"
@@ -427,12 +455,18 @@ dnsmasq_restore() {
log "Restoring DNS servers" "debug"
uci_remove "dhcp" "@dnsmasq[0]" "server"
resolvfile="/tmp/resolv.conf.d/resolv.conf.auto"
backup_servers="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_server")"
if [ -n "$backup_servers" ]; then
for server in $backup_servers; do
uci_add_list "dhcp" "@dnsmasq[0]" "server" "$server"
done
uci_remove "dhcp" "@dnsmasq[0]" "podkop_server"
elif file_exists "$resolvfile"; then
log "Backup DNS servers not found, using default resolvfile" "debug"
uci_set "dhcp" "@dnsmasq[0]" "resolvfile" "$resolvfile"
else
log "Backup DNS servers and default resolvfile not found, possible resolving issues" "warn"
fi
uci_commit "dhcp"
@@ -542,16 +576,6 @@ list_update() {
fi
}
find_working_resolver() {
for resolver in $DNS_RESOLVERS; do
if nslookup -timeout=2 $FAKEIP_TEST_DOMAIN $resolver > /dev/null 2>&1; then
echo "$resolver"
return 0
fi
done
return 1
}
# sing-box funcs
sing_box_uci() {
@@ -675,7 +699,7 @@ configure_outbound_handler() {
else
outbound_tags="$outbound_tags,$outbound_tag"
fi
i=$((i+1))
i=$((i + 1))
done
urltest_tag="$(get_outbound_tag_by_section "$section-urltest")"
@@ -693,15 +717,32 @@ configure_outbound_handler() {
;;
vpn)
log "Configuring outbound in VPN connection mode for the $section section"
local interface_name
local interface_name domain_resolver_enabled domain_resolver_dns_type domain_resolver_dns_server \
outbound_tag domain_resolver_tag dns_domain_resolver
config_get interface_name "$section" "interface"
config_get domain_resolver_enabled "$section" "domain_resolver_enabled"
config_get domain_resolver_dns_type "$section" "domain_resolver_dns_type"
config_get domain_resolver_dns_server "$section" "domain_resolver_dns_server"
if [ -z "$interface_name" ]; then
log "VPN interface is not set. Aborted." "fatal"
exit 1
fi
config=$(sing_box_cf_add_interface_outbound "$config" "$section" "$interface_name")
local outbound_tag
outbound_tag="$(get_outbound_tag_by_section "$section")"
if [ "$domain_resolver_enabled" -eq 1 ]; then
if ! is_ipv4 "$domain_resolver_dns_server"; then
dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG
fi
domain_resolver_tag="$(get_domain_resolver_tag "$section")"
config=$(sing_box_cf_add_dns_server "$config" "$domain_resolver_dns_type" "$domain_resolver_tag" \
"$domain_resolver_dns_server" "$dns_domain_resolver" "$outbound_tag")
fi
config=$(sing_box_cm_add_interface_outbound "$config" "$outbound_tag" "$interface_name" "$domain_resolver_tag")
;;
block)
log "Connection mode 'block' detected for the $section section no outbound will be created (handled via reject route rules)"
@@ -715,53 +756,21 @@ configure_outbound_handler() {
sing_box_configure_dns() {
log "Configure the DNS section of a sing-box JSON configuration"
local split_dns_enabled final_dns_server
config_get_bool split_dns_enabled "main" "split_dns_enabled" 0
if [ "$split_dns_enabled" -eq 1 ]; then
final_dns_server="$SB_SPLIT_DNS_SERVER_TAG"
else
final_dns_server="$SB_DNS_SERVER_TAG"
fi
config=$(sing_box_cm_configure_dns "$config" "$final_dns_server" "ipv4_only" true)
config=$(sing_box_cm_configure_dns "$config" "$SB_DNS_SERVER_TAG" "ipv4_only" true)
local dns_type dns_server split_dns_type split_dns_server dns_server_address split_dns_server_address
log "Adding DNS Servers" "debug"
local dns_type dns_server bootstrap_dns_server dns_domain_resolver
config_get dns_type "main" "dns_type" "doh"
config_get dns_server "main" "dns_server" "1.1.1.1"
config_get split_dns_type "main" "split_dns_type" "udp"
config_get split_dns_server "main" "split_dns_server" "1.1.1.1"
dns_server_address=$(url_get_host "$dns_server")
split_dns_server_address=$(url_get_host "$split_dns_server")
config_get bootstrap_dns_server "main" "bootstrap_dns_server" "77.88.8.8"
local need_dns_domain_resolver=0
if ! is_ipv4 "$dns_server_address" || ! is_ipv4 "$split_dns_server_address"; then
need_dns_domain_resolver=1
fi
log "Adding DNS Servers"
config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE")
local dns_domain_resolver
if [ "$need_dns_domain_resolver" -eq 1 ]; then
log "One of the DNS server addresses is a domain. Searching for a working DNS server..."
dns_domain_resolver=$(find_working_resolver)
if [ -z "$dns_domain_resolver" ]; then
log "Working DNS server not found, using default DNS server"
dns_domain_resolver="1.1.1.1"
else
log "Working DNS server has been found: $dns_domain_resolver"
fi
config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_DNS_DOMAIN_RESOLVER_TAG" "$dns_domain_resolver" 53)
dns_domain_resolver="$SB_DNS_DOMAIN_RESOLVER_TAG"
if ! is_ipv4 "$dns_server"; then
dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG
fi
config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_BOOTSTRAP_SERVER_TAG" "$bootstrap_dns_server" 53)
config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver")
if [ "$split_dns_enabled" -eq 1 ]; then
config=$(
sing_box_cf_add_dns_server "$config" "$split_dns_type" "$SB_SPLIT_DNS_SERVER_TAG" "$split_dns_server" \
"$dns_domain_resolver" "$SB_MAIN_OUTBOUND_TAG"
)
fi
config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE")
log "Adding DNS Rules"
local rewrite_ttl service_domains
@@ -773,11 +782,6 @@ sing_box_configure_dns() {
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rewrite_ttl" "$rewrite_ttl")
service_domains=$(comma_string_to_json_array "$FAKEIP_TEST_DOMAIN,$CHECK_PROXY_IP_DOMAIN")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
if [ "$split_dns_enabled" -eq 1 ]; then
config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_DNS_SERVER_TAG" "$SB_INVERT_FAKEIP_DNS_RULE_TAG")
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "invert" true)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "domain" "$service_domains")
fi
}
sing_box_configure_route() {
@@ -956,7 +960,9 @@ prepare_common_ruleset() {
config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
case "$type" in
domains) _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" ;;
domains)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets) ;;
*) log "Unsupported remote rule set type: $type" "warn" ;;
esac
@@ -977,13 +983,7 @@ configure_community_list_handler() {
config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
for service in $COMMUNITY_DOMAIN_SERVICES; do
if [ "$tag" = "$service" ]; then
_add_ruleset_to_dns_rules "$ruleset_tag"
break
fi
done
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
}
configure_user_domain_or_subnets_list() {
@@ -1042,7 +1042,7 @@ configure_local_domain_or_subnet_lists() {
domains)
config_list_foreach "$section" "local_domain_lists" import_local_domain_or_subnet_list "$type" \
"$section" "$ruleset_filepath"
_add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag"
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets)
config_list_foreach "$section" "local_subnet_lists" import_local_domain_or_subnet_list "$type" \
@@ -1102,7 +1102,9 @@ configure_remote_domain_or_subnet_list_handler() {
config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
case "$type" in
domains) _add_ruleset_to_dns_rules "$ruleset_tag" "$route_rule_tag" ;;
domains)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets) ;;
*) log "Unsupported remote rule set type: $type" "warn" ;;
esac
@@ -1113,17 +1115,6 @@ configure_remote_domain_or_subnet_list_handler() {
esac
}
_add_ruleset_to_dns_rules() {
local ruleset_tag="$1"
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
local split_dns_enabled final_dns_server
config_get_bool split_dns_enabled "main" "split_dns_enabled" 0
if [ "$split_dns_enabled" -eq 1 ]; then
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_INVERT_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
fi
}
sing_box_configure_experimental() {
log "Configure the experimental section of a sing-box JSON configuration"
@@ -1962,6 +1953,16 @@ print_global() {
echo "$message"
}
find_working_resolver() {
for resolver in $DNS_RESOLVERS; do
if nslookup -timeout=2 "$FAKEIP_TEST_DOMAIN" "$resolver" > /dev/null 2>&1; then
echo "$resolver"
return 0
fi
done
return 1
}
global_check() {
print_global "📡 Global check run!"
print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -9,6 +9,8 @@ FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi"
TMP_SING_BOX_FOLDER="/tmp/sing-box"
TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets"
CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker
JQ_REQUIRED_VERSION="1.7.1"
COREUTILS_BASE64_REQUIRED_VERSION="9.7"
## nft
NFT_TABLE_NAME="PodkopTable"
@@ -18,14 +20,14 @@ NFT_DISCORD_SET_NAME="podkop_discord_subnets"
NFT_INTERFACE_SET_NAME="interfaces"
## sing-box
SB_REQUIRED_VERSION="1.12.0"
# Log
SB_DEFAULT_LOG_LEVEL="warn"
# DNS
SB_DNS_SERVER_TAG="dns-server"
SB_SPLIT_DNS_SERVER_TAG="split-dns-server"
SB_FAKEIP_DNS_SERVER_TAG="fakeip-server"
SB_FAKEIP_INET4_RANGE="198.18.0.0/15"
SB_DNS_DOMAIN_RESOLVER_TAG="dns-domain-resolver"
SB_BOOTSTRAP_SERVER_TAG="bootstrap-dns-server"
SB_FAKEIP_DNS_RULE_TAG="fakeip-dns-rule-tag"
SB_INVERT_FAKEIP_DNS_RULE_TAG="invert-fakeip-dns-rule-tag"
# Inbounds
@@ -63,5 +65,4 @@ SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst"
SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst"
SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst"
SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst"
COMMUNITY_DOMAIN_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca"
COMMUNITY_SUBNET_SERVICES="discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram"
COMMUNITY_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram"

View File

@@ -40,6 +40,25 @@ is_base64() {
return 1
}
# Checks if the given string looks like a Shadowsocks userinfo
is_shadowsocks_userinfo_format() {
local str="$1"
local regex='^[^:]+:[^:]+(:[^:]+)?$'
[[ "$str" =~ $regex ]]
}
# Compares the current package version with the required minimum
is_min_package_version() {
local current="$1"
local required="$2"
local lowest
lowest="$(printf '%s\n' "$current" "$required" | sort -V | head -n1)"
[ "$lowest" = "$required" ]
}
# Checks if the given file exists
file_exists() {
local filepath="$1"
@@ -51,6 +70,17 @@ file_exists() {
fi
}
# Checks if a service script exists in /etc/init.d
service_exists() {
local service="$1"
if [ -x "/etc/init.d/$service" ]; then
return 0
else
return 1
fi
}
# Returns the inbound tag name by appending the postfix to the given section
get_inbound_tag_by_section() {
local section="$1"
@@ -67,6 +97,14 @@ get_outbound_tag_by_section() {
echo "$section-$postfix"
}
# Constructs and returns a domain resolver tag by appending a fixed postfix to the given section
get_domain_resolver_tag() {
local section="$1"
local postfix="domain-resolver"
echo "$section-$postfix"
}
# Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix
get_ruleset_tag() {
local section="$1"
@@ -176,6 +214,13 @@ url_get_file_extension() {
esac
}
# Remove url fragment (everything after the first '#')
url_strip_fragment() {
local url="$1"
echo "${url%%#*}"
}
# Decodes and returns a base64-encoded string
base64_decode() {
local str="$1"

View File

@@ -62,6 +62,7 @@ sing_box_cf_add_proxy_outbound() {
local udp_over_tcp="$4"
url=$(url_decode "$url")
url=$(url_strip_fragment "$url")
local scheme="${url%%://*}"
case "$scheme" in
@@ -131,8 +132,12 @@ sing_box_cf_add_proxy_outbound() {
local userinfo tag host port method password udp_over_tcp
userinfo=$(url_get_userinfo "$url")
if is_base64 "$userinfo"; then
if ! is_shadowsocks_userinfo_format "$userinfo"; then
userinfo=$(base64_decode "$userinfo")
if [ $? -ne 0 ]; then
log "Cannot decode shadowsocks userinfo or it does not match the expected format. Aborted." "fatal"
exit 1
fi
fi
tag=$(get_outbound_tag_by_section "$section")

View File

@@ -788,6 +788,7 @@ sing_box_cm_set_vless_tls() {
# config: JSON configuration (string)
# tag: string, identifier for the outbound
# interface: string, network interface to bind the outbound
# domain_resolver: string, tag of the domain resolver to be used for this outbound (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -797,15 +798,20 @@ sing_box_cm_add_interface_outbound() {
local config="$1"
local tag="$2"
local interface="$3"
local domain_resolver="$4"
echo "$config" | jq \
--arg tag "$tag" \
--arg interface "$interface" \
'.outbounds += [{
type: "direct",
tag: $tag,
bind_interface: $interface
}]'
--arg domain_resolver "$domain_resolver" \
'.outbounds += [
{
type: "direct",
tag: $tag,
bind_interface: $interface
}
+ (if $domain_resolver != "" then {domain_resolver: $domain_resolver} else {} end)
]'
}
#######################################