Merge upstream 383b426e (GeoIP localization + dnsmasq DNS-cache monitoring)

Keep both: upstream RAM-usage calculator for dns_cache_size and the fork
AdGuard Home DNS mode options (dns_main_mode / dns_adguard_failover_enabled).
This commit is contained in:
2026-09-17 21:52:45 +03:00
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 ""
"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: {
@@ -135,6 +135,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',
+6
View File
@@ -756,6 +756,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: {
@@ -6792,7 +6792,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)
@@ -7440,7 +7444,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
}
}
});
@@ -9259,6 +9267,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"
@@ -1876,14 +1876,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;
};
// ─── AdGuard Home main DNS mode ──────────────────────────────────────────
o = section.taboption(
+25 -7
View File
@@ -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. Поддерживаются только правила доменов."
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;
}
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,
+121 -60
View File
@@ -1768,24 +1768,82 @@ function outbound_supports_udp(config, tag_name, visited) {
return false;
}
const ACTION_KEYS = {
action: true,
outbound: true,
server: true,
rewrite_ttl: true,
client_subnet: true,
disable_cache: true,
disable_optimistic_cache: true,
timeout: true,
method: true,
no_drop: 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;
let action_keys = {
action: true,
outbound: true,
server: true,
rewrite_ttl: true,
client_subnet: true,
disable_cache: true,
disable_optimistic_cache: true,
timeout: true,
method: true,
no_drop: true,
strategy: true
};
if (rule.type == "logical" && rule.mode == "and" && type(rule.rules) == "array") {
let cloned = {};
for (let k, v in rule) {
@@ -1808,7 +1866,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;
}
@@ -1833,22 +1891,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 = {};
@@ -1857,11 +1920,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) {
@@ -2035,6 +2098,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;
@@ -2068,7 +2148,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) {
@@ -2081,13 +2161,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) {
@@ -2095,47 +2175,26 @@ 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) {
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 ||
fallback_rule.protocol != null || fallback_rule.dscp != null;
if (has_any_matcher)
push_section_route_rule(config, fallback_rule, target.outbound, excluded_cidrs);
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 ||
fallback_rule.protocol != null || fallback_rule.dscp != null;
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");
@@ -2620,6 +2679,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
};
+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'