fix(singbox): prevent UDP routing to HTTP outbounds (#45) and improve dashboard metadata
- Ensure selector and urltest groups require all member outbounds to support UDP - Add explicit reject rule for UDP on non-UDP outbounds to avoid sing-box router log spam - Add http/s:// to connection URL description in LuCI settings and update localization - Display transport protocol badges on dashboard outbound cards and modal rows - Deduplicate and collapse UDP routing error lines in telegram logs and bug report
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "Ожидание перезапуска служб роутера..."
|
||||
|
||||
@@ -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})`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -10,3 +10,5 @@ export * from './parseQueryString';
|
||||
export * from './svgEl';
|
||||
export * from './insertIf';
|
||||
export * from './isCopyableProxyLink';
|
||||
export * from './formatOutboundType';
|
||||
export * from './getProxyUrlTransport';
|
||||
|
||||
@@ -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 (Авто)');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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<string, string>;
|
||||
countries?: Record<string, string>;
|
||||
transports?: Record<string, string>;
|
||||
protocols?: Record<string, string>;
|
||||
securities?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface GetSingBoxStatus {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 "Ожидание перезапуска служб роутера..."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 '
|
||||
|
||||
Reference in New Issue
Block a user