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:
Dushnilin
2026-09-07 22:50:10 +03:00
parent 394c717bf9
commit 5539b9701f
25 changed files with 377 additions and 54 deletions
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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"
]
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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;
}
+2
View File
@@ -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');
+5
View File
@@ -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");
+2 -2
View File
@@ -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 "Ожидание перезапуска служб роутера..."
+1 -1
View File
@@ -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;
+6 -5
View File
@@ -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
};
+11 -4
View File
@@ -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";
+19 -3
View File
@@ -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);
+2 -6
View File
@@ -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 '