From 9a2ef6ebbc3ba4edc543d1f29e8a2e3b983839a0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 May 2026 21:08:15 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=81=20Sync=202026-05-19=2021:08:14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-cpu-perf/Makefile | 6 +- luci-app-cpu-perf/README.md | 24 +- luci-app-cpu-perf/root/etc/init.d/cpu-perf | 6 +- luci-app-modeminfo/Makefile | 2 +- .../luci-static/resources/view/modem/hw.js | 268 +++--- .../luci-static/resources/view/modem/main.js | 905 ++++++++++-------- luci-app-passwall/Makefile | 2 +- .../model/cbi/passwall/client/acl_config.lua | 99 +- .../passwall/client/node_subscribe_config.lua | 9 + .../model/cbi/passwall/client/type/ray.lua | 9 + .../cbi/passwall/client/type/sing-box.lua | 14 +- luci-app-passwall/luasrc/passwall/api.lua | 80 ++ .../luasrc/passwall/util_sing-box.lua | 14 +- .../luasrc/passwall/util_xray.lua | 5 + .../view/passwall/node_list/node_list.htm | 2 + luci-app-passwall/po/zh-cn/passwall.po | 3 + .../root/usr/share/passwall/subscribe.lua | 7 +- luci-app-xray/Makefile | 2 +- .../root/usr/share/xray/firewall_include.ut | 18 + .../resources/view/xray/preview.js | 3 + mihomo-alpha/Makefile | 8 +- modeminfo/Makefile | 2 +- .../usr/share/modeminfo/scripts/modeminfo | 2 +- 23 files changed, 813 insertions(+), 677 deletions(-) diff --git a/luci-app-cpu-perf/Makefile b/luci-app-cpu-perf/Makefile index c1cf4a50..ec0bbe9f 100644 --- a/luci-app-cpu-perf/Makefile +++ b/luci-app-cpu-perf/Makefile @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 gSpot (https://github.com/gSpotx2f/luci-app-cpu-perf) +# Copyright (C) 2026 gSpot (https://github.com/gSpotx2f/luci-app-cpu-perf) # # This is free software, licensed under the MIT License. # @@ -7,8 +7,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-cpu-perf -PKG_VERSION:=0.6.1 -PKG_RELEASE:=2 +PKG_VERSION:=0.6.2 +PKG_RELEASE:=3 LUCI_TITLE:=CPU performance information and management for LuCI LUCI_DEPENDS:=+ucode +ucode-mod-fs LUCI_PKGARCH:=all diff --git a/luci-app-cpu-perf/README.md b/luci-app-cpu-perf/README.md index dee308c1..359a195e 100644 --- a/luci-app-cpu-perf/README.md +++ b/luci-app-cpu-perf/README.md @@ -9,31 +9,31 @@ OpenWrt >= 22.03. **OpenWrt >= 25.12:** - wget --no-check-certificate -O /tmp/luci-app-cpu-perf-0.6.1-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-cpu-perf-0.6.1-r1.apk - apk --allow-untrusted add /tmp/luci-app-cpu-perf-0.6.1-r1.apk - rm /tmp/luci-app-cpu-perf-0.6.1-r1.apk + wget --no-check-certificate -O /tmp/luci-app-cpu-perf-0.6.2-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-app-cpu-perf-0.6.2-r1.apk + apk --allow-untrusted add /tmp/luci-app-cpu-perf-0.6.2-r1.apk + rm /tmp/luci-app-cpu-perf-0.6.2-r1.apk service rpcd restart service cpu-perf start i18n-ru: - wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru-0.6.1-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-cpu-perf-ru-0.6.1-r1.apk - apk --allow-untrusted add /tmp/luci-i18n-cpu-perf-ru-0.6.1-r1.apk - rm /tmp/luci-i18n-cpu-perf-ru-0.6.1-r1.apk + wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru-0.6.2-r1.apk https://github.com/gSpotx2f/packages-openwrt/raw/master/25.12/luci-i18n-cpu-perf-ru-0.6.2-r1.apk + apk --allow-untrusted add /tmp/luci-i18n-cpu-perf-ru-0.6.2-r1.apk + rm /tmp/luci-i18n-cpu-perf-ru-0.6.2-r1.apk **OpenWrt <= 24.10:** - wget --no-check-certificate -O /tmp/luci-app-cpu-perf_0.6.1-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-cpu-perf_0.6.1-r1_all.ipk - opkg install /tmp/luci-app-cpu-perf_0.6.1-r1_all.ipk - rm /tmp/luci-app-cpu-perf_0.6.1-r1_all.ipk + wget --no-check-certificate -O /tmp/luci-app-cpu-perf_0.6.2-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-app-cpu-perf_0.6.2-r1_all.ipk + opkg install /tmp/luci-app-cpu-perf_0.6.2-r1_all.ipk + rm /tmp/luci-app-cpu-perf_0.6.2-r1_all.ipk service rpcd restart service cpu-perf start i18n-ru: - wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru_0.6.1-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-cpu-perf-ru_0.6.1-r1_all.ipk - opkg install /tmp/luci-i18n-cpu-perf-ru_0.6.1-r1_all.ipk - rm /tmp/luci-i18n-cpu-perf-ru_0.6.1-r1_all.ipk + wget --no-check-certificate -O /tmp/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/24.10/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk + opkg install /tmp/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk + rm /tmp/luci-i18n-cpu-perf-ru_0.6.2-r1_all.ipk ## Screenshots: diff --git a/luci-app-cpu-perf/root/etc/init.d/cpu-perf b/luci-app-cpu-perf/root/etc/init.d/cpu-perf index 003c3783..fd553dea 100755 --- a/luci-app-cpu-perf/root/etc/init.d/cpu-perf +++ b/luci-app-cpu-perf/root/etc/init.d/cpu-perf @@ -34,8 +34,8 @@ EAS_DEF=1 config_load $APP_NAME get_cpu_freq_policies() { + [ ! -d "$SYSFS_CPU_FREQ_DIR" ] && return echo $(ls "$SYSFS_CPU_FREQ_DIR" | grep "^policy[0-9]*$") - } reset_cpu_freq_policy_config() { @@ -67,7 +67,7 @@ set_cpu_freq_policy() { local scaling_min_freq scaling_max_freq scaling_governor local policy_dir="${SYSFS_CPU_FREQ_DIR}/${policy}" - [ ! -d $policy_dir ] && return + [ ! -d "$policy_dir" ] && return local scaling_min_freq_file="${policy_dir}/scaling_min_freq" local scaling_max_freq_file="${policy_dir}/scaling_max_freq" @@ -92,7 +92,7 @@ reset_cpu_freq_policy() { local policy="$1" local policy_dir="${SYSFS_CPU_FREQ_DIR}/${policy}" - [ ! -d $policy_dir ] && return + [ ! -d "$policy_dir" ] && return local min_freq_file="${policy_dir}/cpuinfo_min_freq" local max_freq_file="${policy_dir}/cpuinfo_max_freq" diff --git a/luci-app-modeminfo/Makefile b/luci-app-modeminfo/Makefile index 7daea3a1..17d58080 100644 --- a/luci-app-modeminfo/Makefile +++ b/luci-app-modeminfo/Makefile @@ -4,7 +4,7 @@ LUCI_TITLE:=Information dashboard for 3G/LTE dongle LUCI_DEPENDS:=+modeminfo PKG_LICENSE:=GPLv3 PKG_VERSION:=0.4.7 -PKG_RELEASE:=3 +PKG_RELEASE:=4 include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/hw.js b/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/hw.js index 9391a6ed..7b50ba93 100644 --- a/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/hw.js +++ b/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/hw.js @@ -9,158 +9,162 @@ 'require dom'; 'require tools.widgets as widgets'; - /* - Copyright Konstantine Shevlakov 2023 - + Copyright Konstantine Shevlakov 2023-2026 + Licensed to the GNU General Public License v3.0. - + + Refactored: bug fixes, XSS prevention, deduplication. */ +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Safe getElementById. + * @param {string} id + * @returns {HTMLElement|null} + */ +function getEl(id) { + return document.getElementById(id); +} + +/** + * Write a value into a cell by id. + * Falls back to '--' when value is missing/placeholder. + * Uses textContent to prevent XSS. + * + * @param {string} id - Element id + * @param {string} value - Raw value from modem JSON + * @param {string} [suffix] - Optional suffix appended to a real value (e.g. ' °C') + */ +function setCell(id, value, suffix) { + const el = getEl(id); + if (!el) return; + const missing = !value || value === '--'; + el.textContent = missing ? '--' : value + (suffix || ''); +} + +// ─── Field definitions ──────────────────────────────────────────────────────── +// Each entry: { key: modem JSON field, suffix: optional unit string } +const MODEM_FIELDS = [ + { key: 'device', suffix: '' }, + { key: 'firmware', suffix: '' }, + { key: 'imsi', suffix: '' }, + { key: 'iccid', suffix: '' }, + { key: 'imei', suffix: '' }, + { key: 'chiptemp', suffix: ' °C' }, +]; + +// ─── Main view ──────────────────────────────────────────────────────────────── return view.extend({ - load: function(data) { - return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo')); + // FIX: removed unused `data` parameter + load: function() { + return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}'); }, polldata: poll.add(function() { - return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo')).then(function(res) { - var json = JSON.parse(res); - if (!json || !json.modem || !Array.isArray(json.modem)) return; - for (var i = 0; i < json.modem.length; i++) { - if (document.getElementById('device'+i)) { - var view = document.getElementById('device'+i); - if (json.modem[i].device == '--') { - view = document.getElementById('--'); - } else { - view.innerHTML = String.format(json.modem[i].device); - } - } - if (document.getElementById('firmware'+i)) { - var view = document.getElementById('firmware'+i); - if (json.modem[i].firmware == '--') { - view = document.getElementById('--'); - } else { - view.innerHTML = String.format(json.modem[i].firmware); - } - } - if (document.getElementById('imsi'+i)) { - var view = document.getElementById('imsi'+i); - if (json.modem[i].imsi == '--') { - view = document.getElementById('--'); - } else { - view.innerHTML = String.format(json.modem[i].imsi); - } - } - if (document.getElementById('iccid'+i)) { - var view = document.getElementById('iccid'+i); - if (json.modem[i].iccid == '--') { - view = document.getElementById('--'); - } else { - view.innerHTML = String.format(json.modem[i].iccid); - } + return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}') + .then(function(res) { + // FIX: wrapped in try/catch — was crashing on invalid JSON + let json; + try { + json = JSON.parse(res); + } catch (e) { + console.error('modeminfo hw: JSON parse error', e); + return; } - if (document.getElementById('imei'+i)) { - var view = document.getElementById('imei'+i); - if (json.modem[i].imei == '--') { - view = document.getElementById('--'); - } else { - view.innerHTML = String.format(json.modem[i].imei); + if (!json || !Array.isArray(json.modem)) return; + + for (let i = 0; i < json.modem.length; i++) { + const modem = json.modem[i]; + + // FIX: replaced 6 copy-pasted blocks with a single loop. + // FIX: `var view` renamed — was shadowing the LuCI `view` module. + // FIX: value '--' now explicitly written to cell (was silently ignored). + // FIX: innerHTML → textContent to prevent XSS. + // FIX: String.format(x) with no extra args replaced by direct assignment. + for (const { key, suffix } of MODEM_FIELDS) { + setCell(key + i, modem[key], suffix); } } - - if (document.getElementById('chiptemp'+i)) { - var view = document.getElementById('chiptemp'+i); - if (json.modem[i].chiptemp == '--') { - view = document.getElementById('--'); - } else { - view.innerHTML = String.format(json.modem[i].chiptemp+' °C'); - } - } - }; - - }); + }); }), - render: function(data){ - - var m, s, o; - m = new form.Map('modeminfo', _('Modeminfo: Hardware'), _('Hardware and sim-card info.')); - s = m.section(form.TypedSection, 'general', null); - var json = JSON.parse(data); - // for future use - /* - var vendors = []; - var duplicate_modem_index = 1; - json.modem.forEach(obj => { - const device = obj.device; - if (!vendors.includes(device)) { - vendors.push(device); - } else { - vendors.push(device+" ("+duplicate_modem_index+")"); - duplicate_modem_index++; - } - }); - */ - for (var i = 0; i < json.modem.length; i++) { - let device = 'device'+i; - let firmware = 'firmware'+i; - let imsi = 'imsi'+i; - let iccid = 'iccid'+i; - let imei = 'imei'+i; - let chiptemp = 'chiptemp'+i; - let m = i+1; - if ( json.modem.length > 1 ) { - s.tab('modem'+i, _('Modem')+' '+m); - o = s.taboption('modem'+i, form.HiddenValue, 'generic'); + render: function(data) { + // FIX: wrapped in try/catch — was crashing on invalid JSON + let json; + try { + json = JSON.parse(data); + } catch (e) { + json = { modem: [] }; + } + + const m = new form.Map('modeminfo', _('Modeminfo: Hardware'), _('Hardware and sim-card info.')); + const s = m.section(form.TypedSection, 'general', null); + s.anonymous = true; + + for (let i = 0; i < json.modem.length; i++) { + const idx = i + 1; + + // Pre-build all element ids for this modem slot + const ids = {}; + for (const { key } of MODEM_FIELDS) { + ids[key] = key + i; + } + + let o; + if (json.modem.length > 1) { + s.tab('modem' + i, _('Modem') + ' ' + idx); + o = s.taboption('modem' + i, form.HiddenValue, 'generic'); } else { o = s.option(form.HiddenValue, 'generic'); } - o.render = L.bind(function(data){ - return ( - E('div', {}, [ - E('h3', { 'class': 'data-tab' }), - E('div', { 'class': 'cbi-section', 'data-title': 'modem'+i }, [ - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('Device')]), - E('td', { 'class': 'td left', 'id': device }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('Firmware')]), - E('td', { 'class': 'td left', 'id': firmware }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('IMSI')]), - E('td', { 'class': 'td left', 'id': imsi }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('ICCID')]), - E('td', { 'class': 'td left', 'id': iccid }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('IMEI')]), - E('td', { 'class': 'td left', 'id': imei }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('Chiptemp')]), - E('td', { 'class': 'td left', 'id': chiptemp }, [ '--' ]), - ]) - ]) - ]), - ] - ) - )}, o, this.polldata); - o.anonymous = true; - o.rmempty = true; - }; - s.anonymous = true; - return m.render(); + o.render = L.bind(function() { + return E('div', {}, [ + E('h3', { 'class': 'data-tab' }), + E('div', { 'class': 'cbi-section' }, [ + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('Device')]), + E('td', { 'class': 'td left', 'id': ids.device }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('Firmware')]), + E('td', { 'class': 'td left', 'id': ids.firmware }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('IMSI')]), + E('td', { 'class': 'td left', 'id': ids.imsi }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('ICCID')]), + E('td', { 'class': 'td left', 'id': ids.iccid }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('IMEI')]), + E('td', { 'class': 'td left', 'id': ids.imei }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('Chiptemp')]), + E('td', { 'class': 'td left', 'id': ids.chiptemp }, ['--']), + ]), + ]) + ]) + ]); + }, this.polldata); + + o.anonymous = true; + o.rmempty = true; + } + + return m.render(); }, + handleSaveApply: null, - handleSave: null, - handleReset: null + handleSave: null, + handleReset: null, }); diff --git a/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/main.js b/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/main.js index 1f9314c7..75856f58 100644 --- a/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/main.js +++ b/luci-app-modeminfo/htdocs/luci-static/resources/view/modem/main.js @@ -9,464 +9,543 @@ 'require dom'; 'require tools.widgets as widgets'; - /* Copyright Konstantine Shevlakov 2023 - + Licensed to the GNU General Public License v3.0. - + + Refactored: bug fixes, performance improvements, code deduplication. */ const REGISTERED_STATUSES = [1, 6, 9]; -const ROAMING_STATUSES = [3, 5, 7, 10]; +const ROAMING_STATUSES = [3, 5, 7, 10]; +// ─── Signal Icons ──────────────────────────────────────────────────────────── +const SIGNAL_ICONS = [ + { max: 10, icn: 'signal-000-000.svg' }, + { max: 25, icn: 'signal-000-025.svg' }, + { max: 50, icn: 'signal-025-050.svg' }, + { max: 75, icn: 'signal-050-075.svg' }, + { max: Infinity, icn: 'signal-075-100.svg' }, +]; + +// ─── Registration Status Labels ─────────────────────────────────────────────── +const REG_STATUSES = new Map([ + [0, _('No Registration')], + [2, _('Searching')], [8, _('Searching')], + [3, _('Denied')], + [4, _('Unknown')], + [5, _('Roaming')], [7, _('Roaming')], [10, _('Roaming')], +]); + +// ─── Progress bar configuration ─────────────────────────────────────────────── +// calc(vn, mn): vn = clamped signal value, mn = min boundary +// All formulas normalise the value to 0–100% range for the progress bar. +const PROGRESS_CONFIG = { + rssi: { + selector: '#rssi', + min: -110, max: -50, + // Linear map from [-110..-50] → [0..100] + calc: (vn, mn) => Math.floor(100 * (1 - (-50 - vn) / (-50 - mn))), + }, + rsrp: { + selector: '#rsrp', + min: -140, max: -50, + // Slightly expanded scale (×1.2) to better use bar width + calc: (vn, mn) => Math.floor(120 * (1 - (-50 - vn) / (-70 - mn))), + }, + sinr: { + selector: '#sinr', + min: -20, max: 30, + // Linear map from [-20..30] → [0..100] + calc: (vn, mn) => Math.floor(100 - (100 * (1 - ((mn - vn) / (mn - 30))))), + }, + rsrq: { + selector: '#rsrq', + min: -20, max: 0, + // Proportional map from [-20..0] → [0..100] + calc: (vn, mn) => Math.floor(115 - (100 / mn) * vn), + }, + ecio: { + selector: '#sinr', + min: -24, max: 0, + // Same shape as rsrq but for EC/IO range [-24..0] + calc: (vn, mn) => Math.floor(100 - (100 / mn) * vn), + }, +}; + +// ─── LTE EARFCN → band/frequency table ─────────────────────────────────────── +const LTE_BANDS = [ + { min: 0, max: 599, frdl: 2110, frul: 1920, offset: 0, band: '1' }, + { min: 600, max: 1199, frdl: 1930, frul: 1850, offset: 600, band: '2' }, + { min: 1200, max: 1949, frdl: 1805, frul: 1710, offset: 1200, band: '3' }, + { min: 1950, max: 2399, frdl: 2110, frul: 1710, offset: 1950, band: '4' }, + { min: 2400, max: 2469, frdl: 869, frul: 824, offset: 2400, band: '5' }, + { min: 2750, max: 3449, frdl: 2620, frul: 2500, offset: 2750, band: '7' }, + { min: 3450, max: 3799, frdl: 925, frul: 880, offset: 3450, band: '8' }, + { min: 6150, max: 6449, frdl: 791, frul: 832, offset: 6150, band: '20' }, + { min: 9210, max: 9659, frdl: 758, frul: 703, offset: 9210, band: '28' }, + { min: 9870, max: 9919, frdl: 452.5, frul: 462.5, offset: 9870, band: '31' }, + { min: 37750, max: 38249, frdl: 2570, frul: 2570, offset: 37750, band: '38' }, + { min: 38650, max: 39649, frdl: 2300, frul: 2300, offset: 38650, band: '40' }, + { min: 39650, max: 41589, frdl: 2496, frul: 2496, offset: 39650, band: '41' }, +]; + +// ─── Non-LTE (UMTS/GSM) ARFCN → band/frequency table ───────────────────────── +const NON_LTE_BANDS = [ + { + condition: rfcn => rfcn >= 10562 && rfcn <= 10838, + calc: rfcn => ({ offset: 950, dlfreq: rfcn / 5, ulfreq: (rfcn - 950) / 5, band: 'IMT2100' }), + }, + { + condition: rfcn => rfcn >= 2937 && rfcn <= 3088, + calc: rfcn => ({ frul: 925, ulfreq: 340 + (rfcn / 5), dlfreq: (340 + (rfcn / 5)) - 45, band: 'UMTS900' }), + }, + { + condition: rfcn => rfcn >= 955 && rfcn <= 1023, + calc: rfcn => ({ frul: 890, ulfreq: 890 + ((rfcn - 1024) / 5), dlfreq: (890 + ((rfcn - 1024) / 5)) + 45, band: 'DSC900' }), + }, + { + condition: rfcn => rfcn >= 512 && rfcn <= 885, + calc: rfcn => ({ frul: 1710, ulfreq: 1710 + ((rfcn - 512) / 5), dlfreq: (1710 + ((rfcn - 512) / 5)) + 95, band: 'DCS1800' }), + }, + { + condition: rfcn => rfcn >= 1 && rfcn <= 124, + calc: rfcn => ({ frul: 890, ulfreq: 890 + (rfcn / 5), dlfreq: (890 + (rfcn / 5)) + 45, band: 'GSM900' }), + }, +]; + +const UMTS_MODES = /(HS|3G|UMTS|WCDMA)/i; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Safe getElementById wrapper. + * @param {string} id + * @returns {HTMLElement|null} + */ +function getEl(id) { + return document.getElementById(id); +} + +/** + * Show or hide the grandparent row of a signal element. + * @param {HTMLElement} el + * @param {boolean} show + */ +function setRowVisible(el, show) { + const row = el && el.parentElement && el.parentElement.parentElement; + if (row) row.style.display = show ? '' : 'none'; +} + +/** + * Update a cbi-progressbar element. + * @param {string} type - Key in PROGRESS_CONFIG + * @param {string} value - Raw signal value string (e.g. "-85 dBm") + * @param {number} max - Boundary value (passed as second calc argument) + * @param {number} idx - Modem index suffix for the element id + */ +function updateProgressBar(type, value, max, idx) { + const config = PROGRESS_CONFIG[type]; + if (!config) return; + + const pg = document.querySelector(`${config.selector}${idx}`); + if (!pg) return; + + const vn = Math.max(config.min, Math.min(config.max, parseInt(value) || 0)); + const mn = parseInt(max) || 100; + const pc = Math.min(100, Math.max(0, config.calc(vn, mn))); + + pg.firstElementChild.style.width = `${pc}%`; + pg.firstElementChild.style.animationDirection = 'reverse'; + pg.setAttribute('title', String(value)); +} + +/** + * Format distance string, returns empty string when unavailable. + * @param {string|number} dist + * @returns {string} + */ +function formatDistance(dist) { + if (!dist || dist === '--' || dist === '' || dist === '0.00') return ''; + return ' ~' + dist + ' km'; +} + +/** + * Build the operator status badge HTML safely using LuCI E() helper. + * @param {Object} modem - Single modem entry from JSON + * @param {string} icon - URL to signal icon + * @param {string} reg - Human-readable registration status + * @returns {string} - Safe innerHTML string via dom.content + */ +function formatModemStatus(modem, icon, reg) { + const rg = parseInt(modem.reg) || 0; + const p = modem.csq_per || 0; + const cops = modem.cops || '--'; + const color = modem.csq_col || '#000000'; + const distanceText = formatDistance(modem.distance); + + const iconEl = icon ? E('img', { 'class': 'modem-signal-icon', 'src': icon }) : null; + const boldEl = E('b', { 'style': `color:${color}` }, [`${p}%`]); + + const children = iconEl + ? [cops + ' ', iconEl, ' ', boldEl, distanceText] + : [cops + ' ', boldEl, distanceText]; + + if (REGISTERED_STATUSES.includes(rg)) { + return E('span', { 'class': 'ifacebadge' }, children).outerHTML; + } else if (ROAMING_STATUSES.includes(rg)) { + return E('span', { 'class': 'ifacebadge' }, [`${cops} (${reg}) `, ...(iconEl ? [iconEl, ' '] : []), boldEl, distanceText]).outerHTML; + } else { + return E('span', { 'class': 'ifacebadge' }, [reg || '--']).outerHTML; + } +} + +/** + * Derive signal icon URL from signal percentage. + * @param {number} pct + * @returns {string} + */ +function resolveSignalIcon(pct) { + const { icn } = SIGNAL_ICONS.find(({ max }) => pct <= max) || SIGNAL_ICONS[SIGNAL_ICONS.length - 1]; + return L.resource(`view/modem/icons/${icn}`); +} + +/** + * Calculate DL/UL frequencies and band name for LTE mode. + * @param {number} rfcn + * @returns {{ dlfreq: number, ulfreq: number, band: string, frdl: number, frul: number, offset: number }} + */ +function calcLteBand(rfcn) { + const b = LTE_BANDS.find(b => rfcn >= b.min && rfcn <= b.max); + if (!b) return { frdl: 0, frul: 0, offset: 0, band: String(rfcn), dlfreq: 0, ulfreq: 0 }; + const dlfreq = b.frdl + (rfcn - b.offset) / 10; + const ulfreq = b.frul + (rfcn - b.offset) / 10; + return { ...b, dlfreq, ulfreq }; +} + +/** + * Calculate DL/UL frequencies and band name for non-LTE modes. + * @param {number} rfcn + * @returns {{ dlfreq: number, ulfreq: number, band: string }} + */ +function calcNonLteBand(rfcn) { + const match = NON_LTE_BANDS.find(b => b.condition(rfcn)); + return match ? match.calc(rfcn) : { ulfreq: 0, dlfreq: 0, band: String(rfcn) }; +} + +/** + * Build a LAC/CID/eNB/Cell/PCI label and value string. + * @param {Object} modem - Single modem entry + * @returns {{ namecid: string, lactac: string }} + */ +function buildCellId(modem) { + const { enbid, cell, pci, lac, cid } = modem; + const parts = [lac, cid]; + let namecid = 'LAC/CID'; + + if (enbid) { + parts.push(enbid); + namecid += '/eNB ID'; + + if (cell) { + parts.push(cell); + namecid += '/Cell'; + + if (pci) { + parts.push(pci); + namecid += '/PCI'; + } + } + } + + const lactac = parts.join(' / '); + return { namecid, lactac }; +} + +/** + * Derive mode-specific labels and carrier aggregation info. + * @param {Object} modem - Single modem entry + * @param {string} netmode + * @param {string} band + * @param {number} bw + * @returns {{ carrier, bcc, bca, bwDisplay, namech, namesnr, namecid, lactac, namebnd }} + */ +function buildModeInfo(modem, netmode, band, bw) { + let carrier = ''; + let bcc, bca, bwDisplay, namech, namesnr, namecid, lactac; + + if (netmode === 'LTE') { + const calte = modem.lteca; + namech = 'EARFCN'; + namesnr = 'SINR'; + + if (calte > 0) { + carrier = '+'; + bwDisplay = modem.bwca; + bcc = ` B${band}${modem.scc}`; + bca = bwDisplay ? ` / ${bwDisplay} MHz` : ''; + } else { + bwDisplay = bw; + bcc = ` B${band}`; + bca = bw ? ` / ${bw} MHz` : ''; + } + + const cellInfo = buildCellId(modem); + namecid = cellInfo.namecid; + lactac = cellInfo.lactac; + + } else if (UMTS_MODES.test(netmode)) { + namech = 'UARFCN'; + namesnr = 'ECIO'; + namecid = 'LAC/CID'; + lactac = `${modem.lac} / ${modem.cid}`; + bcc = ` ${band}`; + + } else { + namech = 'ARFCN'; + namesnr = 'SINR/ECIO'; + namecid = 'LAC/CID'; + lactac = `${modem.lac} / ${modem.cid}`; + bcc = ` ${band}`; + } + + const namebnd = bw + ? _('Network/Band/Bandwidth') + : _('Network/Band'); + + return { carrier, bcc, bca: bca || '', bwDisplay, namech, namesnr, namecid, lactac, namebnd }; +} + +/** + * Set textContent of element by id (if it exists). + * @param {string} id + * @param {string} text + */ +function setText(id, text) { + const el = getEl(id); + if (el) el.textContent = text; +} + +/** + * Set innerHTML of element by id (if it exists). + * @param {string} id + * @param {string} html + */ +function setHtml(id, html) { + const el = getEl(id); + if (el) el.innerHTML = html; +} + +/** + * Update a signal progress bar, hiding its parent row if value is missing. + * @param {string} elId - Element id (without modem index) + * @param {number} idx - Modem index + * @param {*} rawVal - Raw value from modem JSON + * @param {string} unit - Unit suffix, e.g. ' dBm' + * @param {string} type - Key in PROGRESS_CONFIG + * @param {number} boundary + */ +function updateSignalBar(elId, idx, rawVal, unit, type, boundary) { + const el = getEl(elId + idx); + if (!el) return; + const missing = !rawVal || rawVal === '--' || rawVal === ''; + setRowVisible(el, !missing); + if (!missing) { + updateProgressBar(type, rawVal + unit, boundary, idx); + } +} + +// ─── Main view ──────────────────────────────────────────────────────────────── return view.extend({ - load: function(data) { + load: function() { return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}'); }, polldata: poll.add(function() { - return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}').then(function(res) { - var json = JSON.parse(res); - if (!json || !json.modem || !Array.isArray(json.modem)) return; - for (var i = 0; i < json.modem.length; i++) { - // progressbar cellular metric - const progressConfig = { - rssi: { - selector: '#rssi', min: -110, max: -50, - calc: (vn, mn) => Math.floor(100 * (1 - (-50 - vn) / (-50 - mn))) - }, - rsrp: { - selector: '#rsrp', min: -140, max: -50, - calc: (vn, mn) => Math.floor(120 * (1 - (-50 - vn) / (-70 - mn))) - }, - sinr: { - selector: '#sinr', min: -20, max: 30, - calc: (vn, mn) => Math.floor(100 - (100 * (1 - ((mn - vn) / (mn - 30))))) - }, - rsrq: { - selector: '#rsrq', min: -20, max: 0, - calc: (vn, mn) => Math.floor(115 - (100 / mn) * vn) - }, - ecio: { - selector: '#sinr', min: -24, max: 0, - calc: (vn, mn) => Math.floor(100 - (100 / mn) * vn) - } - }; - - - function updateProgressBar(type, value, max, i) { - const config = progressConfig[type]; - if (!config) return; - - const pg = document.querySelector(`${config.selector}${i}`); - if (!pg) return; - - const vn = Math.max(config.min, Math.min(config.max, parseInt(value) || 0)); - const mn = parseInt(max) || 100; - const pc = config.calc(vn, mn); - - pg.firstElementChild.style.width = `${pc}%`; - pg.style.width = '%d%%'; - pg.firstElementChild.style.animationDirection = "reverse"; - pg.setAttribute('title', '%s'.format(value)); + return L.resolveDefault(fs.exec_direct('/usr/bin/modeminfo'), '{"modem": []}') + .then(function(res) { + let json; + try { + json = JSON.parse(res); + } catch (e) { + console.error('modeminfo: JSON parse error', e); + return; } - function formatDistance(dist) { - if (!dist || dist === "--" || dist === "" || dist === "0.00") { - return ''; - } - return ' ~' + dist + ' km'; - } + if (!json || !Array.isArray(json.modem)) return; - function formatModemStatus(modem, icon, reg) { - var rg = parseInt(modem.reg) || 0; - var p = modem.csq_per || 0; - var cops = modem.cops || '--'; - var color = modem.csq_col || '#000000'; - - var distanceHtml = formatDistance(modem.distance); - var iconHtml = icon ? '' : ''; - - var percentWithColor = p + '%'; - if (REGISTERED_STATUSES.includes(rg)) { - return '' + cops + ' ' + iconHtml + - ' ' + percentWithColor + '' + - distanceHtml + ''; - } else if (ROAMING_STATUSES.includes(rg)) { - return '' + cops + ' (' + reg + ') ' + iconHtml + - ' ' + percentWithColor + '' + - distanceHtml + ''; + for (let i = 0; i < json.modem.length; i++) { + const modem = json.modem[i]; + const netmode = modem.mode || ''; + const rfcn = modem.arfcn || 0; + + // ── Band / Frequency ──────────────────────────────────── + let dlfreq, ulfreq, band, bw; + + if (netmode === 'LTE') { + ({ dlfreq, ulfreq, band } = calcLteBand(rfcn)); + const bandwidths = [1.4, 3, 5, 10, 15, 20]; + bw = bandwidths[modem.bwdl] || ''; } else { - return '' + (reg || '--') + ''; - } - } - - // icon signal strength - var icn; - - var signalIcons = [ - { max: -1, icn: 'signal-000-000.svg' }, - { max: 0, icn: 'signal-000-000.svg' }, - { max: 10, icn: 'signal-000-000.svg' }, - { max: 25, icn: 'signal-000-025.svg' }, - { max: 50, icn: 'signal-025-050.svg' }, - { max: 75, icn: 'signal-050-075.svg' }, - { max: Infinity, icn: 'signal-075-100.svg' } - ]; - - var p = json.modem[i].csq_per || 0; - - var { icn } = signalIcons.find(({ max }) => p <= max); - var icon = L.resource(`view/modem/icons/${icn}`); - - // Registration Status - var regStatuses = new Map([ - [0, _('No Registration')], - [2, _('Searching')], [8, _('Searching')], - [3, _('Denied')], - [4, _('Unknown')], - [5, _('Roaming')], [7, _('Roaming')], [10, _('Roaming')] - ]); - - var rg = Number(json.modem[i].reg); - var reg = regStatuses.get(rg) || _('No Data'); - - // frequency band calculator - var offset, band, bw, frdl, frul; - var netmode = (json.modem[i].mode) - var rfcn = (json.modem[i].arfcn) - if (netmode === "LTE") { - // list LTE-bans - var lteBands = [ - { min: 0, max: 599, frdl: 2110, frul: 1920, offset: 0, band: "1" }, - { min: 600, max: 1199, frdl: 1930, frul: 1850, offset: 600, band: "2" }, - { min: 1200, max: 1949, frdl: 1805, frul: 1710, offset: 1200, band: "3" }, - { min: 1950, max: 2399, frdl: 2110, frul: 1710, offset: 1950, band: "4" }, - { min: 2400, max: 2469, frdl: 869, frul: 824, offset: 2400, band: "5" }, - { min: 2750, max: 3449, frdl: 2620, frul: 2500, offset: 2750, band: "7" }, - { min: 3450, max: 3799, frdl: 925, frul: 880, offset: 3450, band: "8" }, - { min: 6150, max: 6449, frdl: 791, frul: 832, offset: 6150, band: "20" }, - { min: 9210, max: 9659, frdl: 758, frul: 703, offset: 9210, band: "28" }, - { min: 9870, max: 9919, frdl: 452.5, frul: 462.5, offset: 9870, band: "31" }, - { min: 37750, max: 38249, frdl: 2570, frul: 2570, offset: 37750, band: "38" }, - { min: 38650, max: 39649, frdl: 2300, frul: 2300, offset: 38650, band: "40" }, - { min: 39650, max: 41589, frdl: 2496, frul: 2496, offset: 39650, band: "41" } - ]; - - var bandConfig = lteBands.find(b => rfcn >= b.min && rfcn <= b.max) || { - frdl: 0, frul: 0, offset: 0, band: rfcn - }; - ({ frdl, frul, offset, band } = bandConfig); - - // Bandwidth channel (bw) - var bandwidths = [1.4, 3, 5, 10, 15, 20]; - var bw = bandwidths[json.modem[i].bwdl] || ""; - - // Calc frequency - var dlfreq = frdl + (rfcn - offset) / 10; - var ulfreq = frul + (rfcn - offset) / 10; - } else { - var nonLteBands = [ - { - condition: (rfcn) => rfcn >= 10562 && rfcn <= 10838, - calc: (rfcn) => ({ offset: 950, dlfreq: rfcn / 5, ulfreq: (rfcn - 950) / 5, band: "IMT2100" }) - }, - { - condition: (rfcn) => rfcn >= 2937 && rfcn <= 3088, - calc: (rfcn) => ({ frul: 925, offset: 340, ulfreq: 340 + (rfcn / 5), dlfreq: (340 + (rfcn / 5)) - 45, band: "UMTS900" }) - }, - { - condition: (rfcn) => rfcn >= 955 && rfcn <= 1023, - calc: (rfcn) => ({ frul: 890, ulfreq: 890 + ((rfcn - 1024) / 5), dlfreq: (890 + ((rfcn - 1024) / 5)) + 45, band: "DSC900" }) - }, - { - condition: (rfcn) => rfcn >= 512 && rfcn <= 885, - calc: (rfcn) => ({ frul: 1710, ulfreq: 1710 + ((rfcn - 512) / 5), dlfreq: (1710 + ((rfcn - 512) / 5)) + 95, band: "DCS1800" }) - }, - { - condition: (rfcn) => rfcn >= 1 && rfcn <= 124, - calc: (rfcn) => ({ frul: 890, ulfreq: 890 + (rfcn / 5), dlfreq: (890 + (rfcn / 5)) + 45, band: "GSM900" }) - } - ]; - - var bandConfig = nonLteBands.find(b => b.condition(rfcn))?.calc(rfcn) || { ulfreq: 0, dlfreq: 0, band: String(rfcn) }; - - // CALC BANDS - ({ frul, offset, ulfreq, dlfreq, band } = bandConfig); - - } - - var carrier = ""; - var bcc, freq, distance, calte, namebnd; - var dist = (json.modem[i].distance) - - var { enbid, cell, pci, lac, cid } = json.modem[i]; - - const parts = [lac, cid]; - var namecid = "LAC/CID"; - - if (enbid) { - parts.push(enbid); - namecid += "/eNB ID"; - - if (cell) { - parts.push(`/${cell}`); - namecid += "/Cell"; - - if (pci) { - parts.push(`/${pci}`); - namecid += "/PCI"; - } - } + ({ dlfreq, ulfreq, band } = calcNonLteBand(rfcn)); + bw = ''; } - var lactac = parts.join(' / ').replace(' ', ' ').replace('/ /', '/ ').replace('/ /', '/ ') + const arfcnStr = `${rfcn} (${dlfreq} / ${ulfreq} MHz)`; - var UMTS_MODES = /(HS|3G|UMTS|WCDMA)/i; + // ── Registration ──────────────────────────────────────── + const rg = Number(modem.reg); + const reg = REG_STATUSES.get(rg) || _('No Data'); - var bcc, scc, cid; - var bca = ""; - var arfcn = json.modem[i].arfcn + " (" + dlfreq + " / " + ulfreq + " MHz)"; - // name channels and signal/noise - if (netmode == "LTE") { - var calte = (json.modem[i].lteca) - var bwca = json.modem[i].bwca; - distance = " ~"+ dist +" km"; - if (calte > 0) { - carrier = "+"; - scc = json.modem[i].scc; - bw = bwca; - bca = " / " + bw + " MHz"; - bcc = " B" + band + "" + scc; + // ── Signal icon ───────────────────────────────────────── + const icon = resolveSignalIcon(modem.csq_per || 0); + + // ── Mode-specific labels ──────────────────────────────── + const { carrier, bcc, bca, namech, namesnr, namecid, lactac, namebnd } = + buildModeInfo(modem, netmode, band, bw); + + // ── DOM updates ───────────────────────────────────────── + setHtml('status' + i, formatModemStatus(modem, icon, reg)); + + const modeEl = getEl('mode' + i); + if (modeEl) { + // FIX: was `signal = 0` (assignment), must be `=== 0` + if (modem.signal === 0 || modem.signal === '' || !netmode) { + modeEl.textContent = '--'; } else { - scc = ""; - bcc = " B" + band; - if (bw) { - bca = " / " + bw + " MHz"; - } else{ - bca = "" - } + modeEl.textContent = `${netmode}${carrier} /${bcc}${bca}`; } - var namech = "EARFCN"; - var namesnr = "SINR"; - } else if (UMTS_MODES.test(netmode)) { - var namech = "UARFCN"; - var namesnr = "ECIO"; - var namecid = "LAC/CID"; - var lactac = json.modem[i].lac + " / " + json.modem[i].cid; - var bcc = " " + band; - } else { - var namech = "ARFCN"; - var namesnr = "SINR/ECIO"; - var namecid = "LAC/CID"; - var lactac = json.modem[i].lac + " / " + json.modem[i].cid; - var bcc = " " + band; } - if (bw) { - namebnd = _('Network/Band/Bandwidth'); - } else { - namebnd = _('Network/Band'); - } - // data by element + setText('namebnd' + i, namebnd); + setText('chname' + i, namech); + setText('namecid' + i, namecid); + setText('snrname' + i, namesnr); + setHtml('arfcn' + i, arfcnStr); + setHtml('lac' + i, lactac); - if (document.getElementById('status'+i)) { - var view = document.getElementById('status'+i); - view.innerHTML = formatModemStatus(json.modem[i], icon, reg); - } - - if (document.getElementById('mode'+i)){ - var view = document.getElementById('mode'+i); - if (json.modem[i].signal = 0 || json.modem[i].signal == '') { - view.textContent = '--'; - } else { - if (json.modem[i].mode == '') { - view.textContent = '--'; + // ── Progress bars ─────────────────────────────────────── + // RSSI always visible if element exists + if (getEl('rssi' + i)) { + if (!modem.rssi || modem.rssi === '') { + setText('rssi' + i, '--'); } else { - view.innerHTML = String.format(json.modem[i].mode + "" + carrier + " /"+ bcc +""+ bca); + updateProgressBar('rssi', modem.rssi + ' dBm', -110, i); } } - } - if (document.getElementById('namebnd'+i)){ - var view = document.getElementById('namebnd'+i); - view.innerHTML = String.format(namebnd); - } + // SINR / ECIO (shared element, hidden when unavailable) + updateSignalBar( + 'sinr', i, modem.sinr, ' dB', + netmode === 'LTE' ? 'sinr' : 'ecio', + netmode === 'LTE' ? -20 : -24 + ); - if (document.getElementById('chname'+i)){ - var view = document.getElementById('chname'+i); - view.innerHTML = String.format(namech); - } + // RSRP (hidden when unavailable) + updateSignalBar('rsrp', i, modem.rsrp, ' dBm', 'rsrp', -140); - if (document.getElementById('namecid'+i)){ - var view = document.getElementById('namecid'+i); - view.innerHTML = String.format(namecid); + // RSRQ (hidden when unavailable) + updateSignalBar('rsrq', i, modem.rsrq, ' dB', 'rsrq', -20); } - - if (document.getElementById('arfcn'+i)){ - var view = document.getElementById('arfcn'+i); - view.innerHTML = String.format(arfcn); - } - - if (document.getElementById('lac'+i)){ - var view = document.getElementById('lac'+i); - view.innerHTML = String.format(lactac); - } - - if (document.getElementById('snrname'+i)) { - var view = document.getElementById('snrname'+i); - view.innerHTML = String.format(namesnr); - } - - if (document.getElementById('rssi'+i)) { - var view = document.getElementById('rssi'+i); - if (json.modem[i].rssi == '') { - view.textContent = '--'; - } else { - updateProgressBar('rssi', json.modem[i].rssi + ' dBm', -110, i); - } - } - - - if (document.getElementById('sinr'+i)) { - var sinrElement = document.getElementById('sinr'+i); - var sinrParent = sinrElement ? sinrElement.parentElement : null; - - if (!json.modem[i].sinr || json.modem[i].sinr === "--" || json.modem[i].sinr === "") { - if (sinrParent && sinrParent.parentElement) { - sinrParent.parentElement.style.display = 'none'; - } - } else { - if (sinrParent && sinrParent.parentElement) { - sinrParent.parentElement.style.display = ''; - } - if (netmode == "LTE") { - updateProgressBar('sinr', json.modem[i].sinr + ' dB', -20, i); - } else { - updateProgressBar('ecio', json.modem[i].sinr + ' dB', -24, i); - } - } - } - - if (document.getElementById('rsrp'+i)) { - var rsrpElement = document.getElementById('rsrp'+i); - var rsrpParent = rsrpElement ? rsrpElement.parentElement : null; - - if (!json.modem[i].rsrp || json.modem[i].rsrp === "--" || json.modem[i].rsrp === "") { - if (rsrpParent && rsrpParent.parentElement) { - rsrpParent.parentElement.style.display = 'none'; - } - } else { - if (rsrpParent && rsrpParent.parentElement) { - rsrpParent.parentElement.style.display = ''; - } - updateProgressBar('rsrp', json.modem[i].rsrp + ' dBm', -140, i); - } - } - - if (document.getElementById('rsrq'+i)) { - var rsrqElement = document.getElementById('rsrq'+i); - var rsrqParent = rsrqElement ? rsrqElement.parentElement : null; - - if (!json.modem[i].rsrq || json.modem[i].rsrq === "--" || json.modem[i].rsrq === "") { - if (rsrqParent && rsrqParent.parentElement) { - rsrqParent.parentElement.style.display = 'none'; - } - } else { - if (rsrqParent && rsrqParent.parentElement) { - rsrqParent.parentElement.style.display = ''; - } - updateProgressBar('rsrq', json.modem[i].rsrq + ' dB', -20, i); - } - } - }; - - }); + }); }), - render: function(data){ - var m, s, o; + render: function(data) { + let json; + try { + json = JSON.parse(data); + } catch (e) { + json = { modem: [] }; + } - m = new form.Map('modeminfo', _('Modeminfo: Network'), _('Cellular network')); - s = m.section(form.TypedSection, 'general', null); - var json = JSON.parse(data); - - for (var i = 0; i < json.modem.length; i++) { - let status = 'status'+i; - let mode = 'mode'+i; - let namebnd = 'namebnd'+i; - let chname = 'chname'+i; - let namecid = 'namecid'+i; - let arfcn = 'arfcn'+i; - let lac = 'lac'+i; - let rssi = 'rssi'+i; - let sinr = 'sinr'+i; - let snrname = 'snrname'+i; - let rsrp = 'rsrp'+i; - let rsrq = 'rsrq'+i; - let m = i+1; - - if ( json.modem.length > 1 ) { - s.tab("modem" + i, _('Modem')+' '+m); - o = s.taboption('modem'+i, form.HiddenValue, 'generic'); + const m = new form.Map('modeminfo', _('Modeminfo: Network'), _('Cellular network')); + const s = m.section(form.TypedSection, 'general', null); + s.anonymous = true; + + for (let i = 0; i < json.modem.length; i++) { + const idx = i + 1; + const statusId = 'status' + i; + const modeId = 'mode' + i; + const namebndId= 'namebnd'+ i; + const chnameId = 'chname' + i; + const namecidId= 'namecid'+ i; + const arfcnId = 'arfcn' + i; + const lacId = 'lac' + i; + const rssiId = 'rssi' + i; + const sinrId = 'sinr' + i; + const snrnameId= 'snrname'+ i; + const rsrpId = 'rsrp' + i; + const rsrqId = 'rsrq' + i; + + let o; + if (json.modem.length > 1) { + s.tab('modem' + i, _('Modem') + ' ' + idx); + o = s.taboption('modem' + i, form.HiddenValue, 'generic'); } else { o = s.option(form.HiddenValue, 'generic'); } - o.render = L.bind(function(data){ - return ( - E('div', {}, [ - E('h3', { 'class': 'data-tab' }), - E('div', { 'class': 'cbi-section', 'data-title': 'modem'+i }, [ - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('Operator')]), - E('td', { 'class': 'td left', 'id': status }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%', 'id': namebnd }, [ _('Network/band')]), - E('td', { 'class': 'td left', 'id': mode }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%', 'id': chname }, [ _('E/U/ARFCN')]), - E('td', { 'class': 'td left', 'id': arfcn }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%', 'id': namecid }, [ _('LAC/CID')]), - E('td', { 'class': 'td left', 'id': lac }, [ '--' ]), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('RSSI')]), - E('td', { 'class': 'td left'}, E('div', {'id': rssi, 'class': 'cbi-progressbar', 'title': '--' }, E('div'))), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%', 'id': snrname }, [ _('SINR/EcIO')]), - E('td', { 'class': 'td left'}, E('div', {'id': sinr, 'class': 'cbi-progressbar', 'title': '--'}, E('div'))), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('RSRP')]), - E('td', { 'class': 'td left'}, E('div', {'id': rsrp, 'class': 'cbi-progressbar', 'title': '--'}, E('div'))), - ]), - E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ - E('td', { 'class': 'td left', 'width': '50%' }, [ _('RSRQ')]), - E('td', { 'class': 'td left'}, E('div', {'id': rsrq, 'class': 'cbi-progressbar', 'title': '--'}, E('div'))), - ]) - ]) + o.render = L.bind(function() { + return E('div', {}, [ + E('h3', { 'class': 'data-tab' }), + E('div', { 'class': 'cbi-section' }, [ + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('Operator')]), + E('td', { 'class': 'td left', 'id': statusId }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%', 'id': namebndId }, [_('Network/Band')]), + E('td', { 'class': 'td left', 'id': modeId }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%', 'id': chnameId }, [_('E/U/ARFCN')]), + E('td', { 'class': 'td left', 'id': arfcnId }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%', 'id': namecidId }, [_('LAC/CID')]), + E('td', { 'class': 'td left', 'id': lacId }, ['--']), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('RSSI')]), + E('td', { 'class': 'td left' }, + E('div', { 'id': rssiId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) + ), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%', 'id': snrnameId }, [_('SINR/EcIO')]), + E('td', { 'class': 'td left' }, + E('div', { 'id': sinrId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) + ), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-2' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('RSRP')]), + E('td', { 'class': 'td left' }, + E('div', { 'id': rsrpId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) + ), + ]), + E('tr', { 'class': 'tr cbi-rowstyle-1' }, [ + E('td', { 'class': 'td left', 'width': '50%' }, [_('RSRQ')]), + E('td', { 'class': 'td left' }, + E('div', { 'id': rsrqId, 'class': 'cbi-progressbar', 'title': '--' }, E('div')) + ), + ]), ]) ]) - ) + ]); }, this.polldata); - o.anonymous = true; - o.rmempty = true; - }; - s.anonymous = true; - return m.render(); + o.anonymous = true; + o.rmempty = true; + } + + return m.render(); }, + handleSaveApply: null, - handleSave: null, - handleReset: null + handleSave: null, + handleReset: null, }); diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index ea571836..c6410147 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.5.11 -PKG_RELEASE:=127 +PKG_RELEASE:=128 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 7fdb88b1..d908e37d 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 @@ -83,103 +83,10 @@ o.rmempty = false o = s:option(Value, "interface", translate("Source Interface")) o:value("", translate("All")) --- Populate with actual kernel network devices instead of UCI interface names, --- because the backend (nftables iifname / iptables -i) matches kernel device names. -do - local nfs = require "nixio.fs" - local _cursor = require("luci.model.uci").cursor() - local _sysnet = "/sys/class/net/" - - -- Map UCI interface names to their device names and vice versa - local _iface_to_dev = {} - local _dev_to_ifaces = {} - local _iface_proto = {} - _cursor:foreach("network", "interface", function(sec) - local name = sec[".name"] - if name ~= "loopback" then - _iface_proto[name] = sec.proto - if sec.device then - _iface_to_dev[name] = sec.device - _dev_to_ifaces[sec.device] = _dev_to_ifaces[sec.device] or {} - table.insert(_dev_to_ifaces[sec.device], name) - end - end - end) - - -- Classify device type using sysfs attributes - local function classify_sysfs(dev) - if nfs.stat(_sysnet .. dev .. "/bridge", "type") == "dir" then - return translate("Bridge") - elseif nfs.stat(_sysnet .. dev .. "/wireless", "type") == "dir" then - return translate("Wireless Adapter") - elseif dev:match("^tun") or dev:match("^tap") or dev:match("^wg") or dev:match("^ppp") then - return translate("Tunnel Interface") - else - return translate("Ethernet Adapter") - end - end - - -- Classify offline UCI interfaces by config hints - local function classify_uci(dev_name, proto) - if dev_name and dev_name:match("^br%-") then - return translate("Bridge") - elseif proto == "wireguard" or proto == "pppoe" or proto == "pptp" or proto == "l2tp" then - return translate("Tunnel Interface") - else - return translate("Interface") - end - end - - local _seen = {} - local _devices = {} - - -- Active kernel devices from /sys/class/net/. - -- Skip bridge member ports (/master) and DSA master devices (/dsa) because - -- nftables iifname matches the parent bridge for routed traffic, not - -- individual member ports. Also skip internal virtual devices. - local _iter = nfs.dir(_sysnet) - if _iter then - for dev in _iter do - if dev ~= "lo" - and not dev:match("^veth") - and not dev:match("^ifb") - and not dev:match("^gre") - and not dev:match("^sit") - and not dev:match("^ip6tnl") - and not dev:match("^erspan") - and not nfs.stat(_sysnet .. dev .. "/master", "type") - and not nfs.stat(_sysnet .. dev .. "/dsa", "type") - then - local dtype = classify_sysfs(dev) - local label = dtype .. ': "' .. dev .. '"' - if _dev_to_ifaces[dev] then - label = label .. " (" .. table.concat(_dev_to_ifaces[dev], ", ") .. ")" - end - _devices[#_devices + 1] = { name = dev, label = label, sort = dtype .. ":" .. dev } - _seen[dev] = true - end - end - end - - -- UCI interfaces whose device does not currently exist (down tunnels, VPNs, etc.). - -- Stored by UCI name since the kernel device is not available yet. - -- Dedup by device: if two interfaces share a device, only one is shown. - for iface, dev in pairs(_iface_to_dev) do - if not _seen[dev] then - local dtype = classify_uci(dev, _iface_proto[iface]) - local label = dtype .. ': "' .. iface .. '"' - -- Sort offline entries after active devices - _devices[#_devices + 1] = { name = iface, label = label, sort = "zzz:" .. iface } - _seen[dev] = true - end - end - - table.sort(_devices, function(a, b) return a.sort < b.sort end) - for _, d in ipairs(_devices) do - o:value(d.name, d.label) - end +local iface = api.get_network_devices() +for _, d in ipairs(iface) do + o:value(d.name, d.label) end - o.validate = function(self, value, section) if value == "" or value:match("^[a-zA-Z0-9][a-zA-Z0-9%.%_%-]*$") then return value diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua index 80f4a4ef..f61af239 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua @@ -288,6 +288,7 @@ o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) o:value("", translate("Close(Not use)")) o:value("1", translate("Preproxy Node")) o:value("2", translate("Landing Node")) +o:value("3", translate("Outbound Interface")) local descrStr = "Chained proxy works only with Xray or Sing-box nodes.
" descrStr = descrStr .. "You can only use manual or imported nodes as chained nodes." @@ -305,6 +306,14 @@ o2.description = descrStr o2.template = appname .. "/cbi/nodes_listvalue" o2.group = {} +o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) +o3:depends({ ["chain_proxy"] = "3" }) +o3:value("", translate("All")) +local iface = api.get_network_devices() +for _, d in ipairs(iface) do + o3:value(d.name, d.label) +end + for k, v in pairs(nodes_table) do if (v.type == "Xray" or v.type == "sing-box") and (not v.chain_proxy or v.chain_proxy == "") and v.add_mode ~= "2" then o1:value(v.id, v.remark) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua index e1afc603..363866c1 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua @@ -806,6 +806,7 @@ if not load_shunt_options then if not (load_iface_options or load_balancing_options) then -- Special node cannot be use pre-proxy. o:value("1", translate("Preproxy Node")) + o:value("3", translate("Outbound Interface")) end o:value("2", translate("Landing Node")) @@ -814,6 +815,14 @@ if not load_shunt_options then o1.template = appname .. "/cbi/nodes_listvalue" o1.group = {} + o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface")) + o3:depends({ [_n("chain_proxy")] = "3" }) + o3:value("", translate("All")) + local iface = api.get_network_devices() + for _, d in ipairs(iface) do + o3:value(d.name, d.label) + end + o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy.")) o2:depends({ [_n("chain_proxy")] = "2" }) o2.template = appname .. "/cbi/nodes_listvalue" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index 2f8cade7..ada8094f 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -824,17 +824,25 @@ if not load_shunt_options then if not (load_iface_options or load_urltest_options) then -- Special node cannot be use pre-proxy. o:value("1", translate("Preproxy Node")) + o:value("3", translate("Outbound Interface")) end o:value("2", translate("Landing Node")) - o:depends({ [_n("hysteria2_realms")] = false }) o1 = s:option(ListValue, _n("preproxy_node"), translate("Preproxy Node"), translate("Only support a layer of proxy.")) - o1:depends({ [_n("chain_proxy")] = "1" }) + o1:depends({ [_n("chain_proxy")] = "1", [_n("hysteria2_realms")] = false }) o1.template = appname .. "/cbi/nodes_listvalue" o1.group = {} + o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface")) + o3:depends({ [_n("chain_proxy")] = "3" }) + o3:value("", translate("All")) + local iface = api.get_network_devices() + for _, d in ipairs(iface) do + o3:value(d.name, d.label) + end + o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy.")) - o2:depends({ [_n("chain_proxy")] = "2" }) + o2:depends({ [_n("chain_proxy")] = "2", [_n("hysteria2_realms")] = false }) o2.template = appname .. "/cbi/nodes_listvalue" o2.group = {} diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index a4e1cb6b..ebde80e4 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -1626,3 +1626,83 @@ function parse_realm_uri(uri) end return realm end + +function get_network_devices() + local _sysnet = "/sys/class/net/" + -- Map UCI interface names to their device names and vice versa + local _iface_to_dev = {} + local _dev_to_ifaces = {} + local _iface_proto = {} + uci:foreach("network", "interface", function(sec) + local name = sec[".name"] + if name ~= "loopback" then + _iface_proto[name] = sec.proto + if sec.device then + _iface_to_dev[name] = sec.device + _dev_to_ifaces[sec.device] = _dev_to_ifaces[sec.device] or {} + table.insert(_dev_to_ifaces[sec.device], name) + end + end + end) + -- Classify device type using sysfs attributes + local function classify_sysfs(dev) + if fs.stat(_sysnet .. dev .. "/bridge", "type") == "dir" then + return i18n.translate("Bridge") + elseif fs.stat(_sysnet .. dev .. "/wireless", "type") == "dir" then + return i18n.translate("Wireless Adapter") + elseif dev:match("^tun") or dev:match("^tap") or dev:match("^wg") or dev:match("^ppp") then + return i18n.translate("Tunnel Interface") + else + return i18n.translate("Ethernet Adapter") + end + end + -- Classify offline UCI interfaces by config hints + local function classify_uci(dev_name, proto) + if dev_name and dev_name:match("^br%-") then + return i18n.translate("Bridge") + elseif proto == "wireguard" or proto == "pppoe" or proto == "pptp" or proto == "l2tp" then + return i18n.translate("Tunnel Interface") + else + return i18n.translate("Interface") + end + end + + local _seen = {} + local _devices = {} + -- Active kernel devices from /sys/class/net/ + -- Skip bridge member ports (/master) and DSA master devices (/dsa) + local _iter = fs.dir(_sysnet) + if _iter then + for dev in _iter do + if dev ~= "lo" + and not dev:match("^veth") + and not dev:match("^ifb") + and not dev:match("^gre") + and not dev:match("^sit") + and not dev:match("^ip6tnl") + and not dev:match("^erspan") + and not fs.stat(_sysnet .. dev .. "/master", "type") + and not fs.stat(_sysnet .. dev .. "/dsa", "type") + then + local dtype = classify_sysfs(dev) + local label = dtype .. ': "' .. dev .. '"' + if _dev_to_ifaces[dev] then + label = label .. " (" .. table.concat(_dev_to_ifaces[dev], ", ") .. ")" + end + _devices[#_devices + 1] = { name = dev, label = label, sort = dtype .. ":" .. dev } + _seen[dev] = true + end + end + end + -- UCI interfaces whose device does not currently exist + for iface, dev in pairs(_iface_to_dev) do + if not _seen[dev] then + local dtype = classify_uci(dev, _iface_proto[iface]) + local label = dtype .. ': "' .. iface .. '"' + _devices[#_devices + 1] = { name = iface, label = label, sort = "zzz:" .. iface } + _seen[dev] = true + end + end + table.sort(_devices, function(a, b) return a.sort < b.sort end) + return _devices +end diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index d98e1905..3d428e05 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1438,6 +1438,10 @@ function gen_config(var) end end end + if node.chain_proxy == "3" and node.outbound_iface then + outbound.detour = nil + outbound.bind_interface = node.outbound_iface + end return default_outTag, last_insert_outbound end @@ -2006,6 +2010,10 @@ function gen_config(var) } end + route.default_domain_resolver = { + server = "direct" + } + if not dns.rules then dns.rules = {} end for i, v in pairs(GLOBAL.DNS_SERVER) do @@ -2102,16 +2110,12 @@ function gen_config(var) type = "direct", tag = "direct", routing_mark = 255, - domain_resolver = { - server = "direct", - strategy = "prefer_ipv6" - } }) for index, value in ipairs(config.outbounds) do if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end - if not value.detour and value.server then + if not value.detour and not value.bind_interface and value.server then value.detour = "direct" end if (value.server and not api.datatypes.hostname(value.server)) and not value.realm then diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 7e04f06c..edbe252c 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1206,6 +1206,11 @@ function gen_config(var) end end end + if node.chain_proxy == "3" and node.outbound_iface then + if outbound.streamSettings and outbound.streamSettings.sockopt then + outbound.streamSettings.sockopt.interface = node.outbound_iface + end + end return default_outTag, last_insert_outbound end diff --git a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm index 77f77c5b..86c49b14 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm @@ -874,6 +874,8 @@ table td, .table .td { p = "URLTest"; } else if (protocol == "_shunt") { p = "<%:Shunt%>"; + } else if (protocol == "_iface") { + p = "<%:Interface%>"; } else if (protocol == "vmess") { p = "VMess"; } else if (protocol == "vless") { diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po index 008e796a..aa23fe92 100644 --- a/luci-app-passwall/po/zh-cn/passwall.po +++ b/luci-app-passwall/po/zh-cn/passwall.po @@ -1966,6 +1966,9 @@ msgstr "仅支持 Xray 或 Sing-box 类型节点。" msgid "Chain Proxy" msgstr "链式代理" +msgid "Outbound Interface" +msgstr "出站接口" + msgid "Landing Node" msgstr "落地节点" diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index 52f0b0d2..c3a7cf5f 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -1883,7 +1883,7 @@ local function update_node(manual) local list = v["list"] local sub_cfg = v["sub_cfg"] local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy - local preproxy_node_group, to_node_group, chain_node_type = "", "", "" + local preproxy_node_group, to_node_group, outbound_iface_group, chain_node_type = "", "", "", "" -- Subscription Group Chain Agent local function valid_chain_node(node) if not node then return "" end @@ -1904,6 +1904,8 @@ local function update_node(manual) domain_strategy = (sub_cfg.domain_strategy and map[sub_cfg.domain_strategy]) and sub_cfg.domain_strategy or nil preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or "" to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or "" + outbound_iface_group = (sub_cfg.chain_proxy == "3") and sub_cfg.outbound_iface or "" + chain_node_type = (outbound_iface_group ~= "") and "iface" or chain_node_type end for _, vv in ipairs(list) do local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) @@ -1941,6 +1943,9 @@ local function update_node(manual) elseif to_node_group ~= "" then uci:set(appname, cfgid, "chain_proxy", "2") uci:set(appname, cfgid, "to_node", to_node_group) + elseif outbound_iface_group ~= "" then + uci:set(appname, cfgid, "chain_proxy", "3") + uci:set(appname, cfgid, "outbound_iface", outbound_iface_group) end end end diff --git a/luci-app-xray/Makefile b/luci-app-xray/Makefile index b7aaffa3..c43501fb 100644 --- a/luci-app-xray/Makefile +++ b/luci-app-xray/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-xray PKG_VERSION:=3.7.1 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_LICENSE:=MPLv2 PKG_LICENSE_FILES:=LICENSE diff --git a/luci-app-xray/root/usr/share/xray/firewall_include.ut b/luci-app-xray/root/usr/share/xray/firewall_include.ut index 9daac0e3..3e17ec7c 100644 --- a/luci-app-xray/root/usr/share/xray/firewall_include.ut +++ b/luci-app-xray/root/usr/share/xray/firewall_include.ut @@ -616,3 +616,21 @@ chain tp_spec_lan_fw { {{ counter }} mark set 0x000000fb accept } + +{% if (length(fakedns) > 0): %} +{% if (general.fakedns_ping_response_prerouting == "1"): %} + chain xray_fakedns_ping_prerouting { + type nat hook prerouting priority dstnat {{ firewall_priority }}; policy accept; + ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} icmp type echo-request {{ counter }} redirect comment "Xray FakeDNS IPv4 Pool ICMP" + ip6 daddr {{ general.pool_v6 || "fc00::/18" }} icmpv6 type echo-request {{ counter }} redirect comment "Xray FakeDNS IPv6 Pool ICMPv6" + } +{% endif %} + +{% if (general.fakedns_ping_response_output == "1"): %} + chain xray_fakedns_ping_output { + type nat hook output priority dstnat {{ firewall_priority }}; policy accept; + ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} icmp type echo-request {{ counter }} redirect comment "Xray FakeDNS IPv4 Pool ICMP" + ip6 daddr {{ general.pool_v6 || "fc00::/18" }} icmpv6 type echo-request {{ counter }} redirect comment "Xray FakeDNS IPv6 Pool ICMPv6" + } +{% endif %} +{% endif %} diff --git a/luci-app-xray/root/www/luci-static/resources/view/xray/preview.js b/luci-app-xray/root/www/luci-static/resources/view/xray/preview.js index af84ca76..1283d908 100644 --- a/luci-app-xray/root/www/luci-static/resources/view/xray/preview.js +++ b/luci-app-xray/root/www/luci-static/resources/view/xray/preview.js @@ -63,6 +63,9 @@ return view.extend({ let ttl_override_bypass_ports = s.taboption('firewall', form.DynamicList, 'ttl_override_bypass_ports', _('Ports to bypass TTL override'), _("Do not override TTL for packets with these destination TCP / UDP ports.")); ttl_override_bypass_ports.datatype = 'port'; + s.taboption('firewall', form.Flag, 'fakedns_ping_response_prerouting', _('FakeDNS Ping (LAN)'), _('Respond to ICMP echo requests from LAN clients for FakeDNS address pools.')); + s.taboption('firewall', form.Flag, 'fakedns_ping_response_output', _('FakeDNS Ping (Local)'), _('Respond to ICMP echo requests from this router for FakeDNS address pools.')); + s.tab("sniffing", _("Sniffing")); s.taboption('sniffing', form.Flag, 'tproxy_sniffing', _('Enable Sniffing'), _('Route requests according to domain settings in "DNS Settings" tab in core settings. Deprecated; use FakeDNS instead.')); diff --git a/mihomo-alpha/Makefile b/mihomo-alpha/Makefile index e771512b..2239fa93 100644 --- a/mihomo-alpha/Makefile +++ b/mihomo-alpha/Makefile @@ -1,14 +1,14 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mihomo-alpha -PKG_VERSION:=2026.05.16 -PKG_RELEASE:=8 +PKG_VERSION:=2026.05.19 +PKG_RELEASE:=9 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git -PKG_SOURCE_VERSION:=e9e23c4e98c34b158953f1414b317f25f3e3aa1e +PKG_SOURCE_VERSION:=a52a27ed563970b79e50ced79f15100abf0bcc85 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL3.0+ @@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_PARALLEL:=1 PKG_BUILD_FLAGS:=no-mips16 -PKG_BUILD_VERSION:=alpha-e9e23c4e +PKG_BUILD_VERSION:=alpha-a52a27ed PKG_BUILD_TIME:=$(shell date -u -Iseconds) GO_PKG:=github.com/metacubex/mihomo diff --git a/modeminfo/Makefile b/modeminfo/Makefile index 66494344..3eaeca19 100644 --- a/modeminfo/Makefile +++ b/modeminfo/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=modeminfo PKG_VERSION:=0.4.6 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_MAINTAINER:=Konstantine Shevlakov include $(INCLUDE_DIR)/package.mk diff --git a/modeminfo/root/usr/share/modeminfo/scripts/modeminfo b/modeminfo/root/usr/share/modeminfo/scripts/modeminfo index 2308cc48..a136eb47 100644 --- a/modeminfo/root/usr/share/modeminfo/scripts/modeminfo +++ b/modeminfo/root/usr/share/modeminfo/scripts/modeminfo @@ -131,7 +131,7 @@ function generic_data(){ modes["6"] = "HSPA" modes["7"] = modes["13"] = "LTE" - prefix = substr($4, 1, 1) + prefix = substr($4, 1, 2) print (prefix in modes ? modes[prefix] : "--") }') }