Files
op-packages/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/context.uc
T

259 lines
13 KiB
Ucode

/*
* SPDX-License-Identifier: GPL-2.0-only
*
* GenerationContext: the routing-mode/proxy-mode scalars every section
* builder consumes, built as a *pure* function of the domain model plus a
* small, explicit environment record.
*
* build_context(dm, env) -> ctx
*
* env = {
* wan_dns, the WAN resolver the CLI resolved via ubus
* direct_domain_list, /etc/homeproxy-pro/resources/direct_list.txt
* proxy_domain_list, /etc/homeproxy-pro/resources/proxy_list.txt
* }
*
* Why the split exists: this module used to sit inside generator/client.uc
* and resolve `env` itself - it called `ubus network.interface status` for
* the WAN DNS and `readfile()` for the two resource lists. The generator was
* documented as a pure function while depending on live router state, so
* "generate the same config twice" was not guaranteed. That is what made
* reload's preflight generation and start_service's generation two different
* artifacts in principle: the WAN lease can change between them.
*
* Now the impure part (ubus call, resource read, mode-dependent fallback for
* a router with no lease) belongs to the CLI shells - scripts/
* generate_client.uc and scripts/generate_server.uc - which are the
* process-level boundary and already own the atomic write and
* `sing-box check`. Everything under generator/ is a pure function of its
* arguments, which tests/arch-guard.sh enforces (guard 27: no ubus/fs import
* in generator/).
*
* `env` is allowed to be partial. A missing wan_dns still yields the same
* public fallback the pre-split code used, as a pure function of the routing
* mode - not a probe - so a caller that has no ubus keeps producing a valid
* config instead of failing.
*/
'use strict';
import { isEmpty, strToInt } from '../homeproxy-pro.uc';
/* Build the routing-mode-independent ctx the section builders consume.
* Mode-dependent defaults are filled in here so each builder can stay a
* straight consumer of its fields without recomputing them. */
export function build_context(dm, env) {
const routing_mode = dm.general.routing_mode || 'bypass_mainland_china';
const proxy_mode = dm.general.proxy_mode || 'redirect_tproxy';
/* Mode-shared scalars */
const ipv6_support = dm.general.ipv6_support || '0';
const log_level = dm.general.log_level || 'warn';
const ntp_server = dm.infra.ntp_server || 'time.apple.com';
const dns_port = dm.infra.dns_port || '5333';
const mixed_port = dm.infra.mixed_port || '5330';
const udp_timeout = (routing_mode === 'custom')
? (dm.routing.settings.udp_timeout)
: dm.infra.udp_timeout;
/* self_mark is intentionally null for proxy_mode 'tun' only:
* the DNS server detour (default-dns / system-dns / china-dns) is
* gated on it, and the old generator kept the same code shape -
* any non-empty string is truthy, so the test fixture's
* self_mark='100' would leak a 'detour: direct-out' onto every
* DNS server when proxy_mode is tun. removeBlankAttrs() then
* strips the null detour, so the generated JSON stays identical
* to the pre-refactor output. */
const self_mark = match(proxy_mode, /redirect/)
? (dm.infra.self_mark || '100') : null;
/* proxy-mode-derived ports / TUN params */
let redirect_port, tproxy_port, tun_name, tun_addr4, tun_addr6,
tun_mtu, tcpip_stack, endpoint_independent_nat;
if (match(proxy_mode, /redirect/))
redirect_port = dm.infra.redirect_port || '5331';
if (match(proxy_mode, /tproxy/))
if ((dm.general.main_udp_node || 'nil') !== 'nil' || routing_mode === 'custom')
tproxy_port = dm.infra.tproxy_port || '5332';
if (match(proxy_mode, /tun/)) {
tun_name = dm.infra.tun_name || 'singtun0';
tun_addr4 = dm.infra.tun_addr4 || '172.19.0.1/30';
tun_addr6 = dm.infra.tun_addr6 || 'fdfe:dcba:9876::1/126';
tun_mtu = dm.infra.tun_mtu || '9000';
tcpip_stack = 'system';
if (routing_mode === 'custom') {
tcpip_stack = dm.routing.settings.tcpip_stack || 'system';
endpoint_independent_nat = dm.routing.settings.endpoint_independent_nat;
}
}
/* The WAN resolver is an input, not a lookup. See the module header for
* the fallback's provenance - it is the same mode-dependent public
* resolver the pre-split generator chose when ubus had nothing to say. */
const wan_dns = !isEmpty(env.wan_dns)
? env.wan_dns
: ((routing_mode in ['proxy_mainland_china', 'global']) ? '8.8.8.8' : '223.5.5.5');
/* 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;
const dns_query_timeout = (dm.dns.settings || {}).dns_timeout;
const dns_store_dns = (dm.dns.settings || {}).cache_file_store_dns || '0';
const tun_dns_mode_raw = dm.general.tun_dns_mode,
tun_dns_address = dm.general.tun_dns_address,
udp_mapping_raw = dm.general.udp_mapping,
udp_filtering_raw = dm.general.udp_filtering,
udp_nat_max = strToInt(dm.general.udp_nat_max);
const tun_dns_mode = (tun_dns_mode_raw === 'default') ? '' : tun_dns_mode_raw,
udp_mapping = (udp_mapping_raw === 'default') ? '' : udp_mapping_raw,
udp_filtering = (udp_filtering_raw === 'default') ? '' : udp_filtering_raw;
/* Where a destination the preset lists do not mention goes. This is the
* mode's whole default policy, so it is declared once here and read by
* both consumers - the route rule chain and the DNS rule chain:
*
* bypass_mainland_china mainland -> direct, everything else -> proxy
* global everything -> proxy
* gfwlist only the list -> proxy, everything else direct
* proxy_mainland_china mainland -> proxy, everything else -> direct
*
* The two chains have to agree. They used to say it independently (a
* literal `main-dns` in dns.uc, a literal `main-out` in route.uc) with
* nothing tying them together, and the route side applied "unknown goes
* to the proxy" to all four modes - so "Only proxy mainland China" sent
* every unlisted destination through the proxy, which is the opposite of
* what the mode is named. Both now derive from this one value. */
const proxy_fallback = (routing_mode !== 'proxy_mainland_china');
/* Routing-mode-dependent defaults */
const ctx = {
routing_mode, proxy_mode, ipv6_support, log_level,
proxy_fallback,
self_mark, ntp_server, dns_port, mixed_port,
redirect_port, tproxy_port,
tun_name, tun_addr4, tun_addr6, tun_mtu,
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,
dns_query_timeout, dns_store_dns,
default_interface: (dm.access_control.control || {}).bind_interface,
/* udp/tcp shaping knobs the endpoint builder reads */
endpoint_options: {
udp_mapping, udp_filtering, udp_nat_max
},
/* Routing-mode-specific scalars; the dns/route modules consult
* only the fields their branch needs.
*
* The main-node reference is mode-dependent, and custom routing has
* its own outbound/rule tables (routing.default_outbound plus the
* routing_node / routing_rule / ruleset sections). A residual
* config.main_node must therefore not switch the generator back to
* the preset path: LuCI hides that field in custom mode but does not
* clear it (`rmempty = false`), so a router that was configured in a
* preset mode and then switched to custom keeps the old value in
* UCI - and the pre-refactor code read it only when the mode was not
* custom. Reading it here anyway silently dropped every custom
* routing rule and made route.final 'main-out'. */
main_node: (routing_mode === 'custom') ? null : dm.general.main_node,
main_udp_node: (routing_mode === 'custom') ? null : dm.general.main_udp_node,
/* Custom-only scalars. The gate is not cosmetic: route.uc picks its
* branch with `isEmpty(default_outbound)` and outbound.uc does the
* same, while the preset branch keys off main_node. A residual
* default_outbound from an earlier custom configuration therefore
* switched a preset-mode router onto the custom branch with the
* preset DNS builder bailing out (it keys off main_node) - the DNS
* block then ended up with only default-dns/system-dns and no
* main-dns/china-dns, the route block with leftover custom rules -
* and the rule-set tags those rules reference are only built in
* custom mode, so `sing-box check` rejected the whole generation.
* Preset modes read custom-only fields nowhere; gate them all.
*
* The proxy/custom asymmetry the generator then exposes to users:
* proxy mode's route.default_domain_resolver is the literal
* 'default-dns' (route.uc line 78-81), while custom mode's is the
* get_resolver(default_outbound_dns, dm) lookup (route.uc line
* 217-220). The two are deliberately different: proxy mode does
* not surface a user-facing "default outbound DNS" choice (the
* preset path always resolves through the WAN DNS), while custom
* mode lets the user pick. README does not promise otherwise. */
default_outbound: (routing_mode === 'custom') ? (dm.routing.settings || {}).default_outbound : null,
default_outbound_dns: (routing_mode === 'custom')
? ((dm.routing.settings || {}).default_outbound_dns || 'default-dns') : null,
domain_strategy: (routing_mode === 'custom') ? (dm.routing.settings || {}).domain_strategy : null,
find_neighbor: (routing_mode === 'custom') ? (dm.routing.settings || {}).find_neighbor : null,
/* Main-line defaults, mirroring what the pre-refactor
* generate_client.uc did inline: an empty UCI value becomes 'wan'
* for the main DNS server, and 'wan' - the value the UI writes for
* "WAN DNS (read from interface)" - is replaced by the resolver the
* CLI read off the WAN interface (`wan_dns`, which itself falls back
* to a public resolver when ubus has no answer). Leaving the
* literal 'wan' in place makes sing-box resolve a host by that name.
* The China DNS server falls back to the Aliyun public resolver
* instead of leaking into the default-dns pipeline. */
dns_server: (isEmpty(dm.general.dns_server) || dm.general.dns_server === 'wan')
? wan_dns : dm.general.dns_server,
china_dns_server: (dm.general.china_dns_server && dm.general.china_dns_server !== 'wan')
? dm.general.china_dns_server : '223.5.5.5',
/* dns_default_strategy: in proxy mode, ipv4_only is the safe
* default whenever ipv6_support is off; in custom mode the
* UCI default_strategy field is the source of truth (no
* implicit fallback - missing UCI => strategy: null =>
* removeBlankAttrs strips it). */
dns_default_strategy: (routing_mode === 'custom')
? (dm.dns.settings || {}).default_strategy
: ((ipv6_support !== '1') ? 'ipv4_only' : null),
/* cn_ip_fallback: evaluate/match_response fallback for unknown-host
* queries (tutorial §2.1 / docs/linux.json 与 pro 的差距分析.md).
* The v28.9.1.16 flip turns this on by default for fresh installs;
* migrate_config.uc writes '0' explicitly for upgrade-existing users
* so they keep the prior behaviour until they opt in. '0' is truthy
* in JS so the || picks it over '1' when UCI sets it to '0' - a
* user who has cn_ip_fallback='0' written (the upgrade path) gets
* exactly what they had. */
cn_ip_fallback: dm.general.cn_ip_fallback || '1',
/* sniffer_advanced_mode: when '1' (opt-in), the route sniff rule
* gets the universal protocol list + 100ms timeout (tutorial §2.7 /
* docs/linux.json 与 pro 的差距分析.md). Default '0' preserves the
* 300ms / default-list behaviour so an upgrade is invisible. */
sniffer_advanced_mode: dm.general.sniffer_advanced_mode || '0',
main_urltest_nodes: dm.general.main_urltest_nodes || [],
main_urltest_interval: dm.general.main_urltest_interval,
main_urltest_tolerance: dm.general.main_urltest_tolerance,
main_udp_urltest_nodes: dm.general.main_udp_urltest_nodes || [],
main_udp_urltest_interval: dm.general.main_udp_urltest_interval,
main_udp_urltest_tolerance: dm.general.main_udp_urltest_tolerance,
/* Custom-mode DNS settings */
dns_default_server: (dm.dns.settings || {}).default_server,
dns_disable_cache: (dm.dns.settings || {}).disable_cache,
dns_disable_cache_expire: (dm.dns.settings || {}).disable_cache_expire,
dns_client_subnet: (dm.dns.settings || {}).client_subnet,
};
/* Inline static rule-sets (direct-domain, proxy-domain) only fire
* when the routing mode has a non-empty domain list. Both lists are
* inputs now: the CLI reads the two files under
* /etc/homeproxy-pro/resources/ and passes them in. Custom mode ignores
* them entirely, exactly as the pre-split read did. */
ctx.direct_domain_list = [];
ctx.proxy_domain_list = [];
if (routing_mode !== 'custom') {
ctx.direct_domain_list = env.direct_domain_list || [];
ctx.proxy_domain_list = env.proxy_domain_list || [];
}
/* `dedicated_udp_node` means main_udp_node was set to a different
* proxy-able node than main_node (the 'same'/'nil' sentinels don't
* qualify). Kept on ctx so both build_outbounds and build_route
* see the same value. */
ctx.dedicated_udp_node = !isEmpty(ctx.main_udp_node) && !(ctx.main_udp_node in ['same', ctx.main_node]);
return ctx;
};