Compare commits

...

51 Commits

Author SHA1 Message Date
itdoginfo
3cb4be6427 v0.3.33 2025-04-03 16:47:47 +03:00
itdoginfo
25bfdce5ce Added critical log. Rm friendlywrt check. Added iptables check 2025-04-03 16:47:26 +03:00
itdoginfo
6d0f097a07 Merge pull request #75 from itdoginfo/feature/error-notification (#47)
Feature/error notification
2025-04-03 14:52:00 +03:00
Ivan K
5f780955eb 💄 style(podkop): update error log filtering criteria 2025-04-03 13:42:55 +03:00
Ivan K
389def9056 ♻️ refactor(podkop): remove unused createErrorModal function 2025-04-03 13:40:41 +03:00
Ivan K
e816da5133 feat(podkop): add error logging and notification system 2025-04-03 13:36:22 +03:00
Ivan K
e57adbe042 🔒 refactor(config): Mask NextDNS server address in config output 2025-03-30 20:36:49 +03:00
itdoginfo
d78c51360d Merge pull request #73 from itdoginfo/feature/no-more-cache
🐛 fix(doh): Improve DoH server compatibility detection for quad9
2025-03-30 18:44:26 +03:00
Ivan K
c2357337fc 🐛 fix(dns): improve DoH server compatibility and error handling 2025-03-30 17:20:06 +03:00
Ivan K
bc6490b56e 🐛 fix(doh): Improve DoH server compatibility detection for quad9 2025-03-30 17:04:30 +03:00
itdoginfo
2f645d9151 v0.3.32 2025-03-30 16:03:49 +03:00
itdoginfo
94cc65001b Merge pull request #72 from itdoginfo/feature/no-more-cache
🐛 fix(podkop): Handle DNS check errors and timeouts properly
2025-03-30 16:02:44 +03:00
Ivan K
87caa70e97 feat(dns): Mask NextDNS ID in DNS availability check output 2025-03-30 14:53:01 +03:00
Ivan K
90d7c60fcb 🐛 fix(podkop): Handle DNS check errors and timeouts properly 2025-03-30 14:46:03 +03:00
itdoginfo
3f114b4710 v0.3.31 2025-03-30 12:41:40 +03:00
itdoginfo
b821abe82c Merge pull request #67 from itdoginfo/feature/no-more-cache
Feature/add DNS and bypass status checks to diagnostics
2025-03-30 12:37:58 +03:00
Ivan K
732cab2ef3 🐛 fix(podkop): fix typo in translation and dns check timeout 2025-03-30 09:54:31 +03:00
Ivan K
3b4ce9e7a3 feat: add visibility change event listener for diagnostics updates 2025-03-21 15:06:08 +03:00
Ivan K
69c4445c85 refactor: move buttons for NFT and DNSMasq checks 2025-03-21 14:54:52 +03:00
Ivan K
dcebc3d67d docs: update Russian translations for proxy configuration and add new translation strings 2025-03-21 14:53:24 +03:00
Ivan K
1be31eaf59 feat: add local DNS availability check and display in UI 2025-03-21 14:47:20 +03:00
Ivan K
023210e0f0 style: update text for Bypass Status to Main config 2025-03-21 14:21:35 +03:00
Ivan K
5ff832533e feat: add DNS and bypass status checks to diagnostics 2025-03-21 13:03:29 +03:00
Ivan K
5d2163515e refactor: improve caching prevention logic 2025-03-20 21:47:55 +03:00
Ivan K
5865706d0c feat: add timestamp to URL to prevent caching 2025-03-20 21:45:08 +03:00
itdoginfo
aabe1c53dc Update 2025-03-18 00:32:06 +03:00
itdoginfo
8e91b582ad #36 2025-03-18 00:31:56 +03:00
itdoginfo
62ce1f5acc v0.3.30 2025-03-17 14:44:07 +03:00
itdoginfo
93727ddeb5 Processing empty values 2025-03-17 14:43:33 +03:00
itdoginfo
98797d93b1 v0.3.29 2025-03-17 13:16:38 +03:00
itdoginfo
66c6e998a2 #38 #46 2025-03-17 13:14:37 +03:00
itdoginfo
3d9f82b571 Merge pull request #65 from itdoginfo/chore/fakeip-method
feat: add diagnostics functionality only in tab
2025-03-14 12:33:52 +03:00
Ivan K
38d082e236 feat: add diagnostics functionality only in tab 2025-03-14 09:50:28 +03:00
itdoginfo
9f5abcae6d v0.3.28 2025-03-13 19:34:52 +03:00
itdoginfo
7836d2c6ec Fix 2025-03-13 19:32:57 +03:00
itdoginfo
f46c934c59 Test 2025-03-13 19:30:17 +03:00
itdoginfo
23ed10d393 Added check version in Makefile 2025-03-13 19:28:31 +03:00
itdoginfo
26488baad3 Merge pull request #64 from itdoginfo/chore/fakeip-method
fix: fix enable/disable functionality to podkop service
2025-03-13 19:05:49 +03:00
Ivan K
c79016e456 feat: add createInitActionButton function to ButtonFactory 2025-03-13 10:35:48 +03:00
Ivan K
884bbfee42 fix: remove unused button creation code 2025-03-13 10:32:34 +03:00
Ivan K
1263b9b1b8 fix: fix enable/disable functionality to podkop service 2025-03-13 10:00:37 +03:00
Ivan K
23203fd7a1 feat: add createSystemButton function to ButtonFactory and flush cache button 2025-03-13 00:10:18 +03:00
itdoginfo
25c887a952 v0.3.27 2025-03-12 17:20:35 +03:00
itdoginfo
e7a3c7adf1 Merge pull request #63 from itdoginfo/chore/fakeip-method
feat: update DNS checks and improve FakeIP status reporting
2025-03-12 17:02:18 +03:00
Ivan K
3e96b9a1af feat: update DNS checks and improve FakeIP status reporting 2025-03-12 16:20:59 +03:00
itdoginfo
251f94cb88 v0.3.26 2025-03-12 14:59:03 +03:00
itdoginfo
44936c698e Merge pull request #62 from itdoginfo/chore/fakeip-method
feat: add CLI check for FakeIP functionality and update status display
2025-03-12 14:57:41 +03:00
Ivan K
0faaca12fc сhore: remove tabs 2025-03-12 14:54:56 +03:00
Ivan K
c6d1f05916 feat: add CLI check for FakeIP functionality and update status display 2025-03-11 19:14:21 +03:00
itdoginfo
57554d518b v0.3.25 2025-03-11 18:39:30 +03:00
itdoginfo
09d761956c Some fixes 2025-03-11 18:39:18 +03:00
11 changed files with 1037 additions and 190 deletions

View File

@@ -11,6 +11,22 @@ jobs:
steps:
- uses: actions/checkout@v4.2.1
- name: Check version match
run: |
PODKOP_VERSION=$(grep '^PKG_VERSION:=' podkop/Makefile | cut -d '=' -f 2)
LUCI_APP_PODKOP_VERSION=$(grep '^PKG_VERSION:=' luci-app-podkop/Makefile | cut -d '=' -f 2)
TAG_VERSION=${GITHUB_REF#refs/tags/v}
echo "Podkop version: $PODKOP_VERSION"
echo "Luci-app-podkop version: $LUCI_APP_PODKOP_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$PODKOP_VERSION" != "$TAG_VERSION" ] || [ "$LUCI_APP_PODKOP_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Version mismatch"
exit 1
fi
- name: Build and push
uses: docker/build-push-action@v6.9.0
with:

View File

@@ -80,12 +80,9 @@ Luci: Services/podkop
# ToDo
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
- [ ] Проверка, что версия в makefile совпадает с тегом
- [ ] Диагностика: Proxy check completed successfully предположительно не показывает IP, если вернулся это IPv6.
- [ ] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn
- [ ] Обработка ошибки `sing-box[9345]: FATAL[0000] start service: initialize DNS rule[2]: rule-set not found: main`. Когда не задана строка\интерфейс
- [ ] Проверка `/etc/resolv.conf` на наличие DNS-серверов
- [ ] Отслеживание интерфейса wan в sing-box
- [ ] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn (нужно wiki)
- [ ] Рестарт сервиса без рестарта dnsmasq
- [ ] `ash: can't kill pid 9848: No such process` при обновлении
Низкий приоритет
- [ ] Галочка, которая режет доступ к doh серверам

View File

@@ -43,6 +43,13 @@ main() {
add_tunnel
fi
check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest")
if echo "$check_response" | grep -q 'API rate limit '; then
echo "You've reached rate limit from GitHub. Repeat in five minutes."
exit 1
fi
download_success=0
while read -r url; do
filename=$(basename "$url")

View File

@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-podkop
PKG_VERSION:=0.3.24
PKG_VERSION:=0.3.33
PKG_RELEASE:=1
LUCI_TITLE:=LuCI podkop app

View File

@@ -4,6 +4,7 @@
'require ui';
'require network';
'require fs';
'require uci';
const STATUS_COLORS = {
SUCCESS: '#4caf50',
@@ -11,6 +12,8 @@ const STATUS_COLORS = {
WARNING: '#ff9800'
};
const ERROR_POLL_INTERVAL = 5000; // 5 seconds
async function safeExec(command, args = [], timeout = 7000) {
try {
const controller = new AbortController();
@@ -41,18 +44,15 @@ function formatDiagnosticOutput(output) {
.replace(/\r/g, '\n');
}
function getNetworkInterfaces(o, section_id) {
const excludeInterfaces = ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'];
function getNetworkInterfaces(o, section_id, excludeInterfaces = []) {
return network.getDevices().then(devices => {
// Reset the options by creating a new keylist
o.keylist = [];
o.vallist = [];
devices.forEach(device => {
if (device.dev && device.dev.name) {
const deviceName = device.dev.name;
if (!excludeInterfaces.includes(deviceName) && !/^lan\d+$/.test(deviceName)) {
if (!excludeInterfaces.includes(deviceName)) {
o.value(deviceName, deviceName);
}
}
@@ -93,20 +93,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 {
@@ -124,7 +133,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('//'));
@@ -243,11 +251,17 @@ function createConfigSection(section, map, network) {
}
};
o = s.taboption('basic', form.Flag, 'ss_uot', _('Shadowsocks UDP over TCP'), _('Apply for SS2022'));
o.default = '0';
o.depends('mode', 'proxy');
o.rmempty = false;
o.ucisection = 'main';
o = s.taboption('basic', form.ListValue, 'interface', _('Network Interface'), _('Select network interface for VPN connection'));
o.depends('mode', 'vpn');
o.ucisection = s.section;
o.load = function (section_id) {
return getNetworkInterfaces(this, section_id).then(() => {
return getNetworkInterfaces(this, section_id, ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']).then(() => {
return this.super('load', section_id);
});
};
@@ -591,7 +605,7 @@ const createModalContent = (title, content) => {
const showConfigModal = async (command, title) => {
const res = await safeExec('/usr/bin/podkop', [command]);
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_(title), createModalContent(title, formattedOutput));
ui.showModal(_(title), createModalContent(_(title), formattedOutput));
};
// Button Factory
@@ -614,6 +628,16 @@ const ButtonFactory = {
});
},
createInitActionButton: function (config) {
return this.createButton({
label: config.label,
additionalClass: `cbi-button-${config.type || ''}`,
onClick: () => safeExec('/etc/init.d/podkop', [config.action])
.then(() => config.reload && location.reload()),
style: config.style
});
},
createModalButton: function (config) {
return this.createButton({
label: config.label,
@@ -650,32 +674,24 @@ const createStatusPanel = (title, status, buttons) => {
};
// Update the status section creation
let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus) {
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, [
podkopStatus.running ?
ButtonFactory.createActionButton({
label: 'Stop Podkop',
type: 'remove',
action: 'stop',
reload: true
}) :
ButtonFactory.createActionButton({
label: 'Start Podkop',
type: 'apply',
action: 'start',
reload: true
}),
ButtonFactory.createActionButton({
label: podkopStatus.running ? 'Stop Podkop' : 'Start Podkop',
type: podkopStatus.running ? 'remove' : 'apply',
action: podkopStatus.running ? 'stop' : 'start',
reload: true
}),
ButtonFactory.createActionButton({
label: 'Restart Podkop',
type: 'apply',
action: 'restart',
reload: true
}),
ButtonFactory.createActionButton({
ButtonFactory.createInitActionButton({
label: podkopStatus.enabled ? 'Disable Podkop' : 'Enable Podkop',
type: podkopStatus.enabled ? 'remove' : 'apply',
action: podkopStatus.enabled ? 'disable' : 'enable',
@@ -690,6 +706,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')
})
]),
@@ -709,51 +730,209 @@ 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')
})
]),
// FakeIP Status Panel with dynamic status
createStatusPanel('FakeIP Status', {
running: fakeipStatus.state === 'working',
status: fakeipStatus.message
}, [
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'
})
// FakeIP Status Panel with both browser and router checks
createStatusPanel(_('FakeIP Status'), null, [
E('div', { style: 'margin-bottom: 10px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('span', { style: `color: ${fakeipStatus.color}` }, [
fakeipStatus.state === 'working' ? '✔' : fakeipStatus.state === 'not_working' ? '✘' : '!',
' ',
fakeipStatus.state === 'working' ? _('works in browser') : _('not works in browser')
])
]),
E('div', {}, [
E('span', { style: `color: ${fakeipCLIStatus.color}` }, [
fakeipCLIStatus.state === 'working' ? '✔' : fakeipCLIStatus.state === 'not_working' ? '✘' : '!',
' ',
fakeipCLIStatus.state === 'working' ? _('works on router') : _('not works on router')
])
])
]),
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
createStatusPanel('Version Information', null, [
createStatusPanel(_('Version Information'), null, [
E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [
E('strong', {}, 'Podkop: '), podkop.stdout ? podkop.stdout.trim() : _('Unknown'), '\n',
E('strong', {}, 'LuCI App: '), luci.stdout ? luci.stdout.trim() : _('Unknown'), '\n',
E('strong', {}, 'Sing-box: '), singbox.stdout ? singbox.stdout.trim() : _('Unknown'), '\n',
E('strong', {}, 'OpenWrt Version: '), system.stdout ? system.stdout.split('\n')[1].trim() : _('Unknown'), '\n',
E('strong', {}, 'Device Model: '), system.stdout ? system.stdout.split('\n')[4].trim() : _('Unknown')
E('strong', {}, _('Podkop: ')), podkop.stdout ? podkop.stdout.trim() : _('Unknown'), '\n',
E('strong', {}, _('LuCI App: ')), luci.stdout ? luci.stdout.trim() : _('Unknown'), '\n',
E('strong', {}, _('Sing-box: ')), singbox.stdout ? singbox.stdout.trim() : _('Unknown'), '\n',
E('strong', {}, _('OpenWrt Version: ')), system.stdout ? system.stdout.split('\n')[1].trim() : _('Unknown'), '\n',
E('strong', {}, _('Device Model: ')), system.stdout ? system.stdout.split('\n')[4].trim() : _('Unknown')
])
])
])
]);
};
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']);
if (!dnsStatusResult || !dnsStatusResult.stdout) {
return resolve({
remote: createStatus('error', 'DNS check timeout', 'WARNING'),
local: createStatus('error', 'DNS check timeout', 'WARNING')
});
}
try {
const dnsStatus = JSON.parse(dnsStatusResult.stdout);
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 (parseError) {
return resolve({
remote: createStatus('error', 'DNS check parse error', 'WARNING'),
local: createStatus('error', 'DNS check parse error', 'WARNING')
});
}
} catch (error) {
return resolve({
remote: createStatus('error', 'DNS check error', 'WARNING'),
local: createStatus('error', 'DNS check error', 'WARNING')
});
}
});
}
async function getPodkopErrors() {
try {
const result = await safeExec('/usr/bin/podkop', ['check_logs']);
if (!result || !result.stdout) return [];
const logs = result.stdout.split('\n');
const errors = logs.filter(log =>
// log.includes('saved for future filters') ||
log.includes('[critical]')
);
console.log('Found errors:', errors);
return errors;
} catch (error) {
console.error('Error getting podkop logs:', error);
return [];
}
}
let errorPollTimer = null;
let lastErrorsSet = new Set();
let isInitialCheck = true;
function showErrorNotification(error, isMultiple = false) {
const notificationContent = E('div', { 'class': 'alert-message error' }, [
E('pre', { 'class': 'error-log' }, error)
]);
ui.addNotification(null, notificationContent);
}
function startErrorPolling() {
if (errorPollTimer) {
clearInterval(errorPollTimer);
}
async function checkErrors() {
const result = await safeExec('/usr/bin/podkop', ['check_logs']);
if (!result || !result.stdout) return;
const logs = result.stdout;
const errorLines = logs.split('\n').filter(line =>
// line.includes('saved for future filters') ||
line.includes('[critical]')
);
if (errorLines.length > 0) {
const currentErrors = new Set(errorLines);
if (isInitialCheck) {
if (errorLines.length > 0) {
showErrorNotification(errorLines.join('\n'), true);
}
isInitialCheck = false;
} else {
const newErrors = [...currentErrors].filter(error => !lastErrorsSet.has(error));
newErrors.forEach(error => {
showErrorNotification(error, false);
});
}
lastErrorsSet = currentErrors;
}
}
checkErrors();
errorPollTimer = setInterval(checkErrors, ERROR_POLL_INTERVAL);
}
function stopErrorPolling() {
if (errorPollTimer) {
clearInterval(errorPollTimer);
errorPollTimer = null;
}
}
return view.extend({
async render() {
document.head.insertAdjacentHTML('beforeend', `
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<style>
.cbi-value {
margin-bottom: 10px !important;
@@ -899,6 +1078,15 @@ return view.extend({
return true;
};
o = mainSection.taboption('additional', form.MultiValue, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate'));
o.ucisection = 'main';
o.default = 'br-lan';
o.load = function (section_id) {
return getNetworkInterfaces(this, section_id, ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']).then(() => {
return this.super('load', section_id);
});
};
// 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';
@@ -932,7 +1120,39 @@ return view.extend({
o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => E('div', { id: 'diagnostics-status' }, _('Loading diagnostics...'));
o.cfgvalue = () => E('div', {
id: 'diagnostics-status',
'style': 'cursor: pointer;'
}, _('Click to load diagnostics...'));
let diagnosticsUpdateTimer = null;
function startDiagnosticsUpdates() {
if (diagnosticsUpdateTimer) {
clearInterval(diagnosticsUpdateTimer);
}
const container = document.getElementById('diagnostics-status');
if (container) {
container.innerHTML = _('Loading diagnostics...');
}
updateDiagnostics();
diagnosticsUpdateTimer = setInterval(updateDiagnostics, 10000);
}
function stopDiagnosticsUpdates() {
if (diagnosticsUpdateTimer) {
clearInterval(diagnosticsUpdateTimer);
diagnosticsUpdateTimer = null;
}
// Reset the loading state when stopping updates
const container = document.getElementById('diagnostics-status');
if (container) {
container.removeAttribute('data-loading');
}
}
function checkFakeIP() {
const createStatus = (state, message, color) => ({
@@ -957,29 +1177,134 @@ return view.extend({
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch('http://httpbin.org/ip', { signal: controller.signal });
const text = await response.text();
const response = await fetch('https://fakeip.tech-domain.club/check', { signal: controller.signal });
const data = await response.json();
clearTimeout(timeoutId);
if (text.includes('Cannot GET /ip')) {
if (data.fakeip === true) {
return resolve(createStatus('working', 'working', 'SUCCESS'));
}
if (text.includes('"origin":')) {
} else {
return resolve(createStatus('not_working', 'not working', 'ERROR'));
}
return resolve(createStatus('error', 'check error', 'WARNING'));
} catch (fetchError) {
clearTimeout(timeoutId);
const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
return resolve(createStatus('error', message, 'WARNING'));
}
} catch (error) {
console.error('Error in checkFakeIP:', error);
return resolve(createStatus('error', 'check error', 'WARNING'));
}
});
}
function checkFakeIPCLI() {
const createStatus = (state, message, color) => ({
state,
message: _(message),
color: STATUS_COLORS[color]
});
return new Promise(async (resolve) => {
try {
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', 'sing-box not running', 'ERROR'));
}
if (!singboxStatus.dns_configured) {
return resolve(createStatus('not_working', 'DNS not configured', 'ERROR'));
}
const result = await safeExec('nslookup', ['-timeout=2', 'fakeip.tech-domain.club', '127.0.0.42']);
if (result.stdout && result.stdout.includes('198.18')) {
return resolve(createStatus('working', 'working on router', 'SUCCESS'));
} else {
return resolve(createStatus('not_working', 'not working on router', 'ERROR'));
}
} catch (error) {
return resolve(createStatus('error', 'CLI check error', 'WARNING'));
}
});
}
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 [
@@ -989,7 +1314,10 @@ return view.extend({
luci,
singbox,
system,
fakeipStatus
fakeipStatus,
fakeipCLIStatus,
dnsStatus,
bypassStatus
] = await Promise.all([
safeExec('/usr/bin/podkop', ['get_status']),
safeExec('/usr/bin/podkop', ['get_sing_box_status']),
@@ -997,7 +1325,10 @@ return view.extend({
safeExec('/usr/bin/podkop', ['show_luci_version']),
safeExec('/usr/bin/podkop', ['show_sing_box_version']),
safeExec('/usr/bin/podkop', ['show_system_info']),
checkFakeIP()
checkFakeIP(),
checkFakeIPCLI(),
checkDNSAvailability(),
checkBypass()
]);
const parsedPodkopStatus = JSON.parse(podkopStatus.stdout || '{"running":0,"enabled":0,"status":"unknown"}');
@@ -1006,7 +1337,35 @@ return view.extend({
const container = document.getElementById('diagnostics-status');
if (!container) return;
const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system, fakeipStatus);
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);
@@ -1017,8 +1376,31 @@ return view.extend({
fakeipStatus.message
]).outerHTML;
}
const fakeipCLIElement = document.getElementById('fakeip-cli-status');
if (fakeipCLIElement) {
fakeipCLIElement.innerHTML = E('span', { 'style': `color: ${fakeipCLIStatus.color}` }, [
fakeipCLIStatus.state === 'working' ? '✔ ' : fakeipCLIStatus.state === 'not_working' ? '✘ ' : '! ',
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) {
console.error('Error updating diagnostics:', e);
const container = document.getElementById('diagnostics-status');
if (container) {
container.innerHTML = E('div', { 'class': 'alert-message warning' }, [
@@ -1030,61 +1412,6 @@ return view.extend({
}
}
function startPeriodicUpdates(titleDiv) {
let updateTimer = null;
let isVisible = !document.hidden;
let versionText = _('Podkop');
let versionReceived = false;
const updateStatus = async () => {
try {
if (!versionReceived) {
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('/usr/bin/podkop', ['get_sing_box_status']);
const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}');
const fakeipStatus = await checkFakeIP();
titleDiv.textContent = versionText + (!singboxStatus.running || !singboxStatus.dns_configured === 'not_working' ? ' (not working)' : '');
await updateDiagnostics();
} catch (error) {
console.warn('Failed to update status:', error);
titleDiv.textContent = versionText + ' (not working)';
}
};
const toggleUpdates = (visible) => {
if (visible) {
updateStatus();
if (!updateTimer) {
updateTimer = setInterval(updateStatus, 10000);
}
} else if (updateTimer) {
clearInterval(updateTimer);
updateTimer = null;
}
};
document.addEventListener('visibilitychange', () => {
isVisible = !document.hidden;
toggleUpdates(isVisible);
});
toggleUpdates(isVisible);
window.addEventListener('unload', () => {
if (updateTimer) {
clearInterval(updateTimer);
}
});
}
// Extra Section
const extraSection = m.section(form.TypedSection, 'extra', _('Extra configurations'));
extraSection.anonymous = false;
@@ -1097,8 +1424,62 @@ 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();
stopErrorPolling();
} else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) {
startDiagnosticsUpdates();
startErrorPolling();
}
});
setTimeout(() => {
const diagnosticsContainer = document.getElementById('diagnostics-status');
if (diagnosticsContainer) {
diagnosticsContainer.addEventListener('click', function () {
if (!this.hasAttribute('data-loading')) {
this.setAttribute('data-loading', 'true');
startDiagnosticsUpdates();
startErrorPolling();
}
});
}
const tabs = node.querySelectorAll('.cbi-tabmenu');
if (tabs.length > 0) {
tabs[0].addEventListener('click', function (e) {
const tab = e.target.closest('.cbi-tab');
if (tab) {
const tabName = tab.getAttribute('data-tab');
if (tabName === 'diagnostics') {
const container = document.getElementById('diagnostics-status');
if (container && !container.hasAttribute('data-loading')) {
container.setAttribute('data-loading', 'true');
startDiagnosticsUpdates();
startErrorPolling();
}
} else {
stopDiagnosticsUpdates();
stopErrorPolling();
}
}
});
const activeTab = tabs[0].querySelector('.cbi-tab[data-tab="diagnostics"]');
if (activeTab) {
const container = document.getElementById('diagnostics-status');
if (container && !container.hasAttribute('data-loading')) {
container.setAttribute('data-loading', 'true');
startDiagnosticsUpdates();
startErrorPolling();
}
}
}
}, 100);
node.classList.add('fade-in');
startPeriodicUpdates(titleDiv);
return node;
});

View File

@@ -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 "Конфигурация исходящего соединения"
@@ -722,4 +722,94 @@ msgid "stopped but enabled"
msgstr "остановлен, но активирован"
msgid "stopped & disabled"
msgstr "остановлен и деактивирован"
msgstr "остановлен и деактивирован"
msgid "works in browser"
msgstr "работает в браузере"
msgid "works on router"
msgstr "работает на роутере"
msgid "Check Router FakeIP"
msgstr "Проверить FakeIP на роутере"
msgid "FakeIP Router Check"
msgstr "Проверка FakeIP на роутере"
msgid "FakeIP CLI Check"
msgstr "Проверка FakeIP через CLI"
msgid "FakeIP CLI Check Results"
msgstr "Результаты проверки FakeIP через CLI"
msgid "not works in browser"
msgstr "не работает в браузере"
msgid "not works on router"
msgstr "не работает на роутере"
msgid "Diagnostics"
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 "недоступен"

View File

@@ -1076,4 +1076,97 @@ msgid "stopped but enabled"
msgstr ""
msgid "stopped & disabled"
msgstr ""
msgid "works in browser"
msgstr ""
msgid "works on router"
msgstr ""
msgid "Check Router FakeIP"
msgstr ""
msgid "FakeIP Router Check"
msgstr ""
msgid "FakeIP CLI Check"
msgstr ""
msgid "FakeIP CLI Check Results"
msgstr ""
msgid "not works in browser"
msgstr ""
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 ""

View File

@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=podkop
PKG_VERSION:=0.3.24
PKG_VERSION:=0.3.33
PKG_RELEASE:=1
PKG_MAINTAINER:=ITDog <podkop@itdog.info>

View File

@@ -34,4 +34,6 @@ config main 'main'
option dns_type 'doh'
option dns_server '8.8.8.8'
option dns_rewrite_ttl '60'
option cache_file '/tmp/cache.db'
option cache_file '/tmp/cache.db'
list iface 'br-lan'
option ss_uot '0'

View File

@@ -6,6 +6,7 @@ USE_PROCD=1
script=$(readlink "$initscript")
NAME="$(basename ${script:-$initscript})"
config_load "$NAME"
resolv_conf="/etc/resolv.conf"
start_service() {
echo "Start podkop"
@@ -18,17 +19,20 @@ start_service() {
exit 1
fi
if grep -q FriendlyWrt /etc/banner; then
printf "\033[31;1mYou use FriendlyWrt. If you have problems, check out: https://t.me/itdogchat/44512/181082\033[0m\n"
if opkg list-installed | grep -q iptables-mod-extra; then
echo "Conflicting package detected: iptables-mod-extra"
fi
if opkg list-installed | grep -q kmod-ipt-nat; then
echo "Conflicting package detected: kmod-ipt-nat"
fi
if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
printf "\033[31;1mDetected https-dns-proxy. Disable or uninstall it for correct functionality.\033[0m\n"
fi
if ! ip addr | grep -q "br-lan"; then
echo "Interface br-lan not found"
exit 1
if ! grep -q "search lan" "$resolv_conf" || ! grep -q "nameserver 127.0.0.1" "$resolv_conf"; then
echo "/etc/resolv.conf does not contain 'search lan' or 'nameserver 127.0.0.1' entries"
fi
procd_open_instance

View File

@@ -19,7 +19,9 @@ 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"
TEST_DOMAIN="fakeip.tech-domain.club"
INTERFACES_LIST=""
SRC_INTERFACE=""
log() {
local message="$1"
@@ -43,6 +45,33 @@ nolog() {
}
start() {
log "Starting podkop"
# checking
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
required_version="1.11.1"
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
log "[critical] The version of sing-box ($sing_box_version) is lower than the minimum version. Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box"
exit 1
fi
if opkg list-installed | grep -q iptables-mod-extra; then
log "[critical] Conflicting package detected: iptables-mod-extra"
fi
if opkg list-installed | grep -q kmod-ipt-nat; then
log "[critical] Conflicting package detected: kmod-ipt-nat"
fi
if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then
log "[critical] Detected https-dns-proxy. Disable or uninstall it for correct functionality."
fi
if ! grep -q "search lan" "$resolv_conf" || ! grep -q "nameserver 127.0.0.1" "$resolv_conf"; then
log "[critical] /etc/resolv.conf does not contain 'search lan' or 'nameserver 127.0.0.1' entries"
fi
migration
config_foreach process_validate_service
@@ -50,7 +79,7 @@ start() {
sleep 3
mkdir -p /tmp/podkop
# base
route_table_rule_mark
create_nft_table
@@ -61,6 +90,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
@@ -108,7 +138,7 @@ start() {
fi
sing_box_config_check
/etc/init.d/sing-box restart
/etc/init.d/sing-box start
/etc/init.d/sing-box enable
config_get proxy_string "main" "proxy_string"
@@ -261,20 +291,54 @@ route_table_rule_mark() {
fi
}
process_interfaces() {
local iface="$1"
INTERFACES_LIST="$INTERFACES_LIST $iface"
iface_flag=1
}
nft_interfaces() {
local table=PodkopTable
iface_flag=0
config_list_foreach "main" "iface" "process_interfaces"
if [ "$iface_flag" -eq 0 ]; then
SRC_INTERFACE="br-lan"
elif [ $(echo "$INTERFACES_LIST" | wc -w) -eq 1 ]; then
SRC_INTERFACE=$INTERFACES_LIST
else
local set_name="interfaces"
if ! nft list set inet $table $set_name &>/dev/null; then
nft add set inet $table $set_name { type ifname\; flags interval\; }
fi
for interface in $INTERFACES_LIST; do
if ! nft list element inet $table $set_name { $interface } &>/dev/null; then
nft add element inet $table $set_name { $interface }
fi
done
SRC_INTERFACE=@$set_name
fi
}
create_nft_table() {
local table="PodkopTable"
nft add table inet $table
nft_interfaces
log "Create 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 mangle iifname "$SRC_INTERFACE" ip daddr @podkop_subnets meta l4proto tcp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_subnets meta l4proto udp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$FAKEIP" meta l4proto tcp meta mark set 0x105 counter
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr "$FAKEIP" meta l4proto udp meta mark set 0x105 counter
nft add rule inet $table proxy meta mark 0x105 meta l4proto tcp tproxy ip to :1602 counter
nft add rule inet $table proxy meta mark 0x105 meta l4proto udp tproxy ip to :1602 counter
@@ -442,11 +506,12 @@ remove_cron_job() {
prepare_custom_ruleset() {
config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled"
config_get custom_download_domains_list_enabled "$section" "custom_download_subnets_list_enabled"
config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled"
if [ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ]; then
local file="/tmp/podkop/$section-custom-domains-subnets.json"
local tag="custom-$section"
rm -f $file
jq -n '
{
"version": 3,
@@ -510,12 +575,11 @@ list_update() {
find_working_resolver() {
local resolver_found=""
for resolver in $DNS_RESOLVERS; do
if nslookup $TEST_DOMAIN $resolver >/dev/null 2>&1; then
if nslookup -timeout=2 $TEST_DOMAIN $resolver >/dev/null 2>&1; then
echo "$resolver"
return 0
fi
done
echo "8.8.8.8"
return 1
}
@@ -529,8 +593,11 @@ sing_box_uci() {
-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
if grep -q '#\s*list ifaces' "$config"; then
sed -i '/ifaces/s/#//g' $config
log "Uncommented list ifaces"
fi
}
@@ -623,7 +690,12 @@ sing_box_dns() {
if [ "$is_ip" = "0" ]; then
log "Finding working DNS resolver"
local dns_resolver=$(find_working_resolver)
log "Found working resolver: $dns_resolver"
if [ -z "$dns_resolver" ]; then
log "No working resolver found, using default DNS server"
dns_resolver="1.1.1.1"
else
log "Found working resolver: $dns_resolver"
fi
fi
log "Configure DNS in sing-box"
@@ -682,6 +754,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"
@@ -759,6 +867,12 @@ sing_box_outdound() {
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"
if [ -z "$interface" ]; then
log "[critical] VPN interface is not set. Exit"
exit 1
fi
sing_box_outbound_interface $section $interface
;;
"proxy")
@@ -781,12 +895,13 @@ sing_box_outdound() {
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
log "[critical] Proxy string is not set. Exit"
exit 1
fi
if [[ "$active_proxy_string" =~ ^ss:// ]]; then
sing_box_config_shadowsocks "$section" "$active_proxy_string"
config_get ss_uot $section "ss_uot"
sing_box_config_shadowsocks "$section" "$active_proxy_string" "$ss_uot"
elif [[ "$active_proxy_string" =~ ^vless:// ]]; then
sing_box_config_vless "$section" "$active_proxy_string"
else
@@ -853,7 +968,7 @@ sing_box_rule_dns() {
sing_box_config_check() {
if ! sing-box -c $SING_BOX_CONFIG check >/dev/null 2>&1; then
log "Sing-box configuration is invalid"
log "[critical] Sing-box configuration is invalid"
exit 1
fi
}
@@ -893,6 +1008,7 @@ sing_box_config_outbound_json() {
sing_box_config_shadowsocks() {
local section="$1"
local STRING="$2"
ss_uot="${3:-0}"
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 )
@@ -916,6 +1032,7 @@ sing_box_config_shadowsocks() {
--argjson port "$port" \
--arg method "$method" \
--arg password "$password" \
--argjson ss_uot "$ss_uot" \
'. |
.outbounds |= (
map(
@@ -925,9 +1042,8 @@ sing_box_config_shadowsocks() {
"server": $server,
"server_port": ($port | tonumber),
"method": $method,
"password": $password,
"udp_over_tcp": { "enabled": true, "version": 2 }
}
"password": $password
} + (if $ss_uot == 1 then { "udp_over_tcp": { "enabled": true, "version": 2 } } else {} end)
else . end
) +
(
@@ -938,9 +1054,8 @@ sing_box_config_shadowsocks() {
"server": $server,
"server_port": ($port | tonumber),
"method": $method,
"password": $password,
"udp_over_tcp": { "enabled": true, "version": 2 }
}]
"password": $password
} + (if $ss_uot == 1 then { "udp_over_tcp": { "enabled": true, "version": 2 } } else {} end)]
else [] end
)
)' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
@@ -1191,7 +1306,7 @@ sing_box_ruleset_domains_json() {
.rules[0].domain_suffix += if .rules[0].domain_suffix | index($domain) then [] else [$domain] end
' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
log "$domain added to custom-domains-subnets.json"
log "$domain added to $section-custom-domains-subnets.json"
}
sing_box_ruleset_subnets_json() {
@@ -1204,7 +1319,7 @@ sing_box_ruleset_subnets_json() {
.rules[0].ip_cidr += if .rules[0].ip_cidr | index($subnet) then [] else [$subnet] end
' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
log "$subnet added to '$section'-custom-domains-subnets.json"
log "$subnet added to $section-custom-domains-subnets.json"
}
process_domains_for_section() {
@@ -1276,7 +1391,7 @@ list_subnets_download() {
"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
nft add rule inet $table mangle iifname "$SRC_INTERFACE" ip daddr @podkop_discord_subnets udp dport { 50000-65535 } meta mark set 0x105 counter
;;
*)
return
@@ -1417,7 +1532,7 @@ list_custom_url_domains_create() {
wget -q -O "/tmp/podkop/${filename}" "$URL"
while IFS= read -r domain; do
log "From local file: $domain"
log "From downloaded file: $domain"
sing_box_ruleset_domains_json $domain $section
done <"/tmp/podkop/$filename"
}
@@ -1516,9 +1631,11 @@ sing_box_rules_source_ip_cidr() {
## 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 { \
local table="PodkopTable"
if ! nft list chain inet $table mangle | grep -q "ip saddr $ip"; then
nft add set inet $table localv4 { type ipv4_addr\; flags interval\; }
nft add element inet $table localv4 { \
0.0.0.0/8, \
10.0.0.0/8, \
127.0.0.0/8, \
@@ -1532,8 +1649,8 @@ list_all_traffic_from_ip() {
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
nft insert rule inet $table mangle iifname "$SRC_INTERFACE" ip saddr $ip meta l4proto { tcp, udp } meta mark set 0x105 counter
nft insert rule inet $table mangle ip saddr $ip ip daddr @localv4 return
fi
}
@@ -1699,15 +1816,92 @@ check_sing_box_logs() {
echo "$logs"
}
check_fakeip() {
nolog "Checking fakeip functionality..."
if ! command -v nslookup >/dev/null 2>&1; then
nolog "nslookup is not installed"
return 1
fi
local test_domain="$TEST_DOMAIN"
nolog "Testing DNS resolution with default DNS server"
echo "=== Testing with default DNS server ==="
nslookup -timeout=2 $test_domain
echo ""
nolog "Finding a working DNS resolver..."
local working_resolver=$(find_working_resolver)
if [ -z "$working_resolver" ]; then
nolog "No working resolver found, skipping resolver check"
else
nolog "Using resolver: $working_resolver"
nolog "Testing DNS resolution with working resolver ($working_resolver)"
echo "=== Testing with working resolver ($working_resolver) ==="
nslookup -timeout=2 $test_domain $working_resolver
echo ""
fi
# Main FakeIP check
nolog "Testing DNS resolution for $test_domain using 127.0.0.42"
echo "=== Testing with FakeIP DNS (127.0.0.42) ==="
local result=$(nslookup -timeout=2 $test_domain 127.0.0.42 2>&1)
echo "$result"
if echo "$result" | grep -q "198.18"; then
nolog "✅ FakeIP is working correctly! Domain resolved to FakeIP range (198.18.x.x)"
return 0
else
nolog "❌ FakeIP test failed. Domain did not resolve to FakeIP range"
nolog "Checking if sing-box is running..."
if ! pgrep -f "sing-box" >/dev/null; then
nolog "sing-box is not running"
else
nolog "sing-box is running, but FakeIP might not be configured correctly"
nolog "Checking DNS configuration in sing-box..."
if [ -f "$SING_BOX_CONFIG" ]; then
local fakeip_enabled=$(jq -r '.dns.fakeip.enabled' "$SING_BOX_CONFIG")
local fakeip_range=$(jq -r '.dns.fakeip.inet4_range' "$SING_BOX_CONFIG")
nolog "FakeIP enabled: $fakeip_enabled"
nolog "FakeIP range: $fakeip_range"
local dns_rules=$(jq -r '.dns.rules[] | select(.server == "fakeip-server") | .domain' "$SING_BOX_CONFIG")
nolog "FakeIP domain: $dns_rules"
else
nolog "sing-box config file not found"
fi
fi
return 1
fi
}
check_logs() {
nolog "Showing podkop logs from system journal..."
if command -v logread >/dev/null 2>&1; then
logread -e podkop | tail -n 50
else
if ! command -v logread >/dev/null 2>&1; then
nolog "Error: logread command not found"
return 1
fi
# Get all logs first
local all_logs=$(logread)
# Find the last occurrence of "Starting podkop"
local start_line=$(echo "$all_logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1)
if [ -z "$start_line" ]; then
nolog "No 'Starting podkop' message found in logs"
return 1
fi
# Output all logs from the last start
echo "$all_logs" | tail -n +"$start_line"
}
show_sing_box_config() {
@@ -1763,6 +1957,7 @@ show_config() {
-e 's/\(ss:\/\/[^@]*@\)/ss:\/\/MASKED@/g' \
-e 's/\(pbk=[^&]*\)/pbk=MASKED/g' \
-e 's/\(sid=[^&]*\)/sid=MASKED/g' \
-e 's/\(option dns_server '\''[^'\'']*\.dns\.nextdns\.io'\''\)/option dns_server '\''MASKED.dns.nextdns.io'\''/g' \
> "$tmp_config"
cat "$tmp_config"
@@ -1867,20 +2062,76 @@ get_status() {
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
check_dns_available() {
local dns_type=$(uci get podkop.main.dns_type 2>/dev/null)
local dns_server=$(uci get podkop.main.dns_server 2>/dev/null)
local is_available=0
local status="unavailable"
local local_dns_working=0
local local_dns_status="unavailable"
# Mask NextDNS ID if present
local display_dns_server="$dns_server"
if echo "$dns_server" | grep -q "\.dns\.nextdns\.io$"; then
local nextdns_id=$(echo "$dns_server" | cut -d'.' -f1)
display_dns_server="$(echo "$nextdns_id" | sed 's/./*/g').dns.nextdns.io"
fi
if [ "$dns_type" = "doh" ]; then
local result=""
if echo "$dns_server" | grep -q "quad9.net" || \
echo "$dns_server" | grep -qE "^9\.9\.9\.(9|10|11)$|^149\.112\.112\.(112|10|11)$|^2620:fe::(fe|9|10|11)$|^2620:fe::fe:(10|11)$"; then
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server:5053/dns-query?name=itdog.info&type=A")
else
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
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/resolve?name=itdog.info&type=A")
fi
fi
if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then
is_available=1
status="available"
fi
elif [ "$dns_type" = "dot" ]; then
(nc "$dns_server" 853 </dev/null >/dev/null 2>&1) & pid=$!
sleep 2
if kill -0 $pid 2>/dev/null; then
kill $pid 2>/dev/null
wait $pid 2>/dev/null
else
is_available=1
status="available"
fi
elif [ "$dns_type" = "udp" ]; then
if nslookup -timeout=2 itdog.info $dns_server >/dev/null 2>&1; then
is_available=1
status="available"
fi
fi
# Check if local DNS resolver is working
if nslookup -timeout=2 $TEST_DOMAIN 127.0.0.1 >/dev/null 2>&1; then
local_dns_working=1
local_dns_status="available"
fi
echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"is_available\":$is_available,\"status\":\"$status\",\"local_dns_working\":$local_dns_working,\"local_dns_status\":\"$local_dns_status\"}"
}
sing_box_add_secure_dns_probe_domain() {
local domain="$TEST_DOMAIN"
local override_port=8443
log "Adding DNS probe domain ${domain} to fakeip-server configuration"
jq \
--arg domain "$domain" \
--arg override "$override_address" \
--argjson override_port "$override_port" \
'.dns.rules |= map(
if .server == "fakeip-server" then
. + {
@@ -1894,11 +2145,11 @@ sing_box_add_secure_dns_probe_domain() {
{
"domain": $domain,
"action": "route-options",
"override_address": $override
"override_port": $override_port
}
]' "$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}"
log "DNS probe domain ${domain} configured with override to port ${override_port}"
}
case "$1" in
@@ -1936,6 +2187,9 @@ case "$1" in
check_sing_box_logs)
check_sing_box_logs
;;
check_fakeip)
check_fakeip
;;
check_dnsmasq)
check_dnsmasq
;;
@@ -1963,8 +2217,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_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