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 e49d5dc..ae89987 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 @@ -4,6 +4,7 @@ 'require ui'; 'require network'; 'require fs'; +'require uci'; const STATUS_COLORS = { SUCCESS: '#4caf50', @@ -90,20 +91,29 @@ function createConfigSection(section, map, network) { if (cfgvalue) { try { - // Extract only the active configuration (first non-comment line) const activeConfig = cfgvalue.split('\n') .map(line => line.trim()) .find(line => line && !line.startsWith('//')); if (activeConfig) { - const label = activeConfig.split('#').pop() || 'unnamed'; - const decodedLabel = decodeURIComponent(label); - const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel); - container.appendChild(descDiv); + if (activeConfig.includes('#')) { + const label = activeConfig.split('#').pop(); + if (label && label.trim()) { + const decodedLabel = decodeURIComponent(label); + const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel); + container.appendChild(descDiv); + } else { + const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); + container.appendChild(descDiv); + } + } else { + const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); + container.appendChild(descDiv); + } } } catch (e) { console.error('Error parsing config label:', e); - const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + (cfgvalue.split('#').pop() || 'unnamed')); + const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); container.appendChild(descDiv); } } else { @@ -121,7 +131,6 @@ function createConfigSection(section, map, network) { } try { - // Get the first non-comment line as the active configuration const activeConfig = value.split('\n') .map(line => line.trim()) .find(line => line && !line.startsWith('//')); @@ -663,9 +672,8 @@ const createStatusPanel = (title, status, buttons) => { }; // Update the status section creation -let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus, fakeipCLIStatus) { +let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus, fakeipCLIStatus, dnsStatus, bypassStatus, configName) { return E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Service Status')), E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [ // Podkop Status Panel createStatusPanel('Podkop Status', podkopStatus, [ @@ -696,6 +704,11 @@ let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, s label: 'View Logs', command: 'check_logs', title: 'Podkop Logs' + }), + ButtonFactory.createModalButton({ + label: _('Update Lists'), + command: 'list_update', + title: _('Lists Update Results') }) ]), @@ -715,6 +728,16 @@ let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, s label: 'Check Connections', command: 'check_sing_box_connections', title: 'Active Connections' + }), + ButtonFactory.createModalButton({ + label: _('Check NFT Rules'), + command: 'check_nft', + title: _('NFT Rules') + }), + ButtonFactory.createModalButton({ + label: _('Check DNSMasq'), + command: 'check_dnsmasq', + title: _('DNSMasq Configuration') }) ]), @@ -736,26 +759,34 @@ let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, s ]) ]) ]), - ButtonFactory.createModalButton({ - label: _('Check NFT Rules'), - command: 'check_nft', - title: _('NFT Rules') - }), - ButtonFactory.createModalButton({ - label: _('Check DNSMasq'), - command: 'check_dnsmasq', - title: _('DNSMasq Configuration') - }), - ButtonFactory.createModalButton({ - label: _('Update Lists'), - command: 'list_update', - title: _('Lists Update Results') - }), - ButtonFactory.createModalButton({ - label: _('Check Router FakeIP'), - command: 'check_fakeip', - title: _('FakeIP Router Check') - }) + E('div', { style: 'margin-bottom: 10px;' }, [ + E('div', { style: 'margin-bottom: 5px;' }, [ + E('strong', {}, _('DNS Status')), + E('br'), + E('span', { style: `color: ${dnsStatus.remote.color}` }, [ + dnsStatus.remote.state === 'available' ? '✔' : dnsStatus.remote.state === 'unavailable' ? '✘' : '!', + ' ', + dnsStatus.remote.message + ]), + E('br'), + E('span', { style: `color: ${dnsStatus.local.color}` }, [ + dnsStatus.local.state === 'available' ? '✔' : dnsStatus.local.state === 'unavailable' ? '✘' : '!', + ' ', + dnsStatus.local.message + ]) + ]) + ]), + E('div', { style: 'margin-bottom: 10px;' }, [ + E('div', { style: 'margin-bottom: 5px;' }, [ + E('strong', {}, configName), + E('br'), + E('span', { style: `color: ${bypassStatus.color}` }, [ + bypassStatus.state === 'working' ? '✔' : bypassStatus.state === 'not_working' ? '✘' : '!', + ' ', + bypassStatus.message + ]) + ]) + ]) ]), // Version Information Panel @@ -772,6 +803,39 @@ let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, s ]); }; +function checkDNSAvailability() { + const createStatus = (state, message, color) => ({ + state, + message: _(message), + color: STATUS_COLORS[color] + }); + + return new Promise(async (resolve) => { + try { + const dnsStatusResult = await safeExec('/usr/bin/podkop', ['check_dns_available']); + const dnsStatus = JSON.parse(dnsStatusResult.stdout || '{"dns_type":"unknown","dns_server":"unknown","is_available":0,"status":"unknown","local_dns_working":0,"local_dns_status":"unknown"}'); + + const remoteStatus = dnsStatus.is_available ? + createStatus('available', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, 'SUCCESS') : + createStatus('unavailable', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, 'ERROR'); + + const localStatus = dnsStatus.local_dns_working ? + createStatus('available', 'Router DNS working', 'SUCCESS') : + createStatus('unavailable', 'Router DNS not working', 'ERROR'); + + return resolve({ + remote: remoteStatus, + local: localStatus + }); + } catch (error) { + return resolve({ + remote: createStatus('error', 'DNS check error', 'WARNING'), + local: createStatus('error', 'DNS check error', 'WARNING') + }); + } + }); +} + return view.extend({ async render() { document.head.insertAdjacentHTML('beforeend', ` @@ -1071,6 +1135,82 @@ return view.extend({ }); } + function checkBypass() { + const createStatus = (state, message, color) => ({ + state, + message: _(message), + color: STATUS_COLORS[color] + }); + + return new Promise(async (resolve) => { + try { + let configMode = 'proxy'; // Default fallback + try { + const formData = document.querySelector('form.map-podkop'); + if (formData) { + const modeSelect = formData.querySelector('select[name="cbid.podkop.main.mode"]'); + if (modeSelect && modeSelect.value) { + configMode = modeSelect.value; + } + } + } catch (formError) { + console.error('Error getting mode from form:', formError); + } + + // Check if sing-box is running + 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) { + return resolve(createStatus('not_working', `${configMode} not running`, 'ERROR')); + } + + // Fetch IP from first endpoint + let ip1 = null; + try { + const controller1 = new AbortController(); + const timeoutId1 = setTimeout(() => controller1.abort(), 10000); + + const response1 = await fetch('https://fakeip.tech-domain.club/check', { signal: controller1.signal }); + const data1 = await response1.json(); + clearTimeout(timeoutId1); + + ip1 = data1.IP; + } catch (error) { + return resolve(createStatus('error', 'First endpoint check failed', 'WARNING')); + } + + // Fetch IP from second endpoint + let ip2 = null; + try { + const controller2 = new AbortController(); + const timeoutId2 = setTimeout(() => controller2.abort(), 10000); + + const response2 = await fetch('https://ip.tech-domain.club/check', { signal: controller2.signal }); + const data2 = await response2.json(); + clearTimeout(timeoutId2); + + ip2 = data2.IP; + } catch (error) { + return resolve(createStatus('not_working', `${configMode} not working`, 'ERROR')); + } + + // Compare IPs + if (ip1 && ip2) { + if (ip1 !== ip2) { + return resolve(createStatus('working', `${configMode} working correctly`, 'SUCCESS')); + } else { + return resolve(createStatus('not_working', `${configMode} routing incorrect`, 'ERROR')); + } + } else { + return resolve(createStatus('error', 'IP comparison failed', 'WARNING')); + } + } catch (error) { + return resolve(createStatus('error', 'Bypass check error', 'WARNING')); + } + }); + } + async function updateDiagnostics() { try { const [ @@ -1081,7 +1221,9 @@ return view.extend({ singbox, system, fakeipStatus, - fakeipCLIStatus + fakeipCLIStatus, + dnsStatus, + bypassStatus ] = await Promise.all([ safeExec('/usr/bin/podkop', ['get_status']), safeExec('/usr/bin/podkop', ['get_sing_box_status']), @@ -1090,7 +1232,9 @@ return view.extend({ safeExec('/usr/bin/podkop', ['show_sing_box_version']), safeExec('/usr/bin/podkop', ['show_system_info']), checkFakeIP(), - checkFakeIPCLI() + checkFakeIPCLI(), + checkDNSAvailability(), + checkBypass() ]); const parsedPodkopStatus = JSON.parse(podkopStatus.stdout || '{"running":0,"enabled":0,"status":"unknown"}'); @@ -1099,7 +1243,35 @@ return view.extend({ const container = document.getElementById('diagnostics-status'); if (!container) return; - const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system, fakeipStatus, fakeipCLIStatus); + let configName = _('Main config'); + try { + const data = await uci.load('podkop'); + const proxyString = uci.get('podkop', 'main', 'proxy_string'); + + if (proxyString) { + const activeConfig = proxyString.split('\n') + .map(line => line.trim()) + .find(line => line && !line.startsWith('//')); + + if (activeConfig) { + if (activeConfig.includes('#')) { + const label = activeConfig.split('#').pop(); + if (label && label.trim()) { + configName = _('Config: ') + decodeURIComponent(label); + } else { + configName = _('Main config'); + } + } else { + configName = _('Main config'); + } + } + } + } catch (e) { + console.error('Error getting config name from UCI:', e); + } + + // Create a modified statusSection function with the configName + const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system, fakeipStatus, fakeipCLIStatus, dnsStatus, bypassStatus, configName); container.innerHTML = ''; container.appendChild(statusSection); @@ -1118,6 +1290,22 @@ return view.extend({ fakeipCLIStatus.message ]).outerHTML; } + + const dnsRemoteElement = document.getElementById('dns-remote-status'); + if (dnsRemoteElement) { + dnsRemoteElement.innerHTML = E('span', { 'style': `color: ${dnsStatus.remote.color}` }, [ + dnsStatus.remote.state === 'available' ? '✔ ' : dnsStatus.remote.state === 'unavailable' ? '✘ ' : '! ', + dnsStatus.remote.message + ]).outerHTML; + } + + const dnsLocalElement = document.getElementById('dns-local-status'); + if (dnsLocalElement) { + dnsLocalElement.innerHTML = E('span', { 'style': `color: ${dnsStatus.local.color}` }, [ + dnsStatus.local.state === 'available' ? '✔ ' : dnsStatus.local.state === 'unavailable' ? '✘ ' : '! ', + dnsStatus.local.message + ]).outerHTML; + } } catch (e) { const container = document.getElementById('diagnostics-status'); if (container) { @@ -1142,6 +1330,15 @@ return view.extend({ const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop')); node.insertBefore(titleDiv, node.firstChild); + document.addEventListener('visibilitychange', function () { + const diagnosticsContainer = document.getElementById('diagnostics-status'); + if (document.hidden) { + stopDiagnosticsUpdates(); + } else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) { + startDiagnosticsUpdates(); + } + }); + setTimeout(() => { const diagnosticsContainer = document.getElementById('diagnostics-status'); if (diagnosticsContainer) { diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index ccb4e46..66448da 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -40,8 +40,8 @@ msgstr "Конфигурация Outbound" msgid "Proxy Configuration URL" msgstr "URL конфигурации прокси" -msgid "Enter connection string starting with vless:// or ss:// for proxy configuration" -msgstr "Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси" +msgid "Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for saving other configs" +msgstr "Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для сохранения других конфигураций" msgid "Outbound Configuration" msgstr "Конфигурация исходящего соединения" @@ -749,4 +749,67 @@ msgid "not works on router" msgstr "не работает на роутере" msgid "Diagnostics" -msgstr "Диагностика" \ No newline at end of file +msgstr "Диагностика" + +msgid "DNS Status" +msgstr "Статус DNS" + +msgid "Bypass Status" +msgstr "Статус обхода" + +msgid "proxy working correctly" +msgstr "прокси работает корректно" + +msgid "vpn working correctly" +msgstr "vpn работает корректно" + +msgid "proxy not working" +msgstr "прокси не работает" + +msgid "vpn not working" +msgstr "vpn не работает" + +msgid "proxy not running" +msgstr "прокси не запущен" + +msgid "vpn not running" +msgstr "vpn не запущен" + +msgid "proxy routing incorrect" +msgstr "маршрутизация прокси некорректна" + +msgid "vpn routing incorrect" +msgstr "маршрутизация vpn некорректна" + +msgid "First endpoint check failed" +msgstr "Проверка первой конечной точки не удалась" + +msgid "IP comparison failed" +msgstr "Сравнение IP-адресов не удалось" + +msgid "Bypass check error" +msgstr "Ошибка проверки обхода" + +msgid "Main config" +msgstr "Основная конфигурация" + +msgid "Config without description" +msgstr "Конфигурация без описания" + +msgid "DNS working" +msgstr "DNS работает" + +msgid "Router DNS working" +msgstr "DNS роутера работает" + +msgid "Router DNS not working" +msgstr "DNS роутера не работает" + +msgid "DNS check error" +msgstr "Ошибка проверки DNS" + +msgid "available" +msgstr "доступен" + +msgid "unavailable" +msgstr "недоступен" \ No newline at end of file diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot index 3f99179..c017151 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -1103,4 +1103,70 @@ msgid "not works on router" msgstr "" msgid "Diagnostics" +msgstr "" + +msgid "DNS Status" +msgstr "" + +msgid "Bypass Status" +msgstr "" + +msgid "proxy working correctly" +msgstr "" + +msgid "vpn working correctly" +msgstr "" + +msgid "proxy not working" +msgstr "" + +msgid "vpn not working" +msgstr "" + +msgid "proxy not running" +msgstr "" + +msgid "vpn not running" +msgstr "" + +msgid "proxy routing incorrect" +msgstr "" + +msgid "vpn routing incorrect" +msgstr "" + +msgid "First endpoint check failed" +msgstr "" + +msgid "IP comparison failed" +msgstr "" + +msgid "Bypass check error" +msgstr "" + +msgid "Main config" +msgstr "" + +msgid "Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs" +msgstr "" + +msgid "Config without description" +msgstr "" + +msgid "DNS working" +msgstr "" + +msgid "Router DNS working" +msgstr "" + +msgid "Router DNS not working" +msgstr "" + +msgid "DNS check error" +msgstr "" + +msgid "available" +msgstr "" + +msgid "unavailable" msgstr "" \ No newline at end of file diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 51e57b6..abeb9bc 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -63,6 +63,7 @@ start() { sing_box_dns sing_box_dns_rule_fakeip sing_box_rule_dns + sing_box_create_bypass_ruleset sing_box_add_secure_dns_probe_domain sing_box_cache_file process_socks5 @@ -726,6 +727,42 @@ sing_box_dns() { }' $SING_BOX_CONFIG > /tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG } +sing_box_create_bypass_ruleset() { + log "Creating bypass ruleset for direct access" + + jq ' + .route.rule_set += [{ + "tag": "bypass", + "type": "inline", + "rules": [ + { + "domain_suffix": [ + "ip.tech-domain.club" + ] + } + ] + }]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + + # Add a rule to route bypass domains to direct-out outbound + jq ' + .route.rules += [{ + "inbound": ["tproxy-in"], + "rule_set": ["bypass"], + "outbound": "main", + "action": "route" + }]' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG + + # Make sure the bypass ruleset is in the fakeip DNS rule + jq ' + .dns.rules = (.dns.rules | map( + if .server == "fakeip-server" then + .rule_set += ["bypass"] + else + . + end + ))' $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" @@ -1985,6 +2022,57 @@ get_status() { echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\"}" } +check_dns_available() { + local dns_type=$(uci get podkop.main.dns_type 2>/dev/null) + local dns_server=$(uci get podkop.main.dns_server 2>/dev/null) + local is_available=0 + local status="unavailable" + local local_dns_working=0 + local local_dns_status="unavailable" + + if [ "$dns_type" = "doh" ]; then + # Different DoH providers use different endpoints and formats + local result="" + + # Try common DoH endpoints and check for valid responses + # First try /dns-query endpoint (Cloudflare, AdGuard DNS, etc.) + result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/dns-query?name=itdog.info&type=A") + if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then + is_available=1 + status="available" + else + # If that fails, try /resolve endpoint (Google DNS) + result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/resolve?name=itdog.info&type=A") + if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then + is_available=1 + status="available" + fi + fi + elif [ "$dns_type" = "dot" ]; then + nc $dns_server 853 /dev/null 2>&1 & pid=$! + (sleep 3; kill $pid 2>/dev/null) & killpid=$! + wait $pid >/dev/null 2>&1 + if [ $? -eq 0 ]; then + is_available=1 + status="available" + fi + kill $killpid 2>/dev/null + elif [ "$dns_type" = "udp" ]; then + if nslookup -timeout=2 itdog.info $dns_server >/dev/null 2>&1; then + is_available=1 + status="available" + fi + fi + + # Check if local DNS resolver is working + if nslookup -timeout=2 $TEST_DOMAIN 127.0.0.1 >/dev/null 2>&1; then + local_dns_working=1 + local_dns_status="available" + fi + + echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$dns_server\",\"is_available\":$is_available,\"status\":\"$status\",\"local_dns_working\":$local_dns_working,\"local_dns_status\":\"$local_dns_status\"}" +} + sing_box_add_secure_dns_probe_domain() { local domain="$TEST_DOMAIN" local override_port=8443 @@ -2079,8 +2167,11 @@ case "$1" in get_sing_box_status) get_sing_box_status ;; + check_dns_available) + check_dns_available + ;; *) - 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_fakeip|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}" + 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_fakeip|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|check_dns_available}" exit 1 ;; esac \ No newline at end of file