mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 21:02:52 +08:00
259 lines
13 KiB
Ucode
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;
|
|
};
|