Рефакторинг и оптимизация Tachyon: модульный генератор sing-box, атомарная запись файлов, graceful shutdown в watchdog, ускорение nftables, доработка трассировщика правил и локализация

This commit is contained in:
Dushnilin
2026-07-23 10:07:15 +03:00
parent 16b4c39693
commit 1409b6ed3d
32 changed files with 6730 additions and 4895 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+72 -8
View File
@@ -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",
+72 -8
View File
@@ -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": {
+1
View File
@@ -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,
+10 -9
View File
@@ -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))
+41
View File
@@ -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")
+108 -1
View File
@@ -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());
+44 -6
View File
@@ -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;
}
+3 -3
View File
@@ -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;
}
+9
View File
@@ -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")
+477 -276
View File
@@ -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;
}
+31 -11
View File
@@ -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;
}
+93 -32
View File
@@ -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);
}
+1 -1
View File
@@ -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
View File
@@ -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'
+10
View File
@@ -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
View File
@@ -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
+7 -3
View File
@@ -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
+15
View File
@@ -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."