💐 Sync 2026-10-01 01:11:54

This commit is contained in:
github-actions[bot] committed 2026-10-01 01:11:54 +08:00
1 parent 8f2ec5c4da
commit 2909cae209
44 files changed
+1092 -2287

No files matched your search

@@ -72,50 +72,6 @@ function parse_dnsquery(strquery) {
return querys;
}
/* Build the sing-box `headers` object from the UCI string list.
*
* sing-box types this field as `badoption.HTTPHeader`, i.e.
* map[string][]string; an array (or a bare string) fails to decode and the
* whole configuration is rejected. The LuCI form is a DynamicList, so every
* entry lands in UCI as a string and the two sides disagreed on the shape:
* `"headers": ["Name: value"]` is not a map.
*
* The form keeps its list widget and one entry is written `Name: Value`;
* repeating a name appends to that name's value list, which is how HTTP
* expresses a multi-valued header. An entry that does not fit the form is
* named and rejected here rather than left for sing-box to report as
* "cannot unmarshal array into Go value of type badoption.HTTPHeader". */
function parse_http_headers(list, where) {
if (isEmpty(list))
return null;
/* A single hand-written UCI option arrives as a string, not a list. */
if (type(list) !== 'array')
list = [ list ];
const headers = {};
for (let item in list) {
const line = trim(item);
if (line === '')
continue;
const sep = index(line, ':');
if (sep <= 0)
die(sprintf("homeproxy-pro: %s: header '%s' is not in 'Name: Value' form.", where, line));
const name = trim(substr(line, 0, sep));
const value = trim(substr(line, sep + 1));
if (isEmpty(name) || isEmpty(value))
die(sprintf("homeproxy-pro: %s: header '%s' is not in 'Name: Value' form.", where, line));
if (!headers[name])
headers[name] = [];
push(headers[name], value);
}
return isEmpty(keys(headers)) ? null : headers;
}
/* --- proxy routing-mode path (bypass_mainland_china / proxy / / gfwlist) -- */
/* Domains whose NAPTR (qtype 35) queries bypass china-dns. Aliyun
@@ -131,6 +87,7 @@ function parse_http_headers(list, where) {
* someone reads the comment and thinks "I know a domain that should
* go direct". */
const NAPTR_BYPASS_SUFFIXES = [
'r.10086.cn',
'10086.cn',
'pub.3gppnetwork.org'
];
@@ -237,16 +194,7 @@ function append_proxy_dns(config, dm, ctx) {
server: 'main-dns'
});
/* Reject SVCB/HTTPS queries: a client that learns the address from these
* record types dials the embedded IP without asking again, which is what
* lets a query slip past the DNS split (and, in the custom path, past
* Fake-IP).
*
* It sits after the two domain lists on purpose, so a domain the user put
* on a list keeps whatever resolver that list chose - the custom path
* below puts the same reject first instead, because there the user's own
* rules are the ones that must not be shadowed. The two orders are
* deliberate; do not "fix" one into the other. */
/* Reject SVCB/HTTPS queries to avoid proxy DNS timeout on null domains */
push(config.dns.rules, {
query_type: [64, 65],
action: 'reject'
@@ -369,7 +317,7 @@ function append_custom_dns(config, dm, ctx) {
server: cfg.server,
server_port: strToInt(cfg.server_port),
path: cfg.path,
headers: parse_http_headers(cfg.headers, sprintf("dns server '%s'", cfg.name)),
headers: cfg.headers,
tls: cfg.tls_sni ? {
enabled: true,
server_name: cfg.tls_sni
@@ -437,71 +385,24 @@ function append_custom_dns(config, dm, ctx) {
rule_set: get_ruleset(cfg.rule_set, dm),
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
invert: strToBool(cfg.invert),
race: strToBool(cfg.race),
speculative: strToBool(cfg.speculative),
action: cfg.action
};
/* race/speculative are action fields, not match fields, and their
* legality is decided at load time by sing-box's
* validateDNSRuleAction() (route/rule/rule_dns.go):
*
* - race and speculative are mutually exclusive ("`race` and
* `speculative` cannot be combined on the same rule");
* - race is only allowed on a final action - route, respond,
* reject, predefined - and "`race` requires a final action"
* otherwise;
* - speculative exists only in the route and evaluate option
* structs, so on any other action it is an unknown field and
* DisallowUnknownFields rejects the whole configuration.
*
* The old code emitted both unconditionally. The form hides them
* with depends(), but LuCI does not delete the value of a hidden
* option, so "tick race on a route rule, then switch the rule to
* evaluate" left an illegal value behind. Gate by action and die
* on the combination, naming the field rather than letting sing-box
* answer with "unknown field". */
const want_race = strToBool(cfg.race) === true,
want_speculative = strToBool(cfg.speculative) === true;
if (want_race && want_speculative)
die(sprintf("homeproxy-pro: DNS rule '%s' enables both race and speculative; sing-box refuses that combination.", cfg.name));
if (want_race) {
if (!(cfg.action in ['route', 'reject', 'predefined']))
die(sprintf("homeproxy-pro: DNS rule '%s' uses race with action '%s'; race is only allowed on a final action (route, reject, predefined).", cfg.name, cfg.action));
rule.race = true;
}
if (want_speculative) {
if (!(cfg.action in ['route', 'evaluate']))
die(sprintf("homeproxy-pro: DNS rule '%s' uses speculative with action '%s'; speculative is only valid for route and evaluate.", cfg.name, cfg.action));
rule.speculative = true;
}
/* `route` and `evaluate` (which needs a server for the eval step).
* DNS rules have no `resolve` action - that one belongs to the route
* rules - and the form does not offer it here. It used to be handled
* anyway, which would have emitted an action sing-box does not know
* the moment a stale UCI value carried it. */
if (cfg.action === 'route' || cfg.action === 'evaluate') {
/* `route` (and `evaluate`, which needs a server for the eval step).
* `resolve` is also a possibility but is not currently exposed in
* the UI; if it ever is, this branch is where it lands. */
if (cfg.action === 'route' || cfg.action === 'evaluate' || cfg.action === 'resolve') {
rule.server = get_resolver(cfg.server, dm);
rule.disable_cache = strToBool(cfg.dns_disable_cache);
rule.disable_optimistic_cache = strToBool(cfg.disable_optimistic_cache);
rule.rewrite_ttl = strToInt(cfg.rewrite_ttl);
/* Load-time mutual exclusion in validateDNSRuleAction():
* "`client_subnet` and `remove_client_subnet` are mutually
* exclusive". The form draws them as two independent
* controls, so both can be left set. */
if (!isEmpty(cfg.client_subnet) && strToBool(cfg.remove_client_subnet) === true)
die(sprintf("homeproxy-pro: DNS rule '%s' sets both client_subnet and remove_client_subnet; sing-box refuses that combination.", cfg.name));
rule.client_subnet = cfg.client_subnet;
rule.remove_client_subnet = strToBool(cfg.remove_client_subnet);
}
/* 1.14's evaluate action carries the same timeout override as route
* (DNSEvaluateActionOptions embeds AbstractDNSRouteActionOptions), and
* the form shows the field for both actions. Emitting it for route
* only made the value disappear silently on an evaluate rule. */
if (cfg.action === 'route' || cfg.action === 'evaluate')
if (cfg.action === 'route' || cfg.action === 'resolve')
rule.timeout = strToTime(cfg.dns_timeout);
/* `reject` only accepts method + no_drop. Anything else here is
@@ -575,13 +476,6 @@ function append_custom_dns(config, dm, ctx) {
rule.response_extra = cfg.response_extra;
}
/* race also requires match_response in 1.14. This check has to sit
* after every match_response assignment: the legacy address-filter
* wrapping above gives one to those rules too, and that satisfies
* sing-box just as well. */
if (want_race && !rule.match_response)
die(sprintf("homeproxy-pro: DNS rule '%s' enables race without match_response; sing-box requires one.", cfg.name));
push(builtin_dns_rules, rule);
}
config.dns.rules = builtin_dns_rules;
@@ -589,31 +483,6 @@ function append_custom_dns(config, dm, ctx) {
config.dns.final = get_resolver(ctx.dns_default_server, dm);
}
/* Reject AAAA when this router does not proxy IPv6.
*
* Every IPv6 rule-set and rule in firewall_post.ut sits inside
* `ipv6_support === '1'`, so with the switch off the router does not
* intercept v6 at all - and nothing in this DNS block ever refused a AAAA
* query. dns.strategy=ipv4_only on the preset paths is a resolution
* preference, not a query block, and custom routing takes the UCI default
* (prefer_ipv4). A dual-stack LAN therefore still received AAAA records and
* connected straight out over the WAN, bypassing both the proxied resolver
* and the destination-based split.
*
* Appended last so a user rule keeps precedence over it. A client with its
* own DoH/DoT can still bypass this layer; a hard block would have to be an
* nft rule, which changes the user-visible "IPv6 still works" behaviour of
* the switch and is therefore not done here. */
function append_ipv6_guard(config, ctx) {
if (ctx.ipv6_support === '1')
return;
push(config.dns.rules, {
query_type: [28],
action: 'reject'
});
}
/* --- public entry ------------------------------------------------------ */
/* The always-on DNS block. Every routing mode (proxy and custom) gets
@@ -685,7 +554,5 @@ export function build_dns(config, dm, ctx) {
append_proxy_dns(config, dm, ctx);
}
append_ipv6_guard(config, ctx);
return config;
};