release: 1.3.8 - OpenWrt network interfaces for WARP generator, DoH doctor URL normalization & doctor lock fixes
This commit is contained in:
+701
-679
File diff suppressed because it is too large
Load Diff
+685
-672
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-29 21:28+0300\n"
|
||||
"PO-Revision-Date: 2026-08-29 21:28+0300\n"
|
||||
"POT-Creation-Date: 2026-08-30 19:56+0300\n"
|
||||
"PO-Revision-Date: 2026-08-30 19:56+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -2135,6 +2135,9 @@ msgstr "Установленная сборка → Доступная сбор
|
||||
msgid "Installed version is newer than release"
|
||||
msgstr "Установлена версия новее релиза"
|
||||
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
|
||||
msgid "Interface Monitoring"
|
||||
msgstr "Мониторинг интерфейсов"
|
||||
|
||||
@@ -2951,6 +2954,9 @@ msgstr "Откройте настройки приоритета и введит
|
||||
msgid "Open Project Page"
|
||||
msgstr "Открыть страницу проекта"
|
||||
|
||||
msgid "OpenWrt Network Interfaces"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation completed successfully!"
|
||||
msgstr "Операция успешно завершена!"
|
||||
|
||||
@@ -3641,8 +3647,8 @@ msgstr "Действие секции"
|
||||
msgid "Section name"
|
||||
msgstr "Имя секции"
|
||||
|
||||
msgid "Section or tunnel interface used for Cloudflare WARP API registration. Supports Connection, WireGuard, AmneziaWG, and Interface sections. Leave empty to use direct WAN / default mixed proxy."
|
||||
msgstr "Секция или туннельный интерфейс для регистрации Cloudflare WARP API. Поддерживает секции Connection, WireGuard, AmneziaWG и Interface. Оставьте пустым для прямого WAN / стандартного mixed proxy."
|
||||
msgid "Section or network interface used for Cloudflare WARP API registration. Supports Tachyon sections (Connection, WireGuard, AmneziaWG, Interface) and OpenWrt system network devices/interfaces (awg0, wg0, tun0, etc.). Leave empty to use direct WAN / default mixed proxy."
|
||||
msgstr ""
|
||||
|
||||
msgid "Section Rules Content"
|
||||
msgstr "Содержимое правил секции"
|
||||
@@ -4205,6 +4211,9 @@ msgstr "Tachyon управляет жизненным циклом процес
|
||||
msgid "Tachyon manages the nfqws2 process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon управляет жизненным циклом процесса nfqws2 самостоятельно, поэтому режим daemon здесь не допускается."
|
||||
|
||||
msgid "Tachyon Sections"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tachyon service is stopped. Start the service to display connections."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения подключений."
|
||||
|
||||
@@ -4949,6 +4958,9 @@ msgstr "sing-box outbound для Zapret2 настроен"
|
||||
msgid "Zapret2 sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret2 не настроен"
|
||||
|
||||
msgid "Section or tunnel interface used for Cloudflare WARP API registration. Supports Connection, WireGuard, AmneziaWG, and Interface sections. Leave empty to use direct WAN / default mixed proxy."
|
||||
msgstr "Секция или туннельный интерфейс для регистрации Cloudflare WARP API. Поддерживает секции Connection, WireGuard, AmneziaWG и Interface. Оставьте пустым для прямого WAN / стандартного mixed proxy."
|
||||
|
||||
msgid "Block internet access or specific proxy sections for individual devices based on time and days."
|
||||
msgstr "Блокировка доступа в интернет или определенных секций маршрутизации для отдельных устройств по времени и дням недели."
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.7",
|
||||
"version": "1.3.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.7",
|
||||
"version": "1.3.8",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/parser": "7.28.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.7",
|
||||
"version": "1.3.8",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
declare const rpc:
|
||||
| {
|
||||
declare: (spec: unknown) => () => Promise<Record<string, unknown>>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
declare const network:
|
||||
| {
|
||||
getDevices: () => Promise<
|
||||
Array<{
|
||||
getName?: () => string;
|
||||
getType?: () => string;
|
||||
getTypeI18n?: () => string;
|
||||
name?: string;
|
||||
device?: string;
|
||||
type?: string;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export interface AdvNetworkDevice {
|
||||
id: string;
|
||||
label: string;
|
||||
proto?: string;
|
||||
device?: string;
|
||||
}
|
||||
|
||||
interface InterfaceDumpResponse {
|
||||
interface?: Array<{
|
||||
interface?: string;
|
||||
device?: string;
|
||||
l3_device?: string;
|
||||
proto?: string;
|
||||
up?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
const callNetworkInterfaceDump =
|
||||
typeof rpc !== 'undefined' && typeof rpc.declare === 'function'
|
||||
? rpc.declare({
|
||||
object: 'network.interface',
|
||||
method: 'dump',
|
||||
expect: { '': {} },
|
||||
})
|
||||
: null;
|
||||
|
||||
export async function fetchNetworkInterfaces(): Promise<AdvNetworkDevice[]> {
|
||||
const result: AdvNetworkDevice[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// 1. Try fetching logical and physical interfaces from ubus network.interface dump
|
||||
if (callNetworkInterfaceDump) {
|
||||
try {
|
||||
const dump = (await callNetworkInterfaceDump()) as InterfaceDumpResponse;
|
||||
if (dump && Array.isArray(dump.interface)) {
|
||||
for (const iface of dump.interface) {
|
||||
const ifname = iface.interface;
|
||||
const dev = iface.l3_device || iface.device;
|
||||
const proto = iface.proto || '';
|
||||
|
||||
if (dev && dev !== 'lo' && !seen.has(dev)) {
|
||||
seen.add(dev);
|
||||
const protoDesc = proto ? ` (${proto})` : '';
|
||||
result.push({
|
||||
id: dev,
|
||||
label: `${dev}${protoDesc}`,
|
||||
proto,
|
||||
device: dev,
|
||||
});
|
||||
}
|
||||
|
||||
if (ifname && ifname !== 'loopback' && !seen.has(ifname)) {
|
||||
seen.add(ifname);
|
||||
const ifaceDesc =
|
||||
dev && dev !== ifname
|
||||
? ` (${proto || 'iface'}: ${dev})`
|
||||
: proto
|
||||
? ` (${proto})`
|
||||
: '';
|
||||
result.push({
|
||||
id: ifname,
|
||||
label: `${ifname}${ifaceDesc}`,
|
||||
proto,
|
||||
device: dev,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore RPC error in tests or unsupported environments
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback or augment via LuCI network.getDevices() if available
|
||||
if (
|
||||
typeof network !== 'undefined' &&
|
||||
typeof network.getDevices === 'function'
|
||||
) {
|
||||
try {
|
||||
const devices = await network.getDevices();
|
||||
if (Array.isArray(devices)) {
|
||||
for (const dev of devices) {
|
||||
const name =
|
||||
typeof dev.getName === 'function'
|
||||
? dev.getName()
|
||||
: dev.name || dev.device;
|
||||
if (name && name !== 'lo' && !seen.has(name)) {
|
||||
seen.add(name);
|
||||
const type =
|
||||
typeof dev.getTypeI18n === 'function'
|
||||
? dev.getTypeI18n()
|
||||
: typeof dev.getType === 'function'
|
||||
? dev.getType()
|
||||
: dev.type || '';
|
||||
const typeDesc = type ? ` (${type})` : '';
|
||||
result.push({
|
||||
id: name,
|
||||
label: `${name}${typeDesc}`,
|
||||
device: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fetchNetworkInterfaces } from '../fetchNetworkInterfaces';
|
||||
|
||||
describe('fetchNetworkInterfaces', () => {
|
||||
it('returns empty array when rpc and network are not available', async () => {
|
||||
const res = await fetchNetworkInterfaces();
|
||||
expect(Array.isArray(res)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { TachyonShellMethods } from '../../../methods';
|
||||
import { showToast } from '../../../../helpers/showToast';
|
||||
import { getConfigSections } from '../../../methods/custom/getConfigSections';
|
||||
import {
|
||||
fetchNetworkInterfaces,
|
||||
AdvNetworkDevice,
|
||||
} from '../../../fetchers/fetchNetworkInterfaces';
|
||||
import { TACHYON_UCI_PACKAGE } from '../../../../constants';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
@@ -19,6 +23,7 @@ interface AdvancedSettingsState {
|
||||
smartDetectSections: string[];
|
||||
allSectionNames: string[];
|
||||
sectionOptions: AdvSectionOption[];
|
||||
networkDevices: AdvNetworkDevice[];
|
||||
deviceIpsPerSection: Record<string, string[]>;
|
||||
dnsTurboCache: boolean;
|
||||
agentApiToken: string;
|
||||
@@ -59,6 +64,7 @@ let _state: AdvancedSettingsState = {
|
||||
smartDetectSections: [],
|
||||
allSectionNames: [],
|
||||
sectionOptions: [],
|
||||
networkDevices: [],
|
||||
deviceIpsPerSection: {},
|
||||
dnsTurboCache: false,
|
||||
agentApiToken: '',
|
||||
@@ -106,7 +112,10 @@ function rerender() {
|
||||
// ─── Data loading ─────────────────────────────────────────────────────────────
|
||||
|
||||
export async function loadAdvancedSettingsState() {
|
||||
const sections = await getConfigSections();
|
||||
const [sections, networkDevices] = await Promise.all([
|
||||
getConfigSections(),
|
||||
fetchNetworkInterfaces(),
|
||||
]);
|
||||
|
||||
const settingsSec = sections.find((s) => s['.type'] === 'settings');
|
||||
const smartDetect = settingsSec?.smart_detect === '1';
|
||||
@@ -169,6 +178,7 @@ export async function loadAdvancedSettingsState() {
|
||||
: allSectionNames.slice(0, 1),
|
||||
allSectionNames,
|
||||
sectionOptions,
|
||||
networkDevices,
|
||||
deviceIpsPerSection,
|
||||
dnsTurboCache: settingsSec?.dns_turbo_cache === '1',
|
||||
agentApiToken:
|
||||
@@ -825,21 +835,50 @@ function renderAiWatchdogSection(state: AdvancedSettingsState) {
|
||||
},
|
||||
_('Default (Direct WAN / Default mixed proxy)'),
|
||||
),
|
||||
...state.sectionOptions.map((sec) => {
|
||||
const desc = sec.iface
|
||||
? ` (${sec.action}: ${sec.iface})`
|
||||
: ` (${sec.action})`;
|
||||
return E(
|
||||
'option',
|
||||
{
|
||||
value: sec.id,
|
||||
selected: state.warpProxySection === sec.id,
|
||||
},
|
||||
`${sec.label}${desc}`,
|
||||
);
|
||||
}),
|
||||
...(state.sectionOptions.length > 0
|
||||
? [
|
||||
E(
|
||||
'optgroup',
|
||||
{ label: _('Tachyon Sections') },
|
||||
state.sectionOptions.map((sec) => {
|
||||
const desc = sec.iface
|
||||
? ` (${sec.action}: ${sec.iface})`
|
||||
: ` (${sec.action})`;
|
||||
return E(
|
||||
'option',
|
||||
{
|
||||
value: sec.id,
|
||||
selected: state.warpProxySection === sec.id,
|
||||
},
|
||||
`${sec.label}${desc}`,
|
||||
);
|
||||
}),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(state.networkDevices.length > 0
|
||||
? [
|
||||
E(
|
||||
'optgroup',
|
||||
{ label: _('OpenWrt Network Interfaces') },
|
||||
state.networkDevices.map((dev) =>
|
||||
E(
|
||||
'option',
|
||||
{
|
||||
value: dev.id,
|
||||
selected: state.warpProxySection === dev.id,
|
||||
},
|
||||
dev.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(state.warpProxySection &&
|
||||
!state.sectionOptions.some((s) => s.id === state.warpProxySection)
|
||||
!state.sectionOptions.some(
|
||||
(s) => s.id === state.warpProxySection,
|
||||
) &&
|
||||
!state.networkDevices.some((d) => d.id === state.warpProxySection)
|
||||
? [
|
||||
E(
|
||||
'option',
|
||||
@@ -858,7 +897,7 @@ function renderAiWatchdogSection(state: AdvancedSettingsState) {
|
||||
'div',
|
||||
{ class: 'tachyon_adv__hint' },
|
||||
_(
|
||||
'Section or tunnel interface used for Cloudflare WARP API registration. Supports Connection, WireGuard, AmneziaWG, and Interface sections. Leave empty to use direct WAN / default mixed proxy.',
|
||||
'Section or network interface used for Cloudflare WARP API registration. Supports Tachyon sections (Connection, WireGuard, AmneziaWG, Interface) and OpenWrt system network devices/interfaces (awg0, wg0, tun0, etc.). Leave empty to use direct WAN / default mixed proxy.',
|
||||
),
|
||||
),
|
||||
state.enableAiDoctor
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"require uci";
|
||||
"require baseclass";
|
||||
"require fs";
|
||||
"require network";
|
||||
"require ui";
|
||||
"require tools.widgets as widgets";
|
||||
"require view.tachyon.main as main";
|
||||
@@ -1650,9 +1651,31 @@ function createSettingsContent(section, capabilities) {
|
||||
const action = sec.action || "section";
|
||||
const iface = sec.section_interface || sec.interface || "";
|
||||
const desc = iface ? ` (${action}: ${iface})` : ` (${action})`;
|
||||
this.value(secName, `${sec.label || secName}${desc}`);
|
||||
this.value(secName, `${_("Section")}: ${sec.label || secName}${desc}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof network !== "undefined" && typeof network.getDevices === "function") {
|
||||
return network.getDevices().then(
|
||||
L.bind(function (devices) {
|
||||
(devices || []).forEach((dev) => {
|
||||
const name = typeof dev.getName === "function" ? dev.getName() : dev.name;
|
||||
if (name && name !== "lo" && !this.keylist.includes(name)) {
|
||||
const type =
|
||||
typeof dev.getTypeI18n === "function"
|
||||
? dev.getTypeI18n()
|
||||
: typeof dev.getType === "function"
|
||||
? dev.getType()
|
||||
: "";
|
||||
const desc = type ? ` (${type})` : "";
|
||||
this.value(name, `${_("Interface")}: ${name}${desc}`);
|
||||
}
|
||||
});
|
||||
return this.cfgvalue(section_id);
|
||||
}, this),
|
||||
);
|
||||
}
|
||||
|
||||
return this.cfgvalue(section_id);
|
||||
};
|
||||
o.write = function (section_id, value) {
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-29 21:28+0300\n"
|
||||
"PO-Revision-Date: 2026-08-29 21:28+0300\n"
|
||||
"POT-Creation-Date: 2026-08-30 19:56+0300\n"
|
||||
"PO-Revision-Date: 2026-08-30 19:56+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -2135,6 +2135,9 @@ msgstr "Установленная сборка → Доступная сбор
|
||||
msgid "Installed version is newer than release"
|
||||
msgstr "Установлена версия новее релиза"
|
||||
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
|
||||
msgid "Interface Monitoring"
|
||||
msgstr "Мониторинг интерфейсов"
|
||||
|
||||
@@ -2951,6 +2954,9 @@ msgstr "Откройте настройки приоритета и введит
|
||||
msgid "Open Project Page"
|
||||
msgstr "Открыть страницу проекта"
|
||||
|
||||
msgid "OpenWrt Network Interfaces"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation completed successfully!"
|
||||
msgstr "Операция успешно завершена!"
|
||||
|
||||
@@ -3641,8 +3647,8 @@ msgstr "Действие секции"
|
||||
msgid "Section name"
|
||||
msgstr "Имя секции"
|
||||
|
||||
msgid "Section or tunnel interface used for Cloudflare WARP API registration. Supports Connection, WireGuard, AmneziaWG, and Interface sections. Leave empty to use direct WAN / default mixed proxy."
|
||||
msgstr "Секция или туннельный интерфейс для регистрации Cloudflare WARP API. Поддерживает секции Connection, WireGuard, AmneziaWG и Interface. Оставьте пустым для прямого WAN / стандартного mixed proxy."
|
||||
msgid "Section or network interface used for Cloudflare WARP API registration. Supports Tachyon sections (Connection, WireGuard, AmneziaWG, Interface) and OpenWrt system network devices/interfaces (awg0, wg0, tun0, etc.). Leave empty to use direct WAN / default mixed proxy."
|
||||
msgstr ""
|
||||
|
||||
msgid "Section Rules Content"
|
||||
msgstr "Содержимое правил секции"
|
||||
@@ -4205,6 +4211,9 @@ msgstr "Tachyon управляет жизненным циклом процес
|
||||
msgid "Tachyon manages the nfqws2 process lifecycle itself, so daemon mode is not allowed here."
|
||||
msgstr "Tachyon управляет жизненным циклом процесса nfqws2 самостоятельно, поэтому режим daemon здесь не допускается."
|
||||
|
||||
msgid "Tachyon Sections"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tachyon service is stopped. Start the service to display connections."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения подключений."
|
||||
|
||||
@@ -4949,6 +4958,9 @@ msgstr "sing-box outbound для Zapret2 настроен"
|
||||
msgid "Zapret2 sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret2 не настроен"
|
||||
|
||||
msgid "Section or tunnel interface used for Cloudflare WARP API registration. Supports Connection, WireGuard, AmneziaWG, and Interface sections. Leave empty to use direct WAN / default mixed proxy."
|
||||
msgstr "Секция или туннельный интерфейс для регистрации Cloudflare WARP API. Поддерживает секции Connection, WireGuard, AmneziaWG и Interface. Оставьте пустым для прямого WAN / стандартного mixed proxy."
|
||||
|
||||
msgid "Block internet access or specific proxy sections for individual devices based on time and days."
|
||||
msgstr "Блокировка доступа в интернет или определенных секций маршрутизации для отдельных устройств по времени и дням недели."
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2673,13 +2673,20 @@ function run_doctor_checks_impl(repair) {
|
||||
// Check Main DNS
|
||||
let dns_main_reachable = false;
|
||||
if (main_dns_type == "doh") {
|
||||
let curl_cmd = "curl -sS --max-time 4 -o /dev/null -w %{http_code} https://" + display_main + "/dns-query";
|
||||
let doh_url = display_main;
|
||||
if (!match(doh_url, /^https?:\/\//))
|
||||
doh_url = "https://" + doh_url;
|
||||
if (!match(doh_url, /\/dns-query$/) && !match(doh_url, /\/query$/) && !match(doh_url, /^https?:\/\/[^\/]+\/.+/))
|
||||
doh_url = doh_url + "/dns-query";
|
||||
|
||||
let curl_cmd = "curl -s -m 4 -o /dev/null -w '%{http_code}' " + shell_quote(doh_url) + " 2>/dev/null";
|
||||
let curl_res = command_capture(curl_cmd);
|
||||
if (curl_res.status == 0 && int(curl_res.output) < 400) {
|
||||
dns_main_reachable = true;
|
||||
doc_check("✅", "DNS main (" + display_main + ")", "reachable", "");
|
||||
} else {
|
||||
let curl_cmd2 = "curl -sS --max-time 4 -o /dev/null -w %{http_code} https://" + display_main;
|
||||
let base_url = replace(doh_url, /\/dns-query$/, "");
|
||||
let curl_cmd2 = "curl -s -m 4 -o /dev/null -w '%{http_code}' " + shell_quote(base_url) + " 2>/dev/null";
|
||||
let curl_res2 = command_capture(curl_cmd2);
|
||||
if (curl_res2.status == 0 && int(curl_res2.output) < 400) {
|
||||
dns_main_reachable = true;
|
||||
@@ -3276,6 +3283,7 @@ const DOCTOR_LOCK_DIR = "/var/run/tachyon.doctor.lock";
|
||||
const DOCTOR_HISTORY_FILE = "/tmp/tachyon_doctor_history.json";
|
||||
|
||||
function doctor_lock_try() {
|
||||
command_status("mkdir -p /var/run /tmp/run 2>/dev/null");
|
||||
if (command_status("mkdir " + shell_quote(DOCTOR_LOCK_DIR) + " 2>/dev/null") == 0)
|
||||
return true;
|
||||
let st = fs.stat(DOCTOR_LOCK_DIR);
|
||||
|
||||
@@ -70,6 +70,13 @@ if (warp_proxy_section != "") {
|
||||
}
|
||||
} else if (fs.stat("/sys/class/net/" + warp_proxy_section) != null) {
|
||||
proxy_interface = warp_proxy_section;
|
||||
} else if (uci.get("network", warp_proxy_section)) {
|
||||
let net_dev = uci.get("network", warp_proxy_section, "device") ||
|
||||
uci.get("network", warp_proxy_section, "ifname") ||
|
||||
warp_proxy_section;
|
||||
if (fs.stat("/sys/class/net/" + net_dev) != null) {
|
||||
proxy_interface = net_dev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user