Рефакторинг и оптимизация Tachyon: модульный генератор sing-box, атомарная запись файлов, graceful shutdown в watchdog, ускорение nftables, доработка трассировщика правил и локализация
This commit is contained in:
+584
-548
File diff suppressed because it is too large
Load Diff
+699
-677
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-07-19 21:37+0300\n"
|
||||
"PO-Revision-Date: 2026-07-19 21:37+0300\n"
|
||||
"POT-Creation-Date: 2026-07-22 21:48+0300\n"
|
||||
"PO-Revision-Date: 2026-07-22 21:48+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -179,6 +179,9 @@ msgstr "Все оставшиеся серверы"
|
||||
msgid "All remaining servers means every server not already assigned to a higher-priority level."
|
||||
msgstr "Все оставшиеся серверы — это все серверы, не назначенные на уровень с более высоким приоритетом."
|
||||
|
||||
msgid "All Routes"
|
||||
msgstr "Все маршруты"
|
||||
|
||||
msgid "All servers"
|
||||
msgstr "Все серверы"
|
||||
|
||||
@@ -368,6 +371,9 @@ msgstr "Проверить обновление"
|
||||
msgid "Check URL"
|
||||
msgstr "URL проверки"
|
||||
|
||||
msgid "checked"
|
||||
msgstr "проверено"
|
||||
|
||||
msgid "Checking Connection..."
|
||||
msgstr ""
|
||||
|
||||
@@ -398,6 +404,9 @@ msgstr "CIDR должен быть от 0 до 32 для IPv4 или от 0 до
|
||||
msgid "Cipher"
|
||||
msgstr "Шифрование"
|
||||
|
||||
msgid "Clear"
|
||||
msgstr "Сбросить"
|
||||
|
||||
msgid "Client Certificate"
|
||||
msgstr "Сертификат клиента"
|
||||
|
||||
@@ -731,6 +740,9 @@ msgstr "Дублирующееся значение"
|
||||
msgid "e.g. AES-256-GCM — leave empty for default"
|
||||
msgstr "например AES-256-GCM — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "e.g. google.com, 1.1.1.1, 443"
|
||||
msgstr "например: google.com, 1.1.1.1, 443"
|
||||
|
||||
msgid "e.g. SHA256 — leave empty for default"
|
||||
msgstr "например SHA256 — оставьте пустым для значения по умолчанию"
|
||||
|
||||
@@ -800,6 +812,9 @@ msgstr "Введите полный объект outbound для sing-box"
|
||||
msgid "Enter a display name"
|
||||
msgstr "Введите отображаемое имя"
|
||||
|
||||
msgid "Enter a domain, IP address, or port to test routing. The tracer will show which section intercepts the traffic and which rule matches."
|
||||
msgstr "Введите домен, IP-адрес или порт для проверки маршрутизации. Трассировщик покажет, в какую секцию попадет трафик и какое именно правило сработает."
|
||||
|
||||
msgid "Enter a level name"
|
||||
msgstr "Введите название уровня"
|
||||
|
||||
@@ -824,6 +839,9 @@ msgstr ""
|
||||
msgid "Error running generator:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error running trace:"
|
||||
msgstr "Ошибка при выполнении трассировки:"
|
||||
|
||||
msgid "Exclude by proxy parameters"
|
||||
msgstr "Исключить по параметрам прокси"
|
||||
|
||||
@@ -1385,6 +1403,9 @@ msgstr "Локальный адрес"
|
||||
msgid "Log Level"
|
||||
msgstr "Уровень логов"
|
||||
|
||||
msgid "lx"
|
||||
msgstr ""
|
||||
|
||||
msgid "Main DNS"
|
||||
msgstr "Основной DNS"
|
||||
|
||||
@@ -1409,6 +1430,12 @@ msgstr "Совпадение по IP или подсети назначения"
|
||||
msgid "Match destination ports. Use a single port or a range"
|
||||
msgstr "Совпадение по портам назначения. Используйте один порт или диапазон"
|
||||
|
||||
msgid "Match Found"
|
||||
msgstr "Совпадение найдено"
|
||||
|
||||
msgid "Matching Rule"
|
||||
msgstr "Сработавшее правило"
|
||||
|
||||
msgid "Maximum time to wait for example.com to resolve during a DNS health check."
|
||||
msgstr "Максимальное время ожидания разрешения example.com при проверке доступности DNS."
|
||||
|
||||
@@ -1493,6 +1520,9 @@ msgstr "Нет активных клиентов"
|
||||
msgid "No active connections"
|
||||
msgstr "Нет активных подключений"
|
||||
|
||||
msgid "No active routing section intercepts this traffic. Traffic will bypass the custom routing logic and be handled by the default route (Direct/Default)."
|
||||
msgstr "Этот трафик не перехватывается ни одной активной секцией маршрутизации. Он пойдет в обход кастомных правил и будет обработан маршрутом по умолчанию (Напрямую/Дефолт)."
|
||||
|
||||
msgid "No active routing sections found."
|
||||
msgstr "Активные секции маршрутизации не найдены."
|
||||
|
||||
@@ -1502,6 +1532,9 @@ msgstr "Нет закрытых подключений"
|
||||
msgid "No conflict detected"
|
||||
msgstr "Конфликт не обнаружен"
|
||||
|
||||
msgid "No Custom Rules Matched"
|
||||
msgstr "Пользовательские правила не совпали"
|
||||
|
||||
msgid "No domains detected yet"
|
||||
msgstr "Домены пока не обнаружены"
|
||||
|
||||
@@ -1634,6 +1667,9 @@ msgstr "Опциональные значения должны быть указ
|
||||
msgid "Optional values must be attached with '=' here; a separate token would be ignored by nfqws2."
|
||||
msgstr "Опциональные значения должны быть указаны через '=' здесь; отдельный токен будет проигнорирован nfqws2."
|
||||
|
||||
msgid "out of"
|
||||
msgstr "из"
|
||||
|
||||
msgid "outbounds must be a non-empty array of strings"
|
||||
msgstr "outbounds должен быть непустым массивом строк"
|
||||
|
||||
@@ -1856,12 +1892,18 @@ msgstr "Маршрутизировать запросы к этому DNS сер
|
||||
msgid "Routing mode"
|
||||
msgstr "Режим маршрутизации"
|
||||
|
||||
msgid "Routing Rule Tracer"
|
||||
msgstr "Трассировщик правил маршрутизации"
|
||||
|
||||
msgid "Routing rules"
|
||||
msgstr "Правила маршрутизации"
|
||||
|
||||
msgid "Routing section"
|
||||
msgstr "Секция маршрутизации"
|
||||
|
||||
msgid "Routing Section"
|
||||
msgstr "Секция маршрутизации"
|
||||
|
||||
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json path"
|
||||
msgstr "Набор правил должен быть HTTP(S) URL или локальным путём .srs / .json"
|
||||
|
||||
@@ -2432,6 +2474,12 @@ msgstr "Допуск"
|
||||
msgid "Top level has the highest priority; lower levels are used as fallback"
|
||||
msgstr "Верхний уровень имеет наивысший приоритет; нижние уровни используются как резерв"
|
||||
|
||||
msgid "Trace"
|
||||
msgstr "Проверить"
|
||||
|
||||
msgid "Tracing routing rules..."
|
||||
msgstr "Выполняется трассировка..."
|
||||
|
||||
msgid "Traffic"
|
||||
msgstr "Трафик"
|
||||
|
||||
@@ -2834,13 +2882,29 @@ msgstr "Принудительная маршрутизация по IP (Dest)"
|
||||
msgid "All traffic to these destination IPs will be routed through the section unconditionally, ignoring all other conditions."
|
||||
msgstr "Весь трафик к этим IP назначения будет направлен через секцию безусловно, игнорируя все остальные условия."
|
||||
|
||||
msgid "All Routes"
|
||||
msgstr "Все маршруты"
|
||||
msgid "DNS Failures Before Switching"
|
||||
msgstr "Ошибок DNS перед переключением"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "Закрыть"
|
||||
msgid "DNS Successful Checks Before Recovery"
|
||||
msgstr "Успешных проверок DNS перед восстановлением"
|
||||
|
||||
msgid "Cannot save settings"
|
||||
msgstr "Не удалось сохранить настройки"
|
||||
msgid "Number of consecutive failed checks required before switching DNS servers."
|
||||
msgstr "Количество последовательных неудачных проверок, необходимое для переключения DNS-сервера."
|
||||
|
||||
msgid "Number of consecutive successful checks required before returning to a higher-priority DNS server."
|
||||
msgstr "Количество последовательных успешных проверок, необходимое для возврата к DNS-серверу с более высоким приоритетом."
|
||||
|
||||
msgid "Custom device headers"
|
||||
msgstr "Кастомные заголовки устройства"
|
||||
|
||||
msgid "Override the default OpenWrt device headers for this subscription"
|
||||
msgstr "Переопределить стандартные заголовки OpenWrt для этой подписки"
|
||||
|
||||
msgid "Generate HWID"
|
||||
msgstr "Сгенерировать HWID"
|
||||
|
||||
msgid "Generate a random 16-character hexadecimal HWID"
|
||||
msgstr "Сгенерировать случайный 16-символьный шестнадцатеричный HWID"
|
||||
|
||||
msgid "Leave empty to omit this header"
|
||||
msgstr "Оставьте пустым, чтобы опустить этот заголовок"
|
||||
|
||||
@@ -54,7 +54,12 @@ class SocketManager {
|
||||
}
|
||||
|
||||
private scheduleReconnect(url: string): void {
|
||||
if (this.reconnectTimers.has(url)) return;
|
||||
if (
|
||||
this.reconnectTimers.has(url) ||
|
||||
(this.listeners.get(url)?.size || 0) === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
if (attempts >= 10) {
|
||||
@@ -75,9 +80,8 @@ class SocketManager {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.reconnectTimers.delete(url);
|
||||
if (this.sockets.has(url)) {
|
||||
if ((this.listeners.get(url)?.size || 0) > 0) {
|
||||
logger.info('[SOCKET]', `Attempting reconnect to ${url}`);
|
||||
this.sockets.delete(url);
|
||||
this.connect(url);
|
||||
}
|
||||
}, delay);
|
||||
@@ -132,6 +136,7 @@ class SocketManager {
|
||||
logger.warn('[SOCKET]', `Disconnected: ${url}`);
|
||||
this.triggerError(url, 'Connection closed');
|
||||
if (this.sockets.get(url) === ws) {
|
||||
this.sockets.delete(url);
|
||||
this.scheduleReconnect(url);
|
||||
}
|
||||
});
|
||||
@@ -150,14 +155,14 @@ class SocketManager {
|
||||
this.errorListeners.get(url)?.add(onError);
|
||||
}
|
||||
|
||||
if (!this.sockets.has(url)) {
|
||||
this.connect(url);
|
||||
}
|
||||
|
||||
if (!this.listeners.has(url)) {
|
||||
this.listeners.set(url, new Set());
|
||||
}
|
||||
this.listeners.get(url)?.add(listener);
|
||||
|
||||
if (!this.sockets.has(url)) {
|
||||
this.connect(url);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||
@@ -165,6 +170,9 @@ class SocketManager {
|
||||
if (onError) {
|
||||
this.errorListeners.get(url)?.delete(onError);
|
||||
}
|
||||
if (this.listeners.get(url)?.size === 0) {
|
||||
this.disconnect(url);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
|
||||
@@ -5425,7 +5425,9 @@ var SocketManager = class _SocketManager {
|
||||
logger.info("[SOCKET]", "All connections and state have been reset.");
|
||||
}
|
||||
scheduleReconnect(url) {
|
||||
if (this.reconnectTimers.has(url)) return;
|
||||
if (this.reconnectTimers.has(url) || (this.listeners.get(url)?.size || 0) === 0) {
|
||||
return;
|
||||
}
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
if (attempts >= 10) {
|
||||
logger.error(
|
||||
@@ -5442,9 +5444,8 @@ var SocketManager = class _SocketManager {
|
||||
);
|
||||
const timer = setTimeout(() => {
|
||||
this.reconnectTimers.delete(url);
|
||||
if (this.sockets.has(url)) {
|
||||
if ((this.listeners.get(url)?.size || 0) > 0) {
|
||||
logger.info("[SOCKET]", `Attempting reconnect to ${url}`);
|
||||
this.sockets.delete(url);
|
||||
this.connect(url);
|
||||
}
|
||||
}, delay);
|
||||
@@ -5491,6 +5492,7 @@ var SocketManager = class _SocketManager {
|
||||
logger.warn("[SOCKET]", `Disconnected: ${url}`);
|
||||
this.triggerError(url, "Connection closed");
|
||||
if (this.sockets.get(url) === ws) {
|
||||
this.sockets.delete(url);
|
||||
this.scheduleReconnect(url);
|
||||
}
|
||||
});
|
||||
@@ -5506,19 +5508,22 @@ var SocketManager = class _SocketManager {
|
||||
if (onError) {
|
||||
this.errorListeners.get(url)?.add(onError);
|
||||
}
|
||||
if (!this.sockets.has(url)) {
|
||||
this.connect(url);
|
||||
}
|
||||
if (!this.listeners.has(url)) {
|
||||
this.listeners.set(url, /* @__PURE__ */ new Set());
|
||||
}
|
||||
this.listeners.get(url)?.add(listener);
|
||||
if (!this.sockets.has(url)) {
|
||||
this.connect(url);
|
||||
}
|
||||
}
|
||||
unsubscribe(url, listener, onError) {
|
||||
this.listeners.get(url)?.delete(listener);
|
||||
if (onError) {
|
||||
this.errorListeners.get(url)?.delete(onError);
|
||||
}
|
||||
if (this.listeners.get(url)?.size === 0) {
|
||||
this.disconnect(url);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
send(url, data) {
|
||||
|
||||
@@ -2029,6 +2029,22 @@ function validateUrlTestUrl(value) {
|
||||
return validation.valid ? true : validation.message;
|
||||
}
|
||||
|
||||
function generateHwid16() {
|
||||
const bytes = new Uint8Array(8);
|
||||
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
window.crypto.getRandomValues(bytes);
|
||||
} else {
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
bytes[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(bytes)
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function optionMapValue(option, section_id, key) {
|
||||
const value =
|
||||
option && option.map && option.map.data
|
||||
@@ -2048,6 +2064,13 @@ function subscriptionUrlSettingsKeys() {
|
||||
"user_agent",
|
||||
"auto_hwid",
|
||||
"hwid",
|
||||
"custom_device_headers",
|
||||
"device_os",
|
||||
"ver_os",
|
||||
"device_model",
|
||||
"device_locale",
|
||||
"app_version",
|
||||
"accept_language",
|
||||
"show_dashboard_metadata",
|
||||
"prefix_nodes",
|
||||
"node_prefix",
|
||||
@@ -2067,6 +2090,13 @@ function defaultSubscriptionUrlSettings() {
|
||||
user_agent: "",
|
||||
auto_hwid: "1",
|
||||
hwid: "",
|
||||
custom_device_headers: "0",
|
||||
device_os: "",
|
||||
ver_os: "",
|
||||
device_model: "",
|
||||
device_locale: "",
|
||||
app_version: "",
|
||||
accept_language: "",
|
||||
show_dashboard_metadata: "1",
|
||||
prefix_nodes: "0",
|
||||
node_prefix: "",
|
||||
@@ -2363,21 +2393,62 @@ function addSubscriptionUrlItemOptions(itemSection, options = {}) {
|
||||
o.default = "1";
|
||||
o.rmempty = false;
|
||||
|
||||
o = itemSection.option(
|
||||
const hwidOption = itemSection.option(
|
||||
form.Value,
|
||||
"hwid",
|
||||
_("HWID"),
|
||||
_("Enter the HWID sent with subscription requests"),
|
||||
);
|
||||
o.depends("auto_hwid", "0");
|
||||
o.rmempty = false;
|
||||
o.validate = function (itemId, value) {
|
||||
hwidOption.depends("auto_hwid", "0");
|
||||
hwidOption.rmempty = false;
|
||||
hwidOption.validate = function (itemId, value) {
|
||||
if (optionMapValue(this, itemId, "auto_hwid") !== "0") {
|
||||
return true;
|
||||
}
|
||||
return `${value || ""}`.trim() ? true : _("Enter HWID");
|
||||
};
|
||||
|
||||
o = itemSection.option(
|
||||
form.Button,
|
||||
"_generate_hwid",
|
||||
_("Generate HWID"),
|
||||
_("Generate a random 16-character hexadecimal HWID"),
|
||||
);
|
||||
o.depends("auto_hwid", "0");
|
||||
o.inputstyle = "action";
|
||||
o.inputtitle = _("Generate HWID");
|
||||
o.onclick = function (_event, itemId) {
|
||||
hwidOption.getUIElement(itemId).setValue(generateHwid16());
|
||||
};
|
||||
|
||||
o = itemSection.option(
|
||||
form.Flag,
|
||||
"custom_device_headers",
|
||||
_("Custom device headers"),
|
||||
_("Override the default OpenWrt device headers for this subscription"),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
[
|
||||
["device_os", "X-Device-OS"],
|
||||
["ver_os", "X-Ver-OS"],
|
||||
["device_model", "X-Device-Model"],
|
||||
["device_locale", "X-Device-Locale"],
|
||||
["app_version", "X-App-Version"],
|
||||
["accept_language", "Accept-Language"],
|
||||
].forEach(([key, label]) => {
|
||||
o = itemSection.option(
|
||||
form.Value,
|
||||
key,
|
||||
label,
|
||||
_("Leave empty to omit this header"),
|
||||
);
|
||||
o.depends("custom_device_headers", "1");
|
||||
o.rmempty = true;
|
||||
o.retain = true;
|
||||
});
|
||||
|
||||
o = itemSection.option(
|
||||
form.Flag,
|
||||
"show_dashboard_metadata",
|
||||
@@ -8867,6 +8938,536 @@ function loadSectionTableOptions(sectionRef) {
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
const ACTION_COLORS = {
|
||||
connection: "#2980b9",
|
||||
proxy: "#2980b9",
|
||||
outbound: "#3498db",
|
||||
vpn: "#1abc9c",
|
||||
bypass: "#27ae60",
|
||||
block: "#c0392b",
|
||||
zapret: "#8e44ad",
|
||||
zapret2: "#9b59b6",
|
||||
byedpi: "#d35400",
|
||||
};
|
||||
|
||||
function ip4ToInt(ip) {
|
||||
return ip.split('.').reduce((int, octet) => (int << 8) + parseInt(octet, 10), 0) >>> 0;
|
||||
}
|
||||
|
||||
function ipMatchesCidr(ip, cidr) {
|
||||
const parts = cidr.split('/');
|
||||
const subnet = parts[0];
|
||||
const bits = parts[1] ? parseInt(parts[1], 10) : 32;
|
||||
|
||||
if (bits === 0) return true;
|
||||
|
||||
const ipInt = ip4ToInt(ip);
|
||||
const subnetInt = ip4ToInt(subnet);
|
||||
|
||||
const mask = (~0 << (32 - bits)) >>> 0;
|
||||
return (ipInt & mask) === (subnetInt & mask);
|
||||
}
|
||||
|
||||
function matchIpInCidrs(ip, cidrs) {
|
||||
for (let i = 0; i < cidrs.length; i++) {
|
||||
const cidr = cidrs[i];
|
||||
if (!cidr) continue;
|
||||
try {
|
||||
if (ipMatchesCidr(ip, cidr)) {
|
||||
return cidr;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchIpInRuleset(ip, ruleset) {
|
||||
if (!ruleset || !Array.isArray(ruleset.rules)) return null;
|
||||
for (const rule of ruleset.rules) {
|
||||
if (Array.isArray(rule.ip_cidr)) {
|
||||
const matched = matchIpInCidrs(ip, rule.ip_cidr);
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchDomainInList(domain, list) {
|
||||
domain = domain.toLowerCase();
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i].trim().toLowerCase();
|
||||
if (!item) continue;
|
||||
|
||||
if (item.startsWith("full:")) {
|
||||
const target = item.substring(5);
|
||||
if (domain === target) return item;
|
||||
} else if (item.startsWith("keyword:")) {
|
||||
const target = item.substring(8);
|
||||
if (domain.indexOf(target) !== -1) return item;
|
||||
} else if (item.startsWith("regex:")) {
|
||||
const target = item.substring(6);
|
||||
try {
|
||||
const re = new RegExp(target);
|
||||
if (re.test(domain)) return item;
|
||||
} catch (e) {}
|
||||
} else {
|
||||
if (domain === item || domain.endsWith("." + item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchDomainInRuleset(domain, ruleset) {
|
||||
if (!ruleset || !Array.isArray(ruleset.rules)) return null;
|
||||
domain = domain.toLowerCase();
|
||||
|
||||
for (const rule of ruleset.rules) {
|
||||
if (Array.isArray(rule.domain)) {
|
||||
for (let i = 0; i < rule.domain.length; i++) {
|
||||
const d = rule.domain[i];
|
||||
if (d.toLowerCase() === domain) return `domain:${d}`;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(rule.domain_suffix)) {
|
||||
for (let i = 0; i < rule.domain_suffix.length; i++) {
|
||||
const ds = rule.domain_suffix[i];
|
||||
const dsLower = ds.toLowerCase();
|
||||
if (domain === dsLower || domain.endsWith("." + dsLower)) {
|
||||
return `domain_suffix:${ds}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(rule.domain_keyword)) {
|
||||
for (let i = 0; i < rule.domain_keyword.length; i++) {
|
||||
const dk = rule.domain_keyword[i];
|
||||
if (domain.indexOf(dk.toLowerCase()) !== -1) {
|
||||
return `domain_keyword:${dk}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(rule.domain_regex)) {
|
||||
for (let i = 0; i < rule.domain_regex.length; i++) {
|
||||
const dr = rule.domain_regex[i];
|
||||
try {
|
||||
const re = new RegExp(dr);
|
||||
if (re.test(domain)) return `domain_regex:${dr}`;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchPort(port, portsList) {
|
||||
const queryPort = parseInt(port, 10);
|
||||
if (isNaN(queryPort)) return null;
|
||||
|
||||
for (let i = 0; i < portsList.length; i++) {
|
||||
const item = portsList[i];
|
||||
if (!item) continue;
|
||||
if (item.indexOf("-") !== -1) {
|
||||
const parts = item.split("-");
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parseInt(parts[1], 10);
|
||||
if (!isNaN(start) && !isNaN(end) && queryPort >= start && queryPort <= end) {
|
||||
return item;
|
||||
}
|
||||
} else {
|
||||
const p = parseInt(item, 10);
|
||||
if (p === queryPort) return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectQueryType(query) {
|
||||
query = query.trim();
|
||||
if (/^\d+$/.test(query)) {
|
||||
return 'port';
|
||||
}
|
||||
if (/^[\d./]+$/.test(query) && query.indexOf('.') !== -1) {
|
||||
return 'ip';
|
||||
}
|
||||
if (query.indexOf(':') !== -1) {
|
||||
return 'ip';
|
||||
}
|
||||
return 'domain';
|
||||
}
|
||||
|
||||
function readRulesetFile(path) {
|
||||
return fs.read(path)
|
||||
.then((content) => {
|
||||
if (!content) return null;
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse ruleset file JSON:", path, e);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
async function findCachedSrsFile(secName, community) {
|
||||
const tag = `${secName}-${community}-community-ruleset`;
|
||||
const localPath = `/tmp/sing-box/rulesets/community_${tag}.srs`;
|
||||
|
||||
// Check if we already have extracted it
|
||||
let localStat = await fs.stat(localPath).catch(() => null);
|
||||
let dbStat = await fs.stat("/etc/sing-box/cache.db").catch(() => null);
|
||||
|
||||
// If extracted file exists and is newer than cache.db, use it directly!
|
||||
if (localStat && localStat.type === "file" && dbStat && localStat.mtime >= dbStat.mtime) {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
// Otherwise, extract it using the ucode backend command!
|
||||
const res = await fs.exec("/usr/bin/tachyon", ["extract_ruleset", tag]).catch(() => null);
|
||||
if (res && res.code === 0 && res.stdout) {
|
||||
return res.stdout.trim();
|
||||
}
|
||||
|
||||
// Fallback to check if it exists anyway (if extract failed but old file is there)
|
||||
if (localStat && localStat.type === "file") {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function matchCommunityList(secName, community, query, type) {
|
||||
const srsPath = await findCachedSrsFile(secName, community);
|
||||
if (!srsPath) return false;
|
||||
|
||||
// Try /usr/bin/sing-box first
|
||||
let res = await fs.exec("/usr/bin/sing-box", ["rule-set", "match", srsPath, query, "-f", "binary"]).catch(() => null);
|
||||
if (res && res.code === 0) {
|
||||
return true;
|
||||
}
|
||||
// Fallback to /usr/sbin/sing-box
|
||||
res = await fs.exec("/usr/sbin/sing-box", ["rule-set", "match", srsPath, query, "-f", "binary"]).catch(() => null);
|
||||
if (res && res.code === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
async function performTrace(query) {
|
||||
const type = detectQueryType(query);
|
||||
const sections = uci.sections(UCI_PACKAGE, "section") || [];
|
||||
const totalSections = sections.length;
|
||||
|
||||
let queryForMatching = query;
|
||||
if (type === 'ip' && query.indexOf('/') !== -1) {
|
||||
queryForMatching = query.split('/')[0];
|
||||
}
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const sec = sections[i];
|
||||
const secName = sec[".name"];
|
||||
|
||||
if (uci.get(UCI_PACKAGE, secName, "enabled") === "0") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = uci.get(UCI_PACKAGE, secName, "label") || secName;
|
||||
const action = uci.get(UCI_PACKAGE, secName, "action") || "connection";
|
||||
|
||||
if (type === 'port') {
|
||||
const ports = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "ports"));
|
||||
const matchedPort = matchPort(queryForMatching, ports);
|
||||
if (matchedPort) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Ports",
|
||||
pattern: matchedPort,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'ip') {
|
||||
const ipCidr = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "ip_cidr"));
|
||||
const matchedCidr = matchIpInCidrs(queryForMatching, ipCidr);
|
||||
if (matchedCidr) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "IPs (UCI)",
|
||||
pattern: matchedCidr,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
|
||||
const listsRuleset = await readRulesetFile(`/tmp/sing-box/rulesets/${secName}-lists-ruleset.json`);
|
||||
const matchedListIp = matchIpInRuleset(queryForMatching, listsRuleset);
|
||||
if (matchedListIp) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "IP/Subnet List (community/plain)",
|
||||
pattern: matchedListIp,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
|
||||
const remoteSubnetRuleset = await readRulesetFile(`/tmp/sing-box/rulesets/${secName}-remote-subnet-ruleset.json`);
|
||||
const matchedRemoteIp = matchIpInRuleset(queryForMatching, remoteSubnetRuleset);
|
||||
if (matchedRemoteIp) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Remote Subnet List",
|
||||
pattern: matchedRemoteIp,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
|
||||
// Check community list rulesets for matching IPs
|
||||
const communityLists = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "community_lists"));
|
||||
for (const community of communityLists) {
|
||||
if (await matchCommunityList(secName, community, queryForMatching, type)) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Community List (" + community + ")",
|
||||
pattern: queryForMatching,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'domain') {
|
||||
const domainSuffixes = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "domain_suffix"));
|
||||
const exactDomains = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "domain")).map(d => `full:${d}`);
|
||||
const domainKeywords = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "domain_keyword")).map(k => `keyword:${k}`);
|
||||
const domainRegexes = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "domain_regex")).map(r => `regex:${r}`);
|
||||
const userDomains = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "user_domains"));
|
||||
const allUciDomains = [...domainSuffixes, ...exactDomains, ...domainKeywords, ...domainRegexes, ...userDomains];
|
||||
|
||||
const matchedUciDomain = matchDomainInList(queryForMatching, allUciDomains);
|
||||
if (matchedUciDomain) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Domains (UCI)",
|
||||
pattern: matchedUciDomain,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
|
||||
const listsRuleset = await readRulesetFile(`/tmp/sing-box/rulesets/${secName}-lists-ruleset.json`);
|
||||
if (listsRuleset) {
|
||||
const matchedListDomain = matchDomainInRuleset(queryForMatching, listsRuleset);
|
||||
if (matchedListDomain) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Domain List (community/plain)",
|
||||
pattern: matchedListDomain,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const remoteDomainRuleset = await readRulesetFile(`/tmp/sing-box/rulesets/${secName}-remote-domain-ruleset.json`);
|
||||
if (remoteDomainRuleset) {
|
||||
const matchedRemoteDomain = matchDomainInRuleset(queryForMatching, remoteDomainRuleset);
|
||||
if (matchedRemoteDomain) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Remote Domain List",
|
||||
pattern: matchedRemoteDomain,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check community list rulesets for matching domains
|
||||
const communityLists = normalizeOptionValues(uci.get(UCI_PACKAGE, secName, "community_lists"));
|
||||
for (const community of communityLists) {
|
||||
if (await matchCommunityList(secName, community, queryForMatching, type)) {
|
||||
return {
|
||||
matched: true,
|
||||
sectionName: secName,
|
||||
label: label,
|
||||
action: action,
|
||||
ruleType: "Community List (" + community + ")",
|
||||
pattern: queryForMatching,
|
||||
priority: i + 1,
|
||||
totalSections: totalSections
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
function createTracerSearchWidget(sectionRef) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "cbi-section pdk-search-tracer-card";
|
||||
container.style.marginBottom = "1rem";
|
||||
container.style.padding = "0.6rem 0.8rem";
|
||||
container.style.border = "1px solid var(--border-color, #e0e0e0)";
|
||||
container.style.borderRadius = "6px";
|
||||
container.style.background = "var(--background-color-high, #fff)";
|
||||
container.style.boxShadow = "0 1px 3px rgba(0,0,0,0.05)";
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="pdk-tracer-header" style="display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none;">
|
||||
<h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem; color: var(--text-color-high, #333); font-size: 1.1rem; font-weight: bold;">
|
||||
<span>${_("Routing Rule Tracer")}</span>
|
||||
</h3>
|
||||
<span class="pdk-tracer-toggle-icon" style="font-size: 0.8rem; color: var(--text-color-medium, #666); transition: transform 0.2s; padding-right: 0.2rem;">►</span>
|
||||
</div>
|
||||
<div class="pdk-tracer-content" style="display: none; margin-top: 0.6rem; border-top: 1px solid var(--border-color, #e0e0e0); padding-top: 0.6rem;">
|
||||
<p style="margin-top: 0; margin-bottom: 0.8rem; color: var(--text-color-medium, #666); font-size: 85%; line-height: 1.35;">
|
||||
${_("Enter a domain, IP address, or port to test routing. The tracer will show which section intercepts the traffic and which rule matches.")}
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap;">
|
||||
<input type="text" class="cbi-input-text pdk-tracer-input" placeholder="${_("e.g. google.com, 1.1.1.1, 443")}" style="flex: 1; min-width: 200px; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 90%;" />
|
||||
<button class="cbi-button cbi-button-action pdk-tracer-btn" style="padding: 0.3rem 0.8rem; font-weight: bold; cursor: pointer; font-size: 90%;">${_("Trace")}</button>
|
||||
<button class="cbi-button cbi-button-neutral pdk-tracer-clear-btn" style="padding: 0.3rem 0.8rem; cursor: pointer; font-size: 90%; display: none;">${_("Clear")}</button>
|
||||
</div>
|
||||
<div class="pdk-tracer-results" style="margin-top: 0.8rem; padding-top: 0.8rem; border-top: 1px dashed var(--border-color, #e0e0e0); display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const headerEl = container.querySelector(".pdk-tracer-header");
|
||||
const contentEl = container.querySelector(".pdk-tracer-content");
|
||||
const toggleIcon = container.querySelector(".pdk-tracer-toggle-icon");
|
||||
const inputEl = container.querySelector(".pdk-tracer-input");
|
||||
const btnEl = container.querySelector(".pdk-tracer-btn");
|
||||
const clearEl = container.querySelector(".pdk-tracer-clear-btn");
|
||||
const resultsEl = container.querySelector(".pdk-tracer-results");
|
||||
|
||||
headerEl.addEventListener("click", () => {
|
||||
const isCollapsed = contentEl.style.display === "none";
|
||||
if (isCollapsed) {
|
||||
contentEl.style.display = "block";
|
||||
toggleIcon.textContent = "▼";
|
||||
container.style.padding = "0.8rem 1rem";
|
||||
} else {
|
||||
contentEl.style.display = "none";
|
||||
toggleIcon.textContent = "►";
|
||||
container.style.padding = "0.6rem 0.8rem";
|
||||
}
|
||||
});
|
||||
|
||||
function clearResults() {
|
||||
inputEl.value = "";
|
||||
resultsEl.style.display = "none";
|
||||
resultsEl.innerHTML = "";
|
||||
clearEl.style.display = "none";
|
||||
}
|
||||
|
||||
async function handleTrace() {
|
||||
const query = inputEl.value.trim();
|
||||
if (!query) {
|
||||
clearResults();
|
||||
return;
|
||||
}
|
||||
|
||||
clearEl.style.display = "inline-block";
|
||||
resultsEl.style.display = "block";
|
||||
resultsEl.innerHTML = `<div style="color: var(--text-color-medium, #666); display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span class="pdk-spinner" style="border: 2px solid var(--border-color, #ccc); border-top: 2px solid var(--primary-color, #4a90d9); border-radius: 50%; width: 14px; height: 14px; display: inline-block; animation: spin 1s linear infinite;"></span>
|
||||
<span>${_("Tracing routing rules...")}</span>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
const result = await performTrace(query);
|
||||
if (result && result.matched) {
|
||||
let actionBadgeColor = ACTION_COLORS[result.action] || "#7f8c8d";
|
||||
let actionLabel = result.action.toUpperCase();
|
||||
|
||||
resultsEl.innerHTML = `
|
||||
<div style="padding: 0.6rem 0.8rem; border-radius: 4px; background: var(--background-color-low, #f9f9f9); border-left: 3px solid ${actionBadgeColor}; font-size: 90%; line-height: 1.4;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.8rem; align-items: center;">
|
||||
<strong>${_("Match Found")}:</strong>
|
||||
<span>${result.label} <span style="background: ${actionBadgeColor}; color: #fff; padding: 1px 5px; border-radius: 3px; font-weight: bold; font-size: 80%; margin-left: 0.2rem;">${actionLabel}</span></span>
|
||||
<span style="color: var(--text-color-medium, #666);">|</span>
|
||||
<span><strong>${_("Rule")}:</strong> <code>${result.ruleType}</code> (${result.pattern})</span>
|
||||
<span style="color: var(--text-color-medium, #666);">|</span>
|
||||
<span><strong>${_("Priority")}:</strong> ${result.priority}/${result.totalSections}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultsEl.innerHTML = `
|
||||
<div style="padding: 0.6rem 0.8rem; border-radius: 4px; background: var(--background-color-low, #f9f9f9); border-left: 3px solid var(--warning-color, #f1c40f); font-size: 90%; color: var(--text-color-high, #333);">
|
||||
<strong>${_("No Custom Rules Matched")}:</strong> ${_("Traffic will go through the default route (Direct/Default).")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
resultsEl.innerHTML = `
|
||||
<div style="padding: 1rem; border-radius: 6px; background: var(--background-color-low, #f9f9f9); border-left: 4px solid var(--error-color, #e74c3c); color: var(--error-color, #e74c3c);">
|
||||
<strong>${_("Error running trace:")}</strong> ${e.message || e}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
btnEl.addEventListener("click", handleTrace);
|
||||
clearEl.addEventListener("click", clearResults);
|
||||
inputEl.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleTrace();
|
||||
}
|
||||
});
|
||||
|
||||
if (!document.getElementById("pdk-tracer-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "pdk-tracer-styles";
|
||||
style.textContent = `
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function configureSectionSection(sectionRef, options = {}) {
|
||||
setActionProvidersAvailabilityLoader(options.loadActionProvidersAvailability);
|
||||
|
||||
@@ -8875,6 +9476,21 @@ function configureSectionSection(sectionRef, options = {}) {
|
||||
// action/provider details when the user opens it.
|
||||
return loadSectionTableOptions(this);
|
||||
};
|
||||
|
||||
const originalRender = sectionRef.render;
|
||||
sectionRef.render = function () {
|
||||
return originalRender.apply(this, arguments).then((node) => {
|
||||
if (node) {
|
||||
const tracer = createTracerSearchWidget(sectionRef);
|
||||
if (node.firstChild) {
|
||||
node.insertBefore(tracer, node.firstChild);
|
||||
} else {
|
||||
node.appendChild(tracer);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const EntryPoint = {
|
||||
|
||||
@@ -477,6 +477,32 @@ function createSettingsContent(section, capabilities) {
|
||||
);
|
||||
configureDnsDuration(o, "2s", dnsOption, bootstrapOption);
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"dns_failure_threshold",
|
||||
_("DNS Failures Before Switching"),
|
||||
_(
|
||||
"Number of consecutive failed checks required before switching DNS servers.",
|
||||
),
|
||||
);
|
||||
o.default = "3";
|
||||
o.rmempty = false;
|
||||
o.datatype = "range(1, 10)";
|
||||
configureDnsFailoverVisibility(o, dnsOption, bootstrapOption);
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"dns_recovery_threshold",
|
||||
_("DNS Successful Checks Before Recovery"),
|
||||
_(
|
||||
"Number of consecutive successful checks required before returning to a higher-priority DNS server.",
|
||||
),
|
||||
);
|
||||
o.default = "3";
|
||||
o.rmempty = false;
|
||||
o.datatype = "range(1, 10)";
|
||||
configureDnsFailoverVisibility(o, dnsOption, bootstrapOption);
|
||||
|
||||
o = section.option(
|
||||
form.Value,
|
||||
"dns_rewrite_ttl",
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-19 21:37+0300\n"
|
||||
"PO-Revision-Date: 2026-07-19 21:37+0300\n"
|
||||
"POT-Creation-Date: 2026-07-22 21:48+0300\n"
|
||||
"PO-Revision-Date: 2026-07-22 21:48+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -179,6 +179,9 @@ msgstr "Все оставшиеся серверы"
|
||||
msgid "All remaining servers means every server not already assigned to a higher-priority level."
|
||||
msgstr "Все оставшиеся серверы — это все серверы, не назначенные на уровень с более высоким приоритетом."
|
||||
|
||||
msgid "All Routes"
|
||||
msgstr "Все маршруты"
|
||||
|
||||
msgid "All servers"
|
||||
msgstr "Все серверы"
|
||||
|
||||
@@ -368,6 +371,9 @@ msgstr "Проверить обновление"
|
||||
msgid "Check URL"
|
||||
msgstr "URL проверки"
|
||||
|
||||
msgid "checked"
|
||||
msgstr "проверено"
|
||||
|
||||
msgid "Checking Connection..."
|
||||
msgstr ""
|
||||
|
||||
@@ -398,6 +404,9 @@ msgstr "CIDR должен быть от 0 до 32 для IPv4 или от 0 до
|
||||
msgid "Cipher"
|
||||
msgstr "Шифрование"
|
||||
|
||||
msgid "Clear"
|
||||
msgstr "Сбросить"
|
||||
|
||||
msgid "Client Certificate"
|
||||
msgstr "Сертификат клиента"
|
||||
|
||||
@@ -731,6 +740,9 @@ msgstr "Дублирующееся значение"
|
||||
msgid "e.g. AES-256-GCM — leave empty for default"
|
||||
msgstr "например AES-256-GCM — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "e.g. google.com, 1.1.1.1, 443"
|
||||
msgstr "например: google.com, 1.1.1.1, 443"
|
||||
|
||||
msgid "e.g. SHA256 — leave empty for default"
|
||||
msgstr "например SHA256 — оставьте пустым для значения по умолчанию"
|
||||
|
||||
@@ -800,6 +812,9 @@ msgstr "Введите полный объект outbound для sing-box"
|
||||
msgid "Enter a display name"
|
||||
msgstr "Введите отображаемое имя"
|
||||
|
||||
msgid "Enter a domain, IP address, or port to test routing. The tracer will show which section intercepts the traffic and which rule matches."
|
||||
msgstr "Введите домен, IP-адрес или порт для проверки маршрутизации. Трассировщик покажет, в какую секцию попадет трафик и какое именно правило сработает."
|
||||
|
||||
msgid "Enter a level name"
|
||||
msgstr "Введите название уровня"
|
||||
|
||||
@@ -824,6 +839,9 @@ msgstr ""
|
||||
msgid "Error running generator:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error running trace:"
|
||||
msgstr "Ошибка при выполнении трассировки:"
|
||||
|
||||
msgid "Exclude by proxy parameters"
|
||||
msgstr "Исключить по параметрам прокси"
|
||||
|
||||
@@ -1385,6 +1403,9 @@ msgstr "Локальный адрес"
|
||||
msgid "Log Level"
|
||||
msgstr "Уровень логов"
|
||||
|
||||
msgid "lx"
|
||||
msgstr ""
|
||||
|
||||
msgid "Main DNS"
|
||||
msgstr "Основной DNS"
|
||||
|
||||
@@ -1409,6 +1430,12 @@ msgstr "Совпадение по IP или подсети назначения"
|
||||
msgid "Match destination ports. Use a single port or a range"
|
||||
msgstr "Совпадение по портам назначения. Используйте один порт или диапазон"
|
||||
|
||||
msgid "Match Found"
|
||||
msgstr "Совпадение найдено"
|
||||
|
||||
msgid "Matching Rule"
|
||||
msgstr "Сработавшее правило"
|
||||
|
||||
msgid "Maximum time to wait for example.com to resolve during a DNS health check."
|
||||
msgstr "Максимальное время ожидания разрешения example.com при проверке доступности DNS."
|
||||
|
||||
@@ -1493,6 +1520,9 @@ msgstr "Нет активных клиентов"
|
||||
msgid "No active connections"
|
||||
msgstr "Нет активных подключений"
|
||||
|
||||
msgid "No active routing section intercepts this traffic. Traffic will bypass the custom routing logic and be handled by the default route (Direct/Default)."
|
||||
msgstr "Этот трафик не перехватывается ни одной активной секцией маршрутизации. Он пойдет в обход кастомных правил и будет обработан маршрутом по умолчанию (Напрямую/Дефолт)."
|
||||
|
||||
msgid "No active routing sections found."
|
||||
msgstr "Активные секции маршрутизации не найдены."
|
||||
|
||||
@@ -1502,6 +1532,9 @@ msgstr "Нет закрытых подключений"
|
||||
msgid "No conflict detected"
|
||||
msgstr "Конфликт не обнаружен"
|
||||
|
||||
msgid "No Custom Rules Matched"
|
||||
msgstr "Пользовательские правила не совпали"
|
||||
|
||||
msgid "No domains detected yet"
|
||||
msgstr "Домены пока не обнаружены"
|
||||
|
||||
@@ -1634,6 +1667,9 @@ msgstr "Опциональные значения должны быть указ
|
||||
msgid "Optional values must be attached with '=' here; a separate token would be ignored by nfqws2."
|
||||
msgstr "Опциональные значения должны быть указаны через '=' здесь; отдельный токен будет проигнорирован nfqws2."
|
||||
|
||||
msgid "out of"
|
||||
msgstr "из"
|
||||
|
||||
msgid "outbounds must be a non-empty array of strings"
|
||||
msgstr "outbounds должен быть непустым массивом строк"
|
||||
|
||||
@@ -1856,12 +1892,18 @@ msgstr "Маршрутизировать запросы к этому DNS сер
|
||||
msgid "Routing mode"
|
||||
msgstr "Режим маршрутизации"
|
||||
|
||||
msgid "Routing Rule Tracer"
|
||||
msgstr "Трассировщик правил маршрутизации"
|
||||
|
||||
msgid "Routing rules"
|
||||
msgstr "Правила маршрутизации"
|
||||
|
||||
msgid "Routing section"
|
||||
msgstr "Секция маршрутизации"
|
||||
|
||||
msgid "Routing Section"
|
||||
msgstr "Секция маршрутизации"
|
||||
|
||||
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json path"
|
||||
msgstr "Набор правил должен быть HTTP(S) URL или локальным путём .srs / .json"
|
||||
|
||||
@@ -2432,6 +2474,12 @@ msgstr "Допуск"
|
||||
msgid "Top level has the highest priority; lower levels are used as fallback"
|
||||
msgstr "Верхний уровень имеет наивысший приоритет; нижние уровни используются как резерв"
|
||||
|
||||
msgid "Trace"
|
||||
msgstr "Проверить"
|
||||
|
||||
msgid "Tracing routing rules..."
|
||||
msgstr "Выполняется трассировка..."
|
||||
|
||||
msgid "Traffic"
|
||||
msgstr "Трафик"
|
||||
|
||||
@@ -2834,13 +2882,29 @@ msgstr "Принудительная маршрутизация по IP (Dest)"
|
||||
msgid "All traffic to these destination IPs will be routed through the section unconditionally, ignoring all other conditions."
|
||||
msgstr "Весь трафик к этим IP назначения будет направлен через секцию безусловно, игнорируя все остальные условия."
|
||||
|
||||
msgid "All Routes"
|
||||
msgstr "Все маршруты"
|
||||
msgid "DNS Failures Before Switching"
|
||||
msgstr "Ошибок DNS перед переключением"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "Закрыть"
|
||||
msgid "DNS Successful Checks Before Recovery"
|
||||
msgstr "Успешных проверок DNS перед восстановлением"
|
||||
|
||||
msgid "Cannot save settings"
|
||||
msgstr "Не удалось сохранить настройки"
|
||||
msgid "Number of consecutive failed checks required before switching DNS servers."
|
||||
msgstr "Количество последовательных неудачных проверок, необходимое для переключения DNS-сервера."
|
||||
|
||||
msgid "Number of consecutive successful checks required before returning to a higher-priority DNS server."
|
||||
msgstr "Количество последовательных успешных проверок, необходимое для возврата к DNS-серверу с более высоким приоритетом."
|
||||
|
||||
msgid "Custom device headers"
|
||||
msgstr "Кастомные заголовки устройства"
|
||||
|
||||
msgid "Override the default OpenWrt device headers for this subscription"
|
||||
msgstr "Переопределить стандартные заголовки OpenWrt для этой подписки"
|
||||
|
||||
msgid "Generate HWID"
|
||||
msgstr "Сгенерировать HWID"
|
||||
|
||||
msgid "Generate a random 16-character hexadecimal HWID"
|
||||
msgstr "Сгенерировать случайный 16-символьный шестнадцатеричный HWID"
|
||||
|
||||
msgid "Leave empty to omit this header"
|
||||
msgstr "Оставьте пустым, чтобы опустить этот заголовок"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,18 @@
|
||||
],
|
||||
"/usr/bin/tachyon": [
|
||||
"exec"
|
||||
],
|
||||
"/usr/bin/sing-box": [
|
||||
"exec"
|
||||
],
|
||||
"/usr/sbin/sing-box": [
|
||||
"exec"
|
||||
],
|
||||
"/bin/uclient-fetch": [
|
||||
"exec"
|
||||
],
|
||||
"/usr/bin/wget": [
|
||||
"exec"
|
||||
]
|
||||
},
|
||||
"ubus": {
|
||||
|
||||
@@ -198,6 +198,7 @@ function command_spec(command) {
|
||||
component_action_status: [ "components/updates.uc", "component-action-status", 1 ],
|
||||
component_updates_if_due: [ "components/updates.uc", "component-updates-if-due", 0 ],
|
||||
component_update_check_cache: [ "components/updates.uc", "component-update-check-cache", 0 ],
|
||||
extract_ruleset: [ "diagnostics/runtime.uc", "extract-ruleset", 1 ],
|
||||
check_dns_available: [ "diagnostics/runtime.uc", "check-dns-available", 0 ],
|
||||
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
|
||||
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
|
||||
|
||||
@@ -671,6 +671,76 @@ function subscription_hwid(section, value) {
|
||||
return item_option(section, "subscription_url_settings", value, "hwid", "");
|
||||
}
|
||||
|
||||
function subscription_custom_device_headers(section, value) {
|
||||
let child = child_item_by_value(section, "subscription_url", "url", value);
|
||||
if (child != null)
|
||||
return child_bool(child, "custom_device_headers", false);
|
||||
return item_bool(section, "subscription_url_settings", value, "custom_device_headers", false);
|
||||
}
|
||||
|
||||
function subscription_device_header(section, value, name) {
|
||||
let child = child_item_by_value(section, "subscription_url", "url", value);
|
||||
if (child != null)
|
||||
return child_option(child, name, "");
|
||||
return item_option(section, "subscription_url_settings", value, name, "");
|
||||
}
|
||||
|
||||
function subscription_device_os(section, value) {
|
||||
return subscription_device_header(section, value, "device_os");
|
||||
}
|
||||
|
||||
function subscription_ver_os(section, value) {
|
||||
return subscription_device_header(section, value, "ver_os");
|
||||
}
|
||||
|
||||
function subscription_device_model(section, value) {
|
||||
return subscription_device_header(section, value, "device_model");
|
||||
}
|
||||
|
||||
function subscription_device_locale(section, value) {
|
||||
return subscription_device_header(section, value, "device_locale");
|
||||
}
|
||||
|
||||
function subscription_app_version(section, value) {
|
||||
return subscription_device_header(section, value, "app_version");
|
||||
}
|
||||
|
||||
function subscription_accept_language(section, value) {
|
||||
return subscription_device_header(section, value, "accept_language");
|
||||
}
|
||||
|
||||
function subscription_device_headers(section, value) {
|
||||
return {
|
||||
enabled: subscription_custom_device_headers(section, value),
|
||||
device_os: subscription_device_os(section, value),
|
||||
ver_os: subscription_ver_os(section, value),
|
||||
device_model: subscription_device_model(section, value),
|
||||
device_locale: subscription_device_locale(section, value),
|
||||
app_version: subscription_app_version(section, value),
|
||||
accept_language: subscription_accept_language(section, value)
|
||||
};
|
||||
}
|
||||
|
||||
function device_headers_signature(headers) {
|
||||
headers = object_or_empty(headers);
|
||||
if (!headers.enabled)
|
||||
return "";
|
||||
|
||||
return sprintf("%J", {
|
||||
enabled: "1",
|
||||
device_os: as_string(headers.device_os),
|
||||
ver_os: as_string(headers.ver_os),
|
||||
device_model: as_string(headers.device_model),
|
||||
device_locale: as_string(headers.device_locale),
|
||||
app_version: as_string(headers.app_version),
|
||||
accept_language: as_string(headers.accept_language)
|
||||
});
|
||||
}
|
||||
|
||||
function subscription_device_headers_signature(section, value) {
|
||||
return device_headers_signature(subscription_device_headers(section, value));
|
||||
}
|
||||
|
||||
function subscription_download_section(section, value) {
|
||||
let child = child_item_by_value(section, "subscription_url", "url", value);
|
||||
if (child != null) {
|
||||
@@ -1099,6 +1169,16 @@ return {
|
||||
subscription_node_prefix,
|
||||
subscription_user_agent,
|
||||
subscription_hwid,
|
||||
subscription_custom_device_headers,
|
||||
subscription_device_os,
|
||||
subscription_ver_os,
|
||||
subscription_device_model,
|
||||
subscription_device_locale,
|
||||
subscription_app_version,
|
||||
subscription_accept_language,
|
||||
subscription_device_headers,
|
||||
device_headers_signature,
|
||||
subscription_device_headers_signature,
|
||||
subscription_download_section,
|
||||
interface_domain_resolver_enabled,
|
||||
interface_domain_resolver_dns_type,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let uci_core_module = null;
|
||||
let uci_core = require("core.uci");
|
||||
let fixture_uci_data = null;
|
||||
let subscription_parser_module = null;
|
||||
let zapret_validator_module = null;
|
||||
@@ -666,7 +666,7 @@ function mwan3_has_enabled_interface() {
|
||||
}
|
||||
|
||||
function mwan3_has_enabled_interface_from_sections() {
|
||||
for (let section in uci_core().section_objects("mwan3", "interface"))
|
||||
for (let section in uci_core.section_objects("mwan3", "interface"))
|
||||
if (option(section, "enabled", "0") == "1")
|
||||
return true;
|
||||
return false;
|
||||
@@ -741,11 +741,7 @@ function runtime_constants() {
|
||||
return constants_module;
|
||||
}
|
||||
|
||||
function uci_core() {
|
||||
if (uci_core_module == null)
|
||||
uci_core_module = require("core.uci");
|
||||
return uci_core_module;
|
||||
}
|
||||
|
||||
|
||||
function fixture_section_list(type_name) {
|
||||
let value = object_or_empty(fixture_uci_data)[type_name];
|
||||
@@ -781,7 +777,7 @@ function use_fixture_cursor(path) {
|
||||
function settings_section() {
|
||||
if (fixture_uci_data != null)
|
||||
return object_or_empty(fixture_get_section("settings"));
|
||||
return object_or_empty(uci_core().get_all(CONFIG_NAME, "settings"));
|
||||
return object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
|
||||
}
|
||||
|
||||
function sections_by_type(type_name) {
|
||||
@@ -789,7 +785,7 @@ function sections_by_type(type_name) {
|
||||
return fixture_section_list(type_name);
|
||||
|
||||
let result = [];
|
||||
for (let section in uci_core().section_objects(CONFIG_NAME, type_name))
|
||||
for (let section in uci_core.section_objects(CONFIG_NAME, type_name))
|
||||
push(result, object_or_empty(section));
|
||||
return result;
|
||||
}
|
||||
@@ -929,6 +925,11 @@ function validate_dns_settings(settings, sections, context) {
|
||||
validate_required_duration_option(option(settings, "dns_check_interval", "10s"), "settings.dns_check_interval");
|
||||
validate_required_duration_option(option(settings, "dns_recovery_check_interval", "60s"), "settings.dns_recovery_check_interval");
|
||||
validate_required_duration_option(option(settings, "dns_check_timeout", "2s"), "settings.dns_check_timeout");
|
||||
for (let name in [ "dns_failure_threshold", "dns_recovery_threshold" ]) {
|
||||
let value = trim(option(settings, name, "3"));
|
||||
if (match(value, /^[0-9]+$/) == null || int(value) < 1 || int(value) > 10)
|
||||
fail_validation("Invalid " + name + " value '" + value + "'. Use a number from 1 to 10. Aborted.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!bool_option(settings, "dns_detour_enabled", false))
|
||||
|
||||
@@ -68,6 +68,33 @@ function write_text_file(path, text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function write_file_atomic(path, text) {
|
||||
path = as_string(path);
|
||||
let stamp = clock();
|
||||
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
|
||||
let result = fs.writefile(tmp_path, as_string(text));
|
||||
if (result == null || (type(result) == "boolean" && !result)) {
|
||||
fs.unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
if (!fs.rename(tmp_path, path)) {
|
||||
fs.unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function is_process_name_running(pid, expected_name) {
|
||||
pid = as_string(pid);
|
||||
expected_name = as_string(expected_name);
|
||||
if (match(pid, /^[0-9]+$/) == null)
|
||||
return false;
|
||||
let comm = fs.readfile("/proc/" + pid + "/comm");
|
||||
if (comm == null)
|
||||
return false;
|
||||
return trim(comm) == expected_name;
|
||||
}
|
||||
|
||||
function file_remove_cr(path) {
|
||||
path = as_string(path);
|
||||
let data = fs.readfile(path);
|
||||
@@ -536,10 +563,24 @@ function sing_box_version_is_extended(value) {
|
||||
return index(as_string(value), "extended") >= 0 || index(as_string(value), "-lx") >= 0;
|
||||
}
|
||||
|
||||
function module_exports() {
|
||||
return {
|
||||
write_file_atomic,
|
||||
is_process_name_running
|
||||
};
|
||||
}
|
||||
|
||||
if (sourcepath(1) != null && sourcepath(1) != "")
|
||||
return module_exports();
|
||||
|
||||
let mode = ARGV[0] || "";
|
||||
|
||||
if (mode == "stdin-lines-to-json-array")
|
||||
stdin_lines_to_json_array();
|
||||
else if (mode == "write-file-atomic")
|
||||
exit(write_file_atomic(ARGV[1], ARGV[2]) ? 0 : 1);
|
||||
else if (mode == "is-process-name-running")
|
||||
exit(is_process_name_running(ARGV[1], ARGV[2]) ? 0 : 1);
|
||||
else if (mode == "file-has-cr")
|
||||
file_has_cr_exit(ARGV[1]);
|
||||
else if (mode == "file-remove-cr")
|
||||
|
||||
@@ -2546,9 +2546,116 @@ function ai_doctor() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function extract_ruleset(tag) {
|
||||
if (tag == "") {
|
||||
warn("tag is required\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let db_path = "/etc/sing-box/cache.db";
|
||||
let f = fs.open(db_path, "r");
|
||||
if (!f) {
|
||||
warn("failed to open " + db_path + "\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let page_size = 4096;
|
||||
let found_data = null;
|
||||
|
||||
while (true) {
|
||||
let header = f.read(16);
|
||||
if (!header || length(header) < 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
let flags = (ord(header, 9) << 8) | ord(header, 8);
|
||||
let count = (ord(header, 11) << 8) | ord(header, 10);
|
||||
let overflow = (ord(header, 15) << 24) | (ord(header, 14) << 16) | (ord(header, 13) << 8) | ord(header, 12);
|
||||
|
||||
let remaining_page_size = page_size - 16;
|
||||
if (overflow > 0) {
|
||||
remaining_page_size += overflow * page_size;
|
||||
}
|
||||
|
||||
let page_body = f.read(remaining_page_size);
|
||||
if (!page_body || length(page_body) < remaining_page_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
let page_data = header + page_body;
|
||||
|
||||
if (flags == 0x02 && count > 0) {
|
||||
let elem_offset = 16;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (elem_offset + 16 > length(page_data)) break;
|
||||
|
||||
let pos = (ord(page_data, elem_offset + 7) << 24) |
|
||||
(ord(page_data, elem_offset + 6) << 16) |
|
||||
(ord(page_data, elem_offset + 5) << 8) |
|
||||
ord(page_data, elem_offset + 4);
|
||||
let ksize = (ord(page_data, elem_offset + 11) << 24) |
|
||||
(ord(page_data, elem_offset + 10) << 16) |
|
||||
(ord(page_data, elem_offset + 9) << 8) |
|
||||
ord(page_data, elem_offset + 8);
|
||||
let vsize = (ord(page_data, elem_offset + 15) << 24) |
|
||||
(ord(page_data, elem_offset + 14) << 16) |
|
||||
(ord(page_data, elem_offset + 13) << 8) |
|
||||
ord(page_data, elem_offset + 12);
|
||||
|
||||
let key_start = elem_offset + pos;
|
||||
if (key_start + ksize + vsize <= length(page_data)) {
|
||||
let key = substr(page_data, key_start, ksize);
|
||||
if (key == tag) {
|
||||
let raw_val = substr(page_data, key_start + ksize, vsize);
|
||||
// Parse varint length of the ruleset to skip cache header
|
||||
let offset = 1; // skip cache type (0x01)
|
||||
let len = 0;
|
||||
let shift = 0;
|
||||
while (offset < length(raw_val)) {
|
||||
let b = ord(raw_val, offset);
|
||||
len |= ((b & 0x7F) << shift);
|
||||
offset++;
|
||||
if (!(b & 0x80)) {
|
||||
break;
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
found_data = substr(raw_val, offset, len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
elem_offset += 16;
|
||||
}
|
||||
}
|
||||
if (found_data) break;
|
||||
}
|
||||
|
||||
f.close();
|
||||
|
||||
if (!found_data) {
|
||||
warn("tag " + tag + " not found in cache.db\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let out_dir = "/tmp/sing-box/rulesets";
|
||||
system("mkdir -p " + out_dir);
|
||||
|
||||
let out_path = out_dir + "/community_" + tag + ".srs";
|
||||
if (fs.writefile(out_path, found_data) == null) {
|
||||
warn("failed to write ruleset to " + out_path + "\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
print(out_path + "\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mode = ARGV[0] || "";
|
||||
|
||||
if (mode == "check-proxy")
|
||||
if (mode == "extract-ruleset")
|
||||
exit(extract_ruleset(ARGV[1] || ""));
|
||||
else if (mode == "check-proxy")
|
||||
exit(check_proxy());
|
||||
else if (mode == "check-nft")
|
||||
exit(check_nft());
|
||||
|
||||
@@ -133,16 +133,38 @@ function command_output_quiet_from_args(args) {
|
||||
return as_string(data);
|
||||
}
|
||||
|
||||
function log_to_kmsg(message, level) {
|
||||
if (getenv("LOGGER_LOG") != null)
|
||||
return false;
|
||||
|
||||
let priority = 6;
|
||||
let lvl = as_string(level || "info");
|
||||
if (lvl == "warn") priority = 4;
|
||||
else if (lvl == "fatal") priority = 3;
|
||||
else if (lvl == "debug") priority = 7;
|
||||
|
||||
let kmsg = fs.open("/dev/kmsg", "w");
|
||||
if (kmsg) {
|
||||
kmsg.write(sprintf("<%d>tachyon: [%s] %s\n", priority, lvl, as_string(message)));
|
||||
kmsg.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function log_debug(message) {
|
||||
run_args([ "logger", "-t", "tachyon", "[debug] " + as_string(message) ]);
|
||||
if (!log_to_kmsg(message, "debug"))
|
||||
run_args([ "logger", "-t", "tachyon", "[debug] " + as_string(message) ]);
|
||||
}
|
||||
|
||||
function log_warn(message) {
|
||||
run_args([ "logger", "-t", "tachyon", "[warn] " + as_string(message) ]);
|
||||
if (!log_to_kmsg(message, "warn"))
|
||||
run_args([ "logger", "-t", "tachyon", "[warn] " + as_string(message) ]);
|
||||
}
|
||||
|
||||
function log_fatal(message) {
|
||||
run_args([ "logger", "-t", "tachyon", "[fatal] " + as_string(message) ]);
|
||||
if (!log_to_kmsg(message, "fatal"))
|
||||
run_args([ "logger", "-t", "tachyon", "[fatal] " + as_string(message) ]);
|
||||
}
|
||||
|
||||
function strip_list_comment(line) {
|
||||
@@ -447,9 +469,25 @@ function nft_create_ifname_set(table, name) {
|
||||
}
|
||||
|
||||
function nft_add_set_elements(table, set_name, elements) {
|
||||
let cmd = [ "nft", "add", "element", "inet", table, set_name, "{ " + as_string(elements) + " }" ];
|
||||
let cmd_str = command_from_args(cmd);
|
||||
let stamp = clock();
|
||||
let tmp_path = sprintf("/tmp/nft_elements.%d.%d.tmp", stamp[0], stamp[1]);
|
||||
let rules_content = sprintf("add element inet %s %s { %s }\n", table, set_name, as_string(elements));
|
||||
|
||||
let write_stamp = clock();
|
||||
let real_tmp = sprintf("%s.write.%d.%d", tmp_path, write_stamp[0], write_stamp[1]);
|
||||
if (fs.writefile(real_tmp, rules_content) == null) {
|
||||
fs.unlink(real_tmp);
|
||||
return false;
|
||||
}
|
||||
if (!fs.rename(real_tmp, tmp_path)) {
|
||||
fs.unlink(real_tmp);
|
||||
return false;
|
||||
}
|
||||
|
||||
let cmd_str = sprintf("nft -f %s", shell_quote(tmp_path));
|
||||
let res = system(cmd_str + " 2>/tmp/nft_err.log");
|
||||
fs.unlink(tmp_path);
|
||||
|
||||
if (res != 0) {
|
||||
let err_msg = trim(as_string(fs.readfile("/tmp/nft_err.log") || ""));
|
||||
log_fatal("nft add element failed: cmd='" + cmd_str + "', code=" + res + ", err='" + err_msg + "'");
|
||||
@@ -1694,7 +1732,7 @@ function nft_add_extracted_ruleset_subnets(unscoped_path, scoped_path, label, ta
|
||||
}
|
||||
|
||||
if (!has_entries)
|
||||
run_args([ "logger", "-t", "tachyon", "[warn] " + as_string(label) + " has no ip_cidr entries for nftables" ]);
|
||||
log_warn(as_string(label) + " has no ip_cidr entries for nftables");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ function read_json_file(path) {
|
||||
|
||||
function write_text_file(path, text) {
|
||||
let result = fs.writefile(path, text);
|
||||
if (result == null)
|
||||
return false;
|
||||
if (type(result) == "boolean" && !result)
|
||||
if (result == null || (type(result) == "boolean" && !result)) {
|
||||
warn("WRITE_TEXT_FILE FAILED: path=", path, "\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -952,6 +952,13 @@ function subscription_urls_signature(section) {
|
||||
user_agent: connections.subscription_user_agent(section, entry),
|
||||
auto_hwid: connections.subscription_auto_hwid(section, entry) ? "1" : "0",
|
||||
hwid: connections.subscription_hwid(section, entry),
|
||||
custom_device_headers: connections.subscription_custom_device_headers(section, entry) ? "1" : "0",
|
||||
device_os: connections.subscription_device_os(section, entry),
|
||||
ver_os: connections.subscription_ver_os(section, entry),
|
||||
device_model: connections.subscription_device_model(section, entry),
|
||||
device_locale: connections.subscription_device_locale(section, entry),
|
||||
app_version: connections.subscription_app_version(section, entry),
|
||||
accept_language: connections.subscription_accept_language(section, entry),
|
||||
show_dashboard_metadata: connections.subscription_dashboard_metadata_enabled(section, entry) ? "1" : "0",
|
||||
prefix_nodes: connections.subscription_prefix_nodes(section, entry) ? "1" : "0",
|
||||
node_prefix: connections.subscription_node_prefix(section, entry),
|
||||
@@ -1390,6 +1397,8 @@ function sing_box_signature_body(settings, sections, servers, mwan3_active) {
|
||||
body = signature_add_value(body, "settings.dns_check_interval", option(settings, "dns_check_interval", "10s"));
|
||||
body = signature_add_value(body, "settings.dns_recovery_check_interval", option(settings, "dns_recovery_check_interval", "60s"));
|
||||
body = signature_add_value(body, "settings.dns_check_timeout", option(settings, "dns_check_timeout", "2s"));
|
||||
body = signature_add_value(body, "settings.dns_failure_threshold", option(settings, "dns_failure_threshold", "3"));
|
||||
body = signature_add_value(body, "settings.dns_recovery_threshold", option(settings, "dns_recovery_threshold", "3"));
|
||||
let dns_detour_enabled = bool_option_value(settings, "dns_detour_enabled", false);
|
||||
body = signature_add_value(body, "settings.dns_detour_enabled", dns_detour_enabled);
|
||||
if (dns_detour_enabled == "1")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
let fs = require("fs");
|
||||
let uci_core = require("core.uci");
|
||||
let common = require("core.common");
|
||||
let helpers = require("core.helpers");
|
||||
|
||||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
||||
@@ -18,6 +19,7 @@ let shell_quote = common.shell_quote;
|
||||
let command_from_args = common.command_from_args;
|
||||
let command_status = common.command_status;
|
||||
let command_success_from_args = common.command_success_from_args;
|
||||
let is_process_name_running = helpers.is_process_name_running;
|
||||
|
||||
function command_capture(command) {
|
||||
let pipe = fs.popen(command, "r");
|
||||
@@ -51,7 +53,23 @@ function remove_file(path) {
|
||||
}
|
||||
|
||||
function log_message(message, level) {
|
||||
command_success_from_args([ "logger", "-t", "tachyon", "[" + as_string(level || "info") + "] Watchdog: " + as_string(message) ]);
|
||||
let priority = 6;
|
||||
let lvl = as_string(level || "info");
|
||||
if (lvl == "warn" || lvl == "warning") {
|
||||
priority = 4;
|
||||
} else if (lvl == "err" || lvl == "error" || lvl == "fatal") {
|
||||
priority = 3;
|
||||
} else if (lvl == "debug") {
|
||||
priority = 7;
|
||||
}
|
||||
|
||||
let kmsg = fs.open("/dev/kmsg", "w");
|
||||
if (kmsg) {
|
||||
kmsg.write(sprintf("<%d>tachyon: [%s] Watchdog: %s\n", priority, lvl, as_string(message)));
|
||||
kmsg.close();
|
||||
} else {
|
||||
command_success_from_args([ "logger", "-t", "tachyon", "[" + lvl + "] Watchdog: " + as_string(message) ]);
|
||||
}
|
||||
}
|
||||
|
||||
function send_telegram_notification(message) {
|
||||
@@ -61,14 +79,27 @@ function send_telegram_notification(message) {
|
||||
}
|
||||
}
|
||||
|
||||
function process_running(pid) {
|
||||
return match(as_string(pid), /^[0-9]+$/) != null && fs.stat("/proc/" + pid) != null;
|
||||
function process_running(pid, expected_name) {
|
||||
if (match(as_string(pid), /^[0-9]+$/) == null)
|
||||
return false;
|
||||
if (expected_name != null && expected_name != "") {
|
||||
return is_process_name_running(pid, expected_name);
|
||||
}
|
||||
return fs.stat("/proc/" + pid) != null;
|
||||
}
|
||||
|
||||
function stop_runtime() {
|
||||
let pid = trim(fs.readfile(PID_FILE) || "");
|
||||
if (process_running(pid)) {
|
||||
command_success_from_args([ "kill", "-9", pid ]);
|
||||
if (process_running(pid, "ucode")) {
|
||||
command_success_from_args([ "kill", pid ]);
|
||||
let wait_limit = 50; // 5 seconds
|
||||
while (wait_limit > 0 && process_running(pid, "ucode")) {
|
||||
sleep(100);
|
||||
wait_limit--;
|
||||
}
|
||||
if (process_running(pid, "ucode")) {
|
||||
command_success_from_args([ "kill", "-9", pid ]);
|
||||
}
|
||||
}
|
||||
remove_file(PID_FILE);
|
||||
|
||||
@@ -76,6 +107,14 @@ function stop_runtime() {
|
||||
let hp_pid = trim(fs.readfile("/var/run/tachyon_honeypot_listener.pid") || "");
|
||||
if (process_running(hp_pid)) {
|
||||
command_success_from_args([ "kill", hp_pid ]);
|
||||
let wait_limit = 20; // 2 seconds
|
||||
while (wait_limit > 0 && process_running(hp_pid)) {
|
||||
sleep(100);
|
||||
wait_limit--;
|
||||
}
|
||||
if (process_running(hp_pid)) {
|
||||
command_success_from_args([ "kill", "-9", hp_pid ]);
|
||||
}
|
||||
}
|
||||
remove_file("/var/run/tachyon_honeypot_listener.pid");
|
||||
remove_file("/tmp/tachyon_honeypot.fifo");
|
||||
@@ -137,122 +176,7 @@ function smart_detect_add_domain(sec_name, domain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function smart_detect_run(last_time) {
|
||||
let cfg = settings();
|
||||
if (cfg.smart_detect != "1") return last_time;
|
||||
let now = time();
|
||||
if (now - last_time < 60) return last_time;
|
||||
|
||||
let seen = {};
|
||||
let seen_data = fs.readfile(SMART_DETECT_SEEN_FILE);
|
||||
if (seen_data) {
|
||||
try { seen = json(seen_data) || {}; } catch(e) {}
|
||||
}
|
||||
|
||||
// Parse recent sing-box log lines for direct dial failures
|
||||
let log_out = command_output_from_args(["logread", "-l", "150"]) || "";
|
||||
let domains_to_check = {};
|
||||
for (let line in split(log_out, "\n")) {
|
||||
let ll = lc(line);
|
||||
if (index(ll, "direct") < 0 && index(ll, "DIRECT") < 0) continue;
|
||||
if (index(ll, "failed") < 0 && index(ll, "timeout") < 0 && index(ll, "reset") < 0) continue;
|
||||
// Extract quoted hostname like "twitch.tv:443"
|
||||
let m = match(line, /"([a-zA-Z0-9][a-zA-Z0-9.-]{1,60}\.[a-zA-Z]{2,})(:[0-9]+)?"/);
|
||||
if (!m) m = match(line, /target[= ]([a-zA-Z0-9][a-zA-Z0-9.-]{1,60}\.[a-zA-Z]{2,})/);
|
||||
if (!m) continue;
|
||||
let domain = m[1];
|
||||
if (!domain || length(domain) < 5) continue;
|
||||
if (seen[domain]) continue;
|
||||
domains_to_check[domain] = true;
|
||||
}
|
||||
|
||||
let domain_list = keys(domains_to_check);
|
||||
if (length(domain_list) == 0) {
|
||||
return now;
|
||||
}
|
||||
|
||||
let sections = smart_detect_get_proxy_sections();
|
||||
if (length(sections) == 0) return now;
|
||||
|
||||
// Determine proxy address
|
||||
let proxy_addr = "127.0.0.1:4534";
|
||||
let sb_cfg_data = fs.readfile("/etc/sing-box/config.json");
|
||||
if (sb_cfg_data) {
|
||||
try {
|
||||
let sb_cfg = json(sb_cfg_data);
|
||||
if (sb_cfg.inbounds) {
|
||||
for (let inb in sb_cfg.inbounds) {
|
||||
if (inb.type == "http" || inb.type == "mixed") {
|
||||
proxy_addr = "127.0.0.1:" + as_string(inb.listen_port || 4534);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// Build ordered test list: smart_detect_sections UCI list → fallback to proxy sections
|
||||
let detect_sections = [];
|
||||
let raw_list = cfg.smart_detect_sections;
|
||||
if (type(raw_list) == "array") {
|
||||
detect_sections = raw_list;
|
||||
} else if (raw_list && trim(as_string(raw_list)) != "") {
|
||||
detect_sections = [ trim(as_string(raw_list)) ];
|
||||
}
|
||||
if (length(detect_sections) == 0) {
|
||||
detect_sections = sections; // fallback: all proxy sections in UCI order
|
||||
}
|
||||
|
||||
for (let domain in domain_list) {
|
||||
seen[domain] = now;
|
||||
// Verify it actually fails directly
|
||||
let direct_ok = command_success_from_args([
|
||||
"curl", "-s", "-I", "--connect-timeout", "4", "--max-time", "6",
|
||||
"https://" + domain
|
||||
]);
|
||||
if (direct_ok) continue;
|
||||
|
||||
// Try each section in priority order
|
||||
let added = false;
|
||||
for (let sec_name in detect_sections) {
|
||||
sec_name = trim(as_string(sec_name));
|
||||
if (sec_name == "") continue;
|
||||
|
||||
// Try via this section's proxy (we test with global proxy address first)
|
||||
let proxy_ok = command_success_from_args([
|
||||
"curl", "-s", "-I", "--connect-timeout", "5", "--max-time", "8",
|
||||
"--proxy", "http://" + proxy_addr,
|
||||
"https://" + domain
|
||||
]);
|
||||
if (!proxy_ok) continue;
|
||||
|
||||
// Works through proxy — add to this section
|
||||
log_message("Smart Detect: adding " + domain + " to section " + sec_name, "info");
|
||||
if (smart_detect_add_domain(sec_name, domain)) {
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.enabled == "1" && tcfg.bot_token && tcfg.admin_ids) {
|
||||
send_telegram_notification(
|
||||
"\ud83d\udd0d *Smart Detect*: `" + domain + "` \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e, \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u043a\u0441\u0438.\n\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0432 \u0441\u0435\u043a\u0446\u0438\u044e *" + sec_name + "*."
|
||||
);
|
||||
}
|
||||
added = true;
|
||||
break; // domain handled, move to next
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
log_message("Smart Detect: domain " + domain + " not handled by any section", "info");
|
||||
}
|
||||
}
|
||||
|
||||
// Prune seen cache (keep only last 24h)
|
||||
let clean = {};
|
||||
let cutoff = now - 86400;
|
||||
for (let k in keys(seen)) {
|
||||
if (seen[k] >= cutoff) clean[k] = seen[k];
|
||||
}
|
||||
fs.writefile(SMART_DETECT_SEEN_FILE, sprintf("%J", clean));
|
||||
return now;
|
||||
}
|
||||
|
||||
|
||||
function start_runtime() {
|
||||
@@ -279,7 +203,6 @@ function run_zero_rtt_prefetching() {
|
||||
let sec = sections[k];
|
||||
if (sec.enabled == "0") continue;
|
||||
|
||||
// Collect custom domains
|
||||
let list_val = sec.user_domains;
|
||||
let list_array = [];
|
||||
if (type(list_val) == "array") {
|
||||
@@ -309,149 +232,308 @@ function run_zero_rtt_prefetching() {
|
||||
let domain_list = keys(unique_domains);
|
||||
if (length(domain_list) == 0) return;
|
||||
|
||||
log_message("Zero-RTT Prefetcher: pre-resolving " + length(domain_list) + " domains...", "info");
|
||||
for (let dom in domain_list) {
|
||||
system("dig @127.0.0.1 " + shell_quote(dom) + " A </dev/null >/dev/null 2>&1 1000<&- &");
|
||||
log_message("Zero-RTT Prefetcher: pre-resolving " + length(domain_list) + " domains in batches...", "info");
|
||||
let batch = [];
|
||||
for (let i, dom in domain_list) {
|
||||
push(batch, shell_quote(dom));
|
||||
if (length(batch) >= 15 || i == length(domain_list) - 1) {
|
||||
let batch_cmd = "for d in " + join(" ", batch) + "; do dig @127.0.0.1 \"$d\" A >/dev/null 2>&1; done &";
|
||||
system(batch_cmd + " </dev/null >/dev/null 2>&1 1000<&-");
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function worker() {
|
||||
log_message("Watchdog daemon started.", "info");
|
||||
let uloop = null;
|
||||
let ubus = null;
|
||||
try { uloop = require("uloop"); } catch (e) {}
|
||||
try { ubus = require("ubus"); } catch (e) {}
|
||||
|
||||
system("mkfifo /tmp/tachyon_honeypot.fifo >/dev/null 2>&1");
|
||||
system("chmod 0660 /tmp/tachyon_honeypot.fifo >/dev/null 2>&1");
|
||||
let last_oom_time = 0;
|
||||
let last_restart_time = 0;
|
||||
let last_urltest_check = 0;
|
||||
let pending_smart_domains = {};
|
||||
let smart_detect_last_run = 0;
|
||||
|
||||
let cfg = settings();
|
||||
let ttl = cfg.honeypot_ttl || "86400";
|
||||
let nft_table = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||
|
||||
command_success_from_args(["sh", "-c", "kill -9 $(pgrep -f 'tail -f /tmp/tachyon_honeypot.fifo') 2>/dev/null"]);
|
||||
|
||||
system("tail -f /tmp/tachyon_honeypot.fifo | while read ip; do " +
|
||||
"if [ -n \"$ip\" ]; then " +
|
||||
"nft add element inet " + nft_table + " tachyon_honeypot { \"$ip\" timeout " + ttl + "s } >/dev/null 2>&1; " +
|
||||
"fi; done </dev/null >/dev/null 2>&1 1000<&- & echo $! > /var/run/tachyon_honeypot_listener.pid");
|
||||
|
||||
let zero_rtt_done = false;
|
||||
let smart_detect_last_run = 0;
|
||||
|
||||
while (true) {
|
||||
cfg = settings();
|
||||
if (cfg.recovery_bypass == "1") {
|
||||
sleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 0. Check pause file — skip health checks if proxy is intentionally paused
|
||||
if (check_auto_resume_pause()) {
|
||||
sleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Process Check
|
||||
let binary_name = "sing-box";
|
||||
let has_sections = false;
|
||||
let uci_sections = uci_core.get_all(CONFIG_NAME);
|
||||
if (uci_sections) {
|
||||
for (let k in keys(uci_sections)) {
|
||||
if (uci_sections[k][".type"] == "section") {
|
||||
has_sections = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pid = "";
|
||||
let proc = fs.opendir("/proc");
|
||||
if (proc) {
|
||||
let entry;
|
||||
while ((entry = proc.read()) != null) {
|
||||
if (match(entry, /^[0-9]+$/)) {
|
||||
let exe = fs.readlink("/proc/" + entry + "/exe") || "";
|
||||
let slash = rindex(exe, "/");
|
||||
if ((slash >= 0 ? substr(exe, slash + 1) : exe) == binary_name) {
|
||||
pid = entry;
|
||||
function check_tachyon_cli_running() {
|
||||
let running = false;
|
||||
let proc = fs.opendir("/proc");
|
||||
if (proc) {
|
||||
let entry;
|
||||
while ((entry = proc.read()) != null) {
|
||||
if (match(entry, /^[0-9]+$/)) {
|
||||
let cmdline = fs.readfile("/proc/" + entry + "/cmdline") || "";
|
||||
if (index(cmdline, "/usr/bin/tachyon") >= 0) {
|
||||
if (index(cmdline, "start") >= 0 || index(cmdline, "restart") >= 0 || index(cmdline, "reload") >= 0 || index(cmdline, "stop") >= 0) {
|
||||
running = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
proc.close();
|
||||
}
|
||||
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
let list_update_running = process_running(list_update_pid);
|
||||
proc.close();
|
||||
}
|
||||
return running;
|
||||
}
|
||||
|
||||
let tachyon_cli_running = false;
|
||||
proc = fs.opendir("/proc");
|
||||
if (proc) {
|
||||
let entry;
|
||||
while ((entry = proc.read()) != null) {
|
||||
if (match(entry, /^[0-9]+$/)) {
|
||||
let cmdline = fs.readfile("/proc/" + entry + "/cmdline") || "";
|
||||
if (index(cmdline, "/usr/bin/tachyon") >= 0) {
|
||||
if (index(cmdline, "start") >= 0 || index(cmdline, "restart") >= 0 || index(cmdline, "reload") >= 0 || index(cmdline, "stop") >= 0) {
|
||||
tachyon_cli_running = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
proc.close();
|
||||
}
|
||||
function handle_singbox_stop_event(reason) {
|
||||
let now = time();
|
||||
if (now - last_restart_time < 30) return;
|
||||
last_restart_time = now;
|
||||
|
||||
if (tachyon_cli_running) {
|
||||
sleep(10000);
|
||||
continue;
|
||||
}
|
||||
let cfg = settings();
|
||||
if (cfg.recovery_bypass == "1") return;
|
||||
if (check_auto_resume_pause()) return;
|
||||
if (check_tachyon_cli_running()) return;
|
||||
|
||||
if (pid == "" && has_sections && !list_update_running) {
|
||||
log_message("sing-box is stopped. Restarting Tachyon...", "warn");
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.notify_crash != "0") {
|
||||
send_telegram_notification("⚠️ *Watchdog:* sing-box остановлен. Перезапускаю службы Tachyon...");
|
||||
}
|
||||
system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 &");
|
||||
sleep(60000);
|
||||
continue;
|
||||
}
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
if (process_running(list_update_pid, "ucode")) return;
|
||||
|
||||
// 2. Firewall / NFT Check
|
||||
let routing_mode = cfg.routing_mode || "nftables";
|
||||
if (routing_mode == "nftables" && !list_update_running) {
|
||||
let out_nft = command_output_from_args(["nft", "list", "table", "inet", nft_table]);
|
||||
if (index(out_nft, "tproxy") < 0) {
|
||||
log_message("nftables rules are missing or corrupted. Rebuilding...", "warn");
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.notify_crash != "0") {
|
||||
send_telegram_notification("⚠️ *Watchdog:* правила nftables повреждены или отсутствуют. Выполняю пересборку...");
|
||||
}
|
||||
system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 &");
|
||||
sleep(60000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
log_message("sing-box is stopped (" + as_string(reason || "health check") + "). Restarting Tachyon...", "warn");
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.notify_crash != "0") {
|
||||
send_telegram_notification("⚠️ *Watchdog:* sing-box остановлен. Перезапускаю службы Tachyon...");
|
||||
}
|
||||
system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 &");
|
||||
}
|
||||
|
||||
// 3. Memory & OOM Mitigation
|
||||
let free_mb = -1;
|
||||
let mem_info = fs.readfile("/proc/meminfo") || "";
|
||||
for (let line in split(mem_info, "\n")) {
|
||||
if (index(line, "MemAvailable:") == 0) {
|
||||
let fields = split(trim(line), /[ \t]+/);
|
||||
if (length(fields) >= 2) {
|
||||
free_mb = int(fields[1]) / 1024;
|
||||
}
|
||||
function check_singbox_process() {
|
||||
let cfg = settings();
|
||||
if (cfg.recovery_bypass == "1") return;
|
||||
if (check_auto_resume_pause()) return;
|
||||
if (check_tachyon_cli_running()) return;
|
||||
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
if (process_running(list_update_pid, "ucode")) return;
|
||||
|
||||
// Fast-path check: verify if sing-box PID file exists and process is active
|
||||
let sb_pid = trim(fs.readfile("/var/run/sing-box.pid") || "");
|
||||
if (sb_pid != "" && process_running(sb_pid, "sing-box")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let has_sections = false;
|
||||
let uci_sections = uci_core.get_all(CONFIG_NAME);
|
||||
if (uci_sections) {
|
||||
for (let k in keys(uci_sections)) {
|
||||
if (uci_sections[k][".type"] == "section") {
|
||||
has_sections = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (free_mb >= 0 && free_mb < 15) {
|
||||
log_message("Low memory detected (" + free_mb + "MB). Clearing caches...", "warn");
|
||||
system("echo 3 > /proc/sys/vm/drop_caches");
|
||||
}
|
||||
}
|
||||
if (!has_sections) return;
|
||||
|
||||
let logread_out = command_output_from_args(["logread", "-l", "100"]);
|
||||
let logread_lower = lc(logread_out);
|
||||
if (index(logread_lower, "out of memory") >= 0 || index(logread_lower, "oom-killer") >= 0) {
|
||||
log_message("OOM event detected! Reducing GOMEMLIMIT scaling...", "err");
|
||||
let binary_name = "sing-box";
|
||||
let pid = "";
|
||||
let proc = fs.opendir("/proc");
|
||||
if (proc) {
|
||||
let entry;
|
||||
while ((entry = proc.read()) != null) {
|
||||
if (match(entry, /^[0-9]+$/)) {
|
||||
let exe = fs.readlink("/proc/" + entry + "/exe") || "";
|
||||
let slash = rindex(exe, "/");
|
||||
if ((slash >= 0 ? substr(exe, slash + 1) : exe) == binary_name) {
|
||||
pid = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
proc.close();
|
||||
}
|
||||
|
||||
if (pid == "") {
|
||||
handle_singbox_stop_event("process missing from /proc");
|
||||
}
|
||||
}
|
||||
|
||||
function check_firewall_rules() {
|
||||
let cfg = settings();
|
||||
let routing_mode = cfg.routing_mode || "nftables";
|
||||
let nft_table = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
if (process_running(list_update_pid, "ucode")) return;
|
||||
|
||||
if (routing_mode == "nftables") {
|
||||
let out_nft = command_output_from_args(["nft", "list", "table", "inet", nft_table]);
|
||||
if (index(out_nft, "tproxy") < 0) {
|
||||
log_message("nftables rules are missing or corrupted. Rebuilding...", "warn");
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.notify_crash != "0") {
|
||||
send_telegram_notification("⚠️ *Watchdog:* правила nftables повреждены или отсутствуют. Выполняю пересборку...");
|
||||
}
|
||||
system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 &");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function check_memory() {
|
||||
let free_mb = -1;
|
||||
let mem_info = fs.readfile("/proc/meminfo") || "";
|
||||
for (let line in split(mem_info, "\n")) {
|
||||
if (index(line, "MemAvailable:") == 0) {
|
||||
let fields = split(trim(line), /[ \t]+/);
|
||||
if (length(fields) >= 2) {
|
||||
free_mb = int(fields[1]) / 1024;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (free_mb >= 0 && free_mb < 15) {
|
||||
log_message("Low memory detected (" + free_mb + "MB). Clearing caches...", "warn");
|
||||
system("echo 3 > /proc/sys/vm/drop_caches");
|
||||
}
|
||||
}
|
||||
|
||||
function check_urltest_switches() {
|
||||
let now = time();
|
||||
if (now - last_urltest_check < 5) return;
|
||||
last_urltest_check = now;
|
||||
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.enabled != "1" || tcfg.notify_crash == "0") return;
|
||||
|
||||
let p_res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:4534/proxies"]));
|
||||
if (p_res && p_res.status == 0 && p_res.output) {
|
||||
try {
|
||||
let p_data = json(p_res.output);
|
||||
let proxies = p_data.proxies;
|
||||
for (let name in proxies) {
|
||||
let p = proxies[name];
|
||||
if (p.type == "URLTest" && p.now) {
|
||||
let last_now = trim(fs.readfile("/tmp/watchdog_urltest_" + name) || "");
|
||||
if (last_now != "" && last_now != p.now) {
|
||||
send_telegram_notification("🔀 *Watchdog:* Смена прокси в группе `" + name + "`\nНовый активный узел: `" + p.now + "`");
|
||||
}
|
||||
fs.writefile("/tmp/watchdog_urltest_" + name, p.now);
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function smart_detect_process_pending() {
|
||||
let cfg = settings();
|
||||
if (cfg.smart_detect != "1") {
|
||||
pending_smart_domains = {};
|
||||
return;
|
||||
}
|
||||
let now = time();
|
||||
if (now - smart_detect_last_run < 60) return;
|
||||
|
||||
let domain_list = keys(pending_smart_domains);
|
||||
if (length(domain_list) == 0) return;
|
||||
smart_detect_last_run = now;
|
||||
|
||||
let seen = {};
|
||||
let seen_data = fs.readfile(SMART_DETECT_SEEN_FILE);
|
||||
if (seen_data) {
|
||||
try { seen = json(seen_data) || {}; } catch(e) {}
|
||||
}
|
||||
|
||||
let candidate_domains = [];
|
||||
for (let dom in domain_list) {
|
||||
if (!seen[dom]) {
|
||||
push(candidate_domains, dom);
|
||||
}
|
||||
}
|
||||
pending_smart_domains = {};
|
||||
|
||||
if (length(candidate_domains) == 0) return;
|
||||
|
||||
let sections = smart_detect_get_proxy_sections();
|
||||
if (length(sections) == 0) return;
|
||||
|
||||
let proxy_addr = "127.0.0.1:4534";
|
||||
let sb_cfg_data = fs.readfile("/etc/sing-box/config.json");
|
||||
if (sb_cfg_data) {
|
||||
try {
|
||||
let sb_cfg = json(sb_cfg_data);
|
||||
if (sb_cfg.inbounds) {
|
||||
for (let inb in sb_cfg.inbounds) {
|
||||
if (inb.type == "http" || inb.type == "mixed") {
|
||||
proxy_addr = "127.0.0.1:" + as_string(inb.listen_port || 4534);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
let detect_sections = [];
|
||||
let raw_list = cfg.smart_detect_sections;
|
||||
if (type(raw_list) == "array") {
|
||||
detect_sections = raw_list;
|
||||
} else if (raw_list && trim(as_string(raw_list)) != "") {
|
||||
detect_sections = [ trim(as_string(raw_list)) ];
|
||||
}
|
||||
if (length(detect_sections) == 0) {
|
||||
detect_sections = sections;
|
||||
}
|
||||
|
||||
for (let domain in candidate_domains) {
|
||||
seen[domain] = now;
|
||||
let direct_ok = command_success_from_args([
|
||||
"curl", "-s", "-I", "--connect-timeout", "4", "--max-time", "6",
|
||||
"https://" + domain
|
||||
]);
|
||||
if (direct_ok) continue;
|
||||
|
||||
let added = false;
|
||||
for (let sec_name in detect_sections) {
|
||||
sec_name = trim(as_string(sec_name));
|
||||
if (sec_name == "") continue;
|
||||
|
||||
let proxy_ok = command_success_from_args([
|
||||
"curl", "-s", "-I", "--connect-timeout", "5", "--max-time", "8",
|
||||
"--proxy", "http://" + proxy_addr,
|
||||
"https://" + domain
|
||||
]);
|
||||
if (!proxy_ok) continue;
|
||||
|
||||
log_message("Smart Detect: adding " + domain + " to section " + sec_name, "info");
|
||||
if (smart_detect_add_domain(sec_name, domain)) {
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.enabled == "1" && tcfg.bot_token && tcfg.admin_ids) {
|
||||
send_telegram_notification(
|
||||
"🔍 *Smart Detect*: `" + domain + "` недоступен напрямую, работает через прокси.\nДобавлен в секцию *" + sec_name + "*."
|
||||
);
|
||||
}
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
log_message("Smart Detect: domain " + domain + " not handled by any section", "info");
|
||||
}
|
||||
}
|
||||
|
||||
let clean = {};
|
||||
let cutoff = now - 86400;
|
||||
for (let k in keys(seen)) {
|
||||
if (seen[k] >= cutoff) clean[k] = seen[k];
|
||||
}
|
||||
fs.writefile(SMART_DETECT_SEEN_FILE, sprintf("%J", clean));
|
||||
}
|
||||
|
||||
function handle_log_line(line) {
|
||||
if (!line || line == "") return;
|
||||
|
||||
// Fast keyword pre-filter: skip 95%+ of irrelevant log lines instantly
|
||||
if (index(line, "direct") < 0 && index(line, "DIRECT") < 0 &&
|
||||
index(line, "memory") < 0 && index(line, "oom") < 0 && index(line, "OOM") < 0 &&
|
||||
index(line, "URLTest") < 0 && index(line, "proxy") < 0) {
|
||||
return;
|
||||
}
|
||||
let line_lower = lc(line);
|
||||
|
||||
// 1. OOM Detection
|
||||
if (index(line_lower, "out of memory") >= 0 || index(line_lower, "oom-killer") >= 0) {
|
||||
let now = time();
|
||||
if (now - last_oom_time > 30) {
|
||||
last_oom_time = now;
|
||||
log_message("OOM event detected from syslog! Reducing GOMEMLIMIT scaling...", "err");
|
||||
send_telegram_notification("🚨 *Watchdog:* Обнаружено событие OOM (Out Of Memory)! Уменьшаю GOMEMLIMIT и перезапускаю службы...");
|
||||
let scale = 1.0;
|
||||
let scale_path = "/etc/tachyon/mem_scale";
|
||||
@@ -467,45 +549,164 @@ function worker() {
|
||||
system("logread -c >/dev/null 2>&1");
|
||||
command_status("/usr/bin/tachyon restart >/dev/null 2>&1");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Zero-RTT Prefetching
|
||||
if (!zero_rtt_done) {
|
||||
run_zero_rtt_prefetching();
|
||||
zero_rtt_done = true;
|
||||
}
|
||||
|
||||
// 5. Smart Detect — self-healing routing
|
||||
smart_detect_last_run = smart_detect_run(smart_detect_last_run);
|
||||
|
||||
// 6. URLTest Proxy Switch Alerts
|
||||
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
|
||||
if (tcfg.enabled == "1" && tcfg.notify_crash != "0") {
|
||||
let p_res = command_capture(command_from_args(["curl", "-s", "http://127.0.0.1:4534/proxies"]));
|
||||
if (p_res && p_res.status == 0 && p_res.output) {
|
||||
try {
|
||||
let p_data = json(p_res.output);
|
||||
let proxies = p_data.proxies;
|
||||
for (let name in proxies) {
|
||||
let p = proxies[name];
|
||||
if (p.type == "URLTest" && p.now) {
|
||||
let last_now = trim(fs.readfile("/tmp/watchdog_urltest_" + name) || "");
|
||||
if (last_now != "" && last_now != p.now) {
|
||||
send_telegram_notification("🔀 *Watchdog:* Смена прокси в группе `" + name + "`\nНовый активный узел: `" + p.now + "`");
|
||||
}
|
||||
fs.writefile("/tmp/watchdog_urltest_" + name, p.now);
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
// 2. Smart Detect candidate domain extraction
|
||||
let cfg = settings();
|
||||
if (cfg.smart_detect == "1") {
|
||||
if ((index(line_lower, "direct") >= 0 || index(line_lower, "DIRECT") >= 0) &&
|
||||
(index(line_lower, "failed") >= 0 || index(line_lower, "timeout") >= 0 || index(line_lower, "reset") >= 0)) {
|
||||
let m = match(line, /"([a-zA-Z0-9][a-zA-Z0-9.-]{1,60}\.[a-zA-Z]{2,})(:[0-9]+)?"/);
|
||||
if (!m) m = match(line, /target[= ]([a-zA-Z0-9][a-zA-Z0-9.-]{1,60}\.[a-zA-Z]{2,})/);
|
||||
if (m && m[1] && length(m[1]) >= 5) {
|
||||
pending_smart_domains[m[1]] = time();
|
||||
}
|
||||
}
|
||||
|
||||
sleep(15000);
|
||||
}
|
||||
|
||||
// 3. URLTest proxy switch notifications
|
||||
if (index(line, "URLTest") >= 0 || index(line_lower, "selected proxy") >= 0 || index(line_lower, "switch proxy") >= 0) {
|
||||
check_urltest_switches();
|
||||
}
|
||||
}
|
||||
|
||||
function setup_honeypot_listener() {
|
||||
system("mkfifo /tmp/tachyon_honeypot.fifo >/dev/null 2>&1");
|
||||
system("chmod 0660 /tmp/tachyon_honeypot.fifo >/dev/null 2>&1");
|
||||
|
||||
command_success_from_args(["sh", "-c", "kill -9 $(pgrep -f 'tail -f /tmp/tachyon_honeypot.fifo') 2>/dev/null"]);
|
||||
|
||||
let fifo_fd = fs.open("/tmp/tachyon_honeypot.fifo", "r+");
|
||||
if (uloop && fifo_fd) {
|
||||
try {
|
||||
uloop.handle(fifo_fd.fileno(), function(events) {
|
||||
let line;
|
||||
while ((line = fifo_fd.read("line")) != null) {
|
||||
let ip = trim(as_string(line));
|
||||
if (ip != "" && match(ip, /^[0-9a-fA-F:.]+$/) != null) {
|
||||
let cfg = settings();
|
||||
let ttl = cfg.honeypot_ttl || "86400";
|
||||
let nft_table = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||
command_success_from_args(["nft", "add", "element", "inet", nft_table, "tachyon_honeypot", "{", ip, "timeout", ttl + "s", "}"]);
|
||||
}
|
||||
}
|
||||
}, uloop.ULOOP_READ);
|
||||
} catch (e) {
|
||||
log_message("Failed to bind honeypot fifo to uloop: " + as_string(e), "warn");
|
||||
}
|
||||
} else {
|
||||
let cfg = settings();
|
||||
let ttl = cfg.honeypot_ttl || "86400";
|
||||
let nft_table = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||
system("tail -f /tmp/tachyon_honeypot.fifo | while read ip; do " +
|
||||
"if [ -n \"$ip\" ]; then " +
|
||||
"nft add element inet " + nft_table + " tachyon_honeypot { \"$ip\" timeout " + ttl + "s } >/dev/null 2>&1; " +
|
||||
"fi; done </dev/null >/dev/null 2>&1 1000<&- & echo $! > /var/run/tachyon_honeypot_listener.pid");
|
||||
}
|
||||
}
|
||||
|
||||
function setup_syslog_listener() {
|
||||
if (!uloop) return null;
|
||||
let log_pipe = fs.popen("logread -f 2>/dev/null", "r");
|
||||
if (!log_pipe) return null;
|
||||
|
||||
try {
|
||||
uloop.handle(log_pipe.fileno(), function(events) {
|
||||
let line;
|
||||
while ((line = log_pipe.read("line")) != null) {
|
||||
handle_log_line(trim(as_string(line)));
|
||||
}
|
||||
}, uloop.ULOOP_READ);
|
||||
} catch (e) {
|
||||
log_message("Failed to register syslog listener: " + as_string(e), "warn");
|
||||
}
|
||||
return log_pipe;
|
||||
}
|
||||
|
||||
function setup_ubus_listener() {
|
||||
if (!ubus || !uloop) return null;
|
||||
let conn = null;
|
||||
try { conn = ubus.connect(); } catch (e) {}
|
||||
if (!conn) return null;
|
||||
|
||||
try {
|
||||
conn.listen({
|
||||
"service.instance.stop": function(ev, msg) {
|
||||
if (type(msg) == "object" && msg.name == "sing-box") {
|
||||
handle_singbox_stop_event("ubus service.instance.stop event");
|
||||
}
|
||||
},
|
||||
"service.stop": function(ev, msg) {
|
||||
if (type(msg) == "object" && msg.name == "sing-box") {
|
||||
handle_singbox_stop_event("ubus service.stop event");
|
||||
}
|
||||
},
|
||||
"firewall.reload": function(ev, msg) {
|
||||
check_firewall_rules();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
log_message("Failed to register ubus listeners: " + as_string(e), "warn");
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
function worker() {
|
||||
log_message("Watchdog daemon started.", "info");
|
||||
|
||||
setup_honeypot_listener();
|
||||
run_zero_rtt_prefetching();
|
||||
|
||||
if (uloop) {
|
||||
try {
|
||||
uloop.init();
|
||||
} catch (e) {
|
||||
log_message("Failed to initialize uloop: " + as_string(e), "warn");
|
||||
}
|
||||
}
|
||||
|
||||
let log_pipe = setup_syslog_listener();
|
||||
let ubus_conn = setup_ubus_listener();
|
||||
|
||||
function perform_periodic_checks() {
|
||||
check_auto_resume_pause();
|
||||
check_singbox_process();
|
||||
check_firewall_rules();
|
||||
check_memory();
|
||||
smart_detect_process_pending();
|
||||
}
|
||||
|
||||
if (uloop) {
|
||||
let timer_cb;
|
||||
timer_cb = function() {
|
||||
try {
|
||||
perform_periodic_checks();
|
||||
} catch (e) {
|
||||
log_message("Error in periodic check: " + as_string(e), "err");
|
||||
}
|
||||
uloop.timer(120000, timer_cb);
|
||||
};
|
||||
uloop.timer(10000, timer_cb);
|
||||
|
||||
log_message("Watchdog running in event-driven uloop mode.", "info");
|
||||
uloop.run();
|
||||
} else {
|
||||
log_message("uloop not available. Running Watchdog in legacy fallback loop mode.", "warn");
|
||||
while (true) {
|
||||
perform_periodic_checks();
|
||||
sleep(60000);
|
||||
}
|
||||
}
|
||||
|
||||
if (log_pipe) log_pipe.close();
|
||||
if (ubus_conn) try { ubus_conn.close(); } catch (e) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function get_status() {
|
||||
let pid = trim(fs.readfile(PID_FILE) || "");
|
||||
if (process_running(pid)) {
|
||||
if (process_running(pid, "ucode")) {
|
||||
print("running (pid " + pid + ")\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ function now_seconds() {
|
||||
return int(clock()[0]);
|
||||
}
|
||||
|
||||
function choose_index(kind, state, current_index, timeout_seconds, recovery, strikes) {
|
||||
function choose_index(kind, state, current_index, timeout_seconds, recovery, strikes, failure_threshold, recovery_threshold) {
|
||||
let values = kind == "main" ? state.main_servers : state.bootstrap_servers;
|
||||
if (length(values) <= 1)
|
||||
return { index: 0, reason: "single", alive: true };
|
||||
@@ -179,21 +179,38 @@ function choose_index(kind, state, current_index, timeout_seconds, recovery, str
|
||||
let ok = probe_port(kind, i, timeout_seconds);
|
||||
if (ok) {
|
||||
strikes[kind][i] = 0;
|
||||
if (strikes.recovery_successes && strikes.recovery_successes[kind]) {
|
||||
// Do not reset recovery successes on success, we count them!
|
||||
}
|
||||
} else {
|
||||
strikes[kind][i] = (strikes[kind][i] || 0) + 1;
|
||||
if (strikes.recovery_successes && strikes.recovery_successes[kind]) {
|
||||
strikes.recovery_successes[kind][i] = 0;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
};
|
||||
|
||||
if (recovery && current_index > 0) {
|
||||
for (let i = 0; i < current_index; i++)
|
||||
if (is_probe_ok(i))
|
||||
return { index: i, reason: "recovery", alive: true };
|
||||
for (let i = 0; i < current_index; i++) {
|
||||
if (is_probe_ok(i)) {
|
||||
let succ = (strikes.recovery_successes[kind][i] || 0) + 1;
|
||||
strikes.recovery_successes[kind][i] = succ;
|
||||
if (succ >= recovery_threshold) {
|
||||
strikes.recovery_successes[kind][i] = 0;
|
||||
return { index: i, reason: "recovery", alive: true };
|
||||
}
|
||||
} else {
|
||||
if (strikes.recovery_successes && strikes.recovery_successes[kind]) {
|
||||
strikes.recovery_successes[kind][i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { index: current_index, reason: "unchanged", alive: true };
|
||||
}
|
||||
|
||||
let current_ok = is_probe_ok(current_index);
|
||||
if (current_ok || strikes[kind][current_index] < 3)
|
||||
if (current_ok || (strikes[kind][current_index] || 0) < failure_threshold)
|
||||
return { index: current_index, reason: "alive", alive: true };
|
||||
|
||||
for (let i = 0; i < length(values); i++) {
|
||||
@@ -278,7 +295,7 @@ function worker() {
|
||||
let next_active = now_seconds();
|
||||
let next_recovery = now_seconds() + recovery_interval;
|
||||
let all_down = { main: false, bootstrap: false };
|
||||
let strikes = { main: {}, bootstrap: {} };
|
||||
let strikes = { main: {}, bootstrap: {}, recovery_successes: { main: {}, bootstrap: {} } };
|
||||
|
||||
while (true) {
|
||||
let now = now_seconds();
|
||||
@@ -286,14 +303,17 @@ function worker() {
|
||||
if (!runtime_dns.state_matches(runtime_dns.state_template(current_cfg), state))
|
||||
return 0;
|
||||
|
||||
let failure_threshold = int(common.option(current_cfg, "dns_failure_threshold", "3"));
|
||||
let recovery_threshold = int(common.option(current_cfg, "dns_recovery_threshold", "3"));
|
||||
|
||||
if (now >= next_active) {
|
||||
let bootstrap = choose_index("bootstrap", state, int(state.bootstrap_index), timeout_seconds, false, strikes);
|
||||
let bootstrap = choose_index("bootstrap", state, int(state.bootstrap_index), timeout_seconds, false, strikes, failure_threshold, recovery_threshold);
|
||||
if (!bootstrap.alive && !all_down.bootstrap)
|
||||
log_message("all configured bootstrap DNS servers are unavailable", "warn");
|
||||
all_down.bootstrap = !bootstrap.alive;
|
||||
|
||||
if (bootstrap.index != int(state.bootstrap_index)) {
|
||||
let main = choose_index("main", state, int(state.main_index), timeout_seconds, false, strikes);
|
||||
let main = choose_index("main", state, int(state.main_index), timeout_seconds, false, strikes, failure_threshold, recovery_threshold);
|
||||
let selections = { bootstrap };
|
||||
if (main.alive)
|
||||
selections.main = main;
|
||||
@@ -301,7 +321,7 @@ function worker() {
|
||||
next_active = now_seconds() + (applied && !main.alive ? 1 : active_interval);
|
||||
}
|
||||
else {
|
||||
let main = choose_index("main", state, int(state.main_index), timeout_seconds, false, strikes);
|
||||
let main = choose_index("main", state, int(state.main_index), timeout_seconds, false, strikes, failure_threshold, recovery_threshold);
|
||||
if (!main.alive && !all_down.main)
|
||||
log_message("all configured main DNS servers are unavailable", "warn");
|
||||
all_down.main = !main.alive;
|
||||
@@ -311,8 +331,8 @@ function worker() {
|
||||
}
|
||||
|
||||
if (now >= next_recovery) {
|
||||
let bootstrap = choose_index("bootstrap", state, int(state.bootstrap_index), timeout_seconds, true, strikes);
|
||||
let main = choose_index("main", state, int(state.main_index), timeout_seconds, true, strikes);
|
||||
let bootstrap = choose_index("bootstrap", state, int(state.bootstrap_index), timeout_seconds, true, strikes, failure_threshold, recovery_threshold);
|
||||
let main = choose_index("main", state, int(state.main_index), timeout_seconds, true, strikes, failure_threshold, recovery_threshold);
|
||||
let has_changes = bootstrap.index != int(state.bootstrap_index) || main.index != int(state.main_index);
|
||||
let applied = apply_selections(state, { bootstrap, main });
|
||||
let completed = now_seconds();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -281,6 +281,10 @@ function source_hwid_path(source_section) {
|
||||
return TMP_SUBSCRIPTION_FOLDER + "/" + source_section + ".hwid";
|
||||
}
|
||||
|
||||
function source_device_headers_path(source_section) {
|
||||
return TMP_SUBSCRIPTION_FOLDER + "/" + source_section + ".device_headers";
|
||||
}
|
||||
|
||||
function hwid_matches_config(configured_hwid, cached_hwid) {
|
||||
configured_hwid = as_string(configured_hwid);
|
||||
cached_hwid = as_string(cached_hwid);
|
||||
@@ -290,11 +294,12 @@ function hwid_matches_config(configured_hwid, cached_hwid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function source_cache_is_current(source_section, source_entry, expected_user_agent, expected_hwid) {
|
||||
function source_cache_is_current(source_section, source_entry, expected_user_agent, expected_hwid, expected_device_headers_signature) {
|
||||
let parsed = parse_source_entry(source_entry);
|
||||
let cached_url = file_first_line(TMP_SUBSCRIPTION_FOLDER + "/" + source_section + ".url");
|
||||
let cached_user_agent = file_first_line(TMP_SUBSCRIPTION_FOLDER + "/" + source_section + ".user_agent");
|
||||
let cached_hwid = file_first_line(source_hwid_path(source_section));
|
||||
let cached_device_headers = file_first_line(source_device_headers_path(source_section));
|
||||
let configured_user_agent = as_string(expected_user_agent || "");
|
||||
if (configured_user_agent != "")
|
||||
parsed.user_agent = configured_user_agent;
|
||||
@@ -308,6 +313,9 @@ function source_cache_is_current(source_section, source_entry, expected_user_age
|
||||
if (!hwid_matches_config(expected_hwid, cached_hwid))
|
||||
return false;
|
||||
|
||||
if (cached_device_headers != as_string(expected_device_headers_signature))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,8 @@ function cache_name_from_file(path) {
|
||||
return substr(file_name, 0, length(file_name) - 11);
|
||||
if (ends_with(file_name, ".hwid"))
|
||||
return substr(file_name, 0, length(file_name) - 5);
|
||||
if (ends_with(file_name, ".device_headers"))
|
||||
return substr(file_name, 0, length(file_name) - 15);
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -489,6 +491,10 @@ function source_hwid_path(dir, source_section) {
|
||||
return as_string(dir) + "/" + as_string(source_section) + ".hwid";
|
||||
}
|
||||
|
||||
function source_device_headers_path(dir, source_section) {
|
||||
return as_string(dir) + "/" + as_string(source_section) + ".device_headers";
|
||||
}
|
||||
|
||||
function read_text(path) {
|
||||
let data = fs.readfile(as_string(path));
|
||||
return data == null ? "" : as_string(data);
|
||||
@@ -724,7 +730,34 @@ function hwid_matches_config(configured_hwid, cached_hwid) {
|
||||
return cached_hwid == "" || cached_hwid == generate_hwid();
|
||||
}
|
||||
|
||||
function restore_persistent_subscription_cache(source_section, tmp_dir, persistent_dir, expected_url, expected_user_agent, expected_hwid, default_user_agent) {
|
||||
function device_request_headers(headers) {
|
||||
headers = object_or_empty(headers);
|
||||
if (!headers.enabled) {
|
||||
return [
|
||||
"X-Device-OS: OpenWrt Linux",
|
||||
"X-Device-Model: " + get_device_model(),
|
||||
"X-Ver-OS: " + get_kernel_version(),
|
||||
"Accept-Language: ru-RU,en,*",
|
||||
"X-Device-Locale: EN"
|
||||
];
|
||||
}
|
||||
|
||||
let result = [];
|
||||
for (let item in [
|
||||
{ name: "X-Device-OS", value: headers.device_os },
|
||||
{ name: "X-Ver-OS", value: headers.ver_os },
|
||||
{ name: "X-Device-Model", value: headers.device_model },
|
||||
{ name: "X-Device-Locale", value: headers.device_locale },
|
||||
{ name: "X-App-Version", value: headers.app_version },
|
||||
{ name: "Accept-Language", value: headers.accept_language }
|
||||
]) {
|
||||
if (as_string(item.value) != "")
|
||||
push(result, item.name + ": " + as_string(item.value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function restore_persistent_subscription_cache(source_section, tmp_dir, persistent_dir, expected_url, expected_user_agent, expected_hwid, default_user_agent, expected_device_headers_signature) {
|
||||
if (!cache_section_is_safe(source_section))
|
||||
return false;
|
||||
|
||||
@@ -739,17 +772,21 @@ function restore_persistent_subscription_cache(source_section, tmp_dir, persiste
|
||||
let cached_url = read_text(source_url_path(persistent_dir, source_section));
|
||||
let cached_user_agent = read_text(source_user_agent_path(persistent_dir, source_section));
|
||||
let cached_hwid = read_text(source_hwid_path(persistent_dir, source_section));
|
||||
let cached_device_headers = read_text(source_device_headers_path(persistent_dir, source_section));
|
||||
if (cached_url != as_string(expected_url))
|
||||
return false;
|
||||
if (!user_agent_matches_config(expected_user_agent, cached_user_agent, default_user_agent))
|
||||
return false;
|
||||
if (!hwid_matches_config(expected_hwid, cached_hwid))
|
||||
return false;
|
||||
if (cached_device_headers != as_string(expected_device_headers_signature))
|
||||
return false;
|
||||
|
||||
return copy_file(persistent_json, runtime_json) &&
|
||||
write_text(source_url_path(tmp_dir, source_section), cached_url) &&
|
||||
write_text(source_user_agent_path(tmp_dir, source_section), cached_user_agent) &&
|
||||
write_text(source_hwid_path(tmp_dir, source_section), cached_hwid);
|
||||
write_text(source_hwid_path(tmp_dir, source_section), cached_hwid) &&
|
||||
write_text(source_device_headers_path(tmp_dir, source_section), cached_device_headers);
|
||||
}
|
||||
|
||||
function subscription_source_profile(section, entry) {
|
||||
@@ -762,6 +799,8 @@ function subscription_source_profile(section, entry) {
|
||||
parsed.user_agent = user_agent;
|
||||
|
||||
parsed.hwid = connections.subscription_hwid(section, entry);
|
||||
parsed.device_headers = connections.subscription_device_headers(section, entry);
|
||||
parsed.device_headers_signature = connections.subscription_device_headers_signature(section, entry);
|
||||
parsed.download_section = connections.subscription_download_section(section, entry);
|
||||
parsed.update_enabled = connections.subscription_update_enabled(section, entry);
|
||||
parsed.update_interval = connections.subscription_update_interval(section, entry);
|
||||
@@ -769,10 +808,11 @@ function subscription_source_profile(section, entry) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function source_cache_profile_matches(parsed, cached_url, cached_user_agent, cached_hwid, default_user_agent) {
|
||||
function source_cache_profile_matches(parsed, cached_url, cached_user_agent, cached_hwid, default_user_agent, cached_device_headers) {
|
||||
return cached_url == as_string(parsed.url) &&
|
||||
user_agent_matches_config(parsed.user_agent, cached_user_agent, default_user_agent) &&
|
||||
hwid_matches_config(parsed.hwid, cached_hwid);
|
||||
hwid_matches_config(parsed.hwid, cached_hwid) &&
|
||||
as_string(cached_device_headers) == as_string(parsed.device_headers_signature);
|
||||
}
|
||||
|
||||
function section_current_usable_cache(section, tmp_dir, persistent_dir, default_user_agent) {
|
||||
@@ -796,7 +836,8 @@ function section_current_usable_cache(section, tmp_dir, persistent_dir, default_
|
||||
parsed.url,
|
||||
parsed.user_agent,
|
||||
parsed.hwid,
|
||||
default_user_agent
|
||||
default_user_agent,
|
||||
parsed.device_headers_signature
|
||||
))
|
||||
continue;
|
||||
|
||||
@@ -806,7 +847,8 @@ function section_current_usable_cache(section, tmp_dir, persistent_dir, default_
|
||||
let cached_url = read_text(source_url_path(tmp_dir, source_section));
|
||||
let cached_user_agent = read_text(source_user_agent_path(tmp_dir, source_section));
|
||||
let cached_hwid = read_text(source_hwid_path(tmp_dir, source_section));
|
||||
if (source_cache_profile_matches(parsed, cached_url, cached_user_agent, cached_hwid, default_user_agent))
|
||||
let cached_device_headers = read_text(source_device_headers_path(tmp_dir, source_section));
|
||||
if (source_cache_profile_matches(parsed, cached_url, cached_user_agent, cached_hwid, default_user_agent, cached_device_headers))
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1345,9 +1387,10 @@ function remove_subscription_source_runtime_cache(source_section) {
|
||||
unlink_path(source_url_path(TMP_SUBSCRIPTION_FOLDER, source_section));
|
||||
unlink_path(source_user_agent_path(TMP_SUBSCRIPTION_FOLDER, source_section));
|
||||
unlink_path(source_hwid_path(TMP_SUBSCRIPTION_FOLDER, source_section));
|
||||
unlink_path(source_device_headers_path(TMP_SUBSCRIPTION_FOLDER, source_section));
|
||||
}
|
||||
|
||||
function persist_subscription_cache(source_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_path) {
|
||||
function persist_subscription_cache(source_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_path, effective_device_headers_signature) {
|
||||
if (!cache_section_is_safe(source_section) || !subscription_cache_is_usable(subscription_json_path))
|
||||
return false;
|
||||
|
||||
@@ -1358,20 +1401,24 @@ function persist_subscription_cache(source_section, subscription_json_path, subs
|
||||
let persistent_url = source_url_path(TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR, source_section);
|
||||
let persistent_user_agent = source_user_agent_path(TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR, source_section);
|
||||
let persistent_hwid = source_hwid_path(TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR, source_section);
|
||||
let persistent_device_headers = source_device_headers_path(TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR, source_section);
|
||||
let persistent_metadata = persistent_metadata_path(source_section);
|
||||
let previous_url = read_text(persistent_url);
|
||||
let previous_user_agent = read_text(persistent_user_agent);
|
||||
let previous_hwid = read_text(persistent_hwid);
|
||||
let previous_device_headers = read_text(persistent_device_headers);
|
||||
let can_keep_previous_metadata = previous_url == as_string(subscription_url) &&
|
||||
previous_user_agent == as_string(effective_user_agent) &&
|
||||
previous_hwid == as_string(effective_hwid) &&
|
||||
previous_device_headers == as_string(effective_device_headers_signature) &&
|
||||
file_nonempty(persistent_metadata) &&
|
||||
object_has_extra_keys(persistent_metadata);
|
||||
|
||||
if (!copy_file_if_changed(subscription_json_path, persistent_json) ||
|
||||
!write_text_if_changed(persistent_url, subscription_url) ||
|
||||
!write_text_if_changed(persistent_user_agent, effective_user_agent) ||
|
||||
!write_text_if_changed(persistent_hwid, effective_hwid))
|
||||
!write_text_if_changed(persistent_hwid, effective_hwid) ||
|
||||
!write_text_if_changed(persistent_device_headers, effective_device_headers_signature))
|
||||
return false;
|
||||
|
||||
if (metadata_path != null && metadata_path != "" && file_nonempty(metadata_path) && object_has_extra_keys(metadata_path)) {
|
||||
@@ -1463,7 +1510,7 @@ function get_subscription_hwid(custom_hwid) {
|
||||
return custom_hwid != "" ? custom_hwid : generate_hwid();
|
||||
}
|
||||
|
||||
function download_subscription(url, filepath, http_proxy_address, headers_filepath, effective_user_agent, effective_hwid) {
|
||||
function download_subscription(url, filepath, http_proxy_address, headers_filepath, effective_user_agent, effective_hwid, device_headers) {
|
||||
let retries = 3;
|
||||
let wait_seconds = 2;
|
||||
let timeout = 15;
|
||||
@@ -1482,7 +1529,9 @@ function download_subscription(url, filepath, http_proxy_address, headers_filepa
|
||||
"curl", "-fL", "-sS",
|
||||
"--connect-timeout", timeout,
|
||||
"--speed-time", timeout,
|
||||
"--speed-limit", "1"
|
||||
"--speed-limit", "1",
|
||||
"--max-redirs", "5",
|
||||
"--max-filesize", "15728640"
|
||||
];
|
||||
|
||||
if (http_proxy_address != "") {
|
||||
@@ -1496,15 +1545,13 @@ function download_subscription(url, filepath, http_proxy_address, headers_filepa
|
||||
|
||||
push(args, "-o");
|
||||
push(args, tmpfile);
|
||||
for (let header in [
|
||||
let request_headers = [
|
||||
"User-Agent: " + get_subscription_user_agent(effective_user_agent),
|
||||
"X-HWID: " + get_subscription_hwid(effective_hwid),
|
||||
"X-Device-OS: OpenWrt Linux",
|
||||
"X-Device-Model: " + get_device_model(),
|
||||
"X-Ver-OS: " + get_kernel_version(),
|
||||
"Accept-Language: ru-RU,en,*",
|
||||
"X-Device-Locale: EN"
|
||||
]) {
|
||||
"X-HWID: " + get_subscription_hwid(effective_hwid)
|
||||
];
|
||||
for (let header in device_request_headers(device_headers))
|
||||
push(request_headers, header);
|
||||
for (let header in request_headers) {
|
||||
push(args, "-H");
|
||||
push(args, header);
|
||||
}
|
||||
@@ -1596,16 +1643,18 @@ function get_subscription_download_proxy_address(section_name_value, sections, p
|
||||
return address;
|
||||
}
|
||||
|
||||
function download_subscription_into_cache(section_name_value, subscription_url, subscription_json_path, subscription_url_cache_path, service_proxy_address_value, subscription_user_agent, subscription_hwid, source_index, cache_section, metadata_output_path, sections) {
|
||||
function download_subscription_into_cache(section_name_value, subscription_url, subscription_json_path, subscription_url_cache_path, service_proxy_address_value, subscription_user_agent, subscription_hwid, source_index, cache_section, metadata_output_path, sections, subscription_device_headers) {
|
||||
ensure_dir(TMP_SUBSCRIPTION_FOLDER);
|
||||
let subscription_user_agent_cache_path = source_user_agent_path(TMP_SUBSCRIPTION_FOLDER, cache_section);
|
||||
let subscription_hwid_cache_path = source_hwid_path(TMP_SUBSCRIPTION_FOLDER, cache_section);
|
||||
let subscription_device_headers_cache_path = source_device_headers_path(TMP_SUBSCRIPTION_FOLDER, cache_section);
|
||||
let raw_tmpfile = temp_path(TMP_SUBSCRIPTION_FOLDER, cache_section, "download");
|
||||
let headers_tmpfile = temp_path(TMP_SUBSCRIPTION_FOLDER, cache_section, "headers");
|
||||
let normalized_tmpfile = temp_path(TMP_SUBSCRIPTION_FOLDER, cache_section, "normalized");
|
||||
let metadata_tmpfile = temp_path(TMP_SUBSCRIPTION_FOLDER, cache_section, "metadata");
|
||||
let cached_user_agent = read_text(subscription_user_agent_cache_path);
|
||||
let default_user_agent = get_subscription_user_agent("");
|
||||
let subscription_device_headers_signature = connections.device_headers_signature(subscription_device_headers);
|
||||
let parser = subscription_parser();
|
||||
|
||||
let attempt_index = 0;
|
||||
@@ -1617,7 +1666,7 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
unlink_path(metadata_tmpfile);
|
||||
|
||||
let effective_hwid = get_subscription_hwid(subscription_hwid);
|
||||
let download_status = download_subscription(subscription_url, raw_tmpfile, service_proxy_address_value, headers_tmpfile, effective_user_agent, effective_hwid);
|
||||
let download_status = download_subscription(subscription_url, raw_tmpfile, service_proxy_address_value, headers_tmpfile, effective_user_agent, effective_hwid, subscription_device_headers);
|
||||
if (download_status != 0) {
|
||||
if (metadata_output_path != "")
|
||||
unlink_path(metadata_output_path);
|
||||
@@ -1651,7 +1700,7 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
if (!subscription_share_link.populate_subscription_file(normalized_tmpfile))
|
||||
log_message("Failed to cache direct proxy links for subscription rule '" + section_name_value + "'", "warn");
|
||||
|
||||
if (!subscription_config_is_current(section_name_value, subscription_url, subscription_user_agent, subscription_hwid, sections)) {
|
||||
if (!subscription_config_is_current(section_name_value, subscription_url, subscription_user_agent, subscription_hwid, sections, subscription_device_headers_signature)) {
|
||||
log_message("Subscription source settings changed while updating rule '" + section_name_value + "'; discarding superseded download", "warn");
|
||||
if (metadata_output_path != "")
|
||||
unlink_path(metadata_output_path);
|
||||
@@ -1666,7 +1715,8 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
write_text(subscription_url_cache_path, subscription_url);
|
||||
write_text(subscription_user_agent_cache_path, effective_user_agent);
|
||||
write_text(subscription_hwid_cache_path, effective_hwid);
|
||||
persist_subscription_cache(cache_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_tmpfile) ||
|
||||
write_text(subscription_device_headers_cache_path, subscription_device_headers_signature);
|
||||
persist_subscription_cache(cache_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_tmpfile, subscription_device_headers_signature) ||
|
||||
log_message("Failed to persist last working subscription cache for source '" + cache_section + "'", "warn");
|
||||
if (!file_nonempty(metadata_output_path))
|
||||
copy_persistent_metadata_output(cache_section, metadata_output_path);
|
||||
@@ -1691,7 +1741,8 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
write_text(subscription_url_cache_path, subscription_url);
|
||||
write_text(subscription_user_agent_cache_path, effective_user_agent);
|
||||
write_text(subscription_hwid_cache_path, effective_hwid);
|
||||
persist_subscription_cache(cache_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_tmpfile) ||
|
||||
write_text(subscription_device_headers_cache_path, subscription_device_headers_signature);
|
||||
persist_subscription_cache(cache_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_tmpfile, subscription_device_headers_signature) ||
|
||||
log_message("Failed to persist last working subscription cache for source '" + cache_section + "'", "warn");
|
||||
if (!file_nonempty(metadata_output_path))
|
||||
copy_persistent_metadata_output(cache_section, metadata_output_path);
|
||||
@@ -1714,7 +1765,8 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
write_text(subscription_url_cache_path, subscription_url);
|
||||
write_text(subscription_user_agent_cache_path, effective_user_agent);
|
||||
write_text(subscription_hwid_cache_path, effective_hwid);
|
||||
persist_subscription_cache(cache_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_tmpfile) ||
|
||||
write_text(subscription_device_headers_cache_path, subscription_device_headers_signature);
|
||||
persist_subscription_cache(cache_section, subscription_json_path, subscription_url, effective_user_agent, effective_hwid, metadata_tmpfile, subscription_device_headers_signature) ||
|
||||
log_message("Failed to persist last working subscription cache for source '" + cache_section + "'", "warn");
|
||||
if (!file_nonempty(metadata_output_path))
|
||||
copy_persistent_metadata_output(cache_section, metadata_output_path);
|
||||
@@ -1746,13 +1798,15 @@ function cached_source_status(source_section, parsed) {
|
||||
parsed.url,
|
||||
parsed.user_agent,
|
||||
parsed.hwid,
|
||||
get_subscription_user_agent("")
|
||||
get_subscription_user_agent(""),
|
||||
parsed.device_headers_signature
|
||||
);
|
||||
|
||||
let json_path = source_json_path(TMP_SUBSCRIPTION_FOLDER, source_section);
|
||||
let url_path = source_url_path(TMP_SUBSCRIPTION_FOLDER, source_section);
|
||||
let user_agent_path = source_user_agent_path(TMP_SUBSCRIPTION_FOLDER, source_section);
|
||||
let hwid_path = source_hwid_path(TMP_SUBSCRIPTION_FOLDER, source_section);
|
||||
let device_headers_path = source_device_headers_path(TMP_SUBSCRIPTION_FOLDER, source_section);
|
||||
let had_usable_cache = subscription_cache_is_usable(json_path);
|
||||
if (!had_usable_cache)
|
||||
unlink_path(json_path);
|
||||
@@ -1760,15 +1814,17 @@ function cached_source_status(source_section, parsed) {
|
||||
let cached_url = read_text(url_path);
|
||||
let cached_user_agent = read_text(user_agent_path);
|
||||
let cached_hwid = read_text(hwid_path);
|
||||
let cached_device_headers = read_text(device_headers_path);
|
||||
if (had_usable_cache &&
|
||||
!source_cache_profile_matches(parsed, cached_url, cached_user_agent, cached_hwid, get_subscription_user_agent(""))) {
|
||||
!source_cache_profile_matches(parsed, cached_url, cached_user_agent, cached_hwid, get_subscription_user_agent(""), cached_device_headers)) {
|
||||
remove_subscription_source_runtime_cache(source_section);
|
||||
return {
|
||||
usable: false,
|
||||
current: false,
|
||||
cached_url: "",
|
||||
cached_user_agent: "",
|
||||
cached_hwid: ""
|
||||
cached_hwid: "",
|
||||
cached_device_headers: ""
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1777,7 +1833,8 @@ function cached_source_status(source_section, parsed) {
|
||||
current: had_usable_cache,
|
||||
cached_url,
|
||||
cached_user_agent,
|
||||
cached_hwid
|
||||
cached_hwid,
|
||||
cached_device_headers
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1812,7 +1869,8 @@ function update_subscription_source(section_name_value, index_value, entry, phas
|
||||
index,
|
||||
source_section,
|
||||
as_string(metadata_output_path),
|
||||
sections
|
||||
sections,
|
||||
parsed.device_headers
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2128,7 +2186,8 @@ function ensure_subscription_source_for_prepare(state, section, source_index, en
|
||||
source_index,
|
||||
source_section,
|
||||
metadata_output_path,
|
||||
state.sections
|
||||
state.sections,
|
||||
parsed.device_headers
|
||||
);
|
||||
|
||||
if (update_result == 0 || update_result == 2) {
|
||||
@@ -2491,12 +2550,14 @@ else if (mode == "section-current-usable-cache") {
|
||||
else if (mode == "restore-persistent-source") {
|
||||
let expected_hwid = ARGV[7] == null ? "" : ARGV[6];
|
||||
let default_user_agent = ARGV[7] == null ? ARGV[6] : ARGV[7];
|
||||
exit(restore_persistent_subscription_cache(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], expected_hwid, default_user_agent) ? 0 : 1);
|
||||
let expected_device_headers_signature = ARGV[8] == null ? "" : ARGV[8];
|
||||
exit(restore_persistent_subscription_cache(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], expected_hwid, default_user_agent, expected_device_headers_signature) ? 0 : 1);
|
||||
}
|
||||
else if (mode == "persist-source-cache") {
|
||||
let effective_hwid = ARGV[6] == null ? "" : ARGV[5];
|
||||
let metadata_path = ARGV[6] == null ? ARGV[5] : ARGV[6];
|
||||
exit(persist_subscription_cache(ARGV[1], ARGV[2], ARGV[3], ARGV[4], effective_hwid, metadata_path) ? 0 : 1);
|
||||
let effective_device_headers_signature = ARGV[7] == null ? "" : ARGV[7];
|
||||
exit(persist_subscription_cache(ARGV[1], ARGV[2], ARGV[3], ARGV[4], effective_hwid, metadata_path, effective_device_headers_signature) ? 0 : 1);
|
||||
}
|
||||
else if (mode == "section-current-usable-cache-fixture") {
|
||||
let data = object_or_empty(read_json(ARGV[1]));
|
||||
|
||||
@@ -250,6 +250,11 @@ function json_decode_text(text) {
|
||||
}
|
||||
|
||||
function read_file(path) {
|
||||
let stat = fs.stat(path);
|
||||
if (stat != null && stat.size != null && stat.size > 15728640) {
|
||||
warn(sprintf("File %s exceeds maximum size limit (15MB)\n", path));
|
||||
return null;
|
||||
}
|
||||
return fs.readfile(path);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ grep -Fq 'text_list_values,' "$RULE_CONFIG" ||
|
||||
fail "config.rule must export the shared comment-aware text parser"
|
||||
grep -Fq 'rule_config.text_list_values(value, "comma-space")' "$VALIDATOR" ||
|
||||
fail "domain validation must use the shared comment-aware text parser"
|
||||
grep -Fq 'rule_config.text_list_values(option(section, "domain", ""), "comma-space")' "$GENERATOR" ||
|
||||
grep -Fq 'rule_config.text_list_values(option(section, "domain", ""), "comma-space")' "$TACHYON_LIB/singbox/generator_routes.uc" ||
|
||||
fail "sing-box domain generation must use the shared comment-aware text parser"
|
||||
grep -Fq 'return appendUniqueDomainTextValues(textValue, values);' "$SECTION_JS" ||
|
||||
fail "Domains field loading must preserve the original combined text"
|
||||
|
||||
+80
-38
@@ -36,6 +36,34 @@ if [ "$DOCKER_BIN" = "docker.exe" ]; then
|
||||
fi
|
||||
|
||||
|
||||
BUILDER_IMAGE="tachyon-builder"
|
||||
if ! "$DOCKER_BIN" image inspect "$BUILDER_IMAGE:latest" >/dev/null 2>&1; then
|
||||
echo "=== Creating cached builder image '$BUILDER_IMAGE:latest' ==="
|
||||
cat <<EOF > Dockerfile.builder
|
||||
FROM ubuntu:22.04
|
||||
RUN apt-get update && apt-get install -y \
|
||||
sudo \
|
||||
build-essential \
|
||||
curl \
|
||||
fakeroot \
|
||||
file \
|
||||
gawk \
|
||||
git \
|
||||
patch \
|
||||
python3 \
|
||||
rsync \
|
||||
tar \
|
||||
unzip \
|
||||
util-linux \
|
||||
wget \
|
||||
xz-utils \
|
||||
zstd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
EOF
|
||||
"$DOCKER_BIN" build -t "$BUILDER_IMAGE:latest" -f Dockerfile.builder .
|
||||
rm -f Dockerfile.builder
|
||||
fi
|
||||
|
||||
echo "=== Building Tachyon Package inside Ubuntu Container ==="
|
||||
VERSION="1.0.0"
|
||||
BUILD_OUT="$ROOT_DIR/build-out"
|
||||
@@ -45,60 +73,74 @@ mkdir -p "$BUILD_OUT"
|
||||
-v "$ROOT_DIR:/work" \
|
||||
-w /work \
|
||||
-e SDK_CACHE_DIR="/work/.wsl-build/sdk-cache" \
|
||||
ubuntu:22.04 \
|
||||
bash -c "apt-get update && apt-get install -y sudo && sed -i 's/\r$//' ./build.sh && bash ./build.sh $VERSION /work/build-out"
|
||||
"$BUILDER_IMAGE:latest" \
|
||||
bash -c "sed -i 's/\r$//' ./build.sh && bash ./build.sh $VERSION /work/build-out"
|
||||
|
||||
echo "=== Starting OpenWrt Container ==="
|
||||
CONTAINER_NAME="tachyon-e2e-test"
|
||||
"$DOCKER_BIN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
TEST_IMAGE="tachyon-test-image"
|
||||
|
||||
# Run with privileged flag to allow nftables and networking modifications
|
||||
"$DOCKER_BIN" run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--privileged \
|
||||
-e SB_REQUIRED_VERSION="1.11.0" \
|
||||
-v "$ROOT_DIR:/work" \
|
||||
openwrt/rootfs:x86-64-23.05.5 \
|
||||
/bin/sh /work/tests/container_entrypoint.sh
|
||||
if ! "$DOCKER_BIN" image inspect "$TEST_IMAGE:latest" >/dev/null 2>&1; then
|
||||
echo "=== Creating custom cached test image '$TEST_IMAGE:latest' ==="
|
||||
TEMP_CONTAINER="tachyon-test-temp"
|
||||
"$DOCKER_BIN" rm -f "$TEMP_CONTAINER" >/dev/null 2>&1 || true
|
||||
|
||||
"$DOCKER_BIN" run -d \
|
||||
--name "$TEMP_CONTAINER" \
|
||||
--privileged \
|
||||
openwrt/rootfs:x86-64-23.05.5 \
|
||||
/bin/sh -c "sleep 3600"
|
||||
|
||||
echo "Installing dependencies inside temp container..."
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" mkdir -p /var/lock
|
||||
for i in 1 2 3; do
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" opkg update && break || sleep 5
|
||||
done
|
||||
for i in 1 2 3 4 5; do
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" opkg install ucode ucode-mod-fs ucode-mod-uci curl ca-bundle bind-dig nftables ip-full coreutils-base64 sing-box bash node git git-http && break || sleep 5
|
||||
done
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" git config --global --add safe.directory '*' || true
|
||||
|
||||
echo "=== Installing official sing-box 1.12.0 binary ==="
|
||||
for i in 1 2 3; do
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" curl -sSL -o /tmp/sing-box.tar.gz https://github.com/SagerNet/sing-box/releases/download/v1.12.0/sing-box-1.12.0-linux-amd64.tar.gz && break || sleep 5
|
||||
done
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" tar -zxf /tmp/sing-box.tar.gz -C /tmp
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" cp /tmp/sing-box-1.12.0-linux-amd64/sing-box /usr/sbin/sing-box
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" cp /tmp/sing-box-1.12.0-linux-amd64/sing-box /usr/bin/sing-box
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" chmod +x /usr/sbin/sing-box /usr/bin/sing-box
|
||||
"$DOCKER_BIN" exec "$TEMP_CONTAINER" rm -rf /tmp/sing-box.tar.gz /tmp/sing-box-1.12.0-linux-amd64
|
||||
|
||||
"$DOCKER_BIN" commit "$TEMP_CONTAINER" "$TEST_IMAGE:latest"
|
||||
"$DOCKER_BIN" rm -f "$TEMP_CONTAINER"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
echo "=== Container logs (stdout/stderr) ==="
|
||||
"$DOCKER_BIN" logs "$CONTAINER_NAME" || true
|
||||
echo "=== OpenWrt system logs ==="
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" logread || true
|
||||
echo "=== Keeping Container Alive for Debugging ==="
|
||||
# "$DOCKER_BIN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
echo "=== Stopping Container ==="
|
||||
"$DOCKER_BIN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Starting OpenWrt Container from Cached Image ==="
|
||||
"$DOCKER_BIN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
"$DOCKER_BIN" run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--privileged \
|
||||
-e SB_REQUIRED_VERSION="1.11.0" \
|
||||
-e ENABLE_DEPRECATED_SPECIAL_OUTBOUNDS="true" \
|
||||
-v "$ROOT_DIR:/work" \
|
||||
"$TEST_IMAGE:latest" \
|
||||
/bin/sh /work/tests/container_entrypoint.sh
|
||||
|
||||
echo "Waiting for OpenWrt to initialize..."
|
||||
sleep 5
|
||||
|
||||
echo "=== Installing dependencies inside container ==="
|
||||
for i in 1 2 3; do
|
||||
echo "opkg update attempt $i..."
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg update && break || sleep 5
|
||||
done
|
||||
for i in 1 2 3 4 5; do
|
||||
echo "opkg install attempt $i..."
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg install ucode ucode-mod-fs ucode-mod-uci curl ca-bundle bind-dig nftables ip-full coreutils-base64 sing-box bash node git git-http && break || sleep 5
|
||||
done
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" git config --global --add safe.directory '*' || true
|
||||
|
||||
echo "=== Installing official sing-box 1.12.0 binary ==="
|
||||
for i in 1 2 3; do
|
||||
echo "Downloading sing-box attempt $i..."
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" curl -sSL -o /tmp/sing-box.tar.gz https://github.com/SagerNet/sing-box/releases/download/v1.12.0/sing-box-1.12.0-linux-amd64.tar.gz && break || sleep 5
|
||||
done
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" tar -zxf /tmp/sing-box.tar.gz -C /tmp
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" cp /tmp/sing-box-1.12.0-linux-amd64/sing-box /usr/sbin/sing-box
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" cp /tmp/sing-box-1.12.0-linux-amd64/sing-box /usr/bin/sing-box
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" chmod +x /usr/sbin/sing-box /usr/bin/sing-box
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" rm -rf /tmp/sing-box.tar.gz /tmp/sing-box-1.12.0-linux-amd64
|
||||
|
||||
|
||||
echo "=== Installing Tachyon IPK ==="
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg install /work/build-out/tachyon_1.0.0.ipk
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg install --force-reinstall /work/build-out/tachyon_1.0.0.ipk
|
||||
|
||||
echo "=== Configuring Tachyon ==="
|
||||
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.settings.enabled='1'
|
||||
|
||||
@@ -80,6 +80,16 @@ set -eo pipefail
|
||||
printf '\n'
|
||||
} >> "${NFT_LOG:?}"
|
||||
|
||||
if [ "$1" = "-f" ] && [ -f "$2" ]; then
|
||||
while read -r line || [ -n "$line" ]; do
|
||||
if [[ "$line" =~ ^add[[:space:]]+element[[:space:]]+inet[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+(.*)$ ]]; then
|
||||
printf 'nft\tadd\telement\tinet\t%s\t%s\t%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" >> "${NFT_LOG:?}"
|
||||
else
|
||||
printf '%s\n' "$line" >> "${NFT_LOG:?}"
|
||||
fi
|
||||
done < "$2"
|
||||
fi
|
||||
|
||||
if [ "$#" -eq 5 ] && [ "$1" = "list" ] && [ "$2" = "chain" ] &&
|
||||
[ "$3" = "inet" ] && [ "$5" = "mangle" ] && [ -n "${NFT_MANGLE_CHAIN_OUTPUT:-}" ]; then
|
||||
printf '%s\n' "$NFT_MANGLE_CHAIN_OUTPUT"
|
||||
|
||||
+2
-3
@@ -2,12 +2,11 @@
|
||||
set -eo pipefail
|
||||
|
||||
echo "=== Syntax Checking ==="
|
||||
find tachyon/files/usr/lib -name "*.uc" -print0 | xargs -0 -n1 ucode -c -o /dev/null
|
||||
find tachyon/files/usr/lib -name "*.uc" -print0 | xargs -0 -n1 ucode -S -c -o /dev/null
|
||||
bash tests/ucode_syntax_lint.sh
|
||||
|
||||
echo "=== Running Tests ==="
|
||||
for f in tests/*.sh; do
|
||||
if [ "$f" != "tests/run_all.sh" ] && [ "$f" != "tests/docker_e2e_test.sh" ] && [ "$f" != "tests/container_entrypoint.sh" ]; then
|
||||
if [ "$f" != "tests/run_all.sh" ] && [ "$f" != "tests/ucode_syntax_lint.sh" ] && [ "$f" != "tests/docker_e2e_test.sh" ] && [ "$f" != "tests/container_entrypoint.sh" ]; then
|
||||
echo "Running: $f"
|
||||
bash "$f"
|
||||
fi
|
||||
|
||||
@@ -549,6 +549,10 @@ prefer_ipv6
|
||||
60s
|
||||
[settings.dns_check_timeout]
|
||||
2s
|
||||
[settings.dns_failure_threshold]
|
||||
3
|
||||
[settings.dns_recovery_threshold]
|
||||
3
|
||||
[settings.dns_detour_enabled]
|
||||
0
|
||||
[settings.dns_rewrite_ttl]
|
||||
@@ -584,7 +588,7 @@ proxy
|
||||
[rule.proxy1.connection_urls]
|
||||
[ "vless://one", "vless://two" ]
|
||||
[rule.proxy1.subscription_urls]
|
||||
[ { "url": "https://example.com/sub.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "prefix_nodes": "1", "node_prefix": "Example", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
|
||||
[ { "url": "https://example.com/sub.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "custom_device_headers": "0", "device_os": "", "ver_os": "", "device_model": "", "device_locale": "", "app_version": "", "accept_language": "", "show_dashboard_metadata": "1", "prefix_nodes": "1", "node_prefix": "Example", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
|
||||
[rule.proxy1.interfaces]
|
||||
[ ]
|
||||
[rule.proxy1.outbound_jsons]
|
||||
@@ -1341,11 +1345,11 @@ dns
|
||||
[lists.dns_only.domain_ip_lists]
|
||||
https://example.com/dns-domains.lst
|
||||
[subscription.list_proxy.subscription_urls]
|
||||
[ { "url": "https://example.com/sub1.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" }, { "url": "https://example.com/sub2.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
|
||||
[ { "url": "https://example.com/sub1.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "custom_device_headers": "0", "device_os": "", "ver_os": "", "device_model": "", "device_locale": "", "app_version": "", "accept_language": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" }, { "url": "https://example.com/sub2.txt", "subscription_update_enabled": "1", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "custom_device_headers": "0", "device_os": "", "ver_os": "", "device_model": "", "device_locale": "", "app_version": "", "accept_language": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
|
||||
[subscription.list_proxy.subscription_update_interval]
|
||||
1h
|
||||
[subscription.sub_paused.subscription_urls]
|
||||
[ { "url": "https://example.com/off.txt", "subscription_update_enabled": "0", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
|
||||
[ { "url": "https://example.com/off.txt", "subscription_update_enabled": "0", "subscription_update_interval": "1h", "download_via_proxy_section": "", "auto_user_agent": "1", "user_agent": "", "auto_hwid": "1", "hwid": "", "custom_device_headers": "0", "device_os": "", "ver_os": "", "device_model": "", "device_locale": "", "app_version": "", "accept_language": "", "show_dashboard_metadata": "1", "prefix_nodes": "0", "node_prefix": "", "include_urltest_groups": "1", "hide_urltest_group_outbounds": "1", "hide_detour_outbounds": "1" } ]
|
||||
[subscription.sub_paused.subscription_update_interval]
|
||||
|
||||
EOF_CRON_SIG
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "=== Linting ucode files in $ROOT_DIR ==="
|
||||
|
||||
find "$ROOT_DIR/tachyon/files/usr/lib" -name "*.uc" -print0 | while IFS= read -r -d '' file; do
|
||||
echo "Checking: ${file#$ROOT_DIR/}"
|
||||
ucode -c "$file" -o /dev/null
|
||||
ucode -S -c "$file" -o /dev/null
|
||||
done
|
||||
|
||||
echo "All ucode files compiled successfully."
|
||||
Reference in New Issue
Block a user