diff --git a/fe-app-tachyon/fill-po-translations.js b/fe-app-tachyon/fill-po-translations.js index c10b8b3c..da82551d 100644 --- a/fe-app-tachyon/fill-po-translations.js +++ b/fe-app-tachyon/fill-po-translations.js @@ -826,7 +826,7 @@ const dictionary = { "Invalid TUIC URL: udp_relay_mode must be native, quic, or qux": "Неверный TUIC URL: udp_relay_mode должен быть native, quic или qux", "Invalid TUIC URL: zero_rtt_handshake must be 0, 1, true, or false": "Неверный TUIC URL: zero_rtt_handshake должен быть 0, 1, true или false", "Invalid Shadowsocks URL: decoded credentials must contain method:password": "Неверный Shadowsocks URL: декодированные учётные данные должны содержать method:password", - "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links": "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://", + "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links": "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2://", "New version %s is available for %s": "Доступна новая версия %s для %s", "Checking\u2026": "Проверка\u2026", "Starting\u2026": "Запуск\u2026", diff --git a/fe-app-tachyon/locales/calls.json b/fe-app-tachyon/locales/calls.json index e084a9b4..6c022287 100644 --- a/fe-app-tachyon/locales/calls.json +++ b/fe-app-tachyon/locales/calls.json @@ -13593,8 +13593,8 @@ ] }, { - "call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", - "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + "call": "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links", + "key": "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links", "places": [ "../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:9600" ] diff --git a/fe-app-tachyon/locales/tachyon.pot b/fe-app-tachyon/locales/tachyon.pot index 5204c828..ad1a3653 100644 --- a/fe-app-tachyon/locales/tachyon.pot +++ b/fe-app-tachyon/locales/tachyon.pot @@ -8420,7 +8420,7 @@ msgid "VLESS flow" msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:9600 -msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links" msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:19612 diff --git a/fe-app-tachyon/locales/tachyon.ru.po b/fe-app-tachyon/locales/tachyon.ru.po index e73d91b3..839fcf82 100644 --- a/fe-app-tachyon/locales/tachyon.ru.po +++ b/fe-app-tachyon/locales/tachyon.ru.po @@ -5207,8 +5207,8 @@ msgstr "Видимое имя этой секции" msgid "VLESS flow" msgstr "VLESS flow" -msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" -msgstr "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links" +msgstr "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2://" msgid "Waiting for router services to restart..." msgstr "Ожидание перезапуска служб роутера..." diff --git a/fe-app-tachyon/src/helpers/formatOutboundType.ts b/fe-app-tachyon/src/helpers/formatOutboundType.ts new file mode 100644 index 00000000..2f940a11 --- /dev/null +++ b/fe-app-tachyon/src/helpers/formatOutboundType.ts @@ -0,0 +1,18 @@ +export function formatOutboundType(type?: string, transport?: string): string { + const cleanType = (type || '').trim(); + const cleanTransport = (transport || '').trim().toUpperCase(); + + if (!cleanType) { + return cleanTransport ? `(${cleanTransport})` : ''; + } + + if (!cleanTransport) { + return cleanType; + } + + if (cleanType.toUpperCase().includes(`(${cleanTransport})`)) { + return cleanType; + } + + return `${cleanType} (${cleanTransport})`; +} diff --git a/fe-app-tachyon/src/helpers/getProxyUrlTransport.ts b/fe-app-tachyon/src/helpers/getProxyUrlTransport.ts new file mode 100644 index 00000000..c5a264e9 --- /dev/null +++ b/fe-app-tachyon/src/helpers/getProxyUrlTransport.ts @@ -0,0 +1,34 @@ +export function getProxyUrlTransport(url?: string): string | undefined { + if (!url || typeof url !== 'string') { + return undefined; + } + + try { + const trimmed = url.trim(); + if (trimmed.startsWith('vmess://')) { + const b64 = trimmed.slice(8).split('#')[0]; + if (typeof atob === 'function') { + const jsonStr = atob(b64.replace(/[\r\n]/g, '')); + const obj = JSON.parse(jsonStr); + if (obj && typeof obj.net === 'string' && obj.net.trim()) { + return obj.net.trim().toLowerCase(); + } + } + } + + const [base] = trimmed.split('#'); + const queryIdx = base.indexOf('?'); + if (queryIdx !== -1) { + const query = base.slice(queryIdx + 1); + const params = new URLSearchParams(query); + const transport = params.get('type'); + if (transport && transport.trim()) { + return transport.trim().toLowerCase(); + } + } + } catch { + // Ignore URL parse errors + } + + return undefined; +} diff --git a/fe-app-tachyon/src/helpers/index.ts b/fe-app-tachyon/src/helpers/index.ts index a62dd72b..307a8863 100644 --- a/fe-app-tachyon/src/helpers/index.ts +++ b/fe-app-tachyon/src/helpers/index.ts @@ -10,3 +10,5 @@ export * from './parseQueryString'; export * from './svgEl'; export * from './insertIf'; export * from './isCopyableProxyLink'; +export * from './formatOutboundType'; +export * from './getProxyUrlTransport'; diff --git a/fe-app-tachyon/src/helpers/tests/formatOutboundType.test.ts b/fe-app-tachyon/src/helpers/tests/formatOutboundType.test.ts new file mode 100644 index 00000000..063047ff --- /dev/null +++ b/fe-app-tachyon/src/helpers/tests/formatOutboundType.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { formatOutboundType } from '../formatOutboundType'; + +describe('formatOutboundType', () => { + it('formats protocol with transport in uppercase parentheses', () => { + expect(formatOutboundType('VLESS', 'xhttp')).toBe('VLESS (XHTTP)'); + expect(formatOutboundType('vless', 'tcp')).toBe('vless (TCP)'); + expect(formatOutboundType('VMess', 'ws')).toBe('VMess (WS)'); + expect(formatOutboundType('Trojan', 'grpc')).toBe('Trojan (GRPC)'); + }); + + it('leaves protocol untouched if transport is not provided', () => { + expect(formatOutboundType('VLESS')).toBe('VLESS'); + expect(formatOutboundType('Direct')).toBe('Direct'); + expect(formatOutboundType(undefined)).toBe(''); + }); + + it('handles group types without adding transport', () => { + expect(formatOutboundType('Priority (Авто)', 'tcp')).toBe( + 'Priority (Авто) (TCP)', + ); + expect(formatOutboundType('URLTest (Авто)')).toBe('URLTest (Авто)'); + }); +}); diff --git a/fe-app-tachyon/src/helpers/tests/getProxyUrlTransport.test.ts b/fe-app-tachyon/src/helpers/tests/getProxyUrlTransport.test.ts new file mode 100644 index 00000000..ae560a09 --- /dev/null +++ b/fe-app-tachyon/src/helpers/tests/getProxyUrlTransport.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; +import { getProxyUrlTransport } from '../getProxyUrlTransport'; + +describe('getProxyUrlTransport', () => { + it('extracts transport from vless query parameters', () => { + expect( + getProxyUrlTransport( + 'vless://uuid@example.com:443?security=reality&type=xhttp#Server', + ), + ).toBe('xhttp'); + expect( + getProxyUrlTransport( + 'vless://uuid@example.com:443?security=tls&type=ws&path=/ws#Server', + ), + ).toBe('ws'); + expect( + getProxyUrlTransport( + 'vless://uuid@example.com:443?security=tls&type=grpc#Server', + ), + ).toBe('grpc'); + expect( + getProxyUrlTransport( + 'vless://uuid@example.com:443?security=none&type=tcp#Server', + ), + ).toBe('tcp'); + }); + + it('extracts transport from vmess base64 net field', () => { + // base64 of {"v":"2","ps":"test","add":"1.1.1.1","port":"443","id":"...","net":"ws"} + const vmessWs = + 'vmess://' + + btoa( + JSON.stringify({ + v: '2', + ps: 'test', + add: '1.1.1.1', + port: '443', + id: 'uuid', + net: 'ws', + }), + ); + expect(getProxyUrlTransport(vmessWs)).toBe('ws'); + + const vmessTcp = + 'vmess://' + + btoa( + JSON.stringify({ + v: '2', + ps: 'test', + add: '1.1.1.1', + port: '443', + id: 'uuid', + net: 'tcp', + }), + ); + expect(getProxyUrlTransport(vmessTcp)).toBe('tcp'); + }); + + it('returns undefined for invalid or empty links', () => { + expect(getProxyUrlTransport('')).toBeUndefined(); + expect(getProxyUrlTransport(undefined)).toBeUndefined(); + expect(getProxyUrlTransport('invalid-link')).toBeUndefined(); + }); +}); diff --git a/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts b/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts index c2abb1da..54e169b3 100644 --- a/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts +++ b/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts @@ -4,6 +4,7 @@ import { canUseDirectClashApi, getClashHttpUrl, getProxyUrlName, + getProxyUrlTransport, isCopyableProxyLink, } from '../../../helpers'; import { getOutboundTagBySection } from '../../runtimeTags'; @@ -932,6 +933,9 @@ function buildUrlTestInfo({ ), latency: childEntry?.value?.history?.[0]?.delay || 0, type: childEntry?.value?.type || '', + transport: + outboundMetadata?.transports?.[childCode] || + getProxyUrlTransport(link), selected: selectedCode === childCode, link, canCopyLink, @@ -1061,6 +1065,9 @@ function buildPriorityInfo({ ), latency: childEntry?.value?.history?.[0]?.delay || 0, type: childEntry?.value?.type || '', + transport: + outboundMetadata?.transports?.[childCode] || + getProxyUrlTransport(link), selected: selectedCode === childCode, link, canCopyLink, @@ -1252,12 +1259,19 @@ function buildProxyGroupOutbounds( const latency = item?.value.history?.[0]?.delay || activeMemberLatency || 0; + const isGroupType = Boolean( + priorityConfig || urlTestConfig || isRuntimeUrlTest, + ); + return [ { code, displayName, latency, type: priorityConfig ? 'Priority' : item?.value.type || 'URLTest', + transport: isGroupType + ? undefined + : outboundMetadata?.transports?.[code] || getProxyUrlTransport(link), selected: isSelected, link, canCopyLink, @@ -1441,6 +1455,9 @@ function getOutboundMetadata(dashboardCache?: DashboardSectionCache) { return { names: objectMap(metadata.names), countries: objectMap(metadata.countries), + transports: objectMap(metadata.transports), + protocols: objectMap(metadata.protocols), + securities: objectMap(metadata.securities), }; } diff --git a/fe-app-tachyon/src/tachyon/methods/custom/tests/getDashboardSections.test.ts b/fe-app-tachyon/src/tachyon/methods/custom/tests/getDashboardSections.test.ts index fc3d78f4..2005e138 100644 --- a/fe-app-tachyon/src/tachyon/methods/custom/tests/getDashboardSections.test.ts +++ b/fe-app-tachyon/src/tachyon/methods/custom/tests/getDashboardSections.test.ts @@ -22,6 +22,9 @@ vi.mock('../../../../helpers', () => ({ getClashHttpUrl: () => 'http://router.example:9090', getProxyUrlName: (link?: string) => link?.includes('#') ? decodeURIComponent(link.split('#').pop() || '') : '', + getProxyUrlTransport: (_link?: string) => undefined, + formatOutboundType: (type?: string, transport?: string) => + transport ? `${type} (${transport.toUpperCase()})` : type || '', isCopyableProxyLink: (link?: string) => Boolean(link), })); diff --git a/fe-app-tachyon/src/tachyon/tabs/dashboard/initController.ts b/fe-app-tachyon/src/tachyon/tabs/dashboard/initController.ts index cea3d9de..30fd2d54 100644 --- a/fe-app-tachyon/src/tachyon/tabs/dashboard/initController.ts +++ b/fe-app-tachyon/src/tachyon/tabs/dashboard/initController.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + formatOutboundType, getClashWsUrl, isCopyableProxyLink, onMount, @@ -1057,7 +1058,10 @@ function renderCommonDetailsModal( class: 'tachyon_dashboard-page__urltest-details__row-type', }, - member.type, + formatOutboundType( + member.type, + member.transport, + ), ), ] : []), @@ -1072,7 +1076,10 @@ function renderCommonDetailsModal( class: 'tachyon_dashboard-page__urltest-details__row-type', }, - member.type, + formatOutboundType( + member.type, + member.transport, + ), ), ] : []), diff --git a/fe-app-tachyon/src/tachyon/tabs/dashboard/partials/renderSections.ts b/fe-app-tachyon/src/tachyon/tabs/dashboard/partials/renderSections.ts index ebf7dfe4..1a75f5ec 100644 --- a/fe-app-tachyon/src/tachyon/tabs/dashboard/partials/renderSections.ts +++ b/fe-app-tachyon/src/tachyon/tabs/dashboard/partials/renderSections.ts @@ -4,7 +4,11 @@ import { renderLinkIcon24, renderInfoIcon24, } from '../../../../icons'; -import { isCopyableProxyLink, svgEl } from '../../../../helpers'; +import { + formatOutboundType, + isCopyableProxyLink, + svgEl, +} from '../../../../helpers'; import { prettyBytes } from '../../../../helpers/prettyBytes'; import { Tachyon } from '../../../types'; import { renderFlagEmojis } from './renderFlagEmojis'; @@ -525,7 +529,9 @@ function renderDefaultState({ style: 'opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;', }, - [outbound.type].filter(Boolean), + [formatOutboundType(outbound.type, outbound.transport)].filter( + Boolean, + ), ), E( 'div', @@ -579,12 +585,14 @@ function renderDefaultState({ outbound.priorityInfo?.selectedName || ''; + const baseType = formatOutboundType(outbound.type, outbound.transport); + const typeLabel = isManualUrlTest || isManualPriority - ? `${outbound.type} (${_('Manual')})` + ? `${baseType} (${_('Manual')})` : outbound.urlTestInfo || outbound.priorityInfo - ? `${outbound.type} (${_('Auto')})` - : outbound.type; + ? `${baseType} (${_('Auto')})` + : baseType; return E( 'div', diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts index 38e5a1cc..8b3c8024 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts @@ -1712,18 +1712,39 @@ async function handleGenerateBugReport() { args: ['-e', 'sing-box', '-l', '1000'], }); + const filterUdpErrors = (text: string) => { + if (!text || !text.includes('UDP is not supported by outbound:')) + return text; + const lines = text.split('\n'); + const filtered: string[] = []; + let noticeEmitted = false; + for (const line of lines) { + if (line.includes('UDP is not supported by outbound:')) { + if (!noticeEmitted) { + filtered.push( + 'UDP traffic through HTTP outbounds is not supported by sing-box; repeated UDP warnings for HTTP outbounds are hidden by Tachyon.', + ); + noticeEmitted = true; + } + continue; + } + filtered.push(line); + } + return filtered.join('\n'); + }; + const rawReport = [ '--- TACHYON CONFIG ---', configResult || 'Failed to fetch config', '', '--- TACHYON LOGS ---', logsResult.code === 0 - ? logsResult.stdout + ? filterUdpErrors(logsResult.stdout) : 'Failed to fetch tachyon logs', '', '--- SING-BOX LOGS ---', singboxLogsResult.code === 0 - ? singboxLogsResult.stdout + ? filterUdpErrors(singboxLogsResult.stdout) : 'Failed to fetch sing-box logs', ].join('\n'); diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index 24e5e407..f95ab57c 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -135,6 +135,7 @@ export namespace Tachyon { displayName: string; latency: number; type: string; + transport?: string; selected: boolean; link?: string; canCopyLink?: boolean; @@ -149,6 +150,7 @@ export namespace Tachyon { displayName: string; latency: number; type: string; + transport?: string; selected: boolean; link?: string; canCopyLink?: boolean; @@ -567,6 +569,9 @@ export namespace Tachyon { export interface GetOutboundMetadata { names?: Record; countries?: Record; + transports?: Record; + protocols?: Record; + securities?: Record; } export interface GetSingBoxStatus { diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index b92e5cf7..d27e7dc9 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -1423,6 +1423,54 @@ function isCopyableProxyLink(link) { return COPYABLE_PROXY_URI_RE.test((link || "").trim()); } +// src/helpers/formatOutboundType.ts +function formatOutboundType(type, transport) { + const cleanType = (type || "").trim(); + const cleanTransport = (transport || "").trim().toUpperCase(); + if (!cleanType) { + return cleanTransport ? `(${cleanTransport})` : ""; + } + if (!cleanTransport) { + return cleanType; + } + if (cleanType.toUpperCase().includes(`(${cleanTransport})`)) { + return cleanType; + } + return `${cleanType} (${cleanTransport})`; +} + +// src/helpers/getProxyUrlTransport.ts +function getProxyUrlTransport(url) { + if (!url || typeof url !== "string") { + return void 0; + } + try { + const trimmed = url.trim(); + if (trimmed.startsWith("vmess://")) { + const b64 = trimmed.slice(8).split("#")[0]; + if (typeof atob === "function") { + const jsonStr = atob(b64.replace(/[\r\n]/g, "")); + const obj = JSON.parse(jsonStr); + if (obj && typeof obj.net === "string" && obj.net.trim()) { + return obj.net.trim().toLowerCase(); + } + } + } + const [base] = trimmed.split("#"); + const queryIdx = base.indexOf("?"); + if (queryIdx !== -1) { + const query = base.slice(queryIdx + 1); + const params = new URLSearchParams(query); + const transport = params.get("type"); + if (transport && transport.trim()) { + return transport.trim().toLowerCase(); + } + } + } catch { + } + return void 0; +} + // src/icons/renderLoaderCircleIcon24.ts function renderLoaderCircleIcon24() { const NS = "http://www.w3.org/2000/svg"; @@ -2570,7 +2618,9 @@ function renderDefaultState({ { style: "opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;" }, - [outbound.type].filter(Boolean) + [formatOutboundType(outbound.type, outbound.transport)].filter( + Boolean + ) ), E( "div", @@ -2617,7 +2667,8 @@ function renderDefaultState({ const isManualUrlTest = Boolean(outbound.urlTestInfo?.isManualSelection); const isManualPriority = Boolean(outbound.priorityInfo?.isManualSelection); const activeServerName = outbound.urlTestInfo?.selectedName || outbound.priorityInfo?.selectedName || ""; - const typeLabel = isManualUrlTest || isManualPriority ? `${outbound.type} (${_("Manual")})` : outbound.urlTestInfo || outbound.priorityInfo ? `${outbound.type} (${_("Auto")})` : outbound.type; + const baseType = formatOutboundType(outbound.type, outbound.transport); + const typeLabel = isManualUrlTest || isManualPriority ? `${baseType} (${_("Manual")})` : outbound.urlTestInfo || outbound.priorityInfo ? `${baseType} (${_("Auto")})` : baseType; return E( "div", { @@ -5244,6 +5295,7 @@ function buildUrlTestInfo({ ), latency: childEntry?.value?.history?.[0]?.delay || 0, type: childEntry?.value?.type || "", + transport: outboundMetadata?.transports?.[childCode] || getProxyUrlTransport(link), selected: selectedCode === childCode, link, canCopyLink, @@ -5329,6 +5381,7 @@ function buildPriorityInfo({ ), latency: childEntry?.value?.history?.[0]?.delay || 0, type: childEntry?.value?.type || "", + transport: outboundMetadata?.transports?.[childCode] || getProxyUrlTransport(link), selected: selectedCode === childCode, link, canCopyLink, @@ -5458,12 +5511,16 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro (m) => m.selected || m.code === priorityInfo.selectedCode )?.latency || 0; const latency = item?.value.history?.[0]?.delay || activeMemberLatency || 0; + const isGroupType = Boolean( + priorityConfig || urlTestConfig || isRuntimeUrlTest + ); return [ { code, displayName, latency, type: priorityConfig ? "Priority" : item?.value.type || "URLTest", + transport: isGroupType ? void 0 : outboundMetadata?.transports?.[code] || getProxyUrlTransport(link), selected: isSelected, link, canCopyLink, @@ -5570,7 +5627,10 @@ function getOutboundMetadata(dashboardCache) { } return { names: objectMap(metadata.names), - countries: objectMap(metadata.countries) + countries: objectMap(metadata.countries), + transports: objectMap(metadata.transports), + protocols: objectMap(metadata.protocols), + securities: objectMap(metadata.securities) }; } function getCachedProxyLinks(dashboardCache) { @@ -8242,7 +8302,10 @@ function renderCommonDetailsModal(info, fields, renderMemberName, isPriority, se { class: "tachyon_dashboard-page__urltest-details__row-type" }, - member.type + formatOutboundType( + member.type, + member.transport + ) ) ] : [] ] : [ @@ -8253,7 +8316,10 @@ function renderCommonDetailsModal(info, fields, renderMemberName, isPriority, se { class: "tachyon_dashboard-page__urltest-details__row-type" }, - member.type + formatOutboundType( + member.type, + member.transport + ) ) ] : [] ] @@ -16463,15 +16529,35 @@ async function handleGenerateBugReport() { command: "/sbin/logread", args: ["-e", "sing-box", "-l", "1000"] }); + const filterUdpErrors = (text) => { + if (!text || !text.includes("UDP is not supported by outbound:")) + return text; + const lines = text.split("\n"); + const filtered = []; + let noticeEmitted = false; + for (const line of lines) { + if (line.includes("UDP is not supported by outbound:")) { + if (!noticeEmitted) { + filtered.push( + "UDP traffic through HTTP outbounds is not supported by sing-box; repeated UDP warnings for HTTP outbounds are hidden by Tachyon." + ); + noticeEmitted = true; + } + continue; + } + filtered.push(line); + } + return filtered.join("\n"); + }; const rawReport = [ "--- TACHYON CONFIG ---", configResult || "Failed to fetch config", "", "--- TACHYON LOGS ---", - logsResult.code === 0 ? logsResult.stdout : "Failed to fetch tachyon logs", + logsResult.code === 0 ? filterUdpErrors(logsResult.stdout) : "Failed to fetch tachyon logs", "", "--- SING-BOX LOGS ---", - singboxLogsResult.code === 0 ? singboxLogsResult.stdout : "Failed to fetch sing-box logs" + singboxLogsResult.code === 0 ? filterUdpErrors(singboxLogsResult.stdout) : "Failed to fetch sing-box logs" ].join("\n"); const maskedReport = maskGlobalCheckText(rawReport); const blob = new Blob([maskedReport], { type: "text/plain;charset=utf-8" }); diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js index a278825a..0d15175b 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js @@ -9598,7 +9598,7 @@ function createSectionContent(section) { "selector_proxy_links", _("Connection URL"), _( - "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links", ), ); o.depends("action", "connection"); diff --git a/luci-app-tachyon/po/ru/tachyon.po b/luci-app-tachyon/po/ru/tachyon.po index e73d91b3..839fcf82 100644 --- a/luci-app-tachyon/po/ru/tachyon.po +++ b/luci-app-tachyon/po/ru/tachyon.po @@ -5207,8 +5207,8 @@ msgstr "Видимое имя этой секции" msgid "VLESS flow" msgstr "VLESS flow" -msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" -msgstr "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links" +msgstr "Ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2://" msgid "Waiting for router services to restart..." msgstr "Ожидание перезапуска служб роутера..." diff --git a/luci-app-tachyon/po/templates/tachyon.pot b/luci-app-tachyon/po/templates/tachyon.pot index 5204c828..ad1a3653 100644 --- a/luci-app-tachyon/po/templates/tachyon.pot +++ b/luci-app-tachyon/po/templates/tachyon.pot @@ -8420,7 +8420,7 @@ msgid "VLESS flow" msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:9600 -msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http/s://, hy2/hysteria2:// links" msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:19612 diff --git a/tachyon/files/usr/lib/service/telegram.uc b/tachyon/files/usr/lib/service/telegram.uc index ced8db6c..0a6d117c 100644 --- a/tachyon/files/usr/lib/service/telegram.uc +++ b/tachyon/files/usr/lib/service/telegram.uc @@ -1731,8 +1731,16 @@ function view_logs(token, chat_id, msg_id, level, count) { if (res && res.status == 0 && res.output) { let lines = split(res.output, "\n"); let filtered = []; + let udp_http_notice_emitted = false; for (let line in lines) { if (line == "") continue; + if (index(as_string(line), "UDP is not supported by outbound:") >= 0) { + if (!udp_http_notice_emitted) { + push(filtered, "UDP traffic through HTTP outbounds is not supported by sing-box; repeated UDP warnings for HTTP outbounds are hidden by Tachyon."); + udp_http_notice_emitted = true; + } + continue; + } if (level == "error" && !match(line, /\[err\]|\[error\]/i)) continue; if (level == "warn" && !match(line, /\[warn\]/i)) continue; if (level == "info" && !match(line, /\[info\]/i)) continue; diff --git a/tachyon/files/usr/lib/singbox/generator.uc b/tachyon/files/usr/lib/singbox/generator.uc index dbac27aa..a544fd30 100644 --- a/tachyon/files/usr/lib/singbox/generator.uc +++ b/tachyon/files/usr/lib/singbox/generator.uc @@ -632,11 +632,12 @@ function add_mixed_proxy_for_section(config, section, service_address) { inbound.users = [{ username, password }]; } push(config.inbounds, inbound); - push(config.route.rules, { + let section_outbound = runtime_constants.outbound_tag(section[".name"]); + generator_routes.push_section_route_rule(config, { action: "route", inbound: inbound.tag, - outbound: runtime_constants.outbound_tag(section[".name"]) - }); + outbound: section_outbound + }, section_outbound); } /* @@ -685,11 +686,11 @@ function add_service_mixed_proxy_inbound(config, tag_name, listen_port, outbound listen: runtime_constants.SERVICE_MIXED_INBOUND_ADDRESS, listen_port }); - push(config.route.rules, { + generator_routes.push_section_route_rule(config, { action: "route", inbound: tag_name, outbound - }); + }, outbound); } function service_mixed_proxy_inbound_tag_for_purpose(purpose) { diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index 46b28e0f..77805d34 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -1189,31 +1189,35 @@ function tproxy_inbound_matcher() { return [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.TPROXY_INBOUND6_TAG ]; } -function outbound_supports_udp(config, tag_name) { +function outbound_supports_udp(config, tag_name, visited) { tag_name = as_string(tag_name); if (tag_name == "") + return false; + if (tag_name == runtime_constants.DIRECT_OUTBOUND_TAG || tag_name == runtime_constants.BYPASS_OUTBOUND_TAG) return true; + visited = visited || {}; + if (visited[tag_name]) + return true; + visited[tag_name] = true; for (let outb in (config.outbounds || [])) { if (as_string(outb.tag) == tag_name) { let t = as_string(outb.type); - if (t == "http") + if (t == "http" || (t == "socks" && as_string(outb.version) == "4") || t == "block") return false; if (t == "selector" || t == "urltest") { if (type(outb.outbounds) == "array" && length(outb.outbounds) > 0) { - let has_udp = false; for (let nested_tag in outb.outbounds) { - if (outbound_supports_udp(config, nested_tag)) { - has_udp = true; - break; - } + if (!outbound_supports_udp(config, nested_tag, visited)) + return false; } - return has_udp; + return true; } + return false; } return true; } } - return true; + return false; } function push_section_route_rule(config, rule, target_outbound) { @@ -1860,5 +1864,7 @@ return { enabled_sections, enabled_servers, section_by_name, - add_server_routes + add_server_routes, + outbound_supports_udp, + push_section_route_rule }; diff --git a/tachyon/files/usr/lib/singbox/subscription.uc b/tachyon/files/usr/lib/singbox/subscription.uc index d3755035..71e36bbe 100644 --- a/tachyon/files/usr/lib/singbox/subscription.uc +++ b/tachyon/files/usr/lib/singbox/subscription.uc @@ -204,11 +204,18 @@ function remember_outbound_metadata(state, tag_name, display_name, outbound) { state.outboundMetadata.protocols[tag_name] = protocol; let transport = lc(as_string(object_or_empty(outbound.transport).type || "")); - if (transport == "" || transport == "raw") - transport = "tcp"; - else if (transport == "h2") + if (transport == "h2") transport = "http"; - state.outboundMetadata.transports[tag_name] = transport; + else if (transport == "raw") + transport = "tcp"; + else if (transport == "" && protocol != "" && + protocol != "direct" && protocol != "wireguard" && + protocol != "hysteria" && protocol != "hysteria2" && protocol != "tuic" && + protocol != "dns" && protocol != "block" && + protocol != "selector" && protocol != "urltest") + transport = "tcp"; + if (transport != "") + state.outboundMetadata.transports[tag_name] = transport; let tls = type(outbound.tls) == "object" ? outbound.tls : null; let security = "none"; diff --git a/tachyon/files/usr/lib/subscription/cache.uc b/tachyon/files/usr/lib/subscription/cache.uc index 8a7af302..9b257836 100644 --- a/tachyon/files/usr/lib/subscription/cache.uc +++ b/tachyon/files/usr/lib/subscription/cache.uc @@ -1065,7 +1065,8 @@ function write_user_agent_candidates(path, configured_user_agent, preferred_user function write_empty_outbound_metadata() { write_stdout_json({ names: {}, - countries: {} + countries: {}, + transports: {} }); } @@ -1155,6 +1156,9 @@ function normalize_cache(cache, section, format_version) { cache.outboundMetadata = object_or_empty(cache.outboundMetadata); cache.outboundMetadata.names = object_or_empty(cache.outboundMetadata.names); cache.outboundMetadata.countries = object_or_empty(cache.outboundMetadata.countries); + cache.outboundMetadata.transports = object_or_empty(cache.outboundMetadata.transports); + cache.outboundMetadata.protocols = object_or_empty(cache.outboundMetadata.protocols); + cache.outboundMetadata.securities = object_or_empty(cache.outboundMetadata.securities); cache.servers = object_or_empty(cache.servers); cache.urltestCandidateTags = array_or_empty(cache.urltestCandidateTags); cache.urltestGroups = object_or_empty(cache.urltestGroups); @@ -1179,9 +1183,13 @@ function save_cache(cache_dir, section, format_version, cache) { function write_outbound_metadata(cache_dir, format_version, section, names_path, countries_path, servers_path) { let cache = load_cache(cache_dir, section); + let previous_metadata = object_or_empty(cache.outboundMetadata); cache.outboundMetadata = { names: object_or_empty(read_json(names_path)), - countries: object_or_empty(read_json(countries_path)) + countries: object_or_empty(read_json(countries_path)), + transports: object_or_empty(previous_metadata.transports), + protocols: object_or_empty(previous_metadata.protocols), + securities: object_or_empty(previous_metadata.securities) }; cache.servers = object_or_empty(read_json(servers_path)); save_cache(cache_dir, section, format_version, cache); @@ -1354,12 +1362,14 @@ function get_outbound_metadata(cache_dir, section, legacy_path) { metadata = object_or_empty(metadata); let names = object_or_empty(metadata.names); let countries = object_or_empty(metadata.countries); + let transports = object_or_empty(metadata.transports); let candidate_tags = array_or_empty(cache.urltestCandidateTags); let groups = object_or_empty(cache.urltestGroups); let priority_groups = object_or_empty(cache.priorityGroups); let result = { names: {}, - countries: {} + countries: {}, + transports: {} }; if (length(candidate_tags) > 0) { @@ -1371,6 +1381,8 @@ function get_outbound_metadata(cache_dir, section, legacy_path) { result.names[tag] = names[tag]; if (countries[tag] != null) result.countries[tag] = countries[tag]; + if (transports[tag] != null) + result.transports[tag] = transports[tag]; } } else { @@ -1382,6 +1394,10 @@ function get_outbound_metadata(cache_dir, section, legacy_path) { if (!groups[tag] && !priority_groups[tag]) result.countries[tag] = country; } + for (let tag, transport in transports) { + if (!groups[tag] && !priority_groups[tag]) + result.transports[tag] = transport; + } } write_stdout_json(result); diff --git a/tests/luci_localization.sh b/tests/luci_localization.sh index e1859eba..8c72d252 100755 --- a/tests/luci_localization.sh +++ b/tests/luci_localization.sh @@ -20,12 +20,8 @@ fi grep -Fq '_("Close")' "$SECTION_JS" || fail "Tachyon section settings modal must expose a Close action" -if grep -Fq 'http(s)://, hy2/hysteria2:// links' \ - "$SECTION_JS" "$SOURCE_PO" "$PACKAGE_PO" "$SOURCE_POT" "$PACKAGE_POT" "$CALLS_JSON"; then - fail "Connection URL help must not advertise removed HTTP proxy links" -fi -grep -Fq 'socks4/5://, hy2/hysteria2:// links' "$SECTION_JS" || - fail "Connection URL help must list the remaining native proxy links" +grep -Fq 'socks4/5://, http/s://, hy2/hysteria2:// links' "$SECTION_JS" || + fail "Connection URL help must list the supported proxy links" for po in "$SOURCE_PO" "$PACKAGE_PO"; do awk '