Compare commits

...

26 Commits

Author SHA1 Message Date
itdoginfo
3ec6cc4d84 0.3.18 2025-02-24 18:07:15 +03:00
itdoginfo
3413af9f94 Merge pull request #51 from itdoginfo/fix/vpn-devices
feat: add section_id parameter to getNetworkInterfaces function
2025-02-24 17:42:30 +03:00
Ivan K
76b5ceae5c feat: add section_id parameter to getNetworkInterfaces function 2025-02-24 17:39:56 +03:00
itdoginfo
99ccd9fbb3 0.3.17 2025-02-24 16:42:35 +03:00
itdoginfo
b82c6eb718 Merge pull request #50 from itdoginfo/fix/many-sni-support
feat: update network interface loading in podkop.js
2025-02-24 16:24:53 +03:00
Ivan K
ccc87d9aa0 feat: update network interface loading in podkop.js 2025-02-24 16:23:05 +03:00
itdoginfo
8bcdee87f5 0.3.16 2025-02-24 15:39:02 +03:00
itdoginfo
f77ef5626b default dns options 2025-02-24 15:38:50 +03:00
itdoginfo
b50a21ded7 rm wget_github 2025-02-24 15:38:24 +03:00
itdoginfo
a831054e5e Merge pull request #48 from itdoginfo/fix/many-sni-support
feat: add status panels and utility functions for better diagnostics UI
2025-02-24 10:05:05 +03:00
Ivan K
a8dbff816c fix: correct logic for checking fakeipStatus state 2025-02-24 10:00:51 +03:00
Ivan K
171381fa18 refactor: improve error handling and code readability in podkop.js and update init.d script to check sing-box status 2025-02-23 22:56:01 +03:00
Ivan K
b806586a5a fix: проверки диагностики только при активной вкладке 2025-02-23 18:13:41 +03:00
Ivan K
9e2b192181 feat: add status panels and utility functions for better diagnostics UI 2025-02-23 13:35:30 +03:00
itdoginfo
c5be041664 Update 2025-02-23 00:11:52 +03:00
itdoginfo
445ad6d3d2 Update todo 2025-02-22 17:31:17 +03:00
itdoginfo
9203315107 Merge pull request #42 from itdoginfo/fix/many-sni-support
fix: add missing URL decoding for semicolon
2025-02-22 14:52:03 +03:00
Ivan K
d8d8d79d68 feat: add support for outbound JSON configuration in sing-box 2025-02-22 14:15:27 +03:00
Ivan K
615928db4e Merge remote-tracking branch 'origin/main' into fix/many-sni-support 2025-02-22 12:49:59 +03:00
Ivan K
7697754a73 refactor: replace fs.exec with safeExec for command execution with timeout 2025-02-22 12:45:25 +03:00
Ivan K
25107a0481 refactor: simplify label fetching and decoding in podkop.js 2025-02-22 09:52:04 +03:00
itdoginfo
5f5b1cbe1f Warnning for friendlywrt, http-dns-proxy. Validation domains in local file 2025-02-22 00:04:24 +03:00
Ivan K
a278918e77 feat: add timeout and chunking to proxy label fetching 2025-02-21 17:55:39 +03:00
Ivan K
febb69d0be fix: add enable/disable button for Podkop service 2025-02-21 17:38:57 +03:00
Ivan K
1a6ee45612 fix: add function to dynamically fetch network interfaces for VPN configuration 2025-02-21 17:34:31 +03:00
Ivan K
000d2f8e18 fix: add missing URL decoding for semicolon 2025-02-21 15:37:30 +03:00
8 changed files with 805 additions and 589 deletions

View File

@@ -81,91 +81,21 @@ Luci: Services/podkop
**Custom subnets enable** - Добавить подсети или IP-адреса. Для подсетей задать маску.
# Известные баги
- [x] Не работает proxy при режимах main vpn, second proxy
- [x] Не всегда отрабатывает ucitrack (применение настроек из luci). Не удаётся повторить
- [x] All traffic for IP ломает инет на клиенте. Proxy mode
- [x] Не отрабатывает рестарт, при awg и не применяются изменения при awg
- [x] awg работает не стабильно
- [x] Сеть рестартится при любом раскладе
- [x] Выкл-вкл wg через luci не отрабатывает поднятие маршрута
- [ ] Если eof после последней строки в rt_tables, то скрипт не добавляет перенос строки
- [ ] В каких-то случаях плохо отрабатывает localfile
- [ ] warnging если в конфиге присуствует. Значит в системе есть https-dns-proxy, который тоже может перетирать конфигурацию dhcp. Рекомендовать его выключить и сделать disable. Возможно в будущем добавить несовместимость на уровне Makefile
```
option doh_backup_noresolv '0'
list doh_backup_server ''
list doh_backup_server ''
list doh_server '127.0.0.1#5053'
list doh_server '127.0.0.1#5054'
```
- [x] Только кастомный remote list не создаёт секцию в route-rules-rule-set и dns-rules-ruleset
- [ ] Не отрабатывает service podkop stop, если podkop запущен и не может, к пример, зарезолвить домен с сломанным DNS
- [ ] Длинный label ломает sing-box
- [ ] Update list из remote url domain не удаляет старые домены. А добавляет новые. Для подсетей тоже самое скорее всего. Пересоздавать ruleset?
# ToDo
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
Сделано
- [x] Скрипт для автоматической установки.
- [x] Подсети дискорда.
- [x] Удаление getdomains через скрипт. Кроме туннеля и sing-box.
- [x] Дополнительная вкладка для ещё одного туннеля. Домены, подсети.
- [x] Улучшение скрипта автоматической установки. Спрашивать про туннели.
- [x] Зависимость от dnsmasq-full
- [x] Весь трафик для устойства пускать в туннель\прокси
- [x] Исключение для IP, не ходить в туннель\прокси совсем 0x0
- [x] Врубать галочкой yacd в sing-box
- [x] Свои списки. Просто список доменов с переносом строки
- [x] Свои списки ipv4
- [x] В nft разделить правило tproxy на маркировку и tproxy
- [x] Вернуть две цепочки nft
- [x] Ntp (порт 123) делать маркировку 0x0. По галке
- [x] Открытый прокси порт на роутере для браузеров
- [x] Автонастройка wireguard по примеру getdomains
- [x] Автонастройка awg по примеру getdomains
- [x] RU перевод
- [x] Переделать на PROCD и выкинуть ucitrack.
- [x] Нужен дебаг. Restart ucitrack в отдельный скрипт postinst, не отрабатывает.
- [x] Закомментировать дефолтные значения у list. interface поставить в пустое.
- [x] Скрипт установки: проверка установлен ли уже podkop. Если да, то просто предлагать обновится без установки тунелей и прокси.
Приоритет 1
- [x] Изменить название "Alternative Config"
- [x] "domain_service_enabled" Добавить _second
- [x] Установка Ru пакета в install.sh
- [x] Правка nft mark, tproxy
- [x] Правка перевода минимальная
- [x] Вставлять готовый outdbound вместо строки. Отдельная галка, которая в идеале должны скрывать поле для строки
- [ ] udp over tcp для ss сделать с выбором:
1) отключен (ПО на сервере -Shadowsocks)
2) включен, версия 2 (новые релизы xray-core, sing-box на сервере)
3) включен, версия 1 (старые релизы xray, sing-box на сервере)
Проблема в том, что это нужно только если SS. Выставлять выбор при парсинг из конфига вопрос можно ли. Если совсем тупо - сделать костыль в допонительные настройки
- [x] Проверка места в скрипте install. Если доступно меньше 20MB - exit 1 c выводом колько надо и сколько доступно. + показ модели роутера
- [x] Правило запрещающее QUIC
- [ ] Проверить обновление списков, отрабатывает ли
- [ ] Проверка на ванильную openwrt
- [ ] Проверка откуда установлен sing-box. Например, проверять установлен ли он из официального репозитория
- [x] TG в сервисы
- [ ] Проверка, что версия в makefile совпадает с тегом
- [ ] Диагностика: Proxy check completed successfully предположительно не показывает IP, если вернулся это IPv6.
- [ ] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn
- [x] Валидации предустановленных значений. Если прописаны другие, то вывод в лог о неизвестной переменной и продолжение работы
- [ ] Проверка, что версия в makefile совпадает с тегом
Приоритет 2
- [x] Списки доменов и подсетей с роутера
- [x] Кнопка обновления списка доменов и подсетей. Запихнуть в главное меню
- [ ] IPv6
Wiki
- [x] Тема
- [x] Изначальное наполнение
- [ ] Обработка ошибки `sing-box[9345]: FATAL[0000] start service: initialize DNS rule[2]: rule-set not found: main`. Когда не задана строка\интерфейс
Низкий приоритет
- [x] Переменная, раз во сколько часов обновлять списки
- [ ] Галочка, которая режет доступ к doh серверам
- [ ] Свой конфиг sing-box
- [x] Поменять curl на wget, убрать зависимость. Проверять доступность списков лучше всего curl`ом
- [ ] IPv6. Только после наполнения Wiki
Рефактор
- [ ] Handle для sing-box

View File

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

View File

@@ -5,18 +5,50 @@
'require network';
'require fs';
const STATUS_COLORS = {
SUCCESS: '#4caf50',
ERROR: '#f44336',
WARNING: '#ff9800'
};
async function safeExec(command, args = [], timeout = 3000) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fs.exec(command, args),
new Promise((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Command execution timed out'));
});
})
]);
clearTimeout(timeoutId);
return result;
} catch (error) {
console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`);
return { stdout: '', stderr: error.message };
}
}
function formatDiagnosticOutput(output) {
if (typeof output !== 'string') return '';
return output.trim()
.replace(/\x1b\[[0-9;]*m/g, '') // Remove ANSI color codes
.replace(/\r\n/g, '\n') // Normalize line endings
.replace(/\x1b\[[0-9;]*m/g, '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
}
function getNetworkInterfaces(o) {
function getNetworkInterfaces(o, section_id) {
const excludeInterfaces = ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'];
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;
@@ -30,7 +62,6 @@ function getNetworkInterfaces(o) {
});
}
// Общая функция для создания конфигурационных секций
function createConfigSection(section, map, network) {
const s = section;
@@ -48,24 +79,37 @@ function createConfigSection(section, map, network) {
o.depends('mode', 'proxy');
o.ucisection = s.section;
o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), _('Enter connection string starting with vless:// or ss:// for proxy configuration'));
o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), '');
o.depends('proxy_config_type', 'url');
o.rows = 5;
o.ucisection = s.section;
o.load = function (section_id) {
return fs.exec('/etc/init.d/podkop', ['get_proxy_label', section_id]).then(res => {
if (res.stdout) {
try {
const decodedLabel = decodeURIComponent(res.stdout.trim());
this.description = _('Current config: ') + decodedLabel;
} catch (e) {
console.error('Error decoding label:', e);
this.description = _('Current config: ') + res.stdout.trim();
}
o.sectionDescriptions = new Map();
o.renderWidget = function (section_id, option_index, cfgvalue) {
const original = form.TextValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
const container = E('div', {});
container.appendChild(original);
if (cfgvalue) {
try {
const label = cfgvalue.split('#').pop() || 'unnamed';
const decodedLabel = decodeURIComponent(label);
const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel);
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'));
container.appendChild(descDiv);
}
return this.super('load', section_id);
});
} else {
const defaultDesc = E('div', { 'class': 'cbi-value-description' },
_('Enter connection string starting with vless:// or ss:// for proxy configuration'));
container.appendChild(defaultDesc);
}
return container;
};
o.validate = function (section_id, value) {
if (!value || value.length === 0) {
return true;
@@ -185,7 +229,11 @@ function createConfigSection(section, map, network) {
o = s.taboption('basic', form.ListValue, 'interface', _('Network Interface'), _('Select network interface for VPN connection'));
o.depends('mode', 'vpn');
o.ucisection = s.section;
getNetworkInterfaces(o);
o.load = function (section_id) {
return getNetworkInterfaces(this, section_id).then(() => {
return this.super('load', section_id);
});
};
o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists'));
o.default = '0';
@@ -334,13 +382,7 @@ function createConfigSection(section, map, network) {
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
try {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol)) return _('URL must use http:// or https:// protocol');
return true;
} catch (e) {
return _('Invalid URL format. URL must start with http:// or https://');
}
return validateUrl(value);
};
o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets'));
@@ -411,13 +453,7 @@ function createConfigSection(section, map, network) {
o.ucisection = s.section;
o.validate = function (section_id, value) {
if (!value || value.length === 0) return true;
try {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol)) return _('URL must use http:// or https:// protocol');
return true;
} catch (e) {
return _('Invalid URL format. URL must start with http:// or https://');
}
return validateUrl(value);
};
o = s.taboption('basic', form.Flag, 'all_traffic_from_ip_enabled', _('IP for full redirection'), _('Specify local IP addresses whose traffic will always use the configured route'));
@@ -443,9 +479,225 @@ function createConfigSection(section, map, network) {
};
}
// Utility functions
const copyToClipboard = (text, button) => {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
const originalText = button.textContent;
button.textContent = _('Copied!');
setTimeout(() => button.textContent = originalText, 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
};
const validateUrl = (url, protocols = ['http:', 'https:']) => {
try {
const parsedUrl = new URL(url);
if (!protocols.includes(parsedUrl.protocol)) {
return _('URL must use one of the following protocols: ') + protocols.join(', ');
}
return true;
} catch (e) {
return _('Invalid URL format');
}
};
// UI Helper functions
const createModalContent = (title, content) => {
return [
E('div', {
'class': 'panel-body',
style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
'line-height: 1.5; font-size: 14px;'
}, [
E('pre', { style: 'margin: 0;' }, content)
]),
E('div', {
'class': 'right',
style: 'margin-top: 1em;'
}, [
E('button', {
'class': 'btn',
'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target)
}, _('Copy to Clipboard')),
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
];
};
const showConfigModal = async (command, title) => {
const res = await safeExec('/etc/init.d/podkop', [command]);
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_(title), createModalContent(title, formattedOutput));
};
// Button Factory
const ButtonFactory = {
createButton: function (config) {
return E('button', {
'class': `btn ${config.additionalClass || ''}`.trim(),
'click': config.onClick,
'style': config.style || ''
}, _(config.label));
},
createActionButton: 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,
onClick: () => showConfigModal(config.command, config.title),
style: config.style
});
}
};
// Status Panel Factory
const createStatusPanel = (title, status, buttons) => {
const headerContent = [
E('strong', {}, _(title)),
status && E('br'),
status && E('span', {
'style': `color: ${status.running ? STATUS_COLORS.SUCCESS : STATUS_COLORS.ERROR}`
}, [
status.running ? '✔' : '✘',
' ',
status.status
])
].filter(Boolean);
return E('div', {
'class': 'panel',
'style': 'flex: 1; padding: 15px;'
}, [
E('div', { 'class': 'panel-heading' }, headerContent),
E('div', {
'class': 'panel-body',
'style': 'display: flex; flex-direction: column; gap: 8px;'
}, buttons)
]);
};
// Update the status section creation
let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus) {
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: 'Restart Podkop',
type: 'apply',
action: 'restart',
reload: true
}),
ButtonFactory.createActionButton({
label: podkopStatus.enabled ? 'Disable Podkop' : 'Enable Podkop',
type: podkopStatus.enabled ? 'remove' : 'apply',
action: podkopStatus.enabled ? 'disable' : 'enable',
reload: true
}),
ButtonFactory.createModalButton({
label: 'Show Config',
command: 'show_config',
title: 'Podkop Configuration'
}),
ButtonFactory.createModalButton({
label: 'View Logs',
command: 'check_logs',
title: 'Podkop Logs'
})
]),
// Sing-box Status Panel
createStatusPanel('Sing-box Status', singboxStatus, [
ButtonFactory.createModalButton({
label: 'Show Config',
command: 'show_sing_box_config',
title: 'Sing-box Configuration'
}),
ButtonFactory.createModalButton({
label: 'View Logs',
command: 'check_sing_box_logs',
title: 'Sing-box Logs'
}),
ButtonFactory.createModalButton({
label: 'Check Connections',
command: 'check_sing_box_connections',
title: 'Active Connections'
})
]),
// 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'
})
]),
// Version Information Panel
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')
])
])
])
]);
};
return view.extend({
async render() {
document.getElementsByTagName('head')[0].insertAdjacentHTML('beforeend', `
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">
@@ -453,13 +705,26 @@ return view.extend({
.cbi-value {
margin-bottom: 10px !important;
}
#diagnostics-status .table > div {
background: var(--background-color-primary);
border: 1px solid var(--border-color-medium);
border-radius: var(--border-radius);
}
#diagnostics-status .table > div pre,
#diagnostics-status .table > div div[style*="monospace"] {
color: var(--color-text-primary);
}
#diagnostics-status .alert-message {
background: var(--background-color-primary);
border-color: var(--border-color-medium);
}
</style>
`);
const m = new form.Map('podkop', _('Podkop configuration'), null, ['main', 'extra']);
fs.exec('/etc/init.d/podkop', ['show_version']).then(res => {
if (res.stdout) m.title = _('Podkop') + ' v' + res.stdout.trim();
});
const m = new form.Map('podkop', _(''), null, ['main', 'extra']);
// Main Section
const mainSection = m.section(form.TypedSection, 'main');
@@ -512,7 +777,7 @@ return view.extend({
o.default = '1.1.1.1';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function(section_id, value) {
o.validate = function (section_id, value) {
if (!value) {
return _('DNS server address cannot be empty');
}
@@ -560,7 +825,7 @@ return view.extend({
o.default = '/tmp/cache.db';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function(section_id, value) {
o.validate = function (section_id, value) {
if (!value) {
return _('Cache file path cannot be empty');
}
@@ -612,363 +877,53 @@ return view.extend({
// Diagnostics Tab (main section)
o = mainSection.tab('diagnostics', _('Diagnostics'));
let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system) {
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Service Status')),
E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [
E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [
E('div', { 'style': 'margin-bottom: 15px;' }, [
E('strong', {}, _('Podkop Status')),
E('br'),
E('span', { 'style': `color: ${podkopStatus.running ? '#4caf50' : '#f44336'}` }, [
podkopStatus.running ? '✔' : '✘',
' ',
podkopStatus.status
])
]),
E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
podkopStatus.running ?
E('button', {
'class': 'btn cbi-button-remove',
'click': () => fs.exec('/etc/init.d/podkop', ['stop']).then(() => location.reload())
}, _('Stop Podkop')) :
E('button', {
'class': 'btn cbi-button-apply',
'click': () => fs.exec('/etc/init.d/podkop', ['start']).then(() => location.reload())
}, _('Start Podkop')),
E('button', {
'class': 'btn cbi-button-apply',
'click': () => fs.exec('/etc/init.d/podkop', ['restart']).then(() => location.reload())
}, _('Restart Podkop')),
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['show_config']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Podkop Configuration'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('Show Config')),
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_logs']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Podkop Logs'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('View Logs'))
])
]),
E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [
E('div', { 'style': 'margin-bottom: 15px;' }, [
E('strong', {}, _('Sing-box Status')),
E('br'),
E('span', { 'style': `color: ${singboxStatus.running ? '#4caf50' : '#f44336'}` }, [
singboxStatus.running ? '✔' : '✘',
' ',
`${singboxStatus.status}`
])
]),
E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['show_sing_box_config']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Sing-box Configuration'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('Show Config')),
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_sing_box_logs']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Sing-box Logs'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('View Logs')),
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_sing_box_connections']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Active Connections'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('Check Connections'))
])
]),
E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [
E('div', { 'style': 'margin-bottom: 15px;' }, [
E('strong', {}, _('FakeIP Status')),
E('div', { 'id': 'fakeip-status' }, [E('span', {}, _('Checking FakeIP...'))])
]),
E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_nft']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('NFT Rules'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('Check NFT Rules')),
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_dnsmasq']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('DNSMasq Configuration'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('Check DNSMasq')),
E('button', {
'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['list_update']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Lists Update Results'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
E('pre', { style: 'margin: 0;' }, formattedOutput)
]),
E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function (ev) {
const textarea = document.createElement('textarea');
textarea.value = '```txt\n' + formattedOutput + '\n```';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
ev.target.textContent = _('Copied!');
setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
}, _('Copy to Clipboard')),
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
})
}, _('Update Lists'))
])
]),
E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [
E('div', { 'style': 'margin-bottom: 15px;' }, [
E('strong', {}, _('Version Information')),
E('br'),
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')
])
])
])
])
]);
};
o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => E('div', { id: 'diagnostics-status' }, _('Loading diagnostics...'));
function checkFakeIP() {
return new Promise((resolve) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const createStatus = (state, message, color) => ({
state,
message: _(message),
color: STATUS_COLORS[color]
});
fetch('http://httpbin.org/ip', { signal: controller.signal })
.then(response => response.text())
.then(text => {
return new Promise(async (resolve) => {
try {
const singboxStatusResult = await safeExec('/etc/init.d/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 controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch('http://httpbin.org/ip', { signal: controller.signal });
const text = await response.text();
clearTimeout(timeoutId);
let status = {
state: 'unknown',
message: '',
color: '#ff9800'
};
if (text.includes('Cannot GET /ip')) {
status.state = 'working';
status.message = _('working');
status.color = '#4caf50';
} else if (text.includes('"origin":')) {
status.state = 'not_working';
status.message = _('not working');
status.color = '#f44336';
} else {
status.state = 'error';
status.message = _('check error');
return resolve(createStatus('working', 'working', 'SUCCESS'));
}
resolve(status);
})
.catch(error => {
if (text.includes('"origin":')) {
return resolve(createStatus('not_working', 'not working', 'ERROR'));
}
return resolve(createStatus('error', 'check error', 'WARNING'));
} catch (fetchError) {
clearTimeout(timeoutId);
resolve({
state: 'error',
message: error.name === 'AbortError' ? _('timeout') : _('check error'),
color: '#ff9800'
});
});
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'));
}
});
}
@@ -983,12 +938,12 @@ return view.extend({
system,
fakeipStatus
] = await Promise.all([
fs.exec('/etc/init.d/podkop', ['get_status']),
fs.exec('/etc/init.d/podkop', ['get_sing_box_status']),
fs.exec('/etc/init.d/podkop', ['show_version']),
fs.exec('/etc/init.d/podkop', ['show_luci_version']),
fs.exec('/etc/init.d/podkop', ['show_sing_box_version']),
fs.exec('/etc/init.d/podkop', ['show_system_info']),
safeExec('/etc/init.d/podkop', ['get_status']),
safeExec('/etc/init.d/podkop', ['get_sing_box_status']),
safeExec('/etc/init.d/podkop', ['show_version']),
safeExec('/etc/init.d/podkop', ['show_luci_version']),
safeExec('/etc/init.d/podkop', ['show_sing_box_version']),
safeExec('/etc/init.d/podkop', ['show_system_info']),
checkFakeIP()
]);
@@ -998,11 +953,10 @@ return view.extend({
const container = document.getElementById('diagnostics-status');
if (!container) return;
const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system);
const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system, fakeipStatus);
container.innerHTML = '';
container.appendChild(statusSection);
// Update FakeIP status
const fakeipElement = document.getElementById('fakeip-status');
if (fakeipElement) {
fakeipElement.innerHTML = E('span', { 'style': `color: ${fakeipStatus.color}` }, [
@@ -1023,11 +977,59 @@ return view.extend({
}
}
// Start periodic updates
function startPeriodicUpdates() {
updateDiagnostics();
const intervalId = setInterval(updateDiagnostics, 10000);
window.addEventListener('unload', () => clearInterval(intervalId));
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('/etc/init.d/podkop', ['show_version'], 2000);
if (version.stdout) {
versionText = _('Podkop') + ' v' + version.stdout.trim();
versionReceived = true;
}
}
const singboxStatusResult = await safeExec('/etc/init.d/podkop', ['get_sing_box_status']);
const 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
@@ -1038,10 +1040,12 @@ return view.extend({
extraSection.multiple = true;
createConfigSection(extraSection, m, network);
const map_promise = m.render();
map_promise.then(node => {
const map_promise = m.render().then(node => {
const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop'));
node.insertBefore(titleDiv, node.firstChild);
node.classList.add('fade-in');
startPeriodicUpdates();
startPeriodicUpdates(titleDiv);
return node;
});

View File

@@ -554,4 +554,172 @@ msgid "Path must contain at least one directory (like /tmp/cache.db)"
msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)"
msgid "Invalid path format. Must be like /tmp/cache.db"
msgstr "Неверный формат пути. Пример: /tmp/cache.db"
msgstr "Неверный формат пути. Пример: /tmp/cache.db"
msgid "Copy to Clipboard"
msgstr "Копировать в буфер обмена"
msgid "Close"
msgstr "Закрыть"
msgid "Loading..."
msgstr "Загрузка..."
msgid "Loading version information..."
msgstr "Загрузка информации о версии..."
msgid "Checking FakeIP..."
msgstr "Проверка FakeIP..."
msgid "timeout"
msgstr "таймаут"
msgid "Current config: "
msgstr "Текущая конфигурация: "
msgid "Invalid VLESS URL: type must be one of tcp, udp, grpc, http"
msgstr "Неверный URL VLESS: тип должен быть одним из tcp, udp, grpc, http"
msgid "Invalid VLESS URL: security must be one of tls, reality, none"
msgstr "Неверный URL VLESS: security должен быть одним из tls, reality, none"
msgid "Podkop"
msgstr "Podkop"
msgid "Proxy"
msgstr "Прокси"
msgid "VPN"
msgstr "VPN"
msgid "http://openwrt.lan:9090/ui"
msgstr "http://openwrt.lan:9090/ui"
msgid "Podkop Configuration"
msgstr "Конфигурация Podkop"
msgid "Active Connections"
msgstr "Активные соединения"
msgid "DNSMasq Configuration"
msgstr "Конфигурация DNSMasq"
msgid "Sing-box Configuration"
msgstr "Конфигурация Sing-box"
msgid "Extra configurations"
msgstr "Дополнительные конфигурации"
msgid "Add Section"
msgstr "Добавить раздел"
msgid "No output"
msgstr "Нет вывода"
msgid "Failed to copy: "
msgstr "Не удалось скопировать: "
msgid "Show Config"
msgstr "Показать конфигурацию"
msgid "View Logs"
msgstr "Просмотр логов"
msgid "Check Connections"
msgstr "Проверить соединения"
msgid "FakeIP Status"
msgstr "Статус FakeIP"
msgid "Device Model: "
msgstr "Модель устройства: "
msgid "OpenWrt Version: "
msgstr "Версия OpenWrt: "
msgid "Check DNSMasq"
msgstr "Проверить DNSMasq"
msgid "Check NFT Rules"
msgstr "Проверить правила NFT"
msgid "Update Lists"
msgstr "Обновить списки"
msgid "Lists Update Results"
msgstr "Результаты обновления списков"
msgid "NFT Rules"
msgstr "Правила NFT"
msgid "GitHub Connectivity"
msgstr "Подключение к GitHub"
msgid "Check GitHub"
msgstr "Проверить GitHub"
msgid "GitHub Connectivity Results"
msgstr "Результаты проверки подключения к GitHub"
msgid "Sing-Box Logs"
msgstr "Логи Sing-Box"
msgid "View recent sing-box logs from system journal"
msgstr "Просмотр последних логов sing-box из системного журнала"
msgid "View Sing-Box Logs"
msgstr "Просмотр логов Sing-Box"
msgid "Podkop Logs"
msgstr "Логи Podkop"
msgid "View recent podkop logs from system journal"
msgstr "Просмотр последних логов podkop из системного журнала"
msgid "View Podkop Logs"
msgstr "Просмотр логов Podkop"
msgid "Active Connections"
msgstr "Активные соединения"
msgid "View active sing-box network connections"
msgstr "Просмотр активных сетевых подключений sing-box"
msgid "DNSMasq Configuration"
msgstr "Конфигурация DNSMasq"
msgid "View current DNSMasq configuration settings"
msgstr "Просмотр текущих настроек конфигурации DNSMasq"
msgid "Sing-Box Configuration"
msgstr "Конфигурация Sing-Box"
msgid "Show current sing-box configuration"
msgstr "Показать текущую конфигурацию sing-box"
msgid "Show Sing-Box Config"
msgstr "Показать конфигурацию Sing-Box"
msgid "Diagnostic Tools"
msgstr "Инструменты диагностики"
msgid "Unknown"
msgstr "Неизвестно"
msgid "sing-box not running"
msgstr "sing-box не запущен"
msgid "DNS not configured"
msgstr "DNS не настроен"
msgid "running & enabled"
msgstr "запущен и активирован"
msgid "running but disabled"
msgstr "запущен, но деактивирован"
msgid "stopped but enabled"
msgstr "остановлен, но активирован"
msgid "stopped & disabled"
msgstr "остановлен и деактивирован"

View File

@@ -908,4 +908,172 @@ msgid "Path must contain at least one directory (like /tmp/cache.db)"
msgstr ""
msgid "Invalid path format. Must be like /tmp/cache.db"
msgstr ""
msgid "Copy to Clipboard"
msgstr ""
msgid "Close"
msgstr ""
msgid "Loading..."
msgstr ""
msgid "Loading version information..."
msgstr ""
msgid "Checking FakeIP..."
msgstr ""
msgid "timeout"
msgstr ""
msgid "Current config: "
msgstr ""
msgid "Invalid VLESS URL: type must be one of tcp, udp, grpc, http"
msgstr ""
msgid "Invalid VLESS URL: security must be one of tls, reality, none"
msgstr ""
msgid "Podkop"
msgstr ""
msgid "Proxy"
msgstr ""
msgid "VPN"
msgstr ""
msgid "http://openwrt.lan:9090/ui"
msgstr ""
msgid "Podkop Configuration"
msgstr ""
msgid "Active Connections"
msgstr ""
msgid "DNSMasq Configuration"
msgstr ""
msgid "Sing-box Configuration"
msgstr ""
msgid "Extra configurations"
msgstr ""
msgid "Add Section"
msgstr ""
msgid "No output"
msgstr ""
msgid "Failed to copy: "
msgstr ""
msgid "Show Config"
msgstr ""
msgid "View Logs"
msgstr ""
msgid "Check Connections"
msgstr ""
msgid "FakeIP Status"
msgstr ""
msgid "Device Model: "
msgstr ""
msgid "OpenWrt Version: "
msgstr ""
msgid "Check DNSMasq"
msgstr ""
msgid "Check NFT Rules"
msgstr ""
msgid "Update Lists"
msgstr ""
msgid "Lists Update Results"
msgstr ""
msgid "NFT Rules"
msgstr ""
msgid "GitHub Connectivity"
msgstr ""
msgid "Check GitHub"
msgstr ""
msgid "GitHub Connectivity Results"
msgstr ""
msgid "Sing-Box Logs"
msgstr ""
msgid "View recent sing-box logs from system journal"
msgstr ""
msgid "View Sing-Box Logs"
msgstr ""
msgid "Podkop Logs"
msgstr ""
msgid "View recent podkop logs from system journal"
msgstr ""
msgid "View Podkop Logs"
msgstr ""
msgid "Active Connections"
msgstr ""
msgid "View active sing-box network connections"
msgstr ""
msgid "DNSMasq Configuration"
msgstr ""
msgid "View current DNSMasq configuration settings"
msgstr ""
msgid "Sing-Box Configuration"
msgstr ""
msgid "Show current sing-box configuration"
msgstr ""
msgid "Show Sing-Box Config"
msgstr ""
msgid "Diagnostic Tools"
msgstr ""
msgid "Unknown"
msgstr ""
msgid "sing-box not running"
msgstr ""
msgid "DNS not configured"
msgstr ""
msgid "running & enabled"
msgstr ""
msgid "running but disabled"
msgstr ""
msgid "stopped but enabled"
msgstr ""
msgid "stopped & disabled"
msgstr ""

View File

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

View File

@@ -31,4 +31,7 @@ config main 'main'
option quic_disable '0'
option dont_touch_dhcp '0'
option update_interval '1d'
option custom_domains_text
option dns_type 'doh'
option dns_server '1.1.1.1'
option dns_rewrite_ttl '600'
option cache_file '/tmp/cache.db'

View File

@@ -7,7 +7,7 @@ script=$(readlink "$initscript")
NAME="$(basename ${script:-$initscript})"
config_load "$NAME"
EXTRA_COMMANDS="main list_update check_proxy check_nft check_github check_logs check_sing_box_connections check_sing_box_logs check_dnsmasq show_config show_version show_sing_box_config show_luci_version show_sing_box_version show_system_info get_status get_sing_box_status get_proxy_label"
EXTRA_COMMANDS="main list_update check_proxy check_nft check_github check_logs check_sing_box_connections check_sing_box_logs check_dnsmasq show_config show_version show_sing_box_config show_luci_version show_sing_box_version show_system_info get_status get_sing_box_status"
EXTRA_HELP=" list_update Updating domain and subnet lists
check_proxy Check if sing-box proxy works correctly
check_nft Show PodkopTable nftables rules
@@ -53,8 +53,12 @@ start_service() {
exit 1
fi
if opkg list-installed | grep -qE "iptables|kmod-iptab"; then
printf "\033[31;1mFound incompatible iptables packages. If you're using FriendlyWrt: https://t.me/itdogchat/44512/181082\033[0m\n"
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"
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
@@ -142,9 +146,7 @@ nolog() {
}
main() {
sleep 5
config_foreach wget_github
sleep 3
mkdir -p /tmp/podkop
@@ -208,8 +210,9 @@ main() {
config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
if [ -n "$proxy_string" ] || [ -n "$interface" ]; then
if [ -n "$proxy_string" ] || [ -n "$interface" ] || [ -n "$outbound_json" ]; then
config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0"
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_add_resolver
@@ -437,63 +440,6 @@ process_subnets_text() {
rm -f "$tmp_file"
}
wget_github() {
local count_nslookup=0
local count_curl=0
config_get domain_list_enabled "$section" "domain_list_enabled"
config_get subnets_list_enabled "$section" "subnets_list_enabled"
config_get custom_download_domains_list_enabled "$section" "custom_download_domains_list_enabled"
config_get custom_download_subnets_list_enabled "$section" "custom_download_subnets_list_enabled"
if [ "$domain_list_enabled" -eq 1 ] || [ "$subnets_list_enabled" -eq 1 ] ||
[ "$custom_download_domains_list_enabled" -eq 1 ] || [ "$custom_download_subnets_list_enabled" -eq 1 ] ; then
while true; do
if ! nslookup google.com >/dev/null 2>&1; then
log "DNS not working. Retrying... [$count_nslookup sec]"
count_nslookup=$((count_nslookup + 1))
else
return
fi
if [ $count_nslookup -lt 30 ]; then
sleep_interval=1
elif [ $count_nslookup -ge 30 ] && [ $count_nslookup -lt 60 ]; then
sleep_interval=5
elif [ $count_nslookup -ge 60 ] && [ $count_nslookup -lt 90 ]; then
sleep_interval=10
else
sleep_interval=30
fi
sleep $sleep_interval
done
while true; do
if ! curl -m 3 github.com; then
log "GitHub is not available. Check the internet availability [$count_curl sec]"
count_curl=$((count_curl + 1))
else
return
fi
if [ $count_curl -lt 30 ]; then
sleep_interval=1
elif [ $count_curl -ge 30 ] && [ $count_curl -lt 60 ]; then
sleep_interval=5
elif [ $count_curl -ge 60 ] && [ $count_curl -lt 90 ]; then
sleep_interval=10
else
sleep_interval=30
fi
sleep $sleep_interval
done
fi
}
add_cron_job() {
## Future: make a check so that it doesn't recreate many times
config_get domain_list_enabled "$section" "domain_list_enabled"
@@ -794,7 +740,7 @@ sing_box_outdound() {
config_get outbound_json $section "outbound_json"
if [ -n "$outbound_json" ]; then
log "Using JSON outbound configuration"
sing_box_config_outbound_json "$outbound_json"
sing_box_config_outbound_json "$outbound_json" "$section"
else
log "Missing outbound JSON configuration"
return
@@ -876,37 +822,34 @@ sing_box_config_check() {
sing_box_config_outbound_json() {
local json_config="$1"
local listen_port="$2"
local section="$2"
cat > /tmp/base_config.json << EOF
{
"log": {
"level": "warn"
},
"inbounds": [
{
"type": "tproxy",
"listen": "::",
"listen_port": $listen_port,
"tcp_fast_open": true,
"udp_fragment": true
},
{
"tag": "dns-in",
"type": "direct",
"listen": "127.0.0.42",
"listen_port": 53
}
],
"outbounds": [],
"route": {
"auto_detect_interface": true
}
}
EOF
# Create new object with tag first, then merge with the rest of the config
local modified_config=$(echo "$json_config" | jq --arg section "$section" \
'del(.tag) | {"tag": $section} + .')
jq --argjson outbound "$modified_config" \
--arg section "$section" \
'. |
.outbounds |= (
map(
if .tag == $section then
$outbound
else . end
) +
(
if (map(select(.tag == $section)) | length) == 0 then
[$outbound]
else [] end
)
)' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
jq --argjson outbound "$json_config" '.outbounds += [$outbound]' /tmp/base_config.json > $SING_BOX_CONFIG
rm -f /tmp/base_config.json
if [ $? -eq 0 ]; then
log "Outbound config updated successfully"
else
log "Error: Invalid JSON config generated"
return 1
fi
}
@@ -981,7 +924,7 @@ sing_box_config_vless() {
get_param() {
local param="$1"
local value=$(echo "$STRING" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
value=$(echo "$value" | sed 's/%2F/\//g; s/%2C/,/g; s/%3D/=/g; s/%2B/+/g; s/%20/ /g' | tr -d '\n' | tr -d '\r')
value=$(echo "$value" | sed 's/%2F/\//g; s/%2C/,/g; s/%3D/=/g; s/%2B/+/g; s/%20/ /g; s/%3B/;/g' | tr -d '\n' | tr -d '\r')
echo "$value"
}
@@ -1373,8 +1316,13 @@ list_custom_local_domains_create() {
local filename=$(basename "$local_file" | cut -d. -f1)
while IFS= read -r domain; do
log "From local file: $domain"
sing_box_ruleset_domains $domain $section
domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$domain" ] && echo "$domain" | grep -E -q '^([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$'; then
log "Added $domain from local file"
sing_box_ruleset_domains "$domain" "$section"
else
log "Invalid domain skipped: $domain"
fi
done <"$local_file"
}
@@ -1773,6 +1721,7 @@ get_sing_box_status() {
local enabled=0
local status=""
local version=""
local dns_configured=0
# Check if service is enabled
if [ -x /etc/rc.d/S99sing-box ]; then
@@ -1785,6 +1734,12 @@ get_sing_box_status() {
version=$(sing-box version | head -n 1 | awk '{print $3}')
fi
# Check DNS configuration
local dns_server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)
if [ "$dns_server" = "127.0.0.42" ]; then
dns_configured=1
fi
# Format status message
if [ $running -eq 1 ]; then
if [ $enabled -eq 1 ]; then
@@ -1800,7 +1755,7 @@ get_sing_box_status() {
fi
fi
echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\"}"
echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\",\"dns_configured\":$dns_configured}"
}
get_status() {
@@ -1836,18 +1791,6 @@ get_status() {
echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\"}"
}
get_proxy_label() {
local section="$1"
local proxy_string
local label=""
config_get proxy_string "$section" "proxy_string"
if [ -n "$proxy_string" ]; then
label=$(echo "$proxy_string" | sed -n 's/.*#\(.*\)$/\1/p')
echo "$label"
fi
}
sing_box_add_secure_dns_probe_domain() {
local domain="httpbin.org"
local override_address="numbersapi.com"