🌈 Sync 2026-10-01 13:09:37

This commit is contained in:
github-actions[bot] committed 2026-10-01 13:09:37 +08:00
1 parent dceb6de0e4
commit 889951b6d0
116 files changed
+6376 -1093

No files matched your search

@@ -20,7 +20,7 @@
'use strict';
import { isEmpty } from '../homeproxy-pro.uc';
import { isEmpty, isValidCIDR } from '../homeproxy-pro.uc';
/* Canonical credential multiplexing for a Node. The current generator picks
* these apart with ternaries per field (`username` vs `user` vs `password` vs
@@ -268,11 +268,47 @@ export const Node = {
problems = [...problems, `invalid port '${node.port}'`];
}
/* TLS without server_name is a sing-box-side rejection, but the
* rule itself is protocol-agnostic - the reality branch covers the
* only legitimate escape - so it stays here. */
if (node.tls.enabled === '1' && !node.tls.server_name && node.tls.reality.enabled !== '1')
problems = [...problems, 'TLS enabled without server_name'];
/* TLS without an explicit server_name.
*
* sing-box defaults an outbound's tls.server_name to the
* outbound's server address, so a node whose address is a
* *hostname* works fine without one: the handshake sends that
* hostname as SNI and verifies the certificate against it. That
* is the documented default (outbound tls.server_name defaults to
* "server address"), not a leniency we invented.
*
* This check used to fire for both address shapes, and firing
* meant die() - OutboundFactory.create() treats a non-empty
* problems list as fatal. A trojan:// / anytls:// / hysteria2://
* / tuic:// share link with no `sni=` parameter produces exactly
* such a node (each parser writes `tls_sni: params.sni` and
* nothing else), and so does the node form, whose "TLS SNI"
* field is an optional form.Value gated only on tls=1
* (homeproxy-pro.js:672). As a main node it then took the whole
* client configuration down:
*
* node '<section>': TLS enabled without server_name
*
* Reproduced on a real router (2026-10-01, sing-box 1.14.2):
* with the SNI removed from the main node, buildable() is false
* and generate() dies - so a reload rolled back to the previous
* configuration and a boot start refused to come up, over a node
* sing-box would have dialed without complaint.
*
* The case that IS worth a problem is an IP address: the implicit
* SNI is then the literal IP, which no real server presents a
* matching certificate for, so the connection cannot succeed.
* That stays fatal for a node a rule or the main node points at
* (the existing contract for an unbuildable node) and is pruned
* with a warning inside a urltest group
* (generator/outbound.uc keep_candidate). reality is the escape
* hatch: it runs its own handshake and does not use server_name.
*/
if (node.tls.enabled === '1' && !node.tls.server_name
&& node.tls.reality.enabled !== '1'
&& (isValidCIDR(node.address, 4) || isValidCIDR(node.address, 6)))
problems = [...problems,
`TLS enabled on IP address ${node.address} without server_name`];
return problems;
},
@@ -25,7 +25,7 @@
'use strict';
import { isEmpty, strToBool, strToInt, strToTime, parseURL, validation, parse_port } from '../homeproxy-pro.uc';
import { isEmpty, strToBool, strToInt, strToTime, parseURL, validation, parse_port, isValidCIDR } from '../homeproxy-pro.uc';
import { get_outbound, get_resolver, get_ruleset } from './common.uc';
@@ -72,6 +72,32 @@ function parse_dnsquery(strquery) {
return querys;
}
/* parse_dnsserver() returns null for an address it cannot make sense of, and
* both call sites used to spread that null straight into the server object.
* Spreading null is not a no-op in ucode - it throws
*
* Type error: Value (null) is not iterable
*
* so one unusable DNS address took the generator down with an exception that
* names no field, and the user only saw a rollback. Reproduced on a router
* (2026-10-01) by putting a single-label value in the China DNS field:
* parseURL() only sets `hostname` when /sbin/validate_data accepts the value
* as a hostname, and a dotless name ('cafe', 'beef', 'decade') is not one.
*
* A DNS server with no address is not something the configuration can do
* without, so this fails the same way get_resolver()/get_ruleset() do - with
* the field name and a usable example - instead of substituting a working
* resolver the user did not ask for. */
function require_dnsserver(addr, default_protocol, field) {
const server = parse_dnsserver(addr, default_protocol);
if (!server)
die(sprintf("the %s '%s' is not a usable address; use a bare IP, or a DoH/DoT URL such as https://dns.google/dns-query.\n",
field, addr));
return server;
};
/* --- proxy routing-mode path (bypass_mainland_china / proxy / / gfwlist) -- */
/* Domains whose NAPTR (qtype 35) queries bypass china-dns. Aliyun
@@ -117,8 +143,34 @@ function bootstrap_addr(ctx) {
if (isEmpty(addr))
return null;
return (match(addr, /^[0-9]+(\.[0-9]+){3}$/) != null
|| match(addr, /^[0-9a-fA-F:]+$/) != null) ? addr : null;
/* isValidCIDR(), not a hand-rolled shape test.
*
* The pair of regexes this replaces accepted anything made only of hex
* digits and colons - `cafe`, `face`, `abc`, `add`, `dead:beef` are
* all perfectly good hostnames and all passed - and the IPv4 branch did
* not range-check the octets, so 999.999.999.999 passed too. Measured
* on the device (2026-10-01): all six returned an address.
*
* The cost of being wrong here is not one misbehaving resolver.
* append_bootstrap_dns() emits the bootstrap server deliberately
* *without* a domain_resolver of its own - existing is the whole point,
* it is what every other server bootstraps its own hostname through -
* so a hostname there can never be satisfied, and sing-box refuses to
* start at all. Verified against 1.14.2 with `server: "cafe"`:
*
* FATAL create service: initialize DNS server[0]: missing domain
* resolver for domain server address
*
* isValidCIDR() is the validator the firewall path already runs every
* resource value through for the same reason (a value that is not a
* real IP or CIDR reaches a root context), and it range-checks both
* families. Reusing it keeps one definition of "is this an address".
*
* Behaviour for every value this generator is actually pointed at is
* unchanged: 223.5.5.5, 1.12.12.21, fdfe::1 and the other literals the
* option documents all still pass, and 'wan' / empty / a real hostname
* still return null so main-dns keeps borrowing the WAN resolver. */
return (isValidCIDR(addr, 4) || isValidCIDR(addr, 6)) ? addr : null;
}
/* Append the bootstrap resolver, when there is a usable address for it.
@@ -164,7 +216,7 @@ function append_proxy_dns(config, dm, ctx) {
strategy: (ctx.ipv6_support !== '1') ? 'ipv4_only' : null
},
detour: 'main-out',
...parse_dnsserver(ctx.dns_server, 'tcp')
...require_dnsserver(ctx.dns_server, 'tcp', 'DNS server')
});
/* The DNS half of the mode's default policy. It has to name the same
* side the route chain falls through to (ctx.proxy_fallback), or a
@@ -213,7 +265,7 @@ function append_proxy_dns(config, dm, ctx) {
strategy: (ctx.ipv6_support !== '1') ? 'ipv4_only' : 'prefer_ipv6'
},
detour: ctx.self_mark ? 'direct-out' : null,
...parse_dnsserver(ctx.china_dns_server)
...require_dnsserver(ctx.china_dns_server, null, 'China DNS server')
});
/* Route NAPTR (qtype 35) queries for SIP/ENUM domains to the ISP
@@ -48,6 +48,45 @@ hp_require_singbox() {
return 0
}
# hp_require_ucode
# Refuse to start when the installed ucode cannot load this package's own
# modules at all. Returns 1 (and logs) when the interpreter is unusable.
#
# Why a check and not a documented requirement: ucode resolves `import` when a
# module is LOADED, not when a function is called, and homeproxy-pro.uc imports
#
# import { access, lstat, mkdtemp, open, rmdir, unlink } from 'fs';
#
# so on a ucode whose fs module has no `mkdtemp` export, *every* entry point
# into this package dies with the same one-line reference error at load time -
# the install-time migration (etc/uci-defaults/luci-homeproxy-pro-migration),
# both config generators, and the LuCI RPC module that imports homeproxy-pro.uc
# too. The first of those to run is the migration, which is why the only
# thing an affected user ever saw was a stack trace during `apk add` /
# `opkg install` (issue #3) and never a sentence naming the requirement.
#
# `mkdtemp` entered ucode on 2025-11-07 (lib/fs.c, "fs: add mkdtemp() method
# for creating temporary directories"). It replaced `mkstemp`, which every
# older ucode does export - so this is a hard floor, not a preference, and no
# ImmortalWrt 24.10 release can satisfy it.
#
# The probe is `ucode -e`, the same form tests/ucode/run.sh uses for its
# module syntax checks. Its own output is not discarded: if this ucode is so
# old that even the probe fails, that is exactly the information the operator
# needs, so the error goes into the log next to the requirement.
hp_require_ucode() {
local probe_out
probe_out="$(ucode -e "import * as fs from 'fs'; exit('mkdtemp' in keys(fs) ? 0 : 1)" 2>&1)" && return 0
log "Error: this build needs a ucode whose fs module exports mkdtemp (added in ucode 2025-11-07, shipped in ImmortalWrt 25.12); the installed ucode does not provide it."
log "Error: every script in this package imports it, so the service, the config generators and the LuCI RPC all fail to load on this firmware."
[ -n "$probe_out" ] && log "Error: the capability check itself reported: ${probe_out}"
log "Hint: this firmware's ucode is too old for this package; ImmortalWrt 25.12 or newer is required."
return 1
}
# hp_crontab_drop <crontab>
# Remove the auto-update entry from <crontab>.
#