diff --git a/clashoo/Makefile b/clashoo/Makefile
index ef5ee9930..a4584de21 100644
--- a/clashoo/Makefile
+++ b/clashoo/Makefile
@@ -1,17 +1,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo
-PKG_SHORT_SHA:=e4dd968
-PKG_COMMIT_DATE:=2026.10.08
+PKG_SHORT_SHA:=bc76c13
+PKG_COMMIT_DATE:=2026.10.11
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
-PKG_RELEASE:=8
+PKG_RELEASE:=9
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
-PKG_SOURCE_VERSION:=7c870e53cf5004a1000b5c8d9b8caba5cf889927
+PKG_SOURCE_VERSION:=ee4c1d93df08d24d6006d5ce8547fee5d75ce99b
PKG_HASH:=skip
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
diff --git a/honk/Makefile b/honk/Makefile
index 507f11b54..0ab6f6c70 100644
--- a/honk/Makefile
+++ b/honk/Makefile
@@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=honk
PKG_VERSION:=2026.10.9_pre3
-PKG_RELEASE:=16
+PKG_RELEASE:=17
# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49)
# because that is what the GitHub release asset name uses.
diff --git a/honk/files/honk.config b/honk/files/honk.config
index f30d297c5..634ecf711 100644
--- a/honk/files/honk.config
+++ b/honk/files/honk.config
@@ -1,7 +1,7 @@
config honk 'config'
option enabled '0'
- option advanced '0'
+ option advanced '1'
option dashboard 'none'
option config_file '/etc/honk/config.dae'
diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel
index d7025695e..49f33a8c6 100644
--- a/luci-app-adguardhome/.official_channel
+++ b/luci-app-adguardhome/.official_channel
@@ -1 +1 @@
-release
+both
diff --git a/luci-app-clashoo/Makefile b/luci-app-clashoo/Makefile
index 4873fc40f..cbd221359 100644
--- a/luci-app-clashoo/Makefile
+++ b/luci-app-clashoo/Makefile
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-clashoo
PKG_VERSION:=1.31.0
-PKG_RELEASE:=6
+PKG_RELEASE:=7
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js
index ce9acdb76..056f719a8 100644
--- a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js
+++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js
@@ -247,7 +247,7 @@ function clKernelUnlock() {
return view.extend({
_busy: false,
- _op: null, /* 'start' | 'stop' | 'restart' | 'reload' | null */
+ _op: null, /* 'start' | 'stop' | null */
_opTimers: null, /* array of setTimeout IDs for cleanup */
_accessRefreshing: false,
_coreSwitchBusy: false,
@@ -789,29 +789,7 @@ return view.extend({
if (disabled) return;
return running ? self._stop() : self._start();
}
- }, [E('span', { 'class': 'cl-service-knob' })]),
- E('button', {
- type: 'button',
- 'class': 'btn cbi-button-action cl-btn-sm',
- title: _("Restart Service"),
- disabled: (disabled || !running) ? '' : null,
- click: function (ev) {
- ev.preventDefault();
- if (disabled || !running) return;
- return self._restart();
- }
- }, _("Restart")),
- E('button', {
- type: 'button',
- 'class': 'btn cbi-button-action cl-btn-sm',
- title: _("Reload Service"),
- disabled: (disabled || !running) ? '' : null,
- click: function (ev) {
- ev.preventDefault();
- if (disabled || !running) return;
- return self._reload();
- }
- }, _("Reload"))
+ }, [E('span', { 'class': 'cl-service-knob' })])
]);
},
@@ -1966,7 +1944,7 @@ return view.extend({
// preflight: refuse start when no profile selected.
// backend select_config also defends but toast is slow, UI would flash.
- if (opKey === 'start' || opKey === 'restart' || opKey === 'reload') {
+ if (opKey === 'start') {
var st0 = this._lastSt || {};
var hasConfig = !!(st0.config || st0.conf_path);
if (!hasConfig) {
@@ -1988,14 +1966,12 @@ return view.extend({
btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false');
btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off');
}
- self._showOpMsg(opKey === 'stop' ? _("Stopping…") : opKey === 'reload' ? _("Reloading…") : _("Starting…"));
+ self._showOpMsg(opKey === 'stop' ? _("Stopping…") : _("Starting…"));
}
var maxWait = opKey === 'stop' ? 15000 : 35000;
var started = Date.now();
var pollTimer = null;
- var initialPid = (this._lastSt && this._lastSt.pid) || 0;
- var sawStopped = false;
var rpcDone = false;
var finished = false;
@@ -2027,10 +2003,8 @@ return view.extend({
if (opKey === 'stop') {
if (rpcDone && st.running === false) return finish(_("Stopped") + ' ⚪');
} else {
- if (st.running === false) sawStopped = true;
if (rpcDone && st.health_status === 'fail') return finish(null);
- if (rpcDone && st.running === true &&
- (opKey !== 'restart' || (initialPid && st.pid && st.pid !== initialPid) || sawStopped))
+ if (rpcDone && st.running === true)
return finish(_("Running 🟢"));
}
if (elapsed >= maxWait) return finish(null);
@@ -2044,10 +2018,8 @@ return view.extend({
pollTimer = setTimeout(pollOnce, 100);
},
- _start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); },
- _stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
- _restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); },
- _reload: function () { return this._svc(function () { return clashoo.reload(); }, 'reload'); },
+ _start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); },
+ _stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
_updSubs: function (ev) {
var btn = ev && ev.target;
diff --git a/luci-app-homeproxy/Makefile b/luci-app-homeproxy/Makefile
index 98853e626..5ec8c9251 100644
--- a/luci-app-homeproxy/Makefile
+++ b/luci-app-homeproxy/Makefile
@@ -23,7 +23,7 @@ LUCI_DEPENDS:= \
PKG_NAME:=luci-app-homeproxy
PKG_VERSION:=27.919.1.14
-PKG_RELEASE:=22
+PKG_RELEASE:=23
define Package/luci-app-homeproxy/conffiles
/etc/config/homeproxy
diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/homeproxy.js b/luci-app-homeproxy/htdocs/luci-static/resources/homeproxy.js
index 195f7439e..1a56d2917 100644
--- a/luci-app-homeproxy/htdocs/luci-static/resources/homeproxy.js
+++ b/luci-app-homeproxy/htdocs/luci-static/resources/homeproxy.js
@@ -12,9 +12,6 @@
'require uci';
'require ui';
-/* Shared wording for the TLS / transport form blocks. Keeping the strings here
- makes node.js and server.js consume a single definition instead of two
- copies that have to be edited in lockstep. */
const TRANSPORT_NONE_HINT = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
const TRANSPORT_HTTP_HINT = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
const TRANSPORT_QUIC_HINT = _('No additional encryption support: It\'s basically duplicate encryption.');
@@ -38,28 +35,23 @@ return baseclass.extend({
},
shadowsocks_encrypt_length: {
- /* AEAD */
'aes-128-gcm': 0,
'aes-192-gcm': 0,
'aes-256-gcm': 0,
'chacha20-ietf-poly1305': 0,
'xchacha20-ietf-poly1305': 0,
- /* AEAD 2022 */
'2022-blake3-aes-128-gcm': 16,
'2022-blake3-aes-256-gcm': 32,
'2022-blake3-chacha20-poly1305': 32
},
shadowsocks_encrypt_methods: [
- /* Stream */
'none',
- /* AEAD */
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
- /* AEAD 2022 */
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305'
@@ -102,10 +94,6 @@ return baseclass.extend({
}
}),
- /* Build the transport option group shared by the node (client) and server
- forms. `options.side` selects the client/server wording and the handful of
- fields that only exist on one side; option names, defaults and dependency
- sets are otherwise identical. */
renderTransportOptions(section, options) {
const features = options.features || {},
side = options.side || 'client';
@@ -236,10 +224,6 @@ return baseclass.extend({
return o;
},
- /* Build the TLS option group shared by the node (client) and server forms.
- `options.type_depends` lists the node types the TLS flag applies to,
- `options.tls_forced_types` the types that force TLS on, and
- `options.oninsecurechange` the client-side confirm handler. */
renderTlsOptions(section, options) {
const side = options.side || 'client';
let o;
@@ -323,7 +307,6 @@ return baseclass.extend({
},
calcStringMD5(e) {
- /* Thanks to https://stackoverflow.com/a/41602636 */
let h = (a, b) => {
let c, d, e, f, g;
c = a & 2147483648;
@@ -400,7 +383,6 @@ return baseclass.extend({
if (!str)
return null;
- /* Thanks to luci-app-ssr-plus */
str = str.replace(/-/g, '+').replace(/_/g, '/');
let padding = (4 - str.length % 4) % 4;
if (padding)
@@ -427,14 +409,12 @@ return baseclass.extend({
byteArr = crypto.getRandomValues(new Uint8Array(length));
switch (type) {
case 'base64':
- /* Thanks to https://stackoverflow.com/questions/9267899 */
return btoa(String.fromCharCode.apply(null, byteArr));
case 'hex':
return Array.from(byteArr, (byte) =>
(byte & 255).toString(16).padStart(2, '0')
).join('');
case 'uuid':
- /* Thanks to https://stackoverflow.com/a/2117523 */
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
@@ -501,7 +481,6 @@ return baseclass.extend({
},
validateBase64Key(length, section_id, value) {
- /* Thanks to luci-proto-wireguard */
if (section_id && value)
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
@@ -526,7 +505,6 @@ return baseclass.extend({
else if (!value[2])
value[2] = 65535;
- /* numeric comparison: string '<' does lexicographic ordering */
if (parseInt(value[1], 10) < parseInt(value[2], 10) && value[2] <= 65535)
return true;
}
diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js
index 42604072e..2fe8789eb 100644
--- a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js
+++ b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js
@@ -87,7 +87,6 @@ return view.extend({
let features = data[1],
hosts = data[2]?.hosts;
- /* Cache all configured proxy nodes, they will be called multiple times */
let proxy_nodes = {};
uci.sections(data[0], 'node', (res) => {
let nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '',
@@ -352,8 +351,6 @@ return view.extend({
o.depends('proxy_mode', 'redirect_tun');
o.depends('proxy_mode', 'tun');
- /* Custom routing settings start */
- /* Routing settings start */
o = s.taboption('routing', form.SectionValue, '_routing', form.NamedSection, 'routing', 'homeproxy');
o.depends('routing_mode', 'custom');
@@ -443,9 +440,7 @@ return view.extend({
}
so.default = 'default-dns';
so.rmempty = false;
- /* Routing settings end */
- /* Routing nodes start */
s.tab('routing_node', _('Routing Nodes'));
o = s.taboption('routing_node', form.SectionValue, '_routing_node', form.GridSection, 'routing_node');
o.depends('routing_mode', 'custom');
@@ -614,9 +609,7 @@ return view.extend({
_('Interrupt existing connections when the selected outbound has changed.'));
so.depends('node', 'urltest');
so.modalonly = true;
- /* Routing nodes end */
- /* Routing rules start */
s.tab('routing_rule', _('Routing Rules'));
o = s.taboption('routing_rule', form.SectionValue, '_routing_rule', form.GridSection, 'routing_rule');
o.depends('routing_mode', 'custom');
@@ -958,9 +951,7 @@ return view.extend({
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
_('Match process path using regular expression.'));
so.modalonly = true;
- /* Routing rules end */
- /* DNS settings start */
s.tab('dns', _('DNS Settings'));
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'homeproxy');
o.depends('routing_mode', 'custom');
@@ -1015,9 +1006,7 @@ return view.extend({
_('Persist DNS cache across restarts (sing-box 1.14, replaces Store RDRC).'));
so.depends('disable_cache', '0');
so.rmempty = false;
- /* DNS settings end */
- /* DNS servers start */
s.tab('dns_server', _('DNS Servers'));
o = s.taboption('dns_server', form.SectionValue, '_dns_server', form.GridSection, 'dns_server');
o.depends('routing_mode', 'custom');
@@ -1138,9 +1127,7 @@ return view.extend({
so.default = 'direct-out';
so.rmempty = false;
so.editable = true;
- /* DNS servers end */
- /* DNS rules start */
s.tab('dns_rule', _('DNS Rules'));
o = s.taboption('dns_rule', form.SectionValue, '_dns_rule', form.GridSection, 'dns_rule');
o.depends('routing_mode', 'custom');
@@ -1470,10 +1457,7 @@ return view.extend({
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
_('Match process path using regular expression.'));
so.modalonly = true;
- /* DNS rules end */
- /* Custom routing settings end */
- /* Rule set settings start */
s.tab('ruleset', _('Rule Set'));
o = s.taboption('ruleset', form.SectionValue, '_ruleset', form.GridSection, 'ruleset');
o.depends('routing_mode', 'custom');
@@ -1584,15 +1568,12 @@ return view.extend({
_('Update interval of rule set.'));
so.placeholder = '1d';
so.depends('type', 'remote');
- /* Rule set settings end */
- /* ACL settings start */
s.tab('control', _('Access Control'));
o = s.taboption('control', form.SectionValue, '_control', form.NamedSection, 'control', 'homeproxy');
ss = o.subsection;
- /* Interface control start */
ss.tab('interface', _('Interface Control'));
so = ss.taboption('interface', widgets.DeviceSelect, 'listen_interfaces', _('Listen interfaces'),
@@ -1604,9 +1585,7 @@ return view.extend({
_('Bind outbound traffic to specific interface. Leave empty to auto detect.'));
so.multiple = false;
so.noaliases = true;
- /* Interface control end */
- /* 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'));
@@ -1649,9 +1628,7 @@ return view.extend({
so = fwtool.addMACOption(ss, 'lan_ip_policy', 'lan_global_proxy_mac_addrs', _('Global proxy MAC-s'), null, hosts);
so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true});
- /* LAN IP policy end */
- /* WAN IP policy start */
ss.tab('wan_ip_policy', _('WAN IP Policy'));
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'));
@@ -1667,9 +1644,7 @@ return view.extend({
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_direct_ipv6_ips', _('Direct IPv6 IP-s'));
so.datatype = 'or(ip6addr, cidr6)';
so.depends('homeproxy.config.ipv6_support', '1');
- /* WAN IP policy end */
- /* Proxy domain list start */
ss.tab('proxy_domain_list', _('Proxy Domain List'));
so = ss.taboption('proxy_domain_list', form.TextValue, '_proxy_domain_list');
@@ -1699,9 +1674,7 @@ return view.extend({
return true;
}
- /* Proxy domain list end */
- /* Direct domain list start */
ss.tab('direct_domain_list', _('Direct Domain List'));
so = ss.taboption('direct_domain_list', form.TextValue, '_direct_domain_list');
@@ -1731,10 +1704,7 @@ return view.extend({
return true;
}
- /* Direct domain list end */
- /* ACL settings end */
- /* DNS cache / timeout for preset routing modes (rightmost tab, stored in the dns section) */
s.tab('dns_cache', _('DNS Cache (1.14)'));
o = s.taboption('dns_cache', form.SectionValue, '_dns_cache', form.NamedSection, 'dns', 'homeproxy');
o.depends({'routing_mode': 'custom', '!reverse': true});
diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/node.js b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/node.js
index dc25eb39c..0d3b7eca7 100644
--- a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/node.js
+++ b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/node.js
@@ -26,11 +26,9 @@ function parseShareLink(uri, features) {
if (uri[0] && uri[1]) {
switch (uri[0]) {
case 'anytls':
- /* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
url = new URL('http://' + uri[1]);
params = url.searchParams;
- /* Check if password exists */
if (!url.username)
return null;
@@ -62,11 +60,9 @@ function parseShareLink(uri, features) {
break;
case 'hysteria':
- /* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
url = new URL('http://' + uri[1]);
params = url.searchParams;
- /* WeChat-Video / FakeTCP are unsupported by sing-box currently */
if (!features.with_quic || (params.get('protocol') && params.get('protocol') !== 'udp'))
return null;
@@ -90,7 +86,6 @@ function parseShareLink(uri, features) {
break;
case 'hysteria2':
case 'hy2':
- /* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
url = new URL('http://' + uri[1]);
params = url.searchParams;
@@ -114,7 +109,6 @@ function parseShareLink(uri, features) {
break;
case 'snell':
- /* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
url = new URL('http://' + uri[1]);
params = url.searchParams;
@@ -153,7 +147,6 @@ function parseShareLink(uri, features) {
break;
case 'ss':
try {
- /* "Lovely" Shadowrocket format */
try {
let suri = uri[1].split('#'), slabel = '';
if (suri.length <= 2) {
@@ -163,15 +156,12 @@ function parseShareLink(uri, features) {
}
} catch(e) { }
- /* SIP002 format https://shadowsocks.org/guide/sip002.html */
url = new URL('http://' + uri[1]);
let userinfo;
if (url.username && url.password) {
- /* User info encoded with URIComponent */
userinfo = [url.username, decodeURIComponent(url.password)];
} else if (url.username) {
- /* User info encoded with base64 */
userinfo = hp.decodeBase64Str(decodeURIComponent(url.username)).split(':');
if (userinfo.length > 1)
userinfo = [userinfo[0], userinfo.slice(1).join(':')]
@@ -184,9 +174,6 @@ function parseShareLink(uri, features) {
if (url.search && url.searchParams.get('plugin')) {
let plugin_info = url.searchParams.get('plugin').split(';');
plugin = plugin_info[0];
- /* sing-box only knows obfs-local; simple-obfs (an old
- alias providers still emit) would otherwise be rejected
- as "plugin not found" and take the whole config with it */
if (plugin === 'simple-obfs')
plugin = 'obfs-local';
plugin_opts = (plugin_info.length > 1) ? plugin_info.slice(1).join(';') : null;
@@ -203,7 +190,6 @@ function parseShareLink(uri, features) {
shadowsocks_plugin_opts: plugin_opts
};
} catch(e) {
- /* Legacy format https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
uri = uri[1].split('@');
if (uri.length < 2)
return null;
@@ -221,11 +207,9 @@ function parseShareLink(uri, features) {
break;
case 'trojan':
- /* https://p4gefau1t.github.io/trojan-go/developer/url/ */
url = new URL('http://' + uri[1]);
params = url.searchParams;
- /* Check if password exists */
if (!url.username)
return null;
@@ -256,11 +240,9 @@ function parseShareLink(uri, features) {
break;
case 'tuic':
- /* https://github.com/daeuniverse/dae/discussions/182 */
url = new URL('http://' + uri[1]);
params = url.searchParams;
- /* Check if uuid exists */
if (!url.username)
return null;
@@ -280,16 +262,13 @@ function parseShareLink(uri, features) {
break;
case 'vless':
- /* https://github.com/XTLS/Xray-core/discussions/716 */
url = new URL('http://' + uri[1]);
params = url.searchParams;
- /* Unsupported protocol */
if (params.get('type') === 'kcp')
return null;
else if (params.get('type') === 'quic' && ((params.get('quicSecurity') && params.get('quicSecurity') !== 'none') || !features.with_quic))
return null;
- /* Check if uuid and type exist */
if (!url.username || !params.get('type'))
return null;
@@ -307,9 +286,6 @@ function parseShareLink(uri, features) {
tls_reality_public_key: params.get('pbk') ? decodeURIComponent(params.get('pbk')) : null,
tls_reality_short_id: params.get('sid'),
tls_utls: features.with_utls ? params.get('fp') : null,
- /* only the value sing-box supports: any other flow is
- rejected with "unsupported flow" and one bad node breaks
- the whole generated config */
vless_flow: (['tls', 'reality'].includes(params.get('security')) && params.get('flow') === 'xtls-rprx-vision') ? params.get('flow') : null
};
switch (params.get('type')) {
@@ -340,26 +316,17 @@ function parseShareLink(uri, features) {
break;
case 'vmess':
- /* "Lovely" shadowrocket format; uri is the split scheme/rest
- array, so this has to test the link body (testing the array
- itself compared '&' against its elements and never matched) */
if (uri[1].includes('&'))
return null;
- /* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
uri = JSON.parse(hp.decodeBase64Str(uri[1]));
if (uri.v != '2')
return null;
- /* Unsupported protocols */
else if (uri.net === 'kcp')
return null;
else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || !features.with_quic))
return null;
- /* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
- * else if (uri.aid && parseInt(uri.aid) !== 0)
- * return null;
- */
config = {
label: uri.ps,
@@ -524,7 +491,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
}
o.modalonly = true;
- /* Direct config */
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
_('Write proxy protocol in the connection header.'));
o.value('', _('Disable'));
@@ -533,7 +499,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends('type', 'direct');
o.modalonly = true;
- /* AnyTLS config start */
o = s.option(form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
_('Interval checking for idle sessions, in seconds.'));
o.datatype = 'uinteger';
@@ -554,9 +519,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.placeholder = '0';
o.depends('type', 'anytls');
o.modalonly = true;
- /* AnyTLS config end */
- /* Hysteria (2) config start */
o = s.option(form.DynamicList, 'hysteria_hopping_port', _('Hopping port'));
o.depends('type', 'hysteria');
o.depends('type', 'hysteria2');
@@ -580,10 +543,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
o.value('udp');
- /* WeChat-Video / FakeTCP are unsupported by sing-box currently
- * o.value('wechat-video');
- * o.value('faketcp');
- */
o.default = 'udp';
o.depends('type', 'hysteria');
o.rmempty = false;
@@ -655,13 +614,10 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
_('Disable Chrome QUIC handshake parroting, which is enabled by default since sing-box 1.14. Turn this on only when the server uses an Ed25519 certificate or the handshake otherwise fails.'));
o.depends('type', 'hysteria2');
o.modalonly = true;
- /* Hysteria (2) config end */
- /* Shadowsocks config start */
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
for (let i of hp.shadowsocks_encrypt_methods)
o.value(i);
- /* Stream ciphers */
o.value('aes-128-ctr');
o.value('aes-192-ctr');
o.value('aes-256-ctr');
@@ -687,9 +643,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends('shadowsocks_plugin', 'obfs-local');
o.depends('shadowsocks_plugin', 'v2ray-plugin');
o.modalonly = true;
- /* Shadowsocks config end */
- /* ShadowTLS config */
o = s.option(form.ListValue, 'shadowtls_version', _('ShadowTLS version'));
o.value('1', _('v1'));
o.value('2', _('v2'));
@@ -699,7 +653,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.rmempty = false;
o.modalonly = true;
- /* Socks config */
o = s.option(form.ListValue, 'socks_version', _('Socks version'));
o.value('4', _('Socks4'));
o.value('4a', _('Socks4A'));
@@ -709,7 +662,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.rmempty = false;
o.modalonly = true;
- /* SSH config start */
o = s.option(form.Value, 'ssh_client_version', _('Client version'),
_('Random version will be used if empty.'));
o.depends('type', 'ssh');
@@ -733,9 +685,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.password = true;
o.depends('type', 'ssh');
o.modalonly = true;
- /* SSH config end */
- /* TUIC config start */
o = s.option(form.Value, 'uuid', _('UUID'));
o.password = true;
o.depends('type', 'tuic');
@@ -779,9 +729,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.default = '10';
o.depends('type', 'tuic');
o.modalonly = true;
- /* Tuic config end */
- /* VMess / VLESS config start */
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
o.value('', _('None'));
o.value('xtls-rprx-vision');
@@ -816,13 +764,9 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
_('Protocol parameter. Enable length block encryption.'));
o.depends('type', 'vmess');
o.modalonly = true;
- /* VMess config end */
- /* Transport config start */
hp.renderTransportOptions(s, { features: features, side: 'client' });
- /* Transport config end */
- /* Wireguard config start */
o = s.option(form.DynamicList, 'wireguard_local_address', _('Local address'),
_('List of IP (v4 or v6) addresses prefixes to be assigned to the interface.'));
o.datatype = 'cidr';
@@ -868,9 +812,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.datatype = 'uinteger';
o.depends('type', 'wireguard');
o.modalonly = true;
- /* Wireguard config end */
- /* Mux config start */
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
o.depends('type', 'shadowsocks');
o.depends('type', 'trojan');
@@ -927,9 +869,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.datatype = 'uinteger';
o.depends('multiplex_brutal', '1');
o.modalonly = true;
- /* Mux config end */
- /* Snell config start */
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
_('sing-box implements Snell v4/v5 wire as v4 and v6. The pre-shared key (Password above) must be 12-255 bytes for v6.'));
o.value('4', _('v4'));
@@ -967,9 +907,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.value('unsafe-raw', _('unsafe-raw'));
o.depends({'type': 'snell', 'snell_version': '6'});
o.modalonly = true;
- /* Snell config end */
- /* TLS config start */
hp.renderTlsOptions(s, {
side: 'client',
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'shadowtls', 'trojan', 'tuic', 'vless', 'vmess' ],
@@ -1063,9 +1001,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends('tls_reality', '1');
o.modalonly = true;
}
- /* TLS config end */
- /* Extra settings start */
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'));
o.modalonly = true;
@@ -1088,7 +1024,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.default = '2';
o.depends('udp_over_tcp', '1');
o.modalonly = true;
- /* Extra settings end */
return s;
}
@@ -1107,7 +1042,6 @@ return view.extend({
let routing_mode = uci.get(data[0], 'config', 'routing_mode');
let features = data[1];
- /* Cache subscription information, it will be called multiple times */
let subinfo = [];
for (let suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
const url = new URL(suburl);
@@ -1120,8 +1054,6 @@ return view.extend({
s = m.section(form.NamedSection, 'subscription', 'homeproxy');
- /* Node settings start */
- /* User nodes start */
s.tab('node', _('Nodes'));
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
@@ -1133,8 +1065,6 @@ return view.extend({
return true;
}
- /* Import subscription links start */
- /* Thanks to luci-app-shadowsocks-libev */
ss.handleLinkImport = function() {
let textarea = new ui.Textarea();
ui.showModal(_('Import share links'), [
@@ -1151,7 +1081,6 @@ return view.extend({
click: ui.createHandlerFn(this, () => {
let input_links = textarea.getValue().trim().split('\n');
if (input_links && input_links[0]) {
- /* Remove duplicate lines */
input_links = input_links.reduce((pre, cur) =>
(!pre.includes(cur) && pre.push(cur), pre), []);
@@ -1163,9 +1092,6 @@ return view.extend({
try {
config = parseShareLink(l, features);
} catch (e) {
- /* a single unparsable link must not abort
- the whole import (the changes made so
- far were left unsaved before) */
config = null;
}
if (config) {
@@ -1177,10 +1103,6 @@ return view.extend({
let nameHash = hp.calcStringMD5(config.label);
let previous = uci.get(data[0], nameHash) || {};
let sid = uci.add(data[0], 'node', nameHash);
- /* drop options the node no longer has:
- uci.set() only writes the keys below,
- so re-importing a changed protocol
- used to leave the old fields behind */
Object.keys(previous).forEach((k) => {
if (k.charAt(0) !== '.' && !(k in config))
uci.unset(data[0], sid, k);
@@ -1239,10 +1161,7 @@ return view.extend({
return el;
}
- /* Import subscription links end */
- /* User nodes end */
- /* Subscription nodes start */
for (const info of subinfo) {
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
o = s.taboption('sub_' + info.hash, form.SectionValue, '_sub_' + info.hash, form.GridSection, 'node');
@@ -1251,10 +1170,7 @@ return view.extend({
return (uci.get(data[0], section_id, 'grouphash') === info.hash);
}
}
- /* Subscription nodes end */
- /* Node settings end */
- /* Subscriptions settings start */
s.tab('subscription', _('Subscriptions'));
o = s.taboption('subscription', form.Flag, 'auto_update', _('Auto update'),
@@ -1383,7 +1299,6 @@ return view.extend({
return this.map.save(null, true);
}
- /* Subscriptions settings end */
return m.render();
}
diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/server.js b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/server.js
index da6e43ffe..867f5f7e4 100644
--- a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/server.js
+++ b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/server.js
@@ -104,7 +104,6 @@ function handleGenKey(option) {
password = hp.generateRand('hex', 16);
break;
}
- /* AEAD */
((length) => {
if (length && length > 0)
password = hp.generateRand('base64', length);
@@ -242,19 +241,13 @@ return view.extend({
}
o.modalonly = true;
- /* AnyTLS config */
o = s.option(form.DynamicList, 'anytls_padding_scheme', _('Padding scheme'),
_('AnyTLS padding scheme in array.'));
o.depends('type', 'anytls');
o.modalonly = true;
- /* Hysteria (2) config start */
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
o.value('udp');
- /* WeChat-Video / FakeTCP are unsupported by sing-box currently
- o.value('wechat-video');
- o.value('faketcp');
- */
o.default = 'udp';
o.depends('type', 'hysteria');
o.rmempty = false;
@@ -323,9 +316,7 @@ return view.extend({
_('HTTP3 server behavior when authentication fails.
A 404 page will be returned if empty.'));
o.depends('type', 'hysteria2');
o.modalonly = true;
- /* Hysteria (2) config end */
- /* Snell config start */
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
_('The pre-shared key (password) above must be 12-255 bytes for v6.'));
o.value('5', _('v5'));
@@ -348,9 +339,7 @@ return view.extend({
o.value('unsafe-raw', _('unsafe-raw'));
o.depends({'type': 'snell', 'snell_version': '6'});
o.modalonly = true;
- /* Snell config end */
- /* Shadowsocks config */
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
for (let i of hp.shadowsocks_encrypt_methods)
o.value(i);
@@ -358,7 +347,6 @@ return view.extend({
o.depends('type', 'shadowsocks');
o.modalonly = true;
- /* Tuic config start */
o = s.option(CBIGenValue, 'uuid', _('UUID'));
o.password = true;
o.depends('type', 'tuic');
@@ -395,9 +383,7 @@ return view.extend({
o.default = '10';
o.depends('type', 'tuic');
o.modalonly = true;
- /* Tuic config end */
- /* VLESS / VMess config start */
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
o.value('', _('None'));
o.value('xtls-rprx-vision');
@@ -409,13 +395,9 @@ return view.extend({
o.datatype = 'uinteger';
o.depends('type', 'vmess');
o.modalonly = true;
- /* VMess config end */
- /* Transport config start */
hp.renderTransportOptions(s, { features: features, side: 'server' });
- /* Transport config end */
- /* Mux config start */
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
o.depends('type', 'shadowsocks');
o.depends('type', 'trojan');
@@ -445,9 +427,7 @@ return view.extend({
o.depends('multiplex_brutal', '1');
o.modalonly = true;
}
- /* Mux config end */
- /* TLS config start */
hp.renderTlsOptions(s, {
side: 'server',
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'naive', 'trojan', 'tuic', 'vless', 'vmess' ],
@@ -725,9 +705,7 @@ return view.extend({
o.rows = 3;
o.depends('tls', '1');
o.modalonly = true;
- /* TLS config end */
- /* Extra settings start */
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
_('Enable tcp fast open for listener.'));
o.depends({'network': 'udp', '!reverse': true});
@@ -766,7 +744,6 @@ return view.extend({
o = s.option(form.Flag, 'reuse_addr', _('Reuse address'),
_('Reuse listener address.'));
o.modalonly = true;
- /* Extra settings end */
return m.render();
}
diff --git a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js
index d22a42b51..ed545f1de 100644
--- a/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js
+++ b/luci-app-homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js
@@ -14,13 +14,10 @@
'require ui';
'require view';
-/* Thanks to luci-app-aria2 */
const css = ' \
-#log_textarea { \
padding: 10px; \
text-align: left; \
} \
-#log_textarea pre { \
padding: .5rem; \
word-break: break-all; \
margin: 0; \
@@ -257,7 +254,6 @@ return view.extend({
o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list');
o.rawhtml = true;
- /* local rule-sets (used by bypass_mainland_china when present) */
o = s.option(form.DummyValue, '_geoip_cn_version', _('GeoIP CN rule-set version'));
o.cfgvalue = L.bind(getResVersion, this, o, 'geoip_cn');
o.rawhtml = true;
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_pre.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_pre.uc
index b70ddcc40..a0b37d609 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_pre.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_pre.uc
@@ -21,8 +21,6 @@ if (match(proxy_mode, /tun/)) {
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
if (outbound_node !== 'nil') {
- /* the name is interpolated into an nftables rule: keep it to what an
- interface name can actually be */
const raw_tun = trim(uci.get(cfgname, 'infra', 'tun_name') ?? '');
tun_name = match(raw_tun, /^[A-Za-z0-9._-]{1,15}$/) ? raw_tun : 'singtun0';
}
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/fw4_names.sh b/luci-app-homeproxy/root/etc/homeproxy/scripts/fw4_names.sh
index cdff5ee94..285129291 100644
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/fw4_names.sh
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/fw4_names.sh
@@ -1,21 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2025 ImmortalWrt.org
-#
-# Single source of truth for every fw4 chain and set owned by homeproxy.
-#
-# * root/etc/homeproxy/scripts/firewall_post.ut declares these objects and
-# decides which of them actually exist for the current proxy mode.
-# * root/etc/init.d/homeproxy flushes and deletes them on stop.
-#
-# When you add or remove a chain/set in firewall_post.ut, update this list in
-# the same commit -- tests/ucode/test_fw4_names.sh compares both sides and
-# fails when they drift.
-#
-# Each object is cleaned up independently: the cleanup loop deletes one at a
-# time so that a chain/set which does not exist in the current mode cannot
-# abort the whole batch. A single `nft -f` batch is atomic and would roll back
-# every preceding command in that case.
HP_FW4_CHAINS="
homeproxy_output_redir
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
index b37298417..04749fb2e 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
@@ -20,9 +20,7 @@ import {
const ubus = connect();
-/* const features = ubus.call('luci.homeproxy', 'singbox_get_features') || {}; */
-/* UCI config start */
const uci = cursor();
const uciconfig = 'homeproxy';
@@ -45,17 +43,6 @@ const uciruleset = 'ruleset';
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
-/*
- * Single source of truth for "which config branch are we generating".
- *
- * The three generation chains below (DNS, outbounds, route) must be selected by
- * the routing mode, NOT by whether main_node / default_outbound happen to be
- * set. Those two answer a different question: in custom mode default_outbound
- * is legitimately 'nil' (every routing rule picks its own outbound, there is no
- * default) and main_node is never read. Deriving the mode from them made a
- * perfectly normal custom config fall through every branch and emit a config
- * containing none of the user's DNS servers, nodes, rules or rule-sets.
- */
const is_custom_mode = (routing_mode === 'custom');
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
@@ -96,14 +83,12 @@ if (routing_mode !== 'custom') {
proxy_domain_list = proxy_list_raw ? split(trim(proxy_list_raw), /[\r\n]/) : [];
} else {
- /* DNS settings */
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
- /* Routing settings */
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
default_outbound_dns = uci.get(uciconfig, uciroutingsetting, 'default_outbound_dns') || 'default-dns';
domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy');
@@ -148,7 +133,6 @@ if (match(proxy_mode, /tun/)) {
}
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
-/* UCI config end */
const tun_dns_mode_raw = uci.get(uciconfig, ucimain, 'tun_dns_mode'),
tun_dns_address = uci.get(uciconfig, ucimain, 'tun_dns_address'),
@@ -160,13 +144,6 @@ const tun_dns_mode = (tun_dns_mode_raw === 'default') ? '' : tun_dns_mode_raw,
udp_mapping = (udp_mapping_raw === 'default') ? '' : udp_mapping_raw,
udp_filtering = (udp_filtering_raw === 'default') ? '' : udp_filtering_raw;
-/* Config helper start */
-/*
- * Direct-node destination override, keyed by node section name. It must be
- * declared before the helpers that touch it: ucode resolves let/const
- * lexically and does not hoist them, so a function defined earlier would
- * resolve the name as an undeclared global and throw under strict mode.
- */
const direct_overrides = {};
function parse_port(strport) {
@@ -256,28 +233,22 @@ function generate_outbound(node) {
server: node.address,
server_port: strToInt(node.port),
- /* Hysteria(2) */
server_ports: node.hysteria_hopping_port,
username: (node.type !== 'ssh') ? node.username : null,
user: (node.type === 'ssh') ? node.username : null,
- /* Snell authenticates with psk instead of password */
password: (node.type !== 'snell') ? node.password : null,
psk: (node.type === 'snell') ? node.password : null,
userkey: (node.type === 'snell') ? node.snell_userkey : null,
reuse: (node.type === 'snell') ? strToBool(node.snell_reuse) : null,
- /* Snell v4: HTTP obfuscation; v6: traffic shaping mode */
obfs_mode: (node.type === 'snell') ? (node.snell_obfs_mode || null) : null,
obfs_host: (node.type === 'snell') ? (node.snell_obfs_host || null) : null,
mode: (node.type === 'snell') ? (node.snell_mode || null) : null,
- /* Direct */
proxy_protocol: strToInt(node.proxy_protocol),
- /* AnyTLS */
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
min_idle_session: strToInt(node.anytls_min_idle_session),
- /* Hysteria (2) */
hop_interval: strToTime(node.hysteria_hop_interval),
hop_interval_max: strToTime(node.hysteria_hop_interval_max),
up_mbps: strToInt(node.hysteria_up_mbps),
@@ -290,29 +261,23 @@ function generate_outbound(node) {
} : node.hysteria_obfs_password,
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
- /* sing-box 1.14: Hysteria2 QUIC params (Hysteria v1 recv-window tuning removed upstream) */
bbr_profile: (node.type === 'hysteria2') ? (node.hysteria_bbr_profile || null) : null,
disable_chrome_parrot: (node.type === 'hysteria2' && node.hysteria_disable_chrome_parrot === '1') ? true : null,
- /* Shadowsocks */
method: node.shadowsocks_encrypt_method,
plugin: node.shadowsocks_plugin,
plugin_opts: node.shadowsocks_plugin_opts,
- /* ShadowTLS / Socks / Snell */
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : ((node.type === 'snell') ? (strToInt(node.snell_version) || 4) : null)),
- /* SSH */
client_version: node.ssh_client_version,
host_key: node.ssh_host_key,
host_key_algorithms: node.ssh_host_key_algo,
private_key: node.ssh_priv_key,
private_key_passphrase: node.ssh_priv_key_pp,
- /* Tuic */
uuid: node.uuid,
congestion_control: node.tuic_congestion_control,
udp_relay_mode: node.tuic_udp_relay_mode,
udp_over_stream: strToBool(node.tuic_udp_over_stream),
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
heartbeat: strToTime(node.tuic_heartbeat),
- /* VLESS / VMess */
flow: node.vless_flow,
alter_id: strToInt(node.vmess_alterid),
security: node.vmess_encrypt,
@@ -344,8 +309,6 @@ function generate_outbound(node) {
udp_fragment: strToBool(node.udp_fragment)
};
- /* Direct-node destination override: sing-box removed these options from
- the direct outbound since 1.13; emit them via the route action instead */
if (node.type === 'direct' && (!isEmpty(node.override_address) || !isEmpty(node.override_port)))
direct_overrides[node['.name']] = {
override_address: node.override_address,
@@ -432,11 +395,6 @@ function isDirectOutboundTag(tag) {
return !isEmpty(node) && node.type === 'direct';
}
-/*
- * Remote rule-sets are downloaded through a top-level http_client in sing-box
- * 1.14 (the per-rule-set download_detour was removed). One client is created
- * per distinct detour and reused by every rule-set that dials through it.
- */
const http_clients = [];
const http_seen = {};
@@ -450,9 +408,6 @@ function http_client_for(detour) {
http_seen[detour] = true;
const client = { tag: tag };
- /* sing-box 1.14 rejects an empty detour on the direct outbound (pure TUN
- mode has no self_mark on it); omitting detour uses the same system
- direct dialer, so behaviour is unchanged. */
if (!(isEmpty(self_mark) && isDirectOutboundTag(detour)))
client.detour = detour;
@@ -461,10 +416,6 @@ function http_client_for(detour) {
return tag;
}
-/*
- * sing-box requires dns.servers[].headers to be a map, while the LuCI field is
- * a dynamic list of "Name: value" lines (and older configs may hold either).
- */
function parse_headers(headers) {
if (isEmpty(headers))
return null;
@@ -482,11 +433,9 @@ function parse_headers(headers) {
return isEmpty(out) ? null : out;
}
-/* Config helper end */
const config = {};
-/* Log */
config.log = {
disabled: false,
level: log_level,
@@ -494,7 +443,6 @@ config.log = {
timestamp: true
};
-/* NTP */
if (!isEmpty(ntp_server))
config.ntp = {
enabled: true,
@@ -503,8 +451,6 @@ if (!isEmpty(ntp_server))
domain_resolver: 'default-dns',
};
-/* DNS start */
-/* Default settings */
config.dns = {
servers: [
{
@@ -532,7 +478,6 @@ config.dns = {
};
if (!isEmpty(main_node)) {
- /* Main DNS */
push(config.dns.servers, {
tag: 'main-dns',
domain_resolver: {
@@ -551,8 +496,6 @@ if (!isEmpty(main_node)) {
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns'
});
- /* Reject SVCB/HTTPS queries to avoid proxy DNS timeout on null domains.
- Opt out with `block_https_rr 0` when clients should use HTTPS RR/ECH. */
if (uci.get(uciconfig, ucimain, 'block_https_rr') !== '0')
push(config.dns.rules, {
query_type: [64, 65],
@@ -564,18 +507,12 @@ if (!isEmpty(main_node)) {
tag: 'china-dns',
domain_resolver: {
server: 'default-dns',
- /* follow the IPv6 setting like main-dns does, instead of
- preferring AAAA even on IPv4-only setups */
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null
},
detour: self_mark ? 'direct-out' : null,
...parse_dnsserver(china_dns_server)
});
- /* Route NAPTR (qtype 35) queries for SIP/ENUM domains to the ISP
- default-dns directly: china-dns (223.5.5.5) has intermittent
- multi-second first responses for NAPTR, causing 'context deadline
- exceeded' (e.g. sipgz12.hbq.r.10086.cn) */
push(config.dns.rules, {
query_type: [35],
domain_suffix: ['r.10086.cn', '10086.cn', 'pub.3gppnetwork.org'],
@@ -596,7 +533,6 @@ if (!isEmpty(main_node)) {
server: 'china-dns'
});
- /* sing-box 1.14: restore CN-IP fallback via evaluate/match_response (opt-in) */
if (uci.get(uciconfig, ucimain, 'cn_ip_fallback') === '1') {
push(config.dns.rules, {
action: 'evaluate',
@@ -612,7 +548,6 @@ if (!isEmpty(main_node)) {
}
}
} else if (is_custom_mode) {
- /* DNS servers */
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
if (cfg.enabled !== '1')
return;
@@ -640,9 +575,6 @@ if (!isEmpty(main_node)) {
});
});
- /* DNS rules */
- /* sing-box >= 1.14: legacy address-filter rules are auto-wrapped with an
- evaluate action; deprecated strategy/accept_empty fields are dropped. */
const builtin_dns_rules = [];
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
if (cfg.enabled !== '1')
@@ -705,10 +637,6 @@ if (!isEmpty(main_node)) {
const legacy_filter = !isEmpty(cfg.ip_cidr) || strToBool(cfg.ip_is_private) === true;
if (legacy_filter && !rule.match_response && cfg.action === 'route') {
- /* Wrap a legacy address-filter rule into the 1.14 evaluate/match_response
- paradigm. Carry the original query-matching fields onto the evaluate
- prefix rule so only queries that would have hit this rule get
- pre-resolved; an unconditional evaluate would resolve every query. */
const eval_tag = '_hp_eval_' + cfg['.name'];
const eval_rule = {
action: 'evaluate',
@@ -730,14 +658,11 @@ if (!isEmpty(main_node)) {
rule.match_response = eval_tag;
}
- /* ip_cidr / ip_is_private are only valid with match_response in 1.14 */
if (rule.match_response) {
rule.ip_cidr = cfg.ip_cidr;
rule.ip_is_private = strToBool(cfg.ip_is_private);
}
- /* sing-box rejects the whole config when a route/evaluate action has no
- server, so drop such a rule with a reason instead. */
if ((rule.action === 'route' || rule.action === 'evaluate') && isEmpty(rule.server)) {
warn(sprintf("homeproxy: DNS rule '%s' has no server configured, skipping it.", cfg['.name']));
return;
@@ -749,19 +674,9 @@ if (!isEmpty(main_node)) {
config.dns.final = get_resolver(dns_default_server);
}
-/* DNS end */
-/* Inbound start */
config.inbounds = [];
-/*
- * dns-in is the local listener the DNS chain hands queries to: dnsmasq
- * forwards to 127.0.0.1# and the nft DNS hijack redirects to
- * dnsmasq, not here. It used to listen on '::', which published a
- * proxy-backed resolver to every LAN client on the DNS port; loopback is the
- * whole reachable surface it needs. mixed-in stays on '::' - that one is the
- * SOCKS/HTTP listener clients are meant to reach.
- */
push(config.inbounds, {
type: 'direct',
tag: 'dns-in',
@@ -817,12 +732,9 @@ if (match(proxy_mode, /tun/))
udp_nat_max: udp_nat_max,
stack: tcpip_stack,
});
-/* Inbound end */
-/* Outbound start */
config.endpoints = [];
-/* Default outbounds */
config.outbounds = [
{
type: 'direct',
@@ -835,7 +747,6 @@ config.outbounds = [
}
];
-/* Main outbounds */
if (!isEmpty(main_node)) {
let urltest_nodes = [];
@@ -958,16 +869,7 @@ if (!isEmpty(main_node)) {
if (isEmpty(config.endpoints))
config.endpoints = null;
-/* Outbound end */
-/* Routing rules start */
-/* Default settings */
-/*
- * The sniff rule below deliberately carries no `timeout`. Reports from
- * sing-box 1.14.2 show it rejected with "route.rules[1].timeout: json: unknown
- * field". The official builds accept it, so the exact trigger is still open,
- * but the default is already 300ms and omitting the field is behaviour-neutral.
- */
config.route = {
rules: [
{
@@ -983,19 +885,12 @@ config.route = {
default_interface: default_interface
};
-/* Routing rules */
if (!isEmpty(main_node)) {
- /* Resolve outbound server domains through the WAN default resolver.
- Do not use china-dns here: china-dns is for resolving mainland China
- destinations, not the proxy node itself. Coupling node bootstrap to
- china_dns_server can break dialing when that resolver is polluted or
- unsuitable for the node domain. */
config.route.default_domain_resolver = {
server: 'default-dns',
strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null
};
- /* Direct list */
if (length(direct_domain_list))
push(config.route.rules, {
rule_set: 'direct-domain',
@@ -1003,14 +898,17 @@ if (!isEmpty(main_node)) {
outbound: 'direct-out'
});
- /* Bypass CN traffic: resolve the destination first, then route by IP.
- sing-box does not match an IP-based rule set (geoip-cn) against a domain
- destination unless it is resolved first, so add an explicit resolve
- action; geoip-cn then sends China IPs to direct and everything else falls
- through to main-out (proxy). This avoids relying on the geosite-* domain
- lists, which can mis-classify foreign domains (e.g. Google's gvt2.com
- beacons) as "cn" and send them direct to time out. Keep the direct-domain
- fast-path above for known direct domains. */
+ if (dedicated_udp_node) {
+ const udp_override = direct_overrides[main_udp_node] || null;
+ push(config.route.rules, {
+ network: 'udp',
+ action: 'route',
+ outbound: 'main-udp-out',
+ override_address: udp_override ? udp_override.override_address : null,
+ override_port: udp_override ? udp_override.override_port : null
+ });
+ }
+
if (routing_mode === 'bypass_mainland_china') {
push(config.route.rules, {
action: 'resolve',
@@ -1023,20 +921,6 @@ if (!isEmpty(main_node)) {
});
}
- /* Main UDP out */
- if (dedicated_udp_node) {
- const udp_override = direct_overrides[main_udp_node] || null;
- push(config.route.rules, {
- network: 'udp',
- action: 'route',
- outbound: 'main-udp-out',
- override_address: udp_override ? udp_override.override_address : null,
- override_port: udp_override ? udp_override.override_port : null
- });
- }
-
- /* Direct-node destination override, emitted as route-options action
- (direct outbound options removed since sing-box 1.13) */
const main_override = direct_overrides[main_node] || null;
if (main_override)
push(config.route.rules, {
@@ -1047,8 +931,6 @@ if (!isEmpty(main_node)) {
config.route.final = 'main-out';
- /* Rule set */
- /* Direct list */
if (length(direct_domain_list))
push(config.route.rule_set, {
type: 'inline',
@@ -1060,7 +942,6 @@ if (!isEmpty(main_node)) {
]
});
- /* Proxy list */
if (length(proxy_domain_list))
push(config.route.rule_set, {
type: 'inline',
@@ -1073,17 +954,6 @@ if (!isEmpty(main_node)) {
});
if (routing_mode === 'bypass_mainland_china') {
- /*
- * The two rule-sets sing-box actually needs for this mode. Each one is
- * used from the local .srs file when update_resources.sh has fetched
- * it, and only falls back to a remote fetch otherwise: a remote
- * rule-set is initialized at every start (sing-box fails to start when
- * the fetch fails even if nothing references it), which would make a
- * cold boot depend on reaching the CDN through the node. The fallback
- * keeps a fresh install working before the first resource update.
- * geosite-noncn is intentionally not declared: no rule references it,
- * and sing-box downloads it anyway.
- */
const preset_rulesets = [
{
tag: 'geoip-cn',
@@ -1147,11 +1017,6 @@ if (!isEmpty(main_node)) {
rule_override_port = rule_direct_override.override_port;
}
- /* sing-box routing rule: emit match fields plus action-specific fields.
- client is the sniffed client type (only set with protocol quic/ssh);
- resolve adds server/strategy/disable_cache/rewrite_ttl/client_subnet;
- reject adds method/no_drop. Emitted conditionally so a given action
- never carries fields sing-box rejects for it. */
const rule = {
ip_version: strToInt(cfg.ip_version),
protocol: cfg.protocol,
@@ -1207,8 +1072,6 @@ if (!isEmpty(main_node)) {
push(config.route.rules, rule);
});
- /* Direct-node destination override, emitted as route-options action
- (direct outbound options removed since sing-box 1.13) */
const final_override = get_direct_override(default_outbound);
if (final_override)
push(config.route.rules, {
@@ -1219,7 +1082,6 @@ if (!isEmpty(main_node)) {
config.route.final = get_outbound(default_outbound);
- /* Rule set */
uci.foreach(uciconfig, uciruleset, (cfg) => {
if (cfg.enabled !== '1')
return null;
@@ -1230,8 +1092,6 @@ if (!isEmpty(main_node)) {
rs_tag = [rs_tag];
for (let t in extra_tags)
push(rs_tag, 'cfg-' + t + '-rule');
- /* sing-box 1.14: multi-tag requires a {tag} placeholder in the fetch source
- (remote: url and initial_path, local: path) */
const fetch_ref = (cfg.type === 'remote') ? (cfg.url || '') : (cfg.path || '');
if (!match(fetch_ref, /\{tag\}/))
warn(sprintf("homeproxy: rule-set '%s' uses extra tags but its %s source lacks a {tag} placeholder.", cfg['.name'], cfg.type));
@@ -1247,8 +1107,6 @@ if (!isEmpty(main_node)) {
url: cfg.url,
update_interval: cfg.update_interval
};
- /* sing-box 1.14 downloads remote rule-sets through a top-level
- http_client; download_detour no longer exists. */
if (cfg.type === 'remote')
ruleset.http_client = http_client_for(get_outbound(cfg.outbound) || get_outbound(default_outbound));
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path))
@@ -1257,28 +1115,12 @@ if (!isEmpty(main_node)) {
});
}
-/*
- * Belt-and-braces for route.default_domain_resolver.
- *
- * sing-box 1.14 removed the legacy outbound DNS rules and refuses to start when
- * a dial-fields user has a domain to resolve without a resolver. The exemption
- * for "only one DNS server configured" never applies here: default-dns and
- * system-dns are emitted unconditionally, so every config has at least two.
- * That makes this a global requirement, not a per-mode one, so it must not
- * depend on which branch above ran. The branches already pick the right
- * resolver (the WAN resolver, or default_outbound_dns in custom mode); this
- * only covers the degenerate "not custom mode and no main node" case.
- */
if (isEmpty(config.route.default_domain_resolver))
config.route.default_domain_resolver = { server: 'default-dns' };
-/* Remote rule-sets download through the http_clients collected while the
- rule-sets were built (see http_client_for). */
if (length(http_clients))
config.http_clients = http_clients;
-/* Routing rules end */
-/* Experimental start */
if (routing_mode in ['bypass_mainland_china', 'custom']) {
config.experimental = {
cache_file: {
@@ -1288,7 +1130,6 @@ if (routing_mode in ['bypass_mainland_china', 'custom']) {
}
};
}
-/* Experimental end */
config['$schema'] = 'https://sing-box.sagernet.org/schema.json';
@@ -1300,7 +1141,4 @@ if (system('/usr/bin/sing-box check --config ' + client_tmp) !== 0) {
exit(1);
}
system('mv -f ' + client_tmp + ' ' + RUN_DIR + '/sing-box-c.json');
-/* The generated config carries every node credential (passwords, UUIDs,
- private keys) and writefile() has no mode argument, so it lands with the
- process umask; chmod it to the sing-box user only. */
system('chmod 600 ' + RUN_DIR + '/sing-box-c.json');
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_server.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_server.uc
index 23cfdb027..4530dc6c8 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_server.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_server.uc
@@ -15,7 +15,6 @@ import {
removeBlankAttrs, buildTLSObject, buildTransportObject, HP_DIR, RUN_DIR
} from 'homeproxy';
-/* UCI config start */
const uci = cursor();
const uciconfig = 'homeproxy';
@@ -24,11 +23,9 @@ uci.load(uciconfig);
const uciserver = 'server';
const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn';
-/* UCI config end */
const config = {};
-/* Log */
config.log = {
disabled: false,
level: log_level,
@@ -42,7 +39,6 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
if (cfg.enabled !== '1')
return;
- /* Snell (1.14): single-psk inbound; shape differs from the generic users[] block below */
if (cfg.type === 'snell') {
push(config.inbounds, {
type: 'snell',
@@ -76,10 +72,8 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
udp_timeout: strToTime(cfg.udp_timeout),
network: cfg.network,
- /* AnyTLS */
padding_scheme: cfg.anytls_padding_scheme,
- /* Hysteria (2) */
up_mbps: strToInt(cfg.hysteria_up_mbps),
down_mbps: strToInt(cfg.hysteria_down_mbps),
obfs: cfg.hysteria_obfs_type ? {
@@ -91,31 +85,25 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
masquerade: cfg.hysteria_masquerade,
- /* Shadowsocks */
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
- /* Tuic */
congestion_control: cfg.tuic_congestion_control,
auth_timeout: strToTime(cfg.tuic_auth_timeout),
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
heartbeat: strToTime(cfg.tuic_heartbeat),
- /* AnyTLS / HTTP / Hysteria (2) / Mixed / Socks / Trojan / Tuic / VLESS / VMess */
users: (cfg.type !== 'shadowsocks') ? [
{
name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
username: cfg.username,
password: cfg.password,
- /* Hysteria */
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
- /* Tuic */
uuid: cfg.uuid,
- /* VLESS / VMess */
flow: cfg.vless_flow,
alterId: strToInt(cfg.vmess_alterid)
}
@@ -150,5 +138,4 @@ if (system('/usr/bin/sing-box check --config ' + server_tmp) !== 0) {
exit(1);
}
system('mv -f ' + server_tmp + ' ' + RUN_DIR + '/sing-box-s.json');
-/* keep the config (key paths, user credentials) out of world-readable mode */
system('chmod 600 ' + RUN_DIR + '/sing-box-s.json');
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/homeproxy.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
index 80d24fdca..027edfb10 100644
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
@@ -7,13 +7,9 @@
import { mkstemp } from 'fs';
import { urldecode_params } from 'luci.http';
-/* Global variables start */
export const HP_DIR = '/etc/homeproxy';
export const RUN_DIR = '/var/run/homeproxy';
-/* Global variables end */
-/* Utilities start */
-/* Kanged from luci-app-commands */
export function shellQuote(s) {
return `'${replace(s, "'", "'\\''")}'`;
};
@@ -26,26 +22,15 @@ export function isBinary(str) {
return false;
};
-/* Close a file descriptor, swallowing secondary errors (e.g. an already
- closed fd). ucode has no `finally` clause, so both the normal and the
- exceptional path of executeCommand() have to close explicitly. */
function closeFD(fd) {
if (fd) {
try {
fd.close();
} catch (e) {
- /* already closed */
}
}
};
-/*
- * Read a descriptor to EOF. A single read(size) only returns up to `size`
- * bytes, so the previous one-shot read(512 KiB) silently truncated larger
- * responses - a subscription list above that size was cut mid-base64/mid-JSON
- * and only showed up as a parse failure. MAX_OUTPUT bounds what an oversized
- * or endless response can allocate.
- */
const MAX_OUTPUT = 16 * 1024 * 1024;
function readAll(fd) {
@@ -79,7 +64,6 @@ export function executeCommand(...args) {
const binary = isBinary(stdout);
- /* mkstemp() returns delete-on-close files, so closing is enough */
closeFD(outfd);
closeFD(errfd);
@@ -91,8 +75,6 @@ export function executeCommand(...args) {
binary
};
} catch (e) {
- /* Never leak the temporary descriptors on a failing run. ucode has
- no `finally` clause, so the exception is re-raised by hand. */
closeFD(outfd);
closeFD(errfd);
@@ -114,19 +96,6 @@ export function getTime(epoch) {
};
-/*
- * GNU wget and OpenWrt's default /usr/bin/wget do not accept the same flags.
- * On OpenWrt/ImmortalWrt that path comes from the uclient-fetch package
- * (`ALTERNATIVES:=200:/usr/bin/wget:/bin/uclient-fetch`, `PROVIDES:=wget` in
- * package/libs/uclient) and busybox ships no wget applet, so the effective
- * wget is uclient-fetch unless wget-ssl happens to be installed on top of it.
- * GNU-only flags break the fetch outright: uclient-fetch's getopt rejects
- * `-nv` with "unrecognized option: n" before a single request is sent.
- *
- * The probe below tells the two apart. `-nv` (--no-verbose) is how GNU wget
- * mutes the progress meter while still printing the failure reason on stderr;
- * `-q` is the only meter switch uclient-fetch and busybox wget share.
- */
export function classifyWgetFlavor(exitcode, stdout) {
if (exitcode !== 0 || !stdout)
return 'compat';
@@ -146,13 +115,6 @@ export function wgetFlavor() {
return wget_flavor;
};
-/*
- * Fetch a URL and report both the body and, on failure, the reason. The
- * reason is wget's own stderr (whitespace collapsed, length-capped) so the
- * caller can tell a DNS failure from a timeout or a TLS handshake error.
- * Note that uclient-fetch's -q hides most failure messages, so on such a
- * target the reason is often empty and only the exit code is left.
- */
export function wGETVerbose(url, ua) {
if (!url || type(url) !== 'string')
return { content: null, error: 'invalid URL' };
@@ -160,9 +122,6 @@ export function wGETVerbose(url, ua) {
if (!ua)
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
- /* -U/-T are understood by GNU wget, uclient-fetch and busybox wget alike;
- * the long forms --user-agent/--timeout are not, busybox wget in
- * particular knows neither of them. */
const output = executeCommand(`/usr/bin/wget ${wgetFlavor() === 'gnu' ? '-nv' : '-q'} -O- -U ${shellQuote(ua)} -T 10 ${shellQuote(url)}`) || {};
if (output.exitcode !== 0) {
let reason = trim(output.stderr || '');
@@ -180,9 +139,7 @@ export function wGETVerbose(url, ua) {
export function wGET(url, ua) {
return wGETVerbose(url, ua).content;
};
-/* Utilities end */
-/* String helper start */
export function isEmpty(res) {
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
};
@@ -199,16 +156,9 @@ export function strToTime(str) {
if (isEmpty(str))
return null;
- /* Preserve values that already carry a time unit (e.g. "30s", "1m") */
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
};
-/*
- * Milliseconds variant of strToTime(). Used by options whose LuCI field is
- * documented and validated in milliseconds while sing-box expects a duration
- * (e.g. route rule tls_fragment_fallback_delay, whose default is 500ms);
- * strToTime() used to turn "500" into "500s".
- */
export function strToMs(str) {
if (isEmpty(str))
return null;
@@ -255,20 +205,14 @@ export function validation(datatype, data) {
return (ret === 0);
};
-/* Validate IP/CIDR format to prevent nftables template injection from resource files */
export function isValidCIDR(addr, family) {
if (isEmpty(addr))
return false;
- /* Strip leading/trailing whitespace */
addr = trim(addr);
if (!addr)
return false;
- /* Split address and optional prefix. The prefix has to be validated as
- strictly as the address: `int()` accepts a leading number ("1.2.3.4/0 }
- ..." parses as 0), which would let a poisoned resource file inject
- statements into the nftables template this feeds. */
const parts = split(addr, '/');
const ip = parts[0];
const prefix = parts[1];
@@ -278,31 +222,24 @@ export function isValidCIDR(addr, family) {
if (length(parts) === 2 && !match(prefix, /^\d{1,3}$/))
return false;
- /* Validate IP part */
if (family === 4) {
if (!match(ip, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
return false;
- /* Validate octet ranges */
const octets = split(ip, '.');
for (let o in octets)
if (int(o) > 255)
return false;
- /* Validate prefix if present */
if (prefix && int(prefix) > 32)
return false;
} else if (family === 6) {
- /* Only hex digits and colons */
if (!match(ip, /^[0-9a-fA-F:]+$/))
return false;
- /* At most one "::" */
if (match(ip, /::.*::/))
return false;
- /* Validate group structure */
const dcolon = index(ip, '::');
if (dcolon === -1) {
- /* Uncompressed form: exactly 8 groups of 1-4 hex digits */
const groups = split(ip, ':');
if (length(groups) !== 8)
return false;
@@ -310,7 +247,6 @@ export function isValidCIDR(addr, family) {
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
return false;
} else {
- /* Compressed form: "::" expands to at least one zero group */
let count = 0;
for (let part in [substr(ip, 0, dcolon), substr(ip, dcolon + 2)]) {
if (part === '')
@@ -333,9 +269,7 @@ export function isValidCIDR(addr, family) {
return true;
};
-/* String helper end */
-/* String parser start */
export function decodeBase64Str(str) {
if (isEmpty(str))
return null;
@@ -351,14 +285,6 @@ export function decodeBase64Str(str) {
return b64dec(str);
};
-/*
- * Decode %XX escapes without touching '+'. urldecode() maps '+' to a space,
- * which corrupts base64 userinfo ("+" is part of the alphabet), so share-link
- * userinfo is percent-decoded with this instead.
- */
-/*
- * ucode has no tolower(), so both hex cases are mapped by hand.
- */
function hexValue(ch) {
const lower = index('0123456789abcdef', ch);
if (lower >= 0)
@@ -369,10 +295,6 @@ function hexValue(ch) {
return upper < 0 ? -1 : 10 + upper;
}
-/*
- * ucode's regex engine rejects \x00 inside a character class ("Missing ']'"),
- * so control bytes are checked by hand.
- */
function hasControlChar(str) {
for (let i = 0; i < length(str); i++) {
const code = ord(str, i);
@@ -449,9 +371,6 @@ export function parseURL(url) {
return null;
if (objurl.userinfo) {
- /* Percent-decode first: providers emit base64 userinfo with the
- padding escaped ("=" as "%3D"), which used to decode to nothing
- and silently dropped method/password. */
let userinfo = percentDecode(objurl.userinfo);
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
@@ -459,14 +378,6 @@ export function parseURL(url) {
return '';
});
- /*
- * Keep anything without whitespace or control characters: real-world
- * userinfo carries base64 (with '=' or '+'), percent-encoded tokens
- * and passwords with punctuation. The previous class was written as
- * [A-Za-z0-9\+\-\_\.] but ucode's regex reads \+-\_ as the range
- * 0x2B-0x5F, so it accepted '=@:;[]\^<>,/' and rejected '!' - it
- * neither did what it documented nor covered real passwords.
- */
if (!match(userinfo, /\s/) && !hasControlChar(userinfo))
objurl.username = userinfo;
else
@@ -483,18 +394,7 @@ export function parseURL(url) {
return objurl;
};
-/* String parser end */
-/* Config generator helper start */
-/*
- * Shared sing-box TLS object builder for the client outbound and the server
- * inbound. The property order below is significant: the generated JSON keeps
- * the insertion order of this object after removeBlankAttrs() drops the blank
- * attributes, and both generators used to emit the fields in exactly this
- * order. Keep client-only fields (insecure/handshake_timeout/utls) and
- * server-only fields (key_path/certificate_provider) at their current
- * positions when editing.
- */
export function buildTLSObject(cfg, is_server) {
if (cfg.tls !== '1')
return null;
@@ -540,7 +440,6 @@ export function buildTLSObject(cfg, is_server) {
ech: is_server ? (cfg.tls_ech_key ? {
enabled: true,
key: split(cfg.tls_ech_key, '\n')
- /* config: split(cfg.tls_ech_config, '\n') */
} : null) : ((cfg.tls_ech === '1') ? {
enabled: true,
config: cfg.tls_ech_config,
@@ -567,8 +466,6 @@ export function buildTLSObject(cfg, is_server) {
};
};
-/* Shared sing-box transport object builder; the client transport additionally
- supports the gRPC keepalive hint, the server one does not. */
export function buildTransportObject(cfg, is_server) {
if (isEmpty(cfg.transport))
return null;
@@ -589,15 +486,7 @@ export function buildTransportObject(cfg, is_server) {
permit_without_stream: is_server ? null : strToBool(cfg.grpc_permit_without_stream)
};
};
-/* Config generator helper end */
-/* PEM validation start */
-/*
- * Check that `content` is a PEM certificate (is_private_key = false) or an
- * RSA/EC private key (is_private_key = true): matching BEGIN/END boundaries
- * with a base64 body in between. Kanged from luci-proto-openconnect; used by
- * the rpcd certificate upload and available for future certificate features.
- */
export function isValidPEM(content, is_private_key) {
if (isEmpty(content))
return false;
@@ -619,4 +508,3 @@ export function isValidPEM(content, is_private_key) {
return true;
};
-/* PEM validation end */
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/migrate_config.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/migrate_config.uc
index a0b9d8570..c65dfe1d8 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/migrate_config.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/migrate_config.uc
@@ -27,11 +27,9 @@ const uciinfra = 'infra',
uciroutingrule = 'routing_rule',
uciserver = 'server';
-/* chinadns-ng has been removed */
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
uci.delete(uciconfig, uciinfra, 'china_dns_port');
-/* chinadns server now only accepts single server */
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
if (type(china_dns_server) === 'array') {
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
@@ -42,51 +40,41 @@ if (type(china_dns_server) === 'array') {
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
}
-/* github_token option has been moved to config section */
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
if (github_token) {
uci.set(uciconfig, ucimain, 'github_token', github_token);
uci.delete(uciconfig, uciinfra, 'github_token')
}
-/* ntp_server was introduced */
if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
uci.set(uciconfig, uciinfra, 'ntp_server', 'nil');
-/* tun_gso was deprecated in sb 1.11 */
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
uci.delete(uciconfig, uciinfra, 'tun_gso');
-/* create migration section */
if (!uci.get(uciconfig, ucimigration))
uci.set(uciconfig, ucimigration, uciconfig);
-/* delete old crontab command */
const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab');
if (!migration_crontab) {
system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>/dev/null');
uci.set(uciconfig, ucimigration, 'crontab', '1');
}
-/* log_level was introduced */
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
uci.set(uciconfig, ucimain, 'log_level', 'warn');
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
uci.set(uciconfig, uciserver, 'log_level', 'warn');
-/* empty value defaults to all ports now */
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
uci.delete(uciconfig, ucimain, 'routing_port');
-/* experimental section was removed */
if (uci.get(uciconfig, 'experimental'))
uci.delete(uciconfig, 'experimental');
-/* block-dns was removed from built-in dns servers */
const default_dns_server = uci.get(uciconfig, ucidns, 'default_server');
if (default_dns_server === 'block-dns') {
- /* append a rule at last to block all DNS queries */
uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule);
uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns');
uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1');
@@ -98,12 +86,9 @@ else if (default_dns_server === 'local-dns')
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
const dns_server_migration = {};
-/* DNS servers options */
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
- /* legacy format was deprecated in sb 1.12 */
if (cfg.address) {
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
- /* RCode was moved into DNS rules */
if (addr.protocol === 'rcode') {
dns_server_migration[cfg['.name']] = { action: 'predefined' };
switch (addr.hostname) {
@@ -156,9 +141,7 @@ uci.foreach(uciconfig, ucidnsserver, (cfg) => {
}
});
-/* DNS rules options */
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
- /* outbound was removed in sb 1.12 */
if (cfg.outbound) {
uci.delete(uciconfig, cfg['.name']);
if (!cfg.enabled)
@@ -181,20 +164,16 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
return;
}
- /* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
- /* block-dns was moved into action in sb 1.11 */
if (cfg.server === 'block-dns') {
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
uci.delete(uciconfig, cfg['.name'], 'server');
} else if (!cfg.action) {
- /* add missing 'action' field */
uci.set(uciconfig, cfg['.name'], 'action', 'route');
}
- /* strategy and client_subnet were moved into dns rules */
if (dns_server_migration[cfg.server]) {
if (dns_server_migration[cfg.server].strategy)
uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy);
@@ -210,26 +189,20 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
}
});
-/* nodes options */
uci.foreach(uciconfig, ucinode, (cfg) => {
- /* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
- /* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */
if (!isEmpty(cfg.tls_ech_enable_pqss))
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
- /* wireguard_gso was deprecated in sb 1.11 */
if (!isEmpty(cfg.wireguard_gso))
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
- /* hysteria_revc_window was a typo, renamed to hysteria_recv_window */
if (!isEmpty(cfg.hysteria_revc_window))
uci.rename(uciconfig, cfg['.name'], 'hysteria_revc_window', 'hysteria_recv_window');
});
-/* sing-box 1.14: remove deprecated DNS semantics */
if (!isEmpty(uci.get(uciconfig, ucidns, 'independent_cache')))
uci.delete(uciconfig, ucidns, 'independent_cache');
@@ -248,38 +221,29 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
uci.delete(uciconfig, cfg['.name'], 'rule_set_ip_cidr_accept_empty');
});
-/* routing rules options */
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
- /* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
- /* block-out was moved into action in sb 1.11 */
if (cfg.outbound === 'block-out') {
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
uci.delete(uciconfig, cfg['.name'], 'outbound');
} else if (!cfg.action) {
- /* add missing 'action' field */
uci.set(uciconfig, cfg['.name'], 'action', 'route');
}
});
-/* server options */
-/* auto_firewall was moved into server options */
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
if (!isEmpty(auto_firewall))
uci.delete(uciconfig, uciserver, 'auto_firewall');
uci.foreach(uciconfig, uciserver, (cfg) => {
- /* auto_firewall was moved into server options */
if (auto_firewall === '1')
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
- /* sniff_override was deprecated in sb 1.11 */
if (!isEmpty(cfg.sniff_override))
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
- /* domain_strategy is now pointless without sniff override */
if (!isEmpty(cfg.domain_strategy))
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
});
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/parse_uri.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/parse_uri.uc
index 63801deb1..dcbdb338a 100644
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/parse_uri.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/parse_uri.uc
@@ -10,28 +10,13 @@ import { urldecode, urlencode } from 'luci.http';
import { decodeBase64Str, parseURL, percentDecode, isEmpty, validation } from 'homeproxy';
-/*
- * Share-link parsers. Every protocol has its own parse__uri() so the
- * branches can be read, changed and unit-tested in isolation; parse_uri() only
- * dispatches on the scheme and applies the common address/port validation and
- * label fallback afterwards.
- *
- * `features` is the sing-box feature map (with_quic/with_utls/...), `log` is a
- * logging callback, both injected so the parsers stay free of ubus/uci.
- */
-/*
- * Transports sing-box accepts on a V2Ray-style outbound. Anything else
- * (xhttp, splithttp, ...) makes sing-box reject the whole generated config,
- * so such a node is dropped while importing instead of poisoning every node.
- */
const SUPPORTED_TRANSPORTS = ['ws', 'http', 'grpc', 'httpupgrade', 'quic'];
function unsupported_transport(config) {
return !isEmpty(config.transport) && !(config.transport in SUPPORTED_TRANSPORTS);
}
-/* https://shadowsocks.org/guide/sip008.html */
export function parse_sip008_uri(uri) {
return {
label: uri.remarks,
@@ -45,7 +30,6 @@ export function parse_sip008_uri(uri) {
};
};
-/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
export function parse_anytls_uri(uri) {
uri = split(trim(uri), '://');
@@ -80,7 +64,6 @@ export function parse_http_uri(uri) {
};
};
-/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
export function parse_hysteria_uri(uri, features, log) {
uri = split(trim(uri), '://');
@@ -113,7 +96,6 @@ export function parse_hysteria_uri(uri, features, log) {
};
};
-/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
export function parse_hysteria2_uri(uri, features, log) {
uri = split(trim(uri), '://');
@@ -142,7 +124,6 @@ export function parse_hysteria2_uri(uri, features, log) {
};
};
-/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
export function parse_snell_uri(uri) {
uri = split(trim(uri), '://');
@@ -183,7 +164,6 @@ export function parse_socks_uri(uri) {
export function parse_ss_uri(uri) {
uri = split(trim(uri), '://');
- /* "Lovely" Shadowrocket format */
const ss_suri = split(uri[1], '#');
let ss_slabel = '';
if (length(ss_suri) <= 2) {
@@ -194,18 +174,13 @@ export function parse_ss_uri(uri) {
uri[1] = ss_decoded + ss_slabel;
}
- /* Legacy format is not supported, it should be never appeared in modern subscriptions */
- /* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
- /* SIP002 format https://shadowsocks.org/guide/sip002.html */
const url = parseURL('http://' + uri[1]) || {};
let ss_userinfo = [];
if (url.username && url.password)
- /* User info encoded with URIComponent; parseURL() already decoded it */
ss_userinfo = [url.username, url.password];
else if (url.username)
- /* User info encoded with base64 (padding may have arrived as %3D) */
ss_userinfo = split(decodeBase64Str(url.username) ?? '', ':', 2);
let ss_plugin, ss_plugin_opts;
@@ -213,7 +188,6 @@ export function parse_ss_uri(uri) {
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
ss_plugin = ss_plugin_info[0];
if (ss_plugin === 'simple-obfs')
- /* Fix non-standard plugin name */
ss_plugin = 'obfs-local';
ss_plugin_opts = ss_plugin_info[1];
}
@@ -230,7 +204,6 @@ export function parse_ss_uri(uri) {
};
};
-/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
export function parse_trojan_uri(uri) {
uri = split(trim(uri), '://');
@@ -270,7 +243,6 @@ export function parse_trojan_uri(uri) {
return config;
};
-/* https://github.com/daeuniverse/dae/discussions/182 */
export function parse_tuic_uri(uri, features, log) {
uri = split(trim(uri), '://');
@@ -299,14 +271,12 @@ export function parse_tuic_uri(uri, features, log) {
};
};
-/* https://github.com/XTLS/Xray-core/discussions/716 */
export function parse_vless_uri(uri, features, log) {
uri = split(trim(uri), '://');
const url = parseURL('http://' + uri[1]) || {};
const params = url.searchParams || {};
- /* Unsupported protocol */
if (params.type === 'kcp') {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
return null;
@@ -332,10 +302,6 @@ export function parse_vless_uri(uri, features, log) {
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
tls_reality_short_id: params.sid,
tls_utls: features.with_utls ? params.fp : null,
- /* sing-box rejects every other value ("unsupported flow") and a single
- rejected outbound makes the whole generated config fail, so only the
- supported value is imported (the common
- "xtls-rprx-vision-udp443" becomes "no flow" instead of breaking). */
vless_flow: (params.security in ['tls', 'reality'] && params.flow === 'xtls-rprx-vision') ? params.flow : null
};
switch (params.type) {
@@ -372,11 +338,9 @@ export function parse_vless_uri(uri, features, log) {
return config;
};
-/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
export function parse_vmess_uri(uri, features, log) {
uri = split(trim(uri), '://');
- /* "Lovely" shadowrocket format */
if (match(uri, /&/)) {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
@@ -393,7 +357,6 @@ export function parse_vmess_uri(uri, features, log) {
if (payload.v != '2') {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
- /* Unsupported protocol */
} else if (payload.net === 'kcp') {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
return null;
@@ -404,13 +367,6 @@ export function parse_vmess_uri(uri, features, log) {
return null;
}
- /*
- * https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
- * else if (payload.aid && int(payload.aid) !== 0) {
- * log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
- * return null;
- * }
- */
const config = {
label: payload.ps ? urldecode(payload.ps) : null,
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_crond.sh b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_crond.sh
index 4f38f07cc..f735557a5 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_crond.sh
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_crond.sh
@@ -5,21 +5,12 @@
SCRIPTS_DIR="/etc/homeproxy/scripts"
-# update_resources.sh exit codes: 0 updated, 1 failed, 2 another run holds the
-# lock, 3 already up to date.
changed=0
for i in "china_ip4" "china_ip6" "gfw_list" "china_list" "geoip_cn" "geosite_cn"; do
"$SCRIPTS_DIR"/update_resources.sh "$i"
[ "$?" -eq 0 ] && changed=1
done
-# The nft sets (china_ip4/ip6.txt) and the dnsmasq snippets (gfw_list.txt,
-# china_list.txt) are rendered from those files, and the client rule-sets are
-# loaded from the two .srs files, so a downloaded list only takes effect after
-# they have been regenerated. Without this the daily cron update rewrote the
-# files on disk and nothing else.
[ "$changed" -eq 0 ] || /etc/init.d/homeproxy refresh_lists
-# update_subscriptions.uc restarts the service itself when it changed the UCI
-# configuration.
"$SCRIPTS_DIR"/update_subscriptions.uc
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_resources.sh b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_resources.sh
index 6c2bd99e9..9f17825e0 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_resources.sh
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_resources.sh
@@ -35,12 +35,6 @@ check_list_update() {
return 2
fi
- # --header= is the only header switch understood by all three wget
- # implementations this runs on (uclient-fetch, GNU wget, busybox wget);
- # the previous --header-file= is rejected by uclient-fetch and GNU wget
- # alike, which made every version probe fail whenever a GitHub token was
- # configured. The token is therefore visible in `ps` for the duration of
- # the request; that is the trade-off for a single code path.
local api_url="https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1"
local list_info
if [ -n "$github_token" ]; then
diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
index d85f4874c..b623cfd43 100755
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
@@ -20,7 +20,6 @@ import {
import { parse_uri } from 'parse_uri';
-/* UCI config start */
const uci = cursor();
const uciconfig = 'homeproxy';
@@ -44,9 +43,7 @@ if (routing_mode !== 'custom') {
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
}
-/* UCI config end */
-/* String helper start */
function filter_check(name) {
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
return false;
@@ -68,9 +65,7 @@ function filter_check(name) {
return ret;
}
-/* String helper end */
-/* Common var start */
const node_cache = {},
node_result = [];
@@ -78,9 +73,7 @@ const ubus = connect();
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
if (isEmpty(sing_features))
log('Warning: Failed to query sing-box features via ubus, assuming defaults.');
-/* Common var end */
-/* Log */
system(`mkdir -p ${RUN_DIR}`);
function log(...args) {
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
@@ -88,20 +81,10 @@ function log(...args) {
logfile.close();
}
-/*
- * Subscription URLs usually carry the provider token in the query string;
- * the log is surfaced in LuCI, so keep it out.
- */
function redact(url) {
return replace(url, /[?#].*$/, '');
}
-/*
- * mkdir is the lock: atomic everywhere this runs and no flock binding needed.
- * A lock older than LOCK_STALE is broken, so a killed run cannot block every
- * later update. Two concurrent runs (cron + the LuCI button) used to
- * interleave commits and service restarts.
- */
const LOCK_DIR = RUN_DIR + '/update_subscriptions.lock';
const LOCK_STALE = 600;
@@ -126,11 +109,6 @@ function release_lock() {
system(sprintf('rmdir %s 2>/dev/null', shellQuote(LOCK_DIR)));
}
-/*
- * sing-box rejects the whole generated config over one malformed node, and the
- * generator keeps the old config when it fails, so a node that cannot possibly
- * work must not reach UCI. Returns the reason, or null when the node is usable.
- */
function node_problem(cfg) {
if (isEmpty(cfg.address) || isEmpty(cfg.port))
return 'missing address or port';
@@ -199,12 +177,9 @@ function main() {
try {
nodes = json(res).servers || json(res);
- /* Shadowsocks SIP008 format */
if (nodes[0].server && nodes[0].method)
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
} catch(e) {
- /* Clash/mihomo subscriptions are YAML, not base64; say so instead
- of reporting a base64/JSON failure followed by "no valid node". */
if (match(res, /(^|\n)[ \t]*(proxies|proxy-groups)[ \t]*:/)) {
log(sprintf('Unsupported subscription format (Clash/mihomo YAML) at %s.', shown_url));
continue;
@@ -222,9 +197,6 @@ function main() {
const config = parse_uri(node, sing_features, log);
if (isEmpty(config)) {
- /* No credentials in the log: only the scheme says whether a
- parser is missing or the entry is simply not a node. Only
- the first few are printed, the rest is summarized below. */
if (skipped < 5)
log(sprintf('Skipping unparsable entry (%s) from %s.',
type(node) === 'string' ? replace(node, /:.*$/, '') : type(node), shown_url));
@@ -288,11 +260,9 @@ function main() {
let added = 0, removed = 0, changed = false;
uci.foreach(uciconfig, ucinode, (cfg) => {
- /* Nodes created by the user */
if (!cfg.grouphash)
return null;
- /* Empty object - failed to fetch nodes, or subscription URL not yet processed */
if (!node_cache[cfg.grouphash] || length(node_cache[cfg.grouphash]) === 0)
return null;
@@ -304,14 +274,10 @@ function main() {
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
} else {
map(keys(cfg), (v) => {
- /* skip the cursor's own metadata (.name/.type/...): deleting
- those is a no-op and would mark every run as changed */
if (substr(v, 0, 1) === '.')
return null;
if (v in node_cache[cfg.grouphash][cfg['.name']]) {
- /* only write what actually differs, so an unchanged
- subscription does not fake a change below */
const fresh = node_cache[cfg.grouphash][cfg['.name']][v];
const current = uci.get(uciconfig, cfg['.name'], v);
if (type(fresh) === 'array' ? (sprintf('%J', uci.get_all(uciconfig, cfg['.name'], v) || []) !== sprintf('%J', fresh))
@@ -342,9 +308,6 @@ function main() {
});
uci.commit(uciconfig);
- /* via_proxy keeps the service up during the fetch, but a configuration
- change still has to be applied - it used to be committed and then
- ignored until the next restart. */
let need_restart = (via_proxy !== '1') || changed;
if (!isEmpty(main_node)) {
const first_server = uci.get_first(uciconfig, ucinode);
@@ -410,7 +373,6 @@ function main() {
}
}
- /* Scrub stale urltest member references in custom routing nodes */
uci.foreach(uciconfig, 'routing_node', (cfg) => {
if (cfg.node !== 'urltest' || isEmpty(cfg.urltest_nodes))
return null;
diff --git a/luci-app-homeproxy/tests/luci-form-snapshot.js b/luci-app-homeproxy/tests/luci-form-snapshot.js
index 6978586aa..87c5135a3 100644
--- a/luci-app-homeproxy/tests/luci-form-snapshot.js
+++ b/luci-app-homeproxy/tests/luci-form-snapshot.js
@@ -3,14 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
- *
- * Dump the LuCI form definition of the homeproxy node/client/server views as
- * JSON, using a minimal mock of the LuCI client runtime. It is meant to be run
- * against two checkouts (e.g. HEAD and the working tree) and the resulting
- * snapshots diffed, so that a pure refactor can be proven not to change any
- * option name, dependency, default or label.
- *
- * Usage: node tests/luci-form-snapshot.js [node|client|server]
*/
'use strict';
@@ -18,7 +10,6 @@
const fs = require('fs');
const path = require('path');
-/* --- LuCI runtime mock start ------------------------------------------- */
String.prototype.format = function (...args) {
const values = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
@@ -171,8 +162,6 @@ const rpc = { declare: () => () => Promise.resolve({}) };
const fsMock = { exec_direct: () => Promise.resolve(''), read: () => Promise.resolve(''), write: () => Promise.resolve() };
const widgets = { DeviceSelect: class {} };
-/* tools.firewall is used by the client view for the LAN ACL lists; the mock
- has to return an option object like the real addIPOption/addMACOption. */
const fwtool = {
addIPOption: (section, _policy, name, title, _desc, _family, _hosts, _optional) => section.option(form.DynamicList, name, title),
addMACOption: (section, _policy, name, title, _desc, _hosts) => section.option(form.DynamicList, name, title)
@@ -186,7 +175,6 @@ const deps = {
'tools.firewall': fwtool
};
-/* --- LuCI runtime mock end --------------------------------------------- */
function loadLuciModule(file, extraDeps) {
let src = fs.readFileSync(file, 'utf8');
@@ -194,7 +182,6 @@ function loadLuciModule(file, extraDeps) {
(_m, mod, alias) => `const ${alias} = __deps[${JSON.stringify(mod)}];`);
src = src.replace(/^'require ([^']+)';$/gm,
(_m, mod) => `const ${mod.split('.').pop().replace(/[^\w]/g, '_')} = __deps[${JSON.stringify(mod)}];`);
- /* eslint-disable-next-line no-new-func */
const factory = new Function('__deps', '_', 'E', 'L', src);
return factory(Object.assign({}, deps, extraDeps), _, E, L);
}
diff --git a/luci-app-homeproxy/tests/run.sh b/luci-app-homeproxy/tests/run.sh
index a4d7cef87..b5066529f 100755
--- a/luci-app-homeproxy/tests/run.sh
+++ b/luci-app-homeproxy/tests/run.sh
@@ -1,16 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
-#
-# One-shot entry point for the test suite:
-#
-# tests/run.sh
-#
-# Translation coverage and the LuCI form snapshots run locally (they only need
-# python3 / node). The ucode tests run locally when ucode and sing-box are
-# available, otherwise the checkout is copied to $HP_TEST_HOST and run there.
-#
-# HP_TEST_HOST=root@192.168.1.1 tests/run.sh
-# HP_TEST_DIR=/tmp/hp-tests tests/run.sh
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOST="${HP_TEST_HOST:-root@192.168.1.1}"
diff --git a/luci-app-homeproxy/tests/ucode/mocks/homeproxy.uc b/luci-app-homeproxy/tests/ucode/mocks/homeproxy.uc
index 3a76b6d1d..9bbe27b4e 100644
--- a/luci-app-homeproxy/tests/ucode/mocks/homeproxy.uc
+++ b/luci-app-homeproxy/tests/ucode/mocks/homeproxy.uc
@@ -2,14 +2,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
- *
- * Test double for root/etc/homeproxy/scripts/homeproxy.uc.
- *
- * Only `validation()` is stubbed: the real one shells out to
- * /sbin/validate_data, which does not exist outside OpenWrt. Everything else
- * (isEmpty, decodeBase64Str, parseURL) is a verbatim copy so the parser tests
- * exercise the same string handling as production. Keep these in sync when
- * homeproxy.uc changes.
*/
import { urldecode_params } from 'luci.http';
@@ -33,7 +25,6 @@ export function decodeBase64Str(str) {
return b64dec(str);
};
-/* Pure-ucode stand-in for /sbin/validate_data (host/port/ip4/ip6/hostname). */
export function validation(datatype, data) {
if (!datatype || !data)
return null;
@@ -62,14 +53,6 @@ export function validation(datatype, data) {
}
};
-/*
- * Decode %XX escapes without touching '+'. urldecode() maps '+' to a space,
- * which corrupts base64 userinfo ("+" is part of the alphabet), so share-link
- * userinfo is percent-decoded with this instead.
- */
-/*
- * ucode has no tolower(), so both hex cases are mapped by hand.
- */
function hexValue(ch) {
const lower = index('0123456789abcdef', ch);
if (lower >= 0)
@@ -80,10 +63,6 @@ function hexValue(ch) {
return upper < 0 ? -1 : 10 + upper;
}
-/*
- * ucode's regex engine rejects \x00 inside a character class ("Missing ']'"),
- * so control bytes are checked by hand.
- */
function hasControlChar(str) {
for (let i = 0; i < length(str); i++) {
const code = ord(str, i);
@@ -160,7 +139,6 @@ export function parseURL(url) {
return null;
if (objurl.userinfo) {
- /* keep in sync with the production parser (homeproxy.uc) */
let userinfo = percentDecode(objurl.userinfo);
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
diff --git a/luci-app-homeproxy/tests/ucode/run.sh b/luci-app-homeproxy/tests/ucode/run.sh
index 3664f6459..fea88654e 100755
--- a/luci-app-homeproxy/tests/ucode/run.sh
+++ b/luci-app-homeproxy/tests/ucode/run.sh
@@ -1,11 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
-#
-# Run the ucode-level tests: the parse_uri unit tests, the fw4 inventory check
-# and the generator regression fixtures. Requires ucode; sing-box is needed for
-# the generator cases (they validate the emitted config with `sing-box check`).
-#
-# Usage: sh tests/ucode/run.sh [work-dir]
ROOT="${1:-.}"
WORK="${2:-/tmp/hp-ucode-tests}"
@@ -16,8 +10,6 @@ FAILED=0
echo "== ucode syntax check =="
for file in "$ROOT"/root/etc/homeproxy/scripts/*.uc "$ROOT"/root/usr/share/rpcd/ucode/*; do
[ -f "$file" ] || continue
- # Modules (with export statements) cannot be compiled as a program; they
- # are loaded through `import` below instead.
case "$file" in
*homeproxy.uc|*parse_uri.uc) continue ;;
esac
@@ -82,10 +74,6 @@ else
fi
echo "== wget flavour selection =="
-# A stub /usr/bin/wget whose --version answer comes from the environment, so
-# both flavours can be exercised on a single host. The empty-answer/non-zero
-# case is uclient-fetch, whose getopt rejects --version just like it rejects
-# -nv before sending anything.
rm -rf "$WORK/wget_flavor"
mkdir -p "$WORK/wget_flavor"
cat > "$WORK/wget_flavor/wget-stub" <<'STUB'
@@ -125,8 +113,6 @@ for flavor in gnu compat; do
done
echo "== real uclient-fetch compatibility =="
-# uclient-fetch is the default /usr/bin/wget on OpenWrt/ImmortalWrt, so a
-# GNU-only option anywhere in the generated command line has to fail here.
if [ -x /bin/uclient-fetch ]; then
rm -rf "$WORK/uclient"
mkdir -p "$WORK/uclient"
diff --git a/luci-app-homeproxy/tests/ucode/test_firewall_template.sh b/luci-app-homeproxy/tests/ucode/test_firewall_template.sh
index 51a03eb4c..4ccae333e 100644
--- a/luci-app-homeproxy/tests/ucode/test_firewall_template.sh
+++ b/luci-app-homeproxy/tests/ucode/test_firewall_template.sh
@@ -1,15 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
-#
-# Render firewall_post.ut and verify the homeproxy fw4 objects survive as real
-# nft statements.
-#
-# The template opens with `{%-`, which trims the whitespace in front of it: any
-# text placed above that tag loses its trailing newline and is glued onto the
-# first generated statement, turning it into a comment. That silently dropped
-# every homeproxy chain/set from the fw4 ruleset once, so keep this check.
-#
-# Usage: sh tests/ucode/test_firewall_template.sh
ROOT="${1:-.}"
ROOT="$(cd "$ROOT" && pwd)"
@@ -23,8 +13,6 @@ if ! utpl -S "$ROOT/root/etc/homeproxy/scripts/firewall_post.ut" > "$OUT" 2>"/tm
exit 1
fi
-# Declared unconditionally by the template, so they must always be present as
-# standalone statements.
for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
if ! grep -q "^set $name {" "$OUT"; then
echo "FAIL: 'set $name {' is missing from the rendered firewall template"
@@ -33,8 +21,6 @@ for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
fi
done
-# A statement glued onto the preceding comment looks like
-# "# .set homeproxy_x {" -- exactly the failure mode above.
if grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" > "/dev/null"; then
echo "FAIL: an nft statement is glued onto a comment line"
grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" | head -2
diff --git a/luci-app-homeproxy/tests/ucode/test_fw4_names.sh b/luci-app-homeproxy/tests/ucode/test_fw4_names.sh
index fd037df1c..c66ca204b 100644
--- a/luci-app-homeproxy/tests/ucode/test_fw4_names.sh
+++ b/luci-app-homeproxy/tests/ucode/test_fw4_names.sh
@@ -1,13 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
-#
-# The fw4 chain/set inventory in scripts/fw4_names.sh must stay in sync with
-# the objects declared by scripts/firewall_post.ut: the first drives the
-# cleanup in init.d/homeproxy, the second creates them. A drift means either a
-# leftover object or a delete of something that never existed (which used to
-# abort the whole cleanup batch).
-#
-# Usage: sh tests/ucode/test_fw4_names.sh
ROOT="${1:-.}"
ROOT="$(cd "$ROOT" && pwd)"
@@ -18,7 +10,6 @@ PRE="$ROOT/root/etc/homeproxy/scripts/firewall_pre.uc"
FAILED=0
-# shellcheck source=/dev/null
. "$NAMES"
check_group() {
@@ -57,8 +48,6 @@ post_sets="$(grep -oE '^set homeproxy_[a-z0-9_]+' "$POST" | awk '{print $2}' | s
check_group "chain" "$HP_FW4_CHAINS" "$post_chains"
check_group "set" "$HP_FW4_SETS" "$post_sets"
-# firewall_pre.uc only writes forward/input accept rules; it must not own any
-# chain/set of its own.
pre_refs="$(grep -oE 'homeproxy_[a-z0-9_]+' "$PRE" | sort -u)"
if [ -n "$pre_refs" ]; then
echo "NOTE: firewall_pre.uc references fw4 objects:$pre_refs"
diff --git a/luci-app-homeproxy/tests/ucode/test_generators.sh b/luci-app-homeproxy/tests/ucode/test_generators.sh
index 074ba6405..48add4891 100755
--- a/luci-app-homeproxy/tests/ucode/test_generators.sh
+++ b/luci-app-homeproxy/tests/ucode/test_generators.sh
@@ -1,13 +1,5 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
-#
-# Run generate_client.uc / generate_server.uc against the UCI fixtures and let
-# sing-box validate the result. The generators read /etc/config and write
-# /var/run, so this script materialises an isolated copy of them: homeproxy.uc
-# is rewritten to point HP_DIR/RUN_DIR at a scratch directory, and the uci
-# cursor is pointed at the fixture config.
-#
-# Usage: sh tests/ucode/test_generators.sh [work-dir]
ROOT="${1:-.}"
WORK="${2:-/tmp/hp-generator-test}"
@@ -29,8 +21,6 @@ run_case() {
: > "$dir/resources/proxy_list.txt"
if [ "$local_rs" = "1" ]; then
- # with the .srs files that update_resources.sh downloads in place, the
- # generator has to load them instead of the remote URLs
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/rs-src.json"
if ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geoip_cn.srs" \
|| ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geosite_cn.srs"; then
@@ -41,7 +31,6 @@ run_case() {
fi
if grep -q "__RULESET_DIR__" "$fixture"; then
- # The fixture needs a real local rule-set on disk.
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/ruleset/src.json"
if ! sing-box rule-set compile "$dir/ruleset/src.json" -o "$dir/ruleset/test.srs"; then
echo "FAIL: $name: could not compile the local rule-set fixture"
@@ -83,10 +72,6 @@ run_case() {
run_case client "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
-# Golden comparison for the client generator. `sing-box check` only proves the
-# config parses; this proves the generated config did not change silently (rule
-# order, DNS routing decisions, whether a field is emitted at all). The work
-# directory is normalized out of the paths before comparing.
CLIENT_GOLDEN="$ROOT/tests/snapshots/client.generated.json"
CLIENT_OUT="$WORK/client/run/sing-box-c.json"
if [ ! -f "$CLIENT_GOLDEN" ]; then
@@ -103,10 +88,6 @@ else
FAILED=1
fi
-# The preset remote rule-sets must be fetched through the node. A direct
-# download depends on the CDN staying reachable from mainland China and fails
-# intermittently under DNS pollution, which shows up as "open connection to
-# :443 using outbound/direct[direct]: i/o timeout" in sing-box-c.log.
if grep -q '"detour": "direct-out"' "$WORK/client/run/sing-box-c.json"; then
echo "FAIL: client: a remote rule-set would still be downloaded directly"
FAILED=1
@@ -116,9 +97,6 @@ if ! grep -q '"detour": "main-out"' "$WORK/client/run/sing-box-c.json"; then
FAILED=1
fi
-# Local rule-sets (downloaded by update_resources.sh) must be preferred over
-# the remote URLs, so a cold start does not depend on reaching the CDN through
-# the node.
run_case client-local "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json 1
if grep -q '"type": "local"' "$WORK/client-local/run/sing-box-c.json" \
&& ! grep -q '"type": "remote"' "$WORK/client-local/run/sing-box-c.json"; then
@@ -129,12 +107,6 @@ else
fi
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json
-# Custom mode without a default outbound: the default shape of the custom UI.
-# `sing-box check` alone is not enough -- before the fix the generator emitted a
-# config with none of the user's DNS servers, nodes, rules or rule-sets, and
-# sing-box 1.14 then refused to start it for a missing
-# route.default_domain_resolver. Assert on the emitted config so a partial
-# regression cannot pass as green.
run_case custom_no_default "$ROOT/tests/fixtures/generators/custom-no-default-outbound.uci" generate_client.uc sing-box-c.json
CUSTOM_OUT="$WORK/custom_no_default/run/sing-box-c.json"
@@ -150,12 +122,8 @@ assert_json() {
fi
}
-# sing-box always emits default-dns and system-dns, so more than one DNS server
-# is always present and 1.14 requires a global domain resolver.
assert_json "custom/no-default: a global domain resolver is declared" '"default_domain_resolver"'
assert_json "custom/no-default: the user DNS server survived" '"cfg-DNS_Server_Proxy-dns"'
-# A sing-box rule has no label field, so the UCI `option label` never reaches the
-# JSON. Match the marker the rule body actually carries.
assert_json "custom/no-default: the user DNS rule survived" 'dns-rule-marker.example.com'
assert_json "custom/no-default: the urltest group survived" '"cfg-Routing_Node_Proxy-out"'
assert_json "custom/no-default: its member nodes survived" '"cfg-n_hk-out"'
diff --git a/luci-app-homeproxy/tests/ucode/test_homeproxy_utils.uc b/luci-app-homeproxy/tests/ucode/test_homeproxy_utils.uc
index c4d114681..68e099dbb 100644
--- a/luci-app-homeproxy/tests/ucode/test_homeproxy_utils.uc
+++ b/luci-app-homeproxy/tests/ucode/test_homeproxy_utils.uc
@@ -3,10 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
- *
- * Regression tests for homeproxy.uc helpers, focused on executeCommand():
- * return shape, stderr/exit-code capture, binary detection and (most
- * importantly) that the temporary descriptors are closed on every run.
*/
'use strict';
@@ -32,7 +28,6 @@ function fd_count() {
return n;
}
-/* successful command */
const ok = executeCommand('echo', 'hello');
expect('ok.exitcode', ok.exitcode, 0);
expect('ok.stdout', ok.stdout, 'hello\n');
@@ -40,29 +35,22 @@ expect('ok.stderr', ok.stderr, '');
expect('ok.binary', ok.binary, false);
expect('ok.command', ok.command, 'echo hello');
-/* failing command keeps stderr and the exit code */
const bad = executeCommand('sh', '-c', shellQuote('echo oops >&2; exit 3'));
expect('bad.exitcode', bad.exitcode, 3);
expect('bad.stderr', bad.stderr, 'oops\n');
expect('bad.stdout', bad.stdout, '');
-/* nondescript exit code */
expect('false.exitcode', executeCommand('false').exitcode, 1);
-/* binary output is detected and withheld */
const bin = executeCommand('sh', '-c', shellQuote('printf "\\001\\002\\003"'));
expect('bin.binary', bin.binary, true);
expect('bin.stdout', bin.stdout, null);
-/* output larger than the old one-shot 512 KiB read must not be truncated
- (subscription lists above that size used to arrive cut in half) */
const big_want = 700 * 1024;
const big = executeCommand('sh', '-c', shellQuote('yes a | head -c ' + big_want));
expect('big.exitcode', big.exitcode, 0);
expect('big.length', length(big.stdout), big_want);
-/* isValidCIDR(): the prefix needs the same strict validation as the address,
- otherwise a poisoned resource file reaches the nftables template */
expect('cidr.ok', isValidCIDR('1.2.3.4/24', 4), true);
expect('cidr.bare', isValidCIDR('1.2.3.4', 4), true);
expect('cidr.bad-prefix', isValidCIDR('1.2.3.4/abc', 4), false);
@@ -73,7 +61,6 @@ expect('cidr.injection', isValidCIDR('1.2.3.4/0 } flush ruleset; table inet x {'
expect('cidr.v6-ok', isValidCIDR('2001:db8::/32', 6), true);
expect('cidr.v6-injection', isValidCIDR('2001:db8::/32 } flush ruleset;', 6), false);
-/* isValidPEM(): certificate vs private key, boundaries and body */
const pem_cert = '-----BEGIN CERTIFICATE-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END CERTIFICATE-----';
const pem_key = '-----BEGIN RSA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END RSA PRIVATE KEY-----';
expect('pem.cert', isValidPEM(pem_cert, false), true);
@@ -83,7 +70,6 @@ expect('pem.key-as-cert', isValidPEM(pem_key, false), false);
expect('pem.garbage', isValidPEM('not a pem at all', false), false);
expect('pem.empty', isValidPEM('', false), false);
-/* descriptors must not leak across calls */
const before = fd_count();
for (let i = 0; i < 200; i++)
executeCommand('true');
diff --git a/luci-app-homeproxy/tests/ucode/test_homeproxy_utils_inject.uc b/luci-app-homeproxy/tests/ucode/test_homeproxy_utils_inject.uc
index a81722d7b..ed3281933 100644
--- a/luci-app-homeproxy/tests/ucode/test_homeproxy_utils_inject.uc
+++ b/luci-app-homeproxy/tests/ucode/test_homeproxy_utils_inject.uc
@@ -3,11 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
- *
- * Exceptional-path test for executeCommand(): the copy of homeproxy.uc staged
- * next to this file has its `system()` call replaced by `die()`, so every call
- * throws before the descriptors are read. ucode has no `finally`, so this
- * checks that the catch block still closes both descriptors.
*/
'use strict';
diff --git a/luci-app-homeproxy/tests/ucode/test_parse_uri.uc b/luci-app-homeproxy/tests/ucode/test_parse_uri.uc
index af8a1e1de..608bc8848 100644
--- a/luci-app-homeproxy/tests/ucode/test_parse_uri.uc
+++ b/luci-app-homeproxy/tests/ucode/test_parse_uri.uc
@@ -3,10 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
- *
- * Unit tests for the share-link parsers in parse_uri.uc. Run through
- * tests/ucode/run.sh, which stages parse_uri.uc next to the mocked
- * homeproxy.uc.
*/
'use strict';
@@ -44,13 +40,11 @@ function expect_null(name, cfg) {
}
}
-/* anytls */
expect_fields('anytls', parse_uri('anytls://secret@a.example.com:443?sni=a.example.com&insecure=1#AnyTLS-Node', FEATURES, LOG), {
label: 'AnyTLS-Node', type: 'anytls', address: 'a.example.com', port: '443',
password: 'secret', tls: '1', tls_sni: 'a.example.com', tls_insecure: '1'
});
-/* http / https */
expect_fields('https', parse_uri('https://user:pass@b.example.com:8443#HTTPS-Node', FEATURES, LOG), {
label: 'HTTPS-Node', type: 'http', address: 'b.example.com', port: '8443',
username: 'user', password: 'pass', tls: '1'
@@ -59,7 +53,6 @@ expect_fields('http', parse_uri('http://c.example.com:8080#HTTP-Node', FEATURES,
label: 'HTTP-Node', type: 'http', address: 'c.example.com', port: '8080', tls: '0'
});
-/* hysteria */
expect_fields('hysteria', parse_uri('hysteria://d.example.com:36712?auth=pass&peer=d.example.com&insecure=1&upmbps=100&downmbps=200&alpn=h3&protocol=udp#Hy1', FEATURES, LOG), {
label: 'Hy1', type: 'hysteria', address: 'd.example.com', port: '36712',
hysteria_protocol: 'udp', hysteria_auth_type: 'string', hysteria_auth_payload: 'pass',
@@ -67,7 +60,6 @@ expect_fields('hysteria', parse_uri('hysteria://d.example.com:36712?auth=pass&pe
tls: '1', tls_insecure: '1', tls_sni: 'd.example.com', tls_alpn: 'h3'
});
-/* hysteria2 */
expect_fields('hysteria2', parse_uri('hysteria2://pass@e.example.com:443?sni=e.example.com&obfs=salamander&obfs-password=op#Hy2', FEATURES, LOG), {
label: 'Hy2', type: 'hysteria2', address: 'e.example.com', port: '443',
password: 'pass', hysteria_obfs_type: 'salamander', hysteria_obfs_password: 'op',
@@ -77,14 +69,12 @@ expect_fields('hy2-alias', parse_uri('hy2://pass@e.example.com:443#Hy2Alias', FE
label: 'Hy2Alias', type: 'hysteria2', address: 'e.example.com', port: '443'
});
-/* snell */
expect_fields('snell', parse_uri('snell://f.example.com:443?psk=pskvalue&version=4&obfs=http&obfs-host=bing.com&reuse=1#Snell', FEATURES, LOG), {
label: 'Snell', type: 'snell', address: 'f.example.com', port: '443',
password: 'pskvalue', snell_version: '4', snell_obfs_mode: 'http',
snell_obfs_host: 'bing.com', snell_reuse: '1'
});
-/* socks */
expect_fields('socks5', parse_uri('socks5://user:pass@g.example.com:1080#Socks', FEATURES, LOG), {
label: 'Socks', type: 'socks', address: 'g.example.com', port: '1080',
username: 'user', password: 'pass', socks_version: '5'
@@ -93,25 +83,21 @@ expect_fields('socks4', parse_uri('socks4://h.example.com:1080#Socks4', FEATURES
socks_version: '4'
});
-/* shadowsocks: SIP002 with base64 user info */
expect_fields('ss-sip002-b64', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@i.example.com:8388#SS-Node', FEATURES, LOG), {
label: 'SS-Node', type: 'shadowsocks', address: 'i.example.com', port: '8388',
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
});
-/* shadowsocks: SIP002 with plugin */
expect_fields('ss-sip002-plugin', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@j.example.com:8388?plugin=simple-obfs%3Bobfs%3Dhttp#SS-Plugin', FEATURES, LOG), {
label: 'SS-Plugin', type: 'shadowsocks', address: 'j.example.com', port: '8388',
shadowsocks_plugin: 'obfs-local', shadowsocks_plugin_opts: 'obfs=http'
});
-/* shadowsocks: Shadowrocket whole-payload base64 */
expect_fields('ss-shadowrocket', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmRAay5leGFtcGxlLmNvbTo4Mzg4#SS-SR', FEATURES, LOG), {
label: 'SS-SR', type: 'shadowsocks', address: 'k.example.com', port: '8388',
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
});
-/* trojan */
expect_fields('trojan-ws', parse_uri('trojan://pass@l.example.com:443?type=ws&host=l.example.com&path=%2Fpath%3Fed%3D2048&sni=l.example.com#TrojanWs', FEATURES, LOG), {
label: 'TrojanWs', type: 'trojan', address: 'l.example.com', port: '443', password: 'pass',
transport: 'ws', ws_host: 'l.example.com', ws_path: '/path',
@@ -122,14 +108,12 @@ expect_fields('trojan-grpc', parse_uri('trojan://pass@l.example.com:443?type=grp
transport: 'grpc', grpc_servicename: 'gs'
});
-/* tuic */
expect_fields('tuic', parse_uri('tuic://tuic-uuid:pass@m.example.com:443?congestion_control=bbr&udp_relay_mode=native&alpn=h3&sni=m.example.com#Tuic', FEATURES, LOG), {
label: 'Tuic', type: 'tuic', address: 'm.example.com', port: '443',
uuid: 'tuic-uuid', password: 'pass', tuic_congestion_control: 'bbr',
tuic_udp_relay_mode: 'native', tls: '1', tls_sni: 'm.example.com', tls_alpn: [ 'h3' ]
});
-/* vless */
expect_fields('vless-reality-grpc', parse_uri('vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&fp=chrome&type=grpc&serviceName=gs&sni=n.example.com&flow=xtls-rprx-vision#Vless', FEATURES, LOG), {
label: 'Vless', type: 'vless', address: 'n.example.com', port: '443', uuid: 'vless-uuid',
transport: 'grpc', grpc_servicename: 'gs', tls: '1', tls_sni: 'n.example.com',
@@ -142,7 +126,6 @@ expect_fields('vless-ws-early-data', parse_uri('vless://vless-uuid@o.example.com
tls: '1', tls_alpn: [ 'h2', 'http/1.1' ]
});
-/* vmess */
expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0=', FEATURES, LOG), {
label: 'VMess', type: 'vmess', address: 'p.example.com', port: '443',
uuid: '3af88561-9c69-4b19-8f7e-f08d580bc339', vmess_alterid: '0', vmess_encrypt: 'auto',
@@ -150,7 +133,6 @@ expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQ
tls: '1', tls_sni: 'p.example.com'
});
-/* sip008 object */
expect_fields('sip008', parse_uri({
nodetype: 'sip008', remarks: 'SIP008', server: 'q.example.com', server_port: 8388,
method: 'aes-256-gcm', password: 'pw'
@@ -159,30 +141,23 @@ expect_fields('sip008', parse_uri({
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'pw'
});
-/* rejection paths */
expect_null('vless-kcp-unsupported', parse_uri('vless://u@r.example.com:443?type=kcp#Kcp', FEATURES, LOG));
expect_null('hysteria-without-quic', parse_uri('hysteria://s.example.com:443?auth=a#NoQuic', NO_QUIC, LOG));
expect_null('tuic-without-quic', parse_uri('tuic://u:p@s.example.com:443#NoQuic', NO_QUIC, LOG));
expect_null('invalid-port', parse_uri('vless://u@t.example.com:99999?security=tls#BadPort', FEATURES, LOG));
expect_null('unknown-scheme', parse_uri('unknown://u@t.example.com:443', FEATURES, LOG));
-/* sing-box has no xhttp transport; importing it used to make the whole
- generated config fail with "unknown transport type" */
expect_null('vless-xhttp-unsupported', parse_uri('vless://u@t.example.com:443?security=tls&type=xhttp#Xhttp', FEATURES, LOG));
-/* percent-encoded base64 userinfo (%3D instead of =): used to lose method and
- password, which made the node break the generated config */
expect_fields('ss-percent-encoded-userinfo', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ%3D@j.example.com:8388#SSEnc', FEATURES, LOG), {
label: 'SSEnc', type: 'shadowsocks', address: 'j.example.com', port: '8388',
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
});
-/* unsupported flow must import as "no flow" instead of breaking sing-box */
expect_fields('vless-flow-udp443-dropped', parse_uri('vless://vless-uuid@n.example.com:443?security=tls&flow=xtls-rprx-vision-udp443&sni=n.example.com#FlowDrop', FEATURES, LOG), {
label: 'FlowDrop', type: 'vless', address: 'n.example.com', port: '443',
uuid: 'vless-uuid', tls: '1', tls_sni: 'n.example.com', vless_flow: null
});
-/* punctuation in a password survives now that userinfo is not whitelisted */
expect_fields('trojan-punctuation-password', parse_uri('trojan://pa!ss~1@m.example.com:443?sni=m.example.com#Punct', FEATURES, LOG), {
label: 'Punct', type: 'trojan', address: 'm.example.com', port: '443',
password: 'pa!ss~1', tls: '1', tls_sni: 'm.example.com'
diff --git a/luci-app-homeproxy/tests/ucode/test_wget_flavor.uc b/luci-app-homeproxy/tests/ucode/test_wget_flavor.uc
index 40f2ca163..121a96683 100644
--- a/luci-app-homeproxy/tests/ucode/test_wget_flavor.uc
+++ b/luci-app-homeproxy/tests/ucode/test_wget_flavor.uc
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2026 ImmortalWrt.org
- *
- * Regression tests for the wget flavour probe and for the command line
- * wGETVerbose() builds from it.
- *
- * OpenWrt/ImmortalWrt's default /usr/bin/wget is uclient-fetch, which rejects
- * GNU-only flags such as -nv with "unrecognized option: n" *before* it opens a
- * connection, so a single GNU-ism turns every subscription update into
- * "no valid node found". run.sh stages a copy of homeproxy.uc whose
- * /usr/bin/wget points at a stub; HP_WGET_STUB_VERSION steers the answer of
- * the `--version` probe (empty output plus a non-zero exit mimics
- * uclient-fetch, whose getopt rejects the option and returns 1).
*/
'use strict';
@@ -39,7 +28,6 @@ function has(list, value) {
return false;
}
-/* The value following the first occurrence of `key` */
function after(list, key) {
let prev = null;
@@ -53,7 +41,6 @@ function after(list, key) {
return null;
}
-/* Classification of the `wget --version` probe. */
expect('gnu.version', classifyWgetFlavor(0, 'GNU Wget 1.25.0 built on linux-gnu.\n'), 'gnu');
expect('gnu.multiline', classifyWgetFlavor(0, "GNU Wget 1.21.4 built on linux-gnu.\n\nCopyright (C) 2015 Free Software Foundation.\n"), 'gnu');
expect('uclient.rejects-version', classifyWgetFlavor(1, null), 'compat');
@@ -61,7 +48,6 @@ expect('busybox.version', classifyWgetFlavor(0, 'BusyBox v1.36.1 (2024-01-01) mu
expect('empty.stdout', classifyWgetFlavor(0, ''), 'compat');
expect('missing.binary', classifyWgetFlavor(127, null), 'compat');
-/* wgetFlavor() probes the stub and caches the answer. */
const want_flavor = getenv('HP_WGET_EXPECT_FLAVOR');
expect('flavor', wgetFlavor(), want_flavor);
expect('flavor.cached', wgetFlavor(), want_flavor);
@@ -79,8 +65,6 @@ expect('timeout flag', after(args, '-T'), '10');
expect('output flag', has(args, '-O-'), true);
expect('url', has(args, 'https://example.invalid/subs?token=abc'), true);
-/* -nv is GNU wget's --no-verbose; every other wget on the target rejects it,
- * and -q is the meter switch they all share. */
expect('meter flag', has(args, want_flavor === 'gnu' ? '-nv' : '-q'), true);
expect('no gnu-only flag', has(args, '-nv'), want_flavor === 'gnu');
expect('no long options', has(args, '--user-agent') || has(args, '--timeout=10'), false);
diff --git a/luci-app-homeproxy/tests/ucode/test_wget_verbose_uclient.uc b/luci-app-homeproxy/tests/ucode/test_wget_verbose_uclient.uc
index 2fcafc73d..46c4208b6 100644
--- a/luci-app-homeproxy/tests/ucode/test_wget_verbose_uclient.uc
+++ b/luci-app-homeproxy/tests/ucode/test_wget_verbose_uclient.uc
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2026 ImmortalWrt.org
- *
- * Feeds the command line wGETVerbose() builds to the target's real
- * uclient-fetch - the default /usr/bin/wget on OpenWrt/ImmortalWrt. run.sh
- * rewrites the module's wget path to /bin/uclient-fetch and skips this file
- * where that binary does not exist.
- *
- * A closed loopback port is the entire request: what matters is that
- * uclient-fetch gets past getopt. Before the flavour probe existed it did not -
- * `-nv` made it print "unrecognized option: n" and exit before the request was
- * sent, so every subscription fetch came back empty. Any failure observed here
- * must therefore come from the network, never from the option parser.
*/
'use strict';
@@ -30,7 +19,6 @@ function expect(name, actual, want) {
}
}
-/* uclient-fetch has no --version and rejects it, so it is classified compat. */
expect('flavor', wgetFlavor(), 'compat');
const res = wGETVerbose('http://127.0.0.1:1/subs', 'HomeProxy test UA');
diff --git a/luci-app-honk/Makefile b/luci-app-honk/Makefile
index 5c163fa23..dc8c0bd74 100644
--- a/luci-app-honk/Makefile
+++ b/luci-app-honk/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-honk
PKG_VERSION:=1.1.4
-PKG_RELEASE:=21
+PKG_RELEASE:=22
LUCI_TITLE:=LuCI support for honk
LUCI_PKGARCH:=all
diff --git a/luci-app-honk/root/etc/uci-defaults/luci-honk b/luci-app-honk/root/etc/uci-defaults/luci-honk
index b14d801e4..711152bf0 100644
--- a/luci-app-honk/root/etc/uci-defaults/luci-honk
+++ b/luci-app-honk/root/etc/uci-defaults/luci-honk
@@ -10,6 +10,14 @@ if [ "$dash" = "none" ]; then
: > /etc/honk/config.d/api.dae
fi
+# Drop options of the auto-subscribe feature removed from this package
+uci -q show honk 2>/dev/null | sed -n 's/^\(.*\)\.subscribe_[a-z_]*=.*$/\1/p' | sort -u | while read -r s; do
+ for opt in subscribe_auto_update subscribe_update_week_time subscribe_update_day_time; do
+ uci -q delete "$s.$opt"
+ done
+ uci -q commit honk
+done
+
# Migrate legacy /etc/honk/dashboard directory
if [ -d /etc/honk/dashboard ] && [ ! -d /etc/honk/doona ]; then
mv /etc/honk/dashboard /etc/honk/doona
diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile
index 36b96ce9a..11efd4736 100644
--- a/luci-app-open-box/Makefile
+++ b/luci-app-open-box/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
-PKG_VERSION:=0.1.311
-PKG_RELEASE:=23
+PKG_VERSION:=0.1.313
+PKG_RELEASE:=24
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index 5ca7a57bc..527665378 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.4
-PKG_RELEASE:=326
+PKG_RELEASE:=327
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua
index 7ddfdf29d..4a2f7ac7f 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua
@@ -315,9 +315,6 @@ o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
o:depends({ node = "__always__" })
-o = s:taboption("Main", HideValue, "_node_sel_other", "")
-o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
-
---- DNS
o = s:taboption("DNS", ListValue, "dns_shunt", "DNS " .. translate("Shunt"))
o.default = "chinadns-ng"
@@ -347,7 +344,7 @@ end
if has_xray then
o:value("xray", "Xray")
end
-o:depends({ _acl_node_bool = "1", _node_sel_other = "1", _diff_global_node = "1" })
+o:depends({ _acl_node_bool = "1", _node_sel_shunt = "", _diff_global_node = "1" })
o.write = function(self, section, value)
if value == "dns2socks" then
m:del(section, "v2ray_dns_mode")
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua
index 5ec4d6dd1..d353dc0b1 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua
@@ -136,13 +136,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
tips.cfgvalue = function(t, n)
return string.format('%s', translate("There are no available nodes, please add or subscribe nodes first."))
end
- tips:depends("_node", "1")
- for k, v in pairs(shunt_list) do
- tips:depends("node", v.id)
- end
- for k, v in pairs(balancing_list) do
- tips:depends("node", v.id)
- end
+ tips:depends({ node = "", ['!reverse'] = true })
end
end
@@ -150,7 +144,7 @@ o = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks "
o.default = 1070
o.placeholder = 1070
o.datatype = "range(1,65535)"
-o:depends("_node", "1")
+o:depends({ node = "", ['!reverse'] = true })
--[[
if has_singbox or has_xray then
o = s:taboption("Main", Value, "node_http_port", translate("Node") .. " HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use"))
@@ -160,22 +154,16 @@ end
]]--
o = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "1"
-o:depends("_node", "1")
+o:depends({ node = "", ['!reverse'] = true })
o = s:taboption("Main", HideValue, "node_save_before", "")
o.value = current_node[".name"]
o.cbid = function(self, section) return "node_save_before" end
-o = s:taboption("Main", HideValue, "_node", "")
-o:depends({ node = "", ['!reverse'] = true })
-
-- Node → DNS Depends Settings
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
o:depends({ node = "__always__" })
-o = s:taboption("Main", HideValue, "_node_sel_other", "")
-o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
-
-- [[ DNS Settings ]]--
s:tab("DNS", translate("DNS"))
@@ -279,8 +267,8 @@ end
if has_xray then
o:value("xray", "Xray")
end
-o:depends({ dns_shunt = "chinadns-ng", _node_sel_other = "1" })
-o:depends({ dns_shunt = "dnsmasq", _node_sel_other = "1" })
+o:depends({ dns_shunt = "chinadns-ng", _node_sel_shunt = "" })
+o:depends({ dns_shunt = "dnsmasq", _node_sel_shunt = "" })
o.write = function(self, section, value)
if value ~= "sing-box" and value ~= "xray" then
m:del(section, "v2ray_dns_mode")
@@ -305,7 +293,7 @@ if api.is_finded("smartdns") then
if has_xray then
o:value("xray", "Xray")
end
- o:depends({ dns_shunt = "smartdns", _node_sel_other = "1" })
+ o:depends({ dns_shunt = "smartdns", _node_sel_shunt = "" })
o.write = function(self, section, value)
if value == "socks" then
m:del(section, "v2ray_dns_mode")
diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile
index 1aa416f41..9f74ff2fb 100644
--- a/luci-app-ssr-plus/Makefile
+++ b/luci-app-ssr-plus/Makefile
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
-PKG_RELEASE:=87
+PKG_RELEASE:=88
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
diff --git a/luci-app-ssr-plus/root/usr/bin/ssr-rules b/luci-app-ssr-plus/root/usr/bin/ssr-rules
index fcc85da48..a96de48a5 100755
--- a/luci-app-ssr-plus/root/usr/bin/ssr-rules
+++ b/luci-app-ssr-plus/root/usr/bin/ssr-rules
@@ -111,7 +111,7 @@ usage() {
loger() {
# 1.alert 2.crit 3.err 4.warn 5.notice 6.info 7.debug
- logger -st ssr-rules[$$] -p$1 $2
+ logger -st "ssr-rules[$$]" -p "$1" -- "$2"
}
# IP list normalization function (for comparison)
diff --git a/mise/Makefile b/mise/Makefile
index 96c6644cb..0efdfd9de 100644
--- a/mise/Makefile
+++ b/mise/Makefile
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
PKG_NAME:=mise
-PKG_VERSION:=2026.10.7
-PKG_RELEASE:=32
+PKG_VERSION:=2026.10.8
+PKG_RELEASE:=33
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip
diff --git a/nexttrace/Makefile b/nexttrace/Makefile
index c000f3fa7..b2fee762a 100644
--- a/nexttrace/Makefile
+++ b/nexttrace/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=nexttrace
-PKG_VERSION:=1.7.3
-PKG_RELEASE:=16
+PKG_VERSION:=1.7.4
+PKG_RELEASE:=17
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
diff --git a/openwrt-natmapt/Makefile b/openwrt-natmapt/Makefile
index 2a7b5b50c..4973fa6d0 100644
--- a/openwrt-natmapt/Makefile
+++ b/openwrt-natmapt/Makefile
@@ -9,7 +9,7 @@ PKG_NAME:=natmapt
PKG_UPSTREAM_VERSION:=20260214
PKG_UPSTREAM_GITHASH:=
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)))
-PKG_RELEASE:=11
+PKG_RELEASE:=12
SCRIPTS_VERSION:=0.2026.01.24
PKG_SOURCE_SUBDIR:=$(PKG_UPSTREAM_NAME)-$(PKG_UPSTREAM_VERSION)
@@ -23,7 +23,7 @@ PKG_SOURCE:=$(PKG_SOURCE_SUBDIR).tar.xz
else
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/heiher/natmap.git
-PKG_SOURCE_VERSION:=f3b9b2e5d2027eb1c2bd95c0afb582c865d16805
+PKG_SOURCE_VERSION:=ed69834e8139744abe1b70c341dbd2c3b40ea53e
PKG_MIRROR_HASH:=skip
PKG_SOURCE:=$(PKG_SOURCE_SUBDIR)-$(PKG_SOURCE_VERSION).tar.gz