fix(routing): scope excluded_ips to section rules, humanize section names, and add component visibility

This commit is contained in:
Dushnilin
2026-09-16 16:16:32 +03:00
parent 94d629b8bb
commit 0f5967e51c
24 changed files with 6356 additions and 5503 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+41 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-16 15:18+0300\n" "POT-Creation-Date: 2026-09-16 16:15+0300\n"
"PO-Revision-Date: 2026-09-16 15:18+0300\n" "PO-Revision-Date: 2026-09-16 16:15+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -311,6 +311,9 @@ msgstr "Chat ID администраторов"
msgid "Adult content" msgid "Adult content"
msgstr "Взрослый контент" msgstr "Взрослый контент"
msgid "Advanced DPI bypass engine with Lua scripts (nfqws2)"
msgstr ""
msgid "Advanced System" msgid "Advanced System"
msgstr "Расширенные настройки" msgstr "Расширенные настройки"
@@ -1079,6 +1082,9 @@ msgstr "Community-списки"
msgid "Component action timed out" msgid "Component action timed out"
msgstr "Превышено время ожидания действия компонента" msgstr "Превышено время ожидания действия компонента"
msgid "Component Cards Visibility"
msgstr ""
msgid "Component update available" msgid "Component update available"
msgstr "Доступно обновление компонента" msgstr "Доступно обновление компонента"
@@ -1184,6 +1190,9 @@ msgstr "Скопировать отчет для поддержки"
msgid "Core configuration and preferences" msgid "Core configuration and preferences"
msgstr "Основные настройки и параметры" msgstr "Основные настройки и параметры"
msgid "Core orchestration service and LuCI application"
msgstr ""
msgid "Could not compare FakeIP and control public IPs" msgid "Could not compare FakeIP and control public IPs"
msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки" msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки"
@@ -1253,6 +1262,9 @@ msgstr "Пользовательская стратегия добавлена!"
msgid "Customize the order and visibility of tabs in the main navigation menu. Use the arrows to reorder and checkboxes to show or hide tabs." msgid "Customize the order and visibility of tabs in the main navigation menu. Use the arrows to reorder and checkboxes to show or hide tabs."
msgstr "Настройте порядок и видимость вкладок в главном меню навигации. Используйте стрелки для перемещения и галочки для показа или скрытия." msgstr "Настройте порядок и видимость вкладок в главном меню навигации. Используйте стрелки для перемещения и галочки для показа или скрытия."
msgid "Customize which component cards are displayed in the Components tab. Checkboxes show or hide components."
msgstr ""
msgid "Daily Quota" msgid "Daily Quota"
msgstr "Дневная квота" msgstr "Дневная квота"
@@ -1286,6 +1298,9 @@ msgstr "Дни недели"
msgid "Dedicated cgroup unavailable" msgid "Dedicated cgroup unavailable"
msgstr "Выделенная cgroup недоступна" msgstr "Выделенная cgroup недоступна"
msgid "Dedicated direct HTTP/SOCKS5 outbound proxy"
msgstr ""
msgid "Default" msgid "Default"
msgstr "По умолчанию" msgstr "По умолчанию"
@@ -1370,6 +1385,9 @@ msgstr "Прямое (по умолчанию)"
msgid "Direct Bypass" msgid "Direct Bypass"
msgstr "Прямой WAN" msgstr "Прямой WAN"
msgid "Direct cgroup bypass for TorrServer"
msgstr ""
msgid "Direct connection" msgid "Direct connection"
msgstr "Прямое подключение" msgstr "Прямое подключение"
@@ -1622,6 +1640,9 @@ msgstr "Загрузка и применение наборов правил и
msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer." msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer."
msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных." msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных."
msgid "DPI bypass engine (nfqws v1)"
msgstr ""
msgid "DPI Detected" msgid "DPI Detected"
msgstr "DPI обнаружен" msgstr "DPI обнаружен"
@@ -2126,6 +2147,9 @@ msgstr "Семейные профили"
msgid "Family profiles and device grouping" msgid "Family profiles and device grouping"
msgstr "Семейные профили и группировка устройств" msgstr "Семейные профили и группировка устройств"
msgid "Fast packet transport network client"
msgstr ""
msgid "Faster DNS health monitoring than the default cycle. Switches bootstrap DNS on failure." msgid "Faster DNS health monitoring than the default cycle. Switches bootstrap DNS on failure."
msgstr "Более быстрый мониторинг состояния DNS, чем стандартный цикл. Переключает bootstrap DNS при сбое." msgstr "Более быстрый мониторинг состояния DNS, чем стандартный цикл. Переключает bootstrap DNS при сбое."
@@ -3071,6 +3095,9 @@ msgstr "Загрузка..."
msgid "Local Address" msgid "Local Address"
msgstr "Локальный адрес" msgstr "Локальный адрес"
msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr ""
msgid "Location" msgid "Location"
msgstr "Местоположение" msgstr "Местоположение"
@@ -3959,6 +3986,9 @@ msgstr "Ошибка чтения"
msgid "Ready to benchmark DNS providers" msgid "Ready to benchmark DNS providers"
msgstr "Готов к тестированию DNS провайдеров" msgstr "Готов к тестированию DNS провайдеров"
msgid "Real-time WebRTC anti-censorship tunnel"
msgstr ""
msgid "Reality fake site" msgid "Reality fake site"
msgstr "Фейковый сайт Reality" msgstr "Фейковый сайт Reality"
@@ -5489,6 +5519,9 @@ msgstr "Великобритания (gb)"
msgid "United States (us)" msgid "United States (us)"
msgstr "США (us)" msgstr "США (us)"
msgid "Universal proxy platform routing engine"
msgstr ""
msgid "unknown" msgid "unknown"
msgstr "неизвестно" msgstr "неизвестно"
@@ -5834,6 +5867,9 @@ msgstr "Будни (Пн-Пт)"
msgid "Weekends (Sat-Sun)" msgid "Weekends (Sat-Sun)"
msgstr "Выходные (Сб-Вс)" msgstr "Выходные (Сб-Вс)"
msgid "Western Digital Tunnel Transport"
msgstr ""
msgid "What Tachyon should do when this section matches" msgid "What Tachyon should do when this section matches"
msgstr "Что должен делать Tachyon при совпадении этой секции" msgstr "Что должен делать Tachyon при совпадении этой секции"
@@ -5942,6 +5978,9 @@ msgstr "Правила маршрутизации sing-box для Zapret2 нас
msgid "Zapret2 sing-box route rules are not configured" msgid "Zapret2 sing-box route rules are not configured"
msgstr "Правила маршрутизации sing-box для Zapret2 не настроены" msgstr "Правила маршрутизации sing-box для Zapret2 не настроены"
msgid "Zero config mesh VPN daemon"
msgstr ""
msgid "Testing strategy" msgid "Testing strategy"
msgstr "Тестирование стратегии" msgstr "Тестирование стратегии"
@@ -137,7 +137,7 @@ export function resetDirectClashApiState() {
} }
function getDisplayName(section: Tachyon.ConfigSection) { function getDisplayName(section: Tachyon.ConfigSection) {
return section.label || section['.name']; return section.label || (section as any).name || section['.name'];
} }
function getSettingsSection(configSections: Tachyon.ConfigSection[]) { function getSettingsSection(configSections: Tachyon.ConfigSection[]) {
@@ -1806,6 +1806,7 @@ export async function getDashboardSections(
code: outbound?.code || sectionName, code: outbound?.code || sectionName,
displayName: displayName:
section.label || section.label ||
(section as any).name ||
section.interface || section.interface ||
customName || customName ||
defaultLabel, defaultLabel,
@@ -1589,11 +1589,17 @@ function handleOpenLeakCheck() {
function handleOpenStrategyFuzzer() { function handleOpenStrategyFuzzer() {
getConfigSections() getConfigSections()
.then((sections) => { .then((sections) => {
const ruleNames = sections const ruleSections = sections
.filter((s) => s['.type'] === 'section' || s['.type'] === 'rule') .filter((s) => s['.type'] === 'section' || s['.type'] === 'rule')
.map((s) => s.name || s['.name']) .map((s) => ({
.filter((n): n is string => Boolean(n)); id: s['.name'],
renderStrategyFuzzerModal(ruleNames); label:
(typeof s.label === 'string' && s.label.trim()) ||
(typeof s.name === 'string' && s.name.trim()) ||
s['.name'],
}))
.filter((r) => Boolean(r.id));
renderStrategyFuzzerModal(ruleSections);
}) })
.catch(() => { .catch(() => {
renderStrategyFuzzerModal([]); renderStrategyFuzzerModal([]);
@@ -3,7 +3,17 @@ import { renderButton } from '../../../../partials';
import { showToast } from '../../../../helpers/showToast'; import { showToast } from '../../../../helpers/showToast';
import { Tachyon } from '../../../types'; import { Tachyon } from '../../../types';
export function renderStrategyFuzzerModal(ruleNames: string[] = []) { export interface FuzzerRuleSection {
id: string;
label: string;
}
export function renderStrategyFuzzerModal(
ruleNames: Array<string | FuzzerRuleSection> = [],
) {
const normalizedRules: FuzzerRuleSection[] = ruleNames.map((r) =>
typeof r === 'string' ? { id: r, label: r } : r,
);
let pollingInterval: ReturnType<typeof setInterval> | null = null; let pollingInterval: ReturnType<typeof setInterval> | null = null;
let isPolling = false; let isPolling = false;
@@ -292,7 +302,9 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
{ class: 'cbi-input-select', style: 'width: 100%;' }, { class: 'cbi-input-select', style: 'width: 100%;' },
[ [
E('option', { value: '', selected: true }, _('Provider Global Default')), E('option', { value: '', selected: true }, _('Provider Global Default')),
...ruleNames.map((r) => E('option', { value: r }, `${_('Rule:')} ${r}`)), ...normalizedRules.map((r) =>
E('option', { value: r.id }, `${_('Rule:')} ${r.label}`),
),
], ],
); );
ruleSelect.addEventListener('change', () => { ruleSelect.addEventListener('change', () => {
@@ -2035,19 +2047,23 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
selectedRuleSection, selectedRuleSection,
); );
if (res.success) { if (res.success) {
const targetLabel =
normalizedRules.find((r) => r.id === selectedRuleSection)?.label ||
selectedRuleSection ||
_('Global Default');
if (typeof ui?.addNotification === 'function') { if (typeof ui?.addNotification === 'function') {
ui.addNotification( ui.addNotification(
_('Tachyon'), _('Tachyon'),
E( E(
'p', 'p',
{}, {},
`${_('Strategy applied successfully and service reloaded!')} (${item.name} -> ${selectedRuleSection || _('Global Default')})`, `${_('Strategy applied successfully and service reloaded!')} (${item.name} -> ${targetLabel})`,
), ),
'info', 'info',
); );
} else { } else {
showToast( showToast(
`${_('Applied')} "${item.name}" -> ${selectedRuleSection || _('Global Default')}`, `${_('Applied')} "${item.name}" -> ${targetLabel}`,
'success', 'success',
); );
} }
@@ -169,4 +169,17 @@ describe('renderStrategyFuzzerModal', () => {
await new Promise((resolve) => setTimeout(resolve, 20)); await new Promise((resolve) => setTimeout(resolve, 20));
expect(TachyonShellMethods.getFuzzerStatus).toHaveBeenCalled(); expect(TachyonShellMethods.getFuzzerStatus).toHaveBeenCalled();
}); });
it('renders rule options with human-readable labels and UCI section IDs', () => {
renderStrategyFuzzerModal([
{ id: 'direct', label: 'Провайдер Direct' },
{ id: 'jobvpn', label: 'Работа VPN' },
]);
expect(mocks.showModal).toHaveBeenCalledTimes(1);
expect(mocks.showModal).toHaveBeenCalledWith(
'⚡ Strategy Fuzzer & Auto-Tuner',
expect.anything(),
);
});
}); });
@@ -182,7 +182,11 @@ function formatEndpoint(address?: string, port?: string | number): string {
} }
function getDisplayName(section: Tachyon.ConfigSection) { function getDisplayName(section: Tachyon.ConfigSection) {
return normalizeString(section.label) || section['.name']; return (
normalizeString(section.label) ||
normalizeString((section as any).name) ||
section['.name']
);
} }
function buildRouteDisplayNames(sections: Tachyon.ConfigSection[]) { function buildRouteDisplayNames(sections: Tachyon.ConfigSection[]) {
@@ -2029,6 +2029,33 @@ function renderComponentCard(card: ComponentCard) {
return E('div', { class: 'tachyon_updates-page__component' }, cardChildren); return E('div', { class: 'tachyon_updates-page__component' }, cardChildren);
} }
function isComponentCardVisible(
card: ComponentCard,
systemInfo: any,
): boolean {
const component = card.component;
if (component === 'tachyon' || component === 'sing_box') {
return true;
}
if (component === 'fptn') {
const fptnInstalled = Boolean(systemInfo.fptn_installed);
const fptnSupported =
systemInfo.fptn_supported === undefined
? true
: Boolean(systemInfo.fptn_supported);
if (!fptnInstalled && !fptnSupported) {
return false;
}
}
const optKey = `show_component_${component}`;
const val = (systemInfo as Record<string, any>)[optKey];
if (val === undefined || val === null || val === '') {
return true;
}
return String(val) === '1' || val === 1;
}
function renderUpdatesComponents() { function renderUpdatesComponents() {
const container = document.getElementById('tachyon_updates-components'); const container = document.getElementById('tachyon_updates-components');
@@ -2036,9 +2063,24 @@ function renderUpdatesComponents() {
return; return;
} }
const systemInfo = normalizeSingBoxVariantFields(
store.get().diagnosticsSystemInfo,
);
const visibleCards = getComponentCards().filter((card) =>
isComponentCardVisible(card, systemInfo),
);
const columns: Node[][] = [[], [], []]; const columns: Node[][] = [[], [], []];
getComponentCards().forEach((card) => { const colCounts = [0, 0, 0];
columns[card.column]?.push(renderComponentCard(card)); visibleCards.forEach((card) => {
colCounts[card.column] = (colCounts[card.column] || 0) + 1;
});
const hasEmptyColumn =
colCounts.some((c) => c === 0) && visibleCards.length >= 3;
visibleCards.forEach((card, idx) => {
const colIdx = hasEmptyColumn ? idx % 3 : card.column;
columns[colIdx]?.push(renderComponentCard(card));
}); });
return preserveScrollForPage(() => { return preserveScrollForPage(() => {
+10
View File
@@ -661,6 +661,7 @@ export namespace Tachyon {
tailscale_installed: 0 | 1; tailscale_installed: 0 | 1;
fptn_version: string; fptn_version: string;
fptn_installed: 0 | 1; fptn_installed: 0 | 1;
fptn_supported?: 0 | 1;
fptn_backup_version?: string; fptn_backup_version?: string;
fptn_backup_time?: number; fptn_backup_time?: number;
direct_bypass_enabled?: 0 | 1; direct_bypass_enabled?: 0 | 1;
@@ -670,6 +671,15 @@ export namespace Tachyon {
torrserver_direct_available?: 0 | 1; torrserver_direct_available?: 0 | 1;
torrserver_direct_enabled?: 0 | 1; torrserver_direct_enabled?: 0 | 1;
torrserver_direct_active?: 0 | 1; torrserver_direct_active?: 0 | 1;
show_component_zapret?: 0 | 1;
show_component_zapret2?: 0 | 1;
show_component_byedpi?: 0 | 1;
show_component_wdtt?: 0 | 1;
show_component_olcrtc?: 0 | 1;
show_component_fptn?: 0 | 1;
show_component_tailscale?: 0 | 1;
show_component_direct_bypass?: 0 | 1;
show_component_torrserver_direct?: 0 | 1;
openwrt_version: string; openwrt_version: string;
device_model: string; device_model: string;
generated_at?: number; generated_at?: number;
@@ -4489,7 +4489,7 @@ var CLASH_API_FETCH_TIMEOUT_MS = 2500;
var directClashApiFailedAt = 0; var directClashApiFailedAt = 0;
var DIRECT_CLASH_API_COOLDOWN_MS = 1e4; var DIRECT_CLASH_API_COOLDOWN_MS = 1e4;
function getDisplayName(section) { function getDisplayName(section) {
return section.label || section[".name"]; return section.label || section.name || section[".name"];
} }
function getSettingsSection(configSections) { function getSettingsSection(configSections) {
return configSections.find((section) => section[".type"] === "settings"); return configSections.find((section) => section[".type"] === "settings");
@@ -5676,7 +5676,7 @@ async function getDashboardSections(options = {}) {
outbounds: [ outbounds: [
{ {
code: outbound?.code || sectionName, code: outbound?.code || sectionName,
displayName: section.label || section.interface || customName || defaultLabel, displayName: section.label || section.name || section.interface || customName || defaultLabel,
latency: outbound?.value?.history?.length ? outbound.value.history[0].delay > 0 ? outbound.value.history[0].delay : -1 : 0, latency: outbound?.value?.history?.length ? outbound.value.history[0].delay > 0 ? outbound.value.history[0].delay : -1 : 0,
type: ACTION_DISPLAY_NAMES[sectionAction || ""] || outbound?.value?.type || (sectionAction || "").toUpperCase(), type: ACTION_DISPLAY_NAMES[sectionAction || ""] || outbound?.value?.type || (sectionAction || "").toUpperCase(),
selected: true, selected: true,
@@ -13023,6 +13023,9 @@ function renderAiChatModal() {
// src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts // src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
function renderStrategyFuzzerModal(ruleNames = []) { function renderStrategyFuzzerModal(ruleNames = []) {
const normalizedRules = ruleNames.map(
(r) => typeof r === "string" ? { id: r, label: r } : r
);
let pollingInterval = null; let pollingInterval = null;
let isPolling = false; let isPolling = false;
let activeTab2 = "benchmark"; let activeTab2 = "benchmark";
@@ -13263,7 +13266,9 @@ function renderStrategyFuzzerModal(ruleNames = []) {
{ class: "cbi-input-select", style: "width: 100%;" }, { class: "cbi-input-select", style: "width: 100%;" },
[ [
E("option", { value: "", selected: true }, _("Provider Global Default")), E("option", { value: "", selected: true }, _("Provider Global Default")),
...ruleNames.map((r) => E("option", { value: r }, `${_("Rule:")} ${r}`)) ...normalizedRules.map(
(r) => E("option", { value: r.id }, `${_("Rule:")} ${r.label}`)
)
] ]
); );
ruleSelect.addEventListener("change", () => { ruleSelect.addEventListener("change", () => {
@@ -14773,19 +14778,20 @@ function renderStrategyFuzzerModal(ruleNames = []) {
selectedRuleSection selectedRuleSection
); );
if (res.success) { if (res.success) {
const targetLabel = normalizedRules.find((r) => r.id === selectedRuleSection)?.label || selectedRuleSection || _("Global Default");
if (typeof ui?.addNotification === "function") { if (typeof ui?.addNotification === "function") {
ui.addNotification( ui.addNotification(
_("Tachyon"), _("Tachyon"),
E( E(
"p", "p",
{}, {},
`${_("Strategy applied successfully and service reloaded!")} (${item.name} -> ${selectedRuleSection || _("Global Default")})` `${_("Strategy applied successfully and service reloaded!")} (${item.name} -> ${targetLabel})`
), ),
"info" "info"
); );
} else { } else {
showToast( showToast(
`${_("Applied")} "${item.name}" -> ${selectedRuleSection || _("Global Default")}`, `${_("Applied")} "${item.name}" -> ${targetLabel}`,
"success" "success"
); );
} }
@@ -17620,8 +17626,11 @@ function handleOpenLeakCheck() {
} }
function handleOpenStrategyFuzzer() { function handleOpenStrategyFuzzer() {
getConfigSections().then((sections) => { getConfigSections().then((sections) => {
const ruleNames = sections.filter((s) => s[".type"] === "section" || s[".type"] === "rule").map((s) => s.name || s[".name"]).filter((n) => Boolean(n)); const ruleSections = sections.filter((s) => s[".type"] === "section" || s[".type"] === "rule").map((s) => ({
renderStrategyFuzzerModal(ruleNames); id: s[".name"],
label: typeof s.label === "string" && s.label.trim() || typeof s.name === "string" && s.name.trim() || s[".name"]
})).filter((r) => Boolean(r.id));
renderStrategyFuzzerModal(ruleSections);
}).catch(() => { }).catch(() => {
renderStrategyFuzzerModal([]); renderStrategyFuzzerModal([]);
}); });
@@ -18642,7 +18651,7 @@ function formatEndpoint(address, port) {
return `${normalizedAddress}:${normalizedPort}`; return `${normalizedAddress}:${normalizedPort}`;
} }
function getDisplayName2(section) { function getDisplayName2(section) {
return normalizeString(section.label) || section[".name"]; return normalizeString(section.label) || normalizeString(section.name) || section[".name"];
} }
function buildRouteDisplayNames(sections) { function buildRouteDisplayNames(sections) {
const map = { const map = {
@@ -22646,14 +22655,45 @@ function renderComponentCard(card) {
cardChildren.push(actionsContainer); cardChildren.push(actionsContainer);
return E("div", { class: "tachyon_updates-page__component" }, cardChildren); return E("div", { class: "tachyon_updates-page__component" }, cardChildren);
} }
function isComponentCardVisible(card, systemInfo) {
const component = card.component;
if (component === "tachyon" || component === "sing_box") {
return true;
}
if (component === "fptn") {
const fptnInstalled = Boolean(systemInfo.fptn_installed);
const fptnSupported = systemInfo.fptn_supported === void 0 ? true : Boolean(systemInfo.fptn_supported);
if (!fptnInstalled && !fptnSupported) {
return false;
}
}
const optKey = `show_component_${component}`;
const val = systemInfo[optKey];
if (val === void 0 || val === null || val === "") {
return true;
}
return String(val) === "1" || val === 1;
}
function renderUpdatesComponents() { function renderUpdatesComponents() {
const container = document.getElementById("tachyon_updates-components"); const container = document.getElementById("tachyon_updates-components");
if (!container) { if (!container) {
return; return;
} }
const systemInfo = normalizeSingBoxVariantFields(
store.get().diagnosticsSystemInfo
);
const visibleCards = getComponentCards().filter(
(card) => isComponentCardVisible(card, systemInfo)
);
const columns = [[], [], []]; const columns = [[], [], []];
getComponentCards().forEach((card) => { const colCounts = [0, 0, 0];
columns[card.column]?.push(renderComponentCard(card)); visibleCards.forEach((card) => {
colCounts[card.column] = (colCounts[card.column] || 0) + 1;
});
const hasEmptyColumn = colCounts.some((c) => c === 0) && visibleCards.length >= 3;
visibleCards.forEach((card, idx) => {
const colIdx = hasEmptyColumn ? idx % 3 : card.column;
columns[colIdx]?.push(renderComponentCard(card));
}); });
return preserveScrollForPage(() => { return preserveScrollForPage(() => {
container.replaceChildren( container.replaceChildren(
@@ -295,7 +295,10 @@ function createProfileContent(section) {
o.modalonly = false; o.modalonly = false;
o.cfgvalue = function (sectionId) { o.cfgvalue = function (sectionId) {
const avatar = uci.get(UCI_PACKAGE, sectionId, "avatar") || "👶"; const avatar = uci.get(UCI_PACKAGE, sectionId, "avatar") || "👶";
const label = uci.get(UCI_PACKAGE, sectionId, "label") || sectionId; const label =
uci.get(UCI_PACKAGE, sectionId, "label") ||
uci.get(UCI_PACKAGE, sectionId, "name") ||
sectionId;
const rawDevs = uci.get(UCI_PACKAGE, sectionId, "device_ip"); const rawDevs = uci.get(UCI_PACKAGE, sectionId, "device_ip");
const devCount = normalizeListValues(rawDevs).length; const devCount = normalizeListValues(rawDevs).length;
const devBadge = const devBadge =
@@ -313,7 +316,10 @@ function createProfileContent(section) {
}; };
o.textvalue = function (sectionId) { o.textvalue = function (sectionId) {
const avatar = uci.get(UCI_PACKAGE, sectionId, "avatar") || "👶"; const avatar = uci.get(UCI_PACKAGE, sectionId, "avatar") || "👶";
const label = uci.get(UCI_PACKAGE, sectionId, "label") || sectionId; const label =
uci.get(UCI_PACKAGE, sectionId, "label") ||
uci.get(UCI_PACKAGE, sectionId, "name") ||
sectionId;
return `${avatar} ${label}`; return `${avatar} ${label}`;
}; };
@@ -82,7 +82,7 @@ function getUciSectionName(section) {
} }
function getUciSectionLabel(section) { function getUciSectionLabel(section) {
return (section && section.label) || getUciSectionName(section); return (section && section.label) || (section && section.name) || getUciSectionName(section);
} }
function isOutboundDetourTargetSection(section, currentSectionId) { function isOutboundDetourTargetSection(section, currentSectionId) {
@@ -7767,7 +7767,11 @@ function createSectionContent(section) {
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
o.load = function (section_id) { o.load = function (section_id) {
return uci.get(UCI_PACKAGE, section_id, "label") || section_id; return (
uci.get(UCI_PACKAGE, section_id, "label") ||
uci.get(UCI_PACKAGE, section_id, "name") ||
section_id
);
}; };
o = section.taboption( o = section.taboption(
@@ -11891,7 +11895,9 @@ async function performTrace(query) {
(type === "domain" && detourDomains.includes(queryForMatching)); (type === "domain" && detourDomains.includes(queryForMatching));
if (isDnsMatch) { if (isDnsMatch) {
const label = const label =
uci.get(UCI_PACKAGE, detourSection, "label") || detourSection; uci.get(UCI_PACKAGE, detourSection, "label") ||
uci.get(UCI_PACKAGE, detourSection, "name") ||
detourSection;
const action = const action =
uci.get(UCI_PACKAGE, detourSection, "action") || "connection"; uci.get(UCI_PACKAGE, detourSection, "action") || "connection";
return { return {
@@ -11916,7 +11922,10 @@ async function performTrace(query) {
continue; continue;
} }
const label = uci.get(UCI_PACKAGE, secName, "label") || secName; const label =
uci.get(UCI_PACKAGE, secName, "label") ||
uci.get(UCI_PACKAGE, secName, "name") ||
secName;
const action = uci.get(UCI_PACKAGE, secName, "action") || "connection"; const action = uci.get(UCI_PACKAGE, secName, "action") || "connection";
if (type === "port") { if (type === "port") {
@@ -12176,7 +12185,10 @@ async function performTrace(query) {
const secName = sec[".name"]; const secName = sec[".name"];
if (uci.get(UCI_PACKAGE, secName, "enabled") === "0") continue; if (uci.get(UCI_PACKAGE, secName, "enabled") === "0") continue;
const label = uci.get(UCI_PACKAGE, secName, "label") || secName; const label =
uci.get(UCI_PACKAGE, secName, "label") ||
uci.get(UCI_PACKAGE, secName, "name") ||
secName;
const action = uci.get(UCI_PACKAGE, secName, "action") || "connection"; const action = uci.get(UCI_PACKAGE, secName, "action") || "connection";
const ipCidr = normalizeOptionValues( const ipCidr = normalizeOptionValues(
@@ -12574,7 +12586,10 @@ function createTracerSearchWidget(sectionRef) {
} }
function showSectionRulesModal(section_id) { function showSectionRulesModal(section_id) {
const sectionLabel = uci.get(UCI_PACKAGE, section_id, "label") || section_id; const sectionLabel =
uci.get(UCI_PACKAGE, section_id, "label") ||
uci.get(UCI_PACKAGE, section_id, "name") ||
section_id;
const getCleanList = (key) => { const getCleanList = (key) => {
let val = uci.get(UCI_PACKAGE, section_id, key); let val = uci.get(UCI_PACKAGE, section_id, key);
@@ -12822,7 +12837,7 @@ function configureSectionSection(sectionRef, options = {}) {
setActionProvidersAvailabilityLoader(options.loadActionProvidersAvailability); setActionProvidersAvailabilityLoader(options.loadActionProvidersAvailability);
sectionRef.sectiontitle = function (section_id) { sectionRef.sectiontitle = function (section_id) {
return uci.get(UCI_PACKAGE, section_id, "label") || section_id; return uci.get(UCI_PACKAGE, section_id, "label") || uci.get(UCI_PACKAGE, section_id, "name") || section_id;
}; };
sectionRef.load = function () { sectionRef.load = function () {
@@ -596,7 +596,11 @@ function applyServerCapabilities(sectionRef, capabilities) {
} }
function getServerName(sectionId) { function getServerName(sectionId) {
return uci.get(UCI_PACKAGE, sectionId, "label") || sectionId; return (
uci.get(UCI_PACKAGE, sectionId, "label") ||
uci.get(UCI_PACKAGE, sectionId, "name") ||
sectionId
);
} }
function getSocksUsername(sectionId) { function getSocksUsername(sectionId) {
@@ -1288,6 +1288,200 @@ function createMenuTabsOrderWidget(option, section_id) {
return wrapper; return wrapper;
} }
function createComponentsVisibilityWidget(option, section_id) {
const ALL_COMPONENTS = [
{
id: "tachyon",
label: "Tachyon",
desc: _("Core orchestration service and LuCI application"),
defaultVisible: true,
fixed: true,
},
{
id: "sing_box",
label: "Sing-box",
desc: _("Universal proxy platform routing engine"),
defaultVisible: true,
fixed: true,
},
{
id: "zapret",
label: "Zapret",
desc: _("DPI bypass engine (nfqws v1)"),
defaultVisible: true,
fixed: false,
},
{
id: "zapret2",
label: "Zapret2",
desc: _("Advanced DPI bypass engine with Lua scripts (nfqws2)"),
defaultVisible: true,
fixed: false,
},
{
id: "byedpi",
label: "ByeDPI",
desc: _("Local SOCKS proxy desynchronizer (ciadpi)"),
defaultVisible: true,
fixed: false,
},
{
id: "wdtt",
label: "WDTT",
desc: _("Western Digital Tunnel Transport"),
defaultVisible: true,
fixed: false,
},
{
id: "olcrtc",
label: "OlcRTC",
desc: _("Real-time WebRTC anti-censorship tunnel"),
defaultVisible: true,
fixed: false,
},
{
id: "fptn",
label: "FPTN",
desc: _("Fast packet transport network client"),
defaultVisible: true,
fixed: false,
},
{
id: "tailscale",
label: "Tailscale",
desc: _("Zero config mesh VPN daemon"),
defaultVisible: true,
fixed: false,
},
{
id: "direct_bypass",
label: _("Direct Proxy"),
desc: _("Dedicated direct HTTP/SOCKS5 outbound proxy"),
defaultVisible: true,
fixed: false,
},
{
id: "torrserver_direct",
label: _("TorrServer Direct"),
desc: _("Direct cgroup bypass for TorrServer"),
defaultVisible: true,
fixed: false,
},
];
const visibility = {};
ALL_COMPONENTS.forEach(function (c) {
if (c.fixed) {
visibility[c.id] = true;
} else {
const optKey = "show_component_" + c.id;
const val = uci.get(UCI_PACKAGE, section_id, optKey);
if (val === undefined || val === null || val === "") {
visibility[c.id] = c.defaultVisible;
} else {
visibility[c.id] = val === "1" || val === true;
}
}
});
const wrapper = E("div", { id: "components-visibility-widget-" + section_id });
const listEl = E("div", {
style:
"border:1px solid var(--border-color,#dee2e6);border-radius:6px;overflow:hidden;margin-bottom:8px;max-width:560px;background:var(--card-bg,transparent);",
});
function syncToUci() {
wrapper._vis = Object.assign({}, visibility);
ALL_COMPONENTS.forEach(function (c) {
if (!c.fixed) {
const optKey = "show_component_" + c.id;
uci.set(UCI_PACKAGE, section_id, optKey, visibility[c.id] ? "1" : "0");
}
});
}
function renderList() {
listEl.innerHTML = "";
ALL_COMPONENTS.forEach(function (c, idx) {
const isVisible = Boolean(visibility[c.id]);
const isFixed = Boolean(c.fixed);
const row = E("div", {
style: [
"display:flex;align-items:center;gap:12px;padding:8px 12px;",
idx < ALL_COMPONENTS.length - 1
? "border-bottom:1px solid var(--border-color,#dee2e6);"
: "",
isVisible ? "" : "opacity:0.5;background:rgba(0,0,0,0.02);",
].join(""),
});
const toggleBtn = E("button", {
type: "button",
title: isFixed
? _("Always visible")
: isVisible
? _("Click to hide")
: _("Click to show"),
style: [
"width:20px;height:20px;padding:0;border-radius:3px;border:2px solid;",
"display:flex;align-items:center;justify-content:center;",
"font-size:13px;font-weight:bold;line-height:1;cursor:",
isFixed ? "default;" : "pointer;",
"flex-shrink:0;",
isFixed || isVisible
? "border-color:var(--primary-color,#2196f3);background:var(--primary-color,#2196f3);color:#fff;"
: "border-color:var(--border-color,#aaa);background:transparent;color:transparent;",
].join(""),
});
toggleBtn.textContent = "✓";
if (isFixed) toggleBtn.disabled = true;
if (!isFixed) {
toggleBtn.addEventListener("click", function (ev) {
ev.preventDefault();
visibility[c.id] = !visibility[c.id];
syncToUci();
renderList();
});
}
const textWrap = E(
"div",
{ style: "flex:1;min-width:0;line-height:1.3;" },
[
E(
"div",
{
style:
"font-weight:600;font-size:0.875rem;color:var(--text-color,#212529);",
},
c.label,
),
E(
"div",
{
style:
"font-size:0.75rem;color:var(--text-color-medium,#6c757d);",
},
c.desc,
),
],
);
row.appendChild(toggleBtn);
row.appendChild(textWrap);
listEl.appendChild(row);
});
}
renderList();
syncToUci();
wrapper.appendChild(listEl);
return wrapper;
}
function createSettingsContent(section, capabilities) { function createSettingsContent(section, capabilities) {
section.tab("navigation", _("Menu & Navigation")); section.tab("navigation", _("Menu & Navigation"));
section.tab("dns", _("DNS Settings")); section.tab("dns", _("DNS Settings"));
@@ -1342,6 +1536,25 @@ function createSettingsContent(section, capabilities) {
}; };
tabsOrderOpt.remove = function () {}; tabsOrderOpt.remove = function () {};
const compVisOpt = section.taboption(
"navigation",
form.Value,
"_components_visibility_widget",
_("Component Cards Visibility"),
_(
"Customize which component cards are displayed in the Components tab. Checkboxes show or hide components.",
),
);
compVisOpt.renderWidget = function (section_id) {
return createComponentsVisibilityWidget(this, section_id);
};
compVisOpt.formvalue = function (section_id) {
const el = document.getElementById("components-visibility-widget-" + section_id);
return el ? JSON.stringify(el._vis) : "";
};
compVisOpt.write = function (_section_id) {};
compVisOpt.remove = function () {};
// ── DNS Settings ──────────────────────────────────────────────────────── // ── DNS Settings ────────────────────────────────────────────────────────
const dnsBenchmarkBtn = section.taboption( const dnsBenchmarkBtn = section.taboption(
@@ -78,11 +78,11 @@ function configureGridSection(sectionRef, type, title, addTitle) {
sectionRef.rowcolors = true; sectionRef.rowcolors = true;
sectionRef.nodescriptions = true; sectionRef.nodescriptions = true;
sectionRef.modaltitle = function (section_id) { sectionRef.modaltitle = function (section_id) {
const label = uci.get(UCI_PACKAGE, section_id, "label"); const label = uci.get(UCI_PACKAGE, section_id, "label") || uci.get(UCI_PACKAGE, section_id, "name");
return section_id ? `${title}: ${label || section_id}` : addTitle; return section_id ? `${title}: ${label || section_id}` : addTitle;
}; };
sectionRef.sectiontitle = function (section_id) { sectionRef.sectiontitle = function (section_id) {
return uci.get(UCI_PACKAGE, section_id, "label") || section_id; return uci.get(UCI_PACKAGE, section_id, "label") || uci.get(UCI_PACKAGE, section_id, "name") || section_id;
}; };
sectionRef.renderSectionAdd = function (extra_class) { sectionRef.renderSectionAdd = function (extra_class) {
return renderSectionAdd(sectionRef, extra_class); return renderSectionAdd(sectionRef, extra_class);
+41 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-16 15:18+0300\n" "POT-Creation-Date: 2026-09-16 16:15+0300\n"
"PO-Revision-Date: 2026-09-16 15:18+0300\n" "PO-Revision-Date: 2026-09-16 16:15+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -311,6 +311,9 @@ msgstr "Chat ID администраторов"
msgid "Adult content" msgid "Adult content"
msgstr "Взрослый контент" msgstr "Взрослый контент"
msgid "Advanced DPI bypass engine with Lua scripts (nfqws2)"
msgstr ""
msgid "Advanced System" msgid "Advanced System"
msgstr "Расширенные настройки" msgstr "Расширенные настройки"
@@ -1079,6 +1082,9 @@ msgstr "Community-списки"
msgid "Component action timed out" msgid "Component action timed out"
msgstr "Превышено время ожидания действия компонента" msgstr "Превышено время ожидания действия компонента"
msgid "Component Cards Visibility"
msgstr ""
msgid "Component update available" msgid "Component update available"
msgstr "Доступно обновление компонента" msgstr "Доступно обновление компонента"
@@ -1184,6 +1190,9 @@ msgstr "Скопировать отчет для поддержки"
msgid "Core configuration and preferences" msgid "Core configuration and preferences"
msgstr "Основные настройки и параметры" msgstr "Основные настройки и параметры"
msgid "Core orchestration service and LuCI application"
msgstr ""
msgid "Could not compare FakeIP and control public IPs" msgid "Could not compare FakeIP and control public IPs"
msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки" msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки"
@@ -1253,6 +1262,9 @@ msgstr "Пользовательская стратегия добавлена!"
msgid "Customize the order and visibility of tabs in the main navigation menu. Use the arrows to reorder and checkboxes to show or hide tabs." msgid "Customize the order and visibility of tabs in the main navigation menu. Use the arrows to reorder and checkboxes to show or hide tabs."
msgstr "Настройте порядок и видимость вкладок в главном меню навигации. Используйте стрелки для перемещения и галочки для показа или скрытия." msgstr "Настройте порядок и видимость вкладок в главном меню навигации. Используйте стрелки для перемещения и галочки для показа или скрытия."
msgid "Customize which component cards are displayed in the Components tab. Checkboxes show or hide components."
msgstr ""
msgid "Daily Quota" msgid "Daily Quota"
msgstr "Дневная квота" msgstr "Дневная квота"
@@ -1286,6 +1298,9 @@ msgstr "Дни недели"
msgid "Dedicated cgroup unavailable" msgid "Dedicated cgroup unavailable"
msgstr "Выделенная cgroup недоступна" msgstr "Выделенная cgroup недоступна"
msgid "Dedicated direct HTTP/SOCKS5 outbound proxy"
msgstr ""
msgid "Default" msgid "Default"
msgstr "По умолчанию" msgstr "По умолчанию"
@@ -1370,6 +1385,9 @@ msgstr "Прямое (по умолчанию)"
msgid "Direct Bypass" msgid "Direct Bypass"
msgstr "Прямой WAN" msgstr "Прямой WAN"
msgid "Direct cgroup bypass for TorrServer"
msgstr ""
msgid "Direct connection" msgid "Direct connection"
msgstr "Прямое подключение" msgstr "Прямое подключение"
@@ -1622,6 +1640,9 @@ msgstr "Загрузка и применение наборов правил и
msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer." msgid "DPI bandwidth throttling or delay detected — connection throttled during data transfer."
msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных." msgstr "Обнаружено замедление DPI — соединение заблокировано или задушено при передаче данных."
msgid "DPI bypass engine (nfqws v1)"
msgstr ""
msgid "DPI Detected" msgid "DPI Detected"
msgstr "DPI обнаружен" msgstr "DPI обнаружен"
@@ -2126,6 +2147,9 @@ msgstr "Семейные профили"
msgid "Family profiles and device grouping" msgid "Family profiles and device grouping"
msgstr "Семейные профили и группировка устройств" msgstr "Семейные профили и группировка устройств"
msgid "Fast packet transport network client"
msgstr ""
msgid "Faster DNS health monitoring than the default cycle. Switches bootstrap DNS on failure." msgid "Faster DNS health monitoring than the default cycle. Switches bootstrap DNS on failure."
msgstr "Более быстрый мониторинг состояния DNS, чем стандартный цикл. Переключает bootstrap DNS при сбое." msgstr "Более быстрый мониторинг состояния DNS, чем стандартный цикл. Переключает bootstrap DNS при сбое."
@@ -3071,6 +3095,9 @@ msgstr "Загрузка..."
msgid "Local Address" msgid "Local Address"
msgstr "Локальный адрес" msgstr "Локальный адрес"
msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr ""
msgid "Location" msgid "Location"
msgstr "Местоположение" msgstr "Местоположение"
@@ -3959,6 +3986,9 @@ msgstr "Ошибка чтения"
msgid "Ready to benchmark DNS providers" msgid "Ready to benchmark DNS providers"
msgstr "Готов к тестированию DNS провайдеров" msgstr "Готов к тестированию DNS провайдеров"
msgid "Real-time WebRTC anti-censorship tunnel"
msgstr ""
msgid "Reality fake site" msgid "Reality fake site"
msgstr "Фейковый сайт Reality" msgstr "Фейковый сайт Reality"
@@ -5489,6 +5519,9 @@ msgstr "Великобритания (gb)"
msgid "United States (us)" msgid "United States (us)"
msgstr "США (us)" msgstr "США (us)"
msgid "Universal proxy platform routing engine"
msgstr ""
msgid "unknown" msgid "unknown"
msgstr "неизвестно" msgstr "неизвестно"
@@ -5834,6 +5867,9 @@ msgstr "Будни (Пн-Пт)"
msgid "Weekends (Sat-Sun)" msgid "Weekends (Sat-Sun)"
msgstr "Выходные (Сб-Вс)" msgstr "Выходные (Сб-Вс)"
msgid "Western Digital Tunnel Transport"
msgstr ""
msgid "What Tachyon should do when this section matches" msgid "What Tachyon should do when this section matches"
msgstr "Что должен делать Tachyon при совпадении этой секции" msgstr "Что должен делать Tachyon при совпадении этой секции"
@@ -5942,6 +5978,9 @@ msgstr "Правила маршрутизации sing-box для Zapret2 нас
msgid "Zapret2 sing-box route rules are not configured" msgid "Zapret2 sing-box route rules are not configured"
msgstr "Правила маршрутизации sing-box для Zapret2 не настроены" msgstr "Правила маршрутизации sing-box для Zapret2 не настроены"
msgid "Zero config mesh VPN daemon"
msgstr ""
msgid "Testing strategy" msgid "Testing strategy"
msgstr "Тестирование стратегии" msgstr "Тестирование стратегии"
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -135,7 +135,11 @@ function get_zapret2_lua_flags(args_str) {
LIB_DIR + "/providers/zapret2/lua", LIB_DIR + "/providers/zapret2/lua",
"/usr/lib/tachyon/providers/zapret2/lua", "/usr/lib/tachyon/providers/zapret2/lua",
"/opt/zapret2/lua", "/opt/zapret2/lua",
"/opt/zapret2/files/lua",
"/opt/zapret2/share/zapret/lua",
"/opt/zapret2/init.d/sysv/lua",
"/opt/zapret/lua", "/opt/zapret/lua",
"/opt/zapret/files/lua",
"/usr/share/zapret2/lua", "/usr/share/zapret2/lua",
"/usr/share/zapret/lua", "/usr/share/zapret/lua",
"/etc/zapret2/lua", "/etc/zapret2/lua",
@@ -2645,6 +2649,11 @@ function run_probe(engine, args_str, target_key, custom_url, job_id) {
if (is_z2) { if (is_z2) {
lua_init_flags = get_zapret2_lua_flags(args_str); lua_init_flags = get_zapret2_lua_flags(args_str);
blob_flags = resolve_zapret2_blobs(args_str); blob_flags = resolve_zapret2_blobs(args_str);
if (index(args_str, "--lua-desync") >= 0 && lua_init_flags == "" && index(args_str, "--lua-init") < 0) {
result.error = "Missing Zapret2 Lua library: zapret-antidpi.lua not found in /opt/zapret2/lua or system paths";
cleanup_temp_daemons(job_id);
return result;
}
} }
let filter_prefix = ""; let filter_prefix = "";
@@ -2664,9 +2673,9 @@ function run_probe(engine, args_str, target_key, custom_url, job_id) {
fwmark_flag = sprintf("--dpi-desync-fwmark=%s ", FUZZER_FWMARK); fwmark_flag = sprintf("--dpi-desync-fwmark=%s ", FUZZER_FWMARK);
} }
let argv = fuzzer_runner.build_zapret_argv(bin, qnum, fwmark_flag, lua_init_flags, blob_flags, filter_prefix, tok_res.tokens, pid_path); let argv = fuzzer_runner.build_zapret_argv(bin, qnum, fwmark_flag, lua_init_flags, blob_flags, filter_prefix, tok_res.tokens);
let spawn_cmd = "cd /tmp && " + common.command_from_args(argv) + " >" + shell_quote(stderr_log) + " 2>&1"; let spawn_cmd = "cd /tmp && " + common.command_from_args(argv) + " 2>" + shell_quote(stderr_log);
system(common.background_command(spawn_cmd)); system(common.background_command_with_pid(spawn_cmd, ">/dev/null", ">" + shell_quote(pid_path)));
let pid_running = false; let pid_running = false;
for (let wait_i = 0; wait_i < 15; wait_i++) { for (let wait_i = 0; wait_i < 15; wait_i++) {
@@ -97,7 +97,7 @@ function build_byedpi_argv(bin, port, tokens) {
return argv; return argv;
} }
function build_zapret_argv(bin, qnum, fwmark_flag, lua_init_flags, blob_flags, filter_prefix, tokens, pid_path) { function build_zapret_argv(bin, qnum, fwmark_flag, lua_init_flags, blob_flags, filter_prefix, tokens) {
let argv = [ bin, sprintf("--qnum=%d", qnum) ]; let argv = [ bin, sprintf("--qnum=%d", qnum) ];
let append_flags = function(flag_str) { let append_flags = function(flag_str) {
@@ -113,13 +113,12 @@ function build_zapret_argv(bin, qnum, fwmark_flag, lua_init_flags, blob_flags, f
append_flags(blob_flags); append_flags(blob_flags);
append_flags(filter_prefix); append_flags(filter_prefix);
for (let tok in tokens) for (let tok in tokens) {
if (tok == "--daemon" || index(tok, "--pidfile") == 0)
continue;
push(argv, tok); push(argv, tok);
}
if (pid_path && pid_path != "")
push(argv, sprintf("--pidfile=%s", pid_path));
push(argv, "--daemon");
return argv; return argv;
} }
@@ -1140,6 +1140,47 @@ function openwrt_release() {
return "unknown"; return "unknown";
} }
function get_system_arch_candidates() {
let arch_list = "";
if (fs.stat("/etc/apk/arch") != null) {
arch_list += " " + trim(as_string(fs.readfile("/etc/apk/arch")));
}
if (fs.stat("/etc/openwrt_release") != null) {
let content = as_string(fs.readfile("/etc/openwrt_release"));
for (let line in split(content, "\n")) {
if (substr(line, 0, length("DISTRIB_ARCH=")) == "DISTRIB_ARCH=") {
let v = substr(line, length("DISTRIB_ARCH="));
if (length(v) >= 2) {
let q = substr(v, 0, 1);
if ((q == "\"" || q == "'") && substr(v, length(v) - 1) == q)
v = substr(v, 1, length(v) - 2);
}
arch_list += " " + v;
break;
}
}
}
if (command_exists("uname")) {
arch_list += " " + trim(command_output_from_args([ "uname", "-m" ]));
}
return arch_list;
}
function is_fptn_supported() {
let arch_list = get_system_arch_candidates();
let supported = [
"x86_64", "amd64",
"aarch64", "arm64",
"arm_cortex-a7_neon-vfpv4",
"arm_cortex-a7"
];
for (let s in supported) {
if (index(arch_list, s) >= 0)
return true;
}
return false;
}
function build_system_info() { function build_system_info() {
let tachyon_latest_version = first_line_value("/tmp/tachyon.latest-version.cache", "unknown"); let tachyon_latest_version = first_line_value("/tmp/tachyon.latest-version.cache", "unknown");
let luci_app_version = get_luci_app_version(); let luci_app_version = get_luci_app_version();
@@ -1177,6 +1218,7 @@ function build_system_info() {
let tailscale_installed = provider_installed(TAILSCALE_RUNTIME_UC) ? 1 : 0; let tailscale_installed = provider_installed(TAILSCALE_RUNTIME_UC) ? 1 : 0;
let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed"; let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed";
let fptn_installed = provider_installed(FPTN_RUNTIME_UC) ? 1 : 0; let fptn_installed = provider_installed(FPTN_RUNTIME_UC) ? 1 : 0;
let fptn_supported = (fptn_installed == 1 || is_fptn_supported()) ? 1 : 0;
let fptn_version = fptn_installed ? provider_version(FPTN_RUNTIME_UC) : "not installed"; let fptn_version = fptn_installed ? provider_version(FPTN_RUNTIME_UC) : "not installed";
let device_model = first_line_value("/tmp/sysinfo/model", "unknown"); let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
@@ -1189,6 +1231,16 @@ function build_system_info() {
if (type(torrserver_direct_status) != "object") if (type(torrserver_direct_status) != "object")
torrserver_direct_status = {}; torrserver_direct_status = {};
let show_component_zapret = bool_option(settings(), "show_component_zapret", true) ? 1 : 0;
let show_component_zapret2 = bool_option(settings(), "show_component_zapret2", true) ? 1 : 0;
let show_component_byedpi = bool_option(settings(), "show_component_byedpi", true) ? 1 : 0;
let show_component_wdtt = bool_option(settings(), "show_component_wdtt", true) ? 1 : 0;
let show_component_olcrtc = bool_option(settings(), "show_component_olcrtc", true) ? 1 : 0;
let show_component_fptn = bool_option(settings(), "show_component_fptn", true) ? 1 : 0;
let show_component_tailscale = bool_option(settings(), "show_component_tailscale", true) ? 1 : 0;
let show_component_direct_bypass = bool_option(settings(), "show_component_direct_bypass", true) ? 1 : 0;
let show_component_torrserver_direct = bool_option(settings(), "show_component_torrserver_direct", true) ? 1 : 0;
let base_bdir = getenv("TACHYON_COMPONENT_BACKUPS_DIR") || "/etc/tachyon/component-backups"; let base_bdir = getenv("TACHYON_COMPONENT_BACKUPS_DIR") || "/etc/tachyon/component-backups";
let read_backup_meta = function(comp) { let read_backup_meta = function(comp) {
let path = base_bdir + "/" + comp + "/metadata.json"; let path = base_bdir + "/" + comp + "/metadata.json";
@@ -1241,6 +1293,7 @@ function build_system_info() {
tailscale_backup_time: tailscale_meta ? int(tailscale_meta.timestamp || 0) : 0, tailscale_backup_time: tailscale_meta ? int(tailscale_meta.timestamp || 0) : 0,
fptn_version, fptn_version,
fptn_installed, fptn_installed,
fptn_supported,
fptn_backup_version: fptn_meta ? as_string(fptn_meta.version) : "", fptn_backup_version: fptn_meta ? as_string(fptn_meta.version) : "",
fptn_backup_time: fptn_meta ? int(fptn_meta.timestamp || 0) : 0, fptn_backup_time: fptn_meta ? int(fptn_meta.timestamp || 0) : 0,
direct_bypass_enabled, direct_bypass_enabled,
@@ -1250,6 +1303,15 @@ function build_system_info() {
torrserver_direct_available: int(torrserver_direct_status.available || 0), torrserver_direct_available: int(torrserver_direct_status.available || 0),
torrserver_direct_enabled: int(torrserver_direct_status.enabled || 0), torrserver_direct_enabled: int(torrserver_direct_status.enabled || 0),
torrserver_direct_active: int(torrserver_direct_status.active || 0), torrserver_direct_active: int(torrserver_direct_status.active || 0),
show_component_zapret,
show_component_zapret2,
show_component_byedpi,
show_component_wdtt,
show_component_olcrtc,
show_component_fptn,
show_component_tailscale,
show_component_direct_bypass,
show_component_torrserver_direct,
openwrt_version: openwrt_release(), openwrt_version: openwrt_release(),
device_model, device_model,
generated_at: int(clock()[0]) generated_at: int(clock()[0])
+3 -3
View File
@@ -1209,7 +1209,7 @@ function view_sections(token, chat_id, msg_id) {
let keyboard = []; let keyboard = [];
for (let s in sections) { for (let s in sections) {
let label = s.label || s[".name"]; let label = s.label || s.name || s[".name"];
let status = (s.enabled == "1") ? "✅" : "❌"; let status = (s.enabled == "1") ? "✅" : "❌";
push(keyboard, [{ text: status + " " + label, callback_data: "/sec_view " + s[".name"] }]); push(keyboard, [{ text: status + " " + label, callback_data: "/sec_view " + s[".name"] }]);
} }
@@ -1227,7 +1227,7 @@ function view_section_editor(token, chat_id, msg_id, sec_name) {
if (!s) return view_sections(token, chat_id, msg_id); if (!s) return view_sections(token, chat_id, msg_id);
let status = (s.enabled == "1") ? t("status_enabled") : t("status_disabled"); let status = (s.enabled == "1") ? t("status_enabled") : t("status_disabled");
let text = "⚙️ <b>" + t("section_section") + ":</b> " + escape_html(s.label || sec_name) + "\n" + let text = "⚙️ <b>" + t("section_section") + ":</b> " + escape_html(s.label || s.name || sec_name) + "\n" +
t("section_type") + ": <code>" + escape_html(s.action || "none") + "</code>\n" + t("section_type") + ": <code>" + escape_html(s.action || "none") + "</code>\n" +
t("section_status") + ": <b>" + status + "</b>\n\n"; t("section_status") + ": <b>" + status + "</b>\n\n";
@@ -1340,7 +1340,7 @@ function view_sec_list(token, chat_id, msg_id, sec_name, list_type) {
for (let x in rs) push(items, {type: "community_lists", val: x}); for (let x in rs) push(items, {type: "community_lists", val: x});
} }
let text = t("section_list_header") + " " + escape_html(s.label || sec_name) + "\n" + let text = t("section_list_header") + " " + escape_html(s.label || s.name || sec_name) + "\n" +
"📋 <b>" + title + "</b>:\n\n"; "📋 <b>" + title + "</b>:\n\n";
let keyboard = []; let keyboard = [];
@@ -1471,16 +1471,47 @@ function outbound_supports_udp(config, tag_name, visited) {
return false; return false;
} }
function push_section_route_rule(config, rule, target_outbound) { function apply_excluded_source_ips(rule, excluded_cidrs) {
if (!rule || !excluded_cidrs || length(excluded_cidrs) == 0)
return rule;
let action_keys = {
action: true,
outbound: true,
server: true,
rewrite_ttl: true
};
let match_part = {};
let logical_rule = {
type: "logical",
mode: "and",
rules: [
match_part,
{
invert: true,
source_ip_cidr: single_or_array(excluded_cidrs)
}
]
};
for (let k, v in rule) {
if (action_keys[k])
logical_rule[k] = v;
else
match_part[k] = v;
}
return logical_rule;
}
function push_section_route_rule(config, rule, target_outbound, excluded_cidrs) {
if (target_outbound && !outbound_supports_udp(config, target_outbound)) { if (target_outbound && !outbound_supports_udp(config, target_outbound)) {
if (rule.network == "tcp") { if (rule.network == "tcp") {
push(config.route.rules, rule); push(config.route.rules, apply_excluded_source_ips(rule, excluded_cidrs));
return; return;
} }
if (rule.network == "udp") { if (rule.network == "udp") {
delete rule.outbound; delete rule.outbound;
rule.action = "reject"; rule.action = "reject";
push(config.route.rules, rule); push(config.route.rules, apply_excluded_source_ips(rule, excluded_cidrs));
return; return;
} }
let udp_rule = {}; let udp_rule = {};
@@ -1489,11 +1520,11 @@ function push_section_route_rule(config, rule, target_outbound) {
delete udp_rule.outbound; delete udp_rule.outbound;
udp_rule.action = "reject"; udp_rule.action = "reject";
udp_rule.network = "udp"; udp_rule.network = "udp";
push(config.route.rules, udp_rule); push(config.route.rules, apply_excluded_source_ips(udp_rule, excluded_cidrs));
rule.network = "tcp"; rule.network = "tcp";
} }
push(config.route.rules, rule); push(config.route.rules, apply_excluded_source_ips(rule, excluded_cidrs));
} }
function add_fully_routed_ips_rule(config, section) { function add_fully_routed_ips_rule(config, section) {
@@ -1604,11 +1635,11 @@ function add_combined_route_for_section(config, section) {
let domain_regex = domains.domain_regex; let domain_regex = domains.domain_regex;
let ip_cidr = legacy_condition_values(section, "ip_cidr"); let ip_cidr = legacy_condition_values(section, "ip_cidr");
let source_ip_cidr = core_ip.normalize_to_cidrs(legacy_condition_values(section, "source_ip_cidr")); let source_ip_cidr = core_ip.normalize_to_cidrs(legacy_condition_values(section, "source_ip_cidr"));
let excluded_cidrs = core_ip.normalize_to_cidrs(list_option(section, "excluded_ips"));
let rule_set_tags = []; let rule_set_tags = [];
let dns_rule_set_tags = []; let dns_rule_set_tags = [];
let section_name = section[".name"]; let section_name = section[".name"];
add_excluded_ips_rule(config, section);
add_excluded_protocol_rule(config, section); add_excluded_protocol_rule(config, section);
add_fully_routed_ips_rule(config, section); add_fully_routed_ips_rule(config, section);
@@ -1689,9 +1720,9 @@ function add_combined_route_for_section(config, section) {
if (type(resolve) == "object" && resolve.warning) if (type(resolve) == "object" && resolve.warning)
warn(resolve.warning, "\n"); warn(resolve.warning, "\n");
else if (type(resolve) == "object" && resolve.rule) else if (type(resolve) == "object" && resolve.rule)
push(config.route.rules, resolve.rule); push(config.route.rules, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_section_route_rule(config, domain_rule, target.outbound); push_section_route_rule(config, domain_rule, target.outbound, excluded_cidrs);
} }
if (has_ruleset) { if (has_ruleset) {
@@ -1702,15 +1733,15 @@ function add_combined_route_for_section(config, section) {
if (type(resolve) == "object" && resolve.warning) if (type(resolve) == "object" && resolve.warning)
warn(resolve.warning, "\n"); warn(resolve.warning, "\n");
else if (type(resolve) == "object" && resolve.rule) else if (type(resolve) == "object" && resolve.rule)
push(config.route.rules, resolve.rule); push(config.route.rules, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_section_route_rule(config, rs_rule, target.outbound); push_section_route_rule(config, rs_rule, target.outbound, excluded_cidrs);
} }
if (has_ip_cidr) { if (has_ip_cidr) {
let ip_rule = create_section_route_rule(); let ip_rule = create_section_route_rule();
ip_rule.ip_cidr = ip_cidr; ip_rule.ip_cidr = ip_cidr;
push_section_route_rule(config, ip_rule, target.outbound); push_section_route_rule(config, ip_rule, target.outbound, excluded_cidrs);
} }
if (length(discord_cf_subnets) > 0) { if (length(discord_cf_subnets) > 0) {
@@ -1718,7 +1749,7 @@ function add_combined_route_for_section(config, section) {
voice_rule.network = "udp"; voice_rule.network = "udp";
voice_rule.ip_cidr = discord_cf_subnets; voice_rule.ip_cidr = discord_cf_subnets;
voice_rule.port_range = core_ip.DISCORD_VOICE_PORT_RANGES || [ "5000:5020", "3478:3478", "19294:19344", "50000:65535" ]; voice_rule.port_range = core_ip.DISCORD_VOICE_PORT_RANGES || [ "5000:5020", "3478:3478", "19294:19344", "50000:65535" ];
push_section_route_rule(config, voice_rule, target.outbound); push_section_route_rule(config, voice_rule, target.outbound, excluded_cidrs);
} }
let country_list = connections.geoip_country_list(section); let country_list = connections.geoip_country_list(section);
@@ -1744,7 +1775,7 @@ function add_combined_route_for_section(config, section) {
if (country_mode == "exclude") if (country_mode == "exclude")
geoip_route_rule.invert = true; geoip_route_rule.invert = true;
push_section_route_rule(config, geoip_route_rule, target.outbound); push_section_route_rule(config, geoip_route_rule, target.outbound, excluded_cidrs);
} }
} }
@@ -1754,7 +1785,7 @@ function add_combined_route_for_section(config, section) {
fallback_rule.port != null || fallback_rule.port_range != null || fallback_rule.port != null || fallback_rule.port_range != null ||
fallback_rule.protocol != null || fallback_rule.dscp != null; fallback_rule.protocol != null || fallback_rule.dscp != null;
if (has_any_matcher) if (has_any_matcher)
push_section_route_rule(config, fallback_rule, target.outbound); push_section_route_rule(config, fallback_rule, target.outbound, excluded_cidrs);
} }
let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60"); let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60");
@@ -1769,7 +1800,7 @@ function add_combined_route_for_section(config, section) {
add_domain_array(dns_rule, "domain_keyword", domain_keyword); add_domain_array(dns_rule, "domain_keyword", domain_keyword);
add_domain_array(dns_rule, "domain_regex", domain_regex); add_domain_array(dns_rule, "domain_regex", domain_regex);
add_source_dns_matchers(dns_rule, source_ip_cidr); add_source_dns_matchers(dns_rule, source_ip_cidr);
push_dns_matcher_rule(config, dns_rule); push_dns_matcher_rule(config, apply_excluded_source_ips(dns_rule, excluded_cidrs));
} }
if (length(dns_rule_set_tags) > 0) { if (length(dns_rule_set_tags) > 0) {
let dns_rule = { let dns_rule = {
@@ -1779,7 +1810,7 @@ function add_combined_route_for_section(config, section) {
rule_set: single_or_array(dns_rule_set_tags) rule_set: single_or_array(dns_rule_set_tags)
}; };
add_source_dns_matchers(dns_rule, source_ip_cidr); add_source_dns_matchers(dns_rule, source_ip_cidr);
push_dns_matcher_rule(config, dns_rule); push_dns_matcher_rule(config, apply_excluded_source_ips(dns_rule, excluded_cidrs));
} }
} }
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
cat >"$WORK_DIR/fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1",
"dns_type": "udp",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "sec_yt",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_yt-out\"}" ],
"domain_suffix": [ "youtube.com" ],
"excluded_ips": [ "192.168.1.50" ]
},
{
".name": "sec_gg",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_gg-out\"}" ],
"domain_suffix": [ "google.com" ]
}
]
}
JSON
output="$WORK_DIR/out.json"
mkdir -p "$output.section-cache" "$output.rulesets"
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture.json" "$output" "127.0.0.1" "0" "1"
# Verify route rules in out.json
ucode -e '
let fs = require("fs");
let raw = fs.readfile("'"$output"'");
let cfg = json(raw);
let rules = cfg.route.rules || [];
let found_global_direct = false;
let found_scoped_logical = false;
let found_sec_gg = false;
for (let r in rules) {
// A global direct rule would have outbound direct-out, source_ip_cidr set, and no domain conditions
if (r.outbound == "direct-out" && r.source_ip_cidr && !r.domain_suffix && !r.domain && !r.rule_set) {
found_global_direct = true;
}
// Scoped logical rule for sec_yt-out
if (r.type == "logical" && r.outbound == "sec_yt-out" && r.mode == "and" && type(r.rules) == "array") {
let has_domain = false;
let has_inverted_src = false;
for (let sub in r.rules) {
if (sub.domain_suffix && index(sub.domain_suffix, "youtube.com") >= 0)
has_domain = true;
if (sub.invert == true && (sub.source_ip_cidr == "192.168.1.50/32" || index(sub.source_ip_cidr, "192.168.1.50/32") >= 0))
has_inverted_src = true;
}
if (has_domain && has_inverted_src)
found_scoped_logical = true;
}
if (r.outbound == "sec_gg-out") {
found_sec_gg = true;
}
}
if (found_global_direct)
die("Found unconditional global direct-out rule for excluded IP, breaking subsequent sections!\n");
if (!found_scoped_logical)
die("Expected scoped logical rule for sec_yt-out with inverted source_ip_cidr not found!\n");
if (!found_sec_gg)
die("sec_gg-out rule not found in generated sing-box rules!\n");
' || fail "section_excluded_ips failed verification"
printf 'PASS: section_excluded_ips\n'