feat: локализация исключений GeoIP по секциям и мониторинг памяти DNS-кэша dnsmasq
This commit is contained in:
+1110
-1076
File diff suppressed because it is too large
Load Diff
+1094
-1072
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-17 19:42+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 19:42+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 20:48+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 20:48+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\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."
|
||||
msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия."
|
||||
|
||||
msgid "allocated dynamically on demand, not upfront"
|
||||
msgstr "выделяется динамически по мере запросов, а не сразу"
|
||||
|
||||
msgid "Allow / Enable ONLY in scheduled hours (White period)"
|
||||
msgstr "Разрешать / Включать ТОЛЬКО в указанные часы (Белый список)"
|
||||
|
||||
@@ -863,6 +866,9 @@ msgstr "Обходит UDP-трафик для выбранных игровых
|
||||
msgid "CA Certificate"
|
||||
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."
|
||||
msgstr "Кэшировать все встроенные списки и базы данных, даже если они не выбраны ни в одной секции. Рекомендуется при большом объёме хранилища или USB-накопителе."
|
||||
|
||||
@@ -1439,8 +1445,8 @@ msgstr "Автоподбор и бенчмарк DNS"
|
||||
msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS Cache Size"
|
||||
msgstr "Размер кэша DNS"
|
||||
msgid "DNS Cache Size (entries)"
|
||||
msgstr "Размер DNS-кэша (записей)"
|
||||
|
||||
msgid "DNS Check Interval"
|
||||
msgstr "Интервал проверки DNS"
|
||||
@@ -1874,6 +1880,9 @@ msgstr "Ошибка выполнения трассировки:"
|
||||
msgid "Error updating lists:"
|
||||
msgstr "Ошибка обновления списков:"
|
||||
|
||||
msgid "Estimated RAM usage (max):"
|
||||
msgstr "Ориентировочный объём RAM (макс.):"
|
||||
|
||||
msgid "Every day"
|
||||
msgstr "Каждый день"
|
||||
|
||||
@@ -3104,6 +3113,9 @@ msgstr "Локальный адрес"
|
||||
msgid "Local DNS Cache"
|
||||
msgstr "Локальный кэш DNS"
|
||||
|
||||
msgid "Local DNS caching disabled (0 entries)"
|
||||
msgstr "Локальное DNS-кэширование отключено (0 записей)"
|
||||
|
||||
msgid "Local SOCKS proxy desynchronizer (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."
|
||||
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"
|
||||
msgstr "Максимальный размер мусорных пакетов"
|
||||
|
||||
@@ -3479,9 +3494,6 @@ msgstr "Уведомлять о перезапусках сервиса"
|
||||
msgid "Notify on Subscription Updates"
|
||||
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."
|
||||
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
|
||||
|
||||
@@ -5990,6 +6002,12 @@ msgstr "Правила маршрутизации sing-box для Zapret2 не
|
||||
msgid "Zero config mesh VPN daemon"
|
||||
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."
|
||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только правила доменов."
|
||||
|
||||
|
||||
@@ -76,6 +76,10 @@ export async function fetchServicesInfo() {
|
||||
: previousData.watchdogRunning,
|
||||
zapret2Running: previousData.zapret2Running,
|
||||
zapret2MemoryMb: previousData.zapret2MemoryMb,
|
||||
dnsmasqRunning: previousData.dnsmasqRunning,
|
||||
dnsmasqMemoryMb: previousData.dnsmasqMemoryMb,
|
||||
dnsmasqLocalCacheEnabled: previousData.dnsmasqLocalCacheEnabled,
|
||||
dnsmasqCacheSize: previousData.dnsmasqCacheSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -170,6 +170,10 @@ export interface StoreType {
|
||||
singboxMemoryMb?: number;
|
||||
zapret2Running?: number;
|
||||
zapret2MemoryMb?: number;
|
||||
dnsmasqRunning?: number;
|
||||
dnsmasqMemoryMb?: number;
|
||||
dnsmasqLocalCacheEnabled?: number;
|
||||
dnsmasqCacheSize?: number;
|
||||
};
|
||||
};
|
||||
tailscaleWidget: {
|
||||
|
||||
@@ -129,6 +129,18 @@ function applyServiceState(uiState: Tachyon.UiState) {
|
||||
zapret2MemoryMb: uiState.service.zapret2
|
||||
? uiState.service.zapret2.memory_rss_mb
|
||||
: 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),
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
'renderServicesInfoWidget',
|
||||
|
||||
@@ -749,6 +749,12 @@ export namespace Tachyon {
|
||||
running: number;
|
||||
memory_rss_mb?: number;
|
||||
};
|
||||
dnsmasq?: {
|
||||
running: number;
|
||||
memory_rss_mb?: number;
|
||||
local_cache_enabled?: number;
|
||||
cache_size?: number;
|
||||
};
|
||||
};
|
||||
capabilities: GetUiCapabilities;
|
||||
actions: {
|
||||
|
||||
@@ -6772,7 +6772,11 @@ function applyServiceState(uiState) {
|
||||
tachyonStatus: uiState.service.tachyon.status,
|
||||
watchdogRunning: store.get().servicesInfoWidget.data.watchdogRunning,
|
||||
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)
|
||||
@@ -7420,7 +7424,11 @@ async function fetchServicesInfo() {
|
||||
tachyonStatus: tachyon.success ? tachyon.data.status : previousData.tachyonStatus,
|
||||
watchdogRunning: watchdog.success ? Number(watchdog.data.running) : previousData.watchdogRunning,
|
||||
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;
|
||||
},
|
||||
"renderServicesInfoWidget"
|
||||
|
||||
@@ -1869,14 +1869,75 @@ function createSettingsContent(section, capabilities) {
|
||||
"dns",
|
||||
form.Value,
|
||||
"dns_cache_size",
|
||||
_("DNS Cache Size"),
|
||||
_("Number of cached DNS domain entries in dnsmasq (default: 10000)."),
|
||||
_("DNS Cache Size (entries)"),
|
||||
_(
|
||||
"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.default = "10000";
|
||||
o.datatype = "uinteger";
|
||||
o.placeholder = "10000";
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
o = section.taboption(
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-17 19:42+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 19:42+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 20:48+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 20:48+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\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."
|
||||
msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия."
|
||||
|
||||
msgid "allocated dynamically on demand, not upfront"
|
||||
msgstr "выделяется динамически по мере запросов, а не сразу"
|
||||
|
||||
msgid "Allow / Enable ONLY in scheduled hours (White period)"
|
||||
msgstr "Разрешать / Включать ТОЛЬКО в указанные часы (Белый список)"
|
||||
|
||||
@@ -863,6 +866,9 @@ msgstr "Обходит UDP-трафик для выбранных игровых
|
||||
msgid "CA Certificate"
|
||||
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."
|
||||
msgstr "Кэшировать все встроенные списки и базы данных, даже если они не выбраны ни в одной секции. Рекомендуется при большом объёме хранилища или USB-накопителе."
|
||||
|
||||
@@ -1439,8 +1445,8 @@ msgstr "Автоподбор и бенчмарк DNS"
|
||||
msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS Cache Size"
|
||||
msgstr "Размер кэша DNS"
|
||||
msgid "DNS Cache Size (entries)"
|
||||
msgstr "Размер DNS-кэша (записей)"
|
||||
|
||||
msgid "DNS Check Interval"
|
||||
msgstr "Интервал проверки DNS"
|
||||
@@ -1874,6 +1880,9 @@ msgstr "Ошибка выполнения трассировки:"
|
||||
msgid "Error updating lists:"
|
||||
msgstr "Ошибка обновления списков:"
|
||||
|
||||
msgid "Estimated RAM usage (max):"
|
||||
msgstr "Ориентировочный объём RAM (макс.):"
|
||||
|
||||
msgid "Every day"
|
||||
msgstr "Каждый день"
|
||||
|
||||
@@ -3104,6 +3113,9 @@ msgstr "Локальный адрес"
|
||||
msgid "Local DNS Cache"
|
||||
msgstr "Локальный кэш DNS"
|
||||
|
||||
msgid "Local DNS caching disabled (0 entries)"
|
||||
msgstr "Локальное DNS-кэширование отключено (0 записей)"
|
||||
|
||||
msgid "Local SOCKS proxy desynchronizer (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."
|
||||
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"
|
||||
msgstr "Максимальный размер мусорных пакетов"
|
||||
|
||||
@@ -3479,9 +3494,6 @@ msgstr "Уведомлять о перезапусках сервиса"
|
||||
msgid "Notify on Subscription Updates"
|
||||
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."
|
||||
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
|
||||
|
||||
@@ -5990,6 +6002,12 @@ msgstr "Правила маршрутизации sing-box для Zapret2 не
|
||||
msgid "Zero config mesh VPN daemon"
|
||||
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."
|
||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только правила доменов."
|
||||
|
||||
|
||||
+1094
-1072
File diff suppressed because it is too large
Load Diff
@@ -1109,6 +1109,10 @@ function process_memory_rss_mb(process_name) {
|
||||
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() {
|
||||
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 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({
|
||||
service: {
|
||||
@@ -1150,6 +1159,12 @@ function current_ui_state_json() {
|
||||
zapret2: {
|
||||
running: zapret2_rss > 0 ? 1 : 0,
|
||||
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,
|
||||
|
||||
@@ -1716,11 +1716,7 @@ function outbound_supports_udp(config, tag_name, visited) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function apply_excluded_source_ips(rule, excluded_cidrs) {
|
||||
if (!rule || !excluded_cidrs || length(excluded_cidrs) == 0)
|
||||
return rule;
|
||||
|
||||
let action_keys = {
|
||||
const ACTION_KEYS = {
|
||||
action: true,
|
||||
outbound: true,
|
||||
server: true,
|
||||
@@ -1734,6 +1730,68 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
|
||||
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") {
|
||||
let cloned = {};
|
||||
for (let k, v in rule) {
|
||||
@@ -1756,7 +1814,7 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
|
||||
|
||||
let match_part = {};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1781,22 +1839,27 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
|
||||
]
|
||||
};
|
||||
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;
|
||||
}
|
||||
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 (rule.network == "tcp") {
|
||||
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs));
|
||||
push_route_matcher_rule(config, apply_filters(rule));
|
||||
return;
|
||||
}
|
||||
if (rule.network == "udp") {
|
||||
delete rule.outbound;
|
||||
rule.action = "reject";
|
||||
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs));
|
||||
push_route_matcher_rule(config, apply_filters(rule));
|
||||
return;
|
||||
}
|
||||
let udp_rule = {};
|
||||
@@ -1805,11 +1868,11 @@ function push_section_route_rule(config, rule, target_outbound, excluded_cidrs)
|
||||
delete udp_rule.outbound;
|
||||
udp_rule.action = "reject";
|
||||
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";
|
||||
}
|
||||
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) {
|
||||
@@ -1983,6 +2046,23 @@ function add_combined_route_for_section(config, section) {
|
||||
if (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 ||
|
||||
length(domain_keyword) > 0 || length(domain_regex) > 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)
|
||||
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) {
|
||||
@@ -2029,13 +2109,13 @@ function add_combined_route_for_section(config, section) {
|
||||
else if (type(resolve) == "object" && resolve.rule)
|
||||
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) {
|
||||
let ip_rule = create_section_route_rule();
|
||||
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) {
|
||||
@@ -2043,41 +2123,19 @@ function add_combined_route_for_section(config, section) {
|
||||
voice_rule.network = "udp";
|
||||
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" ];
|
||||
push_section_route_rule(config, voice_rule, target.outbound, excluded_cidrs);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
push_section_route_rule(config, voice_rule, target.outbound, excluded_cidrs, geo_tags, country_mode);
|
||||
}
|
||||
|
||||
if (bool_option(section, "match_all", false)) {
|
||||
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 has_any_matcher = fallback_rule.source_ip_cidr != 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)
|
||||
push_section_route_rule(config, fallback_rule, target.outbound, excluded_cidrs);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -2568,6 +2627,8 @@ return {
|
||||
add_server_routes,
|
||||
outbound_supports_udp,
|
||||
push_section_route_rule,
|
||||
apply_section_geoip_filter,
|
||||
apply_excluded_source_ips,
|
||||
is_valid_detour,
|
||||
load_community_subnet_cidrs
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user