⛄ Sync 2026-09-15 17:04:17

This commit is contained in:
github-actions[bot] committed 2026-09-15 17:04:17 +08:00
1 parent 569d3c8c73
commit 6840e76cc4
62 files changed
+16332 -19628

No files matched your search

@@ -14,6 +14,6 @@ while true; do
sleep 180
for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do
[ -s "$i" ] || continue
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
[ "$(( $(wc -c < "$i") / 1024 >= log_max_size))" -eq "0" ] || : > "$i"
done
done
@@ -1,9 +1,20 @@
#!/usr/bin/utpl -S
{%-
/*
* Declares every homeproxy_* fw4 chain/set. The inventory of those objects is
* tracked in scripts/fw4_names.sh, which init.d/homeproxy uses to clean them up
* on stop; add/remove entries there together with any change below.
* tests/ucode/test_fw4_names.sh compares both lists, and
* tests/ucode/test_firewall_template.sh renders this template and checks the
* objects actually survive into the generated nftables script.
*
* Keep this comment inside the ucode block: text placed before `{%-` has its
* trailing newline trimmed, which would glue it onto the first nft statement.
*/
import { readfile } from 'fs';
import { cursor } from 'uci';
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
import { isEmpty, isValidCIDR } from '/etc/homeproxy/scripts/homeproxy.uc';
const fw4 = require('fw4');
@@ -25,7 +36,24 @@ function resolve_ipv6(str) {
if (ipv6.bits > -1)
return `${ipv6.addr}/${ipv6.bits}`;
else
return `& ${ipv6.mask} == ${ipv6.addr}`;
return ipv6.addr;
}
/* Normalize a list of IPv6 entries into an nftables anonymous set
(or null when empty), so IPv6 ACLs match in a single set lookup
instead of one rule per address. */
function ipv6_to_nftarr(list) {
if (isEmpty(list))
return null;
let out = [];
for (let ip in list) {
const r = resolve_ipv6(ip);
if (!isEmpty(r))
push(out, r);
}
return isEmpty(out) ? null : `{ ${join(', ', out)} }`;
}
function resolve_mark(str) {
@@ -182,7 +210,9 @@ set homeproxy_mainland_addr_v4 {
auto-merge
elements = {
{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %}
{% if (isValidCIDR(cnip4, 4)): %}
{{ cnip4 }},
{% endif %}
{% endfor %}
}
}
@@ -193,7 +223,9 @@ set homeproxy_mainland_addr_v6 {
auto-merge
elements = {
{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %}
{% if (isValidCIDR(cnip6, 6)): %}
{{ cnip6 }},
{% endif %}
{% endfor %}
}
}
@@ -286,9 +318,9 @@ chain homeproxy_redirect_lanac {
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect
{% endif /* lan_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect
{% endfor /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_proxy_ipv6_ips) }} counter goto homeproxy_redirect
{% endif /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect
{% endif /* lan_proxy_mac_addrs */ %}
@@ -296,9 +328,9 @@ chain homeproxy_redirect_lanac {
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
{% endif /* lan_direct_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
{% endfor /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_direct_ipv6_ips) }} counter return
{% endif /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
{% endif /* lan_direct_mac_addrs */ %}
@@ -326,9 +358,9 @@ chain homeproxy_redirect {
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port
{% endif /* lan_global_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port
{% endfor /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_global_proxy_ipv6_ips) }} counter goto homeproxy_redirect_proxy_port
{% endif /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port
{% endif /* lan_global_proxy_mac_addrs */ %}
@@ -359,9 +391,9 @@ chain homeproxy_redirect {
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy
{% endif /* lan_gaming_mode_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_gaming_mode_ipv6_ips) }} counter goto homeproxy_redirect_proxy
{% endif /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy
{% endif /* lan_gaming_mode_mac_addrs */ %}
@@ -409,9 +441,9 @@ chain homeproxy_mangle_lanac {
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting
{% endif /* lan_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting
{% endfor /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_proxy_ipv6_ips) }} counter goto homeproxy_mangle_prerouting
{% endif /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting
{% endif /* lan_proxy_mac_addrs */ %}
@@ -419,9 +451,9 @@ chain homeproxy_mangle_lanac {
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
{% endif /* lan_direct_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
{% endfor /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_direct_ipv6_ips) }} counter return
{% endif /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
{% endif /* lan_direct_mac_addrs */ %}
@@ -447,9 +479,9 @@ chain homeproxy_mangle_prerouting {
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port
{% endif /* lan_global_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port
{% endfor /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_global_proxy_ipv6_ips) }} counter goto homeproxy_mangle_tproxy_port
{% endif /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port
{% endif /* lan_global_proxy_mac_addrs */ %}
@@ -465,14 +497,14 @@ chain homeproxy_mangle_prerouting {
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode !== 'custom'): %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
{% endif /* routing_mode */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
@@ -484,9 +516,9 @@ chain homeproxy_mangle_prerouting {
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy
{% endif /* lan_gaming_mode_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_gaming_mode_ipv6_ips) }} counter goto homeproxy_mangle_tproxy
{% endif /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy
{% endif /* lan_gaming_mode_mac_addrs */ %}
@@ -555,9 +587,9 @@ chain homeproxy_mangle_lanac {
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun
{% endif /* lan_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun
{% endfor /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_proxy_ipv6_ips) }} counter goto homeproxy_mangle_tun
{% endif /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun
{% endif /* lan_proxy_mac_addrs */ %}
@@ -565,9 +597,9 @@ chain homeproxy_mangle_lanac {
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
{% endif /* lan_direct_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
{% endfor /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_direct_ipv6_ips) }} counter return
{% endif /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
{% endif /* lan_direct_mac_addrs */ %}
@@ -606,9 +638,9 @@ chain homeproxy_mangle_tun {
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark
{% endif /* lan_global_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark
{% endfor /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_global_proxy_ipv6_ips) }} counter goto homeproxy_mangle_tun_mark
{% endif /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark
{% endif /* lan_global_proxy_mac_addrs */ %}
@@ -626,14 +658,14 @@ chain homeproxy_mangle_tun {
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode !== 'custom'): %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
{% endif /* routing_mode */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
@@ -645,9 +677,9 @@ chain homeproxy_mangle_tun {
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter meta mark set {{ tun_mark }}
{% endif /* lan_gaming_mode_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter meta mark set {{ tun_mark }}
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv6_ips)): %}
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_gaming_mode_ipv6_ips) }} counter meta mark set {{ tun_mark }}
{% endif /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter meta mark set {{ tun_mark }}
{% endif /* lan_gaming_mode_mac_addrs */ %}
@@ -4,7 +4,7 @@
import { writefile } from 'fs';
import { cursor } from 'uci';
import { isEmpty, RUN_DIR } from 'homeproxy';
import { isEmpty, RUN_DIR, validation } from 'homeproxy';
const cfgname = 'homeproxy';
const uci = cursor();
@@ -39,7 +39,17 @@ if (server_enabled === '1') {
if (s.enabled !== '1' || s.firewall !== '1')
return;
if (!validation('port', s.port)) {
print(`WARN: skipping server ${s['.name']}: invalid port "${s.port}".`);
return;
}
let proto = s.network || '{ tcp, udp }';
if (!(proto in ['tcp', 'udp', '{ tcp, udp }'])) {
print(`WARN: skipping server ${s['.name']}: invalid network "${proto}".`);
return;
}
push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`);
});
}
@@ -0,0 +1,48 @@
# 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
homeproxy_redirect
homeproxy_redirect_proxy
homeproxy_redirect_proxy_port
homeproxy_redirect_lanac
homeproxy_mangle_prerouting
homeproxy_mangle_output
homeproxy_mangle_tproxy
homeproxy_mangle_tproxy_port
homeproxy_mangle_lanac
homeproxy_mangle_mark
homeproxy_mangle_tun
homeproxy_mangle_tun_mark
"
HP_FW4_SETS="
homeproxy_local_addr_v4
homeproxy_local_addr_v6
homeproxy_gfw_list_v4
homeproxy_gfw_list_v6
homeproxy_mainland_addr_v4
homeproxy_mainland_addr_v6
homeproxy_wan_proxy_addr_v4
homeproxy_wan_proxy_addr_v6
homeproxy_wan_direct_addr_v4
homeproxy_wan_direct_addr_v6
homeproxy_routing_port
"
File diff suppressed because it is too large. Load diff
@@ -11,8 +11,8 @@ import { writefile } from 'fs';
import { cursor } from 'uci';
import {
isEmpty, strToBool, strToInt, strToTime,
removeBlankAttrs, HP_DIR, RUN_DIR
strToBool, strToInt, strToTime,
removeBlankAttrs, buildTLSObject, buildTransportObject, HP_DIR, RUN_DIR
} from 'homeproxy';
/* UCI config start */
@@ -42,6 +42,26 @@ 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',
tag: 'cfg-' + cfg['.name'] + '-in',
listen: cfg.address || '::',
listen_port: strToInt(cfg.port),
bind_interface: cfg.bind_interface,
reuse_addr: strToBool(cfg.reuse_addr),
tcp_fast_open: strToBool(cfg.tcp_fast_open),
tcp_multi_path: strToBool(cfg.tcp_multi_path),
version: strToInt(cfg.snell_version) || 5,
psk: cfg.password,
obfs_mode: cfg.snell_obfs_mode,
mode: cfg.snell_mode
});
return;
}
push(config.inbounds, {
type: cfg.type,
tag: 'cfg-' + cfg['.name'] + '-in',
@@ -59,17 +79,15 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
/* AnyTLS */
padding_scheme: cfg.anytls_padding_scheme,
/* Hysteria */
/* Hysteria (2) */
up_mbps: strToInt(cfg.hysteria_up_mbps),
down_mbps: strToInt(cfg.hysteria_down_mbps),
obfs: cfg.hysteria_obfs_type ? {
type: cfg.hysteria_obfs_type,
password: cfg.hysteria_obfs_password
password: cfg.hysteria_obfs_password,
min_packet_size: strToInt(cfg.hysteria_obfs_min_packet_size),
max_packet_size: strToInt(cfg.hysteria_obfs_max_packet_size)
} : cfg.hysteria_obfs_password,
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
recv_window_client: strToInt(cfg.hysteria_recv_window_client),
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
masquerade: cfg.hysteria_masquerade,
@@ -113,73 +131,22 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
} : null
} : null,
tls: (cfg.tls === '1') ? {
enabled: true,
server_name: cfg.tls_sni,
alpn: cfg.tls_alpn,
min_version: cfg.tls_min_version,
max_version: cfg.tls_max_version,
cipher_suites: cfg.tls_cipher_suites,
certificate_path: cfg.tls_cert_path,
key_path: cfg.tls_key_path,
acme: (cfg.tls_acme === '1') ? {
domain: cfg.tls_acme_domain,
data_directory: HP_DIR + '/certs',
default_server_name: cfg.tls_acme_dsn,
email: cfg.tls_acme_email,
provider: cfg.tls_acme_provider,
disable_http_challenge: strToBool(cfg.tls_acme_dhc),
disable_tls_alpn_challenge: (cfg.tls_acme_dtac),
alternative_http_port: strToInt(cfg.tls_acme_ahp),
alternative_tls_port: strToInt(cfg.tls_acme_atp),
external_account: (cfg.tls_acme_external_account === '1') ? {
key_id: cfg.tls_acme_ea_keyid,
mac_key: cfg.tls_acme_ea_mackey
} : null,
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
provider: cfg.tls_dns01_provider,
access_key_id: cfg.tls_dns01_ali_akid,
access_key_secret: cfg.tls_dns01_ali_aksec,
region_id: cfg.tls_dns01_ali_rid,
api_token: cfg.tls_dns01_cf_api_token
} : null
} : null,
ech: (cfg.tls_ech_key) ? {
enabled: true,
key: split(cfg.tls_ech_key, '\n'),
// config: split(cfg.tls_ech_config, '\n')
} : null,
reality: (cfg.tls_reality === '1') ? {
enabled: true,
private_key: cfg.tls_reality_private_key,
short_id: cfg.tls_reality_short_id,
max_time_difference: strToTime(cfg.tls_reality_max_time_difference),
handshake: {
server: cfg.tls_reality_server_addr,
server_port: strToInt(cfg.tls_reality_server_port)
}
} : null
} : null,
tls: buildTLSObject(cfg, true),
transport: !isEmpty(cfg.transport) ? {
type: cfg.transport,
host: cfg.http_host || cfg.httpupgrade_host,
path: cfg.http_path || cfg.ws_path,
headers: cfg.ws_host ? {
Host: cfg.ws_host
} : null,
method: cfg.http_method,
max_early_data: strToInt(cfg.websocket_early_data),
early_data_header_name: cfg.websocket_early_data_header,
service_name: cfg.grpc_servicename,
idle_timeout: strToTime(cfg.http_idle_timeout),
ping_timeout: strToTime(cfg.http_ping_timeout)
} : null
transport: buildTransportObject(cfg, true)
});
});
if (length(config.inbounds) === 0)
exit(1);
config['$schema'] = 'https://sing-box.sagernet.org/schema.json';
system('mkdir -p ' + RUN_DIR);
writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config)));
const server_tmp = RUN_DIR + '/sing-box-s.json.tmp';
writefile(server_tmp, sprintf('%.J\n', removeBlankAttrs(config)));
if (system('/usr/bin/sing-box check --config ' + server_tmp) !== 0) {
system('rm -f ' + server_tmp);
exit(1);
}
system('mv -f ' + server_tmp + ' ' + RUN_DIR + '/sing-box-s.json');
+278 -41
View File
@@ -26,30 +26,55 @@ 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 */
}
}
};
export function executeCommand(...args) {
let outfd = mkstemp();
let errfd = mkstemp();
let outfd = null, errfd = null;
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
try {
outfd = mkstemp();
errfd = mkstemp();
outfd.seek(0);
errfd.seek(0);
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
const stdout = outfd.read(1024 * 512) ?? '';
const stderr = errfd.read(1024 * 512) ?? '';
outfd.seek(0);
errfd.seek(0);
outfd.close();
errfd.close();
const stdout = outfd.read(1024 * 512) ?? '';
const stderr = errfd.read(1024 * 512) ?? '';
const binary = isBinary(stdout);
const binary = isBinary(stdout);
return {
command: join(' ', args),
stdout: binary ? null : stdout,
stderr,
exitcode,
binary
};
/* mkstemp() returns delete-on-close files, so closing is enough */
closeFD(outfd);
closeFD(errfd);
return {
command: join(' ', args),
stdout: binary ? null : stdout,
stderr,
exitcode,
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);
die(e);
}
};
export function getTime(epoch) {
@@ -66,34 +91,35 @@ export function getTime(epoch) {
};
export function wGETResponse(url, ua) {
/*
* 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.
*/
export function wGETVerbose(url, ua) {
if (!url || type(url) !== 'string')
return null;
return { content: null, error: 'invalid URL' };
if (!ua)
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
const output = executeCommand(`/usr/bin/wget -O- -S --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
return {
body: trim(output.stdout),
headers: output.stderr || '',
exitcode: output.exitcode
};
/* -nv (not -q) so wget still reports *why* a fetch failed on stderr */
const output = executeCommand(`/usr/bin/wget -nv -O- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
if (output.exitcode !== 0) {
let reason = trim(output.stderr || '');
reason = reason ? replace(reason, /\s+/g, ' ') : 'no error output';
if (length(reason) > 200)
reason = substr(reason, 0, 200) + '...';
return { content: null, error: `wget exited with status ${output.exitcode}: ${reason}` };
}
return { content: trim(output.stdout), error: null };
};
export function wGET(url, ua) {
return wGETResponse(url, ua)?.body;
};
export function wGETHeaders(url, ua) {
if (!url || type(url) !== 'string')
return null;
if (!ua)
ua = 'clash.meta';
const output = executeCommand(`/usr/bin/wget -O /dev/null -S --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
return output.stderr || '';
return wGETVerbose(url, ua).content;
};
/* Utilities end */
@@ -103,7 +129,7 @@ export function isEmpty(res) {
};
export function strToBool(str) {
return (str === '1') || null;
return str === '1' ? true : (str === '0' ? false : null);
};
export function strToInt(str) {
@@ -111,7 +137,11 @@ export function strToInt(str) {
};
export function strToTime(str) {
return !isEmpty(str) ? (str + 's') : null;
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');
};
export function removeBlankAttrs(res) {
@@ -152,6 +182,77 @@ export function validation(datatype, data) {
const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
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 */
const parts = split(addr, '/');
const ip = parts[0];
const prefix = parts[1];
/* 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) < 0 || 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;
for (let g in groups)
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 === '')
continue;
for (let g in split(part, ':')) {
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
return false;
count++;
}
}
if (count > 7)
return false;
}
if (prefix && (int(prefix) < 0 || int(prefix) > 128))
return false;
} else {
return false;
}
return true;
};
/* String helper end */
/* String parser start */
@@ -160,8 +261,8 @@ export function decodeBase64Str(str) {
return null;
str = trim(str);
str = replace(str, '_', '/');
str = replace(str, '-', '+');
str = replace(str, /_/g, '/');
str = replace(str, /-/g, '+');
const padding = length(str) % 4;
if (padding)
@@ -247,3 +348,139 @@ 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;
return {
enabled: true,
server_name: cfg.tls_sni,
insecure: is_server ? null : strToBool(cfg.tls_insecure),
alpn: cfg.tls_alpn,
min_version: cfg.tls_min_version,
max_version: cfg.tls_max_version,
handshake_timeout: is_server ? null : strToTime(cfg.tls_handshake_timeout),
cipher_suites: cfg.tls_cipher_suites,
certificate_path: cfg.tls_cert_path,
key_path: is_server ? cfg.tls_key_path : null,
certificate_provider: (is_server && cfg.tls_acme === '1') ? {
type: 'acme',
domain: (type(cfg.tls_acme_domain) === 'array') ? cfg.tls_acme_domain
: (isEmpty(cfg.tls_acme_domain) ? [] : [cfg.tls_acme_domain]),
data_directory: HP_DIR + '/certs',
default_server_name: cfg.tls_acme_dsn,
email: cfg.tls_acme_email,
provider: cfg.tls_acme_provider,
account_key: cfg.tls_acme_account_key,
key_type: cfg.tls_acme_key_type,
profile: cfg.tls_acme_profile,
disable_http_challenge: strToBool(cfg.tls_acme_dhc),
disable_tls_alpn_challenge: strToBool(cfg.tls_acme_dtac),
alternative_http_port: strToInt(cfg.tls_acme_ahp),
alternative_tls_port: strToInt(cfg.tls_acme_atp),
external_account: (cfg.tls_acme_external_account === '1') ? {
key_id: cfg.tls_acme_ea_keyid,
mac_key: cfg.tls_acme_ea_mackey
} : null,
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
provider: cfg.tls_dns01_provider,
access_key_id: cfg.tls_dns01_ali_akid,
access_key_secret: cfg.tls_dns01_ali_aksec,
region_id: cfg.tls_dns01_ali_rid,
api_token: cfg.tls_dns01_cf_api_token
} : null
} : null,
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,
config_path: cfg.tls_ech_config_path
} : null),
utls: (is_server || isEmpty(cfg.tls_utls)) ? null : {
enabled: true,
fingerprint: cfg.tls_utls
},
reality: (cfg.tls_reality !== '1') ? null : (is_server ? {
enabled: true,
private_key: cfg.tls_reality_private_key,
short_id: cfg.tls_reality_short_id,
max_time_difference: strToTime(cfg.tls_reality_max_time_difference),
handshake: {
server: cfg.tls_reality_server_addr,
server_port: strToInt(cfg.tls_reality_server_port)
}
} : {
enabled: true,
public_key: cfg.tls_reality_public_key,
short_id: cfg.tls_reality_short_id
})
};
};
/* 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;
return {
type: cfg.transport,
host: cfg.http_host || cfg.httpupgrade_host,
path: cfg.http_path || cfg.ws_path,
headers: cfg.ws_host ? {
Host: cfg.ws_host
} : null,
method: cfg.http_method,
max_early_data: strToInt(cfg.websocket_early_data),
early_data_header_name: cfg.websocket_early_data_header,
service_name: cfg.grpc_servicename,
idle_timeout: strToTime(cfg.http_idle_timeout),
ping_timeout: strToTime(cfg.http_ping_timeout),
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;
const beg = is_private_key ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
end = is_private_key ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
lines = split(trim(content), /[\r\n]/);
let start = false, i;
for (i = 0; i < length(lines); i++) {
if (match(lines[i], beg))
start = true;
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
break;
}
if (!start || i < length(lines) - 1 || !match(lines[i], end))
return false;
return true;
};
/* PEM validation end */
@@ -7,34 +7,14 @@
'use strict';
import { popen } from 'fs';
import { cursor } from 'uci';
import { isEmpty, parseURL } from 'homeproxy';
import { isEmpty, parseURL, validation } from 'homeproxy';
const uci = cursor();
const uciconfig = 'homeproxy';
uci.load(uciconfig);
function random_secret() {
const fd = popen('tr -dc "A-Za-z0-9" < /dev/urandom 2>/dev/null | head -c 16');
let random = '';
if (fd) {
random = trim(fd.read('all') || '');
fd.close();
}
if (isEmpty(random)) {
const fallback = popen('awk \'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}\'');
if (fallback) {
random = trim(fallback.read('all') || '');
fallback.close();
}
}
return random;
}
const uciinfra = 'infra',
ucimigration = 'migration',
ucimain = 'config',
@@ -45,16 +25,8 @@ const uciinfra = 'infra',
ucirouting = 'routing',
uciroutingnode = 'routing_node',
uciroutingrule = 'routing_rule',
uciclashapi = 'clash_api',
ucintp = 'ntp',
ucicache = 'cache',
uciserver = 'server';
function is_update_cron(value) {
let matched = match(value || '', /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
return !isEmpty(matched) && int(matched[1]) <= 59 && int(matched[2]) <= 23;
}
/* chinadns-ng has been removed */
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
uci.delete(uciconfig, uciinfra, 'china_dns_port');
@@ -64,7 +36,9 @@ 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]);
} else {
if (match(china_dns_server, /,/))
if (china_dns_server === 'wan_114')
uci.set(uciconfig, ucimain, 'china_dns_server', '114.114.114.114');
else if (match(china_dns_server, /,/))
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
}
@@ -83,93 +57,6 @@ if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
uci.delete(uciconfig, uciinfra, 'tun_gso');
/* endpoint_independent_nat was removed in sing-box 1.13 */
if (!isEmpty(uci.get(uciconfig, ucirouting, 'endpoint_independent_nat')))
uci.delete(uciconfig, ucirouting, 'endpoint_independent_nat');
/* direct outbound proxy_protocol was removed in sing-box 1.13 */
uci.foreach(uciconfig, ucinode, (cfg) => {
if (cfg.type === 'direct' && !isEmpty(cfg.proxy_protocol))
uci.delete(uciconfig, cfg['.name'], 'proxy_protocol');
});
const legacy_panel_url = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip';
const legacy_panel_proxy_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip';
const old_panel_url = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
const old_panel_proxy_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
const default_panel_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
/* clash api panel options were introduced */
if (!uci.get(uciconfig, uciclashapi))
uci.set(uciconfig, uciclashapi, uciconfig);
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_ui')))
uci.set(uciconfig, uciclashapi, 'external_ui', '/etc/homeproxy/run/ui');
const panel_url = uci.get(uciconfig, uciclashapi, 'external_ui_download_url');
if (isEmpty(panel_url) || panel_url === legacy_panel_url || panel_url === legacy_panel_proxy_url || panel_url === old_panel_proxy_url)
uci.set(uciconfig, uciclashapi, 'external_ui_download_url', default_panel_url);
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_ui_download_detour')))
uci.set(uciconfig, uciclashapi, 'external_ui_download_detour', 'direct-out');
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_controller')))
uci.set(uciconfig, uciclashapi, 'external_controller', '0.0.0.0:9095');
if (isEmpty(uci.get(uciconfig, uciclashapi, 'secret')) || uci.get(uciconfig, uciclashapi, 'secret') === 'homeproxy') {
let random = random_secret();
if (!isEmpty(random))
uci.set(uciconfig, uciclashapi, 'secret', random);
}
if (isEmpty(uci.get(uciconfig, uciclashapi, 'default_mode')))
uci.set(uciconfig, uciclashapi, 'default_mode', 'rule');
/* ntp options were moved into a dedicated section */
if (!uci.get(uciconfig, ucintp))
uci.set(uciconfig, ucintp, uciconfig);
const legacy_ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server');
if (isEmpty(uci.get(uciconfig, ucintp, 'enabled')))
uci.set(uciconfig, ucintp, 'enabled', '1');
if (isEmpty(uci.get(uciconfig, ucintp, 'server')))
uci.set(uciconfig, ucintp, 'server', !isEmpty(legacy_ntp_server) ? legacy_ntp_server : 'ntp.aliyun.com');
if (isEmpty(uci.get(uciconfig, ucintp, 'server_port')))
uci.set(uciconfig, ucintp, 'server_port', '123');
if (isEmpty(uci.get(uciconfig, ucintp, 'interval')))
uci.set(uciconfig, ucintp, 'interval', '30m');
/* cache file options were moved into a dedicated section */
if (!uci.get(uciconfig, ucicache))
uci.set(uciconfig, ucicache, uciconfig);
if (isEmpty(uci.get(uciconfig, ucicache, 'enabled')))
uci.set(uciconfig, ucicache, 'enabled', '1');
const old_cache_path = '/var/run/homeproxy/cache.db';
const default_cache_path = '/etc/homeproxy/cache.db';
const cache_path = uci.get(uciconfig, ucicache, 'path');
if (isEmpty(cache_path) || cache_path === old_cache_path)
uci.set(uciconfig, ucicache, 'path', default_cache_path);
if (!isEmpty(uci.get(uciconfig, ucicache, 'store_fakeip')))
uci.delete(uciconfig, ucicache, 'store_fakeip');
if (isEmpty(uci.get(uciconfig, ucicache, 'store_rdrc')))
uci.set(uciconfig, ucicache, 'store_rdrc', isEmpty(uci.get(uciconfig, ucidns, 'cache_file_store_rdrc')) ? '1' : uci.get(uciconfig, ucidns, 'cache_file_store_rdrc'));
if (isEmpty(uci.get(uciconfig, ucicache, 'rdrc_timeout')) && !isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
uci.set(uciconfig, ucicache, 'rdrc_timeout', uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout'));
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_store_rdrc')))
uci.delete(uciconfig, ucidns, 'cache_file_store_rdrc');
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
uci.delete(uciconfig, ucidns, 'cache_file_rdrc_timeout');
/* create migration section */
if (!uci.get(uciconfig, ucimigration))
uci.set(uciconfig, ucimigration, uciconfig);
@@ -177,7 +64,7 @@ if (!uci.get(uciconfig, ucimigration))
/* 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"');
system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>/dev/null');
uci.set(uciconfig, ucimigration, 'crontab', '1');
}
@@ -188,10 +75,6 @@ if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
uci.set(uciconfig, uciserver, 'log_level', 'warn');
if (!isEmpty(uci.get(uciconfig, 'subscription', 'auto_update_time')) &&
!is_update_cron(uci.get(uciconfig, 'subscription', 'auto_update_time')))
uci.set(uciconfig, 'subscription', 'auto_update_time', '0 0 * * *');
/* empty value defaults to all ports now */
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
uci.delete(uciconfig, ucimain, 'routing_port');
@@ -211,6 +94,8 @@ if (default_dns_server === 'block-dns') {
uci.set(uciconfig, '_migration_dns_final_block', 'action', 'reject');
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
}
else if (default_dns_server === 'local-dns')
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
const dns_server_migration = {};
/* DNS servers options */
@@ -288,7 +173,7 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
uci.set(uciconfig, ucirouting, 'default_outbound_dns', cfg.server);
break;
default:
uci.set(uciconfig, cfg.outbound, 'domain_resolver', cfg.server);
uci.set(uciconfig, outbound, 'domain_resolver', cfg.server);
break;
}
});
@@ -338,6 +223,29 @@ uci.foreach(uciconfig, ucinode, (cfg) => {
/* 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');
const legacy_store_rdrc = uci.get(uciconfig, ucidns, 'cache_file_store_rdrc');
if (!isEmpty(legacy_store_rdrc)) {
uci.set(uciconfig, ucidns, 'cache_file_store_dns', legacy_store_rdrc);
uci.delete(uciconfig, ucidns, 'cache_file_store_rdrc');
}
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
uci.delete(uciconfig, ucidns, 'cache_file_rdrc_timeout');
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
if (!isEmpty(cfg.domain_strategy))
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
if (!isEmpty(cfg.rule_set_ip_cidr_accept_empty))
uci.delete(uciconfig, cfg['.name'], 'rule_set_ip_cidr_accept_empty');
});
/* routing rules options */
@@ -356,47 +264,6 @@ uci.foreach(uciconfig, uciroutingrule, (cfg) => {
}
});
/* routing node options */
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
if (cfg.node === 'urltest' && !isEmpty(cfg.urltest_nodes)) {
let custom_nodes = [],
subscription_nodes = [];
for (let node in (type(cfg.urltest_nodes) === 'array' ? cfg.urltest_nodes : [ cfg.urltest_nodes ])) {
let node_cfg = uci.get_all(uciconfig, node);
if (isEmpty(node_cfg))
continue;
if (!isEmpty(node_cfg.grouphash))
push(subscription_nodes, node);
else
push(custom_nodes, node);
}
if (isEmpty(cfg.selected_nodes) && !isEmpty(custom_nodes))
uci.set(uciconfig, cfg['.name'], 'selected_nodes', custom_nodes);
if (isEmpty(cfg.subscription_nodes) && !isEmpty(subscription_nodes))
uci.set(uciconfig, cfg['.name'], 'subscription_nodes', subscription_nodes);
uci.delete(uciconfig, cfg['.name'], 'urltest_nodes');
}
});
/* rule set options */
uci.foreach(uciconfig, uciruleset, (cfg) => {
if (isEmpty(cfg.tag))
uci.set(uciconfig, cfg['.name'], 'tag', 'cfg-' + cfg['.name'] + '-rule');
if (cfg.type === 'remote') {
if (isEmpty(cfg.auto_update))
uci.set(uciconfig, cfg['.name'], 'auto_update', '1');
if (!is_update_cron(cfg.update_interval))
uci.set(uciconfig, cfg['.name'], 'update_interval', '0 0 * * *');
}
});
/* server options */
/* auto_firewall was moved into server options */
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
@@ -404,13 +271,6 @@ if (!isEmpty(auto_firewall))
uci.delete(uciconfig, uciserver, 'auto_firewall');
uci.foreach(uciconfig, uciserver, (cfg) => {
if (!isEmpty(cfg.hysteria_revc_window_client) && isEmpty(cfg.hysteria_recv_window_client)) {
uci.set(uciconfig, cfg['.name'], 'hysteria_recv_window_client', cfg.hysteria_revc_window_client);
}
if (!isEmpty(cfg.hysteria_revc_window_client))
uci.delete(uciconfig, cfg['.name'], 'hysteria_revc_window_client');
/* auto_firewall was moved into server options */
if (auto_firewall === '1')
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
@@ -0,0 +1,500 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023-2025 ImmortalWrt.org
*/
'use strict';
import { urldecode, urlencode } from 'luci.http';
import { decodeBase64Str, parseURL, 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.
*/
/* https://shadowsocks.org/guide/sip008.html */
export function parse_sip008_uri(uri) {
return {
label: uri.remarks,
type: 'shadowsocks',
address: uri.server,
port: uri.server_port,
shadowsocks_encrypt_method: uri.method,
password: uri.password,
shadowsocks_plugin: uri.plugin,
shadowsocks_plugin_opts: uri.plugin_opts
};
};
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
export function parse_anytls_uri(uri) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'anytls',
address: url.hostname,
port: url.port,
password: urldecode(url.username),
tls: '1',
tls_sni: params.sni,
tls_insecure: (params.insecure === '1') ? '1' : '0'
};
};
export function parse_http_uri(uri) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'http',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
tls: (uri[0] === 'https') ? '1' : '0'
};
};
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
export function parse_hysteria_uri(uri, features, log) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
if (!features.with_quic || (params.protocol && params.protocol !== 'udp')) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
if (!features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'hysteria',
address: url.hostname,
port: url.port,
hysteria_protocol: params.protocol || 'udp',
hysteria_auth_type: params.auth ? 'string' : null,
hysteria_auth_payload: params.auth,
hysteria_obfs_password: params.obfsParam,
hysteria_down_mbps: params.downmbps,
hysteria_up_mbps: params.upmbps,
tls: '1',
tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
tls_sni: params.peer,
tls_alpn: params.alpn
};
};
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
export function parse_hysteria2_uri(uri, features, log) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
if (!features.with_quic) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'hysteria2',
address: url.hostname,
port: url.port,
password: url.username ? (
urldecode(url.username + (url.password ? (':' + url.password) : ''))
) : null,
hysteria_obfs_type: params.obfs,
hysteria_obfs_password: params['obfs-password'],
tls: '1',
tls_insecure: (params.insecure === '1') ? '1' : '0',
tls_sni: params.sni
};
};
/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
export function parse_snell_uri(uri) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'snell',
address: url.hostname,
port: url.port,
password: url.username ? urldecode(url.username)
: (params.psk ? urldecode(params.psk) : null),
snell_version: params.version || '4',
snell_userkey: params.userkey,
snell_obfs_mode: (params.obfs === 'http') ? 'http' : null,
snell_obfs_host: params['obfs-host'],
snell_reuse: (params.reuse === '1') ? '1' : '0'
};
};
export function parse_socks_uri(uri) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'socks',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
socks_version: (match(uri[0], /4/)) ? '4' : '5'
};
};
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) {
if (length(ss_suri) === 2)
ss_slabel = '#' + urlencode(ss_suri[1]);
if (decodeBase64Str(ss_suri[0]))
uri[1] = decodeBase64Str(ss_suri[0]) + 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 */
ss_userinfo = [url.username, urldecode(url.password)];
else if (url.username)
/* User info encoded with base64 */
ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':', 2);
let ss_plugin, ss_plugin_opts;
if (url.search && url.searchParams.plugin) {
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];
}
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'shadowsocks',
address: url.hostname,
port: url.port,
shadowsocks_encrypt_method: ss_userinfo[0],
password: ss_userinfo[1],
shadowsocks_plugin: ss_plugin,
shadowsocks_plugin_opts: ss_plugin_opts
};
};
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
export function parse_trojan_uri(uri) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
const config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'trojan',
address: url.hostname,
port: url.port,
password: urldecode(url.username),
transport: (params.type !== 'tcp') ? params.type : null,
tls: '1',
tls_sni: params.sni
};
switch (params.type) {
case 'grpc':
config.grpc_servicename = params.serviceName;
break;
case 'ws':
config.ws_host = params.host ? urldecode(params.host) : null;
config.ws_path = params.path ? urldecode(params.path) : null;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
return config;
};
/* https://github.com/daeuniverse/dae/discussions/182 */
export function parse_tuic_uri(uri, features, log) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
if (!features.with_quic) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
return {
label: url.hash ? urldecode(url.hash) : null,
type: 'tuic',
address: url.hostname,
port: url.port,
uuid: url.username,
password: url.password ? urldecode(url.password) : null,
tuic_congestion_control: params.congestion_control,
tuic_udp_relay_mode: params.udp_relay_mode,
tls: '1',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
};
};
/* 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;
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !features.with_quic)) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
if (!features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
const config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'vless',
address: url.hostname,
port: url.port,
uuid: url.username,
transport: (params.type !== 'tcp') ? params.type : null,
tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
tls_reality: (params.security === 'reality') ? '1' : '0',
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
tls_reality_short_id: params.sid,
tls_utls: features.with_utls ? params.fp : null,
vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
};
switch (params.type) {
case 'grpc':
config.grpc_servicename = params.serviceName;
break;
case 'http':
case 'tcp':
if (params.type === 'http' || params.headerType === 'http') {
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
config.http_path = params.path ? urldecode(params.path) : null;
}
break;
case 'httpupgrade':
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
config.http_path = params.path ? urldecode(params.path) : null;
break;
case 'ws':
config.ws_host = params.host ? urldecode(params.host) : null;
config.ws_path = params.path ? urldecode(params.path) : null;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
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;
}
let payload;
try {
payload = json(decodeBase64Str(uri[1])) || {};
} catch(e) {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
}
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;
} else if (payload.net === 'quic' && ((payload.type && payload.type !== 'none') || payload.path || !features.with_quic)) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
if (!features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
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,
type: 'vmess',
address: payload.add,
port: payload.port,
uuid: payload.id,
vmess_alterid: payload.aid,
vmess_encrypt: payload.scy || 'auto',
vmess_global_padding: '1',
transport: (payload.net !== 'tcp') ? payload.net : null,
tls: (payload.tls === 'tls') ? '1' : '0',
tls_sni: payload.sni || payload.host,
tls_alpn: payload.alpn ? split(payload.alpn, ',') : null,
tls_utls: features.with_utls ? payload.fp : null
};
switch (payload.net) {
case 'grpc':
config.grpc_servicename = payload.path;
break;
case 'h2':
case 'tcp':
if (payload.net === 'h2' || payload.type === 'http') {
config.transport = 'http';
config.http_host = payload.host ? split(payload.host, ',') : null;
config.http_path = payload.path;
}
break;
case 'httpupgrade':
config.httpupgrade_host = payload.host;
config.http_path = payload.path;
break;
case 'ws':
config.ws_host = payload.host;
config.ws_path = payload.path;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
return config;
};
export function parse_uri(uri, features, log) {
if (!features) features = {};
if (!log) log = function() {};
let config;
if (type(uri) === 'object') {
if (uri.nodetype === 'sip008')
config = parse_sip008_uri(uri);
} else if (type(uri) === 'string') {
switch (split(trim(uri), '://')[0]) {
case 'anytls':
config = parse_anytls_uri(uri, features, log);
break;
case 'http':
case 'https':
config = parse_http_uri(uri, features, log);
break;
case 'hysteria':
config = parse_hysteria_uri(uri, features, log);
break;
case 'hysteria2':
case 'hy2':
config = parse_hysteria2_uri(uri, features, log);
break;
case 'snell':
config = parse_snell_uri(uri, features, log);
break;
case 'socks':
case 'socks4':
case 'socks4a':
case 'socks5':
case 'socks5h':
config = parse_socks_uri(uri, features, log);
break;
case 'ss':
config = parse_ss_uri(uri, features, log);
break;
case 'trojan':
config = parse_trojan_uri(uri, features, log);
break;
case 'tuic':
config = parse_tuic_uri(uri, features, log);
break;
case 'vless':
config = parse_vless_uri(uri, features, log);
break;
case 'vmess':
config = parse_vmess_uri(uri, features, log);
break;
}
}
if (!isEmpty(config)) {
if (config.address)
config.address = replace(config.address, /\[|\]/g, '');
if (!validation('host', config.address) || !validation('port', config.port)) {
log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
return null;
} else if (!config.label)
config.label = (validation('ip6addr', config.address) ?
`[${config.address}]` : config.address) + ':' + config.port;
}
return config;
};
@@ -1,46 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
set -eu
NAME="homeproxy"
RUN_DIR="/var/run/$NAME"
REQUEST_FILE="$RUN_DIR/provider-update-id"
LOCK_FILE="$RUN_DIR/provider-update-v2.lock"
LOG_FILE="$RUN_DIR/homeproxy.log"
PROVIDER_ID="${1:-}"
log() {
printf '%s [SUBSCRIBE] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
case "$PROVIDER_ID" in
*[!0-9a-f]*|'') exit 1 ;;
esac
[ "${#PROVIDER_ID}" -eq 32 ] || exit 1
mkdir -p "$RUN_DIR"
if ! lock -n "$LOCK_FILE" 2>/dev/null; then
log "Another provider update is running; waiting for it to finish..."
lock "$LOCK_FILE"
fi
umask 077
printf '%s\n' "$PROVIDER_ID" > "$REQUEST_FILE"
cleanup() {
rm -f "$REQUEST_FILE"
lock -u "$LOCK_FILE" 2>/dev/null || true
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
# Give the Clash API response time to reach the panel before HomeProxy restarts.
log "Updating provider $PROVIDER_ID from Clash API..."
sleep 1
if /etc/homeproxy/scripts/update_subscriptions.uc; then
log "Provider $PROVIDER_ID update completed."
else
log "Provider $PROVIDER_ID update failed."
exit 4
fi
@@ -21,65 +21,89 @@ to_upper() {
}
check_list_update() {
local LIST_FILE="$1"
local REPO_NAME="$2"
local REPO_BRANCH="$3"
local REPO_FILE="$4"
local LOCK_FILE="$RUN_DIR/update_resources-$LIST_FILE.lock"
local GITHUB_TOKEN="$(uci -q get homeproxy.config.github_token)"
local listtype="$1"
local listrepo="$2"
local listref="$3"
local listname="$4"
local lock="$RUN_DIR/update_resources-$listtype.lock"
local github_token="$(uci -q get homeproxy.config.github_token)"
local wget="wget --timeout=10 -q"
if ! lock -n "$LOCK_FILE" 2>"/dev/null"; then
log "[$(to_upper "$LIST_FILE")] A task is already running."
exec 200>"$lock"
if ! flock -n 200 &> "/dev/null"; then
log "[$(to_upper "$listtype")] A task is already running."
return 2
fi
local NEW_VER=$(curl -sL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} "https://api.github.com/repos/$REPO_NAME/releases/latest" | jsonfilter -e "@.tag_name")
if [ -z "$NEW_VER" ]; then
log "[$(to_upper "$LIST_FILE")] Failed to get the latest version, please retry later."
return 1
local github_header_file=""
if [ -n "$github_token" ]; then
github_header_file="$RUN_DIR/.gh_header_${listtype}"
( umask 077; printf 'Authorization: Bearer %s\n' "$github_token" > "$github_header_file" )
trap "[ -n \"$github_header_file\" ] && rm -f \"$github_header_file\"" EXIT INT TERM
fi
local OLD_VER=$(cat "$RESOURCES_DIR/$LIST_FILE.ver" 2>/dev/null || echo "NOT FOUND")
if [ "$OLD_VER" = "$NEW_VER" ]; then
log "[$(to_upper "$LIST_FILE")] Current version: $NEW_VER."
log "[$(to_upper "$LIST_FILE")] You're already at the latest version."
local list_info="$($wget ${github_header_file:+--header-file=$github_header_file} -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
local wget_exit=$?
[ -n "$github_header_file" ] && rm -f "$github_header_file"
trap - EXIT INT TERM
if [ $wget_exit -ne 0 ]; then
log "[$(to_upper "$listtype")] Failed to fetch version info (wget exit $wget_exit)."
return 1
fi
local list_sha="$(echo -e "$list_info" | jsonfilter -qe "@[0].sha")"
local list_date="$(echo -e "$list_info" | jsonfilter -qe "@[0].commit.committer.date" | cut -d 'T' -f1)"
if [ -z "$list_sha" ]; then
log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later."
return 1
fi
local list_ver="${list_date:+$list_date }$list_sha"
local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT_FOUND")"
local local_list_sha="${local_list_ver##* }"
local local_list_disp="${local_list_ver%% *}"
if [ "$local_list_sha" = "$list_sha" ]; then
[ "$local_list_ver" = "$local_list_sha" ] && [ -n "$list_date" ] && \
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
log "[$(to_upper "$listtype")] Current version: ${list_ver%% *}."
log "[$(to_upper "$listtype")] You're already at the latest version."
return 3
else
log "[$(to_upper "$LIST_FILE")] Local version: $OLD_VER, latest version: $NEW_VER."
log "[$(to_upper "$listtype")] Local version: $local_list_disp, latest version: ${list_ver%% *}."
fi
if ! curl -sL -o "$RUN_DIR/$REPO_FILE" "https://cdn.jsdelivr.net/gh/$REPO_NAME@$REPO_BRANCH/$REPO_FILE" || [ ! -s "$RUN_DIR/$REPO_FILE" ]; then
rm -f "$RUN_DIR/$REPO_FILE"
log "[$(to_upper "$LIST_FILE")] Update failed."
if ! $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" || [ ! -s "$RUN_DIR/$listname" ]; then
rm -f "$RUN_DIR/$listname"
log "[$(to_upper "$listtype")] Download failed."
return 1
fi
mv -f "$RUN_DIR/$REPO_FILE" "$RESOURCES_DIR/$LIST_FILE.${REPO_FILE##*.}"
echo -e "$NEW_VER" > "$RESOURCES_DIR/$LIST_FILE.ver"
log "[$(to_upper "$LIST_FILE")] Successfully updated."
if mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"; then
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
log "[$(to_upper "$listtype")] Successfully updated."
else
rm -f "$RUN_DIR/$listname"
log "[$(to_upper "$listtype")] Failed to install update (mv failed)."
return 1
fi
return 0
}
case "$1" in
"china_ip4")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
sed -i "/IP-CIDR6,/d; s/IP-CIDR,//g" "$RESOURCES_DIR/china_ip4.txt"
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
;;
"china_ip6")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
sed -i "/IP-CIDR,/d; s/IP-CIDR6,//g" "$RESOURCES_DIR/china_ip6.txt"
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
;;
"gfw_list")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "gfw.txt" && \
sed -i "s/^\.//g" "$RESOURCES_DIR/gfw_list.txt"
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
;;
"china_list")
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "direct.txt" && \
sed -i "s/^\.//g" "$RESOURCES_DIR/china_list.txt"
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
;;
*)
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
@@ -1,10 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2025 ImmortalWrt.org
CACHE_PATH="$(uci -q get homeproxy.cache.path)"
[ -n "$CACHE_PATH" ] || CACHE_PATH="/etc/homeproxy/cache.db"
rm -f "$CACHE_PATH"
/etc/init.d/homeproxy restart
File diff suppressed because it is too large. Load diff