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