🦄 Sync 2026-10-02 14:52:05

This commit is contained in:
github-actions[bot] committed 2026-10-02 14:52:05 +08:00
1 parent 4c85793342
commit 0bfe84f1e8
133 files changed
+7983 -3225

No files matched your search

@@ -165,6 +165,58 @@ if (routing_port === 'common')
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
/* --- can IPv6 be classified as mainland at all? ------------------------
*
* Every IPv6 decision in this template used to hang off ipv6_support
* alone. That flag only says whether the user *wants* IPv6 handled; it
* says nothing about whether we can tell a mainland IPv6 destination from
* a foreign one. That test is the homeproxy_mainland_addr_v6 set, and the
* set is filled straight from resources/china_ip6.txt - so an install whose
* list is missing, empty or entirely malformed produced a *syntactically
* valid but permanently empty* set. `ip6 daddr @homeproxy_mainland_addr_v6
* counter return` then matched nothing, and every mainland IPv6 connection
* fell through to the proxy hop at the bottom of the chain: a silent,
* unlogged inversion of the mode's promise, and the reason a user who
* enables IPv6 finds Chinese sites taking the long way round.
*
* nft happily accepts `elements = { }`, so nothing downstream could notice.
* The count is taken here, once, with the same isValidCIDR() the set
* rendering uses - a list of entries this function rejects would render an
* empty set too, so counting something else would not be the same test.
*
* v6_handled is what every v6 branch below now keys off. When the list is
* unusable it is false, and the ruleset degrades to exactly the IPv6-off
* shape: IPv6 is not intercepted, not marked, not redirected. That is a
* deliberate choice of failure direction. The alternative - keep
* intercepting and proxy everything - trades a wrong-but-working path for
* a leaked one, and the user cannot see either from the client. Passing
* IPv6 through unproxied is the state the router was in before this package
* touched it, it is recoverable (fix the list, reload), and it is announced:
* see the comment on the dstnat jump below and the warning
* hp_prepare_runtime_files() logs on every start. */
let cn_ipv6_count = 0;
for (let entry in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/))
if (isValidCIDR(entry, 6))
cn_ipv6_count++;
const cn_ipv6_ready = (cn_ipv6_count > 0);
const v6_handled = (ipv6_support === '1') && cn_ipv6_ready;
/* The degraded-state warning, as a ready-made nft comment suffix.
*
* A real `comment` rather than a `#` line on purpose: fw4 keeps `#` lines
* only in the generated file, while a rule comment is part of the rule and
* survives into `nft list ruleset` - which is where someone debugging "why
* is my IPv6 not going through the proxy" actually looks.
*
* Computed once and interpolated into all three jump rules rather than
* wrapped around each of them: the rule text is identical either way, only
* the comment differs, and one string is one place to change if the wording
* ever has to. */
const v6_warn_comment = ((ipv6_support === '1') && !cn_ipv6_ready)
? ' comment "!homeproxy-pro: WARNING china_ip6.txt has 0 usable prefixes - IPv6 is NOT proxied, run update_resources.sh china_ip6"'
: '';
let self_mark, redirect_port,
tproxy_port, tproxy_mark,
tun_name, tun_mark;
@@ -232,7 +284,7 @@ set homeproxy_local_addr_v4 {
240.0.0.0/4
}
}
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
set homeproxy_local_addr_v6 {
type ipv6_addr
flags interval
@@ -261,13 +313,13 @@ set homeproxy_gfw_list_v4 {
flags interval
auto-merge
}
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
set homeproxy_gfw_list_v6 {
type ipv6_addr
flags interval
auto-merge
}
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %}
set homeproxy_mainland_addr_v4 {
type ipv4_addr
@@ -281,7 +333,7 @@ set homeproxy_mainland_addr_v4 {
{% endfor %}
}
}
{% if ((ipv6_support === '1') || china_dns_server): %}
{% if (v6_handled): %}
set homeproxy_mainland_addr_v6 {
type ipv6_addr
flags interval
@@ -294,7 +346,7 @@ set homeproxy_mainland_addr_v6 {
{% endfor %}
}
}
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% endif /* routing_mode */ %}
{# WAN ACL addresses #}
@@ -307,7 +359,7 @@ set homeproxy_wan_proxy_addr_v4 {
{% endif %}
}
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
set homeproxy_wan_proxy_addr_v6 {
type ipv6_addr
flags interval
@@ -316,7 +368,7 @@ set homeproxy_wan_proxy_addr_v6 {
elements = { {{ ipv6_to_nftarr(control_info.wan_proxy_ipv6_ips) }} }
{% endif /* wan_proxy_ipv6_ips*/ %}
}
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
set homeproxy_wan_direct_addr_v4 {
type ipv4_addr
@@ -327,7 +379,7 @@ set homeproxy_wan_direct_addr_v4 {
{% endif %}
}
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
set homeproxy_wan_direct_addr_v6 {
type ipv6_addr
flags interval
@@ -336,7 +388,7 @@ set homeproxy_wan_direct_addr_v6 {
elements = { {{ ipv6_to_nftarr(control_info.wan_direct_ipv6_ips) }} }
{% endif /* wan_direct_ipv6_ips */ %}
}
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% if (routing_port): %}
set homeproxy_routing_port {
@@ -356,7 +408,7 @@ chain dstnat {
meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :{{ dnsmasq_port }} comment "!{{ cfgname }}: DNS hijack"
{% endif /* dns_hijacked */ %}
{% if (match(proxy_mode, /redirect/)): %}
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac{{ v6_warn_comment }}
{% endif /* proxy_mode */ %}
}
@@ -410,12 +462,12 @@ chain homeproxy_redirect {
meta mark {{ self_mark }} counter return
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
@@ -432,25 +484,25 @@ chain homeproxy_redirect {
{% endif /* routing_mode */ %}
ip daddr @homeproxy_wan_direct_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% endif /* routing_mode */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
@@ -468,7 +520,7 @@ chain homeproxy_redirect {
chain homeproxy_output_redir {
type nat hook output priority filter -105; policy accept
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
}
{% endif %}
@@ -476,7 +528,7 @@ chain homeproxy_output_redir {
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %}
chain homeproxy_mangle_tproxy {
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
{% endif %}
}
@@ -531,12 +583,12 @@ chain homeproxy_mangle_lanac {
chain homeproxy_mangle_prerouting {
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
@@ -553,29 +605,29 @@ chain homeproxy_mangle_prerouting {
{% endif /* routing_mode */ %}
ip daddr @homeproxy_wan_direct_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
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'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% if (routing_mode !== 'custom'): %}
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
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% endif /* routing_mode */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
@@ -595,46 +647,46 @@ chain homeproxy_mangle_output {
meta mark {{ self_mark }} counter return
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
ip daddr @homeproxy_wan_direct_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% endif /* routing_mode */ %}
counter goto homeproxy_mangle_mark
}
chain mangle_prerouting {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac{{ v6_warn_comment }}
}
chain mangle_output {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
}
{% endif %}
@@ -690,12 +742,12 @@ chain homeproxy_mangle_tun {
iifname {{ tun_name }} counter return
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
@@ -720,23 +772,23 @@ chain homeproxy_mangle_tun {
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
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'): %}
{% if (v6_handled): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% if (routing_mode !== 'custom'): %}
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
{% if (ipv6_support === '1'): %}
{% if (v6_handled): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* v6_handled */ %}
{% endif /* routing_mode */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
@@ -753,10 +805,10 @@ chain homeproxy_mangle_tun {
}
chain mangle_prerouting {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac{{ v6_warn_comment }}
}
chain mangle_output {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
}
{% endif %}
@@ -25,7 +25,7 @@
'use strict';
import { connect } from 'ubus';
import { mkdtemp, readfile, writefile } from 'fs';
import { lstat, mkdtemp, readfile, writefile } from 'fs';
import { Loader } from './config/loader.uc';
import { generate } from './generator/client.uc';
@@ -52,7 +52,36 @@ function resolve_env(dm) {
const env = {
wan_dns: (ubus?.call('network.interface', 'status', {'interface': 'wan'}))?.['dns-server']?.[0],
direct_domain_list: [],
proxy_domain_list: []
proxy_domain_list: [],
/* Whether the generated mainland rule-set for IPv6 is on disk.
*
* generator/ may not stat anything (guard 27), and this is exactly
* the kind of environment a generator is supposed to be handed:
* geoip-cn.srs and china_ip4.json carry no IPv6, so china_ip6.json -
* written by hp_prepare_runtime_files from the list the resource
* updater maintains - is the only thing that lets the route and DNS
* halves recognise a mainland IPv6 destination. When it is absent
* (the list is missing, or had no usable prefix when the file was
* last written) both halves have to stay silent about IPv6 instead
* of naming a rule-set that is not there: a `rule_set:` pointing at
* a missing file makes sing-box reject the entire config, which the
* health gate turns into a rollback and an unproxied network. The
* firewall has already degraded to passing IPv6 through, with a
* warning in the ruleset and the log.
*
* lstat(), not the two-argument access(). fs.access() on this ucode
* build answers only in its one-argument form: access(path) returns
* true for an existing path and null for a missing one, while
* access(path, mode) returns null either way. The two-argument form
* is therefore the worst possible choice here - it reports every
* file as missing, so this flag would be permanently false and the
* whole IPv6 split would stay switched off with nothing logged. The
* one-argument call sites elsewhere in the tree (homeproxy-pro.uc's
* cleanup_exec_dir) are correct and must stay as they are.
* lstat() is used because it is unambiguous - there is no second
* argument to get wrong - and because the neighbouring stderr-size
* check in homeproxy-pro.uc already reads sizes through it. */
china_ip6_ready: lstat(HP_DIR + '/resources/china_ip6.json') !== null
};
if (routing_mode !== 'custom') {
@@ -131,6 +131,13 @@ export function build_context(dm, env) {
const ctx = {
routing_mode, proxy_mode, ipv6_support, log_level,
proxy_fallback,
/* Resolved by the caller, which is the only layer allowed to look at
* the filesystem (guard 27). The route rule and the DNS cn-fallback
* match must agree on it: they reference the same generated rule-set,
* and one of them naming a tag the other never declared fails the
* whole config. Default false - an absent env must not turn into an
* optimistic "assume the file is there". */
china_ip6_ready: (env?.china_ip6_ready === true),
self_mark, ntp_server, dns_port, mixed_port,
redirect_port, tproxy_port,
tun_name, tun_addr4, tun_addr6, tun_mtu,
@@ -291,9 +291,24 @@ function append_proxy_dns(config, dm, ctx) {
server: 'main-dns',
tag: 'cn-fallback'
});
/* The response is matched by the addresses the upstream returned,
* and geoip-cn has no IPv6 in it, so with IPv6 support on a
* mainland domain that resolved to an AAAA address was never
* recognised as mainland and its answer kept coming from the
* proxy resolver. Adding the v6 rule-set here is what makes the
* DNS half of the split agree with the route half, which
* classifies the same destinations through the same list
* (route.uc: china-ip6). Both tags have to be declared before
* this rule runs, which is why the list is built here rather than
* inlined: a rule_set naming an undeclared tag fails the config.
* china-ip6 is only declared when its file exists, so it is only
* named here under the same condition. */
const cn_fallback_sets = ['geoip-cn'];
if (ctx.ipv6_support === '1' && ctx.china_ip6_ready)
push(cn_fallback_sets, 'china-ip6');
push(config.dns.rules, {
match_response: 'cn-fallback',
rule_set: 'geoip-cn',
rule_set: cn_fallback_sets,
action: 'route',
server: 'china-dns'
});
@@ -28,7 +28,6 @@
import { isEmpty, strToInt, strToTime, strToBool, parse_port, HP_DIR } from '../homeproxy-pro.uc';
import { get_outbound, get_resolver, get_ruleset, get_direct_override } from './common.uc';
/* --- shared initial block (every routing mode) ------------------------- */
@@ -136,6 +135,25 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
action: 'route',
outbound: 'direct-out'
});
/* IPv6 half of the same split, and the reason a mainland site opened
* over IPv6 used to go through the proxy even with IPv6 support on.
* geoip-cn.srs carries no IPv6 and china_ip4.json is generated from an
* IPv4 list, so a destination that resolved to an AAAA address matched
* neither and fell through to `final` - which in this mode is main-out.
* The rule is emitted in both mainland modes (unlike china-ip, which
* proxy_mainland_china does not need twice because geoip-cn already
* picks the mainland side there): with no IPv6 in geoip-cn there is
* nothing left to pick it, so the side has to be named explicitly.
* `prefer_ipv4` on the resolve rule above is what lets both families
* through - it biases, it does not filter. */
if (ctx.ipv6_support === '1' && ctx.china_ip6_ready) {
push(config.route.rules, {
rule_set: 'china-ip6',
action: 'route',
outbound: ctx.proxy_fallback ? 'direct-out' : 'main-out'
});
}
}
/* Main UDP out */
@@ -250,15 +268,27 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
* from, so the two cannot drift apart. `type: local` is watched by
* sing-box with fswatch, so when the resource updater replaces the
* file the running instance reloads it in place - no restart and,
* more importantly, no second copy of the list to keep in sync.
* China IPv6 is deliberately absent: the firewall only fills the v6
* set when ipv6_support is on, and the client refuses to resolve
* AAAA when it is off. */
* more importantly, no second copy of the list to keep in sync. */
push(config.route.rule_set, {
type: 'local',
tag: 'china-ip',
path: HP_DIR + '/resources/china_ip4.json'
});
/* The IPv6 counterpart, from china_ip6.txt - the same list the
* firewall renders homeproxy_mainland_addr_v6 from, and the only
* IPv6 mainland data either side of the split has. Declared only
* when the file is actually there: a rule_set pointing at a missing
* file takes the whole config down with it, and a router whose v6
* list is unusable must still start (the firewall has already
* degraded to passing IPv6 through, with a warning). */
if (ctx.ipv6_support === '1' && ctx.china_ip6_ready) {
push(config.route.rule_set, {
type: 'local',
tag: 'china-ip6',
path: HP_DIR + '/resources/china_ip6.json'
});
}
}
if (isEmpty(config.route.rule_set))
@@ -166,7 +166,7 @@ hp_prepare_runtime_files() {
local client_enabled="$4"
local server_enabled="$5"
# The route rule that keeps mainland destinations direct matches a local
# The route rules that keep mainland destinations direct match a local
# rule-set generated from china_ip4.txt, the same list the firewall
# renders its nft set from, so both sides decide from one source. It is
# regenerated here as well as by the resource updater: an install that
@@ -184,6 +184,40 @@ hp_prepare_runtime_files() {
fi
fi
# IPv6 half of the same arrangement, and the only place the "IPv6 is on
# but cannot be classified" condition is reported in a place a user reads
# without tcpdump. geoip-cn.srs and china_ip4.json are both IPv4-only, so
# without a v6 rule-set a mainland destination reached over IPv6 matches
# no route rule at all and falls through to `final` - i.e. to the proxy.
# The firewall would normally have returned it first, but that set is
# filled from the same china_ip6.txt, so a list that is missing or has no
# usable prefix leaves neither side able to classify it. Say so plainly
# instead: the generated ruleset also carries the same text as a rule
# comment, so `nft list ruleset` shows the degraded state too.
local ipv6_support cn_ipv6
config_get_bool ipv6_support "config" "ipv6_support" "0"
cn_ipv6=0
if [ -f "$hp_dir/resources/china_ip6.txt" ]; then
cn_ipv6="$(grep -cE '^[0-9a-fA-F:]+:[0-9a-fA-F:]*(/[0-9]{1,3})?[[:space:]]*$' \
"$hp_dir/resources/china_ip6.txt" 2>"/dev/null")"
[ -n "$cn_ipv6" ] || cn_ipv6=0
if [ "$cn_ipv6" -gt 0 ] && ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
"$hp_dir/resources/china_ip6.txt" "$hp_dir/resources/china_ip6.json" >>"$LOG_PATH" 2>&1; then
chown sing-box:sing-box "$hp_dir/resources/china_ip6.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip6.json to sing-box."
else
# Only reached when the source is unusable, which china_ip_ruleset.uc
# reports as "no usable CIDR entries". Keep the previous file: an
# empty one would make the route-side set match nothing, which is the
# same silent inversion this whole branch exists to prevent.
log "Warning: could not generate ${hp_dir}/resources/china_ip6.json; the route side keeps the previous list."
fi
fi
if [ "$ipv6_support" -eq 1 ] && [ "$cn_ipv6" -eq 0 ]; then
log "WARNING: IPv6 support is ON but ${hp_dir}/resources/china_ip6.txt has 0 usable prefixes."
log "WARNING: mainland IPv6 cannot be told apart from foreign IPv6, so IPv6 is passed through UNPROXIED (both sides of the split degrade). Run: ${hp_dir}/scripts/update_resources.sh china_ip6"
fi
# cache_file is enabled for bypass_mainland_china and custom alike
# (generator/common.uc:attachExperimental). The file has to exist before
# the client jail binds it, or sing-box cannot create it in there.
@@ -201,15 +201,27 @@ check_list_update() {
# not fatal: the previous rule-set is still valid and the firewall
# has already switched to the new list, which is exactly the state
# the next run (or the next service start) repairs.
if [ "$listtype" = "china_ip4" ]; then
#
# china_ip6 is in the same boat for the same reason: geoip-cn.srs and
# china_ip4.json carry no IPv6 at all, so a mainland destination
# reached over v6 matches no route rule and falls through to `final`
# - the proxy - unless there is a v6 rule-set of our own.
case "$listtype" in
"china_ip4"|"china_ip6")
if ucode -S "$SCRIPT_DIR/runtime/china_ip_ruleset.uc" \
"$RESOURCES_DIR/china_ip4.txt" "$RESOURCES_DIR/china_ip4.json" >>"$LOG_PATH" 2>&1; then
log "[CHINA_IP4] Route-side rule-set regenerated."
chown sing-box:sing-box "$RESOURCES_DIR/china_ip4.json" 2>"/dev/null"
"$RESOURCES_DIR/$listtype.txt" "$RESOURCES_DIR/$listtype.json" >>"$LOG_PATH" 2>&1; then
log "[$(to_upper "$listtype")] Route-side rule-set regenerated."
chown sing-box:sing-box "$RESOURCES_DIR/$listtype.json" 2>"/dev/null"
else
log "[CHINA_IP4] Warning: could not regenerate china_ip4.json; the route side keeps the previous list."
# The helper exits non-zero on an empty or fully malformed
# list, and deliberately leaves the previous .json in place.
# Writing an empty one would be worse than not writing any:
# the rule would then match nothing, which is the silent
# "mainland IPv6 goes through the proxy" inversion.
log "[$(to_upper "$listtype")] Warning: could not regenerate $listtype.json (list has no usable CIDR entry?); the route side keeps the previous list."
fi
fi
;;
esac
else
rm -f "$RUN_DIR/$listname"
log "[$(to_upper "$listtype")] Failed to install update (mv failed)."