💐 Sync 2026-09-30 01:19:34

This commit is contained in:
github-actions[bot] committed 2026-09-30 01:19:34 +08:00
1 parent 67427d9030
commit a64d55f7c5
50 files changed
+2837 -589

No files matched your search

@@ -385,6 +385,7 @@ export const Loader = {
/* A2.1: scalar + list fields on the `config` section. */
dns_server: opt(uci, 'main', 'dns_server'),
china_dns_server: opt(uci, 'main', 'china_dns_server'),
bootstrap_dns: opt(uci, 'main', 'bootstrap_dns') || [],
log_level: opt(uci, 'main', 'log_level') || 'warn',
tun_dns_mode: opt(uci, 'main', 'tun_dns_mode'),
tun_dns_address: opt(uci, 'main', 'tun_dns_address'),
@@ -94,6 +94,26 @@ export function build_context(dm, env) {
? env.wan_dns
: ((routing_mode in ['proxy_mainland_china', 'global']) ? '8.8.8.8' : '223.5.5.5');
/* Bootstrap resolvers: the servers the proxy path uses to look up a DNS
* server's *own* hostname. `main-dns` is the user's DoH/DoT endpoint
* and the only server whose address may be a name, so this lookup has to
* work before any configured resolver can answer.
*
* It used to be borrowed from default-dns (the WAN/ISP resolver), which
* is the one resolver a polluted or unreachable WAN takes down; the
* option lets the user name a resolver that still answers. Empty means
* "keep using the WAN resolver", so an untouched configuration
* generates exactly the same bytes as before. */
const bootstrap_dns = [];
if (type(dm.general.bootstrap_dns) === 'array') {
for (let server in dm.general.bootstrap_dns)
if (!isEmpty(server))
push(bootstrap_dns, server);
} else if (!isEmpty(dm.general.bootstrap_dns)) {
/* UCI hands back a bare string for a list with a single value. */
push(bootstrap_dns, dm.general.bootstrap_dns);
}
/* dns/routing fields used by both sections */
const dns_optimistic_cache = (dm.dns.settings || {}).optimistic_cache || '0';
const dns_optimistic_timeout = (dm.dns.settings || {}).optimistic_timeout;
@@ -119,7 +139,7 @@ export function build_context(dm, env) {
tcpip_stack, endpoint_independent_nat,
udp_timeout, udp_mapping, udp_filtering, udp_nat_max,
tun_dns_mode, tun_dns_address,
wan_dns, dns_optimistic_cache, dns_optimistic_timeout,
wan_dns, bootstrap_dns, dns_optimistic_cache, dns_optimistic_timeout,
dns_query_timeout, dns_store_dns,
default_interface: (dm.access_control.control || {}).bind_interface,
@@ -96,11 +96,17 @@ function append_proxy_dns(config, dm, ctx) {
if (isEmpty(ctx.main_node))
return;
/* The user's DoH/DoT endpoint is the one server in this block whose
* address is a hostname, so it is the one that needs the bootstrap
* resolver; without a configured one it keeps resolving through
* default-dns (the WAN resolver), exactly as before. */
const main_resolver = isEmpty(ctx.bootstrap_dns) ? 'default-dns' : 'bootstrap-dns';
/* Main DNS */
push(config.dns.servers, {
tag: 'main-dns',
domain_resolver: {
server: 'default-dns',
server: main_resolver,
strategy: (ctx.ipv6_support !== '1') ? 'ipv4_only' : null
},
detour: 'main-out',
@@ -115,6 +121,21 @@ function append_proxy_dns(config, dm, ctx) {
server: (ctx.routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns'
});
/* Everything on the proxy list resolves through the proxy path, in
* every proxy mode - not just bypass_mainland_china. The list is the
* user's "this domain must not be answered by the domestic resolver"
* escape hatch (Google Play's connect.googleapis.cn and friends), and
* the routing half of it (route.uc pushes the same tag to main-out in
* all of these modes) was already mode-agnostic. It has to sit after
* direct-domain so that a suffix present in both lists keeps the
* bypass-mode precedence and stays predictable. */
if (length(ctx.proxy_domain_list))
push(config.dns.rules, {
rule_set: 'proxy-domain',
action: 'route',
server: 'main-dns'
});
/* Reject SVCB/HTTPS queries to avoid proxy DNS timeout on null domains */
push(config.dns.rules, {
query_type: [64, 65],
@@ -147,13 +168,6 @@ function append_proxy_dns(config, dm, ctx) {
server: 'default-dns'
});
if (length(ctx.proxy_domain_list))
push(config.dns.rules, {
rule_set: 'proxy-domain',
action: 'route',
server: 'main-dns'
});
push(config.dns.rules, {
rule_set: 'geosite-cn',
action: 'route',
@@ -466,6 +480,39 @@ function initDns(config, ctx) {
};
}
/* Append the bootstrap resolver, if the user configured one.
*
* Every DNS server whose address is a *hostname* needs a domain_resolver
* before sing-box will start it, and that lookup is the one step nothing
* else can bootstrap: it has to work before any of the configured servers
* can answer. `main-dns` is exactly that case - it is the user's DoH/DoT
* endpoint - and it used to borrow default-dns (the WAN/ISP resolver) for
* it, which is the single resolver a polluted or unreachable WAN takes down.
*
* The bootstrap server carries a plain IP, has no domain_resolver of its own
* and is never referenced by a DNS rule: it exists only as the target of the
* domain_resolver pointer, so there is no cycle for sing-box to reject.
* Nothing is emitted when the list is empty ("resolve through the WAN
* resolver"), which keeps an untouched configuration byte-identical.
*
* Only the first entry is used. sing-box resolves a hostname through
* exactly one transport (`DomainResolveOptions.Server` is a single tag), and
* registering extra servers would not make them a fallback chain - it would
* only leave unused servers in the config. The list form is kept because
* the option is a DynamicList in the UI and a value with several entries
* must not produce an unparseable address. */
function append_bootstrap_dns(config, ctx) {
if (isEmpty(ctx.bootstrap_dns))
return;
push(config.dns.servers, {
tag: 'bootstrap-dns',
type: 'udp',
server: ctx.bootstrap_dns[0],
detour: ctx.self_mark ? 'direct-out' : null
});
}
/* Build the sing-box `dns` block. Mutates `config` and returns it for
* chaining. Routing mode is determined by ctx.routing_mode: any non-
* 'custom' mode follows the proxy path (the proxy-mode split inside the
@@ -473,10 +520,14 @@ function initDns(config, ctx) {
export function build_dns(config, dm, ctx) {
initDns(config, ctx);
/* Custom mode has no main-dns and never resolves its own servers'
* hostnames, so it has no bootstrap problem to solve. */
if (ctx.routing_mode === 'custom')
append_custom_dns(config, dm, ctx);
else
else {
append_bootstrap_dns(config, ctx);
append_proxy_dns(config, dm, ctx);
}
return config;
};