mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
💐 Sync 2026-10-01 01:11:54
This commit is contained in:
1 parent
fa5e40404d
commit
ce81b387d8
44 files changed
+1092
-2287
No files matched your search
@@ -23,7 +23,7 @@ LUCI_DEPENDS:= \
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy-pro
|
||||
PKG_VERSION:=28.9.1.14
|
||||
PKG_RELEASE:=12
|
||||
PKG_RELEASE:=13
|
||||
|
||||
# Only files the package actually ships belong here. The previous list named
|
||||
# four paths that exist nowhere in the tree - certs/ and ruleset/ are created at
|
||||
|
||||
@@ -79,9 +79,6 @@
|
||||
- **试验性**:不承诺 API/配置稳定,重大变更可能在 minor 版本里发生。
|
||||
- **真机实测单平台**:本仓库只在 x86-64 软路由上做过端到端验证。
|
||||
- 本包不会编译 sing-box。系统固件必须自带 sing-box 1.14+;否则依赖解析直接失败。
|
||||
- **升级后留意 `/etc` 下的 `.apk-new`**:apk 会保护被本地改动过的包文件——新版本写成同名的 `.apk-new` 而不覆盖它。本项目实测到 `/etc/init.d/homeproxy-pro` 如此,结果是 reload 去抖在升级后静默不生效,直到把两者合并。升级后跑一次 `find /etc -name '*.apk-new'` 确认。
|
||||
- **关闭 `ipv6_support` 会阻断 IPv6 转发**:此时 nft 会拒绝所有 IPv6 转发(包括 LAN 内部),DNS 侧同时把 AAAA 应答改为 REFUSED。这是“本路由不使用 IPv6”的完整语义——只靠 DNS 拒绝挡不住自带解析器或已缓存的地址;如果仍需要 IPv6 直连,请把该开关打开。
|
||||
- **`gfwlist` 模式依赖 dnsmasq 的地址集合**:该模式用 dnsmasq 的 `nftset=` 把解析结果写进 `homeproxy_gfw_list_v4/v6`,nft 再依据该集合决定哪些目的地进入代理(集合外直连)。如果 LAN 客户端用自带 DoH/DoT 绕过了路由器 DNS,集合不会被填充,效果是**全部直连**。所以该模式要求客户端使用路由器下发的 DNS。
|
||||
|
||||
## 贡献
|
||||
|
||||
|
||||
@@ -1128,35 +1128,6 @@ return baseclass.extend({
|
||||
return true;
|
||||
},
|
||||
|
||||
/* A regular expression that does not compile reaches sing-box as-is and
|
||||
* takes the whole configuration with it, so the reload keeps the previous
|
||||
* one and the user only sees "my setting did not take". Go's RE2 and this
|
||||
* engine differ in the corners, but every typo is caught here. */
|
||||
validateRegex(section_id, value) {
|
||||
if (section_id && value) {
|
||||
try {
|
||||
new RegExp(value);
|
||||
} catch (e) {
|
||||
return _('Expecting: %s').format(_('valid regular expression'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/* sing-box resolves a DNS query type name through miekg/dns's uppercase
|
||||
* type table (option/types.go), so "aaaa" is an "unknown DNS query type"
|
||||
* and takes the configuration down with it. A numeric code up to 65535 is
|
||||
* valid as-is. */
|
||||
validateQueryType(section_id, value) {
|
||||
if (section_id && value) {
|
||||
if (!((/^[0-9]{1,5}$/.test(value) && parseInt(value, 10) <= 65535) || /^[A-Z][A-Z0-9-]*$/.test(value)))
|
||||
return _('Expecting: %s').format(_('a numeric code or an uppercase type name (for example AAAA)'));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateUniqueValue(uciconfig, ucisection, ucioption, section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
|
||||
@@ -160,12 +160,12 @@ return view.extend({
|
||||
|
||||
/* Tab ordering follows the order of these calls: routing,
|
||||
* routing_node, routing_rule, dns, dns_server, dns_rule, ruleset,
|
||||
* control, udp_nat, tun_dns. The DNS tab sits before Access Control
|
||||
* because the pages a preset-routing-mode user actually sees are
|
||||
* Routing Settings, DNS Settings, Access Control, UDP NAT Settings
|
||||
* and TUN DNS, in that order. The shared rule-section bodies live in
|
||||
* common.js and are called out-of-order with respect to their file to
|
||||
* keep the rendering sequence intact. */
|
||||
* dns_cache, control, udp_nat, tun_dns. The DNS tab sits before
|
||||
* Access Control because the pages a preset-routing-mode user
|
||||
* actually sees are Routing Settings, DNS Settings, Access Control,
|
||||
* UDP NAT Settings and TUN DNS, in that order. The shared
|
||||
* rule-section bodies live in common.js and are called out-of-order
|
||||
* with respect to their file to keep the rendering sequence intact. */
|
||||
routing.render(ctx);
|
||||
nodes.renderRoutingNodes(ctx);
|
||||
routing.renderRoutingRules(ctx);
|
||||
@@ -173,6 +173,7 @@ return view.extend({
|
||||
nodes.renderDnsServers(ctx);
|
||||
dns.renderDnsRules(ctx);
|
||||
subscription.render(ctx);
|
||||
dns.renderDnsCache(ctx);
|
||||
access.render(ctx);
|
||||
udp_nat.renderUdpNat(ctx);
|
||||
tun_dns.renderTunDns(ctx);
|
||||
|
||||
+2
-3
@@ -44,9 +44,8 @@ function render(ctx) {
|
||||
/* LAN IP policy start */
|
||||
ss.tab('lan_ip_policy', _('LAN IP Policy'));
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.ListValue, 'lan_proxy_mode', _('Proxy filter mode'),
|
||||
_('"No filter" proxies every LAN client, which is the default; the other two values select clients by the lists below.'));
|
||||
so.value('disabled', _('No filter (proxy every client)'));
|
||||
so = ss.taboption('lan_ip_policy', form.ListValue, 'lan_proxy_mode', _('Proxy filter mode'));
|
||||
so.value('disabled', _('Disable'));
|
||||
so.value('listed_only', _('Proxy listed only'));
|
||||
so.value('except_listed', _('Proxy all except listed'));
|
||||
so.default = 'disabled';
|
||||
|
||||
+10
-27
@@ -113,7 +113,6 @@ function renderRuleSection(s, kind, self) {
|
||||
if (is_dns) {
|
||||
so = ss.taboption('field_other', form.DynamicList, 'query_type', _('Query type'),
|
||||
_('Match query type.'));
|
||||
so.validate = hp.validateQueryType;
|
||||
so.modalonly = true;
|
||||
|
||||
so = addNetwork();
|
||||
@@ -146,7 +145,7 @@ function renderRuleSection(s, kind, self) {
|
||||
delete this.vallist;
|
||||
|
||||
uci.sections('homeproxy-pro', 'ruleset', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -168,15 +167,12 @@ function renderRuleSection(s, kind, self) {
|
||||
* (sniff/hijack are emitted by the proxy-mode builder,
|
||||
* not exposed in the form).
|
||||
*
|
||||
* `route-options` is offered for routing rules only. sing-box 1.14's DNS
|
||||
* rule action list does contain a `route-options`, but it takes a
|
||||
* different field set (cache / TTL / timeout / client-subnet, and no
|
||||
* outbound or override), which this shared form does not model - offering
|
||||
* it here would emit routing fields the DNS schema refuses. Earlier code
|
||||
* added it to both branches and the depends() guards for dns_disable_cache
|
||||
* / rewrite_ttl / client_subnet pinned a stale 'route-options' in UCI that
|
||||
* resurfaced as "unknown field" the next time the user flipped back to
|
||||
* 'route'. */
|
||||
* `route-options` is a routing_rule-only action: sing-box 1.14's dns_rule
|
||||
* schema has no such action, and emitting it would make the whole dns block
|
||||
* fail with "unknown action". Earlier code added it to both branches and
|
||||
* the depends() guards for dns_disable_cache / rewrite_ttl / client_subnet
|
||||
* pinned a stale 'route-options' in the UCI that resurfaced as "unknown
|
||||
* field" the next time the user flipped back to 'route'. */
|
||||
so = ss.taboption('field_other', form.ListValue, 'action', _('Action'));
|
||||
so.value('route', _('Route'));
|
||||
so.value('reject', _('Reject'));
|
||||
@@ -201,7 +197,7 @@ function renderRuleSection(s, kind, self) {
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -338,7 +334,7 @@ function renderRuleSection(s, kind, self) {
|
||||
|
||||
this.value('direct-out', _('Direct'));
|
||||
uci.sections('homeproxy-pro', 'routing_node', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -352,48 +348,39 @@ function renderRuleSection(s, kind, self) {
|
||||
_('Override the connection destination address.'));
|
||||
so.datatype = 'ipaddr';
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.Value, 'override_port', _('Override port'),
|
||||
_('Override the connection destination port.'));
|
||||
so.datatype = 'port';
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.Flag, 'udp_disable_domain_unmapping', _('Disable UDP domain unmapping'),
|
||||
_('If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain.'));
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.Flag, 'udp_connect', _('connect UDP connections'),
|
||||
_('If enabled, attempts to connect UDP connection to the destination instead of listen.'));
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.Value, 'udp_timeout', _('UDP timeout'),
|
||||
_('Timeout for UDP connections.<br/>Setting a larger value than the UDP timeout in inbounds will have no effect.'));
|
||||
so.datatype = 'uinteger';
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.Flag, 'tls_record_fragment', _('TLS record fragment'),
|
||||
_('Fragment TLS handshake into multiple TLS records.'));
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
so.depends({'tls_fragment': '1', '!reverse': true});
|
||||
|
||||
so = ss.taboption('field_other', form.Flag, 'tls_fragment', _('TLS fragment'),
|
||||
_('Fragment TLS handshakes. Due to poor performance, try <code>%s</code> first.').format(
|
||||
_('TLS record fragment')));
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.depends({'tls_record_fragment': '1', '!reverse': true});
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.Value, 'tls_fragment_fallback_delay', _('Fragment fallback delay'),
|
||||
@@ -407,7 +394,6 @@ function renderRuleSection(s, kind, self) {
|
||||
_('Inject a forged TLS ClientHello carrying this SNI before the real one to fool SNI-filtering middleboxes. Requires elevated privileges.'));
|
||||
so.datatype = 'hostname';
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_other', form.ListValue, 'tls_spoof_method', _('TLS spoof method (1.14)'),
|
||||
@@ -418,7 +404,6 @@ function renderRuleSection(s, kind, self) {
|
||||
so.value('wrong-md5', _('wrong-md5'));
|
||||
so.value('wrong-timestamp', _('wrong-timestamp'));
|
||||
so.depends('action', 'route');
|
||||
so.depends('action', 'route-options');
|
||||
so.depends('tls_spoof', /[\s\S]/);
|
||||
so.modalonly = true;
|
||||
|
||||
@@ -432,7 +417,7 @@ function renderRuleSection(s, kind, self) {
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -505,7 +490,6 @@ function renderRuleSection(s, kind, self) {
|
||||
|
||||
so = ss.taboption('field_host', form.DynamicList, 'domain_regex', _('Domain regex'),
|
||||
_('Match domain using regular expression.'));
|
||||
so.validate = hp.validateRegex;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_host', form.DynamicList, 'source_ip_cidr', _('Source IP CIDR'),
|
||||
@@ -576,7 +560,6 @@ function renderRuleSection(s, kind, self) {
|
||||
|
||||
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
|
||||
_('Match process path using regular expression.'));
|
||||
so.validate = hp.validateRegex;
|
||||
so.modalonly = true;
|
||||
}
|
||||
|
||||
|
||||
+85
-79
@@ -13,20 +13,17 @@
|
||||
'require homeproxy-pro as hp';
|
||||
'require view.homeproxy-pro.client.common as common';
|
||||
|
||||
/* DNS settings: both routing-mode families in one tab.
|
||||
/* The four cache options exist on the 'dns' NamedSection in both the custom
|
||||
* mode tab (renderDnsSettings) and the preset mode tab (renderDnsCache).
|
||||
* Both used to spell them out separately, which registered the same four
|
||||
* option names twice in one map: LuCI keeps the first registration for writes
|
||||
* and lookupOption(), so the second copy's dependencies did nothing while its
|
||||
* description could drift. They are declared here once and rendered from
|
||||
* wherever the tab needs them, in the original order.
|
||||
*
|
||||
* They used to be two tabs with the same title ('dns' and 'dns_cache'), each
|
||||
* wrapping the same 'dns' NamedSection. Two form.NamedSection instances over
|
||||
* one UCI section produce identical cbids, and LuCI resolves a cbid by taking
|
||||
* the first matching element in DOM order - which follows tab registration,
|
||||
* i.e. the custom-mode tab. The four cache options were registered in both,
|
||||
* so under a preset routing mode the controls the user could actually see were
|
||||
* reading and writing an element inside a hidden tab: "Store DNS cache" and
|
||||
* "DNS query timeout" did not take.
|
||||
*
|
||||
* The tab is declared once now and no option name is registered twice, so
|
||||
* there is no first-match to get wrong. Each option carries the routing mode
|
||||
* it belongs to as a depends(). */
|
||||
* The depends() are the custom-mode copy's: with the cache disabled the
|
||||
* optimistic cache and its persisted file have no effect, so both are hidden
|
||||
* (disable_cache_expire is itself conditional on disable_cache). */
|
||||
function renderDnsCacheOptions(ss) {
|
||||
let so;
|
||||
|
||||
@@ -53,23 +50,85 @@ function renderDnsCacheOptions(ss) {
|
||||
* routing.renderRoutingRules() so the NamedSection chain reads
|
||||
* config -> routing_node -> routing_rule -> dns from left to right. */
|
||||
function renderDnsSettings(ctx) {
|
||||
const { s, stubValidator } = ctx;
|
||||
const { s } = ctx;
|
||||
let o, ss, so;
|
||||
|
||||
s.tab('dns', _('DNS Settings'));
|
||||
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'homeproxy-pro');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
/* The preset-mode fields. They belong to the 'config' section, not to
|
||||
* 'dns', so they were never part of the duplicate-cbid problem above -
|
||||
* they are rendered here because this is the only DNS tab now.
|
||||
*
|
||||
* The name says overseas because that is the only thing that belongs
|
||||
ss = o.subsection;
|
||||
so = ss.option(form.ListValue, 'default_strategy', _('Default DNS strategy'),
|
||||
_('The DNS strategy for resolving the domain name in the address.'));
|
||||
for (let i in hp.dns_strategy)
|
||||
so.value(i, hp.dns_strategy[i]);
|
||||
|
||||
so = ss.option(form.ListValue, 'default_server', _('Default DNS server'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.default = 'default-dns';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.Flag, 'disable_cache', _('Disable DNS cache'));
|
||||
|
||||
so = ss.option(form.Flag, 'disable_cache_expire', _('Disable cache expire'));
|
||||
so.depends('disable_cache', '0');
|
||||
|
||||
so = ss.option(form.Value, 'client_subnet', _('EDNS Client subnet'),
|
||||
_('Append a <code>edns0-subnet</code> OPT extra record with the specified IP prefix to every query by default.<br/>' +
|
||||
'If value is an IP address instead of prefix, <code>/32</code> or <code>/128</code> will be appended automatically.'));
|
||||
so.datatype = 'or(cidr, ipaddr)';
|
||||
|
||||
renderDnsCacheOptions(ss);
|
||||
/* DNS settings end */
|
||||
}
|
||||
|
||||
/* DNS rules sub-section. Split out from renderDnsSettings() so the
|
||||
* orchestrator can call it after nodes.renderDnsServers() and preserve
|
||||
* the original tab ordering (dns, dns_server, dns_rule). */
|
||||
function renderDnsRules(ctx) {
|
||||
const { s, self } = ctx;
|
||||
|
||||
/* DNS rules start */
|
||||
common.renderRuleSection(s, 'dns', self);
|
||||
/* DNS rules end */
|
||||
}
|
||||
|
||||
/* DNS tab for the preset routing modes. Titled 'DNS Settings' exactly like
|
||||
* the custom-mode tab in renderDnsSettings(): the two are mutually exclusive
|
||||
* (custom vs the presets), so whichever one a user lands on is called the
|
||||
* same thing.
|
||||
*
|
||||
* The three server options below are config.* (not dns.*) options that used
|
||||
* to live on the routing tab. They are DNS settings, and the routing page
|
||||
* only carried them because of the order the old monolithic client.js
|
||||
* happened to be written in. They stay on the 'config' NamedSection, so
|
||||
* only the tab they render under changed. */
|
||||
function renderDnsCache(ctx) {
|
||||
const { s, stubValidator } = ctx;
|
||||
let o, ss;
|
||||
|
||||
s.tab('dns_cache', _('DNS Settings'));
|
||||
|
||||
/* The name says overseas because that is the only thing that belongs
|
||||
* here: every query this field answers goes to a domain that is not on
|
||||
* the China path, and a domestic resolver answers those with polluted or
|
||||
* NXDOMAIN results. The Chinese public resolvers that used to be listed
|
||||
* below a divider were therefore a footgun with a helpful-looking label -
|
||||
* the one combination that reliably breaks a working proxy. Only the
|
||||
* overseas public resolvers are offered now. */
|
||||
o = s.taboption('dns', form.Value, 'dns_server', _('Overseas DNS server'),
|
||||
o = s.taboption('dns_cache', form.Value, 'dns_server', _('Overseas DNS server'),
|
||||
_('Resolves every domain that is not on the China DNS path, through the selected proxy node. ' +
|
||||
'A domestic resolver answers these queries with polluted or NXDOMAIN results, which is what ' +
|
||||
'makes a foreign site fail to resolve while the proxy itself works - so only overseas services ' +
|
||||
@@ -105,13 +164,11 @@ function renderDnsSettings(ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.taboption('dns', form.Value, 'china_dns_server', _('China DNS server'),
|
||||
o = s.taboption('dns_cache', form.Value, 'china_dns_server', _('China DNS server'),
|
||||
_('Resolves the domains on the China path, so it has to be a domestic service to get the ' +
|
||||
'correct local CDN answers. Keep the default; the upstream may block UDP/53, in which case ' +
|
||||
'a DoH address (https://dns.alidns.com/dns-query, https://doh.pub/dns-query) also works. ' +
|
||||
'Support UDP, TCP, DoH, DoQ, DoT. A DoH/DoT address here also sends the bootstrap ' +
|
||||
'lookup back to the WAN resolver: only a bare IP can resolve the proxy DNS endpoint ' +
|
||||
'without the ISP.'));
|
||||
'Support UDP, TCP, DoH, DoQ, DoT.'));
|
||||
o.value('wan', _('WAN DNS (read from interface)'));
|
||||
o.value('223.5.5.5', _('Aliyun Public DNS (223.5.5.5)'));
|
||||
o.value('210.2.4.8', _('CNNIC Public DNS (210.2.4.8)'));
|
||||
@@ -144,67 +201,16 @@ function renderDnsSettings(ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.taboption('dns', form.Flag, 'cn_ip_fallback', _('CN-IP DNS fallback (sing-box 1.14)'),
|
||||
o = s.taboption('dns_cache', form.Flag, 'cn_ip_fallback', _('CN-IP DNS fallback (sing-box 1.14)'),
|
||||
_('When the main DNS returns a mainland China IP, re-resolve via China DNS using evaluate/match_response.'));
|
||||
o.depends('routing_mode', 'bypass_mainland_china');
|
||||
o.rmempty = false;
|
||||
|
||||
/* The 'dns' section, registered exactly once: see the file header. The
|
||||
* custom-only options below keep their own depends() because the section
|
||||
* is visible in both routing-mode families. */
|
||||
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'homeproxy-pro');
|
||||
o = s.taboption('dns_cache', form.SectionValue, '_dns_cache', form.NamedSection, 'dns', 'homeproxy-pro');
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
ss = o.subsection;
|
||||
|
||||
so = ss.option(form.ListValue, 'default_strategy', _('Default DNS strategy'),
|
||||
_('The DNS strategy for resolving the domain name in the address.'));
|
||||
for (let i in hp.dns_strategy)
|
||||
so.value(i, hp.dns_strategy[i]);
|
||||
so.depends('routing_mode', 'custom');
|
||||
|
||||
so = ss.option(form.ListValue, 'default_server', _('Default DNS server'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.default = 'default-dns';
|
||||
so.rmempty = false;
|
||||
so.depends('routing_mode', 'custom');
|
||||
|
||||
so = ss.option(form.Flag, 'disable_cache', _('Disable DNS cache'));
|
||||
|
||||
so = ss.option(form.Flag, 'disable_cache_expire', _('Disable cache expire'));
|
||||
so.depends('disable_cache', '0');
|
||||
|
||||
so = ss.option(form.Value, 'client_subnet', _('EDNS Client subnet'),
|
||||
_('Append a <code>edns0-subnet</code> OPT extra record with the specified IP prefix to every query by default.<br/>' +
|
||||
'If value is an IP address instead of prefix, <code>/32</code> or <code>/128</code> will be appended automatically.'));
|
||||
so.datatype = 'or(cidr, ipaddr)';
|
||||
so.depends('routing_mode', 'custom');
|
||||
|
||||
/* The four cache options are common to both families, so they are
|
||||
* registered once, with no routing-mode dependency of their own. */
|
||||
renderDnsCacheOptions(ss);
|
||||
/* DNS settings end */
|
||||
}
|
||||
|
||||
/* DNS rules sub-section. Split out from renderDnsSettings() so the
|
||||
* orchestrator can call it after nodes.renderDnsServers() and preserve
|
||||
* the original tab ordering (dns, dns_server, dns_rule). */
|
||||
function renderDnsRules(ctx) {
|
||||
const { s, self } = ctx;
|
||||
|
||||
/* DNS rules start */
|
||||
common.renderRuleSection(s, 'dns', self);
|
||||
/* DNS rules end */
|
||||
}
|
||||
|
||||
return baseclass.extend({ renderDnsSettings, renderDnsRules });
|
||||
return baseclass.extend({ renderDnsSettings, renderDnsRules, renderDnsCache });
|
||||
+5
-5
@@ -65,7 +65,7 @@ function renderRoutingNodes(ctx) {
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ function renderRoutingNodes(ctx) {
|
||||
|
||||
this.value('', _('Direct'));
|
||||
uci.sections('homeproxy-pro', 'routing_node', (res) => {
|
||||
if (res['.name'] !== section_id && res.enabled !== '0')
|
||||
if (res['.name'] !== section_id && res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -264,7 +264,7 @@ function renderDnsServers(ctx) {
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'headers', _('Headers'),
|
||||
_('Additional headers to be sent to the DNS server, one <code>Name: Value</code> per line.'));
|
||||
_('Additional headers to be sent to the DNS server.'));
|
||||
so.depends('type', 'https');
|
||||
so.depends('type', 'h3');
|
||||
so.modalonly = true;
|
||||
@@ -287,7 +287,7 @@ function renderDnsServers(ctx) {
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res['.name'] !== section_id && res.enabled !== '0')
|
||||
if (res['.name'] !== section_id && res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -324,7 +324,7 @@ function renderDnsServers(ctx) {
|
||||
|
||||
this.value('direct-out', _('Direct'));
|
||||
uci.sections('homeproxy-pro', 'routing_node', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
|
||||
+3
-7
@@ -108,11 +108,7 @@ function render(ctx) {
|
||||
|
||||
let ports = [];
|
||||
for (let i of value.split(',')) {
|
||||
/* A bare decimal port only: the backend's ports_to_nftarr()
|
||||
* (firewall_utils.uc) keeps nothing else, so a range like
|
||||
* "80-90" was accepted here and then dropped without a word -
|
||||
* the port the user asked for was simply never proxied. */
|
||||
if (!stubValidator.apply('port', i) || i === '0')
|
||||
if (!stubValidator.apply('port', i) && !stubValidator.apply('portrange', i))
|
||||
return _('Expecting: %s').format(_('valid port value'));
|
||||
if (ports.includes(i))
|
||||
return _('Port %s alrealy exists!').format(i);
|
||||
@@ -214,7 +210,7 @@ function render(ctx) {
|
||||
this.value('direct-out', _('Direct'));
|
||||
this.value('block-out', _('Block'));
|
||||
uci.sections('homeproxy-pro', 'routing_node', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
@@ -232,7 +228,7 @@ function render(ctx) {
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('system-dns', _('System DNS'));
|
||||
uci.sections('homeproxy-pro', 'dns_server', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
|
||||
+4
-9
@@ -52,16 +52,11 @@ function render(ctx) {
|
||||
so.default = 'remote';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.ListValue, 'format', _('Format'),
|
||||
_('Leave empty to infer it from a <code>.json</code> or <code>.srs</code> path/url; sing-box requires it for any other extension.'));
|
||||
so.value('', _('Automatic (by extension)'));
|
||||
so = ss.option(form.ListValue, 'format', _('Format'));
|
||||
so.value('binary', _('Binary file'));
|
||||
so.value('source', _('Source file'));
|
||||
/* Not `binary` by default: sing-box only infers the format when the
|
||||
* field is absent, and an explicit value wins. Defaulting to binary
|
||||
* meant every local rule-set pointing at a .json file was parsed as a
|
||||
* compiled .srs unless the user remembered to change the dropdown. */
|
||||
so.rmempty = true;
|
||||
so.default = 'binary';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.Value, 'path', _('Path'));
|
||||
so.datatype = 'file';
|
||||
@@ -104,7 +99,7 @@ function render(ctx) {
|
||||
this.value('', _('Default'));
|
||||
this.value('direct-out', _('Direct'));
|
||||
uci.sections('homeproxy-pro', 'routing_node', (res) => {
|
||||
if (res.enabled !== '0')
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
|
||||
@@ -98,38 +98,6 @@ function getConnStat(o, site) {
|
||||
]);
|
||||
}
|
||||
|
||||
/* The gfwlist routing mode routes by an nft set that dnsmasq fills through
|
||||
* `nftset=`. A LAN client with its own DoH/DoT never asks this router's DNS,
|
||||
* so the set stays empty and the mode silently degrades to direct routing.
|
||||
* The count is what tells "the mode is working" from "the mode is doing
|
||||
* nothing", so it sits next to the resource versions. */
|
||||
function getGfwSetStat(o) {
|
||||
return hp.rpcCall('gfw_set_counts', [], { expect: { '': {} } }).then((res) => {
|
||||
let field = E('div', { 'style': 'cbi-value-field' });
|
||||
let warn = false;
|
||||
|
||||
[ [ 'IPv4', res.v4 ], [ 'IPv6', res.v6 ] ].forEach(([ label, count ]) => {
|
||||
let value;
|
||||
|
||||
if (count == null)
|
||||
value = E('em', {}, _('not installed'));
|
||||
else if (count === 0) {
|
||||
value = E('strong', { 'style': 'color:orange' }, '%d'.format(count));
|
||||
warn = warn || (label === 'IPv4');
|
||||
} else
|
||||
value = E('strong', { 'style': 'color:green' }, '%d'.format(count));
|
||||
|
||||
field.appendChild(E('span', {}, [ label + ': ', value ]));
|
||||
field.appendChild(document.createTextNode(' '));
|
||||
});
|
||||
|
||||
if (warn)
|
||||
field.appendChild(E('em', {}, _('empty: clients may be bypassing this router\'s DNS')));
|
||||
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
function getResVersion(o, type) {
|
||||
return hp.rpcCall('resources_get_version', [type],
|
||||
{ params: ['type'], expect: { '': {} } }).then((res) => {
|
||||
@@ -306,9 +274,6 @@ return view.extend({
|
||||
o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list');
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_gfw_set_size', _('GFW list addresses'));
|
||||
o.cfgvalue = L.bind(getGfwSetStat, this, o);
|
||||
|
||||
o = s.option(form.Value, 'github_token', _('GitHub token'));
|
||||
o.password = true;
|
||||
o.renderWidget = function() {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -281,12 +281,7 @@ set homeproxy_mainland_addr_v4 {
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{# The v6 mainland set is only ever matched inside `ipv6_support === '1'`
|
||||
blocks - every reference to it below is gated on that - so the old
|
||||
`|| china_dns_server` alternative only ever built a set nothing pointed
|
||||
at: 1033 prefixes loaded into the kernel on every fw4 reload for nothing.
|
||||
china_dns_server is a DNS-side setting and has no bearing on v6 routing. #}
|
||||
{% if (ipv6_support === '1'): %}
|
||||
{% if ((ipv6_support === '1') || china_dns_server): %}
|
||||
set homeproxy_mainland_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
@@ -352,25 +347,6 @@ set homeproxy_routing_port {
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# IPv6 forwarding is refused outright when the switch is off.
|
||||
The DNS side already answers AAAA with REFUSED, but that only covers clients
|
||||
that ask *this* router: a cached address, a client with its own resolver, or
|
||||
an IPv6-only destination still reaches the WAN and bypasses both the proxy
|
||||
and the mainland split.
|
||||
fw4's own forward chain cannot be used for this - it jumps into the zone
|
||||
chains, whose accept rules end the traversal before anything appended at the
|
||||
end of the chain is reached - so this is a base chain of its own, ordered
|
||||
before fw4's forward (priority filter == 0).
|
||||
Intra-LAN IPv6 is covered as well, which is what "this router does not do
|
||||
IPv6" means for `ipv6_support` off. #}
|
||||
{% if (ipv6_support !== '1'): %}
|
||||
chain homeproxy_ipv6_guard {
|
||||
type filter hook forward priority mangle; policy accept
|
||||
|
||||
meta nfproto ipv6 counter reject comment "!{{ cfgname }}: IPv6 forwarding is disabled (ipv6_support is off)"
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# DNS hijack & TCP redirect #}
|
||||
chain dstnat {
|
||||
{% if ((dns_redirect === '1') && (dns_hijacked !== '1')): %}
|
||||
@@ -586,14 +562,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 }}: reject QUIC (UDP 80/443), clients fall back to TCP"
|
||||
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 }}: reject QUIC (UDP 80/443), clients fall back to TCP"
|
||||
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
|
||||
@@ -747,14 +723,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 }}: reject QUIC (UDP 80/443), clients fall back to TCP"
|
||||
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 }}: reject QUIC (UDP 80/443), clients fall back to TCP"
|
||||
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
|
||||
|
||||
@@ -31,7 +31,6 @@ HP_FW4_CHAINS="
|
||||
homeproxy_mangle_mark
|
||||
homeproxy_mangle_tun
|
||||
homeproxy_mangle_tun_mark
|
||||
homeproxy_ipv6_guard
|
||||
"
|
||||
|
||||
HP_FW4_SETS="
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
'use strict';
|
||||
|
||||
import { connect } from 'ubus';
|
||||
import { mkdtemp, open, readfile, writefile } from 'fs';
|
||||
import { mkdtemp, readfile, writefile } from 'fs';
|
||||
|
||||
import { Loader } from './config/loader.uc';
|
||||
import { generate } from './generator/client.uc';
|
||||
@@ -52,23 +52,9 @@ function resolve_env(dm) {
|
||||
const env = {
|
||||
wan_dns: (ubus?.call('network.interface', 'status', {'interface': 'wan'}))?.['dns-server']?.[0],
|
||||
direct_domain_list: [],
|
||||
proxy_domain_list: [],
|
||||
china_ip_ruleset_available: false
|
||||
proxy_domain_list: []
|
||||
};
|
||||
|
||||
/* The route block references this file as a `type: local` rule-set, and
|
||||
* sing-box refuses the whole configuration when it cannot open the path.
|
||||
* service.sh generates it before the client configuration, so it is
|
||||
* normally there; a fresh install whose generation failed (no ucode, a
|
||||
* read-only filesystem, a full disk) is what this answers. The generator
|
||||
* is a pure function, so the existence check belongs on this side of the
|
||||
* boundary. open() rather than readfile(): the file is a few megabytes
|
||||
* and only its existence matters. */
|
||||
const china_ip_json = open(HP_DIR + '/resources/china_ip4.json');
|
||||
env.china_ip_ruleset_available = china_ip_json != null;
|
||||
if (china_ip_json)
|
||||
china_ip_json.close();
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
const direct_list_raw = readfile(HP_DIR + '/resources/direct_list.txt');
|
||||
env.direct_domain_list = direct_list_raw ? split(trim(direct_list_raw), /[\r\n]/) : [];
|
||||
|
||||
@@ -52,15 +52,6 @@ export function get_outbound(cfg, dm) {
|
||||
return cfg;
|
||||
default:
|
||||
const rn = ConfigQuery.find_by_name(dm.routing.nodes, cfg);
|
||||
/* A routing_node that was disabled after something started referring
|
||||
* to it is skipped by generate_outbound(), so the tag returned below
|
||||
* would exist nowhere: sing-box refuses the whole configuration with
|
||||
* "outbound not found: cfg-<name>-out", which does not name the node
|
||||
* the user actually turned off. Fail here instead, the way
|
||||
* get_resolver()/get_ruleset() already do. The caller passes the
|
||||
* section name, so it is what the message names. */
|
||||
if (rn && (rn.enabled === false || rn.enabled === '0'))
|
||||
die(sprintf("the routing node '%s' is disabled; enable it or point the rule at another outbound.", cfg));
|
||||
if (!rn || isEmpty(rn.node))
|
||||
die(sprintf("%s's node is missing, please check your configuration.", cfg));
|
||||
else if (rn.node === 'urltest')
|
||||
|
||||
@@ -153,11 +153,11 @@ export function build_context(dm, env) {
|
||||
* its own outbound/rule tables (routing.default_outbound plus the
|
||||
* routing_node / routing_rule / ruleset sections). A residual
|
||||
* config.main_node must therefore not switch the generator back to
|
||||
* the preset path. LuCI itself does clear a field the user can no
|
||||
* longer see (depends() makes parse() remove the value), but UCI is
|
||||
* writable from anywhere - a script, an older version, a hand edit -
|
||||
* and the pre-refactor code read this value only when the mode was
|
||||
* not custom. Reading it here anyway silently dropped every custom
|
||||
* the preset path: LuCI hides that field in custom mode but does not
|
||||
* clear it (`rmempty = false`), so a router that was configured in a
|
||||
* preset mode and then switched to custom keeps the old value in
|
||||
* UCI - and the pre-refactor code read it only when the mode was not
|
||||
* custom. Reading it here anyway silently dropped every custom
|
||||
* routing rule and made route.final 'main-out'. */
|
||||
main_node: (routing_mode === 'custom') ? null : dm.general.main_node,
|
||||
main_udp_node: (routing_mode === 'custom') ? null : dm.general.main_udp_node,
|
||||
@@ -248,14 +248,6 @@ export function build_context(dm, env) {
|
||||
ctx.proxy_domain_list = env.proxy_domain_list || [];
|
||||
}
|
||||
|
||||
/* Whether the local china-ip rule-set file exists (the CLI resolved it;
|
||||
* see generate_client.uc). route.uc emits that rule-set - and the rule
|
||||
* that matches it - only when it does, so a file that is missing degrades
|
||||
* the mainland split to geoip-cn instead of making sing-box refuse the
|
||||
* whole configuration over a path it cannot open. A caller that does not
|
||||
* supply the flag keeps the previous behaviour. */
|
||||
ctx.china_ip_ruleset_available = env.china_ip_ruleset_available !== false;
|
||||
|
||||
/* `dedicated_udp_node` means main_udp_node was set to a different
|
||||
* proxy-able node than main_node (the 'same'/'nil' sentinels don't
|
||||
* qualify). Kept on ctx so both build_outbounds and build_route
|
||||
|
||||
@@ -72,50 +72,6 @@ function parse_dnsquery(strquery) {
|
||||
return querys;
|
||||
}
|
||||
|
||||
/* Build the sing-box `headers` object from the UCI string list.
|
||||
*
|
||||
* sing-box types this field as `badoption.HTTPHeader`, i.e.
|
||||
* map[string][]string; an array (or a bare string) fails to decode and the
|
||||
* whole configuration is rejected. The LuCI form is a DynamicList, so every
|
||||
* entry lands in UCI as a string and the two sides disagreed on the shape:
|
||||
* `"headers": ["Name: value"]` is not a map.
|
||||
*
|
||||
* The form keeps its list widget and one entry is written `Name: Value`;
|
||||
* repeating a name appends to that name's value list, which is how HTTP
|
||||
* expresses a multi-valued header. An entry that does not fit the form is
|
||||
* named and rejected here rather than left for sing-box to report as
|
||||
* "cannot unmarshal array into Go value of type badoption.HTTPHeader". */
|
||||
function parse_http_headers(list, where) {
|
||||
if (isEmpty(list))
|
||||
return null;
|
||||
|
||||
/* A single hand-written UCI option arrives as a string, not a list. */
|
||||
if (type(list) !== 'array')
|
||||
list = [ list ];
|
||||
|
||||
const headers = {};
|
||||
for (let item in list) {
|
||||
const line = trim(item);
|
||||
if (line === '')
|
||||
continue;
|
||||
|
||||
const sep = index(line, ':');
|
||||
if (sep <= 0)
|
||||
die(sprintf("homeproxy-pro: %s: header '%s' is not in 'Name: Value' form.", where, line));
|
||||
|
||||
const name = trim(substr(line, 0, sep));
|
||||
const value = trim(substr(line, sep + 1));
|
||||
if (isEmpty(name) || isEmpty(value))
|
||||
die(sprintf("homeproxy-pro: %s: header '%s' is not in 'Name: Value' form.", where, line));
|
||||
|
||||
if (!headers[name])
|
||||
headers[name] = [];
|
||||
push(headers[name], value);
|
||||
}
|
||||
|
||||
return isEmpty(keys(headers)) ? null : headers;
|
||||
}
|
||||
|
||||
/* --- proxy routing-mode path (bypass_mainland_china / proxy / / gfwlist) -- */
|
||||
|
||||
/* Domains whose NAPTR (qtype 35) queries bypass china-dns. Aliyun
|
||||
@@ -131,6 +87,7 @@ function parse_http_headers(list, where) {
|
||||
* someone reads the comment and thinks "I know a domain that should
|
||||
* go direct". */
|
||||
const NAPTR_BYPASS_SUFFIXES = [
|
||||
'r.10086.cn',
|
||||
'10086.cn',
|
||||
'pub.3gppnetwork.org'
|
||||
];
|
||||
@@ -237,16 +194,7 @@ function append_proxy_dns(config, dm, ctx) {
|
||||
server: 'main-dns'
|
||||
});
|
||||
|
||||
/* Reject SVCB/HTTPS queries: a client that learns the address from these
|
||||
* record types dials the embedded IP without asking again, which is what
|
||||
* lets a query slip past the DNS split (and, in the custom path, past
|
||||
* Fake-IP).
|
||||
*
|
||||
* It sits after the two domain lists on purpose, so a domain the user put
|
||||
* on a list keeps whatever resolver that list chose - the custom path
|
||||
* below puts the same reject first instead, because there the user's own
|
||||
* rules are the ones that must not be shadowed. The two orders are
|
||||
* deliberate; do not "fix" one into the other. */
|
||||
/* Reject SVCB/HTTPS queries to avoid proxy DNS timeout on null domains */
|
||||
push(config.dns.rules, {
|
||||
query_type: [64, 65],
|
||||
action: 'reject'
|
||||
@@ -369,7 +317,7 @@ function append_custom_dns(config, dm, ctx) {
|
||||
server: cfg.server,
|
||||
server_port: strToInt(cfg.server_port),
|
||||
path: cfg.path,
|
||||
headers: parse_http_headers(cfg.headers, sprintf("dns server '%s'", cfg.name)),
|
||||
headers: cfg.headers,
|
||||
tls: cfg.tls_sni ? {
|
||||
enabled: true,
|
||||
server_name: cfg.tls_sni
|
||||
@@ -437,71 +385,24 @@ function append_custom_dns(config, dm, ctx) {
|
||||
rule_set: get_ruleset(cfg.rule_set, dm),
|
||||
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
|
||||
invert: strToBool(cfg.invert),
|
||||
race: strToBool(cfg.race),
|
||||
speculative: strToBool(cfg.speculative),
|
||||
action: cfg.action
|
||||
};
|
||||
|
||||
/* race/speculative are action fields, not match fields, and their
|
||||
* legality is decided at load time by sing-box's
|
||||
* validateDNSRuleAction() (route/rule/rule_dns.go):
|
||||
*
|
||||
* - race and speculative are mutually exclusive ("`race` and
|
||||
* `speculative` cannot be combined on the same rule");
|
||||
* - race is only allowed on a final action - route, respond,
|
||||
* reject, predefined - and "`race` requires a final action"
|
||||
* otherwise;
|
||||
* - speculative exists only in the route and evaluate option
|
||||
* structs, so on any other action it is an unknown field and
|
||||
* DisallowUnknownFields rejects the whole configuration.
|
||||
*
|
||||
* The old code emitted both unconditionally. The form hides them
|
||||
* with depends(), but LuCI does not delete the value of a hidden
|
||||
* option, so "tick race on a route rule, then switch the rule to
|
||||
* evaluate" left an illegal value behind. Gate by action and die
|
||||
* on the combination, naming the field rather than letting sing-box
|
||||
* answer with "unknown field". */
|
||||
const want_race = strToBool(cfg.race) === true,
|
||||
want_speculative = strToBool(cfg.speculative) === true;
|
||||
|
||||
if (want_race && want_speculative)
|
||||
die(sprintf("homeproxy-pro: DNS rule '%s' enables both race and speculative; sing-box refuses that combination.", cfg.name));
|
||||
|
||||
if (want_race) {
|
||||
if (!(cfg.action in ['route', 'reject', 'predefined']))
|
||||
die(sprintf("homeproxy-pro: DNS rule '%s' uses race with action '%s'; race is only allowed on a final action (route, reject, predefined).", cfg.name, cfg.action));
|
||||
rule.race = true;
|
||||
}
|
||||
|
||||
if (want_speculative) {
|
||||
if (!(cfg.action in ['route', 'evaluate']))
|
||||
die(sprintf("homeproxy-pro: DNS rule '%s' uses speculative with action '%s'; speculative is only valid for route and evaluate.", cfg.name, cfg.action));
|
||||
rule.speculative = true;
|
||||
}
|
||||
|
||||
/* `route` and `evaluate` (which needs a server for the eval step).
|
||||
* DNS rules have no `resolve` action - that one belongs to the route
|
||||
* rules - and the form does not offer it here. It used to be handled
|
||||
* anyway, which would have emitted an action sing-box does not know
|
||||
* the moment a stale UCI value carried it. */
|
||||
if (cfg.action === 'route' || cfg.action === 'evaluate') {
|
||||
/* `route` (and `evaluate`, which needs a server for the eval step).
|
||||
* `resolve` is also a possibility but is not currently exposed in
|
||||
* the UI; if it ever is, this branch is where it lands. */
|
||||
if (cfg.action === 'route' || cfg.action === 'evaluate' || cfg.action === 'resolve') {
|
||||
rule.server = get_resolver(cfg.server, dm);
|
||||
rule.disable_cache = strToBool(cfg.dns_disable_cache);
|
||||
rule.disable_optimistic_cache = strToBool(cfg.disable_optimistic_cache);
|
||||
rule.rewrite_ttl = strToInt(cfg.rewrite_ttl);
|
||||
/* Load-time mutual exclusion in validateDNSRuleAction():
|
||||
* "`client_subnet` and `remove_client_subnet` are mutually
|
||||
* exclusive". The form draws them as two independent
|
||||
* controls, so both can be left set. */
|
||||
if (!isEmpty(cfg.client_subnet) && strToBool(cfg.remove_client_subnet) === true)
|
||||
die(sprintf("homeproxy-pro: DNS rule '%s' sets both client_subnet and remove_client_subnet; sing-box refuses that combination.", cfg.name));
|
||||
rule.client_subnet = cfg.client_subnet;
|
||||
rule.remove_client_subnet = strToBool(cfg.remove_client_subnet);
|
||||
}
|
||||
|
||||
/* 1.14's evaluate action carries the same timeout override as route
|
||||
* (DNSEvaluateActionOptions embeds AbstractDNSRouteActionOptions), and
|
||||
* the form shows the field for both actions. Emitting it for route
|
||||
* only made the value disappear silently on an evaluate rule. */
|
||||
if (cfg.action === 'route' || cfg.action === 'evaluate')
|
||||
if (cfg.action === 'route' || cfg.action === 'resolve')
|
||||
rule.timeout = strToTime(cfg.dns_timeout);
|
||||
|
||||
/* `reject` only accepts method + no_drop. Anything else here is
|
||||
@@ -575,13 +476,6 @@ function append_custom_dns(config, dm, ctx) {
|
||||
rule.response_extra = cfg.response_extra;
|
||||
}
|
||||
|
||||
/* race also requires match_response in 1.14. This check has to sit
|
||||
* after every match_response assignment: the legacy address-filter
|
||||
* wrapping above gives one to those rules too, and that satisfies
|
||||
* sing-box just as well. */
|
||||
if (want_race && !rule.match_response)
|
||||
die(sprintf("homeproxy-pro: DNS rule '%s' enables race without match_response; sing-box requires one.", cfg.name));
|
||||
|
||||
push(builtin_dns_rules, rule);
|
||||
}
|
||||
config.dns.rules = builtin_dns_rules;
|
||||
@@ -589,31 +483,6 @@ function append_custom_dns(config, dm, ctx) {
|
||||
config.dns.final = get_resolver(ctx.dns_default_server, dm);
|
||||
}
|
||||
|
||||
/* Reject AAAA when this router does not proxy IPv6.
|
||||
*
|
||||
* Every IPv6 rule-set and rule in firewall_post.ut sits inside
|
||||
* `ipv6_support === '1'`, so with the switch off the router does not
|
||||
* intercept v6 at all - and nothing in this DNS block ever refused a AAAA
|
||||
* query. dns.strategy=ipv4_only on the preset paths is a resolution
|
||||
* preference, not a query block, and custom routing takes the UCI default
|
||||
* (prefer_ipv4). A dual-stack LAN therefore still received AAAA records and
|
||||
* connected straight out over the WAN, bypassing both the proxied resolver
|
||||
* and the destination-based split.
|
||||
*
|
||||
* Appended last so a user rule keeps precedence over it. A client with its
|
||||
* own DoH/DoT can still bypass this layer; a hard block would have to be an
|
||||
* nft rule, which changes the user-visible "IPv6 still works" behaviour of
|
||||
* the switch and is therefore not done here. */
|
||||
function append_ipv6_guard(config, ctx) {
|
||||
if (ctx.ipv6_support === '1')
|
||||
return;
|
||||
|
||||
push(config.dns.rules, {
|
||||
query_type: [28],
|
||||
action: 'reject'
|
||||
});
|
||||
}
|
||||
|
||||
/* --- public entry ------------------------------------------------------ */
|
||||
|
||||
/* The always-on DNS block. Every routing mode (proxy and custom) gets
|
||||
@@ -685,7 +554,5 @@ export function build_dns(config, dm, ctx) {
|
||||
append_proxy_dns(config, dm, ctx);
|
||||
}
|
||||
|
||||
append_ipv6_guard(config, ctx);
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -253,7 +253,7 @@ function buildCustomOutbounds(dm, config, ctx, direct_overrides) {
|
||||
* comes from a routing_node. `|| {}` made it look non-null and the
|
||||
* failure surfaced as a null dereference two lines later. */
|
||||
if (isEmpty(outbound))
|
||||
die(sprintf("routing node '%s' refers to a node that does not exist.\n", cfg.name));
|
||||
die(sprintf("routing node '%s' refers to a node that does not exist.\n", cfg['.name']));
|
||||
|
||||
if (outbound.type === 'wireguard') {
|
||||
/* WireGuard goes through generate_endpoint() which tags
|
||||
@@ -274,7 +274,7 @@ function buildCustomOutbounds(dm, config, ctx, direct_overrides) {
|
||||
const ob = generate_outbound(outbound, mark, direct_overrides);
|
||||
|
||||
if (ob == null)
|
||||
die(sprintf("routing node '%s' cannot be built from node '%s'.\n", cfg.name, cfg.node));
|
||||
die(sprintf("routing node '%s' cannot be built from node '%s'.\n", cfg['.name'], cfg.node));
|
||||
|
||||
ob.bind_interface = cfg.bind_interface;
|
||||
ob.detour = get_outbound(cfg.outbound, dm);
|
||||
|
||||
@@ -120,23 +120,17 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
outbound: ctx.proxy_fallback ? 'direct-out' : 'main-out'
|
||||
});
|
||||
}
|
||||
/* geoip-cn.srs is upstream's list; the firewall's mainland set is
|
||||
* built from our own china_ip4.txt (firewall_post.ut reads it
|
||||
* directly). The two disagree - the bundled list has 8.152.0.0/13
|
||||
* (Alibaba Cloud) and geoip-cn.srs does not - so a connection into
|
||||
* that range passes the firewall as "not mainland" and would then be
|
||||
* sent direct by the rule above instead of to the proxy. This local
|
||||
* rule-set is generated from the same china_ip4.txt, which is what
|
||||
* makes both sides decide from one list. In
|
||||
* proxy_mainland_china it is not needed twice: the geoip-cn rule
|
||||
* already selects the mainland side for the proxy.
|
||||
*
|
||||
* Only when the file is actually there: service.sh generates it before
|
||||
* the client configuration, but a fresh install whose generation failed
|
||||
* would otherwise leave a `type: local` rule-set pointing at a path
|
||||
* sing-box cannot open, which fails the whole configuration rather than
|
||||
* this one rule. The geoip-cn rule above still covers the split. */
|
||||
if (ctx.routing_mode === 'bypass_mainland_china' && ctx.china_ip_ruleset_available) {
|
||||
if (ctx.routing_mode === 'bypass_mainland_china') {
|
||||
/* geoip-cn.srs is upstream's list; the firewall's mainland set is
|
||||
* built from our own china_ip4.txt (firewall_post.ut reads it
|
||||
* directly). The two disagree - the bundled list has 8.152.0.0/13
|
||||
* (Alibaba Cloud) and geoip-cn.srs does not - so a connection into
|
||||
* that range passes the firewall as "not mainland" and would then be
|
||||
* sent direct by the rule above instead of to the proxy. This local
|
||||
* rule-set is generated from the same china_ip4.txt, which is what
|
||||
* makes both sides decide from one list. In
|
||||
* proxy_mainland_china it is not needed twice: the geoip-cn rule
|
||||
* already selects the mainland side for the proxy. */
|
||||
push(config.route.rules, {
|
||||
rule_set: 'china-ip',
|
||||
action: 'route',
|
||||
@@ -258,14 +252,13 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
|
||||
* 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. Declared only when the file exists,
|
||||
* for the same reason as the rule that matches it (see above). */
|
||||
if (ctx.china_ip_ruleset_available)
|
||||
push(config.route.rule_set, {
|
||||
type: 'local',
|
||||
tag: 'china-ip',
|
||||
path: HP_DIR + '/resources/china_ip4.json'
|
||||
});
|
||||
* set when ipv6_support is on, and the client refuses to resolve
|
||||
* AAAA when it is off. */
|
||||
push(config.route.rule_set, {
|
||||
type: 'local',
|
||||
tag: 'china-ip',
|
||||
path: HP_DIR + '/resources/china_ip4.json'
|
||||
});
|
||||
}
|
||||
|
||||
if (isEmpty(config.route.rule_set))
|
||||
@@ -310,6 +303,7 @@ function build_route_custom(config, dm, ctx, direct_overrides) {
|
||||
ip_version: strToInt(cfg.ip_version),
|
||||
protocol: cfg.protocol,
|
||||
network: cfg.network,
|
||||
client: cfg.client,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
@@ -334,15 +328,6 @@ function build_route_custom(config, dm, ctx, direct_overrides) {
|
||||
source_hostname: cfg.source_hostname
|
||||
};
|
||||
|
||||
/* The sniffed client type is only set for QUIC and SSH traffic; the
|
||||
* form offers the field under those two protocols only, but a value
|
||||
* left behind by an earlier protocol would make the rule match
|
||||
* nothing, so it is emitted only when it can apply. */
|
||||
const protocols = type(cfg.protocol) === 'array' ? cfg.protocol
|
||||
: (isEmpty(cfg.protocol) ? [] : [ cfg.protocol ]);
|
||||
if ('quic' in protocols || 'ssh' in protocols)
|
||||
rule.client = cfg.client;
|
||||
|
||||
/* `route-options` only takes the override_* fields; `outbound` is the
|
||||
* `route` action's field. The two used to share one branch and
|
||||
* `route-options` ended up carrying an `outbound` sing-box then
|
||||
@@ -362,39 +347,6 @@ function build_route_custom(config, dm, ctx, direct_overrides) {
|
||||
rule.tls_spoof = cfg.tls_spoof || null;
|
||||
rule.tls_spoof_method = cfg.tls_spoof_method || null;
|
||||
}
|
||||
|
||||
/* Load-time mutual exclusion in RouteOptionsActionOptions.UnmarshalJSON()
|
||||
* (option/rule_action.go): "`tls_fragment` and `tls_record_fragment`
|
||||
* are mutually exclusive". The form draws both switches for the
|
||||
* route action and neither hides the other, so a user following the
|
||||
* description ("try TLS record fragment first") can easily tick
|
||||
* both and lose the whole configuration. */
|
||||
if (strToBool(cfg.tls_fragment) === true && strToBool(cfg.tls_record_fragment) === true)
|
||||
die(sprintf("homeproxy-pro: routing rule '%s' enables both tls_fragment and tls_record_fragment; sing-box refuses that combination.", cfg.name));
|
||||
|
||||
/* The same unmarshaller rejects an empty action object with "empty
|
||||
* route option action". Match fields belong to the rule, not to the
|
||||
* action payload, so a rule that selects Route options and fills in
|
||||
* none of the fields above fails the whole configuration with a
|
||||
* message that names nothing. The fields are judged on their parsed
|
||||
* values, not on the raw UCI strings: a UCI '0' becomes `false`,
|
||||
* which is the Go zero value and is not payload. */
|
||||
if (cfg.action === 'route-options') {
|
||||
const has_payload =
|
||||
!isEmpty(rule_override_address) ||
|
||||
!isEmpty(rule_override_port) ||
|
||||
strToBool(cfg.udp_disable_domain_unmapping) === true ||
|
||||
strToBool(cfg.udp_connect) === true ||
|
||||
!isEmpty(cfg.udp_timeout) ||
|
||||
strToBool(cfg.tls_fragment) === true ||
|
||||
!isEmpty(cfg.tls_fragment_fallback_delay) ||
|
||||
strToBool(cfg.tls_record_fragment) === true ||
|
||||
!isEmpty(cfg.tls_spoof) ||
|
||||
!isEmpty(cfg.tls_spoof_method);
|
||||
|
||||
if (!has_payload)
|
||||
die(sprintf("homeproxy-pro: routing rule '%s' selects the route-options action but sets none of its fields; sing-box rejects an empty route option action.", cfg.name));
|
||||
}
|
||||
/* `route` alone is the action that picks a destination - `route-options`
|
||||
* adjusts the existing connection, so it does not take `outbound`. */
|
||||
if (cfg.action === 'route')
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty, strToTime, validateHomeProxyPath } from '../homeproxy-pro.uc';
|
||||
import { isEmpty, validateHomeProxyPath } from '../homeproxy-pro.uc';
|
||||
|
||||
import { get_outbound, isDirectOutboundTag } from './common.uc';
|
||||
|
||||
@@ -40,13 +40,6 @@ export function build_user_rulesets(rule_set_array, dm, ctx) {
|
||||
if (!cfg.enabled)
|
||||
continue;
|
||||
|
||||
/* An inline rule-set is a `rules` array, and nothing in this package
|
||||
* ever writes one: the form only offers local and remote. Emitting
|
||||
* `{type: 'inline'}` without `rules` is rejected by sing-box, so a
|
||||
* hand-written UCI section is refused by name here. */
|
||||
if (cfg.type === 'inline')
|
||||
die(sprintf("homeproxy-pro: rule-set '%s' is declared inline, which this package cannot edit; use a local or remote rule-set.", cfg.name));
|
||||
|
||||
const extra_tags = cfg.extra_tags || [];
|
||||
let rs_tag = 'cfg-' + cfg.name + '-rule';
|
||||
if (length(extra_tags) && cfg.type !== 'inline') {
|
||||
@@ -85,29 +78,18 @@ export function build_user_rulesets(rule_set_array, dm, ctx) {
|
||||
format: cfg.format,
|
||||
path: cfg.path,
|
||||
url: cfg.url,
|
||||
/* A duration to sing-box: a bare number is rejected with
|
||||
* "time: missing unit in duration", taking the whole
|
||||
* configuration down, and the form only offers a placeholder
|
||||
* ("1d"), not a format check. Same treatment as the DNS
|
||||
* timeouts. */
|
||||
update_interval: strToTime(cfg.update_interval)
|
||||
update_interval: cfg.update_interval
|
||||
};
|
||||
/* download_detour is deprecated since 1.14 (removed in 1.16) and only
|
||||
* ever applied to remote rule-sets; emitting it at all makes sing-box
|
||||
* refuse the configuration. It is translated into http_clients right
|
||||
* below. */
|
||||
/* download_detour is a pre-1.14 option that only makes sense for
|
||||
remote rule-sets; emitting it for local/inline ones makes sing-box
|
||||
* 1.14 reject the whole config. It is translated into http_clients
|
||||
right below. */
|
||||
if (cfg.type === 'remote')
|
||||
ruleset.download_detour = get_outbound(cfg.outbound, dm) || get_outbound(ctx.default_outbound, dm);
|
||||
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path)) {
|
||||
/* initial_path is read off the local disk as root; same whitelist
|
||||
* as the local ruleset path - and now the same fail-loud
|
||||
* treatment. Dropping it silently made a rule-set the user
|
||||
* believed had a local copy depend on a download at first start,
|
||||
* with nothing said about why. */
|
||||
if (!validateHomeProxyPath(cfg.initial_path))
|
||||
die(sprintf("homeproxy-pro: rule-set '%s' initial_path '%s' is outside the homeproxy-pro whitelist; choose a path under /etc/homeproxy-pro/.", cfg.name, cfg.initial_path));
|
||||
ruleset.initial_path = cfg.initial_path;
|
||||
}
|
||||
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path))
|
||||
/* initial_path is read off the local disk as root; same
|
||||
* whitelist as the local ruleset path. */
|
||||
ruleset.initial_path = validateHomeProxyPath(cfg.initial_path) ? cfg.initial_path : null;
|
||||
push(rule_set_array, ruleset);
|
||||
}
|
||||
};
|
||||
@@ -132,21 +114,8 @@ export function build_http_clients(rule_set_array, dm, ctx) {
|
||||
if (rs.type !== 'remote')
|
||||
continue;
|
||||
|
||||
if (isEmpty(detour)) {
|
||||
/* A remote rule-set still has to be fetched through something.
|
||||
* The preset modes used to fall back to direct-out, which fails
|
||||
* on a router that can only reach GitHub through the proxy - and
|
||||
* the only trace is a line in the sing-box log. Follow the
|
||||
* mode's main outbound instead, the way the built-in geoip-cn /
|
||||
* geosite-cn rule-sets already do (route.uc gives them
|
||||
* download_detour: main-out). */
|
||||
if (ctx.routing_mode === 'custom')
|
||||
detour = get_outbound(ctx.default_outbound, dm) || 'direct-out';
|
||||
else if (!isEmpty(ctx.main_node))
|
||||
detour = 'main-out';
|
||||
else
|
||||
detour = 'direct-out';
|
||||
}
|
||||
if (isEmpty(detour))
|
||||
detour = (ctx.routing_mode === 'custom') ? (get_outbound(ctx.default_outbound, dm) || 'direct-out') : 'direct-out';
|
||||
|
||||
const tag = 'hp-' + detour;
|
||||
rs.http_client = tag;
|
||||
|
||||
@@ -149,29 +149,3 @@ hp_restore_known_good_uci() {
|
||||
[ "$saved" = "1" ] && uci commit homeproxy-pro </dev/null || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# hp_file_digest <file>
|
||||
# Digest of <file>, or nothing when it is missing or empty. The reload path
|
||||
# uses it to tell "the generator produced the same bytes as what is running"
|
||||
# from "something actually changed".
|
||||
hp_file_digest() {
|
||||
[ -s "$1" ] || return 1
|
||||
|
||||
md5sum "$1" 2>"/dev/null" | awk '{ print $1 }'
|
||||
}
|
||||
|
||||
# hp_config_changed <live> <digest-before>
|
||||
# 0 when <live> differs from <digest-before> (or does not exist), 1 when the
|
||||
# two are byte-identical. An empty <digest-before> means there was nothing
|
||||
# running to compare against, which counts as a change.
|
||||
hp_config_changed() {
|
||||
local live="$1"
|
||||
local before="$2"
|
||||
local now
|
||||
|
||||
now="$(hp_file_digest "$live")" || return 0
|
||||
|
||||
[ -n "$before" ] && [ "$now" = "$before" ] && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -115,19 +115,7 @@ hp_dnsmasq_render_snippets() {
|
||||
hp_dnsmasq_has_nftset || nftset_ok=0
|
||||
|
||||
case "$routing_mode" in
|
||||
"bypass_mainland_china"|"custom"|"global"|"proxy_mainland_china")
|
||||
# Every one of these hands all queries to sing-box, which is the only
|
||||
# DNS front end the mode needs.
|
||||
#
|
||||
# proxy_mainland_china used to be the exception: it expanded
|
||||
# china_list.txt (111353 domains in the bundled list) into one
|
||||
# `server=` line each, so that only those domains reached sing-box.
|
||||
# Measured on a real device (ImmortalWrt 25.12.2, x86-64, dnsmasq
|
||||
# 2.93) that costs about 25 MB of RSS - 1.0 MB empty, 27.2 MB with
|
||||
# the list - while the outcome is the same: this mode's dns.final is
|
||||
# default-dns (the WAN resolver), so a mainland domain resolves
|
||||
# through the ISP either way. The split that actually matters is
|
||||
# decided on the route side, by geoip-cn/china-ip.
|
||||
"bypass_mainland_china"|"custom"|"global")
|
||||
cat <<-EOF > "$stage/redirect-dns.conf" || return 1
|
||||
no-poll
|
||||
no-resolv
|
||||
@@ -147,6 +135,12 @@ hp_dnsmasq_render_snippets() {
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"proxy_mainland_china")
|
||||
if [ -s "$hp_dir/resources/china_list.txt" ]; then
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
|
||||
"$hp_dir/resources/china_list.txt" > "$stage/china_list.conf" || return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$routing_mode" != "custom" ] && [ -s "$hp_dir/resources/proxy_list.txt" ]; then
|
||||
|
||||
@@ -515,21 +515,6 @@ reload_service() {
|
||||
HP_CANDIDATE_CLIENT=0
|
||||
HP_CANDIDATE_SERVER=0
|
||||
|
||||
# Digest what is running *before* the generators overwrite it: a reload
|
||||
# that produces the same bytes must not stop and start the instances.
|
||||
# service_triggers() reloads on every `interface.*.up` for wan, and a
|
||||
# flapping PPPoE link would otherwise interrupt every connection even when
|
||||
# the WAN resolver - the only thing that trigger can change - came back
|
||||
# the same.
|
||||
local digest_c="" digest_s="" server_enabled
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
digest_c="$(hp_file_digest "$RUN_DIR/sing-box-c.json")"
|
||||
fi
|
||||
config_get_bool server_enabled "server" "enabled" "0"
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
digest_s="$(hp_file_digest "$RUN_DIR/sing-box-s.json")"
|
||||
fi
|
||||
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
if ! ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"; then
|
||||
log "Error: new client configuration is invalid, reload aborted; the running configuration is kept."
|
||||
@@ -538,6 +523,8 @@ reload_service() {
|
||||
HP_CANDIDATE_CLIENT=1
|
||||
fi
|
||||
|
||||
local server_enabled
|
||||
config_get_bool server_enabled "server" "enabled" "0"
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
if ! ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"; then
|
||||
log "Error: new server configuration is invalid, reload aborted; the running configuration is kept."
|
||||
@@ -546,23 +533,6 @@ reload_service() {
|
||||
HP_CANDIDATE_SERVER=1
|
||||
fi
|
||||
|
||||
# Both sides regenerated to what is already live: keep them running. The
|
||||
# staged candidates are dropped too, so a later start that never validated
|
||||
# anything cannot pick them up.
|
||||
local reload_needed=0
|
||||
if [ "$outbound_node" != "nil" ] && hp_config_changed "$RUN_DIR/sing-box-c.json" "$digest_c"; then
|
||||
reload_needed=1
|
||||
fi
|
||||
if [ "$server_enabled" = "1" ] && hp_config_changed "$RUN_DIR/sing-box-s.json" "$digest_s"; then
|
||||
reload_needed=1
|
||||
fi
|
||||
|
||||
if [ "$reload_needed" = "0" ]; then
|
||||
rm -rf "$RUN_DIR/candidate"
|
||||
log "Reload: the generated configuration is unchanged; the running instances are kept."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# stop_service() deletes $RUN_DIR/sing-box-*.json (runtime state), so the
|
||||
# file that was just validated would not survive to start_service. Stage a
|
||||
# copy of each validated side first; the stop that follows cannot reach it,
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"luci.homeproxy-pro": [
|
||||
"acllist_read",
|
||||
"connection_check",
|
||||
"gfw_set_counts",
|
||||
"node_parse",
|
||||
"resources_get_version",
|
||||
"singbox_get_features",
|
||||
|
||||
@@ -18,11 +18,6 @@ function hasKernelModule(kmod) {
|
||||
return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellQuote(kmod))) === 0);
|
||||
}
|
||||
|
||||
/* The nft sets gfw_set_counts() reports on. A named constant rather than an
|
||||
* `in [...]` literal: this file keeps its membership tests on index() and
|
||||
* named arrays (arch-guard checks both). */
|
||||
const GFW_SET_FAMILIES = ['v4', 'v6'];
|
||||
|
||||
const HP_DIR = '/etc/homeproxy-pro';
|
||||
const RUN_DIR = '/var/run/homeproxy-pro';
|
||||
const LOG_PATH = `${RUN_DIR}/homeproxy-pro.log`;
|
||||
@@ -56,37 +51,6 @@ function collectFeatures() {
|
||||
return features;
|
||||
}
|
||||
|
||||
/* Element count of an nft set, or null when the set does not exist. Used by
|
||||
* gfw_set_counts(): the gfwlist routing mode decides with a set that dnsmasq
|
||||
* fills through `nftset=`, so an empty set means the mode is silently routing
|
||||
* everything direct. */
|
||||
function nft_set_size(name) {
|
||||
const ret = executeCommand('nft', '-j', 'list', 'set', 'inet', 'fw4', shellQuote(name));
|
||||
|
||||
if (ret == null || ret.exitcode !== 0 || isEmpty(ret.stdout))
|
||||
return null;
|
||||
|
||||
let doc;
|
||||
try {
|
||||
doc = json(ret.stdout);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* `elem` is what nft 1.1.6 emits (`nft -j list set`); older builds used
|
||||
* `elements`. Accepting both keeps the count right on either, because
|
||||
* the wrong key silently reads as "0 elements" - the very answer this
|
||||
* method exists to distinguish from "not filled". */
|
||||
for (let item in (doc?.nftables || [])) {
|
||||
const elements = item.set ? (item.set.elem || item.set.elements) : null;
|
||||
|
||||
if (elements)
|
||||
return length(elements);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
acllist_read: {
|
||||
args: { type: 'type' },
|
||||
@@ -472,23 +436,6 @@ const methods = {
|
||||
|
||||
return { running, log_tail: tail };
|
||||
}
|
||||
},
|
||||
|
||||
/* The gfwlist mode routes by an nft set that dnsmasq populates through
|
||||
* `nftset=`. A LAN client with its own DoH/DoT never asks this router's
|
||||
* DNS, the set stays empty, and the mode degrades to direct routing with
|
||||
* no visible symptom - the count is what separates "the mode is working"
|
||||
* from "the mode is doing nothing". */
|
||||
gfw_set_counts: {
|
||||
args: {},
|
||||
call: function() {
|
||||
let counts = {};
|
||||
|
||||
for (let family in GFW_SET_FAMILIES)
|
||||
counts[family] = nft_set_size('homeproxy_gfw_list_' + family);
|
||||
|
||||
return counts;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# DNS rule action fields that sing-box validates at load time
|
||||
# (route/rule/rule_dns.go, validateDNSRuleAction):
|
||||
#
|
||||
# * `race` and `speculative` cannot be combined on the same rule;
|
||||
# * `client_subnet` and `remove_client_subnet` are mutually exclusive.
|
||||
#
|
||||
# The rules below are a *legal* baseline: both sides of each pair are off.
|
||||
# The testbed opens one side with a one-line sed variation, so the case that
|
||||
# must be refused differs from the case that must pass by exactly the flag
|
||||
# under test.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config dns_rule 'dr_race'
|
||||
option enabled '1'
|
||||
option label 'dr-race'
|
||||
option action 'route'
|
||||
option server 'default-dns'
|
||||
option race '0'
|
||||
option speculative '0'
|
||||
|
||||
config dns_rule 'dr_subnet'
|
||||
option enabled '1'
|
||||
option label 'dr-subnet'
|
||||
option action 'route'
|
||||
option server 'default-dns'
|
||||
option client_subnet '1.2.3.0/24'
|
||||
option remove_client_subnet '0'
|
||||
|
||||
config dns_rule 'dr_eval'
|
||||
option enabled '1'
|
||||
option label 'dr-eval'
|
||||
option action 'evaluate'
|
||||
option server 'default-dns'
|
||||
option dns_timeout '5'
|
||||
@@ -1,33 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# A DNS server carrying HTTP headers. UCI stores them as a string list
|
||||
# ("Name: Value" per entry), sing-box wants map[string][]string, and emitting
|
||||
# the UCI list verbatim makes sing-box refuse the whole configuration
|
||||
# ("cannot unmarshal array into Go value of type badoption.HTTPHeader").
|
||||
# The generator converts one shape into the other; a name that appears twice
|
||||
# becomes a two-value header.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config dns_server 'ds_https'
|
||||
option enabled '1'
|
||||
option type 'https'
|
||||
option server '1.1.1.1'
|
||||
option path '/dns-query'
|
||||
list headers 'X-Test: one'
|
||||
list headers 'X-Test: two'
|
||||
list headers 'Authorization: Bearer t'
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# A routing rule pointing at a routing_node that was switched off afterwards.
|
||||
# generate_outbound() skips disabled nodes, so the tag the rule refers to is
|
||||
# generated nowhere and sing-box refuses the whole configuration with
|
||||
# "outbound not found: cfg-rn_off-out" - a message that never names the node
|
||||
# the user turned off. get_outbound() now refuses it the way
|
||||
# get_resolver()/get_ruleset() already do.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config node 'n_direct'
|
||||
option label 'n-direct'
|
||||
option type 'direct'
|
||||
|
||||
config routing_node 'rn_off'
|
||||
option enabled '0'
|
||||
option label 'rn-off'
|
||||
option node 'n_direct'
|
||||
|
||||
config routing_rule 'rr_off'
|
||||
option enabled '1'
|
||||
option label 'rr-off'
|
||||
option action 'route'
|
||||
option outbound 'rn_off'
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# A routing rule on the action the UI offers but gives no field to fill.
|
||||
# Match fields belong to the rule, not to the action payload, so this rule
|
||||
# reaches sing-box as `{"action": "route-options", ...}` with an empty action
|
||||
# object, and RouteOptionsActionOptions.UnmarshalJSON() refuses exactly that
|
||||
# with "empty route option action" - taking the whole configuration with it.
|
||||
# The generator refuses it by name before writing anything.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config routing_rule 'rr_empty'
|
||||
option enabled '1'
|
||||
option label 'rr-empty'
|
||||
option action 'route-options'
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Both TLS fragmentation switches at once. RouteOptionsActionOptions's
|
||||
# UnmarshalJSON() treats them as mutually exclusive
|
||||
# ("`tls_fragment` and `tls_record_fragment` are mutually exclusive"), and
|
||||
# the form shows both switches without hiding either when the other is on -
|
||||
# a user following the description ("try TLS record fragment first") can
|
||||
# easily tick both.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config routing_rule 'rr_tls'
|
||||
option enabled '1'
|
||||
option label 'rr-tls'
|
||||
option action 'route'
|
||||
option outbound 'direct-out'
|
||||
option tls_fragment '1'
|
||||
option tls_record_fragment '1'
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# A remote rule-set whose initial_path points outside the homeproxy-pro whitelist.
|
||||
# The path is read off the local disk as root, so the same gate as a local
|
||||
# rule-set's path applies - and it has to fail the same way. It used to be
|
||||
# dropped silently (initial_path := null), which left a rule-set the user
|
||||
# believed had a local copy downloading at first start instead.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config ruleset 'rs_seed'
|
||||
option enabled '1'
|
||||
option label 'rs-seed'
|
||||
option type 'remote'
|
||||
option format 'binary'
|
||||
option url 'https://example.invalid/ruleset.srs'
|
||||
option initial_path '/etc/passwd'
|
||||
|
||||
config routing_rule 'rr_seed'
|
||||
option enabled '1'
|
||||
option label 'rr-seed'
|
||||
option action 'route'
|
||||
option outbound 'direct-out'
|
||||
option rule_set 'rs_seed'
|
||||
@@ -1,51 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# A remote rule-set in a preset routing mode. It used to be fetched through
|
||||
# direct-out regardless of the mode, which fails on a router that can only
|
||||
# reach the rule-set host through the proxy - and the only trace is a line in
|
||||
# the sing-box log. The built-in geoip-cn / geosite-cn rule-sets already
|
||||
# download through main-out (route.uc sets download_detour for them).
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
option main_node 'n_vless'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config node 'n_vless'
|
||||
option label 'n-vless'
|
||||
option type 'vless'
|
||||
option address 'example.invalid'
|
||||
option port '443'
|
||||
option uuid '00000000-0000-0000-0000-000000000000'
|
||||
option tls '1'
|
||||
option tls_sni 'example.invalid'
|
||||
|
||||
config routing_node 'rn_main'
|
||||
option enabled '1'
|
||||
option label 'rn-main'
|
||||
option node 'n_vless'
|
||||
|
||||
config ruleset 'rs_remote'
|
||||
option enabled '1'
|
||||
option label 'rs-remote'
|
||||
option type 'remote'
|
||||
option format 'binary'
|
||||
option url 'https://example.invalid/ruleset.srs'
|
||||
|
||||
config routing_rule 'rr_rs'
|
||||
option enabled '1'
|
||||
option label 'rr-rs'
|
||||
option action 'route'
|
||||
option outbound 'rn_main'
|
||||
option rule_set 'rs_remote'
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# A remote rule-set whose update_interval is a bare number. sing-box parses
|
||||
# the field as a duration and refuses a number without a unit
|
||||
# ("time: missing unit in duration"), which rejects the whole configuration -
|
||||
# the same failure the DNS timeouts are normalised for. The form only offers
|
||||
# a placeholder ("1d"), not a format check.
|
||||
|
||||
config homeproxy-pro 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy-pro 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy-pro 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
config ruleset 'rs_remote'
|
||||
option enabled '1'
|
||||
option label 'rs-remote'
|
||||
option type 'remote'
|
||||
option format 'binary'
|
||||
option url 'https://example.invalid/ruleset.srs'
|
||||
option update_interval '3600'
|
||||
|
||||
config routing_rule 'rr_rs'
|
||||
option enabled '1'
|
||||
option label 'rr-rs'
|
||||
option action 'route'
|
||||
option outbound 'direct-out'
|
||||
option rule_set 'rs_remote'
|
||||
@@ -109,8 +109,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -303,8 +301,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -389,8 +385,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -532,8 +526,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -737,8 +729,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -837,8 +827,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -981,8 +969,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1134,8 +1120,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1220,8 +1204,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1429,8 +1411,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1496,8 +1476,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1645,8 +1623,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1829,8 +1805,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
@@ -1952,8 +1926,6 @@ nft flush chain inet fw4 homeproxy_mangle_tun
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun
|
||||
nft flush chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft delete chain inet fw4 homeproxy_mangle_tun_mark
|
||||
nft flush chain inet fw4 homeproxy_ipv6_guard
|
||||
nft delete chain inet fw4 homeproxy_ipv6_guard
|
||||
nft flush set inet fw4 homeproxy_local_addr_v4
|
||||
nft delete set inet fw4 homeproxy_local_addr_v4
|
||||
nft flush set inet fw4 homeproxy_local_addr_v6
|
||||
|
||||
@@ -133,35 +133,5 @@ check('cert path ignores an empty value',
|
||||
check('cert path ignores a missing section',
|
||||
hp.validateCertificatePath(null, '/etc/passwd') === true);
|
||||
|
||||
/* A query type name reaches sing-box's miekg/dns uppercase table
|
||||
* (option/types.go); a lowercase name is an "unknown DNS query type" and the
|
||||
* whole configuration is refused, which the user only ever saw as the reload
|
||||
* keeping the previous state. A numeric code is valid as-is. */
|
||||
check('query type accepts an uppercase name',
|
||||
hp.validateQueryType('sec1', 'AAAA') === true);
|
||||
check('query type accepts a numeric code',
|
||||
hp.validateQueryType('sec1', '65') === true);
|
||||
check('query type rejects a lowercase name',
|
||||
isError(hp.validateQueryType('sec1', 'aaaa')));
|
||||
check('query type rejects a name with a trailing space',
|
||||
isError(hp.validateQueryType('sec1', 'A ')));
|
||||
check('query type rejects a code above 65535',
|
||||
isError(hp.validateQueryType('sec1', '70000')));
|
||||
check('query type ignores an empty value',
|
||||
hp.validateQueryType('sec1', '') === true);
|
||||
check('query type ignores a missing section',
|
||||
hp.validateQueryType(null, 'aaaa') === true);
|
||||
|
||||
/* An uncompilable regex is not caught anywhere downstream either: sing-box
|
||||
* rejects the configuration as a whole. */
|
||||
check('regex accepts a valid pattern',
|
||||
hp.validateRegex('sec1', '^stun\\..+') === true);
|
||||
check('regex rejects an unbalanced group',
|
||||
isError(hp.validateRegex('sec1', '(unclosed')));
|
||||
check('regex ignores an empty value',
|
||||
hp.validateRegex('sec1', '') === true);
|
||||
check('regex ignores a missing section',
|
||||
hp.validateRegex(null, '(unclosed') === true);
|
||||
|
||||
console.log(`frontend validators: ${checks} checks, ${failures} failures`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
@@ -378,29 +378,5 @@ reset_samples
|
||||
hp_wait_instance "sing-box-c" "$CFG" 1
|
||||
expect "wait_instance (compat): times out when down" "$?" "1"
|
||||
|
||||
# A reload that regenerates identical bytes must not restart the instances:
|
||||
# service_triggers() reloads on every `interface.*.up` for wan, so a flapping
|
||||
# link used to interrupt every connection for a configuration that had not
|
||||
# changed. hp_config_changed() is the decision, and it is pure shell.
|
||||
printf 'configuration one\n' > "$WORK/live.json"
|
||||
DIGEST="$(hp_file_digest "$WORK/live.json")"
|
||||
expect "digest is stable" "$(hp_file_digest "$WORK/live.json")" "$DIGEST"
|
||||
|
||||
hp_config_changed "$WORK/live.json" "$DIGEST" && RC=1 || RC=0
|
||||
expect "an identical file is not a change" "$RC" "0"
|
||||
|
||||
printf 'configuration two\n' > "$WORK/live.json"
|
||||
hp_config_changed "$WORK/live.json" "$DIGEST" && RC=1 || RC=0
|
||||
expect "a rewritten file is a change" "$RC" "1"
|
||||
|
||||
hp_config_changed "$WORK/missing.json" "$DIGEST" && RC=1 || RC=0
|
||||
expect "a missing live file is a change" "$RC" "1"
|
||||
|
||||
hp_config_changed "$WORK/live.json" "" && RC=1 || RC=0
|
||||
expect "nothing to compare against is a change" "$RC" "1"
|
||||
|
||||
: > "$WORK/empty.json"
|
||||
expect "an empty file has no digest" "$(hp_file_digest "$WORK/empty.json")" ""
|
||||
|
||||
printf '%d checks, %d failures\n' "$CHECKS" "$FAILURES"
|
||||
exit $FAILED
|
||||
@@ -242,22 +242,6 @@ expect "an unusable dns_port does not restart dnsmasq" "$(restart_count)" "$GOOD
|
||||
expect "an unusable dns_port is reported in the log" \
|
||||
"$(grep -c 'dns_port' "$LOG")" "1"
|
||||
|
||||
echo
|
||||
echo "== proxy_mainland_china: every query goes to sing-box =="
|
||||
|
||||
# This mode used to expand china_list.txt (111353 domains in the bundled list)
|
||||
# into one `server=` line each, which measured at about 25 MB of dnsmasq RSS on
|
||||
# a real device (1.0 MB empty, 27.2 MB with the list) for no behavioural
|
||||
# difference: the mode's dns.final is default-dns, the WAN resolver, so a
|
||||
# mainland domain resolves through the ISP either way.
|
||||
run "proxy_mainland_china"
|
||||
expect "proxy_mainland_china installs the global redirect snippet" \
|
||||
"$(exists "$DNS_DIR/redirect-dns.conf")" "yes"
|
||||
expect "proxy_mainland_china no longer expands china_list.txt" \
|
||||
"$(exists "$DNS_DIR/china_list.conf")" "no"
|
||||
expect "the redirect snippet points at sing-box" \
|
||||
"$(grep -c "server=127.0.0.1#" "$DNS_DIR/redirect-dns.conf")" "1"
|
||||
|
||||
echo
|
||||
printf '%s checks, %s failures\n' "$CHECKS" "$FAILURES"
|
||||
if [ "$FAILED" != 0 ]; then
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
"ruleset",
|
||||
"Rule Set"
|
||||
],
|
||||
[
|
||||
"dns_cache",
|
||||
"DNS Settings"
|
||||
],
|
||||
[
|
||||
"control",
|
||||
"Access Control"
|
||||
@@ -271,7 +275,7 @@
|
||||
],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"validate": "fn(349:31d7a7fd11ff)"
|
||||
"validate": "fn(376:3a1eaf5f61c2)"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -494,7 +498,7 @@
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "nil",
|
||||
"load": "fn(347:1e6b4e82357b)",
|
||||
"load": "fn(347:bdff551797c2)",
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
@@ -508,7 +512,7 @@
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "default-dns",
|
||||
"load": "fn(326:b9d3cd52289f)",
|
||||
"load": "fn(326:345de0db50cf)",
|
||||
"rmempty": false
|
||||
}
|
||||
}
|
||||
@@ -593,7 +597,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"load": "fn(356:8d5ad606a86f)",
|
||||
"load": "fn(356:d9c0755071ec)",
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
@@ -669,7 +673,7 @@
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"editable": true,
|
||||
"load": "fn(284:01fc7f231969)",
|
||||
"load": "fn(284:530aca6575b5)",
|
||||
"validate": "fn(487:34bee8692641)"
|
||||
}
|
||||
},
|
||||
@@ -1024,7 +1028,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"load": "fn(219:0275f2977497)",
|
||||
"load": "fn(219:b7b6c816012f)",
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
@@ -1098,7 +1102,7 @@
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"editable": true,
|
||||
"load": "fn(263:a19a1b2bf7ba)",
|
||||
"load": "fn(263:6f52e51cea77)",
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
@@ -1111,10 +1115,6 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
@@ -1133,10 +1133,6 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
@@ -1155,10 +1151,6 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
@@ -1176,10 +1168,6 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
@@ -1197,10 +1185,6 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
@@ -1219,15 +1203,7 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
],
|
||||
{
|
||||
"tls_fragment": "1",
|
||||
"!reverse": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
@@ -1244,15 +1220,7 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
],
|
||||
{
|
||||
"tls_record_fragment": "1",
|
||||
"!reverse": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
@@ -1288,10 +1256,6 @@
|
||||
[
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
@@ -1311,10 +1275,6 @@
|
||||
"action",
|
||||
"route"
|
||||
],
|
||||
[
|
||||
"action",
|
||||
"route-options"
|
||||
],
|
||||
[
|
||||
"tls_spoof",
|
||||
{}
|
||||
@@ -1361,7 +1321,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"load": "fn(356:8d5ad606a86f)",
|
||||
"load": "fn(356:d9c0755071ec)",
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
@@ -1580,8 +1540,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"modalonly": true,
|
||||
"validate": "fn(184:0290625d9d6f)"
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1744,8 +1703,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"modalonly": true,
|
||||
"validate": "fn(184:0290625d9d6f)"
|
||||
"modalonly": true
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1753,100 +1711,16 @@
|
||||
},
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "dns_server",
|
||||
"title": "Overseas DNS server",
|
||||
"description": "Resolves every domain that is not on the China DNS path, through the selected proxy node. A domestic resolver answers these queries with polluted or NXDOMAIN results, which is what makes a foreign site fail to resolve while the proxy itself works - so only overseas services are offered here. Support UDP, TCP, DoH, DoQ, DoT. TCP protocol will be used if not specified.",
|
||||
"depends": [
|
||||
{
|
||||
"routing_mode": "custom",
|
||||
"!reverse": true
|
||||
}
|
||||
],
|
||||
"values": [
|
||||
[
|
||||
"1.1.1.1",
|
||||
"CloudFlare Public DNS (1.1.1.1)"
|
||||
],
|
||||
[
|
||||
"208.67.222.222",
|
||||
"Cisco Public DNS (208.67.222.222)"
|
||||
],
|
||||
[
|
||||
"8.8.8.8",
|
||||
"Google Public DNS (8.8.8.8)"
|
||||
]
|
||||
],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "8.8.8.8",
|
||||
"rmempty": false,
|
||||
"validate": "fn(730:48f7f360fbd2)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "china_dns_server",
|
||||
"title": "China DNS server",
|
||||
"description": "Resolves the domains on the China path, so it has to be a domestic service to get the correct local CDN answers. Keep the default; the upstream may block UDP/53, in which case a DoH address (https://dns.alidns.com/dns-query, https://doh.pub/dns-query) also works. Support UDP, TCP, DoH, DoQ, DoT. A DoH/DoT address here also sends the bootstrap lookup back to the WAN resolver: only a bare IP can resolve the proxy DNS endpoint without the ISP.",
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"bypass_mainland_china"
|
||||
]
|
||||
],
|
||||
"values": [
|
||||
[
|
||||
"wan",
|
||||
"WAN DNS (read from interface)"
|
||||
],
|
||||
[
|
||||
"223.5.5.5",
|
||||
"Aliyun Public DNS (223.5.5.5)"
|
||||
],
|
||||
[
|
||||
"210.2.4.8",
|
||||
"CNNIC Public DNS (210.2.4.8)"
|
||||
],
|
||||
[
|
||||
"119.29.29.29",
|
||||
"Tencent Public DNS (119.29.29.29)"
|
||||
],
|
||||
[
|
||||
"117.50.10.10",
|
||||
"ThreatBook Public DNS (117.50.10.10)"
|
||||
]
|
||||
],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "223.5.5.5",
|
||||
"rmempty": false,
|
||||
"validate": "fn(624:299cd4e865be)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "cn_ip_fallback",
|
||||
"title": "CN-IP DNS fallback (sing-box 1.14)",
|
||||
"description": "When the main DNS returns a mainland China IP, re-resolve via China DNS using evaluate/match_response.",
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"bypass_mainland_china"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "SectionValue",
|
||||
"name": "_dns",
|
||||
"description": "dns",
|
||||
"depends": [],
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"custom"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": {
|
||||
"id": "GridSection:_dns#5",
|
||||
@@ -1859,12 +1733,7 @@
|
||||
"name": "default_strategy",
|
||||
"title": "Default DNS strategy",
|
||||
"description": "The DNS strategy for resolving the domain name in the address.",
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"custom"
|
||||
]
|
||||
],
|
||||
"depends": [],
|
||||
"values": [
|
||||
[
|
||||
"",
|
||||
@@ -1895,17 +1764,12 @@
|
||||
"name": "default_server",
|
||||
"title": "Default DNS server",
|
||||
"description": null,
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"custom"
|
||||
]
|
||||
],
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "default-dns",
|
||||
"load": "fn(326:b9d3cd52289f)",
|
||||
"load": "fn(326:345de0db50cf)",
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
@@ -1939,12 +1803,7 @@
|
||||
"name": "client_subnet",
|
||||
"title": "EDNS Client subnet",
|
||||
"description": "Append a <code>edns0-subnet</code> OPT extra record with the specified IP prefix to every query by default.<br/>If value is an IP address instead of prefix, <code>/32</code> or <code>/128</code> will be appended automatically.",
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"custom"
|
||||
]
|
||||
],
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
@@ -2150,7 +2009,7 @@
|
||||
"kind": "DynamicList",
|
||||
"name": "headers",
|
||||
"title": "Headers",
|
||||
"description": "Additional headers to be sent to the DNS server, one <code>Name: Value</code> per line.",
|
||||
"description": "Additional headers to be sent to the DNS server.",
|
||||
"depends": [
|
||||
[
|
||||
"type",
|
||||
@@ -2205,7 +2064,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"load": "fn(384:2a0361acfbde)",
|
||||
"load": "fn(384:9fa59da460cf)",
|
||||
"modalonly": true,
|
||||
"validate": "fn(322:e76c15d647fc)"
|
||||
}
|
||||
@@ -2259,7 +2118,7 @@
|
||||
"props": {
|
||||
"default": "direct-out",
|
||||
"editable": true,
|
||||
"load": "fn(263:a19a1b2bf7ba)",
|
||||
"load": "fn(263:6f52e51cea77)",
|
||||
"rmempty": false
|
||||
}
|
||||
}
|
||||
@@ -2383,8 +2242,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"modalonly": true,
|
||||
"validate": "fn(286:47dea50f33ea)"
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2474,7 +2332,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"load": "fn(219:0275f2977497)",
|
||||
"load": "fn(219:b7b6c816012f)",
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
@@ -2552,7 +2410,7 @@
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"editable": true,
|
||||
"load": "fn(326:b9d3cd52289f)",
|
||||
"load": "fn(326:345de0db50cf)",
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
@@ -3022,8 +2880,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"modalonly": true,
|
||||
"validate": "fn(184:0290625d9d6f)"
|
||||
"modalonly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3211,8 +3068,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"modalonly": true,
|
||||
"validate": "fn(184:0290625d9d6f)"
|
||||
"modalonly": true
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -3290,13 +3146,9 @@
|
||||
"kind": "ListValue",
|
||||
"name": "format",
|
||||
"title": "Format",
|
||||
"description": "Leave empty to infer it from a <code>.json</code> or <code>.srs</code> path/url; sing-box requires it for any other extension.",
|
||||
"description": null,
|
||||
"depends": [],
|
||||
"values": [
|
||||
[
|
||||
"",
|
||||
"Automatic (by extension)"
|
||||
],
|
||||
[
|
||||
"binary",
|
||||
"Binary file"
|
||||
@@ -3308,7 +3160,8 @@
|
||||
],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"rmempty": true
|
||||
"default": "binary",
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3364,7 +3217,7 @@
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"load": "fn(293:81de93c500ec)"
|
||||
"load": "fn(293:8a1c8158e8ba)"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3425,6 +3278,177 @@
|
||||
},
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "dns_server",
|
||||
"title": "Overseas DNS server",
|
||||
"description": "Resolves every domain that is not on the China DNS path, through the selected proxy node. A domestic resolver answers these queries with polluted or NXDOMAIN results, which is what makes a foreign site fail to resolve while the proxy itself works - so only overseas services are offered here. Support UDP, TCP, DoH, DoQ, DoT. TCP protocol will be used if not specified.",
|
||||
"depends": [
|
||||
{
|
||||
"routing_mode": "custom",
|
||||
"!reverse": true
|
||||
}
|
||||
],
|
||||
"values": [
|
||||
[
|
||||
"1.1.1.1",
|
||||
"CloudFlare Public DNS (1.1.1.1)"
|
||||
],
|
||||
[
|
||||
"208.67.222.222",
|
||||
"Cisco Public DNS (208.67.222.222)"
|
||||
],
|
||||
[
|
||||
"8.8.8.8",
|
||||
"Google Public DNS (8.8.8.8)"
|
||||
]
|
||||
],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "8.8.8.8",
|
||||
"rmempty": false,
|
||||
"validate": "fn(730:48f7f360fbd2)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "china_dns_server",
|
||||
"title": "China DNS server",
|
||||
"description": "Resolves the domains on the China path, so it has to be a domestic service to get the correct local CDN answers. Keep the default; the upstream may block UDP/53, in which case a DoH address (https://dns.alidns.com/dns-query, https://doh.pub/dns-query) also works. Support UDP, TCP, DoH, DoQ, DoT.",
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"bypass_mainland_china"
|
||||
]
|
||||
],
|
||||
"values": [
|
||||
[
|
||||
"wan",
|
||||
"WAN DNS (read from interface)"
|
||||
],
|
||||
[
|
||||
"223.5.5.5",
|
||||
"Aliyun Public DNS (223.5.5.5)"
|
||||
],
|
||||
[
|
||||
"210.2.4.8",
|
||||
"CNNIC Public DNS (210.2.4.8)"
|
||||
],
|
||||
[
|
||||
"119.29.29.29",
|
||||
"Tencent Public DNS (119.29.29.29)"
|
||||
],
|
||||
[
|
||||
"117.50.10.10",
|
||||
"ThreatBook Public DNS (117.50.10.10)"
|
||||
]
|
||||
],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"default": "223.5.5.5",
|
||||
"rmempty": false,
|
||||
"validate": "fn(624:299cd4e865be)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "cn_ip_fallback",
|
||||
"title": "CN-IP DNS fallback (sing-box 1.14)",
|
||||
"description": "When the main DNS returns a mainland China IP, re-resolve via China DNS using evaluate/match_response.",
|
||||
"depends": [
|
||||
[
|
||||
"routing_mode",
|
||||
"bypass_mainland_china"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "SectionValue",
|
||||
"name": "_dns_cache",
|
||||
"description": "dns",
|
||||
"depends": [
|
||||
{
|
||||
"routing_mode": "custom",
|
||||
"!reverse": true
|
||||
}
|
||||
],
|
||||
"values": [],
|
||||
"subsection": {
|
||||
"id": "GridSection:_dns_cache#9",
|
||||
"kind": "GridSection",
|
||||
"name": "_dns_cache",
|
||||
"tabs": [],
|
||||
"options": [
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "optimistic_cache",
|
||||
"title": "Optimistic DNS cache",
|
||||
"description": "Return expired cache immediately and refresh in background (sing-box 1.14).",
|
||||
"depends": [
|
||||
[
|
||||
"disable_cache",
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"disable_cache_expire",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "optimistic_timeout",
|
||||
"title": "Optimistic cache timeout",
|
||||
"description": "Max time an expired entry may be served. Examples: 3d, 1h.",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "dns_timeout",
|
||||
"title": "DNS query timeout",
|
||||
"description": "Default timeout per DNS query in seconds (sing-box default: 10).",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"datatype": "uinteger"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "cache_file_store_dns",
|
||||
"title": "Store DNS cache",
|
||||
"description": "Persist DNS cache across restarts (sing-box 1.14, replaces Store RDRC).",
|
||||
"depends": [
|
||||
[
|
||||
"disable_cache",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"values": [],
|
||||
"subsection": null,
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"subsection": null
|
||||
},
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "SectionValue",
|
||||
"name": "_control",
|
||||
@@ -3432,7 +3456,7 @@
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"subsection": {
|
||||
"id": "GridSection:_control#9",
|
||||
"id": "GridSection:_control#10",
|
||||
"kind": "GridSection",
|
||||
"name": "_control",
|
||||
"tabs": [
|
||||
@@ -3488,12 +3512,12 @@
|
||||
"kind": "ListValue",
|
||||
"name": "lan_proxy_mode",
|
||||
"title": "Proxy filter mode",
|
||||
"description": "\"No filter\" proxies every LAN client, which is the default; the other two values select clients by the lists below.",
|
||||
"description": null,
|
||||
"depends": [],
|
||||
"values": [
|
||||
[
|
||||
"disabled",
|
||||
"No filter (proxy every client)"
|
||||
"Disable"
|
||||
],
|
||||
[
|
||||
"listed_only",
|
||||
|
||||
@@ -96,28 +96,6 @@ if ! grep -q '\.dnsmasq-nftset' "$ROOT/root/etc/init.d/homeproxy-pro"; then
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# --- ipv6_support off refuses IPv6 forwarding ------------------------------
|
||||
# The DNS side answers AAAA with REFUSED, but only for clients that ask this
|
||||
# router: a cached address or a client with its own resolver still reaches the
|
||||
# WAN. The guard is therefore a forward-hook base chain of its own - fw4's
|
||||
# forward chain jumps into the zone chains, whose accept rules end the traversal
|
||||
# before anything appended at the end of it is reached - ordered ahead of fw4's
|
||||
# forward (priority mangle vs filter).
|
||||
#
|
||||
# Asserted at the source level, like the gfwlist gate below: the render uses
|
||||
# the device's own UCI, so the ipv6_support branch cannot be exercised in both
|
||||
# of its states from here.
|
||||
GUARD_COND="$(grep -c "if (ipv6_support !== '1'):" "$TEMPLATE_SRC")"
|
||||
GUARD_HOOK="$(grep -c 'type filter hook forward priority mangle' "$TEMPLATE_SRC")"
|
||||
GUARD_RULE="$(grep -c 'meta nfproto ipv6 counter reject' "$TEMPLATE_SRC")"
|
||||
|
||||
if [ "$GUARD_COND" -ne 1 ] || [ "$GUARD_HOOK" -ne 1 ] || [ "$GUARD_RULE" -ne 1 ]; then
|
||||
echo "FAIL: the IPv6 forwarding guard is incomplete or ungated"
|
||||
echo " condition=$GUARD_COND hook=$GUARD_HOOK rule=$GUARD_RULE (each must be 1)"
|
||||
echo " a guard on priority filter would run after fw4's forward chain and never match"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# --- layer 2: the render -------------------------------------------------
|
||||
# The render output and its stderr live beside the staged template, never at a
|
||||
# fixed /tmp path. A caller that passes a work dir (tests/ucode/run.sh hands
|
||||
@@ -203,14 +181,6 @@ if grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" > "/dev/null"; then
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# The template defaults ipv6_support to '0', so the guard chain has to appear in
|
||||
# the render both on a device without the switch set and in CI, where there is
|
||||
# no /etc/config/homeproxy-pro at all.
|
||||
if ! grep -q "^chain homeproxy_ipv6_guard {" "$OUT"; then
|
||||
echo "FAIL: the IPv6 forwarding guard chain is missing from the rendered template"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "PASS: firewall_post.ut renders homeproxy-pro objects as standalone nft statements"
|
||||
echo "PASS: the gfwlist gate follows the dnsmasq nftset capability"
|
||||
|
||||
@@ -975,7 +975,7 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
# 3) P2 #4 / §2.2.1: the NAPTR (qtype 35) bypass suffixes must be
|
||||
# 3) P2 #4 / §2.2.1: the three NAPTR (qtype 35) bypass suffixes must be
|
||||
# emitted as a single domain_suffix entry backed by the
|
||||
# NAPTR_BYPASS_SUFFIXES constant. ucode's pretty-printer emits the
|
||||
# query_type array and the domain_suffix array as multi-line:
|
||||
@@ -983,6 +983,7 @@ fi
|
||||
# 35
|
||||
# ],
|
||||
# "domain_suffix": [
|
||||
# "r.10086.cn",
|
||||
# "10086.cn",
|
||||
# "pub.3gppnetwork.org"
|
||||
# ],
|
||||
@@ -999,7 +1000,7 @@ else
|
||||
# naptr_body should contain the three suffixes in order. If
|
||||
# any is missing, the layout shifted and the test fails loud.
|
||||
naptr_ok=1
|
||||
for sfx in '10086.cn' 'pub.3gppnetwork.org'; do
|
||||
for sfx in 'r.10086.cn' '10086.cn' 'pub.3gppnetwork.org'; do
|
||||
if ! printf '%s\n' "$naptr_body" | grep -q "\"$sfx\""; then
|
||||
echo "FAIL: naptr-bypass-domains: NAPTR rule is missing suffix \"$sfx\""
|
||||
echo " grep window:"
|
||||
@@ -1008,7 +1009,7 @@ else
|
||||
fi
|
||||
done
|
||||
if [ "$naptr_ok" = 1 ]; then
|
||||
echo "PASS: naptr-bypass-domains: qtype-35 rule domain_suffix carries every NAPTR_BYPASS_SUFFIXES entry"
|
||||
echo "PASS: naptr-bypass-domains: qtype-35 rule domain_suffix carries the 3 NAPTR_BYPASS_SUFFIXES entries"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1083,220 +1084,4 @@ run_case_type_error local-ruleset-bad-path "outside the homeproxy-pro whitelist"
|
||||
# covered by manual testing on r28. Re-add this case via a
|
||||
# custom_extra_tags.uci fixture when convenient.
|
||||
|
||||
|
||||
# --- review 2026-09-30: fields sing-box refuses at load time ----------------
|
||||
#
|
||||
# Each of these reached sing-box as a configuration it rejects outright - an
|
||||
# unknown field, an empty action object, a mutually exclusive pair - and the
|
||||
# only symptom the user saw was "my setting did not take". The generator now
|
||||
# refuses them by name before writing anything, which is what
|
||||
# run_case_type_error observes.
|
||||
cat > "$WORK/dns-field-guard.uc" <<'EOF'
|
||||
'use strict';
|
||||
|
||||
import { readfile } from 'fs';
|
||||
|
||||
const config = json(readfile(ARGV[0]));
|
||||
const mode = ARGV[1];
|
||||
|
||||
function has_aaaa() {
|
||||
for (let r in (config.dns?.rules || []))
|
||||
if (r.action === 'reject' && type(r.query_type) === 'array' && r.query_type[0] === 28)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case 'headers':
|
||||
for (let s in (config.dns?.servers || [])) {
|
||||
if (s.tag !== 'cfg-ds_https-dns')
|
||||
continue;
|
||||
if (type(s.headers) !== 'object') {
|
||||
print('headers is not an object');
|
||||
exit(1);
|
||||
}
|
||||
if (length(s.headers['X-Test'] || []) !== 2) {
|
||||
print('X-Test is not a two-value list');
|
||||
exit(1);
|
||||
}
|
||||
if (s.headers['X-Test'][0] !== 'one' || s.headers['X-Test'][1] !== 'two') {
|
||||
print('X-Test values are wrong');
|
||||
exit(1);
|
||||
}
|
||||
if ((s.headers['Authorization'] || [])[0] !== 'Bearer t') {
|
||||
print('Authorization is wrong');
|
||||
exit(1);
|
||||
}
|
||||
print('OK');
|
||||
exit(0);
|
||||
}
|
||||
print('cfg-ds_https-dns is missing');
|
||||
exit(1);
|
||||
case 'aaaa-present':
|
||||
print(has_aaaa() ? 'OK' : 'the AAAA reject rule is missing');
|
||||
exit(has_aaaa() ? 0 : 1);
|
||||
case 'aaaa-absent':
|
||||
print(has_aaaa() ? 'an AAAA reject rule was emitted with ipv6_support on' : 'OK');
|
||||
exit(has_aaaa() ? 1 : 0);
|
||||
case 'evaluate-timeout':
|
||||
for (let r in (config.dns?.rules || []))
|
||||
if (r.action === 'evaluate') {
|
||||
print(r.timeout === '5s' ? 'OK' : 'the evaluate rule carries no 5s timeout: ' + r.timeout);
|
||||
exit(r.timeout === '5s' ? 0 : 1);
|
||||
}
|
||||
print('no evaluate DNS rule was generated');
|
||||
exit(1);
|
||||
case 'http-client-detour':
|
||||
for (let c in (config.http_clients || [])) {
|
||||
if (c.detour !== 'main-out') {
|
||||
print('an http_client detours through ' + c.detour);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
print('OK');
|
||||
exit(0);
|
||||
case 'update-interval':
|
||||
for (let rs in (config.route?.rule_set || []))
|
||||
if (rs.tag === 'cfg-rs_remote-rule') {
|
||||
print(rs.update_interval === '3600s' ? 'OK' : 'update_interval is ' + rs.update_interval);
|
||||
exit(rs.update_interval === '3600s' ? 0 : 1);
|
||||
}
|
||||
print('cfg-rs_remote-rule is missing');
|
||||
exit(1);
|
||||
}
|
||||
print('unknown mode');
|
||||
exit(1);
|
||||
EOF
|
||||
|
||||
assert_guard() {
|
||||
local name="$1" json="$2" mode="$3" want="$4"
|
||||
local got
|
||||
|
||||
if [ ! -s "$json" ]; then
|
||||
echo "FAIL: $name: no configuration was generated"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
|
||||
got="$(ucode "$WORK/dns-field-guard.uc" "$json" "$mode" 2>&1)"
|
||||
if [ "$got" = "$want" ]; then
|
||||
echo "PASS: $name"
|
||||
else
|
||||
echo "FAIL: $name: $got"
|
||||
FAILED=1
|
||||
fi
|
||||
}
|
||||
|
||||
# 1) DNS server headers: the UCI string list has to become
|
||||
# map[string][]string, not a JSON array.
|
||||
run_case dns-headers-object "$ROOT/tests/fixtures/generators/dns_headers.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
assert_guard "dns-headers-object: the UCI list became a header object" \
|
||||
"$WORK/dns-headers-object/run/sing-box-c.json" headers OK
|
||||
|
||||
# 2) The baseline both variations below are measured against: with the flags
|
||||
# off, the fixture has to generate and pass sing-box check. Without it a
|
||||
# refusal in 3)-5) could just as well come from the fixture itself.
|
||||
run_case dns-action-guards-baseline "$ROOT/tests/fixtures/generators/dns_action_guards.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
|
||||
# 3) race + speculative on one rule.
|
||||
run_case_type_error dns-race-speculative "enables both race and speculative" \
|
||||
"$ROOT/tests/fixtures/generators/dns_action_guards.uci" generate_client.uc sing-box-c.json \
|
||||
"s#option race '0'#option race '1'#; s#option speculative '0'#option speculative '1'#"
|
||||
|
||||
# 4) race on an action that is not final (evaluate).
|
||||
run_case_type_error dns-race-final-action "race is only allowed on a final action" \
|
||||
"$ROOT/tests/fixtures/generators/dns_action_guards.uci" generate_client.uc sing-box-c.json \
|
||||
"s#option action 'route'#option action 'evaluate'#; s#option race '0'#option race '1'#"
|
||||
|
||||
# 5) client_subnet + remove_client_subnet.
|
||||
run_case_type_error dns-client-subnet-conflict "sets both client_subnet and remove_client_subnet" \
|
||||
"$ROOT/tests/fixtures/generators/dns_action_guards.uci" generate_client.uc sing-box-c.json \
|
||||
"s#option remove_client_subnet '0'#option remove_client_subnet '1'#"
|
||||
|
||||
# 6) route-options with no payload at all.
|
||||
run_case_type_error routing-route-options-empty "selects the route-options action but sets none of its fields" \
|
||||
"$ROOT/tests/fixtures/generators/routing_rule_route_options_empty.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
# 7) tls_fragment + tls_record_fragment.
|
||||
run_case_type_error routing-tls-fragment-conflict "enables both tls_fragment and tls_record_fragment" \
|
||||
"$ROOT/tests/fixtures/generators/routing_rule_tls_fragment.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
# 8) AAAA is refused when the router does not proxy IPv6, and left alone when
|
||||
# it does. firewall_post.ut emits no IPv6 rule at all with the switch off,
|
||||
# so a AAAA answer was the client's ticket straight out over the WAN.
|
||||
run_case ipv6-guard-off "$ROOT/tests/fixtures/generators/custom.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
assert_guard "ipv6-guard-off: AAAA is refused while ipv6_support is off" \
|
||||
"$WORK/ipv6-guard-off/run/sing-box-c.json" aaaa-present OK
|
||||
|
||||
run_case ipv6-guard-on "$ROOT/tests/fixtures/generators/client.uci" \
|
||||
generate_client.uc sing-box-c.json \
|
||||
"s#option ipv6_support '0'#option ipv6_support '1'#"
|
||||
assert_guard "ipv6-guard-on: no AAAA rule while ipv6_support is on" \
|
||||
"$WORK/ipv6-guard-on/run/sing-box-c.json" aaaa-absent OK
|
||||
|
||||
# 9) evaluate carries the same timeout override as route: emitting it for
|
||||
# route only made the field silently ineffective on an evaluate rule.
|
||||
run_case dns-evaluate-timeout "$ROOT/tests/fixtures/generators/dns_action_guards.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
assert_guard "dns-evaluate-timeout: the evaluate rule kept its query timeout" \
|
||||
"$WORK/dns-evaluate-timeout/run/sing-box-c.json" evaluate-timeout OK
|
||||
|
||||
# 10) A bare-number update_interval is a duration to sing-box; without a unit
|
||||
# it reports "time: missing unit in duration" and refuses everything.
|
||||
run_case ruleset-update-interval "$ROOT/tests/fixtures/generators/ruleset_remote_update_interval.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
assert_guard "ruleset-update-interval: '3600' became '3600s'" \
|
||||
"$WORK/ruleset-update-interval/run/sing-box-c.json" update-interval OK
|
||||
|
||||
# 11) A rule pointing at a routing_node that was switched off used to emit a
|
||||
# tag nothing generates ("outbound not found: cfg-rn_off-out").
|
||||
run_case_type_error routing-disabled-node "is disabled" \
|
||||
"$ROOT/tests/fixtures/generators/routing_rule_disabled_node.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
# 12) initial_path outside the whitelist: the same gate as a local rule-set
|
||||
# path, and the same fail-loud treatment (it used to be dropped silently).
|
||||
run_case_type_error ruleset-bad-initial-path "initial_path" \
|
||||
"$ROOT/tests/fixtures/generators/ruleset_remote_bad_initial_path.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
# 13) A preset mode downloads its remote rule-sets through the main outbound,
|
||||
# not direct-out: a router that can only reach the rule-set host through
|
||||
# the proxy otherwise just logs a failed download.
|
||||
run_case ruleset-remote-preset "$ROOT/tests/fixtures/generators/ruleset_remote_preset.uci" \
|
||||
generate_client.uc sing-box-c.json
|
||||
assert_guard "ruleset-remote-preset: the remote rule-set downloads through main-out" \
|
||||
"$WORK/ruleset-remote-preset/run/sing-box-c.json" http-client-detour OK
|
||||
|
||||
# 14) A missing china_ip4.json must not leave a `type: local` rule-set - and
|
||||
# the rule matching it - pointing at a path sing-box cannot open: that
|
||||
# fails the whole configuration, not just the rule. The mode keeps
|
||||
# working through geoip-cn. The staged bypass-mode case is reused with
|
||||
# the file removed and put back.
|
||||
CHINA_CASE="$WORK/client"
|
||||
if [ -f "$CHINA_CASE/resources/china_ip4.json" ]; then
|
||||
mv "$CHINA_CASE/resources/china_ip4.json" "$CHINA_CASE/resources/china_ip4.json.bak"
|
||||
rm -f "$CHINA_CASE/run/sing-box-c.json"
|
||||
if ( cd "$CHINA_CASE/scripts" && ucode -L "$CHINA_CASE/scripts" generate_client.uc 2>"$CHINA_CASE/missing.err" ); then
|
||||
if grep -q '"china-ip"' "$CHINA_CASE/run/sing-box-c.json"; then
|
||||
echo "FAIL: china-ip-missing: the configuration still references the missing rule-set"
|
||||
FAILED=1
|
||||
elif grep -q '"tag": "geoip-cn"' "$CHINA_CASE/run/sing-box-c.json"; then
|
||||
echo "PASS: china-ip-missing: the mainland split falls back to geoip-cn"
|
||||
else
|
||||
echo "FAIL: china-ip-missing: neither china-ip nor geoip-cn is declared"
|
||||
FAILED=1
|
||||
fi
|
||||
else
|
||||
echo "FAIL: china-ip-missing: generation failed"
|
||||
head -3 "$CHINA_CASE/missing.err"
|
||||
FAILED=1
|
||||
fi
|
||||
mv "$CHINA_CASE/resources/china_ip4.json.bak" "$CHINA_CASE/resources/china_ip4.json"
|
||||
else
|
||||
echo "FAIL: china-ip-missing: the client case did not stage china_ip4.json"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
exit ${FAILED:-0}
|
||||
exit ${FAILED:-0}
|
||||
@@ -224,9 +224,9 @@ check('certificate_write tolerates an empty request', threw == null, threw);
|
||||
* executed. Four of these were referenced by no test at all. */
|
||||
const all_methods = [
|
||||
'acllist_read', 'acllist_write', 'certificate_write', 'connection_check',
|
||||
'gfw_set_counts', 'log_clean', 'node_parse', 'resources_get_version',
|
||||
'resources_update', 'singbox_generator', 'singbox_get_features',
|
||||
'update_subscriptions', 'update_subscriptions_status'
|
||||
'log_clean', 'node_parse', 'resources_get_version', 'resources_update',
|
||||
'singbox_generator', 'singbox_get_features', 'update_subscriptions',
|
||||
'update_subscriptions_status'
|
||||
];
|
||||
|
||||
for (let m in all_methods) {
|
||||
|
||||
@@ -36,8 +36,12 @@ function landing_page()
|
||||
if not page or page == "auto" then
|
||||
if is_running("quickstart") then
|
||||
target = {"admin", "quickstart"}
|
||||
elseif is_running("routergo") then
|
||||
target = {"admin", "routerdog"}
|
||||
elseif check_wifi(uci) then
|
||||
target = {"admin", "status", "dashboard"}
|
||||
else
|
||||
target = {"admin", "status", "overview"}
|
||||
end
|
||||
|
||||
-- 2. 常规指定页面
|
||||
|
||||
Reference in new issue
Block a user