feat: локализация исключений GeoIP по секциям и мониторинг памяти DNS-кэша dnsmasq

This commit is contained in:
Dushnilin
2026-09-17 20:48:14 +03:00
parent 623ea59797
commit 383b426e46
15 changed files with 3826 additions and 3298 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+25 -7
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 19:42+0300\n" "POT-Creation-Date: 2026-09-17 20:48+0300\n"
"PO-Revision-Date: 2026-09-17 19:42+0300\n" "PO-Revision-Date: 2026-09-17 20:48+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -425,6 +425,9 @@ msgstr "Все сервисы"
msgid "All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions." msgid "All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions."
msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия." msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия."
msgid "allocated dynamically on demand, not upfront"
msgstr "выделяется динамически по мере запросов, а не сразу"
msgid "Allow / Enable ONLY in scheduled hours (White period)" msgid "Allow / Enable ONLY in scheduled hours (White period)"
msgstr "Разрешать / Включать ТОЛЬКО в указанные часы (Белый список)" msgstr "Разрешать / Включать ТОЛЬКО в указанные часы (Белый список)"
@@ -863,6 +866,9 @@ msgstr "Обходит UDP-трафик для выбранных игровых
msgid "CA Certificate" msgid "CA Certificate"
msgstr "CA-сертификат" msgstr "CA-сертификат"
msgid "Cache"
msgstr ""
msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive." msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive."
msgstr "Кэшировать все встроенные списки и базы данных, даже если они не выбраны ни в одной секции. Рекомендуется при большом объёме хранилища или USB-накопителе." msgstr "Кэшировать все встроенные списки и базы данных, даже если они не выбраны ни в одной секции. Рекомендуется при большом объёме хранилища или USB-накопителе."
@@ -1439,8 +1445,8 @@ msgstr "Автоподбор и бенчмарк DNS"
msgid "DNS Blocking" msgid "DNS Blocking"
msgstr "Блокировка DNS" msgstr "Блокировка DNS"
msgid "DNS Cache Size" msgid "DNS Cache Size (entries)"
msgstr "Размер кэша DNS" msgstr "Размер DNS-кэша (записей)"
msgid "DNS Check Interval" msgid "DNS Check Interval"
msgstr "Интервал проверки DNS" msgstr "Интервал проверки DNS"
@@ -1874,6 +1880,9 @@ msgstr "Ошибка выполнения трассировки:"
msgid "Error updating lists:" msgid "Error updating lists:"
msgstr "Ошибка обновления списков:" msgstr "Ошибка обновления списков:"
msgid "Estimated RAM usage (max):"
msgstr "Ориентировочный объём RAM (макс.):"
msgid "Every day" msgid "Every day"
msgstr "Каждый день" msgstr "Каждый день"
@@ -3104,6 +3113,9 @@ msgstr "Локальный адрес"
msgid "Local DNS Cache" msgid "Local DNS Cache"
msgstr "Локальный кэш DNS" msgstr "Локальный кэш DNS"
msgid "Local DNS caching disabled (0 entries)"
msgstr "Локальное DNS-кэширование отключено (0 записей)"
msgid "Local SOCKS proxy desynchronizer (ciadpi)" msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)" msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
@@ -3176,6 +3188,9 @@ msgstr "Максимально допустимый суммарный траф
msgid "Maximum allowed online time per guest device per day. When reached, access is blocked until midnight. Enter 0 for unlimited." msgid "Maximum allowed online time per guest device per day. When reached, access is blocked until midnight. Enter 0 for unlimited."
msgstr "Максимально допустимое время доступа в интернет на одно гостевое устройство в день. При исчерпании доступ блокируется до полуночи. Введите 0 для отсутствия ограничений." msgstr "Максимально допустимое время доступа в интернет на одно гостевое устройство в день. При исчерпании доступ блокируется до полуночи. Введите 0 для отсутствия ограничений."
msgid "Maximum number of cached domain entries in dnsmasq (default: 10000 ≈ 1.2 MB RAM). Memory is allocated dynamically per request (~120 B per domain), not pre-allocated upfront. Recommended: 10000 for routers with ≥128 MB RAM, 2500–5000 for 64 MB devices."
msgstr "Максимальный лимит кэшируемых доменов в dnsmasq (по умолчанию: 10000 ≈ 1.2 МБ RAM). Память выделяется динамически по мере запросов (~120 Б на домен), а не резервируется сразу. Рекомендуется: 10000 для роутеров от 128 МБ RAM, 2500–5000 для 64 МБ устройств."
msgid "Maximum size of junk packets" msgid "Maximum size of junk packets"
msgstr "Максимальный размер мусорных пакетов" msgstr "Максимальный размер мусорных пакетов"
@@ -3479,9 +3494,6 @@ msgstr "Уведомлять о перезапусках сервиса"
msgid "Notify on Subscription Updates" msgid "Notify on Subscription Updates"
msgstr "Уведомлять об обновлениях подписок" msgstr "Уведомлять об обновлениях подписок"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
msgid "Number of consecutive failed checks required before switching DNS servers." msgid "Number of consecutive failed checks required before switching DNS servers."
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов." msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
@@ -5990,6 +6002,12 @@ msgstr "Правила маршрутизации sing-box для Zapret2 не
msgid "Zero config mesh VPN daemon" msgid "Zero config mesh VPN daemon"
msgstr "Mesh VPN-демон с нулевой конфигурацией" msgstr "Mesh VPN-демон с нулевой конфигурацией"
msgid "DNS Cache Size"
msgstr "Размер кэша DNS"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported." msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только правила доменов." msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только правила доменов."
@@ -76,6 +76,10 @@ export async function fetchServicesInfo() {
: previousData.watchdogRunning, : previousData.watchdogRunning,
zapret2Running: previousData.zapret2Running, zapret2Running: previousData.zapret2Running,
zapret2MemoryMb: previousData.zapret2MemoryMb, zapret2MemoryMb: previousData.zapret2MemoryMb,
dnsmasqRunning: previousData.dnsmasqRunning,
dnsmasqMemoryMb: previousData.dnsmasqMemoryMb,
dnsmasqLocalCacheEnabled: previousData.dnsmasqLocalCacheEnabled,
dnsmasqCacheSize: previousData.dnsmasqCacheSize,
}, },
}, },
}); });
@@ -170,6 +170,10 @@ export interface StoreType {
singboxMemoryMb?: number; singboxMemoryMb?: number;
zapret2Running?: number; zapret2Running?: number;
zapret2MemoryMb?: number; zapret2MemoryMb?: number;
dnsmasqRunning?: number;
dnsmasqMemoryMb?: number;
dnsmasqLocalCacheEnabled?: number;
dnsmasqCacheSize?: number;
}; };
}; };
tailscaleWidget: { tailscaleWidget: {
@@ -129,6 +129,18 @@ function applyServiceState(uiState: Tachyon.UiState) {
zapret2MemoryMb: uiState.service.zapret2 zapret2MemoryMb: uiState.service.zapret2
? uiState.service.zapret2.memory_rss_mb ? uiState.service.zapret2.memory_rss_mb
: undefined, : undefined,
dnsmasqRunning: uiState.service.dnsmasq
? uiState.service.dnsmasq.running
: undefined,
dnsmasqMemoryMb: uiState.service.dnsmasq
? uiState.service.dnsmasq.memory_rss_mb
: undefined,
dnsmasqLocalCacheEnabled: uiState.service.dnsmasq
? uiState.service.dnsmasq.local_cache_enabled
: undefined,
dnsmasqCacheSize: uiState.service.dnsmasq
? uiState.service.dnsmasq.cache_size
: undefined,
}, },
}, },
diagnosticsSystemInfo: normalizeSingBoxVariantFields(nextSystemInfo), diagnosticsSystemInfo: normalizeSingBoxVariantFields(nextSystemInfo),
@@ -2251,6 +2251,30 @@ async function renderServicesInfoWidget() {
}); });
} }
if (data.dnsmasqRunning != null) {
let cacheDetail = '';
if (data.dnsmasqLocalCacheEnabled && data.dnsmasqCacheSize) {
const approxMb = (
(data.dnsmasqCacheSize * 120) /
(1024 * 1024)
).toFixed(1);
cacheDetail = ` [${_('Cache')}: ${data.dnsmasqCacheSize} ≈ ${approxMb} MB]`;
}
items.push({
key: 'Dnsmasq (DNS)',
value: data.dnsmasqRunning
? data.dnsmasqMemoryMb
? `${_('✔ Running')} (${data.dnsmasqMemoryMb} MB)${cacheDetail}`
: `${_('✔ Running')}${cacheDetail}`
: _('✘ Stopped'),
attributes: {
class: data.dnsmasqRunning
? 'tachyon_dashboard-page__widgets-section__item__row--success'
: 'tachyon_dashboard-page__widgets-section__item__row--error',
},
});
}
return items; return items;
}, },
'renderServicesInfoWidget', 'renderServicesInfoWidget',
+6
View File
@@ -749,6 +749,12 @@ export namespace Tachyon {
running: number; running: number;
memory_rss_mb?: number; memory_rss_mb?: number;
}; };
dnsmasq?: {
running: number;
memory_rss_mb?: number;
local_cache_enabled?: number;
cache_size?: number;
};
}; };
capabilities: GetUiCapabilities; capabilities: GetUiCapabilities;
actions: { actions: {
@@ -6772,7 +6772,11 @@ function applyServiceState(uiState) {
tachyonStatus: uiState.service.tachyon.status, tachyonStatus: uiState.service.tachyon.status,
watchdogRunning: store.get().servicesInfoWidget.data.watchdogRunning, watchdogRunning: store.get().servicesInfoWidget.data.watchdogRunning,
zapret2Running: uiState.service.zapret2 ? uiState.service.zapret2.running : void 0, zapret2Running: uiState.service.zapret2 ? uiState.service.zapret2.running : void 0,
zapret2MemoryMb: uiState.service.zapret2 ? uiState.service.zapret2.memory_rss_mb : void 0 zapret2MemoryMb: uiState.service.zapret2 ? uiState.service.zapret2.memory_rss_mb : void 0,
dnsmasqRunning: uiState.service.dnsmasq ? uiState.service.dnsmasq.running : void 0,
dnsmasqMemoryMb: uiState.service.dnsmasq ? uiState.service.dnsmasq.memory_rss_mb : void 0,
dnsmasqLocalCacheEnabled: uiState.service.dnsmasq ? uiState.service.dnsmasq.local_cache_enabled : void 0,
dnsmasqCacheSize: uiState.service.dnsmasq ? uiState.service.dnsmasq.cache_size : void 0
} }
}, },
diagnosticsSystemInfo: normalizeSingBoxVariantFields(nextSystemInfo) diagnosticsSystemInfo: normalizeSingBoxVariantFields(nextSystemInfo)
@@ -7420,7 +7424,11 @@ async function fetchServicesInfo() {
tachyonStatus: tachyon.success ? tachyon.data.status : previousData.tachyonStatus, tachyonStatus: tachyon.success ? tachyon.data.status : previousData.tachyonStatus,
watchdogRunning: watchdog.success ? Number(watchdog.data.running) : previousData.watchdogRunning, watchdogRunning: watchdog.success ? Number(watchdog.data.running) : previousData.watchdogRunning,
zapret2Running: previousData.zapret2Running, zapret2Running: previousData.zapret2Running,
zapret2MemoryMb: previousData.zapret2MemoryMb zapret2MemoryMb: previousData.zapret2MemoryMb,
dnsmasqRunning: previousData.dnsmasqRunning,
dnsmasqMemoryMb: previousData.dnsmasqMemoryMb,
dnsmasqLocalCacheEnabled: previousData.dnsmasqLocalCacheEnabled,
dnsmasqCacheSize: previousData.dnsmasqCacheSize
} }
} }
}); });
@@ -9239,6 +9247,20 @@ async function renderServicesInfoWidget() {
} }
}); });
} }
if (data.dnsmasqRunning != null) {
let cacheDetail = "";
if (data.dnsmasqLocalCacheEnabled && data.dnsmasqCacheSize) {
const approxMb = (data.dnsmasqCacheSize * 120 / (1024 * 1024)).toFixed(1);
cacheDetail = ` [${_("Cache")}: ${data.dnsmasqCacheSize} ≈ ${approxMb} MB]`;
}
items.push({
key: "Dnsmasq (DNS)",
value: data.dnsmasqRunning ? data.dnsmasqMemoryMb ? `${_("✔ Running")} (${data.dnsmasqMemoryMb} MB)${cacheDetail}` : `${_("✔ Running")}${cacheDetail}` : _("✘ Stopped"),
attributes: {
class: data.dnsmasqRunning ? "tachyon_dashboard-page__widgets-section__item__row--success" : "tachyon_dashboard-page__widgets-section__item__row--error"
}
});
}
return items; return items;
}, },
"renderServicesInfoWidget" "renderServicesInfoWidget"
@@ -1869,14 +1869,75 @@ function createSettingsContent(section, capabilities) {
"dns", "dns",
form.Value, form.Value,
"dns_cache_size", "dns_cache_size",
_("DNS Cache Size"), _("DNS Cache Size (entries)"),
_("Number of cached DNS domain entries in dnsmasq (default: 10000)."), _(
"Maximum number of cached domain entries in dnsmasq (default: 10000 ≈ 1.2 MB RAM). Memory is allocated dynamically per request (~120 B per domain), not pre-allocated upfront. Recommended: 10000 for routers with ≥128 MB RAM, 2500–5000 for 64 MB devices.",
),
); );
o.depends("dns_local_cache", "1"); o.depends("dns_local_cache", "1");
o.default = "10000"; o.default = "10000";
o.datatype = "uinteger"; o.datatype = "uinteger";
o.placeholder = "10000";
o.rmempty = true; o.rmempty = true;
const originalDnsCacheRenderWidget = o.renderWidget;
o.renderWidget = function (section_id, option_index, cfgvalue) {
const node = originalDnsCacheRenderWidget.call(
this,
section_id,
option_index,
cfgvalue,
);
const input =
node && typeof node.querySelector === "function"
? node.querySelector("input")
: node;
const calcNode = document.createElement("div");
calcNode.className = "cbi-value-description";
calcNode.style.marginTop = "4px";
calcNode.style.fontSize = "12px";
const updateCalc = () => {
const rawVal =
input && input.value !== undefined && input.value !== ""
? input.value
: (cfgvalue || 10000);
const val = parseInt(rawVal, 10);
if (isNaN(val) || val <= 0) {
calcNode.textContent =
"⚡ " + _("Local DNS caching disabled (0 entries)");
return;
}
const bytes = val * 120;
const sizeText =
bytes < 1024 * 1024
? (bytes / 1024).toFixed(0) + " KB"
: (bytes / (1024 * 1024)).toFixed(1) + " MB";
calcNode.innerHTML =
"📊 " +
_("Estimated RAM usage (max):") +
" <strong>~" +
sizeText +
"</strong> " +
"<span style='opacity: 0.8;'>(" +
_("allocated dynamically on demand, not upfront") +
")</span>";
};
if (input && typeof input.addEventListener === "function") {
input.addEventListener("input", updateCalc);
input.addEventListener("change", updateCalc);
}
updateCalc();
if (node && typeof node.appendChild === "function") {
node.appendChild(calcNode);
}
return node;
};
// ─── DNS Strategy ──────────────────────────────────────────────────────── // ─── DNS Strategy ────────────────────────────────────────────────────────
o = section.taboption( o = section.taboption(
+25 -7
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 19:42+0300\n" "POT-Creation-Date: 2026-09-17 20:48+0300\n"
"PO-Revision-Date: 2026-09-17 19:42+0300\n" "PO-Revision-Date: 2026-09-17 20:48+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -425,6 +425,9 @@ msgstr "Все сервисы"
msgid "All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions." msgid "All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions."
msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия." msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия."
msgid "allocated dynamically on demand, not upfront"
msgstr "выделяется динамически по мере запросов, а не сразу"
msgid "Allow / Enable ONLY in scheduled hours (White period)" msgid "Allow / Enable ONLY in scheduled hours (White period)"
msgstr "Разрешать / Включать ТОЛЬКО в указанные часы (Белый список)" msgstr "Разрешать / Включать ТОЛЬКО в указанные часы (Белый список)"
@@ -863,6 +866,9 @@ msgstr "Обходит UDP-трафик для выбранных игровых
msgid "CA Certificate" msgid "CA Certificate"
msgstr "CA-сертификат" msgstr "CA-сертификат"
msgid "Cache"
msgstr ""
msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive." msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive."
msgstr "Кэшировать все встроенные списки и базы данных, даже если они не выбраны ни в одной секции. Рекомендуется при большом объёме хранилища или USB-накопителе." msgstr "Кэшировать все встроенные списки и базы данных, даже если они не выбраны ни в одной секции. Рекомендуется при большом объёме хранилища или USB-накопителе."
@@ -1439,8 +1445,8 @@ msgstr "Автоподбор и бенчмарк DNS"
msgid "DNS Blocking" msgid "DNS Blocking"
msgstr "Блокировка DNS" msgstr "Блокировка DNS"
msgid "DNS Cache Size" msgid "DNS Cache Size (entries)"
msgstr "Размер кэша DNS" msgstr "Размер DNS-кэша (записей)"
msgid "DNS Check Interval" msgid "DNS Check Interval"
msgstr "Интервал проверки DNS" msgstr "Интервал проверки DNS"
@@ -1874,6 +1880,9 @@ msgstr "Ошибка выполнения трассировки:"
msgid "Error updating lists:" msgid "Error updating lists:"
msgstr "Ошибка обновления списков:" msgstr "Ошибка обновления списков:"
msgid "Estimated RAM usage (max):"
msgstr "Ориентировочный объём RAM (макс.):"
msgid "Every day" msgid "Every day"
msgstr "Каждый день" msgstr "Каждый день"
@@ -3104,6 +3113,9 @@ msgstr "Локальный адрес"
msgid "Local DNS Cache" msgid "Local DNS Cache"
msgstr "Локальный кэш DNS" msgstr "Локальный кэш DNS"
msgid "Local DNS caching disabled (0 entries)"
msgstr "Локальное DNS-кэширование отключено (0 записей)"
msgid "Local SOCKS proxy desynchronizer (ciadpi)" msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)" msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
@@ -3176,6 +3188,9 @@ msgstr "Максимально допустимый суммарный траф
msgid "Maximum allowed online time per guest device per day. When reached, access is blocked until midnight. Enter 0 for unlimited." msgid "Maximum allowed online time per guest device per day. When reached, access is blocked until midnight. Enter 0 for unlimited."
msgstr "Максимально допустимое время доступа в интернет на одно гостевое устройство в день. При исчерпании доступ блокируется до полуночи. Введите 0 для отсутствия ограничений." msgstr "Максимально допустимое время доступа в интернет на одно гостевое устройство в день. При исчерпании доступ блокируется до полуночи. Введите 0 для отсутствия ограничений."
msgid "Maximum number of cached domain entries in dnsmasq (default: 10000 ≈ 1.2 MB RAM). Memory is allocated dynamically per request (~120 B per domain), not pre-allocated upfront. Recommended: 10000 for routers with ≥128 MB RAM, 2500–5000 for 64 MB devices."
msgstr "Максимальный лимит кэшируемых доменов в dnsmasq (по умолчанию: 10000 ≈ 1.2 МБ RAM). Память выделяется динамически по мере запросов (~120 Б на домен), а не резервируется сразу. Рекомендуется: 10000 для роутеров от 128 МБ RAM, 2500–5000 для 64 МБ устройств."
msgid "Maximum size of junk packets" msgid "Maximum size of junk packets"
msgstr "Максимальный размер мусорных пакетов" msgstr "Максимальный размер мусорных пакетов"
@@ -3479,9 +3494,6 @@ msgstr "Уведомлять о перезапусках сервиса"
msgid "Notify on Subscription Updates" msgid "Notify on Subscription Updates"
msgstr "Уведомлять об обновлениях подписок" msgstr "Уведомлять об обновлениях подписок"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
msgid "Number of consecutive failed checks required before switching DNS servers." msgid "Number of consecutive failed checks required before switching DNS servers."
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов." msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
@@ -5990,6 +6002,12 @@ msgstr "Правила маршрутизации sing-box для Zapret2 не
msgid "Zero config mesh VPN daemon" msgid "Zero config mesh VPN daemon"
msgstr "Mesh VPN-демон с нулевой конфигурацией" msgstr "Mesh VPN-демон с нулевой конфигурацией"
msgid "DNS Cache Size"
msgstr "Размер кэша DNS"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported." msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только правила доменов." msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только правила доменов."
File diff suppressed because it is too large Load Diff
+15
View File
@@ -1109,6 +1109,10 @@ function process_memory_rss_mb(process_name) {
return total_kb > 0 ? int(total_kb / 1024) : 0; return total_kb > 0 ? int(total_kb / 1024) : 0;
} }
function truthy(value) {
return value == "1" || value == "true" || value == "on" || value == "yes";
}
function current_ui_state_json() { function current_ui_state_json() {
maybe_refresh_action_dirs(false); maybe_refresh_action_dirs(false);
@@ -1132,6 +1136,11 @@ function current_ui_state_json() {
let sing_box_rss = sing_box_is_running ? process_memory_rss_mb("sing-box") : 0; let sing_box_rss = sing_box_is_running ? process_memory_rss_mb("sing-box") : 0;
let zapret2_rss = file_executable(ZAPRET2_PROVIDER_NFQWS2_BIN) ? process_memory_rss_mb("nfqws2") : 0; let zapret2_rss = file_executable(ZAPRET2_PROVIDER_NFQWS2_BIN) ? process_memory_rss_mb("nfqws2") : 0;
let dnsmasq_pids = get_process_pids("dnsmasq");
let dnsmasq_is_running = length(dnsmasq_pids) > 0 ? 1 : 0;
let dnsmasq_rss = dnsmasq_is_running ? process_memory_rss_mb("dnsmasq") : 0;
let dns_local_cache = truthy(uci_core.get(CONFIG_NAME + ".settings.dns_local_cache"));
let dns_cache_size = dns_local_cache ? int(uci_core.get(CONFIG_NAME + ".settings.dns_cache_size") || 10000) : 0;
write_json({ write_json({
service: { service: {
@@ -1150,6 +1159,12 @@ function current_ui_state_json() {
zapret2: { zapret2: {
running: zapret2_rss > 0 ? 1 : 0, running: zapret2_rss > 0 ? 1 : 0,
memory_rss_mb: zapret2_rss memory_rss_mb: zapret2_rss
},
dnsmasq: {
running: dnsmasq_is_running,
memory_rss_mb: dnsmasq_rss,
local_cache_enabled: dns_local_cache ? 1 : 0,
cache_size: dns_cache_size
} }
}, },
capabilities, capabilities,
+106 -45
View File
@@ -1716,11 +1716,7 @@ function outbound_supports_udp(config, tag_name, visited) {
return false; return false;
} }
function apply_excluded_source_ips(rule, excluded_cidrs) { const ACTION_KEYS = {
if (!rule || !excluded_cidrs || length(excluded_cidrs) == 0)
return rule;
let action_keys = {
action: true, action: true,
outbound: true, outbound: true,
server: true, server: true,
@@ -1734,6 +1730,68 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
strategy: true strategy: true
}; };
function apply_section_geoip_filter(rule, geo_tags, country_mode) {
if (!rule || !geo_tags || length(geo_tags) == 0)
return rule;
let geo_rule = {
rule_set: single_or_array(geo_tags)
};
if (country_mode == "exclude")
geo_rule.invert = true;
if (rule.type == "logical" && rule.mode == "and" && type(rule.rules) == "array") {
let cloned = {};
for (let k, v in rule) {
if (k == "rules") {
cloned.rules = [];
for (let r in v) {
if (r && length(keys(r)) > 0)
push(cloned.rules, r);
}
push(cloned.rules, geo_rule);
} else {
cloned[k] = v;
}
}
return cloned;
}
let match_part = {};
for (let k, v in rule) {
if (!ACTION_KEYS[k] && substr(k, 0, 2) != "__")
match_part[k] = v;
}
if (length(keys(match_part)) == 0) {
let single_rule = {};
for (let k, v in rule)
single_rule[k] = v;
single_rule.rule_set = single_or_array(geo_tags);
if (country_mode == "exclude")
single_rule.invert = true;
return single_rule;
}
let logical_rule = {
type: "logical",
mode: "and",
rules: [
match_part,
geo_rule
]
};
for (let k, v in rule) {
if (ACTION_KEYS[k] || substr(k, 0, 2) == "__")
logical_rule[k] = v;
}
return logical_rule;
}
function apply_excluded_source_ips(rule, excluded_cidrs) {
if (!rule || !excluded_cidrs || length(excluded_cidrs) == 0)
return rule;
if (rule.type == "logical" && rule.mode == "and" && type(rule.rules) == "array") { if (rule.type == "logical" && rule.mode == "and" && type(rule.rules) == "array") {
let cloned = {}; let cloned = {};
for (let k, v in rule) { for (let k, v in rule) {
@@ -1756,7 +1814,7 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
let match_part = {}; let match_part = {};
for (let k, v in rule) { for (let k, v in rule) {
if (!action_keys[k] && substr(k, 0, 2) != "__") if (!ACTION_KEYS[k] && substr(k, 0, 2) != "__")
match_part[k] = v; match_part[k] = v;
} }
@@ -1781,22 +1839,27 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
] ]
}; };
for (let k, v in rule) { for (let k, v in rule) {
if (action_keys[k] || substr(k, 0, 2) == "__") if (ACTION_KEYS[k] || substr(k, 0, 2) == "__")
logical_rule[k] = v; logical_rule[k] = v;
} }
return logical_rule; return logical_rule;
} }
function push_section_route_rule(config, rule, target_outbound, excluded_cidrs) { function push_section_route_rule(config, rule, target_outbound, excluded_cidrs, geo_tags, country_mode) {
let apply_filters = function(r) {
let filtered = apply_section_geoip_filter(r, geo_tags, country_mode);
return apply_excluded_source_ips(filtered, excluded_cidrs);
};
if (target_outbound && !outbound_supports_udp(config, target_outbound)) { if (target_outbound && !outbound_supports_udp(config, target_outbound)) {
if (rule.network == "tcp") { if (rule.network == "tcp") {
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs)); push_route_matcher_rule(config, apply_filters(rule));
return; return;
} }
if (rule.network == "udp") { if (rule.network == "udp") {
delete rule.outbound; delete rule.outbound;
rule.action = "reject"; rule.action = "reject";
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs)); push_route_matcher_rule(config, apply_filters(rule));
return; return;
} }
let udp_rule = {}; let udp_rule = {};
@@ -1805,11 +1868,11 @@ function push_section_route_rule(config, rule, target_outbound, excluded_cidrs)
delete udp_rule.outbound; delete udp_rule.outbound;
udp_rule.action = "reject"; udp_rule.action = "reject";
udp_rule.network = "udp"; udp_rule.network = "udp";
push_route_matcher_rule(config, apply_excluded_source_ips(udp_rule, excluded_cidrs)); push_route_matcher_rule(config, apply_filters(udp_rule));
rule.network = "tcp"; rule.network = "tcp";
} }
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs)); push_route_matcher_rule(config, apply_filters(rule));
} }
function add_fully_routed_ips_rule(config, section) { function add_fully_routed_ips_rule(config, section) {
@@ -1983,6 +2046,23 @@ function add_combined_route_for_section(config, section) {
if (target.unsupported) if (target.unsupported)
ctx.runtime_generate_unsupported(target.unsupported); ctx.runtime_generate_unsupported(target.unsupported);
let country_list = connections.geoip_country_list(section);
let country_mode = connections.geoip_country_mode(section);
let geo_tags = [];
if ((target.outbound || target.action) && length(country_list) > 0) {
for (let cc in country_list) {
let ip_ruleset = ensure_community_ruleset(config, section_name, "geoip_" + cc);
if (ip_ruleset && ip_ruleset.tag)
push(geo_tags, ip_ruleset.tag);
if (cc == "ru") {
let site_ruleset = ensure_community_ruleset(config, section_name, "geosite_ru");
if (site_ruleset && site_ruleset.tag)
push(geo_tags, site_ruleset.tag);
}
}
}
let has_domain = length(domain) > 0 || length(domain_suffix) > 0 || let has_domain = length(domain) > 0 || length(domain_suffix) > 0 ||
length(domain_keyword) > 0 || length(domain_regex) > 0; length(domain_keyword) > 0 || length(domain_regex) > 0;
let has_ruleset = length(rule_set_tags) > 0; let has_ruleset = length(rule_set_tags) > 0;
@@ -2016,7 +2096,7 @@ function add_combined_route_for_section(config, section) {
else if (type(resolve) == "object" && resolve.rule) else if (type(resolve) == "object" && resolve.rule)
push_route_matcher_rule(config, apply_excluded_source_ips(resolve.rule, excluded_cidrs)); push_route_matcher_rule(config, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_section_route_rule(config, domain_rule, target.outbound, excluded_cidrs); push_section_route_rule(config, domain_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
} }
if (has_ruleset) { if (has_ruleset) {
@@ -2029,13 +2109,13 @@ function add_combined_route_for_section(config, section) {
else if (type(resolve) == "object" && resolve.rule) else if (type(resolve) == "object" && resolve.rule)
push_route_matcher_rule(config, apply_excluded_source_ips(resolve.rule, excluded_cidrs)); push_route_matcher_rule(config, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_section_route_rule(config, rs_rule, target.outbound, excluded_cidrs); push_section_route_rule(config, rs_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
} }
if (has_ip_cidr) { if (has_ip_cidr) {
let ip_rule = create_section_route_rule(); let ip_rule = create_section_route_rule();
ip_rule.ip_cidr = ip_cidr; ip_rule.ip_cidr = ip_cidr;
push_section_route_rule(config, ip_rule, target.outbound, excluded_cidrs); push_section_route_rule(config, ip_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
} }
if (length(discord_cf_subnets) > 0) { if (length(discord_cf_subnets) > 0) {
@@ -2043,41 +2123,19 @@ function add_combined_route_for_section(config, section) {
voice_rule.network = "udp"; voice_rule.network = "udp";
voice_rule.ip_cidr = discord_cf_subnets; voice_rule.ip_cidr = discord_cf_subnets;
voice_rule.port_range = core_ip.DISCORD_VOICE_PORT_RANGES || [ "5000:5020", "3478:3478", "19294:19344", "50000:65535" ]; voice_rule.port_range = core_ip.DISCORD_VOICE_PORT_RANGES || [ "5000:5020", "3478:3478", "19294:19344", "50000:65535" ];
push_section_route_rule(config, voice_rule, target.outbound, excluded_cidrs); push_section_route_rule(config, voice_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
}
let country_list = connections.geoip_country_list(section);
let country_mode = connections.geoip_country_mode(section);
if (target.outbound && length(country_list) > 0) {
let geo_tags = [];
for (let cc in country_list) {
let ip_ruleset = ensure_community_ruleset(config, section_name, "geoip_" + cc);
if (ip_ruleset && ip_ruleset.tag)
push(geo_tags, ip_ruleset.tag);
if (cc == "ru") {
let site_ruleset = ensure_community_ruleset(config, section_name, "geosite_ru");
if (site_ruleset && site_ruleset.tag)
push(geo_tags, site_ruleset.tag);
}
}
if (length(geo_tags) > 0) {
let geoip_route_rule = create_section_route_rule();
geoip_route_rule.rule_set = geo_tags;
if (country_mode == "exclude")
geoip_route_rule.invert = true;
push_section_route_rule(config, geoip_route_rule, target.outbound, excluded_cidrs);
}
} }
if (bool_option(section, "match_all", false)) { if (bool_option(section, "match_all", false)) {
let all_rule = create_section_route_rule(); let all_rule = create_section_route_rule();
push_section_route_rule(config, all_rule, target.outbound, excluded_cidrs); push_section_route_rule(config, all_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
} }
else if (!has_domain && !has_ruleset && !has_ip_cidr && length(discord_cf_subnets) == 0 && length(country_list) == 0) { else if (!has_domain && !has_ruleset && !has_ip_cidr && length(discord_cf_subnets) == 0) {
if (length(geo_tags) > 0) {
let geoip_route_rule = create_section_route_rule();
push_section_route_rule(config, geoip_route_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
}
else {
let fallback_rule = create_section_route_rule(); let fallback_rule = create_section_route_rule();
let has_any_matcher = fallback_rule.source_ip_cidr != null || let has_any_matcher = fallback_rule.source_ip_cidr != null ||
fallback_rule.port != null || fallback_rule.port_range != null || fallback_rule.port != null || fallback_rule.port_range != null ||
@@ -2085,6 +2143,7 @@ function add_combined_route_for_section(config, section) {
if (has_any_matcher) if (has_any_matcher)
push_section_route_rule(config, fallback_rule, target.outbound, excluded_cidrs); push_section_route_rule(config, fallback_rule, target.outbound, excluded_cidrs);
} }
}
let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60"); let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60");
if (length(domain) > 0 || length(domain_suffix) > 0 || length(domain_keyword) > 0 || length(domain_regex) > 0) { if (length(domain) > 0 || length(domain_suffix) > 0 || length(domain_keyword) > 0 || length(domain_regex) > 0) {
@@ -2568,6 +2627,8 @@ return {
add_server_routes, add_server_routes,
outbound_supports_udp, outbound_supports_udp,
push_section_route_rule, push_section_route_rule,
apply_section_geoip_filter,
apply_excluded_source_ips,
is_valid_detour, is_valid_detour,
load_community_subnet_cidrs load_community_subnet_cidrs
}; };
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
cat >"$WORK_DIR/fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1",
"dns_type": "udp",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "sec_yt",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_yt-out\"}" ],
"domain_suffix": [ "youtube.com" ],
"geoip_country": [ "ru" ],
"geoip_mode": "exclude"
},
{
".name": "sec_all_noru",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_noru-out\"}" ],
"match_all": "1",
"geoip_country": [ "ru" ],
"geoip_mode": "exclude"
},
{
".name": "sec_pure_ru",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_ru-out\"}" ],
"geoip_country": [ "ru" ],
"geoip_mode": "include"
},
{
".name": "sec_sub",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_sub-out\"}" ],
"domain_suffix": [ "other.com" ]
},
{
".name": "sec_both",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_both-out\"}" ],
"domain_suffix": [ "twitch.tv" ],
"geoip_country": [ "ru" ],
"geoip_mode": "exclude",
"excluded_ips": [ "192.168.1.99" ]
}
]
}
JSON
output="$WORK_DIR/out.json"
mkdir -p "$output.section-cache" "$output.rulesets"
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture.json" "$output" "127.0.0.1" "0" "1"
# Verify route rules in out.json
ucode -e '
let fs = require("fs");
let raw = fs.readfile("'"$output"'");
let cfg = json(raw);
let rules = cfg.route.rules || [];
let sec_yt_logical_found = false;
let sec_yt_unscoped_found = false;
let sec_noru_rule_count = 0;
let sec_noru_inverted_found = false;
let sec_noru_unconstrained_all = false;
let sec_pure_ru_found = false;
let sec_sub_found = false;
let sec_both_found = false;
for (let r in rules) {
if (r.domain == "ip.podkop.fyi" || (type(r.domain) == "array" && index(r.domain, "ip.podkop.fyi") >= 0))
continue;
// 1. Check sec_yt: Should be a logical rule combining domain_suffix and inverted geoip/geosite rule_set
if (r.outbound == "sec_yt-out") {
if (r.type == "logical" && r.mode == "and" && type(r.rules) == "array") {
let has_domain = false;
let has_inverted_geo = false;
for (let sub in r.rules) {
if (sub.domain_suffix && index(sub.domain_suffix, "youtube.com") >= 0)
has_domain = true;
if (sub.invert == true && sub.rule_set) {
let rs = type(sub.rule_set) == "array" ? sub.rule_set : [ sub.rule_set ];
for (let tag in rs) {
if (index(tag, "sec_yt-geoip_ru") >= 0 || index(tag, "sec_yt-geosite_ru") >= 0)
has_inverted_geo = true;
}
}
}
if (has_domain && has_inverted_geo)
sec_yt_logical_found = true;
} else {
// A non-logical rule for sec_yt-out that is unscoped
sec_yt_unscoped_found = true;
}
}
// 2. Check sec_all_noru: Should have an inverted rule_set and NOT have an unconstrained all-traffic rule
if (r.outbound == "sec_all_noru-out") {
sec_noru_rule_count++;
if (r.type == "logical" && r.mode == "and") {
for (let sub in r.rules) {
if (sub.invert == true && sub.rule_set)
sec_noru_inverted_found = true;
}
} else if (r.invert == true && r.rule_set) {
sec_noru_inverted_found = true;
} else if (!r.domain && !r.domain_suffix && !r.rule_set && !r.ip_cidr && !r.source_ip_cidr) {
sec_noru_unconstrained_all = true;
}
}
// 3. Check sec_pure_ru: Pure GeoIP section without domain
if (r.outbound == "sec_pure_ru-out") {
if (r.type == "logical" && r.mode == "and") {
for (let sub in r.rules) {
if (!sub.invert && sub.rule_set)
sec_pure_ru_found = true;
}
} else if (r.rule_set && !r.invert) {
sec_pure_ru_found = true;
}
}
// 4. Check sec_sub
if (r.outbound == "sec_sub-out") {
sec_sub_found = true;
}
// 5. Check sec_both: Single logical rule combining domain, inverted GeoIP, AND inverted client IP
if (r.outbound == "sec_both-out") {
if (r.type == "logical" && r.mode == "and" && type(r.rules) == "array") {
let has_domain = false;
let has_inverted_geo = false;
let has_inverted_src = false;
for (let sub in r.rules) {
if (sub.domain_suffix && index(sub.domain_suffix, "twitch.tv") >= 0)
has_domain = true;
if (sub.invert == true && sub.rule_set)
has_inverted_geo = true;
if (sub.invert == true && (sub.source_ip_cidr == "192.168.1.99/32" || (type(sub.source_ip_cidr) == "array" && index(sub.source_ip_cidr, "192.168.1.99/32") >= 0)))
has_inverted_src = true;
}
if (has_domain && has_inverted_geo && has_inverted_src)
sec_both_found = true;
}
}
}
if (!sec_yt_logical_found)
die("Expected scoped logical rule for sec_yt-out (youtube.com AND NOT ru) not found!\n");
if (sec_yt_unscoped_found)
die("Found unwanted unscoped rule for sec_yt-out!\n");
if (!sec_noru_inverted_found)
die("Expected inverted geoip rule for sec_noru-out not found!\n");
if (sec_noru_unconstrained_all)
die("Found unconstrained all-traffic rule for sec_noru-out that negates GeoIP exclusion!\n");
if (!sec_pure_ru_found)
die("Expected GeoIP include rule for sec_ru-out not found!\n");
if (!sec_sub_found)
die("sec_sub-out rule not found!\n");
if (!sec_both_found)
die("Expected combined logical rule for sec_both-out (domain + inverted geoip + inverted client ip) not found!\n");
' || fail "singbox_section_geoip verification failed"
printf 'PASS: singbox_section_geoip\n'