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