mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 11:36:50 +03:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec6cc4d84 | ||
|
|
3413af9f94 | ||
|
|
76b5ceae5c | ||
|
|
99ccd9fbb3 | ||
|
|
b82c6eb718 | ||
|
|
ccc87d9aa0 | ||
|
|
8bcdee87f5 | ||
|
|
f77ef5626b | ||
|
|
b50a21ded7 | ||
|
|
a831054e5e | ||
|
|
a8dbff816c | ||
|
|
171381fa18 | ||
|
|
b806586a5a | ||
|
|
9e2b192181 | ||
|
|
c5be041664 | ||
|
|
445ad6d3d2 | ||
|
|
9203315107 | ||
|
|
d8d8d79d68 | ||
|
|
615928db4e | ||
|
|
7697754a73 | ||
|
|
25107a0481 | ||
|
|
5f5b1cbe1f | ||
|
|
a278918e77 | ||
|
|
febb69d0be | ||
|
|
1a6ee45612 | ||
|
|
000d2f8e18 |
78
README.md
78
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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 "остановлен и деактивирован"
|
||||
@@ -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 ""
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user