release: 1.3.8 - OpenWrt network interfaces for WARP generator, DoH doctor URL normalization & doctor lock fixes

This commit is contained in:
Dushnilin
2026-08-30 20:05:16 +03:00
parent b3e0a73b2d
commit dfb32722bf
13 changed files with 2342 additions and 2053 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -4
View File
@@ -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 "Блокировка доступа в интернет или определенных секций маршрутизации для отдельных устройств по времени и дням недели."
+2 -2
View File
@@ -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 -1
View File
@@ -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) {
+16 -4
View File
@@ -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
+10 -2
View File
@@ -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;
}
}
}