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
@@ -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");