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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user