Files
NeoTachyon/tachyon/files/usr/lib/singbox/route.uc
T
Dushnilin 26431f50d3 feat: per-section MAC/DNS exclusions, boot latency sweep, WDTT/OlcRTC providers, DNS hijack socket collision fix
Features:
- Per-Section MAC Exclusions: nft, generator_routes, section.js, i18n
- Per-Section Routed DNS: connections, generator_routes, nft, validator,
  types.ts, section.js, state.uc, reload.uc
- Boot Latency Sweep: ui.uc latency boot sweep with fixture dispatchers
- WDTT provider: full backend (common, validator, runtime), sing-box
  outbound, diagnostics, component install, updater, i18n, section.js UI
- OlcRTC provider: full backend (common, validator, runtime), sing-box
  outbound, diagnostics, component install, updater, i18n, section.js UI
- WireGuard-over-TURN tunnel (WDTT) and TCP-over-WebRTC tunnel (OlcRTC)
  as sidecar processes with local SOCKS5, routed through sing-box outbound

Bug fixes:
- route.uc: restrict hijack-dns to local DNS inbounds only (dns-in,
  source-dns-in, dns-health-*) — fixes socket collision between sing-box
  and dnsmasq on port 53 via SO_REUSEADDR transparent write-back sockets
- runtime.uc: add Port 53 socket collision diagnostic with auto-fix
  (sing-box restart to clear parasitic UNCONN sockets with Recv-Q)
- runtime.uc: add dnsmasq dns_redirect check with auto-fix — disables
  firewall4 DNAT that causes TProxy to rewrite destination to router :53
- failover.uc: add wdtt/olcrtc to failover candidate actions
- telegram.uc: show WDTT/OlcRTC installed status in /info

Tests: 135/155 shell + 591/591 frontend
2026-09-11 18:04:03 +03:00

175 lines
7.3 KiB
Ucode

#!/usr/bin/env ucode
let common = require("core.common");
let connections = require("config.connections");
let runtime_constants = require("singbox.constants");
let option = common.option;
let bool_option = common.bool_option;
let as_string = common.as_string;
let bool_value = common.bool_value;
function normalize_port_entry(value) {
value = trim(as_string(value));
if (value == "" || match(value, /^[0-9]+$/) == null)
return null;
let number = int(value);
return number >= 1 && number <= 65535 ? number : null;
}
// P2P leak protection can be pinned down to the torrent client's own ports so
// nothing sneaks past the protocol sniffer. Each entry is "proto" or
// "proto:port" or "proto:port-port" (tcp/udp, comma separated). Without a
// port the whole protocol is routed direct.
function p2p_direct_rules(settings) {
let rules = [];
for (let entry in split(option(settings, "p2p_ports", ""), ",")) {
entry = trim(entry);
if (entry == "")
continue;
let proto = "tcp";
let port_spec = "";
let colon = index(entry, ":");
if (colon >= 0) {
proto = trim(substr(entry, 0, colon));
port_spec = trim(substr(entry, colon + 1));
} else {
proto = entry;
}
if (proto != "tcp" && proto != "udp")
continue;
let rule = { protocol: [ proto ], action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG };
if (port_spec != "") {
let dash = index(port_spec, "-");
if (dash >= 0) {
let start = normalize_port_entry(substr(port_spec, 0, dash));
let end = normalize_port_entry(substr(port_spec, dash + 1));
if (start != null && end != null && start <= end)
rule.source_port_range = [ sprintf("%d:%d", start, end) ];
else
continue;
} else {
let port = normalize_port_entry(port_spec);
if (port != null)
rule.source_port = port;
else
continue;
}
}
push(rules, rule);
}
return rules;
}
function config(settings, runtime) {
let output_network_interface = option(settings, "output_network_interface", "");
let mwan3_active = type(runtime) == "object" && bool_value(runtime.mwan3_active);
let sniff_inbounds = [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.DNS_INBOUND_TAG ];
if (common.ipv6_supported())
push(sniff_inbounds, runtime_constants.TPROXY_INBOUND6_TAG);
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
push(sniff_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG);
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
for (let inbound in runtime.dns_health_inbounds)
push(sniff_inbounds, inbound);
let dns_hijack_inbounds = [ runtime_constants.DNS_INBOUND_TAG ];
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
push(dns_hijack_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG);
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
for (let inbound in runtime.dns_health_inbounds)
push(dns_hijack_inbounds, inbound);
let rules = [
{ action: "sniff", inbound: sniff_inbounds },
{ action: "hijack-dns", inbound: dns_hijack_inbounds }
];
let result = {
rules,
rule_set: [],
final: runtime_constants.DIRECT_OUTBOUND_TAG,
auto_detect_interface: output_network_interface == "" && !mwan3_active,
default_domain_resolver: type(runtime) == "object" && as_string(runtime.default_domain_resolver) != ""
? as_string(runtime.default_domain_resolver)
: runtime_constants.DNS_SERVER_TAG,
default_mark: runtime_constants.OUTBOUND_MARK
};
if (output_network_interface != "")
result.default_interface = output_network_interface;
if (bool_option(settings, "disable_quic", false))
push(result.rules, { action: "reject", inbound: runtime_constants.TPROXY_INBOUND_TAG, protocol: "quic" });
if (bool_option(settings, "isolate_p2p", false)) {
push(result.rules, { protocol: "bittorrent", action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG });
for (let rule in p2p_direct_rules(settings))
push(result.rules, rule);
}
if (bool_option(settings, "block_doh", false)) {
if (option(settings, "dns_type", "udp") == "doh")
warn("block_doh is enabled but upstream DNS type is 'doh'; your own DNS queries would be blocked, switch dns_type to 'udp' or 'dot'\n");
let doh_cidrs = [];
for (let cidr in runtime_constants.DOH_BLOCK_IPV4_CIDRS)
push(doh_cidrs, cidr);
for (let cidr in runtime_constants.DOH_BLOCK_IPV6_CIDRS)
push(doh_cidrs, cidr);
push(result.rules, { action: "reject", inbound: runtime_constants.TPROXY_INBOUND_TAG, ip_cidr: doh_cidrs });
push(result.rules, { action: "reject", inbound: runtime_constants.TPROXY_INBOUND6_TAG, ip_cidr: doh_cidrs });
}
return result;
}
function target(section, outbound_tag_name) {
let action = option(section, "action", "");
if (connections.is_connections_action(action) ||
action == "awg" || action == "warp" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn" ||
action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc")
return { action: "route", outbound: outbound_tag_name };
if (action == "bypass")
return { action: "route", outbound: runtime_constants.BYPASS_OUTBOUND_TAG };
if (action == "block")
return { action: "reject" };
if (action == "hosts" || action == "dns")
return { action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG };
return { unsupported: "unsupported action " + action };
}
function has_resolve_matchers(rule) {
return rule.domain != null || rule.domain_suffix != null || rule.domain_keyword != null ||
rule.domain_regex != null || rule.rule_set != null;
}
function resolve_rule_for_section(section, route_rule) {
let action = option(section, "action", "");
let should_resolve = action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
(connections.is_connections_action(action) &&
bool_option(section, "resolve_real_ip_for_routing", false));
if (!should_resolve)
return null;
if (!has_resolve_matchers(route_rule)) {
return {
warning: "Resolve real IP is enabled for '" + section[".name"] + "', but no domain or rule-set matchers found"
};
}
let resolve_rule = {};
for (let key in [ "inbound", "source_ip_cidr", "domain", "domain_suffix", "domain_keyword", "domain_regex", "rule_set", "port", "port_range" ]) {
if (route_rule[key] != null)
resolve_rule[key] = route_rule[key];
}
resolve_rule.action = "resolve";
resolve_rule.server = runtime_constants.DNS_SERVER_TAG;
return { rule: resolve_rule };
}
return {
config,
target,
has_resolve_matchers,
resolve_rule_for_section
};