mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 12:44:06 +08:00
✨ Sync 2026-10-11 20:48:25
This commit is contained in:
1 parent
78f0ba8251
commit
5ce75c9f2b
45 files changed
+54
-879
No files matched your search
@@ -21,8 +21,6 @@ if (match(proxy_mode, /tun/)) {
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
|
||||
if (outbound_node !== 'nil') {
|
||||
/* the name is interpolated into an nftables rule: keep it to what an
|
||||
interface name can actually be */
|
||||
const raw_tun = trim(uci.get(cfgname, 'infra', 'tun_name') ?? '');
|
||||
tun_name = match(raw_tun, /^[A-Za-z0-9._-]{1,15}$/) ? raw_tun : 'singtun0';
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2025 ImmortalWrt.org
|
||||
#
|
||||
# Single source of truth for every fw4 chain and set owned by homeproxy.
|
||||
#
|
||||
# * root/etc/homeproxy/scripts/firewall_post.ut declares these objects and
|
||||
# decides which of them actually exist for the current proxy mode.
|
||||
# * root/etc/init.d/homeproxy flushes and deletes them on stop.
|
||||
#
|
||||
# When you add or remove a chain/set in firewall_post.ut, update this list in
|
||||
# the same commit -- tests/ucode/test_fw4_names.sh compares both sides and
|
||||
# fails when they drift.
|
||||
#
|
||||
# Each object is cleaned up independently: the cleanup loop deletes one at a
|
||||
# time so that a chain/set which does not exist in the current mode cannot
|
||||
# abort the whole batch. A single `nft -f` batch is atomic and would roll back
|
||||
# every preceding command in that case.
|
||||
|
||||
HP_FW4_CHAINS="
|
||||
homeproxy_output_redir
|
||||
|
||||
@@ -20,9 +20,7 @@ import {
|
||||
|
||||
const ubus = connect();
|
||||
|
||||
/* const features = ubus.call('luci.homeproxy', 'singbox_get_features') || {}; */
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
@@ -45,17 +43,6 @@ const uciruleset = 'ruleset';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
||||
|
||||
/*
|
||||
* Single source of truth for "which config branch are we generating".
|
||||
*
|
||||
* The three generation chains below (DNS, outbounds, route) must be selected by
|
||||
* the routing mode, NOT by whether main_node / default_outbound happen to be
|
||||
* set. Those two answer a different question: in custom mode default_outbound
|
||||
* is legitimately 'nil' (every routing rule picks its own outbound, there is no
|
||||
* default) and main_node is never read. Deriving the mode from them made a
|
||||
* perfectly normal custom config fall through every branch and emit a config
|
||||
* containing none of the user's DNS servers, nodes, rules or rule-sets.
|
||||
*/
|
||||
const is_custom_mode = (routing_mode === 'custom');
|
||||
|
||||
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
|
||||
@@ -96,14 +83,12 @@ if (routing_mode !== 'custom') {
|
||||
proxy_domain_list = proxy_list_raw ? split(trim(proxy_list_raw), /[\r\n]/) : [];
|
||||
|
||||
} else {
|
||||
/* DNS settings */
|
||||
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
|
||||
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
|
||||
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
|
||||
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
|
||||
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
|
||||
|
||||
/* Routing settings */
|
||||
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
||||
default_outbound_dns = uci.get(uciconfig, uciroutingsetting, 'default_outbound_dns') || 'default-dns';
|
||||
domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy');
|
||||
@@ -148,7 +133,6 @@ if (match(proxy_mode, /tun/)) {
|
||||
}
|
||||
|
||||
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
|
||||
/* UCI config end */
|
||||
|
||||
const tun_dns_mode_raw = uci.get(uciconfig, ucimain, 'tun_dns_mode'),
|
||||
tun_dns_address = uci.get(uciconfig, ucimain, 'tun_dns_address'),
|
||||
@@ -160,13 +144,6 @@ 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;
|
||||
|
||||
/* Config helper start */
|
||||
/*
|
||||
* Direct-node destination override, keyed by node section name. It must be
|
||||
* declared before the helpers that touch it: ucode resolves let/const
|
||||
* lexically and does not hoist them, so a function defined earlier would
|
||||
* resolve the name as an undeclared global and throw under strict mode.
|
||||
*/
|
||||
const direct_overrides = {};
|
||||
|
||||
function parse_port(strport) {
|
||||
@@ -256,28 +233,22 @@ function generate_outbound(node) {
|
||||
|
||||
server: node.address,
|
||||
server_port: strToInt(node.port),
|
||||
/* Hysteria(2) */
|
||||
server_ports: node.hysteria_hopping_port,
|
||||
|
||||
username: (node.type !== 'ssh') ? node.username : null,
|
||||
user: (node.type === 'ssh') ? node.username : null,
|
||||
/* Snell authenticates with psk instead of password */
|
||||
password: (node.type !== 'snell') ? node.password : null,
|
||||
psk: (node.type === 'snell') ? node.password : null,
|
||||
userkey: (node.type === 'snell') ? node.snell_userkey : null,
|
||||
reuse: (node.type === 'snell') ? strToBool(node.snell_reuse) : null,
|
||||
/* Snell v4: HTTP obfuscation; v6: traffic shaping mode */
|
||||
obfs_mode: (node.type === 'snell') ? (node.snell_obfs_mode || null) : null,
|
||||
obfs_host: (node.type === 'snell') ? (node.snell_obfs_host || null) : null,
|
||||
mode: (node.type === 'snell') ? (node.snell_mode || null) : null,
|
||||
|
||||
/* Direct */
|
||||
proxy_protocol: strToInt(node.proxy_protocol),
|
||||
/* AnyTLS */
|
||||
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
|
||||
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
|
||||
min_idle_session: strToInt(node.anytls_min_idle_session),
|
||||
/* Hysteria (2) */
|
||||
hop_interval: strToTime(node.hysteria_hop_interval),
|
||||
hop_interval_max: strToTime(node.hysteria_hop_interval_max),
|
||||
up_mbps: strToInt(node.hysteria_up_mbps),
|
||||
@@ -290,29 +261,23 @@ function generate_outbound(node) {
|
||||
} : node.hysteria_obfs_password,
|
||||
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
|
||||
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
|
||||
/* sing-box 1.14: Hysteria2 QUIC params (Hysteria v1 recv-window tuning removed upstream) */
|
||||
bbr_profile: (node.type === 'hysteria2') ? (node.hysteria_bbr_profile || null) : null,
|
||||
disable_chrome_parrot: (node.type === 'hysteria2' && node.hysteria_disable_chrome_parrot === '1') ? true : null,
|
||||
/* Shadowsocks */
|
||||
method: node.shadowsocks_encrypt_method,
|
||||
plugin: node.shadowsocks_plugin,
|
||||
plugin_opts: node.shadowsocks_plugin_opts,
|
||||
/* ShadowTLS / Socks / Snell */
|
||||
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : ((node.type === 'snell') ? (strToInt(node.snell_version) || 4) : null)),
|
||||
/* SSH */
|
||||
client_version: node.ssh_client_version,
|
||||
host_key: node.ssh_host_key,
|
||||
host_key_algorithms: node.ssh_host_key_algo,
|
||||
private_key: node.ssh_priv_key,
|
||||
private_key_passphrase: node.ssh_priv_key_pp,
|
||||
/* Tuic */
|
||||
uuid: node.uuid,
|
||||
congestion_control: node.tuic_congestion_control,
|
||||
udp_relay_mode: node.tuic_udp_relay_mode,
|
||||
udp_over_stream: strToBool(node.tuic_udp_over_stream),
|
||||
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
|
||||
heartbeat: strToTime(node.tuic_heartbeat),
|
||||
/* VLESS / VMess */
|
||||
flow: node.vless_flow,
|
||||
alter_id: strToInt(node.vmess_alterid),
|
||||
security: node.vmess_encrypt,
|
||||
@@ -344,8 +309,6 @@ function generate_outbound(node) {
|
||||
udp_fragment: strToBool(node.udp_fragment)
|
||||
};
|
||||
|
||||
/* Direct-node destination override: sing-box removed these options from
|
||||
the direct outbound since 1.13; emit them via the route action instead */
|
||||
if (node.type === 'direct' && (!isEmpty(node.override_address) || !isEmpty(node.override_port)))
|
||||
direct_overrides[node['.name']] = {
|
||||
override_address: node.override_address,
|
||||
@@ -432,11 +395,6 @@ function isDirectOutboundTag(tag) {
|
||||
return !isEmpty(node) && node.type === 'direct';
|
||||
}
|
||||
|
||||
/*
|
||||
* Remote rule-sets are downloaded through a top-level http_client in sing-box
|
||||
* 1.14 (the per-rule-set download_detour was removed). One client is created
|
||||
* per distinct detour and reused by every rule-set that dials through it.
|
||||
*/
|
||||
const http_clients = [];
|
||||
const http_seen = {};
|
||||
|
||||
@@ -450,9 +408,6 @@ function http_client_for(detour) {
|
||||
|
||||
http_seen[detour] = true;
|
||||
const client = { tag: tag };
|
||||
/* sing-box 1.14 rejects an empty detour on the direct outbound (pure TUN
|
||||
mode has no self_mark on it); omitting detour uses the same system
|
||||
direct dialer, so behaviour is unchanged. */
|
||||
if (!(isEmpty(self_mark) && isDirectOutboundTag(detour)))
|
||||
client.detour = detour;
|
||||
|
||||
@@ -461,10 +416,6 @@ function http_client_for(detour) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
/*
|
||||
* sing-box requires dns.servers[].headers to be a map, while the LuCI field is
|
||||
* a dynamic list of "Name: value" lines (and older configs may hold either).
|
||||
*/
|
||||
function parse_headers(headers) {
|
||||
if (isEmpty(headers))
|
||||
return null;
|
||||
@@ -482,11 +433,9 @@ function parse_headers(headers) {
|
||||
|
||||
return isEmpty(out) ? null : out;
|
||||
}
|
||||
/* Config helper end */
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: log_level,
|
||||
@@ -494,7 +443,6 @@ config.log = {
|
||||
timestamp: true
|
||||
};
|
||||
|
||||
/* NTP */
|
||||
if (!isEmpty(ntp_server))
|
||||
config.ntp = {
|
||||
enabled: true,
|
||||
@@ -503,8 +451,6 @@ if (!isEmpty(ntp_server))
|
||||
domain_resolver: 'default-dns',
|
||||
};
|
||||
|
||||
/* DNS start */
|
||||
/* Default settings */
|
||||
config.dns = {
|
||||
servers: [
|
||||
{
|
||||
@@ -532,7 +478,6 @@ config.dns = {
|
||||
};
|
||||
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Main DNS */
|
||||
push(config.dns.servers, {
|
||||
tag: 'main-dns',
|
||||
domain_resolver: {
|
||||
@@ -551,8 +496,6 @@ if (!isEmpty(main_node)) {
|
||||
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns'
|
||||
});
|
||||
|
||||
/* Reject SVCB/HTTPS queries to avoid proxy DNS timeout on null domains.
|
||||
Opt out with `block_https_rr 0` when clients should use HTTPS RR/ECH. */
|
||||
if (uci.get(uciconfig, ucimain, 'block_https_rr') !== '0')
|
||||
push(config.dns.rules, {
|
||||
query_type: [64, 65],
|
||||
@@ -564,18 +507,12 @@ if (!isEmpty(main_node)) {
|
||||
tag: 'china-dns',
|
||||
domain_resolver: {
|
||||
server: 'default-dns',
|
||||
/* follow the IPv6 setting like main-dns does, instead of
|
||||
preferring AAAA even on IPv4-only setups */
|
||||
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null
|
||||
},
|
||||
detour: self_mark ? 'direct-out' : null,
|
||||
...parse_dnsserver(china_dns_server)
|
||||
});
|
||||
|
||||
/* Route NAPTR (qtype 35) queries for SIP/ENUM domains to the ISP
|
||||
default-dns directly: china-dns (223.5.5.5) has intermittent
|
||||
multi-second first responses for NAPTR, causing 'context deadline
|
||||
exceeded' (e.g. sipgz12.hbq.r.10086.cn) */
|
||||
push(config.dns.rules, {
|
||||
query_type: [35],
|
||||
domain_suffix: ['r.10086.cn', '10086.cn', 'pub.3gppnetwork.org'],
|
||||
@@ -596,7 +533,6 @@ if (!isEmpty(main_node)) {
|
||||
server: 'china-dns'
|
||||
});
|
||||
|
||||
/* sing-box 1.14: restore CN-IP fallback via evaluate/match_response (opt-in) */
|
||||
if (uci.get(uciconfig, ucimain, 'cn_ip_fallback') === '1') {
|
||||
push(config.dns.rules, {
|
||||
action: 'evaluate',
|
||||
@@ -612,7 +548,6 @@ if (!isEmpty(main_node)) {
|
||||
}
|
||||
}
|
||||
} else if (is_custom_mode) {
|
||||
/* DNS servers */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
@@ -640,9 +575,6 @@ if (!isEmpty(main_node)) {
|
||||
});
|
||||
});
|
||||
|
||||
/* DNS rules */
|
||||
/* sing-box >= 1.14: legacy address-filter rules are auto-wrapped with an
|
||||
evaluate action; deprecated strategy/accept_empty fields are dropped. */
|
||||
const builtin_dns_rules = [];
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
@@ -705,10 +637,6 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
const legacy_filter = !isEmpty(cfg.ip_cidr) || strToBool(cfg.ip_is_private) === true;
|
||||
if (legacy_filter && !rule.match_response && cfg.action === 'route') {
|
||||
/* Wrap a legacy address-filter rule into the 1.14 evaluate/match_response
|
||||
paradigm. Carry the original query-matching fields onto the evaluate
|
||||
prefix rule so only queries that would have hit this rule get
|
||||
pre-resolved; an unconditional evaluate would resolve every query. */
|
||||
const eval_tag = '_hp_eval_' + cfg['.name'];
|
||||
const eval_rule = {
|
||||
action: 'evaluate',
|
||||
@@ -730,14 +658,11 @@ if (!isEmpty(main_node)) {
|
||||
rule.match_response = eval_tag;
|
||||
}
|
||||
|
||||
/* ip_cidr / ip_is_private are only valid with match_response in 1.14 */
|
||||
if (rule.match_response) {
|
||||
rule.ip_cidr = cfg.ip_cidr;
|
||||
rule.ip_is_private = strToBool(cfg.ip_is_private);
|
||||
}
|
||||
|
||||
/* sing-box rejects the whole config when a route/evaluate action has no
|
||||
server, so drop such a rule with a reason instead. */
|
||||
if ((rule.action === 'route' || rule.action === 'evaluate') && isEmpty(rule.server)) {
|
||||
warn(sprintf("homeproxy: DNS rule '%s' has no server configured, skipping it.", cfg['.name']));
|
||||
return;
|
||||
@@ -749,19 +674,9 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
config.dns.final = get_resolver(dns_default_server);
|
||||
}
|
||||
/* DNS end */
|
||||
|
||||
/* Inbound start */
|
||||
config.inbounds = [];
|
||||
|
||||
/*
|
||||
* dns-in is the local listener the DNS chain hands queries to: dnsmasq
|
||||
* forwards to 127.0.0.1#<dns_port> and the nft DNS hijack redirects to
|
||||
* dnsmasq, not here. It used to listen on '::', which published a
|
||||
* proxy-backed resolver to every LAN client on the DNS port; loopback is the
|
||||
* whole reachable surface it needs. mixed-in stays on '::' - that one is the
|
||||
* SOCKS/HTTP listener clients are meant to reach.
|
||||
*/
|
||||
push(config.inbounds, {
|
||||
type: 'direct',
|
||||
tag: 'dns-in',
|
||||
@@ -817,12 +732,9 @@ if (match(proxy_mode, /tun/))
|
||||
udp_nat_max: udp_nat_max,
|
||||
stack: tcpip_stack,
|
||||
});
|
||||
/* Inbound end */
|
||||
|
||||
/* Outbound start */
|
||||
config.endpoints = [];
|
||||
|
||||
/* Default outbounds */
|
||||
config.outbounds = [
|
||||
{
|
||||
type: 'direct',
|
||||
@@ -835,7 +747,6 @@ config.outbounds = [
|
||||
}
|
||||
];
|
||||
|
||||
/* Main outbounds */
|
||||
if (!isEmpty(main_node)) {
|
||||
let urltest_nodes = [];
|
||||
|
||||
@@ -958,16 +869,7 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
if (isEmpty(config.endpoints))
|
||||
config.endpoints = null;
|
||||
/* Outbound end */
|
||||
|
||||
/* Routing rules start */
|
||||
/* Default settings */
|
||||
/*
|
||||
* The sniff rule below deliberately carries no `timeout`. Reports from
|
||||
* sing-box 1.14.2 show it rejected with "route.rules[1].timeout: json: unknown
|
||||
* field". The official builds accept it, so the exact trigger is still open,
|
||||
* but the default is already 300ms and omitting the field is behaviour-neutral.
|
||||
*/
|
||||
config.route = {
|
||||
rules: [
|
||||
{
|
||||
@@ -983,19 +885,12 @@ config.route = {
|
||||
default_interface: default_interface
|
||||
};
|
||||
|
||||
/* Routing rules */
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Resolve outbound server domains through the WAN default resolver.
|
||||
Do not use china-dns here: china-dns is for resolving mainland China
|
||||
destinations, not the proxy node itself. Coupling node bootstrap to
|
||||
china_dns_server can break dialing when that resolver is polluted or
|
||||
unsuitable for the node domain. */
|
||||
config.route.default_domain_resolver = {
|
||||
server: 'default-dns',
|
||||
strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null
|
||||
};
|
||||
|
||||
/* Direct list */
|
||||
if (length(direct_domain_list))
|
||||
push(config.route.rules, {
|
||||
rule_set: 'direct-domain',
|
||||
@@ -1003,14 +898,17 @@ if (!isEmpty(main_node)) {
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
|
||||
/* Bypass CN traffic: resolve the destination first, then route by IP.
|
||||
sing-box does not match an IP-based rule set (geoip-cn) against a domain
|
||||
destination unless it is resolved first, so add an explicit resolve
|
||||
action; geoip-cn then sends China IPs to direct and everything else falls
|
||||
through to main-out (proxy). This avoids relying on the geosite-* domain
|
||||
lists, which can mis-classify foreign domains (e.g. Google's gvt2.com
|
||||
beacons) as "cn" and send them direct to time out. Keep the direct-domain
|
||||
fast-path above for known direct domains. */
|
||||
if (dedicated_udp_node) {
|
||||
const udp_override = direct_overrides[main_udp_node] || null;
|
||||
push(config.route.rules, {
|
||||
network: 'udp',
|
||||
action: 'route',
|
||||
outbound: 'main-udp-out',
|
||||
override_address: udp_override ? udp_override.override_address : null,
|
||||
override_port: udp_override ? udp_override.override_port : null
|
||||
});
|
||||
}
|
||||
|
||||
if (routing_mode === 'bypass_mainland_china') {
|
||||
push(config.route.rules, {
|
||||
action: 'resolve',
|
||||
@@ -1023,20 +921,6 @@ if (!isEmpty(main_node)) {
|
||||
});
|
||||
}
|
||||
|
||||
/* Main UDP out */
|
||||
if (dedicated_udp_node) {
|
||||
const udp_override = direct_overrides[main_udp_node] || null;
|
||||
push(config.route.rules, {
|
||||
network: 'udp',
|
||||
action: 'route',
|
||||
outbound: 'main-udp-out',
|
||||
override_address: udp_override ? udp_override.override_address : null,
|
||||
override_port: udp_override ? udp_override.override_port : null
|
||||
});
|
||||
}
|
||||
|
||||
/* Direct-node destination override, emitted as route-options action
|
||||
(direct outbound options removed since sing-box 1.13) */
|
||||
const main_override = direct_overrides[main_node] || null;
|
||||
if (main_override)
|
||||
push(config.route.rules, {
|
||||
@@ -1047,8 +931,6 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
config.route.final = 'main-out';
|
||||
|
||||
/* Rule set */
|
||||
/* Direct list */
|
||||
if (length(direct_domain_list))
|
||||
push(config.route.rule_set, {
|
||||
type: 'inline',
|
||||
@@ -1060,7 +942,6 @@ if (!isEmpty(main_node)) {
|
||||
]
|
||||
});
|
||||
|
||||
/* Proxy list */
|
||||
if (length(proxy_domain_list))
|
||||
push(config.route.rule_set, {
|
||||
type: 'inline',
|
||||
@@ -1073,17 +954,6 @@ if (!isEmpty(main_node)) {
|
||||
});
|
||||
|
||||
if (routing_mode === 'bypass_mainland_china') {
|
||||
/*
|
||||
* The two rule-sets sing-box actually needs for this mode. Each one is
|
||||
* used from the local .srs file when update_resources.sh has fetched
|
||||
* it, and only falls back to a remote fetch otherwise: a remote
|
||||
* rule-set is initialized at every start (sing-box fails to start when
|
||||
* the fetch fails even if nothing references it), which would make a
|
||||
* cold boot depend on reaching the CDN through the node. The fallback
|
||||
* keeps a fresh install working before the first resource update.
|
||||
* geosite-noncn is intentionally not declared: no rule references it,
|
||||
* and sing-box downloads it anyway.
|
||||
*/
|
||||
const preset_rulesets = [
|
||||
{
|
||||
tag: 'geoip-cn',
|
||||
@@ -1147,11 +1017,6 @@ if (!isEmpty(main_node)) {
|
||||
rule_override_port = rule_direct_override.override_port;
|
||||
}
|
||||
|
||||
/* sing-box routing rule: emit match fields plus action-specific fields.
|
||||
client is the sniffed client type (only set with protocol quic/ssh);
|
||||
resolve adds server/strategy/disable_cache/rewrite_ttl/client_subnet;
|
||||
reject adds method/no_drop. Emitted conditionally so a given action
|
||||
never carries fields sing-box rejects for it. */
|
||||
const rule = {
|
||||
ip_version: strToInt(cfg.ip_version),
|
||||
protocol: cfg.protocol,
|
||||
@@ -1207,8 +1072,6 @@ if (!isEmpty(main_node)) {
|
||||
push(config.route.rules, rule);
|
||||
});
|
||||
|
||||
/* Direct-node destination override, emitted as route-options action
|
||||
(direct outbound options removed since sing-box 1.13) */
|
||||
const final_override = get_direct_override(default_outbound);
|
||||
if (final_override)
|
||||
push(config.route.rules, {
|
||||
@@ -1219,7 +1082,6 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
config.route.final = get_outbound(default_outbound);
|
||||
|
||||
/* Rule set */
|
||||
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
@@ -1230,8 +1092,6 @@ if (!isEmpty(main_node)) {
|
||||
rs_tag = [rs_tag];
|
||||
for (let t in extra_tags)
|
||||
push(rs_tag, 'cfg-' + t + '-rule');
|
||||
/* sing-box 1.14: multi-tag requires a {tag} placeholder in the fetch source
|
||||
(remote: url and initial_path, local: path) */
|
||||
const fetch_ref = (cfg.type === 'remote') ? (cfg.url || '') : (cfg.path || '');
|
||||
if (!match(fetch_ref, /\{tag\}/))
|
||||
warn(sprintf("homeproxy: rule-set '%s' uses extra tags but its %s source lacks a {tag} placeholder.", cfg['.name'], cfg.type));
|
||||
@@ -1247,8 +1107,6 @@ if (!isEmpty(main_node)) {
|
||||
url: cfg.url,
|
||||
update_interval: cfg.update_interval
|
||||
};
|
||||
/* sing-box 1.14 downloads remote rule-sets through a top-level
|
||||
http_client; download_detour no longer exists. */
|
||||
if (cfg.type === 'remote')
|
||||
ruleset.http_client = http_client_for(get_outbound(cfg.outbound) || get_outbound(default_outbound));
|
||||
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path))
|
||||
@@ -1257,28 +1115,12 @@ if (!isEmpty(main_node)) {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Belt-and-braces for route.default_domain_resolver.
|
||||
*
|
||||
* sing-box 1.14 removed the legacy outbound DNS rules and refuses to start when
|
||||
* a dial-fields user has a domain to resolve without a resolver. The exemption
|
||||
* for "only one DNS server configured" never applies here: default-dns and
|
||||
* system-dns are emitted unconditionally, so every config has at least two.
|
||||
* That makes this a global requirement, not a per-mode one, so it must not
|
||||
* depend on which branch above ran. The branches already pick the right
|
||||
* resolver (the WAN resolver, or default_outbound_dns in custom mode); this
|
||||
* only covers the degenerate "not custom mode and no main node" case.
|
||||
*/
|
||||
if (isEmpty(config.route.default_domain_resolver))
|
||||
config.route.default_domain_resolver = { server: 'default-dns' };
|
||||
|
||||
/* Remote rule-sets download through the http_clients collected while the
|
||||
rule-sets were built (see http_client_for). */
|
||||
if (length(http_clients))
|
||||
config.http_clients = http_clients;
|
||||
/* Routing rules end */
|
||||
|
||||
/* Experimental start */
|
||||
if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
||||
config.experimental = {
|
||||
cache_file: {
|
||||
@@ -1288,7 +1130,6 @@ if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
||||
}
|
||||
};
|
||||
}
|
||||
/* Experimental end */
|
||||
|
||||
config['$schema'] = 'https://sing-box.sagernet.org/schema.json';
|
||||
|
||||
@@ -1300,7 +1141,4 @@ if (system('/usr/bin/sing-box check --config ' + client_tmp) !== 0) {
|
||||
exit(1);
|
||||
}
|
||||
system('mv -f ' + client_tmp + ' ' + RUN_DIR + '/sing-box-c.json');
|
||||
/* The generated config carries every node credential (passwords, UUIDs,
|
||||
private keys) and writefile() has no mode argument, so it lands with the
|
||||
process umask; chmod it to the sing-box user only. */
|
||||
system('chmod 600 ' + RUN_DIR + '/sing-box-c.json');
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
removeBlankAttrs, buildTLSObject, buildTransportObject, HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
@@ -24,11 +23,9 @@ uci.load(uciconfig);
|
||||
const uciserver = 'server';
|
||||
|
||||
const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn';
|
||||
/* UCI config end */
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: log_level,
|
||||
@@ -42,7 +39,6 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
/* Snell (1.14): single-psk inbound; shape differs from the generic users[] block below */
|
||||
if (cfg.type === 'snell') {
|
||||
push(config.inbounds, {
|
||||
type: 'snell',
|
||||
@@ -76,10 +72,8 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
udp_timeout: strToTime(cfg.udp_timeout),
|
||||
network: cfg.network,
|
||||
|
||||
/* AnyTLS */
|
||||
padding_scheme: cfg.anytls_padding_scheme,
|
||||
|
||||
/* Hysteria (2) */
|
||||
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
||||
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
||||
obfs: cfg.hysteria_obfs_type ? {
|
||||
@@ -91,31 +85,25 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||
masquerade: cfg.hysteria_masquerade,
|
||||
|
||||
/* Shadowsocks */
|
||||
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
|
||||
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
|
||||
|
||||
/* Tuic */
|
||||
congestion_control: cfg.tuic_congestion_control,
|
||||
auth_timeout: strToTime(cfg.tuic_auth_timeout),
|
||||
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
|
||||
heartbeat: strToTime(cfg.tuic_heartbeat),
|
||||
|
||||
/* AnyTLS / HTTP / Hysteria (2) / Mixed / Socks / Trojan / Tuic / VLESS / VMess */
|
||||
users: (cfg.type !== 'shadowsocks') ? [
|
||||
{
|
||||
name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
|
||||
/* Hysteria */
|
||||
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
|
||||
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
|
||||
|
||||
/* Tuic */
|
||||
uuid: cfg.uuid,
|
||||
|
||||
/* VLESS / VMess */
|
||||
flow: cfg.vless_flow,
|
||||
alterId: strToInt(cfg.vmess_alterid)
|
||||
}
|
||||
@@ -150,5 +138,4 @@ if (system('/usr/bin/sing-box check --config ' + server_tmp) !== 0) {
|
||||
exit(1);
|
||||
}
|
||||
system('mv -f ' + server_tmp + ' ' + RUN_DIR + '/sing-box-s.json');
|
||||
/* keep the config (key paths, user credentials) out of world-readable mode */
|
||||
system('chmod 600 ' + RUN_DIR + '/sing-box-s.json');
|
||||
@@ -7,13 +7,9 @@
|
||||
import { mkstemp } from 'fs';
|
||||
import { urldecode_params } from 'luci.http';
|
||||
|
||||
/* Global variables start */
|
||||
export const HP_DIR = '/etc/homeproxy';
|
||||
export const RUN_DIR = '/var/run/homeproxy';
|
||||
/* Global variables end */
|
||||
|
||||
/* Utilities start */
|
||||
/* Kanged from luci-app-commands */
|
||||
export function shellQuote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
};
|
||||
@@ -26,26 +22,15 @@ export function isBinary(str) {
|
||||
return false;
|
||||
};
|
||||
|
||||
/* Close a file descriptor, swallowing secondary errors (e.g. an already
|
||||
closed fd). ucode has no `finally` clause, so both the normal and the
|
||||
exceptional path of executeCommand() have to close explicitly. */
|
||||
function closeFD(fd) {
|
||||
if (fd) {
|
||||
try {
|
||||
fd.close();
|
||||
} catch (e) {
|
||||
/* already closed */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Read a descriptor to EOF. A single read(size) only returns up to `size`
|
||||
* bytes, so the previous one-shot read(512 KiB) silently truncated larger
|
||||
* responses - a subscription list above that size was cut mid-base64/mid-JSON
|
||||
* and only showed up as a parse failure. MAX_OUTPUT bounds what an oversized
|
||||
* or endless response can allocate.
|
||||
*/
|
||||
const MAX_OUTPUT = 16 * 1024 * 1024;
|
||||
|
||||
function readAll(fd) {
|
||||
@@ -79,7 +64,6 @@ export function executeCommand(...args) {
|
||||
|
||||
const binary = isBinary(stdout);
|
||||
|
||||
/* mkstemp() returns delete-on-close files, so closing is enough */
|
||||
closeFD(outfd);
|
||||
closeFD(errfd);
|
||||
|
||||
@@ -91,8 +75,6 @@ export function executeCommand(...args) {
|
||||
binary
|
||||
};
|
||||
} catch (e) {
|
||||
/* Never leak the temporary descriptors on a failing run. ucode has
|
||||
no `finally` clause, so the exception is re-raised by hand. */
|
||||
closeFD(outfd);
|
||||
closeFD(errfd);
|
||||
|
||||
@@ -114,19 +96,6 @@ export function getTime(epoch) {
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* GNU wget and OpenWrt's default /usr/bin/wget do not accept the same flags.
|
||||
* On OpenWrt/ImmortalWrt that path comes from the uclient-fetch package
|
||||
* (`ALTERNATIVES:=200:/usr/bin/wget:/bin/uclient-fetch`, `PROVIDES:=wget` in
|
||||
* package/libs/uclient) and busybox ships no wget applet, so the effective
|
||||
* wget is uclient-fetch unless wget-ssl happens to be installed on top of it.
|
||||
* GNU-only flags break the fetch outright: uclient-fetch's getopt rejects
|
||||
* `-nv` with "unrecognized option: n" before a single request is sent.
|
||||
*
|
||||
* The probe below tells the two apart. `-nv` (--no-verbose) is how GNU wget
|
||||
* mutes the progress meter while still printing the failure reason on stderr;
|
||||
* `-q` is the only meter switch uclient-fetch and busybox wget share.
|
||||
*/
|
||||
export function classifyWgetFlavor(exitcode, stdout) {
|
||||
if (exitcode !== 0 || !stdout)
|
||||
return 'compat';
|
||||
@@ -146,13 +115,6 @@ export function wgetFlavor() {
|
||||
return wget_flavor;
|
||||
};
|
||||
|
||||
/*
|
||||
* Fetch a URL and report both the body and, on failure, the reason. The
|
||||
* reason is wget's own stderr (whitespace collapsed, length-capped) so the
|
||||
* caller can tell a DNS failure from a timeout or a TLS handshake error.
|
||||
* Note that uclient-fetch's -q hides most failure messages, so on such a
|
||||
* target the reason is often empty and only the exit code is left.
|
||||
*/
|
||||
export function wGETVerbose(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return { content: null, error: 'invalid URL' };
|
||||
@@ -160,9 +122,6 @@ export function wGETVerbose(url, ua) {
|
||||
if (!ua)
|
||||
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
||||
|
||||
/* -U/-T are understood by GNU wget, uclient-fetch and busybox wget alike;
|
||||
* the long forms --user-agent/--timeout are not, busybox wget in
|
||||
* particular knows neither of them. */
|
||||
const output = executeCommand(`/usr/bin/wget ${wgetFlavor() === 'gnu' ? '-nv' : '-q'} -O- -U ${shellQuote(ua)} -T 10 ${shellQuote(url)}`) || {};
|
||||
if (output.exitcode !== 0) {
|
||||
let reason = trim(output.stderr || '');
|
||||
@@ -180,9 +139,7 @@ export function wGETVerbose(url, ua) {
|
||||
export function wGET(url, ua) {
|
||||
return wGETVerbose(url, ua).content;
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
/* String helper start */
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
@@ -199,16 +156,9 @@ export function strToTime(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
/* Preserve values that already carry a time unit (e.g. "30s", "1m") */
|
||||
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
|
||||
};
|
||||
|
||||
/*
|
||||
* Milliseconds variant of strToTime(). Used by options whose LuCI field is
|
||||
* documented and validated in milliseconds while sing-box expects a duration
|
||||
* (e.g. route rule tls_fragment_fallback_delay, whose default is 500ms);
|
||||
* strToTime() used to turn "500" into "500s".
|
||||
*/
|
||||
export function strToMs(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
@@ -255,20 +205,14 @@ export function validation(datatype, data) {
|
||||
return (ret === 0);
|
||||
};
|
||||
|
||||
/* Validate IP/CIDR format to prevent nftables template injection from resource files */
|
||||
export function isValidCIDR(addr, family) {
|
||||
if (isEmpty(addr))
|
||||
return false;
|
||||
|
||||
/* Strip leading/trailing whitespace */
|
||||
addr = trim(addr);
|
||||
if (!addr)
|
||||
return false;
|
||||
|
||||
/* Split address and optional prefix. The prefix has to be validated as
|
||||
strictly as the address: `int()` accepts a leading number ("1.2.3.4/0 }
|
||||
..." parses as 0), which would let a poisoned resource file inject
|
||||
statements into the nftables template this feeds. */
|
||||
const parts = split(addr, '/');
|
||||
const ip = parts[0];
|
||||
const prefix = parts[1];
|
||||
@@ -278,31 +222,24 @@ export function isValidCIDR(addr, family) {
|
||||
if (length(parts) === 2 && !match(prefix, /^\d{1,3}$/))
|
||||
return false;
|
||||
|
||||
/* Validate IP part */
|
||||
if (family === 4) {
|
||||
if (!match(ip, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
|
||||
return false;
|
||||
/* Validate octet ranges */
|
||||
const octets = split(ip, '.');
|
||||
for (let o in octets)
|
||||
if (int(o) > 255)
|
||||
return false;
|
||||
/* Validate prefix if present */
|
||||
if (prefix && int(prefix) > 32)
|
||||
return false;
|
||||
} else if (family === 6) {
|
||||
/* Only hex digits and colons */
|
||||
if (!match(ip, /^[0-9a-fA-F:]+$/))
|
||||
return false;
|
||||
|
||||
/* At most one "::" */
|
||||
if (match(ip, /::.*::/))
|
||||
return false;
|
||||
|
||||
/* Validate group structure */
|
||||
const dcolon = index(ip, '::');
|
||||
if (dcolon === -1) {
|
||||
/* Uncompressed form: exactly 8 groups of 1-4 hex digits */
|
||||
const groups = split(ip, ':');
|
||||
if (length(groups) !== 8)
|
||||
return false;
|
||||
@@ -310,7 +247,6 @@ export function isValidCIDR(addr, family) {
|
||||
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
|
||||
return false;
|
||||
} else {
|
||||
/* Compressed form: "::" expands to at least one zero group */
|
||||
let count = 0;
|
||||
for (let part in [substr(ip, 0, dcolon), substr(ip, dcolon + 2)]) {
|
||||
if (part === '')
|
||||
@@ -333,9 +269,7 @@ export function isValidCIDR(addr, family) {
|
||||
|
||||
return true;
|
||||
};
|
||||
/* String helper end */
|
||||
|
||||
/* String parser start */
|
||||
export function decodeBase64Str(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
@@ -351,14 +285,6 @@ export function decodeBase64Str(str) {
|
||||
return b64dec(str);
|
||||
};
|
||||
|
||||
/*
|
||||
* Decode %XX escapes without touching '+'. urldecode() maps '+' to a space,
|
||||
* which corrupts base64 userinfo ("+" is part of the alphabet), so share-link
|
||||
* userinfo is percent-decoded with this instead.
|
||||
*/
|
||||
/*
|
||||
* ucode has no tolower(), so both hex cases are mapped by hand.
|
||||
*/
|
||||
function hexValue(ch) {
|
||||
const lower = index('0123456789abcdef', ch);
|
||||
if (lower >= 0)
|
||||
@@ -369,10 +295,6 @@ function hexValue(ch) {
|
||||
return upper < 0 ? -1 : 10 + upper;
|
||||
}
|
||||
|
||||
/*
|
||||
* ucode's regex engine rejects \x00 inside a character class ("Missing ']'"),
|
||||
* so control bytes are checked by hand.
|
||||
*/
|
||||
function hasControlChar(str) {
|
||||
for (let i = 0; i < length(str); i++) {
|
||||
const code = ord(str, i);
|
||||
@@ -449,9 +371,6 @@ export function parseURL(url) {
|
||||
return null;
|
||||
|
||||
if (objurl.userinfo) {
|
||||
/* Percent-decode first: providers emit base64 userinfo with the
|
||||
padding escaped ("=" as "%3D"), which used to decode to nothing
|
||||
and silently dropped method/password. */
|
||||
let userinfo = percentDecode(objurl.userinfo);
|
||||
|
||||
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
|
||||
@@ -459,14 +378,6 @@ export function parseURL(url) {
|
||||
return '';
|
||||
});
|
||||
|
||||
/*
|
||||
* Keep anything without whitespace or control characters: real-world
|
||||
* userinfo carries base64 (with '=' or '+'), percent-encoded tokens
|
||||
* and passwords with punctuation. The previous class was written as
|
||||
* [A-Za-z0-9\+\-\_\.] but ucode's regex reads \+-\_ as the range
|
||||
* 0x2B-0x5F, so it accepted '=@:;[]\^<>,/' and rejected '!' - it
|
||||
* neither did what it documented nor covered real passwords.
|
||||
*/
|
||||
if (!match(userinfo, /\s/) && !hasControlChar(userinfo))
|
||||
objurl.username = userinfo;
|
||||
else
|
||||
@@ -483,18 +394,7 @@ export function parseURL(url) {
|
||||
|
||||
return objurl;
|
||||
};
|
||||
/* String parser end */
|
||||
|
||||
/* Config generator helper start */
|
||||
/*
|
||||
* Shared sing-box TLS object builder for the client outbound and the server
|
||||
* inbound. The property order below is significant: the generated JSON keeps
|
||||
* the insertion order of this object after removeBlankAttrs() drops the blank
|
||||
* attributes, and both generators used to emit the fields in exactly this
|
||||
* order. Keep client-only fields (insecure/handshake_timeout/utls) and
|
||||
* server-only fields (key_path/certificate_provider) at their current
|
||||
* positions when editing.
|
||||
*/
|
||||
export function buildTLSObject(cfg, is_server) {
|
||||
if (cfg.tls !== '1')
|
||||
return null;
|
||||
@@ -540,7 +440,6 @@ export function buildTLSObject(cfg, is_server) {
|
||||
ech: is_server ? (cfg.tls_ech_key ? {
|
||||
enabled: true,
|
||||
key: split(cfg.tls_ech_key, '\n')
|
||||
/* config: split(cfg.tls_ech_config, '\n') */
|
||||
} : null) : ((cfg.tls_ech === '1') ? {
|
||||
enabled: true,
|
||||
config: cfg.tls_ech_config,
|
||||
@@ -567,8 +466,6 @@ export function buildTLSObject(cfg, is_server) {
|
||||
};
|
||||
};
|
||||
|
||||
/* Shared sing-box transport object builder; the client transport additionally
|
||||
supports the gRPC keepalive hint, the server one does not. */
|
||||
export function buildTransportObject(cfg, is_server) {
|
||||
if (isEmpty(cfg.transport))
|
||||
return null;
|
||||
@@ -589,15 +486,7 @@ export function buildTransportObject(cfg, is_server) {
|
||||
permit_without_stream: is_server ? null : strToBool(cfg.grpc_permit_without_stream)
|
||||
};
|
||||
};
|
||||
/* Config generator helper end */
|
||||
|
||||
/* PEM validation start */
|
||||
/*
|
||||
* Check that `content` is a PEM certificate (is_private_key = false) or an
|
||||
* RSA/EC private key (is_private_key = true): matching BEGIN/END boundaries
|
||||
* with a base64 body in between. Kanged from luci-proto-openconnect; used by
|
||||
* the rpcd certificate upload and available for future certificate features.
|
||||
*/
|
||||
export function isValidPEM(content, is_private_key) {
|
||||
if (isEmpty(content))
|
||||
return false;
|
||||
@@ -619,4 +508,3 @@ export function isValidPEM(content, is_private_key) {
|
||||
|
||||
return true;
|
||||
};
|
||||
/* PEM validation end */
|
||||
@@ -27,11 +27,9 @@ const uciinfra = 'infra',
|
||||
uciroutingrule = 'routing_rule',
|
||||
uciserver = 'server';
|
||||
|
||||
/* chinadns-ng has been removed */
|
||||
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
||||
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
||||
|
||||
/* chinadns server now only accepts single server */
|
||||
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||
if (type(china_dns_server) === 'array') {
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
||||
@@ -42,51 +40,41 @@ if (type(china_dns_server) === 'array') {
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
|
||||
}
|
||||
|
||||
/* github_token option has been moved to config section */
|
||||
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
|
||||
if (github_token) {
|
||||
uci.set(uciconfig, ucimain, 'github_token', github_token);
|
||||
uci.delete(uciconfig, uciinfra, 'github_token')
|
||||
}
|
||||
|
||||
/* ntp_server was introduced */
|
||||
if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
||||
uci.set(uciconfig, uciinfra, 'ntp_server', 'nil');
|
||||
|
||||
/* tun_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
||||
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
||||
|
||||
/* create migration section */
|
||||
if (!uci.get(uciconfig, ucimigration))
|
||||
uci.set(uciconfig, ucimigration, uciconfig);
|
||||
|
||||
/* delete old crontab command */
|
||||
const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab');
|
||||
if (!migration_crontab) {
|
||||
system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>/dev/null');
|
||||
uci.set(uciconfig, ucimigration, 'crontab', '1');
|
||||
}
|
||||
|
||||
/* log_level was introduced */
|
||||
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
||||
uci.set(uciconfig, ucimain, 'log_level', 'warn');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
||||
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
||||
|
||||
/* empty value defaults to all ports now */
|
||||
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
||||
uci.delete(uciconfig, ucimain, 'routing_port');
|
||||
|
||||
/* experimental section was removed */
|
||||
if (uci.get(uciconfig, 'experimental'))
|
||||
uci.delete(uciconfig, 'experimental');
|
||||
|
||||
/* block-dns was removed from built-in dns servers */
|
||||
const default_dns_server = uci.get(uciconfig, ucidns, 'default_server');
|
||||
if (default_dns_server === 'block-dns') {
|
||||
/* append a rule at last to block all DNS queries */
|
||||
uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule);
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1');
|
||||
@@ -98,12 +86,9 @@ else if (default_dns_server === 'local-dns')
|
||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||
|
||||
const dns_server_migration = {};
|
||||
/* DNS servers options */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
/* legacy format was deprecated in sb 1.12 */
|
||||
if (cfg.address) {
|
||||
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
|
||||
/* RCode was moved into DNS rules */
|
||||
if (addr.protocol === 'rcode') {
|
||||
dns_server_migration[cfg['.name']] = { action: 'predefined' };
|
||||
switch (addr.hostname) {
|
||||
@@ -156,9 +141,7 @@ uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* DNS rules options */
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
/* outbound was removed in sb 1.12 */
|
||||
if (cfg.outbound) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
if (!cfg.enabled)
|
||||
@@ -181,20 +164,16 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
return;
|
||||
}
|
||||
|
||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
||||
if (cfg.rule_set_ipcidr_match_source === '1')
|
||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||
|
||||
/* block-dns was moved into action in sb 1.11 */
|
||||
if (cfg.server === 'block-dns') {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||
uci.delete(uciconfig, cfg['.name'], 'server');
|
||||
} else if (!cfg.action) {
|
||||
/* add missing 'action' field */
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||
}
|
||||
|
||||
/* strategy and client_subnet were moved into dns rules */
|
||||
if (dns_server_migration[cfg.server]) {
|
||||
if (dns_server_migration[cfg.server].strategy)
|
||||
uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy);
|
||||
@@ -210,26 +189,20 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* nodes options */
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
|
||||
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
|
||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
|
||||
|
||||
/* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */
|
||||
if (!isEmpty(cfg.tls_ech_enable_pqss))
|
||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
|
||||
|
||||
/* wireguard_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.wireguard_gso))
|
||||
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
|
||||
|
||||
/* hysteria_revc_window was a typo, renamed to hysteria_recv_window */
|
||||
if (!isEmpty(cfg.hysteria_revc_window))
|
||||
uci.rename(uciconfig, cfg['.name'], 'hysteria_revc_window', 'hysteria_recv_window');
|
||||
});
|
||||
|
||||
/* sing-box 1.14: remove deprecated DNS semantics */
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'independent_cache')))
|
||||
uci.delete(uciconfig, ucidns, 'independent_cache');
|
||||
|
||||
@@ -248,38 +221,29 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
uci.delete(uciconfig, cfg['.name'], 'rule_set_ip_cidr_accept_empty');
|
||||
});
|
||||
|
||||
/* routing rules options */
|
||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
||||
if (cfg.rule_set_ipcidr_match_source === '1')
|
||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||
|
||||
/* block-out was moved into action in sb 1.11 */
|
||||
if (cfg.outbound === 'block-out') {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||
uci.delete(uciconfig, cfg['.name'], 'outbound');
|
||||
} else if (!cfg.action) {
|
||||
/* add missing 'action' field */
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||
}
|
||||
});
|
||||
|
||||
/* server options */
|
||||
/* auto_firewall was moved into server options */
|
||||
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
||||
if (!isEmpty(auto_firewall))
|
||||
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
/* auto_firewall was moved into server options */
|
||||
if (auto_firewall === '1')
|
||||
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
||||
|
||||
/* sniff_override was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.sniff_override))
|
||||
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
|
||||
|
||||
/* domain_strategy is now pointless without sniff override */
|
||||
if (!isEmpty(cfg.domain_strategy))
|
||||
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
|
||||
});
|
||||
|
||||
@@ -10,28 +10,13 @@ import { urldecode, urlencode } from 'luci.http';
|
||||
|
||||
import { decodeBase64Str, parseURL, percentDecode, isEmpty, validation } from 'homeproxy';
|
||||
|
||||
/*
|
||||
* Share-link parsers. Every protocol has its own parse_<scheme>_uri() so the
|
||||
* branches can be read, changed and unit-tested in isolation; parse_uri() only
|
||||
* dispatches on the scheme and applies the common address/port validation and
|
||||
* label fallback afterwards.
|
||||
*
|
||||
* `features` is the sing-box feature map (with_quic/with_utls/...), `log` is a
|
||||
* logging callback, both injected so the parsers stay free of ubus/uci.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Transports sing-box accepts on a V2Ray-style outbound. Anything else
|
||||
* (xhttp, splithttp, ...) makes sing-box reject the whole generated config,
|
||||
* so such a node is dropped while importing instead of poisoning every node.
|
||||
*/
|
||||
const SUPPORTED_TRANSPORTS = ['ws', 'http', 'grpc', 'httpupgrade', 'quic'];
|
||||
|
||||
function unsupported_transport(config) {
|
||||
return !isEmpty(config.transport) && !(config.transport in SUPPORTED_TRANSPORTS);
|
||||
}
|
||||
|
||||
/* https://shadowsocks.org/guide/sip008.html */
|
||||
export function parse_sip008_uri(uri) {
|
||||
return {
|
||||
label: uri.remarks,
|
||||
@@ -45,7 +30,6 @@ export function parse_sip008_uri(uri) {
|
||||
};
|
||||
};
|
||||
|
||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
||||
export function parse_anytls_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
@@ -80,7 +64,6 @@ export function parse_http_uri(uri) {
|
||||
};
|
||||
};
|
||||
|
||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
||||
export function parse_hysteria_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
@@ -113,7 +96,6 @@ export function parse_hysteria_uri(uri, features, log) {
|
||||
};
|
||||
};
|
||||
|
||||
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
||||
export function parse_hysteria2_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
@@ -142,7 +124,6 @@ export function parse_hysteria2_uri(uri, features, log) {
|
||||
};
|
||||
};
|
||||
|
||||
/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
|
||||
export function parse_snell_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
@@ -183,7 +164,6 @@ export function parse_socks_uri(uri) {
|
||||
export function parse_ss_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
/* "Lovely" Shadowrocket format */
|
||||
const ss_suri = split(uri[1], '#');
|
||||
let ss_slabel = '';
|
||||
if (length(ss_suri) <= 2) {
|
||||
@@ -194,18 +174,13 @@ export function parse_ss_uri(uri) {
|
||||
uri[1] = ss_decoded + ss_slabel;
|
||||
}
|
||||
|
||||
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
|
||||
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
||||
|
||||
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
|
||||
let ss_userinfo = [];
|
||||
if (url.username && url.password)
|
||||
/* User info encoded with URIComponent; parseURL() already decoded it */
|
||||
ss_userinfo = [url.username, url.password];
|
||||
else if (url.username)
|
||||
/* User info encoded with base64 (padding may have arrived as %3D) */
|
||||
ss_userinfo = split(decodeBase64Str(url.username) ?? '', ':', 2);
|
||||
|
||||
let ss_plugin, ss_plugin_opts;
|
||||
@@ -213,7 +188,6 @@ export function parse_ss_uri(uri) {
|
||||
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
|
||||
ss_plugin = ss_plugin_info[0];
|
||||
if (ss_plugin === 'simple-obfs')
|
||||
/* Fix non-standard plugin name */
|
||||
ss_plugin = 'obfs-local';
|
||||
ss_plugin_opts = ss_plugin_info[1];
|
||||
}
|
||||
@@ -230,7 +204,6 @@ export function parse_ss_uri(uri) {
|
||||
};
|
||||
};
|
||||
|
||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
||||
export function parse_trojan_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
@@ -270,7 +243,6 @@ export function parse_trojan_uri(uri) {
|
||||
return config;
|
||||
};
|
||||
|
||||
/* https://github.com/daeuniverse/dae/discussions/182 */
|
||||
export function parse_tuic_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
@@ -299,14 +271,12 @@ export function parse_tuic_uri(uri, features, log) {
|
||||
};
|
||||
};
|
||||
|
||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
||||
export function parse_vless_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
/* Unsupported protocol */
|
||||
if (params.type === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
return null;
|
||||
@@ -332,10 +302,6 @@ export function parse_vless_uri(uri, features, log) {
|
||||
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
||||
tls_reality_short_id: params.sid,
|
||||
tls_utls: features.with_utls ? params.fp : null,
|
||||
/* sing-box rejects every other value ("unsupported flow") and a single
|
||||
rejected outbound makes the whole generated config fail, so only the
|
||||
supported value is imported (the common
|
||||
"xtls-rprx-vision-udp443" becomes "no flow" instead of breaking). */
|
||||
vless_flow: (params.security in ['tls', 'reality'] && params.flow === 'xtls-rprx-vision') ? params.flow : null
|
||||
};
|
||||
switch (params.type) {
|
||||
@@ -372,11 +338,9 @@ export function parse_vless_uri(uri, features, log) {
|
||||
return config;
|
||||
};
|
||||
|
||||
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
|
||||
export function parse_vmess_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
/* "Lovely" shadowrocket format */
|
||||
if (match(uri, /&/)) {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
@@ -393,7 +357,6 @@ export function parse_vmess_uri(uri, features, log) {
|
||||
if (payload.v != '2') {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
/* Unsupported protocol */
|
||||
} else if (payload.net === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
||||
return null;
|
||||
@@ -404,13 +367,6 @@ export function parse_vmess_uri(uri, features, log) {
|
||||
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
|
||||
* else if (payload.aid && int(payload.aid) !== 0) {
|
||||
* log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
||||
* return null;
|
||||
* }
|
||||
*/
|
||||
|
||||
const config = {
|
||||
label: payload.ps ? urldecode(payload.ps) : null,
|
||||
|
||||
@@ -5,21 +5,12 @@
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
||||
|
||||
# update_resources.sh exit codes: 0 updated, 1 failed, 2 another run holds the
|
||||
# lock, 3 already up to date.
|
||||
changed=0
|
||||
for i in "china_ip4" "china_ip6" "gfw_list" "china_list" "geoip_cn" "geosite_cn"; do
|
||||
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
||||
[ "$?" -eq 0 ] && changed=1
|
||||
done
|
||||
|
||||
# The nft sets (china_ip4/ip6.txt) and the dnsmasq snippets (gfw_list.txt,
|
||||
# china_list.txt) are rendered from those files, and the client rule-sets are
|
||||
# loaded from the two .srs files, so a downloaded list only takes effect after
|
||||
# they have been regenerated. Without this the daily cron update rewrote the
|
||||
# files on disk and nothing else.
|
||||
[ "$changed" -eq 0 ] || /etc/init.d/homeproxy refresh_lists
|
||||
|
||||
# update_subscriptions.uc restarts the service itself when it changed the UCI
|
||||
# configuration.
|
||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
||||
@@ -35,12 +35,6 @@ check_list_update() {
|
||||
return 2
|
||||
fi
|
||||
|
||||
# --header= is the only header switch understood by all three wget
|
||||
# implementations this runs on (uclient-fetch, GNU wget, busybox wget);
|
||||
# the previous --header-file= is rejected by uclient-fetch and GNU wget
|
||||
# alike, which made every version probe fail whenever a GitHub token was
|
||||
# configured. The token is therefore visible in `ps` for the duration of
|
||||
# the request; that is the trade-off for a single code path.
|
||||
local api_url="https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1"
|
||||
local list_info
|
||||
if [ -n "$github_token" ]; then
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
|
||||
import { parse_uri } from 'parse_uri';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
@@ -44,9 +43,7 @@ if (routing_mode !== 'custom') {
|
||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||
}
|
||||
/* UCI config end */
|
||||
|
||||
/* String helper start */
|
||||
function filter_check(name) {
|
||||
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
|
||||
return false;
|
||||
@@ -68,9 +65,7 @@ function filter_check(name) {
|
||||
|
||||
return ret;
|
||||
}
|
||||
/* String helper end */
|
||||
|
||||
/* Common var start */
|
||||
const node_cache = {},
|
||||
node_result = [];
|
||||
|
||||
@@ -78,9 +73,7 @@ const ubus = connect();
|
||||
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
||||
if (isEmpty(sing_features))
|
||||
log('Warning: Failed to query sing-box features via ubus, assuming defaults.');
|
||||
/* Common var end */
|
||||
|
||||
/* Log */
|
||||
system(`mkdir -p ${RUN_DIR}`);
|
||||
function log(...args) {
|
||||
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
|
||||
@@ -88,20 +81,10 @@ function log(...args) {
|
||||
logfile.close();
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscription URLs usually carry the provider token in the query string;
|
||||
* the log is surfaced in LuCI, so keep it out.
|
||||
*/
|
||||
function redact(url) {
|
||||
return replace(url, /[?#].*$/, '');
|
||||
}
|
||||
|
||||
/*
|
||||
* mkdir is the lock: atomic everywhere this runs and no flock binding needed.
|
||||
* A lock older than LOCK_STALE is broken, so a killed run cannot block every
|
||||
* later update. Two concurrent runs (cron + the LuCI button) used to
|
||||
* interleave commits and service restarts.
|
||||
*/
|
||||
const LOCK_DIR = RUN_DIR + '/update_subscriptions.lock';
|
||||
const LOCK_STALE = 600;
|
||||
|
||||
@@ -126,11 +109,6 @@ function release_lock() {
|
||||
system(sprintf('rmdir %s 2>/dev/null', shellQuote(LOCK_DIR)));
|
||||
}
|
||||
|
||||
/*
|
||||
* sing-box rejects the whole generated config over one malformed node, and the
|
||||
* generator keeps the old config when it fails, so a node that cannot possibly
|
||||
* work must not reach UCI. Returns the reason, or null when the node is usable.
|
||||
*/
|
||||
function node_problem(cfg) {
|
||||
if (isEmpty(cfg.address) || isEmpty(cfg.port))
|
||||
return 'missing address or port';
|
||||
@@ -199,12 +177,9 @@ function main() {
|
||||
try {
|
||||
nodes = json(res).servers || json(res);
|
||||
|
||||
/* Shadowsocks SIP008 format */
|
||||
if (nodes[0].server && nodes[0].method)
|
||||
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
||||
} catch(e) {
|
||||
/* Clash/mihomo subscriptions are YAML, not base64; say so instead
|
||||
of reporting a base64/JSON failure followed by "no valid node". */
|
||||
if (match(res, /(^|\n)[ \t]*(proxies|proxy-groups)[ \t]*:/)) {
|
||||
log(sprintf('Unsupported subscription format (Clash/mihomo YAML) at %s.', shown_url));
|
||||
continue;
|
||||
@@ -222,9 +197,6 @@ function main() {
|
||||
|
||||
const config = parse_uri(node, sing_features, log);
|
||||
if (isEmpty(config)) {
|
||||
/* No credentials in the log: only the scheme says whether a
|
||||
parser is missing or the entry is simply not a node. Only
|
||||
the first few are printed, the rest is summarized below. */
|
||||
if (skipped < 5)
|
||||
log(sprintf('Skipping unparsable entry (%s) from %s.',
|
||||
type(node) === 'string' ? replace(node, /:.*$/, '') : type(node), shown_url));
|
||||
@@ -288,11 +260,9 @@ function main() {
|
||||
|
||||
let added = 0, removed = 0, changed = false;
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* Nodes created by the user */
|
||||
if (!cfg.grouphash)
|
||||
return null;
|
||||
|
||||
/* Empty object - failed to fetch nodes, or subscription URL not yet processed */
|
||||
if (!node_cache[cfg.grouphash] || length(node_cache[cfg.grouphash]) === 0)
|
||||
return null;
|
||||
|
||||
@@ -304,14 +274,10 @@ function main() {
|
||||
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
||||
} else {
|
||||
map(keys(cfg), (v) => {
|
||||
/* skip the cursor's own metadata (.name/.type/...): deleting
|
||||
those is a no-op and would mark every run as changed */
|
||||
if (substr(v, 0, 1) === '.')
|
||||
return null;
|
||||
|
||||
if (v in node_cache[cfg.grouphash][cfg['.name']]) {
|
||||
/* only write what actually differs, so an unchanged
|
||||
subscription does not fake a change below */
|
||||
const fresh = node_cache[cfg.grouphash][cfg['.name']][v];
|
||||
const current = uci.get(uciconfig, cfg['.name'], v);
|
||||
if (type(fresh) === 'array' ? (sprintf('%J', uci.get_all(uciconfig, cfg['.name'], v) || []) !== sprintf('%J', fresh))
|
||||
@@ -342,9 +308,6 @@ function main() {
|
||||
});
|
||||
uci.commit(uciconfig);
|
||||
|
||||
/* via_proxy keeps the service up during the fetch, but a configuration
|
||||
change still has to be applied - it used to be committed and then
|
||||
ignored until the next restart. */
|
||||
let need_restart = (via_proxy !== '1') || changed;
|
||||
if (!isEmpty(main_node)) {
|
||||
const first_server = uci.get_first(uciconfig, ucinode);
|
||||
@@ -410,7 +373,6 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrub stale urltest member references in custom routing nodes */
|
||||
uci.foreach(uciconfig, 'routing_node', (cfg) => {
|
||||
if (cfg.node !== 'urltest' || isEmpty(cfg.urltest_nodes))
|
||||
return null;
|
||||
|
||||
Reference in new issue
Block a user